diff --git a/.gitignore b/.gitignore index 42bff162..73321c8b 100644 --- a/.gitignore +++ b/.gitignore @@ -16,4 +16,7 @@ doc/build heatclient/versioninfo *.egg-info *.log -.testrepository +.stestr/ + +# Files created by releasenotes build +releasenotes/build diff --git a/.gitreview b/.gitreview index 054320ab..341941b8 100644 --- a/.gitreview +++ b/.gitreview @@ -1,4 +1,4 @@ [gerrit] -host=review.openstack.org +host=review.opendev.org port=29418 project=openstack/python-heatclient.git diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 00000000..c0ad41ca --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,30 @@ +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v6.0.0 + hooks: + - id: trailing-whitespace + # Replaces or checks mixed line ending + - id: mixed-line-ending + args: ['--fix', 'lf'] + exclude: '.*\.(svg)$' + # Forbid files which have a UTF-8 byte-order marker + - id: fix-byte-order-marker + # Checks that non-binary executables have a proper shebang + - id: check-executables-have-shebangs + # Check for files that contain merge conflict strings. + - id: check-merge-conflict + # Check for debugger imports and py37+ breakpoint() + # calls in python source + - id: debug-statements + - id: check-yaml + files: .*\.(yaml|yml)$ + - repo: https://opendev.org/openstack/hacking + rev: 8.1.0 + hooks: + - id: hacking + additional_dependencies: [] + - repo: https://github.com/asottile/pyupgrade + rev: v3.21.2 + hooks: + - id: pyupgrade + args: [--py311-plus] diff --git a/.stestr.conf b/.stestr.conf new file mode 100644 index 00000000..eb18d61c --- /dev/null +++ b/.stestr.conf @@ -0,0 +1,3 @@ +[DEFAULT] +test_path=${OS_TEST_PATH:-./heatclient/tests/unit} +top_dir=./ diff --git a/.testr.conf b/.testr.conf deleted file mode 100644 index 6e77f6be..00000000 --- a/.testr.conf +++ /dev/null @@ -1,4 +0,0 @@ -[DEFAULT] -test_command=${PYTHON:-python} -m subunit.run discover -t ./ ${OS_TEST_PATH:-./heatclient/tests/unit} $LISTOPT $IDOPTION -test_id_option=--load-list $IDFILE -test_list_option=--list diff --git a/.zuul.yaml b/.zuul.yaml new file mode 100644 index 00000000..ddff6521 --- /dev/null +++ b/.zuul.yaml @@ -0,0 +1,37 @@ +- job: + name: heatclient-functional + parent: devstack-tox-functional + timeout: 4200 + required-projects: + - openstack/heat + - openstack/python-heatclient + vars: + openrc_enable_export: true + devstack_plugins: + heat: https://opendev.org/openstack/heat + irrelevant-files: + - ^\.gitignore$ + - ^\.gitreview$ + - ^\.pre-commit-config\.yaml$ + - ^(test-|)requirements.txt$ + - ^setup.cfg$ + - ^doc/.*$ + - ^.*\.rst$ + - ^releasenotes/.*$ + - ^heatclient/tests/unit/.*$ + +- project: + templates: + - openstack-cover-jobs + - openstack-python3-jobs + - openstack-python3-next-jobs + - check-requirements + - openstackclient-plugin-jobs + - publish-openstack-docs-pti + - release-notes-jobs-python3 + check: + jobs: + - heatclient-functional + gate: + jobs: + - heatclient-functional diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index d7f2b8c8..f2fda38b 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -1,16 +1,20 @@ -If you would like to contribute to the development of OpenStack, -you must follow the steps documented at: +The source repository for this project can be found at: - http://docs.openstack.org/infra/manual/developers.html#development-workflow + https://opendev.org/openstack/python-heatclient -Once those steps have been completed, changes to OpenStack -should be submitted for review via the Gerrit tool, following -the workflow documented at: +Pull requests submitted through GitHub are not monitored. - http://docs.openstack.org/infra/manual/developers.html#development-workflow +To start contributing to OpenStack, follow the steps in the contribution guide +to set up and use Gerrit: -Pull requests submitted through GitHub will be ignored. + https://docs.openstack.org/contributors/code-and-documentation/quick-start.html -Bugs should be filed on Launchpad, not GitHub: +Bugs should be filed on Storyboard,: + + https://storyboard.openstack.org/#!/project/openstack/python-heatclient + +For more specific information about contributing to this repository, see the +python-heatclient contributor guide: + + https://docs.openstack.org/python-heatclient/latest/contributor/contributing.html - https://bugs.launchpad.net/python-heatclient diff --git a/README.rst b/README.rst index dab599c8..4f921d72 100644 --- a/README.rst +++ b/README.rst @@ -1,24 +1,15 @@ -======================== -Team and repository tags -======================== - -.. image:: http://governance.openstack.org/badges/python-heatclient.svg - :target: http://governance.openstack.org/reference/tags/index.html - -.. Change things from this point on - ================= python-heatclient ================= +.. image:: https://governance.openstack.org/tc/badges/python-heatclient.svg + +.. Change things from this point on + .. image:: https://img.shields.io/pypi/v/python-heatclient.svg - :target: https://pypi.python.org/pypi/python-heatclient/ + :target: https://pypi.org/project/python-heatclient/ :alt: Latest Version -.. image:: https://img.shields.io/pypi/dm/python-heatclient.svg - :target: https://pypi.python.org/pypi/python-heatclient/ - :alt: Downloads - OpenStack Orchestration API Client Library @@ -37,12 +28,12 @@ provides a Python API (the ``heatclient`` module) and a command-line tool * `Template`_ * `How to Contribute`_ -.. _PyPi: https://pypi.python.org/pypi/python-heatclient -.. _Online Documentation: http://docs.openstack.org/developer/python-heatclient +.. _PyPi: https://pypi.org/project/python-heatclient +.. _Online Documentation: https://docs.openstack.org/python-heatclient/latest .. _Launchpad project: https://launchpad.net/python-heatclient .. _Blueprints: https://blueprints.launchpad.net/python-heatclient -.. _Bugs: https://bugs.launchpad.net/python-heatclient -.. _Source: https://git.openstack.org/cgit/openstack/python-heatclient -.. _How to Contribute: http://docs.openstack.org/infra/manual/developers.html -.. _Specs: http://specs.openstack.org/openstack/heat-specs/ -.. _Template: https://git.openstack.org/cgit/openstack/heat-templates/ +.. _Bugs: https://storyboard.openstack.org/#!/project/openstack/python-heatclient +.. _Source: https://opendev.org/openstack/python-heatclient +.. _How to Contribute: https://docs.openstack.org/infra/manual/developers.html +.. _Specs: https://specs.openstack.org/openstack/heat-specs/ +.. _Template: https://opendev.org/openstack/heat-templates/ diff --git a/babel.cfg b/babel.cfg deleted file mode 100644 index efceab81..00000000 --- a/babel.cfg +++ /dev/null @@ -1 +0,0 @@ -[python: **.py] diff --git a/doc/.gitignore b/doc/.gitignore deleted file mode 100644 index 8e0be80f..00000000 --- a/doc/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -build/ -source/ref/ diff --git a/doc/Makefile b/doc/Makefile deleted file mode 100644 index 6239df39..00000000 --- a/doc/Makefile +++ /dev/null @@ -1,90 +0,0 @@ -# Makefile for Sphinx documentation -# - -# You can set these variables from the command line. -SPHINXOPTS = -SPHINXBUILD = sphinx-build -SPHINXSOURCE = source -PAPER = -BUILDDIR = build - -# Internal variables. -PAPEROPT_a4 = -D latex_paper_size=a4 -PAPEROPT_letter = -D latex_paper_size=letter -ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) $(SPHINXSOURCE) - -.PHONY: help clean html dirhtml pickle json htmlhelp qthelp latex changes linkcheck doctest - -help: - @echo "Please use \`make ' where is one of" - @echo " html to make standalone HTML files" - @echo " dirhtml to make HTML files named index.html in directories" - @echo " pickle to make pickle files" - @echo " json to make JSON files" - @echo " htmlhelp to make HTML files and a HTML help project" - @echo " qthelp to make HTML files and a qthelp project" - @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" - @echo " changes to make an overview of all changed/added/deprecated items" - @echo " linkcheck to check all external links for integrity" - @echo " doctest to run all doctests embedded in the documentation (if enabled)" - -clean: - -rm -rf $(BUILDDIR)/* - -html: - $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html - @echo - @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." - -dirhtml: - $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml - @echo - @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." - -pickle: - $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle - @echo - @echo "Build finished; now you can process the pickle files." - -json: - $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json - @echo - @echo "Build finished; now you can process the JSON files." - -htmlhelp: - $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp - @echo - @echo "Build finished; now you can run HTML Help Workshop with the" \ - ".hhp project file in $(BUILDDIR)/htmlhelp." - -qthelp: - $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp - @echo - @echo "Build finished; now you can run "qcollectiongenerator" with the" \ - ".qhcp project file in $(BUILDDIR)/qthelp, like this:" - @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/python-heatclient.qhcp" - @echo "To view the help file:" - @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/python-heatclient.qhc" - -latex: - $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex - @echo - @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." - @echo "Run \`make all-pdf' or \`make all-ps' in that directory to" \ - "run these through (pdf)latex." - -changes: - $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes - @echo - @echo "The overview file is in $(BUILDDIR)/changes." - -linkcheck: - $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck - @echo - @echo "Link check complete; look for any errors in the above output " \ - "or in $(BUILDDIR)/linkcheck/output.txt." - -doctest: - $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest - @echo "Testing of doctests in the sources finished, look at the " \ - "results in $(BUILDDIR)/doctest/output.txt." diff --git a/doc/requirements.txt b/doc/requirements.txt new file mode 100644 index 00000000..875f9897 --- /dev/null +++ b/doc/requirements.txt @@ -0,0 +1,4 @@ +openstackdocstheme>=2.2.1 # Apache-2.0 +reno>=3.1.0 # Apache-2.0 +sphinx>=2.0.0 # BSD +python-openstackclient>=3.12.0 # Apache-2.0 diff --git a/doc/source/cli/index.rst b/doc/source/cli/index.rst new file mode 100644 index 00000000..d58a3202 --- /dev/null +++ b/doc/source/cli/index.rst @@ -0,0 +1,9 @@ +============ +Command List +============ + +.. toctree:: + :glob: + :maxdepth: 3 + + * diff --git a/doc/source/cli/orchestration.rst b/doc/source/cli/orchestration.rst new file mode 100644 index 00000000..0aac0ef7 --- /dev/null +++ b/doc/source/cli/orchestration.rst @@ -0,0 +1,24 @@ +============= +orchestration +============= + +.. autoprogram-cliff:: openstack.orchestration.v1 + :command: orchestration template validate + +.. autoprogram-cliff:: openstack.orchestration.v1 + :command: orchestration template version list + +.. autoprogram-cliff:: openstack.orchestration.v1 + :command: orchestration template function list + +.. autoprogram-cliff:: openstack.orchestration.v1 + :command: orchestration resource type list + +.. autoprogram-cliff:: openstack.orchestration.v1 + :command: orchestration resource type show + +.. autoprogram-cliff:: openstack.orchestration.v1 + :command: orchestration build info + +.. autoprogram-cliff:: openstack.orchestration.v1 + :command: orchestration service list diff --git a/doc/source/cli/software_config.rst b/doc/source/cli/software_config.rst new file mode 100644 index 00000000..b7543080 --- /dev/null +++ b/doc/source/cli/software_config.rst @@ -0,0 +1,16 @@ +=============== +software config +=============== + + +.. autoprogram-cliff:: openstack.orchestration.v1 + :command: software config list + +.. autoprogram-cliff:: openstack.orchestration.v1 + :command: software config create + +.. autoprogram-cliff:: openstack.orchestration.v1 + :command: software config show + +.. autoprogram-cliff:: openstack.orchestration.v1 + :command: software config delete diff --git a/doc/source/cli/software_deployment.rst b/doc/source/cli/software_deployment.rst new file mode 100644 index 00000000..b8ccbe14 --- /dev/null +++ b/doc/source/cli/software_deployment.rst @@ -0,0 +1,22 @@ +=================== +software deployment +=================== + + +.. autoprogram-cliff:: openstack.orchestration.v1 + :command: software deployment list + +.. autoprogram-cliff:: openstack.orchestration.v1 + :command: software deployment create + +.. autoprogram-cliff:: openstack.orchestration.v1 + :command: software deployment show + +.. autoprogram-cliff:: openstack.orchestration.v1 + :command: software deployment output show + +.. autoprogram-cliff:: openstack.orchestration.v1 + :command: software deployment delete + +.. autoprogram-cliff:: openstack.orchestration.v1 + :command: software deployment metadata show diff --git a/doc/source/cli/stack.rst b/doc/source/cli/stack.rst new file mode 100644 index 00000000..175867e9 --- /dev/null +++ b/doc/source/cli/stack.rst @@ -0,0 +1,123 @@ +===== +stack +===== + +Basic actions +------------- + +.. autoprogram-cliff:: openstack.orchestration.v1 + :command: stack list + +.. autoprogram-cliff:: openstack.orchestration.v1 + :command: stack create + +.. autoprogram-cliff:: openstack.orchestration.v1 + :command: stack update + +.. autoprogram-cliff:: openstack.orchestration.v1 + :command: stack cancel + +.. autoprogram-cliff:: openstack.orchestration.v1 + :command: stack show + +.. autoprogram-cliff:: openstack.orchestration.v1 + :command: stack delete + +Outputs +------- + +.. autoprogram-cliff:: openstack.orchestration.v1 + :command: stack output list + +.. autoprogram-cliff:: openstack.orchestration.v1 + :command: stack output show + +Resources +--------- + +.. autoprogram-cliff:: openstack.orchestration.v1 + :command: stack resource list + +.. autoprogram-cliff:: openstack.orchestration.v1 + :command: stack resource show + +.. autoprogram-cliff:: openstack.orchestration.v1 + :command: stack resource metadata + +.. autoprogram-cliff:: openstack.orchestration.v1 + :command: stack resource mark unhealthy + +.. autoprogram-cliff:: openstack.orchestration.v1 + :command: stack resource signal + +Events +------ + +.. autoprogram-cliff:: openstack.orchestration.v1 + :command: stack event list + +.. autoprogram-cliff:: openstack.orchestration.v1 + :command: stack event show + +.. autoprogram-cliff:: openstack.orchestration.v1 + :command: stack failures list + +Input data +---------- + +.. autoprogram-cliff:: openstack.orchestration.v1 + :command: stack template show + +.. autoprogram-cliff:: openstack.orchestration.v1 + :command: stack environment show + +.. autoprogram-cliff:: openstack.orchestration.v1 + :command: stack file list + +User hooks +---------- + +.. autoprogram-cliff:: openstack.orchestration.v1 + :command: stack hook poll + +.. autoprogram-cliff:: openstack.orchestration.v1 + :command: stack hook clear + +Other actions +------------- + +.. autoprogram-cliff:: openstack.orchestration.v1 + :command: stack check + +.. autoprogram-cliff:: openstack.orchestration.v1 + :command: stack suspend + +.. autoprogram-cliff:: openstack.orchestration.v1 + :command: stack resume + +.. autoprogram-cliff:: openstack.orchestration.v1 + :command: stack export + +.. autoprogram-cliff:: openstack.orchestration.v1 + :command: stack abandon + +.. autoprogram-cliff:: openstack.orchestration.v1 + :command: stack adopt + +Snapshots +--------- + +.. autoprogram-cliff:: openstack.orchestration.v1 + :command: stack snapshot list + +.. autoprogram-cliff:: openstack.orchestration.v1 + :command: stack snapshot create + +.. autoprogram-cliff:: openstack.orchestration.v1 + :command: stack snapshot restore + +.. autoprogram-cliff:: openstack.orchestration.v1 + :command: stack snapshot show + +.. autoprogram-cliff:: openstack.orchestration.v1 + :command: stack snapshot delete diff --git a/doc/source/conf.py b/doc/source/conf.py index 8bb3fe46..75d5cd80 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at @@ -24,8 +23,6 @@ # serve to show the default. import os -import subprocess -import warnings # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the @@ -38,13 +35,10 @@ # Add any Sphinx extension module names here, as strings. They can be # extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. -extensions = ['sphinx.ext.autodoc', 'oslosphinx'] +extensions = ['sphinx.ext.autodoc', 'openstackdocstheme', 'cliff.sphinxext'] # Add any paths that contain templates here, relative to this directory. -if os.getenv('HUDSON_PUBLISH_DOCS'): - templates_path = ['_ga', '_templates'] -else: - templates_path = ['_templates'] +templates_path = [] # The suffix of source filenames. source_suffix = '.rst' @@ -56,18 +50,8 @@ master_doc = 'index' # General information about the project. -project = 'python-heatclient' copyright = 'OpenStack Contributors' -# The version info for the project you're documenting, acts as replacement for -# |version| and |release|, also used in various other places throughout the -# built documents. -# -# The short X.Y version. -version = '2.13' -# The full version, including alpha/beta/rc tags. -release = '2.13.0' - # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. # language = None @@ -98,7 +82,7 @@ # show_authors = False # The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' +pygments_style = 'native' # A list of ignored prefixes for module index sorting. # modindex_common_prefix = [] @@ -112,7 +96,7 @@ # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # html_theme_path = ['.'] -# html_theme = '_theme' +html_theme = 'openstackdocs' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the @@ -145,19 +129,6 @@ # so a file named "default.css" will overwrite the builtin "default.css". # html_static_path = ['_static'] -# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, -# using the given strftime format. -# html_last_updated_fmt = '%b %d, %Y' -git_cmd = ["git", "log", "--pretty=format:'%ad, commit %h'", "--date=local", - "-n1"] -try: - html_last_updated_fmt = subprocess.Popen( - git_cmd, stdout=subprocess.PIPE).communicate()[0] -except Exception: - warnings.warn('Cannot get last updated time from git repository. ' - 'Not setting "html_last_updated_fmt".') - - # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. # html_use_smartypants = True @@ -198,7 +169,6 @@ # Output file base name for HTML help builder. htmlhelp_basename = 'python-heatclientdoc' - # -- Options for LaTeX output ------------------------------------------------- latex_elements = { @@ -216,7 +186,7 @@ # (source start file, target name, title, author, documentclass [howto/manual]) latex_documents = [ ('index', 'python-heatclient.tex', 'python-heatclient Documentation', - u'OpenStack Foundation', 'manual'), + 'OpenStack Foundation', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of @@ -246,8 +216,8 @@ # (source start file, name, description, authors, manual section). man_pages = [ ('man/heat', 'heat', - u'Command line access to the heat project.', - [u'Heat Developers'], 1), + 'Command line access to the heat project.', + ['Heat Developers'], 1), ] # If true, show URL addresses after external links. @@ -260,8 +230,8 @@ # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ - ('index', 'Heat', u'Heat Documentation', - u'Heat Developers', 'Heat', 'One line description of project.', + ('index', 'Heat', 'Heat Documentation', + 'Heat Developers', 'Heat', 'One line description of project.', 'Miscellaneous'), ] @@ -273,3 +243,7 @@ # How to display URL addresses: 'footnote', 'no', or 'inline'. # texinfo_show_urls = 'footnote' + +# -- Options for openstackdocstheme ------------------------------------------- +openstackdocs_repo_name = 'openstack/python-heatclient' +openstackdocs_use_storyboard = True diff --git a/doc/source/contributor/contributing.rst b/doc/source/contributor/contributing.rst new file mode 100644 index 00000000..46c80d58 --- /dev/null +++ b/doc/source/contributor/contributing.rst @@ -0,0 +1,48 @@ +============================ +So You Want to Contribute... +============================ +For general information on contributing to OpenStack, please check out the +`contributor guide `_ to get started. +It covers all the basics that are common to all OpenStack projects: the accounts +you need, the basics of interacting with our Gerrit review system, how we +communicate as a community, etc. +Below will cover the more project specific information you need to get started +with python-heatclient. + +Communication +~~~~~~~~~~~~~ +* IRC channel #heat at OFTC +* Mailing list (prefix subjects with ``[heat]`` for faster responses) + http://lists.openstack.org/cgi-bin/mailman/listinfo/openstack-discuss + +Contacting the Core Team +~~~~~~~~~~~~~~~~~~~~~~~~ +Please refer the `python-heatclient Core Team +`_ contacts. + +New Feature Planning +~~~~~~~~~~~~~~~~~~~~ +python-heatclient features are tracked on `Storyboard `_. +Please specify the reported story with `python-heatclient`. + +Task Tracking +~~~~~~~~~~~~~ +We track our tasks in `Storyboard `_. +Please specify the reported story with `python-heatclient`. + +Reporting a Bug +~~~~~~~~~~~~~~~ +You found an issue and want to make sure we are aware of it? You can do so on +`Storyboard `_. +Please specify the reported story with `python-heatclient`. + +Getting Your Patch Merged +~~~~~~~~~~~~~~~~~~~~~~~~~ +All changes proposed to the python-heatclient project require one or two +2 votes +from python-heatclient core reviewers before one of the core reviewers can approve +patch by giving ``Workflow +1`` vote. + +Project Team Lead Duties +~~~~~~~~~~~~~~~~~~~~~~~~ +All common PTL duties are enumerated in the `PTL guide +`_. diff --git a/doc/source/ext/gen_ref.py b/doc/source/ext/gen_ref.py index 15ff8182..88bfc856 100644 --- a/doc/source/ext/gen_ref.py +++ b/doc/source/ext/gen_ref.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at @@ -25,7 +24,7 @@ def gen_ref(ver, title, names): refdir = os.path.join(BASE_DIR, "ref") pkg = "heatclient" if ver: - pkg = "%s.%s" % (pkg, ver) + pkg = f"{pkg}.{ver}" refdir = os.path.join(refdir, ver) if not os.path.exists(refdir): os.makedirs(refdir) @@ -37,7 +36,7 @@ def gen_ref(ver, title, names): ".. toctree::\n" " :maxdepth: 1\n" "\n") % {"title": title, "signs": "=" * len(title)}) - for name in names: + for name, sec_title in names.items(): idx.write(" %s\n" % name) rstpath = os.path.join(refdir, "%s.rst" % name) with open(rstpath, "w") as rst: @@ -49,11 +48,16 @@ def gen_ref(ver, title, names): " :undoc-members:\n" " :show-inheritance:\n" " :noindex:\n") - % {"title": name.capitalize(), - "signs": "=" * len(name), + % {"title": sec_title, + "signs": "=" * len(sec_title), "pkg": pkg, "name": name}) -gen_ref("", "Client Reference", ["client", "exc"]) + +gen_ref("", "Client Reference", {"client": "Client", "exc": "Exceptions"}) gen_ref("v1", "Version 1 API Reference", - ["stacks", "resources", "events", "actions", - "software_configs", "software_deployments"]) + {"stacks": "Stacks", + "resources": "Resources", + "events": "Events", + "actions": "Actions", + "software_configs": "Software Configs", + "software_deployments": "Software Deployments"}) diff --git a/doc/source/index.rst b/doc/source/index.rst index 0080645e..44f3a33f 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -1,10 +1,11 @@ Python bindings to the OpenStack Heat API ========================================= -This is a client for OpenStack Heat API. There's a Python API -(the :mod:`heatclient` module), and a command-line script -(installed as :program:`heat`). +This is a client for OpenStack Heat API. There's a Python API (the +:mod:`heatclient` module), a `python-openstackclient`_ plugin for command-line +use, and a legacy command-line script (installed as :program:`heat`). +========== Python API ========== @@ -41,7 +42,7 @@ PROJECT_NAME also provide the domain information as `project_domain_(name|id)`. For more information on keystoneauth API, see `Using Sessions`_. -.. _Using Sessions: http://docs.openstack.org/developer/keystoneauth/using-sessions.html +.. _Using Sessions: https://docs.openstack.org/keystoneauth/latest/using-sessions.html Reference --------- @@ -52,8 +53,28 @@ Reference ref/index ref/v1/index -Command-line Tool -================= +============================ +OpenStackClient Command Line +============================ + +The preferred way of accessing Heat via the command line is using the +python-heatclient's plugin for `python-openstackclient`_. Heat commands are +available through the ``openstack`` CLI command when both python-heatclient and +python-openstackclient are installed. + +.. toctree:: + :maxdepth: 2 + + cli/index + +.. _python-openstackclient: https://docs.openstack.org/python-openstackclient + +======================== +Legacy Command-line Tool +======================== + +The ``heat`` command is provided as a legacy CLI option. Users should prefer +using the python-openstackclient plugin via the ``openstack`` command instead. In order to use the CLI, you must provide your OpenStack username, password, tenant, and auth endpoint. Use the corresponding @@ -79,20 +100,20 @@ Once you've configured your authentication parameters, you can run ``heat help`` to see a complete listing of available commands. Man Pages -========= +--------- .. toctree:: :maxdepth: 1 man/heat -Contributing -============ -Code is hosted `on GitHub`_. Submit bugs to the Heat project on -`Launchpad`_. Submit code to the openstack/python-heatclient project -using `Gerrit`_. +For Contributors +================ + +* If you are a new contributor to python-heatclient please refer: :doc:`contributor/contributing` + + .. toctree:: + :hidden: -.. _on GitHub: https://github.com/openstack/python-heatclient -.. _Launchpad: https://launchpad.net/python-heatclient -.. _Gerrit: http://docs.openstack.org/infra/manual/developers.html#development-workflow + contributor/contributing diff --git a/doc/source/man/heat.rst b/doc/source/man/heat.rst index 9355a80e..d181b39e 100644 --- a/doc/source/man/heat.rst +++ b/doc/source/man/heat.rst @@ -95,4 +95,5 @@ Please ask your OpenStack operator to enable this feature. BUGS ==== -Heat client is hosted in Launchpad so you can view current bugs at https://bugs.launchpad.net/python-heatclient/. +Heat client bugs are managed throught StoryBoard +`OpenStack Heat Client Stories `__ diff --git a/heatclient/_i18n.py b/heatclient/_i18n.py index 1177be41..4abe87e5 100644 --- a/heatclient/_i18n.py +++ b/heatclient/_i18n.py @@ -12,7 +12,7 @@ """oslo.i18n integration module. -See http://docs.openstack.org/developer/oslo.i18n/usage.html +See https://docs.openstack.org/oslo.i18n/latest/user/index.html """ @@ -28,16 +28,6 @@ # The primary translation function using the well-known name "_" _ = _translators.primary -# Translators for log levels. -# -# The abbreviated names are meant to reflect the usual use of a short -# name like '_'. The "L" is for "log" and the other letter comes from -# the level. -_LI = _translators.log_info -_LW = _translators.log_warning -_LE = _translators.log_error -_LC = _translators.log_critical - def get_available_languages(): return oslo_i18n.get_available_languages('heatclient') diff --git a/heatclient/client.py b/heatclient/client.py index 8237b8e1..77444f43 100644 --- a/heatclient/client.py +++ b/heatclient/client.py @@ -10,10 +10,11 @@ # License for the specific language governing permissions and limitations # under the License. -from heatclient.common import utils +from oslo_utils import importutils def Client(version, *args, **kwargs): - module = utils.import_versioned_module(version, 'client') + module = importutils.import_versioned_module('heatclient', + version, 'client') client_class = getattr(module, 'Client') return client_class(*args, **kwargs) diff --git a/heatclient/common/base.py b/heatclient/common/base.py index f631603d..b19c8052 100644 --- a/heatclient/common/base.py +++ b/heatclient/common/base.py @@ -24,8 +24,7 @@ from oslo_utils import reflection from oslo_utils import strutils -import six -from six.moves.urllib import parse +from urllib import parse from heatclient._i18n import _ from heatclient import exc as exceptions @@ -49,7 +48,7 @@ def getid(obj): # TODO(aababilov): call run_hooks() in HookableMixin's child classes -class HookableMixin(object): +class HookableMixin: """Mixin so classes can register and run hooks.""" _hooks_map = {} @@ -93,7 +92,7 @@ def __init__(self, client): :param client: instance of BaseClient descendant for HTTP requests """ - super(BaseManager, self).__init__() + super().__init__() self.client = client def _list(self, url, response_key=None, obj_class=None, json=None): @@ -207,8 +206,7 @@ def _delete(self, url): return self.client.delete(url) -@six.add_metaclass(abc.ABCMeta) -class ManagerWithFind(BaseManager): +class ManagerWithFind(BaseManager, metaclass=abc.ABCMeta): """Manager with additional `find()`/`findall()` methods.""" @abc.abstractmethod @@ -281,25 +279,24 @@ def build_url(self, base_url=None, **kwargs): :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 + url = '/'.join([url, self.collection_key]) # do we have a specific entity? - entity_id = kwargs.get('%s_id' % self.key) + entity_id = kwargs.get(f'{self.key}_id') if entity_id is not None: - url += '/%s' % entity_id + url = '/'.join([url, entity_id]) return url def _filter_kwargs(self, kwargs): """Drop null values and handle ids.""" - for key, ref in six.iteritems(kwargs.copy()): + 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) + kwargs['{}_id'.firmat(key)] = getid(ref) return kwargs def create(self, **kwargs): @@ -325,12 +322,13 @@ def list(self, base_url=None, **kwargs): :param base_url: if provided, the generated URL will be appended to it """ kwargs = self._filter_kwargs(kwargs) + query = f'?{parse.urlencode(kwargs)}' if kwargs else '' 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 '', - }, + '{base_url}{query}'.format( + base_url=self.build_url(base_url=base_url, **kwargs), + query=query, + ), self.collection_key) def put(self, base_url=None, **kwargs): @@ -345,7 +343,7 @@ def put(self, base_url=None, **kwargs): def update(self, **kwargs): kwargs = self._filter_kwargs(kwargs) params = kwargs.copy() - params.pop('%s_id' % self.key) + params.pop(f'{self.key}_id') return self._patch( self.build_url(**kwargs), @@ -364,12 +362,13 @@ def find(self, base_url=None, **kwargs): :param base_url: if provided, the generated URL will be appended to it """ kwargs = self._filter_kwargs(kwargs) + query = f'?{parse.urlencode(kwargs)}' if kwargs else '' 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 '', - }, + '{base_url}{query}'.format( + base_url=self.build_url(base_url=base_url, **kwargs), + query=query, + ), self.collection_key) num = len(rl) @@ -392,7 +391,7 @@ class Extension(HookableMixin): manager_class = None def __init__(self, name, module): - super(Extension, self).__init__() + super().__init__() self.name = name self.module = module self._parse_extension_module() @@ -410,10 +409,10 @@ def _parse_extension_module(self): pass def __repr__(self): - return "" % self.name + return f"" -class Resource(object): +class Resource: """Base class for OpenStack resources (tenant, user, etc.). This is pretty much just a bag for attributes. @@ -435,11 +434,11 @@ def __init__(self, manager, info, loaded=False): def __repr__(self): reprkeys = sorted(k - for k in self.__dict__.keys() + for k in self.__dict__ if k[0] != '_' and k != 'manager') - info = ", ".join("%s=%s" % (k, getattr(self, k)) for k in reprkeys) + info = ", ".join(f"{k}={getattr(self, k)}" for k in reprkeys) class_name = reflection.get_class_name(self, fully_qualified=False) - return "<%s %s>" % (class_name, info) + return f"<{class_name} {info}>" @property def human_id(self): @@ -451,7 +450,7 @@ def human_id(self): return None def _add_details(self, info): - for (k, v) in six.iteritems(info): + for (k, v) in info.items(): try: setattr(self, k, v) self._info[k] = v diff --git a/heatclient/common/deployment_utils.py b/heatclient/common/deployment_utils.py index c44da2f3..1a86e136 100644 --- a/heatclient/common/deployment_utils.py +++ b/heatclient/common/deployment_utils.py @@ -13,10 +13,9 @@ import copy import uuid -import six -from six.moves.urllib import parse as urlparse from swiftclient import client as sc from swiftclient import utils as swiftclient_utils +from urllib import parse as urlparse from heatclient._i18n import _ from heatclient import exc @@ -94,24 +93,24 @@ def build_derived_config_params(action, source, name, input_values, def create_temp_url(swift_client, name, timeout, container=None): - container = container or '%(name)s-%(uuid)s' % { - 'name': name, 'uuid': uuid.uuid4()} + container = container or '{name}-{uuid}'.format( + name=name, uuid=uuid.uuid4()) object_name = str(uuid.uuid4()) swift_client.put_container(container) key_header = 'x-account-meta-temp-url-key' if key_header not in swift_client.head_account(): swift_client.post_account({ - key_header: six.text_type(uuid.uuid4())[:32]}) + key_header: str(uuid.uuid4())[:32]}) key = swift_client.head_account()[key_header] project_path = swift_client.url.split('/')[-1] - path = '/v1/%s/%s/%s' % (project_path, container, object_name) + path = f'/v1/{project_path}/{container}/{object_name}' timeout_secs = timeout * 60 tempurl = swiftclient_utils.generate_temp_url(path, timeout_secs, key, 'PUT') sw_url = urlparse.urlparse(swift_client.url) - put_url = '%s://%s%s' % (sw_url.scheme, sw_url.netloc, tempurl) + put_url = f'{sw_url.scheme}://{sw_url.netloc}{tempurl}' swift_client.put_object(container, object_name, '') return put_url @@ -120,7 +119,9 @@ def build_signal_id(hc, args): if args.signal_transport != 'TEMP_URL_SIGNAL': return - if args.os_no_client_auth: + # NOTE(pas-ha) only heatclient has os-no-client-auth arg, + # osc plugin does not have it + if getattr(args, 'os_no_client_auth', False): raise exc.CommandError(_( 'Cannot use --os-no-client-auth, auth required to create ' 'a Swift TempURL.')) @@ -137,7 +138,7 @@ def create_swift_client(auth, session, args): region_name=args.os_region_name) project_name = args.os_project_name or args.os_tenant_name swift_args = { - 'auth_version': '2.0', + 'auth_version': '3', 'tenant_name': project_name, 'user': args.os_username, 'key': None, diff --git a/heatclient/common/event_utils.py b/heatclient/common/event_utils.py index 309a1939..ca213257 100644 --- a/heatclient/common/event_utils.py +++ b/heatclient/common/event_utils.py @@ -61,7 +61,7 @@ def get_hook_events(hc, stack_id, event_args, nested_depth=0, resource_event_map[(e.stack_name, e.resource_name)] = e elif e.resource_status_reason == hook_clear_event_reason: if resource_event_map.get(stack_resource): - del(resource_event_map[(e.stack_name, e.resource_name)]) + del resource_event_map[(e.stack_name, e.resource_name)] return list(resource_event_map.values()) @@ -84,9 +84,9 @@ def get_events(hc, stack_id, event_args, nested_depth=0, return events first_links = getattr(events[0], 'links', []) - root_stack_link = [l for l in first_links - if l.get('rel') == 'root_stack'] - if root_stack_link: + root_stack_links = [link for link in first_links + if link.get('rel') == 'root_stack'] + if root_stack_links: # response has a root_stack link, indicating this is an API which # supports nested_depth return events @@ -151,6 +151,15 @@ def _get_nested_events(hc, nested_depth, stack_id, event_args): return nested_events +def _get_stack_name_from_links(event): + links = {link.get('rel'): link.get('href') + for link in getattr(event, 'links', [])} + href = links.get('stack') + if not href: + return + return href.split('/stacks/', 1)[-1].split('/')[0] + + def _get_stack_events(hc, stack_id, event_args): event_args['stack_id'] = stack_id try: @@ -160,9 +169,10 @@ def _get_stack_events(hc, stack_id, event_args): # just use the message that the server sent us. raise exc.CommandError(str(ex)) else: + stack_name = stack_id.split("/")[0] # Show which stack the event comes from (for nested events) for e in events: - e.stack_name = stack_id.split("/")[0] + e.stack_name = _get_stack_name_from_links(e) or stack_name return events @@ -172,15 +182,26 @@ def poll_for_events(hc, stack_name, action=None, poll_period=5, marker=None, if action: stop_status = ('%s_FAILED' % action, '%s_COMPLETE' % action) - stop_check = lambda a: a in stop_status + stop_check = lambda a: a in stop_status # noqa: E731 else: - stop_check = lambda a: a.endswith('_COMPLETE') or a.endswith('_FAILED') + stop_check = lambda a: a.endswith('_COMPLETE') or a.endswith('_FAILED') # noqa E731 no_event_polls = 0 msg_template = _("\n Stack %(name)s %(status)s \n") if not out: out = sys.stdout event_log_context = utils.EventLogContext() + + def is_stack_event(event): + if getattr(event, 'resource_name', '') != stack_name: + return False + + phys_id = getattr(event, 'physical_resource_id', '') + links = {link.get('rel'): link.get('href') + for link in getattr(event, 'links', [])} + stack_id = links.get('stack', phys_id).rsplit('/', 1)[-1] + return stack_id == phys_id + while True: events = get_events(hc, stack_id=stack_name, nested_depth=nested_depth, event_args={'sort_dir': 'asc', @@ -198,7 +219,7 @@ def poll_for_events(hc, stack_name, action=None, poll_period=5, marker=None, for event in events: # check if stack event was also received - if getattr(event, 'resource_name', '') == stack_name: + if is_stack_event(event): stack_status = getattr(event, 'resource_status', '') msg = msg_template % dict( name=stack_name, status=stack_status) @@ -230,8 +251,8 @@ def wait_for_events(ws, stack_name, out=None): event = events_mod.Event(None, data['payload'], True) # Keep compatibility with the HTTP API event.event_time = data['timestamp'] - event.resource_status = '%s_%s' % (event.resource_action, - event.resource_status) + event.resource_status = '{}_{}'.format(event.resource_action, + event.resource_status) events_log = utils.event_log_formatter([event], event_log_context) out.write(events_log) out.write('\n') @@ -240,4 +261,4 @@ def wait_for_events(ws, stack_name, out=None): if stack_status in ('COMPLETE', 'FAILED'): msg = msg_template % dict( name=stack_name, status=event.resource_status) - return '%s_%s' % (event.resource_action, stack_status), msg + return f'{event.resource_action}_{stack_status}', msg diff --git a/heatclient/common/format_utils.py b/heatclient/common/format_utils.py index d1a92065..4151c80e 100644 --- a/heatclient/common/format_utils.py +++ b/heatclient/common/format_utils.py @@ -15,7 +15,6 @@ import sys from osc_lib.command import command -import six class RawFormat(command.ShowOne): @@ -63,7 +62,7 @@ def indent_and_truncate(txt, spaces=0, truncate=False, truncate_limit=10, """ if txt is None: return - lines = six.text_type(txt).splitlines() + lines = str(txt).splitlines() if truncate and len(lines) > truncate_limit: lines = lines[-truncate_limit:] if truncate_prefix is not None: @@ -91,6 +90,6 @@ def print_software_deployment_output(data, name, out=sys.stdout, long=False): truncate=not long, truncate_prefix='...', truncate_postfix='(truncated, view all with --long)') - out.write(' %s: |\n%s\n' % (name, output)) + out.write(f' {name}: |\n{output}\n') else: - out.write(' %s: %s\n' % (name, data.get(name))) + out.write(f' {name}: {data.get(name)}\n') diff --git a/heatclient/common/hook_utils.py b/heatclient/common/hook_utils.py index 66b980a6..3e91ae3b 100644 --- a/heatclient/common/hook_utils.py +++ b/heatclient/common/hook_utils.py @@ -10,12 +10,10 @@ # License for the specific language governing permissions and limitations # under the License. +import fnmatch import logging -from oslo_utils import fnmatch - from heatclient._i18n import _ -from heatclient._i18n import _LE from heatclient import exc logger = logging.getLogger(__name__) @@ -29,8 +27,8 @@ def clear_hook(hc, stack_id, resource_name, hook_type): data={'unset_hook': hook_type}) except exc.HTTPNotFound: logger.error( - _LE("Stack %(stack)s or resource %(resource)s " - "not found for hook %(hook_type)"), + "Stack %(stack)s or resource %(resource)s " + "not found for hook %(hook_type)s", {'resource': resource_name, 'stack': stack_id, 'hook_type': hook_type}) diff --git a/heatclient/common/http.py b/heatclient/common/http.py index dc405f4a..8d7a548a 100644 --- a/heatclient/common/http.py +++ b/heatclient/common/http.py @@ -24,11 +24,9 @@ from oslo_utils import encodeutils from oslo_utils import importutils import requests -import six -from six.moves.urllib import parse +from urllib import parse from heatclient._i18n import _ -from heatclient._i18n import _LW from heatclient.common import utils from heatclient import exc @@ -67,10 +65,10 @@ def get_system_ca_file(): if os.path.exists(ca): LOG.debug("Using ca file %s", ca) return ca - LOG.warning(_LW("System ca file could not be found.")) + LOG.warning("System ca file could not be found.") -class HTTPClient(object): +class HTTPClient: def __init__(self, endpoint, **kwargs): self.endpoint = endpoint @@ -138,18 +136,21 @@ def log_curl_request(self, method, url, kwargs): if 'data' in kwargs: curl.append('-d \'%s\'' % kwargs['data']) - curl.append('%s%s' % (self.endpoint, url)) + if not parse.urlparse(url).netloc: + url = self.endpoint + url + + curl.append(url) LOG.debug(' '.join(curl)) @staticmethod def log_http_response(resp): status = (resp.raw.version / 10.0, resp.status_code, resp.reason) dump = ['\nHTTP/%.1f %s %s' % status] - dump.extend(['%s: %s' % (k, v) for k, v in resp.headers.items()]) + dump.extend([f'{k}: {v}' for k, v in resp.headers.items()]) dump.append('') if resp.content: content = resp.content - if isinstance(content, six.binary_type): + if isinstance(content, bytes): content = content.decode() dump.extend([content, '']) LOG.debug('\n'.join(dump)) @@ -215,17 +216,17 @@ def _http_request(self, url, method, **kwargs): message = (_("Error finding address for %(url)s: %(e)s") % {'url': self.endpoint_url + url, 'e': e}) raise exc.InvalidEndpoint(message=message) - except (socket.error, socket.timeout) as e: + except (OSError, TimeoutError) as e: endpoint = self.endpoint message = (_("Error communicating with %(endpoint)s %(e)s") % {'endpoint': endpoint, 'e': e}) raise exc.CommunicationError(message=message) self.log_http_response(resp) - + txt_content = encodeutils.safe_decode(resp.content, 'utf-8') if not ('X-Auth-Key' in kwargs['headers']) and ( resp.status_code == 401 or - (resp.status_code == 500 and "(HTTP 401)" in resp.content)): + (resp.status_code == 500 and "(HTTP 401)" in txt_content)): raise exc.HTTPUnauthorized(_("Authentication failed: %s") % resp.content) elif 400 <= resp.status_code < 600: @@ -306,10 +307,13 @@ def request(self, url, method, **kwargs): redirect = kwargs.get('redirect') kwargs.setdefault('user_agent', USER_AGENT) + headers = kwargs.setdefault('headers', {}) + headers.setdefault('Content-Type', 'application/json') + if 'data' in kwargs: kwargs['data'] = jsonutils.dumps(kwargs['data']) - resp, body = super(SessionClient, self).request( + resp, body = super().request( url, method, raise_exc=False, **kwargs) @@ -335,7 +339,7 @@ def strip_endpoint(self, location): raise exc.InvalidEndpoint(message=message) if (self.endpoint_override is not None and location.lower().startswith(self.endpoint_override.lower())): - return location[len(self.endpoint_override):] + return location[len(self.endpoint_override):] else: return location @@ -349,9 +353,17 @@ def _construct_http_client(endpoint=None, username=None, password=None, if session: if 'endpoint_override' not in kwargs and endpoint: kwargs['endpoint_override'] = endpoint - elif 'service_type' not in kwargs: + + if 'service_type' not in kwargs: kwargs['service_type'] = 'orchestration' + if 'interface' not in kwargs and endpoint_type: + kwargs['interface'] = endpoint_type + + if 'region_name' in kwargs: + kwargs['additional_headers'] = { + 'X-Region-Name': kwargs['region_name']} + return SessionClient(session, auth=auth, **kwargs) else: return HTTPClient(endpoint=endpoint, username=username, diff --git a/heatclient/common/resource_formatter.py b/heatclient/common/resource_formatter.py index 7a5ec7f2..3cfb63f1 100644 --- a/heatclient/common/resource_formatter.py +++ b/heatclient/common/resource_formatter.py @@ -17,11 +17,11 @@ from cliff.formatters import base -class ResourceDotInfo(object): +class ResourceDotInfo: def __init__(self, res): self.resource = res - links = {l['rel']: l['href'] for l in res.links} + links = {link['rel']: link['href'] for link in res.links} self.nested_dot_id = self.dot_id(links.get('nested'), 'stack') self.stack_dot_id = self.dot_id(links.get('stack'), 'stack') self.res_dot_id = self.dot_id(links.get('self')) @@ -34,7 +34,7 @@ def dot_id(url, prefix=None): if not prefix: prefix = 'r' hash_object = hashlib.sha256(url.encode('utf-8')) - return '%s_%s' % (prefix, hash_object.hexdigest()[:20]) + return f'{prefix}_{hash_object.hexdigest()[:20]}' class ResourceDotFormatter(base.ListFormatter): @@ -46,7 +46,7 @@ def emit_list(self, column_names, data, stdout, parsed_args): writer.write() -class ResourceDotWriter(object): +class ResourceDotWriter: def __init__(self, data, stdout): self.resources_by_stack = collections.defaultdict( diff --git a/heatclient/common/template_format.py b/heatclient/common/template_format.py index f73339f0..ff2fdf98 100644 --- a/heatclient/common/template_format.py +++ b/heatclient/common/template_format.py @@ -16,26 +16,37 @@ from heatclient._i18n import _ if hasattr(yaml, 'CSafeLoader'): - yaml_loader = yaml.CSafeLoader + yaml_loader_base = yaml.CSafeLoader else: - yaml_loader = yaml.SafeLoader + yaml_loader_base = yaml.SafeLoader if hasattr(yaml, 'CSafeDumper'): - yaml_dumper = yaml.CSafeDumper + yaml_dumper_base = yaml.CSafeDumper else: - yaml_dumper = yaml.SafeDumper + yaml_dumper_base = yaml.SafeDumper + + +# We create custom class to not overriden the default yaml behavior +class yaml_loader(yaml_loader_base): + pass + + +class yaml_dumper(yaml_dumper_base): + pass def _construct_yaml_str(self, node): # Override the default string handling function # to always return unicode objects return self.construct_scalar(node) -yaml_loader.add_constructor(u'tag:yaml.org,2002:str', _construct_yaml_str) + + +yaml_loader.add_constructor('tag:yaml.org,2002:str', _construct_yaml_str) # Unquoted dates like 2013-05-23 in yaml files get loaded as objects of type # datetime.data which causes problems in API layer when being processed by # openstack.common.jsonutils. Therefore, make unicode string out of timestamps # until jsonutils can handle dates. -yaml_loader.add_constructor(u'tag:yaml.org,2002:timestamp', +yaml_loader.add_constructor('tag:yaml.org,2002:timestamp', _construct_yaml_str) diff --git a/heatclient/common/template_utils.py b/heatclient/common/template_utils.py index c81da43d..cc814356 100644 --- a/heatclient/common/template_utils.py +++ b/heatclient/common/template_utils.py @@ -13,12 +13,11 @@ # License for the specific language governing permissions and limitations # under the License. -import collections +from collections import abc from oslo_serialization import jsonutils -import six -from six.moves.urllib import error -from six.moves.urllib import parse -from six.moves.urllib import request +from urllib import error +from urllib import parse +from urllib import request from heatclient._i18n import _ from heatclient.common import environment_format @@ -27,7 +26,8 @@ from heatclient import exc -def process_template_path(template_path, object_request=None, existing=False): +def process_template_path(template_path, object_request=None, + existing=False, fetch_child=True): """Read template from template path. Attempt to read template first as a file or url. If that is unsuccessful, @@ -37,17 +37,20 @@ def process_template_path(template_path, object_request=None, existing=False): :param object_request: custom object request function used to get template if local or uri path fails :param existing: if the current stack's template should be used + :param fetch_child: Whether to fetch the child templates :returns: get_file dict and template contents :raises: error.URLError """ try: return get_template_contents(template_file=template_path, - existing=existing) + existing=existing, + fetch_child=fetch_child) except error.URLError as template_file_exc: try: return get_template_contents(template_object=template_path, object_request=object_request, - existing=existing) + existing=existing, + fetch_child=fetch_child) except exc.HTTPNotFound: # The initial exception gives the user better failure context. raise template_file_exc @@ -55,7 +58,8 @@ def process_template_path(template_path, object_request=None, existing=False): def get_template_contents(template_file=None, template_url=None, template_object=None, object_request=None, - files=None, existing=False): + files=None, existing=False, + fetch_child=True): is_object = False # Transform a bare file path to a file:// URL. @@ -74,29 +78,32 @@ def get_template_contents(template_file=None, template_url=None, return {}, None else: raise exc.CommandError(_('Need to specify exactly one of ' - '%(arg1)s, %(arg2)s or %(arg3)s') % + '[%(arg1)s, %(arg2)s or %(arg3)s]' + ' or %(arg4)s') % { 'arg1': '--template-file', 'arg2': '--template-url', - 'arg3': '--template-object'}) + 'arg3': '--template-object', + 'arg4': '--existing'}) if not tpl: raise exc.CommandError(_('Could not fetch template from %s') % template_url) try: - if isinstance(tpl, six.binary_type): + if isinstance(tpl, bytes): tpl = tpl.decode('utf-8') template = template_format.parse(tpl) except ValueError as e: raise exc.CommandError(_('Error parsing template %(url)s %(error)s') % {'url': template_url, 'error': e}) - - tmpl_base_url = utils.base_url_for_url(template_url) if files is None: files = {} - resolve_template_get_files(template, files, tmpl_base_url, is_object, - object_request) + + if fetch_child: + tmpl_base_url = utils.base_url_for_url(template_url) + resolve_template_get_files(template, files, tmpl_base_url, is_object, + object_request) return files, template @@ -106,7 +113,7 @@ def resolve_template_get_files(template, files, template_base_url, def ignore_if(key, value): if key != 'get_file' and key != 'type': return True - if not isinstance(value, six.string_types): + if not isinstance(value, str): return True if (key == 'type' and not value.endswith(('.yaml', '.template'))): @@ -122,7 +129,7 @@ def recurse_if(value): def is_template(file_content): try: - if isinstance(file_content, six.binary_type): + if isinstance(file_content, bytes): file_content = file_content.decode('utf-8') template_format.parse(file_content) except (ValueError, TypeError): @@ -136,7 +143,7 @@ def get_file_contents(from_data, files, base_url=None, if recurse_if and recurse_if(from_data): if isinstance(from_data, dict): - recurse_data = six.itervalues(from_data) + recurse_data = from_data.values() else: recurse_data = from_data for value in recurse_data: @@ -144,7 +151,7 @@ def get_file_contents(from_data, files, base_url=None, is_object, object_request) if isinstance(from_data, dict): - for key, value in six.iteritems(from_data): + for key, value in from_data.items(): if ignore_if and ignore_if(key, value): continue @@ -195,9 +202,12 @@ def deep_update(old, new): old = {} for k, v in new.items(): - if isinstance(v, collections.Mapping): + if isinstance(v, abc.Mapping): r = deep_update(old.get(k, {}), v) old[k] = r + elif v is None and isinstance(old.get(k), abc.Mapping): + # Don't override empty data, to work around yaml syntax issue + pass else: old[k] = new[k] return old @@ -207,7 +217,8 @@ def process_multiple_environments_and_files(env_paths=None, template=None, template_url=None, env_path_is_object=None, object_request=None, - env_list_tracker=None): + env_list_tracker=None, + fetch_env_files=True): """Reads one or more environment files. Reads in each specified environment file and returns a dictionary @@ -234,6 +245,7 @@ def process_multiple_environments_and_files(env_paths=None, template=None, :type env_list_tracker: list or None :return: tuple of files dict and a dict of the consolidated environment :rtype: tuple + :param fetch_env_files: fetch env_files or leave it to server """ merged_files = {} merged_env = {} @@ -244,24 +256,28 @@ def process_multiple_environments_and_files(env_paths=None, template=None, if env_paths: for env_path in env_paths: - files, env = process_environment_and_files( - env_path=env_path, - template=template, - template_url=template_url, - env_path_is_object=env_path_is_object, - object_request=object_request, - include_env_in_files=include_env_in_files) - - # 'files' looks like {"filename1": contents, "filename2": contents} - # so a simple update is enough for merging - merged_files.update(files) - - # 'env' can be a deeply nested dictionary, so a simple update is - # not enough - merged_env = deep_update(merged_env, env) + if fetch_env_files: + files, env = process_environment_and_files( + env_path=env_path, + template=template, + template_url=template_url, + env_path_is_object=env_path_is_object, + object_request=object_request, + include_env_in_files=include_env_in_files) + + # 'files' looks like: + # {"filename1": contents, "filename2": contents} + # so a simple update is enough for merging + merged_files.update(files) + + # 'env' can be a deeply nested dictionary, so a simple + # update is not enough + merged_env = deep_update(merged_env, env) + env_url = utils.normalise_file_path_to_url(env_path) + else: + env_url = env_path if env_list_tracker is not None: - env_url = utils.normalise_file_path_to_url(env_path) env_list_tracker.append(env_url) return merged_files, merged_env @@ -351,7 +367,7 @@ def ignore_if(key, value): get_file_contents(rr, files, base_url, ignore_if, is_object=is_object, object_request=object_request) - for res_name, res_dict in six.iteritems(rr.get('resources', {})): + for res_name, res_dict in rr.get('resources', {}).items(): res_base_url = res_dict.get('base_url', base_url) get_file_contents( res_dict, files, res_base_url, ignore_if, diff --git a/heatclient/common/utils.py b/heatclient/common/utils.py index 2408ab21..8d104c7a 100644 --- a/heatclient/common/utils.py +++ b/heatclient/common/utils.py @@ -16,21 +16,19 @@ import base64 import logging import os +from pathlib import Path import textwrap import uuid from oslo_serialization import jsonutils from oslo_utils import encodeutils -from oslo_utils import importutils import prettytable -import six -from six.moves.urllib import error -from six.moves.urllib import parse -from six.moves.urllib import request +from urllib import error +from urllib import parse +from urllib import request import yaml from heatclient._i18n import _ -from heatclient._i18n import _LE from heatclient import exc LOG = logging.getLogger(__name__) @@ -125,24 +123,21 @@ def print_list(objs, fields, formatters=None, sortby_index=0, row.append(data) pt.add_row(row) - if six.PY3: - print(encodeutils.safe_encode(pt.get_string(**kwargs)).decode()) - else: - print(encodeutils.safe_encode(pt.get_string(**kwargs))) + print(encodeutils.safe_encode(pt.get_string(**kwargs)).decode()) def link_formatter(links): - def format_link(l): - if 'rel' in l: - return "%s (%s)" % (l.get('href', ''), l.get('rel', '')) + def format_link(link): + if 'rel' in link: + return "{} ({})".format(link.get('href', ''), link.get('rel', '')) else: - return "%s" % (l.get('href', '')) - return '\n'.join(format_link(l) for l in links or []) + return "%s" % (link.get('href', '')) + return '\n'.join(format_link(link) for link in links or []) def resource_nested_identifier(rsrc): - nested_link = [l for l in rsrc.links or [] - if l.get('rel') == 'nested'] + nested_link = [link for link in rsrc.links or [] + if link.get('rel') == 'nested'] if nested_link: nested_href = nested_link[0].get('href') nested_identifier = nested_href.split("/")[-2:] @@ -172,7 +167,7 @@ def print_dict(d, formatters=None): caching=False, print_empty=False) pt.align = 'l' - for field in d.keys(): + for field in d: if field in formatters: pt.add_row([field, formatters[field](d[field])]) else: @@ -180,7 +175,7 @@ def print_dict(d, formatters=None): print(pt.get_string(sortby='Property')) -class EventLogContext(object): +class EventLogContext: def __init__(self): # key is a stack id or the name of the nested stack, value is a tuple @@ -210,11 +205,11 @@ def build_resource_name(self, event): def get_stack_id(): if getattr(event, 'stack_id', None) is not None: return event.stack_id - for l in getattr(event, 'links', []): - if l.get('rel') == 'stack': - if 'href' not in l: + for link in getattr(event, 'links', []): + if link.get('rel') == 'stack': + if 'href' not in link: return None - stack_link = l['href'] + stack_link = link['href'] return stack_link.split('/')[-1] stack_id = get_stack_id() @@ -244,8 +239,8 @@ def get_stack_id(): def event_log_formatter(events, event_log_context=None): """Return the events in log format.""" event_log = [] - log_format = ("%(event_time)s " - "[%(rsrc_name)s]: %(rsrc_status)s %(rsrc_status_reason)s") + log_format = ("%(event_time)s %(event_id)s [%(rsrc_name)s]: " + "%(rsrc_status)s %(rsrc_status_reason)s") # It is preferable for a context to be passed in, but there might be enough # events in this call to build a better resource name, so create a context @@ -257,12 +252,15 @@ def event_log_formatter(events, event_log_context=None): rsrc_name = event_log_context.build_resource_name(event) event_time = getattr(event, 'event_time', '') - log = log_format % { + log_data = { 'event_time': event_time.replace('T', ' '), 'rsrc_name': rsrc_name, 'rsrc_status': getattr(event, 'resource_status', ''), - 'rsrc_status_reason': getattr(event, 'resource_status_reason', '') + 'rsrc_status_reason': getattr(event, 'resource_status_reason', ''), + 'event_id': getattr(event, 'id', '') } + + log = log_format % log_data event_log.append(log) return "\n".join(event_log) @@ -288,10 +286,7 @@ def print_update_list(lst, fields, formatters=None): pt.add_row(row) - if six.PY3: - print(encodeutils.safe_encode(pt.get_string()).decode()) - else: - print(encodeutils.safe_encode(pt.get_string())) + print(encodeutils.safe_encode(pt.get_string()).decode()) def find_resource(manager, name_or_id): @@ -324,13 +319,6 @@ def find_resource(manager, name_or_id): raise exc.CommandError(msg) -def import_versioned_module(version, submodule=None): - module = 'heatclient.v%s' % version - if submodule: - module = '.'.join((module, submodule)) - return importutils.import_module(module) - - def format_parameters(params, parse_semicolon=True): '''Reformat parameters into dict of format expected by the API.''' @@ -385,9 +373,9 @@ def format_parameter_file(param_files, template_file=None, template_file, template_url)) param_file = {} - for key, value in six.iteritems(params): - param_file[key] = resolve_param_get_file(value, - template_base_url) + for key, value in params.items(): + param_file[key] = resolve_param_get_file(value, + template_base_url) return param_file @@ -429,7 +417,7 @@ def read_url_content(url): try: content.decode('utf-8') except ValueError: - content = base64.encodestring(content) + content = base64.encodebytes(content) return content @@ -442,8 +430,7 @@ def base_url_for_url(url): def normalise_file_path_to_url(path): if parse.urlparse(path).scheme: return path - path = os.path.abspath(path) - return parse.urljoin('file:', request.pathname2url(path)) + return Path(path).resolve().as_uri() def get_response_body(resp): @@ -452,7 +439,7 @@ def get_response_body(resp): try: body = resp.json() except ValueError: - LOG.error(_LE('Could not decode response body as JSON')) + LOG.error('Could not decode response body as JSON') else: body = None return body diff --git a/heatclient/exc.py b/heatclient/exc.py index cc640da9..5cbd915b 100644 --- a/heatclient/exc.py +++ b/heatclient/exc.py @@ -46,7 +46,7 @@ class HTTPException(BaseException): code = 'N/A' def __init__(self, message=None, code=None): - super(HTTPException, self).__init__(message) + super().__init__(message) try: self.error = jsonutils.loads(message) if 'error' not in self.error: @@ -77,8 +77,7 @@ class HTTPMultipleChoices(HTTPException): code = 300 def __str__(self): - self.details = _("Requested version of Heat API is not" - "available.") + self.details = _("Requested version of Heat API is not available.") return (_("%(name)s (HTTP %(code)s) %(details)s") % { 'name': reflection.get_class_name(self, fully_qualified=False), diff --git a/heatclient/locale/zh_CN/LC_MESSAGES/heatclient-log-error.po b/heatclient/locale/zh_CN/LC_MESSAGES/heatclient-log-error.po deleted file mode 100644 index 8ab8300b..00000000 --- a/heatclient/locale/zh_CN/LC_MESSAGES/heatclient-log-error.po +++ /dev/null @@ -1,25 +0,0 @@ -# zzxwill , 2016. #zanata -msgid "" -msgstr "" -"Project-Id-Version: python-heatclient 1.2.1.dev53\n" -"Report-Msgid-Bugs-To: https://bugs.launchpad.net/openstack-i18n/\n" -"POT-Creation-Date: 2016-06-27 08:52+0000\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"PO-Revision-Date: 2016-06-27 11:57+0000\n" -"Last-Translator: zzxwill \n" -"Language-Team: Chinese (China)\n" -"Language: zh-CN\n" -"X-Generator: Zanata 3.7.3\n" -"Plural-Forms: nplurals=1; plural=0\n" - -msgid "Could not decode response body as JSON" -msgstr "无法将响应主题编码为JSON" - -#, python-format -msgid "Stack %(stack)s or resource %(resource)snot found for hook %(hook_type)" -msgstr "" -"Postpone to translate this message as we Need to add a space between " -"'%(resource)s' and 'not'. Submitted a fix for the original string. (https://" -"review.openstack.org/334428)" diff --git a/heatclient/locale/zh_CN/LC_MESSAGES/heatclient-log-info.po b/heatclient/locale/zh_CN/LC_MESSAGES/heatclient-log-info.po deleted file mode 100644 index 188e2c83..00000000 --- a/heatclient/locale/zh_CN/LC_MESSAGES/heatclient-log-info.po +++ /dev/null @@ -1,24 +0,0 @@ -# zzxwill , 2016. #zanata -msgid "" -msgstr "" -"Project-Id-Version: python-heatclient 1.2.1.dev53\n" -"Report-Msgid-Bugs-To: https://bugs.launchpad.net/openstack-i18n/\n" -"POT-Creation-Date: 2016-06-27 08:52+0000\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"PO-Revision-Date: 2016-06-27 11:59+0000\n" -"Last-Translator: zzxwill \n" -"Language-Team: Chinese (China)\n" -"Language: zh-CN\n" -"X-Generator: Zanata 3.7.3\n" -"Plural-Forms: nplurals=1; plural=0\n" - -msgid "User did not confirm stack delete (ctrl-c) so taking no action." -msgstr "因为用户没有确认栈删除操作(ctrl-c),所以不执行任何动作。" - -msgid "User did not confirm stack delete (ctrl-d) so taking no action." -msgstr "因为用户没有确认栈删除操作(ctrl-d),所以不执行任何动作。" - -msgid "User did not confirm stack delete so taking no action." -msgstr "因为用户没有确认栈删除操作,所以不执行任何动作。" diff --git a/heatclient/locale/zh_CN/LC_MESSAGES/heatclient-log-warning.po b/heatclient/locale/zh_CN/LC_MESSAGES/heatclient-log-warning.po deleted file mode 100644 index 6a18c250..00000000 --- a/heatclient/locale/zh_CN/LC_MESSAGES/heatclient-log-warning.po +++ /dev/null @@ -1,26 +0,0 @@ -# zzxwill , 2016. #zanata -msgid "" -msgstr "" -"Project-Id-Version: python-heatclient 1.2.1.dev53\n" -"Report-Msgid-Bugs-To: https://bugs.launchpad.net/openstack-i18n/\n" -"POT-Creation-Date: 2016-06-27 08:52+0000\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"PO-Revision-Date: 2016-06-27 12:01+0000\n" -"Last-Translator: zzxwill \n" -"Language-Team: Chinese (China)\n" -"Language: zh-CN\n" -"X-Generator: Zanata 3.7.3\n" -"Plural-Forms: nplurals=1; plural=0\n" - -#, python-format -msgid "\"%(old)s\" is deprecated, please use \"%(new)s\" instead" -msgstr "\"%(old)s\"已弃用,请使用\"%(new)s\"。" - -#, python-format -msgid "%(arg1)s is deprecated, please use %(arg2)s instead" -msgstr "\"%(arg1)s\"已弃用,请使用\"%(arg2)s\"。" - -msgid "System ca file could not be found." -msgstr "找不到系统CA文件。" diff --git a/heatclient/osc/plugin.py b/heatclient/osc/plugin.py index ea1b69e4..859a13e8 100644 --- a/heatclient/osc/plugin.py +++ b/heatclient/osc/plugin.py @@ -11,6 +11,8 @@ # under the License. # +"""OpenStackClient plugin for Orchestration service.""" + import logging from osc_lib import utils @@ -39,13 +41,15 @@ def make_client(instance): if instance.session: kwargs.update({'session': instance.session, 'service_type': API_NAME}) + elif instance.auth_plugin_name == 'token_endpoint': + kwargs.update({'endpoint': instance.auth.url, + 'token': instance.auth.token}) else: endpoint = instance.get_endpoint_for_service_type( API_NAME, region_name=instance.region_name, interface=instance.interface, ) - kwargs.update({'endpoint': endpoint, 'auth_url': instance.auth.auth_url, 'username': instance.auth_ref.username, diff --git a/heatclient/osc/v1/build_info.py b/heatclient/osc/v1/build_info.py index 9f90dd5b..6428fe42 100644 --- a/heatclient/osc/v1/build_info.py +++ b/heatclient/osc/v1/build_info.py @@ -17,9 +17,8 @@ from osc_lib.command import command from osc_lib import utils -import six -from heatclient.common import utils as heat_utils +from heatclient.osc.v1 import common class BuildInfo(command.ShowOne): @@ -28,7 +27,7 @@ class BuildInfo(command.ShowOne): log = logging.getLogger(__name__ + ".BuildInfo") def get_parser(self, prog_name): - parser = super(BuildInfo, self).get_parser(prog_name) + parser = super().get_parser(prog_name) return parser def take_action(self, parsed_args): @@ -38,9 +37,9 @@ def take_action(self, parsed_args): result = heat_client.build_info.build_info() formatters = { - 'api': heat_utils.json_formatter, - 'engine': heat_utils.json_formatter, + 'api': common.JsonColumn, + 'engine': common.JsonColumn, } - columns = sorted(list(six.iterkeys(result))) + columns = sorted(list(result.keys())) return columns, utils.get_dict_properties(result, columns, formatters=formatters) diff --git a/heatclient/osc/v1/common.py b/heatclient/osc/v1/common.py new file mode 100644 index 00000000..1b5ac701 --- /dev/null +++ b/heatclient/osc/v1/common.py @@ -0,0 +1,42 @@ +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# + +from cliff import columns + +from heatclient.common import utils as heat_utils + + +class LinkColumn(columns.FormattableColumn): + def human_readable(self): + return heat_utils.link_formatter(self._value) + + +class JsonColumn(columns.FormattableColumn): + def human_readable(self): + return heat_utils.json_formatter(self._value) + + +class YamlColumn(columns.FormattableColumn): + def human_readable(self): + return heat_utils.yaml_formatter(self._value) + + +class TextWrapColumn(columns.FormattableColumn): + def human_readable(self): + return heat_utils.text_wrap_formatter(self._value) + + +class NewlineListColumn(columns.FormattableColumn): + def human_readable(self): + return heat_utils.newline_list_formatter(self._value) diff --git a/heatclient/osc/v1/event.py b/heatclient/osc/v1/event.py index b577e941..ea5ff16d 100644 --- a/heatclient/osc/v1/event.py +++ b/heatclient/osc/v1/event.py @@ -23,6 +23,7 @@ from heatclient.common import event_utils from heatclient.common import utils as heat_utils from heatclient import exc +from heatclient.osc.v1 import common class ShowEvent(command.ShowOne): @@ -31,7 +32,7 @@ class ShowEvent(command.ShowOne): log = logging.getLogger(__name__ + '.ShowEvent') def get_parser(self, prog_name): - parser = super(ShowEvent, self).get_parser(prog_name) + parser = super().get_parser(prog_name) parser.add_argument( 'stack', metavar='', @@ -69,8 +70,8 @@ def take_action(self, parsed_args): raise exc.CommandError(str(ex)) formatters = { - 'links': heat_utils.link_formatter, - 'resource_properties': heat_utils.json_formatter + 'links': common.LinkColumn, + 'resource_properties': common.JsonColumn } columns = [] @@ -95,7 +96,7 @@ def formatter_namespace(self): return 'heatclient.event.formatter.list' def get_parser(self, prog_name): - parser = super(ListEvent, self).get_parser(prog_name) + parser = super().get_parser(prog_name) parser.add_argument( 'stack', metavar='', @@ -175,6 +176,20 @@ def take_action(self, parsed_args): else: nested_depth = 0 + if parsed_args.sort: + sorts = [] + sort_keys = [] + for sort in parsed_args.sort: + if sort.startswith(":"): + sorts.append(":".join(["event_time", sort.lstrip(":")])) + else: + sorts.append(sort) + sort_keys.append(sort.split(":")[0]) + kwargs['sort_keys'] = sort_keys + + if ":" in parsed_args.sort[0]: + kwargs['sort_dir'] = parsed_args.sort[0].split(":")[1] + if parsed_args.follow: if parsed_args.formatter != 'log': msg = _('--follow can only be specified with --format log') @@ -207,12 +222,6 @@ def take_action(self, parsed_args): limit=parsed_args.limit) if parsed_args.sort: - sorts = [] - for sort in parsed_args.sort: - if sort.startswith(":"): - sorts.append(":".join(["event_time", sort.lstrip(":")])) - else: - sorts.append(sort) events = utils.sort_items(events, ','.join(sorts)) if parsed_args.formatter == 'log': diff --git a/heatclient/osc/v1/resource.py b/heatclient/osc/v1/resource.py index e29b0778..b2d893f0 100644 --- a/heatclient/osc/v1/resource.py +++ b/heatclient/osc/v1/resource.py @@ -20,8 +20,7 @@ from osc_lib.i18n import _ from osc_lib import utils from oslo_serialization import jsonutils -import six -from six.moves.urllib import request +from urllib import request from heatclient.common import format_utils from heatclient.common import utils as heat_utils @@ -34,7 +33,7 @@ class ResourceShow(command.ShowOne): log = logging.getLogger(__name__ + '.ResourceShowStack') def get_parser(self, prog_name): - parser = super(ResourceShow, self).get_parser(prog_name) + parser = super().get_parser(prog_name) parser.add_argument( 'stack', metavar='', @@ -81,7 +80,7 @@ def formatter_namespace(self): return 'heatclient.resource.formatter.list' def get_parser(self, prog_name): - parser = super(ResourceList, self).get_parser(prog_name) + parser = super().get_parser(prog_name) parser.add_argument( 'stack', metavar='', @@ -154,7 +153,7 @@ class ResourceMetadata(format_utils.JsonFormat): log = logging.getLogger(__name__ + ".ResourceMetadata") def get_parser(self, prog_name): - parser = super(ResourceMetadata, self).get_parser(prog_name) + parser = super().get_parser(prog_name) parser.add_argument( 'stack', metavar='', @@ -184,8 +183,8 @@ def _resource_metadata(heat_client, args): {'stack': args.stack, 'resource': args.resource}) - data = list(six.itervalues(metadata)) - columns = list(six.iterkeys(metadata)) + data = list(metadata.values()) + columns = list(metadata.keys()) return columns, data @@ -195,7 +194,7 @@ class ResourceSignal(command.Command): log = logging.getLogger(__name__ + ".ResourceSignal") def get_parser(self, prog_name): - parser = super(ResourceSignal, self).get_parser(prog_name) + parser = super().get_parser(prog_name) parser.add_argument( 'stack', metavar='', @@ -262,7 +261,7 @@ class ResourceMarkUnhealthy(command.Command): log = logging.getLogger(__name__ + ".ResourceMarkUnhealthy") def get_parser(self, prog_name): - parser = super(ResourceMarkUnhealthy, self).get_parser(prog_name) + parser = super().get_parser(prog_name) parser.add_argument( 'stack', metavar='', diff --git a/heatclient/osc/v1/resource_type.py b/heatclient/osc/v1/resource_type.py index 6d55ab5a..6e5cf634 100644 --- a/heatclient/osc/v1/resource_type.py +++ b/heatclient/osc/v1/resource_type.py @@ -18,7 +18,6 @@ from osc_lib.command import command from osc_lib import exceptions as exc from osc_lib.i18n import _ -import six from heatclient.common import format_utils from heatclient.common import utils as heat_utils @@ -31,8 +30,7 @@ class ResourceTypeShow(format_utils.YamlFormat): log = logging.getLogger(__name__ + ".ResourceTypeShow") def get_parser(self, prog_name): - parser = super(ResourceTypeShow, - self).get_parser(prog_name) + parser = super().get_parser(prog_name) parser.add_argument( 'resource_type', metavar='', @@ -80,8 +78,8 @@ def _show_resourcetype(heat_client, parsed_args): raise exc.CommandError( _('Resource type not found: %s') % parsed_args.resource_type) - rows = list(six.itervalues(data)) - columns = list(six.iterkeys(data)) + rows = list(data.values()) + columns = list(data.keys()) return columns, rows @@ -91,8 +89,7 @@ class ResourceTypeList(command.Lister): log = logging.getLogger(__name__ + '.ResourceTypeList') def get_parser(self, prog_name): - parser = super(ResourceTypeList, - self).get_parser(prog_name) + parser = super().get_parser(prog_name) parser.add_argument( '--filter', dest='filter', diff --git a/heatclient/osc/v1/service.py b/heatclient/osc/v1/service.py index 09dec4d1..3ca042ca 100644 --- a/heatclient/osc/v1/service.py +++ b/heatclient/osc/v1/service.py @@ -25,7 +25,7 @@ class ListService(command.Lister): log = logging.getLogger(__name__ + ".ListService") def get_parser(self, prog_name): - parser = super(ListService, self).get_parser(prog_name) + parser = super().get_parser(prog_name) return parser def take_action(self, parsed_args): diff --git a/heatclient/osc/v1/snapshot.py b/heatclient/osc/v1/snapshot.py index b5cc781d..79eea08d 100644 --- a/heatclient/osc/v1/snapshot.py +++ b/heatclient/osc/v1/snapshot.py @@ -14,12 +14,12 @@ """Orchestration v1 Stack Snapshot implementations.""" import logging +import sys from osc_lib.command import command from osc_lib import exceptions as exc from osc_lib.i18n import _ from osc_lib import utils -import six from heatclient.common import format_utils from heatclient import exc as heat_exc @@ -31,7 +31,7 @@ class ListSnapshot(command.Lister): log = logging.getLogger(__name__ + ".ListSnapshot") def get_parser(self, prog_name): - parser = super(ListSnapshot, self).get_parser(prog_name) + parser = super().get_parser(prog_name) parser.add_argument( 'stack', metavar='', @@ -40,7 +40,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug('take_action(%s)' % parsed_args) + self.log.debug('take_action(%s)', parsed_args) heat_client = self.app.client_manager.orchestration return self._list_snapshot(heat_client, parsed_args) @@ -52,7 +52,14 @@ def _list_snapshot(self, heat_client, parsed_args): raise exc.CommandError(_('Stack not found: %s') % parsed_args.stack) - columns = ['id', 'name', 'status', 'status_reason', 'creation_time'] + columns = [ + 'id', + 'name', + 'action', + 'status', + 'status_reason', + 'creation_time' + ] return ( columns, (utils.get_dict_properties(s, columns) @@ -66,7 +73,7 @@ class ShowSnapshot(format_utils.YamlFormat): log = logging.getLogger(__name__ + ".ShowSnapshot") def get_parser(self, prog_name): - parser = super(ShowSnapshot, self).get_parser(prog_name) + parser = super().get_parser(prog_name) parser.add_argument( 'stack', metavar='', @@ -80,7 +87,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug('take_action(%s)' % parsed_args) + self.log.debug('take_action(%s)', parsed_args) heat_client = self.app.client_manager.orchestration return self._show_snapshot(heat_client, parsed_args.stack, parsed_args.snapshot) @@ -94,8 +101,8 @@ def _show_snapshot(self, heat_client, stack_id, snapshot_id): % {'snapshot_id': snapshot_id, 'stack_id': stack_id}) - rows = list(six.itervalues(data)) - columns = list(six.iterkeys(data)) + rows = list(data.values()) + columns = list(data.keys()) return columns, rows @@ -105,7 +112,7 @@ class RestoreSnapshot(command.Command): log = logging.getLogger(__name__ + ".RestoreSnapshot") def get_parser(self, prog_name): - parser = super(RestoreSnapshot, self).get_parser(prog_name) + parser = super().get_parser(prog_name) parser.add_argument( 'stack', metavar='', @@ -119,7 +126,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug('take_action(%s)' % parsed_args) + self.log.debug('take_action(%s)', parsed_args) heat_client = self.app.client_manager.orchestration return self._restore_snapshot(heat_client, parsed_args) @@ -141,7 +148,7 @@ class CreateSnapshot(command.ShowOne): log = logging.getLogger(__name__ + ".CreateSnapshot") def get_parser(self, prog_name): - parser = super(CreateSnapshot, self).get_parser(prog_name) + parser = super().get_parser(prog_name) parser.add_argument( 'stack', metavar='', @@ -155,7 +162,7 @@ def get_parser(self, prog_name): return parser def take_action(self, parsed_args): - self.log.debug('take_action(%s)' % parsed_args) + self.log.debug('take_action(%s)', parsed_args) heat_client = self.app.client_manager.orchestration try: @@ -168,6 +175,7 @@ def take_action(self, parsed_args): columns = [ 'ID', 'name', + 'action', 'status', 'status_reason', 'data', @@ -181,7 +189,7 @@ class DeleteSnapshot(command.Command): log = logging.getLogger(__name__ + ".DeleteSnapshot") def get_parser(self, prog_name): - parser = super(DeleteSnapshot, self).get_parser(prog_name) + parser = super().get_parser(prog_name) parser.add_argument( 'stack', metavar='', @@ -192,11 +200,34 @@ def get_parser(self, prog_name): metavar='', help=_('ID of stack snapshot') ) + parser.add_argument( + '-y', '--yes', + action='store_true', + help=_('Skip yes/no prompt (assume yes)') + ) return parser def take_action(self, parsed_args): - self.log.debug('take_action(%s)' % parsed_args) + self.log.debug('take_action(%s)', parsed_args) heat_client = self.app.client_manager.orchestration + msg = ('User did not confirm snapshot delete ' + '%sso taking no action.') + try: + if not parsed_args.yes and sys.stdin.isatty(): + sys.stdout.write( + _('Are you sure you want to delete the snapshot of this ' + 'stack [Y/N]?')) + prompt_response = sys.stdin.readline().lower() + if not prompt_response.startswith('y'): + self.log.info(msg, '') + return + except KeyboardInterrupt: # ctrl-c + self.log.info(msg, '(ctrl-c) ') + return + except EOFError: # ctrl-d + self.log.info(msg, '(ctrl-d) ') + return + try: heat_client.stacks.snapshot_delete(parsed_args.stack, parsed_args.snapshot) diff --git a/heatclient/osc/v1/software_config.py b/heatclient/osc/v1/software_config.py index af27be71..89378310 100644 --- a/heatclient/osc/v1/software_config.py +++ b/heatclient/osc/v1/software_config.py @@ -18,8 +18,7 @@ from osc_lib.command import command from osc_lib import exceptions as exc from osc_lib import utils -import six -from six.moves.urllib import request +from urllib import request import yaml from heatclient._i18n import _ @@ -35,7 +34,7 @@ class DeleteConfig(command.Command): log = logging.getLogger(__name__ + ".DeleteConfig") def get_parser(self, prog_name): - parser = super(DeleteConfig, self).get_parser(prog_name) + parser = super().get_parser(prog_name) parser.add_argument( 'config', metavar='', @@ -77,10 +76,11 @@ class ListConfig(command.Lister): log = logging.getLogger(__name__ + ".ListConfig") def get_parser(self, prog_name): - parser = super(ListConfig, self).get_parser(prog_name) + parser = super().get_parser(prog_name) parser.add_argument( '--limit', metavar='', + type=int, help=_('Limit the number of configs returned') ) parser.add_argument( @@ -114,7 +114,7 @@ class CreateConfig(format_utils.JsonFormat): log = logging.getLogger(__name__ + ".CreateConfig") def get_parser(self, prog_name): - parser = super(CreateConfig, self).get_parser(prog_name) + parser = super().get_parser(prog_name) parser.add_argument( 'name', metavar='', @@ -181,8 +181,8 @@ def _create_config(heat_client, args): config['name'] = args.name sc = heat_client.software_configs.create(**config).to_dict() - rows = list(six.itervalues(sc)) - columns = list(six.iterkeys(sc)) + rows = list(sc.values()) + columns = list(sc.keys()) return columns, rows @@ -192,7 +192,7 @@ class ShowConfig(format_utils.YamlFormat): log = logging.getLogger(__name__ + ".ShowConfig") def get_parser(self, prog_name): - parser = super(ShowConfig, self).get_parser(prog_name) + parser = super().get_parser(prog_name) parser.add_argument( 'config', metavar='', diff --git a/heatclient/osc/v1/software_deployment.py b/heatclient/osc/v1/software_deployment.py index 37abcb46..6c5cfd92 100644 --- a/heatclient/osc/v1/software_deployment.py +++ b/heatclient/osc/v1/software_deployment.py @@ -33,7 +33,7 @@ class CreateDeployment(format_utils.YamlFormat): log = logging.getLogger(__name__ + '.CreateDeployment') def get_parser(self, prog_name): - parser = super(CreateDeployment, self).get_parser(prog_name) + parser = super().get_parser(prog_name) parser.add_argument( 'name', metavar='', @@ -136,7 +136,7 @@ class DeleteDeployment(command.Command): log = logging.getLogger(__name__ + '.DeleteDeployment') def get_parser(self, prog_name): - parser = super(DeleteDeployment, self).get_parser(prog_name) + parser = super().get_parser(prog_name) parser.add_argument( 'deployment', metavar='', @@ -185,7 +185,7 @@ class ListDeployment(command.Lister): log = logging.getLogger(__name__ + '.ListDeployment') def get_parser(self, prog_name): - parser = super(ListDeployment, self).get_parser(prog_name) + parser = super().get_parser(prog_name) parser.add_argument( '--server', metavar='', @@ -225,7 +225,7 @@ class ShowDeployment(command.ShowOne): log = logging.getLogger(__name__ + ".ShowSoftwareDeployment") def get_parser(self, prog_name): - parser = super(ShowDeployment, self).get_parser(prog_name) + parser = super().get_parser(prog_name) parser.add_argument( 'deployment', metavar='', @@ -246,7 +246,9 @@ def take_action(self, parsed_args): data = heat_client.software_deployments.get( deployment_id=parsed_args.deployment) except heat_exc.HTTPNotFound: - raise exc.CommandError(_('Software Deployment not found: %s') % id) + raise exc.CommandError( + _('Software Deployment not found: %s') + % parsed_args.deployment) else: columns = [ 'id', @@ -270,7 +272,7 @@ class ShowMetadataDeployment(command.Command): log = logging.getLogger(__name__ + '.ShowMetadataDeployment') def get_parser(self, prog_name): - parser = super(ShowMetadataDeployment, self).get_parser(prog_name) + parser = super().get_parser(prog_name) parser.add_argument( 'server', metavar='', @@ -292,7 +294,7 @@ class ShowOutputDeployment(command.Command): log = logging.getLogger(__name__ + '.ShowOutputDeployment') def get_parser(self, prog_name): - parser = super(ShowOutputDeployment, self).get_parser(prog_name) + parser = super().get_parser(prog_name) parser.add_argument( 'deployment', metavar='', @@ -338,7 +340,7 @@ def take_action(self, parsed_args): if outputs: if parsed_args.all: print('output_values:\n') - for k in outputs.keys(): + for k in outputs: format_utils.print_software_deployment_output( data=outputs, name=k, long=parsed_args.long) else: diff --git a/heatclient/osc/v1/stack.py b/heatclient/osc/v1/stack.py index 719c0de7..92010a8c 100644 --- a/heatclient/osc/v1/stack.py +++ b/heatclient/osc/v1/stack.py @@ -16,15 +16,15 @@ import logging import sys +from openstackclient.identity import common as identity_common from osc_lib.command import command from osc_lib import exceptions as exc from osc_lib import utils from oslo_serialization import jsonutils -import six -from six.moves.urllib import request +from urllib import request +import yaml from heatclient._i18n import _ -from heatclient._i18n import _LI from heatclient.common import event_utils from heatclient.common import format_utils from heatclient.common import hook_utils @@ -32,6 +32,7 @@ from heatclient.common import template_utils from heatclient.common import utils as heat_utils from heatclient import exc as heat_exc +from heatclient.osc.v1 import common class CreateStack(command.ShowOne): @@ -40,13 +41,20 @@ class CreateStack(command.ShowOne): log = logging.getLogger(__name__ + '.CreateStack') def get_parser(self, prog_name): - parser = super(CreateStack, self).get_parser(prog_name) + parser = super().get_parser(prog_name) parser.add_argument( '-e', '--environment', metavar='', action='append', help=_('Path to the environment. Can be specified multiple times') ) + parser.add_argument( + '-s', '--files-container', + metavar='', + help=_('Swift files container name. Local files other than ' + 'root template would be ignored. If other files are not ' + 'found in swift, heat engine would raise an error.') + ) parser.add_argument( '--timeout', metavar='', @@ -60,9 +68,9 @@ def get_parser(self, prog_name): action='append', help=_('Name of a resource to set a pre-create hook to. Resources ' 'in nested stacks can be set using slash as a separator: ' - 'nested_stack/another/my_resource. You can use wildcards ' - 'to match multiple stacks or resources: ' - 'nested_stack/an*/*_resource. This can be specified ' + '``nested_stack/another/my_resource``. You can use ' + 'wildcards to match multiple stacks or resources: ' + '``nested_stack/an*/*_resource``. This can be specified ' 'multiple times') ) parser.add_argument( @@ -90,6 +98,14 @@ def get_parser(self, prog_name): action='store_true', help=_('Wait until stack goes to CREATE_COMPLETE or CREATE_FAILED') ) + parser.add_argument( + '--poll', + metavar='SECONDS', + type=int, + default=5, + help=_('Poll interval in seconds for use with --wait, ' + 'defaults to 5.') + ) parser.add_argument( '--tags', metavar='', @@ -122,13 +138,15 @@ def take_action(self, parsed_args): tpl_files, template = template_utils.process_template_path( parsed_args.template, - object_request=http.authenticated_fetcher(client)) + object_request=http.authenticated_fetcher(client), + fetch_child=parsed_args.files_container is None) env_files_list = [] env_files, env = ( template_utils.process_multiple_environments_and_files( env_paths=parsed_args.environment, - env_list_tracker=env_files_list)) + env_list_tracker=env_files_list, + fetch_env_files=parsed_args.files_container is None)) parameters = heat_utils.format_all_parameters( parsed_args.parameter, @@ -152,6 +170,9 @@ def take_action(self, parsed_args): if env_files_list: fields['environment_files'] = env_files_list + if parsed_args.files_container: + fields['files_container'] = parsed_args.files_container + if parsed_args.tags: fields['tags'] = parsed_args.tags if parsed_args.timeout: @@ -161,13 +182,13 @@ def take_action(self, parsed_args): stack = client.stacks.preview(**fields) formatters = { - 'description': heat_utils.text_wrap_formatter, - 'template_description': heat_utils.text_wrap_formatter, - 'stack_status_reason': heat_utils.text_wrap_formatter, - 'parameters': heat_utils.json_formatter, - 'outputs': heat_utils.json_formatter, - 'resources': heat_utils.json_formatter, - 'links': heat_utils.link_formatter, + 'description': common.TextWrapColumn, + 'template_description': common.TextWrapColumn, + 'stack_status_reason': common.TextWrapColumn, + 'parameters': common.JsonColumn, + 'outputs': common.JsonColumn, + 'resources': common.JsonColumn, + 'links': common.LinkColumn, } columns = [] @@ -184,7 +205,8 @@ def take_action(self, parsed_args): stack = client.stacks.create(**fields)['stack'] if parsed_args.wait: stack_status, msg = event_utils.poll_for_events( - client, parsed_args.name, action='CREATE') + client, parsed_args.name, action='CREATE', + poll_period=parsed_args.poll) if stack_status == 'CREATE_FAILED': raise exc.CommandError(msg) @@ -197,11 +219,18 @@ class UpdateStack(command.ShowOne): log = logging.getLogger(__name__ + '.UpdateStack') def get_parser(self, prog_name): - parser = super(UpdateStack, self).get_parser(prog_name) + parser = super().get_parser(prog_name) parser.add_argument( '-t', '--template', metavar='