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