Retire Murano: remove repo content

Murano project is retiring
- https://review.opendev.org/c/openstack/governance/+/919358

this commit remove the content of this project repo

Depends-On: https://review.opendev.org/c/openstack/project-config/+/919359/
Change-Id: I4142dd143526d47df3b2389b8680543024ab6127
This commit is contained in:
Ghanshyam Mann 2024-05-10 15:52:22 -07:00
parent 3187cb89c2
commit c648cbee08
174 changed files with 8 additions and 19412 deletions

View File

@ -1,8 +0,0 @@
[run]
source = muranoclient
omit =
.tox/*
muranoclient/tests/*
[report]
ignore_errors = True

41
.gitignore vendored
View File

@ -1,41 +0,0 @@
#IntelJ Idea
.idea/
#virtualenv
.venv/
#Build results
build/
dist/
*.egg-info/
.tox
AUTHORS
ChangeLog
eggs/
.eggs/
develop-eggs/
*.egg
#Python
*.pyc
#Translation build
*.mo
#SQLite Database files
*.sqlite
#Autogenerated Documentation
doc/source/api
#Testing framework
.stestr/
.coverage
*,cover
cover
#swap file
*.swp
# Files created by releasenotes build
releasenotes/build

View File

@ -1,4 +0,0 @@
[DEFAULT]
test_path=${OS_TEST_PATH:-./muranoclient/tests/unit}
top_dir=./

View File

@ -1,40 +0,0 @@
- project:
templates:
- openstack-cover-jobs
- openstack-python3-jobs
- check-requirements
- release-notes-jobs-python3
- publish-openstack-docs-pti
- openstackclient-plugin-jobs
check:
jobs:
- muranoclient-functional-test-mysql-backend
gate:
jobs:
- muranoclient-functional-test-mysql-backend
- job:
name: muranoclient-functional-test-mysql-backend
parent: devstack-tox-functional
timeout: 4200
voting: false
vars:
openrc_enable_export: true
devstack_plugins:
heat: https://opendev.org/openstack/heat
murano: https://opendev.org/openstack/murano
devstack_localrc:
KEYSTONE_ADMIN_ENDPOINT: true
irrelevant-files:
- ^(test-|)requirements.txt$
- ^setup.cfg$
- ^doc/.*$
- ^.*\.rst$
- ^releasenotes/.*$
- ^muranoclient/tests/.*$
required-projects:
- openstack/heat
- openstack/murano
- openstack/murano-dashboard
- openstack/python-heatclient
- openstack/python-muranoclient

View File

@ -1,19 +0,0 @@
The source repository for this project can be found at:
https://opendev.org/openstack/python-muranoclient
Pull requests submitted through GitHub are not monitored.
To start contributing to OpenStack, follow the steps in the contribution guide
to set up and use Gerrit:
https://docs.openstack.org/contributors/code-and-documentation/quick-start.html
Bugs should be filed on Launchpad:
https://bugs.launchpad.net/python-muranoclient
For more specific information about contributing to this repository, see the
python-muranoclient contributor guide:
https://docs.openstack.org/python-muranoclient/latest/contributor/contributing.html

View File

@ -1,4 +0,0 @@
Style Commandments
==================
Read the OpenStack Style Commandments https://docs.openstack.org/hacking/latest/

176
LICENSE
View File

@ -1,176 +0,0 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.

View File

@ -1,70 +1,10 @@
========================
Team and repository tags
========================
This project is no longer maintained.
.. image:: https://governance.openstack.org/tc/badges/python-muranoclient.svg
:target: https://governance.openstack.org/tc/reference/tags/index.html
The contents of this repository are still available in the Git
source code management system. To see the contents of this
repository before it reached its end of life, please check out the
previous commit with "git checkout HEAD^1".
.. Change things from this point on
Murano
======
.. image:: https://img.shields.io/pypi/v/python-muranoclient.svg
:target: https://pypi.org/project/python-muranoclient/
:alt: Latest Version
Murano Project introduces an application catalog, which allows application
developers and cloud administrators to publish various cloud-ready
applications in a browsable categorised catalog, which may be used by the
cloud users (including the inexperienced ones) to pick-up the needed
applications and services and composes the reliable environments out of them
in a "push-the-button" manner.
* `PyPi`_ - package installation
* `Launchpad project`_ - release management
* `Blueprints`_ - feature specifications
* `Bugs`_ - issue tracking
* `Source`_
* `Specs`_
* `How to Contribute`_
.. _PyPi: https://pypi.org/project/python-muranoclient
.. _Launchpad project: https://launchpad.net/python-muranoclient
.. _Blueprints: https://blueprints.launchpad.net/python-muranoclient
.. _Bugs: https://bugs.launchpad.net/python-muranoclient
.. _Source: https://opendev.org/openstack/python-muranoclient
.. _How to Contribute: https://docs.openstack.org/infra/manual/developers.html
.. _Specs: https://specs.openstack.org/openstack/murano-specs/
.. _Release Notes: https://docs.openstack.org/releasenotes/python-muranoclient
Python Muranoclient
-------------------
python-muranoclient is a client library for Murano built on the Murano API.
It provides a Python API (the ``muranoclient`` module) and a command-line tool
(``murano``).
Project Resources
-----------------
Project status, bugs, and blueprints are tracked on Launchpad:
* Client bug tracker
* https://launchpad.net/python-muranoclient
* Murano bug tracker
* https://launchpad.net/murano
Developer documentation can be found here:
https://docs.openstack.org/murano/latest/
Additional resources are linked from the project wiki page:
https://wiki.openstack.org/wiki/Murano
License
-------
Apache License Version 2.0 http://www.apache.org/licenses/LICENSE-2.0
For any further questions, please email
openstack-discuss@lists.openstack.org or join #openstack-dev on
OFTC.

View File

@ -1,6 +0,0 @@
# 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.
sphinx>=2.0.0,!=2.1.0 # BSD
openstackdocstheme>=2.2.1 # Apache-2.0
reno>=3.1.0 # Apache-2.0

View File

@ -1,42 +0,0 @@
=================
Murano API Client
=================
In order to use the python api directly, you must first obtain an auth token
and identify which endpoint you wish to speak to. Once you have done so,
you can use the API like so::
>>> from muranoclient import Client
>>> murano = Client('1', endpoint=MURANO_URL, token=OS_AUTH_TOKEN)
...
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 (:option:``--os-username``, :option:``--os-password``,
:option:``--os-tenant-id``, and :option:``--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 :option:``--os-image-url`` and
:option:``--os-auth-token``. You can alternatively set these environment
variables::
export MURANO_URL=http://murano.example.org:8082/
export OS_AUTH_TOKEN=3bcc3d3a03f44e3d8377f9247b0ad155
Once you've configured your authentication parameters, you can run
:command:`murano help` to see a complete listing of available commands.
.. toctree::
murano

File diff suppressed because it is too large Load Diff

View File

@ -1,83 +0,0 @@
# -*- coding: utf-8 -*-
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import os
# -- General configuration ----------------------------------------------------
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
extensions = ['sphinx.ext.autodoc',
'openstackdocstheme',]
# autodoc generation is a bit aggressive and a nuisance when doing heavy
# text edit cycles.
# execute "export SPHINX_DEBUG=1" in your terminal to disable
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
# The suffix of source filenames.
source_suffix = '.rst'
# The master toctree document.
master_doc = 'index'
# General information about the project.
copyright = u'OpenStack Foundation'
exclude_trees = ['api']
# 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
# The name of the Pygments (syntax highlighting) style to use.
pygments_style = 'native'
# -- Options for HTML output --------------------------------------------------
# The theme to use for HTML and HTML Help pages. Major themes that come with
# Sphinx are currently 'default' and 'sphinxdoc'.
html_theme = 'openstackdocs'
# Add any paths that contain custom themes here, relative to this directory.
#html_theme_path = ['_theme']
#html_theme_path = [openstackdocstheme.get_html_theme_path()]
# openstackdocstheme options
openstackdocs_repo_name = 'openstack/python-muranoclient'
openstackdocs_bug_project = 'python-muranoclient'
openstackdocs_bug_tag = ''
# Output file base name for HTML help builder.
htmlhelp_basename = 'python-muranoclientdoc'
# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title, author, documentclass
# [howto/manual]).
latex_documents = [
(
'index',
'python-muranoclient.tex',
u'python-muranoclient Documentation',
u'OpenStack Foundation',
'manual'
),
]
# Example configuration for intersphinx: refer to the Python standard library.
intersphinx_mapping = {'http://docs.python.org/': None}

View File

@ -1,47 +0,0 @@
============================
So You Want to Contribute...
============================
For general information on contributing to OpenStack, please check out the
`contributor guide <https://docs.openstack.org/contributors/>`_ to get started.
It covers all the basics that are common to all OpenStack projects: the accounts
you need, the basics of interacting with our Gerrit review system, how we
communicate as a community, etc.
Below will cover the more project specific information you need to get started
with python-muranoclient.
Communication
~~~~~~~~~~~~~
* IRC channel #murano at OFTC
* Mailing list (prefix subjects with ``[murano]`` for faster responses)
http://lists.openstack.org/cgi-bin/mailman/listinfo/openstack-discuss
Contacting the Core Team
~~~~~~~~~~~~~~~~~~~~~~~~
Please refer the `python-muranoclient Core Team
<https://review.opendev.org/admin/groups/b082df89771ed409e9ce06fd9487aefd9e4fc868,members>`_ contacts.
New Feature Planning
~~~~~~~~~~~~~~~~~~~~
python-muranoclient features are tracked on `Launchpad <https://bugs.launchpad.net/python-muranoclient>`_.
Task Tracking
~~~~~~~~~~~~~
We track our tasks in `Launchpad <https://bugs.launchpad.net/python-muranoclient>`_.
If you're looking for some smaller, easier work item to pick up and get started
on, search for the 'low-hanging-fruit' tag.
Reporting a Bug
~~~~~~~~~~~~~~~
You found an issue and want to make sure we are aware of it? You can do so on
`Launchpad <https://bugs.launchpad.net/python-muranoclient>`_.
Getting Your Patch Merged
~~~~~~~~~~~~~~~~~~~~~~~~~
All changes proposed to the python-muranoclient project require one or two +2 votes
from python-muranoclient core reviewers before one of the core reviewers can approve
patch by giving ``Workflow +1`` vote.
Project Team Lead Duties
~~~~~~~~~~~~~~~~~~~~~~~~
All common PTL duties are enumerated in the `PTL guide
<https://docs.openstack.org/project-team-guide/ptl.html>`_.

View File

@ -1,23 +0,0 @@
=================================
python-muranoclient documentation
=================================
This is a client for the OpenStack Application Catalog API.
There's a Python API (the :mod:`muranoclient`
module) and a :doc:`command-line script <cli/murano>`
(installed as :program:`murano`).
.. toctree::
:maxdepth: 2
cli/index
For Contributors
================
* If you are a new contributor to python-muranoclient please refer: :doc:`contributor/contributing`
.. toctree::
:hidden:
contributor/contributing

View File

@ -1,31 +0,0 @@
# Copyright (c) 2014 Mirantis, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import os
import pbr.version
_ROOT = os.path.abspath(os.path.dirname(__file__))
def get_resource(path):
return os.path.join(_ROOT, 'data', path)
version_info = pbr.version.VersionInfo('python-muranoclient')
try:
__version__ = version_info.version_string()
except AttributeError:
__version__ = None

View File

@ -1,216 +0,0 @@
# 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
import abc
import argparse
import os
from stevedore import extension
from muranoclient.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 = "muranoclient.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 _discovered_plugins.items():
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(iter(_discovered_plugins.keys())):
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"])
class BaseAuthPlugin(object, metaclass=abc.ABCMeta):
"""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

@ -1,523 +0,0 @@
# Copyright 2010 Jacob Kaplan-Moss
# Copyright 2011 OpenStack Foundation
# Copyright 2012 Grid Dynamics
# Copyright 2013 OpenStack Foundation
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
"""
Base utilities to build API operation managers and objects on top of.
"""
# E1102: %s is not callable
# pylint: disable=E1102
import abc
import copy
from oslo_utils import strutils
from oslo_utils import uuidutils
import parse
from muranoclient.apiclient import exceptions
from muranoclient.i18n import _
def getid(obj):
"""Return id if argument is a Resource.
Abstracts the common pattern of allowing both an object or an object's ID
(UUID) as a parameter when dealing with relationships.
"""
try:
if obj.uuid:
return obj.uuid
except AttributeError:
pass
try:
return obj.id
except AttributeError:
return obj
# TODO(aababilov): call run_hooks() in HookableMixin's child classes
class HookableMixin(object):
"""Mixin so classes can register and run hooks."""
_hooks_map = {}
@classmethod
def add_hook(cls, hook_type, hook_func):
"""Add a new hook of specified type.
:param cls: class that registers hooks
:param hook_type: hook type, e.g., '__pre_parse_args__'
:param hook_func: hook function
"""
if hook_type not in cls._hooks_map:
cls._hooks_map[hook_type] = []
cls._hooks_map[hook_type].append(hook_func)
@classmethod
def run_hooks(cls, hook_type, *args, **kwargs):
"""Run all hooks of specified type.
:param cls: class that registers hooks
:param hook_type: hook type, e.g., '__pre_parse_args__'
:param args: args to be passed to every hook function
:param kwargs: kwargs to be passed to every hook function
"""
hook_funcs = cls._hooks_map.get(hook_type) or []
for hook_func in hook_funcs:
hook_func(*args, **kwargs)
class BaseManager(HookableMixin):
"""Basic manager type providing common operations.
Managers interact with a particular type of API (servers, flavors, images,
etc.) and provide CRUD operations for them.
"""
resource_class = None
def __init__(self, client):
"""Initializes BaseManager with `client`.
:param client: instance of BaseClient descendant for HTTP requests
"""
super(BaseManager, self).__init__()
self.client = client
def _list(self, url, response_key, 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'
: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]
# 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):
"""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'
"""
body = self.client.get(url).json()
return self.resource_class(self, body[response_key], 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, 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., 'servers'
: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()
if return_raw:
return body[response_key]
return self.resource_class(self, body[response_key])
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'
"""
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'
"""
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)
class ManagerWithFind(BaseManager, metaclass=abc.ABCMeta):
"""Manager with additional `find()`/`findall()` methods."""
@abc.abstractmethod
def list(self):
pass
def find(self, **kwargs):
"""Find a single item with attributes matching ``**kwargs``.
This isn't very efficient: it loads the entire list then filters on
the Python side.
"""
matches = self.findall(**kwargs)
num_matches = len(matches)
if num_matches == 0:
msg = _("No %(name)s matching %(args)s.") % {
'name': self.resource_class.__name__,
'args': kwargs
}
raise exceptions.NotFound(msg)
elif num_matches > 1:
raise exceptions.NoUniqueMatch()
else:
return matches[0]
def findall(self, **kwargs):
"""Find all items with attributes matching ``**kwargs``.
This isn't very efficient: it loads the entire list then filters on
the Python side.
"""
found = []
searches = kwargs.items()
for obj in self.list():
try:
if all(getattr(obj, attr) == value
for (attr, value) in searches):
found.append(obj)
except AttributeError:
continue
return found
class CrudManager(BaseManager):
"""Base manager class for manipulating entities.
Children of this class are expected to define a `collection_key` and `key`.
- `collection_key`: Usually a plural noun by convention (e.g. `entities`);
used to refer collections in both URL's (e.g. `/v3/entities`) and JSON
objects containing a list of member resources (e.g. `{'entities': [{},
{}, {}]}`).
- `key`: Usually a singular noun by convention (e.g. `entity`); used to
refer to an individual member of the collection.
"""
collection_key = None
key = None
def build_url(self, base_url=None, **kwargs):
"""Builds a resource URL for the given kwargs.
Given an example collection where `collection_key = 'entities'` and
`key = 'entity'`, the following URL's could be generated.
By default, the URL will represent a collection of entities, e.g.::
/entities
If kwargs contains an `entity_id`, then the URL will represent a
specific member, e.g.::
/entities/{entity_id}
:param base_url: if provided, the generated URL will be appended to it
"""
url = base_url if base_url is not None else ''
url += '/%s' % self.collection_key
# do we have a specific entity?
entity_id = kwargs.get('%s_id' % self.key)
if entity_id is not None:
url += '/%s' % entity_id
return url
def _filter_kwargs(self, kwargs):
"""Drop null values and handle ids."""
for key, ref in kwargs.copy().items():
if ref is None:
kwargs.pop(key)
else:
if isinstance(ref, Resource):
kwargs.pop(key)
kwargs['%s_id' % key] = getid(ref)
return kwargs
def create(self, **kwargs):
kwargs = self._filter_kwargs(kwargs)
return self._post(
self.build_url(**kwargs),
{self.key: kwargs},
self.key)
def get(self, **kwargs):
kwargs = self._filter_kwargs(kwargs)
return self._get(
self.build_url(**kwargs),
self.key)
def head(self, **kwargs):
kwargs = self._filter_kwargs(kwargs)
return self._head(self.build_url(**kwargs))
def list(self, base_url=None, **kwargs):
"""List the collection.
:param base_url: if provided, the generated URL will be appended to it
"""
kwargs = self._filter_kwargs(kwargs)
return self._list(
'%(base_url)s%(query)s' % {
'base_url': self.build_url(base_url=base_url, **kwargs),
'query': '?%s' % parse.urlencode(kwargs) if kwargs else '',
},
self.collection_key)
def put(self, base_url=None, **kwargs):
"""Update an element.
:param base_url: if provided, the generated URL will be appended to it
"""
kwargs = self._filter_kwargs(kwargs)
return self._put(self.build_url(base_url=base_url, **kwargs))
def update(self, **kwargs):
kwargs = self._filter_kwargs(kwargs)
params = kwargs.copy()
params.pop('%s_id' % self.key)
return self._patch(
self.build_url(**kwargs),
{self.key: params},
self.key)
def delete(self, **kwargs):
kwargs = self._filter_kwargs(kwargs)
return self._delete(
self.build_url(**kwargs))
def find(self, base_url=None, **kwargs):
"""Find a single item with attributes matching ``**kwargs``.
:param base_url: if provided, the generated URL will be appended to it
"""
kwargs = self._filter_kwargs(kwargs)
rl = self._list(
'%(base_url)s%(query)s' % {
'base_url': self.build_url(base_url=base_url, **kwargs),
'query': '?%s' % parse.urlencode(kwargs) if kwargs else '',
},
self.collection_key)
num = len(rl)
if num == 0:
msg = _("No %(name)s matching %(args)s.") % {
'name': self.resource_class.__name__,
'args': kwargs
}
raise exceptions.NotFound(404, 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
self._init_completion_cache()
def _init_completion_cache(self):
cache_write = getattr(self.manager, 'write_to_completion_cache', None)
if not cache_write:
return
# NOTE(sirp): ensure `id` is already present because if it isn't we'll
# enter an infinite loop of __getattr__ -> get -> __init__ ->
# __getattr__ -> ...
if 'id' in self.__dict__ and uuidutils.is_uuid_like(self.id):
cache_write('uuid', self.id)
if self.human_id:
cache_write('human_id', self.human_id)
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 info.items():
try:
setattr(self, k, v)
self._info[k] = v
except AttributeError:
# In this case we already defined the attribute on the class
pass
def __getattr__(self, k):
if k not in self.__dict__:
# NOTE(bcwaldon): disallow lazy-loading if already loaded once
if not self.is_loaded():
self.get()
return self.__getattr__(k)
raise AttributeError(k)
else:
return self.__dict__[k]
def get(self):
"""Support for lazy loading details.
Some clients, such as novaclient have the option to lazy load the
details, details which can be loaded with this function.
"""
# set_loaded() first ... so if we have to bail, we know we tried.
self.set_loaded(True)
if not hasattr(self.manager, 'get'):
return
new = self.manager.get(self.id)
if new:
self._add_details(new._info)
def __eq__(self, other):
if not isinstance(other, Resource):
return NotImplemented
# two resources of different types are not equal
if not isinstance(other, self.__class__):
return False
return self._info == other._info
def __ne__(self, other):
return not self.__eq__(other)
def is_loaded(self):
return self._loaded
def set_loaded(self, val):
self._loaded = val
def to_dict(self):
return copy.deepcopy(self._info)

View File

@ -1,359 +0,0 @@
# 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
try:
import simplejson as json
except ImportError:
import json
import time
from oslo_log import log as logging
from oslo_utils import importutils
import requests
from muranoclient.apiclient import exceptions
from muranoclient.i18n import _
_logger = logging.getLogger(__name__)
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 = "muranoclient.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
def _http_log_req(self, url, method, kwargs):
if not self.debug:
return
string_parts = [
"curl -i",
"-X '%s'" % method,
"'%s'" % url,
]
for element in kwargs['headers']:
header = "-H '%s: %s'" % (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.pop('json'))
def get_timings(self):
return self.times
def reset_timings(self):
self.times = []
def request(self, url, method, **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.get("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(url, method, kwargs)
if self.timings:
start_time = time.time()
resp = self.http.request(url, method, **kwargs)
if self.timings:
self.times.append(("%s %s" % (url, method),
start_time, time.time()))
self._http_log_resp(resp)
if resp.status_code >= 400:
_logger.debug(
"Request returned failure status: %s",
resp.status_code)
raise exceptions.from_response(resp, url, method)
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, url, method, **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
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, url, method, **kwargs):
return self.http_client.client_request(
self, url, method, **kwargs)
def head(self, url, **kwargs):
return self.client_request(url, "HEAD", **kwargs)
def get(self, url, **kwargs):
return self.client_request(url, "GET", **kwargs)
def post(self, url, **kwargs):
return self.client_request(url, "POST", **kwargs)
def put(self, url, **kwargs):
return self.client_request(url, "PUT", **kwargs)
def delete(self, url, **kwargs):
return self.client_request(url, "DELETE", **kwargs)
def patch(self, url, **kwargs):
return self.client_request(url, "PATCH", **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

@ -1,461 +0,0 @@
# 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.
"""
import inspect
import sys
from muranoclient.i18n import _
class ClientException(Exception):
"""The base exception class for all exceptions this library raises."""
pass
class MissingArgs(ClientException):
"""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)
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 ConnectionRefused(ClientException):
"""Cannot 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: %s") % repr(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: %s") % repr(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 vars(sys.modules[__name__]).items()
if inspect.isclass(obj) and getattr(obj, 'http_status', False)
)
def from_response(response, url, method):
"""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 = list(body.values())[0]
kwargs["message"] = error.get("message")
kwargs["details"] = error.get("details")
elif content_type.startswith("text/"):
kwargs["details"] = 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

@ -1,176 +0,0 @@
# 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.
"""
# W0102: Dangerous default value %s as argument
# pylint: disable=W0102
import json
import parse
import requests
from muranoclient.apiclient import client
def assert_has_keys(dct, required=None, optional=None):
if required is None:
required = []
if optional is None:
optional = []
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 isinstance(self._content, str):
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)
def __ne__(self, other):
return not self.__eq__(other)
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, url, method, body=None, pos=-1):
"""Assert than an API method was just called."""
expected = (url, method)
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, url, method, body=None):
"""Assert than an API method was called anytime in the test."""
expected = (url, method)
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' % \
(url, method, 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, url, method, **kwargs):
# Check that certain things are called correctly
if method in ["GET", "DELETE"]:
assert "json" not in kwargs
# Note the call
self.callstack.append(
(url,
method,
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' %
(url, method, callback))
resp = getattr(self, callback)(**kwargs)
if len(resp) == 3:
status, headers, body = resp
else:
status, body = resp
headers = {}
return TestResponse({
"status_code": status,
"text": body,
"headers": headers,
})

View File

@ -1,21 +0,0 @@
# Copyright (c) 2013 Mirantis, 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 oslo_utils import importutils
def Client(version, *args, **kwargs):
module = importutils.import_versioned_module('muranoclient',
version, 'client')
client_class = getattr(module, 'Client')
return client_class(*args, **kwargs)

View File

@ -1,229 +0,0 @@
# Copyright 2012 OpenStack LLC.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
"""
Base utilities to build API operation managers and objects on top of.
"""
import abc
import copy
from muranoclient.apiclient import exceptions
def getid(obj):
"""Get obj's id or object itself if no id
Abstracts the common pattern of allowing both an object or
an object's ID (UUID) as a parameter when dealing with relationships.
"""
try:
return obj.id
except AttributeError:
return obj
class Manager(object):
"""Interacts with type of API
Managers interact with a particular type of API (servers, flavors,
images, etc.) and provide CRUD operations for them.
"""
resource_class = None
def __init__(self, api):
self.api = api
def _list(self, url, response_key=None, obj_class=None,
data=None, headers=None):
if headers is None:
headers = {}
resp, body = self.api.json_request(url, 'GET', headers=headers)
if obj_class is None:
obj_class = self.resource_class
if response_key:
if response_key not in body:
body[response_key] = []
data = body[response_key]
else:
data = body
return [obj_class(self, res, loaded=True) for res in data if res]
def _delete(self, url, headers=None):
if headers is None:
headers = {}
self.api.request(url, 'DELETE', headers=headers)
def _update(self, url, data, response_key=None, return_raw=False,
headers=None, method='PUT', content_type='application/json'):
if headers is None:
headers = {}
resp, body = self.api.json_request(url, method,
content_type=content_type,
data=data, headers=headers)
# PUT or PATCH requests may not return a body
if body:
if return_raw:
if response_key:
return body[response_key]
return body
if response_key:
return self.resource_class(self, body[response_key])
return self.resource_class(self, body)
def _create(self, url, data=None, response_key=None,
return_raw=False, headers=None):
if headers is None:
headers = {}
if data:
resp, body = self.api.json_request(url, 'POST',
data=data, headers=headers)
else:
resp, body = self.api.json_request(url, 'POST', headers=headers)
if return_raw:
if response_key:
return body[response_key]
return body
if response_key:
return self.resource_class(self, body[response_key])
return self.resource_class(self, body)
def _get(self, url, response_key=None, return_raw=False, headers=None):
if headers is None:
headers = {}
resp, body = self.api.json_request(url, 'GET', headers=headers)
if return_raw:
if response_key:
return body[response_key]
return body
if response_key:
return self.resource_class(self, body[response_key])
return self.resource_class(self, body)
class ManagerWithFind(Manager, metaclass=abc.ABCMeta):
"""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.
"""
rl = self.findall(**kwargs)
num = len(rl)
if num == 0:
msg = "No %s matching %s." % (self.resource_class.__name__, kwargs)
raise exceptions.NotFound(msg)
elif num > 1:
raise exceptions.NoUniqueMatch
else:
return self.get(rl[0].id)
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 Resource(object):
"""Represents an instance of an object
A resource represents a particular instance of an object (tenant, user,
etc). This is pretty much just a bag for attributes.
:param manager: Manager object
:param info: dictionary representing resource attributes
:param loaded: prevent lazy-loading if set to True
"""
def __init__(self, manager, info, loaded=False):
self.manager = manager
self._info = info
self._add_details(info)
self._loaded = loaded
def _add_details(self, info):
for k, v in info.items():
setattr(self, k, v)
def __setstate__(self, d):
for k, v in d.items():
setattr(self, k, v)
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 __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)
def get(self):
# 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)
def __eq__(self, other):
if not isinstance(other, self.__class__):
return False
return self._info == other._info
def __ne__(self, other):
return not self.__eq__(other)
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

@ -1,207 +0,0 @@
# Copyright 2012 OpenStack LLC.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import re
import sys
# TODO(sjmc7): This module is likely redundant because it's replaced
# by openstack.common.apiclient; should be removed
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 InvalidEndpoint(BaseException):
"""The provided endpoint is invalid."""
class CommunicationError(BaseException):
"""Unable to communicate with server."""
class ClientException(Exception):
"""DEPRECATED!"""
class HTTPException(ClientException):
"""Base exception for all HTTP-derived exceptions."""
code = 'N/A'
def __init__(self, details=None):
self.details = details or self.__class__.__name__
def __str__(self):
return "%s (HTTP %s)" % (self.details, self.code)
class HTTPMultipleChoices(HTTPException):
code = 300
def __str__(self):
self.details = ("Requested version of Application Catalog API is not "
"available.")
return "%s (HTTP %s) %s" % (self.__class__.__name__, self.code,
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 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 httplib response."""
cls = _code_map.get(response.status_code, HTTPException)
body = response.content
if body and response.headers['content-type'].\
lower().startswith("application/json"):
# Iterate over the nested objects and retrieve the "message" attribute.
messages = [obj.get('message') for obj in response.json().values()]
# Join all of the messages together nicely and filter out any objects
# that don't have a "message" attr.
details = '\n'.join(i for i in messages if i is not None)
return cls(details=details)
elif body and \
response.headers['content-type'].lower().startswith("text/html"):
# Split the lines, strip whitespace and inline HTML from the response.
details = [re.sub(r'<.+?>', '', i.strip())
for i in response.text.splitlines()]
details = [i for i in details if i]
# Remove duplicates from the list.
details_seen = set()
details_temp = []
for i in details:
if i not in details_seen:
details_temp.append(i)
details_seen.add(i)
# Return joined string separated by colons.
details = ': '.join(details_temp)
return cls(details=details)
elif body:
details = body.replace('\n\n', '\n')
return cls(details=details)
return cls()
def from_code(code):
cls = _code_map.get(code, HTTPException)
return cls()
class NoTokenLookupException(Exception):
"""DEPRECATED!"""
pass
class EndpointNotFound(Exception):
"""DEPRECATED!"""
pass
class SSLConfigurationError(BaseException):
pass
class SSLCertificateError(BaseException):
pass

View File

@ -1,373 +0,0 @@
# Copyright 2012 OpenStack LLC.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import copy
import hashlib
import os
import socket
import keystoneclient.adapter as keystone_adapter
from oslo_log import log as logging
from oslo_serialization import jsonutils
from oslo_utils import encodeutils
import requests
import urllib
from muranoclient.common import exceptions as exc
LOG = logging.getLogger(__name__)
USER_AGENT = 'python-muranoclient'
CHUNKSIZE = 1024 * 64 # 64kB
def get_system_ca_file():
"""Return path to system default CA file."""
# Standard CA file locations for Debian/Ubuntu, RedHat/Fedora,
# Suse, FreeBSD/OpenBSD, MacOSX, and the bundled ca
ca_path = ['/etc/ssl/certs/ca-certificates.crt',
'/etc/pki/tls/certs/ca-bundle.crt',
'/etc/ssl/ca-bundle.pem',
'/etc/ssl/cert.pem',
'/System/Library/OpenSSL/certs/cacert.pem',
requests.certs.where()]
for ca in ca_path:
LOG.debug("Looking for ca file %s", ca)
if os.path.exists(ca):
LOG.debug("Using ca file %s", ca)
return ca
LOG.warning("System ca file could not be found.")
class HTTPClient(object):
def __init__(self, endpoint, **kwargs):
self.endpoint = endpoint
self.auth_url = kwargs.get('auth_url')
self.auth_token = kwargs.get('token')
self.username = kwargs.get('username')
self.password = kwargs.get('password')
self.region_name = kwargs.get('region_name')
self.include_pass = kwargs.get('include_pass')
self.endpoint_url = endpoint
self.cert_file = kwargs.get('cert_file')
self.key_file = kwargs.get('key_file')
self.timeout = kwargs.get('timeout')
self.ssl_connection_params = {
'cacert': kwargs.get('cacert'),
'cert_file': kwargs.get('cert_file'),
'key_file': kwargs.get('key_file'),
'insecure': kwargs.get('insecure'),
}
self.verify_cert = None
if urllib.parse.urlparse(endpoint).scheme == "https":
if kwargs.get('insecure'):
self.verify_cert = False
else:
self.verify_cert = kwargs.get('cacert', get_system_ca_file())
def _safe_header(self, name, value):
if name in ['X-Auth-Token', 'X-Subject-Token']:
# 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, url, method, kwargs):
curl = ['curl -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'),
('cacert', '--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, bytes):
try:
content = encodeutils.safe_decode(resp.content)
except UnicodeDecodeError:
pass
else:
dump.extend([content, ''])
LOG.debug('\n'.join(dump))
def request(self, url, method, log=True, **kwargs):
"""Send an http request with the specified characteristics.
Wrapper around requests.request to handle tasks such
as setting headers and error handling.
"""
_set_data(kwargs)
# 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)
self.log_curl_request(url, method, 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 the option not to follow redirects
follow_redirects = kwargs.pop('follow_redirects', 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,
requests.exceptions.ConnectionError) as e:
endpoint = self.endpoint
message = ("Error communicating with %(endpoint)s %(e)s" %
{'endpoint': endpoint, 'e': e})
raise exc.CommunicationError(message=message)
if log:
self.log_http_response(resp)
if 'X-Auth-Key' not 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.\n%s"
% resp.content)
elif 400 <= resp.status_code < 600:
raise exc.from_response(resp)
elif resp.status_code in (301, 302, 305):
# Redirected. Reissue the request to the new location,
# unless caller specified follow_redirects=False
if follow_redirects:
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 strip_endpoint(self, location):
if location is None:
message = "Location not returned with 302"
raise exc.InvalidEndpoint(message=message)
elif location.startswith(self.endpoint):
return location[len(self.endpoint):]
else:
message = "Prohibited endpoint redirect %s" % location
raise exc.InvalidEndpoint(message=message)
def credentials_headers(self):
creds = {}
if self.username:
creds['X-Auth-User'] = self.username
if self.password:
creds['X-Auth-Key'] = self.password
return creds
def json_request(self, url, method, content_type='application/json',
**kwargs):
kwargs.setdefault('headers', {})
kwargs['headers'].setdefault('Content-Type', content_type)
# Don't set Accept because we aren't always dealing in JSON
_set_data(kwargs)
if 'data' in kwargs:
kwargs['data'] = jsonutils.dumps(kwargs['data'])
resp = self.request(url, method, **kwargs)
body = resp.content
if body and 'application/json' in resp.headers['content-type']:
try:
body = resp.json()
except ValueError:
LOG.error('Could not decode response body as JSON')
else:
body = None
return resp, body
def json_patch_request(self, url, method='PATCH', **kwargs):
content_type = 'application/murano-packages-json-patch'
return self.json_request(
url, method, content_type=content_type, **kwargs)
def head(self, url, **kwargs):
return self.json_request(url, "HEAD", **kwargs)
def get(self, url, **kwargs):
return self.json_request(url, "GET", **kwargs)
def post(self, url, **kwargs):
return self.json_request(url, "POST", **kwargs)
def put(self, url, **kwargs):
return self.json_request(url, "PUT", **kwargs)
def delete(self, url, **kwargs):
return self.request(url, "DELETE", **kwargs)
def patch(self, url, **kwargs):
return self.json_request(url, "PATCH", **kwargs)
class SessionClient(keystone_adapter.Adapter):
"""Murano specific keystoneclient Adapter.
Murano can't use keystoneclient LegacyJsonAdapter, because murano has the
check for right content-type for "update" operation which is
'application/murano-packages-json-patch'. So, we need to create our own
adapter.
"""
def request(self, url, method, **kwargs):
raise_exc = kwargs.pop('raise_exc', True)
_set_data(kwargs)
resp = super(SessionClient, self).request(url,
method,
raise_exc=False,
**kwargs)
if raise_exc and resp.status_code >= 400:
LOG.trace("Error communicating with {url}: {exc}"
.format(url=url, exc=exc.from_response(resp)))
raise exc.from_response(resp)
return resp
def json_request(self, url, method, **kwargs):
headers = kwargs.setdefault('headers', {})
headers['Content-Type'] = kwargs.pop('content_type',
'application/json')
_set_data(kwargs)
if 'data' in kwargs:
kwargs['data'] = jsonutils.dumps(kwargs['data'])
# NOTE(starodubcevna): We need to prove that json field is empty,
# or it will be modified by keystone adapter.
kwargs['json'] = None
resp = self.request(url, method, **kwargs)
body = resp.text
if body:
try:
body = jsonutils.loads(body)
except ValueError:
pass
return resp, body
def json_patch_request(self, url, method='PATCH', **kwargs):
content_type = 'application/murano-packages-json-patch'
return self.json_request(
url, method, content_type=content_type, **kwargs)
def _construct_http_client(*args, **kwargs):
session = kwargs.pop('session', None)
auth = kwargs.pop('auth', None)
endpoint = next(iter(args), None)
if session:
service_type = kwargs.pop('service_type', None)
endpoint_type = kwargs.pop('endpoint_type', None)
region_name = kwargs.pop('region_name', None)
service_name = kwargs.pop('service_name', None)
parameters = {
'endpoint_override': endpoint,
'session': session,
'auth': auth,
'interface': endpoint_type,
'service_type': service_type,
'region_name': region_name,
'service_name': service_name,
'user_agent': 'python-muranoclient',
}
parameters.update(kwargs)
return SessionClient(**parameters)
else:
return HTTPClient(*args, **kwargs)
def _set_data(kwargs):
if 'body' in kwargs:
if 'data' in kwargs:
raise ValueError("Can't provide both 'data' and "
"'body' to a request")
LOG.warning("Use of 'body' is deprecated; use 'data' instead")
kwargs['data'] = kwargs.pop('body')

View File

@ -1,831 +0,0 @@
# Copyright 2012 OpenStack LLC.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import collections
from io import BytesIO
import json
from muranopkgcheck import manager as check_manager
from muranopkgcheck import pkg_loader as check_pkg_loader
from muranopkgcheck import validators as check_validators
import os
import re
import shutil
import sys
import tempfile
import textwrap
import uuid
import warnings
import zipfile
from oslo_log import log as logging
from oslo_serialization import jsonutils
from oslo_utils import encodeutils
from oslo_utils import uuidutils
import prettytable
import requests
import urllib
import yaml
import yaql
from muranoclient.common import exceptions
from muranoclient.i18n import _
try:
import yaql.language # noqa
from muranoclient.common.yaqlexpression import YaqlExpression
except ImportError:
# no yaql.language means legacy yaql
from muranoclient.common.yaqlexpression_legacy import YaqlExpression
LOG = logging.getLogger(__name__)
# Decorator for cli-args
def arg(*args, **kwargs):
def _decorator(func):
# Because of the semantics of decorator composition if we just append
# to the options list positional options will appear to be backwards.
func.__dict__.setdefault('arguments', []).insert(0, (args, kwargs))
return func
return _decorator
def json_formatter(js):
return jsonutils.dumps(js, indent=2)
def text_wrap_formatter(d):
return '\n'.join(textwrap.wrap(d or '', 55))
def pretty_choice_list(l):
return ', '.join("'%s'" % i for i in l)
def print_list(objs, fields, field_labels, formatters=None, sortby=0):
if formatters is None:
formatters = {}
pt = prettytable.PrettyTable([f for f in field_labels], caching=False)
pt.align = 'l'
for o in objs:
row = []
for field in fields:
if field in formatters:
row.append(formatters[field](o))
else:
data = getattr(o, field, None) or ''
row.append(data)
pt.add_row(row)
result = encodeutils.safe_encode(pt.get_string())
result = result.decode()
print(result)
def print_dict(d, formatters=None):
if formatters is None:
formatters = {}
pt = prettytable.PrettyTable(['Property', 'Value'], caching=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]])
result = encodeutils.safe_encode(pt.get_string(sortby='Property'))
result = result.decode()
print(result)
def find_resource(manager, name_or_id, *args, **kwargs):
"""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), *args, **kwargs)
except exceptions.NotFound:
pass
# now try to get entity as uuid
try:
uuid.UUID(str(name_or_id))
return manager.get(name_or_id, *args, **kwargs)
except (ValueError, exceptions.NotFound):
pass
# finally try to find entity by name
try:
return manager.find(name=name_or_id)
except exceptions.NotFound:
msg = "No %s with a name or ID of '%s' exists." % \
(manager.resource_class.__name__.lower(), name_or_id)
raise exceptions.CommandError(msg)
def string_to_bool(arg):
return arg.strip().lower() in ('t', 'true', 'yes', '1')
def env(*vars, **kwargs):
"""Search for the first defined of possibly many env vars
Returns the first environment variable defined in vars, or
returns the default defined in kwargs.
"""
for v in vars:
value = os.environ.get(v, None)
if value:
return value
return kwargs.get('default', '')
def exit(msg=''):
if msg:
print(encodeutils.safe_encode(msg), file=sys.stderr)
sys.exit(1)
def getsockopt(self, *args, **kwargs):
"""Allows us to monkey patch eventlet's GreenSocket
A function which allows us to monkey patch eventlet's
GreenSocket, adding a required 'getsockopt' method.
TODO: (mclaren) we can remove this once the eventlet fix
(https://bitbucket.org/eventlet/eventlet/commits/609f230)
lands in mainstream packages.
NOTE: Already in 0.13, but we can't be sure that all clients
that use python-muranoclient also use newest eventlet
"""
return self.fd.getsockopt(*args, **kwargs)
def exception_to_str(exc):
try:
error = str(exc)
except UnicodeError:
error = ("Caught '%(exception)s' exception." %
{"exception": exc.__class__.__name__})
return encodeutils.safe_encode(error, errors='ignore')
class NoCloseProxy(object):
"""A proxy object, that does nothing on close."""
def __init__(self, obj):
self.obj = obj
def close(self):
return
def __getattr__(self, name):
return getattr(self.obj, name)
class File(object):
def __init__(self, name, binary=True):
self.name = name
self.binary = binary
def open(self):
mode = 'rb' if self.binary else 'r'
if hasattr(self.name, 'read'):
# NOTE(kzaitsev) We do not want to close a file object
# passed to File wrapper. The caller should be responsible
# for closing it
return NoCloseProxy(self.name)
else:
if os.path.isfile(self.name):
return open(self.name, mode)
if os.path.isdir(self.name):
tmp = tempfile.NamedTemporaryFile()
archive = zipfile.ZipFile(tmp, 'w', zipfile.ZIP_DEFLATED)
for root, dirs, files in os.walk(self.name):
for _file in files:
destination = os.path.relpath(
os.path.join(root, _file), os.path.join(self.name))
archive.write(os.path.join(root, _file), destination)
tmp.flush()
return open(tmp.name, mode)
url = urllib.parse.urlparse(self.name)
if url.scheme in ('http', 'https'):
resp = requests.get(self.name, stream=True)
if not resp.ok:
raise ValueError("Got non-ok status({0}) "
"while connecting to {1}".format(
resp.status_code, self.name))
temp_file = tempfile.NamedTemporaryFile(mode='w+b')
for chunk in resp.iter_content(1024 * 1024):
temp_file.write(chunk)
temp_file.flush()
return open(temp_file.name, mode)
raise ValueError("Can't open {0}".format(self.name))
def to_url(filename, base_url, version='', path='/', extension=''):
if urllib.parse.urlparse(filename).scheme in ('http', 'https'):
return filename
if not base_url:
raise ValueError("No base_url for repository supplied")
if '/' in filename or filename in ('.', '..'):
raise ValueError("Invalid filename path supplied: {0}".format(
filename))
version = '.' + version if version else ''
return urllib.parse.urljoin(base_url,
path + filename + version + extension)
class FileWrapperMixin(object):
def __init__(self, file_wrapper):
self.file_wrapper = file_wrapper
try:
self._file = self.file_wrapper.open()
except Exception:
# NOTE(kzaitsev): We need to have _file available at __del__ time.
self._file = None
raise
def file(self):
self._file.seek(0)
return self._file
def close(self):
if self._file and not self._file.closed:
self._file.close()
def save(self, dst, binary=True):
file_name = self.file_wrapper.name
if urllib.parse.urlparse(file_name).scheme:
file_name = file_name.split('/')[-1]
dst = os.path.join(dst, file_name)
mode = 'wb' if binary else 'w'
with open(dst, mode) as dst_file:
self._file.seek(0)
shutil.copyfileobj(self._file, dst_file)
def __del__(self):
self.close()
class Package(FileWrapperMixin):
"""Represents murano package contents."""
@staticmethod
def from_file(file_obj):
if not isinstance(file_obj, File):
file_obj = File(file_obj)
pkg = Package(file_obj)
errs = pkg.validate()
if errs:
raise exceptions.HTTPBadRequest(details=errs)
return pkg
@staticmethod
def fromFile(file_obj):
warnings.warn("Use from_file function", DeprecationWarning)
return Package.from_file(file_obj)
@staticmethod
def from_location(name, base_url='', version='', url='', path=None):
"""Open file using one of three possible options
If path is supplied search for name file in the path, otherwise
if url is supplied - open that url and finally search murano
repository for the package.
"""
if path:
pkg_name = os.path.join(path, name)
file_name = None
for f in [pkg_name, pkg_name + '.zip']:
if os.path.exists(f):
file_name = f
if file_name:
return Package.from_file(file_name)
LOG.error("Couldn't find file for package {0}, tried {1}".format(
name, [pkg_name, pkg_name + '.zip']))
if url:
return Package.from_file(url)
return Package.from_file(to_url(
name,
base_url=base_url,
version=version,
path='apps/',
extension='.zip')
)
def validate(self):
m = check_manager.Manager(self._file,
loader=check_pkg_loader.ZipLoader)
errors = m.validate(
validators=[check_validators.manifest.ManifestValidator],
only_errors=True)
if errors:
fmt = check_manager.PlainTextFormatter().format
return 'Invalid Murano package\n{}\n'.format(fmt(errors))
@property
def contents(self):
"""Contents of a package."""
if not hasattr(self, '_contents'):
try:
self._file.seek(0)
self._zip_obj = zipfile.ZipFile(
BytesIO(self._file.read()))
except Exception as e:
LOG.error("Error {0} occurred,"
" while parsing the package".format(e))
raise
return self._zip_obj
@property
def manifest(self):
"""Parsed manifest file of a package."""
if not hasattr(self, '_manifest'):
try:
self._manifest = yaml.safe_load(
self.contents.open('manifest.yaml'))
except Exception as e:
LOG.error("Error {0} occurred, while extracting "
"manifest from package".format(e))
raise
return self._manifest
def images(self):
"""Returns a list of required image specifications."""
if 'images.lst' not in self.contents.namelist():
return []
try:
return yaml.safe_load(
self.contents.open('images.lst')).get('Images', [])
except Exception:
return []
@property
def resolvers(self):
if not hasattr(self, '_resolvers'):
self.classes
return self._resolvers
@property
def classes(self):
if not hasattr(self, '_classes'):
self._classes = {}
self._resolvers = {}
for class_name, class_file in (
self.manifest.get('Classes', {}).items()):
filename = "Classes/%s" % class_file
if filename not in self.contents.namelist():
continue
klass_list = yaml.load_all(self.contents.open(filename),
DummyYaqlYamlLoader)
if not klass_list:
raise ValueError('No classes defined in file')
resolver = None
for klass in klass_list:
ns = klass.get('Namespaces')
if ns:
resolver = NamespaceResolver(ns)
name = klass.get('Name')
if name and resolver:
name = resolver.resolve_name(name)
if name == class_name:
self._classes[class_name] = klass
self._resolvers[class_name] = resolver
break
return self._classes
@property
def ui(self):
if not hasattr(self, '_ui'):
if 'UI/ui.yaml' in self.contents.namelist():
self._ui = self.contents.open('UI/ui.yaml')
else:
self._ui = None
return self._ui
@property
def logo(self):
if not hasattr(self, '_logo'):
if 'logo.png' in self.contents.namelist():
self._logo = self.contents.open('logo.png')
else:
self._logo = None
return self._logo
def _get_package_order(self, packages_graph):
"""Sorts packages according to dependencies between them
Murano allows cyclic dependencies. It is impossible
to do topological sort for graph with cycles, so at first
graph condensation should be built.
For condensation building Kosaraju's algorithm is used.
Packages in strongly connected components can be situated
in random order to each other.
"""
def topological_sort(graph, start_node):
order = []
not_seen = set(graph)
def dfs(node):
not_seen.discard(node)
for dep_node in graph[node]:
if dep_node in not_seen:
dfs(dep_node)
order.append(node)
dfs(start_node)
return order
def transpose_graph(graph):
transposed = collections.defaultdict(list)
for node, deps in graph.items():
for dep in deps:
transposed[dep].append(node)
return transposed
order = topological_sort(packages_graph, self.manifest['FullName'])
order.reverse()
transposed = transpose_graph(packages_graph)
def top_sort_by_components(graph, component_order):
result = []
seen = set()
def dfs(node):
seen.add(node)
result.append(node)
for dep_node in graph[node]:
if dep_node not in seen:
dfs(dep_node)
for item in component_order:
if item not in seen:
dfs(item)
return reversed(result)
return top_sort_by_components(transposed, order)
def requirements(self, base_url, path=None, dep_dict=None):
"""Scans Require section of manifests of all the dependencies.
Returns a dict with FQPNs as keys and respective Package objects
as values, ordered by topological sort.
:param base_url: url of packages location
:param path: local path of packages location
:param dep_dict: unused. Left for backward compatibility
"""
unordered_requirements = {}
requirements_graph = collections.defaultdict(list)
dep_queue = collections.deque([(self.manifest['FullName'], self)])
while dep_queue:
dep_name, dep_file = dep_queue.popleft()
unordered_requirements[dep_name] = dep_file
direct_deps = Package._get_direct_deps(dep_file, base_url, path)
for name, file in direct_deps:
if name not in unordered_requirements:
dep_queue.append((name, file))
requirements_graph[dep_name] = [dep[0] for dep in direct_deps]
ordered_reqs_names = self._get_package_order(requirements_graph)
ordered_reqs_dict = collections.OrderedDict()
for name in ordered_reqs_names:
ordered_reqs_dict[name] = unordered_requirements[name]
return ordered_reqs_dict
@staticmethod
def _get_direct_deps(package, base_url, path):
result = []
if 'Require' in package.manifest:
for dep_name, ver in package.manifest['Require'].items():
try:
req_file = Package.from_location(
dep_name,
version=ver,
path=path,
base_url=base_url,
)
except Exception as e:
LOG.error("Error {0} occurred while parsing package {1}, "
"required by {2} package".format(
e, dep_name,
package.manifest['FullName']))
continue
result.append((req_file.manifest['FullName'], req_file))
return result
def save_image_local(image_spec, base_url, dst):
dst = os.path.join(dst, image_spec['Name'])
download_url = to_url(
image_spec.get("Url", image_spec['Name']),
base_url=base_url,
path='images/'
)
with open(dst, "w") as image_file:
response = requests.get(download_url, stream=True)
total_length = response.headers.get('content-length')
if total_length is None:
image_file.write(response.content)
else:
dl = 0
total_length = int(total_length)
for chunk in response.iter_content(1024 * 1024):
dl += len(chunk)
image_file.write(chunk)
done = int(50 * dl / total_length)
sys.stdout.write("\r[{0}{1}]".
format('=' * done, ' ' * (50 - done)))
sys.stdout.flush()
sys.stdout.write("\n")
image_file.flush()
def ensure_images(glance_client, image_specs, base_url,
local_path=None,
is_package_public=False):
"""Ensure that images are available
Ensure that images from image_specs are available in glance. If not
attempts: instructs glance to download the images and sets murano-specific
metadata for it.
"""
def _image_valid(image, keys):
for key in keys:
if key not in image:
LOG.warning("Image specification invalid: "
"No {0} key in image ").format(key)
return False
return True
keys = ['Name', 'DiskFormat', 'ContainerFormat', ]
installed_images = []
for image_spec in image_specs:
if not _image_valid(image_spec, keys):
continue
filters = {
'name': image_spec["Name"],
'disk_format': image_spec["DiskFormat"],
'container_format': image_spec["ContainerFormat"],
}
images = glance_client.images.list(filters=filters)
try:
img = next(images).to_dict()
except StopIteration:
img = None
update_metadata = False
if img:
LOG.info("Found desired image {0}, id {1}".format(
img['name'], img['id']))
# check for murano meta-data
if 'murano_image_info' in img.get('properties', {}):
LOG.info("Image {0} already has murano meta-data".format(
image_spec['Name']))
else:
update_metadata = True
else:
LOG.info("Desired image {0} not found attempting "
"to download".format(image_spec['Name']))
update_metadata = True
img_file = None
if local_path:
img_file = os.path.join(local_path, image_spec['Name'])
if img_file and not os.path.exists(img_file):
LOG.error("Image file {0} does not exist."
.format(img_file))
if img_file and os.path.exists(img_file):
img = glance_client.images.create(
name=image_spec['Name'],
container_format=image_spec['ContainerFormat'],
disk_format=image_spec['DiskFormat'],
data=open(img_file, 'rb'),
)
img = img.to_dict()
else:
download_url = to_url(
image_spec.get("Url", image_spec['Name']),
base_url=base_url,
path='images/',
)
LOG.info("Instructing glance to download image {0}".format(
image_spec['Name']))
img = glance_client.images.create(
name=image_spec["Name"],
container_format=image_spec['ContainerFormat'],
disk_format=image_spec['DiskFormat'],
copy_from=download_url)
img = img.to_dict()
if is_package_public:
try:
glance_client.images.update(img['id'], is_public=True)
LOG.debug('Success update for image {0}'.format(img['id']))
except Exception as e:
LOG.exception(_("Error {0} occurred while setting "
"image {1} public").format(e, img['id']))
installed_images.append(img)
if update_metadata and 'Meta' in image_spec:
LOG.info("Updating image {0} metadata".format(
image_spec['Name']))
murano_image_info = jsonutils.dumps(image_spec['Meta'])
glance_client.images.update(
img['id'], properties={'murano_image_info':
murano_image_info})
return installed_images
class Bundle(FileWrapperMixin):
"""Represents murano bundle contents."""
@staticmethod
def from_file(file_obj):
if not isinstance(file_obj, File):
file_obj = File(file_obj, binary=False)
return Bundle(file_obj)
@staticmethod
def fromFile(file_obj):
warnings.warn("Use from_file function", DeprecationWarning)
return Bundle.from_file(file_obj)
def package_specs(self):
"""Get a generator yielding package specifications
Returns a generator yielding package specifications i.e.
dicts with 'Name' and 'Version' fields
"""
self._file.seek(0)
bundle = None
try:
# NOTE(kzaitsev) jsonutils throws a type error here
# see bug 1515231
bundle = json.load(self._file)
except ValueError:
pass
if bundle is None:
try:
bundle = yaml.safe_load(self._file)
except yaml.error.YAMLError:
pass
if bundle is None or 'Packages' not in bundle:
raise ValueError("Can't parse bundle contents")
for package in bundle['Packages']:
if 'Name' not in package:
continue
yield package
def packages(self, base_url='', path=None):
"""Get a generator yielding Package objects
Returns a generator, yielding Package objects for each package
found in the bundle.
"""
for package in self.package_specs():
try:
pkg_obj = Package.from_location(
package['Name'],
version=package.get('Version'),
url=package.get('Url'),
path=path,
base_url=base_url,
)
except Exception as e:
LOG.error("Error {0} occurred while obtaining "
"package {1}".format(e, package['Name']))
continue
yield pkg_obj
class DummyYaqlYamlLoader(yaml.SafeLoader):
"""Constructor that treats !yaql as string."""
pass
DummyYaqlYamlLoader.add_constructor(
u'!yaql', DummyYaqlYamlLoader.yaml_constructors[u'tag:yaml.org,2002:str'])
class YaqlYamlLoader(yaml.SafeLoader):
pass
# workaround for PyYAML bug: http://pyyaml.org/ticket/221
resolvers = {}
for k, v in yaml.SafeLoader.yaml_implicit_resolvers.items():
resolvers[k] = v[:]
YaqlYamlLoader.yaml_implicit_resolvers = resolvers
def yaql_constructor(loader, node):
value = loader.construct_scalar(node)
return YaqlExpression(value)
YaqlYamlLoader.add_constructor(u'!yaql', yaql_constructor)
YaqlYamlLoader.add_implicit_resolver(u'!yaql', YaqlExpression, None)
def traverse_and_replace(obj,
pattern=re.compile(r'^===id(\d+)===$'),
replacements=None):
"""Helper function that traverses object model and substitutes ids.
Recursively checks values of objects found in `obj` against `pattern`,
and replaces strings that match pattern with uuidutils.generate_uuid().
Keeps track of any replacements already made, i.e. ===id1=== would
always be the same, across `obj`. Uses 1st group, found in the `pattern`
regexp as unique identifier of a replacement
"""
if replacements is None:
replacements = collections.defaultdict(
lambda: uuidutils.generate_uuid(dashed=False))
def _maybe_replace(obj, key, value):
"""Check and replace value against pattern"""
if isinstance(value, str):
m = pattern.search(value)
if m:
if m.group(1) not in replacements:
replacements[m.group(1)] = uuidutils.generate_uuid(
dashed=False)
obj[key] = replacements[m.group(1)]
if isinstance(obj, list):
for key, value in enumerate(obj):
if isinstance(value, (list, dict)):
traverse_and_replace(value, pattern, replacements)
else:
_maybe_replace(obj, key, value)
elif isinstance(obj, dict):
for key, value in obj.items():
if isinstance(value, (list, dict)):
traverse_and_replace(value, pattern, replacements)
else:
_maybe_replace(obj, key, value)
else:
_maybe_replace(obj, key, value)
class NamespaceResolver(object):
"""Copied from main murano repo
original at murano/dsl/namespace_resolver.py
"""
def __init__(self, namespaces):
self._namespaces = namespaces
self._namespaces[''] = ''
def resolve_name(self, name, relative=None):
if name is None:
raise ValueError()
if name and name.startswith(':'):
return name[1:]
if ':' in name:
parts = name.split(':')
if len(parts) != 2 or not parts[1]:
raise NameError('Incorrectly formatted name ' + name)
if parts[0] not in self._namespaces:
raise KeyError('Unknown namespace prefix ' + parts[0])
return '.'.join((self._namespaces[parts[0]], parts[1]))
if not relative and '=' in self._namespaces and '.' not in name:
return '.'.join((self._namespaces['='], name))
if relative and '.' not in name:
return '.'.join((relative, name))
return name

View File

@ -1,60 +0,0 @@
# 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 re
import yaql
from yaql.language import exceptions as yaql_exc
def _set_up_yaql():
legacy_engine_options = {
'yaql.limitIterators': 10000,
'yaql.memoryQuota': 1000000
}
return yaql.YaqlFactory().create(options=legacy_engine_options)
YAQL = _set_up_yaql()
class YaqlExpression(object):
def __init__(self, expression):
self._expression = str(expression)
self._parsed_expression = YAQL(self._expression)
def expression(self):
return self._expression
def __repr__(self):
return 'YAQL(%s)' % self._expression
def __str__(self):
return self._expression
@staticmethod
def match(expr):
if not isinstance(expr, str):
return False
if re.match(r'^[\s\w\d.:]*$', expr):
return False
try:
YAQL(expr)
return True
except yaql_exc.YaqlGrammarException:
return False
except yaql_exc.YaqlLexicalException:
return False
def evaluate(self, data=yaql.utils.NO_VALUE, context=None):
return self._parsed_expression.evaluate(data=data, context=context)

View File

@ -1,48 +0,0 @@
# 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 re
import yaql
class YaqlExpression(object):
def __init__(self, expression):
self._expression = str(expression)
self._parsed_expression = yaql.parse(self._expression)
def expression(self):
return self._expression
def __repr__(self):
return 'YAQL(%s)' % self._expression
def __str__(self):
return self._expression
@staticmethod
def match(expr):
if not isinstance(expr, str):
return False
if re.match(r'^[\s\w\d.:]*$', expr):
return False
try:
yaql.parse(expr)
return True
except yaql.exceptions.YaqlGrammarException:
return False
except yaql.exceptions.YaqlLexicalException:
return False
def evaluate(self, data=None, context=None):
return self._parsed_expression.evaluate(data=data, context=context)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.0 KiB

View File

@ -1,60 +0,0 @@
# Copyright (c) 2015 Mirantis, 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 glanceclient import exc
class ArtifactType(object):
generic_properties = ('created_at', 'id', 'name', 'owner', 'state',
'type_name', 'type_version', 'updated_at',
'version', 'visibility', 'description', 'tags',
'published_at', 'deleted_at')
supported_show_levels = ('none', 'basic', 'direct', 'transitive')
def __init__(self, **kwargs):
try:
for prop in self.generic_properties:
setattr(self, prop, kwargs.pop(prop))
except KeyError:
msg = "Invalid parameters were provided"
raise exc.HTTPBadRequest(msg)
self.type_specific_properties = {}
for key, value in kwargs.items():
try:
if _is_dependency(value):
self.type_specific_properties[key] = ArtifactType(**value)
elif _is_dependencies_list(value):
self.type_specific_properties[key] = [ArtifactType(**elem)
for elem in value]
else:
self.type_specific_properties[key] = value
except exc.HTTPBadRequest:
# if it's not possible to generate artifact object then
# assign the value as a regular dict.
self.type_specific_properties[key] = value
def _is_dependency(d):
if type(d) is dict and d.get('type_name') and d.get('type_version'):
return True
return False
def _is_dependencies_list(l):
if type(l) is list and all(_is_dependency(d) for d in l):
return True
return False

View File

@ -1,360 +0,0 @@
# Copyright (c) 2015 Mirantis, 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 glanceclient.common import utils
from glanceclient import exc
from oslo_utils import encodeutils
from urllib import parse
from muranoclient.glance import ArtifactType
glare_urls = {
'create': '/v{version}/artifacts/{type_name}/v{type_version}/drafts',
'update_get_delete': '/v{version}/artifacts/{type_name}/v{type_version}'
'/{artifact_id}',
'list_drafts': '/v{version}/artifacts/{type_name}/v{type_version}/drafts?',
'list_no_drafts': '/v{version}/artifacts/{type_name}/v{type_version}?',
'publish': '/v{version}/artifacts/{type_name}/v{type_version}/'
'{artifact_id}/publish',
'blob': '/v{version}/artifacts/{type_name}/v{type_version}/{artifact_id}'
'/{blob_property}',
}
class Controller(object):
def __init__(self, http_client, type_name=None, type_version=None,
version='0.1'):
self.http_client = http_client
self.type_name = type_name
self.type_version = type_version
self.version = version
self.default_page_size = 20
self.sort_dir_values = ('asc', 'desc')
def _check_type_params(self, type_name, type_version):
"""Check that type name and type versions were specified"""
type_name = type_name or self.type_name
type_version = type_version or self.type_version
if type_name is None:
msg = "Type name must be specified"
raise exc.HTTPBadRequest(msg)
if type_version is None:
msg = "Type version must be specified"
raise exc.HTTPBadRequest(msg)
return type_name, type_version
def _validate_sort_param(self, sort):
"""Validates sorting argument for invalid keys and directions values.
:param sort: comma-separated list of sort keys with optional <:dir>
after each key
"""
for sort_param in sort.strip().split(','):
key, _sep, dir = sort_param.partition(':')
if dir and dir not in self.sort_dir_values:
msg = ('Invalid sort direction: %(sort_dir)s.'
' It must be one of the following: %(available)s.'
) % {'sort_dir': dir,
'available': ', '.join(self.sort_dir_values)}
raise exc.HTTPBadRequest(msg)
return sort
def create(self, name, version, type_name=None, type_version=None,
**kwargs):
"""Create an artifact of given type and version.
:param name: name of creating artifact.
:param version: semver string describing an artifact version
"""
type_name, type_version = self._check_type_params(type_name,
type_version)
kwargs.update({'name': name, 'version': version})
url = glare_urls['create'].format(version=self.version,
type_name=type_name,
type_version=type_version)
resp, body = self.http_client.post(url, data=kwargs)
return ArtifactType(**body)
def update(self, artifact_id, type_name=None, type_version=None,
remove_props=None, **kwargs):
"""Update attributes of an artifact.
:param artifact_id: ID of the artifact to modify.
:param remove_props: List of property names to remove
:param **kwargs: Artifact attribute names and their new values.
"""
type_name, type_version = self._check_type_params(type_name,
type_version)
url = glare_urls['update_get_delete'].format(version=self.version,
type_name=type_name,
type_version=type_version,
artifact_id=artifact_id)
hdrs = {
'Content-Type': 'application/openstack-images-v2.1-json-patch'}
artifact_obj = self.get(artifact_id, type_name, type_version)
changes = []
if remove_props:
for prop in remove_props:
if prop in ArtifactType.generic_properties:
msg = "Generic properties cannot be removed"
raise exc.HTTPBadRequest(msg)
if prop not in kwargs:
changes.append({'op': 'remove',
'path': '/' + prop})
for prop in kwargs:
if prop in artifact_obj.generic_properties:
op = 'add' if getattr(artifact_obj,
prop) is None else 'replace'
elif prop in artifact_obj.type_specific_properties:
if artifact_obj.type_specific_properties[prop] is None:
op = 'add'
else:
op = 'replace'
else:
msg = ("Property '%s' doesn't exist in type '%s' with version"
" '%s'" % (prop, type_name, type_version))
raise exc.HTTPBadRequest(msg)
changes.append({'op': op, 'path': '/' + prop,
'value': kwargs[prop]})
resp, body = self.http_client.patch(url, headers=hdrs, data=changes)
return ArtifactType(**body)
def get(self, artifact_id, type_name=None, type_version=None,
show_level=None):
"""Get information about an artifact.
:param artifact_id: ID of the artifact to get.
:param show_level: value of datalization. Possible values:
"none", "basic", "direct", "transitive"
"""
type_name, type_version = self._check_type_params(type_name,
type_version)
url = glare_urls['update_get_delete'].format(version=self.version,
type_name=type_name,
type_version=type_version,
artifact_id=artifact_id)
if show_level:
if show_level not in ArtifactType.supported_show_levels:
msg = "Invalid show level: %s" % show_level
raise exc.HTTPBadRequest(msg)
url += '?show_level=%s' % show_level
resp, body = self.http_client.get(url)
return ArtifactType(**body)
def list(self, **kwargs):
return self._list(drafts=False, **kwargs)
def drafts(self, **kwargs):
return self._list(drafts=True, **kwargs)
def _list(self, drafts, type_name=None, type_version=None, **kwargs):
"""Retrieve a listing of Image objects.
:param page_size: Number of images to request in each
paginated request.
:returns: generator over list of artifacts.
"""
type_name, type_version = self._check_type_params(type_name,
type_version)
limit = kwargs.get('limit')
page_size = kwargs.get('page_size') or self.default_page_size
def paginate(url, page_size, limit=None):
next_url = url
while True:
if limit and page_size > limit:
next_url = next_url.replace("limit=%s" % page_size,
"limit=%s" % limit)
resp, body = self.http_client.get(next_url)
for artifact in body['artifacts']:
yield ArtifactType(**artifact)
if limit:
limit -= 1
if limit <= 0:
raise StopIteration
try:
next_url = body['next']
except KeyError:
return
filters = kwargs.get('filters', {})
filters['limit'] = page_size
url_params = []
for param, items in filters.items():
values = [items] if not isinstance(items, list) else items
for value in values:
if isinstance(value, str):
value = encodeutils.safe_encode(value)
url_params.append({param: value})
if drafts:
url = glare_urls['list_drafts'].format(version=self.version,
type_name=type_name,
type_version=type_version)
else:
url = glare_urls['list_no_drafts'].format(
version=self.version,
type_name=type_name,
type_version=type_version
)
for param in url_params:
url = '%s&%s' % (url, parse.urlencode(param))
if 'sort' in kwargs:
url = '%s&sort=%s' % (url, self._validate_sort_param(
kwargs['sort']))
for artifact in paginate(url, page_size, limit):
yield artifact
def active(self, artifact_id, type_name=None, type_version=None):
"""Set artifact status to 'active'.
:param artifact_id: ID of the artifact to get.
"""
type_name, type_version = self._check_type_params(type_name,
type_version)
url = glare_urls['publish'].format(version=self.version,
type_name=type_name,
type_version=type_version,
artifact_id=artifact_id)
resp, body = self.http_client.post(url)
return ArtifactType(**body)
def deactivate(self, artifact_id, type_name=None, type_version=None):
raise NotImplementedError()
def delete(self, artifact_id, type_name=None, type_version=None):
"""Delete an artifact and all its data.
:param artifact_id: ID of the artifact to delete.
"""
type_name, type_version = self._check_type_params(type_name,
type_version)
url = glare_urls['update_get_delete'].format(version=self.version,
type_name=type_name,
type_version=type_version,
artifact_id=artifact_id)
self.http_client.delete(url)
def upload_blob(self, artifact_id, blob_property, data, position=None,
type_name=None, type_version=None):
"""Upload blob data.
:param artifact_id: ID of the artifact to download a blob
:param blob_property: blob property name
:param position: if blob_property is a list then the
position must be specified
"""
type_name, type_version = self._check_type_params(type_name,
type_version)
hdrs = {'Content-Type': 'application/octet-stream'}
url = glare_urls['blob'].format(version=self.version,
type_name=type_name,
type_version=type_version,
artifact_id=artifact_id,
blob_property=blob_property)
if position:
url += "/%s" % position
self.http_client.put(url, headers=hdrs, data=data)
def download_blob(self, artifact_id, blob_property, position=None,
type_name=None, type_version=None, do_checksum=True):
"""Get blob data.
:param artifact_id: ID of the artifact to download a blob
:param blob_property: blob property name
:param position: if blob_property is a list then the
position must be specified
:param do_checksum: Enable/disable checksum validation.
"""
type_name, type_version = self._check_type_params(type_name,
type_version)
url = glare_urls['blob'].format(version=self.version,
type_name=type_name,
type_version=type_version,
artifact_id=artifact_id,
blob_property=blob_property)
if position:
url += '/%s' % position
url += '/download'
resp, body = self.http_client.get(url)
checksum = resp.headers.get('content-md5', None)
content_length = int(resp.headers.get('content-length', 0))
if checksum is not None and do_checksum:
body = utils.integrity_iter(body, checksum)
return utils.IterableWithLength(body, content_length)
def delete_blob(self, artifact_id, blob_property, position=None,
type_name=None, type_version=None):
"""Delete blob and related data.
:param artifact_id: ID of the artifact to delete a blob
:param blob_property: blob property name
:param position: if blob_property is a list then the
position must be specified
"""
type_name, type_version = self._check_type_params(type_name,
type_version)
url = glare_urls['blob'].format(version=self.version,
type_name=type_name,
type_version=type_version,
artifact_id=artifact_id,
blob_property=blob_property)
if position:
url += '/%s' % position
self.http_client.delete(url)
def add_property(self, artifact_id, dependency_id, position=None,
type_name=None, type_version=None):
raise NotImplementedError()
def replace_property(self, artifact_id, dependency_id, position=None,
type_name=None, type_version=None):
raise NotImplementedError()
def remove_property(self, artifact_id, dependency_id, position=None,
type_name=None, type_version=None):
raise NotImplementedError()
def artifact_export(self, artifact_id,
type_name=None, type_version=None):
raise NotImplementedError()
def artifact_import(self, data, type_name=None, type_version=None):
raise NotImplementedError()

View File

@ -1,42 +0,0 @@
# Copyright (c) 2015 Mirantis, 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 glanceclient.common import http
from glanceclient.common import utils
from muranoclient.glance import artifacts
class Client(object):
"""Client for the OpenStack glance-glare API.
:param string endpoint: A user-supplied endpoint URL for the glance
service.
:param string token: Token for authentication.
:param integer timeout: Allows customization of the timeout for client
http requests. (optional)
"""
def __init__(self, endpoint, type_name, type_version, **kwargs):
endpoint, version = utils.strip_version(endpoint)
self.version = version or '0.1'
self.http_client = http.HTTPClient(endpoint, **kwargs)
self.type_name = type_name
self.type_version = type_version
self.artifacts = artifacts.Controller(self.http_client,
self.type_name,
self.type_version,
self.version)

View File

@ -1,24 +0,0 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
"""oslo.i18n integration module.
See https://docs.openstack.org/oslo.i18n/latest/user/index.html
"""
import oslo_i18n
_translators = oslo_i18n.TranslatorFactory(domain='muranoclient')
# The primary translation function using the well-known name "_"
_ = _translators.primary

View File

@ -1,153 +0,0 @@
# Andi Chandler <andi@gowling.com>, 2017. #zanata
msgid ""
msgstr ""
"Project-Id-Version: python-muranoclient VERSION\n"
"Report-Msgid-Bugs-To: https://bugs.launchpad.net/openstack-i18n/\n"
"POT-Creation-Date: 2018-01-30 01:30+0000\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"PO-Revision-Date: 2017-10-21 09:07+0000\n"
"Last-Translator: Andi Chandler <andi@gowling.com>\n"
"Language-Team: English (United Kingdom)\n"
"Language: en_GB\n"
"X-Generator: Zanata 4.3.3\n"
"Plural-Forms: nplurals=2; plural=(n != 1)\n"
#, python-format
msgid "AmbiguousEndpoints: %s"
msgstr "AmbiguousEndpoints: %s"
msgid ""
"Application catalog API version, default={0}(Env:"
"OS_APPLICATION_CATALOG_API_VERSION)"
msgstr ""
"Application catalogue API version, default={0}(Env:"
"OS_APPLICATION_CATALOG_API_VERSION)"
#, python-format
msgid "AuthSystemNotFound: %s"
msgstr "AuthSystemNotFound: %s"
#, python-format
msgid "Authentication failed. Missing options: %s"
msgstr "Authentication failed. Missing options: %s"
msgid "Bad Gateway"
msgstr "Bad Gateway"
msgid "Bad Request"
msgstr "Bad Request"
msgid "Cannot find endpoint or token for request"
msgstr "Cannot find endpoint or token for request"
msgid "Conflict"
msgstr "Conflict"
msgid "Defaults to env[MURANO_URL]."
msgstr "Defaults to env[MURANO_URL]."
msgid "Error {0} occurred while setting image {1} public"
msgstr "Error {0} occurred while setting image {1} public"
msgid "Expectation Failed"
msgstr "Expectation Failed"
msgid "Forbidden"
msgstr "Forbidden"
msgid "Gateway Timeout"
msgstr "Gateway Timeout"
msgid "Gone"
msgstr "Gone"
msgid "HTTP Client Error"
msgstr "HTTP Client Error"
msgid "HTTP Error"
msgstr "HTTP Error"
msgid "HTTP Redirection"
msgstr "HTTP Redirection"
msgid "HTTP Server Error"
msgstr "HTTP Server Error"
msgid "HTTP Version Not Supported"
msgstr "HTTP Version Not Supported"
msgid "Internal Server Error"
msgstr "Internal Server Error"
#, python-format
msgid ""
"Invalid %(api_name)s client version '%(version)s'. Must be one of: "
"%(version_map)s"
msgstr ""
"Invalid %(api_name)s client version '%(version)s'. Must be one of: "
"%(version_map)s"
msgid "Length Required"
msgstr "Length Required"
msgid "Method Not Allowed"
msgstr "Method Not Allowed"
#, python-format
msgid "Missing arguments: %s"
msgstr "Missing arguments: %s"
msgid "Multiple Choices"
msgstr "Multiple Choices"
#, python-format
msgid "No %(name)s matching %(args)s."
msgstr "No %(name)s matching %(args)s."
msgid "Not Acceptable"
msgstr "Not Acceptable"
msgid "Not Found"
msgstr "Not Found"
msgid "Not Implemented"
msgstr "Not Implemented"
msgid "Payment Required"
msgstr "Payment Required"
msgid "Precondition Failed"
msgstr "Precondition Failed"
msgid "Proxy Authentication Required"
msgstr "Proxy Authentication Required"
msgid "Request Entity Too Large"
msgstr "Request Entity Too Large"
msgid "Request Timeout"
msgstr "Request Timeout"
msgid "Request-URI Too Long"
msgstr "Request-URI Too Long"
msgid "Requested Range Not Satisfiable"
msgstr "Requested Range Not Satisfiable"
msgid "Service Unavailable"
msgstr "Service Unavailable"
#, python-format
msgid "Some attributes are missing in %(pkg_name)s: %(attrs)s."
msgstr "Some attributes are missing in %(pkg_name)s: %(attrs)s."
msgid "Unauthorized"
msgstr "Unauthorised"
msgid "Unprocessable Entity"
msgstr "Unprocessable Entity"
msgid "Unsupported Media Type"
msgstr "Unsupported Media Type"

View File

@ -1,114 +0,0 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
"""OpenStackClient plugin for Application Catalog service."""
from osc_lib import utils
from oslo_log import log as logging
from muranoclient.apiclient import exceptions as exc
from muranoclient.glance import client as art_client
from muranoclient.i18n import _
LOG = logging.getLogger(__name__)
DEFAULT_APPLICATION_CATALOG_API_VERSION = "1"
API_VERSION_OPTION = "os_application_catalog_api_version"
API_NAME = "application_catalog"
API_VERSIONS = {
'1': 'muranoclient.v1.client.Client',
}
def make_client(instance):
"""Returns an application-catalog service client"""
application_catalog_client = utils.get_client_class(
API_NAME,
instance._api_version[API_NAME],
API_VERSIONS)
LOG.debug("Instantiating application-catalog client: {0}".format(
application_catalog_client))
kwargs = {
'session': instance.session,
'service_type': 'application-catalog',
'region_name': instance._region_name
}
murano_packages_service = \
instance.get_configuration().get('murano_packages_service')
if murano_packages_service == 'glare':
glare_endpoint = instance.get_configuration().get('glare_url')
if not glare_endpoint:
try:
# no glare_endpoint and we requested to store packages in glare
# check keystone catalog
glare_endpoint = instance.get_endpoint_for_service_type(
'artifact',
region_name=instance._region_name,
interface=instance._interface
)
except Exception:
raise exc.CommandError(
"You set murano-packages-service to {}"
" but there is not 'artifact' endpoint in keystone"
" Either register one or specify endpoint "
" via either --glare-url or env[GLARE_API]".format(
murano_packages_service))
artifacts_client = art_client.Client(
endpoint=glare_endpoint,
type_name='murano',
type_version=1,
token=instance.auth_ref.auth_token)
kwargs['artifacts_client'] = artifacts_client
murano_endpoint = instance.get_configuration().get('murano_url')
if not murano_endpoint:
murano_endpoint = instance.get_endpoint_for_service_type(
'application-catalog',
region_name=instance._region_name,
interface=instance._interface
)
client = application_catalog_client(murano_endpoint, **kwargs)
return client
def build_option_parser(parser):
"""Hook to add global options"""
parser.add_argument(
'--os-application-catalog-api-version',
metavar='<application-catalog-api-version>',
default=utils.env(
'OS_APPLICATION_CATALOG_API_VERSION',
default=DEFAULT_APPLICATION_CATALOG_API_VERSION),
help=_("Application catalog API version, default={0}"
"(Env:OS_APPLICATION_CATALOG_API_VERSION)").format(
DEFAULT_APPLICATION_CATALOG_API_VERSION))
parser.add_argument('--murano-url',
default=utils.env('MURANO_URL'),
help=_('Defaults to env[MURANO_URL].'))
parser.add_argument('--glare-url',
default=utils.env('GLARE_URL'),
help='Defaults to env[GLARE_URL].')
parser.add_argument('--murano-packages-service',
choices=['murano', 'glare'],
default=utils.env('MURANO_PACKAGES_SERVICE',
default='murano'),
help='Specifies if murano-api ("murano") or '
'Glance Artifact Repository ("glare") '
'should be used to store murano packages. '
'Defaults to env[MURANO_PACKAGES_SERVICE] or '
'to "murano"')
return parser

View File

@ -1,90 +0,0 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
"""Application-catalog v1 action implementation"""
import json
from osc_lib.command import command
from oslo_log import log as logging
from muranoclient.apiclient import exceptions
LOG = logging.getLogger(__name__)
class StaticActionCall(command.ShowOne):
"""Call static method of the MuranoPL class."""
def get_parser(self, prog_name):
parser = super(StaticActionCall, self).get_parser(prog_name)
parser.add_argument(
"class_name",
metavar='<CLASS>',
help="FQN of the class with static method",
)
parser.add_argument(
"method_name",
metavar='<METHOD>',
help="Static method to run",
)
parser.add_argument(
"--arguments",
metavar='<KEY=VALUE>',
nargs='*',
help="Method arguments. No arguments by default",
)
parser.add_argument(
"--package-name",
metavar='<PACKAGE>',
default='',
help='Optional FQN of the package to look for the class in',
)
parser.add_argument(
"--class-version",
default='',
help='Optional version of the class, otherwise version =0 is '
'used ',
)
return parser
def take_action(self, parsed_args):
LOG.debug("take_action({0})".format(parsed_args))
client = self.app.client_manager.application_catalog
arguments = {}
for argument in parsed_args.arguments or []:
if '=' not in argument:
raise exceptions.CommandError(
"Argument should be in form of KEY=VALUE. Found: "
"{0}".format(argument))
key, value = argument.split('=', 1)
try:
value = json.loads(value)
except ValueError:
# treat value as a string if it doesn't load as json
pass
arguments[key] = value
request_body = {
"className": parsed_args.class_name,
"methodName": parsed_args.method_name,
"packageName": parsed_args.package_name or None,
"classVersion": parsed_args.class_version or '=0',
"parameters": arguments
}
print("Waiting for result...")
result = client.static_actions.call(request_body).get_result()
return ["Static action result"], [result]

View File

@ -1,151 +0,0 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
"""Application-catalog v1 category action implementation"""
import textwrap
from osc_lib.command import command
from osc_lib import utils
from oslo_log import log as logging
from muranoclient.apiclient import exceptions
LOG = logging.getLogger(__name__)
class ListCategories(command.Lister):
"""List all available categories."""
def take_action(self, parsed_args=None):
LOG.debug("take_action({0})".format(parsed_args))
client = self.app.client_manager.application_catalog
if parsed_args is None:
parsed_args = {}
data = client.categories.list()
fields = ["id", "name"]
field_labels = ["ID", "Name"]
return (
field_labels,
list(utils.get_item_properties(
s,
fields,
) for s in data)
)
class ShowCategory(command.ShowOne):
"""Display category details."""
def get_parser(self, prog_name):
parser = super(ShowCategory, self).get_parser(prog_name)
parser.add_argument(
"id",
metavar="<ID>",
help=("ID of a category(s) to show."),
)
return parser
def take_action(self, parsed_args):
LOG.debug("take_action({0})".format(parsed_args))
client = self.app.client_manager.application_catalog
category = client.categories.get(parsed_args.id)
packages = client.packages.filter(category=category.name)
to_display = dict(id=category.id,
name=category.name,
packages=', '.join(p.name
for p in packages))
to_display['packages'] = '\n'.join(textwrap.wrap(to_display['packages']
or '', 55))
return self.dict2columns(to_display)
class CreateCategory(command.Lister):
"""Create a category."""
def get_parser(self, prog_name):
parser = super(CreateCategory, self).get_parser(prog_name)
parser.add_argument(
"name",
metavar="<CATEGORY_NAME>",
help=("Category name."),
)
return parser
def take_action(self, parsed_args):
LOG.debug("take_action({0})".format(parsed_args))
client = self.app.client_manager.application_catalog
data = [client.categories.add({"name": parsed_args.name})]
fields = ["id", "name"]
field_labels = ["ID", "Name"]
return (
field_labels,
list(utils.get_item_properties(
s,
fields,
) for s in data)
)
class DeleteCategory(command.Lister):
"""Delete a category."""
def get_parser(self, prog_name):
parser = super(DeleteCategory, self).get_parser(prog_name)
parser.add_argument(
"id",
metavar="<ID>",
nargs="+",
help=("ID of a category(ies) to delete."),
)
return parser
def take_action(self, parsed_args):
LOG.debug("take_action({0})".format(parsed_args))
client = self.app.client_manager.application_catalog
failure_count = 0
for category_id in parsed_args.id:
try:
client.categories.delete(category_id)
except Exception:
failure_count += 1
print("Failed to delete '{0}'; category not found".
format(category_id))
if failure_count == len(parsed_args.id):
raise exceptions.CommandError("Unable to find and delete any of "
"the specified categories.")
data = client.categories.list()
fields = ["id", "name"]
field_labels = ["ID", "Name"]
return (
field_labels,
list(utils.get_item_properties(
s,
fields,
) for s in data)
)

View File

@ -1,74 +0,0 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
"""Application-catalog v1 deployments action implementation"""
from osc_lib.command import command
from osc_lib import utils
from oslo_log import log as logging
from muranoclient.apiclient import exceptions
LOG = logging.getLogger(__name__)
class ListDeployment(command.Lister):
"""List deployments for an environment."""
def get_parser(self, prog_name):
parser = super(ListDeployment, self).get_parser(prog_name)
parser.add_argument(
"id",
metavar="<ID>",
nargs="?",
default=None,
help=("Environment ID for which to list deployments."),
)
parser.add_argument(
"--all-environments",
action="store_true",
default=False,
help="List all deployments for all environments in user's tenant."
)
return parser
def take_action(self, parsed_args):
LOG.debug("take_action({0})".format(parsed_args))
client = self.app.client_manager.application_catalog
all_environments = getattr(parsed_args, 'all_environments', False)
env_id = getattr(parsed_args, 'id', None)
if env_id and all_environments:
raise exceptions.CommandError(
'Environment ID and all-environments flag cannot both be set.')
elif not env_id and not all_environments:
raise exceptions.CommandError(
'Either environment ID or all-environments flag must be set.')
if all_environments:
data = client.deployments.list(None, all_environments)
else:
environment = utils.find_resource(client.environments,
env_id)
data = client.deployments.list(environment.id)
columns = ('id', 'state', 'created', 'updated', 'finished')
column_headers = [c.capitalize() for c in columns]
return (
column_headers,
list(utils.get_item_properties(
s,
columns,
) for s in data)
)

View File

@ -1,494 +0,0 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
"""Application-catalog v1 stack action implementation"""
import json
import sys
import urllib
import jsonpatch
from osc_lib.command import command
from osc_lib import utils
from oslo_log import log as logging
from oslo_serialization import jsonutils
from oslo_utils import uuidutils
from muranoclient.apiclient import exceptions
from muranoclient.common import utils as murano_utils
LOG = logging.getLogger(__name__)
class ListEnvironments(command.Lister):
"""Lists environments"""
def get_parser(self, prog_name):
parser = super(ListEnvironments, self).get_parser(prog_name)
parser.add_argument(
'--all-tenants',
action='store_true',
default=False,
help='List environments from all tenants (admin only).',
)
parser.add_argument(
'--tenant',
metavar='<TENANT_ID>',
default=None,
help='Allows to list environments for a given tenant (admin only).'
)
return parser
def take_action(self, parsed_args):
LOG.debug("take_action({0})".format(parsed_args))
client = self.app.client_manager.application_catalog
data = client.environments.list(
parsed_args.all_tenants, parsed_args.tenant)
columns = ('id', 'name', 'status', 'created', 'updated')
column_headers = [c.capitalize() for c in columns]
return (
column_headers,
list(utils.get_item_properties(
s,
columns,
) for s in data)
)
class ShowEnvironment(command.ShowOne):
"""Display environment details"""
def get_parser(self, prog_name):
parser = super(ShowEnvironment, self).get_parser(prog_name)
parser.add_argument(
"id",
metavar="<NAME or ID>",
help=("Name or ID of the environment to display"),
)
parser.add_argument(
"--only-apps",
action='store_true',
default=False,
help="Only print apps of the environment (useful for automation).",
)
parser.add_argument(
"--session-id",
metavar="<SESSION_ID>",
default='',
help="Id of a config session.",
)
return parser
def take_action(self, parsed_args):
LOG.debug("take_action({0})".format(parsed_args))
client = self.app.client_manager.application_catalog
environment = utils.find_resource(client.environments,
parsed_args.id)
data = client.environments.get(environment.id,
parsed_args.session_id).to_dict()
data['services'] = jsonutils.dumps(data['services'], indent=2)
if getattr(parsed_args, 'only_apps', False):
return(['services'], [data['services']])
else:
return self.dict2columns(data)
class RenameEnvironment(command.Lister):
"""Rename an environment."""
def get_parser(self, prog_name):
parser = super(RenameEnvironment, self).get_parser(prog_name)
parser.add_argument(
'id',
metavar="<NAME or ID>",
help="Environment ID or name.",
)
parser.add_argument(
'name',
metavar="<ENVIRONMENT_NAME>",
help="A name to which the environment will be renamed.",
)
return parser
def take_action(self, parsed_args):
LOG.debug("take_action({0})".format(parsed_args))
client = self.app.client_manager.application_catalog
environment = utils.find_resource(client.environments,
parsed_args.id)
data = client.environments.update(environment.id,
parsed_args.name)
columns = ('id', 'name', 'status', 'created', 'updated')
column_headers = [c.capitalize() for c in columns]
return (
column_headers,
[utils.get_item_properties(
data,
columns,
)]
)
class EnvironmentSessionCreate(command.ShowOne):
"""Creates a new configuration session for environment ID."""
def get_parser(self, prog_name):
parser = super(EnvironmentSessionCreate, self).get_parser(prog_name)
parser.add_argument(
'id',
metavar="<ID>",
help="ID of Environment to add session to.",
)
return parser
def take_action(self, parsed_args):
LOG.debug("take_action({0})".format(parsed_args))
client = self.app.client_manager.application_catalog
environment_id = parsed_args.id
session_id = client.sessions.configure(environment_id).id
sessionid = murano_utils.text_wrap_formatter(session_id)
return (['id'], [sessionid])
class EnvironmentCreate(command.Lister):
"""Create an environment."""
def get_parser(self, prog_name):
parser = super(EnvironmentCreate, self).get_parser(prog_name)
parser.add_argument(
'name',
metavar="<ENVIRONMENT_ID>",
help="Environment name.",
)
parser.add_argument(
'--region',
metavar="<REGION_NAME>",
help="Name of the target OpenStack region.",
)
parser.add_argument(
'--join-subnet-id',
metavar="<SUBNET_ID>",
help="Subnetwork id to join.",
)
parser.add_argument(
'--join-net-id',
metavar="<NET_ID>",
help="Network id to join.",
)
return parser
def take_action(self, parsed_args):
LOG.debug("take_action({0})".format(parsed_args))
client = self.app.client_manager.application_catalog
body = {"name": parsed_args.name, "region": parsed_args.region}
if parsed_args.join_net_id or parsed_args.join_subnet_id:
res = {
'defaultNetworks': {
'environment': {
'?': {
'id': uuidutils.generate_uuid(dashed=False),
'type':
'io.murano.resources.ExistingNeutronNetwork'
},
},
'flat': None
}
}
if parsed_args.join_net_id:
res['defaultNetworks']['environment']['internalNetworkName'] \
= parsed_args.join_net_id
if parsed_args.join_subnet_id:
res['defaultNetworks']['environment']['internalSubnetworkName'
] = \
parsed_args.join_subnet_id
body.update(res)
data = client.environments.create(body)
columns = ('id', 'name', 'status', 'created', 'updated')
column_headers = [c.capitalize() for c in columns]
return (
column_headers,
[utils.get_item_properties(
data,
columns,
)]
)
class EnvironmentDelete(command.Lister):
"""Delete an environment."""
def get_parser(self, prog_name):
parser = super(EnvironmentDelete, self).get_parser(prog_name)
parser.add_argument(
'id',
metavar="<NAME or ID>",
nargs="+",
help="Id or name of environment(s) to delete.",
)
parser.add_argument(
'--abandon',
action='store_true',
default=False,
help="If set will abandon environment without deleting any of its"
" resources.",
)
return parser
def take_action(self, parsed_args):
LOG.debug("take_action({0})".format(parsed_args))
client = self.app.client_manager.application_catalog
abandon = getattr(parsed_args, 'abandon', False)
failure_count = 0
for environment_id in parsed_args.id:
try:
environment = murano_utils.find_resource(client.environments,
environment_id)
client.environments.delete(environment.id, abandon)
except exceptions.NotFound:
failure_count += 1
print("Failed to delete '{0}'; environment not found".
format(environment_id))
if failure_count == len(parsed_args.id):
raise exceptions.CommandError("Unable to find and delete any of "
"the specified environments.")
data = client.environments.list()
columns = ('id', 'name', 'status', 'created', 'updated')
column_headers = [c.capitalize() for c in columns]
return (
column_headers,
list(utils.get_item_properties(
s,
columns,
) for s in data)
)
class EnvironmentDeploy(command.ShowOne):
"""Start deployment of a murano environment session."""
def get_parser(self, prog_name):
parser = super(EnvironmentDeploy, self).get_parser(prog_name)
parser.add_argument(
'id',
metavar="<ENVIRONMENT_ID>",
help="ID of Environment to deploy.",
)
parser.add_argument(
'--session-id',
metavar="<SESSION>",
help="ID of configuration session to deploy.",
)
return parser
def take_action(self, parsed_args):
LOG.debug("take_action({0})".format(parsed_args))
client = self.app.client_manager.application_catalog
client.sessions.deploy(parsed_args.id, parsed_args.session_id)
environment = utils.find_resource(client.environments,
parsed_args.id)
data = client.environments.get(environment.id,
parsed_args.session_id).to_dict()
data['services'] = jsonutils.dumps(data['services'], indent=2)
if getattr(parsed_args, 'only_apps', False):
return(['services'], [data['services']])
else:
return self.dict2columns(data)
class EnvironmentAppsEdit(command.Command):
"""Edit environment's services list.
`FILE` is path to a file, that contains jsonpatch, that describes changes
to be made to environment's object-model.
[
{ "op": "add", "path": "/-",
"value": { ... your-app object model here ... }
},
{ "op": "replace", "path": "/0/?/name",
"value": "new_name"
},
]
NOTE: Values '===id1===', '===id2===', etc. in the resulting object-model
will be substituted with uuids.
For more info on jsonpatch see RFC 6902
"""
def get_parser(self, prog_name):
parser = super(EnvironmentAppsEdit, self).get_parser(prog_name)
parser.add_argument(
'id',
metavar="<ENVIRONMENT_ID>",
help="ID of Environment to edit.",
)
parser.add_argument(
'filename',
metavar="<FILE>",
help="File to read jsonpatch from (defaults to stdin).",
)
parser.add_argument(
'--session-id',
metavar="<SESSION>",
help="ID of configuration session to edit.",
)
return parser
def take_action(self, parsed_args):
LOG.debug("take_action(%s)", parsed_args)
client = self.app.client_manager.application_catalog
jp_obj = None
if not parsed_args.filename:
jp_obj = json.load(sys.stdin)
else:
with open(parsed_args.filename) as fpatch:
jp_obj = json.load(fpatch)
jpatch = jsonpatch.JsonPatch(jp_obj)
environment_id = parsed_args.id
session_id = parsed_args.session_id
environment = client.environments.get(environment_id, session_id)
object_model = jpatch.apply(environment.services)
murano_utils.traverse_and_replace(object_model)
client.services.put(
environment_id,
path='/',
data=jpatch.apply(environment.services),
session_id=session_id)
class EnvironmentModelShow(command.ShowOne):
"""Show environment's object model."""
def get_parser(self, prog_name):
parser = super(EnvironmentModelShow, self).get_parser(prog_name)
parser.add_argument(
'id',
metavar="<ENVIRONMENT_ID>",
help="ID of Environment to show.",
)
parser.add_argument(
"--path",
metavar="<PATH>",
default='/',
help="Path to Environment model section. Defaults to '/'."
)
parser.add_argument(
'--session-id',
metavar="<SESSION_ID>",
help="Id of a config session.",
)
return parser
def take_action(self, parsed_args):
LOG.debug("take_action(%s)", parsed_args)
client = self.app.client_manager.application_catalog
session_id = parsed_args.session_id or None
path = urllib.parse.quote(parsed_args.path)
env_model = client.environments.get_model(parsed_args.id, path,
session_id)
return self.dict2columns(env_model)
class EnvironmentModelEdit(command.ShowOne):
"""Edit environment's object model."""
def get_parser(self, prog_name):
parser = super(EnvironmentModelEdit, self).get_parser(prog_name)
parser.add_argument(
'id',
metavar="<ENVIRONMENT_ID>",
help="ID of Environment to edit.",
)
parser.add_argument(
"filename",
metavar="<FILE>",
nargs="?",
help="File to read JSON-patch from (defaults to stdin)."
)
parser.add_argument(
'--session-id',
metavar="<SESSION_ID>",
help="Id of a config session.",
)
return parser
def take_action(self, parsed_args):
LOG.debug("take_action(%s)", parsed_args)
client = self.app.client_manager.application_catalog
jp_obj = None
if not parsed_args.filename:
jp_obj = json.load(sys.stdin)
else:
with open(parsed_args.filename) as fpatch:
jp_obj = json.load(fpatch)
if not isinstance(jp_obj, list):
raise exceptions.CommandError(
'JSON-patch must be a list of changes')
for change in jp_obj:
if 'op' not in change or 'path' not in change:
raise exceptions.CommandError(
'Every change in JSON-patch must contain "op" and "path" '
'keys')
op = change['op']
if op not in ['add', 'replace', 'remove']:
raise exceptions.CommandError('The value of "op" item must be '
'"add", "replace" or "remove", '
'got {0}'.format(op))
if op != 'remove' and 'value' not in change:
raise exceptions.CommandError('"add" or "replace" change in '
'JSON-patch must contain "value"'
' key')
session_id = parsed_args.session_id
new_model = client.environments.update_model(parsed_args.id, jp_obj,
session_id)
return self.dict2columns(new_model)

View File

@ -1,797 +0,0 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
"""Application-catalog v1 package action implementation"""
import collections
import functools
import itertools
import os
import shutil
import sys
import tempfile
import zipfile
from osc_lib.command import command
from osc_lib import exceptions as exc
from osc_lib import utils
from oslo_log import log as logging
from oslo_serialization import jsonutils
from oslo_utils import strutils
from muranoclient.apiclient import exceptions
from muranoclient.common import exceptions as common_exceptions
from muranoclient.common import utils as murano_utils
from muranoclient.v1.package_creator import hot_package
from muranoclient.v1.package_creator import mpl_package
LOG = logging.getLogger(__name__)
DEFAULT_REPO_URL = "http://apps.openstack.org/api/v1/murano_repo/liberty/"
_bool_from_str_strict = functools.partial(
strutils.bool_from_string, strict=True)
class CreatePackage(command.Command):
"""Create an application package."""
def get_parser(self, prog_name):
parser = super(CreatePackage, self).get_parser(prog_name)
parser.add_argument(
'-t', '--template',
metavar='<HEAT_TEMPLATE>',
help=("Path to the Heat template to import as "
"an Application Definition."),
)
parser.add_argument(
'-c', '--classes-dir',
metavar='<CLASSES_DIRECTORY>',
help=("Path to the directory containing application classes."),
)
parser.add_argument(
'-r', '--resources-dir',
metavar='<RESOURCES_DIRECTORY>',
help=("Path to the directory containing application resources."),
)
parser.add_argument(
'-n', '--name',
metavar='<DISPLAY_NAME>',
help=("Display name of the Application in Catalog."),
)
parser.add_argument(
'-f', '--full-name',
metavar='<full-name>',
help=("Fully-qualified name of the Application in Catalog."),
)
parser.add_argument(
'-a', '--author',
metavar='<AUTHOR>',
help=("Name of the publisher."),
)
parser.add_argument(
'--tags',
metavar='<TAG1 TAG2>',
nargs='*',
help=("A list of keywords connected to the application."),
)
parser.add_argument(
'-d', '--description',
metavar='<DESCRIPTION>',
help=("Detailed description for the Application in Catalog."),
)
parser.add_argument(
'-o', '--output',
metavar='<PACKAGE_NAME>',
help=("The name of the output file archive to save locally."),
)
parser.add_argument(
'-u', '--ui',
metavar='<UI_DEFINITION>',
help=("Dynamic UI form definition."),
)
parser.add_argument(
'--type',
metavar='<TYPE>',
help=("Package type. Possible values: Application or Library."),
)
parser.add_argument(
'-l', '--logo',
metavar='<LOGO>',
help=("Path to the package logo."),
)
return parser
def take_action(self, parsed_args):
LOG.debug("take_action({0})".format(parsed_args))
parsed_args.os_username = os.getenv('OS_USERNAME')
def _make_archive(archive_name, path):
zip_file = zipfile.ZipFile(archive_name, 'w')
for root, dirs, files in os.walk(path):
for f in files:
zip_file.write(os.path.join(root, f),
arcname=os.path.join(
os.path.relpath(root, path), f))
if parsed_args.template and parsed_args.classes_dir:
raise exc.CommandError(
"Provide --template for a HOT-based package, OR"
" --classes-dir for a MuranoPL-based package")
if not parsed_args.template and not parsed_args.classes_dir:
raise exc.CommandError(
"Provide --template for a HOT-based package, OR at least"
" --classes-dir for a MuranoPL-based package")
directory_path = None
try:
archive_name = parsed_args.output if parsed_args.output else None
if parsed_args.template:
directory_path = hot_package.prepare_package(parsed_args)
if not archive_name:
archive_name = os.path.basename(parsed_args.template)
archive_name = os.path.splitext(archive_name)[0] + ".zip"
else:
directory_path = mpl_package.prepare_package(parsed_args)
if not archive_name:
archive_name = tempfile.mkstemp(
prefix="murano_", dir=os.getcwd())[1] + ".zip"
_make_archive(archive_name, directory_path)
print("Application package is available at " +
os.path.abspath(archive_name))
finally:
if directory_path:
shutil.rmtree(directory_path)
class ListPackages(command.Lister):
"""List available packages."""
def get_parser(self, prog_name):
parser = super(ListPackages, self).get_parser(prog_name)
parser.add_argument(
"--limit",
type=int,
default=0,
help='Show limited number of packages'
)
parser.add_argument(
"--marker",
default='',
help='Show packages starting from package with id excluding it'
)
parser.add_argument(
"--include-disabled",
default=False,
action="store_true"
)
parser.add_argument(
"--owned",
default=False,
action="store_true"
)
parser.add_argument(
'--search',
metavar='<SEARCH_KEYS>',
dest='search',
required=False,
help='Show packages, that match search keys fuzzily'
)
parser.add_argument(
'--name',
metavar='<PACKAGE_NAME>',
dest='name',
required=False,
help='Show packages, whose name match parameter exactly'
)
parser.add_argument(
'--fqn',
metavar="<PACKAGE_FULLY_QUALIFIED_NAME>",
dest='fqn',
required=False,
help='Show packages, '
'whose fully qualified name match parameter exactly'
)
parser.add_argument(
'--type',
metavar='<PACKAGE_TYPE>',
dest='type',
required=False,
help='Show packages, whose type match parameter exactly'
)
parser.add_argument(
'--category',
metavar='<PACKAGE_CATEGORY>',
dest='category',
required=False,
help='Show packages, whose categories include parameter'
)
parser.add_argument(
'--class_name',
metavar='<PACKAGE_CLASS_NAME>',
dest='class_name',
required=False,
help='Show packages, whose class name match parameter exactly'
)
parser.add_argument(
'--tag',
metavar='<PACKAGE_TAG>',
dest='tag',
required=False,
help='Show packages, whose tags include parameter'
)
return parser
def take_action(self, parsed_args):
LOG.debug("take_action({0})".format(parsed_args))
client = self.app.client_manager.application_catalog
filter_args = {
"include_disabled": getattr(parsed_args,
'include_disabled', False),
"owned": getattr(parsed_args, 'owned', False),
}
if parsed_args:
if parsed_args.limit < 0:
raise exceptions.CommandError(
'--limit parameter must be non-negative')
if parsed_args.limit != 0:
filter_args['limit'] = parsed_args.limit
if parsed_args.marker:
filter_args['marker'] = parsed_args.marker
if parsed_args.search:
filter_args['search'] = parsed_args.search
if parsed_args.name:
filter_args['name'] = parsed_args.name
if parsed_args.fqn:
filter_args['fqn'] = parsed_args.fqn
if parsed_args.type:
filter_args['type'] = parsed_args.type
if parsed_args.category:
filter_args['category'] = parsed_args.category
if parsed_args.class_name:
filter_args['class_name'] = parsed_args.class_name
if parsed_args.tag:
filter_args['tag'] = parsed_args.tag
data = client.packages.filter(**filter_args)
columns = ('id', 'name', 'fully_qualified_name', 'author', 'active',
'is public', 'type', 'version')
column_headers = [c.capitalize() for c in columns]
if not parsed_args or parsed_args.limit == 0:
return (
column_headers,
list(utils.get_item_properties(
s,
columns,
) for s in data)
)
else:
return (
column_headers,
list(utils.get_item_properties(
s,
columns,
) for s in itertools.islice(data, parsed_args.limit))
)
class DeletePackage(command.Lister):
"""Delete a package."""
def get_parser(self, prog_name):
parser = super(DeletePackage, self).get_parser(prog_name)
parser.add_argument(
'id',
metavar="<ID>",
nargs="+",
help="Package ID to delete.",
)
return parser
def take_action(self, parsed_args):
LOG.debug("take_action({0})".format(parsed_args))
client = self.app.client_manager.application_catalog
failure_count = 0
for package_id in parsed_args.id:
try:
client.packages.delete(package_id)
except exceptions.NotFound:
failure_count += 1
print("Failed to delete '{0}'; package not found".
format(package_id))
if failure_count == len(parsed_args.id):
raise exceptions.CommandError("Unable to find and delete any of "
"the specified packages.")
data = client.packages.filter()
columns = ('id', 'name', 'fully_qualified_name', 'author', 'active',
'is public', 'type', 'version')
column_headers = [c.capitalize() for c in columns]
return (
column_headers,
list(utils.get_item_properties(
s,
columns,
) for s in data)
)
def _handle_package_exists(mc, data, package, exists_action):
name = package.manifest['FullName']
version = package.manifest.get('Version', '0')
while True:
print("Importing package {0}".format(name))
try:
return mc.packages.create(data, {name: package.file()})
except common_exceptions.HTTPConflict:
print("Importing package {0} failed. Package with the same"
" name/classes is already registered.".format(name))
allowed_results = ['s', 'u', 'a']
res = exists_action
if not res:
while True:
print("What do you want to do? (s)kip, (u)pdate, (a)bort")
res = input()
if res in allowed_results:
break
if res == 's':
print("Skipping.")
return None
elif res == 'a':
print("Exiting.")
sys.exit()
elif res == 'u':
pkgs = list(mc.packages.filter(fqn=name, version=version,
owned=True))
if not pkgs:
msg = (
"Got a conflict response, but could not find the "
"package '{0}' in the current tenant.\nThis probably "
"means the conflicting package is in another tenant.\n"
"Please delete it manually."
).format(name)
raise exceptions.CommandError(msg)
elif len(pkgs) > 1:
msg = (
"Got {0} packages with name '{1}'.\nI do not trust "
"myself, please delete the package manually."
).format(len(pkgs), name)
raise exceptions.CommandError(msg)
print("Deleting package {0}({1})".format(name, pkgs[0].id))
mc.packages.delete(pkgs[0].id)
continue
class ImportPackage(command.Lister):
"""Import a package."""
def get_parser(self, prog_name):
parser = super(ImportPackage, self).get_parser(prog_name)
parser.add_argument(
'filename',
metavar='<FILE>',
nargs='+',
help='URL of the murano zip package, FQPN, path to zip package'
' or path to directory with package.'
)
parser.add_argument(
'--categories',
metavar='<CATEGORY>',
nargs='*',
help='Category list to attach.',
)
parser.add_argument(
'--is-public',
action='store_true',
default=False,
help="Make the package available for users from other tenants.",
)
parser.add_argument(
'--package-version',
default='',
help='Version of the package to use from repository '
'(ignored when importing with multiple packages).'
)
parser.add_argument(
'--exists-action',
default='',
choices=['a', 's', 'u'],
help='Default action when a package already exists: '
'(s)kip, (u)pdate, (a)bort.'
)
parser.add_argument(
'--dep-exists-action',
default='',
choices=['a', 's', 'u'],
help='Default action when a dependency package already exists: '
'(s)kip, (u)pdate, (a)bort.'
)
parser.add_argument('--murano-repo-url',
default=murano_utils.env(
'MURANO_REPO_URL',
default=DEFAULT_REPO_URL),
help=('Defaults to env[MURANO_REPO_URL] '
'or {0}'.format(DEFAULT_REPO_URL)))
return parser
def take_action(self, parsed_args):
LOG.debug("take_action({0})".format(parsed_args))
client = self.app.client_manager.application_catalog
data = {"is_public": parsed_args.is_public}
version = parsed_args.package_version
if version and len(parsed_args.filename) >= 2:
print("Requested to import more than one package, "
"ignoring version.")
version = ''
if parsed_args.categories:
data["categories"] = parsed_args.categories
total_reqs = collections.OrderedDict()
main_packages_names = []
for filename in parsed_args.filename:
if os.path.isfile(filename) or os.path.isdir(filename):
_file = filename
else:
print("Package file '{0}' does not exist, attempting to "
"download".format(filename))
_file = murano_utils.to_url(
filename,
version=version,
base_url=parsed_args.murano_repo_url,
extension='.zip',
path='apps/',
)
try:
package = murano_utils.Package.from_file(_file)
except Exception as e:
print("Failed to create package for '{0}', reason: {1}".format(
filename, e))
continue
total_reqs.update(
package.requirements(base_url=parsed_args.murano_repo_url))
main_packages_names.append(package.manifest['FullName'])
imported_list = []
dep_exists_action = parsed_args.dep_exists_action
if dep_exists_action == '':
dep_exists_action = parsed_args.exists_action
for name, package in iter(total_reqs.items()):
image_specs = package.images()
if image_specs:
print("Inspecting required images")
try:
imgs = murano_utils.ensure_images(
glance_client=client.glance_client,
image_specs=image_specs,
base_url=parsed_args.murano_repo_url,
is_package_public=parsed_args.is_public)
for img in imgs:
print("Added {0}, {1} image".format(
img['name'], img['id']))
except Exception as e:
print("Error {0} occurred while installing "
"images for {1}".format(e, name))
if name in main_packages_names:
exists_action = parsed_args.exists_action
else:
exists_action = dep_exists_action
try:
imported_package = _handle_package_exists(
client, data, package, exists_action)
if imported_package:
imported_list.append(imported_package)
except Exception as e:
print("Error {0} occurred while installing package {1}".format(
e, name))
columns = ('id', 'name', 'fully_qualified_name', 'author', 'active',
'is public', 'type', 'version')
column_headers = [c.capitalize() for c in columns]
return (
column_headers,
list(utils.get_item_properties(
s,
columns,
) for s in imported_list)
)
class ImportBundle(command.Lister):
"""Import a bundle."""
def get_parser(self, prog_name):
parser = super(ImportBundle, self).get_parser(prog_name)
parser.add_argument(
'filename',
metavar='<FILE>',
nargs='+',
help='Bundle URL, bundle name, or path to the bundle file.'
)
parser.add_argument(
'--is-public',
action='store_true',
default=False,
help="Make the package available for users from other tenants.",
)
parser.add_argument(
'--exists-action',
default='',
choices=['a', 's', 'u'],
help='Default action when a package already exists: '
'(s)kip, (u)pdate, (a)bort.'
)
parser.add_argument('--murano-repo-url',
default=murano_utils.env(
'MURANO_REPO_URL',
default=DEFAULT_REPO_URL),
help=('Defaults to env[MURANO_REPO_URL] '
'or {0}'.format(DEFAULT_REPO_URL)))
return parser
def take_action(self, parsed_args):
LOG.debug("take_action({0})".format(parsed_args))
client = self.app.client_manager.application_catalog
total_reqs = collections.OrderedDict()
for filename in parsed_args.filename:
local_path = None
if os.path.isfile(filename):
_file = filename
local_path = os.path.dirname(os.path.abspath(filename))
else:
print("Bundle file '{0}' does not exist, attempting "
"to download".format(filename))
_file = murano_utils.to_url(
filename,
base_url=parsed_args.murano_repo_url,
path='bundles/',
extension='.bundle',
)
try:
bundle_file = murano_utils.Bundle.from_file(_file)
except Exception as e:
print("Failed to create bundle for '{0}', reason: {1}".format(
filename, e))
continue
data = {"is_public": parsed_args.is_public}
for package in bundle_file.packages(
base_url=parsed_args.murano_repo_url, path=local_path):
requirements = package.requirements(
base_url=parsed_args.murano_repo_url,
path=local_path,
)
total_reqs.update(requirements)
imported_list = []
for name, dep_package in total_reqs.items():
image_specs = dep_package.images()
if image_specs:
print("Inspecting required images")
try:
imgs = parsed_args.ensure_images(
glance_client=client.glance_client,
image_specs=image_specs,
base_url=parsed_args.murano_repo_url,
local_path=local_path,
is_package_public=parsed_args.is_public)
for img in imgs:
print("Added {0}, {1} image".format(
img['name'], img['id']))
except Exception as e:
print("Error {0} occurred while installing "
"images for {1}".format(e, name))
try:
imported_package = _handle_package_exists(
client, data, dep_package, parsed_args.exists_action)
if imported_package:
imported_list.append(imported_package)
except exceptions.CommandError:
raise
except Exception as e:
print("Error {0} occurred while "
"installing package {1}".format(e, name))
columns = ('id', 'name', 'fully_qualified_name', 'author', 'active',
'is public', 'type', 'version')
column_headers = [c.capitalize() for c in columns]
return (
column_headers,
list(utils.get_item_properties(
s,
columns,
) for s in imported_list)
)
class ShowPackage(command.ShowOne):
"""Display details for a package."""
def get_parser(self, prog_name):
parser = super(ShowPackage, self).get_parser(prog_name)
parser.add_argument(
"id",
metavar="<ID>",
help=("Package ID to show."),
)
return parser
def take_action(self, parsed_args):
LOG.debug("take_action({0})".format(parsed_args))
client = self.app.client_manager.application_catalog
try:
package = client.packages.get(parsed_args.id)
except common_exceptions.HTTPNotFound:
raise exceptions.CommandError("Package with id %s not "
"found" % parsed_args.id)
else:
to_display = dict(
id=package.id,
type=package.type,
owner_id=package.owner_id,
name=package.name,
fully_qualified_name=package.fully_qualified_name,
is_public=package.is_public,
enabled=package.enabled,
class_definitions=jsonutils.dumps(package.class_definitions,
indent=2),
categories=jsonutils.dumps(package.categories, indent=2),
tags=jsonutils.dumps(package.tags, indent=2),
description=package.description
)
return self.dict2columns(to_display)
class UpdatePackage(command.ShowOne):
"""Update an existing package."""
def get_parser(self, prog_name):
parser = super(UpdatePackage, self).get_parser(prog_name)
parser.add_argument(
'id',
metavar="<ID>",
help="Package ID to update.",
)
parser.add_argument(
'--is-public',
type=_bool_from_str_strict,
metavar="{true|false}",
help="Make package available to users from other tenants.",
)
parser.add_argument(
'--enabled',
type=_bool_from_str_strict,
metavar="{true|false}",
help="Make package active and available for deployments.",
)
parser.add_argument(
'--name',
default=None,
help="New name for the package.",
)
parser.add_argument(
'--description',
default=None,
help="New package description.",
)
parser.add_argument(
'--tags',
metavar='<TAG>', nargs='*',
default=None,
help="A list of keywords connected to the application.",
)
return parser
def take_action(self, parsed_args):
LOG.debug("take_action({0})".format(parsed_args))
client = self.app.client_manager.application_catalog
data = {}
parameters = ('is_public', 'enabled',
'name', 'description',
'tags')
for parameter in parameters:
param_value = getattr(parsed_args, parameter, None)
if param_value is not None:
data[parameter] = param_value
_, package = client.packages.update(parsed_args.id, data)
to_display = dict(
id=package["id"],
type=package["type"],
owner_id=package["owner_id"],
name=package["name"],
fully_qualified_name=package["fully_qualified_name"],
is_public=package["is_public"],
enabled=package["enabled"],
class_definitions=jsonutils.dumps(package["class_definitions"],
indent=2),
categories=jsonutils.dumps(package["categories"], indent=2),
tags=jsonutils.dumps(package["tags"], indent=2),
description=package["description"]
)
return self.dict2columns(to_display)
class DownloadPackage(command.Command):
"""Download a package to a filename or stdout."""
def get_parser(self, prog_name):
parser = super(DownloadPackage, self).get_parser(prog_name)
parser.add_argument(
"id",
metavar="<ID>",
help=("Package ID to download."),
)
parser.add_argument(
"filename",
metavar="file", nargs="?",
help=("Filename to save package to. If it is not "
"specified and there is no stdout redirection "
"the package won't be saved."),
)
return parser
def take_action(self, parsed_args):
LOG.debug("take_action({0})".format(parsed_args))
client = self.app.client_manager.application_catalog
def download_to_fh(package_id, fh):
try:
fh.write(client.packages.download(package_id))
except common_exceptions.HTTPNotFound:
raise exceptions.CommandError("Package with id %s not "
"found" % parsed_args.id)
if parsed_args.filename:
with open(parsed_args.filename, 'wb') as fh:
download_to_fh(parsed_args.id, fh)
print("Package downloaded to %s" % parsed_args.filename)
elif not sys.stdout.isatty():
download_to_fh(parsed_args.id, sys.stdout)
else:
msg = ("No stdout redirection or local file specified for "
"downloaded package. Please specify a local file to "
"save downloaded package or redirect output to "
"another source.")
raise exceptions.CommandError(msg)

View File

@ -1,52 +0,0 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
"""Application-catalog v1 class-schema action implementation"""
from osc_lib.command import command
from oslo_log import log as logging
LOG = logging.getLogger(__name__)
class ShowSchema(command.ShowOne):
"""Show class schema."""
def get_parser(self, prog_name):
parser = super(ShowSchema, self).get_parser(prog_name)
parser.add_argument(
"class_name", metavar="<CLASS>", help="Class FQN")
parser.add_argument(
"method_names", metavar="<METHOD>",
help="Method name", nargs='*')
parser.add_argument(
"--package-name", default=None,
help="FQN of the package where the class is located")
parser.add_argument(
"--class-version", default='=0',
help="Class version or version range (version spec)")
return parser
def take_action(self, parsed_args):
LOG.debug("take_action({0})".format(parsed_args))
client = self.app.client_manager.application_catalog
schema = client.schemas.get(
parsed_args.class_name, parsed_args.method_names,
class_version=parsed_args.class_version,
package_name=parsed_args.package_name)
return self.dict2columns(schema.data)
@property
def formatter_default(self):
return 'json'

View File

@ -1,530 +0,0 @@
# Copyright (c) 2013 Mirantis, 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.
"""
Command-line interface to the Murano Project.
"""
import argparse
import sys
import glanceclient
from keystoneclient.auth.identity.generic.cli import DefaultCLI
from keystoneclient.auth.identity import v3 as identity
from keystoneclient import discover
from keystoneclient import exceptions as ks_exc
from keystoneclient import session as ksession
from oslo_log import handlers
from oslo_log import log as logging
from oslo_utils import encodeutils
from oslo_utils import importutils
import urllib.parse as urlparse
import muranoclient
from muranoclient.apiclient import exceptions as exc
from muranoclient import client as murano_client
from muranoclient.common import utils
from muranoclient.glance import client as art_client
logger = logging.getLogger(__name__)
DEFAULT_REPO_URL = "http://apps.openstack.org/api/v1/murano_repo/liberty/"
# quick local fix for keystoneclient bug which blocks built-in reauth
# functionality in case of expired token.
# bug: https://bugs.launchpad.net/python-keystoneclient/+bug/1551392
# fix: https://review.opendev.org/#/c/286236/
class AuthCLI(DefaultCLI):
def invalidate(self):
retval = super(AuthCLI, self).invalidate()
if self._token:
self._token = None
retval = True
return retval
class MuranoShell(object):
def _append_global_identity_args(self, parser):
# Register the CLI arguments that have moved to the session object.
ksession.Session.register_cli_options(parser)
identity.Password.register_argparse_arguments(parser)
def get_base_parser(self, argv):
parser = argparse.ArgumentParser(
prog='murano',
description=__doc__.strip(),
epilog='See "murano help COMMAND" '
'for help on a specific 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=muranoclient.__version__,
help="Show program's version number and exit.")
parser.add_argument('-d', '--debug',
default=bool(utils.env('MURANOCLIENT_DEBUG')),
action='store_true',
help='Defaults to env[MURANOCLIENT_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.')
parser.add_argument('--os-tenant-id',
default=utils.env('OS_TENANT_ID'),
help='Defaults to env[OS_TENANT_ID].')
parser.add_argument('--os-tenant-name',
default=utils.env('OS_TENANT_NAME'),
help='Defaults to env[OS_TENANT_NAME].')
parser.add_argument('--os-region-name',
default=utils.env('OS_REGION_NAME'),
help='Defaults to env[OS_REGION_NAME].')
parser.add_argument('--os-auth-token',
default=utils.env('OS_AUTH_TOKEN'),
help='Defaults to env[OS_AUTH_TOKEN].')
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 env[OS_NO_CLIENT_AUTH].")
parser.add_argument('--murano-url',
default=utils.env('MURANO_URL'),
help='Defaults to env[MURANO_URL].')
parser.add_argument('--glance-url',
default=utils.env('GLANCE_URL'),
help='Defaults to env[GLANCE_URL].')
parser.add_argument('--glare-url',
default=utils.env('GLARE_URL'),
help='Defaults to env[GLARE_URL].')
parser.add_argument('--murano-api-version',
default=utils.env(
'MURANO_API_VERSION', default='1'),
help='Defaults to env[MURANO_API_VERSION] '
'or 1.')
parser.add_argument('--os-service-type',
default=utils.env('OS_SERVICE_TYPE'),
help='Defaults to env[OS_SERVICE_TYPE].')
parser.add_argument('--os-endpoint-type',
default=utils.env('OS_ENDPOINT_TYPE'),
help='Defaults to env[OS_ENDPOINT_TYPE].')
parser.add_argument('--include-password',
default=bool(utils.env('MURANO_INCLUDE_PASSWORD')),
action='store_true',
help='Send os-username and os-password to murano.')
parser.add_argument('--murano-repo-url',
default=utils.env(
'MURANO_REPO_URL',
default=DEFAULT_REPO_URL),
help=('Defaults to env[MURANO_REPO_URL] '
'or {0}'.format(DEFAULT_REPO_URL)))
parser.add_argument('--murano-packages-service',
choices=['murano', 'glance', 'glare'],
default=utils.env('MURANO_PACKAGES_SERVICE',
default='murano'),
help='Specifies if murano-api ("murano") or '
'Glance Artifact Repository ("glare") '
'should be used to store murano packages. '
'Defaults to env[MURANO_PACKAGES_SERVICE] or '
'to "murano"')
# The following 3 arguments are deprecated and are all added
# by keystone session register_cli_opts later. Only add these
# arguments if they are present on the command line.
if '--cert-file' in argv:
parser.add_argument('--cert-file',
dest='os_cert',
help='DEPRECATED! Use --os-cert.')
if '--key-file' in argv:
parser.add_argument('--key-file',
dest='os_key',
help='DEPRECATED! Use --os-key.')
if '--ca-file' in argv:
parser.add_argument('--ca-file',
dest='os_cacert',
help='DEPRECATED! Use --os-cacert.')
self._append_global_identity_args(parser)
return parser
def get_subcommand_parser(self, version, argv):
parser = self.get_base_parser(argv)
self.subcommands = {}
subparsers = parser.add_subparsers(metavar='<subcommand>')
submodule = importutils.import_versioned_module('muranoclient',
version, 'shell')
self._find_actions(subparsers, submodule)
self._find_actions(subparsers, self)
return parser
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 hypen-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 _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 as e:
# 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. error=%s') % (e)
raise exc.CommandError(msg)
return (v2_auth_url, v3_auth_url)
def _setup_logging(self, debug):
# Output the logs to command-line interface
color_handler = handlers.ColorHandler(sys.stdout)
logger_root = logging.getLogger(None).logger
logger_root.level = logging.DEBUG if debug else logging.WARNING
logger_root.addHandler(color_handler)
# Set the logger level of special library
logging.getLogger('iso8601') \
.logger.setLevel(logging.WARNING)
logging.getLogger('urllib3.connectionpool') \
.logger.setLevel(logging.WARNING)
def main(self, argv):
# Parse args once to find version
parser = self.get_base_parser(argv)
(options, args) = parser.parse_known_args(argv)
self._setup_logging(options.debug)
# build available subcommands based on version
api_version = options.murano_api_version
subcommand_parser = self.get_subcommand_parser(api_version, argv)
self.parser = subcommand_parser
keystone_session = None
keystone_auth = None
# 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 args.murano_packages_service == 'glance':
args.murano_packages_service = 'glare'
if args.os_no_client_auth:
if not args.murano_url:
raise exc.CommandError(
"If you specify --os-no-client-auth"
" you must also specify a Murano API URL"
" via either --murano-url or env[MURANO_URL]")
if (not args.glare_url and
args.murano_packages_service == 'glare'):
raise exc.CommandError(
"If you specify --os-no-client-auth and"
" set murano-packages-service to 'glare'"
" you must also specify a glare API URL"
" via either --glare-url or env[GLARE_API]")
if (not any([args.os_tenant_id, args.os_project_id]) and
args.murano_packages_service == 'glare'):
# TODO(kzaitsev): see if we can use project's name here
# NOTE(kzaitsev): glare v0.1 needs project_id to operate
# correctly
raise exc.CommandError(
"If you specify --os-no-client-auth and"
" set murano-packages-service to 'glare'"
" you must also specify your project's id"
" via either --os-project-id or env[OS_PROJECT_ID] or"
" --os-tenant-id or env[OS_TENANT_ID]")
else:
# Tenant 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 any([args.os_tenant_name, args.os_tenant_id,
args.os_project_id, args.os_project_name]):
raise exc.CommandError("You must provide a project name or"
" project id via --os-project-name,"
" --os-project-id, env[OS_PROJECT_ID]"
" or env[OS_PROJECT_NAME]. You may"
" use os-project and os-tenant"
" interchangeably.")
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]")
endpoint_type = args.os_endpoint_type or 'publicURL'
endpoint = args.murano_url
glance_endpoint = args.glance_url
if args.os_no_client_auth:
# Authenticate through murano, don't use session
kwargs = {
'username': args.os_username,
'password': args.os_password,
'auth_token': args.os_auth_token,
'auth_url': args.os_auth_url,
'token': args.os_auth_token,
'insecure': args.insecure,
'timeout': args.api_timeout,
'tenant': args.os_project_id or args.os_tenant_id,
}
glance_kwargs = kwargs.copy()
if args.os_region_name:
kwargs['region_name'] = args.os_region_name
glance_kwargs['region_name'] = args.os_region_name
else:
# Create a keystone session and keystone auth
keystone_session = ksession.Session.load_from_cli_options(args)
args.os_project_name = args.os_project_name or args.os_tenant_name
args.os_project_id = args.os_project_id or args.os_tenant_id
# make args compatible with DefaultCLI/AuthCLI
args.os_token = args.os_auth_token
args.os_endpoint = ''
# avoid password prompt if no password given
args.os_password = args.os_password or '<no password>'
(v2_auth_url, v3_auth_url) = self._discover_auth_versions(
keystone_session, args.os_auth_url)
if v3_auth_url:
if (not args.os_user_domain_id and
not args.os_user_domain_name):
args.os_user_domain_name = 'default'
if (not args.os_project_domain_id and
not args.os_project_domain_name):
args.os_project_domain_name = 'default'
keystone_auth = AuthCLI.load_from_argparse_arguments(args)
service_type = args.os_service_type or 'application-catalog'
if not endpoint:
endpoint = keystone_auth.get_endpoint(
keystone_session,
service_type=service_type,
interface=endpoint_type,
region_name=args.os_region_name)
kwargs = {
'session': keystone_session,
'auth': keystone_auth,
'service_type': service_type,
'region_name': args.os_region_name,
}
glance_kwargs = kwargs.copy()
# glance doesn't need endpoint_type
kwargs['endpoint_type'] = endpoint_type
kwargs['tenant'] = keystone_auth.get_project_id(keystone_session)
if args.api_timeout:
kwargs['timeout'] = args.api_timeout
if not glance_endpoint:
try:
glance_endpoint = keystone_auth.get_endpoint(
keystone_session,
service_type='image',
interface=endpoint_type,
region_name=args.os_region_name)
except Exception:
pass
glance_client = None
if glance_endpoint:
try:
# TODO(starodubcevna): switch back to glance APIv2 when it will
# be ready for use.
glance_client = glanceclient.Client(
'1', glance_endpoint, **glance_kwargs)
except Exception:
pass
if glance_client:
kwargs['glance_client'] = glance_client
else:
logger.warning("Could not initialize glance client. "
"Image creation will be unavailable.")
kwargs['glance_client'] = None
if args.murano_packages_service == 'glare':
glare_endpoint = args.glare_url
if not glare_endpoint:
# no glare_endpoint and we requested to store packages in glare
# let's check keystone
try:
glare_endpoint = keystone_auth.get_endpoint(
keystone_session,
service_type='artifact',
interface=endpoint_type,
region_name=args.os_region_name)
except Exception:
raise exc.CommandError(
"You set murano-packages-service to {}"
" but there is not 'artifact' endpoint in keystone"
" Either register one or specify endpoint "
" via either --glare-url or env[GLARE_API]".format(
args.murano_packages_service))
auth_token = \
args.os_auth_token or keystone_auth.get_token(keystone_session)
artifacts_client = art_client.Client(endpoint=glare_endpoint,
type_name='murano',
type_version=1,
token=auth_token,
insecure=args.insecure)
kwargs['artifacts_client'] = artifacts_client
client = murano_client.Client(api_version, endpoint, **kwargs)
args.func(client, args)
def do_bash_completion(self, args):
"""Prints all of the commands and options to stdout."""
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')
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:
msg = "'%s' is not a valid subcommand"
raise exc.CommandError(msg % 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=sys.argv[1:]):
try:
MuranoShell().main(args)
except KeyboardInterrupt:
print('... terminating murano client', file=sys.stderr)
sys.exit(1)
except Exception as e:
if '--debug' in args or '-d' in args:
raise
else:
print(encodeutils.safe_encode(str(e)), file=sys.stderr)
sys.exit(1)
if __name__ == "__main__":
main()

View File

@ -1,34 +0,0 @@
Namespaces:
=: io.murano.apps
std: io.murano
# Name: MockApp # Using Name from manifest.yaml
Extends: std:Application
Properties:
greeting:
Usage: Static
Contract: $.string()
Default: 'Hello, '
Methods:
testAction:
Usage: Action
Body:
- sleep(1)
- $this.find(std:Environment).reporter.report($this, 'Completed')
deploy:
Body:
- $this.find(std:Environment).reporter.report($this, 'Follow the white rabbit')
staticAction:
Scope: Public
Usage: Static
Arguments:
- myName:
Contract: $.string().notNull()
- myAge:
Contract: $.int().notNull()
Body:
- $futureAge: $myAge + 5
- Return: concat($.greeting, $myName, ". In 5 years you will be {0} years old.".format($futureAge))

View File

@ -1,256 +0,0 @@
# Copyright (c) 2015 Mirantis, 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 http import server as SimpleHTTPServer
import json
import multiprocessing
import os
import shutil
import socketserver
import tempfile
import time
from oslo_utils import uuidutils
from tempest.lib.cli import output_parser
from tempest.lib import exceptions
from muranoclient.tests.functional.cli import utils
from muranoclient.tests.functional import muranoclient
class CLIUtilsTestBase(muranoclient.ClientTestBase):
"""Basic methods for Murano CLI client."""
def delete_murano_object(self, murano_object, obj_to_del):
"""Delete Murano object
Delete Murano object like environment, category or
environment-template.
"""
if obj_to_del not in self.listing('{0}-list'.format(murano_object)):
return
object_list = self.listing('{0}-delete'.format(murano_object),
params=obj_to_del['ID'])
start_time = time.time()
while obj_to_del in self.listing('{0}-list'.format(murano_object)):
if start_time - time.time() > 60:
self.fail("{0} is not deleted in 60 seconds".
format(murano_object))
return object_list
def create_murano_object(self, murano_object, prefix_object_name):
"""Create Murano object
Create Murano object like environment, category or
environment-template.
"""
object_name = self.generate_name(prefix_object_name)
mrn_objects = self.listing('{0}-create'.format(murano_object),
params=object_name)
mrn_object = None
for obj in mrn_objects:
if object_name == obj['Name']:
mrn_object = obj
break
if mrn_object is None:
self.fail("Murano {0} has not been created!".format(murano_object))
self.addCleanup(self.delete_murano_object, murano_object, mrn_object)
return mrn_object
def create_murano_object_parameter(self, murano_object, prefix_object_name,
param):
"""Create Murano object
Create Murano object like environment, category or
environment-template.
"""
object_name = self.generate_name(prefix_object_name)
params = '{0} {1}'.format(param, object_name)
mrn_objects = self.listing('{0}-create'.format(murano_object),
params=params)
mrn_object = None
for obj in mrn_objects:
if object_name == obj['Name']:
mrn_object = obj
break
if mrn_object is None:
self.fail("Murano {0} has not been created!".format(murano_object))
self.addCleanup(self.delete_murano_object, murano_object, mrn_object)
return mrn_object
@staticmethod
def generate_uuid():
"""Generate uuid for objects."""
return uuidutils.generate_uuid(dashed=False)
@staticmethod
def generate_name(prefix):
"""Generate name for objects."""
suffix = CLIUtilsTestBase.generate_uuid()[:8]
return "{0}_{1}".format(prefix, suffix)
def get_table_struct(self, command, params=""):
"""Get table structure i.e. header of table."""
return output_parser.table(self.murano(command,
params=params))['headers']
def get_object(self, object_list, object_value):
""""Get Murano object by value from list of Murano objects."""
for obj in object_list:
if object_value in obj.values():
return obj
def get_property_value(self, obj, prop):
return [o['Value'] for o in obj
if o['Property'] == '{0}'.format(prop)][0]
class TestSuiteRepository(CLIUtilsTestBase):
def setUp(self):
super(TestSuiteRepository, self).setUp()
self.serve_dir = tempfile.mkdtemp(suffix="repo")
self.app_name = self.generate_name("dummy_app")
self.dummy_app_path = self._compose_app(name=self.app_name)
def tearDown(self):
super(TestSuiteRepository, self).tearDown()
shutil.rmtree(self.serve_dir)
def run_server(self):
def serve_function():
class Handler(SimpleHTTPServer.SimpleHTTPRequestHandler):
pass
os.chdir(self.serve_dir)
httpd = socketserver.TCPServer(
("0.0.0.0", 8089),
Handler, bind_and_activate=False)
httpd.allow_reuse_address = True
httpd.server_bind()
httpd.server_activate()
httpd.serve_forever()
self.p = multiprocessing.Process(target=serve_function)
self.p.start()
def stop_server(self):
self.p.terminate()
def _compose_app(self, name, require=None):
package_dir = os.path.join(self.serve_dir, 'apps/', name)
shutil.copytree(os.path.join(os.path.dirname(
os.path.realpath(__file__)), 'MockApp'), package_dir)
app_name = utils.compose_package(
name,
os.path.join(package_dir, 'manifest.yaml'),
package_dir,
require=require,
archive_dir=os.path.join(self.serve_dir, 'apps/'),
)
return app_name
class CLIUtilsTestPackagesBase(TestSuiteRepository):
"""Basic methods for Murano Packages CLI client."""
def import_package(self, pkg_name, pkg_path, *args):
"""Create Murano dummy package and import it by url."""
actions = ' '.join(args)
params = '{0} {1}'.format(pkg_path, actions)
package = self.listing('package-import', params=params)
package = self.get_object(package, pkg_name)
self.addCleanup(self.delete_murano_object, 'package', package)
return package
def prepare_file_with_obj_model(self, obj_model):
temp_file = tempfile.NamedTemporaryFile(prefix="murano-obj-model",
delete=False)
self.addCleanup(os.remove, temp_file.name)
with open(temp_file.name, 'w') as tf:
tf.write(json.dumps([obj_model]))
return temp_file.name
def wait_deployment_result(self, env_id, timeout=180):
start_time = time.time()
env = self.listing('environment-show', params=env_id)
env_status = self.get_property_value(env, 'status')
expected_statuses = ['ready', 'deploying']
while env_status != 'ready':
if time.time() - start_time > timeout:
msg = ("Environment exceeds timeout {0} to change state "
"to Ready. Environment: {1}".format(timeout, env))
raise exceptions.TimeoutException(msg)
env = self.listing('environment-show', params=env_id)
env_status = self.get_property_value(env, 'status')
if env_status not in expected_statuses:
msg = ("Environment status %s is not in expected "
"statuses: %s" % (env_status, expected_statuses))
raise exceptions.TempestException(msg)
time.sleep(2)
return True
def prepare_bundle_with_non_existed_package(self):
temp_file = tempfile.NamedTemporaryFile(mode='w',
delete=False)
self.addCleanup(os.remove, temp_file.name)
with open(temp_file.name, 'w') as tf:
tf.write(json.dumps({'Packages': [
{'Name': 'first_app'},
{'Name': 'second_app', 'Version': '1.0'}
]}))
return temp_file.name
def prepare_bundle_with_invalid_format(self):
temp_file = tempfile.NamedTemporaryFile(mode='w', delete=False)
self.addCleanup(os.remove, temp_file.name)
with open(temp_file.name, 'w') as tf:
tf.write('Packages: [{Name: first_app}, {Name: second_app}]')
return temp_file.name
def deploy_environment(self, env_id, obj_model):
session = self.listing('environment-session-create',
params=env_id)
session_id = self.get_property_value(session, 'id')
temp_file = self.prepare_file_with_obj_model(obj_model)
self.listing('environment-apps-edit',
params='--session-id {0} {1} {2}'.
format(session_id, env_id, temp_file))
self.listing('environment-deploy',
params='{0} --session-id {1}'.
format(env_id, session_id))
result = self.wait_deployment_result(env_id)
self.assertTrue(result)

View File

@ -1,893 +0,0 @@
# Copyright (c) 2015 Mirantis, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import json
import unittest
from muranoclient.tests.functional.cli import \
murano_test_utils as utils
from muranoclient.tests.functional import muranoclient as murano_client
from oslo_utils.strutils import bool_from_string as str2bool
# TODO(mstolyarenko): need to remove this raw when
# https://bugs.launchpad.net/python-muranoclient/+bug/1625039 is fixed
backend_name =\
murano_client.ClientTestBase.get_backend_flag().rstrip().split()[-1]
class SimpleReadOnlyMuranoClientTest(utils.CLIUtilsTestBase):
"""Basic, read-only tests for Murano CLI client.
Basic smoke test for the Murano CLI commands which do not require
creating or modifying murano objects.
"""
def test_category_list(self):
category = self.get_table_struct('category-list')
self.assertEqual(['ID', 'Name'], category)
def test_env_template_list(self):
templates = self.get_table_struct('env-template-list')
self.assertEqual(['ID', 'Name', 'Created', 'Updated', 'Is public'],
templates)
def test_environment_list(self):
environment = self.get_table_struct('environment-list')
self.assertEqual(['ID', 'Name', 'Status', 'Created', 'Updated'],
environment)
def test_package_list(self):
packages = self.get_table_struct('package-list')
self.assertEqual(['ID', 'Name', 'FQN', 'Author', 'Active',
'Is Public', 'Type', 'Version'], packages)
class TableStructureMuranoClientTest(utils.CLIUtilsTestBase):
"""Smoke test for the Murano CLI commands
Smoke test for the Murano CLI commands which checks table
structure after create or delete category, env-template
environment and package.
"""
def test_table_struct_deployment_list(self):
"""Test scenario:
1) create environment
2) check table structure
"""
environment = self.create_murano_object('environment',
'MuranoTestTS-depl-list')
table_struct = self.get_table_struct('deployment-list',
params=environment['ID'])
self.assertEqual(['ID', 'State', 'Created', 'Updated', 'Finished'],
table_struct)
def test_table_struct_of_environment_create(self):
"""Test scenario:
1) create environment
2) check table structure
"""
self.create_murano_object('environment', 'MuranoTestTS-env-create')
table_struct = self.get_table_struct('environment-list')
self.assertEqual(['ID', 'Name', 'Status', 'Created', 'Updated'],
table_struct)
def test_table_struct_of_environment_delete(self):
"""Test scenario:
1) create environment
2) delete environment
3) check table structure
"""
environment = self.create_murano_object('environment',
'MuranoTestTS-env-del')
self.delete_murano_object('environment', environment)
table_struct = self.get_table_struct('environment-list')
self.assertEqual(['ID', 'Name', 'Status', 'Created', 'Updated'],
table_struct)
def test_table_struct_of_category_create(self):
"""Test scenario:
1) create category
2) check table structure
"""
self.create_murano_object('category', 'MuranoTestTS-cat-create')
table_struct = self.get_table_struct('category-list')
self.assertEqual(['ID', 'Name'], table_struct)
def test_table_struct_of_category_delete(self):
"""Test scenario:
1) create category
2) delete category
3) check table structure
"""
category = self.create_murano_object('category',
'MuranoTestTS-cat-create')
self.delete_murano_object('category', category)
category = self.get_table_struct('category-list')
self.assertEqual(['ID', 'Name'], category)
def test_table_struct_of_env_template_create(self):
"""Test scenario:
1) create env_template
2) check table structure
"""
self.create_murano_object('env-template',
'MuranoTestTS-env-tmp-create')
table_struct = self.get_table_struct('env-template-list')
self.assertEqual(['ID', 'Name', 'Created', 'Updated', 'Is public'],
table_struct)
def test_table_struct_of_env_template_delete(self):
"""Test scenario:
1) create env_template
2) delete env_template
3) check table structure
"""
env_template = self.create_murano_object('env-template',
'MuranoTestTS-env-tmp-create')
self.delete_murano_object('env-template', env_template)
table_struct = self.get_table_struct('env-template-list')
self.assertEqual(['ID', 'Name', 'Created', 'Updated', 'Is public'],
table_struct)
class EnvironmentMuranoSanityClientTest(utils.CLIUtilsTestBase):
"""Sanity tests for testing actions with environment.
Smoke test for the Murano CLI commands which checks basic actions with
environment command like create, delete, rename etc.
"""
def test_environment_create(self):
"""Test scenario:
1) create environment
2) check that created environment exist
"""
environment = self.create_murano_object('environment',
'TestMuranoSanityEnv')
env_list = self.listing('environment-list')
# Deleting dates from dictionaries to skip it in assert
list(map(lambda x: x.pop('Updated', None),
env_list + [environment]))
list(map(lambda x: x.pop('Created', None),
env_list + [environment]))
self.assertIn(environment, env_list)
def test_environment_delete(self):
"""Test scenario:
1) create environment
2) delete environment
"""
environment = self.create_murano_object('environment',
'TestMuranoSanityEnv')
self.delete_murano_object('environment', environment)
env_list = self.listing('environment-list')
self.assertNotIn(environment, env_list)
def test_environment_rename(self):
"""Test scenario:
1) create environment
2) rename environment
"""
environment = self.create_murano_object('environment',
'TestMuranoSanityEnv')
new_env_name = self.generate_name('TestMuranoSEnv-env-rename')
rename_params = "{0} {1}".format(environment['Name'], new_env_name)
new_list = self.listing('environment-rename', params=rename_params)
renamed_env = self.get_object(new_list, new_env_name)
self.addCleanup(self.delete_murano_object, 'environment', renamed_env)
new_env_list = self.listing('environment-list')
# Deleting dates from dictionaries to skip it in assert
list(map(lambda x: x.pop('Updated', None),
new_env_list + [environment] + [renamed_env]))
list(map(lambda x: x.pop('Created', None),
new_env_list + [environment] + [renamed_env]))
self.assertIn(renamed_env, new_env_list)
self.assertNotIn(environment, new_env_list)
def test_table_struct_env_show(self):
"""Test scenario:
1) create environment
2) check structure of env_show object
"""
environment = self.create_murano_object('environment',
'TestMuranoSanityEnv')
env_show = self.listing('environment-show', params=environment['Name'])
# Check structure of env_show object
self.assertEqual(['acquired_by', 'created', 'description_text', 'id',
'name', 'services', 'status', 'tenant_id',
'updated', 'version'],
list(map(lambda x: x['Property'], env_show)))
def test_environment_show(self):
"""Test scenario:
1) create environment
2) check that env_name, ID, updated and created values
exist in env_show object
"""
environment = self.create_murano_object('environment',
'TestMuranoSanityEnv')
env_show = self.listing('environment-show', params=environment['Name'])
self.assertIn(environment['Created'],
list(map(lambda x: x['Value'], env_show)))
self.assertIn(environment['Updated'],
list(map(lambda x: x['Value'], env_show)))
self.assertIn(environment['Name'],
list(map(lambda x: x['Value'], env_show)))
self.assertIn(environment['ID'],
list(map(lambda x: x['Value'], env_show)))
def test_environment_delete_by_id(self):
"""Test scenario:
1) create environment
2) delete environment by environment ID
"""
env_name = self.generate_name('TestMuranoSanityEnv')
environment = self.create_murano_object('environment', env_name)
result = self.murano('environment-delete', params=environment['ID'],
fail_ok=False)
self.assertNotIn(environment['Name'], result)
env_list = self.listing('environment-list')
self.assertNotIn(environment, env_list)
def test_environment_model_show(self):
"""Test scenario:
1) create environment
2) check that the result of environment-model-show is a valid
non-empty json
"""
env_name = self.generate_name('TestMuranoSanityEnv')
environment = self.create_murano_object('environment', env_name)
model = self.murano('environment-model-show', params=environment['ID'])
result = json.loads(model)
self.assertEqual(4, len(result))
class CategoryMuranoSanityClientTest(utils.CLIUtilsTestBase):
"""Sanity tests for testing actions with Category.
Smoke test for the Murano CLI commands which checks basic actions with
category command like create, delete etc.
"""
def test_category_create(self):
"""Test scenario:
1) create category
2) check that created category exist
"""
category = self.create_murano_object('category',
'TestMuranoSanityCategory')
category_list = self.listing('category-list')
self.assertIn(category, category_list)
def test_category_delete(self):
"""Test scenario:
1) create category
2) delete category
3) check that category has been deleted successfully
"""
category = self.create_murano_object('category',
'TestMuranoSanityCategory')
self.delete_murano_object('category', category)
category_list = self.listing('category-list')
self.assertNotIn(category, category_list)
def test_table_struct_category_show(self):
"""Test scenario:
1) create category
2) check table structure of category-show object
"""
category = self.create_murano_object('category',
'TestMuranoSanityCategory')
category_show = self.listing('category-show', params=category['ID'])
self.assertEqual(['id', 'name', 'packages'],
list(map(lambda x: x['Property'], category_show)))
def test_category_show(self):
"""Test scenario:
1) create category
2) check that category values exist in category_show object
"""
category = self.create_murano_object('category',
'TestMuranoSanityCategory')
category_show = self.listing('category-show', params=category['ID'])
self.assertIn(category['ID'],
list(map(lambda x: x['Value'], category_show)))
self.assertIn(category['Name'],
list(map(lambda x: x['Value'], category_show)))
def test_non_existing_category_delete(self):
"""Test scenario:
1) try to call category-delete for non existing category
2) check that error message contains user friendly substring
"""
result = self.murano('category-delete', params='non-existing',
fail_ok=True)
self.assertIn("Failed to delete 'non-existing'; category not found",
result)
def test_non_existing_category_show(self):
"""Test scenario:
1) try to call category-show for non existing category
2) check that error message contains user friendly substring
"""
result = self.murano('category-show', params='non-existing',
fail_ok=True)
self.assertIn("Category id 'non-existing' not found", str(result))
def test_category_create_with_long_name(self):
"""Test scenario:
1) try to create category with long name (>80)
2) check that error message contains user friendly substring
"""
result = self.murano('category-create', params='name' * 21,
fail_ok=True)
self.assertIn(
"Category name should be 80 characters maximum",
result)
class EnvTemplateMuranoSanityClientTest(utils.CLIUtilsTestBase):
"""Sanity tests for testing actions with Environment template.
Smoke test for the Murano CLI commands which checks basic actions with
env-temlate command like create, delete etc.
"""
def test_environment_template_create(self):
"""Test scenario:
1) create environment template
2) check that created environment template exist
"""
env_template = self.create_murano_object('env-template',
'TestMuranoSanityEnvTemp')
env_template_list = self.listing('env-template-list')
# Deleting dates from dictionaries to skip it in assert
list(map(lambda x: x.pop('Updated', None),
env_template_list + [env_template]))
list(map(lambda x: x.pop('Created', None),
env_template_list + [env_template]))
self.assertIn(env_template, env_template_list)
def test_environment_template_delete(self):
"""Test scenario:
1) create environment template
2) delete environment template
3) check that deleted environment template doesn't exist
"""
env_template = self.create_murano_object('env-template',
'TestMuranoSanityEnvTemp')
env_template_list = self.delete_murano_object('env-template',
env_template)
self.assertNotIn(env_template, env_template_list)
def test_table_struct_env_template_show(self):
"""Test scenario:
1) create environment template
2) check table structure of env-template-show object
"""
env_template = self.create_murano_object('env-template',
'TestMuranoSanityEnvTemp')
env_template_show = self.listing('env-template-show',
params=env_template['ID'])
tested_env_template = list(
map(lambda x: x['Property'], env_template_show))
self.assertIn('created', tested_env_template)
self.assertIn('id', tested_env_template)
self.assertIn('name', tested_env_template)
self.assertIn('services', tested_env_template)
self.assertIn('tenant_id', tested_env_template)
self.assertIn('updated', tested_env_template)
self.assertIn('version', tested_env_template)
def test_env_template_show(self):
"""Test scenario:
1) create environment template
2) check that environment template values exist in
env-template-show object
"""
env_template = self.create_murano_object('env-template',
'TestMuranoSanityEnvTemp')
env_template_show = self.listing('env-template-show',
params=env_template['ID'])
tested_env = list(map(lambda x: x['Value'], env_template_show))
self.assertIn(env_template['ID'], tested_env)
self.assertIn(env_template['Name'], tested_env)
def test_env_template_create_environment(self):
"""Test scenario:
1) create environment template
2) create environment from template
"""
env_template = self.create_murano_object('env-template',
'TestMuranoSanityEnvTemp')
new_env_name = self.generate_name('EnvFromTemp')
params = "{0} {1}".format(env_template['ID'], new_env_name)
env_created = self.listing('env-template-create-env', params=params)
tested_env_created = list(map(lambda x: x['Property'], env_created))
self.assertIn('environment_id', tested_env_created)
self.assertIn('session_id', tested_env_created)
def test_env_template_clone(self):
"""Test scenario:
1) create environment template
2) clone template
3) check that create environment template has the new name
4) delete new template
"""
env_template = self.create_murano_object_parameter(
'env-template', 'TestMuranoSanityEnvTemp', '--is-public')
new_template = self.generate_name('TestMuranoSanityEnvTemp')
params = "{0} {1}".format(env_template['ID'], new_template)
template_created = self.listing('env-template-clone', params=params)
tp_list = list(
map(lambda x: ({x['Property']: x['Value']}), template_created))
result_name = list(filter(lambda x: x.get('name'), tp_list))[0]['name']
result_id = list(filter(lambda x: x.get('id'), tp_list))[0]['id']
self.listing('env-template-delete', params=result_id)
self.assertIn(result_name, new_template)
class PackageMuranoSanityClientTest(utils.CLIUtilsTestPackagesBase):
"""Sanity tests for testing actions with Packages.
Smoke tests for the Murano CLI commands which check basic actions with
packages like import, create, delete etc.
"""
def test_package_import_by_url(self):
"""Test scenario:
1) import package by url
2) check that package exists
"""
try:
self.run_server()
package = self.import_package(
self.app_name,
'http://localhost:8089/apps/{0}.zip'.format(self.app_name)
)
finally:
self.stop_server()
package_list = self.listing('package-list')
self.assertIn(package, package_list)
def test_package_import_by_path(self):
"""Test scenario:
1) import package by path
2) check that package exists
"""
package = self.import_package(
self.app_name,
self.dummy_app_path
)
package_list = self.listing('package-list')
self.assertIn(package, package_list)
def test_package_is_public(self):
"""Test scenario:
1) import package
2) check that package is public
"""
package = self.import_package(
self.app_name,
self.dummy_app_path,
'--is-public')
package_show = self.listing('package-show', params=package['ID'])
package_show = {item['Property']: item['Value']
for item in package_show}
self.assertEqual(package['Is Public'], 'True')
self.assertEqual(
str2bool(package['Is Public']),
str2bool(package_show['is_public']))
def test_package_delete(self):
"""Test scenario:
1) import package
2) delete package
3) check that package has been deleted
"""
package = self.import_package(
self.app_name,
self.dummy_app_path
)
package_list = self.delete_murano_object('package', package)
self.assertNotIn(package, package_list)
def test_package_show(self):
"""Test scenario:
1) import package
2) check that package values exist in
return by package-show object
"""
package = self.import_package(
self.app_name,
self.dummy_app_path
)
package_show = self.listing('package-show', params=package['ID'])
package_show = {item['Property']: item['Value']
for item in package_show}
self.assertEqual(
str2bool(package['Active']),
str2bool(package_show['enabled']))
self.assertEqual(
package['FQN'],
package_show['fully_qualified_name'])
self.assertEqual(
package['ID'],
package_show['id'])
self.assertEqual(
str2bool(package['Is Public']),
str2bool(package_show['is_public']))
self.assertEqual(
package['Name'],
package_show['name'])
self.assertEqual(
package['Type'],
package_show['type'])
def test_package_import_update(self):
"""Test scenario:
1) import package
2) import new_package using option 'u' - update
3) check that package has been updated
"""
package = self.import_package(
self.app_name,
self.dummy_app_path
)
upd_package = self.import_package(
self.app_name,
self.dummy_app_path,
'--exists-action', 'u'
)
self.assertEqual(package['Name'], upd_package['Name'])
self.assertNotEqual(package['ID'], upd_package['ID'])
def test_package_import_skip(self):
"""Test scenario:
1) import package using option 's' - skip for existing package
2) try to import the same package using option 's' - skip
3) check that package hasn't been updated
"""
package = self.import_package(
self.app_name,
self.dummy_app_path,
'--exists-action', 's'
)
updated_package = self.import_package(
self.app_name,
self.dummy_app_path,
'--exists-action', 's'
)
package_list = self.listing("package-list")
self.assertIn(package, package_list)
self.assertIsNone(updated_package)
def test_package_import_abort(self):
"""Test scenario:
1) import package
2) import new_package using option 'a' - skip
3) check that package hasn't been updated
"""
package = self.import_package(
self.app_name,
self.dummy_app_path
)
package_list = self.listing('package-list')
self.assertIn(package, package_list)
package = self.import_package(
self.app_name,
self.dummy_app_path,
'--exists-action', 'a'
)
package_list = self.listing('package-list')
self.assertNotIn(package, package_list)
class DeployMuranoEnvironmentTest(utils.CLIUtilsTestPackagesBase):
"""Test for testing Murano environment deployment.
Test for the Murano CLI commands which checks addition of app
to the environment, session creation and deployment of
environment.
"""
# TODO(mstolyarenko): need to unskip this test when
# https://bugs.launchpad.net/python-muranoclient/+bug/1625039 is fixed
@unittest.skipIf(backend_name == 'glare',
"This test fails when GLARE is used as packages "
"service. To be fixed as part of #1625039")
def test_environment_deployment(self):
"""Test scenario:
1) import package
2) create environment
3) create session for created environment
4) add application to the environment
5) send environment to deploy
6) check that deployment was successful
"""
self.import_package(
self.app_name,
self.dummy_app_path
)
env_id = self.create_murano_object('environment',
'TestMuranoDeployEnv')['ID']
obj_model = {
'op': 'add',
'path': '/-',
'value': {
'?': {
'type': 'io.murano.apps.{0}'.format(self.app_name),
'id': '{0}'.format(self.generate_uuid()),
}
}
}
self.deploy_environment(env_id, obj_model)
deployments = self.listing('deployment-list', params=env_id)
self.assertEqual('success', deployments[0]['State'])
self.assertEqual(1, len(deployments))
# TODO(mstolyarenko): need to unskip this test when
# https://bugs.launchpad.net/python-muranoclient/+bug/1625039 is fixed
@unittest.skipIf(backend_name == 'glare',
"This test fails when GLARE is used as packages "
"service. To be fixed as part of #1625039")
def test_add_component_to_deployed_env(self):
"""Test scenario:
1) import package
2) create environment
3) create session for created environment
4) add application to the environment
5) send environment to deploy
6) check that deployment was successful
7) add application to environment
8) deploy environment again
"""
self.import_package(
self.app_name,
self.dummy_app_path
)
env_id = self.create_murano_object('environment',
'TestMuranoDeployEnv')['ID']
obj_model = {
'op': 'add',
'path': '/-',
'value': {
'?': {
'type': 'io.murano.apps.{0}'.format(self.app_name),
'id': '',
}
}
}
obj_model['value']['?']['id'] = self.generate_uuid()
self.deploy_environment(env_id, obj_model)
deployments = self.listing('deployment-list', params=env_id)
self.assertEqual('success', deployments[0]['State'])
self.assertEqual(1, len(deployments))
obj_model['value']['?']['id'] = self.generate_uuid()
self.deploy_environment(env_id, obj_model)
deployments = self.listing('deployment-list', params=env_id)
self.assertEqual('success', deployments[1]['State'])
self.assertEqual(2, len(deployments))
# TODO(mstolyarenko): need to unskip this test when
# https://bugs.launchpad.net/python-muranoclient/+bug/1625039 is fixed
@unittest.skipIf(backend_name == 'glare',
"This test fails when GLARE is used as packages "
"service. To be fixed as part of #1625039")
def test_delete_component_from_deployed_env(self):
"""Test scenario:
1) import package
2) create environment
3) create session for created environment
4) add application to the environment
5) send environment to deploy
6) check that deployment was successful
7) delete application from environment
8) deploy environment again
"""
self.import_package(
self.app_name,
self.dummy_app_path
)
env_id = self.create_murano_object('environment',
'TestMuranoDeployEnv')['ID']
obj_model = {
'op': 'add',
'path': '/-',
'value': {
'?': {
'type': 'io.murano.apps.{0}'.format(self.app_name),
'id': '{0}'.format(self.generate_uuid()),
}
}
}
self.deploy_environment(env_id, obj_model)
obj_model = {
'op': 'remove',
'path': '/0'
}
self.deploy_environment(env_id, obj_model)
deployments = self.listing('deployment-list', params=env_id)
self.assertEqual('success', deployments[1]['State'])
self.assertEqual(2, len(deployments))
class BundleMuranoSanityClientTest(utils.CLIUtilsTestPackagesBase):
"""Sanity tests for testing actions with bundle.
Tests for the Murano CLI commands which check basic actions with
bundles.
"""
def test_bundle_import_without_bundle_name(self):
"""Test scenario:
1) Execute murano bundle-import command without bundle name
2) check that error message contains user friendly substring
"""
result = self.murano('bundle-import', params='',
fail_ok=True)
self.assertIn("murano bundle-import: error: the following "
"arguments are required", result)
@unittest.skip("Skip due to apps.openstack.org website is retired.")
def test_bundle_import_with_non_existing_package_name(self):
"""Test scenario:
1) Execute murano bundle-import command with non-existing packages
name inside
2) check that error message contains user friendly substring
"""
result = self.murano(
'bundle-import',
params=self.prepare_bundle_with_non_existed_package(),
fail_ok=False)
self.assertIn("Couldn't find file for package", result)
self.assertIn("Error Got non-ok status(404) while connecting", result)
@unittest.skip("Skip due to apps.openstack.org website is retired.")
def test_bundle_import_with_non_existing_name(self):
"""Test scenario:
1) Execute murano bundle-import command with non-existing bundle
name
2) check that error message contains user friendly substring
"""
result = self.murano('bundle-import', params=self.app_name,
fail_ok=True)
self.assertIn("Bundle file '{}' does not exist".format(self.app_name),
result)
self.assertIn("reason: Got non-ok status(404) while connecting to",
result)
def test_bundle_import_with_invalid_file_format(self):
"""Test scenario:
1) Execute murano bundle-import command with invalid bundle file
format
2) check that error message contains user friendly substring
"""
try:
self.murano(
'bundle-import',
params=self.prepare_bundle_with_invalid_format(),
fail_ok=False)
except utils.exceptions.CommandFailed as exception:
self.assertIn("Can't parse bundle contents", str(exception))
class StaticActionMuranoClientTest(utils.CLIUtilsTestPackagesBase):
"""Tests for testing static actions execution.
Tests for the Murano CLI commands which check the result of sample
static action execution.
"""
def test_static_action_call(self):
"""Test scenario:
1) import package
2) call static action of the class in that package
3) check the result of action
"""
package = self.import_package(
self.app_name,
self.dummy_app_path
)
result = self.murano(
'static-action-call', params='{0} staticAction --package-name {1} '
'--arguments myName=John myAge=28'.format(package['FQN'],
package['FQN']))
expected = "Waiting for result...\nStatic action result: Hello, " \
"John. In 5 years you will be 33 years old.\n"
self.assertEqual(expected, result)

View File

@ -1,61 +0,0 @@
# Copyright (c) 2015 Mirantis, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import os
import zipfile
import yaml
MANIFEST = {'Format': 'MuranoPL/1.0',
'Type': 'Application',
'Description': 'MockApp for CLI tests',
'Author': 'Mirantis, Inc'}
def compose_package(app_name, manifest, package_dir,
require=None, archive_dir=None):
"""Composes a murano package
Composes package `app_name` with `manifest` file as a template for the
manifest and files from `package_dir`.
Includes `require` section if any in the manifest file.
Puts the resulting .zip file into `archive_dir` if present or in the
`package_dir`.
"""
with open(manifest, 'w') as f:
fqn = 'io.murano.apps.' + app_name
mfest_copy = MANIFEST.copy()
mfest_copy['FullName'] = fqn
mfest_copy['Name'] = app_name
mfest_copy['Classes'] = {fqn: 'mock_muranopl.yaml'}
if require:
mfest_copy['Require'] = require
f.write(yaml.dump(mfest_copy, default_flow_style=False))
name = app_name + '.zip'
if not archive_dir:
archive_dir = os.path.dirname(os.path.abspath(__file__))
archive_path = os.path.join(archive_dir, name)
with zipfile.ZipFile(archive_path, 'w') as zip_file:
for root, dirs, files in os.walk(package_dir):
for f in files:
zip_file.write(
os.path.join(root, f),
arcname=os.path.join(os.path.relpath(root, package_dir), f)
)
return archive_path

View File

@ -1,70 +0,0 @@
# Copyright (c) 2014 Mirantis, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import configparser
import os
from tempest.lib.cli import base
class ClientTestBase(base.ClientTestBase):
def murano(self, action, flags='', params='',
fail_ok=False, endpoint_type='publicURL', merge_stderr=True):
flags += self.get_backend_flag()
return self.clients.cmd_with_auth(
'murano', action, flags, params, fail_ok, merge_stderr)
def _get_clients(self):
cli_dir = os.environ.get(
'OS_MURANOCLIENT_EXEC_DIR',
os.path.join(os.path.abspath('.'), '.tox/functional/bin'))
self.username = os.environ.get('OS_USERNAME')
self.password = os.environ.get('OS_PASSWORD')
self.tenant_name = os.environ.get('OS_PROJECT_NAME',
os.environ.get('OS_TENANT_NAME'))
self.uri = os.environ.get('OS_AUTH_URL')
config = configparser.RawConfigParser()
if config.read('functional_creds.conf'):
# the OR pattern means the environment is preferred for
# override
self.username = self.username or config.get('admin', 'user')
self.password = self.password or config.get('admin', 'pass')
self.tenant_name = self.tenant_name or config.get('admin',
'tenant')
self.uri = self.uri or config.get('auth', 'uri')
clients = base.CLIClient(
username=self.username,
password=self.password,
tenant_name=self.tenant_name,
uri=self.uri,
cli_dir=cli_dir
)
return clients
def listing(self, command, params=""):
return self.parser.listing(self.murano(command, params=params))
def get_value(self, need_field, known_field, known_value, somelist):
for element in somelist:
if element[known_field] == known_value:
return element[need_field]
@staticmethod
def get_backend_flag():
backend = os.environ.get('MURANO_PACKAGES_SERVICE', 'murano')
backend_flag = " --murano-packages-service {0} ".format(backend)
return backend_flag

View File

@ -1,44 +0,0 @@
# Copyright 2011 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 os
import fixtures
import testtools
class TestCaseShell(testtools.TestCase):
TEST_REQUEST_BASE = {
'verify': True,
}
def setUp(self):
super(TestCaseShell, self).setUp()
if (os.environ.get('OS_STDOUT_CAPTURE') == 'True' or
os.environ.get('OS_STDOUT_CAPTURE') == '1'):
stdout = self.useFixture(fixtures.StringStream('stdout')).stream
self.useFixture(fixtures.MonkeyPatch('sys.stdout', stdout))
if (os.environ.get('OS_STDERR_CAPTURE') == 'True' or
os.environ.get('OS_STDERR_CAPTURE') == '1'):
stderr = self.useFixture(fixtures.StringStream('stderr')).stream
self.useFixture(fixtures.MonkeyPatch('sys.stderr', stderr))
class TestAdditionalAsserts(testtools.TestCase):
def check_dict_is_subset(self, dict1, dict2):
# There is an assert for this in Python 2.7 but not 2.6
self.assertTrue(all(k in dict2 and dict2[k] == v for k, v
in dict1.items()))

View File

@ -1,47 +0,0 @@
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from oslo_serialization import jsonutils
class FakeHTTPResponse(object):
version = 1.1
def __init__(self, status_code, reason, headers, content):
self.headers = headers
self.content = content
self.status_code = status_code
self.reason = reason
self.raw = FakeRaw()
def getheader(self, name, default=None):
return self.headers.get(name, default)
def getheaders(self):
return self.headers.items()
def read(self, amt=None):
b = self.content
self.content = None
return b
def iter_content(self, chunksize):
return self.content
def json(self):
return jsonutils.loads(self.content)
class FakeRaw(object):
version = 110

View File

@ -1,18 +0,0 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
Format: 1.3
Type: Application
FullName: empty
Name: empty
Description: empty description
Author: 'Mirantis, Inc'

View File

@ -1 +0,0 @@
heat_template_version: 2013-05-23

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

View File

@ -1,35 +0,0 @@
Namespaces:
=: io.murano.apps.test
std: io.murano
res: io.murano.resources
Name: APP
Extends: std:Application
Properties:
name:
Contract: $.string().notNull()
instance:
Contract: $.class(res:Instance).notNull()
Workflow:
initialize:
Body:
- $.environment: $.find(std:Environment).require()
deploy:
Body:
- $securityGroupIngress:
- ToPort: 23
FromPort: 23
IpProtocol: tcp
External: True
- $.environment.securityGroupManager.addGroupIngress($securityGroupIngress)
- $.instance.deploy()
- $resources: new('io.murano.system.Resources')
- $template: $resources.yaml('Deploy.template')
- $.instance.agent.call($template, $resources)

View File

@ -1,21 +0,0 @@
FormatVersion: 2.0.0
Version: 1.0.0
Name: Deploy
Parameters:
appName: $appName
Body: |
return deploy(args.appName).stdout
Scripts:
deploy:
Type: Application
Version: 1.0.0
EntryPoint: deploy.sh
Files:
- installer.sh
- common.sh
Options:
captureStdout: true
captureStderr: false

View File

@ -1 +0,0 @@
Version: 2

View File

@ -1,34 +0,0 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from unittest import mock
from muranoclient.osc import plugin
from muranoclient.tests.unit import base
class TestApplicationCatalogPlugin(base.TestCaseShell):
@mock.patch("muranoclient.v1.client.Client")
def test_make_client(self, p_client):
instance = mock.Mock()
instance._api_version = {"application_catalog": '1'}
instance._region_name = 'murano_region'
instance.session = 'murano_session'
plugin.make_client(instance)
p_client.assert_called_with(
mock.ANY,
region_name='murano_region',
session='murano_session',
service_type='application-catalog')

View File

@ -1,22 +0,0 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from osc_lib.tests import utils
from unittest import mock
class TestApplicationCatalog(utils.TestCommand):
def setUp(self):
super(TestApplicationCatalog, self).setUp()
self.app.client_manager.application_catalog = mock.Mock()

View File

@ -1 +0,0 @@
heat_template_version: 2013-05-23

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

View File

@ -1,35 +0,0 @@
Namespaces:
=: io.murano.apps.test
std: io.murano
res: io.murano.resources
Name: APP
Extends: std:Application
Properties:
name:
Contract: $.string().notNull()
instance:
Contract: $.class(res:Instance).notNull()
Workflow:
initialize:
Body:
- $.environment: $.find(std:Environment).require()
deploy:
Body:
- $securityGroupIngress:
- ToPort: 23
FromPort: 23
IpProtocol: tcp
External: True
- $.environment.securityGroupManager.addGroupIngress($securityGroupIngress)
- $.instance.deploy()
- $resources: new('io.murano.system.Resources')
- $template: $resources.yaml('Deploy.template')
- $.instance.agent.call($template, $resources)

View File

@ -1,21 +0,0 @@
FormatVersion: 2.0.0
Version: 1.0.0
Name: Deploy
Parameters:
appName: $appName
Body: |
return deploy(args.appName).stdout
Scripts:
deploy:
Type: Application
Version: 1.0.0
EntryPoint: deploy.sh
Files:
- installer.sh
- common.sh
Options:
captureStdout: true
captureStderr: false

View File

@ -1,80 +0,0 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from unittest import mock
from muranoclient.osc.v1 import action as osc_action
from muranoclient.tests.unit.osc.v1 import fakes
from muranoclient.v1 import static_actions as api_static_actions
class TestAction(fakes.TestApplicationCatalog):
def setUp(self):
super(TestAction, self).setUp()
self.static_actions_mock = \
self.app.client_manager.application_catalog.static_actions
class TestStaticActionCall(TestAction):
def setUp(self):
super(TestStaticActionCall, self).setUp()
self.static_actions_mock.call.return_value = \
api_static_actions.StaticActionResult('result')
# Command to test
self.cmd = osc_action.StaticActionCall(self.app, None)
@mock.patch('osc_lib.utils.get_item_properties')
def test_static_action_call_basic(self, mock_util):
mock_util.return_value = 'result'
arglist = ['class.name', 'method.name']
verifylist = [('class_name', 'class.name'),
('method_name', 'method.name')]
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
columns, data = self.cmd.take_action(parsed_args)
# Check that columns are correct
expected_columns = ['Static action result']
self.assertEqual(expected_columns, columns)
# Check that data is correct
expected_data = ['result']
self.assertEqual(expected_data, data)
@mock.patch('osc_lib.utils.get_item_properties')
def test_static_action_call_full(self, mock_util):
mock_util.return_value = 'result'
arglist = ['class.name', 'method.name',
'--arguments', 'food=spam', 'parrot=dead',
'--package-name', 'package.name',
'--class-version', '>1']
verifylist = [('class_name', 'class.name'),
('method_name', 'method.name'),
('arguments', ['food=spam', 'parrot=dead']),
('package_name', 'package.name'),
('class_version', '>1')]
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
columns, data = self.cmd.take_action(parsed_args)
# Check that columns are correct
expected_columns = ['Static action result']
self.assertEqual(expected_columns, columns)
# Check that data is correct
expected_data = ['result']
self.assertEqual(expected_data, data)

View File

@ -1,149 +0,0 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from unittest import mock
from muranoclient.osc.v1 import category as osc_category
from muranoclient.tests.unit.osc.v1 import fakes
from muranoclient.v1 import categories as api_category
from muranoclient.v1 import packages as api_packages
CATEGORY_INFO = {'id': 'xyz123',
'name': 'fake1',
'packages': [{'name': 'package1'}, {'name': 'package2'}]}
class TestCategory(fakes.TestApplicationCatalog):
def setUp(self):
super(TestCategory, self).setUp()
self.category_mock = self.app.client_manager.application_catalog.\
categories
self.category_mock.reset_mock()
self.packages_mock = \
self.app.client_manager.application_catalog.packages
class TestListCategories(TestCategory):
def setUp(self):
super(TestListCategories, self).setUp()
self.category_mock.list.return_value = [api_category.Category(None,
CATEGORY_INFO)]
# Command to test
self.cmd = osc_category.ListCategories(self.app, None)
@mock.patch('osc_lib.utils.get_item_properties')
def test_category_list(self, mock_util):
mock_util.return_value = ('xyz123', 'fake1')
columns, data = self.cmd.take_action(parsed_args=None)
# Check that columns are correct
expected_columns = ['ID', 'Name']
self.assertEqual(expected_columns, columns)
# Check that data is correct
expected_data = [('xyz123', 'fake1')]
self.assertEqual(expected_data, data)
class TestShowCategory(TestCategory):
def setUp(self):
super(TestShowCategory, self).setUp()
self.category_mock.get.return_value = api_category.\
Category(None, CATEGORY_INFO)
self.packages_mock.filter.return_value = [
api_packages.Package(None, pkg_info) for pkg_info in CATEGORY_INFO[
'packages']
]
# Command to test
self.cmd = osc_category.ShowCategory(self.app, None)
@mock.patch('textwrap.wrap')
def test_category_show(self, mock_wrap):
arglist = ['xyz123']
verifylist = [('id', 'xyz123')]
mock_wrap.return_value = ['package1, package2']
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
columns, data = self.cmd.take_action(parsed_args)
# Check that columns are correct
expected_columns = ('id', 'name', 'packages')
self.assertEqual(expected_columns, columns)
# Check that data is correct
expected_data = ('xyz123', 'fake1', 'package1, package2')
self.assertEqual(expected_data, data)
class TestCreateCategory(TestCategory):
def setUp(self):
super(TestCreateCategory, self).setUp()
self.category_mock.add.return_value = [api_category.Category(None,
CATEGORY_INFO)]
# Command to test
self.cmd = osc_category.CreateCategory(self.app, None)
@mock.patch('osc_lib.utils.get_item_properties')
def test_category_list(self, mock_util):
arglist = ['fake1']
verifylist = [('name', 'fake1')]
mock_util.return_value = ('xyz123', 'fake1')
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
columns, data = self.cmd.take_action(parsed_args)
# Check that columns are correct
expected_columns = ['ID', 'Name']
self.assertEqual(expected_columns, columns)
# Check that data is correct
expected_data = [('xyz123', 'fake1')]
self.assertEqual(expected_data, data)
class TestDeleteCategory(TestCategory):
def setUp(self):
super(TestDeleteCategory, self).setUp()
self.category_mock.delete.return_value = None
self.category_mock.list.return_value = [api_category.Category(None,
CATEGORY_INFO)]
# Command to test
self.cmd = osc_category.DeleteCategory(self.app, None)
@mock.patch('osc_lib.utils.get_item_properties')
def test_category_list(self, mock_util):
arglist = ['abc123', '123abc']
verifylist = [('id', ['abc123', '123abc'])]
mock_util.return_value = ('xyz123', 'fake1')
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
columns, data = self.cmd.take_action(parsed_args)
# Check that columns are correct
expected_columns = ['ID', 'Name']
self.assertEqual(expected_columns, columns)
# Check that data is correct
expected_data = [('xyz123', 'fake1')]
self.assertEqual(expected_data, data)

View File

@ -1,82 +0,0 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from unittest import mock
from muranoclient.osc.v1 import deployment as osc_deployment
from muranoclient.tests.unit.osc.v1 import fakes
from muranoclient.v1 import deployments as api_deployment
DEPLOYMENT_COLUMNS = ('id', 'state', 'created', 'updated', 'finished')
DEPLOYMENT_DATA = ('xyz123', 'success', '2016-06-25T12:21:37',
'2016-06-25T12:21:47', '2016-06-25T12:21:47')
ALL_DEPLOYMENT_DATA = (('abc123', 'success', '2016-06-25T12:21:37',
'2016-06-25T12:21:47', '2016-06-25T12:21:47'),
('xyz456', 'success', '2017-01-31T11:22:35',
'2017-01-31T11:22:47', '2017-01-31T11:22:47'))
class TestDeployment(fakes.TestApplicationCatalog):
def setUp(self):
super(TestDeployment, self).setUp()
self.deployment_mock = self.app.client_manager.application_catalog.\
deployments
self.deployment_mock.reset_mock()
self.environment_mock = self.app.client_manager.application_catalog.\
environments
class TestListDeployment(TestDeployment):
def setUp(self):
super(TestListDeployment, self).setUp()
deployment_info = dict(zip(DEPLOYMENT_COLUMNS, DEPLOYMENT_DATA))
self.deployment_mock.list.return_value = \
[api_deployment.Deployment(None, deployment_info)]
# Command to test
self.cmd = osc_deployment.ListDeployment(self.app, None)
@mock.patch('osc_lib.utils.get_item_properties')
def test_deployment_list(self, mock_util):
arglist = ['xyz123']
verifylist = [('id', 'xyz123')]
mock_util.return_value = DEPLOYMENT_DATA
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
columns, data = self.cmd.take_action(parsed_args)
# Check that columns are correct
expected_columns = [c.title() for c in DEPLOYMENT_COLUMNS]
self.assertEqual(expected_columns, columns)
# Check that data is correct
expected_data = [DEPLOYMENT_DATA]
self.assertEqual(expected_data, data)
@mock.patch('osc_lib.utils.get_item_properties', autospec=True)
def test_deployment_list_all_environments(self, mock_util):
arglist = ['--all-environments']
verifylist = [('id', None), ('all_environments', True)]
mock_util.return_value = ALL_DEPLOYMENT_DATA
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
columns, data = self.cmd.take_action(parsed_args)
# Check that columns are correct
expected_columns = [c.title() for c in DEPLOYMENT_COLUMNS]
self.assertEqual(expected_columns, columns)
# Check that data is correct
expected_data = [ALL_DEPLOYMENT_DATA]
self.assertEqual(expected_data, data)

View File

@ -1,584 +0,0 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import collections
import json
import tempfile
from unittest import mock
from muranoclient.osc.v1 import environment as osc_env
from muranoclient.tests.unit.osc.v1 import fakes
from muranoclient.v1 import environments as api_env
ENV_INFO = {'id': '1234',
'name': 'Fake Environment',
'created': '2015-12-16T17:31:54',
'updated': '2015-12-16T17:31:54',
'networking': {},
'services': ['fake services'],
'status': 'fake deployed',
'tenant_id': 'xyz123',
'version': '1'}
ENV_MODEL = {
"defaultNetworks": {
"environment": {
"name": "env-network",
"?": {
"type": "io.murano.resources.NeutronNetwork",
"id": "5678"
}
},
"flat": None
},
"region": "RegionOne",
"name": "env",
"?": {
"updated": "2016-10-03 09:33:41.039789",
"type": "io.murano.Environment",
"id": "1234"
}
}
class TestEnvironment(fakes.TestApplicationCatalog):
def setUp(self):
super(TestEnvironment, self).setUp()
self.environment_mock = self.app.client_manager.application_catalog.\
environments
self.session_mock = self.app.client_manager.application_catalog.\
sessions
self.services_mock = self.app.client_manager.application_catalog.\
services
self.environment_mock.reset_mock()
class TestListEnvironment(TestEnvironment):
def setUp(self):
super(TestListEnvironment, self).setUp()
self.environment_mock.list.return_value = [api_env.Environment(None,
ENV_INFO)]
# Command to test
self.cmd = osc_env.ListEnvironments(self.app, None)
@mock.patch('osc_lib.utils.get_item_properties')
def test_environment_list_with_no_options(self, mock_util):
arglist = []
verifylist = []
mock_util.return_value = ('1234', 'Environment of all tenants',
'fake deployed', '2015-12-16T17:31:54',
'2015-12-16T17:31:54'
)
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
columns, data = self.cmd.take_action(parsed_args)
# Check that columns are correct
expected_columns = ['Id', 'Name', 'Status', 'Created', 'Updated']
self.assertEqual(expected_columns, columns)
# Check that data is correct
expected_data = [('1234', 'Environment of all tenants',
'fake deployed', '2015-12-16T17:31:54',
'2015-12-16T17:31:54')]
self.assertEqual(expected_data, data)
@mock.patch('osc_lib.utils.get_item_properties')
def test_environment_list_with_all_tenants(self, mock_util):
arglist = ['--all-tenants']
verifylist = [('all_tenants', True), ('tenant', None)]
mock_util.return_value = ('1234', 'Environment of all tenants',
'fake deployed', '2015-12-16T17:31:54',
'2015-12-16T17:31:54'
)
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
columns, data = self.cmd.take_action(parsed_args)
# Check that columns are correct
expected_columns = ['Id', 'Name', 'Status', 'Created', 'Updated']
self.assertEqual(expected_columns, columns)
# Check that data is correct
expected_data = [('1234', 'Environment of all tenants',
'fake deployed', '2015-12-16T17:31:54',
'2015-12-16T17:31:54')]
self.assertEqual(expected_data, data)
self.environment_mock.list.assert_called_once_with(True, None)
@mock.patch('osc_lib.utils.get_item_properties')
def test_environment_list_with_tenant(self, mock_util):
arglist = ['--tenant=ABC']
verifylist = [('all_tenants', False), ('tenant', 'ABC')]
mock_util.return_value = ('1234', 'Environment of tenant ABC',
'fake deployed', '2015-12-16T17:31:54',
'2015-12-16T17:31:54'
)
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
columns, data = self.cmd.take_action(parsed_args)
# Check that columns are correct
expected_columns = ['Id', 'Name', 'Status', 'Created', 'Updated']
self.assertEqual(expected_columns, columns)
# Check that data is correct
expected_data = [('1234', 'Environment of tenant ABC',
'fake deployed', '2015-12-16T17:31:54',
'2015-12-16T17:31:54')]
self.assertEqual(expected_data, data)
self.environment_mock.list.assert_called_once_with(False, 'ABC')
class TestShowEnvironment(TestEnvironment):
def setUp(self):
super(TestShowEnvironment, self).setUp()
mock_to_dict = self.environment_mock.get.return_value.to_dict
mock_to_dict.return_value = ENV_INFO
self.cmd = osc_env.ShowEnvironment(self.app, None)
@mock.patch('oslo_serialization.jsonutils.dumps')
def test_environment_show_with_no_options(self, mock_json):
arglist = ['fake']
verifylist = [('id', 'fake')]
mock_json.return_value = ['fake services']
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
columns, data = self.cmd.take_action(parsed_args)
# Check that columns are correct
expected_columns = ('created', 'id', 'name', 'networking', 'services',
'status', 'tenant_id', 'updated', 'version')
self.assertEqual(expected_columns, columns)
# Check that data is correct
expected_data = ('2015-12-16T17:31:54', '1234', 'Fake Environment',
{}, ['fake services'], 'fake deployed', 'xyz123',
'2015-12-16T17:31:54', '1')
self.assertEqual(expected_data, data)
@mock.patch('oslo_serialization.jsonutils.dumps')
def test_environment_show_with_only_app_option(self, mock_json):
arglist = ['fake', '--only-apps']
verifylist = [('id', 'fake'), ('only_apps', True)]
mock_json.return_value = ['fake services']
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
columns, data = self.cmd.take_action(parsed_args)
# Check that columns are correct
expected_columns = ['services']
self.assertEqual(expected_columns, columns)
# Check that data is correct
expected_data = [['fake services']]
self.assertEqual(expected_data, data)
@mock.patch('oslo_serialization.jsonutils.dumps')
def test_environment_show_with_session_id_option(self, mock_json):
arglist = ['fake', '--session-id', 'abc123']
verifylist = [('id', 'fake'), ('session_id', 'abc123')]
mock_json.return_value = ['fake services']
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
columns, data = self.cmd.take_action(parsed_args)
# Check that columns are correct
expected_columns = ('created', 'id', 'name', 'networking', 'services',
'status', 'tenant_id', 'updated', 'version')
self.assertEqual(expected_columns, columns)
# Check that data is correct
expected_data = ('2015-12-16T17:31:54', '1234', 'Fake Environment',
{}, ['fake services'], 'fake deployed', 'xyz123',
'2015-12-16T17:31:54', '1')
self.assertEqual(expected_data, data)
class TestRenameEnvironment(TestEnvironment):
def setUp(self):
super(TestRenameEnvironment, self).setUp()
self.environment_mock.update.return_value = [api_env.Environment(None,
ENV_INFO)]
# Command to test
self.cmd = osc_env.RenameEnvironment(self.app, None)
@mock.patch('osc_lib.utils.get_item_properties')
def test_environment_rename(self, mock_util):
arglist = ['1234', 'fake-1']
verifylist = [('id', '1234'), ('name', 'fake-1')]
mock_util.return_value = ('1234', 'fake-1', 'fake deployed',
'2015-12-16T17:31:54', '2015-12-16T17:31:54'
)
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
columns, data = self.cmd.take_action(parsed_args)
# Check that columns are correct
expected_columns = ['Id', 'Name', 'Status', 'Created', 'Updated']
self.assertEqual(expected_columns, columns)
# Check that data is correct
expected_data = [('1234', 'fake-1', 'fake deployed',
'2015-12-16T17:31:54', '2015-12-16T17:31:54')]
self.assertEqual(expected_data, data)
class TestEnvironmentSessionCreate(TestEnvironment):
def setUp(self):
super(TestEnvironmentSessionCreate, self).setUp()
# Command to test
self.cmd = osc_env.EnvironmentSessionCreate(self.app, None)
@mock.patch('muranoclient.common.utils.text_wrap_formatter')
def test_environment_session_create(self, mock_util):
arglist = ['1234']
verifylist = [('id', '1234')]
mock_util.return_value = '1abc2xyz'
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
columns, data = self.cmd.take_action(parsed_args)
# Check that columns are correct
expected_columns = ['id']
self.assertEqual(expected_columns, columns)
# Check that data is correct
expected_data = ['1abc2xyz']
self.assertEqual(expected_data, data)
class TestEnvironmentCreate(TestEnvironment):
def setUp(self):
super(TestEnvironmentCreate, self).setUp()
self.environment_mock.create.return_value = [api_env.Environment(None,
ENV_INFO)]
# Command to test
self.cmd = osc_env.EnvironmentCreate(self.app, None)
@mock.patch('osc_lib.utils.get_item_properties')
def test_environment_create_with_no_option(self, mock_util):
arglist = ['fake']
verifylist = [('name', 'fake')]
mock_util.return_value = ('1234', 'fake', 'ready',
'2015-12-16T17:31:54', '2015-12-16T17:31:54')
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
columns, data = self.cmd.take_action(parsed_args)
# Check that columns are correct
expected_columns = ['Id', 'Name', 'Status', 'Created', 'Updated']
self.assertEqual(expected_columns, columns)
# Check that data is correct
expected_data = [('1234', 'fake', 'ready',
'2015-12-16T17:31:54', '2015-12-16T17:31:54')]
self.assertEqual(expected_data, data)
@mock.patch('osc_lib.utils.get_item_properties')
def test_environment_create_with_region_option(self, mock_util):
arglist = ['fake', '--region', 'region_one']
verifylist = [('name', 'fake'), ('region', 'region_one')]
mock_util.return_value = ('1234', 'fake', 'ready',
'2015-12-16T17:31:54', '2015-12-16T17:31:54')
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
columns, data = self.cmd.take_action(parsed_args)
# Check that correct arguments are passed
self.environment_mock.create.assert_has_calls([mock.call(
{'name': 'fake', 'region': 'region_one'})])
# Check that columns are correct
expected_columns = ['Id', 'Name', 'Status', 'Created', 'Updated']
self.assertEqual(expected_columns, columns)
# Check that data is correct
expected_data = [('1234', 'fake', 'ready',
'2015-12-16T17:31:54', '2015-12-16T17:31:54')]
self.assertEqual(expected_data, data)
@mock.patch('osc_lib.utils.get_item_properties')
def test_environment_create_with_net_option(self, mock_util):
arglist = ['fake', '--join-net-id', 'x1y2z3']
verifylist = [('name', 'fake'), ('join_net_id', 'x1y2z3')]
mock_util.return_value = ('1234', 'fake', 'ready',
'2015-12-16T17:31:54', '2015-12-16T17:31:54')
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
columns, data = self.cmd.take_action(parsed_args)
expected_call = {
'defaultNetworks': {
'environment': {
'internalNetworkName': 'x1y2z3',
'?': {
'type': 'io.murano.resources.ExistingNeutronNetwork',
'id': mock.ANY
}
},
'flat': None
},
'name': 'fake',
'region': None
}
# Check that correct arguments are passed
self.environment_mock.create.assert_called_with(expected_call)
# Check that columns are correct
expected_columns = ['Id', 'Name', 'Status', 'Created', 'Updated']
self.assertEqual(expected_columns, columns)
# Check that data is correct
expected_data = [('1234', 'fake', 'ready',
'2015-12-16T17:31:54', '2015-12-16T17:31:54')]
self.assertEqual(expected_data, data)
@mock.patch('osc_lib.utils.get_item_properties')
def test_environment_create_with_subnet_option(self, mock_util):
arglist = ['fake', '--join-subnet-id', 'x1y2z3']
verifylist = [('name', 'fake'), ('join_subnet_id', 'x1y2z3')]
mock_util.return_value = ('1234', 'fake', 'ready',
'2015-12-16T17:31:54', '2015-12-16T17:31:54')
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
columns, data = self.cmd.take_action(parsed_args)
expected_call = {
'defaultNetworks': {
'environment': {
'internalSubnetworkName': 'x1y2z3',
'?': {
'type': 'io.murano.resources.ExistingNeutronNetwork',
'id': mock.ANY
}
},
'flat': None
},
'name': 'fake',
'region': None
}
# Check that correct arguments are passed
self.environment_mock.create.assert_called_with(expected_call)
# Check that columns are correct
expected_columns = ['Id', 'Name', 'Status', 'Created', 'Updated']
self.assertEqual(expected_columns, columns)
# Check that data is correct
expected_data = [('1234', 'fake', 'ready',
'2015-12-16T17:31:54', '2015-12-16T17:31:54')]
self.assertEqual(expected_data, data)
class TestEnvironmentDelete(TestEnvironment):
def setUp(self):
super(TestEnvironmentDelete, self).setUp()
self.environment_mock.delete.return_value = None
self.environment_mock.list.return_value = [api_env.Environment(None,
ENV_INFO)]
# Command to test
self.cmd = osc_env.EnvironmentDelete(self.app, None)
@mock.patch('osc_lib.utils.get_item_properties')
def test_environment_delete(self, mock_util):
arglist = ['fake1', 'fake2']
verifylist = [('id', ['fake1', 'fake2'])]
mock_util.return_value = ('1234', 'Environment of all tenants',
'fake deployed', '2015-12-16T17:31:54',
'2015-12-16T17:31:54'
)
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
columns, data = self.cmd.take_action(parsed_args)
# Check that columns are correct
expected_columns = ['Id', 'Name', 'Status', 'Created', 'Updated']
self.assertEqual(expected_columns, columns)
# Check that data is correct
expected_data = [('1234', 'Environment of all tenants',
'fake deployed', '2015-12-16T17:31:54',
'2015-12-16T17:31:54')]
self.assertEqual(expected_data, data)
class TestEnvironmentDeploy(TestEnvironment):
def setUp(self):
super(TestEnvironmentDeploy, self).setUp()
mock_to_dict = self.environment_mock.get.return_value.to_dict
mock_to_dict.return_value = ENV_INFO
# Command to test
self.cmd = osc_env.EnvironmentDeploy(self.app, None)
@mock.patch('oslo_serialization.jsonutils.dumps')
def test_environment_deploy(self, mock_json):
arglist = ['fake', '--session-id', 'abc123']
verifylist = [('id', 'fake'), ('session_id', 'abc123')]
mock_json.return_value = ['fake services']
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
columns, data = self.cmd.take_action(parsed_args)
# Check that columns are correct
expected_columns = ('created', 'id', 'name', 'networking', 'services',
'status', 'tenant_id', 'updated', 'version')
self.assertEqual(expected_columns, columns)
# Check that data is correct
expected_data = ('2015-12-16T17:31:54', '1234', 'Fake Environment',
{}, ['fake services'], 'fake deployed', 'xyz123',
'2015-12-16T17:31:54', '1')
self.assertEqual(expected_data, data)
class TestEnvironmentAppsEdit(TestEnvironment):
def setUp(self):
super(TestEnvironmentAppsEdit, self).setUp()
# Command to test
self.cmd = osc_env.EnvironmentAppsEdit(self.app, None)
def test_environment_apps_edit(self):
fake = collections.namedtuple('fakeEnv', 'services')
self.environment_mock.get.side_effect = [
fake(services=[
{'?': {'name': "foo"}}
]),
]
temp_file = tempfile.NamedTemporaryFile(prefix="murano-test", mode='w')
json.dump([
{'op': 'replace', 'path': '/0/?/name',
'value': "dummy"
}
], temp_file)
temp_file.file.flush()
arglist = ['fake', '--session-id', 'abc123', temp_file.name]
parsed_args = self.check_parser(self.cmd, arglist, [])
self.cmd.take_action(parsed_args)
self.services_mock.put.assert_called_once_with(
'fake',
session_id='abc123',
path='/',
data=[{'?': {'name': 'dummy'}}]
)
class TestEnvironmentModelShow(TestEnvironment):
def setUp(self):
super(TestEnvironmentModelShow, self).setUp()
self.env_mock = \
self.app.client_manager.application_catalog.environments
self.env_mock.get_model.return_value = ENV_MODEL
# Command to test
self.cmd = osc_env.EnvironmentModelShow(self.app, None)
def test_environment_model_show_basic(self):
arglist = ['env-id']
verifylist = [('id', 'env-id')]
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
columns, data = self.cmd.take_action(parsed_args)
# Check that columns are correct
expected_columns = ('?', 'defaultNetworks', 'name', 'region')
self.assertEqual(expected_columns, columns)
# Check that data is correct
self.assertCountEqual(ENV_MODEL.values(), data)
def test_environment_model_show_full(self):
arglist = ['env-id', '--path', '/path', '--session-id', 'sess-id']
verifylist = [('id', 'env-id'), ('path', '/path'),
('session_id', 'sess-id')]
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
columns, data = self.cmd.take_action(parsed_args)
# Check that columns are correct
expected_columns = ('?', 'defaultNetworks', 'name', 'region')
self.assertEqual(expected_columns, columns)
# Check that data is correct
self.assertCountEqual(ENV_MODEL.values(), data)
class TestEnvironmentModelEdit(TestEnvironment):
def setUp(self):
super(TestEnvironmentModelEdit, self).setUp()
self.env_mock = \
self.app.client_manager.application_catalog.environments
self.env_mock.update_model.return_value = ENV_MODEL
# Command to test
self.cmd = osc_env.EnvironmentModelEdit(self.app, None)
def test_environment_model_edit(self):
temp_file = tempfile.NamedTemporaryFile(prefix="murano-test", mode='w')
patch = [{'op': 'replace', 'path': '/name', 'value': 'dummy'}]
json.dump(patch, temp_file)
temp_file.file.flush()
arglist = ['env-id', temp_file.name, '--session-id', 'sess-id']
verifylist = [('id', 'env-id'), ('filename', temp_file.name),
('session_id', 'sess-id')]
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
columns, data = self.cmd.take_action(parsed_args)
# Check that columns are correct
expected_columns = ('?', 'defaultNetworks', 'name', 'region')
self.assertEqual(expected_columns, columns)
# Check that data is correct
self.assertCountEqual(ENV_MODEL.values(), data)

View File

@ -1,781 +0,0 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import io
import json
import os
import shutil
import sys
import tempfile
from unittest import mock
from testtools import matchers
from muranoclient.common import exceptions as common_exceptions
from muranoclient.common import utils as mc_utils
from muranoclient.osc.v1 import package as osc_pkg
from muranoclient.tests.unit.osc.v1 import fakes
from muranoclient.tests.unit import test_utils
from muranoclient.v1 import packages
from osc_lib import exceptions as exc
from osc_lib import utils
import requests_mock
make_pkg = test_utils.make_pkg
FIXTURE_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__),
'fixture_data'))
COLUMNS = ['Id', 'Name', 'Fully_qualified_name', 'Author', 'Active',
'Is public', 'Type', 'Version']
DATA = {
'class_definitions': ['com.example.apache.ApacheHttpServer'],
'updated': '2016-09-20T06:23:45.000000',
'description': 'Test description.\n',
'created': '2016-09-20T06:23:15.000000',
'author': 'Mirantis, Inc',
'enabled': True,
'owner_id': 'a203405ea871484a940850d6c0b8dfd9',
'tags': ['Server', 'WebServer', 'Apache', 'HTTP', 'HTML'],
'is_public': False,
'fully_qualified_name': 'com.example.apache.ApacheHttpServer',
'type': 'Application',
'id': '46860070-5f8a-4936-96e8-d7b89e5187d7',
'categories': [],
'name': 'Apache HTTP Server'
}
class TestPackage(fakes.TestApplicationCatalog):
def setUp(self):
super(TestPackage, self).setUp()
self.package_mock = self.app.client_manager.application_catalog.\
packages
self.package_mock.reset_mock()
class TestCreatePackage(TestPackage):
def setUp(self):
super(TestCreatePackage, self).setUp()
# Command to test
self.cmd = osc_pkg.CreatePackage(self.app, None)
def test_create_package_without_args(self):
arglist = []
parsed_args = self.check_parser(self.cmd, arglist, [])
error = self.assertRaises(exc.CommandError,
self.cmd.take_action, parsed_args)
self.assertEqual('Provide --template for a HOT-based package, OR at '
'least --classes-dir for a MuranoPL-based package',
str(error))
def test_create_package_template_and_classes_args(self):
heat_template = os.path.join(FIXTURE_DIR, 'heat-template.yaml')
classes_dir = os.path.join(FIXTURE_DIR, 'test-app', 'Classes')
arglist = ['--template', heat_template, '--classes-dir', classes_dir]
parsed_args = self.check_parser(self.cmd, arglist, [])
error = self.assertRaises(exc.CommandError,
self.cmd.take_action, parsed_args)
self.assertEqual('Provide --template for a HOT-based package, OR'
' --classes-dir for a MuranoPL-based package',
str(error))
def test_create_hot_based_package(self):
with tempfile.NamedTemporaryFile() as f:
RESULT_PACKAGE = f.name
heat_template = os.path.join(FIXTURE_DIR, 'heat-template.yaml')
logo = os.path.join(FIXTURE_DIR, 'logo.png')
arglist = ['--template', heat_template, '--output', RESULT_PACKAGE,
'-l', logo]
parsed_args = self.check_parser(self.cmd, arglist, [])
orig = sys.stdout
try:
sys.stdout = io.StringIO()
self.cmd.take_action(parsed_args)
finally:
stdout = sys.stdout.getvalue()
sys.stdout.close()
sys.stdout = orig
matchers.MatchesRegex(stdout,
"Application package "
"is available at {0}".format(RESULT_PACKAGE))
def test_create_mpl_package(self):
with tempfile.NamedTemporaryFile() as f:
RESULT_PACKAGE = f.name
classes_dir = os.path.join(FIXTURE_DIR, 'test-app', 'Classes')
resources_dir = os.path.join(FIXTURE_DIR, 'test-app', 'Resources')
ui = os.path.join(FIXTURE_DIR, 'test-app', 'ui.yaml')
arglist = ['-c', classes_dir, '-r', resources_dir,
'-u', ui, '-o', RESULT_PACKAGE]
parsed_args = self.check_parser(self.cmd, arglist, [])
orig = sys.stdout
try:
sys.stdout = io.StringIO()
self.cmd.take_action(parsed_args)
finally:
stdout = sys.stdout.getvalue()
sys.stdout.close()
sys.stdout = orig
matchers.MatchesRegex(stdout,
"Application package "
"is available at {0}".format(RESULT_PACKAGE))
class TestPackageList(TestPackage):
def setUp(self):
super(TestPackageList, self).setUp()
self.cmd = osc_pkg.ListPackages(self.app, None)
self.package_mock.filter.return_value = \
[packages.Package(None, DATA)]
utils.get_dict_properties = mock.MagicMock(return_value='')
def test_package_list_defaults(self):
arglist = []
parsed_args = self.check_parser(self.cmd, arglist, [])
columns, data = self.cmd.take_action(parsed_args)
self.package_mock.filter.assert_called_with(
include_disabled=False,
owned=False)
self.assertEqual(COLUMNS, columns)
def test_package_list_with_limit(self):
arglist = ['--limit', '10']
parsed_args = self.check_parser(self.cmd, arglist, [])
columns, data = self.cmd.take_action(parsed_args)
self.package_mock.filter.assert_called_with(
include_disabled=False,
limit=10,
owned=False)
def test_package_list_with_marker(self):
arglist = ['--marker', '12345']
parsed_args = self.check_parser(self.cmd, arglist, [])
columns, data = self.cmd.take_action(parsed_args)
self.package_mock.filter.assert_called_with(
include_disabled=False,
marker='12345',
owned=False)
def test_package_list_with_name(self):
arglist = ['--name', 'mysql']
parsed_args = self.check_parser(self.cmd, arglist, [])
columns, data = self.cmd.take_action(parsed_args)
self.package_mock.filter.assert_called_with(
include_disabled=False,
name='mysql',
owned=False)
def test_package_list_with_fqn(self):
arglist = ['--fqn', 'mysql']
parsed_args = self.check_parser(self.cmd, arglist, [])
columns, data = self.cmd.take_action(parsed_args)
self.package_mock.filter.assert_called_with(
include_disabled=False,
fqn='mysql',
owned=False)
class TestPackageDelete(TestPackage):
def setUp(self):
super(TestPackageDelete, self).setUp()
self.package_mock.delete.return_value = None
self.package_mock.filter.return_value = \
[packages.Package(None, DATA)]
# Command to test
self.cmd = osc_pkg.DeletePackage(self.app, None)
@mock.patch('osc_lib.utils.get_item_properties')
def test_package_delete(self, mock_util):
arglist = ['fake1']
verifylist = [('id', ['fake1'])]
mock_util.return_value = ('1234', 'Core library',
'io.murano', 'murano.io', '',
'True', 'Library', '0.0.0'
)
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
columns, data = self.cmd.take_action(parsed_args)
# Check that columns are correct
self.assertEqual(COLUMNS, columns)
# Check that data is correct
expected_data = [('1234', 'Core library', 'io.murano',
'murano.io', '', 'True', 'Library', '0.0.0')]
self.assertEqual(expected_data, data)
class TestPackageImport(TestPackage):
def setUp(self):
super(TestPackageImport, self).setUp()
self.package_mock.filter.return_value = \
[packages.Package(None, DATA)]
# Command to test
self.cmd = osc_pkg.ImportPackage(self.app, None)
@mock.patch('muranoclient.common.utils.Package.from_file')
def test_package_import(self, from_file):
with tempfile.NamedTemporaryFile() as f:
RESULT_PACKAGE = f.name
categories = ['Cat1', 'Cat2 with space']
pkg = make_pkg({'FullName': RESULT_PACKAGE})
from_file.return_value = mc_utils.Package(mc_utils.File(pkg))
arglist = [RESULT_PACKAGE, '--categories',
categories, '--is-public']
parsed_args = self.check_parser(self.cmd, arglist, [])
self.cmd.take_action(parsed_args)
self.package_mock.create.assert_called_once_with({
'categories': [categories],
'is_public': True
}, {RESULT_PACKAGE: mock.ANY},)
def _test_conflict(self,
packages, from_file, raw_input_mock,
input_action, exists_action=''):
packages.create = mock.MagicMock(
side_effect=[common_exceptions.HTTPConflict("Conflict"), None])
packages.filter.return_value = [mock.Mock(id='test_id')]
raw_input_mock.return_value = input_action
with tempfile.NamedTemporaryFile() as f:
pkg = make_pkg({'FullName': f.name})
from_file.return_value = mc_utils.Package(mc_utils.File(pkg))
if exists_action:
arglist = [f.name, '--exists-action', exists_action]
else:
arglist = [f.name]
parsed_args = self.check_parser(self.cmd, arglist, [])
self.cmd.take_action(parsed_args)
return f.name
@mock.patch('builtins.input')
@mock.patch('muranoclient.common.utils.Package.from_file')
def test_package_import_conflict_skip(self, from_file, raw_input_mock):
name = self._test_conflict(
self.package_mock,
from_file,
raw_input_mock,
's',
)
self.package_mock.create.assert_called_once_with({
'is_public': False,
}, {name: mock.ANY},)
@mock.patch('builtins.input')
@mock.patch('muranoclient.common.utils.Package.from_file')
def test_package_import_conflict_skip_ea(self, from_file, raw_input_mock):
name = self._test_conflict(
self.package_mock,
from_file,
raw_input_mock,
'',
exists_action='s',
)
self.package_mock.create.assert_called_once_with({
'is_public': False,
}, {name: mock.ANY},)
self.assertFalse(raw_input_mock.called)
@mock.patch('builtins.input')
@mock.patch('muranoclient.common.utils.Package.from_file')
def test_package_import_conflict_abort(self, from_file, raw_input_mock):
self.assertRaises(SystemExit, self._test_conflict,
self.package_mock,
from_file,
raw_input_mock,
'a',
)
self.package_mock.create.assert_called_once_with({
'is_public': False,
}, mock.ANY,)
@mock.patch('builtins.input')
@mock.patch('muranoclient.common.utils.Package.from_file')
def test_package_import_conflict_abort_ea(self,
from_file, raw_input_mock):
self.assertRaises(SystemExit, self._test_conflict,
self.package_mock,
from_file,
raw_input_mock,
'',
exists_action='a',
)
self.package_mock.create.assert_called_once_with({
'is_public': False,
}, mock.ANY,)
self.assertFalse(raw_input_mock.called)
@mock.patch('builtins.input')
@mock.patch('muranoclient.common.utils.Package.from_file')
def test_package_import_conflict_update(self, from_file, raw_input_mock):
name = self._test_conflict(
self.package_mock,
from_file,
raw_input_mock,
'u',
)
self.assertEqual(2, self.package_mock.create.call_count)
self.package_mock.delete.assert_called_once_with('test_id')
self.package_mock.create.assert_has_calls(
[
mock.call({'is_public': False}, {name: mock.ANY},),
mock.call({'is_public': False}, {name: mock.ANY},)
], any_order=True,
)
@mock.patch('builtins.input')
@mock.patch('muranoclient.common.utils.Package.from_file')
def test_package_import_conflict_update_ea(self,
from_file, raw_input_mock):
name = self._test_conflict(
self.package_mock,
from_file,
raw_input_mock,
'',
exists_action='u',
)
self.assertEqual(2, self.package_mock.create.call_count)
self.package_mock.delete.assert_called_once_with('test_id')
self.package_mock.create.assert_has_calls(
[
mock.call({'is_public': False}, {name: mock.ANY},),
mock.call({'is_public': False}, {name: mock.ANY},)
], any_order=True,
)
self.assertFalse(raw_input_mock.called)
def _test_conflict_dep(self,
packages, from_file,
dep_exists_action=''):
packages.create = mock.MagicMock(
side_effect=[common_exceptions.HTTPConflict("Conflict"),
common_exceptions.HTTPConflict("Conflict"),
None])
packages.filter.return_value = [mock.Mock(id='test_id')]
pkg1 = make_pkg(
{'FullName': 'first_app', 'Require': {'second_app': '1.0'}, })
pkg2 = make_pkg({'FullName': 'second_app', })
def side_effect(name):
if 'first_app' in name:
return mc_utils.Package(mc_utils.File(pkg1))
if 'second_app' in name:
return mc_utils.Package(mc_utils.File(pkg2))
from_file.side_effect = side_effect
arglist = ['first_app', '--exists-action', 's',
'--dep-exists-action', dep_exists_action]
parsed_args = self.check_parser(self.cmd, arglist, [])
self.cmd.take_action(parsed_args)
@mock.patch('muranoclient.common.utils.Package.from_file')
def test_package_import_conflict_dep_skip_ea(self, from_file):
self._test_conflict_dep(
self.package_mock,
from_file,
dep_exists_action='s',
)
self.assertEqual(2, self.package_mock.create.call_count)
self.package_mock.create.assert_has_calls(
[
mock.call({'is_public': False}, {'first_app': mock.ANY}),
mock.call({'is_public': False}, {'second_app': mock.ANY}),
], any_order=True,
)
@mock.patch('muranoclient.common.utils.Package.from_file')
def test_package_import_conflict_dep_abort_ea(self, from_file):
self.assertRaises(SystemExit, self._test_conflict_dep,
self.package_mock,
from_file,
dep_exists_action='a',
)
self.package_mock.create.assert_called_with({
'is_public': False,
}, {'second_app': mock.ANY},)
@mock.patch('muranoclient.common.utils.Package.from_file')
def test_package_import_conflict_dep_update_ea(self, from_file):
self._test_conflict_dep(
self.package_mock,
from_file,
dep_exists_action='u',
)
self.assertGreater(self.package_mock.create.call_count, 2)
self.assertLess(self.package_mock.create.call_count, 5)
self.assertTrue(self.package_mock.delete.called)
self.package_mock.create.assert_has_calls(
[
mock.call({'is_public': False}, {'first_app': mock.ANY}),
mock.call({'is_public': False}, {'second_app': mock.ANY}),
mock.call({'is_public': False}, {'second_app': mock.ANY}),
], any_order=True,
)
@mock.patch('muranoclient.common.utils.Package.from_file')
def test_package_import_no_categories(self, from_file):
with tempfile.NamedTemporaryFile() as f:
RESULT_PACKAGE = f.name
pkg = make_pkg({'FullName': RESULT_PACKAGE})
from_file.return_value = mc_utils.Package(mc_utils.File(pkg))
arglist = [RESULT_PACKAGE]
parsed_args = self.check_parser(self.cmd, arglist, [])
self.cmd.take_action(parsed_args)
self.package_mock.create.assert_called_once_with(
{'is_public': False},
{RESULT_PACKAGE: mock.ANY},
)
@requests_mock.mock()
@mock.patch('muranoclient.common.utils.Package.from_file')
def test_package_import_url(self, rm, from_file):
filename = "http://127.0.0.1/test_package.zip"
pkg = make_pkg({'FullName': 'test_package'})
from_file.return_value = mc_utils.Package(mc_utils.File(pkg))
rm.get(filename, body=make_pkg({'FullName': 'test_package'}))
arglist = [filename]
parsed_args = self.check_parser(self.cmd, arglist, [])
self.cmd.take_action(parsed_args)
self.package_mock.create.assert_called_once_with(
{'is_public': False},
{'test_package': mock.ANY},
)
@requests_mock.mock()
@mock.patch('muranoclient.common.utils.Package.from_file')
def test_package_import_by_name(self, rm, from_file):
filename = "io.test.apps.test_application"
murano_repo_url = "http://127.0.0.1"
pkg = make_pkg({'FullName': filename})
from_file.return_value = mc_utils.Package(mc_utils.File(pkg))
rm.get(murano_repo_url + '/apps/' + filename + '.zip',
body=make_pkg({'FullName': 'first_app'}))
arglist = [filename, '--murano-repo-url', murano_repo_url]
parsed_args = self.check_parser(self.cmd, arglist, [])
self.cmd.take_action(parsed_args)
self.assertTrue(self.package_mock.create.called)
self.package_mock.create.assert_called_once_with(
{'is_public': False},
{filename: mock.ANY},
)
@requests_mock.mock()
def test_package_import_multiple(self, rm):
filename = ["io.test.apps.test_application",
"http://127.0.0.1/test_app2.zip", ]
murano_repo_url = "http://127.0.0.1"
rm.get(murano_repo_url + '/apps/' + filename[0] + '.zip',
body=make_pkg({'FullName': 'first_app'}))
rm.get(filename[1],
body=make_pkg({'FullName': 'second_app'}))
arglist = [filename[0], filename[1],
'--murano-repo-url', murano_repo_url]
parsed_args = self.check_parser(self.cmd, arglist, [])
self.cmd.take_action(parsed_args)
self.assertEqual(2, self.package_mock.create.call_count)
self.package_mock.create.assert_has_calls(
[
mock.call({'is_public': False}, {'first_app': mock.ANY}),
mock.call({'is_public': False}, {'second_app': mock.ANY}),
], any_order=True,
)
class TestBundleImport(TestPackage):
def setUp(self):
super(TestBundleImport, self).setUp()
# Command to test
self.cmd = osc_pkg.ImportBundle(self.app, None)
@requests_mock.mock()
def test_import_bundle_by_name(self, m):
"""Asserts bundle import calls packages create once for each pkg."""
pkg1 = make_pkg({'FullName': 'first_app'})
pkg2 = make_pkg({'FullName': 'second_app'})
murano_repo_url = "http://127.0.0.1"
m.get(murano_repo_url + '/apps/first_app.zip', body=pkg1)
m.get(murano_repo_url + '/apps/second_app.1.0.zip',
body=pkg2)
s = io.StringIO()
bundle_contents = {'Packages': [
{'Name': 'first_app'},
{'Name': 'second_app', 'Version': '1.0'}
]}
json.dump(bundle_contents, s)
s = io.BytesIO(s.getvalue().encode('ascii'))
m.get(murano_repo_url + '/bundles/test_bundle.bundle',
body=s)
arglist = ["test_bundle", '--murano-repo-url', murano_repo_url]
parsed_args = self.check_parser(self.cmd, arglist, [])
self.cmd.take_action(parsed_args)
self.package_mock.create.assert_has_calls(
[
mock.call({'is_public': False}, {'first_app': mock.ANY}),
mock.call({'is_public': False}, {'second_app': mock.ANY}),
], any_order=True,
)
@requests_mock.mock()
def test_import_bundle_wrong_url(self, m):
url = 'http://127.0.0.2/test_bundle.bundle'
m.get(url, status_code=404)
arglist = [url]
parsed_args = self.check_parser(self.cmd, arglist, [])
self.cmd.take_action(parsed_args)
self.assertFalse(self.package_mock.packages.create.called)
@requests_mock.mock()
def test_import_bundle_no_bundle(self, m):
url = 'http://127.0.0.1/bundles/test_bundle.bundle'
m.get(url, status_code=404)
arglist = ["test_bundle"]
parsed_args = self.check_parser(self.cmd, arglist, [])
self.cmd.take_action(parsed_args)
self.assertFalse(self.package_mock.packages.create.called)
@requests_mock.mock()
def test_import_bundle_by_url(self, m):
"""Asserts bundle import calls packages create once for each pkg."""
pkg1 = make_pkg({'FullName': 'first_app'})
pkg2 = make_pkg({'FullName': 'second_app'})
murano_repo_url = 'http://127.0.0.1'
m.get(murano_repo_url + '/apps/first_app.zip', body=pkg1)
m.get(murano_repo_url + '/apps/second_app.1.0.zip',
body=pkg2)
s = io.StringIO()
bundle_contents = {'Packages': [
{'Name': 'first_app'},
{'Name': 'second_app', 'Version': '1.0'}
]}
json.dump(bundle_contents, s)
s = io.BytesIO(s.getvalue().encode('ascii'))
url = 'http://127.0.0.2/test_bundle.bundle'
m.get(url, body=s)
arglist = [url, '--murano-repo-url', murano_repo_url]
parsed_args = self.check_parser(self.cmd, arglist, [])
self.cmd.take_action(parsed_args)
self.package_mock.create.assert_has_calls(
[
mock.call({'is_public': False}, {'first_app': mock.ANY}),
mock.call({'is_public': False}, {'second_app': mock.ANY}),
], any_order=True,
)
@requests_mock.mock()
def test_import_local_bundle(self, m):
"""Asserts local bundles are first searched locally."""
tmp_dir = tempfile.mkdtemp()
bundle_file = os.path.join(tmp_dir, 'bundle.bundle')
with open(os.path.join(tmp_dir, 'bundle.bundle'), 'w') as f:
bundle_contents = {'Packages': [
{'Name': 'first_app'},
{'Name': 'second_app', 'Version': '1.0'}
]}
json.dump(bundle_contents, f)
pkg1 = make_pkg({'FullName': 'first_app',
'Require': {'third_app': None}})
pkg2 = make_pkg({'FullName': 'second_app'})
pkg3 = make_pkg({'FullName': 'third_app'})
with open(os.path.join(tmp_dir, 'first_app'), 'wb') as f:
f.write(pkg1.read())
with open(os.path.join(tmp_dir, 'third_app'), 'wb') as f:
f.write(pkg3.read())
murano_repo_url = "http://127.0.0.1"
m.get(murano_repo_url + '/apps/first_app.zip',
status_code=404)
m.get(murano_repo_url + '/apps/second_app.1.0.zip',
body=pkg2)
m.get(murano_repo_url + '/apps/third_app.zip',
status_code=404)
arglist = [bundle_file, '--murano-repo-url', murano_repo_url]
parsed_args = self.check_parser(self.cmd, arglist, [])
self.cmd.take_action(parsed_args)
self.package_mock.create.assert_has_calls(
[
mock.call({'is_public': False}, {'first_app': mock.ANY}),
mock.call({'is_public': False}, {'second_app': mock.ANY}),
mock.call({'is_public': False}, {'third_app': mock.ANY}),
], any_order=True,
)
shutil.rmtree(tmp_dir)
class TestShowPackage(TestPackage):
def setUp(self):
super(TestShowPackage, self).setUp()
# Command to test
self.cmd = osc_pkg.ShowPackage(self.app, None)
def test_package_show(self):
arglist = ['fake']
verifylist = [('id', 'fake')]
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
columns, data = self.cmd.take_action(parsed_args)
# Check that columns are correct
expected_columns = ('categories', 'class_definitions', 'description',
'enabled', 'fully_qualified_name', 'id',
'is_public', 'name', 'owner_id', 'tags', 'type')
self.assertEqual(expected_columns, columns)
self.package_mock.get.assert_called_with('fake')
class TestUpdatePackage(TestPackage):
def setUp(self):
super(TestUpdatePackage, self).setUp()
self.package_mock.update.return_value = \
(mock.MagicMock(), mock.MagicMock())
# Command to test
self.cmd = osc_pkg.UpdatePackage(self.app, None)
def test_package_update(self):
arglist = ['123', '--is-public', 'true']
verifylist = [('id', '123'), ('is_public', True)]
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
self.cmd.take_action(parsed_args)
self.package_mock.update.assert_called_with('123', {'is_public': True})
arglist = ['123', '--enabled', 'true']
verifylist = [('id', '123'), ('enabled', True)]
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
self.cmd.take_action(parsed_args)
self.package_mock.update.assert_called_with('123', {'enabled': True})
arglist = ['123', '--name', 'foo', '--description', 'bar']
verifylist = [('id', '123'), ('name', 'foo'), ('description', 'bar')]
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
self.cmd.take_action(parsed_args)
self.package_mock.update.assert_called_with(
'123', {'name': 'foo', 'description': 'bar'})
arglist = ['123', '--tags', 'foo']
verifylist = [('id', '123'), ('tags', ['foo'])]
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
self.cmd.take_action(parsed_args)
self.package_mock.update.assert_called_with(
'123', {'tags': ['foo']})
class TestDownloadPackage(TestPackage):
def setUp(self):
super(TestDownloadPackage, self).setUp()
self.package_mock.download.return_value = \
b'This is a fake package buffer'
# Command to test
self.cmd = osc_pkg.DownloadPackage(self.app, None)
def test_package_download(self):
arglist = ['1234', '/tmp/foo.zip']
verifylist = [('id', '1234'), ('filename', '/tmp/foo.zip')]
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
self.cmd.take_action(parsed_args)
self.package_mock.download.assert_called_with('1234')

View File

@ -1,54 +0,0 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from muranoclient.osc.v1 import schema as osc_schema
from muranoclient.tests.unit.osc.v1 import fakes
from muranoclient.v1 import schemas as api_schemas
SAMPLE_CLASS_SCHEMA = {
'': {
"$schema": "http://json-schema.org/draft-04/schema#",
"type": "object",
"properties": {}
},
'modelBuilder': {
"$schema": "http://json-schema.org/draft-04/schema#",
"type": "object",
"properties": {}
}
}
class TestSchema(fakes.TestApplicationCatalog):
def setUp(self):
super(TestSchema, self).setUp()
self.schemas_mock = \
self.app.client_manager.application_catalog.schemas
self.schemas_mock.get.return_value = api_schemas.Schema(
None, SAMPLE_CLASS_SCHEMA)
self.cmd = osc_schema.ShowSchema(self.app, None)
def test_query_class_schema(self):
arglist = ['class.name', 'methodName1',
'--package-name', 'package.name',
'--class-version', '>1']
verifylist = [('class_name', 'class.name'),
('method_names', ['methodName1']),
('package_name', 'package.name'),
('class_version', '>1')]
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
columns, data = self.cmd.take_action(parsed_args)
expected_columns = ['', 'modelBuilder']
self.assertCountEqual(expected_columns, columns)
self.assertCountEqual(tuple(SAMPLE_CLASS_SCHEMA.values()), data)

View File

@ -1,46 +0,0 @@
# Copyright 2015 Huawei.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import testtools
from muranoclient.common import base
from muranoclient.v1 import packages
class BaseTest(testtools.TestCase):
def test_two_resources_with_same_id_are_not_equal(self):
# Two resources with same ID: never equal if their info is not equal
r1 = base.Resource(None, {'id': 1, 'name': 'hi'})
r2 = base.Resource(None, {'id': 1, 'name': 'hello'})
self.assertNotEqual(r1, r2)
def test_two_resources_with_same_id_and_info_are_equal(self):
# Two resources with same ID: equal if their info is equal
r1 = base.Resource(None, {'id': 1, 'name': 'hello'})
r2 = base.Resource(None, {'id': 1, 'name': 'hello'})
self.assertEqual(r1, r2)
def test_two_resources_with_diff_type_are_not_equal(self):
# Two resources of different types: never equal
r1 = base.Resource(None, {'id': 1})
r2 = packages.Package(None, {'id': 1})
self.assertNotEqual(r1, r2)
def test_two_resources_with_no_id_are_equal(self):
# Two resources with no ID: equal if their info is equal
r1 = base.Resource(None, {'name': 'joe', 'age': 12})
r2 = base.Resource(None, {'name': 'joe', 'age': 12})
self.assertEqual(r1, r2)

View File

@ -1,463 +0,0 @@
# -*- coding:utf-8 -*-
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import socket
from unittest import mock
import testtools
from muranoclient.common import exceptions as exc
from muranoclient.common import http
from muranoclient.tests.unit import fakes
@mock.patch('muranoclient.common.http.requests.request')
class HttpClientTest(testtools.TestCase):
def test_http_raw_request(self, mock_request):
headers = {'User-Agent': 'python-muranoclient'}
mock_request.return_value = \
fakes.FakeHTTPResponse(
200, 'OK',
{},
'')
client = http.HTTPClient('http://example.com:8082')
resp = client.request('', 'GET')
self.assertEqual(200, resp.status_code)
self.assertEqual('', ''.join([x for x in resp.content]))
mock_request.assert_called_with('GET', 'http://example.com:8082',
allow_redirects=False,
headers=headers)
def test_token_or_credentials(self, mock_request):
# Record a 200
fake200 = fakes.FakeHTTPResponse(
200, 'OK',
{},
'')
mock_request.side_effect = [fake200, fake200, fake200]
# Replay, create client, assert
client = http.HTTPClient('http://example.com:8082')
resp = client.request('', 'GET')
self.assertEqual(200, resp.status_code)
client.username = 'user'
client.password = 'pass'
resp = client.request('', 'GET')
self.assertEqual(200, resp.status_code)
client.auth_token = 'abcd1234'
resp = client.request('', 'GET')
self.assertEqual(200, resp.status_code)
# no token or credentials
mock_request.assert_has_calls([
mock.call('GET', 'http://example.com:8082',
allow_redirects=False,
headers={'User-Agent': 'python-muranoclient'}),
mock.call('GET', 'http://example.com:8082',
allow_redirects=False,
headers={'User-Agent': 'python-muranoclient',
'X-Auth-Key': 'pass',
'X-Auth-User': 'user'}),
mock.call('GET', 'http://example.com:8082',
allow_redirects=False,
headers={'User-Agent': 'python-muranoclient',
'X-Auth-Token': 'abcd1234'})
])
def test_region_name(self, mock_request):
# Record a 200
fake200 = fakes.FakeHTTPResponse(
200, 'OK',
{},
'')
mock_request.return_value = fake200
client = http.HTTPClient('http://example.com:8082')
client.region_name = 'RegionOne'
resp = client.request('', 'GET')
self.assertEqual(200, resp.status_code)
mock_request.assert_called_once_with(
'GET', 'http://example.com:8082',
allow_redirects=False,
headers={'X-Region-Name': 'RegionOne',
'User-Agent': 'python-muranoclient'})
def test_http_json_request(self, mock_request):
# Record a 200
mock_request.return_value = \
fakes.FakeHTTPResponse(
200, 'OK',
{'content-type': 'application/json'},
'{}')
client = http.HTTPClient('http://example.com:8082')
resp, body = client.json_request('', 'GET')
self.assertEqual(200, resp.status_code)
self.assertEqual({}, body)
mock_request.assert_called_once_with(
'GET', 'http://example.com:8082',
allow_redirects=False,
headers={'Content-Type': 'application/json',
'User-Agent': 'python-muranoclient'})
def test_http_json_request_argument_passed_to_requests(self, mock_request):
"""Check that we have sent the proper arguments to requests."""
# Record a 200
mock_request.return_value = \
fakes.FakeHTTPResponse(
200, 'OK',
{'content-type': 'application/json'},
'{}')
client = http.HTTPClient('http://example.com:8082')
client.verify_cert = True
client.cert_file = 'RANDOM_CERT_FILE'
client.key_file = 'RANDOM_KEY_FILE'
client.auth_url = 'http://AUTH_URL'
resp, body = client.json_request('', 'GET', data='text')
self.assertEqual(200, resp.status_code)
self.assertEqual({}, body)
mock_request.assert_called_once_with(
'GET', 'http://example.com:8082',
allow_redirects=False,
cert=('RANDOM_CERT_FILE', 'RANDOM_KEY_FILE'),
verify=True,
data='"text"',
headers={'Content-Type': 'application/json',
'X-Auth-Url': 'http://AUTH_URL',
'User-Agent': 'python-muranoclient'})
def test_http_json_request_w_req_body(self, mock_request):
# Record a 200
mock_request.return_value = \
fakes.FakeHTTPResponse(
200, 'OK',
{'content-type': 'application/json'},
'{}')
client = http.HTTPClient('http://example.com:8082')
resp, body = client.json_request('', 'GET', data='test-body')
self.assertEqual(200, resp.status_code)
self.assertEqual({}, body)
mock_request.assert_called_once_with(
'GET', 'http://example.com:8082',
data='"test-body"',
allow_redirects=False,
headers={'Content-Type': 'application/json',
'User-Agent': 'python-muranoclient'})
def test_http_json_request_non_json_resp_cont_type(self, mock_request):
# Record a 200
mock_request.return_value = \
fakes.FakeHTTPResponse(
200, 'OK',
{'content-type': 'not/json'},
'{}')
client = http.HTTPClient('http://example.com:8082')
resp, body = client.json_request('', 'GET', data='test-data')
self.assertEqual(200, resp.status_code)
self.assertIsNone(body)
mock_request.assert_called_once_with(
'GET', 'http://example.com:8082', data='"test-data"',
allow_redirects=False,
headers={'Content-Type': 'application/json',
'User-Agent': 'python-muranoclient'})
def test_http_json_request_invalid_json(self, mock_request):
# Record a 200
mock_request.return_value = \
fakes.FakeHTTPResponse(
200, 'OK',
{'content-type': 'application/json'},
'invalid-json')
client = http.HTTPClient('http://example.com:8082')
resp, body = client.json_request('', 'GET')
self.assertEqual(200, resp.status_code)
self.assertEqual('invalid-json', body)
mock_request.assert_called_once_with(
'GET', 'http://example.com:8082',
allow_redirects=False,
headers={'Content-Type': 'application/json',
'User-Agent': 'python-muranoclient'})
def test_http_manual_redirect_delete(self, mock_request):
mock_request.side_effect = [
fakes.FakeHTTPResponse(
302, 'Found',
{'location': 'http://example.com:8082/foo/bar'},
''),
fakes.FakeHTTPResponse(
200, 'OK',
{'content-type': 'application/json'},
'{}')]
client = http.HTTPClient('http://example.com:8082/foo')
resp, body = client.json_request('', 'DELETE')
self.assertEqual(200, resp.status_code)
mock_request.assert_has_calls([
mock.call('DELETE', 'http://example.com:8082/foo',
allow_redirects=False,
headers={'Content-Type': 'application/json',
'User-Agent': 'python-muranoclient'}),
mock.call('DELETE', 'http://example.com:8082/foo/bar',
allow_redirects=False,
headers={'Content-Type': 'application/json',
'User-Agent': 'python-muranoclient'})
])
def test_http_manual_redirect_post(self, mock_request):
mock_request.side_effect = [
fakes.FakeHTTPResponse(
302, 'Found',
{'location': 'http://example.com:8082/foo/bar'},
''),
fakes.FakeHTTPResponse(
200, 'OK',
{'content-type': 'application/json'},
'{}')]
client = http.HTTPClient('http://example.com:8082/foo')
resp, body = client.json_request('', 'POST')
self.assertEqual(200, resp.status_code)
mock_request.assert_has_calls([
mock.call('POST', 'http://example.com:8082/foo',
allow_redirects=False,
headers={'Content-Type': 'application/json',
'User-Agent': 'python-muranoclient'}),
mock.call('POST', 'http://example.com:8082/foo/bar',
allow_redirects=False,
headers={'Content-Type': 'application/json',
'User-Agent': 'python-muranoclient'})
])
def test_http_manual_redirect_put(self, mock_request):
mock_request.side_effect = [
fakes.FakeHTTPResponse(
302, 'Found',
{'location': 'http://example.com:8082/foo/bar'},
''),
fakes.FakeHTTPResponse(
200, 'OK',
{'content-type': 'application/json'},
'{}')]
client = http.HTTPClient('http://example.com:8082/foo')
resp, body = client.json_request('', 'PUT')
self.assertEqual(200, resp.status_code)
mock_request.assert_has_calls([
mock.call('PUT', 'http://example.com:8082/foo',
allow_redirects=False,
headers={'Content-Type': 'application/json',
'User-Agent': 'python-muranoclient'}),
mock.call('PUT', 'http://example.com:8082/foo/bar',
allow_redirects=False,
headers={'Content-Type': 'application/json',
'User-Agent': 'python-muranoclient'})
])
def test_http_manual_redirect_prohibited(self, mock_request):
mock_request.return_value = \
fakes.FakeHTTPResponse(
302, 'Found',
{'location': 'http://example.com:8082/'},
'')
client = http.HTTPClient('http://example.com:8082/foo')
self.assertRaises(exc.InvalidEndpoint,
client.json_request, '', 'DELETE')
mock_request.assert_called_once_with(
'DELETE', 'http://example.com:8082/foo',
allow_redirects=False,
headers={'Content-Type': 'application/json',
'User-Agent': 'python-muranoclient'})
def test_http_manual_redirect_error_without_location(self, mock_request):
mock_request.return_value = \
fakes.FakeHTTPResponse(
302, 'Found',
{},
'')
client = http.HTTPClient('http://example.com:8082/foo')
self.assertRaises(exc.InvalidEndpoint,
client.json_request, '', 'DELETE')
mock_request.assert_called_once_with(
'DELETE', 'http://example.com:8082/foo',
allow_redirects=False,
headers={'Content-Type': 'application/json',
'User-Agent': 'python-muranoclient'})
def test_http_json_request_redirect(self, mock_request):
# Record the 302
mock_request.side_effect = [
fakes.FakeHTTPResponse(
302, 'Found',
{'location': 'http://example.com:8082'},
''),
fakes.FakeHTTPResponse(
200, 'OK',
{'content-type': 'application/json'},
'{}')]
client = http.HTTPClient('http://example.com:8082')
resp, body = client.json_request('', 'GET')
self.assertEqual(200, resp.status_code)
self.assertEqual({}, body)
mock_request.assert_has_calls([
mock.call('GET', 'http://example.com:8082',
allow_redirects=False,
headers={'Content-Type': 'application/json',
'User-Agent': 'python-muranoclient'}),
mock.call('GET', 'http://example.com:8082',
allow_redirects=False,
headers={'Content-Type': 'application/json',
'User-Agent': 'python-muranoclient'})
])
def test_http_404_json_request(self, mock_request):
mock_request.return_value = \
fakes.FakeHTTPResponse(
404, 'Not Found', {'content-type': 'application/json'},
'{}')
client = http.HTTPClient('http://example.com:8082')
e = self.assertRaises(exc.HTTPNotFound, client.json_request, '', 'GET')
# Assert that the raised exception can be converted to string
self.assertIsNotNone(str(e))
# Record a 404
mock_request.assert_called_once_with(
'GET', 'http://example.com:8082',
allow_redirects=False,
headers={'Content-Type': 'application/json',
'User-Agent': 'python-muranoclient'})
def test_http_300_json_request(self, mock_request):
mock_request.return_value = \
fakes.FakeHTTPResponse(
300, 'OK', {'content-type': 'application/json'},
'{}')
client = http.HTTPClient('http://example.com:8082')
e = self.assertRaises(
exc.HTTPMultipleChoices, client.json_request, '', 'GET')
# Assert that the raised exception can be converted to string
self.assertIsNotNone(str(e))
# Record a 300
mock_request.assert_called_once_with(
'GET', 'http://example.com:8082',
allow_redirects=False,
headers={'Content-Type': 'application/json',
'User-Agent': 'python-muranoclient'})
def test_fake_json_request(self, mock_request):
headers = {'User-Agent': 'python-muranoclient'}
mock_request.side_effect = [socket.gaierror]
client = http.HTTPClient('fake://example.com:8082')
self.assertRaises(exc.InvalidEndpoint,
client.request, "/", "GET")
mock_request.assert_called_once_with('GET', 'fake://example.com:8082/',
allow_redirects=False,
headers=headers)
def test_http_request_socket_error(self, mock_request):
headers = {'User-Agent': 'python-muranoclient'}
mock_request.side_effect = [socket.gaierror]
client = http.HTTPClient('http://example.com:8082')
self.assertRaises(exc.InvalidEndpoint,
client.request, "/", "GET")
mock_request.assert_called_once_with('GET', 'http://example.com:8082/',
allow_redirects=False,
headers=headers)
def test_http_request_socket_timeout(self, mock_request):
headers = {'User-Agent': 'python-muranoclient'}
mock_request.side_effect = [socket.timeout]
client = http.HTTPClient('http://example.com:8082')
self.assertRaises(exc.CommunicationError,
client.request, "/", "GET")
mock_request.assert_called_once_with('GET', 'http://example.com:8082/',
allow_redirects=False,
headers=headers)
def test_http_request_specify_timeout(self, mock_request):
mock_request.return_value = \
fakes.FakeHTTPResponse(
200, 'OK',
{'content-type': 'application/json'},
'{}')
client = http.HTTPClient('http://example.com:8082', timeout='123')
resp, body = client.json_request('', 'GET')
self.assertEqual(200, resp.status_code)
self.assertEqual({}, body)
mock_request.assert_called_once_with(
'GET', 'http://example.com:8082',
allow_redirects=False,
headers={'Content-Type': 'application/json',
'User-Agent': 'python-muranoclient'},
timeout=float(123))
def test_get_system_ca_file(self, mock_request):
chosen = '/etc/ssl/certs/ca-certificates.crt'
with mock.patch('os.path.exists') as mock_os:
mock_os.return_value = chosen
ca = http.get_system_ca_file()
self.assertEqual(chosen, ca)
mock_os.assert_called_once_with(chosen)
def test_insecure_verify_cert_None(self, mock_request):
client = http.HTTPClient('https://foo', insecure=True)
self.assertFalse(client.verify_cert)
def test_passed_cert_to_verify_cert(self, mock_request):
client = http.HTTPClient('https://foo', cacert="NOWHERE")
self.assertEqual("NOWHERE", client.verify_cert)
with mock.patch('muranoclient.common.http.get_system_ca_file') as gsf:
gsf.return_value = "SOMEWHERE"
client = http.HTTPClient('https://foo')
self.assertEqual("SOMEWHERE", client.verify_cert)
# def test_curl_log_i18n_headers(self, mock_request):
# self.m.StubOutWithMock(logging.Logger, 'debug')
# kwargs = {'headers': {'Key': b'foo\xe3\x8a\x8e'}}
#
# mock_logging_debug = logging.Logger.debug(
# u"curl -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)
#
# self.m.VerifyAll()

View File

@ -1,63 +0,0 @@
# Copyright 2012 OpenStack Foundation
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import testtools
from unittest import mock
from muranoclient.common import exceptions as exc
HTML_MSG = """<html>
<head>
<title>403 Forbidden</title>
</head>
<body>
<h1>403 Forbidden</h1>
Access was denied to this resource.<br /><br />
</body>
</html>"""
class TestHTTPExceptions(testtools.TestCase):
def test_handles_json(self):
"""exc.from_response should not print JSON."""
mock_resp = mock.Mock()
mock_resp.status_code = 413
mock_resp.json.return_value = {
"overLimit": {
"code": 413,
"message": "OverLimit Retry...",
"details": "Error Details...",
"retryAt": "2015-08-31T21:21:06Z"
}
}
mock_resp.headers = {
"content-type": "application/json"
}
err = exc.from_response(mock_resp)
self.assertIsInstance(err, exc.HTTPOverLimit)
self.assertEqual("OverLimit Retry...", err.details)
def test_handles_html(self):
"""exc.from_response should not print HTML."""
mock_resp = mock.Mock()
mock_resp.status_code = 403
mock_resp.text = HTML_MSG
mock_resp.headers = {
"content-type": "text/html"
}
err = exc.from_response(mock_resp)
self.assertIsInstance(err, exc.HTTPForbidden)
self.assertEqual("403 Forbidden: Access was denied to this resource.",
err.details)

View File

@ -1,354 +0,0 @@
# Copyright (c) 2013 Mirantis, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import testtools
from unittest import mock
from muranoclient import client
from muranoclient.v1 import actions
from muranoclient.v1 import deployments
import muranoclient.v1.environments as environments
from muranoclient.v1 import packages
import muranoclient.v1.sessions as sessions
from muranoclient.v1 import static_actions
import muranoclient.v1.templates as templates
def my_mock(*a, **b):
return [a, b]
api = mock.MagicMock(json_request=my_mock)
class UnitTestsForClassesAndFunctions(testtools.TestCase):
def test_create_client_instance(self):
endpoint = 'http://no-resolved-host:8001'
test_client = client.Client('1', endpoint=endpoint,
token='1', timeout=10)
self.assertIsNotNone(test_client.environments)
self.assertIsNotNone(test_client.sessions)
self.assertIsNotNone(test_client.services)
def test_env_manager_list(self):
manager = environments.EnvironmentManager(api)
result = manager.list()
self.assertEqual([], result)
def test_env_manager_create(self):
manager = environments.EnvironmentManager(api)
result = manager.create({'name': 'test'})
self.assertEqual({'name': 'test'}, result.data)
def test_env_manager_create_with_named_parameters(self):
manager = environments.EnvironmentManager(api)
result = manager.create(data={'name': 'test'})
self.assertEqual({'name': 'test'}, result.data)
def test_env_manager_create_negative_without_parameters(self):
manager = environments.EnvironmentManager(api)
self.assertRaises(TypeError, manager.create)
def test_env_manager_delete(self):
manager = environments.EnvironmentManager(api)
result = manager.delete('test')
self.assertIsNone(result)
def test_env_manager_delete_with_named_parameters(self):
manager = environments.EnvironmentManager(api)
result = manager.delete(environment_id='1')
self.assertIsNone(result)
def test_env_manager_delete_negative_without_parameters(self):
manager = environments.EnvironmentManager(api)
self.assertRaises(TypeError, manager.delete)
def test_env_manager_update(self):
manager = environments.EnvironmentManager(api)
result = manager.update('1', 'test')
self.assertEqual({'name': 'test'}, result.data)
def test_env_manager_update_with_named_parameters(self):
manager = environments.EnvironmentManager(api)
result = manager.update(environment_id='1',
name='test')
self.assertEqual({'name': 'test'}, result.data)
def test_env_manager_update_negative_with_one_parameter(self):
manager = environments.EnvironmentManager(api)
self.assertRaises(TypeError, manager.update, 'test')
def test_env_manager_update_negative_without_parameters(self):
manager = environments.EnvironmentManager(api)
self.assertRaises(TypeError, manager.update)
def test_env_manager_get(self):
manager = environments.EnvironmentManager(api)
result = manager.get('test')
self.assertIsNotNone(result.manager)
def test_env(self):
environment = environments.Environment(api, api)
self.assertIsNotNone(environment.data())
def test_session_manager_delete(self):
manager = sessions.SessionManager(api)
result = manager.delete('datacenter1', 'session1')
self.assertIsNone(result)
def test_session_manager_delete_with_named_parameters(self):
manager = sessions.SessionManager(api)
result = manager.delete(environment_id='datacenter1',
session_id='session1')
self.assertIsNone(result)
def test_session_manager_delete_negative_with_one_parameter(self):
manager = sessions.SessionManager(api)
self.assertRaises(TypeError, manager.delete, 'datacenter1')
def test_session_manager_delete_negative_without_parameters(self):
manager = sessions.SessionManager(api)
self.assertRaises(TypeError, manager.delete)
def test_session_manager_get(self):
manager = sessions.SessionManager(api)
result = manager.get('datacenter1', 'session1')
# WTF?
self.assertIsNotNone(result.manager)
def test_session_manager_configure(self):
manager = sessions.SessionManager(api)
result = manager.configure('datacenter1')
self.assertIsNotNone(result)
def test_session_manager_configure_with_named_parameter(self):
manager = sessions.SessionManager(api)
result = manager.configure(environment_id='datacenter1')
self.assertIsNotNone(result)
def test_session_manager_configure_negative_without_parameters(self):
manager = sessions.SessionManager(api)
self.assertRaises(TypeError, manager.configure)
def test_session_manager_deploy(self):
manager = sessions.SessionManager(api)
result = manager.deploy('datacenter1', '1')
self.assertIsNone(result)
def test_session_manager_deploy_with_named_parameters(self):
manager = sessions.SessionManager(api)
result = manager.deploy(environment_id='datacenter1',
session_id='1')
self.assertIsNone(result)
def test_session_manager_deploy_negative_with_one_parameter(self):
manager = sessions.SessionManager(api)
self.assertRaises(TypeError, manager.deploy, 'datacenter1')
def test_session_manager_deploy_negative_without_parameters(self):
manager = sessions.SessionManager(api)
self.assertRaises(TypeError, manager.deploy)
def test_action_manager_call(self):
api_mock = mock.MagicMock(
json_request=lambda *args, **kwargs: (None, {'task_id': '1234'}))
manager = actions.ActionManager(api_mock)
result = manager.call('testEnvId', 'testActionId', ['arg1', 'arg2'])
self.assertEqual('1234', result)
def test_package_filter_pagination_next_marker(self):
# ``PackageManager.filter`` handles `next_marker` parameter related
# to pagination in API correctly.
responses = [
{'next_marker': 'test_marker',
'packages': [{'name': 'test_package_1'}]},
{'packages': [{'name': 'test_package_2'}]}
]
def json_request(url, method, *args, **kwargs):
self.assertIn('/v1/catalog/packages', url)
return mock.MagicMock(), responses.pop(0)
api = mock.MagicMock()
api.configure_mock(**{'json_request.side_effect': json_request})
manager = packages.PackageManager(api)
list(manager.filter())
self.assertEqual(2, api.json_request.call_count)
def test_package_filter_encoding_good(self):
responses = [
{'next_marker': 'test_marker',
'packages': [{'name': 'test_package_1'}]},
{'packages': [{'name': 'test_package_2'}]}
]
def json_request(url, method, *args, **kwargs):
self.assertIn('category=%D0%BF%D0%B8%D0%B2%D0%BE', url)
return mock.MagicMock(), responses.pop(0)
api = mock.MagicMock()
api.configure_mock(**{'json_request.side_effect': json_request})
manager = packages.PackageManager(api)
category = b'\xd0\xbf\xd0\xb8\xd0\xb2\xd0\xbe'
kwargs = {'category': category.decode('utf-8')}
list(manager.filter(**kwargs))
self.assertEqual(2, api.json_request.call_count)
def test_action_manager_get_result(self):
api_mock = mock.MagicMock(
json_request=lambda *args, **kwargs: (None, {'a': 'b'}))
manager = actions.ActionManager(api_mock)
result = manager.get_result('testEnvId', '1234')
self.assertEqual({'a': 'b'}, result)
def test_static_action_manager_call(self):
api_mock = mock.MagicMock(
json_request=lambda *args, **kwargs: (None, 'result'))
manager = static_actions.StaticActionManager(api_mock)
args = {'className': 'cls', 'methodName': 'method'}
result = manager.call(args).get_result()
self.assertEqual('result', result)
def test_env_template_manager_list(self):
"""Tests the list of environment templates."""
manager = templates.EnvTemplateManager(api)
result = manager.list()
self.assertEqual([], result)
def test_env_template_manager_create(self):
manager = templates.EnvTemplateManager(api)
result = manager.create({'name': 'test'})
self.assertEqual({'name': 'test'}, result.data)
def test_env_template_manager_create_with_named_parameters(self):
manager = templates.EnvTemplateManager(api)
result = manager.create(data={'name': 'test'})
self.assertEqual({'name': 'test'}, result.data)
def test_env_template_manager_create_negative_without_parameters(self):
manager = templates.EnvTemplateManager(api)
self.assertRaises(TypeError, manager.create)
def test_env_template_manager_delete(self):
manager = templates.EnvTemplateManager(api)
result = manager.delete('test')
self.assertIsNone(result)
def test_env_template_manager_delete_with_named_parameters(self):
manager = templates.EnvTemplateManager(api)
result = manager.delete(env_template_id='1')
self.assertIsNone(result)
def test_env_template_manager_delete_negative_without_parameters(self):
manager = templates.EnvTemplateManager(api)
self.assertRaises(TypeError, manager.delete)
def test_env_template_manager_update(self):
manager = templates.EnvTemplateManager(api)
result = manager.update('1', 'test')
self.assertEqual({'name': 'test'}, result.data)
def test_env_template_manager_update_with_named_parameters(self):
manager = templates.EnvTemplateManager(api)
result = manager.update(env_template_id='1',
name='test')
self.assertEqual({'name': 'test'}, result.data)
def test_env_template_manager_update_negative_with_one_parameter(self):
manager = templates.EnvTemplateManager(api)
self.assertRaises(TypeError, manager.update, 'test')
def test_env_template_manager_update_negative_without_parameters(self):
manager = templates.EnvTemplateManager(api)
self.assertRaises(TypeError, manager.update)
def test_env_template_manager_get(self):
manager = templates.EnvTemplateManager(api)
result = manager.get('test')
self.assertIsNotNone(result.manager)
def test_deployment_manager_list(self):
manager = deployments.DeploymentManager(api)
manager._list = mock.Mock(return_value=mock.sentinel.deployments)
result = manager.list(mock.sentinel.environment_id)
self.assertEqual(mock.sentinel.deployments, result)
manager._list.assert_called_once_with(
'/v1/environments/sentinel.environment_id/deployments',
'deployments')
def test_deployment_manager_list_all_environments(self):
manager = deployments.DeploymentManager(api)
manager._list = mock.Mock(return_value=mock.sentinel.deployments)
result = manager.list(mock.sentinel.environment_id,
all_environments=True)
self.assertEqual(mock.sentinel.deployments, result)
manager._list.assert_called_once_with('/v1/deployments', 'deployments')

View File

@ -1,210 +0,0 @@
# Copyright (c) 2014 Mirantis, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import os
import shutil
from muranoclient.apiclient import exceptions
from muranoclient.tests.unit import base
from muranoclient.v1.package_creator import hot_package
from muranoclient.v1.package_creator import mpl_package
FIXTURE_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__),
'fixture_data'))
TEMPLATE = os.path.join(FIXTURE_DIR, 'heat-template.yaml')
CLASSES_DIR = os.path.join(FIXTURE_DIR, 'test-app', 'Classes')
RESOURCES_DIR = os.path.join(FIXTURE_DIR, 'test-app', 'Resources')
UI = os.path.join(FIXTURE_DIR, 'test-app', 'ui.yaml')
LOGO = os.path.join(FIXTURE_DIR, 'logo.png')
class TestArgs(object):
pass
class PackageCreatorTest(base.TestAdditionalAsserts):
def test_generate_hot_manifest(self):
args = TestArgs()
args.template = TEMPLATE
args.name = 'test_name'
args.author = 'TestAuthor'
args.full_name = None
args.tags = None
args.description = None
expected_manifest = {
'Format': 'Heat.HOT/1.0',
'Type': 'Application',
'FullName': 'io.murano.apps.generated.TestName',
'Name': 'test_name',
'Description': 'Heat-defined application '
'for a template "heat-template.yaml"',
'Author': 'TestAuthor',
'Tags': ['Heat-generated']
}
result_manifest = hot_package.generate_manifest(args)
self.check_dict_is_subset(expected_manifest, result_manifest)
def test_generate_hot_manifest_nonexistent_template(self):
args = TestArgs()
args.template = '/home/this/path/does/not/exist'
self.assertRaises(exceptions.CommandError,
hot_package.generate_manifest,
args)
def test_generate_hot_manifest_with_all_parameters(self):
args = TestArgs()
args.template = TEMPLATE
args.name = 'test_name'
args.author = 'TestAuthor'
args.full_name = 'test.full.name.TestName'
args.tags = ['test', 'tag', 'Heat']
args.description = 'Test description'
expected_manifest = {
'Format': 'Heat.HOT/1.0',
'Type': 'Application',
'FullName': 'test.full.name.TestName',
'Name': 'test_name',
'Description': 'Test description',
'Author': 'TestAuthor',
'Tags': ['test', 'tag', 'Heat']
}
result_manifest = hot_package.generate_manifest(args)
self.check_dict_is_subset(expected_manifest, result_manifest)
def test_generate_hot_manifest_template_not_yaml(self):
args = TestArgs()
args.template = LOGO
args.name = None
args.full_name = None
self.assertRaises(exceptions.CommandError,
hot_package.generate_manifest, args)
def test_prepare_hot_package(self):
args = TestArgs()
args.template = TEMPLATE
args.name = 'test_name'
args.author = 'TestAuthor'
args.full_name = 'test.full.name.TestName'
args.tags = 'test, tag, Heat'
args.description = 'Test description'
args.resources_dir = RESOURCES_DIR
args.logo = LOGO
package_dir = hot_package.prepare_package(args)
prepared_files = ['manifest.yaml', 'logo.png',
'template.yaml', 'Resources']
self.assertEqual(sorted(prepared_files),
sorted(os.listdir(package_dir)))
shutil.rmtree(package_dir)
def test_generate_mpl_manifest(self):
args = TestArgs()
args.template = TEMPLATE
args.classes_dir = CLASSES_DIR
args.resources_dir = RESOURCES_DIR
args.type = 'Application'
args.author = 'TestAuthor'
args.name = None
args.full_name = None
args.tags = None
args.description = None
expected_manifest = {
'Format': 'MuranoPL/1.0',
'Type': 'Application',
'Classes': {'io.murano.apps.test.APP': 'testapp.yaml'},
'FullName': 'io.murano.apps.test.APP',
'Name': 'APP',
'Description': 'Description for the application is not provided',
'Author': 'TestAuthor',
}
result_manifest = mpl_package.generate_manifest(args)
self.check_dict_is_subset(expected_manifest, result_manifest)
def test_generate_mpl_manifest_with_all_parameters(self):
args = TestArgs()
args.template = TEMPLATE
args.classes_dir = CLASSES_DIR
args.resources_dir = RESOURCES_DIR
args.type = 'Application'
args.name = 'test_name'
args.author = 'TestAuthor'
args.full_name = 'test.full.name.TestName'
args.tags = ['test', 'tag', 'Heat']
args.description = 'Test description'
expected_manifest = {
'Format': 'MuranoPL/1.0',
'Type': 'Application',
'Classes': {'io.murano.apps.test.APP': 'testapp.yaml'},
'FullName': 'test.full.name.TestName',
'Name': 'test_name',
'Description': 'Test description',
'Author': 'TestAuthor',
'Tags': ['test', 'tag', 'Heat']
}
result_manifest = mpl_package.generate_manifest(args)
self.check_dict_is_subset(expected_manifest, result_manifest)
def test_generate_mpl_wrong_classes_dir(self):
args = TestArgs()
args.classes_dir = '/home/this/path/does/not/exist'
expected = ("'--classes-dir' parameter should be a directory", )
try:
mpl_package.generate_manifest(args)
except exceptions.CommandError as message:
self.assertEqual(expected, message.args)
def test_prepare_mpl_wrong_resources_dir(self):
args = TestArgs()
args.template = TEMPLATE
args.classes_dir = CLASSES_DIR
args.resources_dir = '/home/this/path/does/not/exist'
args.type = 'Application'
args.name = 'Test'
args.tags = ''
args.ui = UI
args.logo = LOGO
args.full_name = 'test.full.name.TestName'
args.author = 'TestAuthor'
args.description = 'Test description'
expected = ("'--resources-dir' parameter should be a directory", )
try:
mpl_package.prepare_package(args)
except exceptions.CommandError as message:
self.assertEqual(expected, message.args)
def test_prepare_mpl_package(self):
args = TestArgs()
args.template = TEMPLATE
args.classes_dir = CLASSES_DIR
args.resources_dir = RESOURCES_DIR
args.type = 'Application'
args.name = 'test_name'
args.author = 'TestAuthor'
args.full_name = 'test.full.name.TestName'
args.tags = 'test, tag, Heat'
args.description = 'Test description'
args.ui = UI
args.logo = LOGO
prepared_files = ['UI', 'Classes', 'manifest.yaml',
'Resources', 'logo.png']
package_dir = mpl_package.prepare_package(args)
self.assertEqual(sorted(prepared_files),
sorted(os.listdir(package_dir)))
shutil.rmtree(package_dir)

File diff suppressed because it is too large Load Diff

View File

@ -1,490 +0,0 @@
# Copyright (c) 2015 Mirantis, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import io
import json
import os.path
import tempfile
from unittest import mock
import zipfile
import requests
import requests_mock
import testtools
import yaml
from muranoclient.common import utils
class FileTest(testtools.TestCase):
def test_file_object_from_file(self):
f_obj = tempfile.NamedTemporaryFile(delete=True)
new_f_obj = utils.File(f_obj).open()
self.assertTrue(hasattr(new_f_obj, 'read'))
new_f_obj = utils.File(f_obj.name).open()
self.assertTrue(hasattr(new_f_obj, 'read'))
def test_file_object_file_fails(self):
f_obj = utils.File('')
self.assertRaises(ValueError, f_obj.open)
def test_file_object_url_fails(self):
resp = requests.Response()
resp.status_code = 400
resp.raw = io.BytesIO(b"123")
with mock.patch(
'requests.get',
mock.Mock(side_effect=lambda k, *args, **kwargs: resp)):
f = utils.File("http://127.0.0.1")
self.assertRaises(ValueError, f.open)
def test_file_object_url(self):
resp = requests.Response()
resp.raw = io.BytesIO(b"123")
resp.status_code = 200
with mock.patch(
'requests.get',
mock.Mock(side_effect=lambda k, *args, **kwargs: resp)):
new_f_obj = utils.File('http://127.0.0.1/').open()
self.assertTrue(hasattr(new_f_obj, 'read'))
def make_pkg(manifest_override, image_dicts=None):
manifest = {
'Author': '',
'Classes': {'foo': 'foo.yaml'},
'Description': '',
'Format': 1.0,
'FullName': 'org.foo',
'Name': 'Apache HTTP Server',
'Type': 'Application'}
manifest.update(manifest_override)
file_obj = io.BytesIO()
zfile = zipfile.ZipFile(file_obj, "a")
zfile.writestr('manifest.yaml', yaml.dump(manifest))
zfile.writestr('Classes/foo.yaml', yaml.dump({}))
if image_dicts:
images_list = []
default_image_spec = {
'ContainerFormat': 'bare',
'DiskFormat': 'qcow2',
'Name': '',
}
for image_dict in image_dicts:
image_spec = default_image_spec.copy()
image_spec.update(image_dict)
images_list.append(image_spec)
images = {'Images': images_list, }
zfile.writestr('images.lst', yaml.dump(images))
zfile.close()
file_obj.seek(0)
return file_obj
class PackageTest(testtools.TestCase):
base_url = "http://127.0.0.1"
@requests_mock.mock()
def test_from_location_local_file(self, m):
temp = tempfile.NamedTemporaryFile()
pkg = make_pkg({'FullName': 'single_app'})
temp.write(pkg.read())
temp.flush()
path, name = os.path.split(temp.name)
# ensure we do not go to base url
m.get(self.base_url + '/apps/{0}.zip'.format(name), status_code=404)
self.assertEqual('single_app', utils.Package.from_location(
name=name, base_url=self.base_url,
path=path,
).manifest['FullName'])
def test_package_from_directory(self):
path = os.path.join(os.path.dirname(os.path.realpath(__file__)),
"fixture_data/empty-app")
pkg = utils.Package(utils.File(path))
self.assertEqual('empty', pkg.manifest['FullName'])
pkg = utils.Package.from_location('', path=path)
self.assertEqual('empty', pkg.manifest['FullName'])
@requests_mock.mock()
def test_from_location_url(self, m):
"""Test that url overrides name specification."""
pkg = make_pkg({'FullName': 'single_app'})
m.get('http://127.0.0.2/apps/single_app.zip', body=pkg)
m.get(self.base_url + '/apps/single_app.zip', status_code=404)
self.assertEqual('single_app', utils.Package.from_location(
name='single_app', base_url=self.base_url,
url="http://127.0.0.2/apps/single_app.zip",
).manifest['FullName'])
@requests_mock.mock()
def test_from_location(self, m):
"""Test from location url requesting mechanism."""
pkg = make_pkg({'FullName': 'single_app'})
pkg_ver = make_pkg({'FullName': 'single_app'})
m.get(self.base_url + '/apps/single_app.zip', body=pkg)
m.get(self.base_url + '/apps/single_app.1.0.zip', body=pkg_ver)
m.get(self.base_url + '/apps/single_app.2.0.zip', status_code=404)
self.assertEqual('single_app', utils.Package.from_location(
name='single_app', base_url=self.base_url).manifest['FullName'])
self.assertEqual('single_app', utils.Package.from_location(
name='single_app',
version='1.0',
base_url=self.base_url).manifest['FullName'])
self.assertRaises(
ValueError,
utils.Package.from_location,
name='single_app',
version='2.0',
base_url=self.base_url)
def test_no_requirements(self):
pkg = make_pkg({'FullName': 'single_app'})
app = utils.Package.fromFile(pkg)
reqs = app.requirements(base_url=self.base_url)
self.assertEqual({'single_app': app}, reqs)
@requests_mock.mock()
def test_requirements(self, m):
"""Test that dependencies are parsed correctly."""
pkg3 = make_pkg({'FullName': 'dep_of_dep'})
pkg2 = make_pkg({'FullName': 'dep_app', 'Require': {
'dep_of_dep': "1.0"}, })
pkg1 = make_pkg({'FullName': 'main_app', 'Require': {
'dep_app': None}, })
m.get(self.base_url + '/apps/main_app.zip', body=pkg1)
m.get(self.base_url + '/apps/dep_app.zip', body=pkg2)
m.get(self.base_url + '/apps/dep_of_dep.1.0.zip', body=pkg3)
app = utils.Package.fromFile(pkg1)
reqs = app.requirements(base_url=self.base_url)
self.assertEqual(
{'main_app': app, 'dep_app': mock.ANY, 'dep_of_dep': mock.ANY},
reqs)
@mock.patch('muranoclient.common.utils.Package.from_file')
def test_requirements_order(self, from_file):
"""Test that dependencies are parsed in correct order."""
pkg5 = make_pkg({'FullName': 'd4', })
pkg4 = make_pkg({'FullName': 'd3', 'Require': {'d4': None}, })
pkg3 = make_pkg({'FullName': 'd2', 'Require': {'d3': None}, })
pkg2 = make_pkg({'FullName': 'd1', 'Require': {'d3': None}, })
pkg1 = make_pkg({'FullName': 'M', 'Require': {'d1': None,
'd2': None,
'd4': None}, })
def side_effect(name):
if 'M' in name:
return utils.Package(utils.File(pkg1))
if 'd1' in name:
return utils.Package(utils.File(pkg2))
if 'd2' in name:
return utils.Package(utils.File(pkg3))
if 'd3' in name:
return utils.Package(utils.File(pkg4))
if 'd4' in name:
return utils.Package(utils.File(pkg5))
from_file.side_effect = side_effect
app = from_file('M')
reqs = app.requirements(base_url=self.base_url)
def key_position(key):
keys = list(iter(reqs.keys()))
return keys.index(key)
self.assertTrue(
key_position('d4') < key_position('d3') and
key_position('d4') < key_position('M') and
key_position('d3') < key_position('d1') and
key_position('d3') < key_position('d2') < key_position('M')
)
@mock.patch('muranoclient.common.utils.Package.from_file')
def test_requirements_order2(self, from_file):
"""Test that dependencies are parsed in correct order."""
pkg5 = make_pkg({'FullName': 'd4', 'Require': {'d6': None}, })
pkg4 = make_pkg({'FullName': 'd3', 'Require': {'d4': None}, })
pkg3 = make_pkg({'FullName': 'd1', 'Require': {'d3': None,
'd7': None}, })
pkg2 = make_pkg({'FullName': 'd2', 'Require': {'d3': None}, })
pkg6 = make_pkg({'FullName': 'd6', })
pkg7 = make_pkg({'FullName': 'd7', 'Require': {'d8': None}, })
pkg8 = make_pkg({'FullName': 'd8', })
pkg1 = make_pkg({'FullName': 'M', 'Require': {'d1': None,
'd2': None,
'd4': None, }, })
def side_effect(name):
if 'M' in name:
return utils.Package(utils.File(pkg1))
if 'd1' in name:
return utils.Package(utils.File(pkg2))
if 'd2' in name:
return utils.Package(utils.File(pkg3))
if 'd3' in name:
return utils.Package(utils.File(pkg4))
if 'd4' in name:
return utils.Package(utils.File(pkg5))
if 'd6' in name:
return utils.Package(utils.File(pkg6))
if 'd7' in name:
return utils.Package(utils.File(pkg7))
if 'd8' in name:
return utils.Package(utils.File(pkg8))
from_file.side_effect = side_effect
app = from_file('M')
reqs = app.requirements(base_url=self.base_url)
def key_position(key):
keys = list(iter(reqs.keys()))
return keys.index(key)
self.assertTrue(
key_position('d6') < key_position('d4') <
key_position('d3') < key_position('d1') and
key_position('d3') < key_position('d2') and
key_position('d1') < key_position('M') and
key_position('d2') < key_position('M') and
key_position('d8') < key_position('d7') < key_position('d1')
)
@mock.patch('muranoclient.common.utils.Package.from_file')
def test_cyclic_requirements(self, from_file):
"""Test that a cyclic dependency would be handled correctly."""
pkg3 = make_pkg({'FullName': 'dep_of_dep', 'Require': {
'main_app': None, 'dep_app': None}, })
pkg2 = make_pkg({'FullName': 'dep_app', 'Require': {
'dep_of_dep': None, 'main_app': None}, })
pkg1 = make_pkg({'FullName': 'main_app', 'Require': {
'dep_app': None, 'dep_of_dep': None}, })
def side_effect(name):
if 'main_app' in name:
return utils.Package(utils.File(pkg1))
if 'dep_app' in name:
return utils.Package(utils.File(pkg2))
if 'dep_of_dep' in name:
return utils.Package(utils.File(pkg3))
from_file.side_effect = side_effect
app = from_file('main_app')
reqs = app.requirements(base_url=self.base_url)
self.assertEqual(
{'main_app': app, 'dep_app': mock.ANY, 'dep_of_dep': mock.ANY},
reqs)
@mock.patch('muranoclient.common.utils.Package.from_file')
def test_order_with_cyclic_requirements2(self, from_file):
"""Test that dependencies are parsed in correct order."""
pkg6 = make_pkg({'FullName': 'd5', 'Require': {'d6': None}, })
pkg7 = make_pkg({'FullName': 'd6', })
pkg5 = make_pkg({'FullName': 'd4', 'Require': {'d3': None,
'd5': None}})
pkg4 = make_pkg({'FullName': 'd3', 'Require': {'d4': None}, })
pkg3 = make_pkg({'FullName': 'd2', 'Require': {'d1': None,
'd5': None,
'd6': None}, })
pkg2 = make_pkg({'FullName': 'd1', 'Require': {'d2': None}, })
pkg1 = make_pkg({'FullName': 'M', 'Require': {'d1': None,
'd3': None}, })
def side_effect(name):
if 'M' in name:
return utils.Package(utils.File(pkg1))
if 'd1' in name:
return utils.Package(utils.File(pkg2))
if 'd2' in name:
return utils.Package(utils.File(pkg3))
if 'd3' in name:
return utils.Package(utils.File(pkg4))
if 'd4' in name:
return utils.Package(utils.File(pkg5))
if 'd5' in name:
return utils.Package(utils.File(pkg6))
if 'd6' in name:
return utils.Package(utils.File(pkg7))
from_file.side_effect = side_effect
app = from_file('M')
reqs = app.requirements(base_url=self.base_url)
def key_position(key):
keys = list(iter(reqs.keys()))
return keys.index(key)
self.assertTrue(
key_position('d5') < key_position('d4') and
key_position('d5') < key_position('d2') and
key_position('d5') < key_position('d3') < key_position('M') and
key_position('d5') < key_position('d1') < key_position('M')
)
@mock.patch('muranoclient.common.utils.Package.from_file')
def test_order_with_cyclic_requirements3(self, from_file):
"""Test that dependencies are parsed in correct order."""
pkg5 = make_pkg({'FullName': 'd4', })
pkg4 = make_pkg({'FullName': 'd3', 'Require': {'M': None}, })
pkg3 = make_pkg({'FullName': 'd2', 'Require': {'d3': None,
'd4': None}, })
pkg2 = make_pkg({'FullName': 'd1', 'Require': {'d2': None}, })
pkg1 = make_pkg({'FullName': 'M', 'Require': {'d1': None}, })
def side_effect(name):
if 'M' in name:
return utils.Package(utils.File(pkg1))
if 'd1' in name:
return utils.Package(utils.File(pkg2))
if 'd2' in name:
return utils.Package(utils.File(pkg3))
if 'd3' in name:
return utils.Package(utils.File(pkg4))
if 'd4' in name:
return utils.Package(utils.File(pkg5))
from_file.side_effect = side_effect
app = from_file('M')
reqs = app.requirements(base_url=self.base_url)
def key_position(key):
keys = list(iter(reqs.keys()))
return keys.index(key)
self.assertTrue(
key_position('d4') < key_position('M') and
key_position('d4') < key_position('d1') and
key_position('d4') < key_position('d2') and
key_position('d4') < key_position('d3')
)
def test_images(self):
pkg = make_pkg({})
app = utils.Package.fromFile(pkg)
self.assertEqual([], app.images())
pkg = make_pkg(
{}, [{'Name': 'test.qcow2'}, {'Name': 'test2.qcow2'}])
app = utils.Package.fromFile(pkg)
self.assertEqual(
set(['test.qcow2', 'test2.qcow2']),
set([img['Name'] for img in app.images()]))
def test_file_object_repo_fails(self):
resp = requests.Response()
resp.raw = io.BytesIO(b"123")
resp.status_code = 400
with mock.patch(
'requests.get',
mock.Mock(side_effect=lambda k, *args, **kwargs: resp)):
self.assertRaises(
ValueError, utils.Package.from_location,
name='foo.bar.baz', base_url='http://127.0.0.1')
def test_no_repo_url_fails(self):
self.assertRaises(ValueError, utils.Package.from_location,
name='foo.bar.baz', base_url='')
@mock.patch.object(utils.Package, 'validate')
def test_file_object_repo(self, m_validate):
resp = requests.Response()
resp.raw = io.BytesIO(b"123")
resp.status_code = 200
m_validate.return_value = None
with mock.patch(
'requests.get',
mock.Mock(side_effect=lambda k, *args, **kwargs: resp)):
new_f_obj = utils.Package.from_location(
name='foo.bar.baz', base_url='http://127.0.0.1').file()
self.assertTrue(hasattr(new_f_obj, 'read'))
class BundleTest(testtools.TestCase):
base_url = "http://127.0.0.1"
@requests_mock.mock()
def test_packages(self, m):
s = io.StringIO()
bundle_contents = {'Packages': [
{'Name': 'first_app'},
{'Name': 'second_app', 'Version': '1.0'}
]}
json.dump(bundle_contents, s)
s.seek(0)
bundle = utils.Bundle.from_file(s)
self.assertEqual(
set(['first_app', 'second_app']),
set([p['Name'] for p in bundle.package_specs()])
)
# setup packages
pkg1 = make_pkg({'FullName': 'first_app'})
pkg2 = make_pkg({'FullName': 'second_app'})
m.get(self.base_url + '/apps/first_app.zip', body=pkg1)
m.get(self.base_url + '/apps/second_app.1.0.zip', body=pkg2)
self.assertEqual(
set(['first_app', 'second_app']),
set([p.manifest['FullName']
for p in
bundle.packages(base_url=self.base_url)])
)
class TraverseTest(testtools.TestCase):
def test_traverse_and_replace(self):
obj = [
{'id': '===id1==='},
{'id': '===id2===', 'x': [{'bar': '===id1==='}]},
['===id1===', '===id2==='],
'===id3===',
'===nonid0===',
'===id3===',
]
utils.traverse_and_replace(obj)
self.assertNotEqual('===id1===', obj[0]['id'])
self.assertNotEqual('===id2===', obj[1]['id'])
self.assertNotEqual('===id1===', obj[1]['x'][0]['bar'])
self.assertNotEqual('===id1===', obj[2][0])
self.assertNotEqual('===id2===', obj[2][1])
self.assertNotEqual('===id3===', obj[3])
self.assertEqual('===nonid0===', obj[4])
self.assertNotEqual('===id3===', obj[5])
self.assertEqual(obj[0]['id'], obj[1]['x'][0]['bar'])
self.assertEqual(obj[0]['id'], obj[2][0])
self.assertEqual(obj[1]['id'], obj[2][1])
self.assertEqual(obj[3], obj[5])

View File

@ -1,35 +0,0 @@
# Copyright (c) 2014 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.
# Not a true manager yet; should be changed to be one if CRUD
# functionality becomes available for actions.
class ActionManager(object):
def __init__(self, api):
self.api = api
def call(self, environment_id, action_id, arguments=None):
if arguments is None:
arguments = {}
url = '/v1/environments/{environment_id}/actions/{action_id}'.format(
environment_id=environment_id, action_id=action_id)
resp, body = self.api.json_request(url, 'POST', body=arguments)
return body['task_id']
def get_result(self, environment_id, task_id):
url = '/v1/environments/{environment_id}/actions/{task_id}'.format(
environment_id=environment_id, task_id=task_id)
resp, body = self.api.json_request(url, 'GET')
return body or None

View File

@ -1,363 +0,0 @@
# Copyright (c) 2015 Mirantis, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import collections
from glanceclient import exc as glance_exc
import yaml
from muranoclient.common import exceptions as exc
from muranoclient.common import utils
from muranoclient.i18n import _
def rewrap_http_exceptions(func):
def inner(*args, **kwargs):
try:
return func(*args, **kwargs)
except glance_exc.HTTPException as e:
raise exc.from_code(e.code)
return inner
class ArtifactRepo(object):
def __init__(self, client, tenant=None):
self.tenant = tenant
self.client = client
def create(self, fqn, data, **kwargs):
package = utils.Package.from_file(data)
manifest = package.manifest
package_draft = {
'name': manifest.get('FullName', fqn),
'version': manifest.get('Version', '0.0.0'),
'description': manifest.get('Description'),
'display_name': manifest.get('Name', fqn),
'type': manifest.get('Type', 'Application'),
'author': manifest.get('Author'),
'tags': manifest.get('Tags', []),
'class_definitions': package.classes.keys()
}
for k, v in kwargs.items():
package_draft[k] = v
inherits = self._get_local_inheritance(package.classes,
package.resolvers)
# check for global inheritance
ancestor_queue = collections.deque(inherits.keys())
while ancestor_queue:
ancestor_name = ancestor_queue.popleft()
child_classes = inherits[ancestor_name]
ancestors = self.list(class_definitions=ancestor_name)
for ancestor in ancestors:
# check if ancestor inherits anything
ancestor_inherits = \
ancestor.type_specific_properties.get('inherits', {})
for name, value in ancestor_inherits.items():
# check if this is the class we actually inherit
if ancestor_name in value:
ancestor_queue.append(name)
inherits[name] = child_classes
package_draft['inherits'] = inherits
keywords = self._keywords_from_display_name(
package_draft['display_name'])
keywords.extend(package_draft['tags'])
package_draft['keywords'] = keywords
# NOTE(ativelkov): this is very racy, but until we have a chance to
# enforce uniqueness right in glance this is the only way to do it
visibility = package_draft.get('visibility', 'private')
if visibility == 'public':
filters = {}
else:
filters = {'owner': self.tenant}
existing = self.list(name=package_draft['name'],
version=package_draft['version'], **filters)
try:
next(existing)
raise exc.HTTPConflict("Package already exists")
except StopIteration:
pass
res = self.client.artifacts.create(**package_draft)
app_id = res.id
self.client.artifacts.upload_blob(app_id, 'archive', package.file())
if package.logo is not None:
self.client.artifacts.upload_blob(app_id, 'logo', package.logo)
if package.ui is not None:
self.client.artifacts.upload_blob(app_id, 'ui_definition',
package.ui)
package.file().close()
self.client.artifacts.active(app_id)
return self.client.artifacts.get(app_id)
@staticmethod
def _get_local_inheritance(classes, resolvers):
result = {}
for class_name, klass in classes.items():
if 'Extends' not in klass:
continue
ns = klass.get('Namespaces')
if ns:
resolver = utils.NamespaceResolver(ns)
else:
resolver = resolvers.get(class_name)
if isinstance(klass['Extends'], list):
bases = klass['Extends']
else:
bases = [klass['Extends']]
for base_class in bases:
if resolver:
base_fqn = resolver.resolve_name(base_class)
else:
base_fqn = base_class
result.setdefault(base_fqn, []).append(class_name)
return result
@staticmethod
def _keywords_from_display_name(display_name):
return display_name.split()[:10]
def list(self,
sort_field='name',
sort_dir='asc',
type=None,
tags=None,
limit=None,
page_size=None,
**filters):
sort = "%s:%s" % (sort_field, sort_dir)
if type is not None:
filters['type'] = type
if tags is not None:
filters['tag'] = tags
return self.client.artifacts.list(sort=sort,
limit=limit,
page_size=page_size,
filters=filters)
def get(self, app_id):
return self.client.artifacts.get(app_id)
def delete(self, app_id):
return self.client.artifacts.delete(app_id)
def update(self, app_id, props_to_remove=None, **new_props):
new_keywords = []
new_name = new_props.get('display_name')
new_tags = new_props.get('tags')
if new_name:
new_keywords.extend(self._keywords_from_display_name(new_name))
if new_tags:
new_keywords.extend(new_tags)
if new_keywords:
new_props['keywords'] = new_keywords
visibility = new_props.get('visibility')
if visibility == 'public':
package = self.client.artifacts.get(app_id)
# NOTE(ativelkov): this is very racy, but until we have a chance to
# enforce uniqueness right in glance this is the only way to do it
existing = self.list(name=package.name,
version=package.version,
visibility='public')
try:
while True:
package = next(existing)
if package.id == app_id:
continue
else:
raise exc.HTTPConflict("Package already exists")
except StopIteration:
pass
return self.client.artifacts.update(app_id,
remove_props=props_to_remove,
**new_props)
def toggle_active(self, app_id):
old_val = self.get(app_id).type_specific_properties['enabled']
return self.update(app_id, enabled=(not old_val))
def toggle_public(self, app_id):
visibility = self.get(app_id).visibility
if visibility == 'public':
return self.update(app_id, visibility='private')
else:
return self.update(app_id, visibility='public')
def download(self, app_id):
return self.client.artifacts.download_blob(app_id, 'archive')
def get_ui(self, app_id, loader_cls=None):
ui_stream = "".join(
self.client.artifacts.download_blob(app_id, 'ui_definition'))
if loader_cls is None:
loader_cls = yaml.SafeLoader
return yaml.load(ui_stream, loader_cls)
def get_logo(self, app_id):
return self.client.artifacts.download_blob(app_id, 'logo')
class PackageManagerAdapter(object):
def __init__(self, legacy, glare):
self.legacy = legacy
self.glare = glare
def categories(self):
return self.legacy.categories()
@rewrap_http_exceptions
def create(self, data, files):
is_public = data.pop('is_public', None)
if is_public is not None:
data['visibility'] = 'public' if is_public else 'private'
fqn = list(files.keys())[0]
pkg = self.glare.create(fqn, files[fqn], **data)
return PackageWrapper(pkg)
@rewrap_http_exceptions
def filter(self, **kwargs):
kwargs.pop('catalog', None) # NOTE(ativelkov): Glare ignores 'catalog'
include_disabled = kwargs.pop('include_disabled', False)
order_by = kwargs.pop('order_by', None)
search = kwargs.pop('search', None)
category = kwargs.pop('category', None)
fqn = kwargs.pop('fqn', None)
class_name = kwargs.pop('class_name', None)
name = kwargs.pop('name', None)
if category:
kwargs['categories'] = category
if search:
kwargs['keywords'] = search
if order_by:
kwargs['sort_field'] = order_by
if not include_disabled:
kwargs['enabled'] = True
if fqn:
kwargs['name'] = fqn
if class_name:
kwargs['class_definitions'] = class_name
if name:
kwargs['display_name'] = name
# if 'owned' is used there should be a filter with 'owner' parameter
if kwargs.pop('owned', None):
kwargs['owner'] = self.glare.tenant
for pkg in self.glare.list(**kwargs):
yield PackageWrapper(pkg)
@rewrap_http_exceptions
def list(self, include_disabled=False):
return self.filter(include_disabled=include_disabled)
@rewrap_http_exceptions
def delete(self, app_id):
return self.glare.delete(app_id)
@rewrap_http_exceptions
def get(self, app_id):
return PackageWrapper(self.glare.get(app_id))
@rewrap_http_exceptions
def update(self, app_id, body, operation='replace'):
is_public = body.pop('is_public', None)
name = body.pop('name', None)
if is_public is not None:
body['visibility'] = 'public' if is_public else 'private'
if name is not None:
body['display_name'] = name
if operation == 'replace':
return PackageWrapper(self.glare.update(app_id, None, **body))
@rewrap_http_exceptions
def toggle_active(self, app_id):
return self.glare.toggle_active(app_id)
@rewrap_http_exceptions
def toggle_public(self, app_id):
return self.glare.toggle_public(app_id)
@rewrap_http_exceptions
def download(self, app_id):
return "".join(self.glare.download(app_id))
@rewrap_http_exceptions
def get_logo(self, app_id):
return "".join(self.glare.get_logo(app_id))
@rewrap_http_exceptions
def get_ui(self, app_id, loader_cls=None):
return self.legacy.get_ui(app_id, loader_cls)
class PackageWrapper(object):
def __init__(self, item):
self._item = item
@property
def updated(self):
return self._item.updated_at
@property
def created(self):
return self._item.created_at
@property
def is_public(self):
return self._item.visibility == 'public'
@property
def name(self):
return self._item.type_specific_properties['display_name']
@property
def fully_qualified_name(self):
return self._item.name
@property
def owner_id(self):
return self._item.owner
def __getstate__(self):
return {"item": self._item}
def __setstate__(self, state):
self._item = state['item']
def __getattr__(self, name):
if name in self._item.type_specific_properties:
return self._item.type_specific_properties.get(name)
else:
return getattr(self._item, name)
def to_dict(self):
keys = ('author', 'categories', 'class_definitions', 'created',
'description', 'enabled', 'fully_qualified_name', 'id',
'is_public', 'name', 'owner_id', 'tags', 'type',
'updated')
missing_keys = [key for key in keys if not hasattr(self, key)]
if missing_keys:
raise KeyError(_("Some attributes are missing in "
"%(pkg_name)s: %(attrs)s.") %
{'pkg_name': self.name,
'attrs': ", ".join(missing_keys)})
return {key: getattr(self, key) for key in keys}

View File

@ -1,57 +0,0 @@
# Copyright (c) 2015 Mirantis, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import urllib
from muranoclient.common import base
class Category(base.Resource):
def __repr__(self):
return "<Category %s>" % self._info
def data(self, **kwargs):
return self.manager.data(self, **kwargs)
class CategoryManager(base.Manager):
resource_class = Category
def list(self, **kwargs):
"""Get category list with pagination support.
:param sort_keys: an array of fields used to sort the list (string)
:param sort_dir: 'asc' or 'desc' for ascending or descending sort
:param limit: maximum number of categories to return
:param marker: begin returning categories that appear later in the
category list than that represented by this marker id
"""
params = {}
for key, value in kwargs.items():
if value:
params[key] = value
url = '/v1/catalog/categories?{0}'.format(
urllib.parse.urlencode(params, True))
return self._list(url, response_key='categories')
def get(self, id):
return self._get('/v1/catalog/categories/{0}'.format(id))
def add(self, data):
return self._create('/v1/catalog/categories', data)
def delete(self, id):
return self._delete('/v1/catalog/categories/{0}'.format(id))

View File

@ -1,67 +0,0 @@
# Copyright (c) 2013 Mirantis, 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 muranoclient.common import http
from muranoclient.v1 import actions
from muranoclient.v1 import artifact_packages
from muranoclient.v1 import categories
from muranoclient.v1 import deployments
from muranoclient.v1 import environments
from muranoclient.v1 import instance_statistics
from muranoclient.v1 import packages
from muranoclient.v1 import request_statistics
from muranoclient.v1 import schemas
from muranoclient.v1 import services
from muranoclient.v1 import sessions
from muranoclient.v1 import static_actions
from muranoclient.v1 import templates
class Client(object):
"""Client for the Murano v1 API.
:param string endpoint: A user-supplied endpoint URL for the 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 Murano v1 API."""
self.glance_client = kwargs.pop('glance_client', None)
tenant = kwargs.pop('tenant', None)
artifacts_client = kwargs.pop('artifacts_client', None)
self.http_client = http._construct_http_client(*args, **kwargs)
self.environments = environments.EnvironmentManager(self.http_client)
self.env_templates = templates.EnvTemplateManager(self.http_client)
self.sessions = sessions.SessionManager(self.http_client)
self.services = services.ServiceManager(self.http_client)
self.deployments = deployments.DeploymentManager(self.http_client)
self.schemas = schemas.SchemaManager(self.http_client)
self.request_statistics = \
request_statistics.RequestStatisticsManager(self.http_client)
self.instance_statistics = \
instance_statistics.InstanceStatisticsManager(self.http_client)
pkg_mgr = packages.PackageManager(self.http_client)
if artifacts_client:
artifact_repo = artifact_packages.ArtifactRepo(artifacts_client,
tenant)
self.packages = artifact_packages.PackageManagerAdapter(
pkg_mgr, artifact_repo)
else:
self.packages = pkg_mgr
self.actions = actions.ActionManager(self.http_client)
self.static_actions = static_actions.StaticActionManager(
self.http_client)
self.categories = categories.CategoryManager(self.http_client)

View File

@ -1,54 +0,0 @@
# Copyright (c) 2013 Mirantis, 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 muranoclient.common import base
class Deployment(base.Resource):
def __repr__(self):
return '<Deployment %s>' % self._info
def data(self, **kwargs):
return self.manager.data(self, **kwargs)
class Status(base.Resource):
def __repr__(self):
return '<Status %s>' % self._info
def data(self, **kwargs):
return self.manager.data(self, **kwargs)
class DeploymentManager(base.Manager):
resource_class = Deployment
def list(self, environment_id, all_environments=False):
if all_environments:
return self._list('/v1/deployments', 'deployments')
else:
return self._list('/v1/environments/{id}/deployments'.
format(id=environment_id), 'deployments')
def reports(self, environment_id, deployment_id, *service_ids):
path = '/v1/environments/{id}/deployments/{deployment_id}'
path = path.format(id=environment_id, deployment_id=deployment_id)
if service_ids:
for service_id in service_ids:
path += '?service_id={0}'.format(service_id)
resp, body = self.api.json_request(path, 'GET')
data = body.get('reports', [])
return [Status(self, res, loaded=True) for res in data if res]

View File

@ -1,92 +0,0 @@
# Copyright (c) 2013 Mirantis, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import urllib
from muranoclient.common import base
class Environment(base.Resource):
def __repr__(self):
return "<Environment %s>" % self._info
def data(self, **kwargs):
return self.manager.data(self, **kwargs)
class Status(base.Resource):
def __repr__(self):
return '<Status %s>' % self._info
def data(self, **kwargs):
return self.manager.data(self, **kwargs)
class EnvironmentManager(base.ManagerWithFind):
resource_class = Environment
def list(self, all_tenants=False, tenant_id=None):
params = {'all_tenants': all_tenants}
if tenant_id:
params['tenant'] = tenant_id
path = '/v1/environments?{query}'.format(
query=urllib.parse.urlencode(params))
return self._list(path, 'environments')
def create(self, data):
return self._create('/v1/environments', data)
def update(self, environment_id, name):
return self._update('/v1/environments/{id}'.format(id=environment_id),
data={'name': name})
def delete(self, environment_id, abandon=False):
path = '/v1/environments/{id}?{query}'.format(
id=environment_id,
query=urllib.parse.urlencode({'abandon': abandon}))
return self._delete(path)
def get(self, environment_id, session_id=None):
if session_id:
headers = {'X-Configuration-Session': session_id}
else:
headers = {}
return self._get("/v1/environments/{id}".format(id=environment_id),
headers=headers)
def last_status(self, environment_id, session_id):
headers = {'X-Configuration-Session': session_id}
path = '/v1/environments/{id}/lastStatus'
path = path.format(id=environment_id)
status_dict = self._get(path, return_raw=True,
response_key='lastStatuses',
headers=headers)
result = {}
for k, v in status_dict.items():
if v:
result[k] = Status(self, v, loaded=True)
return result
def get_model(self, environment_id, path, session_id=None):
headers = {'X-Configuration-Session': session_id}
url = '/v1/environments/{id}/model/{path}'
url = url.format(id=environment_id, path=path)
return self._get(url, return_raw=True, headers=headers)
def update_model(self, environment_id, data, session_id):
headers = {'X-Configuration-Session': session_id}
url = '/v1/environments/{id}/model/'
url = url.format(id=environment_id)
return self._update(url, data, return_raw=True, headers=headers,
method='PATCH',
content_type='application/env-model-json-patch')

View File

@ -1,42 +0,0 @@
# Copyright (c) 2013 Mirantis, 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 muranoclient.common import base
class InstanceStatistics(base.Resource):
def __repr__(self):
return "<Instance statistics %s>" % self._info
def data(self, **kwargs):
return self.manager.data(self, **kwargs)
class InstanceStatisticsManager(base.Manager):
resource_class = InstanceStatistics
def get(self, environment_id, instance_id=None):
if instance_id:
path = '/v1/environments/{id}/instance-statistics/raw/' \
'{instance_id}'.format(id=environment_id,
instance_id=instance_id)
else:
path = '/v1/environments/{id}/instance-statistics/raw'.format(
id=environment_id)
return self._list(path, None)
def get_aggregated(self, environment_id):
path = '/v1/environments/{id}/instance-statistics/aggregated'.format(
id=environment_id)
return self._list(path, None)

Some files were not shown because too many files have changed in this diff Show More