Initial version of python-stacktaskclient

Based on python-heatclient 04b3880 (2015-06-04)

Change-Id: Ie54c889a4b89ec32f9a00b954560929f35712021
This commit is contained in:
Dale Smith 2015-09-17 16:41:56 +01:00
parent ace3212557
commit f85dcba1a1
85 changed files with 15846 additions and 0 deletions

18
.gitignore vendored Normal file
View 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
View 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
View 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
View 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
View 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
View 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

1
babel.cfg Normal file
View File

@ -0,0 +1 @@
[python: **.py]

2
doc/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
build/
source/ref/

90
doc/Makefile Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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)

View 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
View 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)

View File

View 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)

View 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] = {}

View 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

View 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)

View 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

View 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)

View 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
View 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

View File

View 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

View 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
"""

View 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)

View 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)

View 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)

View 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,
})

View 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)

View 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
View 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()

View File

View 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)

View 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

View 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}

View File

@ -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 }

View 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'])

View File

View 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)

View 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)

View 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)

View 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())

View 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)

View 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)

View 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)

View 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)

View 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)

View 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)

View 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)

File diff suppressed because it is too large Load Diff

View 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)

View 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)

View 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)

View 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)

File diff suppressed because it is too large Load Diff

View 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'))

View 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'))

View 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'])

View File

@ -0,0 +1,6 @@
{
"action": "CREATE",
"status": "COMPLETE",
"name": "teststack",
"resources": {}
}

View File

@ -0,0 +1,9 @@
{
"AWSTemplateFormatVersion" : "2010-09-09",
"Parameters" : {
},
"Resources" : {
},
"Outputs" : {
}
}

View 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

View 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

View 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)

View 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
View 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
View 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
View 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

View 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
View 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)

View 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
View 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
View 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