Retire the qinling-dashboard project
As announced in openstack-discuss ML[1], Qinling project is retiring in Wallaby cycle. This commit retires the qinling-dashboard repository as per process deinfed in project-guide[2]. Anyone would like to maintain it again, please revert back this commit and propose the re-adding it to governance. The community wishes to express our thanks and appreciation to all of those who have contributed to the qinling-dashboard project over the years. Depends-On: https://review.opendev.org/c/openstack/project-config/+/764520 Needed-By: https://review.opendev.org/c/openstack/governance/+/764523 [1] http://lists.openstack.org/pipermail/openstack-discuss/2020-November/018638.html [2] https://docs.openstack.org/project-team-guide/repository.html#retiring-a-repository Change-Id: Ib1953185ed61f237040a9e4e5e727fd5e2befdb8
This commit is contained in:
parent
6611e136f1
commit
ce8599cf34
108
.gitignore
vendored
108
.gitignore
vendored
@ -1,108 +0,0 @@
|
||||
# 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/*
|
@ -1,7 +0,0 @@
|
||||
- project:
|
||||
templates:
|
||||
- check-requirements
|
||||
- horizon-non-primary-django-jobs
|
||||
- openstack-python3-victoria-jobs-horizon
|
||||
- publish-openstack-docs-pti
|
||||
- release-notes-jobs-python3
|
@ -1,17 +0,0 @@
|
||||
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/1050
|
@ -1,4 +0,0 @@
|
||||
OpenStack Style Commandments
|
||||
============================
|
||||
|
||||
Read the OpenStack Style Commandments https://docs.openstack.org/hacking/latest/
|
201
LICENSE
201
LICENSE
@ -1,201 +0,0 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
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.
|
@ -1,4 +0,0 @@
|
||||
recursive-include qinling_dashboard *.html *.scss *.css *.js *.map *.svg *.png *.json
|
||||
|
||||
include AUTHORS
|
||||
include ChangeLog
|
22
README.rst
22
README.rst
@ -1,16 +1,10 @@
|
||||
=============================
|
||||
Welcome to Qinling Dashboard!
|
||||
=============================
|
||||
This project is no longer maintained.
|
||||
|
||||
Qinling dashboard is a horizon plugin for Qinling.
|
||||
The contents of this repository are still available in the Git
|
||||
source code management system. To see the contents of this
|
||||
repository before it reached its end of life, please check out the
|
||||
previous commit with "git checkout HEAD^1".
|
||||
|
||||
* 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
|
||||
For any further questions, please email
|
||||
openstack-discuss@lists.openstack.org or join #openstack-dev on
|
||||
Freenode.
|
||||
|
@ -1,3 +0,0 @@
|
||||
[python: **.py]
|
||||
[django: **/templates/**.html]
|
||||
[django: **/templates/**.csv]
|
@ -1,2 +0,0 @@
|
||||
[javascript: **.js]
|
||||
[angular: **/static/**.html]
|
@ -1,61 +0,0 @@
|
||||
# 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
|
@ -1,2 +0,0 @@
|
||||
# settings file for qinling-dashboard plugin
|
||||
enable_service qinling-dashboard
|
@ -1,16 +0,0 @@
|
||||
# The order of packages is significant, because pip processes them in the order
|
||||
# of appearance. Changing the order has an impact on the overall integration
|
||||
# process, which may cause wedges in the gate later.
|
||||
# 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>=2.2.1 # Apache-2.0
|
||||
reno>=3.1.0 # Apache-2.0
|
||||
sphinx>=2.0.0,!=2.1.0 # BSD
|
||||
sphinxcontrib-apidoc>=0.2.0 # BSD
|
||||
sphinxcontrib-svg2pdfconverter>=0.1.0 # BSD
|
||||
|
||||
# NOTE: The following are required as horizon.test.settings loads it.
|
||||
django-nose>=1.4.4 # BSD
|
@ -1,105 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
# implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import os
|
||||
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',
|
||||
'sphinxcontrib.rsvgconverter',
|
||||
# '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
|
||||
openstackdocs_repo_name = 'openstack/qinling-dashboard'
|
||||
openstackdocs_pdf_link = True
|
||||
openstackdocs_auto_name = False
|
||||
openstackdocs_use_storyboard = True
|
||||
|
||||
# If true, '()' will be appended to :func: etc. cross-reference text.
|
||||
add_function_parentheses = True
|
||||
|
||||
# If true, the current module name will be prepended to all description
|
||||
# unit titles (such as .. function::).
|
||||
add_module_names = True
|
||||
|
||||
# The name of the Pygments (syntax highlighting) style to use.
|
||||
pygments_style = 'native'
|
||||
|
||||
# -- Options for HTML output --------------------------------------------------
|
||||
|
||||
# The theme to use for HTML and HTML Help pages. Major themes that come with
|
||||
# Sphinx are currently 'default' and 'sphinxdoc'.
|
||||
# html_theme_path = ["."]
|
||||
# html_theme = '_theme'
|
||||
# html_static_path = ['static']
|
||||
html_theme = 'openstackdocs'
|
||||
|
||||
# Output file base name for HTML help builder.
|
||||
htmlhelp_basename = '%sdoc' % project
|
||||
|
||||
# -- Options for LaTeX output ---------------------------------------------
|
||||
|
||||
# Disable usage of xindy https://bugzilla.redhat.com/show_bug.cgi?id=1643664
|
||||
latex_use_xindy = False
|
||||
|
||||
latex_domain_indices = False
|
||||
|
||||
latex_elements = {
|
||||
'makeindex': '',
|
||||
'printindex': '',
|
||||
'preamble': r'\setcounter{tocdepth}{3}',
|
||||
}
|
||||
|
||||
# Grouping the document tree into LaTeX files. List of tuples
|
||||
# (source start file, target name, title,
|
||||
# author, documentclass [howto, manual, or own class]).
|
||||
# NOTE: Specify toctree_only=True for a better document structure of
|
||||
# the generated PDF file.
|
||||
latex_documents = [
|
||||
('index',
|
||||
'doc-qinling-dashboard.tex',
|
||||
u'%s Documentation' % project,
|
||||
u'OpenStack Developers', 'manual', True),
|
||||
]
|
||||
|
||||
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}
|
@ -1,10 +0,0 @@
|
||||
=============
|
||||
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.
|
@ -1,30 +0,0 @@
|
||||
=================
|
||||
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/1050
|
||||
|
||||
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
|
@ -1,13 +0,0 @@
|
||||
=================================
|
||||
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.
|
@ -1,9 +0,0 @@
|
||||
=========================
|
||||
Contributor Documentation
|
||||
=========================
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
contributing
|
||||
devstack
|
@ -1,18 +0,0 @@
|
||||
.. 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>
|
@ -1,76 +0,0 @@
|
||||
====================================
|
||||
Qinling Dashboard installation guide
|
||||
====================================
|
||||
|
||||
This page describes the manual installation of qinling-dashboard,
|
||||
while distribution packages may provide more automated process.
|
||||
|
||||
.. note::
|
||||
|
||||
This page assumes horizon has been installed.
|
||||
Horizon setup is beyond the scope of this page.
|
||||
|
||||
Install Qinling Dashboard with all relevant packages to your Horizon environment.
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
pip install qinling-dashboard
|
||||
|
||||
In most cases, qinling-dashboard is installed into your python "site-packages"
|
||||
directory like ``/usr/local/lib/python2.7/site-packages``.
|
||||
We refer to the directory of qinling-dashboard as ``<qinling-dashboard-dir>`` below
|
||||
and it would be ``<site-packages>/qinling_dashboard`` if installed via pip.
|
||||
The path varies depending on Linux distribution you use.
|
||||
|
||||
To enable qinling-dashboard plugin, you need to put horizon plugin setup files
|
||||
into horizon "enabled" directory.
|
||||
|
||||
The plugin setup files are found in ``<qinling-dashboard-dir>/enabled``.
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
$ cp <qinling-dashboard-dir>/enabled/_[1-9]*.py \
|
||||
/usr/share/openstack-dashboard/openstack_dashboard/local/enabled
|
||||
|
||||
.. note::
|
||||
|
||||
The directory ``local/enabled`` may be different depending on your
|
||||
environment or distribution used. The path above is one used in Ubuntu
|
||||
horizon package.
|
||||
|
||||
Configure the policy file for qinling-dashboard in OpenStack Dashboard
|
||||
``local_settings.py``.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
POLICY_FILES['function_engine'] = '<qinling-dashboard-dir>/conf/qinling_policy.json'
|
||||
|
||||
.. note::
|
||||
|
||||
If your ``local_settings.py`` has no ``POLICY_FILES`` yet,
|
||||
you need to define the default ``POLICY_FILES`` in
|
||||
``local_settings.py``. If you use the example ``local_settings.py`` file
|
||||
from horizon, what you need is to uncomment ``POLICY_FILES`` (which contains
|
||||
the default values).
|
||||
|
||||
Compile the translation message catalogs of qinling-dashboard.
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
$ cd <qinling-dashboard-dir>
|
||||
$ python ./manage.py compilemessages
|
||||
|
||||
Run the Django update commands.
|
||||
Note that ``compress`` is required when you enable compression.
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
$ cd <horizon-dir>
|
||||
$ DJANGO_SETTINGS_MODULE=openstack_dashboard.settings python manage.py collectstatic --noinput
|
||||
$ DJANGO_SETTINGS_MODULE=openstack_dashboard.settings python manage.py compress --force
|
||||
|
||||
Finally, restart your web server. For example, in case of apache:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
$ sudo service apache2 restart
|
23
manage.py
23
manage.py
@ -1,23 +0,0 @@
|
||||
#!/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)
|
@ -1,39 +0,0 @@
|
||||
# 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",
|
||||
]
|
@ -1,184 +0,0 @@
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import json
|
||||
|
||||
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 runtime_create(request, **params):
|
||||
resource = qinlingclient(request).runtimes.create(**params)
|
||||
return resource
|
||||
|
||||
|
||||
def set_code(datum):
|
||||
if isinstance(datum.code, str):
|
||||
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, str):
|
||||
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)
|
@ -1,20 +0,0 @@
|
||||
{
|
||||
"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",
|
||||
|
||||
"runtime:create": "rule:context_is_admin",
|
||||
|
||||
"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"
|
||||
}
|
@ -1,134 +0,0 @@
|
||||
# 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:
|
||||
redirect = reverse("horizon:project:executions:index")
|
||||
exceptions.handle(request,
|
||||
_("Unable to create execution."),
|
||||
redirect=redirect)
|
@ -1,22 +0,0 @@
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
# implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
import horizon
|
||||
|
||||
|
||||
class Executions(horizon.Panel):
|
||||
name = _("Executions")
|
||||
slug = "executions"
|
||||
permissions = ('openstack.services.function-engine',)
|
@ -1,172 +0,0 @@
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from 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,)
|
@ -1,49 +0,0 @@
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import 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
|
@ -1,6 +0,0 @@
|
||||
{% 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 %}
|
@ -1,2 +0,0 @@
|
||||
{% load i18n %}
|
||||
<pre class="logs">{{ execution_logs }}</pre>
|
@ -1,57 +0,0 @@
|
||||
{% 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>
|
@ -1,3 +0,0 @@
|
||||
{% load i18n %}
|
||||
<b>{% trans 'Duration' %}</b>: {{ duration }}<br/>
|
||||
<b>{% trans 'Output' %}</b>: {{ output }}
|
@ -1,7 +0,0 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load i18n %}
|
||||
{% block title %}{% trans "Create Execution" %}{% endblock %}
|
||||
|
||||
{% block main %}
|
||||
{% include 'project/executions/_create.html' %}
|
||||
{% endblock %}
|
@ -1,11 +0,0 @@
|
||||
{% 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 %}
|
@ -1,25 +0,0 @@
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from 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'),
|
||||
]
|
@ -1,107 +0,0 @@
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from 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')
|
@ -1,455 +0,0 @@
|
||||
# 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 horizon import exceptions
|
||||
from horizon import forms
|
||||
from horizon import messages
|
||||
|
||||
from qinling_dashboard import api
|
||||
|
||||
from qinling_dashboard.utils import calculate_md5
|
||||
|
||||
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:
|
||||
redirect = reverse("horizon:project:functions:index")
|
||||
msg = _("Unable to create execution.")
|
||||
exceptions.handle(request, msg, redirect=redirect)
|
@ -1,22 +0,0 @@
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
# implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
import horizon
|
||||
|
||||
|
||||
class Functions(horizon.Panel):
|
||||
name = _("Functions")
|
||||
slug = "functions"
|
||||
permissions = ('openstack.services.function-engine',)
|
@ -1,291 +0,0 @@
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from 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,)
|
@ -1,83 +0,0 @@
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import 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
|
@ -1,7 +0,0 @@
|
||||
{% 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 %}
|
@ -1,6 +0,0 @@
|
||||
{% 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 %}
|
@ -1,3 +0,0 @@
|
||||
{% load i18n %}
|
||||
|
||||
{{ table.render }}
|
@ -1,58 +0,0 @@
|
||||
{% 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>
|
@ -1,31 +0,0 @@
|
||||
{% 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>
|
@ -1,3 +0,0 @@
|
||||
{% load i18n %}
|
||||
|
||||
{{ table.render }}
|
@ -1,7 +0,0 @@
|
||||
{% 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 %}
|
@ -1,7 +0,0 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load i18n %}
|
||||
{% block title %}{% trans "Create Function" %}{% endblock %}
|
||||
|
||||
{% block main %}
|
||||
{% include 'project/functions/_create_function.html' %}
|
||||
{% endblock %}
|
@ -1,7 +0,0 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load i18n %}
|
||||
{% block title %}{% trans "Create Function Version" %}{% endblock %}
|
||||
|
||||
{% block main %}
|
||||
{% include 'project/executions/_create_version.html' %}
|
||||
{% endblock %}
|
@ -1,11 +0,0 @@
|
||||
{% 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 %}
|
@ -1,11 +0,0 @@
|
||||
{% 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 %}
|
@ -1,7 +0,0 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load i18n %}
|
||||
{% block title %}{% trans "Update Function" %}{% endblock %}
|
||||
|
||||
{% block main %}
|
||||
{% include 'project/executions/_update_function.html' %}
|
||||
{% endblock %}
|
@ -1,42 +0,0 @@
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from 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'),
|
||||
]
|
@ -1,309 +0,0 @@
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import io
|
||||
|
||||
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())
|
@ -1,88 +0,0 @@
|
||||
# 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 horizon import exceptions
|
||||
from horizon import forms
|
||||
from horizon import messages
|
||||
|
||||
from qinling_dashboard import api
|
||||
|
||||
# from qinling_dashboard.utils import calculate_md5
|
||||
|
||||
from qinling_dashboard import validators
|
||||
|
||||
|
||||
class CreateRuntimeForm(forms.SelfHandlingForm):
|
||||
|
||||
image = forms.CharField(max_length=255,
|
||||
label=_("Image"),
|
||||
validators=[validators.validate_one_line_string],
|
||||
required=True)
|
||||
|
||||
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)
|
||||
|
||||
untrusted = forms.BooleanField(
|
||||
label=_("Create as Untrusted Image"),
|
||||
required=False,
|
||||
help_text=_("Check this item if you would like to "
|
||||
"create untrusted runtime"),
|
||||
initial=False
|
||||
)
|
||||
|
||||
def __init__(self, request, *args, **kwargs):
|
||||
super(CreateRuntimeForm, self).__init__(request, *args, **kwargs)
|
||||
|
||||
def handle(self, request, context):
|
||||
params = {}
|
||||
|
||||
# basic parameters
|
||||
params.update({'image': context.get('image')})
|
||||
|
||||
if context.get('name'):
|
||||
params.update({'name': context.get('name')})
|
||||
|
||||
if context.get('description'):
|
||||
params.update({'description': context.get('description')})
|
||||
|
||||
if context.get('untrusted'):
|
||||
trusted = not bool(context.get('untrusted'))
|
||||
params.update({'trusted': trusted})
|
||||
|
||||
try:
|
||||
api.qinling.runtime_create(request, **params)
|
||||
message = _('Created runtime "%s"') % params.get('name',
|
||||
'unknown name')
|
||||
messages.success(request, message)
|
||||
return True
|
||||
except Exception:
|
||||
redirect = reverse("horizon:project:runtimes:index")
|
||||
exceptions.handle(request,
|
||||
_("Unable to create runtime."),
|
||||
redirect=redirect)
|
@ -1,22 +0,0 @@
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
# implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
import horizon
|
||||
|
||||
|
||||
class Runtimes(horizon.Panel):
|
||||
name = _("Runtimes")
|
||||
slug = "runtimes"
|
||||
permissions = ('openstack.services.function-engine',)
|
@ -1,92 +0,0 @@
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from horizon import tables
|
||||
|
||||
from qinling_dashboard import api
|
||||
from qinling_dashboard import utils
|
||||
|
||||
|
||||
class CreateRuntime(tables.LinkAction):
|
||||
name = "create"
|
||||
verbose_name = _("Create Runtime")
|
||||
url = "horizon:project:runtimes:create"
|
||||
classes = ("ajax-modal", "btn-create")
|
||||
policy_rules = (("function_engine", "runtime:create"),)
|
||||
icon = "plus"
|
||||
ajax = True
|
||||
|
||||
|
||||
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 = (CreateRuntime,
|
||||
RuntimesFilterAction,)
|
@ -1,37 +0,0 @@
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import 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
|
@ -1,6 +0,0 @@
|
||||
{% extends "horizon/common/_modal_form.html" %}
|
||||
{% load i18n %}
|
||||
{% block modal-body-right %}
|
||||
<h3>{% trans "Description:" %}</h3>
|
||||
<p>{% trans "Create a new runtime." %}</p>
|
||||
{% endblock %}
|
@ -1,38 +0,0 @@
|
||||
{% 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>
|
@ -1,7 +0,0 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load i18n %}
|
||||
{% block title %}{% trans "Create Runtime" %}{% endblock %}
|
||||
|
||||
{% block main %}
|
||||
{% include 'project/runtimes/_create.html' %}
|
||||
{% endblock %}
|
@ -1,11 +0,0 @@
|
||||
{% 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 %}
|
@ -1,22 +0,0 @@
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from 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'),
|
||||
url(r'^create', views.CreateRuntimeView.as_view(), name='create'),
|
||||
]
|
@ -1,94 +0,0 @@
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from 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.runtimes import forms as project_forms
|
||||
from qinling_dashboard.content.runtimes import tables as project_tables
|
||||
from qinling_dashboard.content.runtimes import tabs as project_tabs
|
||||
|
||||
|
||||
class CreateRuntimeView(forms.ModalFormView):
|
||||
|
||||
form_class = project_forms.CreateRuntimeForm
|
||||
modal_header = submit_label = page_title = _("Create Runtime")
|
||||
template_name = 'project/runtimes/create.html'
|
||||
submit_url = reverse_lazy("horizon:project:runtimes:create")
|
||||
success_url = reverse_lazy("horizon:project:runtimes:index")
|
||||
|
||||
|
||||
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')
|
@ -1,30 +0,0 @@
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from 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
|
||||
}
|
@ -1,24 +0,0 @@
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
# 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
|
@ -1,24 +0,0 @@
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
# 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
|
@ -1,24 +0,0 @@
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
# 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
|
@ -1,28 +0,0 @@
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from qinlingclient.common import exceptions as exc
|
||||
|
||||
|
||||
UNAUTHORIZED = (
|
||||
exc.Unauthorized,
|
||||
)
|
||||
|
||||
|
||||
NOT_FOUND = (
|
||||
exc.NotFound,
|
||||
)
|
||||
|
||||
|
||||
RECOVERABLE = (
|
||||
exc.HTTPException,
|
||||
)
|
@ -1,462 +0,0 @@
|
||||
# Andi Chandler <andi@gowling.com>, 2018. #zanata
|
||||
# Andi Chandler <andi@gowling.com>, 2020. #zanata
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: qinling-dashboard VERSION\n"
|
||||
"Report-Msgid-Bugs-To: https://bugs.launchpad.net/openstack-i18n/\n"
|
||||
"POT-Creation-Date: 2020-09-11 21:48+0000\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"PO-Revision-Date: 2020-06-15 05:40+0000\n"
|
||||
"Last-Translator: Andi Chandler <andi@gowling.com>\n"
|
||||
"Language-Team: English (United Kingdom)\n"
|
||||
"Language: en_GB\n"
|
||||
"X-Generator: Zanata 4.3.3\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1)\n"
|
||||
|
||||
msgid "1st character is not valid."
|
||||
msgstr "1st character is not valid."
|
||||
|
||||
msgctxt "current status of runtime"
|
||||
msgid "Available"
|
||||
msgstr "Available"
|
||||
|
||||
msgid "CPU"
|
||||
msgstr "CPU"
|
||||
|
||||
msgid "CPU (Milli CPU)"
|
||||
msgstr "CPU (Milli CPU)"
|
||||
|
||||
msgid "CPU (Milli CPU)"
|
||||
msgstr "CPU (Milli CPU)"
|
||||
|
||||
msgctxt "current status of runtime"
|
||||
msgid "Cancelled"
|
||||
msgstr "Cancelled"
|
||||
|
||||
msgid "Check this item if you would like sync execution."
|
||||
msgstr "Check this item if you would like sync execution."
|
||||
|
||||
msgid "Check this item if you would like to create untrusted runtime"
|
||||
msgstr "Check this item if you would like to create untrusted runtime"
|
||||
|
||||
msgid "Code"
|
||||
msgstr "Code"
|
||||
|
||||
msgid "Code Type"
|
||||
msgstr "Code Type"
|
||||
|
||||
msgid "Code Type can not be changed when you update function."
|
||||
msgstr "Code Type can not be changed when you update function."
|
||||
|
||||
msgid "Code package zip file path."
|
||||
msgstr "Code package zip file path."
|
||||
|
||||
msgid "Container name in Swift."
|
||||
msgstr "Container name in Swift."
|
||||
|
||||
msgid "Count"
|
||||
msgstr "Count"
|
||||
|
||||
msgid "Create Execution"
|
||||
msgstr "Create Execution"
|
||||
|
||||
msgid "Create Function"
|
||||
msgstr "Create Function"
|
||||
|
||||
msgid "Create Function Version"
|
||||
msgstr "Create Function Version"
|
||||
|
||||
msgid "Create Runtime"
|
||||
msgstr "Create Runtime"
|
||||
|
||||
msgid "Create Version"
|
||||
msgstr "Create Version"
|
||||
|
||||
msgid "Create a new execution of function."
|
||||
msgstr "Create a new execution of function."
|
||||
|
||||
msgid "Create a new function."
|
||||
msgstr "Create a new function."
|
||||
|
||||
msgid "Create a new runtime."
|
||||
msgstr "Create a new runtime."
|
||||
|
||||
msgid "Create a new version of function."
|
||||
msgstr "Create a new version of function."
|
||||
|
||||
msgid "Create as Untrusted Image"
|
||||
msgstr "Create as Untrusted Image"
|
||||
|
||||
msgid "Created At"
|
||||
msgstr "Created At"
|
||||
|
||||
#, python-format
|
||||
msgid "Created execution of \"%s\""
|
||||
msgstr "Created execution of \"%s\""
|
||||
|
||||
#, python-format
|
||||
msgid "Created function \"%s\""
|
||||
msgstr "Created function \"%s\""
|
||||
|
||||
#, python-format
|
||||
msgid "Created new version of \"%s\""
|
||||
msgstr "Created new version of \"%s\""
|
||||
|
||||
#, python-format
|
||||
msgid "Created runtime \"%s\""
|
||||
msgstr "Created runtime \"%s\""
|
||||
|
||||
msgctxt "current status of runtime"
|
||||
msgid "Creating"
|
||||
msgstr "Creating"
|
||||
|
||||
msgid "Delete Execution"
|
||||
msgid_plural "Delete Executions"
|
||||
msgstr[0] "Delete Execution"
|
||||
msgstr[1] "Delete Executions"
|
||||
|
||||
msgid "Delete Function"
|
||||
msgid_plural "Delete Functions"
|
||||
msgstr[0] "Delete Function"
|
||||
msgstr[1] "Delete Functions"
|
||||
|
||||
msgid "Delete Function Version"
|
||||
msgid_plural "Delete Function Versions"
|
||||
msgstr[0] "Delete Function Version"
|
||||
msgstr[1] "Delete Function Versions"
|
||||
|
||||
msgid "Deleted executions are not recoverable."
|
||||
msgstr "Deleted executions are not recoverable."
|
||||
|
||||
msgid "Deleted function versions are not recoverable."
|
||||
msgstr "Deleted function versions are not recoverable."
|
||||
|
||||
msgid "Deleted functions are not recoverable."
|
||||
msgstr "Deleted functions are not recoverable."
|
||||
|
||||
msgctxt "current status of runtime"
|
||||
msgid "Deleting"
|
||||
msgstr "Deleting"
|
||||
|
||||
msgid "Description"
|
||||
msgstr "Description"
|
||||
|
||||
msgid "Description:"
|
||||
msgstr "Description:"
|
||||
|
||||
msgctxt "current status of runtime"
|
||||
msgid "Done"
|
||||
msgstr "Done"
|
||||
|
||||
msgid "Download Function"
|
||||
msgstr "Download Function"
|
||||
|
||||
msgid "Duration"
|
||||
msgstr "Duration"
|
||||
|
||||
msgid "Entry"
|
||||
msgstr "Entry"
|
||||
|
||||
msgctxt "current status of runtime"
|
||||
msgid "Error"
|
||||
msgstr "Error"
|
||||
|
||||
#, python-format
|
||||
msgid "Error because file not found function: %s"
|
||||
msgstr "Error because file not found function: %s"
|
||||
|
||||
#, python-format
|
||||
msgid "Error downloading function: %s"
|
||||
msgstr "Error downloading function: %s"
|
||||
|
||||
msgid "Execution Details"
|
||||
msgstr "Execution Details"
|
||||
|
||||
msgid "Execution Details: {{ resource_name }}"
|
||||
msgstr "Execution Details: {{ resource_name }}"
|
||||
|
||||
msgid "Execution Logs"
|
||||
msgstr "Execution Logs"
|
||||
|
||||
msgid "Executions"
|
||||
msgstr "Executions"
|
||||
|
||||
msgid "Executions of this function"
|
||||
msgstr "Executions of this function"
|
||||
|
||||
msgctxt "current status of runtime"
|
||||
msgid "Failed"
|
||||
msgstr "Failed"
|
||||
|
||||
msgid "Function"
|
||||
msgstr "Function"
|
||||
|
||||
msgid "Function Details"
|
||||
msgstr "Function Details"
|
||||
|
||||
msgid "Function Details: {{ resource_name }}"
|
||||
msgstr "Function Details: {{ resource_name }}"
|
||||
|
||||
msgid "Function Engine"
|
||||
msgstr "Function Engine"
|
||||
|
||||
msgid "Function ID"
|
||||
msgstr "Function ID"
|
||||
|
||||
msgid "Function Version"
|
||||
msgstr "Function Version"
|
||||
|
||||
msgid "Function Version Details"
|
||||
msgstr "Function Version Details"
|
||||
|
||||
msgid ""
|
||||
"Function Version Details: {{ version.function_id }} (Version "
|
||||
"Number={{ version_number }})"
|
||||
msgstr ""
|
||||
"Function Version Details: {{ version.function_id }} (Version "
|
||||
"Number={{ version_number }})"
|
||||
|
||||
msgid "Function entry in the format of <module_name>.<method_name>"
|
||||
msgstr "Function entry in the format of <module_name>.<method_name>"
|
||||
|
||||
msgid "Function to execute."
|
||||
msgstr "Function to execute."
|
||||
|
||||
msgid "Function which new version will be created."
|
||||
msgstr "Function which new version will be created."
|
||||
|
||||
msgid "Functions"
|
||||
msgstr "Functions"
|
||||
|
||||
msgid "ID"
|
||||
msgstr "ID"
|
||||
|
||||
msgid "Id"
|
||||
msgstr "Id"
|
||||
|
||||
msgid "Image"
|
||||
msgstr "Image"
|
||||
|
||||
msgid "Image name in Docker hub."
|
||||
msgstr "Image name in Docker hub."
|
||||
|
||||
msgid "Input"
|
||||
msgstr "Input"
|
||||
|
||||
msgid "Invalid character is used or exceeding maximum length."
|
||||
msgstr "Invalid character is used or exceeding maximum length."
|
||||
|
||||
msgid "Is Public"
|
||||
msgstr "Is Public"
|
||||
|
||||
msgid "Last character is not valid."
|
||||
msgstr "Last character is not valid."
|
||||
|
||||
msgid "Limit of cpu resource(unit: millicpu). Range: {0} ~ {1}"
|
||||
msgstr "Limit of CPU resource(unit: millicpu). Range: {0} ~ {1}"
|
||||
|
||||
msgid "Limit of memory resource(unit: bytes). Range: {0} ~ {1} (bytes)."
|
||||
msgstr "Limit of memory resource(unit: bytes). Range: {0} ~ {1} (bytes)."
|
||||
|
||||
msgid "Md5sum"
|
||||
msgstr "MD5sum"
|
||||
|
||||
msgid "Memory Size"
|
||||
msgstr "Memory Size"
|
||||
|
||||
msgid "Memory Size (Bytes)"
|
||||
msgstr "Memory Size (Bytes)"
|
||||
|
||||
msgid "Name"
|
||||
msgstr "Name"
|
||||
|
||||
msgid "No functions available."
|
||||
msgstr "No functions available."
|
||||
|
||||
msgid "No runtimes found"
|
||||
msgstr "No runtimes found"
|
||||
|
||||
msgid "Not key-value pair."
|
||||
msgstr "Not key-value pair."
|
||||
|
||||
msgid "Object name in Swift."
|
||||
msgstr "Object name in Swift."
|
||||
|
||||
msgid "Output"
|
||||
msgstr "Output"
|
||||
|
||||
msgid "Overview"
|
||||
msgstr "Overview"
|
||||
|
||||
msgid "Package"
|
||||
msgstr "Package"
|
||||
|
||||
msgctxt "current status of runtime"
|
||||
msgid "Paused"
|
||||
msgstr "Paused"
|
||||
|
||||
msgid "Project ID"
|
||||
msgstr "Project ID"
|
||||
|
||||
msgid "Result"
|
||||
msgstr "Result"
|
||||
|
||||
msgctxt "current status of runtime"
|
||||
msgid "Running"
|
||||
msgstr "Running"
|
||||
|
||||
msgid "Runtime"
|
||||
msgstr "Runtime"
|
||||
|
||||
msgid "Runtime Details"
|
||||
msgstr "Runtime Details"
|
||||
|
||||
msgid "Runtime Details: {{ resource_name }}"
|
||||
msgstr "Runtime Details: {{ resource_name }}"
|
||||
|
||||
msgid "Runtime ID"
|
||||
msgstr "Runtime ID"
|
||||
|
||||
msgid "Runtimes"
|
||||
msgstr "Runtimes"
|
||||
|
||||
msgid "Select Your Code Type."
|
||||
msgstr "Select Your Code Type."
|
||||
|
||||
msgid "Show Execution Logs"
|
||||
msgstr "Show Execution Logs"
|
||||
|
||||
msgid "Source"
|
||||
msgstr "Source"
|
||||
|
||||
msgid ""
|
||||
"Specify input parmeters like name1=value2. One line is equivalent of one "
|
||||
"input parameter."
|
||||
msgstr ""
|
||||
"Specify input parameters like name1=value2. One line is equivalent of one "
|
||||
"input parameter."
|
||||
|
||||
msgid "Status"
|
||||
msgstr "Status"
|
||||
|
||||
msgctxt "current status of runtime"
|
||||
msgid "Success"
|
||||
msgstr "Success"
|
||||
|
||||
msgid "Swift Container"
|
||||
msgstr "Swift Container"
|
||||
|
||||
msgid "Swift Object"
|
||||
msgstr "Swift Object"
|
||||
|
||||
msgid "Sync"
|
||||
msgstr "Sync"
|
||||
|
||||
#, python-format
|
||||
msgid "This function does not have specified version number: %s"
|
||||
msgstr "This function does not have specified version number: %s"
|
||||
|
||||
msgid "Unable to create function."
|
||||
msgstr "Unable to create function."
|
||||
|
||||
msgid "Unable to create runtime."
|
||||
msgstr "Unable to create runtime."
|
||||
|
||||
#, python-format
|
||||
msgid "Unable to get executions of this function \"%s\"."
|
||||
msgstr "Unable to get executions of this function \"%s\"."
|
||||
|
||||
#, python-format
|
||||
msgid "Unable to get versions of this function \"%s\"."
|
||||
msgstr "Unable to get versions of this function \"%s\"."
|
||||
|
||||
#, python-format
|
||||
msgid "Unable to load the specified function. %s"
|
||||
msgstr "Unable to load the specified function. %s"
|
||||
|
||||
#, python-format
|
||||
msgid "Unable to retrieve details for Function \"%s\"."
|
||||
msgstr "Unable to retrieve details for Function \"%s\"."
|
||||
|
||||
#, python-format
|
||||
msgid "Unable to retrieve details for Runtime \"%s\"."
|
||||
msgstr "Unable to retrieve details for Runtime \"%s\"."
|
||||
|
||||
#, python-format
|
||||
msgid "Unable to retrieve details for execution \"%s\"."
|
||||
msgstr "Unable to retrieve details for execution \"%s\"."
|
||||
|
||||
#, python-format
|
||||
msgid "Unable to retrieve details for function version \"%s\"."
|
||||
msgstr "Unable to retrieve details for function version \"%s\"."
|
||||
|
||||
msgid "Unable to retrieve execution logs."
|
||||
msgstr "Unable to retrieve execution logs."
|
||||
|
||||
msgid "Unable to retrieve executions list."
|
||||
msgstr "Unable to retrieve executions list."
|
||||
|
||||
msgid "Unable to retrieve function details."
|
||||
msgstr "Unable to retrieve function details."
|
||||
|
||||
msgid "Unable to retrieve functions list."
|
||||
msgstr "Unable to retrieve functions list."
|
||||
|
||||
msgid "Unable to retrieve runtimes list."
|
||||
msgstr "Unable to retrieve runtimes list."
|
||||
|
||||
msgid "Unable to retrieve runtimes."
|
||||
msgstr "Unable to retrieve runtimes."
|
||||
|
||||
#, python-format
|
||||
msgid "Unable to update function %s"
|
||||
msgstr "Unable to update function %s"
|
||||
|
||||
msgid "Unknown"
|
||||
msgstr "Unknown"
|
||||
|
||||
msgid "Update Function"
|
||||
msgstr "Update Function"
|
||||
|
||||
msgid "Update function."
|
||||
msgstr "Update function."
|
||||
|
||||
msgid "Updated At"
|
||||
msgstr "Updated At"
|
||||
|
||||
#, python-format
|
||||
msgid "Updated function \"%s\""
|
||||
msgstr "Updated function \"%s\""
|
||||
|
||||
msgctxt "current status of runtime"
|
||||
msgid "Upgrading"
|
||||
msgstr "Upgrading"
|
||||
|
||||
msgid "Version"
|
||||
msgstr "Version"
|
||||
|
||||
msgid "Version Number"
|
||||
msgstr "Version Number"
|
||||
|
||||
msgid "Version detail"
|
||||
msgstr "Version detail"
|
||||
|
||||
msgid "Versions"
|
||||
msgstr "Versions"
|
||||
|
||||
msgid "Versions of this function"
|
||||
msgstr "Versions of this function"
|
||||
|
||||
msgid "You must specify Docker image."
|
||||
msgstr "You must specify Docker image."
|
||||
|
||||
msgid "You must specify container and object both in case code type is Swift."
|
||||
msgstr "You must specify container and object both in case code type is Swift."
|
||||
|
||||
msgid "You must specify package file."
|
||||
msgstr "You must specify package file."
|
||||
|
||||
msgid "You must specify runtime."
|
||||
msgstr "You must specify runtime."
|
@ -1,466 +0,0 @@
|
||||
# ByungYeol Woo <wby1089@gmail.com>, 2018. #zanata
|
||||
# SEEUNG KANG <seeung0305@naver.com>, 2018. #zanata
|
||||
# Hongjae Kim <neo415ha@gmail.com>, 2019. #zanata
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: qinling-dashboard VERSION\n"
|
||||
"Report-Msgid-Bugs-To: https://bugs.launchpad.net/openstack-i18n/\n"
|
||||
"POT-Creation-Date: 2020-09-11 21:48+0000\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"PO-Revision-Date: 2019-11-02 09:02+0000\n"
|
||||
"Last-Translator: Hongjae Kim <neo415ha@gmail.com>\n"
|
||||
"Language-Team: Korean (South Korea)\n"
|
||||
"Language: ko_KR\n"
|
||||
"X-Generator: Zanata 4.3.3\n"
|
||||
"Plural-Forms: nplurals=1; plural=0\n"
|
||||
|
||||
msgid "1st character is not valid."
|
||||
msgstr "첫번째 문자가 올바르지 않습니다."
|
||||
|
||||
msgctxt "current status of runtime"
|
||||
msgid "Available"
|
||||
msgstr "사용 가능"
|
||||
|
||||
msgid "CPU"
|
||||
msgstr "CPU"
|
||||
|
||||
msgid "CPU (Milli CPU)"
|
||||
msgstr "CPU (Milli CPU)"
|
||||
|
||||
msgid "CPU (Milli CPU)"
|
||||
msgstr "CPU (Milli CPU)"
|
||||
|
||||
msgctxt "current status of runtime"
|
||||
msgid "Cancelled"
|
||||
msgstr "취소됨"
|
||||
|
||||
msgid "Check this item if you would like sync execution."
|
||||
msgstr "싱크 실행을 하려면 이 항목을 확인하세요."
|
||||
|
||||
msgid "Check this item if you would like to create untrusted runtime"
|
||||
msgstr "신뢰할 수 없는 런타임을 생성하려면 이 항목을 선택하십시오."
|
||||
|
||||
msgid "Code"
|
||||
msgstr "코드"
|
||||
|
||||
msgid "Code Type"
|
||||
msgstr "코드 타입"
|
||||
|
||||
msgid "Code Type can not be changed when you update function."
|
||||
msgstr "기능을 업데이트하면 코드 타입을 바꿀 수 없습니다."
|
||||
|
||||
msgid "Code package zip file path."
|
||||
msgstr "코드 패키지 압축 파일 경로"
|
||||
|
||||
msgid "Container name in Swift."
|
||||
msgstr "Swift에서의 컨테이너 이름"
|
||||
|
||||
msgid "Count"
|
||||
msgstr "개수"
|
||||
|
||||
msgid "Create Execution"
|
||||
msgstr "실행 생성"
|
||||
|
||||
msgid "Create Function"
|
||||
msgstr "기능 생성"
|
||||
|
||||
msgid "Create Function Version"
|
||||
msgstr "기능 버전 생성"
|
||||
|
||||
msgid "Create Runtime"
|
||||
msgstr "런타임 만들기"
|
||||
|
||||
msgid "Create Version"
|
||||
msgstr "버전 생성"
|
||||
|
||||
msgid "Create a new execution of function."
|
||||
msgstr "새로운 기능의 실행 생성"
|
||||
|
||||
msgid "Create a new function."
|
||||
msgstr "새로운 기능 생성"
|
||||
|
||||
msgid "Create a new runtime."
|
||||
msgstr "새 런타임을 생성하십시오."
|
||||
|
||||
msgid "Create a new version of function."
|
||||
msgstr "새로운 버전의 기능 생성"
|
||||
|
||||
msgid "Create as Untrusted Image"
|
||||
msgstr "신뢰할 수 없는 이미지로 만들기"
|
||||
|
||||
msgid "Created At"
|
||||
msgstr "생성 시점"
|
||||
|
||||
#, python-format
|
||||
msgid "Created execution of \"%s\""
|
||||
msgstr "\"%s\"을 실행 생성"
|
||||
|
||||
#, python-format
|
||||
msgid "Created function \"%s\""
|
||||
msgstr "기능 생성 \"%s\""
|
||||
|
||||
#, python-format
|
||||
msgid "Created new version of \"%s\""
|
||||
msgstr "새로운 버전\"%s\" 생성"
|
||||
|
||||
#, python-format
|
||||
msgid "Created runtime \"%s\""
|
||||
msgstr "%s 런타임 생성"
|
||||
|
||||
msgctxt "current status of runtime"
|
||||
msgid "Creating"
|
||||
msgstr "생성 중"
|
||||
|
||||
msgid "Delete Execution"
|
||||
msgid_plural "Delete Executions"
|
||||
msgstr[0] ""
|
||||
"실행 삭제\n"
|
||||
"실행 삭제"
|
||||
|
||||
msgid "Delete Function"
|
||||
msgid_plural "Delete Functions"
|
||||
msgstr[0] ""
|
||||
"기능 삭제\n"
|
||||
"기능들 삭제"
|
||||
|
||||
msgid "Delete Function Version"
|
||||
msgid_plural "Delete Function Versions"
|
||||
msgstr[0] ""
|
||||
"삭제된 기능 버전\n"
|
||||
"삭제된 기능 버전들"
|
||||
|
||||
msgid "Deleted executions are not recoverable."
|
||||
msgstr "삭제된 실행은 되살릴 수 없습니다."
|
||||
|
||||
msgid "Deleted function versions are not recoverable."
|
||||
msgstr "삭제된 기능 버전은 되살릴 수 없습니다."
|
||||
|
||||
msgid "Deleted functions are not recoverable."
|
||||
msgstr "삭제된 기능은 되살릴 수 없습니다."
|
||||
|
||||
msgctxt "current status of runtime"
|
||||
msgid "Deleting"
|
||||
msgstr "삭제 중"
|
||||
|
||||
msgid "Description"
|
||||
msgstr "설명"
|
||||
|
||||
msgid "Description:"
|
||||
msgstr "설명:"
|
||||
|
||||
msgctxt "current status of runtime"
|
||||
msgid "Done"
|
||||
msgstr "완료"
|
||||
|
||||
msgid "Download Function"
|
||||
msgstr "기능 다운로드"
|
||||
|
||||
msgid "Duration"
|
||||
msgstr "기간"
|
||||
|
||||
msgid "Entry"
|
||||
msgstr "진입점"
|
||||
|
||||
msgctxt "current status of runtime"
|
||||
msgid "Error"
|
||||
msgstr "오류"
|
||||
|
||||
#, python-format
|
||||
msgid "Error because file not found function: %s"
|
||||
msgstr "파일이 발견되지 않은 오류:%s"
|
||||
|
||||
#, python-format
|
||||
msgid "Error downloading function: %s"
|
||||
msgstr "기능 다운로드 오류:%s"
|
||||
|
||||
msgid "Execution Details"
|
||||
msgstr "운영 정보"
|
||||
|
||||
msgid "Execution Details: {{ resource_name }}"
|
||||
msgstr "실행 정보: {{ resource_name }}"
|
||||
|
||||
msgid "Execution Logs"
|
||||
msgstr "운영 로그"
|
||||
|
||||
msgid "Executions"
|
||||
msgstr "실행"
|
||||
|
||||
msgid "Executions of this function"
|
||||
msgstr "이 기능을 실행"
|
||||
|
||||
msgctxt "current status of runtime"
|
||||
msgid "Failed"
|
||||
msgstr "실패함"
|
||||
|
||||
msgid "Function"
|
||||
msgstr "기능"
|
||||
|
||||
msgid "Function Details"
|
||||
msgstr "기능 세부사항"
|
||||
|
||||
msgid "Function Details: {{ resource_name }}"
|
||||
msgstr "기능 세부사항:{{ resource_name }}"
|
||||
|
||||
msgid "Function Engine"
|
||||
msgstr "기능 엔진"
|
||||
|
||||
msgid "Function ID"
|
||||
msgstr "기능 ID"
|
||||
|
||||
msgid "Function Version"
|
||||
msgstr "기능 버전"
|
||||
|
||||
msgid "Function Version Details"
|
||||
msgstr "기능 버전 세부사항"
|
||||
|
||||
msgid ""
|
||||
"Function Version Details: {{ version.function_id }} (Version "
|
||||
"Number={{ version_number }})"
|
||||
msgstr ""
|
||||
"기능 버전 세부사항: {{ version.function_id }} (Version "
|
||||
"Number={{ version_number }})"
|
||||
|
||||
msgid "Function entry in the format of <module_name>.<method_name>"
|
||||
msgstr "기능 진입점 형태는 <module_name>.<method_name> "
|
||||
|
||||
msgid "Function to execute."
|
||||
msgstr "실행할 기능"
|
||||
|
||||
msgid "Function which new version will be created."
|
||||
msgstr "새로운 버전이 생성될 기능"
|
||||
|
||||
msgid "Functions"
|
||||
msgstr "기능"
|
||||
|
||||
msgid "ID"
|
||||
msgstr "ID"
|
||||
|
||||
msgid "Id"
|
||||
msgstr "ID"
|
||||
|
||||
msgid "Image"
|
||||
msgstr "이미지"
|
||||
|
||||
msgid "Image name in Docker hub."
|
||||
msgstr "도커 허브에서의 이미지 이름"
|
||||
|
||||
msgid "Input"
|
||||
msgstr "입력"
|
||||
|
||||
msgid "Invalid character is used or exceeding maximum length."
|
||||
msgstr "올바르지 않은 문자가 사용되거나 길이가 최대값보다 큽니다."
|
||||
|
||||
msgid "Is Public"
|
||||
msgstr "공개 여부"
|
||||
|
||||
msgid "Last character is not valid."
|
||||
msgstr "마지막 문자가 올바르지 않습니다."
|
||||
|
||||
msgid "Limit of cpu resource(unit: millicpu). Range: {0} ~ {1}"
|
||||
msgstr "CPU 자원 제한(단위: millicpu). 범위: {0} ~ {1}"
|
||||
|
||||
msgid "Limit of memory resource(unit: bytes). Range: {0} ~ {1} (bytes)."
|
||||
msgstr "메모리 자원 제한(단위: bytes). 범위: {0} ~ {1} (bytes)."
|
||||
|
||||
msgid "Md5sum"
|
||||
msgstr "Md5sum"
|
||||
|
||||
msgid "Memory Size"
|
||||
msgstr "메모리 크기"
|
||||
|
||||
msgid "Memory Size (Bytes)"
|
||||
msgstr "메모리 크기(Bytes)"
|
||||
|
||||
msgid "Name"
|
||||
msgstr "이름"
|
||||
|
||||
msgid "No functions available."
|
||||
msgstr "사용 가능한 기능이 없습니다."
|
||||
|
||||
msgid "No runtimes found"
|
||||
msgstr "런타임을 찾지 못했습니다."
|
||||
|
||||
msgid "Not key-value pair."
|
||||
msgstr "키값이 맞지 않습니다."
|
||||
|
||||
msgid "Object name in Swift."
|
||||
msgstr "Swift에서의 오브젝트 이름"
|
||||
|
||||
msgid "Output"
|
||||
msgstr "출력"
|
||||
|
||||
msgid "Overview"
|
||||
msgstr "개요"
|
||||
|
||||
msgid "Package"
|
||||
msgstr "패키지"
|
||||
|
||||
msgctxt "current status of runtime"
|
||||
msgid "Paused"
|
||||
msgstr "정지함"
|
||||
|
||||
msgid "Project ID"
|
||||
msgstr "프로젝트 ID"
|
||||
|
||||
msgid "Result"
|
||||
msgstr "결과"
|
||||
|
||||
msgctxt "current status of runtime"
|
||||
msgid "Running"
|
||||
msgstr "실행중"
|
||||
|
||||
msgid "Runtime"
|
||||
msgstr "런타임"
|
||||
|
||||
msgid "Runtime Details"
|
||||
msgstr "런타임 세부사항"
|
||||
|
||||
msgid "Runtime Details: {{ resource_name }}"
|
||||
msgstr "런타임 세부사항: {{ resource_name }}"
|
||||
|
||||
msgid "Runtime ID"
|
||||
msgstr "런타임 ID"
|
||||
|
||||
msgid "Runtimes"
|
||||
msgstr "런타임"
|
||||
|
||||
msgid "Select Your Code Type."
|
||||
msgstr "코드 타입을 선택하세요."
|
||||
|
||||
msgid "Show Execution Logs"
|
||||
msgstr "운영 로그를 나타냄"
|
||||
|
||||
msgid "Source"
|
||||
msgstr "소스"
|
||||
|
||||
msgid ""
|
||||
"Specify input parmeters like name1=value2. One line is equivalent of one "
|
||||
"input parameter."
|
||||
msgstr ""
|
||||
"name1=value2처럼 입력 매개변수를 지정하시오. 한 줄이 하나의 매개변수와 같습"
|
||||
"니다."
|
||||
|
||||
msgid "Status"
|
||||
msgstr "상태"
|
||||
|
||||
msgctxt "current status of runtime"
|
||||
msgid "Success"
|
||||
msgstr "완료"
|
||||
|
||||
msgid "Swift Container"
|
||||
msgstr "Swift 컨테이너"
|
||||
|
||||
msgid "Swift Object"
|
||||
msgstr "Swift 오브젝트"
|
||||
|
||||
msgid "Sync"
|
||||
msgstr "싱크"
|
||||
|
||||
#, python-format
|
||||
msgid "This function does not have specified version number: %s"
|
||||
msgstr "이 기능은 지정된 버전 숫자가 없습니다:%s"
|
||||
|
||||
msgid "Unable to create function."
|
||||
msgstr "기능을 생성하는데 실패하였습니다."
|
||||
|
||||
msgid "Unable to create runtime."
|
||||
msgstr "런타임을 생성할 수 없음."
|
||||
|
||||
#, python-format
|
||||
msgid "Unable to get executions of this function \"%s\"."
|
||||
msgstr "기능 \"%s\"의 실행을 가져오지 못했습니다."
|
||||
|
||||
#, python-format
|
||||
msgid "Unable to get versions of this function \"%s\"."
|
||||
msgstr "기능 \"%s\"의 버전을 가져오지 못했습니다."
|
||||
|
||||
#, python-format
|
||||
msgid "Unable to load the specified function. %s"
|
||||
msgstr "지정된 기능을 불러올 수 없습니다.%s"
|
||||
|
||||
#, python-format
|
||||
msgid "Unable to retrieve details for Function \"%s\"."
|
||||
msgstr "기능 \"%s\"의 세부사항을 가져오지 못했습니다."
|
||||
|
||||
#, python-format
|
||||
msgid "Unable to retrieve details for Runtime \"%s\"."
|
||||
msgstr "런타임 \"%s\"에 대한 세부사항을 가져오지 못했습니다."
|
||||
|
||||
#, python-format
|
||||
msgid "Unable to retrieve details for execution \"%s\"."
|
||||
msgstr "\"%s\" 실행에 대한 정보를 찾을 수 없습니다."
|
||||
|
||||
#, python-format
|
||||
msgid "Unable to retrieve details for function version \"%s\"."
|
||||
msgstr "기능 버전\"%s\"의 세부사항을 가져오지 못했습니다."
|
||||
|
||||
msgid "Unable to retrieve execution logs."
|
||||
msgstr "실행 로그를 찾아올 수 없습니다."
|
||||
|
||||
msgid "Unable to retrieve executions list."
|
||||
msgstr "실행 목록을 찾을 수 없습니다."
|
||||
|
||||
msgid "Unable to retrieve function details."
|
||||
msgstr "기능 세부사항을 가져오지 못했습니다."
|
||||
|
||||
msgid "Unable to retrieve functions list."
|
||||
msgstr "기능 리스트를 가져오지 못했습니다."
|
||||
|
||||
msgid "Unable to retrieve runtimes list."
|
||||
msgstr "런타임 리스트를 가져오지 못했습니다."
|
||||
|
||||
msgid "Unable to retrieve runtimes."
|
||||
msgstr "런타임을 가져오지 못했습니다."
|
||||
|
||||
#, python-format
|
||||
msgid "Unable to update function %s"
|
||||
msgstr "기능 갱신 실패 \"%s\""
|
||||
|
||||
msgid "Unknown"
|
||||
msgstr "알 수 없음"
|
||||
|
||||
msgid "Update Function"
|
||||
msgstr "기능 갱신"
|
||||
|
||||
msgid "Update function."
|
||||
msgstr "기능 갱신"
|
||||
|
||||
msgid "Updated At"
|
||||
msgstr "갱신 시점"
|
||||
|
||||
#, python-format
|
||||
msgid "Updated function \"%s\""
|
||||
msgstr "갱신된 기능 \"%s\""
|
||||
|
||||
msgctxt "current status of runtime"
|
||||
msgid "Upgrading"
|
||||
msgstr "업그레이드하는 중"
|
||||
|
||||
msgid "Version"
|
||||
msgstr "버전"
|
||||
|
||||
msgid "Version Number"
|
||||
msgstr "버전 번호"
|
||||
|
||||
msgid "Version detail"
|
||||
msgstr "버전 세부사항"
|
||||
|
||||
msgid "Versions"
|
||||
msgstr "버전"
|
||||
|
||||
msgid "Versions of this function"
|
||||
msgstr "이 기능의 버전"
|
||||
|
||||
msgid "You must specify Docker image."
|
||||
msgstr "Docker 이미지를 지정해야 합니다."
|
||||
|
||||
msgid "You must specify container and object both in case code type is Swift."
|
||||
msgstr "코드 타입이 Swift인 경우에 컨테이너와 오브젝트를 지정해야 합니다."
|
||||
|
||||
msgid "You must specify package file."
|
||||
msgstr "패키지 파일을 지정해야 합니다."
|
||||
|
||||
msgid "You must specify runtime."
|
||||
msgstr "런타임을 지정해야 합니다."
|
@ -1,412 +0,0 @@
|
||||
# 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 unittest import mock
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.messages.storage import default_storage
|
||||
from django.test.client import RequestFactory
|
||||
from django.utils import http
|
||||
from horizon.test import helpers as horizon_helpers
|
||||
from openstack_auth import user
|
||||
from openstack_auth import utils
|
||||
from openstack_dashboard import context_processors
|
||||
from requests.packages.urllib3.connection import HTTPConnection
|
||||
|
||||
from qinling_dashboard.test.test_data import utils as test_utils
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
# 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_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.
|
||||
* 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 = str(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 str(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(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(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(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 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
|
@ -1,39 +0,0 @@
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
# 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) # noqa: F405
|
||||
HORIZON_CONFIG.pop('default_dashboard', None) # noqa: F405
|
||||
|
||||
# Update the dashboards with heat_dashboard enabled files
|
||||
# and current INSTALLED_APPS
|
||||
settings.update_dashboards(
|
||||
[
|
||||
openstack_dashboard.enabled,
|
||||
qinling_dashboard.enabled,
|
||||
],
|
||||
HORIZON_CONFIG, # noqa: F405
|
||||
INSTALLED_APPS # noqa: F405
|
||||
)
|
||||
|
||||
# Remove duplicated apps
|
||||
INSTALLED_APPS = list(set(INSTALLED_APPS)) # noqa: F405
|
@ -1,51 +0,0 @@
|
||||
# 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
|
||||
|
||||
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)
|
||||
|
||||
cls.__init__ = fake_init_exception
|
||||
cls.__str__ = fake_str
|
||||
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)
|
@ -1,339 +0,0 @@
|
||||
# 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)
|
@ -1,104 +0,0 @@
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from 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)
|
@ -1,122 +0,0 @@
|
||||
# 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)
|
@ -1,272 +0,0 @@
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from unittest import mock
|
||||
|
||||
from 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.assertCountEqual(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.assertCountEqual(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.assertCountEqual(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.assertCountEqual(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")
|
@ -1,329 +0,0 @@
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import json
|
||||
from unittest 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.assertCountEqual(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),
|
||||
])
|
@ -1,959 +0,0 @@
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import copy
|
||||
from unittest import mock
|
||||
|
||||
from django.core.files import uploadedfile
|
||||
|
||||
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
|
||||
from qinling_dashboard.utils import calculate_md5
|
||||
|
||||
|
||||
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 = 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.assertCountEqual(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),
|
||||
])
|
@ -1,157 +0,0 @@
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from unittest import mock
|
||||
|
||||
from 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:runtimes:index')
|
||||
|
||||
|
||||
class RuntimesTests(test.TestCase):
|
||||
|
||||
@test.create_mocks({
|
||||
api.qinling: [
|
||||
'runtime_create',
|
||||
]})
|
||||
def test_execution_create_with_maximum_params(self):
|
||||
data_runtime = self.runtimes.first()
|
||||
|
||||
self.mock_runtime_create.return_value = data_runtime
|
||||
|
||||
image_name = 'dummy/dockerimage'
|
||||
form_data = {'image': image_name,
|
||||
'name': 'test_name',
|
||||
'description': 'description',
|
||||
'untrusted': 'on'}
|
||||
|
||||
url = reverse('horizon:project:runtimes:create')
|
||||
res = self.client.post(url, form_data)
|
||||
|
||||
self.assertNoFormErrors(res)
|
||||
self.assertRedirectsNoFollow(res, INDEX_URL)
|
||||
|
||||
self.mock_runtime_create.assert_called_once_with(
|
||||
test.IsHttpRequest(),
|
||||
image=image_name,
|
||||
name='test_name',
|
||||
description='description',
|
||||
trusted=False)
|
||||
|
||||
@test.create_mocks({
|
||||
api.qinling: [
|
||||
'runtime_create',
|
||||
]})
|
||||
def test_execution_create_with_minimum_params(self):
|
||||
data_runtime = self.runtimes.first()
|
||||
|
||||
self.mock_runtime_create.return_value = data_runtime
|
||||
|
||||
image_name = 'dummy/dockerimage'
|
||||
form_data = {'image': image_name}
|
||||
|
||||
url = reverse('horizon:project:runtimes:create')
|
||||
res = self.client.post(url, form_data)
|
||||
|
||||
self.assertNoFormErrors(res)
|
||||
self.assertRedirectsNoFollow(res, INDEX_URL)
|
||||
|
||||
self.mock_runtime_create.assert_called_once_with(
|
||||
test.IsHttpRequest(), image=image_name)
|
||||
|
||||
@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.assertCountEqual(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),
|
||||
])
|
@ -1,293 +0,0 @@
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import 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>?@[\\]^_`{|}~abcdefghijklmnopqrstuvwxyz'
|
||||
u'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!"#$%&\'()*+,-./:;<F>?@[\\'
|
||||
u']^_`{|}~abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01'
|
||||
u'23456789!"#$=v', 'raise': True},
|
||||
# upper limit + 1
|
||||
{'d': u' abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ012345678'
|
||||
u'9!"#$%&\'()*+,-./:;<F>?@[\\]^_`{|}~abcdefghijklmnopqrstuvwxyzA'
|
||||
u'BCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!"#$%&\'()*+,-./:;<F>?@[\\]'
|
||||
u'^_`{|}~abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ012'
|
||||
u'3456789!"#$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']^_`{|}~abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01'
|
||||
u'23456789!"#$=v', 'raise': False},
|
||||
# upper limit + 1
|
||||
{'d': u'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
|
||||
u' !"#$%&\'()*+,-./:;<F>?@[\\]^_`{|}~abcdefghijklmnopqrstuvwxyzA'
|
||||
u'BCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!"#$%&\'()*+,-./:;<F>?@[\\]'
|
||||
u'^_`{|}~abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ012'
|
||||
u'3456789!"#$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>?@[\\]^_`{|}~abcdefghijklmnopqrstuvwxyz'
|
||||
u'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!"#$%&\'()*+,-./:;<F>?@[\\'
|
||||
u']^_`{|}~abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01'
|
||||
u'23456789!"#$%', 'raise': False},
|
||||
# upper limit + 1
|
||||
{'d': u'k=abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234567'
|
||||
u'89!"#$%&\'()*+,-./:;<F>?@[\\]^_`{|}~abcdefghijklmnopqrstuvwxyz'
|
||||
u'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!"#$%&\'()*+,-./:;<F>?@[\\'
|
||||
u']^_`{|}~abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01'
|
||||
u'23456789!"#$%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>?@[\\]^_`{|}~abcdefghijklmnopqrstuvwxy'
|
||||
u'zABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!"#$%&\'()*+,-./:;<F>?@['
|
||||
u'\\]^_`{|}~abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
|
||||
u'0123456789!"#$', 'raise': True},
|
||||
# upper limit + 1
|
||||
{'d': u'k= abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456'
|
||||
u'789!"#$%&\'()*+,-./:;<F>?@[\\]^_`{|}~abcdefghijklmnopqrstuvwxy'
|
||||
u'zABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!"#$%&\'()*+,-./:;<F>?@['
|
||||
u'\\]^_`{|}~abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
|
||||
u'0123456789!"#$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))
|
@ -1,79 +0,0 @@
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
# This file is to be included for configuring application which relates
|
||||
# to orchestration(Heat) functions.
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
|
||||
from django.utils.translation import pgettext_lazy
|
||||
|
||||
|
||||
def calculate_md5(target):
|
||||
if not target:
|
||||
return ''
|
||||
|
||||
md5 = hashlib.md5()
|
||||
for chunk in target.chunks():
|
||||
md5.update(chunk)
|
||||
|
||||
return md5.hexdigest()
|
||||
|
||||
|
||||
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")),
|
||||
)
|
@ -1,77 +0,0 @@
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import re
|
||||
|
||||
from django.core.exceptions import ValidationError
|
||||
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
|
||||
STRING_VALIDATE_PATTERN = \
|
||||
r"""[a-zA-Z0-9\$\[\]\(\)\{\}\*\+\?\^\s\|\.\-\\\\!#%&'",/:;=<>@_`~]"""
|
||||
|
||||
MAX_LENGTH = 255
|
||||
|
||||
|
||||
def validate_1st_space(value):
|
||||
"""Raise execption if 1st character is blank(space)"""
|
||||
if re.match(r'^\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(r'\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)
|
@ -1,6 +0,0 @@
|
||||
---
|
||||
upgrade:
|
||||
- |
|
||||
Python 2.7 support has been dropped. Last release of qinling-dashboard
|
||||
to support python 2.7 is OpenStack Train. The minimum version of Python now
|
||||
supported by qinling-dashboard is Python 3.6.
|
@ -1,274 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
# implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
# This file is execfile()d with the current directory set to its
|
||||
# containing dir.
|
||||
#
|
||||
# Note that not all possible configuration values are present in this
|
||||
# autogenerated file.
|
||||
#
|
||||
# All configuration values have a default; values that are commented out
|
||||
# serve to show the default.
|
||||
|
||||
# If extensions (or modules to document with autodoc) are in another directory,
|
||||
# add these directories to sys.path here. If the directory is relative to the
|
||||
# documentation root, use os.path.abspath to make it absolute, like shown here.
|
||||
# sys.path.insert(0, os.path.abspath('.'))
|
||||
|
||||
# -- General configuration ------------------------------------------------
|
||||
|
||||
# If your documentation needs a minimal Sphinx version, state it here.
|
||||
# needs_sphinx = '1.0'
|
||||
|
||||
# Add any Sphinx extension module names here, as strings. They can be
|
||||
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
|
||||
# ones.
|
||||
extensions = [
|
||||
'openstackdocstheme',
|
||||
'reno.sphinxext',
|
||||
]
|
||||
|
||||
# Add any paths that contain templates here, relative to this directory.
|
||||
templates_path = ['_templates']
|
||||
|
||||
# The suffix of source filenames.
|
||||
source_suffix = '.rst'
|
||||
|
||||
# The encoding of source files.
|
||||
# source_encoding = 'utf-8-sig'
|
||||
|
||||
# The master toctree document.
|
||||
master_doc = 'index'
|
||||
|
||||
# General information about the project.
|
||||
project = u'Qinling Dashboard Release Notes'
|
||||
copyright = u'2017, OpenStack Foundation'
|
||||
|
||||
# openstackdocstheme options
|
||||
openstackdocs_repo_name = 'openstack/qinling-dashboard'
|
||||
openstackdocs_auto_name = False
|
||||
openstackdocs_use_storyboard = True
|
||||
|
||||
# Release notes are version independent, no need to set version and release
|
||||
release = ''
|
||||
version = ''
|
||||
|
||||
# The language for content autogenerated by Sphinx. Refer to documentation
|
||||
# for a list of supported languages.
|
||||
# language = None
|
||||
|
||||
# There are two options for replacing |today|: either, you set today to some
|
||||
# non-false value, then it is used:
|
||||
# today = ''
|
||||
# Else, today_fmt is used as the format for a strftime call.
|
||||
# today_fmt = '%B %d, %Y'
|
||||
|
||||
# List of patterns, relative to source directory, that match files and
|
||||
# directories to ignore when looking for source files.
|
||||
exclude_patterns = []
|
||||
|
||||
# The reST default role (used for this markup: `text`) to use for all
|
||||
# documents.
|
||||
# default_role = None
|
||||
|
||||
# If true, '()' will be appended to :func: etc. cross-reference text.
|
||||
# add_function_parentheses = True
|
||||
|
||||
# If true, the current module name will be prepended to all description
|
||||
# unit titles (such as .. function::).
|
||||
# add_module_names = True
|
||||
|
||||
# If true, sectionauthor and moduleauthor directives will be shown in the
|
||||
# output. They are ignored by default.
|
||||
# show_authors = False
|
||||
|
||||
# The name of the Pygments (syntax highlighting) style to use.
|
||||
pygments_style = 'native'
|
||||
|
||||
# A list of ignored prefixes for module index sorting.
|
||||
# modindex_common_prefix = []
|
||||
|
||||
# If true, keep warnings as "system message" paragraphs in the built documents.
|
||||
# keep_warnings = False
|
||||
|
||||
|
||||
# -- Options for HTML output ----------------------------------------------
|
||||
|
||||
# The theme to use for HTML and HTML Help pages. See the documentation for
|
||||
# a list of builtin themes.
|
||||
html_theme = 'openstackdocs'
|
||||
|
||||
# Theme options are theme-specific and customize the look and feel of a theme
|
||||
# further. For a list of options available for each theme, see the
|
||||
# documentation.
|
||||
# html_theme_options = {}
|
||||
|
||||
# Add any paths that contain custom themes here, relative to this directory.
|
||||
# html_theme_path = []
|
||||
|
||||
# The name for this set of Sphinx documents. If None, it defaults to
|
||||
# "<project> v<release> documentation".
|
||||
# html_title = None
|
||||
|
||||
# A shorter title for the navigation bar. Default is the same as html_title.
|
||||
# html_short_title = None
|
||||
|
||||
# The name of an image file (relative to this directory) to place at the top
|
||||
# of the sidebar.
|
||||
# html_logo = None
|
||||
|
||||
# The name of an image file (within the static path) to use as favicon of the
|
||||
# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
|
||||
# pixels large.
|
||||
# html_favicon = None
|
||||
|
||||
# Add any paths that contain custom static files (such as style sheets) here,
|
||||
# relative to this directory. They are copied after the builtin static files,
|
||||
# so a file named "default.css" will overwrite the builtin "default.css".
|
||||
html_static_path = ['_static']
|
||||
|
||||
# Add any extra paths that contain custom files (such as robots.txt or
|
||||
# .htaccess) here, relative to this directory. These files are copied
|
||||
# directly to the root of the documentation.
|
||||
# html_extra_path = []
|
||||
|
||||
# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
|
||||
# using the given strftime format.
|
||||
# html_last_updated_fmt = '%b %d, %Y'
|
||||
|
||||
# If true, SmartyPants will be used to convert quotes and dashes to
|
||||
# typographically correct entities.
|
||||
# html_use_smartypants = True
|
||||
|
||||
# Custom sidebar templates, maps document names to template names.
|
||||
# html_sidebars = {}
|
||||
|
||||
# Additional templates that should be rendered to pages, maps page names to
|
||||
# template names.
|
||||
# html_additional_pages = {}
|
||||
|
||||
# If false, no module index is generated.
|
||||
# html_domain_indices = True
|
||||
|
||||
# If false, no index is generated.
|
||||
# html_use_index = True
|
||||
|
||||
# If true, the index is split into individual pages for each letter.
|
||||
# html_split_index = False
|
||||
|
||||
# If true, links to the reST sources are added to the pages.
|
||||
# html_show_sourcelink = True
|
||||
|
||||
# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
|
||||
# html_show_sphinx = True
|
||||
|
||||
# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
|
||||
# html_show_copyright = True
|
||||
|
||||
# If true, an OpenSearch description file will be output, and all pages will
|
||||
# contain a <link> tag referring to it. The value of this option must be the
|
||||
# base URL from which the finished HTML is served.
|
||||
# html_use_opensearch = ''
|
||||
|
||||
# This is the file name suffix for HTML files (e.g. ".xhtml").
|
||||
# html_file_suffix = None
|
||||
|
||||
# Output file base name for HTML help builder.
|
||||
htmlhelp_basename = 'QinlingDashboardReleaseNotesdoc'
|
||||
|
||||
|
||||
# -- Options for LaTeX output ---------------------------------------------
|
||||
|
||||
latex_elements = {
|
||||
# The paper size ('letterpaper' or 'a4paper').
|
||||
# 'papersize': 'letterpaper',
|
||||
|
||||
# The font size ('10pt', '11pt' or '12pt').
|
||||
# 'pointsize': '10pt',
|
||||
|
||||
# Additional stuff for the LaTeX preamble.
|
||||
# 'preamble': '',
|
||||
}
|
||||
|
||||
# Grouping the document tree into LaTeX files. List of tuples
|
||||
# (source start file, target name, title,
|
||||
# author, documentclass [howto, manual, or own class]).
|
||||
latex_documents = [
|
||||
('index', 'QinlingDashboardReleaseNotes.tex',
|
||||
u'Qinling Dashboard Release Notes Documentation',
|
||||
u'OpenStack Foundation', 'manual'),
|
||||
]
|
||||
|
||||
# The name of an image file (relative to this directory) to place at the top of
|
||||
# the title page.
|
||||
# latex_logo = None
|
||||
|
||||
# For "manual" documents, if this is true, then toplevel headings are parts,
|
||||
# not chapters.
|
||||
# latex_use_parts = False
|
||||
|
||||
# If true, show page references after internal links.
|
||||
# latex_show_pagerefs = False
|
||||
|
||||
# If true, show URL addresses after external links.
|
||||
# latex_show_urls = False
|
||||
|
||||
# Documents to append as an appendix to all manuals.
|
||||
# latex_appendices = []
|
||||
|
||||
# If false, no module index is generated.
|
||||
# latex_domain_indices = True
|
||||
|
||||
|
||||
# -- Options for manual page output ---------------------------------------
|
||||
|
||||
# One entry per manual page. List of tuples
|
||||
# (source start file, name, description, authors, manual section).
|
||||
man_pages = [
|
||||
('index', 'QinlingDashboardReleaseNotes',
|
||||
u'Qinling Dashboard Release Notes Documentation',
|
||||
[u'OpenStack Foundation'], 1)
|
||||
]
|
||||
|
||||
# If true, show URL addresses after external links.
|
||||
# man_show_urls = False
|
||||
|
||||
|
||||
# -- Options for Texinfo output -------------------------------------------
|
||||
|
||||
# Grouping the document tree into Texinfo files. List of tuples
|
||||
# (source start file, target name, title, author,
|
||||
# dir menu entry, description, category)
|
||||
texinfo_documents = [
|
||||
('index', 'QinlingDashboardReleaseNotes',
|
||||
u'Qinling Dashboard Release Notes Documentation',
|
||||
u'OpenStack Foundation', 'QinlingDashboardReleaseNotes',
|
||||
'Dashboard for Qinling',
|
||||
'Miscellaneous'),
|
||||
]
|
||||
|
||||
# Documents to append as an appendix to all manuals.
|
||||
# texinfo_appendices = []
|
||||
|
||||
# If false, no module index is generated.
|
||||
# texinfo_domain_indices = True
|
||||
|
||||
# How to display URL addresses: 'footnote', 'no', or 'inline'.
|
||||
# texinfo_show_urls = 'footnote'
|
||||
|
||||
# If true, do not generate a @detailmenu in the "Top" node's menu.
|
||||
# texinfo_no_detailmenu = False
|
||||
|
||||
# -- Options for Internationalization output ------------------------------
|
||||
locale_dirs = ['locale/']
|
@ -1,9 +0,0 @@
|
||||
===============================
|
||||
Qinling Dashboard Release Notes
|
||||
===============================
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
|
||||
unreleased
|
||||
victoria
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user