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:
Tony Breeds 2017-09-12 15:41:49 -06:00
parent a33ee4ba39
commit f77dc23e6b
307 changed files with 14 additions and 41430 deletions

View File

@ -1,9 +0,0 @@
[run]
source = muranodashboard
omit =
.tox/*
muranodashboard/tests/*
muranodashboard/local/*
[report]
ignore_errors = True

View File

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

42
.gitignore vendored
View File

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

View File

@ -1,4 +0,0 @@
[gerrit]
host=review.openstack.org
port=29418
project=openstack/murano-dashboard.git

View File

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

View File

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

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

14
README Normal file
View File

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

View File

@ -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/>`_

View File

@ -1,5 +0,0 @@
[extractors]
django = django_babel.extract:extract_django
[python: **.py]
[django: **/templates/**.html]

View File

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

View File

@ -1,2 +0,0 @@
[theme]
inherit = default

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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&nbsp;Components"
msgstr "Composants&nbsp;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

View File

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

View File

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

View File

@ -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 "암호는 최소 다음과 같아야 합니다"

View File

@ -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&nbsp;Components"
msgstr "Aplicação&nbsp;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

View File

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

View File

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

View File

@ -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 "你的密码应该至少有"

View File

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