diff -Nru python-zunclient-0.2.0/AUTHORS python-zunclient-0.4.0/AUTHORS --- python-zunclient-0.2.0/AUTHORS 2017-04-24 13:29:54.000000000 +0000 +++ python-zunclient-0.4.0/AUTHORS 2017-07-28 16:14:37.000000000 +0000 @@ -1,14 +1,19 @@ +00129207 AleptNmarata Andreas Jaeger Bharath Thiruveedula +Bin Zhou Cao Xuan Hoang Deepak Eli Qiao Feng Shengqin +Hangdong Zhang Hongbin Lu Jeremy Liu +Kevin Zhao Kevin Zhao Lei Li +M V P Nitesh Madhuri Kumari Madhuri Kumari Michael Lekkas @@ -17,10 +22,15 @@ Pradeep Kumar Singh Ranler Cao Sharat Sharma +ShunliZhou Tony Xu +Tovin Seven Xianghui Zeng avnish bhavani +bhavani.cr +chenlx +haobing1 miaohb prameswar ricolin diff -Nru python-zunclient-0.2.0/ChangeLog python-zunclient-0.4.0/ChangeLog --- python-zunclient-0.2.0/ChangeLog 2017-04-24 13:29:54.000000000 +0000 +++ python-zunclient-0.4.0/ChangeLog 2017-07-28 16:14:37.000000000 +0000 @@ -1,6 +1,73 @@ CHANGES ======= +0.4.0 +----- + +* Updated from global requirements +* Update the documentation link for doc migration +* Add warning-is-error in setup.cfg +* [doc-migration] Move documents to their respective folders +* Fixed the api version issue on OSC plugin +* Add the parameter interactive in ExecContainer +* Add uts for parse\_command + +0.3.0 +----- + +* Remove deprecated parameter command in RunContainer +* Re-enable osc tempest tests +* Updated from global requirements +* Rename parameter '--nets' to '--net' +* Fix the typo that missing blank between words +* Enhance api version support in CLI +* Support all\_tenants in show and delete +* Fixed wrap from taking negative values +* Support api micro version in OSC +* Add network options to zunclient +* Updated from global requirements +* switch to openstackdocstheme +* Remove unused code from zunclient/common/apiclient +* Updated from global requirements +* Don't run zun server tempest tests in gate +* Add security groups to container create/run +* Upgrade from docker-py to docker +* Make --profile load from environment variables +* Add scheduler hints for zunclient +* Updated from global requirements +* Return image ID in container commit +* OSC: return columns instead of using print\_dict +* Zunclient should escape special character +* Set concurrency to 1 for OSC tempest tests +* Updated from global requirements +* OSC support service api +* Fix spelling mistake in osc client +* Fix some spelling mistakes +* Client support for service force-down +* Add image-list and pull to osc +* Revert "Add scheduler hint for zunclient" +* Implement container snapshot +* Add scheduler hint for zunclient +* Updated from global requirements +* Compile stats on server side +* Improve style of zun pull +* Client support for service-enable/disable +* Replace assertRaisesRegexp with assertRaisesRegex +* Make client work with websocket proxy +* Remove the code that add uuid to websocket header +* Replace the magic number with const +* Client support for display snapshot of zun stats +* Skip run OSC tests on unit tests +* Updated from global requirements +* Updated from global requirements +* Client support for service delete +* Revert file mode from 0755 to 0644 +* Support interactive mode for exec command +* Optimize the link address +* Fix confusing error message on interactive run +* Introduce API micro version +* Add container uuid to the websocket connection header + 0.2.0 ----- diff -Nru python-zunclient-0.2.0/CONTRIBUTING.rst python-zunclient-0.4.0/CONTRIBUTING.rst --- python-zunclient-0.2.0/CONTRIBUTING.rst 2017-04-24 13:28:10.000000000 +0000 +++ python-zunclient-0.4.0/CONTRIBUTING.rst 2017-07-28 16:11:40.000000000 +0000 @@ -1,14 +1,14 @@ If you would like to contribute to the development of OpenStack, you must follow the steps in this page: - http://docs.openstack.org/infra/manual/developers.html + https://docs.openstack.org/infra/manual/developers.html If you already have a good understanding of how the system works and your OpenStack accounts are set up, you can skip to the development workflow section of this documentation to learn how changes to OpenStack should be submitted for review via the Gerrit tool: - http://docs.openstack.org/infra/manual/developers.html#development-workflow + https://docs.openstack.org/infra/manual/developers.html#development-workflow Pull requests submitted through GitHub will be ignored. diff -Nru python-zunclient-0.2.0/debian/changelog python-zunclient-0.4.0/debian/changelog --- python-zunclient-0.2.0/debian/changelog 2017-06-05 14:10:09.000000000 +0000 +++ python-zunclient-0.4.0/debian/changelog 2017-08-15 19:42:59.000000000 +0000 @@ -1,3 +1,16 @@ +python-zunclient (0.4.0-0ubuntu1) artful; urgency=medium + + * d/watch: Get tarball from tarballs.openstack.org. + * New upstream release. + * d/control: Align (Build-)Depends with upstream. + * d/p/drop-openstackdoctheme.patch: Temporarily drop openstackdocstheme + sphinx extension until sphinx>=1.6.2 is available. + * d/p/docker-1.9.0.patch: Revert to using python 1.9.0 for arftul until + new versions of python-docker and docker-compose are available. + * d/control: Add python(3)-setuptools to BDs. + + -- Corey Bryant Tue, 15 Aug 2017 15:42:59 -0400 + python-zunclient (0.2.0-0ubuntu1) artful; urgency=low * Initial package. diff -Nru python-zunclient-0.2.0/debian/control python-zunclient-0.4.0/debian/control --- python-zunclient-0.2.0/debian/control 2017-06-05 14:10:09.000000000 +0000 +++ python-zunclient-0.4.0/debian/control 2017-08-15 19:42:59.000000000 +0000 @@ -7,37 +7,43 @@ openstack-pkg-tools, python-all (>= 2.6.6-3), python-pbr (>= 2.0.0), + python-setuptools, python3-all, python3-pbr (>= 2.0.0), + python3-setuptools, Build-Depends-Indep: python-ddt (>= 1.0.1), - python-keystoneauth1 (>= 2.18.0), + python-docker (>= 1.9.0), + python-keystoneauth1 (>= 3.1.0), python-openstackclient (>= 3.3.0), + python-openstackdocstheme (>= 1.16.0), python-os-testr (>= 0.8.0), - python-osc-lib (>= 1.2.0), + python-osc-lib (>= 1.7.0), python-oslo.i18n (>= 2.1.0), python-oslo.utils (>= 3.20.0), python-oslotest (>= 1.10.0), python-osprofiler (>= 1.4.0), python-prettytable (>= 0.7.1), python-subunit (>= 0.0.18), - python-tempest (>= 1:14.0.0), + python-tempest (>= 1:16.1.0), python-testrepository (>= 0.0.18), python-testresources (>= 0.2.4), python-testscenarios (>= 0.4), python-testtools (>= 1.4.0), python-websocket (>= 0.32.0), python3-ddt (>= 1.0.1), - python3-keystoneauth1 (>= 2.18.0), + python3-docker (>= 1.9.0), + python3-keystoneauth1 (>= 3.1.0), python3-openstackclient (>= 3.3.0), + python3-openstackdocstheme (>= 1.16.0), python3-os-testr (>= 0.8.0), - python3-osc-lib (>= 1.2.0), + python3-osc-lib (>= 1.7.0), python3-oslo.i18n (>= 2.1.0), python3-oslo.utils (>= 3.20.0), python3-oslotest (>= 1.10.0), python3-osprofiler (>= 1.4.0), python3-prettytable (>= 0.7.1), python3-subunit (>= 0.0.18), - python3-tempest (>= 1:14.0.0), + python3-tempest (>= 1:16.1.0), python3-testrepository (>= 0.0.18), python3-testresources (>= 0.2.4), python3-testscenarios (>= 0.4), diff -Nru python-zunclient-0.2.0/debian/patches/docker-1.9.0.patch python-zunclient-0.4.0/debian/patches/docker-1.9.0.patch --- python-zunclient-0.2.0/debian/patches/docker-1.9.0.patch 1970-01-01 00:00:00.000000000 +0000 +++ python-zunclient-0.4.0/debian/patches/docker-1.9.0.patch 2017-08-15 19:42:59.000000000 +0000 @@ -0,0 +1,26 @@ +Description: Revert to using python 1.9.0 for arftul. python-docker + and docker-compose are currently at 1.9.0 so we need to stay at + that version for now. +Author: Corey Bryant +Forwarded: no +Last-Update: 2017-08-15 + +--- a/requirements.txt ++++ b/requirements.txt +@@ -10,4 +10,4 @@ + oslo.i18n!=3.15.2,>=2.1.0 # Apache-2.0 + oslo.utils>=3.20.0 # Apache-2.0 + websocket-client>=0.32.0 # LGPLv2+ +-docker>=2.0.0 # Apache-2.0 ++docker-py>=1.9.0 # Apache-2.0 +--- a/zunclient/common/websocketclient/websocketclient.py ++++ b/zunclient/common/websocketclient/websocketclient.py +@@ -280,7 +280,7 @@ + + def connect(self): + try: +- client = docker.APIClient(base_url=self.url) ++ client = docker.Client(base_url=self.url) + self.socket = client.exec_start(self.exec_id, socket=True, + tty=True) + print('connected to container "%s"' % self.id) diff -Nru python-zunclient-0.2.0/debian/patches/drop-openstackdoctheme.patch python-zunclient-0.4.0/debian/patches/drop-openstackdoctheme.patch --- python-zunclient-0.2.0/debian/patches/drop-openstackdoctheme.patch 1970-01-01 00:00:00.000000000 +0000 +++ python-zunclient-0.4.0/debian/patches/drop-openstackdoctheme.patch 2017-08-15 19:42:59.000000000 +0000 @@ -0,0 +1,26 @@ +Description: Temporarily drop openstackdocstheme sphinx extension + until sphinx>=1.6.2 is available. +Author: Corey Bryant +Forwarded: no +Last-Update: 2017-08-15 + +--- a/doc/source/conf.py ++++ b/doc/source/conf.py +@@ -22,7 +22,7 @@ + extensions = [ + 'sphinx.ext.autodoc', + #'sphinx.ext.intersphinx', +- 'openstackdocstheme', ++ #'openstackdocstheme', + ] + + # openstackdocstheme options +@@ -60,7 +60,7 @@ + # The theme to use for HTML and HTML Help pages. Major themes that come with + # Sphinx are currently 'default' and 'sphinxdoc'. + +-html_theme = 'openstackdocs' ++# html_theme = 'openstackdocs' + + # Output file base name for HTML help builder. + htmlhelp_basename = 'zunclientdoc' diff -Nru python-zunclient-0.2.0/debian/patches/series python-zunclient-0.4.0/debian/patches/series --- python-zunclient-0.2.0/debian/patches/series 1970-01-01 00:00:00.000000000 +0000 +++ python-zunclient-0.4.0/debian/patches/series 2017-08-15 19:42:59.000000000 +0000 @@ -0,0 +1,2 @@ +docker-1.9.0.patch +drop-openstackdoctheme.patch diff -Nru python-zunclient-0.2.0/debian/watch python-zunclient-0.4.0/debian/watch --- python-zunclient-0.2.0/debian/watch 2017-06-05 14:10:09.000000000 +0000 +++ python-zunclient-0.4.0/debian/watch 2017-08-15 19:42:59.000000000 +0000 @@ -1,4 +1,3 @@ -# please also check http://pypi.debian.net/python-zunclient/watch version=3 -opts=uversionmangle=s/(rc|a|b|c)/~$1/ \ -http://pypi.debian.net/python-zunclient/python-zunclient-(.+)\.(?:zip|tgz|tbz|txz|(?:tar\.(?:gz|bz2|xz))) \ No newline at end of file +opts="uversionmangle=s/\.(b|rc)/~$1/" \ + http://tarballs.openstack.org/python-zunclient/ python-zunclient-(\d.*)\.tar\.gz diff -Nru python-zunclient-0.2.0/doc/source/conf.py python-zunclient-0.4.0/doc/source/conf.py --- python-zunclient-0.2.0/doc/source/conf.py 2017-04-24 13:28:10.000000000 +0000 +++ python-zunclient-0.4.0/doc/source/conf.py 2017-07-28 16:11:40.000000000 +0000 @@ -22,9 +22,15 @@ extensions = [ 'sphinx.ext.autodoc', #'sphinx.ext.intersphinx', - 'oslosphinx', + 'openstackdocstheme', ] +# openstackdocstheme options +repository_name = 'openstack/python-zunclient' +bug_project = 'python-zunclient' +bug_tag = 'doc' +html_last_updated_fmt = '%Y-%m-%d %H:%M' + # 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 @@ -53,22 +59,8 @@ # The theme to use for HTML and HTML Help pages. Major themes that come with # Sphinx are currently 'default' and 'sphinxdoc'. -# html_theme_path = ["."] -# html_theme = '_theme' -# html_static_path = ['static'] -# Output file base name for HTML help builder. -htmlhelp_basename = '%sdoc' % project +html_theme = 'openstackdocs' -# Grouping the document tree into LaTeX files. List of tuples -# (source start file, target name, title, author, documentclass -# [howto/manual]). -latex_documents = [ - ('index', - '%s.tex' % project, - u'%s Documentation' % project, - u'OpenStack Foundation', 'manual'), -] - -# Example configuration for intersphinx: refer to the Python standard library. -#intersphinx_mapping = {'http://docs.python.org/': None} +# Output file base name for HTML help builder. +htmlhelp_basename = 'zunclientdoc' diff -Nru python-zunclient-0.2.0/doc/source/contributing.rst python-zunclient-0.4.0/doc/source/contributing.rst --- python-zunclient-0.2.0/doc/source/contributing.rst 2017-04-24 13:28:10.000000000 +0000 +++ python-zunclient-0.4.0/doc/source/contributing.rst 1970-01-01 00:00:00.000000000 +0000 @@ -1 +0,0 @@ -.. include:: ../../CONTRIBUTING.rst \ No newline at end of file diff -Nru python-zunclient-0.2.0/doc/source/contributor/index.rst python-zunclient-0.4.0/doc/source/contributor/index.rst --- python-zunclient-0.2.0/doc/source/contributor/index.rst 1970-01-01 00:00:00.000000000 +0000 +++ python-zunclient-0.4.0/doc/source/contributor/index.rst 2017-07-28 16:11:40.000000000 +0000 @@ -0,0 +1,5 @@ +=================== +Contributor's Guide +=================== + +.. include:: ../../../CONTRIBUTING.rst diff -Nru python-zunclient-0.2.0/doc/source/index.rst python-zunclient-0.4.0/doc/source/index.rst --- python-zunclient-0.2.0/doc/source/index.rst 2017-04-24 13:28:10.000000000 +0000 +++ python-zunclient-0.4.0/doc/source/index.rst 2017-07-28 16:11:40.000000000 +0000 @@ -7,9 +7,8 @@ :maxdepth: 2 readme - installation - usage - contributing + install/index + contributor/index Indices and tables ================== diff -Nru python-zunclient-0.2.0/doc/source/install/index.rst python-zunclient-0.4.0/doc/source/install/index.rst --- python-zunclient-0.2.0/doc/source/install/index.rst 1970-01-01 00:00:00.000000000 +0000 +++ python-zunclient-0.4.0/doc/source/install/index.rst 2017-07-28 16:11:40.000000000 +0000 @@ -0,0 +1,11 @@ +Installation Guide +================== + +At the command line:: + + $ pip install python-zunclient + +Or, if you have virtualenvwrapper installed:: + + $ mkvirtualenv python-zunclient + $ pip install python-zunclient diff -Nru python-zunclient-0.2.0/doc/source/installation.rst python-zunclient-0.4.0/doc/source/installation.rst --- python-zunclient-0.2.0/doc/source/installation.rst 2017-04-24 13:28:10.000000000 +0000 +++ python-zunclient-0.4.0/doc/source/installation.rst 1970-01-01 00:00:00.000000000 +0000 @@ -1,12 +0,0 @@ -============ -Installation -============ - -At the command line:: - - $ pip install python-zunclient - -Or, if you have virtualenvwrapper installed:: - - $ mkvirtualenv python-zunclient - $ pip install python-zunclient diff -Nru python-zunclient-0.2.0/doc/source/readme.rst python-zunclient-0.4.0/doc/source/readme.rst --- python-zunclient-0.2.0/doc/source/readme.rst 2017-04-24 13:28:10.000000000 +0000 +++ python-zunclient-0.4.0/doc/source/readme.rst 2017-07-28 16:11:40.000000000 +0000 @@ -1 +1 @@ -.. include:: ../README.rst \ No newline at end of file +.. include:: ../../README.rst diff -Nru python-zunclient-0.2.0/doc/source/usage.rst python-zunclient-0.4.0/doc/source/usage.rst --- python-zunclient-0.2.0/doc/source/usage.rst 2017-04-24 13:28:10.000000000 +0000 +++ python-zunclient-0.4.0/doc/source/usage.rst 1970-01-01 00:00:00.000000000 +0000 @@ -1,7 +0,0 @@ -======== -Usage -======== - -To use python-zunclient in a project:: - - import zunclient diff -Nru python-zunclient-0.2.0/HACKING.rst python-zunclient-0.4.0/HACKING.rst --- python-zunclient-0.2.0/HACKING.rst 2017-04-24 13:28:10.000000000 +0000 +++ python-zunclient-0.4.0/HACKING.rst 2017-07-28 16:11:40.000000000 +0000 @@ -1,4 +1,4 @@ python-zunclient Style Commandments =============================================== -Read the OpenStack Style Commandments http://docs.openstack.org/developer/hacking/ +Read the OpenStack Style Commandments https://docs.openstack.org/hacking/latest/ diff -Nru python-zunclient-0.2.0/PKG-INFO python-zunclient-0.4.0/PKG-INFO --- python-zunclient-0.2.0/PKG-INFO 2017-04-24 13:29:55.000000000 +0000 +++ python-zunclient-0.4.0/PKG-INFO 2017-07-28 16:14:39.000000000 +0000 @@ -1,8 +1,8 @@ Metadata-Version: 1.1 Name: python-zunclient -Version: 0.2.0 +Version: 0.4.0 Summary: Client Library for Zun -Home-page: https://docs.openstack.org/developer/zun/ +Home-page: https://docs.openstack.org/zun/latest/ Author: OpenStack Author-email: openstack-dev@lists.openstack.org License: UNKNOWN @@ -17,8 +17,8 @@ Note that this is a hard requirement. * Free software: Apache license - * Documentation: http://docs.openstack.org/developer/python-zunclient - * Source: http://git.openstack.org/cgit/openstack/python-zunclient + * Documentation: https://docs.openstack.org/python-zunclient/latest/ + * Source: https://git.openstack.org/cgit/openstack/python-zunclient * Bugs: http://bugs.launchpad.net/python-zunclient Features diff -Nru python-zunclient-0.2.0/python_zunclient.egg-info/entry_points.txt python-zunclient-0.4.0/python_zunclient.egg-info/entry_points.txt --- python-zunclient-0.2.0/python_zunclient.egg-info/entry_points.txt 2017-04-24 13:29:54.000000000 +0000 +++ python-zunclient-0.4.0/python_zunclient.egg-info/entry_points.txt 2017-07-28 16:14:37.000000000 +0000 @@ -6,10 +6,13 @@ [openstack.container.v1] appcontainer_attach = zunclient.osc.v1.containers:AttachContainer +appcontainer_commit = zunclient.osc.v1.containers:CommitContainer appcontainer_cp = zunclient.osc.v1.containers:CopyContainer appcontainer_create = zunclient.osc.v1.containers:CreateContainer appcontainer_delete = zunclient.osc.v1.containers:DeleteContainer appcontainer_exec = zunclient.osc.v1.containers:ExecContainer +appcontainer_image_list = zunclient.osc.v1.images:ListImage +appcontainer_image_pull = zunclient.osc.v1.images:PullImage appcontainer_kill = zunclient.osc.v1.containers:KillContainer appcontainer_list = zunclient.osc.v1.containers:ListContainer appcontainer_logs = zunclient.osc.v1.containers:LogsContainer @@ -17,8 +20,14 @@ appcontainer_rename = zunclient.osc.v1.containers:RenameContainer appcontainer_restart = zunclient.osc.v1.containers:RestartContainer appcontainer_run = zunclient.osc.v1.containers:RunContainer +appcontainer_service_delete = zunclient.osc.v1.services:DeleteService +appcontainer_service_disable = zunclient.osc.v1.services:DisableService +appcontainer_service_enable = zunclient.osc.v1.services:EnableService +appcontainer_service_forcedown = zunclient.osc.v1.services:ForceDownService +appcontainer_service_list = zunclient.osc.v1.services:ListService appcontainer_show = zunclient.osc.v1.containers:ShowContainer appcontainer_start = zunclient.osc.v1.containers:StartContainer +appcontainer_stats = zunclient.osc.v1.containers:StatsContainer appcontainer_stop = zunclient.osc.v1.containers:StopContainer appcontainer_top = zunclient.osc.v1.containers:TopContainer appcontainer_unpause = zunclient.osc.v1.containers:UnpauseContainer diff -Nru python-zunclient-0.2.0/python_zunclient.egg-info/pbr.json python-zunclient-0.4.0/python_zunclient.egg-info/pbr.json --- python-zunclient-0.2.0/python_zunclient.egg-info/pbr.json 2017-04-24 13:29:54.000000000 +0000 +++ python-zunclient-0.4.0/python_zunclient.egg-info/pbr.json 2017-07-28 16:14:38.000000000 +0000 @@ -1 +1 @@ -{"git_version": "fa61ba0", "is_release": true} \ No newline at end of file +{"git_version": "d4ea2de", "is_release": true} \ No newline at end of file diff -Nru python-zunclient-0.2.0/python_zunclient.egg-info/PKG-INFO python-zunclient-0.4.0/python_zunclient.egg-info/PKG-INFO --- python-zunclient-0.2.0/python_zunclient.egg-info/PKG-INFO 2017-04-24 13:29:54.000000000 +0000 +++ python-zunclient-0.4.0/python_zunclient.egg-info/PKG-INFO 2017-07-28 16:14:37.000000000 +0000 @@ -1,8 +1,8 @@ Metadata-Version: 1.1 Name: python-zunclient -Version: 0.2.0 +Version: 0.4.0 Summary: Client Library for Zun -Home-page: https://docs.openstack.org/developer/zun/ +Home-page: https://docs.openstack.org/zun/latest/ Author: OpenStack Author-email: openstack-dev@lists.openstack.org License: UNKNOWN @@ -17,8 +17,8 @@ Note that this is a hard requirement. * Free software: Apache license - * Documentation: http://docs.openstack.org/developer/python-zunclient - * Source: http://git.openstack.org/cgit/openstack/python-zunclient + * Documentation: https://docs.openstack.org/python-zunclient/latest/ + * Source: https://git.openstack.org/cgit/openstack/python-zunclient * Bugs: http://bugs.launchpad.net/python-zunclient Features diff -Nru python-zunclient-0.2.0/python_zunclient.egg-info/requires.txt python-zunclient-0.4.0/python_zunclient.egg-info/requires.txt --- python-zunclient-0.2.0/python_zunclient.egg-info/requires.txt 2017-04-24 13:29:54.000000000 +0000 +++ python-zunclient-0.4.0/python_zunclient.egg-info/requires.txt 2017-07-28 16:14:37.000000000 +0000 @@ -1,8 +1,9 @@ pbr!=2.1.0,>=2.0.0 PrettyTable<0.8,>=0.7.1 -python-openstackclient>=3.3.0 -keystoneauth1>=2.18.0 -osc-lib>=1.2.0 -oslo.i18n>=2.1.0 +python-openstackclient!=3.10.0,>=3.3.0 +keystoneauth1>=3.1.0 +osc-lib>=1.7.0 +oslo.i18n!=3.15.2,>=2.1.0 oslo.utils>=3.20.0 websocket-client>=0.32.0 +docker>=2.0.0 diff -Nru python-zunclient-0.2.0/python_zunclient.egg-info/SOURCES.txt python-zunclient-0.4.0/python_zunclient.egg-info/SOURCES.txt --- python-zunclient-0.2.0/python_zunclient.egg-info/SOURCES.txt 2017-04-24 13:29:55.000000000 +0000 +++ python-zunclient-0.4.0/python_zunclient.egg-info/SOURCES.txt 2017-07-28 16:14:39.000000000 +0000 @@ -14,11 +14,10 @@ test-requirements.txt tox.ini doc/source/conf.py -doc/source/contributing.rst doc/source/index.rst -doc/source/installation.rst doc/source/readme.rst -doc/source/usage.rst +doc/source/contributor/index.rst +doc/source/install/index.rst python_zunclient.egg-info/PKG-INFO python_zunclient.egg-info/SOURCES.txt python_zunclient.egg-info/dependency_links.txt @@ -41,6 +40,7 @@ tools/run_functional.sh tools/zun.bash_completion zunclient/__init__.py +zunclient/api_versions.py zunclient/client.py zunclient/exceptions.py zunclient/i18n.py @@ -62,8 +62,9 @@ zunclient/osc/plugin.py zunclient/osc/v1/__init__.py zunclient/osc/v1/containers.py +zunclient/osc/v1/images.py +zunclient/osc/v1/services.py zunclient/tests/__init__.py -zunclient/tests/test_websocketclient.py zunclient/tests/functional/__init__.py zunclient/tests/functional/base.py zunclient/tests/functional/hooks/__init__.py @@ -74,8 +75,10 @@ zunclient/tests/functional/osc/v1/test_container.py zunclient/tests/unit/__init__.py zunclient/tests/unit/base.py +zunclient/tests/unit/test_api_versions.py zunclient/tests/unit/test_client.py zunclient/tests/unit/test_shell.py +zunclient/tests/unit/test_websocketclient.py zunclient/tests/unit/utils.py zunclient/tests/unit/common/__init__.py zunclient/tests/unit/common/test_httpclient.py diff -Nru python-zunclient-0.2.0/README.rst python-zunclient-0.4.0/README.rst --- python-zunclient-0.2.0/README.rst 2017-04-24 13:28:10.000000000 +0000 +++ python-zunclient-0.4.0/README.rst 2017-07-28 16:11:40.000000000 +0000 @@ -9,8 +9,8 @@ Note that this is a hard requirement. * Free software: Apache license -* Documentation: http://docs.openstack.org/developer/python-zunclient -* Source: http://git.openstack.org/cgit/openstack/python-zunclient +* Documentation: https://docs.openstack.org/python-zunclient/latest/ +* Source: https://git.openstack.org/cgit/openstack/python-zunclient * Bugs: http://bugs.launchpad.net/python-zunclient Features diff -Nru python-zunclient-0.2.0/requirements.txt python-zunclient-0.4.0/requirements.txt --- python-zunclient-0.2.0/requirements.txt 2017-04-24 13:28:10.000000000 +0000 +++ python-zunclient-0.4.0/requirements.txt 2017-07-28 16:11:40.000000000 +0000 @@ -4,9 +4,10 @@ pbr!=2.1.0,>=2.0.0 # Apache-2.0 PrettyTable<0.8,>=0.7.1 # BSD -python-openstackclient>=3.3.0 # Apache-2.0 -keystoneauth1>=2.18.0 # Apache-2.0 -osc-lib>=1.2.0 # Apache-2.0 -oslo.i18n>=2.1.0 # Apache-2.0 +python-openstackclient!=3.10.0,>=3.3.0 # Apache-2.0 +keystoneauth1>=3.1.0 # Apache-2.0 +osc-lib>=1.7.0 # Apache-2.0 +oslo.i18n!=3.15.2,>=2.1.0 # Apache-2.0 oslo.utils>=3.20.0 # Apache-2.0 websocket-client>=0.32.0 # LGPLv2+ +docker>=2.0.0 # Apache-2.0 diff -Nru python-zunclient-0.2.0/setup.cfg python-zunclient-0.4.0/setup.cfg --- python-zunclient-0.2.0/setup.cfg 2017-04-24 13:29:55.000000000 +0000 +++ python-zunclient-0.4.0/setup.cfg 2017-07-28 16:14:39.000000000 +0000 @@ -5,7 +5,7 @@ README.rst author = OpenStack author-email = openstack-dev@lists.openstack.org -home-page = https://docs.openstack.org/developer/zun/ +home-page = https://docs.openstack.org/zun/latest/ classifier = Environment :: OpenStack Intended Audience :: Information Technology @@ -28,6 +28,11 @@ openstack.cli.extension = container = zunclient.osc.plugin openstack.container.v1 = + appcontainer_service_list = zunclient.osc.v1.services:ListService + appcontainer_service_delete = zunclient.osc.v1.services:DeleteService + appcontainer_service_enable = zunclient.osc.v1.services:EnableService + appcontainer_service_disable = zunclient.osc.v1.services:DisableService + appcontainer_service_forcedown = zunclient.osc.v1.services:ForceDownService appcontainer_create = zunclient.osc.v1.containers:CreateContainer appcontainer_show = zunclient.osc.v1.containers:ShowContainer appcontainer_list = zunclient.osc.v1.containers:ListContainer @@ -46,11 +51,16 @@ appcontainer_update = zunclient.osc.v1.containers:UpdateContainer appcontainer_attach = zunclient.osc.v1.containers:AttachContainer appcontainer_cp = zunclient.osc.v1.containers:CopyContainer + appcontainer_stats = zunclient.osc.v1.containers:StatsContainer + appcontainer_commit = zunclient.osc.v1.containers:CommitContainer + appcontainer_image_list = zunclient.osc.v1.images:ListImage + appcontainer_image_pull = zunclient.osc.v1.images:PullImage [build_sphinx] source-dir = doc/source build-dir = doc/build all_files = 1 +warning-is-error = 1 [upload_sphinx] upload-dir = doc/build/html diff -Nru python-zunclient-0.2.0/.testr.conf python-zunclient-0.4.0/.testr.conf --- python-zunclient-0.2.0/.testr.conf 2017-04-24 13:28:10.000000000 +0000 +++ python-zunclient-0.4.0/.testr.conf 2017-07-28 16:11:40.000000000 +0000 @@ -2,6 +2,6 @@ test_command=OS_STDOUT_CAPTURE=${OS_STDOUT_CAPTURE:-1} \ OS_STDERR_CAPTURE=${OS_STDERR_CAPTURE:-1} \ OS_TEST_TIMEOUT=${OS_TEST_TIMEOUT:-60} \ - ${PYTHON:-python} -m subunit.run discover -t ./ . $LISTOPT $IDOPTION + ${PYTHON:-python} -m subunit.run discover -t ./ ${OS_TEST_PATH:-./zunclient/tests/unit} $LISTOPT $IDOPTION test_id_option=--load-list $IDFILE test_list_option=--list diff -Nru python-zunclient-0.2.0/test-requirements.txt python-zunclient-0.4.0/test-requirements.txt --- python-zunclient-0.2.0/test-requirements.txt 2017-04-24 13:28:10.000000000 +0000 +++ python-zunclient-0.4.0/test-requirements.txt 2017-07-28 16:11:40.000000000 +0000 @@ -3,21 +3,21 @@ # process, which may cause wedges in the gate later. bandit>=1.1.0 # Apache-2.0 -coverage>=4.0 # Apache-2.0 +coverage!=4.4,>=4.0 # Apache-2.0 doc8 # Apache-2.0 ddt>=1.0.1 # MIT hacking!=0.13.0,<0.14,>=0.12.0 # Apache-2.0 -oslosphinx>=4.7.0 # Apache-2.0 +openstackdocstheme>=1.16.0 # Apache-2.0 oslotest>=1.10.0 # Apache-2.0 osprofiler>=1.4.0 # Apache-2.0 os-testr>=0.8.0 # Apache-2.0 python-subunit>=0.0.18 # Apache-2.0/BSD -sphinx>=1.5.1 # BSD -tempest>=14.0.0 # Apache-2.0 +sphinx>=1.6.2 # BSD +tempest>=16.1.0 # Apache-2.0 testresources>=0.2.4 # Apache-2.0/BSD testrepository>=0.0.18 # Apache-2.0/BSD testscenarios>=0.4 # Apache-2.0/BSD testtools>=1.4.0 # MIT # releasenotes -reno>=1.8.0 # Apache-2.0 +reno!=2.3.1,>=1.8.0 # Apache-2.0 diff -Nru python-zunclient-0.2.0/tools/run_functional.sh python-zunclient-0.4.0/tools/run_functional.sh --- python-zunclient-0.2.0/tools/run_functional.sh 2017-04-24 13:28:10.000000000 +0000 +++ python-zunclient-0.4.0/tools/run_functional.sh 2017-07-28 16:11:40.000000000 +0000 @@ -26,4 +26,4 @@ os_endpoint_type=public END fi -tox -e functional +tox -e functional -- --concurrency=1 diff -Nru python-zunclient-0.2.0/zunclient/api_versions.py python-zunclient-0.4.0/zunclient/api_versions.py --- python-zunclient-0.2.0/zunclient/api_versions.py 1970-01-01 00:00:00.000000000 +0000 +++ python-zunclient-0.4.0/zunclient/api_versions.py 2017-07-28 16:11:40.000000000 +0000 @@ -0,0 +1,318 @@ +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# 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 logging +import os +import pkgutil +import re +import traceback + +from oslo_utils import strutils + +from zunclient import exceptions +from zunclient.i18n import _ + +LOG = logging.getLogger(__name__) +if not LOG.handlers: + LOG.addHandler(logging.StreamHandler()) + + +HEADER_NAME = "OpenStack-API-Version" +SERVICE_TYPE = "container" + +_SUBSTITUTIONS = {} + + +_type_error_msg = _("'%(other)s' should be an instance of '%(cls)s'") + + +class APIVersion(object): + """This class represents an API Version Request. + + This class provides convenience methods for manipulation + and comparison of version numbers that we need to do to + implement microversions. + """ + + def __init__(self, version_str=None): + """Create an API version object. + + :param version_str: String representation of APIVersionRequest. + Correct format is 'X.Y', where 'X' and 'Y' + are int values. None value should be used + to create Null APIVersionRequest, which is + equal to 0.0 + """ + self.ver_major = 0 + self.ver_minor = 0 + + if version_str is not None: + match = re.match(r"^([1-9]\d*)\.([1-9]\d*|0|latest)$", version_str) + if match: + self.ver_major = int(match.group(1)) + if match.group(2) == "latest": + # NOTE(andreykurilin): Infinity allows to easily determine + # latest version and doesn't require any additional checks + # in comparison methods. + self.ver_minor = float("inf") + else: + self.ver_minor = int(match.group(2)) + else: + msg = _("Invalid format of client version '%s'. " + "Expected format 'X.Y', where X is a major part and Y " + "is a minor part of version.") % version_str + raise exceptions.UnsupportedVersion(msg) + + def __str__(self): + """Debug/Logging representation of object.""" + if self.is_latest(): + return "Latest API Version Major: %s" % self.ver_major + return ("API Version Major: %s, Minor: %s" + % (self.ver_major, self.ver_minor)) + + def __repr__(self): + if self.is_null(): + return "" + else: + return "" % self.get_string() + + def is_null(self): + return self.ver_major == 0 and self.ver_minor == 0 + + def is_latest(self): + return self.ver_minor == float("inf") + + def __lt__(self, other): + if not isinstance(other, APIVersion): + raise TypeError(_type_error_msg % {"other": other, + "cls": self.__class__}) + + return ((self.ver_major, self.ver_minor) < + (other.ver_major, other.ver_minor)) + + def __eq__(self, other): + if not isinstance(other, APIVersion): + raise TypeError(_type_error_msg % {"other": other, + "cls": self.__class__}) + + return ((self.ver_major, self.ver_minor) == + (other.ver_major, other.ver_minor)) + + def __gt__(self, other): + if not isinstance(other, APIVersion): + raise TypeError(_type_error_msg % {"other": other, + "cls": self.__class__}) + + return ((self.ver_major, self.ver_minor) > + (other.ver_major, other.ver_minor)) + + def __le__(self, other): + return self < other or self == other + + def __ne__(self, other): + return not self.__eq__(other) + + def __ge__(self, other): + return self > other or self == other + + def matches(self, min_version, max_version): + """Matches the version object. + + Returns whether the version object represents a version + greater than or equal to the minimum version and less than + or equal to the maximum version. + + :param min_version: Minimum acceptable version. + :param max_version: Maximum acceptable version. + :returns: boolean + + If min_version is null then there is no minimum limit. + If max_version is null then there is no maximum limit. + If self is null then raise ValueError + """ + + if self.is_null(): + raise ValueError(_("Null APIVersion doesn't support 'matches'.")) + if max_version.is_null() and min_version.is_null(): + return True + elif max_version.is_null(): + return min_version <= self + elif min_version.is_null(): + return self <= max_version + else: + return min_version <= self <= max_version + + def get_string(self): + """Version string representation. + + Converts object to string representation which if used to create + an APIVersion object results in the same version. + """ + if self.is_null(): + raise ValueError( + _("Null APIVersion cannot be converted to string.")) + elif self.is_latest(): + return "%s.%s" % (self.ver_major, "latest") + return "%s.%s" % (self.ver_major, self.ver_minor) + + +class VersionedMethod(object): + + def __init__(self, name, start_version, end_version, func): + """Versioning information for a single method + + :param name: Name of the method + :param start_version: Minimum acceptable version + :param end_version: Maximum acceptable_version + :param func: Method to call + + Minimum and maximums are inclusive + """ + self.name = name + self.start_version = start_version + self.end_version = end_version + self.func = func + + def __str__(self): + return ("Version Method %s: min: %s, max: %s" + % (self.name, self.start_version, self.end_version)) + + def __repr__(self): + return "" % self.name + + +def get_available_major_versions(): + # NOTE(andreykurilin): available clients version should not be + # hardcoded, so let's discover them. + matcher = re.compile(r"v[0-9]*$") + submodules = pkgutil.iter_modules([os.path.dirname(__file__)]) + available_versions = [name[1:] for loader, name, ispkg in submodules + if matcher.search(name)] + + return available_versions + + +def check_major_version(api_version): + """Checks major part of ``APIVersion`` obj is supported. + + :raises exceptions.UnsupportedVersion: if major part is not supported + """ + available_versions = get_available_major_versions() + if (not api_version.is_null() and + str(api_version.ver_major) not in available_versions): + if len(available_versions) == 1: + msg = _("Invalid client version '%(version)s'. " + "Major part should be '%(major)s'") % { + "version": api_version.get_string(), + "major": available_versions[0]} + else: + msg = _("Invalid client version '%(version)s'. " + "Major part must be one of: '%(major)s'") % { + "version": api_version.get_string(), + "major": ", ".join(available_versions)} + raise exceptions.UnsupportedVersion(msg) + + +def get_api_version(version_string): + """Returns checked APIVersion object""" + version_string = str(version_string) + if strutils.is_int_like(version_string): + version_string = "%s.0" % version_string + + api_version = APIVersion(version_string) + check_major_version(api_version) + return api_version + + +def update_headers(headers, api_version): + """Set microversion headers if api_version is not null""" + + if not api_version.is_null() and api_version.ver_minor != 0: + version_string = api_version.get_string() + headers[HEADER_NAME] = '%s %s' % (SERVICE_TYPE, version_string) + + +def _add_substitution(versioned_method): + _SUBSTITUTIONS.setdefault(versioned_method.name, []) + _SUBSTITUTIONS[versioned_method.name].append(versioned_method) + + +def _get_function_name(func): + # NOTE(andreykurilin): Based on the facts: + # - Python 2 does not have __qualname__ property as Python 3 has; + # - we cannot use im_class here, since we need to obtain name of + # function in `wraps` decorator during class initialization + # ("im_class" property does not exist at that moment) + # we need to write own logic to obtain the full function name which + # include module name, owner name(optional) and just function name. + filename, _lineno, _name, line = traceback.extract_stack()[-4] + module, _file_extension = os.path.splitext(filename) + module = module.replace("/", ".") + if module.endswith(func.__module__): + return "%s.[%s].%s" % (func.__module__, line, func.__name__) + else: + return "%s.%s" % (func.__module__, func.__name__) + + +def get_substitutions(func_name, api_version=None): + if hasattr(func_name, "__id__"): + func_name = func_name.__id__ + + substitutions = _SUBSTITUTIONS.get(func_name, []) + if api_version and not api_version.is_null(): + return [m for m in substitutions + if api_version.matches(m.start_version, m.end_version)] + return sorted(substitutions, key=lambda m: m.start_version) + + +def wraps(start_version, end_version=None): + start_version = APIVersion(start_version) + if end_version: + end_version = APIVersion(end_version) + else: + end_version = APIVersion("%s.latest" % start_version.ver_major) + + def decor(func): + func.versioned = True + name = _get_function_name(func) + + versioned_method = VersionedMethod(name, start_version, + end_version, func) + _add_substitution(versioned_method) + + @functools.wraps(func) + def substitution(obj, *args, **kwargs): + methods = get_substitutions(name, obj.api_version) + + if not methods: + raise exceptions.VersionNotFoundForAPIMethod( + obj.api_version.get_string(), name) + return methods[-1].func(obj, *args, **kwargs) + + # Let's share "arguments" with original method and substitution to + # allow put cliutils.arg and wraps decorators in any order + if not hasattr(func, 'arguments'): + func.arguments = [] + substitution.arguments = func.arguments + + # NOTE(andreykurilin): The way to obtain function's name in Python 2 + # bases on traceback(see _get_function_name for details). Since the + # right versioned method method is used in several places, one object + # can have different names. Let's generate name of function one time + # and use __id__ property in all other places. + substitution.__id__ = name + + return substitution + + return decor diff -Nru python-zunclient-0.2.0/zunclient/common/apiclient/auth.py python-zunclient-0.4.0/zunclient/common/apiclient/auth.py --- python-zunclient-0.2.0/zunclient/common/apiclient/auth.py 2017-04-24 13:28:10.000000000 +0000 +++ python-zunclient-0.4.0/zunclient/common/apiclient/auth.py 2017-07-28 16:11:40.000000000 +0000 @@ -17,25 +17,10 @@ # E0202: An attribute inherited from %s hide this method # pylint: disable=E0202 -######################################################################## -# -# THIS MODULE IS DEPRECATED -# -# Please refer to -# https://etherpad.openstack.org/p/kilo-zunclient-library-proposals for -# the discussion leading to this deprecation. -# -# We recommend checking out the python-openstacksdk project -# (https://launchpad.net/python-openstacksdk) instead. -# -######################################################################## - import abc import argparse import os - import six -from stevedore import extension from zunclient.common.apiclient import exceptions @@ -43,22 +28,6 @@ _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 = "zunclient.common.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. @@ -82,34 +51,6 @@ 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(six.iterkeys(_discovered_plugins)): - 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"]) - - @six.add_metaclass(abc.ABCMeta) class BaseAuthPlugin(object): """Base class for authentication plugins. @@ -214,18 +155,3 @@ 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 -Nru python-zunclient-0.2.0/zunclient/common/apiclient/base.py python-zunclient-0.4.0/zunclient/common/apiclient/base.py --- python-zunclient-0.2.0/zunclient/common/apiclient/base.py 2017-04-24 13:28:10.000000000 +0000 +++ python-zunclient-0.4.0/zunclient/common/apiclient/base.py 2017-07-28 16:11:40.000000000 +0000 @@ -20,422 +20,8 @@ Base utilities to build API operation managers and objects on top of. """ -######################################################################## -# -# THIS MODULE IS DEPRECATED -# -# Please refer to -# https://etherpad.openstack.org/p/kilo-zunclient-library-proposals for -# the discussion leading to this deprecation. -# -# We recommend checking out the python-openstacksdk project -# (https://launchpad.net/python-openstacksdk) instead. -# -######################################################################## - - -# E1102: %s is not callable -# pylint: disable=E1102 - -import abc import copy -from oslo_utils import strutils -import six -from six.moves.urllib import parse - -from zunclient.common.apiclient import exceptions -from zunclient.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=None, obj_class=None, json=None): - """List the collection. - - :param url: a partial URL, e.g., '/servers' - :param response_key: the key to be looked up in response dictionary, - e.g., 'servers'. If response_key is None - all response body - will be used. - :param obj_class: class for constructing the returned objects - (self.resource_class will be used by default) - :param json: data that will be encoded as JSON and passed in POST - request (GET will be sent by default) - """ - if json: - body = self.client.post(url, json=json).json() - else: - body = self.client.get(url).json() - - if obj_class is None: - obj_class = self.resource_class - - data = body[response_key] if response_key is not None else body - # NOTE(ja): keystone returns values as list as {'values': [ ... ]} - # unlike other services which just return the list... - try: - data = data['values'] - except (KeyError, TypeError): - pass - - return [obj_class(self, res, loaded=True) for res in data if res] - - def _get(self, url, response_key=None): - """Get an object from collection. - - :param url: a partial URL, e.g., '/servers' - :param response_key: the key to be looked up in response dictionary, - e.g., 'server'. If response_key is None - all response body - will be used. - """ - body = self.client.get(url).json() - data = body[response_key] if response_key is not None else body - return self.resource_class(self, data, loaded=True) - - def _head(self, url): - """Retrieve request headers for an object. - - :param url: a partial URL, e.g., '/servers' - """ - resp = self.client.head(url) - return resp.status_code == 204 - - def _post(self, url, json, response_key=None, return_raw=False): - """Create an object. - - :param url: a partial URL, e.g., '/servers' - :param json: data that will be encoded as JSON and passed in POST - request (GET will be sent by default) - :param response_key: the key to be looked up in response dictionary, - e.g., 'server'. If response_key is None - all response body - will be used. - :param return_raw: flag to force returning raw JSON instead of - Python object of self.resource_class - """ - body = self.client.post(url, json=json).json() - data = body[response_key] if response_key is not None else body - if return_raw: - return data - return self.resource_class(self, data) - - def _put(self, url, json=None, response_key=None): - """Update an object with PUT method. - - :param url: a partial URL, e.g., '/servers' - :param json: data that will be encoded as JSON and passed in POST - request (GET will be sent by default) - :param response_key: the key to be looked up in response dictionary, - e.g., 'servers'. If response_key is None - all response body - will be used. - """ - resp = self.client.put(url, json=json) - # PUT requests may not return a body - if resp.content: - body = resp.json() - if response_key is not None: - return self.resource_class(self, body[response_key]) - else: - return self.resource_class(self, body) - - def _patch(self, url, json=None, response_key=None): - """Update an object with PATCH method. - - :param url: a partial URL, e.g., '/servers' - :param json: data that will be encoded as JSON and passed in POST - request (GET will be sent by default) - :param response_key: the key to be looked up in response dictionary, - e.g., 'servers'. If response_key is None - all response body - will be used. - """ - body = self.client.patch(url, json=json).json() - if response_key is not None: - return self.resource_class(self, body[response_key]) - else: - return self.resource_class(self, body) - - def _delete(self, url): - """Delete an object. - - :param url: a partial URL, e.g., '/servers/my-server' - """ - return self.client.delete(url) - - -@six.add_metaclass(abc.ABCMeta) -class ManagerWithFind(BaseManager): - """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(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.). @@ -443,9 +29,6 @@ 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. @@ -465,15 +48,6 @@ 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: diff -Nru python-zunclient-0.2.0/zunclient/common/apiclient/exceptions.py python-zunclient-0.4.0/zunclient/common/apiclient/exceptions.py --- python-zunclient-0.2.0/zunclient/common/apiclient/exceptions.py 2017-04-24 13:28:10.000000000 +0000 +++ python-zunclient-0.4.0/zunclient/common/apiclient/exceptions.py 2017-07-28 16:11:40.000000000 +0000 @@ -20,19 +20,6 @@ Exception definitions. """ -######################################################################## -# -# THIS MODULE IS DEPRECATED -# -# Please refer to -# https://etherpad.openstack.org/p/kilo-zunclient-library-proposals for -# the discussion leading to this deprecation. -# -# We recommend checking out the python-openstacksdk project -# (https://launchpad.net/python-openstacksdk) instead. -# -######################################################################## - import inspect import sys @@ -41,13 +28,19 @@ from zunclient.i18n import _ -class ClientException(Exception): - """The base exception class for all exceptions this library raises.""" - pass +class VersionNotFoundForAPIMethod(Exception): + msg_fmt = "API version '%(vers)s' is not supported on '%(method)s' method." + def __init__(self, version, method): + self.version = version + self.method = method -class ValidationError(ClientException): - """Error in validation on API client side.""" + def __str__(self): + return self.msg_fmt % {"vers": self.version, "method": self.method} + + +class ClientException(Exception): + """The base exception class for all exceptions this library raises.""" pass diff -Nru python-zunclient-0.2.0/zunclient/common/base.py python-zunclient-0.4.0/zunclient/common/base.py --- python-zunclient-0.2.0/zunclient/common/base.py 2017-04-24 13:28:10.000000000 +0000 +++ python-zunclient-0.4.0/zunclient/common/base.py 2017-07-28 16:11:40.000000000 +0000 @@ -44,6 +44,10 @@ def __init__(self, api): self.api = api + @property + def api_version(self): + return self.api.api_version + def _create(self, url, body): resp, body = self.api.json_request('POST', url, body=body) if body: @@ -116,7 +120,11 @@ return object_list - def _list(self, url, response_key=None, obj_class=None, body=None): + def _list(self, url, response_key=None, obj_class=None, body=None, + qparams=None): + if qparams: + url = "%s?%s" % (url, urlparse.urlencode(qparams)) + resp, body = self.api.json_request('GET', url) if obj_class is None: diff -Nru python-zunclient-0.2.0/zunclient/common/cliutils.py python-zunclient-0.4.0/zunclient/common/cliutils.py --- python-zunclient-0.2.0/zunclient/common/cliutils.py 2017-04-24 13:28:10.000000000 +0000 +++ python-zunclient-0.4.0/zunclient/common/cliutils.py 2017-07-28 16:11:40.000000000 +0000 @@ -216,6 +216,8 @@ v = six.text_type(keys_and_vals_to_strs(v)) if wrap > 0: v = textwrap.fill(six.text_type(v), wrap) + elif wrap < 0: + raise ValueError(_("Wrap argument should be a positive integer")) # if value has a newline, add in multiple rows # e.g. fault with stacktrace if v and isinstance(v, six.string_types) and r'\n' in v: diff -Nru python-zunclient-0.2.0/zunclient/common/httpclient.py python-zunclient-0.4.0/zunclient/common/httpclient.py --- python-zunclient-0.2.0/zunclient/common/httpclient.py 2017-04-24 13:28:10.000000000 +0000 +++ python-zunclient-0.4.0/zunclient/common/httpclient.py 2017-07-28 16:11:40.000000000 +0000 @@ -26,6 +26,7 @@ import six import six.moves.urllib.parse as urlparse +from zunclient import api_versions from zunclient import exceptions osprofiler_web = importutils.try_import("osprofiler.web") @@ -35,6 +36,7 @@ CHUNKSIZE = 1024 * 64 # 64kB API_VERSION = '/v1' +DEFAULT_API_VERSION = '1.latest' def _extract_error_json(body): @@ -65,10 +67,11 @@ class HTTPClient(object): - def __init__(self, endpoint, **kwargs): + def __init__(self, endpoint, api_version=DEFAULT_API_VERSION, **kwargs): self.endpoint = endpoint self.auth_token = kwargs.get('token') self.auth_ref = kwargs.get('auth_ref') + self.api_version = api_version or api_versions.APIVersion() self.connection_params = self.get_connection_params(endpoint, **kwargs) @staticmethod @@ -155,6 +158,8 @@ # 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) + api_versions.update_headers(kwargs["headers"], self.api_version) + if self.auth_token: kwargs['headers'].setdefault('X-Auth-Token', self.auth_token) @@ -307,7 +312,10 @@ class SessionClient(adapter.LegacyJsonAdapter): """HTTP client based on Keystone client session.""" - def __init__(self, user_agent=USER_AGENT, logger=LOG, *args, **kwargs): + def __init__(self, user_agent=USER_AGENT, logger=LOG, + api_version=DEFAULT_API_VERSION, *args, **kwargs): + self.user_agent = USER_AGENT + self.api_version = api_version or api_versions.APIVersion() super(SessionClient, self).__init__(*args, **kwargs) def _http_request(self, url, method, **kwargs): @@ -318,6 +326,11 @@ kwargs.setdefault('auth', self.auth) kwargs.setdefault('endpoint_override', self.endpoint_override) + # Copy the kwargs so we can reuse the original in case of redirects + kwargs['headers'] = copy.deepcopy(kwargs.get('headers', {})) + kwargs['headers'].setdefault('User-Agent', self.user_agent) + api_versions.update_headers(kwargs["headers"], self.api_version) + # NOTE(kevinz): osprofiler_web.get_trace_id_headers does not add any # headers in case if osprofiler is not initialized. if osprofiler_web: diff -Nru python-zunclient-0.2.0/zunclient/common/utils.py python-zunclient-0.4.0/zunclient/common/utils.py --- python-zunclient-0.2.0/zunclient/common/utils.py 2017-04-24 13:28:10.000000000 +0000 +++ python-zunclient-0.4.0/zunclient/common/utils.py 2017-07-28 16:11:40.000000000 +0000 @@ -16,10 +16,23 @@ import json +from oslo_utils import netutils + +from zunclient.common.apiclient import exceptions as apiexec from zunclient.common import cliutils as utils from zunclient import exceptions as exc from zunclient.i18n import _ +VALID_UNITS = ( + K, + M, + G, +) = ( + 1024, + 1024 * 1024, + 1024 * 1024 * 1024, +) + def common_filters(marker=None, limit=None, sort_key=None, sort_dir=None, all_tenants=False): @@ -156,3 +169,50 @@ utils.print_list(containers, columns, {'versions': print_list_field('versions')}, sortby_index=None) + + +def parse_command(command): + output = [] + if command: + for c in command: + c = '"' + c + '"' + output.append(c) + return " ".join(output) + + +def parse_nets(ns): + err_msg = ("Invalid nets argument '%s'. nets arguments must be of " + "the form --nets , " + "with only one of network, or port specified.") + nets = [] + for net_str in ns: + net_info = {"network": "", "v4-fixed-ip": "", "v6-fixed-ip": "", + "port": ""} + for kv_str in net_str.split(","): + try: + k, v = kv_str.split("=", 1) + k = k.strip() + v = v.strip() + except ValueError: + raise apiexec.CommandError(err_msg % net_str) + if k in net_info: + if net_info[k]: + raise apiexec.CommandError(err_msg % net_str) + net_info[k] = v + else: + raise apiexec.CommandError(err_msg % net_str) + + if net_info['v4-fixed-ip'] and not netutils.is_valid_ipv4( + net_info['v4-fixed-ip']): + raise apiexec.CommandError("Invalid ipv4 address.") + + if net_info['v6-fixed-ip'] and not netutils.is_valid_ipv6( + net_info['v6-fixed-ip']): + raise apiexec.CommandError("Invalid ipv6 address.") + + if bool(net_info['network']) == bool(net_info['port']): + raise apiexec.CommandError(err_msg % net_str) + + nets.append(net_info) + return nets diff -Nru python-zunclient-0.2.0/zunclient/common/websocketclient/exceptions.py python-zunclient-0.4.0/zunclient/common/websocketclient/exceptions.py --- python-zunclient-0.2.0/zunclient/common/websocketclient/exceptions.py 2017-04-24 13:28:10.000000000 +0000 +++ python-zunclient-0.4.0/zunclient/common/websocketclient/exceptions.py 2017-07-28 16:11:40.000000000 +0000 @@ -45,3 +45,7 @@ class ContainerFailtoStart(ContainerWebSocketException): message = "Container fail to start" + + +class ContainerStateError(ContainerWebSocketException): + message = "Container state is error, can not attach container" diff -Nru python-zunclient-0.2.0/zunclient/common/websocketclient/websocketclient.py python-zunclient-0.4.0/zunclient/common/websocketclient/websocketclient.py --- python-zunclient-0.2.0/zunclient/common/websocketclient/websocketclient.py 2017-04-24 13:28:10.000000000 +0000 +++ python-zunclient-0.4.0/zunclient/common/websocketclient/websocketclient.py 2017-07-28 16:11:40.000000000 +0000 @@ -31,6 +31,8 @@ import tty import websocket +import docker + from zunclient.common.websocketclient import exceptions LOG = logging.getLogger(__name__) @@ -40,36 +42,42 @@ DEFAULT_SERVICE_TYPE = 'container' -class WebSocketClient(object): +class BaseClient(object): - def __init__(self, zunclient, host_url, id, escape='~', + def __init__(self, zunclient, url, id, escape='~', close_wait=0.5): + self.url = url self.id = id self.escape = escape self.close_wait = close_wait - self.host_url = host_url self.cs = zunclient def connect(self): - url = self.host_url - LOG.debug('connecting to: %s', url) - try: - self.ws = websocket.create_connection(url, - skip_utf8_validation=True) - print('connected to %s ,press Enter to continue' % self.id) - print('type %s. to disconnect' % self.escape) - except socket.error as e: - raise exceptions.ConnectionFailed(e) - except websocket.WebSocketConnectionClosedException as e: - raise exceptions.ConnectionFailed(e) - except websocket.WebSocketBadStatusException as e: - raise exceptions.ConnectionFailed(e) + raise NotImplementedError() + + def fileno(self): + raise NotImplementedError() + + def send(self, data): + raise NotImplementedError() + + def recv(self): + raise NotImplementedError() + + def tty_resize(self, height, width): + """Resize the tty session + + Get the client and send the tty size data to zun api server + The environment variables need to get when implement sending + operation. + """ + raise NotImplementedError() def start_loop(self): self.poll = select.poll() self.poll.register(sys.stdin, select.POLLIN | select.POLLHUP | select.POLLPRI) - self.poll.register(self.ws, + self.poll.register(self.fileno(), select.POLLIN | select.POLLHUP | select.POLLPRI) self.start_of_line = False @@ -94,8 +102,8 @@ while True: try: for fd, event in self.poll.poll(500): - if fd == self.ws.fileno(): - self.handle_websocket(event) + if fd == self.fileno(): + self.handle_socket(event) elif fd == sys.stdin.fileno(): self.handle_stdin(event) except select.error as e: @@ -107,12 +115,12 @@ raise e if self.quit and not quitting: - self.log.debug('entering close_wait') + LOG.debug('entering close_wait') quitting = True when = time.time() + self.close_wait if quitting and time.time() > when: - self.log.debug('quitting') + LOG.debug('quitting') break def setup_tty(self): @@ -150,27 +158,26 @@ raise exceptions.UserExit() elif self.read_escape: self.read_escape = False - self.ws.send(self.escape) + self.send(self.escape) - self.ws.send(data) + self.send(data) if data == '\r': self.start_of_line = True else: self.start_of_line = False - def handle_websocket(self, event): + def handle_socket(self, event): if event in (select.POLLHUP, select.POLLNVAL): - LOG.debug('event %d on websocket', event) - - LOG.debug('eof on websocket') - self.poll.unregister(self.ws) + self.poll.unregister(self.fileno()) self.quit = True - data = self.ws.recv() - LOG.debug('read %s (%d bytes) from websocket from container', + data = self.recv() + LOG.debug('read %s (%d bytes) from socket from container', repr(data), len(data)) if not data: + self.poll.unregister(self.fileno()) + self.quit = True return sys.stdout.write(data) @@ -217,6 +224,39 @@ return dims + +class WebSocketClient(BaseClient): + + def __init__(self, zunclient, url, id, escape='~', + close_wait=0.5): + super(WebSocketClient, self).__init__( + zunclient, url, id, escape, close_wait) + + def connect(self): + url = self.url + LOG.debug('connecting to: %s', url) + try: + self.ws = websocket.create_connection( + url, skip_utf8_validation=True, + subprotocols=["binary", "base64"]) + print('connected to %s ,press Enter to continue' % self.id) + print('type %s. to disconnect' % self.escape) + except socket.error as e: + raise exceptions.ConnectionFailed(e) + except websocket.WebSocketConnectionClosedException as e: + raise exceptions.ConnectionFailed(e) + except websocket.WebSocketBadStatusException as e: + raise exceptions.ConnectionFailed(e) + + def fileno(self): + return self.ws.fileno() + + def send(self, data): + self.ws.send(data) + + def recv(self): + return self.ws.recv() + def tty_resize(self, height, width): """Resize the tty session @@ -230,6 +270,46 @@ self.cs.containers.resize(self.id, width, height) +class HTTPClient(BaseClient): + + def __init__(self, zunclient, url, exec_id, id, escape='~', + close_wait=0.5): + super(HTTPClient, self).__init__(zunclient, url, id, escape, + close_wait) + self.exec_id = exec_id + + def connect(self): + try: + client = docker.APIClient(base_url=self.url) + self.socket = client.exec_start(self.exec_id, socket=True, + tty=True) + print('connected to container "%s"' % self.id) + print('type %s. to disconnect' % self.escape) + except docker.errors.APIError as e: + raise exceptions.ConnectionFailed(e) + + def fileno(self): + return self.socket.fileno() + + def send(self, data): + self.socket.send(data) + + def recv(self): + return self.socket.recv(4096) + + def tty_resize(self, height, width): + """Resize the tty session + + Get the client and send the tty size data to zun api server + The environment variables need to get when implement sending + operation. + """ + height = str(height) + width = str(width) + + self.cs.containers.execute_resize(self.id, self.exec_id, width, height) + + class WINCHHandler(object): """WINCH Signal handler @@ -288,17 +368,25 @@ signal.signal(signal.SIGWINCH, self.original_handler) -def do_attach(zunclient, url, container, escape, close_wait): +def do_attach(zunclient, url, container_id, escape, close_wait): if url.startswith("ws://"): try: - wscls = WebSocketClient(zunclient=zunclient, host_url=url, - id=container, escape=escape, + wscls = WebSocketClient(zunclient=zunclient, url=url, + id=container_id, escape=escape, close_wait=close_wait) wscls.connect() wscls.handle_resize() wscls.start_loop() except exceptions.ContainerWebSocketException as e: print("%(e)s:%(container)s" % - {'e': e, 'container': container}) + {'e': e, 'container': container_id}) else: - raise exceptions.InvalidWebSocketLink(container) + raise exceptions.InvalidWebSocketLink(container_id) + + +def do_exec(zunclient, url, container_id, exec_id, escape, close_wait): + httpcls = HTTPClient(zunclient=zunclient, url=url, exec_id=exec_id, + id=container_id, escape="~", close_wait=0.5) + httpcls.connect() + httpcls.handle_resize() + httpcls.start_loop() diff -Nru python-zunclient-0.2.0/zunclient/i18n.py python-zunclient-0.4.0/zunclient/i18n.py --- python-zunclient-0.2.0/zunclient/i18n.py 2017-04-24 13:28:10.000000000 +0000 +++ python-zunclient-0.4.0/zunclient/i18n.py 2017-07-28 16:11:40.000000000 +0000 @@ -12,7 +12,7 @@ """oslo_i18n integration module for zunclient. -See http://docs.openstack.org/developer/oslo.i18n/usage.html . +See https://docs.openstack.org/oslo.i18n/latest/user/usage.html. """ diff -Nru python-zunclient-0.2.0/zunclient/osc/plugin.py python-zunclient-0.4.0/zunclient/osc/plugin.py --- python-zunclient-0.2.0/zunclient/osc/plugin.py 2017-04-24 13:28:10.000000000 +0000 +++ python-zunclient-0.4.0/zunclient/osc/plugin.py 2017-07-28 16:11:40.000000000 +0000 @@ -10,18 +10,25 @@ # License for the specific language governing permissions and limitations # under the License. +import argparse import logging from osc_lib import utils +from zunclient import api_versions + + LOG = logging.getLogger(__name__) -DEFAULT_CONTAINER_API_VERSION = "1" +DEFAULT_CONTAINER_API_VERSION = "1.2" API_VERSION_OPTION = "os_container_api_version" API_NAME = "container" +LAST_KNOWN_API_VERSION = 2 API_VERSIONS = { - '1': 'zunclient.v1.client.Client', + '1.%d' % i: 'zunclient.v1.client.Client' + for i in range(1, LAST_KNOWN_API_VERSION + 1) } +API_VERSIONS['1'] = API_VERSIONS[DEFAULT_CONTAINER_API_VERSION] def make_client(instance): @@ -33,7 +40,9 @@ LOG.debug("Instantiating zun client: {0}".format( zun_client)) + api_version = api_versions.get_api_version(instance._api_version[API_NAME]) client = zun_client( + api_version=api_version, region_name=instance._region_name, session=instance.session, service_type='container', @@ -49,7 +58,22 @@ default=utils.env( 'OS_CONTAINER_API_VERSION', default=DEFAULT_CONTAINER_API_VERSION), + action=ReplaceLatestVersion, + choices=sorted( + API_VERSIONS, + key=lambda k: [int(x) for x in k.split('.')]) + ['latest'], help=("Container API version, default={0}" "(Env:OS_CONTAINER_API_VERSION)").format( DEFAULT_CONTAINER_API_VERSION)) return parser + + +class ReplaceLatestVersion(argparse.Action): + """Replaces `latest` keyword by last known version.""" + def __call__(self, parser, namespace, values, option_string=None): + latest = values == 'latest' + if latest: + values = '1.%d' % LAST_KNOWN_API_VERSION + LOG.debug("Replacing 'latest' API version with the " + "latest known version '%s'", values) + setattr(namespace, self.dest, values) diff -Nru python-zunclient-0.2.0/zunclient/osc/v1/containers.py python-zunclient-0.4.0/zunclient/osc/v1/containers.py --- python-zunclient-0.2.0/zunclient/osc/v1/containers.py 2017-04-24 13:28:10.000000000 +0000 +++ python-zunclient-0.4.0/zunclient/osc/v1/containers.py 2017-07-28 16:11:40.000000000 +0000 @@ -86,7 +86,7 @@ 'It can have following values: ' '"ifnotpresent": only pull the image if it does not ' 'already exist on the node. ' - '"always": Always pull the image from repositery.' + '"always": Always pull the image from repository.' '"never": never pull the image') parser.add_argument( '--restart', @@ -107,10 +107,37 @@ default=False, help='Keep STDIN open even if not attached, allocate a pseudo-TTY') parser.add_argument( + '--security-group', + metavar='', + action='append', default=[], + help='The name of security group for the container. ' + 'May be used multiple times.') + parser.add_argument( 'command', metavar='', nargs=argparse.REMAINDER, help='Send command to the container') + parser.add_argument( + '--hint', + metavar='key=value', + action='append', + default=[], + help='The key-value pair(s) for scheduler to select host. ' + 'The format of this parameter is "key=value[,key=value]". ' + 'May be used multiple times.') + parser.add_argument( + '--net', + metavar='', + action='append', + default=[], + help='Create network enpoints for the container. ' + 'auto: do not specify the network, zun will automatically ' + 'create one. ' + 'network: attach container to the specified neutron networks.' + ' port: attach container to the neutron port with this UUID. ' + 'v4-fixed-ip: IPv4 fixed address for container. ' + 'v6-fixed-ip: IPv6 fixed address for container.') return parser def take_action(self, parsed_args): @@ -125,13 +152,17 @@ opts['labels'] = zun_utils.format_args(parsed_args.label) opts['image_pull_policy'] = parsed_args.image_pull_policy opts['image_driver'] = parsed_args.image_driver + if parsed_args.security_group: + opts['security_groups'] = parsed_args.security_group if parsed_args.command: - opts['command'] = ' '.join(parsed_args.command) + opts['command'] = zun_utils.parse_command(parsed_args.command) if parsed_args.restart: opts['restart_policy'] = \ zun_utils.check_restart_policy(parsed_args.restart) if parsed_args.interactive: opts['interactive'] = True + opts['hints'] = zun_utils.format_args(parsed_args.hint) + opts['nets'] = zun_utils.parse_nets(parsed_args.net) opts = zun_utils.remove_null_parms(**opts) container = client.containers.create(**opts) @@ -150,12 +181,20 @@ 'container', metavar='', help='ID or name of the container to show.') + parser.add_argument( + '--all-tenants', + action="store_true", + default=False, + help='Show container(s) in all tenant by name.') return parser def take_action(self, parsed_args): client = _get_client(self, parsed_args) - container = parsed_args.container - container = client.containers.get(container) + opts = {} + opts['id'] = parsed_args.container + opts['all_tenants'] = parsed_args.all_tenants + opts = zun_utils.remove_null_parms(**opts) + container = client.containers.get(**opts) columns = _container_columns(container) return columns, utils.get_item_properties(container, columns) @@ -228,15 +267,24 @@ '--force', action='store_true', help='Force delete the container.') + parser.add_argument( + '--all-tenants', + action="store_true", + default=False, + help='Delete container(s) in all tenant by name.') return parser def take_action(self, parsed_args): client = _get_client(self, parsed_args) containers = parsed_args.container - force = getattr(parsed_args, 'force') for container in containers: + opts = {} + opts['id'] = container + opts['force'] = parsed_args.force + opts['all_tenants'] = parsed_args.all_tenants + opts = zun_utils.remove_null_parms(**opts) try: - client.containers.delete(container, force) + client.containers.delete(**opts) print(_('Request to delete container %s has been accepted.') % container) except Exception as e: @@ -368,17 +416,32 @@ metavar='', nargs=argparse.REMAINDER, help='The command to execute.') + parser.add_argument( + '--interactive', + dest='interactive', + action='store_true', + default=False, + help='Keep STDIN open and allocate a pseudo-TTY for interactive') return parser def take_action(self, parsed_args): client = _get_client(self, parsed_args) container = parsed_args.container - command = ' '.join(parsed_args.command) - response = client.containers.execute(container, command) - output = response['output'] - exit_code = response['exit_code'] - print(output) - return exit_code + opts = {} + opts['command'] = zun_utils.parse_command(parsed_args.command) + if parsed_args.interactive: + opts['interactive'] = True + opts['run'] = False + response = client.containers.execute(container, **opts) + if parsed_args.interactive: + exec_id = response['exec_id'] + url = response['url'] + websocketclient.do_exec(client, url, container, exec_id, "~", 0.5) + else: + output = response['output'] + exit_code = response['exit_code'] + print(output) + return exit_code class LogsContainer(command.Command): @@ -514,10 +577,6 @@ metavar='', help='name or ID of the image') parser.add_argument( - '--command', - metavar='', - help='Send command to the container') - parser.add_argument( '--cpu', metavar='', help='The number of virtual cpus.') @@ -550,7 +609,7 @@ 'It can have following values: ' '"ifnotpresent": only pull the image if it does not ' 'already exist on the node. ' - '"always": Always pull the image from repositery.' + '"always": Always pull the image from repository.' '"never": never pull the image') parser.add_argument( '--restart', @@ -571,10 +630,37 @@ default=False, help='Keep STDIN open even if not attached, allocate a pseudo-TTY') parser.add_argument( + '--security-group', + metavar='', + action='append', default=[], + help='The name of security group for the container. ' + 'May be used multiple times.') + parser.add_argument( 'command', metavar='', nargs=argparse.REMAINDER, help='Send command to the container') + parser.add_argument( + '--hint', + metavar='key=value', + action='append', + default=[], + help='The key-value pair(s) for scheduler to select host. ' + 'The format of this parameter is "key=value[,key=value]". ' + 'May be used multiple times.') + parser.add_argument( + '--net', + metavar='', + action='append', + default=[], + help='Create network enpoints for the container. ' + 'auto: do not specify the network, zun will automatically ' + 'create one. ' + 'network: attach container to the specified neutron networks.' + ' port: attach container to the neutron port with this UUID. ' + 'v4-fixed-ip: IPv4 fixed address for container. ' + 'v6-fixed-ip: IPv6 fixed address for container.') return parser def take_action(self, parsed_args): @@ -589,13 +675,17 @@ opts['labels'] = zun_utils.format_args(parsed_args.label) opts['image_pull_policy'] = parsed_args.image_pull_policy opts['image_driver'] = parsed_args.image_driver + if parsed_args.security_group: + opts['security_groups'] = parsed_args.security_group if parsed_args.command: - opts['command'] = ' '.join(parsed_args.command) + opts['command'] = zun_utils.parse_command(parsed_args.command) if parsed_args.restart: opts['restart_policy'] = \ zun_utils.check_restart_policy(parsed_args.restart) if parsed_args.interactive: opts['interactive'] = True + opts['hints'] = zun_utils.format_args(parsed_args.hint) + opts['nets'] = zun_utils.parse_nets(parsed_args.net) opts = zun_utils.remove_null_parms(**opts) container = client.containers.run(**opts) @@ -729,7 +819,7 @@ parser.add_argument( 'container', metavar='', - help='ID or name of the container to be attahed to.') + help='ID or name of the container to be attached to.') return parser def take_action(self, parsed_args): @@ -791,3 +881,59 @@ print("Usage:") print("openstack appcontainer cp container:src_path dest_path|-") print("openstack appcontainer cp src_path|- container:dest_path") + + +class StatsContainer(command.ShowOne): + """Display stats of the container.""" + log = logging.getLogger(__name__ + ".StatsContainer") + + def get_parser(self, prog_name): + parser = super(StatsContainer, self).get_parser(prog_name) + parser.add_argument( + 'container', + metavar='', + help='ID or name of the (container)s to display stats.') + return parser + + def take_action(self, parsed_args): + client = _get_client(self, parsed_args) + container = parsed_args.container + stats_info = client.containers.stats(container) + return stats_info.keys(), stats_info.values() + + +class CommitContainer(command.Command): + """Create a new image from a container's changes""" + log = logging.getLogger(__name__ + ".CommitContainer") + + def get_parser(self, prog_name): + parser = super(CommitContainer, self).get_parser(prog_name) + parser.add_argument( + 'container', + metavar='', + help='ID or name of the (container)s to commit.') + parser.add_argument( + '--repository', + required=True, + metavar='', + help='Repository of the new image.') + parser.add_argument( + '--tag', + metavar='', + help='Tag of the new iamge') + return parser + + def take_action(self, parsed_args): + client = _get_client(self, parsed_args) + container = parsed_args.container + opts = {} + opts['repository'] = parsed_args.repository + opts['tag'] = parsed_args.tag + opts = zun_utils.remove_null_parms(**opts) + try: + image = client.containers.commit(container, **opts) + print("Request to commit container %s has been accepted. " + "The image is %s." % (container, image)) + except Exception as e: + print("commit container %(container)s failed: %(e)s" % + {'container': container, 'e': e}) diff -Nru python-zunclient-0.2.0/zunclient/osc/v1/images.py python-zunclient-0.4.0/zunclient/osc/v1/images.py --- python-zunclient-0.2.0/zunclient/osc/v1/images.py 1970-01-01 00:00:00.000000000 +0000 +++ python-zunclient-0.4.0/zunclient/osc/v1/images.py 2017-07-28 16:11:40.000000000 +0000 @@ -0,0 +1,90 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import logging + +from osc_lib.command import command +from osc_lib import utils + + +def _image_columns(image): + del image._info['links'] + return image._info.keys() + + +def _get_client(obj, parsed_args): + obj.log.debug("take_action(%s)" % parsed_args) + return obj.app.client_manager.container + + +class ListImage(command.Lister): + """List available images""" + + log = logging.getLogger(__name__ + ".ListImage") + + def get_parser(self, prog_name): + parser = super(ListImage, self).get_parser(prog_name) + parser.add_argument( + '--marker', + metavar='', + default=None, + help='The last image UUID of the previous page; ' + 'displays list of images after "marker".') + parser.add_argument( + '--limit', + metavar='', + type=int, + help='Maximum number of images to return') + parser.add_argument( + '--sort-key', + metavar='', + help='Column to sort results by') + parser.add_argument( + '--sort-dir', + metavar='', + choices=['desc', 'asc'], + help='Direction to sort. "asc" or "desc".') + return parser + + def take_action(self, parsed_args): + client = _get_client(self, parsed_args) + opts = {} + opts['marker'] = parsed_args.marker + opts['limit'] = parsed_args.limit + opts['sort_key'] = parsed_args.sort_key + opts['sort_dir'] = parsed_args.sort_dir + images = client.images.list(**opts) + columns = ('uuid', 'image_id', 'repo', 'tag', 'size') + return (columns, (utils.get_item_properties(image, columns) + for image in images)) + + +class PullImage(command.ShowOne): + """Pull specified image""" + + log = logging.getLogger(__name__ + ".PullImage") + + def get_parser(self, prog_name): + parser = super(PullImage, self).get_parser(prog_name) + parser.add_argument( + 'image', + metavar='', + help='Name of the image') + return parser + + def take_action(self, parsed_args): + client = _get_client(self, parsed_args) + opts = {} + opts['repo'] = parsed_args.image + image = client.images.create(**opts) + columns = _image_columns(image) + return columns, utils.get_item_properties(image, columns) diff -Nru python-zunclient-0.2.0/zunclient/osc/v1/services.py python-zunclient-0.4.0/zunclient/osc/v1/services.py --- python-zunclient-0.2.0/zunclient/osc/v1/services.py 1970-01-01 00:00:00.000000000 +0000 +++ python-zunclient-0.4.0/zunclient/osc/v1/services.py 2017-07-28 16:11:40.000000000 +0000 @@ -0,0 +1,159 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import logging + +from osc_lib.command import command +from osc_lib import utils + + +def _get_client(obj, parsed_args): + obj.log.debug("take_action(%s)" % parsed_args) + return obj.app.client_manager.container + + +class ListService(command.Lister): + """Print a list of zun services.""" + + log = logging.getLogger(__name__ + ".ListService") + + def get_parser(self, prog_name): + parser = super(ListService, self).get_parser(prog_name) + return parser + + def take_action(self, parsed_args): + client = _get_client(self, parsed_args) + services = client.services.list() + columns = ('Id', 'Host', 'Binary', 'State', 'Disabled', + 'Disabled Reason', 'Created At', 'Updated At') + return (columns, (utils.get_item_properties(service, columns) + for service in services)) + + +class DeleteService(command.Command): + """Delete the Zun binaries/services.""" + + log = logging.getLogger(__name__ + ".DeleteService") + + def get_parser(self, prog_name): + parser = super(DeleteService, self).get_parser(prog_name) + parser.add_argument( + 'host', + metavar='', + help='Name of host') + parser.add_argument( + 'binary', + metavar='', + help='Name of the binary to delete') + return parser + + def take_action(self, parsed_args): + client = _get_client(self, parsed_args) + host = parsed_args.host + binary = parsed_args.binary + try: + client.services.delete(host, binary) + print("Request to delete binary %s on host %s has been accepted." % + (binary, host)) + except Exception as e: + print("Delete for binary %s on host %s failed: %s" % + (binary, host, e)) + + +class EnableService(command.ShowOne): + """Enable the Zun service.""" + log = logging.getLogger(__name__ + ".EnableService") + + def get_parser(self, prog_name): + parser = super(EnableService, self).get_parser(prog_name) + parser.add_argument( + 'host', + metavar='', + help='Name of host') + parser.add_argument( + 'binary', + metavar='', + help='Name of the binary to enable') + return parser + + def take_action(self, parsed_args): + client = _get_client(self, parsed_args) + host = parsed_args.host + binary = parsed_args.binary + res = client.services.enable(host, binary) + columns = ('Host', 'Binary', 'Disabled', 'Disabled Reason') + return columns, (utils.get_dict_properties(res[1]['service'], + columns)) + + +class DisableService(command.ShowOne): + """Disable the Zun service.""" + log = logging.getLogger(__name__ + ".DisableService") + + def get_parser(self, prog_name): + parser = super(DisableService, self).get_parser(prog_name) + parser.add_argument( + 'host', + metavar='', + help='Name of host') + parser.add_argument( + 'binary', + metavar='', + help='Name of the binary to disable') + parser.add_argument( + '--reason', + metavar='', + help='Reason for disabling service') + return parser + + def take_action(self, parsed_args): + client = _get_client(self, parsed_args) + host = parsed_args.host + binary = parsed_args.binary + reason = parsed_args.reason + res = client.services.disable(host, binary, reason) + columns = ('Host', 'Binary', 'Disabled', 'Disabled Reason') + return columns, (utils.get_dict_properties(res[1]['service'], + columns)) + + +class ForceDownService(command.ShowOne): + """Force the Zun service to down or up.""" + log = logging.getLogger(__name__ + ".ForceDownService") + + def get_parser(self, prog_name): + parser = super(ForceDownService, self).get_parser(prog_name) + parser.add_argument( + 'host', + metavar='', + help='Name of host') + parser.add_argument( + 'binary', + metavar='', + help='Name of the binary to disable') + parser.add_argument( + '--unset', + dest='force_down', + help='Unset the force state down of service', + action='store_false', + default=True) + return parser + + def take_action(self, parsed_args): + client = _get_client(self, parsed_args) + host = parsed_args.host + binary = parsed_args.binary + force_down = parsed_args.force_down + res = client.services.force_down(host, binary, force_down) + columns = ('Host', 'Binary', 'Forced_down') + return columns, (utils.get_dict_properties(res[1]['service'], + columns)) diff -Nru python-zunclient-0.2.0/zunclient/shell.py python-zunclient-0.4.0/zunclient/shell.py --- python-zunclient-0.2.0/zunclient/shell.py 2017-04-24 13:28:10.000000000 +0000 +++ python-zunclient-0.4.0/zunclient/shell.py 2017-07-28 16:11:40.000000000 +0000 @@ -52,14 +52,16 @@ except ImportError: pass +from zunclient import api_versions from zunclient.common.apiclient import auth from zunclient.common import cliutils from zunclient import exceptions as exc +from zunclient.i18n import _ from zunclient.v1 import client as client_v1 from zunclient.v1 import shell as shell_v1 from zunclient import version -DEFAULT_API_VERSION = '1' +DEFAULT_API_VERSION = '1.2' DEFAULT_ENDPOINT_TYPE = 'publicURL' DEFAULT_SERVICE_TYPE = 'container' @@ -332,8 +334,8 @@ default=cliutils.env( 'ZUN_API_VERSION', default=DEFAULT_API_VERSION), - help='Accepts "api", ' - 'defaults to env[ZUN_API_VERSION].') + help='Accepts X, X.Y (where X is major, Y is minor' + ' part), defaults to env[ZUN_API_VERSION].') parser.add_argument('--zun_api_version', help=argparse.SUPPRESS) @@ -362,11 +364,13 @@ if profiler: parser.add_argument('--profile', metavar='HMAC_KEY', + default=cliutils.env('OS_PROFILE', + default=None), help='HMAC key to use for encrypting context ' 'data for performance profiling of ' 'operation. This key should be the ' 'value of the HMAC key configured for ' - 'the OSprofiler middleware in nova; it ' + 'the OSprofiler middleware in zun; it ' 'is specified in the Zun configuration ' 'file at "/etc/zun/zun.conf". Without ' 'the key, profiling functions will not ' @@ -378,7 +382,7 @@ return parser - def get_subcommand_parser(self, version): + def get_subcommand_parser(self, version, do_help=False): parser = self.get_base_parser() self.subcommands = {} @@ -386,14 +390,14 @@ try: actions_modules = { - '1': shell_v1.COMMAND_MODULES, - }[version] + '1': shell_v1.COMMAND_MODULES + }[version.ver_major] except KeyError: actions_modules = shell_v1.COMMAND_MODULES for actions_module in actions_modules: - self._find_actions(subparsers, actions_module) - self._find_actions(subparsers, self) + self._find_actions(subparsers, actions_module, version, do_help) + self._find_actions(subparsers, self, version, do_help) self._add_bash_completion_subparser(subparsers) @@ -408,12 +412,27 @@ self.subcommands['bash_completion'] = subparser subparser.set_defaults(func=self.do_bash_completion) - def _find_actions(self, subparsers, actions_module): + def _find_actions(self, subparsers, actions_module, version, do_help): + msg = _(" (Supported by API versions '%(start)s' - '%(end)s')") for attr in (a for a in dir(actions_module) if a.startswith('do_')): # I prefer to be hyphen-separated instead of underscores. command = attr[3:].replace('_', '-') callback = getattr(actions_module, attr) desc = callback.__doc__ or '' + if hasattr(callback, "versioned"): + subs = api_versions.get_substitutions(callback) + if do_help: + desc += msg % {'start': subs[0].start_version.get_string(), + 'end': subs[-1].end_version.get_string()} + else: + for versioned_method in subs: + if version.matches(versioned_method.start_version, + versioned_method.end_version): + callback = versioned_method.func + break + else: + continue + action_help = desc.strip() arguments = getattr(callback, 'arguments', []) @@ -430,6 +449,25 @@ self.subcommands[command] = subparser for (args, kwargs) in arguments: + start_version = kwargs.get("start_version", None) + if start_version: + start_version = api_versions.APIVersion(start_version) + end_version = kwargs.get("end_version", None) + if end_version: + end_version = api_versions.APIVersion(end_version) + else: + end_version = api_versions.APIVersion( + "%s.latest" % start_version.ver_major) + if do_help: + kwargs["help"] = kwargs.get("help", "") + (msg % { + "start": start_version.get_string(), + "end": end_version.get_string()}) + else: + if not version.matches(start_version, end_version): + continue + kw = kwargs.copy() + kw.pop("start_version", None) + kw.pop("end_version", None) subparser.add_argument(*args, **kwargs) subparser.set_defaults(func=callback) @@ -456,6 +494,8 @@ (options, args) = parser.parse_known_args(argv) self.setup_debugging(options.debug) + api_version = api_versions.get_api_version(options.zun_api_version) + # NOTE(dtroyer): Hackery to handle --endpoint_type due to argparse # thinking usage-list --end is ambiguous; but it # works fine with only --endpoint-type present @@ -464,9 +504,9 @@ spot = argv.index('--endpoint_type') argv[spot] = '--endpoint-type' - subcommand_parser = ( - self.get_subcommand_parser(options.zun_api_version) - ) + subcommand_parser = self.get_subcommand_parser( + api_version, do_help=("help" in args)) + self.parser = subcommand_parser if options.help or not argv: @@ -571,7 +611,7 @@ try: client = { '1': client_v1, - }[options.zun_api_version] + }[api_version.ver_major] except KeyError: client = client_v1 @@ -593,6 +633,7 @@ zun_url=bypass_url, endpoint_type=endpoint_type, insecure=insecure, + api_version=api_version, **kwargs) args.func(self.cs, args) diff -Nru python-zunclient-0.2.0/zunclient/tests/functional/hooks/post_test_hook.sh python-zunclient-0.4.0/zunclient/tests/functional/hooks/post_test_hook.sh --- python-zunclient-0.2.0/zunclient/tests/functional/hooks/post_test_hook.sh 2017-04-24 13:28:10.000000000 +0000 +++ python-zunclient-0.4.0/zunclient/tests/functional/hooks/post_test_hook.sh 2017-07-28 16:11:40.000000000 +0000 @@ -42,23 +42,6 @@ constraints="-c $REQUIREMENTS_DIR/upper-constraints.txt" sudo -H pip install $constraints -U -r requirements.txt -r test-requirements.txt -export ZUN_DIR="$BASE/new/zun" -sudo chown -R jenkins:stack $ZUN_DIR - -# Use tempest to test zun api service - -# Import devstack functions 'iniset', 'iniget' and 'trueorfalse' -source $BASE/new/devstack/functions -echo "TEMPEST_SERVICES+=,zun" >> $localrc_path -pushd $BASE/new/tempest -sudo chown -R jenkins:stack $BASE/new/tempest - -# Missing tempest.conf ?? -# show tempest config -cat etc/tempest.conf - -sudo -E tox -eall-plugin -- zun.tests.tempest.api --concurrency=1 - echo "Running OSC commands test for Zun" export ZUNCLIENT_DIR="$BASE/new/python-zunclient" @@ -77,7 +60,5 @@ set -e -popd - $XTRACE exit $EXIT_CODE diff -Nru python-zunclient-0.2.0/zunclient/tests/functional/osc/v1/base.py python-zunclient-0.4.0/zunclient/tests/functional/osc/v1/base.py --- python-zunclient-0.2.0/zunclient/tests/functional/osc/v1/base.py 2017-04-24 13:28:10.000000000 +0000 +++ python-zunclient-0.4.0/zunclient/tests/functional/osc/v1/base.py 2017-07-28 16:11:40.000000000 +0000 @@ -56,6 +56,27 @@ self.fail('Container has not been created!') return container + def container_run(self, image='cirros', name=None, params='sleep 100000'): + """Run container and add cleanup. + + :param String image: Image for a new container + :param String name: Name for a new container + :param String params: Additional args and kwargs + :return: JSON object of created container + """ + if not name: + name = data_utils.rand_name('container') + + opts = self.get_opts() + output = self.openstack('appcontainer run {0}' + ' --name {1} {2} {3}' + .format(opts, name, image, params)) + container = json.loads(output) + + if not output: + self.fail('Container has not run!') + return container + def container_delete(self, identifier, ignore_exceptions=False): """Try to delete container by name or UUID. @@ -105,3 +126,12 @@ """ self.openstack('appcontainer rename {0} {1}' .format(identifier, name)) + + def container_execute(self, identifier, command): + """Execute in specified container. + + :param String identifier: Name or UUID of the container + :param String command: command execute in the container + """ + return self.openstack('appcontainer exec {0} {1}' + .format(identifier, command)) diff -Nru python-zunclient-0.2.0/zunclient/tests/functional/osc/v1/test_container.py python-zunclient-0.4.0/zunclient/tests/functional/osc/v1/test_container.py --- python-zunclient-0.2.0/zunclient/tests/functional/osc/v1/test_container.py 2017-04-24 13:28:10.000000000 +0000 +++ python-zunclient-0.4.0/zunclient/tests/functional/osc/v1/test_container.py 2017-07-28 16:11:40.000000000 +0000 @@ -68,7 +68,7 @@ [x['uuid'] for x in container_list]) count = 0 while count < 5: - self.container_show(container['name']) + container = self.container_show(container['name']) if container['status'] == 'Created': break if container['status'] == 'Error': @@ -109,3 +109,25 @@ self.container_rename(container['name'], new_name) container_list = self.container_list() self.assertIn(new_name, [x['name'] for x in container_list]) + + def test_execute(self): + """Check container execute command with name and UUID arguments. + + Test steps: + 1) Create container in setUp. + 2) Execute command calling it with name and UUID arguments. + 3) Check the container logs. + """ + container = self.container_run(name='test_execute') + count = 0 + while count < 50: + container = self.container_show(container['name']) + if container['status'] == 'Running': + break + if container['status'] == 'Error': + break + time.sleep(2) + count = count + 1 + command = "sh -c 'echo hello'" + result = self.container_execute(container['name'], command) + self.assertIn('hello', result) diff -Nru python-zunclient-0.2.0/zunclient/tests/test_websocketclient.py python-zunclient-0.4.0/zunclient/tests/test_websocketclient.py --- python-zunclient-0.2.0/zunclient/tests/test_websocketclient.py 2017-04-24 13:28:10.000000000 +0000 +++ python-zunclient-0.4.0/zunclient/tests/test_websocketclient.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,42 +0,0 @@ -# Copyright 2015 OpenStack LLC. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -import mock -import testtools - -from zunclient.common.websocketclient import websocketclient - -CONTAINER_ID = "0f96db5a-26dc-4550-b1a8-b110bd9247cb" -ESCAPE_FLAG = "~" -URL = "ws://localhost:2375/v1.17/containers/201e4e22c5b2/" \ - "attach/ws?logs=0&stream=1&stdin=1&stdout=1&stderr=1" -URL1 = "ws://10.10.10.10:2375/v1.17/containers/***********/" \ - "attach/ws?logs=0&stream=1&stdin=1&stdout=1&stderr=1" -WAIT_TIME = 0.5 - - -class WebSocketClientTest(testtools.TestCase): - - def test_websocketclient_variables(self): - mock_client = mock.Mock() - wsclient = websocketclient.WebSocketClient(zunclient=mock_client, - host_url=URL, - id=CONTAINER_ID, - escape=ESCAPE_FLAG, - close_wait=WAIT_TIME) - self.assertEqual(wsclient.host_url, URL) - self.assertEqual(wsclient.id, CONTAINER_ID) - self.assertEqual(wsclient.escape, ESCAPE_FLAG) - self.assertEqual(wsclient.close_wait, WAIT_TIME) diff -Nru python-zunclient-0.2.0/zunclient/tests/unit/common/test_httpclient.py python-zunclient-0.4.0/zunclient/tests/unit/common/test_httpclient.py --- python-zunclient-0.2.0/zunclient/tests/unit/common/test_httpclient.py 2017-04-24 13:28:10.000000000 +0000 +++ python-zunclient-0.4.0/zunclient/tests/unit/common/test_httpclient.py 2017-07-28 16:11:40.000000000 +0000 @@ -18,6 +18,8 @@ import mock import six + +from zunclient import api_versions from zunclient.common.apiclient import exceptions from zunclient.common import httpclient as http from zunclient import exceptions as exc @@ -68,7 +70,9 @@ six.StringIO(error_body), version=1, status=500) - client = http.HTTPClient('http://localhost/') + client = http.HTTPClient( + 'http://localhost/', + api_version=api_versions.APIVersion('1.latest')) client.get_connection = ( lambda *a, **kw: utils.FakeConnection(fake_resp)) @@ -84,7 +88,9 @@ six.StringIO(error_body), version=1, status=500) - client = http.HTTPClient('http://localhost/') + client = http.HTTPClient( + 'http://localhost/', + api_version=api_versions.APIVersion('1.latest')) client.get_connection = ( lambda *a, **kw: utils.FakeConnection(fake_resp)) @@ -102,7 +108,9 @@ six.StringIO(error_body), version=1, status=500) - client = http.HTTPClient('http://localhost/') + client = http.HTTPClient( + 'http://localhost/', + api_version=api_versions.APIVersion('1.latest')) client.get_connection = ( lambda *a, **kw: utils.FakeConnection(fake_resp)) @@ -225,7 +233,10 @@ six.StringIO(error_body), version=1, status=401) - client = http.HTTPClient('http://localhost/') + client = http.HTTPClient( + 'http://localhost/', + api_version=api_versions.APIVersion('1.latest')) + client.get_connection = (lambda *a, **kw: utils.FakeConnection(fake_resp)) @@ -245,7 +256,9 @@ error_body, 500) - client = http.SessionClient(session=fake_session) + client = http.SessionClient( + api_version=api_versions.APIVersion('1.latest'), + session=fake_session) error = self.assertRaises(exc.InternalServerError, client.json_request, @@ -264,7 +277,9 @@ error_body, 500) - client = http.SessionClient(session=fake_session) + client = http.SessionClient( + api_version=api_versions.APIVersion('1.latest'), + session=fake_session) error = self.assertRaises(exc.InternalServerError, client.json_request, @@ -279,6 +294,7 @@ fake_session.request.side_effect = [fake_response] client = http.SessionClient( + api_version=api_versions.APIVersion('1.latest'), session=fake_session, endpoint_override='http://zun') client.json_request('GET', '/v1/services') @@ -293,6 +309,7 @@ fake_session = mock.MagicMock() fake_session.request.side_effect = [fake_response] client = http.SessionClient( + api_version=api_versions.APIVersion('1.latest'), session=fake_session, endpoint_override='http://zun') self.assertRaises(exceptions.GatewayTimeout, client.json_request, diff -Nru python-zunclient-0.2.0/zunclient/tests/unit/common/test_utils.py python-zunclient-0.4.0/zunclient/tests/unit/common/test_utils.py --- python-zunclient-0.2.0/zunclient/tests/unit/common/test_utils.py 2017-04-24 13:28:10.000000000 +0000 +++ python-zunclient-0.4.0/zunclient/tests/unit/common/test_utils.py 2017-07-28 16:11:40.000000000 +0000 @@ -208,3 +208,57 @@ ('c', dict_out['c'])]) self.assertEqual(six.text_type(dict_exp), six.text_type(dict_act)) + + +class ParseNetsTest(test_utils.BaseTestCase): + + def test_no_nets(self): + nets = [] + result = utils.parse_nets(nets) + self.assertEqual([], result) + + def test_nets_with_network(self): + nets = [' network = 1234567 , v4-fixed-ip = 172.17.0.3 '] + result = utils.parse_nets(nets) + self.assertEqual([{'network': '1234567', 'v4-fixed-ip': '172.17.0.3', + 'port': '', 'v6-fixed-ip': ''}], result) + + def test_nets_with_port(self): + nets = ['port=1234567, v6-fixed-ip=2001:db8::2'] + result = utils.parse_nets(nets) + self.assertEqual([{'network': '', 'v4-fixed-ip': '', + 'port': '1234567', 'v6-fixed-ip': '2001:db8::2'}], + result) + + def test_nets_with_only_ip(self): + nets = ['v4-fixed-ip = 172.17.0.3'] + self.assertRaises(exc.CommandError, + utils.parse_nets, nets) + + def test_nets_with_both_network_port(self): + nets = ['port=1234567, network=2345678, v4-fixed-ip=172.17.0.3'] + self.assertRaises(exc.CommandError, + utils.parse_nets, nets) + + def test_nets_with_invalid_ip(self): + nets = ['network=1234567, v4-fixed-ip=23.555.567,789'] + self.assertRaises(exc.CommandError, + utils.parse_nets, nets) + + +class ParseCommandTest(test_utils.BaseTestCase): + + def test_no_command(self): + command = [] + result = utils.parse_command(command) + self.assertEqual('', result) + + def test_command_ls(self): + command = ['ls', '-al'] + result = utils.parse_command(command) + self.assertEqual('"ls" "-al"', result) + + def test_command_echo_hello(self): + command = ['sh', '-c', 'echo hello'] + result = utils.parse_command(command) + self.assertEqual('"sh" "-c" "echo hello"', result) diff -Nru python-zunclient-0.2.0/zunclient/tests/unit/osc/test_plugin.py python-zunclient-0.4.0/zunclient/tests/unit/osc/test_plugin.py --- python-zunclient-0.2.0/zunclient/tests/unit/osc/test_plugin.py 2017-04-24 13:28:10.000000000 +0000 +++ python-zunclient-0.4.0/zunclient/tests/unit/osc/test_plugin.py 2017-07-28 16:11:40.000000000 +0000 @@ -18,15 +18,18 @@ class TestContainerPlugin(base.TestCase): + @mock.patch("zunclient.api_versions.get_api_version") @mock.patch("zunclient.v1.client.Client") - def test_make_client(self, p_client): + def test_make_client(self, p_client, mock_get_api_version): instance = mock.Mock() instance._api_version = {"container": '1'} instance._region_name = 'zun_region' instance.session = 'zun_session' + mock_get_api_version.return_value = '1.2' plugin.make_client(instance) p_client.assert_called_with(region_name='zun_region', session='zun_session', - service_type='container') + service_type='container', + api_version='1.2') diff -Nru python-zunclient-0.2.0/zunclient/tests/unit/test_api_versions.py python-zunclient-0.4.0/zunclient/tests/unit/test_api_versions.py --- python-zunclient-0.2.0/zunclient/tests/unit/test_api_versions.py 1970-01-01 00:00:00.000000000 +0000 +++ python-zunclient-0.4.0/zunclient/tests/unit/test_api_versions.py 2017-07-28 16:11:40.000000000 +0000 @@ -0,0 +1,246 @@ +# Copyright 2015 Mirantis +# 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 mock + +from zunclient import api_versions +from zunclient import exceptions +from zunclient.tests.unit import utils + + +class APIVersionTestCase(utils.TestCase): + def test_valid_version_strings(self): + def _test_string(version, exp_major, exp_minor): + v = api_versions.APIVersion(version) + self.assertEqual(v.ver_major, exp_major) + self.assertEqual(v.ver_minor, exp_minor) + + _test_string("1.1", 1, 1) + _test_string("2.10", 2, 10) + _test_string("5.234", 5, 234) + _test_string("12.5", 12, 5) + _test_string("2.0", 2, 0) + _test_string("2.200", 2, 200) + + def test_null_version(self): + v = api_versions.APIVersion() + self.assertTrue(v.is_null()) + + def test_invalid_version_strings(self): + self.assertRaises(exceptions.UnsupportedVersion, + api_versions.APIVersion, "2") + + self.assertRaises(exceptions.UnsupportedVersion, + api_versions.APIVersion, "200") + + self.assertRaises(exceptions.UnsupportedVersion, + api_versions.APIVersion, "2.1.4") + + self.assertRaises(exceptions.UnsupportedVersion, + api_versions.APIVersion, "200.23.66.3") + + self.assertRaises(exceptions.UnsupportedVersion, + api_versions.APIVersion, "5 .3") + + self.assertRaises(exceptions.UnsupportedVersion, + api_versions.APIVersion, "5. 3") + + self.assertRaises(exceptions.UnsupportedVersion, + api_versions.APIVersion, "5.03") + + self.assertRaises(exceptions.UnsupportedVersion, + api_versions.APIVersion, "02.1") + + self.assertRaises(exceptions.UnsupportedVersion, + api_versions.APIVersion, "2.001") + + self.assertRaises(exceptions.UnsupportedVersion, + api_versions.APIVersion, "") + + self.assertRaises(exceptions.UnsupportedVersion, + api_versions.APIVersion, " 2.1") + + self.assertRaises(exceptions.UnsupportedVersion, + api_versions.APIVersion, "2.1 ") + + def test_version_comparisons(self): + v1 = api_versions.APIVersion("2.0") + v2 = api_versions.APIVersion("2.5") + v3 = api_versions.APIVersion("5.23") + v4 = api_versions.APIVersion("2.0") + v_null = api_versions.APIVersion() + + self.assertTrue(v1 < v2) + self.assertTrue(v3 > v2) + self.assertTrue(v1 != v2) + self.assertTrue(v1 == v4) + self.assertTrue(v1 != v_null) + self.assertTrue(v_null == v_null) + self.assertRaises(TypeError, v1.__le__, "2.1") + + def test_version_matches(self): + v1 = api_versions.APIVersion("2.0") + v2 = api_versions.APIVersion("2.5") + v3 = api_versions.APIVersion("2.45") + v4 = api_versions.APIVersion("3.3") + v5 = api_versions.APIVersion("3.23") + v6 = api_versions.APIVersion("2.0") + v7 = api_versions.APIVersion("3.3") + v8 = api_versions.APIVersion("4.0") + v_null = api_versions.APIVersion() + + self.assertTrue(v2.matches(v1, v3)) + self.assertTrue(v2.matches(v1, v_null)) + self.assertTrue(v1.matches(v6, v2)) + self.assertTrue(v4.matches(v2, v7)) + self.assertTrue(v4.matches(v_null, v7)) + self.assertTrue(v4.matches(v_null, v8)) + self.assertFalse(v1.matches(v2, v3)) + self.assertFalse(v5.matches(v2, v4)) + self.assertFalse(v2.matches(v3, v1)) + + self.assertRaises(ValueError, v_null.matches, v1, v3) + + def test_get_string(self): + v1_string = "3.23" + v1 = api_versions.APIVersion(v1_string) + self.assertEqual(v1_string, v1.get_string()) + + self.assertRaises(ValueError, + api_versions.APIVersion().get_string) + + +class UpdateHeadersTestCase(utils.TestCase): + def test_api_version_is_null(self): + headers = {} + api_versions.update_headers(headers, api_versions.APIVersion()) + self.assertEqual({}, headers) + + def test_api_version_is_major(self): + headers = {} + api_versions.update_headers(headers, api_versions.APIVersion("7.0")) + self.assertEqual({}, headers) + + def test_api_version_is_not_null(self): + api_version = api_versions.APIVersion("2.3") + headers = {} + api_versions.update_headers(headers, api_version) + self.assertEqual( + {"OpenStack-API-Version": + "container %s" % api_version.get_string()}, + headers) + + +class GetAPIVersionTestCase(utils.TestCase): + def test_get_available_client_versions(self): + output = api_versions.get_available_major_versions() + self.assertNotEqual([], output) + + def test_wrong_format(self): + self.assertRaises(exceptions.UnsupportedVersion, + api_versions.get_api_version, "something_wrong") + + @mock.patch("zunclient.api_versions.APIVersion") + def test_only_major_part_is_presented(self, mock_apiversion): + version = 7 + self.assertEqual(mock_apiversion.return_value, + api_versions.get_api_version(version)) + mock_apiversion.assert_called_once_with("%s.0" % str(version)) + + @mock.patch("zunclient.api_versions.APIVersion") + def test_major_and_minor_parts_is_presented(self, mock_apiversion): + version = "2.7" + self.assertEqual(mock_apiversion.return_value, + api_versions.get_api_version(version)) + mock_apiversion.assert_called_once_with(version) + + +class WrapsTestCase(utils.TestCase): + + def _get_obj_with_vers(self, vers): + return mock.MagicMock(api_version=api_versions.APIVersion(vers)) + + def _side_effect_of_vers_method(self, *args, **kwargs): + m = mock.MagicMock(start_version=args[1], end_version=args[2]) + m.name = args[0] + return m + + @mock.patch("zunclient.api_versions._get_function_name") + @mock.patch("zunclient.api_versions.VersionedMethod") + def test_end_version_is_none(self, mock_versioned_method, mock_name): + func_name = "foo" + mock_name.return_value = func_name + mock_versioned_method.side_effect = self._side_effect_of_vers_method + + @api_versions.wraps("2.2") + def foo(*args, **kwargs): + pass + + foo(self._get_obj_with_vers("2.4")) + + mock_versioned_method.assert_called_once_with( + func_name, api_versions.APIVersion("2.2"), + api_versions.APIVersion("2.latest"), mock.ANY) + + @mock.patch("zunclient.api_versions._get_function_name") + @mock.patch("zunclient.api_versions.VersionedMethod") + def test_start_and_end_version_are_presented(self, mock_versioned_method, + mock_name): + func_name = "foo" + mock_name.return_value = func_name + mock_versioned_method.side_effect = self._side_effect_of_vers_method + + @api_versions.wraps("2.2", "2.6") + def foo(*args, **kwargs): + pass + + foo(self._get_obj_with_vers("2.4")) + + mock_versioned_method.assert_called_once_with( + func_name, api_versions.APIVersion("2.2"), + api_versions.APIVersion("2.6"), mock.ANY) + + @mock.patch("zunclient.api_versions._get_function_name") + @mock.patch("zunclient.api_versions.VersionedMethod") + def test_api_version_doesnt_match(self, mock_versioned_method, mock_name): + func_name = "foo" + mock_name.return_value = func_name + mock_versioned_method.side_effect = self._side_effect_of_vers_method + + @api_versions.wraps("2.2", "2.6") + def foo(*args, **kwargs): + pass + + self.assertRaises(exceptions.VersionNotFoundForAPIMethod, + foo, self._get_obj_with_vers("2.1")) + + mock_versioned_method.assert_called_once_with( + func_name, api_versions.APIVersion("2.2"), + api_versions.APIVersion("2.6"), mock.ANY) + + def test_define_method_is_actually_called(self): + checker = mock.MagicMock() + + @api_versions.wraps("2.2", "2.6") + def some_func(*args, **kwargs): + checker(*args, **kwargs) + + obj = self._get_obj_with_vers("2.4") + some_args = ("arg_1", "arg_2") + some_kwargs = {"key1": "value1", "key2": "value2"} + + some_func(obj, *some_args, **some_kwargs) + + checker.assert_called_once_with(*((obj,) + some_args), **some_kwargs) diff -Nru python-zunclient-0.2.0/zunclient/tests/unit/test_shell.py python-zunclient-0.4.0/zunclient/tests/unit/test_shell.py --- python-zunclient-0.2.0/zunclient/tests/unit/test_shell.py 2017-04-24 13:28:10.000000000 +0000 +++ python-zunclient-0.4.0/zunclient/tests/unit/test_shell.py 2017-07-28 16:11:40.000000000 +0000 @@ -21,6 +21,7 @@ import six from testtools import matchers +from zunclient import api_versions from zunclient import exceptions import zunclient.shell from zunclient.tests.unit import utils @@ -202,13 +203,12 @@ _, create_args = mock_client.return_value.containers.create.call_args self.assertEqual({'key': 'value'}, create_args['environment']) - @mock.patch('zunclient.v1.services_shell.do_service_list') - @mock.patch('zunclient.v1.client.ksa_session') - def test_insecure(self, mock_session, mock_services_list): + @mock.patch('zunclient.v1.client.Client') + def test_insecure(self, mock_client): self.make_env() self.shell('--insecure service-list') - _, session_kwargs = mock_session.Session.call_args_list[0] - self.assertEqual(False, session_kwargs['verify']) + _, session_kwargs = mock_client.call_args_list[0] + self.assertEqual(True, session_kwargs['insecure']) @mock.patch('sys.stdin', side_effect=mock.MagicMock) @mock.patch('getpass.getpass', side_effect=EOFError) @@ -248,7 +248,8 @@ service_type='container', region_name=expected_region_name, project_domain_id='', project_domain_name='', user_domain_id='', user_domain_name='', profile=None, - zun_url=None, insecure=False) + zun_url=None, insecure=False, + api_version=api_versions.APIVersion('1.2')) def test_main_option_region(self): self.make_env() @@ -275,7 +276,8 @@ service_type='container', region_name=None, project_domain_id='', project_domain_name='', user_domain_id='', user_domain_name='', profile=None, - zun_url=None, insecure=False) + zun_url=None, insecure=False, + api_version=api_versions.APIVersion('1.2')) @mock.patch('zunclient.v1.client.Client') def test_main_endpoint_internal(self, mock_client): @@ -288,7 +290,8 @@ service_type='container', region_name=None, project_domain_id='', project_domain_name='', user_domain_id='', user_domain_name='', profile=None, - zun_url=None, insecure=False) + zun_url=None, insecure=False, + api_version=api_versions.APIVersion('1.2')) class ShellTestKeystoneV3(ShellTest): @@ -318,4 +321,5 @@ service_type='container', region_name=None, project_domain_id='', project_domain_name='Default', user_domain_id='', user_domain_name='Default', - zun_url=None, insecure=False, profile=None) + zun_url=None, insecure=False, profile=None, + api_version=api_versions.APIVersion('1.2')) diff -Nru python-zunclient-0.2.0/zunclient/tests/unit/test_websocketclient.py python-zunclient-0.4.0/zunclient/tests/unit/test_websocketclient.py --- python-zunclient-0.2.0/zunclient/tests/unit/test_websocketclient.py 1970-01-01 00:00:00.000000000 +0000 +++ python-zunclient-0.4.0/zunclient/tests/unit/test_websocketclient.py 2017-07-28 16:11:40.000000000 +0000 @@ -0,0 +1,42 @@ +# Copyright 2015 OpenStack LLC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import mock +import testtools + +from zunclient.common.websocketclient import websocketclient + +CONTAINER_ID = "0f96db5a-26dc-4550-b1a8-b110bd9247cb" +ESCAPE_FLAG = "~" +URL = "ws://localhost:2375/v1.17/containers/201e4e22c5b2/" \ + "attach/ws?logs=0&stream=1&stdin=1&stdout=1&stderr=1" +URL1 = "ws://10.10.10.10:2375/v1.17/containers/***********/" \ + "attach/ws?logs=0&stream=1&stdin=1&stdout=1&stderr=1" +WAIT_TIME = 0.5 + + +class WebSocketClientTest(testtools.TestCase): + + def test_websocketclient_variables(self): + mock_client = mock.Mock() + wsclient = websocketclient.WebSocketClient(zunclient=mock_client, + url=URL, + id=CONTAINER_ID, + escape=ESCAPE_FLAG, + close_wait=WAIT_TIME) + self.assertEqual(wsclient.url, URL) + self.assertEqual(wsclient.id, CONTAINER_ID) + self.assertEqual(wsclient.escape, ESCAPE_FLAG) + self.assertEqual(wsclient.close_wait, WAIT_TIME) diff -Nru python-zunclient-0.2.0/zunclient/tests/unit/v1/test_client.py python-zunclient-0.4.0/zunclient/tests/unit/v1/test_client.py --- python-zunclient-0.2.0/zunclient/tests/unit/v1/test_client.py 2017-04-24 13:28:10.000000000 +0000 +++ python-zunclient-0.4.0/zunclient/tests/unit/v1/test_client.py 2017-07-28 16:11:40.000000000 +0000 @@ -31,7 +31,8 @@ region_name=None, service_name=None, service_type='container', - session=session) + session=session, + api_version=None) @mock.patch('zunclient.common.httpclient.SessionClient') @mock.patch('keystoneauth1.token_endpoint.Token') @@ -51,7 +52,8 @@ region_name=None, service_name=None, service_type='container', - session=session) + session=session, + api_version=None) @mock.patch('zunclient.common.httpclient.SessionClient') @mock.patch('keystoneauth1.loading.get_plugin_loader') @@ -76,7 +78,8 @@ region_name=None, service_name=None, service_type='container', - session=mock.ANY) + session=mock.ANY, + api_version=None) @mock.patch('zunclient.common.httpclient.SessionClient') @mock.patch('keystoneauth1.loading.get_plugin_loader') @@ -102,7 +105,8 @@ region_name=None, service_name=None, service_type='container', - session=mock.ANY) + session=mock.ANY, + api_version=None) @mock.patch('zunclient.common.httpclient.SessionClient') @mock.patch('keystoneauth1.loading.get_plugin_loader') @@ -142,7 +146,8 @@ service_name=None, service_type='container', session=session, - endpoint_override='zunurl') + endpoint_override='zunurl', + api_version=None) @mock.patch('zunclient.common.httpclient.SessionClient') @mock.patch('keystoneauth1.session.Session') @@ -158,4 +163,5 @@ service_name=None, service_type='container', session=session, - endpoint_override='zunurl') + endpoint_override='zunurl', + api_version=None) diff -Nru python-zunclient-0.2.0/zunclient/tests/unit/v1/test_containers.py python-zunclient-0.4.0/zunclient/tests/unit/v1/test_containers.py --- python-zunclient-0.2.0/zunclient/tests/unit/v1/test_containers.py 2017-04-24 13:28:10.000000000 +0000 +++ python-zunclient-0.4.0/zunclient/tests/unit/v1/test_containers.py 2017-07-28 16:11:40.000000000 +0000 @@ -23,13 +23,15 @@ 'name': 'test1', 'image_pull_policy': 'never', 'image': 'cirros', - 'command': 'sleep 100000000', + 'command': 'sh -c "echo hello"', 'cpu': '1', 'memory': '256', - 'environment': 'hostname=zunsystem', + 'environment': {'hostname': 'zunsystem'}, 'workdir': '/', - 'labels': 'faketest', + 'labels': {'label1': 'foo'}, + 'hints': {'hint1': 'bar'}, 'restart_policy': 'no', + 'security_groups': ['test'], } CONTAINER2 = {'id': '1235', @@ -40,10 +42,12 @@ 'command': 'sleep 100000000', 'cpu': '1', 'memory': '256', - 'environment': 'hostname=zunsystem', + 'environment': {'hostname': 'zunsystem'}, 'workdir': '/', - 'labels': 'faketest', + 'labels': {'label2': 'foo'}, + 'hints': {'hint2': 'bar'}, 'restart_policy': 'on-failure:5', + 'security_groups': ['test'], } CREATE_CONTAINER1 = copy.deepcopy(CONTAINER1) @@ -52,6 +56,7 @@ force_delete1 = False force_delete2 = True +all_tenants = True signal = "SIGTERM" name = "new-name" timeout = 10 @@ -59,6 +64,8 @@ tty_width = "121" path = "/tmp/test.txt" data = "/tmp/test.tar" +repo = "repo-test" +tag = "tag-test" fake_responses = { '/v1/containers': @@ -149,6 +156,13 @@ None, ), }, + '/v1/containers/%s?all_tenants=%s' % (CONTAINER1['id'], all_tenants): + { + 'DELETE': ( + {}, + None, + ), + }, '/v1/containers/%s/stop?timeout=10' % CONTAINER1['id']: { 'POST': ( @@ -264,6 +278,24 @@ {'data': data}, ), }, + '/v1/containers/%s/stats?%s' + % (CONTAINER1['id'], parse.urlencode({'decode': False, + 'stream': False})): + { + 'GET': ( + {}, + None, + ), + }, + '/v1/containers/%s/commit?%s' + % (CONTAINER1['id'], parse.urlencode({'repository': repo, + 'tag': tag})): + { + 'POST': ( + {}, + None, + ), + }, } @@ -285,10 +317,10 @@ def test_container_create_fail(self): create_container_fail = copy.deepcopy(CREATE_CONTAINER1) create_container_fail["wrong_key"] = "wrong" - self.assertRaisesRegexp(exceptions.InvalidAttribute, - ("Key must be in %s" % - ','.join(containers.CREATION_ATTRIBUTES)), - self.mgr.create, **create_container_fail) + self.assertRaisesRegex(exceptions.InvalidAttribute, + ("Key must be in %s" % + ','.join(containers.CREATION_ATTRIBUTES)), + self.mgr.create, **create_container_fail) self.assertEqual([], self.api.calls) def test_containers_list(self): @@ -387,7 +419,7 @@ self.assertTrue(containers) def test_containers_delete(self): - containers = self.mgr.delete(CONTAINER1['id'], force_delete1) + containers = self.mgr.delete(CONTAINER1['id'], force=force_delete1) expect = [ ('DELETE', '/v1/containers/%s?force=%s' % (CONTAINER1['id'], force_delete1), @@ -397,7 +429,7 @@ self.assertIsNone(containers) def test_containers_delete_with_force(self): - containers = self.mgr.delete(CONTAINER1['id'], force_delete2) + containers = self.mgr.delete(CONTAINER1['id'], force=force_delete2) expect = [ ('DELETE', '/v1/containers/%s?force=%s' % (CONTAINER1['id'], force_delete2), @@ -406,6 +438,16 @@ self.assertEqual(expect, self.api.calls) self.assertIsNone(containers) + def test_containers_delete_with_all_tenants(self): + containers = self.mgr.delete(CONTAINER1['id'], all_tenants=all_tenants) + expect = [ + ('DELETE', '/v1/containers/%s?all_tenants=%s' % (CONTAINER1['id'], + all_tenants), + {}, None) + ] + self.assertEqual(expect, self.api.calls) + self.assertIsNone(containers) + def test_containers_stop(self): containers = self.mgr.stop(CONTAINER1['id'], timeout) expect = [ @@ -458,7 +500,8 @@ self.assertIsNone(containers) def test_containers_execute(self): - containers = self.mgr.execute(CONTAINER1['id'], CONTAINER1['command']) + containers = self.mgr.execute(CONTAINER1['id'], + command=CONTAINER1['command']) expect = [ ('POST', '/v1/containers/%s/execute?%s' % (CONTAINER1['id'], parse.urlencode({'command': @@ -489,10 +532,10 @@ def test_container_run_fail(self): run_container_fail = copy.deepcopy(CREATE_CONTAINER1) run_container_fail["wrong_key"] = "wrong" - self.assertRaisesRegexp(exceptions.InvalidAttribute, - ("Key must be in %s" % - ','.join(containers.CREATION_ATTRIBUTES)), - self.mgr.run, **run_container_fail) + self.assertRaisesRegex(exceptions.InvalidAttribute, + ("Key must be in %s" % + ','.join(containers.CREATION_ATTRIBUTES)), + self.mgr.run, **run_container_fail) self.assertEqual([], self.api.calls) def test_containers_rename(self): @@ -559,3 +602,13 @@ ] self.assertEqual(expect, self.api.calls) self.assertTrue(containers) + + def test_containers_commit(self): + containers = self.mgr.commit(CONTAINER1['id'], repo, tag) + expect = [ + ('POST', '/v1/containers/%s/commit?%s' % (CONTAINER1['id'], + parse.urlencode({'repository': repo, 'tag': tag})), + {'Content-Length': '0'}, None) + ] + self.assertEqual(expect, self.api.calls) + self.assertIsNone(containers) diff -Nru python-zunclient-0.2.0/zunclient/tests/unit/v1/test_containers_shell.py python-zunclient-0.4.0/zunclient/tests/unit/v1/test_containers_shell.py --- python-zunclient-0.2.0/zunclient/tests/unit/v1/test_containers_shell.py 2017-04-24 13:28:10.000000000 +0000 +++ python-zunclient-0.4.0/zunclient/tests/unit/v1/test_containers_shell.py 2017-07-28 16:11:40.000000000 +0000 @@ -13,6 +13,7 @@ import mock from zunclient.common import utils as zun_utils +from zunclient.common.websocketclient import exceptions from zunclient.tests.unit.v1 import shell_test_base from zunclient.v1 import containers_shell @@ -133,3 +134,18 @@ 'run --image-pull-policy wrong x', self._invalid_choice_error) self.assertFalse(mock_run.called) + + @mock.patch('zunclient.v1.containers.ContainerManager.get') + @mock.patch('zunclient.v1.containers_shell._show_container') + @mock.patch('zunclient.v1.containers.ContainerManager.run') + def test_zun_container_run_interactive(self, mock_run, + mock_show_container, + mock_get_container): + fake_container = mock.MagicMock() + fake_container.uuid = 'fake_uuid' + mock_run.return_value = fake_container + fake_container.status = 'Error' + mock_get_container.return_value = fake_container + self.assertRaises(exceptions.ContainerStateError, + self.shell, + 'run -i x ') diff -Nru python-zunclient-0.2.0/zunclient/v1/client.py python-zunclient-0.4.0/zunclient/v1/client.py --- python-zunclient-0.2.0/zunclient/v1/client.py 2017-04-24 13:28:10.000000000 +0000 +++ python-zunclient-0.4.0/zunclient/v1/client.py 2017-07-28 16:11:40.000000000 +0000 @@ -35,7 +35,7 @@ interface='public', service_name=None, insecure=False, user_domain_id=None, user_domain_name=None, project_domain_id=None, project_domain_name=None, - **kwargs): + api_version=None, **kwargs): # We have to keep the api_key are for backwards compat, but let's # remove it from the rest of our code since it's not a keystone @@ -111,6 +111,7 @@ interface=interface, region_name=region_name, session=session, + api_version=api_version, **client_kwargs) self.containers = containers.ContainerManager(self.http_client) self.images = images.ImageManager(self.http_client) diff -Nru python-zunclient-0.2.0/zunclient/v1/containers.py python-zunclient-0.4.0/zunclient/v1/containers.py --- python-zunclient-0.2.0/zunclient/v1/containers.py 2017-04-24 13:28:10.000000000 +0000 +++ python-zunclient-0.4.0/zunclient/v1/containers.py 2017-07-28 16:11:40.000000000 +0000 @@ -21,7 +21,8 @@ CREATION_ATTRIBUTES = ['name', 'image', 'command', 'cpu', 'memory', 'environment', 'workdir', 'labels', 'image_pull_policy', - 'restart_policy', 'interactive', 'image_driver'] + 'restart_policy', 'interactive', 'image_driver', + 'security_groups', 'hints', 'nets'] class Container(base.Resource): @@ -89,9 +90,10 @@ "containers", limit=limit) - def get(self, id): + def get(self, id, **kwargs): try: - return self._list(self._path(id))[0] + return self._list(self._path(id), + qparams=kwargs)[0] except IndexError: return None @@ -105,9 +107,9 @@ "Key must be in %s" % ','.join(CREATION_ATTRIBUTES)) return self._create(self._path(), new) - def delete(self, id, force): + def delete(self, id, **kwargs): return self._delete(self._path(id), - qparams={'force': force}) + qparams=kwargs) def _action(self, id, action, method='POST', qparams=None, **kwargs): if qparams: @@ -144,9 +146,13 @@ return self._action(id, '/logs', method='GET', qparams=kwargs)[1] - def execute(self, id, command): + def execute(self, id, **kwargs): return self._action(id, '/execute', - qparams={'command': command})[1] + qparams=kwargs)[1] + + def execute_resize(self, id, exec_id, width, height): + self._action(id, '/execute_resize', + qparams={'exec_id': exec_id, 'w': width, 'h': height})[1] def kill(self, id, signal=None): return self._action(id, '/kill', @@ -185,3 +191,14 @@ return self._action(id, '/put_archive', qparams={'path': path}, body={'data': data}) + + def stats(self, id): + return self._action(id, '/stats', method='GET')[1] + + def commit(self, id, repository, tag=None): + if tag is not None: + return self._action(id, '/commit', qparams={ + 'repository': repository, 'tag': tag})[1] + else: + return self._action(id, '/commit', qparams={ + 'repository': repository})[1] diff -Nru python-zunclient-0.2.0/zunclient/v1/containers_shell.py python-zunclient-0.4.0/zunclient/v1/containers_shell.py --- python-zunclient-0.2.0/zunclient/v1/containers_shell.py 2017-04-24 13:28:10.000000000 +0000 +++ python-zunclient-0.4.0/zunclient/v1/containers_shell.py 2017-07-28 16:11:40.000000000 +0000 @@ -63,7 +63,7 @@ 'It can have following values: ' '"ifnotpresent": only pull the image if it does not ' 'already exist on the node. ' - '"always": Always pull the image from repositery.' + '"always": Always pull the image from repository.' '"never": never pull the image') @utils.arg('image', metavar='', help='name or ID of the image') @utils.arg('--restart', @@ -81,10 +81,34 @@ 'It can have following values: ' '"docker": pull the image from Docker Hub. ' '"glance": pull the image from Glance. ') +@utils.arg('--security-group', + metavar='security-group', + action='append', default=[], + help='The name of security group for the container. ' + 'May be used multiple times.') @utils.arg('command', metavar='', nargs=argparse.REMAINDER, help='Send command to the container') +@utils.arg('--hint', + action='append', + default=[], + metavar='', + help='The key-value pair(s) for scheduler to select host. ' + 'The format of this parameter is "key=value[,key=value]". ' + 'May be used multiple times.') +@utils.arg('--net', + action='append', + default=[], + metavar='', + help='Create network enpoints for the container. ' + 'auto: do not specify the network, zun will automatically ' + 'create one. ' + 'network: attach container to the specified neturon networks. ' + 'port: attach container to the neutron port with this UUID. ' + 'v4-fixed-ip: IPv4 fixed address for container. ' + 'v6-fixed-ip: IPv6 fixed address for container.') def do_create(cs, args): """Create a container.""" opts = {} @@ -97,8 +121,13 @@ opts['labels'] = zun_utils.format_args(args.label) opts['image_pull_policy'] = args.image_pull_policy opts['image_driver'] = args.image_driver + opts['hints'] = zun_utils.format_args(args.hint) + opts['nets'] = zun_utils.parse_nets(args.net) + + if args.security_group: + opts['security_groups'] = args.security_group if args.command: - opts['command'] = ' '.join(args.command) + opts['command'] = zun_utils.parse_command(args.command) if args.restart: opts['restart_policy'] = zun_utils.check_restart_policy(args.restart) if args.interactive: @@ -147,11 +176,20 @@ @utils.arg('-f', '--force', action='store_true', help='Force delete the container.') +@utils.arg('--all-tenants', + action="store_true", + default=False, + help='Delete container(s) in all tenant by name.') def do_delete(cs, args): """Delete specified containers.""" for container in args.containers: + opts = {} + opts['id'] = container + opts['force'] = args.force + opts['all_tenants'] = args.all_tenants + opts = zun_utils.remove_null_parms(**opts) try: - cs.containers.delete(container, args.force) + cs.containers.delete(**opts) print("Request to delete container %s has been accepted." % container) except Exception as e: @@ -170,9 +208,17 @@ help='Print representation of the container.' 'The choices of the output format is json,table,yaml.' 'Defaults to table.') +@utils.arg('--all-tenants', + action="store_true", + default=False, + help='Show container(s) in all tenant by name.') def do_show(cs, args): """Show details of a container.""" - container = cs.containers.get(args.container) + opts = {} + opts['id'] = args.container + opts['all_tenants'] = args.all_tenants + opts = zun_utils.remove_null_parms(**opts) + container = cs.containers.get(**opts) if args.format == 'json': print(json.dumps(container._info, indent=4, sort_keys=True)) elif args.format == 'yaml': @@ -313,13 +359,28 @@ metavar='', nargs=argparse.REMAINDER, help='The command to execute in a container') +@utils.arg('-i', '--interactive', + dest='interactive', + action='store_true', + default=False, + help='Keep STDIN open and allocate a pseudo-TTY for interactive') def do_exec(cs, args): """Execute command in a running container.""" - response = cs.containers.execute(args.container, ' '.join(args.command)) - output = response['output'] - exit_code = response['exit_code'] - print(output) - return exit_code + opts = {} + opts['command'] = zun_utils.parse_command(args.command) + if args.interactive: + opts['interactive'] = True + opts['run'] = False + response = cs.containers.execute(args.container, **opts) + if args.interactive: + exec_id = response['exec_id'] + url = response['url'] + websocketclient.do_exec(cs, url, args.container, exec_id, "~", 0.5) + else: + output = response['output'] + exit_code = response['exit_code'] + print(output) + return exit_code @utils.arg('containers', @@ -378,7 +439,7 @@ 'It can have following values: ' '"ifnotpresent": only pull the image if it does not ' 'already exist on the node. ' - '"always": Always pull the image from repositery.' + '"always": Always pull the image from repository.' '"never": never pull the image') @utils.arg('image', metavar='', help='name or ID of the image') @utils.arg('--restart', @@ -396,10 +457,34 @@ 'It can have following values: ' '"docker": pull the image from Docker Hub. ' '"glance": pull the image from Glance. ') +@utils.arg('--security-group', + metavar='security-group', + action='append', default=[], + help='The name of security group for the container. ' + 'May be used multiple times.') @utils.arg('command', metavar='', nargs=argparse.REMAINDER, help='Send command to the container') +@utils.arg('--hint', + action='append', + default=[], + metavar='', + help='The key-value pair(s) for scheduler to select host. ' + 'The format of this parameter is "key=value[,key=value]". ' + 'May be used multiple times.') +@utils.arg('--net', + action='append', + default=[], + metavar='', + help='Create network enpoints for the container. ' + 'auto: do not specify the network, zun will automatically ' + 'create one. ' + 'network: attach container to the specified neutron networks. ' + 'port: attach container to the neutron port with this UUID. ' + 'v4-fixed-ip: IPv4 fixed address for container. ' + 'v6-fixed-ip: IPv6 fixed address for container.') def do_run(cs, args): """Run a command in a new container.""" opts = {} @@ -412,8 +497,13 @@ opts['labels'] = zun_utils.format_args(args.label) opts['image_pull_policy'] = args.image_pull_policy opts['image_driver'] = args.image_driver + opts['hints'] = zun_utils.format_args(args.hint) + opts['nets'] = zun_utils.parse_nets(args.net) + + if args.security_group: + opts['security_groups'] = args.security_group if args.command: - opts['command'] = ' '.join(args.command) + opts['command'] = zun_utils.parse_command(args.command) if args.restart: opts['restart_policy'] = zun_utils.check_restart_policy(args.restart) if args.interactive: @@ -430,7 +520,7 @@ ready_for_attach = True break if zun_utils.check_container_status(container, 'Error'): - break + raise exceptions.ContainerStateError(container_uuid) print("Waiting for container start") time.sleep(1) if ready_for_attach is True: @@ -453,7 +543,7 @@ @utils.arg('container', metavar='', - help="ID or name of the container to udate.") + help="ID or name of the container to update.") @utils.arg('--cpu', metavar='', help='The number of virtual cpus.') @@ -474,7 +564,7 @@ @utils.arg('container', metavar='', - help='ID or name of the container to be attahed to.') + help='ID or name of the container to be attached to.') def do_attach(cs, args): """Attach to a running container.""" response = cs.containers.attach(args.container) @@ -542,3 +632,38 @@ print("Usage:") print("zun cp container:src_path dest_path|-") print("zun cp src_path|- container:dest_path") + + +@utils.arg('container', + metavar='', + help='ID or name of the container to display stats.') +def do_stats(cs, args): + """Display stats snapshot of the container.""" + stats_info = cs.containers.stats(args.container) + utils.print_dict(stats_info) + + +@utils.arg('container', + metavar='', + help='ID or name of the container to commit.') +@utils.arg('--repository', + metavar='', + required=True, + help='The repository of the image.') +@utils.arg('--tag', + metavar='', + help='The tag of the image') +def do_commit(cs, args): + """Create a new image from a container's changes.""" + opts = {} + if args.repository is not None: + opts['repository'] = args.repository + if args.tag is not None: + opts['tag'] = args.tag + try: + image = cs.containers.commit(args.container, **opts) + print("Request to commit container %s has been accepted. " + "The image is %s." % (args.container, image)) + except Exception as e: + print("Commit for container %(container)s failed: %(e)s" % + {'container': args.container, 'e': e}) diff -Nru python-zunclient-0.2.0/zunclient/v1/images_shell.py python-zunclient-0.4.0/zunclient/v1/images_shell.py --- python-zunclient-0.2.0/zunclient/v1/images_shell.py 2017-04-24 13:28:10.000000000 +0000 +++ python-zunclient-0.4.0/zunclient/v1/images_shell.py 2017-07-28 16:11:40.000000000 +0000 @@ -18,13 +18,13 @@ utils.print_dict(image._info) -@utils.arg('repo', - metavar='', - help='Image repository') +@utils.arg('image', + metavar='', + help='Name of the image') def do_pull(cs, args): """Pull an image.""" opts = {} - opts['repo'] = args.repo + opts['repo'] = args.image _show_image(cs.images.create(**opts)) diff -Nru python-zunclient-0.2.0/zunclient/v1/services.py python-zunclient-0.4.0/zunclient/v1/services.py --- python-zunclient-0.2.0/zunclient/v1/services.py 2017-04-24 13:28:10.000000000 +0000 +++ python-zunclient-0.4.0/zunclient/v1/services.py 2017-07-28 16:11:40.000000000 +0000 @@ -10,6 +10,8 @@ # License for the specific language governing permissions and limitations # under the License. +from six.moves.urllib import parse + from zunclient.common import base from zunclient.common import utils @@ -69,3 +71,45 @@ else: return self._list_pagination(self._path(path), "services", limit=limit) + + def delete(self, host, binary): + """Delete a service.""" + return self._delete(self._path(), + qparams={'host': host, + 'binary': binary}) + + def _action(self, action, method='PUT', qparams=None, **kwargs): + if qparams: + action = "%s?%s" % (action, + parse.urlencode(qparams)) + kwargs.setdefault('headers', {}) + kwargs['headers'].setdefault('Content-Length', '0') + resp, body = self.api.json_request(method, + self._path() + action, + **kwargs) + return resp, body + + def _update_body(self, host, binary, disabled_reason=None, + force_down=None): + body = {"host": host, + "binary": binary} + if disabled_reason is not None: + body["disabled_reason"] = disabled_reason + if force_down is not None: + body["forced_down"] = force_down + return body + + def enable(self, host, binary): + """Enable the service specified by hostname and binary.""" + body = self._update_body(host, binary) + return self._action("/enable", qparams=body) + + def disable(self, host, binary, reason=None): + """Disable the service specified by hostname and binary.""" + body = self._update_body(host, binary, reason) + return self._action("/disable", qparams=body) + + def force_down(self, host, binary, force_down=None): + """Force service state to down specified by hostname and binary.""" + body = self._update_body(host, binary, force_down=force_down) + return self._action("/force_down", qparams=body) diff -Nru python-zunclient-0.2.0/zunclient/v1/services_shell.py python-zunclient-0.4.0/zunclient/v1/services_shell.py --- python-zunclient-0.2.0/zunclient/v1/services_shell.py 2017-04-24 13:28:10.000000000 +0000 +++ python-zunclient-0.4.0/zunclient/v1/services_shell.py 2017-07-28 16:11:40.000000000 +0000 @@ -24,3 +24,54 @@ 'Disabled Reason', 'Created At', 'Updated At') utils.print_list(services, columns, {'versions': zun_utils.print_list_field('versions')}) + + +@utils.arg('host', + metavar='', + help='Name of host.') +@utils.arg('binary', + metavar='', + help='Name of the binary to delete.') +def do_service_delete(cs, args): + """Delete the Zun binaries/services.""" + try: + cs.services.delete(args.host, args.binary) + print("Request to delete binary %s on host %s has been accepted." % + (args.binary, args.host)) + except Exception as e: + print("Delete for binary %(binary)s on host %(host)s failed: %(e)s" % + {'binary': args.binary, 'host': args.host, 'e': e}) + + +@utils.arg('host', metavar='', help='Name of host.') +@utils.arg('binary', metavar='', help='Service binary.') +def do_service_enable(cs, args): + """Enable the Zun service.""" + res = cs.services.enable(args.host, args.binary) + utils.print_dict(res[1]['service']) + + +@utils.arg('host', metavar='', help='Name of host.') +@utils.arg('binary', metavar='', help='Service binary.') +@utils.arg( + '--reason', + metavar='', + help='Reason for disabling service.') +def do_service_disable(cs, args): + """Disable the Zun service.""" + res = cs.services.disable(args.host, args.binary, args.reason) + utils.print_dict(res[1]['service']) + + +@utils.arg('host', metavar='', help='Name of host.') +@utils.arg('binary', metavar='', help='Service binary.') +@utils.arg( + '--unset', + dest='force_down', + help="Unset the force state down of service.", + action='store_false', + default=True) +def do_service_force_down(cs, args): + """Force Zun service to down or unset the force state.""" + res = cs.services.force_down(args.host, args.binary, args.force_down) + utils.print_dict(res[1]['service'])