Retire Packaging Deb project repos
This commit is part of a series to retire the Packaging Deb project. Step 2 is to remove all content from the project repos, replacing it with a README notification where to find ongoing work, and how to recover the repo if needed at some future point (as in https://docs.openstack.org/infra/manual/drivers.html#retiring-a-project). Change-Id: Ic4a7ec1aeaa1e10887fd1ce5808a7dcaac3357d6
This commit is contained in:
parent
6462e5183f
commit
13e7c438bb
|
@ -1,8 +0,0 @@
|
|||
[run]
|
||||
branch = True
|
||||
source = heatclient
|
||||
omit = heatclient/tests/*
|
||||
|
||||
[report]
|
||||
ignore_errors = True
|
||||
precision = 2
|
|
@ -1,19 +0,0 @@
|
|||
.coverage*
|
||||
.venv
|
||||
*,cover
|
||||
cover
|
||||
*.pyc
|
||||
AUTHORS
|
||||
build
|
||||
dist
|
||||
ChangeLog
|
||||
run_tests.err.log
|
||||
.tox
|
||||
doc/source/api
|
||||
doc/build
|
||||
*.egg
|
||||
*.eggs
|
||||
heatclient/versioninfo
|
||||
*.egg-info
|
||||
*.log
|
||||
.testrepository
|
|
@ -1,4 +0,0 @@
|
|||
[gerrit]
|
||||
host=review.openstack.org
|
||||
port=29418
|
||||
project=openstack/python-heatclient.git
|
|
@ -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
|
|
@ -1,16 +0,0 @@
|
|||
If you would like to contribute to the development of OpenStack,
|
||||
you must follow the steps documented at:
|
||||
|
||||
http://docs.openstack.org/infra/manual/developers.html#development-workflow
|
||||
|
||||
Once those steps have been completed, changes to OpenStack
|
||||
should be submitted for review via the Gerrit tool, following
|
||||
the workflow documented at:
|
||||
|
||||
http://docs.openstack.org/infra/manual/developers.html#development-workflow
|
||||
|
||||
Pull requests submitted through GitHub will be ignored.
|
||||
|
||||
Bugs should be filed on Launchpad, not GitHub:
|
||||
|
||||
https://bugs.launchpad.net/python-heatclient
|
175
LICENSE
175
LICENSE
|
@ -1,175 +0,0 @@
|
|||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
|
@ -0,0 +1,14 @@
|
|||
This project is no longer maintained.
|
||||
|
||||
The contents of this repository are still available in the Git
|
||||
source code management system. To see the contents of this
|
||||
repository before it reached its end of life, please check out the
|
||||
previous commit with "git checkout HEAD^1".
|
||||
|
||||
For ongoing work on maintaining OpenStack packages in the Debian
|
||||
distribution, please see the Debian OpenStack packaging team at
|
||||
https://wiki.debian.org/OpenStack/.
|
||||
|
||||
For any further questions, please email
|
||||
openstack-dev@lists.openstack.org or join #openstack-dev on
|
||||
Freenode.
|
48
README.rst
48
README.rst
|
@ -1,48 +0,0 @@
|
|||
========================
|
||||
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://img.shields.io/pypi/v/python-heatclient.svg
|
||||
:target: https://pypi.python.org/pypi/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
|
||||
|
||||
This is a client library for Heat built on the Heat orchestration API. It
|
||||
provides a Python API (the ``heatclient`` module) and a command-line tool
|
||||
(``heat``).
|
||||
|
||||
* Free software: Apache license
|
||||
* `PyPi`_ - package installation
|
||||
* `Online Documentation`_
|
||||
* `Launchpad project`_ - release management
|
||||
* `Blueprints`_ - feature specifications
|
||||
* `Bugs`_ - issue tracking
|
||||
* `Source`_
|
||||
* `Specs`_
|
||||
* `Template`_
|
||||
* `How to Contribute`_
|
||||
|
||||
.. _PyPi: https://pypi.python.org/pypi/python-heatclient
|
||||
.. _Online Documentation: http://docs.openstack.org/developer/python-heatclient
|
||||
.. _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/
|
|
@ -1,2 +0,0 @@
|
|||
build/
|
||||
source/ref/
|
90
doc/Makefile
90
doc/Makefile
|
@ -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 <target>' where <target> 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."
|
|
@ -1,269 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
#
|
||||
# python-heatclient documentation build configuration file, created by
|
||||
# sphinx-quickstart on Sun Dec 6 14:19:25 2009.
|
||||
#
|
||||
# This file is execfile()d with the current directory set to its containing
|
||||
# dir.
|
||||
#
|
||||
# Note that not all possible configuration values are present in this
|
||||
# autogenerated file.
|
||||
#
|
||||
# All configuration values have a default; values that are commented out
|
||||
# serve to show the default.
|
||||
|
||||
import os
|
||||
|
||||
# If extensions (or modules to document with autodoc) are in another directory,
|
||||
# add these directories to sys.path here. If the directory is relative to the
|
||||
# documentation root, use os.path.abspath to make it absolute, like shown here.
|
||||
# sys.path.append(os.path.abspath('.'))
|
||||
|
||||
exec(open(os.path.join("ext", "gen_ref.py")).read())
|
||||
# -- General configuration ----------------------------------------------------
|
||||
|
||||
# Add any Sphinx extension module names here, as strings. They can be
|
||||
# extensions
|
||||
# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
|
||||
extensions = ['sphinx.ext.autodoc', 'openstackdocstheme']
|
||||
|
||||
# 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']
|
||||
|
||||
# The suffix of source filenames.
|
||||
source_suffix = '.rst'
|
||||
|
||||
# The encoding of source files.
|
||||
# source_encoding = 'utf-8'
|
||||
|
||||
# The master toctree document.
|
||||
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
|
||||
|
||||
# There are two options for replacing |today|: either, you set today to some
|
||||
# non-false value, then it is used:
|
||||
# today = ''
|
||||
# Else, today_fmt is used as the format for a strftime call.
|
||||
# today_fmt = '%B %d, %Y'
|
||||
|
||||
# List of patterns, relative to source directory, that match files and
|
||||
# directories to ignore when looking for source files.
|
||||
exclude_patterns = ['**/#*', '**~', '**/#*#']
|
||||
|
||||
# The reST default role (used for this markup: `text`)
|
||||
# to use for all documents.
|
||||
# default_role = None
|
||||
|
||||
# If true, '()' will be appended to :func: etc. cross-reference text.
|
||||
# add_function_parentheses = True
|
||||
|
||||
# If true, the current module name will be prepended to all description
|
||||
# unit titles (such as .. function::).
|
||||
# add_module_names = True
|
||||
|
||||
# If true, sectionauthor and moduleauthor directives will be shown in the
|
||||
# output. They are ignored by default.
|
||||
# show_authors = False
|
||||
|
||||
# The name of the Pygments (syntax highlighting) style to use.
|
||||
pygments_style = 'sphinx'
|
||||
|
||||
# A list of ignored prefixes for module index sorting.
|
||||
# modindex_common_prefix = []
|
||||
|
||||
primary_domain = 'py'
|
||||
nitpicky = False
|
||||
|
||||
|
||||
# -- Options for HTML output --------------------------------------------------
|
||||
|
||||
# The theme to use for HTML and HTML Help pages. See the documentation for
|
||||
# a list of builtin themes.
|
||||
# html_theme_path = ['.']
|
||||
html_theme = 'openstackdocs'
|
||||
|
||||
# Theme options are theme-specific and customize the look and feel of a theme
|
||||
# further. For a list of options available for each theme, see the
|
||||
# documentation.
|
||||
html_theme_options = {
|
||||
"nosidebar": "false"
|
||||
}
|
||||
|
||||
# Add any paths that contain custom themes here, relative to this directory.
|
||||
# html_theme_path = []
|
||||
|
||||
# The name for this set of Sphinx documents. If None, it defaults to
|
||||
# "<project> v<release> documentation".
|
||||
# html_title = None
|
||||
|
||||
# A shorter title for the navigation bar. Default is the same as html_title.
|
||||
# html_short_title = None
|
||||
|
||||
# The name of an image file (relative to this directory) to place at the top
|
||||
# of the sidebar.
|
||||
# html_logo = None
|
||||
|
||||
# The name of an image file (within the static path) to use as favicon of the
|
||||
# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
|
||||
# pixels large.
|
||||
# html_favicon = None
|
||||
|
||||
# Add any paths that contain custom static files (such as style sheets) here,
|
||||
# relative to this directory. They are copied after the builtin static files,
|
||||
# so a file named "default.css" will overwrite the builtin "default.css".
|
||||
# html_static_path = ['_static']
|
||||
|
||||
# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
|
||||
# using the given strftime format.
|
||||
html_last_updated_fmt = '%Y-%m-%d %H:%M'
|
||||
|
||||
|
||||
# If true, SmartyPants will be used to convert quotes and dashes to
|
||||
# typographically correct entities.
|
||||
# html_use_smartypants = True
|
||||
|
||||
# Custom sidebar templates, maps document names to template names.
|
||||
# html_sidebars = {}
|
||||
|
||||
# Additional templates that should be rendered to pages, maps page names to
|
||||
# template names.
|
||||
# html_additional_pages = {}
|
||||
|
||||
# If false, no module index is generated.
|
||||
# html_domain_indices = True
|
||||
|
||||
# If false, no index is generated.
|
||||
# html_use_index = True
|
||||
|
||||
# If true, the index is split into individual pages for each letter.
|
||||
# html_split_index = False
|
||||
|
||||
# If true, links to the reST sources are added to the pages.
|
||||
# html_show_sourcelink = True
|
||||
|
||||
# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
|
||||
# html_show_sphinx = True
|
||||
|
||||
# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
|
||||
# html_show_copyright = True
|
||||
|
||||
# If true, an OpenSearch description file will be output, and all pages will
|
||||
# contain a <link> tag referring to it. The value of this option must be the
|
||||
# base URL from which the finished HTML is served.
|
||||
# html_use_opensearch = ''
|
||||
|
||||
# This is the file name suffix for HTML files (e.g. ".xhtml").
|
||||
# html_file_suffix = None
|
||||
|
||||
# Output file base name for HTML help builder.
|
||||
htmlhelp_basename = 'python-heatclientdoc'
|
||||
|
||||
# -- Options for LaTeX output -------------------------------------------------
|
||||
|
||||
latex_elements = {
|
||||
# The paper size ('letterpaper' or 'a4paper').
|
||||
# 'papersize': 'letterpaper',
|
||||
|
||||
# The font size ('10pt', '11pt' or '12pt').
|
||||
# 'pointsize': '10pt',
|
||||
|
||||
# Additional stuff for the LaTeX preamble.
|
||||
# 'preamble': '',
|
||||
}
|
||||
|
||||
# Grouping the document tree into LaTeX files. List of tuples
|
||||
# (source start file, target name, title, author, documentclass [howto/manual])
|
||||
latex_documents = [
|
||||
('index', 'python-heatclient.tex', 'python-heatclient Documentation',
|
||||
u'OpenStack Foundation', 'manual'),
|
||||
]
|
||||
|
||||
# The name of an image file (relative to this directory) to place at the top of
|
||||
# the title page.
|
||||
# latex_logo = None
|
||||
|
||||
# For "manual" documents, if this is true, then toplevel headings are parts,
|
||||
# not chapters.
|
||||
# latex_use_parts = False
|
||||
|
||||
# If true, show page references after internal links.
|
||||
# latex_show_pagerefs = False
|
||||
|
||||
# If true, show URL addresses after external links.
|
||||
# latex_show_urls = False
|
||||
|
||||
# Documents to append as an appendix to all manuals.
|
||||
# latex_appendices = []
|
||||
|
||||
# If false, no module index is generated.
|
||||
# latex_domain_indices = True
|
||||
|
||||
|
||||
# -- Options for manual page output -------------------------------------------
|
||||
|
||||
# One entry per manual page. List of tuples
|
||||
# (source start file, name, description, authors, manual section).
|
||||
man_pages = [
|
||||
('man/heat', 'heat',
|
||||
u'Command line access to the heat project.',
|
||||
[u'Heat Developers'], 1),
|
||||
]
|
||||
|
||||
# If true, show URL addresses after external links.
|
||||
# man_show_urls = False
|
||||
|
||||
|
||||
# -- Options for Texinfo output -----------------------------------------------
|
||||
|
||||
# Grouping the document tree into Texinfo files. List of tuples
|
||||
# (source start file, target name, title, author,
|
||||
# dir menu entry, description, category)
|
||||
texinfo_documents = [
|
||||
('index', 'Heat', u'Heat Documentation',
|
||||
u'Heat Developers', 'Heat', 'One line description of project.',
|
||||
'Miscellaneous'),
|
||||
]
|
||||
|
||||
# Documents to append as an appendix to all manuals.
|
||||
# texinfo_appendices = []
|
||||
|
||||
# If false, no module index is generated.
|
||||
# texinfo_domain_indices = True
|
||||
|
||||
# How to display URL addresses: 'footnote', 'no', or 'inline'.
|
||||
# texinfo_show_urls = 'footnote'
|
||||
|
||||
# -- Options for openstackdocstheme -------------------------------------------
|
||||
repository_name = 'openstack/python-heatclient'
|
||||
bug_project = 'python-heatclient'
|
||||
bug_tag = ''
|
|
@ -1,59 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
ROOT = os.path.abspath(os.path.join(BASE_DIR, "..", "..", ".."))
|
||||
|
||||
sys.path.insert(0, ROOT)
|
||||
sys.path.insert(0, BASE_DIR)
|
||||
|
||||
|
||||
def gen_ref(ver, title, names):
|
||||
refdir = os.path.join(BASE_DIR, "ref")
|
||||
pkg = "heatclient"
|
||||
if ver:
|
||||
pkg = "%s.%s" % (pkg, ver)
|
||||
refdir = os.path.join(refdir, ver)
|
||||
if not os.path.exists(refdir):
|
||||
os.makedirs(refdir)
|
||||
idxpath = os.path.join(refdir, "index.rst")
|
||||
with open(idxpath, "w") as idx:
|
||||
idx.write(("%(title)s\n"
|
||||
"%(signs)s\n"
|
||||
"\n"
|
||||
".. toctree::\n"
|
||||
" :maxdepth: 1\n"
|
||||
"\n") % {"title": title, "signs": "=" * len(title)})
|
||||
for name in names:
|
||||
idx.write(" %s\n" % name)
|
||||
rstpath = os.path.join(refdir, "%s.rst" % name)
|
||||
with open(rstpath, "w") as rst:
|
||||
rst.write(("%(title)s\n"
|
||||
"%(signs)s\n"
|
||||
"\n"
|
||||
".. automodule:: %(pkg)s.%(name)s\n"
|
||||
" :members:\n"
|
||||
" :undoc-members:\n"
|
||||
" :show-inheritance:\n"
|
||||
" :noindex:\n")
|
||||
% {"title": name.capitalize(),
|
||||
"signs": "=" * len(name),
|
||||
"pkg": pkg, "name": name})
|
||||
|
||||
gen_ref("", "Client Reference", ["client", "exc"])
|
||||
gen_ref("v1", "Version 1 API Reference",
|
||||
["stacks", "resources", "events", "actions",
|
||||
"software_configs", "software_deployments"])
|
|
@ -1,98 +0,0 @@
|
|||
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`).
|
||||
|
||||
Python API
|
||||
==========
|
||||
|
||||
In order to use the python api directly, you must first obtain an auth
|
||||
token and identify which endpoint you wish to speak to::
|
||||
|
||||
>>> tenant_id = 'b363706f891f48019483f8bd6503c54b'
|
||||
>>> heat_url = 'http://heat.example.org:8004/v1/%s' % tenant_id
|
||||
>>> auth_token = '3bcc3d3a03f44e3d8377f9247b0ad155'
|
||||
|
||||
Once you have done so, you can use the API like so::
|
||||
|
||||
>>> from heatclient.client import Client
|
||||
>>> heat = Client('1', endpoint=heat_url, token=auth_token)
|
||||
|
||||
Alternatively, you can create a client instance using the keystoneauth session API::
|
||||
|
||||
>>> from keystoneauth1 import loading
|
||||
>>> from keystoneauth1 import session
|
||||
>>> from heatclient import client
|
||||
>>> loader = loading.get_plugin_loader('password')
|
||||
>>> auth = loader.load_from_options(auth_url=AUTH_URL,
|
||||
... username=USERNAME,
|
||||
... password=PASSWORD,
|
||||
... project_id=PROJECT_ID)
|
||||
>>> sess = session.Session(auth=auth)
|
||||
>>> heat = client.Client('1', session=sess)
|
||||
>>> heat.stacks.list()
|
||||
|
||||
If you have PROJECT_NAME instead of a PROJECT_ID, use the project_name
|
||||
parameter. Similarly, if your cloud uses keystone v3 and you have a DOMAIN_NAME
|
||||
or DOMAIN_ID, provide it as `user_domain_(name|id)` and if you are using a
|
||||
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
|
||||
|
||||
Reference
|
||||
---------
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
|
||||
ref/index
|
||||
ref/v1/index
|
||||
|
||||
Command-line Tool
|
||||
=================
|
||||
|
||||
In order to use the CLI, you must provide your OpenStack username,
|
||||
password, tenant, and auth endpoint. Use the corresponding
|
||||
configuration options (``--os-username``, ``--os-password``,
|
||||
``--os-tenant-id``, and ``--os-auth-url``) or set them in environment
|
||||
variables::
|
||||
|
||||
export OS_USERNAME=user
|
||||
export OS_PASSWORD=pass
|
||||
export OS_TENANT_ID=b363706f891f48019483f8bd6503c54b
|
||||
export OS_AUTH_URL=http://auth.example.com:5000/v2.0
|
||||
|
||||
The command line tool will attempt to reauthenticate using your
|
||||
provided credentials for every request. You can override this behavior
|
||||
by manually supplying an auth token using ``--heat-url`` and
|
||||
``--os-auth-token``. You can alternatively set these environment
|
||||
variables::
|
||||
|
||||
export HEAT_URL=http://heat.example.org:8004/v1/b363706f891f48019483f8bd6503c54b
|
||||
export OS_AUTH_TOKEN=3bcc3d3a03f44e3d8377f9247b0ad155
|
||||
|
||||
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`_.
|
||||
|
||||
.. _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
|
|
@ -1,98 +0,0 @@
|
|||
====
|
||||
heat
|
||||
====
|
||||
|
||||
.. program:: heat
|
||||
|
||||
SYNOPSIS
|
||||
========
|
||||
|
||||
`heat` [options] <command> [command-options]
|
||||
|
||||
`heat help`
|
||||
|
||||
`heat help` <command>
|
||||
|
||||
|
||||
DESCRIPTION
|
||||
===========
|
||||
|
||||
`heat` is a command line client for controlling OpenStack Heat.
|
||||
|
||||
Before the `heat` command is issued, ensure the environment contains
|
||||
the necessary variables so that the CLI can pass user credentials to
|
||||
the server.
|
||||
See `Getting Credentials for a CLI` section of `OpenStack CLI Guide`
|
||||
for more info.
|
||||
|
||||
|
||||
OPTIONS
|
||||
=======
|
||||
|
||||
To get a list of available commands and options run::
|
||||
|
||||
heat help
|
||||
|
||||
To get usage and options of a command run::
|
||||
|
||||
heat help <command>
|
||||
|
||||
|
||||
EXAMPLES
|
||||
========
|
||||
|
||||
Get information about stack-create command::
|
||||
|
||||
heat help stack-create
|
||||
|
||||
List available stacks::
|
||||
|
||||
heat stack-list
|
||||
|
||||
List available resources in a stack::
|
||||
|
||||
heat resource-list <stack name>
|
||||
|
||||
Create a stack::
|
||||
|
||||
heat stack-create mystack -f some-template.yaml -P "KeyName=mine"
|
||||
|
||||
View stack information::
|
||||
|
||||
heat stack-show mystack
|
||||
|
||||
List stack outputs::
|
||||
|
||||
heat output-list <stack name>
|
||||
|
||||
Show the value of a single output::
|
||||
|
||||
heat output-show <stack name> <output key>
|
||||
|
||||
List events::
|
||||
|
||||
heat event-list mystack
|
||||
|
||||
Delete a stack::
|
||||
|
||||
heat stack-delete mystack
|
||||
|
||||
Abandon a stack::
|
||||
|
||||
heat stack-abandon mystack
|
||||
|
||||
Adopt a stack ::
|
||||
|
||||
heat stack-adopt -a <adopt_file> mystack
|
||||
|
||||
List heat-engines running status ::
|
||||
|
||||
heat service-list
|
||||
|
||||
Note: stack-adopt and stack-abandon commands are not available by default.
|
||||
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/.
|
|
@ -1,16 +0,0 @@
|
|||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import pbr.version
|
||||
|
||||
|
||||
__version__ = pbr.version.VersionInfo('python-heatclient').version_string()
|
|
@ -1,37 +0,0 @@
|
|||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
"""oslo.i18n integration module.
|
||||
|
||||
See http://docs.openstack.org/developer/oslo.i18n/usage.html
|
||||
|
||||
"""
|
||||
|
||||
import oslo_i18n
|
||||
|
||||
# NOTE(dhellmann): This reference to o-s-l-o will be replaced by the
|
||||
# application name when this module is synced into the separate
|
||||
# repository. It is OK to have more than one translation function
|
||||
# using the same domain, since there will still only be one message
|
||||
# catalog.
|
||||
_translators = oslo_i18n.TranslatorFactory(domain='heatclient')
|
||||
|
||||
# The primary translation function using the well-known name "_"
|
||||
_ = _translators.primary
|
||||
|
||||
|
||||
def get_available_languages():
|
||||
return oslo_i18n.get_available_languages('heatclient')
|
||||
|
||||
|
||||
def enable_lazy():
|
||||
return oslo_i18n.enable_lazy()
|
|
@ -1,20 +0,0 @@
|
|||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from oslo_utils import importutils
|
||||
|
||||
|
||||
def Client(version, *args, **kwargs):
|
||||
module = importutils.import_versioned_module('heatclient',
|
||||
version, 'client')
|
||||
client_class = getattr(module, 'Client')
|
||||
return client_class(*args, **kwargs)
|
|
@ -1,515 +0,0 @@
|
|||
# Copyright 2010 Jacob Kaplan-Moss
|
||||
# Copyright 2011 OpenStack Foundation
|
||||
# Copyright 2012 Grid Dynamics
|
||||
# Copyright 2013 OpenStack Foundation
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
"""
|
||||
Base utilities to build API operation managers and objects on top of.
|
||||
"""
|
||||
import abc
|
||||
import copy
|
||||
|
||||
from oslo_utils import reflection
|
||||
from oslo_utils import strutils
|
||||
import six
|
||||
from six.moves.urllib import parse
|
||||
|
||||
from heatclient._i18n import _
|
||||
from heatclient import exc as exceptions
|
||||
|
||||
|
||||
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 "<Extension '%s'>" % self.name
|
||||
|
||||
|
||||
class Resource(object):
|
||||
"""Base class for OpenStack resources (tenant, user, etc.).
|
||||
|
||||
This is pretty much just a bag for attributes.
|
||||
"""
|
||||
HUMAN_ID = False
|
||||
NAME_ATTR = 'name'
|
||||
|
||||
def __init__(self, manager, info, loaded=False):
|
||||
"""Populate and bind to a manager.
|
||||
|
||||
:param manager: BaseManager object
|
||||
:param info: dictionary representing resource attributes
|
||||
:param loaded: prevent lazy-loading if set to True
|
||||
"""
|
||||
self.manager = manager
|
||||
self._info = info
|
||||
self._add_details(info)
|
||||
self._loaded = loaded
|
||||
|
||||
def __repr__(self):
|
||||
reprkeys = sorted(k
|
||||
for k in self.__dict__.keys()
|
||||
if k[0] != '_' and k != 'manager')
|
||||
info = ", ".join("%s=%s" % (k, getattr(self, k)) for k in reprkeys)
|
||||
class_name = reflection.get_class_name(self, fully_qualified=False)
|
||||
return "<%s %s>" % (class_name, info)
|
||||
|
||||
@property
|
||||
def human_id(self):
|
||||
"""Human-readable ID which can be used for bash completion. """
|
||||
if self.HUMAN_ID:
|
||||
name = getattr(self, self.NAME_ATTR, None)
|
||||
if name is not None:
|
||||
return strutils.to_slug(name)
|
||||
return None
|
||||
|
||||
def _add_details(self, info):
|
||||
for (k, v) in info.items():
|
||||
try:
|
||||
setattr(self, k, v)
|
||||
self._info[k] = v
|
||||
except AttributeError:
|
||||
# In this case we already defined the attribute on the class
|
||||
pass
|
||||
|
||||
def __getattr__(self, k):
|
||||
if k not in self.__dict__:
|
||||
# NOTE(bcwaldon): disallow lazy-loading if already loaded once
|
||||
if not self.is_loaded():
|
||||
self.get()
|
||||
return self.__getattr__(k)
|
||||
|
||||
raise AttributeError(k)
|
||||
else:
|
||||
return self.__dict__[k]
|
||||
|
||||
def get(self):
|
||||
"""Support for lazy loading details.
|
||||
|
||||
Some clients, such as novaclient have the option to lazy load the
|
||||
details, details which can be loaded with this function.
|
||||
"""
|
||||
# set_loaded() first ... so if we have to bail, we know we tried.
|
||||
self.set_loaded(True)
|
||||
if not hasattr(self.manager, 'get'):
|
||||
return
|
||||
|
||||
new = self.manager.get(self.id)
|
||||
if new:
|
||||
self._add_details(new._info)
|
||||
self._add_details(
|
||||
{'x_request_id': self.manager.client.last_request_id})
|
||||
|
||||
def __eq__(self, other):
|
||||
if not isinstance(other, Resource):
|
||||
return NotImplemented
|
||||
# two resources of different types are not equal
|
||||
if not isinstance(other, self.__class__):
|
||||
return False
|
||||
return self._info == other._info
|
||||
|
||||
def __ne__(self, other):
|
||||
return not self.__eq__(other)
|
||||
|
||||
def is_same_obj(self, other):
|
||||
"""Identify the two objects are same one with same id."""
|
||||
if isinstance(other, self.__class__):
|
||||
if hasattr(self, 'id') and hasattr(other, 'id'):
|
||||
return self.id == other.id
|
||||
return False
|
||||
|
||||
def is_loaded(self):
|
||||
return self._loaded
|
||||
|
||||
def set_loaded(self, val):
|
||||
self._loaded = val
|
||||
|
||||
def to_dict(self):
|
||||
return copy.deepcopy(self._info)
|
|
@ -1,151 +0,0 @@
|
|||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
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 heatclient._i18n import _
|
||||
from heatclient import exc
|
||||
from heatclient.v1 import software_configs
|
||||
|
||||
|
||||
def build_derived_config_params(action, source, name, input_values,
|
||||
server_id, signal_transport, signal_id=None):
|
||||
|
||||
if isinstance(source, software_configs.SoftwareConfig):
|
||||
source = source.to_dict()
|
||||
input_values = input_values or {}
|
||||
inputs = copy.deepcopy(source.get('inputs')) or []
|
||||
|
||||
for inp in inputs:
|
||||
input_key = inp['name']
|
||||
inp['value'] = input_values.pop(input_key, inp.get('default'))
|
||||
|
||||
# for any input values that do not have a declared input, add
|
||||
# a derived declared input so that they can be used as config
|
||||
# inputs
|
||||
for inpk, inpv in input_values.items():
|
||||
inputs.append({
|
||||
'name': inpk,
|
||||
'type': 'String',
|
||||
'value': inpv
|
||||
})
|
||||
|
||||
inputs.extend([{
|
||||
'name': 'deploy_server_id',
|
||||
'description': _('ID of the server being deployed to'),
|
||||
'type': 'String',
|
||||
'value': server_id
|
||||
}, {
|
||||
'name': 'deploy_action',
|
||||
'description': _('Name of the current action being deployed'),
|
||||
'type': 'String',
|
||||
'value': action
|
||||
}, {
|
||||
'name': 'deploy_signal_transport',
|
||||
'description': _('How the server should signal to heat with '
|
||||
'the deployment output values.'),
|
||||
'type': 'String',
|
||||
'value': signal_transport
|
||||
}])
|
||||
|
||||
if signal_transport == 'TEMP_URL_SIGNAL':
|
||||
inputs.append({
|
||||
'name': 'deploy_signal_id',
|
||||
'description': _('ID of signal to use for signaling '
|
||||
'output values'),
|
||||
'type': 'String',
|
||||
'value': signal_id
|
||||
})
|
||||
inputs.append({
|
||||
'name': 'deploy_signal_verb',
|
||||
'description': _('HTTP verb to use for signaling '
|
||||
'output values'),
|
||||
'type': 'String',
|
||||
'value': 'PUT'
|
||||
})
|
||||
elif signal_transport != 'NO_SIGNAL':
|
||||
raise exc.CommandError(
|
||||
_('Unsupported signal transport %s') % signal_transport)
|
||||
|
||||
return {
|
||||
'group': source.get('group') or 'Heat::Ungrouped',
|
||||
'config': source.get('config') or '',
|
||||
'options': source.get('options') or {},
|
||||
'inputs': inputs,
|
||||
'outputs': source.get('outputs') or [],
|
||||
'name': name
|
||||
}
|
||||
|
||||
|
||||
def create_temp_url(swift_client, name, timeout, container=None):
|
||||
|
||||
container = container or '%(name)s-%(uuid)s' % {
|
||||
'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 = swift_client.head_account()[key_header]
|
||||
project_path = swift_client.url.split('/')[-1]
|
||||
path = '/v1/%s/%s/%s' % (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)
|
||||
swift_client.put_object(container, object_name, '')
|
||||
return put_url
|
||||
|
||||
|
||||
def build_signal_id(hc, args):
|
||||
if args.signal_transport != 'TEMP_URL_SIGNAL':
|
||||
return
|
||||
|
||||
if args.os_no_client_auth:
|
||||
raise exc.CommandError(_(
|
||||
'Cannot use --os-no-client-auth, auth required to create '
|
||||
'a Swift TempURL.'))
|
||||
swift_client = create_swift_client(
|
||||
hc.http_client.auth, hc.http_client.session, args)
|
||||
|
||||
return create_temp_url(swift_client, args.name, args.timeout)
|
||||
|
||||
|
||||
def create_swift_client(auth, session, args):
|
||||
auth_token = auth.get_token(session)
|
||||
endpoint = auth.get_endpoint(session,
|
||||
service_type='object-store',
|
||||
region_name=args.os_region_name)
|
||||
project_name = args.os_project_name or args.os_tenant_name
|
||||
swift_args = {
|
||||
'auth_version': '2.0',
|
||||
'tenant_name': project_name,
|
||||
'user': args.os_username,
|
||||
'key': None,
|
||||
'authurl': None,
|
||||
'preauthtoken': auth_token,
|
||||
'preauthurl': endpoint,
|
||||
'cacert': args.os_cacert,
|
||||
'insecure': args.insecure
|
||||
}
|
||||
|
||||
return sc.Connection(**swift_args)
|
|
@ -1,67 +0,0 @@
|
|||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import yaml
|
||||
|
||||
from heatclient._i18n import _
|
||||
from heatclient.common import template_format
|
||||
|
||||
|
||||
SECTIONS = (
|
||||
PARAMETER_DEFAULTS, PARAMETERS, RESOURCE_REGISTRY,
|
||||
ENCRYPTED_PARAM_NAMES, EVENT_SINKS,
|
||||
PARAMETER_MERGE_STRATEGIES
|
||||
) = (
|
||||
'parameter_defaults', 'parameters', 'resource_registry',
|
||||
'encrypted_param_names', 'event_sinks',
|
||||
'parameter_merge_strategies'
|
||||
)
|
||||
|
||||
|
||||
def parse(env_str):
|
||||
"""Takes a string and returns a dict containing the parsed structure.
|
||||
|
||||
This includes determination of whether the string is using the
|
||||
YAML format.
|
||||
"""
|
||||
try:
|
||||
env = yaml.load(env_str, Loader=template_format.yaml_loader)
|
||||
except yaml.YAMLError:
|
||||
# NOTE(prazumovsky): we need to return more informative error for
|
||||
# user, so use SafeLoader, which return error message with template
|
||||
# snippet where error has been occurred.
|
||||
try:
|
||||
env = yaml.load(env_str, Loader=yaml.SafeLoader)
|
||||
except yaml.YAMLError as yea:
|
||||
raise ValueError(yea)
|
||||
else:
|
||||
if env is None:
|
||||
env = {}
|
||||
elif not isinstance(env, dict):
|
||||
raise ValueError(_('The environment is not a valid '
|
||||
'YAML mapping data type.'))
|
||||
|
||||
for param in env:
|
||||
if param not in SECTIONS:
|
||||
raise ValueError(_('environment has wrong section "%s"') % param)
|
||||
|
||||
return env
|
||||
|
||||
|
||||
def default_for_missing(env):
|
||||
"""Checks a parsed environment for missing sections."""
|
||||
for param in SECTIONS:
|
||||
if param not in env and param != PARAMETER_MERGE_STRATEGIES:
|
||||
if param in (ENCRYPTED_PARAM_NAMES, EVENT_SINKS):
|
||||
env[param] = []
|
||||
else:
|
||||
env[param] = {}
|
|
@ -1,264 +0,0 @@
|
|||
# Copyright 2015 Red Hat Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import sys
|
||||
import time
|
||||
|
||||
from heatclient._i18n import _
|
||||
from heatclient.common import utils
|
||||
import heatclient.exc as exc
|
||||
from heatclient.v1 import events as events_mod
|
||||
|
||||
|
||||
def get_hook_events(hc, stack_id, event_args, nested_depth=0,
|
||||
hook_type='pre-create'):
|
||||
if hook_type == 'pre-create':
|
||||
stack_action_reason = 'Stack CREATE started'
|
||||
hook_event_reason = 'CREATE paused until Hook pre-create is cleared'
|
||||
hook_clear_event_reason = 'Hook pre-create is cleared'
|
||||
elif hook_type == 'pre-update':
|
||||
stack_action_reason = 'Stack UPDATE started'
|
||||
hook_event_reason = 'UPDATE paused until Hook pre-update is cleared'
|
||||
hook_clear_event_reason = 'Hook pre-update is cleared'
|
||||
elif hook_type == 'pre-delete':
|
||||
stack_action_reason = 'Stack DELETE started'
|
||||
hook_event_reason = 'DELETE paused until Hook pre-delete is cleared'
|
||||
hook_clear_event_reason = 'Hook pre-delete is cleared'
|
||||
else:
|
||||
raise exc.CommandError(_('Unexpected hook type %s') % hook_type)
|
||||
|
||||
events = get_events(hc, stack_id=stack_id, event_args=event_args,
|
||||
nested_depth=nested_depth)
|
||||
|
||||
# Get the most recent event associated with this action, which gives us the
|
||||
# event when we moved into IN_PROGRESS for the hooks we're interested in.
|
||||
stack_name = stack_id.split("/")[0]
|
||||
action_start_event = [e for e in enumerate(events)
|
||||
if e[1].resource_status_reason == stack_action_reason
|
||||
and e[1].stack_name == stack_name][-1]
|
||||
# Slice the events with the index from the enumerate
|
||||
action_start_index = action_start_event[0]
|
||||
events = events[action_start_index:]
|
||||
|
||||
# Get hook events still pending by some list filtering/comparison
|
||||
# We build a map hook events per-resource, and remove any event
|
||||
# for which there is a corresponding hook-clear event.
|
||||
resource_event_map = {}
|
||||
for e in events:
|
||||
stack_resource = (e.stack_name, e.resource_name)
|
||||
if e.resource_status_reason == hook_event_reason:
|
||||
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)])
|
||||
return list(resource_event_map.values())
|
||||
|
||||
|
||||
def get_events(hc, stack_id, event_args, nested_depth=0,
|
||||
marker=None, limit=None):
|
||||
event_args = dict(event_args)
|
||||
if marker:
|
||||
event_args['marker'] = marker
|
||||
if limit:
|
||||
event_args['limit'] = limit
|
||||
if not nested_depth:
|
||||
# simple call with no nested_depth
|
||||
return _get_stack_events(hc, stack_id, event_args)
|
||||
|
||||
# assume an API which supports nested_depth
|
||||
event_args['nested_depth'] = nested_depth
|
||||
events = _get_stack_events(hc, stack_id, event_args)
|
||||
|
||||
if not events:
|
||||
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:
|
||||
# response has a root_stack link, indicating this is an API which
|
||||
# supports nested_depth
|
||||
return events
|
||||
|
||||
# API doesn't support nested_depth, do client-side paging and recursive
|
||||
# event fetch
|
||||
marker = event_args.pop('marker', None)
|
||||
limit = event_args.pop('limit', None)
|
||||
event_args.pop('nested_depth', None)
|
||||
events = _get_stack_events(hc, stack_id, event_args)
|
||||
events.extend(_get_nested_events(hc, nested_depth,
|
||||
stack_id, event_args))
|
||||
# Because there have been multiple stacks events mangled into
|
||||
# one list, we need to sort before passing to print_list
|
||||
# Note we can't use the prettytable sortby_index here, because
|
||||
# the "start" option doesn't allow post-sort slicing, which
|
||||
# will be needed to make "--marker" work for nested_depth lists
|
||||
events.sort(key=lambda x: x.event_time)
|
||||
|
||||
# Slice the list if marker is specified
|
||||
if marker:
|
||||
try:
|
||||
marker_index = [e.id for e in events].index(marker)
|
||||
events = events[marker_index:]
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
# Slice the list if limit is specified
|
||||
if limit:
|
||||
limit_index = min(int(limit), len(events))
|
||||
events = events[:limit_index]
|
||||
return events
|
||||
|
||||
|
||||
def _get_nested_ids(hc, stack_id):
|
||||
nested_ids = []
|
||||
try:
|
||||
resources = hc.resources.list(stack_id=stack_id)
|
||||
except exc.HTTPNotFound:
|
||||
raise exc.CommandError(_('Stack not found: %s') % stack_id)
|
||||
for r in resources:
|
||||
nested_id = utils.resource_nested_identifier(r)
|
||||
if nested_id:
|
||||
nested_ids.append(nested_id)
|
||||
return nested_ids
|
||||
|
||||
|
||||
def _get_nested_events(hc, nested_depth, stack_id, event_args):
|
||||
# FIXME(shardy): this is very inefficient, we should add nested_depth to
|
||||
# the event_list API in a future heat version, but this will be required
|
||||
# until kilo heat is EOL.
|
||||
nested_ids = _get_nested_ids(hc, stack_id)
|
||||
nested_events = []
|
||||
for n_id in nested_ids:
|
||||
stack_events = _get_stack_events(hc, n_id, event_args)
|
||||
if stack_events:
|
||||
nested_events.extend(stack_events)
|
||||
if nested_depth > 1:
|
||||
next_depth = nested_depth - 1
|
||||
nested_events.extend(_get_nested_events(
|
||||
hc, next_depth, n_id, event_args))
|
||||
return nested_events
|
||||
|
||||
|
||||
def _get_stack_name_from_links(event):
|
||||
links = dict((l.get('rel'),
|
||||
l.get('href')) for l 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:
|
||||
events = hc.events.list(**event_args)
|
||||
except exc.HTTPNotFound as ex:
|
||||
# it could be the stack or resource that is not found
|
||||
# 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 = _get_stack_name_from_links(e) or stack_name
|
||||
return events
|
||||
|
||||
|
||||
def poll_for_events(hc, stack_name, action=None, poll_period=5, marker=None,
|
||||
out=None, nested_depth=0):
|
||||
"""Continuously poll events and logs for performed action on stack."""
|
||||
|
||||
if action:
|
||||
stop_status = ('%s_FAILED' % action, '%s_COMPLETE' % action)
|
||||
stop_check = lambda a: a in stop_status
|
||||
else:
|
||||
stop_check = lambda a: a.endswith('_COMPLETE') or a.endswith('_FAILED')
|
||||
|
||||
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 = dict((l.get('rel'),
|
||||
l.get('href')) for l 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',
|
||||
'marker': marker})
|
||||
|
||||
if len(events) == 0:
|
||||
no_event_polls += 1
|
||||
else:
|
||||
no_event_polls = 0
|
||||
# set marker to last event that was received.
|
||||
marker = getattr(events[-1], 'id', None)
|
||||
events_log = utils.event_log_formatter(events, event_log_context)
|
||||
out.write(events_log)
|
||||
out.write('\n')
|
||||
|
||||
for event in events:
|
||||
# check if stack event was also received
|
||||
if is_stack_event(event):
|
||||
stack_status = getattr(event, 'resource_status', '')
|
||||
msg = msg_template % dict(
|
||||
name=stack_name, status=stack_status)
|
||||
if stop_check(stack_status):
|
||||
return stack_status, msg
|
||||
|
||||
if no_event_polls >= 2:
|
||||
# after 2 polls with no events, fall back to a stack get
|
||||
stack = hc.stacks.get(stack_name, resolve_outputs=False)
|
||||
stack_status = stack.stack_status
|
||||
msg = msg_template % dict(
|
||||
name=stack_name, status=stack_status)
|
||||
if stop_check(stack_status):
|
||||
return stack_status, msg
|
||||
# go back to event polling again
|
||||
no_event_polls = 0
|
||||
|
||||
time.sleep(poll_period)
|
||||
|
||||
|
||||
def wait_for_events(ws, stack_name, out=None):
|
||||
"""Receive events over the passed websocket and wait for final status."""
|
||||
msg_template = _("\n Stack %(name)s %(status)s \n")
|
||||
if not out:
|
||||
out = sys.stdout
|
||||
event_log_context = utils.EventLogContext()
|
||||
while True:
|
||||
data = ws.recv()['body']
|
||||
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)
|
||||
events_log = utils.event_log_formatter([event], event_log_context)
|
||||
out.write(events_log)
|
||||
out.write('\n')
|
||||
if data['payload']['resource_name'] == stack_name:
|
||||
stack_status = data['payload']['resource_status']
|
||||
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
|
|
@ -1,96 +0,0 @@
|
|||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
#
|
||||
# Copyright 2015 IBM Corp.
|
||||
|
||||
import sys
|
||||
|
||||
from osc_lib.command import command
|
||||
import six
|
||||
|
||||
|
||||
class RawFormat(command.ShowOne):
|
||||
|
||||
def produce_output(self, parsed_args, column_names, data):
|
||||
if data is None:
|
||||
return
|
||||
|
||||
self.formatter.emit_one(column_names, data,
|
||||
self.app.stdout, parsed_args)
|
||||
|
||||
|
||||
class JsonFormat(RawFormat):
|
||||
|
||||
@property
|
||||
def formatter_default(self):
|
||||
return 'json'
|
||||
|
||||
|
||||
class YamlFormat(RawFormat):
|
||||
|
||||
@property
|
||||
def formatter_default(self):
|
||||
return 'yaml'
|
||||
|
||||
|
||||
class ShellFormat(RawFormat):
|
||||
|
||||
@property
|
||||
def formatter_default(self):
|
||||
return 'shell'
|
||||
|
||||
|
||||
class ValueFormat(RawFormat):
|
||||
|
||||
@property
|
||||
def formatter_default(self):
|
||||
return 'value'
|
||||
|
||||
|
||||
def indent_and_truncate(txt, spaces=0, truncate=False, truncate_limit=10,
|
||||
truncate_prefix=None, truncate_postfix=None):
|
||||
"""Indents supplied multiline text by the specified number of spaces
|
||||
|
||||
"""
|
||||
if txt is None:
|
||||
return
|
||||
lines = six.text_type(txt).splitlines()
|
||||
if truncate and len(lines) > truncate_limit:
|
||||
lines = lines[-truncate_limit:]
|
||||
if truncate_prefix is not None:
|
||||
lines.insert(0, truncate_prefix)
|
||||
if truncate_postfix is not None:
|
||||
lines.append(truncate_postfix)
|
||||
|
||||
if spaces > 0:
|
||||
lines = [" " * spaces + line for line in lines]
|
||||
return '\n'.join(lines)
|
||||
|
||||
|
||||
def print_software_deployment_output(data, name, out=sys.stdout, long=False):
|
||||
"""Prints details of the software deployment for user consumption
|
||||
|
||||
The format attempts to be valid yaml, but is primarily aimed at showing
|
||||
useful information to the user in a helpful layout.
|
||||
"""
|
||||
if data is None:
|
||||
data = {}
|
||||
if name in ('deploy_stdout', 'deploy_stderr'):
|
||||
output = indent_and_truncate(
|
||||
data.get(name),
|
||||
spaces=4,
|
||||
truncate=not long,
|
||||
truncate_prefix='...',
|
||||
truncate_postfix='(truncated, view all with --long)')
|
||||
out.write(' %s: |\n%s\n' % (name, output))
|
||||
else:
|
||||
out.write(' %s: %s\n' % (name, data.get(name)))
|
|
@ -1,80 +0,0 @@
|
|||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import logging
|
||||
|
||||
from oslo_utils import fnmatch
|
||||
|
||||
from heatclient._i18n import _
|
||||
from heatclient import exc
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def clear_hook(hc, stack_id, resource_name, hook_type):
|
||||
try:
|
||||
hc.resources.signal(
|
||||
stack_id=stack_id,
|
||||
resource_name=resource_name,
|
||||
data={'unset_hook': hook_type})
|
||||
except exc.HTTPNotFound:
|
||||
logger.error(
|
||||
"Stack %(stack)s or resource %(resource)s "
|
||||
"not found for hook %(hook_type)",
|
||||
{'resource': resource_name, 'stack': stack_id,
|
||||
'hook_type': hook_type})
|
||||
|
||||
|
||||
def clear_wildcard_hooks(hc, stack_id, stack_patterns, hook_type,
|
||||
resource_pattern):
|
||||
if stack_patterns:
|
||||
for resource in hc.resources.list(stack_id):
|
||||
res_name = resource.resource_name
|
||||
if fnmatch.fnmatchcase(res_name, stack_patterns[0]):
|
||||
nested_stack = hc.resources.get(
|
||||
stack_id=stack_id,
|
||||
resource_name=res_name)
|
||||
clear_wildcard_hooks(
|
||||
hc,
|
||||
nested_stack.physical_resource_id,
|
||||
stack_patterns[1:], hook_type, resource_pattern)
|
||||
else:
|
||||
for resource in hc.resources.list(stack_id):
|
||||
res_name = resource.resource_name
|
||||
if fnmatch.fnmatchcase(res_name, resource_pattern):
|
||||
clear_hook(hc, stack_id, res_name, hook_type)
|
||||
|
||||
|
||||
def get_hook_type_via_status(hc, stack_id):
|
||||
# Figure out if the hook should be pre-create, pre-update or
|
||||
# pre-delete based on the stack status, also sanity assertions
|
||||
# that we're in-progress.
|
||||
try:
|
||||
stack = hc.stacks.get(stack_id=stack_id)
|
||||
except exc.HTTPNotFound:
|
||||
raise exc.CommandError(_('Stack not found: %s') % stack_id)
|
||||
else:
|
||||
if 'IN_PROGRESS' not in stack.stack_status:
|
||||
raise exc.CommandError(_('Stack status %s not IN_PROGRESS') %
|
||||
stack.stack_status)
|
||||
|
||||
if 'CREATE' in stack.stack_status:
|
||||
hook_type = 'pre-create'
|
||||
elif 'UPDATE' in stack.stack_status:
|
||||
hook_type = 'pre-update'
|
||||
elif 'DELETE' in stack.stack_status:
|
||||
hook_type = 'pre-delete'
|
||||
else:
|
||||
raise exc.CommandError(_('Unexpected stack status %s, '
|
||||
'only create, update and delete supported')
|
||||
% stack.stack_status)
|
||||
return hook_type
|
|
@ -1,363 +0,0 @@
|
|||
# Copyright 2012 OpenStack Foundation
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import copy
|
||||
import hashlib
|
||||
import logging
|
||||
import os
|
||||
import socket
|
||||
|
||||
from keystoneauth1 import adapter
|
||||
from oslo_serialization import jsonutils
|
||||
from oslo_utils import encodeutils
|
||||
from oslo_utils import importutils
|
||||
import requests
|
||||
import six
|
||||
from six.moves.urllib import parse
|
||||
|
||||
from heatclient._i18n import _
|
||||
from heatclient.common import utils
|
||||
from heatclient import exc
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
USER_AGENT = 'python-heatclient'
|
||||
CHUNKSIZE = 1024 * 64 # 64kB
|
||||
SENSITIVE_HEADERS = ('X-Auth-Token',)
|
||||
osprofiler_web = importutils.try_import("osprofiler.web")
|
||||
|
||||
|
||||
def authenticated_fetcher(hc):
|
||||
"""A wrapper around the heat client object to fetch a template."""
|
||||
|
||||
def _do(*args, **kwargs):
|
||||
if isinstance(hc.http_client, SessionClient):
|
||||
method, url = args
|
||||
return hc.http_client.request(url, method, **kwargs).content
|
||||
else:
|
||||
return hc.http_client.raw_request(*args, **kwargs).content
|
||||
|
||||
return _do
|
||||
|
||||
|
||||
def get_system_ca_file():
|
||||
"""Return path to system default CA file."""
|
||||
# Standard CA file locations for Debian/Ubuntu, RedHat/Fedora,
|
||||
# Suse, FreeBSD/OpenBSD, MacOSX, and the bundled ca
|
||||
ca_path = ['/etc/ssl/certs/ca-certificates.crt',
|
||||
'/etc/pki/tls/certs/ca-bundle.crt',
|
||||
'/etc/ssl/ca-bundle.pem',
|
||||
'/etc/ssl/cert.pem',
|
||||
'/System/Library/OpenSSL/certs/cacert.pem',
|
||||
requests.certs.where()]
|
||||
for ca in ca_path:
|
||||
LOG.debug("Looking for ca file %s", ca)
|
||||
if os.path.exists(ca):
|
||||
LOG.debug("Using ca file %s", ca)
|
||||
return ca
|
||||
LOG.warning("System ca file could not be found.")
|
||||
|
||||
|
||||
class HTTPClient(object):
|
||||
|
||||
def __init__(self, endpoint, **kwargs):
|
||||
self.endpoint = endpoint
|
||||
self.auth_url = kwargs.get('auth_url')
|
||||
self.auth_token = kwargs.get('token')
|
||||
self.username = kwargs.get('username')
|
||||
self.password = kwargs.get('password')
|
||||
self.region_name = kwargs.get('region_name')
|
||||
self.include_pass = kwargs.get('include_pass')
|
||||
self.endpoint_url = endpoint
|
||||
|
||||
self.cert_file = kwargs.get('cert_file')
|
||||
self.key_file = kwargs.get('key_file')
|
||||
self.timeout = kwargs.get('timeout')
|
||||
|
||||
self.ssl_connection_params = {
|
||||
'ca_file': kwargs.get('ca_file'),
|
||||
'cert_file': kwargs.get('cert_file'),
|
||||
'key_file': kwargs.get('key_file'),
|
||||
'insecure': kwargs.get('insecure'),
|
||||
}
|
||||
|
||||
self.verify_cert = None
|
||||
if parse.urlparse(endpoint).scheme == "https":
|
||||
if kwargs.get('insecure'):
|
||||
self.verify_cert = False
|
||||
else:
|
||||
self.verify_cert = kwargs.get('ca_file', get_system_ca_file())
|
||||
|
||||
# FIXME(shardy): We need this for compatibility with the oslo apiclient
|
||||
# we should move to inheriting this class from the oslo HTTPClient
|
||||
self.last_request_id = None
|
||||
|
||||
def safe_header(self, name, value):
|
||||
if name in SENSITIVE_HEADERS:
|
||||
# because in python3 byte string handling is ... ug
|
||||
v = value.encode('utf-8')
|
||||
h = hashlib.sha1(v)
|
||||
d = h.hexdigest()
|
||||
return encodeutils.safe_decode(name), "{SHA1}%s" % d
|
||||
else:
|
||||
return (encodeutils.safe_decode(name),
|
||||
encodeutils.safe_decode(value))
|
||||
|
||||
def log_curl_request(self, method, url, kwargs):
|
||||
curl = ['curl -g -i -X %s' % method]
|
||||
|
||||
for (key, value) in kwargs['headers'].items():
|
||||
header = '-H \'%s: %s\'' % self.safe_header(key, value)
|
||||
curl.append(header)
|
||||
|
||||
conn_params_fmt = [
|
||||
('key_file', '--key %s'),
|
||||
('cert_file', '--cert %s'),
|
||||
('ca_file', '--cacert %s'),
|
||||
]
|
||||
for (key, fmt) in conn_params_fmt:
|
||||
value = self.ssl_connection_params.get(key)
|
||||
if value:
|
||||
curl.append(fmt % value)
|
||||
|
||||
if self.ssl_connection_params.get('insecure'):
|
||||
curl.append('-k')
|
||||
|
||||
if 'data' in kwargs:
|
||||
curl.append('-d \'%s\'' % kwargs['data'])
|
||||
|
||||
curl.append('%s%s' % (self.endpoint, url))
|
||||
LOG.debug(' '.join(curl))
|
||||
|
||||
@staticmethod
|
||||
def log_http_response(resp):
|
||||
status = (resp.raw.version / 10.0, resp.status_code, resp.reason)
|
||||
dump = ['\nHTTP/%.1f %s %s' % status]
|
||||
dump.extend(['%s: %s' % (k, v) for k, v in resp.headers.items()])
|
||||
dump.append('')
|
||||
if resp.content:
|
||||
content = resp.content
|
||||
if isinstance(content, six.binary_type):
|
||||
content = content.decode()
|
||||
dump.extend([content, ''])
|
||||
LOG.debug('\n'.join(dump))
|
||||
|
||||
def _http_request(self, url, method, **kwargs):
|
||||
"""Send an http request with the specified characteristics.
|
||||
|
||||
Wrapper around requests.request to handle tasks such as
|
||||
setting headers and error handling.
|
||||
"""
|
||||
# Copy the kwargs so we can reuse the original in case of redirects
|
||||
kwargs['headers'] = copy.deepcopy(kwargs.get('headers', {}))
|
||||
kwargs['headers'].setdefault('User-Agent', USER_AGENT)
|
||||
if self.auth_token:
|
||||
kwargs['headers'].setdefault('X-Auth-Token', self.auth_token)
|
||||
else:
|
||||
kwargs['headers'].update(self.credentials_headers())
|
||||
if self.auth_url:
|
||||
kwargs['headers'].setdefault('X-Auth-Url', self.auth_url)
|
||||
if self.region_name:
|
||||
kwargs['headers'].setdefault('X-Region-Name', self.region_name)
|
||||
if self.include_pass and 'X-Auth-Key' not in kwargs['headers']:
|
||||
kwargs['headers'].update(self.credentials_headers())
|
||||
if osprofiler_web:
|
||||
kwargs['headers'].update(osprofiler_web.get_trace_id_headers())
|
||||
|
||||
self.log_curl_request(method, url, kwargs)
|
||||
|
||||
if self.cert_file and self.key_file:
|
||||
kwargs['cert'] = (self.cert_file, self.key_file)
|
||||
|
||||
if self.verify_cert is not None:
|
||||
kwargs['verify'] = self.verify_cert
|
||||
|
||||
if self.timeout is not None:
|
||||
kwargs['timeout'] = float(self.timeout)
|
||||
|
||||
# Allow caller to specify not to follow redirects, in which case we
|
||||
# just return the redirect response. Useful for using stacks:lookup.
|
||||
redirect = kwargs.pop('redirect', True)
|
||||
|
||||
# Since requests does not follow the RFC when doing redirection to sent
|
||||
# back the same method on a redirect we are simply bypassing it. For
|
||||
# example if we do a DELETE/POST/PUT on a URL and we get a 302 RFC says
|
||||
# that we should follow that URL with the same method as before,
|
||||
# requests doesn't follow that and send a GET instead for the method.
|
||||
# Hopefully this could be fixed as they say in a comment in a future
|
||||
# point version i.e.: 3.x
|
||||
# See issue: https://github.com/kennethreitz/requests/issues/1704
|
||||
allow_redirects = False
|
||||
|
||||
# Use fully qualified URL from response header for redirects
|
||||
if not parse.urlparse(url).netloc:
|
||||
url = self.endpoint_url + url
|
||||
|
||||
try:
|
||||
resp = requests.request(
|
||||
method,
|
||||
url,
|
||||
allow_redirects=allow_redirects,
|
||||
**kwargs)
|
||||
except socket.gaierror as e:
|
||||
message = (_("Error finding address for %(url)s: %(e)s") %
|
||||
{'url': self.endpoint_url + url, 'e': e})
|
||||
raise exc.InvalidEndpoint(message=message)
|
||||
except (socket.error, socket.timeout) 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)
|
||||
|
||||
if not ('X-Auth-Key' in kwargs['headers']) and (
|
||||
resp.status_code == 401 or
|
||||
(resp.status_code == 500 and "(HTTP 401)" in resp.content)):
|
||||
raise exc.HTTPUnauthorized(_("Authentication failed: %s")
|
||||
% resp.content)
|
||||
elif 400 <= resp.status_code < 600:
|
||||
raise exc.from_response(resp)
|
||||
elif resp.status_code in (301, 302, 305):
|
||||
# Redirected. Reissue the request to the new location,
|
||||
# unless caller specified redirect=False
|
||||
if redirect:
|
||||
location = resp.headers.get('location')
|
||||
if not location:
|
||||
message = _("Location not returned with redirect")
|
||||
raise exc.InvalidEndpoint(message=message)
|
||||
resp = self._http_request(location, method, **kwargs)
|
||||
elif resp.status_code == 300:
|
||||
raise exc.from_response(resp)
|
||||
|
||||
return resp
|
||||
|
||||
def credentials_headers(self):
|
||||
creds = {}
|
||||
# NOTE(dhu): (shardy) When deferred_auth_method=password, Heat
|
||||
# encrypts and stores username/password. For Keystone v3, the
|
||||
# intent is to use trusts since SHARDY is working towards
|
||||
# deferred_auth_method=trusts as the default.
|
||||
# TODO(dhu): Make Keystone v3 work in Heat standalone mode. Maye
|
||||
# require X-Auth-User-Domain.
|
||||
if self.username:
|
||||
creds['X-Auth-User'] = self.username
|
||||
if self.password:
|
||||
creds['X-Auth-Key'] = self.password
|
||||
return creds
|
||||
|
||||
def json_request(self, method, url, **kwargs):
|
||||
kwargs.setdefault('headers', {})
|
||||
kwargs['headers'].setdefault('Content-Type', 'application/json')
|
||||
kwargs['headers'].setdefault('Accept', 'application/json')
|
||||
|
||||
if 'data' in kwargs:
|
||||
kwargs['data'] = jsonutils.dumps(kwargs['data'])
|
||||
|
||||
resp = self._http_request(url, method, **kwargs)
|
||||
body = utils.get_response_body(resp)
|
||||
return resp, body
|
||||
|
||||
def raw_request(self, method, url, **kwargs):
|
||||
kwargs.setdefault('headers', {})
|
||||
kwargs['headers'].setdefault('Content-Type',
|
||||
'application/octet-stream')
|
||||
return self._http_request(url, method, **kwargs)
|
||||
|
||||
def client_request(self, method, url, **kwargs):
|
||||
resp, body = self.json_request(method, url, **kwargs)
|
||||
return resp
|
||||
|
||||
def head(self, url, **kwargs):
|
||||
return self.client_request("HEAD", url, **kwargs)
|
||||
|
||||
def get(self, url, **kwargs):
|
||||
return self.client_request("GET", url, **kwargs)
|
||||
|
||||
def post(self, url, **kwargs):
|
||||
return self.client_request("POST", url, **kwargs)
|
||||
|
||||
def put(self, url, **kwargs):
|
||||
return self.client_request("PUT", url, **kwargs)
|
||||
|
||||
def delete(self, url, **kwargs):
|
||||
return self.raw_request("DELETE", url, **kwargs)
|
||||
|
||||
def patch(self, url, **kwargs):
|
||||
return self.client_request("PATCH", url, **kwargs)
|
||||
|
||||
|
||||
class SessionClient(adapter.LegacyJsonAdapter):
|
||||
"""HTTP client based on Keystone client session."""
|
||||
|
||||
def request(self, url, method, **kwargs):
|
||||
redirect = kwargs.get('redirect')
|
||||
kwargs.setdefault('user_agent', USER_AGENT)
|
||||
|
||||
if 'data' in kwargs:
|
||||
kwargs['data'] = jsonutils.dumps(kwargs['data'])
|
||||
|
||||
resp, body = super(SessionClient, self).request(
|
||||
url, method,
|
||||
raise_exc=False,
|
||||
**kwargs)
|
||||
|
||||
if 400 <= resp.status_code < 600:
|
||||
raise exc.from_response(resp)
|
||||
elif resp.status_code in (301, 302, 305):
|
||||
if redirect:
|
||||
location = resp.headers.get('location')
|
||||
path = self.strip_endpoint(location)
|
||||
resp = self.request(path, method, **kwargs)
|
||||
elif resp.status_code == 300:
|
||||
raise exc.from_response(resp)
|
||||
|
||||
return resp
|
||||
|
||||
def credentials_headers(self):
|
||||
return {}
|
||||
|
||||
def strip_endpoint(self, location):
|
||||
if location is None:
|
||||
message = _("Location not returned with 302")
|
||||
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):]
|
||||
else:
|
||||
return location
|
||||
|
||||
|
||||
def _construct_http_client(endpoint=None, username=None, password=None,
|
||||
include_pass=None, endpoint_type=None,
|
||||
auth_url=None, **kwargs):
|
||||
session = kwargs.pop('session', None)
|
||||
auth = kwargs.pop('auth', None)
|
||||
|
||||
if session:
|
||||
if 'endpoint_override' not in kwargs and endpoint:
|
||||
kwargs['endpoint_override'] = endpoint
|
||||
|
||||
if 'service_type' not in kwargs:
|
||||
kwargs['service_type'] = 'orchestration'
|
||||
|
||||
if 'interface' not in kwargs and endpoint_type:
|
||||
kwargs['interface'] = endpoint_type
|
||||
|
||||
return SessionClient(session, auth=auth, **kwargs)
|
||||
else:
|
||||
return HTTPClient(endpoint=endpoint, username=username,
|
||||
password=password, include_pass=include_pass,
|
||||
endpoint_type=endpoint_type, auth_url=auth_url,
|
||||
**kwargs)
|
|
@ -1,150 +0,0 @@
|
|||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
#
|
||||
|
||||
import collections
|
||||
import hashlib
|
||||
|
||||
from cliff.formatters import base
|
||||
|
||||
|
||||
class ResourceDotInfo(object):
|
||||
|
||||
def __init__(self, res):
|
||||
self.resource = res
|
||||
links = {l['rel']: l['href'] for l 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'))
|
||||
|
||||
@staticmethod
|
||||
def dot_id(url, prefix=None):
|
||||
"""Build an id with a prefix and a truncated hash of the URL"""
|
||||
if not url:
|
||||
return None
|
||||
if not prefix:
|
||||
prefix = 'r'
|
||||
hash_object = hashlib.sha256(url.encode('utf-8'))
|
||||
return '%s_%s' % (prefix, hash_object.hexdigest()[:20])
|
||||
|
||||
|
||||
class ResourceDotFormatter(base.ListFormatter):
|
||||
def add_argument_group(self, parser):
|
||||
pass
|
||||
|
||||
def emit_list(self, column_names, data, stdout, parsed_args):
|
||||
writer = ResourceDotWriter(data, stdout)
|
||||
writer.write()
|
||||
|
||||
|
||||
class ResourceDotWriter(object):
|
||||
|
||||
def __init__(self, data, stdout):
|
||||
self.resources_by_stack = collections.defaultdict(
|
||||
collections.OrderedDict)
|
||||
self.resources_by_dot_id = collections.OrderedDict()
|
||||
self.nested_stack_ids = []
|
||||
self.stdout = stdout
|
||||
|
||||
for r in data:
|
||||
rinfo = ResourceDotInfo(r)
|
||||
if rinfo.stack_dot_id:
|
||||
self.resources_by_stack[
|
||||
rinfo.stack_dot_id][r.resource_name] = rinfo
|
||||
if rinfo.res_dot_id:
|
||||
self.resources_by_dot_id[rinfo.res_dot_id] = rinfo
|
||||
if rinfo.nested_dot_id:
|
||||
self.nested_stack_ids.append(rinfo.nested_dot_id)
|
||||
|
||||
def write(self):
|
||||
stdout = self.stdout
|
||||
|
||||
stdout.write('digraph G {\n')
|
||||
stdout.write(' graph [\n'
|
||||
' fontsize=10 fontname="Verdana" '
|
||||
'compound=true rankdir=LR\n'
|
||||
' ]\n')
|
||||
|
||||
self.write_root_nodes()
|
||||
self.write_subgraphs()
|
||||
self.write_nested_stack_edges()
|
||||
self.write_required_by_edges()
|
||||
stdout.write('}\n')
|
||||
|
||||
def write_root_nodes(self):
|
||||
for stack_dot_id in set(self.resources_by_stack.keys()).difference(
|
||||
self.nested_stack_ids):
|
||||
resources = self.resources_by_stack[stack_dot_id]
|
||||
self.write_nodes(resources, 2)
|
||||
|
||||
def write_subgraphs(self):
|
||||
for dot_id, rinfo in self.resources_by_dot_id.items():
|
||||
if rinfo.nested_dot_id:
|
||||
resources = self.resources_by_stack[rinfo.nested_dot_id]
|
||||
if resources:
|
||||
self.write_subgraph(resources, rinfo)
|
||||
|
||||
def write_nodes(self, resources, indent):
|
||||
stdout = self.stdout
|
||||
spaces = ' ' * indent
|
||||
for rinfo in resources.values():
|
||||
r = rinfo.resource
|
||||
dot_id = rinfo.res_dot_id
|
||||
if r.resource_status.endswith('FAILED'):
|
||||
style = 'style=filled color=red'
|
||||
else:
|
||||
style = ''
|
||||
stdout.write('%s%s [label="%s\n%s" %s];\n'
|
||||
% (spaces, dot_id, r.resource_name,
|
||||
r.resource_type, style))
|
||||
stdout.write('\n')
|
||||
|
||||
def write_subgraph(self, resources, nested_resource):
|
||||
stdout = self.stdout
|
||||
stack_dot_id = nested_resource.nested_dot_id
|
||||
nested_name = nested_resource.resource.resource_name
|
||||
stdout.write(' subgraph cluster_%s {\n' % stack_dot_id)
|
||||
stdout.write(' label="%s";\n' % nested_name)
|
||||
self.write_nodes(resources, 4)
|
||||
stdout.write(' }\n\n')
|
||||
|
||||
def write_required_by_edges(self):
|
||||
stdout = self.stdout
|
||||
for dot_id, rinfo in self.resources_by_dot_id.items():
|
||||
r = rinfo.resource
|
||||
|
||||
required_by = r.required_by
|
||||
stack_dot_id = rinfo.stack_dot_id
|
||||
if not required_by or not stack_dot_id:
|
||||
continue
|
||||
|
||||
stack_resources = self.resources_by_stack.get(stack_dot_id, {})
|
||||
for req in required_by:
|
||||
other_rinfo = stack_resources.get(req)
|
||||
if other_rinfo:
|
||||
stdout.write(' %s -> %s;\n'
|
||||
% (rinfo.res_dot_id, other_rinfo.res_dot_id))
|
||||
stdout.write('\n')
|
||||
|
||||
def write_nested_stack_edges(self):
|
||||
stdout = self.stdout
|
||||
for dot_id, rinfo in self.resources_by_dot_id.items():
|
||||
if rinfo.nested_dot_id:
|
||||
nested_resources = self.resources_by_stack[rinfo.nested_dot_id]
|
||||
if nested_resources:
|
||||
first_resource = list(nested_resources.values())[0]
|
||||
stdout.write(
|
||||
' %s -> %s [\n color=dimgray lhead=cluster_%s '
|
||||
'arrowhead=none\n ];\n'
|
||||
% (dot_id, first_resource.res_dot_id,
|
||||
rinfo.nested_dot_id))
|
||||
stdout.write('\n')
|
|
@ -1,71 +0,0 @@
|
|||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import json
|
||||
import yaml
|
||||
|
||||
from heatclient._i18n import _
|
||||
|
||||
if hasattr(yaml, 'CSafeLoader'):
|
||||
yaml_loader = yaml.CSafeLoader
|
||||
else:
|
||||
yaml_loader = yaml.SafeLoader
|
||||
|
||||
if hasattr(yaml, 'CSafeDumper'):
|
||||
yaml_dumper = yaml.CSafeDumper
|
||||
else:
|
||||
yaml_dumper = yaml.SafeDumper
|
||||
|
||||
|
||||
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)
|
||||
# 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',
|
||||
_construct_yaml_str)
|
||||
|
||||
|
||||
def parse(tmpl_str):
|
||||
"""Takes a string and returns a dict containing the parsed structure.
|
||||
|
||||
This includes determination of whether the string is using the
|
||||
JSON or YAML format.
|
||||
"""
|
||||
# strip any whitespace before the check
|
||||
tmpl_str = tmpl_str.strip()
|
||||
if tmpl_str.startswith('{'):
|
||||
tpl = json.loads(tmpl_str)
|
||||
else:
|
||||
try:
|
||||
tpl = yaml.load(tmpl_str, Loader=yaml_loader)
|
||||
except yaml.YAMLError:
|
||||
# NOTE(prazumovsky): we need to return more informative error for
|
||||
# user, so use SafeLoader, which return error message with template
|
||||
# snippet where error has been occurred.
|
||||
try:
|
||||
tpl = yaml.load(tmpl_str, Loader=yaml.SafeLoader)
|
||||
except yaml.YAMLError as yea:
|
||||
raise ValueError(yea)
|
||||
else:
|
||||
if tpl is None:
|
||||
tpl = {}
|
||||
# Looking for supported version keys in the loaded template
|
||||
if not ('HeatTemplateFormatVersion' in tpl
|
||||
or 'heat_template_version' in tpl
|
||||
or 'AWSTemplateFormatVersion' in tpl):
|
||||
raise ValueError(_("Template format version not found."))
|
||||
return tpl
|
|
@ -1,382 +0,0 @@
|
|||
# Copyright 2012 OpenStack Foundation
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import collections
|
||||
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 heatclient._i18n import _
|
||||
from heatclient.common import environment_format
|
||||
from heatclient.common import template_format
|
||||
from heatclient.common import utils
|
||||
from heatclient import exc
|
||||
|
||||
|
||||
def process_template_path(template_path, object_request=None, existing=False):
|
||||
"""Read template from template path.
|
||||
|
||||
Attempt to read template first as a file or url. If that is unsuccessful,
|
||||
try again assuming path is to a template object.
|
||||
|
||||
:param template_path: local or uri path to template
|
||||
: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
|
||||
:returns: get_file dict and template contents
|
||||
:raises: error.URLError
|
||||
"""
|
||||
try:
|
||||
return get_template_contents(template_file=template_path,
|
||||
existing=existing)
|
||||
except error.URLError as template_file_exc:
|
||||
try:
|
||||
return get_template_contents(template_object=template_path,
|
||||
object_request=object_request,
|
||||
existing=existing)
|
||||
except exc.HTTPNotFound:
|
||||
# The initial exception gives the user better failure context.
|
||||
raise template_file_exc
|
||||
|
||||
|
||||
def get_template_contents(template_file=None, template_url=None,
|
||||
template_object=None, object_request=None,
|
||||
files=None, existing=False):
|
||||
|
||||
is_object = False
|
||||
# Transform a bare file path to a file:// URL.
|
||||
if template_file:
|
||||
template_url = utils.normalise_file_path_to_url(template_file)
|
||||
|
||||
if template_url:
|
||||
tpl = request.urlopen(template_url).read()
|
||||
|
||||
elif template_object:
|
||||
is_object = True
|
||||
template_url = template_object
|
||||
tpl = object_request and object_request('GET',
|
||||
template_object)
|
||||
elif existing:
|
||||
return {}, None
|
||||
else:
|
||||
raise exc.CommandError(_('Need to specify exactly one of '
|
||||
'%(arg1)s, %(arg2)s or %(arg3)s') %
|
||||
{
|
||||
'arg1': '--template-file',
|
||||
'arg2': '--template-url',
|
||||
'arg3': '--template-object'})
|
||||
|
||||
if not tpl:
|
||||
raise exc.CommandError(_('Could not fetch template from %s')
|
||||
% template_url)
|
||||
|
||||
try:
|
||||
if isinstance(tpl, six.binary_type):
|
||||
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)
|
||||
return files, template
|
||||
|
||||
|
||||
def resolve_template_get_files(template, files, template_base_url,
|
||||
is_object=False, object_request=None):
|
||||
|
||||
def ignore_if(key, value):
|
||||
if key != 'get_file' and key != 'type':
|
||||
return True
|
||||
if not isinstance(value, six.string_types):
|
||||
return True
|
||||
if (key == 'type' and
|
||||
not value.endswith(('.yaml', '.template'))):
|
||||
return True
|
||||
return False
|
||||
|
||||
def recurse_if(value):
|
||||
return isinstance(value, (dict, list))
|
||||
|
||||
get_file_contents(template, files, template_base_url,
|
||||
ignore_if, recurse_if, is_object, object_request)
|
||||
|
||||
|
||||
def is_template(file_content):
|
||||
try:
|
||||
if isinstance(file_content, six.binary_type):
|
||||
file_content = file_content.decode('utf-8')
|
||||
template_format.parse(file_content)
|
||||
except (ValueError, TypeError):
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def get_file_contents(from_data, files, base_url=None,
|
||||
ignore_if=None, recurse_if=None,
|
||||
is_object=False, object_request=None):
|
||||
|
||||
if recurse_if and recurse_if(from_data):
|
||||
if isinstance(from_data, dict):
|
||||
recurse_data = six.itervalues(from_data)
|
||||
else:
|
||||
recurse_data = from_data
|
||||
for value in recurse_data:
|
||||
get_file_contents(value, files, base_url, ignore_if, recurse_if,
|
||||
is_object, object_request)
|
||||
|
||||
if isinstance(from_data, dict):
|
||||
for key, value in from_data.items():
|
||||
if ignore_if and ignore_if(key, value):
|
||||
continue
|
||||
|
||||
if base_url and not base_url.endswith('/'):
|
||||
base_url = base_url + '/'
|
||||
|
||||
str_url = parse.urljoin(base_url, value)
|
||||
if str_url not in files:
|
||||
if is_object and object_request:
|
||||
file_content = object_request('GET', str_url)
|
||||
else:
|
||||
file_content = utils.read_url_content(str_url)
|
||||
if is_template(file_content):
|
||||
if is_object:
|
||||
template = get_template_contents(
|
||||
template_object=str_url, files=files,
|
||||
object_request=object_request)[1]
|
||||
else:
|
||||
template = get_template_contents(
|
||||
template_url=str_url, files=files)[1]
|
||||
file_content = jsonutils.dumps(template)
|
||||
files[str_url] = file_content
|
||||
# replace the data value with the normalised absolute URL
|
||||
from_data[key] = str_url
|
||||
|
||||
|
||||
def read_url_content(url):
|
||||
'''DEPRECATED! Use 'utils.read_url_content' instead.'''
|
||||
return utils.read_url_content(url)
|
||||
|
||||
|
||||
def base_url_for_url(url):
|
||||
'''DEPRECATED! Use 'utils.base_url_for_url' instead.'''
|
||||
return utils.base_url_for_url(url)
|
||||
|
||||
|
||||
def normalise_file_path_to_url(path):
|
||||
'''DEPRECATED! Use 'utils.normalise_file_path_to_url' instead.'''
|
||||
return utils.normalise_file_path_to_url(path)
|
||||
|
||||
|
||||
def deep_update(old, new):
|
||||
'''Merge nested dictionaries.'''
|
||||
|
||||
# Prevents an error if in a previous iteration
|
||||
# old[k] = None but v[k] = {...},
|
||||
if old is None:
|
||||
old = {}
|
||||
|
||||
for k, v in new.items():
|
||||
if isinstance(v, collections.Mapping):
|
||||
r = deep_update(old.get(k, {}), v)
|
||||
old[k] = r
|
||||
else:
|
||||
old[k] = new[k]
|
||||
return old
|
||||
|
||||
|
||||
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):
|
||||
"""Reads one or more environment files.
|
||||
|
||||
Reads in each specified environment file and returns a dictionary
|
||||
of the filenames->contents (suitable for the files dict)
|
||||
and the consolidated environment (after having applied the correct
|
||||
overrides based on order).
|
||||
|
||||
If a list is provided in the env_list_tracker parameter, the behavior
|
||||
is altered to take advantage of server-side environment resolution.
|
||||
Specifically, this means:
|
||||
|
||||
* Populating env_list_tracker with an ordered list of environment file
|
||||
URLs to be passed to the server
|
||||
* Including the contents of each environment file in the returned
|
||||
files dict, keyed by one of the URLs in env_list_tracker
|
||||
|
||||
:param env_paths: list of paths to the environment files to load; if
|
||||
None, empty results will be returned
|
||||
:type env_paths: list or None
|
||||
:param template: unused; only included for API compatibility
|
||||
:param template_url: unused; only included for API compatibility
|
||||
:param env_list_tracker: if specified, environment filenames will be
|
||||
stored within
|
||||
:type env_list_tracker: list or None
|
||||
:return: tuple of files dict and a dict of the consolidated environment
|
||||
:rtype: tuple
|
||||
"""
|
||||
merged_files = {}
|
||||
merged_env = {}
|
||||
|
||||
# If we're keeping a list of environment files separately, include the
|
||||
# contents of the files in the files dict
|
||||
include_env_in_files = env_list_tracker is not 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 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
|
||||
|
||||
|
||||
def process_environment_and_files(env_path=None,
|
||||
template=None,
|
||||
template_url=None,
|
||||
env_path_is_object=None,
|
||||
object_request=None,
|
||||
include_env_in_files=False):
|
||||
"""Loads a single environment file.
|
||||
|
||||
Returns an entry suitable for the files dict which maps the environment
|
||||
filename to its contents.
|
||||
|
||||
:param env_path: full path to the file to load
|
||||
:type env_path: str or None
|
||||
:param include_env_in_files: if specified, the raw environment file itself
|
||||
will be included in the returned files dict
|
||||
:type include_env_in_files: bool
|
||||
:return: tuple of files dict and the loaded environment as a dict
|
||||
:rtype: (dict, dict)
|
||||
"""
|
||||
files = {}
|
||||
env = {}
|
||||
|
||||
is_object = env_path_is_object and env_path_is_object(env_path)
|
||||
|
||||
if is_object:
|
||||
raw_env = object_request and object_request('GET', env_path)
|
||||
env = environment_format.parse(raw_env)
|
||||
env_base_url = utils.base_url_for_url(env_path)
|
||||
|
||||
resolve_environment_urls(
|
||||
env.get('resource_registry'),
|
||||
files,
|
||||
env_base_url, is_object=True, object_request=object_request)
|
||||
|
||||
elif env_path:
|
||||
env_url = utils.normalise_file_path_to_url(env_path)
|
||||
env_base_url = utils.base_url_for_url(env_url)
|
||||
raw_env = request.urlopen(env_url).read()
|
||||
|
||||
env = environment_format.parse(raw_env)
|
||||
|
||||
resolve_environment_urls(
|
||||
env.get('resource_registry'),
|
||||
files,
|
||||
env_base_url)
|
||||
|
||||
if include_env_in_files:
|
||||
files[env_url] = jsonutils.dumps(env)
|
||||
|
||||
return files, env
|
||||
|
||||
|
||||
def resolve_environment_urls(resource_registry, files, env_base_url,
|
||||
is_object=False, object_request=None):
|
||||
"""Handles any resource URLs specified in an environment.
|
||||
|
||||
:param resource_registry: mapping of type name to template filename
|
||||
:type resource_registry: dict
|
||||
:param files: dict to store loaded file contents into
|
||||
:type files: dict
|
||||
:param env_base_url: base URL to look in when loading files
|
||||
:type env_base_url: str or None
|
||||
"""
|
||||
if resource_registry is None:
|
||||
return
|
||||
|
||||
rr = resource_registry
|
||||
base_url = rr.get('base_url', env_base_url)
|
||||
|
||||
def ignore_if(key, value):
|
||||
if key == 'base_url':
|
||||
return True
|
||||
if isinstance(value, dict):
|
||||
return True
|
||||
if '::' in value:
|
||||
# Built in providers like: "X::Compute::Server"
|
||||
# don't need downloading.
|
||||
return True
|
||||
if key in ['hooks', 'restricted_actions']:
|
||||
return True
|
||||
|
||||
get_file_contents(rr, files, base_url, ignore_if,
|
||||
is_object=is_object, object_request=object_request)
|
||||
|
||||
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,
|
||||
is_object=is_object, object_request=object_request)
|
||||
|
||||
|
||||
def hooks_to_env(env, arg_hooks, hook):
|
||||
"""Add hooks from args to environment's resource_registry section.
|
||||
|
||||
Hooks are either "resource_name" (if it's a top-level resource) or
|
||||
"nested_stack/resource_name" (if the resource is in a nested stack).
|
||||
|
||||
The environment expects each hook to be associated with the resource
|
||||
within `resource_registry/resources` using the `hooks: pre-create` format.
|
||||
"""
|
||||
if 'resource_registry' not in env:
|
||||
env['resource_registry'] = {}
|
||||
if 'resources' not in env['resource_registry']:
|
||||
env['resource_registry']['resources'] = {}
|
||||
for hook_declaration in arg_hooks:
|
||||
hook_path = [r for r in hook_declaration.split('/') if r]
|
||||
resources = env['resource_registry']['resources']
|
||||
for nested_stack in hook_path:
|
||||
if nested_stack not in resources:
|
||||
resources[nested_stack] = {}
|
||||
resources = resources[nested_stack]
|
||||
else:
|
||||
resources['hooks'] = hook
|
|
@ -1,449 +0,0 @@
|
|||
# Copyright 2012 OpenStack Foundation
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import base64
|
||||
import logging
|
||||
import os
|
||||
import textwrap
|
||||
import uuid
|
||||
|
||||
from oslo_serialization import jsonutils
|
||||
from oslo_utils import encodeutils
|
||||
import prettytable
|
||||
import six
|
||||
from six.moves.urllib import error
|
||||
from six.moves.urllib import parse
|
||||
from six.moves.urllib import request
|
||||
import yaml
|
||||
|
||||
from heatclient._i18n import _
|
||||
from heatclient import exc
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
supported_formats = {
|
||||
"json": lambda x: jsonutils.dumps(x, indent=2),
|
||||
"yaml": yaml.safe_dump
|
||||
}
|
||||
|
||||
|
||||
def arg(*args, **kwargs):
|
||||
"""Decorator for CLI args.
|
||||
|
||||
Example:
|
||||
|
||||
>>> @arg("name", help="Name of the new entity")
|
||||
... def entity_create(args):
|
||||
... pass
|
||||
"""
|
||||
def _decorator(func):
|
||||
add_arg(func, *args, **kwargs)
|
||||
return func
|
||||
return _decorator
|
||||
|
||||
|
||||
def env(*args, **kwargs):
|
||||
"""Returns the first environment variable set.
|
||||
|
||||
If all are empty, defaults to '' or keyword arg `default`.
|
||||
"""
|
||||
for arg in args:
|
||||
value = os.environ.get(arg)
|
||||
if value:
|
||||
return value
|
||||
return kwargs.get('default', '')
|
||||
|
||||
|
||||
def add_arg(func, *args, **kwargs):
|
||||
"""Bind CLI arguments to a shell.py `do_foo` function."""
|
||||
|
||||
if not hasattr(func, 'arguments'):
|
||||
func.arguments = []
|
||||
|
||||
# NOTE(sirp): avoid dups that can occur when the module is shared across
|
||||
# tests.
|
||||
if (args, kwargs) not in func.arguments:
|
||||
# Because of the semantics of decorator composition if we just append
|
||||
# to the options list positional options will appear to be backwards.
|
||||
func.arguments.insert(0, (args, kwargs))
|
||||
|
||||
|
||||
def print_list(objs, fields, formatters=None, sortby_index=0,
|
||||
mixed_case_fields=None, field_labels=None):
|
||||
"""Print a list of objects as a table, one row per object.
|
||||
|
||||
:param objs: iterable of :class:`Resource`
|
||||
:param fields: attributes that correspond to columns, in order
|
||||
:param formatters: `dict` of callables for field formatting
|
||||
:param sortby_index: index of the field for sorting table rows
|
||||
:param mixed_case_fields: fields corresponding to object attributes that
|
||||
have mixed case names (e.g., 'serverId')
|
||||
:param field_labels: Labels to use in the heading of the table, default to
|
||||
fields.
|
||||
"""
|
||||
formatters = formatters or {}
|
||||
mixed_case_fields = mixed_case_fields or []
|
||||
field_labels = field_labels or fields
|
||||
if len(field_labels) != len(fields):
|
||||
raise ValueError(_("Field labels list %(labels)s has different number "
|
||||
"of elements than fields list %(fields)s"),
|
||||
{'labels': field_labels, 'fields': fields})
|
||||
|
||||
if sortby_index is None:
|
||||
kwargs = {}
|
||||
else:
|
||||
kwargs = {'sortby': field_labels[sortby_index]}
|
||||
pt = prettytable.PrettyTable(field_labels)
|
||||
pt.align = 'l'
|
||||
|
||||
for o in objs:
|
||||
row = []
|
||||
for field in fields:
|
||||
if field in formatters:
|
||||
row.append(formatters[field](o))
|
||||
else:
|
||||
if field in mixed_case_fields:
|
||||
field_name = field.replace(' ', '_')
|
||||
else:
|
||||
field_name = field.lower().replace(' ', '_')
|
||||
data = getattr(o, field_name, '')
|
||||
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)))
|
||||
|
||||
|
||||
def link_formatter(links):
|
||||
def format_link(l):
|
||||
if 'rel' in l:
|
||||
return "%s (%s)" % (l.get('href', ''), l.get('rel', ''))
|
||||
else:
|
||||
return "%s" % (l.get('href', ''))
|
||||
return '\n'.join(format_link(l) for l in links or [])
|
||||
|
||||
|
||||
def resource_nested_identifier(rsrc):
|
||||
nested_link = [l for l in rsrc.links or []
|
||||
if l.get('rel') == 'nested']
|
||||
if nested_link:
|
||||
nested_href = nested_link[0].get('href')
|
||||
nested_identifier = nested_href.split("/")[-2:]
|
||||
return "/".join(nested_identifier)
|
||||
|
||||
|
||||
def json_formatter(js):
|
||||
return jsonutils.dumps(js, indent=2, ensure_ascii=False,
|
||||
separators=(', ', ': '))
|
||||
|
||||
|
||||
def yaml_formatter(js):
|
||||
return yaml.safe_dump(js, default_flow_style=False)
|
||||
|
||||
|
||||
def text_wrap_formatter(d):
|
||||
return '\n'.join(textwrap.wrap(d or '', 55))
|
||||
|
||||
|
||||
def newline_list_formatter(r):
|
||||
return '\n'.join(r or [])
|
||||
|
||||
|
||||
def print_dict(d, formatters=None):
|
||||
formatters = formatters or {}
|
||||
pt = prettytable.PrettyTable(['Property', 'Value'],
|
||||
caching=False, print_empty=False)
|
||||
pt.align = 'l'
|
||||
|
||||
for field in d.keys():
|
||||
if field in formatters:
|
||||
pt.add_row([field, formatters[field](d[field])])
|
||||
else:
|
||||
pt.add_row([field, d[field]])
|
||||
print(pt.get_string(sortby='Property'))
|
||||
|
||||
|
||||
class EventLogContext(object):
|
||||
|
||||
def __init__(self):
|
||||
# key is a stack id or the name of the nested stack, value is a tuple
|
||||
# of the parent stack id, and the name of the resource in the parent
|
||||
# stack
|
||||
self.id_to_res_info = {}
|
||||
|
||||
def prepend_paths(self, resource_path, stack_id):
|
||||
if stack_id not in self.id_to_res_info:
|
||||
return
|
||||
stack_id, res_name = self.id_to_res_info.get(stack_id)
|
||||
if res_name in self.id_to_res_info:
|
||||
# do a double lookup to skip the ugly stack name that doesn't
|
||||
# correspond to an actual resource name
|
||||
n_stack_id, res_name = self.id_to_res_info.get(res_name)
|
||||
resource_path.insert(0, res_name)
|
||||
self.prepend_paths(resource_path, n_stack_id)
|
||||
elif res_name:
|
||||
resource_path.insert(0, res_name)
|
||||
|
||||
def build_resource_name(self, event):
|
||||
res_name = getattr(event, 'resource_name')
|
||||
|
||||
# Contribute this event to self.id_to_res_info to assist with
|
||||
# future calls to build_resource_name
|
||||
|
||||
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:
|
||||
return None
|
||||
stack_link = l['href']
|
||||
return stack_link.split('/')[-1]
|
||||
|
||||
stack_id = get_stack_id()
|
||||
if not stack_id:
|
||||
return res_name
|
||||
phys_id = getattr(event, 'physical_resource_id', None)
|
||||
status = getattr(event, 'resource_status', None)
|
||||
|
||||
is_stack_event = stack_id == phys_id
|
||||
if is_stack_event:
|
||||
# this is an event for a stack
|
||||
self.id_to_res_info[stack_id] = (stack_id, res_name)
|
||||
elif phys_id and status == 'CREATE_IN_PROGRESS':
|
||||
# this might be an event for a resource which creates a stack
|
||||
self.id_to_res_info[phys_id] = (stack_id, res_name)
|
||||
|
||||
# Now build this resource path based on previous calls to
|
||||
# build_resource_name
|
||||
resource_path = []
|
||||
if res_name and not is_stack_event:
|
||||
resource_path.append(res_name)
|
||||
self.prepend_paths(resource_path, stack_id)
|
||||
|
||||
return '.'.join(resource_path)
|
||||
|
||||
|
||||
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")
|
||||
|
||||
# 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
|
||||
# anyway
|
||||
if event_log_context is None:
|
||||
event_log_context = EventLogContext()
|
||||
|
||||
for event in events:
|
||||
rsrc_name = event_log_context.build_resource_name(event)
|
||||
|
||||
event_time = getattr(event, 'event_time', '')
|
||||
log = log_format % {
|
||||
'event_time': event_time.replace('T', ' '),
|
||||
'rsrc_name': rsrc_name,
|
||||
'rsrc_status': getattr(event, 'resource_status', ''),
|
||||
'rsrc_status_reason': getattr(event, 'resource_status_reason', '')
|
||||
}
|
||||
event_log.append(log)
|
||||
|
||||
return "\n".join(event_log)
|
||||
|
||||
|
||||
def print_update_list(lst, fields, formatters=None):
|
||||
"""Print the stack-update --dry-run output as a table.
|
||||
|
||||
This function is necessary to print the stack-update --dry-run
|
||||
output, which contains additional information about the update.
|
||||
"""
|
||||
formatters = formatters or {}
|
||||
pt = prettytable.PrettyTable(fields, caching=False, print_empty=False)
|
||||
pt.align = 'l'
|
||||
|
||||
for change in lst:
|
||||
row = []
|
||||
for field in fields:
|
||||
if field in formatters:
|
||||
row.append(formatters[field](change.get(field, None)))
|
||||
else:
|
||||
row.append(change.get(field, None))
|
||||
|
||||
pt.add_row(row)
|
||||
|
||||
if six.PY3:
|
||||
print(encodeutils.safe_encode(pt.get_string()).decode())
|
||||
else:
|
||||
print(encodeutils.safe_encode(pt.get_string()))
|
||||
|
||||
|
||||
def find_resource(manager, name_or_id):
|
||||
"""Helper for the _find_* methods."""
|
||||
# first try to get entity as integer id
|
||||
try:
|
||||
if isinstance(name_or_id, int) or name_or_id.isdigit():
|
||||
return manager.get(int(name_or_id))
|
||||
except exc.NotFound:
|
||||
pass
|
||||
|
||||
# now try to get entity as uuid
|
||||
try:
|
||||
uuid.UUID(str(name_or_id))
|
||||
return manager.get(name_or_id)
|
||||
except (ValueError, exc.NotFound):
|
||||
pass
|
||||
|
||||
# finally try to find entity by name
|
||||
try:
|
||||
return manager.find(name=name_or_id)
|
||||
except exc.NotFound:
|
||||
msg = (
|
||||
_("No %(name)s with a name or ID of "
|
||||
"'%(name_or_id)s' exists.")
|
||||
% {
|
||||
'name': manager.resource_class.__name__.lower(),
|
||||
'name_or_id': name_or_id
|
||||
})
|
||||
raise exc.CommandError(msg)
|
||||
|
||||
|
||||
def format_parameters(params, parse_semicolon=True):
|
||||
'''Reformat parameters into dict of format expected by the API.'''
|
||||
|
||||
if not params:
|
||||
return {}
|
||||
|
||||
if parse_semicolon:
|
||||
# expect multiple invocations of --parameters but fall back
|
||||
# to ; delimited if only one --parameters is specified
|
||||
if len(params) == 1:
|
||||
params = params[0].split(';')
|
||||
|
||||
parameters = {}
|
||||
for p in params:
|
||||
try:
|
||||
(n, v) = p.split(('='), 1)
|
||||
except ValueError:
|
||||
msg = _('Malformed parameter(%s). Use the key=value format.') % p
|
||||
raise exc.CommandError(msg)
|
||||
|
||||
if n not in parameters:
|
||||
parameters[n] = v
|
||||
else:
|
||||
if not isinstance(parameters[n], list):
|
||||
parameters[n] = [parameters[n]]
|
||||
parameters[n].append(v)
|
||||
|
||||
return parameters
|
||||
|
||||
|
||||
def format_all_parameters(params, param_files,
|
||||
template_file=None, template_url=None):
|
||||
parameters = {}
|
||||
parameters.update(format_parameters(params))
|
||||
parameters.update(format_parameter_file(
|
||||
param_files,
|
||||
template_file,
|
||||
template_url))
|
||||
return parameters
|
||||
|
||||
|
||||
def format_parameter_file(param_files, template_file=None,
|
||||
template_url=None):
|
||||
'''Reformat file parameters into dict of format expected by the API.'''
|
||||
if not param_files:
|
||||
return {}
|
||||
params = format_parameters(param_files, False)
|
||||
|
||||
template_base_url = None
|
||||
if template_file or template_url:
|
||||
template_base_url = base_url_for_url(get_template_url(
|
||||
template_file, template_url))
|
||||
|
||||
param_file = {}
|
||||
for key, value in params.items():
|
||||
param_file[key] = resolve_param_get_file(value,
|
||||
template_base_url)
|
||||
return param_file
|
||||
|
||||
|
||||
def resolve_param_get_file(file, base_url):
|
||||
if base_url and not base_url.endswith('/'):
|
||||
base_url = base_url + '/'
|
||||
str_url = parse.urljoin(base_url, file)
|
||||
return read_url_content(str_url)
|
||||
|
||||
|
||||
def format_output(output, format='yaml'):
|
||||
"""Format the supplied dict as specified."""
|
||||
output_format = format.lower()
|
||||
try:
|
||||
return supported_formats[output_format](output)
|
||||
except KeyError:
|
||||
raise exc.HTTPUnsupported(_("The format(%s) is unsupported.")
|
||||
% output_format)
|
||||
|
||||
|
||||
def parse_query_url(url):
|
||||
base_url, query_params = url.split('?')
|
||||
return base_url, parse.parse_qs(query_params)
|
||||
|
||||
|
||||
def get_template_url(template_file=None, template_url=None):
|
||||
if template_file:
|
||||
template_url = normalise_file_path_to_url(template_file)
|
||||
return template_url
|
||||
|
||||
|
||||
def read_url_content(url):
|
||||
try:
|
||||
content = request.urlopen(url).read()
|
||||
except error.URLError:
|
||||
raise exc.CommandError(_('Could not fetch contents for %s') % url)
|
||||
|
||||
if content:
|
||||
try:
|
||||
content.decode('utf-8')
|
||||
except ValueError:
|
||||
content = base64.encodestring(content)
|
||||
return content
|
||||
|
||||
|
||||
def base_url_for_url(url):
|
||||
parsed = parse.urlparse(url)
|
||||
parsed_dir = os.path.dirname(parsed.path)
|
||||
return parse.urljoin(url, parsed_dir)
|
||||
|
||||
|
||||
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))
|
||||
|
||||
|
||||
def get_response_body(resp):
|
||||
body = resp.content
|
||||
if 'application/json' in resp.headers.get('content-type', ''):
|
||||
try:
|
||||
body = resp.json()
|
||||
except ValueError:
|
||||
LOG.error('Could not decode response body as JSON')
|
||||
else:
|
||||
body = None
|
||||
return body
|
|
@ -1,202 +0,0 @@
|
|||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import sys
|
||||
|
||||
from oslo_serialization import jsonutils
|
||||
from oslo_utils import reflection
|
||||
|
||||
from heatclient._i18n import _
|
||||
|
||||
verbose = 0
|
||||
|
||||
|
||||
class BaseException(Exception):
|
||||
"""An error occurred."""
|
||||
def __init__(self, message=None):
|
||||
self.message = message
|
||||
|
||||
def __str__(self):
|
||||
return self.message or self.__class__.__doc__
|
||||
|
||||
|
||||
class CommandError(BaseException):
|
||||
"""Invalid usage of CLI."""
|
||||
|
||||
|
||||
class InvalidEndpoint(BaseException):
|
||||
"""The provided endpoint is invalid."""
|
||||
|
||||
|
||||
class CommunicationError(BaseException):
|
||||
"""Unable to communicate with server."""
|
||||
|
||||
|
||||
class HTTPException(BaseException):
|
||||
"""Base exception for all HTTP-derived exceptions."""
|
||||
code = 'N/A'
|
||||
|
||||
def __init__(self, message=None, code=None):
|
||||
super(HTTPException, self).__init__(message)
|
||||
try:
|
||||
self.error = jsonutils.loads(message)
|
||||
if 'error' not in self.error:
|
||||
raise KeyError(_('Key "error" not exists'))
|
||||
except KeyError:
|
||||
# NOTE(jianingy): If key 'error' happens not exist,
|
||||
# self.message becomes no sense. In this case, we
|
||||
# return doc of current exception class instead.
|
||||
self.error = {'error':
|
||||
{'message': self.__class__.__doc__}}
|
||||
except Exception:
|
||||
self.error = {'error':
|
||||
{'message': self.message or self.__class__.__doc__}}
|
||||
if self.code == "N/A" and code is not None:
|
||||
self.code = code
|
||||
|
||||
def __str__(self):
|
||||
message = self.error['error'].get('message', 'Internal Error')
|
||||
if verbose:
|
||||
traceback = self.error['error'].get('traceback', '')
|
||||
return (_('ERROR: %(message)s\n%(traceback)s') %
|
||||
{'message': message, 'traceback': traceback})
|
||||
else:
|
||||
return _('ERROR: %s') % message
|
||||
|
||||
|
||||
class HTTPMultipleChoices(HTTPException):
|
||||
code = 300
|
||||
|
||||
def __str__(self):
|
||||
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),
|
||||
'code': self.code,
|
||||
'details': self.details})
|
||||
|
||||
|
||||
class BadRequest(HTTPException):
|
||||
"""DEPRECATED."""
|
||||
code = 400
|
||||
|
||||
|
||||
class HTTPBadRequest(BadRequest):
|
||||
pass
|
||||
|
||||
|
||||
class Unauthorized(HTTPException):
|
||||
"""DEPRECATED."""
|
||||
code = 401
|
||||
|
||||
|
||||
class HTTPUnauthorized(Unauthorized):
|
||||
pass
|
||||
|
||||
|
||||
class Forbidden(HTTPException):
|
||||
"""DEPRECATED."""
|
||||
code = 403
|
||||
|
||||
|
||||
class HTTPForbidden(Forbidden):
|
||||
pass
|
||||
|
||||
|
||||
class NotFound(HTTPException):
|
||||
"""DEPRECATED."""
|
||||
code = 404
|
||||
|
||||
|
||||
class HTTPNotFound(NotFound):
|
||||
pass
|
||||
|
||||
|
||||
class NoUniqueMatch(HTTPException):
|
||||
pass
|
||||
|
||||
|
||||
class HTTPMethodNotAllowed(HTTPException):
|
||||
code = 405
|
||||
|
||||
|
||||
class Conflict(HTTPException):
|
||||
"""DEPRECATED."""
|
||||
code = 409
|
||||
|
||||
|
||||
class HTTPConflict(Conflict):
|
||||
pass
|
||||
|
||||
|
||||
class OverLimit(HTTPException):
|
||||
"""DEPRECATED."""
|
||||
code = 413
|
||||
|
||||
|
||||
class HTTPOverLimit(OverLimit):
|
||||
pass
|
||||
|
||||
|
||||
class HTTPUnsupported(HTTPException):
|
||||
code = 415
|
||||
|
||||
|
||||
class HTTPInternalServerError(HTTPException):
|
||||
code = 500
|
||||
|
||||
|
||||
class HTTPNotImplemented(HTTPException):
|
||||
code = 501
|
||||
|
||||
|
||||
class HTTPBadGateway(HTTPException):
|
||||
code = 502
|
||||
|
||||
|
||||
class ServiceUnavailable(HTTPException):
|
||||
"""DEPRECATED."""
|
||||
code = 503
|
||||
|
||||
|
||||
class HTTPServiceUnavailable(ServiceUnavailable):
|
||||
pass
|
||||
|
||||
|
||||
# NOTE(bcwaldon): Build a mapping of HTTP codes to corresponding exception
|
||||
# classes
|
||||
_code_map = {}
|
||||
for obj_name in dir(sys.modules[__name__]):
|
||||
if obj_name.startswith('HTTP'):
|
||||
obj = getattr(sys.modules[__name__], obj_name)
|
||||
_code_map[obj.code] = obj
|
||||
|
||||
|
||||
def from_response(response):
|
||||
"""Return an instance of an HTTPException based on requests response."""
|
||||
cls = _code_map.get(response.status_code, HTTPException)
|
||||
return cls(response.content, response.status_code)
|
||||
|
||||
|
||||
class NoTokenLookupException(Exception):
|
||||
"""DEPRECATED."""
|
||||
pass
|
||||
|
||||
|
||||
class EndpointNotFound(Exception):
|
||||
"""DEPRECATED."""
|
||||
pass
|
||||
|
||||
|
||||
class StackFailure(Exception):
|
||||
pass
|
|
@ -1,25 +0,0 @@
|
|||
# zzxwill <zzxwill@gmail.com>, 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 <zzxwill@gmail.com>\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)"
|
|
@ -1,24 +0,0 @@
|
|||
# zzxwill <zzxwill@gmail.com>, 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 <zzxwill@gmail.com>\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 "因为用户没有确认栈删除操作,所以不执行任何动作。"
|
|
@ -1,26 +0,0 @@
|
|||
# zzxwill <zzxwill@gmail.com>, 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 <zzxwill@gmail.com>\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文件。"
|
|
@ -1,70 +0,0 @@
|
|||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
#
|
||||
|
||||
import logging
|
||||
|
||||
from osc_lib import utils
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_ORCHESTRATION_API_VERSION = '1'
|
||||
API_VERSION_OPTION = 'os_orchestration_api_version'
|
||||
API_NAME = 'orchestration'
|
||||
API_VERSIONS = {
|
||||
'1': 'heatclient.v1.client.Client',
|
||||
}
|
||||
|
||||
|
||||
def make_client(instance):
|
||||
"""Returns an orchestration service client"""
|
||||
heat_client = utils.get_client_class(
|
||||
API_NAME,
|
||||
instance._api_version[API_NAME],
|
||||
API_VERSIONS)
|
||||
LOG.debug('Instantiating orchestration client: %s', heat_client)
|
||||
|
||||
kwargs = {'region_name': instance.region_name,
|
||||
'interface': instance.interface}
|
||||
|
||||
if instance.session:
|
||||
kwargs.update({'session': instance.session,
|
||||
'service_type': API_NAME})
|
||||
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,
|
||||
'token': instance.auth_ref.auth_token})
|
||||
|
||||
client = heat_client(**kwargs)
|
||||
|
||||
return client
|
||||
|
||||
|
||||
def build_option_parser(parser):
|
||||
"""Hook to add global options"""
|
||||
parser.add_argument(
|
||||
'--os-orchestration-api-version',
|
||||
metavar='<orchestration-api-version>',
|
||||
default=utils.env(
|
||||
'OS_ORCHESTRATION_API_VERSION',
|
||||
default=DEFAULT_ORCHESTRATION_API_VERSION),
|
||||
help='Orchestration API version, default=' +
|
||||
DEFAULT_ORCHESTRATION_API_VERSION +
|
||||
' (Env: OS_ORCHESTRATION_API_VERSION)')
|
||||
return parser
|
|
@ -1,46 +0,0 @@
|
|||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
#
|
||||
|
||||
"""Orchestration v1 build info action implementations"""
|
||||
|
||||
import logging
|
||||
|
||||
from osc_lib.command import command
|
||||
from osc_lib import utils
|
||||
import six
|
||||
|
||||
from heatclient.common import utils as heat_utils
|
||||
|
||||
|
||||
class BuildInfo(command.ShowOne):
|
||||
"""Retrieve build information."""
|
||||
|
||||
log = logging.getLogger(__name__ + ".BuildInfo")
|
||||
|
||||
def get_parser(self, prog_name):
|
||||
parser = super(BuildInfo, self).get_parser(prog_name)
|
||||
return parser
|
||||
|
||||
def take_action(self, parsed_args):
|
||||
self.log.debug("take_action(%s)", parsed_args)
|
||||
|
||||
heat_client = self.app.client_manager.orchestration
|
||||
result = heat_client.build_info.build_info()
|
||||
|
||||
formatters = {
|
||||
'api': heat_utils.json_formatter,
|
||||
'engine': heat_utils.json_formatter,
|
||||
}
|
||||
columns = sorted(list(six.iterkeys(result)))
|
||||
return columns, utils.get_dict_properties(result, columns,
|
||||
formatters=formatters)
|
|
@ -1,250 +0,0 @@
|
|||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
#
|
||||
# Copyright 2015 IBM Corp.
|
||||
|
||||
import logging
|
||||
import time
|
||||
|
||||
from cliff.formatters import base
|
||||
from osc_lib.command import command
|
||||
from osc_lib import utils
|
||||
|
||||
from heatclient._i18n import _
|
||||
from heatclient.common import event_utils
|
||||
from heatclient.common import utils as heat_utils
|
||||
from heatclient import exc
|
||||
|
||||
|
||||
class ShowEvent(command.ShowOne):
|
||||
"""Show event details."""
|
||||
|
||||
log = logging.getLogger(__name__ + '.ShowEvent')
|
||||
|
||||
def get_parser(self, prog_name):
|
||||
parser = super(ShowEvent, self).get_parser(prog_name)
|
||||
parser.add_argument(
|
||||
'stack',
|
||||
metavar='<stack>',
|
||||
help=_('Name or ID of stack to show events for')
|
||||
)
|
||||
parser.add_argument(
|
||||
'resource',
|
||||
metavar='<resource>',
|
||||
help=_('Name of the resource event belongs to')
|
||||
)
|
||||
parser.add_argument(
|
||||
'event',
|
||||
metavar='<event>',
|
||||
help=_('ID of event to display details for')
|
||||
)
|
||||
|
||||
return parser
|
||||
|
||||
def take_action(self, parsed_args):
|
||||
self.log.debug('take_action(%s)', parsed_args)
|
||||
|
||||
client = self.app.client_manager.orchestration
|
||||
|
||||
fields = {
|
||||
'stack_id': parsed_args.stack,
|
||||
'resource_name': parsed_args.resource,
|
||||
'event_id': parsed_args.event
|
||||
}
|
||||
|
||||
try:
|
||||
client.stacks.get(parsed_args.stack)
|
||||
client.resources.get(parsed_args.stack, parsed_args.resource)
|
||||
event = client.events.get(**fields)
|
||||
except exc.HTTPNotFound as ex:
|
||||
raise exc.CommandError(str(ex))
|
||||
|
||||
formatters = {
|
||||
'links': heat_utils.link_formatter,
|
||||
'resource_properties': heat_utils.json_formatter
|
||||
}
|
||||
|
||||
columns = []
|
||||
for key in event.to_dict():
|
||||
columns.append(key)
|
||||
|
||||
return columns, utils.get_item_properties(event, columns,
|
||||
formatters=formatters)
|
||||
|
||||
|
||||
class ListEvent(command.Lister):
|
||||
"""List events."""
|
||||
|
||||
log = logging.getLogger(__name__ + '.ListEvent')
|
||||
|
||||
@property
|
||||
def formatter_default(self):
|
||||
return 'log'
|
||||
|
||||
@property
|
||||
def formatter_namespace(self):
|
||||
return 'heatclient.event.formatter.list'
|
||||
|
||||
def get_parser(self, prog_name):
|
||||
parser = super(ListEvent, self).get_parser(prog_name)
|
||||
parser.add_argument(
|
||||
'stack',
|
||||
metavar='<stack>',
|
||||
help=_('Name or ID of stack to show events for')
|
||||
)
|
||||
parser.add_argument(
|
||||
'--resource',
|
||||
metavar='<resource>',
|
||||
help=_('Name of resource to show events for. Note: this cannot '
|
||||
'be specified with --nested-depth')
|
||||
)
|
||||
parser.add_argument(
|
||||
'--filter',
|
||||
metavar='<key=value>',
|
||||
action='append',
|
||||
help=_('Filter parameters to apply on returned events')
|
||||
)
|
||||
parser.add_argument(
|
||||
'--limit',
|
||||
metavar='<limit>',
|
||||
type=int,
|
||||
help=_('Limit the number of events returned')
|
||||
)
|
||||
parser.add_argument(
|
||||
'--marker',
|
||||
metavar='<id>',
|
||||
help=_('Only return events that appear after the given ID')
|
||||
)
|
||||
parser.add_argument(
|
||||
'--nested-depth',
|
||||
metavar='<depth>',
|
||||
type=int,
|
||||
help=_('Depth of nested stacks from which to display events. '
|
||||
'Note: this cannot be specified with --resource')
|
||||
)
|
||||
parser.add_argument(
|
||||
'--sort',
|
||||
metavar='<key>[:<direction>]',
|
||||
action='append',
|
||||
help=_('Sort output by selected keys and directions (asc or desc) '
|
||||
'(default: asc). Specify multiple times to sort on '
|
||||
'multiple keys. Sort key can be: '
|
||||
'"event_time" (default), "resource_name", "links", '
|
||||
'"logical_resource_id", "resource_status", '
|
||||
'"resource_status_reason", "physical_resource_id", or '
|
||||
'"id". You can leave the key empty and specify ":desc" '
|
||||
'for sorting by reverse time.')
|
||||
)
|
||||
parser.add_argument(
|
||||
'--follow',
|
||||
action='store_true',
|
||||
help=_('Print events until process is halted')
|
||||
)
|
||||
return parser
|
||||
|
||||
def take_action(self, parsed_args):
|
||||
self.log.debug('take_action(%s)', parsed_args)
|
||||
|
||||
client = self.app.client_manager.orchestration
|
||||
|
||||
columns = ['id', 'resource_status', 'resource_status_reason',
|
||||
'event_time', 'physical_resource_id']
|
||||
|
||||
kwargs = {
|
||||
'resource_name': parsed_args.resource,
|
||||
'filters': heat_utils.format_parameters(parsed_args.filter),
|
||||
'sort_dir': 'asc'
|
||||
}
|
||||
|
||||
if parsed_args.resource and parsed_args.nested_depth:
|
||||
msg = _('--nested-depth cannot be specified with --resource')
|
||||
raise exc.CommandError(msg)
|
||||
|
||||
if parsed_args.nested_depth:
|
||||
columns.append('stack_name')
|
||||
nested_depth = parsed_args.nested_depth
|
||||
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')
|
||||
raise exc.CommandError(msg)
|
||||
|
||||
marker = parsed_args.marker
|
||||
try:
|
||||
event_log_context = heat_utils.EventLogContext()
|
||||
while True:
|
||||
events = event_utils.get_events(
|
||||
client,
|
||||
stack_id=parsed_args.stack,
|
||||
event_args=kwargs,
|
||||
nested_depth=nested_depth,
|
||||
marker=marker)
|
||||
if events:
|
||||
marker = getattr(events[-1], 'id', None)
|
||||
events_log = heat_utils.event_log_formatter(
|
||||
events, event_log_context)
|
||||
self.app.stdout.write(events_log)
|
||||
self.app.stdout.write('\n')
|
||||
time.sleep(5)
|
||||
# this loop never exits
|
||||
except (KeyboardInterrupt, EOFError): # ctrl-c, ctrl-d
|
||||
return [], []
|
||||
|
||||
events = event_utils.get_events(
|
||||
client, stack_id=parsed_args.stack, event_args=kwargs,
|
||||
nested_depth=nested_depth, marker=parsed_args.marker,
|
||||
limit=parsed_args.limit)
|
||||
|
||||
if parsed_args.sort:
|
||||
events = utils.sort_items(events, ','.join(sorts))
|
||||
|
||||
if parsed_args.formatter == 'log':
|
||||
return [], events
|
||||
|
||||
if len(events):
|
||||
if hasattr(events[0], 'resource_name'):
|
||||
columns.insert(0, 'resource_name')
|
||||
columns.append('logical_resource_id')
|
||||
else:
|
||||
columns.insert(0, 'logical_resource_id')
|
||||
|
||||
return (
|
||||
columns,
|
||||
(utils.get_item_properties(s, columns) for s in events)
|
||||
)
|
||||
|
||||
|
||||
class LogFormatter(base.ListFormatter):
|
||||
"""A formatter which prints event objects in a log style"""
|
||||
|
||||
def add_argument_group(self, parser):
|
||||
pass
|
||||
|
||||
def emit_list(self, column_names, data, stdout, parsed_args):
|
||||
stdout.write(heat_utils.event_log_formatter(data))
|
||||
stdout.write('\n')
|
|
@ -1,304 +0,0 @@
|
|||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
#
|
||||
|
||||
"""Orchestration v1 Stack action implementations"""
|
||||
|
||||
import logging
|
||||
|
||||
from osc_lib.command import command
|
||||
from osc_lib import exceptions as exc
|
||||
from osc_lib.i18n import _
|
||||
from osc_lib import utils
|
||||
from oslo_serialization import jsonutils
|
||||
import six
|
||||
from six.moves.urllib import request
|
||||
|
||||
from heatclient.common import format_utils
|
||||
from heatclient.common import utils as heat_utils
|
||||
from heatclient import exc as heat_exc
|
||||
|
||||
|
||||
class ResourceShow(command.ShowOne):
|
||||
"""Display stack resource."""
|
||||
|
||||
log = logging.getLogger(__name__ + '.ResourceShowStack')
|
||||
|
||||
def get_parser(self, prog_name):
|
||||
parser = super(ResourceShow, self).get_parser(prog_name)
|
||||
parser.add_argument(
|
||||
'stack',
|
||||
metavar='<stack>',
|
||||
help=_('Name or ID of stack to query')
|
||||
)
|
||||
parser.add_argument(
|
||||
'resource',
|
||||
metavar='<resource>',
|
||||
help=_('Name of resource')
|
||||
)
|
||||
parser.add_argument(
|
||||
'--with-attr',
|
||||
metavar='<attribute>',
|
||||
action='append',
|
||||
help=_('Attribute to show, can be specified multiple times')
|
||||
)
|
||||
return parser
|
||||
|
||||
def take_action(self, parsed_args):
|
||||
self.log.debug('take_action(%s)', parsed_args)
|
||||
|
||||
client = self.app.client_manager.orchestration
|
||||
|
||||
try:
|
||||
resource = client.resources.get(parsed_args.stack,
|
||||
parsed_args.resource,
|
||||
with_attr=parsed_args.with_attr)
|
||||
except heat_exc.HTTPNotFound:
|
||||
msg = (_('Stack or resource not found: %(stack)s %(resource)s') %
|
||||
{'stack': parsed_args.stack,
|
||||
'resource': parsed_args.resource})
|
||||
raise exc.CommandError(msg)
|
||||
|
||||
return self.dict2columns(resource.to_dict())
|
||||
|
||||
|
||||
class ResourceList(command.Lister):
|
||||
"""List stack resources."""
|
||||
|
||||
log = logging.getLogger(__name__ + '.ResourceListStack')
|
||||
|
||||
@property
|
||||
def formatter_namespace(self):
|
||||
return 'heatclient.resource.formatter.list'
|
||||
|
||||
def get_parser(self, prog_name):
|
||||
parser = super(ResourceList, self).get_parser(prog_name)
|
||||
parser.add_argument(
|
||||
'stack',
|
||||
metavar='<stack>',
|
||||
help=_('Name or ID of stack to query')
|
||||
)
|
||||
parser.add_argument(
|
||||
'--long',
|
||||
action='store_true',
|
||||
help=_('Enable detailed information presented for each resource '
|
||||
'in resource list')
|
||||
)
|
||||
parser.add_argument(
|
||||
'-n', '--nested-depth',
|
||||
metavar='<nested-depth>',
|
||||
type=int,
|
||||
help=_('Depth of nested stacks from which to display resources')
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'--filter',
|
||||
metavar='<key=value>',
|
||||
action='append',
|
||||
help=_('Filter parameters to apply on returned resources based on '
|
||||
'their name, status, type, action, id and '
|
||||
'physical_resource_id')
|
||||
)
|
||||
|
||||
return parser
|
||||
|
||||
def take_action(self, parsed_args):
|
||||
self.log.debug('take_action(%s)', parsed_args)
|
||||
|
||||
client = self.app.client_manager.orchestration
|
||||
|
||||
fields = {
|
||||
'nested_depth': parsed_args.nested_depth,
|
||||
'with_detail': parsed_args.long,
|
||||
'filters': heat_utils.format_parameters(parsed_args.filter),
|
||||
}
|
||||
|
||||
try:
|
||||
resources = client.resources.list(parsed_args.stack, **fields)
|
||||
except heat_exc.HTTPNotFound:
|
||||
msg = _('Stack not found: %s') % parsed_args.stack
|
||||
raise exc.CommandError(msg)
|
||||
|
||||
if parsed_args.formatter == 'dot':
|
||||
return [], resources
|
||||
|
||||
columns = ['physical_resource_id', 'resource_type', 'resource_status',
|
||||
'updated_time']
|
||||
|
||||
if len(resources) >= 1 and not hasattr(resources[0], 'resource_name'):
|
||||
columns.insert(0, 'logical_resource_id')
|
||||
else:
|
||||
columns.insert(0, 'resource_name')
|
||||
|
||||
if parsed_args.nested_depth or parsed_args.long:
|
||||
columns.append('stack_name')
|
||||
|
||||
return (
|
||||
columns,
|
||||
(utils.get_item_properties(r, columns) for r in resources)
|
||||
)
|
||||
|
||||
|
||||
class ResourceMetadata(format_utils.JsonFormat):
|
||||
"""Show resource metadata"""
|
||||
|
||||
log = logging.getLogger(__name__ + ".ResourceMetadata")
|
||||
|
||||
def get_parser(self, prog_name):
|
||||
parser = super(ResourceMetadata, self).get_parser(prog_name)
|
||||
parser.add_argument(
|
||||
'stack',
|
||||
metavar='<stack>',
|
||||
help=_('Stack to display (name or ID)'),
|
||||
)
|
||||
parser.add_argument(
|
||||
'resource',
|
||||
metavar='<resource>',
|
||||
help=_('Name of the resource to show the metadata for'))
|
||||
return parser
|
||||
|
||||
def take_action(self, parsed_args):
|
||||
self.log.debug("take_action(%s)", parsed_args)
|
||||
|
||||
heat_client = self.app.client_manager.orchestration
|
||||
return _resource_metadata(heat_client, parsed_args)
|
||||
|
||||
|
||||
def _resource_metadata(heat_client, args):
|
||||
fields = {'stack_id': args.stack,
|
||||
'resource_name': args.resource}
|
||||
try:
|
||||
metadata = heat_client.resources.metadata(**fields)
|
||||
except heat_exc.HTTPNotFound:
|
||||
raise exc.CommandError(_('Stack %(stack)s or resource %(resource)s '
|
||||
'not found.') %
|
||||
{'stack': args.stack,
|
||||
'resource': args.resource})
|
||||
|
||||
data = list(six.itervalues(metadata))
|
||||
columns = list(six.iterkeys(metadata))
|
||||
return columns, data
|
||||
|
||||
|
||||
class ResourceSignal(command.Command):
|
||||
"""Signal a resource with optional data."""
|
||||
|
||||
log = logging.getLogger(__name__ + ".ResourceSignal")
|
||||
|
||||
def get_parser(self, prog_name):
|
||||
parser = super(ResourceSignal, self).get_parser(prog_name)
|
||||
parser.add_argument(
|
||||
'stack',
|
||||
metavar='<stack>',
|
||||
help=_('Name or ID of stack the resource belongs to'),
|
||||
)
|
||||
parser.add_argument(
|
||||
'resource',
|
||||
metavar='<resource>',
|
||||
help=_('Name of the resoure to signal'),
|
||||
)
|
||||
parser.add_argument(
|
||||
'--data',
|
||||
metavar='<data>',
|
||||
help=_('JSON Data to send to the signal handler')
|
||||
)
|
||||
parser.add_argument(
|
||||
'--data-file',
|
||||
metavar='<data-file>',
|
||||
help=_('File containing JSON data to send to the signal handler')
|
||||
)
|
||||
return parser
|
||||
|
||||
def take_action(self, parsed_args):
|
||||
self.log.debug("take_action(%s)", parsed_args)
|
||||
|
||||
heat_client = self.app.client_manager.orchestration
|
||||
return _resource_signal(heat_client, parsed_args)
|
||||
|
||||
|
||||
def _resource_signal(heat_client, args):
|
||||
fields = {'stack_id': args.stack,
|
||||
'resource_name': args.resource}
|
||||
data = args.data
|
||||
data_file = args.data_file
|
||||
if data and data_file:
|
||||
raise exc.CommandError(_('Should only specify one of data or '
|
||||
'data-file'))
|
||||
|
||||
if data_file:
|
||||
data_url = heat_utils.normalise_file_path_to_url(data_file)
|
||||
data = request.urlopen(data_url).read()
|
||||
|
||||
if data:
|
||||
try:
|
||||
data = jsonutils.loads(data)
|
||||
except ValueError as ex:
|
||||
raise exc.CommandError(_('Data should be in JSON format: %s') % ex)
|
||||
if not isinstance(data, dict):
|
||||
raise exc.CommandError(_('Data should be a JSON dict'))
|
||||
|
||||
fields['data'] = data
|
||||
try:
|
||||
heat_client.resources.signal(**fields)
|
||||
except heat_exc.HTTPNotFound:
|
||||
raise exc.CommandError(_('Stack %(stack)s or resource %(resource)s '
|
||||
'not found.') %
|
||||
{'stack': args.stack,
|
||||
'resource': args.resource})
|
||||
|
||||
|
||||
class ResourceMarkUnhealthy(command.Command):
|
||||
"""Set resource's health."""
|
||||
|
||||
log = logging.getLogger(__name__ + ".ResourceMarkUnhealthy")
|
||||
|
||||
def get_parser(self, prog_name):
|
||||
parser = super(ResourceMarkUnhealthy, self).get_parser(prog_name)
|
||||
parser.add_argument(
|
||||
'stack',
|
||||
metavar='<stack>',
|
||||
help=_('Name or ID of stack the resource belongs to')
|
||||
)
|
||||
parser.add_argument(
|
||||
'resource',
|
||||
metavar='<resource>',
|
||||
help=_('Name of the resource')
|
||||
)
|
||||
parser.add_argument(
|
||||
'reason',
|
||||
default="",
|
||||
nargs='?',
|
||||
help=_('Reason for state change')
|
||||
)
|
||||
parser.add_argument(
|
||||
'--reset',
|
||||
default=False,
|
||||
action="store_true",
|
||||
help=_('Set the resource as healthy')
|
||||
)
|
||||
return parser
|
||||
|
||||
def take_action(self, parsed_args):
|
||||
self.log.debug("take_action(%s)", parsed_args)
|
||||
|
||||
heat_client = self.app.client_manager.orchestration
|
||||
fields = {'stack_id': parsed_args.stack,
|
||||
'resource_name': parsed_args.resource,
|
||||
'mark_unhealthy': not parsed_args.reset,
|
||||
'resource_status_reason': parsed_args.reason}
|
||||
try:
|
||||
heat_client.resources.mark_unhealthy(**fields)
|
||||
except heat_exc.HTTPNotFound:
|
||||
raise exc.CommandError(_('Stack or resource not found: '
|
||||
'%(id)s %(resource)s') %
|
||||
{'id': parsed_args.stack,
|
||||
'resource': parsed_args.resource})
|
|
@ -1,132 +0,0 @@
|
|||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
#
|
||||
|
||||
"""Orchestration v1 resource type implementations"""
|
||||
|
||||
import logging
|
||||
|
||||
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
|
||||
from heatclient import exc as heat_exc
|
||||
|
||||
|
||||
class ResourceTypeShow(format_utils.YamlFormat):
|
||||
"""Show details and optionally generate a template for a resource type."""
|
||||
|
||||
log = logging.getLogger(__name__ + ".ResourceTypeShow")
|
||||
|
||||
def get_parser(self, prog_name):
|
||||
parser = super(ResourceTypeShow,
|
||||
self).get_parser(prog_name)
|
||||
parser.add_argument(
|
||||
'resource_type',
|
||||
metavar='<resource-type>',
|
||||
help=_('Resource type to show details for'),
|
||||
)
|
||||
parser.add_argument(
|
||||
'--template-type',
|
||||
metavar='<template-type>',
|
||||
help=_('Optional template type to generate, hot or cfn')
|
||||
)
|
||||
parser.add_argument(
|
||||
'--long',
|
||||
default=False,
|
||||
action='store_true',
|
||||
help=_('Show resource type with corresponding description.')
|
||||
)
|
||||
return parser
|
||||
|
||||
def take_action(self, parsed_args):
|
||||
self.log.debug("take_action(%s)", parsed_args)
|
||||
|
||||
if parsed_args.template_type is not None and parsed_args.long:
|
||||
msg = _('Cannot use --template-type and --long in one time.')
|
||||
raise exc.CommandError(msg)
|
||||
|
||||
heat_client = self.app.client_manager.orchestration
|
||||
return _show_resourcetype(heat_client, parsed_args)
|
||||
|
||||
|
||||
def _show_resourcetype(heat_client, parsed_args):
|
||||
try:
|
||||
if parsed_args.template_type:
|
||||
template_type = parsed_args.template_type.lower()
|
||||
if template_type not in ('hot', 'cfn'):
|
||||
raise exc.CommandError(
|
||||
_('Template type invalid: %s') % parsed_args.template_type)
|
||||
|
||||
fields = {'resource_type': parsed_args.resource_type,
|
||||
'template_type': template_type}
|
||||
data = heat_client.resource_types.generate_template(**fields)
|
||||
else:
|
||||
data = heat_client.resource_types.get(parsed_args.resource_type,
|
||||
parsed_args.long)
|
||||
except heat_exc.HTTPNotFound:
|
||||
raise exc.CommandError(
|
||||
_('Resource type not found: %s') % parsed_args.resource_type)
|
||||
|
||||
rows = list(six.itervalues(data))
|
||||
columns = list(six.iterkeys(data))
|
||||
return columns, rows
|
||||
|
||||
|
||||
class ResourceTypeList(command.Lister):
|
||||
"""List resource types."""
|
||||
|
||||
log = logging.getLogger(__name__ + '.ResourceTypeList')
|
||||
|
||||
def get_parser(self, prog_name):
|
||||
parser = super(ResourceTypeList,
|
||||
self).get_parser(prog_name)
|
||||
parser.add_argument(
|
||||
'--filter',
|
||||
dest='filter',
|
||||
metavar='<key=value>',
|
||||
help=_('Filter parameters to apply on returned resource types. '
|
||||
'This can be specified multiple times. It can be any of '
|
||||
'name, version or support_status'),
|
||||
action='append'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--long',
|
||||
default=False,
|
||||
action='store_true',
|
||||
help=_('Show resource types with corresponding description of '
|
||||
'each resource type.')
|
||||
)
|
||||
return parser
|
||||
|
||||
def take_action(self, parsed_args):
|
||||
self.log.debug("take_action(%s)", parsed_args)
|
||||
|
||||
heat_client = self.app.client_manager.orchestration
|
||||
return _list_resourcetypes(heat_client, parsed_args)
|
||||
|
||||
|
||||
def _list_resourcetypes(heat_client, parsed_args):
|
||||
resource_types = heat_client.resource_types.list(
|
||||
filters=heat_utils.format_parameters(parsed_args.filter),
|
||||
with_description=parsed_args.long
|
||||
)
|
||||
if parsed_args.long:
|
||||
columns = ['Resource Type', 'Description']
|
||||
rows = sorted([r.resource_type, r.description] for r in resource_types)
|
||||
else:
|
||||
columns = ['Resource Type']
|
||||
rows = sorted([r.resource_type] for r in resource_types)
|
||||
return columns, rows
|
|
@ -1,41 +0,0 @@
|
|||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
#
|
||||
|
||||
"""Orchestration v1 Service action implementations"""
|
||||
|
||||
import logging
|
||||
|
||||
from osc_lib.command import command
|
||||
from osc_lib import utils
|
||||
|
||||
|
||||
class ListService(command.Lister):
|
||||
"""List the Heat engines."""
|
||||
|
||||
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):
|
||||
self.log.debug("take_action(%s)", parsed_args)
|
||||
|
||||
heat_client = self.app.client_manager.orchestration
|
||||
columns = ['Hostname', 'Binary', 'Engine ID', 'Host',
|
||||
'Topic', 'Updated At', 'Status']
|
||||
services = heat_client.services.list()
|
||||
return (
|
||||
columns,
|
||||
(utils.get_item_properties(s, columns) for s in services)
|
||||
)
|
|
@ -1,231 +0,0 @@
|
|||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
#
|
||||
|
||||
"""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
|
||||
|
||||
|
||||
class ListSnapshot(command.Lister):
|
||||
"""List stack snapshots."""
|
||||
|
||||
log = logging.getLogger(__name__ + ".ListSnapshot")
|
||||
|
||||
def get_parser(self, prog_name):
|
||||
parser = super(ListSnapshot, self).get_parser(prog_name)
|
||||
parser.add_argument(
|
||||
'stack',
|
||||
metavar='<stack>',
|
||||
help=_('Name or ID of stack containing the snapshots')
|
||||
)
|
||||
return parser
|
||||
|
||||
def take_action(self, 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)
|
||||
|
||||
def _list_snapshot(self, heat_client, parsed_args):
|
||||
fields = {'stack_id': parsed_args.stack}
|
||||
try:
|
||||
snapshots = heat_client.stacks.snapshot_list(**fields)
|
||||
except heat_exc.HTTPNotFound:
|
||||
raise exc.CommandError(_('Stack not found: %s') %
|
||||
parsed_args.stack)
|
||||
|
||||
columns = ['id', 'name', 'status', 'status_reason', 'creation_time']
|
||||
return (
|
||||
columns,
|
||||
(utils.get_dict_properties(s, columns)
|
||||
for s in snapshots['snapshots'])
|
||||
)
|
||||
|
||||
|
||||
class ShowSnapshot(format_utils.YamlFormat):
|
||||
"""Show stack snapshot."""
|
||||
|
||||
log = logging.getLogger(__name__ + ".ShowSnapshot")
|
||||
|
||||
def get_parser(self, prog_name):
|
||||
parser = super(ShowSnapshot, self).get_parser(prog_name)
|
||||
parser.add_argument(
|
||||
'stack',
|
||||
metavar='<stack>',
|
||||
help=_('Name or ID of stack containing the snapshot')
|
||||
)
|
||||
parser.add_argument(
|
||||
'snapshot',
|
||||
metavar='<snapshot>',
|
||||
help=_('ID of the snapshot to show')
|
||||
)
|
||||
return parser
|
||||
|
||||
def take_action(self, 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)
|
||||
|
||||
def _show_snapshot(self, heat_client, stack_id, snapshot_id):
|
||||
try:
|
||||
data = heat_client.stacks.snapshot_show(stack_id, snapshot_id)
|
||||
except heat_exc.HTTPNotFound:
|
||||
raise exc.CommandError(_('Snapshot ID <%(snapshot_id)s> not found '
|
||||
'for stack <%(stack_id)s>')
|
||||
% {'snapshot_id': snapshot_id,
|
||||
'stack_id': stack_id})
|
||||
|
||||
rows = list(six.itervalues(data))
|
||||
columns = list(six.iterkeys(data))
|
||||
return columns, rows
|
||||
|
||||
|
||||
class RestoreSnapshot(command.Command):
|
||||
"""Restore stack snapshot"""
|
||||
|
||||
log = logging.getLogger(__name__ + ".RestoreSnapshot")
|
||||
|
||||
def get_parser(self, prog_name):
|
||||
parser = super(RestoreSnapshot, self).get_parser(prog_name)
|
||||
parser.add_argument(
|
||||
'stack',
|
||||
metavar='<stack>',
|
||||
help=_('Name or ID of stack containing the snapshot')
|
||||
)
|
||||
parser.add_argument(
|
||||
'snapshot',
|
||||
metavar='<snapshot>',
|
||||
help=_('ID of the snapshot to restore')
|
||||
)
|
||||
return parser
|
||||
|
||||
def take_action(self, 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)
|
||||
|
||||
def _restore_snapshot(self, heat_client, parsed_args):
|
||||
fields = {'stack_id': parsed_args.stack,
|
||||
'snapshot_id': parsed_args.snapshot}
|
||||
try:
|
||||
heat_client.stacks.restore(**fields)
|
||||
except heat_exc.HTTPNotFound:
|
||||
raise exc.CommandError(_('Stack %(stack)s or '
|
||||
'snapshot %(snapshot)s not found.') %
|
||||
{'stack': parsed_args.stack,
|
||||
'snapshot': parsed_args.snapshot})
|
||||
|
||||
|
||||
class CreateSnapshot(command.ShowOne):
|
||||
"""Create stack snapshot."""
|
||||
|
||||
log = logging.getLogger(__name__ + ".CreateSnapshot")
|
||||
|
||||
def get_parser(self, prog_name):
|
||||
parser = super(CreateSnapshot, self).get_parser(prog_name)
|
||||
parser.add_argument(
|
||||
'stack',
|
||||
metavar='<stack>',
|
||||
help=_('Name or ID of stack')
|
||||
)
|
||||
parser.add_argument(
|
||||
'--name',
|
||||
metavar='<name>',
|
||||
help=_('Name of snapshot')
|
||||
)
|
||||
return parser
|
||||
|
||||
def take_action(self, parsed_args):
|
||||
self.log.debug('take_action(%s)', parsed_args)
|
||||
heat_client = self.app.client_manager.orchestration
|
||||
|
||||
try:
|
||||
data = heat_client.stacks.snapshot(parsed_args.stack,
|
||||
parsed_args.name)
|
||||
except heat_exc.HTTPNotFound:
|
||||
raise exc.CommandError(_('Stack not found: %s')
|
||||
% parsed_args.stack)
|
||||
|
||||
columns = [
|
||||
'ID',
|
||||
'name',
|
||||
'status',
|
||||
'status_reason',
|
||||
'data',
|
||||
'creation_time'
|
||||
]
|
||||
return (columns, utils.get_dict_properties(data, columns))
|
||||
|
||||
|
||||
class DeleteSnapshot(command.Command):
|
||||
"""Delete stack snapshot."""
|
||||
log = logging.getLogger(__name__ + ".DeleteSnapshot")
|
||||
|
||||
def get_parser(self, prog_name):
|
||||
parser = super(DeleteSnapshot, self).get_parser(prog_name)
|
||||
parser.add_argument(
|
||||
'stack',
|
||||
metavar='<stack>',
|
||||
help=_('Name or ID of stack')
|
||||
)
|
||||
parser.add_argument(
|
||||
'snapshot',
|
||||
metavar='<snapshot>',
|
||||
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)
|
||||
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)
|
||||
except heat_exc.HTTPNotFound:
|
||||
raise exc.CommandError(_('Snapshot ID <%(snapshot_id)s> not found '
|
||||
'for stack <%(stack_id)s>')
|
||||
% {'snapshot_id': parsed_args.snapshot,
|
||||
'stack_id': parsed_args.stack})
|
|
@ -1,240 +0,0 @@
|
|||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
#
|
||||
|
||||
"""Orchestration v1 software config action implementations"""
|
||||
|
||||
import logging
|
||||
|
||||
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
|
||||
import yaml
|
||||
|
||||
from heatclient._i18n import _
|
||||
from heatclient.common import format_utils
|
||||
from heatclient.common import template_format
|
||||
from heatclient.common import utils as heat_utils
|
||||
from heatclient import exc as heat_exc
|
||||
|
||||
|
||||
class DeleteConfig(command.Command):
|
||||
"""Delete software configs"""
|
||||
|
||||
log = logging.getLogger(__name__ + ".DeleteConfig")
|
||||
|
||||
def get_parser(self, prog_name):
|
||||
parser = super(DeleteConfig, self).get_parser(prog_name)
|
||||
parser.add_argument(
|
||||
'config',
|
||||
metavar='<config>',
|
||||
nargs='+',
|
||||
help=_('IDs of the software configs to delete')
|
||||
)
|
||||
return parser
|
||||
|
||||
def take_action(self, parsed_args):
|
||||
self.log.debug("take_action(%s)", parsed_args)
|
||||
|
||||
heat_client = self.app.client_manager.orchestration
|
||||
return _delete_config(heat_client, parsed_args)
|
||||
|
||||
|
||||
def _delete_config(heat_client, args):
|
||||
failure_count = 0
|
||||
|
||||
for config_id in args.config:
|
||||
try:
|
||||
heat_client.software_configs.delete(
|
||||
config_id=config_id)
|
||||
except Exception as e:
|
||||
if isinstance(e, heat_exc.HTTPNotFound):
|
||||
print(_('Software config with ID %s not found') % config_id)
|
||||
failure_count += 1
|
||||
continue
|
||||
|
||||
if failure_count:
|
||||
raise exc.CommandError(_('Unable to delete %(count)s of the '
|
||||
'%(total)s software configs.') %
|
||||
{'count': failure_count,
|
||||
'total': len(args.config)})
|
||||
|
||||
|
||||
class ListConfig(command.Lister):
|
||||
"""List software configs"""
|
||||
|
||||
log = logging.getLogger(__name__ + ".ListConfig")
|
||||
|
||||
def get_parser(self, prog_name):
|
||||
parser = super(ListConfig, self).get_parser(prog_name)
|
||||
parser.add_argument(
|
||||
'--limit',
|
||||
metavar='<limit>',
|
||||
help=_('Limit the number of configs returned')
|
||||
)
|
||||
parser.add_argument(
|
||||
'--marker',
|
||||
metavar='<id>',
|
||||
help=_('Return configs that appear after the given config ID')
|
||||
)
|
||||
return parser
|
||||
|
||||
def take_action(self, parsed_args):
|
||||
self.log.debug("take_action(%s)", parsed_args)
|
||||
heat_client = self.app.client_manager.orchestration
|
||||
return _list_config(heat_client, parsed_args)
|
||||
|
||||
|
||||
def _list_config(heat_client, args):
|
||||
kwargs = {}
|
||||
if args.limit:
|
||||
kwargs['limit'] = args.limit
|
||||
if args.marker:
|
||||
kwargs['marker'] = args.marker
|
||||
scs = heat_client.software_configs.list(**kwargs)
|
||||
|
||||
columns = ['id', 'name', 'group', 'creation_time']
|
||||
return (columns, (utils.get_item_properties(s, columns) for s in scs))
|
||||
|
||||
|
||||
class CreateConfig(format_utils.JsonFormat):
|
||||
"""Create software config"""
|
||||
|
||||
log = logging.getLogger(__name__ + ".CreateConfig")
|
||||
|
||||
def get_parser(self, prog_name):
|
||||
parser = super(CreateConfig, self).get_parser(prog_name)
|
||||
parser.add_argument(
|
||||
'name',
|
||||
metavar='<config-name>',
|
||||
help=_('Name of the software config to create')
|
||||
)
|
||||
parser.add_argument(
|
||||
'--config-file',
|
||||
metavar='<config-file>',
|
||||
help=_('Path to JSON/YAML containing map defining '
|
||||
'<inputs>, <outputs>, and <options>')
|
||||
)
|
||||
parser.add_argument(
|
||||
'--definition-file',
|
||||
metavar='<destination-file>',
|
||||
help=_('Path to software config script/data')
|
||||
)
|
||||
parser.add_argument(
|
||||
'--group',
|
||||
metavar='<group>',
|
||||
default='Heat::Ungrouped',
|
||||
help=_('Group name of tool expected by the software config')
|
||||
)
|
||||
return parser
|
||||
|
||||
def take_action(self, parsed_args):
|
||||
self.log.debug("take_action(%s)", parsed_args)
|
||||
heat_client = self.app.client_manager.orchestration
|
||||
return _create_config(heat_client, parsed_args)
|
||||
|
||||
|
||||
def _create_config(heat_client, args):
|
||||
config = {
|
||||
'group': args.group,
|
||||
'config': ''
|
||||
}
|
||||
|
||||
defn = {}
|
||||
if args.definition_file:
|
||||
defn_url = heat_utils.normalise_file_path_to_url(
|
||||
args.definition_file)
|
||||
defn_raw = request.urlopen(defn_url).read() or '{}'
|
||||
defn = yaml.load(defn_raw, Loader=template_format.yaml_loader)
|
||||
|
||||
config['inputs'] = defn.get('inputs', [])
|
||||
config['outputs'] = defn.get('outputs', [])
|
||||
config['options'] = defn.get('options', {})
|
||||
|
||||
if args.config_file:
|
||||
config_url = heat_utils.normalise_file_path_to_url(
|
||||
args.config_file)
|
||||
config['config'] = request.urlopen(config_url).read()
|
||||
|
||||
# build a mini-template with a config resource and validate it
|
||||
validate_template = {
|
||||
'heat_template_version': '2013-05-23',
|
||||
'resources': {
|
||||
args.name: {
|
||||
'type': 'OS::Heat::SoftwareConfig',
|
||||
'properties': config
|
||||
}
|
||||
}
|
||||
}
|
||||
heat_client.stacks.validate(template=validate_template)
|
||||
|
||||
config['name'] = args.name
|
||||
sc = heat_client.software_configs.create(**config).to_dict()
|
||||
rows = list(six.itervalues(sc))
|
||||
columns = list(six.iterkeys(sc))
|
||||
return columns, rows
|
||||
|
||||
|
||||
class ShowConfig(format_utils.YamlFormat):
|
||||
"""Show software config details"""
|
||||
|
||||
log = logging.getLogger(__name__ + ".ShowConfig")
|
||||
|
||||
def get_parser(self, prog_name):
|
||||
parser = super(ShowConfig, self).get_parser(prog_name)
|
||||
parser.add_argument(
|
||||
'config',
|
||||
metavar='<config>',
|
||||
help=_('ID of the config')
|
||||
)
|
||||
parser.add_argument(
|
||||
'--config-only',
|
||||
default=False,
|
||||
action="store_true",
|
||||
help=_('Only display the value of the <config> property.')
|
||||
)
|
||||
return parser
|
||||
|
||||
def take_action(self, parsed_args):
|
||||
self.log.debug("take_action(%s)", parsed_args)
|
||||
heat_client = self.app.client_manager.orchestration
|
||||
return _show_config(heat_client, config_id=parsed_args.config,
|
||||
config_only=parsed_args.config_only)
|
||||
|
||||
|
||||
def _show_config(heat_client, config_id, config_only):
|
||||
try:
|
||||
sc = heat_client.software_configs.get(config_id=config_id)
|
||||
except heat_exc.HTTPNotFound:
|
||||
raise exc.CommandError(_('Configuration not found: %s') % config_id)
|
||||
|
||||
columns = None
|
||||
rows = None
|
||||
|
||||
if config_only:
|
||||
print(sc.config)
|
||||
else:
|
||||
columns = (
|
||||
'id',
|
||||
'name',
|
||||
'group',
|
||||
'config',
|
||||
'inputs',
|
||||
'outputs',
|
||||
'options',
|
||||
'creation_time',
|
||||
)
|
||||
rows = utils.get_dict_properties(sc.to_dict(), columns)
|
||||
|
||||
return columns, rows
|
|
@ -1,356 +0,0 @@
|
|||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
#
|
||||
|
||||
"""Orchestration v1 Software Deployment action implementations"""
|
||||
|
||||
import logging
|
||||
|
||||
from osc_lib.command import command
|
||||
from osc_lib import exceptions as exc
|
||||
from osc_lib import utils
|
||||
from oslo_serialization import jsonutils
|
||||
|
||||
from heatclient._i18n import _
|
||||
from heatclient.common import deployment_utils
|
||||
from heatclient.common import format_utils
|
||||
from heatclient.common import utils as heat_utils
|
||||
from heatclient import exc as heat_exc
|
||||
|
||||
|
||||
class CreateDeployment(format_utils.YamlFormat):
|
||||
"""Create a software deployment."""
|
||||
|
||||
log = logging.getLogger(__name__ + '.CreateDeployment')
|
||||
|
||||
def get_parser(self, prog_name):
|
||||
parser = super(CreateDeployment, self).get_parser(prog_name)
|
||||
parser.add_argument(
|
||||
'name',
|
||||
metavar='<deployment-name>',
|
||||
help=_('Name of the derived config associated with this '
|
||||
'deployment. This is used to apply a sort order to the '
|
||||
'list of configurations currently deployed to the server.')
|
||||
)
|
||||
parser.add_argument(
|
||||
'--input-value',
|
||||
metavar='<key=value>',
|
||||
action='append',
|
||||
help=_('Input value to set on the deployment. This can be '
|
||||
'specified multiple times.')
|
||||
)
|
||||
parser.add_argument(
|
||||
'--action',
|
||||
metavar='<action>',
|
||||
default='UPDATE',
|
||||
help=_('Name of an action for this deployment. This can be a '
|
||||
'custom action, or one of CREATE, UPDATE, DELETE, SUSPEND, '
|
||||
'RESUME. Default is UPDATE')
|
||||
)
|
||||
parser.add_argument(
|
||||
'--config',
|
||||
metavar='<config>',
|
||||
help=_('ID of the configuration to deploy')
|
||||
)
|
||||
parser.add_argument(
|
||||
'--signal-transport',
|
||||
metavar='<signal-transport>',
|
||||
default='TEMP_URL_SIGNAL',
|
||||
help=_('How the server should signal to heat with the deployment '
|
||||
'output values. TEMP_URL_SIGNAL will create a Swift '
|
||||
'TempURL to be signaled via HTTP PUT. ZAQAR_SIGNAL will '
|
||||
'create a dedicated zaqar queue to be signaled using the '
|
||||
'provided keystone credentials.NO_SIGNAL will result in '
|
||||
'the resource going to the COMPLETE state without waiting '
|
||||
'for any signal')
|
||||
)
|
||||
parser.add_argument(
|
||||
'--container',
|
||||
metavar='<container>',
|
||||
help=_('Optional name of container to store TEMP_URL_SIGNAL '
|
||||
'objects in. If not specified a container will be created '
|
||||
'with a name derived from the DEPLOY_NAME')
|
||||
)
|
||||
parser.add_argument(
|
||||
'--timeout',
|
||||
metavar='<timeout>',
|
||||
type=int,
|
||||
default=60,
|
||||
help=_('Deployment timeout in minutes')
|
||||
)
|
||||
parser.add_argument(
|
||||
'--server',
|
||||
metavar='<server>',
|
||||
required=True,
|
||||
help=_('ID of the server being deployed to')
|
||||
)
|
||||
return parser
|
||||
|
||||
def take_action(self, parsed_args):
|
||||
self.log.debug('take_action(%s)', parsed_args)
|
||||
|
||||
client = self.app.client_manager.orchestration
|
||||
|
||||
config = {}
|
||||
if parsed_args.config:
|
||||
try:
|
||||
config = client.software_configs.get(parsed_args.config)
|
||||
except heat_exc.HTTPNotFound:
|
||||
msg = (_('Software configuration not found: %s') %
|
||||
parsed_args.config)
|
||||
raise exc.CommandError(msg)
|
||||
|
||||
derived_params = deployment_utils.build_derived_config_params(
|
||||
parsed_args.action,
|
||||
config,
|
||||
parsed_args.name,
|
||||
heat_utils.format_parameters(parsed_args.input_value, False),
|
||||
parsed_args.server,
|
||||
parsed_args.signal_transport,
|
||||
signal_id=deployment_utils.build_signal_id(client, parsed_args)
|
||||
)
|
||||
derived_config = client.software_configs.create(**derived_params)
|
||||
|
||||
sd = client.software_deployments.create(
|
||||
config_id=derived_config.id,
|
||||
server_id=parsed_args.server,
|
||||
action=parsed_args.action,
|
||||
status='IN_PROGRESS'
|
||||
)
|
||||
|
||||
return zip(*sorted(sd.to_dict().items()))
|
||||
|
||||
|
||||
class DeleteDeployment(command.Command):
|
||||
"""Delete software deployment(s) and correlative config(s)."""
|
||||
|
||||
log = logging.getLogger(__name__ + '.DeleteDeployment')
|
||||
|
||||
def get_parser(self, prog_name):
|
||||
parser = super(DeleteDeployment, self).get_parser(prog_name)
|
||||
parser.add_argument(
|
||||
'deployment',
|
||||
metavar='<deployment>',
|
||||
nargs='+',
|
||||
help=_('ID of the deployment(s) to delete.')
|
||||
)
|
||||
return parser
|
||||
|
||||
def take_action(self, parsed_args):
|
||||
self.log.debug("take_action(%s)", parsed_args)
|
||||
hc = self.app.client_manager.orchestration
|
||||
failure_count = 0
|
||||
|
||||
for deploy_id in parsed_args.deployment:
|
||||
try:
|
||||
sd = hc.software_deployments.get(deployment_id=deploy_id)
|
||||
hc.software_deployments.delete(
|
||||
deployment_id=deploy_id)
|
||||
except Exception as e:
|
||||
if isinstance(e, heat_exc.HTTPNotFound):
|
||||
print(_('Deployment with ID %s not found') % deploy_id)
|
||||
else:
|
||||
print(_('Deployment with ID %s failed to delete')
|
||||
% deploy_id)
|
||||
failure_count += 1
|
||||
continue
|
||||
# just try best to delete the corresponding config
|
||||
try:
|
||||
config_id = getattr(sd, 'config_id')
|
||||
hc.software_configs.delete(config_id=config_id)
|
||||
except Exception:
|
||||
print(_('Failed to delete the correlative config'
|
||||
' %(config_id)s of deployment %(deploy_id)s') %
|
||||
{'config_id': config_id, 'deploy_id': deploy_id})
|
||||
|
||||
if failure_count:
|
||||
raise exc.CommandError(_('Unable to delete %(count)s of the '
|
||||
'%(total)s deployments.') %
|
||||
{'count': failure_count,
|
||||
'total': len(parsed_args.deployment)})
|
||||
|
||||
|
||||
class ListDeployment(command.Lister):
|
||||
"""List software deployments."""
|
||||
|
||||
log = logging.getLogger(__name__ + '.ListDeployment')
|
||||
|
||||
def get_parser(self, prog_name):
|
||||
parser = super(ListDeployment, self).get_parser(prog_name)
|
||||
parser.add_argument(
|
||||
'--server',
|
||||
metavar='<server>',
|
||||
help=_('ID of the server to fetch deployments for')
|
||||
)
|
||||
parser.add_argument(
|
||||
'--long',
|
||||
action='store_true',
|
||||
help=_('List more fields in output')
|
||||
)
|
||||
return parser
|
||||
|
||||
def take_action(self, parsed_args):
|
||||
self.log.debug("take_action(%s)", parsed_args)
|
||||
|
||||
heat_client = self.app.client_manager.orchestration
|
||||
return _list_deployment(heat_client, args=parsed_args)
|
||||
|
||||
|
||||
def _list_deployment(heat_client, args=None):
|
||||
kwargs = {'server_id': args.server} if args.server else {}
|
||||
columns = ['id', 'config_id', 'server_id', 'action', 'status']
|
||||
if args.long:
|
||||
columns.append('creation_time')
|
||||
columns.append('status_reason')
|
||||
|
||||
deployments = heat_client.software_deployments.list(**kwargs)
|
||||
return (
|
||||
columns,
|
||||
(utils.get_item_properties(s, columns) for s in deployments)
|
||||
)
|
||||
|
||||
|
||||
class ShowDeployment(command.ShowOne):
|
||||
"""Show SoftwareDeployment Details."""
|
||||
|
||||
log = logging.getLogger(__name__ + ".ShowSoftwareDeployment")
|
||||
|
||||
def get_parser(self, prog_name):
|
||||
parser = super(ShowDeployment, self).get_parser(prog_name)
|
||||
parser.add_argument(
|
||||
'deployment',
|
||||
metavar='<deployment>',
|
||||
help=_('ID of the deployment')
|
||||
)
|
||||
parser.add_argument(
|
||||
'--long',
|
||||
action='store_true',
|
||||
help=_('Show more fields in output')
|
||||
)
|
||||
return parser
|
||||
|
||||
def take_action(self, parsed_args):
|
||||
self.log.debug("take_action(%s)", parsed_args)
|
||||
|
||||
heat_client = self.app.client_manager.orchestration
|
||||
try:
|
||||
data = heat_client.software_deployments.get(
|
||||
deployment_id=parsed_args.deployment)
|
||||
except heat_exc.HTTPNotFound:
|
||||
raise exc.CommandError(
|
||||
_('Software Deployment not found: %s')
|
||||
% parsed_args.deployment)
|
||||
else:
|
||||
columns = [
|
||||
'id',
|
||||
'server_id',
|
||||
'config_id',
|
||||
'creation_time',
|
||||
'updated_time',
|
||||
'status',
|
||||
'status_reason',
|
||||
'input_values',
|
||||
'action',
|
||||
]
|
||||
if parsed_args.long:
|
||||
columns.append('output_values')
|
||||
return columns, utils.get_item_properties(data, columns)
|
||||
|
||||
|
||||
class ShowMetadataDeployment(command.Command):
|
||||
"""Get deployment configuration metadata for the specified server."""
|
||||
|
||||
log = logging.getLogger(__name__ + '.ShowMetadataDeployment')
|
||||
|
||||
def get_parser(self, prog_name):
|
||||
parser = super(ShowMetadataDeployment, self).get_parser(prog_name)
|
||||
parser.add_argument(
|
||||
'server',
|
||||
metavar='<server>',
|
||||
help=_('ID of the server to fetch deployments for')
|
||||
)
|
||||
return parser
|
||||
|
||||
def take_action(self, parsed_args):
|
||||
self.log.debug("take_action(%s)", parsed_args)
|
||||
heat_client = self.app.client_manager.orchestration
|
||||
md = heat_client.software_deployments.metadata(
|
||||
server_id=parsed_args.server)
|
||||
print(jsonutils.dumps(md, indent=2))
|
||||
|
||||
|
||||
class ShowOutputDeployment(command.Command):
|
||||
"""Show a specific deployment output."""
|
||||
|
||||
log = logging.getLogger(__name__ + '.ShowOutputDeployment')
|
||||
|
||||
def get_parser(self, prog_name):
|
||||
parser = super(ShowOutputDeployment, self).get_parser(prog_name)
|
||||
parser.add_argument(
|
||||
'deployment',
|
||||
metavar='<deployment>',
|
||||
help=_('ID of deployment to show the output for')
|
||||
)
|
||||
parser.add_argument(
|
||||
'output',
|
||||
metavar='<output-name>',
|
||||
nargs='?',
|
||||
default=None,
|
||||
help=_('Name of an output to display')
|
||||
)
|
||||
parser.add_argument(
|
||||
'--all',
|
||||
default=False,
|
||||
action='store_true',
|
||||
help=_('Display all deployment outputs')
|
||||
)
|
||||
parser.add_argument(
|
||||
'--long',
|
||||
action='store_true',
|
||||
default=False,
|
||||
help='Show full deployment logs in output',
|
||||
)
|
||||
return parser
|
||||
|
||||
def take_action(self, parsed_args):
|
||||
self.log.debug("take_action(%s)", parsed_args)
|
||||
|
||||
heat_client = self.app.client_manager.orchestration
|
||||
if (not parsed_args.all and parsed_args.output is None or
|
||||
parsed_args.all and parsed_args.output is not None):
|
||||
raise exc.CommandError(
|
||||
_('Error: either %(output)s or %(all)s argument is needed.')
|
||||
% {'output': '<output-name>', 'all': '--all'})
|
||||
try:
|
||||
sd = heat_client.software_deployments.get(
|
||||
deployment_id=parsed_args.deployment)
|
||||
except heat_exc.HTTPNotFound:
|
||||
raise exc.CommandError(_('Deployment not found: %s')
|
||||
% parsed_args.deployment)
|
||||
outputs = sd.output_values
|
||||
if outputs:
|
||||
if parsed_args.all:
|
||||
print('output_values:\n')
|
||||
for k in outputs.keys():
|
||||
format_utils.print_software_deployment_output(
|
||||
data=outputs, name=k, long=parsed_args.long)
|
||||
else:
|
||||
if parsed_args.output not in outputs:
|
||||
msg = (_('Output %(output)s does not exist in deployment'
|
||||
' %(deployment)s')
|
||||
% {'output': parsed_args.output,
|
||||
'deployment': parsed_args.deployment})
|
||||
raise exc.CommandError(msg)
|
||||
else:
|
||||
print('output_value:\n')
|
||||
format_utils.print_software_deployment_output(
|
||||
data=outputs, name=parsed_args.output)
|
File diff suppressed because it is too large
Load Diff
|
@ -1,136 +0,0 @@
|
|||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
#
|
||||
|
||||
import collections
|
||||
|
||||
from osc_lib.command import command
|
||||
|
||||
from heatclient._i18n import _
|
||||
from heatclient.common import format_utils
|
||||
from heatclient import exc
|
||||
|
||||
|
||||
class ListStackFailures(command.Command):
|
||||
"""Show information about failed stack resources."""
|
||||
|
||||
def take_action(self, parsed_args):
|
||||
self.heat_client = self.app.client_manager.orchestration
|
||||
failures = self._build_failed_resources(parsed_args.stack)
|
||||
deployment_failures = self._build_software_deployments(failures)
|
||||
self._print_failures(failures, deployment_failures,
|
||||
long=parsed_args.long)
|
||||
|
||||
def get_parser(self, prog_name):
|
||||
parser = super(ListStackFailures, self).get_parser(prog_name)
|
||||
parser.add_argument(
|
||||
'stack',
|
||||
metavar='<stack>',
|
||||
help=_('Stack to display (name or ID)'),
|
||||
)
|
||||
parser.add_argument(
|
||||
'--long',
|
||||
action='store_true',
|
||||
default=False,
|
||||
help=_('Show full deployment logs in output'),
|
||||
)
|
||||
return parser
|
||||
|
||||
def _build_failed_resources(self, stack):
|
||||
"""List information about FAILED stack resources.
|
||||
|
||||
Failed resources are added by recursing from the top level stack into
|
||||
failed nested stack resources. A failed nested stack resource is only
|
||||
added to the failed list if it contains no failed resources.
|
||||
"""
|
||||
s = self.heat_client.stacks.get(stack)
|
||||
if s.status != 'FAILED':
|
||||
return []
|
||||
resources = self.heat_client.resources.list(s.id)
|
||||
failures = collections.OrderedDict()
|
||||
self._append_failed_resources(failures, resources, [s.stack_name])
|
||||
return failures
|
||||
|
||||
def _append_failed_resources(self, failures, resources, resource_path):
|
||||
"""Recursively build list of failed resources."""
|
||||
appended = False
|
||||
for r in resources:
|
||||
if not r.resource_status.endswith('FAILED'):
|
||||
continue
|
||||
# determine if this resources is a nested stack
|
||||
links_rel = list([l['rel'] for l in r.links])
|
||||
is_nested = 'nested' in links_rel
|
||||
nested_appended = False
|
||||
next_resource_path = list(resource_path)
|
||||
next_resource_path.append(r.resource_name)
|
||||
if is_nested:
|
||||
try:
|
||||
nested_resources = self.heat_client.resources.list(
|
||||
r.physical_resource_id)
|
||||
nested_appended = self._append_failed_resources(
|
||||
failures, nested_resources, next_resource_path)
|
||||
except exc.HTTPNotFound:
|
||||
# there is a failed resource but no stack
|
||||
pass
|
||||
if not nested_appended:
|
||||
failures['.'.join(next_resource_path)] = r
|
||||
appended = True
|
||||
return appended
|
||||
|
||||
def _build_software_deployments(self, resources):
|
||||
"""Build a dict of software deployments from the supplied resources.
|
||||
|
||||
The key is the deployment ID.
|
||||
"""
|
||||
df = {}
|
||||
if not resources:
|
||||
return df
|
||||
for r in resources.values():
|
||||
if r.resource_type not in ('OS::Heat::StructuredDeployment',
|
||||
'OS::Heat::SoftwareDeployment'):
|
||||
continue
|
||||
try:
|
||||
sd = self.heat_client.software_deployments.get(
|
||||
deployment_id=r.physical_resource_id)
|
||||
df[r.physical_resource_id] = sd
|
||||
except exc.HTTPNotFound:
|
||||
pass
|
||||
return df
|
||||
|
||||
def _print_failures(self, failures, deployment_failures, long=False):
|
||||
"""Print failed resources.
|
||||
|
||||
If the resource is a deployment resource, look up the deployment and
|
||||
print deploy_stdout and deploy_stderr.
|
||||
"""
|
||||
out = self.app.stdout
|
||||
|
||||
if not failures:
|
||||
return
|
||||
for k, f in failures.items():
|
||||
out.write('%s:\n' % k)
|
||||
out.write(' resource_type: %s\n' % f.resource_type)
|
||||
out.write(' physical_resource_id: %s\n' %
|
||||
f.physical_resource_id)
|
||||
out.write(' status: %s\n' % f.resource_status)
|
||||
reason = format_utils.indent_and_truncate(
|
||||
f.resource_status_reason,
|
||||
spaces=4,
|
||||
truncate=not long,
|
||||
truncate_prefix='...\n')
|
||||
out.write(' status_reason: |\n%s\n' % reason)
|
||||
df = deployment_failures.get(f.physical_resource_id)
|
||||
if df:
|
||||
for output in ('deploy_stdout', 'deploy_stderr'):
|
||||
format_utils.print_software_deployment_output(
|
||||
data=df.output_values, name=output, long=long, out=out)
|
|
@ -1,174 +0,0 @@
|
|||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
#
|
||||
# Copyright 2015 IBM Corp.
|
||||
|
||||
import logging
|
||||
|
||||
from osc_lib.command import command
|
||||
from osc_lib import utils
|
||||
import six
|
||||
|
||||
from heatclient._i18n import _
|
||||
from heatclient.common import format_utils
|
||||
from heatclient.common import http
|
||||
from heatclient.common import template_utils
|
||||
from heatclient.common import utils as heat_utils
|
||||
from heatclient import exc
|
||||
|
||||
|
||||
class VersionList(command.Lister):
|
||||
"""List the available template versions."""
|
||||
|
||||
log = logging.getLogger(__name__ + '.VersionList')
|
||||
|
||||
def take_action(self, parsed_args):
|
||||
self.log.debug('take_action(%s)', parsed_args)
|
||||
|
||||
client = self.app.client_manager.orchestration
|
||||
|
||||
versions = client.template_versions.list()
|
||||
try:
|
||||
versions[1].aliases
|
||||
|
||||
def format_alias(aliases):
|
||||
return ','.join(aliases)
|
||||
|
||||
fields = ['Version', 'Type', 'Aliases']
|
||||
formatters = {'Aliases': format_alias}
|
||||
except AttributeError:
|
||||
fields = ['Version', 'Type']
|
||||
formatters = None
|
||||
|
||||
items = (utils.get_item_properties(s, fields,
|
||||
formatters=formatters)
|
||||
for s in versions)
|
||||
|
||||
return (fields, items)
|
||||
|
||||
|
||||
class FunctionList(command.Lister):
|
||||
"""List the available functions."""
|
||||
|
||||
log = logging.getLogger(__name__ + '.FunctionList')
|
||||
|
||||
def get_parser(self, prog_name):
|
||||
parser = super(FunctionList, self).get_parser(prog_name)
|
||||
parser.add_argument(
|
||||
'template_version',
|
||||
metavar='<template-version>',
|
||||
help=_('Template version to get the functions for')
|
||||
)
|
||||
parser.add_argument(
|
||||
'--with_conditions',
|
||||
default=False,
|
||||
action='store_true',
|
||||
help=_('Show condition functions for template.')
|
||||
)
|
||||
|
||||
return parser
|
||||
|
||||
def take_action(self, parsed_args):
|
||||
self.log.debug('take_action(%s)', parsed_args)
|
||||
|
||||
client = self.app.client_manager.orchestration
|
||||
|
||||
version = parsed_args.template_version
|
||||
try:
|
||||
functions = client.template_versions.get(
|
||||
version, with_condition_func=parsed_args.with_conditions)
|
||||
except exc.HTTPNotFound:
|
||||
msg = _('Template version not found: %s') % version
|
||||
raise exc.CommandError(msg)
|
||||
|
||||
fields = ['Functions', 'Description']
|
||||
return (
|
||||
fields,
|
||||
(utils.get_item_properties(s, fields) for s in functions)
|
||||
)
|
||||
|
||||
|
||||
class Validate(format_utils.YamlFormat):
|
||||
"""Validate a template"""
|
||||
|
||||
log = logging.getLogger(__name__ + ".Validate")
|
||||
|
||||
def get_parser(self, prog_name):
|
||||
parser = super(Validate, self).get_parser(prog_name)
|
||||
parser.add_argument(
|
||||
'-e', '--environment',
|
||||
metavar='<environment>',
|
||||
action='append',
|
||||
help=_('Path to the environment. Can be specified multiple times')
|
||||
)
|
||||
parser.add_argument(
|
||||
'--show-nested',
|
||||
action='store_true',
|
||||
help=_('Resolve parameters from nested templates as well')
|
||||
)
|
||||
parser.add_argument(
|
||||
'--parameter',
|
||||
metavar='<key=value>',
|
||||
action='append',
|
||||
help=_('Parameter values used to create the stack. This can be '
|
||||
'specified multiple times')
|
||||
)
|
||||
parser.add_argument(
|
||||
'--ignore-errors',
|
||||
metavar='<error1,error2,...>',
|
||||
help=_('List of heat errors to ignore')
|
||||
)
|
||||
parser.add_argument(
|
||||
'-t', '--template',
|
||||
metavar='<template>',
|
||||
required=True,
|
||||
help=_('Path to the template')
|
||||
)
|
||||
return parser
|
||||
|
||||
def take_action(self, parsed_args):
|
||||
self.log.debug("take_action(%s)", parsed_args)
|
||||
|
||||
heat_client = self.app.client_manager.orchestration
|
||||
return _validate(heat_client, parsed_args)
|
||||
|
||||
|
||||
def _validate(heat_client, args):
|
||||
tpl_files, template = template_utils.process_template_path(
|
||||
args.template,
|
||||
object_request=http.authenticated_fetcher(heat_client))
|
||||
|
||||
env_files_list = []
|
||||
env_files, env = template_utils.process_multiple_environments_and_files(
|
||||
env_paths=args.environment, env_list_tracker=env_files_list)
|
||||
|
||||
fields = {
|
||||
'template': template,
|
||||
'parameters': heat_utils.format_parameters(args.parameter),
|
||||
'files': dict(list(tpl_files.items()) + list(env_files.items())),
|
||||
'environment': env,
|
||||
}
|
||||
|
||||
if args.ignore_errors:
|
||||
fields['ignore_errors'] = args.ignore_errors
|
||||
|
||||
# If one or more environments is found, pass the listing to the server
|
||||
if env_files_list:
|
||||
fields['environment_files'] = env_files_list
|
||||
|
||||
if args.show_nested:
|
||||
fields['show_nested'] = args.show_nested
|
||||
|
||||
validation = heat_client.stacks.validate(**fields)
|
||||
data = list(six.itervalues(validation))
|
||||
columns = list(six.iterkeys(validation))
|
||||
return columns, data
|
|
@ -1,619 +0,0 @@
|
|||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
"""
|
||||
Command-line interface to the Heat API.
|
||||
"""
|
||||
|
||||
from __future__ import print_function
|
||||
|
||||
import argparse
|
||||
import logging
|
||||
import sys
|
||||
|
||||
from keystoneauth1.identity import generic
|
||||
from keystoneauth1 import session as kssession
|
||||
from oslo_utils import encodeutils
|
||||
from oslo_utils import importutils
|
||||
import six
|
||||
|
||||
import heatclient
|
||||
from heatclient._i18n import _
|
||||
from heatclient import client as heat_client
|
||||
from heatclient.common import utils
|
||||
from heatclient import exc
|
||||
|
||||
osprofiler_profiler = importutils.try_import("osprofiler.profiler")
|
||||
|
||||
|
||||
class HeatShell(object):
|
||||
|
||||
def _append_global_identity_args(self, parser):
|
||||
# FIXME(gyee): these are global identity (Keystone) arguments which
|
||||
# should be consistent and shared by all service clients. Therefore,
|
||||
# they should be provided by python-keystoneclient. We will need to
|
||||
# refactor this code once this functionality is available in
|
||||
# python-keystoneclient.
|
||||
parser.add_argument(
|
||||
'-k', '--insecure', default=False, action='store_true',
|
||||
help=_('Explicitly allow heatclient to perform '
|
||||
'\"insecure SSL\" (https) requests. '
|
||||
'The server\'s certificate will not be verified '
|
||||
'against any certificate authorities. '
|
||||
'This option should be used with caution.'))
|
||||
|
||||
parser.add_argument(
|
||||
'--os-cert',
|
||||
default=utils.env('OS_CERT'),
|
||||
help=_('Path of certificate file to use in SSL connection. '
|
||||
'This file can optionally be prepended with '
|
||||
'the private key.'))
|
||||
|
||||
# for backward compatibility only
|
||||
parser.add_argument('--cert-file',
|
||||
dest='os_cert',
|
||||
help=_('DEPRECATED! Use %(arg)s.') %
|
||||
{'arg': '--os-cert'})
|
||||
|
||||
parser.add_argument('--os-key',
|
||||
default=utils.env('OS_KEY'),
|
||||
help=_('Path of client key to use in SSL '
|
||||
'connection. This option is not necessary '
|
||||
'if your key is prepended to your cert '
|
||||
'file.'))
|
||||
|
||||
parser.add_argument('--key-file',
|
||||
dest='os_key',
|
||||
help=_('DEPRECATED! Use %(arg)s.') %
|
||||
{'arg': '--os-key'})
|
||||
|
||||
parser.add_argument('--os-cacert',
|
||||
metavar='<ca-certificate-file>',
|
||||
dest='os_cacert',
|
||||
default=utils.env('OS_CACERT'),
|
||||
help=_('Path of CA TLS certificate(s) used to '
|
||||
'verify the remote server\'s certificate. '
|
||||
'Without this option glance looks for the '
|
||||
'default system CA certificates.'))
|
||||
|
||||
parser.add_argument('--ca-file',
|
||||
dest='os_cacert',
|
||||
help=_('DEPRECATED! Use %(arg)s.') %
|
||||
{'arg': '--os-cacert'})
|
||||
|
||||
parser.add_argument('--os-username',
|
||||
default=utils.env('OS_USERNAME'),
|
||||
help=_('Defaults to %(value)s.') % {
|
||||
'value': 'env[OS_USERNAME]'
|
||||
})
|
||||
|
||||
parser.add_argument('--os_username',
|
||||
help=argparse.SUPPRESS)
|
||||
|
||||
parser.add_argument('--os-user-id',
|
||||
default=utils.env('OS_USER_ID'),
|
||||
help=_('Defaults to %(value)s.') % {
|
||||
'value': 'env[OS_USER_ID]'
|
||||
})
|
||||
|
||||
parser.add_argument('--os_user_id',
|
||||
help=argparse.SUPPRESS)
|
||||
|
||||
parser.add_argument('--os-user-domain-id',
|
||||
default=utils.env('OS_USER_DOMAIN_ID'),
|
||||
help=_('Defaults to %(value)s.') % {
|
||||
'value': 'env[OS_USER_DOMAIN_ID]'
|
||||
})
|
||||
|
||||
parser.add_argument('--os_user_domain_id',
|
||||
help=argparse.SUPPRESS)
|
||||
|
||||
parser.add_argument('--os-user-domain-name',
|
||||
default=utils.env('OS_USER_DOMAIN_NAME'),
|
||||
help=_('Defaults to %(value)s.') % {
|
||||
'value': 'env[OS_USER_DOMAIN_NAME]'
|
||||
})
|
||||
|
||||
parser.add_argument('--os_user_domain_name',
|
||||
help=argparse.SUPPRESS)
|
||||
|
||||
parser.add_argument('--os-project-id',
|
||||
default=utils.env('OS_PROJECT_ID'),
|
||||
help=(_('Another way to specify tenant ID. '
|
||||
'This option is mutually exclusive with '
|
||||
'%(arg)s. Defaults to %(value)s.') %
|
||||
{
|
||||
'arg': '--os-tenant-id',
|
||||
'value': 'env[OS_PROJECT_ID]'}))
|
||||
|
||||
parser.add_argument('--os_project_id',
|
||||
help=argparse.SUPPRESS)
|
||||
|
||||
parser.add_argument('--os-project-name',
|
||||
default=utils.env('OS_PROJECT_NAME'),
|
||||
help=(_('Another way to specify tenant name. '
|
||||
'This option is mutually exclusive with '
|
||||
'%(arg)s. Defaults to %(value)s.') %
|
||||
{
|
||||
'arg': '--os-tenant-name',
|
||||
'value': 'env[OS_PROJECT_NAME]'}))
|
||||
|
||||
parser.add_argument('--os_project_name',
|
||||
help=argparse.SUPPRESS)
|
||||
|
||||
parser.add_argument('--os-project-domain-id',
|
||||
default=utils.env('OS_PROJECT_DOMAIN_ID'),
|
||||
help=_('Defaults to %(value)s.') % {
|
||||
'value': 'env[OS_PROJECT_DOMAIN_ID]'
|
||||
})
|
||||
|
||||
parser.add_argument('--os_project_domain_id',
|
||||
help=argparse.SUPPRESS)
|
||||
|
||||
parser.add_argument('--os-project-domain-name',
|
||||
default=utils.env('OS_PROJECT_DOMAIN_NAME'),
|
||||
help=_('Defaults to %(value)s.') % {
|
||||
'value': 'env[OS_PROJECT_DOMAIN_NAME]'
|
||||
})
|
||||
|
||||
parser.add_argument('--os_project_domain_name',
|
||||
help=argparse.SUPPRESS)
|
||||
|
||||
parser.add_argument('--os-password',
|
||||
default=utils.env('OS_PASSWORD'),
|
||||
help=_('Defaults to %(value)s.') % {
|
||||
'value': 'env[OS_PASSWORD]'
|
||||
})
|
||||
|
||||
parser.add_argument('--os_password',
|
||||
help=argparse.SUPPRESS)
|
||||
|
||||
parser.add_argument('--os-tenant-id',
|
||||
default=utils.env('OS_TENANT_ID'),
|
||||
help=_('Defaults to %(value)s.') % {
|
||||
'value': 'env[OS_TENANT_ID]'
|
||||
})
|
||||
|
||||
parser.add_argument('--os_tenant_id',
|
||||
default=utils.env('OS_TENANT_ID'),
|
||||
help=argparse.SUPPRESS)
|
||||
|
||||
parser.add_argument('--os-tenant-name',
|
||||
default=utils.env('OS_TENANT_NAME'),
|
||||
help=_('Defaults to %(value)s.') % {
|
||||
'value': 'env[OS_TENANT_NAME]'
|
||||
})
|
||||
|
||||
parser.add_argument('--os_tenant_name',
|
||||
default=utils.env('OS_TENANT_NAME'),
|
||||
help=argparse.SUPPRESS)
|
||||
|
||||
parser.add_argument('--os-auth-url',
|
||||
default=utils.env('OS_AUTH_URL'),
|
||||
help=_('Defaults to %(value)s.') % {
|
||||
'value': 'env[OS_AUTH_URL]'
|
||||
})
|
||||
|
||||
parser.add_argument('--os_auth_url',
|
||||
help=argparse.SUPPRESS)
|
||||
|
||||
parser.add_argument('--os-region-name',
|
||||
default=utils.env('OS_REGION_NAME'),
|
||||
help=_('Defaults to %(value)s.') % {
|
||||
'value': 'env[OS_REGION_NAME]'
|
||||
})
|
||||
|
||||
parser.add_argument('--os_region_name',
|
||||
help=argparse.SUPPRESS)
|
||||
|
||||
parser.add_argument('--os-auth-token',
|
||||
default=utils.env('OS_AUTH_TOKEN'),
|
||||
help=_('Defaults to %(value)s.') % {
|
||||
'value': 'env[OS_AUTH_TOKEN]'
|
||||
})
|
||||
|
||||
parser.add_argument('--os_auth_token',
|
||||
help=argparse.SUPPRESS)
|
||||
|
||||
parser.add_argument('--os-service-type',
|
||||
default=utils.env('OS_SERVICE_TYPE'),
|
||||
help=_('Defaults to %(value)s.') % {
|
||||
'value': 'env[OS_SERVICE_TYPE]'
|
||||
})
|
||||
|
||||
parser.add_argument('--os_service_type',
|
||||
help=argparse.SUPPRESS)
|
||||
|
||||
parser.add_argument('--os-endpoint-type',
|
||||
default=utils.env('OS_ENDPOINT_TYPE'),
|
||||
help=_('Defaults to %(value)s.') % {
|
||||
'value': 'env[OS_ENDPOINT_TYPE]'
|
||||
})
|
||||
|
||||
parser.add_argument('--os_endpoint_type',
|
||||
help=argparse.SUPPRESS)
|
||||
|
||||
def get_base_parser(self):
|
||||
parser = argparse.ArgumentParser(
|
||||
prog='heat',
|
||||
description=__doc__.strip(),
|
||||
epilog=_('See "%(arg)s" for help on a specific command.') % {
|
||||
'arg': 'heat help COMMAND'
|
||||
},
|
||||
add_help=False,
|
||||
formatter_class=HelpFormatter,
|
||||
)
|
||||
|
||||
# Global arguments
|
||||
parser.add_argument('-h', '--help',
|
||||
action='store_true',
|
||||
help=argparse.SUPPRESS)
|
||||
|
||||
parser.add_argument('--version',
|
||||
action='version',
|
||||
version=heatclient.__version__,
|
||||
help=_("Shows the client version and exits."))
|
||||
|
||||
parser.add_argument('-d', '--debug',
|
||||
default=bool(utils.env('HEATCLIENT_DEBUG')),
|
||||
action='store_true',
|
||||
help=_('Defaults to %(value)s.') % {
|
||||
'value': 'env[HEATCLIENT_DEBUG]'
|
||||
})
|
||||
|
||||
parser.add_argument('-v', '--verbose',
|
||||
default=False, action="store_true",
|
||||
help=_("Print more verbose output."))
|
||||
|
||||
parser.add_argument('--api-timeout',
|
||||
help=_('Number of seconds to wait for an '
|
||||
'API response, '
|
||||
'defaults to system socket timeout'))
|
||||
|
||||
# os-no-client-auth tells heatclient to use token, instead of
|
||||
# env[OS_AUTH_URL]
|
||||
parser.add_argument('--os-no-client-auth',
|
||||
default=utils.env('OS_NO_CLIENT_AUTH'),
|
||||
action='store_true',
|
||||
help=(_("Do not contact keystone for a token. "
|
||||
"Defaults to %(value)s.") %
|
||||
{'value': 'env[OS_NO_CLIENT_AUTH]'}))
|
||||
|
||||
parser.add_argument('--heat-url',
|
||||
default=utils.env('HEAT_URL'),
|
||||
help=_('Defaults to %(value)s.') % {
|
||||
'value': 'env[HEAT_URL]'
|
||||
})
|
||||
|
||||
parser.add_argument('--heat_url',
|
||||
help=argparse.SUPPRESS)
|
||||
|
||||
parser.add_argument('--heat-api-version',
|
||||
default=utils.env('HEAT_API_VERSION', default='1'),
|
||||
help=_('Defaults to %(value)s or 1.') % {
|
||||
'value': 'env[HEAT_API_VERSION]'
|
||||
})
|
||||
|
||||
parser.add_argument('--heat_api_version',
|
||||
help=argparse.SUPPRESS)
|
||||
|
||||
# This unused option should remain so that scripts that
|
||||
# use it do not break. It is suppressed so it will not
|
||||
# appear in the help.
|
||||
parser.add_argument('-t', '--token-only',
|
||||
default=bool(False),
|
||||
action='store_true',
|
||||
help=argparse.SUPPRESS)
|
||||
|
||||
parser.add_argument('--include-password',
|
||||
default=bool(utils.env('HEAT_INCLUDE_PASSWORD')),
|
||||
action='store_true',
|
||||
help=_('Send %(arg1)s and %(arg2)s to heat.') % {
|
||||
'arg1': 'os-username',
|
||||
'arg2': 'os-password'
|
||||
})
|
||||
|
||||
# FIXME(gyee): this method should come from python-keystoneclient.
|
||||
# Will refactor this code once it is available.
|
||||
# https://bugs.launchpad.net/python-keystoneclient/+bug/1332337
|
||||
|
||||
self._append_global_identity_args(parser)
|
||||
|
||||
if osprofiler_profiler:
|
||||
parser.add_argument(
|
||||
'--profile',
|
||||
metavar='HMAC_KEY',
|
||||
help=_('HMAC key to use for encrypting context data '
|
||||
'for performance profiling of operation. '
|
||||
'This key should be the value of HMAC key '
|
||||
'configured in osprofiler middleware in heat, '
|
||||
'it is specified in the paste configuration '
|
||||
'(/etc/heat/api-paste.ini). Without the key, '
|
||||
'profiling will not be triggered '
|
||||
'even if osprofiler is enabled on server side.'))
|
||||
return parser
|
||||
|
||||
def get_subcommand_parser(self, version):
|
||||
parser = self.get_base_parser()
|
||||
|
||||
self.subcommands = {}
|
||||
subparsers = parser.add_subparsers(metavar='<subcommand>')
|
||||
submodule = importutils.import_versioned_module('heatclient',
|
||||
version, 'shell')
|
||||
self._find_actions(subparsers, submodule)
|
||||
self._find_actions(subparsers, self)
|
||||
self._add_bash_completion_subparser(subparsers)
|
||||
|
||||
return parser
|
||||
|
||||
def _add_bash_completion_subparser(self, subparsers):
|
||||
subparser = subparsers.add_parser(
|
||||
'bash_completion',
|
||||
add_help=False,
|
||||
formatter_class=HelpFormatter
|
||||
)
|
||||
self.subcommands['bash_completion'] = subparser
|
||||
subparser.set_defaults(func=self.do_bash_completion)
|
||||
|
||||
def _find_actions(self, subparsers, actions_module):
|
||||
for attr in (a for a in dir(actions_module) if a.startswith('do_')):
|
||||
# I prefer to be hyphen-separated instead of underscores.
|
||||
command = attr[3:].replace('_', '-')
|
||||
callback = getattr(actions_module, attr)
|
||||
desc = callback.__doc__ or ''
|
||||
help = desc.strip().split('\n')[0]
|
||||
arguments = getattr(callback, 'arguments', [])
|
||||
|
||||
subparser = subparsers.add_parser(command,
|
||||
help=help,
|
||||
description=desc,
|
||||
add_help=False,
|
||||
formatter_class=HelpFormatter)
|
||||
subparser.add_argument('-h', '--help',
|
||||
action='help',
|
||||
help=argparse.SUPPRESS)
|
||||
self.subcommands[command] = subparser
|
||||
for (args, kwargs) in arguments:
|
||||
subparser.add_argument(*args, **kwargs)
|
||||
subparser.set_defaults(func=callback)
|
||||
|
||||
def _setup_logging(self, debug):
|
||||
log_lvl = logging.DEBUG if debug else logging.WARNING
|
||||
logging.basicConfig(
|
||||
format="%(levelname)s (%(module)s) %(message)s",
|
||||
level=log_lvl)
|
||||
logging.getLogger('iso8601').setLevel(logging.WARNING)
|
||||
logging.getLogger('urllib3.connectionpool').setLevel(logging.WARNING)
|
||||
|
||||
def _setup_verbose(self, verbose):
|
||||
if verbose:
|
||||
exc.verbose = 1
|
||||
|
||||
def _get_keystone_session(self, **kwargs):
|
||||
# first create a Keystone session
|
||||
cacert = kwargs.pop('cacert', None)
|
||||
cert = kwargs.pop('cert', None)
|
||||
key = kwargs.pop('key', None)
|
||||
insecure = kwargs.pop('insecure', False)
|
||||
timeout = kwargs.pop('timeout', None)
|
||||
verify = kwargs.pop('verify', None)
|
||||
|
||||
if verify is None:
|
||||
if insecure:
|
||||
verify = False
|
||||
else:
|
||||
# TODO(gyee): should we do
|
||||
# heatclient.common.http.get_system_ca_fle()?
|
||||
verify = cacert or True
|
||||
if cert and key:
|
||||
# passing cert and key together is deprecated in favour of the
|
||||
# requests lib form of having the cert and key as a tuple
|
||||
cert = (cert, key)
|
||||
|
||||
return kssession.Session(verify=verify, cert=cert, timeout=timeout)
|
||||
|
||||
def main(self, argv):
|
||||
# Parse args once to find version
|
||||
parser = self.get_base_parser()
|
||||
(options, args) = parser.parse_known_args(argv)
|
||||
self._setup_logging(options.debug)
|
||||
self._setup_verbose(options.verbose)
|
||||
|
||||
# build available subcommands based on version
|
||||
api_version = options.heat_api_version
|
||||
subcommand_parser = self.get_subcommand_parser(api_version)
|
||||
self.parser = subcommand_parser
|
||||
|
||||
# Handle top-level --help/-h before attempting to parse
|
||||
# a command off the command line
|
||||
if not args and options.help or not argv:
|
||||
self.do_help(options)
|
||||
return 0
|
||||
|
||||
# Parse args again and call whatever callback was selected
|
||||
args = subcommand_parser.parse_args(argv)
|
||||
|
||||
# Short-circuit and deal with help command right away.
|
||||
if args.func == self.do_help:
|
||||
self.do_help(args)
|
||||
return 0
|
||||
elif args.func == self.do_bash_completion:
|
||||
self.do_bash_completion(args)
|
||||
return 0
|
||||
|
||||
if not args.os_username and not args.os_auth_token:
|
||||
raise exc.CommandError(_("You must provide a username via either "
|
||||
"--os-username or env[OS_USERNAME] "
|
||||
"or a token via --os-auth-token or "
|
||||
"env[OS_AUTH_TOKEN]"))
|
||||
|
||||
if not args.os_password and not args.os_auth_token:
|
||||
raise exc.CommandError(_("You must provide a password via either "
|
||||
"--os-password or env[OS_PASSWORD] "
|
||||
"or a token via --os-auth-token or "
|
||||
"env[OS_AUTH_TOKEN]"))
|
||||
|
||||
if args.os_no_client_auth:
|
||||
if not args.heat_url:
|
||||
raise exc.CommandError(_("If you specify --os-no-client-auth "
|
||||
"you must also specify a Heat API "
|
||||
"URL via either --heat-url or "
|
||||
"env[HEAT_URL]"))
|
||||
else:
|
||||
# Tenant/project name or ID is needed to make keystoneclient
|
||||
# retrieve a service catalog, it's not required if
|
||||
# os_no_client_auth is specified, neither is the auth URL
|
||||
|
||||
if not (args.os_tenant_id or args.os_tenant_name or
|
||||
args.os_project_id or args.os_project_name):
|
||||
raise exc.CommandError(
|
||||
_("You must provide a tenant id via either "
|
||||
"--os-tenant-id or env[OS_TENANT_ID] or a tenant name "
|
||||
"via either --os-tenant-name or env[OS_TENANT_NAME] "
|
||||
"or a project id via either --os-project-id or "
|
||||
"env[OS_PROJECT_ID] or a project name via "
|
||||
"either --os-project-name or env[OS_PROJECT_NAME]"))
|
||||
|
||||
if not args.os_auth_url:
|
||||
raise exc.CommandError(_("You must provide an auth url via "
|
||||
"either --os-auth-url or via "
|
||||
"env[OS_AUTH_URL]"))
|
||||
kwargs = {
|
||||
'insecure': args.insecure,
|
||||
'cacert': args.os_cacert,
|
||||
'cert': args.os_cert,
|
||||
'key': args.os_key,
|
||||
'timeout': args.api_timeout
|
||||
}
|
||||
|
||||
service_type = args.os_service_type or 'orchestration'
|
||||
if args.os_no_client_auth:
|
||||
# Do not use session since no_client_auth means using heat to
|
||||
# to authenticate
|
||||
kwargs = {
|
||||
'username': args.os_username,
|
||||
'password': args.os_password,
|
||||
'auth_url': args.os_auth_url,
|
||||
'token': args.os_auth_token,
|
||||
'include_pass': args.include_password,
|
||||
'insecure': args.insecure,
|
||||
'timeout': args.api_timeout,
|
||||
'endpoint': args.heat_url
|
||||
}
|
||||
else:
|
||||
keystone_session = self._get_keystone_session(**kwargs)
|
||||
endpoint_type = args.os_endpoint_type or 'publicURL'
|
||||
if args.os_auth_token:
|
||||
kwargs = {
|
||||
'token': args.os_auth_token,
|
||||
'auth_url': args.os_auth_url
|
||||
}
|
||||
keystone_auth = generic.Token(**kwargs)
|
||||
else:
|
||||
project_id = args.os_project_id or args.os_tenant_id
|
||||
project_name = args.os_project_name or args.os_tenant_name
|
||||
kwargs = {
|
||||
'username': args.os_username,
|
||||
'user_id': args.os_user_id,
|
||||
'user_domain_id': args.os_user_domain_id,
|
||||
'user_domain_name': args.os_user_domain_name,
|
||||
'password': args.os_password,
|
||||
'auth_url': args.os_auth_url,
|
||||
'project_id': project_id,
|
||||
'project_name': project_name,
|
||||
'project_domain_id': args.os_project_domain_id,
|
||||
'project_domain_name': args.os_project_domain_name,
|
||||
}
|
||||
keystone_auth = generic.Password(**kwargs)
|
||||
|
||||
kwargs = {
|
||||
'auth_url': args.os_auth_url,
|
||||
'session': keystone_session,
|
||||
'auth': keystone_auth,
|
||||
'service_type': service_type,
|
||||
'endpoint_type': endpoint_type,
|
||||
'region_name': args.os_region_name,
|
||||
'username': args.os_username,
|
||||
'password': args.os_password,
|
||||
'include_pass': args.include_password,
|
||||
'endpoint_override': args.heat_url,
|
||||
}
|
||||
|
||||
client = heat_client.Client(api_version, **kwargs)
|
||||
|
||||
profile = osprofiler_profiler and options.profile
|
||||
if profile:
|
||||
osprofiler_profiler.init(options.profile)
|
||||
|
||||
args.func(client, args)
|
||||
|
||||
if profile:
|
||||
trace_id = osprofiler_profiler.get().get_base_id()
|
||||
print(_("Trace ID: %s") % trace_id)
|
||||
print(_("To display trace use next command:\n"
|
||||
"osprofiler trace show --html %s ") % trace_id)
|
||||
|
||||
def do_bash_completion(self, args):
|
||||
"""Prints all of the commands and options to stdout.
|
||||
|
||||
The heat.bash_completion script doesn't have to hard code them.
|
||||
"""
|
||||
commands = set()
|
||||
options = set()
|
||||
for sc_str, sc in self.subcommands.items():
|
||||
commands.add(sc_str)
|
||||
for option in list(sc._optionals._option_string_actions):
|
||||
options.add(option)
|
||||
|
||||
commands.remove('bash-completion')
|
||||
commands.remove('bash_completion')
|
||||
print(' '.join(commands | options))
|
||||
|
||||
@utils.arg('command', metavar='<subcommand>', nargs='?',
|
||||
help=_('Display help for <subcommand>.'))
|
||||
def do_help(self, args):
|
||||
"""Display help about this program or one of its subcommands."""
|
||||
if getattr(args, 'command', None):
|
||||
if args.command in self.subcommands:
|
||||
self.subcommands[args.command].print_help()
|
||||
else:
|
||||
raise exc.CommandError("'%s' is not a valid subcommand" %
|
||||
args.command)
|
||||
else:
|
||||
self.parser.print_help()
|
||||
|
||||
|
||||
class HelpFormatter(argparse.HelpFormatter):
|
||||
def start_section(self, heading):
|
||||
# Title-case the headings
|
||||
heading = '%s%s' % (heading[0].upper(), heading[1:])
|
||||
super(HelpFormatter, self).start_section(heading)
|
||||
|
||||
|
||||
def main(args=None):
|
||||
try:
|
||||
if args is None:
|
||||
args = sys.argv[1:]
|
||||
|
||||
HeatShell().main(args)
|
||||
except KeyboardInterrupt:
|
||||
print(_("... terminating heat client"), file=sys.stderr)
|
||||
sys.exit(130)
|
||||
except Exception as e:
|
||||
if '--debug' in args or '-d' in args:
|
||||
raise
|
||||
else:
|
||||
print(encodeutils.safe_encode(six.text_type(e)), file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
|
@ -1,42 +0,0 @@
|
|||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import os
|
||||
|
||||
from tempest.lib.cli import base
|
||||
|
||||
|
||||
class ClientTestBase(base.ClientTestBase):
|
||||
"""This is a first pass at a simple read only python-heatclient test.
|
||||
|
||||
This only exercises client commands that are read only.
|
||||
This should test commands:
|
||||
* as a regular user
|
||||
* as a admin user
|
||||
* with and without optional parameters
|
||||
* initially just check return codes, and later test command outputs
|
||||
"""
|
||||
|
||||
def _get_clients(self):
|
||||
cli_dir = os.environ.get(
|
||||
'OS_HEATCLIENT_EXEC_DIR',
|
||||
os.path.join(os.path.abspath('.'), '.tox/functional/bin'))
|
||||
|
||||
return base.CLIClient(
|
||||
username=os.environ.get('OS_USERNAME'),
|
||||
password=os.environ.get('OS_PASSWORD'),
|
||||
tenant_name=os.environ.get('OS_TENANT_NAME'),
|
||||
uri=os.environ.get('OS_AUTH_URL'),
|
||||
cli_dir=cli_dir)
|
||||
|
||||
def heat(self, *args, **kwargs):
|
||||
return self.clients.heat(*args, **kwargs)
|
|
@ -1,14 +0,0 @@
|
|||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
|
||||
HEAT_MINIMAL_HOT_TEMPLATE_URL = 'https://raw.githubusercontent.com/openstack/python-heatclient/acb378364c96f9c5d46d8d4095d69a81fe91727a/heatclient/tests/functional/templates/heat_minimal_hot.yaml' # noqa
|
|
@ -1,50 +0,0 @@
|
|||
#!/bin/bash -xe
|
||||
|
||||
# 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.
|
||||
|
||||
# This script is executed inside post_test_hook function in devstack gate.
|
||||
|
||||
function generate_testr_results {
|
||||
if [ -f .testrepository/0 ]; then
|
||||
sudo .tox/functional/bin/testr last --subunit > $WORKSPACE/testrepository.subunit
|
||||
sudo mv $WORKSPACE/testrepository.subunit $BASE/logs/testrepository.subunit
|
||||
sudo /usr/os-testr-env/bin/subunit2html $BASE/logs/testrepository.subunit $BASE/logs/testr_results.html
|
||||
sudo gzip -9 $BASE/logs/testrepository.subunit
|
||||
sudo gzip -9 $BASE/logs/testr_results.html
|
||||
sudo chown jenkins:jenkins $BASE/logs/testrepository.subunit.gz $BASE/logs/testr_results.html.gz
|
||||
sudo chmod a+r $BASE/logs/testrepository.subunit.gz $BASE/logs/testr_results.html.gz
|
||||
fi
|
||||
}
|
||||
|
||||
export HEATCLIENT_DIR="$BASE/new/python-heatclient"
|
||||
|
||||
# Get admin credentials
|
||||
cd $BASE/new/devstack
|
||||
source openrc admin admin
|
||||
|
||||
# Go to the heatclient dir
|
||||
cd $HEATCLIENT_DIR
|
||||
|
||||
sudo chown -R jenkins:stack $HEATCLIENT_DIR
|
||||
|
||||
# Run tests
|
||||
echo "Running heatclient functional test suite"
|
||||
set +e
|
||||
# Preserve env for OS_ credentials
|
||||
sudo -E -H -u jenkins tox -efunctional
|
||||
EXIT_CODE=$?
|
||||
set -e
|
||||
|
||||
# Collect and parse result
|
||||
generate_testr_results
|
||||
exit $EXIT_CODE
|
|
@ -1,116 +0,0 @@
|
|||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import os
|
||||
|
||||
import six
|
||||
from tempest.lib.cli import base
|
||||
from tempest.lib.cli import output_parser
|
||||
|
||||
|
||||
class OpenStackClientTestBase(base.ClientTestBase):
|
||||
"""Command line client base functions."""
|
||||
|
||||
def setUp(self):
|
||||
super(OpenStackClientTestBase, self).setUp()
|
||||
self.parser = output_parser
|
||||
|
||||
def _get_clients(self):
|
||||
cli_dir = os.environ.get(
|
||||
'OS_HEATCLIENT_EXEC_DIR',
|
||||
os.path.join(os.path.abspath('.'), '.tox/functional/bin'))
|
||||
|
||||
return base.CLIClient(
|
||||
username=os.environ.get('OS_USERNAME'),
|
||||
password=os.environ.get('OS_PASSWORD'),
|
||||
tenant_name=os.environ.get('OS_TENANT_NAME'),
|
||||
uri=os.environ.get('OS_AUTH_URL'),
|
||||
cli_dir=cli_dir)
|
||||
|
||||
def openstack(self, *args, **kwargs):
|
||||
return self.clients.openstack(*args, **kwargs)
|
||||
|
||||
def get_template_path(self, templ_name):
|
||||
return os.path.join(os.path.dirname(os.path.realpath(__file__)),
|
||||
'../../templates/%s' % templ_name)
|
||||
|
||||
def show_to_dict(self, output):
|
||||
obj = {}
|
||||
items = self.parser.listing(output)
|
||||
for item in items:
|
||||
obj[item['Field']] = six.text_type(item['Value'])
|
||||
return dict((self._key_name(k), v) for k, v in obj.items())
|
||||
|
||||
def _key_name(self, key):
|
||||
return key.lower().replace(' ', '_')
|
||||
|
||||
def list_to_dict(self, output, id):
|
||||
obj = {}
|
||||
items = self.parser.listing(output)
|
||||
for item in items:
|
||||
if item['ID'] == id:
|
||||
obj = item
|
||||
break
|
||||
return dict((self._key_name(k), v) for k, v in obj.items())
|
||||
|
||||
def _stack_create(self, name, template, parameters=[], wait=True):
|
||||
cmd = 'stack create ' + name
|
||||
if template:
|
||||
cmd += ' -t ' + template
|
||||
if wait:
|
||||
cmd += ' --wait'
|
||||
|
||||
for parameter in parameters:
|
||||
cmd += ' --parameter ' + parameter
|
||||
stack_raw = self.openstack(cmd)
|
||||
stack = self.show_to_dict(stack_raw)
|
||||
self.addCleanup(self._stack_delete, stack['id'])
|
||||
return stack
|
||||
|
||||
def _stack_delete(self, id, wait=False):
|
||||
cmd = 'stack delete ' + id + ' --yes'
|
||||
if wait:
|
||||
cmd += ' --wait'
|
||||
if id in self.openstack('stack list --short'):
|
||||
self.openstack(cmd)
|
||||
|
||||
def _stack_suspend(self, id, wait=True):
|
||||
cmd = 'stack suspend ' + id
|
||||
if wait:
|
||||
cmd += ' --wait'
|
||||
stack_raw = self.openstack(cmd)
|
||||
stack = self.list_to_dict(stack_raw, id)
|
||||
return stack
|
||||
|
||||
def _stack_resume(self, id, wait=True):
|
||||
cmd = 'stack resume ' + id
|
||||
if wait:
|
||||
cmd += ' --wait'
|
||||
stack_raw = self.openstack(cmd)
|
||||
stack = self.list_to_dict(stack_raw, id)
|
||||
return stack
|
||||
|
||||
def _stack_snapshot_create(self, id, name):
|
||||
cmd = 'stack snapshot create ' + id + ' --name ' + name
|
||||
snapshot_raw = self.openstack(cmd)
|
||||
snapshot = self.show_to_dict(snapshot_raw)
|
||||
self.addCleanup(self._stack_snapshot_delete, id, snapshot['id'])
|
||||
return snapshot
|
||||
|
||||
def _stack_snapshot_delete(self, id, snapshot_id):
|
||||
cmd = 'stack snapshot delete ' + id + ' ' + snapshot_id
|
||||
if snapshot_id in self.openstack('stack snapshot list ' + id):
|
||||
self.openstack(cmd)
|
||||
|
||||
def _stack_snapshot_restore(self, id, snapshot_id):
|
||||
cmd = 'stack snapshot restore ' + id + ' ' + snapshot_id
|
||||
self.openstack(cmd)
|
|
@ -1,135 +0,0 @@
|
|||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import yaml
|
||||
|
||||
from tempest.lib import exceptions
|
||||
|
||||
from heatclient.tests.functional.osc.v1 import base
|
||||
|
||||
|
||||
class SimpleReadOnlyOpenStackClientTest(base.OpenStackClientTestBase):
|
||||
"""Basic, read-only tests for Openstack CLI client heat plugin.
|
||||
|
||||
Basic smoke test for the openstack CLI commands which do not require
|
||||
creating or modifying stacks.
|
||||
"""
|
||||
|
||||
def test_openstack_fake_action(self):
|
||||
self.assertRaises(exceptions.CommandFailed,
|
||||
self.openstack,
|
||||
'this-does-not-exist')
|
||||
|
||||
# Empty list commands
|
||||
def test_openstack_empty_lists(self):
|
||||
cmds = [
|
||||
'software config',
|
||||
'software deployment',
|
||||
'stack',
|
||||
]
|
||||
for cmd in cmds:
|
||||
self.openstack(cmd + ' list')
|
||||
|
||||
# Stack not found commands
|
||||
def test_openstack_stack_not_found(self):
|
||||
cmds = [
|
||||
'stack abandon',
|
||||
'stack check',
|
||||
'stack output list',
|
||||
'stack resume',
|
||||
'stack show',
|
||||
'stack snapshot list',
|
||||
'stack suspend',
|
||||
'stack template show',
|
||||
'stack cancel'
|
||||
]
|
||||
for cmd in cmds:
|
||||
err = self.assertRaises(exceptions.CommandFailed,
|
||||
self.openstack,
|
||||
cmd + ' I-AM-NOT-FOUND')
|
||||
self.assertIn('Stack not found: I-AM-NOT-FOUND', str(err))
|
||||
|
||||
def test_openstack_stack_list_debug(self):
|
||||
self.openstack('stack list', flags='--debug')
|
||||
|
||||
def test_openstack_stack_list_property(self):
|
||||
self.openstack('stack list --property id=123')
|
||||
|
||||
def test_openstack_help_cmd(self):
|
||||
help_text = self.openstack('help stack list')
|
||||
lines = help_text.split('\n')
|
||||
self.assertFirstLineStartsWith(lines, 'usage: openstack stack list')
|
||||
|
||||
def test_openstack_version(self):
|
||||
self.openstack('', flags='--version')
|
||||
|
||||
def test_openstack_template_version_list(self):
|
||||
ret = self.openstack('orchestration template version list')
|
||||
tmpl_types = self.parser.listing(ret)
|
||||
self.assertTableStruct(tmpl_types, ['Version', 'Type'])
|
||||
|
||||
def test_openstack_template_function_list(self):
|
||||
ret = self.openstack('orchestration template function list '
|
||||
'heat_template_version.2015-10-15')
|
||||
tmpl_functions = self.parser.listing(ret)
|
||||
self.assertTableStruct(tmpl_functions, ['Functions', 'Description'])
|
||||
|
||||
def test_openstack_resource_type_list(self):
|
||||
ret = self.openstack('orchestration resource type list')
|
||||
rsrc_types = self.parser.listing(ret)
|
||||
self.assertTableStruct(rsrc_types, ['Resource Type'])
|
||||
|
||||
def test_openstack_resource_type_show(self):
|
||||
rsrc_schema = self.openstack('orchestration resource type show '
|
||||
'OS::Heat::RandomString')
|
||||
self.assertIsInstance(yaml.load(rsrc_schema), dict)
|
||||
|
||||
def _template_validate(self, templ_name, parms):
|
||||
heat_template_path = self.get_template_path(templ_name)
|
||||
cmd = 'stack create test-stack --dry-run --template %s'\
|
||||
% heat_template_path
|
||||
for parm in parms:
|
||||
cmd += ' --parameter ' + parm
|
||||
ret = self.openstack(cmd)
|
||||
self.assertRegex(ret, r'stack_name.*|.*test_stack')
|
||||
|
||||
def test_heat_template_validate_yaml(self):
|
||||
self._template_validate(
|
||||
'heat_minimal.yaml',
|
||||
['ClientName=ClientName', 'WaitSecs=123']
|
||||
)
|
||||
|
||||
def test_heat_template_validate_hot(self):
|
||||
self._template_validate(
|
||||
'heat_minimal_hot.yaml',
|
||||
['test_client_name=test_client_name', 'test_wait_secs=123']
|
||||
)
|
||||
|
||||
def _orchestration_template_validate(self, templ_name, parms):
|
||||
template_path = self.get_template_path(templ_name)
|
||||
cmd = 'orchestration template validate --template %s' % template_path
|
||||
for parm in parms:
|
||||
cmd += ' --parameter ' + parm
|
||||
ret = self.openstack(cmd)
|
||||
self.assertRegex(ret, r'Value:.*123')
|
||||
|
||||
def test_orchestration_template_validate_yaml(self):
|
||||
self._orchestration_template_validate(
|
||||
'heat_minimal.yaml',
|
||||
['ClientName=ClientName', 'WaitSecs=123']
|
||||
)
|
||||
|
||||
def test_orchestration_template_validate_hot(self):
|
||||
self._orchestration_template_validate(
|
||||
'heat_minimal_hot.yaml',
|
||||
['test_client_name=test_client_name', 'test_wait_secs=123']
|
||||
)
|
|
@ -1,77 +0,0 @@
|
|||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from tempest.lib.common.utils import data_utils as utils
|
||||
|
||||
from heatclient.tests.functional import config
|
||||
from heatclient.tests.functional.osc.v1 import base
|
||||
|
||||
|
||||
class OpenStackClientStackTest(base.OpenStackClientTestBase):
|
||||
"""Basic stack operation tests for Openstack CLI client heat plugin.
|
||||
|
||||
Basic smoke test for the openstack CLI stack commands.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
super(OpenStackClientStackTest, self).setUp()
|
||||
self.stack_name = utils.rand_name(name='test-stack')
|
||||
|
||||
def _stack_create_minimal(self, from_url=False):
|
||||
if from_url:
|
||||
template = config.HEAT_MINIMAL_HOT_TEMPLATE_URL
|
||||
else:
|
||||
template = self.get_template_path('heat_minimal_hot.yaml')
|
||||
parameters = ['test_client_name=test_client_name']
|
||||
return self._stack_create(self.stack_name, template=template,
|
||||
parameters=parameters)
|
||||
|
||||
def test_stack_create_minimal_from_file(self):
|
||||
stack = self._stack_create_minimal()
|
||||
self.assertEqual(self.stack_name, stack['stack_name'])
|
||||
self.assertEqual("CREATE_COMPLETE", stack['stack_status'])
|
||||
|
||||
def test_stack_create_minimal_from_url(self):
|
||||
stack = self._stack_create_minimal(from_url=True)
|
||||
self.assertEqual(self.stack_name, stack['stack_name'])
|
||||
self.assertEqual("CREATE_COMPLETE", stack['stack_status'])
|
||||
|
||||
def test_stack_suspend_resume(self):
|
||||
stack = self._stack_create_minimal()
|
||||
stack = self._stack_suspend(stack['id'])
|
||||
self.assertEqual(self.stack_name, stack['stack_name'])
|
||||
self.assertEqual("SUSPEND_COMPLETE", stack['stack_status'])
|
||||
stack = self._stack_resume(stack['id'])
|
||||
self.assertEqual(self.stack_name, stack['stack_name'])
|
||||
self.assertEqual("RESUME_COMPLETE", stack['stack_status'])
|
||||
|
||||
def test_stack_snapshot_create_restore(self):
|
||||
snapshot_name = utils.rand_name(name='test-stack-snapshot')
|
||||
stack = self._stack_create_minimal()
|
||||
snapshot = self._stack_snapshot_create(stack['id'], snapshot_name)
|
||||
self.assertEqual(snapshot_name, snapshot['name'])
|
||||
self._stack_snapshot_restore(stack['id'], snapshot['id'])
|
||||
|
||||
def test_stack_delete(self):
|
||||
stack = self._stack_create_minimal()
|
||||
self._stack_delete(stack['id'])
|
||||
stacks_raw = self.openstack('stack list')
|
||||
self.assertNotIn(stack['id'], stacks_raw)
|
||||
|
||||
def test_stack_snapshot_delete(self):
|
||||
snapshot_name = utils.rand_name(name='test-stack-snapshot')
|
||||
stack = self._stack_create_minimal()
|
||||
snapshot = self._stack_snapshot_create(stack['id'], snapshot_name)
|
||||
self._stack_snapshot_delete(stack['id'], snapshot['id'])
|
||||
stacks_raw = self.openstack(
|
||||
'stack snapshot list' + ' ' + self.stack_name)
|
||||
self.assertNotIn(snapshot['id'], stacks_raw)
|
|
@ -1,16 +0,0 @@
|
|||
HeatTemplateFormatVersion: '2012-12-12'
|
||||
Description: Minimal template to test validation
|
||||
Parameters:
|
||||
ClientName:
|
||||
Description: Client to poll
|
||||
Type: String
|
||||
WaitSecs:
|
||||
Description: Seconds to wait after an action (-1 is infinite)
|
||||
Type: Number
|
||||
Default: 0
|
||||
Resources:
|
||||
TestResource:
|
||||
Type: OS::Heat::TestResource
|
||||
Properties:
|
||||
client_name: {Ref: ClientName}
|
||||
wait_secs: {Ref: WaitSecs}
|
|
@ -1,16 +0,0 @@
|
|||
heat_template_version: 2015-04-30
|
||||
description: A minimal HOT test template
|
||||
parameters:
|
||||
test_client_name:
|
||||
description: Client to poll
|
||||
type: string
|
||||
test_wait_secs:
|
||||
description: Seconds to wait after an action (-1 is infinite)
|
||||
type: number
|
||||
default: 0
|
||||
resources:
|
||||
test_resource:
|
||||
type: OS::Heat::TestResource
|
||||
properties:
|
||||
client_name: { get_param: test_client_name }
|
||||
wait_secs: { get_param: test_wait_secs }
|
|
@ -1,103 +0,0 @@
|
|||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import json
|
||||
import os
|
||||
|
||||
from tempest.lib import exceptions
|
||||
import yaml
|
||||
|
||||
from heatclient.tests.functional import base
|
||||
|
||||
|
||||
class SimpleReadOnlyHeatClientTest(base.ClientTestBase):
|
||||
"""Basic, read-only tests for Heat CLI client.
|
||||
|
||||
Basic smoke test for the heat CLI commands which do not require
|
||||
creating or modifying stacks.
|
||||
"""
|
||||
|
||||
def test_heat_fake_action(self):
|
||||
self.assertRaises(exceptions.CommandFailed,
|
||||
self.heat,
|
||||
'this-does-not-exist')
|
||||
|
||||
def test_heat_stack_list(self):
|
||||
self.heat('stack-list')
|
||||
|
||||
def test_heat_stack_list_debug(self):
|
||||
self.heat('stack-list', flags='--debug')
|
||||
|
||||
def test_heat_resource_template_fmt_default(self):
|
||||
ret = self.heat('resource-template OS::Nova::Server')
|
||||
self.assertIn('Type: OS::Nova::Server', ret)
|
||||
|
||||
def test_heat_resource_template_fmt_arg_short_yaml(self):
|
||||
ret = self.heat('resource-template -F yaml OS::Nova::Server')
|
||||
self.assertIn('Type: OS::Nova::Server', ret)
|
||||
self.assertIsInstance(yaml.safe_load(ret), dict)
|
||||
|
||||
def test_heat_resource_template_fmt_arg_long_json(self):
|
||||
ret = self.heat('resource-template --format json OS::Nova::Server')
|
||||
self.assertIn('"Type": "OS::Nova::Server"', ret)
|
||||
self.assertIsInstance(json.loads(ret), dict)
|
||||
|
||||
def test_heat_resource_type_list(self):
|
||||
ret = self.heat('resource-type-list')
|
||||
rsrc_types = self.parser.listing(ret)
|
||||
self.assertTableStruct(rsrc_types, ['resource_type'])
|
||||
|
||||
def test_heat_resource_type_show(self):
|
||||
rsrc_schema = self.heat('resource-type-show OS::Heat::RandomString')
|
||||
# resource-type-show returns a json resource schema
|
||||
self.assertIsInstance(json.loads(rsrc_schema), dict)
|
||||
|
||||
def _template_validate(self, templ_name):
|
||||
heat_template_path = os.path.join(
|
||||
os.path.dirname(os.path.realpath(__file__)),
|
||||
'templates/%s' % templ_name)
|
||||
ret = self.heat('template-validate -f %s' % heat_template_path)
|
||||
# On success template-validate returns a json representation
|
||||
# of the template parameters
|
||||
self.assertIsInstance(json.loads(ret), dict)
|
||||
|
||||
def test_heat_template_validate_yaml(self):
|
||||
self._template_validate('heat_minimal.yaml')
|
||||
|
||||
def test_heat_template_validate_hot(self):
|
||||
self._template_validate('heat_minimal_hot.yaml')
|
||||
|
||||
def test_heat_help(self):
|
||||
self.heat('help')
|
||||
|
||||
def test_heat_bash_completion(self):
|
||||
self.heat('bash-completion')
|
||||
|
||||
def test_heat_help_cmd(self):
|
||||
# Check requesting help for a specific command works
|
||||
help_text = self.heat('help resource-template')
|
||||
lines = help_text.split('\n')
|
||||
self.assertFirstLineStartsWith(lines, 'usage: heat resource-template')
|
||||
|
||||
def test_heat_version(self):
|
||||
self.heat('', flags='--version')
|
||||
|
||||
def test_heat_template_version_list(self):
|
||||
ret = self.heat('template-version-list')
|
||||
tmpl_types = self.parser.listing(ret)
|
||||
self.assertTableStruct(tmpl_types, ['version', 'type'])
|
||||
|
||||
def test_heat_template_function_list(self):
|
||||
ret = self.heat('template-function-list '
|
||||
'heat_template_version.2013-05-23')
|
||||
tmpl_functions = self.parser.listing(ret)
|
||||
self.assertTableStruct(tmpl_functions, ['functions', 'description'])
|
|
@ -1,78 +0,0 @@
|
|||
#
|
||||
# Copyright 2016 IBM Corp.
|
||||
#
|
||||
# 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.
|
||||
|
||||
FULL_TEMPLATE = '''
|
||||
heat_template_version: 2016-04-08
|
||||
|
||||
description: a template
|
||||
|
||||
parameter_groups:
|
||||
- label: param_group_1
|
||||
description: parameter group 1
|
||||
parameters:
|
||||
- param1
|
||||
- param2
|
||||
- label: param_group_2
|
||||
description: parameter group 2
|
||||
parameters:
|
||||
- param3
|
||||
|
||||
parameters:
|
||||
param1:
|
||||
type: string
|
||||
label: parameter 5
|
||||
description: parameter 5
|
||||
default: foo
|
||||
hidden: false
|
||||
constraints:
|
||||
- allowed_values: ['foo', 'bar', 'bax']
|
||||
param2:
|
||||
type: number
|
||||
default: 0
|
||||
constraints:
|
||||
- range: {min: 0, max: 10}
|
||||
description: must be between 0 and 10
|
||||
param3:
|
||||
type: boolean
|
||||
|
||||
resources:
|
||||
resource1:
|
||||
type: OS::Heat::None
|
||||
properties:
|
||||
prop1: { get_param: param1 }
|
||||
prop2: { get_param: param2 }
|
||||
prop3: value
|
||||
resource2:
|
||||
type: OS::Heat::None
|
||||
properties:
|
||||
prop1: { get_param: param3 }
|
||||
depends_on: resource1
|
||||
|
||||
outputs:
|
||||
output1:
|
||||
description: resource 1 prop 3
|
||||
value: { get_attr: [resource1, prop3] }
|
||||
output2:
|
||||
description: resource 2 prop 1
|
||||
value: { get_attr: [resource2, prop1] }
|
||||
'''
|
||||
|
||||
SHORT_TEMPLATE = '''
|
||||
heat_template_version: 2016-04-08
|
||||
|
||||
resources:
|
||||
res1:
|
||||
type: OS::Heat::None
|
||||
'''
|
|
@ -1,32 +0,0 @@
|
|||
{
|
||||
"files": {},
|
||||
"status": "COMPLETE",
|
||||
"name": "my_stack",
|
||||
"tags": null,
|
||||
"stack_user_project_id": "123456",
|
||||
"environment": {},
|
||||
"template": {
|
||||
"heat_template_version": "2016-04-08",
|
||||
"resources": {
|
||||
"thing": {
|
||||
"type": "OS::Heat::TestResource"
|
||||
}
|
||||
}
|
||||
},
|
||||
"action": "CREATE",
|
||||
"project_id": "56789",
|
||||
"id": "2468",
|
||||
"resources": {
|
||||
"thing": {
|
||||
"status": "COMPLETE",
|
||||
"name": "thing",
|
||||
"resource_data": {
|
||||
"value": "test_string"
|
||||
},
|
||||
"resource_id": "my_stack-thing-1234",
|
||||
"action": "CREATE",
|
||||
"type": "OS::Heat::TestResource",
|
||||
"metadata": {}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
heat_template_version: 2013-05-23
|
|
@ -1,7 +0,0 @@
|
|||
heat_template_version: 2013-05-23
|
||||
|
||||
parameters:
|
||||
p1:
|
||||
type: string
|
||||
p2:
|
||||
type: number
|
|
@ -1,5 +0,0 @@
|
|||
import sys
|
||||
|
||||
from mox3 import mox
|
||||
|
||||
sys.modules['mox'] = mox
|
|
@ -1,47 +0,0 @@
|
|||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
# implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from oslo_serialization import jsonutils
|
||||
|
||||
|
||||
class FakeRaw(object):
|
||||
version = 110
|
||||
|
||||
|
||||
class FakeHTTPResponse(object):
|
||||
|
||||
version = 1.1
|
||||
|
||||
def __init__(self, status_code, reason, headers, content):
|
||||
self.headers = headers
|
||||
self.content = content
|
||||
self.status_code = status_code
|
||||
self.reason = reason
|
||||
self.raw = FakeRaw()
|
||||
|
||||
def getheader(self, name, default=None):
|
||||
return self.headers.get(name, default)
|
||||
|
||||
def getheaders(self):
|
||||
return self.headers.items()
|
||||
|
||||
def read(self, amt=None):
|
||||
b = self.content
|
||||
self.content = None
|
||||
return b
|
||||
|
||||
def iter_content(self, chunksize):
|
||||
return self.content
|
||||
|
||||
def json(self):
|
||||
return jsonutils.loads(self.content)
|
|
@ -1,45 +0,0 @@
|
|||
# Copyright 2013 Nebula Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
#
|
||||
|
||||
import json
|
||||
|
||||
import requests
|
||||
import six
|
||||
|
||||
|
||||
class FakeStdout(object):
|
||||
def __init__(self):
|
||||
self.content = []
|
||||
|
||||
def write(self, text):
|
||||
self.content.append(text)
|
||||
|
||||
def make_string(self):
|
||||
result = ''
|
||||
for line in self.content:
|
||||
result = result + line
|
||||
return result
|
||||
|
||||
|
||||
class FakeResponse(requests.Response):
|
||||
def __init__(self, headers={}, status_code=200, data=None, encoding=None):
|
||||
super(FakeResponse, self).__init__()
|
||||
|
||||
self.status_code = status_code
|
||||
|
||||
self.headers.update(headers)
|
||||
self._content = json.dumps(data)
|
||||
if not isinstance(self._content, six.binary_type):
|
||||
self._content = self._content.encode()
|
|
@ -1,83 +0,0 @@
|
|||
# Copyright 2012-2013 OpenStack Foundation
|
||||
# Copyright 2013 Nebula Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
#
|
||||
|
||||
import os
|
||||
|
||||
import fixtures
|
||||
import mock
|
||||
import sys
|
||||
import testtools
|
||||
|
||||
from heatclient.tests.unit.osc import fakes
|
||||
|
||||
|
||||
class TestCase(testtools.TestCase):
|
||||
def setUp(self):
|
||||
testtools.TestCase.setUp(self)
|
||||
|
||||
if (os.environ.get("OS_STDOUT_CAPTURE") == "True" or
|
||||
os.environ.get("OS_STDOUT_CAPTURE") == "1"):
|
||||
stdout = self.useFixture(fixtures.StringStream("stdout")).stream
|
||||
self.useFixture(fixtures.MonkeyPatch("sys.stdout", stdout))
|
||||
|
||||
if (os.environ.get("OS_STDERR_CAPTURE") == "True" or
|
||||
os.environ.get("OS_STDERR_CAPTURE") == "1"):
|
||||
stderr = self.useFixture(fixtures.StringStream("stderr")).stream
|
||||
self.useFixture(fixtures.MonkeyPatch("sys.stderr", stderr))
|
||||
|
||||
def assertNotCalled(self, m, msg=None):
|
||||
"""Assert a function was not called"""
|
||||
|
||||
if m.called:
|
||||
if not msg:
|
||||
msg = 'method %s should not have been called' % m
|
||||
self.fail(msg)
|
||||
|
||||
# 2.6 doesn't have the assert dict equals so make sure that it exists
|
||||
if tuple(sys.version_info)[0:2] < (2, 7):
|
||||
|
||||
def assertIsInstance(self, obj, cls, msg=None):
|
||||
"""self.assertTrue(isinstance(obj, cls)), with a nicer message"""
|
||||
|
||||
if not isinstance(obj, cls):
|
||||
standardMsg = '%s is not an instance of %r' % (obj, cls)
|
||||
self.fail(self._formatMessage(msg, standardMsg))
|
||||
|
||||
|
||||
class TestCommand(TestCase):
|
||||
"""Test cliff command classes"""
|
||||
|
||||
def setUp(self):
|
||||
super(TestCommand, self).setUp()
|
||||
# Build up a fake app
|
||||
self.fake_stdout = fakes.FakeStdout()
|
||||
self.app = mock.MagicMock()
|
||||
self.app.stdout = self.fake_stdout
|
||||
self.app.stdin = sys.stdin
|
||||
self.app.stderr = sys.stderr
|
||||
|
||||
def check_parser(self, cmd, args, verify_args):
|
||||
cmd_parser = cmd.get_parser('check_parser')
|
||||
try:
|
||||
parsed_args = cmd_parser.parse_args(args)
|
||||
except SystemExit:
|
||||
raise Exception("Argument parse failed")
|
||||
for av in verify_args:
|
||||
attr, value = av
|
||||
if attr:
|
||||
self.assertIn(attr, parsed_args)
|
||||
self.assertEqual(getattr(parsed_args, attr), value)
|
||||
return parsed_args
|
|
@ -1,25 +0,0 @@
|
|||
# Copyright 2014 OpenStack Foundation
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
#
|
||||
|
||||
import mock
|
||||
|
||||
from heatclient.tests.unit.osc import utils
|
||||
|
||||
|
||||
class TestOrchestrationv1(utils.TestCommand):
|
||||
def setUp(self):
|
||||
super(TestOrchestrationv1, self).setUp()
|
||||
|
||||
self.app.client_manager.orchestration = mock.MagicMock()
|
|
@ -1,44 +0,0 @@
|
|||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
#
|
||||
|
||||
import mock
|
||||
|
||||
from heatclient.osc.v1 import build_info as osc_build_info
|
||||
from heatclient.tests.unit.osc.v1 import fakes as orchestration_fakes
|
||||
|
||||
|
||||
class TestBuildInfo(orchestration_fakes.TestOrchestrationv1):
|
||||
response = {"api": {
|
||||
"revision": "{api_build_revision}"
|
||||
},
|
||||
"engine": {
|
||||
"revision": "{engine_build_revision}"
|
||||
}
|
||||
}
|
||||
|
||||
def setUp(self):
|
||||
super(TestBuildInfo, self).setUp()
|
||||
self.cmd = osc_build_info.BuildInfo(self.app, None)
|
||||
self.mock_client = self.app.client_manager.orchestration
|
||||
self.mock_client.build_info.build_info = mock.Mock(
|
||||
return_value=self.response)
|
||||
|
||||
def test_build_info(self):
|
||||
arglist = []
|
||||
parsed_args = self.check_parser(self.cmd, arglist, [])
|
||||
columns, data = self.cmd.take_action(parsed_args)
|
||||
self.mock_client.build_info.build_info.assert_called_with()
|
||||
self.assertEqual(['api', 'engine'], columns)
|
||||
self.assertEqual(['{\n "revision": "{api_build_revision}"\n}',
|
||||
'{\n "revision": "{engine_build_revision}"\n}'],
|
||||
list(data))
|
|
@ -1,269 +0,0 @@
|
|||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
#
|
||||
# Copyright 2015 IBM Corp.
|
||||
|
||||
import copy
|
||||
|
||||
import mock
|
||||
import testscenarios
|
||||
|
||||
from heatclient import exc
|
||||
from heatclient.osc.v1 import event
|
||||
from heatclient.tests.unit.osc.v1 import fakes
|
||||
from heatclient.v1 import events
|
||||
|
||||
load_tests = testscenarios.load_tests_apply_scenarios
|
||||
|
||||
|
||||
class TestEvent(fakes.TestOrchestrationv1):
|
||||
def setUp(self):
|
||||
super(TestEvent, self).setUp()
|
||||
self.mock_client = self.app.client_manager.orchestration
|
||||
self.event_client = self.app.client_manager.orchestration.events
|
||||
self.stack_client = self.app.client_manager.orchestration.stacks
|
||||
self.resource_client = self.app.client_manager.orchestration.resources
|
||||
|
||||
|
||||
class TestEventShow(TestEvent):
|
||||
|
||||
scenarios = [
|
||||
('table', dict(format='table')),
|
||||
('shell', dict(format='shell')),
|
||||
('value', dict(format='value')),
|
||||
]
|
||||
|
||||
response = {
|
||||
'event': {
|
||||
"resource_name": "my_resource",
|
||||
"event_time": "2015-11-11T15:23:47Z",
|
||||
"links": [],
|
||||
"logical_resource_id": "my_resource",
|
||||
"resource_status": "CREATE_FAILED",
|
||||
"resource_status_reason": "NotFound",
|
||||
"physical_resource_id": "null",
|
||||
"id": "474bfdf0-a450-46ec-a78a-0c7faa404073"
|
||||
}
|
||||
}
|
||||
|
||||
def setUp(self):
|
||||
super(TestEventShow, self).setUp()
|
||||
self.cmd = event.ShowEvent(self.app, None)
|
||||
|
||||
def test_event_show(self):
|
||||
arglist = ['--format', self.format, 'my_stack', 'my_resource', '1234']
|
||||
parsed_args = self.check_parser(self.cmd, arglist, [])
|
||||
self.event_client.get.return_value = events.Event(None, self.response)
|
||||
|
||||
self.cmd.take_action(parsed_args)
|
||||
|
||||
self.event_client.get.assert_called_with(**{
|
||||
'stack_id': 'my_stack',
|
||||
'resource_name': 'my_resource',
|
||||
'event_id': '1234'
|
||||
})
|
||||
|
||||
def _test_not_found(self, error):
|
||||
arglist = ['my_stack', 'my_resource', '1234']
|
||||
parsed_args = self.check_parser(self.cmd, arglist, [])
|
||||
ex = self.assertRaises(exc.CommandError, self.cmd.take_action,
|
||||
parsed_args)
|
||||
self.assertIn(error, str(ex))
|
||||
|
||||
def test_event_show_stack_not_found(self):
|
||||
error = 'Stack not found'
|
||||
self.stack_client.get.side_effect = exc.HTTPNotFound(error)
|
||||
self._test_not_found(error)
|
||||
|
||||
def test_event_show_resource_not_found(self):
|
||||
error = 'Resource not found'
|
||||
self.stack_client.get.side_effect = exc.HTTPNotFound(error)
|
||||
self._test_not_found(error)
|
||||
|
||||
def test_event_show_event_not_found(self):
|
||||
error = 'Event not found'
|
||||
self.stack_client.get.side_effect = exc.HTTPNotFound(error)
|
||||
self._test_not_found(error)
|
||||
|
||||
|
||||
class TestEventList(TestEvent):
|
||||
|
||||
defaults = {
|
||||
'stack_id': 'my_stack',
|
||||
'resource_name': None,
|
||||
'filters': {},
|
||||
'sort_dir': 'asc'
|
||||
}
|
||||
|
||||
fields = ['resource_name', 'id', 'resource_status',
|
||||
'resource_status_reason', 'event_time', 'physical_resource_id',
|
||||
'logical_resource_id']
|
||||
|
||||
class MockEvent(object):
|
||||
|
||||
data = {
|
||||
'event_time': '2015-11-13T10:02:17',
|
||||
'id': '1234',
|
||||
'logical_resource_id': 'resource1',
|
||||
'physical_resource_id': '',
|
||||
'resource_name': 'resource1',
|
||||
'resource_status': 'CREATE_COMPLETE',
|
||||
'resource_status_reason': 'state changed',
|
||||
'stack_name': 'my_stack',
|
||||
}
|
||||
|
||||
def __getattr__(self, key):
|
||||
try:
|
||||
return self.data[key]
|
||||
except KeyError:
|
||||
# hasattr() in python 3 expects an AttributeError to be raised
|
||||
raise AttributeError
|
||||
|
||||
def setUp(self):
|
||||
super(TestEventList, self).setUp()
|
||||
self.cmd = event.ListEvent(self.app, None)
|
||||
self.event = self.MockEvent()
|
||||
self.event_client.list.return_value = [self.event]
|
||||
self.resource_client.list.return_value = {}
|
||||
|
||||
def test_event_list_defaults(self):
|
||||
arglist = ['my_stack', '--format', 'table']
|
||||
parsed_args = self.check_parser(self.cmd, arglist, [])
|
||||
|
||||
columns, data = self.cmd.take_action(parsed_args)
|
||||
|
||||
self.event_client.list.assert_called_with(**self.defaults)
|
||||
self.assertEqual(self.fields, columns)
|
||||
|
||||
def test_event_list_resource_nested_depth(self):
|
||||
arglist = ['my_stack', '--resource', 'my_resource',
|
||||
'--nested-depth', '3', '--format', 'table']
|
||||
parsed_args = self.check_parser(self.cmd, arglist, [])
|
||||
|
||||
self.assertRaises(exc.CommandError, self.cmd.take_action, parsed_args)
|
||||
|
||||
def test_event_list_logical_resource_id(self):
|
||||
arglist = ['my_stack', '--format', 'table']
|
||||
del self.event.data['resource_name']
|
||||
cols = copy.deepcopy(self.fields)
|
||||
cols.pop()
|
||||
cols[0] = 'logical_resource_id'
|
||||
parsed_args = self.check_parser(self.cmd, arglist, [])
|
||||
|
||||
columns, data = self.cmd.take_action(parsed_args)
|
||||
|
||||
self.event_client.list.assert_called_with(**self.defaults)
|
||||
self.assertEqual(cols, columns)
|
||||
self.event.data['resource_name'] = 'resource1'
|
||||
|
||||
def test_event_list_nested_depth(self):
|
||||
arglist = ['my_stack', '--nested-depth', '3', '--format', 'table']
|
||||
kwargs = copy.deepcopy(self.defaults)
|
||||
kwargs['nested_depth'] = 3
|
||||
cols = copy.deepcopy(self.fields)
|
||||
cols[-1] = 'stack_name'
|
||||
cols.append('logical_resource_id')
|
||||
parsed_args = self.check_parser(self.cmd, arglist, [])
|
||||
|
||||
columns, data = self.cmd.take_action(parsed_args)
|
||||
|
||||
self.event_client.list.assert_has_calls([
|
||||
mock.call(**kwargs),
|
||||
mock.call(**self.defaults)
|
||||
])
|
||||
self.assertEqual(cols, columns)
|
||||
|
||||
@mock.patch('osc_lib.utils.sort_items')
|
||||
def test_event_list_sort(self, mock_sort_items):
|
||||
arglist = ['my_stack', '--sort', 'resource_name:desc',
|
||||
'--format', 'table']
|
||||
parsed_args = self.check_parser(self.cmd, arglist, [])
|
||||
|
||||
mock_event = self.MockEvent()
|
||||
mock_sort_items.return_value = [mock_event]
|
||||
columns, data = self.cmd.take_action(parsed_args)
|
||||
|
||||
mock_sort_items.assert_called_with(mock.ANY,
|
||||
"resource_name:desc")
|
||||
self.event_client.list.assert_called_with(
|
||||
filters={}, resource_name=None, sort_dir='desc',
|
||||
sort_keys=['resource_name'], stack_id='my_stack')
|
||||
self.assertEqual(self.fields, columns)
|
||||
|
||||
@mock.patch('osc_lib.utils.sort_items')
|
||||
def test_event_list_sort_multiple(self, mock_sort_items):
|
||||
arglist = ['my_stack', '--sort', 'resource_name:desc',
|
||||
'--sort', 'id:asc', '--format', 'table']
|
||||
parsed_args = self.check_parser(self.cmd, arglist, [])
|
||||
|
||||
mock_event = self.MockEvent()
|
||||
mock_sort_items.return_value = [mock_event]
|
||||
columns, data = self.cmd.take_action(parsed_args)
|
||||
|
||||
mock_sort_items.assert_called_with(mock.ANY,
|
||||
"resource_name:desc,id:asc")
|
||||
self.event_client.list.assert_called_with(
|
||||
filters={}, resource_name=None, sort_dir='desc',
|
||||
sort_keys=['resource_name', 'id'], stack_id='my_stack')
|
||||
self.assertEqual(self.fields, columns)
|
||||
|
||||
@mock.patch('osc_lib.utils.sort_items')
|
||||
def test_event_list_sort_default_key(self, mock_sort_items):
|
||||
arglist = ['my_stack', '--sort', ':desc',
|
||||
'--format', 'table']
|
||||
parsed_args = self.check_parser(self.cmd, arglist, [])
|
||||
|
||||
mock_event = self.MockEvent()
|
||||
mock_sort_items.return_value = [mock_event]
|
||||
columns, data = self.cmd.take_action(parsed_args)
|
||||
|
||||
mock_sort_items.assert_called_with(mock.ANY, "event_time:desc")
|
||||
self.event_client.list.assert_called_with(
|
||||
filters={}, resource_name=None, sort_dir='desc', sort_keys=[],
|
||||
stack_id='my_stack')
|
||||
self.assertEqual(self.fields, columns)
|
||||
|
||||
@mock.patch('time.sleep')
|
||||
def test_event_list_follow(self, sleep):
|
||||
sleep.side_effect = [None, KeyboardInterrupt()]
|
||||
arglist = ['--follow', 'my_stack']
|
||||
expected = (
|
||||
'2015-11-13 10:02:17 [resource1]: '
|
||||
'CREATE_COMPLETE state changed\n'
|
||||
'2015-11-13 10:02:17 [resource1]: '
|
||||
'CREATE_COMPLETE state changed\n'
|
||||
)
|
||||
parsed_args = self.check_parser(self.cmd, arglist, [])
|
||||
|
||||
columns, data = self.cmd.take_action(parsed_args)
|
||||
|
||||
defaults_with_marker = dict(self.defaults)
|
||||
defaults_with_marker['marker'] = '1234'
|
||||
|
||||
self.event_client.list.assert_has_calls([
|
||||
mock.call(**self.defaults),
|
||||
mock.call(**defaults_with_marker)
|
||||
])
|
||||
self.assertEqual([], columns)
|
||||
self.assertEqual([], data)
|
||||
self.assertEqual(expected, self.fake_stdout.make_string())
|
||||
|
||||
def test_event_list_log_format(self):
|
||||
arglist = ['my_stack']
|
||||
expected = ('2015-11-13 10:02:17 [resource1]: CREATE_COMPLETE '
|
||||
'state changed\n')
|
||||
parsed_args = self.check_parser(self.cmd, arglist, [])
|
||||
|
||||
self.cmd.run(parsed_args)
|
||||
|
||||
self.event_client.list.assert_called_with(**self.defaults)
|
||||
self.assertEqual(expected, self.fake_stdout.make_string())
|
|
@ -1,361 +0,0 @@
|
|||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
#
|
||||
|
||||
import copy
|
||||
|
||||
import mock
|
||||
from osc_lib import exceptions as exc
|
||||
|
||||
from heatclient import exc as heat_exc
|
||||
from heatclient.osc.v1 import resource
|
||||
from heatclient.tests.unit.osc.v1 import fakes as orchestration_fakes
|
||||
from heatclient.v1 import resources as v1_resources
|
||||
|
||||
|
||||
class TestResource(orchestration_fakes.TestOrchestrationv1):
|
||||
def setUp(self):
|
||||
super(TestResource, self).setUp()
|
||||
self.resource_client = self.app.client_manager.orchestration.resources
|
||||
|
||||
|
||||
class TestStackResourceShow(TestResource):
|
||||
|
||||
response = {
|
||||
'attributes': {},
|
||||
'creation_time': '2016-02-01T20:20:53',
|
||||
'description': 'a resource',
|
||||
'links': [
|
||||
{'rel': 'stack',
|
||||
"href": "http://heat.example.com:8004/my_stack/12"}
|
||||
],
|
||||
'logical_resource_id': 'my_resource',
|
||||
'physical_resource_id': '1234',
|
||||
'required_by': [],
|
||||
'resource_name': 'my_resource',
|
||||
'resource_status': 'CREATE_COMPLETE',
|
||||
'resource_status_reason': 'state changed',
|
||||
'resource_type': 'OS::Heat::None',
|
||||
'updated_time': '2016-02-01T20:20:53',
|
||||
}
|
||||
|
||||
def setUp(self):
|
||||
super(TestStackResourceShow, self).setUp()
|
||||
self.cmd = resource.ResourceShow(self.app, None)
|
||||
self.resource_client.get.return_value = v1_resources.Resource(
|
||||
None, self.response)
|
||||
|
||||
def test_resource_show(self):
|
||||
arglist = ['my_stack', 'my_resource']
|
||||
parsed_args = self.check_parser(self.cmd, arglist, [])
|
||||
|
||||
columns, data = self.cmd.take_action(parsed_args)
|
||||
|
||||
self.resource_client.get.assert_called_with('my_stack', 'my_resource',
|
||||
with_attr=None)
|
||||
for key in self.response:
|
||||
self.assertIn(key, columns)
|
||||
self.assertIn(self.response[key], data)
|
||||
|
||||
def test_resource_show_with_attr(self):
|
||||
arglist = ['my_stack', 'my_resource',
|
||||
'--with-attr', 'foo', '--with-attr', 'bar']
|
||||
parsed_args = self.check_parser(self.cmd, arglist, [])
|
||||
|
||||
columns, data = self.cmd.take_action(parsed_args)
|
||||
|
||||
self.resource_client.get.assert_called_with('my_stack', 'my_resource',
|
||||
with_attr=['foo', 'bar'])
|
||||
for key in self.response:
|
||||
self.assertIn(key, columns)
|
||||
self.assertIn(self.response[key], data)
|
||||
|
||||
def test_resource_show_not_found(self):
|
||||
arglist = ['my_stack', 'bad_resource']
|
||||
self.resource_client.get.side_effect = heat_exc.HTTPNotFound
|
||||
parsed_args = self.check_parser(self.cmd, arglist, [])
|
||||
|
||||
error = self.assertRaises(exc.CommandError,
|
||||
self.cmd.take_action, parsed_args)
|
||||
self.assertEqual('Stack or resource not found: my_stack bad_resource',
|
||||
str(error))
|
||||
|
||||
|
||||
class TestStackResourceList(TestResource):
|
||||
|
||||
response = {
|
||||
'attributes': {},
|
||||
'creation_time': '2016-02-01T20:20:53',
|
||||
'description': 'a resource',
|
||||
'links': [
|
||||
{'rel': 'stack',
|
||||
"href": "http://heat.example.com:8004/my_stack/12"}
|
||||
],
|
||||
'logical_resource_id': '1234',
|
||||
'physical_resource_id': '1234',
|
||||
'required_by': [],
|
||||
'resource_name': 'my_resource',
|
||||
'resource_status': 'CREATE_COMPLETE',
|
||||
'resource_status_reason': 'state changed',
|
||||
'resource_type': 'OS::Heat::None',
|
||||
'updated_time': '2016-02-01T20:20:53',
|
||||
}
|
||||
|
||||
columns = ['resource_name', 'physical_resource_id', 'resource_type',
|
||||
'resource_status', 'updated_time']
|
||||
|
||||
data = ['my_resource', '1234', 'OS::Heat::None',
|
||||
'CREATE_COMPLETE', '2016-02-01T20:20:53']
|
||||
|
||||
def setUp(self):
|
||||
super(TestStackResourceList, self).setUp()
|
||||
self.cmd = resource.ResourceList(self.app, None)
|
||||
self.resource_client.list.return_value = [
|
||||
v1_resources.Resource(None, self.response)]
|
||||
|
||||
def test_resource_list(self):
|
||||
arglist = ['my_stack']
|
||||
parsed_args = self.check_parser(self.cmd, arglist, [])
|
||||
|
||||
columns, data = self.cmd.take_action(parsed_args)
|
||||
|
||||
self.resource_client.list.assert_called_with(
|
||||
'my_stack',
|
||||
filters={},
|
||||
with_detail=False,
|
||||
nested_depth=None)
|
||||
self.assertEqual(self.columns, columns)
|
||||
self.assertEqual(tuple(self.data), list(data)[0])
|
||||
|
||||
def test_resource_list_not_found(self):
|
||||
arglist = ['bad_stack']
|
||||
self.resource_client.list.side_effect = heat_exc.HTTPNotFound
|
||||
parsed_args = self.check_parser(self.cmd, arglist, [])
|
||||
|
||||
self.assertRaises(exc.CommandError, self.cmd.take_action, parsed_args)
|
||||
|
||||
def test_resource_list_with_detail(self):
|
||||
arglist = ['my_stack', '--long']
|
||||
cols = copy.deepcopy(self.columns)
|
||||
cols.append('stack_name')
|
||||
out = copy.deepcopy(self.data)
|
||||
out.append('my_stack')
|
||||
parsed_args = self.check_parser(self.cmd, arglist, [])
|
||||
|
||||
columns, data = self.cmd.take_action(parsed_args)
|
||||
|
||||
self.resource_client.list.assert_called_with(
|
||||
'my_stack',
|
||||
filters={},
|
||||
with_detail=True,
|
||||
nested_depth=None)
|
||||
self.assertEqual(cols, columns)
|
||||
self.assertEqual(tuple(out), list(data)[0])
|
||||
|
||||
def test_resource_list_nested_depth(self):
|
||||
arglist = ['my_stack', '--nested-depth', '3']
|
||||
cols = copy.deepcopy(self.columns)
|
||||
cols.append('stack_name')
|
||||
out = copy.deepcopy(self.data)
|
||||
out.append('my_stack')
|
||||
parsed_args = self.check_parser(self.cmd, arglist, [])
|
||||
|
||||
columns, data = self.cmd.take_action(parsed_args)
|
||||
|
||||
self.resource_client.list.assert_called_with(
|
||||
'my_stack',
|
||||
filters={},
|
||||
with_detail=False,
|
||||
nested_depth=3)
|
||||
self.assertEqual(cols, columns)
|
||||
self.assertEqual(tuple(out), list(data)[0])
|
||||
|
||||
def test_resource_list_no_resource_name(self):
|
||||
arglist = ['my_stack']
|
||||
resp = copy.deepcopy(self.response)
|
||||
del resp['resource_name']
|
||||
cols = copy.deepcopy(self.columns)
|
||||
cols[0] = 'logical_resource_id'
|
||||
out = copy.deepcopy(self.data)
|
||||
out[1] = '1234'
|
||||
self.resource_client.list.return_value = [
|
||||
v1_resources.Resource(None, resp)]
|
||||
parsed_args = self.check_parser(self.cmd, arglist, [])
|
||||
|
||||
columns, data = self.cmd.take_action(parsed_args)
|
||||
|
||||
self.resource_client.list.assert_called_with(
|
||||
'my_stack',
|
||||
filters={},
|
||||
with_detail=False,
|
||||
nested_depth=None)
|
||||
self.assertEqual(cols, columns)
|
||||
|
||||
def test_resource_list_filter(self):
|
||||
arglist = ['my_stack', '--filter', 'name=my_resource']
|
||||
out = copy.deepcopy(self.data)
|
||||
parsed_args = self.check_parser(self.cmd, arglist, [])
|
||||
|
||||
columns, data = self.cmd.take_action(parsed_args)
|
||||
|
||||
self.resource_client.list.assert_called_with(
|
||||
'my_stack',
|
||||
filters=dict(name='my_resource'),
|
||||
with_detail=False,
|
||||
nested_depth=None)
|
||||
self.assertEqual(tuple(out), list(data)[0])
|
||||
|
||||
|
||||
class TestResourceMetadata(TestResource):
|
||||
|
||||
def setUp(self):
|
||||
super(TestResourceMetadata, self).setUp()
|
||||
self.cmd = resource.ResourceMetadata(self.app, None)
|
||||
self.resource_client.metadata.return_value = {}
|
||||
|
||||
def test_resource_metadata(self):
|
||||
arglist = ['my_stack', 'my_resource']
|
||||
parsed_args = self.check_parser(self.cmd, arglist, [])
|
||||
self.cmd.take_action(parsed_args)
|
||||
self.resource_client.metadata.assert_called_with(**{
|
||||
'stack_id': 'my_stack',
|
||||
'resource_name': 'my_resource'
|
||||
})
|
||||
|
||||
def test_resource_metadata_yaml(self):
|
||||
arglist = ['my_stack', 'my_resource', '--format', 'yaml']
|
||||
parsed_args = self.check_parser(self.cmd, arglist, [])
|
||||
self.cmd.take_action(parsed_args)
|
||||
self.resource_client.metadata.assert_called_with(**{
|
||||
'stack_id': 'my_stack',
|
||||
'resource_name': 'my_resource'
|
||||
})
|
||||
|
||||
def test_resource_metadata_error(self):
|
||||
arglist = ['my_stack', 'my_resource']
|
||||
parsed_args = self.check_parser(self.cmd, arglist, [])
|
||||
self.resource_client.metadata.side_effect = heat_exc.HTTPNotFound
|
||||
error = self.assertRaises(exc.CommandError,
|
||||
self.cmd.take_action,
|
||||
parsed_args)
|
||||
self.assertEqual('Stack my_stack or resource my_resource not found.',
|
||||
str(error))
|
||||
|
||||
|
||||
class TestResourceSignal(TestResource):
|
||||
|
||||
def setUp(self):
|
||||
super(TestResourceSignal, self).setUp()
|
||||
self.cmd = resource.ResourceSignal(self.app, None)
|
||||
|
||||
def test_resource_signal(self):
|
||||
arglist = ['my_stack', 'my_resource']
|
||||
parsed_args = self.check_parser(self.cmd, arglist, [])
|
||||
self.cmd.take_action(parsed_args)
|
||||
self.resource_client.signal.assert_called_with(**{
|
||||
'stack_id': 'my_stack',
|
||||
'resource_name': 'my_resource'
|
||||
})
|
||||
|
||||
def test_resource_signal_error(self):
|
||||
arglist = ['my_stack', 'my_resource']
|
||||
parsed_args = self.check_parser(self.cmd, arglist, [])
|
||||
self.resource_client.signal.side_effect = heat_exc.HTTPNotFound
|
||||
error = self.assertRaises(exc.CommandError,
|
||||
self.cmd.take_action,
|
||||
parsed_args)
|
||||
self.assertEqual('Stack my_stack or resource my_resource not found.',
|
||||
str(error))
|
||||
|
||||
def test_resource_signal_data(self):
|
||||
arglist = ['my_stack', 'my_resource',
|
||||
'--data', '{"message":"Content"}']
|
||||
parsed_args = self.check_parser(self.cmd, arglist, [])
|
||||
self.cmd.take_action(parsed_args)
|
||||
self.resource_client.signal.assert_called_with(**{
|
||||
'data': {u'message': u'Content'},
|
||||
'stack_id': 'my_stack',
|
||||
'resource_name': 'my_resource'
|
||||
})
|
||||
|
||||
def test_resource_signal_data_not_json(self):
|
||||
arglist = ['my_stack', 'my_resource', '--data', '{']
|
||||
parsed_args = self.check_parser(self.cmd, arglist, [])
|
||||
error = self.assertRaises(exc.CommandError,
|
||||
self.cmd.take_action,
|
||||
parsed_args)
|
||||
self.assertIn('Data should be in JSON format', str(error))
|
||||
|
||||
def test_resource_signal_data_and_file_error(self):
|
||||
arglist = ['my_stack', 'my_resource',
|
||||
'--data', '{}', '--data-file', 'file']
|
||||
parsed_args = self.check_parser(self.cmd, arglist, [])
|
||||
error = self.assertRaises(exc.CommandError,
|
||||
self.cmd.take_action,
|
||||
parsed_args)
|
||||
self.assertEqual('Should only specify one of data or data-file',
|
||||
str(error))
|
||||
|
||||
@mock.patch('six.moves.urllib.request.urlopen')
|
||||
def test_resource_signal_file(self, urlopen):
|
||||
data = mock.Mock()
|
||||
data.read.side_effect = ['{"message":"Content"}']
|
||||
urlopen.return_value = data
|
||||
|
||||
arglist = ['my_stack', 'my_resource', '--data-file', 'test_file']
|
||||
parsed_args = self.check_parser(self.cmd, arglist, [])
|
||||
self.cmd.take_action(parsed_args)
|
||||
self.resource_client.signal.assert_called_with(**{
|
||||
'data': {u'message': u'Content'},
|
||||
'stack_id': 'my_stack',
|
||||
'resource_name': 'my_resource'
|
||||
})
|
||||
|
||||
|
||||
class TestResourceMarkUnhealthy(TestResource):
|
||||
def setUp(self):
|
||||
super(TestResourceMarkUnhealthy, self).setUp()
|
||||
self.cmd = resource.ResourceMarkUnhealthy(self.app, None)
|
||||
self.resource_client.mark_unhealthy = mock.Mock()
|
||||
|
||||
def test_resource_mark_unhealthy(self):
|
||||
arglist = ['my_stack', 'my_resource', 'reason']
|
||||
parsed_args = self.check_parser(self.cmd, arglist, [])
|
||||
self.cmd.take_action(parsed_args)
|
||||
self.resource_client.mark_unhealthy.assert_called_with(**{
|
||||
"stack_id": "my_stack",
|
||||
"resource_name": "my_resource",
|
||||
"mark_unhealthy": True,
|
||||
"resource_status_reason": "reason"
|
||||
})
|
||||
|
||||
def test_resource_mark_unhealthy_reset(self):
|
||||
arglist = ['my_stack', 'my_resource', '--reset']
|
||||
parsed_args = self.check_parser(self.cmd, arglist, [])
|
||||
self.cmd.take_action(parsed_args)
|
||||
self.resource_client.mark_unhealthy.assert_called_with(**{
|
||||
"stack_id": "my_stack",
|
||||
"resource_name": "my_resource",
|
||||
"mark_unhealthy": False,
|
||||
"resource_status_reason": ""
|
||||
})
|
||||
|
||||
def test_resource_mark_unhealthy_not_found(self):
|
||||
arglist = ['my_stack', 'my_resource', '--reset']
|
||||
self.resource_client.mark_unhealthy.side_effect = (
|
||||
heat_exc.HTTPNotFound)
|
||||
parsed_args = self.check_parser(self.cmd, arglist, [])
|
||||
|
||||
error = self.assertRaises(exc.CommandError,
|
||||
self.cmd.take_action, parsed_args)
|
||||
self.assertEqual('Stack or resource not found: my_stack my_resource',
|
||||
str(error))
|
|
@ -1,178 +0,0 @@
|
|||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
#
|
||||
|
||||
from osc_lib import exceptions as exc
|
||||
|
||||
from heatclient import exc as heat_exc
|
||||
from heatclient.osc.v1 import resource_type
|
||||
from heatclient.tests.unit.osc.v1 import fakes as orchestration_fakes
|
||||
from heatclient.v1 import resource_types
|
||||
|
||||
|
||||
class TestResourceType(orchestration_fakes.TestOrchestrationv1):
|
||||
def setUp(self):
|
||||
super(TestResourceType, self).setUp()
|
||||
self.mock_client = self.app.client_manager.orchestration
|
||||
|
||||
|
||||
class TestResourceTypeShow(TestResourceType):
|
||||
|
||||
def setUp(self):
|
||||
super(TestResourceTypeShow, self).setUp()
|
||||
self.cmd = resource_type.ResourceTypeShow(self.app, None)
|
||||
self.mock_client.resource_types.get.return_value = {}
|
||||
self.mock_client.resource_types.generate_template.return_value = {}
|
||||
|
||||
def test_resourcetype_show(self):
|
||||
arglist = ['OS::Heat::None']
|
||||
parsed_args = self.check_parser(self.cmd, arglist, [])
|
||||
self.cmd.take_action(parsed_args)
|
||||
self.mock_client.resource_types.get.assert_called_once_with(
|
||||
'OS::Heat::None', False)
|
||||
|
||||
def test_resourcetype_show_json(self):
|
||||
arglist = ['OS::Heat::None',
|
||||
'--format', 'json']
|
||||
parsed_args = self.check_parser(self.cmd, arglist, [])
|
||||
self.cmd.take_action(parsed_args)
|
||||
self.mock_client.resource_types.get.assert_called_once_with(
|
||||
'OS::Heat::None', False)
|
||||
|
||||
def test_resourcetype_show_error_get(self):
|
||||
arglist = ['OS::Heat::None']
|
||||
parsed_args = self.check_parser(self.cmd, arglist, [])
|
||||
self.mock_client.resource_types.get.side_effect = heat_exc.HTTPNotFound
|
||||
self.assertRaises(exc.CommandError, self.cmd.take_action, parsed_args)
|
||||
|
||||
def test_resourcetype_show_error_template(self):
|
||||
arglist = ['OS::Heat::None',
|
||||
'--template-type', 'hot']
|
||||
parsed_args = self.check_parser(self.cmd, arglist, [])
|
||||
self.mock_client.resource_types.generate_template.side_effect = \
|
||||
heat_exc.HTTPNotFound
|
||||
self.assertRaises(exc.CommandError, self.cmd.take_action, parsed_args)
|
||||
|
||||
def test_resourcetype_show_template_hot(self):
|
||||
arglist = ['OS::Heat::None',
|
||||
'--template-type', 'Hot']
|
||||
parsed_args = self.check_parser(self.cmd, arglist, [])
|
||||
self.cmd.take_action(parsed_args)
|
||||
self.mock_client.resource_types.generate_template.assert_called_with(
|
||||
**{'resource_type': 'OS::Heat::None',
|
||||
'template_type': 'hot'})
|
||||
|
||||
def test_resourcetype_show_template_cfn(self):
|
||||
arglist = ['OS::Heat::None',
|
||||
'--template-type', 'cfn']
|
||||
parsed_args = self.check_parser(self.cmd, arglist, [])
|
||||
self.cmd.take_action(parsed_args)
|
||||
self.mock_client.resource_types.generate_template.assert_called_with(
|
||||
**{'resource_type': 'OS::Heat::None',
|
||||
'template_type': 'cfn'})
|
||||
|
||||
def test_resourcetype_show_template_cfn_yaml(self):
|
||||
arglist = ['OS::Heat::None',
|
||||
'--template-type', 'Cfn',
|
||||
'--format', 'yaml']
|
||||
parsed_args = self.check_parser(self.cmd, arglist, [])
|
||||
self.cmd.take_action(parsed_args)
|
||||
self.mock_client.resource_types.generate_template.assert_called_with(
|
||||
**{'resource_type': 'OS::Heat::None',
|
||||
'template_type': 'cfn'})
|
||||
|
||||
def test_resourcetype_show_invalid_template_type(self):
|
||||
arglist = ['OS::Heat::None',
|
||||
'--template-type', 'abc']
|
||||
parsed_args = self.check_parser(self.cmd, arglist, [])
|
||||
self.assertRaises(exc.CommandError, self.cmd.take_action, parsed_args)
|
||||
|
||||
def test_resourcetype_show_with_description(self):
|
||||
arglist = ['OS::Heat::None', '--long']
|
||||
parsed_args = self.check_parser(self.cmd, arglist, [])
|
||||
self.cmd.take_action(parsed_args)
|
||||
self.mock_client.resource_types.get.assert_called_with(
|
||||
'OS::Heat::None', True)
|
||||
|
||||
def test_resourcetype_show_long_and_template_type_error(self):
|
||||
arglist = ['OS::Heat::None',
|
||||
'--template-type', 'cfn',
|
||||
'--long']
|
||||
parsed_args = self.check_parser(self.cmd, arglist, [])
|
||||
self.assertRaises(exc.CommandError, self.cmd.take_action, parsed_args)
|
||||
|
||||
|
||||
class TestTypeList(TestResourceType):
|
||||
|
||||
expected_columns = ['Resource Type']
|
||||
list_response = [
|
||||
resource_types.ResourceType(None, {'resource_type': 'BBB',
|
||||
'description': 'This is BBB'}),
|
||||
resource_types.ResourceType(None, {'resource_type': 'AAA',
|
||||
'description': 'Well done'}),
|
||||
resource_types.ResourceType(None,
|
||||
{'resource_type': 'CCC',
|
||||
'description': 'No description given'})
|
||||
]
|
||||
expected_rows = [
|
||||
['AAA'],
|
||||
['BBB'],
|
||||
['CCC']
|
||||
]
|
||||
|
||||
def setUp(self):
|
||||
super(TestTypeList, self).setUp()
|
||||
self.cmd = resource_type.ResourceTypeList(self.app, None)
|
||||
self.mock_client.resource_types.list.return_value = self.list_response
|
||||
|
||||
def test_resourcetype_list(self):
|
||||
arglist = []
|
||||
parsed_args = self.check_parser(self.cmd, arglist, [])
|
||||
columns, rows = self.cmd.take_action(parsed_args)
|
||||
|
||||
self.mock_client.resource_types.list.assert_called_with(
|
||||
filters={}, with_description=False)
|
||||
self.assertEqual(self.expected_columns, columns)
|
||||
self.assertEqual(self.expected_rows, rows)
|
||||
|
||||
def test_resourcetype_list_filter(self):
|
||||
arglist = ['--filter', 'name=B']
|
||||
parsed_args = self.check_parser(self.cmd, arglist, [])
|
||||
columns, rows = self.cmd.take_action(parsed_args)
|
||||
|
||||
self.mock_client.resource_types.list.assert_called_once_with(
|
||||
filters={'name': 'B'}, with_description=False)
|
||||
self.assertEqual(self.expected_columns, columns)
|
||||
self.assertEqual(self.expected_rows, rows)
|
||||
|
||||
def test_resourcetype_list_filters(self):
|
||||
arglist = ['--filter', 'name=B', '--filter', 'version=123']
|
||||
parsed_args = self.check_parser(self.cmd, arglist, [])
|
||||
columns, rows = self.cmd.take_action(parsed_args)
|
||||
|
||||
self.mock_client.resource_types.list.assert_called_once_with(
|
||||
filters={'name': 'B', 'version': '123'}, with_description=False)
|
||||
self.assertEqual(self.expected_columns, columns)
|
||||
self.assertEqual(self.expected_rows, rows)
|
||||
|
||||
def test_resourcetype_list_with_description(self):
|
||||
arglist = ['--long']
|
||||
parsed_args = self.check_parser(self.cmd, arglist, [])
|
||||
columns, rows = self.cmd.take_action(parsed_args)
|
||||
|
||||
self.mock_client.resource_types.list.assert_called_once_with(
|
||||
filters={}, with_description=True)
|
||||
self.assertEqual(['Resource Type', 'Description'], columns)
|
||||
self.assertEqual([['AAA', 'Well done'],
|
||||
['BBB', 'This is BBB'],
|
||||
['CCC', 'No description given']],
|
||||
rows)
|
|
@ -1,62 +0,0 @@
|
|||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
#
|
||||
|
||||
from heatclient.osc.v1 import service as osc_service
|
||||
from heatclient.tests.unit.osc.v1 import fakes as orchestration_fakes
|
||||
|
||||
|
||||
class TestServiceList(orchestration_fakes.TestOrchestrationv1):
|
||||
response = {"services": [
|
||||
{
|
||||
"status": "up",
|
||||
"binary": "heat-engine",
|
||||
"report_interval": 60,
|
||||
"engine_id": "9d9242c3-4b9e-45e1-9e74-7615fbf20e5d",
|
||||
"created_at": "2015-02-03T05:55:59.000000",
|
||||
"hostname": "mrkanag",
|
||||
"updated_at": "2015-02-03T05:57:59.000000",
|
||||
"topic": "engine",
|
||||
"host": "engine-1",
|
||||
"deleted_at": 'null',
|
||||
"id": "e1908f44-42f9-483f-b778-bc814072c33d"
|
||||
},
|
||||
{
|
||||
"status": "down",
|
||||
"binary": "heat-engine",
|
||||
"report_interval": 60,
|
||||
"engine_id": "2d2434bf-adb6-4453-9c6b-b22fb8bd2306",
|
||||
"created_at": "2015-02-03T06:03:14.000000",
|
||||
"hostname": "mrkanag",
|
||||
"updated_at": "2015-02-03T06:09:55.000000",
|
||||
"topic": "engine",
|
||||
"host": "engine",
|
||||
"deleted_at": 'null',
|
||||
"id": "582b5657-6db7-48ad-8483-0096350faa21"
|
||||
}
|
||||
]}
|
||||
|
||||
columns = ['Hostname', 'Binary', 'Engine ID', 'Host',
|
||||
'Topic', 'Updated At', 'Status']
|
||||
|
||||
def setUp(self):
|
||||
super(TestServiceList, self).setUp()
|
||||
self.cmd = osc_service.ListService(self.app, None)
|
||||
self.mock_client = self.app.client_manager.orchestration
|
||||
self.mock_client.services.list.return_value = self.response
|
||||
|
||||
def test_service_list(self):
|
||||
arglist = []
|
||||
parsed_args = self.check_parser(self.cmd, arglist, [])
|
||||
columns, data = self.cmd.take_action(parsed_args)
|
||||
self.mock_client.services.list.assert_called_with()
|
||||
self.assertEqual(self.columns, columns)
|
|
@ -1,185 +0,0 @@
|
|||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
#
|
||||
import mock
|
||||
from osc_lib import exceptions as exc
|
||||
import six
|
||||
|
||||
from heatclient import exc as heat_exc
|
||||
from heatclient.osc.v1 import snapshot
|
||||
from heatclient.tests.unit.osc.v1 import fakes as orchestration_fakes
|
||||
|
||||
|
||||
class TestStack(orchestration_fakes.TestOrchestrationv1):
|
||||
def setUp(self):
|
||||
super(TestStack, self).setUp()
|
||||
self.mock_client = self.app.client_manager.orchestration
|
||||
self.stack_client = self.app.client_manager.orchestration.stacks
|
||||
|
||||
|
||||
class TestListSnapshot(TestStack):
|
||||
def setUp(self):
|
||||
super(TestListSnapshot, self).setUp()
|
||||
self.cmd = snapshot.ListSnapshot(self.app, None)
|
||||
self.stack_client.snapshot_list.return_value = {'snapshots': []}
|
||||
|
||||
def test_snapshot_list(self):
|
||||
arglist = ['my_stack']
|
||||
parsed_args = self.check_parser(self.cmd, arglist, [])
|
||||
self.cmd.take_action(parsed_args)
|
||||
self.stack_client.snapshot_list.assert_called_with(
|
||||
stack_id='my_stack')
|
||||
|
||||
def test_snapshot_list_error(self):
|
||||
self.stack_client.snapshot_list.side_effect = heat_exc.HTTPNotFound()
|
||||
arglist = ['my_stack']
|
||||
parsed_args = self.check_parser(self.cmd, arglist, [])
|
||||
error = self.assertRaises(
|
||||
exc.CommandError,
|
||||
self.cmd.take_action,
|
||||
parsed_args)
|
||||
self.assertEqual('Stack not found: my_stack',
|
||||
str(error))
|
||||
|
||||
|
||||
class TestSnapshotShow(TestStack):
|
||||
def setUp(self):
|
||||
super(TestSnapshotShow, self).setUp()
|
||||
self.cmd = snapshot.ShowSnapshot(self.app, None)
|
||||
|
||||
def test_snapshot_show(self):
|
||||
arglist = ['my_stack', 'snapshot_id']
|
||||
parsed_args = self.check_parser(self.cmd, arglist, [])
|
||||
self.stack_client.snapshot_show.return_value = {}
|
||||
self.cmd.take_action(parsed_args)
|
||||
self.stack_client.snapshot_show.assert_called_with(
|
||||
'my_stack', 'snapshot_id')
|
||||
|
||||
def test_snapshot_not_found(self):
|
||||
arglist = ['my_stack', 'snapshot_id']
|
||||
parsed_args = self.check_parser(self.cmd, arglist, [])
|
||||
self.stack_client.snapshot_show.side_effect = heat_exc.HTTPNotFound()
|
||||
self.assertRaises(
|
||||
exc.CommandError,
|
||||
self.cmd.take_action,
|
||||
parsed_args)
|
||||
|
||||
|
||||
class TestRestoreSnapshot(TestStack):
|
||||
def setUp(self):
|
||||
super(TestRestoreSnapshot, self).setUp()
|
||||
self.cmd = snapshot.RestoreSnapshot(self.app, None)
|
||||
|
||||
def test_snapshot_restore(self):
|
||||
arglist = ['my_stack', 'my_snapshot']
|
||||
parsed_args = self.check_parser(self.cmd, arglist, [])
|
||||
self.cmd.take_action(parsed_args)
|
||||
self.stack_client.restore.assert_called_with(
|
||||
snapshot_id='my_snapshot', stack_id='my_stack')
|
||||
|
||||
def test_snapshot_restore_error(self):
|
||||
self.stack_client.restore.side_effect = heat_exc.HTTPNotFound()
|
||||
arglist = ['my_stack', 'my_snapshot']
|
||||
parsed_args = self.check_parser(self.cmd, arglist, [])
|
||||
error = self.assertRaises(
|
||||
exc.CommandError,
|
||||
self.cmd.take_action,
|
||||
parsed_args)
|
||||
self.assertEqual('Stack my_stack or snapshot my_snapshot not found.',
|
||||
str(error))
|
||||
|
||||
|
||||
class TestSnapshotCreate(TestStack):
|
||||
get_response = {
|
||||
"status": "IN_PROGRESS",
|
||||
"name": "test_snapshot",
|
||||
"status_reason": None,
|
||||
"creation_time": "2015-11-09T04:35:38.534130",
|
||||
"data": None,
|
||||
"id": "108604fe-6d13-41b7-aa3a-79b6cf60c4ff"
|
||||
}
|
||||
|
||||
def setUp(self):
|
||||
super(TestSnapshotCreate, self).setUp()
|
||||
self.cmd = snapshot.CreateSnapshot(self.app, None)
|
||||
|
||||
def test_snapshot_create(self):
|
||||
arglist = ['my_stack', '--name', 'test_snapshot']
|
||||
parsed_args = self.check_parser(self.cmd, arglist, [])
|
||||
self.stack_client.snapshot.return_value = self.get_response
|
||||
self.cmd.take_action(parsed_args)
|
||||
self.stack_client.snapshot.assert_called_with(
|
||||
'my_stack', 'test_snapshot')
|
||||
|
||||
def test_snapshot_create_no_name(self):
|
||||
arglist = ['my_stack']
|
||||
parsed_args = self.check_parser(self.cmd, arglist, [])
|
||||
self.stack_client.snapshot.return_value = self.get_response
|
||||
self.cmd.take_action(parsed_args)
|
||||
self.stack_client.snapshot.assert_called_with(
|
||||
'my_stack', None)
|
||||
|
||||
def test_snapshot_create_error(self):
|
||||
arglist = ['my_stack', '--name', 'test_snapshot']
|
||||
parsed_args = self.check_parser(self.cmd, arglist, [])
|
||||
self.stack_client.snapshot.side_effect = heat_exc.HTTPNotFound
|
||||
self.assertRaises(
|
||||
exc.CommandError,
|
||||
self.cmd.take_action,
|
||||
parsed_args)
|
||||
|
||||
|
||||
class TestSnapshotDelete(TestStack):
|
||||
def setUp(self):
|
||||
super(TestSnapshotDelete, self).setUp()
|
||||
self.cmd = snapshot.DeleteSnapshot(self.app, None)
|
||||
|
||||
def test_snapshot_delete(self):
|
||||
arglist = ['my_stack', 'snapshot_id']
|
||||
parsed_args = self.check_parser(self.cmd, arglist, [])
|
||||
self.cmd.take_action(parsed_args)
|
||||
self.stack_client.snapshot_delete.assert_called_with(
|
||||
'my_stack', 'snapshot_id')
|
||||
|
||||
def test_snapshot_delete_not_found(self):
|
||||
arglist = ['my_stack', 'snapshot_id']
|
||||
parsed_args = self.check_parser(self.cmd, arglist, [])
|
||||
self.stack_client.snapshot_delete.side_effect = heat_exc.HTTPNotFound()
|
||||
self.assertRaises(
|
||||
exc.CommandError,
|
||||
self.cmd.take_action,
|
||||
parsed_args)
|
||||
|
||||
@mock.patch('sys.stdin', spec=six.StringIO)
|
||||
def test_snapshot_delete_prompt(self, mock_stdin):
|
||||
arglist = ['my_stack', 'snapshot_id']
|
||||
mock_stdin.isatty.return_value = True
|
||||
mock_stdin.readline.return_value = 'y'
|
||||
parsed_args = self.check_parser(self.cmd, arglist, [])
|
||||
|
||||
self.cmd.take_action(parsed_args)
|
||||
|
||||
mock_stdin.readline.assert_called_with()
|
||||
self.stack_client.snapshot_delete.assert_called_with('my_stack',
|
||||
'snapshot_id')
|
||||
|
||||
@mock.patch('sys.stdin', spec=six.StringIO)
|
||||
def test_snapshot_delete_prompt_no(self, mock_stdin):
|
||||
arglist = ['my_stack', 'snapshot_id']
|
||||
mock_stdin.isatty.return_value = True
|
||||
mock_stdin.readline.return_value = 'n'
|
||||
parsed_args = self.check_parser(self.cmd, arglist, [])
|
||||
|
||||
self.cmd.take_action(parsed_args)
|
||||
|
||||
mock_stdin.readline.assert_called_with()
|
||||
self.stack_client.snapshot_delete.assert_not_called()
|
|
@ -1,270 +0,0 @@
|
|||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
#
|
||||
|
||||
import mock
|
||||
from osc_lib import exceptions as exc
|
||||
import yaml
|
||||
|
||||
from heatclient import exc as heat_exc
|
||||
from heatclient.osc.v1 import software_config
|
||||
from heatclient.tests.unit.osc.v1 import fakes as orchestration_fakes
|
||||
from heatclient.v1 import software_configs
|
||||
|
||||
|
||||
class TestConfig(orchestration_fakes.TestOrchestrationv1):
|
||||
def setUp(self):
|
||||
super(TestConfig, self).setUp()
|
||||
self.mock_client = self.app.client_manager.orchestration
|
||||
|
||||
|
||||
class TestDeleteConfig(TestConfig):
|
||||
|
||||
def setUp(self):
|
||||
super(TestDeleteConfig, self).setUp()
|
||||
self.cmd = software_config.DeleteConfig(self.app, None)
|
||||
self.mock_delete = self.mock_client.software_configs.delete
|
||||
|
||||
def test_config_delete(self):
|
||||
arglist = ['id_123']
|
||||
parsed_args = self.check_parser(self.cmd, arglist, [])
|
||||
self.cmd.take_action(parsed_args)
|
||||
self.mock_delete.assert_called_with(
|
||||
config_id='id_123')
|
||||
|
||||
def test_config_delete_multi(self):
|
||||
arglist = ['id_123', 'id_456']
|
||||
parsed_args = self.check_parser(self.cmd, arglist, [])
|
||||
self.cmd.take_action(parsed_args)
|
||||
self.mock_delete.assert_has_calls(
|
||||
[mock.call(config_id='id_123'),
|
||||
mock.call(config_id='id_456')])
|
||||
|
||||
def test_config_delete_not_found(self):
|
||||
arglist = ['id_123', 'id_456', 'id_789']
|
||||
self.mock_client.software_configs.delete.side_effect = [
|
||||
None, heat_exc.HTTPNotFound, None]
|
||||
parsed_args = self.check_parser(self.cmd, arglist, [])
|
||||
error = self.assertRaises(exc.CommandError, self.cmd.take_action,
|
||||
parsed_args)
|
||||
self.mock_delete.assert_has_calls(
|
||||
[mock.call(config_id='id_123'),
|
||||
mock.call(config_id='id_456'),
|
||||
mock.call(config_id='id_789')])
|
||||
self.assertEqual('Unable to delete 1 of the 3 software configs.',
|
||||
str(error))
|
||||
|
||||
|
||||
class TestListConfig(TestConfig):
|
||||
|
||||
def setUp(self):
|
||||
super(TestListConfig, self).setUp()
|
||||
self.cmd = software_config.ListConfig(self.app, None)
|
||||
self.mock_client.software_configs.list.return_value = [
|
||||
software_configs.SoftwareConfig(None, {})]
|
||||
|
||||
def test_config_list(self):
|
||||
arglist = []
|
||||
parsed_args = self.check_parser(self.cmd, arglist, [])
|
||||
self.cmd.take_action(parsed_args)
|
||||
self.mock_client.software_configs.list.assert_called_once_with()
|
||||
|
||||
def test_config_list_limit(self):
|
||||
arglist = ['--limit', '3']
|
||||
parsed_args = self.check_parser(self.cmd, arglist, [])
|
||||
self.cmd.take_action(parsed_args)
|
||||
self.mock_client.software_configs.list.assert_called_with(limit='3')
|
||||
|
||||
def test_config_list_marker(self):
|
||||
arglist = ['--marker', 'id123']
|
||||
parsed_args = self.check_parser(self.cmd, arglist, [])
|
||||
self.cmd.take_action(parsed_args)
|
||||
self.mock_client.software_configs.list.assert_called_with(
|
||||
marker='id123')
|
||||
|
||||
|
||||
class TestCreateConfig(TestConfig):
|
||||
|
||||
def setUp(self):
|
||||
super(TestCreateConfig, self).setUp()
|
||||
self.cmd = software_config.CreateConfig(self.app, None)
|
||||
self.mock_client.software_configs.create.return_value = \
|
||||
software_configs.SoftwareConfig(None, {})
|
||||
|
||||
def test_config_create(self):
|
||||
properties = {
|
||||
'config': '',
|
||||
'group': 'Heat::Ungrouped',
|
||||
'name': 'test',
|
||||
'options': {},
|
||||
'inputs': [],
|
||||
'outputs': []
|
||||
}
|
||||
arglist = ['test']
|
||||
parsed_args = self.check_parser(self.cmd, arglist, [])
|
||||
columns, rows = self.cmd.take_action(parsed_args)
|
||||
self.mock_client.stacks.validate.assert_called_with(**{
|
||||
'template': {
|
||||
'heat_template_version': '2013-05-23',
|
||||
'resources': {
|
||||
'test': {
|
||||
'type': 'OS::Heat::SoftwareConfig',
|
||||
'properties': properties}}}})
|
||||
self.mock_client.software_configs.create.assert_called_with(
|
||||
**properties)
|
||||
|
||||
def test_config_create_group(self):
|
||||
properties = {
|
||||
'config': '',
|
||||
'group': 'group',
|
||||
'name': 'test',
|
||||
'options': {},
|
||||
'inputs': [],
|
||||
'outputs': []
|
||||
}
|
||||
arglist = ['test', '--group', 'group']
|
||||
parsed_args = self.check_parser(self.cmd, arglist, [])
|
||||
columns, rows = self.cmd.take_action(parsed_args)
|
||||
self.mock_client.stacks.validate.assert_called_with(**{
|
||||
'template': {
|
||||
'heat_template_version': '2013-05-23',
|
||||
'resources': {
|
||||
'test': {
|
||||
'type': 'OS::Heat::SoftwareConfig',
|
||||
'properties': properties}}}})
|
||||
self.mock_client.software_configs.create.assert_called_with(
|
||||
**properties)
|
||||
|
||||
@mock.patch('six.moves.urllib.request.urlopen')
|
||||
def test_config_create_config_file(self, urlopen):
|
||||
properties = {
|
||||
'config': 'config',
|
||||
'group': 'Heat::Ungrouped',
|
||||
'name': 'test',
|
||||
'options': {},
|
||||
'inputs': [],
|
||||
'outputs': []
|
||||
}
|
||||
data = mock.Mock()
|
||||
data.read.side_effect = ['config']
|
||||
urlopen.return_value = data
|
||||
|
||||
arglist = ['test', '--config-file', 'config_file']
|
||||
parsed_args = self.check_parser(self.cmd, arglist, [])
|
||||
columns, rows = self.cmd.take_action(parsed_args)
|
||||
self.mock_client.stacks.validate.assert_called_with(**{
|
||||
'template': {
|
||||
'heat_template_version': '2013-05-23',
|
||||
'resources': {
|
||||
'test': {
|
||||
'type': 'OS::Heat::SoftwareConfig',
|
||||
'properties': properties}}}})
|
||||
self.mock_client.software_configs.create.assert_called_with(
|
||||
**properties)
|
||||
|
||||
@mock.patch('six.moves.urllib.request.urlopen')
|
||||
def test_config_create_definition_file(self, urlopen):
|
||||
definition = {
|
||||
'inputs': [
|
||||
{'name': 'input'},
|
||||
],
|
||||
'outputs': [
|
||||
{'name': 'output'}
|
||||
],
|
||||
'options': {'option': 'value'}
|
||||
}
|
||||
|
||||
properties = {
|
||||
'config': '',
|
||||
'group': 'Heat::Ungrouped',
|
||||
'name': 'test'
|
||||
}
|
||||
properties.update(definition)
|
||||
|
||||
data = mock.Mock()
|
||||
data.read.side_effect = [yaml.safe_dump(definition)]
|
||||
urlopen.return_value = data
|
||||
|
||||
arglist = ['test', '--definition-file', 'definition-file']
|
||||
parsed_args = self.check_parser(self.cmd, arglist, [])
|
||||
columns, rows = self.cmd.take_action(parsed_args)
|
||||
self.mock_client.stacks.validate.assert_called_with(**{
|
||||
'template': {
|
||||
'heat_template_version': '2013-05-23',
|
||||
'resources': {
|
||||
'test': {
|
||||
'type': 'OS::Heat::SoftwareConfig',
|
||||
'properties': properties}}}})
|
||||
self.mock_client.software_configs.create.assert_called_with(
|
||||
**properties)
|
||||
|
||||
|
||||
class TestConfigShow(TestConfig):
|
||||
columns = (
|
||||
'id',
|
||||
'name',
|
||||
'group',
|
||||
'config',
|
||||
'inputs',
|
||||
'outputs',
|
||||
'options',
|
||||
'creation_time',
|
||||
)
|
||||
|
||||
data = (
|
||||
'96dfee3f-27b7-42ae-a03e-966226871ae6',
|
||||
'test',
|
||||
'Heat::Ungrouped',
|
||||
'',
|
||||
[],
|
||||
[],
|
||||
{},
|
||||
'2015-12-09T11:55:06',
|
||||
)
|
||||
|
||||
response = dict(zip(columns, data))
|
||||
|
||||
def setUp(self):
|
||||
super(TestConfigShow, self).setUp()
|
||||
self.cmd = software_config.ShowConfig(self.app, None)
|
||||
self.mock_client.software_configs.get.return_value = \
|
||||
software_configs.SoftwareConfig(None, self.response)
|
||||
|
||||
def test_config_show(self):
|
||||
arglist = ['96dfee3f-27b7-42ae-a03e-966226871ae6']
|
||||
parsed_args = self.check_parser(self.cmd, arglist, [])
|
||||
columns, data = self.cmd.take_action(parsed_args)
|
||||
self.mock_client.software_configs.get.assert_called_with(**{
|
||||
'config_id': '96dfee3f-27b7-42ae-a03e-966226871ae6',
|
||||
})
|
||||
self.assertEqual(self.columns, columns)
|
||||
self.assertEqual(self.data, data)
|
||||
|
||||
def test_config_show_config_only(self):
|
||||
arglist = ['--config-only', '96dfee3f-27b7-42ae-a03e-966226871ae6']
|
||||
parsed_args = self.check_parser(self.cmd, arglist, [])
|
||||
columns, data = self.cmd.take_action(parsed_args)
|
||||
self.mock_client.software_configs.get.assert_called_with(**{
|
||||
'config_id': '96dfee3f-27b7-42ae-a03e-966226871ae6',
|
||||
})
|
||||
self.assertIsNone(columns)
|
||||
self.assertIsNone(data)
|
||||
|
||||
def test_config_show_not_found(self):
|
||||
arglist = ['96dfee3f-27b7-42ae-a03e-966226871ae6']
|
||||
parsed_args = self.check_parser(self.cmd, arglist, [])
|
||||
self.mock_client.software_configs.get.side_effect = \
|
||||
heat_exc.HTTPNotFound()
|
||||
self.assertRaises(
|
||||
exc.CommandError,
|
||||
self.cmd.take_action,
|
||||
parsed_args)
|
|
@ -1,440 +0,0 @@
|
|||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
#
|
||||
|
||||
import copy
|
||||
|
||||
import mock
|
||||
from osc_lib import exceptions as exc
|
||||
|
||||
from heatclient import exc as heat_exc
|
||||
from heatclient.osc.v1 import software_deployment
|
||||
from heatclient.tests.unit.osc.v1 import fakes as orchestration_fakes
|
||||
from heatclient.v1 import software_configs
|
||||
from heatclient.v1 import software_deployments
|
||||
|
||||
|
||||
class TestDeployment(orchestration_fakes.TestOrchestrationv1):
|
||||
def setUp(self):
|
||||
super(TestDeployment, self).setUp()
|
||||
self.mock_client = self.app.client_manager.orchestration
|
||||
self.config_client = self.mock_client.software_configs
|
||||
self.sd_client = self.mock_client.software_deployments
|
||||
|
||||
|
||||
class TestDeploymentCreate(TestDeployment):
|
||||
|
||||
server_id = '1234'
|
||||
config_id = '5678'
|
||||
deploy_id = '910'
|
||||
|
||||
config = {
|
||||
'name': 'my_deploy',
|
||||
'group': 'strict',
|
||||
'config': '#!/bin/bash',
|
||||
'inputs': [],
|
||||
'outputs': [],
|
||||
'options': [],
|
||||
'id': config_id,
|
||||
}
|
||||
|
||||
deployment = {
|
||||
'server_id': server_id,
|
||||
'input_values': {},
|
||||
'action': 'UPDATE',
|
||||
'status': 'IN_PROGRESS',
|
||||
'status_reason': None,
|
||||
'signal_id': 'signal_id',
|
||||
'config_id': config_id,
|
||||
'id': deploy_id,
|
||||
}
|
||||
|
||||
config_defaults = {
|
||||
'group': 'Heat::Ungrouped',
|
||||
'config': '',
|
||||
'options': {},
|
||||
'inputs': [
|
||||
{
|
||||
'name': 'deploy_server_id',
|
||||
'description': 'ID of the server being deployed to',
|
||||
'type': 'String',
|
||||
'value': server_id,
|
||||
},
|
||||
{
|
||||
'name': 'deploy_action',
|
||||
'description': 'Name of the current action being deployed',
|
||||
'type': 'String',
|
||||
'value': 'UPDATE',
|
||||
},
|
||||
{
|
||||
'name': 'deploy_signal_transport',
|
||||
'description': 'How the server should signal to heat with the '
|
||||
'deployment output values.',
|
||||
'type': 'String',
|
||||
'value': 'TEMP_URL_SIGNAL',
|
||||
},
|
||||
{
|
||||
'name': 'deploy_signal_id',
|
||||
'description': 'ID of signal to use for signaling output '
|
||||
'values',
|
||||
'type': 'String',
|
||||
'value': 'signal_id',
|
||||
},
|
||||
{
|
||||
'name': 'deploy_signal_verb',
|
||||
'description': 'HTTP verb to use for signaling output values',
|
||||
'type': 'String',
|
||||
'value': 'PUT',
|
||||
},
|
||||
],
|
||||
'outputs': [],
|
||||
'name': 'my_deploy',
|
||||
}
|
||||
|
||||
deploy_defaults = {
|
||||
'config_id': config_id,
|
||||
'server_id': server_id,
|
||||
'action': 'UPDATE',
|
||||
'status': 'IN_PROGRESS',
|
||||
}
|
||||
|
||||
def setUp(self):
|
||||
super(TestDeploymentCreate, self).setUp()
|
||||
self.cmd = software_deployment.CreateDeployment(self.app, None)
|
||||
self.config_client.create.return_value = \
|
||||
software_configs.SoftwareConfig(None, self.config)
|
||||
self.config_client.get.return_value = \
|
||||
software_configs.SoftwareConfig(None, self.config)
|
||||
self.sd_client.create.return_value = \
|
||||
software_deployments.SoftwareDeployment(None, self.deployment)
|
||||
|
||||
@mock.patch('heatclient.common.deployment_utils.build_signal_id',
|
||||
return_value='signal_id')
|
||||
def test_deployment_create(self, mock_build):
|
||||
arglist = ['my_deploy', '--server', self.server_id]
|
||||
expected_cols = ('action', 'config_id', 'id', 'input_values',
|
||||
'server_id', 'signal_id', 'status', 'status_reason')
|
||||
expected_data = ('UPDATE', self.config_id, self.deploy_id, {},
|
||||
self.server_id, 'signal_id', 'IN_PROGRESS', None)
|
||||
parsed_args = self.check_parser(self.cmd, arglist, [])
|
||||
|
||||
columns, data = self.cmd.take_action(parsed_args)
|
||||
|
||||
self.config_client.create.assert_called_with(**self.config_defaults)
|
||||
self.sd_client.create.assert_called_with(
|
||||
**self.deploy_defaults)
|
||||
self.assertEqual(expected_cols, columns)
|
||||
self.assertEqual(expected_data, data)
|
||||
|
||||
@mock.patch('heatclient.common.deployment_utils.build_signal_id',
|
||||
return_value='signal_id')
|
||||
def test_deployment_create_with_config(self, mock_build):
|
||||
arglist = ['my_deploy', '--server', self.server_id,
|
||||
'--config', self.config_id]
|
||||
config = copy.deepcopy(self.config_defaults)
|
||||
config['config'] = '#!/bin/bash'
|
||||
config['group'] = 'strict'
|
||||
parsed_args = self.check_parser(self.cmd, arglist, [])
|
||||
|
||||
columns, data = self.cmd.take_action(parsed_args)
|
||||
|
||||
self.config_client.get.assert_called_with(self.config_id)
|
||||
self.config_client.create.assert_called_with(**config)
|
||||
self.sd_client.create.assert_called_with(
|
||||
**self.deploy_defaults)
|
||||
|
||||
def test_deployment_create_config_not_found(self):
|
||||
arglist = ['my_deploy', '--server', self.server_id,
|
||||
'--config', 'bad_id']
|
||||
self.config_client.get.side_effect = heat_exc.HTTPNotFound
|
||||
parsed_args = self.check_parser(self.cmd, arglist, [])
|
||||
|
||||
self.assertRaises(exc.CommandError, self.cmd.take_action, parsed_args)
|
||||
|
||||
def test_deployment_create_no_signal(self):
|
||||
arglist = ['my_deploy', '--server', self.server_id,
|
||||
'--signal-transport', 'NO_SIGNAL']
|
||||
config = copy.deepcopy(self.config_defaults)
|
||||
config['inputs'] = config['inputs'][:-2]
|
||||
config['inputs'][2]['value'] = 'NO_SIGNAL'
|
||||
parsed_args = self.check_parser(self.cmd, arglist, [])
|
||||
|
||||
columns, data = self.cmd.take_action(parsed_args)
|
||||
|
||||
self.config_client.create.assert_called_with(**config)
|
||||
self.sd_client.create.assert_called_with(
|
||||
**self.deploy_defaults)
|
||||
|
||||
@mock.patch('heatclient.common.deployment_utils.build_signal_id',
|
||||
return_value='signal_id')
|
||||
def test_deployment_create_invalid_signal_transport(self, mock_build):
|
||||
arglist = ['my_deploy', '--server', self.server_id,
|
||||
'--signal-transport', 'A']
|
||||
parsed_args = self.check_parser(self.cmd, arglist, [])
|
||||
|
||||
self.assertRaises(heat_exc.CommandError,
|
||||
self.cmd.take_action, parsed_args)
|
||||
|
||||
@mock.patch('heatclient.common.deployment_utils.build_signal_id',
|
||||
return_value='signal_id')
|
||||
def test_deployment_create_input_value(self, mock_build):
|
||||
arglist = ['my_deploy', '--server', self.server_id,
|
||||
'--input-value', 'foo=bar']
|
||||
config = copy.deepcopy(self.config_defaults)
|
||||
config['inputs'].insert(
|
||||
0, {'name': 'foo', 'type': 'String', 'value': 'bar'})
|
||||
parsed_args = self.check_parser(self.cmd, arglist, [])
|
||||
|
||||
columns, data = self.cmd.take_action(parsed_args)
|
||||
|
||||
self.config_client.create.assert_called_with(**config)
|
||||
self.sd_client.create.assert_called_with(
|
||||
**self.deploy_defaults)
|
||||
|
||||
@mock.patch('heatclient.common.deployment_utils.build_signal_id',
|
||||
return_value='signal_id')
|
||||
def test_deployment_create_action(self, mock_build):
|
||||
arglist = ['my_deploy', '--server', self.server_id,
|
||||
'--action', 'DELETE']
|
||||
config = copy.deepcopy(self.config_defaults)
|
||||
config['inputs'][1]['value'] = 'DELETE'
|
||||
deploy = copy.deepcopy(self.deploy_defaults)
|
||||
deploy['action'] = 'DELETE'
|
||||
parsed_args = self.check_parser(self.cmd, arglist, [])
|
||||
|
||||
columns, data = self.cmd.take_action(parsed_args)
|
||||
|
||||
self.config_client.create.assert_called_with(**config)
|
||||
self.sd_client.create.assert_called_with(**deploy)
|
||||
|
||||
|
||||
class TestDeploymentDelete(TestDeployment):
|
||||
|
||||
def setUp(self):
|
||||
super(TestDeploymentDelete, self).setUp()
|
||||
self.cmd = software_deployment.DeleteDeployment(self.app, None)
|
||||
|
||||
def test_deployment_delete_success(self):
|
||||
arglist = ['test_deployment']
|
||||
parsed_args = self.check_parser(self.cmd, arglist, [])
|
||||
self.cmd.take_action(parsed_args)
|
||||
self.sd_client.delete.assert_called_with(
|
||||
deployment_id='test_deployment')
|
||||
|
||||
def test_deployment_delete_multiple(self):
|
||||
arglist = ['test_deployment', 'test_deployment2']
|
||||
parsed_args = self.check_parser(self.cmd, arglist, [])
|
||||
self.cmd.take_action(parsed_args)
|
||||
self.sd_client.delete.assert_has_calls(
|
||||
[mock.call(deployment_id='test_deployment'),
|
||||
mock.call(deployment_id='test_deployment2')])
|
||||
|
||||
def test_deployment_delete_not_found(self):
|
||||
arglist = ['test_deployment', 'test_deployment2']
|
||||
parsed_args = self.check_parser(self.cmd, arglist, [])
|
||||
self.sd_client.delete.side_effect = heat_exc.HTTPNotFound()
|
||||
error = self.assertRaises(
|
||||
exc.CommandError, self.cmd.take_action, parsed_args)
|
||||
self.assertIn("Unable to delete 2 of the 2 deployments.", str(error))
|
||||
|
||||
def test_deployment_config_delete_failed(self):
|
||||
arglist = ['test_deployment']
|
||||
parsed_args = self.check_parser(self.cmd, arglist, [])
|
||||
self.config_client.delete.side_effect = heat_exc.HTTPNotFound()
|
||||
self.assertIsNone(self.cmd.take_action(parsed_args))
|
||||
|
||||
|
||||
class TestDeploymentList(TestDeployment):
|
||||
|
||||
columns = ['id', 'config_id', 'server_id', 'action', 'status']
|
||||
|
||||
data = {"software_deployments": [
|
||||
{
|
||||
"status": "COMPLETE",
|
||||
"server_id": "ec14c864-096e-4e27-bb8a-2c2b4dc6f3f5",
|
||||
"config_id": "8da95794-2ad9-4979-8ae5-739ce314c5cd",
|
||||
"output_values": {
|
||||
"deploy_stdout": "Writing to /tmp/barmy Written to /tmp/barmy",
|
||||
"deploy_stderr": "+ echo Writing to /tmp/barmy\n+ echo fu\n+ c"
|
||||
"at /tmp/barmy\n+ echo -n The file /tmp/barmy"
|
||||
"contains for server ec14c864-096e-4e27-bb8a-"
|
||||
"2c2b4dc6f3f5 during CREATE\n+"
|
||||
"echo Output to stderr\nOutput to stderr\n",
|
||||
"deploy_status_code": 0,
|
||||
"result": "The file /tmp/barmy contains fu for server "
|
||||
"ec14c864-096e-4e27-bb8a-2c2b4dc6f3f5 during CREATE"
|
||||
},
|
||||
"input_values": None,
|
||||
"action": "CREATE",
|
||||
"status_reason": "Outputs received",
|
||||
"id": "ef422fa5-719a-419e-a10c-72e3a367b0b8",
|
||||
"creation_time": "2015-01-31T15:12:36Z",
|
||||
"updated_time": "2015-01-31T15:18:21Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
def setUp(self):
|
||||
super(TestDeploymentList, self).setUp()
|
||||
self.cmd = software_deployment.ListDeployment(self.app, None)
|
||||
self.sd_client.list = mock.MagicMock(return_value=[self.data])
|
||||
|
||||
def test_deployment_list(self):
|
||||
arglist = []
|
||||
parsed_args = self.check_parser(self.cmd, arglist, [])
|
||||
columns, data = self.cmd.take_action(parsed_args)
|
||||
self.sd_client.list.assert_called_with()
|
||||
self.assertEqual(self.columns, columns)
|
||||
|
||||
def test_deployment_list_server(self):
|
||||
kwargs = {}
|
||||
kwargs['server_id'] = 'ec14c864-096e-4e27-bb8a-2c2b4dc6f3f5'
|
||||
arglist = ['--server', 'ec14c864-096e-4e27-bb8a-2c2b4dc6f3f5']
|
||||
parsed_args = self.check_parser(self.cmd, arglist, [])
|
||||
columns, data = self.cmd.take_action(parsed_args)
|
||||
self.sd_client.list.assert_called_with(**kwargs)
|
||||
self.assertEqual(self.columns, columns)
|
||||
|
||||
def test_deployment_list_long(self):
|
||||
kwargs = {}
|
||||
cols = ['id', 'config_id', 'server_id', 'action', 'status',
|
||||
'creation_time', 'status_reason']
|
||||
arglist = ['--long']
|
||||
parsed_args = self.check_parser(self.cmd, arglist, [])
|
||||
|
||||
columns, data = self.cmd.take_action(parsed_args)
|
||||
|
||||
self.sd_client.list.assert_called_with(**kwargs)
|
||||
self.assertEqual(cols, columns)
|
||||
|
||||
|
||||
class TestDeploymentShow(TestDeployment):
|
||||
get_response = {"software_deployment": {
|
||||
"status": "IN_PROGRESS",
|
||||
"server_id": "ec14c864-096e-4e27-bb8a-2c2b4dc6f3f5",
|
||||
"config_id": "3d5ec2a8-7004-43b6-a7f6-542bdbe9d434",
|
||||
"output_values": 'null',
|
||||
"input_values": 'null',
|
||||
"action": "CREATE",
|
||||
"status_reason": "Deploy data available",
|
||||
"id": "06e87bcc-33a2-4bce-aebd-533e698282d3",
|
||||
"creation_time": "2015-01-31T15:12:36Z",
|
||||
"updated_time": "2015-01-31T15:18:21Z"
|
||||
}}
|
||||
|
||||
def setUp(self):
|
||||
super(TestDeploymentShow, self).setUp()
|
||||
self.cmd = software_deployment.ShowDeployment(self.app, None)
|
||||
|
||||
def test_deployment_show(self):
|
||||
arglist = ['my_deployment']
|
||||
cols = ['id', 'server_id', 'config_id', 'creation_time',
|
||||
'updated_time', 'status', 'status_reason',
|
||||
'input_values', 'action']
|
||||
parsed_args = self.check_parser(self.cmd, arglist, [])
|
||||
self.sd_client.get.return_value = \
|
||||
software_deployments.SoftwareDeployment(
|
||||
None, self.get_response)
|
||||
columns, data = self.cmd.take_action(parsed_args)
|
||||
self.sd_client.get.assert_called_with(**{
|
||||
'deployment_id': 'my_deployment',
|
||||
})
|
||||
self.assertEqual(cols, columns)
|
||||
|
||||
def test_deployment_show_long(self):
|
||||
arglist = ['my_deployment', '--long']
|
||||
cols = ['id', 'server_id', 'config_id', 'creation_time',
|
||||
'updated_time', 'status', 'status_reason',
|
||||
'input_values', 'action', 'output_values']
|
||||
parsed_args = self.check_parser(self.cmd, arglist, [])
|
||||
self.sd_client.get.return_value = \
|
||||
software_deployments.SoftwareDeployment(
|
||||
None, self.get_response)
|
||||
columns, data = self.cmd.take_action(parsed_args)
|
||||
self.sd_client.get.assert_called_once_with(**{
|
||||
'deployment_id': 'my_deployment',
|
||||
})
|
||||
self.assertEqual(cols, columns)
|
||||
|
||||
def test_deployment_not_found(self):
|
||||
arglist = ['my_deployment']
|
||||
parsed_args = self.check_parser(self.cmd, arglist, [])
|
||||
self.sd_client.get.side_effect = heat_exc.HTTPNotFound()
|
||||
self.assertRaises(
|
||||
exc.CommandError,
|
||||
self.cmd.take_action,
|
||||
parsed_args)
|
||||
|
||||
|
||||
class TestDeploymentMetadataShow(TestDeployment):
|
||||
|
||||
def setUp(self):
|
||||
super(TestDeploymentMetadataShow, self).setUp()
|
||||
self.cmd = software_deployment.ShowMetadataDeployment(self.app, None)
|
||||
self.sd_client.metadata.return_value = {}
|
||||
|
||||
def test_deployment_show_metadata(self):
|
||||
arglist = ['ec14c864-096e-4e27-bb8a-2c2b4dc6f3f5']
|
||||
parsed_args = self.check_parser(self.cmd, arglist, [])
|
||||
self.cmd.take_action(parsed_args)
|
||||
self.sd_client.metadata.assert_called_with(
|
||||
server_id='ec14c864-096e-4e27-bb8a-2c2b4dc6f3f5')
|
||||
|
||||
|
||||
class TestDeploymentOutputShow(TestDeployment):
|
||||
|
||||
get_response = {
|
||||
"status": "IN_PROGRESS",
|
||||
"server_id": "ec14c864-096e-4e27-bb8a-2c2b4dc6f3f5",
|
||||
"config_id": "3d5ec2a8-7004-43b6-a7f6-542bdbe9d434",
|
||||
"output_values": None,
|
||||
"input_values": None,
|
||||
"action": "CREATE",
|
||||
"status_reason": "Deploy data available",
|
||||
"id": "06e87bcc-33a2-4bce-aebd-533e698282d3",
|
||||
"creation_time": "2015-01-31T15:12:36Z",
|
||||
"updated_time": "2015-01-31T15:18:21Z"
|
||||
}
|
||||
|
||||
def setUp(self):
|
||||
super(TestDeploymentOutputShow, self).setUp()
|
||||
self.cmd = software_deployment.ShowOutputDeployment(self.app, None)
|
||||
|
||||
def test_deployment_output_show(self):
|
||||
arglist = ['85c3a507-351b-4b28-a7d8-531c8d53f4e6', '--all', '--long']
|
||||
parsed_args = self.check_parser(self.cmd, arglist, [])
|
||||
self.sd_client.get.return_value = \
|
||||
software_deployments.SoftwareDeployment(
|
||||
None, self.get_response)
|
||||
self.cmd.take_action(parsed_args)
|
||||
self.sd_client.get.assert_called_with(**{
|
||||
'deployment_id': '85c3a507-351b-4b28-a7d8-531c8d53f4e6'
|
||||
})
|
||||
|
||||
def test_deployment_output_show_invalid(self):
|
||||
arglist = ['85c3a507-351b-4b28-a7d8-531c8d53f4e6']
|
||||
parsed_args = self.check_parser(self.cmd, arglist, [])
|
||||
error = self.assertRaises(
|
||||
exc.CommandError,
|
||||
self.cmd.take_action,
|
||||
parsed_args)
|
||||
self.assertIn('either <output-name> or --all argument is needed',
|
||||
str(error))
|
||||
|
||||
def test_deployment_output_show_not_found(self):
|
||||
arglist = ['85c3a507-351b-4b28-a7d8-531c8d53f4e6', '--all']
|
||||
parsed_args = self.check_parser(self.cmd, arglist, [])
|
||||
self.sd_client.get.side_effect = heat_exc.HTTPNotFound()
|
||||
self.assertRaises(
|
||||
exc.CommandError,
|
||||
self.cmd.take_action,
|
||||
parsed_args)
|
File diff suppressed because it is too large
Load Diff
|
@ -1,261 +0,0 @@
|
|||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
# implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import collections
|
||||
|
||||
import mock
|
||||
|
||||
from heatclient import exc
|
||||
from heatclient.osc.v1 import stack_failures
|
||||
from heatclient.tests.unit.osc.v1 import fakes as orchestration_fakes
|
||||
|
||||
|
||||
class ListStackFailuresTest(orchestration_fakes.TestOrchestrationv1):
|
||||
|
||||
def setUp(self):
|
||||
super(ListStackFailuresTest, self).setUp()
|
||||
self.cmd = stack_failures.ListStackFailures(self.app, None)
|
||||
self.cmd.heat_client = self.app.client_manager.orchestration
|
||||
self.stack_client = self.app.client_manager.orchestration.stacks
|
||||
self.resource_client = self.app.client_manager.orchestration.resources
|
||||
self.software_deployments_client = \
|
||||
self.app.client_manager.orchestration.software_deployments
|
||||
|
||||
self.stack = mock.MagicMock(id='123', status='FAILED',
|
||||
stack_name='stack')
|
||||
self.stack_client.get.return_value = self.stack
|
||||
self.failed_template_resource = mock.MagicMock(
|
||||
physical_resource_id='aaaa',
|
||||
resource_type='My::TemplateResource',
|
||||
resource_status='CREATE_FAILED',
|
||||
links=[{'rel': 'nested'}],
|
||||
resource_name='my_templateresource',
|
||||
resource_status_reason='All gone Pete Tong',
|
||||
logical_resource_id='my_templateresource',
|
||||
)
|
||||
self.failed_resource = mock.MagicMock(
|
||||
physical_resource_id='cccc',
|
||||
resource_type='OS::Nova::Server',
|
||||
resource_status='CREATE_FAILED',
|
||||
links=[],
|
||||
resource_name='my_server',
|
||||
resource_status_reason='All gone Pete Tong',
|
||||
logical_resource_id='my_server',
|
||||
)
|
||||
self.other_failed_template_resource = mock.MagicMock(
|
||||
physical_resource_id='dddd',
|
||||
resource_type='My::OtherTemplateResource',
|
||||
resource_status='CREATE_FAILED',
|
||||
links=[{'rel': 'nested'}],
|
||||
resource_name='my_othertemplateresource',
|
||||
resource_status_reason='RPC timeout',
|
||||
logical_resource_id='my_othertemplateresource',
|
||||
)
|
||||
self.working_resource = mock.MagicMock(
|
||||
physical_resource_id='bbbb',
|
||||
resource_type='OS::Nova::Server',
|
||||
resource_status='CREATE_COMPLETE',
|
||||
resource_name='my_server',
|
||||
)
|
||||
self.failed_deployment_resource = mock.MagicMock(
|
||||
physical_resource_id='eeee',
|
||||
resource_type='OS::Heat::SoftwareDeployment',
|
||||
resource_status='CREATE_FAILED',
|
||||
links=[],
|
||||
resource_name='my_deployment',
|
||||
resource_status_reason='Returned deploy_statuscode 1',
|
||||
logical_resource_id='my_deployment',
|
||||
)
|
||||
self.failed_deployment = mock.MagicMock(
|
||||
id='eeee',
|
||||
output_values={
|
||||
'deploy_statuscode': '1',
|
||||
'deploy_stderr': 'It broke',
|
||||
'deploy_stdout': ('1\n2\n3\n4\n5\n6\n7\n8\n9\n10'
|
||||
'\n11\n12')
|
||||
},
|
||||
)
|
||||
self.software_deployments_client.get.return_value = (
|
||||
self.failed_deployment)
|
||||
|
||||
def test_build_failed_none(self):
|
||||
self.stack = mock.MagicMock(id='123', status='COMPLETE',
|
||||
stack_name='stack')
|
||||
failures = self.cmd._build_failed_resources('stack')
|
||||
expected = collections.OrderedDict()
|
||||
self.assertEqual(expected, failures)
|
||||
|
||||
def test_build_failed_resources(self):
|
||||
self.resource_client.list.side_effect = [[
|
||||
# resource-list stack
|
||||
self.failed_template_resource,
|
||||
self.other_failed_template_resource,
|
||||
self.working_resource,
|
||||
], [ # resource-list aaaa
|
||||
self.failed_resource
|
||||
], [ # resource-list dddd
|
||||
]]
|
||||
failures = self.cmd._build_failed_resources('stack')
|
||||
expected = collections.OrderedDict()
|
||||
expected['stack.my_templateresource.my_server'] = self.failed_resource
|
||||
expected['stack.my_othertemplateresource'] = (
|
||||
self.other_failed_template_resource)
|
||||
self.assertEqual(expected, failures)
|
||||
|
||||
def test_build_failed_resources_not_found(self):
|
||||
self.resource_client.list.side_effect = [[
|
||||
# resource-list stack
|
||||
self.failed_template_resource,
|
||||
self.other_failed_template_resource,
|
||||
self.working_resource,
|
||||
], exc.HTTPNotFound(), [ # resource-list dddd
|
||||
]]
|
||||
|
||||
failures = self.cmd._build_failed_resources('stack')
|
||||
expected = collections.OrderedDict()
|
||||
expected['stack.my_templateresource'] = self.failed_template_resource
|
||||
expected['stack.my_othertemplateresource'] = (
|
||||
self.other_failed_template_resource)
|
||||
self.assertEqual(expected, failures)
|
||||
|
||||
def test_build_software_deployments(self):
|
||||
resources = {
|
||||
'stack.my_server': self.working_resource,
|
||||
'stack.my_deployment': self.failed_deployment_resource
|
||||
}
|
||||
deployments = self.cmd._build_software_deployments(resources)
|
||||
self.assertEqual({
|
||||
'eeee': self.failed_deployment
|
||||
}, deployments)
|
||||
|
||||
def test_build_software_deployments_not_found(self):
|
||||
resources = {
|
||||
'stack.my_server': self.working_resource,
|
||||
'stack.my_deployment': self.failed_deployment_resource
|
||||
}
|
||||
self.software_deployments_client.get.side_effect = exc.HTTPNotFound()
|
||||
deployments = self.cmd._build_software_deployments(resources)
|
||||
self.assertEqual({}, deployments)
|
||||
|
||||
def test_build_software_deployments_no_resources(self):
|
||||
resources = {}
|
||||
self.software_deployments_client.get.side_effect = exc.HTTPNotFound()
|
||||
deployments = self.cmd._build_software_deployments(resources)
|
||||
self.assertEqual({}, deployments)
|
||||
|
||||
def test_list_stack_failures(self):
|
||||
self.resource_client.list.side_effect = [[
|
||||
# resource-list stack
|
||||
self.failed_template_resource,
|
||||
self.other_failed_template_resource,
|
||||
self.working_resource,
|
||||
self.failed_deployment_resource
|
||||
], [ # resource-list aaaa
|
||||
self.failed_resource
|
||||
], [ # resource-list dddd
|
||||
]]
|
||||
|
||||
arglist = ['stack']
|
||||
parsed_args = self.check_parser(self.cmd, arglist, [])
|
||||
self.cmd.take_action(parsed_args)
|
||||
|
||||
self.assertEqual(
|
||||
self.app.stdout.make_string(),
|
||||
'''stack.my_templateresource.my_server:
|
||||
resource_type: OS::Nova::Server
|
||||
physical_resource_id: cccc
|
||||
status: CREATE_FAILED
|
||||
status_reason: |
|
||||
All gone Pete Tong
|
||||
stack.my_othertemplateresource:
|
||||
resource_type: My::OtherTemplateResource
|
||||
physical_resource_id: dddd
|
||||
status: CREATE_FAILED
|
||||
status_reason: |
|
||||
RPC timeout
|
||||
stack.my_deployment:
|
||||
resource_type: OS::Heat::SoftwareDeployment
|
||||
physical_resource_id: eeee
|
||||
status: CREATE_FAILED
|
||||
status_reason: |
|
||||
Returned deploy_statuscode 1
|
||||
deploy_stdout: |
|
||||
...
|
||||
3
|
||||
4
|
||||
5
|
||||
6
|
||||
7
|
||||
8
|
||||
9
|
||||
10
|
||||
11
|
||||
12
|
||||
(truncated, view all with --long)
|
||||
deploy_stderr: |
|
||||
It broke
|
||||
''')
|
||||
|
||||
def test_list_stack_failures_long(self):
|
||||
self.resource_client.list.side_effect = [[
|
||||
# resource-list stack
|
||||
self.failed_template_resource,
|
||||
self.other_failed_template_resource,
|
||||
self.working_resource,
|
||||
self.failed_deployment_resource
|
||||
], [ # resource-list aaaa
|
||||
self.failed_resource
|
||||
], [ # resource-list dddd
|
||||
]]
|
||||
|
||||
arglist = ['--long', 'stack']
|
||||
parsed_args = self.check_parser(self.cmd, arglist, [])
|
||||
self.cmd.take_action(parsed_args)
|
||||
|
||||
self.assertEqual(
|
||||
self.app.stdout.make_string(),
|
||||
'''stack.my_templateresource.my_server:
|
||||
resource_type: OS::Nova::Server
|
||||
physical_resource_id: cccc
|
||||
status: CREATE_FAILED
|
||||
status_reason: |
|
||||
All gone Pete Tong
|
||||
stack.my_othertemplateresource:
|
||||
resource_type: My::OtherTemplateResource
|
||||
physical_resource_id: dddd
|
||||
status: CREATE_FAILED
|
||||
status_reason: |
|
||||
RPC timeout
|
||||
stack.my_deployment:
|
||||
resource_type: OS::Heat::SoftwareDeployment
|
||||
physical_resource_id: eeee
|
||||
status: CREATE_FAILED
|
||||
status_reason: |
|
||||
Returned deploy_statuscode 1
|
||||
deploy_stdout: |
|
||||
1
|
||||
2
|
||||
3
|
||||
4
|
||||
5
|
||||
6
|
||||
7
|
||||
8
|
||||
9
|
||||
10
|
||||
11
|
||||
12
|
||||
deploy_stderr: |
|
||||
It broke
|
||||
''')
|
|
@ -1,182 +0,0 @@
|
|||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
#
|
||||
# Copyright 2015 IBM Corp.
|
||||
|
||||
import mock
|
||||
|
||||
from heatclient import exc
|
||||
from heatclient.osc.v1 import template
|
||||
from heatclient.tests.unit.osc.v1 import fakes
|
||||
from heatclient.v1 import template_versions
|
||||
|
||||
|
||||
class TestTemplate(fakes.TestOrchestrationv1):
|
||||
def setUp(self):
|
||||
super(TestTemplate, self).setUp()
|
||||
self.mock_client = self.app.client_manager.orchestration
|
||||
self.template_versions = self.mock_client.template_versions
|
||||
|
||||
|
||||
class TestTemplateVersionList(TestTemplate):
|
||||
|
||||
def _stub_versions_list(self, ret_data):
|
||||
tv1 = template_versions.TemplateVersion(None, ret_data[0])
|
||||
tv2 = template_versions.TemplateVersion(None, ret_data[1])
|
||||
self.template_versions.list.return_value = [tv1, tv2]
|
||||
self.cmd = template.VersionList(self.app, None)
|
||||
|
||||
def test_version_list(self):
|
||||
ret_data = [
|
||||
{'version': 'HOT123', 'type': 'hot'},
|
||||
{'version': 'CFN456', 'type': 'cfn'}]
|
||||
self._stub_versions_list(ret_data)
|
||||
parsed_args = self.check_parser(self.cmd, [], [])
|
||||
|
||||
columns, data = self.cmd.take_action(parsed_args)
|
||||
|
||||
self.assertEqual(['Version', 'Type'], columns)
|
||||
self.assertEqual([('HOT123', 'hot'), ('CFN456', 'cfn')], list(data))
|
||||
|
||||
def test_version_list_with_aliases(self):
|
||||
ret_data = [
|
||||
{'version': 'HOT123', 'type': 'hot', 'aliases': ['releasex']},
|
||||
{'version': 'CFN456', 'type': 'cfn', 'aliases': ['releasey']}]
|
||||
self._stub_versions_list(ret_data)
|
||||
parsed_args = self.check_parser(self.cmd, [], [])
|
||||
|
||||
columns, data = self.cmd.take_action(parsed_args)
|
||||
|
||||
self.assertEqual(['Version', 'Type', 'Aliases'], columns)
|
||||
self.assertEqual([('HOT123', 'hot', 'releasex'),
|
||||
('CFN456', 'cfn', 'releasey')], list(data))
|
||||
|
||||
|
||||
class TestTemplateFunctionList(TestTemplate):
|
||||
|
||||
defaults = [
|
||||
{'functions': 'func1', 'description': 'Function 1'},
|
||||
{'functions': 'func2', 'description': 'Function 2'},
|
||||
{'functions': 'condition func', 'description': 'Condition Function'}
|
||||
]
|
||||
|
||||
def setUp(self):
|
||||
super(TestTemplateFunctionList, self).setUp()
|
||||
self.tv1 = template_versions.TemplateVersion(None, self.defaults[0])
|
||||
self.tv2 = template_versions.TemplateVersion(None, self.defaults[1])
|
||||
self.tv_with_cf = template_versions.TemplateVersion(
|
||||
None, self.defaults[2])
|
||||
|
||||
self.cmd = template.FunctionList(self.app, None)
|
||||
|
||||
def test_function_list(self):
|
||||
arglist = ['version1']
|
||||
parsed_args = self.check_parser(self.cmd, arglist, [])
|
||||
self.template_versions.get.return_value = [self.tv1, self.tv2]
|
||||
columns, data = self.cmd.take_action(parsed_args)
|
||||
|
||||
self.assertEqual(['Functions', 'Description'], columns)
|
||||
self.assertEqual([('func1', 'Function 1'), ('func2', 'Function 2')],
|
||||
list(data))
|
||||
|
||||
def test_function_list_with_condition_func(self):
|
||||
arglist = ['version1', '--with_conditions']
|
||||
parsed_args = self.check_parser(self.cmd, arglist, [])
|
||||
self.template_versions.get.return_value = [self.tv1, self.tv2,
|
||||
self.tv_with_cf]
|
||||
columns, data = self.cmd.take_action(parsed_args)
|
||||
|
||||
self.assertEqual(['Functions', 'Description'], columns)
|
||||
self.assertEqual([('func1', 'Function 1'),
|
||||
('func2', 'Function 2'),
|
||||
('condition func', 'Condition Function')],
|
||||
list(data))
|
||||
|
||||
def test_function_list_not_found(self):
|
||||
arglist = ['bad_version']
|
||||
self.template_versions.get.side_effect = exc.HTTPNotFound
|
||||
parsed_args = self.check_parser(self.cmd, arglist, [])
|
||||
|
||||
self.assertRaises(exc.CommandError, self.cmd.take_action, parsed_args)
|
||||
|
||||
|
||||
class TestTemplateValidate(TestTemplate):
|
||||
|
||||
template_path = 'heatclient/tests/test_templates/empty.yaml'
|
||||
env_path = 'heatclient/tests/unit/var/environment.json'
|
||||
|
||||
defaults = {
|
||||
'environment': {},
|
||||
'files': {},
|
||||
'parameters': {},
|
||||
'template': {'heat_template_version': '2013-05-23'}
|
||||
}
|
||||
|
||||
def setUp(self):
|
||||
super(TestTemplateValidate, self).setUp()
|
||||
self.stack_client = self.app.client_manager.orchestration.stacks
|
||||
self.stack_client.validate = mock.MagicMock(return_value={})
|
||||
self.cmd = template.Validate(self.app, None)
|
||||
|
||||
def test_validate(self):
|
||||
arglist = ['-t', self.template_path]
|
||||
parsed_args = self.check_parser(self.cmd, arglist, [])
|
||||
columns, data = self.cmd.take_action(parsed_args)
|
||||
|
||||
self.stack_client.validate.assert_called_once_with(**self.defaults)
|
||||
self.assertEqual([], columns)
|
||||
self.assertEqual([], data)
|
||||
|
||||
def test_validate_env(self):
|
||||
arglist = ['-t', self.template_path, '-e', self.env_path]
|
||||
parsed_args = self.check_parser(self.cmd, arglist, [])
|
||||
columns, data = self.cmd.take_action(parsed_args)
|
||||
|
||||
self.assertEqual(1, self.stack_client.validate.call_count)
|
||||
args = self.stack_client.validate.call_args[1]
|
||||
self.assertEqual(args.get('environment'), {'parameters': {}})
|
||||
self.assertIn(self.env_path, args.get('environment_files')[0])
|
||||
self.assertEqual([], columns)
|
||||
self.assertEqual([], data)
|
||||
|
||||
def test_validate_nested(self):
|
||||
arglist = ['-t', self.template_path, '--show-nested']
|
||||
parsed_args = self.check_parser(self.cmd, arglist, [])
|
||||
columns, data = self.cmd.take_action(parsed_args)
|
||||
args = dict(self.defaults)
|
||||
args['show_nested'] = True
|
||||
self.stack_client.validate.assert_called_once_with(**args)
|
||||
self.assertEqual([], columns)
|
||||
self.assertEqual([], data)
|
||||
|
||||
def test_validate_parameter(self):
|
||||
arglist = ['-t', self.template_path,
|
||||
'--parameter', 'key1=value1',
|
||||
'--parameter', 'key2=value2']
|
||||
parsed_args = self.check_parser(self.cmd, arglist, [])
|
||||
columns, data = self.cmd.take_action(parsed_args)
|
||||
args = dict(self.defaults)
|
||||
args['parameters'] = {'key1': 'value1', 'key2': 'value2'}
|
||||
self.stack_client.validate.assert_called_once_with(**args)
|
||||
self.assertEqual([], columns)
|
||||
self.assertEqual([], data)
|
||||
|
||||
def test_validate_ignore_errors(self):
|
||||
arglist = ['-t', self.template_path,
|
||||
'--ignore-errors', 'err1,err2']
|
||||
parsed_args = self.check_parser(self.cmd, arglist, [])
|
||||
columns, data = self.cmd.take_action(parsed_args)
|
||||
args = dict(self.defaults)
|
||||
args['ignore_errors'] = 'err1,err2'
|
||||
self.stack_client.validate.assert_called_once_with(**args)
|
||||
self.assertEqual([], columns)
|
||||
self.assertEqual([], data)
|
|
@ -1,108 +0,0 @@
|
|||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
# implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import testtools
|
||||
|
||||
from heatclient.tests.unit import fakes
|
||||
from heatclient.v1 import actions
|
||||
|
||||
|
||||
class ActionManagerTest(testtools.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(ActionManagerTest, self).setUp()
|
||||
|
||||
def _base_test(self, expect_args, expect_kwargs):
|
||||
|
||||
class FakeAPI(object):
|
||||
"""Fake API and ensure request url is correct."""
|
||||
|
||||
def json_request(self, *args, **kwargs):
|
||||
assert expect_args == args
|
||||
assert expect_kwargs['data'] == kwargs['data']
|
||||
return fakes.FakeHTTPResponse(
|
||||
'200',
|
||||
'',
|
||||
{'content-type': 'application/json'},
|
||||
{}), {}
|
||||
|
||||
def raw_request(self, *args, **kwargs):
|
||||
assert expect_args == args
|
||||
return fakes.FakeHTTPResponse(
|
||||
'200',
|
||||
'',
|
||||
{},
|
||||
{})
|
||||
|
||||
def head(self, url, **kwargs):
|
||||
resp, body = self.json_request("HEAD", url, **kwargs)
|
||||
return resp
|
||||
|
||||
def get(self, url, **kwargs):
|
||||
resp, body = self.json_request("GET", url, **kwargs)
|
||||
return resp
|
||||
|
||||
def post(self, url, **kwargs):
|
||||
resp, body = self.json_request("POST", url, **kwargs)
|
||||
return resp
|
||||
|
||||
def put(self, url, **kwargs):
|
||||
resp, body = self.json_request("PUT", url, **kwargs)
|
||||
return resp
|
||||
|
||||
def delete(self, url, **kwargs):
|
||||
resp, body = self.raw_request("DELETE", url, **kwargs)
|
||||
return resp
|
||||
|
||||
def patch(self, url, **kwargs):
|
||||
resp, body = self.json_request("PATCH", url, **kwargs)
|
||||
return resp
|
||||
|
||||
manager = actions.ActionManager(FakeAPI())
|
||||
return manager
|
||||
|
||||
def test_suspend(self):
|
||||
fields = {'stack_id': 'teststack%2Fabcd1234'}
|
||||
expect_args = ('POST',
|
||||
'/stacks/teststack%2Fabcd1234/actions')
|
||||
expect_kwargs = {'data': {'suspend': None}}
|
||||
|
||||
manager = self._base_test(expect_args, expect_kwargs)
|
||||
manager.suspend(**fields)
|
||||
|
||||
def test_resume(self):
|
||||
fields = {'stack_id': 'teststack%2Fabcd1234'}
|
||||
expect_args = ('POST',
|
||||
'/stacks/teststack%2Fabcd1234/actions')
|
||||
expect_kwargs = {'data': {'resume': None}}
|
||||
|
||||
manager = self._base_test(expect_args, expect_kwargs)
|
||||
manager.resume(**fields)
|
||||
|
||||
def test_cancel_update(self):
|
||||
fields = {'stack_id': 'teststack%2Fabcd1234'}
|
||||
expect_args = ('POST',
|
||||
'/stacks/teststack%2Fabcd1234/actions')
|
||||
expect_kwargs = {'data': {'cancel_update': None}}
|
||||
|
||||
manager = self._base_test(expect_args, expect_kwargs)
|
||||
manager.cancel_update(**fields)
|
||||
|
||||
def test_check(self):
|
||||
fields = {'stack_id': 'teststack%2Fabcd1234'}
|
||||
expect_args = ('POST',
|
||||
'/stacks/teststack%2Fabcd1234/actions')
|
||||
expect_kwargs = {'data': {'check': None}}
|
||||
|
||||
manager = self._base_test(expect_args, expect_kwargs)
|
||||
manager.check(**fields)
|
|
@ -1,42 +0,0 @@
|
|||
# Copyright 2012 OpenStack Foundation
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import mock
|
||||
from oslo_serialization import jsonutils
|
||||
import testtools
|
||||
|
||||
from heatclient.tests.unit import fakes
|
||||
from heatclient.v1 import build_info
|
||||
|
||||
|
||||
class BuildInfoManagerTest(testtools.TestCase):
|
||||
def setUp(self):
|
||||
super(BuildInfoManagerTest, self).setUp()
|
||||
self.client = mock.Mock()
|
||||
self.client.get.return_value = fakes.FakeHTTPResponse(
|
||||
200,
|
||||
None,
|
||||
{'content-type': 'application/json'},
|
||||
jsonutils.dumps('body')
|
||||
)
|
||||
self.manager = build_info.BuildInfoManager(self.client)
|
||||
|
||||
def test_build_info_makes_a_call_to_the_api(self):
|
||||
self.manager.build_info()
|
||||
self.client.get.assert_called_once_with('/build_info')
|
||||
|
||||
def test_build_info_returns_the_response_body(self):
|
||||
response = self.manager.build_info()
|
||||
self.assertEqual('body', response)
|
|
@ -1,799 +0,0 @@
|
|||
# -*- coding:utf-8 -*-
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
# implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import socket
|
||||
|
||||
from keystoneauth1 import adapter
|
||||
import mock
|
||||
from oslo_serialization import jsonutils
|
||||
import six
|
||||
import testtools
|
||||
|
||||
from heatclient.common import http
|
||||
from heatclient.common import utils
|
||||
from heatclient import exc
|
||||
from heatclient.tests.unit import fakes
|
||||
|
||||
|
||||
@mock.patch('heatclient.common.http.requests.request')
|
||||
class HttpClientTest(testtools.TestCase):
|
||||
|
||||
def test_http_raw_request(self, mock_request):
|
||||
headers = {'Content-Type': 'application/octet-stream',
|
||||
'User-Agent': 'python-heatclient'}
|
||||
|
||||
# Record a 200
|
||||
mock_request.return_value = fakes.FakeHTTPResponse(
|
||||
200, 'OK',
|
||||
{'content-type': 'application/octet-stream'},
|
||||
'')
|
||||
# Replay, create client, assert
|
||||
client = http.HTTPClient('http://example.com:8004')
|
||||
resp = client.raw_request('GET', '')
|
||||
self.assertEqual(200, resp.status_code)
|
||||
self.assertEqual('', ''.join([x for x in resp.content]))
|
||||
mock_request.assert_called_with('GET', 'http://example.com:8004',
|
||||
allow_redirects=False,
|
||||
headers=headers)
|
||||
|
||||
def test_token_or_credentials(self, mock_request):
|
||||
# Record a 200
|
||||
fake200 = fakes.FakeHTTPResponse(
|
||||
200, 'OK',
|
||||
{'content-type': 'application/octet-stream'},
|
||||
'')
|
||||
|
||||
mock_request.return_value = fake200
|
||||
# no token or credentials
|
||||
client = http.HTTPClient('http://example.com:8004')
|
||||
resp = client.raw_request('GET', '')
|
||||
self.assertEqual(200, resp.status_code)
|
||||
# credentials
|
||||
client.username = 'user'
|
||||
client.password = 'pass'
|
||||
resp = client.raw_request('GET', '')
|
||||
self.assertEqual(200, resp.status_code)
|
||||
# token suppresses credentials
|
||||
client.auth_token = 'abcd1234'
|
||||
resp = client.raw_request('GET', '')
|
||||
self.assertEqual(200, resp.status_code)
|
||||
mock_request.assert_has_calls([
|
||||
mock.call('GET', 'http://example.com:8004',
|
||||
allow_redirects=False,
|
||||
headers={'Content-Type': 'application/octet-stream',
|
||||
'User-Agent': 'python-heatclient'}),
|
||||
mock.call('GET', 'http://example.com:8004',
|
||||
allow_redirects=False,
|
||||
headers={'Content-Type': 'application/octet-stream',
|
||||
'User-Agent': 'python-heatclient',
|
||||
'X-Auth-Key': 'pass',
|
||||
'X-Auth-User': 'user'}),
|
||||
mock.call('GET', 'http://example.com:8004',
|
||||
allow_redirects=False,
|
||||
headers={'Content-Type': 'application/octet-stream',
|
||||
'User-Agent': 'python-heatclient',
|
||||
'X-Auth-Token': 'abcd1234'})
|
||||
])
|
||||
|
||||
def test_include_pass(self, mock_request):
|
||||
# Record a 200
|
||||
fake200 = fakes.FakeHTTPResponse(
|
||||
200, 'OK',
|
||||
{'content-type': 'application/octet-stream'},
|
||||
'')
|
||||
mock_request.return_value = fake200
|
||||
# no token or credentials
|
||||
client = http.HTTPClient('http://example.com:8004')
|
||||
resp = client.raw_request('GET', '')
|
||||
self.assertEqual(200, resp.status_code)
|
||||
# credentials
|
||||
client.username = 'user'
|
||||
client.password = 'pass'
|
||||
client.include_pass = True
|
||||
resp = client.raw_request('GET', '')
|
||||
self.assertEqual(200, resp.status_code)
|
||||
# token suppresses credentials
|
||||
client.auth_token = 'abcd1234'
|
||||
resp = client.raw_request('GET', '')
|
||||
self.assertEqual(200, resp.status_code)
|
||||
mock_request.assert_has_calls([
|
||||
mock.call('GET', 'http://example.com:8004',
|
||||
allow_redirects=False,
|
||||
headers={'Content-Type': 'application/octet-stream',
|
||||
'User-Agent': 'python-heatclient'}),
|
||||
mock.call('GET', 'http://example.com:8004',
|
||||
allow_redirects=False,
|
||||
headers={'Content-Type': 'application/octet-stream',
|
||||
'User-Agent': 'python-heatclient',
|
||||
'X-Auth-Key': 'pass',
|
||||
'X-Auth-User': 'user'}),
|
||||
mock.call('GET', 'http://example.com:8004',
|
||||
allow_redirects=False,
|
||||
headers={'Content-Type': 'application/octet-stream',
|
||||
'User-Agent': 'python-heatclient',
|
||||
'X-Auth-Token': 'abcd1234',
|
||||
'X-Auth-Key': 'pass',
|
||||
'X-Auth-User': 'user'})
|
||||
])
|
||||
|
||||
def test_not_include_pass(self, mock_request):
|
||||
# Record a 200
|
||||
fake500 = fakes.FakeHTTPResponse(
|
||||
500, 'ERROR',
|
||||
{'content-type': 'application/octet-stream'},
|
||||
'(HTTP 401)')
|
||||
|
||||
# no token or credentials
|
||||
mock_request.return_value = fake500
|
||||
|
||||
# Replay, create client, assert
|
||||
client = http.HTTPClient('http://example.com:8004')
|
||||
e = self.assertRaises(exc.HTTPUnauthorized,
|
||||
client.raw_request, 'GET', '')
|
||||
self.assertIn('Authentication failed', str(e))
|
||||
mock_request.assert_called_with(
|
||||
'GET', 'http://example.com:8004',
|
||||
allow_redirects=False,
|
||||
headers={'Content-Type': 'application/octet-stream',
|
||||
'User-Agent': 'python-heatclient'})
|
||||
|
||||
def test_region_name(self, mock_request):
|
||||
# Record a 200
|
||||
fake200 = fakes.FakeHTTPResponse(
|
||||
200, 'OK',
|
||||
{'content-type': 'application/octet-stream'},
|
||||
'')
|
||||
|
||||
# Specify region name
|
||||
mock_request.return_value = fake200
|
||||
|
||||
# Replay, create client, assert
|
||||
client = http.HTTPClient('http://example.com:8004')
|
||||
client.region_name = 'RegionOne'
|
||||
resp = client.raw_request('GET', '')
|
||||
self.assertEqual(200, resp.status_code)
|
||||
mock_request.assert_called_with(
|
||||
'GET', 'http://example.com:8004',
|
||||
allow_redirects=False,
|
||||
headers={'Content-Type': 'application/octet-stream',
|
||||
'X-Region-Name': 'RegionOne',
|
||||
'User-Agent': 'python-heatclient'})
|
||||
|
||||
def test_http_json_request(self, mock_request):
|
||||
# Record a 200
|
||||
mock_request.return_value = fakes.FakeHTTPResponse(
|
||||
200, 'OK',
|
||||
{'content-type': 'application/json'},
|
||||
'{}')
|
||||
# Replay, create client, assert
|
||||
client = http.HTTPClient('http://example.com:8004')
|
||||
resp, body = client.json_request('GET', '')
|
||||
self.assertEqual(200, resp.status_code)
|
||||
self.assertEqual({}, body)
|
||||
mock_request.assert_called_with(
|
||||
'GET', 'http://example.com:8004',
|
||||
allow_redirects=False,
|
||||
headers={'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'User-Agent': 'python-heatclient'})
|
||||
|
||||
def test_http_json_request_argument_passed_to_requests(self, mock_request):
|
||||
"""Check that we have sent the proper arguments to requests."""
|
||||
# Record a 200
|
||||
mock_request.return_value = fakes.FakeHTTPResponse(
|
||||
200, 'OK',
|
||||
{'content-type': 'application/json'},
|
||||
'{}')
|
||||
|
||||
client = http.HTTPClient('http://example.com:8004')
|
||||
client.verify_cert = True
|
||||
client.cert_file = 'RANDOM_CERT_FILE'
|
||||
client.key_file = 'RANDOM_KEY_FILE'
|
||||
client.auth_url = 'http://AUTH_URL'
|
||||
resp, body = client.json_request('GET', '', data='text')
|
||||
self.assertEqual(200, resp.status_code)
|
||||
self.assertEqual({}, body)
|
||||
mock_request.assert_called_with(
|
||||
'GET', 'http://example.com:8004',
|
||||
allow_redirects=False,
|
||||
cert=('RANDOM_CERT_FILE', 'RANDOM_KEY_FILE'),
|
||||
verify=True,
|
||||
data='"text"',
|
||||
headers={'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'X-Auth-Url': 'http://AUTH_URL',
|
||||
'User-Agent': 'python-heatclient'})
|
||||
|
||||
def test_http_json_request_w_req_body(self, mock_request):
|
||||
# Record a 200
|
||||
mock_request.return_value = fakes.FakeHTTPResponse(
|
||||
200, 'OK',
|
||||
{'content-type': 'application/json'},
|
||||
'{}')
|
||||
# Replay, create client, assert
|
||||
client = http.HTTPClient('http://example.com:8004')
|
||||
resp, body = client.json_request('GET', '', body='test-body')
|
||||
self.assertEqual(200, resp.status_code)
|
||||
mock_request.assert_called_with(
|
||||
'GET', 'http://example.com:8004',
|
||||
body='test-body',
|
||||
allow_redirects=False,
|
||||
headers={'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'User-Agent': 'python-heatclient'})
|
||||
|
||||
def test_http_json_request_non_json_resp_cont_type(self, mock_request):
|
||||
# Record a 200i
|
||||
mock_request.return_value = fakes.FakeHTTPResponse(
|
||||
200, 'OK',
|
||||
{'content-type': 'not/json'},
|
||||
'{}')
|
||||
# Replay, create client, assert
|
||||
client = http.HTTPClient('http://example.com:8004')
|
||||
resp, body = client.json_request('GET', '', body='test-body')
|
||||
self.assertEqual(200, resp.status_code)
|
||||
self.assertIsNone(body)
|
||||
mock_request.assert_called_with(
|
||||
'GET', 'http://example.com:8004',
|
||||
body='test-body',
|
||||
allow_redirects=False,
|
||||
headers={'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'User-Agent': 'python-heatclient'})
|
||||
|
||||
def test_http_json_request_invalid_json(self, mock_request):
|
||||
# Record a 200
|
||||
mock_request.return_value = fakes.FakeHTTPResponse(
|
||||
200, 'OK',
|
||||
{'content-type': 'application/json'},
|
||||
'invalid-json')
|
||||
|
||||
# Replay, create client, assert
|
||||
client = http.HTTPClient('http://example.com:8004')
|
||||
resp, body = client.json_request('GET', '')
|
||||
self.assertEqual(200, resp.status_code)
|
||||
self.assertEqual('invalid-json', body)
|
||||
mock_request.assert_called_with(
|
||||
'GET', 'http://example.com:8004',
|
||||
allow_redirects=False,
|
||||
headers={'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'User-Agent': 'python-heatclient'})
|
||||
|
||||
def test_http_manual_redirect_delete(self, mock_request):
|
||||
mock_request.side_effect = [
|
||||
fakes.FakeHTTPResponse(
|
||||
302, 'Found',
|
||||
{'location': 'http://example.com:8004/foo/bar'},
|
||||
''),
|
||||
fakes.FakeHTTPResponse(
|
||||
200, 'OK',
|
||||
{'content-type': 'application/json'},
|
||||
'invalid-json')
|
||||
]
|
||||
|
||||
client = http.HTTPClient('http://example.com:8004/foo')
|
||||
resp, body = client.json_request('DELETE', '')
|
||||
|
||||
mock_request.assert_has_calls([
|
||||
mock.call('DELETE', 'http://example.com:8004/foo',
|
||||
allow_redirects=False,
|
||||
headers={'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'User-Agent': 'python-heatclient'}),
|
||||
mock.call('DELETE', 'http://example.com:8004/foo/bar',
|
||||
allow_redirects=False,
|
||||
headers={'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'User-Agent': 'python-heatclient'})
|
||||
])
|
||||
|
||||
def test_http_manual_redirect_post(self, mock_request):
|
||||
mock_request.side_effect = [
|
||||
fakes.FakeHTTPResponse(
|
||||
302, 'Found',
|
||||
{'location': 'http://example.com:8004/foo/bar'},
|
||||
''),
|
||||
fakes.FakeHTTPResponse(
|
||||
200, 'OK',
|
||||
{'content-type': 'application/json'},
|
||||
'invalid-json')
|
||||
]
|
||||
|
||||
client = http.HTTPClient('http://example.com:8004/foo')
|
||||
resp, body = client.json_request('POST', '')
|
||||
|
||||
mock_request.assert_has_calls([
|
||||
mock.call('POST', 'http://example.com:8004/foo',
|
||||
allow_redirects=False,
|
||||
headers={'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'User-Agent': 'python-heatclient'}),
|
||||
mock.call('POST', 'http://example.com:8004/foo/bar',
|
||||
allow_redirects=False,
|
||||
headers={'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'User-Agent': 'python-heatclient'})
|
||||
])
|
||||
|
||||
def test_http_manual_redirect_put(self, mock_request):
|
||||
mock_request.side_effect = [
|
||||
fakes.FakeHTTPResponse(
|
||||
302, 'Found',
|
||||
{'location': 'http://example.com:8004/foo/bar'},
|
||||
''),
|
||||
fakes.FakeHTTPResponse(
|
||||
200, 'OK',
|
||||
{'content-type': 'application/json'},
|
||||
'invalid-json')
|
||||
]
|
||||
|
||||
client = http.HTTPClient('http://example.com:8004/foo')
|
||||
resp, body = client.json_request('PUT', '')
|
||||
|
||||
mock_request.assert_has_calls([
|
||||
mock.call('PUT', 'http://example.com:8004/foo',
|
||||
allow_redirects=False,
|
||||
headers={'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'User-Agent': 'python-heatclient'}),
|
||||
mock.call('PUT', 'http://example.com:8004/foo/bar',
|
||||
allow_redirects=False,
|
||||
headers={'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'User-Agent': 'python-heatclient'})
|
||||
])
|
||||
|
||||
def test_http_manual_redirect_put_uppercase(self, mock_request):
|
||||
mock_request.side_effect = [
|
||||
fakes.FakeHTTPResponse(
|
||||
302, 'Found',
|
||||
{'location': 'http://example.com:8004/foo/bar'},
|
||||
''),
|
||||
fakes.FakeHTTPResponse(
|
||||
200, 'OK',
|
||||
{'content-type': 'application/json'},
|
||||
'invalid-json')
|
||||
]
|
||||
client = http.HTTPClient('http://EXAMPLE.com:8004/foo')
|
||||
resp, body = client.json_request('PUT', '')
|
||||
self.assertEqual(200, resp.status_code)
|
||||
|
||||
mock_request.assert_has_calls([
|
||||
mock.call('PUT', 'http://EXAMPLE.com:8004/foo',
|
||||
allow_redirects=False,
|
||||
headers={'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'User-Agent': 'python-heatclient'}),
|
||||
mock.call('PUT', 'http://example.com:8004/foo/bar',
|
||||
allow_redirects=False,
|
||||
headers={'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'User-Agent': 'python-heatclient'})
|
||||
])
|
||||
|
||||
def test_http_manual_redirect_error_without_location(self, mock_request):
|
||||
mock_request.return_value = fakes.FakeHTTPResponse(
|
||||
302, 'Found',
|
||||
{},
|
||||
'')
|
||||
client = http.HTTPClient('http://example.com:8004/foo')
|
||||
self.assertRaises(exc.InvalidEndpoint,
|
||||
client.json_request, 'DELETE', '')
|
||||
mock_request.assert_called_once_with(
|
||||
'DELETE', 'http://example.com:8004/foo',
|
||||
allow_redirects=False,
|
||||
headers={'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'User-Agent': 'python-heatclient'})
|
||||
|
||||
def test_http_json_request_redirect(self, mock_request):
|
||||
# Record the 302
|
||||
mock_request.side_effect = [
|
||||
fakes.FakeHTTPResponse(
|
||||
302, 'Found',
|
||||
{'location': 'http://example.com:8004'},
|
||||
''),
|
||||
fakes.FakeHTTPResponse(
|
||||
200, 'OK',
|
||||
{'content-type': 'application/json'},
|
||||
'{}')
|
||||
]
|
||||
client = http.HTTPClient('http://example.com:8004')
|
||||
resp, body = client.json_request('GET', '')
|
||||
self.assertEqual(200, resp.status_code)
|
||||
self.assertEqual({}, body)
|
||||
mock_request.assert_has_calls([
|
||||
mock.call('GET', 'http://example.com:8004',
|
||||
allow_redirects=False,
|
||||
headers={'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'User-Agent': 'python-heatclient'}),
|
||||
mock.call('GET', 'http://example.com:8004',
|
||||
allow_redirects=False,
|
||||
headers={'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'User-Agent': 'python-heatclient'})
|
||||
])
|
||||
|
||||
def test_http_404_json_request(self, mock_request):
|
||||
# Record a 404
|
||||
mock_request.return_value = fakes.FakeHTTPResponse(
|
||||
404, 'OK', {'content-type': 'application/json'},
|
||||
'{}')
|
||||
client = http.HTTPClient('http://example.com:8004')
|
||||
e = self.assertRaises(exc.HTTPNotFound, client.json_request, 'GET', '')
|
||||
# Assert that the raised exception can be converted to string
|
||||
self.assertIsNotNone(str(e))
|
||||
mock_request.assert_called_with(
|
||||
'GET', 'http://example.com:8004',
|
||||
allow_redirects=False,
|
||||
headers={'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'User-Agent': 'python-heatclient'})
|
||||
|
||||
def test_http_300_json_request(self, mock_request):
|
||||
# Record a 300
|
||||
mock_request.return_value = fakes.FakeHTTPResponse(
|
||||
300, 'OK', {'content-type': 'application/json'},
|
||||
'{}')
|
||||
# Assert that the raised exception can be converted to string
|
||||
client = http.HTTPClient('http://example.com:8004')
|
||||
e = self.assertRaises(
|
||||
exc.HTTPMultipleChoices, client.json_request, 'GET', '')
|
||||
self.assertIsNotNone(str(e))
|
||||
mock_request.assert_called_with(
|
||||
'GET', 'http://example.com:8004',
|
||||
allow_redirects=False,
|
||||
headers={'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'User-Agent': 'python-heatclient'})
|
||||
|
||||
def test_fake_json_request(self, mock_request):
|
||||
headers = {'User-Agent': 'python-heatclient'}
|
||||
mock_request.side_effect = [socket.gaierror]
|
||||
client = http.HTTPClient('fake://example.com:8004')
|
||||
self.assertRaises(exc.InvalidEndpoint,
|
||||
client._http_request, "/", "GET")
|
||||
|
||||
mock_request.assert_called_with('GET', 'fake://example.com:8004/',
|
||||
allow_redirects=False,
|
||||
headers=headers)
|
||||
|
||||
def test_debug_curl_command(self, mock_request):
|
||||
with mock.patch('logging.Logger.debug') as mock_logging_debug:
|
||||
|
||||
ssl_connection_params = {'ca_file': 'TEST_CA',
|
||||
'cert_file': 'TEST_CERT',
|
||||
'key_file': 'TEST_KEY',
|
||||
'insecure': 'TEST_NSA'}
|
||||
|
||||
headers = {'key': 'value'}
|
||||
mock_logging_debug.return_value = None
|
||||
client = http.HTTPClient('http://foo')
|
||||
client.ssl_connection_params = ssl_connection_params
|
||||
client.log_curl_request('GET', '/bar', {'headers': headers,
|
||||
'data': 'text'})
|
||||
mock_logging_debug.assert_called_with(
|
||||
"curl -g -i -X GET -H 'key: value' --key TEST_KEY "
|
||||
"--cert TEST_CERT --cacert TEST_CA "
|
||||
"-k -d 'text' http://foo/bar"
|
||||
)
|
||||
|
||||
def test_http_request_socket_error(self, mock_request):
|
||||
headers = {'User-Agent': 'python-heatclient'}
|
||||
mock_request.side_effect = [socket.error]
|
||||
|
||||
client = http.HTTPClient('http://example.com:8004')
|
||||
self.assertRaises(exc.CommunicationError,
|
||||
client._http_request, "/", "GET")
|
||||
mock_request.assert_called_with('GET', 'http://example.com:8004/',
|
||||
allow_redirects=False,
|
||||
headers=headers)
|
||||
|
||||
def test_http_request_socket_timeout(self, mock_request):
|
||||
headers = {'User-Agent': 'python-heatclient'}
|
||||
mock_request.side_effect = [socket.timeout]
|
||||
|
||||
client = http.HTTPClient('http://example.com:8004')
|
||||
self.assertRaises(exc.CommunicationError,
|
||||
client._http_request, "/", "GET")
|
||||
mock_request.assert_called_with('GET', 'http://example.com:8004/',
|
||||
allow_redirects=False,
|
||||
headers=headers)
|
||||
|
||||
def test_http_request_specify_timeout(self, mock_request):
|
||||
mock_request.return_value = fakes.FakeHTTPResponse(
|
||||
200, 'OK',
|
||||
{'content-type': 'application/json'},
|
||||
'{}')
|
||||
client = http.HTTPClient('http://example.com:8004', timeout='123')
|
||||
resp, body = client.json_request('GET', '')
|
||||
self.assertEqual(200, resp.status_code)
|
||||
mock_request.assert_called_with(
|
||||
'GET', 'http://example.com:8004',
|
||||
allow_redirects=False,
|
||||
headers={'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'User-Agent': 'python-heatclient'},
|
||||
timeout=float(123))
|
||||
|
||||
def test_get_system_ca_file(self, mock_request):
|
||||
chosen = '/etc/ssl/certs/ca-certificates.crt'
|
||||
with mock.patch('os.path.exists') as mock_os:
|
||||
mock_os.return_value = chosen
|
||||
ca = http.get_system_ca_file()
|
||||
self.assertEqual(chosen, ca)
|
||||
mock_os.assert_called_once_with(chosen)
|
||||
|
||||
def test_insecure_verify_cert_None(self, mock_request):
|
||||
client = http.HTTPClient('https://foo', insecure=True)
|
||||
self.assertFalse(client.verify_cert)
|
||||
|
||||
def test_passed_cert_to_verify_cert(self, mock_request):
|
||||
client = http.HTTPClient('https://foo', ca_file="NOWHERE")
|
||||
self.assertEqual("NOWHERE", client.verify_cert)
|
||||
|
||||
with mock.patch('heatclient.common.http.get_system_ca_file') as gsf:
|
||||
gsf.return_value = "SOMEWHERE"
|
||||
client = http.HTTPClient('https://foo')
|
||||
self.assertEqual("SOMEWHERE", client.verify_cert)
|
||||
|
||||
@mock.patch('logging.Logger.debug', return_value=None)
|
||||
def test_curl_log_i18n_headers(self, mock_log, mock_request):
|
||||
kwargs = {'headers': {'Key': b'foo\xe3\x8a\x8e'}}
|
||||
|
||||
client = http.HTTPClient('http://somewhere')
|
||||
client.log_curl_request("GET", '', kwargs=kwargs)
|
||||
mock_log.assert_called_once_with(
|
||||
u"curl -g -i -X GET -H 'Key: foo㊎' http://somewhere")
|
||||
|
||||
|
||||
class SessionClientTest(testtools.TestCase):
|
||||
def setUp(self):
|
||||
super(SessionClientTest, self).setUp()
|
||||
self.request = mock.patch.object(adapter.LegacyJsonAdapter,
|
||||
'request').start()
|
||||
|
||||
def test_session_simple_request(self):
|
||||
resp = fakes.FakeHTTPResponse(
|
||||
200,
|
||||
'OK',
|
||||
{'content-type': 'application/octet-stream'},
|
||||
'')
|
||||
self.request.return_value = (resp, '')
|
||||
|
||||
client = http.SessionClient(session=mock.ANY,
|
||||
auth=mock.ANY)
|
||||
response = client.request(method='GET', url='')
|
||||
self.assertEqual(200, response.status_code)
|
||||
self.assertEqual('', ''.join([x for x in response.content]))
|
||||
|
||||
def test_session_json_request(self):
|
||||
fake = fakes.FakeHTTPResponse(
|
||||
200,
|
||||
'OK',
|
||||
{'content-type': 'application/json'},
|
||||
jsonutils.dumps({'some': 'body'}))
|
||||
self.request.return_value = (fake, {})
|
||||
|
||||
client = http.SessionClient(session=mock.ANY,
|
||||
auth=mock.ANY)
|
||||
|
||||
resp = client.request('', 'GET')
|
||||
self.assertEqual(200, resp.status_code)
|
||||
self.assertEqual({'some': 'body'}, resp.json())
|
||||
|
||||
def test_404_error_response(self):
|
||||
fake = fakes.FakeHTTPResponse(
|
||||
404,
|
||||
'FAIL',
|
||||
{'content-type': 'application/octet-stream'},
|
||||
'')
|
||||
self.request.return_value = (fake, '')
|
||||
|
||||
client = http.SessionClient(session=mock.ANY,
|
||||
auth=mock.ANY)
|
||||
e = self.assertRaises(exc.HTTPNotFound,
|
||||
client.request, '', 'GET')
|
||||
# Assert that the raised exception can be converted to string
|
||||
self.assertIsNotNone(six.text_type(e))
|
||||
|
||||
def test_redirect_302_location(self):
|
||||
fake1 = fakes.FakeHTTPResponse(
|
||||
302,
|
||||
'OK',
|
||||
{'location': 'http://no.where/ishere'},
|
||||
''
|
||||
)
|
||||
fake2 = fakes.FakeHTTPResponse(
|
||||
200,
|
||||
'OK',
|
||||
{'content-type': 'application/json'},
|
||||
jsonutils.dumps({'Mount': 'Fuji'})
|
||||
)
|
||||
self.request.side_effect = [
|
||||
(fake1, ''), (fake2, jsonutils.dumps({'Mount': 'Fuji'}))]
|
||||
|
||||
client = http.SessionClient(session=mock.ANY,
|
||||
auth=mock.ANY,
|
||||
endpoint_override='http://no.where/')
|
||||
resp = client.request('', 'GET', redirect=True)
|
||||
|
||||
self.assertEqual(200, resp.status_code)
|
||||
self.assertEqual({'Mount': 'Fuji'}, utils.get_response_body(resp))
|
||||
|
||||
self.assertEqual(('', 'GET'), self.request.call_args_list[0][0])
|
||||
self.assertEqual(('ishere', 'GET'), self.request.call_args_list[1][0])
|
||||
for call in self.request.call_args_list:
|
||||
self.assertEqual({'user_agent': 'python-heatclient',
|
||||
'raise_exc': False,
|
||||
'redirect': True}, call[1])
|
||||
|
||||
def test_302_location_no_endpoint(self):
|
||||
fake1 = fakes.FakeHTTPResponse(
|
||||
302,
|
||||
'OK',
|
||||
{'location': 'http://no.where/ishere'},
|
||||
''
|
||||
)
|
||||
fake2 = fakes.FakeHTTPResponse(
|
||||
200,
|
||||
'OK',
|
||||
{'content-type': 'application/json'},
|
||||
jsonutils.dumps({'Mount': 'Fuji'})
|
||||
)
|
||||
self.request.side_effect = [
|
||||
(fake1, ''), (fake2, jsonutils.dumps({'Mount': 'Fuji'}))]
|
||||
|
||||
client = http.SessionClient(session=mock.ANY,
|
||||
auth=mock.ANY)
|
||||
resp = client.request('', 'GET', redirect=True)
|
||||
|
||||
self.assertEqual(200, resp.status_code)
|
||||
self.assertEqual({'Mount': 'Fuji'}, utils.get_response_body(resp))
|
||||
|
||||
self.assertEqual(('', 'GET'), self.request.call_args_list[0][0])
|
||||
self.assertEqual(('http://no.where/ishere',
|
||||
'GET'), self.request.call_args_list[1][0])
|
||||
for call in self.request.call_args_list:
|
||||
self.assertEqual({'user_agent': 'python-heatclient',
|
||||
'raise_exc': False,
|
||||
'redirect': True}, call[1])
|
||||
|
||||
def test_redirect_302_no_location(self):
|
||||
fake = fakes.FakeHTTPResponse(
|
||||
302,
|
||||
'OK',
|
||||
{},
|
||||
''
|
||||
)
|
||||
self.request.side_effect = [(fake, '')]
|
||||
|
||||
client = http.SessionClient(session=mock.ANY,
|
||||
auth=mock.ANY)
|
||||
e = self.assertRaises(exc.InvalidEndpoint,
|
||||
client.request, '', 'GET', redirect=True)
|
||||
self.assertEqual("Location not returned with 302", six.text_type(e))
|
||||
|
||||
def test_no_redirect_302_no_location(self):
|
||||
fake = fakes.FakeHTTPResponse(
|
||||
302,
|
||||
'OK',
|
||||
{'location': 'http://no.where/ishere'},
|
||||
''
|
||||
)
|
||||
self.request.side_effect = [(fake, '')]
|
||||
|
||||
client = http.SessionClient(session=mock.ANY,
|
||||
auth=mock.ANY)
|
||||
|
||||
self.assertEqual(fake, client.request('', 'GET'))
|
||||
|
||||
def test_300_error_response(self):
|
||||
fake = fakes.FakeHTTPResponse(
|
||||
300,
|
||||
'FAIL',
|
||||
{'content-type': 'application/octet-stream'},
|
||||
'')
|
||||
self.request.return_value = (fake, '')
|
||||
|
||||
client = http.SessionClient(session=mock.ANY,
|
||||
auth=mock.ANY)
|
||||
e = self.assertRaises(exc.HTTPMultipleChoices,
|
||||
client.request, '', 'GET')
|
||||
# Assert that the raised exception can be converted to string
|
||||
self.assertIsNotNone(six.text_type(e))
|
||||
|
||||
def test_504_error_response(self):
|
||||
# for 504 we don't have specific exception type
|
||||
fake = fakes.FakeHTTPResponse(
|
||||
504,
|
||||
'FAIL',
|
||||
{'content-type': 'application/octet-stream'},
|
||||
'')
|
||||
self.request.return_value = (fake, '')
|
||||
|
||||
client = http.SessionClient(session=mock.ANY,
|
||||
auth=mock.ANY)
|
||||
e = self.assertRaises(exc.HTTPException,
|
||||
client.request, '', 'GET')
|
||||
|
||||
self.assertEqual(504, e.code)
|
||||
|
||||
def test_kwargs(self):
|
||||
fake = fakes.FakeHTTPResponse(
|
||||
200,
|
||||
'OK',
|
||||
{'content-type': 'application/json'},
|
||||
{}
|
||||
)
|
||||
kwargs = dict(endpoint_override='http://no.where/',
|
||||
data='some_data')
|
||||
|
||||
client = http.SessionClient(mock.ANY)
|
||||
|
||||
self.request.return_value = (fake, {})
|
||||
|
||||
resp = client.request('', 'GET', **kwargs)
|
||||
|
||||
self.assertEqual({'endpoint_override': 'http://no.where/',
|
||||
'data': '"some_data"',
|
||||
'user_agent': 'python-heatclient',
|
||||
'raise_exc': False}, self.request.call_args[1])
|
||||
self.assertEqual(200, resp.status_code)
|
||||
|
||||
@mock.patch.object(jsonutils, 'dumps')
|
||||
def test_kwargs_with_files(self, mock_dumps):
|
||||
fake = fakes.FakeHTTPResponse(
|
||||
200,
|
||||
'OK',
|
||||
{'content-type': 'application/json'},
|
||||
{}
|
||||
)
|
||||
mock_dumps.return_value = "{'files': test}}"
|
||||
data = six.BytesIO(b'test')
|
||||
kwargs = {'endpoint_override': 'http://no.where/',
|
||||
'data': {'files': data}}
|
||||
client = http.SessionClient(mock.ANY)
|
||||
|
||||
self.request.return_value = (fake, {})
|
||||
|
||||
resp = client.request('', 'GET', **kwargs)
|
||||
|
||||
self.assertEqual({'endpoint_override': 'http://no.where/',
|
||||
'data': "{'files': test}}",
|
||||
'user_agent': 'python-heatclient',
|
||||
'raise_exc': False}, self.request.call_args[1])
|
||||
self.assertEqual(200, resp.status_code)
|
||||
|
||||
def test_methods(self):
|
||||
fake = fakes.FakeHTTPResponse(
|
||||
200,
|
||||
'OK',
|
||||
{'content-type': 'application/json'},
|
||||
{}
|
||||
)
|
||||
self.request.return_value = (fake, {})
|
||||
|
||||
client = http.SessionClient(mock.ANY)
|
||||
methods = [client.get, client.put, client.post, client.patch,
|
||||
client.delete, client.head]
|
||||
for method in methods:
|
||||
resp = method('')
|
||||
self.assertEqual(200, resp.status_code)
|
||||
|
||||
def test_credentials_headers(self):
|
||||
client = http.SessionClient(mock.ANY)
|
||||
self.assertEqual({}, client.credentials_headers())
|
|
@ -1,359 +0,0 @@
|
|||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
# implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import mock
|
||||
import six
|
||||
import swiftclient.client
|
||||
import testscenarios
|
||||
import testtools
|
||||
from testtools import matchers
|
||||
|
||||
from heatclient.common import deployment_utils
|
||||
from heatclient import exc
|
||||
from heatclient.v1 import software_configs
|
||||
|
||||
|
||||
load_tests = testscenarios.load_tests_apply_scenarios
|
||||
|
||||
|
||||
def mock_sc(group=None, config=None, options=None,
|
||||
inputs=None, outputs=None):
|
||||
return software_configs.SoftwareConfig(None, {
|
||||
'group': group,
|
||||
'config': config,
|
||||
'options': options or {},
|
||||
'inputs': inputs or [],
|
||||
'outputs': outputs or [],
|
||||
}, True)
|
||||
|
||||
|
||||
class DerivedConfigTest(testtools.TestCase):
|
||||
|
||||
scenarios = [
|
||||
('defaults', dict(
|
||||
action='UPDATE',
|
||||
source=mock_sc(),
|
||||
name='s1',
|
||||
input_values=None,
|
||||
server_id='1234',
|
||||
signal_transport='NO_SIGNAL',
|
||||
signal_id=None,
|
||||
result={
|
||||
'config': '',
|
||||
'group': 'Heat::Ungrouped',
|
||||
'inputs': [{
|
||||
'description': 'ID of the server being deployed to',
|
||||
'name': 'deploy_server_id',
|
||||
'type': 'String',
|
||||
'value': '1234'
|
||||
}, {
|
||||
'description': 'Name of the current action '
|
||||
'being deployed',
|
||||
'name': 'deploy_action',
|
||||
'type': 'String',
|
||||
'value': 'UPDATE'
|
||||
}, {
|
||||
'description': 'How the server should signal to '
|
||||
'heat with the deployment output values.',
|
||||
'name': 'deploy_signal_transport',
|
||||
'type': 'String',
|
||||
'value': 'NO_SIGNAL'}],
|
||||
'name': 's1',
|
||||
'options': {},
|
||||
'outputs': []})),
|
||||
('defaults_empty', dict(
|
||||
action='UPDATE',
|
||||
source={},
|
||||
name='s1',
|
||||
input_values=None,
|
||||
server_id='1234',
|
||||
signal_transport='NO_SIGNAL',
|
||||
signal_id=None,
|
||||
result={
|
||||
'config': '',
|
||||
'group': 'Heat::Ungrouped',
|
||||
'inputs': [{
|
||||
'description': 'ID of the server being deployed to',
|
||||
'name': 'deploy_server_id',
|
||||
'type': 'String',
|
||||
'value': '1234'
|
||||
}, {
|
||||
'description': 'Name of the current action '
|
||||
'being deployed',
|
||||
'name': 'deploy_action',
|
||||
'type': 'String',
|
||||
'value': 'UPDATE'
|
||||
}, {
|
||||
'description': 'How the server should signal to '
|
||||
'heat with the deployment output values.',
|
||||
'name': 'deploy_signal_transport',
|
||||
'type': 'String',
|
||||
'value': 'NO_SIGNAL'}],
|
||||
'name': 's1',
|
||||
'options': {},
|
||||
'outputs': []})),
|
||||
|
||||
('config_values', dict(
|
||||
action='UPDATE',
|
||||
source=mock_sc(
|
||||
group='puppet',
|
||||
config='do the foo',
|
||||
inputs=[
|
||||
{'name': 'one', 'default': '1'},
|
||||
{'name': 'two'}],
|
||||
options={'option1': 'value'},
|
||||
outputs=[
|
||||
{'name': 'output1'},
|
||||
{'name': 'output2'}],
|
||||
),
|
||||
name='s2',
|
||||
input_values={'one': 'foo', 'two': 'bar', 'three': 'baz'},
|
||||
server_id='1234',
|
||||
signal_transport='NO_SIGNAL',
|
||||
signal_id=None,
|
||||
result={
|
||||
'config': 'do the foo',
|
||||
'group': 'puppet',
|
||||
'inputs': [{
|
||||
'name': 'one',
|
||||
'default': '1',
|
||||
'value': 'foo'
|
||||
}, {
|
||||
'name': 'two',
|
||||
'value': 'bar'
|
||||
}, {
|
||||
'name': 'three',
|
||||
'type': 'String',
|
||||
'value': 'baz'
|
||||
}, {
|
||||
'description': 'ID of the server being deployed to',
|
||||
'name': 'deploy_server_id',
|
||||
'type': 'String',
|
||||
'value': '1234'
|
||||
}, {
|
||||
'description': 'Name of the current action '
|
||||
'being deployed',
|
||||
'name': 'deploy_action',
|
||||
'type': 'String',
|
||||
'value': 'UPDATE'
|
||||
}, {
|
||||
'description': 'How the server should signal to '
|
||||
'heat with the deployment output values.',
|
||||
'name': 'deploy_signal_transport',
|
||||
'type': 'String',
|
||||
'value': 'NO_SIGNAL'
|
||||
}],
|
||||
'name': 's2',
|
||||
'options': {'option1': 'value'},
|
||||
'outputs': [
|
||||
{'name': 'output1'},
|
||||
{'name': 'output2'}]})),
|
||||
('temp_url', dict(
|
||||
action='UPDATE',
|
||||
source=mock_sc(),
|
||||
name='s1',
|
||||
input_values=None,
|
||||
server_id='1234',
|
||||
signal_transport='TEMP_URL_SIGNAL',
|
||||
signal_id='http://192.0.2.1:8080/foo',
|
||||
result={
|
||||
'config': '',
|
||||
'group': 'Heat::Ungrouped',
|
||||
'inputs': [{
|
||||
'description': 'ID of the server being deployed to',
|
||||
'name': 'deploy_server_id',
|
||||
'type': 'String',
|
||||
'value': '1234'
|
||||
}, {
|
||||
'description': 'Name of the current action '
|
||||
'being deployed',
|
||||
'name': 'deploy_action',
|
||||
'type': 'String',
|
||||
'value': 'UPDATE'
|
||||
}, {
|
||||
'description': 'How the server should signal to '
|
||||
'heat with the deployment output values.',
|
||||
'name': 'deploy_signal_transport',
|
||||
'type': 'String',
|
||||
'value': 'TEMP_URL_SIGNAL'
|
||||
}, {
|
||||
'description': 'ID of signal to use for signaling '
|
||||
'output values',
|
||||
'name': 'deploy_signal_id',
|
||||
'type': 'String',
|
||||
'value': 'http://192.0.2.1:8080/foo'
|
||||
}, {
|
||||
'description': 'HTTP verb to use for signaling '
|
||||
'output values',
|
||||
'name': 'deploy_signal_verb',
|
||||
'type': 'String',
|
||||
'value': 'PUT'}],
|
||||
'name': 's1',
|
||||
'options': {},
|
||||
'outputs': []})),
|
||||
('unsupported', dict(
|
||||
action='UPDATE',
|
||||
source=mock_sc(),
|
||||
name='s1',
|
||||
input_values=None,
|
||||
server_id='1234',
|
||||
signal_transport='ASDF',
|
||||
signal_id=None,
|
||||
result_error=exc.CommandError,
|
||||
result_error_msg='Unsupported signal transport ASDF',
|
||||
result=None)),
|
||||
]
|
||||
|
||||
def test_build_derived_config_params(self):
|
||||
try:
|
||||
self.assertEqual(
|
||||
self.result,
|
||||
deployment_utils.build_derived_config_params(
|
||||
action=self.action,
|
||||
source=self.source,
|
||||
name=self.name,
|
||||
input_values=self.input_values,
|
||||
server_id=self.server_id,
|
||||
signal_transport=self.signal_transport,
|
||||
signal_id=self.signal_id))
|
||||
except Exception as e:
|
||||
if not self.result_error:
|
||||
raise e
|
||||
self.assertIsInstance(e, self.result_error)
|
||||
self.assertEqual(self.result_error_msg, six.text_type(e))
|
||||
|
||||
|
||||
class TempURLSignalTest(testtools.TestCase):
|
||||
|
||||
@mock.patch.object(swiftclient.client, 'Connection')
|
||||
def test_create_swift_client(self, sc_conn):
|
||||
auth = mock.MagicMock()
|
||||
auth.get_token.return_value = '1234'
|
||||
auth.get_endpoint.return_value = 'http://192.0.2.1:8080'
|
||||
|
||||
session = mock.MagicMock()
|
||||
|
||||
args = mock.MagicMock()
|
||||
args.os_region_name = 'Region1'
|
||||
args.os_project_name = 'project'
|
||||
args.os_username = 'user'
|
||||
args.os_cacert = None
|
||||
args.insecure = True
|
||||
|
||||
sc_conn.return_value = mock.MagicMock()
|
||||
|
||||
sc = deployment_utils.create_swift_client(auth, session, args)
|
||||
|
||||
self.assertEqual(sc_conn.return_value, sc)
|
||||
|
||||
self.assertEqual(
|
||||
mock.call(session),
|
||||
auth.get_token.call_args)
|
||||
|
||||
self.assertEqual(
|
||||
mock.call(
|
||||
session,
|
||||
service_type='object-store',
|
||||
region_name='Region1'),
|
||||
auth.get_endpoint.call_args)
|
||||
|
||||
self.assertEqual(
|
||||
mock.call(
|
||||
cacert=None,
|
||||
insecure=True,
|
||||
key=None,
|
||||
tenant_name='project',
|
||||
preauthtoken='1234',
|
||||
authurl=None,
|
||||
user='user',
|
||||
preauthurl='http://192.0.2.1:8080',
|
||||
auth_version='2.0'),
|
||||
sc_conn.call_args)
|
||||
|
||||
def test_create_temp_url(self):
|
||||
swift_client = mock.MagicMock()
|
||||
swift_client.url = ("http://fake-host.com:8080/v1/AUTH_demo")
|
||||
swift_client.head_account = mock.Mock(return_value={
|
||||
'x-account-meta-temp-url-key': '123456'})
|
||||
swift_client.post_account = mock.Mock()
|
||||
|
||||
uuid_pattern = ('[a-f0-9]{8}-[a-f0-9]{4}-4[a-f0-9]{3}-[89aAbB]'
|
||||
'[a-f0-9]{3}-[a-f0-9]{12}')
|
||||
url = deployment_utils.create_temp_url(swift_client, 'bar', 60)
|
||||
self.assertFalse(swift_client.post_account.called)
|
||||
regexp = ("http://fake-host.com:8080/v1/AUTH_demo/bar-%s"
|
||||
"/%s\?temp_url_sig=[0-9a-f]{40}&"
|
||||
"temp_url_expires=[0-9]{10}" % (uuid_pattern, uuid_pattern))
|
||||
self.assertThat(url, matchers.MatchesRegex(regexp))
|
||||
|
||||
timeout = int(url.split('=')[-1])
|
||||
self.assertTrue(timeout < 2147483647)
|
||||
|
||||
def test_get_temp_url_no_account_key(self):
|
||||
swift_client = mock.MagicMock()
|
||||
swift_client.url = ("http://fake-host.com:8080/v1/AUTH_demo")
|
||||
head_account = {}
|
||||
|
||||
def post_account(data):
|
||||
head_account.update(data)
|
||||
|
||||
swift_client.head_account = mock.Mock(return_value=head_account)
|
||||
swift_client.post_account = post_account
|
||||
|
||||
self.assertNotIn('x-account-meta-temp-url-key', head_account)
|
||||
deployment_utils.create_temp_url(swift_client, 'bar', 60, 'foo')
|
||||
self.assertIn('x-account-meta-temp-url-key', head_account)
|
||||
|
||||
def test_build_signal_id_no_signal(self):
|
||||
hc = mock.MagicMock()
|
||||
args = mock.MagicMock()
|
||||
args.signal_transport = 'NO_SIGNAL'
|
||||
self.assertIsNone(deployment_utils.build_signal_id(hc, args))
|
||||
|
||||
def test_build_signal_id_no_client_auth(self):
|
||||
hc = mock.MagicMock()
|
||||
args = mock.MagicMock()
|
||||
args.os_no_client_auth = True
|
||||
args.signal_transport = 'TEMP_URL_SIGNAL'
|
||||
e = self.assertRaises(exc.CommandError,
|
||||
deployment_utils.build_signal_id, hc, args)
|
||||
self.assertEqual((
|
||||
'Cannot use --os-no-client-auth, auth required to create '
|
||||
'a Swift TempURL.'),
|
||||
six.text_type(e))
|
||||
|
||||
@mock.patch.object(deployment_utils, 'create_temp_url')
|
||||
@mock.patch.object(deployment_utils, 'create_swift_client')
|
||||
def test_build_signal_id(self, csc, ctu):
|
||||
hc = mock.MagicMock()
|
||||
args = mock.MagicMock()
|
||||
args.name = 'foo'
|
||||
args.timeout = 60
|
||||
args.os_no_client_auth = False
|
||||
args.signal_transport = 'TEMP_URL_SIGNAL'
|
||||
csc.return_value = mock.MagicMock()
|
||||
temp_url = (
|
||||
'http://fake-host.com:8080/v1/AUTH_demo/foo/'
|
||||
'a81a74d5-c395-4269-9670-ddd0824fd696'
|
||||
'?temp_url_sig=6a68371d602c7a14aaaa9e3b3a63b8b85bd9a503'
|
||||
'&temp_url_expires=1425270977')
|
||||
ctu.return_value = temp_url
|
||||
|
||||
self.assertEqual(
|
||||
temp_url, deployment_utils.build_signal_id(hc, args))
|
||||
self.assertEqual(
|
||||
mock.call(hc.http_client.auth, hc.http_client.session, args),
|
||||
csc.call_args)
|
||||
self.assertEqual(
|
||||
mock.call(csc.return_value, 'foo', 60),
|
||||
ctu.call_args)
|
|
@ -1,108 +0,0 @@
|
|||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import mock
|
||||
import six
|
||||
import testscenarios
|
||||
import testtools
|
||||
import yaml
|
||||
|
||||
from heatclient.common import environment_format
|
||||
|
||||
load_tests = testscenarios.load_tests_apply_scenarios
|
||||
|
||||
|
||||
class YamlEnvironmentTest(testtools.TestCase):
|
||||
|
||||
def test_minimal_yaml(self):
|
||||
yaml1 = ''
|
||||
yaml2 = '''
|
||||
parameter_defaults: {}
|
||||
encrypted_param_names: []
|
||||
parameters: {}
|
||||
resource_registry: {}
|
||||
event_sinks: []
|
||||
'''
|
||||
tpl1 = environment_format.parse(yaml1)
|
||||
environment_format.default_for_missing(tpl1)
|
||||
tpl2 = environment_format.parse(yaml2)
|
||||
self.assertEqual(tpl2, tpl1)
|
||||
|
||||
def test_param_valid_strategy_section(self):
|
||||
yaml1 = ''
|
||||
yaml2 = '''
|
||||
parameters: {}
|
||||
encrypted_param_names: []
|
||||
parameter_defaults: {}
|
||||
parameter_merge_strategies: {}
|
||||
event_sinks: []
|
||||
resource_registry: {}
|
||||
'''
|
||||
tpl1 = environment_format.parse(yaml1)
|
||||
environment_format.default_for_missing(tpl1)
|
||||
tpl2 = environment_format.parse(yaml2)
|
||||
self.assertNotEqual(tpl1, tpl2)
|
||||
|
||||
def test_wrong_sections(self):
|
||||
env = '''
|
||||
parameters: {}
|
||||
resource_regis: {}
|
||||
'''
|
||||
self.assertRaises(ValueError, environment_format.parse, env)
|
||||
|
||||
def test_bad_yaml(self):
|
||||
env = '''
|
||||
parameters: }
|
||||
'''
|
||||
self.assertRaises(ValueError, environment_format.parse, env)
|
||||
|
||||
def test_parse_string_environment(self):
|
||||
env = 'just string'
|
||||
expect = 'The environment is not a valid YAML mapping data type.'
|
||||
e = self.assertRaises(ValueError, environment_format.parse, env)
|
||||
self.assertIn(expect, str(e))
|
||||
|
||||
def test_parse_document(self):
|
||||
env = '["foo", "bar"]'
|
||||
expect = 'The environment is not a valid YAML mapping data type.'
|
||||
e = self.assertRaises(ValueError, environment_format.parse, env)
|
||||
self.assertIn(expect, str(e))
|
||||
|
||||
|
||||
class YamlParseExceptions(testtools.TestCase):
|
||||
|
||||
scenarios = [
|
||||
('scanner', dict(raised_exception=yaml.scanner.ScannerError())),
|
||||
('parser', dict(raised_exception=yaml.parser.ParserError())),
|
||||
('reader',
|
||||
dict(raised_exception=yaml.reader.ReaderError('', '', '', '', ''))),
|
||||
]
|
||||
|
||||
def test_parse_to_value_exception(self):
|
||||
text = 'not important'
|
||||
|
||||
with mock.patch.object(yaml, 'load') as yaml_loader:
|
||||
yaml_loader.side_effect = self.raised_exception
|
||||
|
||||
self.assertRaises(ValueError,
|
||||
environment_format.parse, text)
|
||||
|
||||
|
||||
class DetailedYAMLParseExceptions(testtools.TestCase):
|
||||
|
||||
def test_parse_to_value_exception(self):
|
||||
yaml = """not important
|
||||
but very:
|
||||
- incorrect
|
||||
"""
|
||||
ex = self.assertRaises(ValueError, environment_format.parse, yaml)
|
||||
self.assertIn('but very:\n ^', six.text_type(ex))
|
|
@ -1,337 +0,0 @@
|
|||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import mock
|
||||
import testtools
|
||||
|
||||
from heatclient.common import event_utils
|
||||
from heatclient.v1 import events as hc_ev
|
||||
from heatclient.v1 import resources as hc_res
|
||||
|
||||
|
||||
class FakeWebSocket(object):
|
||||
|
||||
def __init__(self, events):
|
||||
self.events = events
|
||||
|
||||
def recv(self):
|
||||
return self.events.pop(0)
|
||||
|
||||
|
||||
class ShellTestEventUtils(testtools.TestCase):
|
||||
@staticmethod
|
||||
def _mock_resource(resource_id, nested_id=None):
|
||||
res_info = {"links": [{"href": "http://heat/foo", "rel": "self"},
|
||||
{"href": "http://heat/foo2", "rel": "resource"}],
|
||||
"logical_resource_id": resource_id,
|
||||
"physical_resource_id": resource_id,
|
||||
"resource_status": "CREATE_COMPLETE",
|
||||
"resource_status_reason": "state changed",
|
||||
"resource_type": "OS::Nested::Server",
|
||||
"updated_time": "2014-01-06T16:14:26Z"}
|
||||
if nested_id:
|
||||
nested_link = {"href": "http://heat/%s" % nested_id,
|
||||
"rel": "nested"}
|
||||
res_info["links"].append(nested_link)
|
||||
return hc_res.Resource(manager=None, info=res_info)
|
||||
|
||||
@staticmethod
|
||||
def _mock_event(event_id, resource_id,
|
||||
resource_status='CREATE_COMPLETE'):
|
||||
ev_info = {"links": [
|
||||
{"href": "http://heat/foo", "rel": "self"},
|
||||
{"href": "http://heat/stacks/astack", "rel": "stack"}],
|
||||
"logical_resource_id": resource_id,
|
||||
"physical_resource_id": resource_id,
|
||||
"resource_name": resource_id,
|
||||
"resource_status": resource_status,
|
||||
"resource_status_reason": "state changed",
|
||||
"event_time": "2014-12-05T14:14:30Z",
|
||||
"id": event_id}
|
||||
return hc_ev.Event(manager=None, info=ev_info)
|
||||
|
||||
@staticmethod
|
||||
def _mock_stack_event(event_id, stack_name,
|
||||
stack_status='CREATE_COMPLETE'):
|
||||
stack_id = 'abcdef'
|
||||
ev_info = {"links": [{"href": "http://heat/foo", "rel": "self"},
|
||||
{"href": "http://heat/stacks/%s/%s" % (stack_name,
|
||||
stack_id),
|
||||
"rel": "stack"}],
|
||||
"logical_resource_id": stack_name,
|
||||
"physical_resource_id": stack_id,
|
||||
"resource_name": stack_name,
|
||||
"resource_status": stack_status,
|
||||
"resource_status_reason": "state changed",
|
||||
"event_time": "2014-12-05T14:14:30Z",
|
||||
"id": event_id}
|
||||
return hc_ev.Event(manager=None, info=ev_info)
|
||||
|
||||
def test_get_nested_ids(self):
|
||||
def list_stub(stack_id):
|
||||
return [self._mock_resource('aresource', 'foo3/3id')]
|
||||
mock_client = mock.MagicMock()
|
||||
mock_client.resources.list.side_effect = list_stub
|
||||
ids = event_utils._get_nested_ids(hc=mock_client,
|
||||
stack_id='astack/123')
|
||||
mock_client.resources.list.assert_called_once_with(
|
||||
stack_id='astack/123')
|
||||
self.assertEqual(['foo3/3id'], ids)
|
||||
|
||||
def test_get_stack_events(self):
|
||||
def event_stub(stack_id, argfoo):
|
||||
return [self._mock_event('event1', 'aresource')]
|
||||
mock_client = mock.MagicMock()
|
||||
mock_client.events.list.side_effect = event_stub
|
||||
ev_args = {'argfoo': 123}
|
||||
evs = event_utils._get_stack_events(hc=mock_client,
|
||||
stack_id='astack/123',
|
||||
event_args=ev_args)
|
||||
mock_client.events.list.assert_called_once_with(
|
||||
stack_id='astack/123', argfoo=123)
|
||||
self.assertEqual(1, len(evs))
|
||||
self.assertEqual('event1', evs[0].id)
|
||||
self.assertEqual('astack', evs[0].stack_name)
|
||||
|
||||
def test_get_nested_events(self):
|
||||
resources = {'parent': self._mock_resource('resource1', 'foo/child1'),
|
||||
'foo/child1': self._mock_resource('res_child1',
|
||||
'foo/child2'),
|
||||
'foo/child2': self._mock_resource('res_child2',
|
||||
'foo/child3'),
|
||||
'foo/child3': self._mock_resource('res_child3',
|
||||
'foo/END')}
|
||||
|
||||
def resource_list_stub(stack_id):
|
||||
return [resources[stack_id]]
|
||||
mock_client = mock.MagicMock()
|
||||
mock_client.resources.list.side_effect = resource_list_stub
|
||||
|
||||
events = {'foo/child1': self._mock_event('event1', 'res_child1'),
|
||||
'foo/child2': self._mock_event('event2', 'res_child2'),
|
||||
'foo/child3': self._mock_event('event3', 'res_child3')}
|
||||
|
||||
def event_list_stub(stack_id, argfoo):
|
||||
return [events[stack_id]]
|
||||
mock_client.events.list.side_effect = event_list_stub
|
||||
|
||||
ev_args = {'argfoo': 123}
|
||||
# Check nested_depth=1 (non recursive)..
|
||||
evs = event_utils._get_nested_events(hc=mock_client,
|
||||
nested_depth=1,
|
||||
stack_id='parent',
|
||||
event_args=ev_args)
|
||||
|
||||
rsrc_calls = [mock.call(stack_id='parent')]
|
||||
mock_client.resources.list.assert_has_calls(rsrc_calls)
|
||||
ev_calls = [mock.call(stack_id='foo/child1', argfoo=123)]
|
||||
mock_client.events.list.assert_has_calls(ev_calls)
|
||||
self.assertEqual(1, len(evs))
|
||||
self.assertEqual('event1', evs[0].id)
|
||||
|
||||
# ..and the recursive case via nested_depth=3
|
||||
mock_client.resources.list.reset_mock()
|
||||
mock_client.events.list.reset_mock()
|
||||
evs = event_utils._get_nested_events(hc=mock_client,
|
||||
nested_depth=3,
|
||||
stack_id='parent',
|
||||
event_args=ev_args)
|
||||
|
||||
rsrc_calls = [mock.call(stack_id='parent'),
|
||||
mock.call(stack_id='foo/child1'),
|
||||
mock.call(stack_id='foo/child2')]
|
||||
mock_client.resources.list.assert_has_calls(rsrc_calls)
|
||||
ev_calls = [mock.call(stack_id='foo/child1', argfoo=123),
|
||||
mock.call(stack_id='foo/child2', argfoo=123),
|
||||
mock.call(stack_id='foo/child3', argfoo=123)]
|
||||
mock_client.events.list.assert_has_calls(ev_calls)
|
||||
self.assertEqual(3, len(evs))
|
||||
self.assertEqual('event1', evs[0].id)
|
||||
self.assertEqual('event2', evs[1].id)
|
||||
self.assertEqual('event3', evs[2].id)
|
||||
|
||||
@mock.patch('heatclient.common.event_utils.get_events')
|
||||
def test_poll_for_events(self, ge):
|
||||
ge.side_effect = [[
|
||||
self._mock_stack_event('1', 'astack', 'CREATE_IN_PROGRESS'),
|
||||
self._mock_event('2', 'res_child1', 'CREATE_IN_PROGRESS'),
|
||||
self._mock_event('3', 'res_child2', 'CREATE_IN_PROGRESS'),
|
||||
self._mock_event('4', 'res_child3', 'CREATE_IN_PROGRESS')
|
||||
], [
|
||||
self._mock_event('5', 'res_child1', 'CREATE_COMPLETE'),
|
||||
self._mock_event('6', 'res_child2', 'CREATE_COMPLETE'),
|
||||
self._mock_event('7', 'res_child3', 'CREATE_COMPLETE'),
|
||||
self._mock_stack_event('8', 'astack', 'CREATE_COMPLETE')
|
||||
]]
|
||||
|
||||
stack_status, msg = event_utils.poll_for_events(
|
||||
None, 'astack', action='CREATE', poll_period=0)
|
||||
self.assertEqual('CREATE_COMPLETE', stack_status)
|
||||
self.assertEqual('\n Stack astack CREATE_COMPLETE \n', msg)
|
||||
ge.assert_has_calls([
|
||||
mock.call(None, stack_id='astack', nested_depth=0, event_args={
|
||||
'sort_dir': 'asc', 'marker': None
|
||||
}),
|
||||
mock.call(None, stack_id='astack', nested_depth=0, event_args={
|
||||
'sort_dir': 'asc', 'marker': '4'
|
||||
})
|
||||
])
|
||||
|
||||
@mock.patch('heatclient.common.event_utils.get_events')
|
||||
def test_poll_for_events_same_name(self, ge):
|
||||
ge.side_effect = [[
|
||||
self._mock_stack_event('1', 'mything', 'CREATE_IN_PROGRESS'),
|
||||
self._mock_event('2', 'res_child1', 'CREATE_IN_PROGRESS'),
|
||||
self._mock_event('3', 'mything', 'CREATE_IN_PROGRESS'),
|
||||
], [
|
||||
self._mock_event('4', 'mything', 'CREATE_COMPLETE'),
|
||||
], [
|
||||
self._mock_event('5', 'res_child1', 'CREATE_COMPLETE'),
|
||||
self._mock_stack_event('6', 'mything', 'CREATE_COMPLETE'),
|
||||
]]
|
||||
|
||||
stack_status, msg = event_utils.poll_for_events(
|
||||
None, 'mything', action='CREATE', poll_period=0)
|
||||
self.assertEqual('CREATE_COMPLETE', stack_status)
|
||||
self.assertEqual('\n Stack mything CREATE_COMPLETE \n', msg)
|
||||
ge.assert_has_calls([
|
||||
mock.call(None, stack_id='mything', nested_depth=0, event_args={
|
||||
'sort_dir': 'asc', 'marker': None
|
||||
}),
|
||||
mock.call(None, stack_id='mything', nested_depth=0, event_args={
|
||||
'sort_dir': 'asc', 'marker': '3'
|
||||
}),
|
||||
mock.call(None, stack_id='mything', nested_depth=0, event_args={
|
||||
'sort_dir': 'asc', 'marker': '4'
|
||||
})
|
||||
])
|
||||
|
||||
@mock.patch('heatclient.common.event_utils.get_events')
|
||||
def test_poll_for_events_with_marker(self, ge):
|
||||
ge.side_effect = [[
|
||||
self._mock_event('5', 'res_child1', 'CREATE_COMPLETE'),
|
||||
self._mock_event('6', 'res_child2', 'CREATE_COMPLETE'),
|
||||
self._mock_event('7', 'res_child3', 'CREATE_COMPLETE'),
|
||||
self._mock_stack_event('8', 'astack', 'CREATE_COMPLETE')
|
||||
]]
|
||||
|
||||
stack_status, msg = event_utils.poll_for_events(
|
||||
None, 'astack', action='CREATE', poll_period=0, marker='4',
|
||||
nested_depth=0)
|
||||
self.assertEqual('CREATE_COMPLETE', stack_status)
|
||||
self.assertEqual('\n Stack astack CREATE_COMPLETE \n', msg)
|
||||
ge.assert_has_calls([
|
||||
mock.call(None, stack_id='astack', nested_depth=0, event_args={
|
||||
'sort_dir': 'asc', 'marker': '4'
|
||||
})
|
||||
])
|
||||
|
||||
@mock.patch('heatclient.common.event_utils.get_events')
|
||||
def test_poll_for_events_in_progress_resource(self, ge):
|
||||
ge.side_effect = [[
|
||||
self._mock_stack_event('1', 'astack', 'CREATE_IN_PROGRESS'),
|
||||
self._mock_event('2', 'res_child1', 'CREATE_IN_PROGRESS'),
|
||||
self._mock_stack_event('3', 'astack', 'CREATE_COMPLETE')
|
||||
]]
|
||||
|
||||
stack_status, msg = event_utils.poll_for_events(
|
||||
None, 'astack', action='CREATE', poll_period=0)
|
||||
self.assertEqual('CREATE_COMPLETE', stack_status)
|
||||
self.assertEqual('\n Stack astack CREATE_COMPLETE \n', msg)
|
||||
|
||||
@mock.patch('heatclient.common.event_utils.get_events')
|
||||
def test_poll_for_events_failed(self, ge):
|
||||
ge.side_effect = [[
|
||||
self._mock_stack_event('1', 'astack', 'CREATE_IN_PROGRESS'),
|
||||
self._mock_event('2', 'res_child1', 'CREATE_IN_PROGRESS'),
|
||||
self._mock_event('3', 'res_child2', 'CREATE_IN_PROGRESS'),
|
||||
self._mock_event('4', 'res_child3', 'CREATE_IN_PROGRESS')
|
||||
], [
|
||||
self._mock_event('5', 'res_child1', 'CREATE_COMPLETE'),
|
||||
self._mock_event('6', 'res_child2', 'CREATE_FAILED'),
|
||||
self._mock_event('7', 'res_child3', 'CREATE_COMPLETE'),
|
||||
self._mock_stack_event('8', 'astack', 'CREATE_FAILED')
|
||||
]]
|
||||
|
||||
stack_status, msg = event_utils.poll_for_events(
|
||||
None, 'astack', action='CREATE', poll_period=0)
|
||||
self.assertEqual('CREATE_FAILED', stack_status)
|
||||
self.assertEqual('\n Stack astack CREATE_FAILED \n', msg)
|
||||
|
||||
@mock.patch('heatclient.common.event_utils.get_events')
|
||||
def test_poll_for_events_no_action(self, ge):
|
||||
ge.side_effect = [[
|
||||
self._mock_stack_event('1', 'astack', 'CREATE_IN_PROGRESS'),
|
||||
self._mock_event('2', 'res_child1', 'CREATE_IN_PROGRESS'),
|
||||
self._mock_event('3', 'res_child2', 'CREATE_IN_PROGRESS'),
|
||||
self._mock_event('4', 'res_child3', 'CREATE_IN_PROGRESS')
|
||||
], [
|
||||
self._mock_event('5', 'res_child1', 'CREATE_COMPLETE'),
|
||||
self._mock_event('6', 'res_child2', 'CREATE_FAILED'),
|
||||
self._mock_event('7', 'res_child3', 'CREATE_COMPLETE'),
|
||||
self._mock_stack_event('8', 'astack', 'FOO_FAILED')
|
||||
]]
|
||||
|
||||
stack_status, msg = event_utils.poll_for_events(
|
||||
None, 'astack', action=None, poll_period=0)
|
||||
self.assertEqual('FOO_FAILED', stack_status)
|
||||
self.assertEqual('\n Stack astack FOO_FAILED \n', msg)
|
||||
|
||||
@mock.patch('heatclient.common.event_utils.get_events')
|
||||
def test_poll_for_events_stack_get(self, ge):
|
||||
mock_client = mock.MagicMock()
|
||||
mock_client.stacks.get.return_value.stack_status = 'CREATE_FAILED'
|
||||
|
||||
ge.side_effect = [[
|
||||
self._mock_stack_event('1', 'astack', 'CREATE_IN_PROGRESS'),
|
||||
self._mock_event('2', 'res_child1', 'CREATE_IN_PROGRESS'),
|
||||
self._mock_event('3', 'res_child2', 'CREATE_IN_PROGRESS'),
|
||||
self._mock_event('4', 'res_child3', 'CREATE_IN_PROGRESS')
|
||||
], [], []]
|
||||
|
||||
stack_status, msg = event_utils.poll_for_events(
|
||||
mock_client, 'astack', action='CREATE', poll_period=0)
|
||||
self.assertEqual('CREATE_FAILED', stack_status)
|
||||
self.assertEqual('\n Stack astack CREATE_FAILED \n', msg)
|
||||
|
||||
def test_wait_for_events(self):
|
||||
ws = FakeWebSocket([
|
||||
{'body': {
|
||||
'timestamp': '2014-01-06T16:14:26Z',
|
||||
'payload': {'resource_action': 'CREATE',
|
||||
'resource_status': 'COMPLETE',
|
||||
'resource_name': 'mystack',
|
||||
'physical_resource_id': 'stackid1',
|
||||
'stack_id': 'stackid1'}}}])
|
||||
stack_status, msg = event_utils.wait_for_events(ws, 'mystack')
|
||||
self.assertEqual('CREATE_COMPLETE', stack_status)
|
||||
self.assertEqual('\n Stack mystack CREATE_COMPLETE \n', msg)
|
||||
|
||||
def test_wait_for_events_failed(self):
|
||||
ws = FakeWebSocket([
|
||||
{'body': {
|
||||
'timestamp': '2014-01-06T16:14:23Z',
|
||||
'payload': {'resource_action': 'CREATE',
|
||||
'resource_status': 'IN_PROGRESS',
|
||||
'resource_name': 'mystack',
|
||||
'physical_resource_id': 'stackid1',
|
||||
'stack_id': 'stackid1'}}},
|
||||
{'body': {
|
||||
'timestamp': '2014-01-06T16:14:26Z',
|
||||
'payload': {'resource_action': 'CREATE',
|
||||
'resource_status': 'FAILED',
|
||||
'resource_name': 'mystack',
|
||||
'physical_resource_id': 'stackid1',
|
||||
'stack_id': 'stackid1'}}}])
|
||||
stack_status, msg = event_utils.wait_for_events(ws, 'mystack')
|
||||
self.assertEqual('CREATE_FAILED', stack_status)
|
||||
self.assertEqual('\n Stack mystack CREATE_FAILED \n', msg)
|
|
@ -1,153 +0,0 @@
|
|||
# Copyright 2013 IBM Corp.
|
||||
#
|
||||
# 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 heatclient.common import utils
|
||||
from heatclient.v1 import events
|
||||
|
||||
|
||||
class EventManagerTest(testtools.TestCase):
|
||||
|
||||
def test_list_event(self):
|
||||
stack_id = 'teststack',
|
||||
resource_name = 'testresource'
|
||||
manager = events.EventManager(None)
|
||||
with mock.patch('heatclient.v1.events.EventManager._resolve_stack_id')\
|
||||
as mock_re:
|
||||
mock_re.return_value = 'teststack/abcd1234'
|
||||
|
||||
manager._list = mock.MagicMock()
|
||||
manager.list(stack_id, resource_name)
|
||||
# Make sure url is correct.
|
||||
manager._list.assert_called_once_with(
|
||||
'/stacks/teststack/abcd1234/'
|
||||
'resources/testresource/events',
|
||||
"events")
|
||||
mock_re.assert_called_once_with(stack_id)
|
||||
|
||||
def test_list_event_with_unicode_resource_name(self):
|
||||
stack_id = 'teststack',
|
||||
resource_name = u'\u5de5\u4f5c'
|
||||
manager = events.EventManager(None)
|
||||
with mock.patch('heatclient.v1.events.EventManager._resolve_stack_id')\
|
||||
as mock_re:
|
||||
mock_re.return_value = 'teststack/abcd1234'
|
||||
|
||||
manager._list = mock.MagicMock()
|
||||
manager.list(stack_id, resource_name)
|
||||
# Make sure url is correct.
|
||||
manager._list.assert_called_once_with(
|
||||
'/stacks/teststack/abcd1234/'
|
||||
'resources/%E5%B7%A5%E4%BD%9C/'
|
||||
'events', "events")
|
||||
mock_re.assert_called_once_with(stack_id)
|
||||
|
||||
def test_list_event_with_none_resource_name(self):
|
||||
stack_id = 'teststack',
|
||||
manager = events.EventManager(None)
|
||||
manager._list = mock.MagicMock()
|
||||
manager.list(stack_id)
|
||||
# Make sure url is correct.
|
||||
manager._list.assert_called_once_with('/stacks/teststack/'
|
||||
'events', "events")
|
||||
|
||||
def test_list_event_with_kwargs(self):
|
||||
stack_id = 'teststack',
|
||||
resource_name = 'testresource'
|
||||
kwargs = {'limit': 2,
|
||||
'marker': '6d6935f4-0ae5',
|
||||
'filters': {
|
||||
'resource_action': 'CREATE',
|
||||
'resource_status': 'COMPLETE'
|
||||
}}
|
||||
manager = events.EventManager(None)
|
||||
manager = events.EventManager(None)
|
||||
with mock.patch('heatclient.v1.events.EventManager._resolve_stack_id')\
|
||||
as mock_re:
|
||||
mock_re.return_value = 'teststack/abcd1234'
|
||||
|
||||
manager._list = mock.MagicMock()
|
||||
manager.list(stack_id, resource_name, **kwargs)
|
||||
# Make sure url is correct.
|
||||
self.assertEqual(1, manager._list.call_count)
|
||||
args = manager._list.call_args
|
||||
self.assertEqual(2, len(args[0]))
|
||||
url, param = args[0]
|
||||
self.assertEqual("events", param)
|
||||
base_url, query_params = utils.parse_query_url(url)
|
||||
expected_base_url = ('/stacks/teststack/abcd1234/'
|
||||
'resources/testresource/events')
|
||||
self.assertEqual(expected_base_url, base_url)
|
||||
expected_query_dict = {'marker': ['6d6935f4-0ae5'],
|
||||
'limit': ['2'],
|
||||
'resource_action': ['CREATE'],
|
||||
'resource_status': ['COMPLETE']}
|
||||
self.assertEqual(expected_query_dict, query_params)
|
||||
mock_re.assert_called_once_with(stack_id)
|
||||
|
||||
@mock.patch('heatclient.v1.events.EventManager._resolve_stack_id')
|
||||
@mock.patch('heatclient.common.utils.get_response_body')
|
||||
def test_get_event(self, mock_utils, mock_re):
|
||||
fields = {'stack_id': 'teststack',
|
||||
'resource_name': 'testresource',
|
||||
'event_id': '1'}
|
||||
|
||||
class FakeAPI(object):
|
||||
"""Fake API and ensure request url is correct."""
|
||||
|
||||
def json_request(self, *args, **kwargs):
|
||||
expect = ('GET',
|
||||
'/stacks/teststack/abcd1234/resources'
|
||||
'/testresource/events/1')
|
||||
assert args == expect
|
||||
return {}, {'event': []}
|
||||
|
||||
def get(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
manager = events.EventManager(FakeAPI())
|
||||
with mock.patch('heatclient.v1.events.Event'):
|
||||
mock_utils.return_value = {'event': []}
|
||||
mock_re.return_value = 'teststack/abcd1234'
|
||||
manager.get(**fields)
|
||||
mock_re.assert_called_once_with('teststack')
|
||||
|
||||
@mock.patch('heatclient.v1.events.EventManager._resolve_stack_id')
|
||||
@mock.patch('heatclient.common.utils.get_response_body')
|
||||
def test_get_event_with_unicode_resource_name(self, mock_utils, mock_re):
|
||||
fields = {'stack_id': 'teststack',
|
||||
'resource_name': u'\u5de5\u4f5c',
|
||||
'event_id': '1'}
|
||||
|
||||
class FakeAPI(object):
|
||||
"""Fake API and ensure request url is correct."""
|
||||
|
||||
def json_request(self, *args, **kwargs):
|
||||
expect = ('GET',
|
||||
'/stacks/teststack/abcd1234/resources'
|
||||
'/%E5%B7%A5%E4%BD%9C/events/1')
|
||||
assert args == expect
|
||||
return {}, {'event': []}
|
||||
|
||||
def get(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
manager = events.EventManager(FakeAPI())
|
||||
with mock.patch('heatclient.v1.events.Event'):
|
||||
mock_utils.return_value = {'event': []}
|
||||
mock_re.return_value = 'teststack/abcd1234'
|
||||
manager.get(**fields)
|
||||
mock_re.assert_called_once_with('teststack')
|
|
@ -1,177 +0,0 @@
|
|||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
#
|
||||
# Copyright 2015 IBM Corp.
|
||||
|
||||
import json
|
||||
import six
|
||||
import yaml
|
||||
|
||||
from heatclient.common import format_utils
|
||||
from heatclient.tests.unit.osc import utils
|
||||
|
||||
|
||||
columns = ['col1', 'col2', 'col3']
|
||||
data = ['abcde', ['fg', 'hi', 'jk'], {'lmnop': 'qrstu'}]
|
||||
|
||||
|
||||
class ShowJson(format_utils.JsonFormat):
|
||||
def take_action(self, parsed_args):
|
||||
return columns, data
|
||||
|
||||
|
||||
class ShowYaml(format_utils.YamlFormat):
|
||||
def take_action(self, parsed_args):
|
||||
return columns, data
|
||||
|
||||
|
||||
class ShowShell(format_utils.ShellFormat):
|
||||
def take_action(self, parsed_args):
|
||||
return columns, data
|
||||
|
||||
|
||||
class ShowValue(format_utils.ValueFormat):
|
||||
def take_action(self, parsed_args):
|
||||
return columns, data
|
||||
|
||||
|
||||
class TestFormats(utils.TestCommand):
|
||||
|
||||
def test_json_format(self):
|
||||
self.cmd = ShowJson(self.app, None)
|
||||
parsed_args = self.check_parser(self.cmd, [], [])
|
||||
expected = json.dumps(dict(zip(columns, data)), indent=2)
|
||||
|
||||
self.cmd.run(parsed_args)
|
||||
|
||||
self.assertEqual(expected, self.app.stdout.make_string())
|
||||
|
||||
def test_yaml_format(self):
|
||||
self.cmd = ShowYaml(self.app, None)
|
||||
parsed_args = self.check_parser(self.cmd, [], [])
|
||||
expected = yaml.safe_dump(dict(zip(columns, data)),
|
||||
default_flow_style=False)
|
||||
|
||||
self.cmd.run(parsed_args)
|
||||
|
||||
self.assertEqual(expected, self.app.stdout.make_string())
|
||||
|
||||
def test_shell_format(self):
|
||||
self.cmd = ShowShell(self.app, None)
|
||||
parsed_args = self.check_parser(self.cmd, [], [])
|
||||
expected = '''\
|
||||
col1="abcde"
|
||||
col2="['fg', 'hi', 'jk']"
|
||||
col3="{'lmnop': 'qrstu'}"
|
||||
'''
|
||||
|
||||
self.cmd.run(parsed_args)
|
||||
|
||||
self.assertEqual(expected, self.app.stdout.make_string())
|
||||
|
||||
def test_value_format(self):
|
||||
self.cmd = ShowValue(self.app, None)
|
||||
parsed_args = self.check_parser(self.cmd, [], [])
|
||||
expected = '''\
|
||||
abcde
|
||||
['fg', 'hi', 'jk']
|
||||
{'lmnop': 'qrstu'}
|
||||
'''
|
||||
|
||||
self.cmd.run(parsed_args)
|
||||
|
||||
self.assertEqual(expected, self.app.stdout.make_string())
|
||||
|
||||
def test_indent_and_truncate(self):
|
||||
self.assertIsNone(format_utils.indent_and_truncate(None))
|
||||
self.assertIsNone(format_utils.indent_and_truncate(None,
|
||||
truncate=True))
|
||||
self.assertEqual(
|
||||
'',
|
||||
format_utils.indent_and_truncate(''))
|
||||
self.assertEqual(
|
||||
'one',
|
||||
format_utils.indent_and_truncate('one'))
|
||||
self.assertIsNone(format_utils.indent_and_truncate(None, spaces=2))
|
||||
self.assertEqual(
|
||||
'',
|
||||
format_utils.indent_and_truncate('', spaces=2))
|
||||
self.assertEqual(
|
||||
' one',
|
||||
format_utils.indent_and_truncate('one', spaces=2))
|
||||
self.assertEqual(
|
||||
'one\ntwo\nthree\nfour\nfive',
|
||||
format_utils.indent_and_truncate('one\ntwo\nthree\nfour\nfive'))
|
||||
self.assertEqual(
|
||||
'three\nfour\nfive',
|
||||
format_utils.indent_and_truncate(
|
||||
'one\ntwo\nthree\nfour\nfive',
|
||||
truncate=True,
|
||||
truncate_limit=3))
|
||||
self.assertEqual(
|
||||
' and so on\n three\n four\n five\n truncated',
|
||||
format_utils.indent_and_truncate(
|
||||
'one\ntwo\nthree\nfour\nfive',
|
||||
spaces=2,
|
||||
truncate=True,
|
||||
truncate_limit=3,
|
||||
truncate_prefix='and so on',
|
||||
truncate_postfix='truncated'))
|
||||
|
||||
def test_print_software_deployment_output(self):
|
||||
out = six.StringIO()
|
||||
format_utils.print_software_deployment_output(
|
||||
{'deploy_stdout': ''}, out=out, name='deploy_stdout')
|
||||
self.assertEqual(
|
||||
' deploy_stdout: |\n\n',
|
||||
out.getvalue())
|
||||
ov = {'deploy_stdout': '', 'deploy_stderr': '1\n2\n3\n4\n5\n6\n7\n8\n9'
|
||||
'\n10\n11',
|
||||
'deploy_status_code': 0}
|
||||
out = six.StringIO()
|
||||
format_utils.print_software_deployment_output(ov, out=out,
|
||||
name='deploy_stderr')
|
||||
self.assertEqual(
|
||||
u'''\
|
||||
deploy_stderr: |
|
||||
...
|
||||
2
|
||||
3
|
||||
4
|
||||
5
|
||||
6
|
||||
7
|
||||
8
|
||||
9
|
||||
10
|
||||
11
|
||||
(truncated, view all with --long)
|
||||
''', out.getvalue())
|
||||
out = six.StringIO()
|
||||
format_utils.print_software_deployment_output(ov, out=out,
|
||||
name='deploy_stderr',
|
||||
long=True)
|
||||
self.assertEqual(
|
||||
u'''\
|
||||
deploy_stderr: |
|
||||
1
|
||||
2
|
||||
3
|
||||
4
|
||||
5
|
||||
6
|
||||
7
|
||||
8
|
||||
9
|
||||
10
|
||||
11
|
||||
''', out.getvalue())
|
|
@ -1,75 +0,0 @@
|
|||
# Copyright 2015 Huawei.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import testtools
|
||||
|
||||
from heatclient.common import base
|
||||
from heatclient.v1 import events
|
||||
from heatclient.v1 import stacks
|
||||
|
||||
|
||||
class BaseTest(testtools.TestCase):
|
||||
|
||||
def test_two_resources_with_same_id_are_not_equal(self):
|
||||
# Two resources with same ID: never equal if their info is not equal
|
||||
r1 = base.Resource(None, {'id': 1, 'name': 'hi'})
|
||||
r2 = base.Resource(None, {'id': 1, 'name': 'hello'})
|
||||
self.assertNotEqual(r1, r2)
|
||||
|
||||
def test_two_resources_with_same_id_and_info_are_equal(self):
|
||||
# Two resources with same ID: equal if their info is equal
|
||||
r1 = base.Resource(None, {'id': 1, 'name': 'hello'})
|
||||
r2 = base.Resource(None, {'id': 1, 'name': 'hello'})
|
||||
self.assertEqual(r1, r2)
|
||||
|
||||
def test_two_resources_with_diff_type_are_not_equal(self):
|
||||
# Two resoruces of different types: never equal
|
||||
r1 = base.Resource(None, {'id': 1})
|
||||
r2 = events.Event(None, {'id': 1})
|
||||
self.assertNotEqual(r1, r2)
|
||||
|
||||
def test_two_resources_with_no_id_are_equal(self):
|
||||
# Two resources with no ID: equal if their info is equal
|
||||
r1 = base.Resource(None, {'name': 'joe', 'age': 12})
|
||||
r2 = base.Resource(None, {'name': 'joe', 'age': 12})
|
||||
self.assertEqual(r1, r2)
|
||||
|
||||
def test_is_same_object(self):
|
||||
# Two resources with same type and same ID: is same object
|
||||
r1 = base.Resource(None, {'id': 1, 'name': 'hi'})
|
||||
r2 = base.Resource(None, {'id': 1, 'name': 'hello'})
|
||||
self.assertTrue(r1.is_same_obj(r2))
|
||||
self.assertTrue(r2.is_same_obj(r1))
|
||||
|
||||
def test_is_diff_object_with_diff_id(self):
|
||||
# Two resources with same type and different ID: is different object
|
||||
r1 = base.Resource(None, {'id': 1, 'name': 'hello'})
|
||||
r2 = base.Resource(None, {'id': 2, 'name': 'hello'})
|
||||
self.assertFalse(r1.is_same_obj(r2))
|
||||
self.assertFalse(r2.is_same_obj(r1))
|
||||
|
||||
def test_is_diff_object_with_diff_type(self):
|
||||
# Two resources with different types: is different object
|
||||
r1 = events.Event(None, {'id': 1})
|
||||
r2 = stacks.Stack(None, {'id': 1})
|
||||
self.assertFalse(r1.is_same_obj(r2))
|
||||
self.assertFalse(r2.is_same_obj(r1))
|
||||
|
||||
def test_is_diff_object_with_no_id(self):
|
||||
# Two resources with no ID: is different object
|
||||
r1 = base.Resource(None, {'name': 'joe', 'age': 12})
|
||||
r2 = base.Resource(None, {'name': 'joe', 'age': 12})
|
||||
self.assertFalse(r1.is_same_obj(r2))
|
||||
self.assertFalse(r2.is_same_obj(r1))
|
|
@ -1,101 +0,0 @@
|
|||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
#
|
||||
|
||||
import json
|
||||
import os
|
||||
|
||||
import six
|
||||
|
||||
from heatclient.common import resource_formatter
|
||||
from heatclient.osc.v1 import resource
|
||||
from heatclient.tests.unit.osc.v1 import fakes as orchestration_fakes
|
||||
from heatclient.v1 import resources as v1_resources
|
||||
|
||||
|
||||
TEST_VAR_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__),
|
||||
'var'))
|
||||
|
||||
|
||||
class TestStackResourceListDotFormat(orchestration_fakes.TestOrchestrationv1):
|
||||
|
||||
response_path = os.path.join(TEST_VAR_DIR, 'dot_test.json')
|
||||
|
||||
data = '''digraph G {
|
||||
graph [
|
||||
fontsize=10 fontname="Verdana" compound=true rankdir=LR
|
||||
]
|
||||
r_f34a35baf594b319a741 [label="rg1
|
||||
OS::Heat::ResourceGroup" ];
|
||||
r_121e343b017a6d246f36 [label="random2
|
||||
OS::Heat::RandomString" ];
|
||||
r_dbcae38ad41dc991751d [label="random1
|
||||
OS::Heat::RandomString" style=filled color=red];
|
||||
|
||||
subgraph cluster_stack_16437984473ec64a8e6c {
|
||||
label="rg1";
|
||||
r_30e9aa76bc0d53310cde [label="1
|
||||
OS::Heat::ResourceGroup" ];
|
||||
r_63c05d424cb708f1599f [label="0
|
||||
OS::Heat::ResourceGroup" ];
|
||||
|
||||
}
|
||||
|
||||
subgraph cluster_stack_fbfb461c8cc84b686c08 {
|
||||
label="1";
|
||||
r_e2e5c36ae18e29d9c299 [label="1
|
||||
OS::Heat::RandomString" ];
|
||||
r_56c62630a0d655bce234 [label="0
|
||||
OS::Heat::RandomString" ];
|
||||
|
||||
}
|
||||
|
||||
subgraph cluster_stack_d427657dfccc28a131a7 {
|
||||
label="0";
|
||||
r_240756913e2e940387ff [label="1
|
||||
OS::Heat::RandomString" ];
|
||||
r_81c64c43d9131aceedbb [label="0
|
||||
OS::Heat::RandomString" ];
|
||||
|
||||
}
|
||||
|
||||
r_f34a35baf594b319a741 -> r_30e9aa76bc0d53310cde [
|
||||
color=dimgray lhead=cluster_stack_16437984473ec64a8e6c arrowhead=none
|
||||
];
|
||||
r_30e9aa76bc0d53310cde -> r_e2e5c36ae18e29d9c299 [
|
||||
color=dimgray lhead=cluster_stack_fbfb461c8cc84b686c08 arrowhead=none
|
||||
];
|
||||
r_63c05d424cb708f1599f -> r_240756913e2e940387ff [
|
||||
color=dimgray lhead=cluster_stack_d427657dfccc28a131a7 arrowhead=none
|
||||
];
|
||||
|
||||
r_dbcae38ad41dc991751d -> r_121e343b017a6d246f36;
|
||||
|
||||
}
|
||||
'''
|
||||
|
||||
def setUp(self):
|
||||
super(TestStackResourceListDotFormat, self).setUp()
|
||||
self.resource_client = self.app.client_manager.orchestration.resources
|
||||
self.cmd = resource.ResourceList(self.app, None)
|
||||
with open(self.response_path) as f:
|
||||
response = json.load(f)
|
||||
self.resources = []
|
||||
for r in response['resources']:
|
||||
self.resources.append(v1_resources.Resource(None, r))
|
||||
|
||||
def test_resource_list(self):
|
||||
out = six.StringIO()
|
||||
formatter = resource_formatter.ResourceDotFormatter()
|
||||
formatter.emit_list(None, self.resources, out, None)
|
||||
|
||||
self.assertEqual(self.data, out.getvalue())
|
|
@ -1,114 +0,0 @@
|
|||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import mock
|
||||
import testtools
|
||||
|
||||
from heatclient.common import utils
|
||||
from heatclient.v1 import resource_types
|
||||
|
||||
|
||||
class ResourceTypeManagerTest(testtools.TestCase):
|
||||
|
||||
def _base_test(self, expect, key):
|
||||
|
||||
class FakeAPI(object):
|
||||
"""Fake API and ensure request url is correct."""
|
||||
|
||||
def get(self, *args, **kwargs):
|
||||
assert ('GET', args[0]) == expect
|
||||
|
||||
def json_request(self, *args, **kwargs):
|
||||
assert args == expect
|
||||
ret = key and {key: []} or {}
|
||||
return {}, {key: ret}
|
||||
|
||||
def raw_request(self, *args, **kwargs):
|
||||
assert args == expect
|
||||
return {}
|
||||
|
||||
def head(self, url, **kwargs):
|
||||
return self.json_request("HEAD", url, **kwargs)
|
||||
|
||||
def post(self, url, **kwargs):
|
||||
return self.json_request("POST", url, **kwargs)
|
||||
|
||||
def put(self, url, **kwargs):
|
||||
return self.json_request("PUT", url, **kwargs)
|
||||
|
||||
def delete(self, url, **kwargs):
|
||||
return self.raw_request("DELETE", url, **kwargs)
|
||||
|
||||
def patch(self, url, **kwargs):
|
||||
return self.json_request("PATCH", url, **kwargs)
|
||||
|
||||
manager = resource_types.ResourceTypeManager(FakeAPI())
|
||||
return manager
|
||||
|
||||
def test_list_types(self):
|
||||
key = 'resource_types'
|
||||
expect = ('GET', '/resource_types')
|
||||
|
||||
class FakeResponse(object):
|
||||
def json(self):
|
||||
return {key: {}}
|
||||
|
||||
class FakeClient(object):
|
||||
def get(self, *args, **kwargs):
|
||||
assert ('GET', args[0]) == expect
|
||||
return FakeResponse()
|
||||
|
||||
manager = resource_types.ResourceTypeManager(FakeClient())
|
||||
manager.list()
|
||||
|
||||
def test_list_types_with_filters(self):
|
||||
filters = {'name': 'OS::Keystone::*',
|
||||
'version': '5.0.0',
|
||||
'support_status': 'SUPPORTED'}
|
||||
|
||||
manager = resource_types.ResourceTypeManager(None)
|
||||
with mock.patch.object(manager, '_list') as mock_list:
|
||||
mock_list.return_value = None
|
||||
manager.list(filters=filters)
|
||||
self.assertEqual(1, mock_list.call_count)
|
||||
url, param = mock_list.call_args[0]
|
||||
self.assertEqual("resource_types", param)
|
||||
base_url, query_params = utils.parse_query_url(url)
|
||||
self.assertEqual('/%s' % manager.KEY, base_url)
|
||||
# parameters in query_params is in list format, so filter params
|
||||
# are made to be inline with it
|
||||
filters_params = {}
|
||||
for item in filters:
|
||||
filters_params[item] = [filters[item]]
|
||||
|
||||
self.assertEqual(filters_params, query_params)
|
||||
|
||||
@mock.patch.object(utils, 'get_response_body')
|
||||
def test_get(self, mock_utils):
|
||||
key = 'resource_types'
|
||||
resource_type = 'OS::Nova::KeyPair'
|
||||
expect = ('GET', '/resource_types/OS%3A%3ANova%3A%3AKeyPair')
|
||||
manager = self._base_test(expect, key)
|
||||
mock_utils.return_value = None
|
||||
manager.get(resource_type)
|
||||
|
||||
@mock.patch.object(utils, 'get_response_body')
|
||||
def test_generate_template(self, mock_utils):
|
||||
key = 'resource_types'
|
||||
resource_type = 'OS::Nova::KeyPair'
|
||||
template_type = 'cfn'
|
||||
expect = ('GET', '/resource_types/OS%3A%3ANova%3A%3AKeyPair/template'
|
||||
'?template_type=cfn')
|
||||
manager = self._base_test(expect, key)
|
||||
mock_utils.return_value = None
|
||||
manager.generate_template(resource_type, template_type)
|
|
@ -1,232 +0,0 @@
|
|||
# Copyright 2013 IBM Corp.
|
||||
#
|
||||
# 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 mox
|
||||
from six.moves.urllib import parse
|
||||
import testtools
|
||||
|
||||
from heatclient.common import utils
|
||||
from heatclient.v1 import resources
|
||||
|
||||
|
||||
class ResourceManagerTest(testtools.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(ResourceManagerTest, self).setUp()
|
||||
self.m = mox.Mox()
|
||||
self.addCleanup(self.m.UnsetStubs)
|
||||
|
||||
def _base_test(self, expect, key):
|
||||
|
||||
class FakeAPI(object):
|
||||
"""Fake API and ensure request url is correct."""
|
||||
|
||||
def get(self, *args, **kwargs):
|
||||
assert ('GET', args[0]) == expect
|
||||
|
||||
def json_request(self, *args, **kwargs):
|
||||
assert args == expect
|
||||
ret = key and {key: []} or {}
|
||||
return {}, {key: ret}
|
||||
|
||||
def raw_request(self, *args, **kwargs):
|
||||
assert args == expect
|
||||
return {}
|
||||
|
||||
def head(self, url, **kwargs):
|
||||
return self.json_request("HEAD", url, **kwargs)
|
||||
|
||||
def post(self, url, **kwargs):
|
||||
return self.json_request("POST", url, **kwargs)
|
||||
|
||||
def put(self, url, **kwargs):
|
||||
return self.json_request("PUT", url, **kwargs)
|
||||
|
||||
def delete(self, url, **kwargs):
|
||||
return self.raw_request("DELETE", url, **kwargs)
|
||||
|
||||
def patch(self, url, **kwargs):
|
||||
return self.json_request("PATCH", url, **kwargs)
|
||||
|
||||
manager = resources.ResourceManager(FakeAPI())
|
||||
self.m.StubOutWithMock(manager, '_resolve_stack_id')
|
||||
self.m.StubOutWithMock(utils, 'get_response_body')
|
||||
utils.get_response_body(mox.IgnoreArg()).AndReturn(
|
||||
{key: key and {key: []} or {}})
|
||||
manager._resolve_stack_id('teststack').AndReturn('teststack/abcd1234')
|
||||
self.m.ReplayAll()
|
||||
|
||||
return manager
|
||||
|
||||
def test_get(self):
|
||||
fields = {'stack_id': 'teststack',
|
||||
'resource_name': 'testresource'}
|
||||
expect = ('GET',
|
||||
'/stacks/teststack/abcd1234/resources'
|
||||
'/testresource')
|
||||
key = 'resource'
|
||||
|
||||
manager = self._base_test(expect, key)
|
||||
manager.get(**fields)
|
||||
self.m.VerifyAll()
|
||||
|
||||
def test_get_with_attr(self):
|
||||
fields = {'stack_id': 'teststack',
|
||||
'resource_name': 'testresource',
|
||||
'with_attr': ['attr_a', 'attr_b']}
|
||||
expect = ('GET',
|
||||
'/stacks/teststack/abcd1234/resources'
|
||||
'/testresource?with_attr=attr_a&with_attr=attr_b')
|
||||
key = 'resource'
|
||||
|
||||
manager = self._base_test(expect, key)
|
||||
manager.get(**fields)
|
||||
self.m.VerifyAll()
|
||||
|
||||
def test_get_with_unicode_resource_name(self):
|
||||
fields = {'stack_id': 'teststack',
|
||||
'resource_name': u'\u5de5\u4f5c'}
|
||||
expect = ('GET',
|
||||
'/stacks/teststack/abcd1234/resources'
|
||||
'/%E5%B7%A5%E4%BD%9C')
|
||||
key = 'resource'
|
||||
|
||||
manager = self._base_test(expect, key)
|
||||
manager.get(**fields)
|
||||
self.m.VerifyAll()
|
||||
|
||||
def test_list(self):
|
||||
self._test_list(
|
||||
fields={'stack_id': 'teststack'},
|
||||
expect='/stacks/teststack/resources')
|
||||
|
||||
def test_list_nested(self):
|
||||
self._test_list(
|
||||
fields={'stack_id': 'teststack', 'nested_depth': '99'},
|
||||
expect='/stacks/teststack/resources?%s' % parse.urlencode({
|
||||
'nested_depth': 99
|
||||
}, True)
|
||||
)
|
||||
|
||||
def test_list_filtering(self):
|
||||
self._test_list(
|
||||
fields={'stack_id': 'teststack', 'filters': {'name': 'rsc_1'}},
|
||||
expect='/stacks/teststack/resources?%s' % parse.urlencode({
|
||||
'name': 'rsc_1'
|
||||
}, True)
|
||||
)
|
||||
|
||||
def test_list_detail(self):
|
||||
self._test_list(
|
||||
fields={'stack_id': 'teststack', 'with_detail': 'True'},
|
||||
expect='/stacks/teststack/resources?%s' % parse.urlencode({
|
||||
'with_detail': True,
|
||||
}, True)
|
||||
)
|
||||
|
||||
def _test_list(self, fields, expect):
|
||||
key = 'resources'
|
||||
|
||||
class FakeResponse(object):
|
||||
def json(self):
|
||||
return {key: {}}
|
||||
|
||||
class FakeClient(object):
|
||||
def get(self, *args, **kwargs):
|
||||
assert args[0] == expect
|
||||
return FakeResponse()
|
||||
|
||||
manager = resources.ResourceManager(FakeClient())
|
||||
manager.list(**fields)
|
||||
|
||||
def test_metadata(self):
|
||||
fields = {'stack_id': 'teststack',
|
||||
'resource_name': 'testresource'}
|
||||
expect = ('GET',
|
||||
'/stacks/teststack/abcd1234/resources'
|
||||
'/testresource/metadata')
|
||||
key = 'metadata'
|
||||
|
||||
manager = self._base_test(expect, key)
|
||||
manager.metadata(**fields)
|
||||
self.m.VerifyAll()
|
||||
|
||||
def test_generate_template(self):
|
||||
fields = {'resource_name': 'testresource'}
|
||||
expect = ('GET', '/resource_types/testresource/template')
|
||||
key = None
|
||||
|
||||
class FakeAPI(object):
|
||||
"""Fake API and ensure request url is correct."""
|
||||
|
||||
def get(self, *args, **kwargs):
|
||||
assert ('GET', args[0]) == expect
|
||||
|
||||
def json_request(self, *args, **kwargs):
|
||||
assert args == expect
|
||||
ret = key and {key: []} or {}
|
||||
return {}, {key: ret}
|
||||
|
||||
manager = resources.ResourceManager(FakeAPI())
|
||||
self.m.StubOutWithMock(utils, 'get_response_body')
|
||||
utils.get_response_body(mox.IgnoreArg()).AndReturn(
|
||||
{key: key and {key: []} or {}})
|
||||
self.m.ReplayAll()
|
||||
|
||||
manager.generate_template(**fields)
|
||||
self.m.VerifyAll()
|
||||
|
||||
def test_signal(self):
|
||||
fields = {'stack_id': 'teststack',
|
||||
'resource_name': 'testresource',
|
||||
'data': 'Some content'}
|
||||
expect = ('POST',
|
||||
'/stacks/teststack/abcd1234/resources'
|
||||
'/testresource/signal')
|
||||
key = 'signal'
|
||||
|
||||
manager = self._base_test(expect, key)
|
||||
manager.signal(**fields)
|
||||
self.m.VerifyAll()
|
||||
|
||||
def test_mark_unhealthy(self):
|
||||
fields = {'stack_id': 'teststack',
|
||||
'resource_name': 'testresource',
|
||||
'mark_unhealthy': 'True',
|
||||
'resource_status_reason': 'Anything'}
|
||||
expect = ('PATCH',
|
||||
'/stacks/teststack/abcd1234/resources'
|
||||
'/testresource')
|
||||
key = 'mark_unhealthy'
|
||||
|
||||
manager = self._base_test(expect, key)
|
||||
manager.mark_unhealthy(**fields)
|
||||
self.m.VerifyAll()
|
||||
|
||||
|
||||
class ResourceStackNameTest(testtools.TestCase):
|
||||
|
||||
def test_stack_name(self):
|
||||
resource = resources.Resource(None, {"links": [{
|
||||
"href": "http://heat.example.com:8004/foo/12/resources/foobar",
|
||||
"rel": "self"
|
||||
}, {
|
||||
"href": "http://heat.example.com:8004/foo/12",
|
||||
"rel": "stack"
|
||||
}]})
|
||||
self.assertEqual('foo', resource.stack_name)
|
||||
|
||||
def test_stack_name_no_links(self):
|
||||
resource = resources.Resource(None, {})
|
||||
self.assertIsNone(resource.stack_name)
|
|
@ -1,58 +0,0 @@
|
|||
# Copyright 2012 OpenStack Foundation
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import testtools
|
||||
|
||||
from heatclient import exc
|
||||
from heatclient.v1 import services
|
||||
|
||||
|
||||
class ManageServiceTest(testtools.TestCase):
|
||||
def setUp(self):
|
||||
super(ManageServiceTest, self).setUp()
|
||||
|
||||
def test_service_list(self):
|
||||
class FakeResponse(object):
|
||||
def json(self):
|
||||
return {'services': []}
|
||||
|
||||
class FakeClient(object):
|
||||
def get(self, *args, **kwargs):
|
||||
assert args[0] == ('/services')
|
||||
return FakeResponse()
|
||||
|
||||
manager = services.ServiceManager(FakeClient())
|
||||
self.assertEqual([], manager.list())
|
||||
|
||||
def test_service_list_403(self):
|
||||
class FakeClient403(object):
|
||||
|
||||
def get(self, *args, **kwargs):
|
||||
assert args[0] == ('/services')
|
||||
raise exc.HTTPForbidden()
|
||||
|
||||
manager = services.ServiceManager(FakeClient403())
|
||||
self.assertRaises(exc.HTTPForbidden,
|
||||
manager.list)
|
||||
|
||||
def test_service_list_503(self):
|
||||
class FakeClient503(object):
|
||||
def get(self, *args, **kwargs):
|
||||
assert args[0] == ('/services')
|
||||
raise exc.HTTPServiceUnavailable()
|
||||
|
||||
manager = services.ServiceManager(FakeClient503())
|
||||
self.assertRaises(exc.HTTPServiceUnavailable,
|
||||
manager.list)
|
File diff suppressed because it is too large
Load Diff
|
@ -1,110 +0,0 @@
|
|||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import mock
|
||||
import testtools
|
||||
|
||||
from heatclient.common import utils
|
||||
from heatclient.v1 import software_configs
|
||||
|
||||
|
||||
class SoftwareConfigTest(testtools.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(SoftwareConfigTest, self).setUp()
|
||||
config_id = 'bca6871d-86c0-4aff-b792-58a1f6947b57'
|
||||
self.config = software_configs.SoftwareConfig(mock.MagicMock(),
|
||||
info={'id': config_id})
|
||||
self.config_id = config_id
|
||||
|
||||
def test_delete(self):
|
||||
self.config.manager.delete.return_value = None
|
||||
self.assertIsNone(self.config.delete())
|
||||
kwargs = self.config.manager.delete.call_args[1]
|
||||
self.assertEqual(self.config_id, kwargs['config_id'])
|
||||
|
||||
def test_data(self):
|
||||
self.assertEqual(
|
||||
"<SoftwareConfig {'id': '%s'}>" % self.config_id, str(self.config))
|
||||
self.config.manager.data.return_value = None
|
||||
self.config.data(name='config_mysql')
|
||||
kwargs = self.config.manager.data.call_args[1]
|
||||
self.assertEqual('config_mysql', kwargs['name'])
|
||||
|
||||
|
||||
class SoftwareConfigManagerTest(testtools.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(SoftwareConfigManagerTest, self).setUp()
|
||||
self.manager = software_configs.SoftwareConfigManager(mock.MagicMock())
|
||||
|
||||
def test_list(self):
|
||||
config_id = 'bca6871d-86c0-4aff-b792-58a1f6947b57'
|
||||
self.manager.client.json_request.return_value = (
|
||||
{},
|
||||
{'software_configs': []})
|
||||
result = self.manager.list(limit=1, marker=config_id)
|
||||
self.assertEqual([], result)
|
||||
call_args = self.manager.client.get.call_args
|
||||
self.assertEqual(
|
||||
('/software_configs?limit=1&marker=%s' % config_id,), *call_args)
|
||||
|
||||
@mock.patch.object(utils, 'get_response_body')
|
||||
def test_get(self, mock_body):
|
||||
config_id = 'bca6871d-86c0-4aff-b792-58a1f6947b57'
|
||||
data = {
|
||||
'id': config_id,
|
||||
'name': 'config_mysql',
|
||||
'group': 'Heat::Shell',
|
||||
'config': '#!/bin/bash',
|
||||
'inputs': [],
|
||||
'ouputs': [],
|
||||
'options': []}
|
||||
|
||||
self.manager.client.json_request.return_value = (
|
||||
{}, {'software_config': data})
|
||||
mock_body.return_value = {'software_config': data}
|
||||
result = self.manager.get(config_id=config_id)
|
||||
self.assertEqual(software_configs.SoftwareConfig(self.manager, data),
|
||||
result)
|
||||
call_args = self.manager.client.get.call_args
|
||||
self.assertEqual(
|
||||
('/software_configs/%s' % config_id,), *call_args)
|
||||
|
||||
@mock.patch.object(utils, 'get_response_body')
|
||||
def test_create(self, mock_body):
|
||||
config_id = 'bca6871d-86c0-4aff-b792-58a1f6947b57'
|
||||
body = {
|
||||
'name': 'config_mysql',
|
||||
'group': 'Heat::Shell',
|
||||
'config': '#!/bin/bash',
|
||||
'inputs': [],
|
||||
'ouputs': [],
|
||||
'options': []}
|
||||
data = body.copy()
|
||||
data['id'] = config_id
|
||||
self.manager.client.json_request.return_value = (
|
||||
{}, {'software_config': data})
|
||||
mock_body.return_value = {'software_config': data}
|
||||
result = self.manager.create(**body)
|
||||
self.assertEqual(software_configs.SoftwareConfig(self.manager, data),
|
||||
result)
|
||||
args, kargs = self.manager.client.post.call_args
|
||||
self.assertEqual('/software_configs', args[0])
|
||||
self.assertEqual({'data': body}, kargs)
|
||||
|
||||
def test_delete(self):
|
||||
config_id = 'bca6871d-86c0-4aff-b792-58a1f6947b57'
|
||||
self.manager.delete(config_id)
|
||||
call_args = self.manager.client.delete.call_args
|
||||
self.assertEqual(
|
||||
('/software_configs/%s' % config_id,), *call_args)
|
|
@ -1,166 +0,0 @@
|
|||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import mock
|
||||
import testtools
|
||||
|
||||
from heatclient.common import utils
|
||||
from heatclient.v1 import software_deployments
|
||||
|
||||
|
||||
class SoftwareDeploymentTest(testtools.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(SoftwareDeploymentTest, self).setUp()
|
||||
deployment_id = 'bca6871d-86c0-4aff-b792-58a1f6947b57'
|
||||
self.deployment = software_deployments.SoftwareDeployment(
|
||||
mock.MagicMock(), info={'id': deployment_id})
|
||||
self.deployment_id = deployment_id
|
||||
|
||||
def test_delete(self):
|
||||
self.deployment.manager.delete.return_value = None
|
||||
self.assertIsNone(self.deployment.delete())
|
||||
kwargs = self.deployment.manager.delete.call_args[1]
|
||||
self.assertEqual(self.deployment_id, kwargs['deployment_id'])
|
||||
|
||||
def test_update(self):
|
||||
self.assertEqual(
|
||||
"<SoftwareDeployment {'id': '%s'}>" % self.deployment_id,
|
||||
str(self.deployment))
|
||||
self.deployment.manager.update.return_value = None
|
||||
config_id = 'd00ba4aa-db33-42e1-92f4-2a6469260107'
|
||||
self.assertIsNone(self.deployment.update(config_id=config_id))
|
||||
kwargs = self.deployment.manager.update.call_args[1]
|
||||
self.assertEqual(self.deployment_id, kwargs['deployment_id'])
|
||||
self.assertEqual(config_id, kwargs['config_id'])
|
||||
|
||||
|
||||
class SoftwareDeploymentManagerTest(testtools.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(SoftwareDeploymentManagerTest, self).setUp()
|
||||
self.manager = software_deployments.SoftwareDeploymentManager(
|
||||
mock.MagicMock())
|
||||
|
||||
def test_list(self):
|
||||
server_id = 'fc01f89f-e151-4dc5-9c28-543c0d20ed6a'
|
||||
self.manager.client.json_request.return_value = (
|
||||
{},
|
||||
{'software_deployments': []})
|
||||
result = self.manager.list(server_id=server_id)
|
||||
self.assertEqual([], result)
|
||||
call_args = self.manager.client.get.call_args
|
||||
self.assertEqual(
|
||||
('/software_deployments?server_id=%s' % server_id,),
|
||||
*call_args)
|
||||
|
||||
@mock.patch.object(utils, 'get_response_body')
|
||||
def test_metadata(self, mock_utils):
|
||||
server_id = 'fc01f89f-e151-4dc5-9c28-543c0d20ed6a'
|
||||
metadata = {
|
||||
'group1': [{'foo': 'bar'}],
|
||||
'group2': [{'foo': 'bar'}, {'bar': 'baz'}],
|
||||
}
|
||||
self.manager.client.get.return_value = {}
|
||||
mock_utils.return_value = {'metadata': metadata}
|
||||
result = self.manager.metadata(server_id=server_id)
|
||||
self.assertEqual(metadata, result)
|
||||
call_args = self.manager.client.get.call_args
|
||||
self.assertEqual(
|
||||
'/software_deployments/metadata/%s' % server_id,
|
||||
call_args[0][0])
|
||||
|
||||
@mock.patch.object(utils, 'get_response_body')
|
||||
def test_get(self, mock_utils):
|
||||
deployment_id = 'bca6871d-86c0-4aff-b792-58a1f6947b57'
|
||||
config_id = 'd00ba4aa-db33-42e1-92f4-2a6469260107'
|
||||
server_id = 'fb322564-7927-473d-8aad-68ae7fbf2abf'
|
||||
data = {
|
||||
'id': deployment_id,
|
||||
'server_id': server_id,
|
||||
'input_values': {},
|
||||
'output_values': {},
|
||||
'action': 'INIT',
|
||||
'status': 'COMPLETE',
|
||||
'status_reason': None,
|
||||
'signal_id': None,
|
||||
'config_id': config_id,
|
||||
'config': '#!/bin/bash',
|
||||
'name': 'config_mysql',
|
||||
'group': 'Heat::Shell',
|
||||
'inputs': [],
|
||||
'outputs': [],
|
||||
'options': []}
|
||||
|
||||
self.manager.client.get.return_value = {}
|
||||
mock_utils.return_value = {'software_deployment': data}
|
||||
result = self.manager.get(deployment_id=deployment_id)
|
||||
self.assertEqual(software_deployments.SoftwareDeployment(
|
||||
self.manager, data), result)
|
||||
call_args = self.manager.client.get.call_args
|
||||
self.assertEqual(
|
||||
('/software_deployments/%s' % deployment_id,), *call_args)
|
||||
|
||||
@mock.patch.object(utils, 'get_response_body')
|
||||
def test_create(self, mock_utils):
|
||||
deployment_id = 'bca6871d-86c0-4aff-b792-58a1f6947b57'
|
||||
config_id = 'd00ba4aa-db33-42e1-92f4-2a6469260107'
|
||||
server_id = 'fb322564-7927-473d-8aad-68ae7fbf2abf'
|
||||
body = {
|
||||
'server_id': server_id,
|
||||
'input_values': {},
|
||||
'action': 'INIT',
|
||||
'status': 'COMPLETE',
|
||||
'status_reason': None,
|
||||
'signal_id': None,
|
||||
'config_id': config_id}
|
||||
data = body.copy()
|
||||
data['id'] = deployment_id
|
||||
self.manager.client.post.return_value = {}
|
||||
mock_utils.return_value = {'software_deployment': data}
|
||||
result = self.manager.create(**body)
|
||||
self.assertEqual(software_deployments.SoftwareDeployment(
|
||||
self.manager, data), result)
|
||||
args, kwargs = self.manager.client.post.call_args
|
||||
self.assertEqual('/software_deployments', args[0])
|
||||
self.assertEqual({'data': body}, kwargs)
|
||||
|
||||
def test_delete(self):
|
||||
deployment_id = 'bca6871d-86c0-4aff-b792-58a1f6947b57'
|
||||
self.manager.delete(deployment_id)
|
||||
call_args = self.manager.client.delete.call_args
|
||||
self.assertEqual(
|
||||
('/software_deployments/%s' % deployment_id,), *call_args)
|
||||
|
||||
@mock.patch.object(utils, 'get_response_body')
|
||||
def test_update(self, mock_utils):
|
||||
deployment_id = 'bca6871d-86c0-4aff-b792-58a1f6947b57'
|
||||
config_id = 'd00ba4aa-db33-42e1-92f4-2a6469260107'
|
||||
server_id = 'fb322564-7927-473d-8aad-68ae7fbf2abf'
|
||||
body = {
|
||||
'server_id': server_id,
|
||||
'input_values': {},
|
||||
'action': 'DEPLOYED',
|
||||
'status': 'COMPLETE',
|
||||
'status_reason': None,
|
||||
'signal_id': None,
|
||||
'config_id': config_id}
|
||||
data = body.copy()
|
||||
data['id'] = deployment_id
|
||||
self.manager.client.put.return_value = {}
|
||||
mock_utils.return_value = {'software_deployment': data}
|
||||
result = self.manager.update(deployment_id, **body)
|
||||
self.assertEqual(software_deployments.SoftwareDeployment(
|
||||
self.manager, data), result)
|
||||
args, kwargs = self.manager.client.put.call_args
|
||||
self.assertEqual('/software_deployments/%s' % deployment_id, args[0])
|
||||
self.assertEqual({'data': body}, kwargs)
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue