Retire Packaging Deb project repos
This commit is part of a series to retire the Packaging Deb project. Step 2 is to remove all content from the project repos, replacing it with a README notification where to find ongoing work, and how to recover the repo if needed at some future point (as in https://docs.openstack.org/infra/manual/drivers.html#retiring-a-project). Change-Id: Ifd8e31498ab45cbc5dc86f71a967e521046de6d7
This commit is contained in:
parent
a33ee4ba39
commit
f77dc23e6b
|
@ -1,9 +0,0 @@
|
||||||
[run]
|
|
||||||
source = muranodashboard
|
|
||||||
omit =
|
|
||||||
.tox/*
|
|
||||||
muranodashboard/tests/*
|
|
||||||
muranodashboard/local/*
|
|
||||||
|
|
||||||
[report]
|
|
||||||
ignore_errors = True
|
|
62
.eslintrc
62
.eslintrc
|
@ -1,62 +0,0 @@
|
||||||
# For a detailed list of all options please see here:
|
|
||||||
# http://eslint.org/docs/configuring/
|
|
||||||
|
|
||||||
extends: openstack
|
|
||||||
|
|
||||||
env:
|
|
||||||
# Use jquery global variables
|
|
||||||
jquery: true
|
|
||||||
browser: true
|
|
||||||
|
|
||||||
globals:
|
|
||||||
# allow accessing horizon
|
|
||||||
horizon: false
|
|
||||||
|
|
||||||
# allow passing TENANT_ID from django templates
|
|
||||||
TENANT_ID: false
|
|
||||||
|
|
||||||
# Below we adjust rules specific to horizon's usage of openstack's linting
|
|
||||||
# rules, and its own plugin inclusions.
|
|
||||||
rules:
|
|
||||||
#############################################################################
|
|
||||||
# Disabled Rules from eslint-config-openstack
|
|
||||||
#############################################################################
|
|
||||||
valid-jsdoc: [1, {
|
|
||||||
requireParamDescription: false
|
|
||||||
}]
|
|
||||||
no-undefined: 1
|
|
||||||
brace-style: 1
|
|
||||||
no-extra-parens: 1
|
|
||||||
callback-return: 1
|
|
||||||
block-scoped-var: 1
|
|
||||||
quote-props: 0
|
|
||||||
space-in-parens: 1
|
|
||||||
no-use-before-define: 1
|
|
||||||
no-unneeded-ternary: 1
|
|
||||||
|
|
||||||
# Only support ECMA5, disable everything else.
|
|
||||||
# NOTE(kzaitsev): blatantly copied from horizon
|
|
||||||
ecmaFeatures:
|
|
||||||
arrowFunctions: false
|
|
||||||
binaryLiterals: false
|
|
||||||
blockBindings: false
|
|
||||||
classes: false
|
|
||||||
defaultParams: false
|
|
||||||
destructuring: false
|
|
||||||
forOf: false
|
|
||||||
generators: false
|
|
||||||
modules: false
|
|
||||||
objectLiteralComputedProperties: false
|
|
||||||
objectLiteralDuplicateProperties: false
|
|
||||||
objectLiteralShorthandMethods: false
|
|
||||||
objectLiteralShorthandProperties: false
|
|
||||||
octalLiterals: false
|
|
||||||
regexUFlag: false
|
|
||||||
regexYFlag: false
|
|
||||||
restParams: false
|
|
||||||
spread: false
|
|
||||||
superInFunctions: false
|
|
||||||
templateStrings: false
|
|
||||||
unicodeCodePointEscapes: false
|
|
||||||
globalReturn: false
|
|
||||||
jsx: false
|
|
|
@ -1,42 +0,0 @@
|
||||||
eggs/
|
|
||||||
.eggs/
|
|
||||||
develop-eggs/
|
|
||||||
*.egg-info/
|
|
||||||
*.egg
|
|
||||||
*~
|
|
||||||
*.orig
|
|
||||||
*.pyc
|
|
||||||
*.swp
|
|
||||||
.environment_version
|
|
||||||
.selenium_log
|
|
||||||
.coverage*
|
|
||||||
.noseids
|
|
||||||
.venv
|
|
||||||
.idea
|
|
||||||
.tox
|
|
||||||
coverage.xml
|
|
||||||
pep8.txt
|
|
||||||
pylint.txt
|
|
||||||
reports
|
|
||||||
muranodashboard/local/local_settings.py
|
|
||||||
muranodashboard/settings.py
|
|
||||||
/static/
|
|
||||||
doc/build/
|
|
||||||
doc/source/sourcecode
|
|
||||||
build
|
|
||||||
dist
|
|
||||||
cover
|
|
||||||
|
|
||||||
# Ignore i18n compiled files
|
|
||||||
*.mo
|
|
||||||
|
|
||||||
# Autogenerated Documentation
|
|
||||||
doc/source/api
|
|
||||||
|
|
||||||
# Tests
|
|
||||||
muranodashboard/tests/functional/config/config.conf
|
|
||||||
node_modules
|
|
||||||
npm-debug.log
|
|
||||||
|
|
||||||
# RElease NOtes
|
|
||||||
releasenotes/build
|
|
|
@ -1,4 +0,0 @@
|
||||||
[gerrit]
|
|
||||||
host=review.openstack.org
|
|
||||||
port=29418
|
|
||||||
project=openstack/murano-dashboard.git
|
|
|
@ -1,51 +0,0 @@
|
||||||
======================
|
|
||||||
Contributing to Murano
|
|
||||||
======================
|
|
||||||
|
|
||||||
If you're interested in contributing to the Murano project,
|
|
||||||
the following will help get you started.
|
|
||||||
|
|
||||||
Contributor License Agreement
|
|
||||||
=============================
|
|
||||||
|
|
||||||
In order to contribute to the Murano project, you need to have
|
|
||||||
signed OpenStack's contributor's agreement:
|
|
||||||
|
|
||||||
* http://docs.openstack.org/infra/manual/developers.html
|
|
||||||
* http://wiki.openstack.org/CLA
|
|
||||||
|
|
||||||
|
|
||||||
Project Hosting Details
|
|
||||||
=======================
|
|
||||||
|
|
||||||
* Bug tracker
|
|
||||||
* https://launchpad.net/murano
|
|
||||||
|
|
||||||
* https://launchpad.net/python-muranoclient
|
|
||||||
|
|
||||||
* Mailing list (prefix subjects with ``[Murano]`` for faster responses)
|
|
||||||
http://lists.openstack.org/cgi-bin/mailman/listinfo/openstack-dev
|
|
||||||
|
|
||||||
* Wiki
|
|
||||||
https://wiki.openstack.org/wiki/Murano
|
|
||||||
|
|
||||||
* IRC channel
|
|
||||||
* #murano at FreeNode
|
|
||||||
|
|
||||||
* https://wiki.openstack.org/wiki/Meetings#Murano_meeting
|
|
||||||
|
|
||||||
* Code Hosting
|
|
||||||
* https://git.openstack.org/cgit/openstack/murano
|
|
||||||
|
|
||||||
* https://git.openstack.org/cgit/openstack/murano-agent
|
|
||||||
|
|
||||||
* https://git.openstack.org/cgit/openstack/murano-dashboard
|
|
||||||
|
|
||||||
* https://git.openstack.org/cgit/openstack/python-muranoclient
|
|
||||||
|
|
||||||
* https://git.openstack.org/cgit/openstack/murano-apps
|
|
||||||
|
|
||||||
* Code Review
|
|
||||||
* https://review.openstack.org/#/q/murano-dashboard+AND+status:+open,n,z
|
|
||||||
|
|
||||||
* http://docs.openstack.org/infra/manual/developers.html#development-workflow
|
|
11
HACKING.rst
11
HACKING.rst
|
@ -1,11 +0,0 @@
|
||||||
Murano Dashboard Style Commandments
|
|
||||||
===================================
|
|
||||||
|
|
||||||
*- Step 1: Read the OpenStack Style Commandments
|
|
||||||
http://docs.openstack.org/developer/hacking/
|
|
||||||
|
|
||||||
* Step 2: Read [hacking] section in tox.ini to find the list of names which
|
|
||||||
can be imported directly without triggering the "H302: import only modules"
|
|
||||||
flake8 warning
|
|
||||||
|
|
||||||
* Step 3: Read on
|
|
176
LICENSE
176
LICENSE
|
@ -1,176 +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.
|
|
||||||
|
|
|
@ -0,0 +1,14 @@
|
||||||
|
This project is no longer maintained.
|
||||||
|
|
||||||
|
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".
|
||||||
|
|
||||||
|
For ongoing work on maintaining OpenStack packages in the Debian
|
||||||
|
distribution, please see the Debian OpenStack packaging team at
|
||||||
|
https://wiki.debian.org/OpenStack/.
|
||||||
|
|
||||||
|
For any further questions, please email
|
||||||
|
openstack-dev@lists.openstack.org or join #openstack-dev on
|
||||||
|
Freenode.
|
44
README.rst
44
README.rst
|
@ -1,44 +0,0 @@
|
||||||
========================
|
|
||||||
Team and repository tags
|
|
||||||
========================
|
|
||||||
|
|
||||||
.. image:: https://governance.openstack.org/badges/murano-dashboard.svg
|
|
||||||
:target: https://governance.openstack.org/reference/tags/index.html
|
|
||||||
|
|
||||||
.. Change things from this point on
|
|
||||||
|
|
||||||
Murano
|
|
||||||
======
|
|
||||||
|
|
||||||
Murano Project introduces an application catalog, which allows application
|
|
||||||
developers and cloud administrators to publish various cloud-ready
|
|
||||||
applications in a browsable categorised catalog. Cloud users,
|
|
||||||
including inexperienced ones, can then use the catalog to
|
|
||||||
compose reliable application environments with the push of a button.
|
|
||||||
|
|
||||||
Murano Dashboard
|
|
||||||
----------------
|
|
||||||
Murano Dashboard is an extension for OpenStack Dashboard that provides a UI for
|
|
||||||
Murano. With murano-dashboard, a user is able to easily manage and control
|
|
||||||
an application catalog, running applications and created environments alongside
|
|
||||||
with all other OpenStack resources.
|
|
||||||
|
|
||||||
For developer purposes, please symlink the following OpenStack Dashboard plugin
|
|
||||||
files:
|
|
||||||
* muranodashboard/local/enabled/*.py into
|
|
||||||
horizon/openstack_dashboard/local/enabled/
|
|
||||||
* muranodashboard/local/local_settings.d/_50_murano.py into
|
|
||||||
horizon/openstack_dashboard/local/local_settings.d/_50_murano.py
|
|
||||||
* muranodashboard/conf/murano_policy.json into
|
|
||||||
horizon/openstack_dashboard/conf/
|
|
||||||
|
|
||||||
re-compress static assets and restart Horizon web-server as usual.
|
|
||||||
|
|
||||||
Project Resources
|
|
||||||
-----------------
|
|
||||||
|
|
||||||
* `Murano at Launchpad <https://launchpad.net/murano>`_
|
|
||||||
* `Wiki <https://wiki.openstack.org/wiki/Murano>`_
|
|
||||||
* `Code Review <https://review.openstack.org/>`_
|
|
||||||
* `Sources <https://wiki.openstack.org/wiki/Murano/SourceCode>`_
|
|
||||||
* `Documentation <https://docs.openstack.org/developer/murano/>`_
|
|
|
@ -1,5 +0,0 @@
|
||||||
[extractors]
|
|
||||||
django = django_babel.extract:extract_django
|
|
||||||
|
|
||||||
[python: **.py]
|
|
||||||
[django: **/templates/**.html]
|
|
|
@ -1,14 +0,0 @@
|
||||||
[extractors]
|
|
||||||
# We use a custom extractor to find translatable strings in AngularJS
|
|
||||||
# templates. The extractor is included in horizon.utils for now.
|
|
||||||
# See http://babel.pocoo.org/docs/messages/#referencing-extraction-methods for
|
|
||||||
# details on how this works.
|
|
||||||
angular = horizon.utils.babel_extract_angular:extract_angular
|
|
||||||
|
|
||||||
[javascript: **.js]
|
|
||||||
|
|
||||||
# We need to look into all static folders for HTML files.
|
|
||||||
# The **/static ensures that we also search within
|
|
||||||
# /openstack_dashboard/dashboards/XYZ/static which will ensure
|
|
||||||
# that plugins are also translated.
|
|
||||||
[angular: **/static/**.html]
|
|
|
@ -1,2 +0,0 @@
|
||||||
[theme]
|
|
||||||
inherit = default
|
|
|
@ -1,247 +0,0 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
# Copyright (c) 2010 OpenStack Foundation.
|
|
||||||
#
|
|
||||||
# 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.
|
|
||||||
|
|
||||||
#
|
|
||||||
# Portas documentation build configuration file, created by
|
|
||||||
# sphinx-quickstart on Tue February 28 13:50:15 2013.
|
|
||||||
#
|
|
||||||
# This file is execfile()'d with the current directory set to its containing
|
|
||||||
# dir.
|
|
||||||
#
|
|
||||||
# Note that not all possible configuration values are present in this
|
|
||||||
# autogenerated file.
|
|
||||||
#
|
|
||||||
# All configuration values have a default; values that are commented out
|
|
||||||
# serve to show the default.
|
|
||||||
|
|
||||||
import os
|
|
||||||
import subprocess
|
|
||||||
import sys
|
|
||||||
|
|
||||||
# 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 = [os.path.abspath('../../muranodashboard'),
|
|
||||||
os.path.abspath('../..')] + sys.path
|
|
||||||
|
|
||||||
# -- 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',
|
|
||||||
'sphinx.ext.intersphinx',
|
|
||||||
'sphinx.ext.coverage',
|
|
||||||
'sphinx.ext.ifconfig',
|
|
||||||
'sphinx.ext.graphviz',
|
|
||||||
'openstackdocstheme']
|
|
||||||
|
|
||||||
# Add any paths that contain templates here, relative to this directory.
|
|
||||||
templates_path = []
|
|
||||||
if os.getenv('HUDSON_PUBLISH_DOCS'):
|
|
||||||
templates_path = ['_ga', '_templates']
|
|
||||||
else:
|
|
||||||
templates_path = ['_templates']
|
|
||||||
|
|
||||||
# The suffix of source filenames.
|
|
||||||
source_suffix = '.rst'
|
|
||||||
|
|
||||||
# The encoding of source files.
|
|
||||||
#source_encoding = 'utf-8'
|
|
||||||
|
|
||||||
# The master toctree document.
|
|
||||||
master_doc = 'index'
|
|
||||||
|
|
||||||
# General information about the project.
|
|
||||||
project = u'Dashboard'
|
|
||||||
copyright = u'OpenStack Foundation'
|
|
||||||
|
|
||||||
# The version info for the project you're documenting, acts as replacement for
|
|
||||||
# |version| and |release|, also used in various other places throughout the
|
|
||||||
# built documents.
|
|
||||||
#
|
|
||||||
# The short X.Y version.
|
|
||||||
from muranodashboard.version import version_info as muranodashboard_version
|
|
||||||
# The full version, including alpha/beta/rc tags.
|
|
||||||
release = muranodashboard_version.version_string_with_vcs()
|
|
||||||
# The short X.Y version.
|
|
||||||
version = muranodashboard_version.canonical_version_string()
|
|
||||||
|
|
||||||
# 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 documents that shouldn't be included in the build.
|
|
||||||
#unused_docs = []
|
|
||||||
|
|
||||||
# List of directories, relative to source directory, that shouldn't be searched
|
|
||||||
# for source files.
|
|
||||||
exclude_trees = ['api']
|
|
||||||
|
|
||||||
# The reST default role (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 = True
|
|
||||||
|
|
||||||
# The name of the Pygments (syntax highlighting) style to use.
|
|
||||||
pygments_style = 'sphinx'
|
|
||||||
|
|
||||||
# A list of ignored prefixes for module index sorting.
|
|
||||||
modindex_common_prefix = ['muranodashboard.']
|
|
||||||
|
|
||||||
# -- Options for man page output --------------------------------------------
|
|
||||||
|
|
||||||
# Grouping the document tree for man pages.
|
|
||||||
# List of tuples 'sourcefile', 'target', u'title', u'Authors name', 'manual'
|
|
||||||
|
|
||||||
man_pages = []
|
|
||||||
|
|
||||||
|
|
||||||
# -- 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 = 'openstackdocs'
|
|
||||||
|
|
||||||
# openstackdocstheme options
|
|
||||||
repository_name = 'openstack/murano-dashboard'
|
|
||||||
bug_project = 'murano'
|
|
||||||
bug_tag = ''
|
|
||||||
|
|
||||||
# Must set this variable to include year, month, day, hours, and minutes.
|
|
||||||
html_last_updated_fmt = '%Y-%m-%d %H:%M'
|
|
||||||
|
|
||||||
# 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 = ['_theme']
|
|
||||||
|
|
||||||
# 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']
|
|
||||||
|
|
||||||
# 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'
|
|
||||||
git_cmd = ["git", "log", "--pretty=format:'%ad, commit %h'", "--date=local",
|
|
||||||
"-n1"]
|
|
||||||
html_last_updated_fmt = subprocess.check_output(git_cmd).decode('utf-8')
|
|
||||||
|
|
||||||
# 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_use_modindex = False
|
|
||||||
|
|
||||||
# If false, no index is generated.
|
|
||||||
html_use_index = False
|
|
||||||
|
|
||||||
# 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, 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 = ''
|
|
||||||
|
|
||||||
# If nonempty, this is the file name suffix for HTML files (e.g. ".xhtml").
|
|
||||||
#html_file_suffix = ''
|
|
||||||
|
|
||||||
# Output file base name for HTML help builder.
|
|
||||||
htmlhelp_basename = 'muranodashboarddoc'
|
|
||||||
|
|
||||||
|
|
||||||
# -- Options for LaTeX output ------------------------------------------------
|
|
||||||
|
|
||||||
# The paper size ('letter' or 'a4').
|
|
||||||
#latex_paper_size = 'letter'
|
|
||||||
|
|
||||||
# The font size ('10pt', '11pt' or '12pt').
|
|
||||||
#latex_font_size = '10pt'
|
|
||||||
|
|
||||||
# Grouping the document tree into LaTeX files. List of tuples
|
|
||||||
# (source start file, target name, title, author,
|
|
||||||
# documentclass [howto/manual]).
|
|
||||||
latex_documents = [
|
|
||||||
('index', 'Dashboard.tex', u'Dashboard Documentation',
|
|
||||||
u'Murano Team', '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
|
|
||||||
|
|
||||||
# Additional stuff for the LaTeX preamble.
|
|
||||||
#latex_preamble = ''
|
|
||||||
|
|
||||||
# Documents to append as an appendix to all manuals.
|
|
||||||
#latex_appendices = []
|
|
||||||
|
|
||||||
# If false, no module index is generated.
|
|
||||||
#latex_use_modindex = True
|
|
||||||
|
|
||||||
# Example configuration for intersphinx: refer to the Python standard library.
|
|
||||||
intersphinx_mapping = {'python': ('http://docs.python.org/', None)}
|
|
|
@ -1,65 +0,0 @@
|
||||||
..
|
|
||||||
Copyright 2010 OpenStack Foundation
|
|
||||||
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.
|
|
||||||
|
|
||||||
================================================
|
|
||||||
Welcome to Dashboard, the Murano Project Web UI!
|
|
||||||
================================================
|
|
||||||
|
|
||||||
Dashboard is a project that provides Web UI to Murano Project.
|
|
||||||
|
|
||||||
This document describes Murano Dashboard for contributors of the project, and assumes
|
|
||||||
that you are already familiar with Murano from an `end-user perspective`_.
|
|
||||||
|
|
||||||
.. _`end-user perspective`: http://murano.readthedocs.org/
|
|
||||||
|
|
||||||
This documentation is generated by the Sphinx toolkit and lives in the source
|
|
||||||
tree.
|
|
||||||
|
|
||||||
Installation Guide
|
|
||||||
==================
|
|
||||||
Install
|
|
||||||
-------
|
|
||||||
1. Check out sources to some directory (*<home>/murano-dashboard*)::
|
|
||||||
|
|
||||||
user@work:~/$ git clone git://git.openstack.org/openstack/murano-dashboard
|
|
||||||
|
|
||||||
2. Install virtualenv::
|
|
||||||
|
|
||||||
user@work:~/$ cd murano-dashboard && sudo python ./tools/install_venv.py
|
|
||||||
|
|
||||||
Configure
|
|
||||||
---------
|
|
||||||
1. Copy configuration file from template::
|
|
||||||
|
|
||||||
user@work:~/$ cp murano-dashboard/muranodashboard/local/local_settings.py.example murano-dashboard/muranodashboard/local/local_settings.py
|
|
||||||
|
|
||||||
2. Open configuration file for editing::
|
|
||||||
|
|
||||||
user@work:~/$ cd murano-dashboard/muranodashboard/local/ && nano local_settings.py
|
|
||||||
|
|
||||||
2. Configure according to you environment::
|
|
||||||
|
|
||||||
...
|
|
||||||
SECRET_KEY = 'some_random_value'
|
|
||||||
...
|
|
||||||
OPENSTACK_HOST = "localhost"
|
|
||||||
...
|
|
||||||
|
|
||||||
Run
|
|
||||||
----
|
|
||||||
Run Dashboard in virtualenv::
|
|
||||||
|
|
||||||
user@work:~/$ cd murano-dashboard && sudo ./tools/with_venv.sh python manage.py runserver 0.0.0.0:8080
|
|
140
karma.conf.js
140
karma.conf.js
|
@ -1,140 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2015 IBM Corp.
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the 'License');
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an 'AS IS' BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
var fs = require('fs');
|
|
||||||
var path = require('path');
|
|
||||||
|
|
||||||
module.exports = function (config) {
|
|
||||||
// This tox venv is setup in the post-install npm step
|
|
||||||
var toxPath = '.tox/py27/lib/python2.7/site-packages/';
|
|
||||||
var xstaticPath = toxPath + 'xstatic/pkg/';
|
|
||||||
|
|
||||||
config.set({
|
|
||||||
preprocessors: {
|
|
||||||
// Used to collect templates for preprocessing.
|
|
||||||
// NOTE: the templates must also be listed in the files section below.
|
|
||||||
'./static/**/*.html': ['ng-html2js']
|
|
||||||
},
|
|
||||||
|
|
||||||
// Sets up module to process templates.
|
|
||||||
ngHtml2JsPreprocessor: {
|
|
||||||
prependPrefix: '/',
|
|
||||||
moduleName: 'templates'
|
|
||||||
},
|
|
||||||
|
|
||||||
basePath: './',
|
|
||||||
|
|
||||||
// Contains both source and test files.
|
|
||||||
files: [
|
|
||||||
/*
|
|
||||||
* shim, partly stolen from /i18n/js/horizon/
|
|
||||||
* Contains expected items not provided elsewhere (dynamically by
|
|
||||||
* Django or via jasmine template.
|
|
||||||
*/
|
|
||||||
'./test-shim.js',
|
|
||||||
|
|
||||||
// from jasmine.html
|
|
||||||
xstaticPath + 'jquery/data/jquery.js',
|
|
||||||
xstaticPath + 'angular/data/angular.js',
|
|
||||||
xstaticPath + 'angular/data/angular-route.js',
|
|
||||||
xstaticPath + 'angular/data/angular-mocks.js',
|
|
||||||
xstaticPath + 'angular/data/angular-cookies.js',
|
|
||||||
xstaticPath + 'angular_bootstrap/data/angular-bootstrap.js',
|
|
||||||
xstaticPath + 'angular_gettext/data/angular-gettext.js',
|
|
||||||
xstaticPath + 'angular_fileupload/data/ng-file-upload-all.js',
|
|
||||||
xstaticPath + 'angular/data/angular-sanitize.js',
|
|
||||||
xstaticPath + 'd3/data/d3.js',
|
|
||||||
xstaticPath + 'rickshaw/data/rickshaw.js',
|
|
||||||
xstaticPath + 'angular_smart_table/data/smart-table.js',
|
|
||||||
xstaticPath + 'angular_lrdragndrop/data/lrdragndrop.js',
|
|
||||||
xstaticPath + 'spin/data/spin.js',
|
|
||||||
xstaticPath + 'spin/data/spin.jquery.js',
|
|
||||||
xstaticPath + 'tv4/data/tv4.js',
|
|
||||||
xstaticPath + 'objectpath/data/ObjectPath.js',
|
|
||||||
xstaticPath + 'angular_schema_form/data/schema-form.js',
|
|
||||||
|
|
||||||
// TODO: These should be mocked.
|
|
||||||
toxPath + '/horizon/static/horizon/js/horizon.js',
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Include framework source code from horizon that we need.
|
|
||||||
* Otherwise, karma will not be able to find them when testing.
|
|
||||||
* These files should be mocked in the foreseeable future.
|
|
||||||
*/
|
|
||||||
toxPath + 'horizon/static/framework/**/*.module.js',
|
|
||||||
toxPath + 'horizon/static/framework/**/!(*.spec|*.mock).js',
|
|
||||||
toxPath + 'openstack_dashboard/static/**/*.module.js',
|
|
||||||
toxPath + 'openstack_dashboard/static/**/!(*.spec|*.mock).js',
|
|
||||||
toxPath + 'openstack_dashboard/dashboards/**/static/**/*.module.js',
|
|
||||||
toxPath + 'openstack_dashboard/dashboards/**/static/**/!(*.spec|*.mock).js',
|
|
||||||
|
|
||||||
/**
|
|
||||||
* First, list all the files that defines application's angular modules.
|
|
||||||
* Those files have extension of `.module.js`. The order among them is
|
|
||||||
* not significant.
|
|
||||||
*/
|
|
||||||
'./muranodashboard/static/**/*.module.js',
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Followed by other JavaScript files that defines angular providers
|
|
||||||
* on the modules defined in files listed above. And they are not mock
|
|
||||||
* files or spec files defined below. The order among them is not
|
|
||||||
* significant.
|
|
||||||
*/
|
|
||||||
'./muranodashboard/static/app/**/!(*.spec|*.mock).js',
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Then, list files for mocks with `mock.js` extension. The order
|
|
||||||
* among them should not be significant.
|
|
||||||
*/
|
|
||||||
toxPath + 'openstack_dashboard/static/**/*.mock.js',
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Finally, list files for spec with `spec.js` extension. The order
|
|
||||||
* among them should not be significant.
|
|
||||||
*/
|
|
||||||
'./muranodashboard/static/app/**/*.spec.js',
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Angular external templates
|
|
||||||
*/
|
|
||||||
'./muranodashboard/static/app/**/*.html'
|
|
||||||
],
|
|
||||||
|
|
||||||
autoWatch: true,
|
|
||||||
|
|
||||||
frameworks: ['jasmine'],
|
|
||||||
|
|
||||||
browsers: ['Chrome'],
|
|
||||||
|
|
||||||
phantomjsLauncher: {
|
|
||||||
// Have phantomjs exit if a ResourceError is encountered
|
|
||||||
// (useful if karma exits without killing phantom)
|
|
||||||
exitOnResourceError: true
|
|
||||||
},
|
|
||||||
|
|
||||||
reporters: ['progress'],
|
|
||||||
|
|
||||||
plugins: [
|
|
||||||
'karma-chrome-launcher',
|
|
||||||
'karma-jasmine',
|
|
||||||
'karma-ng-html2js-preprocessor'
|
|
||||||
]
|
|
||||||
|
|
||||||
});
|
|
||||||
};
|
|
23
manage.py
23
manage.py
|
@ -1,23 +0,0 @@
|
||||||
#!/usr/bin/env python
|
|
||||||
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
||||||
# not use this file except in compliance with the License. You may obtain
|
|
||||||
# a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
||||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
||||||
# License for the specific language governing permissions and limitations
|
|
||||||
# under the License.
|
|
||||||
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE",
|
|
||||||
"muranodashboard.tests.settings")
|
|
||||||
from django.core.management import execute_from_command_line # noqa
|
|
||||||
execute_from_command_line(sys.argv)
|
|
|
@ -1,152 +0,0 @@
|
||||||
# Copyright (c) 2014 Mirantis, Inc.
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
||||||
# not use this file except in compliance with the License. You may obtain
|
|
||||||
# a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
||||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
||||||
# License for the specific language governing permissions and limitations
|
|
||||||
# under the License.
|
|
||||||
|
|
||||||
import contextlib
|
|
||||||
|
|
||||||
from django.conf import settings
|
|
||||||
from django.contrib.messages import api as msg_api
|
|
||||||
from django.utils.encoding import force_text
|
|
||||||
from django.utils.translation import ugettext_lazy as _
|
|
||||||
from glanceclient.common import exceptions as glance_exc
|
|
||||||
from horizon import exceptions
|
|
||||||
import muranoclient.client as client
|
|
||||||
from muranoclient.common import exceptions as exc
|
|
||||||
from muranoclient.glance import client as art_client
|
|
||||||
from openstack_dashboard.api import base
|
|
||||||
from oslo_log import log as logging
|
|
||||||
|
|
||||||
|
|
||||||
from muranodashboard.common import utils as muranodashboard_utils
|
|
||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
def _handle_message(request, message):
|
|
||||||
def horizon_message_already_queued(_message):
|
|
||||||
_message = force_text(_message)
|
|
||||||
if request.is_ajax():
|
|
||||||
for tag, msg, extra in request.horizon['async_messages']:
|
|
||||||
if _message == msg:
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
for msg in msg_api.get_messages(request):
|
|
||||||
if msg.message == _message:
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
if horizon_message_already_queued(message):
|
|
||||||
exceptions.handle(request, ignore=True)
|
|
||||||
else:
|
|
||||||
exceptions.handle(request, message=message)
|
|
||||||
|
|
||||||
|
|
||||||
@contextlib.contextmanager
|
|
||||||
def handled_exceptions(request):
|
|
||||||
"""Handles all murano-api specific exceptions."""
|
|
||||||
try:
|
|
||||||
yield
|
|
||||||
except exc.CommunicationError:
|
|
||||||
msg = _('Unable to communicate to murano-api server.')
|
|
||||||
LOG.exception(msg)
|
|
||||||
_handle_message(request, msg)
|
|
||||||
except glance_exc.CommunicationError:
|
|
||||||
msg = _('Unable to communicate to glare-api server.')
|
|
||||||
LOG.exception(msg)
|
|
||||||
_handle_message(request, msg)
|
|
||||||
except exc.HTTPUnauthorized:
|
|
||||||
msg = _('Check Keystone configuration of murano-api server.')
|
|
||||||
LOG.exception(msg)
|
|
||||||
_handle_message(request, msg)
|
|
||||||
except exc.HTTPForbidden:
|
|
||||||
msg = _('Operation is forbidden by murano-api server.')
|
|
||||||
LOG.exception(msg)
|
|
||||||
_handle_message(request, msg)
|
|
||||||
except exc.HTTPNotFound:
|
|
||||||
msg = _('Requested object is not found on murano server.')
|
|
||||||
LOG.exception(msg)
|
|
||||||
_handle_message(request, msg)
|
|
||||||
except exc.HTTPConflict:
|
|
||||||
msg = _('Requested operation conflicts with an existing object.')
|
|
||||||
LOG.exception(msg)
|
|
||||||
_handle_message(request, msg)
|
|
||||||
except exc.BadRequest as e:
|
|
||||||
msg = _('The request data is not acceptable by the server')
|
|
||||||
LOG.exception(msg)
|
|
||||||
reason = muranodashboard_utils.parse_api_error(
|
|
||||||
getattr(e, 'details', ''))
|
|
||||||
if not reason:
|
|
||||||
reason = msg
|
|
||||||
_handle_message(request, reason)
|
|
||||||
except (exc.HTTPInternalServerError,
|
|
||||||
glance_exc.HTTPInternalServerError) as e:
|
|
||||||
msg = _("There was an error communicating with server")
|
|
||||||
LOG.exception(msg)
|
|
||||||
reason = muranodashboard_utils.parse_api_error(
|
|
||||||
getattr(e, 'details', ''))
|
|
||||||
if not reason:
|
|
||||||
reason = msg
|
|
||||||
_handle_message(request, reason)
|
|
||||||
|
|
||||||
|
|
||||||
def _get_endpoint(request):
|
|
||||||
# prefer location specified in settings for dev purposes
|
|
||||||
endpoint = getattr(settings, 'MURANO_API_URL', None)
|
|
||||||
|
|
||||||
if not endpoint:
|
|
||||||
try:
|
|
||||||
endpoint = base.url_for(request, 'application-catalog')
|
|
||||||
except exceptions.ServiceCatalogException:
|
|
||||||
endpoint = 'http://localhost:8082'
|
|
||||||
LOG.warning('Murano API location could not be found in Service '
|
|
||||||
'Catalog, using default: {0}'.format(endpoint))
|
|
||||||
return endpoint
|
|
||||||
|
|
||||||
|
|
||||||
def _get_glare_endpoint(request):
|
|
||||||
endpoint = getattr(settings, 'GLARE_API_URL', None)
|
|
||||||
if not endpoint:
|
|
||||||
try:
|
|
||||||
endpoint = base.url_for(request, "artifact")
|
|
||||||
except exceptions.ServiceCatalogException:
|
|
||||||
endpoint = 'http://localhost:9494'
|
|
||||||
LOG.warning('Glare API location could not be found in Service '
|
|
||||||
'Catalog, using default: {0}'.format(endpoint))
|
|
||||||
return endpoint
|
|
||||||
|
|
||||||
|
|
||||||
def artifactclient(request):
|
|
||||||
endpoint = _get_glare_endpoint(request)
|
|
||||||
insecure = getattr(settings, 'GLARE_API_INSECURE', False)
|
|
||||||
token_id = request.user.token.id
|
|
||||||
return art_client.Client(endpoint=endpoint, token=token_id,
|
|
||||||
insecure=insecure, type_name='murano',
|
|
||||||
type_version=1)
|
|
||||||
|
|
||||||
|
|
||||||
def muranoclient(request):
|
|
||||||
endpoint = _get_endpoint(request)
|
|
||||||
insecure = getattr(settings, 'MURANO_API_INSECURE', False)
|
|
||||||
|
|
||||||
use_artifacts = getattr(settings, 'MURANO_USE_GLARE', False)
|
|
||||||
if use_artifacts:
|
|
||||||
artifacts = artifactclient(request)
|
|
||||||
else:
|
|
||||||
artifacts = None
|
|
||||||
|
|
||||||
token_id = request.user.token.id
|
|
||||||
LOG.debug('Murano::Client <Url: {0}>'.format(endpoint))
|
|
||||||
|
|
||||||
return client.Client(1, endpoint=endpoint, token=token_id,
|
|
||||||
insecure=insecure, artifacts_client=artifacts,
|
|
||||||
tenant=request.user.tenant_id)
|
|
|
@ -1,128 +0,0 @@
|
||||||
# Copyright (c) 2014 Mirantis, Inc.
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
||||||
# not use this file except in compliance with the License. You may obtain
|
|
||||||
# a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
||||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
||||||
# License for the specific language governing permissions and limitations
|
|
||||||
# under the License.
|
|
||||||
|
|
||||||
import itertools
|
|
||||||
|
|
||||||
from django.conf import settings
|
|
||||||
import yaml
|
|
||||||
|
|
||||||
from muranodashboard import api
|
|
||||||
from muranodashboard.common import cache
|
|
||||||
from muranodashboard.dynamic_ui import yaql_expression
|
|
||||||
|
|
||||||
|
|
||||||
def package_list(request, marker=None, filters=None, paginate=False,
|
|
||||||
page_size=20, sort_dir=None, limit=None):
|
|
||||||
limit = limit or getattr(settings, 'PACKAGES_LIMIT', 100)
|
|
||||||
filters = filters or {}
|
|
||||||
|
|
||||||
if paginate:
|
|
||||||
request_size = page_size + 1
|
|
||||||
else:
|
|
||||||
request_size = limit
|
|
||||||
|
|
||||||
if marker:
|
|
||||||
filters['marker'] = marker
|
|
||||||
if sort_dir:
|
|
||||||
filters['sort_dir'] = sort_dir
|
|
||||||
|
|
||||||
client = api.muranoclient(request)
|
|
||||||
|
|
||||||
packages_iter = client.packages.filter(limit=request_size,
|
|
||||||
**filters)
|
|
||||||
|
|
||||||
has_more_data = False
|
|
||||||
if paginate:
|
|
||||||
packages = list(itertools.islice(packages_iter, request_size))
|
|
||||||
if len(packages) > page_size:
|
|
||||||
packages.pop()
|
|
||||||
has_more_data = True
|
|
||||||
else:
|
|
||||||
packages = list(packages_iter)
|
|
||||||
|
|
||||||
return packages, has_more_data
|
|
||||||
|
|
||||||
|
|
||||||
def apps_that_inherit(request, fqn):
|
|
||||||
glare = getattr(settings, 'MURANO_USE_GLARE', False)
|
|
||||||
if not glare:
|
|
||||||
return []
|
|
||||||
apps = api.muranoclient(request).packages.filter(inherits=fqn)
|
|
||||||
return apps
|
|
||||||
|
|
||||||
|
|
||||||
def app_by_fqn(request, fqn, catalog=True, version=None):
|
|
||||||
kwargs = {'fqn': fqn, 'catalog': catalog}
|
|
||||||
glare = getattr(settings, 'MURANO_USE_GLARE', False)
|
|
||||||
if glare and version:
|
|
||||||
kwargs['version'] = version
|
|
||||||
apps = api.muranoclient(request).packages.filter(**kwargs)
|
|
||||||
try:
|
|
||||||
return next(apps)
|
|
||||||
except StopIteration:
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def make_loader_cls():
|
|
||||||
class Loader(yaml.SafeLoader):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def yaql_constructor(loader, node):
|
|
||||||
value = loader.construct_scalar(node)
|
|
||||||
return yaql_expression.YaqlExpression(value)
|
|
||||||
|
|
||||||
# workaround for PyYAML bug: http://pyyaml.org/ticket/221
|
|
||||||
resolvers = {}
|
|
||||||
for k, v in yaml.SafeLoader.yaml_implicit_resolvers.items():
|
|
||||||
resolvers[k] = v[:]
|
|
||||||
Loader.yaml_implicit_resolvers = resolvers
|
|
||||||
|
|
||||||
Loader.add_constructor(u'!yaql', yaql_constructor)
|
|
||||||
Loader.add_implicit_resolver(
|
|
||||||
u'!yaql', yaql_expression.YaqlExpression, None)
|
|
||||||
|
|
||||||
return Loader
|
|
||||||
|
|
||||||
|
|
||||||
# Here are cached some data calls to api; note that not every package attribute
|
|
||||||
# getter should be cached - only immutable ones could be safely cached. E.g.,
|
|
||||||
# it would be a mistake to cache Application Name because it is mutable and can
|
|
||||||
# be changed in Manage -> Packages while cache is immutable (i.e. it
|
|
||||||
# its contents are obtained from the api only the first time).
|
|
||||||
@cache.with_cache('ui', 'ui.yaml')
|
|
||||||
def get_app_ui(request, app_id):
|
|
||||||
return api.muranoclient(request).packages.get_ui(app_id, make_loader_cls())
|
|
||||||
|
|
||||||
|
|
||||||
@cache.with_cache('logo', 'logo.png')
|
|
||||||
def get_app_logo(request, app_id):
|
|
||||||
return api.muranoclient(request).packages.get_logo(app_id)
|
|
||||||
|
|
||||||
|
|
||||||
@cache.with_cache('supplier_logo', 'supplier_logo.png')
|
|
||||||
def get_app_supplier_logo(request, app_id):
|
|
||||||
return api.muranoclient(request).packages.get_supplier_logo(app_id)
|
|
||||||
|
|
||||||
|
|
||||||
def get_app_fqn(request, app_id):
|
|
||||||
return get_package_details(request, app_id).fully_qualified_name
|
|
||||||
|
|
||||||
|
|
||||||
def get_service_name(request, app_id):
|
|
||||||
return get_package_details(request, app_id).name
|
|
||||||
|
|
||||||
|
|
||||||
@cache.with_cache('package_details')
|
|
||||||
def get_package_details(request, app_id):
|
|
||||||
return api.muranoclient(request).packages.get(app_id)
|
|
|
@ -1,15 +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 REST API modules here
|
|
||||||
from . import environments # noqa
|
|
||||||
from . import packages # noqa
|
|
|
@ -1,158 +0,0 @@
|
||||||
# Copyright (c) 2016 Mirantis, Inc.
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
||||||
# not use this file except in compliance with the License. You may obtain
|
|
||||||
# a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
||||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
||||||
# License for the specific language governing permissions and limitations
|
|
||||||
# under the License.
|
|
||||||
|
|
||||||
|
|
||||||
from django.views import generic
|
|
||||||
|
|
||||||
from openstack_dashboard.api.rest import urls
|
|
||||||
from openstack_dashboard.api.rest import utils as rest_utils
|
|
||||||
|
|
||||||
from muranodashboard import api
|
|
||||||
from muranodashboard.environments import api as env_api
|
|
||||||
|
|
||||||
|
|
||||||
@urls.register
|
|
||||||
class ComponentsMetadata(generic.View):
|
|
||||||
"""API for Murano components Metadata"""
|
|
||||||
|
|
||||||
url_regex = r'app-catalog/environments/(?P<environment>[^/]+)' \
|
|
||||||
r'/components/(?P<component>[^/]+)/metadata/$'
|
|
||||||
|
|
||||||
@rest_utils.ajax()
|
|
||||||
def get(self, request, environment, component):
|
|
||||||
"""Get a metadata object for a component in a given environment
|
|
||||||
|
|
||||||
Example GET:
|
|
||||||
http://localhost/api/app-catalog/environments/123/components/456/metadata
|
|
||||||
|
|
||||||
The following get parameters may be passed in the GET
|
|
||||||
request:
|
|
||||||
|
|
||||||
:param environment: identifier of the environment
|
|
||||||
:param component: identifier of the component
|
|
||||||
|
|
||||||
Any additionally a "session" parameter should be passed through the API
|
|
||||||
as a keyword.
|
|
||||||
"""
|
|
||||||
filters, keywords = rest_utils.parse_filters_kwargs(request,
|
|
||||||
['session'])
|
|
||||||
session = keywords.get('session')
|
|
||||||
if not session:
|
|
||||||
session = env_api.Session.get_or_create_or_delete(request,
|
|
||||||
environment)
|
|
||||||
component = api.muranoclient(request).services.get(
|
|
||||||
environment, '/' + component, session)
|
|
||||||
if component:
|
|
||||||
return component.to_dict()['?'].get('metadata', {})
|
|
||||||
return {}
|
|
||||||
|
|
||||||
@rest_utils.ajax(data_required=True)
|
|
||||||
def post(self, request, environment, component):
|
|
||||||
"""Set a metadata object for a component in a given environment
|
|
||||||
|
|
||||||
Example POST:
|
|
||||||
http://localhost/api/app-catalog/environments/123/components/456/metadata
|
|
||||||
|
|
||||||
The following get parameters may be passed in the GET
|
|
||||||
request:
|
|
||||||
|
|
||||||
:param environment: identifier of the environment
|
|
||||||
:param component: identifier of the component
|
|
||||||
|
|
||||||
Any additionally a "session" parameter should be passed through the API
|
|
||||||
as a keyword. Request body should contain 'updated' keyword, contain
|
|
||||||
all the updated metadata attributes. If it is empty, the metadata is
|
|
||||||
considered to be deleted.
|
|
||||||
"""
|
|
||||||
client = api.muranoclient(request)
|
|
||||||
filters, keywords = rest_utils.parse_filters_kwargs(request,
|
|
||||||
['session'])
|
|
||||||
session = keywords.get('session')
|
|
||||||
if not session:
|
|
||||||
session = env_api.Session.get_or_create_or_delete(request,
|
|
||||||
environment)
|
|
||||||
updated = request.DATA.get('updated', {})
|
|
||||||
path = '/{0}/%3F/metadata'.format(component)
|
|
||||||
|
|
||||||
if updated:
|
|
||||||
client.services.put(environment, path, updated, session)
|
|
||||||
else:
|
|
||||||
client.services.delete(environment, path, session)
|
|
||||||
|
|
||||||
|
|
||||||
@urls.register
|
|
||||||
class EnvironmentsMetadata(generic.View):
|
|
||||||
"""API for Murano components Metadata"""
|
|
||||||
|
|
||||||
url_regex = r'app-catalog/environments/(?P<environment>[^/]+)/metadata/$'
|
|
||||||
|
|
||||||
@rest_utils.ajax()
|
|
||||||
def get(self, request, environment):
|
|
||||||
"""Get a metadata object for an environment
|
|
||||||
|
|
||||||
Example GET:
|
|
||||||
http://localhost/api/app-catalog/environments/123/metadata
|
|
||||||
|
|
||||||
The following get parameters may be passed in the GET
|
|
||||||
request:
|
|
||||||
|
|
||||||
:param environment: identifier of the environment
|
|
||||||
|
|
||||||
Any additionally a "session" parameter should be passed through the API
|
|
||||||
as a keyword.
|
|
||||||
"""
|
|
||||||
filters, keywords = rest_utils.parse_filters_kwargs(request,
|
|
||||||
['session'])
|
|
||||||
session = keywords.get('session')
|
|
||||||
if not session:
|
|
||||||
session = env_api.Session.get_or_create_or_delete(request,
|
|
||||||
environment)
|
|
||||||
env = api.muranoclient(request).environments.get_model(
|
|
||||||
environment, '/', session)
|
|
||||||
if env:
|
|
||||||
return env['?'].get('metadata', {})
|
|
||||||
return {}
|
|
||||||
|
|
||||||
@rest_utils.ajax(data_required=True)
|
|
||||||
def post(self, request, environment):
|
|
||||||
"""Set a metadata object for a given environment
|
|
||||||
|
|
||||||
Example POST:
|
|
||||||
http://localhost/api/app-catalog/environments/123/metadata
|
|
||||||
|
|
||||||
The following get parameters may be passed in the GET
|
|
||||||
request:
|
|
||||||
|
|
||||||
:param environment: identifier of the environment
|
|
||||||
|
|
||||||
Any additionally a "session" parameter should be passed through the API
|
|
||||||
as a keyword. Request body should contain 'updated' keyword, contain
|
|
||||||
all the updated metadata attributes. If it is empty, the metadata is
|
|
||||||
considered to be deleted.
|
|
||||||
"""
|
|
||||||
client = api.muranoclient(request)
|
|
||||||
filters, keywords = rest_utils.parse_filters_kwargs(request,
|
|
||||||
['session'])
|
|
||||||
|
|
||||||
session = keywords.get('session')
|
|
||||||
if not session:
|
|
||||||
session = env_api.Session.get_or_create_or_delete(request,
|
|
||||||
environment)
|
|
||||||
updated = request.DATA.get('updated', {})
|
|
||||||
patch = {
|
|
||||||
"op": "replace",
|
|
||||||
"path": "/?/metadata",
|
|
||||||
"value": updated
|
|
||||||
}
|
|
||||||
client.environments.update_model(environment, [patch], session)
|
|
|
@ -1,63 +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.
|
|
||||||
"""API for the murano packages service."""
|
|
||||||
|
|
||||||
from django.views import generic
|
|
||||||
from openstack_dashboard.api.rest import utils as rest_utils
|
|
||||||
|
|
||||||
from muranodashboard import api
|
|
||||||
from openstack_dashboard.api.rest import urls
|
|
||||||
|
|
||||||
|
|
||||||
CLIENT_KEYWORDS = {'marker', 'sort_dir', 'paginate'}
|
|
||||||
|
|
||||||
|
|
||||||
@urls.register
|
|
||||||
class Packages(generic.View):
|
|
||||||
"""API for Murano packages."""
|
|
||||||
url_regex = r'app-catalog/packages/$'
|
|
||||||
|
|
||||||
@rest_utils.ajax()
|
|
||||||
def get(self, request):
|
|
||||||
"""Get a list of packages.
|
|
||||||
|
|
||||||
The listing result is an object with property "packages".
|
|
||||||
|
|
||||||
Example GET:
|
|
||||||
http://localhost/api/app-catalog/packages?sort_dir=desc #flake8: noqa
|
|
||||||
|
|
||||||
The following get parameters may be passed in the GET
|
|
||||||
request:
|
|
||||||
|
|
||||||
:param paginate: If true will perform pagination based on settings.
|
|
||||||
:param marker: Specifies the namespace of the last-seen package.
|
|
||||||
The typical pattern of limit and marker is to make an
|
|
||||||
initial limited request and then to use the last
|
|
||||||
namespace from the response as the marker parameter
|
|
||||||
in a subsequent limited request. With paginate, limit
|
|
||||||
is automatically set.
|
|
||||||
:param sort_dir: The sort direction ('asc' or 'desc').
|
|
||||||
|
|
||||||
Any additional request parameters will be passed through the API as
|
|
||||||
filters.
|
|
||||||
"""
|
|
||||||
|
|
||||||
filters, kwargs = rest_utils.parse_filters_kwargs(request,
|
|
||||||
CLIENT_KEYWORDS)
|
|
||||||
|
|
||||||
packages, has_more_data = api.packages.package_list(
|
|
||||||
request, filters=filters, **kwargs)
|
|
||||||
|
|
||||||
return {
|
|
||||||
'packages': [p.to_dict() for p in packages],
|
|
||||||
'has_more_data': has_more_data,
|
|
||||||
}
|
|
|
@ -1,42 +0,0 @@
|
||||||
# Copyright (c) 2014 Mirantis, Inc.
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
||||||
# not use this file except in compliance with the License. You may obtain
|
|
||||||
# a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
||||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
||||||
# License for the specific language governing permissions and limitations
|
|
||||||
# under the License.
|
|
||||||
|
|
||||||
WF_MANAGEMENT_NAME = 'workflowManagement'
|
|
||||||
|
|
||||||
|
|
||||||
class WorkflowManagementForm(object):
|
|
||||||
def __init__(self):
|
|
||||||
self.name = WF_MANAGEMENT_NAME
|
|
||||||
self.field_specs = [
|
|
||||||
{'name': 'stay_at_the_catalog',
|
|
||||||
'initial': False,
|
|
||||||
'description': 'If checked, you will be returned to the '
|
|
||||||
'Catalog page. If not - to the '
|
|
||||||
'Environment page, where you can deploy'
|
|
||||||
' the application.',
|
|
||||||
'required': False,
|
|
||||||
'type': 'boolean',
|
|
||||||
'label': 'Continue application adding'}]
|
|
||||||
self.validators = []
|
|
||||||
|
|
||||||
def name_field(self, fqn):
|
|
||||||
return {'name': 'application_name',
|
|
||||||
'type': 'string',
|
|
||||||
'description': 'Enter a desired name for the application. '
|
|
||||||
'Just A-Z, a-z, 0-9, dash and underline'
|
|
||||||
' are allowed',
|
|
||||||
'label': 'Application Name',
|
|
||||||
'regexpValidator': '^[-\w]+$',
|
|
||||||
'initial': fqn.split('.')[-1]
|
|
||||||
}
|
|
|
@ -1,21 +0,0 @@
|
||||||
# Copyright (c) 2014 Mirantis, Inc.
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
||||||
# not use this file except in compliance with the License. You may obtain
|
|
||||||
# a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
||||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
||||||
# License for the specific language governing permissions and limitations
|
|
||||||
# under the License.
|
|
||||||
|
|
||||||
from django.utils.translation import ugettext_lazy as _
|
|
||||||
import horizon
|
|
||||||
|
|
||||||
|
|
||||||
class AppCatalog(horizon.Panel):
|
|
||||||
name = _('Browse Local')
|
|
||||||
slug = 'catalog'
|
|
|
@ -1,127 +0,0 @@
|
||||||
# Copyright (c) 2014 Mirantis, Inc.
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
||||||
# not use this file except in compliance with the License. You may obtain
|
|
||||||
# a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
||||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
||||||
# License for the specific language governing permissions and limitations
|
|
||||||
# under the License.
|
|
||||||
|
|
||||||
from horizon import tabs
|
|
||||||
|
|
||||||
from django.utils.translation import ugettext_lazy as _
|
|
||||||
from oslo_log import log as logging
|
|
||||||
|
|
||||||
from muranodashboard.dynamic_ui import services
|
|
||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class AppOverviewTab(tabs.Tab):
|
|
||||||
name = _('Overview')
|
|
||||||
slug = 'app_overview'
|
|
||||||
template_name = 'catalog/_overview.html'
|
|
||||||
preload = False
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
super(AppOverviewTab, self).__init__(*args, **kwargs)
|
|
||||||
self.app = self.tab_group.kwargs['application']
|
|
||||||
LOG.debug('AppOverviewTab: {0}'.format(self.app))
|
|
||||||
|
|
||||||
def get_context_data(self, request):
|
|
||||||
return {'app': self.app}
|
|
||||||
|
|
||||||
|
|
||||||
class AppRequirementsTab(tabs.Tab):
|
|
||||||
name = _('Requirements')
|
|
||||||
slug = 'app_requirements'
|
|
||||||
template_name = 'catalog/_app_requirements.html'
|
|
||||||
preload = False
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
super(AppRequirementsTab, self).__init__(*args, **kwargs)
|
|
||||||
self.app = self.tab_group.kwargs['application']
|
|
||||||
LOG.debug('AppREquirementsTab: {0}'.format(self.app))
|
|
||||||
|
|
||||||
def get_context_data(self, request):
|
|
||||||
self._get_requirements()
|
|
||||||
return {'application': self.app}
|
|
||||||
|
|
||||||
def _get_requirements(self):
|
|
||||||
forms = services.get_app_forms(self.request, {'app_id': self.app.id})
|
|
||||||
self.app.requirements = []
|
|
||||||
for step_name, step in forms:
|
|
||||||
for key in step.base_fields:
|
|
||||||
# Check for instance size requirements in the UI yaml file.
|
|
||||||
if key == 'flavor':
|
|
||||||
reqs = getattr(step.base_fields[key], 'requirements', '')
|
|
||||||
if reqs:
|
|
||||||
# Make the requirement values screen-printable.
|
|
||||||
self.app.requirements.append('Instance flavor:')
|
|
||||||
requirements = []
|
|
||||||
for req in reqs:
|
|
||||||
if req == 'min_disk':
|
|
||||||
requirements.append(
|
|
||||||
'Minimum disk size: {0} GB'.format(
|
|
||||||
str(reqs[req])))
|
|
||||||
elif req == 'min_vcpus':
|
|
||||||
requirements.append(
|
|
||||||
'Minimum vCPUs: {0}'.format(
|
|
||||||
str(reqs[req])))
|
|
||||||
elif req == 'min_memory_mb':
|
|
||||||
requirements.append(
|
|
||||||
'Minimum RAM size: {0} MB'.format(
|
|
||||||
str(reqs[req])))
|
|
||||||
elif req == 'max_disk':
|
|
||||||
requirements.append(
|
|
||||||
'Maximum disk size: {0} GB'.format(
|
|
||||||
str(reqs[req])))
|
|
||||||
elif req == 'max_vcpus':
|
|
||||||
requirements.append(
|
|
||||||
'Maximum vCPUs: {0}'.format(
|
|
||||||
str(reqs[req])))
|
|
||||||
elif req == 'max_memory_mb':
|
|
||||||
requirements.append(
|
|
||||||
'Maximum RAM size: {0} MB'.format(
|
|
||||||
str(reqs[req])))
|
|
||||||
self.app.requirements.append(requirements)
|
|
||||||
|
|
||||||
|
|
||||||
class AppLicenseAgreementTab(tabs.Tab):
|
|
||||||
name = _('License')
|
|
||||||
slug = 'app_license'
|
|
||||||
template_name = 'catalog/_app_license.html'
|
|
||||||
preload = False
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
super(AppLicenseAgreementTab, self).__init__(*args, **kwargs)
|
|
||||||
self.app = self.tab_group.kwargs['application']
|
|
||||||
LOG.debug('AppLicenseAgreementTab: {0}'.format(self.app))
|
|
||||||
|
|
||||||
def get_context_data(self, request):
|
|
||||||
self._get_license()
|
|
||||||
return {'application': self.app}
|
|
||||||
|
|
||||||
def _get_license(self):
|
|
||||||
forms = services.get_app_forms(self.request, {'app_id': self.app.id})
|
|
||||||
self.app.license = ''
|
|
||||||
for step_name, step in forms:
|
|
||||||
for key in step.base_fields.keys():
|
|
||||||
# Check for a license in the UI yaml file.
|
|
||||||
if key == 'license':
|
|
||||||
self.app.license = step.base_fields[key].description
|
|
||||||
|
|
||||||
|
|
||||||
class ApplicationTabs(tabs.TabGroup):
|
|
||||||
slug = 'app_details'
|
|
||||||
tabs = (AppOverviewTab, AppRequirementsTab, AppLicenseAgreementTab)
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
self.app = kwargs.get('application', None)
|
|
||||||
LOG.debug('ApplicationTabs: {0}'.format(self.app))
|
|
||||||
super(ApplicationTabs, self).__init__(*args, **kwargs)
|
|
|
@ -1,40 +0,0 @@
|
||||||
# Copyright (c) 2014 Mirantis, Inc.
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
||||||
# not use this file except in compliance with the License. You may obtain
|
|
||||||
# a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
||||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
||||||
# License for the specific language governing permissions and limitations
|
|
||||||
# under the License.
|
|
||||||
|
|
||||||
from django.conf import urls
|
|
||||||
|
|
||||||
from muranodashboard.catalog import views
|
|
||||||
from muranodashboard.dynamic_ui import services
|
|
||||||
|
|
||||||
wizard_view = views.Wizard.as_view(
|
|
||||||
services.get_app_forms, condition_dict=services.condition_getter)
|
|
||||||
|
|
||||||
urlpatterns = [
|
|
||||||
urls.url(r'^$', views.IndexView.as_view(), name='index'),
|
|
||||||
urls.url(r'^switch_environment/(?P<environment_id>[^/]+)$',
|
|
||||||
views.switch, name='switch_env'),
|
|
||||||
urls.url(r'^add/(?P<app_id>[^/]+)/(?P<environment_id>[^/]+)/'
|
|
||||||
r'(?P<do_redirect>[^/]+)/(?P<drop_wm_form>[^/]+)$',
|
|
||||||
wizard_view, name='add'),
|
|
||||||
urls.url(r'^add/(?P<app_id>[^/]+)/(?P<environment_id>[^/]+)$',
|
|
||||||
views.deploy, name='deploy'),
|
|
||||||
urls.url(r'^quick-add/(?P<app_id>[^/]+)$',
|
|
||||||
views.quick_deploy, name='quick_deploy'),
|
|
||||||
urls.url(r'^details/(?P<application_id>[^/]+)$',
|
|
||||||
views.AppDetailsView.as_view(), name='application_details'),
|
|
||||||
urls.url(r'^images/(?P<app_id>[^/]*)',
|
|
||||||
views.get_image, name="images"),
|
|
||||||
urls.url(r'^supplier-images/(?P<app_id>[^/]*)',
|
|
||||||
views.get_supplier_image, name="supplier_images")
|
|
||||||
]
|
|
|
@ -1,640 +0,0 @@
|
||||||
# Copyright (c) 2014 Mirantis, Inc.
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
||||||
# not use this file except in compliance with the License. You may obtain
|
|
||||||
# a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
||||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
||||||
# License for the specific language governing permissions and limitations
|
|
||||||
# under the License.
|
|
||||||
|
|
||||||
import collections
|
|
||||||
import copy
|
|
||||||
import functools
|
|
||||||
import json
|
|
||||||
import re
|
|
||||||
import uuid
|
|
||||||
|
|
||||||
from django.conf import settings
|
|
||||||
from django.contrib import auth
|
|
||||||
from django.contrib.auth import decorators as auth_dec
|
|
||||||
from django.contrib.staticfiles.templatetags.staticfiles import static
|
|
||||||
from django.core.urlresolvers import reverse
|
|
||||||
# django.contrib.formtools migration to django 1.8
|
|
||||||
# https://docs.djangoproject.com/en/1.8/ref/contrib/formtools/
|
|
||||||
try:
|
|
||||||
from django.contrib.formtools.wizard import views as wizard_views
|
|
||||||
except ImportError:
|
|
||||||
from formtools.wizard import views as wizard_views
|
|
||||||
from django import http
|
|
||||||
from django import shortcuts
|
|
||||||
from django.utils import decorators as django_dec
|
|
||||||
from django.utils import html
|
|
||||||
from django.utils import http as http_utils
|
|
||||||
from django.utils.translation import ugettext_lazy as _
|
|
||||||
from django.views.generic import list as list_view
|
|
||||||
from horizon import exceptions
|
|
||||||
from horizon.forms import views
|
|
||||||
from horizon import messages
|
|
||||||
from horizon import tabs
|
|
||||||
from horizon import views as generic_views
|
|
||||||
from oslo_log import log as logging
|
|
||||||
import six
|
|
||||||
|
|
||||||
from muranoclient.common import exceptions as exc
|
|
||||||
from muranodashboard import api
|
|
||||||
from muranodashboard.api import packages as pkg_api
|
|
||||||
from muranodashboard.catalog import tabs as catalog_tabs
|
|
||||||
from muranodashboard.common import utils
|
|
||||||
from muranodashboard.dynamic_ui import helpers
|
|
||||||
from muranodashboard.dynamic_ui import services
|
|
||||||
from muranodashboard.environments import api as env_api
|
|
||||||
from muranodashboard.environments import consts
|
|
||||||
from muranodashboard.packages import consts as pkg_consts
|
|
||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
|
||||||
ALL_CATEGORY_NAME = 'All'
|
|
||||||
LATEST_APPS_QUEUE_LIMIT = 3
|
|
||||||
|
|
||||||
|
|
||||||
class DictToObj(object):
|
|
||||||
def __init__(self, **kwargs):
|
|
||||||
for key, value in six.iteritems(kwargs):
|
|
||||||
setattr(self, key, value)
|
|
||||||
|
|
||||||
|
|
||||||
def get_available_environments(request):
|
|
||||||
envs = []
|
|
||||||
for env in env_api.environments_list(request):
|
|
||||||
obj = DictToObj(id=env.id, name=env.name, status=env.status)
|
|
||||||
envs.append(obj)
|
|
||||||
|
|
||||||
return envs
|
|
||||||
|
|
||||||
|
|
||||||
def is_valid_environment(environment, valid_environments):
|
|
||||||
for env in valid_environments:
|
|
||||||
if environment.id == env.id:
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def get_environments_context(request):
|
|
||||||
envs = get_available_environments(request)
|
|
||||||
context = {'available_environments': envs}
|
|
||||||
environment = request.session.get('environment')
|
|
||||||
if environment and is_valid_environment(environment, envs):
|
|
||||||
context['environment'] = environment
|
|
||||||
elif envs:
|
|
||||||
context['environment'] = envs[0]
|
|
||||||
return context
|
|
||||||
|
|
||||||
|
|
||||||
def get_categories_list(request):
|
|
||||||
"""Returns a list of categories, sorted.
|
|
||||||
|
|
||||||
Categories with packages come first, categories without
|
|
||||||
packages come second. both groups alphabetically sorted.
|
|
||||||
"""
|
|
||||||
|
|
||||||
categories = []
|
|
||||||
with api.handled_exceptions(request):
|
|
||||||
client = api.muranoclient(request)
|
|
||||||
categories = client.categories.list()
|
|
||||||
|
|
||||||
# NOTE(kzaitsev) We rely here on tuple comparison and ascending order of
|
|
||||||
# sorted(). i.e. (False, 'a') < (False, 'b') < (True, 'a') < (True, 'b')
|
|
||||||
# So to make order more human-friendly we sort based on
|
|
||||||
# package_count == 0, pushing categories without packages in front and
|
|
||||||
# and then sorting them alphabetically
|
|
||||||
categories = [cat for cat in sorted(
|
|
||||||
categories, key=lambda c: (c.package_count == 0, c.name))]
|
|
||||||
# TODO(kzaitsev): add sorting options to category API
|
|
||||||
|
|
||||||
return categories
|
|
||||||
|
|
||||||
|
|
||||||
@auth_dec.login_required
|
|
||||||
def switch(request, environment_id,
|
|
||||||
redirect_field_name=auth.REDIRECT_FIELD_NAME):
|
|
||||||
redirect_to = request.GET.get(redirect_field_name, '')
|
|
||||||
if not http_utils.is_safe_url(url=redirect_to, host=request.get_host()):
|
|
||||||
redirect_to = settings.LOGIN_REDIRECT_URL
|
|
||||||
|
|
||||||
for env in get_available_environments(request):
|
|
||||||
if env.id == environment_id:
|
|
||||||
request.session['environment'] = env
|
|
||||||
break
|
|
||||||
return shortcuts.redirect(redirect_to)
|
|
||||||
|
|
||||||
|
|
||||||
def get_next_quick_environment_name(request):
|
|
||||||
quick_env_prefix = 'quick-env-'
|
|
||||||
quick_env_re = re.compile('^' + quick_env_prefix + '([\d]+)$')
|
|
||||||
|
|
||||||
def parse_number(env):
|
|
||||||
match = re.match(quick_env_re, env.name)
|
|
||||||
return int(match.group(1)) if match else 0
|
|
||||||
|
|
||||||
numbers = [parse_number(e) for e in env_api.environments_list(request)]
|
|
||||||
new_env_number = 1
|
|
||||||
if numbers:
|
|
||||||
numbers.sort()
|
|
||||||
new_env_number = numbers[-1] + 1
|
|
||||||
|
|
||||||
return quick_env_prefix + str(new_env_number)
|
|
||||||
|
|
||||||
|
|
||||||
def create_quick_environment(request):
|
|
||||||
params = {'name': get_next_quick_environment_name(request)}
|
|
||||||
return env_api.environment_create(request, params)
|
|
||||||
|
|
||||||
|
|
||||||
def update_latest_apps(func):
|
|
||||||
"""Update 'app_id's in session
|
|
||||||
|
|
||||||
Adds package id to a session queue with Applications which were
|
|
||||||
recently added to an environment or to the Catalog itself. Thus it is
|
|
||||||
used as decorator for views adding application to an environment or
|
|
||||||
uploading new package definition to a catalog.
|
|
||||||
"""
|
|
||||||
@functools.wraps(func)
|
|
||||||
def __inner(request, **kwargs):
|
|
||||||
apps = request.session.setdefault('latest_apps', collections.deque())
|
|
||||||
app_id = kwargs['app_id']
|
|
||||||
if app_id in apps: # move recent app to the beginning
|
|
||||||
apps.remove(app_id)
|
|
||||||
|
|
||||||
apps.appendleft(app_id)
|
|
||||||
if len(apps) > LATEST_APPS_QUEUE_LIMIT:
|
|
||||||
apps.pop()
|
|
||||||
|
|
||||||
return func(request, **kwargs)
|
|
||||||
|
|
||||||
return __inner
|
|
||||||
|
|
||||||
|
|
||||||
def cleaned_latest_apps(request):
|
|
||||||
"""Returns a list of recently used apps
|
|
||||||
|
|
||||||
Verifies, that apps in the list are either public or belong to current
|
|
||||||
project.
|
|
||||||
"""
|
|
||||||
id_param = "in:" + ",".join(request.session.get('latest_apps', []))
|
|
||||||
query_params = {'type': 'Application', 'catalog': True, 'id': id_param}
|
|
||||||
user_apps = list(api.muranoclient(request).packages.filter(**query_params))
|
|
||||||
request.session['latest_apps'] = collections.deque([app.id
|
|
||||||
for app in user_apps])
|
|
||||||
return user_apps
|
|
||||||
|
|
||||||
|
|
||||||
def clear_forms_data(func):
|
|
||||||
"""Removes form data from session
|
|
||||||
|
|
||||||
Clears user's session from a data for a specific application. It
|
|
||||||
guarantees that previous additions of that application won't interfere
|
|
||||||
with the next ones. Should be used as a decorator for entry points for
|
|
||||||
adding an application in an environment.
|
|
||||||
"""
|
|
||||||
@functools.wraps(func)
|
|
||||||
def __inner(request, **kwargs):
|
|
||||||
app_id = kwargs['app_id']
|
|
||||||
fqn = pkg_api.get_app_fqn(request, app_id)
|
|
||||||
LOG.debug('Clearing forms data for application {0}.'.format(fqn))
|
|
||||||
services.get_apps_data(request)[app_id] = {}
|
|
||||||
LOG.debug('Clearing any leftover wizard step data.')
|
|
||||||
for key in request.session.keys():
|
|
||||||
# TODO(tsufiev): unhardcode the prefix for wizard step data
|
|
||||||
if key.startswith('wizard_wizard'):
|
|
||||||
request.session.pop(key)
|
|
||||||
return func(request, **kwargs)
|
|
||||||
|
|
||||||
return __inner
|
|
||||||
|
|
||||||
|
|
||||||
def clear_quick_env_id(func):
|
|
||||||
@functools.wraps(func)
|
|
||||||
def __inner(request, **kwargs):
|
|
||||||
request.session.pop('quick_env_id', None)
|
|
||||||
return func(request, **kwargs)
|
|
||||||
|
|
||||||
return __inner
|
|
||||||
|
|
||||||
|
|
||||||
@update_latest_apps
|
|
||||||
@clear_forms_data
|
|
||||||
@auth_dec.login_required
|
|
||||||
def deploy(request, environment_id, app_id,
|
|
||||||
do_redirect=False, drop_wm_form=False):
|
|
||||||
view = Wizard.as_view(services.get_app_forms,
|
|
||||||
condition_dict=services.condition_getter)
|
|
||||||
return view(request, app_id=app_id, environment_id=environment_id,
|
|
||||||
do_redirect=do_redirect, drop_wm_form=drop_wm_form)
|
|
||||||
|
|
||||||
|
|
||||||
@clear_quick_env_id
|
|
||||||
@update_latest_apps
|
|
||||||
@clear_forms_data
|
|
||||||
@auth_dec.login_required
|
|
||||||
def quick_deploy(request, app_id):
|
|
||||||
return deploy(request, app_id=app_id, environment_id=None,
|
|
||||||
do_redirect=True, drop_wm_form=True)
|
|
||||||
|
|
||||||
|
|
||||||
def get_image(request, app_id):
|
|
||||||
try:
|
|
||||||
content = pkg_api.get_app_logo(request, app_id)
|
|
||||||
except (AttributeError, exc.HTTPNotFound):
|
|
||||||
message = _("Can not get logo for {0}.").format(app_id)
|
|
||||||
LOG.warning(message)
|
|
||||||
content = None
|
|
||||||
if content:
|
|
||||||
return http.HttpResponse(content=content, content_type='image/png')
|
|
||||||
else:
|
|
||||||
universal_logo = static('muranodashboard/images/icon.png')
|
|
||||||
return http.HttpResponseRedirect(universal_logo)
|
|
||||||
|
|
||||||
|
|
||||||
def get_supplier_image(request, app_id):
|
|
||||||
try:
|
|
||||||
content = pkg_api.get_app_supplier_logo(request, app_id)
|
|
||||||
except (AttributeError, exc.HTTPNotFound):
|
|
||||||
message = _("Can not get supplier logo for {0}.").format(app_id)
|
|
||||||
LOG.warning(message)
|
|
||||||
content = None
|
|
||||||
if content:
|
|
||||||
return http.HttpResponse(content=content, content_type='image/png')
|
|
||||||
else:
|
|
||||||
universal_logo = static('muranodashboard/images/icon.png')
|
|
||||||
return http.HttpResponseRedirect(universal_logo)
|
|
||||||
|
|
||||||
|
|
||||||
class LazyWizard(wizard_views.SessionWizardView):
|
|
||||||
"""Lazy version of SessionWizardView
|
|
||||||
|
|
||||||
The class which defers evaluation of form_list and condition_dict
|
|
||||||
until view method is called. So, each time we load a page with a dynamic
|
|
||||||
UI form, it will have markup/logic from the newest YAML-file definition.
|
|
||||||
"""
|
|
||||||
@django_dec.classonlymethod
|
|
||||||
def as_view(cls, initforms, *initargs, **initkwargs):
|
|
||||||
"""Main entry point for a request-response process."""
|
|
||||||
# sanitize keyword arguments
|
|
||||||
for key in initkwargs:
|
|
||||||
if key in cls.http_method_names:
|
|
||||||
raise TypeError(u"You tried to pass in the %s method name as a"
|
|
||||||
u" keyword argument to %s(). Don't do that."
|
|
||||||
% (key, cls.__name__))
|
|
||||||
if not hasattr(cls, key):
|
|
||||||
raise TypeError(u"%s() received an invalid keyword %r" % (
|
|
||||||
cls.__name__, key))
|
|
||||||
|
|
||||||
@update_latest_apps
|
|
||||||
def view(request, *args, **kwargs):
|
|
||||||
forms = initforms
|
|
||||||
if hasattr(initforms, '__call__'):
|
|
||||||
forms = initforms(request, kwargs)
|
|
||||||
_kwargs = copy.copy(initkwargs)
|
|
||||||
|
|
||||||
_kwargs = cls.get_initkwargs(forms, *initargs, **_kwargs)
|
|
||||||
|
|
||||||
cdict = _kwargs.get('condition_dict')
|
|
||||||
if cdict and hasattr(cdict, '__call__'):
|
|
||||||
_kwargs['condition_dict'] = cdict(request, kwargs)
|
|
||||||
|
|
||||||
self = cls(**_kwargs)
|
|
||||||
if hasattr(self, 'get') and not hasattr(self, 'head'):
|
|
||||||
self.head = self.get
|
|
||||||
self.request = request
|
|
||||||
self.args = args
|
|
||||||
self.kwargs = kwargs
|
|
||||||
return self.dispatch(request, *args, **kwargs)
|
|
||||||
|
|
||||||
# take name and docstring from class
|
|
||||||
functools.update_wrapper(view, cls, updated=())
|
|
||||||
|
|
||||||
# and possible attributes set by decorators
|
|
||||||
# like csrf_exempt from dispatch
|
|
||||||
functools.update_wrapper(view, cls.dispatch, assigned=())
|
|
||||||
return view
|
|
||||||
|
|
||||||
|
|
||||||
class Wizard(generic_views.PageTitleMixin, views.ModalFormMixin, LazyWizard):
|
|
||||||
template_name = 'services/wizard_create.html'
|
|
||||||
do_redirect = False
|
|
||||||
page_title = _("Add Application")
|
|
||||||
|
|
||||||
def get_prefix(self, *args, **kwargs):
|
|
||||||
base = super(Wizard, self).get_prefix(*args, **kwargs)
|
|
||||||
fmt = utils.BlankFormatter()
|
|
||||||
return fmt.format('{0}_{app_id}', base, **kwargs)
|
|
||||||
|
|
||||||
def get_form_prefix(self, step=None, form=None):
|
|
||||||
if step is None:
|
|
||||||
return self.steps.step0
|
|
||||||
else:
|
|
||||||
index0 = self.steps.all.index(step)
|
|
||||||
return str(index0)
|
|
||||||
|
|
||||||
def done(self, form_list, **kwargs):
|
|
||||||
app_name = self.storage.extra_data['app'].name
|
|
||||||
service = tuple(form_list)[0].service
|
|
||||||
attributes = service.extract_attributes()
|
|
||||||
attributes = helpers.insert_hidden_ids(attributes)
|
|
||||||
|
|
||||||
storage = attributes.setdefault('?', {}).setdefault(
|
|
||||||
consts.DASHBOARD_ATTRS_KEY, {})
|
|
||||||
storage['name'] = app_name
|
|
||||||
|
|
||||||
do_redirect = self.get_wizard_flag('do_redirect')
|
|
||||||
wm_form_data = service.cleaned_data.get('workflowManagement')
|
|
||||||
if wm_form_data:
|
|
||||||
do_redirect = do_redirect or not wm_form_data.get(
|
|
||||||
'stay_at_the_catalog', True)
|
|
||||||
|
|
||||||
fail_url = reverse("horizon:app-catalog:environments:index")
|
|
||||||
environment_id = utils.ensure_python_obj(kwargs.get('environment_id'))
|
|
||||||
quick_environment_id = self.request.session.get('quick_env_id')
|
|
||||||
try:
|
|
||||||
# NOTE (tsufiev): create new quick environment only if we came
|
|
||||||
# here after pressing 'Quick Deploy' button and quick environment
|
|
||||||
# wasn't created yet during addition of some referred App
|
|
||||||
if environment_id is None:
|
|
||||||
if quick_environment_id is None:
|
|
||||||
env = create_quick_environment(self.request)
|
|
||||||
self.request.session['quick_env_id'] = env.id
|
|
||||||
environment_id = env.id
|
|
||||||
else:
|
|
||||||
environment_id = quick_environment_id
|
|
||||||
env_url = reverse('horizon:app-catalog:environments:services',
|
|
||||||
args=(environment_id,))
|
|
||||||
|
|
||||||
srv = env_api.service_create(
|
|
||||||
self.request, environment_id, attributes)
|
|
||||||
except exc.HTTPForbidden:
|
|
||||||
msg = _("Sorry, you can't add application right now. "
|
|
||||||
"The environment is deploying.")
|
|
||||||
exceptions.handle(self.request, msg, redirect=fail_url)
|
|
||||||
except Exception:
|
|
||||||
message = _('Adding application to an environment failed.')
|
|
||||||
LOG.exception(message)
|
|
||||||
if quick_environment_id:
|
|
||||||
env_api.environment_delete(self.request, quick_environment_id)
|
|
||||||
fail_url = reverse('horizon:app-catalog:catalog:index')
|
|
||||||
exceptions.handle(self.request, message, redirect=fail_url)
|
|
||||||
else:
|
|
||||||
message = _("The '{0}' application successfully added to "
|
|
||||||
"environment.").format(app_name)
|
|
||||||
LOG.info(message)
|
|
||||||
messages.success(self.request, message)
|
|
||||||
|
|
||||||
if do_redirect:
|
|
||||||
return http.HttpResponseRedirect(env_url)
|
|
||||||
else:
|
|
||||||
srv_id = getattr(srv, '?')['id']
|
|
||||||
return self.create_hacked_response(
|
|
||||||
srv_id,
|
|
||||||
attributes['?'].get('name') or attributes.get('name'))
|
|
||||||
|
|
||||||
def create_hacked_response(self, obj_id, obj_name):
|
|
||||||
# copy-paste from horizon.forms.views.ModalFormView; should be done
|
|
||||||
# that way until we move here from django Wizard to horizon workflow
|
|
||||||
if views.ADD_TO_FIELD_HEADER in self.request.META:
|
|
||||||
field_id = self.request.META[views.ADD_TO_FIELD_HEADER]
|
|
||||||
response = http.HttpResponse(json.dumps(
|
|
||||||
[obj_id, html.escape(obj_name)]
|
|
||||||
))
|
|
||||||
response["X-Horizon-Add-To-Field"] = field_id
|
|
||||||
return response
|
|
||||||
else:
|
|
||||||
return http.HttpResponse()
|
|
||||||
|
|
||||||
def get_form_initial(self, step):
|
|
||||||
env_id = utils.ensure_python_obj(self.kwargs.get('environment_id'))
|
|
||||||
if env_id is None:
|
|
||||||
env_id = self.request.session.get('quick_env_id')
|
|
||||||
init_dict = {'request': self.request,
|
|
||||||
'app_id': self.kwargs['app_id'],
|
|
||||||
'environment_id': env_id}
|
|
||||||
|
|
||||||
return self.initial_dict.get(step, init_dict)
|
|
||||||
|
|
||||||
def _get_wizard_param(self, key):
|
|
||||||
param = self.kwargs.get(key)
|
|
||||||
return param if param is not None else self.request.POST.get(key)
|
|
||||||
|
|
||||||
def get_wizard_flag(self, key):
|
|
||||||
value = self._get_wizard_param(key)
|
|
||||||
return utils.ensure_python_obj(value)
|
|
||||||
|
|
||||||
def get_context_data(self, form, **kwargs):
|
|
||||||
context = super(Wizard, self).get_context_data(form=form, **kwargs)
|
|
||||||
mc = api.muranoclient(self.request)
|
|
||||||
app_id = self.kwargs.get('app_id')
|
|
||||||
app = self.storage.extra_data.get('app')
|
|
||||||
|
|
||||||
# Save extra data to prevent extra API calls
|
|
||||||
if not app:
|
|
||||||
app = mc.packages.get(app_id)
|
|
||||||
self.storage.extra_data['app'] = app
|
|
||||||
|
|
||||||
wizard_id = self.request.POST.get('wizard_id')
|
|
||||||
if wizard_id is None:
|
|
||||||
wizard_id = uuid.uuid4()
|
|
||||||
|
|
||||||
environment_id = self.kwargs.get('environment_id')
|
|
||||||
environment_id = utils.ensure_python_obj(environment_id)
|
|
||||||
if environment_id is not None:
|
|
||||||
env_name = mc.environments.get(environment_id).name
|
|
||||||
else:
|
|
||||||
env_name = get_next_quick_environment_name(self.request)
|
|
||||||
|
|
||||||
field_descr, extended_descr = services.get_app_field_descriptions(
|
|
||||||
self.request, app_id, self.steps.index)
|
|
||||||
|
|
||||||
context.update({'type': app.fully_qualified_name,
|
|
||||||
'service_name': app.name,
|
|
||||||
'app_id': app_id,
|
|
||||||
'environment_id': environment_id,
|
|
||||||
'environment_name': env_name,
|
|
||||||
'do_redirect': self.get_wizard_flag('do_redirect'),
|
|
||||||
'drop_wm_form': self.get_wizard_flag('drop_wm_form'),
|
|
||||||
'prefix': self.prefix,
|
|
||||||
'wizard_id': wizard_id,
|
|
||||||
'field_descriptions': field_descr,
|
|
||||||
'extended_descriptions': extended_descr,
|
|
||||||
})
|
|
||||||
return context
|
|
||||||
|
|
||||||
|
|
||||||
class IndexView(generic_views.PageTitleMixin, list_view.ListView):
|
|
||||||
paginate_by = 6
|
|
||||||
page_title = _("Browse")
|
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
|
||||||
super(IndexView, self).__init__(**kwargs)
|
|
||||||
self._more = None
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_object_id(datum):
|
|
||||||
return datum.id
|
|
||||||
|
|
||||||
def get_marker(self, index=-1):
|
|
||||||
"""Get the pagination marker
|
|
||||||
|
|
||||||
Returns the identifier for the object indexed by ``index`` in the
|
|
||||||
current data set for APIs that use marker/limit-based paging.
|
|
||||||
"""
|
|
||||||
data = self.object_list
|
|
||||||
if data:
|
|
||||||
return http_utils.urlquote_plus(self.get_object_id(data[index]))
|
|
||||||
else:
|
|
||||||
return ''
|
|
||||||
|
|
||||||
def get_query_params(self, internal_query=False):
|
|
||||||
if internal_query:
|
|
||||||
query_params = {'type': 'Application'}
|
|
||||||
else:
|
|
||||||
query_params = {}
|
|
||||||
category = self.get_current_category()
|
|
||||||
search = self.request.GET.get('search')
|
|
||||||
|
|
||||||
if search:
|
|
||||||
query_params['search'] = search
|
|
||||||
else:
|
|
||||||
if category != ALL_CATEGORY_NAME:
|
|
||||||
query_params['category'] = category
|
|
||||||
|
|
||||||
query_params['order_by'] = self.request.GET.get('order_by', 'name')
|
|
||||||
query_params['sort_dir'] = self.request.GET.get('sort_dir', 'asc')
|
|
||||||
return query_params
|
|
||||||
|
|
||||||
def get_queryset(self):
|
|
||||||
query_params = self.get_query_params(internal_query=True)
|
|
||||||
marker = self.request.GET.get('marker')
|
|
||||||
|
|
||||||
sort_dir = query_params['sort_dir']
|
|
||||||
|
|
||||||
packages = []
|
|
||||||
with api.handled_exceptions(self.request):
|
|
||||||
query_params['catalog'] = True
|
|
||||||
packages, self._more = pkg_api.package_list(
|
|
||||||
self.request, filters=query_params, paginate=True,
|
|
||||||
marker=marker, page_size=self.paginate_by, sort_dir=sort_dir,
|
|
||||||
limit=self.paginate_by)
|
|
||||||
|
|
||||||
if sort_dir == 'desc':
|
|
||||||
packages = list(reversed(packages))
|
|
||||||
|
|
||||||
return packages
|
|
||||||
|
|
||||||
def get_template_names(self):
|
|
||||||
return ['catalog/index.html']
|
|
||||||
|
|
||||||
def has_next_page(self):
|
|
||||||
if self.request.GET.get('sort_dir', 'asc') == 'asc':
|
|
||||||
return self._more
|
|
||||||
else:
|
|
||||||
query_params = self.get_query_params(internal_query=True)
|
|
||||||
query_params['sort_dir'] = 'asc'
|
|
||||||
query_params['catalog'] = True
|
|
||||||
packages, more = pkg_api.package_list(
|
|
||||||
self.request, filters=query_params, paginate=True,
|
|
||||||
marker=self.get_marker(), page_size=1)
|
|
||||||
return len(packages) > 0
|
|
||||||
|
|
||||||
def has_prev_page(self):
|
|
||||||
if self.request.GET.get('sort_dir', 'asc') == 'desc':
|
|
||||||
return self._more
|
|
||||||
else:
|
|
||||||
return self.request.GET.get('marker') is not None
|
|
||||||
|
|
||||||
def paginate_queryset(self, queryset, page_size):
|
|
||||||
# override this method explicitly to skip unnecessary calculations
|
|
||||||
# during call to parent's get_context_data() method
|
|
||||||
return None, None, queryset, None
|
|
||||||
|
|
||||||
def get_current_category(self):
|
|
||||||
return self.request.GET.get('category', ALL_CATEGORY_NAME)
|
|
||||||
|
|
||||||
def current_page_url(self):
|
|
||||||
query_params = self.get_query_params()
|
|
||||||
marker = self.request.GET.get('marker')
|
|
||||||
sort_dir = self.request.GET.get('sort_dir')
|
|
||||||
if marker:
|
|
||||||
query_params['marker'] = marker
|
|
||||||
if sort_dir:
|
|
||||||
query_params['sort_dir'] = sort_dir
|
|
||||||
return '{0}?{1}'.format(reverse('horizon:app-catalog:catalog:index'),
|
|
||||||
http_utils.urlencode(query_params))
|
|
||||||
|
|
||||||
def prev_page_url(self):
|
|
||||||
query_params = self.get_query_params()
|
|
||||||
query_params['marker'] = self.get_marker(0)
|
|
||||||
query_params['sort_dir'] = 'desc'
|
|
||||||
return '{0}?{1}'.format(reverse('horizon:app-catalog:catalog:index'),
|
|
||||||
http_utils.urlencode(query_params))
|
|
||||||
|
|
||||||
def next_page_url(self):
|
|
||||||
query_params = self.get_query_params()
|
|
||||||
query_params['marker'] = self.get_marker()
|
|
||||||
query_params['sort_dir'] = 'asc'
|
|
||||||
return '{0}?{1}'.format(reverse('horizon:app-catalog:catalog:index'),
|
|
||||||
http_utils.urlencode(query_params))
|
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
|
||||||
context = super(IndexView, self).get_context_data(**kwargs)
|
|
||||||
|
|
||||||
context.update({
|
|
||||||
'ALL_CATEGORY_NAME': ALL_CATEGORY_NAME,
|
|
||||||
'categories': get_categories_list(self.request),
|
|
||||||
'current_category': self.get_current_category(),
|
|
||||||
'latest_list': cleaned_latest_apps(self.request)
|
|
||||||
})
|
|
||||||
|
|
||||||
search = self.request.GET.get('search')
|
|
||||||
if search:
|
|
||||||
context['search'] = search
|
|
||||||
|
|
||||||
context['tenant_id'] = self.request.session['token'].tenant['id']
|
|
||||||
context.update(get_environments_context(self.request))
|
|
||||||
context['display_repo_url'] = pkg_consts.DISPLAY_MURANO_REPO_URL
|
|
||||||
context['pkg_def_url'] = reverse('horizon:app-catalog:packages:index')
|
|
||||||
context['no_apps'] = True
|
|
||||||
if self.get_current_category() != ALL_CATEGORY_NAME or search:
|
|
||||||
context['no_apps'] = False
|
|
||||||
context['MURANO_USE_GLARE'] = getattr(settings, 'MURANO_USE_GLARE',
|
|
||||||
False)
|
|
||||||
return context
|
|
||||||
|
|
||||||
|
|
||||||
class AppDetailsView(tabs.TabView):
|
|
||||||
tab_group_class = catalog_tabs.ApplicationTabs
|
|
||||||
template_name = 'catalog/app_details.html'
|
|
||||||
page_title = '{{ app.name }}'
|
|
||||||
|
|
||||||
app = None
|
|
||||||
|
|
||||||
def get_data(self, **kwargs):
|
|
||||||
LOG.debug(('AppDetailsView get_data: {0}'.format(kwargs)))
|
|
||||||
app_id = kwargs.get('application_id')
|
|
||||||
self.app = api.muranoclient(self.request).packages.get(app_id)
|
|
||||||
return self.app
|
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
|
||||||
context = super(AppDetailsView, self).get_context_data(**kwargs)
|
|
||||||
LOG.debug('AppDetailsView get_context called with kwargs: {0}'.
|
|
||||||
format(kwargs))
|
|
||||||
context['app'] = self.app
|
|
||||||
|
|
||||||
context.update(get_environments_context(self.request))
|
|
||||||
|
|
||||||
return context
|
|
||||||
|
|
||||||
def get_tabs(self, request, *args, **kwargs):
|
|
||||||
app = self.get_data(**kwargs)
|
|
||||||
return self.tab_group_class(request, application=app, **kwargs)
|
|
|
@ -1,35 +0,0 @@
|
||||||
# Copyright (c) 2015 Mirantis, Inc.
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
||||||
# not use this file except in compliance with the License. You may obtain
|
|
||||||
# a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
||||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
||||||
# License for the specific language governing permissions and limitations
|
|
||||||
# under the License.
|
|
||||||
|
|
||||||
from django import forms
|
|
||||||
from django.utils.translation import ugettext_lazy as _
|
|
||||||
from horizon import forms as horizon_forms
|
|
||||||
from horizon import messages
|
|
||||||
|
|
||||||
from muranodashboard import api
|
|
||||||
|
|
||||||
|
|
||||||
class AddCategoryForm(horizon_forms.SelfHandlingForm):
|
|
||||||
|
|
||||||
name = forms.CharField(label=_('Category Name'),
|
|
||||||
max_length=80,
|
|
||||||
help_text=_('80 characters max.'))
|
|
||||||
|
|
||||||
def handle(self, request, data):
|
|
||||||
if data:
|
|
||||||
with api.handled_exceptions(self.request):
|
|
||||||
category = api.muranoclient(self.request).categories.add(data)
|
|
||||||
messages.success(request, _('Category {0} created.')
|
|
||||||
.format(data['name']))
|
|
||||||
return category
|
|
|
@ -1,22 +0,0 @@
|
||||||
# Copyright (c) 2015 Mirantis, Inc.
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
||||||
# not use this file except in compliance with the License. You may obtain
|
|
||||||
# a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
||||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
||||||
# License for the specific language governing permissions and limitations
|
|
||||||
# under the License.
|
|
||||||
|
|
||||||
from django.utils.translation import ugettext_lazy as _
|
|
||||||
import horizon
|
|
||||||
|
|
||||||
|
|
||||||
class Categories(horizon.Panel):
|
|
||||||
name = _("Categories")
|
|
||||||
slug = 'categories'
|
|
||||||
policy_rules = (("murano", "get_category"),)
|
|
|
@ -1,90 +0,0 @@
|
||||||
# Copyright (c) 2015 Mirantis, Inc.
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
||||||
# not use this file except in compliance with the License. You may obtain
|
|
||||||
# a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
||||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
||||||
# License for the specific language governing permissions and limitations
|
|
||||||
# under the License.
|
|
||||||
|
|
||||||
from django.conf import settings
|
|
||||||
from django.core.urlresolvers import reverse
|
|
||||||
from django.utils.translation import ugettext_lazy as _
|
|
||||||
from django.utils.translation import ungettext_lazy
|
|
||||||
from horizon import exceptions
|
|
||||||
from horizon import tables
|
|
||||||
from muranoclient.common import exceptions as exc
|
|
||||||
from openstack_dashboard import policy
|
|
||||||
from oslo_log import log as logging
|
|
||||||
|
|
||||||
from muranodashboard import api
|
|
||||||
from muranodashboard.common import utils as md_utils
|
|
||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class AddCategory(tables.LinkAction):
|
|
||||||
name = "add_category"
|
|
||||||
verbose_name = _("Add Category")
|
|
||||||
url = "horizon:app-catalog:categories:add"
|
|
||||||
classes = ("ajax-modal",)
|
|
||||||
icon = "plus"
|
|
||||||
policy_rules = (("murano", "add_category"),)
|
|
||||||
|
|
||||||
|
|
||||||
class DeleteCategory(policy.PolicyTargetMixin, tables.DeleteAction):
|
|
||||||
policy_rules = (("murano", "delete_category"),)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def action_present(count):
|
|
||||||
return ungettext_lazy(
|
|
||||||
u"Delete Category",
|
|
||||||
u"Delete Categories",
|
|
||||||
count
|
|
||||||
)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def action_past(count):
|
|
||||||
return ungettext_lazy(
|
|
||||||
u"Deleted Category",
|
|
||||||
u"Deleted Categories",
|
|
||||||
count
|
|
||||||
)
|
|
||||||
|
|
||||||
def allowed(self, request, category=None):
|
|
||||||
use_artifacts = getattr(settings, 'MURANO_USE_GLARE', False)
|
|
||||||
if use_artifacts:
|
|
||||||
return category is not None
|
|
||||||
if category is not None:
|
|
||||||
if not category.package_count:
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
def delete(self, request, obj_id):
|
|
||||||
try:
|
|
||||||
api.muranoclient(request).categories.delete(obj_id)
|
|
||||||
except exc.HTTPException:
|
|
||||||
msg = _('Unable to delete category')
|
|
||||||
LOG.exception(msg)
|
|
||||||
url = reverse('horizon:app-catalog:categories:index')
|
|
||||||
exceptions.handle(request, msg, redirect=url)
|
|
||||||
|
|
||||||
|
|
||||||
class CategoriesTable(tables.DataTable):
|
|
||||||
name = md_utils.Column('name', verbose_name=_('Category Name'))
|
|
||||||
use_artifacts = getattr(settings, 'MURANO_USE_GLARE', False)
|
|
||||||
if not use_artifacts:
|
|
||||||
package_count = tables.Column('package_count',
|
|
||||||
verbose_name=_('Package Count'))
|
|
||||||
|
|
||||||
class Meta(object):
|
|
||||||
name = 'categories'
|
|
||||||
verbose_name = _('Application Categories')
|
|
||||||
table_actions = (AddCategory,)
|
|
||||||
row_actions = (DeleteCategory,)
|
|
||||||
multi_select = False
|
|
|
@ -1,24 +0,0 @@
|
||||||
# Copyright (c) 2015 Mirantis, Inc.
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
||||||
# not use this file except in compliance with the License. You may obtain
|
|
||||||
# a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
||||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
||||||
# License for the specific language governing permissions and limitations
|
|
||||||
# under the License.
|
|
||||||
|
|
||||||
from django.conf import urls
|
|
||||||
|
|
||||||
from muranodashboard.categories import views
|
|
||||||
|
|
||||||
|
|
||||||
urlpatterns = [
|
|
||||||
urls.url(r'^$', views.CategoriesView.as_view(), name='index'),
|
|
||||||
urls.url(r'^add$', views.AddCategoryView.as_view(), name='add'),
|
|
||||||
urls.url(r'^delete$', views.CategoriesView.as_view(), name='delete'),
|
|
||||||
]
|
|
|
@ -1,94 +0,0 @@
|
||||||
# Copyright (c) 2015 Mirantis, Inc.
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
||||||
# not use this file except in compliance with the License. You may obtain
|
|
||||||
# a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
||||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
||||||
# License for the specific language governing permissions and limitations
|
|
||||||
# under the License.
|
|
||||||
|
|
||||||
import itertools
|
|
||||||
|
|
||||||
from django.core.urlresolvers import reverse_lazy
|
|
||||||
from django.utils.translation import ugettext_lazy as _
|
|
||||||
from horizon.forms import views
|
|
||||||
from horizon import tables as horizon_tables
|
|
||||||
from horizon.utils import functions as utils
|
|
||||||
|
|
||||||
from muranodashboard import api
|
|
||||||
from muranodashboard.categories import forms
|
|
||||||
from muranodashboard.categories import tables
|
|
||||||
|
|
||||||
|
|
||||||
class CategoriesView(horizon_tables.DataTableView):
|
|
||||||
table_class = tables.CategoriesTable
|
|
||||||
template_name = 'categories/index.html'
|
|
||||||
page_title = _("Application Categories")
|
|
||||||
|
|
||||||
def has_prev_data(self, table):
|
|
||||||
return self._prev
|
|
||||||
|
|
||||||
def has_more_data(self, table):
|
|
||||||
return self._more
|
|
||||||
|
|
||||||
def get_data(self):
|
|
||||||
prev_marker = self.request.GET.get(
|
|
||||||
tables.CategoriesTable._meta.prev_pagination_param, None)
|
|
||||||
|
|
||||||
if prev_marker is not None:
|
|
||||||
sort_dir = 'asc'
|
|
||||||
marker = prev_marker
|
|
||||||
else:
|
|
||||||
sort_dir = 'desc'
|
|
||||||
marker = self.request.GET.get(
|
|
||||||
tables.CategoriesTable._meta.pagination_param, None)
|
|
||||||
|
|
||||||
page_size = utils.get_page_size(self.request)
|
|
||||||
|
|
||||||
request_size = page_size + 1
|
|
||||||
kwargs = {'filters': {}}
|
|
||||||
if marker:
|
|
||||||
kwargs['marker'] = marker
|
|
||||||
kwargs['sort_dir'] = sort_dir
|
|
||||||
|
|
||||||
categories = []
|
|
||||||
self._prev = False
|
|
||||||
self._more = False
|
|
||||||
with api.handled_exceptions(self.request):
|
|
||||||
categories_iter = api.muranoclient(self.request).categories.list(
|
|
||||||
limit=request_size, **kwargs)
|
|
||||||
|
|
||||||
categories = list(itertools.islice(categories_iter, request_size))
|
|
||||||
# first and middle page condition
|
|
||||||
if len(categories) > page_size:
|
|
||||||
categories.pop(-1)
|
|
||||||
self._more = True
|
|
||||||
# middle page condition
|
|
||||||
if marker is not None:
|
|
||||||
self._prev = True
|
|
||||||
# first page condition when reached via prev back
|
|
||||||
elif sort_dir == 'asc' and marker is not None:
|
|
||||||
self._more = True
|
|
||||||
# last page condition
|
|
||||||
elif marker is not None:
|
|
||||||
self._prev = True
|
|
||||||
if prev_marker is not None:
|
|
||||||
categories.reverse()
|
|
||||||
return categories
|
|
||||||
|
|
||||||
|
|
||||||
class AddCategoryView(views.ModalFormView):
|
|
||||||
form_class = forms.AddCategoryForm
|
|
||||||
form_id = 'add_category_form'
|
|
||||||
modal_header = _('Add Category')
|
|
||||||
template_name = 'categories/add.html'
|
|
||||||
context_object_name = 'category'
|
|
||||||
page_title = _('Add Application Category')
|
|
||||||
success_url = reverse_lazy('horizon:app-catalog:categories:index')
|
|
||||||
submit_label = _('Add')
|
|
||||||
submit_url = reverse_lazy('horizon:app-catalog:categories:add')
|
|
|
@ -1,85 +0,0 @@
|
||||||
# Copyright (c) 2014 Mirantis, Inc.
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
||||||
# not use this file except in compliance with the License. You may obtain
|
|
||||||
# a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
||||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
||||||
# License for the specific language governing permissions and limitations
|
|
||||||
# under the License.
|
|
||||||
|
|
||||||
import functools
|
|
||||||
import os
|
|
||||||
|
|
||||||
from oslo_log import log as logging
|
|
||||||
|
|
||||||
from muranodashboard.common import utils
|
|
||||||
from muranodashboard.environments import consts
|
|
||||||
|
|
||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
|
||||||
OBJS_PATH = os.path.join(consts.CACHE_DIR, 'apps')
|
|
||||||
|
|
||||||
if not os.path.exists(OBJS_PATH):
|
|
||||||
os.makedirs(OBJS_PATH)
|
|
||||||
LOG.info('Creating apps cache directory located at {dir}'.
|
|
||||||
format(dir=OBJS_PATH))
|
|
||||||
|
|
||||||
LOG.info('Using apps cache directory located at {dir}'.
|
|
||||||
format(dir=OBJS_PATH))
|
|
||||||
|
|
||||||
|
|
||||||
def _get_entry_path(app_id):
|
|
||||||
head, tail = app_id[:2], app_id[2:]
|
|
||||||
head = os.path.join(OBJS_PATH, head)
|
|
||||||
if not os.path.exists(head):
|
|
||||||
os.mkdir(head)
|
|
||||||
tail = os.path.join(head, tail)
|
|
||||||
if not os.path.exists(tail):
|
|
||||||
os.mkdir(tail)
|
|
||||||
return tail
|
|
||||||
|
|
||||||
|
|
||||||
def _load_from_file(file_name):
|
|
||||||
if os.path.isfile(file_name) and os.path.getsize(file_name) > 0:
|
|
||||||
with open(file_name, 'rb') as f:
|
|
||||||
p = utils.CustomUnpickler(f)
|
|
||||||
return p.load()
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def _save_to_file(file_name, content):
|
|
||||||
dir_path = os.path.dirname(file_name)
|
|
||||||
if not os.path.exists(dir_path):
|
|
||||||
os.makedirs(dir_path)
|
|
||||||
with open(file_name, 'wb') as f:
|
|
||||||
p = utils.CustomPickler(f)
|
|
||||||
p.dump(content)
|
|
||||||
|
|
||||||
|
|
||||||
def with_cache(*dst_parts):
|
|
||||||
def _decorator(func):
|
|
||||||
@functools.wraps(func)
|
|
||||||
def __inner(request, app_id):
|
|
||||||
path = os.path.join(_get_entry_path(app_id), *dst_parts)
|
|
||||||
# Remove file extensions since file content is pickled and
|
|
||||||
# could not be open as usual files
|
|
||||||
path = os.path.splitext(path)[0] + '-pickled'
|
|
||||||
content = _load_from_file(path)
|
|
||||||
if content is None:
|
|
||||||
content = func(request, app_id)
|
|
||||||
if content:
|
|
||||||
LOG.debug('Caching value at {0}.'.format(path))
|
|
||||||
_save_to_file(path, content)
|
|
||||||
else:
|
|
||||||
LOG.debug('Using cached value from {0}.'.format(path))
|
|
||||||
|
|
||||||
return content
|
|
||||||
|
|
||||||
return __inner
|
|
||||||
|
|
||||||
return _decorator
|
|
|
@ -1,72 +0,0 @@
|
||||||
# Copyright (c) 2015 Mirantis, Inc.
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
||||||
# not use this file except in compliance with the License. You may obtain
|
|
||||||
# a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
||||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
||||||
# License for the specific language governing permissions and limitations
|
|
||||||
# under the License.
|
|
||||||
|
|
||||||
from muranodashboard.common import widgets
|
|
||||||
|
|
||||||
from django.core.exceptions import ValidationError
|
|
||||||
from django.core import validators
|
|
||||||
from django import forms
|
|
||||||
from django.utils.translation import ugettext_lazy as _
|
|
||||||
|
|
||||||
|
|
||||||
class TriStateMultipleChoiceField(forms.ChoiceField):
|
|
||||||
"""A multiple choice checkbox field where checkboxes has three states.
|
|
||||||
|
|
||||||
States are:
|
|
||||||
|
|
||||||
- Checked
|
|
||||||
- Unchecked
|
|
||||||
- Indeterminate
|
|
||||||
|
|
||||||
It takes a ``dict`` instance as a value,
|
|
||||||
where keys are internal values from `choices`
|
|
||||||
and values are ones from following (in order respectively to states):
|
|
||||||
|
|
||||||
- True
|
|
||||||
- False
|
|
||||||
- None
|
|
||||||
"""
|
|
||||||
widget = widgets.TriStateCheckboxSelectMultiple
|
|
||||||
default_error_messages = {
|
|
||||||
'invalid_choice': _('Select a valid choice. %(value)s is not one '
|
|
||||||
'of the available choices.'),
|
|
||||||
'invalid_value': _('Enter a dict with choices and values. '
|
|
||||||
'Got %(value)s.'),
|
|
||||||
}
|
|
||||||
|
|
||||||
def to_python(self, value):
|
|
||||||
"""Checks if value, that comes from widget, is a dict."""
|
|
||||||
if value in validators.EMPTY_VALUES:
|
|
||||||
return {}
|
|
||||||
elif not isinstance(value, dict):
|
|
||||||
raise ValidationError(self.error_messages['invalid_value'],
|
|
||||||
code='invalid_value')
|
|
||||||
return value
|
|
||||||
|
|
||||||
def validate(self, value):
|
|
||||||
"""Ensures that value has only allowed values."""
|
|
||||||
if not set(value.keys()) <= {k for k, _ in self.choices}:
|
|
||||||
raise ValidationError(
|
|
||||||
self.error_messages['invalid_choice'],
|
|
||||||
code='invalid_choice',
|
|
||||||
params={'value': value},
|
|
||||||
)
|
|
||||||
elif not (set(value.values()) <=
|
|
||||||
set(widgets.TriStateCheckboxSelectMultiple
|
|
||||||
.VALUES_MAP.values())):
|
|
||||||
raise ValidationError(
|
|
||||||
self.error_messages['invalid_value'],
|
|
||||||
code='invalid_value',
|
|
||||||
params={'value': value},
|
|
||||||
)
|
|
|
@ -1,129 +0,0 @@
|
||||||
# Copyright (c) 2015 Mirantis, Inc.
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
||||||
# not use this file except in compliance with the License. You may obtain
|
|
||||||
# a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
||||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
||||||
# License for the specific language governing permissions and limitations
|
|
||||||
# under the License.
|
|
||||||
|
|
||||||
|
|
||||||
import re
|
|
||||||
import uuid
|
|
||||||
|
|
||||||
from django.conf import settings
|
|
||||||
from django.utils.translation import ugettext_lazy as _
|
|
||||||
from horizon import exceptions
|
|
||||||
from neutronclient.common import exceptions as exc
|
|
||||||
from openstack_dashboard.api import keystone
|
|
||||||
from openstack_dashboard.api import neutron
|
|
||||||
from oslo_log import log as logging
|
|
||||||
|
|
||||||
from muranodashboard.environments import api as env_api
|
|
||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
NEUTRON_NET_HELP = _("The VMs of the applications in this environment will "
|
|
||||||
"join this net by default, unless configured "
|
|
||||||
"individually. Choosing 'Create New' will generate a new "
|
|
||||||
"Network with a Subnet having an IP range allocated "
|
|
||||||
"among the ones available for the default Murano Router "
|
|
||||||
"of this project")
|
|
||||||
NN_HELP = _("OpenStack Networking (Neutron) is not available in current "
|
|
||||||
"environment. Custom Network Settings cannot be applied")
|
|
||||||
|
|
||||||
|
|
||||||
def get_project_assigned_network(request):
|
|
||||||
tenant_id = request.user.tenant_id
|
|
||||||
|
|
||||||
tenant = keystone.tenant_get(request, tenant_id)
|
|
||||||
network_name = getattr(settings, 'FIXED_MURANO_NETWORK', 'murano_network')
|
|
||||||
tenant_network_id = getattr(tenant, network_name, None)
|
|
||||||
if not tenant_network_id:
|
|
||||||
LOG.warning(("murano_network property is not "
|
|
||||||
"defined for project '%s'") % tenant_id)
|
|
||||||
return []
|
|
||||||
|
|
||||||
try:
|
|
||||||
tenant_network = neutron.network_get(request, tenant_network_id)
|
|
||||||
return [((tenant_network.id, None), tenant_network.name_or_id)]
|
|
||||||
except exc.NeutronClientException:
|
|
||||||
return []
|
|
||||||
|
|
||||||
|
|
||||||
def get_available_networks(request, include_subnets=True,
|
|
||||||
filter=None, murano_networks=None):
|
|
||||||
if murano_networks:
|
|
||||||
env_names = [e.name for e in env_api.environments_list(request)]
|
|
||||||
|
|
||||||
def get_net_env(name):
|
|
||||||
for env_name in env_names:
|
|
||||||
if name.startswith(env_name + '-network'):
|
|
||||||
return env_name
|
|
||||||
|
|
||||||
network_choices = []
|
|
||||||
tenant_id = request.user.tenant_id
|
|
||||||
try:
|
|
||||||
networks = neutron.network_list_for_tenant(request,
|
|
||||||
tenant_id=tenant_id)
|
|
||||||
except exceptions.ServiceCatalogException:
|
|
||||||
LOG.warning("Neutron not found. Assuming Nova Network usage")
|
|
||||||
return []
|
|
||||||
|
|
||||||
# Remove external networks
|
|
||||||
networks = [network for network in networks
|
|
||||||
if network.router__external is False]
|
|
||||||
if filter:
|
|
||||||
networks = [network for network in networks
|
|
||||||
if re.match(filter, network.name) is not None]
|
|
||||||
|
|
||||||
for net in networks:
|
|
||||||
env = None
|
|
||||||
netname = None
|
|
||||||
|
|
||||||
if murano_networks and len(net.subnets) == 1:
|
|
||||||
env = get_net_env(net.name)
|
|
||||||
if env:
|
|
||||||
if murano_networks == 'exclude':
|
|
||||||
continue
|
|
||||||
else:
|
|
||||||
netname = _("Network of '%s'") % env
|
|
||||||
|
|
||||||
if include_subnets:
|
|
||||||
for subnet in net.subnets:
|
|
||||||
if not netname:
|
|
||||||
full_name = (
|
|
||||||
"%(net)s: %(cidr)s %(subnet)s" %
|
|
||||||
dict(net=net.name_or_id,
|
|
||||||
cidr=subnet.cidr,
|
|
||||||
subnet=subnet.name_or_id))
|
|
||||||
|
|
||||||
network_choices.append(
|
|
||||||
((net.id, subnet.id), netname or full_name))
|
|
||||||
|
|
||||||
else:
|
|
||||||
netname = netname or net.name_or_id
|
|
||||||
network_choices.append(((net.id, None), netname))
|
|
||||||
return network_choices
|
|
||||||
|
|
||||||
|
|
||||||
def generate_join_existing_net(net_config):
|
|
||||||
res = {
|
|
||||||
"defaultNetworks": {
|
|
||||||
'environment': {
|
|
||||||
'?': {
|
|
||||||
'id': uuid.uuid4().hex,
|
|
||||||
'type': 'io.murano.resources.ExistingNeutronNetwork'
|
|
||||||
},
|
|
||||||
'internalNetworkName': net_config[0],
|
|
||||||
'internalSubnetworkName': net_config[1]
|
|
||||||
},
|
|
||||||
'flat': None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return res
|
|
|
@ -1,133 +0,0 @@
|
||||||
# Copyright (c) 2014 Mirantis, Inc.
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
||||||
# not use this file except in compliance with the License. You may obtain
|
|
||||||
# a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
||||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
||||||
# License for the specific language governing permissions and limitations
|
|
||||||
# under the License.
|
|
||||||
|
|
||||||
try:
|
|
||||||
import cPickle as pickle
|
|
||||||
except ImportError:
|
|
||||||
import pickle
|
|
||||||
import bs4
|
|
||||||
import string
|
|
||||||
|
|
||||||
import iso8601
|
|
||||||
from muranodashboard.dynamic_ui import yaql_expression
|
|
||||||
import pytz
|
|
||||||
import six
|
|
||||||
import yaql
|
|
||||||
|
|
||||||
from horizon.utils import functions as utils
|
|
||||||
|
|
||||||
# WrappingColumn is only available in N-horizon
|
|
||||||
# This make murano-dashboard compatible with Mitaka-horizon
|
|
||||||
try:
|
|
||||||
from horizon.tables import WrappingColumn as Column
|
|
||||||
except ImportError:
|
|
||||||
from horizon.tables import Column as Column # noqa
|
|
||||||
|
|
||||||
|
|
||||||
def parse_api_error(api_error_html):
|
|
||||||
error_html = bs4.BeautifulSoup(api_error_html, "html.parser")
|
|
||||||
body = error_html.find('body')
|
|
||||||
if (not body or not body.text):
|
|
||||||
return None
|
|
||||||
h1 = body.find('h1')
|
|
||||||
if h1:
|
|
||||||
h1.replace_with('')
|
|
||||||
return body.text.strip()
|
|
||||||
|
|
||||||
|
|
||||||
def ensure_python_obj(obj):
|
|
||||||
mappings = {'True': True, 'False': False, 'None': None}
|
|
||||||
return mappings.get(obj, obj)
|
|
||||||
|
|
||||||
|
|
||||||
def adjust_datestr(request, datestr):
|
|
||||||
tz = pytz.timezone(utils.get_timezone(request))
|
|
||||||
dt = iso8601.parse_date(datestr).astimezone(tz)
|
|
||||||
return dt.strftime('%Y-%m-%d %H:%M:%S')
|
|
||||||
|
|
||||||
|
|
||||||
class Bunch(object):
|
|
||||||
"""Bunch dict/object-like container.
|
|
||||||
|
|
||||||
Bunch container provides both dictionary-like and
|
|
||||||
object-like attribute access.
|
|
||||||
"""
|
|
||||||
def __init__(self, **kwargs):
|
|
||||||
for key, value in six.iteritems(kwargs):
|
|
||||||
setattr(self, key, value)
|
|
||||||
|
|
||||||
def __getitem__(self, item):
|
|
||||||
return getattr(self, item)
|
|
||||||
|
|
||||||
def __setitem__(self, key, value):
|
|
||||||
setattr(self, key, value)
|
|
||||||
|
|
||||||
def __delitem__(self, key):
|
|
||||||
delattr(self, key)
|
|
||||||
|
|
||||||
def __contains__(self, item):
|
|
||||||
return hasattr(self, item)
|
|
||||||
|
|
||||||
def __iter__(self):
|
|
||||||
return iter(six.itervalues(self.__dict__))
|
|
||||||
|
|
||||||
|
|
||||||
class BlankFormatter(string.Formatter):
|
|
||||||
"""Utility class aimed to provide empty string for non-existent keys."""
|
|
||||||
def __init__(self, default=''):
|
|
||||||
self.default = default
|
|
||||||
|
|
||||||
def get_value(self, key, args, kwargs):
|
|
||||||
if isinstance(key, str):
|
|
||||||
return kwargs.get(key, self.default)
|
|
||||||
else:
|
|
||||||
return string.Formatter.get_value(self, key, args, kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
class CustomPickler(object):
|
|
||||||
"""Custom pickle object to perform correct serializing.
|
|
||||||
|
|
||||||
YAQL Engine is not serializable and it's not necessary to store
|
|
||||||
it in cache. This class replace YAQL Engine instance to string.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, file, protocol=0):
|
|
||||||
pickler = pickle.Pickler(file, protocol)
|
|
||||||
pickler.persistent_id = self.persistent_id
|
|
||||||
self.dump = pickler.dump
|
|
||||||
self.clear_memo = pickler.clear_memo
|
|
||||||
|
|
||||||
def persistent_id(self, obj):
|
|
||||||
if isinstance(obj, yaql.factory.YaqlEngine):
|
|
||||||
return "filtered:YaqlEngine"
|
|
||||||
else:
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
class CustomUnpickler(object):
|
|
||||||
"""Custom pickle object to perform correct deserializing.
|
|
||||||
|
|
||||||
This class replace filtered YAQL Engine to the real instance.
|
|
||||||
"""
|
|
||||||
def __init__(self, file):
|
|
||||||
unpickler = pickle.Unpickler(file)
|
|
||||||
unpickler.persistent_load = self.persistent_load
|
|
||||||
self.load = unpickler.load
|
|
||||||
self.noload = getattr(unpickler, 'noload', None)
|
|
||||||
|
|
||||||
def persistent_load(self, obj_id):
|
|
||||||
if obj_id == 'filtered:YaqlEngine':
|
|
||||||
return yaql_expression.YAQL
|
|
||||||
else:
|
|
||||||
raise pickle.UnpicklingError('Invalid persistent id')
|
|
|
@ -1,96 +0,0 @@
|
||||||
# Copyright (c) 2015 Mirantis, Inc.
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
||||||
# not use this file except in compliance with the License. You may obtain
|
|
||||||
# a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
||||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
||||||
# License for the specific language governing permissions and limitations
|
|
||||||
# under the License.
|
|
||||||
|
|
||||||
import itertools as it
|
|
||||||
|
|
||||||
import floppyforms as floppy
|
|
||||||
|
|
||||||
|
|
||||||
class TriStateCheckboxSelectMultiple(floppy.widgets.Input):
|
|
||||||
"""Renders tri-state multi-selectable checkbox.
|
|
||||||
|
|
||||||
.. note:: Subclassed from ``CheckboxSelectMultiple`` and not from
|
|
||||||
``SelectMultiple`` only to make
|
|
||||||
``horizon.templatetags.form_helpers.is_checkbox`` able to recognize
|
|
||||||
this widget.
|
|
||||||
|
|
||||||
Otherwise template ``horizon/common/_form_field.html`` would render
|
|
||||||
this widget slightly incorrectly.
|
|
||||||
"""
|
|
||||||
template_name = 'common/tri_state_checkbox/base.html'
|
|
||||||
|
|
||||||
VALUES_MAP = {
|
|
||||||
'True': True,
|
|
||||||
'False': False,
|
|
||||||
'None': None
|
|
||||||
}
|
|
||||||
|
|
||||||
def get_context(self, name, value, attrs=None, choices=()):
|
|
||||||
"""Renders html and JavaScript.
|
|
||||||
|
|
||||||
:param value: Dictionary of form
|
|
||||||
Choice => Value (Checked|Uncheckec|Indeterminate)
|
|
||||||
:type value: dict
|
|
||||||
"""
|
|
||||||
context = super(TriStateCheckboxSelectMultiple, self).get_context(
|
|
||||||
name, value, attrs
|
|
||||||
)
|
|
||||||
|
|
||||||
choices = dict(it.chain(self.choices, choices))
|
|
||||||
if value is None:
|
|
||||||
value = dict.fromkeys(choices, False)
|
|
||||||
else:
|
|
||||||
value = dict(dict.fromkeys(choices, False).items() +
|
|
||||||
value.items())
|
|
||||||
|
|
||||||
context['values'] = [
|
|
||||||
(choice, label, value[choice])
|
|
||||||
for choice, label in choices.iteritems()
|
|
||||||
]
|
|
||||||
|
|
||||||
return context
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def parse_value(cls, value):
|
|
||||||
"""Converts encoded string with value to Python values."""
|
|
||||||
choice, value = value.split('=')
|
|
||||||
value = cls.VALUES_MAP[value]
|
|
||||||
|
|
||||||
return choice, value
|
|
||||||
|
|
||||||
def value_from_datadict(self, data, files, name):
|
|
||||||
"""Expects values in ``"key=False/True/None"`` form."""
|
|
||||||
try:
|
|
||||||
values = data.getlist(name)
|
|
||||||
except AttributeError:
|
|
||||||
if name in data:
|
|
||||||
values = [data[name]]
|
|
||||||
else:
|
|
||||||
values = []
|
|
||||||
|
|
||||||
return dict(map(self.parse_value, values))
|
|
||||||
|
|
||||||
|
|
||||||
class ExtraContextWidgetMixin(object):
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
super(ExtraContextWidgetMixin, self).__init__(*args, **kwargs)
|
|
||||||
|
|
||||||
self.extra_context = kwargs.pop('extra_context', {})
|
|
||||||
|
|
||||||
def get_context(self, *args, **kwargs):
|
|
||||||
context = super(ExtraContextWidgetMixin, self).get_context(
|
|
||||||
*args, **kwargs
|
|
||||||
)
|
|
||||||
context.update(self.extra_context)
|
|
||||||
return context
|
|
|
@ -1,39 +0,0 @@
|
||||||
{
|
|
||||||
"context_is_admin": "role:admin",
|
|
||||||
"admin_api": "is_admin:True",
|
|
||||||
"default": "",
|
|
||||||
|
|
||||||
"get_package": "rule:default",
|
|
||||||
"upload_package": "rule:default",
|
|
||||||
"modify_package": "rule:default",
|
|
||||||
"publicize_package": "rule:admin_api",
|
|
||||||
"manage_public_package": "rule:default",
|
|
||||||
"delete_package": "rule:default",
|
|
||||||
"download_package": "rule:default",
|
|
||||||
|
|
||||||
"get_category": "rule:default",
|
|
||||||
"delete_category": "rule:admin_api",
|
|
||||||
"add_category": "rule:admin_api",
|
|
||||||
|
|
||||||
"list_deployments": "rule:default",
|
|
||||||
"statuses_deployments": "rule:default",
|
|
||||||
|
|
||||||
"list_environments": "rule:default",
|
|
||||||
"list_environments_all_tenants": "rule:admin_api",
|
|
||||||
"show_environment": "rule:default",
|
|
||||||
"update_environment": "rule:default",
|
|
||||||
"create_environment": "rule:default",
|
|
||||||
"delete_environment": "rule:default",
|
|
||||||
|
|
||||||
"list_env_templates": "rule:default",
|
|
||||||
"create_env_template": "rule:default",
|
|
||||||
"show_env_template": "rule:default",
|
|
||||||
"update_env_template": "rule:default",
|
|
||||||
"delete_env_template": "rule:default",
|
|
||||||
|
|
||||||
"execute_action": "rule:default",
|
|
||||||
|
|
||||||
"mark_image": "rule:admin_api",
|
|
||||||
"remove_image_metadata": "rule:admin_api"
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,32 +0,0 @@
|
||||||
# Copyright (c) 2013 Mirantis, Inc.
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
||||||
# not use this file except in compliance with the License. You may obtain
|
|
||||||
# a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
||||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
||||||
# License for the specific language governing permissions and limitations
|
|
||||||
# under the License.
|
|
||||||
|
|
||||||
from django.conf import settings
|
|
||||||
from django.utils.translation import ugettext_lazy as _
|
|
||||||
import horizon
|
|
||||||
|
|
||||||
# Load the api rest services into Horizon
|
|
||||||
import muranodashboard.api.rest # noqa
|
|
||||||
|
|
||||||
|
|
||||||
class AppCatalog(horizon.Dashboard):
|
|
||||||
name = getattr(settings, 'MURANO_DASHBOARD_NAME', _("App Catalog"))
|
|
||||||
slug = "app-catalog"
|
|
||||||
default_panel = "environments"
|
|
||||||
supports_tenants = True
|
|
||||||
|
|
||||||
try:
|
|
||||||
horizon.base.Horizon.registered('app-catalog')
|
|
||||||
except horizon.base.NotRegistered:
|
|
||||||
horizon.register(AppCatalog)
|
|
|
@ -1,752 +0,0 @@
|
||||||
# Copyright (c) 2013 Mirantis, Inc.
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
||||||
# not use this file except in compliance with the License. You may obtain
|
|
||||||
# a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
||||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
||||||
# License for the specific language governing permissions and limitations
|
|
||||||
# under the License.
|
|
||||||
|
|
||||||
import ast
|
|
||||||
import copy
|
|
||||||
import json
|
|
||||||
import re
|
|
||||||
|
|
||||||
from django.core.urlresolvers import reverse
|
|
||||||
from django.core import validators as django_validator
|
|
||||||
from django import forms
|
|
||||||
from django.forms import widgets
|
|
||||||
from django.template import defaultfilters
|
|
||||||
from django.utils.encoding import force_text
|
|
||||||
from django.utils import html
|
|
||||||
from django.utils.translation import ugettext_lazy as _
|
|
||||||
from horizon import exceptions
|
|
||||||
from horizon import forms as hz_forms
|
|
||||||
from horizon import messages
|
|
||||||
from openstack_dashboard.api import cinder
|
|
||||||
from openstack_dashboard.api import glance
|
|
||||||
from openstack_dashboard.api import neutron
|
|
||||||
from openstack_dashboard.api import nova
|
|
||||||
from oslo_log import log as logging
|
|
||||||
from oslo_log import versionutils
|
|
||||||
import six
|
|
||||||
from yaql import legacy
|
|
||||||
|
|
||||||
from muranodashboard.api import packages as pkg_api
|
|
||||||
from muranodashboard.common import net
|
|
||||||
from muranodashboard.environments import api as env_api
|
|
||||||
|
|
||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
def with_request(func):
|
|
||||||
"""Injects request into func
|
|
||||||
|
|
||||||
The decorator is meant to be used together with `UpdatableFieldsForm':
|
|
||||||
apply it to the `update' method of fields inside that form.
|
|
||||||
"""
|
|
||||||
def update(self, initial, request=None, **kwargs):
|
|
||||||
initial_request = initial.get('request')
|
|
||||||
for key, value in six.iteritems(initial):
|
|
||||||
if key != 'request' and key not in kwargs:
|
|
||||||
kwargs[key] = value
|
|
||||||
|
|
||||||
if initial_request:
|
|
||||||
LOG.debug("Using 'request' value from initial dictionary")
|
|
||||||
func(self, initial_request, **kwargs)
|
|
||||||
elif request:
|
|
||||||
LOG.debug("Using direct 'request' value")
|
|
||||||
func(self, request, **kwargs)
|
|
||||||
else:
|
|
||||||
LOG.error("No 'request' value passed neither via initial "
|
|
||||||
"dictionary, nor directly")
|
|
||||||
raise forms.ValidationError("Can't get a request information")
|
|
||||||
return update
|
|
||||||
|
|
||||||
|
|
||||||
def make_yaql_validator(validator_property):
|
|
||||||
"""Field-level validator uses field's value as its '$' root object."""
|
|
||||||
expr = validator_property['expr'].spec
|
|
||||||
message = validator_property.get('message', '')
|
|
||||||
|
|
||||||
def validator_func(value):
|
|
||||||
context = legacy.create_context()
|
|
||||||
context['$'] = value
|
|
||||||
if not expr.evaluate(context=context):
|
|
||||||
raise forms.ValidationError(message)
|
|
||||||
|
|
||||||
return validator_func
|
|
||||||
|
|
||||||
|
|
||||||
def get_regex_validator(expr):
|
|
||||||
try:
|
|
||||||
validator = expr['validators'][0]
|
|
||||||
if isinstance(validator, django_validator.RegexValidator):
|
|
||||||
return validator
|
|
||||||
except (TypeError, KeyError, IndexError):
|
|
||||||
pass
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
# This function is needed if we don't want to change existing services
|
|
||||||
# regexpValidators
|
|
||||||
def wrap_regex_validator(validator, message):
|
|
||||||
def _validator(value):
|
|
||||||
try:
|
|
||||||
validator(value)
|
|
||||||
except forms.ValidationError:
|
|
||||||
# provide our own message
|
|
||||||
raise forms.ValidationError(message)
|
|
||||||
return _validator
|
|
||||||
|
|
||||||
|
|
||||||
def get_murano_images(request):
|
|
||||||
images = []
|
|
||||||
try:
|
|
||||||
# https://bugs.launchpad.net/murano/+bug/1339261 - glance
|
|
||||||
# client version change alters the API. Other tuple values
|
|
||||||
# are _more and _prev (in recent glance client)
|
|
||||||
images = glance.image_list_detailed(request)[0]
|
|
||||||
except Exception:
|
|
||||||
LOG.error("Error to request image list from glance ")
|
|
||||||
exceptions.handle(request, _("Unable to retrieve public images."))
|
|
||||||
murano_images = []
|
|
||||||
# filter out the snapshot image type
|
|
||||||
images = filter(
|
|
||||||
lambda x: x.properties.get("image_type", '') != 'snapshot', images)
|
|
||||||
for image in images:
|
|
||||||
# Additional properties, whose value is always a string data type, are
|
|
||||||
# only included in the response if they have a value.
|
|
||||||
murano_property = getattr(image, 'murano_image_info', None)
|
|
||||||
if murano_property:
|
|
||||||
try:
|
|
||||||
murano_metadata = json.loads(murano_property)
|
|
||||||
except ValueError:
|
|
||||||
LOG.warning("JSON in image metadata is not valid. "
|
|
||||||
"Check it in glance.")
|
|
||||||
messages.error(request, _("Invalid murano image metadata"))
|
|
||||||
else:
|
|
||||||
image.murano_property = murano_metadata
|
|
||||||
murano_images.append(image)
|
|
||||||
return murano_images
|
|
||||||
|
|
||||||
|
|
||||||
class RawProperty(object):
|
|
||||||
def __init__(self, key, spec):
|
|
||||||
self.key = key
|
|
||||||
self.spec = spec
|
|
||||||
self.value = None
|
|
||||||
self.value_evaluated = False
|
|
||||||
|
|
||||||
def finalize(self, form_name, service, cls):
|
|
||||||
def _get(field):
|
|
||||||
if self.value_evaluated:
|
|
||||||
return self.value
|
|
||||||
return service.get_data(form_name, self.spec)
|
|
||||||
|
|
||||||
def _set(field, value):
|
|
||||||
self.value = value
|
|
||||||
self.value_evaluated = value is not None
|
|
||||||
if hasattr(cls, self.key):
|
|
||||||
getattr(cls, self.key).fset(field, value)
|
|
||||||
|
|
||||||
def _del(field):
|
|
||||||
_set(field, None)
|
|
||||||
return property(_get, _set, _del)
|
|
||||||
|
|
||||||
|
|
||||||
FIELD_ARGS_TO_ESCAPE = ['help_text', 'initial', 'description', 'label']
|
|
||||||
|
|
||||||
|
|
||||||
class CustomPropertiesField(forms.Field):
|
|
||||||
js_validation = False
|
|
||||||
|
|
||||||
def __init__(self, description=None, description_title=None,
|
|
||||||
*args, **kwargs):
|
|
||||||
self.description = description
|
|
||||||
self.description_title = (description_title or
|
|
||||||
force_text(kwargs.get('label', '')))
|
|
||||||
|
|
||||||
for arg in FIELD_ARGS_TO_ESCAPE:
|
|
||||||
if kwargs.get(arg):
|
|
||||||
kwargs[arg] = html.escape(force_text(kwargs[arg]))
|
|
||||||
|
|
||||||
validators = []
|
|
||||||
validators_js = []
|
|
||||||
for validator in kwargs.get('validators', []):
|
|
||||||
if hasattr(validator, '__call__'): # single regexpValidator
|
|
||||||
validators.append(validator)
|
|
||||||
if hasattr(validator, 'regex'):
|
|
||||||
regex_message = ''
|
|
||||||
error_messages = kwargs.get('error_messages', {})
|
|
||||||
if hasattr(validator, 'code') and \
|
|
||||||
validator.code in error_messages:
|
|
||||||
regex_message = force_text(
|
|
||||||
error_messages[validator.code]
|
|
||||||
)
|
|
||||||
validators_js. \
|
|
||||||
append({'regex': force_text(validator.regex.pattern),
|
|
||||||
'message': regex_message})
|
|
||||||
else: # mixed list of regexpValidator and YAQL validators
|
|
||||||
expr = validator.get('expr')
|
|
||||||
regex_validator = get_regex_validator(expr)
|
|
||||||
regex_message = validator.get('message', '')
|
|
||||||
if regex_validator:
|
|
||||||
validators.append(wrap_regex_validator(
|
|
||||||
regex_validator, regex_message))
|
|
||||||
elif isinstance(expr, RawProperty):
|
|
||||||
validators.append(validator)
|
|
||||||
if hasattr(regex_validator, 'regex'):
|
|
||||||
validators_js.\
|
|
||||||
append({'regex': regex_validator.regex.pattern,
|
|
||||||
'message': regex_message})
|
|
||||||
kwargs['validators'] = validators
|
|
||||||
if validators_js:
|
|
||||||
self.js_validation = json.dumps(validators_js)
|
|
||||||
|
|
||||||
super(CustomPropertiesField, self).__init__(*args, **kwargs)
|
|
||||||
|
|
||||||
def widget_attrs(self, widget):
|
|
||||||
attrs = super(CustomPropertiesField, self).widget_attrs(widget)
|
|
||||||
if self.js_validation:
|
|
||||||
attrs['data-validators'] = self.js_validation
|
|
||||||
return attrs
|
|
||||||
|
|
||||||
def clean(self, value):
|
|
||||||
"""Skip all validators if field is disabled."""
|
|
||||||
# form is assigned in ServiceConfigurationForm.finalize_fields()
|
|
||||||
form = self.form
|
|
||||||
# the only place to ensure that Service object has up-to-date
|
|
||||||
# cleaned_data
|
|
||||||
form.service.update_cleaned_data(form.cleaned_data, form=form)
|
|
||||||
if getattr(self, 'enabled', True):
|
|
||||||
return super(CustomPropertiesField, self).clean(value)
|
|
||||||
else:
|
|
||||||
return super(CustomPropertiesField, self).to_python(value)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def finalize_properties(cls, kwargs, form_name, service):
|
|
||||||
props = {}
|
|
||||||
kwargs_ = copy.copy(kwargs)
|
|
||||||
for key, value in kwargs_.items():
|
|
||||||
if isinstance(value, RawProperty):
|
|
||||||
props[key] = value.finalize(form_name, service, cls)
|
|
||||||
del kwargs[key]
|
|
||||||
if props:
|
|
||||||
return type(cls.__name__, (cls,), props)
|
|
||||||
else:
|
|
||||||
return cls
|
|
||||||
|
|
||||||
|
|
||||||
class CharField(forms.CharField, CustomPropertiesField):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class PasswordField(CharField):
|
|
||||||
special_characters = '!@#$%^&*()_+|\/.,~?><:{}-'
|
|
||||||
password_re = re.compile('^.*(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[%s]).*$'
|
|
||||||
% special_characters)
|
|
||||||
has_clone = False
|
|
||||||
original = True
|
|
||||||
attrs = {'data-type': 'password'}
|
|
||||||
validate_password = django_validator.RegexValidator(
|
|
||||||
password_re, _('The password must contain at least one letter, one \
|
|
||||||
number and one special character'), 'invalid')
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_clone_name(name):
|
|
||||||
return name + '-clone'
|
|
||||||
|
|
||||||
def compare(self, name, form_data):
|
|
||||||
if self.original and self.required:
|
|
||||||
# run compare only for original fields
|
|
||||||
# do not run compare for hidden fields (they are not required)
|
|
||||||
if form_data.get(name) != form_data.get(self.get_clone_name(name)):
|
|
||||||
raise forms.ValidationError(_(u"{0}{1} don't match").format(
|
|
||||||
self.label, defaultfilters.pluralize(2)))
|
|
||||||
|
|
||||||
def __init__(self, label, *args, **kwargs):
|
|
||||||
self.confirm_input = kwargs.pop('confirm_input', True)
|
|
||||||
|
|
||||||
kwargs.update({'label': label,
|
|
||||||
'error_messages': kwargs.get('error_messages', {}),
|
|
||||||
'widget': forms.PasswordInput(attrs=self.attrs,
|
|
||||||
render_value=True)})
|
|
||||||
|
|
||||||
validators = kwargs.get('validators')
|
|
||||||
help_text = kwargs.get('help_text')
|
|
||||||
|
|
||||||
if not validators:
|
|
||||||
# No custom validators, using default validator
|
|
||||||
validators = [self.validate_password]
|
|
||||||
if not help_text:
|
|
||||||
help_text = _(
|
|
||||||
'Enter a complex password with at least one letter, '
|
|
||||||
'one number and one special character')
|
|
||||||
|
|
||||||
kwargs['error_messages'].setdefault(
|
|
||||||
'invalid', self.validate_password.message)
|
|
||||||
kwargs['min_length'] = kwargs.get('min_length', 7)
|
|
||||||
kwargs['max_length'] = kwargs.get('max_length', 255)
|
|
||||||
kwargs['widget'] = forms.PasswordInput(attrs=self.attrs,
|
|
||||||
render_value=True)
|
|
||||||
else:
|
|
||||||
if not help_text:
|
|
||||||
# NOTE(kzaitsev) There are custom validators for password,
|
|
||||||
# but no help text let's leave only a generic message,
|
|
||||||
# since we do not know exact constraints
|
|
||||||
help_text = _('Enter a password')
|
|
||||||
|
|
||||||
kwargs.update({'validators': validators,
|
|
||||||
'help_text': help_text})
|
|
||||||
|
|
||||||
super(PasswordField, self).__init__(*args, **kwargs)
|
|
||||||
|
|
||||||
def __deepcopy__(self, memo):
|
|
||||||
result = super(PasswordField, self).__deepcopy__(memo)
|
|
||||||
result.error_messages = copy.deepcopy(self.error_messages)
|
|
||||||
return result
|
|
||||||
|
|
||||||
def clone_field(self):
|
|
||||||
self.has_clone = True
|
|
||||||
|
|
||||||
field = copy.deepcopy(self)
|
|
||||||
field.original = False
|
|
||||||
field.label = _('Confirm password')
|
|
||||||
field.error_messages['required'] = _('Please confirm your password')
|
|
||||||
field.help_text = _('Retype your password')
|
|
||||||
return field
|
|
||||||
|
|
||||||
|
|
||||||
class IntegerField(forms.IntegerField, CustomPropertiesField):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
def _get_title(data):
|
|
||||||
if isinstance(data, Choice):
|
|
||||||
return data.title
|
|
||||||
return data
|
|
||||||
|
|
||||||
|
|
||||||
def _disable_non_ready(data):
|
|
||||||
if getattr(data, 'enabled', True):
|
|
||||||
return {}
|
|
||||||
else:
|
|
||||||
return {'disabled': 'disabled'}
|
|
||||||
|
|
||||||
|
|
||||||
class ChoiceField(forms.ChoiceField, CustomPropertiesField):
|
|
||||||
def __init__(self, **kwargs):
|
|
||||||
choices = kwargs.get('choices') or getattr(self, 'choices', None)
|
|
||||||
if choices:
|
|
||||||
if isinstance(choices, dict):
|
|
||||||
choices = list(choices.items())
|
|
||||||
kwargs['choices'] = choices
|
|
||||||
kwargs['widget'] = hz_forms.ThemableSelectWidget(transform=_get_title)
|
|
||||||
super(ChoiceField, self).__init__(**kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
class DynamicChoiceField(hz_forms.ThemableDynamicChoiceField,
|
|
||||||
CustomPropertiesField):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class FlavorChoiceField(ChoiceField):
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
if 'requirements' in kwargs:
|
|
||||||
self.requirements = kwargs.pop('requirements')
|
|
||||||
super(FlavorChoiceField, self).__init__(*args, **kwargs)
|
|
||||||
|
|
||||||
@with_request
|
|
||||||
def update(self, request, **kwargs):
|
|
||||||
choices = []
|
|
||||||
flavors = nova.novaclient(request).flavors.list()
|
|
||||||
|
|
||||||
# If no requirements are present, return all the flavors.
|
|
||||||
if not hasattr(self, 'requirements'):
|
|
||||||
choices = [(flavor.name, flavor.name) for flavor in flavors]
|
|
||||||
else:
|
|
||||||
for flavor in flavors:
|
|
||||||
# If a flavor doesn't meet a minimum requirement,
|
|
||||||
# do not add it to the options list and skip to the
|
|
||||||
# next flavor.
|
|
||||||
if flavor.vcpus < self.requirements.get('min_vcpus', 0):
|
|
||||||
continue
|
|
||||||
if flavor.disk < self.requirements.get('min_disk', 0):
|
|
||||||
continue
|
|
||||||
if flavor.ram < self.requirements.get('min_memory_mb', 0):
|
|
||||||
continue
|
|
||||||
if 'max_vcpus' in self.requirements:
|
|
||||||
if flavor.vcpus > self.requirements['max_vcpus']:
|
|
||||||
continue
|
|
||||||
if 'max_disk' in self.requirements:
|
|
||||||
if flavor.disk > self.requirements['max_disk']:
|
|
||||||
continue
|
|
||||||
if 'max_memory_mb' in self.requirements:
|
|
||||||
if flavor.ram > self.requirements['max_memory_mb']:
|
|
||||||
continue
|
|
||||||
choices.append((flavor.name, flavor.name))
|
|
||||||
|
|
||||||
choices.sort(key=lambda e: e[1])
|
|
||||||
self.choices = choices
|
|
||||||
if kwargs.get('form'):
|
|
||||||
kwargs_form_flavor = kwargs["form"].fields.get('flavor')
|
|
||||||
else:
|
|
||||||
kwargs_form_flavor = None
|
|
||||||
if kwargs_form_flavor:
|
|
||||||
self.initial = kwargs["form"]["flavor"].value()
|
|
||||||
else:
|
|
||||||
# Search through selected flavors
|
|
||||||
for flavor_name, flavor_name in self.choices:
|
|
||||||
if 'medium' in flavor_name:
|
|
||||||
self.initial = flavor_name
|
|
||||||
break
|
|
||||||
|
|
||||||
|
|
||||||
class KeyPairChoiceField(DynamicChoiceField):
|
|
||||||
"""This widget allows to select keypair for VMs"""
|
|
||||||
@with_request
|
|
||||||
def update(self, request, **kwargs):
|
|
||||||
self.choices = [('', _('No keypair'))]
|
|
||||||
for keypair in sorted(
|
|
||||||
nova.novaclient(request).keypairs.list(),
|
|
||||||
key=lambda e: e.name):
|
|
||||||
self.choices.append((keypair.name, keypair.name))
|
|
||||||
|
|
||||||
|
|
||||||
class SecurityGroupChoiceField(DynamicChoiceField):
|
|
||||||
"""This widget allows to select a security group for VMs"""
|
|
||||||
@with_request
|
|
||||||
def update(self, request, **kwargs):
|
|
||||||
self.choices = [('', _('Application default security group'))]
|
|
||||||
# TODO(pbourke): remove sorted when supported natively in Horizon
|
|
||||||
# (https://bugs.launchpad.net/horizon/+bug/1692972)
|
|
||||||
for secgroup in sorted(
|
|
||||||
neutron.security_group_list(request),
|
|
||||||
key=lambda e: e.name_or_id):
|
|
||||||
if not secgroup.name_or_id.startswith('murano--'):
|
|
||||||
self.choices.append((secgroup.name_or_id, secgroup.name_or_id))
|
|
||||||
|
|
||||||
|
|
||||||
# NOTE(kzaitsev): for transform to work correctly on horizon SelectWidget
|
|
||||||
# Choice has to be non-string
|
|
||||||
class Choice(object):
|
|
||||||
"""A choice that allows disabling specific choices in a SelectWidget."""
|
|
||||||
def __init__(self, title, enabled):
|
|
||||||
self.title = title
|
|
||||||
self.enabled = enabled
|
|
||||||
|
|
||||||
|
|
||||||
class ImageChoiceField(ChoiceField):
|
|
||||||
widget = hz_forms.ThemableSelectWidget(
|
|
||||||
transform=_get_title,
|
|
||||||
transform_html_attrs=_disable_non_ready)
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
self.image_type = kwargs.pop('image_type', None)
|
|
||||||
super(ImageChoiceField, self).__init__(*args, **kwargs)
|
|
||||||
|
|
||||||
@with_request
|
|
||||||
def update(self, request, **kwargs):
|
|
||||||
image_map, image_choices = {}, []
|
|
||||||
murano_images = get_murano_images(request)
|
|
||||||
for image in murano_images:
|
|
||||||
murano_data = image.murano_property
|
|
||||||
title = murano_data.get('title', image.name)
|
|
||||||
|
|
||||||
if image.status == 'active':
|
|
||||||
title = Choice(title, enabled=True)
|
|
||||||
else:
|
|
||||||
title = Choice("{} ({})".format(title, image.status),
|
|
||||||
enabled=False)
|
|
||||||
if self.image_type is not None:
|
|
||||||
itype = murano_data.get('type')
|
|
||||||
|
|
||||||
if not self.image_type and itype is None:
|
|
||||||
continue
|
|
||||||
|
|
||||||
prefix = '{type}.'.format(type=self.image_type)
|
|
||||||
if (not itype.startswith(prefix) and
|
|
||||||
not self.image_type == itype):
|
|
||||||
continue
|
|
||||||
image_map[image.id] = title
|
|
||||||
|
|
||||||
for id_, title in sorted(six.iteritems(image_map),
|
|
||||||
key=lambda e: e[1].title):
|
|
||||||
image_choices.append((id_, title))
|
|
||||||
if image_choices:
|
|
||||||
image_choices.insert(0, ("", _("Select Image")))
|
|
||||||
else:
|
|
||||||
image_choices.insert(0, ("", _("No images available")))
|
|
||||||
|
|
||||||
self.choices = image_choices
|
|
||||||
|
|
||||||
|
|
||||||
class NetworkChoiceField(ChoiceField):
|
|
||||||
def __init__(self,
|
|
||||||
include_subnets=True,
|
|
||||||
filter=None,
|
|
||||||
murano_networks=None,
|
|
||||||
allow_auto=True,
|
|
||||||
*args,
|
|
||||||
**kwargs):
|
|
||||||
self.filter = filter
|
|
||||||
if murano_networks:
|
|
||||||
if murano_networks.lower() not in ["exclude", "translate"]:
|
|
||||||
raise ValueError(_("Invalid value of 'murano_nets' option"))
|
|
||||||
self.murano_networks = murano_networks
|
|
||||||
self.include_subnets = include_subnets
|
|
||||||
self.allow_auto = allow_auto
|
|
||||||
super(NetworkChoiceField, self).__init__(*args,
|
|
||||||
**kwargs)
|
|
||||||
|
|
||||||
@with_request
|
|
||||||
def update(self, request, **kwargs):
|
|
||||||
"""Populates available networks in the control
|
|
||||||
|
|
||||||
This method is called automatically when the form which contains it is
|
|
||||||
rendered
|
|
||||||
"""
|
|
||||||
network_choices = net.get_available_networks(request,
|
|
||||||
self.include_subnets,
|
|
||||||
self.filter,
|
|
||||||
self.murano_networks)
|
|
||||||
if self.allow_auto:
|
|
||||||
network_choices.insert(0, ((None, None), _('Auto')))
|
|
||||||
self.choices = network_choices or []
|
|
||||||
|
|
||||||
def to_python(self, value):
|
|
||||||
"""Converts string representation of widget to tuple value
|
|
||||||
|
|
||||||
Is called implicitly during form cleanup phase
|
|
||||||
"""
|
|
||||||
if value:
|
|
||||||
return ast.literal_eval(value)
|
|
||||||
else: # may happen if no networks are available and "Auto" is disabled
|
|
||||||
return None, None
|
|
||||||
|
|
||||||
|
|
||||||
class AZoneChoiceField(ChoiceField):
|
|
||||||
@with_request
|
|
||||||
def update(self, request, **kwargs):
|
|
||||||
try:
|
|
||||||
availability_zones = nova.novaclient(
|
|
||||||
request).availability_zones.list(detailed=False)
|
|
||||||
except Exception:
|
|
||||||
availability_zones = []
|
|
||||||
exceptions.handle(request,
|
|
||||||
_("Unable to retrieve availability zones."))
|
|
||||||
|
|
||||||
az_choices = [(az.zoneName, az.zoneName)
|
|
||||||
for az in availability_zones if az.zoneState]
|
|
||||||
if not az_choices:
|
|
||||||
az_choices.insert(0, ("", _("No availability zones available")))
|
|
||||||
|
|
||||||
az_choices.sort(key=lambda e: e[1])
|
|
||||||
self.choices = az_choices
|
|
||||||
|
|
||||||
|
|
||||||
class VolumeChoiceField(ChoiceField):
|
|
||||||
def __init__(self,
|
|
||||||
include_snapshots=True,
|
|
||||||
*args,
|
|
||||||
**kwargs):
|
|
||||||
self.include_snapshots = include_snapshots
|
|
||||||
super(VolumeChoiceField, self).__init__(*args, **kwargs)
|
|
||||||
|
|
||||||
@with_request
|
|
||||||
def update(self, request, **kwargs):
|
|
||||||
"""This widget allows selection of Volumes and Volume Snapshots"""
|
|
||||||
available = {'status': cinder.VOLUME_STATE_AVAILABLE}
|
|
||||||
try:
|
|
||||||
choices = [(volume.id, volume.name)
|
|
||||||
for volume in cinder.volume_list(request,
|
|
||||||
search_opts=available)]
|
|
||||||
except Exception:
|
|
||||||
choices = []
|
|
||||||
exceptions.handle(request,
|
|
||||||
_("Unable to retrieve volume list."))
|
|
||||||
|
|
||||||
if self.include_snapshots:
|
|
||||||
try:
|
|
||||||
choices.extend((snap.id, snap.name)
|
|
||||||
for snap in cinder.volume_snapshot_list(request,
|
|
||||||
search_opts=available))
|
|
||||||
except Exception:
|
|
||||||
exceptions.handle(request,
|
|
||||||
_("Unable to retrieve snapshot list."))
|
|
||||||
|
|
||||||
if choices:
|
|
||||||
choices.sort(key=lambda e: e[1])
|
|
||||||
choices.insert(0, ("", _("Select volume")))
|
|
||||||
else:
|
|
||||||
choices.insert(0, ("", _("No volumes available")))
|
|
||||||
self.choices = choices
|
|
||||||
|
|
||||||
|
|
||||||
class BooleanField(forms.BooleanField, CustomPropertiesField):
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
if 'widget' in kwargs:
|
|
||||||
widget = kwargs['widget']
|
|
||||||
if isinstance(widget, type):
|
|
||||||
widget = widget(attrs={'class': 'checkbox'})
|
|
||||||
else:
|
|
||||||
widget = forms.CheckboxInput(attrs={'class': 'checkbox'})
|
|
||||||
kwargs['widget'] = widget
|
|
||||||
kwargs['required'] = False
|
|
||||||
super(BooleanField, self).__init__(*args, **kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
@versionutils.deprecated(
|
|
||||||
as_of=versionutils.deprecated.JUNO,
|
|
||||||
in_favor_of='type boolean (regular BooleanField)',
|
|
||||||
remove_in=1)
|
|
||||||
class FloatingIpBooleanField(BooleanField):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class ClusterIPField(forms.GenericIPAddressField, CustomPropertiesField):
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
super(ClusterIPField, self).__init__(protocol='ipv4', *args, **kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
class DatabaseListField(CharField):
|
|
||||||
validate_mssql_identifier = django_validator.RegexValidator(
|
|
||||||
re.compile(r'^[a-zA-z_][a-zA-Z0-9_$#@]*$'),
|
|
||||||
_(u'First symbol should be latin letter or underscore. Subsequent '
|
|
||||||
u'symbols can be latin letter, numeric, underscore, at sign, '
|
|
||||||
u'number sign or dollar sign'))
|
|
||||||
|
|
||||||
default_error_messages = {'invalid': validate_mssql_identifier.message}
|
|
||||||
|
|
||||||
def to_python(self, value):
|
|
||||||
"""Normalize data to a list of strings."""
|
|
||||||
if not value:
|
|
||||||
return []
|
|
||||||
return [name.strip() for name in value.split(',')]
|
|
||||||
|
|
||||||
def validate(self, value):
|
|
||||||
"""Check if value consists only of valid names."""
|
|
||||||
super(DatabaseListField, self).validate(value)
|
|
||||||
for db_name in value:
|
|
||||||
self.validate_mssql_identifier(db_name)
|
|
||||||
|
|
||||||
|
|
||||||
class ErrorWidget(widgets.Widget):
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
self.message = kwargs.pop(
|
|
||||||
'message', _("There was an error initialising this field."))
|
|
||||||
super(ErrorWidget, self).__init__(*args, **kwargs)
|
|
||||||
|
|
||||||
def render(self, name, value, attrs=None):
|
|
||||||
return "<div name={name}>{message}</div>".format(
|
|
||||||
name=name, message=self.message)
|
|
||||||
|
|
||||||
|
|
||||||
class MuranoTypeWidget(hz_forms.fields.DynamicSelectWidget):
|
|
||||||
def __init__(self, attrs=None, **kwargs):
|
|
||||||
if attrs is None:
|
|
||||||
attrs = {'class': 'murano_add_select'}
|
|
||||||
else:
|
|
||||||
attrs.setdefault('class', '')
|
|
||||||
attrs['class'] += ' murano_add_select'
|
|
||||||
super(MuranoTypeWidget, self).__init__(attrs=attrs, **kwargs)
|
|
||||||
|
|
||||||
class Media(object):
|
|
||||||
js = ('muranodashboard/js/add-select.js',)
|
|
||||||
|
|
||||||
|
|
||||||
def make_select_cls(fqns):
|
|
||||||
if not isinstance(fqns, (tuple, list)):
|
|
||||||
fqns = (fqns,)
|
|
||||||
|
|
||||||
class DynamicSelect(hz_forms.DynamicChoiceField, CustomPropertiesField):
|
|
||||||
widget = MuranoTypeWidget
|
|
||||||
|
|
||||||
def __init__(self, empty_value_message=None, *args, **kwargs):
|
|
||||||
super(DynamicSelect, self).__init__(*args, **kwargs)
|
|
||||||
if empty_value_message is not None:
|
|
||||||
self.empty_value_message = empty_value_message
|
|
||||||
else:
|
|
||||||
self.empty_value_message = _('Select Application')
|
|
||||||
|
|
||||||
@with_request
|
|
||||||
def update(self, request, environment_id, **kwargs):
|
|
||||||
matching_classes = []
|
|
||||||
fqns_seen = set()
|
|
||||||
# NOTE(kzaitsev): it's possible to have a private
|
|
||||||
# and public apps with the same fqn, however the engine would
|
|
||||||
# currently favor private package. Therefore we should squash
|
|
||||||
# these until we devise a better way to work with this
|
|
||||||
# situation and versioning
|
|
||||||
|
|
||||||
for class_fqn in fqns:
|
|
||||||
app_found = pkg_api.app_by_fqn(request, class_fqn)
|
|
||||||
if app_found:
|
|
||||||
fqns_seen.add(app_found.fully_qualified_name)
|
|
||||||
matching_classes.append(app_found)
|
|
||||||
|
|
||||||
apps_found = pkg_api.apps_that_inherit(request, class_fqn)
|
|
||||||
for app in apps_found:
|
|
||||||
if app.fully_qualified_name in fqns_seen:
|
|
||||||
continue
|
|
||||||
fqns_seen.add(app.fully_qualified_name)
|
|
||||||
matching_classes.append(app)
|
|
||||||
|
|
||||||
if not matching_classes:
|
|
||||||
msg = _(
|
|
||||||
"Couldn't find any apps, required for this field.\n"
|
|
||||||
"Tried: {fqns}").format(fqns=', '.join(fqns))
|
|
||||||
self.widget = ErrorWidget(message=msg)
|
|
||||||
|
|
||||||
# NOTE(kzaitsev): this closure is needed to allow us have custom
|
|
||||||
# logic when clicking add button
|
|
||||||
def _make_link():
|
|
||||||
ns_url = 'horizon:app-catalog:catalog:add'
|
|
||||||
ns_url_args = (environment_id, False, True)
|
|
||||||
|
|
||||||
# This will prevent horizon from adding an extra '+' button
|
|
||||||
if not matching_classes:
|
|
||||||
return ''
|
|
||||||
|
|
||||||
return json.dumps([
|
|
||||||
(app.name, reverse(ns_url, args=((app.id,) + ns_url_args)))
|
|
||||||
for app in matching_classes])
|
|
||||||
|
|
||||||
self.widget.add_item_link = _make_link
|
|
||||||
|
|
||||||
apps = env_api.service_list_by_fqns(
|
|
||||||
request, environment_id,
|
|
||||||
[app.fully_qualified_name for app in matching_classes])
|
|
||||||
choices = [('', self.empty_value_message)]
|
|
||||||
choices.extend([(app['?']['id'],
|
|
||||||
html.escape(app.name)) for app in apps])
|
|
||||||
self.choices = choices
|
|
||||||
# NOTE(tsufiev): streamline the drop-down UX: auto-select the
|
|
||||||
# single available option in a drop-down
|
|
||||||
if len(choices) == 2:
|
|
||||||
self.initial = choices[1][0]
|
|
||||||
|
|
||||||
def clean(self, value):
|
|
||||||
value = super(DynamicSelect, self).clean(value)
|
|
||||||
return None if value == '' else value
|
|
||||||
|
|
||||||
return DynamicSelect
|
|
||||||
|
|
||||||
|
|
||||||
@versionutils.deprecated(
|
|
||||||
as_of=versionutils.deprecated.JUNO,
|
|
||||||
in_favor_of='type io.murano.windows.ActiveDirectory with a custom '
|
|
||||||
'emptyValueMessage attribute',
|
|
||||||
remove_in=1)
|
|
||||||
class DomainChoiceField(make_select_cls('io.murano.windows.ActiveDirectory')):
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
super(DomainChoiceField, self).__init__(*args, **kwargs)
|
|
||||||
self.choices = [('', _('Not in domain'))]
|
|
|
@ -1,232 +0,0 @@
|
||||||
# Copyright (c) 2013 Mirantis, Inc.
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
||||||
# not use this file except in compliance with the License. You may obtain
|
|
||||||
# a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
||||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
||||||
# License for the specific language governing permissions and limitations
|
|
||||||
# under the License.
|
|
||||||
|
|
||||||
from collections import defaultdict
|
|
||||||
|
|
||||||
from django import forms
|
|
||||||
from django.utils.translation import ugettext_lazy as _
|
|
||||||
from oslo_log import log as logging
|
|
||||||
import six
|
|
||||||
from yaql import legacy
|
|
||||||
|
|
||||||
import muranodashboard.dynamic_ui.fields as fields
|
|
||||||
import muranodashboard.dynamic_ui.helpers as helpers
|
|
||||||
from muranodashboard.dynamic_ui import yaql_expression
|
|
||||||
from muranodashboard.dynamic_ui import yaql_functions
|
|
||||||
|
|
||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class AnyFieldDict(defaultdict):
|
|
||||||
def __missing__(self, key):
|
|
||||||
return fields.make_select_cls(key)
|
|
||||||
|
|
||||||
|
|
||||||
TYPES = AnyFieldDict()
|
|
||||||
TYPES.update({
|
|
||||||
'string': fields.CharField,
|
|
||||||
'boolean': fields.BooleanField,
|
|
||||||
'clusterip': fields.ClusterIPField,
|
|
||||||
'domain': fields.DomainChoiceField,
|
|
||||||
'password': fields.PasswordField,
|
|
||||||
'integer': fields.IntegerField,
|
|
||||||
'databaselist': fields.DatabaseListField,
|
|
||||||
'flavor': fields.FlavorChoiceField,
|
|
||||||
'keypair': fields.KeyPairChoiceField,
|
|
||||||
'image': fields.ImageChoiceField,
|
|
||||||
'azone': fields.AZoneChoiceField,
|
|
||||||
'network': fields.NetworkChoiceField,
|
|
||||||
'text': (fields.CharField, forms.Textarea),
|
|
||||||
'choice': fields.ChoiceField,
|
|
||||||
'floatingip': fields.FloatingIpBooleanField,
|
|
||||||
'securitygroup': fields.SecurityGroupChoiceField,
|
|
||||||
'volume': fields.VolumeChoiceField
|
|
||||||
})
|
|
||||||
|
|
||||||
KEYPAIR_IMPORT_URL = "horizon:project:key_pairs:import"
|
|
||||||
TYPES_KWARGS = {
|
|
||||||
'keypair': {'add_item_link': KEYPAIR_IMPORT_URL}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def _collect_fields(field_specs, form_name, service):
|
|
||||||
def process_widget(cls, kwargs):
|
|
||||||
if isinstance(cls, tuple):
|
|
||||||
cls, _w = cls
|
|
||||||
kwargs['widget'] = _w
|
|
||||||
|
|
||||||
widget = kwargs.get('widget') or cls.widget
|
|
||||||
if 'widget_media' in kwargs:
|
|
||||||
media = kwargs['widget_media']
|
|
||||||
del kwargs['widget_media']
|
|
||||||
|
|
||||||
class Widget(widget):
|
|
||||||
class Media(object):
|
|
||||||
js = media.get('js', ())
|
|
||||||
css = media.get('css', {})
|
|
||||||
widget = Widget
|
|
||||||
|
|
||||||
if 'widget_attrs' in kwargs:
|
|
||||||
widget = widget(attrs=kwargs.pop('widget_attrs'))
|
|
||||||
return cls, widget
|
|
||||||
|
|
||||||
def parse_spec(spec, keys=None):
|
|
||||||
if keys is None:
|
|
||||||
keys = []
|
|
||||||
if not isinstance(keys, list):
|
|
||||||
keys = [keys]
|
|
||||||
key = keys and keys[-1] or None
|
|
||||||
|
|
||||||
if isinstance(spec, yaql_expression.YaqlExpression):
|
|
||||||
return key, fields.RawProperty(key, spec)
|
|
||||||
elif isinstance(spec, dict):
|
|
||||||
items = []
|
|
||||||
for k, v in six.iteritems(spec):
|
|
||||||
k = helpers.decamelize(k)
|
|
||||||
new_key, v = parse_spec(v, keys + [k])
|
|
||||||
if new_key:
|
|
||||||
k = new_key
|
|
||||||
items.append((k, v))
|
|
||||||
return key, dict(items)
|
|
||||||
elif isinstance(spec, list):
|
|
||||||
return key, [parse_spec(_spec, keys)[1] for _spec in spec]
|
|
||||||
elif isinstance(spec,
|
|
||||||
six.string_types) and helpers.is_localizable(keys):
|
|
||||||
return key, spec
|
|
||||||
else:
|
|
||||||
if key == 'hidden':
|
|
||||||
if spec:
|
|
||||||
return 'widget', forms.HiddenInput
|
|
||||||
else:
|
|
||||||
return 'widget', None
|
|
||||||
elif key == 'regexp_validator':
|
|
||||||
return 'validators', [helpers.prepare_regexp(spec)]
|
|
||||||
else:
|
|
||||||
return key, spec
|
|
||||||
|
|
||||||
def make_field(field_spec):
|
|
||||||
_type, name = field_spec.pop('type'), field_spec.pop('name')
|
|
||||||
if isinstance(_type, list): # make list keys hashable for TYPES dict
|
|
||||||
_type = tuple(_type)
|
|
||||||
_ignorable, kwargs = parse_spec(field_spec)
|
|
||||||
kwargs.update(TYPES_KWARGS.get(_type, {}))
|
|
||||||
cls, kwargs['widget'] = process_widget(TYPES[_type], kwargs)
|
|
||||||
cls = cls.finalize_properties(kwargs, form_name, service)
|
|
||||||
|
|
||||||
return name, cls(**kwargs)
|
|
||||||
|
|
||||||
return [make_field(_spec) for _spec in field_specs]
|
|
||||||
|
|
||||||
|
|
||||||
class DynamicFormMetaclass(forms.forms.DeclarativeFieldsMetaclass):
|
|
||||||
def __new__(meta, name, bases, dct):
|
|
||||||
name = dct.pop('name', name)
|
|
||||||
field_specs = dct.pop('field_specs', [])
|
|
||||||
service = dct['service']
|
|
||||||
for field_name, field in _collect_fields(field_specs, name, service):
|
|
||||||
dct[field_name] = field
|
|
||||||
return super(DynamicFormMetaclass, meta).__new__(
|
|
||||||
meta, name, bases, dct)
|
|
||||||
|
|
||||||
|
|
||||||
class UpdatableFieldsForm(forms.Form):
|
|
||||||
"""Dynamic updatable form
|
|
||||||
|
|
||||||
This class is supposed to be a base for forms belonging to a FormWizard
|
|
||||||
descendant, or be used as a mixin for workflows.Action class.
|
|
||||||
|
|
||||||
In first case the `request' used in `update' method is provided in
|
|
||||||
`self.initial' dictionary, in the second case request should be provided
|
|
||||||
directly in `request' parameter.
|
|
||||||
"""
|
|
||||||
required_css_class = 'required'
|
|
||||||
|
|
||||||
def update_fields(self, request=None):
|
|
||||||
# Create 'Confirm Password' fields by duplicating password fields
|
|
||||||
|
|
||||||
# django.utils.datastructures.SortedDict for Django < 1.7
|
|
||||||
# collections.OrderedDict for Django >= 1.7
|
|
||||||
updated_fields = self.fields.__class__()
|
|
||||||
|
|
||||||
for name, field in six.iteritems(self.fields):
|
|
||||||
updated_fields[name] = field
|
|
||||||
if isinstance(field, fields.PasswordField) and field.confirm_input:
|
|
||||||
if not field.has_clone and field.original:
|
|
||||||
updated_fields[
|
|
||||||
field.get_clone_name(name)] = field.clone_field()
|
|
||||||
|
|
||||||
self.fields = updated_fields
|
|
||||||
|
|
||||||
for name, field in six.iteritems(self.fields):
|
|
||||||
if hasattr(field, 'update'):
|
|
||||||
field.update(self.initial, form=self, request=request)
|
|
||||||
if not field.required:
|
|
||||||
field.widget.attrs['placeholder'] = _('Optional')
|
|
||||||
|
|
||||||
|
|
||||||
class ServiceConfigurationForm(UpdatableFieldsForm):
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
LOG.info("Creating form {0}".format(self.__class__.__name__))
|
|
||||||
super(ServiceConfigurationForm, self).__init__(*args, **kwargs)
|
|
||||||
|
|
||||||
self.auto_id = '{0}_%s'.format(self.initial.get('app_id'))
|
|
||||||
self.context = legacy.create_context()
|
|
||||||
yaql_functions.register(self.context)
|
|
||||||
|
|
||||||
self.finalize_fields()
|
|
||||||
self.update_fields()
|
|
||||||
|
|
||||||
def finalize_fields(self):
|
|
||||||
for field_name, field in six.iteritems(self.fields):
|
|
||||||
field.form = self
|
|
||||||
|
|
||||||
validators = []
|
|
||||||
for v in field.validators:
|
|
||||||
expr = isinstance(v, dict) and v.get('expr')
|
|
||||||
if expr and isinstance(expr, fields.RawProperty):
|
|
||||||
v = fields.make_yaql_validator(v)
|
|
||||||
validators.append(v)
|
|
||||||
field.validators = validators
|
|
||||||
|
|
||||||
def clean(self):
|
|
||||||
if self._errors:
|
|
||||||
return self.cleaned_data
|
|
||||||
else:
|
|
||||||
cleaned_data = super(ServiceConfigurationForm, self).clean()
|
|
||||||
all_data = self.service.update_cleaned_data(
|
|
||||||
cleaned_data, form=self)
|
|
||||||
error_messages = []
|
|
||||||
for validator in self.validators:
|
|
||||||
expr = validator['expr']
|
|
||||||
if not expr.evaluate(data=all_data, context=self.context):
|
|
||||||
error_messages.append(validator.get('message',
|
|
||||||
_('Validation Error occurred')))
|
|
||||||
if error_messages:
|
|
||||||
raise forms.ValidationError(error_messages)
|
|
||||||
|
|
||||||
for name, field in six.iteritems(self.fields):
|
|
||||||
if (isinstance(field, fields.PasswordField) and
|
|
||||||
getattr(field, 'enabled', True) and
|
|
||||||
field.confirm_input):
|
|
||||||
field.compare(name, cleaned_data)
|
|
||||||
|
|
||||||
if hasattr(field, 'postclean'):
|
|
||||||
value = field.postclean(self, cleaned_data)
|
|
||||||
if value:
|
|
||||||
cleaned_data[name] = value
|
|
||||||
LOG.debug("Update cleaned data in postclean method")
|
|
||||||
|
|
||||||
self.service.update_cleaned_data(cleaned_data, form=self)
|
|
||||||
return cleaned_data
|
|
|
@ -1,162 +0,0 @@
|
||||||
# Copyright (c) 2013 Mirantis, Inc.
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
||||||
# not use this file except in compliance with the License. You may obtain
|
|
||||||
# a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
||||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
||||||
# License for the specific language governing permissions and limitations
|
|
||||||
# under the License.
|
|
||||||
|
|
||||||
import re
|
|
||||||
import string
|
|
||||||
import types
|
|
||||||
import uuid
|
|
||||||
|
|
||||||
import six
|
|
||||||
|
|
||||||
from django.core import validators
|
|
||||||
|
|
||||||
_LOCALIZABLE_KEYS = set(['label', 'help_text', 'error_messages'])
|
|
||||||
|
|
||||||
|
|
||||||
class ObjectID(object):
|
|
||||||
def __init__(self):
|
|
||||||
self.object_id = str(uuid.uuid4())
|
|
||||||
|
|
||||||
|
|
||||||
def is_localizable(keys):
|
|
||||||
return set(keys).intersection(_LOCALIZABLE_KEYS)
|
|
||||||
|
|
||||||
|
|
||||||
def camelize(name):
|
|
||||||
"""Turns snake_case name into SnakeCase."""
|
|
||||||
return ''.join([bit.capitalize() for bit in name.split('_')])
|
|
||||||
|
|
||||||
|
|
||||||
def decamelize(name):
|
|
||||||
"""Turns CamelCase/camelCase name into camel_case."""
|
|
||||||
pat = re.compile(r'([A-Z]*[^A-Z]*)(.*)')
|
|
||||||
bits = []
|
|
||||||
while True:
|
|
||||||
head, tail = re.match(pat, name).groups()
|
|
||||||
bits.append(head)
|
|
||||||
if tail:
|
|
||||||
name = tail
|
|
||||||
else:
|
|
||||||
break
|
|
||||||
return '_'.join([bit.lower() for bit in bits])
|
|
||||||
|
|
||||||
|
|
||||||
def explode(_string):
|
|
||||||
"""Explodes a string into a list of one-character strings."""
|
|
||||||
if not _string or not isinstance(_string, six.string_types):
|
|
||||||
return _string
|
|
||||||
else:
|
|
||||||
return list(_string)
|
|
||||||
|
|
||||||
|
|
||||||
def prepare_regexp(regexp):
|
|
||||||
"""Converts regular expression string pattern into RegexValidator object.
|
|
||||||
|
|
||||||
Also /regexp/flags syntax is allowed, where flags is a string of
|
|
||||||
one-character flags that will be appended to the compiled regexp.
|
|
||||||
"""
|
|
||||||
if regexp.startswith('/'):
|
|
||||||
groups = re.match(r'^/(.*)/([A-Za-z]*)$', regexp).groups()
|
|
||||||
regexp, flags_str = groups
|
|
||||||
flags = 0
|
|
||||||
for flag in explode(flags_str):
|
|
||||||
flag = flag.upper()
|
|
||||||
if hasattr(re, flag):
|
|
||||||
flags |= getattr(re, flag)
|
|
||||||
return validators.RegexValidator(re.compile(regexp, flags))
|
|
||||||
else:
|
|
||||||
return validators.RegexValidator(re.compile(regexp))
|
|
||||||
|
|
||||||
|
|
||||||
def recursive_apply(predicate, transformer, value, *args):
|
|
||||||
def rec(val):
|
|
||||||
if predicate(val, *args):
|
|
||||||
return rec(transformer(val, *args))
|
|
||||||
elif isinstance(val, dict):
|
|
||||||
return dict((rec(k), rec(v)) for (k, v) in six.iteritems(val))
|
|
||||||
elif isinstance(val, list):
|
|
||||||
return [rec(v) for v in val]
|
|
||||||
elif isinstance(val, tuple):
|
|
||||||
return tuple([rec(v) for v in val])
|
|
||||||
elif isinstance(val, types.GeneratorType):
|
|
||||||
return rec(val)
|
|
||||||
else:
|
|
||||||
return val
|
|
||||||
|
|
||||||
return rec(value)
|
|
||||||
|
|
||||||
|
|
||||||
def evaluate(value, context):
|
|
||||||
return recursive_apply(
|
|
||||||
lambda v, _ctx: hasattr(v, 'evaluate'),
|
|
||||||
lambda v, _ctx: v.evaluate(context=_ctx),
|
|
||||||
value, context)
|
|
||||||
|
|
||||||
|
|
||||||
def insert_hidden_ids(application):
|
|
||||||
def wrap(k, v):
|
|
||||||
if k == '?' and isinstance(v, dict) and not isinstance(
|
|
||||||
v.get('id'), ObjectID):
|
|
||||||
v['id'] = str(uuid.uuid4())
|
|
||||||
return k, v
|
|
||||||
elif isinstance(v, ObjectID):
|
|
||||||
return k, v.object_id
|
|
||||||
else:
|
|
||||||
return rec(k), rec(v)
|
|
||||||
|
|
||||||
def rec(val):
|
|
||||||
if isinstance(val, dict):
|
|
||||||
return dict(wrap(k, v) for k, v in six.iteritems(val))
|
|
||||||
elif isinstance(val, list):
|
|
||||||
return [rec(v) for v in val]
|
|
||||||
else:
|
|
||||||
return val
|
|
||||||
|
|
||||||
return rec(application)
|
|
||||||
|
|
||||||
|
|
||||||
def int2base(x, base):
|
|
||||||
"""Converts decimal integers to another number base from base-2 to base-36
|
|
||||||
|
|
||||||
:param x: decimal integer
|
|
||||||
:param base: number base, max value is 36
|
|
||||||
:return: integer converted to the specified base
|
|
||||||
"""
|
|
||||||
digs = string.digits + string.ascii_lowercase
|
|
||||||
if x < 0:
|
|
||||||
sign = -1
|
|
||||||
elif x == 0:
|
|
||||||
return '0'
|
|
||||||
else:
|
|
||||||
sign = 1
|
|
||||||
x *= sign
|
|
||||||
digits = []
|
|
||||||
while x:
|
|
||||||
digits.append(digs[x % base])
|
|
||||||
x //= base
|
|
||||||
if sign < 0:
|
|
||||||
digits.append('-')
|
|
||||||
digits.reverse()
|
|
||||||
return ''.join(digits)
|
|
||||||
|
|
||||||
|
|
||||||
def to_str(text):
|
|
||||||
if not isinstance(text, str):
|
|
||||||
# unicode in python2
|
|
||||||
if isinstance(text, six.text_type):
|
|
||||||
text = text.encode('utf-8')
|
|
||||||
# bytes in python3
|
|
||||||
elif isinstance(text, six.binary_type):
|
|
||||||
text = text.decode('utf-8')
|
|
||||||
return text
|
|
|
@ -1,285 +0,0 @@
|
||||||
# Copyright (c) 2013 Mirantis, Inc.
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
||||||
# not use this file except in compliance with the License. You may obtain
|
|
||||||
# a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
||||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
||||||
# License for the specific language governing permissions and limitations
|
|
||||||
# under the License.
|
|
||||||
|
|
||||||
import os
|
|
||||||
import re
|
|
||||||
import semantic_version
|
|
||||||
|
|
||||||
from django.utils.encoding import force_text
|
|
||||||
from django.utils.translation import ugettext_lazy as _
|
|
||||||
from oslo_log import log as logging
|
|
||||||
import six
|
|
||||||
from yaql import legacy
|
|
||||||
|
|
||||||
from muranodashboard import api
|
|
||||||
from muranodashboard.api import packages as pkg_api
|
|
||||||
from muranodashboard.catalog import forms as catalog_forms
|
|
||||||
from muranodashboard.dynamic_ui import helpers
|
|
||||||
from muranodashboard.dynamic_ui import version
|
|
||||||
from muranodashboard.dynamic_ui import yaql_functions
|
|
||||||
from muranodashboard.environments import consts
|
|
||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
if not os.path.exists(consts.CACHE_DIR):
|
|
||||||
os.mkdir(consts.CACHE_DIR)
|
|
||||||
LOG.info('Creating cache directory located at {dir}'.format(
|
|
||||||
dir=consts.CACHE_DIR))
|
|
||||||
LOG.info('Using cache directory located at {dir}'.format(
|
|
||||||
dir=consts.CACHE_DIR))
|
|
||||||
|
|
||||||
|
|
||||||
class Service(object):
|
|
||||||
"""Murano Service representation object
|
|
||||||
|
|
||||||
Class for keeping service persistent data, the most important are two:
|
|
||||||
``self.forms`` list of service's steps (as Django form classes) and
|
|
||||||
``self.cleaned_data`` dictionary of data from service validated steps.
|
|
||||||
|
|
||||||
Attribute ``self.cleaned_data`` is needed for, e.g. ServiceA.Step2, be
|
|
||||||
able to reference data at ServiceA.Step1 while actual form instance
|
|
||||||
representing Step1 is already gone. That attribute is stored per-user,
|
|
||||||
so sessions are employed - the reference to a dictionary with forms data
|
|
||||||
stored in a session is passed to Service during its initialization,
|
|
||||||
because Service instance is re-created on each request from UI definition
|
|
||||||
stored at local file-system cache .
|
|
||||||
"""
|
|
||||||
def __init__(self, cleaned_data, version, fqn, forms=None, templates=None,
|
|
||||||
application=None, parameters=None, **kwargs):
|
|
||||||
self.cleaned_data = cleaned_data
|
|
||||||
self.templates = templates or {}
|
|
||||||
self.spec_version = str(version)
|
|
||||||
if forms is None:
|
|
||||||
forms = []
|
|
||||||
|
|
||||||
if application is None:
|
|
||||||
raise ValueError('Application section is required')
|
|
||||||
else:
|
|
||||||
self.application = application
|
|
||||||
|
|
||||||
self.context = legacy.create_context()
|
|
||||||
self.context['?service'] = self
|
|
||||||
yaql_functions.register(self.context)
|
|
||||||
|
|
||||||
params = parameters or {}
|
|
||||||
self.parameters = {}
|
|
||||||
for k, v in six.iteritems(params):
|
|
||||||
if not k or not k[0].isalpha():
|
|
||||||
continue
|
|
||||||
v = helpers.evaluate(v, self.context)
|
|
||||||
self.parameters[k] = v
|
|
||||||
self.context[k] = v
|
|
||||||
|
|
||||||
self.forms = []
|
|
||||||
for key, value in six.iteritems(kwargs):
|
|
||||||
setattr(self, key, value)
|
|
||||||
|
|
||||||
for form in forms:
|
|
||||||
name, field_specs, validators = self.extract_form_data(form)
|
|
||||||
# NOTE(kzaitsev) should be str (not unicode) under python2
|
|
||||||
# however it also works as str under python3
|
|
||||||
name = helpers.to_str(name)
|
|
||||||
self._add_form(name, field_specs, validators)
|
|
||||||
|
|
||||||
# Add ManageWorkflowForm
|
|
||||||
workflow_form = catalog_forms.WorkflowManagementForm()
|
|
||||||
if semantic_version.Version.coerce(self.spec_version) >= \
|
|
||||||
semantic_version.Version.coerce('2.2'):
|
|
||||||
app_name_field = workflow_form.name_field(fqn)
|
|
||||||
workflow_form.field_specs.insert(0, app_name_field)
|
|
||||||
|
|
||||||
self._add_form(workflow_form.name,
|
|
||||||
workflow_form.field_specs,
|
|
||||||
workflow_form.validators)
|
|
||||||
|
|
||||||
def _add_form(self, _name, _specs, _validators, _verbose_name=None):
|
|
||||||
import muranodashboard.dynamic_ui.forms as forms
|
|
||||||
|
|
||||||
class Form(six.with_metaclass(forms.DynamicFormMetaclass,
|
|
||||||
forms.ServiceConfigurationForm)):
|
|
||||||
service = self
|
|
||||||
name = _name
|
|
||||||
verbose_name = _verbose_name
|
|
||||||
field_specs = _specs
|
|
||||||
validators = _validators
|
|
||||||
|
|
||||||
self.forms.append(Form)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def extract_form_data(data):
|
|
||||||
for form_name, form_data in six.iteritems(data):
|
|
||||||
return form_name, form_data['fields'], form_data.get('validators',
|
|
||||||
[])
|
|
||||||
|
|
||||||
def extract_attributes(self):
|
|
||||||
context = self.context.create_child_context()
|
|
||||||
context['$'] = self.cleaned_data
|
|
||||||
context['$forms'] = self.cleaned_data
|
|
||||||
|
|
||||||
for name, template in six.iteritems(self.templates):
|
|
||||||
context[name] = template
|
|
||||||
if semantic_version.Version.coerce(self.spec_version) \
|
|
||||||
>= semantic_version.Version.coerce('2.2'):
|
|
||||||
management_form = catalog_forms.WF_MANAGEMENT_NAME
|
|
||||||
name = self.cleaned_data[management_form]['application_name']
|
|
||||||
self.application['?']['name'] = name
|
|
||||||
attributes = helpers.evaluate(self.application, context)
|
|
||||||
return attributes
|
|
||||||
|
|
||||||
def get_data(self, form_name, expr, data=None):
|
|
||||||
"""Try to get value from cleaned data, if none found, use raw data."""
|
|
||||||
if data:
|
|
||||||
self.update_cleaned_data(data, form_name=form_name)
|
|
||||||
data = self.cleaned_data
|
|
||||||
return expr.evaluate(data=data, context=self.context)
|
|
||||||
|
|
||||||
def update_cleaned_data(self, data, form=None, form_name=None):
|
|
||||||
form_name = form_name or form.__class__.__name__
|
|
||||||
if data:
|
|
||||||
self.cleaned_data[form_name] = data
|
|
||||||
return self.cleaned_data
|
|
||||||
|
|
||||||
def set_data(self, data):
|
|
||||||
self.cleaned_data = data
|
|
||||||
|
|
||||||
|
|
||||||
def get_apps_data(request):
|
|
||||||
return request.session.setdefault('apps_data', {})
|
|
||||||
|
|
||||||
|
|
||||||
def import_app(request, app_id):
|
|
||||||
app_data = get_apps_data(request).setdefault(app_id, {})
|
|
||||||
|
|
||||||
ui_desc = pkg_api.get_app_ui(request, app_id)
|
|
||||||
fqn = pkg_api.get_app_fqn(request, app_id)
|
|
||||||
LOG.debug('Using data {0} for app {1}'.format(app_data, fqn))
|
|
||||||
app_version = ui_desc.pop('Version', version.LATEST_FORMAT_VERSION)
|
|
||||||
version.check_version(app_version)
|
|
||||||
service = dict(
|
|
||||||
(helpers.decamelize(k), v) for (k, v) in six.iteritems(ui_desc))
|
|
||||||
parameters = service.pop('parameters', {})
|
|
||||||
parameters_source = service.pop('parameters_source', None)
|
|
||||||
if parameters_source is not None:
|
|
||||||
parts = parameters_source.rsplit('.', 1)
|
|
||||||
if 2 >= len(parts) > 0:
|
|
||||||
if len(parts) == 2:
|
|
||||||
class_name, method_name = parts
|
|
||||||
else:
|
|
||||||
method_name = parts[0]
|
|
||||||
class_name = service.get('application', {}).get('?', {}).get(
|
|
||||||
'type', fqn)
|
|
||||||
|
|
||||||
details = pkg_api.get_package_details(request, app_id)
|
|
||||||
pkg_version = getattr(details, 'version', '*')
|
|
||||||
request_body = {
|
|
||||||
'className': class_name,
|
|
||||||
'methodName': method_name,
|
|
||||||
'packageName': fqn,
|
|
||||||
'classVersion': pkg_version,
|
|
||||||
'parameters': {}
|
|
||||||
}
|
|
||||||
|
|
||||||
result = api.muranoclient(request).static_actions.call(
|
|
||||||
request_body).get_result()
|
|
||||||
if result and isinstance(result, dict):
|
|
||||||
parameters.update(result)
|
|
||||||
|
|
||||||
return Service(app_data, app_version, fqn, parameters=parameters,
|
|
||||||
**service)
|
|
||||||
|
|
||||||
|
|
||||||
def condition_getter(request, kwargs):
|
|
||||||
"""Define wizard conditional dictionary.
|
|
||||||
|
|
||||||
This function generates conditional dictionary for application creation
|
|
||||||
wizard. The last form of the wizard may be a management form, that
|
|
||||||
is provided by murano, not by a user. But in some cases this field
|
|
||||||
should be hidden. So here all situations are proceeded.
|
|
||||||
Management form may contain the following fields:
|
|
||||||
* continue adding applications chechkbox
|
|
||||||
Hidden, when user adds an app from 'quick deploy' and from
|
|
||||||
the other form (while creating depending app with '+' sign
|
|
||||||
* automatic inserted name
|
|
||||||
Hidden, if app version not higher then 2.0
|
|
||||||
|
|
||||||
So if both fields should not be shown - the management form is hidden.
|
|
||||||
"""
|
|
||||||
def _func(wizard):
|
|
||||||
# Get last key in OrderDict
|
|
||||||
last_step = next(reversed(wizard.form_list))
|
|
||||||
app_spec_version = wizard.form_list[last_step].service.spec_version
|
|
||||||
hide_stay_at_catalog_dialog = wizard.get_wizard_flag('drop_wm_form')
|
|
||||||
# Hide management form if version is old and additional dialog should
|
|
||||||
# not be shown
|
|
||||||
if not semantic_version.Version.coerce(app_spec_version) >= \
|
|
||||||
semantic_version.Version.coerce('2.2')\
|
|
||||||
and hide_stay_at_catalog_dialog:
|
|
||||||
return False
|
|
||||||
last_form_fields = wizard.form_list[last_step].base_fields
|
|
||||||
# If version is old, do not ask for app name
|
|
||||||
if not semantic_version.Version.coerce(app_spec_version) >= \
|
|
||||||
semantic_version.Version.coerce('2.2'):
|
|
||||||
if 'application_name' in last_form_fields.keys():
|
|
||||||
del last_form_fields['application_name']
|
|
||||||
# If workflow checkbox is not needed, remove it
|
|
||||||
if hide_stay_at_catalog_dialog:
|
|
||||||
if 'stay_at_the_catalog' in last_form_fields.keys():
|
|
||||||
del last_form_fields['stay_at_the_catalog']
|
|
||||||
return True
|
|
||||||
|
|
||||||
app = import_app(request, kwargs['app_id'])
|
|
||||||
key = force_text(_get_form_name(len(app.forms) - 1, app.forms[-1]()))
|
|
||||||
|
|
||||||
return {key: _func}
|
|
||||||
|
|
||||||
|
|
||||||
def _get_form_name(i, form, step_tmpl='Step {0}'):
|
|
||||||
name = form.verbose_name
|
|
||||||
return step_tmpl.format(i + 1) if name is None else name
|
|
||||||
|
|
||||||
|
|
||||||
def get_app_forms(request, kwargs):
|
|
||||||
app = import_app(request, kwargs.get('app_id'))
|
|
||||||
|
|
||||||
def get_form_name(i, form):
|
|
||||||
return _get_form_name(i, form, _('Step {0}'))
|
|
||||||
|
|
||||||
step_names = [get_form_name(*pair) for pair in enumerate(app.forms)]
|
|
||||||
return list(zip(step_names, app.forms))
|
|
||||||
|
|
||||||
|
|
||||||
def service_type_from_id(service_id):
|
|
||||||
match = re.match('(.*)-[0-9]+', service_id)
|
|
||||||
if match:
|
|
||||||
return match.group(1)
|
|
||||||
else: # if no number suffix found, it was service_type itself passed in
|
|
||||||
return service_id
|
|
||||||
|
|
||||||
|
|
||||||
def get_app_field_descriptions(request, app_id, index):
|
|
||||||
app = import_app(request, app_id)
|
|
||||||
|
|
||||||
form_cls = app.forms[index]
|
|
||||||
descriptions = []
|
|
||||||
no_field_descriptions = []
|
|
||||||
for name, field in six.iteritems(form_cls.base_fields):
|
|
||||||
title = field.description_title
|
|
||||||
description = field.description
|
|
||||||
if description:
|
|
||||||
if field.widget.is_hidden:
|
|
||||||
no_field_descriptions.extend([description, title])
|
|
||||||
else:
|
|
||||||
descriptions.append((name, title, description))
|
|
||||||
return descriptions, no_field_descriptions
|
|
|
@ -1,37 +0,0 @@
|
||||||
# Copyright (c) 2014 Mirantis, Inc.
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
||||||
# not use this file except in compliance with the License. You may obtain
|
|
||||||
# a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
||||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
||||||
# License for the specific language governing permissions and limitations
|
|
||||||
# under the License.
|
|
||||||
|
|
||||||
import semantic_version
|
|
||||||
|
|
||||||
LATEST_FORMAT_VERSION = '2.4'
|
|
||||||
|
|
||||||
|
|
||||||
def check_version(version):
|
|
||||||
latest = get_latest_version()
|
|
||||||
supported = semantic_version.Version(str(latest.major), partial=True)
|
|
||||||
requested = semantic_version.Version.coerce(str(version))
|
|
||||||
if supported != requested:
|
|
||||||
msg = 'Unsupported Dynamic UI format version: ' \
|
|
||||||
'requested format version {0} is not compatible with the ' \
|
|
||||||
'supported family {1}'
|
|
||||||
raise ValueError(msg.format(requested, supported))
|
|
||||||
if requested > latest:
|
|
||||||
msg = 'Unsupported Dynamic UI format version: ' \
|
|
||||||
'requested format version {0} is newer than ' \
|
|
||||||
'latest supported {1}'
|
|
||||||
raise ValueError(msg.format(requested, latest))
|
|
||||||
|
|
||||||
|
|
||||||
def get_latest_version():
|
|
||||||
return semantic_version.Version.coerce(LATEST_FORMAT_VERSION)
|
|
|
@ -1,61 +0,0 @@
|
||||||
# Copyright (c) 2014 Mirantis, Inc.
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
||||||
# not use this file except in compliance with the License. You may obtain
|
|
||||||
# a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
||||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
||||||
# License for the specific language governing permissions and limitations
|
|
||||||
# under the License.
|
|
||||||
|
|
||||||
import re
|
|
||||||
import six
|
|
||||||
|
|
||||||
import yaql
|
|
||||||
from yaql.language import exceptions as yaql_exc
|
|
||||||
|
|
||||||
|
|
||||||
def _set_up_yaql():
|
|
||||||
legacy_engine_options = {
|
|
||||||
'yaql.limitIterators': 10000,
|
|
||||||
'yaql.memoryQuota': 1000000
|
|
||||||
}
|
|
||||||
return yaql.YaqlFactory().create(options=legacy_engine_options)
|
|
||||||
|
|
||||||
YAQL = _set_up_yaql()
|
|
||||||
|
|
||||||
|
|
||||||
class YaqlExpression(object):
|
|
||||||
def __init__(self, expression):
|
|
||||||
self._expression = str(expression)
|
|
||||||
self._parsed_expression = YAQL(self._expression)
|
|
||||||
|
|
||||||
def expression(self):
|
|
||||||
return self._expression
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return 'YAQL(%s)' % self._expression
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return self._expression
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def match(expr):
|
|
||||||
if not isinstance(expr, six.string_types):
|
|
||||||
return False
|
|
||||||
if re.match('^[\s\w\d.:]*$', expr):
|
|
||||||
return False
|
|
||||||
try:
|
|
||||||
YAQL(expr)
|
|
||||||
return True
|
|
||||||
except yaql_exc.YaqlGrammarException:
|
|
||||||
return False
|
|
||||||
except yaql_exc.YaqlLexicalException:
|
|
||||||
return False
|
|
||||||
|
|
||||||
def evaluate(self, data=yaql.utils.NO_VALUE, context=None):
|
|
||||||
return self._parsed_expression.evaluate(data=data, context=context)
|
|
|
@ -1,109 +0,0 @@
|
||||||
# Copyright (c) 2013 Mirantis, Inc.
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
||||||
# not use this file except in compliance with the License. You may obtain
|
|
||||||
# a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
||||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
||||||
# License for the specific language governing permissions and limitations
|
|
||||||
# under the License.
|
|
||||||
|
|
||||||
import random
|
|
||||||
import string
|
|
||||||
import time
|
|
||||||
|
|
||||||
from yaql.language import specs
|
|
||||||
from yaql.language import yaqltypes
|
|
||||||
|
|
||||||
from muranodashboard.catalog import forms as catalog_forms
|
|
||||||
from muranodashboard.dynamic_ui import helpers
|
|
||||||
|
|
||||||
|
|
||||||
@specs.parameter('times', int)
|
|
||||||
def _repeat(context, template, times):
|
|
||||||
for i in range(times):
|
|
||||||
context['index'] = i + 1
|
|
||||||
yield helpers.evaluate(template, context)
|
|
||||||
|
|
||||||
|
|
||||||
_random_string_counter = None
|
|
||||||
|
|
||||||
|
|
||||||
@specs.parameter('pattern', yaqltypes.String())
|
|
||||||
@specs.parameter('number', int)
|
|
||||||
def _generate_hostname(pattern, number):
|
|
||||||
"""Generates hostname based on pattern
|
|
||||||
|
|
||||||
Replaces '#' char in pattern with supplied number, if no pattern is
|
|
||||||
supplied generates short and unique name for the host.
|
|
||||||
|
|
||||||
:param pattern: hostname pattern
|
|
||||||
:param number: number to replace with in pattern
|
|
||||||
:return: hostname
|
|
||||||
"""
|
|
||||||
global _random_string_counter
|
|
||||||
|
|
||||||
if pattern:
|
|
||||||
# NOTE(kzaitsev) works both for unicode and simple strings in py2
|
|
||||||
# and works as expected in py3
|
|
||||||
return pattern.replace('#', str(number))
|
|
||||||
|
|
||||||
counter = _random_string_counter or 1
|
|
||||||
# generate first 5 random chars
|
|
||||||
prefix = ''.join(random.choice(string.ascii_lowercase) for _ in range(5))
|
|
||||||
# convert timestamp to higher base to shorten hostname string
|
|
||||||
# (up to 8 chars)
|
|
||||||
timestamp = helpers.int2base(int(time.time() * 1000), 36)[:8]
|
|
||||||
# third part of random name up to 2 chars
|
|
||||||
# (1295 is last 2-digit number in base-36, 1296 is first 3-digit number)
|
|
||||||
suffix = helpers.int2base(counter, 36)
|
|
||||||
_random_string_counter = (counter + 1) % 1296
|
|
||||||
return prefix + timestamp + suffix
|
|
||||||
|
|
||||||
|
|
||||||
def _name(context):
|
|
||||||
name = context.get_data[
|
|
||||||
catalog_forms.WF_MANAGEMENT_NAME]['application_name']
|
|
||||||
return name
|
|
||||||
|
|
||||||
|
|
||||||
@specs.parameter('template_name', yaqltypes.String())
|
|
||||||
@specs.parameter('parameter_name', yaqltypes.String(nullable=True))
|
|
||||||
@specs.parameter('id_only', yaqltypes.PythonType(bool, nullable=True))
|
|
||||||
def _ref(context, template_name, parameter_name=None, id_only=None):
|
|
||||||
service = context['?service']
|
|
||||||
data = None
|
|
||||||
if not parameter_name:
|
|
||||||
parameter_name = template_name
|
|
||||||
# add special symbol to avoid collisions with regular parameters
|
|
||||||
# and prevent it from overwriting '?service' context variable
|
|
||||||
parameter_name = '#' + parameter_name
|
|
||||||
if parameter_name in service.parameters:
|
|
||||||
data = service.parameters[parameter_name]
|
|
||||||
elif template_name in service.templates:
|
|
||||||
data = helpers.evaluate(service.templates[template_name], context)
|
|
||||||
service.parameters[parameter_name] = data
|
|
||||||
if not isinstance(data, dict):
|
|
||||||
return None
|
|
||||||
if not isinstance(data.get('?', {}).get('id'), helpers.ObjectID):
|
|
||||||
data.setdefault('?', {})['id'] = helpers.ObjectID()
|
|
||||||
if id_only is None:
|
|
||||||
id_only = False
|
|
||||||
elif id_only is None:
|
|
||||||
id_only = True
|
|
||||||
|
|
||||||
if id_only:
|
|
||||||
return data['?']['id']
|
|
||||||
else:
|
|
||||||
return data
|
|
||||||
|
|
||||||
|
|
||||||
def register(context):
|
|
||||||
context.register_function(_repeat, 'repeat')
|
|
||||||
context.register_function(_generate_hostname, 'generateHostname')
|
|
||||||
context.register_function(_name, 'name')
|
|
||||||
context.register_function(_ref, 'ref')
|
|
|
@ -1,467 +0,0 @@
|
||||||
# Copyright (c) 2013 Mirantis, Inc.
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
||||||
# not use this file except in compliance with the License. You may obtain
|
|
||||||
# a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
||||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
||||||
# License for the specific language governing permissions and limitations
|
|
||||||
# under the License.
|
|
||||||
|
|
||||||
from django.utils.translation import ugettext_lazy as _
|
|
||||||
from horizon import exceptions
|
|
||||||
from oslo_log import log as logging
|
|
||||||
import six
|
|
||||||
|
|
||||||
from muranoclient.common import exceptions as exc
|
|
||||||
from muranodashboard import api
|
|
||||||
from muranodashboard.api import packages as packages_api
|
|
||||||
from muranodashboard.common import utils
|
|
||||||
from muranodashboard.environments import consts
|
|
||||||
from muranodashboard.environments import topology
|
|
||||||
|
|
||||||
|
|
||||||
KEY_ERROR_TEMPLATE = _(
|
|
||||||
"Error fetching the environment. The page may be rendered incorrectly. "
|
|
||||||
"Reason: %s")
|
|
||||||
LOG = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
def get_status_messages_for_service(request, service_id, environment_id):
|
|
||||||
client = api.muranoclient(request)
|
|
||||||
deployments = client.deployments.list(environment_id)
|
|
||||||
LOG.debug('Deployment::List {0}'.format(deployments))
|
|
||||||
|
|
||||||
result = '\n'
|
|
||||||
# TODO(efedorova): Add updated time to logs
|
|
||||||
if deployments:
|
|
||||||
for deployment in reversed(deployments):
|
|
||||||
reports = client.deployments.reports(
|
|
||||||
environment_id, deployment.id, service_id)
|
|
||||||
|
|
||||||
for report in reports:
|
|
||||||
result += utils.adjust_datestr(request, report.created) + ' - ' + \
|
|
||||||
report.text + '\n'
|
|
||||||
return result
|
|
||||||
|
|
||||||
|
|
||||||
def create_session(request, environment_id):
|
|
||||||
sessions = request.session.get('sessions', {})
|
|
||||||
id = api.muranoclient(request).sessions.configure(environment_id).id
|
|
||||||
sessions[environment_id] = id
|
|
||||||
request.session['sessions'] = sessions
|
|
||||||
return id
|
|
||||||
|
|
||||||
|
|
||||||
class Session(object):
|
|
||||||
@staticmethod
|
|
||||||
def get_or_create(request, environment_id):
|
|
||||||
"""Get an open session id
|
|
||||||
|
|
||||||
Gets id from already opened session for specified environment,
|
|
||||||
otherwise opens new session and returns its id
|
|
||||||
|
|
||||||
:param request:
|
|
||||||
:param environment_id:
|
|
||||||
:return: Session Id
|
|
||||||
"""
|
|
||||||
# We store opened sessions for each environment in dictionary per user
|
|
||||||
sessions = request.session.get('sessions', {})
|
|
||||||
|
|
||||||
if environment_id in sessions:
|
|
||||||
id = sessions[environment_id]
|
|
||||||
else:
|
|
||||||
id = create_session(request, environment_id)
|
|
||||||
return id
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_or_create_or_delete(request, environment_id):
|
|
||||||
"""Get an open session id
|
|
||||||
|
|
||||||
Gets id from session in open state for specified environment.
|
|
||||||
If state is deployed, then the session is deleted and a new one
|
|
||||||
is created. If there are no sessions, then a new one is created.
|
|
||||||
Returns id of chosen or created session.
|
|
||||||
|
|
||||||
:param request:
|
|
||||||
:param environment_id:
|
|
||||||
:return: Session id
|
|
||||||
"""
|
|
||||||
sessions = request.session.get('sessions', {})
|
|
||||||
client = api.muranoclient(request)
|
|
||||||
|
|
||||||
if environment_id in sessions:
|
|
||||||
id = sessions[environment_id]
|
|
||||||
try:
|
|
||||||
session_data = client.sessions.get(environment_id, id)
|
|
||||||
except exc.HTTPForbidden:
|
|
||||||
del sessions[environment_id]
|
|
||||||
LOG.debug("The environment is being deployed by other user. "
|
|
||||||
"Creating a new session "
|
|
||||||
"for the environment {0}".format(environment_id))
|
|
||||||
return create_session(request, environment_id)
|
|
||||||
else:
|
|
||||||
if session_data.state in [consts.STATUS_ID_DEPLOY_FAILURE,
|
|
||||||
consts.STATUS_ID_READY]:
|
|
||||||
del sessions[environment_id]
|
|
||||||
LOG.debug("The existing session has been already deployed."
|
|
||||||
" Creating a new session "
|
|
||||||
"for the environment {0}".format(environment_id))
|
|
||||||
return create_session(request, environment_id)
|
|
||||||
else:
|
|
||||||
LOG.debug("Creating a new session")
|
|
||||||
return create_session(request, environment_id)
|
|
||||||
LOG.debug("Found active session for the environment {0}"
|
|
||||||
.format(environment_id))
|
|
||||||
return id
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_if_available(request, environment_id):
|
|
||||||
"""Get an id of open session if it exists and is not in deployed state.
|
|
||||||
|
|
||||||
Returns None otherwise
|
|
||||||
"""
|
|
||||||
sessions = request.session.get('sessions', {})
|
|
||||||
client = api.muranoclient(request)
|
|
||||||
|
|
||||||
if environment_id in sessions:
|
|
||||||
id = sessions[environment_id]
|
|
||||||
try:
|
|
||||||
session_data = client.sessions.get(environment_id, id)
|
|
||||||
except exc.HTTPForbidden:
|
|
||||||
return None
|
|
||||||
else:
|
|
||||||
if session_data.state in [consts.STATUS_ID_DEPLOY_FAILURE,
|
|
||||||
consts.STATUS_ID_READY]:
|
|
||||||
return None
|
|
||||||
return id
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get(request, environment_id):
|
|
||||||
"""Get an open session id
|
|
||||||
|
|
||||||
Gets id from already opened session for specified environment,
|
|
||||||
otherwise returns None
|
|
||||||
|
|
||||||
:param request:
|
|
||||||
:param environment_id:
|
|
||||||
:return: Session Id
|
|
||||||
"""
|
|
||||||
# We store opened sessions for each environment in dictionary per user
|
|
||||||
sessions = request.session.get('sessions', {})
|
|
||||||
session_id = sessions.get(environment_id, '')
|
|
||||||
if session_id:
|
|
||||||
LOG.debug("Using session_id {0} for the environment {1}".format(
|
|
||||||
session_id, environment_id))
|
|
||||||
else:
|
|
||||||
LOG.debug("Session for the environment {0} not found".format(
|
|
||||||
environment_id))
|
|
||||||
return session_id
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def set(request, environment_id, session_id):
|
|
||||||
"""Set an open session id
|
|
||||||
|
|
||||||
sets id from already opened session for specified environment.
|
|
||||||
|
|
||||||
:param request:
|
|
||||||
:param environment_id:
|
|
||||||
:param session_id
|
|
||||||
"""
|
|
||||||
# We store opened sessions for each environment in dictionary per user
|
|
||||||
sessions = request.session.get('sessions', {})
|
|
||||||
sessions[environment_id] = session_id
|
|
||||||
request.session['sessions'] = sessions
|
|
||||||
|
|
||||||
|
|
||||||
def _update_env(env, request):
|
|
||||||
# TODO(vakovalchuk): optimize latest deployment when limit is available
|
|
||||||
deployments = deployments_list(request, env.id)
|
|
||||||
if deployments:
|
|
||||||
latest_deployment = deployments[0]
|
|
||||||
try:
|
|
||||||
deployed_services = {service['?']['id'] for service in
|
|
||||||
latest_deployment.description['services']}
|
|
||||||
except KeyError as e:
|
|
||||||
deployed_services = set()
|
|
||||||
exceptions.handle_recoverable(
|
|
||||||
request, KEY_ERROR_TEMPLATE % e.message)
|
|
||||||
else:
|
|
||||||
deployed_services = set()
|
|
||||||
|
|
||||||
if env.services:
|
|
||||||
try:
|
|
||||||
current_services = {service['?']['id'] for service in env.services}
|
|
||||||
except KeyError as e:
|
|
||||||
current_services = set()
|
|
||||||
exceptions.handle_recoverable(
|
|
||||||
request, KEY_ERROR_TEMPLATE % e.message)
|
|
||||||
else:
|
|
||||||
current_services = set()
|
|
||||||
|
|
||||||
env.has_new_services = current_services != deployed_services
|
|
||||||
|
|
||||||
if not env.has_new_services and env.status == consts.STATUS_ID_PENDING:
|
|
||||||
env.status = consts.STATUS_ID_READY
|
|
||||||
|
|
||||||
if not env.has_new_services and env.version == 0:
|
|
||||||
if env.status == consts.STATUS_ID_READY:
|
|
||||||
env.status = consts.STATUS_ID_NEW
|
|
||||||
return env
|
|
||||||
|
|
||||||
|
|
||||||
def environments_list(request):
|
|
||||||
environments = []
|
|
||||||
client = api.muranoclient(request)
|
|
||||||
with api.handled_exceptions(request):
|
|
||||||
environments = client.environments.list()
|
|
||||||
LOG.debug('Environment::List {0}'.format(environments))
|
|
||||||
for index, env in enumerate(environments):
|
|
||||||
environments[index] = environment_get(request, env.id)
|
|
||||||
|
|
||||||
return environments
|
|
||||||
|
|
||||||
|
|
||||||
def environment_create(request, parameters):
|
|
||||||
# name is required param
|
|
||||||
body = {'name': parameters['name']}
|
|
||||||
if 'defaultNetworks' in parameters:
|
|
||||||
body['defaultNetworks'] = parameters['defaultNetworks']
|
|
||||||
env = api.muranoclient(request).environments.create(body)
|
|
||||||
LOG.debug('Environment::Create {0}'.format(env))
|
|
||||||
return env
|
|
||||||
|
|
||||||
|
|
||||||
def environment_delete(request, environment_id, abandon=False):
|
|
||||||
action = 'Abandon' if abandon else 'Delete'
|
|
||||||
LOG.debug('Environment::{0} <Id : {1}>'.format(action, environment_id))
|
|
||||||
return api.muranoclient(request).environments.delete(
|
|
||||||
environment_id, abandon)
|
|
||||||
|
|
||||||
|
|
||||||
def environment_get(request, environment_id):
|
|
||||||
session_id = Session.get(request, environment_id)
|
|
||||||
LOG.debug('Environment::Get <Id: {0}, SessionId: {1}>'.
|
|
||||||
format(environment_id, session_id))
|
|
||||||
client = api.muranoclient(request)
|
|
||||||
env = client.environments.get(environment_id, session_id)
|
|
||||||
acquired = getattr(env, 'acquired_by', None)
|
|
||||||
if acquired and acquired != session_id:
|
|
||||||
env = client.environments.get(environment_id, acquired)
|
|
||||||
Session.set(request, environment_id, acquired)
|
|
||||||
|
|
||||||
env = _update_env(env, request)
|
|
||||||
|
|
||||||
LOG.debug('Environment::Get {0}'.format(env))
|
|
||||||
return env
|
|
||||||
|
|
||||||
|
|
||||||
def environment_deploy(request, environment_id):
|
|
||||||
session_id = Session.get_or_create_or_delete(request, environment_id)
|
|
||||||
LOG.debug('Session::Get <Id: {0}>'.format(session_id))
|
|
||||||
env = api.muranoclient(request).sessions.deploy(environment_id, session_id)
|
|
||||||
LOG.debug('Environment::Deploy <EnvId: {0}, SessionId: {1}>'
|
|
||||||
''.format(environment_id, session_id))
|
|
||||||
return env
|
|
||||||
|
|
||||||
|
|
||||||
def environment_update(request, environment_id, name):
|
|
||||||
return api.muranoclient(request).environments.update(environment_id, name)
|
|
||||||
|
|
||||||
|
|
||||||
def action_allowed(request, environment_id):
|
|
||||||
env = environment_get(request, environment_id)
|
|
||||||
status = getattr(env, 'status', None)
|
|
||||||
return status not in ('deploying',)
|
|
||||||
|
|
||||||
|
|
||||||
def services_list(request, environment_id):
|
|
||||||
"""Get environment applications.
|
|
||||||
|
|
||||||
This function collects data from Murano API and modifies it only for
|
|
||||||
dashboard purposes. Those changes don't impact application
|
|
||||||
deployment parameters.
|
|
||||||
"""
|
|
||||||
def strip(msg, to=100):
|
|
||||||
return u'%s...' % msg[:to] if len(msg) > to else msg
|
|
||||||
|
|
||||||
services = []
|
|
||||||
# need to create new session to see services deployed by other user
|
|
||||||
session_id = Session.get(request, environment_id)
|
|
||||||
|
|
||||||
get_environment = api.muranoclient(request).environments.get
|
|
||||||
environment = get_environment(environment_id, session_id)
|
|
||||||
try:
|
|
||||||
client = api.muranoclient(request)
|
|
||||||
reports = client.environments.last_status(environment_id, session_id)
|
|
||||||
except exc.HTTPNotFound:
|
|
||||||
LOG.exception(_('Could not retrieve latest status for '
|
|
||||||
'the {0} environment').format(environment_id))
|
|
||||||
reports = {}
|
|
||||||
|
|
||||||
for service_item in environment.services or []:
|
|
||||||
service_data = service_item
|
|
||||||
try:
|
|
||||||
service_id = service_data['?']['id']
|
|
||||||
except KeyError as e:
|
|
||||||
exceptions.handle_recoverable(
|
|
||||||
request, KEY_ERROR_TEMPLATE % e.message)
|
|
||||||
continue
|
|
||||||
|
|
||||||
if service_id in reports and reports[service_id]:
|
|
||||||
last_operation = strip(reports[service_id].text)
|
|
||||||
time = reports[service_id].updated
|
|
||||||
else:
|
|
||||||
last_operation = 'Component draft created' \
|
|
||||||
if environment.version == 0 else ''
|
|
||||||
try:
|
|
||||||
time = service_data['updated'][:-7]
|
|
||||||
except KeyError:
|
|
||||||
time = None
|
|
||||||
|
|
||||||
service_data['environment_id'] = environment_id
|
|
||||||
service_data['environment_version'] = environment.version
|
|
||||||
service_data['operation'] = last_operation
|
|
||||||
service_data['operation_updated'] = time
|
|
||||||
if service_data['?'].get('name'):
|
|
||||||
service_data['name'] = service_data['?']['name']
|
|
||||||
if (consts.DASHBOARD_ATTRS_KEY not in service_data['?'] or
|
|
||||||
not service_data['?'][consts.DASHBOARD_ATTRS_KEY].get('name')):
|
|
||||||
try:
|
|
||||||
fqn = service_data['?']['type']
|
|
||||||
except KeyError as e:
|
|
||||||
exceptions.handle_recoverable(
|
|
||||||
request, KEY_ERROR_TEMPLATE % e.message)
|
|
||||||
continue
|
|
||||||
version = None
|
|
||||||
if '/' in fqn:
|
|
||||||
version, fqn = fqn.split('/')[1].split('@')
|
|
||||||
pkg = packages_api.app_by_fqn(request, fqn, version=version)
|
|
||||||
if pkg:
|
|
||||||
app_name = pkg.name
|
|
||||||
storage = service_data['?'].setdefault(
|
|
||||||
consts.DASHBOARD_ATTRS_KEY, {})
|
|
||||||
storage['name'] = app_name
|
|
||||||
|
|
||||||
services.append(service_data)
|
|
||||||
|
|
||||||
LOG.debug('Service::List')
|
|
||||||
return [utils.Bunch(**service) for service in services]
|
|
||||||
|
|
||||||
|
|
||||||
def service_list_by_fqns(request, environment_id, fqns):
|
|
||||||
if environment_id is None:
|
|
||||||
return []
|
|
||||||
services = services_list(request, environment_id)
|
|
||||||
LOG.debug('Service::Instances::List')
|
|
||||||
try:
|
|
||||||
services = [service for service in services
|
|
||||||
if service['?']['type'].split('/')[0] in fqns]
|
|
||||||
except KeyError as e:
|
|
||||||
services = []
|
|
||||||
exceptions.handle_recoverable(request, KEY_ERROR_TEMPLATE % e.message)
|
|
||||||
|
|
||||||
return services
|
|
||||||
|
|
||||||
|
|
||||||
def service_create(request, environment_id, parameters):
|
|
||||||
# We should be able to delete session if we want to add new services to
|
|
||||||
# this environment.
|
|
||||||
session_id = Session.get_or_create_or_delete(request, environment_id)
|
|
||||||
LOG.debug('Service::Create {0}'.format(parameters['?']['type']))
|
|
||||||
return api.muranoclient(request).services.post(environment_id,
|
|
||||||
path='/',
|
|
||||||
data=parameters,
|
|
||||||
session_id=session_id)
|
|
||||||
|
|
||||||
|
|
||||||
def service_delete(request, environment_id, service_id):
|
|
||||||
LOG.debug('Service::Delete <SrvId: {0}>'.format(service_id))
|
|
||||||
session_id = Session.get_or_create_or_delete(request, environment_id)
|
|
||||||
return api.muranoclient(request).services.delete(environment_id,
|
|
||||||
'/' + service_id,
|
|
||||||
session_id)
|
|
||||||
|
|
||||||
|
|
||||||
def service_get(request, environment_id, service_id):
|
|
||||||
services = services_list(request, environment_id)
|
|
||||||
LOG.debug("Return service detail for a specified id")
|
|
||||||
try:
|
|
||||||
for service in services:
|
|
||||||
if service['?']['id'] == service_id:
|
|
||||||
return service
|
|
||||||
except KeyError as e:
|
|
||||||
exceptions.handle_recoverable(request, KEY_ERROR_TEMPLATE % e.message)
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def extract_actions_list(service):
|
|
||||||
actions_data = service['?'].get('_actions', {})
|
|
||||||
|
|
||||||
def make_action_datum(action_id, _action):
|
|
||||||
return dict(_action.items() + [('id', action_id)])
|
|
||||||
|
|
||||||
return [make_action_datum(_id, action) for (_id, action) in
|
|
||||||
six.iteritems(actions_data) if action.get('enabled')]
|
|
||||||
|
|
||||||
|
|
||||||
def run_action(request, environment_id, action_id):
|
|
||||||
mc = api.muranoclient(request)
|
|
||||||
return mc.actions.call(environment_id, action_id)
|
|
||||||
|
|
||||||
|
|
||||||
def deployments_list(request, environment_id):
|
|
||||||
LOG.debug('Deployments::List')
|
|
||||||
deployments = api.muranoclient(request).deployments.list(environment_id)
|
|
||||||
|
|
||||||
LOG.debug('Environment::List {0}'.format(deployments))
|
|
||||||
return deployments
|
|
||||||
|
|
||||||
|
|
||||||
def deployment_history(request):
|
|
||||||
LOG.debug('Deployment::History')
|
|
||||||
deployment_history = api.muranoclient(request).deployments.list(
|
|
||||||
None, all_environments=True)
|
|
||||||
|
|
||||||
for deployment in deployment_history:
|
|
||||||
reports = deployment_reports(request, deployment.environment_id,
|
|
||||||
deployment.id)
|
|
||||||
deployment.reports = reports
|
|
||||||
|
|
||||||
LOG.debug('Deployment::History {0}'.format(deployment_history))
|
|
||||||
return deployment_history
|
|
||||||
|
|
||||||
|
|
||||||
def deployment_reports(request, environment_id, deployment_id):
|
|
||||||
LOG.debug('Deployment::Reports::List')
|
|
||||||
reports = api.muranoclient(request).deployments.reports(environment_id,
|
|
||||||
deployment_id)
|
|
||||||
LOG.debug('Deployment::Reports::List {0}'.format(reports))
|
|
||||||
return reports
|
|
||||||
|
|
||||||
|
|
||||||
def get_deployment_start(request, environment_id, deployment_id):
|
|
||||||
deployments = api.muranoclient(request).deployments.list(environment_id)
|
|
||||||
LOG.debug('Get deployment start time')
|
|
||||||
for deployment in deployments:
|
|
||||||
if deployment.id == deployment_id:
|
|
||||||
return utils.adjust_datestr(request, deployment.started)
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def get_deployment_descr(request, environment_id, deployment_id):
|
|
||||||
deployments = api.muranoclient(request).deployments.list(environment_id)
|
|
||||||
LOG.debug('Get deployment description')
|
|
||||||
for deployment in deployments:
|
|
||||||
if deployment.id == deployment_id:
|
|
||||||
return deployment.description
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def load_environment_data(request, environment_id):
|
|
||||||
environment = environment_get(request, environment_id)
|
|
||||||
return topology.render_d3_data(request, environment)
|
|
|
@ -1,98 +0,0 @@
|
||||||
# Copyright (c) 2013 Mirantis, Inc.
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
||||||
# not use this file except in compliance with the License. You may obtain
|
|
||||||
# a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
||||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
||||||
# License for the specific language governing permissions and limitations
|
|
||||||
# under the License.
|
|
||||||
|
|
||||||
import os
|
|
||||||
import tempfile
|
|
||||||
|
|
||||||
from django.conf import settings
|
|
||||||
from django.utils.translation import ugettext_lazy as _
|
|
||||||
|
|
||||||
|
|
||||||
# ---- Metadata Consts ---- #
|
|
||||||
CHUNK_SIZE = 1 << 20 # 1MB
|
|
||||||
ARCHIVE_PKG_NAME = 'archive.tar.gz'
|
|
||||||
CACHE_DIR = getattr(settings, 'METADATA_CACHE_DIR',
|
|
||||||
os.path.join(tempfile.gettempdir(),
|
|
||||||
'muranodashboard-cache'))
|
|
||||||
|
|
||||||
CACHE_REFRESH_SECONDS_INTERVAL = 5
|
|
||||||
|
|
||||||
DASHBOARD_ATTRS_KEY = '_26411a1861294160833743e45d0eaad9'
|
|
||||||
|
|
||||||
# ---- Forms Consts ---- #
|
|
||||||
STATUS_ID_READY = 'ready'
|
|
||||||
STATUS_ID_PENDING = 'pending'
|
|
||||||
STATUS_ID_DEPLOYING = 'deploying'
|
|
||||||
STATUS_ID_DELETING = 'deleting'
|
|
||||||
STATUS_ID_DELETE_FAILURE = 'delete failure'
|
|
||||||
STATUS_ID_DEPLOY_FAILURE = 'deploy failure'
|
|
||||||
STATUS_ID_NEW = 'new'
|
|
||||||
|
|
||||||
NO_ACTION_ALLOWED_STATUSES = (STATUS_ID_DEPLOYING,
|
|
||||||
STATUS_ID_DELETING)
|
|
||||||
|
|
||||||
DEP_STATUS_ID_RUNNING = 'running'
|
|
||||||
DEP_STATUS_ID_RUNNING_W_ERRORS = 'running_w_errors'
|
|
||||||
DEP_STATUS_ID_RUNNING_W_WARNINGS = 'running_w_warnings'
|
|
||||||
DEP_STATUS_ID_COMPLETED_W_ERRORS = 'completed_w_errors'
|
|
||||||
DEP_STATUS_ID_COMPLETED_W_WARNINGS = 'completed_w_warnings'
|
|
||||||
DEP_STATUS_ID_SUCCESS = 'success'
|
|
||||||
|
|
||||||
# A tuple of tuples representing the possible data values for the
|
|
||||||
# status column and their associated boolean equivalent. Positive
|
|
||||||
# states should equate to ``True``, negative states should equate
|
|
||||||
# to ``False``, and indeterminate states should be ``None``.
|
|
||||||
# When value is None progress bar will be displayed.
|
|
||||||
|
|
||||||
STATUS_CHOICES = (
|
|
||||||
(None, True),
|
|
||||||
(STATUS_ID_READY, True),
|
|
||||||
(STATUS_ID_PENDING, True),
|
|
||||||
(STATUS_ID_DEPLOYING, None),
|
|
||||||
(STATUS_ID_DELETING, None),
|
|
||||||
(STATUS_ID_NEW, True),
|
|
||||||
(STATUS_ID_DELETE_FAILURE, False),
|
|
||||||
(STATUS_ID_DEPLOY_FAILURE, False),
|
|
||||||
)
|
|
||||||
|
|
||||||
DEPLOYMENT_STATUS_CHOICES = (
|
|
||||||
(None, True),
|
|
||||||
(DEP_STATUS_ID_RUNNING, True),
|
|
||||||
(DEP_STATUS_ID_SUCCESS, True),
|
|
||||||
(DEP_STATUS_ID_RUNNING_W_ERRORS, False),
|
|
||||||
(DEP_STATUS_ID_RUNNING_W_WARNINGS, False),
|
|
||||||
(DEP_STATUS_ID_COMPLETED_W_WARNINGS, False),
|
|
||||||
(DEP_STATUS_ID_COMPLETED_W_ERRORS, False),
|
|
||||||
)
|
|
||||||
|
|
||||||
STATUS_DISPLAY_CHOICES = (
|
|
||||||
(STATUS_ID_READY, _('Ready')),
|
|
||||||
(STATUS_ID_DEPLOYING, _('Deploying')),
|
|
||||||
(STATUS_ID_DELETING, _('Deleting')),
|
|
||||||
(STATUS_ID_PENDING, _('Ready to deploy')),
|
|
||||||
(STATUS_ID_NEW, _('Ready to configure')),
|
|
||||||
(STATUS_ID_DELETE_FAILURE, _('Delete failure')),
|
|
||||||
(STATUS_ID_DEPLOY_FAILURE, _('Deploy failure')),
|
|
||||||
('', _('Ready to configure')),
|
|
||||||
)
|
|
||||||
|
|
||||||
DEPLOYMENT_STATUS_DISPLAY_CHOICES = (
|
|
||||||
(DEP_STATUS_ID_COMPLETED_W_ERRORS, _('Failed')),
|
|
||||||
(DEP_STATUS_ID_COMPLETED_W_WARNINGS, _('Completed with warnings')),
|
|
||||||
(DEP_STATUS_ID_RUNNING, _('Running')),
|
|
||||||
(DEP_STATUS_ID_RUNNING_W_ERRORS, _('Running with errors')),
|
|
||||||
(DEP_STATUS_ID_RUNNING_W_WARNINGS, _('Running with warnings')),
|
|
||||||
(DEP_STATUS_ID_SUCCESS, _('Successful')),
|
|
||||||
('', _('Unknown')),
|
|
||||||
)
|
|
|
@ -1,98 +0,0 @@
|
||||||
# Copyright (c) 2013 Mirantis, Inc.
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
||||||
# not use this file except in compliance with the License. You may obtain
|
|
||||||
# a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
||||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
||||||
# License for the specific language governing permissions and limitations
|
|
||||||
# under the License.
|
|
||||||
import ast
|
|
||||||
|
|
||||||
from django.conf import settings
|
|
||||||
from django import forms
|
|
||||||
from django.utils.translation import ugettext_lazy as _
|
|
||||||
from horizon import exceptions
|
|
||||||
from horizon import forms as horizon_forms
|
|
||||||
from horizon import messages
|
|
||||||
from oslo_log import log as logging
|
|
||||||
|
|
||||||
from muranoclient.common import exceptions as exc
|
|
||||||
from muranodashboard.common import net
|
|
||||||
from muranodashboard.environments import api
|
|
||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
|
||||||
ENV_NAME_HELP_TEXT = _("Environment name must contain at least one "
|
|
||||||
"non-white space symbol.")
|
|
||||||
|
|
||||||
|
|
||||||
class CreateEnvironmentForm(horizon_forms.SelfHandlingForm):
|
|
||||||
name = forms.CharField(label=_("Environment Name"),
|
|
||||||
help_text=ENV_NAME_HELP_TEXT,
|
|
||||||
max_length=255)
|
|
||||||
|
|
||||||
net_config = forms.ChoiceField(
|
|
||||||
label=_("Environment Default Network"))
|
|
||||||
|
|
||||||
def __init__(self, request, *args, **kwargs):
|
|
||||||
super(CreateEnvironmentForm, self).__init__(request, *args, **kwargs)
|
|
||||||
env_fixed_network = getattr(settings, 'USE_FIXED_NETWORK', False)
|
|
||||||
if env_fixed_network:
|
|
||||||
net_choices = net.get_project_assigned_network(request)
|
|
||||||
help_text = None
|
|
||||||
if not net_choices:
|
|
||||||
msg = _("Default network is either not specified for "
|
|
||||||
"this project, or specified incorrectly, "
|
|
||||||
"please contact administrator.")
|
|
||||||
messages.error(request, msg)
|
|
||||||
raise exceptions.ConfigurationError(msg)
|
|
||||||
else:
|
|
||||||
self.fields['net_config'].required = False
|
|
||||||
self.fields['net_config'].widget.attrs['readonly'] = True
|
|
||||||
else:
|
|
||||||
net_choices = net.get_available_networks(
|
|
||||||
request,
|
|
||||||
murano_networks='translate')
|
|
||||||
|
|
||||||
if net_choices is None: # NovaNetwork case
|
|
||||||
net_choices = [((None, None), _('Unavailable'))]
|
|
||||||
help_text = net.NN_HELP
|
|
||||||
else:
|
|
||||||
net_choices.insert(0, ((None, None), _('Create New')))
|
|
||||||
help_text = net.NEUTRON_NET_HELP
|
|
||||||
self.fields['net_config'].choices = net_choices
|
|
||||||
self.fields['net_config'].help_text = help_text
|
|
||||||
|
|
||||||
def clean_name(self):
|
|
||||||
cleaned_data = super(CreateEnvironmentForm, self).clean()
|
|
||||||
env_name = cleaned_data.get('name')
|
|
||||||
if not env_name.strip():
|
|
||||||
self._errors['name'] = self.error_class([ENV_NAME_HELP_TEXT])
|
|
||||||
return env_name
|
|
||||||
|
|
||||||
def handle(self, request, data):
|
|
||||||
try:
|
|
||||||
net_config = ast.literal_eval(data.pop('net_config'))
|
|
||||||
if net_config[0] is not None:
|
|
||||||
data.update(net.generate_join_existing_net(net_config))
|
|
||||||
env = api.environment_create(request, data)
|
|
||||||
request.session['env_id'] = env.id
|
|
||||||
messages.success(request,
|
|
||||||
u'Created environment "{0}"'.format(data['name']))
|
|
||||||
return True
|
|
||||||
except exc.HTTPConflict:
|
|
||||||
msg = _('Environment with specified name already exists')
|
|
||||||
LOG.exception(msg)
|
|
||||||
exceptions.handle(request, ignore=True)
|
|
||||||
messages.error(request, msg)
|
|
||||||
return False
|
|
||||||
except Exception:
|
|
||||||
msg = _('Failed to create environment')
|
|
||||||
LOG.exception(msg)
|
|
||||||
exceptions.handle(request)
|
|
||||||
messages.error(request, msg)
|
|
||||||
return False
|
|
|
@ -1,22 +0,0 @@
|
||||||
# Copyright (c) 2013 Mirantis, Inc.
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
||||||
# not use this file except in compliance with the License. You may obtain
|
|
||||||
# a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
||||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
||||||
# License for the specific language governing permissions and limitations
|
|
||||||
# under the License.
|
|
||||||
|
|
||||||
from django.utils.translation import ugettext_lazy as _
|
|
||||||
import horizon
|
|
||||||
|
|
||||||
|
|
||||||
class Environments(horizon.Panel):
|
|
||||||
name = _("Environments")
|
|
||||||
slug = 'environments'
|
|
||||||
policy_rules = (("murano", "list_environments"),)
|
|
|
@ -1,757 +0,0 @@
|
||||||
# Copyright (c) 2013 Mirantis, Inc.
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
||||||
# not use this file except in compliance with the License. You may obtain
|
|
||||||
# a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
||||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
||||||
# License for the specific language governing permissions and limitations
|
|
||||||
# under the License.
|
|
||||||
|
|
||||||
import json
|
|
||||||
|
|
||||||
from django.core.urlresolvers import reverse
|
|
||||||
from django import http as django_http
|
|
||||||
from django import shortcuts
|
|
||||||
from django import template
|
|
||||||
from django.template import defaultfilters
|
|
||||||
from django.utils.translation import ugettext_lazy as _
|
|
||||||
from django.utils.translation import ungettext_lazy
|
|
||||||
|
|
||||||
from horizon import exceptions
|
|
||||||
from horizon import forms
|
|
||||||
from horizon import messages
|
|
||||||
from horizon import tables
|
|
||||||
from horizon.utils import filters
|
|
||||||
from muranoclient.common import exceptions as exc
|
|
||||||
from openstack_dashboard import policy
|
|
||||||
from oslo_log import log as logging
|
|
||||||
|
|
||||||
from muranodashboard import api as api_utils
|
|
||||||
from muranodashboard.api import packages as pkg_api
|
|
||||||
from muranodashboard.catalog import views as catalog_views
|
|
||||||
from muranodashboard.common import utils as md_utils
|
|
||||||
from muranodashboard.environments import api
|
|
||||||
from muranodashboard.environments import consts
|
|
||||||
from muranodashboard.packages import consts as pkg_consts
|
|
||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
def _get_environment_status_and_version(request, table):
|
|
||||||
environment_id = table.kwargs.get('environment_id')
|
|
||||||
env = api.environment_get(request, environment_id)
|
|
||||||
status = getattr(env, 'status', None)
|
|
||||||
version = getattr(env, 'version', None)
|
|
||||||
return status, version
|
|
||||||
|
|
||||||
|
|
||||||
def _check_row_actions_allowed(action, request):
|
|
||||||
envs = action.table.data
|
|
||||||
if not envs:
|
|
||||||
return False
|
|
||||||
for env in envs:
|
|
||||||
if action.allowed(request, env):
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def _environment_has_deployed_services(request, environment_id):
|
|
||||||
deployments = api.deployments_list(request, environment_id)
|
|
||||||
if not deployments:
|
|
||||||
return False
|
|
||||||
if not deployments[0].description['services']:
|
|
||||||
return False
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
class AddApplication(tables.LinkAction):
|
|
||||||
name = 'AddApplication'
|
|
||||||
verbose_name = _('Add Component')
|
|
||||||
icon = 'plus'
|
|
||||||
|
|
||||||
def allowed(self, request, environment):
|
|
||||||
status, version = _get_environment_status_and_version(request,
|
|
||||||
self.table)
|
|
||||||
return status not in consts.NO_ACTION_ALLOWED_STATUSES
|
|
||||||
|
|
||||||
def get_link_url(self, datum=None):
|
|
||||||
base_url = reverse('horizon:app-catalog:catalog:switch_env',
|
|
||||||
args=(self.table.kwargs['environment_id'],))
|
|
||||||
redirect_url = reverse('horizon:app-catalog:catalog:index')
|
|
||||||
return '{0}?next={1}'.format(base_url, redirect_url)
|
|
||||||
|
|
||||||
|
|
||||||
class CreateEnvironment(tables.LinkAction):
|
|
||||||
name = 'CreateEnvironment'
|
|
||||||
verbose_name = _('Create Environment')
|
|
||||||
url = 'horizon:app-catalog:environments:create_environment'
|
|
||||||
classes = ('btn-launch', 'add_env')
|
|
||||||
redirect_url = "horizon:app-catalog:environments:index"
|
|
||||||
icon = 'plus'
|
|
||||||
policy_rules = (("murano", "create_environment"),)
|
|
||||||
|
|
||||||
def allowed(self, request, datum):
|
|
||||||
return True if self.table.data else False
|
|
||||||
|
|
||||||
def action(self, request, environment):
|
|
||||||
try:
|
|
||||||
api.environment_create(request, environment)
|
|
||||||
except Exception as e:
|
|
||||||
msg = (_('Unable to create environment {0}'
|
|
||||||
' due to: {1}').format(environment, e))
|
|
||||||
LOG.error(msg)
|
|
||||||
redirect = reverse(self.redirect_url)
|
|
||||||
exceptions.handle(request, msg, redirect=redirect)
|
|
||||||
|
|
||||||
|
|
||||||
class DeploymentHistory(tables.LinkAction):
|
|
||||||
name = 'DeploymentHistory'
|
|
||||||
verbose_name = _('Deployment History')
|
|
||||||
url = 'horizon:app-catalog:environments:deployment_history'
|
|
||||||
classes = ('deployment-history')
|
|
||||||
redirect_url = "horizon:app-catalog:environments:index"
|
|
||||||
icon = 'history'
|
|
||||||
policy_rules = (("murano", "list_deployments_all_environments"),)
|
|
||||||
|
|
||||||
def allowed(self, request, datum):
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
class DeleteEnvironment(policy.PolicyTargetMixin, tables.DeleteAction):
|
|
||||||
redirect_url = "horizon:app-catalog:environments:index"
|
|
||||||
policy_rules = (("murano", "delete_environment"),)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def action_present(count):
|
|
||||||
return ungettext_lazy(
|
|
||||||
u"Delete Environment",
|
|
||||||
u"Delete Environments",
|
|
||||||
count
|
|
||||||
)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def action_past(count):
|
|
||||||
return ungettext_lazy(
|
|
||||||
u"Started Deleting Environment",
|
|
||||||
u"Started Deleting Environments",
|
|
||||||
count
|
|
||||||
)
|
|
||||||
|
|
||||||
def allowed(self, request, environment):
|
|
||||||
# table action case: action allowed if any row action allowed
|
|
||||||
if not environment:
|
|
||||||
return _check_row_actions_allowed(self, request)
|
|
||||||
|
|
||||||
# row action case
|
|
||||||
return environment.status not in (consts.STATUS_ID_DEPLOYING,
|
|
||||||
consts.STATUS_ID_DELETING)
|
|
||||||
|
|
||||||
def action(self, request, environment_id):
|
|
||||||
try:
|
|
||||||
api.environment_delete(request, environment_id)
|
|
||||||
except Exception as e:
|
|
||||||
msg = (_('Unable to delete environment {0}'
|
|
||||||
' due to: {1}').format(environment_id, e))
|
|
||||||
LOG.error(msg)
|
|
||||||
redirect = reverse(self.redirect_url)
|
|
||||||
exceptions.handle(request, msg, redirect=redirect)
|
|
||||||
|
|
||||||
|
|
||||||
class AbandonEnvironment(tables.DeleteAction):
|
|
||||||
help_text = _("This action cannot be undone. Any resources created by "
|
|
||||||
"this environment will have to be released manually.")
|
|
||||||
name = 'abandon'
|
|
||||||
redirect_url = "horizon:app-catalog:environments:index"
|
|
||||||
policy_rules = (("murano", "delete_environment"),)
|
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
|
||||||
super(AbandonEnvironment, self).__init__(**kwargs)
|
|
||||||
self.icon = 'stop'
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def action_present(count):
|
|
||||||
return ungettext_lazy(
|
|
||||||
u"Abandon Environment",
|
|
||||||
u"Abandon Environments",
|
|
||||||
count
|
|
||||||
)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def action_past(count):
|
|
||||||
return ungettext_lazy(
|
|
||||||
u"Abandoned Environment",
|
|
||||||
u"Abandoned Environments",
|
|
||||||
count
|
|
||||||
)
|
|
||||||
|
|
||||||
def allowed(self, request, environment):
|
|
||||||
"""Limit when 'Abandon Environment' button is shown
|
|
||||||
|
|
||||||
'Abandon Environment' button is hidden in several cases:
|
|
||||||
* environment is new
|
|
||||||
* app added to env, but not deploy is not started
|
|
||||||
"""
|
|
||||||
|
|
||||||
# table action case: action allowed if any row action allowed
|
|
||||||
if not environment:
|
|
||||||
return _check_row_actions_allowed(self, request)
|
|
||||||
|
|
||||||
# row action case
|
|
||||||
status = getattr(environment, 'status', None)
|
|
||||||
if status in [consts.STATUS_ID_NEW, consts.STATUS_ID_PENDING]:
|
|
||||||
return False
|
|
||||||
return True
|
|
||||||
|
|
||||||
def action(self, request, environment_id):
|
|
||||||
try:
|
|
||||||
api.environment_delete(request, environment_id, True)
|
|
||||||
except Exception as e:
|
|
||||||
msg = (_('Unable to abandon an environment {0}'
|
|
||||||
' due to: {1}').format(environment_id, e))
|
|
||||||
LOG.error(msg)
|
|
||||||
redirect = reverse(self.redirect_url)
|
|
||||||
exceptions.handle(request, msg, redirect=redirect)
|
|
||||||
|
|
||||||
|
|
||||||
class DeleteService(tables.DeleteAction):
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def action_present(count):
|
|
||||||
return ungettext_lazy(
|
|
||||||
u"Delete Component",
|
|
||||||
u"Delete Components",
|
|
||||||
count
|
|
||||||
)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def action_past(count):
|
|
||||||
return ungettext_lazy(
|
|
||||||
u"Started Deleting Component",
|
|
||||||
u"Started Deleting Components",
|
|
||||||
count
|
|
||||||
)
|
|
||||||
|
|
||||||
def allowed(self, request, service=None):
|
|
||||||
status, version = _get_environment_status_and_version(request,
|
|
||||||
self.table)
|
|
||||||
return status != consts.STATUS_ID_DEPLOYING
|
|
||||||
|
|
||||||
def action(self, request, service_id):
|
|
||||||
try:
|
|
||||||
environment_id = self.table.kwargs.get('environment_id')
|
|
||||||
for service in self.table.data:
|
|
||||||
if service['?']['id'] == service_id:
|
|
||||||
api.service_delete(request,
|
|
||||||
environment_id,
|
|
||||||
service_id)
|
|
||||||
except Exception:
|
|
||||||
msg = _('Sorry, you can\'t delete service right now')
|
|
||||||
redirect = reverse("horizon:app-catalog:environments:index")
|
|
||||||
exceptions.handle(request, msg, redirect=redirect)
|
|
||||||
|
|
||||||
|
|
||||||
class DeployEnvironment(tables.BatchAction):
|
|
||||||
name = 'deploy'
|
|
||||||
classes = ('btn-launch',)
|
|
||||||
icon = "play"
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def action_present_deploy(count):
|
|
||||||
return ungettext_lazy(
|
|
||||||
u"Deploy Environment",
|
|
||||||
u"Deploy Environments",
|
|
||||||
count
|
|
||||||
)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def action_past_deploy(count):
|
|
||||||
return ungettext_lazy(
|
|
||||||
u"Started deploying Environment",
|
|
||||||
u"Started deploying Environments",
|
|
||||||
count
|
|
||||||
)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def action_present_update(count):
|
|
||||||
return ungettext_lazy(
|
|
||||||
u"Update Environment",
|
|
||||||
# there can be cases when some of the envs are new and some are not
|
|
||||||
# so it is better to just leave "Deploy" for multiple envs
|
|
||||||
u"Deploy Environments",
|
|
||||||
count
|
|
||||||
)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def action_past_update(count):
|
|
||||||
return ungettext_lazy(
|
|
||||||
u"Updated Environment",
|
|
||||||
u"Deployed Environments",
|
|
||||||
count
|
|
||||||
)
|
|
||||||
|
|
||||||
action_present = action_present_deploy
|
|
||||||
action_past = action_past_deploy
|
|
||||||
|
|
||||||
def allowed(self, request, environment):
|
|
||||||
"""Limit when 'Deploy Environment' button is shown
|
|
||||||
|
|
||||||
'Deploy environment' is shown when set of environment's services
|
|
||||||
changed or previous deploy failed.
|
|
||||||
If environment has already deployed services,
|
|
||||||
button is shown as 'Update environment'
|
|
||||||
"""
|
|
||||||
|
|
||||||
# table action case: action allowed if any row action allowed
|
|
||||||
if not environment:
|
|
||||||
return _check_row_actions_allowed(self, request)
|
|
||||||
|
|
||||||
# row action case
|
|
||||||
if _environment_has_deployed_services(request, environment.id):
|
|
||||||
self.action_present = self.action_present_update
|
|
||||||
self.action_past = self.action_past_update
|
|
||||||
else:
|
|
||||||
self.action_present = self.action_present_deploy
|
|
||||||
self.action_past = self.action_past_deploy
|
|
||||||
|
|
||||||
status = getattr(environment, 'status', None)
|
|
||||||
if status in (consts.STATUS_ID_PENDING,
|
|
||||||
consts.STATUS_ID_DEPLOY_FAILURE):
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
def action(self, request, environment_id):
|
|
||||||
try:
|
|
||||||
api.environment_deploy(request, environment_id)
|
|
||||||
except Exception:
|
|
||||||
msg = _('Unable to deploy. Try again later')
|
|
||||||
redirect = reverse('horizon:app-catalog:environments:index')
|
|
||||||
exceptions.handle(request, msg, redirect=redirect)
|
|
||||||
|
|
||||||
|
|
||||||
class DeployThisEnvironment(tables.Action):
|
|
||||||
name = 'deploy_env'
|
|
||||||
verbose_name = _('Deploy This Environment')
|
|
||||||
requires_input = False
|
|
||||||
classes = ('btn-launch',)
|
|
||||||
icon = "play"
|
|
||||||
|
|
||||||
def allowed(self, request, service):
|
|
||||||
"""Limit when 'Deploy This Environment' button is shown
|
|
||||||
|
|
||||||
'Deploy environment' is not shown in several cases:
|
|
||||||
* when deploy is already in progress
|
|
||||||
* delete is in progress
|
|
||||||
* env was just created and no apps added
|
|
||||||
* previous deployment finished successfully
|
|
||||||
If environment has already deployed services, button is shown
|
|
||||||
as 'Update This Environment'
|
|
||||||
"""
|
|
||||||
environment_id = self.table.kwargs['environment_id']
|
|
||||||
if _environment_has_deployed_services(request, environment_id):
|
|
||||||
self.verbose_name = _('Update This Environment')
|
|
||||||
else:
|
|
||||||
self.verbose_name = _('Deploy This Environment')
|
|
||||||
|
|
||||||
status, version = _get_environment_status_and_version(request,
|
|
||||||
self.table)
|
|
||||||
if (status in consts.NO_ACTION_ALLOWED_STATUSES or
|
|
||||||
status == consts.STATUS_ID_READY):
|
|
||||||
return False
|
|
||||||
|
|
||||||
apps = self.table.data
|
|
||||||
if version == 0 and not apps:
|
|
||||||
return False
|
|
||||||
return True
|
|
||||||
|
|
||||||
def single(self, data_table, request, service_id):
|
|
||||||
environment_id = data_table.kwargs['environment_id']
|
|
||||||
try:
|
|
||||||
api.environment_deploy(request, environment_id)
|
|
||||||
messages.success(request, _('Deploy started'))
|
|
||||||
except Exception:
|
|
||||||
msg = _('Unable to deploy. Try again later')
|
|
||||||
exceptions.handle(
|
|
||||||
request, msg,
|
|
||||||
redirect=reverse('horizon:app-catalog:environments:index'))
|
|
||||||
return shortcuts.redirect(
|
|
||||||
reverse('horizon:app-catalog:environments:services',
|
|
||||||
args=(environment_id,)))
|
|
||||||
|
|
||||||
|
|
||||||
class ShowEnvironmentServices(tables.LinkAction):
|
|
||||||
name = 'show'
|
|
||||||
verbose_name = _('Manage Components')
|
|
||||||
url = 'horizon:app-catalog:environments:services'
|
|
||||||
|
|
||||||
def allowed(self, request, environment):
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
class UpdateEnvironmentRow(tables.Row):
|
|
||||||
ajax = True
|
|
||||||
|
|
||||||
def __init__(self, table, datum=None):
|
|
||||||
super(UpdateEnvironmentRow, self).__init__(table, datum)
|
|
||||||
if hasattr(datum, 'status'):
|
|
||||||
self.attrs['status'] = datum.status
|
|
||||||
|
|
||||||
def get_data(self, request, environment_id):
|
|
||||||
try:
|
|
||||||
return api.environment_get(request, environment_id)
|
|
||||||
except exc.HTTPNotFound:
|
|
||||||
# returning 404 to the ajax call removes the
|
|
||||||
# row from the table on the ui
|
|
||||||
raise django_http.Http404
|
|
||||||
except Exception:
|
|
||||||
# let our unified handler take care of errors here
|
|
||||||
with api_utils.handled_exceptions(request):
|
|
||||||
raise
|
|
||||||
|
|
||||||
|
|
||||||
class UpdateServiceRow(tables.Row):
|
|
||||||
ajax = True
|
|
||||||
|
|
||||||
def get_data(self, request, service_id):
|
|
||||||
environment_id = self.table.kwargs['environment_id']
|
|
||||||
return api.service_get(request, environment_id, service_id)
|
|
||||||
|
|
||||||
|
|
||||||
class UpdateName(tables.UpdateAction):
|
|
||||||
def allowed(self, request, environment, cell):
|
|
||||||
policy_rule = (("murano", "update_environment"),)
|
|
||||||
return policy.check(policy_rule, request)
|
|
||||||
|
|
||||||
def update_cell(self, request, datum, obj_id, cell_name, new_cell_value):
|
|
||||||
try:
|
|
||||||
if not new_cell_value or new_cell_value.isspace():
|
|
||||||
message = _("The environment name field cannot be empty.")
|
|
||||||
messages.warning(request, message)
|
|
||||||
raise ValueError(message)
|
|
||||||
mc = api_utils.muranoclient(request)
|
|
||||||
mc.environments.update(datum.id, name=new_cell_value)
|
|
||||||
except exc.HTTPConflict:
|
|
||||||
message = _("Couldn't update environment. Reason: This name is "
|
|
||||||
"already taken.")
|
|
||||||
messages.warning(request, message)
|
|
||||||
LOG.warning(message)
|
|
||||||
|
|
||||||
# FIXME(kzaitsev): There is a bug in horizon and inline error
|
|
||||||
# icons are missing. This means, that if we return 400 here, by
|
|
||||||
# raising django.core.exceptions.ValidationError(message) the UI
|
|
||||||
# will break a little. Until the bug is fixed this will raise 500
|
|
||||||
# bug link: https://bugs.launchpad.net/horizon/+bug/1359399
|
|
||||||
# Alternatively this could somehow raise 409, which would result
|
|
||||||
# in the same behaviour.
|
|
||||||
raise ValueError(message)
|
|
||||||
except Exception:
|
|
||||||
exceptions.handle(request, ignore=True)
|
|
||||||
return False
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
class UpdateEnvMetadata(tables.LinkAction):
|
|
||||||
name = "update_env_metadata"
|
|
||||||
verbose_name = _("Update Metadata")
|
|
||||||
ajax = False
|
|
||||||
icon = "pencil"
|
|
||||||
attrs = {"ng-controller": "MetadataModalHelperController as modal"}
|
|
||||||
|
|
||||||
def __init__(self, attrs=None, **kwargs):
|
|
||||||
kwargs['preempt'] = True
|
|
||||||
self.session_id = None
|
|
||||||
super(UpdateEnvMetadata, self).__init__(attrs, **kwargs)
|
|
||||||
|
|
||||||
def get_link_url(self, environment):
|
|
||||||
target = json.dumps({
|
|
||||||
'environment': environment.id,
|
|
||||||
'session': self.session_id
|
|
||||||
})
|
|
||||||
self.attrs['ng-click'] = (
|
|
||||||
"modal.openMetadataModal('muranoenv', %s, true)" % target)
|
|
||||||
return "javascript:void(0);"
|
|
||||||
|
|
||||||
def allowed(self, request, environment=None):
|
|
||||||
return environment.status not in (consts.STATUS_ID_DEPLOYING,
|
|
||||||
consts.STATUS_ID_DELETING)
|
|
||||||
|
|
||||||
def update(self, request, datum):
|
|
||||||
if datum:
|
|
||||||
env_id = datum.id
|
|
||||||
self.session_id = api.Session.get_if_available(request, env_id)
|
|
||||||
|
|
||||||
|
|
||||||
class EnvironmentsTable(tables.DataTable):
|
|
||||||
name = md_utils.Column(
|
|
||||||
'name',
|
|
||||||
link='horizon:app-catalog:environments:services',
|
|
||||||
verbose_name=_('Name'),
|
|
||||||
form_field=forms.CharField(required=False),
|
|
||||||
update_action=UpdateName)
|
|
||||||
|
|
||||||
status = tables.Column('status',
|
|
||||||
verbose_name=_('Status'),
|
|
||||||
status=True,
|
|
||||||
status_choices=consts.STATUS_CHOICES,
|
|
||||||
display_choices=consts.STATUS_DISPLAY_CHOICES)
|
|
||||||
|
|
||||||
def get_env_detail_link(self, environment):
|
|
||||||
# NOTE: using the policy check for show_environment
|
|
||||||
if policy.check((("murano", "show_environment"),),
|
|
||||||
self.request, target={"environment": environment}):
|
|
||||||
return reverse("horizon:app-catalog:environments:services",
|
|
||||||
args=(environment.id,))
|
|
||||||
return None
|
|
||||||
|
|
||||||
def __init__(self, request, data=None, needs_form_wrapper=None, **kwargs):
|
|
||||||
super(EnvironmentsTable,
|
|
||||||
self).__init__(request, data=data,
|
|
||||||
needs_form_wrapper=needs_form_wrapper,
|
|
||||||
**kwargs)
|
|
||||||
self.columns['name'].get_link_url = self.get_env_detail_link
|
|
||||||
|
|
||||||
class Meta(object):
|
|
||||||
name = 'environments'
|
|
||||||
verbose_name = _('Environments')
|
|
||||||
template = 'environments/_data_table.html'
|
|
||||||
row_class = UpdateEnvironmentRow
|
|
||||||
status_columns = ['status']
|
|
||||||
no_data_message = _('NO ENVIRONMENTS')
|
|
||||||
table_actions_menu = (AbandonEnvironment,
|
|
||||||
DeploymentHistory)
|
|
||||||
table_actions = (CreateEnvironment, DeployEnvironment,
|
|
||||||
DeleteEnvironment)
|
|
||||||
row_actions = (ShowEnvironmentServices, DeployEnvironment,
|
|
||||||
DeleteEnvironment, AbandonEnvironment,
|
|
||||||
UpdateEnvMetadata)
|
|
||||||
|
|
||||||
|
|
||||||
def get_service_details_link(service):
|
|
||||||
return reverse('horizon:app-catalog:environments:service_details',
|
|
||||||
args=(service.environment_id, service['?']['id']))
|
|
||||||
|
|
||||||
|
|
||||||
def get_service_type(datum):
|
|
||||||
return datum['?'].get(consts.DASHBOARD_ATTRS_KEY, {}).get('name')
|
|
||||||
|
|
||||||
|
|
||||||
class UpdateMetadata(tables.LinkAction):
|
|
||||||
name = "update_metadata"
|
|
||||||
verbose_name = _("Update Metadata")
|
|
||||||
ajax = False
|
|
||||||
icon = "pencil"
|
|
||||||
attrs = {"ng-controller": "MetadataModalHelperController as modal"}
|
|
||||||
|
|
||||||
def __init__(self, attrs=None, **kwargs):
|
|
||||||
kwargs['preempt'] = True
|
|
||||||
self.session_id = None
|
|
||||||
super(UpdateMetadata, self).__init__(attrs, **kwargs)
|
|
||||||
|
|
||||||
def get_link_url(self, service):
|
|
||||||
env_id = self.table.kwargs.get('environment_id')
|
|
||||||
comp_id = service['?']['id']
|
|
||||||
target = json.dumps({
|
|
||||||
'environment': env_id,
|
|
||||||
'component': comp_id,
|
|
||||||
'session': self.session_id,
|
|
||||||
})
|
|
||||||
self.attrs['ng-click'] = (
|
|
||||||
"modal.openMetadataModal('muranoapp', %s, true)" % target)
|
|
||||||
return "javascript:void(0);"
|
|
||||||
|
|
||||||
def allowed(self, request, service=None):
|
|
||||||
status, version = _get_environment_status_and_version(request,
|
|
||||||
self.table)
|
|
||||||
return status != consts.STATUS_ID_DEPLOYING
|
|
||||||
|
|
||||||
def update(self, request, datum):
|
|
||||||
env_id = self.table.kwargs.get('environment_id')
|
|
||||||
self.session_id = api.Session.get_if_available(request, env_id)
|
|
||||||
|
|
||||||
|
|
||||||
class ServicesTable(tables.DataTable):
|
|
||||||
name = md_utils.Column(
|
|
||||||
'name',
|
|
||||||
verbose_name=_('Name'),
|
|
||||||
link=get_service_details_link)
|
|
||||||
|
|
||||||
_type = tables.Column(get_service_type,
|
|
||||||
verbose_name=_('Type'))
|
|
||||||
|
|
||||||
status = tables.Column(lambda datum: datum['?'].get('status'),
|
|
||||||
verbose_name=_('Status'),
|
|
||||||
status=True,
|
|
||||||
status_choices=consts.STATUS_CHOICES,
|
|
||||||
display_choices=consts.STATUS_DISPLAY_CHOICES)
|
|
||||||
operation = tables.Column('operation',
|
|
||||||
verbose_name=_('Last operation'),
|
|
||||||
filters=(defaultfilters.urlize, ))
|
|
||||||
operation_updated = tables.Column('operation_updated',
|
|
||||||
verbose_name=_('Time updated'),
|
|
||||||
filters=(filters.parse_isotime,))
|
|
||||||
|
|
||||||
def get_object_id(self, datum):
|
|
||||||
return datum['?']['id']
|
|
||||||
|
|
||||||
def get_apps_list(self):
|
|
||||||
packages = []
|
|
||||||
with api_utils.handled_exceptions(self.request):
|
|
||||||
packages, self._more = pkg_api.package_list(
|
|
||||||
self.request,
|
|
||||||
filters={'type': 'Application', 'catalog': True})
|
|
||||||
return [package.to_dict() for package in packages]
|
|
||||||
|
|
||||||
def actions_allowed(self):
|
|
||||||
status, version = _get_environment_status_and_version(
|
|
||||||
self.request, self)
|
|
||||||
return status not in consts.NO_ACTION_ALLOWED_STATUSES
|
|
||||||
|
|
||||||
def get_categories_list(self):
|
|
||||||
return catalog_views.get_categories_list(self.request)
|
|
||||||
|
|
||||||
def get_row_actions(self, datum):
|
|
||||||
actions = super(ServicesTable, self).get_row_actions(datum)
|
|
||||||
environment_id = self.kwargs['environment_id']
|
|
||||||
app_actions = []
|
|
||||||
for action_datum in api.extract_actions_list(datum):
|
|
||||||
_classes = ('murano_action',)
|
|
||||||
|
|
||||||
class CustomAction(tables.LinkAction):
|
|
||||||
name = action_datum['name']
|
|
||||||
verbose_name = action_datum.get('title') or name
|
|
||||||
url = reverse('horizon:app-catalog:environments:start_action',
|
|
||||||
args=(environment_id, action_datum['id']))
|
|
||||||
classes = _classes
|
|
||||||
table = self
|
|
||||||
|
|
||||||
def allowed(self, request, datum):
|
|
||||||
status, version = _get_environment_status_and_version(
|
|
||||||
request, self.table)
|
|
||||||
if status in consts.NO_ACTION_ALLOWED_STATUSES:
|
|
||||||
return False
|
|
||||||
return True
|
|
||||||
|
|
||||||
bound_action = CustomAction()
|
|
||||||
if not bound_action.allowed(self.request, datum):
|
|
||||||
continue
|
|
||||||
bound_action.datum = datum
|
|
||||||
if issubclass(bound_action.__class__, tables.LinkAction):
|
|
||||||
bound_action.bound_url = bound_action.get_link_url(datum)
|
|
||||||
app_actions.append(bound_action)
|
|
||||||
if app_actions:
|
|
||||||
# Show native actions first (such as "Delete Component") and
|
|
||||||
# then add sorted application actions
|
|
||||||
actions.extend(sorted(app_actions, key=lambda x: x.name))
|
|
||||||
return actions
|
|
||||||
|
|
||||||
def get_repo_url(self):
|
|
||||||
return pkg_consts.DISPLAY_MURANO_REPO_URL
|
|
||||||
|
|
||||||
def get_pkg_def_url(self):
|
|
||||||
return reverse('horizon:app-catalog:packages:index')
|
|
||||||
|
|
||||||
class Meta(object):
|
|
||||||
name = 'services'
|
|
||||||
verbose_name = _('Component List')
|
|
||||||
no_data_message = _('No components')
|
|
||||||
status_columns = ['status']
|
|
||||||
row_class = UpdateServiceRow
|
|
||||||
table_actions = (AddApplication, DeployThisEnvironment)
|
|
||||||
row_actions = (DeleteService, UpdateMetadata)
|
|
||||||
multi_select = False
|
|
||||||
|
|
||||||
|
|
||||||
class ShowDeploymentDetails(tables.LinkAction):
|
|
||||||
name = 'show_deployment_details'
|
|
||||||
verbose_name = _('Show Details')
|
|
||||||
|
|
||||||
def get_link_url(self, deployment=None):
|
|
||||||
kwargs = {'environment_id': deployment.environment_id,
|
|
||||||
'deployment_id': deployment.id}
|
|
||||||
return reverse('horizon:app-catalog:environments:deployment_details',
|
|
||||||
kwargs=kwargs)
|
|
||||||
|
|
||||||
def allowed(self, request, environment):
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
class DeploymentsTable(tables.DataTable):
|
|
||||||
started = tables.Column('started',
|
|
||||||
verbose_name=_('Time Started'),
|
|
||||||
filters=(filters.parse_isotime,))
|
|
||||||
finished = tables.Column('finished',
|
|
||||||
verbose_name=_('Time Finished'),
|
|
||||||
filters=(filters.parse_isotime,))
|
|
||||||
|
|
||||||
status = tables.Column(
|
|
||||||
'state',
|
|
||||||
verbose_name=_('Status'),
|
|
||||||
status=True,
|
|
||||||
status_choices=consts.DEPLOYMENT_STATUS_CHOICES,
|
|
||||||
display_choices=consts.DEPLOYMENT_STATUS_DISPLAY_CHOICES)
|
|
||||||
|
|
||||||
class Meta(object):
|
|
||||||
name = 'deployments'
|
|
||||||
verbose_name = _('Deployments')
|
|
||||||
row_actions = (ShowDeploymentDetails,)
|
|
||||||
|
|
||||||
|
|
||||||
class EnvConfigTable(tables.DataTable):
|
|
||||||
name = md_utils.Column('name', verbose_name=_('Name'))
|
|
||||||
_type = tables.Column(
|
|
||||||
lambda datum: get_service_type(datum) or 'Unknown',
|
|
||||||
verbose_name=_('Type'))
|
|
||||||
|
|
||||||
def get_object_id(self, datum):
|
|
||||||
return datum['?']['id']
|
|
||||||
|
|
||||||
class Meta(object):
|
|
||||||
name = 'environment_configuration'
|
|
||||||
verbose_name = _('Deployed Components')
|
|
||||||
|
|
||||||
|
|
||||||
def get_deployment_history_reports(deployment):
|
|
||||||
template_name = 'deployments/_cell_reports.html'
|
|
||||||
context = {
|
|
||||||
"reports": deployment.reports,
|
|
||||||
}
|
|
||||||
return template.loader.render_to_string(template_name, context)
|
|
||||||
|
|
||||||
|
|
||||||
def get_deployment_history_services(deployment):
|
|
||||||
template_name = 'deployments/_cell_services.html'
|
|
||||||
services = {}
|
|
||||||
for service in deployment.description['services']:
|
|
||||||
service_type = service['?']['type']
|
|
||||||
if service_type.find('/') != -1:
|
|
||||||
service_type = service_type[:service_type.find('/')]
|
|
||||||
services[service.get('name', service['?']['name'])] = service_type
|
|
||||||
context = {
|
|
||||||
"services": services,
|
|
||||||
}
|
|
||||||
return template.loader.render_to_string(template_name, context)
|
|
||||||
|
|
||||||
|
|
||||||
class DeploymentHistoryTable(tables.DataTable):
|
|
||||||
environment_name = tables.WrappingColumn(
|
|
||||||
lambda d: d.description['name'],
|
|
||||||
verbose_name=_('Environment'))
|
|
||||||
logs = tables.Column(get_deployment_history_reports,
|
|
||||||
verbose_name=_('Logs (Created, Message)'))
|
|
||||||
services = tables.Column(get_deployment_history_services,
|
|
||||||
verbose_name=_('Services (Name, Type)'))
|
|
||||||
status = tables.Column(
|
|
||||||
'state',
|
|
||||||
verbose_name=_('Status'),
|
|
||||||
status=True,
|
|
||||||
status_choices=consts.DEPLOYMENT_STATUS_CHOICES,
|
|
||||||
display_choices=consts.DEPLOYMENT_STATUS_DISPLAY_CHOICES)
|
|
||||||
|
|
||||||
class Meta(object):
|
|
||||||
name = 'deployment_history'
|
|
||||||
verbose_name = _('Deployment History')
|
|
||||||
row_actions = (ShowDeploymentDetails,)
|
|
|
@ -1,281 +0,0 @@
|
||||||
# Copyright (c) 2013 Mirantis, Inc.
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
||||||
# not use this file except in compliance with the License. You may obtain
|
|
||||||
# a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
||||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
||||||
# License for the specific language governing permissions and limitations
|
|
||||||
# under the License.
|
|
||||||
|
|
||||||
import collections
|
|
||||||
import json
|
|
||||||
|
|
||||||
from django.conf import settings
|
|
||||||
from django.core.urlresolvers import reverse
|
|
||||||
from django.utils.translation import ugettext_lazy as _
|
|
||||||
from horizon import exceptions
|
|
||||||
from horizon import tabs
|
|
||||||
from openstack_dashboard.api import heat as heat_api
|
|
||||||
from openstack_dashboard.api import nova as nova_api
|
|
||||||
from openstack_dashboard import policy
|
|
||||||
|
|
||||||
from muranoclient.common import exceptions as exc
|
|
||||||
from muranodashboard.common import utils
|
|
||||||
from muranodashboard.environments import api
|
|
||||||
from muranodashboard.environments import consts
|
|
||||||
from muranodashboard.environments import tables
|
|
||||||
|
|
||||||
|
|
||||||
class OverviewTab(tabs.Tab):
|
|
||||||
name = _("Component")
|
|
||||||
slug = "_service"
|
|
||||||
template_name = 'services/_overview.html'
|
|
||||||
|
|
||||||
def get_context_data(self, request):
|
|
||||||
"""Return application details.
|
|
||||||
|
|
||||||
:param request:
|
|
||||||
:return:
|
|
||||||
"""
|
|
||||||
def find_stack(name, **kwargs):
|
|
||||||
stacks, has_more, has_prev = heat_api.stacks_list(
|
|
||||||
request, sort_dir='asc', **kwargs)
|
|
||||||
for stack in stacks:
|
|
||||||
if name in stack.stack_name:
|
|
||||||
stack_data = {'id': stack.id,
|
|
||||||
'name': stack.stack_name}
|
|
||||||
return stack_data
|
|
||||||
if has_more:
|
|
||||||
return find_stack(name, marker=stacks[-1].id)
|
|
||||||
return {}
|
|
||||||
|
|
||||||
def get_instance_and_stack(instance_data, request):
|
|
||||||
instance_name = instance_data['name']
|
|
||||||
nova_openstackid = instance_data['openstackId']
|
|
||||||
stack_name = ''
|
|
||||||
instance_result_data = {}
|
|
||||||
stack_result_data = {}
|
|
||||||
instances, more = nova_api.server_list(request)
|
|
||||||
|
|
||||||
for instance in instances:
|
|
||||||
if nova_openstackid in instance.id:
|
|
||||||
instance_result_data = {'name': instance.name,
|
|
||||||
'id': instance.id}
|
|
||||||
stack_name = instance.name.split('-' + instance_name)[0]
|
|
||||||
break
|
|
||||||
# Add link to stack details page
|
|
||||||
if stack_name:
|
|
||||||
stack_result_data = find_stack(stack_name)
|
|
||||||
return instance_result_data, stack_result_data
|
|
||||||
|
|
||||||
service_data = self.tab_group.kwargs['service']
|
|
||||||
|
|
||||||
status_name = ''
|
|
||||||
for id, name in consts.STATUS_DISPLAY_CHOICES:
|
|
||||||
if id == service_data['?']['status']:
|
|
||||||
status_name = name
|
|
||||||
|
|
||||||
detail_info = collections.OrderedDict([
|
|
||||||
('Name', getattr(service_data, 'name', '')),
|
|
||||||
('ID', service_data['?']['id']),
|
|
||||||
('Type', tables.get_service_type(service_data) or 'Unknown'),
|
|
||||||
('Status', status_name), ])
|
|
||||||
|
|
||||||
if hasattr(service_data, 'domain'):
|
|
||||||
if not service_data.domain:
|
|
||||||
detail_info['Domain'] = 'Not in domain'
|
|
||||||
else:
|
|
||||||
detail_info['Domain'] = service_data.domain
|
|
||||||
|
|
||||||
if hasattr(service_data, 'repository'):
|
|
||||||
detail_info['Application repository'] = service_data.repository
|
|
||||||
|
|
||||||
if hasattr(service_data, 'uri'):
|
|
||||||
detail_info['Load Balancer URI'] = service_data.uri
|
|
||||||
|
|
||||||
if hasattr(service_data, 'floatingip'):
|
|
||||||
detail_info['Floating IP'] = service_data.floatingip
|
|
||||||
|
|
||||||
# TODO(efedorova): Need to determine Instance subclass
|
|
||||||
# after it would be possible
|
|
||||||
if hasattr(service_data,
|
|
||||||
'instance') and service_data['instance'] is not None:
|
|
||||||
instance, stack = get_instance_and_stack(
|
|
||||||
service_data['instance'], request)
|
|
||||||
if instance:
|
|
||||||
detail_info['Instance'] = instance
|
|
||||||
if stack:
|
|
||||||
detail_info['Stack'] = stack
|
|
||||||
|
|
||||||
if hasattr(service_data,
|
|
||||||
'instances') and service_data['instances'] is not None:
|
|
||||||
instances_for_template = []
|
|
||||||
stacks_for_template = []
|
|
||||||
for instance in service_data['instances']:
|
|
||||||
instance, stack = get_instance_and_stack(instance, request)
|
|
||||||
instances_for_template.append(instance)
|
|
||||||
if stack:
|
|
||||||
stacks_for_template.append(stack)
|
|
||||||
if instances_for_template:
|
|
||||||
detail_info['Instances'] = instances_for_template
|
|
||||||
if stacks_for_template:
|
|
||||||
detail_info['Stacks'] = stacks_for_template
|
|
||||||
|
|
||||||
return {'service': detail_info}
|
|
||||||
|
|
||||||
|
|
||||||
class ServiceLogsTab(tabs.Tab):
|
|
||||||
name = _("Logs")
|
|
||||||
slug = "service_logs"
|
|
||||||
template_name = 'services/_logs.html'
|
|
||||||
preload = False
|
|
||||||
|
|
||||||
def get_context_data(self, request):
|
|
||||||
service_id = self.tab_group.kwargs['service_id']
|
|
||||||
environment_id = self.tab_group.kwargs['environment_id']
|
|
||||||
reports = api.get_status_messages_for_service(request, service_id,
|
|
||||||
environment_id)
|
|
||||||
return {"reports": reports}
|
|
||||||
|
|
||||||
|
|
||||||
class EnvLogsTab(tabs.Tab):
|
|
||||||
name = _("Logs")
|
|
||||||
slug = "env_logs"
|
|
||||||
template_name = 'deployments/_logs.html'
|
|
||||||
preload = False
|
|
||||||
|
|
||||||
def get_context_data(self, request):
|
|
||||||
reports = self.tab_group.kwargs['logs']
|
|
||||||
for report in reports:
|
|
||||||
report.created = utils.adjust_datestr(request, report.created)
|
|
||||||
return {"reports": reports}
|
|
||||||
|
|
||||||
|
|
||||||
class LatestLogsTab(EnvLogsTab):
|
|
||||||
name = _("Latest Deployment Log")
|
|
||||||
|
|
||||||
def allowed(self, request):
|
|
||||||
return self.data.get('reports')
|
|
||||||
|
|
||||||
|
|
||||||
class EnvConfigTab(tabs.TableTab):
|
|
||||||
name = _("Configuration")
|
|
||||||
slug = "env_config"
|
|
||||||
table_classes = (tables.EnvConfigTable,)
|
|
||||||
template_name = 'horizon/common/_detail_table.html'
|
|
||||||
preload = False
|
|
||||||
|
|
||||||
def get_environment_configuration_data(self):
|
|
||||||
deployment = self.tab_group.kwargs['deployment']
|
|
||||||
return deployment.get('services')
|
|
||||||
|
|
||||||
|
|
||||||
class EnvironmentTopologyTab(tabs.Tab):
|
|
||||||
name = _("Topology")
|
|
||||||
slug = "topology"
|
|
||||||
template_name = "services/_detail_topology.html"
|
|
||||||
preload = False
|
|
||||||
|
|
||||||
def allowed(self, request):
|
|
||||||
if self.data.get('d3_data'):
|
|
||||||
if json.loads(self.data['d3_data'])['environment']['status']:
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
def get_context_data(self, request):
|
|
||||||
context = {}
|
|
||||||
environment_id = self.tab_group.kwargs['environment_id']
|
|
||||||
context['environment_id'] = environment_id
|
|
||||||
d3_data = api.load_environment_data(self.request, environment_id)
|
|
||||||
context['d3_data'] = d3_data
|
|
||||||
return context
|
|
||||||
|
|
||||||
|
|
||||||
class EnvironmentServicesTab(tabs.TableTab):
|
|
||||||
name = _("Components")
|
|
||||||
slug = "services"
|
|
||||||
table_classes = (tables.ServicesTable,)
|
|
||||||
template_name = "services/_service_list.html"
|
|
||||||
preload = False
|
|
||||||
|
|
||||||
def get_services_data(self):
|
|
||||||
services = []
|
|
||||||
self.environment_id = self.tab_group.kwargs['environment_id']
|
|
||||||
ns_url = "horizon:app-catalog:environments:index"
|
|
||||||
try:
|
|
||||||
services = api.services_list(self.request, self.environment_id)
|
|
||||||
except exc.HTTPForbidden:
|
|
||||||
msg = _('Unable to retrieve list of services. This environment '
|
|
||||||
'is deploying or already deployed by other user.')
|
|
||||||
exceptions.handle(self.request, msg, redirect=reverse(ns_url))
|
|
||||||
|
|
||||||
except (exc.HTTPInternalServerError, exc.HTTPNotFound):
|
|
||||||
msg = _("Environment with id %s doesn't exist anymore")
|
|
||||||
exceptions.handle(self.request,
|
|
||||||
msg % self.environment_id,
|
|
||||||
redirect=reverse(ns_url))
|
|
||||||
except exc.HTTPUnauthorized:
|
|
||||||
exceptions.handle(self.request)
|
|
||||||
|
|
||||||
return services
|
|
||||||
|
|
||||||
def get_context_data(self, request, **kwargs):
|
|
||||||
context = super(EnvironmentServicesTab,
|
|
||||||
self).get_context_data(request, **kwargs)
|
|
||||||
context['MURANO_USE_GLARE'] = getattr(settings, 'MURANO_USE_GLARE',
|
|
||||||
False)
|
|
||||||
return context
|
|
||||||
|
|
||||||
|
|
||||||
class DeploymentTab(tabs.TableTab):
|
|
||||||
slug = "deployments"
|
|
||||||
name = _("Deployment History")
|
|
||||||
table_classes = (tables.DeploymentsTable,)
|
|
||||||
template_name = 'horizon/common/_detail_table.html'
|
|
||||||
preload = False
|
|
||||||
|
|
||||||
def allowed(self, request):
|
|
||||||
return policy.check((("murano", "list_deployments"),), request)
|
|
||||||
|
|
||||||
def get_deployments_data(self):
|
|
||||||
deployments = []
|
|
||||||
self.environment_id = self.tab_group.kwargs['environment_id']
|
|
||||||
ns_url = "horizon:app-catalog:environments:index"
|
|
||||||
try:
|
|
||||||
deployments = api.deployments_list(self.request,
|
|
||||||
self.environment_id)
|
|
||||||
|
|
||||||
except exc.HTTPForbidden:
|
|
||||||
msg = _('Unable to retrieve list of deployments')
|
|
||||||
exceptions.handle(self.request, msg, redirect=reverse(ns_url))
|
|
||||||
|
|
||||||
except exc.HTTPInternalServerError:
|
|
||||||
msg = _("Environment with id %s doesn't exist anymore")
|
|
||||||
exceptions.handle(self.request,
|
|
||||||
msg % self.environment_id,
|
|
||||||
redirect=reverse(ns_url))
|
|
||||||
return deployments
|
|
||||||
|
|
||||||
|
|
||||||
class EnvironmentDetailsTabs(tabs.TabGroup):
|
|
||||||
slug = "environment_details"
|
|
||||||
tabs = (EnvironmentServicesTab, EnvironmentTopologyTab,
|
|
||||||
DeploymentTab, LatestLogsTab)
|
|
||||||
sticky = True
|
|
||||||
|
|
||||||
|
|
||||||
class ServicesTabs(tabs.TabGroup):
|
|
||||||
slug = "services_details"
|
|
||||||
tabs = (OverviewTab, ServiceLogsTab)
|
|
||||||
sticky = True
|
|
||||||
|
|
||||||
|
|
||||||
class DeploymentDetailsTabs(tabs.TabGroup):
|
|
||||||
slug = "deployment_details"
|
|
||||||
tabs = (EnvConfigTab, EnvLogsTab,)
|
|
||||||
sticky = True
|
|
|
@ -1,311 +0,0 @@
|
||||||
# Copyright (c) 2014 Mirantis, Inc.
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
||||||
# not use this file except in compliance with the License. You may obtain
|
|
||||||
# a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
||||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
||||||
# License for the specific language governing permissions and limitations
|
|
||||||
# under the License.
|
|
||||||
|
|
||||||
import json
|
|
||||||
|
|
||||||
from django.contrib.staticfiles.templatetags.staticfiles import static
|
|
||||||
from django.core.urlresolvers import reverse
|
|
||||||
from django.template import loader
|
|
||||||
|
|
||||||
import six
|
|
||||||
|
|
||||||
from muranodashboard.api import packages as pkg_cli
|
|
||||||
from muranodashboard.environments import consts
|
|
||||||
|
|
||||||
|
|
||||||
def get_app_image(request, app_fqdn, status=None):
|
|
||||||
if '@' in app_fqdn:
|
|
||||||
class_fqn, package_fqn = app_fqdn.split('@')
|
|
||||||
if '/' in class_fqn:
|
|
||||||
class_fqn, version = class_fqn.split('/')
|
|
||||||
else:
|
|
||||||
version = None
|
|
||||||
else:
|
|
||||||
package_fqn = app_fqdn
|
|
||||||
version = None
|
|
||||||
package = pkg_cli.app_by_fqn(request, package_fqn, version=version)
|
|
||||||
if status in [
|
|
||||||
consts.STATUS_ID_DEPLOY_FAILURE,
|
|
||||||
consts.STATUS_ID_DELETE_FAILURE,
|
|
||||||
]:
|
|
||||||
url = static('dashboard/img/stack-red.svg')
|
|
||||||
elif status == consts.STATUS_ID_READY:
|
|
||||||
url = static('dashboard/img/stack-green.svg')
|
|
||||||
else:
|
|
||||||
url = static('dashboard/img/stack-gray.svg')
|
|
||||||
if package:
|
|
||||||
app_id = package.id
|
|
||||||
url = reverse("horizon:app-catalog:catalog:images", args=(app_id,))
|
|
||||||
return url
|
|
||||||
|
|
||||||
|
|
||||||
def _get_environment_status_message(entity):
|
|
||||||
if hasattr(entity, 'status'):
|
|
||||||
status = entity.status
|
|
||||||
else:
|
|
||||||
status = entity['?']['status']
|
|
||||||
|
|
||||||
in_progress = True
|
|
||||||
status_message = ''
|
|
||||||
if status in (consts.STATUS_ID_PENDING, consts.STATUS_ID_READY):
|
|
||||||
in_progress = False
|
|
||||||
if status == consts.STATUS_ID_PENDING:
|
|
||||||
status_message = 'Waiting for deployment'
|
|
||||||
elif status == consts.STATUS_ID_READY:
|
|
||||||
status_message = 'Deployed'
|
|
||||||
elif status == consts.STATUS_ID_DEPLOYING:
|
|
||||||
status_message = 'Deployment is in progress'
|
|
||||||
elif status == consts.STATUS_ID_DEPLOY_FAILURE:
|
|
||||||
status_message = 'Deployment failed'
|
|
||||||
return in_progress, status_message
|
|
||||||
|
|
||||||
|
|
||||||
def _truncate_type(type_str, num_of_chars):
|
|
||||||
if len(type_str) < num_of_chars:
|
|
||||||
return type_str
|
|
||||||
else:
|
|
||||||
parts = type_str.split('.')
|
|
||||||
type_str, type_len = parts[-1], len(parts[-1])
|
|
||||||
for part in reversed(parts[:-1]):
|
|
||||||
if type_len + len(part) + 1 > num_of_chars:
|
|
||||||
return '...' + type_str
|
|
||||||
else:
|
|
||||||
type_str = part + '.' + type_str
|
|
||||||
type_len += len(part) + 1
|
|
||||||
return type_str
|
|
||||||
|
|
||||||
|
|
||||||
def _application_info(application, app_image, status):
|
|
||||||
name = application['?'].get('name')
|
|
||||||
if not name:
|
|
||||||
name = application.get('name')
|
|
||||||
context = {'name': name,
|
|
||||||
'type': _truncate_type(application['?']['type'], 45),
|
|
||||||
'status': status,
|
|
||||||
'app_image': app_image}
|
|
||||||
return loader.render_to_string('services/_application_info.html',
|
|
||||||
context)
|
|
||||||
|
|
||||||
|
|
||||||
def _network_info(name, image):
|
|
||||||
context = {'name': name,
|
|
||||||
'image': image}
|
|
||||||
return loader.render_to_string('services/_network_info.html',
|
|
||||||
context)
|
|
||||||
|
|
||||||
|
|
||||||
def _unit_info(unit, unit_image):
|
|
||||||
data = dict(unit)
|
|
||||||
data['type'] = _truncate_type(data['type'], 45)
|
|
||||||
context = {'data': data,
|
|
||||||
'unit_image': unit_image}
|
|
||||||
|
|
||||||
return loader.render_to_string('services/_unit_info.html', context)
|
|
||||||
|
|
||||||
|
|
||||||
def _environment_info(environment, status):
|
|
||||||
context = {'name': environment.name,
|
|
||||||
'status': status}
|
|
||||||
return loader.render_to_string('services/_environment_info.html',
|
|
||||||
context)
|
|
||||||
|
|
||||||
|
|
||||||
def _create_empty_node():
|
|
||||||
node = {
|
|
||||||
'name': '',
|
|
||||||
'status': 'ready',
|
|
||||||
'image': '',
|
|
||||||
'image_size': 60,
|
|
||||||
'required_by': [],
|
|
||||||
'image_x': -30,
|
|
||||||
'image_y': -30,
|
|
||||||
'text_x': 40,
|
|
||||||
'text_y': ".35em",
|
|
||||||
'link_type': "relation",
|
|
||||||
'in_progress': False,
|
|
||||||
'info_box': ''
|
|
||||||
}
|
|
||||||
return node
|
|
||||||
|
|
||||||
|
|
||||||
def _create_ext_network_node(name):
|
|
||||||
node = _create_empty_node()
|
|
||||||
node.update({'id': name,
|
|
||||||
'image': static('muranodashboard/images/ext-net.png'),
|
|
||||||
'link_type': 'relation',
|
|
||||||
'info_box': _network_info(name, static(
|
|
||||||
'dashboard/img/lb-green.svg'))}
|
|
||||||
)
|
|
||||||
return node
|
|
||||||
|
|
||||||
|
|
||||||
def _convert_lists(node_data):
|
|
||||||
for key, value in six.iteritems(node_data):
|
|
||||||
if isinstance(value, list) and all(
|
|
||||||
map(lambda s: not isinstance(s, (dict, list)), value)):
|
|
||||||
new_value = ', '.join(str(v) for v in value)
|
|
||||||
node_data[key] = new_value
|
|
||||||
|
|
||||||
|
|
||||||
def _split_seq_by_predicate(seq, predicate):
|
|
||||||
holds, not_holds = [], []
|
|
||||||
for elt in seq:
|
|
||||||
if predicate(elt):
|
|
||||||
holds.append(elt)
|
|
||||||
else:
|
|
||||||
not_holds.append(elt)
|
|
||||||
return holds, not_holds
|
|
||||||
|
|
||||||
|
|
||||||
def _is_atomic(elt):
|
|
||||||
key, value = elt
|
|
||||||
return not isinstance(value, (dict, list))
|
|
||||||
|
|
||||||
|
|
||||||
def render_d3_data(request, environment):
|
|
||||||
if not (environment and environment.services):
|
|
||||||
return None
|
|
||||||
|
|
||||||
ext_net_name = None
|
|
||||||
d3_data = {"nodes": [], "environment": {}}
|
|
||||||
|
|
||||||
in_progress, status_message = _get_environment_status_message(environment)
|
|
||||||
environment_node = _create_empty_node()
|
|
||||||
environment_node.update({
|
|
||||||
'id': environment.id,
|
|
||||||
'name': environment.name,
|
|
||||||
'status': status_message,
|
|
||||||
'image': static('dashboard/img/stack-green.svg'),
|
|
||||||
'in_progress': in_progress,
|
|
||||||
'info_box': _environment_info(environment, status_message)
|
|
||||||
})
|
|
||||||
d3_data['environment'] = environment_node
|
|
||||||
|
|
||||||
unit_image_active = static('dashboard/img/server-green.svg')
|
|
||||||
unit_image_non_active = static('dashboard/img/server-gray.svg')
|
|
||||||
|
|
||||||
node_refs = {}
|
|
||||||
|
|
||||||
def get_image(fqdn, node_data):
|
|
||||||
if fqdn.startswith('io.murano.resources'):
|
|
||||||
if len(node_data.get('ipAddresses', [])) > 0:
|
|
||||||
image = unit_image_active
|
|
||||||
else:
|
|
||||||
image = unit_image_non_active
|
|
||||||
else:
|
|
||||||
image = get_app_image(request, fqdn)
|
|
||||||
return image
|
|
||||||
|
|
||||||
def rec(node_data, node_key, parent_node=None):
|
|
||||||
if not isinstance(node_data, dict):
|
|
||||||
return
|
|
||||||
_convert_lists(node_data)
|
|
||||||
node_type = node_data.get('?', {}).get('type')
|
|
||||||
node_id = node_data.get('?', {}).get('id')
|
|
||||||
atomics, containers = _split_seq_by_predicate(
|
|
||||||
six.iteritems(node_data), _is_atomic)
|
|
||||||
if node_type and node_data is not parent_node:
|
|
||||||
node = _create_empty_node()
|
|
||||||
node_refs[node_id] = node
|
|
||||||
atomics.extend([('id', node_data['?']['id']),
|
|
||||||
('type', node_type),
|
|
||||||
('name', node_data.get('name', node_key))])
|
|
||||||
|
|
||||||
image = get_image(node_type, node_data)
|
|
||||||
node.update({
|
|
||||||
'id': node_id,
|
|
||||||
'info_box': _unit_info(atomics, image),
|
|
||||||
'image': image,
|
|
||||||
'link_type': 'unit',
|
|
||||||
'in_progress': in_progress})
|
|
||||||
d3_data['nodes'].append(node)
|
|
||||||
|
|
||||||
for key, value in containers:
|
|
||||||
if key == '?':
|
|
||||||
continue
|
|
||||||
if isinstance(value, dict):
|
|
||||||
rec(value, key, node_data)
|
|
||||||
elif isinstance(value, list):
|
|
||||||
for index, val in enumerate(value):
|
|
||||||
rec(val, '{0}[{1}]'.format(key, index), node_data)
|
|
||||||
|
|
||||||
def build_links_rec(node_data, parent_node=None):
|
|
||||||
if not isinstance(node_data, dict):
|
|
||||||
return
|
|
||||||
node_id = node_data.get('?', {}).get('id')
|
|
||||||
if not node_id:
|
|
||||||
return
|
|
||||||
node = node_refs[node_id]
|
|
||||||
|
|
||||||
atomics, containers = _split_seq_by_predicate(
|
|
||||||
six.iteritems(node_data), _is_atomic)
|
|
||||||
|
|
||||||
# the actual second pass of node linking
|
|
||||||
if parent_node is not None:
|
|
||||||
node['required_by'].append(parent_node['?']['id'])
|
|
||||||
node['link_type'] = 'aggregation'
|
|
||||||
|
|
||||||
for key, value in atomics:
|
|
||||||
if value in node_refs:
|
|
||||||
remote_node = node_refs[value]
|
|
||||||
if node_id not in remote_node['required_by']:
|
|
||||||
remote_node['required_by'].append(node_id)
|
|
||||||
remote_node['link_type'] = 'reference'
|
|
||||||
|
|
||||||
for key, value in containers:
|
|
||||||
if key == '?':
|
|
||||||
continue
|
|
||||||
if isinstance(value, dict):
|
|
||||||
build_links_rec(value, node_data)
|
|
||||||
elif isinstance(value, list):
|
|
||||||
for val in value:
|
|
||||||
build_links_rec(val, node_data)
|
|
||||||
|
|
||||||
for service in environment.services:
|
|
||||||
in_progress, status_message = _get_environment_status_message(service)
|
|
||||||
required_by = None
|
|
||||||
if 'instance' in service and service['instance'] is not None:
|
|
||||||
if service['instance'].get('assignFloatingIp', False):
|
|
||||||
if ext_net_name:
|
|
||||||
required_by = ext_net_name
|
|
||||||
else:
|
|
||||||
ext_net_name = 'External_Network'
|
|
||||||
ext_network_node = _create_ext_network_node(ext_net_name)
|
|
||||||
d3_data['nodes'].append(ext_network_node)
|
|
||||||
required_by = ext_net_name
|
|
||||||
|
|
||||||
service_node = _create_empty_node()
|
|
||||||
service_image = get_app_image(request, service['?']['type'],
|
|
||||||
service['?']['status'])
|
|
||||||
node_id = service['?']['id']
|
|
||||||
node_refs[node_id] = service_node
|
|
||||||
service_node.update({
|
|
||||||
'name': service.get('name', ''),
|
|
||||||
'status': status_message,
|
|
||||||
'image': service_image,
|
|
||||||
'id': node_id,
|
|
||||||
'link_type': 'relation',
|
|
||||||
'in_progress': in_progress,
|
|
||||||
'info_box': _application_info(
|
|
||||||
service, service_image, status_message)
|
|
||||||
})
|
|
||||||
if required_by:
|
|
||||||
service_node['required_by'].append(required_by)
|
|
||||||
d3_data['nodes'].append(service_node)
|
|
||||||
rec(service, None, service)
|
|
||||||
|
|
||||||
for service in environment.services:
|
|
||||||
build_links_rec(service)
|
|
||||||
|
|
||||||
return json.dumps(d3_data)
|
|
|
@ -1,44 +0,0 @@
|
||||||
# Copyright (c) 2013 Mirantis, Inc.
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
||||||
# not use this file except in compliance with the License. You may obtain
|
|
||||||
# a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
||||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
||||||
# License for the specific language governing permissions and limitations
|
|
||||||
# under the License.
|
|
||||||
|
|
||||||
from django.conf import urls
|
|
||||||
|
|
||||||
from openstack_dashboard.dashboards.project.instances import views as inst_view
|
|
||||||
|
|
||||||
from muranodashboard.environments import views
|
|
||||||
|
|
||||||
ENVIRONMENT_ID = r'^(?P<environment_id>[^/]+)'
|
|
||||||
|
|
||||||
urlpatterns = [
|
|
||||||
urls.url(r'^environments/$', views.IndexView.as_view(), name='index'),
|
|
||||||
urls.url(r'^create_environment$', views.CreateEnvironmentView.as_view(),
|
|
||||||
name='create_environment'),
|
|
||||||
urls.url(ENVIRONMENT_ID + r'/services$',
|
|
||||||
views.EnvironmentDetails.as_view(), name='services'),
|
|
||||||
urls.url(ENVIRONMENT_ID + r'/services/get_d3_data$',
|
|
||||||
views.JSONView.as_view(), name='d3_data'),
|
|
||||||
urls.url(ENVIRONMENT_ID + r'/(?P<service_id>[^/]+)/$',
|
|
||||||
views.DetailServiceView.as_view(), name='service_details'),
|
|
||||||
urls.url(ENVIRONMENT_ID + r'/start_action/(?P<action_id>[^/]+)/$',
|
|
||||||
views.StartActionView.as_view(), name='start_action'),
|
|
||||||
urls.url(ENVIRONMENT_ID +
|
|
||||||
r'/actions/(?P<task_id>[^/]+)(?:/(?P<optional>[^/]+))?/$',
|
|
||||||
views.ActionResultView.as_view(), name='action_result'),
|
|
||||||
urls.url(r'^(?P<instance_id>[^/]+)/$',
|
|
||||||
inst_view.DetailView.as_view(), name='detail'),
|
|
||||||
urls.url(r'^deployment_history$', views.DeploymentHistoryView.as_view(),
|
|
||||||
name='deployment_history'),
|
|
||||||
urls.url(ENVIRONMENT_ID + r'/deployments/(?P<deployment_id>[^/]+)$',
|
|
||||||
views.DeploymentDetailsView.as_view(), name='deployment_details'),
|
|
||||||
]
|
|
|
@ -1,343 +0,0 @@
|
||||||
# Copyright (c) 2013 Mirantis, Inc.
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
||||||
# not use this file except in compliance with the License. You may obtain
|
|
||||||
# a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
||||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
||||||
# License for the specific language governing permissions and limitations
|
|
||||||
# under the License.
|
|
||||||
|
|
||||||
import base64
|
|
||||||
import json
|
|
||||||
|
|
||||||
from django.core.urlresolvers import reverse
|
|
||||||
from django.core.urlresolvers import reverse_lazy
|
|
||||||
from django import http
|
|
||||||
from django.utils.translation import ugettext_lazy as _
|
|
||||||
from django.views import generic
|
|
||||||
from horizon import conf
|
|
||||||
from horizon import exceptions
|
|
||||||
from horizon.forms import views
|
|
||||||
from horizon import tables
|
|
||||||
from horizon import tabs
|
|
||||||
|
|
||||||
from muranoclient.common import exceptions as exc
|
|
||||||
from muranodashboard import api as api_utils
|
|
||||||
from muranodashboard.environments import api
|
|
||||||
from muranodashboard.environments import forms as env_forms
|
|
||||||
from muranodashboard.environments import tables as env_tables
|
|
||||||
from muranodashboard.environments import tabs as env_tabs
|
|
||||||
|
|
||||||
|
|
||||||
class IndexView(tables.DataTableView):
|
|
||||||
table_class = env_tables.EnvironmentsTable
|
|
||||||
template_name = 'environments/index.html'
|
|
||||||
page_title = _("Environments")
|
|
||||||
|
|
||||||
def get_data(self):
|
|
||||||
environments = []
|
|
||||||
try:
|
|
||||||
environments = api.environments_list(self.request)
|
|
||||||
except exc.CommunicationError:
|
|
||||||
exceptions.handle(self.request,
|
|
||||||
'Could not connect to Murano API '
|
|
||||||
'Service, check connection details')
|
|
||||||
except exc.HTTPInternalServerError:
|
|
||||||
exceptions.handle(self.request,
|
|
||||||
'Murano API Service is not responding. '
|
|
||||||
'Try again later')
|
|
||||||
except exc.HTTPUnauthorized:
|
|
||||||
exceptions.handle(self.request, ignore=True, escalate=True)
|
|
||||||
|
|
||||||
return environments
|
|
||||||
|
|
||||||
|
|
||||||
class EnvironmentDetails(tabs.TabbedTableView):
|
|
||||||
tab_group_class = env_tabs.EnvironmentDetailsTabs
|
|
||||||
template_name = 'services/index.html'
|
|
||||||
page_title = '{{ environment_name }}'
|
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
|
||||||
context = super(EnvironmentDetails, self).get_context_data(**kwargs)
|
|
||||||
|
|
||||||
try:
|
|
||||||
self.environment_id = self.kwargs['environment_id']
|
|
||||||
env = api.environment_get(self.request, self.environment_id)
|
|
||||||
context['environment_name'] = env.name
|
|
||||||
except Exception:
|
|
||||||
msg = _("Sorry, this environment doesn't exist anymore")
|
|
||||||
redirect = self.get_redirect_url()
|
|
||||||
exceptions.handle(self.request, msg, redirect=redirect)
|
|
||||||
return context
|
|
||||||
context['tenant_id'] = self.request.session['token'].tenant['id']
|
|
||||||
context["url"] = self.get_redirect_url()
|
|
||||||
table = env_tables.EnvironmentsTable(self.request)
|
|
||||||
# record the origin row_action for EnvironmentsTable Meta
|
|
||||||
ori_row_actions = table._meta.row_actions
|
|
||||||
# remove the duplicate 'Manage Components' and 'DeployEnvironment'
|
|
||||||
# actions that have already in Environment Details page
|
|
||||||
# from table.render_row_actions, so the action render to the detail
|
|
||||||
# page will exclude those two actions.
|
|
||||||
table._meta.row_actions = filter(
|
|
||||||
lambda x: x.name not in ('show', 'deploy'),
|
|
||||||
table._meta.row_actions)
|
|
||||||
context["actions"] = table.render_row_actions(env)
|
|
||||||
# recover the origin row_action for EnvironmentsTable Meta
|
|
||||||
table._meta.row_actions = ori_row_actions
|
|
||||||
context['poll_interval'] = conf.HORIZON_CONFIG['ajax_poll_interval']
|
|
||||||
return context
|
|
||||||
|
|
||||||
def get_tabs(self, request, *args, **kwargs):
|
|
||||||
environment_id = self.kwargs['environment_id']
|
|
||||||
deployments = []
|
|
||||||
try:
|
|
||||||
deployments = api.deployments_list(self.request,
|
|
||||||
environment_id)
|
|
||||||
except exc.HTTPException:
|
|
||||||
msg = _('Unable to retrieve list of deployments')
|
|
||||||
exceptions.handle(self.request,
|
|
||||||
msg,
|
|
||||||
redirect=self.get_redirect_url())
|
|
||||||
|
|
||||||
logs = []
|
|
||||||
if deployments:
|
|
||||||
last_deployment = deployments[0]
|
|
||||||
logs = api.deployment_reports(self.request,
|
|
||||||
environment_id,
|
|
||||||
last_deployment.id)
|
|
||||||
return self.tab_group_class(request, logs=logs,
|
|
||||||
**kwargs)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_redirect_url():
|
|
||||||
return reverse_lazy("horizon:app-catalog:environments:index")
|
|
||||||
|
|
||||||
|
|
||||||
class DetailServiceView(tabs.TabbedTableView):
|
|
||||||
tab_group_class = env_tabs.ServicesTabs
|
|
||||||
template_name = 'services/details.html'
|
|
||||||
page_title = '{{ service_name }}'
|
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
|
||||||
context = super(DetailServiceView, self).get_context_data(**kwargs)
|
|
||||||
service = self.get_data()
|
|
||||||
context["service"] = service
|
|
||||||
context["service_name"] = getattr(self.service, 'name', '-')
|
|
||||||
env = api.environment_get(self.request, self.environment_id)
|
|
||||||
context["environment_name"] = env.name
|
|
||||||
breadcrumb = [
|
|
||||||
(context["environment_name"],
|
|
||||||
reverse("horizon:app-catalog:environments:services",
|
|
||||||
args=[self.environment_id])),
|
|
||||||
(_('Applications'), None)]
|
|
||||||
context["custom_breadcrumb"] = breadcrumb
|
|
||||||
return context
|
|
||||||
|
|
||||||
def get_data(self):
|
|
||||||
service_id = self.kwargs['service_id']
|
|
||||||
self.environment_id = self.kwargs['environment_id']
|
|
||||||
try:
|
|
||||||
self.service = api.service_get(self.request,
|
|
||||||
self.environment_id,
|
|
||||||
service_id)
|
|
||||||
except exc.HTTPUnauthorized:
|
|
||||||
exceptions.handle(self.request)
|
|
||||||
|
|
||||||
except exc.HTTPForbidden:
|
|
||||||
redirect = reverse('horizon:app-catalog:environments:index')
|
|
||||||
exceptions.handle(self.request,
|
|
||||||
_('Unable to retrieve details for '
|
|
||||||
'service'),
|
|
||||||
redirect=redirect)
|
|
||||||
else:
|
|
||||||
self._service = self.service
|
|
||||||
return self._service
|
|
||||||
|
|
||||||
def get_tabs(self, request, *args, **kwargs):
|
|
||||||
service = self.get_data()
|
|
||||||
return self.tab_group_class(request, service=service, **kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
class CreateEnvironmentView(views.ModalFormView):
|
|
||||||
form_class = env_forms.CreateEnvironmentForm
|
|
||||||
form_id = 'create_environment_form'
|
|
||||||
modal_header = _('Create Environment')
|
|
||||||
template_name = 'environments/create.html'
|
|
||||||
page_title = _('Create Environment')
|
|
||||||
context_object_name = 'environment'
|
|
||||||
submit_label = _('Create')
|
|
||||||
submit_url = reverse_lazy(
|
|
||||||
'horizon:app-catalog:environments:create_environment')
|
|
||||||
|
|
||||||
def get_form(self, **kwargs):
|
|
||||||
if 'next' in self.request.GET:
|
|
||||||
self.request.session['next_url'] = self.request.GET['next']
|
|
||||||
form_class = kwargs.get('form_class', self.get_form_class())
|
|
||||||
return super(CreateEnvironmentView, self).get_form(form_class)
|
|
||||||
|
|
||||||
def get_success_url(self):
|
|
||||||
if 'next_url' in self.request.session:
|
|
||||||
return self.request.session['next_url']
|
|
||||||
env_id = self.request.session.get('env_id')
|
|
||||||
if env_id:
|
|
||||||
del self.request.session['env_id']
|
|
||||||
return reverse("horizon:app-catalog:environments:services",
|
|
||||||
args=[env_id])
|
|
||||||
return reverse_lazy('horizon:app-catalog:environments:index')
|
|
||||||
|
|
||||||
|
|
||||||
class DeploymentHistoryView(tables.DataTableView):
|
|
||||||
table_class = env_tables.DeploymentHistoryTable
|
|
||||||
template_name = 'environments/index.html'
|
|
||||||
page_title = _("Deployment History")
|
|
||||||
|
|
||||||
def get_data(self):
|
|
||||||
deployment_history = []
|
|
||||||
try:
|
|
||||||
deployment_history = api.deployment_history(self.request)
|
|
||||||
except exc.HTTPUnauthorized:
|
|
||||||
exceptions.handle(self.request)
|
|
||||||
except exc.HTTPForbidden:
|
|
||||||
redirect = reverse('horizon:app-catalog:environments:services',
|
|
||||||
args=[self.environment_id])
|
|
||||||
exceptions.handle(self.request,
|
|
||||||
_('Unable to retrieve deployment history.'),
|
|
||||||
redirect=redirect)
|
|
||||||
return deployment_history
|
|
||||||
|
|
||||||
|
|
||||||
class DeploymentDetailsView(tabs.TabbedTableView):
|
|
||||||
tab_group_class = env_tabs.DeploymentDetailsTabs
|
|
||||||
table_class = env_tables.EnvConfigTable
|
|
||||||
template_name = 'deployments/reports.html'
|
|
||||||
page_title = 'Deployment at {{ deployment_start_time }}'
|
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
|
||||||
context = super(DeploymentDetailsView, self).get_context_data(**kwargs)
|
|
||||||
context["environment_id"] = self.environment_id
|
|
||||||
env = api.environment_get(self.request, self.environment_id)
|
|
||||||
context["environment_name"] = env.name
|
|
||||||
context["deployment_start_time"] = \
|
|
||||||
api.get_deployment_start(self.request,
|
|
||||||
self.environment_id,
|
|
||||||
self.deployment_id)
|
|
||||||
breadcrumb = [
|
|
||||||
(context["environment_name"],
|
|
||||||
reverse("horizon:app-catalog:environments:services",
|
|
||||||
args=[self.environment_id])),
|
|
||||||
(_('Deployments'),), ]
|
|
||||||
context["custom_breadcrumb"] = breadcrumb
|
|
||||||
return context
|
|
||||||
|
|
||||||
def get_deployment(self):
|
|
||||||
deployment = None
|
|
||||||
try:
|
|
||||||
deployment = api.get_deployment_descr(self.request,
|
|
||||||
self.environment_id,
|
|
||||||
self.deployment_id)
|
|
||||||
except (exc.HTTPInternalServerError, exc.HTTPNotFound):
|
|
||||||
msg = _("Deployment with id %s doesn't exist anymore")
|
|
||||||
redirect = reverse("horizon:app-catalog:environments:deployments")
|
|
||||||
exceptions.handle(self.request,
|
|
||||||
msg % self.deployment_id,
|
|
||||||
redirect=redirect)
|
|
||||||
return deployment
|
|
||||||
|
|
||||||
def get_logs(self):
|
|
||||||
logs = []
|
|
||||||
try:
|
|
||||||
logs = api.deployment_reports(self.request,
|
|
||||||
self.environment_id,
|
|
||||||
self.deployment_id)
|
|
||||||
except (exc.HTTPInternalServerError, exc.HTTPNotFound):
|
|
||||||
msg = _('Deployment with id %s doesn\'t exist anymore')
|
|
||||||
redirect = reverse("horizon:app-catalog:environments:deployments")
|
|
||||||
exceptions.handle(self.request,
|
|
||||||
msg % self.deployment_id,
|
|
||||||
redirect=redirect)
|
|
||||||
return logs
|
|
||||||
|
|
||||||
def get_tabs(self, request, *args, **kwargs):
|
|
||||||
self.deployment_id = self.kwargs['deployment_id']
|
|
||||||
self.environment_id = self.kwargs['environment_id']
|
|
||||||
deployment = self.get_deployment()
|
|
||||||
logs = self.get_logs()
|
|
||||||
|
|
||||||
return self.tab_group_class(request, deployment=deployment, logs=logs,
|
|
||||||
**kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
class JSONView(generic.View):
|
|
||||||
@staticmethod
|
|
||||||
def get(request, **kwargs):
|
|
||||||
data = api.load_environment_data(request, kwargs['environment_id'])
|
|
||||||
return http.HttpResponse(data, content_type='application/json')
|
|
||||||
|
|
||||||
|
|
||||||
class JSONResponse(http.HttpResponse):
|
|
||||||
def __init__(self, content=None, **kwargs):
|
|
||||||
if content is None:
|
|
||||||
content = {}
|
|
||||||
kwargs.pop('content_type', None)
|
|
||||||
super(JSONResponse, self).__init__(
|
|
||||||
content=json.dumps(content), content_type='application/json',
|
|
||||||
**kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
class StartActionView(generic.View):
|
|
||||||
@staticmethod
|
|
||||||
def post(request, environment_id, action_id):
|
|
||||||
if api.action_allowed(request, environment_id):
|
|
||||||
task_id = api.run_action(request, environment_id, action_id)
|
|
||||||
url = reverse('horizon:app-catalog:environments:action_result',
|
|
||||||
args=(environment_id, task_id))
|
|
||||||
return JSONResponse({'url': url})
|
|
||||||
else:
|
|
||||||
return JSONResponse()
|
|
||||||
|
|
||||||
|
|
||||||
class ActionResultView(generic.View):
|
|
||||||
@staticmethod
|
|
||||||
def is_file_returned(result):
|
|
||||||
try:
|
|
||||||
return result['result']['?']['type'] == 'io.murano.File'
|
|
||||||
except (KeyError, ValueError, TypeError):
|
|
||||||
return False
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def compose_response(result, is_file=False, is_exc=False):
|
|
||||||
filename = 'exception.json' if is_exc else 'result.json'
|
|
||||||
content_type = 'application/octet-stream'
|
|
||||||
if is_file:
|
|
||||||
filename = result.get('filename') or 'action_result_file'
|
|
||||||
content_type = result.get('mimeType') or content_type
|
|
||||||
content = base64.b64decode(result['base64Content'])
|
|
||||||
else:
|
|
||||||
content = json.dumps(result, indent=True)
|
|
||||||
|
|
||||||
response = http.HttpResponse(content_type=content_type)
|
|
||||||
response['Content-Disposition'] = (
|
|
||||||
'attachment; filename=%s' % filename)
|
|
||||||
response.write(content)
|
|
||||||
response['Content-Length'] = str(len(response.content))
|
|
||||||
return response
|
|
||||||
|
|
||||||
def get(self, request, environment_id, task_id, optional):
|
|
||||||
mc = api_utils.muranoclient(request)
|
|
||||||
result = mc.actions.get_result(environment_id, task_id)
|
|
||||||
if result:
|
|
||||||
if result and optional == 'poll':
|
|
||||||
if result['result'] is not None:
|
|
||||||
# Remove content from response on first successful poll
|
|
||||||
del result['result']
|
|
||||||
return JSONResponse(result)
|
|
||||||
return self.compose_response(result['result'],
|
|
||||||
self.is_file_returned(result),
|
|
||||||
result['isException'])
|
|
||||||
# Polling hasn't returned content yet
|
|
||||||
return JSONResponse()
|
|
|
@ -1,25 +0,0 @@
|
||||||
# Copyright (c) 2015 Mirantis, Inc.
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
||||||
# not use this file except in compliance with the License. You may obtain
|
|
||||||
# a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
||||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
||||||
# License for the specific language governing permissions and limitations
|
|
||||||
# under the License.
|
|
||||||
|
|
||||||
from muranoclient.common import exceptions as muranoclient_exc
|
|
||||||
|
|
||||||
|
|
||||||
RECOVERABLE = (muranoclient_exc.HTTPException,
|
|
||||||
muranoclient_exc.HTTPForbidden,
|
|
||||||
muranoclient_exc.CommunicationError)
|
|
||||||
|
|
||||||
NOT_FOUND = (muranoclient_exc.NotFound,
|
|
||||||
muranoclient_exc.EndpointNotFound)
|
|
||||||
|
|
||||||
UNAUTHORIZED = (muranoclient_exc.HTTPUnauthorized,)
|
|
|
@ -1,136 +0,0 @@
|
||||||
# Copyright (c) 2013 Mirantis, Inc.
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
||||||
# not use this file except in compliance with the License. You may obtain
|
|
||||||
# a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
||||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
||||||
# License for the specific language governing permissions and limitations
|
|
||||||
# under the License.
|
|
||||||
|
|
||||||
import json
|
|
||||||
|
|
||||||
from django.core.urlresolvers import reverse
|
|
||||||
from django import forms
|
|
||||||
from django.utils.translation import ugettext_lazy as _
|
|
||||||
from horizon import exceptions
|
|
||||||
from horizon import forms as horizon_forms
|
|
||||||
from horizon import messages
|
|
||||||
from openstack_dashboard.api import glance
|
|
||||||
from oslo_log import log as logging
|
|
||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
def filter_murano_images(images, request=None):
|
|
||||||
# filter out the snapshot image type
|
|
||||||
images = filter(
|
|
||||||
lambda x: getattr(x, 'image_type', None) != 'snapshot', list(images))
|
|
||||||
marked_images = []
|
|
||||||
for image in images:
|
|
||||||
# Additional properties, whose value is always a string data type, are
|
|
||||||
# only included in the response if they have a value.
|
|
||||||
metadata = getattr(image, 'murano_image_info', None)
|
|
||||||
if metadata:
|
|
||||||
try:
|
|
||||||
metadata = json.loads(metadata)
|
|
||||||
except ValueError:
|
|
||||||
msg = _('Invalid metadata for image: {0}').format(image.id)
|
|
||||||
LOG.warning(msg)
|
|
||||||
if request:
|
|
||||||
exceptions.handle(request, msg)
|
|
||||||
metadata = {}
|
|
||||||
image.title = metadata.get('title', 'No Title')
|
|
||||||
image.type = metadata.get('type', 'No Type')
|
|
||||||
|
|
||||||
marked_images.append(image)
|
|
||||||
return marked_images
|
|
||||||
|
|
||||||
|
|
||||||
class MarkImageForm(horizon_forms.SelfHandlingForm):
|
|
||||||
_metadata = {
|
|
||||||
'windows.2012': ' Windows Server 2012',
|
|
||||||
'linux': 'Generic Linux',
|
|
||||||
'cirros.demo': 'CirrOS for Murano Demo',
|
|
||||||
'custom': "Custom type"
|
|
||||||
}
|
|
||||||
|
|
||||||
image = forms.ChoiceField(
|
|
||||||
label=_('Image'),
|
|
||||||
widget=horizon_forms.ThemableSelectWidget())
|
|
||||||
title = forms.CharField(max_length="255", label=_("Title"))
|
|
||||||
type = forms.ChoiceField(
|
|
||||||
label=_("Type"),
|
|
||||||
choices=_metadata.items(),
|
|
||||||
initial='custom',
|
|
||||||
widget=horizon_forms.ThemableSelectWidget(attrs={
|
|
||||||
'class': 'switchable',
|
|
||||||
'data-slug': 'type'}))
|
|
||||||
custom_type = forms.CharField(
|
|
||||||
max_length="255",
|
|
||||||
label=_("Custom Type"),
|
|
||||||
widget=forms.TextInput(attrs={
|
|
||||||
'class': 'switched',
|
|
||||||
'data-switch-on': 'type',
|
|
||||||
'data-type-custom': _('Custom Type')}),
|
|
||||||
required=False)
|
|
||||||
existing_titles = forms.CharField(widget=forms.HiddenInput)
|
|
||||||
|
|
||||||
def __init__(self, request, *args, **kwargs):
|
|
||||||
super(MarkImageForm, self).__init__(request, *args, **kwargs)
|
|
||||||
|
|
||||||
images = []
|
|
||||||
try:
|
|
||||||
# https://bugs.launchpad.net/murano/+bug/1339261 - glance
|
|
||||||
# client version change alters the API. Other tuple values
|
|
||||||
# are _more and _prev (in recent glance client)
|
|
||||||
images = glance.image_list_detailed(request)[0]
|
|
||||||
except Exception:
|
|
||||||
LOG.error('Failed to request image list from Glance')
|
|
||||||
exceptions.handle(request, _('Unable to retrieve list of images'))
|
|
||||||
|
|
||||||
# filter out the image format aki and ari
|
|
||||||
images = filter(
|
|
||||||
lambda x: x.container_format not in ('aki', 'ari'), images)
|
|
||||||
|
|
||||||
# filter out the snapshot image type
|
|
||||||
images = filter(
|
|
||||||
lambda x: x.properties.get("image_type", '') != 'snapshot', images)
|
|
||||||
|
|
||||||
self.fields['image'].choices = [(i.id, i.name) for i in images]
|
|
||||||
self.fields['existing_titles'].initial = \
|
|
||||||
[image.title for image in filter_murano_images(images)]
|
|
||||||
|
|
||||||
def handle(self, request, data):
|
|
||||||
LOG.debug('Marking image with specified metadata: {0}'.format(data))
|
|
||||||
|
|
||||||
image_id = data['image']
|
|
||||||
image_type = data['type'] if data['type'] != 'custom' else \
|
|
||||||
data['custom_type']
|
|
||||||
kwargs = {}
|
|
||||||
kwargs['murano_image_info'] = json.dumps({
|
|
||||||
'title': data['title'],
|
|
||||||
'type': image_type
|
|
||||||
})
|
|
||||||
try:
|
|
||||||
img = glance.image_update_properties(request, image_id, **kwargs)
|
|
||||||
messages.success(request, _('Image successfully marked'))
|
|
||||||
return img
|
|
||||||
except Exception:
|
|
||||||
exceptions.handle(request, _('Unable to mark image'),
|
|
||||||
redirect=reverse(
|
|
||||||
'horizon:app-catalog:images:index'))
|
|
||||||
|
|
||||||
def clean_title(self):
|
|
||||||
cleaned_data = super(MarkImageForm, self).clean()
|
|
||||||
title = cleaned_data.get('title')
|
|
||||||
existing_titles = self.fields['existing_titles'].initial
|
|
||||||
if title in existing_titles:
|
|
||||||
raise forms.ValidationError(_('Specified title already in use.'
|
|
||||||
' Please choose another one.'))
|
|
||||||
|
|
||||||
return title
|
|
|
@ -1,21 +0,0 @@
|
||||||
# Copyright (c) 2013 Mirantis, Inc.
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
||||||
# not use this file except in compliance with the License. You may obtain
|
|
||||||
# a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
||||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
||||||
# License for the specific language governing permissions and limitations
|
|
||||||
# under the License.
|
|
||||||
|
|
||||||
from django.utils.translation import ugettext_lazy as _
|
|
||||||
import horizon
|
|
||||||
|
|
||||||
|
|
||||||
class Images(horizon.Panel):
|
|
||||||
name = _("Images")
|
|
||||||
slug = 'images'
|
|
|
@ -1,79 +0,0 @@
|
||||||
# Copyright (c) 2013 Mirantis, Inc.
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
||||||
# not use this file except in compliance with the License. You may obtain
|
|
||||||
# a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
||||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
||||||
# License for the specific language governing permissions and limitations
|
|
||||||
# under the License.
|
|
||||||
|
|
||||||
from django.core.urlresolvers import reverse
|
|
||||||
from django.utils.translation import ugettext_lazy as _
|
|
||||||
from django.utils.translation import ungettext_lazy
|
|
||||||
from horizon import exceptions
|
|
||||||
from horizon import tables
|
|
||||||
from openstack_dashboard.api import glance
|
|
||||||
from openstack_dashboard import policy
|
|
||||||
|
|
||||||
from muranodashboard.common import utils as md_utils
|
|
||||||
|
|
||||||
|
|
||||||
class MarkImage(tables.LinkAction):
|
|
||||||
name = "mark_image"
|
|
||||||
verbose_name = _("Mark Image")
|
|
||||||
url = "horizon:app-catalog:images:mark_image"
|
|
||||||
classes = ("ajax-modal",)
|
|
||||||
icon = "plus"
|
|
||||||
policy_rules = (("murano", "mark_image"),)
|
|
||||||
|
|
||||||
|
|
||||||
class RemoveImageMetadata(policy.PolicyTargetMixin, tables.DeleteAction):
|
|
||||||
policy_rules = (("murano", "remove_image_metadata"),)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def action_present(count):
|
|
||||||
return ungettext_lazy(
|
|
||||||
u"Delete Metadata",
|
|
||||||
u"Delete Metadata",
|
|
||||||
count
|
|
||||||
)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def action_past(count):
|
|
||||||
return ungettext_lazy(
|
|
||||||
u"Deleted Metadata",
|
|
||||||
u"Deleted Metadata",
|
|
||||||
count
|
|
||||||
)
|
|
||||||
|
|
||||||
def delete(self, request, obj_id):
|
|
||||||
try:
|
|
||||||
remove_props = ['murano_image_info']
|
|
||||||
glance.image_update_properties(request, obj_id, remove_props)
|
|
||||||
except Exception:
|
|
||||||
exceptions.handle(request, _('Unable to remove metadata'),
|
|
||||||
redirect=reverse(
|
|
||||||
'horizon:app-catalog:images:index'))
|
|
||||||
|
|
||||||
|
|
||||||
class MarkedImagesTable(tables.DataTable):
|
|
||||||
image = tables.Column(
|
|
||||||
'name',
|
|
||||||
link='horizon:project:images:images:detail',
|
|
||||||
verbose_name=_('Image')
|
|
||||||
)
|
|
||||||
type = tables.Column(lambda obj: getattr(obj, 'type', None),
|
|
||||||
verbose_name=_('Type'))
|
|
||||||
title = md_utils.Column(lambda obj: getattr(obj, 'title', None),
|
|
||||||
verbose_name=_('Title'))
|
|
||||||
|
|
||||||
class Meta(object):
|
|
||||||
name = 'marked_images'
|
|
||||||
verbose_name = _('Marked Images')
|
|
||||||
table_actions = (MarkImage, RemoveImageMetadata)
|
|
||||||
row_actions = (RemoveImageMetadata,)
|
|
|
@ -1,26 +0,0 @@
|
||||||
# Copyright (c) 2013 Mirantis, Inc.
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
||||||
# not use this file except in compliance with the License. You may obtain
|
|
||||||
# a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
||||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
||||||
# License for the specific language governing permissions and limitations
|
|
||||||
# under the License.
|
|
||||||
|
|
||||||
from django.conf import urls
|
|
||||||
|
|
||||||
from muranodashboard.images import views
|
|
||||||
|
|
||||||
|
|
||||||
urlpatterns = [
|
|
||||||
urls.url(r'^$', views.MarkedImagesView.as_view(), name='index'),
|
|
||||||
urls.url(r'^mark_image$', views.MarkImageView.as_view(),
|
|
||||||
name='mark_image'),
|
|
||||||
urls.url(r'^remove_metadata$', views.MarkedImagesView.as_view(),
|
|
||||||
name='remove_metadata'),
|
|
||||||
]
|
|
|
@ -1,107 +0,0 @@
|
||||||
# Copyright (c) 2013 Mirantis, Inc.
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
||||||
# not use this file except in compliance with the License. You may obtain
|
|
||||||
# a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
||||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
||||||
# License for the specific language governing permissions and limitations
|
|
||||||
# under the License.
|
|
||||||
|
|
||||||
import itertools
|
|
||||||
|
|
||||||
from django.core.urlresolvers import reverse
|
|
||||||
from django.core.urlresolvers import reverse_lazy
|
|
||||||
from django.utils.translation import ugettext_lazy as _
|
|
||||||
|
|
||||||
from horizon import exceptions
|
|
||||||
from horizon.forms import views
|
|
||||||
from horizon import tables as horizon_tables
|
|
||||||
from horizon.utils import functions as utils
|
|
||||||
from openstack_dashboard.api import glance
|
|
||||||
|
|
||||||
from muranodashboard.images import forms
|
|
||||||
from muranodashboard.images import tables
|
|
||||||
|
|
||||||
|
|
||||||
class MarkedImagesView(horizon_tables.DataTableView):
|
|
||||||
table_class = tables.MarkedImagesTable
|
|
||||||
template_name = 'images/index.html'
|
|
||||||
page_title = _("Marked Images")
|
|
||||||
|
|
||||||
def has_prev_data(self, table):
|
|
||||||
return self._prev
|
|
||||||
|
|
||||||
def has_more_data(self, table):
|
|
||||||
return self._more
|
|
||||||
|
|
||||||
def get_data(self):
|
|
||||||
prev_marker = self.request.GET.get(
|
|
||||||
tables.MarkedImagesTable._meta.prev_pagination_param, None)
|
|
||||||
|
|
||||||
if prev_marker is not None:
|
|
||||||
sort_dir = 'asc'
|
|
||||||
marker = prev_marker
|
|
||||||
else:
|
|
||||||
sort_dir = 'desc'
|
|
||||||
marker = self.request.GET.get(
|
|
||||||
tables.MarkedImagesTable._meta.pagination_param, None)
|
|
||||||
|
|
||||||
page_size = utils.get_page_size(self.request)
|
|
||||||
|
|
||||||
request_size = page_size + 1
|
|
||||||
kwargs = {'filters': {}}
|
|
||||||
if marker:
|
|
||||||
kwargs['marker'] = marker
|
|
||||||
kwargs['sort_dir'] = sort_dir
|
|
||||||
images = []
|
|
||||||
self._prev = False
|
|
||||||
self._more = False
|
|
||||||
|
|
||||||
glance_v2_client = glance.glanceclient(self.request, "2")
|
|
||||||
|
|
||||||
try:
|
|
||||||
images_iter = glance_v2_client.images.list(
|
|
||||||
**kwargs)
|
|
||||||
except Exception:
|
|
||||||
msg = _('Unable to retrieve list of images')
|
|
||||||
uri = reverse('horizon:app-catalog:catalog:index')
|
|
||||||
|
|
||||||
exceptions.handle(self.request, msg, redirect=uri)
|
|
||||||
|
|
||||||
marked_images_iter = forms.filter_murano_images(
|
|
||||||
images_iter,
|
|
||||||
request=self.request)
|
|
||||||
images = list(itertools.islice(marked_images_iter, request_size))
|
|
||||||
# first and middle page condition
|
|
||||||
if len(images) > page_size:
|
|
||||||
images.pop(-1)
|
|
||||||
self._more = True
|
|
||||||
# middle page condition
|
|
||||||
if marker is not None:
|
|
||||||
self._prev = True
|
|
||||||
# first page condition when reached via prev back
|
|
||||||
elif sort_dir == 'asc' and marker is not None:
|
|
||||||
self._more = True
|
|
||||||
# last page condition
|
|
||||||
elif marker is not None:
|
|
||||||
self._prev = True
|
|
||||||
if prev_marker is not None:
|
|
||||||
images.reverse()
|
|
||||||
return images
|
|
||||||
|
|
||||||
|
|
||||||
class MarkImageView(views.ModalFormView):
|
|
||||||
form_class = forms.MarkImageForm
|
|
||||||
form_id = 'mark_murano_image_form'
|
|
||||||
modal_header = _('Add Murano Metadata')
|
|
||||||
template_name = 'images/mark.html'
|
|
||||||
context_object_name = 'image'
|
|
||||||
page_title = _("Update Image")
|
|
||||||
success_url = reverse_lazy('horizon:app-catalog:images:index')
|
|
||||||
submit_label = _('Mark Image')
|
|
||||||
submit_url = reverse_lazy('horizon:app-catalog:images:mark_image')
|
|
|
@ -1,5 +0,0 @@
|
||||||
# The name of the dashboard to be added to HORIZON['dashboards']. Required.
|
|
||||||
DASHBOARD = 'app-catalog'
|
|
||||||
|
|
||||||
# If set to True, this dashboard will not be added to the settings.
|
|
||||||
DISABLED = False
|
|
|
@ -1,47 +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 muranodashboard import exceptions
|
|
||||||
|
|
||||||
ADD_INSTALLED_APPS = [
|
|
||||||
'muranodashboard',
|
|
||||||
]
|
|
||||||
|
|
||||||
ADD_EXCEPTIONS = {
|
|
||||||
'recoverable': exceptions.RECOVERABLE,
|
|
||||||
'not_found': exceptions.NOT_FOUND,
|
|
||||||
'unauthorized': exceptions.UNAUTHORIZED,
|
|
||||||
}
|
|
||||||
|
|
||||||
ADD_ANGULAR_MODULES = ['horizon.app.murano']
|
|
||||||
|
|
||||||
ADD_JS_FILES = [
|
|
||||||
'muranodashboard/js/upload_form.js',
|
|
||||||
'muranodashboard/js/import_bundle_form.js',
|
|
||||||
'muranodashboard/js/more-less.js',
|
|
||||||
'app/murano/murano.service.js',
|
|
||||||
'app/murano/murano.module.js',
|
|
||||||
'muranodashboard/js/add-select.js',
|
|
||||||
'muranodashboard/js/draggable-components.js',
|
|
||||||
'muranodashboard/js/environments-in-place.js',
|
|
||||||
'muranodashboard/js/external-ad.js',
|
|
||||||
'muranodashboard/js/horizon.muranotopology.js',
|
|
||||||
'muranodashboard/js/murano.tables.js',
|
|
||||||
'muranodashboard/js/load-modals.js',
|
|
||||||
'muranodashboard/js/mixed-mode.js',
|
|
||||||
'muranodashboard/js/passwordfield.js',
|
|
||||||
'muranodashboard/js/submit-disabled.js',
|
|
||||||
'muranodashboard/js/support_placeholder.js',
|
|
||||||
'muranodashboard/js/validators.js'
|
|
||||||
]
|
|
||||||
|
|
||||||
FEATURE = True
|
|
|
@ -1,8 +0,0 @@
|
||||||
from django.utils.translation import ugettext_lazy as _
|
|
||||||
|
|
||||||
# The name of the panel group to be added to HORIZON_CONFIG. Required.
|
|
||||||
PANEL_GROUP = 'app-catalog_browse_group'
|
|
||||||
# The display name of the PANEL_GROUP. Required.
|
|
||||||
PANEL_GROUP_NAME = _('Browse')
|
|
||||||
# The name of the dashboard the PANEL_GROUP associated with. Required.
|
|
||||||
PANEL_GROUP_DASHBOARD = 'app-catalog'
|
|
|
@ -1,9 +0,0 @@
|
||||||
# The name of the panel to be added to HORIZON_CONFIG. Required.
|
|
||||||
PANEL = 'catalog'
|
|
||||||
# The name of the dashboard the PANEL associated with. Required.
|
|
||||||
PANEL_DASHBOARD = 'app-catalog'
|
|
||||||
# The name of the panel group the PANEL is associated with.
|
|
||||||
PANEL_GROUP = 'app-catalog_browse_group'
|
|
||||||
|
|
||||||
# Python panel class of the PANEL to be added.
|
|
||||||
ADD_PANEL = 'muranodashboard.catalog.panel.AppCatalog'
|
|
|
@ -1,8 +0,0 @@
|
||||||
from django.utils.translation import ugettext_lazy as _
|
|
||||||
|
|
||||||
# The name of the panel group to be added to HORIZON_CONFIG. Required.
|
|
||||||
PANEL_GROUP = 'app-catalog_manage_group'
|
|
||||||
# The display name of the PANEL_GROUP. Required.
|
|
||||||
PANEL_GROUP_NAME = _('Manage')
|
|
||||||
# The name of the dashboard the PANEL_GROUP associated with. Required.
|
|
||||||
PANEL_GROUP_DASHBOARD = 'app-catalog'
|
|
|
@ -1,9 +0,0 @@
|
||||||
# The name of the panel to be added to HORIZON_CONFIG. Required.
|
|
||||||
PANEL = 'packages'
|
|
||||||
# The name of the dashboard the PANEL associated with. Required.
|
|
||||||
PANEL_DASHBOARD = 'app-catalog'
|
|
||||||
# The name of the panel group the PANEL is associated with.
|
|
||||||
PANEL_GROUP = 'app-catalog_manage_group'
|
|
||||||
|
|
||||||
# Python panel class of the PANEL to be added.
|
|
||||||
ADD_PANEL = 'muranodashboard.packages.panel.PackageDefinitions'
|
|
|
@ -1,9 +0,0 @@
|
||||||
# The name of the panel to be added to HORIZON_CONFIG. Required.
|
|
||||||
PANEL = 'images'
|
|
||||||
# The name of the dashboard the PANEL associated with. Required.
|
|
||||||
PANEL_DASHBOARD = 'app-catalog'
|
|
||||||
# The name of the panel group the PANEL is associated with.
|
|
||||||
PANEL_GROUP = 'app-catalog_manage_group'
|
|
||||||
|
|
||||||
# Python panel class of the PANEL to be added.
|
|
||||||
ADD_PANEL = 'muranodashboard.images.panel.Images'
|
|
|
@ -1,9 +0,0 @@
|
||||||
# The name of the panel to be added to HORIZON_CONFIG. Required.
|
|
||||||
PANEL = 'categories'
|
|
||||||
# The name of the dashboard the PANEL associated with. Required.
|
|
||||||
PANEL_DASHBOARD = 'app-catalog'
|
|
||||||
# The name of the panel group the PANEL is associated with.
|
|
||||||
PANEL_GROUP = 'app-catalog_manage_group'
|
|
||||||
|
|
||||||
# Python panel class of the PANEL to be added.
|
|
||||||
ADD_PANEL = 'muranodashboard.categories.panel.Categories'
|
|
|
@ -1,8 +0,0 @@
|
||||||
from django.utils.translation import ugettext_lazy as _
|
|
||||||
|
|
||||||
# The name of the panel group to be added to HORIZON_CONFIG. Required.
|
|
||||||
PANEL_GROUP = 'app-catalog_applications_group'
|
|
||||||
# The display name of the PANEL_GROUP. Required.
|
|
||||||
PANEL_GROUP_NAME = _('Applications')
|
|
||||||
# The name of the dashboard the PANEL_GROUP associated with. Required.
|
|
||||||
PANEL_GROUP_DASHBOARD = 'app-catalog'
|
|
|
@ -1,9 +0,0 @@
|
||||||
# The name of the panel to be added to HORIZON_CONFIG. Required.
|
|
||||||
PANEL = 'environments'
|
|
||||||
# The name of the dashboard the PANEL associated with. Required.
|
|
||||||
PANEL_DASHBOARD = 'app-catalog'
|
|
||||||
# The name of the panel group the PANEL is associated with.
|
|
||||||
PANEL_GROUP = 'app-catalog_applications_group'
|
|
||||||
|
|
||||||
# Python panel class of the PANEL to be added.
|
|
||||||
ADD_PANEL = 'muranodashboard.environments.panel.Environments'
|
|
|
@ -1,49 +0,0 @@
|
||||||
# MURANO_API_URL = "http://localhost:8082"
|
|
||||||
|
|
||||||
# Set to True to use Glare Artifact Repository to store murano packages
|
|
||||||
MURANO_USE_GLARE = False
|
|
||||||
|
|
||||||
# Sets the Glare API endpoint to interact with Artifact Repo.
|
|
||||||
# If left commented the one from keystone will be used
|
|
||||||
# GLARE_API_URL = 'http://ubuntu1:9494'
|
|
||||||
|
|
||||||
MURANO_REPO_URL = 'http://apps.openstack.org/api/v1/murano_repo/liberty/'
|
|
||||||
|
|
||||||
DISPLAY_MURANO_REPO_URL = 'http://apps.openstack.org/#tab=murano-apps'
|
|
||||||
|
|
||||||
# Overrides the default dashboard name (App Catalog) that is displayed
|
|
||||||
# in the main accordion navigation
|
|
||||||
# MURANO_DASHBOARD_NAME = "App Catalog"
|
|
||||||
|
|
||||||
# Specify a maximum number of limit packages.
|
|
||||||
# PACKAGES_LIMIT = 100
|
|
||||||
|
|
||||||
# Make sure horizon has config the DATABASES, If horizon config use horizon's
|
|
||||||
# DATABASES, if not, set it by murano.
|
|
||||||
try:
|
|
||||||
from openstack_dashboard.settings import DATABASES
|
|
||||||
DATABASES_CONFIG = DATABASES.has_key('default')
|
|
||||||
except ImportError:
|
|
||||||
DATABASES_CONFIG = False
|
|
||||||
|
|
||||||
# Set default session backend from browser cookies to database to
|
|
||||||
# avoid issues with forms during creating applications.
|
|
||||||
if not DATABASES_CONFIG:
|
|
||||||
DATABASES = {
|
|
||||||
'default': {
|
|
||||||
'ENGINE': 'django.db.backends.sqlite3',
|
|
||||||
'NAME': 'murano-dashboard.sqlite',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
SESSION_ENGINE = 'django.contrib.sessions.backends.db'
|
|
||||||
|
|
||||||
try:
|
|
||||||
from openstack_dashboard import static_settings
|
|
||||||
LEGACY_STATIC_SETTINGS = True
|
|
||||||
except ImportError:
|
|
||||||
LEGACY_STATIC_SETTINGS = False
|
|
||||||
|
|
||||||
HORIZON_CONFIG['legacy_static_settings'] = LEGACY_STATIC_SETTINGS
|
|
||||||
|
|
||||||
# from openstack_dashboard.settings import POLICY_FILES
|
|
||||||
POLICY_FILES.update({'murano': 'murano_policy.json',})
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,84 +0,0 @@
|
||||||
# Stanislav Ulrych <stanislav.ulrych@ultimum.io>, 2016. #zanata
|
|
||||||
msgid ""
|
|
||||||
msgstr ""
|
|
||||||
"Project-Id-Version: murano-dashboard 3.0.0.0rc2.dev145\n"
|
|
||||||
"Report-Msgid-Bugs-To: https://bugs.launchpad.net/openstack-i18n/\n"
|
|
||||||
"POT-Creation-Date: 2016-11-24 15:30+0000\n"
|
|
||||||
"MIME-Version: 1.0\n"
|
|
||||||
"Content-Type: text/plain; charset=UTF-8\n"
|
|
||||||
"Content-Transfer-Encoding: 8bit\n"
|
|
||||||
"PO-Revision-Date: 2016-11-22 08:55+0000\n"
|
|
||||||
"Last-Translator: Stanislav Ulrych <stanislav.ulrych@ultimum.io>\n"
|
|
||||||
"Language-Team: Czech\n"
|
|
||||||
"Language: cs\n"
|
|
||||||
"X-Generator: Zanata 3.7.3\n"
|
|
||||||
"Plural-Forms: nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2\n"
|
|
||||||
|
|
||||||
msgid " 1 capital letter"
|
|
||||||
msgstr "1 velké písmeno"
|
|
||||||
|
|
||||||
msgid " 1 digit"
|
|
||||||
msgstr "1 číslice"
|
|
||||||
|
|
||||||
msgid " 1 non-capital letter"
|
|
||||||
msgstr "1 malé písméno"
|
|
||||||
|
|
||||||
msgid " 1 special character"
|
|
||||||
msgstr "1 speciální charakter"
|
|
||||||
|
|
||||||
msgid " 7 characters"
|
|
||||||
msgstr "7 znaků"
|
|
||||||
|
|
||||||
msgid "An error occurred. Please try again later."
|
|
||||||
msgstr "Vyskytla se chyba. Prosím zkuste to znovu později."
|
|
||||||
|
|
||||||
msgid "Cancel"
|
|
||||||
msgstr "Zrušit"
|
|
||||||
|
|
||||||
msgid "Create"
|
|
||||||
msgstr "Vytvořit"
|
|
||||||
|
|
||||||
msgid "Loading"
|
|
||||||
msgstr "Načítání"
|
|
||||||
|
|
||||||
msgid "New"
|
|
||||||
msgstr "Nový"
|
|
||||||
|
|
||||||
msgid "Passwords do not match"
|
|
||||||
msgstr "Hesla se neshodují."
|
|
||||||
|
|
||||||
msgid "Show less"
|
|
||||||
msgstr "Zobrazit méně"
|
|
||||||
|
|
||||||
msgid "Show more"
|
|
||||||
msgstr "Zobrazit více"
|
|
||||||
|
|
||||||
msgid "There was an error submitting the form. Please try again."
|
|
||||||
msgstr "Při odesílání formuláře nastal problém. Zkuste to prosím znovu."
|
|
||||||
|
|
||||||
msgid "Unable to edit component metadata."
|
|
||||||
msgstr "Nelze upravit metadata komponenty."
|
|
||||||
|
|
||||||
msgid "Unable to edit environment metadata."
|
|
||||||
msgstr "Nelze upravit metadata prostředí."
|
|
||||||
|
|
||||||
msgid "Unable to retrieve component metadata."
|
|
||||||
msgstr "Nelze získat metadata komponenty."
|
|
||||||
|
|
||||||
msgid "Unable to retrieve environment metadata."
|
|
||||||
msgstr "Nelze získat metadata prostředí."
|
|
||||||
|
|
||||||
msgid "Unable to retrieve the packages."
|
|
||||||
msgstr "Nelze získat balíčky."
|
|
||||||
|
|
||||||
msgid "Unable to run action."
|
|
||||||
msgstr "Nelze provést akci."
|
|
||||||
|
|
||||||
msgid "Waiting for a result"
|
|
||||||
msgstr "Čekání na výsledek"
|
|
||||||
|
|
||||||
msgid "Working"
|
|
||||||
msgstr "Zpracovávání"
|
|
||||||
|
|
||||||
msgid "Your password should have at least"
|
|
||||||
msgstr "Vaše heslo by mělo mít nejméně"
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,87 +0,0 @@
|
||||||
# Frank Kloeker <eumel@arcor.de>, 2016. #zanata
|
|
||||||
# Robert Simai <robert.simai@suse.com>, 2016. #zanata
|
|
||||||
msgid ""
|
|
||||||
msgstr ""
|
|
||||||
"Project-Id-Version: murano-dashboard 3.0.0.0rc2.dev56\n"
|
|
||||||
"Report-Msgid-Bugs-To: https://bugs.launchpad.net/openstack-i18n/\n"
|
|
||||||
"POT-Creation-Date: 2016-10-17 14:17+0000\n"
|
|
||||||
"MIME-Version: 1.0\n"
|
|
||||||
"Content-Type: text/plain; charset=UTF-8\n"
|
|
||||||
"Content-Transfer-Encoding: 8bit\n"
|
|
||||||
"PO-Revision-Date: 2016-10-18 10:41+0000\n"
|
|
||||||
"Last-Translator: Robert Simai <robert.simai@suse.com>\n"
|
|
||||||
"Language-Team: German\n"
|
|
||||||
"Language: de\n"
|
|
||||||
"X-Generator: Zanata 3.7.3\n"
|
|
||||||
"Plural-Forms: nplurals=2; plural=(n != 1)\n"
|
|
||||||
|
|
||||||
msgid " 1 capital letter"
|
|
||||||
msgstr "1 Grossbuchstabe"
|
|
||||||
|
|
||||||
msgid " 1 digit"
|
|
||||||
msgstr "1 Ziffer"
|
|
||||||
|
|
||||||
msgid " 1 non-capital letter"
|
|
||||||
msgstr "1 Kleinbuchstabe"
|
|
||||||
|
|
||||||
msgid " 1 special character"
|
|
||||||
msgstr "1 Sonderzeichen"
|
|
||||||
|
|
||||||
msgid " 7 characters"
|
|
||||||
msgstr "7 Zeichen"
|
|
||||||
|
|
||||||
msgid "An error occurred. Please try again later."
|
|
||||||
msgstr "Ein Fehler ist aufgetreten. Bitte versuchen Sie es später noch einmal."
|
|
||||||
|
|
||||||
msgid "Cancel"
|
|
||||||
msgstr "Abbrechen"
|
|
||||||
|
|
||||||
msgid "Create"
|
|
||||||
msgstr "Erstellen"
|
|
||||||
|
|
||||||
msgid "Loading"
|
|
||||||
msgstr "Ladevorgang"
|
|
||||||
|
|
||||||
msgid "New"
|
|
||||||
msgstr "Neu"
|
|
||||||
|
|
||||||
msgid "Passwords do not match"
|
|
||||||
msgstr "Passwörter stimmen nicht überein"
|
|
||||||
|
|
||||||
msgid "Show less"
|
|
||||||
msgstr "Zeige weniger"
|
|
||||||
|
|
||||||
msgid "Show more"
|
|
||||||
msgstr "Zeige mehr"
|
|
||||||
|
|
||||||
msgid "There was an error submitting the form. Please try again."
|
|
||||||
msgstr ""
|
|
||||||
"Beim Absenden des Formulars ist ein Fehler aufgetreten. Bitte versuchen Sie "
|
|
||||||
"es nochmal."
|
|
||||||
|
|
||||||
msgid "Unable to edit component metadata."
|
|
||||||
msgstr "Komponenten-Metadaten können nicht bearbeitet werden."
|
|
||||||
|
|
||||||
msgid "Unable to edit environment metadata."
|
|
||||||
msgstr "Umgebungs-Metadaten können nicht bearbeitet werden."
|
|
||||||
|
|
||||||
msgid "Unable to retrieve component metadata."
|
|
||||||
msgstr "Komponenten-Metadaten können nicht abgerufen werden."
|
|
||||||
|
|
||||||
msgid "Unable to retrieve environment metadata."
|
|
||||||
msgstr "Umgebungs-Metadaten können nicht abgerufen werden."
|
|
||||||
|
|
||||||
msgid "Unable to retrieve the packages."
|
|
||||||
msgstr "Pakete können nicht abgerufen werden."
|
|
||||||
|
|
||||||
msgid "Unable to run action."
|
|
||||||
msgstr "Aktion kann nicht ausgeführt werden."
|
|
||||||
|
|
||||||
msgid "Waiting for a result"
|
|
||||||
msgstr "Bitte warten"
|
|
||||||
|
|
||||||
msgid "Working"
|
|
||||||
msgstr "In Arbeit"
|
|
||||||
|
|
||||||
msgid "Your password should have at least"
|
|
||||||
msgstr "Ihr Passwort sollte wenigstens enthalten:"
|
|
|
@ -1,845 +0,0 @@
|
||||||
# Andreas Jaeger <jaegerandi@gmail.com>, 2016. #zanata
|
|
||||||
# Gérald LONLAS <g.lonlas@gmail.com>, 2016. #zanata
|
|
||||||
msgid ""
|
|
||||||
msgstr ""
|
|
||||||
"Project-Id-Version: murano-dashboard 4.0.0.0b3.dev4\n"
|
|
||||||
"Report-Msgid-Bugs-To: https://bugs.launchpad.net/openstack-i18n/\n"
|
|
||||||
"POT-Creation-Date: 2017-06-10 02:57+0000\n"
|
|
||||||
"MIME-Version: 1.0\n"
|
|
||||||
"Content-Type: text/plain; charset=UTF-8\n"
|
|
||||||
"Content-Transfer-Encoding: 8bit\n"
|
|
||||||
"PO-Revision-Date: 2016-11-08 10:31+0000\n"
|
|
||||||
"Last-Translator: Gérald LONLAS <g.lonlas@gmail.com>\n"
|
|
||||||
"Language-Team: French\n"
|
|
||||||
"Language: fr\n"
|
|
||||||
"X-Generator: Zanata 3.9.6\n"
|
|
||||||
"Plural-Forms: nplurals=2; plural=(n > 1)\n"
|
|
||||||
|
|
||||||
msgid "-"
|
|
||||||
msgstr "-"
|
|
||||||
|
|
||||||
msgid "80 characters max."
|
|
||||||
msgstr "80 caractères max."
|
|
||||||
|
|
||||||
msgid "A local zip file to upload"
|
|
||||||
msgstr "Fichier Zip local à envoyer"
|
|
||||||
|
|
||||||
msgid "Abandon Environment"
|
|
||||||
msgid_plural "Abandon Environments"
|
|
||||||
msgstr[0] "Abandonner l'environnement"
|
|
||||||
msgstr[1] "Abandonner les environnements"
|
|
||||||
|
|
||||||
msgid "Abandoned Environment"
|
|
||||||
msgid_plural "Abandoned Environments"
|
|
||||||
msgstr[0] "Environnement abandonné"
|
|
||||||
msgstr[1] "Environnements abandonnés"
|
|
||||||
|
|
||||||
msgid "Active"
|
|
||||||
msgstr "Active"
|
|
||||||
|
|
||||||
msgid "Add"
|
|
||||||
msgstr "Ajouter"
|
|
||||||
|
|
||||||
msgid "Add Application"
|
|
||||||
msgstr "Ajouter une application"
|
|
||||||
|
|
||||||
msgid "Add Application Category"
|
|
||||||
msgstr "Ajouter une catégorie d'application."
|
|
||||||
|
|
||||||
msgid "Add Category"
|
|
||||||
msgstr "Ajouter une catégorie"
|
|
||||||
|
|
||||||
msgid "Add Component"
|
|
||||||
msgstr "Ajout d'un composant"
|
|
||||||
|
|
||||||
msgid "Add Murano Metadata"
|
|
||||||
msgstr "Ajouter des métadonnées Murano"
|
|
||||||
|
|
||||||
msgid "Add New"
|
|
||||||
msgstr "Ajouter un nouveau"
|
|
||||||
|
|
||||||
msgid "Add new category to the application catalog."
|
|
||||||
msgstr "Ajouter une nouvelle catégorie au catalogue de l'application"
|
|
||||||
|
|
||||||
msgid "Add to Env"
|
|
||||||
msgstr "Ajouter à l'Env"
|
|
||||||
|
|
||||||
msgid "Adding application to an environment failed."
|
|
||||||
msgstr "Échec de l'ajout de l'application à l'environnement."
|
|
||||||
|
|
||||||
msgid "All"
|
|
||||||
msgstr "Tout"
|
|
||||||
|
|
||||||
msgid "An external http/https URL to load the bundle from."
|
|
||||||
msgstr "Une URL externe HTTP/HTTPS à partir de laquelle charger le bundle."
|
|
||||||
|
|
||||||
msgid "An external http/https URL to load the package from."
|
|
||||||
msgstr "Une URL externe HTTP/HTTPS à partir de laquelle charger le paquet."
|
|
||||||
|
|
||||||
msgid "App Catalog"
|
|
||||||
msgstr "Catalogue d'application"
|
|
||||||
|
|
||||||
msgid "App Category:"
|
|
||||||
msgstr "Catégorie de l'app :"
|
|
||||||
|
|
||||||
msgid "App category"
|
|
||||||
msgstr "Catégorie de l'app"
|
|
||||||
|
|
||||||
msgid "Application Categories"
|
|
||||||
msgstr "Catégories d'application."
|
|
||||||
|
|
||||||
msgid "Application Category"
|
|
||||||
msgstr "Catégorie de l'application"
|
|
||||||
|
|
||||||
msgid "Application Details"
|
|
||||||
msgstr "Détails de l'application"
|
|
||||||
|
|
||||||
msgid "Application Package"
|
|
||||||
msgstr "Package d'application"
|
|
||||||
|
|
||||||
msgid "Application Components"
|
|
||||||
msgstr "Composants d'applications"
|
|
||||||
|
|
||||||
msgid "Applications"
|
|
||||||
msgstr "Applications"
|
|
||||||
|
|
||||||
msgid "Author"
|
|
||||||
msgstr "Auteur"
|
|
||||||
|
|
||||||
msgid "Auto"
|
|
||||||
msgstr "Auto"
|
|
||||||
|
|
||||||
msgid "Back"
|
|
||||||
msgstr "Retour"
|
|
||||||
|
|
||||||
msgid "Browse"
|
|
||||||
msgstr "Parcourir"
|
|
||||||
|
|
||||||
msgid "Browse Local"
|
|
||||||
msgstr "Parcourir le dossier local"
|
|
||||||
|
|
||||||
msgid "Bundle creation failed.Reason: {0}"
|
|
||||||
msgstr "La création du bundle a échoué. Raison : {0}"
|
|
||||||
|
|
||||||
msgid "Cancel"
|
|
||||||
msgstr "Annuler"
|
|
||||||
|
|
||||||
msgid "Categories"
|
|
||||||
msgstr "Catégories"
|
|
||||||
|
|
||||||
msgid "Category Name"
|
|
||||||
msgstr "Nom de la catégorie"
|
|
||||||
|
|
||||||
msgid "Category {0} created."
|
|
||||||
msgstr "Catégorie {0} créée."
|
|
||||||
|
|
||||||
msgid "Check Keystone configuration of murano-api server."
|
|
||||||
msgstr "Vérifiez la configuration de Keystone du service murano-api."
|
|
||||||
|
|
||||||
msgid "Choose a Zip archive to upload into the catalog."
|
|
||||||
msgstr "Choisir une archive Zip pour l'envoi dans le catalogue."
|
|
||||||
|
|
||||||
msgid "Choose a name for the environment"
|
|
||||||
msgstr "Choisir un nom pour cet environnement"
|
|
||||||
|
|
||||||
msgid "Click to create new environment"
|
|
||||||
msgstr "Cliquer pour créer un nouvel environnement"
|
|
||||||
|
|
||||||
msgid "Completed with warnings"
|
|
||||||
msgstr "Terminé avec avertissements"
|
|
||||||
|
|
||||||
msgid "Component"
|
|
||||||
msgstr "Composant"
|
|
||||||
|
|
||||||
msgid "Component Details"
|
|
||||||
msgstr "Détails du composant"
|
|
||||||
|
|
||||||
msgid "Component List"
|
|
||||||
msgstr "Liste des composants"
|
|
||||||
|
|
||||||
msgid "Component Logs"
|
|
||||||
msgstr "Journaux du composant"
|
|
||||||
|
|
||||||
msgid "Components"
|
|
||||||
msgstr "Composants "
|
|
||||||
|
|
||||||
msgid "Configuration"
|
|
||||||
msgstr "Configuration"
|
|
||||||
|
|
||||||
msgid "Configure Application"
|
|
||||||
msgstr "Configurer l'application"
|
|
||||||
|
|
||||||
msgid "Confirm password"
|
|
||||||
msgstr "Confirmer le mot de passe"
|
|
||||||
|
|
||||||
msgid "Couldn't update package {0} parameters. Error: {1}"
|
|
||||||
msgstr "Impossible de mettre à jour les paramètres du paquet {0}. Erreur : {1}"
|
|
||||||
|
|
||||||
msgid "Create"
|
|
||||||
msgstr "Créer"
|
|
||||||
|
|
||||||
msgid "Create Env"
|
|
||||||
msgstr "Créer un Env"
|
|
||||||
|
|
||||||
msgid "Create Environment"
|
|
||||||
msgstr "Créer un environnement"
|
|
||||||
|
|
||||||
msgid "Create New"
|
|
||||||
msgstr "Créer un nouveau"
|
|
||||||
|
|
||||||
msgid "Create a title for an image."
|
|
||||||
msgstr "Créer un titre pour une image"
|
|
||||||
|
|
||||||
msgid "Created"
|
|
||||||
msgstr "Créé"
|
|
||||||
|
|
||||||
msgid "Custom Type"
|
|
||||||
msgstr "Type personnalisée"
|
|
||||||
|
|
||||||
msgid "Delete Category"
|
|
||||||
msgid_plural "Delete Categories"
|
|
||||||
msgstr[0] "Supprimer la catégorie."
|
|
||||||
msgstr[1] "Supprimer les catégories."
|
|
||||||
|
|
||||||
msgid "Delete Component"
|
|
||||||
msgid_plural "Delete Components"
|
|
||||||
msgstr[0] "Supprimer un composant"
|
|
||||||
msgstr[1] "Supprimer les composants"
|
|
||||||
|
|
||||||
msgid "Delete Environment"
|
|
||||||
msgid_plural "Delete Environments"
|
|
||||||
msgstr[0] "Supprimer l'environnement"
|
|
||||||
msgstr[1] "Supprimer les environnements"
|
|
||||||
|
|
||||||
msgid "Delete Metadata"
|
|
||||||
msgid_plural "Delete Metadata"
|
|
||||||
msgstr[0] "Supprimer les Metadata"
|
|
||||||
msgstr[1] "Supprimer les Metadata"
|
|
||||||
|
|
||||||
msgid "Delete Package"
|
|
||||||
msgid_plural "Delete Packages"
|
|
||||||
msgstr[0] "Supprimer le package"
|
|
||||||
msgstr[1] "Supprimer les packages"
|
|
||||||
|
|
||||||
msgid "Delete failure"
|
|
||||||
msgstr "Échec de la suppression"
|
|
||||||
|
|
||||||
msgid "Deleted Category"
|
|
||||||
msgid_plural "Deleted Categories"
|
|
||||||
msgstr[0] "Catégorie supprimée."
|
|
||||||
msgstr[1] "Catégories supprimées."
|
|
||||||
|
|
||||||
msgid "Deleted Metadata"
|
|
||||||
msgid_plural "Deleted Metadata"
|
|
||||||
msgstr[0] "Metadata en cours de suppression"
|
|
||||||
msgstr[1] "Metadata en cours de suppression"
|
|
||||||
|
|
||||||
msgid "Deleted Package"
|
|
||||||
msgid_plural "Deleted Packages"
|
|
||||||
msgstr[0] "Supprimer le package"
|
|
||||||
msgstr[1] "Supprimer les packages"
|
|
||||||
|
|
||||||
msgid "Deleting"
|
|
||||||
msgstr "Suppression en cours"
|
|
||||||
|
|
||||||
msgid "Deploy Environment"
|
|
||||||
msgid_plural "Deploy Environments"
|
|
||||||
msgstr[0] "Déployer l'environnement"
|
|
||||||
msgstr[1] "Déployer les environnements"
|
|
||||||
|
|
||||||
msgid "Deploy This Environment"
|
|
||||||
msgstr "Déployer cet environnement"
|
|
||||||
|
|
||||||
msgid "Deploy failure"
|
|
||||||
msgstr "Échec du déploiement"
|
|
||||||
|
|
||||||
msgid "Deploy started"
|
|
||||||
msgstr "Déploiement commencé"
|
|
||||||
|
|
||||||
msgid "Deployed Components"
|
|
||||||
msgstr "Composants déployés"
|
|
||||||
|
|
||||||
msgid "Deploying"
|
|
||||||
msgstr "En cours de déploiement"
|
|
||||||
|
|
||||||
msgid "Deployment Details"
|
|
||||||
msgstr "Détail des déploiements"
|
|
||||||
|
|
||||||
msgid "Deployment History"
|
|
||||||
msgstr "Historique du déploiement"
|
|
||||||
|
|
||||||
msgid "Deployment Logs"
|
|
||||||
msgstr "Journaux de déploiement"
|
|
||||||
|
|
||||||
#, python-format
|
|
||||||
msgid "Deployment with id %s doesn't exist anymore"
|
|
||||||
msgstr "Déploiement avec l'ID %s n'existe plus"
|
|
||||||
|
|
||||||
msgid "Deployments"
|
|
||||||
msgstr "Déploiements"
|
|
||||||
|
|
||||||
msgid "Description"
|
|
||||||
msgstr "Description"
|
|
||||||
|
|
||||||
msgid "Details"
|
|
||||||
msgstr "Détails"
|
|
||||||
|
|
||||||
msgid "Download Package"
|
|
||||||
msgstr "Télécharger le paquet"
|
|
||||||
|
|
||||||
msgid "Drop Components here"
|
|
||||||
msgstr "Déposer les composants ici"
|
|
||||||
|
|
||||||
msgid "Enabled"
|
|
||||||
msgstr "Activé"
|
|
||||||
|
|
||||||
msgid ""
|
|
||||||
"Enter a complex password with at least one letter, one number and one "
|
|
||||||
"special character"
|
|
||||||
msgstr ""
|
|
||||||
"Entrer un mot de passe complexe comprenant au moins une lettre, "
|
|
||||||
"un nombre et un caractère spécial"
|
|
||||||
|
|
||||||
msgid "Enter a password"
|
|
||||||
msgstr "Entrez un mot de passe"
|
|
||||||
|
|
||||||
msgid "Environment"
|
|
||||||
msgstr "Environnement"
|
|
||||||
|
|
||||||
msgid "Environment Name"
|
|
||||||
msgstr "Nom de l'environnement"
|
|
||||||
|
|
||||||
#, python-format
|
|
||||||
msgid "Environment with id %s doesn't exist anymore"
|
|
||||||
msgstr "Environnement avec l'ID %s n'existe plus"
|
|
||||||
|
|
||||||
msgid "Environments"
|
|
||||||
msgstr "Environnements"
|
|
||||||
|
|
||||||
msgid "FQN"
|
|
||||||
msgstr "FQN"
|
|
||||||
|
|
||||||
msgid "Failed"
|
|
||||||
msgstr "Échec"
|
|
||||||
|
|
||||||
msgid "Failed to create environment"
|
|
||||||
msgstr "Échec de la création de l'environnement "
|
|
||||||
|
|
||||||
msgid "File"
|
|
||||||
msgstr "Fichier"
|
|
||||||
|
|
||||||
msgid "Filter"
|
|
||||||
msgstr "Filtrer"
|
|
||||||
|
|
||||||
msgid "Find in a selected category"
|
|
||||||
msgstr "Trouvé dans la catégorie sélectionnée"
|
|
||||||
|
|
||||||
msgid "Foo"
|
|
||||||
msgstr "Foo"
|
|
||||||
|
|
||||||
#, python-format
|
|
||||||
msgid ""
|
|
||||||
"Go to <b> <a href=%(pkg_def_url)s>Packages</a> </b>, click 'Import Package' "
|
|
||||||
"and select <i>Repository</i> as <i>Package Source</i>."
|
|
||||||
msgstr ""
|
|
||||||
"Aller à <b> <a href=%(pkg_def_url)s>Paquets</a> </b>, cliquez sur 'Importer "
|
|
||||||
"un paquet' et sélectionnez <i>Dépôt</i> en tant que <i>source de paquet</i>."
|
|
||||||
|
|
||||||
msgid "Heat Orchestration stack name"
|
|
||||||
msgstr "Nom de la pile d'orchestration Heat"
|
|
||||||
|
|
||||||
msgid "Heat Orchestration stack%(forloop.counter)s name"
|
|
||||||
msgstr "Nom de la pile d'orchestration Heat %(forloop.counter)s"
|
|
||||||
|
|
||||||
msgid "ID"
|
|
||||||
msgstr "ID"
|
|
||||||
|
|
||||||
msgid "Image"
|
|
||||||
msgstr "Image"
|
|
||||||
|
|
||||||
msgid "Image Title"
|
|
||||||
msgstr "Titre de l'image"
|
|
||||||
|
|
||||||
msgid "Image Type"
|
|
||||||
msgstr "Type d'image"
|
|
||||||
|
|
||||||
msgid "Image successfully marked"
|
|
||||||
msgstr "Image marquée avec succès"
|
|
||||||
|
|
||||||
msgid "Images"
|
|
||||||
msgstr "Images"
|
|
||||||
|
|
||||||
msgid "Import Bundle"
|
|
||||||
msgstr "Importer un Bundle"
|
|
||||||
|
|
||||||
msgid "Import Package"
|
|
||||||
msgstr "Importer un paquet"
|
|
||||||
|
|
||||||
msgid "Importing package {0} failed. Reason: {1}"
|
|
||||||
msgstr "L'import du paquet {0} a échoué. Raison : {1}"
|
|
||||||
|
|
||||||
msgid "Info"
|
|
||||||
msgstr "Info"
|
|
||||||
|
|
||||||
msgid "Instance name"
|
|
||||||
msgstr "Nom de l'instance"
|
|
||||||
|
|
||||||
msgid "Invalid metadata for image: {0}"
|
|
||||||
msgstr "Métadonnées non valides pour l'image : {0}"
|
|
||||||
|
|
||||||
msgid "Invalid murano image metadata"
|
|
||||||
msgstr "Métadonnées d'image Murano non valides"
|
|
||||||
|
|
||||||
msgid "It is forbidden to upload files larger than {0} MB."
|
|
||||||
msgstr "Il est interdit d'envoyer un fichier plus grand que {0} Mo."
|
|
||||||
|
|
||||||
msgid "KeyWord"
|
|
||||||
msgstr "Mot clé"
|
|
||||||
|
|
||||||
msgid "Last operation"
|
|
||||||
msgstr "Dernière opération"
|
|
||||||
|
|
||||||
msgid "Latest Deployment Log"
|
|
||||||
msgstr "Dernier journal de déploiement"
|
|
||||||
|
|
||||||
msgid "License"
|
|
||||||
msgstr "Licence"
|
|
||||||
|
|
||||||
msgid "Logs"
|
|
||||||
msgstr "Journaux"
|
|
||||||
|
|
||||||
msgid "Manage"
|
|
||||||
msgstr "Gérer"
|
|
||||||
|
|
||||||
msgid "Manage Components"
|
|
||||||
msgstr "Gérer les composants"
|
|
||||||
|
|
||||||
msgctxt "Package requirements"
|
|
||||||
msgid "Manifest file"
|
|
||||||
msgstr "Fichier manifeste"
|
|
||||||
|
|
||||||
msgid "Mark Image"
|
|
||||||
msgstr "Marquer l'image"
|
|
||||||
|
|
||||||
msgid "Marked Images"
|
|
||||||
msgstr "Images marquées"
|
|
||||||
|
|
||||||
msgid "Modify Package"
|
|
||||||
msgstr "Modifier le paquet"
|
|
||||||
|
|
||||||
msgid "Modifying package failed"
|
|
||||||
msgstr "La modification du paquet a échoué."
|
|
||||||
|
|
||||||
msgid "NO ENVIRONMENTS"
|
|
||||||
msgstr "PAS D'ENVIRONNEMENT"
|
|
||||||
|
|
||||||
msgid "Name"
|
|
||||||
msgstr "Nom"
|
|
||||||
|
|
||||||
#, python-format
|
|
||||||
msgid "Network of '%s'"
|
|
||||||
msgstr "Réseau de '%s'"
|
|
||||||
|
|
||||||
msgid "Next"
|
|
||||||
msgstr "Suivant"
|
|
||||||
|
|
||||||
msgid "Next Page"
|
|
||||||
msgstr "Page suivante"
|
|
||||||
|
|
||||||
msgid "No availability zones available"
|
|
||||||
msgstr "Pas de zones de disponibilité disponible"
|
|
||||||
|
|
||||||
msgid "No categories available"
|
|
||||||
msgstr "Pas de catégorie disponible"
|
|
||||||
|
|
||||||
msgid "No components"
|
|
||||||
msgstr "Aucun composant"
|
|
||||||
|
|
||||||
msgid "No images available"
|
|
||||||
msgstr "Aucune image disponible"
|
|
||||||
|
|
||||||
msgid "No keypair"
|
|
||||||
msgstr "Pas de paire de clés"
|
|
||||||
|
|
||||||
msgid "No license"
|
|
||||||
msgstr "Aucune de licence"
|
|
||||||
|
|
||||||
msgid "No recent activity to report at this time."
|
|
||||||
msgstr "Aucune activité récente à reporter pour le moment"
|
|
||||||
|
|
||||||
msgid "No requirements"
|
|
||||||
msgstr "Non requis"
|
|
||||||
|
|
||||||
msgid "None"
|
|
||||||
msgstr "Aucun"
|
|
||||||
|
|
||||||
msgid "Not in domain"
|
|
||||||
msgstr "Pas dans le domaine"
|
|
||||||
|
|
||||||
msgid "Note"
|
|
||||||
msgstr "Note"
|
|
||||||
|
|
||||||
msgid ""
|
|
||||||
"OpenStack Networking (Neutron) is not available in current environment. "
|
|
||||||
"Custom Network Settings cannot be applied"
|
|
||||||
msgstr ""
|
|
||||||
"Le Réseau OpenStack (Neutron) n'est pas disponible dans l'environnement "
|
|
||||||
"courant. Les paramètres réseau personnalisés ne peuvent pas être appliqués"
|
|
||||||
|
|
||||||
msgid "Operation is forbidden by murano-api server."
|
|
||||||
msgstr "Opération interdite par le serveur murano-api."
|
|
||||||
|
|
||||||
msgid "Optional"
|
|
||||||
msgstr "Optionnel"
|
|
||||||
|
|
||||||
msgid "Overview"
|
|
||||||
msgstr "Vue d'ensemble"
|
|
||||||
|
|
||||||
msgid "Package Count"
|
|
||||||
msgstr "Nom du package"
|
|
||||||
|
|
||||||
msgid "Package Name"
|
|
||||||
msgstr "Nom du paquet"
|
|
||||||
|
|
||||||
msgid "Package Source"
|
|
||||||
msgstr "Source du paquet"
|
|
||||||
|
|
||||||
msgid "Package Tags"
|
|
||||||
msgstr "Etiquettes du paquet"
|
|
||||||
|
|
||||||
msgid "Package URL"
|
|
||||||
msgstr "URL du paquet"
|
|
||||||
|
|
||||||
msgid "Package Version"
|
|
||||||
msgstr "Version du paquet"
|
|
||||||
|
|
||||||
msgid "Package creation failed.Reason: {0}"
|
|
||||||
msgstr "La création du paquet a échoué. Raison : {0}"
|
|
||||||
|
|
||||||
msgid "Package foo uploaded"
|
|
||||||
msgstr "Paquet foo envoyé"
|
|
||||||
|
|
||||||
msgid "Package modified."
|
|
||||||
msgstr "Package modifié."
|
|
||||||
|
|
||||||
msgid "Package parameters successfully updated."
|
|
||||||
msgstr "Les paramètres du paquet a été mis à jour avec succès"
|
|
||||||
|
|
||||||
msgid "Package version"
|
|
||||||
msgstr "Version du paquet"
|
|
||||||
|
|
||||||
msgid "Package with id {0} is not found"
|
|
||||||
msgstr "Le paquet avec l'id {0} n'a pas été trouvé"
|
|
||||||
|
|
||||||
msgid "Package {0} already registered."
|
|
||||||
msgstr "Paquet{0} déjà enregistré."
|
|
||||||
|
|
||||||
msgid "Package {0} upload failed. {1}"
|
|
||||||
msgstr "L'envoi du paquet {0} a échoué. {1}"
|
|
||||||
|
|
||||||
msgid "Package {0} uploaded"
|
|
||||||
msgstr "Paquet {0} envoyé"
|
|
||||||
|
|
||||||
msgid "Packages"
|
|
||||||
msgstr "Paquets"
|
|
||||||
|
|
||||||
msgid "Packages should contain:"
|
|
||||||
msgstr "Les paquets doivent contenir :"
|
|
||||||
|
|
||||||
msgid "Please confirm your password"
|
|
||||||
msgstr "Merci de confirmer votre mot de passe"
|
|
||||||
|
|
||||||
msgid "Previous Page"
|
|
||||||
msgstr "Page précédente"
|
|
||||||
|
|
||||||
msgid "Public"
|
|
||||||
msgstr "Publique"
|
|
||||||
|
|
||||||
msgid "Quick Deploy"
|
|
||||||
msgstr "Déploiement rapide"
|
|
||||||
|
|
||||||
msgid "Ready"
|
|
||||||
msgstr "Prêt"
|
|
||||||
|
|
||||||
msgid "Ready to configure"
|
|
||||||
msgstr "Prêt à configurer"
|
|
||||||
|
|
||||||
msgid "Ready to deploy"
|
|
||||||
msgstr "Prêt à déployer"
|
|
||||||
|
|
||||||
msgid "Recent Activity"
|
|
||||||
msgstr "Activité récente"
|
|
||||||
|
|
||||||
msgid "Repository"
|
|
||||||
msgstr "Dépot"
|
|
||||||
|
|
||||||
msgid "Requested object is not found on murano server."
|
|
||||||
msgstr "L'objet demandé n'a pas été trouvé sur le serveur Murano."
|
|
||||||
|
|
||||||
msgid "Requested operation conflicts with an existing object."
|
|
||||||
msgstr "L'opération demandée rentre en conflit avec un objet existant."
|
|
||||||
|
|
||||||
msgid "Requirements"
|
|
||||||
msgstr "Requirements"
|
|
||||||
|
|
||||||
msgid "Retype your password"
|
|
||||||
msgstr "Entrez à nouveau votre mot de passe"
|
|
||||||
|
|
||||||
msgid "Running"
|
|
||||||
msgstr "En fonctionnement"
|
|
||||||
|
|
||||||
msgid "Running with errors"
|
|
||||||
msgstr "Exécution avec erreurs"
|
|
||||||
|
|
||||||
msgid "Running with warnings"
|
|
||||||
msgstr "Exécution avec avertissements"
|
|
||||||
|
|
||||||
msgid "Select Application"
|
|
||||||
msgstr "Sélectionner l'application"
|
|
||||||
|
|
||||||
msgid "Select Image"
|
|
||||||
msgstr "Sélectionner une image "
|
|
||||||
|
|
||||||
msgid "Select an image registered in Glance Image Services."
|
|
||||||
msgstr "Sélectionner une image enregistrée dans les services d'image Glance."
|
|
||||||
|
|
||||||
msgid "Select one or more categories for a package."
|
|
||||||
msgstr "Sélectionner une ou plusieurs catégories pour le paquet"
|
|
||||||
|
|
||||||
msgid "Show Details"
|
|
||||||
msgstr "Afficher les détails"
|
|
||||||
|
|
||||||
msgid "Something went wrong during package downloading"
|
|
||||||
msgstr "Quelque chose s'est mal passé pendant le téléchargement du paquet"
|
|
||||||
|
|
||||||
msgid ""
|
|
||||||
"Sorry, you can't add application right now. The environment is deploying."
|
|
||||||
msgstr ""
|
|
||||||
"Désolé, vous ne pouvez ajouter d'application maintenant. L'environnement est "
|
|
||||||
"en cours de déploiement."
|
|
||||||
|
|
||||||
msgid "Sorry, you can't delete service right now"
|
|
||||||
msgstr "Désolé, vous ne pouvez pas supprimer le service maintenant"
|
|
||||||
|
|
||||||
msgid "Specified title already in use. Please choose another one."
|
|
||||||
msgstr "Le titre spécifié est déjà utilisé. Veuillez en choisir un autre."
|
|
||||||
|
|
||||||
msgid "Started Deleting Component"
|
|
||||||
msgid_plural "Started Deleting Components"
|
|
||||||
msgstr[0] "La suppression du composant a débutée"
|
|
||||||
msgstr[1] "La suppression des composants a débutée"
|
|
||||||
|
|
||||||
msgid "Started Deleting Environment"
|
|
||||||
msgid_plural "Started Deleting Environments"
|
|
||||||
msgstr[0] "La suppression de l'environnement a débutée"
|
|
||||||
msgstr[1] "La suppression des environnements a débutée"
|
|
||||||
|
|
||||||
msgid "Started deploying Environment"
|
|
||||||
msgid_plural "Started deploying Environments"
|
|
||||||
msgstr[0] "Le déploiement de l'environnement a débutée"
|
|
||||||
msgstr[1] "Le déploiement des environnements a débutée"
|
|
||||||
|
|
||||||
msgid "Status"
|
|
||||||
msgstr "Statut"
|
|
||||||
|
|
||||||
msgid "Step {0}"
|
|
||||||
msgstr "Étape {0}"
|
|
||||||
|
|
||||||
msgid "Successful"
|
|
||||||
msgstr "Succès"
|
|
||||||
|
|
||||||
msgid "Tags"
|
|
||||||
msgstr "Etiquettes"
|
|
||||||
|
|
||||||
msgid "Tenant Name"
|
|
||||||
msgstr "Nom du titulaire"
|
|
||||||
|
|
||||||
msgid "The '{0}' application successfully added to environment."
|
|
||||||
msgstr "Succès de l'ajout de l'application '{0}' à l'environnement."
|
|
||||||
|
|
||||||
msgid "The environment name field cannot be empty."
|
|
||||||
msgstr "Le champ du nom de l'environnement ne peut pas être vide."
|
|
||||||
|
|
||||||
msgid ""
|
|
||||||
"The password must contain at least one letter, "
|
|
||||||
"one number and one special character"
|
|
||||||
msgstr ""
|
|
||||||
"Le mot de passe doit contenir au moins une lettre, "
|
|
||||||
"un nombre et un caractère spécial"
|
|
||||||
|
|
||||||
msgid "The request data is not acceptable by the server"
|
|
||||||
msgstr "Les données de la requête ne sont pas acceptées par le serveur"
|
|
||||||
|
|
||||||
msgid "There are no applications in the catalog. You can import apps from"
|
|
||||||
msgstr ""
|
|
||||||
"Il n'y a aucun application dans le catalogue. Vous pouvez importer votre app "
|
|
||||||
"depuis"
|
|
||||||
|
|
||||||
msgid "There are no applications matching your criteria."
|
|
||||||
msgstr "Il n'y a aucune application que correspond avec vos critères"
|
|
||||||
|
|
||||||
msgid "There was an error communicating with server"
|
|
||||||
msgstr "Erreur lors de la communication avec le serveur."
|
|
||||||
|
|
||||||
msgid "There was an error initialising this field."
|
|
||||||
msgstr "Une erreur s'est produite à l'initialisation de ce champ."
|
|
||||||
|
|
||||||
msgid "Time Finished"
|
|
||||||
msgstr "Terminé"
|
|
||||||
|
|
||||||
msgid "Time Started"
|
|
||||||
msgstr "Démarré"
|
|
||||||
|
|
||||||
msgid "Time updated"
|
|
||||||
msgstr "Heure mise à jour"
|
|
||||||
|
|
||||||
msgid "Title"
|
|
||||||
msgstr "Titre"
|
|
||||||
|
|
||||||
msgid "Toggle Active"
|
|
||||||
msgid_plural "Toggle Active"
|
|
||||||
msgstr[0] "Activer"
|
|
||||||
msgstr[1] "Activer"
|
|
||||||
|
|
||||||
msgid "Toggle Public"
|
|
||||||
msgid_plural "Toggle Public"
|
|
||||||
msgstr[0] "Publier"
|
|
||||||
msgstr[1] "Publier"
|
|
||||||
|
|
||||||
msgid "Toggled Active"
|
|
||||||
msgid_plural "Toggled Active"
|
|
||||||
msgstr[0] "Activer"
|
|
||||||
msgstr[1] "Activer"
|
|
||||||
|
|
||||||
msgid "Toggled Public"
|
|
||||||
msgid_plural "Toggled Public"
|
|
||||||
msgstr[0] "Publier"
|
|
||||||
msgstr[1] "Publier"
|
|
||||||
|
|
||||||
msgid "Topology"
|
|
||||||
msgstr "Topologie"
|
|
||||||
|
|
||||||
msgid "Type"
|
|
||||||
msgstr "Type"
|
|
||||||
|
|
||||||
msgctxt "Package requirements"
|
|
||||||
msgid "UI definition folder"
|
|
||||||
msgstr "Dossier de définition de l'UI"
|
|
||||||
|
|
||||||
msgid "UNKNOWN"
|
|
||||||
msgstr "INCONNU"
|
|
||||||
|
|
||||||
msgid "URL"
|
|
||||||
msgstr "URL"
|
|
||||||
|
|
||||||
msgid "Unable to abandon an environment {0} due to: {1}"
|
|
||||||
msgstr "Impossible d'abandonner un environnement {0} à cause de : {1}"
|
|
||||||
|
|
||||||
msgid "Unable to communicate to glare-api server."
|
|
||||||
msgstr "Impossible de communiquer avec le serveur glare-api. "
|
|
||||||
|
|
||||||
msgid "Unable to communicate to murano-api server."
|
|
||||||
msgstr "Impossible de communiquer avec le serveur murano-api. "
|
|
||||||
|
|
||||||
msgid "Unable to create environment {0} due to: {1}"
|
|
||||||
msgstr "Impossible de créer l'environnement {0} à cause de : {1}"
|
|
||||||
|
|
||||||
msgid "Unable to delete category"
|
|
||||||
msgstr "Impossible de supprimer la catégorie"
|
|
||||||
|
|
||||||
msgid "Unable to delete environment {0} due to: {1}"
|
|
||||||
msgstr "Impossible de supprimer l'environnement {0} à cause de : {1}"
|
|
||||||
|
|
||||||
msgid "Unable to delete package in murano-api server"
|
|
||||||
msgstr "Impossible de supprimer le paquet dans le serveur murano-api"
|
|
||||||
|
|
||||||
msgid "Unable to deploy. Try again later"
|
|
||||||
msgstr "Impossible de déployer. Veuillez réessayer plus tard."
|
|
||||||
|
|
||||||
msgid "Unable to download package."
|
|
||||||
msgstr "Impossible de télécharger le paquet."
|
|
||||||
|
|
||||||
msgid "Unable to get list of categories"
|
|
||||||
msgstr "Impossible d'obtenir la liste des catégories"
|
|
||||||
|
|
||||||
msgid "Unable to mark image"
|
|
||||||
msgstr "Impossible de marquer l'image"
|
|
||||||
|
|
||||||
msgid "Unable to modify package"
|
|
||||||
msgstr "Impossible de modifier le paquet"
|
|
||||||
|
|
||||||
msgid "Unable to remove metadata"
|
|
||||||
msgstr "Impossible de supprimer les métadonnées."
|
|
||||||
|
|
||||||
msgid "Unable to remove package."
|
|
||||||
msgstr "Impossible de supprimer le paquet."
|
|
||||||
|
|
||||||
msgid "Unable to retrieve availability zones."
|
|
||||||
msgstr "Impossible de récupérer les zones de disponibilité."
|
|
||||||
|
|
||||||
msgid "Unable to retrieve details for service"
|
|
||||||
msgstr "Impossible de récupérer les détails du service"
|
|
||||||
|
|
||||||
msgid "Unable to retrieve list of deployments"
|
|
||||||
msgstr "Impossible de récupérer la liste des déploiements."
|
|
||||||
|
|
||||||
msgid "Unable to retrieve list of images"
|
|
||||||
msgstr "Impossible de récupérer la liste des images."
|
|
||||||
|
|
||||||
msgid "Unable to retrieve package details."
|
|
||||||
msgstr "Impossible de récupérer les détails du paquet."
|
|
||||||
|
|
||||||
msgid "Unable to retrieve project list."
|
|
||||||
msgstr "Impossible de récupérer la liste des projets."
|
|
||||||
|
|
||||||
msgid "Unable to retrieve public images."
|
|
||||||
msgstr "Impossible de récupérer les images publiques."
|
|
||||||
|
|
||||||
msgid "Unavailable"
|
|
||||||
msgstr "Indisponible"
|
|
||||||
|
|
||||||
msgid "Unknown"
|
|
||||||
msgstr "Inconnu"
|
|
||||||
|
|
||||||
msgid "Update"
|
|
||||||
msgstr "Mettre à jour"
|
|
||||||
|
|
||||||
msgid "Update Environment"
|
|
||||||
msgid_plural "Deploy Environments"
|
|
||||||
msgstr[0] "Mettre à jour l'environnement"
|
|
||||||
msgstr[1] "Mettre à jour les environnements"
|
|
||||||
|
|
||||||
msgid "Update Image"
|
|
||||||
msgstr "Mettre à jour une image"
|
|
||||||
|
|
||||||
msgid "Update Metadata"
|
|
||||||
msgstr "Mettre à jour les métadonnées"
|
|
||||||
|
|
||||||
msgid "Update This Environment"
|
|
||||||
msgstr "Mettre à jour cet environnement"
|
|
||||||
|
|
||||||
msgid "Updated"
|
|
||||||
msgstr "Mis à jour"
|
|
||||||
|
|
||||||
msgid "Updated Environment"
|
|
||||||
msgid_plural "Deployed Environments"
|
|
||||||
msgstr[0] "Environnement mis à jour"
|
|
||||||
msgstr[1] "Environnements mis à jour"
|
|
||||||
|
|
||||||
msgid "Uploading package failed. {0}"
|
|
||||||
msgstr "L'envoi du paquet a échoué. {0}"
|
|
||||||
|
|
||||||
msgid "Validation Error occurred"
|
|
||||||
msgstr "Une erreur de validation s'est produite"
|
|
||||||
|
|
||||||
msgid "Version"
|
|
||||||
msgstr "Version"
|
|
||||||
|
|
||||||
msgid "You are not allowed to delete this package"
|
|
||||||
msgstr "Vous n'êtes pas autorisé à supprimer ce paquet"
|
|
||||||
|
|
||||||
msgid "You are not allowed to perform this operation"
|
|
||||||
msgstr "Vous n'êtes pas autorisé à executer cette opération"
|
|
||||||
|
|
||||||
msgid "{0}{1} don't match"
|
|
||||||
msgstr "{0}{1} ne sont pas identique"
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,84 +0,0 @@
|
||||||
# suhartono <cloudsuhartono@gmail.com>, 2016. #zanata
|
|
||||||
msgid ""
|
|
||||||
msgstr ""
|
|
||||||
"Project-Id-Version: murano-dashboard 3.0.0.0rc2.dev100\n"
|
|
||||||
"Report-Msgid-Bugs-To: https://bugs.launchpad.net/openstack-i18n/\n"
|
|
||||||
"POT-Creation-Date: 2016-11-07 23:03+0000\n"
|
|
||||||
"MIME-Version: 1.0\n"
|
|
||||||
"Content-Type: text/plain; charset=UTF-8\n"
|
|
||||||
"Content-Transfer-Encoding: 8bit\n"
|
|
||||||
"PO-Revision-Date: 2016-11-08 06:49+0000\n"
|
|
||||||
"Last-Translator: suhartono <cloudsuhartono@gmail.com>\n"
|
|
||||||
"Language-Team: Indonesian\n"
|
|
||||||
"Language: id\n"
|
|
||||||
"X-Generator: Zanata 3.7.3\n"
|
|
||||||
"Plural-Forms: nplurals=1; plural=0\n"
|
|
||||||
|
|
||||||
msgid " 1 capital letter"
|
|
||||||
msgstr " 1 huruf kapital"
|
|
||||||
|
|
||||||
msgid " 1 digit"
|
|
||||||
msgstr " 1 digit"
|
|
||||||
|
|
||||||
msgid " 1 non-capital letter"
|
|
||||||
msgstr " 1 huruf non-kapital"
|
|
||||||
|
|
||||||
msgid " 1 special character"
|
|
||||||
msgstr " 1 karakter spesial"
|
|
||||||
|
|
||||||
msgid " 7 characters"
|
|
||||||
msgstr " 7 karakter"
|
|
||||||
|
|
||||||
msgid "An error occurred. Please try again later."
|
|
||||||
msgstr "Terjadi kesalahan. Silakan coba lagi nanti."
|
|
||||||
|
|
||||||
msgid "Cancel"
|
|
||||||
msgstr "Cancel (membatalkan)"
|
|
||||||
|
|
||||||
msgid "Create"
|
|
||||||
msgstr "Create (membuat)"
|
|
||||||
|
|
||||||
msgid "Loading"
|
|
||||||
msgstr "Loading (pemuatan)"
|
|
||||||
|
|
||||||
msgid "New"
|
|
||||||
msgstr "New (baru)"
|
|
||||||
|
|
||||||
msgid "Passwords do not match"
|
|
||||||
msgstr "Kata sandi tidak cocok"
|
|
||||||
|
|
||||||
msgid "Show less"
|
|
||||||
msgstr "Tampilkan kurang sedikit"
|
|
||||||
|
|
||||||
msgid "Show more"
|
|
||||||
msgstr "Tampilkan lebih banyak"
|
|
||||||
|
|
||||||
msgid "There was an error submitting the form. Please try again."
|
|
||||||
msgstr "Terjadi kesalahan mengirimkan formulir. Silakan coba lagi."
|
|
||||||
|
|
||||||
msgid "Unable to edit component metadata."
|
|
||||||
msgstr "Tidak dapat mengedit komponen metadata."
|
|
||||||
|
|
||||||
msgid "Unable to edit environment metadata."
|
|
||||||
msgstr "Tidak dapat mengedit metadata lingkungan."
|
|
||||||
|
|
||||||
msgid "Unable to retrieve component metadata."
|
|
||||||
msgstr "Tidak dapat mengambil komponen metadata."
|
|
||||||
|
|
||||||
msgid "Unable to retrieve environment metadata."
|
|
||||||
msgstr "Tidak dapat mengambil metadata lingkungan."
|
|
||||||
|
|
||||||
msgid "Unable to retrieve the packages."
|
|
||||||
msgstr "Tidak dapat mengambil paket."
|
|
||||||
|
|
||||||
msgid "Unable to run action."
|
|
||||||
msgstr "Tidak dapat menjalankan aksi."
|
|
||||||
|
|
||||||
msgid "Waiting for a result"
|
|
||||||
msgstr "Menunggu hasilnya"
|
|
||||||
|
|
||||||
msgid "Working"
|
|
||||||
msgstr "Working (kerja)"
|
|
||||||
|
|
||||||
msgid "Your password should have at least"
|
|
||||||
msgstr "Kata sandi Anda harus memiliki setidaknya"
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,86 +0,0 @@
|
||||||
# Andreas Jaeger <jaegerandi@gmail.com>, 2016. #zanata
|
|
||||||
# Yusuke Higashino <yusuke_higashino@adoc.co.jp>, 2016. #zanata
|
|
||||||
# Yuko Katabami <yukokatabami@gmail.com>, 2017. #zanata
|
|
||||||
msgid ""
|
|
||||||
msgstr ""
|
|
||||||
"Project-Id-Version: murano-dashboard 3.1.1.dev12\n"
|
|
||||||
"Report-Msgid-Bugs-To: https://bugs.launchpad.net/openstack-i18n/\n"
|
|
||||||
"POT-Creation-Date: 2017-02-09 17:38+0000\n"
|
|
||||||
"MIME-Version: 1.0\n"
|
|
||||||
"Content-Type: text/plain; charset=UTF-8\n"
|
|
||||||
"Content-Transfer-Encoding: 8bit\n"
|
|
||||||
"PO-Revision-Date: 2017-02-10 10:30+0000\n"
|
|
||||||
"Last-Translator: Yuko Katabami <yukokatabami@gmail.com>\n"
|
|
||||||
"Language-Team: Japanese\n"
|
|
||||||
"Language: ja\n"
|
|
||||||
"X-Generator: Zanata 3.7.3\n"
|
|
||||||
"Plural-Forms: nplurals=1; plural=0\n"
|
|
||||||
|
|
||||||
msgid " 1 capital letter"
|
|
||||||
msgstr "1つ以上の大文字"
|
|
||||||
|
|
||||||
msgid " 1 digit"
|
|
||||||
msgstr "1つ以上の数字"
|
|
||||||
|
|
||||||
msgid " 1 non-capital letter"
|
|
||||||
msgstr "1つ以上の小文字"
|
|
||||||
|
|
||||||
msgid " 1 special character"
|
|
||||||
msgstr "1つ以上の記号"
|
|
||||||
|
|
||||||
msgid " 7 characters"
|
|
||||||
msgstr "7文字以上のアルファベット"
|
|
||||||
|
|
||||||
msgid "An error occurred. Please try again later."
|
|
||||||
msgstr "エラーが発生しました。後からもう一度お試しください。"
|
|
||||||
|
|
||||||
msgid "Cancel"
|
|
||||||
msgstr "取り消し"
|
|
||||||
|
|
||||||
msgid "Create"
|
|
||||||
msgstr "作成"
|
|
||||||
|
|
||||||
msgid "Loading"
|
|
||||||
msgstr "読み込み中"
|
|
||||||
|
|
||||||
msgid "New"
|
|
||||||
msgstr "新規"
|
|
||||||
|
|
||||||
msgid "Passwords do not match"
|
|
||||||
msgstr "パスワードが一致しません。"
|
|
||||||
|
|
||||||
msgid "Show less"
|
|
||||||
msgstr "元に戻す"
|
|
||||||
|
|
||||||
msgid "Show more"
|
|
||||||
msgstr "さらに表示"
|
|
||||||
|
|
||||||
msgid "There was an error submitting the form. Please try again."
|
|
||||||
msgstr "フォームの送信中にエラーが発生しました。再度お試しください。"
|
|
||||||
|
|
||||||
msgid "Unable to edit component metadata."
|
|
||||||
msgstr "コンポーネントのメタデータを編集できません。"
|
|
||||||
|
|
||||||
msgid "Unable to edit environment metadata."
|
|
||||||
msgstr "環境のメタデータを編集できません。"
|
|
||||||
|
|
||||||
msgid "Unable to retrieve component metadata."
|
|
||||||
msgstr "コンポーネントのメタデータを取得できません。"
|
|
||||||
|
|
||||||
msgid "Unable to retrieve environment metadata."
|
|
||||||
msgstr "環境のメタデータを取得できません。"
|
|
||||||
|
|
||||||
msgid "Unable to retrieve the packages."
|
|
||||||
msgstr "パッケージ一覧を取得できません。"
|
|
||||||
|
|
||||||
msgid "Unable to run action."
|
|
||||||
msgstr "アクションを実行できません。"
|
|
||||||
|
|
||||||
msgid "Waiting for a result"
|
|
||||||
msgstr "実行結果の待機中"
|
|
||||||
|
|
||||||
msgid "Working"
|
|
||||||
msgstr "反映中"
|
|
||||||
|
|
||||||
msgid "Your password should have at least"
|
|
||||||
msgstr "あなたのパスワードには以下の内容が最低必要です。"
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,86 +0,0 @@
|
||||||
# Andreas Jaeger <jaegerandi@gmail.com>, 2016. #zanata
|
|
||||||
# Eunseop Shin <kairos9603@gmail.com>, 2016. #zanata
|
|
||||||
# Ian Y. Choi <ianyrchoi@gmail.com>, 2016. #zanata
|
|
||||||
msgid ""
|
|
||||||
msgstr ""
|
|
||||||
"Project-Id-Version: murano-dashboard 3.0.0.0rc2.dev57\n"
|
|
||||||
"Report-Msgid-Bugs-To: https://bugs.launchpad.net/openstack-i18n/\n"
|
|
||||||
"POT-Creation-Date: 2016-10-20 20:47+0000\n"
|
|
||||||
"MIME-Version: 1.0\n"
|
|
||||||
"Content-Type: text/plain; charset=UTF-8\n"
|
|
||||||
"Content-Transfer-Encoding: 8bit\n"
|
|
||||||
"PO-Revision-Date: 2016-10-20 11:24+0000\n"
|
|
||||||
"Last-Translator: Eunseop Shin <kairos9603@gmail.com>\n"
|
|
||||||
"Language-Team: Korean (South Korea)\n"
|
|
||||||
"Language: ko-KR\n"
|
|
||||||
"X-Generator: Zanata 3.7.3\n"
|
|
||||||
"Plural-Forms: nplurals=1; plural=0\n"
|
|
||||||
|
|
||||||
msgid " 1 capital letter"
|
|
||||||
msgstr "1 대문자"
|
|
||||||
|
|
||||||
msgid " 1 digit"
|
|
||||||
msgstr "1 숫자"
|
|
||||||
|
|
||||||
msgid " 1 non-capital letter"
|
|
||||||
msgstr "1 소문자"
|
|
||||||
|
|
||||||
msgid " 1 special character"
|
|
||||||
msgstr "1 특수문자"
|
|
||||||
|
|
||||||
msgid " 7 characters"
|
|
||||||
msgstr "7 문자"
|
|
||||||
|
|
||||||
msgid "An error occurred. Please try again later."
|
|
||||||
msgstr "오류가 발생했습니다. 나중에 다시 시도하십시오."
|
|
||||||
|
|
||||||
msgid "Cancel"
|
|
||||||
msgstr "취소"
|
|
||||||
|
|
||||||
msgid "Create"
|
|
||||||
msgstr "생성"
|
|
||||||
|
|
||||||
msgid "Loading"
|
|
||||||
msgstr "불러오는 중"
|
|
||||||
|
|
||||||
msgid "New"
|
|
||||||
msgstr "New"
|
|
||||||
|
|
||||||
msgid "Passwords do not match"
|
|
||||||
msgstr "비밀번호가 일치하지 않습니다"
|
|
||||||
|
|
||||||
msgid "Show less"
|
|
||||||
msgstr "덜 보기"
|
|
||||||
|
|
||||||
msgid "Show more"
|
|
||||||
msgstr "더 보기"
|
|
||||||
|
|
||||||
msgid "There was an error submitting the form. Please try again."
|
|
||||||
msgstr "양식을 제출하는 동안 오류가 발생하였습니다. 다시 시도하세요."
|
|
||||||
|
|
||||||
msgid "Unable to edit component metadata."
|
|
||||||
msgstr "컴포넌트 메타데이터를 수정할 수 없습니다."
|
|
||||||
|
|
||||||
msgid "Unable to edit environment metadata."
|
|
||||||
msgstr "환경 메타데이터를 수정 할 수 없습니다."
|
|
||||||
|
|
||||||
msgid "Unable to retrieve component metadata."
|
|
||||||
msgstr "컴포넌트 메타데이터를 찾을 수 없습니다."
|
|
||||||
|
|
||||||
msgid "Unable to retrieve environment metadata."
|
|
||||||
msgstr "환경 메타데이터를 찾을 수 없습니다."
|
|
||||||
|
|
||||||
msgid "Unable to retrieve the packages."
|
|
||||||
msgstr "패키지를 찾지 못 했습니다."
|
|
||||||
|
|
||||||
msgid "Unable to run action."
|
|
||||||
msgstr "실행 작업을 할 수 없습니다."
|
|
||||||
|
|
||||||
msgid "Waiting for a result"
|
|
||||||
msgstr "결과를 기다리는 동안"
|
|
||||||
|
|
||||||
msgid "Working"
|
|
||||||
msgstr "작업 중"
|
|
||||||
|
|
||||||
msgid "Your password should have at least"
|
|
||||||
msgstr "암호는 최소 다음과 같아야 합니다"
|
|
|
@ -1,974 +0,0 @@
|
||||||
# Andreas Jaeger <jaegerandi@gmail.com>, 2016. #zanata
|
|
||||||
msgid ""
|
|
||||||
msgstr ""
|
|
||||||
"Project-Id-Version: murano-dashboard 4.0.0.0b3.dev4\n"
|
|
||||||
"Report-Msgid-Bugs-To: https://bugs.launchpad.net/openstack-i18n/\n"
|
|
||||||
"POT-Creation-Date: 2017-06-10 02:57+0000\n"
|
|
||||||
"MIME-Version: 1.0\n"
|
|
||||||
"Content-Type: text/plain; charset=UTF-8\n"
|
|
||||||
"Content-Transfer-Encoding: 8bit\n"
|
|
||||||
"PO-Revision-Date: 2016-04-12 05:47+0000\n"
|
|
||||||
"Last-Translator: Copied by Zanata <copied-by-zanata@zanata.org>\n"
|
|
||||||
"Language-Team: Portuguese (Brazil)\n"
|
|
||||||
"Language: pt-BR\n"
|
|
||||||
"X-Generator: Zanata 3.9.6\n"
|
|
||||||
"Plural-Forms: nplurals=2; plural=(n != 1)\n"
|
|
||||||
|
|
||||||
msgid "-"
|
|
||||||
msgstr "- "
|
|
||||||
|
|
||||||
msgid "A local zip file to upload"
|
|
||||||
msgstr "Um arquivo zip local para enviar"
|
|
||||||
|
|
||||||
msgid "Abandon Environment"
|
|
||||||
msgid_plural "Abandon Environments"
|
|
||||||
msgstr[0] "Abandonar Ambiente"
|
|
||||||
msgstr[1] "Abandonar Ambientes"
|
|
||||||
|
|
||||||
msgid "Abandoned Environment"
|
|
||||||
msgid_plural "Abandoned Environments"
|
|
||||||
msgstr[0] "Ambiente Abandonado"
|
|
||||||
msgstr[1] "Ambientes Abandonados"
|
|
||||||
|
|
||||||
msgid "Active"
|
|
||||||
msgstr "Ativo"
|
|
||||||
|
|
||||||
msgid "Add"
|
|
||||||
msgstr "Adicionar"
|
|
||||||
|
|
||||||
msgid "Add Application"
|
|
||||||
msgstr "Adicionar Aplicação"
|
|
||||||
|
|
||||||
msgid "Add Application Category"
|
|
||||||
msgstr "Adicionar Categoria de Aplicação."
|
|
||||||
|
|
||||||
msgid "Add Category"
|
|
||||||
msgstr "Adicionar categoria"
|
|
||||||
|
|
||||||
msgid "Add Component"
|
|
||||||
msgstr "Adicionar componente"
|
|
||||||
|
|
||||||
msgid "Add Murano Metadata"
|
|
||||||
msgstr "Adicionar Metadados do Murano"
|
|
||||||
|
|
||||||
msgid "Add New"
|
|
||||||
msgstr "Adicionar Novo"
|
|
||||||
|
|
||||||
msgid "Add new category to the application catalog."
|
|
||||||
msgstr "Adicionar nova categoria ao catálogo de aplicações."
|
|
||||||
|
|
||||||
msgid "Add to Env"
|
|
||||||
msgstr "Adicionar ao ambiente"
|
|
||||||
|
|
||||||
msgid "Adding application to an environment failed."
|
|
||||||
msgstr "Falha ao adicionar a aplicação a um ambiente."
|
|
||||||
|
|
||||||
msgid "All"
|
|
||||||
msgstr "Todos"
|
|
||||||
|
|
||||||
msgid "Allows adding additional information about a package."
|
|
||||||
msgstr "Permite adicionar informações adicionais sobre o pacote."
|
|
||||||
|
|
||||||
msgid ""
|
|
||||||
"Allows to hide a package from the catalog. (Applies to package dependencies)"
|
|
||||||
msgstr ""
|
|
||||||
"Permite esconder um pacote do catálogo. (Se aplica as dependências do pacote)"
|
|
||||||
|
|
||||||
msgid ""
|
|
||||||
"An environment is a collection of applications that are meant to operate "
|
|
||||||
"under similar conditions."
|
|
||||||
msgstr ""
|
|
||||||
"Um ambiente é uma coleção de aplicações que devem operar sob condições "
|
|
||||||
"semelhantes."
|
|
||||||
|
|
||||||
msgid "An external http/https URL to load the bundle from."
|
|
||||||
msgstr "Uma URL http/https externa de onde o conjunto será carregado."
|
|
||||||
|
|
||||||
msgid "An external http/https URL to load the package from."
|
|
||||||
msgstr "Uma URL http/https externa de onde o pacote será carregado."
|
|
||||||
|
|
||||||
msgid "App Category:"
|
|
||||||
msgstr "Categoria de Aplicativos:"
|
|
||||||
|
|
||||||
msgid "App category"
|
|
||||||
msgstr "Categoria de aplicação"
|
|
||||||
|
|
||||||
msgid "Application Categories"
|
|
||||||
msgstr "Categoria de Aplicações"
|
|
||||||
|
|
||||||
msgid "Application Category"
|
|
||||||
msgstr "Categoria da Aplicação"
|
|
||||||
|
|
||||||
msgid "Application Details"
|
|
||||||
msgstr "Detalhes da aplicação"
|
|
||||||
|
|
||||||
msgid "Application Package"
|
|
||||||
msgstr "Pacote da aplicação"
|
|
||||||
|
|
||||||
msgid "Application Components"
|
|
||||||
msgstr "Aplicação Componentes"
|
|
||||||
|
|
||||||
msgid "Applications"
|
|
||||||
msgstr "Aplicações"
|
|
||||||
|
|
||||||
msgid "Author"
|
|
||||||
msgstr "Autor"
|
|
||||||
|
|
||||||
msgid "Auto"
|
|
||||||
msgstr "Auto"
|
|
||||||
|
|
||||||
msgid "Back"
|
|
||||||
msgstr "Voltar"
|
|
||||||
|
|
||||||
msgid "Bundle Name"
|
|
||||||
msgstr "Nome do Conjunto"
|
|
||||||
|
|
||||||
msgid "Bundle URL"
|
|
||||||
msgstr "URL do conjunto"
|
|
||||||
|
|
||||||
msgid "Bundle creation failed.Reason: Can't find Bundle name from repository."
|
|
||||||
msgstr ""
|
|
||||||
"A criação do conjunto falhou. Motivo: Não foi possível encontrar o nome do "
|
|
||||||
"repositório."
|
|
||||||
|
|
||||||
msgid "Bundle creation failed.Reason: {0}"
|
|
||||||
msgstr "A criação do conjunto falhou. Motivo: {0}"
|
|
||||||
|
|
||||||
msgid "Bundle successfully imported."
|
|
||||||
msgstr "Conjunto importado com sucesso."
|
|
||||||
|
|
||||||
msgid "Bundle's full name."
|
|
||||||
msgstr "Nome completo do conjunto."
|
|
||||||
|
|
||||||
msgid "Cancel"
|
|
||||||
msgstr "Cancelar"
|
|
||||||
|
|
||||||
msgid "Categories"
|
|
||||||
msgstr "Categorias"
|
|
||||||
|
|
||||||
msgid "Category Name"
|
|
||||||
msgstr "Nome da Categoria"
|
|
||||||
|
|
||||||
msgid "Category {0} created."
|
|
||||||
msgstr "Categoria {0} criada."
|
|
||||||
|
|
||||||
msgid "Check Keystone configuration of murano-api server."
|
|
||||||
msgstr "Verifique a configuração do Keystone no murano-api server."
|
|
||||||
|
|
||||||
msgid "Choose a Zip archive to upload into the catalog."
|
|
||||||
msgstr "Escolha um arquivo Zip para enviar ao catálogo."
|
|
||||||
|
|
||||||
msgid "Choose a name for the environment"
|
|
||||||
msgstr "Escolha um nome para o ambiente"
|
|
||||||
|
|
||||||
msgctxt "Package requirements"
|
|
||||||
msgid "Classes definition folder"
|
|
||||||
msgstr "Pasta de definições de classes"
|
|
||||||
|
|
||||||
msgid "Click to create new environment"
|
|
||||||
msgstr "Clique para criar um novo ambiente"
|
|
||||||
|
|
||||||
msgid "Component"
|
|
||||||
msgstr "Componente"
|
|
||||||
|
|
||||||
msgid "Component Details"
|
|
||||||
msgstr "Detalhes do componente"
|
|
||||||
|
|
||||||
msgid "Component List"
|
|
||||||
msgstr "Lista de componentes"
|
|
||||||
|
|
||||||
msgid "Component Logs"
|
|
||||||
msgstr "Registros do componente"
|
|
||||||
|
|
||||||
msgid "Components"
|
|
||||||
msgstr "Componentes"
|
|
||||||
|
|
||||||
msgid "Configuration"
|
|
||||||
msgstr "Configuração"
|
|
||||||
|
|
||||||
msgid "Configure Application"
|
|
||||||
msgstr "Configurar Aplicação"
|
|
||||||
|
|
||||||
msgid "Confirm password"
|
|
||||||
msgstr "Confirmar Senha"
|
|
||||||
|
|
||||||
msgid "Could not retrieve latest status for the {0} environment"
|
|
||||||
msgstr "Não foi possível obter o último estado para o ambiente {0}"
|
|
||||||
|
|
||||||
msgid "Couldn't update package {0} parameters. Error: {1}"
|
|
||||||
msgstr "Não foi possível atualizar os parâmetros do pacote {0}. Erro: {1}"
|
|
||||||
|
|
||||||
msgid "Create"
|
|
||||||
msgstr "Criar"
|
|
||||||
|
|
||||||
msgid "Create Env"
|
|
||||||
msgstr "Criar ambiente"
|
|
||||||
|
|
||||||
msgid "Create Environment"
|
|
||||||
msgstr "Criar ambiente"
|
|
||||||
|
|
||||||
msgid "Create New"
|
|
||||||
msgstr "Criar um novo"
|
|
||||||
|
|
||||||
msgid "Create a title for an image."
|
|
||||||
msgstr "Criar um título para a imagem"
|
|
||||||
|
|
||||||
msgid "Created"
|
|
||||||
msgstr "Criado"
|
|
||||||
|
|
||||||
msgid ""
|
|
||||||
"Defines whether or not a package can be used by other tenants. (Applies to "
|
|
||||||
"package dependencies)"
|
|
||||||
msgstr ""
|
|
||||||
"Define se um pacote pode ser usado por outros locatários. (Se aplica as "
|
|
||||||
"dependências do pacote)"
|
|
||||||
|
|
||||||
msgid "Delete Category"
|
|
||||||
msgid_plural "Delete Categories"
|
|
||||||
msgstr[0] "Deletar Categoria"
|
|
||||||
msgstr[1] "Deletar Categorias"
|
|
||||||
|
|
||||||
msgid "Delete Component"
|
|
||||||
msgid_plural "Delete Components"
|
|
||||||
msgstr[0] "Remover Componente"
|
|
||||||
msgstr[1] "Remover Componentes"
|
|
||||||
|
|
||||||
msgid "Delete Environment"
|
|
||||||
msgid_plural "Delete Environments"
|
|
||||||
msgstr[0] "Remover Ambiente"
|
|
||||||
msgstr[1] "Remover Ambientes"
|
|
||||||
|
|
||||||
msgid "Delete Metadata"
|
|
||||||
msgid_plural "Delete Metadata"
|
|
||||||
msgstr[0] "Remover Metadado"
|
|
||||||
msgstr[1] "Remover Metadado"
|
|
||||||
|
|
||||||
msgid "Delete Package"
|
|
||||||
msgid_plural "Delete Packages"
|
|
||||||
msgstr[0] "Remover Pacote"
|
|
||||||
msgstr[1] "Remover Pacotes"
|
|
||||||
|
|
||||||
msgid "Deleted Category"
|
|
||||||
msgid_plural "Deleted Categories"
|
|
||||||
msgstr[0] "Categoria Deletada"
|
|
||||||
msgstr[1] "Categoria Deletadas"
|
|
||||||
|
|
||||||
msgid "Deleted Metadata"
|
|
||||||
msgid_plural "Deleted Metadata"
|
|
||||||
msgstr[0] "Metadado Removido"
|
|
||||||
msgstr[1] "Metadado Removido"
|
|
||||||
|
|
||||||
msgid "Deleted Package"
|
|
||||||
msgid_plural "Deleted Packages"
|
|
||||||
msgstr[0] "Pacote Removido"
|
|
||||||
msgstr[1] "Pacotes Removidos"
|
|
||||||
|
|
||||||
msgid "Deploy Environment"
|
|
||||||
msgid_plural "Deploy Environments"
|
|
||||||
msgstr[0] "Implantar Ambiente"
|
|
||||||
msgstr[1] "Implantar Ambientes"
|
|
||||||
|
|
||||||
msgid "Deploy This Environment"
|
|
||||||
msgstr "Implantar Este Ambiente"
|
|
||||||
|
|
||||||
msgid "Deploy started"
|
|
||||||
msgstr "Implatação iniciada"
|
|
||||||
|
|
||||||
msgid "Deployed Components"
|
|
||||||
msgstr "Componentes Implantados"
|
|
||||||
|
|
||||||
msgid "Deployment Details"
|
|
||||||
msgstr "Detalhes da Implantação"
|
|
||||||
|
|
||||||
msgid "Deployment History"
|
|
||||||
msgstr "Histórico da Implantação"
|
|
||||||
|
|
||||||
msgid "Deployment Logs"
|
|
||||||
msgstr "Registros de Implantação"
|
|
||||||
|
|
||||||
#, python-format
|
|
||||||
msgid "Deployment with id %s doesn't exist anymore"
|
|
||||||
msgstr "A implantação com id %s não existe mais"
|
|
||||||
|
|
||||||
msgid "Deployments"
|
|
||||||
msgstr "Implantações"
|
|
||||||
|
|
||||||
msgid "Description"
|
|
||||||
msgstr "Descrição"
|
|
||||||
|
|
||||||
msgid "Details"
|
|
||||||
msgstr "Detalhes"
|
|
||||||
|
|
||||||
msgid "Download Package"
|
|
||||||
msgstr "Baixar Pacote"
|
|
||||||
|
|
||||||
msgid "Drop Components here"
|
|
||||||
msgstr "Coloque os componentes aqui"
|
|
||||||
|
|
||||||
msgid "Enabled"
|
|
||||||
msgstr "Habilitado"
|
|
||||||
|
|
||||||
msgid ""
|
|
||||||
"Enter a complex password with at least one letter, one number and one "
|
|
||||||
"special character"
|
|
||||||
msgstr ""
|
|
||||||
"Insira uma senha complexa, com pelo menos uma letra, um número e um carácter "
|
|
||||||
"especial"
|
|
||||||
|
|
||||||
msgid "Enter a password"
|
|
||||||
msgstr "Insira uma senha"
|
|
||||||
|
|
||||||
msgid "Environment"
|
|
||||||
msgstr "Ambiente"
|
|
||||||
|
|
||||||
msgid "Environment Default Network"
|
|
||||||
msgstr "Rede padrão do ambiente"
|
|
||||||
|
|
||||||
msgid "Environment Name"
|
|
||||||
msgstr "Nome do Ambiente"
|
|
||||||
|
|
||||||
msgid "Environment name must contain at least one non-white space symbol."
|
|
||||||
msgstr ""
|
|
||||||
"O nome do ambiente deve conter pelo menos um caractere que não seja um "
|
|
||||||
"espaço em branco."
|
|
||||||
|
|
||||||
#, python-format
|
|
||||||
msgid "Environment with id %s doesn't exist anymore"
|
|
||||||
msgstr "Ambiente com id %s não existe mais"
|
|
||||||
|
|
||||||
msgid "Environment with specified name already exists"
|
|
||||||
msgstr "Um ambiente com o nome especificado já existe"
|
|
||||||
|
|
||||||
msgid "Environments"
|
|
||||||
msgstr "Ambientes"
|
|
||||||
|
|
||||||
msgid "Error {0} occurred while installing images for {1}"
|
|
||||||
msgstr "O erro {0} ocorreu durante a instalação das imagens para {1}"
|
|
||||||
|
|
||||||
msgid "Error {0} occurred while installing package {1}"
|
|
||||||
msgstr "O erro {0} ocorreu durante a instalação do pacote {1}"
|
|
||||||
|
|
||||||
msgid "Error {0} occurred while parsing package {1}"
|
|
||||||
msgstr "O erro {0} ocorreu durante a análise do pacote {1}"
|
|
||||||
|
|
||||||
msgid "Error {0} occurred while setting image {1}, {2} public"
|
|
||||||
msgstr "O erro {0} ocorreu ao configurar a imagem {1}, {2} como pública"
|
|
||||||
|
|
||||||
msgctxt "Package requirements"
|
|
||||||
msgid "Execution plans folder"
|
|
||||||
msgstr "Pasta de planos de execução"
|
|
||||||
|
|
||||||
msgid "FQN"
|
|
||||||
msgstr "NTQ"
|
|
||||||
|
|
||||||
msgid "Failed to create environment"
|
|
||||||
msgstr "Falha ao criar ambiente"
|
|
||||||
|
|
||||||
msgid "Failed to modify the package. {0}"
|
|
||||||
msgstr "Falha ao modificar o pacote. {0}"
|
|
||||||
|
|
||||||
msgid "File"
|
|
||||||
msgstr "Arquivo"
|
|
||||||
|
|
||||||
msgid "Filter"
|
|
||||||
msgstr "Filtro"
|
|
||||||
|
|
||||||
msgid "Find in a selected category"
|
|
||||||
msgstr "Procure em uma categoria selecionada"
|
|
||||||
|
|
||||||
msgid ""
|
|
||||||
"First symbol should be latin letter or underscore. Subsequent symbols can be "
|
|
||||||
"latin letter, numeric, underscore, at sign, number sign or dollar sign"
|
|
||||||
msgstr ""
|
|
||||||
"O primeiro símbolo deve ser uma letra latina ou sublinhado. Os símbolos "
|
|
||||||
"subsequentes podem ser letras latinas, números, sublinhados, arroba, jogo da "
|
|
||||||
"velha ou cifrão"
|
|
||||||
|
|
||||||
msgid "Fully qualified package name."
|
|
||||||
msgstr "Nome totalmente qualificado do pacote."
|
|
||||||
|
|
||||||
msgid "HTTP/HTTPS URL of the bundle file."
|
|
||||||
msgstr "URL HTTP/HTTPS do arquivo do conjunto."
|
|
||||||
|
|
||||||
msgid "HTTP/HTTPS URL of the package file."
|
|
||||||
msgstr "URL HTTP/HTTPS do arquivo do pacote."
|
|
||||||
|
|
||||||
msgid "Heat Orchestration stack name"
|
|
||||||
msgstr "Nome do Stack de Orquestração do Heat"
|
|
||||||
|
|
||||||
msgid "Heat Orchestration stack%(forloop.counter)s name"
|
|
||||||
msgstr "Nome do stack%(forloop.counter)s de Orquestração do Heat"
|
|
||||||
|
|
||||||
msgid "ID"
|
|
||||||
msgstr "ID"
|
|
||||||
|
|
||||||
msgid ""
|
|
||||||
"If packages depend upon other packages and/or require specific glance "
|
|
||||||
"images, those are going to be installed with them from murano repository."
|
|
||||||
msgstr ""
|
|
||||||
"Se os pacotes dependem de outros pacotes e/ou precisam de imagens "
|
|
||||||
"específicas do glance, estas serão instaladas com eles do repositório do "
|
|
||||||
"Murano"
|
|
||||||
|
|
||||||
msgid ""
|
|
||||||
"If the package depends upon other packages and/or requires specific glance "
|
|
||||||
"images, those are going to be installed with it from murano repository."
|
|
||||||
msgstr ""
|
|
||||||
"Se o pacote depende de outros pacotes e/ou precisa de imagens específicas do "
|
|
||||||
"glance, estes serão instaladas com ele a partir do repositório do Murano"
|
|
||||||
|
|
||||||
msgid "Image"
|
|
||||||
msgstr "Imagem"
|
|
||||||
|
|
||||||
msgid "Image Title"
|
|
||||||
msgstr "Título da Imagem"
|
|
||||||
|
|
||||||
msgid "Image Type"
|
|
||||||
msgstr "Tipo de Imagem"
|
|
||||||
|
|
||||||
msgid "Image successfully marked"
|
|
||||||
msgstr "Imagem marcada com sucesso"
|
|
||||||
|
|
||||||
msgid "Images"
|
|
||||||
msgstr "Imagens"
|
|
||||||
|
|
||||||
msgid "Import Bundle"
|
|
||||||
msgstr "Importar Conjunto"
|
|
||||||
|
|
||||||
msgid "Import Package"
|
|
||||||
msgstr "Importar Pacote"
|
|
||||||
|
|
||||||
msgid "Importing package {0} failed. Reason: {1}"
|
|
||||||
msgstr "Falha ao importar o pacote {0}. Motivo: {1}"
|
|
||||||
|
|
||||||
msgid "Info"
|
|
||||||
msgstr "Info"
|
|
||||||
|
|
||||||
msgid "Instance name"
|
|
||||||
msgstr "Nome da instância"
|
|
||||||
|
|
||||||
msgid "Instance%(forloop.counter)s name"
|
|
||||||
msgstr "Nome da Instância%(forloop.counter)s"
|
|
||||||
|
|
||||||
msgid "Invalid metadata for image: {0}"
|
|
||||||
msgstr "Metadados inválidos para a imagem: {0}"
|
|
||||||
|
|
||||||
msgid "Invalid murano image metadata"
|
|
||||||
msgstr "Metadados da imagem do murano inválidos"
|
|
||||||
|
|
||||||
msgid "Invalid value of 'murano_nets' option"
|
|
||||||
msgstr "Valor inválido na opção 'murano_nets'"
|
|
||||||
|
|
||||||
msgid "It is forbidden to upload files larger than {0} MB."
|
|
||||||
msgstr "Não é permitido enviar arquivos maiores do que {0} MB."
|
|
||||||
|
|
||||||
msgid "KeyWord"
|
|
||||||
msgstr "Palavra Chave"
|
|
||||||
|
|
||||||
msgid "Last operation"
|
|
||||||
msgstr "Última operação"
|
|
||||||
|
|
||||||
msgid "Latest Deployment Log"
|
|
||||||
msgstr "Registros da última implantação"
|
|
||||||
|
|
||||||
msgid "License"
|
|
||||||
msgstr "Licença"
|
|
||||||
|
|
||||||
msgid "Logs"
|
|
||||||
msgstr "Registros"
|
|
||||||
|
|
||||||
msgid "Manage"
|
|
||||||
msgstr "Gerenciar"
|
|
||||||
|
|
||||||
msgid "Manage Components"
|
|
||||||
msgstr "Gerenciar Componentes"
|
|
||||||
|
|
||||||
msgctxt "Package requirements"
|
|
||||||
msgid "Manifest file"
|
|
||||||
msgstr "Arquivo de manifesto"
|
|
||||||
|
|
||||||
msgid "Mark Image"
|
|
||||||
msgstr "Marcar Imagem"
|
|
||||||
|
|
||||||
msgid ""
|
|
||||||
"Mark an image with Murano specific metadata to be added to the selected "
|
|
||||||
"image."
|
|
||||||
msgstr ""
|
|
||||||
"Marcar uma imagem com específico metadados do Murano para ser adicionado na "
|
|
||||||
"imagem selecionada."
|
|
||||||
|
|
||||||
msgid "Marked Images"
|
|
||||||
msgstr "Imagens Marcadas"
|
|
||||||
|
|
||||||
msgid "Modify Package"
|
|
||||||
msgstr "Modificar Pacote"
|
|
||||||
|
|
||||||
msgid "Modifying package failed"
|
|
||||||
msgstr "Falha ao modificar pacote"
|
|
||||||
|
|
||||||
msgid "NO ENVIRONMENTS"
|
|
||||||
msgstr "NENHUM AMBIENTE"
|
|
||||||
|
|
||||||
msgid "Name"
|
|
||||||
msgstr "Nome"
|
|
||||||
|
|
||||||
msgid "Name of the bundle."
|
|
||||||
msgstr "Nome do conjunto."
|
|
||||||
|
|
||||||
#, python-format
|
|
||||||
msgid "Network of '%s'"
|
|
||||||
msgstr "Rede de '%s'"
|
|
||||||
|
|
||||||
msgid "Next"
|
|
||||||
msgstr "Próximo"
|
|
||||||
|
|
||||||
msgid "Next Page"
|
|
||||||
msgstr "Próxima Página"
|
|
||||||
|
|
||||||
msgid "No availability zones available"
|
|
||||||
msgstr "Nenhuma zona de disponibilidade disponível"
|
|
||||||
|
|
||||||
msgid "No categories available"
|
|
||||||
msgstr "Nenhuma categoria disponível"
|
|
||||||
|
|
||||||
msgid "No components"
|
|
||||||
msgstr "Nenhum componente"
|
|
||||||
|
|
||||||
msgid "No images available"
|
|
||||||
msgstr "Sem imagens disponíveis"
|
|
||||||
|
|
||||||
msgid "No keypair"
|
|
||||||
msgstr "Sem par de chaves"
|
|
||||||
|
|
||||||
msgid "No license"
|
|
||||||
msgstr "Nenhuma licença"
|
|
||||||
|
|
||||||
msgid "No recent activity to report at this time."
|
|
||||||
msgstr "Não há atividades recentes para relatar neste momento."
|
|
||||||
|
|
||||||
msgid "No requirements"
|
|
||||||
msgstr "Nenhum requisito"
|
|
||||||
|
|
||||||
msgid "None"
|
|
||||||
msgstr "Nenhum"
|
|
||||||
|
|
||||||
msgid "Not in domain"
|
|
||||||
msgstr "Fora do domínio"
|
|
||||||
|
|
||||||
msgid "Note"
|
|
||||||
msgstr "Nota"
|
|
||||||
|
|
||||||
msgid ""
|
|
||||||
"OpenStack Networking (Neutron) is not available in current environment. "
|
|
||||||
"Custom Network Settings cannot be applied"
|
|
||||||
msgstr ""
|
|
||||||
"O módulo de rede do OpenStack (Neutron) não está disponível no ambiente "
|
|
||||||
"atual. As regras de rede padrão não podem ser aplicadas."
|
|
||||||
|
|
||||||
msgid "Operation is forbidden by murano-api server."
|
|
||||||
msgstr "Operação proibida pelo murano-api server"
|
|
||||||
|
|
||||||
msgid "Optional"
|
|
||||||
msgstr "Opcional"
|
|
||||||
|
|
||||||
msgid "Overview"
|
|
||||||
msgstr "Visão Geral"
|
|
||||||
|
|
||||||
msgid "Package Bundle Source"
|
|
||||||
msgstr "Origem do Conjunto de Pacotes"
|
|
||||||
|
|
||||||
msgid "Package Count"
|
|
||||||
msgstr "Contagem de Pacotes"
|
|
||||||
|
|
||||||
msgid "Package Details"
|
|
||||||
msgstr "Detalhes do Pacote"
|
|
||||||
|
|
||||||
msgid "Package Name"
|
|
||||||
msgstr "Nome do Pacote"
|
|
||||||
|
|
||||||
msgid "Package Source"
|
|
||||||
msgstr "Origem do Pacote"
|
|
||||||
|
|
||||||
msgid "Package Tags"
|
|
||||||
msgstr "Etiquetas de pacotes"
|
|
||||||
|
|
||||||
msgid "Package URL"
|
|
||||||
msgstr "URL do Pacote"
|
|
||||||
|
|
||||||
msgid "Package Version"
|
|
||||||
msgstr "Versão do Pacote"
|
|
||||||
|
|
||||||
msgid ""
|
|
||||||
"Package creation failed.Reason: Can't find Package name from repository."
|
|
||||||
msgstr ""
|
|
||||||
"Falha ao criar o pacote. Motivo: Não foi possível encontrar o nome do pacote "
|
|
||||||
"no repositório."
|
|
||||||
|
|
||||||
msgid "Package creation failed.Reason: {0}"
|
|
||||||
msgstr "Falha ao criar o pacote. Motivo: {0}"
|
|
||||||
|
|
||||||
msgid "Package modified."
|
|
||||||
msgstr "Pacote modificado."
|
|
||||||
|
|
||||||
msgid "Package name in the repository, usually a fully qualified name"
|
|
||||||
msgstr ""
|
|
||||||
"Nome do pacote no repositório, geralmente é um nome totalmente qualificado."
|
|
||||||
|
|
||||||
msgid "Package or Class with the same name is already made public"
|
|
||||||
msgstr "Um Pacote ou Classe com o mesmo nome já foi tornado público"
|
|
||||||
|
|
||||||
msgid "Package parameters successfully updated."
|
|
||||||
msgstr "Parâmetros do pacote atualizados com sucesso."
|
|
||||||
|
|
||||||
msgid "Package version"
|
|
||||||
msgstr "Versão do Pacote"
|
|
||||||
|
|
||||||
msgid "Package with id {0} is not found"
|
|
||||||
msgstr "Pacote com id {0} não foi encontrado"
|
|
||||||
|
|
||||||
msgid "Package with specified name already exists"
|
|
||||||
msgstr "Um pacote com o nome especificado já existe"
|
|
||||||
|
|
||||||
msgid "Package {0} already registered."
|
|
||||||
msgstr "O pacote {0} já está registrado"
|
|
||||||
|
|
||||||
msgid "Package {0} upload failed. {1}"
|
|
||||||
msgstr "O envio do pacote {0} falhou. {1}"
|
|
||||||
|
|
||||||
msgid "Package {0} uploaded"
|
|
||||||
msgstr "Pacote {0} enviado"
|
|
||||||
|
|
||||||
msgid "Packages"
|
|
||||||
msgstr "Pacotes"
|
|
||||||
|
|
||||||
msgid "Packages should contain:"
|
|
||||||
msgstr "Pacotes devem conter:"
|
|
||||||
|
|
||||||
msgid "Please confirm your password"
|
|
||||||
msgstr "Por favor confirme a sua senha"
|
|
||||||
|
|
||||||
msgid "Please supply a bundle name"
|
|
||||||
msgstr "Por favor dê um nome ao conjunto"
|
|
||||||
|
|
||||||
msgid "Please supply a bundle url"
|
|
||||||
msgstr "Por favor forneça uma url para o conjunto"
|
|
||||||
|
|
||||||
msgid "Please supply a package file"
|
|
||||||
msgstr "Por favor forneça o arquivo do pacote"
|
|
||||||
|
|
||||||
msgid "Please supply a package name"
|
|
||||||
msgstr "Por favor forneça um nome para o pacote"
|
|
||||||
|
|
||||||
msgid "Please supply a package url"
|
|
||||||
msgstr "Por favor forneça uma url para o pacote"
|
|
||||||
|
|
||||||
msgid "Previous Page"
|
|
||||||
msgstr "Página Anterior"
|
|
||||||
|
|
||||||
msgid "Provide desired name for a new category"
|
|
||||||
msgstr "Forneça o nome desejado para uma nova categoria"
|
|
||||||
|
|
||||||
msgid "Public"
|
|
||||||
msgstr "Público"
|
|
||||||
|
|
||||||
msgid "Quick Deploy"
|
|
||||||
msgstr "Implantação Rápida"
|
|
||||||
|
|
||||||
msgid "Recent Activity"
|
|
||||||
msgstr "Atividade Recente"
|
|
||||||
|
|
||||||
msgid "Repository"
|
|
||||||
msgstr "Repositório"
|
|
||||||
|
|
||||||
msgid "Requested object is not found on murano server."
|
|
||||||
msgstr "O objeto solicitado não foi encontrado no servidor do murano"
|
|
||||||
|
|
||||||
msgid "Requested operation conflicts with an existing object."
|
|
||||||
msgstr "A operação solicitada tem conflito com um objeto existente."
|
|
||||||
|
|
||||||
msgid "Requirements"
|
|
||||||
msgstr "Requisitos"
|
|
||||||
|
|
||||||
msgid "Retype your password"
|
|
||||||
msgstr "Digite sua senha novamente"
|
|
||||||
|
|
||||||
msgid "Select Application"
|
|
||||||
msgstr "Selecione um aplicação"
|
|
||||||
|
|
||||||
msgid "Select Image"
|
|
||||||
msgstr "Selecione a Imagem"
|
|
||||||
|
|
||||||
msgid "Select an image registered in Glance Image Services."
|
|
||||||
msgstr "Selecione uma imagem registrada no Serviço de Imagens Glance."
|
|
||||||
|
|
||||||
msgid "Select one or more categories for a package."
|
|
||||||
msgstr "Selecione uma ou mais categorias para um pacote"
|
|
||||||
|
|
||||||
msgid "Set up for identifying a package."
|
|
||||||
msgstr "Configure para identificar um pacote"
|
|
||||||
|
|
||||||
msgid "Show Details"
|
|
||||||
msgstr "Mostrar detalhes"
|
|
||||||
|
|
||||||
msgid "Something went wrong during package downloading"
|
|
||||||
msgstr "Algo deu errado ao baixar o pacote."
|
|
||||||
|
|
||||||
msgid "Sorry, this environment doesn't exist anymore"
|
|
||||||
msgstr "Desculpe, este ambiente não existe mais"
|
|
||||||
|
|
||||||
msgid ""
|
|
||||||
"Sorry, you can't add application right now. The environment is deploying."
|
|
||||||
msgstr ""
|
|
||||||
"Desculpe, você não pode adicionar esta aplicação agora. O ambiente está "
|
|
||||||
"sendo implantado."
|
|
||||||
|
|
||||||
msgid "Sorry, you can't delete service right now"
|
|
||||||
msgstr "Desculpe, você não pode remover o serviço agora"
|
|
||||||
|
|
||||||
msgid "Specified title already in use. Please choose another one."
|
|
||||||
msgstr "O título especificado já esta em uso. Por favor escolha outro."
|
|
||||||
|
|
||||||
msgid "Specifying a category helps to filter applications in the catalog"
|
|
||||||
msgstr "Especificar uma categoria ajuda a filtrar aplicações no catálogo"
|
|
||||||
|
|
||||||
msgid "Status"
|
|
||||||
msgstr "Estado"
|
|
||||||
|
|
||||||
msgid "Step {0}"
|
|
||||||
msgstr "Passo {0}"
|
|
||||||
|
|
||||||
msgid "Tags"
|
|
||||||
msgstr "Etiquetas"
|
|
||||||
|
|
||||||
msgid "Tenant Name"
|
|
||||||
msgstr "Nome do Locatário"
|
|
||||||
|
|
||||||
msgid "The '{0}' application successfully added to environment."
|
|
||||||
msgstr "A aplicação '{0}' foi adicionada com sucesso ao ambiente."
|
|
||||||
|
|
||||||
msgid ""
|
|
||||||
"The VMs of the applications in this environment will join this net by "
|
|
||||||
"default, unless configured individually. Choosing 'Create New' will generate "
|
|
||||||
"a new Network with a Subnet having an IP range allocated among the ones "
|
|
||||||
"available for the default Murano Router of this project"
|
|
||||||
msgstr ""
|
|
||||||
"As VMs das aplicações neste ambiente se conectarão a esta rede por padrão, a "
|
|
||||||
"menos que sejam configuradas individualmente. Escolher 'Criar Nova' criara "
|
|
||||||
"uma nova rede com uma subrede contendo um conjunto de IPs alocados dentre os "
|
|
||||||
"disponíveis para o roteador padrão do Murano deste projeto."
|
|
||||||
|
|
||||||
#, python-format
|
|
||||||
msgid ""
|
|
||||||
"The bundle is going to be installed from <a href=\"%(murano_repo_url)s\" "
|
|
||||||
"target=\"_blank\">%(murano_repo_url)s</a> repository."
|
|
||||||
msgstr ""
|
|
||||||
"O conjunto será instalado do repositório <a href=\"%(murano_repo_url)s\" "
|
|
||||||
"target=\"_blank\">%(murano_repo_url)s</a>."
|
|
||||||
|
|
||||||
#, python-format
|
|
||||||
msgid ""
|
|
||||||
"The package is going to be imported from <a href=\"%(murano_repo_url)s\" "
|
|
||||||
"target=\"_blank\">%(murano_repo_url)s</a> repository."
|
|
||||||
msgstr ""
|
|
||||||
"O pacote será importado do repositório <a href=\"%(murano_repo_url)s\" "
|
|
||||||
"target=\"_blank\">%(murano_repo_url)s</a>."
|
|
||||||
|
|
||||||
msgid ""
|
|
||||||
"The password must contain at least one letter, "
|
|
||||||
"one number and one special character"
|
|
||||||
msgstr ""
|
|
||||||
"A senha deve conter pelo menos uma letra, um número e um caractere especial"
|
|
||||||
|
|
||||||
msgid "The request data is not acceptable by the server"
|
|
||||||
msgstr "O dado solicitado não é adequado para o servidor."
|
|
||||||
|
|
||||||
msgid "There are no applications in the catalog. You can import apps from"
|
|
||||||
msgstr ""
|
|
||||||
"Não existem aplicações neste catálogo. Você pode importas aplicações de"
|
|
||||||
|
|
||||||
msgid "There are no applications matching your criteria."
|
|
||||||
msgstr "Não há aplicações correspondentes aos seus critérios."
|
|
||||||
|
|
||||||
msgid ""
|
|
||||||
"This action cannot be undone. Any resources created by this environment will "
|
|
||||||
"have to be released manually."
|
|
||||||
msgstr ""
|
|
||||||
"Esta ação não pode ser desfeita. Qualquer recurso criado por este ambiente "
|
|
||||||
"deverá ser liberado manualmente."
|
|
||||||
|
|
||||||
msgid "Time Finished"
|
|
||||||
msgstr "Horário de Término"
|
|
||||||
|
|
||||||
msgid "Time Started"
|
|
||||||
msgstr "Horário de Início"
|
|
||||||
|
|
||||||
msgid "Time updated"
|
|
||||||
msgstr "Hora atualizada"
|
|
||||||
|
|
||||||
msgid "Title"
|
|
||||||
msgstr "Título"
|
|
||||||
|
|
||||||
msgid "Toggle Active"
|
|
||||||
msgid_plural "Toggle Active"
|
|
||||||
msgstr[0] "Alternar Ativo"
|
|
||||||
msgstr[1] "Alternar Ativo"
|
|
||||||
|
|
||||||
msgid "Toggle Enabled"
|
|
||||||
msgstr " Alternar Habilitado"
|
|
||||||
|
|
||||||
msgid "Toggle Public"
|
|
||||||
msgid_plural "Toggle Public"
|
|
||||||
msgstr[0] "Alternar Público"
|
|
||||||
msgstr[1] "Alternar Público"
|
|
||||||
|
|
||||||
msgid "Toggled Active"
|
|
||||||
msgid_plural "Toggled Active"
|
|
||||||
msgstr[0] "Alternar Ativo"
|
|
||||||
msgstr[1] "Alternar Ativo"
|
|
||||||
|
|
||||||
msgid "Toggled Public"
|
|
||||||
msgid_plural "Toggled Public"
|
|
||||||
msgstr[0] "Alternado Público"
|
|
||||||
msgstr[1] "Alternado Público"
|
|
||||||
|
|
||||||
msgid "Topology"
|
|
||||||
msgstr "Topologia"
|
|
||||||
|
|
||||||
msgid ""
|
|
||||||
"Trying to add {0} image to glance. Image will be ready for deployment after "
|
|
||||||
"successful upload"
|
|
||||||
msgstr ""
|
|
||||||
"Tentando adicionar a imagem {0} ao glance. A imagem estará pronta para "
|
|
||||||
"implantação depois de ser enviada com sucesso"
|
|
||||||
|
|
||||||
msgid ""
|
|
||||||
"Trying to add {0}, {1} image to glance. Image will be ready for deployment "
|
|
||||||
"after successful upload"
|
|
||||||
msgstr ""
|
|
||||||
"Tentando adicionar a imagem {0}, {1} ao glance. A imagem estará pronta para "
|
|
||||||
"implantação depois de ser enviada com sucesso"
|
|
||||||
|
|
||||||
msgid "Type"
|
|
||||||
msgstr "Tipo"
|
|
||||||
|
|
||||||
msgctxt "Package requirements"
|
|
||||||
msgid "UI definition folder"
|
|
||||||
msgstr "Pasta de definições de UI"
|
|
||||||
|
|
||||||
msgid "UNKNOWN"
|
|
||||||
msgstr "DESCONHECIDO"
|
|
||||||
|
|
||||||
msgid "URL"
|
|
||||||
msgstr "URL"
|
|
||||||
|
|
||||||
msgid "Unable to abandon an environment {0} due to: {1}"
|
|
||||||
msgstr "Não foi possível abandonar o ambiente {0} por causa de: {1}"
|
|
||||||
|
|
||||||
msgid "Unable to communicate to murano-api server."
|
|
||||||
msgstr "Não foi possível se comunicar com o murano-api server."
|
|
||||||
|
|
||||||
msgid "Unable to create environment {0} due to: {1}"
|
|
||||||
msgstr "Não foi possível criar o ambiente {0} por causa de: {1}"
|
|
||||||
|
|
||||||
msgid "Unable to delete category"
|
|
||||||
msgstr "Não foi possível remover a categoria"
|
|
||||||
|
|
||||||
msgid "Unable to delete environment {0} due to: {1}"
|
|
||||||
msgstr "Não foi possível remover o ambiente {0} por causa de: {1}"
|
|
||||||
|
|
||||||
msgid "Unable to delete package in murano-api server"
|
|
||||||
msgstr "Não foi possível remover o pacote no servidor murano-api"
|
|
||||||
|
|
||||||
msgid "Unable to deploy. Try again later"
|
|
||||||
msgstr "Não foi possível implantar. Tente novamente mais tarde"
|
|
||||||
|
|
||||||
msgid "Unable to download package."
|
|
||||||
msgstr "Não foi possível baixar pacote."
|
|
||||||
|
|
||||||
msgid "Unable to get list of categories"
|
|
||||||
msgstr "Não foi possível obter a lista de categorias"
|
|
||||||
|
|
||||||
msgid "Unable to mark image"
|
|
||||||
msgstr "Não foi possível marcar a imagem"
|
|
||||||
|
|
||||||
msgid "Unable to modify package"
|
|
||||||
msgstr "Não foi possível modificar o pacote"
|
|
||||||
|
|
||||||
msgid "Unable to remove metadata"
|
|
||||||
msgstr "Não foi possível remover metadados"
|
|
||||||
|
|
||||||
msgid "Unable to remove package."
|
|
||||||
msgstr "Não foi possível remover o pacote."
|
|
||||||
|
|
||||||
msgid "Unable to retrieve availability zones."
|
|
||||||
msgstr "Não foi possível obter as zonas de disponibilidade"
|
|
||||||
|
|
||||||
msgid "Unable to retrieve details for service"
|
|
||||||
msgstr "Não foi possível obter detalhes do serviço"
|
|
||||||
|
|
||||||
msgid "Unable to retrieve list of deployments"
|
|
||||||
msgstr "Não foi possível obter a lista de implantações"
|
|
||||||
|
|
||||||
msgid "Unable to retrieve list of images"
|
|
||||||
msgstr "Não foi possível obter lista de imagens"
|
|
||||||
|
|
||||||
msgid ""
|
|
||||||
"Unable to retrieve list of services. This environment is deploying or "
|
|
||||||
"already deployed by other user."
|
|
||||||
msgstr ""
|
|
||||||
"Não foi possível obter a lista dos serviços. Este ambiente está sendo "
|
|
||||||
"implantado ou já foi implantado por outro usuário."
|
|
||||||
|
|
||||||
msgid "Unable to retrieve package details."
|
|
||||||
msgstr "Não foi possível obter os detalhes do pacote"
|
|
||||||
|
|
||||||
msgid "Unable to retrieve project list."
|
|
||||||
msgstr "Não foi possível obter a lista de projeto."
|
|
||||||
|
|
||||||
msgid "Unable to retrieve public images."
|
|
||||||
msgstr "Não foi possível obter as imagens públicas."
|
|
||||||
|
|
||||||
msgid "Unavailable"
|
|
||||||
msgstr "Indisponível"
|
|
||||||
|
|
||||||
msgid "Update"
|
|
||||||
msgstr "Atualizar"
|
|
||||||
|
|
||||||
msgid "Update Image"
|
|
||||||
msgstr "Atualizar Imagem"
|
|
||||||
|
|
||||||
msgid "Updated"
|
|
||||||
msgstr "Atualizado"
|
|
||||||
|
|
||||||
msgid "Uploading package failed. {0}"
|
|
||||||
msgstr "Falha ao enviar o pacote. {0}"
|
|
||||||
|
|
||||||
msgid "Used for identifying and filtering packages."
|
|
||||||
msgstr "Usado para identificas e filtrar pacotes."
|
|
||||||
|
|
||||||
msgid "Validation Error occurred"
|
|
||||||
msgstr "Ocorreu um erro de validação"
|
|
||||||
|
|
||||||
msgid "Version"
|
|
||||||
msgstr "Versão"
|
|
||||||
|
|
||||||
msgid "Version of the package (optional)."
|
|
||||||
msgstr "Versão do pacote (opcional)."
|
|
||||||
|
|
||||||
msgid "You are not allowed to change this properties of the package"
|
|
||||||
msgstr "Você não tem permissão para modificar as propriedades deste pacote."
|
|
||||||
|
|
||||||
msgid "You are not allowed to delete this package"
|
|
||||||
msgstr "Você não tem permissão para remover este pacote"
|
|
||||||
|
|
||||||
msgid "You are not allowed to perform this operation"
|
|
||||||
msgstr "Você não tem permissão para executar esta operação"
|
|
||||||
|
|
||||||
msgid ""
|
|
||||||
"You'll have to configure each package installed from this bundle separately."
|
|
||||||
msgstr ""
|
|
||||||
"Você terá que configurar cada pacote instalado deste conjunto "
|
|
||||||
"individualmente."
|
|
||||||
|
|
||||||
msgid "{0}{1} don't match"
|
|
||||||
msgstr "{0}{1} não correspondem"
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,87 +0,0 @@
|
||||||
# Aleksey Alekseenko <9118250541@mail.ru>, 2016. #zanata
|
|
||||||
# Andreas Jaeger <jaegerandi@gmail.com>, 2016. #zanata
|
|
||||||
# Yulia Ryndenkova <yryndenkova@hystax.com>, 2016. #zanata
|
|
||||||
msgid ""
|
|
||||||
msgstr ""
|
|
||||||
"Project-Id-Version: murano-dashboard 3.0.0.0rc2.dev57\n"
|
|
||||||
"Report-Msgid-Bugs-To: https://bugs.launchpad.net/openstack-i18n/\n"
|
|
||||||
"POT-Creation-Date: 2016-10-20 20:47+0000\n"
|
|
||||||
"MIME-Version: 1.0\n"
|
|
||||||
"Content-Type: text/plain; charset=UTF-8\n"
|
|
||||||
"Content-Transfer-Encoding: 8bit\n"
|
|
||||||
"PO-Revision-Date: 2016-10-19 02:23+0000\n"
|
|
||||||
"Last-Translator: Aleksey Alekseenko <9118250541@mail.ru>\n"
|
|
||||||
"Language-Team: Russian\n"
|
|
||||||
"Language: ru\n"
|
|
||||||
"X-Generator: Zanata 3.7.3\n"
|
|
||||||
"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n"
|
|
||||||
"%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2)\n"
|
|
||||||
|
|
||||||
msgid " 1 capital letter"
|
|
||||||
msgstr "1 заглавной буквы"
|
|
||||||
|
|
||||||
msgid " 1 digit"
|
|
||||||
msgstr "1 цифры "
|
|
||||||
|
|
||||||
msgid " 1 non-capital letter"
|
|
||||||
msgstr "1 строчной буквы"
|
|
||||||
|
|
||||||
msgid " 1 special character"
|
|
||||||
msgstr "1 специального символа"
|
|
||||||
|
|
||||||
msgid " 7 characters"
|
|
||||||
msgstr "7 символов"
|
|
||||||
|
|
||||||
msgid "An error occurred. Please try again later."
|
|
||||||
msgstr "Произошла ошибка. Повторите попытку."
|
|
||||||
|
|
||||||
msgid "Cancel"
|
|
||||||
msgstr "Отмена"
|
|
||||||
|
|
||||||
msgid "Create"
|
|
||||||
msgstr "Создать"
|
|
||||||
|
|
||||||
msgid "Loading"
|
|
||||||
msgstr "Загрузка"
|
|
||||||
|
|
||||||
msgid "New"
|
|
||||||
msgstr "Новый"
|
|
||||||
|
|
||||||
msgid "Passwords do not match"
|
|
||||||
msgstr "Пароли не совпадают"
|
|
||||||
|
|
||||||
msgid "Show less"
|
|
||||||
msgstr "Показать меньше"
|
|
||||||
|
|
||||||
msgid "Show more"
|
|
||||||
msgstr "Показать больше"
|
|
||||||
|
|
||||||
msgid "There was an error submitting the form. Please try again."
|
|
||||||
msgstr "При отправке формы произошла ошибка. Повторите попытку."
|
|
||||||
|
|
||||||
msgid "Unable to edit component metadata."
|
|
||||||
msgstr "Не удаётся изменить компоненты метаданных экземпляра."
|
|
||||||
|
|
||||||
msgid "Unable to edit environment metadata."
|
|
||||||
msgstr "Не удалось отредактировать окружение метаданных."
|
|
||||||
|
|
||||||
msgid "Unable to retrieve component metadata."
|
|
||||||
msgstr "Не удалось получить компоненты метаданных."
|
|
||||||
|
|
||||||
msgid "Unable to retrieve environment metadata."
|
|
||||||
msgstr "Не удалось получить окружение метаданных."
|
|
||||||
|
|
||||||
msgid "Unable to retrieve the packages."
|
|
||||||
msgstr "Не удалось получить пакеты."
|
|
||||||
|
|
||||||
msgid "Unable to run action."
|
|
||||||
msgstr "Невозможно выполнить действие."
|
|
||||||
|
|
||||||
msgid "Waiting for a result"
|
|
||||||
msgstr "Ожидаю результат"
|
|
||||||
|
|
||||||
msgid "Working"
|
|
||||||
msgstr "Обработка"
|
|
||||||
|
|
||||||
msgid "Your password should have at least"
|
|
||||||
msgstr "Ваш пароль должен состоять по крайней мере из"
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,85 +0,0 @@
|
||||||
# işbaran akçayır <isbaran@gmail.com>, 2017. #zanata
|
|
||||||
msgid ""
|
|
||||||
msgstr ""
|
|
||||||
"Project-Id-Version: murano-dashboard 4.0.0.0b2.dev13\n"
|
|
||||||
"Report-Msgid-Bugs-To: https://bugs.launchpad.net/openstack-i18n/\n"
|
|
||||||
"POT-Creation-Date: 2017-05-18 16:05+0000\n"
|
|
||||||
"MIME-Version: 1.0\n"
|
|
||||||
"Content-Type: text/plain; charset=UTF-8\n"
|
|
||||||
"Content-Transfer-Encoding: 8bit\n"
|
|
||||||
"PO-Revision-Date: 2017-05-22 02:38+0000\n"
|
|
||||||
"Last-Translator: Copied by Zanata <copied-by-zanata@zanata.org>\n"
|
|
||||||
"Language-Team: Turkish (Turkey)\n"
|
|
||||||
"Language: tr-TR\n"
|
|
||||||
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
|
|
||||||
"X-Generator: Zanata 3.9.6\n"
|
|
||||||
"X-POOTLE-MTIME: 1495463858.000000\n"
|
|
||||||
|
|
||||||
msgid " 1 capital letter"
|
|
||||||
msgstr " 1 büyük harf"
|
|
||||||
|
|
||||||
msgid " 1 digit"
|
|
||||||
msgstr " 1 sayı"
|
|
||||||
|
|
||||||
msgid " 1 non-capital letter"
|
|
||||||
msgstr " 1 büyük olmayan harf"
|
|
||||||
|
|
||||||
msgid " 1 special character"
|
|
||||||
msgstr " 1 özel karakter"
|
|
||||||
|
|
||||||
msgid " 7 characters"
|
|
||||||
msgstr " 7 karakter"
|
|
||||||
|
|
||||||
msgid "An error occurred. Please try again later."
|
|
||||||
msgstr "Bir hata oluştu. Lütfen sonra tekrar deneyin."
|
|
||||||
|
|
||||||
msgid "Cancel"
|
|
||||||
msgstr "İptal"
|
|
||||||
|
|
||||||
msgid "Create"
|
|
||||||
msgstr "Oluştur"
|
|
||||||
|
|
||||||
msgid "Loading"
|
|
||||||
msgstr "Yükleniyor"
|
|
||||||
|
|
||||||
msgid "New"
|
|
||||||
msgstr "Yeni"
|
|
||||||
|
|
||||||
msgid "Passwords do not match"
|
|
||||||
msgstr "Parolalar eşleşmiyor"
|
|
||||||
|
|
||||||
msgid "Show less"
|
|
||||||
msgstr "Daha az göster"
|
|
||||||
|
|
||||||
msgid "Show more"
|
|
||||||
msgstr "Daha fazla göster"
|
|
||||||
|
|
||||||
msgid "There was an error submitting the form. Please try again."
|
|
||||||
msgstr "Formu göndermede bir hata oluştu. Lütfen tekrar deneyin."
|
|
||||||
|
|
||||||
msgid "Unable to edit component metadata."
|
|
||||||
msgstr "Bileşen metaverisi düzenlenemedi."
|
|
||||||
|
|
||||||
msgid "Unable to edit environment metadata."
|
|
||||||
msgstr "Çevre metaverisi alınamadı."
|
|
||||||
|
|
||||||
msgid "Unable to retrieve component metadata."
|
|
||||||
msgstr "Bileşen metaverisi alınamadı."
|
|
||||||
|
|
||||||
msgid "Unable to retrieve environment metadata."
|
|
||||||
msgstr "Çevre metaverisi alınamadı."
|
|
||||||
|
|
||||||
msgid "Unable to retrieve the packages."
|
|
||||||
msgstr "Paketler alınamadı."
|
|
||||||
|
|
||||||
msgid "Unable to run action."
|
|
||||||
msgstr "Eylem çalıştırılamadı."
|
|
||||||
|
|
||||||
msgid "Waiting for a result"
|
|
||||||
msgstr "Bir sonuç bekleniyor"
|
|
||||||
|
|
||||||
msgid "Working"
|
|
||||||
msgstr "Çalışıyor"
|
|
||||||
|
|
||||||
msgid "Your password should have at least"
|
|
||||||
msgstr "Parolanız en az şunlara sahip olmalı"
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,86 +0,0 @@
|
||||||
# Andreas Jaeger <jaegerandi@gmail.com>, 2016. #zanata
|
|
||||||
# Wu Han <wu.han@h3c.com>, 2016. #zanata
|
|
||||||
# Wu Han <wu.han@h3c.com>, 2017. #zanata
|
|
||||||
msgid ""
|
|
||||||
msgstr ""
|
|
||||||
"Project-Id-Version: murano-dashboard 3.1.1.dev12\n"
|
|
||||||
"Report-Msgid-Bugs-To: https://bugs.launchpad.net/openstack-i18n/\n"
|
|
||||||
"POT-Creation-Date: 2017-02-09 17:38+0000\n"
|
|
||||||
"MIME-Version: 1.0\n"
|
|
||||||
"Content-Type: text/plain; charset=UTF-8\n"
|
|
||||||
"Content-Transfer-Encoding: 8bit\n"
|
|
||||||
"PO-Revision-Date: 2017-02-10 02:26+0000\n"
|
|
||||||
"Last-Translator: Wu Han <wu.han@h3c.com>\n"
|
|
||||||
"Language-Team: Chinese (China)\n"
|
|
||||||
"Language: zh-CN\n"
|
|
||||||
"X-Generator: Zanata 3.7.3\n"
|
|
||||||
"Plural-Forms: nplurals=1; plural=0\n"
|
|
||||||
|
|
||||||
msgid " 1 capital letter"
|
|
||||||
msgstr "1个大写字母"
|
|
||||||
|
|
||||||
msgid " 1 digit"
|
|
||||||
msgstr "1个数字"
|
|
||||||
|
|
||||||
msgid " 1 non-capital letter"
|
|
||||||
msgstr "1个非大写字母"
|
|
||||||
|
|
||||||
msgid " 1 special character"
|
|
||||||
msgstr "1个特殊字符"
|
|
||||||
|
|
||||||
msgid " 7 characters"
|
|
||||||
msgstr "7个字符"
|
|
||||||
|
|
||||||
msgid "An error occurred. Please try again later."
|
|
||||||
msgstr "发生错,请稍后重试。"
|
|
||||||
|
|
||||||
msgid "Cancel"
|
|
||||||
msgstr "取消"
|
|
||||||
|
|
||||||
msgid "Create"
|
|
||||||
msgstr "创建"
|
|
||||||
|
|
||||||
msgid "Loading"
|
|
||||||
msgstr "加载中"
|
|
||||||
|
|
||||||
msgid "New"
|
|
||||||
msgstr "新"
|
|
||||||
|
|
||||||
msgid "Passwords do not match"
|
|
||||||
msgstr "密码不匹配"
|
|
||||||
|
|
||||||
msgid "Show less"
|
|
||||||
msgstr "显示更少"
|
|
||||||
|
|
||||||
msgid "Show more"
|
|
||||||
msgstr "显示更多"
|
|
||||||
|
|
||||||
msgid "There was an error submitting the form. Please try again."
|
|
||||||
msgstr "提交表单时出错,请重试。"
|
|
||||||
|
|
||||||
msgid "Unable to edit component metadata."
|
|
||||||
msgstr "不能编辑组件元数据。"
|
|
||||||
|
|
||||||
msgid "Unable to edit environment metadata."
|
|
||||||
msgstr "不能编辑环境元数据。"
|
|
||||||
|
|
||||||
msgid "Unable to retrieve component metadata."
|
|
||||||
msgstr "不能检索组件元数据。"
|
|
||||||
|
|
||||||
msgid "Unable to retrieve environment metadata."
|
|
||||||
msgstr "不能检索环境元数据。"
|
|
||||||
|
|
||||||
msgid "Unable to retrieve the packages."
|
|
||||||
msgstr "无法获取包。"
|
|
||||||
|
|
||||||
msgid "Unable to run action."
|
|
||||||
msgstr "无法执行操作。"
|
|
||||||
|
|
||||||
msgid "Waiting for a result"
|
|
||||||
msgstr "等待结果"
|
|
||||||
|
|
||||||
msgid "Working"
|
|
||||||
msgstr "进行中"
|
|
||||||
|
|
||||||
msgid "Your password should have at least"
|
|
||||||
msgstr "你的密码应该至少有"
|
|
|
@ -1,30 +0,0 @@
|
||||||
# Copyright (c) 2013 Mirantis, Inc.
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
||||||
# not use this file except in compliance with the License. You may obtain
|
|
||||||
# a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
||||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
||||||
# License for the specific language governing permissions and limitations
|
|
||||||
# under the License.
|
|
||||||
|
|
||||||
import traceback
|
|
||||||
|
|
||||||
from horizon import exceptions
|
|
||||||
from horizon import middleware
|
|
||||||
from oslo_log import log as logging
|
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class ExceptionMiddleware(middleware.HorizonMiddleware):
|
|
||||||
def process_exception(self, request, exception):
|
|
||||||
if not isinstance(exception, exceptions.Http302):
|
|
||||||
logger.error(traceback.format_exc())
|
|
||||||
return super(ExceptionMiddleware, self).process_exception(
|
|
||||||
request, exception)
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue