diff --git a/.gitreview b/.gitreview new file mode 100644 index 00000000..6ee9eaf9 --- /dev/null +++ b/.gitreview @@ -0,0 +1,4 @@ +[gerrit] +host=review.openstack.org +port=29418 +project=stackforge/python-mistralclient.git diff --git a/AUTHORS b/AUTHORS new file mode 100644 index 00000000..32ce1cb3 --- /dev/null +++ b/AUTHORS @@ -0,0 +1,4 @@ +Renat Akhmerov +Nikolay Makhotkin +Alexander Kuznetsov + diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..67db8588 --- /dev/null +++ b/LICENSE @@ -0,0 +1,175 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. diff --git a/doc/source/conf.py b/doc/source/conf.py new file mode 100644 index 00000000..d3826b06 --- /dev/null +++ b/doc/source/conf.py @@ -0,0 +1,245 @@ +# -*- coding: utf-8 -*- +# +# Mistral documentation build configuration file, created by +# sphinx-quickstart on Fri Nov 1 02:06:28 2013. +# +# This file is execfile()d with the current directory set to its containing +# dir. +# +# Note that not all possible configuration values are present in this +# autogenerated file. +# +# All configuration values have a default; values that are commented out +# serve to show the default. + +# import sys, os + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +#sys.path.insert(0, os.path.abspath('.')) + +# -- General configuration --------------------------------------------------- + +# If your documentation needs a minimal Sphinx version, state it here. +#needs_sphinx = '1.0' + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones. +extensions = [] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# The suffix of source filenames. +source_suffix = '.rst' + +# The encoding of source files. +#source_encoding = 'utf-8-sig' + +# The master toctree document. +master_doc = 'index' + +# General information about the project. +project = u'Mistral Client' +copyright = u'2013, OpenStack Foundation' + +# The version info for the project you're documenting, acts as replacement for +# |version| and |release|, also used in various other places throughout the +# built documents. +# +# The short X.Y version. +version = '0.01' +# The full version, including alpha/beta/rc tags. +release = '0.01' + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +#language = None + +# There are two options for replacing |today|: either, you set today to some +# non-false value, then it is used: +#today = '' +# Else, today_fmt is used as the format for a strftime call. +#today_fmt = '%B %d, %Y' + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +exclude_patterns = [] + +# The reST default role (used for this markup: `text`) to use for all +# documents. +#default_role = None + +# If true, '()' will be appended to :func: etc. cross-reference text. +#add_function_parentheses = True + +# If true, the current module name will be prepended to all description +# unit titles (such as .. function::). +#add_module_names = True + +# If true, sectionauthor and moduleauthor directives will be shown in the +# output. They are ignored by default. +#show_authors = False + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'sphinx' + +# A list of ignored prefixes for module index sorting. +#modindex_common_prefix = [] + + +# -- Options for HTML output ------------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +html_theme = 'default' + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +#html_theme_options = {} + +# Add any paths that contain custom themes here, relative to this directory. +#html_theme_path = [] + +# The name for this set of Sphinx documents. If None, it defaults to +# " v documentation". +#html_title = None + +# A shorter title for the navigation bar. Default is the same as html_title. +#html_short_title = None + +# The name of an image file (relative to this directory) to place at the top +# of the sidebar. +#html_logo = None + +# The name of an image file (within the static path) to use as favicon of the +# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 +# pixels large. +#html_favicon = None + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] + +# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, +# using the given strftime format. +#html_last_updated_fmt = '%b %d, %Y' + +# If true, SmartyPants will be used to convert quotes and dashes to +# typographically correct entities. +#html_use_smartypants = True + +# Custom sidebar templates, maps document names to template names. +#html_sidebars = {} + +# Additional templates that should be rendered to pages, maps page names to +# template names. +#html_additional_pages = {} + +# If false, no module index is generated. +#html_domain_indices = True + +# If false, no index is generated. +#html_use_index = True + +# If true, the index is split into individual pages for each letter. +#html_split_index = False + +# If true, links to the reST sources are added to the pages. +#html_show_sourcelink = True + +# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. +#html_show_sphinx = True + +# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. +#html_show_copyright = True + +# If true, an OpenSearch description file will be output, and all pages will +# contain a tag referring to it. The value of this option must be the +# base URL from which the finished HTML is served. +#html_use_opensearch = '' + +# This is the file name suffix for HTML files (e.g. ".xhtml"). +#html_file_suffix = None + +# Output file base name for HTML help builder. +htmlhelp_basename = 'Mistraldoc' + + +# -- Options for LaTeX output ------------------------------------------------ + +latex_elements = { + # The paper size ('letterpaper' or 'a4paper'). + #'papersize': 'letterpaper', + + # The font size ('10pt', '11pt' or '12pt'). + #'pointsize': '10pt', + + # Additional stuff for the LaTeX preamble. + #'preamble': '', +} + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, author, +# documentclass [howto/manual]). +latex_documents = [ + ('index', 'MistralClient.tex', u'Mistral Client Documentation', + u'OpenStack Foundation', 'manual'), +] + +# The name of an image file (relative to this directory) to place at the top of +# the title page. +#latex_logo = None + +# For "manual" documents, if this is true, then toplevel headings are parts, +# not chapters. +#latex_use_parts = False + +# If true, show page references after internal links. +#latex_show_pagerefs = False + +# If true, show URL addresses after external links. +#latex_show_urls = False + +# Documents to append as an appendix to all manuals. +#latex_appendices = [] + +# If false, no module index is generated. +#latex_domain_indices = True + + +# -- Options for manual page output ------------------------------------------ + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [ + ('index', 'mistral_client', u'Mistral Client Documentation', + [u'OpenStack Foundation'], 1) +] + +# If true, show URL addresses after external links. +#man_show_urls = False + + +# -- Options for Texinfo output ---------------------------------------------- + +# Grouping the document tree into Texinfo files. List of tuples +# (source start file, target name, title, author, +# dir menu entry, description, category) +texinfo_documents = [ + ('index', 'MistralClient', u'Mistral Client Documentation', + u'OpenStack Foundation', 'MistralClient', + 'One line description of project.', 'Miscellaneous'), +] + +# Documents to append as an appendix to all manuals. +#texinfo_appendices = [] + +# If false, no module index is generated. +#texinfo_domain_indices = True + +# How to display URL addresses: 'footnote', 'no', or 'inline'. +#texinfo_show_urls = 'footnote' diff --git a/doc/source/index.rst b/doc/source/index.rst new file mode 100644 index 00000000..52ae6c9a --- /dev/null +++ b/doc/source/index.rst @@ -0,0 +1,22 @@ +.. Mistral Client documentation master file, created by + sphinx-quickstart on Fri Nov 1 02:06:28 2013. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +Welcome to Mistral Client documentation! +=================================== + +Contents: + +.. toctree:: + :maxdepth: 2 + + + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` + diff --git a/mistralclient/__init__.py b/mistralclient/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/mistralclient/api/__init__.py b/mistralclient/api/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/mistralclient/api/base.py b/mistralclient/api/base.py new file mode 100644 index 00000000..fb4b92b4 --- /dev/null +++ b/mistralclient/api/base.py @@ -0,0 +1,153 @@ +# -*- coding: utf-8 -*- +# +# Copyright 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 json +import logging + +LOG = logging.getLogger(__name__) + + +class Resource(object): + resource_name = 'Something' + defaults = {} + + def __init__(self, manager, data): + self.manager = manager + self._data = data + self._set_defaults() + self._set_attributes() + + def _set_defaults(self): + for k, v in self.defaults.iteritems(): + if k not in self._data: + self._data[k] = v + + def _set_attributes(self): + for k, v in self._data.iteritems(): + try: + setattr(self, k, v) + except AttributeError: + # In this case we already defined the attribute on the class + pass + + def __str__(self): + vals = ", ".join(["%s='%s'" % (n, v) + for n, v in self._data.iteritems()]) + return "%s [%s]" % (self.resource_name, vals) + + +def _check_items(obj, searches): + try: + return all(getattr(obj, attr) == value for (attr, value) in searches) + except AttributeError: + return False + + +def extract_json(response, response_key): + if response_key is not None: + return get_json(response)[response_key] + else: + return get_json(response) + + +class ResourceManager(object): + resource_class = None + + def __init__(self, client): + self.client = client + + def find(self, **kwargs): + return [i for i in self.list() if _check_items(i, kwargs.items())] + + def _ensure_not_empty(self, **kwargs): + for name, value in kwargs.iteritems(): + if value is None or (isinstance(value, str) and len(value) == 0): + raise APIException('%s is missing field "%s"' % + (self.resource_class.__name__, name)) + + def _copy_if_defined(self, data, **kwargs): + for name, value in kwargs.iteritems(): + if value is not None: + data[name] = value + + def _create(self, url, data, response_key=None, dump_json=True): + if dump_json: + data = json.dumps(data) + + resp = self.client.http_client.post(url, data) + + if resp.status_code != 201: + self._raise_api_exception(resp) + + return self.resource_class(self, extract_json(resp, response_key)) + + def _update(self, url, data, response_key=None, dump_json=True): + if dump_json: + data = json.dumps(data) + + resp = self.client.http_client.put(url, data) + + if resp.status_code != 200: + self._raise_api_exception(resp) + + return self.resource_class(self, extract_json(resp, response_key)) + + def _list(self, url, response_key=None): + resp = self.client.http_client.get(url) + + if resp.status_code == 200: + return [self.resource_class(self, resource_data) + for resource_data in extract_json(resp, response_key)] + else: + self._raise_api_exception(resp) + + def _get(self, url, response_key=None): + resp = self.client.http_client.get(url) + + if resp.status_code == 200: + return self.resource_class(self, extract_json(resp, response_key)) + else: + self._raise_api_exception(resp) + + def _delete(self, url): + resp = self.client.http_client.delete(url) + + if resp.status_code != 204: + self._raise_api_exception(resp) + + def _plurify_resource_name(self): + return self.resource_class.resource_name + 's' + + def _raise_api_exception(self, resp): + error_data = get_json(resp) + raise APIException(error_data["faultstring"]) + + +def get_json(response): + """This method provided backward compatibility with old versions + of requests library + + """ + json_field_or_function = getattr(response, 'json', None) + + if callable(json_field_or_function): + return response.json() + else: + return json.loads(response.content) + + +class APIException(Exception): + pass diff --git a/mistralclient/api/client.py b/mistralclient/api/client.py new file mode 100644 index 00000000..3f1d0e93 --- /dev/null +++ b/mistralclient/api/client.py @@ -0,0 +1,98 @@ +# -*- coding: utf-8 -*- +# +# Copyright 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 six + +from keystoneclient.v3 import client as keystone_client + +from mistralclient.api import httpclient +from mistralclient.api import workbooks +from mistralclient.api import executions +from mistralclient.api import tasks +from mistralclient.api import listeners + + +class Client(object): + def __init__(self, mistral_url=None, username=None, api_key=None, + project_name=None, auth_url=None, project_id=None, + endpoint_type='publicURL', service_type='workflow', + input_auth_token=None): + + (mistral_url, + token, + project_id, + user_id) = self.authenticate(mistral_url, username, + api_key, project_name, + auth_url, project_id, + endpoint_type, service_type, + input_auth_token) + + self.http_client = httpclient.HTTPClient(mistral_url, + token, + project_id, + user_id) + # Create all resource managers. + self.workbooks = workbooks.WorkbookManager(self) + self.executions = executions.ExecutionManager(self) + self.tasks = tasks.TaskManager(self) + self.listeners = listeners.ListenerManager(self) + + def authenticate(self, mistral_url=None, username=None, api_key=None, + project_name=None, auth_url=None, project_id=None, + endpoint_type='publicURL', service_type='workflow', + input_auth_token=None): + if mistral_url and not isinstance(mistral_url, six.string_types): + raise RuntimeError('Mistral url should be string') + if (isinstance(project_name, six.string_types) or + isinstance(project_id, six.string_types)): + if project_name and project_id: + raise RuntimeError('Only project name or ' + 'project id should be set') + + if "v2.0" in auth_url: + raise RuntimeError('Mistral supports only v3 ' + 'keystone API.') + + keystone = keystone_client.Client(username=username, + password=api_key, + token=input_auth_token, + tenant_id=project_id, + tenant_name=project_name, + auth_url=auth_url) + + keystone.authenticate() + token = keystone.auth_token + user_id = keystone.user_id + if project_name and not project_id: + if keystone.tenants.find(name=project_name): + project_id = str(keystone.tenants.find( + name=project_name).id) + else: + raise RuntimeError('Project name or project id should' + ' not be empty and should be string') + + if not mistral_url: + catalog = keystone.service_catalog.get_endpoints(service_type) + if service_type in catalog: + for e_type, endpoint in catalog.get[service_type][0].items(): + if str(e_type).lower() == str(endpoint_type).lower(): + mistral_url = endpoint + break + + if not mistral_url: + mistral_url = "http://localhost:8989/v1" + + return mistral_url, token, project_id, user_id diff --git a/mistralclient/api/executions.py b/mistralclient/api/executions.py new file mode 100644 index 00000000..7499489a --- /dev/null +++ b/mistralclient/api/executions.py @@ -0,0 +1,65 @@ +# -*- coding: utf-8 -*- +# +# Copyright 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 mistralclient.api import base + + +class Execution(base.Resource): + resource_name = 'Execution' + + +class ExecutionManager(base.ResourceManager): + resource_class = Execution + + def create(self, workbook_name, target_task): + self._ensure_not_empty(workbook_name=workbook_name, + target_task=target_task) + + data = { + 'workbook_name': workbook_name, + 'target_task': target_task + } + + return self._create('/workbooks/%s/executions' % workbook_name, data) + + def update(self, workbook_name, id, state): + self._ensure_not_empty(workbook_name=workbook_name, id=id, + state=state) + + data = { + 'workbook_name': workbook_name, + 'id': id, + 'state': state + } + + return self._update('/workbooks/%s/executions/%s' % + (workbook_name, id), data) + + def list(self, workbook_name): + self._ensure_not_empty(workbook_name=workbook_name) + + return self._list('/workbooks/%s/executions' % workbook_name, + 'executions') + + def get(self, workbook_name, id): + self._ensure_not_empty(workbook_name=workbook_name, id=id) + + return self._get('/workbooks/%s/executions/%s' % (workbook_name, id)) + + def delete(self, workbook_name, id): + self._ensure_not_empty(workbook_name=workbook_name, id=id) + + self._delete('/workbooks/%s/executions/%s' % (workbook_name, id)) diff --git a/mistralclient/api/httpclient.py b/mistralclient/api/httpclient.py new file mode 100644 index 00000000..3a9bd5ec --- /dev/null +++ b/mistralclient/api/httpclient.py @@ -0,0 +1,62 @@ +# -*- coding: utf-8 -*- +# +# Copyright 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 requests + + +class HTTPClient(object): + def __init__(self, base_url, token, project_id, user_id): + self.base_url = base_url + self.token = token + self.project_id = project_id + self.user_id = user_id + + def get(self, url, headers=None): + headers = self._update_headers(headers) + + return requests.get(self.base_url + url, headers=headers) + + def post(self, url, body, headers=None): + headers = self._update_headers(headers) + content_type = headers.get('content-type', 'application/json') + headers['content-type'] = content_type + + return requests.post(self.base_url + url, body, headers=headers) + + def put(self, url, body, headers=None): + headers = self._update_headers(headers) + content_type = headers.get('content-type', 'application/json') + headers['content-type'] = content_type + + return requests.put(self.base_url + url, body, headers=headers) + + def delete(self, url, headers=None): + headers = self._update_headers(headers) + + return requests.delete(self.base_url + url, headers=headers) + + def _update_headers(self, headers): + if not headers: + headers = {} + token = headers.get('x-auth-token', self.token) + headers['x-auth-token'] = token + + project_id = headers.get('X-Project-Id', self.project_id) + headers['X-Project-Id'] = project_id + + user_id = headers.get('X-User-Id', self.user_id) + headers['X-User-Id'] = user_id + return headers diff --git a/mistralclient/api/listeners.py b/mistralclient/api/listeners.py new file mode 100644 index 00000000..92480c7b --- /dev/null +++ b/mistralclient/api/listeners.py @@ -0,0 +1,71 @@ +# -*- coding: utf-8 -*- +# +# Copyright 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 mistralclient.api import base + + +class Listener(base.Resource): + resource_name = 'Listener' + + +class ListenerManager(base.ResourceManager): + resource_class = Listener + + def create(self, workbook_name, webhook, description=None, events=None): + # TODO(rakhmerov): need to describe what events is (data type) + + self._ensure_not_empty(workbook_name=workbook_name, + webhook=webhook) + data = { + 'workbook_name': workbook_name, + 'description': description, + 'webhook': webhook, + 'events': events + } + + return self._create('/workbooks/%s/listeners' % workbook_name, data) + + def update(self, workbook_name, id, webhook=None, description=None, + events=None): + #TODO: need to describe what events is + self._ensure_not_empty(workbook_name=workbook_name, id=id) + + data = { + 'id': id, + 'workbook_name': workbook_name, + 'description': description, + 'webhook': webhook, + 'events': events + } + + return self._update('/workbooks/%s/listeners/%s' % + (workbook_name, id), data) + + def list(self, workbook_name): + self._ensure_not_empty(workbook_name=workbook_name) + + return self._list('/workbooks/%s/listeners' % workbook_name, + 'listeners') + + def get(self, workbook_name, id): + self._ensure_not_empty(workbook_name=workbook_name, id=id) + + return self._get('/workbooks/%s/listeners/%s' % (workbook_name, id)) + + def delete(self, workbook_name, id): + self._ensure_not_empty(workbook_name=workbook_name, id=id) + + self._delete('/workbooks/%s/listeners/%s' % (workbook_name, id)) diff --git a/mistralclient/api/tasks.py b/mistralclient/api/tasks.py new file mode 100644 index 00000000..af7ec49b --- /dev/null +++ b/mistralclient/api/tasks.py @@ -0,0 +1,57 @@ +# -*- coding: utf-8 -*- +# +# Copyright 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 mistralclient.api import base + + +class Task(base.Resource): + resource_name = 'Task' + + +class TaskManager(base.ResourceManager): + resource_class = Task + + def update(self, workbook_name, execution_id, id, state): + self._ensure_not_empty(workbook_name=workbook_name, + execution_id=execution_id, + id=id, + state=state) + + data = { + 'workbook_name': workbook_name, + 'execution_id': execution_id, + 'id': id, + 'state': state + } + + return self._update('/workbooks/%s/executions/%s/tasks/%s' % + (workbook_name, execution_id, id), data) + + def list(self, workbook_name, execution_id): + self._ensure_not_empty(workbook_name=workbook_name, + execution_id=execution_id) + + return self._list('/workbooks/%s/executions/%s/tasks' % + (workbook_name, execution_id), + 'tasks') + + def get(self, workbook_name, execution_id, id): + self._ensure_not_empty(workbook_name=workbook_name, + execution_id=execution_id, + id=id) + + return self._get('/workbooks/%s/executions/%s/tasks/%s' % + (workbook_name, execution_id, id)) diff --git a/mistralclient/api/workbooks.py b/mistralclient/api/workbooks.py new file mode 100644 index 00000000..0be0356d --- /dev/null +++ b/mistralclient/api/workbooks.py @@ -0,0 +1,73 @@ +# -*- coding: utf-8 -*- +# +# Copyright 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 mistralclient.api import base + + +class Workbook(base.Resource): + resource_name = 'Workbook' + + +class WorkbookManager(base.ResourceManager): + resource_class = Workbook + + def create(self, name, description=None, tags=None): + self._ensure_not_empty(name=name) + + data = { + 'name': name, + 'description': description, + 'tags': tags, + } + + return self._create('/workbooks', data) + + def update(self, name, description=None, tags=None): + self._ensure_not_empty(name=name) + + data = { + 'name': name, + 'description': description, + 'tags': tags, + } + + return self._update('/workbooks', data) + + def list(self): + return self._list('/workbooks', 'workbooks') + + def get(self, name): + self._ensure_not_empty(name=name) + + return self._get('/workbooks/%s' % name) + + def delete(self, name): + self._ensure_not_empty(name=name) + + self._delete('/workbooks/%s' % name) + + def upload_definition(self, name, text): + self._ensure_not_empty(name=name) + + self.client.http_client.put('/workbooks/%s/definition' % name, + text, + headers={'content-type': 'text/plain'}) + + def get_definition(self, name): + self._ensure_not_empty(name=name) + + return self.client.http_client.get('/workbooks/%s/definition' + % name).content diff --git a/mistralclient/tests/__init__.py b/mistralclient/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/mistralclient/tests/base.py b/mistralclient/tests/base.py new file mode 100644 index 00000000..6090535d --- /dev/null +++ b/mistralclient/tests/base.py @@ -0,0 +1,61 @@ +# -*- coding: utf-8 -*- +# +# Copyright 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 unittest2 +import mock + +from mistralclient.api import client + + +class FakeResponse(object): + """Fake response for testing Mistral Client.""" + + def __init__(self, status_code, json_values={}, content=None): + self.status_code = status_code + self.json_values = json_values + self.content = content + + def json(self): + return self.json_values + + +class BaseClientTest(unittest2.TestCase): + @mock.patch('keystoneclient.v3.client.Client') + def setUp(self, keystone): + keystone.return_value = mock.Mock() + self._client = client.Client(project_name="test", + auth_url="v3.0", + mistral_url="test") + self.workbooks = self._client.workbooks + self.executions = self._client.executions + self.tasks = self._client.tasks + self.listeners = self._client.listeners + + def mock_http_get(self, json, status_code=200): + self._client.http_client.get = \ + mock.MagicMock(return_value=FakeResponse(status_code, json)) + + def mock_http_post(self, json, status_code=201): + self._client.http_client.post = \ + mock.MagicMock(return_value=FakeResponse(status_code, json)) + + def mock_http_put(self, json, status_code=200): + self._client.http_client.put = \ + mock.MagicMock(return_value=FakeResponse(status_code, json)) + + def mock_http_delete(self, status_code=204): + self._client.http_client.delete = \ + mock.MagicMock(return_value=FakeResponse(status_code)) diff --git a/mistralclient/tests/test_executions.py b/mistralclient/tests/test_executions.py new file mode 100644 index 00000000..579b672c --- /dev/null +++ b/mistralclient/tests/test_executions.py @@ -0,0 +1,86 @@ +# -*- coding: utf-8 -*- +# +# Copyright 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 mistralclient.tests import base + +# TODO: later we need additional tests verifying all the errors etc. + +EXECS = [ + { + 'id': "123", + 'workbook_name': "my_workbook", + 'target_task': 'my_task', + 'state': 'RUNNING' + } +] + + +class TestExecutions(base.BaseClientTest): + + def test_create(self): + self.mock_http_post(json=EXECS[0]) + + wb = self.executions.create(EXECS[0]['workbook_name'], + EXECS[0]['target_task']) + + self.assertIsNotNone(wb) + self.assertEqual(EXECS[0]['id'], wb.id) + self.assertEqual(EXECS[0]['workbook_name'], wb.workbook_name) + self.assertEqual(EXECS[0]['target_task'], wb.target_task) + self.assertEqual(EXECS[0]['state'], wb.state) + + def test_update(self): + self.mock_http_put(json=EXECS[0]) + + ex = self.executions.update(EXECS[0]['workbook_name'], + EXECS[0]['id'], + EXECS[0]['state']) + + self.assertIsNotNone(ex) + self.assertEqual(EXECS[0]['id'], ex.id) + self.assertEqual(EXECS[0]['workbook_name'], ex.workbook_name) + self.assertEqual(EXECS[0]['target_task'], ex.target_task) + self.assertEqual(EXECS[0]['state'], ex.state) + + def test_list(self): + self.mock_http_get(json={'executions': EXECS}) + + executions = self.executions.list(EXECS[0]['workbook_name']) + + self.assertEqual(1, len(executions)) + + ex = executions[0] + + self.assertEqual(EXECS[0]['id'], ex.id) + self.assertEqual(EXECS[0]['workbook_name'], ex.workbook_name) + self.assertEqual(EXECS[0]['target_task'], ex.target_task) + self.assertEqual(EXECS[0]['state'], ex.state) + + def test_get(self): + self.mock_http_get(json=EXECS[0]) + + ex = self.executions.get(EXECS[0]['workbook_name'], EXECS[0]['id']) + + self.assertEqual(EXECS[0]['id'], ex.id) + self.assertEqual(EXECS[0]['workbook_name'], ex.workbook_name) + self.assertEqual(EXECS[0]['target_task'], ex.target_task) + self.assertEqual(EXECS[0]['state'], ex.state) + + def test_delete(self): + self.mock_http_delete(status_code=204) + + # Just make sure it doesn't throw any exceptions. + self.executions.delete(EXECS[0]['workbook_name'], EXECS[0]['id']) diff --git a/mistralclient/tests/test_listeners.py b/mistralclient/tests/test_listeners.py new file mode 100644 index 00000000..af5f0053 --- /dev/null +++ b/mistralclient/tests/test_listeners.py @@ -0,0 +1,88 @@ +# -*- coding: utf-8 -*- +# +# Copyright 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 mistralclient.tests import base + +# TODO: later we need additional tests verifying all the errors etc. + +LISTENERS = [ + { + 'id': "1", + 'workbook_name': "my_workbook", + 'description': "My cool Mistral workbook", + 'webhook': "http://my.website.org" + } +] + + +class TestListeners(base.BaseClientTest): + def test_create(self): + self.mock_http_post(json=LISTENERS[0]) + + lsnr = self.listeners.create(LISTENERS[0]['workbook_name'], + LISTENERS[0]['webhook'], + LISTENERS[0]['description']) + + self.assertIsNotNone(lsnr) + self.assertEqual(LISTENERS[0]['id'], lsnr.id) + self.assertEqual(LISTENERS[0]['workbook_name'], lsnr.workbook_name) + self.assertEqual(LISTENERS[0]['webhook'], lsnr.webhook) + self.assertEqual(LISTENERS[0]['description'], lsnr.description) + + def test_update(self): + self.mock_http_put(json=LISTENERS[0]) + + lsnr = self.listeners.update(LISTENERS[0]['workbook_name'], + LISTENERS[0]['webhook'], + LISTENERS[0]['description']) + + self.assertIsNotNone(lsnr) + self.assertEqual(LISTENERS[0]['id'], lsnr.id) + self.assertEqual(LISTENERS[0]['workbook_name'], lsnr.workbook_name) + self.assertEqual(LISTENERS[0]['webhook'], lsnr.webhook) + self.assertEqual(LISTENERS[0]['description'], lsnr.description) + + def test_list(self): + self.mock_http_get(json={'listeners': LISTENERS}) + + listeners = self.listeners.list(LISTENERS[0]['workbook_name']) + + self.assertEqual(1, len(listeners)) + + lsnr = listeners[0] + + self.assertEqual(LISTENERS[0]['id'], lsnr.id) + self.assertEqual(LISTENERS[0]['workbook_name'], lsnr.workbook_name) + self.assertEqual(LISTENERS[0]['webhook'], lsnr.webhook) + self.assertEqual(LISTENERS[0]['description'], lsnr.description) + + def test_get(self): + self.mock_http_get(json=LISTENERS[0]) + + lsnr = self.listeners.get(LISTENERS[0]['workbook_name'], + LISTENERS[0]['id']) + + self.assertEqual(LISTENERS[0]['id'], lsnr.id) + self.assertEqual(LISTENERS[0]['workbook_name'], lsnr.workbook_name) + self.assertEqual(LISTENERS[0]['webhook'], lsnr.webhook) + self.assertEqual(LISTENERS[0]['description'], lsnr.description) + + def test_delete(self): + self.mock_http_delete(status_code=204) + + # Just make sure it doesn't throw any exceptions. + self.listeners.delete(LISTENERS[0]['workbook_name'], + LISTENERS[0]['id']) diff --git a/mistralclient/tests/test_tasks.py b/mistralclient/tests/test_tasks.py new file mode 100644 index 00000000..92ee35b1 --- /dev/null +++ b/mistralclient/tests/test_tasks.py @@ -0,0 +1,84 @@ +# -*- coding: utf-8 -*- +# +# Copyright 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 mistralclient.tests import base + +# TODO: later we need additional tests verifying all the errors etc. + +TASKS = [ + { + 'id': "1", + 'workbook_name': "my_workbook", + 'execution_id': '123', + 'name': 'my_task', + 'description': 'My cool task', + 'action': 'my_action', + 'state': 'RUNNING', + 'tags': ['deployment', 'demo'] + } +] + + +class TestTasks(base.BaseClientTest): + def test_update(self): + self.mock_http_put(json=TASKS[0]) + + task = self.tasks.update(TASKS[0]['workbook_name'], + TASKS[0]['execution_id'], + TASKS[0]['id'], + TASKS[0]['state']) + + self.assertIsNotNone(task) + self.assertEqual(TASKS[0]['id'], task.id) + self.assertEqual(TASKS[0]['workbook_name'], task.workbook_name) + self.assertEqual(TASKS[0]['execution_id'], task.execution_id) + self.assertEqual(TASKS[0]['description'], task.description) + self.assertEqual(TASKS[0]['action'], task.action) + self.assertEqual(TASKS[0]['state'], task.state) + self.assertEqual(TASKS[0]['tags'], task.tags) + + def test_list(self): + self.mock_http_get(json={'tasks': TASKS}) + + tasks = self.tasks.list(TASKS[0]['workbook_name'], + TASKS[0]['execution_id']) + + self.assertEqual(1, len(tasks)) + + task = tasks[0] + + self.assertEqual(TASKS[0]['id'], task.id) + self.assertEqual(TASKS[0]['workbook_name'], task.workbook_name) + self.assertEqual(TASKS[0]['execution_id'], task.execution_id) + self.assertEqual(TASKS[0]['description'], task.description) + self.assertEqual(TASKS[0]['action'], task.action) + self.assertEqual(TASKS[0]['state'], task.state) + self.assertEqual(TASKS[0]['tags'], task.tags) + + def test_get(self): + self.mock_http_get(json=TASKS[0]) + + task = self.tasks.get(TASKS[0]['workbook_name'], + TASKS[0]['execution_id'], + TASKS[0]['id']) + + self.assertEqual(TASKS[0]['id'], task.id) + self.assertEqual(TASKS[0]['workbook_name'], task.workbook_name) + self.assertEqual(TASKS[0]['execution_id'], task.execution_id) + self.assertEqual(TASKS[0]['description'], task.description) + self.assertEqual(TASKS[0]['action'], task.action) + self.assertEqual(TASKS[0]['state'], task.state) + self.assertEqual(TASKS[0]['tags'], task.tags) diff --git a/mistralclient/tests/test_workbooks.py b/mistralclient/tests/test_workbooks.py new file mode 100644 index 00000000..15ac26a0 --- /dev/null +++ b/mistralclient/tests/test_workbooks.py @@ -0,0 +1,122 @@ +# -*- coding: utf-8 -*- +# +# Copyright 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 mock +from mistralclient.tests import base + +# TODO: later we need additional tests verifying all the errors etc. + +WORKBOOKS = [ + { + 'name': "my_workbook", + 'description': "My cool Mistral workbook", + 'tags': ['deployment', 'demo'] + } +] + +WB_DEF = """ +Service: + name: my_service + type: REST + parameters: + baseUrl: http://my.service.org + actions: + action1: + parameters: + url: servers + method: POST + task-parameters: + param1: + optional: false + param2: + optional: false +Workflow: + tasks: + task1: + action: my_service:create-vm + parameters: + param1: 1234 + param2: 42 +""" + + +class TestWorkbooks(base.BaseClientTest): + def test_create(self): + self.mock_http_post(json=WORKBOOKS[0]) + + wb = self.workbooks.create(WORKBOOKS[0]['name'], + WORKBOOKS[0]['description'], + WORKBOOKS[0]['tags']) + + self.assertIsNotNone(wb) + self.assertEqual(WORKBOOKS[0]['name'], wb.name) + self.assertEqual(WORKBOOKS[0]['description'], wb.description) + self.assertEqual(WORKBOOKS[0]['tags'], wb.tags) + + def test_update(self): + self.mock_http_put(json=WORKBOOKS[0]) + + wb = self.workbooks.update(WORKBOOKS[0]['name'], + WORKBOOKS[0]['description'], + WORKBOOKS[0]['tags']) + + self.assertIsNotNone(wb) + self.assertEqual(WORKBOOKS[0]['name'], wb.name) + self.assertEqual(WORKBOOKS[0]['description'], wb.description) + self.assertEqual(WORKBOOKS[0]['tags'], wb.tags) + + def test_list(self): + self.mock_http_get(json={'workbooks': WORKBOOKS}) + + workbooks = self.workbooks.list() + + self.assertEqual(1, len(workbooks)) + + wb = workbooks[0] + + self.assertEqual(WORKBOOKS[0]['name'], wb.name) + self.assertEqual(WORKBOOKS[0]['description'], wb.description) + self.assertEqual(WORKBOOKS[0]['tags'], wb.tags) + + def test_get(self): + self.mock_http_get(json=WORKBOOKS[0]) + + wb = self.workbooks.get(WORKBOOKS[0]['name']) + + self.assertIsNotNone(wb) + self.assertEqual(WORKBOOKS[0]['name'], wb.name) + self.assertEqual(WORKBOOKS[0]['description'], wb.description) + self.assertEqual(WORKBOOKS[0]['tags'], wb.tags) + + def test_delete(self): + self.mock_http_delete(status_code=204) + + # Just make sure it doesn't throw any exceptions. + self.workbooks.delete(WORKBOOKS[0]['name']) + + def test_upload_definition(self): + self.mock_http_put(None, status_code=200) + + # Just make sure it doesn't throw any exceptions. + self.workbooks.upload_definition("my_workbook", WB_DEF) + + def test_get_definition(self): + self._client.http_client.get =\ + mock.MagicMock(return_value=base.FakeResponse(200, None, WB_DEF)) + + text = self.workbooks.get_definition("my_workbook") + + self.assertEqual(WB_DEF, text) diff --git a/openstack-common.conf b/openstack-common.conf new file mode 100644 index 00000000..29d311bd --- /dev/null +++ b/openstack-common.conf @@ -0,0 +1,7 @@ +[DEFAULT] + +# The list of modules to copy from oslo-incubator.git +# TODO(rakhmerov): We'll need to use apiclient later. + +# The base module to hold the copy of openstack.common +base=mistralclient diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 00000000..3896e5f7 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +pbr>=0.5.21,<1.0 +requests +python-keystoneclient>=0.3.2 \ No newline at end of file diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 00000000..fa8d1219 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,27 @@ +[metadata] +name = python-mistralclient +version = 0.01 +summary = Mistral Client Library +description-file = README.rd +#license = Apache Software License +classifiers = + Programming Language :: Python + Programming Language :: Python :: 2 + Programming Language :: Python :: 2.7 + Environment :: OpenStack + Intended Audience :: Information Technology + Intended Audience :: System Administrators + #License :: OSI Approved :: Apache Software License + Operating System :: POSIX :: Linux +author = OpenStack +author-email = openstack-dev@lists.openstack.org + +[files] +packages = + mistralclient + +[nosetests] +cover-package = mistralclient + +[extract_messages] +keywords = _ gettext ngettext l_ lazy_gettext diff --git a/setup.py b/setup.py new file mode 100644 index 00000000..911b87e6 --- /dev/null +++ b/setup.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# Copyright 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. + +# THIS FILE IS MANAGED BY THE GLOBAL REQUIREMENTS REPO - DO NOT EDIT +import setuptools + +setuptools.setup( + name="python-mistralclient") diff --git a/test-requirements.txt b/test-requirements.txt new file mode 100644 index 00000000..e73a995c --- /dev/null +++ b/test-requirements.txt @@ -0,0 +1,11 @@ +pep8==1.4.5 +pyflakes>=0.7.2,<0.7.4 +flake8==2.0 +pylint==0.25.2 +sphinx>=1.1.2 +unittest2 +fixtures>=0.3.14 +mock>=1.0 +nose +testtools>=0.9.32 +lockfile>=0.9.1 diff --git a/tools/config/generate_sample.sh b/tools/config/generate_sample.sh new file mode 100755 index 00000000..29fb346b --- /dev/null +++ b/tools/config/generate_sample.sh @@ -0,0 +1,99 @@ +#!/usr/bin/env bash + +print_hint() { + echo "Try \`${0##*/} --help' for more information." >&2 +} + +PARSED_OPTIONS=$(getopt -n "${0##*/}" -o hb:p:o: \ + --long help,base-dir:,package-name:,output-dir: -- "$@") + +if [ $? != 0 ] ; then print_hint ; exit 1 ; fi + +eval set -- "$PARSED_OPTIONS" + +while true; do + case "$1" in + -h|--help) + echo "${0##*/} [options]" + echo "" + echo "options:" + echo "-h, --help show brief help" + echo "-b, --base-dir=DIR project base directory" + echo "-p, --package-name=NAME project package name" + echo "-o, --output-dir=DIR file output directory" + exit 0 + ;; + -b|--base-dir) + shift + BASEDIR=`echo $1 | sed -e 's/\/*$//g'` + shift + ;; + -p|--package-name) + shift + PACKAGENAME=`echo $1` + shift + ;; + -o|--output-dir) + shift + OUTPUTDIR=`echo $1 | sed -e 's/\/*$//g'` + shift + ;; + --) + break + ;; + esac +done + +BASEDIR=${BASEDIR:-`pwd`} +if ! [ -d $BASEDIR ] +then + echo "${0##*/}: missing project base directory" >&2 ; print_hint ; exit 1 +elif [[ $BASEDIR != /* ]] +then + BASEDIR=$(cd "$BASEDIR" && pwd) +fi + +PACKAGENAME=${PACKAGENAME:-${BASEDIR##*/}} +TARGETDIR=$BASEDIR/$PACKAGENAME +if ! [ -d $TARGETDIR ] +then + echo "${0##*/}: invalid project package name" >&2 ; print_hint ; exit 1 +fi + +OUTPUTDIR=${OUTPUTDIR:-$BASEDIR/etc} +# NOTE(bnemec): Some projects put their sample config in etc/, +# some in etc/$PACKAGENAME/ +if [ -d $OUTPUTDIR/$PACKAGENAME ] +then + OUTPUTDIR=$OUTPUTDIR/$PACKAGENAME +elif ! [ -d $OUTPUTDIR ] +then + echo "${0##*/}: cannot access \`$OUTPUTDIR': No such file or directory" >&2 + exit 1 +fi + +BASEDIRESC=`echo $BASEDIR | sed -e 's/\//\\\\\//g'` +find $TARGETDIR -type f -name "*.pyc" -delete +FILES=$(find $TARGETDIR -type f -name "*.py" ! -path "*/tests/*" \ + -exec grep -l "Opt(" {} + | sed -e "s/^$BASEDIRESC\///g" | sort -u) + +EXTRA_MODULES_FILE="`dirname $0`/oslo.config.generator.rc" +if test -r "$EXTRA_MODULES_FILE" +then + source "$EXTRA_MODULES_FILE" +fi + +export EVENTLET_NO_GREENDNS=yes + +OS_VARS=$(set | sed -n '/^OS_/s/=[^=]*$//gp' | xargs) +[ "$OS_VARS" ] && eval "unset \$OS_VARS" +DEFAULT_MODULEPATH=mistral.openstack.common.config.generator +MODULEPATH=${MODULEPATH:-$DEFAULT_MODULEPATH} +OUTPUTFILE=$OUTPUTDIR/$PACKAGENAME.conf.sample +python -m $MODULEPATH $FILES > $OUTPUTFILE + +# Hook to allow projects to append custom config file snippets +CONCAT_FILES=$(ls $BASEDIR/tools/config/*.conf.sample 2>/dev/null) +for CONCAT_FILE in $CONCAT_FILES; do + cat $CONCAT_FILE >> $OUTPUTFILE +done diff --git a/tools/install_venv b/tools/install_venv new file mode 100755 index 00000000..6a4aea2a --- /dev/null +++ b/tools/install_venv @@ -0,0 +1,3 @@ +#!/bin/sh + +tox -vvv -evenv -- python --version diff --git a/tools/install_venv.py b/tools/install_venv.py new file mode 100644 index 00000000..0011a8be --- /dev/null +++ b/tools/install_venv.py @@ -0,0 +1,77 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2010 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Copyright 2010 OpenStack Foundation +# Copyright 2013 IBM Corp. +# Copyright (c) 2013 Hewlett-Packard Development Company, L.P. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import ConfigParser +import os +import sys + +import install_venv_common as install_venv # flake8: noqa + + +def print_help(project, venv, root): + help = """ + %(project)s development environment setup is complete. + + %(project)s development uses virtualenv to track and manage Python + dependencies while in development and testing. + + To activate the %(project)s virtualenv for the extent of your current + shell session you can run: + + $ source %(venv)s/bin/activate + + Or, if you prefer, you can run commands in the virtualenv on a case by + case basis by running: + + $ %(root)s/tools/with_venv.sh + """ + print help % dict(project=project, venv=venv, root=root) + + +def main(argv): + root = os.path.dirname(os.path.dirname(os.path.realpath(__file__))) + + if os.environ.get('tools_path'): + root = os.environ['tools_path'] + venv = os.path.join(root, '.venv') + if os.environ.get('venv'): + venv = os.environ['venv'] + + pip_requires = os.path.join(root, 'requirements.txt') + test_requires = os.path.join(root, 'test-requirements.txt') + py_version = "python%s.%s" % (sys.version_info[0], sys.version_info[1]) + setup_cfg = ConfigParser.ConfigParser() + setup_cfg.read('setup.cfg') + project = setup_cfg.get('metadata', 'name') + + install = install_venv.InstallVenv( + root, venv, pip_requires, test_requires, py_version, project) + options = install.parse_args(argv) + install.check_python_version() + install.check_dependencies() + install.create_virtualenv(no_site_packages=options.no_site_packages) + install.install_dependencies() + install.post_process() + print_help(project, venv, root) + +if __name__ == '__main__': + main(sys.argv) diff --git a/tools/install_venv_common.py b/tools/install_venv_common.py new file mode 100644 index 00000000..f428c1e0 --- /dev/null +++ b/tools/install_venv_common.py @@ -0,0 +1,212 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2013 OpenStack Foundation +# Copyright 2013 IBM Corp. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +"""Provides methods needed by installation script for OpenStack development +virtual environments. + +Since this script is used to bootstrap a virtualenv from the system's Python +environment, it should be kept strictly compatible with Python 2.6. + +Synced in from openstack-common +""" + +from __future__ import print_function + +import optparse +import os +import subprocess +import sys + + +class InstallVenv(object): + + def __init__(self, root, venv, requirements, + test_requirements, py_version, + project): + self.root = root + self.venv = venv + self.requirements = requirements + self.test_requirements = test_requirements + self.py_version = py_version + self.project = project + + def die(self, message, *args): + print(message % args, file=sys.stderr) + sys.exit(1) + + def check_python_version(self): + if sys.version_info < (2, 6): + self.die("Need Python Version >= 2.6") + + def run_command_with_code(self, cmd, redirect_output=True, + check_exit_code=True): + """Runs a command in an out-of-process shell. + + Returns the output of that command. Working directory is self.root. + """ + if redirect_output: + stdout = subprocess.PIPE + else: + stdout = None + + proc = subprocess.Popen(cmd, cwd=self.root, stdout=stdout) + output = proc.communicate()[0] + if check_exit_code and proc.returncode != 0: + self.die('Command "%s" failed.\n%s', ' '.join(cmd), output) + return (output, proc.returncode) + + def run_command(self, cmd, redirect_output=True, check_exit_code=True): + return self.run_command_with_code(cmd, redirect_output, + check_exit_code)[0] + + def get_distro(self): + if (os.path.exists('/etc/fedora-release') or + os.path.exists('/etc/redhat-release')): + return Fedora( + self.root, self.venv, self.requirements, + self.test_requirements, self.py_version, self.project) + else: + return Distro( + self.root, self.venv, self.requirements, + self.test_requirements, self.py_version, self.project) + + def check_dependencies(self): + self.get_distro().install_virtualenv() + + def create_virtualenv(self, no_site_packages=True): + """Creates the virtual environment and installs PIP. + + Creates the virtual environment and installs PIP only into the + virtual environment. + """ + if not os.path.isdir(self.venv): + print('Creating venv...', end=' ') + if no_site_packages: + self.run_command(['virtualenv', '-q', '--no-site-packages', + self.venv]) + else: + self.run_command(['virtualenv', '-q', self.venv]) + print('done.') + else: + print("venv already exists...") + pass + + def pip_install(self, *args): + self.run_command(['tools/with_venv.sh', + 'pip', 'install', '--upgrade'] + list(args), + redirect_output=False) + + def install_dependencies(self): + print('Installing dependencies with pip (this can take a while)...') + + # First things first, make sure our venv has the latest pip and + # setuptools. + self.pip_install('pip>=1.3') + self.pip_install('setuptools') + + self.pip_install('-r', self.requirements) + self.pip_install('-r', self.test_requirements) + + def post_process(self): + self.get_distro().post_process() + + def parse_args(self, argv): + """Parses command-line arguments.""" + parser = optparse.OptionParser() + parser.add_option('-n', '--no-site-packages', + action='store_true', + help="Do not inherit packages from global Python " + "install") + return parser.parse_args(argv[1:])[0] + + +class Distro(InstallVenv): + + def check_cmd(self, cmd): + return bool(self.run_command(['which', cmd], + check_exit_code=False).strip()) + + def install_virtualenv(self): + if self.check_cmd('virtualenv'): + return + + if self.check_cmd('easy_install'): + print('Installing virtualenv via easy_install...', end=' ') + if self.run_command(['easy_install', 'virtualenv']): + print('Succeeded') + return + else: + print('Failed') + + self.die('ERROR: virtualenv not found.\n\n%s development' + ' requires virtualenv, please install it using your' + ' favorite package management tool' % self.project) + + def post_process(self): + """Any distribution-specific post-processing gets done here. + + In particular, this is useful for applying patches to code inside + the venv. + """ + pass + + +class Fedora(Distro): + """This covers all Fedora-based distributions. + + Includes: Fedora, RHEL, CentOS, Scientific Linux + """ + + def check_pkg(self, pkg): + return self.run_command_with_code(['rpm', '-q', pkg], + check_exit_code=False)[1] == 0 + + def apply_patch(self, originalfile, patchfile): + self.run_command(['patch', '-N', originalfile, patchfile], + check_exit_code=False) + + def install_virtualenv(self): + if self.check_cmd('virtualenv'): + return + + if not self.check_pkg('python-virtualenv'): + self.die("Please install 'python-virtualenv'.") + + super(Fedora, self).install_virtualenv() + + def post_process(self): + """Workaround for a bug in eventlet. + + This currently affects RHEL6.1, but the fix can safely be + applied to all RHEL and Fedora distributions. + + This can be removed when the fix is applied upstream. + + Nova: https://bugs.launchpad.net/nova/+bug/884915 + Upstream: https://bitbucket.org/eventlet/eventlet/issue/89 + RHEL: https://bugzilla.redhat.com/958868 + """ + + # Install "patch" program if it's not there + if not self.check_pkg('patch'): + self.die("Please install 'patch'.") + + # Apply the eventlet patch + self.apply_patch(os.path.join(self.venv, 'lib', self.py_version, + 'site-packages', + 'eventlet/green/subprocess.py'), + 'contrib/redhat-eventlet.patch') diff --git a/tools/lintstack.py b/tools/lintstack.py new file mode 100755 index 00000000..8b70c0db --- /dev/null +++ b/tools/lintstack.py @@ -0,0 +1,200 @@ +#!/usr/bin/env python +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright (c) 2012, AT&T Labs, Yun Mao +# 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. + +# Stolen from OpenStack Nova + +"""pylint error checking.""" + +import cStringIO as StringIO +import json +import re +import sys + +from pylint import lint +from pylint.reporters import text + +# Note(maoy): E1103 is error code related to partial type inference +ignore_codes = ["E1103"] +# Note(maoy): the error message is the pattern of E0202. It should be ignored +# for savanna.tests modules +ignore_messages = ["An attribute affected in savanna.tests"] +# We ignore all errors in openstack.common because it should be checked +# elsewhere. +ignore_modules = ["savanna/openstack/common/"] + +KNOWN_PYLINT_EXCEPTIONS_FILE = "tools/pylint_exceptions" + + +class LintOutput(object): + + _cached_filename = None + _cached_content = None + + def __init__(self, filename, lineno, line_content, code, message, + lintoutput): + self.filename = filename + self.lineno = lineno + self.line_content = line_content + self.code = code + self.message = message + self.lintoutput = lintoutput + + @classmethod + def from_line(cls, line): + m = re.search(r"(\S+):(\d+): \[(\S+)(, \S+)?] (.*)", line) + matched = m.groups() + filename, lineno, code, message = (matched[0], int(matched[1]), + matched[2], matched[-1]) + if cls._cached_filename != filename: + with open(filename) as f: + cls._cached_content = list(f.readlines()) + cls._cached_filename = filename + line_content = cls._cached_content[lineno - 1].rstrip() + return cls(filename, lineno, line_content, code, message, + line.rstrip()) + + @classmethod + def from_msg_to_dict(cls, msg): + """From the output of pylint msg, to a dict, where each key + is a unique error identifier, value is a list of LintOutput + """ + result = {} + for line in msg.splitlines(): + obj = cls.from_line(line) + if obj.is_ignored(): + continue + key = obj.key() + if key not in result: + result[key] = [] + result[key].append(obj) + return result + + def is_ignored(self): + if self.code in ignore_codes: + return True + if any(self.filename.startswith(name) for name in ignore_modules): + return True + if any(msg in self.message for msg in ignore_messages): + return True + return False + + def key(self): + if self.code in ["E1101", "E1103"]: + # These two types of errors are like Foo class has no member bar. + # We discard the source code so that the error will be ignored + # next time another Foo.bar is encountered. + return self.message, "" + return self.message, self.line_content.strip() + + def json(self): + return json.dumps(self.__dict__) + + def review_str(self): + return ("File %(filename)s\nLine %(lineno)d:%(line_content)s\n" + "%(code)s: %(message)s" % self.__dict__) + + +class ErrorKeys(object): + + @classmethod + def print_json(cls, errors, output=sys.stdout): + print >>output, "# automatically generated by tools/lintstack.py" + for i in sorted(errors.keys()): + print >>output, json.dumps(i) + + @classmethod + def from_file(cls, filename): + keys = set() + for line in open(filename): + if line and line[0] != "#": + d = json.loads(line) + keys.add(tuple(d)) + return keys + + +def run_pylint(): + buff = StringIO.StringIO() + reporter = text.ParseableTextReporter(output=buff) + args = ["--include-ids=y", "-E", "savanna"] + lint.Run(args, reporter=reporter, exit=False) + val = buff.getvalue() + buff.close() + return val + + +def generate_error_keys(msg=None): + print "Generating", KNOWN_PYLINT_EXCEPTIONS_FILE + if msg is None: + msg = run_pylint() + errors = LintOutput.from_msg_to_dict(msg) + with open(KNOWN_PYLINT_EXCEPTIONS_FILE, "w") as f: + ErrorKeys.print_json(errors, output=f) + + +def validate(newmsg=None): + print "Loading", KNOWN_PYLINT_EXCEPTIONS_FILE + known = ErrorKeys.from_file(KNOWN_PYLINT_EXCEPTIONS_FILE) + if newmsg is None: + print "Running pylint. Be patient..." + newmsg = run_pylint() + errors = LintOutput.from_msg_to_dict(newmsg) + + print "Unique errors reported by pylint: was %d, now %d." \ + % (len(known), len(errors)) + passed = True + for err_key, err_list in errors.items(): + for err in err_list: + if err_key not in known: + print err.lintoutput + print + passed = False + if passed: + print "Congrats! pylint check passed." + redundant = known - set(errors.keys()) + if redundant: + print "Extra credit: some known pylint exceptions disappeared." + for i in sorted(redundant): + print json.dumps(i) + print "Consider regenerating the exception file if you will." + else: + print ("Please fix the errors above. If you believe they are false" + " positives, run 'tools/lintstack.py generate' to overwrite.") + sys.exit(1) + + +def usage(): + print """Usage: tools/lintstack.py [generate|validate] + To generate pylint_exceptions file: tools/lintstack.py generate + To validate the current commit: tools/lintstack.py + """ + + +def main(): + option = "validate" + if len(sys.argv) > 1: + option = sys.argv[1] + if option == "generate": + generate_error_keys() + elif option == "validate": + validate() + else: + usage() + + +if __name__ == "__main__": + main() diff --git a/tools/lintstack.sh b/tools/lintstack.sh new file mode 100755 index 00000000..f2464b06 --- /dev/null +++ b/tools/lintstack.sh @@ -0,0 +1,61 @@ +#!/usr/bin/env bash + +# Copyright (c) 2012-2013, AT&T Labs, Yun Mao +# 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. + +# Stolen from OpenStack Nova + +# Use lintstack.py to compare pylint errors. +# We run pylint twice, once on HEAD, once on the code before the latest +# commit for review. +set -e +TOOLS_DIR=$(cd $(dirname "$0") && pwd) +# Get the current branch name. +GITHEAD=`git rev-parse --abbrev-ref HEAD` +if [[ "$GITHEAD" == "HEAD" ]]; then + # In detached head mode, get revision number instead + GITHEAD=`git rev-parse HEAD` + echo "Currently we are at commit $GITHEAD" +else + echo "Currently we are at branch $GITHEAD" +fi + +cp -f $TOOLS_DIR/lintstack.py $TOOLS_DIR/lintstack.head.py + +if git rev-parse HEAD^2 2>/dev/null; then + # The HEAD is a Merge commit. Here, the patch to review is + # HEAD^2, the master branch is at HEAD^1, and the patch was + # written based on HEAD^2~1. + PREV_COMMIT=`git rev-parse HEAD^2~1` + git checkout HEAD~1 + # The git merge is necessary for reviews with a series of patches. + # If not, this is a no-op so won't hurt either. + git merge $PREV_COMMIT +else + # The HEAD is not a merge commit. This won't happen on gerrit. + # Most likely you are running against your own patch locally. + # We assume the patch to examine is HEAD, and we compare it against + # HEAD~1 + git checkout HEAD~1 +fi + +# First generate tools/pylint_exceptions from HEAD~1 +$TOOLS_DIR/lintstack.head.py generate +# Then use that as a reference to compare against HEAD +git checkout $GITHEAD +$TOOLS_DIR/lintstack.head.py +echo "Check passed. FYI: the pylint exceptions are:" +cat $TOOLS_DIR/pylint_exceptions + diff --git a/tools/run_pep8 b/tools/run_pep8 new file mode 100755 index 00000000..256ec835 --- /dev/null +++ b/tools/run_pep8 @@ -0,0 +1,3 @@ +#!/bin/sh + +tox -epep8 diff --git a/tools/run_pylint b/tools/run_pylint new file mode 100755 index 00000000..c69b8d1f --- /dev/null +++ b/tools/run_pylint @@ -0,0 +1,3 @@ +#!/bin/sh + +tox -epylint diff --git a/tools/with_venv.sh b/tools/with_venv.sh new file mode 100755 index 00000000..c8d2940f --- /dev/null +++ b/tools/with_venv.sh @@ -0,0 +1,4 @@ +#!/bin/bash +TOOLS=`dirname $0` +VENV=$TOOLS/../.venv +source $VENV/bin/activate && $@ diff --git a/tox.ini b/tox.ini new file mode 100644 index 00000000..1a8f5fed --- /dev/null +++ b/tox.ini @@ -0,0 +1,42 @@ +[tox] +envlist = py26,py27,py33,pep8 +minversion = 1.6 +skipsdist = True + +[testenv] +usedevelop = True +install_command = pip install -U {opts} {packages} +setenv = + VIRTUAL_ENV={envdir} + NOSE_WITH_OPENSTACK=1 + NOSE_OPENSTACK_COLOR=1 + NOSE_OPENSTACK_RED=0.05 + NOSE_OPENSTACK_YELLOW=0.025 + NOSE_OPENSTACK_SHOW_ELAPSED=1 + NOSE_OPENSTACK_STDOUT=1 + NOSE_XUNIT=1 +deps = + -r{toxinidir}/requirements.txt + -r{toxinidir}/test-requirements.txt +commands = nosetests + +[testenv:pep8] +commands = flake8 {posargs} + +[testenv:venv] +commands = {posargs} + +[testenv:docs] +commands = + rm -rf doc/html doc/build + rm -rf doc/source/apidoc doc/source/api + python setup.py build_sphinx + +[testenv:pylint] +setenv = VIRTUAL_ENV={envdir} +commands = bash tools/lintstack.sh + +[flake8] +show-source = true +builtins = _ +exclude=.venv,.git,.tox,dist,doc,*openstack/common*,*lib/python*,*egg,tools