Initial commit for qinling-dashboard
This commit is contained in:
commit
8211b51a40
|
@ -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/*
|
|
@ -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
|
|
@ -0,0 +1,4 @@
|
||||||
|
OpenStack Style Commandments
|
||||||
|
============================
|
||||||
|
|
||||||
|
Read the OpenStack Style Commandments https://docs.openstack.org/hacking/latest/
|
|
@ -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.
|
|
@ -0,0 +1,4 @@
|
||||||
|
recursive-include qinling_dashboard *.html *.scss *.css *.js *.map *.svg *.png *.json
|
||||||
|
|
||||||
|
include AUTHORS
|
||||||
|
include ChangeLog
|
|
@ -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
|
|
@ -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
|
|
@ -0,0 +1,2 @@
|
||||||
|
# settings file for qinling-dashboard plugin
|
||||||
|
enable_service qinling-dashboard
|
|
@ -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
|
|
@ -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}
|
|
@ -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.
|
|
@ -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
|
|
@ -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.
|
|
@ -0,0 +1,9 @@
|
||||||
|
=========================
|
||||||
|
Contributor Documentation
|
||||||
|
=========================
|
||||||
|
|
||||||
|
.. toctree::
|
||||||
|
:maxdepth: 2
|
||||||
|
|
||||||
|
contributing
|
||||||
|
devstack
|
|
@ -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>
|
|
@ -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
|
|
@ -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)
|
|
@ -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",
|
||||||
|
]
|
|
@ -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)
|
|
@ -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"
|
||||||
|
}
|
|
@ -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)
|
|
@ -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',)
|
|
@ -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,)
|
|
@ -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
|
|
@ -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 %}
|
|
@ -0,0 +1,2 @@
|
||||||
|
{% load i18n %}
|
||||||
|
<pre class="logs">{{ execution_logs }}</pre>
|
|
@ -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>
|
|
@ -0,0 +1,3 @@
|
||||||
|
{% load i18n %}
|
||||||
|
<b>{% trans 'Duration' %}</b>: {{ duration }}<br/>
|
||||||
|
<b>{% trans 'Output' %}</b>: {{ output }}
|
|
@ -0,0 +1,7 @@
|
||||||
|
{% extends 'base.html' %}
|
||||||
|
{% load i18n %}
|
||||||
|
{% block title %}{% trans "Create Execution" %}{% endblock %}
|
||||||
|
|
||||||
|
{% block main %}
|
||||||
|
{% include 'project/executions/_create.html' %}
|
||||||
|
{% endblock %}
|
|
@ -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 %}
|
|
@ -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'),
|
||||||
|
]
|
|
@ -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')
|
|
@ -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)
|
|
@ -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',)
|
|
@ -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,)
|
|
@ -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
|
|
@ -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 %}
|
|
@ -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 %}
|
|
@ -0,0 +1,3 @@
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{{ table.render }}
|
|
@ -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>
|
|
@ -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>
|
|
@ -0,0 +1,3 @@
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{{ table.render }}
|
|
@ -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 %}
|
|
@ -0,0 +1,7 @@
|
||||||
|
{% extends 'base.html' %}
|
||||||
|
{% load i18n %}
|
||||||
|
{% block title %}{% trans "Create Function" %}{% endblock %}
|
||||||
|
|
||||||
|
{% block main %}
|
||||||
|
{% include 'project/functions/_create_function.html' %}
|
||||||
|
{% endblock %}
|
|
@ -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 %}
|
|
@ -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 %}
|
|
@ -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 %}
|
|
@ -0,0 +1,7 @@
|
||||||
|
{% extends 'base.html' %}
|
||||||
|
{% load i18n %}
|
||||||
|
{% block title %}{% trans "Update Function" %}{% endblock %}
|
||||||
|
|
||||||
|
{% block main %}
|
||||||
|
{% include 'project/executions/_update_function.html' %}
|
||||||
|
{% endblock %}
|
|
@ -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'),
|
||||||
|
]
|
|
@ -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())
|
|
@ -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',)
|
|
@ -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,)
|
|
@ -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
|
|
@ -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>
|
|
@ -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 %}
|
|
@ -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'),
|
||||||
|
]
|
|
@ -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')
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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,
|
||||||
|
)
|
|
@ -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
|
|
@ -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))
|
|
@ -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)
|
|
@ -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)
|
|
@ -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)
|
|
@ -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)
|
|
@ -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")
|
|
@ -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),
|
||||||
|
])
|
|
@ -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),
|
||||||
|
])
|
|
@ -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),
|
||||||
|
])
|
|
@ -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))
|
|
@ -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")),
|
||||||
|
)
|
|
@ -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)
|
|
@ -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
|
|
@ -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
|
|
@ -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)
|
|
@ -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
|
|
@ -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
|
Loading…
Reference in New Issue