Initial commit for qinling-dashboard

This commit is contained in:
keiichi-hikita 2018-08-13 16:01:33 +09:00
commit 8211b51a40
91 changed files with 6827 additions and 0 deletions

108
.gitignore vendored Normal file
View File

@ -0,0 +1,108 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
.hypothesis/
.pytest_cache/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
target/
# Jupyter Notebook
.ipynb_checkpoints
# pyenv
.python-version
# celery beat schedule file
celerybeat-schedule
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
# Editor specific settings
.idea/*
.vscode/*

17
CONTRIBUTING.rst Normal file
View File

@ -0,0 +1,17 @@
If you would like to contribute to the development of OpenStack, you must
follow the steps in this page:
https://docs.openstack.org/infra/manual/developers.html
If you already have a good understanding of how the system works and your
OpenStack accounts are set up, you can skip to the development workflow
section of this documentation to learn how changes to OpenStack should be
submitted for review via the Gerrit tool:
https://docs.openstack.org/infra/manual/developers.html#development-workflow
Pull requests submitted through GitHub will be ignored.
Bugs should be filed on StoryBoard, not GitHub and Launchpad:
https://storyboard.openstack.org/#!/project/992

4
HACKING.rst Normal file
View File

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

201
LICENSE Normal file
View File

@ -0,0 +1,201 @@
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.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
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.

4
MANIFEST.in Normal file
View File

@ -0,0 +1,4 @@
recursive-include qinling_dashboard *.html *.scss *.css *.js *.map *.svg *.png *.json
include AUTHORS
include ChangeLog

16
README.rst Normal file
View File

@ -0,0 +1,16 @@
=============================
Welcome to Qinling Dashboard!
=============================
Qinling dashboard is a horizon plugin for Qinling.
* License: Apache license
* Documentation: https://docs.openstack.org/qinling-dashboard/latest/
* Source: https://git.openstack.org/cgit/openstack/qinling-dashboard
* Bugs: https://bugs.launchpad.net/qinling-dashboard
Team and repository tags
------------------------
.. image:: https://governance.openstack.org/tc/badges/qinling-dashboard.svg
:target: https://governance.openstack.org/tc/reference/tags/index.html

61
devstack/plugin.sh Normal file
View File

@ -0,0 +1,61 @@
# plugin.sh - DevStack plugin.sh dispatch script qinling-dashboard
QINLING_DASHBOARD_DIR=$(cd $(dirname $BASH_SOURCE)/.. && pwd)
function install_qinling_dashboard {
# NOTE(shu-mutou): workaround for devstack bug: 1540328
# where devstack install 'test-requirements' but should not do it
# for qinling-dashboard project as it installs Horizon from url.
# Remove following two 'mv' commands when mentioned bug is fixed.
mv $QINLING_DASHBOARD_DIR/test-requirements.txt $QINLING_DASHBOARD_DIR/_test-requirements.txt
setup_develop ${QINLING_DASHBOARD_DIR}
mv $QINLING_DASHBOARD_DIR/_test-requirements.txt $QINLING_DASHBOARD_DIR/test-requirements.txt
}
function configure_qinling_dashboard {
cp -a ${QINLING_DASHBOARD_DIR}/qinling_dashboard/enabled/* ${DEST}/horizon/openstack_dashboard/local/enabled/
cp -a ${QINLING_DASHBOARD_DIR}/qinling_dashboard/conf/qinling_policy.json ${DEST}/horizon/openstack_dashboard/conf/
# NOTE: If locale directory does not exist, compilemessages will fail,
# so check for an existence of locale directory is required.
if [ -d ${QINLING_DASHBOARD_DIR}/qinling_dashboard/locale ]; then
(cd ${QINLING_DASHBOARD_DIR}/qinling_dashboard; DJANGO_SETTINGS_MODULE=openstack_dashboard.settings $PYTHON ../manage.py compilemessages)
fi
}
# check for service enabled
if is_service_enabled qinling-dashboard; then
if [[ "$1" == "stack" && "$2" == "pre-install" ]]; then
# Set up system services
# no-op
:
elif [[ "$1" == "stack" && "$2" == "install" ]]; then
# Perform installation of service source
echo_summary "Installing Qinling Dashboard"
install_qinling_dashboard
elif [[ "$1" == "stack" && "$2" == "post-config" ]]; then
# Configure after the other layer 1 and 2 services have been configured
echo_summary "Configuring Qinling Dashboard"
configure_qinling_dashboard
elif [[ "$1" == "stack" && "$2" == "extra" ]]; then
# no-op
:
fi
if [[ "$1" == "unstack" ]]; then
# no-op
:
fi
if [[ "$1" == "clean" ]]; then
# Remove state and transient data
# Remember clean.sh first calls unstack.sh
# no-op
:
fi
fi

2
devstack/settings Normal file
View File

@ -0,0 +1,2 @@
# settings file for qinling-dashboard plugin
enable_service qinling-dashboard

17
doc/requirements.txt Normal file
View File

@ -0,0 +1,17 @@
# The order of packages is significant, because pip processes them in the order
# of appearance. Changing the order has an impact on the overall integration
# process, which may cause wedges in the gate later.
# Order matters to the pip dependency resolver, so sorting this file
# changes how packages are installed. New dependencies should be
# added in alphabetical order, however, some dependencies may need to
# be installed in a specific order.
#
openstackdocstheme>=1.18.1 # Apache-2.0
reno>=2.5.0 # Apache-2.0
sphinx!=1.6.6,!=1.6.7,>=1.6.2 # BSD
sphinxcontrib-apidoc>=0.2.0 # BSD
# NOTE: The following are required as horizon.test.settings loads it.
django-nose>=1.4.4 # BSD
mock>=2.0.0 # BSD
mox3>=0.20.0 # Apache-2.0

89
doc/source/conf.py Executable file
View File

@ -0,0 +1,89 @@
# -*- coding: utf-8 -*-
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import os
import sys
sys.path.insert(0, os.path.abspath('../..'))
# -- 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',
# 'sphinx.ext.intersphinx',
]
# 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
# The suffix of source filenames.
source_suffix = '.rst'
# The master toctree document.
master_doc = 'index'
# General information about the project.
project = u'Qinling Dashboard'
copyright = u'2017, OpenStack Developers'
# openstackdocstheme options
repository_name = 'openstack/qinling-dashboard'
bug_project = '927'
bug_tag = 'doc'
html_last_updated_fmt = '%Y-%m-%d %H:%M'
# 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 = 'sphinx'
# -- 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_path = ["."]
# html_theme = '_theme'
# html_static_path = ['static']
html_theme = 'openstackdocs'
# Output file base name for HTML help builder.
htmlhelp_basename = '%sdoc' % project
# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title, author, documentclass
# [howto/manual]).
latex_documents = [
('index',
'%s.tex' % project,
u'%s Documentation' % project,
u'OpenStack Developers', 'manual'),
]
man_pages = [
('index', u'Qinling Dashboard Documentation',
'Documentation for Qinling Dashboard plugin to Openstack\
Dashboard (Horizon)',
[u'OpenStack'], 1)
]
# Example configuration for intersphinx: refer to the Python standard library.
# intersphinx_mapping = {'http://docs.python.org/': None}

View File

@ -0,0 +1,10 @@
=============
Configuration
=============
Qinling Dashboard currently has no configuration option.
For more configurations, see
`Configuration Guide
<https://docs.openstack.org/horizon/latest/configuration/index.html>`__
in the Horizon documentation.

View File

@ -0,0 +1,30 @@
=================
How to Contribute
=================
Contributor License Agreement
-----------------------------
.. index::
single: license; agreement
In order to contribute to the Qinling Dashboard project, you need to have
signed OpenStack's contributor's agreement.
.. seealso::
* https://docs.openstack.org/infra/manual/developers.html
* https://wiki.openstack.org/CLA
Project Hosting Details
-------------------------
Bug tracker
https://storyboard.openstack.org/#!/project/927
Code Hosting
https://git.openstack.org/cgit/openstack/qinling-dashboard
Code Review
https://review.openstack.org/#/q/status:open+project:openstack/qinling-dashboard,n,z

View File

@ -0,0 +1,13 @@
=================================
Use Qinling Dashboard in DevStack
=================================
Set up your ``local.conf`` to enable qinling-dashboard::
[[local|localrc]]
enable_plugin qinling-dashboard https://git.openstack.org/openstack/qinling-dashboard
.. note::
You also need to install Qinling itself into DevStack to use Qinling Dashboard.

View File

@ -0,0 +1,9 @@
=========================
Contributor Documentation
=========================
.. toctree::
:maxdepth: 2
contributing
devstack

18
doc/source/index.rst Normal file
View File

@ -0,0 +1,18 @@
.. openstack documentation master file, created by
sphinx-quickstart on Tue Jul 9 22:26:36 2013.
You can adapt this file completely to your liking, but it should at least
contain the root `toctree` directive.
.. the main title comes from README.rst
.. include:: ../../README.rst
Contents
--------
.. toctree::
:maxdepth: 2
Installation Guide <install/index>
Configuration Guide <configuration/index>
Contribution <contributor/index>

View File

@ -0,0 +1,76 @@
====================================
Qinling Dashboard installation guide
====================================
This page describes the manual installation of qinling-dashboard,
while distribution packages may provide more automated process.
.. note::
This page assumes horizon has been installed.
Horizon setup is beyond the scope of this page.
Install Qinling Dashboard with all relevant packages to your Horizon environment.
.. code-block:: console
pip install qinling-dashboard
In most cases, qinling-dashboard is installed into your python "site-packages"
directory like ``/usr/local/lib/python2.7/site-packages``.
We refer to the directory of qinling-dashboard as ``<qinling-dashboard-dir>`` below
and it would be ``<site-packages>/qinling_dashboard`` if installed via pip.
The path varies depending on Linux distribution you use.
To enable qinling-dashboard plugin, you need to put horizon plugin setup files
into horizon "enabled" directory.
The plugin setup files are found in ``<qinling-dashboard-dir>/enabled``.
.. code-block:: console
$ cp <qinling-dashboard-dir>/enabled/_[1-9]*.py \
/usr/share/openstack-dashboard/openstack_dashboard/local/enabled
.. note::
The directory ``local/enabled`` may be different depending on your
environment or distribution used. The path above is one used in Ubuntu
horizon package.
Configure the policy file for qinling-dashboard in OpenStack Dashboard
``local_settings.py``.
.. code-block:: python
POLICY_FILES['function_engine'] = '<qinling-dashboard-dir>/conf/qinling_policy.json'
.. note::
If your ``local_settings.py`` has no ``POLICY_FILES`` yet,
you need to define the default ``POLICY_FILES`` in
``local_settings.py``. If you use the example ``local_settings.py`` file
from horizon, what you need is to uncomment ``POLICY_FILES`` (which contains
the default values).
Compile the translation message catalogs of qinling-dashboard.
.. code-block:: console
$ cd <qinling-dashboard-dir>
$ python ./manage.py compilemessages
Run the Django update commands.
Note that ``compress`` is required when you enable compression.
.. code-block:: console
$ cd <horizon-dir>
$ DJANGO_SETTINGS_MODULE=openstack_dashboard.settings python manage.py collectstatic --noinput
$ DJANGO_SETTINGS_MODULE=openstack_dashboard.settings python manage.py compress --force
Finally, restart your web server. For example, in case of apache:
.. code-block:: console
$ sudo service apache2 restart

23
manage.py Executable file
View File

@ -0,0 +1,23 @@
#!/usr/bin/env python
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import os
import sys
from django.core.management import execute_from_command_line
if __name__ == "__main__":
os.environ.setdefault("DJANGO_SETTINGS_MODULE",
"qinling_dashboard.test.settings")
execute_from_command_line(sys.argv)

View File

View File

@ -0,0 +1,39 @@
# Copyright 2012 United States Government as represented by the
# Administrator of the National Aeronautics and Space Administration.
# All Rights Reserved.
#
# Copyright 2012 Nebula, Inc.
# Copyright 2013 Big Switch Networks
#
# 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.
"""
Methods and interface objects used to interact with external APIs.
API method calls return objects that are in many cases objects with
attributes that are direct maps to the data returned from the API http call.
Unfortunately, these objects are also often constructed dynamically, making
it difficult to know what data is available from the API object. Because of
this, all API calls should wrap their returned object in one defined here,
using only explicitly defined attributes and/or methods.
In other words, Horizon developers not working on openstack_dashboard.api
shouldn't need to understand the finer details of APIs for
Keystone/Nova/Glance/Swift et. al.
"""
from qinling_dashboard.api import qinling
__all__ = [
"qinling",
]

View File

@ -0,0 +1,181 @@
# 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 six
from django.conf import settings
from horizon.utils.memoized import memoized
from openstack_dashboard.api import base
from openstack_dashboard.contrib.developer.profiler import api as profiler
from qinlingclient import client as qinling_client
@memoized
def qinlingclient(request, password=None):
api_version = "1"
insecure = getattr(settings, 'OPENSTACK_SSL_NO_VERIFY', False)
cacert = getattr(settings, 'OPENSTACK_SSL_CACERT', None)
endpoint = base.url_for(request, 'function-engine')
kwargs = {
'token': request.user.token.id,
'insecure': insecure,
'ca_file': cacert,
'username': request.user.username,
'password': password
}
client = qinling_client.Client(api_version, endpoint, **kwargs)
return client
@profiler.trace
def runtimes_list(request):
return qinlingclient(request).runtimes.list()
@profiler.trace
def runtime_get(request, runtime_id):
return qinlingclient(request).runtimes.get(runtime_id)
def set_code(datum):
if isinstance(datum.code, six.string_types):
code_dict = json.loads(datum.code)
setattr(datum, "code", code_dict)
@profiler.trace
def functions_list(request, with_version=False):
functions = qinlingclient(request).functions.list()
for f in functions:
set_code(f)
if with_version:
for f in functions:
function_id = f.id
my_versions = \
qinlingclient(request).function_versions.list(function_id)
setattr(f, 'versions', my_versions)
return functions
@profiler.trace
def function_get(request, function_id):
function = qinlingclient(request).functions.get(function_id)
set_code(function)
return function
@profiler.trace
def function_create(request, **params):
resource = qinlingclient(request).functions.create(**params)
return resource
@profiler.trace
def function_update(request, function_id, **params):
resource = qinlingclient(request).functions.update(function_id, **params)
return resource
@profiler.trace
def function_delete(request, function_id):
qinlingclient(request).functions.delete(function_id)
@profiler.trace
def function_download(request, function_id):
function = qinlingclient(request).functions.get(function_id,
download=True)
return function
def set_result(datum):
if isinstance(datum.result, six.string_types):
result_dict = json.loads(datum.result)
setattr(datum, "result", result_dict)
@profiler.trace
def executions_list(request, function_id=None):
executions = qinlingclient(request).function_executions.list()
for e in executions:
set_result(e)
if function_id:
executions = [e for e in executions
if e.function_id == function_id]
return executions
@profiler.trace
def execution_get(request, execution_id):
execution = qinlingclient(request).function_executions.get(execution_id)
set_result(execution)
return execution
@profiler.trace
def execution_create(request, function_id, version=0,
sync=True, input=None):
execution = qinlingclient(request).function_executions.\
create(function_id, version, sync, input)
return execution
@profiler.trace
def execution_delete(request, execution_id):
qinlingclient(request).function_executions.delete(execution_id)
@profiler.trace
def execution_log_get(request, execution_id):
try:
jr = qinlingclient(request).\
function_executions.http_client.json_request
resp, body = jr('/v1/executions/%s/log' % execution_id, 'GET')
raw_logs = resp._content
except Exception:
raw_logs = ""
return raw_logs
@profiler.trace
def versions_list(request, function_id):
versions = qinlingclient(request).function_versions.list(function_id)
return versions
@profiler.trace
def version_get(request, function_id, version_number):
version = qinlingclient(request).function_versions.get(function_id,
version_number)
return version
@profiler.trace
def version_create(request, function_id, description=""):
version = qinlingclient(request).function_versions.create(function_id,
description)
return version
@profiler.trace
def version_delete(request, function_id, version_number):
qinlingclient(request).function_versions.delete(function_id,
version_number)

View File

@ -0,0 +1,18 @@
{
"context_is_admin": "role:admin or is_admin:1",
"owner" : "project_id:%(project_id)s",
"admin_or_owner": "rule:context_is_admin or rule:owner",
"default": "rule:admin_or_owner",
"function:create": "rule:admin_or_owner",
"function:update": "rule:admin_or_owner",
"function:delete": "rule:admin_or_owner",
"function:download": "rule:admin_or_owner",
"function_execution:create": "rule:admin_or_owner",
"function_execution:delete": "rule:admin_or_owner",
"function_execution:log_show": "rule:admin_or_owner",
"function_version:create": "rule:admin_or_owner",
"function_version:delete": "rule:admin_or_owner"
}

View File

View File

@ -0,0 +1,134 @@
# Copyright 2012 Nebula, Inc.
# 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.
"""
Views for managing volumes.
"""
from django.urls import reverse
from django.utils.translation import ugettext_lazy as _
from horizon import exceptions
from horizon import forms
from horizon import messages
from qinling_dashboard import api
from qinling_dashboard import utils as q_utils
from qinling_dashboard import validators
class CreateExecutionForm(forms.SelfHandlingForm):
func = forms.ChoiceField(
label=_("Function"),
help_text=_("Function to execute."),
required=True)
version = forms.IntegerField(
label=_("Version"),
required=False,
initial=0
)
sync = forms.BooleanField(
label=_("Sync"),
required=False,
help_text=_("Check this item if you would like sync execution."),
initial=True
)
input_params = forms.CharField(
label=_('Input'),
help_text=_('Specify input parmeters like name1=value2. '
'One line is equivalent of one input parameter.'),
validators=[validators.validate_key_value_pairs],
widget=forms.widgets.Textarea(),
required=False)
def __init__(self, request, *args, **kwargs):
super(CreateExecutionForm, self).__init__(request, *args, **kwargs)
self.fields['func'].choices = self.get_func_choices(request)
try:
if 'function_id' in request.GET and 'version' in request.GET:
self.prepare_source_fields_if_function_specified(request)
except Exception:
pass
def prepare_source_fields_if_function_specified(self, request):
try:
function_id = request.GET['function_id']
func = api.qinling.function_get(request, function_id)
self.fields['func'].choices = [(function_id, func.name or func.id)]
self.fields['func'].initial = function_id
self.fields['version'].initial = request.GET['version']
except Exception:
msg = _('Unable to load the specified function. %s')
exceptions.handle(request, msg % request.GET['function_id'])
def clean(self):
cleaned = super(CreateExecutionForm, self).clean()
version_number = cleaned.get('version')
# version number is not specified or specified as zero.
if not version_number:
return cleaned
function_id = cleaned.get('func')
versions = api.qinling.versions_list(self.request, function_id)
# in case versions are returned as empty array.
if not versions and not version_number:
return cleaned
available_versions = [v.version_number for v in versions]
if version_number not in available_versions:
msg = _('This function does not '
'have specified version number: %s') % version_number
raise forms.ValidationError(msg)
return cleaned
def get_func_choices(self, request):
try:
functions = api.qinling.functions_list(request)
except Exception:
functions = []
function_choices = [(f.id, f.name or f.id) for f in functions]
if len(function_choices) == 0:
function_choices = [('', _('No functions available.'))]
return function_choices
def handle(self, request, data):
try:
function_id = data['func']
version = int(data['version'])
sync = data['sync']
inp = \
q_utils.convert_raw_input_to_api_format(data['input_params'])
api.qinling.execution_create(request, function_id, version,
sync, inp)
message = _('Created execution of "%s"') % function_id
messages.success(request, message)
return True
except Exception as e:
redirect = reverse("horizon:project:executions:index")
exceptions.handle(request,
_("Unable to create execution. %s") % e,
redirect=redirect)

View File

@ -0,0 +1,22 @@
# 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 django.utils.translation import ugettext_lazy as _
import horizon
class Executions(horizon.Panel):
name = _("Executions")
slug = "executions"
permissions = ('openstack.services.function-engine',)

View File

@ -0,0 +1,172 @@
# 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 django import template
from django.urls import reverse
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import ungettext_lazy
from horizon import tables
from qinling_dashboard import api
from qinling_dashboard import utils
class CreateExecution(tables.LinkAction):
name = "create"
verbose_name = _("Create Execution")
url = "horizon:project:executions:create"
classes = ("ajax-modal", "btn-create")
policy_rules = (("function_engine", "function_execution:create"),)
icon = "plus"
ajax = True
class LogLink(tables.LinkAction):
name = "console"
verbose_name = _("Show Execution Logs")
url = "horizon:project:executions:detail"
classes = ("btn-console",)
policy_rules = (("function_engine", "function_execution:log_show"),)
def get_link_url(self, datum):
base_url = super(LogLink, self).get_link_url(datum)
return base_url + "?tab=execution_details__execution_logs"
class DeleteExecution(tables.DeleteAction):
policy_rules = (("function_engine", "function_execution:delete"),)
help_text = _("Deleted executions are not recoverable.")
@staticmethod
def action_present(count):
return ungettext_lazy(
u"Delete Execution",
u"Delete Executions",
count
)
@staticmethod
def action_past(count):
return ungettext_lazy(
u"Delete Execution",
u"Delete Executions",
count
)
def delete(self, request, execution):
api.qinling.execution_delete(request, execution)
class ExecutionsFilterAction(tables.FilterAction):
def filter(self, table, functions, filter_string):
"""Naive case-insensitive search."""
q = filter_string.lower()
return [function for function in functions
if q in function.name.lower()]
class FunctionIDColumn(tables.Column):
def get_link_url(self, datum):
function_id = datum.function_id
result = reverse(self.link, args=(function_id,))
result += "?tab=function_details__overview"
return result
def get_result(datum):
template_name = 'project/executions/_execution_result.html'
result = datum.result
if result is None:
return result
duration = result.get('duration', '')
output = result.get('output', '')
if isinstance(output, dict) and 'error' in output:
output = output.get('error')
context = {
"duration": duration,
"output": output
}
return template.loader.render_to_string(template_name, context)
class UpdateRow(tables.Row):
ajax = True
def get_data(self, request, execution_id):
execution = api.qinling.execution_get(request, execution_id)
return execution
class ExecutionsTable(tables.DataTable):
id = tables.Column("id",
verbose_name=_("Id"),
link="horizon:project:executions:detail")
function_id = FunctionIDColumn("function_id",
verbose_name=_("Function ID"),
link="horizon:project:functions:detail")
function_version = tables.Column("function_version",
verbose_name=_("Function Version"))
description = tables.Column("description",
verbose_name=_("Description"))
input = tables.Column("input", verbose_name=_("Input"))
result = tables.Column(get_result,
verbose_name=_("Result"))
sync = tables.Column("sync", verbose_name=_("Sync"))
created_at = tables.Column("created_at",
verbose_name=_("Created At"))
updated_at = tables.Column("updated_at",
verbose_name=_("Updated At"))
project_id = tables.Column("project_id",
verbose_name=_("Project ID"))
status = tables.Column(
"status",
status=True,
status_choices=utils.FUNCTION_ENGINE_STATUS_CHOICES,
display_choices=utils.FUNCTION_ENGINE_STATUS_DISPLAY_CHOICES)
def get_object_display(self, datum):
return datum.id
class Meta(object):
name = "executions"
verbose_name = _("Executions")
status_columns = ["status"]
multi_select = True
row_class = UpdateRow
table_actions = (
CreateExecution,
DeleteExecution,
ExecutionsFilterAction,
)
row_actions = (LogLink,
DeleteExecution,)

View File

@ -0,0 +1,49 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import logging
from django.utils.translation import ugettext_lazy as _
from horizon import tabs
LOG = logging.getLogger(__name__)
class ExecutionOverviewTab(tabs.Tab):
name = _("Overview")
slug = "overview"
template_name = "project/executions/_detail_overview.html"
def get_context_data(self, request):
execution = self.tab_group.kwargs["execution"]
return {"execution": execution,
"result": execution.result}
class ExecutionLogsTab(tabs.Tab):
name = _("Execution Logs")
slug = "execution_logs"
template_name = "project/executions/_detail_logs.html"
def get_context_data(self, request):
execution_logs = self.tab_group.kwargs["execution_logs"]
return {"execution_logs": execution_logs}
class ExecutionDetailTabs(tabs.TabGroup):
slug = "execution_details"
tabs = (ExecutionOverviewTab,
ExecutionLogsTab)
sticky = True
show_single_tab = False

View File

@ -0,0 +1,6 @@
{% extends "horizon/common/_modal_form.html" %}
{% load i18n %}
{% block modal-body-right %}
<h3>{% trans "Description:" %}</h3>
<p>{% trans "Create a new execution of function." %}</p>
{% endblock %}

View File

@ -0,0 +1,2 @@
{% load i18n %}
<pre class="logs">{{ execution_logs }}</pre>

View File

@ -0,0 +1,57 @@
{% load i18n %}
<div class="detail">
<dl class="dl-horizontal">
<dt>{% trans "ID" %}</dt>
<dd>{{ execution.id }}</dd>
<dt>{% trans "Function ID" %}</dt>
<dd>{{ execution.function_id }}</dd>
<dt>{% trans "Function Version" %}</dt>
<dd>{{ execution.function_version }}</dd>
<dt>{% trans "Description" %}</dt>
<dd>{{ execution.description }}</dd>
<dt>{% trans "Input" %}</dt>
<dd>{{ execution.input }}</dd>
<dt>{% trans "Result" %}</dt>
<dd>
<table class="table">
<thead>
<tr>
<th>{% trans "Duration" %}</th>
<th>{% trans "Output" %}</th>
</tr>
</thead>
<tbody>
<tr>
<td>{{ result.duration }}</td>
<td>{{ result.output }}</td>
</tr>
</tbody>
</table>
</dd>
<dt>{% trans "Sync" %}</dt>
<dd>{{ execution.sync }}</dd>
<dt>{% trans "Project ID" %}</dt>
<dd>{{ execution.project_id }}</dd>
<dt>{% trans "Created At" %}</dt>
<dd>{{ execution.created_at|parse_isotime }}</dd>
<dt>{% trans "Updated At" %}</dt>
<dd>{{ execution.updated_at|parse_isotime }}</dd>
<dt>{% trans "Status" %}</dt>
{% with execution.status|title as rt_status %}
<dd>{% trans rt_status context "current status of a runtime" %}</dd>
{% endwith %}
</dl>
</div>

View File

@ -0,0 +1,3 @@
{% load i18n %}
<b>{% trans 'Duration' %}</b>: {{ duration }}<br/>
<b>{% trans 'Output' %}</b>: {{ output }}

View File

@ -0,0 +1,7 @@
{% extends 'base.html' %}
{% load i18n %}
{% block title %}{% trans "Create Execution" %}{% endblock %}
{% block main %}
{% include 'project/executions/_create.html' %}
{% endblock %}

View File

@ -0,0 +1,11 @@
{% extends 'base.html' %}
{% load i18n %}
{% block title %}{% trans "Execution Details" %}{% endblock %}
{% block main %}
<div class="row">
<div class="col-sm-12">
{{ tab_group.render }}
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,25 @@
# 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 django.conf.urls import url
from qinling_dashboard.content.executions import views
urlpatterns = [
url(r'^$', views.IndexView.as_view(), name='index'),
url(r'^create', views.CreateExecutionView.as_view(), name='create'),
url(r'^(?P<execution_id>[^/]+)/$',
views.DetailView.as_view(), name='detail'),
url(r'^(?P<execution_id>[^/]+)/\?tab=execution_details__execution_logs$',
views.DetailView.as_view(), name='detail_execution_logs'),
]

View File

@ -0,0 +1,107 @@
# 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 django.urls import reverse
from django.urls import reverse_lazy
from django.utils.translation import ugettext_lazy as _
from horizon import exceptions
from horizon import forms
from horizon import tables
from horizon import tabs
from horizon.utils import memoized
from qinling_dashboard import api
from qinling_dashboard.content.executions import forms as project_forms
from qinling_dashboard.content.executions import tables as project_tables
from qinling_dashboard.content.executions import tabs as project_tabs
class CreateExecutionView(forms.ModalFormView):
form_class = project_forms.CreateExecutionForm
modal_header = submit_label = page_title = _("Create Execution")
template_name = 'project/executions/create.html'
submit_url = reverse_lazy("horizon:project:executions:create")
success_url = reverse_lazy("horizon:project:executions:index")
class IndexView(tables.DataTableView):
table_class = project_tables.ExecutionsTable
page_title = _("Executions")
def get_data(self):
try:
executions = api.qinling.executions_list(self.request)
except Exception:
executions = []
msg = _('Unable to retrieve executions list.')
exceptions.handle(self.request, msg)
return executions
class DetailView(tabs.TabbedTableView):
tab_group_class = project_tabs.ExecutionDetailTabs
template_name = 'project/executions/detail.html'
page_title = _("Execution Details: {{ resource_name }}")
failure_url = reverse_lazy('horizon:project:executions:index')
@memoized.memoized_method
def _get_data(self):
execution_id = self.kwargs['execution_id']
try:
execution = api.qinling.execution_get(self.request, execution_id)
except Exception:
exceptions.handle(
self.request,
_('Unable to retrieve '
'details for execution "%s".') % execution_id,
redirect=self.failure_url
)
return execution
def _get_execution_logs(self):
execution_id = self.kwargs['execution_id']
try:
execution_logs = api.qinling.execution_log_get(self.request,
execution_id)
except Exception:
execution_logs = ""
msg = _('Unable to retrieve execution logs.')
exceptions.handle(self.request, msg)
return execution_logs
def get_context_data(self, **kwargs):
context = super(DetailView, self).get_context_data(**kwargs)
execution = self._get_data()
table = project_tables.ExecutionsTable(self.request)
context["execution"] = execution
context["url"] = self.get_redirect_url()
context["actions"] = table.render_row_actions(execution)
context["table_id"] = "execution"
context["resource_name"] = execution.id
context["object_id"] = execution.id
return context
def get_tabs(self, request, *args, **kwargs):
execution = self._get_data()
execution_logs = self._get_execution_logs()
return self.tab_group_class(request,
execution=execution,
execution_logs=execution_logs,
**kwargs)
@staticmethod
def get_redirect_url():
return reverse('horizon:project:executions:index')

View File

@ -0,0 +1,458 @@
# Copyright 2012 Nebula, Inc.
# All rights reserved.
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from django.urls import reverse
from django.utils.translation import ugettext_lazy as _
from django_file_md5 import calculate_md5
from horizon import exceptions
from horizon import forms
from horizon import messages
from qinling_dashboard import api
from qinling_dashboard import validators
CPU_MIN_VALUE = 100
CPU_MAX_VALUE = 300
MEMORY_MIN_VALUE = 33554432
MEMORY_MAX_VALUE = 134217728
CPU_HELP_TEXT = _("Limit of cpu resource(unit: millicpu). Range: {0} ~ {1}").\
format(CPU_MIN_VALUE, CPU_MAX_VALUE)
MEMORY_HELP_TEXT = _("Limit of memory resource(unit: bytes). "
"Range: {0} ~ {1} (bytes).").format(MEMORY_MIN_VALUE,
MEMORY_MAX_VALUE)
UPLOAD_PACKAGE_LABEL = _('Package')
UPLOAD_SWIFT_CONTAINER_LABEL = _('Swift Container')
UPLOAD_SWIFT_OBJECT_LABEL = _('Swift Object')
UPLOAD_IMAGE_LABEL = _('Image')
CODE_TYPE_CHOICES = [('package', UPLOAD_PACKAGE_LABEL),
('swift', UPLOAD_SWIFT_OBJECT_LABEL),
('image', UPLOAD_IMAGE_LABEL)]
class CreateFunctionForm(forms.SelfHandlingForm):
name = forms.CharField(max_length=255,
label=_("Name"),
validators=[validators.validate_one_line_string],
required=False)
description = forms.CharField(
max_length=255,
widget=forms.Textarea(
attrs={'class': 'modal-body-fixed-width',
'rows': 3}),
label=_("Description"),
required=False)
cpu = forms.IntegerField(label=_("CPU"),
help_text=CPU_HELP_TEXT,
min_value=CPU_MIN_VALUE,
max_value=CPU_MAX_VALUE,
required=False)
memory_size = forms.IntegerField(label=_("Memory Size"),
help_text=MEMORY_HELP_TEXT,
min_value=MEMORY_MIN_VALUE,
max_value=MEMORY_MAX_VALUE,
required=False)
code_type = forms.ThemableChoiceField(
label=_("Code Type"),
help_text=_("Select Your Code Type."),
widget=forms.ThemableSelectWidget(
attrs={'class': 'switchable',
'data-slug': 'code_type'}
),
choices=CODE_TYPE_CHOICES,
required=True)
package_file = forms.FileField(
label=UPLOAD_PACKAGE_LABEL,
help_text=_('Code package zip file path.'),
widget=forms.FileInput(
attrs={
'class': 'switched',
'data-switch-on': 'code_type',
'data-code_type-package': UPLOAD_PACKAGE_LABEL,
},
),
required=False)
entry = forms.CharField(
label=_("Entry"),
help_text=_('Function entry in the format of '
'<module_name>.<method_name>'),
validators=[validators.validate_one_line_string],
widget=forms.TextInput(attrs={
'class': 'switched',
'data-switch-on': 'code_type',
'data-code_type-package': _("Entry")
}),
required=False)
runtime = forms.ThemableChoiceField(
label=_("Runtime"),
widget=forms.SelectWidget(attrs={
'class': 'switched',
'data-switch-on': 'code_type',
'data-code_type-package': _("Runtime")
}),
required=False)
swift_container = forms.CharField(
label=UPLOAD_SWIFT_CONTAINER_LABEL,
help_text=_('Container name in Swift.'),
validators=[validators.validate_one_line_string],
widget=forms.TextInput(attrs={
'class': 'switched',
'data-switch-on': 'code_type',
'data-code_type-swift': UPLOAD_SWIFT_CONTAINER_LABEL
}),
required=False)
swift_object = forms.CharField(
label=UPLOAD_SWIFT_OBJECT_LABEL,
help_text=_('Object name in Swift.'),
validators=[validators.validate_one_line_string],
widget=forms.TextInput(attrs={
'class': 'switched',
'data-switch-on': 'code_type',
'data-code_type-swift': UPLOAD_SWIFT_OBJECT_LABEL
}),
required=False)
entry_swift = forms.CharField(
label=_("Entry"),
help_text=_('Function entry in the format of '
'<module_name>.<method_name>'),
validators=[validators.validate_one_line_string],
widget=forms.TextInput(attrs={
'class': 'switched',
'data-switch-on': 'code_type',
'data-code_type-swift': _("Entry")
}),
required=False)
runtime_swift = forms.ThemableChoiceField(
label=_("Runtime"),
widget=forms.SelectWidget(attrs={
'class': 'switched',
'data-switch-on': 'code_type',
'data-code_type-swift': _("Runtime")
}),
required=False)
image = forms.CharField(
label=UPLOAD_IMAGE_LABEL,
help_text=_('Image name in Docker hub.'),
validators=[validators.validate_one_line_string],
widget=forms.TextInput(attrs={
'class': 'switched',
'data-switch-on': 'code_type',
'data-code_type-image': UPLOAD_IMAGE_LABEL
}),
required=False)
def get_runtime_choices(self, request):
try:
runtimes = api.qinling.runtimes_list(request)
except Exception:
runtimes = []
exceptions.handle(request, _('Unable to retrieve runtimes.'))
runtime_list = [(runtime.id, runtime.name) for runtime in runtimes
if runtime.status == 'available']
runtime_list.sort()
if not runtime_list:
runtime_list.insert(0, ("", _("No runtimes found")))
return runtime_list
def __init__(self, request, *args, **kwargs):
super(CreateFunctionForm, self).__init__(request, *args, **kwargs)
runtime_choices = self.get_runtime_choices(request)
self.fields['runtime'].choices = runtime_choices
self.fields['runtime_swift'].choices = runtime_choices
def _validation_for_swift_case(self, cleaned):
swift_container = cleaned.get('swift_container', None)
swift_object = cleaned.get('swift_object', None)
if not all([swift_container, swift_object]):
msg = _('You must specify container and object '
'both in case code type is Swift.')
raise forms.ValidationError(msg)
runtime = cleaned.get('runtime_swift', None)
if not runtime:
msg = _('You must specify runtime.')
raise forms.ValidationError(msg)
def _validation_for_image_case(self, cleaned):
image = cleaned.get('image', None)
if not image:
msg = _('You must specify Docker image.')
raise forms.ValidationError(msg)
def _get_package_file(self, cleaned):
package_file = cleaned.get('package_file', None)
return package_file
def _validation_for_package_case(self, cleaned):
package_file = self._get_package_file(cleaned)
if not package_file:
msg = _('You must specify package file.')
raise forms.ValidationError(msg)
runtime = cleaned.get('runtime', None)
if not runtime:
msg = _('You must specify runtime.')
raise forms.ValidationError(msg)
def clean(self):
cleaned = super(CreateFunctionForm, self).clean()
code_type = cleaned['code_type']
if code_type == 'package':
self._validation_for_package_case(cleaned)
if code_type == 'swift':
self._validation_for_swift_case(cleaned)
if code_type == 'image':
self._validation_for_image_case(cleaned)
return cleaned
def handle_package_case(self, params, context, update=False):
upload_files = self.request.FILES
package = upload_files.get('package_file')
md5sum = calculate_md5(package)
code = {'source': 'package', 'md5sum': md5sum}
# case1: creation case.
# Package upload is required in creation case.
# case2: update case.
# In case update, package/code are only added
# when user specify them.
if not update or (update and package):
package = [ck for ck in package.chunks()][0]
params.update({
'package': package,
'code': code,
})
if not update and context.get('runtime'):
params.update({'runtime': context.get('runtime')})
self._handle_entry_for_package(params, context, update)
def _handle_entry_for_package(self, params, context, update=False):
if update:
params.update({'entry': context.get('entry')})
else:
if context.get('entry'):
params.update({'entry': context.get('entry')})
def handle_swift_case(self, params, context, update=False):
swift_container = context.get('swift_container')
swift_object = context.get('swift_object')
if not update or all([not update, swift_container, swift_object]):
code = {
'source': 'swift',
'swift': {
'container': swift_container,
'object': swift_object
}
}
params.update({'code': code})
if not update and context.get('runtime_swift'):
params.update({'runtime': context.get('runtime_swift')})
if not update and context.get('entry_swift'):
params.update({'entry': context.get('entry_swift')})
def handle_image_case(self, params, context, update=False):
if not update or all([not update, context.get('image')]):
code = {
'source': 'image',
'image': context.get('image')
}
params.update({'code': code})
def handle(self, request, context):
params = {}
# basic parameters
if context.get('name'):
params.update({'name': context.get('name')})
if context.get('description'):
params.update({'description': context.get('description')})
if context.get('cpu'):
params.update({'cpu': int(context.get('cpu'))})
if context.get('memory_size'):
params.update({'memory_size': int(context.get('memory_size'))})
code_type = context.get('code_type')
if code_type == 'package':
self.handle_package_case(params, context)
elif code_type == 'swift':
self.handle_swift_case(params, context)
elif code_type == 'image':
self.handle_image_case(params, context)
try:
api.qinling.function_create(request, **params)
message = _('Created function "%s"') % params.get('name',
'unknown name')
messages.success(request, message)
return True
except Exception:
redirect = reverse("horizon:project:functions:index")
exceptions.handle(request,
_("Unable to create function."),
redirect=redirect)
class UpdateFunctionForm(CreateFunctionForm):
function_id = forms.CharField(widget=forms.HiddenInput(),
required=False)
code_type = forms.ThemableChoiceField(
label=_("Code Type"),
help_text=_("Code Type can not be changed when you update function."),
widget=forms.TextInput(
attrs={'readonly': 'readonly'}
),
choices=CODE_TYPE_CHOICES,
required=True)
# override this to skip
def clean(self):
cleaned = super(forms.SelfHandlingForm, self).clean()
return cleaned
def __init__(self, request, *args, **kwargs):
super(UpdateFunctionForm, self).__init__(request, *args, **kwargs)
code_type = self.initial.get('code_type')
# written field names here will be regarded as updatable
common_fields = ['name', 'description',
'code_type', 'function_id']
available_fields = {
'package': ['package_file', 'entry', 'cpu', 'memory_size'],
'swift': [],
'image': [],
}
available_fields = available_fields[code_type] + common_fields
field_names = [fn for fn in self.fields.keys()]
for field_name in field_names:
if field_name not in available_fields:
del self.fields[field_name]
def handle(self, request, context):
# basic parameters
params = {
'name': context.get('name', ''),
'description': context.get('description', '')
}
if context.get('cpu'):
params.update({'cpu': int(context.get('cpu'))})
if context.get('memory_size'):
params.update({'memory_size': int(context.get('memory_size'))})
code_type = context.get('code_type')
if code_type == 'package':
self.handle_package_case(params, context, update=True)
elif code_type == 'swift':
self.handle_swift_case(params, context, update=True)
elif code_type == 'image':
self.handle_image_case(params, context, update=True)
function_id = context['function_id']
try:
api.qinling.function_update(request, function_id, **params)
message = _('Updated function "%s"') % params.get('name',
'unknown name')
messages.success(request, message)
return True
except Exception:
redirect = reverse("horizon:project:functions:index")
exceptions.handle(request,
_("Unable to update function %s") % function_id,
redirect=redirect)
class CreateFunctionVersionForm(forms.SelfHandlingForm):
function_id = forms.CharField(
label=_("Function ID"),
help_text=_("Function which new version will be created."),
widget=forms.TextInput(
attrs={'readonly': 'readonly'}
),
required=False)
description = forms.CharField(
max_length=255,
widget=forms.Textarea(
attrs={'class': 'modal-body-fixed-width',
'rows': 3}),
label=_("Description"),
required=False)
def handle(self, request, data):
try:
function_id = data['function_id']
description = data['description']
api.qinling.version_create(request, function_id, description)
message = _('Created new version of "%s"') % function_id
messages.success(request, message)
return True
except Exception as e:
redirect = reverse("horizon:project:functions:index")
if hasattr(e, 'details'):
msg = _("Unable to create execution. %s") % e.details
else:
msg = _("Unable to create execution")
exceptions.handle(request, msg, redirect=redirect)

View File

@ -0,0 +1,22 @@
# 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 django.utils.translation import ugettext_lazy as _
import horizon
class Functions(horizon.Panel):
name = _("Functions")
slug = "functions"
permissions = ('openstack.services.function-engine',)

View File

@ -0,0 +1,291 @@
# 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 django.urls import reverse
from django.utils.http import urlencode
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import ungettext_lazy
from horizon import tables
from qinling_dashboard import api
from qinling_dashboard.content.executions import tables as e_tables
class CreateFunction(tables.LinkAction):
name = "create"
verbose_name = _("Create Function")
url = "horizon:project:functions:create"
classes = ("ajax-modal", "btn-create")
policy_rules = (("function_engine", "function:create"),)
icon = "plus"
ajax = True
class CreateFunctionVersion(tables.LinkAction):
name = "create_version"
verbose_name = _("Create Version")
url = "horizon:project:functions:create_version"
classes = ("ajax-modal",)
icon = "plus"
policy_rules = (("function_engine", "function:version_create"),)
def allowed(self, request, datum):
"""Function versioning is only allowed for package type function."""
code = datum.code
if code['source'] == 'package':
return True
return False
class UpdateFunction(tables.LinkAction):
name = "update"
verbose_name = _("Update Function")
url = "horizon:project:functions:update"
classes = ("ajax-modal",)
policy_rules = (("function_engine", "function:update"),)
icon = "edit"
ajax = True
class DownloadFunction(tables.LinkAction):
name = "download"
verbose_name = _("Download Function")
url = "horizon:project:functions:download"
icon = "download"
policy_rules = (("function_engine", "function:download"),)
def allowed(self, request, datum):
"""Function downloading is only allowed for package type function."""
code = datum.code
if code['source'] == 'package':
return True
return False
class DeleteFunction(tables.DeleteAction):
policy_rules = (("function_engine", "function:delete"),)
help_text = _("Deleted functions are not recoverable.")
@staticmethod
def action_present(count):
return ungettext_lazy(
u"Delete Function",
u"Delete Functions",
count
)
@staticmethod
def action_past(count):
return ungettext_lazy(
u"Delete Function",
u"Delete Functions",
count
)
def delete(self, request, func):
api.qinling.function_delete(request, func)
class FunctionsFilterAction(tables.FilterAction):
def filter(self, table, functions, filter_string):
"""Naive case-insensitive search."""
q = filter_string.lower()
return [function for function in functions
if q in function.name.lower()]
class RuntimeIDColumn(tables.Column):
def get_link_url(self, datum):
if not hasattr(datum, 'id'):
return None
runtime_id = datum.runtime_id
result = reverse(self.link, args=(runtime_id,))
return result
def get_memory_size(function):
return "{:,}".format(function.memory_size)
class FunctionsTable(tables.DataTable):
id = tables.Column("id",
verbose_name=_("Id"),
link="horizon:project:functions:detail")
name = tables.Column("name",
verbose_name=_("Name"))
description = tables.Column("description",
verbose_name=_("Description"))
runtime_id = RuntimeIDColumn("runtime_id",
verbose_name=_("Runtime ID"),
link="horizon:project:runtimes:detail")
entry = tables.Column("entry",
verbose_name=_("Entry"))
created_at = tables.Column("created_at",
verbose_name=_("Created At"))
updated_at = tables.Column("updated_at",
verbose_name=_("Updated At"))
cpu = tables.Column("cpu", verbose_name=_("CPU (Milli CPU)"))
memory_size = tables.Column(get_memory_size,
verbose_name=_("Memory Size (Bytes)"))
def get_object_display(self, datum):
return datum.id
class Meta(object):
name = "functions"
verbose_name = _("Functions")
multi_select = True
table_actions = (
CreateFunction,
DeleteFunction,
FunctionsFilterAction
)
row_actions = (
UpdateFunction,
DownloadFunction,
CreateFunctionVersion,
DeleteFunction
)
class DeleteFunctionVersion(tables.DeleteAction):
policy_rules = (("function_engine", "function_version:delete"),)
help_text = _("Deleted function versions are not recoverable.")
@staticmethod
def action_present(count):
return ungettext_lazy(
u"Delete Function Version",
u"Delete Function Versions",
count
)
@staticmethod
def action_past(count):
return ungettext_lazy(
u"Delete Function Version",
u"Delete Function Versions",
count
)
def delete(self, request, version_id):
functions = api.qinling.functions_list(request, with_version=True)
for f in functions:
for v in f.versions:
if v.id == version_id:
version_number = v.version_number
function_id = v.function_id
api.qinling.version_delete(request,
function_id, version_number)
class CreateFunctionExecution(tables.LinkAction):
name = "create_execution_from_function"
verbose_name = _("Create Execution")
url = "horizon:project:functions:create_execution"
classes = ("ajax-modal",)
policy_rules = (("function_engine", "function_execution:create"),)
icon = "plus"
def get_link_url(self, datum):
function_id = datum.function_id
version_number = datum.version_number
base_url = reverse(self.url, args=(function_id,))
params = urlencode({"function_id": function_id,
"version": version_number})
return "?".join([base_url, params])
class FunctionVersionColumn(tables.Column):
def get_link_url(self, datum):
version_number = datum.version_number
function_id = self.table.kwargs['function_id']
result = reverse(self.link, args=(function_id,
version_number))
return result
class FunctionColumn(tables.Column):
def get_link_url(self, datum):
function_id = datum.function_id
result = reverse(self.link, args=(function_id,))
result += "?tab=function_details__overview"
return result
class FunctionVersionsTable(tables.DataTable):
id = FunctionVersionColumn("id",
verbose_name=_("Id"),
link="horizon:project:functions:version_detail")
description = tables.Column("description",
verbose_name=_("Description"))
function_id = FunctionColumn("function_id",
verbose_name=_("Function ID"),
link="horizon:project:functions:detail")
version_number = tables.Column("version_number",
verbose_name=_("Version Number"))
count = tables.Column("count", verbose_name=_("Count"))
created_at = tables.Column("created_at",
verbose_name=_("Created At"))
updated_at = tables.Column("updated_at",
verbose_name=_("Updated At"))
def get_object_display(self, datum):
return datum.id
class Meta(object):
name = "function_versions"
verbose_name = _("Versions")
multi_select = True
table_actions = (DeleteFunctionVersion,)
row_actions = (CreateFunctionExecution,
DeleteFunctionVersion,)
class FunctionExecutionsTable(e_tables.ExecutionsTable):
class Meta(object):
name = "function_executions"
verbose_name = _("Executions")
multi_select = True
table_actions = (e_tables.DeleteExecution,)
row_actions = (e_tables.LogLink,
e_tables.DeleteExecution,)

View File

@ -0,0 +1,83 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import logging
from django.utils.translation import ugettext_lazy as _
from horizon import tabs
from qinling_dashboard.content.functions import tables as project_tables
LOG = logging.getLogger(__name__)
class FunctionOverviewTab(tabs.Tab):
name = _("Overview")
slug = "overview"
template_name = "project/functions/_detail_overview.html"
failure_url = 'horizon:project:functions:index'
def get_context_data(self, request):
func = self.tab_group.kwargs['function']
func.memory_size = "{:,}".format(func.memory_size)
return {"function": func,
"code": func.code}
class FunctionExecutionsTab(tabs.TableTab):
table_classes = (project_tables.FunctionExecutionsTable,)
name = _("Executions of this function")
slug = "executions_of_this_function"
template_name = "project/functions/_detail_executions.html"
# preload = False
def get_function_executions_data(self):
return self.tab_group.kwargs['executions']
class FunctionVersionsTab(tabs.TableTab):
table_classes = (project_tables.FunctionVersionsTable,)
name = _("Versions of this function")
slug = "versions_of_this_function"
template_name = "project/functions/_detail_versions.html"
def get_function_versions_data(self):
return self.tab_group.kwargs['versions']
class FunctionDetailTabs(tabs.TabGroup):
slug = "function_details"
tabs = (FunctionOverviewTab,
FunctionExecutionsTab,
FunctionVersionsTab)
sticky = True
show_single_tab = False
class FunctionVersionOerviewTab(tabs.Tab):
name = _("Version detail")
slug = "versions_detail_overview"
template_name = "project/functions/_detail_version_overview.html"
preload = False
def get_context_data(self, request):
version = self.tab_group.kwargs['version']
return {"version": version}
class FunctionVersionDetailTabs(tabs.TabGroup):
slug = "function_version_details"
tabs = (FunctionVersionOerviewTab,)
sticky = True
show_single_tab = False

View File

@ -0,0 +1,7 @@
{% extends "horizon/common/_modal_form.html" %}
{% load i18n %}
{% block form_attrs %}enctype="multipart/form-data"{% endblock %}
{% block modal-body-right %}
<h3>{% trans "Description:" %}</h3>
<p>{% trans "Create a new function." %}</p>
{% endblock %}

View File

@ -0,0 +1,6 @@
{% extends "horizon/common/_modal_form.html" %}
{% load i18n %}
{% block modal-body-right %}
<h3>{% trans "Description:" %}</h3>
<p>{% trans "Create a new version of function." %}</p>
{% endblock %}

View File

@ -0,0 +1,3 @@
{% load i18n %}
{{ table.render }}

View File

@ -0,0 +1,58 @@
{% load i18n %}
<div class="detail">
<dl class="dl-horizontal">
<dt>{% trans "ID" %}</dt>
<dd>{{ function.id }}</dd>
<dt>{% trans "Name" %}</dt>
<dd data-display="{{ runtime.name }}">{{ function.name }}</dd>
<dt>{% trans "Description" %}</dt>
<dd>{{ function.description }}</dd>
<dt>{% trans "Count" %}</dt>
<dd>{{ function.count }}</dd>
<dt>{% trans "Code" %}</dt>
<dd>
<table class="table">
<thead>
<tr>
<th>{% trans "Source" %}</th>
<th>{% trans "Md5sum" %}</th>
</tr>
</thead>
<tbody>
<tr>
<td>{{ code.source }}</td>
<td>{{ code.md5sum }}</td>
</tr>
</tbody>
</table>
</dd>
<dt>{% trans "Runtime ID" %}</dt>
<dd>{{ function.runtime_id }}</dd>
<dt>{% trans "Entry" %}</dt>
<dd>{{ function.entry }}</dd>
<dt>{% trans "Project ID" %}</dt>
<dd>{{ function.project_id }}</dd>
<dt>{% trans "Created At" %}</dt>
<dd>{{ function.created_at|parse_isotime }}</dd>
<dt>{% trans "Updated At" %}</dt>
<dd>{{ function.updated_at|parse_isotime }}</dd>
<dt>{% trans "CPU (Milli CPU)" %}</dt>
<dd>{{ function.cpu }}</dd>
<dt>{% trans "Memory Size (Bytes)" %}</dt>
<dd>{{ function.memory_size }}</dd>
</dl>
</div>

View File

@ -0,0 +1,31 @@
{% load i18n %}
<div class="detail">
<dl class="dl-horizontal">
<dt>{% trans "ID" %}</dt>
<dd>{{ version.id }}</dd>
<dt>{% trans "Function ID" %}</dt>
<dd>{{ version.function_id }}</dd>
<dt>{% trans "Description" %}</dt>
<dd>{{ version.description }}</dd>
<dt>{% trans "Version Number" %}</dt>
<dd>{{ version.version_number }}</dd>
<dt>{% trans "Count" %}</dt>
<dd>{{ version.count }}</dd>
<dt>{% trans "Project ID" %}</dt>
<dd>{{ version.project_id }}</dd>
<dt>{% trans "Created At" %}</dt>
<dd>{{ version.created_at|parse_isotime }}</dd>
<dt>{% trans "Updated At" %}</dt>
<dd>{{ version.updated_at|parse_isotime }}</dd>
</dl>
</div>

View File

@ -0,0 +1,3 @@
{% load i18n %}
{{ table.render }}

View File

@ -0,0 +1,7 @@
{% extends "horizon/common/_modal_form.html" %}
{% load i18n %}
{% block form_attrs %}enctype="multipart/form-data"{% endblock %}
{% block modal-body-right %}
<h3>{% trans "Description:" %}</h3>
<p>{% trans "Update function." %}</p>
{% endblock %}

View File

@ -0,0 +1,7 @@
{% extends 'base.html' %}
{% load i18n %}
{% block title %}{% trans "Create Function" %}{% endblock %}
{% block main %}
{% include 'project/functions/_create_function.html' %}
{% endblock %}

View File

@ -0,0 +1,7 @@
{% extends 'base.html' %}
{% load i18n %}
{% block title %}{% trans "Create Function Version" %}{% endblock %}
{% block main %}
{% include 'project/executions/_create_version.html' %}
{% endblock %}

View File

@ -0,0 +1,11 @@
{% extends 'base.html' %}
{% load i18n %}
{% block title %}{% trans "Function Details" %}{% endblock %}
{% block main %}
<div class="row">
<div class="col-sm-12">
{{ tab_group.render }}
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,11 @@
{% extends 'base.html' %}
{% load i18n %}
{% block title %}{% trans "Function Version Details" %}{% endblock %}
{% block main %}
<div class="row">
<div class="col-sm-12">
{{ tab_group.render }}
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,7 @@
{% extends 'base.html' %}
{% load i18n %}
{% block title %}{% trans "Update Function" %}{% endblock %}
{% block main %}
{% include 'project/executions/_update_function.html' %}
{% endblock %}

View File

@ -0,0 +1,42 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from django.conf.urls import url
from qinling_dashboard.content.functions import views
urlpatterns = [
url(r'^$', views.IndexView.as_view(), name='index'),
url(r'^create', views.CreateFunctionView.as_view(), name='create'),
url(r'^(?P<function_id>[^/]+)/update',
views.UpdateFunctionView.as_view(), name='update'),
url(r'^(?P<function_id>[^/]+)/download/$',
views.download_function, name='download'),
url(r'^(?P<function_id>[^/]+)/versions/create',
views.CreateFunctionVersionView.as_view(), name='create_version'),
url(r'^(?P<function_id>[^/]+)/versions/(?P<version_number>[^/]+)/$',
views.VersionDetailView.as_view(), name='version_detail'),
url(r'^(?P<function_id>[^/]+)/functions/create_execution',
views.CreateFunctionExecutionView.as_view(), name='create_execution'),
# detail
url(r'^(?P<function_id>[^/]+)/$',
views.DetailView.as_view(), name='detail'),
# detail(tab=executions)
url(r'^(?P<function_id>[^/]+)/'
r'\?tab=function_details_executions_of_this_function$',
views.DetailView.as_view(), name='detail_executions'),
# detail(tab=versions)
url(r'^(?P<function_id>[^/]+)/'
r'\?tab=function_details_versions_of_this_function$',
views.DetailView.as_view(), name='detail_versions'),
]

View File

@ -0,0 +1,309 @@
# 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
from django import shortcuts
from django.http import HttpResponse
from django.urls import reverse
from django.urls import reverse_lazy
from django.utils.translation import ugettext_lazy as _
from horizon import exceptions
from horizon import forms
from horizon import messages
from horizon import tables
from horizon import tabs
from horizon.utils import memoized
from qinling_dashboard import api
from qinling_dashboard import exceptions as q_exc
from qinling_dashboard.content.executions import views as ex_views
from qinling_dashboard.content.functions import forms as project_forms
from qinling_dashboard.content.functions import tables as project_tables
from qinling_dashboard.content.functions import tabs as project_tabs
class CreateFunctionExecutionView(ex_views.CreateExecutionView):
modal_header = submit_label = page_title = _("Create Execution")
submit_url = "horizon:project:functions:create_execution"
success_url = "horizon:project:functions:detail"
def get_context_data(self, **kwargs):
context = super(CreateFunctionExecutionView, self).\
get_context_data(**kwargs)
submit_url = reverse(self.submit_url,
args=(self.kwargs['function_id'],))
context['submit_url'] = submit_url
return context
def get_success_url(self):
function_id = self.kwargs['function_id']
result = reverse(self.success_url, args=(function_id,))
result += "?tab=function_details__executions_of_this_function"
return result
class CreateFunctionView(forms.ModalFormView):
form_class = project_forms.CreateFunctionForm
modal_header = submit_label = page_title = _("Create Function")
template_name = "project/functions/create_function.html"
submit_url = reverse_lazy("horizon:project:functions:create")
success_url = reverse_lazy("horizon:project:functions:index")
class UpdateFunctionView(forms.ModalFormView):
form_class = project_forms.UpdateFunctionForm
modal_header = submit_label = page_title = _("Update Function")
template_name = "project/functions/update_function.html"
submit_url = "horizon:project:functions:update"
success_url = reverse_lazy("horizon:project:functions:index")
@memoized.memoized_method
def get_object(self, *args, **kwargs):
function_id = self.kwargs['function_id']
try:
return api.qinling.function_get(self.request, function_id)
except Exception:
redirect = reverse("horizon:project:functions:index")
msg = _('Unable to retrieve function details.')
exceptions.handle(self.request, msg, redirect=redirect)
def get_context_data(self, **kwargs):
context = super(UpdateFunctionView, self).\
get_context_data(**kwargs)
function_id = self.kwargs['function_id']
context['function_id'] = function_id
context['submit_url'] = reverse(self.submit_url, args=(function_id,))
return context
def get_initial(self):
initial = super(UpdateFunctionView, self).get_initial()
func = self.get_object()
code = getattr(func, 'code', {})
code_swift = code.get('swift', {})
source = code.get('source', '')
initial.update({
'function_id': func.id,
'name': getattr(func, 'name', ''),
'description': getattr(func, 'description', ''),
'cpu': getattr(func, 'cpu', ''),
'memory_size': getattr(func, 'memory_size', ''),
'runtime_id': getattr(func, 'runtime_id', ''),
'entry': getattr(func, 'entry', ''),
'code_type': code.get('source', ''),
'swift_container': code_swift.get('container', ''),
'swift_object': code_swift.get('object', ''),
'image': code.get('image', ''),
'source': source,
})
return initial
class CreateFunctionVersionView(forms.ModalFormView):
form_class = project_forms.CreateFunctionVersionForm
modal_header = submit_label = page_title = _("Create Version")
template_name = "project/functions/create_version.html"
submit_url = "horizon:project:functions:create_version"
success_url = "horizon:project:functions:detail"
def get_success_url(self):
function_id = self.kwargs['function_id']
result = reverse(self.success_url, args=(function_id,))
result += "?tab=function_details__versions_of_this_function"
return result
def get_context_data(self, **kwargs):
context = super(CreateFunctionVersionView, self).\
get_context_data(**kwargs)
function_id = self.kwargs['function_id']
context['function_id'] = function_id
context['submit_url'] = reverse(self.submit_url, args=(function_id,))
return context
def get_initial(self):
initial = super(CreateFunctionVersionView, self).get_initial()
initial.update({
'function_id': self.kwargs["function_id"],
})
return initial
class IndexView(tables.DataTableView):
table_class = project_tables.FunctionsTable
page_title = _("Functions")
def get_data(self):
try:
functions = api.qinling.functions_list(self.request)
except Exception:
functions = []
msg = _('Unable to retrieve functions list.')
exceptions.handle(self.request, msg)
return functions
class DetailView(tabs.TabbedTableView):
tab_group_class = project_tabs.FunctionDetailTabs
template_name = 'project/functions/detail.html'
page_title = _("Function Details: {{ resource_name }}")
failure_url = reverse_lazy('horizon:project:functions:index')
@memoized.memoized_method
def _get_data(self):
function_id = self.kwargs['function_id']
try:
function = api.qinling.function_get(self.request, function_id)
except Exception:
exceptions.handle(
self.request,
_('Unable to retrieve '
'details for Function "%s".') % function_id,
redirect=self.failure_url
)
return function
def get_context_data(self, **kwargs):
context = super(DetailView, self).get_context_data(**kwargs)
func = self._get_data()
table = project_tables.FunctionsTable(self.request)
resource_name = func.name if func.name else func.id
context["function"] = func
context["url"] = self.get_redirect_url()
context["actions"] = table.render_row_actions(func)
context["table_id"] = "runtime"
context["resource_name"] = resource_name
context["object_id"] = func.id
return context
def _get_executions(self):
function_id = self.kwargs['function_id']
try:
executions = api.qinling.executions_list(self.request, function_id)
except Exception:
executions = []
messages.error(self.request, _(
'Unable to get executions of '
'this function "%s".') % function_id)
return executions
def _get_versions(self):
function_id = self.kwargs['function_id']
try:
versions = api.qinling.versions_list(self.request, function_id)
except Exception:
versions = []
messages.error(self.request, _(
'Unable to get versions of this function "%s".') % function_id)
return versions
def get_tabs(self, request, *args, **kwargs):
func = self._get_data()
executions = self._get_executions()
versions = self._get_versions()
return self.tab_group_class(request,
function=func,
executions=executions,
versions=versions,
**kwargs)
@staticmethod
def get_redirect_url():
return reverse('horizon:project:functions:index')
class VersionDetailView(tabs.TabView):
tab_group_class = project_tabs.FunctionVersionDetailTabs
template_name = 'project/functions/detail_version.html'
page_title = _("Function Version Details: "
"{{ version.function_id }} "
"(Version Number={{ version_number }})")
failure_url = reverse_lazy('horizon:project:functions:index')
def get_redirect_url(self, version_number=None):
function_id = self.kwargs['function_id']
if not version_number:
return reverse('horizon:project:functions:detail',
args=(function_id,))
return reverse('horizon:project:functions:version_detail',
args=(function_id, version_number))
@memoized.memoized_method
def get_data(self):
function_id = self.kwargs['function_id']
version_number = int(self.kwargs['version_number'])
try:
version = api.qinling.version_get(self.request,
function_id,
version_number)
except Exception:
exceptions.handle(
self.request,
_('Unable to retrieve details for '
'function version "%s".') % (function_id, version_number),
redirect=self.failure_url
)
return version
def get_context_data(self, **kwargs):
context = super(VersionDetailView, self).get_context_data(**kwargs)
version = self.get_data()
table = project_tables.FunctionVersionsTable(self.request)
context["version"] = version
context["url"] = self.get_redirect_url()
context["actions"] = table.render_row_actions(version)
context["table_id"] = "function_version"
context["object_id"] = version.version_number
return context
def get_tabs(self, request, *args, **kwargs):
version = self.get_data()
return self.tab_group_class(request,
version=version,
**kwargs)
def download_function(request, function_id):
try:
data = api.qinling.function_download(request, function_id)
output = io.BytesIO()
for chunk in data:
output.write(chunk)
ctx = output.getvalue()
response = HttpResponse(ctx, content_type='application/octet-stream')
response['Content-Length'] = str(len(response.content))
response['Content-Disposition'] = \
'attachment; filename=qinling-function-' + function_id + '.zip'
return response
except q_exc.NOT_FOUND as ne:
messages.error(request,
_('Error because file not found function: %s') % ne)
except Exception as e:
messages.error(request, _('Error downloading function: %s') % e)
return shortcuts.redirect(request.build_absolute_uri())

View File

@ -0,0 +1,22 @@
# 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 django.utils.translation import ugettext_lazy as _
import horizon
class Runtimes(horizon.Panel):
name = _("Runtimes")
slug = "runtimes"
permissions = ('openstack.services.function-engine',)

View File

@ -0,0 +1,81 @@
# 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 django.utils.translation import ugettext_lazy as _
from horizon import tables
from qinling_dashboard import api
from qinling_dashboard import utils
class RuntimesFilterAction(tables.FilterAction):
def filter(self, table, runtimes, filter_string):
"""Naive case-insensitive search."""
q = filter_string.lower()
return [runtime for runtime in runtimes
if q in runtime.name.lower()]
class UpdateRow(tables.Row):
ajax = True
def get_data(self, request, runtime_id):
execution = api.qinling.runtime_get(request, runtime_id)
return execution
class RuntimesTable(tables.DataTable):
id = tables.Column("id",
verbose_name=_("Id"),
link="horizon:project:runtimes:detail")
name = tables.Column("name",
verbose_name=_("Name"))
image = tables.Column("image",
verbose_name=_("Image"))
created_at = tables.Column("created_at",
verbose_name=_("Created At"))
updated_at = tables.Column("updated_at",
verbose_name=_("Updated At"))
project_id = tables.Column("project_id",
verbose_name=_("Project ID"))
is_public = tables.Column("is_public",
verbose_name=_("Is Public"))
status = tables.Column(
"status",
status=True,
status_choices=utils.FUNCTION_ENGINE_STATUS_CHOICES,
display_choices=utils.FUNCTION_ENGINE_STATUS_DISPLAY_CHOICES)
def __init__(self, request, data=None, needs_form_wrapper=None, **kwargs):
super(RuntimesTable, self).__init__(request, data,
needs_form_wrapper, **kwargs)
if not request.user.is_superuser:
del self.columns["is_public"]
class Meta(object):
name = "runtimes"
verbose_name = _("Runtimes")
status_columns = ["status"]
multi_select = True
row_class = UpdateRow
table_actions = (RuntimesFilterAction,)

View File

@ -0,0 +1,37 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import logging
from django.utils.translation import ugettext_lazy as _
from horizon import tabs
LOG = logging.getLogger(__name__)
class RuntimeOverviewTab(tabs.Tab):
name = _("Overview")
slug = "overview"
template_name = "project/runtimes/_detail_overview.html"
def get_context_data(self, request):
return {"runtime": self.tab_group.kwargs['runtime'],
"is_superuser": self.request.user.is_superuser}
class RuntimeDetailTabs(tabs.TabGroup):
slug = "runtime_details"
tabs = (RuntimeOverviewTab,)
sticky = True
show_single_tab = False

View File

@ -0,0 +1,38 @@
{% load i18n %}
<div class="detail">
<dl class="dl-horizontal">
<dt>{% trans "ID" %}</dt>
<dd>{{ runtime.id }}</dd>
<dt>{% trans "Name" %}</dt>
<dd data-display="{{ runtime.name }}">{{ runtime.name }}</dd>
<dt>{% trans "Image" %}</dt>
<dd>{{ runtime.image }}</dd>
<dt>{% trans "Description" %}</dt>
<dd>{{ runtime.description }}</dd>
<dt>{% trans "Project ID" %}</dt>
<dd>{{ runtime.project_id }}</dd>
<dt>{% trans "Created At" %}</dt>
<dd>{{ runtime.created_at|parse_isotime }}</dd>
<dt>{% trans "Updated At" %}</dt>
<dd>{{ runtime.updated_at|parse_isotime }}</dd>
{% if is_superuser %}
<dt>{% trans "Is Public" %}</dt>
<dd>{{ runtime.is_public|default:_("Unknown") }}</dd>
{% endif %}
<dt>{% trans "Status" %}</dt>
{% with runtime.status|title as rt_status %}
<dd>{% trans rt_status context "current status of a runtime" %}</dd>
{% endwith %}
</dl>
</div>

View File

@ -0,0 +1,11 @@
{% extends 'base.html' %}
{% load i18n %}
{% block title %}{% trans "Runtime Details" %}{% endblock %}
{% block main %}
<div class="row">
<div class="col-sm-12">
{{ tab_group.render }}
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,21 @@
# 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 django.conf.urls import url
from qinling_dashboard.content.runtimes import views
urlpatterns = [
url(r'^$', views.IndexView.as_view(), name='index'),
url(r'^(?P<runtime_id>[^/]+)/$',
views.DetailView.as_view(), name='detail'),
]

View File

@ -0,0 +1,83 @@
# 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 django.urls import reverse
from django.urls import reverse_lazy
from django.utils.translation import ugettext_lazy as _
from horizon import exceptions
from horizon import tables
from horizon import tabs
from horizon.utils import memoized
from qinling_dashboard import api
from qinling_dashboard.content.runtimes import tables as project_tables
from qinling_dashboard.content.runtimes import tabs as project_tabs
class IndexView(tables.DataTableView):
table_class = project_tables.RuntimesTable
page_title = _("Runtimes")
def get_data(self):
try:
runtimes = api.qinling.runtimes_list(self.request)
except Exception:
runtimes = []
msg = _('Unable to retrieve runtimes list.')
exceptions.handle(self.request, msg)
return runtimes
class DetailView(tabs.TabView):
tab_group_class = project_tabs.RuntimeDetailTabs
template_name = 'project/runtimes/detail.html'
page_title = _("Runtime Details: {{ resource_name }}")
failure_url = reverse_lazy('horizon:project:runtimes:index')
@memoized.memoized_method
def get_data(self):
runtime_id = self.kwargs['runtime_id']
try:
runtime = api.qinling.runtime_get(self.request, runtime_id)
except Exception:
exceptions.handle(
self.request,
_('Unable to retrieve '
'details for Runtime "%s".') % runtime_id,
redirect=self.failure_url
)
return runtime
def get_context_data(self, **kwargs):
context = super(DetailView, self).get_context_data(**kwargs)
runtime = self.get_data()
table = project_tables.RuntimesTable(self.request)
resource_name = runtime.name if runtime.name else runtime.id
context["runtime"] = runtime
context["url"] = self.get_redirect_url()
context["actions"] = table.render_row_actions(runtime)
context["table_id"] = "runtime"
context["resource_name"] = resource_name
context["object_id"] = runtime.id
return context
def get_tabs(self, request, *args, **kwargs):
runtime = self.get_data()
return self.tab_group_class(request,
runtime=runtime,
**kwargs)
@staticmethod
def get_redirect_url():
return reverse('horizon:project:runtimes:index')

View File

@ -0,0 +1,30 @@
# 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 django.utils.translation import ugettext_lazy as _
from qinling_dashboard import exceptions
# The slug of the panel group to be added to HORIZON_CONFIG. Required.
PANEL_GROUP = 'function_engine'
# The display name of the PANEL_GROUP. Required.
PANEL_GROUP_NAME = _('Function Engine')
# The slug of the dashboard the PANEL_GROUP associated with. Required.
PANEL_GROUP_DASHBOARD = 'project'
ADD_INSTALLED_APPS = ["qinling_dashboard", ]
ADD_EXCEPTIONS = {
'not_found': exceptions.NOT_FOUND,
'recoverable': exceptions.RECOVERABLE,
'unauthorized': exceptions.UNAUTHORIZED
}

View File

@ -0,0 +1,24 @@
# 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.
# The slug of the panel to be added to HORIZON_CONFIG. Required.
PANEL = 'runtimes'
# The slug of the dashboard the PANEL associated with. Required.
PANEL_DASHBOARD = 'project'
# The slug of the panel group the PANEL is associated with.
PANEL_GROUP = 'function_engine'
# Python panel class of the PANEL to be added.
ADD_PANEL = 'qinling_dashboard.content.runtimes.panel.Runtimes'
# Automatically discover static resources in installed apps
AUTO_DISCOVER_STATIC_FILES = True

View File

@ -0,0 +1,24 @@
# 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.
# The slug of the panel to be added to HORIZON_CONFIG. Required.
PANEL = 'functions'
# The slug of the dashboard the PANEL associated with. Required.
PANEL_DASHBOARD = 'project'
# The slug of the panel group the PANEL is associated with.
PANEL_GROUP = 'function_engine'
# Python panel class of the PANEL to be added.
ADD_PANEL = 'qinling_dashboard.content.functions.panel.Functions'
# Automatically discover static resources in installed apps
AUTO_DISCOVER_STATIC_FILES = True

View File

@ -0,0 +1,24 @@
# 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.
# The slug of the panel to be added to HORIZON_CONFIG. Required.
PANEL = 'executions'
# The slug of the dashboard the PANEL associated with. Required.
PANEL_DASHBOARD = 'project'
# The slug of the panel group the PANEL is associated with.
PANEL_GROUP = 'function_engine'
# Python panel class of the PANEL to be added.
ADD_PANEL = 'qinling_dashboard.content.executions.panel.Executions'
# Automatically discover static resources in installed apps
AUTO_DISCOVER_STATIC_FILES = True

View File

View File

@ -0,0 +1,28 @@
# 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 qinlingclient.common import exceptions as exc
UNAUTHORIZED = (
exc.Unauthorized,
)
NOT_FOUND = (
exc.NotFound,
)
RECOVERABLE = (
exc.HTTPException,
)

View File

View File

@ -0,0 +1,554 @@
# Copyright 2012 United States Government as represented by the
# Administrator of the National Aeronautics and Space Administration.
# All Rights Reserved.
#
# Copyright 2012 Nebula, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from functools import wraps
from importlib import import_module
import logging
import traceback
from django.conf import settings
from django.contrib.messages.storage import default_storage
from django.core.handlers import wsgi
from django.test.client import RequestFactory
from django.utils import http
from keystoneclient.v2_0 import client as keystone_client
from qinlingclient import client as qinling_client
# As of Rocky, we are in the process of removing mox usage.
# To allow mox-free horizon plugins to consume the test helper,
# mox import is now optional. If tests depends on mox,
# mox (or mox3) must be declared in test-requirements.txt.
import mock
try:
from mox3 import mox # noqa: F401
except ImportError:
pass
from openstack_auth import user
from openstack_auth import utils
from requests.packages.urllib3.connection import HTTPConnection
import six
from six import moves
from horizon.test import helpers as horizon_helpers
from openstack_dashboard import api as project_api
from openstack_dashboard import context_processors
from qinling_dashboard.test.test_data import utils as test_utils
LOG = logging.getLogger(__name__)
# Makes output of failing mox tests much easier to read.
wsgi.WSGIRequest.__repr__ = lambda self: "<class 'django.http.HttpRequest'>"
# Shortcuts to avoid importing horizon_helpers and for backward compatibility.
update_settings = horizon_helpers.update_settings
IsA = horizon_helpers.IsA
IsHttpRequest = horizon_helpers.IsHttpRequest
def create_stubs(stubs_to_create=None):
"""decorator to simplify setting up multiple stubs at once via mox
:param stubs_to_create: methods to stub in one or more modules
:type stubs_to_create: dict
The keys are python paths to the module containing the methods to mock.
To mock a method in openstack_dashboard/api/nova.py, the key is::
api.nova
The values are either a tuple or list of methods to mock in the module
indicated by the key.
For example::
('server_list',)
-or-
('flavor_list', 'server_list',)
-or-
['flavor_list', 'server_list']
Additionally, multiple modules can be mocked at once::
{
api.nova: ('flavor_list', 'server_list'),
api.glance: ('image_list_detailed',),
}
"""
if stubs_to_create is None:
stubs_to_create = {}
if not isinstance(stubs_to_create, dict):
raise TypeError("create_stub must be passed a dict, but a %s was "
"given." % type(stubs_to_create).__name__)
def inner_stub_out(fn):
@wraps(fn)
def instance_stub_out(self, *args, **kwargs):
for key in stubs_to_create:
if not (isinstance(stubs_to_create[key], tuple) or
isinstance(stubs_to_create[key], list)):
raise TypeError("The values of the create_stub "
"dict must be lists or tuples, but "
"is a %s."
% type(stubs_to_create[key]).__name__)
for value in stubs_to_create[key]:
self.mox.StubOutWithMock(key, value)
return fn(self, *args, **kwargs)
return instance_stub_out
return inner_stub_out
def create_mocks(target_methods):
"""decorator to simplify setting up multiple mocks at once
:param target_methods: a dict to define methods to be patched using mock.
A key of "target_methods" is a target object whose attribute(s) are
patched.
A value of "target_methods" is a list of methods to be patched
using mock. Each element of the list can be a string or a tuple
consisting of two strings.
A string specifies a method name of "target" object to be mocked.
The decorator create a mock object for the method and the started mock
can be accessed via 'mock_<method-name>' of the test class.
For example, in case of::
@create_mocks({api.nova: ['server_list',
'flavor_list']})
def test_example(self):
...
self.mock_server_list.return_value = ...
self.mock_flavar_list.side_effect = ...
you can access the mocked method via "self.mock_server_list"
inside a test class.
The tuple version is useful when there are multiple methods with
a same name are mocked in a single test.
The format of the tuple is::
("<method-name-to-be-mocked>", "<attr-name>")
The decorator create a mock object for "<method-name-to-be-mocked>"
and the started mock can be accessed via 'mock_<attr-name>' of
the test class.
Example::
@create_mocks({
api.nova: [
'usage_get',
('tenant_absolute_limits', 'nova_tenant_absolute_limits'),
'extension_supported',
],
api.cinder: [
('tenant_absolute_limits', 'cinder_tenant_absolute_limits'),
],
})
def test_example(self):
...
self.mock_usage_get.return_value = ...
self.mock_nova_tenant_absolute_limits.return_value = ...
self.mock_cinder_tenant_absolute_limits.return_value = ...
...
self.mock_extension_supported.assert_has_calls(....)
"""
def wrapper(function):
@wraps(function)
def wrapped(inst, *args, **kwargs):
for target, methods in target_methods.items():
for method in methods:
if isinstance(method, str):
method_mocked = method
attr_name = method
else:
method_mocked = method[0]
attr_name = method[1]
m = mock.patch.object(target, method_mocked)
setattr(inst, 'mock_%s' % attr_name, m.start())
return function(inst, *args, **kwargs)
return wrapped
return wrapper
def _apply_panel_mocks(patchers=None):
"""Global mocks on panels that get called on all views."""
if patchers is None:
patchers = {}
mocked_methods = getattr(settings, 'TEST_GLOBAL_MOCKS_ON_PANELS', {})
for name, mock_config in mocked_methods.items():
method = mock_config['method']
mock_params = {}
for param in ['return_value', 'side_effect']:
if param in mock_config:
mock_params[param] = mock_config[param]
patcher = mock.patch(method, **mock_params)
patcher.start()
patchers[name] = patcher
return patchers
class RequestFactoryWithMessages(RequestFactory):
def get(self, *args, **kwargs):
req = super(RequestFactoryWithMessages, self).get(*args, **kwargs)
req.user = utils.get_user(req)
req.session = []
req._messages = default_storage(req)
return req
def post(self, *args, **kwargs):
req = super(RequestFactoryWithMessages, self).post(*args, **kwargs)
req.user = utils.get_user(req)
req.session = []
req._messages = default_storage(req)
return req
class TestCase(horizon_helpers.TestCase):
"""Specialized base test case class for Horizon.
It gives access to numerous additional features:
* A full suite of test data through various attached objects and
managers (e.g. ``self.servers``, ``self.user``, etc.). See the
docs for
:class:`~openstack_dashboard.test.test_data.utils.TestData`
for more information.
* The ``mox`` mocking framework via ``self.mox``.
if ``use_mox`` attribute is set to True.
* A set of request context data via ``self.context``.
* A ``RequestFactory`` class which supports Django's ``contrib.messages``
framework via ``self.factory``.
* A ready-to-go request object via ``self.request``.
* The ability to override specific time data controls for easier testing.
* Several handy additional assertion methods.
"""
# To force test failures when unmocked API calls are attempted, provide
# boolean variable to store failures
missing_mocks = False
def fake_conn_request(self):
# print a stacktrace to illustrate where the unmocked API call
# is being made from
traceback.print_stack()
# forcing a test failure for missing mock
self.missing_mocks = True
def setUp(self):
self._real_conn_request = HTTPConnection.connect
HTTPConnection.connect = self.fake_conn_request
self._real_context_processor = context_processors.openstack
context_processors.openstack = lambda request: self.context
self.patchers = _apply_panel_mocks()
super(TestCase, self).setUp()
def _setup_test_data(self):
super(TestCase, self)._setup_test_data()
test_utils.load_test_data(self)
self.context = {
'authorized_tenants': self.tenants.list(),
'JS_CATALOG': context_processors.get_js_catalog(settings),
}
def _setup_factory(self):
# For some magical reason we need a copy of this here.
self.factory = RequestFactoryWithMessages()
def _setup_user(self, **kwargs):
self._real_get_user = utils.get_user
tenants = self.context['authorized_tenants']
base_kwargs = {
'id': self.user.id,
'token': self.token,
'username': self.user.name,
'domain_id': self.domain.id,
'user_domain_name': self.domain.name,
'tenant_id': self.tenant.id,
'service_catalog': self.service_catalog,
'authorized_tenants': tenants
}
base_kwargs.update(kwargs)
self.setActiveUser(**base_kwargs)
def _setup_request(self):
super(TestCase, self)._setup_request()
self.request.session['token'] = self.token.id
def tearDown(self):
HTTPConnection.connect = self._real_conn_request
context_processors.openstack = self._real_context_processor
utils.get_user = self._real_get_user
mock.patch.stopall()
super(TestCase, self).tearDown()
# cause a test failure if an unmocked API call was attempted
if self.missing_mocks:
raise AssertionError("An unmocked API call was made.")
def setActiveUser(self, id=None, token=None, username=None, tenant_id=None,
service_catalog=None, tenant_name=None, roles=None,
authorized_tenants=None, enabled=True, domain_id=None,
user_domain_name=None):
def get_user(request):
return user.User(id=id,
token=token,
user=username,
domain_id=domain_id,
user_domain_name=user_domain_name,
tenant_id=tenant_id,
tenant_name=tenant_name,
service_catalog=service_catalog,
roles=roles,
enabled=enabled,
authorized_tenants=authorized_tenants,
endpoint=settings.OPENSTACK_KEYSTONE_URL)
utils.get_user = get_user
def assertRedirectsNoFollow(self, response, expected_url):
"""Check for redirect.
Asserts that the given response issued a 302 redirect without
processing the view which is redirected to.
"""
loc = six.text_type(response._headers.get('location', None)[1])
loc = http.urlunquote(loc)
expected_url = http.urlunquote(expected_url)
self.assertEqual(loc, expected_url)
self.assertEqual(response.status_code, 302)
def assertNoFormErrors(self, response, context_name="form"):
"""Checks for no form errors.
Asserts that the response either does not contain a form in its
context, or that if it does, that form has no errors.
"""
context = getattr(response, "context", {})
if not context or context_name not in context:
return True
errors = response.context[context_name]._errors
assert len(errors) == 0, \
"Unexpected errors were found on the form: %s" % errors
def assertFormErrors(self, response, count=0, message=None,
context_name="form"):
"""Check for form errors.
Asserts that the response does contain a form in its
context, and that form has errors, if count were given,
it must match the exact numbers of errors
"""
context = getattr(response, "context", {})
assert (context and context_name in context), \
"The response did not contain a form."
errors = response.context[context_name]._errors
if count:
assert len(errors) == count, \
"%d errors were found on the form, %d expected" % \
(len(errors), count)
if message and message not in six.text_type(errors):
self.fail("Expected message not found, instead found: %s"
% ["%s: %s" % (key, [e for e in field_errors]) for
(key, field_errors) in errors.items()])
else:
assert len(errors) > 0, "No errors were found on the form"
def assertStatusCode(self, response, expected_code):
"""Validates an expected status code.
Matches camel case of other assert functions
"""
if response.status_code == expected_code:
return
self.fail('status code %r != %r: %s' % (response.status_code,
expected_code,
response.content))
def assertItemsCollectionEqual(self, response, items_list):
self.assertEqual(response.json, {"items": items_list})
def getAndAssertTableRowAction(self, response, table_name,
action_name, row_id):
table = response.context[table_name + '_table']
rows = list(moves.filter(lambda x: x.id == row_id,
table.data))
self.assertEqual(1, len(rows),
"Did not find a row matching id '%s'" % row_id)
row_actions = table.get_row_actions(rows[0])
actions = list(moves.filter(lambda x: x.name == action_name,
row_actions))
msg_args = (action_name, table_name, row_id)
self.assertGreater(
len(actions), 0,
"No action named '%s' found in '%s' table for id '%s'" % msg_args)
self.assertEqual(
1, len(actions),
"Multiple actions named '%s' found in '%s' table for id '%s'"
% msg_args)
return actions[0]
def getAndAssertTableAction(self, response, table_name, action_name):
table = response.context[table_name + '_table']
table_actions = table.get_table_actions()
actions = list(moves.filter(lambda x: x.name == action_name,
table_actions))
msg_args = (action_name, table_name)
self.assertGreater(
len(actions), 0,
"No action named '%s' found in '%s' table" % msg_args)
self.assertEqual(
1, len(actions),
"More than one action named '%s' found in '%s' table" % msg_args)
return actions[0]
@staticmethod
def mock_rest_request(**args):
mock_args = {
'user.is_authenticated': True,
'is_ajax.return_value': True,
'policy.check.return_value': True,
'body': ''
}
mock_args.update(args)
return mock.Mock(**mock_args)
def assert_mock_multiple_calls_with_same_arguments(
self, mocked_method, count, expected_call):
self.assertEqual(count, mocked_method.call_count)
mocked_method.assert_has_calls([expected_call] * count)
class BaseAdminViewTests(TestCase):
"""Sets an active user with the "admin" role.
For testing admin-only views and functionality.
"""
def setActiveUser(self, *args, **kwargs):
if "roles" not in kwargs:
kwargs['roles'] = [self.roles.admin._info]
super(BaseAdminViewTests, self).setActiveUser(*args, **kwargs)
def setSessionValues(self, **kwargs):
settings.SESSION_ENGINE = 'django.contrib.sessions.backends.file'
engine = import_module(settings.SESSION_ENGINE)
store = engine.SessionStore()
for key in kwargs:
store[key] = kwargs[key]
self.request.session[key] = kwargs[key]
store.save()
self.session = store
self.client.cookies[settings.SESSION_COOKIE_NAME] = store.session_key
class APITestCase(TestCase):
"""Testing APIs.
For use with tests which deal with the underlying clients rather than
stubbing out the openstack_dashboard.api.* methods.
"""
# NOTE: This test class depends on mox but does not declare use_mox = True
# to notify mox is no longer recommended.
# If a consumer would like to use this class, declare use_mox = True.
def setUp(self):
super(APITestCase, self).setUp()
LOG.warning("APITestCase has been deprecated in favor of mock usage "
"and will be removed at the beginning of 'Stein' release. "
"Please convert your to use APIMockTestCase instead.")
utils.patch_middleware_get_user()
def fake_keystoneclient(request, admin=False):
"""Returns the stub keystoneclient.
Only necessary because the function takes too many arguments to
conveniently be a lambda.
"""
return self.stub_keystoneclient()
self._original_keystoneclient = project_api.keystone.keystoneclient
project_api.keystone.keystoneclient = fake_keystoneclient
def tearDown(self):
super(APITestCase, self).tearDown()
project_api.keystone.keystoneclient = self._original_keystoneclient
def _warn_client(self, service, removal_version):
LOG.warning(
"APITestCase has been deprecated for %(service)s-related "
"tests and will be removerd in '%(removal_version)s' release. "
"Please convert your to use APIMockTestCase instead.",
{'service': service, 'removal_version': removal_version}
)
def stub_keystoneclient(self):
self._warn_client('keystone', 'Stein')
if not hasattr(self, "keystoneclient"):
self.mox.StubOutWithMock(keystone_client, 'Client')
# NOTE(saschpe): Mock properties, MockObject.__init__ ignores them:
keystone_client.Client.auth_token = 'foo'
keystone_client.Client.service_catalog = None
keystone_client.Client.tenant_id = '1'
keystone_client.Client.tenant_name = 'tenant_1'
keystone_client.Client.management_url = ""
keystone_client.Client.__dir__ = lambda: []
self.keystoneclient = self.mox.CreateMock(keystone_client.Client)
return self.keystoneclient
def stub_qinlingclient(self):
if not hasattr(self, "qinlingclient"):
self.mox.StubOutWithMock(qinling_client, 'Client')
self.qinlingclient = self.mox.CreateMock(qinling_client.Client)
return self.qinlingclient
class APIMockTestCase(TestCase):
def setUp(self):
super(APIMockTestCase, self).setUp()
utils.patch_middleware_get_user()
def mock_obj_to_dict(r):
return mock.Mock(**{'to_dict.return_value': r})
def mock_factory(r):
"""mocks all the attributes as well as the to_dict """
mocked = mock_obj_to_dict(r)
mocked.configure_mock(**r)
return mocked

View File

@ -0,0 +1,39 @@
#
# 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.
# Default to Horizons test settings to avoid any missing keys
import openstack_dashboard.enabled # noqa: F811
from openstack_dashboard.test.settings import * # noqa: F403,H303
from openstack_dashboard.utils import settings
import qinling_dashboard.enabled
# pop these keys to avoid log warnings about deprecation
# update_dashboards will populate them anyway
HORIZON_CONFIG.pop('dashboards', None)
HORIZON_CONFIG.pop('default_dashboard', None)
# Update the dashboards with heat_dashboard enabled files
# and current INSTALLED_APPS
settings.update_dashboards(
[
openstack_dashboard.enabled,
qinling_dashboard.enabled,
],
HORIZON_CONFIG,
INSTALLED_APPS
)
# Remove duplicated apps
INSTALLED_APPS = list(set(INSTALLED_APPS))

View File

@ -0,0 +1,56 @@
# Copyright 2012 Nebula, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import qinlingclient.common.exceptions as qinling_exceptions
import six
from qinling_dashboard.test.test_data import utils
def create_stubbed_exception(cls, status_code=500):
msg = "Expected failure."
def fake_init_exception(self, code=None, message=None, **kwargs):
if code is not None:
if hasattr(self, 'http_status'):
self.http_status = code
else:
self.code = code
self.message = message or self.__class__.message
try:
# Neutron sometimes updates the message with additional
# information, like a reason.
self.message = self.message % kwargs
except Exception:
pass # We still have the main error message.
def fake_str(self):
return str(self.message)
def fake_unicode(self):
return six.text_type(self.message)
cls.__init__ = fake_init_exception
cls.__str__ = fake_str
cls.__unicode__ = fake_unicode
cls.silence_logging = True
return cls(status_code, msg)
def data(TEST):
TEST.exceptions = utils.TestDataContainer()
qinling_exception = qinling_exceptions.HTTPException
TEST.exceptions.qinling = create_stubbed_exception(qinling_exception)

View File

@ -0,0 +1,339 @@
# Copyright 2012 Nebula, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import copy
from datetime import timedelta
from django.conf import settings
from django.utils import datetime_safe
from keystoneclient import access
from keystoneclient.v2_0 import tenants
from keystoneclient.v2_0 import users
from keystoneclient.v3.contrib.federation import identity_providers
from keystoneclient.v3.contrib.federation import mappings
from keystoneclient.v3.contrib.federation import protocols
from keystoneclient.v3 import domains
from openstack_auth import user as auth_user
from qinling_dashboard.test.test_data import utils
# Dummy service catalog with all service.
# All endpoint URLs should point to example.com.
# Try to keep them as accurate to real data as possible (ports, URIs, etc.)
SERVICE_CATALOG = [
{"type": "compute",
"name": "nova",
"endpoints_links": [],
"endpoints": [
{"region": "RegionOne",
"adminURL": "http://admin.nova.example.com:8774/v2",
"internalURL": "http://int.nova.example.com:8774/v2",
"publicURL": "http://public.nova.example.com:8774/v2"},
{"region": "RegionTwo",
"adminURL": "http://admin.nova2.example.com:8774/v2",
"internalURL": "http://int.nova2.example.com:8774/v2",
"publicURL": "http://public.nova2.example.com:8774/v2"}]},
{"type": "volumev2",
"name": "cinderv2",
"endpoints_links": [],
"endpoints": [
{"region": "RegionOne",
"adminURL": "http://admin.nova.example.com:8776/v2",
"internalURL": "http://int.nova.example.com:8776/v2",
"publicURL": "http://public.nova.example.com:8776/v2"},
{"region": "RegionTwo",
"adminURL": "http://admin.nova.example.com:8776/v2",
"internalURL": "http://int.nova.example.com:8776/v2",
"publicURL": "http://public.nova.example.com:8776/v2"}]},
{"type": "image",
"name": "glance",
"endpoints_links": [],
"endpoints": [
{"region": "RegionOne",
"adminURL": "http://admin.glance.example.com:9292",
"internalURL": "http://int.glance.example.com:9292",
"publicURL": "http://public.glance.example.com:9292"}]},
{"type": "identity",
"name": "keystone",
"endpoints_links": [],
"endpoints": [
{"region": "RegionOne",
"adminURL": "http://admin.keystone.example.com:35357/v2.0",
"internalURL": "http://int.keystone.example.com:5000/v2.0",
"publicURL": "http://public.keystone.example.com:5000/v2.0"}]},
{"type": "object-store",
"name": "swift",
"endpoints_links": [],
"endpoints": [
{"region": "RegionOne",
"adminURL": "http://admin.swift.example.com:8080/",
"internalURL": "http://int.swift.example.com:8080/",
"publicURL": "http://public.swift.example.com:8080/"}]},
{"type": "network",
"name": "neutron",
"endpoints_links": [],
"endpoints": [
{"region": "RegionOne",
"adminURL": "http://admin.neutron.example.com:9696/",
"internalURL": "http://int.neutron.example.com:9696/",
"publicURL": "http://public.neutron.example.com:9696/"}]},
{"type": "ec2",
"name": "EC2 Service",
"endpoints_links": [],
"endpoints": [
{"region": "RegionOne",
"adminURL": "http://admin.nova.example.com:8773/services/Admin",
"publicURL": "http://public.nova.example.com:8773/services/Cloud",
"internalURL": "http://int.nova.example.com:8773/services/Cloud"}]},
{"type": "function-engine",
"name": "qinling",
"endpoints_links": [],
"endpoints": [
{"region": "RegionOne",
"adminURL": "http://admin.heat.example.com:7070/v1",
"publicURL": "http://public.heat.example.com:7070/v1",
"internalURL": "http://int.heat.example.com:7070/v1"}]}
]
def data(TEST):
# Make a deep copy of the catalog to avoid persisting side-effects
# when tests modify the catalog.
TEST.service_catalog = copy.deepcopy(SERVICE_CATALOG)
TEST.tokens = utils.TestDataContainer()
TEST.domains = utils.TestDataContainer()
TEST.users = utils.TestDataContainer()
# TEST.groups = utils.TestDataContainer()
TEST.tenants = utils.TestDataContainer()
# TEST.role_assignments = utils.TestDataContainer()
# TEST.roles = utils.TestDataContainer()
# TEST.ec2 = utils.TestDataContainer()
TEST.identity_providers = utils.TestDataContainer()
TEST.idp_mappings = utils.TestDataContainer()
TEST.idp_protocols = utils.TestDataContainer()
# admin_role_dict = {'id': '1',
# 'name': 'admin'}
# admin_role = roles.Role(roles.RoleManager, admin_role_dict, loaded=True)
member_role_dict = {'id': "2",
'name': settings.OPENSTACK_KEYSTONE_DEFAULT_ROLE}
# member_role = roles.Role(roles.RoleManager,
# member_role_dict, loaded=True)
# TEST.roles.add(admin_role, member_role)
# TEST.roles.admin = admin_role
# TEST.roles.member = member_role
domain_dict = {'id': "1",
'name': 'test_domain',
'description': "a test domain.",
'enabled': True}
domain_dict_2 = {'id': "2",
'name': 'disabled_domain',
'description': "a disabled test domain.",
'enabled': False}
domain_dict_3 = {'id': "3",
'name': 'another_test_domain',
'description': "another test domain.",
'enabled': True}
domain = domains.Domain(domains.DomainManager, domain_dict)
disabled_domain = domains.Domain(domains.DomainManager, domain_dict_2)
another_domain = domains.Domain(domains.DomainManager, domain_dict_3)
TEST.domains.add(domain, disabled_domain, another_domain)
TEST.domain = domain # Your "current" domain
user_dict = {'id': "1",
'name': 'test_user',
'description': 'test_description',
'email': 'test@example.com',
'password': 'password',
'token': 'test_token',
'project_id': '1',
'enabled': True,
'domain_id': "1"}
user = users.User(None, user_dict)
user_dict = {'id': "2",
'name': 'user_two',
'description': 'test_description',
'email': 'two@example.com',
'password': 'password',
'token': 'test_token',
'project_id': '1',
'enabled': True,
'domain_id': "1"}
user2 = users.User(None, user_dict)
user_dict = {'id': "3",
'name': 'user_three',
'description': 'test_description',
'email': 'three@example.com',
'password': 'password',
'token': 'test_token',
'project_id': '1',
'enabled': True,
'domain_id': "1"}
user3 = users.User(None, user_dict)
user_dict = {'id': "4",
'name': 'user_four',
'description': 'test_description',
'email': 'four@example.com',
'password': 'password',
'token': 'test_token',
'project_id': '2',
'enabled': True,
'domain_id': "2"}
user4 = users.User(None, user_dict)
user_dict = {'id': "5",
'name': 'user_five',
'description': 'test_description',
'email': None,
'password': 'password',
'token': 'test_token',
'project_id': '2',
'enabled': True,
'domain_id': "1"}
user5 = users.User(None, user_dict)
TEST.users.add(user, user2, user3, user4, user5)
TEST.user = user # Your "current" user
TEST.user.service_catalog = copy.deepcopy(SERVICE_CATALOG)
tenant_dict = {'id': "1",
'name': 'test_tenant',
'description': "a test tenant.",
'enabled': True,
'domain_id': '1',
'domain_name': 'test_domain'}
tenant_dict_2 = {'id': "2",
'name': 'disabled_tenant',
'description': "a disabled test tenant.",
'enabled': False,
'domain_id': '2',
'domain_name': 'disabled_domain'}
tenant_dict_3 = {'id': "3",
'name': u'\u4e91\u89c4\u5219',
'description': "an unicode-named tenant.",
'enabled': True,
'domain_id': '2',
'domain_name': 'disabled_domain'}
tenant = tenants.Tenant(tenants.TenantManager, tenant_dict)
disabled_tenant = tenants.Tenant(tenants.TenantManager, tenant_dict_2)
tenant_unicode = tenants.Tenant(tenants.TenantManager, tenant_dict_3)
TEST.tenants.add(tenant, disabled_tenant, tenant_unicode)
TEST.tenant = tenant # Your "current" tenant
tomorrow = datetime_safe.datetime.now() + timedelta(days=1)
expiration = tomorrow.isoformat()
scoped_token_dict = {
'access': {
'token': {
'id': "test_token_id",
'expires': expiration,
'tenant': tenant_dict,
'tenants': [tenant_dict]},
'user': {
'id': "test_user_id",
'name': "test_user",
'roles': [member_role_dict]},
'serviceCatalog': TEST.service_catalog
}
}
scoped_access_info = access.AccessInfo.factory(resp=None,
body=scoped_token_dict)
unscoped_token_dict = {
'access': {
'token': {
'id': "test_token_id",
'expires': expiration},
'user': {
'id': "test_user_id",
'name': "test_user",
'roles': [member_role_dict]},
'serviceCatalog': TEST.service_catalog
}
}
unscoped_access_info = access.AccessInfo.factory(resp=None,
body=unscoped_token_dict)
scoped_token = auth_user.Token(scoped_access_info)
unscoped_token = auth_user.Token(unscoped_access_info)
TEST.tokens.add(scoped_token, unscoped_token)
TEST.token = scoped_token # your "current" token.
TEST.tokens.scoped_token = scoped_token
TEST.tokens.unscoped_token = unscoped_token
idp_dict_1 = {'id': 'idp_1',
'description': 'identity provider 1',
'enabled': True,
'remote_ids': ['rid_1', 'rid_2']}
idp_1 = identity_providers.IdentityProvider(
identity_providers.IdentityProviderManager,
idp_dict_1, loaded=True)
idp_dict_2 = {'id': 'idp_2',
'description': 'identity provider 2',
'enabled': True,
'remote_ids': ['rid_3', 'rid_4']}
idp_2 = identity_providers.IdentityProvider(
identity_providers.IdentityProviderManager,
idp_dict_2, loaded=True)
TEST.identity_providers.add(idp_1, idp_2)
idp_mapping_dict = {
"id": "mapping_1",
"rules": [
{
"local": [
{
"user": {
"name": "{0}"
}
},
{
"group": {
"id": "0cd5e9"
}
}
],
"remote": [
{
"type": "UserName"
},
{
"type": "orgPersonType",
"not_any_of": [
"Contractor",
"Guest"
]
}
]
}
]
}
idp_mapping = mappings.Mapping(
mappings.MappingManager(None),
idp_mapping_dict)
TEST.idp_mappings.add(idp_mapping)
idp_protocol_dict_1 = {'id': 'protocol_1',
'mapping_id': 'mapping_1'}
idp_protocol = protocols.Protocol(
protocols.ProtocolManager,
idp_protocol_dict_1,
loaded=True)
TEST.idp_protocols.add(idp_protocol)

View File

@ -0,0 +1,104 @@
# 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 qinlingclient.v1 import function
from qinlingclient.v1 import function_execution
from qinlingclient.v1 import function_version
from qinlingclient.v1 import runtime
from qinling_dashboard.test.test_data import utils
PROJECT_ID = "87173d0c07d547bfa1343cabe2e6fe69"
RUNTIME_ID_BASE = "6cb1f505-9a42-4569-a94d-9c2f4b7e7b4{0}"
FUNCTION_ID_BASE = "aacb5032-f8b9-4ad5-8fd4-8017d0d1c54{0}"
EXECUTION_ID_BASE = "f0b5d7f5-d1f1-4f3e-9080-16e4cd918f9{0}"
VERSION_ID_BASE = "30c3e142-2850-4a89-90a7-0a4e4d654f{0}{1}"
def data(TEST):
TEST.runtimes = utils.TestDataContainer()
TEST.functions = utils.TestDataContainer()
TEST.executions = utils.TestDataContainer()
TEST.versions = utils.TestDataContainer()
for i in range(10):
runtime_data = {
"id": RUNTIME_ID_BASE.format(i),
"name": "python2.7-runtime-{0}".format(i),
"status": "available",
"created_at": "2018-07-11 01:09:13",
"description": None,
"image": "openstackqinling/python-runtime",
"updated_at": "2018-07-11 01:09:28",
"is_public": True,
"project_id": PROJECT_ID,
}
rt = runtime.Runtime(runtime.RuntimeManager(None), runtime_data)
TEST.runtimes.add(rt)
for i in range(10):
function_data = {
"id": FUNCTION_ID_BASE.format(i),
"count": 0,
"code": "{\"source\": \"package\", "
"\"md5sum\": \"976325c9b41bc5a2ddb54f3493751f7e\"}",
"description": None,
"created_at": "2018-08-01 08:33:50",
"updated_at": None,
"latest_version": 0,
"memory_size": 33554432,
"timeout": None,
"entry": "qinling_test.main",
"project_id": PROJECT_ID,
"cpu": 100,
"runtime_id": RUNTIME_ID_BASE.format(i),
"name": "github_test"
}
func = function.Function(function.FunctionManager(None), function_data)
TEST.functions.add(func)
for i in range(10):
execution_data = {
"status": "success",
"project_id": PROJECT_ID,
"description": None,
"updated_at": "2018-08-01 06:09:58",
"created_at": "2018-08-01 06:09:55",
"sync": True,
"function_version": 0,
"result": "{\"duration\": 0.788, \"output\": 30}",
"input": None,
"function_id": FUNCTION_ID_BASE.format(i),
"id": EXECUTION_ID_BASE.format(i),
}
execution = function_execution.FunctionExecution(
function_execution.ExecutionManager(None), execution_data)
TEST.executions.add(execution)
# Each mocked function has 10 of version data.
for i in range(10):
this_function_id = FUNCTION_ID_BASE.format(i)
for j in range(10):
version_data = {
"count": 0,
"version_number": j,
"function_id": this_function_id,
"description": "",
"created_at": "2018-08-03 02:01:44",
"updated_at": None,
"project_id": PROJECT_ID,
"id": VERSION_ID_BASE.format(i, j)
}
version = function_version.FunctionVersion(
function_version.FunctionVersionManager(None), version_data)
TEST.versions.add(version)

View File

@ -0,0 +1,122 @@
# Copyright 2012 Nebula, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
def load_test_data(load_onto=None):
from qinling_dashboard.test.test_data import exceptions
from qinling_dashboard.test.test_data import keystone_data
from qinling_dashboard.test.test_data import qinling_data
# The order of these loaders matters, some depend on others.
loaders = (
exceptions.data,
keystone_data.data,
qinling_data.data,
)
if load_onto:
for data_func in loaders:
data_func(load_onto)
return load_onto
else:
return TestData(*loaders)
class TestData(object):
"""Holder object for test data.
Any functions passed to the init method will be called with the
``TestData`` object as their only argument.
They can then load data onto the object as desired.
The idea is to use the instantiated object like this::
>>> import qinling_data
>>> TEST = TestData(qinling_data.data)
>>> TEST.runtimes.list()
[<Runtime: runtime>, <Runtime: runtime>]
>>> TEST.runtimes.first()
<Image: visible_image>
You can load as little or as much data as you like as long as the loaders
don't conflict with each other.
See the
:class:`~openstack_dashboard.test.test_data.utils.TestDataContainer`
class for a list of available methods.
"""
def __init__(self, *args):
for data_func in args:
data_func(self)
class TestDataContainer(object):
"""A container for test data objects.
The behavior of this class is meant to mimic a "manager" class, which
has convenient shortcuts for common actions like "list", "filter", "get",
and "add".
"""
def __init__(self):
self._objects = []
def add(self, *args):
"""Add a new object to this container.
Generally this method should only be used during data loading, since
adding data during a test can affect the results of other tests.
"""
for obj in args:
if obj not in self._objects:
self._objects.append(obj)
def list(self):
"""Returns a list of all objects in this container."""
return self._objects
def filter(self, filtered=None, **kwargs):
"""Returns objects whose attributes match the given kwargs."""
if filtered is None:
filtered = self._objects
try:
key, value = kwargs.popitem()
except KeyError:
# We're out of filters, return
return filtered
def get_match(obj):
return hasattr(obj, key) and getattr(obj, key) == value
filtered = [obj for obj in filtered if get_match(obj)]
return self.filter(filtered=filtered, **kwargs)
def get(self, **kwargs):
"""Returns a single object whose attributes match the given kwargs.
An error will be raised if the arguments
provided don't return exactly one match.
"""
matches = self.filter(**kwargs)
if not matches:
raise Exception("No matches found.")
elif len(matches) > 1:
raise Exception("Multiple matches found.")
else:
return matches.pop()
def first(self):
"""Returns the first object from this container."""
return self._objects[0]
def count(self):
return len(self._objects)

View File

View File

@ -0,0 +1,272 @@
# 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 qinling_dashboard import api
from qinling_dashboard.test import helpers as test
class QinlingApiTests(test.APIMockTestCase):
# Runtimes
@mock.patch.object(api.qinling, 'qinlingclient')
def test_runtimes_list(self, mock_qinlingclient):
"""Test for api.qinling.runtimes_list()"""
runtimes = self.runtimes.list()
qclient = mock_qinlingclient.return_value
qclient.runtimes.list.return_value = runtimes
result = api.qinling.runtimes_list(self.request)
self.assertItemsEqual(result, runtimes)
qclient.runtimes.list.assert_called_once_with()
@mock.patch.object(api.qinling, 'qinlingclient')
def test_runtime_get(self, mock_qinlingclient):
"""Test for api.qinling.runtime_get()"""
runtime = self.runtimes.first()
qclient = mock_qinlingclient.return_value
qclient.runtimes.get.return_value = runtime
result = api.qinling.runtime_get(self.request, runtime.id)
self.assertEqual(result, runtime)
qclient.runtimes.get.assert_called_once_with(runtime.id)
# Functions
@mock.patch.object(api.qinling, 'qinlingclient')
def test_function_create(self, mock_qinlingclient):
"""Test for api.qinling.function_create()"""
func = self.functions.first()
qclient = mock_qinlingclient.return_value
qclient.functions.create.return_value = func
params = {}
result = api.qinling.function_create(self.request, **params)
self.assertEqual(result, func)
qclient.functions.create.assert_called_once_with(**params)
@mock.patch.object(api.qinling, 'qinlingclient')
def test_function_update(self, mock_qinlingclient):
"""Test for api.qinling.function_update()"""
func = self.functions.first()
qclient = mock_qinlingclient.return_value
qclient.functions.update.return_value = func
params = {}
result = api.qinling.function_update(self.request, func.id, **params)
self.assertEqual(result, func)
qclient.functions.update.assert_called_once_with(func.id, **params)
@mock.patch.object(api.qinling, 'qinlingclient')
def test_function_delete(self, mock_qinlingclient):
"""Test for api.qinling.function_delete()"""
func = self.functions.first()
qclient = mock_qinlingclient.return_value
qclient.functions.delete.return_value = None
result = api.qinling.function_delete(self.request, func.id)
self.assertIsNone(result)
qclient.functions.delete.assert_called_once_with(func.id)
@mock.patch.object(api.qinling, 'qinlingclient')
def test_functions_list(self, mock_qinlingclient):
"""Test for api.qinling.functions_list()"""
functions = self.functions.list()
qclient = mock_qinlingclient.return_value
qclient.functions.list.return_value = functions
result = api.qinling.functions_list(self.request)
self.assertItemsEqual(result, functions)
qclient.functions.list.assert_called_once_with()
@mock.patch.object(api.qinling, 'qinlingclient')
def test_function_get(self, mock_qinlingclient):
"""Test for api.qinling.function_get()"""
func = self.functions.first()
qclient = mock_qinlingclient.return_value
qclient.functions.get.return_value = func
result = api.qinling.function_get(self.request, func.id)
self.assertEqual(result, func)
qclient.functions.get.assert_called_once_with(func.id)
@mock.patch.object(api.qinling, 'qinlingclient')
def test_function_download(self, mock_qinlingclient):
"""Test for api.qinling.function_download()"""
func = self.functions.first()
qclient = mock_qinlingclient.return_value
qclient.functions.get.return_value = func
result = api.qinling.function_download(self.request, func.id)
self.assertEqual(result, func)
qclient.functions.get.assert_called_once_with(func.id, download=True)
# Function Executions
@mock.patch.object(api.qinling, 'qinlingclient')
def test_execution_create(self, mock_qinlingclient):
"""Test for api.qinling.execution_create()"""
func = self.functions.first()
execution = self.executions.first()
qclient = mock_qinlingclient.return_value
qclient.function_executions.create.return_value = execution
result = api.qinling.execution_create(self.request, func.id,
version=1, sync=True,
input=None)
self.assertEqual(result, execution)
qclient.function_executions.create.assert_called_once_with(func.id,
1,
True,
None)
@mock.patch.object(api.qinling, 'qinlingclient')
def test_execution_delete(self, mock_qinlingclient):
"""Test for api.qinling.execution_delete()"""
execution = self.executions.first()
qclient = mock_qinlingclient.return_value
qclient.function_executions.create.return_value = execution
result = api.qinling.execution_delete(self.request, execution.id)
self.assertIsNone(result)
qclient.function_executions.delete.\
assert_called_once_with(execution.id)
@mock.patch.object(api.qinling, 'qinlingclient')
def test_execution_log_get(self, mock_qinlingclient):
"""Test for api.qinling.execution_log_get()"""
execution = self.executions.first()
log_contents = "this is log line."
class FakeResponse(object):
_content = log_contents
qclient = mock_qinlingclient.return_value
qclient.function_executions.http_client.json_request.\
return_value = FakeResponse(), "dummybody"
result = api.qinling.execution_log_get(self.request, execution.id)
self.assertEqual(result, log_contents)
url = '/v1/executions/%s/log' % execution.id
qclient.function_executions.http_client.\
json_request.assert_called_once_with(url, 'GET')
@mock.patch.object(api.qinling, 'qinlingclient')
def test_executions_list(self, mock_qinlingclient):
"""Test for api.qinling.executions_list()"""
executions = self.executions.list()
qclient = mock_qinlingclient.return_value
qclient.function_executions.list.return_value = executions
result = api.qinling.executions_list(self.request)
self.assertItemsEqual(result, executions)
qclient.function_executions.list.assert_called_once_with()
@mock.patch.object(api.qinling, 'qinlingclient')
def test_execution_get(self, mock_qinlingclient):
"""Test for api.qinling.execution_get()"""
execution = self.executions.first()
qclient = mock_qinlingclient.return_value
qclient.function_executions.get.return_value = execution
result = api.qinling.execution_get(self.request, execution.id)
self.assertEqual(result, execution)
qclient.function_executions.get.assert_called_once_with(execution.id)
# Function Versions
@mock.patch.object(api.qinling, 'qinlingclient')
def test_versions_list(self, mock_qinlingclient):
"""Test for api.qinling.versions_list()"""
versions = self.versions.list()
func = self.functions.first()
this_function_id = func.id
my_versions = [v for v in versions
if v.function_id == this_function_id]
qclient = mock_qinlingclient.return_value
qclient.function_versions.list.return_value = my_versions
result = api.qinling.versions_list(self.request,
this_function_id)
self.assertItemsEqual(result, my_versions)
qclient.function_versions.list.\
assert_called_once_with(this_function_id)
@mock.patch.object(api.qinling, 'qinlingclient')
def test_version_get(self, mock_qinlingclient):
"""Test for api.qinling.version_get()"""
versions = self.versions.list()
func = self.functions.first()
this_function_id = func.id
this_version_number = 1
my_version = [v for v in versions
if v.function_id == this_function_id and
v.version_number == this_version_number][0]
qclient = mock_qinlingclient.return_value
qclient.function_versions.get.return_value = my_version
result = api.qinling.version_get(self.request,
this_function_id,
this_version_number)
self.assertEqual(result, my_version)
qclient.function_versions.get.\
assert_called_once_with(this_function_id, this_version_number)
@mock.patch.object(api.qinling, 'qinlingclient')
def test_version_create(self, mock_qinlingclient):
"""Test for api.qinling.version_create()"""
version = self.versions.first()
func = self.functions.first()
this_function_id = func.id
qclient = mock_qinlingclient.return_value
qclient.function_versions.create.return_value = version
result = api.qinling.version_create(self.request,
this_function_id,
"description sample")
self.assertEqual(result, version)
qclient.function_versions.create.\
assert_called_once_with(this_function_id, "description sample")

View File

@ -0,0 +1,329 @@
# 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 mock
from django.urls import reverse
from django.utils.http import urlunquote
from qinling_dashboard import api
from qinling_dashboard.test import helpers as test
INDEX_TEMPLATE = 'horizon/common/_data_table_view.html'
INDEX_URL = reverse('horizon:project:executions:index')
class ExecutionsTests(test.TestCase):
@test.create_mocks({
api.qinling: [
'executions_list',
'execution_delete',
]})
def test_execution_delete(self):
data_executions = self.executions.list()
data_execution = self.executions.first()
execution_id = data_execution.id
self.mock_executions_list.return_value = data_executions
self.mock_execution_delete.return_value = None
form_data = {'action': 'executions__delete__%s' % execution_id}
res = self.client.post(INDEX_URL, form_data)
self.assertRedirectsNoFollow(res, INDEX_URL)
self.mock_execution_delete.assert_called_once_with(
test.IsHttpRequest(), execution_id)
@test.create_mocks({
api.qinling: [
'execution_create',
'functions_list',
'function_get',
'versions_list',
]})
def test_execution_create(self):
data_execution = self.executions.first()
data_functions = self.functions.list()
data_function = self.functions.first()
function_id = data_function.id
data_versions = self.versions.list()
my_versions = [v for v in data_versions
if v.function_id == function_id]
self.mock_versions_list.return_value = data_versions
self.mock_function_get.return_value = data_function
self.mock_functions_list.return_value = data_functions
self.mock_execution_create.return_value = data_execution
self.mock_versions_list.return_value = my_versions
form_data = {'func': function_id,
'version': 0,
'sync': True,
'input_params': 'K1=V1\nK2=V2'}
url = reverse('horizon:project:executions:create')
res = self.client.post(url, form_data)
self.assertNoFormErrors(res)
self.assertRedirectsNoFollow(res, INDEX_URL)
input_dict = {'K1': 'V1', 'K2': 'V2'}
input_params = json.dumps(input_dict)
self.mock_execution_create.assert_called_once_with(
test.IsHttpRequest(), function_id, 0, True, input_params)
@test.create_mocks({
api.qinling: [
'execution_create',
'functions_list',
'function_get',
'versions_list',
]})
def test_execution_create_function_input_params_is_not_key_value(self):
data_functions = self.functions.list()
data_function = self.functions.first()
function_id = data_function.id
data_versions = self.versions.list()
my_versions = [v for v in data_versions
if v.function_id == function_id]
self.mock_versions_list.return_value = data_versions
self.mock_function_get.return_value = data_function
self.mock_functions_list.return_value = data_functions
self.mock_versions_list.return_value = my_versions
form_data = {'func': function_id,
'version': 0,
'sync': True,
'input_params': 'K1=V1\nK2='}
url = reverse('horizon:project:executions:create')
res = self.client.post(url, form_data)
expected_msg = "Not key-value pair."
self.assertContains(res, expected_msg)
@test.create_mocks({
api.qinling: [
'execution_create',
'functions_list',
'function_get',
'versions_list',
]})
def test_execution_create_function_version_does_not_exist(self):
data_functions = self.functions.list()
data_function = self.functions.first()
function_id = data_function.id
data_versions = self.versions.list()
my_versions = [v for v in data_versions
if v.function_id == function_id]
self.mock_versions_list.return_value = data_versions
self.mock_function_get.return_value = data_function
self.mock_functions_list.return_value = data_functions
self.mock_versions_list.return_value = my_versions
invalid_version = 10
form_data = {'func': function_id,
'version': invalid_version,
'sync': True,
'input_params': ""}
url = reverse('horizon:project:executions:create')
res = self.client.post(url, form_data)
expected_msg = "This function does not " \
"have specified version number: %s" % invalid_version
self.assertContains(res, expected_msg)
@test.create_mocks({
api.qinling: [
'execution_create',
'functions_list',
'function_get',
'versions_list',
]})
def test_execution_create_function_id_is_not_in_choices(self):
data_functions = self.functions.list()
data_function = self.functions.first()
function_id = data_function.id
data_versions = self.versions.list()
my_versions = [v for v in data_versions
if v.function_id == function_id]
self.mock_versions_list.return_value = data_versions
self.mock_function_get.return_value = data_function
self.mock_functions_list.return_value = data_functions
self.mock_versions_list.return_value = my_versions
form_data = {'func': function_id + 'a', # function does not exist
'version': 0,
'sync': True,
'input_params': ""}
url = reverse('horizon:project:executions:create')
res = self.client.post(url, form_data)
expected_msg = \
"%s is not one of the available choices." % (function_id + 'a')
self.assertContains(res, expected_msg)
# You should not mock api.qinling.executions_list/get itself here,
# because inside of executions_list, execution.result is converted
# from str to dict.
# If you mock everything about this executions_list, above conversion
# method won't be called then it causes error in table rendering.
@mock.patch.object(api.qinling, 'qinlingclient')
def test_index(self, mock_qinlingclient):
data_executions = self.executions.list()
qclient = mock_qinlingclient.return_value
qclient.function_executions.list.return_value = data_executions
res = self.client.get(INDEX_URL)
self.assertTemplateUsed(res, INDEX_TEMPLATE)
executions = res.context['executions_table'].data
self.assertItemsEqual(executions, self.executions.list())
qclient.function_executions.list.assert_called_once_with()
@mock.patch.object(api.qinling, 'qinlingclient')
def test_index_executions_list_returns_exception(self, mock_qinlingclient):
# data_executions = self.executions.list()
qclient = mock_qinlingclient.return_value
qclient.function_executions.list.side_effect = self.exceptions.qinling
res = self.client.get(INDEX_URL)
self.assertTemplateUsed(res, INDEX_TEMPLATE)
self.assertEqual(len(res.context['executions_table'].data), 0)
self.assertMessageCount(res, error=1)
qclient.function_executions.list.assert_called_once_with()
@mock.patch.object(api.qinling, 'qinlingclient')
def test_detail(self, mock_qinlingclient):
execution_id = self.executions.first().id
data_execution = self.executions.first()
qclient = mock_qinlingclient.return_value
qclient.function_executions.get.return_value = data_execution
url = urlunquote(reverse('horizon:project:executions:detail',
args=[execution_id]))
res = self.client.get(url)
result_execution = res.context['execution']
self.assertEqual(execution_id, result_execution.id)
self.assertTemplateUsed(res, 'project/executions/detail.html')
qclient.function_executions.get.assert_called_once_with(execution_id)
@mock.patch.object(api.qinling, 'qinlingclient')
def test_detail_execution_get_returns_exception(self, mock_qinlingclient):
execution_id = self.executions.first().id
qclient = mock_qinlingclient.return_value
qclient.function_executions.get.side_effect = self.exceptions.qinling
url = urlunquote(reverse('horizon:project:executions:detail',
args=[execution_id]))
res = self.client.get(url)
redir_url = INDEX_URL
self.assertRedirectsNoFollow(res, redir_url)
qclient.function_executions.get.assert_called_once_with(execution_id)
@test.create_mocks({
api.qinling: [
'execution_get',
'execution_log_get',
]})
def test_detail_execution_log_tab(self):
execution_id = self.executions.first().id
log_contents = "this is log line."
self.mock_execution_get.return_value = self.executions.first()
self.mock_execution_log_get.return_value = log_contents
url_base = 'horizon:project:executions:detail_execution_logs'
url = urlunquote(reverse(url_base, args=[execution_id]))
res = self.client.get(url)
result_logs = res.context['execution_logs']
self.assertTemplateUsed(res, 'project/executions/detail.html')
self.assertEqual(log_contents, result_logs)
self.mock_execution_get.assert_has_calls(
[
mock.call(test.IsHttpRequest(), execution_id),
])
self.mock_execution_log_get.assert_has_calls(
[
mock.call(test.IsHttpRequest(), execution_id),
])
@test.create_mocks({
api.qinling: [
'execution_get',
'execution_log_get',
]})
def test_detail_execution_log_tab_log_get_returns_exception(self):
execution_id = self.executions.first().id
self.mock_execution_get.return_value = self.executions.first()
self.mock_execution_log_get.side_effect = self.exceptions.qinling
url_base = 'horizon:project:executions:detail_execution_logs'
url = urlunquote(reverse(url_base, args=[execution_id]))
res = self.client.get(url)
result_logs = res.context['execution_logs']
self.assertTemplateUsed(res, 'project/executions/detail.html')
self.assertEqual(result_logs, "")
self.mock_execution_get.assert_has_calls(
[
mock.call(test.IsHttpRequest(), execution_id),
])
self.mock_execution_log_get.assert_has_calls(
[
mock.call(test.IsHttpRequest(), execution_id),
])

View File

@ -0,0 +1,962 @@
# 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
from django.core.files import uploadedfile
from django_file_md5 import calculate_md5
from django.http import response
from django.urls import reverse
from django.utils.http import urlunquote
from qinling_dashboard import api
from qinling_dashboard.content.functions import forms as project_fm
from qinling_dashboard.test import helpers as test
import mock
import six
INDEX_TEMPLATE = 'horizon/common/_data_table_view.html'
INDEX_URL = reverse('horizon:project:functions:index')
FULL_FORM = {
"name": "",
"description": "",
"cpu": "",
"memory_size": "",
"code_type": "",
"package_file": "",
"runtime": "",
"entry": "",
"swift_container": "",
"swift_object": "",
"image": "",
}
FILE_CONTENT = six.b('DUMMY_FILE')
class FunctionsTests(test.TestCase):
def _mock_function_version_list(self, mock_qinlingclient):
data_functions = self.functions.list()
def _versions_list_side_effect(function_id):
all_versions = self.versions.list()
my_versions = [v for v in all_versions
if v.function_id == function_id]
return my_versions
qclient = mock_qinlingclient.return_value
# mock function_versions.list
qclient.function_versions.list.side_effect = \
_versions_list_side_effect
# mock functions.list
qclient.functions.list.return_value = data_functions
def _create_temp_file(self):
temp_file = uploadedfile.SimpleUploadedFile(
name='aaaa.zip',
content=FILE_CONTENT,
content_type='application/zip',
)
return temp_file
@mock.patch.object(api.qinling, 'qinlingclient')
def test_function_update_image_case(self, mock_qinlingclient):
"""Test update function by image"""
self._mock_function_version_list(mock_qinlingclient)
func = self.functions.first()
qclient = mock_qinlingclient.return_value
qclient.functions.get.return_value = func
qclient.functions.update.return_value = self.functions.first()
qclient.runtimes.list.return_value = self.runtimes.list()
image = "dummy/image"
form_data = {}
form_data.update({
"function_id": func.id,
"name": "test_name",
"description": "description",
"cpu": project_fm.CPU_MIN_VALUE,
"memory_size": project_fm.MEMORY_MIN_VALUE,
"code_type": "image",
"image": image,
})
url = reverse('horizon:project:functions:update', args=[func.id])
res = self.client.post(url, form_data)
self.assertNoFormErrors(res)
self.assertRedirectsNoFollow(res, INDEX_URL)
# create expected form data.
actual_form_data = copy.deepcopy(form_data)
del actual_form_data['code_type']
del actual_form_data['function_id']
del actual_form_data['image']
qclient.functions.update.\
assert_called_once_with(func.id, **actual_form_data)
@mock.patch.object(api.qinling, 'qinlingclient')
def test_function_update_swift_case(self, mock_qinlingclient):
"""Test update function by swift"""
self._mock_function_version_list(mock_qinlingclient)
func = self.functions.first()
qclient = mock_qinlingclient.return_value
qclient.functions.get.return_value = func
qclient.functions.update.return_value = self.functions.first()
qclient.runtimes.list.return_value = self.runtimes.list()
swift_container = "dummy_container"
swift_object = "dummy_object"
form_data = {}
form_data.update({
"function_id": func.id,
"name": "test_name",
"description": "description",
"cpu": project_fm.CPU_MIN_VALUE,
"memory_size": project_fm.MEMORY_MIN_VALUE,
"code_type": "swift",
"entry_swift": "main.main",
"swift_container": swift_container,
"swift_object": swift_object,
})
url = reverse('horizon:project:functions:update', args=[func.id])
res = self.client.post(url, form_data)
self.assertNoFormErrors(res)
self.assertRedirectsNoFollow(res, INDEX_URL)
# create expected form data.
actual_form_data = copy.deepcopy(form_data)
del actual_form_data['code_type']
del actual_form_data['function_id']
del actual_form_data['entry_swift']
del actual_form_data['swift_container']
del actual_form_data['swift_object']
qclient.functions.update.\
assert_called_once_with(func.id, **actual_form_data)
@mock.patch.object(api.qinling, 'qinlingclient')
def test_function_update_package_case_if_blank_value_is_correctly_handled(
self, mock_qinlingclient):
"""Test update function by package
check if blank string is correctly handled as meaning of
'removing values'
"""
self._mock_function_version_list(mock_qinlingclient)
func = self.functions.first()
qclient = mock_qinlingclient.return_value
qclient.functions.get.return_value = func
qclient.functions.update.return_value = self.functions.first()
qclient.runtimes.list.return_value = self.runtimes.list()
form_data = {}
form_data.update({
"function_id": func.id,
"name": "",
"description": "",
"cpu": "",
"memory_size": "",
"code_type": "package",
"entry": "",
})
url = reverse('horizon:project:functions:update', args=[func.id])
res = self.client.post(url, form_data)
self.assertNoFormErrors(res)
self.assertRedirectsNoFollow(res, INDEX_URL)
# create expected form data.
actual_form_data = copy.deepcopy(form_data)
del actual_form_data['code_type']
del actual_form_data['cpu']
del actual_form_data['memory_size']
del actual_form_data['function_id']
qclient.functions.update.\
assert_called_once_with(func.id, **actual_form_data)
@mock.patch.object(api.qinling, 'qinlingclient')
def test_function_update_package_case(self, mock_qinlingclient):
"""Test update function by package"""
self._mock_function_version_list(mock_qinlingclient)
func = self.functions.first()
qclient = mock_qinlingclient.return_value
qclient.functions.get.return_value = func
qclient.functions.update.return_value = self.functions.first()
qclient.runtimes.list.return_value = self.runtimes.list()
temp_file = self._create_temp_file()
form_data = {}
form_data.update({
"function_id": func.id,
"name": "test_name",
"description": "description",
"cpu": project_fm.CPU_MIN_VALUE,
"memory_size": project_fm.MEMORY_MIN_VALUE,
"code_type": "package",
"entry": "main.main",
"package_file": temp_file,
})
url = reverse('horizon:project:functions:update', args=[func.id])
res = self.client.post(url, form_data)
self.assertNoFormErrors(res)
self.assertRedirectsNoFollow(res, INDEX_URL)
# create expected form data.
actual_form_data = copy.deepcopy(form_data)
del actual_form_data['code_type']
del actual_form_data['package_file']
del actual_form_data['function_id']
actual_form_data.update({
'package': FILE_CONTENT,
'code': {'source': 'package',
'md5sum': calculate_md5(temp_file)}
})
qclient.functions.update.\
assert_called_once_with(func.id, **actual_form_data)
@mock.patch.object(api.qinling, 'qinlingclient')
def test_function_create_swift_case_no_runtime_specified(
self, mock_qinlingclient):
"""Test error case of function creation
Because no runtime is specified.
"""
delete_key = ['runtime_swift']
message = 'You must specify runtime.'
self._function_create_swift_case_error(mock_qinlingclient,
delete_key,
message)
@mock.patch.object(api.qinling, 'qinlingclient')
def test_function_create_swift_case_no_container_specified(
self, mock_qinlingclient):
"""Test error case of function creation
Because no swift container is specified.
"""
delete_key = ['swift_container']
message = 'You must specify container and object ' \
'both in case code type is Swift.'
self._function_create_swift_case_error(mock_qinlingclient,
delete_key,
message)
@mock.patch.object(api.qinling, 'qinlingclient')
def test_function_create_swift_case_no_object_specified(
self, mock_qinlingclient):
"""Test error case of function creation
Because no swift object is specified.
"""
delete_key = ['swift_object']
message = 'You must specify container and object ' \
'both in case code type is Swift.'
self._function_create_swift_case_error(mock_qinlingclient,
delete_key,
message)
@mock.patch.object(api.qinling, 'qinlingclient')
def test_function_create_swift_case_no_container_object_specified(
self, mock_qinlingclient):
"""Test error case of function creation
Because both swift container/object are not specified.
"""
delete_key = ['swift_container', 'swift_object']
message = 'You must specify container and object ' \
'both in case code type is Swift.'
self._function_create_swift_case_error(mock_qinlingclient,
delete_key,
message)
def _function_create_swift_case_error(self, mock_qinlingclient,
delete_key=None, message=''):
"""Base function for function creation error test in swift case"""
if not delete_key:
delete_key = []
self._mock_function_version_list(mock_qinlingclient)
qclient = mock_qinlingclient.return_value
qclient.functions.get.return_value = self.functions.first()
qclient.functions.create.return_value = self.functions.first()
qclient.runtimes.list.return_value = self.runtimes.list()
form_data = copy.deepcopy(FULL_FORM)
swift_container = "dummy_container"
swift_object = "dummy_object"
runtime_id = self.runtimes.first().id
form_data.update({
"name": "test_name",
"description": "description",
"cpu": project_fm.CPU_MIN_VALUE,
"memory_size": project_fm.MEMORY_MIN_VALUE,
"code_type": "swift",
"swift_container": swift_container,
"swift_object": swift_object,
"runtime_swift": runtime_id,
})
for k in delete_key:
if k in form_data:
del form_data[k]
url = reverse('horizon:project:functions:create')
res = self.client.post(url, form_data)
self.assertContains(res, message)
qclient.functions.create.assert_not_called()
@mock.patch.object(api.qinling, 'qinlingclient')
def test_function_create_swift_case(self, mock_qinlingclient):
"""Test create function by swift container/object"""
self._mock_function_version_list(mock_qinlingclient)
qclient = mock_qinlingclient.return_value
qclient.functions.get.return_value = self.functions.first()
qclient.functions.create.return_value = self.functions.first()
qclient.runtimes.list.return_value = self.runtimes.list()
form_data = copy.deepcopy(FULL_FORM)
swift_container = "dummy_container"
swift_object = "dummy_object"
runtime_id = self.runtimes.first().id
form_data.update({
"name": "test_name",
"description": "description",
"cpu": project_fm.CPU_MIN_VALUE,
"memory_size": project_fm.MEMORY_MIN_VALUE,
"code_type": "swift",
"swift_container": swift_container,
"swift_object": swift_object,
"runtime_swift": runtime_id,
})
url = reverse('horizon:project:functions:create')
res = self.client.post(url, form_data)
self.assertNoFormErrors(res)
self.assertRedirectsNoFollow(res, INDEX_URL)
# create expected form data.
actual_form_data = copy.deepcopy(form_data)
for k, v in form_data.items():
if not v:
del actual_form_data[k]
del actual_form_data['code_type']
del actual_form_data['swift_container']
del actual_form_data['swift_object']
# PIOST value "runtine_swift" to Horizon
# will be set as "runtime" in POST value to Qinling.
tmp_swift_runtime = form_data['runtime_swift']
del actual_form_data['runtime_swift']
actual_form_data.update({'runtime': tmp_swift_runtime})
actual_form_data.update({
'code': {
'source': 'swift',
'swift': {
'container': swift_container,
'object': swift_object,
}
}
})
qclient.functions.create.\
assert_called_once_with(**actual_form_data)
@mock.patch.object(api.qinling, 'qinlingclient')
def test_function_create_image_case_error(self, mock_qinlingclient):
"""Test error case of function creation from image.
Error case because no image is specified.
"""
self._mock_function_version_list(mock_qinlingclient)
qclient = mock_qinlingclient.return_value
qclient.functions.get.return_value = self.functions.first()
qclient.functions.create.return_value = self.functions.first()
qclient.runtimes.list.return_value = self.runtimes.list()
form_data = copy.deepcopy(FULL_FORM)
form_data.update({
"name": "test_name",
"description": "description",
"cpu": project_fm.CPU_MIN_VALUE,
"memory_size": project_fm.MEMORY_MIN_VALUE,
"code_type": "image",
})
url = reverse('horizon:project:functions:create')
res = self.client.post(url, form_data)
self.assertContains(res, 'You must specify Docker image.')
qclient.functions.create.assert_not_called()
@mock.patch.object(api.qinling, 'qinlingclient')
def test_function_create_image_case(self, mock_qinlingclient):
"""Test create function by image"""
self._mock_function_version_list(mock_qinlingclient)
qclient = mock_qinlingclient.return_value
qclient.functions.get.return_value = self.functions.first()
qclient.functions.create.return_value = self.functions.first()
qclient.runtimes.list.return_value = self.runtimes.list()
form_data = copy.deepcopy(FULL_FORM)
image = "dummy/image"
form_data.update({
"name": "test_name",
"description": "description",
"cpu": project_fm.CPU_MIN_VALUE,
"memory_size": project_fm.MEMORY_MIN_VALUE,
"code_type": "image",
"image": image
})
url = reverse('horizon:project:functions:create')
res = self.client.post(url, form_data)
self.assertNoFormErrors(res)
self.assertRedirectsNoFollow(res, INDEX_URL)
# create expected form data.
actual_form_data = copy.deepcopy(form_data)
for k, v in form_data.items():
if not v:
del actual_form_data[k]
del actual_form_data['code_type']
del actual_form_data['image']
actual_form_data.update({
'code': {
'source': 'image',
'image': image
}
})
qclient.functions.create.\
assert_called_once_with(**actual_form_data)
@mock.patch.object(api.qinling, 'qinlingclient')
def test_function_create_package_case(self, mock_qinlingclient):
"""Test create function by package"""
self._mock_function_version_list(mock_qinlingclient)
qclient = mock_qinlingclient.return_value
qclient.functions.get.return_value = self.functions.first()
qclient.functions.create.return_value = self.functions.first()
qclient.runtimes.list.return_value = self.runtimes.list()
runtime = self.runtimes.first()
temp_file = self._create_temp_file()
form_data = copy.deepcopy(FULL_FORM)
form_data.update({
"name": "test_name",
"description": "description",
"cpu": project_fm.CPU_MIN_VALUE,
"memory_size": project_fm.MEMORY_MIN_VALUE,
"code_type": "package",
"runtime": runtime.id,
"entry": "main.main",
"package_file": temp_file,
})
url = reverse('horizon:project:functions:create')
res = self.client.post(url, form_data)
self.assertNoFormErrors(res)
self.assertRedirectsNoFollow(res, INDEX_URL)
# create expected form data.
actual_form_data = copy.deepcopy(form_data)
for k, v in form_data.items():
if not v:
del actual_form_data[k]
del actual_form_data['code_type']
del actual_form_data['package_file']
actual_form_data.update({
'package': FILE_CONTENT,
'code': {'source': 'package',
'md5sum': calculate_md5(temp_file)}
})
qclient.functions.create.\
assert_called_once_with(**actual_form_data)
@mock.patch.object(api.qinling, 'qinlingclient')
def test_function_create_package_case_with_least_params(
self, mock_qinlingclient):
"""Test create function by package with least parameters"""
self._mock_function_version_list(mock_qinlingclient)
qclient = mock_qinlingclient.return_value
qclient.functions.get.return_value = self.functions.first()
qclient.functions.create.return_value = self.functions.first()
qclient.runtimes.list.return_value = self.runtimes.list()
runtime = self.runtimes.first()
temp_file = self._create_temp_file()
form_data = copy.deepcopy(FULL_FORM)
form_data.update({
"code_type": "package",
"runtime": runtime.id,
"package_file": temp_file,
})
url = reverse('horizon:project:functions:create')
res = self.client.post(url, form_data)
self.assertNoFormErrors(res)
self.assertRedirectsNoFollow(res, INDEX_URL)
# create expected form data.
actual_form_data = copy.deepcopy(form_data)
for k, v in form_data.items():
if not v:
del actual_form_data[k]
del actual_form_data['code_type']
del actual_form_data['package_file']
actual_form_data.update({
'package': FILE_CONTENT,
'code': {'source': 'package',
'md5sum': calculate_md5(temp_file)}
})
qclient.functions.create.\
assert_called_once_with(**actual_form_data)
@mock.patch.object(api.qinling, 'qinlingclient')
def test_function_create_package_case_no_package_file_specified(
self, mock_qinlingclient):
"""Error case of function creation from package
Because no package_file is specified
"""
delete_key = ['package_file']
message = 'You must specify package file.'
self._function_create_package_case_error(mock_qinlingclient,
delete_key,
message)
@mock.patch.object(api.qinling, 'qinlingclient')
def test_function_create_package_case_no_runtime_specified(
self, mock_qinlingclient):
"""Error case of function creation from package
Because no runtime is specified
"""
delete_key = ['runtime']
message = 'You must specify runtime.'
self._function_create_package_case_error(mock_qinlingclient,
delete_key,
message)
def _function_create_package_case_error(
self, mock_qinlingclient, delete_key=None, message=''):
"""Base function for testing function creation from package"""
if not delete_key:
delete_key = []
self._mock_function_version_list(mock_qinlingclient)
qclient = mock_qinlingclient.return_value
qclient.functions.get.return_value = self.functions.first()
qclient.functions.create.return_value = self.functions.first()
qclient.runtimes.list.return_value = self.runtimes.list()
temp_file = self._create_temp_file()
form_data = copy.deepcopy(FULL_FORM)
form_data.update({
"name": "test_name",
"description": "description",
"cpu": project_fm.CPU_MIN_VALUE,
"memory_size": project_fm.MEMORY_MIN_VALUE,
"code_type": "package",
"entry": "main.main",
"package_file": temp_file,
})
for k in delete_key:
if k in form_data:
del form_data[k]
url = reverse('horizon:project:functions:create')
res = self.client.post(url, form_data)
self.assertContains(res, message)
qclient.functions.create.assert_not_called()
@mock.patch.object(api.qinling, 'qinlingclient')
def test_function_version_delete(self, mock_qinlingclient):
"""Test function version delete"""
function_id = self.functions.first().id
version_id = self.versions.first().id
version_number = self.versions.first().version_number
self._mock_function_version_list(mock_qinlingclient)
qclient = mock_qinlingclient.return_value
qclient.function_versions.delete.return_value = None
url = reverse('horizon:project:functions:detail', args=[function_id])
url += '?tab=function_details__versions_of_this_function'
form_data = {'action': 'function_versions__delete__%s' % version_id}
res = self.client.post(url, form_data)
self.assertRedirectsNoFollow(res, url)
qclient.function_versions.delete.\
assert_called_once_with(function_id, version_number)
@mock.patch.object(api.qinling, 'qinlingclient')
def test_function_version_create(self, mock_qinlingclient):
"""Test function version create"""
self._mock_function_version_list(mock_qinlingclient)
function_id = self.functions.first().id
qclient = mock_qinlingclient.return_value
qclient.functions.get.return_value = self.functions.first()
description_data = 'some description'
form_data = {'function_id': function_id,
'description': description_data}
url = reverse('horizon:project:functions:create_version',
args=[function_id])
res = self.client.post(url, form_data)
self.assertNoFormErrors(res)
create_version_success_url = \
reverse('horizon:project:functions:detail', args=[function_id])
create_version_success_url += '?tab=function_details__' \
'versions_of_this_function'
self.assertRedirectsNoFollow(res, create_version_success_url)
qclient.function_versions.create.\
assert_called_once_with(function_id, description_data)
@mock.patch.object(api.qinling, 'qinlingclient')
def test_function_download(self, mock_qinlingclient):
"""Test function download"""
self._mock_function_version_list(mock_qinlingclient)
function_id = self.functions.first().id
qclient = mock_qinlingclient.return_value
qclient.functions.get.return_value = \
response.HttpResponse(content="DUMMY_DOWNLOAD_DATA")
url = reverse('horizon:project:functions:download', args=[function_id])
res = self.client.get(url)
# res._headers[content-disposition] will be set like
# ('Content-Disposition',
# 'attachment; filename=qinling-function-<function_id>.zip')
result_header = res._headers['content-disposition'][1]
expected_header = \
'attachment; filename=qinling-function-%s.zip' % function_id
self.assertEqual(result_header, expected_header)
qclient.functions.get.assert_called_once_with(function_id,
download=True)
@mock.patch.object(api.qinling, 'qinlingclient')
def test_function_delete(self, mock_qinlingclient):
"""Test function delete"""
function_id = self.functions.first().id
self._mock_function_version_list(mock_qinlingclient)
qclient = mock_qinlingclient.return_value
qclient.functions.delete.return_value = None
form_data = {'action': 'functions__delete__%s' % function_id}
res = self.client.post(INDEX_URL, form_data)
self.assertRedirectsNoFollow(res, INDEX_URL)
qclient.functions.delete.assert_called_once_with(function_id)
@mock.patch.object(api.qinling, 'qinlingclient')
def test_index(self, mock_qinlingclient):
"""Test IndexView"""
qclient = mock_qinlingclient.return_value
self._mock_function_version_list(mock_qinlingclient)
res = self.client.get(INDEX_URL)
self.assertTemplateUsed(res, INDEX_TEMPLATE)
result_functions = res.context['functions_table'].data
api_functions = api.qinling.functions_list(self.request)
calls = [(), ()] # called twice, without any argument.
self.assertItemsEqual(result_functions, api_functions)
qclient.functions.list.assert_has_calls(calls)
@test.create_mocks({
api.qinling: [
'functions_list',
]})
def test_index_functions_list_returns_exception(self):
"""Test IndexView with exception from functions list"""
self.mock_functions_list.side_effect = self.exceptions.qinling
res = self.client.get(INDEX_URL)
self.assertTemplateUsed(res, INDEX_TEMPLATE)
self.assertEqual(len(res.context['functions_table'].data), 0)
self.assertMessageCount(res, error=1)
self.mock_functions_list.assert_has_calls(
[
mock.call(test.IsHttpRequest()),
])
# Do not mock function_get directly to call set_code
# so that function.code is converted into dict correctly.
@mock.patch.object(api.qinling, 'qinlingclient')
def test_detail(self, mock_qinlingclient):
"""Test DetailView"""
function_id = self.functions.first().id
qclient = mock_qinlingclient.return_value
qclient.functions.get.return_value = self.functions.first()
url = urlunquote(reverse('horizon:project:functions:detail',
args=[function_id]))
res = self.client.get(url)
result_function = res.context['function']
self.assertEqual(function_id, result_function.id)
self.assertTemplateUsed(res, 'project/functions/detail.html')
qclient.functions.get.assert_called_once_with(function_id)
@test.create_mocks({
api.qinling: [
'function_get',
]})
def test_detail_function_get_returns_exception(self):
"""Test DetailView with exception from function get"""
function_id = self.functions.first().id
self.mock_function_get.side_effect = self.exceptions.qinling
url = urlunquote(reverse('horizon:project:functions:detail',
args=[function_id]))
res = self.client.get(url)
redir_url = INDEX_URL
self.assertRedirectsNoFollow(res, redir_url)
self.mock_function_get.assert_has_calls(
[
mock.call(test.IsHttpRequest(), function_id),
])
# Do not mock function_get directly to call set_code
# so that function.code is converted into dict correctly.
@mock.patch.object(api.qinling, 'qinlingclient')
def test_detail_executions_tab(self, mock_qinlingclient):
data_executions = self.executions.list()
data_function = self.functions.first()
function_id = data_function.id
my_executions = [ex for ex in data_executions
if ex.function_id == function_id]
qclient = mock_qinlingclient.return_value
qclient.function_executions.list.return_value = my_executions
qclient.functions.get.return_value = data_function
url_base = 'horizon:project:functions:detail_executions'
url = urlunquote(reverse(url_base, args=[function_id]))
res = self.client.get(url)
result_executions = res.context['function_executions_table'].data
self.assertTemplateUsed(res, 'project/functions/detail.html')
self.assertEqual(my_executions, result_executions)
qclient.functions.get.assert_called_once_with(function_id)
calls = [mock.call(),
mock.call()]
qclient.function_executions.list.assert_has_calls(calls)
# Do not mock function_get directly to call set_code
# so that function.code is converted into dict correctly.
@mock.patch.object(api.qinling, 'qinlingclient')
def test_detail_executions_tab_executions_list_returns_exception(
self, mock_qinlingclient):
data_function = self.functions.first()
function_id = data_function.id
qclient = mock_qinlingclient.return_value
qclient.function_executions.list.side_effect = self.exceptions.qinling
qclient.functions.get.return_value = data_function
url_base = 'horizon:project:functions:detail_executions'
url = urlunquote(reverse(url_base, args=[function_id]))
res = self.client.get(url)
result_executions = res.context['function_executions_table'].data
self.assertTemplateUsed(res, 'project/functions/detail.html')
self.assertEqual(len(result_executions), 0)
qclient.functions.get.assert_called_once_with(function_id)
calls = [mock.call(),
mock.call()]
qclient.function_executions.list.assert_has_calls(calls)
# Do not mock function_get directly to call set_code
# so that function.code is converted into dict correctly.
@mock.patch.object(api.qinling, 'qinlingclient')
def test_detail_versions_tab(self, mock_qinlingclient):
data_versions = self.versions.list()
data_function = self.functions.first()
function_id = data_function.id
my_versions = [v for v in data_versions
if v.function_id == function_id]
qclient = mock_qinlingclient.return_value
qclient.function_versions.list.return_value = my_versions
qclient.functions.get.return_value = data_function
url_base = 'horizon:project:functions:detail_executions'
url = urlunquote(reverse(url_base, args=[function_id]))
res = self.client.get(url)
result_versions = res.context['function_versions_table'].data
self.assertTemplateUsed(res, 'project/functions/detail.html')
self.assertEqual(my_versions, result_versions)
qclient.functions.get.assert_called_once_with(function_id)
calls = [mock.call(function_id,),
mock.call(function_id,)]
qclient.function_versions.list.assert_has_calls(calls)
# Do not mock function_get directly to call set_code
# so that function.code is converted into dict correctly.
@mock.patch.object(api.qinling, 'qinlingclient')
def test_detail_versions_tab_versions_list_returns_exception(
self, mock_qinlingclient):
data_function = self.functions.first()
function_id = data_function.id
qclient = mock_qinlingclient.return_value
qclient.function_versions.list.side_effect = self.exceptions.qinling
qclient.functions.get.return_value = data_function
url_base = 'horizon:project:functions:detail_executions'
url = urlunquote(reverse(url_base, args=[function_id]))
res = self.client.get(url)
result_versions = res.context['function_versions_table'].data
self.assertEqual(len(result_versions), 0)
self.assertTemplateUsed(res, 'project/functions/detail.html')
qclient.functions.get.assert_called_once_with(function_id)
calls = [mock.call(function_id,),
mock.call(function_id,)]
qclient.function_versions.list.assert_has_calls(calls)
@test.create_mocks({
api.qinling: [
'version_get',
'versions_list',
]})
def test_version_detail(self):
function_id = self.functions.first().id
data_versions = self.versions.list()
version_number = 1
data_version = [v for v in data_versions
if v.function_id == function_id and
v.version_number == version_number][0]
self.mock_version_get.return_value = data_version
self.mock_versions_list.return_value = data_versions
url = urlunquote(reverse('horizon:project:functions:version_detail',
args=[function_id, version_number]))
res = self.client.get(url)
result_version = res.context['version']
self.assertEqual(version_number, result_version.version_number)
self.assertTemplateUsed(res, 'project/functions/detail_version.html')
self.mock_version_get.assert_has_calls(
[
mock.call(test.IsHttpRequest(), function_id, version_number),
])

View File

@ -0,0 +1,109 @@
# 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 django.urls import reverse
from django.utils.http import urlunquote
from qinling_dashboard import api
from qinling_dashboard.test import helpers as test
import mock
INDEX_TEMPLATE = 'horizon/common/_data_table_view.html'
INDEX_URL = reverse('horizon:project:runtimes:index')
class RuntimesTests(test.TestCase):
@test.create_mocks({
api.qinling: [
'runtimes_list',
]})
def test_index(self):
data_runtimes = self.runtimes.list()
self.mock_runtimes_list.return_value = data_runtimes
res = self.client.get(INDEX_URL)
self.assertTemplateUsed(res, INDEX_TEMPLATE)
runtimes = res.context['runtimes_table'].data
self.assertItemsEqual(runtimes, self.runtimes.list())
self.mock_runtimes_list.assert_has_calls(
[
mock.call(test.IsHttpRequest()),
])
@test.create_mocks({
api.qinling: [
'runtimes_list',
]})
def test_index_runtimes_list_returns_exception(self):
self.mock_runtimes_list.side_effect = self.exceptions.qinling
res = self.client.get(INDEX_URL)
self.assertTemplateUsed(res, INDEX_TEMPLATE)
self.assertEqual(len(res.context['runtimes_table'].data), 0)
self.assertMessageCount(res, error=1)
self.mock_runtimes_list.assert_has_calls(
[
mock.call(test.IsHttpRequest()),
])
@test.create_mocks({
api.qinling: [
'runtime_get',
]})
def test_detail(self):
runtime_id = self.runtimes.first().id
self.mock_runtime_get.return_value = self.runtimes.first()
url = urlunquote(reverse('horizon:project:runtimes:detail',
args=[runtime_id]))
res = self.client.get(url)
result_runtime = res.context['runtime']
self.assertEqual(runtime_id, result_runtime.id)
self.assertTemplateUsed(res, 'project/runtimes/detail.html')
self.mock_runtime_get.assert_has_calls(
[
mock.call(test.IsHttpRequest(), runtime_id),
])
@test.create_mocks({
api.qinling: [
'runtime_get',
]})
def test_detail_runtime_get_returns_exception(self):
runtime_id = self.runtimes.first().id
self.mock_runtime_get.side_effect = self.exceptions.qinling
url = urlunquote(reverse('horizon:project:runtimes:detail',
args=[runtime_id]))
res = self.client.get(url)
redir_url = INDEX_URL
self.assertRedirectsNoFollow(res, redir_url)
self.mock_runtime_get.assert_has_calls(
[
mock.call(test.IsHttpRequest(), runtime_id),
])

View File

@ -0,0 +1,293 @@
# 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 unittest
from django.core.exceptions import ValidationError
from qinling_dashboard import validators
def provider_validate_key_value_pairs():
provider = list()
# blank check
empty_row = [
# blank string
{'d': u'', 'raise': False},
# multiple lines consist of blank string
{'d': u'\n\n\n\n\n\n\n\n\n\n\n', 'raise': False},
# multiple lines consist of blank string + valid row
{'d': u'\n\n\n\n\n\n\n\n\n\n\nkey=value', 'raise': False},
# multiple lines consist of blank string + valid row
# + multiple lines consist of blank string
{'d': u'\n\n\n\n\n\n\n\n\n\n\nkey=value\n\n', 'raise': False},
]
provider += empty_row
# key part check
# consist of valid character case
key_check_normal = [
# lower limit
{'d': u'k=v', 'raise': False},
# upper limit
{'d': u'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ012345'
u'6789!"#$%&\'()*+,-./:;<F>?@[\]^_`{|}~abcdefghijklmnopqrstu'
u'vwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!"#$%&\'()*+,-./:;'
u'<F>?@[\]^_`{|}~abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQR'
u'STUVWXYZ0123456789!"#$%=v', 'raise': False},
# upper limit +1
{'d': u'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234567'
u'89!"#$%&\'()*+,-./:;<F>?@[\]^_`{|}~abcdefghijklmnopqrstuvwxy'
u'zABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!"#$%&\'()*+,-./:;<F>?@'
u'[\]^_`{|}~abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWX'
u'YZ0123456789!"#$%A=v', 'raise': True},
]
provider += key_check_normal
# key has initial blank string
key_check_starts_space = [
{'d': u' =v', 'raise': True},
# upper limit
{'d': u' abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234567'
u'89!"#$%&\'()*+,-./:;<F>?@[\]^_`{|}~abcdefghijklmnopqrstuvwxyzA'
u'BCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!"#$%&\'()*+,-./:;<F>?@[\]^'
u'_`{|}~abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123'
u'456789!"#$=v', 'raise': True},
# upper limit + 1
{'d': u' abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ012345678'
u'9!"#$%&\'()*+,-./:;<F>?@[\]^_`{|}~abcdefghijklmnopqrstuvwxyzAB'
u'CDEFGHIJKLMNOPQRSTUVWXYZ0123456789!"#$%&\'()*+,-./:;<F>?@[\]^_'
u'`{|}~abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234'
u'56789!"#$A=v', 'raise': True},
]
provider += key_check_starts_space
# key has last blank string
key_check_normal = [
# upper limit
{'d': u'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ012345678'
u'9!"#$%&\'()*+,-./:;<F>?@[\]^_`{|}~abcdefghijklmnopqrstuvwxyzA'
u'BCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!"#$%&\'()*+,-./:;<F>?@[\]'
u'^_`{|}~abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01'
u'23456789!"#$ =v', 'raise': True},
# upper limit + 1
{'d': u'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ012345678'
u'9!"#$%&\'()*+,-./:;<F>?@[\]^_`{|}~abcdefghijklmnopqrstuvwxyzA'
u'BCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!"#$%&\'()*+,-./:;<F>?@[\]'
u'^_`{|}~abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01'
u'23456789!"#$% =v', 'raise': True},
]
provider += key_check_normal
# key has blank string in the middle of it
key_check_middle_space = [
# upper limit
{'d': u'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ012345678'
u'9 !"#$%&\'()*+,-./:;<F>?@[\]^_`{|}~abcdefghijklmnopqrstuvwxyz'
u'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!"#$%&\'()*+,-./:;<F>?@[\]'
u'^_`{|}~abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ012'
u'3456789!"#$=v', 'raise': False},
# upper limit + 1
{'d': u'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
u' !"#$%&\'()*+,-./:;<F>?@[\]^_`{|}~abcdefghijklmnopqrstuvwxyzAB'
u'CDEFGHIJKLMNOPQRSTUVWXYZ0123456789!"#$%&\'()*+,-./:;<F>?@[\]^_'
u'`{|}~abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234'
u'56789!"#$A=v', 'raise': True},
]
provider += key_check_middle_space
# check for key part
no_key = [
{'d': u'=v', 'raise': True},
]
provider += no_key
# equal part check
equal_check = [
# no equal
{'d': 'key1value1', 'raise': True},
# multiple equal in it
{'d': 'key=value=key\n', 'raise': True},
]
provider += equal_check
# check for value part
# consist of valid charcters
value_check_normal = [
# upper limit
{'d': u'k=abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234567'
u'89!"#$%&\'()*+,-./:;<F>?@[\]^_`{|}~abcdefghijklmnopqrstuvwxyzA'
u'BCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!"#$%&\'()*+,-./:;<F>?@[\]^'
u'_`{|}~abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123'
u'456789!"#$%', 'raise': False},
# upper limit + 1
{'d': u'k=abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234567'
u'89!"#$%&\'()*+,-./:;<F>?@[\]^_`{|}~abcdefghijklmnopqrstuvwxyzA'
u'BCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!"#$%&\'()*+,-./:;<F>?@[\]^'
u'_`{|}~abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123'
u'456789!"#$%A', 'raise': True},
]
provider += value_check_normal
# value has initial blank
value_check_starts_space = [
{'d': u'k= ', 'raise': True},
# upper limit
{'d': u'k= abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456'
u'789!"#$%&\'()*+,-./:;<F>?@[\]^_`{|}~abcdefghijklmnopqrstuvwxyz'
u'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!"#$%&\'()*+,-./:;<F>?@[\]'
u'^_`{|}~abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ012'
u'3456789!"#$', 'raise': True},
# upper limit + 1
{'d': u'k= abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456'
u'789!"#$%&\'()*+,-./:;<F>?@[\]^_`{|}~abcdefghijklmnopqrstuvwxyz'
u'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!"#$%&\'()*+,-./:;<F>?@[\]'
u'^_`{|}~abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ012'
u'3456789!"#$A', 'raise': True},
]
provider += value_check_starts_space
# value has last blank
value_check_normal = [
# upper limit
{'d': u'k=abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456'
u'789!"#$%&\'()*+,-./:;<F>?@[\]^_`{|}~abcdefghijklmnopqrstuvwxy'
u'zABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!"#$%&\'()*+,-./:;<F>?@'
u'[\]^_`{|}~abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWX'
u'YZ0123456789!"#$ ', 'raise': True},
# upper limit + 1
{'d': u'k=abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456'
u'789!"#$%&\'()*+,-./:;<F>?@[\]^_`{|}~abcdefghijklmnopqrstuvwxy'
u'zABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!"#$%&\'()*+,-./:;<F>?@'
u'[\]^_`{|}~abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWX'
u'YZ0123456789!"#$% ', 'raise': True},
]
provider += value_check_normal
# value has middle blank
value_check_middle_space = [
# upper limit
{'d': u'k=abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ012345'
u'6789 !"#$%&\'()*+,-./:;<F>?@[\]^_`{|}~abcdefghijklmnopqrstuv'
u'wxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!"#$%&\'()*+,-./:;<F'
u'>?@[\]^_`{|}~abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTU'
u'VWXYZ0123456789!"#$', 'raise': False},
# upper limit + 1
{'d': u'k=abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ012345'
u'6789 !"#$%&\'()*+,-./:;<F>?@[\]^_`{|}~abcdefghijklmnopqrstuv'
u'wxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!"#$%&\'()*+,-./:;<F'
u'>?@[\]^_`{|}~abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTU'
u'VWXYZ0123456789!"#$A', 'raise': True},
]
provider += value_check_middle_space
# no value is specified
no_value = [
{'d': u'k=', 'raise': True},
]
provider += no_value
return provider
def provider_validate_one_line_string():
return [
# Threshold check for number of charaters
# lower limit - 1
{'d': u'', 'raise': True},
# lower limit
{'d': u'a', 'raise': False},
# upper limit
{'d': u"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVwXYZ012345"
u"6789!\"#$%&'()*+,-./:;<=>?@[\]^_`{|}~ abcdefghijklmnopqrst"
u"uvwxyzABCDEFGHIJKLMNOPQRSTUVwXYZ0123456789!\"#$%&'()*+,-./:"
u";<=>?@[\]^_`{|}~ abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOP"
u"QRSTUVwXYZ0123456789!\"#",
'raise': False},
# upper limit + 1
{'d': u"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVwXYZ012345"
u"6789!\"#$%&'()*+,-./:;<=>?@[\]^_`{|}~ abcdefghijklmnopqrst"
u"uvwxyzABCDEFGHIJKLMNOPQRSTUVwXYZ0123456789!\"#$%&'()*+,-./"
u":;<=>?@[\]^_`{|}~ abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMN"
u"OPQRSTUVwXYZ0123456789!\"#$",
'raise': True},
# initial character is blank
# lower limit
{'d': u' ', 'raise': True},
# upper limit
{'d': u" bcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVwXYZ01234567"
u"89!\"#$%&'()*+,-./:;<=>?@[\]^_`{|}~ abcdefghijklmnopqrstuvwx"
u"yzABCDEFGHIJKLMNOPQRSTUVwXYZ0123456789!\"#$%&'()*+,-./:;<=>?"
u"@[\]^_`{|}~ abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUV"
u"wXYZ0123456789!\"#",
'raise': True},
# upper limit + 1
{'d': u" bcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVwXYZ0123456"
u"789!\"#$%&'()*+,-./:;<=>?@[\]^_`{|}~ abcdefghijklmnopqrstuv"
u"wxyzABCDEFGHIJKLMNOPQRSTUVwXYZ0123456789!\"#$%&'()*+,-./:;<"
u"=>?@[\]^_`{|}~ abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQR"
u"STUVwXYZ0123456789!\"#a",
'raise': True},
# last character is blank
# upper limit
{'d': u"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVwXYZ0123456"
u"789!\"#$%&'()*+,-./:;<=>?@[\]^_`{|}~ abcdefghijklmnopqrstuv"
u"wxyzABCDEFGHIJKLMNOPQRSTUVwXYZ0123456789!\"#$%&'()*+,-./:;<"
u"=>?@[\]^_`{|}~ abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQR"
u"STUVwXYZ0123456789!\" """,
'raise': True},
# upper limit + 1
{'d': u"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVwXYZ0123456"
u"789!\"#$%&'()*+,-./:;<=>?@[\]^_`{|}~ abcdefghijklmnopqrstuv"
u"wxyzABCDEFGHIJKLMNOPQRSTUVwXYZ0123456789!\"#$%&'()*+,-./:;<"
u"=>?@[\]^_`{|}~ abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQR"
u"STUVwXYZ0123456789!\"# """,
'raise': True},
]
class ValidatorsTests(unittest.TestCase):
def test_validate_metadata(self, data=provider_validate_key_value_pairs()):
for datum in data:
d = datum.get('d')
raise_expected = datum.get('raise')
if raise_expected:
self.assertRaises(ValidationError,
validators.validate_key_value_pairs, d)
else:
self.assertIsNone(validators.validate_key_value_pairs(d))
def test_validate_openstack_string(
self, data=provider_validate_one_line_string()):
for datum in data:
d = datum.get('d')
raise_expected = datum.get('raise')
if raise_expected:
self.assertRaises(ValidationError,
validators.validate_one_line_string, d)
else:
self.assertIsNone(validators.validate_one_line_string(d))

View File

@ -0,0 +1,67 @@
# 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 to be included for configuring application which relates
# to orchestration(Heat) functions.
import json
from django.utils.translation import pgettext_lazy
def convert_raw_input_to_api_format(value):
if value == '':
return None
value = value.replace('\r\n', '\n')
data = value.split('\n')
input_dict = {}
for datum in data:
if datum == "":
continue
k, v = datum.split('=')
input_dict.update({k: v})
inp = json.dumps(input_dict)
return inp
FUNCTION_ENGINE_STATUS_CHOICES = (
("Creating", None),
("Available", True),
("Upgrading", True),
("Error", False),
("Deleting", None),
("Running", None),
("Done", True),
("Paused", True),
("Cancelled", True),
("Success", True),
("Failed", False),
)
FUNCTION_ENGINE_STATUS_DISPLAY_CHOICES = (
("creating", pgettext_lazy("current status of runtime", u"Creating")),
("available",
pgettext_lazy("current status of runtime", u"Available")),
("upgrading",
pgettext_lazy("current status of runtime", u"Upgrading")),
("error", pgettext_lazy("current status of runtime", u"Error")),
("deleting", pgettext_lazy("current status of runtime", u"Deleting")),
("running", pgettext_lazy("current status of runtime", u"Running")),
("done", pgettext_lazy("current status of runtime", u"Done")),
("paused", pgettext_lazy("current status of runtime", u"Paused")),
("cancelled",
pgettext_lazy("current status of runtime", u"Cancelled")),
("success", pgettext_lazy("current status of runtime", u"Success")),
("failed", pgettext_lazy("current status of runtime", u"Failed")),
)

View File

@ -0,0 +1,77 @@
# 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
from django.core.exceptions import ValidationError
from django.utils.translation import ugettext_lazy as _
STRING_VALIDATE_PATTERN = \
"""[a-zA-Z0-9\$\[\]\(\)\{\}\*\+\?\^\s\|\.\-\\\\!#%&'",/:;=<>@_`~]"""
MAX_LENGTH = 255
def validate_1st_space(value):
"""Raise execption if 1st character is blank(space)"""
if re.match('^\s', value):
raise ValidationError(_("1st character is not valid."))
def validate_last_space(value):
"""Raise execption if last character is blank(space)"""
if re.search('\s$', value):
raise ValidationError(_("Last character is not valid."))
def validate_one_line_string(value):
"""Validate if invalid charcter is included in value.
Followings are regarded as valid.
- length <= 255
- consist of
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVwXYZ0123456789"
!#$%&'()*+,-./:;<=>?@[\]^_`{|}~
"""
base_pattern = STRING_VALIDATE_PATTERN
pattern = '^%s{1,%s}$' % (base_pattern, MAX_LENGTH)
if not re.match(pattern, value):
raise ValidationError(_('Invalid character is used '
'or exceeding maximum length.'))
validate_1st_space(value)
validate_last_space(value)
def validate_key_value_pairs(value):
"""Validation logic for execution input.
Check if value has u'A=B\r\nC=D...' format.
"""
value = value.replace('\r\n', '\n')
data = value.split('\n')
pattern = '^[^=]+=[^=]+$'
for datum in data:
# Skip validation if value is blank
if datum == '':
continue
if re.match(pattern, datum) is None:
raise ValidationError(_('Not key-value pair.'))
metadata_key, metadata_value = datum.split('=')
# Check key, value both by using validation logic for one line string.
validate_one_line_string(metadata_key)
validate_one_line_string(metadata_value)

16
requirements.txt Normal file
View File

@ -0,0 +1,16 @@
# 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.
# Order matters to the pip dependency resolver, so sorting this file
# changes how packages are installed. New dependencies should be
# added in alphabetical order, however, some dependencies may need to
# be installed in a specific order.
#
# PBR should always appear first
pbr!=2.1.0,>=2.0.0 # Apache-2.0
django-file-md5
python-qinlingclient>=1.1.0 # Apache-2.0
# This will be installed from git in OpenStack CI if the job setting
# required-projects for horizon:
horizon>=14.0.0.0b1 # Apache-2.0

34
setup.cfg Normal file
View File

@ -0,0 +1,34 @@
[metadata]
name = qinling-dashboard
summary = Qinling Management Dashboard
description-file =
README.rst
author = OpenStack
author-email = openstack-dev@lists.openstack.org
home-page = https://docs.openstack.org/qinling-dashboard/latest/
classifier =
Environment :: OpenStack
Intended Audience :: Information Technology
Intended Audience :: System Administrators
License :: OSI Approved :: Apache Software License
Operating System :: POSIX :: Linux
Programming Language :: Python
Programming Language :: Python :: 2
Programming Language :: Python :: 2.7
Programming Language :: Python :: 3
Programming Language :: Python :: 3.3
Programming Language :: Python :: 3.4
Programming Language :: Python :: 3.5
[files]
packages =
qinling_dashboard
[build_sphinx]
source-dir = doc/source
build-dir = doc/build
all_files = 1
warning-is-error = 1
[upload_sphinx]
upload-dir = doc/build/html

29
setup.py Normal file
View File

@ -0,0 +1,29 @@
# Copyright (c) 2013 Hewlett-Packard Development Company, L.P.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# THIS FILE IS MANAGED BY THE GLOBAL REQUIREMENTS REPO - DO NOT EDIT
import setuptools
# In python < 2.7.4, a lazy loading of package `pbr` will break
# setuptools if some other modules registered functions in `atexit`.
# solution from: http://bugs.python.org/issue15881#msg170215
try:
import multiprocessing # noqa
except ImportError:
pass
setuptools.setup(
setup_requires=['pbr>=2.0.0'],
pbr=True)

23
test-requirements.txt Normal file
View File

@ -0,0 +1,23 @@
# 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.
# Order matters to the pip dependency resolver, so sorting this file
# changes how packages are installed. New dependencies should be
# added in alphabetical order, however, some dependencies may need to
# be installed in a specific order.
#
# Hacking should appear first in case something else depends on pep8
hacking!=0.13.0,<0.14,>=0.12.0 # Apache-2.0
#
coverage!=4.4,>=4.0 # Apache-2.0
django-nose>=1.4.4 # BSD
doc8>=0.6.0 # Apache-2.0
flake8-import-order==0.12 # LGPLv3
mock>=2.0.0 # BSD
mox3>=0.20.0 # Apache-2.0
nose>=1.3.7 # LGPL
nose-exclude>=0.3.0 # LGPL
nosehtmloutput>=0.0.3 # Apache-2.0
nosexcover>=1.0.10 # BSD
openstack.nose-plugin>=0.7 # Apache-2.0
testtools>=2.2.0 # MIT

85
tox.ini Normal file
View File

@ -0,0 +1,85 @@
[tox]
envlist = py35,py3-dj111,pep8,docs
minversion = 2.3.2
skipsdist = True
[testenv]
usedevelop = True
setenv =
VIRTUAL_ENV={envdir}
INTEGRATION_TESTS=0
NOSE_WITH_OPENSTACK=1
NOSE_OPENSTACK_COLOR=1
NOSE_OPENSTACK_RED=0.05
NOSE_OPENSTACK_YELLOW=0.025
NOSE_OPENSTACK_SHOW_ELAPSED=1
deps = -c{env:UPPER_CONSTRAINTS_FILE:https://git.openstack.org/cgit/openstack/requirements/plain/upper-constraints.txt}
-r{toxinidir}/requirements.txt
-r{toxinidir}/test-requirements.txt
commands =
py27: {[unit_tests]commands}
py35: {[unit_tests]commands}
[unit_tests]
commands =
pip install -r requirements.txt
python manage.py test qinling_dashboard.test --settings=qinling_dashboard.test.settings
[testenv:pep8]
commands = flake8 {posargs}
[testenv:venv]
commands = {posargs}
[testenv:cover]
commands =
coverage erase
coverage run --source=qinling_dashboard {toxinidir}/manage.py test qinling_dashboard.test.tests --settings=qinling_dashboard.test.settings {posargs}
coverage xml
coverage html
[testenv:py35]
basepython = python3
commands = {[unit_tests]commands}
[testenv:py3-dj111]
basepython = python3
commands =
pip install django>=1.11,<2
{[unit_tests]commands}
[testenv:docs]
deps = -c{env:UPPER_CONSTRAINTS_FILE:https://git.openstack.org/cgit/openstack/requirements/plain/upper-constraints.txt}
-r{toxinidir}/doc/requirements.txt
commands = python setup.py build_sphinx
[testenv:releasenotes]
deps = -c{env:UPPER_CONSTRAINTS_FILE:https://git.openstack.org/cgit/openstack/requirements/plain/upper-constraints.txt}
-r{toxinidir}/doc/requirements.txt
commands = sphinx-build -a -E -W -d releasenotes/build/doctrees -b html releasenotes/source releasenotes/build/html
[hacking]
local-check-factory = horizon.hacking.checks.factory
[flake8]
exclude = .venv,.git,.tox,dist,*lib/python*,*egg,build,panel_template,dash_template,local_settings.py,*/local/*,*/test/test_plugins/*,.ropeproject,node_modules,.tmp
max-complexity = 20
import-order-style = pep8
[doc8]
# File extensions to check
extensions = .rst, .yaml
# Maximal line length should be 80 but we have some overlong lines.
# Let's not get far more in.
max-line-length = 80
# Disable some doc8 checks:
# D000: Check RST validity
# - cannot handle "none" for code-block directive
ignore = D000
[testenv:lower-constraints]
basepython = python3
deps =
-c{toxinidir}/lower-constraints.txt
-r{toxinidir}/test-requirements.txt
-r{toxinidir}/requirements.txt