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:
Ghanshyam Mann 2020-11-27 21:02:56 -06:00
parent 6611e136f1
commit ce8599cf34
110 changed files with 8 additions and 8156 deletions

108
.gitignore vendored
View File

@ -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/*

View File

@ -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

View File

@ -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

View File

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

201
LICENSE
View File

@ -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.

View File

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

View File

@ -1,16 +1,10 @@
============================= This project is no longer maintained.
Welcome to Qinling Dashboard!
=============================
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 For any further questions, please email
* Documentation: https://docs.openstack.org/qinling-dashboard/latest/ openstack-discuss@lists.openstack.org or join #openstack-dev on
* Source: https://git.openstack.org/cgit/openstack/qinling-dashboard Freenode.
* 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

View File

@ -1,3 +0,0 @@
[python: **.py]
[django: **/templates/**.html]
[django: **/templates/**.csv]

View File

@ -1,2 +0,0 @@
[javascript: **.js]
[angular: **/static/**.html]

View File

@ -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

View File

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

View File

@ -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

View File

@ -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}

View File

@ -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.

View File

@ -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

View File

@ -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.

View File

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

View File

@ -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>

View File

@ -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

View File

@ -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)

View File

@ -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",
]

View File

@ -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)

View File

@ -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"
}

View File

@ -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)

View File

@ -1,22 +0,0 @@
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from django.utils.translation import ugettext_lazy as _
import horizon
class Executions(horizon.Panel):
name = _("Executions")
slug = "executions"
permissions = ('openstack.services.function-engine',)

View File

@ -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,)

View File

@ -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

View File

@ -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 %}

View File

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

View File

@ -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>

View File

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

View File

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

View File

@ -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 %}

View File

@ -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'),
]

View File

@ -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')

View File

@ -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)

View File

@ -1,22 +0,0 @@
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from django.utils.translation import ugettext_lazy as _
import horizon
class Functions(horizon.Panel):
name = _("Functions")
slug = "functions"
permissions = ('openstack.services.function-engine',)

View File

@ -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,)

View File

@ -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

View File

@ -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 %}

View File

@ -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 %}

View File

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

View File

@ -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>

View File

@ -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>

View File

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

View File

@ -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 %}

View File

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

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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 %}

View File

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

View File

@ -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'),
]

View File

@ -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())

View File

@ -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)

View File

@ -1,22 +0,0 @@
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from django.utils.translation import ugettext_lazy as _
import horizon
class Runtimes(horizon.Panel):
name = _("Runtimes")
slug = "runtimes"
permissions = ('openstack.services.function-engine',)

View File

@ -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,)

View File

@ -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

View File

@ -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 %}

View File

@ -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>

View File

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

View File

@ -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 %}

View File

@ -1,22 +0,0 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from 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'),
]

View File

@ -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')

View File

@ -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
}

View File

@ -1,24 +0,0 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
# The slug of the panel to be added to HORIZON_CONFIG. Required.
PANEL = 'runtimes'
# The slug of the dashboard the PANEL associated with. Required.
PANEL_DASHBOARD = 'project'
# The slug of the panel group the PANEL is associated with.
PANEL_GROUP = 'function_engine'
# Python panel class of the PANEL to be added.
ADD_PANEL = 'qinling_dashboard.content.runtimes.panel.Runtimes'
# Automatically discover static resources in installed apps
AUTO_DISCOVER_STATIC_FILES = True

View File

@ -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

View File

@ -1,24 +0,0 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
# The slug of the panel to be added to HORIZON_CONFIG. Required.
PANEL = 'executions'
# The slug of the dashboard the PANEL associated with. Required.
PANEL_DASHBOARD = 'project'
# The slug of the panel group the PANEL is associated with.
PANEL_GROUP = 'function_engine'
# Python panel class of the PANEL to be added.
ADD_PANEL = 'qinling_dashboard.content.executions.panel.Executions'
# Automatically discover static resources in installed apps
AUTO_DISCOVER_STATIC_FILES = True

View File

@ -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,
)

View File

@ -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."

View File

@ -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 "런타임을 지정해야 합니다."

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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")

View File

@ -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),
])

View File

@ -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),
])

View File

@ -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),
])

View File

@ -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))

View File

@ -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")),
)

View File

@ -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)

View File

@ -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.

View File

@ -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/']

View File

@ -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