Initial version of python-stacktaskclient
Based on python-heatclient 04b3880 (2015-06-04) Change-Id: Ie54c889a4b89ec32f9a00b954560929f35712021
This commit is contained in:
parent
ace3212557
commit
f85dcba1a1
18
.gitignore
vendored
Normal file
18
.gitignore
vendored
Normal file
@ -0,0 +1,18 @@
|
||||
.coverage
|
||||
.venv
|
||||
*,cover
|
||||
cover
|
||||
*.pyc
|
||||
AUTHORS
|
||||
build
|
||||
dist
|
||||
ChangeLog
|
||||
run_tests.err.log
|
||||
.tox
|
||||
doc/source/api
|
||||
doc/build
|
||||
*.egg
|
||||
stacktaskclient/versioninfo
|
||||
*.egg-info
|
||||
*.log
|
||||
.testrepository
|
4
.testr.conf
Normal file
4
.testr.conf
Normal file
@ -0,0 +1,4 @@
|
||||
[DEFAULT]
|
||||
test_command=${PYTHON:-python} -m subunit.run discover -t ./ ${OS_TEST_PATH:-./stacktaskclient/tests/unit} $LISTOPT $IDOPTION
|
||||
test_id_option=--load-list $IDFILE
|
||||
test_list_option=--list
|
11
CONTRIBUTING.rst
Normal file
11
CONTRIBUTING.rst
Normal file
@ -0,0 +1,11 @@
|
||||
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
|
||||
|
175
LICENSE
Normal file
175
LICENSE
Normal file
@ -0,0 +1,175 @@
|
||||
|
||||
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.
|
10
MANIFEST.in
Normal file
10
MANIFEST.in
Normal file
@ -0,0 +1,10 @@
|
||||
include AUTHORS
|
||||
include babel.cfg
|
||||
include LICENSE
|
||||
include README.rst
|
||||
include ChangeLog
|
||||
include tox.ini
|
||||
include .testr.conf
|
||||
recursive-include doc *
|
||||
recursive-include tools *
|
||||
recursive-include python-stacktaskclient *.po *.pot
|
23
README.rst
Normal file
23
README.rst
Normal file
@ -0,0 +1,23 @@
|
||||
=================
|
||||
python-stacktaskclient
|
||||
=================
|
||||
|
||||
OpenStack Orchestration API Client Library
|
||||
|
||||
This is a client library for Stacktask built on the Catalyst Stacktask API. It
|
||||
provides a Python API (the ``stacktaskclient`` module) and a command-line tool
|
||||
(``stacktask``).
|
||||
|
||||
* Free software: Apache license
|
||||
* Documentation: http://docs.openstack.org/developer/python-heatclient
|
||||
* Source: http://git.openstack.org/cgit/openstack/python-heatclient
|
||||
* Bugs: http://bugs.launchpad.net/python-heatclient
|
||||
|
||||
Setup:
|
||||
python tools/install_venv.py
|
||||
source .tox/venv/bin/activate
|
||||
source openrc.sh
|
||||
stacktask user-tenant-list
|
||||
|
||||
'pip list' should give: python-stacktaskclient (0.6.1.dev63, /home/dale/dale/dev/openstack/python-heatclient)
|
||||
'which stacktask' should give: /home/dale/dale/dev/openstack/python-heatclient/.tox/venv/bin/stacktask
|
2
doc/.gitignore
vendored
Normal file
2
doc/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
build/
|
||||
source/ref/
|
90
doc/Makefile
Normal file
90
doc/Makefile
Normal file
@ -0,0 +1,90 @@
|
||||
# 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."
|
266
doc/source/conf.py
Normal file
266
doc/source/conf.py
Normal file
@ -0,0 +1,266 @@
|
||||
# -*- 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', 'oslosphinx']
|
||||
|
||||
# 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 = '_theme'
|
||||
|
||||
# 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 = '%b %d, %Y'
|
||||
git_cmd = "git log --pretty=format:'%ad, commit %h' --date=local -n1"
|
||||
html_last_updated_fmt = os.popen(git_cmd).read()
|
||||
|
||||
# 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'
|
59
doc/source/ext/gen_ref.py
Normal file
59
doc/source/ext/gen_ref.py
Normal file
@ -0,0 +1,59 @@
|
||||
# -*- 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"])
|
75
doc/source/index.rst
Normal file
75
doc/source/index.rst
Normal file
@ -0,0 +1,75 @@
|
||||
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)
|
||||
|
||||
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
|
98
doc/source/man/heat.rst
Normal file
98
doc/source/man/heat.rst
Normal file
@ -0,0 +1,98 @@
|
||||
====
|
||||
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/.
|
8
openstack-common.conf
Normal file
8
openstack-common.conf
Normal file
@ -0,0 +1,8 @@
|
||||
[DEFAULT]
|
||||
|
||||
# The list of modules to copy from openstack-common
|
||||
modules=apiclient
|
||||
module=cliutils
|
||||
|
||||
# The base module to hold the copy of openstack.common
|
||||
base=stacktaskclient
|
17
requirements.txt
Normal file
17
requirements.txt
Normal file
@ -0,0 +1,17 @@
|
||||
# The order of packages is significant, because pip processes them in the order
|
||||
# of appearance. Changing the order has an impact on the overall integration
|
||||
# process, which may cause wedges in the gate later.
|
||||
|
||||
Babel>=1.3
|
||||
pbr<2.0,>=1.4
|
||||
argparse
|
||||
iso8601>=0.1.9
|
||||
PrettyTable<0.8,>=0.7
|
||||
oslo.i18n>=1.5.0 # Apache-2.0
|
||||
oslo.serialization>=1.4.0 # Apache-2.0
|
||||
oslo.utils>=2.0.0 # Apache-2.0
|
||||
python-keystoneclient>=1.6.0
|
||||
python-swiftclient>=2.2.0
|
||||
PyYAML>=3.1.0
|
||||
requests>=2.5.2
|
||||
six>=1.9.0
|
121
run_tests.sh
Executable file
121
run_tests.sh
Executable file
@ -0,0 +1,121 @@
|
||||
#!/bin/bash
|
||||
|
||||
BASE_DIR=`dirname $0`
|
||||
|
||||
function usage {
|
||||
echo "Usage: $0 [OPTION]..."
|
||||
echo "Run stacktaskclient test suite(s)"
|
||||
echo ""
|
||||
echo " -V, --virtual-env Use virtualenv. Install automatically if not present."
|
||||
echo " (Default is to run tests in local environment)"
|
||||
echo " -F, --force Force a clean re-build of the virtual environment. Useful when dependencies have been added."
|
||||
echo " -f, --func Functional tests have been removed."
|
||||
echo " -u, --unit Run unit tests (default when nothing specified)"
|
||||
echo " -p, --pep8 Run pep8 tests"
|
||||
echo " --all Run pep8 and unit tests"
|
||||
echo " -c, --coverage Generate coverage report"
|
||||
echo " -d, --debug Run tests with testtools instead of testr. This allows you to use the debugger."
|
||||
echo " -h, --help Print this usage message"
|
||||
exit
|
||||
}
|
||||
|
||||
# must not assign -a as an option, needed for selecting custom attributes
|
||||
no_venv=1
|
||||
function process_option {
|
||||
case "$1" in
|
||||
-V|--virtual-env) no_venv=0;;
|
||||
-F|--force) force=1;;
|
||||
-f|--func) test_func=1;;
|
||||
-u|--unit) test_unit=1;;
|
||||
-p|--pep8) test_pep8=1;;
|
||||
--all) test_unit=1; test_pep8=1;;
|
||||
-c|--coverage) coverage=1;;
|
||||
-d|--debug) debug=1;;
|
||||
-h|--help) usage;;
|
||||
*) args="$args $1"; test_unit=1;;
|
||||
esac
|
||||
}
|
||||
|
||||
venv=.venv
|
||||
with_venv=tools/with_venv.sh
|
||||
wrapper=""
|
||||
debug=0
|
||||
|
||||
function run_tests {
|
||||
echo 'Running tests'
|
||||
|
||||
if [ $debug -eq 1 ]; then
|
||||
echo "Debugging..."
|
||||
if [ "$args" = "" ]; then
|
||||
# Default to running all tests if specific test is not
|
||||
# provided.
|
||||
testrargs="discover ./stacktaskclient/tests"
|
||||
fi
|
||||
${wrapper} python -m testtools.run $args $testrargs
|
||||
|
||||
# Short circuit because all of the testr and coverage stuff
|
||||
# below does not make sense when running testtools.run for
|
||||
# debugging purposes.
|
||||
return $?
|
||||
fi
|
||||
|
||||
# Just run the test suites in current environment
|
||||
if [ -n "$args" ] ; then
|
||||
args="-t $args"
|
||||
fi
|
||||
${wrapper} python setup.py testr --slowest $args
|
||||
}
|
||||
|
||||
function run_pep8 {
|
||||
echo "Running flake8..."
|
||||
bash -c "${wrapper} flake8"
|
||||
}
|
||||
|
||||
# run unit tests with pep8 when no arguments are specified
|
||||
# otherwise process CLI options
|
||||
if [[ $# == 0 ]]; then
|
||||
test_pep8=1
|
||||
test_unit=1
|
||||
else
|
||||
for arg in "$@"; do
|
||||
process_option $arg
|
||||
done
|
||||
fi
|
||||
|
||||
if [ "$no_venv" == 0 ]
|
||||
then
|
||||
# Remove the virtual environment if --force used
|
||||
if [ "$force" == 1 ]; then
|
||||
echo "Cleaning virtualenv..."
|
||||
rm -rf ${venv}
|
||||
fi
|
||||
if [ -e ${venv} ]; then
|
||||
wrapper="${with_venv}"
|
||||
else
|
||||
# Automatically install the virtualenv
|
||||
python tools/install_venv.py
|
||||
wrapper="${with_venv}"
|
||||
fi
|
||||
fi
|
||||
|
||||
result=0
|
||||
|
||||
# If functional or unit tests have been selected, run them
|
||||
if [ "$test_unit" == 1 ] || [ "$debug" == 1 ] ; then
|
||||
run_tests
|
||||
result=$?
|
||||
fi
|
||||
|
||||
# Run pep8 if it was selected
|
||||
if [ "$test_pep8" == 1 ]; then
|
||||
run_pep8
|
||||
fi
|
||||
|
||||
# Generate coverage report
|
||||
if [ "$coverage" == 1 ]; then
|
||||
echo "Generating coverage report in ./cover"
|
||||
${wrapper} python setup.py testr --coverage --slowest
|
||||
${wrapper} python -m coverage report --show-missing
|
||||
fi
|
||||
|
||||
exit $result
|
57
setup.cfg
Normal file
57
setup.cfg
Normal file
@ -0,0 +1,57 @@
|
||||
[metadata]
|
||||
name = python-stacktaskclient
|
||||
summary = OpenStack Orchestration API Client Library
|
||||
description-file =
|
||||
README.rst
|
||||
author = OpenStack
|
||||
author-email = openstack-dev@lists.openstack.org
|
||||
home-page = http://www.openstack.org/
|
||||
classifier =
|
||||
Environment :: OpenStack
|
||||
Intended Audience :: Information Technology
|
||||
Intended Audience :: System Administrators
|
||||
License :: OSI Approved :: Apache Software License
|
||||
Operating System :: POSIX :: Linux
|
||||
Programming Language :: Python
|
||||
Programming Language :: Python :: 2
|
||||
Programming Language :: Python :: 2.7
|
||||
Programming Language :: Python :: 2.6
|
||||
Programming Language :: Python :: 3
|
||||
Programming Language :: Python :: 3.4
|
||||
|
||||
[files]
|
||||
packages =
|
||||
stacktaskclient
|
||||
|
||||
[entry_points]
|
||||
console_scripts =
|
||||
stacktask = stacktaskclient.shell:main
|
||||
|
||||
[global]
|
||||
setup-hooks =
|
||||
pbr.hooks.setup_hook
|
||||
|
||||
[build_sphinx]
|
||||
source-dir = doc/source
|
||||
build-dir = doc/build
|
||||
all_files = 1
|
||||
|
||||
[upload_sphinx]
|
||||
upload-dir = doc/build/html
|
||||
|
||||
[wheel]
|
||||
universal = 1
|
||||
|
||||
[extract_messages]
|
||||
keywords = _ gettext ngettext l_ lazy_gettext
|
||||
mapping_file = babel.cfg
|
||||
output_file = python-stacktaskclient/locale/python-stacktaskclient.pot
|
||||
|
||||
[compile_catalog]
|
||||
directory = python-stacktaskclient/locale
|
||||
domain = python-stacktaskclient
|
||||
|
||||
[update_catalog]
|
||||
domain = python-stacktaskclient
|
||||
output_dir = python-stacktaskclient/locale
|
||||
input_file = python-stacktaskclient/locale/python-stacktaskclient.pot
|
29
setup.py
Normal file
29
setup.py
Normal file
@ -0,0 +1,29 @@
|
||||
# Copyright (c) 2013 Hewlett-Packard Development Company, L.P.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
# implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
# THIS FILE IS MANAGED BY THE GLOBAL REQUIREMENTS REPO - DO NOT EDIT
|
||||
import setuptools
|
||||
|
||||
# In python < 2.7.4, a lazy loading of package `pbr` will break
|
||||
# setuptools if some other modules registered functions in `atexit`.
|
||||
# solution from: http://bugs.python.org/issue15881#msg170215
|
||||
try:
|
||||
import multiprocessing # noqa
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
setuptools.setup(
|
||||
setup_requires=['pbr>=1.3'],
|
||||
pbr=True)
|
16
stacktaskclient/__init__.py
Normal file
16
stacktaskclient/__init__.py
Normal file
@ -0,0 +1,16 @@
|
||||
# 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-stacktaskclient').version_string()
|
19
stacktaskclient/client.py
Normal file
19
stacktaskclient/client.py
Normal file
@ -0,0 +1,19 @@
|
||||
# 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 stacktaskclient.common import utils
|
||||
|
||||
|
||||
def Client(version, *args, **kwargs):
|
||||
module = utils.import_versioned_module(version, 'client')
|
||||
client_class = getattr(module, 'Client')
|
||||
return client_class(*args, **kwargs)
|
0
stacktaskclient/common/__init__.py
Normal file
0
stacktaskclient/common/__init__.py
Normal file
147
stacktaskclient/common/deployment_utils.py
Normal file
147
stacktaskclient/common/deployment_utils.py
Normal file
@ -0,0 +1,147 @@
|
||||
# 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 six
|
||||
from six.moves.urllib import parse as urlparse
|
||||
from swiftclient import client as sc
|
||||
from swiftclient import utils as swiftclient_utils
|
||||
import uuid
|
||||
|
||||
from stacktaskclient import exc
|
||||
from stacktaskclient.openstack.common._i18n import _
|
||||
|
||||
|
||||
def build_derived_config_params(action, source, name, input_values,
|
||||
server_id, signal_transport, signal_id=None):
|
||||
|
||||
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 stacktask 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)
|
52
stacktaskclient/common/environment_format.py
Normal file
52
stacktaskclient/common/environment_format.py
Normal file
@ -0,0 +1,52 @@
|
||||
# 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 stacktaskclient.common import template_format
|
||||
from stacktaskclient.openstack.common._i18n import _
|
||||
|
||||
import yaml
|
||||
|
||||
|
||||
SECTIONS = (PARAMETER_DEFAULTS, PARAMETERS, RESOURCE_REGISTRY) = \
|
||||
('parameter_defaults', 'parameters', 'resource_registry')
|
||||
|
||||
|
||||
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 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:
|
||||
env[param] = {}
|
128
stacktaskclient/common/event_utils.py
Normal file
128
stacktaskclient/common/event_utils.py
Normal file
@ -0,0 +1,128 @@
|
||||
# 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.
|
||||
|
||||
from stacktaskclient.common import utils
|
||||
import stacktaskclient.exc as exc
|
||||
|
||||
from stacktaskclient.openstack.common._i18n import _
|
||||
|
||||
|
||||
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'
|
||||
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):
|
||||
events = _get_stack_events(hc, stack_id, event_args)
|
||||
if nested_depth > 0:
|
||||
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:
|
||||
marker_index = [e.id for e in events].index(marker)
|
||||
events = events[marker_index:]
|
||||
|
||||
# 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 stacktask version, but this will be required
|
||||
# until kilo stacktask 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_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:
|
||||
# Show which stack the event comes from (for nested events)
|
||||
for e in events:
|
||||
e.stack_name = stack_id.split("/")[0]
|
||||
return events
|
355
stacktaskclient/common/http.py
Normal file
355
stacktaskclient/common/http.py
Normal file
@ -0,0 +1,355 @@
|
||||
# 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 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 stacktaskclient.common import utils
|
||||
from stacktaskclient import exc
|
||||
from stacktaskclient.openstack.common._i18n import _
|
||||
from stacktaskclient.openstack.common._i18n import _LW
|
||||
from keystoneclient import adapter
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
USER_AGENT = 'python-stacktaskclient'
|
||||
CHUNKSIZE = 1024 * 64 # 64kB
|
||||
SENSITIVE_HEADERS = ('X-Auth-Token',)
|
||||
osprofiler_web = importutils.try_import("osprofiler.web")
|
||||
|
||||
|
||||
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.warn(_LW("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 not 'X-Auth-Key' 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
|
||||
|
||||
try:
|
||||
resp = requests.request(
|
||||
method,
|
||||
self.endpoint_url + url,
|
||||
allow_redirects=allow_redirects,
|
||||
**kwargs)
|
||||
except socket.gaierror as e:
|
||||
message = (_("Error finding address for %(url)s: %(e)s") %
|
||||
{'url': self.endpoint_url + url, 'e': e})
|
||||
raise exc.InvalidEndpoint(message=message)
|
||||
except (socket.error, socket.timeout) 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. Please try"
|
||||
" again with option %(option)s or "
|
||||
"export %(var)s\n%(content)s") %
|
||||
{
|
||||
'option': '--include-password',
|
||||
'var': 'HEAT_INCLUDE_PASSWORD=1',
|
||||
'content': 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')
|
||||
path = self.strip_endpoint(location)
|
||||
resp = self._http_request(path, method, **kwargs)
|
||||
elif resp.status_code == 300:
|
||||
raise exc.from_response(resp)
|
||||
|
||||
return resp
|
||||
|
||||
def strip_endpoint(self, location):
|
||||
if location is None:
|
||||
message = _("Location not returned with 302")
|
||||
raise exc.InvalidEndpoint(message=message)
|
||||
elif location.lower().startswith(self.endpoint.lower()):
|
||||
return location[len(self.endpoint):]
|
||||
else:
|
||||
message = _("Prohibited endpoint redirect %s") % location
|
||||
raise exc.InvalidEndpoint(message=message)
|
||||
|
||||
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)
|
||||
|
||||
try:
|
||||
kwargs.setdefault('json', kwargs.pop('data'))
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
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:
|
||||
kwargs['endpoint_override'] = endpoint
|
||||
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)
|
63
stacktaskclient/common/template_format.py
Normal file
63
stacktaskclient/common/template_format.py
Normal file
@ -0,0 +1,63 @@
|
||||
# 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 stacktaskclient.openstack.common._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.
|
||||
'''
|
||||
if tmpl_str.startswith('{'):
|
||||
tpl = json.loads(tmpl_str)
|
||||
else:
|
||||
try:
|
||||
tpl = yaml.load(tmpl_str, Loader=yaml_loader)
|
||||
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
|
224
stacktaskclient/common/template_utils.py
Normal file
224
stacktaskclient/common/template_utils.py
Normal file
@ -0,0 +1,224 @@
|
||||
# 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 parse
|
||||
from six.moves.urllib import request
|
||||
|
||||
from stacktaskclient.common import environment_format
|
||||
from stacktaskclient.common import template_format
|
||||
from stacktaskclient.common import utils
|
||||
from stacktaskclient import exc
|
||||
from stacktaskclient.openstack.common._i18n import _
|
||||
|
||||
|
||||
def get_template_contents(template_file=None, template_url=None,
|
||||
template_object=None, object_request=None,
|
||||
files=None, existing=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:
|
||||
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)
|
||||
return files, template
|
||||
|
||||
|
||||
def resolve_template_get_files(template, files, template_base_url):
|
||||
|
||||
def ignore_if(key, value):
|
||||
if key != 'get_file' and key != 'type':
|
||||
return True
|
||||
if not isinstance(value, six.string_types):
|
||||
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)
|
||||
|
||||
|
||||
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):
|
||||
|
||||
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)
|
||||
|
||||
if isinstance(from_data, dict):
|
||||
for key, value in iter(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:
|
||||
file_content = utils.read_url_content(str_url)
|
||||
if is_template(file_content):
|
||||
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.'''
|
||||
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):
|
||||
merged_files = {}
|
||||
merged_env = {}
|
||||
|
||||
if env_paths:
|
||||
for env_path in env_paths:
|
||||
files, env = process_environment_and_files(env_path, template,
|
||||
template_url)
|
||||
|
||||
# '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)
|
||||
|
||||
return merged_files, merged_env
|
||||
|
||||
|
||||
def process_environment_and_files(env_path=None, template=None,
|
||||
template_url=None):
|
||||
files = {}
|
||||
env = {}
|
||||
|
||||
if 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)
|
||||
|
||||
return files, env
|
||||
|
||||
|
||||
def resolve_environment_urls(resource_registry, files, env_base_url):
|
||||
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 == 'hooks':
|
||||
return True
|
||||
|
||||
get_file_contents(rr, files, base_url, ignore_if)
|
||||
|
||||
for res_name, res_dict in iter(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)
|
280
stacktaskclient/common/utils.py
Normal file
280
stacktaskclient/common/utils.py
Normal file
@ -0,0 +1,280 @@
|
||||
# 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 importutils
|
||||
import prettytable
|
||||
from six.moves.urllib import error
|
||||
from six.moves.urllib import parse
|
||||
from six.moves.urllib import request
|
||||
import yaml
|
||||
|
||||
from stacktaskclient import exc
|
||||
from stacktaskclient.openstack.common._i18n import _
|
||||
from stacktaskclient.openstack.common._i18n import _LE
|
||||
from stacktaskclient.openstack.common import cliutils
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
supported_formats = {
|
||||
"json": lambda x: jsonutils.dumps(x, indent=2),
|
||||
"yaml": yaml.safe_dump
|
||||
}
|
||||
|
||||
# Using common methods from oslo cliutils
|
||||
arg = cliutils.arg
|
||||
env = cliutils.env
|
||||
print_list = cliutils.print_list
|
||||
|
||||
|
||||
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 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'))
|
||||
|
||||
|
||||
def event_log_formatter(events):
|
||||
"""Return the events in log format."""
|
||||
event_log = []
|
||||
log_format = _("%(event_date)s %(event_time)s %(event_id)s "
|
||||
"[%(rsrc_name)s]: %(rsrc_status)s %(rsrc_status_reason)s")
|
||||
for event in events:
|
||||
event_time = getattr(event, 'event_time', '')
|
||||
time_date = event_time.split('T')
|
||||
try:
|
||||
event_time = time_date[0]
|
||||
event_date = time_date[1][:-1]
|
||||
except IndexError:
|
||||
event_time = event_date = ''
|
||||
|
||||
log = log_format % {
|
||||
'event_date': event_date, 'event_time': event_time,
|
||||
'event_id': getattr(event, 'id', ''),
|
||||
'rsrc_name': getattr(event, 'resource_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 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 import_versioned_module(version, submodule=None):
|
||||
module = 'stacktaskclient.v%s' % version
|
||||
if submodule:
|
||||
module = '.'.join((module, submodule))
|
||||
return importutils.import_module(module)
|
||||
|
||||
|
||||
def format_parameters(params, parse_semicolon=True):
|
||||
'''Reformat parameters into dict of format expected by the API.'''
|
||||
|
||||
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 iter(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(_LE('Could not decode response body as JSON'))
|
||||
else:
|
||||
body = None
|
||||
return body
|
196
stacktaskclient/exc.py
Normal file
196
stacktaskclient/exc.py
Normal file
@ -0,0 +1,196 @@
|
||||
# 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 stacktaskclient.openstack.common._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):
|
||||
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__}}
|
||||
|
||||
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 Stacktask API is not"
|
||||
"available.")
|
||||
return (_("%(name)s (HTTP %(code)s) %(details)s") %
|
||||
{
|
||||
'name': self.__class__.__name__,
|
||||
'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 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)
|
||||
|
||||
|
||||
class NoTokenLookupException(Exception):
|
||||
"""DEPRECATED."""
|
||||
pass
|
||||
|
||||
|
||||
class EndpointNotFound(Exception):
|
||||
"""DEPRECATED."""
|
||||
pass
|
||||
|
||||
|
||||
class StackFailure(Exception):
|
||||
pass
|
0
stacktaskclient/openstack/__init__.py
Normal file
0
stacktaskclient/openstack/__init__.py
Normal file
0
stacktaskclient/openstack/common/__init__.py
Normal file
0
stacktaskclient/openstack/common/__init__.py
Normal file
45
stacktaskclient/openstack/common/_i18n.py
Normal file
45
stacktaskclient/openstack/common/_i18n.py
Normal file
@ -0,0 +1,45 @@
|
||||
# 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
|
||||
|
||||
"""
|
||||
|
||||
try:
|
||||
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='stacktaskclient')
|
||||
|
||||
# The primary translation function using the well-known name "_"
|
||||
_ = _translators.primary
|
||||
|
||||
# Translators for log levels.
|
||||
#
|
||||
# The abbreviated names are meant to reflect the usual use of a short
|
||||
# name like '_'. The "L" is for "log" and the other letter comes from
|
||||
# the level.
|
||||
_LI = _translators.log_info
|
||||
_LW = _translators.log_warning
|
||||
_LE = _translators.log_error
|
||||
_LC = _translators.log_critical
|
||||
except ImportError:
|
||||
# NOTE(dims): Support for cases where a project wants to use
|
||||
# code from oslo-incubator, but is not ready to be internationalized
|
||||
# (like tempest)
|
||||
_ = _LI = _LW = _LE = _LC = lambda x: x
|
234
stacktaskclient/openstack/common/apiclient/auth.py
Normal file
234
stacktaskclient/openstack/common/apiclient/auth.py
Normal file
@ -0,0 +1,234 @@
|
||||
# Copyright 2013 OpenStack Foundation
|
||||
# Copyright 2013 Spanish National Research Council.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
# E0202: An attribute inherited from %s hide this method
|
||||
# pylint: disable=E0202
|
||||
|
||||
########################################################################
|
||||
#
|
||||
# THIS MODULE IS DEPRECATED
|
||||
#
|
||||
# Please refer to
|
||||
# https://etherpad.openstack.org/p/kilo-oslo-library-proposals for
|
||||
# the discussion leading to this deprecation.
|
||||
#
|
||||
# We recommend checking out the python-openstacksdk project
|
||||
# (https://launchpad.net/python-openstacksdk) instead.
|
||||
#
|
||||
########################################################################
|
||||
|
||||
import abc
|
||||
import argparse
|
||||
import os
|
||||
|
||||
import six
|
||||
from stevedore import extension
|
||||
|
||||
from stacktaskclient.openstack.common.apiclient import exceptions
|
||||
|
||||
|
||||
_discovered_plugins = {}
|
||||
|
||||
|
||||
def discover_auth_systems():
|
||||
"""Discover the available auth-systems.
|
||||
|
||||
This won't take into account the old style auth-systems.
|
||||
"""
|
||||
global _discovered_plugins
|
||||
_discovered_plugins = {}
|
||||
|
||||
def add_plugin(ext):
|
||||
_discovered_plugins[ext.name] = ext.plugin
|
||||
|
||||
ep_namespace = "stacktaskclient.openstack.common.apiclient.auth"
|
||||
mgr = extension.ExtensionManager(ep_namespace)
|
||||
mgr.map(add_plugin)
|
||||
|
||||
|
||||
def load_auth_system_opts(parser):
|
||||
"""Load options needed by the available auth-systems into a parser.
|
||||
|
||||
This function will try to populate the parser with options from the
|
||||
available plugins.
|
||||
"""
|
||||
group = parser.add_argument_group("Common auth options")
|
||||
BaseAuthPlugin.add_common_opts(group)
|
||||
for name, auth_plugin in six.iteritems(_discovered_plugins):
|
||||
group = parser.add_argument_group(
|
||||
"Auth-system '%s' options" % name,
|
||||
conflict_handler="resolve")
|
||||
auth_plugin.add_opts(group)
|
||||
|
||||
|
||||
def load_plugin(auth_system):
|
||||
try:
|
||||
plugin_class = _discovered_plugins[auth_system]
|
||||
except KeyError:
|
||||
raise exceptions.AuthSystemNotFound(auth_system)
|
||||
return plugin_class(auth_system=auth_system)
|
||||
|
||||
|
||||
def load_plugin_from_args(args):
|
||||
"""Load required plugin and populate it with options.
|
||||
|
||||
Try to guess auth system if it is not specified. Systems are tried in
|
||||
alphabetical order.
|
||||
|
||||
:type args: argparse.Namespace
|
||||
:raises: AuthPluginOptionsMissing
|
||||
"""
|
||||
auth_system = args.os_auth_system
|
||||
if auth_system:
|
||||
plugin = load_plugin(auth_system)
|
||||
plugin.parse_opts(args)
|
||||
plugin.sufficient_options()
|
||||
return plugin
|
||||
|
||||
for plugin_auth_system in sorted(six.iterkeys(_discovered_plugins)):
|
||||
plugin_class = _discovered_plugins[plugin_auth_system]
|
||||
plugin = plugin_class()
|
||||
plugin.parse_opts(args)
|
||||
try:
|
||||
plugin.sufficient_options()
|
||||
except exceptions.AuthPluginOptionsMissing:
|
||||
continue
|
||||
return plugin
|
||||
raise exceptions.AuthPluginOptionsMissing(["auth_system"])
|
||||
|
||||
|
||||
@six.add_metaclass(abc.ABCMeta)
|
||||
class BaseAuthPlugin(object):
|
||||
"""Base class for authentication plugins.
|
||||
|
||||
An authentication plugin needs to override at least the authenticate
|
||||
method to be a valid plugin.
|
||||
"""
|
||||
|
||||
auth_system = None
|
||||
opt_names = []
|
||||
common_opt_names = [
|
||||
"auth_system",
|
||||
"username",
|
||||
"password",
|
||||
"tenant_name",
|
||||
"token",
|
||||
"auth_url",
|
||||
]
|
||||
|
||||
def __init__(self, auth_system=None, **kwargs):
|
||||
self.auth_system = auth_system or self.auth_system
|
||||
self.opts = dict((name, kwargs.get(name))
|
||||
for name in self.opt_names)
|
||||
|
||||
@staticmethod
|
||||
def _parser_add_opt(parser, opt):
|
||||
"""Add an option to parser in two variants.
|
||||
|
||||
:param opt: option name (with underscores)
|
||||
"""
|
||||
dashed_opt = opt.replace("_", "-")
|
||||
env_var = "OS_%s" % opt.upper()
|
||||
arg_default = os.environ.get(env_var, "")
|
||||
arg_help = "Defaults to env[%s]." % env_var
|
||||
parser.add_argument(
|
||||
"--os-%s" % dashed_opt,
|
||||
metavar="<%s>" % dashed_opt,
|
||||
default=arg_default,
|
||||
help=arg_help)
|
||||
parser.add_argument(
|
||||
"--os_%s" % opt,
|
||||
metavar="<%s>" % dashed_opt,
|
||||
help=argparse.SUPPRESS)
|
||||
|
||||
@classmethod
|
||||
def add_opts(cls, parser):
|
||||
"""Populate the parser with the options for this plugin.
|
||||
"""
|
||||
for opt in cls.opt_names:
|
||||
# use `BaseAuthPlugin.common_opt_names` since it is never
|
||||
# changed in child classes
|
||||
if opt not in BaseAuthPlugin.common_opt_names:
|
||||
cls._parser_add_opt(parser, opt)
|
||||
|
||||
@classmethod
|
||||
def add_common_opts(cls, parser):
|
||||
"""Add options that are common for several plugins.
|
||||
"""
|
||||
for opt in cls.common_opt_names:
|
||||
cls._parser_add_opt(parser, opt)
|
||||
|
||||
@staticmethod
|
||||
def get_opt(opt_name, args):
|
||||
"""Return option name and value.
|
||||
|
||||
:param opt_name: name of the option, e.g., "username"
|
||||
:param args: parsed arguments
|
||||
"""
|
||||
return (opt_name, getattr(args, "os_%s" % opt_name, None))
|
||||
|
||||
def parse_opts(self, args):
|
||||
"""Parse the actual auth-system options if any.
|
||||
|
||||
This method is expected to populate the attribute `self.opts` with a
|
||||
dict containing the options and values needed to make authentication.
|
||||
"""
|
||||
self.opts.update(dict(self.get_opt(opt_name, args)
|
||||
for opt_name in self.opt_names))
|
||||
|
||||
def authenticate(self, http_client):
|
||||
"""Authenticate using plugin defined method.
|
||||
|
||||
The method usually analyses `self.opts` and performs
|
||||
a request to authentication server.
|
||||
|
||||
:param http_client: client object that needs authentication
|
||||
:type http_client: HTTPClient
|
||||
:raises: AuthorizationFailure
|
||||
"""
|
||||
self.sufficient_options()
|
||||
self._do_authenticate(http_client)
|
||||
|
||||
@abc.abstractmethod
|
||||
def _do_authenticate(self, http_client):
|
||||
"""Protected method for authentication.
|
||||
"""
|
||||
|
||||
def sufficient_options(self):
|
||||
"""Check if all required options are present.
|
||||
|
||||
:raises: AuthPluginOptionsMissing
|
||||
"""
|
||||
missing = [opt
|
||||
for opt in self.opt_names
|
||||
if not self.opts.get(opt)]
|
||||
if missing:
|
||||
raise exceptions.AuthPluginOptionsMissing(missing)
|
||||
|
||||
@abc.abstractmethod
|
||||
def token_and_endpoint(self, endpoint_type, service_type):
|
||||
"""Return token and endpoint.
|
||||
|
||||
:param service_type: Service type of the endpoint
|
||||
:type service_type: string
|
||||
:param endpoint_type: Type of endpoint.
|
||||
Possible values: public or publicURL,
|
||||
internal or internalURL,
|
||||
admin or adminURL
|
||||
:type endpoint_type: string
|
||||
:returns: tuple of token and endpoint strings
|
||||
:raises: EndpointException
|
||||
"""
|
532
stacktaskclient/openstack/common/apiclient/base.py
Normal file
532
stacktaskclient/openstack/common/apiclient/base.py
Normal file
@ -0,0 +1,532 @@
|
||||
# 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.
|
||||
"""
|
||||
|
||||
########################################################################
|
||||
#
|
||||
# THIS MODULE IS DEPRECATED
|
||||
#
|
||||
# Please refer to
|
||||
# https://etherpad.openstack.org/p/kilo-oslo-library-proposals for
|
||||
# the discussion leading to this deprecation.
|
||||
#
|
||||
# We recommend checking out the python-openstacksdk project
|
||||
# (https://launchpad.net/python-openstacksdk) instead.
|
||||
#
|
||||
########################################################################
|
||||
|
||||
|
||||
# E1102: %s is not callable
|
||||
# pylint: disable=E1102
|
||||
|
||||
import abc
|
||||
import copy
|
||||
|
||||
from oslo_utils import strutils
|
||||
import six
|
||||
from six.moves.urllib import parse
|
||||
|
||||
from stacktaskclient.openstack.common._i18n import _
|
||||
from stacktaskclient.openstack.common.apiclient import 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 six.iteritems(kwargs.copy()):
|
||||
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)
|
||||
return "<%s %s>" % (self.__class__.__name__, info)
|
||||
|
||||
@property
|
||||
def human_id(self):
|
||||
"""Human-readable ID which can be used for bash completion.
|
||||
"""
|
||||
if self.HUMAN_ID:
|
||||
name = getattr(self, self.NAME_ATTR, None)
|
||||
if name is not None:
|
||||
return strutils.to_slug(name)
|
||||
return None
|
||||
|
||||
def _add_details(self, info):
|
||||
for (k, v) in six.iteritems(info):
|
||||
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
|
||||
if hasattr(self, 'id') and hasattr(other, 'id'):
|
||||
return self.id == other.id
|
||||
return self._info == other._info
|
||||
|
||||
def is_loaded(self):
|
||||
return self._loaded
|
||||
|
||||
def set_loaded(self, val):
|
||||
self._loaded = val
|
||||
|
||||
def to_dict(self):
|
||||
return copy.deepcopy(self._info)
|
388
stacktaskclient/openstack/common/apiclient/client.py
Normal file
388
stacktaskclient/openstack/common/apiclient/client.py
Normal file
@ -0,0 +1,388 @@
|
||||
# Copyright 2010 Jacob Kaplan-Moss
|
||||
# Copyright 2011 OpenStack Foundation
|
||||
# Copyright 2011 Piston Cloud Computing, Inc.
|
||||
# Copyright 2013 Alessio Ababilov
|
||||
# Copyright 2013 Grid Dynamics
|
||||
# Copyright 2013 OpenStack Foundation
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
"""
|
||||
OpenStack Client interface. Handles the REST calls and responses.
|
||||
"""
|
||||
|
||||
# E0202: An attribute inherited from %s hide this method
|
||||
# pylint: disable=E0202
|
||||
|
||||
import hashlib
|
||||
import logging
|
||||
import time
|
||||
|
||||
try:
|
||||
import simplejson as json
|
||||
except ImportError:
|
||||
import json
|
||||
|
||||
from oslo_utils import encodeutils
|
||||
from oslo_utils import importutils
|
||||
import requests
|
||||
|
||||
from stacktaskclient.openstack.common._i18n import _
|
||||
from stacktaskclient.openstack.common.apiclient import exceptions
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
SENSITIVE_HEADERS = ('X-Auth-Token', 'X-Subject-Token',)
|
||||
|
||||
|
||||
class HTTPClient(object):
|
||||
"""This client handles sending HTTP requests to OpenStack servers.
|
||||
|
||||
Features:
|
||||
|
||||
- share authentication information between several clients to different
|
||||
services (e.g., for compute and image clients);
|
||||
- reissue authentication request for expired tokens;
|
||||
- encode/decode JSON bodies;
|
||||
- raise exceptions on HTTP errors;
|
||||
- pluggable authentication;
|
||||
- store authentication information in a keyring;
|
||||
- store time spent for requests;
|
||||
- register clients for particular services, so one can use
|
||||
`http_client.identity` or `http_client.compute`;
|
||||
- log requests and responses in a format that is easy to copy-and-paste
|
||||
into terminal and send the same request with curl.
|
||||
"""
|
||||
|
||||
user_agent = "stacktaskclient.openstack.common.apiclient"
|
||||
|
||||
def __init__(self,
|
||||
auth_plugin,
|
||||
region_name=None,
|
||||
endpoint_type="publicURL",
|
||||
original_ip=None,
|
||||
verify=True,
|
||||
cert=None,
|
||||
timeout=None,
|
||||
timings=False,
|
||||
keyring_saver=None,
|
||||
debug=False,
|
||||
user_agent=None,
|
||||
http=None):
|
||||
self.auth_plugin = auth_plugin
|
||||
|
||||
self.endpoint_type = endpoint_type
|
||||
self.region_name = region_name
|
||||
|
||||
self.original_ip = original_ip
|
||||
self.timeout = timeout
|
||||
self.verify = verify
|
||||
self.cert = cert
|
||||
|
||||
self.keyring_saver = keyring_saver
|
||||
self.debug = debug
|
||||
self.user_agent = user_agent or self.user_agent
|
||||
|
||||
self.times = [] # [("item", starttime, endtime), ...]
|
||||
self.timings = timings
|
||||
|
||||
# requests within the same session can reuse TCP connections from pool
|
||||
self.http = http or requests.Session()
|
||||
|
||||
self.cached_token = None
|
||||
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 _http_log_req(self, method, url, kwargs):
|
||||
if not self.debug:
|
||||
return
|
||||
|
||||
string_parts = [
|
||||
"curl -g -i",
|
||||
"-X '%s'" % method,
|
||||
"'%s'" % url,
|
||||
]
|
||||
|
||||
for element in kwargs['headers']:
|
||||
header = ("-H '%s: %s'" %
|
||||
self._safe_header(element, kwargs['headers'][element]))
|
||||
string_parts.append(header)
|
||||
|
||||
_logger.debug("REQ: %s" % " ".join(string_parts))
|
||||
if 'data' in kwargs:
|
||||
_logger.debug("REQ BODY: %s\n" % (kwargs['data']))
|
||||
|
||||
def _http_log_resp(self, resp):
|
||||
if not self.debug:
|
||||
return
|
||||
_logger.debug(
|
||||
"RESP: [%s] %s\n",
|
||||
resp.status_code,
|
||||
resp.headers)
|
||||
if resp._content_consumed:
|
||||
_logger.debug(
|
||||
"RESP BODY: %s\n",
|
||||
resp.text)
|
||||
|
||||
def serialize(self, kwargs):
|
||||
if kwargs.get('json') is not None:
|
||||
kwargs['headers']['Content-Type'] = 'application/json'
|
||||
kwargs['data'] = json.dumps(kwargs['json'])
|
||||
try:
|
||||
del kwargs['json']
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
def get_timings(self):
|
||||
return self.times
|
||||
|
||||
def reset_timings(self):
|
||||
self.times = []
|
||||
|
||||
def request(self, method, url, **kwargs):
|
||||
"""Send an http request with the specified characteristics.
|
||||
|
||||
Wrapper around `requests.Session.request` to handle tasks such as
|
||||
setting headers, JSON encoding/decoding, and error handling.
|
||||
|
||||
:param method: method of HTTP request
|
||||
:param url: URL of HTTP request
|
||||
:param kwargs: any other parameter that can be passed to
|
||||
requests.Session.request (such as `headers`) or `json`
|
||||
that will be encoded as JSON and used as `data` argument
|
||||
"""
|
||||
kwargs.setdefault("headers", {})
|
||||
kwargs["headers"]["User-Agent"] = self.user_agent
|
||||
if self.original_ip:
|
||||
kwargs["headers"]["Forwarded"] = "for=%s;by=%s" % (
|
||||
self.original_ip, self.user_agent)
|
||||
if self.timeout is not None:
|
||||
kwargs.setdefault("timeout", self.timeout)
|
||||
kwargs.setdefault("verify", self.verify)
|
||||
if self.cert is not None:
|
||||
kwargs.setdefault("cert", self.cert)
|
||||
self.serialize(kwargs)
|
||||
|
||||
self._http_log_req(method, url, kwargs)
|
||||
if self.timings:
|
||||
start_time = time.time()
|
||||
resp = self.http.request(method, url, **kwargs)
|
||||
if self.timings:
|
||||
self.times.append(("%s %s" % (method, url),
|
||||
start_time, time.time()))
|
||||
self._http_log_resp(resp)
|
||||
|
||||
self.last_request_id = resp.headers.get('x-openstack-request-id')
|
||||
|
||||
if resp.status_code >= 400:
|
||||
_logger.debug(
|
||||
"Request returned failure status: %s",
|
||||
resp.status_code)
|
||||
raise exceptions.from_response(resp, method, url)
|
||||
|
||||
return resp
|
||||
|
||||
@staticmethod
|
||||
def concat_url(endpoint, url):
|
||||
"""Concatenate endpoint and final URL.
|
||||
|
||||
E.g., "http://keystone/v2.0/" and "/tokens" are concatenated to
|
||||
"http://keystone/v2.0/tokens".
|
||||
|
||||
:param endpoint: the base URL
|
||||
:param url: the final URL
|
||||
"""
|
||||
return "%s/%s" % (endpoint.rstrip("/"), url.strip("/"))
|
||||
|
||||
def client_request(self, client, method, url, **kwargs):
|
||||
"""Send an http request using `client`'s endpoint and specified `url`.
|
||||
|
||||
If request was rejected as unauthorized (possibly because the token is
|
||||
expired), issue one authorization attempt and send the request once
|
||||
again.
|
||||
|
||||
:param client: instance of BaseClient descendant
|
||||
:param method: method of HTTP request
|
||||
:param url: URL of HTTP request
|
||||
:param kwargs: any other parameter that can be passed to
|
||||
`HTTPClient.request`
|
||||
"""
|
||||
|
||||
filter_args = {
|
||||
"endpoint_type": client.endpoint_type or self.endpoint_type,
|
||||
"service_type": client.service_type,
|
||||
}
|
||||
token, endpoint = (self.cached_token, client.cached_endpoint)
|
||||
just_authenticated = False
|
||||
if not (token and endpoint):
|
||||
try:
|
||||
token, endpoint = self.auth_plugin.token_and_endpoint(
|
||||
**filter_args)
|
||||
except exceptions.EndpointException:
|
||||
pass
|
||||
if not (token and endpoint):
|
||||
self.authenticate()
|
||||
just_authenticated = True
|
||||
token, endpoint = self.auth_plugin.token_and_endpoint(
|
||||
**filter_args)
|
||||
if not (token and endpoint):
|
||||
raise exceptions.AuthorizationFailure(
|
||||
_("Cannot find endpoint or token for request"))
|
||||
|
||||
old_token_endpoint = (token, endpoint)
|
||||
kwargs.setdefault("headers", {})["X-Auth-Token"] = token
|
||||
self.cached_token = token
|
||||
client.cached_endpoint = endpoint
|
||||
# Perform the request once. If we get Unauthorized, then it
|
||||
# might be because the auth token expired, so try to
|
||||
# re-authenticate and try again. If it still fails, bail.
|
||||
try:
|
||||
return self.request(
|
||||
method, self.concat_url(endpoint, url), **kwargs)
|
||||
except exceptions.Unauthorized as unauth_ex:
|
||||
if just_authenticated:
|
||||
raise
|
||||
self.cached_token = None
|
||||
client.cached_endpoint = None
|
||||
if self.auth_plugin.opts.get('token'):
|
||||
self.auth_plugin.opts['token'] = None
|
||||
if self.auth_plugin.opts.get('endpoint'):
|
||||
self.auth_plugin.opts['endpoint'] = None
|
||||
self.authenticate()
|
||||
try:
|
||||
token, endpoint = self.auth_plugin.token_and_endpoint(
|
||||
**filter_args)
|
||||
except exceptions.EndpointException:
|
||||
raise unauth_ex
|
||||
if (not (token and endpoint) or
|
||||
old_token_endpoint == (token, endpoint)):
|
||||
raise unauth_ex
|
||||
self.cached_token = token
|
||||
client.cached_endpoint = endpoint
|
||||
kwargs["headers"]["X-Auth-Token"] = token
|
||||
return self.request(
|
||||
method, self.concat_url(endpoint, url), **kwargs)
|
||||
|
||||
def add_client(self, base_client_instance):
|
||||
"""Add a new instance of :class:`BaseClient` descendant.
|
||||
|
||||
`self` will store a reference to `base_client_instance`.
|
||||
|
||||
Example:
|
||||
|
||||
>>> def test_clients():
|
||||
... from keystoneclient.auth import keystone
|
||||
... from openstack.common.apiclient import client
|
||||
... auth = keystone.KeystoneAuthPlugin(
|
||||
... username="user", password="pass", tenant_name="tenant",
|
||||
... auth_url="http://auth:5000/v2.0")
|
||||
... openstack_client = client.HTTPClient(auth)
|
||||
... # create nova client
|
||||
... from novaclient.v1_1 import client
|
||||
... client.Client(openstack_client)
|
||||
... # create keystone client
|
||||
... from keystoneclient.v2_0 import client
|
||||
... client.Client(openstack_client)
|
||||
... # use them
|
||||
... openstack_client.identity.tenants.list()
|
||||
... openstack_client.compute.servers.list()
|
||||
"""
|
||||
service_type = base_client_instance.service_type
|
||||
if service_type and not hasattr(self, service_type):
|
||||
setattr(self, service_type, base_client_instance)
|
||||
|
||||
def authenticate(self):
|
||||
self.auth_plugin.authenticate(self)
|
||||
# Store the authentication results in the keyring for later requests
|
||||
if self.keyring_saver:
|
||||
self.keyring_saver.save(self)
|
||||
|
||||
|
||||
class BaseClient(object):
|
||||
"""Top-level object to access the OpenStack API.
|
||||
|
||||
This client uses :class:`HTTPClient` to send requests. :class:`HTTPClient`
|
||||
will handle a bunch of issues such as authentication.
|
||||
"""
|
||||
|
||||
service_type = None
|
||||
endpoint_type = None # "publicURL" will be used
|
||||
cached_endpoint = None
|
||||
|
||||
def __init__(self, http_client, extensions=None):
|
||||
self.http_client = http_client
|
||||
http_client.add_client(self)
|
||||
|
||||
# Add in any extensions...
|
||||
if extensions:
|
||||
for extension in extensions:
|
||||
if extension.manager_class:
|
||||
setattr(self, extension.name,
|
||||
extension.manager_class(self))
|
||||
|
||||
def client_request(self, method, url, **kwargs):
|
||||
return self.http_client.client_request(
|
||||
self, method, url, **kwargs)
|
||||
|
||||
@property
|
||||
def last_request_id(self):
|
||||
return self.http_client.last_request_id
|
||||
|
||||
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.client_request("DELETE", url, **kwargs)
|
||||
|
||||
def patch(self, url, **kwargs):
|
||||
return self.client_request("PATCH", url, **kwargs)
|
||||
|
||||
@staticmethod
|
||||
def get_class(api_name, version, version_map):
|
||||
"""Returns the client class for the requested API version
|
||||
|
||||
:param api_name: the name of the API, e.g. 'compute', 'image', etc
|
||||
:param version: the requested API version
|
||||
:param version_map: a dict of client classes keyed by version
|
||||
:rtype: a client class for the requested API version
|
||||
"""
|
||||
try:
|
||||
client_path = version_map[str(version)]
|
||||
except (KeyError, ValueError):
|
||||
msg = _("Invalid %(api_name)s client version '%(version)s'. "
|
||||
"Must be one of: %(version_map)s") % {
|
||||
'api_name': api_name,
|
||||
'version': version,
|
||||
'version_map': ', '.join(version_map.keys())}
|
||||
raise exceptions.UnsupportedVersion(msg)
|
||||
|
||||
return importutils.import_class(client_path)
|
479
stacktaskclient/openstack/common/apiclient/exceptions.py
Normal file
479
stacktaskclient/openstack/common/apiclient/exceptions.py
Normal file
@ -0,0 +1,479 @@
|
||||
# Copyright 2010 Jacob Kaplan-Moss
|
||||
# Copyright 2011 Nebula, Inc.
|
||||
# Copyright 2013 Alessio Ababilov
|
||||
# Copyright 2013 OpenStack Foundation
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
"""
|
||||
Exception definitions.
|
||||
"""
|
||||
|
||||
########################################################################
|
||||
#
|
||||
# THIS MODULE IS DEPRECATED
|
||||
#
|
||||
# Please refer to
|
||||
# https://etherpad.openstack.org/p/kilo-oslo-library-proposals for
|
||||
# the discussion leading to this deprecation.
|
||||
#
|
||||
# We recommend checking out the python-openstacksdk project
|
||||
# (https://launchpad.net/python-openstacksdk) instead.
|
||||
#
|
||||
########################################################################
|
||||
|
||||
import inspect
|
||||
import sys
|
||||
|
||||
import six
|
||||
|
||||
from stacktaskclient.openstack.common._i18n import _
|
||||
|
||||
|
||||
class ClientException(Exception):
|
||||
"""The base exception class for all exceptions this library raises.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class ValidationError(ClientException):
|
||||
"""Error in validation on API client side."""
|
||||
pass
|
||||
|
||||
|
||||
class UnsupportedVersion(ClientException):
|
||||
"""User is trying to use an unsupported version of the API."""
|
||||
pass
|
||||
|
||||
|
||||
class CommandError(ClientException):
|
||||
"""Error in CLI tool."""
|
||||
pass
|
||||
|
||||
|
||||
class AuthorizationFailure(ClientException):
|
||||
"""Cannot authorize API client."""
|
||||
pass
|
||||
|
||||
|
||||
class ConnectionError(ClientException):
|
||||
"""Cannot connect to API service."""
|
||||
pass
|
||||
|
||||
|
||||
class ConnectionRefused(ConnectionError):
|
||||
"""Connection refused while trying to connect to API service."""
|
||||
pass
|
||||
|
||||
|
||||
class AuthPluginOptionsMissing(AuthorizationFailure):
|
||||
"""Auth plugin misses some options."""
|
||||
def __init__(self, opt_names):
|
||||
super(AuthPluginOptionsMissing, self).__init__(
|
||||
_("Authentication failed. Missing options: %s") %
|
||||
", ".join(opt_names))
|
||||
self.opt_names = opt_names
|
||||
|
||||
|
||||
class AuthSystemNotFound(AuthorizationFailure):
|
||||
"""User has specified an AuthSystem that is not installed."""
|
||||
def __init__(self, auth_system):
|
||||
super(AuthSystemNotFound, self).__init__(
|
||||
_("AuthSystemNotFound: %r") % auth_system)
|
||||
self.auth_system = auth_system
|
||||
|
||||
|
||||
class NoUniqueMatch(ClientException):
|
||||
"""Multiple entities found instead of one."""
|
||||
pass
|
||||
|
||||
|
||||
class EndpointException(ClientException):
|
||||
"""Something is rotten in Service Catalog."""
|
||||
pass
|
||||
|
||||
|
||||
class EndpointNotFound(EndpointException):
|
||||
"""Could not find requested endpoint in Service Catalog."""
|
||||
pass
|
||||
|
||||
|
||||
class AmbiguousEndpoints(EndpointException):
|
||||
"""Found more than one matching endpoint in Service Catalog."""
|
||||
def __init__(self, endpoints=None):
|
||||
super(AmbiguousEndpoints, self).__init__(
|
||||
_("AmbiguousEndpoints: %r") % endpoints)
|
||||
self.endpoints = endpoints
|
||||
|
||||
|
||||
class HttpError(ClientException):
|
||||
"""The base exception class for all HTTP exceptions.
|
||||
"""
|
||||
http_status = 0
|
||||
message = _("HTTP Error")
|
||||
|
||||
def __init__(self, message=None, details=None,
|
||||
response=None, request_id=None,
|
||||
url=None, method=None, http_status=None):
|
||||
self.http_status = http_status or self.http_status
|
||||
self.message = message or self.message
|
||||
self.details = details
|
||||
self.request_id = request_id
|
||||
self.response = response
|
||||
self.url = url
|
||||
self.method = method
|
||||
formatted_string = "%s (HTTP %s)" % (self.message, self.http_status)
|
||||
if request_id:
|
||||
formatted_string += " (Request-ID: %s)" % request_id
|
||||
super(HttpError, self).__init__(formatted_string)
|
||||
|
||||
|
||||
class HTTPRedirection(HttpError):
|
||||
"""HTTP Redirection."""
|
||||
message = _("HTTP Redirection")
|
||||
|
||||
|
||||
class HTTPClientError(HttpError):
|
||||
"""Client-side HTTP error.
|
||||
|
||||
Exception for cases in which the client seems to have erred.
|
||||
"""
|
||||
message = _("HTTP Client Error")
|
||||
|
||||
|
||||
class HttpServerError(HttpError):
|
||||
"""Server-side HTTP error.
|
||||
|
||||
Exception for cases in which the server is aware that it has
|
||||
erred or is incapable of performing the request.
|
||||
"""
|
||||
message = _("HTTP Server Error")
|
||||
|
||||
|
||||
class MultipleChoices(HTTPRedirection):
|
||||
"""HTTP 300 - Multiple Choices.
|
||||
|
||||
Indicates multiple options for the resource that the client may follow.
|
||||
"""
|
||||
|
||||
http_status = 300
|
||||
message = _("Multiple Choices")
|
||||
|
||||
|
||||
class BadRequest(HTTPClientError):
|
||||
"""HTTP 400 - Bad Request.
|
||||
|
||||
The request cannot be fulfilled due to bad syntax.
|
||||
"""
|
||||
http_status = 400
|
||||
message = _("Bad Request")
|
||||
|
||||
|
||||
class Unauthorized(HTTPClientError):
|
||||
"""HTTP 401 - Unauthorized.
|
||||
|
||||
Similar to 403 Forbidden, but specifically for use when authentication
|
||||
is required and has failed or has not yet been provided.
|
||||
"""
|
||||
http_status = 401
|
||||
message = _("Unauthorized")
|
||||
|
||||
|
||||
class PaymentRequired(HTTPClientError):
|
||||
"""HTTP 402 - Payment Required.
|
||||
|
||||
Reserved for future use.
|
||||
"""
|
||||
http_status = 402
|
||||
message = _("Payment Required")
|
||||
|
||||
|
||||
class Forbidden(HTTPClientError):
|
||||
"""HTTP 403 - Forbidden.
|
||||
|
||||
The request was a valid request, but the server is refusing to respond
|
||||
to it.
|
||||
"""
|
||||
http_status = 403
|
||||
message = _("Forbidden")
|
||||
|
||||
|
||||
class NotFound(HTTPClientError):
|
||||
"""HTTP 404 - Not Found.
|
||||
|
||||
The requested resource could not be found but may be available again
|
||||
in the future.
|
||||
"""
|
||||
http_status = 404
|
||||
message = _("Not Found")
|
||||
|
||||
|
||||
class MethodNotAllowed(HTTPClientError):
|
||||
"""HTTP 405 - Method Not Allowed.
|
||||
|
||||
A request was made of a resource using a request method not supported
|
||||
by that resource.
|
||||
"""
|
||||
http_status = 405
|
||||
message = _("Method Not Allowed")
|
||||
|
||||
|
||||
class NotAcceptable(HTTPClientError):
|
||||
"""HTTP 406 - Not Acceptable.
|
||||
|
||||
The requested resource is only capable of generating content not
|
||||
acceptable according to the Accept headers sent in the request.
|
||||
"""
|
||||
http_status = 406
|
||||
message = _("Not Acceptable")
|
||||
|
||||
|
||||
class ProxyAuthenticationRequired(HTTPClientError):
|
||||
"""HTTP 407 - Proxy Authentication Required.
|
||||
|
||||
The client must first authenticate itself with the proxy.
|
||||
"""
|
||||
http_status = 407
|
||||
message = _("Proxy Authentication Required")
|
||||
|
||||
|
||||
class RequestTimeout(HTTPClientError):
|
||||
"""HTTP 408 - Request Timeout.
|
||||
|
||||
The server timed out waiting for the request.
|
||||
"""
|
||||
http_status = 408
|
||||
message = _("Request Timeout")
|
||||
|
||||
|
||||
class Conflict(HTTPClientError):
|
||||
"""HTTP 409 - Conflict.
|
||||
|
||||
Indicates that the request could not be processed because of conflict
|
||||
in the request, such as an edit conflict.
|
||||
"""
|
||||
http_status = 409
|
||||
message = _("Conflict")
|
||||
|
||||
|
||||
class Gone(HTTPClientError):
|
||||
"""HTTP 410 - Gone.
|
||||
|
||||
Indicates that the resource requested is no longer available and will
|
||||
not be available again.
|
||||
"""
|
||||
http_status = 410
|
||||
message = _("Gone")
|
||||
|
||||
|
||||
class LengthRequired(HTTPClientError):
|
||||
"""HTTP 411 - Length Required.
|
||||
|
||||
The request did not specify the length of its content, which is
|
||||
required by the requested resource.
|
||||
"""
|
||||
http_status = 411
|
||||
message = _("Length Required")
|
||||
|
||||
|
||||
class PreconditionFailed(HTTPClientError):
|
||||
"""HTTP 412 - Precondition Failed.
|
||||
|
||||
The server does not meet one of the preconditions that the requester
|
||||
put on the request.
|
||||
"""
|
||||
http_status = 412
|
||||
message = _("Precondition Failed")
|
||||
|
||||
|
||||
class RequestEntityTooLarge(HTTPClientError):
|
||||
"""HTTP 413 - Request Entity Too Large.
|
||||
|
||||
The request is larger than the server is willing or able to process.
|
||||
"""
|
||||
http_status = 413
|
||||
message = _("Request Entity Too Large")
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
try:
|
||||
self.retry_after = int(kwargs.pop('retry_after'))
|
||||
except (KeyError, ValueError):
|
||||
self.retry_after = 0
|
||||
|
||||
super(RequestEntityTooLarge, self).__init__(*args, **kwargs)
|
||||
|
||||
|
||||
class RequestUriTooLong(HTTPClientError):
|
||||
"""HTTP 414 - Request-URI Too Long.
|
||||
|
||||
The URI provided was too long for the server to process.
|
||||
"""
|
||||
http_status = 414
|
||||
message = _("Request-URI Too Long")
|
||||
|
||||
|
||||
class UnsupportedMediaType(HTTPClientError):
|
||||
"""HTTP 415 - Unsupported Media Type.
|
||||
|
||||
The request entity has a media type which the server or resource does
|
||||
not support.
|
||||
"""
|
||||
http_status = 415
|
||||
message = _("Unsupported Media Type")
|
||||
|
||||
|
||||
class RequestedRangeNotSatisfiable(HTTPClientError):
|
||||
"""HTTP 416 - Requested Range Not Satisfiable.
|
||||
|
||||
The client has asked for a portion of the file, but the server cannot
|
||||
supply that portion.
|
||||
"""
|
||||
http_status = 416
|
||||
message = _("Requested Range Not Satisfiable")
|
||||
|
||||
|
||||
class ExpectationFailed(HTTPClientError):
|
||||
"""HTTP 417 - Expectation Failed.
|
||||
|
||||
The server cannot meet the requirements of the Expect request-header field.
|
||||
"""
|
||||
http_status = 417
|
||||
message = _("Expectation Failed")
|
||||
|
||||
|
||||
class UnprocessableEntity(HTTPClientError):
|
||||
"""HTTP 422 - Unprocessable Entity.
|
||||
|
||||
The request was well-formed but was unable to be followed due to semantic
|
||||
errors.
|
||||
"""
|
||||
http_status = 422
|
||||
message = _("Unprocessable Entity")
|
||||
|
||||
|
||||
class InternalServerError(HttpServerError):
|
||||
"""HTTP 500 - Internal Server Error.
|
||||
|
||||
A generic error message, given when no more specific message is suitable.
|
||||
"""
|
||||
http_status = 500
|
||||
message = _("Internal Server Error")
|
||||
|
||||
|
||||
# NotImplemented is a python keyword.
|
||||
class HttpNotImplemented(HttpServerError):
|
||||
"""HTTP 501 - Not Implemented.
|
||||
|
||||
The server either does not recognize the request method, or it lacks
|
||||
the ability to fulfill the request.
|
||||
"""
|
||||
http_status = 501
|
||||
message = _("Not Implemented")
|
||||
|
||||
|
||||
class BadGateway(HttpServerError):
|
||||
"""HTTP 502 - Bad Gateway.
|
||||
|
||||
The server was acting as a gateway or proxy and received an invalid
|
||||
response from the upstream server.
|
||||
"""
|
||||
http_status = 502
|
||||
message = _("Bad Gateway")
|
||||
|
||||
|
||||
class ServiceUnavailable(HttpServerError):
|
||||
"""HTTP 503 - Service Unavailable.
|
||||
|
||||
The server is currently unavailable.
|
||||
"""
|
||||
http_status = 503
|
||||
message = _("Service Unavailable")
|
||||
|
||||
|
||||
class GatewayTimeout(HttpServerError):
|
||||
"""HTTP 504 - Gateway Timeout.
|
||||
|
||||
The server was acting as a gateway or proxy and did not receive a timely
|
||||
response from the upstream server.
|
||||
"""
|
||||
http_status = 504
|
||||
message = _("Gateway Timeout")
|
||||
|
||||
|
||||
class HttpVersionNotSupported(HttpServerError):
|
||||
"""HTTP 505 - HttpVersion Not Supported.
|
||||
|
||||
The server does not support the HTTP protocol version used in the request.
|
||||
"""
|
||||
http_status = 505
|
||||
message = _("HTTP Version Not Supported")
|
||||
|
||||
|
||||
# _code_map contains all the classes that have http_status attribute.
|
||||
_code_map = dict(
|
||||
(getattr(obj, 'http_status', None), obj)
|
||||
for name, obj in six.iteritems(vars(sys.modules[__name__]))
|
||||
if inspect.isclass(obj) and getattr(obj, 'http_status', False)
|
||||
)
|
||||
|
||||
|
||||
def from_response(response, method, url):
|
||||
"""Returns an instance of :class:`HttpError` or subclass based on response.
|
||||
|
||||
:param response: instance of `requests.Response` class
|
||||
:param method: HTTP method used for request
|
||||
:param url: URL used for request
|
||||
"""
|
||||
|
||||
req_id = response.headers.get("x-openstack-request-id")
|
||||
# NOTE(hdd) true for older versions of nova and cinder
|
||||
if not req_id:
|
||||
req_id = response.headers.get("x-compute-request-id")
|
||||
kwargs = {
|
||||
"http_status": response.status_code,
|
||||
"response": response,
|
||||
"method": method,
|
||||
"url": url,
|
||||
"request_id": req_id,
|
||||
}
|
||||
if "retry-after" in response.headers:
|
||||
kwargs["retry_after"] = response.headers["retry-after"]
|
||||
|
||||
content_type = response.headers.get("Content-Type", "")
|
||||
if content_type.startswith("application/json"):
|
||||
try:
|
||||
body = response.json()
|
||||
except ValueError:
|
||||
pass
|
||||
else:
|
||||
if isinstance(body, dict):
|
||||
error = body.get(list(body)[0])
|
||||
if isinstance(error, dict):
|
||||
kwargs["message"] = (error.get("message") or
|
||||
error.get("faultstring"))
|
||||
kwargs["details"] = (error.get("details") or
|
||||
six.text_type(body))
|
||||
elif content_type.startswith("text/"):
|
||||
kwargs["details"] = getattr(response, 'text', '')
|
||||
|
||||
try:
|
||||
cls = _code_map[response.status_code]
|
||||
except KeyError:
|
||||
if 500 <= response.status_code < 600:
|
||||
cls = HttpServerError
|
||||
elif 400 <= response.status_code < 500:
|
||||
cls = HTTPClientError
|
||||
else:
|
||||
cls = HttpError
|
||||
return cls(**kwargs)
|
190
stacktaskclient/openstack/common/apiclient/fake_client.py
Normal file
190
stacktaskclient/openstack/common/apiclient/fake_client.py
Normal file
@ -0,0 +1,190 @@
|
||||
# Copyright 2013 OpenStack Foundation
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
"""
|
||||
A fake server that "responds" to API methods with pre-canned responses.
|
||||
|
||||
All of these responses come from the spec, so if for some reason the spec's
|
||||
wrong the tests might raise AssertionError. I've indicated in comments the
|
||||
places where actual behavior differs from the spec.
|
||||
"""
|
||||
|
||||
########################################################################
|
||||
#
|
||||
# THIS MODULE IS DEPRECATED
|
||||
#
|
||||
# Please refer to
|
||||
# https://etherpad.openstack.org/p/kilo-oslo-library-proposals for
|
||||
# the discussion leading to this deprecation.
|
||||
#
|
||||
# We recommend checking out the python-openstacksdk project
|
||||
# (https://launchpad.net/python-openstacksdk) instead.
|
||||
#
|
||||
########################################################################
|
||||
|
||||
# W0102: Dangerous default value %s as argument
|
||||
# pylint: disable=W0102
|
||||
|
||||
import json
|
||||
|
||||
import requests
|
||||
import six
|
||||
from six.moves.urllib import parse
|
||||
|
||||
from stacktaskclient.openstack.common.apiclient import client
|
||||
|
||||
|
||||
def assert_has_keys(dct, required=None, optional=None):
|
||||
required = required or []
|
||||
optional = optional or []
|
||||
for k in required:
|
||||
try:
|
||||
assert k in dct
|
||||
except AssertionError:
|
||||
extra_keys = set(dct.keys()).difference(set(required + optional))
|
||||
raise AssertionError("found unexpected keys: %s" %
|
||||
list(extra_keys))
|
||||
|
||||
|
||||
class TestResponse(requests.Response):
|
||||
"""Wrap requests.Response and provide a convenient initialization.
|
||||
"""
|
||||
|
||||
def __init__(self, data):
|
||||
super(TestResponse, self).__init__()
|
||||
self._content_consumed = True
|
||||
if isinstance(data, dict):
|
||||
self.status_code = data.get('status_code', 200)
|
||||
# Fake the text attribute to streamline Response creation
|
||||
text = data.get('text', "")
|
||||
if isinstance(text, (dict, list)):
|
||||
self._content = json.dumps(text)
|
||||
default_headers = {
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
else:
|
||||
self._content = text
|
||||
default_headers = {}
|
||||
if six.PY3 and isinstance(self._content, six.string_types):
|
||||
self._content = self._content.encode('utf-8', 'strict')
|
||||
self.headers = data.get('headers') or default_headers
|
||||
else:
|
||||
self.status_code = data
|
||||
|
||||
def __eq__(self, other):
|
||||
return (self.status_code == other.status_code and
|
||||
self.headers == other.headers and
|
||||
self._content == other._content)
|
||||
|
||||
|
||||
class FakeHTTPClient(client.HTTPClient):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.callstack = []
|
||||
self.fixtures = kwargs.pop("fixtures", None) or {}
|
||||
if not args and "auth_plugin" not in kwargs:
|
||||
args = (None, )
|
||||
super(FakeHTTPClient, self).__init__(*args, **kwargs)
|
||||
|
||||
def assert_called(self, method, url, body=None, pos=-1):
|
||||
"""Assert than an API method was just called.
|
||||
"""
|
||||
expected = (method, url)
|
||||
called = self.callstack[pos][0:2]
|
||||
assert self.callstack, \
|
||||
"Expected %s %s but no calls were made." % expected
|
||||
|
||||
assert expected == called, 'Expected %s %s; got %s %s' % \
|
||||
(expected + called)
|
||||
|
||||
if body is not None:
|
||||
if self.callstack[pos][3] != body:
|
||||
raise AssertionError('%r != %r' %
|
||||
(self.callstack[pos][3], body))
|
||||
|
||||
def assert_called_anytime(self, method, url, body=None):
|
||||
"""Assert than an API method was called anytime in the test.
|
||||
"""
|
||||
expected = (method, url)
|
||||
|
||||
assert self.callstack, \
|
||||
"Expected %s %s but no calls were made." % expected
|
||||
|
||||
found = False
|
||||
entry = None
|
||||
for entry in self.callstack:
|
||||
if expected == entry[0:2]:
|
||||
found = True
|
||||
break
|
||||
|
||||
assert found, 'Expected %s %s; got %s' % \
|
||||
(method, url, self.callstack)
|
||||
if body is not None:
|
||||
assert entry[3] == body, "%s != %s" % (entry[3], body)
|
||||
|
||||
self.callstack = []
|
||||
|
||||
def clear_callstack(self):
|
||||
self.callstack = []
|
||||
|
||||
def authenticate(self):
|
||||
pass
|
||||
|
||||
def client_request(self, client, method, url, **kwargs):
|
||||
# Check that certain things are called correctly
|
||||
if method in ["GET", "DELETE"]:
|
||||
assert "json" not in kwargs
|
||||
|
||||
# Note the call
|
||||
self.callstack.append(
|
||||
(method,
|
||||
url,
|
||||
kwargs.get("headers") or {},
|
||||
kwargs.get("json") or kwargs.get("data")))
|
||||
try:
|
||||
fixture = self.fixtures[url][method]
|
||||
except KeyError:
|
||||
pass
|
||||
else:
|
||||
return TestResponse({"headers": fixture[0],
|
||||
"text": fixture[1]})
|
||||
|
||||
# Call the method
|
||||
args = parse.parse_qsl(parse.urlparse(url)[4])
|
||||
kwargs.update(args)
|
||||
munged_url = url.rsplit('?', 1)[0]
|
||||
munged_url = munged_url.strip('/').replace('/', '_').replace('.', '_')
|
||||
munged_url = munged_url.replace('-', '_')
|
||||
|
||||
callback = "%s_%s" % (method.lower(), munged_url)
|
||||
|
||||
if not hasattr(self, callback):
|
||||
raise AssertionError('Called unknown API method: %s %s, '
|
||||
'expected fakes method name: %s' %
|
||||
(method, url, callback))
|
||||
|
||||
resp = getattr(self, callback)(**kwargs)
|
||||
if len(resp) == 3:
|
||||
status, headers, body = resp
|
||||
else:
|
||||
status, body = resp
|
||||
headers = {}
|
||||
self.last_request_id = headers.get('x-openstack-request-id',
|
||||
'req-test')
|
||||
return TestResponse({
|
||||
"status_code": status,
|
||||
"text": body,
|
||||
"headers": headers,
|
||||
})
|
100
stacktaskclient/openstack/common/apiclient/utils.py
Normal file
100
stacktaskclient/openstack/common/apiclient/utils.py
Normal file
@ -0,0 +1,100 @@
|
||||
#
|
||||
# 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 MODULE IS DEPRECATED
|
||||
#
|
||||
# Please refer to
|
||||
# https://etherpad.openstack.org/p/kilo-oslo-library-proposals for
|
||||
# the discussion leading to this deprecation.
|
||||
#
|
||||
# We recommend checking out the python-openstacksdk project
|
||||
# (https://launchpad.net/python-openstacksdk) instead.
|
||||
#
|
||||
########################################################################
|
||||
|
||||
from oslo_utils import encodeutils
|
||||
from oslo_utils import uuidutils
|
||||
import six
|
||||
|
||||
from stacktaskclient.openstack.common._i18n import _
|
||||
from stacktaskclient.openstack.common.apiclient import exceptions
|
||||
|
||||
|
||||
def find_resource(manager, name_or_id, **find_args):
|
||||
"""Look for resource in a given manager.
|
||||
|
||||
Used as a helper for the _find_* methods.
|
||||
Example:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
def _find_hypervisor(cs, hypervisor):
|
||||
#Get a hypervisor by name or ID.
|
||||
return cliutils.find_resource(cs.hypervisors, hypervisor)
|
||||
"""
|
||||
# first try to get entity as integer id
|
||||
try:
|
||||
return manager.get(int(name_or_id))
|
||||
except (TypeError, ValueError, exceptions.NotFound):
|
||||
pass
|
||||
|
||||
# now try to get entity as uuid
|
||||
try:
|
||||
if six.PY2:
|
||||
tmp_id = encodeutils.safe_encode(name_or_id)
|
||||
else:
|
||||
tmp_id = encodeutils.safe_decode(name_or_id)
|
||||
|
||||
if uuidutils.is_uuid_like(tmp_id):
|
||||
return manager.get(tmp_id)
|
||||
except (TypeError, ValueError, exceptions.NotFound):
|
||||
pass
|
||||
|
||||
# for str id which is not uuid
|
||||
if getattr(manager, 'is_alphanum_id_allowed', False):
|
||||
try:
|
||||
return manager.get(name_or_id)
|
||||
except exceptions.NotFound:
|
||||
pass
|
||||
|
||||
try:
|
||||
try:
|
||||
return manager.find(human_id=name_or_id, **find_args)
|
||||
except exceptions.NotFound:
|
||||
pass
|
||||
|
||||
# finally try to find entity by name
|
||||
try:
|
||||
resource = getattr(manager, 'resource_class', None)
|
||||
name_attr = resource.NAME_ATTR if resource else 'name'
|
||||
kwargs = {name_attr: name_or_id}
|
||||
kwargs.update(find_args)
|
||||
return manager.find(**kwargs)
|
||||
except exceptions.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 exceptions.CommandError(msg)
|
||||
except exceptions.NoUniqueMatch:
|
||||
msg = _("Multiple %(name)s matches found for "
|
||||
"'%(name_or_id)s', use an ID to be more specific.") % \
|
||||
{
|
||||
"name": manager.resource_class.__name__.lower(),
|
||||
"name_or_id": name_or_id
|
||||
}
|
||||
raise exceptions.CommandError(msg)
|
271
stacktaskclient/openstack/common/cliutils.py
Normal file
271
stacktaskclient/openstack/common/cliutils.py
Normal file
@ -0,0 +1,271 @@
|
||||
# Copyright 2012 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.
|
||||
|
||||
# W0603: Using the global statement
|
||||
# W0621: Redefining name %s from outer scope
|
||||
# pylint: disable=W0603,W0621
|
||||
|
||||
from __future__ import print_function
|
||||
|
||||
import getpass
|
||||
import inspect
|
||||
import os
|
||||
import sys
|
||||
import textwrap
|
||||
|
||||
from oslo_utils import encodeutils
|
||||
from oslo_utils import strutils
|
||||
import prettytable
|
||||
import six
|
||||
from six import moves
|
||||
|
||||
from stacktaskclient.openstack.common._i18n import _
|
||||
|
||||
|
||||
class MissingArgs(Exception):
|
||||
"""Supplied arguments are not sufficient for calling a function."""
|
||||
def __init__(self, missing):
|
||||
self.missing = missing
|
||||
msg = _("Missing arguments: %s") % ", ".join(missing)
|
||||
super(MissingArgs, self).__init__(msg)
|
||||
|
||||
|
||||
def validate_args(fn, *args, **kwargs):
|
||||
"""Check that the supplied args are sufficient for calling a function.
|
||||
|
||||
>>> validate_args(lambda a: None)
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
MissingArgs: Missing argument(s): a
|
||||
>>> validate_args(lambda a, b, c, d: None, 0, c=1)
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
MissingArgs: Missing argument(s): b, d
|
||||
|
||||
:param fn: the function to check
|
||||
:param arg: the positional arguments supplied
|
||||
:param kwargs: the keyword arguments supplied
|
||||
"""
|
||||
argspec = inspect.getargspec(fn)
|
||||
|
||||
num_defaults = len(argspec.defaults or [])
|
||||
required_args = argspec.args[:len(argspec.args) - num_defaults]
|
||||
|
||||
def isbound(method):
|
||||
return getattr(method, '__self__', None) is not None
|
||||
|
||||
if isbound(fn):
|
||||
required_args.pop(0)
|
||||
|
||||
missing = [arg for arg in required_args if arg not in kwargs]
|
||||
missing = missing[len(args):]
|
||||
if missing:
|
||||
raise MissingArgs(missing)
|
||||
|
||||
|
||||
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 unauthenticated(func):
|
||||
"""Adds 'unauthenticated' attribute to decorated function.
|
||||
|
||||
Usage:
|
||||
|
||||
>>> @unauthenticated
|
||||
... def mymethod(f):
|
||||
... pass
|
||||
"""
|
||||
func.unauthenticated = True
|
||||
return func
|
||||
|
||||
|
||||
def isunauthenticated(func):
|
||||
"""Checks if the function does not require authentication.
|
||||
|
||||
Mark such functions with the `@unauthenticated` decorator.
|
||||
|
||||
:returns: bool
|
||||
"""
|
||||
return getattr(func, 'unauthenticated', False)
|
||||
|
||||
|
||||
def print_list(objs, fields, formatters=None, sortby_index=0,
|
||||
mixed_case_fields=None, field_labels=None):
|
||||
"""Print a list or 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 print_dict(dct, dict_property="Property", wrap=0):
|
||||
"""Print a `dict` as a table of two columns.
|
||||
|
||||
:param dct: `dict` to print
|
||||
:param dict_property: name of the first column
|
||||
:param wrap: wrapping for the second column
|
||||
"""
|
||||
pt = prettytable.PrettyTable([dict_property, 'Value'])
|
||||
pt.align = 'l'
|
||||
for k, v in six.iteritems(dct):
|
||||
# convert dict to str to check length
|
||||
if isinstance(v, dict):
|
||||
v = six.text_type(v)
|
||||
if wrap > 0:
|
||||
v = textwrap.fill(six.text_type(v), wrap)
|
||||
# if value has a newline, add in multiple rows
|
||||
# e.g. fault with stacktrace
|
||||
if v and isinstance(v, six.string_types) and r'\n' in v:
|
||||
lines = v.strip().split(r'\n')
|
||||
col1 = k
|
||||
for line in lines:
|
||||
pt.add_row([col1, line])
|
||||
col1 = ''
|
||||
else:
|
||||
pt.add_row([k, v])
|
||||
|
||||
if six.PY3:
|
||||
print(encodeutils.safe_encode(pt.get_string()).decode())
|
||||
else:
|
||||
print(encodeutils.safe_encode(pt.get_string()))
|
||||
|
||||
|
||||
def get_password(max_password_prompts=3):
|
||||
"""Read password from TTY."""
|
||||
verify = strutils.bool_from_string(env("OS_VERIFY_PASSWORD"))
|
||||
pw = None
|
||||
if hasattr(sys.stdin, "isatty") and sys.stdin.isatty():
|
||||
# Check for Ctrl-D
|
||||
try:
|
||||
for __ in moves.range(max_password_prompts):
|
||||
pw1 = getpass.getpass("OS Password: ")
|
||||
if verify:
|
||||
pw2 = getpass.getpass("Please verify: ")
|
||||
else:
|
||||
pw2 = pw1
|
||||
if pw1 == pw2 and pw1:
|
||||
pw = pw1
|
||||
break
|
||||
except EOFError:
|
||||
pass
|
||||
return pw
|
||||
|
||||
|
||||
def service_type(stype):
|
||||
"""Adds 'service_type' attribute to decorated function.
|
||||
|
||||
Usage:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
@service_type('volume')
|
||||
def mymethod(f):
|
||||
...
|
||||
"""
|
||||
def inner(f):
|
||||
f.service_type = stype
|
||||
return f
|
||||
return inner
|
||||
|
||||
|
||||
def get_service_type(f):
|
||||
"""Retrieves service type from function."""
|
||||
return getattr(f, 'service_type', None)
|
||||
|
||||
|
||||
def pretty_choice_list(l):
|
||||
return ', '.join("'%s'" % i for i in l)
|
||||
|
||||
|
||||
def exit(msg=''):
|
||||
if msg:
|
||||
print (msg, file=sys.stderr)
|
||||
sys.exit(1)
|
676
stacktaskclient/shell.py
Normal file
676
stacktaskclient/shell.py
Normal file
@ -0,0 +1,676 @@
|
||||
# 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 Stacktask API.
|
||||
"""
|
||||
|
||||
from __future__ import print_function
|
||||
|
||||
import argparse
|
||||
import logging
|
||||
import sys
|
||||
|
||||
from oslo_utils import encodeutils
|
||||
from oslo_utils import importutils
|
||||
import six
|
||||
import six.moves.urllib.parse as urlparse
|
||||
|
||||
from keystoneclient.auth.identity import v2 as v2_auth
|
||||
from keystoneclient.auth.identity import v3 as v3_auth
|
||||
from keystoneclient import discover
|
||||
from keystoneclient.openstack.common.apiclient import exceptions as ks_exc
|
||||
from keystoneclient import session as kssession
|
||||
|
||||
import stacktaskclient
|
||||
from stacktaskclient import client as stacktask_client
|
||||
from stacktaskclient.common import utils
|
||||
from stacktaskclient import exc
|
||||
from stacktaskclient.openstack.common._i18n import _
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
osprofiler_profiler = importutils.try_import("osprofiler.profiler")
|
||||
|
||||
|
||||
class StacktaskShell(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 avaible in
|
||||
# python-keystoneclient.
|
||||
parser.add_argument('-k', '--insecure',
|
||||
default=False,
|
||||
action='store_true',
|
||||
help=_('Explicitly allow this client 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',
|
||||
help=_('Path of certificate file to use in SSL '
|
||||
'connection. This file can optionally be '
|
||||
'prepended with the private key.'))
|
||||
|
||||
parser.add_argument('--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('--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('--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='stacktask',
|
||||
description=__doc__.strip(),
|
||||
epilog=_('See "%(arg)s" for help on a specific command.') % {
|
||||
'arg': 'stacktask 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=stacktaskclient.__version__,
|
||||
help=_("Shows the client version and exits."))
|
||||
|
||||
parser.add_argument('-d', '--debug',
|
||||
default=bool(utils.env('STACKTASKCLIENT_DEBUG')),
|
||||
action='store_true',
|
||||
help=_('Defaults to %(value)s.') % {
|
||||
'value': 'env[STACKTASKCLIENT_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 stacktaskclient 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('--stacktask-url',
|
||||
default=utils.env('STACKTASK_URL'),
|
||||
help=_('Defaults to %(value)s.') % {
|
||||
'value': 'env[STACKTASK_URL]'
|
||||
})
|
||||
|
||||
parser.add_argument('--api-version',
|
||||
default=utils.env('STACKTASK_API_VERSION', default='1'),
|
||||
help=_('Defaults to %(value)s or 1.') % {
|
||||
'value': 'env[STACKTASK_API_VERSION]'
|
||||
})
|
||||
|
||||
|
||||
# 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 = utils.import_versioned_module(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 _discover_auth_versions(self, session, auth_url):
|
||||
# discover the API versions the server is supporting base on the
|
||||
# given URL
|
||||
v2_auth_url = None
|
||||
v3_auth_url = None
|
||||
try:
|
||||
ks_discover = discover.Discover(session=session, auth_url=auth_url)
|
||||
v2_auth_url = ks_discover.url_for('2.0')
|
||||
v3_auth_url = ks_discover.url_for('3.0')
|
||||
except ks_exc.ClientException:
|
||||
# Identity service may not support discover API version.
|
||||
# Lets trying to figure out the API version from the original URL.
|
||||
url_parts = urlparse.urlparse(auth_url)
|
||||
(scheme, netloc, path, params, query, fragment) = url_parts
|
||||
path = path.lower()
|
||||
if path.startswith('/v3'):
|
||||
v3_auth_url = auth_url
|
||||
elif path.startswith('/v2'):
|
||||
v2_auth_url = auth_url
|
||||
else:
|
||||
# not enough information to determine the auth version
|
||||
msg = _('Unable to determine the Keystone version '
|
||||
'to authenticate with using the given '
|
||||
'auth_url. Identity service may not support API '
|
||||
'version discovery. Please provide a versioned '
|
||||
'auth_url instead.')
|
||||
raise exc.CommandError(msg)
|
||||
|
||||
return (v2_auth_url, v3_auth_url)
|
||||
|
||||
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)
|
||||
|
||||
# FIXME(gyee): this code should come from keystoneclient
|
||||
if verify is None:
|
||||
if insecure:
|
||||
verify = False
|
||||
else:
|
||||
# TODO(gyee): should we do
|
||||
# stacktaskclient.common.http.get_system_ca_fle()?
|
||||
verify = cacert or True
|
||||
|
||||
return kssession.Session(verify=verify, cert=cert, timeout=timeout)
|
||||
|
||||
def _get_keystone_v3_auth(self, v3_auth_url, **kwargs):
|
||||
auth_token = kwargs.pop('auth_token', None)
|
||||
if auth_token:
|
||||
return v3_auth.Token(v3_auth_url, auth_token)
|
||||
else:
|
||||
return v3_auth.Password(v3_auth_url, **kwargs)
|
||||
|
||||
def _get_keystone_v2_auth(self, v2_auth_url, **kwargs):
|
||||
auth_token = kwargs.pop('auth_token', None)
|
||||
tenant_id = kwargs.pop('project_id', None)
|
||||
tenant_name = kwargs.pop('project_name', None)
|
||||
if auth_token:
|
||||
return v2_auth.Token(v2_auth_url, auth_token,
|
||||
tenant_id=tenant_id,
|
||||
tenant_name=tenant_name)
|
||||
else:
|
||||
return v2_auth.Password(v2_auth_url,
|
||||
username=kwargs.pop('username', None),
|
||||
password=kwargs.pop('password', None),
|
||||
tenant_id=tenant_id,
|
||||
tenant_name=tenant_name)
|
||||
|
||||
def _get_keystone_auth(self, session, auth_url, **kwargs):
|
||||
# FIXME(dhu): this code should come from keystoneclient
|
||||
|
||||
# discover the supported keystone versions using the given url
|
||||
(v2_auth_url, v3_auth_url) = self._discover_auth_versions(
|
||||
session=session,
|
||||
auth_url=auth_url)
|
||||
|
||||
# Determine which authentication plugin to use. First inspect the
|
||||
# auth_url to see the supported version. If both v3 and v2 are
|
||||
# supported, then use the highest version if possible.
|
||||
auth = None
|
||||
if v3_auth_url and v2_auth_url:
|
||||
user_domain_name = kwargs.get('user_domain_name', None)
|
||||
user_domain_id = kwargs.get('user_domain_id', None)
|
||||
project_domain_name = kwargs.get('project_domain_name', None)
|
||||
project_domain_id = kwargs.get('project_domain_id', None)
|
||||
|
||||
# support both v2 and v3 auth. Use v3 if domain information is
|
||||
# provided.
|
||||
if (user_domain_name or user_domain_id or project_domain_name or
|
||||
project_domain_id):
|
||||
auth = self._get_keystone_v3_auth(v3_auth_url, **kwargs)
|
||||
else:
|
||||
auth = self._get_keystone_v2_auth(v2_auth_url, **kwargs)
|
||||
elif v3_auth_url:
|
||||
# support only v3
|
||||
auth = self._get_keystone_v3_auth(v3_auth_url, **kwargs)
|
||||
elif v2_auth_url:
|
||||
# support only v2
|
||||
auth = self._get_keystone_v2_auth(v2_auth_url, **kwargs)
|
||||
else:
|
||||
raise exc.CommandError(_('Unable to determine the Keystone '
|
||||
'version to authenticate with using the '
|
||||
'given auth_url.'))
|
||||
|
||||
return auth
|
||||
|
||||
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.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
|
||||
}
|
||||
|
||||
endpoint = None # args.heat_url
|
||||
service_type = args.os_service_type or 'registration'
|
||||
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': None, # args.include_password,
|
||||
'insecure': args.insecure,
|
||||
'timeout': args.api_timeout
|
||||
}
|
||||
else:
|
||||
keystone_session = self._get_keystone_session(**kwargs)
|
||||
project_id = args.os_project_id or args.os_tenant_id
|
||||
project_name = args.os_project_name or args.os_tenant_name
|
||||
endpoint_type = args.os_endpoint_type or 'publicURL'
|
||||
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_token': args.os_auth_token,
|
||||
'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 = self._get_keystone_auth(keystone_session,
|
||||
args.os_auth_url,
|
||||
**kwargs)
|
||||
if not endpoint:
|
||||
svc_type = service_type
|
||||
region_name = args.os_region_name
|
||||
endpoint = keystone_auth.get_endpoint(keystone_session,
|
||||
service_type=svc_type,
|
||||
interface=endpoint_type,
|
||||
region_name=region_name)
|
||||
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': None # args.include_password
|
||||
}
|
||||
|
||||
client = stacktask_client.Client(api_version, endpoint, **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:]
|
||||
|
||||
StacktaskShell().main(args)
|
||||
except KeyboardInterrupt:
|
||||
print(_("... terminating stacktask 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()
|
0
stacktaskclient/tests/__init__.py
Normal file
0
stacktaskclient/tests/__init__.py
Normal file
0
stacktaskclient/tests/functional/__init__.py
Normal file
0
stacktaskclient/tests/functional/__init__.py
Normal file
42
stacktaskclient/tests/functional/base.py
Normal file
42
stacktaskclient/tests/functional/base.py
Normal file
@ -0,0 +1,42 @@
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
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)
|
50
stacktaskclient/tests/functional/hooks/post_test_hook.sh
Executable file
50
stacktaskclient/tests/functional/hooks/post_test_hook.sh
Executable file
@ -0,0 +1,50 @@
|
||||
#!/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 .tox/functional/bin/python /usr/local/jenkins/slave_scripts/subunit2html.py $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
|
18
stacktaskclient/tests/functional/templates/heat_minimal.yaml
Normal file
18
stacktaskclient/tests/functional/templates/heat_minimal.yaml
Normal file
@ -0,0 +1,18 @@
|
||||
HeatTemplateFormatVersion: '2012-12-12'
|
||||
Description: Minimal template to test validation
|
||||
Parameters:
|
||||
InstanceImage:
|
||||
Description: Glance image name
|
||||
Type: String
|
||||
InstanceType:
|
||||
Description: Nova instance type
|
||||
Type: String
|
||||
Default: m1.small
|
||||
AllowedValues: [m1.tiny, m1.small, m1.medium, m1.large, m1.nano, m1.xlarge, m1.micro, m1.heat]
|
||||
ConstraintDescription: must be a valid nova instance type.
|
||||
Resources:
|
||||
InstanceResource:
|
||||
Type: OS::Nova::Server
|
||||
Properties:
|
||||
flavor: {Ref: InstanceType}
|
||||
image: {Ref: InstanceImage}
|
@ -0,0 +1,19 @@
|
||||
heat_template_version: 2015-04-30
|
||||
description: A minimal HOT test template
|
||||
parameters:
|
||||
instance_image:
|
||||
description: Glance image name
|
||||
type: string
|
||||
instance_type:
|
||||
description: Nova instance type
|
||||
type: string
|
||||
default: m1.small
|
||||
constraints:
|
||||
- allowed_values: [m1.tiny, m1.small, m1.medium, m1.large, m1.nano, m1.xlarge, m1.micro, m1.heat]
|
||||
description: must be a valid nova instance type.
|
||||
resources:
|
||||
instance:
|
||||
type: OS::Nova::Server
|
||||
properties:
|
||||
image: { get_param: instance_image }
|
||||
flavor: { get_param: instance_type }
|
102
stacktaskclient/tests/functional/test_readonly_heat.py
Normal file
102
stacktaskclient/tests/functional/test_readonly_heat.py
Normal file
@ -0,0 +1,102 @@
|
||||
# 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'])
|
0
stacktaskclient/tests/unit/__init__.py
Normal file
0
stacktaskclient/tests/unit/__init__.py
Normal file
256
stacktaskclient/tests/unit/fakes.py
Normal file
256
stacktaskclient/tests/unit/fakes.py
Normal file
@ -0,0 +1,256 @@
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
# implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from oslo_serialization import jsonutils
|
||||
|
||||
from heatclient.common import http
|
||||
from heatclient import exc
|
||||
|
||||
|
||||
def script_heat_list(url=None, show_nested=False, client=http.HTTPClient):
|
||||
if url is None:
|
||||
url = '/stacks?'
|
||||
|
||||
resp_dict = {"stacks": [
|
||||
{
|
||||
"id": "1",
|
||||
"stack_name": "teststack",
|
||||
"stack_owner": "testowner",
|
||||
"project": "testproject",
|
||||
"stack_status": 'CREATE_COMPLETE',
|
||||
"creation_time": "2012-10-25T01:58:47Z"
|
||||
},
|
||||
{
|
||||
"id": "2",
|
||||
"stack_name": "teststack2",
|
||||
"stack_owner": "testowner",
|
||||
"project": "testproject",
|
||||
"stack_status": 'IN_PROGRESS',
|
||||
"creation_time": "2012-10-25T01:58:47Z"
|
||||
}]
|
||||
}
|
||||
if show_nested:
|
||||
nested = {
|
||||
"id": "3",
|
||||
"stack_name": "teststack_nested",
|
||||
"stack_status": 'IN_PROGRESS',
|
||||
"creation_time": "2012-10-25T01:58:47Z",
|
||||
"parent": "theparentof3"
|
||||
}
|
||||
resp_dict["stacks"].append(nested)
|
||||
resp = FakeHTTPResponse(200,
|
||||
'success, you',
|
||||
{'content-type': 'application/json'},
|
||||
jsonutils.dumps(resp_dict))
|
||||
if client == http.SessionClient:
|
||||
client.request(url, 'GET').AndReturn(resp)
|
||||
else:
|
||||
client.json_request('GET', url).AndReturn((resp, resp_dict))
|
||||
|
||||
|
||||
def mock_script_heat_list(show_nested=False):
|
||||
resp_dict = {"stacks": [
|
||||
{
|
||||
"id": "1",
|
||||
"stack_name": "teststack",
|
||||
"stack_owner": "testowner",
|
||||
"project": "testproject",
|
||||
"stack_status": 'CREATE_COMPLETE',
|
||||
"creation_time": "2012-10-25T01:58:47Z"
|
||||
},
|
||||
{
|
||||
"id": "2",
|
||||
"stack_name": "teststack2",
|
||||
"stack_owner": "testowner",
|
||||
"project": "testproject",
|
||||
"stack_status": 'IN_PROGRESS',
|
||||
"creation_time": "2012-10-25T01:58:47Z"
|
||||
}]
|
||||
}
|
||||
if show_nested:
|
||||
nested = {
|
||||
"id": "3",
|
||||
"stack_name": "teststack_nested",
|
||||
"stack_status": 'IN_PROGRESS',
|
||||
"creation_time": "2012-10-25T01:58:47Z",
|
||||
"parent": "theparentof3"
|
||||
}
|
||||
resp_dict["stacks"].append(nested)
|
||||
resp = FakeHTTPResponse(200,
|
||||
'success, you',
|
||||
{'content-type': 'application/json'},
|
||||
jsonutils.dumps(resp_dict))
|
||||
return resp, resp_dict
|
||||
|
||||
|
||||
def mock_script_event_list(
|
||||
stack_name="teststack", resource_name=None,
|
||||
rsrc_eventid1="7fecaeed-d237-4559-93a5-92d5d9111205",
|
||||
rsrc_eventid2="e953547a-18f8-40a7-8e63-4ec4f509648b",
|
||||
action="CREATE", final_state="COMPLETE", fakehttp=True):
|
||||
|
||||
resp_dict = {"events": [
|
||||
{"event_time": "2013-12-05T14:14:31Z",
|
||||
"id": rsrc_eventid1,
|
||||
"links": [{"href": "http://heat.example.com:8004/foo",
|
||||
"rel": "self"},
|
||||
{"href": "http://heat.example.com:8004/foo2",
|
||||
"rel": "resource"},
|
||||
{"href": "http://heat.example.com:8004/foo3",
|
||||
"rel": "stack"}],
|
||||
"logical_resource_id": "myDeployment",
|
||||
"physical_resource_id": None,
|
||||
"resource_name": resource_name if resource_name else "testresource",
|
||||
"resource_status": "%s_IN_PROGRESS" % action,
|
||||
"resource_status_reason": "state changed"},
|
||||
{"event_time": "2013-12-05T14:14:32Z",
|
||||
"id": rsrc_eventid2,
|
||||
"links": [{"href": "http://heat.example.com:8004/foo",
|
||||
"rel": "self"},
|
||||
{"href": "http://heat.example.com:8004/foo2",
|
||||
"rel": "resource"},
|
||||
{"href": "http://heat.example.com:8004/foo3",
|
||||
"rel": "stack"}],
|
||||
"logical_resource_id": "myDeployment",
|
||||
"physical_resource_id": "bce15ec4-8919-4a02-8a90-680960fb3731",
|
||||
"resource_name": resource_name if resource_name else "testresource",
|
||||
"resource_status": "%s_%s" % (action, final_state),
|
||||
"resource_status_reason": "state changed"}]}
|
||||
|
||||
if resource_name is None:
|
||||
# if resource_name is not specified,
|
||||
# then request is made for stack events. Hence include the stack event
|
||||
stack_event1 = "0159dccd-65e1-46e8-a094-697d20b009e5"
|
||||
stack_event2 = "8f591a36-7190-4adb-80da-00191fe22388"
|
||||
resp_dict["events"].insert(
|
||||
0, {"event_time": "2013-12-05T14:14:30Z",
|
||||
"id": stack_event1,
|
||||
"links": [{"href": "http://heat.example.com:8004/foo",
|
||||
"rel": "self"},
|
||||
{"href": "http://heat.example.com:8004/foo2",
|
||||
"rel": "resource"},
|
||||
{"href": "http://heat.example.com:8004/foo3",
|
||||
"rel": "stack"}],
|
||||
"logical_resource_id": "aResource",
|
||||
"physical_resource_id": None,
|
||||
"resource_name": stack_name,
|
||||
"resource_status": "%s_IN_PROGRESS" % action,
|
||||
"resource_status_reason": "state changed"})
|
||||
resp_dict["events"].append(
|
||||
{"event_time": "2013-12-05T14:14:33Z",
|
||||
"id": stack_event2,
|
||||
"links": [{"href": "http://heat.example.com:8004/foo",
|
||||
"rel": "self"},
|
||||
{"href": "http://heat.example.com:8004/foo2",
|
||||
"rel": "resource"},
|
||||
{"href": "http://heat.example.com:8004/foo3",
|
||||
"rel": "stack"}],
|
||||
"logical_resource_id": "aResource",
|
||||
"physical_resource_id": None,
|
||||
"resource_name": stack_name,
|
||||
"resource_status": "%s_%s" % (action, final_state),
|
||||
"resource_status_reason": "state changed"})
|
||||
|
||||
resp = FakeHTTPResponse(
|
||||
200,
|
||||
'OK',
|
||||
{'content-type': 'application/json'},
|
||||
jsonutils.dumps(resp_dict)) if fakehttp else None
|
||||
|
||||
return resp, resp_dict
|
||||
|
||||
|
||||
def script_heat_normal_error(client=http.HTTPClient):
|
||||
resp_dict = {
|
||||
"explanation": "The resource could not be found.",
|
||||
"code": 404,
|
||||
"error": {
|
||||
"message": "The Stack (bad) could not be found.",
|
||||
"type": "StackNotFound",
|
||||
"traceback": "",
|
||||
},
|
||||
"title": "Not Found"
|
||||
}
|
||||
resp = FakeHTTPResponse(400,
|
||||
'The resource could not be found',
|
||||
{'content-type': 'application/json'},
|
||||
jsonutils.dumps(resp_dict))
|
||||
if client == http.SessionClient:
|
||||
client.request('/stacks/bad', 'GET').AndRaise(exc.from_response(resp))
|
||||
else:
|
||||
client.json_request('GET',
|
||||
'/stacks/bad').AndRaise(exc.from_response(resp))
|
||||
|
||||
|
||||
def script_heat_error(resp_string, client=http.HTTPClient):
|
||||
resp = FakeHTTPResponse(400,
|
||||
'The resource could not be found',
|
||||
{'content-type': 'application/json'},
|
||||
resp_string)
|
||||
if client == http.SessionClient:
|
||||
client.request('/stacks/bad', 'GET').AndRaise(exc.from_response(resp))
|
||||
else:
|
||||
client.json_request('GET',
|
||||
'/stacks/bad').AndRaise(exc.from_response(resp))
|
||||
|
||||
|
||||
def fake_headers():
|
||||
return {'X-Auth-Token': 'abcd1234',
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'User-Agent': 'python-heatclient'}
|
||||
|
||||
|
||||
class FakeServiceCatalog():
|
||||
def url_for(self, endpoint_type, service_type):
|
||||
return 'http://192.168.1.5:8004/v1/f14b41234'
|
||||
|
||||
|
||||
class FakeKeystone():
|
||||
service_catalog = FakeServiceCatalog()
|
||||
|
||||
def __init__(self, auth_token):
|
||||
self.auth_token = auth_token
|
||||
|
||||
|
||||
class FakeRaw():
|
||||
version = 110
|
||||
|
||||
|
||||
class FakeHTTPResponse():
|
||||
|
||||
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)
|
108
stacktaskclient/tests/unit/test_actions.py
Normal file
108
stacktaskclient/tests/unit/test_actions.py
Normal file
@ -0,0 +1,108 @@
|
||||
# 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.tests.unit import fakes
|
||||
from heatclient.v1 import actions
|
||||
|
||||
import testtools
|
||||
|
||||
|
||||
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)
|
41
stacktaskclient/tests/unit/test_build_info.py
Normal file
41
stacktaskclient/tests/unit/test_build_info.py
Normal file
@ -0,0 +1,41 @@
|
||||
# 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)
|
861
stacktaskclient/tests/unit/test_common_http.py
Normal file
861
stacktaskclient/tests/unit/test_common_http.py
Normal file
@ -0,0 +1,861 @@
|
||||
# -*- 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 logging
|
||||
import mock
|
||||
import os
|
||||
import socket
|
||||
|
||||
|
||||
from oslo_serialization import jsonutils
|
||||
import requests
|
||||
import six
|
||||
import testtools
|
||||
|
||||
from heatclient.common import http
|
||||
from heatclient.common import utils
|
||||
from heatclient import exc
|
||||
from heatclient.tests.unit import fakes
|
||||
from keystoneclient import adapter
|
||||
from mox3 import mox
|
||||
|
||||
|
||||
class HttpClientTest(testtools.TestCase):
|
||||
|
||||
# Patch os.environ to avoid required auth info.
|
||||
def setUp(self):
|
||||
super(HttpClientTest, self).setUp()
|
||||
self.m = mox.Mox()
|
||||
self.m.StubOutWithMock(requests, 'request')
|
||||
self.addCleanup(self.m.VerifyAll)
|
||||
self.addCleanup(self.m.UnsetStubs)
|
||||
|
||||
def test_http_raw_request(self):
|
||||
headers = {'Content-Type': 'application/octet-stream',
|
||||
'User-Agent': 'python-heatclient'}
|
||||
|
||||
# Record a 200
|
||||
mock_conn = http.requests.request('GET', 'http://example.com:8004',
|
||||
allow_redirects=False,
|
||||
headers=headers)
|
||||
mock_conn.AndReturn(
|
||||
fakes.FakeHTTPResponse(
|
||||
200, 'OK',
|
||||
{'content-type': 'application/octet-stream'},
|
||||
''))
|
||||
# Replay, create client, assert
|
||||
self.m.ReplayAll()
|
||||
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]))
|
||||
|
||||
def test_token_or_credentials(self):
|
||||
# Record a 200
|
||||
fake200 = fakes.FakeHTTPResponse(
|
||||
200, 'OK',
|
||||
{'content-type': 'application/octet-stream'},
|
||||
'')
|
||||
|
||||
# no token or credentials
|
||||
mock_conn = http.requests.request(
|
||||
'GET', 'http://example.com:8004',
|
||||
allow_redirects=False,
|
||||
headers={'Content-Type': 'application/octet-stream',
|
||||
'User-Agent': 'python-heatclient'})
|
||||
mock_conn.AndReturn(fake200)
|
||||
|
||||
# credentials
|
||||
mock_conn = http.requests.request(
|
||||
'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_conn.AndReturn(fake200)
|
||||
|
||||
# token suppresses credentials
|
||||
mock_conn = http.requests.request(
|
||||
'GET', 'http://example.com:8004',
|
||||
allow_redirects=False,
|
||||
headers={'Content-Type': 'application/octet-stream',
|
||||
'User-Agent': 'python-heatclient',
|
||||
'X-Auth-Token': 'abcd1234'})
|
||||
mock_conn.AndReturn(fake200)
|
||||
|
||||
# Replay, create client, assert
|
||||
self.m.ReplayAll()
|
||||
client = http.HTTPClient('http://example.com:8004')
|
||||
resp = client.raw_request('GET', '')
|
||||
self.assertEqual(200, resp.status_code)
|
||||
|
||||
client.username = 'user'
|
||||
client.password = 'pass'
|
||||
resp = client.raw_request('GET', '')
|
||||
self.assertEqual(200, resp.status_code)
|
||||
|
||||
client.auth_token = 'abcd1234'
|
||||
resp = client.raw_request('GET', '')
|
||||
self.assertEqual(200, resp.status_code)
|
||||
|
||||
def test_include_pass(self):
|
||||
# Record a 200
|
||||
fake200 = fakes.FakeHTTPResponse(
|
||||
200, 'OK',
|
||||
{'content-type': 'application/octet-stream'},
|
||||
'')
|
||||
|
||||
# no token or credentials
|
||||
mock_conn = http.requests.request(
|
||||
'GET', 'http://example.com:8004',
|
||||
allow_redirects=False,
|
||||
headers={'Content-Type': 'application/octet-stream',
|
||||
'User-Agent': 'python-heatclient'})
|
||||
mock_conn.AndReturn(fake200)
|
||||
|
||||
# credentials
|
||||
mock_conn = http.requests.request(
|
||||
'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_conn.AndReturn(fake200)
|
||||
|
||||
# token suppresses credentials
|
||||
mock_conn = http.requests.request(
|
||||
'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'})
|
||||
mock_conn.AndReturn(fake200)
|
||||
|
||||
# Replay, create client, assert
|
||||
self.m.ReplayAll()
|
||||
client = http.HTTPClient('http://example.com:8004')
|
||||
resp = client.raw_request('GET', '')
|
||||
self.assertEqual(200, resp.status_code)
|
||||
|
||||
client.username = 'user'
|
||||
client.password = 'pass'
|
||||
client.include_pass = True
|
||||
resp = client.raw_request('GET', '')
|
||||
self.assertEqual(200, resp.status_code)
|
||||
|
||||
client.auth_token = 'abcd1234'
|
||||
resp = client.raw_request('GET', '')
|
||||
self.assertEqual(200, resp.status_code)
|
||||
|
||||
def test_not_include_pass(self):
|
||||
# Record a 200
|
||||
fake500 = fakes.FakeHTTPResponse(
|
||||
500, 'ERROR',
|
||||
{'content-type': 'application/octet-stream'},
|
||||
'(HTTP 401)')
|
||||
|
||||
# no token or credentials
|
||||
mock_conn = http.requests.request(
|
||||
'GET', 'http://example.com:8004',
|
||||
allow_redirects=False,
|
||||
headers={'Content-Type': 'application/octet-stream',
|
||||
'User-Agent': 'python-heatclient'})
|
||||
mock_conn.AndReturn(fake500)
|
||||
|
||||
# Replay, create client, assert
|
||||
self.m.ReplayAll()
|
||||
client = http.HTTPClient('http://example.com:8004')
|
||||
e = self.assertRaises(exc.HTTPUnauthorized,
|
||||
client.raw_request, 'GET', '')
|
||||
self.assertIn('include-password', str(e))
|
||||
|
||||
def test_region_name(self):
|
||||
# Record a 200
|
||||
fake200 = fakes.FakeHTTPResponse(
|
||||
200, 'OK',
|
||||
{'content-type': 'application/octet-stream'},
|
||||
'')
|
||||
|
||||
# Specify region name
|
||||
mock_conn = http.requests.request(
|
||||
'GET', 'http://example.com:8004',
|
||||
allow_redirects=False,
|
||||
headers={'Content-Type': 'application/octet-stream',
|
||||
'X-Region-Name': 'RegionOne',
|
||||
'User-Agent': 'python-heatclient'})
|
||||
mock_conn.AndReturn(fake200)
|
||||
|
||||
# Replay, create client, assert
|
||||
self.m.ReplayAll()
|
||||
client = http.HTTPClient('http://example.com:8004')
|
||||
client.region_name = 'RegionOne'
|
||||
resp = client.raw_request('GET', '')
|
||||
self.assertEqual(200, resp.status_code)
|
||||
|
||||
def test_http_json_request(self):
|
||||
# Record a 200
|
||||
mock_conn = http.requests.request(
|
||||
'GET', 'http://example.com:8004',
|
||||
allow_redirects=False,
|
||||
headers={'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'User-Agent': 'python-heatclient'})
|
||||
mock_conn.AndReturn(
|
||||
fakes.FakeHTTPResponse(
|
||||
200, 'OK',
|
||||
{'content-type': 'application/json'},
|
||||
'{}'))
|
||||
# Replay, create client, assert
|
||||
self.m.ReplayAll()
|
||||
client = http.HTTPClient('http://example.com:8004')
|
||||
resp, body = client.json_request('GET', '')
|
||||
self.assertEqual(200, resp.status_code)
|
||||
self.assertEqual({}, body)
|
||||
|
||||
def test_http_json_request_argument_passed_to_requests(self):
|
||||
"""Check that we have sent the proper arguments to requests."""
|
||||
# Record a 200
|
||||
mock_conn = http.requests.request(
|
||||
'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'})
|
||||
mock_conn.AndReturn(
|
||||
fakes.FakeHTTPResponse(
|
||||
200, 'OK',
|
||||
{'content-type': 'application/json'},
|
||||
'{}'))
|
||||
# Replay, create client, assert
|
||||
self.m.ReplayAll()
|
||||
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)
|
||||
|
||||
def test_http_json_request_w_req_body(self):
|
||||
# Record a 200
|
||||
mock_conn = http.requests.request(
|
||||
'GET', 'http://example.com:8004',
|
||||
body='test-body',
|
||||
allow_redirects=False,
|
||||
headers={'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'User-Agent': 'python-heatclient'})
|
||||
mock_conn.AndReturn(
|
||||
fakes.FakeHTTPResponse(
|
||||
200, 'OK',
|
||||
{'content-type': 'application/json'},
|
||||
'{}'))
|
||||
# Replay, create client, assert
|
||||
self.m.ReplayAll()
|
||||
client = http.HTTPClient('http://example.com:8004')
|
||||
resp, body = client.json_request('GET', '', body='test-body')
|
||||
self.assertEqual(200, resp.status_code)
|
||||
self.assertEqual({}, body)
|
||||
|
||||
def test_http_json_request_non_json_resp_cont_type(self):
|
||||
# Record a 200
|
||||
mock_conn = http.requests.request(
|
||||
'GET', 'http://example.com:8004', body='test-body',
|
||||
allow_redirects=False,
|
||||
headers={'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'User-Agent': 'python-heatclient'})
|
||||
mock_conn.AndReturn(
|
||||
fakes.FakeHTTPResponse(
|
||||
200, 'OK',
|
||||
{'content-type': 'not/json'},
|
||||
{}))
|
||||
# Replay, create client, assert
|
||||
self.m.ReplayAll()
|
||||
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)
|
||||
|
||||
def test_http_json_request_invalid_json(self):
|
||||
# Record a 200
|
||||
mock_conn = http.requests.request(
|
||||
'GET', 'http://example.com:8004',
|
||||
allow_redirects=False,
|
||||
headers={'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'User-Agent': 'python-heatclient'})
|
||||
mock_conn.AndReturn(
|
||||
fakes.FakeHTTPResponse(
|
||||
200, 'OK',
|
||||
{'content-type': 'application/json'},
|
||||
'invalid-json'))
|
||||
# Replay, create client, assert
|
||||
self.m.ReplayAll()
|
||||
client = http.HTTPClient('http://example.com:8004')
|
||||
resp, body = client.json_request('GET', '')
|
||||
self.assertEqual(200, resp.status_code)
|
||||
self.assertEqual('invalid-json', body)
|
||||
|
||||
def test_http_manual_redirect_delete(self):
|
||||
mock_conn = http.requests.request(
|
||||
'DELETE', 'http://example.com:8004/foo',
|
||||
allow_redirects=False,
|
||||
headers={'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'User-Agent': 'python-heatclient'})
|
||||
mock_conn.AndReturn(
|
||||
fakes.FakeHTTPResponse(
|
||||
302, 'Found',
|
||||
{'location': 'http://example.com:8004/foo/bar'},
|
||||
''))
|
||||
mock_conn = http.requests.request(
|
||||
'DELETE', 'http://example.com:8004/foo/bar',
|
||||
allow_redirects=False,
|
||||
headers={'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'User-Agent': 'python-heatclient'})
|
||||
mock_conn.AndReturn(
|
||||
fakes.FakeHTTPResponse(
|
||||
200, 'OK',
|
||||
{'content-type': 'application/json'},
|
||||
'{}'))
|
||||
|
||||
self.m.ReplayAll()
|
||||
|
||||
client = http.HTTPClient('http://example.com:8004/foo')
|
||||
resp, body = client.json_request('DELETE', '')
|
||||
|
||||
self.assertEqual(200, resp.status_code)
|
||||
|
||||
def test_http_manual_redirect_post(self):
|
||||
mock_conn = http.requests.request(
|
||||
'POST', 'http://example.com:8004/foo',
|
||||
allow_redirects=False,
|
||||
headers={'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'User-Agent': 'python-heatclient'})
|
||||
mock_conn.AndReturn(
|
||||
fakes.FakeHTTPResponse(
|
||||
302, 'Found',
|
||||
{'location': 'http://example.com:8004/foo/bar'},
|
||||
''))
|
||||
mock_conn = http.requests.request(
|
||||
'POST', 'http://example.com:8004/foo/bar',
|
||||
allow_redirects=False,
|
||||
headers={'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'User-Agent': 'python-heatclient'})
|
||||
mock_conn.AndReturn(
|
||||
fakes.FakeHTTPResponse(
|
||||
200, 'OK',
|
||||
{'content-type': 'application/json'},
|
||||
'{}'))
|
||||
|
||||
self.m.ReplayAll()
|
||||
|
||||
client = http.HTTPClient('http://example.com:8004/foo')
|
||||
resp, body = client.json_request('POST', '')
|
||||
|
||||
self.assertEqual(200, resp.status_code)
|
||||
|
||||
def test_http_manual_redirect_put(self):
|
||||
mock_conn = http.requests.request(
|
||||
'PUT', 'http://example.com:8004/foo',
|
||||
allow_redirects=False,
|
||||
headers={'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'User-Agent': 'python-heatclient'})
|
||||
mock_conn.AndReturn(
|
||||
fakes.FakeHTTPResponse(
|
||||
302, 'Found',
|
||||
{'location': 'http://example.com:8004/foo/bar'},
|
||||
''))
|
||||
mock_conn = http.requests.request(
|
||||
'PUT', 'http://example.com:8004/foo/bar',
|
||||
allow_redirects=False,
|
||||
headers={'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'User-Agent': 'python-heatclient'})
|
||||
mock_conn.AndReturn(
|
||||
fakes.FakeHTTPResponse(
|
||||
200, 'OK',
|
||||
{'content-type': 'application/json'},
|
||||
'{}'))
|
||||
|
||||
self.m.ReplayAll()
|
||||
|
||||
client = http.HTTPClient('http://example.com:8004/foo')
|
||||
resp, body = client.json_request('PUT', '')
|
||||
|
||||
self.assertEqual(200, resp.status_code)
|
||||
|
||||
def test_http_manual_redirect_put_uppercase(self):
|
||||
mock_conn = http.requests.request(
|
||||
'PUT', 'http://EXAMPLE.com:8004/foo',
|
||||
allow_redirects=False,
|
||||
headers={'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'User-Agent': 'python-heatclient'})
|
||||
mock_conn.AndReturn(
|
||||
fakes.FakeHTTPResponse(
|
||||
302, 'Found',
|
||||
{'location': 'http://example.com:8004/foo/bar'},
|
||||
''))
|
||||
mock_conn = http.requests.request(
|
||||
'PUT', 'http://EXAMPLE.com:8004/foo/bar',
|
||||
allow_redirects=False,
|
||||
headers={'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'User-Agent': 'python-heatclient'})
|
||||
mock_conn.AndReturn(
|
||||
fakes.FakeHTTPResponse(
|
||||
200, 'OK',
|
||||
{'content-type': 'application/json'},
|
||||
'{}'))
|
||||
|
||||
self.m.ReplayAll()
|
||||
|
||||
client = http.HTTPClient('http://EXAMPLE.com:8004/foo')
|
||||
resp, body = client.json_request('PUT', '')
|
||||
|
||||
self.assertEqual(200, resp.status_code)
|
||||
|
||||
def test_http_manual_redirect_prohibited(self):
|
||||
mock_conn = http.requests.request(
|
||||
'DELETE', 'http://example.com:8004/foo',
|
||||
allow_redirects=False,
|
||||
headers={'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'User-Agent': 'python-heatclient'})
|
||||
mock_conn.AndReturn(
|
||||
fakes.FakeHTTPResponse(
|
||||
302, 'Found',
|
||||
{'location': 'http://example.com:8004/'},
|
||||
''))
|
||||
self.m.ReplayAll()
|
||||
client = http.HTTPClient('http://example.com:8004/foo')
|
||||
self.assertRaises(exc.InvalidEndpoint,
|
||||
client.json_request, 'DELETE', '')
|
||||
|
||||
def test_http_manual_redirect_error_without_location(self):
|
||||
mock_conn = http.requests.request(
|
||||
'DELETE', 'http://example.com:8004/foo',
|
||||
allow_redirects=False,
|
||||
headers={'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'User-Agent': 'python-heatclient'})
|
||||
mock_conn.AndReturn(
|
||||
fakes.FakeHTTPResponse(
|
||||
302, 'Found',
|
||||
{},
|
||||
''))
|
||||
self.m.ReplayAll()
|
||||
client = http.HTTPClient('http://example.com:8004/foo')
|
||||
self.assertRaises(exc.InvalidEndpoint,
|
||||
client.json_request, 'DELETE', '')
|
||||
|
||||
def test_http_json_request_redirect(self):
|
||||
# Record the 302
|
||||
mock_conn = http.requests.request(
|
||||
'GET', 'http://example.com:8004',
|
||||
allow_redirects=False,
|
||||
headers={'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'User-Agent': 'python-heatclient'})
|
||||
mock_conn.AndReturn(
|
||||
fakes.FakeHTTPResponse(
|
||||
302, 'Found',
|
||||
{'location': 'http://example.com:8004'},
|
||||
''))
|
||||
# Record the following 200
|
||||
mock_conn = http.requests.request(
|
||||
'GET', 'http://example.com:8004',
|
||||
allow_redirects=False,
|
||||
headers={'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'User-Agent': 'python-heatclient'})
|
||||
mock_conn.AndReturn(
|
||||
fakes.FakeHTTPResponse(
|
||||
200, 'OK',
|
||||
{'content-type': 'application/json'},
|
||||
'{}'))
|
||||
# Replay, create client, assert
|
||||
self.m.ReplayAll()
|
||||
client = http.HTTPClient('http://example.com:8004')
|
||||
resp, body = client.json_request('GET', '')
|
||||
self.assertEqual(200, resp.status_code)
|
||||
self.assertEqual({}, body)
|
||||
|
||||
def test_http_404_json_request(self):
|
||||
# Record a 404
|
||||
mock_conn = http.requests.request(
|
||||
'GET', 'http://example.com:8004',
|
||||
allow_redirects=False,
|
||||
headers={'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'User-Agent': 'python-heatclient'})
|
||||
mock_conn.AndReturn(
|
||||
fakes.FakeHTTPResponse(
|
||||
404, 'OK', {'content-type': 'application/json'},
|
||||
'{}'))
|
||||
# Replay, create client, assert
|
||||
self.m.ReplayAll()
|
||||
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))
|
||||
|
||||
def test_http_300_json_request(self):
|
||||
# Record a 300
|
||||
mock_conn = http.requests.request(
|
||||
'GET', 'http://example.com:8004',
|
||||
allow_redirects=False,
|
||||
headers={'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'User-Agent': 'python-heatclient'})
|
||||
mock_conn.AndReturn(
|
||||
fakes.FakeHTTPResponse(
|
||||
300, 'OK', {'content-type': 'application/json'},
|
||||
'{}'))
|
||||
# Replay, create client, assert
|
||||
self.m.ReplayAll()
|
||||
client = http.HTTPClient('http://example.com:8004')
|
||||
e = self.assertRaises(
|
||||
exc.HTTPMultipleChoices, client.json_request, 'GET', '')
|
||||
# Assert that the raised exception can be converted to string
|
||||
self.assertIsNotNone(str(e))
|
||||
|
||||
def test_fake_json_request(self):
|
||||
headers = {'User-Agent': 'python-heatclient'}
|
||||
mock_conn = http.requests.request('GET', 'fake://example.com:8004/',
|
||||
allow_redirects=False,
|
||||
headers=headers)
|
||||
mock_conn.AndRaise(socket.gaierror)
|
||||
self.m.ReplayAll()
|
||||
|
||||
client = http.HTTPClient('fake://example.com:8004')
|
||||
self.assertRaises(exc.InvalidEndpoint,
|
||||
client._http_request, "/", "GET")
|
||||
|
||||
def test_debug_curl_command(self):
|
||||
self.m.StubOutWithMock(logging.Logger, '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 = logging.Logger.debug(
|
||||
"curl -g -i -X GET -H 'key: value' --key TEST_KEY "
|
||||
"--cert TEST_CERT --cacert TEST_CA "
|
||||
"-k -d 'text' http://foo/bar"
|
||||
)
|
||||
mock_logging_debug.AndReturn(None)
|
||||
self.m.ReplayAll()
|
||||
|
||||
client = http.HTTPClient('http://foo')
|
||||
client.ssl_connection_params = ssl_connection_params
|
||||
client.log_curl_request('GET', '/bar', {'headers': headers,
|
||||
'data': 'text'})
|
||||
|
||||
def test_http_request_socket_error(self):
|
||||
headers = {'User-Agent': 'python-heatclient'}
|
||||
mock_conn = http.requests.request('GET', 'http://example.com:8004/',
|
||||
allow_redirects=False,
|
||||
headers=headers)
|
||||
mock_conn.AndRaise(socket.error)
|
||||
self.m.ReplayAll()
|
||||
|
||||
client = http.HTTPClient('http://example.com:8004')
|
||||
self.assertRaises(exc.CommunicationError,
|
||||
client._http_request, "/", "GET")
|
||||
|
||||
def test_http_request_socket_timeout(self):
|
||||
headers = {'User-Agent': 'python-heatclient'}
|
||||
mock_conn = http.requests.request('GET', 'http://example.com:8004/',
|
||||
allow_redirects=False,
|
||||
headers=headers)
|
||||
mock_conn.AndRaise(socket.timeout)
|
||||
self.m.ReplayAll()
|
||||
|
||||
client = http.HTTPClient('http://example.com:8004')
|
||||
self.assertRaises(exc.CommunicationError,
|
||||
client._http_request, "/", "GET")
|
||||
|
||||
def test_http_request_specify_timeout(self):
|
||||
mock_conn = http.requests.request(
|
||||
'GET', 'http://example.com:8004',
|
||||
allow_redirects=False,
|
||||
headers={'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'User-Agent': 'python-heatclient'},
|
||||
timeout=float(123))
|
||||
mock_conn.AndReturn(
|
||||
fakes.FakeHTTPResponse(
|
||||
200, 'OK',
|
||||
{'content-type': 'application/json'},
|
||||
'{}'))
|
||||
# Replay, create client, assert
|
||||
self.m.ReplayAll()
|
||||
client = http.HTTPClient('http://example.com:8004', timeout='123')
|
||||
resp, body = client.json_request('GET', '')
|
||||
self.assertEqual(200, resp.status_code)
|
||||
self.assertEqual({}, body)
|
||||
|
||||
def test_get_system_ca_file(self):
|
||||
chosen = '/etc/ssl/certs/ca-certificates.crt'
|
||||
self.m.StubOutWithMock(os.path, 'exists')
|
||||
os.path.exists(chosen).AndReturn(chosen)
|
||||
self.m.ReplayAll()
|
||||
|
||||
ca = http.get_system_ca_file()
|
||||
self.assertEqual(chosen, ca)
|
||||
|
||||
def test_insecure_verify_cert_None(self):
|
||||
client = http.HTTPClient('https://foo', insecure=True)
|
||||
self.assertFalse(client.verify_cert)
|
||||
|
||||
def test_passed_cert_to_verify_cert(self):
|
||||
client = http.HTTPClient('https://foo', ca_file="NOWHERE")
|
||||
self.assertEqual("NOWHERE", client.verify_cert)
|
||||
|
||||
self.m.StubOutWithMock(http, 'get_system_ca_file')
|
||||
http.get_system_ca_file().AndReturn("SOMEWHERE")
|
||||
self.m.ReplayAll()
|
||||
client = http.HTTPClient('https://foo')
|
||||
self.assertEqual("SOMEWHERE", client.verify_cert)
|
||||
|
||||
def test_curl_log_i18n_headers(self):
|
||||
self.m.StubOutWithMock(logging.Logger, 'debug')
|
||||
kwargs = {'headers': {'Key': b'foo\xe3\x8a\x8e'}}
|
||||
|
||||
mock_logging_debug = logging.Logger.debug(
|
||||
u"curl -g -i -X GET -H 'Key: foo㊎' http://somewhere"
|
||||
)
|
||||
mock_logging_debug.AndReturn(None)
|
||||
|
||||
self.m.ReplayAll()
|
||||
|
||||
client = http.HTTPClient('http://somewhere')
|
||||
client.log_curl_request("GET", '', kwargs=kwargs)
|
||||
|
||||
|
||||
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_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/',
|
||||
'json': 'some_data',
|
||||
'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())
|
326
stacktaskclient/tests/unit/test_deployment_utils.py
Normal file
326
stacktaskclient/tests/unit/test_deployment_utils.py
Normal file
@ -0,0 +1,326 @@
|
||||
# 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 heatclient.common import deployment_utils
|
||||
from heatclient import exc
|
||||
from testtools import matchers
|
||||
|
||||
|
||||
load_tests = testscenarios.load_tests_apply_scenarios
|
||||
|
||||
|
||||
def mock_sc(group=None, config=None, options=None,
|
||||
inputs=None, outputs=None):
|
||||
return {
|
||||
'group': group,
|
||||
'config': config,
|
||||
'options': options or {},
|
||||
'inputs': inputs or [],
|
||||
'outputs': outputs or [],
|
||||
}
|
||||
|
||||
|
||||
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': []})),
|
||||
('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)
|
80
stacktaskclient/tests/unit/test_environment_format.py
Normal file
80
stacktaskclient/tests/unit/test_environment_format.py
Normal file
@ -0,0 +1,80 @@
|
||||
# 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.common import environment_format
|
||||
|
||||
import mock
|
||||
import testscenarios
|
||||
import testtools
|
||||
import yaml
|
||||
|
||||
|
||||
load_tests = testscenarios.load_tests_apply_scenarios
|
||||
|
||||
|
||||
class YamlEnvironmentTest(testtools.TestCase):
|
||||
|
||||
def test_minimal_yaml(self):
|
||||
yaml1 = ''
|
||||
yaml2 = '''
|
||||
parameter_defaults: {}
|
||||
parameters: {}
|
||||
resource_registry: {}
|
||||
'''
|
||||
tpl1 = environment_format.parse(yaml1)
|
||||
environment_format.default_for_missing(tpl1)
|
||||
tpl2 = environment_format.parse(yaml2)
|
||||
self.assertEqual(tpl2, tpl1)
|
||||
|
||||
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)
|
130
stacktaskclient/tests/unit/test_event_utils.py
Normal file
130
stacktaskclient/tests/unit/test_event_utils.py
Normal file
@ -0,0 +1,130 @@
|
||||
# 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 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):
|
||||
ev_info = {"links": [{"href": "http://heat/foo", "rel": "self"}],
|
||||
"logical_resource_id": resource_id,
|
||||
"physical_resource_id": resource_id,
|
||||
"resource_status": "CREATE_COMPLETE",
|
||||
"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)
|
153
stacktaskclient/tests/unit/test_events.py
Normal file
153
stacktaskclient/tests/unit/test_events.py
Normal file
@ -0,0 +1,153 @@
|
||||
# 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.
|
||||
|
||||
from heatclient.common import utils
|
||||
from heatclient.v1 import events
|
||||
|
||||
import mock
|
||||
from mox3 import mox
|
||||
import testtools
|
||||
|
||||
|
||||
class EventManagerTest(testtools.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(EventManagerTest, self).setUp()
|
||||
self.m = mox.Mox()
|
||||
self.addCleanup(self.m.VerifyAll)
|
||||
self.addCleanup(self.m.UnsetStubs)
|
||||
|
||||
def test_list_event(self):
|
||||
stack_id = 'teststack',
|
||||
resource_name = 'testresource'
|
||||
manager = events.EventManager(None)
|
||||
self.m.StubOutWithMock(manager, '_resolve_stack_id')
|
||||
manager._resolve_stack_id(stack_id).AndReturn('teststack/abcd1234')
|
||||
self.m.ReplayAll()
|
||||
manager._list = mock.MagicMock()
|
||||
manager.list(stack_id, resource_name)
|
||||
# Make sure url is correct.
|
||||
manager._list.assert_called_once_with('/stacks/teststack%2Fabcd1234/'
|
||||
'resources/testresource/events',
|
||||
"events")
|
||||
|
||||
def test_list_event_with_unicode_resource_name(self):
|
||||
stack_id = 'teststack',
|
||||
resource_name = u'\u5de5\u4f5c'
|
||||
manager = events.EventManager(None)
|
||||
self.m.StubOutWithMock(manager, '_resolve_stack_id')
|
||||
manager._resolve_stack_id(stack_id).AndReturn('teststack/abcd1234')
|
||||
self.m.ReplayAll()
|
||||
manager._list = mock.MagicMock()
|
||||
manager.list(stack_id, resource_name)
|
||||
# Make sure url is correct.
|
||||
manager._list.assert_called_once_with('/stacks/teststack%2Fabcd1234/'
|
||||
'resources/%E5%B7%A5%E4%BD%9C/'
|
||||
'events', "events")
|
||||
|
||||
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)
|
||||
self.m.StubOutWithMock(manager, '_resolve_stack_id')
|
||||
manager._resolve_stack_id(stack_id).AndReturn('teststack/abcd1234')
|
||||
self.m.ReplayAll()
|
||||
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%2Fabcd1234/'
|
||||
'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)
|
||||
|
||||
def test_get_event(self):
|
||||
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%2Fabcd1234/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'):
|
||||
self.m.StubOutWithMock(manager, '_resolve_stack_id')
|
||||
self.m.StubOutWithMock(utils, 'get_response_body')
|
||||
utils.get_response_body(mox.IgnoreArg()).AndReturn({'event': []})
|
||||
manager._resolve_stack_id('teststack').AndReturn(
|
||||
'teststack/abcd1234')
|
||||
self.m.ReplayAll()
|
||||
manager.get(**fields)
|
||||
|
||||
def test_get_event_with_unicode_resource_name(self):
|
||||
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%2Fabcd1234/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'):
|
||||
self.m.StubOutWithMock(manager, '_resolve_stack_id')
|
||||
self.m.StubOutWithMock(utils, 'get_response_body')
|
||||
utils.get_response_body(mox.IgnoreArg()).AndReturn({'event': []})
|
||||
manager._resolve_stack_id('teststack').AndReturn(
|
||||
'teststack/abcd1234')
|
||||
self.m.ReplayAll()
|
||||
manager.get(**fields)
|
91
stacktaskclient/tests/unit/test_resource_types.py
Normal file
91
stacktaskclient/tests/unit/test_resource_types.py
Normal file
@ -0,0 +1,91 @@
|
||||
#
|
||||
# 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()
|
||||
|
||||
@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)
|
210
stacktaskclient/tests/unit/test_resources.py
Normal file
210
stacktaskclient/tests/unit/test_resources.py
Normal file
@ -0,0 +1,210 @@
|
||||
# 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.
|
||||
from heatclient.common import utils
|
||||
|
||||
from heatclient.v1 import resources
|
||||
from six.moves.urllib import parse
|
||||
|
||||
from mox3 import mox
|
||||
import testtools
|
||||
|
||||
|
||||
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%2Fabcd1234/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%2Fabcd1234/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%2Fabcd1234/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_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%2Fabcd1234/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%2Fabcd1234/resources'
|
||||
'/testresource/signal')
|
||||
key = 'signal'
|
||||
|
||||
manager = self._base_test(expect, key)
|
||||
manager.signal(**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)
|
59
stacktaskclient/tests/unit/test_service.py
Normal file
59
stacktaskclient/tests/unit/test_service.py
Normal file
@ -0,0 +1,59 @@
|
||||
# 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.
|
||||
|
||||
from heatclient import exc
|
||||
import testtools
|
||||
|
||||
|
||||
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)
|
4581
stacktaskclient/tests/unit/test_shell.py
Normal file
4581
stacktaskclient/tests/unit/test_shell.py
Normal file
File diff suppressed because it is too large
Load Diff
99
stacktaskclient/tests/unit/test_software_configs.py
Normal file
99
stacktaskclient/tests/unit/test_software_configs.py
Normal file
@ -0,0 +1,99 @@
|
||||
# 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())
|
||||
|
||||
@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)
|
166
stacktaskclient/tests/unit/test_software_deployments.py
Normal file
166
stacktaskclient/tests/unit/test_software_deployments.py
Normal file
@ -0,0 +1,166 @@
|
||||
# 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)
|
316
stacktaskclient/tests/unit/test_stacks.py
Normal file
316
stacktaskclient/tests/unit/test_stacks.py
Normal file
@ -0,0 +1,316 @@
|
||||
# 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.v1 import stacks
|
||||
|
||||
import mock
|
||||
import testscenarios
|
||||
from testscenarios import scenarios as scnrs
|
||||
import testtools
|
||||
|
||||
load_tests = testscenarios.load_tests_apply_scenarios
|
||||
|
||||
|
||||
def mock_stack(manager, stack_name, stack_id):
|
||||
return stacks.Stack(manager, {
|
||||
"id": stack_id,
|
||||
"stack_name": stack_name,
|
||||
"links": [{
|
||||
"href": "http://192.0.2.1:8004/v1/1234/stacks/%s/%s" % (
|
||||
stack_name, stack_id),
|
||||
"rel": "self"}],
|
||||
"description": "No description",
|
||||
"stack_status_reason": "Stack create completed successfully",
|
||||
"creation_time": "2013-08-04T20:57:55Z",
|
||||
"updated_time": "2013-08-04T20:57:55Z",
|
||||
"stack_status": "CREATE_COMPLETE"
|
||||
})
|
||||
|
||||
|
||||
class StackStatusActionTest(testtools.TestCase):
|
||||
|
||||
scenarios = scnrs.multiply_scenarios([
|
||||
('CREATE', dict(action='CREATE')),
|
||||
('DELETE', dict(action='DELETE')),
|
||||
('UPDATE', dict(action='UPDATE')),
|
||||
('ROLLBACK', dict(action='ROLLBACK')),
|
||||
('SUSPEND', dict(action='SUSPEND')),
|
||||
('RESUME', dict(action='RESUME')),
|
||||
('CHECK', dict(action='CHECK'))
|
||||
], [
|
||||
('IN_PROGRESS', dict(status='IN_PROGRESS')),
|
||||
('FAILED', dict(status='FAILED')),
|
||||
('COMPLETE', dict(status='COMPLETE'))
|
||||
])
|
||||
|
||||
def test_status_action(self):
|
||||
stack_status = '%s_%s' % (self.action, self.status)
|
||||
stack = mock_stack(None, 'stack_1', 'abcd1234')
|
||||
stack.stack_status = stack_status
|
||||
self.assertEqual(self.action, stack.action)
|
||||
self.assertEqual(self.status, stack.status)
|
||||
|
||||
|
||||
class StackIdentifierTest(testtools.TestCase):
|
||||
|
||||
def test_stack_identifier(self):
|
||||
stack = mock_stack(None, 'the_stack', 'abcd1234')
|
||||
self.assertEqual('the_stack/abcd1234', stack.identifier)
|
||||
|
||||
|
||||
class StackOperationsTest(testtools.TestCase):
|
||||
|
||||
def test_delete_stack(self):
|
||||
manager = mock.MagicMock()
|
||||
stack = mock_stack(manager, 'the_stack', 'abcd1234')
|
||||
stack.delete()
|
||||
manager.delete.assert_called_once_with('the_stack/abcd1234')
|
||||
|
||||
def test_abandon_stack(self):
|
||||
manager = mock.MagicMock()
|
||||
stack = mock_stack(manager, 'the_stack', 'abcd1234')
|
||||
stack.abandon()
|
||||
manager.abandon.assert_called_once_with('the_stack/abcd1234')
|
||||
|
||||
def test_get_stack(self):
|
||||
manager = mock.MagicMock()
|
||||
stack = mock_stack(manager, 'the_stack', 'abcd1234')
|
||||
stack.get()
|
||||
manager.get.assert_called_once_with('the_stack/abcd1234')
|
||||
|
||||
def test_update_stack(self):
|
||||
manager = mock.MagicMock()
|
||||
stack = mock_stack(manager, 'the_stack', 'abcd1234')
|
||||
stack.update()
|
||||
manager.update.assert_called_once_with('the_stack/abcd1234')
|
||||
|
||||
def test_create_stack(self):
|
||||
manager = mock.MagicMock()
|
||||
stack = mock_stack(manager, 'the_stack', 'abcd1234')
|
||||
stack = stack.create()
|
||||
manager.create.assert_called_once_with('the_stack/abcd1234')
|
||||
|
||||
def test_preview_stack(self):
|
||||
manager = mock.MagicMock()
|
||||
stack = mock_stack(manager, 'the_stack', 'abcd1234')
|
||||
stack = stack.preview()
|
||||
manager.preview.assert_called_once_with()
|
||||
|
||||
def test_snapshot(self):
|
||||
manager = mock.MagicMock()
|
||||
stack = mock_stack(manager, 'the_stack', 'abcd1234')
|
||||
stack.snapshot('foo')
|
||||
manager.snapshot.assert_called_once_with('the_stack/abcd1234', 'foo')
|
||||
|
||||
def test_snapshot_show(self):
|
||||
manager = mock.MagicMock()
|
||||
stack = mock_stack(manager, 'the_stack', 'abcd1234')
|
||||
stack.snapshot_show('snap1234')
|
||||
manager.snapshot_show.assert_called_once_with(
|
||||
'the_stack/abcd1234', 'snap1234')
|
||||
|
||||
def test_snapshot_delete(self):
|
||||
manager = mock.MagicMock()
|
||||
stack = mock_stack(manager, 'the_stack', 'abcd1234')
|
||||
stack.snapshot_delete('snap1234')
|
||||
manager.snapshot_delete.assert_called_once_with(
|
||||
'the_stack/abcd1234', 'snap1234')
|
||||
|
||||
def test_restore(self):
|
||||
manager = mock.MagicMock()
|
||||
stack = mock_stack(manager, 'the_stack', 'abcd1234')
|
||||
stack.restore('snap1234')
|
||||
manager.restore.assert_called_once_with(
|
||||
'the_stack/abcd1234', 'snap1234')
|
||||
|
||||
def test_snapshot_list(self):
|
||||
manager = mock.MagicMock()
|
||||
stack = mock_stack(manager, 'the_stack', 'abcd1234')
|
||||
stack.snapshot_list()
|
||||
manager.snapshot_list.assert_called_once_with('the_stack/abcd1234')
|
||||
|
||||
|
||||
class StackManagerNoPaginationTest(testtools.TestCase):
|
||||
|
||||
scenarios = [
|
||||
('total_0', dict(total=0)),
|
||||
('total_1', dict(total=1)),
|
||||
('total_9', dict(total=9)),
|
||||
('total_10', dict(total=10)),
|
||||
('total_11', dict(total=11)),
|
||||
('total_19', dict(total=19)),
|
||||
('total_20', dict(total=20)),
|
||||
('total_21', dict(total=21)),
|
||||
('total_49', dict(total=49)),
|
||||
('total_50', dict(total=50)),
|
||||
('total_51', dict(total=51)),
|
||||
('total_95', dict(total=95)),
|
||||
]
|
||||
|
||||
# absolute limit for results returned
|
||||
limit = 50
|
||||
|
||||
def mock_manager(self):
|
||||
manager = stacks.StackManager(None)
|
||||
manager._list = mock.MagicMock()
|
||||
|
||||
def mock_list(*args, **kwargs):
|
||||
def results():
|
||||
for i in range(0, self.total):
|
||||
stack_name = 'stack_%s' % (i + 1)
|
||||
stack_id = 'abcd1234-%s' % (i + 1)
|
||||
yield mock_stack(manager, stack_name, stack_id)
|
||||
|
||||
return list(results())
|
||||
|
||||
manager._list.side_effect = mock_list
|
||||
return manager
|
||||
|
||||
def test_stack_list_no_pagination(self):
|
||||
manager = self.mock_manager()
|
||||
|
||||
results = list(manager.list())
|
||||
manager._list.assert_called_once_with(
|
||||
'/stacks?', 'stacks')
|
||||
|
||||
# paginate is not specified, so the total
|
||||
# results is always returned
|
||||
self.assertEqual(self.total, len(results))
|
||||
|
||||
if self.total > 0:
|
||||
self.assertEqual('stack_1', results[0].stack_name)
|
||||
self.assertEqual('stack_%s' % self.total, results[-1].stack_name)
|
||||
|
||||
|
||||
class StackManagerPaginationTest(testtools.TestCase):
|
||||
|
||||
scenarios = [
|
||||
('0_offset_0', dict(
|
||||
offset=0,
|
||||
total=0,
|
||||
results=((0, 0),)
|
||||
)),
|
||||
('1_offset_0', dict(
|
||||
offset=0,
|
||||
total=1,
|
||||
results=((0, 1),)
|
||||
)),
|
||||
('9_offset_0', dict(
|
||||
offset=0,
|
||||
total=9,
|
||||
results=((0, 9),)
|
||||
)),
|
||||
('10_offset_0', dict(
|
||||
offset=0,
|
||||
total=10,
|
||||
results=((0, 10), (10, 10))
|
||||
)),
|
||||
('11_offset_0', dict(
|
||||
offset=0,
|
||||
total=11,
|
||||
results=((0, 10), (10, 11))
|
||||
)),
|
||||
('11_offset_10', dict(
|
||||
offset=10,
|
||||
total=11,
|
||||
results=((10, 11),)
|
||||
)),
|
||||
('19_offset_10', dict(
|
||||
offset=10,
|
||||
total=19,
|
||||
results=((10, 19),)
|
||||
)),
|
||||
('20_offset_10', dict(
|
||||
offset=10,
|
||||
total=20,
|
||||
results=((10, 20), (20, 20))
|
||||
)),
|
||||
('21_offset_10', dict(
|
||||
offset=10,
|
||||
total=21,
|
||||
results=((10, 20), (20, 21))
|
||||
)),
|
||||
('21_offset_0', dict(
|
||||
offset=0,
|
||||
total=21,
|
||||
results=((0, 10), (10, 20), (20, 21))
|
||||
)),
|
||||
('21_offset_20', dict(
|
||||
offset=20,
|
||||
total=21,
|
||||
results=((20, 21),)
|
||||
)),
|
||||
('95_offset_90', dict(
|
||||
offset=90,
|
||||
total=95,
|
||||
results=((90, 95),)
|
||||
)),
|
||||
]
|
||||
|
||||
# absolute limit for results returned
|
||||
limit = 50
|
||||
|
||||
def mock_manager(self):
|
||||
manager = stacks.StackManager(None)
|
||||
manager._list = mock.MagicMock()
|
||||
|
||||
def mock_list(arg_url, arg_response_key):
|
||||
try:
|
||||
result = self.results[self.result_index]
|
||||
except IndexError:
|
||||
return []
|
||||
self.result_index = self.result_index + 1
|
||||
|
||||
limit_string = 'limit=%s' % self.limit
|
||||
self.assertIn(limit_string, arg_url)
|
||||
|
||||
offset = result[0]
|
||||
if offset > 0:
|
||||
offset_string = 'marker=abcd1234-%s' % offset
|
||||
self.assertIn(offset_string, arg_url)
|
||||
|
||||
def results():
|
||||
|
||||
for i in range(*result):
|
||||
self.limit -= 1
|
||||
stack_name = 'stack_%s' % (i + 1)
|
||||
stack_id = 'abcd1234-%s' % (i + 1)
|
||||
yield mock_stack(manager, stack_name, stack_id)
|
||||
|
||||
return list(results())
|
||||
|
||||
manager._list.side_effect = mock_list
|
||||
return manager
|
||||
|
||||
def test_stack_list_pagination(self):
|
||||
manager = self.mock_manager()
|
||||
|
||||
list_params = {'limit': self.limit}
|
||||
|
||||
if self.offset > 0:
|
||||
marker = 'abcd1234-%s' % self.offset
|
||||
list_params['marker'] = marker
|
||||
|
||||
self.result_index = 0
|
||||
results = list(manager.list(**list_params))
|
||||
|
||||
# assert that the list method has been called enough times
|
||||
self.assertEqual(len(self.results), self.result_index)
|
||||
|
||||
last_result = min(self.limit, self.total - self.offset)
|
||||
# one or more list calls have been recomposed into a single list
|
||||
self.assertEqual(last_result, len(results))
|
||||
|
||||
if last_result > 0:
|
||||
self.assertEqual('stack_%s' % (self.offset + 1),
|
||||
results[0].stack_name)
|
||||
self.assertEqual('stack_%s' % (self.offset + last_result),
|
||||
results[-1].stack_name)
|
50
stacktaskclient/tests/unit/test_template_format.py
Normal file
50
stacktaskclient/tests/unit/test_template_format.py
Normal file
@ -0,0 +1,50 @@
|
||||
# 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 testscenarios
|
||||
import testtools
|
||||
import yaml
|
||||
|
||||
from heatclient.common import template_format
|
||||
|
||||
|
||||
load_tests = testscenarios.load_tests_apply_scenarios
|
||||
|
||||
|
||||
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,
|
||||
template_format.parse, text)
|
||||
|
||||
def test_parse_no_version_format(self):
|
||||
yaml = ''
|
||||
self.assertRaises(ValueError, template_format.parse, yaml)
|
||||
yaml2 = '''Parameters: {}
|
||||
Mappings: {}
|
||||
Resources: {}
|
||||
Outputs: {}
|
||||
'''
|
||||
self.assertRaises(ValueError, template_format.parse, yaml2)
|
1017
stacktaskclient/tests/unit/test_template_utils.py
Normal file
1017
stacktaskclient/tests/unit/test_template_utils.py
Normal file
File diff suppressed because it is too large
Load Diff
57
stacktaskclient/tests/unit/test_template_versions.py
Normal file
57
stacktaskclient/tests/unit/test_template_versions.py
Normal file
@ -0,0 +1,57 @@
|
||||
#
|
||||
# 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.v1 import template_versions
|
||||
|
||||
|
||||
class TemplateVersionManagerTest(testtools.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(TemplateVersionManagerTest, self).setUp()
|
||||
|
||||
def test_list_versions(self):
|
||||
expect = ('GET', '/template_versions')
|
||||
|
||||
class FakeResponse(object):
|
||||
def json(self):
|
||||
return {'template_versions': [{'version': '2013-05-23',
|
||||
'type': 'hot'}]}
|
||||
|
||||
class FakeClient(object):
|
||||
def get(self, *args, **kwargs):
|
||||
assert ('GET', args[0]) == expect
|
||||
return FakeResponse()
|
||||
|
||||
manager = template_versions.TemplateVersionManager(FakeClient())
|
||||
versions = manager.list()
|
||||
self.assertEqual('2013-05-23', getattr(versions[0], 'version'))
|
||||
self.assertEqual('hot', getattr(versions[0], 'type'))
|
||||
|
||||
def test_get(self):
|
||||
expect = ('GET', '/template_versions/heat_template_version.2015-04-30'
|
||||
'/functions')
|
||||
|
||||
class FakeResponse(object):
|
||||
def json(self):
|
||||
return {'template_functions': [{'function': 'get_attr'}]}
|
||||
|
||||
class FakeClient(object):
|
||||
def get(self, *args, **kwargs):
|
||||
assert ('GET', args[0]) == expect
|
||||
return FakeResponse()
|
||||
|
||||
manager = template_versions.TemplateVersionManager(FakeClient())
|
||||
functions = manager.get('heat_template_version.2015-04-30')
|
||||
self.assertEqual('get_attr', getattr(functions[0], 'function'))
|
278
stacktaskclient/tests/unit/test_utils.py
Normal file
278
stacktaskclient/tests/unit/test_utils.py
Normal file
@ -0,0 +1,278 @@
|
||||
# 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.
|
||||
from heatclient.common import utils
|
||||
from heatclient import exc
|
||||
from heatclient.v1 import resources as hc_res
|
||||
import mock
|
||||
import os
|
||||
import testtools
|
||||
|
||||
|
||||
class ShellTest(testtools.TestCase):
|
||||
|
||||
def test_format_parameter_none(self):
|
||||
self.assertEqual({}, utils.format_parameters(None))
|
||||
|
||||
def test_format_parameters(self):
|
||||
p = utils.format_parameters([
|
||||
'InstanceType=m1.large;DBUsername=wp;'
|
||||
'DBPassword=verybadpassword;KeyName=heat_key;'
|
||||
'LinuxDistribution=F17'])
|
||||
self.assertEqual({'InstanceType': 'm1.large',
|
||||
'DBUsername': 'wp',
|
||||
'DBPassword': 'verybadpassword',
|
||||
'KeyName': 'heat_key',
|
||||
'LinuxDistribution': 'F17'
|
||||
}, p)
|
||||
|
||||
def test_format_parameters_split(self):
|
||||
p = utils.format_parameters([
|
||||
'KeyName=heat_key;'
|
||||
'DnsSecKey=hsgx1m31PbamNF4WEcHlwjIlCGgifOdoB58/wwC7a4oAONQ/fDV5ct'
|
||||
'qrYBoLlKHhTfkyQEw9iVScKYZbbMtMNg==;'
|
||||
'UpstreamDNS=8.8.8.8'])
|
||||
self.assertEqual({'KeyName': 'heat_key',
|
||||
'DnsSecKey': 'hsgx1m31PbamNF4WEcHlwjIlCGgifOdoB58/ww'
|
||||
'C7a4oAONQ/fDV5ctqrYBoLlKHhTfkyQEw9iVScKYZbbMtMNg==',
|
||||
'UpstreamDNS': '8.8.8.8'}, p)
|
||||
|
||||
def test_format_parameters_multiple(self):
|
||||
p = utils.format_parameters([
|
||||
'KeyName=heat_key',
|
||||
'DnsSecKey=hsgx1m31PbamNF4WEcHlwjIlCGgifOdoB58/wwC7a4oAONQ/fDV5ct'
|
||||
'qrYBoLlKHhTfkyQEw9iVScKYZbbMtMNg==',
|
||||
'UpstreamDNS=8.8.8.8'])
|
||||
self.assertEqual({'KeyName': 'heat_key',
|
||||
'DnsSecKey': 'hsgx1m31PbamNF4WEcHlwjIlCGgifOdoB58/ww'
|
||||
'C7a4oAONQ/fDV5ctqrYBoLlKHhTfkyQEw9iVScKYZbbMtMNg==',
|
||||
'UpstreamDNS': '8.8.8.8'}, p)
|
||||
|
||||
def test_format_parameters_multiple_semicolon_values(self):
|
||||
p = utils.format_parameters([
|
||||
'KeyName=heat_key',
|
||||
'DnsSecKey=hsgx1m31;PbaNF4WEcHlwj;IlCGgfOdoB;58/ww7a4oAO;NQ/fD==',
|
||||
'UpstreamDNS=8.8.8.8'])
|
||||
self.assertEqual({'KeyName': 'heat_key',
|
||||
'DnsSecKey': 'hsgx1m31;PbaNF4WEcHlwj;IlCGgfOdoB;58/'
|
||||
'ww7a4oAO;NQ/fD==',
|
||||
'UpstreamDNS': '8.8.8.8'}, p)
|
||||
|
||||
def test_format_parameters_parse_semicolon_false(self):
|
||||
p = utils.format_parameters(
|
||||
['KeyName=heat_key;UpstreamDNS=8.8.8.8;a=b'],
|
||||
parse_semicolon=False)
|
||||
self.assertEqual({'KeyName': 'heat_key;UpstreamDNS=8.8.8.8;a=b'}, p)
|
||||
|
||||
def test_format_parameters_multiple_values_per_pamaters(self):
|
||||
p = utils.format_parameters([
|
||||
'status=COMPLETE',
|
||||
'status=FAILED'])
|
||||
self.assertIn('status', p)
|
||||
self.assertIn('COMPLETE', p['status'])
|
||||
self.assertIn('FAILED', p['status'])
|
||||
|
||||
def test_format_parameter_bad_parameter(self):
|
||||
params = ['KeyName=heat_key;UpstreamDNS8.8.8.8']
|
||||
ex = self.assertRaises(exc.CommandError,
|
||||
utils.format_parameters, params)
|
||||
self.assertEqual('Malformed parameter(UpstreamDNS8.8.8.8). '
|
||||
'Use the key=value format.', str(ex))
|
||||
|
||||
def test_format_multiple_bad_parameter(self):
|
||||
params = ['KeyName=heat_key', 'UpstreamDNS8.8.8.8']
|
||||
ex = self.assertRaises(exc.CommandError,
|
||||
utils.format_parameters, params)
|
||||
self.assertEqual('Malformed parameter(UpstreamDNS8.8.8.8). '
|
||||
'Use the key=value format.', str(ex))
|
||||
|
||||
def test_link_formatter(self):
|
||||
self.assertEqual('', utils.link_formatter(None))
|
||||
self.assertEqual('', utils.link_formatter([]))
|
||||
self.assertEqual(
|
||||
'http://foo.example.com\nhttp://bar.example.com',
|
||||
utils.link_formatter([
|
||||
{'href': 'http://foo.example.com'},
|
||||
{'href': 'http://bar.example.com'}]))
|
||||
self.assertEqual(
|
||||
'http://foo.example.com (a)\nhttp://bar.example.com (b)',
|
||||
utils.link_formatter([
|
||||
{'href': 'http://foo.example.com', 'rel': 'a'},
|
||||
{'href': 'http://bar.example.com', 'rel': 'b'}]))
|
||||
self.assertEqual(
|
||||
'\n',
|
||||
utils.link_formatter([
|
||||
{'hrf': 'http://foo.example.com'},
|
||||
{}]))
|
||||
|
||||
def test_resource_nested_identifier(self):
|
||||
rsrc_info = {'resource_name': 'aresource',
|
||||
'links': [{'href': u'http://foo/name/id/resources/0',
|
||||
'rel': u'self'},
|
||||
{'href': u'http://foo/name/id',
|
||||
'rel': u'stack'},
|
||||
{'href': u'http://foo/n_name/n_id',
|
||||
'rel': u'nested'}]}
|
||||
rsrc = hc_res.Resource(manager=None, info=rsrc_info)
|
||||
self.assertEqual('n_name/n_id', utils.resource_nested_identifier(rsrc))
|
||||
|
||||
def test_resource_nested_identifier_none(self):
|
||||
rsrc_info = {'resource_name': 'aresource',
|
||||
'links': [{'href': u'http://foo/name/id/resources/0',
|
||||
'rel': u'self'},
|
||||
{'href': u'http://foo/name/id',
|
||||
'rel': u'stack'}]}
|
||||
rsrc = hc_res.Resource(manager=None, info=rsrc_info)
|
||||
self.assertIsNone(utils.resource_nested_identifier(rsrc))
|
||||
|
||||
def test_json_formatter(self):
|
||||
self.assertEqual('null', utils.json_formatter(None))
|
||||
self.assertEqual('{}', utils.json_formatter({}))
|
||||
self.assertEqual('{\n "foo": "bar"\n}',
|
||||
utils.json_formatter({"foo": "bar"}))
|
||||
self.assertEqual(u'{\n "Uni": "test\u2665"\n}',
|
||||
utils.json_formatter({"Uni": u"test\u2665"}))
|
||||
|
||||
def test_text_wrap_formatter(self):
|
||||
self.assertEqual('', utils.text_wrap_formatter(None))
|
||||
self.assertEqual('', utils.text_wrap_formatter(''))
|
||||
self.assertEqual('one two three',
|
||||
utils.text_wrap_formatter('one two three'))
|
||||
self.assertEqual(
|
||||
'one two three four five six seven eight nine ten eleven\ntwelve',
|
||||
utils.text_wrap_formatter(
|
||||
('one two three four five six seven '
|
||||
'eight nine ten eleven twelve')))
|
||||
|
||||
def test_newline_list_formatter(self):
|
||||
self.assertEqual('', utils.newline_list_formatter(None))
|
||||
self.assertEqual('', utils.newline_list_formatter([]))
|
||||
self.assertEqual('one\ntwo',
|
||||
utils.newline_list_formatter(['one', 'two']))
|
||||
|
||||
|
||||
class ShellTestParameterFiles(testtools.TestCase):
|
||||
|
||||
def test_format_parameter_file_none(self):
|
||||
self.assertEqual({}, utils.format_parameter_file(None))
|
||||
|
||||
def test_format_parameter_file(self):
|
||||
tmpl_file = '/opt/stack/template.yaml'
|
||||
contents = 'DBUsername=wp\nDBPassword=verybadpassword'
|
||||
utils.read_url_content = mock.MagicMock()
|
||||
utils.read_url_content.return_value = 'DBUsername=wp\n' \
|
||||
'DBPassword=verybadpassword'
|
||||
|
||||
p = utils.format_parameter_file([
|
||||
'env_file1=test_file1'], tmpl_file)
|
||||
self.assertEqual({'env_file1': contents
|
||||
}, p)
|
||||
|
||||
def test_format_parameter_file_no_template(self):
|
||||
tmpl_file = None
|
||||
contents = 'DBUsername=wp\nDBPassword=verybadpassword'
|
||||
utils.read_url_content = mock.MagicMock()
|
||||
utils.read_url_content.return_value = 'DBUsername=wp\n' \
|
||||
'DBPassword=verybadpassword'
|
||||
p = utils.format_parameter_file([
|
||||
'env_file1=test_file1'], tmpl_file)
|
||||
self.assertEqual({'env_file1': contents
|
||||
}, p)
|
||||
|
||||
def test_format_all_parameters(self):
|
||||
tmpl_file = '/opt/stack/template.yaml'
|
||||
contents = 'DBUsername=wp\nDBPassword=verybadpassword'
|
||||
params = ['KeyName=heat_key;UpstreamDNS=8.8.8.8']
|
||||
utils.read_url_content = mock.MagicMock()
|
||||
utils.read_url_content.return_value = 'DBUsername=wp\n' \
|
||||
'DBPassword=verybadpassword'
|
||||
p = utils.format_all_parameters(params, [
|
||||
'env_file1=test_file1'], template_file=tmpl_file)
|
||||
self.assertEqual({'KeyName': 'heat_key',
|
||||
'UpstreamDNS': '8.8.8.8',
|
||||
'env_file1': contents}, p)
|
||||
|
||||
|
||||
class TestURLFunctions(testtools.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(TestURLFunctions, self).setUp()
|
||||
self.m = mock.MagicMock()
|
||||
|
||||
self.addCleanup(self.m.UnsetStubs)
|
||||
|
||||
def test_normalise_file_path_to_url_relative(self):
|
||||
self.assertEqual(
|
||||
'file://%s/foo' % os.getcwd(),
|
||||
utils.normalise_file_path_to_url(
|
||||
'foo'))
|
||||
|
||||
def test_normalise_file_path_to_url_absolute(self):
|
||||
self.assertEqual(
|
||||
'file:///tmp/foo',
|
||||
utils.normalise_file_path_to_url(
|
||||
'/tmp/foo'))
|
||||
|
||||
def test_normalise_file_path_to_url_file(self):
|
||||
self.assertEqual(
|
||||
'file:///tmp/foo',
|
||||
utils.normalise_file_path_to_url(
|
||||
'file:///tmp/foo'))
|
||||
|
||||
def test_normalise_file_path_to_url_http(self):
|
||||
self.assertEqual(
|
||||
'http://localhost/foo',
|
||||
utils.normalise_file_path_to_url(
|
||||
'http://localhost/foo'))
|
||||
|
||||
def test_get_template_url(self):
|
||||
tmpl_file = '/opt/stack/template.yaml'
|
||||
tmpl_url = 'file:///opt/stack/template.yaml'
|
||||
self.assertEqual(utils.get_template_url(tmpl_file, None),
|
||||
tmpl_url)
|
||||
self.assertEqual(utils.get_template_url(None, tmpl_url),
|
||||
tmpl_url)
|
||||
self.assertEqual(utils.get_template_url(None, None),
|
||||
None)
|
||||
|
||||
def test_base_url_for_url(self):
|
||||
self.assertEqual(
|
||||
'file:///foo/bar',
|
||||
utils.base_url_for_url(
|
||||
'file:///foo/bar/baz'))
|
||||
self.assertEqual(
|
||||
'file:///foo/bar',
|
||||
utils.base_url_for_url(
|
||||
'file:///foo/bar/baz.txt'))
|
||||
self.assertEqual(
|
||||
'file:///foo/bar',
|
||||
utils.base_url_for_url(
|
||||
'file:///foo/bar/'))
|
||||
self.assertEqual(
|
||||
'file:///',
|
||||
utils.base_url_for_url(
|
||||
'file:///'))
|
||||
self.assertEqual(
|
||||
'file:///',
|
||||
utils.base_url_for_url(
|
||||
'file:///foo'))
|
||||
|
||||
self.assertEqual(
|
||||
'http://foo/bar',
|
||||
utils.base_url_for_url(
|
||||
'http://foo/bar/'))
|
||||
self.assertEqual(
|
||||
'http://foo/bar',
|
||||
utils.base_url_for_url(
|
||||
'http://foo/bar/baz.template'))
|
0
stacktaskclient/tests/unit/v1/__init__.py
Normal file
0
stacktaskclient/tests/unit/v1/__init__.py
Normal file
318
stacktaskclient/tests/unit/v1/test_hooks.py
Normal file
318
stacktaskclient/tests/unit/v1/test_hooks.py
Normal file
@ -0,0 +1,318 @@
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
# implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import mock
|
||||
import testtools
|
||||
|
||||
import heatclient.v1.shell as shell
|
||||
|
||||
|
||||
class TestHooks(testtools.TestCase):
|
||||
def setUp(self):
|
||||
super(TestHooks, self).setUp()
|
||||
self.client = mock.Mock()
|
||||
nested_stack = mock.Mock()
|
||||
self.client.resources.get = mock.Mock(name='thingy',
|
||||
return_value=nested_stack)
|
||||
type(nested_stack).physical_resource_id = mock.PropertyMock(
|
||||
return_value='nested_id')
|
||||
self.args = mock.Mock()
|
||||
stack_name_p = mock.PropertyMock(return_value="mystack")
|
||||
type(self.args).name = stack_name_p
|
||||
type(self.args).id = stack_name_p
|
||||
shell.template_utils.get_template_contents = mock.Mock(
|
||||
return_value=({}, ""))
|
||||
shell.template_utils.process_multiple_environments_and_files = \
|
||||
mock.Mock(return_value=({}, {}))
|
||||
shell.utils.format_all_parameters = mock.Mock(return_value=[])
|
||||
shell.do_stack_list = mock.Mock()
|
||||
shell.logger = mock.Mock()
|
||||
type(self.args).clear_parameter = mock.PropertyMock(return_value=[])
|
||||
type(self.args).rollback = mock.PropertyMock(return_value=None)
|
||||
type(self.args).pre_create = mock.PropertyMock(return_value=False)
|
||||
type(self.args).pre_update = mock.PropertyMock(return_value=False)
|
||||
type(self.args).poll = mock.PropertyMock(return_value=None)
|
||||
|
||||
def test_create_hooks_in_args(self):
|
||||
type(self.args).pre_create = mock.PropertyMock(
|
||||
return_value=['bp', 'another_bp'])
|
||||
|
||||
shell.do_stack_create(self.client, self.args)
|
||||
self.assertEqual(1, self.client.stacks.create.call_count)
|
||||
expected_hooks = {
|
||||
'bp': {'hooks': 'pre-create'},
|
||||
'another_bp': {'hooks': 'pre-create'}
|
||||
}
|
||||
actual_hooks = self.client.stacks.create.call_args[1][
|
||||
'environment']['resource_registry']['resources']
|
||||
self.assertEqual(expected_hooks, actual_hooks)
|
||||
|
||||
def test_create_nested_hooks_in_args(self):
|
||||
type(self.args).pre_create = mock.PropertyMock(
|
||||
return_value=['nested/bp', 'super/nested/bp'])
|
||||
|
||||
shell.do_stack_create(self.client, self.args)
|
||||
self.assertEqual(1, self.client.stacks.create.call_count)
|
||||
expected_hooks = {
|
||||
'nested': {
|
||||
'bp': {'hooks': 'pre-create'},
|
||||
},
|
||||
'super': {
|
||||
'nested': {
|
||||
'bp': {'hooks': 'pre-create'},
|
||||
}
|
||||
}
|
||||
}
|
||||
actual_hooks = self.client.stacks.create.call_args[1][
|
||||
'environment']['resource_registry']['resources']
|
||||
self.assertEqual(expected_hooks, actual_hooks)
|
||||
|
||||
def test_create_hooks_in_env_and_args(self):
|
||||
type(self.args).pre_create = mock.PropertyMock(return_value=[
|
||||
'nested_a/bp',
|
||||
'bp_a',
|
||||
'another_bp_a',
|
||||
'super_a/nested/bp',
|
||||
])
|
||||
env = {
|
||||
'resource_registry': {
|
||||
'resources': {
|
||||
'bp_e': {'hooks': 'pre-create'},
|
||||
'another_bp_e': {'hooks': 'pre-create'},
|
||||
'nested_e': {
|
||||
'bp': {'hooks': 'pre-create'}
|
||||
},
|
||||
'super_e': {
|
||||
'nested': {
|
||||
'bp': {'hooks': 'pre-create'}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
shell.template_utils.process_multiple_environments_and_files = \
|
||||
mock.Mock(return_value=({}, env))
|
||||
|
||||
shell.do_stack_create(self.client, self.args)
|
||||
self.assertEqual(1, self.client.stacks.create.call_count)
|
||||
actual_hooks = self.client.stacks.create.call_args[1][
|
||||
'environment']['resource_registry']['resources']
|
||||
expected_hooks = {
|
||||
'bp_e': {'hooks': 'pre-create'},
|
||||
'another_bp_e': {'hooks': 'pre-create'},
|
||||
'nested_e': {
|
||||
'bp': {'hooks': 'pre-create'}
|
||||
},
|
||||
'super_e': {
|
||||
'nested': {
|
||||
'bp': {'hooks': 'pre-create'}
|
||||
}
|
||||
},
|
||||
'bp_a': {'hooks': 'pre-create'},
|
||||
'another_bp_a': {'hooks': 'pre-create'},
|
||||
'nested_a': {
|
||||
'bp': {'hooks': 'pre-create'}
|
||||
},
|
||||
'super_a': {
|
||||
'nested': {
|
||||
'bp': {'hooks': 'pre-create'}
|
||||
}
|
||||
},
|
||||
}
|
||||
self.assertEqual(expected_hooks, actual_hooks)
|
||||
|
||||
def test_update_hooks_in_args(self):
|
||||
type(self.args).pre_update = mock.PropertyMock(
|
||||
return_value=['bp', 'another_bp'])
|
||||
|
||||
shell.do_stack_update(self.client, self.args)
|
||||
self.assertEqual(1, self.client.stacks.update.call_count)
|
||||
expected_hooks = {
|
||||
'bp': {'hooks': 'pre-update'},
|
||||
'another_bp': {'hooks': 'pre-update'},
|
||||
}
|
||||
actual_hooks = self.client.stacks.update.call_args[1][
|
||||
'environment']['resource_registry']['resources']
|
||||
self.assertEqual(expected_hooks, actual_hooks)
|
||||
|
||||
def test_update_nested_hooks_in_args(self):
|
||||
type(self.args).pre_update = mock.PropertyMock(
|
||||
return_value=['nested/bp', 'super/nested/bp'])
|
||||
|
||||
shell.do_stack_update(self.client, self.args)
|
||||
self.assertEqual(1, self.client.stacks.update.call_count)
|
||||
expected_hooks = {
|
||||
'nested': {
|
||||
'bp': {'hooks': 'pre-update'}
|
||||
},
|
||||
'super': {
|
||||
'nested': {
|
||||
'bp': {'hooks': 'pre-update'}
|
||||
}
|
||||
}
|
||||
}
|
||||
actual_hooks = self.client.stacks.update.call_args[1][
|
||||
'environment']['resource_registry']['resources']
|
||||
self.assertEqual(expected_hooks, actual_hooks)
|
||||
|
||||
def test_update_hooks_in_env_and_args(self):
|
||||
type(self.args).pre_update = mock.PropertyMock(return_value=[
|
||||
'nested_a/bp',
|
||||
'bp_a',
|
||||
'another_bp_a',
|
||||
'super_a/nested/bp',
|
||||
])
|
||||
env = {
|
||||
'resource_registry': {
|
||||
'resources': {
|
||||
'bp_e': {'hooks': 'pre-update'},
|
||||
'another_bp_e': {'hooks': 'pre-update'},
|
||||
'nested_e': {
|
||||
'bp': {'hooks': 'pre-update'}
|
||||
},
|
||||
'super_e': {
|
||||
'nested': {
|
||||
'bp': {'hooks': 'pre-update'}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
shell.template_utils.process_multiple_environments_and_files = \
|
||||
mock.Mock(return_value=({}, env))
|
||||
|
||||
shell.do_stack_update(self.client, self.args)
|
||||
self.assertEqual(1, self.client.stacks.update.call_count)
|
||||
actual_hooks = self.client.stacks.update.call_args[1][
|
||||
'environment']['resource_registry']['resources']
|
||||
expected_hooks = {
|
||||
'bp_e': {'hooks': 'pre-update'},
|
||||
'another_bp_e': {'hooks': 'pre-update'},
|
||||
'nested_e': {
|
||||
'bp': {'hooks': 'pre-update'}
|
||||
},
|
||||
'super_e': {
|
||||
'nested': {
|
||||
'bp': {'hooks': 'pre-update'}
|
||||
}
|
||||
},
|
||||
'bp_a': {'hooks': 'pre-update'},
|
||||
'another_bp_a': {'hooks': 'pre-update'},
|
||||
'nested_a': {
|
||||
'bp': {'hooks': 'pre-update'}
|
||||
},
|
||||
'super_a': {
|
||||
'nested': {
|
||||
'bp': {'hooks': 'pre-update'}
|
||||
}
|
||||
},
|
||||
}
|
||||
self.assertEqual(expected_hooks, actual_hooks)
|
||||
|
||||
def test_clear_all_hooks(self):
|
||||
shell._get_hook_type_via_status =\
|
||||
mock.Mock(return_value='pre-create')
|
||||
type(self.args).hook = mock.PropertyMock(
|
||||
return_value=['bp'])
|
||||
type(self.args).pre_create = mock.PropertyMock(return_value=True)
|
||||
bp = mock.Mock()
|
||||
type(bp).resource_name = 'bp'
|
||||
self.client.resources.list = mock.Mock(return_value=[bp])
|
||||
|
||||
shell.do_hook_clear(self.client, self.args)
|
||||
self.assertEqual(1, self.client.resources.signal.call_count)
|
||||
payload_pre_create = self.client.resources.signal.call_args_list[0][1]
|
||||
self.assertEqual({'unset_hook': 'pre-create'},
|
||||
payload_pre_create['data'])
|
||||
self.assertEqual('bp', payload_pre_create['resource_name'])
|
||||
self.assertEqual('mystack', payload_pre_create['stack_id'])
|
||||
|
||||
def test_clear_pre_create_hooks(self):
|
||||
type(self.args).hook = mock.PropertyMock(
|
||||
return_value=['bp'])
|
||||
type(self.args).pre_create = mock.PropertyMock(return_value=True)
|
||||
bp = mock.Mock()
|
||||
type(bp).resource_name = 'bp'
|
||||
self.client.resources.list = mock.Mock(return_value=[bp])
|
||||
|
||||
shell.do_hook_clear(self.client, self.args)
|
||||
self.assertEqual(1, self.client.resources.signal.call_count)
|
||||
payload = self.client.resources.signal.call_args_list[0][1]
|
||||
self.assertEqual({'unset_hook': 'pre-create'}, payload['data'])
|
||||
self.assertEqual('bp', payload['resource_name'])
|
||||
self.assertEqual('mystack', payload['stack_id'])
|
||||
|
||||
def test_clear_pre_update_hooks(self):
|
||||
type(self.args).hook = mock.PropertyMock(
|
||||
return_value=['bp'])
|
||||
type(self.args).pre_update = mock.PropertyMock(return_value=True)
|
||||
bp = mock.Mock()
|
||||
type(bp).resource_name = 'bp'
|
||||
self.client.resources.list = mock.Mock(return_value=[bp])
|
||||
|
||||
shell.do_hook_clear(self.client, self.args)
|
||||
self.assertEqual(1, self.client.resources.signal.call_count)
|
||||
payload = self.client.resources.signal.call_args_list[0][1]
|
||||
self.assertEqual({'unset_hook': 'pre-update'}, payload['data'])
|
||||
self.assertEqual('bp', payload['resource_name'])
|
||||
self.assertEqual('mystack', payload['stack_id'])
|
||||
|
||||
def test_clear_nested_hook(self):
|
||||
type(self.args).hook = mock.PropertyMock(
|
||||
return_value=['a/b/bp'])
|
||||
type(self.args).pre_create = mock.PropertyMock(return_value=True)
|
||||
|
||||
a = mock.Mock()
|
||||
type(a).resource_name = 'a'
|
||||
b = mock.Mock()
|
||||
type(b).resource_name = 'b'
|
||||
bp = mock.Mock()
|
||||
type(bp).resource_name = 'bp'
|
||||
self.client.resources.list = mock.Mock(
|
||||
side_effect=[[a], [b], [bp]])
|
||||
m1 = mock.Mock()
|
||||
m2 = mock.Mock()
|
||||
type(m2).physical_resource_id = 'nested_id'
|
||||
self.client.resources.get = mock.Mock(
|
||||
side_effect=[m1, m2])
|
||||
|
||||
shell.do_hook_clear(self.client, self.args)
|
||||
payload = self.client.resources.signal.call_args_list[0][1]
|
||||
self.assertEqual({'unset_hook': 'pre-create'}, payload['data'])
|
||||
self.assertEqual('bp', payload['resource_name'])
|
||||
self.assertEqual('nested_id', payload['stack_id'])
|
||||
|
||||
def test_clear_wildcard_hooks(self):
|
||||
type(self.args).hook = mock.PropertyMock(
|
||||
return_value=['a/*b/bp*'])
|
||||
type(self.args).pre_create = mock.PropertyMock(return_value=True)
|
||||
a = mock.Mock()
|
||||
type(a).resource_name = 'a'
|
||||
b = mock.Mock()
|
||||
type(b).resource_name = 'matcthis_b'
|
||||
bp = mock.Mock()
|
||||
type(bp).resource_name = 'bp_matchthis'
|
||||
self.client.resources.list = mock.Mock(
|
||||
side_effect=[[a], [b], [bp]])
|
||||
m1 = mock.Mock()
|
||||
m2 = mock.Mock()
|
||||
type(m2).physical_resource_id = 'nested_id'
|
||||
self.client.resources.get = mock.Mock(
|
||||
side_effect=[m1, m2])
|
||||
|
||||
shell.do_hook_clear(self.client, self.args)
|
||||
payload = self.client.resources.signal.call_args_list[0][1]
|
||||
self.assertEqual({'unset_hook': 'pre-create'},
|
||||
payload['data'])
|
||||
self.assertEqual('bp_matchthis', payload['resource_name'])
|
||||
self.assertEqual('nested_id', payload['stack_id'])
|
6
stacktaskclient/tests/unit/var/adopt_stack_data.json
Normal file
6
stacktaskclient/tests/unit/var/adopt_stack_data.json
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"action": "CREATE",
|
||||
"status": "COMPLETE",
|
||||
"name": "teststack",
|
||||
"resources": {}
|
||||
}
|
9
stacktaskclient/tests/unit/var/minimal.template
Normal file
9
stacktaskclient/tests/unit/var/minimal.template
Normal file
@ -0,0 +1,9 @@
|
||||
{
|
||||
"AWSTemplateFormatVersion" : "2010-09-09",
|
||||
"Parameters" : {
|
||||
},
|
||||
"Resources" : {
|
||||
},
|
||||
"Outputs" : {
|
||||
}
|
||||
}
|
18
stacktaskclient/v1/__init__.py
Normal file
18
stacktaskclient/v1/__init__.py
Normal file
@ -0,0 +1,18 @@
|
||||
# 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.
|
||||
|
||||
__all__ = ['Client']
|
||||
|
||||
from stacktaskclient.v1.client import Client # noqa
|
34
stacktaskclient/v1/build_info.py
Normal file
34
stacktaskclient/v1/build_info.py
Normal file
@ -0,0 +1,34 @@
|
||||
# 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.
|
||||
|
||||
from stacktaskclient.common import utils
|
||||
from stacktaskclient.openstack.common.apiclient import base
|
||||
|
||||
|
||||
class BuildInfo(base.Resource):
|
||||
def __repr__(self):
|
||||
return "<BuildInfo %s>" % self._info
|
||||
|
||||
def build_info(self):
|
||||
return self.manager.build_info()
|
||||
|
||||
|
||||
class BuildInfoManager(base.BaseManager):
|
||||
resource_class = BuildInfo
|
||||
|
||||
def build_info(self):
|
||||
resp = self.client.get('/build_info')
|
||||
body = utils.get_response_body(resp)
|
||||
return body
|
51
stacktaskclient/v1/client.py
Normal file
51
stacktaskclient/v1/client.py
Normal file
@ -0,0 +1,51 @@
|
||||
# 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.
|
||||
|
||||
from stacktaskclient.common import http
|
||||
#from stacktaskclient.v1 import stacks
|
||||
from stacktaskclient.v1 import users
|
||||
from stacktaskclient.v1 import roles
|
||||
|
||||
|
||||
class Client(object):
|
||||
"""Client for the Stacktask v1 API.
|
||||
|
||||
:param string endpoint: A user-supplied endpoint URL for the stacktask
|
||||
service.
|
||||
:param string token: Token for authentication.
|
||||
:param integer timeout: Allows customization of the timeout for client
|
||||
http requests. (optional)
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize a new client for the Stacktask v1 API."""
|
||||
self.http_client = http._construct_http_client(*args, **kwargs)
|
||||
#self.stacks = stacks.StackManager(self.http_client)
|
||||
self.users = users.UsersManager(self.http_client)
|
||||
self.roles = roles.RolesManager(self.http_client)
|
||||
#self.resources = resources.ResourceManager(self.http_client)
|
||||
#self.resource_types = resource_types.ResourceTypeManager(
|
||||
# self.http_client)
|
||||
#self.events = events.EventManager(self.http_client)
|
||||
#self.actions = actions.ActionManager(self.http_client)
|
||||
#self.build_info = build_info.BuildInfoManager(self.http_client)
|
||||
#self.software_deployments = \
|
||||
# software_deployments.SoftwareDeploymentManager(
|
||||
# self.http_client)
|
||||
#self.software_configs = software_configs.SoftwareConfigManager(
|
||||
# self.http_client)
|
||||
#self.services = services.ServiceManager(self.http_client)
|
||||
#self.template_versions = template_versions.TemplateVersionManager(
|
||||
# self.http_client)
|
75
stacktaskclient/v1/roles.py
Normal file
75
stacktaskclient/v1/roles.py
Normal file
@ -0,0 +1,75 @@
|
||||
# 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.
|
||||
from stacktaskclient.common import utils
|
||||
|
||||
import six
|
||||
from six.moves.urllib import parse
|
||||
|
||||
from stacktaskclient.openstack.common.apiclient import base
|
||||
|
||||
|
||||
class Roles(base.Resource):
|
||||
def __repr__(self):
|
||||
return "<Roles %s>" % self._info
|
||||
|
||||
def create(self, **fields):
|
||||
return self.manager.create(self.identifier, **fields)
|
||||
|
||||
def get(self):
|
||||
# set_loaded() first ... so if we have to bail, we know we tried.
|
||||
self._loaded = True
|
||||
if not hasattr(self.manager, 'get'):
|
||||
return
|
||||
|
||||
new = self.manager.get(self.identifier)
|
||||
if new:
|
||||
self._add_details(new._info)
|
||||
|
||||
@property
|
||||
def action(self):
|
||||
s = self.stack_status
|
||||
# Return everything before the first underscore
|
||||
return s[:s.index('_')]
|
||||
|
||||
@property
|
||||
def status(self):
|
||||
s = self.stack_status
|
||||
# Return everything after the first underscore
|
||||
return s[s.index('_') + 1:]
|
||||
|
||||
@property
|
||||
def identifier(self):
|
||||
return '%s/%s' % (self.stack_name, self.id)
|
||||
|
||||
|
||||
class RolesManager(base.BaseManager):
|
||||
resource_class = Roles
|
||||
|
||||
def list(self, **kwargs):
|
||||
"""Get a list of roles that can be managed.
|
||||
|
||||
:param limit: maximum number of stacks to return
|
||||
:param marker: begin returning stacks that appear later in the stack
|
||||
list than that represented by this stack id
|
||||
:param filters: dict of direct comparison filters that mimics the
|
||||
structure of a stack object
|
||||
:rtype: list of :class:`Users`
|
||||
"""
|
||||
params = {}
|
||||
#import pdb; pdb.set_trace()
|
||||
url = '/roles?%(params)s' % {'params': parse.urlencode(params, True)}
|
||||
roles = self._list(url, 'roles')
|
||||
for role in roles:
|
||||
yield role
|
170
stacktaskclient/v1/shell.py
Normal file
170
stacktaskclient/v1/shell.py
Normal file
@ -0,0 +1,170 @@
|
||||
# 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 fnmatch
|
||||
import logging
|
||||
|
||||
from oslo_serialization import jsonutils
|
||||
from oslo_utils import strutils
|
||||
import six
|
||||
from six.moves.urllib import request
|
||||
import time
|
||||
import yaml
|
||||
|
||||
from stacktaskclient.common import deployment_utils
|
||||
from stacktaskclient.common import event_utils
|
||||
from stacktaskclient.common import http
|
||||
from stacktaskclient.common import template_format
|
||||
from stacktaskclient.common import template_utils
|
||||
from stacktaskclient.common import utils
|
||||
|
||||
from stacktaskclient.openstack.common._i18n import _
|
||||
from stacktaskclient.openstack.common._i18n import _LE
|
||||
from stacktaskclient.openstack.common._i18n import _LW
|
||||
|
||||
import stacktaskclient.exc as exc
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _authenticated_fetcher(hc):
|
||||
"""A wrapper around the stacktask client object to fetch a template.
|
||||
"""
|
||||
def _do(*args, **kwargs):
|
||||
if isinstance(hc.http_client, http.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 do_user_tenant_list(hc, args):
|
||||
"""List all users in tenant"""
|
||||
kwargs = {}
|
||||
fields = ['id', 'username', 'email', 'roles']
|
||||
|
||||
tenant_users = hc.users.list(**kwargs)
|
||||
utils.print_list(tenant_users, fields, sortby_index=1)
|
||||
|
||||
|
||||
@utils.arg('--tenant-id', metavar='<tenant>',
|
||||
help=_('Invite to a particular tenant'))
|
||||
@utils.arg('--user-email', metavar='<email>',
|
||||
help=_('Email address of user to invite'))
|
||||
def do_user_tenant_invite(hc, args):
|
||||
"""
|
||||
Invites a user to become a member of a tenant.
|
||||
User does not need to have an existing openstack account.
|
||||
"""
|
||||
print("do_user_tenant_invite")
|
||||
pass
|
||||
|
||||
|
||||
@utils.arg('--user', '--user-id', metavar='<user>',
|
||||
help=_('Name or ID of user.'))
|
||||
@utils.arg('--tenant', '--tenant-id', metavar='<tenant>',
|
||||
help=_('Name or ID of tenant.'))
|
||||
def do_user_role_add(hc, args):
|
||||
"""Add a role to user"""
|
||||
print("do_user_role_add")
|
||||
pass
|
||||
|
||||
|
||||
@utils.arg('--user', '--user-id', metavar='<user>',
|
||||
help=_('Name or ID of user.'))
|
||||
@utils.arg('--tenant', '--tenant-id', metavar='<tenant>',
|
||||
help=_('Name or ID of tenant.'))
|
||||
#@utils.arg('--role', keystone )
|
||||
def do_user_role_remove(hc, args):
|
||||
"""Remove a role from a user"""
|
||||
print("do_user_role_remove")
|
||||
pass
|
||||
|
||||
|
||||
@utils.arg('--tenant', metavar='<tenant>',
|
||||
help=_('Name or ID of tenant.'))
|
||||
def do_managed_role_list(rc, args):
|
||||
"""List roles that may be managed in a given tenant"""
|
||||
fields = ['id', 'name']
|
||||
kwargs = {}
|
||||
#import pdb; pdb.set_trace()
|
||||
roles = rc.roles.list(**kwargs)
|
||||
utils.print_list(roles, fields, sortby_index=1)
|
||||
|
||||
|
||||
#----- ------------ OLD HEAT SHELL COMMANDS -------------------
|
||||
|
||||
|
||||
@utils.arg('-s', '--show-deleted', default=False, action="store_true",
|
||||
help=_('Include soft-deleted stacks in the stack listing.'))
|
||||
@utils.arg('-n', '--show-nested', default=False, action="store_true",
|
||||
help=_('Include nested stacks in the stack listing.'))
|
||||
@utils.arg('-a', '--show-hidden', default=False, action="store_true",
|
||||
help=_('Include hidden stacks in the stack listing.'))
|
||||
@utils.arg('-f', '--filters', metavar='<KEY1=VALUE1;KEY2=VALUE2...>',
|
||||
help=_('Filter parameters to apply on returned stacks. '
|
||||
'This can be specified multiple times, or once with parameters '
|
||||
'separated by a semicolon.'),
|
||||
action='append')
|
||||
@utils.arg('-t', '--tags', metavar='<TAG1,TAG2...>',
|
||||
help=_('Show stacks containing these tags, combine multiple tags '
|
||||
'using the boolean AND expression'))
|
||||
@utils.arg('--tags-any', metavar='<TAG1,TAG2...>',
|
||||
help=_('Show stacks containing these tags, combine multiple tags '
|
||||
'using the boolean OR expression'))
|
||||
@utils.arg('--not-tags', metavar='<TAG1,TAG2...>',
|
||||
help=_('Show stacks not containing these tags, combine multiple '
|
||||
'tags using the boolean AND expression'))
|
||||
@utils.arg('--not-tags-any', metavar='<TAG1,TAG2...>',
|
||||
help=_('Show stacks not containing these tags, combine multiple '
|
||||
'tags using the boolean OR expression'))
|
||||
@utils.arg('-l', '--limit', metavar='<LIMIT>',
|
||||
help=_('Limit the number of stacks returned.'))
|
||||
@utils.arg('-m', '--marker', metavar='<ID>',
|
||||
help=_('Only return stacks that appear after the given stack ID.'))
|
||||
@utils.arg('-g', '--global-tenant', action='store_true', default=False,
|
||||
help=_('Display stacks from all tenants. Operation only authorized '
|
||||
'for users who match the policy in heat\'s policy.json.'))
|
||||
@utils.arg('-o', '--show-owner', action='store_true', default=False,
|
||||
help=_('Display stack owner information. This is automatically '
|
||||
'enabled when using %(arg)s.') % {'arg': '--global-tenant'})
|
||||
def do_stack_list(hc, args=None):
|
||||
'''List the user's stacks.'''
|
||||
kwargs = {}
|
||||
fields = ['id', 'username', 'email', 'roles']
|
||||
if args:
|
||||
kwargs = {'limit': args.limit,
|
||||
'marker': args.marker,
|
||||
'filters': utils.format_parameters(args.filters),
|
||||
'tags': args.tags,
|
||||
'tags_any': args.tags_any,
|
||||
'not_tags': args.not_tags,
|
||||
'not_tags_any': args.not_tags_any,
|
||||
'global_tenant': args.global_tenant,
|
||||
'show_deleted': args.show_deleted,
|
||||
'show_hidden': args.show_hidden}
|
||||
if args.show_nested:
|
||||
fields.append('parent')
|
||||
kwargs['show_nested'] = True
|
||||
|
||||
if args.global_tenant or args.show_owner:
|
||||
fields.insert(2, 'stack_owner')
|
||||
if args.global_tenant:
|
||||
fields.insert(2, 'project')
|
||||
|
||||
stacks = hc.stacks.list(**kwargs)
|
||||
utils.print_list(stacks, fields, sortby_index=3)
|
243
stacktaskclient/v1/users.py
Normal file
243
stacktaskclient/v1/users.py
Normal file
@ -0,0 +1,243 @@
|
||||
# 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.
|
||||
from stacktaskclient.common import utils
|
||||
|
||||
import six
|
||||
from six.moves.urllib import parse
|
||||
|
||||
from stacktaskclient.openstack.common.apiclient import base
|
||||
|
||||
|
||||
class Users(base.Resource):
|
||||
def __repr__(self):
|
||||
return "<Users %s>" % self._info
|
||||
|
||||
def preview(self, **fields):
|
||||
return self.manager.preview(**fields)
|
||||
|
||||
def create(self, **fields):
|
||||
return self.manager.create(self.identifier, **fields)
|
||||
|
||||
def update(self, **fields):
|
||||
self.manager.update(self.identifier, **fields)
|
||||
|
||||
def delete(self):
|
||||
return self.manager.delete(self.identifier)
|
||||
|
||||
def abandon(self):
|
||||
return self.manager.abandon(self.identifier)
|
||||
|
||||
def snapshot(self, name=None):
|
||||
return self.manager.snapshot(self.identifier, name)
|
||||
|
||||
def snapshot_show(self, snapshot_id):
|
||||
return self.manager.snapshot_show(self.identifier, snapshot_id)
|
||||
|
||||
def snapshot_delete(self, snapshot_id):
|
||||
return self.manager.snapshot_delete(self.identifier, snapshot_id)
|
||||
|
||||
def restore(self, snapshot_id):
|
||||
return self.manager.restore(self.identifier, snapshot_id)
|
||||
|
||||
def snapshot_list(self):
|
||||
return self.manager.snapshot_list(self.identifier)
|
||||
|
||||
def get(self):
|
||||
# set_loaded() first ... so if we have to bail, we know we tried.
|
||||
self._loaded = True
|
||||
if not hasattr(self.manager, 'get'):
|
||||
return
|
||||
|
||||
new = self.manager.get(self.identifier)
|
||||
if new:
|
||||
self._add_details(new._info)
|
||||
|
||||
@property
|
||||
def action(self):
|
||||
s = self.stack_status
|
||||
# Return everything before the first underscore
|
||||
return s[:s.index('_')]
|
||||
|
||||
@property
|
||||
def status(self):
|
||||
s = self.stack_status
|
||||
# Return everything after the first underscore
|
||||
return s[s.index('_') + 1:]
|
||||
|
||||
@property
|
||||
def identifier(self):
|
||||
return '%s/%s' % (self.stack_name, self.id)
|
||||
|
||||
|
||||
class UsersManager(base.BaseManager):
|
||||
resource_class = Users
|
||||
|
||||
def list(self, **kwargs):
|
||||
"""Get a list of stacks.
|
||||
|
||||
:param limit: maximum number of stacks to return
|
||||
:param marker: begin returning stacks that appear later in the stack
|
||||
list than that represented by this stack id
|
||||
:param filters: dict of direct comparison filters that mimics the
|
||||
structure of a stack object
|
||||
:rtype: list of :class:`Users`
|
||||
"""
|
||||
def paginate(params):
|
||||
'''Paginate stacks, even if more than API limit.'''
|
||||
current_limit = int(params.get('limit') or 0)
|
||||
url = '/users?%s' % parse.urlencode(params, True)
|
||||
#import pdb; pdb.set_trace()
|
||||
stacks = self._list(url, 'users')
|
||||
for stack in stacks:
|
||||
yield stack
|
||||
|
||||
num_stacks = len(stacks)
|
||||
remaining_limit = current_limit - num_stacks
|
||||
if remaining_limit > 0 and num_stacks > 0:
|
||||
params['limit'] = remaining_limit
|
||||
params['marker'] = stack.id
|
||||
for stack in paginate(params):
|
||||
yield stack
|
||||
|
||||
params = {}
|
||||
if 'filters' in kwargs:
|
||||
filters = kwargs.pop('filters')
|
||||
params.update(filters)
|
||||
|
||||
for key, value in six.iteritems(kwargs):
|
||||
if value:
|
||||
params[key] = value
|
||||
|
||||
return paginate(params)
|
||||
|
||||
def preview(self, **kwargs):
|
||||
"""Preview a stack."""
|
||||
headers = self.client.credentials_headers()
|
||||
resp = self.client.post('/stacks/preview',
|
||||
data=kwargs, headers=headers)
|
||||
body = utils.get_response_body(resp)
|
||||
return Stack(self, body.get('stack'))
|
||||
|
||||
def create(self, **kwargs):
|
||||
"""Create a stack."""
|
||||
headers = self.client.credentials_headers()
|
||||
resp = self.client.post('/stacks',
|
||||
data=kwargs, headers=headers)
|
||||
body = utils.get_response_body(resp)
|
||||
return body
|
||||
|
||||
def update(self, stack_id, **kwargs):
|
||||
"""Update a stack."""
|
||||
headers = self.client.credentials_headers()
|
||||
if kwargs.pop('existing', None):
|
||||
self.client.patch('/stacks/%s' % stack_id, data=kwargs,
|
||||
headers=headers)
|
||||
else:
|
||||
self.client.put('/stacks/%s' % stack_id, data=kwargs,
|
||||
headers=headers)
|
||||
|
||||
def delete(self, stack_id):
|
||||
"""Delete a stack."""
|
||||
self._delete("/stacks/%s" % stack_id)
|
||||
|
||||
def abandon(self, stack_id):
|
||||
"""Abandon a stack."""
|
||||
stack = self.get(stack_id)
|
||||
resp = self.client.delete('/stacks/%s/abandon' % stack.identifier)
|
||||
body = utils.get_response_body(resp)
|
||||
return body
|
||||
|
||||
def snapshot(self, stack_id, name=None):
|
||||
"""Snapshot a stack."""
|
||||
stack = self.get(stack_id)
|
||||
data = {}
|
||||
if name:
|
||||
data['name'] = name
|
||||
resp = self.client.post('/stacks/%s/snapshots' % stack.identifier,
|
||||
data=data)
|
||||
body = utils.get_response_body(resp)
|
||||
return body
|
||||
|
||||
def snapshot_show(self, stack_id, snapshot_id):
|
||||
stack = self.get(stack_id)
|
||||
resp = self.client.get('/stacks/%s/snapshots/%s' % (stack.identifier,
|
||||
snapshot_id))
|
||||
body = utils.get_response_body(resp)
|
||||
return body
|
||||
|
||||
def snapshot_delete(self, stack_id, snapshot_id):
|
||||
stack = self.get(stack_id)
|
||||
resp = self.client.delete('/stacks/%s/snapshots/%s' %
|
||||
(stack.identifier, snapshot_id))
|
||||
body = utils.get_response_body(resp)
|
||||
return body
|
||||
|
||||
def restore(self, stack_id, snapshot_id):
|
||||
stack = self.get(stack_id)
|
||||
resp = self.client.post('/stacks/%s/snapshots/%s/restore' %
|
||||
(stack.identifier, snapshot_id))
|
||||
body = utils.get_response_body(resp)
|
||||
return body
|
||||
|
||||
def snapshot_list(self, stack_id):
|
||||
stack = self.get(stack_id)
|
||||
resp = self.client.get('/stacks/%s/snapshots' % stack.identifier)
|
||||
body = utils.get_response_body(resp)
|
||||
return body
|
||||
|
||||
def get(self, stack_id):
|
||||
"""Get the metadata for a specific stack.
|
||||
|
||||
:param stack_id: Stack ID to lookup
|
||||
"""
|
||||
resp = self.client.get('/stacks/%s' % stack_id)
|
||||
body = utils.get_response_body(resp)
|
||||
return Stack(self, body.get('stack'))
|
||||
|
||||
def template(self, stack_id):
|
||||
"""Get the template content for a specific stack as a parsed JSON
|
||||
object.
|
||||
|
||||
:param stack_id: Stack ID to get the template for
|
||||
"""
|
||||
resp = self.client.get('/stacks/%s/template' % stack_id)
|
||||
body = utils.get_response_body(resp)
|
||||
return body
|
||||
|
||||
def validate(self, **kwargs):
|
||||
"""Validate a stack template."""
|
||||
resp = self.client.post('/validate', data=kwargs)
|
||||
body = utils.get_response_body(resp)
|
||||
return body
|
||||
|
||||
|
||||
class StackChildManager(base.BaseManager):
|
||||
@property
|
||||
def api(self):
|
||||
return self.client
|
||||
|
||||
def _resolve_stack_id(self, stack_id):
|
||||
# if the id already has a slash in it,
|
||||
# then it is already {stack_name}/{stack_id}
|
||||
if stack_id.find('/') > 0:
|
||||
return stack_id
|
||||
# We want to capture the redirect, not actually get the stack,
|
||||
# since all we want is the stacks:lookup response to get the
|
||||
# fully qualified ID, and not all users are allowed to do the
|
||||
# redirected stacks:show, so pass redirect=False
|
||||
resp = self.client.get('/stacks/%s' % stack_id, redirect=False)
|
||||
location = resp.headers.get('location')
|
||||
path = self.client.strip_endpoint(location)
|
||||
return path[len('/stacks/'):]
|
19
test-requirements.txt
Normal file
19
test-requirements.txt
Normal file
@ -0,0 +1,19 @@
|
||||
# The order of packages is significant, because pip processes them in the order
|
||||
# of appearance. Changing the order has an impact on the overall integration
|
||||
# process, which may cause wedges in the gate later.
|
||||
|
||||
# Hacking already pins down pep8, pyflakes and flake8
|
||||
hacking<0.11,>=0.10.0
|
||||
coverage>=3.6
|
||||
discover
|
||||
fixtures>=1.3.1
|
||||
requests-mock>=0.6.0 # Apache-2.0
|
||||
mock>=1.2
|
||||
mox3>=0.7.0
|
||||
oslosphinx>=2.5.0 # Apache-2.0
|
||||
oslotest>=1.10.0 # Apache-2.0
|
||||
sphinx!=1.2.0,!=1.3b1,<1.3,>=1.1.2
|
||||
tempest-lib>=0.6.1
|
||||
testrepository>=0.0.18
|
||||
testscenarios>=0.4
|
||||
testtools>=1.4.0
|
27
tools/heat.bash_completion
Normal file
27
tools/heat.bash_completion
Normal file
@ -0,0 +1,27 @@
|
||||
# bash completion for openstack heat
|
||||
|
||||
_heat_opts="" # lazy init
|
||||
_heat_flags="" # lazy init
|
||||
_heat_opts_exp="" # lazy init
|
||||
_heat()
|
||||
{
|
||||
local cur prev kbc
|
||||
COMPREPLY=()
|
||||
cur="${COMP_WORDS[COMP_CWORD]}"
|
||||
prev="${COMP_WORDS[COMP_CWORD-1]}"
|
||||
|
||||
if [ "x$_heat_opts" == "x" ] ; then
|
||||
kbc="`heat bash-completion | sed -e "s/ -h / /"`"
|
||||
_heat_opts="`echo "$kbc" | sed -e "s/--[a-z0-9_-]*//g" -e "s/[ ][ ]*/ /g"`"
|
||||
_heat_flags="`echo " $kbc" | sed -e "s/ [^-][^-][a-z0-9_-]*//g" -e "s/[ ][ ]*/ /g"`"
|
||||
_heat_opts_exp="`echo $_heat_opts | sed -e "s/[ ]/|/g"`"
|
||||
fi
|
||||
|
||||
if [[ " ${COMP_WORDS[@]} " =~ " "($_heat_opts_exp)" " && "$prev" != "help" ]] ; then
|
||||
COMPREPLY=($(compgen -W "${_heat_flags}" -- ${cur}))
|
||||
else
|
||||
COMPREPLY=($(compgen -W "${_heat_opts}" -- ${cur}))
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
complete -o default -F _heat heat
|
74
tools/install_venv.py
Normal file
74
tools/install_venv.py
Normal file
@ -0,0 +1,74 @@
|
||||
# Copyright 2010 United States Government as represented by the
|
||||
# Administrator of the National Aeronautics and Space Administration.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Copyright 2010 OpenStack Foundation
|
||||
# Copyright 2013 IBM Corp.
|
||||
# Copyright (c) 2013 Hewlett-Packard Development Company, L.P.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import ConfigParser
|
||||
import os
|
||||
import sys
|
||||
|
||||
import install_venv_common as install_venv # flake8: noqa
|
||||
|
||||
|
||||
def print_help(project, venv, root):
|
||||
help = """
|
||||
%(project)s development environment setup is complete.
|
||||
|
||||
%(project)s development uses virtualenv to track and manage Python
|
||||
dependencies while in development and testing.
|
||||
|
||||
To activate the %(project)s virtualenv for the extent of your current
|
||||
shell session you can run:
|
||||
|
||||
$ source %(venv)s/bin/activate
|
||||
|
||||
Or, if you prefer, you can run commands in the virtualenv on a case by
|
||||
case basis by running:
|
||||
|
||||
$ %(root)s/tools/with_venv.sh <your command>
|
||||
"""
|
||||
print help % dict(project=project, venv=venv, root=root)
|
||||
|
||||
|
||||
def main(argv):
|
||||
root = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
|
||||
|
||||
if os.environ.get('tools_path'):
|
||||
root = os.environ['tools_path']
|
||||
venv = os.path.join(root, '.venv')
|
||||
if os.environ.get('venv'):
|
||||
venv = os.environ['venv']
|
||||
|
||||
pip_requires = os.path.join(root, 'requirements.txt')
|
||||
test_requires = os.path.join(root, 'test-requirements.txt')
|
||||
py_version = "python%s.%s" % (sys.version_info[0], sys.version_info[1])
|
||||
setup_cfg = ConfigParser.ConfigParser()
|
||||
setup_cfg.read('setup.cfg')
|
||||
project = setup_cfg.get('metadata', 'name')
|
||||
|
||||
install = install_venv.InstallVenv(
|
||||
root, venv, pip_requires, test_requires, py_version, project)
|
||||
options = install.parse_args(argv)
|
||||
install.check_python_version()
|
||||
install.check_dependencies()
|
||||
install.create_virtualenv(no_site_packages=options.no_site_packages)
|
||||
install.install_dependencies()
|
||||
print_help(project, venv, root)
|
||||
|
||||
if __name__ == '__main__':
|
||||
main(sys.argv)
|
172
tools/install_venv_common.py
Normal file
172
tools/install_venv_common.py
Normal file
@ -0,0 +1,172 @@
|
||||
# Copyright 2013 OpenStack Foundation
|
||||
# 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.
|
||||
|
||||
"""Provides methods needed by installation script for OpenStack development
|
||||
virtual environments.
|
||||
|
||||
Since this script is used to bootstrap a virtualenv from the system's Python
|
||||
environment, it should be kept strictly compatible with Python 2.6.
|
||||
|
||||
Synced in from openstack-common
|
||||
"""
|
||||
|
||||
from __future__ import print_function
|
||||
|
||||
import optparse
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
|
||||
class InstallVenv(object):
|
||||
|
||||
def __init__(self, root, venv, requirements,
|
||||
test_requirements, py_version,
|
||||
project):
|
||||
self.root = root
|
||||
self.venv = venv
|
||||
self.requirements = requirements
|
||||
self.test_requirements = test_requirements
|
||||
self.py_version = py_version
|
||||
self.project = project
|
||||
|
||||
def die(self, message, *args):
|
||||
print(message % args, file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
def check_python_version(self):
|
||||
if sys.version_info < (2, 6):
|
||||
self.die("Need Python Version >= 2.6")
|
||||
|
||||
def run_command_with_code(self, cmd, redirect_output=True,
|
||||
check_exit_code=True):
|
||||
"""Runs a command in an out-of-process shell.
|
||||
|
||||
Returns the output of that command. Working directory is self.root.
|
||||
"""
|
||||
if redirect_output:
|
||||
stdout = subprocess.PIPE
|
||||
else:
|
||||
stdout = None
|
||||
|
||||
proc = subprocess.Popen(cmd, cwd=self.root, stdout=stdout)
|
||||
output = proc.communicate()[0]
|
||||
if check_exit_code and proc.returncode != 0:
|
||||
self.die('Command "%s" failed.\n%s', ' '.join(cmd), output)
|
||||
return (output, proc.returncode)
|
||||
|
||||
def run_command(self, cmd, redirect_output=True, check_exit_code=True):
|
||||
return self.run_command_with_code(cmd, redirect_output,
|
||||
check_exit_code)[0]
|
||||
|
||||
def get_distro(self):
|
||||
if (os.path.exists('/etc/fedora-release') or
|
||||
os.path.exists('/etc/redhat-release')):
|
||||
return Fedora(
|
||||
self.root, self.venv, self.requirements,
|
||||
self.test_requirements, self.py_version, self.project)
|
||||
else:
|
||||
return Distro(
|
||||
self.root, self.venv, self.requirements,
|
||||
self.test_requirements, self.py_version, self.project)
|
||||
|
||||
def check_dependencies(self):
|
||||
self.get_distro().install_virtualenv()
|
||||
|
||||
def create_virtualenv(self, no_site_packages=True):
|
||||
"""Creates the virtual environment and installs PIP.
|
||||
|
||||
Creates the virtual environment and installs PIP only into the
|
||||
virtual environment.
|
||||
"""
|
||||
if not os.path.isdir(self.venv):
|
||||
print('Creating venv...', end=' ')
|
||||
if no_site_packages:
|
||||
self.run_command(['virtualenv', '-q', '--no-site-packages',
|
||||
self.venv])
|
||||
else:
|
||||
self.run_command(['virtualenv', '-q', self.venv])
|
||||
print('done.')
|
||||
else:
|
||||
print("venv already exists...")
|
||||
pass
|
||||
|
||||
def pip_install(self, *args):
|
||||
self.run_command(['tools/with_venv.sh',
|
||||
'pip', 'install', '--upgrade'] + list(args),
|
||||
redirect_output=False)
|
||||
|
||||
def install_dependencies(self):
|
||||
print('Installing dependencies with pip (this can take a while)...')
|
||||
|
||||
# First things first, make sure our venv has the latest pip and
|
||||
# setuptools and pbr
|
||||
self.pip_install('pip>=1.4')
|
||||
self.pip_install('setuptools')
|
||||
self.pip_install('pbr')
|
||||
|
||||
self.pip_install('-r', self.requirements, '-r', self.test_requirements)
|
||||
|
||||
def parse_args(self, argv):
|
||||
"""Parses command-line arguments."""
|
||||
parser = optparse.OptionParser()
|
||||
parser.add_option('-n', '--no-site-packages',
|
||||
action='store_true',
|
||||
help="Do not inherit packages from global Python "
|
||||
"install")
|
||||
return parser.parse_args(argv[1:])[0]
|
||||
|
||||
|
||||
class Distro(InstallVenv):
|
||||
|
||||
def check_cmd(self, cmd):
|
||||
return bool(self.run_command(['which', cmd],
|
||||
check_exit_code=False).strip())
|
||||
|
||||
def install_virtualenv(self):
|
||||
if self.check_cmd('virtualenv'):
|
||||
return
|
||||
|
||||
if self.check_cmd('easy_install'):
|
||||
print('Installing virtualenv via easy_install...', end=' ')
|
||||
if self.run_command(['easy_install', 'virtualenv']):
|
||||
print('Succeeded')
|
||||
return
|
||||
else:
|
||||
print('Failed')
|
||||
|
||||
self.die('ERROR: virtualenv not found.\n\n%s development'
|
||||
' requires virtualenv, please install it using your'
|
||||
' favorite package management tool' % self.project)
|
||||
|
||||
|
||||
class Fedora(Distro):
|
||||
"""This covers all Fedora-based distributions.
|
||||
|
||||
Includes: Fedora, RHEL, CentOS, Scientific Linux
|
||||
"""
|
||||
|
||||
def check_pkg(self, pkg):
|
||||
return self.run_command_with_code(['rpm', '-q', pkg],
|
||||
check_exit_code=False)[1] == 0
|
||||
|
||||
def install_virtualenv(self):
|
||||
if self.check_cmd('virtualenv'):
|
||||
return
|
||||
|
||||
if not self.check_pkg('python-virtualenv'):
|
||||
self.die("Please install 'python-virtualenv'.")
|
||||
|
||||
super(Fedora, self).install_virtualenv()
|
10
tools/with_venv.sh
Executable file
10
tools/with_venv.sh
Executable file
@ -0,0 +1,10 @@
|
||||
#!/bin/bash
|
||||
|
||||
command -v tox > /dev/null 2>&1
|
||||
if [ $? -ne 0 ]; then
|
||||
echo 'This script requires "tox" to run.'
|
||||
echo 'You can install it with "pip install tox".'
|
||||
exit 1;
|
||||
fi
|
||||
|
||||
tox -evenv -- $@
|
51
tox.ini
Normal file
51
tox.ini
Normal file
@ -0,0 +1,51 @@
|
||||
[tox]
|
||||
envlist = py26,py27,pypy,pep8
|
||||
minversion = 1.6
|
||||
skipsdist = True
|
||||
|
||||
[testenv]
|
||||
setenv = VIRTUAL_ENV={envdir}
|
||||
usedevelop = True
|
||||
install_command = pip install -U {opts} {packages}
|
||||
deps = -r{toxinidir}/requirements.txt
|
||||
-r{toxinidir}/test-requirements.txt
|
||||
commands = python setup.py testr --slowest --testr-args='{posargs}'
|
||||
|
||||
[testenv:pypy]
|
||||
deps = setuptools<3.2
|
||||
-r{toxinidir}/requirements.txt
|
||||
-r{toxinidir}/test-requirements.txt
|
||||
|
||||
[testenv:pep8]
|
||||
commands =
|
||||
flake8
|
||||
# Check that .po and .pot files are valid:
|
||||
bash -c "find python-registrationclient -type f -regex '.*\.pot?' -print0|xargs -0 -n 1 msgfmt --check-format -o /dev/null"
|
||||
whitelist_externals = bash
|
||||
|
||||
[testenv:venv]
|
||||
commands = {posargs}
|
||||
|
||||
[testenv:functional]
|
||||
setenv =
|
||||
OS_TEST_PATH = ./registrationclient/tests/functional
|
||||
passenv = OS_*
|
||||
|
||||
[testenv:cover]
|
||||
commands = python setup.py testr --coverage --testr-args='{posargs}'
|
||||
|
||||
[testenv:docs]
|
||||
commands=
|
||||
python setup.py build_sphinx
|
||||
|
||||
[tox:jenkins]
|
||||
downloadcache = ~/cache/pip
|
||||
|
||||
[flake8]
|
||||
ignore = E123,E126,E128,E241,E265,E713,H202,H405,H238
|
||||
show-source = True
|
||||
exclude=.venv,.git,.tox,dist,*openstack/common*,*lib/python*,*egg,build
|
||||
max-complexity=20
|
||||
|
||||
[hacking]
|
||||
import_exceptions = registrationclient.openstack.common._i18n
|
Loading…
Reference in New Issue
Block a user