murano-dashboard-5.0.0/0000775000175100017510000000000013245511556014775 5ustar zuulzuul00000000000000murano-dashboard-5.0.0/package.json0000666000175100017510000000126613245511125017262 0ustar zuulzuul00000000000000{ "version": "0.0.0", "private": true, "name": "muranodashboard", "description": "Murano Dashboard", "repository": "none", "license": "Apache 2.0", "devDependencies": { "eslint": "1.10.3", "eslint-config-openstack": "1.2.4", "jasmine-core": "2.4.1", "karma": "1.1.2", "karma-chrome-launcher": "1.0.1", "karma-cli": "1.0.1", "karma-jasmine": "1.0.2", "karma-ng-html2js-preprocessor": "1.0.0" }, "scripts": { "postinstall": "./tools/post_install.sh", "lint": "eslint --no-color muranodashboard/static", "lintq": "eslint --quiet muranodashboard/static", "test": "karma start karma.conf.js --single-run" }, "dependencies": {} } murano-dashboard-5.0.0/releasenotes/0000775000175100017510000000000013245511556017466 5ustar zuulzuul00000000000000murano-dashboard-5.0.0/releasenotes/notes/0000775000175100017510000000000013245511556020616 5ustar zuulzuul00000000000000murano-dashboard-5.0.0/releasenotes/notes/reload-empty-env-10165198e8384b08.yaml0000666000175100017510000000033113245511125026603 0ustar zuulzuul00000000000000--- fixes: - The :guilabel:`Environment components` page now reloads after an empty environment deployment. This allows adding new components to the empty environment without having to reload the page manually. murano-dashboard-5.0.0/releasenotes/notes/fixed-network-mode-for-envs-0af7dad3bee9957b.yaml0000666000175100017510000000116313245511125031405 0ustar zuulzuul00000000000000--- fixes: - | Introduce a fixed network mode for environments. Specifically, when this mode is activated, in the environment creation dialog user is no longer prompted for a network and instead a network previously assigned to the current project is used. Network is assigned to project using project metadata key (custom) with network ID as the value. Specify this metadata key in Horizon config to be able to use it This behavior is disabled by default and could be enabled by adding: USE_FIXED_NETWORK = yes FIXED_MURANO_NETWORK = murano_network to the Horizon configuration. murano-dashboard-5.0.0/releasenotes/notes/display_repo_url-47c3cb0b45c2d68d.yaml0000666000175100017510000000025713245511125027345 0ustar zuulzuul00000000000000--- features: - Added ``DISPLAY_MURANO_REPO_URL`` setting that is used as a user-visible link to ``apps.openstack.org`` or any other murano applications repository. murano-dashboard-5.0.0/releasenotes/notes/images-filter-project.yaml-081bffde1b91057f.yaml0000666000175100017510000000026013245511125031123 0ustar zuulzuul00000000000000--- features: - A new parameter MURANO_IMAGE_FILTER_PROJECT_ID has been added which, when given, will filter the list of public Murano images to the given project ID murano-dashboard-5.0.0/releasenotes/notes/safeloader-cve-2016-4972-82523879a6c3b1a5.yaml0000666000175100017510000000062513245511125027427 0ustar zuulzuul00000000000000--- security: - cve-2016-4972 has been addressed. In several places Murano used loaders inherited directly from yaml.Loader when parsing MuranoPL and UI files from packages. This is unsafe, because this loader is capable of creating custom python objects from specifically constructed yaml files. With this change all yaml loading operations are done using safe loaders instead. ././@LongLink0000000000000000000000000000014700000000000011217 Lustar 00000000000000murano-dashboard-5.0.0/releasenotes/notes/create-deployment-history-all-env-view-7610fd4301604b45.yamlmurano-dashboard-5.0.0/releasenotes/notes/create-deployment-history-all-env-view-7610fd4301604b45.ya0000666000175100017510000000041313245511125032547 0ustar zuulzuul00000000000000--- features: - | A new button, called "Deployment History", has been added to the Environments > Applications view. When clicked, the deployment history view is loaded, which shows deployments for all environments for the current project (tenant). murano-dashboard-5.0.0/releasenotes/notes/ui-definition-parameters-c9d2cb3a7da7459b.yaml0000666000175100017510000000466713245511125030774 0ustar zuulzuul00000000000000--- features: - > New section ``Parameters`` was added to UI definition markup. Parameters is a key-value storage, whose values are available as YAQL variables. Thus if the section has a key ``var`` its value can be retrieved using ``$var`` syntax and used anywhere in the markup - both as a field attribute values and in Application/Templates sections. Parameter values can be a YAQL expressions. The difference between Templates and Parameters is that Parameters are evaluated once before form render whereas Templates are evaluated on each access. - > It is possible to specify static action (MuranoPL method) that is going to be called before form is rendered. This allows MuranoPL class to provide parameter values to the form. Because parameters can be used as initial control values this also allows to have dynamic content in the form. Parameters source method can be specified in ``ParametersSource`` attribute of UI definition markup: ``ParametersSource: com.namespace.MyClass.myMethod``. If class name is not specified dashboard will try to infer it from the ``Application`` section or the package FQN. If specified, static action must be present in one of the classes in the same package that was used to obtain UI definition file. The method must return a dictionary which will be combined with Parameters that are already present in the file. - > ``ref(templateName [, parameterName] [, idOnly])`` YAQL function was added to UI definition DSL. This function evaluates template ``templateName`` and fixes the result in parameters under ``parameterName`` key (or ``templateName`` if the second parameter was omitted). Then it generates object ID and places it into ``?/id`` field. On the first use of ``parameterName`` or if ``idOnly`` is ``false`` the function will return the whole object structure. On subsequent calls or if ``idOnly`` is ``true`` it will return the ID that was generated upon the first call. Thus the function brings ability to reference single object several times. - > ``choice`` field type now can accept list of choices in a form of dictionary. I.e. in addition to ``[[key1, value1], [key2, value2]]`` one can provide ``{key1: value1, key2: value2}`` - > UI definition version was bumped to ``2.4``. If application is going to use Parameters it should indicate it by setting the version in UI file. murano-dashboard-5.0.0/releasenotes/notes/abstract-base-class-fix-7cb06a0924b973f3.yaml0000666000175100017510000000055613245511125030241 0ustar zuulzuul00000000000000--- fixes: - Specifying a base class in the UI definition now also fetches all the packages with classes that inherit from that class, when glare is used. For example, if you specify the 'io.example.Parent' class, the dashboard fetches 'io.example.Child1' and 'io.example.Child2', and any other descendants of 'io.example.Parent' that are present. murano-dashboard-5.0.0/releasenotes/notes/dashboard-rename-split-650ba2f7d4f846c2.yaml0000666000175100017510000000140313245511125030236 0ustar zuulzuul00000000000000--- prelude: > Murano Dashboard has been renamed to App Catalog and now allows seamless integration and single panel structure with App Catalog UI dashboard. features: - Murano dashboard has been renamed to App Catalog, monolithic config file has been split into multiple small files. Every such file defines either a panel group or adds general murano-related settings to horizon. upgrade: - To upgrade to Newton version of app catalog you need to remove old ``_50_murano.py`` config file, that defined in murano dashboard. Be sure to also remove any .pyc and .po files. After that you need to copy all new config files from ``muranodashboard/local/enabled/*.py`` to ``openstack_dashboard/local/enabled/`` and restart horizon murano-dashboard-5.0.0/releasenotes/notes/topology-icon-fix-6572c069d127ed95.yaml0000666000175100017510000000013213245511125027146 0ustar zuulzuul00000000000000--- fixes: - Topology viewer now properly displays icons of the deployed applications. murano-dashboard-5.0.0/releasenotes/notes/password-checking-780dc07fa1d9926a.yaml0000666000175100017510000000054613245511125027335 0ustar zuulzuul00000000000000--- fixes: - Fixed the password check in dynamic UI forms. Previously, the dashboard did not validate the password fields with IDs not ending with 'password'. Now, to determine whether to add default password validators to the field or not, the dashbord only checks the field type itself, instead of both field type and ending of the field ID. murano-dashboard-5.0.0/releasenotes/notes/reorganize-dashboard-settings-11733b5c1003154b.yaml0000666000175100017510000000110613245511125031370 0ustar zuulzuul00000000000000--- features: - Murano dashboard now comes with the ``muranodashboard/local/local_settings.d/_50_murano.py`` file that contains murano-specific settings for horizon (for example, ``MURANO_API_URL``). upgrade: - Previously all murano-specific horizon settings had to be kept in ``local_settings.py`` file of Horizon. You need to remove those settings from local_settings.py and copy ``muranodashboard/local/local_settings.d/_50_murano.py`` to ``openstack_dashboard/local/local_settings.d/`` directory in horizon and keep all murano-related changes there. murano-dashboard-5.0.0/releasenotes/notes/filter-in-package-definition-d463e434c856a412.yaml0000666000175100017510000000032213245511125031155 0ustar zuulzuul00000000000000--- fixes: - Filter by 'Name' in package definition only matches package name. features: - Added :filter by 'KeyWord' in package definition can matches all the package parameters like name, tags ..etc. murano-dashboard-5.0.0/releasenotes/notes/show-resource-91a1f73cdb5d74ab.yaml0000666000175100017510000000024613245511125026654 0ustar zuulzuul00000000000000--- features: - | Show resource usages in the description section right under the Flavor field title (as quota usages + predicted increment progress bar). murano-dashboard-5.0.0/releasenotes/notes/glance-glare-aa81451d785591ed.yaml0000666000175100017510000000042013245511125026153 0ustar zuulzuul00000000000000--- features: - When using glare the endpoint for client is now 'artifact' since glare has been moved to a separate service upgrade: - Parameters ``GLANCE_API_INSECURE`` and ``GLANCE_API_URL`` have been renamed to ``GLARE_API_INSECURE`` and ``GLARE_API_URL``. murano-dashboard-5.0.0/releasenotes/notes/add-encrypt-data-function-73f0407bf1427040.yaml0000666000175100017510000000042413245511125030422 0ustar zuulzuul00000000000000--- features: - | Adds a new yaql function 'encryptData' which encrypts values passed from MuranoPL UI definitions. Requires Barbican to be configured, see https://docs.openstack.org/murano/latest/admin/appdev-guide/encrypting_properties.html for more info. murano-dashboard-5.0.0/releasenotes/notes/import-horizon-filters-af5dcf0720502567.yaml0000666000175100017510000000025413245511125030273 0ustar zuulzuul00000000000000--- fixes: - Fixed the issue that prevented the murano dashboard from finding certain horizon filters, such as 'parse_isotime', 'timesince_or_never', and others. murano-dashboard-5.0.0/releasenotes/notes/fix_type_format-798a646071b1104b.yaml0000666000175100017510000000023013245511125026656 0ustar zuulzuul00000000000000--- fixes: - The issue with adding already deployed components to environment via dropdown is fixed with applying changes for the new type format.murano-dashboard-5.0.0/releasenotes/notes/ips-display-topology-5a45876dafc637eb.yaml0000666000175100017510000000014213245511125030107 0ustar zuulzuul00000000000000--- fixes: - VM IP addresses are now properly displayed in the environment topology viewer. murano-dashboard-5.0.0/releasenotes/notes/python3-cae8a08d96696550.yaml0000666000175100017510000000007113245511125025247 0ustar zuulzuul00000000000000--- prelude: > Murano-dashboard now supports python3 murano-dashboard-5.0.0/releasenotes/notes/bug-1405788-2c8b2708e3bfc63f.yaml0000666000175100017510000000011713245511125025405 0ustar zuulzuul00000000000000--- fixes: - It is now possible to use any symbols in environments name. murano-dashboard-5.0.0/releasenotes/notes/extend-flavor-requirements-d007f54c68c571ad.yaml0000666000175100017510000000027513245511125031214 0ustar zuulzuul00000000000000--- features: - Requirements for the flavor field accepts 'max_vcpus' 'max_disk' and 'max_memory_mb'. Previously only minimum values can be specified in the flavor requirements. murano-dashboard-5.0.0/releasenotes/notes/update_password_field-21a3b60658de3575.yaml0000666000175100017510000000124713245511125030123 0ustar zuulzuul00000000000000--- features: - Version of Dynamic UI is increased to 2.3 due to *password* field update. Now *password* supports validator overloading and control of automatic password conformation field insertion. * If ``regexpValidator`` is provided, default complex check for numbers, capital and small letters in the password is not performed. Also, several validators with corresponding Dynamic UI field may be used. * ``confirmInput`` parameter is supported now for controlling whether password field should be cloned or not. If application author decided to turn off automatic field cloning, he should set the new parameter to *false*. murano-dashboard-5.0.0/releasenotes/notes/bug-1650406-4e4a3bdcfcc5718a.yaml0000666000175100017510000000011513245511125025522 0ustar zuulzuul00000000000000--- features: - | Flavor field will show the initial value by default. murano-dashboard-5.0.0/releasenotes/notes/status-session-b06786d470910080.yaml0000666000175100017510000000044513245511125026416 0ustar zuulzuul00000000000000--- fixes: - Fixed the issue that reset all environment changes from the previous session (adding or removing components without deployment) after consequent login. Also, the :guilabel:`Ready to deploy` status now only displays if there are changes pending in the current session. murano-dashboard-5.0.0/releasenotes/notes/add-package-details-126fe8cbefcd0229.yaml0000666000175100017510000000020213245511125027607 0ustar zuulzuul00000000000000--- features: - Add details page for packages, The details page will show more info to user, such as FQN, Description, etc. murano-dashboard-5.0.0/releasenotes/notes/.placeholder0000666000175100017510000000000013245511125023061 0ustar zuulzuul00000000000000murano-dashboard-5.0.0/releasenotes/notes/manage-multiple-envs-e587c2e9432e39d7.yaml0000666000175100017510000000017513245511125027707 0ustar zuulzuul00000000000000--- features: - Added the capability to execute actions (delete, abandon or deploy) on multiple selected environments. murano-dashboard-5.0.0/releasenotes/notes/single_request_latest_apps-4f6add404ab07c15.yaml0000666000175100017510000000017513245511125031410 0ustar zuulzuul00000000000000--- fixes: - Improved the performance of the :guilabel:`Recent Activity` panel on the :guilabel:`Browse Catalog` page. murano-dashboard-5.0.0/releasenotes/notes/volume-selection-ui-element-d7ac69eceea3e584.yaml0000666000175100017510000000015213245511125031475 0ustar zuulzuul00000000000000--- features: - | Added a widget to display and select available volumes and volume snapshots. ././@LongLink0000000000000000000000000000016000000000000011212 Lustar 00000000000000murano-dashboard-5.0.0/releasenotes/notes/bp-murano-dynamic-ui-custom-realtime-validation-2151342003f6a385.yamlmurano-dashboard-5.0.0/releasenotes/notes/bp-murano-dynamic-ui-custom-realtime-validation-21513420030000666000175100017510000000063513245511125032776 0ustar zuulzuul00000000000000--- features: - | If a UI definition of the murano-applications has regex validation for input field then before generating html an additional attribute called 'data-validations' will be added to the form. This attribute has an array of objects. These objects have regex patterns and error messages. When filling a Murano-Applications form input fields will be validated by js script. murano-dashboard-5.0.0/releasenotes/notes/glance-v2-wanring-b7ef3e3ce0ce6ce1.yaml0000666000175100017510000000030313245511125027424 0ustar zuulzuul00000000000000--- other: - Murano Dashboard relies on Glance v1 API for image uploads. In case it is not available an error will be shown and all the image-related functionality will be unavailable. murano-dashboard-5.0.0/releasenotes/notes/bug-1579220-0a3fe23ac8f249ee.yaml0000666000175100017510000000026513245511125025457 0ustar zuulzuul00000000000000--- fixes: - Fixed the issue with sequential download of packages. Dashboard is now using 'tables.LinkAction' instead of 'tables.Action' for DownloadPackage table action. murano-dashboard-5.0.0/releasenotes/source/0000775000175100017510000000000013245511556020766 5ustar zuulzuul00000000000000murano-dashboard-5.0.0/releasenotes/source/newton.rst0000666000175100017510000000023213245511125023021 0ustar zuulzuul00000000000000=================================== Newton Series Release Notes =================================== .. release-notes:: :branch: origin/stable/newton murano-dashboard-5.0.0/releasenotes/source/_static/0000775000175100017510000000000013245511556022414 5ustar zuulzuul00000000000000murano-dashboard-5.0.0/releasenotes/source/_static/.placeholder0000666000175100017510000000000013245511125024657 0ustar zuulzuul00000000000000murano-dashboard-5.0.0/releasenotes/source/liberty.rst0000666000175100017510000000022213245511125023160 0ustar zuulzuul00000000000000============================== Liberty Series Release Notes ============================== .. release-notes:: :branch: origin/stable/liberty murano-dashboard-5.0.0/releasenotes/source/pike.rst0000666000175100017510000000021713245511125022442 0ustar zuulzuul00000000000000=================================== Pike Series Release Notes =================================== .. release-notes:: :branch: stable/pike murano-dashboard-5.0.0/releasenotes/source/conf.py0000666000175100017510000002200613245511125022257 0ustar zuulzuul00000000000000# -*- coding: utf-8 -*- # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or # implied. # See the License for the specific language governing permissions and # limitations under the License. # Murano Release Notes documentation build configuration file, created by # sphinx-quickstart on Tue Nov 3 17:40:50 2015. # # This file is execfile()d with the current directory set to its # containing dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. # sys.path.insert(0, os.path.abspath('.')) # -- General configuration ------------------------------------------------ # If your documentation needs a minimal Sphinx version, state it here. # needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ 'openstackdocstheme', 'reno.sphinxext', ] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # The suffix of source filenames. source_suffix = '.rst' # The encoding of source files. # source_encoding = 'utf-8-sig' # The master toctree document. master_doc = 'index' # General information about the project. project = u'Murano Dashboard Release Notes' copyright = u'2015, Murano Developers' # Release notes are version independent # The full version, including alpha/beta/rc tags. release = '' # The short X.Y version. version = '' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. # language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: # today = '' # Else, today_fmt is used as the format for a strftime call. # today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. exclude_patterns = [] # The reST default role (used for this markup: `text`) to use for all # documents. # default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. # add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). # add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. # show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' # A list of ignored prefixes for module index sorting. # modindex_common_prefix = [] # If true, keep warnings as "system message" paragraphs in the built documents. # keep_warnings = False # -- Options for HTML output ---------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. html_theme = 'openstackdocs' # 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 = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". # html_title = None # A shorter title for the navigation bar. Default is the same as html_title. # html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. # html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. # html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ['_static'] # Add any extra paths that contain custom files (such as robots.txt or # .htaccess) here, relative to this directory. These files are copied # directly to the root of the documentation. # html_extra_path = [] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. # html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. # html_use_smartypants = True # Custom sidebar templates, maps document names to template names. # html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. # html_additional_pages = {} # If false, no module index is generated. # html_domain_indices = True # If false, no index is generated. # html_use_index = True # If true, the index is split into individual pages for each letter. # html_split_index = False # If true, links to the reST sources are added to the pages. # html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. # html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. # html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. # html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). # html_file_suffix = None # Output file base name for HTML help builder. htmlhelp_basename = 'MuranoDashboardReleaseNotesdoc' # -- Options for LaTeX output --------------------------------------------- latex_elements = { # The paper size ('letterpaper' or 'a4paper'). # 'papersize': 'letterpaper', # The font size ('10pt', '11pt' or '12pt'). # 'pointsize': '10pt', # Additional stuff for the LaTeX preamble. # 'preamble': '', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ ('index', 'MuranoDashboardReleaseNotes.tex', u'Murano Dashboard Release Notes Documentation', u'Murano Developers', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of # the title page. # latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. # latex_use_parts = False # If true, show page references after internal links. # latex_show_pagerefs = False # If true, show URL addresses after external links. # latex_show_urls = False # Documents to append as an appendix to all manuals. # latex_appendices = [] # If false, no module index is generated. # latex_domain_indices = True # -- Options for manual page output --------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ ('index', 'muranodashboardreleasenotes', u'Murano Dashboard Release Notes Documentation', [u'Murano Developers'], 1) ] # If true, show URL addresses after external links. # man_show_urls = False # -- Options for Texinfo output ------------------------------------------- # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ ('index', 'MuranoDashboardReleaseNotes', u'Murano Dashboard Release Notes Documentation', u'Murano Developers', 'MuranoDashboardReleaseNotes', 'One line description of project.', 'Miscellaneous'), ] # Documents to append as an appendix to all manuals. # texinfo_appendices = [] # If false, no module index is generated. # texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. # texinfo_show_urls = 'footnote' # If true, do not generate a @detailmenu in the "Top" node's menu. # texinfo_no_detailmenu = False # -- Options for Internationalization output ------------------------------ locale_dirs = ['locale/'] murano-dashboard-5.0.0/releasenotes/source/unreleased.rst0000666000175100017510000000016013245511125023636 0ustar zuulzuul00000000000000============================== Current Series Release Notes ============================== .. release-notes:: murano-dashboard-5.0.0/releasenotes/source/index.rst0000666000175100017510000000027713245511141022625 0ustar zuulzuul00000000000000================================ Murano Dashboard Release Notes ================================ .. toctree:: :maxdepth: 2 unreleased pike ocata newton mitaka liberty murano-dashboard-5.0.0/releasenotes/source/mitaka.rst0000666000175100017510000000023213245511125022755 0ustar zuulzuul00000000000000=================================== Mitaka Series Release Notes =================================== .. release-notes:: :branch: origin/stable/mitaka murano-dashboard-5.0.0/releasenotes/source/ocata.rst0000666000175100017510000000023013245511125022574 0ustar zuulzuul00000000000000=================================== Ocata Series Release Notes =================================== .. release-notes:: :branch: origin/stable/ocata murano-dashboard-5.0.0/releasenotes/source/_templates/0000775000175100017510000000000013245511556023123 5ustar zuulzuul00000000000000murano-dashboard-5.0.0/releasenotes/source/_templates/.placeholder0000666000175100017510000000000013245511125025366 0ustar zuulzuul00000000000000murano-dashboard-5.0.0/doc/0000775000175100017510000000000013245511556015542 5ustar zuulzuul00000000000000murano-dashboard-5.0.0/doc/source/0000775000175100017510000000000013245511556017042 5ustar zuulzuul00000000000000murano-dashboard-5.0.0/doc/source/_static/0000775000175100017510000000000013245511556020470 5ustar zuulzuul00000000000000murano-dashboard-5.0.0/doc/source/_static/.placeholder0000777000175100017510000000000013245511125022736 0ustar zuulzuul00000000000000murano-dashboard-5.0.0/doc/source/conf.py0000666000175100017510000001777013245511125020347 0ustar zuulzuul00000000000000# -*- 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 = [] # 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 # " v 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 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)} murano-dashboard-5.0.0/doc/source/index.rst0000666000175100017510000000406713245511125020704 0ustar zuulzuul00000000000000.. 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 (*/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 murano-dashboard-5.0.0/doc/source/_theme/0000775000175100017510000000000013245511556020303 5ustar zuulzuul00000000000000murano-dashboard-5.0.0/doc/source/_theme/theme.conf0000777000175100017510000000003213245511125022244 0ustar zuulzuul00000000000000[theme] inherit = default murano-dashboard-5.0.0/playbooks/0000775000175100017510000000000013245511556017000 5ustar zuulzuul00000000000000murano-dashboard-5.0.0/playbooks/legacy/0000775000175100017510000000000013245511556020244 5ustar zuulzuul00000000000000murano-dashboard-5.0.0/playbooks/legacy/murano-dashboard-sanity-check/0000775000175100017510000000000013245511556026052 5ustar zuulzuul00000000000000murano-dashboard-5.0.0/playbooks/legacy/murano-dashboard-sanity-check/post.yaml0000666000175100017510000000375513245511125027727 0ustar zuulzuul00000000000000- hosts: primary tasks: - name: Copy files from {{ ansible_user_dir }}/workspace/ on node synchronize: src: '{{ ansible_user_dir }}/workspace/' dest: '{{ zuul.executor.log_root }}' mode: pull copy_links: true verify_host: true rsync_opts: - --include=/logs/** - --include=*/ - --exclude=* - --prune-empty-dirs - name: Copy files from {{ ansible_user_dir }}/workspace/ on node synchronize: src: '{{ ansible_user_dir }}/workspace/' dest: '{{ zuul.executor.log_root }}' mode: pull copy_links: true verify_host: true rsync_opts: - --include=**/*index.html - --include=**/*index.html.gz - --include=index.html - --include=index.html.gz - name: Copy files from {{ ansible_user_dir }}/workspace/ on node synchronize: src: '{{ ansible_user_dir }}/workspace/' dest: '{{ zuul.executor.log_root }}' mode: pull copy_links: true verify_host: true rsync_opts: - --include=**/*testr_results.html.gz - --include=*/ - --exclude=* - --prune-empty-dirs - name: Copy files from {{ ansible_user_dir }}/workspace/ on node synchronize: src: '{{ ansible_user_dir }}/workspace/' dest: '{{ zuul.executor.log_root }}' mode: pull copy_links: true verify_host: true rsync_opts: - --include=**/*nose_results.html - --include=*/ - --exclude=* - --prune-empty-dirs - name: Copy files from {{ ansible_user_dir }}/workspace/ on node synchronize: src: '{{ ansible_user_dir }}/workspace/' dest: '{{ zuul.executor.log_root }}' mode: pull copy_links: true verify_host: true rsync_opts: - --include=/.testrepository/tmp* - --include=*/ - --exclude=* - --prune-empty-dirs murano-dashboard-5.0.0/playbooks/legacy/murano-dashboard-sanity-check/run.yaml0000666000175100017510000000477713245511125027553 0ustar zuulzuul00000000000000- hosts: all name: Autoconverted job legacy-tempest-dsvm-murano-api from old job gate-tempest-dsvm-murano-api-ubuntu-xenial tasks: - name: Ensure legacy workspace directory file: path: '{{ ansible_user_dir }}/workspace' state: directory - shell: cmd: | set -e set -x cat > clonemap.yaml << EOF clonemap: - name: openstack-infra/devstack-gate dest: devstack-gate EOF /usr/zuul-env/bin/zuul-cloner -m clonemap.yaml --cache-dir /opt/git \ git://git.openstack.org \ openstack-infra/devstack-gate executable: /bin/bash chdir: '{{ ansible_user_dir }}/workspace' environment: '{{ zuul | zuul_legacy_vars }}' - shell: cmd: | set -e set -x export PYTHONUNBUFFERED=true export DEVSTACK_GATE_TEMPEST=0 export DEVSTACK_GATE_NEUTRON=1 export PROJECTS="openstack/heat $PROJECTS" export PROJECTS="openstack/python-heatclient $PROJECTS" export PROJECTS="openstack/murano $PROJECTS" export PROJECTS="openstack/murano-dashboard $PROJECTS" export PROJECTS="openstack/python-muranoclient $PROJECTS" export PROJECTS="openstack/horizon $PROJECTS" export ENABLED_SERVICES=horizon export PROJECTS="openstack/heat-dashboard $PROJECTS" export DEVSTACK_LOCAL_CONFIG="enable_plugin heat git://git.openstack.org/openstack/heat" export DEVSTACK_LOCAL_CONFIG+=$'\n'"enable_plugin murano git://git.openstack.org/openstack/murano" export DEVSTACK_LOCAL_CONFIG+=$'\n'"enable_plugin heat-dashboard git://git.openstack.org/openstack/heat-dashboard" export BRANCH_OVERRIDE=default if [ "$BRANCH_OVERRIDE" != "default" ] ; then export OVERRIDE_ZUUL_BRANCH=$BRANCH_OVERRIDE fi function pre_test_hook { cd /opt/stack/new/murano-dashboard/functional_tests ./pre_test_hook.sh } export -f pre_test_hook function post_test_hook { cd /opt/stack/new/murano-dashboard/functional_tests ./post_test_hook.sh } export -f post_test_hook cp devstack-gate/devstack-vm-gate-wrap.sh ./safe-devstack-vm-gate-wrap.sh ./safe-devstack-vm-gate-wrap.sh executable: /bin/bash chdir: '{{ ansible_user_dir }}/workspace' environment: '{{ zuul | zuul_legacy_vars }}' murano-dashboard-5.0.0/babel-djangojs.cfg0000666000175100017510000000110113245511125020303 0ustar zuulzuul00000000000000[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] murano-dashboard-5.0.0/LICENSE0000666000175100017510000002363713245511125016007 0ustar zuulzuul00000000000000 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. murano-dashboard-5.0.0/muranodashboard/0000775000175100017510000000000013245511556020146 5ustar zuulzuul00000000000000murano-dashboard-5.0.0/muranodashboard/api/0000775000175100017510000000000013245511556020717 5ustar zuulzuul00000000000000murano-dashboard-5.0.0/muranodashboard/api/packages.py0000666000175100017510000000770513245511125023052 0ustar zuulzuul00000000000000# 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) murano-dashboard-5.0.0/muranodashboard/api/__init__.py0000666000175100017510000001267213245511125023032 0ustar zuulzuul00000000000000# 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 '.format(endpoint)) return client.Client(1, endpoint=endpoint, token=token_id, insecure=insecure, artifacts_client=artifacts, tenant=request.user.tenant_id) murano-dashboard-5.0.0/muranodashboard/api/rest/0000775000175100017510000000000013245511556021674 5ustar zuulzuul00000000000000murano-dashboard-5.0.0/muranodashboard/api/rest/environments.py0000666000175100017510000001372013245511125024772 0ustar zuulzuul00000000000000# 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[^/]+)' \ r'/components/(?P[^/]+)/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[^/]+)/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) murano-dashboard-5.0.0/muranodashboard/api/rest/packages.py0000666000175100017510000000433613245511125024024 0ustar zuulzuul00000000000000# 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, } murano-dashboard-5.0.0/muranodashboard/api/rest/__init__.py0000666000175100017510000000120113245511125023771 0ustar zuulzuul00000000000000# 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 murano-dashboard-5.0.0/muranodashboard/conf/0000775000175100017510000000000013245511556021073 5ustar zuulzuul00000000000000murano-dashboard-5.0.0/muranodashboard/conf/murano_policy.json0000666000175100017510000000222513245511125024641 0ustar zuulzuul00000000000000{ "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" } murano-dashboard-5.0.0/muranodashboard/version.py0000666000175100017510000000135213245511125022200 0ustar zuulzuul00000000000000# Copyright 2012 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. from pbr import version version_info = version.VersionInfo('muranodashboard') __version__ = version_info.cached_version_string() murano-dashboard-5.0.0/muranodashboard/common/0000775000175100017510000000000013245511556021436 5ustar zuulzuul00000000000000murano-dashboard-5.0.0/muranodashboard/common/net.py0000666000175100017510000001060613245511125022573 0ustar zuulzuul00000000000000# 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, 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 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)) netname = _("%s: random subnet") % ( 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 murano-dashboard-5.0.0/muranodashboard/common/widgets.py0000666000175100017510000000600313245511125023447 0ustar zuulzuul00000000000000# 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 murano-dashboard-5.0.0/muranodashboard/common/__init__.py0000666000175100017510000000000013245511125023527 0ustar zuulzuul00000000000000murano-dashboard-5.0.0/muranodashboard/common/utils.py0000666000175100017510000000756713245511125023161 0ustar zuulzuul00000000000000# 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') murano-dashboard-5.0.0/muranodashboard/common/cache.py0000666000175100017510000000514313245511125023050 0ustar zuulzuul00000000000000# 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 murano-dashboard-5.0.0/muranodashboard/common/fields.py0000666000175100017510000000510013245511125023244 0ustar zuulzuul00000000000000# 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}, ) murano-dashboard-5.0.0/muranodashboard/views.py0000666000175100017510000000132313245511125021646 0ustar zuulzuul00000000000000# 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 horizon def get_user_home(user): return horizon.get_dashboard('app-catalog').get_absolute_url() murano-dashboard-5.0.0/muranodashboard/templates/0000775000175100017510000000000013245511556022144 5ustar zuulzuul00000000000000murano-dashboard-5.0.0/muranodashboard/templates/common/0000775000175100017510000000000013245511556023434 5ustar zuulzuul00000000000000murano-dashboard-5.0.0/muranodashboard/templates/common/tri_state_checkbox/0000775000175100017510000000000013245511556027300 5ustar zuulzuul00000000000000murano-dashboard-5.0.0/muranodashboard/templates/common/tri_state_checkbox/input.html0000666000175100017510000000073013245511125031317 0ustar zuulzuul00000000000000 murano-dashboard-5.0.0/muranodashboard/templates/common/tri_state_checkbox/base.html0000666000175100017510000000056213245511125031075 0ustar zuulzuul00000000000000
    {% for choice, label, value in values %}
  • {% with id=attrs.id|add:forloop.counter %} {% include 'common/tri_state_checkbox/input.html' %} {% endwith %}
  • {% endfor %}
murano-dashboard-5.0.0/muranodashboard/templates/common/_detail_header.html0000666000175100017510000000105413245511125027225 0ustar zuulzuul00000000000000{% if HORIZON_CONFIG.legacy_static_settings %} {% else %} {% include "horizon/common/_detail_header.html" %} {% endif %}murano-dashboard-5.0.0/muranodashboard/templates/common/_form_fields.html0000666000175100017510000000140013245511125026737 0ustar zuulzuul00000000000000{% load custom_filters %} {% for hidden in form.hidden_fields %} {{ hidden }} {% endfor %} {% if form.non_field_errors %}
{{ wizard.form.non_field_errors }}
{% endif %} {% for field in form.visible_fields %}
{% if field|is_checkbox %} {{ field }}{{ field.label_tag }} {% else %} {{ field.label_tag }} {% if field.errors %} {% for error in field.errors %} {{ error }} {% endfor %} {% endif %} {{ field.help_text }}
{{ field }}
{% endif %}
{% endfor %} murano-dashboard-5.0.0/muranodashboard/templates/categories/0000775000175100017510000000000013245511556024271 5ustar zuulzuul00000000000000murano-dashboard-5.0.0/muranodashboard/templates/categories/_packages.html0000666000175100017510000000013713245511125027067 0ustar zuulzuul00000000000000
    {% for name in category.packages %}
  • {{ name }}
  • {% endfor %}
murano-dashboard-5.0.0/muranodashboard/templates/categories/_add.html0000666000175100017510000000053013245511125026036 0ustar zuulzuul00000000000000{% extends 'horizon/common/_modal_form.html' %} {% load i18n %} {% block modal-body-right %}

{% trans 'Description' %}:

{% trans 'Add new category to the application catalog.' %}

{% trans 'Name' %}: {% trans "Provide desired name for a new category" %}

{% endblock %} murano-dashboard-5.0.0/muranodashboard/templates/categories/index.html0000666000175100017510000000024413245511125026260 0ustar zuulzuul00000000000000{% extends 'base.html' %} {% load i18n %} {% block title %}{% trans "Application Categories" %}{% endblock %} {% block main %} {{ table.render }} {% endblock %} murano-dashboard-5.0.0/muranodashboard/templates/categories/add.html0000666000175100017510000000025413245511125025702 0ustar zuulzuul00000000000000{% extends 'base.html' %} {% load i18n %} {% block title %}{% trans "Add Category" %}{% endblock %} {% block main %} {% include 'categories/_add.html' %} {% endblock %} murano-dashboard-5.0.0/muranodashboard/templates/environments/0000775000175100017510000000000013245511556024673 5ustar zuulzuul00000000000000murano-dashboard-5.0.0/muranodashboard/templates/environments/create.html0000666000175100017510000000027013245511125027015 0ustar zuulzuul00000000000000{% extends 'base.html' %} {% load i18n %} {% block title %}{% trans "Create Environment" %}{% endblock %} {% block main %} {% include 'environments/_create.html' %} {% endblock %} murano-dashboard-5.0.0/muranodashboard/templates/environments/index.html0000666000175100017510000000062013245511125026660 0ustar zuulzuul00000000000000{% extends 'base.html' %} {% load i18n %} {% load static %} {% load compress %} {% block title %}{% trans "Environments" %}{% endblock %} {% block main %} {{ table.render }} {% endblock %} {% block css %} {{ block.super }} {% compress css %} {% endcompress %} {% endblock %} murano-dashboard-5.0.0/muranodashboard/templates/environments/_create.html0000666000175100017510000000064113245511125027156 0ustar zuulzuul00000000000000{% extends 'horizon/common/_modal_form.html' %} {% load i18n %} {% block modal-body-right %}

{% trans "Description" %}:

{% trans "Environment Name" %}: {% trans "Choose a name for the environment" %}

{% trans "An environment is a collection of applications that are meant to operate under similar conditions." %}

{% endblock %} murano-dashboard-5.0.0/muranodashboard/templates/environments/_data_table.html0000666000175100017510000000122613245511125027773 0ustar zuulzuul00000000000000{% extends 'horizon/common/_data_table.html' %} {% load i18n %} {% block title %}{% trans "Environments" %}{% endblock %} {% block table_body %} {% for row in rows %} {{ row.render }} {% empty %}

{{ table.get_empty_message }}

{% endfor %} {% endblock table_body %} murano-dashboard-5.0.0/muranodashboard/templates/packages/0000775000175100017510000000000013245511556023722 5ustar zuulzuul00000000000000murano-dashboard-5.0.0/muranodashboard/templates/packages/import_bundle.html0000666000175100017510000000047213245511125027450 0ustar zuulzuul00000000000000{% extends 'base.html' %} {% load i18n %} {% block title %}{% trans "Import Bundle" %}{% endblock %} {% block page_header %} {% include 'horizon/common/_page_header.html' with title=_('Import Bundle') %} {% endblock page_header %} {% block main %} {% include 'packages/_import_bundle.html' %} {% endblock %} murano-dashboard-5.0.0/muranodashboard/templates/packages/_upload.html0000666000175100017510000000656513245511125026241 0ustar zuulzuul00000000000000{% extends "horizon/common/_modal_form.html" %} {% load i18n %} {% load static %} {% block form_id %}upload_package{% endblock %} {% block form_action %}{% url 'horizon:app-catalog:packages:upload' %}{% endblock %} {% block form_attrs %}enctype="multipart/form-data"{% endblock %} {% block modal_id %}upload_package_modal{% endblock %} {% block modal-header %}{% trans 'Import Package' %}{% endblock %} {% block modal-body %}
{{ wizard.management_form }} {% if wizard.form.forms %} {{ wizard.form.management_form }} {% for form in wizard.form.forms %} {{ form }} {% endfor %} {% else %}
{% with form=wizard.form %} {% include "horizon/common/_form_fields.html" %} {% endwith %}
{% endif %}
{% if wizard.steps.prev == 'upload' %} {% include 'packages/_package_params.html' %} {% elif wizard.steps.prev == 'modify' %}

{% trans "Description" %}:

{% trans "Categories" %} {% trans "Select one or more categories for a package." %}

{% trans "Specifying a category helps to filter applications in the catalog" %}

{% else %}

{% trans "Description" %}:

{% trans "Choose a Zip archive to upload into the catalog." %}

{% trans "Packages should contain:" %}
* {% trans "Manifest file" context "Package requirements" %}
* {% trans "UI definition folder" context "Package requirements" %}
* {% trans "Classes definition folder" context "Package requirements" %}
* {% trans "Execution plans folder" context "Package requirements" %}

{% trans "Description" %}:

{% trans "Package Name" %}: {% trans "Fully qualified package name." %}

{% trans "Package Version" %}: {% trans "Version of the package (optional)." %}

{% blocktrans trimmed %}The package is going to be imported from {{murano_repo_url}} repository.{% endblocktrans %}

{% trans "Description" %}:

{% trans "Package URL" %}: {% trans "HTTP/HTTPS URL of the package file." %}

{% trans "Note" %}: {% trans "If the package depends upon other packages and/or requires specific glance images, those are going to be installed with it from murano repository." %}

{% endif %}
{% endblock %} {% block modal-footer %} {% trans 'Cancel' %} {% if wizard.steps.next %} {% else %} {% endif %} {% endblock %} murano-dashboard-5.0.0/muranodashboard/templates/packages/_import_bundle.html0000666000175100017510000000425413245511125027611 0ustar zuulzuul00000000000000{% extends "horizon/common/_modal_form.html" %} {% load i18n %} {% load static %} {% block form_id %}import_bundle{% endblock %} {% block form_action %}{% url 'horizon:app-catalog:packages:import_bundle' %}{% endblock %} {% block modal_id %}upload_bundle_modal{% endblock %} {% block modal-header %}{% trans 'Import Bundle' %}{% endblock %} {% block modal-body %}
{{ wizard.management_form }} {% if wizard.form.forms %} {{ wizard.form.management_form }} {% for form in wizard.form.forms %} {{ form }} {% endfor %} {% else %}
{% with form=wizard.form %} {% include "horizon/common/_form_fields.html" %} {% endwith %}
{% endif %}

{% trans "Description" %}:

{% trans "Bundle Name" %}: {% trans "Bundle's full name."%}

{% blocktrans trimmed %}The bundle is going to be installed from {{murano_repo_url}} repository.{% endblocktrans %}

{% trans "Description" %}:

{% trans "Bundle URL" %}: {% trans "HTTP/HTTPS URL of the bundle file."%}

{% trans "Note" %}: {% trans "You'll have to configure each package installed from this bundle separately." %}
{% trans "If packages depend upon other packages and/or require specific glance images, those are going to be installed with them from murano repository." %}

{% endblock %} {% block modal-footer %} {% trans 'Cancel' %} {% if wizard.steps.next %} {% else %} {% endif %} {% endblock %} murano-dashboard-5.0.0/muranodashboard/templates/packages/_modify_package.html0000666000175100017510000000145213245511125027705 0ustar zuulzuul00000000000000{% extends 'horizon/common/_modal_form.html' %} {% load i18n %} {% block form_id %}{% endblock %} {% block form_action %}{% url 'horizon:app-catalog:packages:modify' app_id %}{% endblock %} {% block modal-header %}{% trans "Modify Package" %}{% endblock %} {% block modal_id %}modify_package_modal{% endblock %} {% block modal-body-right %} {% include 'packages/_package_params.html' %} {% if type != 'Library' %}

{% trans "Categories" %}. {% trans "Select one or more categories for a package." %}

{% endif %} {% endblock %} {% block modal-footer %} {% trans "Cancel" %} {% endblock %} murano-dashboard-5.0.0/muranodashboard/templates/packages/detail.html0000666000175100017510000000051713245511125026047 0ustar zuulzuul00000000000000{% extends 'base.html' %} {% load i18n %} {% block title %}{% trans "Package Details" %}{% endblock %} {% block page_header %} {% include "common/_detail_header.html" %} {% endblock %} {% block main %}
{% include "packages/_detail.html" %}
{% endblock %} murano-dashboard-5.0.0/muranodashboard/templates/packages/modify_package.html0000666000175100017510000000047513245511125027552 0ustar zuulzuul00000000000000{% extends 'base.html' %} {% load i18n %} {% block title %}{% trans "Modify Package" %}{% endblock %} {% block page_header %} {% include 'horizon/common/_page_header.html' with title=_('Modify Package') %} {% endblock page_header %} {% block main %} {% include 'packages/_modify_package.html' %} {% endblock %} murano-dashboard-5.0.0/muranodashboard/templates/packages/_package_params.html0000666000175100017510000000122513245511125027677 0ustar zuulzuul00000000000000{% load i18n %}

{% trans "Description" %}:

{% trans "Name" %}: {% trans "Set up for identifying a package." %}

{% trans "Tags" %}: {% trans "Used for identifying and filtering packages." %}

{% trans "Public" %}: {% trans "Defines whether or not a package can be used by other tenants. (Applies to package dependencies)" %}

{% trans "Active" %}: {% trans "Allows to hide a package from the catalog. (Applies to package dependencies)" %}

{% trans "Description" %}: {% trans "Allows adding additional information about a package." %}

murano-dashboard-5.0.0/muranodashboard/templates/packages/index.html0000666000175100017510000000055613245511125025717 0ustar zuulzuul00000000000000{% extends 'base.html' %} {% load i18n %} {% load static %} {% load compress %} {% block title %}{% trans "Packages" %}{% endblock %} {% block css %} {{ block.super }} {% compress css %} {% endcompress %} {% endblock %} {% block main %} {{ table.render }} {% endblock %} murano-dashboard-5.0.0/muranodashboard/templates/packages/_detail.html0000666000175100017510000000174113245511125026206 0ustar zuulzuul00000000000000{% load i18n %}
{% trans "Name" %}
{{ app.name }}
{% trans "FQN" %}
{{ app.fully_qualified_name }}
{% trans "Type" %}
{{ app.type }}
{% trans "ID" %}
{{ app.id }}
{% trans "Package Tags" %}
{{ app.tags|join:", " }}
{% trans "Enabled" %}
{{ app.enabled|yesno|capfirst }}
{% trans "Public" %}
{{ app.is_public|yesno|capfirst }}
{% trans "Categories" %}
{{ app.categories|join:", "|default:_("None") }}
{% trans "Version" %}
{{ app.version|default:_("-") }}
{% trans "Author" %}
{{ app.author }}
{% trans "Created" %}
{{ app.created|parse_isotime }}
{% trans "Description" %}
{{ app.description|default:_("None") }}
murano-dashboard-5.0.0/muranodashboard/templates/packages/upload.html0000666000175100017510000000046513245511125026073 0ustar zuulzuul00000000000000{% extends 'base.html' %} {% load i18n %} {% block title %}{% trans "Import Package" %}{% endblock %} {% block page_header %} {% include 'horizon/common/_page_header.html' with title=_('Import Package') %} {% endblock page_header %} {% block main %} {% include 'packages/_upload.html' %} {% endblock %} murano-dashboard-5.0.0/muranodashboard/templates/services/0000775000175100017510000000000013245511556023767 5ustar zuulzuul00000000000000murano-dashboard-5.0.0/muranodashboard/templates/services/_overview.html0000666000175100017510000000320013245511125026647 0ustar zuulzuul00000000000000{% load i18n %}

{% trans "Component Details" %}

{% trans "Info" %}

{% for key, value in service.items %} {% if key == 'Instance' %}
{% trans "Instance name" %}
{{ value.name }}

{% elif key == 'Instances' %} {% for instance in value %}
{% blocktrans trimmed %}Instance{{forloop.counter}} name{% endblocktrans %}
{{ instance.name }}

{% endfor %} {% elif key == 'Stack'%}
{% trans "Heat Orchestration stack name" %}
{{ value.name }}

{% elif key == 'Stacks'%} {% for stack in value %}
{% blocktrans trimmed %}Heat Orchestration stack{{forloop.counter}} name{% endblocktrans %}
{{ stack.name }}

{% endfor %} {% else %}
{{ key }}
{{ value }}

{% endif %} {% endfor %}
murano-dashboard-5.0.0/muranodashboard/templates/services/_wizard_create.html0000666000175100017510000003025413245511125027635 0ustar zuulzuul00000000000000{% extends "horizon/common/_modal_form.html" %} {% load i18n horizon humanize bootstrap %} {% block form_action %} {% url 'horizon:app-catalog:catalog:add' app_id environment_id do_redirect drop_wm_form %} {% endblock %} {% block form_id %}form_{{ app_id }}{% endblock %} {% block modal_id %}modal_{{ app_id }}{% endblock %} {% block modal-header %} {% trans "Configure Application" %}: {{ service_name }} {% endblock %} {% block steps-list %}
    {% with steps=wizard.steps %} {% for step in steps.all %} {% with counter0=forloop.counter0 %}
  • {{ step }}
  • {% endwith %} {% endfor %} {% endwith %}
{% endblock %} {% block modal-body %} {% for ext_description in extended_descriptions %}

{{ ext_description }}

{% endfor %}
{{ wizard.management_form }} {% if wizard.form.forms %} {{ wizard.form.management_form }} {% for form in wizard.form.forms %} {{ form }} {% endfor %} {% else %}
{% with form=wizard.form %} {% include "horizon/common/_form_fields.html" %} {% endwith %}
{% endif %}
{% for name, title, description in field_descriptions %}

{% if title %} {{ title }}: {% endif %}

{{ description|linebreaksbr }}

{% endfor %}
{% if usages %} {% endif %} {% endblock %} {% block modal-footer %} {{ wizard.form.media }} {% if wizard.steps.next %} {% trans "Next" as next %} {% else %} {% trans "Create" as next %} {% endif %} {% if wizard.steps.index > 0 %} {% else %} {% endif %} {% endblock %} murano-dashboard-5.0.0/muranodashboard/templates/services/_service_list.html0000666000175100017510000000034213245511125027500 0ustar zuulzuul00000000000000{% load i18n %} {% block main %}
{% include "services/_data_table.html" %}
{{ table.render }}
{% endblock %} murano-dashboard-5.0.0/muranodashboard/templates/services/_network_info.html0000666000175100017510000000012413245511125027507 0ustar zuulzuul00000000000000

Name: {{ name }}

murano-dashboard-5.0.0/muranodashboard/templates/services/_environment_info.html0000666000175100017510000000007413245511125030366 0ustar zuulzuul00000000000000

Environment: {{ name }}

Status: {{ status }}

murano-dashboard-5.0.0/muranodashboard/templates/services/_detail_topology.html0000666000175100017510000000051113245511125030201 0ustar zuulzuul00000000000000{% load i18n sizeformat %} {% load static %}
murano-dashboard-5.0.0/muranodashboard/templates/services/_unit_info.html0000666000175100017510000000044413245511125027002 0ustar zuulzuul00000000000000 {% if data.name %}

Name: {{ data.name }}

{% endif %} {% for key, value in data.items %} {% if key != "name" and not "password" in key|lower %}

{{ key|title }}: {{ value }}

{% endif %} {% endfor %} murano-dashboard-5.0.0/muranodashboard/templates/services/_logs.html0000666000175100017510000000022713245511125025753 0ustar zuulzuul00000000000000{% load i18n %}

{% trans "Component Logs" %}

{{ reports | urlize }}
murano-dashboard-5.0.0/muranodashboard/templates/services/wizard_create.html0000666000175100017510000000047613245511125027501 0ustar zuulzuul00000000000000{% extends 'base.html' %} {% load i18n %} {% block title %}{% trans "Add Application" %}{% endblock %} {% block page_header %} {% include "horizon/common/_page_header.html" with title=_("Add Application") %} {% endblock page_header %} {% block main %} {% include 'services/_wizard_create.html' %} {% endblock %} murano-dashboard-5.0.0/muranodashboard/templates/services/app_tile_small.html0000666000175100017510000000064313245511125027637 0ustar zuulzuul00000000000000{% load i18n %}
{{ app.name }}
murano-dashboard-5.0.0/muranodashboard/templates/services/index.html0000666000175100017510000000365513245511125025767 0ustar zuulzuul00000000000000{% extends 'base.html' %} {% load i18n %} {% load static %} {% load compress %} {% load horizon %} {% load custom_filters %} {% block title %}{% trans "Components" %}{% endblock %} {% block page_header %} {% include "common/_detail_header.html" %} {% endblock page_header %} {% block main %}
{{ tab_group.render }}
{% endblock %} {% block css %} {{ block.super }} {% compress css %} {% endcompress %} {% endblock %} {% block js %} {{ block.super }} {% endblock %} murano-dashboard-5.0.0/muranodashboard/templates/services/_application_info.html0000666000175100017510000000020613245511125030322 0ustar zuulzuul00000000000000

Name: {{ name }}

Type: {{ type }}

Status: {{ status }}

murano-dashboard-5.0.0/muranodashboard/templates/services/_data_table.html0000666000175100017510000001107513245511125027072 0ustar zuulzuul00000000000000{% load i18n %} {% load custom_filters %} {% load jsonify %} {% block table_caption %} {% with apps=table.get_apps_list %} {% if apps %}

{% trans "Application Components" %}

{% trans 'App category' %}

{% else %}

{% with display_repo_url=table.get_repo_url pkg_def_url=table.get_pkg_def_url %} {% trans "There are no applications in the catalog. You can import apps from" %} {{ display_repo_url }}.

{% blocktrans trimmed %}Go to Packages , click 'Import Package' and select Repository as Package Source. {% endblocktrans %} {% endwith %}

{% endif %}

{% trans "There are no applications matching your criteria." %}

{% endwith %} {% endblock table_caption %} {% block table_body %} {% if table.actions_allowed %}

{% trans 'Drop Components here' %}

{% endif %} {% endblock table_body %} murano-dashboard-5.0.0/muranodashboard/templates/services/details.html0000666000175100017510000000104113245511125026270 0ustar zuulzuul00000000000000{% extends 'base.html' %} {% load i18n sizeformat %} {% load static %} {% load compress %} {% block title %}{% trans "Component Details" %}{% endblock %} {% block css %} {{ block.super }} {% compress css %} {% endcompress %} {% endblock %} {% block page_header %} {% include "common/_detail_header.html" %} {% endblock page_header %} {% block main %}
{{ tab_group.render }}
{% endblock %} murano-dashboard-5.0.0/muranodashboard/templates/deployments/0000775000175100017510000000000013245511556024507 5ustar zuulzuul00000000000000murano-dashboard-5.0.0/muranodashboard/templates/deployments/reports.html0000666000175100017510000000054513245511125027071 0ustar zuulzuul00000000000000{% extends 'base.html' %} {% load i18n sizeformat %} {% block title %}{% trans "Deployment Details" %}{% endblock %} {% block page_header %} {% include "common/_detail_header.html" %} {% endblock page_header %} {% block main %}
{{ tab_group.render }}
{% endblock %} murano-dashboard-5.0.0/muranodashboard/templates/deployments/_logs.html0000666000175100017510000000053613245511125026476 0ustar zuulzuul00000000000000{% load i18n static %}

{% trans "Deployment Logs" %}

{% for report in reports %}
{{report.created}} — {{report.text | linebreaksbr | urlize}}
{% endfor %}
murano-dashboard-5.0.0/muranodashboard/templates/deployments/index.html0000666000175100017510000000025313245511125026476 0ustar zuulzuul00000000000000{% extends 'base.html' %} {% load i18n %} {% block title %}{% trans "Environment Deployment History" %}{% endblock %} {% block main %} {{ table.render }} {% endblock %}murano-dashboard-5.0.0/muranodashboard/templates/deployments/_cell_reports.html0000666000175100017510000000103013245511125030215 0ustar zuulzuul00000000000000{% load i18n %} {% load static %} {% load compress %} {% block css %} {% compress css %} {% endcompress %} {% endblock %} {% block main %} {% for report in reports %} {% endfor %}
{{report.created | parse_isotime}} — {{report.text | linebreaksbr | urlize}}
{% endblock %}murano-dashboard-5.0.0/muranodashboard/templates/deployments/_cell_services.html0000666000175100017510000000075513245511125030357 0ustar zuulzuul00000000000000{% load i18n %} {% load static %} {% load compress %} {% block css %} {% compress css %} {% endcompress %} {% endblock %} {% block main %} {% for name, type in services.items %} {% endfor %}
{{name}} — {{type | linebreaksbr}}
{% endblock %}murano-dashboard-5.0.0/muranodashboard/templates/images/0000775000175100017510000000000013245511556023411 5ustar zuulzuul00000000000000murano-dashboard-5.0.0/muranodashboard/templates/images/_mark.html0000666000175100017510000000146513245511125025370 0ustar zuulzuul00000000000000{% extends 'horizon/common/_modal_form.html' %} {% load i18n %} {% block modal-body-right %}

{% trans "Description" %}:

{% trans "Mark an image with Murano specific metadata to be added to the selected image." %}

{% trans "Image" %}: {% trans "Select an image registered in Glance Image Services." %}

{% trans "Image Title" %}: {% trans "Create a title for an image." %}

{% trans "Image Type" %}: {% trans "Select an image type supported by Murano. Choose 'Custom type' to enter type manually." %}

{% trans "Custom Type" %}: {% trans "Enter an image type supported by Murano." %}

{% endblock %} murano-dashboard-5.0.0/muranodashboard/templates/images/index.html0000666000175100017510000000023313245511125025376 0ustar zuulzuul00000000000000{% extends 'base.html' %} {% load i18n %} {% block title %}{% trans "Marked Images" %}{% endblock %} {% block main %} {{ table.render }} {% endblock %} murano-dashboard-5.0.0/muranodashboard/templates/images/mark.html0000666000175100017510000000024713245511125025226 0ustar zuulzuul00000000000000{% extends 'base.html' %} {% load i18n %} {% block title %}{% trans "Mark Image" %}{% endblock %} {% block main %} {% include 'images/_mark.html' %} {% endblock %} murano-dashboard-5.0.0/muranodashboard/templates/catalog/0000775000175100017510000000000013245511556023556 5ustar zuulzuul00000000000000murano-dashboard-5.0.0/muranodashboard/templates/catalog/_overview.html0000666000175100017510000000305713245511125026450 0ustar zuulzuul00000000000000{% load i18n %}
{% if app.supplier.Name %}
{% if app.supplier.Logo %}
{% endif %}
    {% if app.supplier.Name %}
  • {{ app.supplier.Name }}
  • {% endif %} {% if app.supplier.CompanyUrl.Link %} {% if app.supplier.CompanyUrl.Text %}
  • {{ app.supplier.CompanyUrl.Text }}
  • {% else %}
  • {{ app.supplier.CompanyUrl.Link }}
  • {% endif %} {% endif %} {% if app.supplier.Summary %}
  • {{ app.supplier.Summary }}
  • {% endif %} {% if app.supplier.Description %}
  • {{ app.supplier.Description }}
  • {% endif %}
{% endif %}
murano-dashboard-5.0.0/muranodashboard/templates/catalog/quick_deploy.html0000666000175100017510000000027413245511125027131 0ustar zuulzuul00000000000000{% load i18n %} {% trans "Quick Deploy" as label %} {{ label }} murano-dashboard-5.0.0/muranodashboard/templates/catalog/categories.html0000666000175100017510000000153713245511125026571 0ustar zuulzuul00000000000000{% load custom_filters %} {% load i18n %}

{% trans "Categories" %}

{% if categories|slice:"1:" %}
    {% for category in categories|firsthalf %}
  • {{ category }}
  • {% endfor %}
    {% for category in categories|lasthalf %}
  • {{ category }}
  • {% endfor %}
{% else %} {{ categories.0 }} {% endif %}
murano-dashboard-5.0.0/muranodashboard/templates/catalog/env_switcher.html0000666000175100017510000000255713245511125027147 0ustar zuulzuul00000000000000{% load i18n %}

{% trans "Environment" %}:

murano-dashboard-5.0.0/muranodashboard/templates/catalog/add_app.html0000666000175100017510000000065713245511125026036 0ustar zuulzuul00000000000000{% load i18n %} {% if environment %} {% trans "Add to Env" %} {% else %} {% trans "Create Env" %} {% endif %} murano-dashboard-5.0.0/muranodashboard/templates/catalog/_app_license.html0000666000175100017510000000037313245511125027062 0ustar zuulzuul00000000000000{% load i18n %}

{% if application.license %} {{ application.license|linebreaksbr }} {% else %} {% trans 'No license' %} {% endif %}

murano-dashboard-5.0.0/muranodashboard/templates/catalog/index.html0000666000175100017510000001445213245511125025553 0ustar zuulzuul00000000000000{% extends 'base.html' %} {% load i18n %} {% load static %} {% load compress %} {% block title %}{% trans "Browse" %}{% endblock %} {% block css %} {{ block.super }} {% compress css %} {% if HORIZON_CONFIG.legacy_static_settings %} {% else %} {% endif %} {% endcompress %} {% endblock %} {% block page_header %} {% include "horizon/common/_page_header.html" with title=_("Browse") %} {% endblock page_header %} {% block main %}

{% trans 'Recent Activity' %}

{% if latest_list|length > 0 %}
{% for app in latest_list %} {% include 'catalog/app_tile.html' %} {% endfor %}
{% else %}

{% trans "No recent activity to report at this time." %}

{% endif %}
{% include 'catalog/env_switcher.html' %}
{% if object_list|length > 0 %}
{% for app in object_list %} {% include 'catalog/app_tile.html' %} {% endfor %}
{% if view.has_prev_page %} {% trans "Previous Page" %} {% endif %} {% if view.has_next_page %} {% trans "Next Page" %} {% endif %}
{% else %}

{% if no_apps %} {% blocktrans trimmed %}There are no applications in the catalog. You can import apps from {{ display_repo_url }}.{% endblocktrans %}

{% blocktrans trimmed %}Go to Packages, click 'Import Package' and select Repository as Package Source. {% endblocktrans %} {% else %} {% trans "There are no applications matching your criteria." %} {% endif %}

{% endif %}
{% endblock %} murano-dashboard-5.0.0/muranodashboard/templates/catalog/app_tile.html0000666000175100017510000000327013245511125026235 0ustar zuulzuul00000000000000{% load i18n %} {% load static %}

{{ app.name }}

{{ app.description|striptags|truncatechars:130 }}

{% with class='btn btn-default btn-sm ajax-modal' %} {% include 'catalog/add_app.html' %} {% endwith %} {% with class='btn btn-default btn-sm ajax-modal' %} {% include 'catalog/quick_deploy.html' %} {% endwith %}
{% if app.owner_id != tenant_id %} {% endif %}
murano-dashboard-5.0.0/muranodashboard/templates/catalog/_app_requirements.html0000666000175100017510000000041713245511125030162 0ustar zuulzuul00000000000000{% load i18n %}
{% if application.requirements %}
    {{ application.requirements|unordered_list }}
{% else %}

{% trans 'No requirements' %}

{% endif %}
murano-dashboard-5.0.0/muranodashboard/templates/catalog/app_details.html0000666000175100017510000000262413245511125026727 0ustar zuulzuul00000000000000{% extends 'base.html' %} {% load i18n sizeformat %} {% load static %} {% load compress %} {% block title %}{% trans "Application Details" %}: {{ app.name }}{% endblock %} {% block css %} {{ block.super }} {% compress css %} {% if HORIZON_CONFIG.legacy_static_settings %} {% else %} {% endif %} {% endcompress %} {% endblock %} {% block page_header %} {% include "common/_detail_header.html" %} {% endblock %} {% block main %}
{% with class='btn btn-default btn-sm btn-add ajax-modal' %} {% include 'catalog/add_app.html' %} {% endwith %} {% with class='btn btn-default btn-sm btn-add ajax-modal' %} {% include 'catalog/quick_deploy.html' %} {% endwith %}
{{ tab_group.render }}
{% endblock %} murano-dashboard-5.0.0/muranodashboard/local/0000775000175100017510000000000013245511556021240 5ustar zuulzuul00000000000000murano-dashboard-5.0.0/muranodashboard/local/enabled/0000775000175100017510000000000013245511556022632 5ustar zuulzuul00000000000000murano-dashboard-5.0.0/muranodashboard/local/enabled/_50_dashboard_catalog.py0000666000175100017510000000027413245511125027265 0ustar zuulzuul00000000000000# 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 murano-dashboard-5.0.0/muranodashboard/local/enabled/_80_panel_group_applications.py0000666000175100017510000000056213245511125030730 0ustar zuulzuul00000000000000from 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' murano-dashboard-5.0.0/muranodashboard/local/enabled/_73_panel_murano_categories.py0000666000175100017510000000060613245511125030535 0ustar zuulzuul00000000000000# 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' murano-dashboard-5.0.0/muranodashboard/local/enabled/_70_panel_group_manage.py0000666000175100017510000000054613245511125027473 0ustar zuulzuul00000000000000from 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' murano-dashboard-5.0.0/muranodashboard/local/enabled/_81_panel_applications_environments.py0000666000175100017510000000062213245511125032321 0ustar zuulzuul00000000000000# 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' murano-dashboard-5.0.0/muranodashboard/local/enabled/_63_panel_murano_catalog.py0000666000175100017510000000060013245511125030013 0ustar zuulzuul00000000000000# 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' murano-dashboard-5.0.0/muranodashboard/local/enabled/_60_panel_group_browse.py0000666000175100017510000000054613245511125027543 0ustar zuulzuul00000000000000from 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' murano-dashboard-5.0.0/muranodashboard/local/enabled/_72_panel_murano_images.py0000666000175100017510000000057213245511125027656 0ustar zuulzuul00000000000000# 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' murano-dashboard-5.0.0/muranodashboard/local/enabled/_51_muranodashboard.py0000666000175100017510000000310713245511125027014 0ustar zuulzuul00000000000000# 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 murano-dashboard-5.0.0/muranodashboard/local/enabled/_71_panel_murano_packages.py0000666000175100017510000000061213245511125030161 0ustar zuulzuul00000000000000# 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' murano-dashboard-5.0.0/muranodashboard/local/local_settings.d/0000775000175100017510000000000013245511556024474 5ustar zuulzuul00000000000000murano-dashboard-5.0.0/muranodashboard/local/local_settings.d/_50_murano.py0000666000175100017510000000406613245511125027012 0ustar zuulzuul00000000000000# 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" # Filter the list of Murano images displayed to be only those owned by this # project ID # MURANO_IMAGE_FILTER_PROJECT_ID = # 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',}) # Applications using the encryptData/decryptData yaql functions will require # the below to be configured #KEY_MANAGER = { # 'auth_url': 'http://192.168.5.254:5000/v3', # 'username': 'admin', # 'user_domain_name': 'default', # 'password': 'password', # 'project_name': 'admin', # 'project_domain_name': 'default' #} murano-dashboard-5.0.0/muranodashboard/local/__init__.py0000666000175100017510000000000013245511125023331 0ustar zuulzuul00000000000000murano-dashboard-5.0.0/muranodashboard/categories/0000775000175100017510000000000013245511556022273 5ustar zuulzuul00000000000000murano-dashboard-5.0.0/muranodashboard/categories/views.py0000666000175100017510000000640313245511125023777 0ustar zuulzuul00000000000000# 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') murano-dashboard-5.0.0/muranodashboard/categories/urls.py0000666000175100017510000000162713245511125023632 0ustar zuulzuul00000000000000# 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'), ] murano-dashboard-5.0.0/muranodashboard/categories/panel.py0000666000175100017510000000146413245511125023743 0ustar zuulzuul00000000000000# 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"),) murano-dashboard-5.0.0/muranodashboard/categories/__init__.py0000666000175100017510000000000013245511125024364 0ustar zuulzuul00000000000000murano-dashboard-5.0.0/muranodashboard/categories/forms.py0000666000175100017510000000252113245511125023765 0ustar zuulzuul00000000000000# 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 murano-dashboard-5.0.0/muranodashboard/categories/tables.py0000666000175100017510000000572213245511125024117 0ustar zuulzuul00000000000000# 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 murano-dashboard-5.0.0/muranodashboard/middleware.py0000666000175100017510000000207713245511125022635 0ustar zuulzuul00000000000000# 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) murano-dashboard-5.0.0/muranodashboard/exceptions.py0000666000175100017510000000170713245511125022700 0ustar zuulzuul00000000000000# 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,) murano-dashboard-5.0.0/muranodashboard/static/0000775000175100017510000000000013245511556021435 5ustar zuulzuul00000000000000murano-dashboard-5.0.0/muranodashboard/static/muranodashboard/0000775000175100017510000000000013245511556024606 5ustar zuulzuul00000000000000murano-dashboard-5.0.0/muranodashboard/static/muranodashboard/js/0000775000175100017510000000000013245511556025222 5ustar zuulzuul00000000000000murano-dashboard-5.0.0/muranodashboard/static/muranodashboard/js/triStateCheckbox.js0000666000175100017510000000254313245511125031024 0ustar zuulzuul00000000000000$(function() { "use strict"; //Updates value of hidden input based on state of visible input function updateValue ($beautyInput, $valueInput) { var value; if ($beautyInput.prop('indeterminate')) { value = 'None'; } else if ($beautyInput.prop('checked')) { value = 'True'; } else { value = 'False'; } $valueInput.val($valueInput.val().split('=')[0] + '=' + value); } function makeUpdater(beautyInput, valueInput) { return function() { updateValue(beautyInput, valueInput); }; } var i, len, $beautyInput, $valueInput, updater, value, $inputs; $inputs = $('[data-tri-state-checkbox=]'); for (i = 0, len = $inputs.length; i < len; i++) { //Subscribe hidden input to updates of visible input $valueInput = $inputs.eq(i); $beautyInput = $valueInput.prev(); updater = makeUpdater($beautyInput, $valueInput); $beautyInput.change(updater); //Set initial state of visible input value = $valueInput.val().split('=')[1]; if (value === 'True') { $beautyInput.prop('checked', true); $beautyInput.prop('indeterminate', false); } else if (value === 'False') { $beautyInput.prop('checked', false); $beautyInput.prop('indeterminate', false); } else { $beautyInput.prop('checked', false); $beautyInput.prop('indeterminate', true); } } }); murano-dashboard-5.0.0/muranodashboard/static/muranodashboard/js/more-less.js0000666000175100017510000000233413245511125027462 0ustar zuulzuul00000000000000$(function() { "use strict"; horizon.modals.addModalInitFunction(muranoMoreLess); function muranoMoreLess(modal) { var showChar = 150; var ellipsestext = "..."; var moretext = gettext("Show more"); var lesstext = gettext("Show less"); $(modal).find('.more_dynamicui_description').each(function() { var content = $.trim($(this).html()); if (content.length > showChar) { var c = content.substr(0, showChar); var h = content.substr(showChar, content.length - showChar); var html = c + '' + ellipsestext + ' ' + h + '  ' + moretext + ''; $(this).html(html); } }); $(modal).find(".more_link_dynamicui").click(function() { if ($(this).hasClass("less_dynamicui")) { $(this).removeClass("less_dynamicui"); $(this).html(moretext); } else { $(this).addClass("less_dynamicui"); $(this).html(lesstext); } $(this).parent().prev().toggle(); $(this).prev().toggle(); return false; }); } }); murano-dashboard-5.0.0/muranodashboard/static/muranodashboard/js/load-modals.js0000666000175100017510000000410013245511125027741 0ustar zuulzuul00000000000000/* 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. */ $(function() { "use strict"; horizon.modals.loadModal = function (url, updateFieldId) { // If there's an existing modal request open, cancel it out. if (horizon.modals.request && typeof horizon.modals.request.abort !== "undefined") { horizon.modals.request.abort(); } horizon.modals.request = $.ajax(url, { beforeSend: function () { horizon.modals.modal_spinner(gettext("Loading")); }, complete: function () { // Clear the global storage; horizon.modals.request = null; horizon.modals.spinner.modal('hide'); }, error: function(jqXHR) { if (jqXHR.status === 401) { var redirUrl = jqXHR.getResponseHeader("X-Horizon-Location"); if (redirUrl) { location.href = redirUrl; } else { location.reload(true); } } else { if (!horizon.ajax.get_messages(jqXHR)) { // Generic error handler. Really generic. horizon.alert("danger", gettext("An error occurred. Please try again later.")); } } }, success: function (data, textStatus, jqXHR) { var formUpdateFieldId = updateFieldId; var modal, form; modal = horizon.modals.success(data, textStatus, jqXHR); if (formUpdateFieldId) { form = modal.find("form"); if (form.length) { form.attr("data-add-to-field", formUpdateFieldId); } } } }); }; }); murano-dashboard-5.0.0/muranodashboard/static/muranodashboard/js/environments-in-place.js0000666000175100017510000000715613245511125032000 0ustar zuulzuul00000000000000/* 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. */ $(function() { "use strict"; $('table#environments .add_env a').on('click', createEnv); $('table#environments .table_actions a.add_env').on('click', createEnv); function createEnv(ev) { function showSpinner() { horizon.modals.modal_spinner(gettext("Working")); } function hideSpinner() { horizon.modals.spinner.modal('hide'); } var $tbody = $('table tbody'); var $thead = $('table thead'); var CREATE_URL = $(this).attr('href'); $.ajax({ type: 'GET', url: CREATE_URL, async: false, beforeSend: showSpinner, complete: hideSpinner, success: drawWorkflowInline }); function drawWorkflowInline(data, validationFailed) { var $form = $(data).find('form'); var $name = $form.find('div.form-group'); $name.addClass("col-md-6"); if (validationFailed) { $thead.find('tr.new_env').remove(); } var $newEnvTr = $('' + '' + '' + gettext("New") + '' + '' + '
' + '' + '' + '
'); $name.appendTo($newEnvTr.find('td#input_create_env')); var $emptyRow = $tbody.find('tr.empty'); $emptyRow.hide(); $newEnvTr.appendTo($thead); $name.find('input#id_name').focus(); $('button#cancel_create_env').on('click', function(ev) { $newEnvTr.remove(); $emptyRow.show(); ev.preventDefault(); }); $('button#confirm_create_env').on('click', function(ev) { // putting name group back to detached form to serialize it $name.appendTo($form); $.ajax({ type: 'POST', url: CREATE_URL, async: false, data: $form.serialize(), beforeSend: showSpinner, error: function() { $newEnvTr.remove(); hideSpinner(); horizon.alert('error', gettext("There was an error submitting the form. Please try again.")); }, success: function(data, status, xhr) { if (data === '') { // environment was created successfully var redirUrl = xhr.getResponseHeader('X-Horizon-Location'); $newEnvTr.remove(); window.location.replace(redirUrl); } else { // environment wasn't created because data is invalid // FIXME: recursion is used, so in case user repeatedly enters // invalid Env name (a LOT of attempts), maximum stack depth // could be exceeded hideSpinner(); drawWorkflowInline(data, true); } } }); ev.preventDefault(); }); } ev.preventDefault(); } }); murano-dashboard-5.0.0/muranodashboard/static/muranodashboard/js/support_placeholder.js0000666000175100017510000000363513245511125031637 0ustar zuulzuul00000000000000/* 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. */ /* The fix was borrowed from http://www.hagenburger.net/BLOG/HTML5-Input-Placeholder-Fix-With-jQuery.html */ $(function() { "use strict"; var getIeVersion = function() { if (/MSIE (\d+\.\d+);/.test(navigator.userAgent)) { //test for MSIE x.x; var ieVersion = Number(RegExp.$1); // capture x.x portion and store as a number return ieVersion; } }; if (getIeVersion() < 10) { // placeholder attribute for optional drop-down fields doesn't work // very well together with this fix - so remove placeholder // attribute for every drop-down list. $('select').filter('[placeholder]').removeAttr('placeholder'); $('[placeholder]').focus(function() { var input = $(this); if (input.val() === input.attr('placeholder')) { input.val(''); input.removeClass('placeholder'); } }).blur(function() { var input = $(this); if (input.val() === '' || input.val() === input.attr('placeholder')) { input.addClass('placeholder'); input.val(input.attr('placeholder')); } }).blur(); $('[placeholder]').parents('form').submit(function() { $(this).find('[placeholder]').each(function() { var input = $(this); if (input.val() === input.attr('placeholder')) { input.val(''); } }); }); } }); murano-dashboard-5.0.0/muranodashboard/static/muranodashboard/js/draggable-components.js0000666000175100017510000002252613245511125031654 0ustar zuulzuul00000000000000/* 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. */ $(function() { "use strict"; horizon.tabs.addTabLoadFunction(initServicesTab); initServicesTab($('.tab-content .tab-pane.active')); function initServicesTab($tab) { var $dropArea = $tab.find('.drop_component'); var draggedAppUrl = null; var firstDropTarget = null; function bindAppTileHandlers() { $('.draggable_app').each(function () { $(this).on('dragstart', function (ev) { ev.originalEvent.dataTransfer.effectAllowed = 'copy'; // we have to use an external variable for this since // storing data in dataTransfer object works only for FF draggedAppUrl = $(this).find('input[type="hidden"]').val(); // set it so the DND works in FF ev.originalEvent.dataTransfer.setData('text/uri-list', draggedAppUrl); }).on('dragend', function () { $dropArea.removeClass('over'); }); }); } $dropArea.on('dragover', function (ev) { ev.preventDefault(); ev.originalEvent.dataTransfer.dropEffect = 'copy'; return false; }).on('dragenter', function (ev) { $dropArea.addClass('over'); firstDropTarget = ev.target; }).on('dragleave', function (ev) { if (firstDropTarget === ev.target) { $dropArea.removeClass('over'); } }).on('drop', function (ev) { ev.preventDefault(); horizon.modals.loadModal(draggedAppUrl); return false; }); var packages = $.parseJSON($('#apps_carousel_contents').val()); function subdivide(array, numOfItems) { var chunks = []; var seq = array; var head = seq.slice(0, numOfItems); var tail = seq.slice(numOfItems); while (tail.length) { chunks.push(head); head = tail.slice(0, numOfItems); tail = tail.slice(numOfItems); } chunks.push(head); return chunks; } var $carouselInner = $tab.find('.carousel-inner'); var $carousel = $('#apps_carousel'); var $filter = $('#envAppsFilter').find('input'); var $noAppMsg = $('#no_apps_found_message'); var category = 'All'; var ALL_CATEGORY = 'All'; var filterValue = ''; var ENTER_KEYCODE = 13; var tileTemplate, environmentId; var $appTitleSmall = $('#app_tile_small'); if ($appTitleSmall.length > 0) { tileTemplate = Hogan.compile($appTitleSmall.html()); } if ($('#environmentId').length > 0) { environmentId = $('#environmentId').val(); } function fillCarousel(apps) { var i = apps.length; while (i--) { if (TENANT_ID !== apps[i].owner_id && apps[i].is_public === false) { apps.splice(i, 1); } } if (apps.length) { $dropArea.show(); $noAppMsg.hide(); if ($carousel.css('display') === 'none') { $carousel.show(); } subdivide(apps, 6).forEach(function (chunk, index) { var $item = $('
'); var $row = $('
'); if (index === 0) { $item.addClass('active'); } $item.appendTo($row); chunk.forEach(function (pkg) { var html = tileTemplate.render({ app_name: pkg.name, environment_id: environmentId, app_id: pkg.id }); // tenant_id is obtained from corresponding Django template if (TENANT_ID === pkg.owner_id) { html = $(html).find('img.ribbon').remove().end(); } $(html).appendTo($item); }); $item.appendTo($carouselInner); }); $('div.carousel-control').removeClass('item'); bindAppTileHandlers(); } else { if ($('#no_apps_in_catalog_message').length === 0) { $noAppMsg.show(); } $carousel.hide(); $dropArea.hide(); } } if (packages) { fillCarousel(packages); } $carousel.carousel({interval: false}); function refillCarousel() { $carouselInner.empty(); if (category === ALL_CATEGORY && filterValue === '') { fillCarousel(packages); } else { var filterRegexp = new RegExp(filterValue, 'i'); var filterRegexpExact = new RegExp('\\b' + filterValue + '\\b', 'i'); fillCarousel(packages.filter(function (pkg) { var categorySatisfied = true; var filterSatisfied = true; if (category !== ALL_CATEGORY) { categorySatisfied = pkg.categories.indexOf(category) > -1; } if (filterValue !== '') { filterSatisfied = pkg.name.match(filterRegexp); filterSatisfied = filterSatisfied || pkg.description.match(filterRegexp); filterSatisfied = filterSatisfied || pkg.tags.some(function (tag) { return tag.match(filterRegexpExact); }); } return categorySatisfied && filterSatisfied; })); } } // dynamic carousel refilling on category change $('#envAppsCategory').on('click', 'a', function (env) { var $category = $(this); category = $category.attr('data-category-name'); $('#envAppsCategoryName').text($category.text()); refillCarousel(); env.preventDefault(); }); // dynamic carousel refilling on search box non-empty submission $filter.keypress(function (ev) { if (ev.which === ENTER_KEYCODE) { filterValue = $filter.val(); refillCarousel(); ev.preventDefault(); } }); // show full name on text overflow $('.may_overflow').each(function() { $(this).bind('mouseenter', function () { var $this = $(this); if (this.offsetWidth < this.scrollWidth && !$this.attr('title')) { $this.attr('title', $this.text()); } }); }); // actions function hideSpinner() { horizon.modals.spinner.modal('hide'); } function handleError() { hideSpinner(); horizon.alert('error', gettext('Unable to run action.')); } bindActionHandlers($tab); var $table = $('table.datatable'); $table.on('update', function () { bindActionHandlers($table); }); function bindActionHandlers($parent) { $parent.find('.murano_action').off('click').on('click', function(event) { var $this = $(this); var $form = $this.closest('.table_wrapper > form'); var startUrl = $this.attr('href'); var resultUrl = null; var ERRDATA = 'error'; var data = null; function doRequest(url) { var requestData; $.ajax({ type: 'GET', url: url, async: false, error: function () { handleError(); requestData = ERRDATA; }, success: function (newData) { requestData = newData; } }); return requestData; } horizon.modals.modal_spinner(gettext("Waiting for a result")); var button = ''; var modalContent = horizon.modals.spinner.find(".modal-content"); var intervalId; modalContent.append(button); $('.modal-close button').tooltip(); $('.modal-close button').on("click", function () { window.clearInterval(intervalId); document.location = $form.attr('action'); }); if (startUrl) { $.ajax({ type: 'POST', url: startUrl, data: $form.serialize(), async: false, success: function (successData) { resultUrl = successData && successData.url; }, error: handleError }); if (resultUrl) { intervalId = window.setInterval(function () { // it's better to avoid placing the whole downloadable content // into JS memory in case of downloading very large files data = doRequest(resultUrl + 'poll'); if (!$.isEmptyObject(data)) { window.clearInterval(intervalId); if (data !== ERRDATA) { if (data.isException) { handleError(); document.location = resultUrl; } else if (typeof data.result !== "undefined" && data.result === null) { hideSpinner(); document.location = $form.attr('action'); } else { hideSpinner(); document.location = resultUrl; } } } }, 1000); } } event.preventDefault(); }); } } }); murano-dashboard-5.0.0/muranodashboard/static/muranodashboard/js/murano.tables.js0000666000175100017510000001324613245511125030332 0ustar zuulzuul00000000000000/* 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. */ // In some cases successful update events can stack up in case we have lots of apps in an env. // This might lead to a situation when lots of reloads are scheduled simultaneously. // The following variable forces reload to be called only once. var reloadCalled = false; $(function() { "use strict"; $("table#services.datatable").on("update", function () { // If every component has finished installing (with error or success): reloads the page. var $rowsToUpdate = $(this).find('tr.warning.ajax-update'); if ($rowsToUpdate.length === 0) { if (reloadCalled === false) { reloadCalled = true; location.reload(true); } } }); }); // Reload page using horizon ajax_poll_interval // until deployment of empty environment is finished $(function() { "use strict"; if ($("div#environment_details__services").find("div.drop_component").length === 0 && $("table#services.datatable").find("tr.empty").length && $("button#services__action_deploy_env").length === 0) { var $pollInterval = $("input#pollInterval")[0].value; setTimeout(function () { if (reloadCalled === false) { reloadCalled = true; location.reload(true); } }, $pollInterval); } }); var reloadEnvironmentCalled = false; var lastStatuses = []; // Reload page after table update if no more environments left // or status of some environment changed $(function() { "use strict"; $("table#environments").on("update", function () { var $environmentsRows = $(this).find('tbody tr:visible').not('.empty'); if ($environmentsRows.length === 0) { if (reloadEnvironmentCalled === false) { reloadEnvironmentCalled = true; location.reload(true); } } else { var $statuses = []; for (var $i = 0; $i < $environmentsRows.length; $i++) { var $row = $($environmentsRows[$i]); var $rowStatus = getRowStatus($row); $statuses.push($rowStatus); } if (lastStatuses.length !== 0 && areArraysEqual($statuses, lastStatuses) === false) { if (reloadEnvironmentCalled === false) { reloadEnvironmentCalled = true; location.reload(true); } } else { lastStatuses = $statuses; } } }); }); function getRowStatus($row) { "use strict"; if ($row.hasClass('warning')) { return "in process"; } else { return $row.attr("status"); } } function areArraysEqual($arr1, $arr2) { "use strict"; if ($arr1.length !== $arr2.length) { return false; } for (var $i = 0; $i < $arr1.length; $i++) { if ($arr1[$i] !== $arr2[$i]) { return false; } } return true; } // Disable action buttons according to the statuses of checked environments $(function() { "use strict"; var $statuses = { environments__deploy: ['pending', 'deploy failure'], environments__delete: ['ready', 'pending', 'new', 'deploy failure', 'delete failure'], environments__abandon: ['ready', 'in process', 'deploy failure', 'delete failure'] }; // Change of individual checkboxes or table update // TODO(vakovalchuk): improve checkbox detection on the deploying rows // Deploying rows don't react to selectors less broad than table body, e.g.: // $("table#environments tbody input[type='checkbox']").change(enableButtons); $("table#environments tbody").click(enableButtons); $("table#environments").on("update", enableButtons); function enableButtons() { var $buttons = $("table#environments div.table_actions").find('button[name="action"]'); var $environmentsRows = $("table#environments").find('tbody tr:visible').not('.empty'); for (var $i = 0; $i < $buttons.length; $i++) { var $buttonValue = $buttons[$i].value; for (var $j = 0; $j < $environmentsRows.length; $j++) { var $row = $($environmentsRows[$j]); var $checkbox = $row.find("input.table-row-multi-select").first(); if ($checkbox.prop('checked')) { var $rowStatus = getRowStatus($row); if ($statuses[$buttonValue].indexOf($rowStatus) === -1) { $($buttons[$i]).prop("disabled", true); break; } } else { $($buttons[$i]).prop("disabled", false); } } } } // Change of all checkboxes at once $("table#environments thead input.table-row-multi-select:checkbox").change(function () { var $buttons = $("table#environments div.table_actions").find('button[name="action"]'); var $environmentsRows = $("table#environments").find('tbody tr:visible').not('.empty'); if ($(this).prop('checked')) { for (var $j = 0; $j < $buttons.length; $j++) { var $buttonValue = $buttons[$j].value; for (var $k = 0; $k < $environmentsRows.length; $k++) { var $row = $($environmentsRows[$k]); var $rowStatus = getRowStatus($row); if ($statuses[$buttonValue].indexOf($rowStatus) === -1) { $($buttons[$j]).prop("disabled", true); break; } } } } else { for (var $l = 0; $l < $buttons.length; $l++) { $($buttons[$l]).prop("disabled", false); } } }); }); murano-dashboard-5.0.0/muranodashboard/static/muranodashboard/js/import_bundle_form.js0000666000175100017510000000276213245511125031447 0ustar zuulzuul00000000000000/* 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. */ $(function() { "use strict"; horizon.modals.addModalInitFunction(muranoUploadBundle); function muranoUploadBundle(modal) { var uploadForm = $(modal).find('#import_bundle'); var importType = uploadForm.find('[name=upload-import_type]'); uploadForm.find('input[name=upload-url]').closest('.form-group').addClass('required'); uploadForm.find('input[name=upload-name]').closest('.form-group').addClass('required'); importType.change(function() { var uploadType = $(this).val(); if (uploadType === 'by_name') { uploadForm.find('.description-by_name').show(); uploadForm.find('.description-by_url').hide(); } else if (uploadType === 'by_url') { uploadForm.find('.description-by_name').hide(); uploadForm.find('.description-by_url').show(); } }); importType.change(); } muranoUploadBundle($('#import_bundle').parent()); }); murano-dashboard-5.0.0/muranodashboard/static/muranodashboard/js/validators.js0000666000175100017510000000342513245511125027726 0ustar zuulzuul00000000000000/* 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. */ $(function() { "use strict"; function mainCheck(div, parameter1, parameter2, text) { var msg = "
" + text + '
'; var errorNode = div.find("div.alert-message"); var notAdded; if (errorNode.length) { notAdded = false; errorNode.html(text); } else { notAdded = true; } if (parameter1 !== parameter2 && notAdded) { div.addClass("error"); div.find("label").after(msg); } else if (parameter1 === parameter2) { div.removeClass("error"); errorNode.remove(); } } function validateField(event) { var $this = $(event.target); var value = $this.val(); var arrayPattern = $this.data('validators'); var text = gettext(""); var meetRequirements = true; arrayPattern.forEach(function(n) { var re = new RegExp(n.regex); if (value.match(re) === null) { text += gettext(n.message) + "
"; meetRequirements = false; } }); var div = $this.closest(".form-field,.form-group"); mainCheck(div, meetRequirements, true, text); } $(document).on("keyup", "input[data-validators]:not([id$='clone'])", validateField); }); murano-dashboard-5.0.0/muranodashboard/static/muranodashboard/js/mixed-mode.js0000666000175100017510000000202013245511125027574 0ustar zuulzuul00000000000000/* 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. */ $(function() { "use strict"; function checkMixedMode() { var checked = $("input[id*='mixedModeAuth']").prop('checked'); if (checked === true) { $("label[for*='saPassword']").parent().css({"display": 'inline-block'}); } else if (checked === false) { $("label[for*='saPassword']").parent().css({"display": 'none'}); } } $("input[id*='mixedModeAuth']").change(checkMixedMode); checkMixedMode(); }); murano-dashboard-5.0.0/muranodashboard/static/muranodashboard/js/external-ad.js0000666000175100017510000000251513245511125027761 0ustar zuulzuul00000000000000/* 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. */ $(function() { "use strict"; function checkPreconfiguredAd() { var checked = $("input[id*='externalAD']").prop('checked'); if (checked === true) { $("select[id*='-domain']").attr("disabled", "disabled"); $("label[for*='domainAdminUserName']").parent().css({"display": 'inline-block'}); $("label[for*='domainAdminPassword']").parent().css({"display": 'inline-block'}); } if (checked === false) { $("select[id*='-domain']").removeAttr("disabled"); $("label[for*='domainAdminUserName']").parent().css({"display": 'none'}); $("label[for*='domainAdminPassword']").parent().css({"display": 'none'}); } } $("input[id*='externalAD']").change(checkPreconfiguredAd); checkPreconfiguredAd(); }); murano-dashboard-5.0.0/muranodashboard/static/muranodashboard/js/submit-disabled.js0000666000175100017510000000134713245511125030627 0ustar zuulzuul00000000000000/* 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. */ $(function() { "use strict"; $(':disabled').closest('form').submit(function() { $(':disabled').attr('disabled', false); }); }); murano-dashboard-5.0.0/muranodashboard/static/muranodashboard/js/upload_form.js0000666000175100017510000000402613245511125030063 0ustar zuulzuul00000000000000/* 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. */ $(function() { "use strict"; horizon.modals.addModalInitFunction(muranoUploadPackage); function muranoUploadPackage(modal) { var uploadForm = $(modal).find('#upload_package'); var importType = uploadForm.find('[name=upload-import_type]'); uploadForm.find('input[name=upload-url]').closest('.form-group').addClass('required'); uploadForm.find('input[name=upload-repo_name]').closest('.form-group').addClass('required'); uploadForm.find('input[name=upload-package]').closest('.form-group').addClass('required'); importType.on('change', function() { var uploadType = $(this).val(); if (uploadType === 'upload') { uploadForm.find('.description-upload').show(); uploadForm.find('.description-by_name').hide(); uploadForm.find('.description-by_url').hide(); } else if (uploadType === 'by_name') { uploadForm.find('.description-upload').hide(); uploadForm.find('.description-by_name').show(); uploadForm.find('.description-by_url').hide(); } else if (uploadType === 'by_url') { uploadForm.find('.description-upload').hide(); uploadForm.find('.description-by_name').hide(); uploadForm.find('.description-by_url').show(); } }); importType.change(); $('#upload_package_modal .cancel').on('click', function() { location.reload(); }); } muranoUploadPackage($('#upload_package').parent()); }); murano-dashboard-5.0.0/muranodashboard/static/muranodashboard/js/horizon.muranotopology.js0000666000175100017510000002570213245511125032345 0ustar zuulzuul00000000000000/** * Adapted for Murano js topology generator. * Based on: * HeatTop JS Framework * Dependencies: jQuery 1.7.1 or later, d3 v3 or later * Date: June 2013 * Description: JS Framework that subclasses the D3 Force Directed Graph library to create * Heat-specific objects and relationships with the purpose of displaying * Stacks, Resources, and related Properties in a Resource Topology Graph. * * 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. */ $(function() { "use strict"; horizon.tabs._init_load_functions.push(loadMuranoTopology); function loadMuranoTopology() { var muranoContainer = "#murano_application_topology"; if ($(muranoContainer).length === 0) { return; } /** * var diagonal = d3.svg.diagonal() * .projection(function(d) { return [d.y, d.x]; }); */ /** * If d3 is undefined, and give an assignment * It solves no-def error: d3 is not defined */ //var d3 = d3 || {}; /** * Declare global variables */ var ajaxUrl, force, node, link, needsUpdate, nodes, links, inProgress; function update() { node = node.data(nodes, function(d) { return d.id; }); link = link.data(links); var nodeEnter = node.enter().append("g") .attr("class", "node") .attr("node_name", function(d) { return d.name; }) .attr("node_id", function(d) { return d.id; }) .call(force.drag); nodeEnter.append("image") .attr("xlink:href", function(d) { return d.image; }) .attr("id", function(d) { return "image_" + d.id; }) .attr("x", function(d) { return d.image_x; }) .attr("y", function(d) { return d.image_y; }) .attr("width", function(d) { return d.image_size; }) .attr("height", function(d) { return d.image_size; }) .attr("clip-path", "url(#clipCircle)"); node.exit().remove(); link.enter().insert("path", "g.node") .attr("class", function(d) { return "link " + d.link_type; }); link.exit().remove(); //Setup click action for all nodes node.on("mouseover", function(d) { $("#info_box").html(d.info_box); //current_info = d.name; }); node.on("mouseout", function() { $("#info_box").html(''); }); force.start(); } function drawLink(d) { return "M" + d.source.x + "," + d.source.y + "L" + d.target.x + "," + d.target.y; } function tick() { link.attr('d', drawLink).style('stroke-width', 3).attr('marker-end', "url(#end)"); node.attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; }); } function setInProgress(stack, innerNodes) { if (stack.in_progress === true) { inProgress = true; } for (var i = 0; i < innerNodes.length; i++) { var d = innerNodes[i]; if (d.in_progress === true) { inProgress = true; return false; } } } function findNode(id) { for (var i = 0; i < nodes.length; i++) { if (nodes[i].id === id) { return nodes[i]; } } } function findNodeIndex(id) { for (var i = 0; i < nodes.length; i++) { if (nodes[i].id === id) { return i; } } } function addNode(innerNode) { nodes.push(innerNode); needsUpdate = true; } function removeNode(id) { var i = 0; var n = findNode(id); while (i < links.length) { if (links[i].source === n || links[i].target === n) { links.splice(i, 1); } else { i++; } } nodes.splice(findNodeIndex(id), 1); needsUpdate = true; } function removeNodes(oldNodes, newNodes) { //Check for removed nodes for (var i = 0; i < oldNodes.length; i++) { var isRemoveNode = true; for (var j = 0; j < newNodes.length; j++) { if (oldNodes[i].id === newNodes[j].id) { isRemoveNode = false; break; } } if (isRemoveNode === true) { removeNode(oldNodes[i].id); } } } function buildNodeLinks(innerNode) { for (var j = 0; j < innerNode.required_by.length; j++) { var pushLink = true; var targetIdx = ''; var sourceIdx = findNodeIndex(innerNode.id); //make sure target node exists try { targetIdx = findNodeIndex(innerNode.required_by[j]); } catch (err) { if (window.console) { window.console.log(err); } pushLink = false; } //check for duplicates for (var lidx = 0; lidx < links.length; lidx++) { if (links[lidx].source === sourceIdx && links[lidx].target === targetIdx) { pushLink = false; break; } } if (pushLink === true && (sourceIdx && targetIdx)) { links.push({ "target": sourceIdx, "source": targetIdx, "value": 1, "link_type": innerNode.link_type }); } } } function buildReverseLinks(innerNode) { for (var i = 0; i < nodes.length; i++) { if (nodes[i].required_by) { for (var j = 0; j < nodes[i].required_by.length; j++) { var dependency = nodes[i].required_by[j]; //if new node is required by existing node, push new link if (innerNode.id === dependency) { links.push({ "target": findNodeIndex(nodes[i].id), "source": findNodeIndex(innerNode.id), "value": 1, "link_type": nodes[i].link_type }); } } } } } function buildLinks() { for (var i = 0; i < nodes.length; i++) { buildNodeLinks(nodes[i]); buildReverseLinks(nodes[i]); } } function ajaxPoll(pollTime) { setTimeout(function() { $.getJSON(ajaxUrl, function(json) { //update d3 data element $("#d3_data").attr("data-d3_data", JSON.stringify(json)); //update stack $("#stack_box").html(json.environment.info_box); setInProgress(json.environment, json.nodes); needsUpdate = false; //Check Remove nodes removeNodes(nodes, json.nodes); //Check for updates and new nodes json.nodes.forEach(function(d) { var currentNode = findNode(d.id); //Check if node already exists if (currentNode) { //Node already exists, just update it currentNode.status = d.status; //Status has changed, image should be updated if (currentNode.image !== d.image) { currentNode.image = d.image; var thisImage = d3.select("#image_" + currentNode.id); thisImage .transition() .attr("x", function(dImage) { return dImage.image_x + 5; }) .duration(100) .transition() .attr("x", function(dImage) { return dImage.image_x - 5; }) .duration(100) .transition() .attr("x", function(dImage) { return dImage.image_x + 5; }) .duration(100) .transition() .attr("x", function(dImage) { return dImage.image_x - 5; }) .duration(100) .transition() .attr("xlink:href", d.image) .transition() .attr("x", function(dImage) { return dImage.image_x; }) .duration(100) .ease("bounce"); } //Status has changed, update info_box currentNode.info_box = d.info_box; } else { addNode(d); buildLinks(); } }); //if any updates needed, do update now if (needsUpdate === true) { update(); } }); //if no nodes still in progress, slow AJAX polling if (inProgress === false) { pollTime = 30000; } else { pollTime = 3000; } ajaxPoll(pollTime); }, pollTime); } if ($(muranoContainer).length) { var width = $(muranoContainer).width(); var height = 1040; var environmentId = $("#environment_id").data("environment_id"); var graph = $("#d3_data").data("d3_data"); var svg = d3.select(muranoContainer).append("svg") .attr("width", width) .attr("height", height); ajaxUrl = '/app-catalog/' + environmentId + '/services/get_d3_data'; force = d3.layout.force() .nodes(graph.nodes) .links([]) .gravity(0.25) .charge(-3000) .linkDistance(100) .size([width, height]) .on("tick", tick); node = svg.selectAll(".node"); link = svg.selectAll(".link"); needsUpdate = false; nodes = force.nodes(); links = force.links(); svg.append("svg:clipPath") .attr("id", "clipCircle") .append("svg:circle") .attr("cursor", "pointer") .attr("r", "38px"); svg.append("svg:defs").selectAll("marker") .data(["end"]) // Different link/path types can be defined here .enter().append("svg:marker") // This section adds in the arrows .attr("id", String) .attr("viewBox", "0 -5 10 10") .attr("refX", 25) .attr("refY", 0) .attr("fill", "#999") .attr("markerWidth", 6) .attr("markerHeight", 6) .attr("orient", "auto") .append("svg:path") .attr("d", "M0,-3L10,0L0,3"); buildLinks(); update(); //Load initial Stack box $("#stack_box").html(graph.environment.info_box); //On Page load, set Action In Progress inProgress = false; setInProgress(graph.environment, node); //If status is In Progress, start AJAX polling var pollTime = 0; if (inProgress === true) { pollTime = 3000; } else { pollTime = 30000; } ajaxPoll(pollTime); } } }); murano-dashboard-5.0.0/muranodashboard/static/muranodashboard/js/add-select.js0000666000175100017510000000575413245511125027572 0ustar zuulzuul00000000000000/* 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. */ $(function() { "use strict"; var plus = ""; if (typeof window.murano === "undefined") { window.murano = {}; } if (!window.murano.bind_add_item_handlers) { window.murano.bind_add_item_handlers = true; horizon.modals.addModalInitFunction(initPlusButton); // in case this script is executed on static page and not on a modal // we have to call the init function manually initPlusButton($('div.static_page form')); } function initPlusButton(el) { var $selects = $(el).find('select[data-add-item-url]'); $selects.each(function () { var $this = $(this); var urls, link, $choices; try { urls = $.parseJSON($this.attr("data-add-item-url")); } catch (err) { if (window.console) { window.console.log(err); } } if (urls && urls[0].length) { if (urls.length === 1) { link = $this.next().find('a'); link.html(plus); link.attr('href', urls[0][1]); } else { link = $this.next().find('a').toggleClass('dropdown-toggle'); link.html(plus); link.attr('href', '#'); link.attr('data-toggle', 'dropdown'); link.removeClass('ajax-add ajax-modal'); $choices = $(""); $(urls).each(function(i, url) { $choices.append($("
  • " + url[0] + "
  • ")); }); $this.next('span').append($choices); } } if ($this.hasClass('murano_add_select')) { // NOTE(tsufiev): hide selectbox in case it contains no elements if (this.options.length === 1) { $this.hide(); $this.next('span').removeClass('input-group-btn').find('i').text( ' Add Application'); } // NOTE(tsufiev): show hidden select once the new option was added to it // programmatically (on return from the finished modal dialog) $this.change(function() { if (!$this.is(':visible') && this.options.length > 1) { $this.show(); $this.next('span').addClass('input-group-btn').find('i').text(''); $this.val($(this.options[1]).val()); } }); } }); } }); murano-dashboard-5.0.0/muranodashboard/static/muranodashboard/js/passwordfield.js0000666000175100017510000000571013245511125030423 0ustar zuulzuul00000000000000/* 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. */ $(function() { "use strict"; function mainCheck(div, parameter1, parameter2, text) { var msg = "
    " + gettext(text) + '
    '; var errorNode = div.find("div.alert-message"); var notAdded; if (errorNode.length) { notAdded = false; errorNode.text(text); } else { notAdded = true; } if (parameter1 !== parameter2 && notAdded) { div.addClass("error"); div.find("label").after(msg); } else if (parameter1 === parameter2) { div.removeClass("error"); errorNode.remove(); } } function checkPasswordsMatch(event) { var $this = $(event.target); var password = $this.closest(".form-field,.form-group").prev().find("input").val(); var confirmPassword = $this.val(); var div = $this.closest(".form-field,.form-group"); mainCheck(div, password, confirmPassword, gettext("Passwords do not match")); } function checkStrengthRemoveErrIfMatches(event) { var $this = $(event.target); var password = $this.val(); var confirmPassId = $this.attr('id') + '-clone'; var confirmPassword = $('#' + confirmPassId).val(); var div = $this.closest(".form-field,.form-group").next(); if (confirmPassword.length) { mainCheck(div, password, confirmPassword, gettext("Passwords do not match")); } var text = gettext("Your password should have at least"); var meetRequirements = true; if (password.length < 7) { text += gettext(" 7 characters"); meetRequirements = false; } if (password.match(/[A-Z]+/) === null) { text += gettext(" 1 capital letter"); meetRequirements = false; } if (password.match(/[a-z]+/) === null) { text += gettext(" 1 non-capital letter"); meetRequirements = false; } if (password.match(/[0-9]+/) === null) { text += gettext(" 1 digit"); meetRequirements = false; } if (password.match(/[!@#$%^&*()_+|\/.,~?><:{}-]+/) === null) { text += gettext(" 1 special character"); meetRequirements = false; } div = $this.closest(".form-field,.form-group"); mainCheck(div, meetRequirements, true, text); } $(document).on("keyup", "input[data-type='password']:not([id$='clone'])", checkStrengthRemoveErrIfMatches); $(document).on("keyup", "input[id$='-clone'][data-type='password']", checkPasswordsMatch); }); murano-dashboard-5.0.0/muranodashboard/static/muranodashboard/css/0000775000175100017510000000000013245511556025376 5ustar zuulzuul00000000000000murano-dashboard-5.0.0/muranodashboard/static/muranodashboard/css/catalog.css0000666000175100017510000002152413245511125027520 0ustar zuulzuul00000000000000table#murano div.table_cell_wrapper div.inline-edit-form input { /* * overrides from form-control, to make inline edit consistent * with the rest of UI */ display: block; height: 32px; font-size: 13px; line-height: 1.42857; color: #555; background-color: #fff; background-image: none; border: 1px solid #ccc; border-radius: 4px; -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); -webkit-transition: border-color ease-in-out 0.15s, box-shadow ease-in-out 0.15s; -o-transition: border-color ease-in-out 0.15s, box-shadow ease-in-out 0.15s; transition: border-color ease-in-out 0.15s, box-shadow ease-in-out 0.15s; /* Additional styles, not present in form-control */ margin: 7px 7px 0 7px; width: calc(100% - 14px); } table#murano div.table_cell_wrapper div.inline-edit-form input:focus { border-color: #66afe9; outline: 0; -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(102, 175, 233, 0.6); box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(102, 175, 233, 0.6); } .table_header.catalog { border-bottom: 2px solid #e5e5e5; padding-bottom:5px; margin-bottom: 10px; } .table_search { min-width: 265px; } .catalog .table_actions { width: 100%; } .scrollable-menu { height: auto; max-height: 200px; max-width: 250px; overflow-x: hidden; } .scrollable-menu a { text-overflow: ellipsis; white-space: nowrap; overflow: hidden; } .murano-dropdown-menu { margin-top: -15px; margin-left: 20px; } h3.heading_switcher { font-size: 18px; } .add_env { margin-left: auto; margin-right: auto; } .no_envs { color: slategray; } .link.reference { stroke-dasharray: 5,5; } h3.heading_switcher a.btn { font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; } .catalog .table_actions a, .catalog .table_actions button { margin-left: 0; } .catalog .row_actions button, .catalog .row_actions a { background: none; float: none; display: block; padding: 5px 10px; color: black; text-align: left; border-radius: 0; border: 0 none; -webkit-box-shadow: none; -moz-box-shadow: none; box-shadow: none; } .catalog .dropdown-menu li > a:hover, .catalog .dropdown-menu .active > a, .catalog .dropdown-menu .active > a:hover { color: black; background-color: #CDCDCD; } .catalog .dropdown.open .dropdown-toggle { color: black; background: #e6e6e6; } .app-list .actions { border-top:1px #ccc solid; padding-top:15px; margin-top:15px; } .app-list .app-description { height: 135px; } .app-list .app-description h4 { margin-top: 0; } .btn-link { padding: 5px 0; } .centering { margin-left: auto; margin-right: auto; text-align: center; white-space: nowrap; text-overflow: ellipsis; overflow: hidden; } .draggable_app img { margin-bottom: 5px; display: block; } .draggable_app .well { margin-bottom: 0; } .well .app-actions-container { padding-right: 0; } .draggable_app .may_overflow { max-width: 85%; white-space: normal; } @media (min-width: 1020px) { .draggable_app .well, .no_apps.well { min-height: 105px; } } .search-icon { line-height: 32px; } .draggable_app { font-size: 11px; } .draggable_app.col-xs-2 { padding-left: 5px; padding-right: 5px; } .carousel-inner .row { margin-left: 0; margin-right: 0; } .carousel { padding: 0 40px; margin-bottom: 20px; margin-top: 2px; } .carousel-control { position: absolute; top: 38px; left: -7px; bottom: 0; width: 30px; opacity: 1; filter: alpha(opacity=100); font-size: 20px; color: #666; text-align: center; text-shadow: none; } .carousel-control:hover:focus { color: #d93c27; } .carousel-control:hover { color: #d93c27; opacity: 1; } .carousel-control:focus { color: #666; } .carousel-control.left { background: inherit; } .carousel-control.right { background: inherit; right: 7px; } .category-title { float: left; line-height: 26px; margin-top: 20px; font-weight: normal; } .extra_title { line-height: 26px; font-weight: normal; font-size: 24px; } .draggable_app .well:hover { border-color: #d93c27; background: #fff; } #envAppsCategory.dropdown { min-width: 160px; } #envAppsCategory > button{ margin-top: 16px; max-width:60%; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } #envAppsFilter { margin-top: 15px; } div.drop_component { border: 2px #ccc dashed; background-color: #EBF3F9; width: 94%; margin: 0 auto -2px; border-radius: 5px; display: none; } div.drop_component.over { border-color: #3B88C3; } #no_apps_found_message { display: none; } h3.link { color: yellow; opacity: 0.5; } h3.link:hover { opacity: 1; } h3.link .caret { border-top: 4px solid yellow !important; } .app .ribbon { position: absolute; bottom: 0; left: 0; } .draggable_app .ribbon{ position: absolute; bottom: -4px; } .well:hover { float: none; border: 1px solid #bbb; background: #eee; -o-transition: all .15s; -moz-transition: all .15s ease-out; -webkit-transition: all .15s ease-out; transition: all .15s ease-out; } .row.row-header { padding-left: 8px; padding-right: 8px; } .row.app-list { margin-top: 20px; margin-bottom:20px; } .row .filter-bar { position: absolute; right: 18px; } .app-list .app { position:relative; -moz-border-radius: 8px; -webkit-border-radius: 8px; border-radius: 8px; } .app-list .app > div { margin:0; } .app-list .app div.description { margin-left:0; height: 100%; } .app-list .app h4 { margin-bottom:0.25em; text-overflow: ellipsis; white-space: nowrap; overflow: hidden; } .app-list .app p { max-height: 75px; overflow: hidden; } .app-list .app .app-actions { float: right; margin-top: 5px; } .app-actions div[class*='span'] { margin-left:0; } .header.dropdown { display:inline-block; margin-left: 5px; } .header.dropdown ul { font-size:14px; font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; } .nav-tabs { margin-top:20px; } .app-meta { padding:10px; background-color: #eeeeee; -moz-border-radius: 8px; -webkit-border-radius: 8px; border-radius: 8px; } .tab-content { margin-bottom:10px; } .app-meta .app-supplier { border-top:1px solid #ddd; padding-top:15px; margin-top:15px; } .app-supplier-data { overflow: hidden; padding-left: 10px; } .app-meta ul { padding: 15px 0; } .app-icon { line-height:0; overflow:hidden; margin-left:0; padding: 0; float: left; max-width: 80px; } .app-icon img { max-height:70px; width: 100%; margin: 0; } #environment_switcher { display: inline-block; } .panel-visible{ display: block; } .columns{ clear: both; width: 330px; padding: 0 0 20px 0; line-height: 22px; } .colleft{ float: left; width: 150px; line-height: 22px; } .colright{ float: right; width: 150px; line-height: 22px; } .app-icon.server { margin-right:20px; } .no-results { margin-bottom: 25px; } .app_requirements ul li { font-weight: bold; } .app_requirements ul li ul { margin-left: 30px; } .app_requirements ul li ul li { font-weight: normal; } /* Bootstrap 3 grid always applies 15px left and right padding on grid columns */ /* special class to override first and last column in row */ .col-row:first-child { padding-left:0; } .col-row:last-child { padding-right: 0; } /* LANDSCAPE PHONE TO SMALL DESKTOP & PORTRAIT TABLET */ /* Workarounds/hacks for filter forms at top of catalog page when fluid grid... */ /* changes layout due to device/width settings in Bootstrap 3's grid */ @media (max-width: 767px) { .col-xs-12 .pull-right { float: none !important; } .col-xs-12.col-row { padding-left: 0; } .col-xs-12 .table_actions form { float: none; margin-left: 0; } .col-xs-12 .table_search { margin-left: -5px; } .col-xs-12 .table_search .form-group { float: left; margin-right: 15px; } .col-xs-12 .table_search .form-control-feedback { top: 0; } .app-list .app .app-details-link { float: left; } .app-list .app .app-actions { float: left; } } .modal-header img { margin-top: -7px; } .modal-dialog h3, span.wizard_title { text-overflow: ellipsis; white-space: nowrap; overflow: hidden; } .selected-field { color: #428BCA; margin-left: -12px; font-weight: bold; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; } .steps_list_container { margin: 0 -15px; border-top: #d3d3d3 1px solid; } .steps_list { padding: 10px 20px 0 20px; } .steps_list li { padding-bottom: 7px; } .steps_list li.done { color: #c8c6c8; } button.wizard_cancel { margin-right: 10px; } h3 .wizard_title { margin-top: 15px; margin-bottom: 15px; } .steps_list li.active { color: #d93c27; font-weight: bold; } .more_content_dynamicui span { display: none; } .more_link_dynamicui { display: block; }murano-dashboard-5.0.0/muranodashboard/static/muranodashboard/css/deployments.css0000666000175100017510000000030213245511125030440 0ustar zuulzuul00000000000000table.deployment_history_cell { margin: 0 auto; } table.deployment_history_cell td, table.deployment_history_cell th { border-top: none !important; padding-bottom: 2px !important; }murano-dashboard-5.0.0/muranodashboard/static/muranodashboard/css/reports.css0000666000175100017510000000067713245511125027612 0ustar zuulzuul00000000000000.reports { display: block; padding: 9.5px; margin: 0 0 10px; font-size: 14px; line-height: 1.42857; word-break: break-all; word-wrap: break-word; color: #333; background-color: #f5f5f5; border: 1px solid #ccc; border-radius: 4px; font-family: monospace; } .reports .report-error { color: #ff0000; font-weight: bold; } .reports .report-warning { color: #df7401; font-weight: bold; } murano-dashboard-5.0.0/muranodashboard/static/muranodashboard/css/hide_app_name.css0000666000175100017510000000014413245511125030652 0ustar zuulzuul00000000000000div.right h3{ display: none !important; } div.right p strong { display: none !important; } murano-dashboard-5.0.0/muranodashboard/static/muranodashboard/css/checkbox.css0000666000175100017510000000025713245511125027674 0ustar zuulzuul00000000000000input[type="checkbox"] { float: left; width: auto !important; margin-right: 10px; -webkit-box-shadow: none; -moz-box-shadow: none; box-shadow: none; } murano-dashboard-5.0.0/muranodashboard/static/muranodashboard/css/packages.css0000666000175100017510000000012013245511125027651 0ustar zuulzuul00000000000000#id_upload-package { width: 100%; } .fa-user-plus { margin-left: 6px; }murano-dashboard-5.0.0/muranodashboard/static/muranodashboard/css/components.css0000666000175100017510000000021213245511125030262 0ustar zuulzuul00000000000000.modal-close { padding-bottom: 20px; text-align: center; } .modal-close button + .tooltip { font-size: 17px; width: 500px; } murano-dashboard-5.0.0/muranodashboard/static/muranodashboard/css/support_placeholder.css0000666000175100017510000000007413245511125032161 0ustar zuulzuul00000000000000.placeholder { color: #999999; font-style: italic; }murano-dashboard-5.0.0/muranodashboard/static/muranodashboard/images/0000775000175100017510000000000013245511556026053 5ustar zuulzuul00000000000000murano-dashboard-5.0.0/muranodashboard/static/muranodashboard/images/murano_srv_red.png0000666000175100017510000000432113245511125031600 0ustar zuulzuul00000000000000‰PNG  IHDR00Wù‡sBIT|dˆ pHYsÈÈý×;tEXtSoftwarewww.inkscape.org›î<NIDAThí™]lÕÇgfö#^ÛIlœ&8M-‹„%$ih*JÉMùx*„ö…‡‚ªªj©ÔÁô­â…'Ô>TZ¨*R› ABƒ¨ ªHC#I„1 ÎÚ»kïîÌ{OffwÖùÀ­¬H>ÒÑ|Ü;žó¿ÿsÎýÏZT•ËÙ¼¥à«Ú2€¥¶eKm—=€`±÷Š”ÎÁõÿÇxþý€ê_;yQ&DÖUFFÞÝùôÓ†vìATÁ9ÔZ°5â89ƒƒFQçœ0D£5gmÇ­µ8cøìÍ7y^ä…=ª{þg<øÕÎ_Ü00:Š›žF*¤Rr9Eh³™øÜZ¯£Z¯C½ž\×j{Z«áêu¬µÄÎaœÃXKìyl¸ï>L£qÿ„Ès¨î[Dl—¶çDÖ oÞüÐàõ×cOžLnú>”JÈÀÞÐÒß”ËH¡€ø>ây€*š²£);ÙêEÐnC«Í&Újáêuf^z‰±»îRyb‘‹{iøÅö'Ÿ,Ù?„8kÁ¹ä˜¦ ™IS+sÉ_{^׳ñÎK’sâéiâÉIYóÍ;&DîøJþ"24|íµ?]uãØ'Ð0DÛmt~>I‡™Üôt’Íf2Eh'+î\6­T»`U¹˜ «½þ:_ßµkQ,\²"øÙ¶ÇﳇÁüa¤ÕJk·;¹Ÿ¹Sí: ª]fdô¶Û”/aáR üdÛ# Ä µZÒ"ã‰"hµÐb‚ñ’5Ь&¢( ¼ÕBç绌´Û‰§ÌdÁëBOÁ˜™¢ÉIYsà ß|Ad÷ýª_4€gEúÖ®[÷Ëá-[ŸyéëK‚Ã$øR I¨¤¥è\·ÿ‡aÍf/#­Îœs8Ulž‰>óÎ;|m÷n={äÈÀâôÃozè¡!óê«èìl’Ëaív|©„ø>âû šÎÏj"cCççÑV ëV›HyWUL½N85%C›6m¿ çØ+RêþõÈÖ­Ú~ì1‘r C$ëÙÅ"Z,"AÐe@$é.ÖvYÈŠºÝN¤@bç:n3OAXÕf¬*ÕC‡¾ýv­=zAÎP…¿õàƒkZ/¿ŒmµðsÁg«O¡€A²ú9šÛ4Šz@8cˆÓ@ãnó€òŒ8‡i4hMMÉàøøö ‘ï? ú·‹xV¤0´zõoF¶mÓÙG•ðUñ[-¼0Ä ‚ÞÕ÷ýdc‚nÏé!¢(Ñ9ùU^ôB62㜣zø0«o½U'N<\@?|÷š{ï]ߨ·c ÎóðUñDðÃsÏð²ÖËubç:ÝÄAg;)‘ ìB â Œ[ç0ss4OŸ–þ±±m/ˆì¸_õŸÙ+{ö…Í£÷ÜCxö,Æó0žG$‚!JÏ#‘ÎýH„HµëÙ=ÏäÏeLj{^ÛÔçá|׌åB‚€Õããô­]‹Á½‚»ÿ$òm™€+W_}õþÒôtôE­våî>.«âfg»Û~¦y2©œÛa5?–IfcÐ4…ÔÚ _æù"Îw#€™O>¡zô(WŽQ>vŒÿœ9óÑœ»&¿±k×Èè¡CCÿxÿ}Ô9Š×]×e%Ó=9‰¬F²»6p¡±ìØhÓ‘ËÆ9bk{Ï3)ÍY0–Yßè(ˆÐ?2BervO t„Uî‡.5†S{÷291A¡P X(ø>…ôa?Žñ¬Å3&éXéf'Ù†µ «XUJ[·Ò8q‚™y÷]ÖîÜÉǯ¼Â†;ïäã}ûXwË-L½÷k6näì[o%±8×ùˆ:'›8^¯ïÃÐóK]2wì§`Åà åJ…r_¥R‰B±H±P ‚ñUÑ8Æ"¤Ý&çÕU«è«TØ87GåàAlc£P,‹ ¥FÊeŒ*ÒׇdÅ ¬^©Ô]LçŽV«T=•ù"^_©Ø±|;„D|µÛ¸v_$–õú¬ß缓û©œ>rÕUì;s†S¥ÕM›`d¤'¿m¦}rìhÆV®±œ½£Ä™ &ó¬åº¡!¾cm/%ß×í¾Ï<€v C˜Ÿ§x˜ ]õÀóÁem"¯3Öœë(M C®zí5îGNbàí·‰¦¦ÎÛ¤ò@¬öŠ»ÌÆß&üâ,¬,$ýÄ9Vå@øÉo„SÕêT=ŽW;c*†ísÅ–-øÖ"Î!ÖâY‹“㸓û^³‰ßl&¹/‚m6¹éðánAæ‚?ÿÚmÚŸ}†ÃüÉ“xªÌ?ÆÐúôS¤Õ¢95E±´§pqÌ¿öïÿ hµN‰ÈqQUöˆô•wÃw?üðï·<õ”ú"B¦å3)‘W˜©¾éQ›éÑZKÇÄqœœCl-ÖLznŒÁÆ1&õؘÎ3YšºôÃG׿éÓöq|ë8 ÔT5UED|`ø-<|…Èî«V ª®§;-ü4ÌÆPßJS2ÓÚžK… >ëT=IÆ4V=õüñw‰ kUUu’=˜‚ú@ (’ôÛ¥ú'B_úî0 44 \"$ð"PH¶,•F|€ËÍ.ûw—,µ-Xj[°Ô¶ `©mÀRÛ2€¥¶ÿ-9Ò…y¼þIEND®B`‚murano-dashboard-5.0.0/muranodashboard/static/muranodashboard/images/icon.png0000666000175100017510000001405213245511125027505 0ustar zuulzuul00000000000000‰PNG  IHDR,,N£~GPLTE‡‡‡$\¢R•>|Ë7uÀG†$K'R!C9wÅ4A;51864%M7/k´Î<{É;zÈ;{È=|Ë>~Ì3.b-_+Z*Y0g0f1T—4O‘3p»LG‡E„B€ B!D?#H:yÇ=}Ì*X<|Ê>~Í/c-` >|.a/d2l =y:u ;x&P,]=@€Ð?€Ï95C‚B€A @~#F$I$K@+[)V8<5'Q4o:v ;w'R {8t8s(T&O&N2?Î1i ?|(S4n2j5p5o6q7r9u;$J2k76>!E!C%L#G24A13l3mÿÿÿ!.tRNSÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿfÏÙIDATxÚíkp]UÇoêŒ#ˆyÜkˆµ_¤c™vp†oÈð3™QFSpùPDDP‹D¸Ð¥£ÕÔjyµ„Ë¡y¨<2<, *¾hCŸÅ±7›ãyŸýXkïµ÷9'¹·wÿÓœ{ÎÞk­ÿÚ¿»ÏMÒL’Ê'²*ƒå`9X–ƒå`9–ƒå`9XÝ «²¯{Ô“ VPÁëù«­XÂêé*P/X݈*âU1…UéVTáîê1‚µ¯‹Y)68ÜݨÂÍE†Å±šíéiUÔ¬f»KZ«Ùî“’VmŸíJe´z´°zºœƒkŸ–c¥ Uq¬PZÒˆÀšu´ä­Uq¬è[ËÁ2ØZèËçY'=,·±DX-,‡ ¹+À'¤¯;½þºVr:RtX¿uò}òÀú»S `k9Xù`ýÍ)ƒU8¬8¢Áú®S ¬:r° ‡õ§@Vá°þìˆë_N¬ÂaýÂ) ÖïœÑ`}Ï) Öï9X…Ãú¥S ¬?8¢ÁzÄN}¤4•X Ö9X…Ãú“S ¬Ÿ;¢ÁzµA?·ðjGižayû!¬+ ü#1Wt’h°ž)@0¬g:I4XO ä§­žî Í,¯k`Ý•_¬»Ê×ÓE¡Áz*·ðŸz|ªsDƒuyná°.ïÑ`=™[Ѝ}²ôý"t°.Ë+ÕO_VºŽåò®BëμRÁÂr޽³ÝDƒuiN©°ýÒN ÖÅ‚>t±™Ô°.îÑ`Ý‘Sšß™pG‡ˆë"_¾ÈN~žîL¨³-MK Ö…ù¤ƒua{ë"~:XGç’þw—|ãèŽ Ö¹¤‡uAgˆëö\"üZœÛ;B4Xççåw_”¾~~‰¢Á:*(°ŽêÑ`ÝšC´_Ouk'ˆë¼¢Á:¯DƒuDóÙó¨-EëÉ]QMÜ9…éÈsÊ ÖÍö’±Ü º¹Dƒu®µ,çÂ[ëÜö ÖW¬%CsyÌ™h°¾d- 8˜ËcÎDƒuöW϶“ÌMfl}æF4X_¶•ŒÎc2g¢Áúš¥$Øx—9 Ö,%A'r¸Øõdªƒu¸¥d èÕåm‡^\7†+£Áú¡äÞð™6v­X¸Ò`}ÆNrcŠ)[JÝ×ZFV:Xß¶“Ü•bÊÖG_†ò¥©•Ö·¬$·¤œ´ôÑ•!~!O·ÒÁú¤•䎔“–>š2ô_OµÒÁú„€~Ô³vFÊ*FL€h¥ƒu¼än4ÓvFª*†zf¥ƒuI–p‰Bü$ÔLèŸÁý¦ó¡U˜ÀT G¦“q¤n¡¶Î¸¬E‹-5–ŒEÙ¥ÜJ5–äíŠuXß1  °Lè16fñg=-R,sŒkåBIcéaezþóßüÇ•+åN‚ ±…QŒv»2ª’„1†c S‡d±¬M4É~›6Y§R³½®j$Hk+M²ßÖ­Ö©Ôl/‡«Ú Ê kSNV0,oî`m-ÖÕ$ÉvWÓ²L¼ÚB”24X¯PؽB«|[£2¥ÂÊÛ-©BÛÁz‚"Ùí °ìòôI¯X•¡Ázœ ¯Yù>n!J«pXÇéU+ÏÊø8 QÊÐ`ÍÌ4gb5Ù‡fôJ‚å!ŽýiC`NÖoÒv8Ô ýaÏa¥8²‰”a*ùÑTXL§ÉÁ7lf=•‹uìçgT«ŸÜ™ˆTS`7æ¾eö,%1DX3ÉsèoÄcýåm,ß»)´,¬Uµ% ýåMF“ìZf&pgcH°$göY.–ìØŸ>mMxg5,ý3ÙM6Ó/vÏ¥‹P8'÷oƒ« ðŠnÍôn.ë¨ÞÍi3&Êr‹OšILt ”ég<ÙO¾ ûÅÛ zÅ,qc1/˜œc:‚e4…WŽ„S³ŸjÆw'fÌne:¬æL“}m^LƒÕˆ:n G¶~bX¤œµ&Ñ͵Üâ7æ$ô¤£ÜcÇ H+Û q6¾³Z–€S:6QNvWXG>D̈;‹Q£?¶â“`ÔˆCØœF4å%“ìÞY¢c߀ÝÑ5dñcà —žö±jDsFÚr–ÉF‘L]­(¹Á-yª8ÇFêØñ"^öDó»ÂhgqÃN-*÷ Ià æ Å;ú×›“Á°LÂìjô4Xµ°$WŸÂª·fË+ –²FÍVñ«¦à¦†%¤Õ€Øšbó®etX¼ÓAµé Êó ÷wÍ}X+mgyÄÛÃxgÕ&¸ Š‹žÈóªU£ÞBè3uÐ4Ùkb¢6Áª/~Ÿ˜m&0Õ&Ò¬l E‚°¸T¦€gÐBÖ{ U¨AáTXIbÐñT¶fÙ¦o*ˆðú˜©>ò”Ô– V&V¢jŸ½@Z|ÈTb¡‰ÊW¹UUlKk à3¥VuŠ"RÅÓû±ÕªÂž`UåBi°ª#f²®‡T&ëDl®‡Waÿ*w¦5ªŠm`…÷TcÒÕ¨J]öÑ@a­ËDé[K»³€ð*c—5d°‡ vVÝ×ÁÃlcí‘}êñU¿«{ †åÙÞððÍP§ÂÚ38¸‡U|%ûìEÖÓ6=Æ®^ˆ©ã‘‘ÁfØk0-è¶t:H„d 2k«ÖA'¾•A…°êâÚ•ß⟓ºÈ Ë„SaÑž}¶ö ¸süÓˆº,Úšo£eë•ö•"al‹« ØÀ“ùæÈh h‹'õø Úï:† RÚÖ:gè`½ kàÙ' þ1‘ü5!VM. ¥|—pÄB“º²Ó V¸ÞtÑÑa0Y Äÿ˜ ÈøÀ\ãKà ¿«Í:*¢eÇdbžÖÈÚµãþÛÚ‘àmÄ¿òß§`8 Š’C|2ÂŒ°s#éhl²¬ï;$MÁãkÑ8[G⸅0KÖÁŠ2F²DÿŸì”¬?ŠO3*™Fÿ ÕHF0ì]MŠ~¤pXÉvȺ Ÿ“¸¿¬q@²Õx±—C ·…EêÈÉi5Êêâa—Škµ,Ù*ž_ i|u/4 â\+½h–«QÖkX»iHk·$ÀK éU\±ãÈÌ’ÝàŠz‰£Ú­QoR³X ‘^ ®Þ ’¡ZÔjýÓfͪW´à­(°–`EZµ[ŽG¥ Óî€^Ö3Z„}E…µJ”ì–Íõ®Bµd•R½Zɨw•*„ŠŠÒî’¤œÖöPCÛSÉ~Û˸2}Œ¦€¤!E CÌÂɰ†Â ÿ¼A e03ƒ!Þ5=åBƒë¡í ÖóØö¡Ädˆ†j(^ÙP²ÐíB·CtX ú+Eàâô1„"V}ÛÁÒè͵±T) u Ö›*Ýóæ¼ €@ ´·ÊËP÷œA†UÔó¢ƒuO €U®•ÖoÚX¬r­¬a½«À*×ÊÁr°æÖOÚX¬r­t°¾9ozKÀ*«¬·(:ð­2…V`•ÕC°æImëîBu`¡ÕXw—$¬ÿ´±XåZ9X…ÃZ—Ì…¦­[gTb].XëCëôÑ4X¿ž;í5 `•Õ[Ù°öÁ†°‚6‚µ·Ít/sÀ*˶3aímgX÷¶±XåZé`mkc° °7 ³êXÛæ Öm,õlr²MœÙfj5°žSXZé`=ÛÆ`qóï(ÚJë¿m,V¹VV°ÞÉk3A Þ%·v«@X;vìŒÞvîl7XPC…w¹3XüŽ4X;çQ;Ìfw0ug9ù¢Áú•S ¬+q½¨¼Ü¿Dƒõâ~¯+…ë— K¬—ºM€—óë€EHƒõÚ~¥—m»ÖkåÂz9ÔÇ^îF1«¦ÁúŽž/8®dÚ Áz~~”}qFó„)®( ޹`ÉS°>h jèØóH‹#ÃÂ*×ö°N„%OqÝ(ÆNŠòÕĵK.¨-›Õ*ˆ‘%Ó`Ý ˜âûÅÇîŠrÕ„Ú|4Ú”4,ÕT¸qvº`-¬ƒæ²ëlŠ?óÄ@®|A0F²ØaRœÜq_¬Ó yÀsÎ1AÙ)PÔƒ’ãÓày¨´¦Ð;‹¸Ó`Ý)'™ËtÊã¥@)á†POÔÉSÕäçà6åòzXÏA ÆÅIæ2òøA)PJxî9Ô3„#¤QeMnNŒÅœõ°N “ÌešçñƒR ”ÎÆÞ銄šßÞŸëQ@Ѱ0Ë\¥3LHv åãEK%5Ån”Ý% zXŸ ³ÌUzÊ„xì PPÎÀ|Á r¢²¦Ô§ªÏëa*+å§³‹l:;( ÆQ‘DeMn’@üi°®—•ŒòÓ쨗yé˜'ò½ëšŠ(|’R3z@\ âOƒõ€¤l”›f?W‡ÆØAeIÀRL¢¤!eM` ñâOƒõ ¤l›g°(ÇŠf°¥ÒRÖä&¹!Ä¿X€…‡å›Áb§Œ`=H‚å‚õ#Qâ—øì8 PÄAjcÄJY3›W€øÓ`ýL”Ø3;ņ'ñƒh\FmŒä)krs\bOƒõ é?Ø (ø!`P¦ÈiR˜j@3'® q'À:S?ÄD ÁÒØ™š80GeL'ù¶ùqÔë AüKcghâ@W¹”ä©„šÂ?™'3:X7ñG² »I'ØÆç¸1b…×g„$¹ãdB ëF^âHƒc7jãcîLYÚS¤5ÅUéÊpù:XWqG°)xì*þãçÉ!r! G7Ï› ý+:NÓu°vq’v¥1r0<¶‹'lj*ã]x ¸æ.  «p•UwWy°þêÈÁ2fUÑÀú·“/`cq°*ŽV²µvzøa*,ïNžƒe ‹gÅÃr´”ËÁRn¬Š VOBk¶»o,Vºµ<ÇJÞX"¬G+e%m,Ö‚}ŽVʪG+»½ng%m,V—ÓÊþWMfÀÊ^¶º—’‹£åu')˜‹§Õ%Ä„ƒ¬@X2­.Ó>˜ ‹ý ±ÒÂJÿ#°Qõ,0„l®}V°»öu)*5¬h{uÇ ש¡ƒpÜÿµ€¢Ê'²,ËÁr°,ËÉÁr°,«“ô±„[Éa> ·IEND®B`‚murano-dashboard-5.0.0/muranodashboard/static/muranodashboard/images/murano_srv.png0000666000175100017510000000364513245511125030756 0ustar zuulzuul00000000000000‰PNG  IHDR00Wù‡sBIT|dˆ pHYsÈÈý×;tEXtSoftwarewww.inkscape.org›î<"IDAThíXûoÕþîÌì®÷ qw7^‚íd‡G11ª¨¶SÂV iRˆc *SUUÕª´¿õÿ¨T!$TuÕR¥ù±­’–WZ¬ U"HJ Yïc;sÏé;¯ÝµÝR´äcíÎ̽sg¿ï<¾s=‚™q#›Òo_Ô¶ôÛ¶ôÛnxÚfo,•J‰t.½ ¿L@ ~÷å—_ýÍfo›éÇŽvd¶ýÞ3K»GFFw 3ƒ['þ93×Ì`xçƘpÛ53ƒ˜ðÎ;ocyyù¥W~ýêüfl*ª-ž{þ¹ŸïÚ¹¦i€ „€âf ƒ`X;P€!Üs³ð×€ ss‡`ZÖ‰ò‰c/¾òÒòé°mXÇ Ý~Ûžg‡w £¡7@Ô‹pà¸EÂönˆxp H uML ’¨Õ*˜›eM‰ÿb3ÎÝ€j)?YüîS Ýh @î‚Ãÿv"H)¡iš8thnùıíY—@¹\ýá­»o…išn ¸7eˆÉO! œ/ÀM7ꘙ™eMÑ6ŒÂúPèG‹ O¥t=ð~ÔQ„JŸdøyò¦¥”ˆÇcbvfnÿcO<öÈEàäÉ“¹âð-?…a*BÌ`¢NB^qz Õw^2ºÞÀÌÌ Ç4¬…ž,ÛøÁââBV7]<Þ ?Z>² n|å ç]Ä)‹ÇÄôÁÙûü;¥ÿˆÀ‘#GR» …ŸîÙs; Cw%.ìá Dî§K:q[¼ \Íë¨ÀÐ 3=ΦôŒBWÉt|i~~aP׺Þ&½"ÑU>Ýæ@¤øÃ}•H"‘ˆ‹oLOOõŠBR©”Ø>¸ãù‰ñ îž>Qo·¡î7:€ûèè!ÃÔ1=}E(tHçÒOÏÏÏéFC„[|øØj<½ˆEÀ£Û1èÚÝU+H)I‰d\8p`ªüäño­K`ii)¶mÛM?»cï¬õ.éÓîyr‰O®»´Fe¶}ûô”¨jy\,ËÄÁéiVµ# m*•ÏfŽ9Z4›†è€( ²;pêF¦k4:UËß ºRJ ¤ă÷?¸¯\.ïcnÛÌøkãããR†F€ƒ! ܑ༛1¸µ/ð„_ ½:áM»ßÂÂ+vá-€H&’( Q«W¾=y÷Ý;l¢Õ•••¿iwíÝ»+_ÈŸÖ4õC¡ª_­TÖÍ¥1H¶ƒba'O1BEçõ ÐK‡G½ÔpÇÛR(”RÁ| ºçþyÞ»€ñ‰‰'Œýñ÷§ŠžÐä&§&c‡ž{÷…_½HŽt&®^½Ó2[Q!7߉Áäi¿[ÔþµÏy}!Ýæ±íÛqõÓUº‰L&!¿`UUCQ4MQëÈÒ¢j`4Um«+fÆÇŸü+û×3oÜÙF “JZù|~ˆ)ˆ±˜†x<EQüGyyGd‹F­ÖÀ[oŸCµZC,Gr`  –‡~ohY3Õ¯¼é“/jž}Û×@Ó4þúû>¸p~å.&jëš™tc£i¨š )%š– ˲ ÝJf¯ÒC°ðA;xàþ)Ä´®¶ Ç—_×+ÖÐ>)"V¼6ùчàÛZõã8¶¸9—µÂ¬soxoåü”娣޴¡ªJðC Τ M§  ÀÛÜIbض¦eÁq$ Ó„” é4‘ËeÁ’áDYC×A`Ô:€Je ªªb­º!TÔë €µZqi£ilÇÁÙ3g×–_›f)/î[‰B>Ÿ©×ë7Ý{ß½‡K¥G~yôÑ£ÌÌ¢¥*2¤ž¢¯8jH2ˆ[÷“ôþÏeH"°w- ’È“î3HºGŠnMZ˲øïÿxSþù/:péýK—¬1³%˜B@@ö›‡ç¾ŸJ§KéTj»P˜Ažšø9³W]Œ»L0û]#غµ¹Oqû7›ö'Ÿ¯®¾ðúëgN¨øœ™É/ä’ÈÈHHˆ0{ÃûÒ-åþ¶À`¨¨yNèx±%„PÐsKôÇ-6GoêÍÜÿ³Ýð/w·ôÛ¶ôÛ¶ôÛ¶ôÛ¶ôÛ¶ôÛþ âõÚ7$SPIEND®B`‚murano-dashboard-5.0.0/muranodashboard/static/muranodashboard/images/ext-net.png0000666000175100017510000013004713245511125030144 0ustar zuulzuul00000000000000‰PNG  IHDR\r¨f¯îIDATxÚì½€wu?þ¦lß»½ÞtÒ©ÉÝÆ`À6ŒmŠ1¡ý ÇR€äŸJ ¡…$ÅÓ b¡°1îM¶%Ûê½ÜI×ëö:óïMûÎììÝÉÁº“µOš›ÙÙ™ÙiïóúûJP§:Õé´%i¡O NuªÓÂQêT§Ó˜êP§:ÆT€:Õé4¦:Ô©N§1Õ Nu:©uªÓiLu¨SNcª@êtSêT§Ó˜êP§:ÆT€:Õé4¦:Ô©N§1Õ Nu:©uªÓiLu¨SNcª@êtSêT§Ó˜êP§:ÆT€:Õé4¦:Ô©N§1Õ Nu:©uªÓiLu¨SNcª@êtSêT§Ó˜êP§:ÆT€:Õé4¦:¼Àèíþ/j4Ñ¡VŠù®UBÁHC"il–$h„£Í•r1 T‚áVI’ÜEwöÖõJ¹4ƒ IYV4­RNiš6©•K©\jr<‰á†b©-éŸÞQ^èk­Óÿêpê’ôÖ÷þcCS×ê65]‚ÌÜ.+J¯$ËËeIY"+jO²I Ê’¬Ò·“ dÜ=ˆO_bö·Þ†½¤ëz‰ʺ¦é³®k9\žÀù¨®ÃÎâ4€Û Hä’ƒ_ùèÉ…¾)u:1ªÀ)Boû‹…CÑDs í †ÎTƒ¡³e%°]Y¢(EU¸Yd~@æ —%âxYæ§ü„ÆÿßÒ+â~sÒxŽê€1U4œÊ8• R!|¨Œ(hšN€pWìE-âÔ4ö£‚‘úÂßßYè{W§ÚT€EJgõ%”³¯ù«ÖÆÎ•ë5x¶œ‹ós”@¨OQqäõ°¢A प (*3º¤ÈÆÄ Ÿe“щéeãØ’õ¿Óék’üºh à: ™Ÿh5.ÓD`œk(h€ÌÎS¹bÌÑ\("0d´Je ·{·|·ÝVÊgv|ùCo—ÉQ§…¦:,"zÃ~0’èY×FÎ „ã/S‘‹Ô@`%2}'ñ»ÊÌ`†—iBF'fzɘ3Ã{ŸŸòüµkKÝž@ÀsÔ L `B5„r™4¨”ŠP*å¡L P)O ìGÀ؆›Ý_©”ž,ç3ƒ_ýÈÛ² }ÏOwªÀÓe—^¤.=ÿõ]Ñ–ÞKÑ–¿B †/S‘åÈìQ5TõYÊËTëUTçqΌΌo2»ùYÕ—Œ9­Bí¸mRdþ.€ûd[¨zˆ·óÈÔšÁãPÀå~(²äwÃ<l0Ðy®—È\¨0(” Ê<• Çð»qËû5­üprrôð÷þí¦ºf°T€¢7ÿÙÇ[ÃMÝÑžM »™|• 7!ß#Ó‡A éUò×3Ó“„'Õ^dxbìp@† ~ !œpûXP‚8îV%fxâwU–šäÙÏ­TÑmF/㟠2x×å±ÓE÷TDÆÏ— Êd˜9ŒÒ>¤²š,±Ç¢ “ToD†o©ÐW¡)¬²d'•žða¾bQòýàÇþ&é³~¬>6þ!-¡@šBI‡ñlFqšÀ)‰À+fkôB5ƒ.ç@ÒËeÄ¿#ñH྾ÎÄ/^¼±w×ÒÎÄþB§"N•º6ðû£:ŠÒ¼»!ÀSGŒ˜^AU^vüy0‹Ô–j¬÷#: ë@só•>ç °q…¾"ÆŸD @8ž,ÂXºÌ A`Aš™jA?¥2å2(€“¤Uù±¦Xð§Ww<°zië’´­¿ªÀó@W¿î-‘Äê—^Œ·¾Me?º$ŽK*1~4ˆª>Ù÷†çžþ‘ºN*|gc–&‚ЃŒ¶½#¤…GU#„_ j¬šUð’>Ëúlûè&Îè†vDó` YB0(!”عHÆ~¯µ ¦€MŠá€ÎaÅL&3éÜt:[|͉ï®YÚrï+.X>Y€ßÕà÷H¯¸òŠ`ó™×žJtã߀Lß7@ …ŸÂw²í¥'oŠ*°¢%ËñÛPÚd.A­õÕ4ߨŸÅÿµ´ŠYiŒï|v>ðoâGr(ŽgÊpdº£8§Ën¨¨ñ(Іóš9d3¤ó%™Ì@ÿЛO¥ ¿-U*_ËæË}íï®.´¤±sõÛÂñæ?.éµj8 á¦4¶7AK" *ªö*ñÍžHf ;†¾–´ÄCÌ”²˜½gÔÌÉwRïu|ù+wó×b…PÏkÊjÓ¿477Ÿ¡Bœ†ÖÓÙgô6Ñ3ÇúîFŽ[‡ í~dz³òŽ_f\ '^2W„"¾Åe³PF2‹s$Û«/ U}æÜŒáY‰€rÿ¥ç†sªÿ–³ÑrØÑDÍElÉï&º¸ÒrêÕ¦‡Ï¤;Àáu :> °MMw*‰èš4´‰lÉÖ*Tœ”*@)ƒ PH¾Jy/nðqüúŽüµõ¼Õ =~pò24(o ƒWâmêÕQýÝzdÚˆ¹àe(YAæÁª– $Âf .N:b|ëÅÃxùR™€¦’YGo3š¨î‹e¾8O„ö/ˆÎ¼¡Zq|f®úÓ´ÒÅ’a¦Ðosù ä³¿>‹„w/ëàc讣!Fç{Ý5wk £é<ÕŸAÓ €ŒnV0✠"¿@§4ò“xy7ãþŸÿßO¼nìd½C§ÕÀ‡^õö¿^±|ÓåŸX¾bÅ›—ô´ÉË{ )`©þÌáQ¸wÛt´$à-‡•­!ˆ¨¤¦jÌôŠÅøìä;œ%zûUcçZm×<Ú±ÌhÔäƒ<ìævN?‰Ãd'gS•OüÑÕr’ä/SwÍñQùožCŽ’+'Àצ÷‘⺺°“,ì¡^ûøºçàª"ÛaN:ßiTý·ÏÀîáœQÆlj å| 3¨ ä •b¶¤kÚñ«ÿô¯;xrÞ¤ÅOuhçÐdÛÏïÞò’¶¾Y‰µ¿!m ]pV/¼âü.ho Aödþ¨ 4FàM-ƒ%ÍÓÞ×m©n4Ò0â Õ`XoŠ.-¦ðEL¡¤Ò-ÀMm@qB}’#§z#òs?Im;ÚÌÚJ¼Ñü‚õPÞ~æ5iºÏwºû7õºLßÕ²d€ªª`""TvœÌ—áiƒYŽä‹fCÖùL“ RHðRMÁ‡´ré‘;ÿã†Ó¾ž &mί»ó®‡^þÄîÑ?‰5¶K´Ê©J.ÜÐ WÛ á€Ê/tÿxŠÓv‹…<¬l‹Ckc‚Èœ$A'Ó´õK,É­[k1kkT… *ô¸°ê|ð˜¹b™Óa)ч&³™§Õ FÑAx.DÇæâYrå”uO‚ŽÙï=ô²äbdêÈš•äÉü3ww«÷~RßÞWØ CƒîYïwÀdz__‚󑉴(ò ¦5M ƒ&Ú$2þ4šyN•B‚RžšîÎçKû³O¾ö— öÂ-ªÒ® íüB>û®ÁÑ™k†S}+û:¤D<…J…½øíÈäªÙF‡RÔÃ²Ž’ˆâûÆ:R™3…2L Mª ñÃo+vv}æí‘óE² cJÜUGãxwÉb0!Hm½Z"f>€@µž%e+¦VALoI÷Š‚‰á„!iuü2uæ0„‘MH€AÎ@ËgA¤F§a[Yƒ.»ßÇæwå˜Ç P‡#U¶A®V¨Ñõðµ~͇ZóÅ ÃÁþƒ“pèè45à¼õí0plôðÖm‡þåšW_xךÕÝÊ+z¥Ó2\xZÀÎ ê4Wáâ§ñFl²œkb?M‹˜AðO< C©T†3MøâI2‡©8¥•D´FÀ¡CK< !4Þ‹¥2«öôb“Ý?…š@"„lÉh­E„ °Ë}Ì,¼8wR«³}HBwš$3—4ÝuÖ±˜N;b6!€’M7¼Q®,!@UP»)0sK¦†€ˆå¶ÇÞrÞx€BÐ(Ì/ÃŽñjƒŸSÑ Šß{“ŒÈ3’.Cÿt‰[¢F·d¯§vÁøxV.kƒžŽìÚ=0Ü××ñPï’Ö8îö©«–I-ô;y²é´€]º‚/Ìëpñ¿ðýè躖…u©‚ª¸• ©ûy˜É–8å–^°Æh:‚\éG$6çt‘N5ñ%C•4€/§fæÚz™ìóP*J\*«ÈÒ¬Œ^òÉlA‰TtEvú€;ûI®p£Å@ û$ž$Aújš£=P“XRd3²'ž¯ ëÝ6¿ º}j,õ‰ùgS÷ lSxm´Mï[8ϦöojºîƒßíÒäñ8tµ'Øoc8ñAÕÐt@Ò¼±¸ßNüòƒ÷œü·qáè´€úëñâ?Ó2‘!, €^¨lÁÙåPBwÆ®U§mÈ–&fœÉ•ašãÐ:ªú*;éòŪû gôÅÂ6¬›LŒ‘.±õél‘Ûör¸C~b,Ür‚µE®ô#:.kªP„’.#°Ì!:(„ûø¢U¬ß³œl¦QÑt‰ë£æÛë­#‡‘…¢%«Ø‰’ž‚ªâîMâQ÷éú¦©1N´-m4¬êg":é~l>4é j¸}8äçaEi¬só„ Þ¯ÉpßÕ½§GÑÓ¶Oè—á…ß‚ÓZ¯Ô§§>‚†äÐt¶ $ÙÃ|fw®XÛÄLM O4ãñÔ ·9ªB4¤@¡Xfó€˜>•+²íÜ@ÝØ‹­ðËK¶>…®œŸµlÙåvcO0œpD”D~€€€TþñTžŠ ˜ùý’Šì_rùWçYäáóGÝÍh^©nuÿÑt/óÛ†¹NB±|¡ìµ>Sx’j3„©"5Mõh þáIjZ 0œAÐ-‹ÅOb”¢hì‚_Ý'=uòßÌ“O§<;NÖ:¼¯ú lóƒ#õI}Ka2ýÑ,Jý «¾KšpÙª;óìY;UUbÆ\ÖB¶¾ìªº›Au•¤|Õ}záTdÌEÈFæ¡·4äÃ>ÉKõgmÄô¾…ÛeѲejNj?9»T¶we¡iØ âb|k.”3yãùÖ¼¦G_¯brk;·úo¤ëÛ¶ÄBb²»@<†@”=9•)°ÙAEU‰˜áx÷õa|qñF²¹Šó]U‚‘gÿÜó^Ý'=»poëÉ¡Ó ¶ëqö-|ñϳU}|òƒSYØrxå S¨Ø IÝx_º¦ºƒ†ê*Äå‰HŠ-i ³àwSI ˜H±$ ƒøòØ3Ìæ¸ôWÅuïl‘ihdGL[qpÉ ’ú_tZÀ³cz;Î>‡<ðVKÛ¦iËþ¸ó‰ƒ,õC‘8#1£þïm ÂKV7²ÝÏ7ÊØ$ù;ƒ®(† ‰9(Í7[,ãK/gÐH%¦úÈXMƒ2ƒL æ¡Ë (†#…ƒº4Oª°ßþÕ à¾'ÉÇ>oqYd$Ý¿wåçƒá¯ >~´Lƒ…óT܆ \.³¤   0ó'Ñ|ÉŠ ‘PÐ¥Ö“@‰U™b ЏMÅÌ‘  ènŽC<`°)áv¡@uÁ>çB?M \Ë,¦ôk¸ø3 ý?ô‚€gÆtŠýü+^èHƒí~r0Ý÷ì<¸}€Ut’‹Ôò¾ˆ—®IÀÒ¦}—l»Ü¼cMV´Eíò\?»%Ñl«Õ<Îñ©4ŒNç¸È&@À)"È`fýéà–æ"TI}Ipö‰š¸s¬ý¿<€­ôû$éØ`}o9ÔH½GÆ&»¾9²cîÜ+As´Ú¦T,²GŸk(L‰`@)ÎYfn:š ,ŽLá1&2EþU.öÁ}©ÐŠLJËniŒ0デ‘kuš. &wÝãüóú>‹óáâç®].åú}þ}Ó žãñåÞƒùïäC¥8IØã8<4 ÛŒÁx2Ëδ5ÝÍðâõPt\‡ùf¤t'B®!¸‡{•ÃÍZ&/ä(š“^GŽAÕ̪“\f†ä€n¦À³Ÿ±¡äQú»Ì‰ÌâÍã·ú zcûdì"Jw }6£d§{F©Í3¨UiÂv\qÈ#WŒëÁ‹‹‡ƒ ÉL&R9hmˆrˆv!ÑKÒŸ¢.ü;f‡¤VÜžª1³…']T;+«¯É*ÎdË'4ŸÀÅàòm×­xa…_èp ÎnÆ‹\êfçâ-~®P†ã“ixöÀ(t6¨Ð‰ nØ×F•ùˆi¨ÕWW‚Ôÿµæ×±Ç/kÏbbê­72“å„ R_i=¥Ö2̬˜C}‹ûrhPr'óøiNB‘\þŸ§ïW¨cåö»ÖÑþT˜DüÄÙŽ4va(ÄÇNâý$S@t°Ù„”¢ ¤ „ðz;šbxïKvò÷=D”̺kÀÑ’¹O7nOÉ=öùŠsÝóÀ×!8–œæ0DÀÛ³?Bx䤿ÈÏ#½``ë¨~ξƒ/ù^‡˜ìd²bH!„Ñ™D¢hos²O<¬pQŽìaL?F÷Õ$CšÑ‹MRlµÊ Áƒ€u>F¾d‡Æ®ÍðÞíÀs^pð‹ØL ØýnfЙécª1dQ2“…étâÑ($bQÞ.†wŠ‚øÖ1Ì©Œ÷·˜/p¤=ã‰d6Ï÷ƒÏMV¡F3å[„Ítjʱ (@@u:‹Üuîâga ŸFQ ÈTÀÅèDsÍ‹‹üÚRÿB¿ß¿/zAÀ–Q­/býoµ„å\@däúKyRc·gg]4ƒõ]qh‰»ÓO«½êž[Èq¥ÊF× J:4šäa¶C(= m*ØÅ?xLßeÙ~Ë.€ÉÛw‡Ý*lË:Úà4â‘Ùº|tjò(¡›  3@–¸.¡Œf•Ñk@3+£-ke6($á´\cJ¢²ª­Tf+_¡–j/~ö[ç*š(Ìô:µ/à‡^·RÊ.ÈËý{¦lÕ]ÓþmÌáÅ…ÅL?·ç¼Èáß4b&S€gñ –¡HÎîM`…¼ü¤>Ìn¸ÖÁXäè:2–bSDÀ(ÔiúÁŽD/³Wi’+rP¥! J½àqºT~Ýe ªLNªx#2y ‹˜†œ~dÇ`5Ç£Ž7^7z L£©CEOÜå¯4)Òs»šê»ða.U¶ý½ë©Vb8gÌ«ÝÜЇÜ'd8>€óï¿~¥tÊw~ÁÀC•+dY¾•"u.‡Ÿyµ³åóÐDë(‰ç™C£ür÷¶5@_{ש‹wÎ+Ýý¤¾Ÿ†@’ŽÂƒÔä‚*MepY…`(h‡e3ÁrÆÙ¾/Ó Zä³ì@$¯Ä·ÀlÊI¿b¡`8/e4<ï"µ«”Ñ< AIÙ Y£"ݾÆp€kýEffsÚòsJza½µÎ{Lbî €tÉ_Ò[ÛøjÆú=8ÀÓ'ç­~þèÍ, …ÂßB&º²–ÃOše¹!v+or_MSÙ.~Ù Øý¼Î½*¦7m|Vy-U–À¨£Ë—¨ ¨ÌcàQåÝ¡ÑiÎv#ÕŸòø9!F2Êr­‘}\™æ•Í“w€™ãÙí@÷µ5½4A ~!—7ÒzÍó¤’[7 DŠfP¼¾ÌíÅЖףS3øcªõóqÚ‰ßù®w9,…mfý Z ú Vû7ŒMfsù6\üÓ7¬<µó^0ð“ÍûÔî%}QÕÀ?àK®ÎÀ\¦xìJ¸qîY¿ALO©¾CÓ9I‡Ê†éE £ª¬›ŒI Ý…vFwcˆ=ÛGÐþ§D ?*Ô3 æ2a…A@2½ân[^dvÙ«îKnæ¯ý½äøB,WƒÈ VAVárZÚŸ™½Ï³gŸ›é†ç?ŠÚ€¬9®æªÇxNŸe½xî³í'‚AQ3‡”5_[._cþùk´¾vêS74ø‚€‡ús¯ C(ý¥n—êï²ð=˜Ë!™òëç!á}ÖSJ1™ öØ}:¥¯f¹%%)\¡'1#ô&BÐÑ´mz’–c©<1 ùHP%ÉUd# ¨õn p›V%¡ÍðP ^_rÍf|,íÀÉõ/—J¬Š¡(³Ózb|îìŽc.•ßëä›KÂW%ÕØ® 0t£.€4»H¨Ìâ  å}8{Ëõ«N]Sà÷Ju„B‘[Uy•(áAX&† 퓤Uús¶"Ç7DŒB²ýÃÂx—Þý½7ÌZO>Šc“º M;†§3px$ R}C̬Ô(dE+‘E2]QÅÛ>ÜžråC¤¨Æqd!ÏÀahÑ ªõ"8xü¶ŸÀ?: ’× °µpƒ}A‚¶£B¥˜*›Í<Uj0c `Vo¿€Ì×oàÝnª*¹·F‰p­ï5Óøó7®:5MSîxx§ÜµlÕ_¡¤ý¾ìA? ÆOåJ0‘*BÿX ÎÀÈT6­l‡Wlì4šy¨ÆµâM™K¿³G²ÁWbÇÀ8kd#S‰.3´B =4¶õIZRóªo§¢"j:˜4âR”Ù&É’«ÀÒXD&§_¶*cÝj¾?ø1¿ì½@ 0ãcX @QR©ÄÒÑD ¸~kcÜ(ªâoÍb¯ûí3ßõs™æ<]˜.Ö6|@8†©Qe×{n{Þ^òç‘Ny¸÷@òÜP4úUVÖ¹ßR±QªŒ§à‘íƒPBÛuI[ƒ©T"¡0\sñ ¸`u+DÃþ×Á#å…;åçÕ÷‚1õH3àä8‰% ÀàLÞð5 ¶ ˜Òœar×]w…éTst!Ý,ØwTüj;^4¬Ï6(Èžð äzÉ`D‘ñÁ¥ò[ÚKÍP‘É  ð`¹Tæ’_ú ò´%8E¸J2›æcëÏæà›ËÛOT®ŽUUqŽ%ÍFÆQ „ìÐå,Ý‚ ͼ±‰4LNe «3‰D”´ÊÇñënX-_8NxntJÀ=û¦ÃÁpäs(ýßc]Œ7í•rì˜äqùV-i‚Φ(Jÿ 'àP²§š¢ »¼³º›"¬ ÌæèójÞ›h1-;¿MJkMœ¦Ÿ Jg‹¹/½lšFƒNp1{•PdxÉ.´5ÜŽDç% zãYÝ}5 jš0§\ÿ|.k; »[A¢_§€˜sò©”9/"à:ß} 6€PÊuÿÈdðYS¡QkSŒµ£¨ýM#8O£ ÐÙÓ ápÈu,? _Í0 ’§· À3;`z&ž³®}Õ9¤å•p*úä›VŸZ¹§4üî@òªp4ö}”¨mÖÅø†ú•˜BnÛŽ¢dRa}_3ÜùØ844W·zZcpöÒ„›9$¨RýÅ9½ø48ÅT¶MhßSù0Eh`’úTÍf¨ë²‹!-›^TïEF§ÁDØÓn#®HP-áçZgš ¢&à5D§ŠQmF7™\X§yæ0”Ë%(ä³<þáÒö úÚéÓJ`_£õåD* ƒãIèhŠCK"æ$yÎÍuÎUç®ÃÑ‘8r|œµ@ê!¸jY;÷"8ŽÇžIç!‚ëbÍÍ@º—æ=®Ïì>[÷‡£G§PÓhM„ ŒçuÝ]Í´?6ô&€SÊ!xÊÀ¯wµÄ≯+Àõ²ÓƒÇÀê[Ýyœ«ÐÎ^ÙÆ/Ém÷î‚ýÐöc0™ÌÃë^¼ÚèÑjúƒ¨Òu5G¸ü—^ÌñT ц"t¡¹@yê ^ðc¨ NBTN„¡ý(ý ¿ŠxcJ`+/„ëdÎÎóÆìF˜Y‡Vý¼[Í÷‹_Ž€7üçNRÌÔHÝRìÞXpGÜAÄA£hŒGíAWkiÖò4‚ð”Ö”HÌ_Êç¸3‘¬£#ø '84–†æDlXÕ]mF£Ý *ô|÷LÃÏ2¬l™(@zѤ4rì{Cƒ‹¼ùÖH/4Ì—NIøþïžnZ²úŒ¨Áà«YšAmÆ·Ö?´.?w9\vf‡4òP_ú°*Î/óå‡U£9…p zŸÇ¦³ðÈ®AÈW$Dÿ(· ¢fô `Ó‘ê"ÓŠÌîµáù³l„æøô‚9 "nëÕ|4ð0?8 `ÀáÑ,' Ë¸½W© !ªä‹‡í†%ÞÐ]•Í2…"ì?6†&S5ÈÌHD¾—©d&’YX‚Ò¸»¥‘%þjÇÙ†â3jˆF ¿K €ßoX¿'º÷x:c^£ÎJî($Î@J]&Ø{|‚ýCd,éhs7ôBOG“‘´¸Q¸÷¡mƒðìþHOL@ß—]ºV­è0q€é«¸ù_¾y”[h>™’pמ©·F¿ŠvuƒÜ`@Ëä‘¿óÑýDiþÊ –Cs,ÈN&r’Ä')»®;íh×·wÜ–üÔ¾*_†~|© ¥ÐvU¸01R¹¢»[®–ØbhOtæ¹5ÉîõGŠe_ûit<·Iàçí÷61þÐLšÌkXŒo,» —Ëâ”CÍ(KÛší†UÒX˜Ó~”é8‘.ÀjäK ۜά£1 Ó3IvÎѹ÷ ô"RÅ$I\jÔ28> 3!üÝ3Wvq¯ÿZ¿É?¦ <’Ÿ‡L:ÍÕ€K;Z]á3k?›)ÍpÛ(ªùÉ¢ÎÕwbÔ€|d&ôu¶@6—‡#ƒ€˜l­—%g¿ln#€©†›÷ ÃáÑ,(«¥²ÉØ´-yü96\ÑÝL(zùåZRßþ“…´]Ò(€úèàþ¾–9¡>[Û°®ß 7:æ€chšxwuá¯;Àb†ÚÒÙL†C|­h;ÛÛ{Ô}"YÒ²F|%%àø4㘅BžçëzÛ¤5Ø?4y<6· à.Lm XÚžàkšLÁÀÐ47FaͲÖ>È>ŸJç x ÇfP‡àü ËØÎ§ñ cÈðœ\e>xQªSsª,VP²žOU.€¹L7uAàþ'ûáég —A@ZÑ =MÐ×Û ëÖ.±}‹ÞpJÀ­wo‰÷¬9“¤ÿõ^fŸË ¨µžˆ¥¹â„ƈH;Øz`iI%àbPK‚’ï€Þ‰²¦ƒŸ·ßêég€‚WbK¶©à•â”DÚE¹¢™Éÿ€ŠÀä–³||VesEó¿×Î˯۹lÇgÓPBi¹º§ƒÃhU1zá3Ý—cSY6\aC³´8ŸË°]Þ×ÕÆÎº‘£¨¨T(28Pôe]-°¼»öôÁñ‘I>ö4Ö,íà1þ†PSˆŽºÑŸ½®Ùd˜}=ÖyšçHÊœ_À¹R€é>IÁíwî†cýƒÆ—ç†k΃3×w³bþæçñ2ÿîík¥ÂBóÍltJÀ/w޽*ÖØü]TÛ}æŽÔZOÃ~óËG=¸ÑÝ[Ž¢ôÊB07%¿ÛGjz8(³4«èn¦´%´‡i™É¯Úî¨þv‡—Y³àëòó/€É̲Gýpi. 7À:z53¼Ž¹Ð¼32ðã§úÏnjïþ¥¢ªËjÚùàfj_Ÿøƒ©Ä¹|Ýq Nƒ¦F¸I'WΌWÙCÙ¢sÔGŠ»·u389)ìEv1‘ ’Û¡h©ÿ~‰F’ æ·Öx͑٢ýn-}ò ©MÒš€æÄtÔÕ—4&Zîln4@¥3õù'‡é8)Ÿ ž•³!9Ä©¦“Q5(±¯ Àª–aÿÐ4$9{’ŒÈ8`:k FCªä¤ºƒ\&Åý ûºÛ`eo‡ý†'2¨¤8ñ‡œy/Ú´’ãú²Ò3r ûŸÕggtï²eL"¸mÛ?;÷ŒÁãPD ÖòÆk·‹Ï_nE(p‚À¢Y蔀W¿äéý·üîoñ†O £(Ö‰ûùj0þ`À™‚¨Úþò±ýpd8‘8eŸ\’Ù`BÈFá‹mõY´Íe?3À«¸œ…tlƒIŠlŽxãþ~9>Ù.Û hÝ‹  %lÒ=j³i Ã-šæQzÄ&!«—tÀD2Íi¼áp„Y!§D*Ôj§æe&Q­& ¿ ™it<‰ Dj¾ j(Ì>:nõvŠÉ²(泬}45% ŽØ” y(ä²ü#”ØC…>K»›ù¢‡ÐDèhm„ö–8ž†Áñ ôâwºçšÅLÀZ‚tíãS9ز}„–â V/oƒ¾¥-ü{º1¨È»þß:鎅æ¡ZtJÀWzûŠ—üH †^êUç½’ü¹h³ŒNfàÎÍ!YXú‹Ž:YrÛ÷!”þôR“ýéh ¦½ö²ŽóÏÇ_ 8ÌL¾RÑm @®vø‰‰·˜·m˜}?­  íÆ8‰ÉºY_€·C0@ÑãOÇcPkïš;_ÚÑÌ >T!s°Ä’ÕJÍ/Rà—?pd,ûÑž§ß¡Ø½¬†ìlHÒŠ2ù ›TsÏ$nœŽ›ùüF…b¥q‘2ÿL.&S¾Kšùÿ-‰(MrÉöKÏ_×-qÕøù üüü½ä¾M«Š[qþžw¬[œ£ ð¿Ï ]on¿UQ”æ¹¼ú®æŸslkÍéE94<o†ÉlT5à0¼ÈÖË-áK¯ L.JdIcE4<ÑG8¼ÈPv j?‘Ñ=Úèô"bÝÒ^èºË¯à„m楢PLš€R)º¥¨j«ÕÒ³“ûyä‰H½?<š†Á‰¬QW`¶·Ú¢QùH F+WÊP)WTŽèfFc²‚9I4VeNÿ¥´`]p—âl.g®è‚W\ºá„ìßð`íï©Dø*€] ÍG~´èàÓ_¿=´áÒW|*ÖØü—U’ æÖü¶p€‚„žÚ7ÛŽLBEŽ=U”¸.;ÝQác!cp‚9²Oµ”÷J¯-/Uƒ†¹ Ǧ&Qª»# ~É?Â2Hµµ 8Ϡ Wp1¬øB[QÚƒŠô2t£4M4˜©¹º?c{™¤–&@޼TËÇf NbÒ·T (ÜM™4#+¢  ÍW­¼|Í 7èùdvrïBYårí,š3úÚáòKÖ‹©¼®óŸ“Ñ}¶õì_Äùß¿s½ôÙ…æ%?Zôð­»·.ï^}Æ@èŒù„óÄœ€j-ÀbºJ’-ûF`Gÿ ›2ºÙPz,ÙÆœìb~+Þª "°¼ô¢3OqùüME`ZqÿXØ“°PÔ\ÑÛ¾s @²·ñŽ ’c€xÏL3ƒ€œk.ö·_r½Ê3n8­± t‘—ÞtèÕbv_Æ7iÞÐ1pªP‚cãYîàT(µ–&DZi(DKecV< ¡iXˆYCc0”p{­˜†—Ÿ»Ö®ìtBPƒ‘æoˆÛë~‰‹ï@˜^h~òÒ¢€;¶k¢­ë²,GækÏÕÒHM¤Üò­û'àñ}PÒŒ‘jeÁUÅÍü^S€RÔKmb|™DÆvT{Cå÷FD‰_!0 b *&C“ÀÛħ+èðdþ¹F&û K›0=ó¬õP’RH*Áë^¾žË„çÉÈ'ªþ[ßàìõ¸^z|¡ùÉK‹®|ñEênùíÍ‘XÃ’dvÈó³ó½ŸgÑvƒßl> “y£§½,;’Ø KY «HŽtu$¸‘©×€LJÒ³ìÖ1ÄãVIz mÀŽÆ¨Ê‹µ é«ÃŒN]¿7À=‚põ`¢QÔˆ Špiày©)}7—emÄxMö÷ÖnUuù _3±¦CY4ƒÚÀÎ#SœšMCµQ¸rú/9£ZBv(о'©p(Ë•žÔñ‡’øÉû=ЍUQFJfêiÀ+_´†Û°ÏÅèó Îú=À_âôy¯ÓeAiQÀ—ÿ÷ËμðŽP$rþlŽ<¿õD¢@Ìqàøüðž]0•W¸ÿ¾ìÔ’‹‘ݪ½µ yÐ H)Rµú_Sê Z‚³Û ÈP1: ‰iÅUYƒ ˜àÎð•ü&RÒvÑÂ@K½IdR2©ùg!„õ}ÐÕšø?1¿½@m-Á\&õ¼P®°Æ2“¡oÓ09“‚‹ÎìŽæ¸«(ˆö¡k"‰OæÂØLƧòÐ݃& *ŸÎC6W„Ζ0t·'N˜éO4iȜ߉³ÞµaqE5üð‰þ×'Ú»oUUµÁ:Y¯ZOwW1¥tÑå!`ÙñÂ6d'þäþÝðèŽAÇ›AQUƒÁ-Ý­¸•¥D%˜âÖH¦wG Ä0ž"0³âÉ ïh$"VƒËšKúó¶Šd'#¹´—ºïv4Šª¿å$Ó̌ėÁIÙö×5HÏLC1Ÿ‡¾žvT¿ÛÀ·fwú1­íjƒüé Dq4T–ŸAð9‰QйFòÛËè– ÕªY€€F¾`ÇBó•H‹>~Ë”õç¿äc ­ÿ ª¸·&ï8©€FŸŽ'a`$ CˆôÈ!\ï¿zI#«¬q´Õ—£8•ÎÃ-?ÛÊM"BŠZåÙ™×úóHpR%) @5îšUdåe0ª1´—•ࣘ~ë7hÙb|EÔ£˜êà%ŸÌ?»Áˆ,šAB©¯Kí7°å·IÄcèÎKn‘.,‰ÌDq÷ôÔ'Ò,ëlåäŸf~sa.Õ.`°.¢¢ÍÁ¬5ÖÓûCÀG…D–ÓÐLÜ©y ?͆–õÃÐXV.mƒÎŽF' z¿.ÿùm¾¹Ð¼%Ò¢€ÏÝö«–ç¾äŽp¬á ÙDïã£)øÝæcÈðiX±¬ –uÅaUOìퟂ_t|²kã;Ûñ÷‚í/&öˆ*¿oÄcP¨‘L€’`è‚øÂ+’Q;ÐXÖ‘pš€ÌÁü.÷9îsõhžÏ5^üNXOpx`–/msÀQ–ç­æ‹ÇùŽ;`ËΣÜÊìªË΀åËÚfÛïË8ÿËwoŠ Ì^6-Zøï;îÝ´ê¼—Ü©K†ñ·ì…{ž„ã#4$›ÍmQˆÆ‚ÆðSøIÅWí*:†éÙöuÆá=¯YÃ6ïí÷ì„'vG ‚Á¨-E²½ÿ†tv¤´ÕÃßë mèÁ2“™&ŒìUÇíx¾¹-Ý­ýÍudº€ìrÐÙ¾0†PÝþÛqz@–Ö`ýHîeœÇɉY6ü ^2B€Îuô¶D¡+6zü™ỏ0¿ß÷5Õý9@B›çþ³mCªú਑£1H hiŠŸPìߺEŽŒÁOî~§aI{ÞùæK¡µ%æ« àô0þùƒwŸ±xj-|÷¡ØÖ»üËÇÇ2Ñoüdì8”äN»Ã6!‚Š„#óð[”C_Ôòmh‡7^Ö­þùƒ{á±ýŽ% ŠTIf¯-Í*µ.8Íï©H…"”öÊ7Ñ–SÌœ˜Ãï2+äjÕÞ:_:ncIJZ@î¬?1ìçŒìòü[Ü܇˜$ýóÅÚN@Ò@È×qVo³‘: ÎËoͪ$»ðù÷í¨§Ïûyž@@D!Æû†àбq£)pÙEkq˜kXðªß¡Ãï?÷=¼ Ò398cýxõågAï’fW=q?š\…°ea¸ªš%¬imQ?uß¾ÿ×Þÿµm•Ñ>VíxÔ0³¹ÝŠË‚Ä&UÝRçéM?³¯ Þ~Å ˆe¸ëQøÅC{@ Çqߘ-)Aˆ™»òó=ÑÅ€Äô-kåmGF“¸>+–wTU¹éÆ3¤o-4Y´(àßn¹£}ùWÞöý_ïÅ/<Hƒ‹ÁB! *>+ÛÌdyþa¶ŒKkk Á_¼a=´7áׄ_#Z¢M p¾u¸Oò0³kÙd\ ’:L ‚…8xG­àª:pG ÈÁHǧW¡÷wU3¿`Š˜Ošçæï’–a€—ؾÍç`Y[Ö.í4Öy¥<ø3³÷ósb~¨fZ jɵqÏ£{`ûþAèh‰Ã5—o„6!¬x"”´õßn…=ŽC:•ãñhØ0’ll< Í A¸îê `íÚî¯) ¼ïk¤½˜N.-J¸å7[7Zûnÿׯ>ºîðpÔ`Ä ¢=Ÿh‰p¨$HHJ%²ZtYá®k/î…M+›à»wícc.1õÏÆóxCq#ˆ*ÓÜÒÝalw5 ¿“‰­¤àÔcpcPí¢ÿ†Ÿn¦w%ñSu:9f€àÀ©)¦r%c¾PýV*e+y8om+KŽÈWå‡jUþ„4æ÷óUýE†ô~žÍn·Áû09…Ÿß»ŽLÁEg/‡—]²Î¸AþN¼YJƒLC.W€Ÿüj+ôLص ËûÚáìu]Jf^wÝ…ß?Uè/IÔ &}ãží× ¤c·}ö{[ãéR»ÄŠL@5¹±ÅÔìØ=pí9õ'©í­¨PØp”„•ÛÚÏÌ*¹Â„t³TÓIgŒÿ§¹™_8IŠ îäîW7ø´*)tIuï"Y¾Ñã/K>ª¿Ç¤‘lÅ\Qƒ¦¸Êç_(h®·€TÿR. ë—Q¾V!,8O{ßg›Ù4…ùøNÄñ7ÆõžóÓ;Á=íF)†k¯<Ú[ì£wß¹rˆá¿öƒ§`ë¶£/`ˆ)ÂU9ü’Û@ÛP‚@š:éj¢ç_ +ºUGÚûæf\S<ÀZG¬ÐÞ»\: kQúoXÞYÅ´¼ìY÷œ™ °¶õ³û­ï|¥ù Jk=Ý–=GážGvB*[€Ž¶&xý+ÏÆxØñˆç8Çﳸ”›š˜(®m¶ãô€ûŒÉZtpË÷¿×²·¼î»ßøÙ¾×L£ S[.—gx8®†æ°i/:åP°§5¯º —Gn%i݉@Alö‡G¶õÃàX9 òÐðT>?/óŠ A@ö35á\}—Çß-ùçG‚[ëHú“¹B/n @²ÁÂåðF2À#ýÁmÑ}iF -ƒ c¬— ŸMƒª—àÒM+9Ív>Œï·n>’¿ãZÛÎò«úni/.×’B¡ ÷oÞ » ²côê—mDÉÝSÝ)h–cÎåˆè38ý=‚À‚"ºèàÃýhÏ~õ¯þô‘±k Jghy‡¿ ]•©_c[KU%ˆF"°qugÿNey(§î–0´7ÇàÂõ­\¶}ÿlÞuŒÛPç_£û¯ìôfª­`ÏSø/ ‰:%Í£ÎW‡ýÇ¢'* ÚñàdöÑ@{'}n{ßëô,»¤µÀÐ`@ÁTÔ`3“œBéߊ÷°Ç~5NH 0ÿø™ óðg~{ûðÌÄmèr©éoÜ ;öƒUK;àúWc ZkßYÞïÚº§?FH,¾ªE‹>÷?¿½äñ£êw~toÿÚr@’]’Ž:ö\pf'NíÐFu<„í†áþ-‡!màˆm߆†ìÈdŽËBÉël ÃË7uÁKÎ™,ì>4Oîàa¡4 ÔPÔ¨ÓyªÊoIÓhFÈ*vœÞbfÅÇî÷Ëp¢ÃZæid$³5L? ð:ý¬ï¡ˆÄZ‚¨î–ÙHT. ›ž=-pþ†¥v£_†ö¬÷[wÂZT3¿¸í‰$†àù¼}ßüò¾mÜóàí¯¿˜‚Ús¬9¨!ý‰¨[ðõ#'‡«jÓ¢€¯ÞõìÜ•ýÆ~½7¡Y]·ˆ<ï}Ý ð‘›.„%F*ºËw>´~õ¨‘/P(‹Ìh1±›í‰\y^7\zfÛéÔòÑmÇ`ç‘ Ð¥€ñÒ“¼\bÍ#HmUÕUÈC}éšã*‡,f<ÿn¯¼àÄÝp Šj¼1§…3§3Æ`Ö5€Aðñ¸"æSu©þàZQHe+Ü\ƒÇüCÛŸúçE‚xé…«¡¥1ZSÂW­ƒçà˜ù=ßÏÆüsIx€ÙÔ{ÇFÓpû¯ž†t*W]v&œ{Ö2°‡ ›§º?‡ô'Àé¥GžWfš-6n}øÐŸKÿã3ßz$2‘¬ ÇÅÁ¶_ñn^º©þêÿmBIiT¥àËw<8ƒ`8Âé­¢ÔtTh£Âî¼Õ-hDÙICÉ6cS98kE× $"¨zgrl&šDÆW C(âÞöä KDZÅ.Öq˜Ìê¼ùruäP˜«×Hú¿ãš °i]+ß]ŠYÿäw»à.ÜŽòû© $tèmó>@!¶T*B>—C»_…X4ŠÌ€+Ð4¸êü.Ȇ‡œRCŸ„mûǘiÉa¨3ój\< 8QÇêV[Õˆ³†}NT1“CÊöìK ¬ýíŸ.íÝôÒïÆšš_ÆÌÕi¬F6îyô ×÷kJˆG¥¡F 3ˆ°$-C¡{ü—¶„¡)0¤¼è)— ñ·ÛÝYz øÈAG ”-=¦]R¼*T'´ëOÓNpogþ 稙²+Çëô#*k‚v€ÚR¾d€L l à€?âT¦Ä¹´šFÍÉ¥¦¡ïÛ+.^‡šHÐí´æóe|ŸuÖvµB}³Á,ߟóÏå°¶¡{rmËüjŒŽŒ‚V.òøoºæˆFƒ<Ù\€B#'ŽÌÀÑ#Ã0=>ƒ¸¼`‚²öùîhæožüöûJ'­f¥E_ùùÃ}ñ¾³¿18^¹rû~à û¢szð¥ `ÜØg÷ ±Ä§¤ Rÿ‰v…]†¸©fgk3JÒ€›ùÀëM×k‡íÀÑD5žúé3œQ}ëñ]N@·³ÎQÂ[€C…@”âœÌ”]ÇáXÖƒ½ÿt.d䋆†-VÌa¹ApZ¹ ”!†©t‰5 ZG)ÓÅl6®í†sÖ/ᨉW­·—Åù,¶¿k½¹P+Éç¹2­åRýÅÏÂö)úÕÇáÉPÂ{C¦ß¿å2èéLø ™srãPajz: ÷Ý¿’3)~&Ò86 (~¥7–ùàæo¿¯p’X«&-*øÒç>Ùúócëþaëñàû'S¥ÕÜ_¸i ÜxýYÐÖv4Õ©ˆæ >K#Ä=µ­žÝ;l$)ªÍDbQM•].dãUÅØ=û’@f8âŽÓK®c‰Z€7¬'ž‡8,Ùæ’Gúƒ=wÀKÁò%ЋX6-IS¾?š š ¢v"P®ldâºB>´¢Š æ–øæÂœ¶¾ùg6{6æ¶¶9†Ÿs˜0M§ ððÓÇaûž˜šš‚7]}>œ{ÖR߈ÞVv2—s9˜›=û‡axtÚ K*ŒôADÊÞÚ˽ïño½7û¼1ÓÔþÙ-«þîàLüÅ7¯olÃË.Z›Ö¶CoWÕû(Ú¬o×” ¢j×L¦¿~p/Œ'‹EAªbÂj°WLÑui ³™ëUsÜ@;‰Ç³Îß’Àîߨ6/ÄàD ŒTg±HG44óM¶}\>?ÇŸäú½Š)¡’êñd‰C¤þç33üÆŸwÖ28kUw5ãûIvs¡j¸™6f·>×2'˜¡Ö­é%Í®ík1¼ul¿íæå€j0 }oÛ®ã°cÏ1xý«ÏA *Ìy#²^l&GûÇñ\§`hdJøìtîK¾\3sH& ÷›ˆZyûþ¾{òyb¥yÓ¢€oüׇÚ>ñøêOMÆÞ]Ùá S¬’O «=­18cYÞvÕJîÎCDÌ38ž‚ß½*h Èpuæ¸|sèFg!Έã1éeÙÙÜ—rHê’ê 2ø\iº€÷œ„n¿Ôk€@†<ôbx°ÊàZ/jÎ.Ç©@ämi  D*ÁèL&§¦¹—eQ^pV\¼q™okY÷_?›Ô÷Û®&ó›Ÿ) üijG ÂÆõ½†æ"|?óÏ7j0k¡yÏ©zÿÑhI„Ñ$"³ÏÀþã09™A‰Ÿ1°üÞ*<9?XLÐrpp`"² ?¨€‹¾õ¹·}ü±•Ÿ>2}wE·@R‘èj ÃÙ+šà×®ƒ5KÁHÒï<8 ¿B ÔWú‰Î5Éä +þn\½ÎóWµÀÄT&&“ ©!H¢&A±_…Ë…e—}O™zl£s/=Ýe÷W9¡†PkIâ4cJ9ž6£¢ ³» ÀÅýb4€RÛF&`:W†b!Ça*ª…ïëmƒóQ  4èÿ ã‹ëæ²÷çÚÆÂÜèü<_æ—OÈ!(\i•d×£Edd‰øà˶m÷ 8:Æiã©TJô.ÓT €&À¨=qÌ+¾+8£êÔCÇÈÈ×5?úöçÿ©íc®øô‘鈩¸µ€HX…Ý1èn޲¾ðŒN¸êÒ¥F/@ÜäÉÇáþ'ˆ@ð†Þœ:|ª¹ïhŽÀ¦Õ-Ü„ó±gú¡«5=]ͰïÈÛjÅ2¾T¤(&hH&ãÎÇ’}<þîÄ æ7ï¾ís๬$:ÅÏnÍÀahˆÛ”ž¤Ð8{Ô„^êb>ÍsÚ§·§V÷uð੽š1ˆüœ„'"õçú\Ëy÷a~öóÃzVÄäÄìªÉðŠ.À#ì±-‡Žñú|ž³P+T‚ ª*k©VµB_ž­»†ÑT5išcÓ臨\`ØW7Ýú…nûèCËP@З!Ý š% Ü]U2CTÿÓ+WÃúåMü”A&~hë”þ¤„Üv¸É<ôòoXÞ ëûÐÝ1³îŠ02‘†¾î&Î’£n±T4•Êá—``x·S`ÝŠN– LJ'ðaæA¡>Ü]XšEÒûh¶ý.b€eR{3ŠÑ€K«?;ÒßQÿ]1Á!è}М Ø„.sç!â‚b.Å@ ¨°iÃènoäZÀÖDä+ñÍ?sù fSù½Ÿçãð€ÚN>_•Þºu<ê‘)éUÙqžúj&æú£Ç'á±§û¡P¢aÖƒ`IgÄñÙµ¢p"áPÿÜyß~(J`hŠøÞLdáøÐ$4Šuð£ï|á_Ú?þðÒ=4yOÅŠ–£„¥àÊž8K_ݲl%ÃN¿üÂ^xÅ%½ü„ޤàÇ¿ÝÉ!4%e f\¤nÛü½1xãËWZƒnx MsNoG#?hö˜v #8P+ò®–8äòExbûìíŸDpQCÙ]‹ü†ŽCN²ÏÁ6Cªœ‘F5 i£÷·pköƒôÓ$ïC–xX³ö„‘ LaBÊj)å³ì±ˆÚa]¼±b”ô¢ƒ½gƒf|æu>þ‚ù¨øµ>Ï*ÙáÄ™_ÔÄè°]Ÿ‘^œ|Ο&J'§ºÒJ)*9ýépCIøÙ]{PÃ*šo¬ƒƒÃ"ÜX‘¾ñßkÿ¯Çº>¸g"ú×]æ0`{õ¦oà*5Ëÿ-r©ížÕ g®jæ|þ§w ÂÃOåL[k3¬ëkFû> ûGÙ9sé9}ðâ]öÅ£oÛ?ÌÕ‚g¯îp½0ö 2ÎJF¢qêEmƒd4ÔPŒ›Žú%ýøI~¯“Àq2R»1 N¥JvèÏëüójnPð:ÝšX©À¨L›`D.4(²œhm·º¯ ÕÙ.£@ɇÈQ¨™Z5·–«Ñ\8³`.p%ºµuoíM vknƒß¹Ìu>¢- Ûyü"ôû»÷Ã]@SÀÑúG3026‰Pé7Q€½upÓ·¾ø‰öÿ|¤ã{Ç#S6`eO,댹¢â2Ùdä!¾à̸µRo›Ä_–&cü¿x$Ïî2J¸K6.eû_¼x«{À, ²¿óQ¡­ŸŸœÉÁO†áñ;”@; ‰’]ö0¿h³‹ßYë¹ø(` 5™*¹m~É9'HƒjÆ׋_’ÙÓÑ€i42f*0PÊgX# š@ñ¢MË¡»­¡:!¨†d´ß$ÀÑ"@×°Yö~žcº~Ï}YβÀ䮫–µÌ€¹¤¼Wðó{ø9-ïyølÛ=Ä’Ìœ‘ÃÃiŸ†¶héÇ-‘Ò»·Þú'Éy3ÇóD‹ ¾óåO¶}æ‘öì ÿmY7He_Ñ7<«¢8Q\¼e¬òò‹W0‰1“ÊÁ‘ãÓ.CÈH%‚a¨JiíÀ1 À4l ÀkJ€#õ]çæcó +DÆ  ëLX@Ùu]媶Å<_!½èg­í +;‹7CVÙûòµÇ=Ì(öÜ«5Z†¥ZÉÂz±s°÷||}sùj­÷8Ck]/=ñÉ,üïov¡æ™5ÏY犠ƒƒ)HÎ$¡=Vúæ†öÜŸÿòK–ƒ¦E·Þü©ÄÍ›[nzêxäŸKšÂt¶D`õ’F³i¦gâ+02ó¨èæ0Ýkûš`ãêxìÙA86’„ËÎ[ Wœ¿Ä÷©¹¼ìž»2»-m•xûãýÑO+2u…æÆ¬G†VDð˜Ô8PAS¤ X$ÀvHëi‹Áõ/_퉰«æ[dOĬ|n–µÿà–#°ëõxT¸Á(ùªï˜×în@ vÝx“yª˜Ýã´>Õºk 7Tª.L+—LSÀ¸YMkVt@_O‹z\‹Ia6 )ì*ªüöqæ°ïùÜ 6øTIg½€ÔTé}ý>ûÔº<ôöŸ=ÃyÓ¹h„)ÉjßÀ @)-ÑÊ';c¥zä›ïYðáÁÜú•ýà™†ëï?ùL®¬´Ó$F>{e3÷d3ÎØ :€Ì.š¬r'WÜ\2¤ðµ/^ç­k³Ÿ ¯zïÇô~Ú€÷&âÊT¦Oî<†ê_ÏEEs S39ŽðÐføÙv‚x)N(ÎÓ€‰T©:ãÏZÓ{-€ßùúùtà~]MA£0_©º&nŒÊZ€ !€=çŒ%°²·ÕÖnj©ÿU6²d´YŸÎ@®Pâ0#u׋Žf‹ ižß:Ì_kŸZD·åð‘qøÙÝ»9¿Â²ÿ‰ Å2ì>:a)ÍáʇößþîOÁ" EßþÊg"¿Ú¾êÎ]±/eJòº£Á ÊM;)æ_%ý- @‘Ù®-á½V‚*H’»þŸÀáú—¯„3—7Ù/’ n¦ýâç~Òßå˜Ã‰ZçðA[˜£pûÞa8:4…ïÄéÉ–yàçYP95yDMˆŠ¤¨å˜j–kҺщì;< ãS¾G¤E,Cmââs–óÕ¯šÏË~s3 &ƒ×Øo¶ð_MóE¼¯ rxèñC°ù™ãìT¶:ªÐfóeØux šBÅ"À_ìùŸ¿‹€|募 m\ð½§¢ß)(+˜¹ ¬]šà^€ºOl @æî8ŠjÅÿûk_ºÎ55¯Šl'ãxoŠ)1góTçØY”x›Ÿ€û‡À0"œ<ä×4ÔÐŒ0àDÒÏÇ' ,»ÎÉãÏùÑ@ýŠ&ø½h·–‹P)xÐcÎ×¾%-°M‚æÆ($ÓyAŸšÉB*]` ˆs?†÷¤ÎMCc3:•ˆ'$£™ksc ^vÉhNDínÉlæéÕ…¬¶zîùÞZžc¯ê8³ø j=Çg¶ Ào>…8Òß¼×ÔW²5ZJ6‡Ë7îúáM?šû¨Ï?-*øâ>IʾüHô;Ye‰QRé©põ¨²ÿÁpâKIR‡J])q( ¸mðßu—-‡ Îh¯2Ä› {>×Z®²±= â×tÌt®÷o>úÇñzT#oÀ”¢3œ€Ít” H®(€%×=@ žOU꯰ ~CÐÝâÎÃÉl¹j?&K WJP.æÐ,0AÀdJ"ßÀL2‡æj ç{Š´´6„ð Ü¥ÈÈá7<¦…Ífª- ÐÓÕ„ûçÑT*ÃjÔ.–-i6NØ#qýs6Ïÿs5æ*Oö#6Sy¸ãgOÂñÑ<›zN?5cN#S™¶he¸5Z~û¶Ûnº-*¸ùKÿ­Ne¥Õ_z(ò_ÇgÔ«-Fïn‹Â²Î¸G ³—í(€Ê€Æ©»A4dUqqêU-…—žÛ]íá÷‚@ä›Vë· J*P¶áÏïÝɵåj0ÆiÄÞì>ÊL£TgªŸqÀËìU!?)R­÷Kþ€fF-@$ʬ”òì$2˜QC²µ%Ã_ sÑɴE]åº –‰(… k"P¥ µ‚Ë/] ­f?šþðÇÕbÚY™Ú+íÅù<™¿X*ÃïƒÍOM ;;Ð?’æŠÁ¶Xù@OCåú'ný“í7Ýt“ôõ¯}úÅóG‹ ¾üÅÏÊÅŠ²ü‹F>|p\ù#®©ÆSlM„ayOƒY™W Õ‘Fa-”®7¤5)5‘˜Ö¯îMÀ+/îåâw‹¬aYö¬ó–~¦k=; ðóûvÃðx’G'¦p¡ h›-qc„¢ñ™‚â9Š Eމ ùž›Pƒö £ÑѪßv¡`KprhòdmƒÓ6¨ PƒÖvÔØÆ3:k¼¿Åh¸M™ŒZÙùIݨ·§1^vÉZ41š~„¿Gæ¯eç‹ßÏ?ÜgÜVòyl~ò <øè>(jB% þÿ·÷@r\gšàŸYÞvWµG7¦á ‚Þ‰"‡”!©E+/Í®ÆÄÌîÅÍMÜDÜÝÎÅíÝÆÞÞÝ\ÜŽbg5ÒÌÊŽHJ¢$ŠFÔÐ@- á=@x4hoÊ›Ì{ÿË|™/_½ÌªáQ?PY™Yéÿï÷ÿ³?x¿Ÿž†r1G@ÛvëÜÂg~ñw1‚€û¹œ pEÒûî·{¿ýVø/öùÿ§Šn¼ÞñXö&)3g­X/þG§™h@ª8„¸ªÛh>|ñÓ‹ioπ䦰íTq`¸…Ñ6~wÛqغï4]‚Zõ€Åç´°HN´‘é’<Àƒwþ9ÎM°êE ekÖ=^C&õi~V bý­µÐ…tYÖY`ÛñsD‹Áf¥)«f6Ù¤%´8¼+t:Æ£axðÞåÐÙfJ"sÚYǨËÐüÏfåð¾¦ÓOƒ=äy¾ñÖ^ V ¹'AsGšµS ­*U8|j üZ :Õ—>¿|ê‹§lÌ·¶¶Zû»\ pÅÀËÏü]úoß ?õÁ1ÿ·‹Z\M[T )Ž!Aë´¹HjØÎ‡ì®2'YÖÒ‡ï^7.ëzíAX&Þ/©/:kö!ùÍL®¿üÝnÌО…>ì[ȶ5•h;£ 0w!À]\½¤&¤P€˜U©0€©L$W»›[Á˜*B‹ÎM–hg#tSB2Ûœ†‹èëjîYFë>jltQ;©ã¥¯±ï=L YÇ O"7,Ÿ/Á›À‡;ŽB¾@ѶOÌaÿcn@‰˜€3 ”¡3^ýûçžøËÜäP•¼·ŽC]¸âàw?û»ÄwÞ =üÆ~ßßeKj§áÆWa>‘äédtŨë×9S5¬æÃæ—TPPkÁCŸ€Wt:rüÅ wÓ¤ª¾ÌÉç±½ãXä¦%¿ºáí%€= U³ëß”´Íô °_×”‹çãâà¹ß~4ÂÄ(Ù@ä`v{Ö©~ëFãÔNr<pf1¿ùØèå“w,¡Ù‡nj¾”Ù½"ºË>Ì)Þ¯j•³ Ôr6}x”Hþ=FEÄø=§ö3Àhö<—t¬ªõ$´¿üï¿ò÷›†:jëЮ«_ÿüÁWDï~n‹ïo'òêZ¦î£ý>§#F™Ýòਸ਼#Õl:›úzZà›ß@G⛈Š=›ŸµÜdæ~ÎghNf&úA854 #ã3´ë/LÂKiG`²$Hg?Af·Kk„óOÍ0Â4ÛG2£Xµl$âayD„#üͮݧàÍ·÷@–Ü;5±Ÿ¾®™á?[ËÁWÏŒfa|2] ­ÐÛRyòÙ[ÿŸßþ§]÷3£« Ì¢iš²vÙ5âÿ{Ã÷V>Ã<ÿ8jí‚9Iò£]|ÍN<† `˜ô€±îî›ûáîZ`Ä–…‹u‹Xó°P<€Àë¦2`Øqà $ˆV2З†±‹ßÛz ‡§`2Slƒ=°˜¨h9xÎÈ<ö1$C~ñ¾n;ÙyñÛãfhCÈ•=^zÝÉü²PÝ©nÛ÷º1v¶?K®!gc G8ÏäPÌ8Œø+äy-†¹=)yØ$LìÚ«Î#çmàžùCÈuþ{VŠ¥sêÚý‡Ÿ…W_ßaTQbÓ3ºað»fÞ /‡ùX•züÌ ™aN‹v²/Y|ü›Ý?ß±q|){b’ã%‚+ ÈT’ɤ²nÕ@ßÿñŠÿßVþ•¦ãz£“o>£ñ P -Mì^ƒI%Ø–éé‡Wê¥íÔã¸X™óO ºm'Ù¶î¼bdˆ¡I/xn˜4ðù ðác#pèèLgò´­T4‚-ÈDuÖ9|I°ácÒÁÊ#`Çss’ïT;2¹{¦ãAªþ³Öf ‘#d¦;ÁœGÆœ à÷9ð\÷q‡Úo€^…1¡QýOšê]É/Ì»©ýnßñæœ97 ¯¼¶Fî»{%ôö¦\“~PŽLÏàÇOƒ0Ô~]³=ý`6‘¡W¨`€£ 9= ap6,l™úæJÿæ3ÓZ+;VÞpýc|þ|ªÕªr÷mk:~¼)ôõ÷>Rþ×rU‰Q)O^ª9qH%Ã`;ÁP{Í”a¼ç%bÏ!X|ñs«áÆ•]@óW°ñeêþÇþ;¾*eBæ„#E#AªàÓÇ’Í;ŽÃ‘Ãà‚Ž&Èrg:³Èì4ú¡ f(R.ãß(:\:¹Ÿ´B]ƒ—ŸèÉjGÆ_·ynåŠ :œô·ÀËÔè;47 kW/€eKz€á8–ùG—œ¢ x`5<Çû†àwöÁ+zá¾»–XZ¤ìœ#ÚÚ{Þýƒ )Atù;}ŽyóBÉÌÄtGˆýÑÐøÁ}GÿÇÂÌp^Wü¬¡’tz©@ಀŒùñ“Íf•;n]Û²u0qï/?T¾3SR:M·>´µ†¡»=VÓ@¡y~ú²•áîá–5}pßó¡«=n_¨hã7ªês ܘÜë7ž7Z1€ŽŸ™„w‚I:Œ”J‡‘B_J•:ë|ù0ù†ß¥ÂI{EwdÔ^ƒq$dÐÞ¶0M7žÊy‡õš§ô節:±ÜØðØ)‚3Œ`µˆ‰áÖæuƒ÷,ƒX,d3t` ×[ï¢ èš=À«Œpùà™ xö—a|ºBÛÍ×2<€LÀBN›!Wî}l •ûÓ¡mëKzˆú Á€ËW °â<­-‰@ïÜ…ß};ðׇÏÁ—5ÓæGg__w‚¶øÒ9¹bæ à4_Ò`þÜ4|ý‰uÐJÌ…†UûF¢.¿—ÝÕFLWg£i£R/õÑQø`ûqZpƒÌ¹ªjz*« °öéRPAnb/–ôçAÑÄáÇÏŒ XÖ¤ ;|*pµœ­\¤ëÑTºû–EpóÚ~£†ÀËÑ'œc#ÞÿªÉðnºeÛqxùµÝPÑýÎK%? ÿí6}ú\¢ `ßí=g¿)€*8}¸&ÀMú³OµZõÝqÓ²®ŸlŽuÓå!B$ÀòþÛSèH™qWN@­ÕbÿnÕ²øÆÓ7Ùw‚;Ó7p¹a^j¶µM3@qYÉ/ÇÒÖݧa÷Á34« ‡MT}F{tÖùØ>–R³Oñ¸}&Lº€î±ÈÅ/@¿éÆèIN£ ñ¿u„ÍyZkPÊжYhND£!xè¾U°h~»5§ÔpQñÝLŒ÷Ÿ&Ò¼¯7eªêA¸ &ñüâňŸp*¾€ÏÃØdÒ1€¹©ÊÏoo;øï´r.KV—Íoˆþ€k8æç§*7E-À¿|Q_r÷HÇ/lWÿ÷©<Ìeï¡ó¬·3N‚<`².Cho‹ÃW[G4V{à[&ME矗D÷4”ú7\ d!2>†­ Å2lÜ~Žž£àÚ_Àþ­R{Lq–¶@gõÐl¿¸HÊ„lÛ·c1à`dÛg@¡¡R$Z@ÞŸ‘ïkVôÁ§î]Nþ uì©9Ð jÓöcpÙwÂ41<Ÿ¹/»ö¯^ÝGÍ39ÃùY0aê™ü®3 Õtþ߯Mìy¦XQóã‡×\ý®`ÌoÆ÷@%/„?…Û»ç¯úÎÛÁ?8w*?yá±’­=FÓ‚uÓ9h”«4K‚±Ôö‰GÖÀí7Í53…‹eÖƒäF¸…“þõ¤/¿Œy£eëuÉoYþÓîýgáå·öÒ—@ñcS@+©¥f÷•CœÅñ Ñ xj4Oý5`ÁíÓv âèº9 8ŸNÅàËÞJ†5 v﬩ ùeçG5ò2lÙ~Í¢Kèt,{¶¿zyì:0l˜ 2ußÍýGÊõ€AUƒÞ4 /Mý Cm)VýÈôÈü%0€}4k×48T0@?ŸÏX´°¿ÿ¹mÉ?!ÏìU]1 èUH·†¡­5|U :ÉXST{ç´Âc­†ùýiëhnLîÆô8/„àµ}Í:òÉæKðý_l¢¦¦ûBf×dë·Šç¾xÀáðRû¹6óÙ_xFLFýÐÂI€ªs§ä·ý †C®J@ KMô‡ï_ «–Ïášk6îì“mƒ]zÞÿð(ôÏIÁ¼¹ioP0_¾´ ¢úï8 7Õß^62‘‡‰©´DúÓ•÷oi?þ7þÊİ*c~Ìôª€;\Sà’€„ùÙ‡I¿9@3 ¿¯»ãÐdç'³Ý÷¿eŠÐÊ> @OGÌèh¾½øh€–R`^_žzt-ôö$­„ 7i-õæ+rpðrð¹©ù2µÜ0<î!^.æ|ïÙ÷i‹- Á„= ’Äq÷ËëÝÞ4w¼Íä6lœLª›3¼)¤UK •Œ~„Kõ_K»BÕuþ5`О}'ÆàÜè4ܼfM+÷"Ü~ûî3D Ø år¥6Ì'É@ À¶gg‰ôÇAA:[”ê‚TáG7§öÿS¡¬c5Æü ˜)à¸6ÀCõgà‚D2Z’ñD0Ù»ò'C}z\±Â¨Žu¶EiÁ{Rè1Æ ”8†Ð14ƒe‹»áñÏÝ@ÇlÃ[é&å]£lW²V'rЈPo;Ùöß}†À¸j(aÔDÈö¡(î0S¢Ãƒ9@C¯ε²Ä à40 RÑë*»îƒŒ° j˜9ùäçn‚ÞîjÊÕH{—sp»¼Î|¾ î<«—÷BkK¤®ðöûGáÕ7÷Ñœ~ÃÛ` YË3Ù2 õ߯ÜVezMçèèRo"Æ)2?‚Þ`NA œþ€‹ª\.àÕœú…V£‡çÏëëqOò«S¿TÑÌnŸ`ôèHǨç¿Súh^=¦óæÁ}w/‡?µÜòüz1¶§mï¡Ê{5Q$?¨ .„ÇÉ‘—‚—F€?iÇUºhsëø\A@°÷9J>Ê'šµÓFç‰pZ2|XüÔ#7Á’ú\Åssxœ?Òñ“c&;{m‡÷ú7¯ì÷6«<ÉMýçR€q`Œ´D‰ÚVÝskDZÿS©LãØtÈô8‚’ù£¢CÐ:Í — ^0Õ~°E;jáîÎtÛ±™Îû_Üøó™‚’f^Tÿ;ÓQckiºH5ÒÍl@ãHFºkoO+|óË·Cg{ÌÑÉv6&€Ûöâo”:ŒÞ“×Y¿cï ¼´~É…¢Pñ…-¶ûÌm‹P˜¤&€×½€Öƒ'£AÎ h¼¿5ÌÊ¢_±}é$ZzêŸþÛaþÜ”Qv+?;YñP£$s¸Ò;EnÛyžqTÊUû,Æ×8æ7–ãÈÒ(ý±‘jW‹ª­èÌ<»4öѯ*U€i ˜&À>x¥¼?à¢j—Dæg¶?~`ú‚@( Å[Úz–ÿtsìÏ*køÐ_k"L“}Øw”xÇʬ¾Ûܵ„‡?µî¿g±õ`ù‹÷tþÍr¹×M­»]çž÷¹Ñ üìåmpæÜ4øaPhS¹ÓÏÍ\Á0à\bBL‰ P’: uòûNL¢%#~ÏÓˆÖoôZ' ¨I0Ó ZÌv‚’hË—tY MMÖÏñŽÍPá‘LDœ÷œÜ—¡³ÓðÃg>€ññ wN›_ç45xj¦a¿ýí0¶¦}èïÛ|C{ªºÊ˜Ÿÿ0S€iÌàæ¤t!AàR€Ìñgyþ¹> €0šÝ]½ïŸHnÃAßSåªb ËÙm¶3¶ #SŒWgä`£Ðþ¾üéÝCËŠù«oØùîÌÕ¨:ïÚOÐëáp` îWÖï‡ûNƒBTUH –£&œ©›Ðnh³€éï0Œ/éD¢ÄCÀä]~/0¾©<;µjh€ÏÜ¿|’¬@ Î÷e† ¹· :k z/þv|°é1!u“ï5óXÓ£ÚÂÆCû'¿IÇUh/m»©ýè÷«åü$9;ÆôLÈÜs.ªpÑÀ#ë÷ü3éäP¦DÈKjMÆÓY¥óÆç·E¾52 ],+%;j¨*²l@Í`Ž'Åò„#øÂ#7Â-ëú© žÎ?p×ܶÉþ\o¶Ò˜„ÌõûŽÀkï ×¦ãf·]n{Ù¹IÎGùé#&¶íž¤Pß   3^Z¶5î@ƒãš|C®óš€œéùåmBºbQ<ýè-ܽ<üçKxé#£301‘…%‹»k®C‡¿ý—Ýð¹ï¬ã•ü XÌçöéO­…ÝûNÁ†·Ê£ž”Z^Ó5ñ‹¾Ð‰wÈ-à¥~Þœ"¡V @¦‚Í ¸À'þðÞd~jûƒÖ‡0u¬½£kþo÷µ<±ã„z­3AU7Ì @-À „…ø6âø`tÂ×¾t;´`r‰p\mznA=©îÊðF$+™êÃŒ}ÿç›hoyLþQQ'ãŠ~ Åý(aŒ´GÌLÀ²ë9èKdñvFmI¦`®g íÚc™×†,Ç|¿R‡ÿ`5ÜvÓ|jæè²ã~LÂk?üÑY(é½zÕ\G"‹<ó³°ïÀSzé´zSçTÿ… :àË_ù¼·ñüèG¿bÁ‚ýÌÍ'~¨N é†óÏ DS€\tgà¥^ú‹“üìƒ&2LM ÝšìÌuÜö›Á'gò·:ãh:D À¨mN¤Ú¬5˜Îž¤ ßúÆ=°xq§‘ Ø'%»)³aøºQÉŠF‚áÛº{ž}q›ÑôÔ¡hÅv`5çär€SÙ²ëv®!2™çkóÅ4€3T`²^f³Ûkr*%¸}M|ú¾å†£W8î…¤]»NPgñkç;®™ øñ?؃ƒ´,›ŽQk2•¨úóçwÀcÝsZaûŽSðŸÿßß@{BÑVve6,Ž{­ZÕD»_f0¥ 3°nÚUuŠ~d¶Èüð€ÓH0H$[;üjGâñCC°Œ—îØYãü¨Ša(©¡HNž–ѰE1ìPrØûî]øðÇ›äj¸9ÿÌ™z~™ŠïzÃ=€åð±Qøõk;‰}9 J~ÈÁÝòs­m Έi#SÅú©À&ÕrÚ€õǘ´'&Ï Ê;¹ýè ×.c-ÂÈdnW¾öè*HµD>–‡¿Ú¼ù0t´'aá@W LLæà¿ü××aj*4oH×­.@¨qÞ}÷røôgÖB…l|ìè(|ûoÑâĺî¡çã0rTÓiî?“ò<dAîDÆç‚5'àr€•õµ¶?c| ˆºI§Z;Œ¥omwà³¹`L ‘:è @@§šžŠ9¤8moá³ß‹$’ݼoÝ­!ºþìTÉÒœá;!ÿ—C¬èNà_}é.¢¥%ë6í¸Ðä7ûÍŽÏÀk¯n={Ïa¢Ò¾~+VÎ…'Ÿ¾ "äÃóÂañB3áÅg~“Çöº¡}p½†iŒà>Þ/óä¹å|z°è¼à~€‹’®¿bÝ?_ø#Æþó0 ûŽë‚D ˆ£íý¿Ùù䑳ÐËGPŠã¸õ‘°ŸÚýŠ ¨6(†_ LŒL Uá7¯ž~ü6bÛ¥=™ý{NÀæMûáÌ™Q:Ü·N^W ùݸn€0Ñ0±8©dV(ž=5 /þäÅü‚бגêÈ Sú3 Î×ÿ×ÓÄJÁ‹î¼ÀâÿL€i¼€ù(æ ¦S­m‡ÆZVþn‡ÿÎ\I·z¢tǸq2¢ÝÁìøª(öHBlT!ö}¬‘ÈÂùðô“·AwOKÍ‹=ÛDŸF#^À€ ôú†ýð/oí¥æ‹Ä1|•š9Úøt_çy9½üÚ#0“¯3 \ÿa <×Ítƒ‘pT UìlÌÎÓ§:ȯ*Ö¾X|î£R€žt¾øÔ]N_|PØñÉôÀþ“ðæk[áôé¨`ý¿Bî3ZéÆà.·Þ²êðü”ùÑAY©Vá_¯×‡÷m=°4uúbû#³‚7`NÀ<4²â «éyÀMàM¶]  F£É¶îWvFïÞwúù„ÔâÑD1ïß28>`æ™—*Í"¼ÿ“+à¹Ñµ«.;”ë:Éõ¼Ñ`@S€ßÞï|p€¶:ÃÒ_FŒD·Ήù8ûcë4cõxÄGM bYnd»çâœþá3]ÕíåHA¿bË ÖíØJú1³ètóœØZÕbBþ*|þs·Á²¥s.ª@1ì˜Øs`ß)xñ…waøÜ$õ»à°åº¢f$ðÞCŸ½î{p”5¢9j†ëéä‘AxíÙ—fFŽ¿UƇ4ÝJßåM˜Ôç£üwÞ`©ÁR@ú¸ p¥@Œ›ò&óÐ~©Ödr(›ZüÒ6ÿ“= fܵLJļÖD„Wÿm °Í¼]Ý)ø:QA;:ÜûÆÉœl²Z/¢àùpÌD”øá8;<j DTë|½ùÌABøãøÛ8ê#Ë£a‰JÅC¢»|£ÃŒiµÎ<¶÷÷1oŽ €ÛÒ,—ð£Ù©t½QdW5ºaà-‹à‰ÏßJŸãÅ ŸûÔ¹·kÛG°þ­pîì„É]ˆ Fëyz®d ?ùôÝÐÖ•¢¶¿‘W¢Áû¯oÔOoY¿c yfK¥Raå¼ øþõ4b³‹ ¼R€WÿãËLøýþP*ÝÖ¶~ìÖ-ÁâªaôSÎAõ?öÓ®AØ.ÌHT,SÀÕ\®XN®–Ö(|‘<ìy Ú–@n€àv“]¢¶ÍÀúöoéxôà ÛCQ‹•¨$2½ äzç;{¦P‰™LÉí~ÆÝx8ô8‚&ûgàßZDn6æÎhehi‰Â_ýÛ‡!EžÅ…4˜¹¡šÁ£ãGÎÁ¯~ö&ŒŽNÆÆçï§+˜–‚Ò?ÃW¾~?,& †ý*æ©È3yç×/DFÞ_ЧǘBÞÀBÌ Èkü6¢ ПˆÇbE%Ý÷ëwÇm;ÂØF6tÜ7•WýM“@¢`RÑcÝ7®›O=uÇÏóº¡.ND¥Þ>ÈŸ±C7¼{^ú—D‚¢XÑò_'¸ìÝãxs; ÀeRÑyÐQ¿$&H–õš0aû˜6à@ „´íûøgÿúS°h sV÷ßè‹§Ú¾ã> ™€çŸyÎ Ðüæi˜‰ÊºNmÿå+úàËÂÑ0Uý53Ú‘œ.ïzá‡ïû&ö  Ï ç ̸îÀͨÉ0·e¹>ÂØ¶tªuÏPrùë;áÆ| “ƒl- HA€˜!Ÿ§/À2C ¸÷ÞðÙ‡ÖÒ‚*—Jü±nrƒ&KB9~b ¾÷ãßÃøx ý¢.;r1<Ü´ r˜ œ)Tj™´Òkf¬ï˜cГÁt®b =nm&÷ú;µt^w¿R ßï¹{9<þèí®ƒv6zïãË`w>û“õ°ÿQ¹¨ÛÖßá@7|ù›@¼%f fjîxêèÞ£Ç^ûþ;…ìtÎÔÀ˜Þˆ †¯k@æä@¦Í}ùB¡P8žlkyGè¶½'´¹Æ2=¶¡ "Dð›Ã‡I5³· ³›o½m <úè-T‹`/ u®} p½á¿2 …ÞelBñË·—T3T_ÐcGŠ÷ÎùAÒÛf@Fž×§»|5fplLÂ*Ãi«ÐH’ð/köëp8çJtrý­­1øæ×ˆê½¸{Ö÷œ2¾b|DëH6|îŸ7ÀÖ­G ÉϘsþñyà‘ˆ¾ùg@?‚Š9ÚtµÍžÞð܆¡=œ£%;_¶+s2à¾^àšvЉ@^a@> P¸ŸÖ–dt¢Ô:ï7[ÔÛG¦´8«Àat$añF˜€€Ï|#0=XQm' .Äçÿ‰{WÂì3²Y’»ImV s]B!7=] YgXe61‘‡×ß9@–‘÷GþQüΓ¨×|P¶ÐüŠRº5bL¨{³¨±p`P€™\Y¾}Må õ CÞu×rxò‰»j²4݈¨Ÿ- ç¨s£„ÿåÏß…-›>2“šè°eåüÑŸVÝ4@L©ٜ–9¼iûñwžß^¡ýÂ)‰À·üæË}E‰Ï—àê "ydz%‰ÀW²õì7¬žÀGÕŸJµµìŒ-_¿K¿!_Ô¬:?5ˆ&öÓj0‰ÏûTÈÿ,ì‚o|ã~ˆ'Â`Äk"R544¯¼ú!m<ŽD!ÂðT‰zÓÎ<©Ú/ó<º?Z¼ô9Ô¨Âäù˜Hz­ú`„PýŸaunü¬‰ TOЂ4X¶|.üñ·>M}^$c|ÏC‚º§NŽÁÏžy‹ÖûVˆf©ÿèý/ Ÿ™"Ò¿>ñà-"˜>qàDbf뻥Üä ØFc~MbN@™àêKBšE*°uóÌy ðƒ…Äh{uGà¦]Ǫó gaߣ/ ôS‡ :ùœö¿ `vFÃðGÿúf‰¨Ä¬f©,hž¯i€Ç:ybž{þ8~|„zúCÑ(¤“aêE/aÌ©¦@©Ý‰×c­É$v:1²ÔÜ¢zE²õF< н³ùŠÿ¯¿¢YÀ}'ÒX°nÝ"øÚ×ï§îvÿ˜ªÏ3¾ž¸…(pøî oî†ß¿¹ƒFY˜äg9 Erï‹ù”rÓä9”hxs 76¹®?óvXbYÎàÑþwK–™üv²Z€«¨iòb ¾€ß$ÂmÃk<¨ÄˆäõTï ›áÖÓ#Õp  ¨MýQ+àý š =q'Üzûb)Èn3 4½1ó€Ž>›)À²vï>N[}?JJ© ]0c鵌¯Èµ¯x${*@˜@Õêù$¡ÀPH…tɈåµß9 ƒm[À‡[Ž@Ž˜+q1@£û*]¿H„ÅÜøU ú:£¥›ê›;Bg÷W*U±8§‘@½Z€k¾H±˜}dµ¼“m_„|métòèX|á«[µuSÙjˆù0* ùißÀÑh~€%ýyê<üÂãwÂíw.u€ÛMãù“iUlýÊã”ÙzTû?ÜxÞø—0:2EÀ6J{ÈðL¨/Êt80,À‚J:ZÚ ‚§G6—K¹·[™úÏ€Õõ7R ȶ‘_l]ÎÕ È€ùxˆ€\à},ȃ5/‚Á@ •noùàpxõ»{ÊËŠe]µÒ„Q &NC¡ Y",f •Â_xâ.ËjÀíÆÕx›u§y {Zhg~ç;¯Àî]ÇÁÒX?-hJ!OTPT£­QŽÜ¤¸B’”:S«¯Àtv¶‰@ÖÕIé´ÙK{aÛטÉϾ£$.Í@,„?ýÓ‡aÅò>K껽®Àîʯ¶ÞzãCòŒC G‰ô×h¼ßpúi´A(e~`X”J¨°¬O=}܉÷´Òä$?ž)È€õÇßH~}™›ÊF º`ÌÏîÇE£[‚ñŽ@YO@± ~0?·o5‹†"ñ¶¶ßmW×í:Rî¯Zþ#Ñ'DAÀ˜ª* `&@žøâ=pËíK,ðr»yõügµã¼V s¿{þïÀ›oì0Ä“?N4Z+Hóô Oº îŸêÏw>"óíÄNÇ‘…¯¡Ü{¶Ä4º€6´”á¥ßmæWL) <ÒBÀä©'?7ß²Øò±¸ž‰0 Ïe ðì {v…@»IèX‹ @æÇ!×±¯?Jþ2™OD00'8µ¶oòݰ6|ºªñª‹§ôgÀœzn öä‚\Ý-Á<Æä€9õØ”1;?eóÌ`Î@Q °@&ÕÚ-«­]/lÔo9v¶ÜÎOýĦ¤ŽAê XùõFt@%&B¾ñ­OÁ²•s?6ðßuŒÏþózxûíÝäØäqjšà‹u”AÅ‚‚ó{5€68lš:x½Ku¸NX û!ñÓ£R¹ îz¹3H™Þ‡ƒ»¨D¢­éy:ÇÞ ÷Þƒ•–}¾@“«>: ?úî‹09•'¦ a¢ýj¿ÁüêxE T¡¿;R¼y~èp‰sfïR´ýY oÿó vG *q¿ãG ¾hc\j@»ó~±-8süñNÀ· IàÍ „©Õt:Î$ú_Þ\Ywn¢’°ò°l8l8ýDââs˜€¦Á§¾ |hÝþ ÆòŸºNŽCU ‘§  ŠMNñ¸(I­0 ”ñ=Ô—Üü öîGð™äJvåä p®ÃýÄÂ>ˆEˆ0S„‘¤²L@TµY¶c€¦l« ù©ù`XdŠ‘\É©\†… ºáóßm­®g¥{&¦Ö¡}'`Û;áÐAº.@s(!S,ëT ¢º91S4ˆ«ê—9ðþ³õ Õ‚Ô?a@@½õæ$B N´®RÙö0Þó ßòÈð8ï7 {+§›óUÂZ‘6w¹÷“ëàŽ{×X™š³ñà½>{z ~ü½àìÙ,˜™{|Ú8›+Q[Ï7_(ѧ»-¬­ž‡Rç¶” ™œ ‰ª¿ÌùÇ€Xòlh0Ù|ÑÕ?0’Çà |RÓø±ùÐo°uáwŽˆ€ù!v~:·Ÿ.}kGqe&§@e‘ÕªßÈÄþØUøö»VÀ“_½Ÿjâͺ°ïIxþ¹ßSÕ}R@1ü1ìkHf†' DBUií>FžlÔCÊ{vù44€4ÑP¥žÌg¯X«j5€8Qÿ#„¹¸PÊúý¶j åȪ©°Q‚1=Å8Æ”h?•„C>X½fþ½ֳhðZÑ¡÷ÒÏ×Ã{vÒ6X!–P££¯T,[Ì;éH…õ•ó|Ç—uŽlª&§t½ÆšpsüñÀ¼ù2µ¿6‡c$Tìä{#ÀÌx‡ =^$µ¦Ú[>8è[þήâŠBI³"hsGÍÜt bTR{zÛáüt÷¥(³\HÀ¢¾W_Üï¾½“0¾Öù[NH@F2"™|™ÖËc×"ô  s;a" «³×t¡Ç¾âþp:š’è­¯Ñ<×þY-Õ bš5 '÷2G ¯1@–ùi1nïÚr£óŒÏI~ DýWªš[Ð×ß _úÆginÇyxœ6åP­ ;·„_?÷d1䩆ìQ‰É½CÍÕþ½Ï¤[B°¼?0¸zÎÄZqlÌìièf÷Ë<ÿLò3ï¿ìã58è%HöŽ^4ª3D3dcò‚Z_Òˆ;^< Fméwöª«6(ŠDçæ4,ÆŒ³`ÈO#h<òØ=pç}«é6ªäÅüÅOß„Ý;ãÍN¿Šµ£hØpRfÉ‹‰Ln¬2Öóy3òÒ¢vÀz2-A3?U³P•kl€ûi¥ ÃtFæ° SÔgm2b¶[GµÝø*|LPÀ<†™?&€úÐEæçA€‰Z6Œ8¹G 1¢X A@ú“Ÿºn¹cìF¯ù½õÛ`ý«ïÓ: ‚²@;ýà}©V©ç¥6_¢dþeýÁ¡Õs&7Byl¸j«[ªþ²Ü>ù‡—ü²!Áx-BêýGºÖçý‡õ‹•‚¼gmÞ`iñx<K¶µ½µK_±y_a€HR««°5(ç Ó¥+æÁ×ÿìZHt¡_öMïí7_Û yL—ÅR_¢þ³:d6Z¼DŽŸ)”í¦$ 8¶±­8Ökº &ßa]ßA¢%n4îÄ(ƒCÃÕýü&à™êº¢Ñ3ZJçY.´ÅÔdŠQ,B`Ñt'“ÛhÖy8žIeˆ•Q…HèôöwÁ—ÿè!H¶Æäö¾eسý#øÕOG‡õR±™ 6‚ÕŒ ñ¡ôÏ™ÌQ—ÅsÃc7ΛyßWª°A íÝóÌS^Eç<ˆÙb8° \VüsÁ¥¿ì½hä1J0¯ˆB2M€wŠÛ± ÁÔšÆñEmM&¡X:½~{uŇ ‹¨FáPÆE‚F+*ò¶·µ'á/þú)èš“væ’Ën¤°°JTt”–¨]ð«§§²ðÓüNž8KÖ…iþ¿ÈØh†D `¶žÕßO±õz…Óñùö`µe’aBȲd,H™-“+»&ÌðŒ `)ñ’J^{A,ì§×Œ#åXQ>¾…¸ÎkP#õÁªÇ7æ}ªa_ ²¹,[5O|åAGCR+…5%Åϱƒ§á×ϼ ÃCãôþ¢ä§¡¾J*e#Ì—Í æO óGÆ×ôg6…´‘Se#ÜÇ_˜›äÃ~ü8üˆ@|¦?Ã~ž±¤k xM€ òþ¾ROâý¼¦ÀGxSÀ¡ R“Éd8M¥~¿³ºrëÁÂ@¡dj`€j¨ÊÆ[ãð'ÿýc0°´×è2aa¼´ãÓÔqKDÉ>C05‘ßÉ¿oÏQ¨’C¢MZÛÛÛpš¡4ÅÎ:(±“ó‘§ä·gœ§!ô4É€¼$È%kÑ’ÐüÐtA-ašö°Sæl)5Zƒ-ù¡F0|EÈçò°æ¦åðè€Fk\G“áÍ~˜Ÿ†SGÏÀÛ¯o„SLJ[|iºŸÞC”üZÕ°ûs9cÔ"dþE}á±µór›‚Úð`¹\©ÚgYòó²ûyfæËzùX?Ïøp2¿WáÏE‘þŽ÷éRç D’iøaŒÌÔz>ûOfˆ­ÂxK:rhH--Ép„hvi+7íÍ-²ƒ¸aZl"2w^|ë/‡9sÛ?ñFïÙ™,Ü} †Çúƒ™™"œš0Øp6âÎ7F#(EÑ  1j«$˜„ ‚Ài0•À1d}" &B6'IryÍ,vvhE£Š`ïÛblîBÍßs8ÆÝ_cô*Z *¥"uÊ>ú¥OC[W+5/0vòè aúhI%`lx¶oÚ§ˆf…*>šUºÉü(é5Ó釒Ï!݆åóBgWöe·µPüâG&ùÅ¢>îÏœb& ïàµYÚïEsþ9^KI™¢C_,Äk^ ‹ 8’„€š±Š’$æ@8šN½·W[±e~ “«X• õÍï„¿ù¿ÿâɈÃóÝ(à ^]A@ `hc#Sðƒÿò< Ÿ~ª=h‰íÝ·UxœÅqQ‚Ñ¢Åvð±õŒ¡mÍÁ˜7wb29Xß}ìXŠ1ŽN+U><è.ïXn.`ŒËo‹~ÚI‡œ³%Í50 åcýºÃ`;íl@<,/ƶ\Å|†Jræ°}š#{}a~:( YW&ç1“-Ð)F'ætF++ø.îœÚ^ÊOT*šÈø¼§_d|Y¶¼P”|øT_qì¿Kªúóïç%'-@àkx àÕ~±Q¯ ˆNAWM ÛÚÛã'F‚ýo|˜[}úl! ‡à«ÿæ1¸ëé‹P&Ž“WœS$ÕÜ+6‹ÎÎä`ã†mðÆ+‡ó…¨ýÏúò=+ôFÓ”ýô 0ÊjméÍ´öâ9XÀ"yêÑP€Æï3Ôࢠ&Õ8î8rsªtÀ?-2Â|zÞAÈpŒo3½!Ð-À©¥<1ôŠ3J`wñ¡Œï³ª-„¨§?[¤Þ¼—s»£…5 Õ}½­ûòÙÉŒÆPÉÝÞwËôã~¢ãO¬ÿçÁ¡ Þvÿ%Sý¹WáòG©0NeMCDm µ¾bÛ0ö;T؇?ž ýíméØT1ÖýÆ–üêƒG³=Ën^®üñ_ â ÀZkJa]€¥Z£ÍŸ…c‡NRóøè4Ú{Μ¡Ý}Ÿ1ð„#ìÇ…ÿØE8ÅŒN"èl#À‡¹¥‚¡- Qbˆµ ²$ø_ž½ã!ݸv¬¯/”ªlçðß:ÿ;¨‘úvÞ€aB`2:æ+Å"Câ+TÒSW!ËyÐ ¦§½ü‰VƒŒOÛ|‘ïÉxúbS7,Ôw§C£‡³Ù™¼GM¿›Ã“ܼ*/Ódö>/ùeÍ>.zØO¤+ ÄРèà™™W÷ù<¾T˜€hȃ–6à#”N§¢Š?™~{giù©©ðÂoþÕ×ó–ôЮÕHÈüÎ$`L€ÿ…Ÿ¼ ûv"/£B }œãFãGv ïÀÿaʤ*u¨‰Ûrph(8OLÌÀ樘ه‰FNîf r’݀ͫ§f‹.J,d±Q PÍp(–ëV*LÕç‰LŸóàÓz3„ù±¾.Ý֖΋œ[Õ_ÚÔGOåó4î)³÷½Ô~ÞVç¥>³ùKPëä{ð?±Ó¯«êtÍR/™Ýƒ¢I ›ws Šš€&T[Z’‘D"•<5Ó²pɃ_XÕÖ?'ªkÎFM€)™ O^Àçþá—°Ÿ€/!ˆr2ºÊIjKMçü&Ð~Ħ¦`IdчïÀýI;“ˆP»ÀÔÝzQ;È59˜m ‹=ô‹ä|±N¡& IzúE÷Þ,0Æ0D»5ìàíÆðì;uô‰ŸËaf_•6zé鈖V- ]Ò“ÝU̕쫨ü¢ÚÏ3°ÈÜ¢šï%ùejÿ%Wýùwø²RS@ òŒ;T(~xí@Þœð¬Pa$ ¤ÓéDzígoˆÎ_½D F¢â5 €ã‡Œ ŽÀþ¨ °cÓ~ÈLg óh±-¥‡&à´ÿ…l@³0 {ëño‚#ˆkæì&$9žº ¹|¤‚õÕ¬®I¤›¡omm<ߊ™OÃGìD ÍNÁ3 |ªQk€¹èd¶¿ÆæMÀî=ÙL‘…âKÆC0¿7:½zº·+1y(›™ÌV«šØÌÃMݯ'ùea?Èì}·pŸxn”® @ré s ŠÑY”€xiüÀ"n@ ’L ~t¶/¿calé]kÕH²U¼êÅ\6¾ö>=p ޤ‰(*Vúñê?g§3àÕ~~ê7›˜¢CMãfÙ}¢£ÏR÷íe5ššA#e—‚‹ÞØ{fInnÊû¨ã’žoÙ /ÚŽÛÑÇðùÿlÊûŒã¨UËeKÍç‡îB-#G´­\ÁÈçGm©=ÑV.Œœ\>·¼ÇW;“Ëe˺îP­E¦ã_æð%?ÏünÒÞ+ÅW&ùµþH—‚ùÍWáò“d¡zš€˜ï/2¸Œùe ó ˆ¡B–4ä‹'áîÅkç$—ݵÚß>w®¢ú…Ñ*tªlxé÷°ãý´ÊO!ö?˜` ëÍ›‚Ô·óýMú–UU]¢þój=÷89 áo)¸_Ôò…z‰@Ó Òo–™½ˆû¬rµ4:WTçpôq)À5AV£b©ÂyûjèàC[óúñº± ÌëM/Ÿç?Üß–9PÈŽOÑYk[{©ûbНŒùeÒ_´óÅô^Y{¯Ëf÷;ߊ+ˆ\²ݲyÆû ðšŸHÄûPëWà÷-æ `FŠ †m=}-í+îZž·f9¯  _ïÐÎà»ÿñŸ 7x2áp؈ͳ–ã¬å• ¼×ßQåg¶%C?-ñ­ò¡:ÎWà4æj/ÛM Ü?†ÊÜ£.CƒhÌ‹+s4n5ïñ‡ªoçØ’Ÿi ¸?T÷ ‚1P'žo&W¤~\†÷§«=Z^2/<¸¼_Û‚ñÁLf¦(„øÜ’zdö¾[–ŸèýÕ|™£¯ váŠc~çÛrËx‚2àóûÅRbˆ>>G@–' ë,dÍ£0O$’áÎE7ÌiYzç*Gÿ<Å Ñ.?ÛÂwÿÃ÷`z|Ô`ŒYC”óI<ªàð ¨fãªf„¶ló„H/ùÙBÞ¨½×¨.£ƒ-_lÜpúE6‚ Cb—j§[›I¢–Çßž×ùZ4|>Z·Oó †ƒ»ö 6€ÇjM„õ…s£c+æûv·d>ÊÎŒÏíN¤nŒï•ÜÇúÜ>2UŸ×ÄÂY’,óð’3?÷æ\äa xi2àÍQê‹Èʉ€ ëj ðµuõ$ۖݶ0¶pÝŠ3ƒÉŸÿÓo¢ûw|¤h´ÒϨE¢…=b±íFìcEõ, Xµ3ù 6'1jØ5‡vàˆú)¼âR?€µ¹¹(`ö;0Lï÷®fuýÏTv•jEê³pŽž*VýYËÌï ¢]O|DÕGÉßñ^Æ övG3Ëæ…Ž ô”è剑L&[Òu]d,Ó{1=Õ_q[@Äã]qÌϽ1WÍÂ' ÖÈrd` úx0€,gÀôèj4 vö´•ªÁô›NÍÝýQ~áÈ$$+Ýá•Çâžp8H;EP#ð9s˜`ù°¦9&^¹Â²åÉ?|8@˜H2PúÍ£¦sçiÎÊ|ÿlâ4 l©Ž%sÑôÚ³:]Gò×$Dçì~‹ñuchtV¯OMêÀWh·áÎŽhaé¼ÈéŽÚþ¨êÌôôT®bç3‹zë1¾ÌÓ/j¼Ú/š"pȤ¾,ÌwE0í[q…Ø¹Š KXdpÑ (ÓĬAY#‹)‘ˆ‡S©t|dRïÚv °hÿ±Â܉éj‚&±pW„¦ªÊ±(ÑÈKM’2­€7Àh ‚ËX±Z-¨žÌîèdD Û¨`Ùðök‚sدƒ€ ÍŸkéÏç Æ€¶~!_¢…;¨É#ã³îHm‘üâyÑ3Kú”ÃñàÌ`.;9S4FUk/u_tòÉ~¢äwûîÅøUá\.k¨Ï‹®H@rÑÜü¼½.ÚónZAÜA@æù¬cë¦Y€}¢ÑXbtÊ×½çXuþãùy#c…x¹¬ÕçQ Gm{æê`r\O‹v¨6¡ƒåt¢€“Ç©ûÏ´ªßP׉€¢•ÏØŸ ó!ù*Ýw™2/ ;SИc½úhŽÐüÝÐ~°Qk{*R\B$þ’¹¾Z#™“™™‰L>_¨èNI/2›,´çeïËâý2S@¦-4*õk~HMð :š€[ê°.ˆdŽ?‘ñ.¿Í|,­ÀO8¬¥%N&Sñ©|°}÷G…ù{>ÊÎ+¶µÛ'^JNÔÐ<`C˜£=M«öÈë (û[Z>WÀùœwª ̯¨®ûÍ,;¹ Kg¯BV ©Ò|ª9ÒíBè´õz,tìÑ¡· FO¾²Ù© M´ñ»:"3}‘3s”#-‘Ü`>79“ÍæËš¦éŠ¢¸ÙøØúl^dhž©Å0žLâ‹I=¢6"Ë˜Ÿ{®\òÐÜœƒ2¦tóð…Fnà¦ø%ÇHE h%A2ÙÍ•ÃmO”úöÉ-8u6ߑɕƒìÅçŸ ÿa¬‡2ÇaÍ}*‹ðñ}EpøK²ÿÄLA“¨ÃŽfÇ6pdZpWåjÕ©êÓîÄÕ6PÚ#èà‡Õö³´gr›ª=íá©Eý¡óº”±`æÌÌôdó÷MåA´ñyi/2¾WˆÏ d怌ñÅО[bÏÍüW0jøË>w ™¼Aô%È~/j5 €çâóùü‰D,˜jMEu5š<5¬õì=’Ÿl0ß=6AµtîªÌyÕL‡E­³Y}ö-´“‰dORÞ á‡¶óVÍ_(Vq iïÚ%³L]—1µè™ ^R_‡¤þ•ÈøÂktõ‘K)±h¨PßGÀçˆ9nfƒÛ62¤ÌIi9 M­@ …BX,À‚ÏMLf”–3£•öãgŠsNŸ+tOL•bÙ|%R.k>ZØÆ› R."yhü¼È2ýܪýëêUú<ŽMµ0ŠŽÔ ÑïÉx0‹ÃáhXSBñ‘ è8=\j-¦G&J)¢´ÎdËqb&+•ЦŠ!9WšÍwuØKˆätõH8 E"þr*˜êLG»Û‚ißH:^ö)…™b!WÈff ù|±‚†½n¨A<ó¸…óxG_#!>Qew/)ïeã‹]{j{®d¦žäÕOurØwYÄ€}—Æõ¡Vª‹5n€!¤&T(œ‡ÃiÈó ‚Dœ¢ß ™H„‚¡h¤XñEsˆNÌè-£“•ôðD)=NÌ…éL9–ËW‚¥’,U´`¥¢4S}Ç÷ÓŠ84@ §90MÂïWªA¿Z…}Åx4PŒEü%ÂðÓ©¤²£Ui‰)S‘–õ+¥™J)›#ö| “uJ%ŒÞYθ…ü¼\ƒÆ$}ÕãÌ/j'n€@?*Aô!ø÷’ÿj(T pJÀ+’üª/@4,9ÂöDXO¯«Ä´÷)\—QÝhÓ£«*è~ŸBä5ab]Ó|Š^©VËeÐËel©U(”´atdrÌÙGm¾Êúuu.ºIJY‘Ž[&ŸÈ”nŒÏÖy1µ›Zï¿¿æŸÑ5Œ<2ÙÔ+™ÈmêeË×cv7m€wÖ˜SéÇþZ¹vªø­‘¨³Žˆy@¯T¨ZCÓl=Âù»äGÈæìwkÔgüz’ß+—¿‘¤7?ÛÇ Œ¼àa~¤k5lêUgÀû dÉFõÀÀ- à¦mÈ€ a® \–I³ÿ<›Šr?¦ür‘qdózv#¦€—$Ÿ Ã{Ù÷éjf|F×4 ¹€í^ÁlµY…¢—Iáõ{Ù1Äón¹xþn×êâÖ¯!]X¯{¬wcœF™ß Ä(@#L^­s/  ŽÄGº˜éšžêhl~¶`àÍÀõ˜]¦]Ȥ½—v";o·k¼P$~ÞKÂj L1ê1y£^|™¤—2øµÂøŒ®+@òмÌ7¨Ï°^Ò]&á½Ô}/@vβëû8äeû3Ò¸©—ÀIëÙç^j¼àx3¿x}”®5¦çéºF³7P}¯Ç°nR\mà7³³ÿešÀ…$Ýcz>ÎÀFÖ/£ëç)^ƒ®eæGºn€§üü¼ó‹ßë™õÖCÛz“ì.$y™ü²ÙÂù®°5F™$߯y¦ç© ‡y N?04Êä³ñü/¹™ü²zööÇý€Çwð˜Šó]OŒÏ¨ jdó|÷ UØo£6þ¥7 ê%më€l$m–û­wN5t=2=OM¨C³þ»[<¾QÆõ ·mÅå^çùqIwYVÏAè%¹eÛ6j·»1z“ñëPfIܦøÀe{Ùy\l’1^£`p>Ëf¡Ú#5^NM8OªH^Ly>@Áæg³ŸKEH`/¯{=föbrOÆn2¾75àÓ,5Ù2¥ùzûºR@\6Fnªó—€šp¨í€Q½ðãlíùËa 4ʨõ´ƒF~SCMæÿxÔ€KD³¤Fò¼¶ktçK2ÝlÔõ†¹ÉôŽšp™h–€ÀÓ…’ E³eÄYmßdô‹OM¸‚èc€‚Œ.Õ³½ LÚdöËCM¸ èÃAM†¿2èš{±®Gº¢ÉàW]q/N“šÔ¤KGMhR“®cj@“štSšÔ¤ë˜šÐ¤&]ÇÔ€&5é:¦&4©I×15 IMºŽ© MjÒuLMhR“®cj@“štSšÔ¤ë˜šÐ¤&]ÇÔ€&5é:¦&4©I×15 IMºŽ© MjÒuLMhR“®cj@“štSšÔ¤ë˜šÐ¤&]ÇôÿP,ÑÎiñêIEND®B`‚murano-dashboard-5.0.0/muranodashboard/static/muranodashboard/images/shared.png0000666000175100017510000000747613245511125030037 0ustar zuulzuul00000000000000‰PNG  IHDRTTkÁIDATx^í XÔeÇ¿ÿ™áDäPTñ–.$ËL³,ÓĶK-ÓÒ¶vm7m­íñvk[+ݲRÑÜÊ.[¥Ó4,W×¼QDA˜ãÿßç÷þç?Ì sü&œ÷yæa˜‡ùð¾¿÷ûûþ~ï+ïð(Σ¯æ}1Pz^ž!@0& ^¨`J@•@½P[•€ú0z¡¶¤éÇ ¨¿ ¨ª˜Ð`z/TФþ–×¶}”ýrÚ4/TM[ø‹`4è7x¡zèãïîâ‹* œª‡€ÖÔéùY™ûQXeàƒaÃö¹·z— Ør'Î]â;†`Öú(<ßÀõ:ïòo ÐÛ_þ‚_9=ùeµX¸)Sø¨á]þÍ'Ê¥ÌÞ(Ûúu^—„™™û@1U0½Ë¿\¹Ô9_ ¼¾_aÅ´Û -ÿ¢JgÐ5x—¿›P¹›ç}Ë€òF=}-‚:yÕ^”×½»¿»@o}m«Àtà zØ@µJÈHKDYµ'*êØò÷ÆTùT¹´E¿2 ‚Q£®žA¥±ä±›„ñáðJ*ù0é™\úÒÝ‚ÀólÉóºzõõŒª•V1Õ+©äe@é©o4ÍÐzðFƒyù¯œ> ¥—0ç“C^I%ƒ©(=WI3”f*-AÐ3.Bx{ÊM^Iå.P ªÀ‹3TŠ©ö$¯×{uªÀV3Tú>ÍJ”f)mT^I%cnŠO± ´qù‹ËÞ2¦¾06 Ùyç‘_Q‡ŠËðJ*Ôš¡’¤2è­bêÒÇoöJ*sÖ)Pi÷%•)ôÐø©¼’ª¹@›H*ʨ :–Q‘¤ªm0bVf§Py]*§1Ôö ÆÑF*¥©‰#0ùöD,ÿ>ßëR9Û”ìÍh‚J•7XJ*µ°bÚ­.•žãõºVR¹Œ¡¶`YšjÐYH*ƒÉ¥&ºTïïCyÍk¨¸ Ô™¤š7q(>Ùue5úVR5 ¨³4õF—TÍêXRùXÅÔÍ¥jPW’Šã8<ñÞžJRµ¨UL5Ù~’¤ŠîŠçFkÍ’êFpþmï‡éÃâì§Â¶¼*|¶·¼ƒRû’Ê¢ð·.EU$©Ú¶KåèëÑ?6çkõ×ø`ûñ øÛ¦|+%å«â 3ˆ”™Ke¡Q©N%þDI5k]®ØLц{©ìU*8yk&kYj9c݌г†wƼ¯ò±ãÄ0:T×ê…78‰_ó/²¯™3*+—J)Ì›˜‚¬ÜsmÞ¥jtRJ´1Axù‹xê¶XŒè +÷C¥Pàógâ§£U˜¿¹€ÁóQrX<®'úDkðê¦|¨ ,«jtþyÌ{(E¸¥wT›.ü5úÅŒ(«ÑáÙõGˆå½ðïO¢°¢KÇ÷IJo‹°õhªÖ`ѸžPû(°ì›"|sè¼9PйT’óoÐAãG’ª±™¢-Jª&@ß~¸7|• ê‚û{àõïNâpI-{²åìݰûVý\ìêe¬t*“T¬å‡Ú) ¢¤ÚÁÑïÕÚÒTnKN ¿$«£Yêj¦Ò®/™#–0?Û[†w~:m†5qHŇ`ëÑJl9Páðì¸9M5iTÉ¥J° dŸ¨ju’Š«­×ó#oc³® ÚÎL[˜ôýäÎÁ˜”Ò‰+; .â¯_žpáR‰¢ß²?5ówˆ’*3E ­Æ¥âR^üLøþ•1Bö±r,Þ’Ï2NAGè]ÇÔ&¯>ˆ²K: ïR‡ÎÖâ@q ûáû“"1#=ŽÍ^ïhˆý©:ëf êOµ”T­äÈ:zPaθ$.+÷œ°$«È´ü]Cýû„D–M%Fi0apG3/Ú˜H ”×èðå3I((¿‚>9&k÷·•TdýeåœÃÆÿ•´ŠþTnÐÌÕg”ꀘуㅹâ6î>#¼õÃIN¡òq¹üÉmÒlz6‰ÅI²ùxAÀ #âAæôæÜ <’Ú ?­Ä‚Í….7,G’*#-Qx"=±UH*N=­ïÄùsT~1·ic…ÒQa|¾·ŒÅU91uýÔþÐy<þÁAV¥m½qAÌ|ž–y§*ë˜ú»îí°qO©C¸,MµìOme’вõ±êðè}'Οˠö‹?<”-ÿÅ[ 8¥ÚåÌ© ÇGÄcë±*ì,¸€~1AÝ¿3©-}Òqƒ:âé´X¼÷s1þ½ûœó˜Êœÿ:QR±þT¥Ùù¿ž%MBPµ æªüƒbF%wæ>˜Ä}“[*,Î*%©Rº†âž,·§Í‰¬?K˜D¯k„?ÞžÔWtF<ðÎ~1Uý–G~Ƥt8%rŠk®KIE@‡P5,:B;iþKbLí*Ì}0™ÿÏ_:&wù¿zowfª,ÚRˆïTš¡ÌeãAÊ€<Õÿ\ľÓÕØž'šÕö†èRµ.IE@µLWe«Ã£ÃµóçTN¡ÂÒÇR²åäJª±AÌÅß| Ë<Å8Y¼pŀŠ+èÙ1µ’Ù‚d¶IE@ø™ Шã# *-ÿу»s$eT*—:• –V7°8ºãø,x ›™´­Ê.fÊ€þÞ+ _ìs¬O%ÈÒîoOReçU`õϧ¯IE@cM@ *Ý£1ÅÔHíà _RúøÇŠ’*Ù-IÕ7ZÖ8I+rÃ.ëˆêý½;i ñSâhI-Î^Oñ9Zþì˜Í‘Ÿ1C» 3Gi¯›#?ôn#,€J3•nÊ¡*̼ûkc…M4jÙ’jp—¼66eÕ ˜²ú›™Ò¸[ééqð÷iL 6å”cŶӿ:¿-Ø&’Š¢¶íO½¶.¥ ‰ì&‚)=¤˜JËß,©zƆ kžf’T¦4•s~!A}ñî®øÓ'ÇPt¾Ž1"‡îènìs’OTE‚ɷݰð¯ígdH*鄊í)ê=(¯å¯™óO4I‹Û*-Ú¨"m%•˜¦Ês©¨dB%ilœ>A~J*®E‡`_<ÿñ1TPéúöά¡bä{˜ìr¾üm%•RÈHë…üò+(­Ñ_3IE@I¹PGPJª'ÞÝ%Pm]®¤"@BÕÈœÒÿÉ­À[[OaÞ˜îèæÏ ’™2~pGLX™c.W;…jçÈϺçï4¹T׿bJ ɲwÕJRI1•RÒ%Þ$P«Ž\IEpÈá§Tuí¯%XóëY¶iÔ.áþhèÃ*§Tΰu©®‡SÔÒíŒÒGgPÍ’Š úøk˜¡ÒèRÉ“TêãiXuô©µâF%Aý]·Pf¢™B_£¬«ê²ž¹VÒ è¤k÷œK-Î$Õµ8E-]ÈJ[­+¨’*,º£vÒSFÕÅm—Jj¦8x¶K² Qr±LíŠì1´2R¢ V‰ `ß©j,5Ùä¯RœuÝKumNQK@é÷–UZþ$©Â%—ŠÒÔ`Mju<¾;\)+¦ÞÓ¿3Ÿi¬Üv_îËÒ´û“ ¸xÅÀfj€¯#ú†ãxéeÌX„ÅÛUõe6á“kDKš©MOQ_}I%ié£;P­$U˜0aíÌt·$õJQ¶D¢žfë½Ãðç»»²Æ´7æ±åNcDŸpÌϪª³Gve=VöŠ‚V§¨©ÈúS¹«z1¥ˆt*É-“K%¦©£’iù»çü[n>ËÆ÷ÄÀ¸`<úþA_¨7+ÐW‰M3“PyY°@û0¥'7þ®ÍÅ4¶ªÜ¨SJª™k÷ Ške- ƺ©ýا¿wÀj“—b.}QN¹šAµ¹˜†fïÕ8òc/ÍqªeL5§©=bÚ Ã´qt¹ÞÞzJVá`Q×ß®!x⃃æ¼>*D73z¹œ™¶2ËöbZþWãµ£¼Ñ]¨M%Õx —ªˆã”*Ð1g#®½ëG%-úéžRfïÝŸIn½¬™Ùªã‹iØ)êµ9w©œ½Cw Z»T æ¨ücF ¢˜jr©~<ÉQ½ÊUŠ þ>5Ý;€>—»Ìý¡¢¦þTºëoÁ×G=zŠÚù”µi‹%Utx0sçž½,;¦Þ7°Ëí¿Î­`-é-Wóµ+ ô>šÕ¤*o.0Au^÷§Œè­I½Q§3bêšC;Oä‚nz1ÍosŠZÐæ@%Ier©ìI*yi*õO…k|ÍÍirá¹\þ6Óxòµ\ îBµ’T}3æ¿D%j©ð·pÓáÛCçe/ÿ–‚´üyGÍž’TîuªCI5(! QíM’J^3…'¡šüX]Lã™SÔîmT’TA’óo×¥¢Ýß…¤ò$Pz-«»þå¯íy¥Ùø´,ù£¤±ìÚ)ŸF^AIJβö Íb~Í»x¹óÕÉñ`ŒaÖšŸ˜d¨Ni¯1ÍF.Êâ²É‚ޏ­~3*ëv”@w¢}&26æ­mÜ"]A{wòä%Ý£lï ‡ +:ýÒž¥­oO1Íîyg;·˜`ÙlBOØÞ*t^~I»ÊÞ,nén.«À³ïâåÆÓfÄbg~êëÚ•²Ø¸åû¸lå3ÔÕæ\Fꯇñbúµ+e `jþ¹ll1A–Œ ´Y‚·–bÚ®,]á©6Ó6`ÚB±,¶¸$ UeQL'¦ïd’á\›VV3`‚V¶7­°I«ÊÞ1{aËÑŠ6­¬À*4§ím– * Œ!cÞxÞÖ•åXÙÞ2d‹d‹iJh¤¬æÄ!П<}yù%m®ö¾(°}{[cÚ¦,¥ ]ûãIì:Qͤº¶U†^غ¹²8fÆEòYãÂÛœ²šûvrG\x7>u%UM¶Q5%2¡-Ó%¦Í|:{ñUϱuYmEỲߛŽAAÞb2¿vç)|ó¿ M–)¦ÍFX¤&e¹ñÕÉãt[Q–v›/òÊë±|zöÕàèéz,›ŽBM£ ï<†'j°ùЫ²HW’(P¼½ÜùäØP0;rõç¡+hÝ]KX±ŸoœƒU?”ÁGëŽ{»aÕʰ|z8žXs•ç%¼0®Žé…”Ïu8¢¯…PKb‹“²6¾:‰{k=’‘ ]qy«­ÈØøô}|óC±aO9ö×"sN4ž^{f™£¼Öˆ„ÁX0ߪÄûÛJÅ!”P–pt“xQB ííËWωçP&'­TYbKóÇë“Cp°¬wö÷Cò†<äŸi°¯$ @Nq-ÞüG!LûIŒ¨È$JbMJ n2",¨+Oz 2sÊðõþòV©,[ÒìƒqÝQt¶›r+m+Û(YpÁ$ÃÃáÝ¿Ä ç\¬,Å1ýÜd§(+¢U*«Y–Ö0àå ýQÝ`Âc±AØr¤ ïÿ»™cáÄÞßSWåÚt% e‹ØšÁIY]´­VY- §FöÁ“#ƒðßüj,Î*²¿š8#vÅÀìtƨĴƒ²,49!eÅ‹241}“j«ZE—Åâ—åp¦¡#bûCЃûxã•¿å‹7ŸÛ3îê?n-Á?Ÿuyvnﲨ­TbšZËcCÐ qœmj]–ñ0›Ñ:>žî ’™Û`?Ùq _ìýYü“Ô ýÑÇ_‹õ»N‹ªL}DEæÜeÈxù>¥ËjÊbK6éäìcçÎ+s›/–O ÇÆƒ•øàûRôè£I†¡É„#‚0mx žÿôJÏÙËÐKvYê°ÿ*‹½¿Y'çê (>gb7д½ûvÓâtÍ>[ŽV ‡;>êŒL œº¬ÜÒZ|°µè–)‹-ÌÈ‘—=>‚-ٔdzU3»g‹íMÙ{õ“Q¢äüðûRÌÓ÷îÿÎîxccvÖ"ÀÛUõ¦fÛ›âØ¹ËŠÚ¿6%/e¸%1ÍF,ÈlL›1B;1¦[šuœgçÕ0Æ4-VzÖè¾?¨;>þQröƒÑ=ñ¯#gñ‡ìÐÖcr(æ}©sÑe)£_û°ßë–Y÷¨±¿™4ÿÍÇG èÙköó¢*c‹íM« fD2ËÈ>V}õ{)ê4¤ÅÛËM þBÊ"àh]­/ïîQc#h{ÏŸz—–*¤'Î]RYGÛ›¶ó”a½ßO$.꣣ƒ}± ó8ޕ׷ÈÞŽ³ouØÆ`Ģ̣7UY 3­°õ “¯-¦kW>3ú’ÊòÕº£î‚4ËLŠqMEÛ+™Ç…¾¼=ñÚ·.}ó•EÀ½xY¡µÛ›Vúö+QPáñUR4æ}¡Ã‘Óõ¶üí¬"ì.¬Ao?/‘‘fY ‡ôõÁ¡2ƒ29¡ Nг=¦ýIYp£¯TÑ濾åé­noÚâÓŠ²kÃûtÏÅ”E³¯û`ÅÖ“ØQP#ÕßÏŽ †ÖCƒº&3W$ƒ+‹½¶m0H‡xeèü‡‡!s¯ &~Cϧ ˜¶1]0u†öµÆ´PVÈýsRçO¹K(kÉ&ߪ«e7÷f±IÅ û¨@yýÛáèE“Bð÷b&–xGOÜ9Àk¶ë‘ö`(öDi*@IY“ý, À{¿Å5 à†*‹€iÓÿÜšÞ§{˜"ƒ_²ÔOaõQøÙ`Ä[› Å[Ôr~“#~î?i﫾VΧ­£"IQö¾ÑWªÔëÃtgÚ4%²Ê¢27¬Û^‚Êj§[vYøùsC‘•[‰/ö)}´ª­=EµÍ`Õˆ •f?®U•Uo´P?}ÝWªÔ â4ò¸´KeE Ô®1þ’Ê¢†còÐXùCý¼ðäÝAØç´²ÎãÛäÄÏŽÊš}o>ØVx]ÊR¿ó@À—ƒ¦X§Df-C_HM›Û¼ËršœPLSW•0¤ÂzvÆÅVÖôÍR– L¿órÐ׬¬1aþxó¡P¼°!TŠ^Écö;(ËñJÕì¿\›²ÔA–úórÐ.•5óÞÁÚ ¨7ÉØ]tÞeLÓJo?QÚFº‡~em{;)+mÆ¡¬Šó¦«V–ãäîj /­¬¼¦q§¤ýë+UËŸ}MÊrþ¢Ö•BSöV•E1š<ÿJº¬kùœ¯TQ|‡õñ竞%š–yŸç!¿øÔ‹;'~z¹k’®¾™v5Ъ²|š*2RVX?fýyÓxxábʺZðfʲ(aAÐ+gÅ¢°²©_å3Épvîå ÿVÞǵÓc“XIEND®B`‚murano-dashboard-5.0.0/muranodashboard/static/app/0000775000175100017510000000000013245511556022215 5ustar zuulzuul00000000000000murano-dashboard-5.0.0/muranodashboard/static/app/core/0000775000175100017510000000000013245511556023145 5ustar zuulzuul00000000000000murano-dashboard-5.0.0/muranodashboard/static/app/core/metadata/0000775000175100017510000000000013245511556024725 5ustar zuulzuul00000000000000murano-dashboard-5.0.0/muranodashboard/static/app/core/metadata/metadata.service.spec.js0000666000175100017510000001437713245511125031441 0ustar zuulzuul00000000000000/* * Copyright 2015, ThoughtWorks 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. */ (function () { 'use strict'; describe('metadata.service', function () { /** * That's almost a copy-paste of original metadata.service.spec.js file, * with the exception that a murano module is added to modules registry to * check that murano decorator for metadata service doesn't break core * OpenStack services metadata. */ beforeEach(module('horizon.app.core')); beforeEach(module('horizon.app.murano')); var nova = {getAggregateExtraSpecs: function() {}, getFlavorExtraSpecs: function() {}, editAggregateExtraSpecs: function() {}, editFlavorExtraSpecs: function() {}, getInstanceMetadata: function() {}, editInstanceMetadata: function() {} }; var glance = {getImageProps: function() {}, editImageProps: function() {}, getNamespaces: function() {}}; var cinder = {getVolumeMetadata:function() {}, getVolumeSnapshotMetadata:function() {}, getVolumeTypeMetadata:function() {}, editVolumeMetadata: function() {}, editVolumeSnapshotMetadata: function() {}}; beforeEach(function() { module(function($provide) { $provide.value('horizon.app.core.openstack-service-api.nova', nova); $provide.value('horizon.app.core.openstack-service-api.glance', glance); $provide.value('horizon.app.core.openstack-service-api.cinder', cinder); }); }); var metadataService; beforeEach(inject(function($injector) { metadataService = $injector.get('horizon.app.core.metadata.service'); })); it('should get aggregate metadata', function() { var expected = 'aggregate metadata'; spyOn(nova, 'getAggregateExtraSpecs').and.returnValue(expected); var actual = metadataService.getMetadata('aggregate', '1'); expect(actual).toBe(expected); }); it('should edit aggregate metadata', function() { spyOn(nova, 'editAggregateExtraSpecs'); metadataService.editMetadata('aggregate', '1', 'updated', ['removed']); expect(nova.editAggregateExtraSpecs).toHaveBeenCalledWith('1', 'updated', ['removed']); }); it('should get aggregate namespace', function() { spyOn(glance, 'getNamespaces'); metadataService.getNamespaces('aggregate'); expect(glance.getNamespaces) .toHaveBeenCalledWith({ resource_type: 'OS::Nova::Aggregate' }, false); }); it('should get flavor metadata', function() { var expected = 'flavor metadata'; spyOn(nova, 'getFlavorExtraSpecs').and.returnValue(expected); var actual = metadataService.getMetadata('flavor', '1'); expect(actual).toBe(expected); }); it('should edit flavor metadata', function() { spyOn(nova, 'editFlavorExtraSpecs'); metadataService.editMetadata('flavor', '1', 'updated', ['removed']); expect(nova.editFlavorExtraSpecs).toHaveBeenCalledWith('1', 'updated', ['removed']); }); it('should get flavor namespace', function() { spyOn(glance, 'getNamespaces'); metadataService.getNamespaces('flavor'); expect(glance.getNamespaces) .toHaveBeenCalledWith({ resource_type: 'OS::Nova::Flavor' }, false); }); it('should get image metadata', function() { var expected = 'image metadata'; spyOn(glance, 'getImageProps').and.returnValue(expected); var actual = metadataService.getMetadata('image', '1'); expect(actual).toBe(expected); }); it('should edit image metadata', function() { spyOn(glance, 'editImageProps'); metadataService.editMetadata('image', '1', 'updated', ['removed']); expect(glance.editImageProps).toHaveBeenCalledWith('1', 'updated', ['removed']); }); it('should edit volume metadata', function() { spyOn(cinder, 'editVolumeMetadata'); metadataService.editMetadata('volume', '1', 'updated', ['removed']); expect(cinder.editVolumeMetadata).toHaveBeenCalledWith('1', 'updated', ['removed']); }); it('should edit volume snapshot metadata', function() { spyOn(cinder, 'editVolumeSnapshotMetadata'); metadataService.editMetadata('volume_snapshot', '1', 'updated', ['removed']); expect(cinder.editVolumeSnapshotMetadata).toHaveBeenCalledWith('1', 'updated', ['removed']); }); it('should get image namespace', function() { spyOn(glance, 'getNamespaces'); metadataService.getNamespaces('image'); expect(glance.getNamespaces) .toHaveBeenCalledWith({ resource_type: 'OS::Glance::Image' }, false); }); it('should get instance metadata', function() { var expected = 'instance metadata'; spyOn(nova, 'getInstanceMetadata').and.returnValue(expected); var actual = metadataService.getMetadata('instance', '1'); expect(actual).toBe(expected); }); it('should get volume metadata', function() { var expected = 'volume metadata'; spyOn(cinder, 'getVolumeMetadata').and.returnValue(expected); var actual = metadataService.getMetadata('volume', '1'); expect(actual).toBe(expected); }); it('should edit instance metadata', function() { spyOn(nova, 'editInstanceMetadata'); metadataService.editMetadata('instance', '1', 'updated', ['removed']); expect(nova.editInstanceMetadata).toHaveBeenCalledWith('1', 'updated', ['removed']); }); it('should get instance namespace', function() { spyOn(glance, 'getNamespaces'); metadataService.getNamespaces('instance', 'metadata'); expect(glance.getNamespaces) .toHaveBeenCalledWith({ resource_type: 'OS::Nova::Server', properties_target: 'metadata' }, false); }); }); })(); murano-dashboard-5.0.0/muranodashboard/static/app/murano/0000775000175100017510000000000013245511556023516 5ustar zuulzuul00000000000000murano-dashboard-5.0.0/muranodashboard/static/app/murano/murano.service.spec.js0000666000175100017510000000664513245511125027752 0ustar zuulzuul00000000000000/** * Copyright 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. */ (function () { 'use strict'; describe('Murano API', function () { var testCall, service; var apiService = {}; var toastService = {}; beforeEach( module('horizon.mock.openstack-service-api', function($provide, initServices) { testCall = initServices($provide, apiService, toastService); }) ); beforeEach(module('horizon.app.core.openstack-service-api')); beforeEach(inject([ 'horizon.app.core.openstack-service-api.murano', function(muranoAPI) { service = muranoAPI; }])); it('defines the service', function () { expect(service).toBeDefined(); }); var tests = [ { func: 'getPackages', method: 'get', path: '/api/app-catalog/packages/', data: {params: 'config'}, error: 'Unable to retrieve the packages.', testInput: [ 'config' ] }, { func: 'getComponentMeta', method: 'get', path: '/api/app-catalog/environments/1/components/2/metadata/', data: {params: {session: 'sessionId'}}, error: 'Unable to retrieve component metadata.', testInput: [{session: 'sessionId', environment: '1', component: '2'}] }, { func: 'editComponentMeta', method: 'post', path: '/api/app-catalog/environments/1/components/2/metadata/', call_args: [ '/api/app-catalog/environments/1/components/2/metadata/', {updated: {'key1': 10}, removed: ['key2']}, {params: {session: 'sessionId'}} ], error: 'Unable to edit component metadata.', testInput: [ {session: 'sessionId', environment: '1', component: '2'}, {'key1': 10}, ['key2'] ] }, { func: 'getEnvironmentMeta', method: 'get', path: '/api/app-catalog/environments/1/metadata/', data: {params: {session: 'sessionId'}}, error: 'Unable to retrieve environment metadata.', testInput: [{session: 'sessionId', environment: '1'}] }, { func: 'editEnvironmentMeta', method: 'post', path: '/api/app-catalog/environments/1/metadata/', call_args: [ '/api/app-catalog/environments/1/metadata/', {updated: {'key1': 10}, removed: ['key2']}, {params: {session: 'sessionId'}} ], error: 'Unable to edit environment metadata.', testInput: [ {session: 'sessionId', environment: '1'}, {'key1': 10}, ['key2'] ] } ]; // Iterate through the defined tests and apply as Jasmine specs. angular.forEach(tests, function(params) { it('defines the ' + params.func + ' call properly', function () { var callParams = [apiService, service, toastService, params]; testCall.apply(this, callParams); }); }); }); })(); murano-dashboard-5.0.0/muranodashboard/static/app/murano/murano.module.js0000666000175100017510000000574513245511125026646 0ustar zuulzuul00000000000000/** * Copyright 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. */ (function() { 'use strict'; /** * @ngdoc horizon.app.murano * @ng-module * @description * Provides all of the services and widgets required * to support and display the Murano Packages panel. */ angular .module('horizon.app.murano', []) .config(config); config.$inject = [ '$injector', '$provide' ]; function config($injector, $provide) { if ($injector.has('horizon.app.core.metadata.service')) { $provide.decorator('horizon.app.core.metadata.service', patchMetadata); } patchMetadata.$inject = [ '$delegate', 'horizon.app.core.openstack-service-api.murano', 'horizon.app.core.openstack-service-api.glance' ]; function patchMetadata($delegate, murano, glance) { var origEditMetadata = $delegate.editMetadata; var origGetMetadata = $delegate.getMetadata; var origGetNamespaces = $delegate.getNamespaces; $delegate.editMetadata = editMetadata; $delegate.getMetadata = getMetadata; $delegate.getNamespaces = getNamespaces; return $delegate; function getMetadata(resource, id) { if (resource === 'muranoapp') { return murano.getComponentMeta(id); } if (resource === 'muranoenv') { return murano.getEnvironmentMeta(id); } return origGetMetadata(resource, id); } function editMetadata(resource, id, updated, removed) { if (resource === 'muranoapp') { return murano.editComponentMeta(id, updated, removed); } if (resource === 'muranoenv') { return murano.editEnvironmentMeta(id, updated, removed); } return origEditMetadata(resource, id, updated, removed); } function getNamespaces(resource, propertiesTarget) { var params; if (resource === 'muranoapp') { params = {resource_type: 'OS::Murano::Application'}; if (propertiesTarget) { params.properties_target = propertiesTarget; } return glance.getNamespaces(params, false); } if (resource === 'muranoenv') { params = {resource_type: 'OS::Murano::Environment'}; if (propertiesTarget) { params.properties_target = propertiesTarget; } return glance.getNamespaces(params, false); } return origGetNamespaces(resource, propertiesTarget); } } } })(); murano-dashboard-5.0.0/muranodashboard/static/app/murano/murano.service.js0000666000175100017510000001542013245511125027010 0ustar zuulzuul00000000000000/** * 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. */ (function () { 'use strict'; angular .module('horizon.app.core.openstack-service-api') .factory('horizon.app.core.openstack-service-api.murano', muranoAPI); muranoAPI.$inject = [ 'horizon.framework.util.http.service', 'horizon.framework.widgets.toast.service' ]; /** * @ngdoc service * @name horizon.app.core.openstack-service-api.murano * @description Provides direct pass through to Murano with NO abstraction. */ function muranoAPI(apiService, toastService) { var service = { getPackages: getPackages, getComponentMeta: getComponentMeta, editComponentMeta: editComponentMeta, getEnvironmentMeta: getEnvironmentMeta, editEnvironmentMeta: editEnvironmentMeta }; return service; /** * @name horizon.app.core.openstack-service-api.murano.getPackages * @description * Get a list of packages. * * The listing result is an object with property "packages". Each item is * an packages. * * @param {Object} params * Query parameters. Optional. * * @param {boolean} params.paginate * True to paginate automatically. * * @param {string} params.marker * Specifies the image of the last-seen image. * * The typical pattern of limit and marker is to make an * initial limited request and then to use the last * image from the response as the marker parameter * in a subsequent limited request. With paginate, limit * is automatically set. * * @param {string} params.sort_dir * The sort direction ('asc' or 'desc'). */ function getPackages(params) { var config = params ? { "params" : params} : {}; return apiService.get('/api/app-catalog/packages/', config) .error(function () { toastService.add('error', gettext('Unable to retrieve the packages.')); }); } /** * @name horizon.app.core.openstack-service-api.murano.getComponentMeta * @description * Get metadata attributes associated with a given component * * @param {Object} target * The object identifying the target component * * @param {string} target.environment * The identifier of the environment the component belongs to * * @param {string} target.component * The identifier of the component within the environment * * @param {string} target.session * The identifier of the configuration session for which the data should be * fetched * * @returns {Object} The metadata object */ function getComponentMeta(target) { var params = { params: { session: target.session} }; var url = '/api/app-catalog/environments/' + target.environment + '/components/' + target.component + '/metadata/'; return apiService.get(url, params) .error(function () { toastService.add('error', gettext('Unable to retrieve component metadata.')); }); } /** * @name horizon.app.core.openstack-service-api.murano.editComponentMetadata * @description * Update metadata attributes associated with a given component * * @param {Object} target * The object identifying the target component * * @param {string} target.environment * The identifier of the environment the component belongs to * * @param {string} target.component * The identifier of the component within the environment * * @param {string} target.session * The identifier of the configuration session for which the data should be * updated * * @param {object} updated New metadata definitions. * * @param {[]} removed Names of removed metadata definitions. * * @returns {Object} The result of the API call */ function editComponentMeta(target, updated, removed) { var params = { params: { session: target.session} }; var url = '/api/app-catalog/environments/' + target.environment + '/components/' + target.component + '/metadata/'; return apiService.post( url, { updated: updated, removed: removed}, params) .error(function () { toastService.add('error', gettext('Unable to edit component metadata.')); }); } /** * @name horizon.app.core.openstack-service-api.murano.getEnvironmentMeta * @description * Get metadata attributes associated with a given environment * * @param {Object} target * The object identifying the target environment * * @param {string} target.environment * The identifier of the target environment * * @param {string} target.session * The identifier of the configuration session for which the data should be * fetched * * @returns {Object} The metadata object */ function getEnvironmentMeta(target) { var params = { params: { session: target.session} }; var url = '/api/app-catalog/environments/' + target.environment + '/metadata/'; return apiService.get(url, params) .error(function () { toastService.add('error', gettext('Unable to retrieve environment metadata.')); }); } /** * @name horizon.app.core.openstack-service-api.murano.editEnvironmentMeta * @description * Update metadata attributes associated with a given environment * * @param {Object} target * The object identifying the target environment * * @param {string} target.environment * The identifier of the environment the component belongs to * * @param {string} target.session * The identifier of the configuration session for which the data should be * updated * * @param {object} updated New metadata definitions. * * @param {[]} removed Names of removed metadata definitions. * * @returns {Object} The result of the API call */ function editEnvironmentMeta(target, updated, removed) { var params = { params: { session: target.session} }; var url = '/api/app-catalog/environments/' + target.environment + '/metadata/'; return apiService.post( url, { updated: updated, removed: removed}, params) .error(function () { toastService.add('error', gettext('Unable to edit environment metadata.')); }); } } })(); murano-dashboard-5.0.0/muranodashboard/static/app/murano/murano.module.spec.js0000666000175100017510000000707413245511125027574 0ustar zuulzuul00000000000000/** * Copyright 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. */ (function () { 'use strict'; describe('horizon.app.murano', function () { it('should be defined', function () { expect(angular.module('horizon.app.murano')).toBeDefined(); }); }); describe('murano metadata patcher', function () { var metadataService, glance, murano; var lastFakeCallArgs = []; var fakeResult = 'fakeMeta'; beforeEach(function () { murano = { getComponentMeta: fakeMeta, editComponentMeta: fakeMeta, getEnvironmentMeta: fakeMeta, editEnvironmentMeta: fakeMeta }; module('horizon.framework'); module('horizon.app.core'); module('horizon.app.murano'); module(function($provide) { $provide.value('horizon.app.core.openstack-service-api.murano', murano); }); function fakeMeta() { var i; for (i = 0, lastFakeCallArgs = []; i < arguments.length; i++) { lastFakeCallArgs.push(arguments[i]); } return fakeResult; } }); beforeEach(inject(function ($injector) { metadataService = $injector.get('horizon.app.core.metadata.service'); glance = $injector.get('horizon.app.core.openstack-service-api.glance'); })); it('should get component metadata', function () { var actual = metadataService.getMetadata('muranoapp', 'compId'); expect(actual).toBe(fakeResult); expect(lastFakeCallArgs).toEqual(['compId']); }); it('should get environment metadata', function () { var actual = metadataService.getMetadata('muranoenv', 'envId'); expect(actual).toBe(fakeResult); expect(lastFakeCallArgs).toEqual(['envId']); }); it('should edit component metadata', function () { metadataService.editMetadata('muranoapp', 'compId', {'key1': 10}, ['key2']); expect(lastFakeCallArgs).toEqual(['compId', {'key1': 10}, ['key2']]); }); it('should edit environment metadata', function () { metadataService.editMetadata('muranoenv', 'envId', {'key1': 10}, ['key2']); expect(lastFakeCallArgs).toEqual(['envId', {'key1': 10}, ['key2']]); }); it('should get component namespace', function () { var params, flag; spyOn(glance, 'getNamespaces').and.callFake(function(_params, _flag) { params = _params; flag = _flag; }); metadataService.getNamespaces('muranoapp', 'something'); expect(params).toEqual({ resource_type: 'OS::Murano::Application', properties_target: 'something' }); expect(flag).toBe(false); }); it('should get environment namespace', function () { var params, flag; spyOn(glance, 'getNamespaces').and.callFake(function(_params, _flag) { params = _params; flag = _flag; }); metadataService.getNamespaces('muranoenv', 'something'); expect(params).toEqual({ resource_type: 'OS::Murano::Environment', properties_target: 'something' }); expect(flag).toBe(false); }); }); })(); murano-dashboard-5.0.0/muranodashboard/environments/0000775000175100017510000000000013245511556022675 5ustar zuulzuul00000000000000murano-dashboard-5.0.0/muranodashboard/environments/views.py0000666000175100017510000003336513245511125024410 0ustar zuulzuul00000000000000# 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'), None)] 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() murano-dashboard-5.0.0/muranodashboard/environments/tabs.py0000666000175100017510000002365213245511125024202 0ustar zuulzuul00000000000000# 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 heat_dashboard.api import heat as heat_api from horizon import exceptions from horizon import tabs 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 murano-dashboard-5.0.0/muranodashboard/environments/urls.py0000666000175100017510000000403313245511125024226 0ustar zuulzuul00000000000000# 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[^/]+)' 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[^/]+)/$', views.DetailServiceView.as_view(), name='service_details'), urls.url(ENVIRONMENT_ID + r'/start_action/(?P[^/]+)/$', views.StartActionView.as_view(), name='start_action'), urls.url(ENVIRONMENT_ID + r'/actions/(?P[^/]+)(?:/(?P[^/]+))?/$', views.ActionResultView.as_view(), name='action_result'), urls.url(r'^(?P[^/]+)/$', 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[^/]+)$', views.DeploymentDetailsView.as_view(), name='deployment_details'), ] murano-dashboard-5.0.0/muranodashboard/environments/panel.py0000666000175100017510000000147713245511125024351 0ustar zuulzuul00000000000000# 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"),) murano-dashboard-5.0.0/muranodashboard/environments/api.py0000666000175100017510000004171113245511125024016 0ustar zuulzuul00000000000000# 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} '.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 '. 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 '.format(session_id)) env = api.muranoclient(request).sessions.deploy(environment_id, session_id) LOG.debug('Environment::Deploy ' ''.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 '.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) murano-dashboard-5.0.0/muranodashboard/environments/__init__.py0000666000175100017510000000000013245511125024766 0ustar zuulzuul00000000000000murano-dashboard-5.0.0/muranodashboard/environments/consts.py0000666000175100017510000000654513245511125024564 0ustar zuulzuul00000000000000# 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')), ) murano-dashboard-5.0.0/muranodashboard/environments/forms.py0000666000175100017510000000775013245511125024400 0ustar zuulzuul00000000000000# 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 = horizon_forms.ThemableChoiceField( 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(self): cleaned_data = super(CreateEnvironmentForm, self).clean() env_name = cleaned_data.get('name') if not env_name or not env_name.strip(): self._errors['name'] = self.error_class([ENV_NAME_HELP_TEXT]) return cleaned_data 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 murano-dashboard-5.0.0/muranodashboard/environments/tables.py0000666000175100017510000006526413245511125024530 0ustar zuulzuul00000000000000# 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,) murano-dashboard-5.0.0/muranodashboard/environments/topology.py0000666000175100017510000002446613245511125025131 0ustar zuulzuul00000000000000# 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) murano-dashboard-5.0.0/muranodashboard/dynamic_ui/0000775000175100017510000000000013245511556022267 5ustar zuulzuul00000000000000murano-dashboard-5.0.0/muranodashboard/dynamic_ui/version.py0000666000175100017510000000266713245511125024333 0ustar zuulzuul00000000000000# 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) murano-dashboard-5.0.0/muranodashboard/dynamic_ui/services.py0000666000175100017510000002565513245511125024473 0ustar zuulzuul00000000000000# 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, region) = 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, region) # 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, _region=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 region = _region 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', []), form_data.get('region')) 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 murano-dashboard-5.0.0/muranodashboard/dynamic_ui/yaql_expression.py0000666000175100017510000000341113245511125026057 0ustar zuulzuul00000000000000# 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) murano-dashboard-5.0.0/muranodashboard/dynamic_ui/__init__.py0000666000175100017510000000000013245511125024360 0ustar zuulzuul00000000000000murano-dashboard-5.0.0/muranodashboard/dynamic_ui/helpers.py0000666000175100017510000001141013245511125024272 0ustar zuulzuul00000000000000# 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 contextlib 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 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] elif isinstance(val, ObjectID): return val.object_id 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 @contextlib.contextmanager def current_region(request, region): orig_region = request.user.services_region if region is not None: request.user.services_region = region try: yield finally: request.user.services_region = orig_region murano-dashboard-5.0.0/muranodashboard/dynamic_ui/forms.py0000666000175100017510000002065113245511125023765 0ustar zuulzuul00000000000000# 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, name, cleaned_data) if value: cleaned_data[name] = value LOG.debug("Update '%s' data in postclean method" % name) self.service.update_cleaned_data(cleaned_data, form=self) return cleaned_data murano-dashboard-5.0.0/muranodashboard/dynamic_ui/yaql_functions.py0000666000175100017510000001352213245511125025674 0ustar zuulzuul00000000000000# 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 from castellan.common import exception as castellan_exception from castellan.common.objects import opaque_data from castellan import key_manager from castellan import options from keystoneauth1 import identity from keystoneauth1 import session from django.conf import settings from oslo_config import cfg from oslo_context import context as _oslo_context from oslo_log import log as logging LOG = logging.getLogger(__name__) @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 @specs.parameter('data', yaqltypes.String()) def _encrypt_data(context, data): try: # TODO(pbourke): move auth construction into common area if it ends up # been required in other areas auth = identity.V3Password( auth_url=settings.KEY_MANAGER['auth_url'], username=settings.KEY_MANAGER['username'], user_domain_name=settings.KEY_MANAGER['user_domain_name'], password=settings.KEY_MANAGER['password'], project_name=settings.KEY_MANAGER['project_name'], project_domain_name=settings.KEY_MANAGER['project_domain_name'] ) except (KeyError, AttributeError) as e: LOG.exception(e) msg = ('Could not find valid key manager credentials in the ' 'murano-dashboard config. encryptData yaql function not ' 'available') raise castellan_exception.KeyManagerError(message_arg=msg) sess = session.Session(auth=auth) auth_context = _oslo_context.RequestContext( auth_token=auth.get_token(sess), tenant=auth.get_project_id(sess)) options.set_defaults(cfg.CONF, auth_endpoint=settings.KEY_MANAGER['auth_url']) manager = key_manager.API() try: # TODO(pbourke): while we feel opaque data should cover the most common # use case, we may want to add support for other secret types in the # future (see https://goo.gl/tZhfqe) stored_key_id = manager.store(auth_context, opaque_data.OpaqueData(data)) except castellan_exception.KeyManagerError as e: LOG.exception(e) raise return stored_key_id def register(context): context.register_function(_repeat, 'repeat') context.register_function(_generate_hostname, 'generateHostname') context.register_function(_name, 'name') context.register_function(_ref, 'ref') context.register_function(_encrypt_data, 'encryptData') murano-dashboard-5.0.0/muranodashboard/dynamic_ui/fields.py0000666000175100017510000007105713245511125024113 0ustar zuulzuul00000000000000# 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.dynamic_ui import helpers 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, region=None): 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) with helpers.current_region(request, region): 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 super(ChoiceField, self).__init__(**kwargs) class DynamicChoiceField(hz_forms.DynamicChoiceField, CustomPropertiesField): pass class FlavorWidget(widgets.Select): def __init__(self, *args, **kwargs): super(FlavorWidget, self).__init__(*args, **kwargs) self.attrs['class'] = self.attrs.get('class', '') + ' flavor' self.attrs['id'] = 'id_flavor' class FlavorChoiceField(ChoiceField): widget = FlavorWidget 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, form=None, **kwargs): choices = [] with helpers.current_region(request, getattr(form, 'region', None)): flavors = nova.novaclient(request).flavors.list() # If no requirements are present, return all the flavors. if not hasattr(self, 'requirements'): choices = [(flavor.id, 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.id, 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_id, flavor_name in self.choices: if 'medium' in flavor_name: self.initial = flavor_id break def clean(self, value): for flavor_id, flavor_name in self.choices: if flavor_id == value: return flavor_name return value class KeyPairChoiceField(DynamicChoiceField): """This widget allows to select keypair for VMs""" @with_request def update(self, request, form=None, **kwargs): self.choices = [('', _('No keypair'))] with helpers.current_region(request, getattr(form, 'region', None)): keypairs = nova.novaclient(request).keypairs.list() for keypair in sorted(keypairs, 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.SelectWidget(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, form=None, **kwargs): image_map, image_choices = {}, [] murano_images = get_murano_images( request, getattr(form, 'region', None)) 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, 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.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.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, form=None, **kwargs): try: with helpers.current_region(request, getattr(form, 'region', None)): 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_volumes=True, include_snapshots=True, *args, **kwargs): self.include_volumes = include_volumes 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} choices = [] if self.include_volumes: try: choices.extend((volume.id, volume.name) for volume in cinder.volume_list(request, search_opts=available)) except Exception: 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 "
    {message}
    ".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'))] murano-dashboard-5.0.0/muranodashboard/packages/0000775000175100017510000000000013245511556021724 5ustar zuulzuul00000000000000murano-dashboard-5.0.0/muranodashboard/packages/views.py0000666000175100017510000006022013245511125023425 0ustar zuulzuul00000000000000# 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 import sys from django.core.files import storage 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 _ # 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 horizon import exceptions from horizon.forms import views from horizon import messages from horizon import tables as horizon_tables from horizon.utils import functions as utils from horizon import views as horizon_views from muranoclient.common import exceptions as exc from muranoclient.common import utils as muranoclient_utils from openstack_dashboard.api import glance from openstack_dashboard.api import keystone from oslo_log import log as logging import six import six.moves.urllib.parse as urlparse from muranodashboard import api from muranodashboard.api import packages as pkg_api from muranodashboard.catalog import views as catalog_views from muranodashboard.common import utils as muranodashboard_utils from muranodashboard.environments import consts from muranodashboard.packages import consts as packages_consts from muranodashboard.packages import forms from muranodashboard.packages import tables LOG = logging.getLogger(__name__) FORMS = [('upload', forms.ImportPackageForm), ('modify', forms.UpdatePackageForm), ('add_category', forms.SelectCategories)] BUNDLE_FORMS = [('upload', forms.ImportBundleForm), ] def is_app(wizard): """Check if we're uploading an application Return true if uploading package is an application. In that case, category selection form need to be shown. """ step_data = wizard.storage.get_step_data('upload') if step_data: return step_data['package'].type == 'Application' return False def _ensure_images(name, package, request, step_data=None): glance_client = glance.glanceclient( request, version='2') base_url = packages_consts.MURANO_REPO_URL image_specs = package.images() try: imgs = muranoclient_utils.ensure_images( glance_client=glance_client, image_specs=image_specs, base_url=base_url) for img in imgs: msg = _("Trying to add {0} image to glance. " "Image will be ready for deployment after " "successful upload").format(img['name'],) messages.warning(request, msg) log_msg = _("Trying to add {0}, {1} image to " "glance. Image will be ready for " "deployment after successful upload")\ .format(img['name'], img['id'],) LOG.info(log_msg) if step_data: step_data['images'].append(img) except Exception as e: msg = _("Error {0} occurred while installing " "images for {1}").format(e, name) messages.error(request, msg) LOG.exception(msg) class PackageDefinitionsView(horizon_tables.DataTableView): table_class = tables.PackageDefinitionsTable template_name = 'packages/index.html' page_title = _("Packages") _more = False _prev = False def has_more_data(self, table): return self._more def has_prev_data(self, table): return self._prev def get_data(self): sort_dir = self.request.GET.get('sort_dir', 'asc') opts = { 'include_disabled': True, 'sort_dir': sort_dir, } marker = self.request.GET.get( tables.PackageDefinitionsTable._meta.pagination_param, None) opts = self.get_filters(opts) packages = [] page_size = utils.get_page_size(self.request) with api.handled_exceptions(self.request): packages, extra = pkg_api.package_list( self.request, marker=marker, filters=opts, paginate=True, page_size=page_size) if sort_dir == 'asc': self._more = extra else: packages = list(reversed(packages)) self._prev = extra if packages: if sort_dir == 'asc': backward_marker = packages[0].id opts['sort_dir'] = 'desc' else: backward_marker = packages[-1].id opts['sort_dir'] = 'asc' __, extra = pkg_api.package_list( self.request, filters=opts, paginate=True, marker=backward_marker, page_size=0) if sort_dir == 'asc': self._prev = extra else: self._more = extra # Add information about project tenant for admin user if self.request.user.is_superuser: tenants = [] try: tenants, _more = keystone.tenant_list(self.request) except Exception: exceptions.handle(self.request, _("Unable to retrieve project list.")) tenent_name_by_id = {tenant.id: tenant.name for tenant in tenants} for i, p in enumerate(packages): packages[i].tenant_name = tenent_name_by_id.get(p.owner_id) else: current_tenant = self.request.session['token'].tenant for i, package in enumerate(packages): if package.owner_id == current_tenant['id']: packages[i].tenant_name = current_tenant['name'] else: packages[i].tenant_name = _('UNKNOWN') return packages def get_context_data(self, **kwargs): context = super(PackageDefinitionsView, self).get_context_data(**kwargs) context['tenant_id'] = self.request.session['token'].tenant['id'] return context def get_filters(self, filters): filter_action = self.table._meta._filter_action if filter_action: filter_field = self.table.get_filter_field() if filter_action.is_api_filter(filter_field): filter_string = self.table.get_filter_string() if filter_field and filter_string: filters[filter_field] = filter_string return filters class ImportBundleWizard(horizon_views.PageTitleMixin, views.ModalFormMixin, wizard_views.SessionWizardView): template_name = 'packages/import_bundle.html' page_title = _("Import Bundle") def get_context_data(self, **kwargs): context = super(ImportBundleWizard, self).get_context_data(**kwargs) repo_url = urlparse.urlparse(packages_consts.MURANO_REPO_URL) context['murano_repo_url'] = "{}://{}".format( repo_url.scheme, repo_url.netloc) return context def get_form_initial(self, step): initial_dict = self.initial_dict.get(step, {}) if step == 'upload': for name in ['url', 'name', 'import_type']: if name in self.request.GET: initial_dict[name] = self.request.GET[name] return initial_dict def process_step(self, form): @catalog_views.update_latest_apps def _update_latest_apps(request, app_id): LOG.info('Adding {0} application to the' ' latest apps list'.format(app_id)) step_data = self.get_form_step_data(form) if self.steps.current == 'upload': import_type = form.cleaned_data['import_type'] data = {} f = None base_url = packages_consts.MURANO_REPO_URL if import_type == 'by_url': f = form.cleaned_data['url'] elif import_type == 'by_name': f = muranoclient_utils.to_url( form.cleaned_data['name'], path='bundles/', base_url=base_url, extension='.bundle', ) try: bundle = muranoclient_utils.Bundle.from_file(f) except Exception as e: if '(404)' in e.message: msg = _("Bundle creation failed." "Reason: Can't find Bundle name from repository.") else: msg = _("Bundle creation failed." "Reason: {0}").format(e) LOG.exception(msg) messages.error(self.request, msg) raise exceptions.Http302( reverse('horizon:app-catalog:packages:index')) for package_spec in bundle.package_specs(): try: package = muranoclient_utils.Package.from_location( package_spec['Name'], version=package_spec.get('Version'), url=package_spec.get('Url'), base_url=base_url, path=None, ) except Exception as e: msg = _("Error {0} occurred while parsing package {1}")\ .format(e, package_spec.get('Name')) messages.error(self.request, msg) LOG.exception(msg) continue reqs = package.requirements(base_url=base_url) for dep_name, dep_package in six.iteritems(reqs): _ensure_images(dep_name, dep_package, self.request) try: files = {dep_name: dep_package.file()} package = api.muranoclient( self.request).packages.create(data, files) messages.success( self.request, _('Package {0} uploaded').format(dep_name) ) _update_latest_apps( request=self.request, app_id=package.id) except exc.HTTPConflict: msg = _("Package {0} already registered.").format( dep_name) messages.warning(self.request, msg) LOG.exception(msg) except exc.HTTPException as e: reason = muranodashboard_utils.parse_api_error( getattr(e, 'details', '')) if not reason: raise msg = _("Package {0} upload failed. {1}").format( dep_name, reason) messages.warning(self.request, msg) LOG.exception(msg) except Exception as e: msg = _("Importing package {0} failed. " "Reason: {1}").format(dep_name, e) messages.warning(self.request, msg) LOG.exception(msg) continue return step_data def done(self, form_list, **kwargs): redirect = reverse('horizon:app-catalog:packages:index') msg = _('Bundle successfully imported.') LOG.info(msg) messages.success(self.request, msg) return http.HttpResponseRedirect(redirect) class ImportPackageWizard(horizon_views.PageTitleMixin, views.ModalFormMixin, wizard_views.SessionWizardView): file_storage = storage.FileSystemStorage(location=consts.CACHE_DIR) template_name = 'packages/upload.html' condition_dict = {'add_category': is_app} page_title = _("Import Package") def get_form_initial(self, step): initial_dict = self.initial_dict.get(step, {}) if step == 'upload': for name in ['url', 'repo_name', 'repo_version', 'import_type']: if name in self.request.GET: initial_dict[name] = self.request.GET[name] return initial_dict def get_context_data(self, **kwargs): context = super(ImportPackageWizard, self).get_context_data(**kwargs) repo_url = urlparse.urlparse(packages_consts.MURANO_REPO_URL) context['murano_repo_url'] = "{}://{}".format( repo_url.scheme, repo_url.netloc) return context def done(self, form_list, **kwargs): data = self.get_all_cleaned_data() app_id = self.storage.get_step_data('upload')['package'].id # Remove package file from result data for key in ('package', 'import_type', 'url', 'repo_version', 'repo_name'): del data[key] dep_pkgs = self.storage.get_step_data('upload').get( 'dependencies', []) installed_images = self.storage.get_step_data('upload').get( 'images', []) redirect = reverse('horizon:app-catalog:packages:index') dep_data = {'enabled': data['enabled'], 'is_public': data['is_public']} murano_client = api.muranoclient(self.request) for dep_pkg in dep_pkgs: try: murano_client.packages.update(dep_pkg.id, dep_data) LOG.debug('Success update for package {0}.'.format(dep_pkg.id)) except Exception as e: msg = _("Couldn't update package {0} parameters. Error: {1}")\ .format(dep_pkg.fully_qualified_name, e) LOG.warning(msg) messages.warning(self.request, msg) # Images have been imported as private images during the 'upload' step # If the package is public, make the required images public if data['is_public']: try: glance_client = glance.glanceclient(self.request, '1') except Exception: glance_client = None if glance_client: for img in installed_images: try: glance_client.images.update(img['id'], is_public=True) LOG.debug( 'Success update for image {0}'.format(img['id'])) except Exception as e: msg = _("Error {0} occurred while setting image {1}, " "{2} public").format(e, img['name'], img['id']) messages.error(self.request, msg) LOG.exception(msg) elif len(installed_images): msg = _("Couldn't initialise glance v1 client, " "therefore could not make the following images " "public: {0}").format(' '.join( [img['name'] for img in installed_images])) messages.warning(self.request, msg) LOG.warning(msg) try: data['tags'] = [t.strip() for t in data['tags'].split(',')] murano_client.packages.update(app_id, data) except exc.HTTPForbidden: msg = _("You are not allowed to change" " this properties of the package") LOG.exception(msg) exceptions.handle( self.request, msg, redirect=reverse('horizon:app-catalog:packages:index')) except (exc.HTTPException, Exception): LOG.exception(_('Modifying package failed')) exceptions.handle(self.request, _('Unable to modify package'), redirect=redirect) else: msg = _('Package parameters successfully updated.') LOG.info(msg) messages.success(self.request, msg) return http.HttpResponseRedirect(redirect) def _handle_exception(self, original_e): exc_info = sys.exc_info() reason = '' if hasattr(original_e, 'details'): try: error = json.loads(original_e.details).get('error') if error: reason = error.get('message') except ValueError: # Let horizon operate with original exception six.reraise(original_e.__class__, original_e.__class__(original_e), exc_info[2]) msg = _('Uploading package failed. {0}').format(reason) LOG.exception(msg) exceptions.handle( self.request, msg, redirect=reverse('horizon:app-catalog:packages:index')) def process_step(self, form): @catalog_views.update_latest_apps def _update_latest_apps(request, app_id): LOG.info('Adding {0} application to the' ' latest apps list'.format(app_id)) step_data = self.get_form_step_data(form).copy() if self.steps.current == 'upload': import_type = form.cleaned_data['import_type'] data = {} f = None base_url = packages_consts.MURANO_REPO_URL if import_type == 'upload': pkg = form.cleaned_data['package'] f = pkg.file elif import_type == 'by_url': f = form.cleaned_data['url'] elif import_type == 'by_name': name = form.cleaned_data['repo_name'] version = form.cleaned_data['repo_version'] f = muranoclient_utils.to_url( name, version=version, path='apps/', extension='.zip', base_url=base_url, ) try: package = muranoclient_utils.Package.from_file(f) name = package.manifest['FullName'] except Exception as e: if '(404)' in e.message: msg = _("Package creation failed." "Reason: Can't find Package name from repository.") else: msg = _("Package creation failed." "Reason: {0}").format(e) LOG.exception(msg) messages.error(self.request, msg) raise exceptions.Http302( reverse('horizon:app-catalog:packages:index')) reqs = package.requirements(base_url=base_url) original_package = reqs.pop(name) step_data['dependencies'] = [] step_data['images'] = [] for dep_name, dep_package in six.iteritems(reqs): _ensure_images(dep_name, dep_package, self.request, step_data) try: files = {dep_name: dep_package.file()} package = api.muranoclient(self.request).packages.create( data, files) messages.success( self.request, _('Package {0} uploaded').format(dep_name) ) _update_latest_apps( request=self.request, app_id=package.id) step_data['dependencies'].append(package) except exc.HTTPConflict: msg = _("Package {0} already registered.").format( dep_name) messages.warning(self.request, msg) LOG.exception(msg) except Exception as e: msg = _("Error {0} occurred while " "installing package {1}").format(e, dep_name) messages.error(self.request, msg) LOG.exception(msg) continue # add main packages images _ensure_images(name, original_package, self.request, step_data) # import main package itself try: files = {name: original_package.file()} package = api.muranoclient(self.request).packages.create( data, files) messages.success(self.request, _('Package {0} uploaded').format(name)) _update_latest_apps(request=self.request, app_id=package.id) step_data['package'] = package except exc.HTTPConflict: msg = _("Package with specified name already exists") LOG.exception(msg) exceptions.handle( self.request, msg, redirect=reverse('horizon:app-catalog:packages:index')) except exc.HTTPInternalServerError as e: self._handle_exception(e) except exc.HTTPException as e: reason = muranodashboard_utils.parse_api_error( getattr(e, 'details', '')) if not reason: raise LOG.exception(reason) exceptions.handle( self.request, reason, redirect=reverse('horizon:app-catalog:packages:index')) except Exception as original_e: self._handle_exception(original_e) return step_data def get_form_kwargs(self, step=None): kwargs = {} if step == 'add_category': kwargs.update({'request': self.request}) if step == 'modify': package = self.storage.get_step_data('upload').get('package') kwargs.update({'package': package, 'request': self.request}) return kwargs class ModifyPackageView(views.ModalFormView): form_class = forms.ModifyPackageForm template_name = 'packages/modify_package.html' success_url = reverse_lazy('horizon:app-catalog:packages:index') failure_url = reverse_lazy('horizon:app-catalog:packages:index') page_title = _("Modify Package") def get_initial(self): app_id = self.kwargs['app_id'] package = api.muranoclient(self.request).packages.get(app_id) return { 'package': package, 'app_id': app_id, } def get_context_data(self, **kwargs): context = super(ModifyPackageView, self).get_context_data(**kwargs) context['app_id'] = self.kwargs['app_id'] context['type'] = self.get_form().initial['package'].type return context class DetailView(horizon_views.HorizonTemplateView): template_name = 'packages/detail.html' page_title = "{{ app.name }}" def get_context_data(self, **kwargs): context = super(DetailView, self).get_context_data(**kwargs) app = self.get_data() context["app"] = app return context def get_data(self): app = None try: app_id = self.kwargs['app_id'] app = api.muranoclient(self.request).packages.get(app_id) except Exception: INDEX_URL = 'horizon:app-catalog:packages:index' exceptions.handle(self.request, _('Unable to retrieve package details.'), redirect=reverse(INDEX_URL)) return app def download_packge(request, app_name, app_id): try: body = api.muranoclient(request).packages.download(app_id) content_type = 'application/octet-stream' response = http.HttpResponse(body, content_type=content_type) response['Content-Disposition'] = 'filename={name}.zip'.format( name=app_name) return response except exc.HTTPException: LOG.exception(_('Something went wrong during package downloading')) redirect = reverse('horizon:app-catalog:packages:index') exceptions.handle(request, _('Unable to download package.'), redirect=redirect) murano-dashboard-5.0.0/muranodashboard/packages/urls.py0000666000175100017510000000246713245511125023266 0ustar zuulzuul00000000000000# 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.packages import views urlpatterns = [ urls.url(r'^$', views.PackageDefinitionsView.as_view(), name='index'), urls.url(r'^upload$', views.ImportPackageWizard.as_view( views.FORMS), name='upload'), urls.url(r'^import_bundle$', views.ImportBundleWizard.as_view( views.BUNDLE_FORMS), name='import_bundle'), urls.url(r'^modify/(?P[^/]+)?$', views.ModifyPackageView.as_view(), name='modify'), urls.url(r'^(?P[^/]+)?$', views.DetailView.as_view(), name='detail'), urls.url(r'^download/(?P[^/]+)/(?P[^/]+)?$', views.download_packge, name='download'), ] murano-dashboard-5.0.0/muranodashboard/packages/panel.py0000666000175100017510000000146713245511125023377 0ustar zuulzuul00000000000000# 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 PackageDefinitions(horizon.Panel): name = _("Packages") slug = 'packages' policy_rules = (("murano", "get_package"),) murano-dashboard-5.0.0/muranodashboard/packages/__init__.py0000666000175100017510000000000013245511125024015 0ustar zuulzuul00000000000000murano-dashboard-5.0.0/muranodashboard/packages/consts.py0000666000175100017510000000220613245511125023601 0ustar zuulzuul00000000000000# 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 oslo_log import log as logging LOG = logging.getLogger(__name__) MURANO_REPO_URL = getattr( settings, 'MURANO_REPO_URL', "http://apps.openstack.org/api/v1/murano_repo/liberty/") DISPLAY_MURANO_REPO_URL = getattr( settings, 'DISPLAY_MURANO_REPO_URL', "http://apps.openstack.org/#tab=murano-apps") try: MAX_FILE_SIZE_MB = int(getattr(settings, 'MAX_FILE_SIZE_MB', 5)) except ValueError: LOG.warning("MAX_FILE_SIZE_MB parameter has the incorrect value.") MAX_FILE_SIZE_MB = 5 murano-dashboard-5.0.0/muranodashboard/packages/forms.py0000666000175100017510000002677513245511125023437 0ustar zuulzuul00000000000000# 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 import sys from django.core.urlresolvers import reverse from django.core import validators 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 import policy from oslo_log import log as logging from muranoclient.common import exceptions as exc from muranodashboard import api from muranodashboard.packages import consts LOG = logging.getLogger(__name__) IMPORT_TYPE_CHOICES = [ ('upload', _('File')), ('by_name', _('Repository')), ('by_url', _('URL')), ] IMPORT_BUNDLE_TYPE_CHOICES = [ ('by_name', _('Repository')), ('by_url', _('URL')), ] class PackageURLField(forms.URLField): default_validators = [validators.URLValidator(schemes=["http", "https"])] class ImportBundleForm(forms.Form): import_type = forms.ChoiceField( label=_("Package Bundle Source"), choices=IMPORT_BUNDLE_TYPE_CHOICES, widget=forms.Select(attrs={ 'class': 'switchable', 'data-slug': 'source'})) url = PackageURLField( label=_("Bundle URL"), required=False, widget=forms.TextInput(attrs={ 'class': 'switched', 'data-switch-on': 'source', 'data-source-by_url': _('Bundle URL')}), help_text=_('An external http/https URL to load the bundle from.')) name = forms.CharField( label=_("Bundle Name"), required=False, widget=forms.TextInput(attrs={ 'class': 'switched', 'data-switch-on': 'source', 'data-source-by_name': _('Bundle Name')}), help_text=_("Name of the bundle.")) def clean(self): cleaned_data = super(ImportBundleForm, self).clean() import_type = cleaned_data.get('import_type') if import_type == 'by_name' and not cleaned_data.get('name'): msg = _('Please supply a bundle name') raise forms.ValidationError(msg) elif import_type == 'by_url' and not cleaned_data.get('url'): msg = _('Please supply a bundle url') raise forms.ValidationError(msg) return cleaned_data class ImportPackageForm(forms.Form): import_type = forms.ChoiceField( label=_("Package Source"), choices=IMPORT_TYPE_CHOICES, widget=forms.Select(attrs={ 'class': 'switchable', 'data-slug': 'source'})) url = PackageURLField( label=_("Package URL"), required=False, widget=forms.TextInput(attrs={ 'class': 'switched', 'data-switch-on': 'source', 'data-source-by_url': _('Package URL')}), help_text=_('An external http/https URL to load the package from.')) repo_name = horizon_forms.CharField( label=_("Package Name"), required=False, widget=forms.TextInput(attrs={ 'class': 'switched', 'data-switch-on': 'source', 'data-source-by_name': _('Package Name')}), help_text=_( 'Package name in the repository, usually a fully qualified name'), ) package = forms.FileField( label=_('Application Package'), required=False, widget=forms.FileInput(attrs={ 'class': 'switched', 'data-switch-on': 'source', 'data-source-upload': _('Application Package')}), help_text=_('A local zip file to upload')) repo_version = horizon_forms.CharField( label=_("Package version"), widget=forms.TextInput(attrs={ 'class': 'switched', 'data-switch-on': 'source', 'data-source-by_name': _('Package version')}), required=False) def __init__(self, *args, **kwargs): super(ImportPackageForm, self).__init__(*args, **kwargs) self.fields['repo_version'].widget.attrs['placeholder'] = \ _('Optional') def clean_package(self): package = self.cleaned_data.get('package') if package: max_size_in_bytes = consts.MAX_FILE_SIZE_MB << 20 if package.size > max_size_in_bytes: msg = _('It is forbidden to upload files larger than ' '{0} MB.').format(consts.MAX_FILE_SIZE_MB) LOG.error(msg) raise forms.ValidationError(msg) return package def clean(self): cleaned_data = super(ImportPackageForm, self).clean() import_type = cleaned_data.get('import_type') if import_type == 'upload' and not cleaned_data.get('package'): msg = _('Please supply a package file') LOG.error(msg) raise forms.ValidationError(msg) elif import_type == 'by_name' and not cleaned_data.get('repo_name'): msg = _('Please supply a package name') LOG.error(msg) raise forms.ValidationError(msg) elif import_type == 'by_url' and not cleaned_data.get('url'): msg = _('Please supply a package url') LOG.error(msg) raise forms.ValidationError(msg) return cleaned_data class PackageParamsMixin(forms.Form): name = forms.CharField(label=_('Name'), max_length=80, help_text=_('80 characters max.')) tags = forms.CharField(label=_('Tags'), required=False, help_text=_('Provide comma-separated list of words,' ' associated with the package')) is_public = forms.BooleanField(label=_('Public'), required=False, widget=forms.CheckboxInput) enabled = forms.BooleanField(label=_('Active'), required=False, widget=forms.CheckboxInput) description = forms.CharField(label=_('Description'), widget=forms.Textarea, required=False) def set_initial(self, request, package): self.fields['name'].initial = package.name self.fields['tags'].initial = ', '.join(package.tags) self.fields['is_public'].initial = package.is_public self.fields['enabled'].initial = package.enabled self.fields['description'].initial = package.description if not policy.check((("murano", "publicize_package"),), request): self.fields['is_public'].widget = forms.CheckboxInput( attrs={'readonly': 'readonly', 'disabled': 'disabled'}) self.fields['is_public'].help_text = _( 'You are not allowed to make packages public.') class UpdatePackageForm(PackageParamsMixin): def __init__(self, *args, **kwargs): package = kwargs.pop('package') request = kwargs.pop('request') super(UpdatePackageForm, self).__init__(*args, **kwargs) self.set_initial(request, package) class ModifyPackageForm(PackageParamsMixin, horizon_forms.SelfHandlingForm): def __init__(self, request, *args, **kwargs): super(ModifyPackageForm, self).__init__(request, *args, **kwargs) package = kwargs['initial']['package'] self.set_initial(request, package) if package.type == 'Application': self.fields['categories'] = forms.MultipleChoiceField( label=_('Application Category'), choices=[('', 'No categories available')], required=False) try: categories = api.muranoclient(request).categories.list() if categories: category_names = [(c.name, c.name) for c in categories] self.fields['categories'].choices = category_names if package.categories: self.fields['categories'].initial = dict( (key, True) for key in package.categories) except (exc.HTTPException, Exception): msg = _('Unable to get list of categories') LOG.exception(msg) redirect = reverse('horizon:app-catalog:packages:index') exceptions.handle(request, msg, redirect=redirect) def handle(self, request, data): app_id = self.initial.get('app_id') LOG.debug('Updating package {0} with {1}'.format(app_id, data)) try: data['tags'] = [t.strip() for t in data['tags'].split(',')] result = api.muranoclient(request).packages.update(app_id, data) messages.success(request, _('Package modified.')) return result except exc.HTTPForbidden: msg = _("You are not allowed to perform this operation") LOG.exception(msg) exceptions.handle( request, msg, redirect=reverse('horizon:app-catalog:packages:index')) except exc.HTTPConflict: msg = _('Package or Class with the same name is already made ' 'public') LOG.exception(msg) messages.error(request, msg) exceptions.handle( request, msg, redirect=reverse('horizon:app-catalog:packages:index')) except Exception as original_e: reason = '' exc_info = sys.exc_info() if hasattr(original_e, 'details'): try: error = json.loads(original_e.details).get('error') if error: reason = error.get('message') except ValueError: # Let horizon operate with original exception raise (exc_info[0], exc_info[1], exc_info[2]) msg = _('Failed to modify the package. {0}').format(reason) LOG.exception(msg) redirect = reverse('horizon:app-catalog:packages:index') exceptions.handle(request, msg, redirect=redirect) class SelectCategories(forms.Form): categories = forms.MultipleChoiceField( label=_('Application Category'), choices=[('', _('No categories available'))], required=False) def __init__(self, *args, **kwargs): request = kwargs.pop('request') super(SelectCategories, self).__init__(*args, **kwargs) try: categories = api.muranoclient(request).categories.list() if categories: category_names = [(c.name, c.name) for c in categories] self.fields['categories'].choices = category_names except (exc.HTTPException, Exception): msg = _('Unable to get list of categories') LOG.exception(msg) redirect = reverse('horizon:app-catalog:packages:index') exceptions.handle(request, msg, redirect=redirect) murano-dashboard-5.0.0/muranodashboard/packages/tables.py0000666000175100017510000002061613245511125023547 0ustar zuulzuul00000000000000# 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.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 messages from horizon import tables from horizon.utils import filters from openstack_dashboard import policy from oslo_log import log as logging from muranoclient.common import exceptions as exc from muranodashboard import api from muranodashboard.common import utils as md_utils LOG = logging.getLogger(__name__) class ImportBundle(tables.LinkAction): name = 'import_bundle' verbose_name = _('Import Bundle') url = 'horizon:app-catalog:packages:import_bundle' classes = ('ajax-modal',) icon = "plus" policy_rules = (("murano", "upload_package"),) class ImportPackage(tables.LinkAction): name = 'upload_package' verbose_name = _('Import Package') url = 'horizon:app-catalog:packages:upload' classes = ('ajax-modal',) icon = "plus" policy_rules = (("murano", "upload_package"),) def allowed(self, request, package): _allowed = False with api.handled_exceptions(request): client = api.muranoclient(request) if client.categories.list(): _allowed = True return _allowed class PackagesFilterAction(tables.FilterAction): name = "filter_packages" filter_type = "server" filter_choices = (('search', _("KeyWord"), True), ('type', _("Type"), True), ('name', _("Name"), True)) class DownloadPackage(tables.LinkAction): name = 'download_package' verbose_name = _('Download Package') policy_rules = (("murano", "download_package"),) url = 'horizon:app-catalog:packages:download' def allowed(self, request, package): return True def get_link_url(self, app): app_name = defaultfilters.slugify(app.name) return reverse(self.url, args=(app_name, app.id)) class ToggleEnabled(tables.BatchAction): name = 'toggle_enabled' verbose_name = _("Toggle Enabled") icon = "toggle-on" policy_rules = (("murano", "modify_package"),) @staticmethod def action_present(count): return ungettext_lazy( u"Toggle Active", u"Toggle Active", count ) @staticmethod def action_past(count): return ungettext_lazy( u"Toggled Active", u"Toggled Active", count ) def action(self, request, obj_id): try: api.muranoclient(request).packages.toggle_active(obj_id) LOG.debug('Toggle Active for package {0}.'.format(obj_id)) except exc.HTTPForbidden: msg = _("You are not allowed to perform this operation") LOG.exception(msg) messages.error(request, msg) exceptions.handle( request, msg, redirect=reverse('horizon:app-catalog:packages:index')) class TogglePublicEnabled(tables.BatchAction): name = 'toggle_public_enabled' verbose_name = _("Toggle Public") icon = "share-alt" policy_rules = (("murano", "publicize_package"),) @staticmethod def action_present(count): return ungettext_lazy( u"Toggle Public", u"Toggle Public", count ) @staticmethod def action_past(count): return ungettext_lazy( u"Toggled Public", u"Toggled Public", count ) def action(self, request, obj_id): try: api.muranoclient(request).packages.toggle_public(obj_id) LOG.debug('Toggle Public for package {0}.'.format(obj_id)) except exc.HTTPForbidden: msg = _("You are not allowed to perform this operation") LOG.exception(msg) messages.error(request, msg) exceptions.handle( request, msg, redirect=reverse('horizon:app-catalog:packages:index')) except exc.HTTPConflict: msg = _('Package or Class with the same name is already made ' 'public') LOG.exception(msg) messages.error(request, msg) exceptions.handle( request, msg, redirect=reverse('horizon:app-catalog:packages:index')) class DeletePackage(policy.PolicyTargetMixin, tables.DeleteAction): name = 'delete_package' policy_rules = (("murano", "delete_package"),) @staticmethod def action_present(count): return ungettext_lazy( u"Delete Package", u"Delete Packages", count ) @staticmethod def action_past(count): return ungettext_lazy( u"Deleted Package", u"Deleted Packages", count ) def delete(self, request, obj_id): try: api.muranoclient(request).packages.delete(obj_id) except exc.HTTPNotFound: msg = _("Package with id {0} is not found").format(obj_id) LOG.exception(msg) exceptions.handle( self.request, msg, redirect=reverse('horizon:app-catalog:packages:index')) except exc.HTTPForbidden: msg = _("You are not allowed to delete this package") LOG.exception(msg) exceptions.handle( request, msg, redirect=reverse('horizon:app-catalog:packages:index')) except Exception: LOG.exception(_('Unable to delete package in murano-api server')) url = reverse('horizon:app-catalog:packages:index') exceptions.handle(request, _('Unable to remove package.'), redirect=url) class ModifyPackage(tables.LinkAction): name = 'modify_package' verbose_name = _('Modify Package') url = 'horizon:app-catalog:packages:modify' classes = ('ajax-modal',) icon = "edit" policy_rules = (("murano", "modify_package"),) def allowed(self, request, package): return True class PackageDefinitionsTable(tables.DataTable): name = md_utils.Column( 'name', link="horizon:app-catalog:packages:detail", verbose_name=_('Package Name')) tenant_name = tables.Column('tenant_name', verbose_name=_('Tenant Name')) enabled = tables.Column('enabled', verbose_name=_('Active')) is_public = tables.Column('is_public', verbose_name=_('Public')) type = tables.Column('type', verbose_name=_('Type')) version = tables.Column(lambda obj: getattr(obj, 'version', None), verbose_name=_('Version')) created_time = tables.Column('created', verbose_name=_('Created'), filters=(filters.parse_isotime,)) updated_time = tables.Column('updated', verbose_name=_('Updated'), filters=(filters.parse_isotime,)) def get_prev_pagination_string(self): pagination_string = super( PackageDefinitionsTable, self).get_prev_pagination_string() return pagination_string + "&sort_dir=desc" class Meta(object): name = 'packages' prev_pagination_param = 'marker' verbose_name = _('Packages') table_actions_menu = (ToggleEnabled, TogglePublicEnabled) table_actions = (PackagesFilterAction, ImportPackage, ImportBundle, DeletePackage) row_actions = (ModifyPackage, DownloadPackage, ToggleEnabled, TogglePublicEnabled, DeletePackage) murano-dashboard-5.0.0/muranodashboard/__init__.py0000666000175100017510000000000013245511125022237 0ustar zuulzuul00000000000000murano-dashboard-5.0.0/muranodashboard/locale/0000775000175100017510000000000013245511556021405 5ustar zuulzuul00000000000000murano-dashboard-5.0.0/muranodashboard/locale/ja/0000775000175100017510000000000013245511556021777 5ustar zuulzuul00000000000000murano-dashboard-5.0.0/muranodashboard/locale/ja/LC_MESSAGES/0000775000175100017510000000000013245511556023564 5ustar zuulzuul00000000000000murano-dashboard-5.0.0/muranodashboard/locale/ja/LC_MESSAGES/django.po0000666000175100017510000007454013245511125025372 0ustar zuulzuul00000000000000# Akihiro Motoki , 2016. #zanata # Andreas Jaeger , 2016. #zanata # Shu Muto , 2016. #zanata # Yoshiki Eguchi , 2016. #zanata # Yuko Katabami , 2017. #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: 2017-02-10 10:36+0000\n" "Last-Translator: Yuko Katabami \n" "Language-Team: Japanese\n" "Language: ja\n" "X-Generator: Zanata 3.9.6\n" "Plural-Forms: nplurals=1; plural=0\n" msgid "-" msgstr "-" msgid "80 characters max." msgstr "最大 80 文字。" msgid "A local zip file to upload" msgstr "アップロードã™ã‚‹ãƒ­ãƒ¼ã‚«ãƒ«ã® zip ファイル" msgid "Abandon Environment" msgid_plural "Abandon Environments" msgstr[0] "環境ã®ä¸­æ­¢" msgid "Abandoned Environment" msgid_plural "Abandoned Environments" msgstr[0] "環境を中止ã—ã¾ã—ãŸ" msgid "Active" msgstr "稼åƒä¸­" msgid "Add" msgstr "追加" msgid "Add Application" msgstr "アプリケーションã®è¿½åŠ " msgid "Add Application Category" msgstr "アプリケーションカテゴリーã®è¿½åŠ " msgid "Add Category" msgstr "カテゴリーã®è¿½åŠ " msgid "Add Component" msgstr "コンãƒãƒ¼ãƒãƒ³ãƒˆã®è¿½åŠ " msgid "Add Murano Metadata" msgstr "Murano メタデータã®è¿½åŠ " msgid "Add New" msgstr "æ–°è¦è¿½åŠ " msgid "Add new category to the application catalog." msgstr "ã‚¢ãƒ—ãƒªã‚±ãƒ¼ã‚·ãƒ§ãƒ³ã‚«ã‚¿ãƒ­ã‚°ã«æ–°ã—ã„カテゴリーを追加ã—ã¾ã™ã€‚" msgid "Add to Env" msgstr "環境ã¸ã®è¿½åŠ " msgid "Adding application to an environment failed." msgstr "アプリケーションを環境ã«è¿½åŠ ã§ãã¾ã›ã‚“ã§ã—ãŸã€‚" msgid "All" msgstr "ã™ã¹ã¦" msgid "Allows adding additional information about a package." msgstr "パッケージã«ã¤ã„ã¦ã®è¿½åŠ ã®æƒ…報を記載ã§ãã¾ã™ã€‚" msgid "" "Allows to hide a package from the catalog. (Applies to package dependencies)" msgstr "" "パッケージをカタログã«è¡¨ç¤ºã™ã‚‹ã‹ã©ã†ã‹ (パッケージã®ä¾å­˜é–¢ä¿‚ã«å½±éŸ¿ã—ã¾ã™)。" msgid "" "An environment is a collection of applications that are meant to operate " "under similar conditions." msgstr "環境ã¯é¡žä¼¼ã—ãŸæ¡ä»¶ã§å®Ÿè¡Œã•れるアプリケーションã®é›†åˆã§ã™ã€‚" msgid "An external http/https URL to load the bundle from." msgstr "ãƒãƒ³ãƒ‰ãƒ«ã‚’読ã¿è¾¼ã‚€å¤–部 http/https URL。" msgid "An external http/https URL to load the package from." msgstr "パッケージを読ã¿è¾¼ã‚€å¤–部 http/https URL。" msgid "App Catalog" msgstr "アプリカタログ" msgid "App Category:" msgstr "アプリケーションカテゴリー:" msgid "App category" msgstr "アプリケーションカテゴリー" msgid "Application Categories" msgstr "アプリケーションカテゴリー" msgid "Application Category" msgstr "アプリケーションカテゴリー" msgid "Application Details" msgstr "アプリケーション詳細" msgid "Application Package" msgstr "アプリケーションパッケージ" msgid "Application Components" msgstr "アプリケーション ã‚³ãƒ³ãƒãƒ¼ãƒãƒ³ãƒˆ" msgid "Applications" msgstr "アプリケーション" msgid "Author" msgstr "作æˆè€…" msgid "Auto" msgstr "自動" msgid "Back" msgstr "戻る" msgid "Browse" msgstr "ブラウズ" msgid "Browse Local" msgstr "ローカルã§ãƒ–ラウズ" msgid "Bundle Name" msgstr "ãƒãƒ³ãƒ‰ãƒ«å" msgid "Bundle URL" msgstr "ãƒãƒ³ãƒ‰ãƒ« URL" msgid "Bundle creation failed.Reason: Can't find Bundle name from repository." msgstr "ãƒãƒ³ãƒ‰ãƒ«ä½œæˆã«å¤±æ•—ã—ã¾ã—ãŸã€‚リãƒã‚¸ãƒˆãƒªãƒ¼ã«ãƒãƒ³ãƒ‰ãƒ«åãŒè¦‹ã¤ã‹ã‚Šã¾ã›ã‚“。" msgid "Bundle creation failed.Reason: {0}" msgstr "ãƒãƒ³ãƒ‰ãƒ«ä½œæˆã«å¤±æ•—ã—ã¾ã—ãŸã€‚ç†ç”±: {0}" msgid "Bundle successfully imported." msgstr "ãƒãƒ³ãƒ‰ãƒ«ãŒæ­£å¸¸ã«ã‚¤ãƒ³ãƒãƒ¼ãƒˆã•れã¾ã—ãŸã€‚" msgid "Bundle's full name." msgstr "ãƒãƒ³ãƒ‰ãƒ«ã®å®Œå…¨ãªåå‰" msgid "Cancel" msgstr "å–り消ã—" msgid "Categories" msgstr "カテゴリー" msgid "Category Name" msgstr "カテゴリーå" msgid "Category {0} created." msgstr "カテゴリー {0} ãŒä½œæˆã•れã¾ã—ãŸã€‚" msgid "Check Keystone configuration of murano-api server." msgstr "murano-api サーãƒãƒ¼ã® Keystone 設定を確èªã—ã¾ã™ã€‚" msgid "Choose a Zip archive to upload into the catalog." msgstr "カタログã«ã‚¢ãƒƒãƒ—ロードã™ã‚‹ Zip ã‚¢ãƒ¼ã‚«ã‚¤ãƒ–ã‚’é¸æŠžã—ã¦ãã ã•ã„。" msgid "Choose a name for the environment" msgstr "環境ã®åå‰ã‚’指定ã—ã¾ã™" msgctxt "Package requirements" msgid "Classes definition folder" msgstr "クラス定義フォルダー" msgid "Click to create new environment" msgstr "クリックã—ã¦ã€æ–°ã—ã„環境を作æˆã—ã¾ã™ã€‚" msgid "Completed with warnings" msgstr "警告付ã完了" msgid "Component" msgstr "コンãƒãƒ¼ãƒãƒ³ãƒˆ" msgid "Component Details" msgstr "コンãƒãƒ¼ãƒãƒ³ãƒˆè©³ç´°" msgid "Component List" msgstr "コンãƒãƒ¼ãƒãƒ³ãƒˆä¸€è¦§" msgid "Component Logs" msgstr "コンãƒãƒ¼ãƒãƒ³ãƒˆãƒ­ã‚°" msgid "Components" msgstr "コンãƒãƒ¼ãƒãƒ³ãƒˆ" msgid "Configuration" msgstr "設定" msgid "Configure Application" msgstr "アプリケーションã®è¨­å®š" msgid "Confirm password" msgstr "パスワードã®ç¢ºèª" msgid "Could not retrieve latest status for the {0} environment" msgstr "{0} ç’°å¢ƒã®æœ€æ–°æƒ…報をå–å¾—ã§ãã¾ã›ã‚“ã§ã—ãŸã€‚" msgid "" "Couldn't find any apps, required for this field.\n" "Tried: {fqns}" msgstr "" "ã“ã®ãƒ•ィールドã«å¿…è¦ãªã‚¢ãƒ—リを見ã¤ã‘られã¾ã›ã‚“ã§ã—ãŸã€‚\n" "次を試ã—ã¾ã—ãŸ: {fqns}" msgid "Couldn't update environment. Reason: This name is already taken." msgstr "環境を更新ã§ãã¾ã›ã‚“ã§ã—ãŸã€‚ç†ç”±: ã“ã®åå‰ã¯ã™ã§ã«ä½¿ç”¨ã•れã¦ã„ã¾ã™ã€‚" msgid "Couldn't update package {0} parameters. Error: {1}" msgstr "パッケージ {0} ã®ãƒ‘ラメーターを更新ã§ãã¾ã›ã‚“ã§ã—ãŸã€‚エラー: {1}" msgid "Create" msgstr "作æˆ" msgid "Create Env" msgstr "環境ã®ä½œæˆ" msgid "Create Environment" msgstr "環境ã®ä½œæˆ" msgid "Create New" msgstr "æ–°è¦ä½œæˆ" msgid "Create a title for an image." msgstr "イメージã®ã‚¿ã‚¤ãƒˆãƒ«ã‚’作æˆã—ã¾ã™ã€‚" msgid "Created" msgstr "ä½œæˆæ™‚刻" msgid "Custom Type" msgstr "カスタムタイプ" msgid "" "Defines whether or not a package can be used by other tenants. (Applies to " "package dependencies)" msgstr "" "パッケージを他ã®ãƒ—ロジェクトãŒä½¿ç”¨ã§ãã‚‹ã‹ã‚’定義ã—ã¾ã™ (パッケージã®ä¾å­˜é–¢ä¿‚" "ã«å½±éŸ¿ã—ã¾ã™)。" msgid "Delete Category" msgid_plural "Delete Categories" msgstr[0] "カテゴリーã®å‰Šé™¤" msgid "Delete Component" msgid_plural "Delete Components" msgstr[0] "コンãƒãƒ¼ãƒãƒ³ãƒˆã®å‰Šé™¤" msgid "Delete Environment" msgid_plural "Delete Environments" msgstr[0] "環境ã®å‰Šé™¤" msgid "Delete Metadata" msgid_plural "Delete Metadata" msgstr[0] "メタデータã®å‰Šé™¤" msgid "Delete Package" msgid_plural "Delete Packages" msgstr[0] "パッケージã®å‰Šé™¤" msgid "Delete failure" msgstr "削除失敗" msgid "Deleted Category" msgid_plural "Deleted Categories" msgstr[0] "カテゴリーを削除ã—ã¾ã—ãŸ" msgid "Deleted Metadata" msgid_plural "Deleted Metadata" msgstr[0] "メタデータを削除ã—ã¾ã—ãŸ" msgid "Deleted Package" msgid_plural "Deleted Packages" msgstr[0] "パッケージを削除ã—ã¾ã—ãŸ" msgid "Deleting" msgstr "削除中" msgid "Deploy Environment" msgid_plural "Deploy Environments" msgstr[0] "環境ã®ãƒ‡ãƒ—ロイ" msgid "Deploy This Environment" msgstr "ã“ã®ç’°å¢ƒã®ãƒ‡ãƒ—ロイ" msgid "Deploy failure" msgstr "デプロイ失敗" msgid "Deploy started" msgstr "デプロイを開始ã—ã¾ã—ãŸ" msgid "Deployed Components" msgstr "デプロイ済ã¿ã‚³ãƒ³ãƒãƒ¼ãƒãƒ³ãƒˆ" msgid "Deploying" msgstr "デプロイ中" msgid "Deployment Details" msgstr "デプロイã®è©³ç´°" msgid "Deployment History" msgstr "デプロイ履歴" msgid "Deployment Logs" msgstr "デプロイログ" #, python-format msgid "Deployment with id %s doesn't exist anymore" msgstr "ID %s ã®ãƒ‡ãƒ—ロイã¯ã™ã§ã«å­˜åœ¨ã—ã¾ã›ã‚“" msgid "Deployments" msgstr "デプロイ" msgid "Description" msgstr "説明" msgid "Details" msgstr "詳細" msgid "Download Package" msgstr "ダウンロードパッケージ" msgid "Drop Components here" msgstr "コンãƒãƒ¼ãƒãƒ³ãƒˆã‚’ã“ã“ã«ãƒ‰ãƒ­ãƒƒãƒ—ã—ã¦ãã ã•ã„。" msgid "Enabled" msgstr "有効" msgid "" "Enter a complex password with at least one letter, one number and one " "special character" msgstr "" "å°‘ãªãã¨ã‚‚ 1 文字ã€1 ã¤ã®æ•°å­—ã€1 ã¤ã®ç‰¹æ®Šæ–‡å­—ã‚’æŒã¤è¤‡é›‘ãªãƒ‘スワードを入力ã—ã¦" "ãã ã•ã„。" msgid "Enter a password" msgstr "パスワードを入力ã—ã¦ãã ã•ã„" msgid "Environment" msgstr "環境" msgid "Environment Default Network" msgstr "環境ã®ãƒ‡ãƒ•ォルトãƒãƒƒãƒˆãƒ¯ãƒ¼ã‚¯" msgid "Environment Name" msgstr "環境å" msgid "Environment name must contain at least one non-white space symbol." msgstr "環境ã®åå‰ã¯ã€ç©ºç™½ä»¥å¤–ã®è¨˜å·ã‚’å°‘ãªãã¨ã‚‚ 1 ã¤å«ã‚€å¿…è¦ãŒã‚りã¾ã™ã€‚" #, python-format msgid "Environment with id %s doesn't exist anymore" msgstr "ID %s ã®ç’°å¢ƒã¯ã™ã§ã«å­˜åœ¨ã—ã¾ã›ã‚“" msgid "Environment with specified name already exists" msgstr "指定ã•れãŸåå‰ã‚’æŒã¤ç’°å¢ƒãŒæ—¢ã«å­˜åœ¨ã—ã¾ã™ã€‚" msgid "Environments" msgstr "環境" msgid "Error {0} occurred while installing images for {1}" msgstr "{1} 用ã®ã‚¤ãƒ¡ãƒ¼ã‚¸ã®æº–備中ã«ã‚¨ãƒ©ãƒ¼ {0} ãŒç™ºç”Ÿã—ã¾ã—ãŸ" msgid "Error {0} occurred while installing package {1}" msgstr "パッケージ {1} ã®ã‚¤ãƒ³ã‚¹ãƒˆãƒ¼ãƒ«ä¸­ã«ã‚¨ãƒ©ãƒ¼ {0} ãŒç™ºç”Ÿã—ã¾ã—ãŸã€‚" msgid "Error {0} occurred while parsing package {1}" msgstr "パッケージ {1} ã®è§£æžä¸­ã«ã‚¨ãƒ©ãƒ¼ {0} ãŒç™ºç”Ÿã—ã¾ã—ãŸã€‚" msgid "Error {0} occurred while setting image {1}, {2} public" msgstr "イメージ {1}, {2} をパブリックã«è¨­å®šã™ã‚‹éš›ã«ã‚¨ãƒ©ãƒ¼ {0} ãŒç™ºç”Ÿã—ã¾ã—ãŸ" msgctxt "Package requirements" msgid "Execution plans folder" msgstr "実行プランフォルダー" msgid "FQN" msgstr "FQN" msgid "Failed" msgstr "失敗" msgid "Failed to create environment" msgstr "環境ã®ä½œæˆã«å¤±æ•—ã—ã¾ã—ãŸ" msgid "Failed to modify the package. {0}" msgstr "パッケージã®å¤‰æ›´ã«å¤±æ•—ã—ã¾ã—ãŸã€‚ {0}" msgid "File" msgstr "ファイル" msgid "Filter" msgstr "フィルター" msgid "Find in a selected category" msgstr "é¸æŠžã—ãŸã‚«ãƒ†ã‚´ãƒªãƒ¼ã«ãŠã‘る検索" 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 "" "最åˆã®ã‚·ãƒ³ãƒœãƒ«ã¯ãƒ©ãƒ†ãƒ³æ–‡å­—ã‹ã‚¢ãƒ³ãƒ€ãƒ¼ã‚¹ã‚³ã‚¢ã§ãªã‘れã°ã„ã‘ã¾ã›ã‚“。ãれ以é™ã®ã‚·" "ンボルã«ã¯ã€ãƒ©ãƒ†ãƒ³æ–‡å­—ã€æ•°å­—ã€ã‚¢ãƒ³ãƒ€ãƒ¼ã‚¹ã‚³ã‚¢ã€@ 記å·ã€# 記å·ã€ãƒ‰ãƒ«è¨˜å·ãŒä½¿ç”¨" "ã§ãã¾ã™ã€‚" msgid "Fully qualified package name." msgstr "完全ãªå½¢ã®ãƒ‘ッケージå。" #, python-format msgid "" "Go to Packages , click 'Import Package' " "and select Repository as Package Source." msgstr "" " パッケージ ã«é€²ã‚“ã§ã€ã€Œãƒ‘ッケージã®ã‚¤ãƒ³" "ãƒãƒ¼ãƒˆã€ã‚’クリックã—ã¦ã€ãƒ‘ッケージã®ã‚½ãƒ¼ã‚¹ã¨ã—ã¦ãƒªãƒã‚¸ãƒˆãƒªãƒ¼ã‚’" "é¸æŠžã—ã¦ãã ã•ã„。 " msgid "HTTP/HTTPS URL of the bundle file." msgstr "ãƒãƒ³ãƒ‰ãƒ«ãƒ•ァイル㮠HTTP/HTTPS URL。" msgid "HTTP/HTTPS URL of the package file." msgstr "パッケージファイル㮠HTTP/HTTPS URL。" msgid "Heat Orchestration stack name" msgstr "Heat Orchestration スタックå" msgid "Heat Orchestration stack%(forloop.counter)s name" msgstr "Heat Orchestration スタック %(forloop.counter)s ã®åå‰" 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 "" "パッケージãŒä»–ã®ãƒ‘ッケージや特定㮠glance イメージã«ä¾å­˜ã—ã¦ã„ã‚‹å ´åˆã€ãれら" "ã‚‚ 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 "" "ã“ã®ãƒ‘ッケージãŒä»–ã®ãƒ‘ッケージや特定㮠glance イメージã«ä¾å­˜ã—ã¦ã„ã‚‹å ´åˆã€ã" "れらも murano リãƒã‚¸ãƒˆãƒªãƒ¼ã‹ã‚‰ä¸€ç·’ã«ã‚¤ãƒ³ã‚¹ãƒˆãƒ¼ãƒ«ã•れã¾ã™ã€‚" msgid "Image" msgstr "イメージ" msgid "Image Title" msgstr "イメージタイトル" msgid "Image Type" msgstr "イメージ種別" msgid "Image successfully marked" msgstr "ã‚¤ãƒ¡ãƒ¼ã‚¸ãŒæ­£å¸¸ã«ãƒžãƒ¼ã‚¯ã•れã¾ã—ãŸ" msgid "Images" msgstr "イメージ" msgid "Import Bundle" msgstr "ãƒãƒ³ãƒ‰ãƒ«ã®ã‚¤ãƒ³ãƒãƒ¼ãƒˆ" msgid "Import Package" msgstr "インãƒãƒ¼ãƒˆãƒ‘ッケージ" msgid "Importing package {0} failed. Reason: {1}" msgstr "パッケージ {0} ã®ã‚¤ãƒ³ãƒãƒ¼ãƒˆã«å¤±æ•—ã—ã¾ã—ãŸã€‚ç†ç”±: {1}" msgid "Info" msgstr "情報" msgid "Instance name" msgstr "インスタンスå" msgid "Instance%(forloop.counter)s name" msgstr "インスタンス %(forloop.counter)s ã®åå‰" msgid "Invalid metadata for image: {0}" msgstr "無効ãªã‚¤ãƒ¡ãƒ¼ã‚¸ã®ãƒ¡ã‚¿ãƒ‡ãƒ¼ã‚¿: {0}" msgid "Invalid murano image metadata" msgstr "無効㪠murano イメージメタデータ" msgid "Invalid value of 'murano_nets' option" msgstr "'murano_nets' オプションã®ç„¡åйãªå€¤" msgid "It is forbidden to upload files larger than {0} MB." msgstr "{0} MB より大ãã„ファイルã®ã‚¢ãƒƒãƒ—ロードã¯è¨±å¯ã•れã¦ã„ã¾ã›ã‚“。" msgid "KeyWord" msgstr "キーワード" msgid "Last operation" msgstr "最終æ“作" msgid "Latest Deployment Log" msgstr "最新ã®ãƒ‡ãƒ—ロイログ" msgid "License" msgstr "ライセンス" msgid "Logs" msgstr "ログ" msgid "Manage" msgstr "管ç†" msgid "Manage Components" msgstr "コンãƒãƒ¼ãƒãƒ³ãƒˆã®ç®¡ç†" msgctxt "Package requirements" msgid "Manifest file" msgstr "マニフェストファイル" msgid "Mark Image" msgstr "イメージã®ãƒžãƒ¼ã‚¯" msgid "" "Mark an image with Murano specific metadata to be added to the selected " "image." msgstr "" "イメージを Murano 固有ã®ãƒ¡ã‚¿ãƒ‡ãƒ¼ã‚¿ã§ãƒžãƒ¼ã‚¯ã—ã¦ã€é¸æŠžã—ãŸã‚¤ãƒ¡ãƒ¼ã‚¸ã«è¿½åŠ ã•れã¾" "ã™ã€‚" msgid "Marked Images" msgstr "マーク済ã¿ã‚¤ãƒ¡ãƒ¼ã‚¸" msgid "Modify Package" msgstr "パッケージã®å¤‰æ›´" msgid "Modifying package failed" msgstr "パッケージã®å¤‰æ›´ã«å¤±æ•—ã—ã¾ã—ãŸã€‚" msgid "NO ENVIRONMENTS" msgstr "環境ãªã—" msgid "Name" msgstr "åå‰" msgid "Name of the bundle." msgstr "ãƒãƒ³ãƒ‰ãƒ«ã®åå‰ã€‚" #, python-format msgid "Network of '%s'" msgstr "'%s' ã®ãƒãƒƒãƒˆãƒ¯ãƒ¼ã‚¯" msgid "Next" msgstr "次ã¸" msgid "Next Page" msgstr "次ã®ãƒšãƒ¼ã‚¸" msgid "No availability zones available" msgstr "利用å¯èƒ½ãªã‚¢ãƒ™ã‚¤ãƒ©ãƒ“リティーゾーンãŒã‚りã¾ã›ã‚“" msgid "No categories available" msgstr "利用å¯èƒ½ãªã‚«ãƒ†ã‚´ãƒªãƒ¼ãŒã‚りã¾ã›ã‚“" msgid "No components" msgstr "コンãƒãƒ¼ãƒãƒ³ãƒˆãªã—" msgid "No images available" msgstr "利用å¯èƒ½ãªã‚¤ãƒ¡ãƒ¼ã‚¸ãŒã‚りã¾ã›ã‚“。" msgid "No keypair" msgstr "キーペアãªã—" msgid "No license" msgstr "ライセンスãªã—" msgid "No recent activity to report at this time." msgstr "ç¾åœ¨ã€è¡¨ç¤ºã™ã‚‹æœ€è¿‘ã®ä½¿ç”¨çжæ³ãŒã‚りã¾ã›ã‚“。" msgid "No requirements" msgstr "è¦ä»¶ãªã—" msgid "None" msgstr "ãªã—" msgid "Not in domain" msgstr "ドメインã«ã‚りã¾ã›ã‚“" msgid "Note" msgstr "注æ„" msgid "" "OpenStack Networking (Neutron) is not available in current environment. " "Custom Network Settings cannot be applied" msgstr "" "OpenStack Networking (Neutron) ãŒç¾åœ¨ã®ç’°å¢ƒã§åˆ©ç”¨ã§ãã¾ã›ã‚“。カスタムãƒãƒƒãƒˆ" "ワーク設定をé©ç”¨ã§ãã¾ã›ã‚“。" msgid "Operation is forbidden by murano-api server." msgstr "æ“作㌠murano-api サーãƒãƒ¼ã«ã‚ˆã‚Šç¦æ­¢ã•れã¦ã„ã¾ã™ã€‚" msgid "Optional" msgstr "オプション" msgid "Overview" msgstr "概è¦" msgid "Package Bundle Source" msgstr "パッケージãƒãƒ³ãƒ‰ãƒ«ã‚½ãƒ¼ã‚¹" msgid "Package Count" msgstr "パッケージ数" msgid "Package Details" msgstr "パッケージ詳細" msgid "Package Name" msgstr "パッケージå" msgid "Package Source" msgstr "パッケージソース" msgid "Package Tags" msgstr "パッケージタグ" msgid "Package URL" msgstr "パッケージ URL" msgid "Package Version" msgstr "パッケージãƒãƒ¼ã‚¸ãƒ§ãƒ³" msgid "" "Package creation failed.Reason: Can't find Package name from repository." msgstr "" "パッケージ作æˆã«å¤±æ•—ã—ã¾ã—ãŸã€‚リãƒã‚¸ãƒˆãƒªãƒ¼ã‹ã‚‰ãƒ‘ッケージåを見ã¤ã‘られãªã„ãŸ" "ã‚。" msgid "Package creation failed.Reason: {0}" msgstr "パッケージ作æˆã«å¤±æ•—ã—ã¾ã—ãŸã€‚ç†ç”±: {0}" msgid "Package modified." msgstr "パッケージ更新時刻" msgid "Package name in the repository, usually a fully qualified name" msgstr "リãƒã‚¸ãƒˆãƒªãƒ¼ã§ã®ãƒ‘ッケージåã€é€šå¸¸ã¯å®Œå…¨ãªå½¢ã®åå‰" msgid "Package or Class with the same name is already made public" msgstr "åŒã˜åå‰ã‚’æŒã¤ãƒ‘ッケージã‹ã‚¯ãƒ©ã‚¹ãŒæ—¢ã«ãƒ‘ブリックã«ä½œæˆã•れã¦ã„ã¾ã™ã€‚" msgid "Package parameters successfully updated." msgstr "パッケージã®ãƒ‘ãƒ©ãƒ¡ãƒ¼ã‚¿ãƒ¼ãŒæ­£å¸¸ã«æ›´æ–°ã•れã¾ã—ãŸã€‚" msgid "Package version" msgstr "パッケージãƒãƒ¼ã‚¸ãƒ§ãƒ³" msgid "Package with id {0} is not found" msgstr "ID {0} ã®ãƒ‘ッケージãŒè¦‹ã¤ã‹ã‚Šã¾ã›ã‚“" msgid "Package with specified name already exists" msgstr "指定ã•れãŸåå‰ã‚’æŒã¤ãƒ‘ãƒƒã‚±ãƒ¼ã‚¸ãŒæ—¢ã«å­˜åœ¨ã—ã¾ã™ã€‚" msgid "Package {0} already registered." msgstr "パッケージ {0} ãŒæ—¢ã«ç™»éŒ²ã•れã¦ã„ã¾ã™ã€‚" msgid "Package {0} upload failed. {1}" msgstr "パッケージ {0} ã®ã‚¢ãƒƒãƒ—ロードã«å¤±æ•—ã—ã¾ã—ãŸã€‚ {1}" msgid "Package {0} uploaded" msgstr "パッケージ {0} をアップロードã—ã¾ã—ãŸ" msgid "Packages" msgstr "パッケージ" msgid "Packages should contain:" msgstr "パッケージã«ã¯ä»¥ä¸‹ãŒå«ã¾ã‚Œã¦ã„ã‚‹å¿…è¦ãŒã‚りã¾ã™ã€‚" msgid "Please confirm your password" msgstr "パスワードを確èªã—ã¦ãã ã•ã„" msgid "Please supply a bundle name" msgstr "ãƒãƒ³ãƒ‰ãƒ«ã®åå‰ã‚’指定ã—ã¦ãã ã•ã„。" msgid "Please supply a bundle url" msgstr "ãƒãƒ³ãƒ‰ãƒ«ã® URL を指定ã—ã¦ãã ã•ã„。" msgid "Please supply a package file" msgstr "パッケージファイルを指定ã—ã¦ãã ã•ã„。" msgid "Please supply a package name" msgstr "パッケージã®åå‰ã‚’指定ã—ã¦ãã ã•ã„。" msgid "Please supply a package url" msgstr "パッケージ㮠URL を指定ã—ã¦ãã ã•ã„。" msgid "Previous Page" msgstr "å‰ã®ãƒšãƒ¼ã‚¸" msgid "Provide comma-separated list of words, associated with the package" msgstr "パッケージã«é–¢é€£ä»˜ã‘るキーワードをコンマ区切りã®ãƒªã‚¹ãƒˆã§æŒ‡å®šã—ã¾ã™ã€‚" msgid "Provide desired name for a new category" msgstr "æ–°ã—ã„カテゴリーã«ã¯é©åˆ‡ãªåå‰ã‚’付ã‘ã¦ãã ã•ã„" msgid "Public" msgstr "パブリック" msgid "Quick Deploy" msgstr "クイックデプロイ" msgid "Ready" msgstr "準備完了" msgid "Ready to configure" msgstr "設定準備完了" msgid "Ready to deploy" msgstr "ãƒ‡ãƒ—ãƒ­ã‚¤ã®æº–備完了" msgid "Recent Activity" msgstr "最近ã®ä½¿ç”¨çжæ³" msgid "Repository" msgstr "リãƒã‚¸ãƒˆãƒªãƒ¼" msgid "Requested object is not found on murano server." msgstr "è¦æ±‚ã•れãŸã‚ªãƒ–ジェクト㌠murano サーãƒãƒ¼ã«è¦‹ã¤ã‹ã‚Šã¾ã›ã‚“。" msgid "Requested operation conflicts with an existing object." msgstr "è¦æ±‚ã•ã‚ŒãŸæ“ä½œãŒæ—¢å­˜ã®ã‚ªãƒ–ジェクトã¨ç«¶åˆã—ã¦ã„ã¾ã™ã€‚" msgid "Requirements" msgstr "è¦ä»¶" msgid "Retype your password" msgstr "パスワードã®å†å…¥åŠ›" msgid "Running" msgstr "実行中" msgid "Select Application" msgstr "アプリケーションã®é¸æŠž" msgid "Select Image" msgstr "ã‚¤ãƒ¡ãƒ¼ã‚¸ã‚’é¸æŠžã—ã¦ãã ã•ã„" msgid "Select an image registered in Glance Image Services." msgstr "Glance Image サービスã«ç™»éŒ²ã•れã¦ã„ã‚‹ã‚¤ãƒ¡ãƒ¼ã‚¸ã‚’é¸æŠžã—ã¦ãã ã•ã„。" msgid "Select one or more categories for a package." msgstr "パッケージã®ã‚«ãƒ†ã‚´ãƒªãƒ¼ã‚’ 1 ã¤ä»¥ä¸Šé¸æŠžã—ã¦ãã ã•ã„" msgid "Set up for identifying a package." msgstr "パッケージã®è­˜åˆ¥ã«ä½¿ç”¨ã•れã¾ã™ã€‚" msgid "Show Details" msgstr "詳細表示" msgid "Something went wrong during package downloading" msgstr "パッケージã®ãƒ€ã‚¦ãƒ³ãƒ­ãƒ¼ãƒ‰ä¸­ã«ã€ã©ã“ã‹ãŒãŠã‹ã—ããªã‚Šã¾ã—ãŸï¼" msgid "Sorry, this environment doesn't exist anymore" msgstr "ã”ã‚ã‚“ãªã•ã„ã€ã“ã®ç’°å¢ƒã¯ã™ã§ã«å­˜åœ¨ã—ã¾ã›ã‚“" msgid "" "Sorry, you can't add application right now. The environment is deploying." msgstr "" "ã™ã¿ã¾ã›ã‚“ãŒã€ã‚¢ãƒ—リケーションをã™ãã«è¿½åŠ ã§ãã¾ã›ã‚“。環境ãŒãƒ‡ãƒ—ロイ中ã§ã™ã€‚" msgid "Sorry, you can't delete service right now" msgstr "申ã—訳ã‚りã¾ã›ã‚“。サービスをã™ãã«å‰Šé™¤ã§ãã¾ã›ã‚“。" msgid "Specified title already in use. Please choose another one." msgstr "ã™ã§ã«ä½¿ç”¨ä¸­ã®ã‚¿ã‚¤ãƒˆãƒ«ã‚’指定ã—ã¾ã—ãŸã€‚別ã®ã‚‚ã®ã‚’é¸æŠžã—ã¦ãã ã•ã„。" msgid "Specifying a category helps to filter applications in the catalog" msgstr "" "カテゴリを指定ã™ã‚‹ã¨ã€ã‚«ã‚¿ãƒ­ã‚°å†…ã§ã‚¢ãƒ—リケーションをフィルタリングã™ã‚‹éš›ã«ä½¿" "用ã§ãã¾ã™ã€‚" msgid "Started Deleting Component" msgid_plural "Started Deleting Components" msgstr[0] "コンãƒãƒ¼ãƒãƒ³ãƒˆã®å‰Šé™¤ã‚’é–‹å§‹ã—ã¾ã—ãŸ" msgid "Started Deleting Environment" msgid_plural "Started Deleting Environments" msgstr[0] "環境ã®å‰Šé™¤ã‚’é–‹å§‹ã—ã¾ã—ãŸ" msgid "Started deploying Environment" msgid_plural "Started deploying Environments" msgstr[0] "環境ã®ãƒ‡ãƒ—ロイを開始ã—ã¾ã—ãŸ" msgid "Status" msgstr "ステータス" msgid "Step {0}" msgstr "手順 {0}" msgid "Successful" msgstr "æˆåŠŸ" msgid "Tags" msgstr "ã‚¿ã‚°" msgid "Tenant Name" msgstr "テナントå" msgid "The '{0}' application successfully added to environment." msgstr "アプリケーション '{0}' ãŒæ­£å¸¸ã«ç’°å¢ƒã«è¿½åŠ ã•れã¾ã—ãŸã€‚" 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 "" "ã“ã®ç’°å¢ƒã®ã‚¢ãƒ—リケーション㮠VM ã¯ã€å€‹åˆ¥ã«æŒ‡å®šã•れãªã„é™ã‚Šã€ãƒ‡ãƒ•ォルトã§ã“ã®" "ãƒãƒƒãƒˆãƒ¯ãƒ¼ã‚¯ã«æŽ¥ç¶šã•れã¾ã™ã€‚「新è¦ä½œæˆã€ã‚’é¸æŠžã™ã‚‹ã¨ã€æ–°ã—ã„ãƒãƒƒãƒˆãƒ¯ãƒ¼ã‚¯ã‚’生" "æˆã—ã€ã‚µãƒ–ãƒãƒƒãƒˆã§ IP アドレス範囲を指定ã—ã¾ã™ã€‚プロジェクトã®ãƒ‡ãƒ•ォルト㮠" "Murano 用ã®ãƒ«ãƒ¼ã‚¿ãƒ¼ã«ã‚‚ã€ã“ã® IP アドレス範囲ã‹ã‚‰ã‚¢ãƒ‰ãƒ¬ã‚¹ãŒå‰²ã‚Šå½“ã¦ã‚‰ã‚Œã¾ã™ã€‚" #, python-format msgid "" "The bundle is going to be installed from %(murano_repo_url)s repository." msgstr "" "ãƒãƒ³ãƒ‰ãƒ«ã¯ " "%(murano_repo_url)s リãƒã‚¸ãƒˆãƒªãƒ¼ã‹ã‚‰ã‚¤ãƒ³ã‚¹ãƒˆãƒ¼ãƒ«ã•れã¾ã™ã€‚" msgid "The environment name field cannot be empty." msgstr "環境åã¯ç©ºã«ã§ãã¾ã›ã‚“。" #, python-format msgid "" "The package is going to be imported from %(murano_repo_url)s repository." msgstr "" "パッケージ㌠" "%(murano_repo_url)s リãƒã‚¸ãƒˆãƒªãƒ¼ã‹ã‚‰ã‚¤ãƒ³ãƒãƒ¼ãƒˆã•れã¾ã™ã€‚" msgid "" "The password must contain at least one letter, " "one number and one special character" msgstr "" "パスワードã¯ã€å°‘ãªãã¨ã‚‚ 1 文字ã€1 ã¤ã®æ•°å­—ã€1 ã¤ã®ç‰¹æ®Šæ–‡å­—ã‚’å«ã‚ã‚‹å¿…è¦ãŒã‚り" "ã¾ã™ã€‚" msgid "The request data is not acceptable by the server" msgstr "è¦æ±‚ã•れãŸãƒ‡ãƒ¼ã‚¿ãŒã‚µãƒ¼ãƒãƒ¼ã«ã‚ˆã‚Šå—ã‘入れられã¾ã›ã‚“ã§ã—ãŸã€‚" msgid "There are no applications in the catalog. You can import apps from" msgstr "" "カタログã«ã¯ã‚¢ãƒ—リケーションãŒã‚りã¾ã›ã‚“。次㮠URL ã‹ã‚‰ã‚¢ãƒ—リケーションをイン" "ãƒãƒ¼ãƒˆã§ãã¾ã™ã€‚" msgid "There are no applications matching your criteria." msgstr "基準を満ãŸã™ã‚¢ãƒ—リケーションã¯ã‚りã¾ã›ã‚“。" msgid "There was an error communicating with server" msgstr "サーãƒã¨ã®é€šä¿¡ä¸­ã«ã‚¨ãƒ©ãƒ¼ãŒç™ºç”Ÿã—ã¾ã—ãŸã€‚" msgid "There was an error initialising this field." msgstr "ã“ã®ãƒ•ィールドã®åˆæœŸåŒ–中ã«ã‚¨ãƒ©ãƒ¼ãŒç™ºç”Ÿã—ã¾ã—ãŸã€‚" msgid "" "This action cannot be undone. Any resources created by this environment will " "have to be released manually." msgstr "" "ã“ã®æ“作ã¯å–り消ã›ã¾ã›ã‚“。ã“ã®ç’°å¢ƒã«ã‚ˆã‚Šä½œæˆã•れãŸãƒªã‚½ãƒ¼ã‚¹ã¯ã€æ‰‹å‹•ã§é–‹æ”¾ã™ã‚‹" "å¿…è¦ãŒã‚りã¾ã™ã€‚" msgid "Time Finished" msgstr "終了時刻" msgid "Time Started" msgstr "開始時刻" msgid "Time updated" msgstr "更新時刻" msgid "Title" msgstr "タイトル" msgid "Toggle Active" msgid_plural "Toggle Active" msgstr[0] "アクティブã®åˆ‡ã‚Šæ›¿ãˆ" msgid "Toggle Enabled" msgstr "有効化ã®åˆ‡ã‚Šæ›¿ãˆ" msgid "Toggle Public" msgid_plural "Toggle Public" msgstr[0] "パブリックã®åˆ‡ã‚Šæ›¿ãˆ" msgid "Toggled Active" msgid_plural "Toggled Active" msgstr[0] "アクティブã®åˆ‡ã‚Šæ›¿ãˆæ¸ˆã¿" msgid "Toggled Public" msgid_plural "Toggled Public" msgstr[0] "パブリックã®åˆ‡ã‚Šæ›¿ãˆæ¸ˆã¿" msgid "Topology" msgstr "トãƒãƒ­ã‚¸ãƒ¼" msgid "" "Trying to add {0} image to glance. Image will be ready for deployment after " "successful upload" msgstr "" "glance ã«ã‚¤ãƒ¡ãƒ¼ã‚¸ {0} を追加ã—ã¦ã„ã¾ã™ã€‚ã‚¢ãƒƒãƒ—ãƒ­ãƒ¼ãƒ‰ã«æˆåŠŸã™ã‚‹ã¨ã€ã‚¤ãƒ¡ãƒ¼ã‚¸ã‚’" "デプロイã«ä½¿ç”¨ã§ãるよã†ã«ãªã‚Šã¾ã™ã€‚" msgid "" "Trying to add {0}, {1} image to glance. Image will be ready for deployment " "after successful upload" msgstr "" "glance ã«ã‚¤ãƒ¡ãƒ¼ã‚¸ {0}, {1} を追加ã—ã¦ã„ã¾ã™ã€‚ã‚¢ãƒƒãƒ—ãƒ­ãƒ¼ãƒ‰ã«æˆåŠŸã™ã‚‹ã¨ã€ã‚¤ãƒ¡ãƒ¼" "ジをデプロイã«ä½¿ç”¨ã§ãるよã†ã«ãªã‚Šã¾ã™ã€‚" msgid "Type" msgstr "種別" msgctxt "Package requirements" msgid "UI definition folder" msgstr "UI 定義フォルダー" msgid "UNKNOWN" msgstr "䏿˜Ž" msgid "URL" msgstr "URL" msgid "Unable to abandon an environment {0} due to: {1}" msgstr "環境 {0} を中断ã§ãã¾ã›ã‚“ã§ã—ãŸã€‚ç†ç”±: {1}" msgid "Unable to communicate to glare-api server." msgstr "glare-api サーãƒãƒ¼ã¨é€šä¿¡ã§ãã¾ã›ã‚“。" msgid "Unable to communicate to murano-api server." msgstr "murano-api サーãƒãƒ¼ã¨é€šä¿¡ã§ãã¾ã›ã‚“。" msgid "Unable to create environment {0} due to: {1}" msgstr "環境 {0} を作æˆã§ãã¾ã›ã‚“ã§ã—ãŸã€‚ç†ç”±: {1}" msgid "Unable to delete category" msgstr "カテゴリーを削除ã§ãã¾ã›ã‚“。" msgid "Unable to delete environment {0} due to: {1}" msgstr "環境 {0} を削除ã§ãã¾ã›ã‚“。ç†ç”±: {1}" msgid "Unable to delete package in murano-api server" msgstr "murano-api サーãƒãƒ¼ã«ãŠã„ã¦ãƒ‘ッケージを削除ã§ãã¾ã›ã‚“。" msgid "Unable to deploy. Try again later" msgstr "デプロイã§ãã¾ã›ã‚“。後ã‹ã‚‰ã‚‚ã†ä¸€åº¦ãŠè©¦ã—ãã ã•ã„。" msgid "Unable to download package." msgstr "パッケージをダウンロードã§ãã¾ã›ã‚“。" msgid "Unable to get list of categories" msgstr "カテゴリーã®ä¸€è¦§ã‚’å–å¾—ã§ãã¾ã›ã‚“" msgid "Unable to mark image" msgstr "イメージをマークã§ãã¾ã›ã‚“" msgid "Unable to modify package" msgstr "パッケージを変更ã§ãã¾ã›ã‚“。" msgid "Unable to remove metadata" msgstr "メタデータを削除ã§ãã¾ã›ã‚“" msgid "Unable to remove package." msgstr "パッケージを削除ã§ãã¾ã›ã‚“" msgid "Unable to retrieve availability zones." msgstr "アベイラビリティーゾーンをå–å¾—ã§ãã¾ã›ã‚“。" msgid "Unable to retrieve details for service" msgstr "サービスã®è©³ç´°ã‚’å–å¾—ã§ãã¾ã›ã‚“。" msgid "Unable to retrieve list of deployments" msgstr "デプロイã®ä¸€è¦§ã‚’å–å¾—ã§ãã¾ã›ã‚“。" msgid "Unable to retrieve list of images" msgstr "イメージã®ä¸€è¦§ã‚’å–å¾—ã§ãã¾ã›ã‚“。" msgid "" "Unable to retrieve list of services. This environment is deploying or " "already deployed by other user." msgstr "" "サービスã®ä¸€è¦§ã‚’å–å¾—ã§ãã¾ã›ã‚“。ã“ã®ç’°å¢ƒã¯ã€ãƒ‡ãƒ—ロイ中ã‹ã€ä»–ã®ãƒ¦ãƒ¼ã‚¶ãƒ¼ã«ã‚ˆã‚Š" "デプロイã•れã¦ã„ã¾ã™ã€‚" msgid "Unable to retrieve package details." msgstr "パッケージã®è©³ç´°ã‚’å–å¾—ã§ãã¾ã›ã‚“。" msgid "Unable to retrieve project list." msgstr "プロジェクト一覧をå–å¾—ã§ãã¾ã›ã‚“。" msgid "Unable to retrieve public images." msgstr "公開イメージã®ä¸€è¦§ã‚’å–å¾—ã§ãã¾ã›ã‚“。" msgid "Unavailable" msgstr "利用ä¸å¯" msgid "Unknown" msgstr "䏿˜Ž" msgid "Update" msgstr "æ›´æ–°" msgid "Update Image" msgstr "ã‚¤ãƒ¡ãƒ¼ã‚¸ã®æ›´æ–°" msgid "Update Metadata" msgstr "ãƒ¡ã‚¿ãƒ‡ãƒ¼ã‚¿ã®æ›´æ–°" msgid "Update This Environment" msgstr "ã“ã®ç’°å¢ƒã®æ›´æ–°" msgid "Updated" msgstr "更新時刻" msgid "Uploading package failed. {0}" msgstr "パッケージã®ã‚¢ãƒƒãƒ—ロードã«å¤±æ•—ã—ã¾ã—ãŸã€‚ {0}" msgid "Used for identifying and filtering packages." msgstr "パッケージã®è­˜åˆ¥ã‚„フィルタリングã«ä½¿ç”¨ã•れã¾ã™ã€‚" msgid "Validation Error occurred" msgstr "検証エラーãŒç™ºç”Ÿã—ã¾ã—ãŸ" msgid "Version" msgstr "ãƒãƒ¼ã‚¸ãƒ§ãƒ³" msgid "Version of the package (optional)." msgstr "パッケージã®ãƒãƒ¼ã‚¸ãƒ§ãƒ³ (オプション)。" msgid "You are not allowed to change this properties of the package" msgstr "パッケージã®ã“ã®ãƒ—ロパティーã®å¤‰æ›´ã¯è¨±å¯ã•れã¦ã„ã¾ã›ã‚“。" msgid "You are not allowed to delete this package" msgstr "ã“ã®ãƒ‘ッケージã®å‰Šé™¤ã¯è¨±å¯ã•れã¦ã„ã¾ã›ã‚“。" msgid "You are not allowed to perform this operation" msgstr "ã“ã®æ“作ã¯è¨±å¯ã•れã¦ã„ã¾ã›ã‚“。" msgid "" "You'll have to configure each package installed from this bundle separately." msgstr "" "ã“ã®ãƒãƒ³ãƒ‰ãƒ«ã‹ã‚‰ã‚¤ãƒ³ã‚¹ãƒˆãƒ¼ãƒ«ã•れるå„パッケージã¯ãれãžã‚Œè¨­å®šã™ã‚‹å¿…è¦ãŒã‚りã¾" "ã™ã€‚" msgid "{0}{1} don't match" msgstr "{0}{1} ãŒä¸€è‡´ã—ã¾ã›ã‚“" murano-dashboard-5.0.0/muranodashboard/locale/ja/LC_MESSAGES/djangojs.po0000666000175100017510000000464413245511125025725 0ustar zuulzuul00000000000000# Andreas Jaeger , 2016. #zanata # Yusuke Higashino , 2016. #zanata # Yuko Katabami , 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 \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 "ã‚ãªãŸã®ãƒ‘スワードã«ã¯ä»¥ä¸‹ã®å†…å®¹ãŒæœ€ä½Žå¿…è¦ã§ã™ã€‚" murano-dashboard-5.0.0/muranodashboard/locale/zh_CN/0000775000175100017510000000000013245511556022406 5ustar zuulzuul00000000000000murano-dashboard-5.0.0/muranodashboard/locale/zh_CN/LC_MESSAGES/0000775000175100017510000000000013245511556024173 5ustar zuulzuul00000000000000murano-dashboard-5.0.0/muranodashboard/locale/zh_CN/LC_MESSAGES/django.po0000666000175100017510000007014713245511125026000 0ustar zuulzuul00000000000000# Andreas Jaeger , 2016. #zanata # Eric Lei <1165970798@qq.com>, 2016. #zanata # Wu Han , 2016. #zanata # sunanchen , 2016. #zanata # Bin , 2017. #zanata # Gaoxiao Zhu , 2017. #zanata # Wu Han , 2017. #zanata # liujunpeng , 2017. #zanata msgid "" msgstr "" "Project-Id-Version: murano-dashboard 4.0.0.0rc2.dev14\n" "Report-Msgid-Bugs-To: https://bugs.launchpad.net/openstack-i18n/\n" "POT-Creation-Date: 2017-09-04 12:56+0000\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "PO-Revision-Date: 2017-08-17 01:52+0000\n" "Last-Translator: Bin \n" "Language-Team: Chinese (China)\n" "Language: zh-CN\n" "X-Generator: Zanata 3.9.6\n" "Plural-Forms: nplurals=1; plural=0\n" #, python-format msgid "%s: random subnet" msgstr "%s: éšæœºå­ç½‘" msgid "-" msgstr "-" msgid "80 characters max." msgstr "最长80个字符" msgid "A local zip file to upload" msgstr "从本地上传的一个 ZIP 压缩包" msgid "Abandon Environment" msgid_plural "Abandon Environments" msgstr[0] "放弃环境å˜é‡" msgid "Abandoned Environment" msgid_plural "Abandoned Environments" msgstr[0] "已放弃环境å˜é‡" msgid "Active" msgstr "è¿è¡Œä¸­" msgid "Add" msgstr "添加" msgid "Add Application" msgstr "添加应用程åº" msgid "Add Application Category" msgstr "添加应用程åºç›®å½•" msgid "Add Category" msgstr "添加目录" msgid "Add Component" msgstr "增加组件" msgid "Add Murano Metadata" msgstr "增加 Murano 元数æ®" msgid "Add New" msgstr "增加新的" msgid "Add new category to the application catalog." msgstr "在应用程åºç›®å½•中加入一个新分类。" msgid "Add to Env" msgstr "添加到 Env 中" msgid "Adding application to an environment failed." msgstr "å‘环境中添加应用程åºå¤±è´¥ã€‚" msgid "All" msgstr "全部" msgid "Allows adding additional information about a package." msgstr "å…许给软件包添加é¢å¤–的信æ¯ã€‚" msgid "" "Allows to hide a package from the catalog. (Applies to package dependencies)" msgstr "å…许把软件包从目录中éšè—ã€‚ï¼ˆåŒæ—¶åº”用到软件包的ä¾èµ–)" msgid "" "An environment is a collection of applications that are meant to operate " "under similar conditions." msgstr "环境是è¿è¡Œåœ¨ç›¸ä¼¼æ¡ä»¶ä¸‹çš„应用程åºçš„集åˆã€‚" msgid "An external http/https URL to load the bundle from." msgstr "从外部的 http/https URL æ¥åŠ è½½ Bundle。" msgid "An external http/https URL to load the package from." msgstr "从外部的 http/https URL æ¥åŠ è½½è½¯ä»¶åŒ…ã€‚" msgid "App Catalog" msgstr "应用目录" msgid "App Category:" msgstr "应用程åºç›®å½•:" msgid "App category" msgstr "应用程åºåˆ†ç±»" msgid "Application Categories" msgstr "应用程åºç›®å½•" msgid "Application Category" msgstr "应用程åºç›®å½•" msgid "Application Details" msgstr "应用程åºè¯¦æƒ…" msgid "Application Package" msgstr "应用软件包" msgid "Application default security group" msgstr "应用缺çœå®‰å…¨ç»„" msgid "Application Components" msgstr "应用程åºç»„ä»¶" msgid "Applications" msgstr "应用程åº" msgid "Author" msgstr "作者" msgid "Auto" msgstr "自动" msgid "Back" msgstr "返回" msgid "Browse" msgstr "æµè§ˆ" msgid "Browse Local" msgstr "æµè§ˆæœ¬åœ°" msgid "Bundle Name" msgstr "Bundle åç§°" msgid "Bundle URL" msgstr "Bundle URL" msgid "Bundle creation failed.Reason: Can't find Bundle name from repository." msgstr "Bundle 创建失败,原因是:无法从仓库中找到该 Bundle。" msgid "Bundle creation failed.Reason: {0}" msgstr "Bundle 创建失败,原因是:{0}" msgid "Bundle successfully imported." msgstr "å·²æˆåŠŸå¯¼å…¥ Bundle" msgid "Bundle's full name." msgstr "Bundle 全称。" msgid "Can not get logo for {0}." msgstr "ä¸èƒ½èŽ·å–{0}的标识。" msgid "Can not get supplier logo for {0}." msgstr "ä¸èƒ½èŽ·å–{0}çš„æä¾›è€…标识。" msgid "Cancel" msgstr "å–æ¶ˆ" msgid "Categories" msgstr "目录" msgid "Category Name" msgstr "目录åç§°" msgid "Category {0} created." msgstr "目录{0}已创建" msgid "Check Keystone configuration of murano-api server." msgstr "检查 Keystone é…置中关于 murano-api æœåŠ¡çš„è®¾ç½®" msgid "Choose a Zip archive to upload into the catalog." msgstr "选择一个 ZIP 压缩包æ¥ä¸Šä¼ åˆ°ç›®å½•中。" msgid "Choose a name for the environment" msgstr "给这个环境选一个åç§°" msgctxt "Package requirements" msgid "Classes definition folder" msgstr "Classes 定义文件夹" msgid "Click to create new environment" msgstr "点击以创建新的环境" msgid "Completed with warnings" msgstr "完æˆåŒæ—¶æœ‰ä¸€äº›è­¦å‘Š" msgid "Component" msgstr "组件" msgid "Component Details" msgstr "组件详情" msgid "Component List" msgstr "组件列表" msgid "Component Logs" msgstr "组件日志" msgid "Components" msgstr "组件" msgid "Configuration" msgstr "é…ç½®" msgid "Configure Application" msgstr "设置应用程åº" msgid "Confirm password" msgstr "确认密ç " msgid "Could not retrieve latest status for the {0} environment" msgstr "ä¸èƒ½èŽ·å–环境 {0} 的最近状æ€" msgid "" "Couldn't find any apps, required for this field.\n" "Tried: {fqns}" msgstr "" "无法找到应用,需è¦ä»¥ä¸‹å­—段。\n" "Tried:{fqns}" msgid "" "Couldn't initialise glance v1 client, therefore could not make the following " "images public: {0}" msgstr "ä¸èƒ½åˆå§‹åŒ– glance v1 客户端, 所以ä¸èƒ½ä½¿ä¸‹é¢è¿™äº›é•œåƒå…¬æœ‰: {0}" msgid "Couldn't update environment. Reason: This name is already taken." msgstr "无法更新环境,原因是:åç§°å·²ç»å­˜åœ¨ã€‚" msgid "Couldn't update package {0} parameters. Error: {1}" msgstr "无法更新创建包 {0} 傿•°é”™è¯¯: {1}" msgid "Create" msgstr "创建" msgid "Create Env" msgstr "创建 Env" msgid "Create Environment" msgstr "创建环境" msgid "Create New" msgstr "新建" msgid "Create a title for an image." msgstr "给镜åƒåˆ›å»ºä¸€ä¸ªæ ‡é¢˜ã€‚" msgid "Created" msgstr "已创建" msgid "Custom Type" msgstr "定制类型" msgid "" "Default network is either not specified for this project, or specified " "incorrectly, please contact administrator." msgstr "缺çœç½‘络è¦ä¹ˆæ²¡æœ‰æŒ‡å®šç»™è¿™ä¸ªé¡¹ç›®ï¼Œè¦ä¹ˆæŒ‡å®šé”™è¯¯ï¼Œè¯·è”系管ç†å‘˜ã€‚" msgid "" "Defines whether or not a package can be used by other tenants. (Applies to " "package dependencies)" msgstr "定义一个软件包能å¦è¢«å…¶ä»–ç§Ÿæˆ·ä½¿ç”¨ã€‚ï¼ˆåŒæ—¶åº”用到软件包的ä¾èµ–)" msgid "Delete Category" msgid_plural "Delete Categories" msgstr[0] "删除类别" msgid "Delete Component" msgid_plural "Delete Components" msgstr[0] "删除组件" msgid "Delete Environment" msgid_plural "Delete Environments" msgstr[0] "删除环境å˜é‡" msgid "Delete Metadata" msgid_plural "Delete Metadata" msgstr[0] "删除元数æ®" msgid "Delete Package" msgid_plural "Delete Packages" msgstr[0] "删除包" msgid "Delete failure" msgstr "删除失败" msgid "Deleted Category" msgid_plural "Deleted Categories" msgstr[0] "已删除的类别" msgid "Deleted Metadata" msgid_plural "Deleted Metadata" msgstr[0] "已删除的元数æ®" msgid "Deleted Package" msgid_plural "Deleted Packages" msgstr[0] "已删除的包" msgid "Deleting" msgstr "删除中" msgid "Deploy Environment" msgid_plural "Deploy Environments" msgstr[0] "部署环境å˜é‡" msgid "Deploy This Environment" msgstr "部署此环境" msgid "Deploy failure" msgstr "部署失败" msgid "Deploy started" msgstr "部署已ç»å¼€å§‹" msgid "Deployed Components" msgstr "已部署的组件" msgid "Deploying" msgstr "部署中" msgid "Deployment Details" msgstr "部署详情" msgid "Deployment History" msgstr "部署历å²" msgid "Deployment Logs" msgstr "部署日志" #, python-format msgid "Deployment with id %s doesn't exist anymore" msgstr "ID 为 %s 的部署ä¸å†å­˜åœ¨" msgid "Deployment with id foo_deployment_id doesn't exist anymore" msgstr "ID为 foo_deployment_id 的部署ä¸å†å­˜åœ¨" msgid "Deployments" msgstr "部署" msgid "Description" msgstr "æè¿°" msgid "Details" msgstr "详情" msgid "Download Package" msgstr "下载软件包" msgid "Drop Components here" msgstr "把组件拖拽到此" msgid "Enabled" msgstr "å·²å¯ç”¨" msgid "" "Enter a complex password with at least one letter, one number and one " "special character" msgstr "è¯·è¾“å…¥å¤æ‚密ç ï¼Œå¿…须包å«è‡³å°‘一个字æ¯ã€ä¸€ä¸ªæ•°å­—和一个特殊字符" #, python-format msgid "Enter a dict with choices and values. Got %(value)s." msgstr "输入有选项和值的字典。获å–%(value)s。" msgid "Enter a password" msgstr "输入密ç " msgid "Enter an image type supported by Murano." msgstr "输入Murano支æŒçš„镜åƒç±»åž‹ã€‚" msgid "Environment" msgstr "环境" msgid "Environment Default Network" msgstr "环境默认网络" msgid "Environment Deployment History" msgstr "环境部署历å²" msgid "Environment Name" msgstr "环境åç§°" msgid "Environment name must contain at least one non-white space symbol." msgstr "环境å称必须包å«ä¸€ä¸ªéžç©ºç™½å­—符" #, python-format msgid "Environment with id %s doesn't exist anymore" msgstr "ID 为 %s 的环境ä¸å†å­˜åœ¨" msgid "Environment with specified name already exists" msgstr "å·²ç»å­˜åœ¨ç›¸åŒå称的环境" msgid "Environments" msgstr "环境" #, python-format msgid "" "Error fetching the environment. The page may be rendered incorrectly. " "Reason: %s" msgstr "获å–环境时出错。页é¢è¢«é”™è¯¯çš„æ¸²æŸ“。原因:%s" msgid "Error test_error_message occurred while installing package bar" msgstr "当安装包bar时,出现错误测试错误信æ¯" msgid "Error {0} occurred while installing images for {1}" msgstr "å®‰è£…é•œåƒ {1} æ—¶å‘生错误 {0}" msgid "Error {0} occurred while installing package {1}" msgstr "安装软件包 {1} æ—¶å‘生错误 {0}" msgid "Error {0} occurred while parsing package {1}" msgstr "è§£æžè½¯ä»¶åŒ… {1} æ—¶å‘生错误 {0}" msgid "Error {0} occurred while setting image {1}, {2} public" msgstr "è®¾ç½®é•œåƒ {1}ã€{2} 为公有时å‘生错误 {0}" msgctxt "Package requirements" msgid "Execution plans folder" msgstr "执行计划文件夹" msgid "FQN" msgstr "FQN" msgid "Failed" msgstr "失败" msgid "Failed to create environment" msgstr "创建环境失败" msgid "Failed to modify the package. {0}" msgstr "修改软件包失败。{0}" msgid "File" msgstr "文件" msgid "Filter" msgstr "过滤" msgid "Find in a selected category" msgstr "在所选分类中查找" 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 "" "ç¬¬ä¸€ä¸ªå­—ç¬¦éœ€è¦æ˜¯å­—æ¯æˆ–下划线,åŽé¢çš„字符å¯ä»¥æ˜¯å­—æ¯ã€æ•°å­—ã€ä¸‹åˆ’线或者, @, #, $" msgid "Foo" msgstr "Foo" msgid "Fully qualified package name." msgstr "有效的软件包å称。" #, python-format msgid "" "Go to Packages , click 'Import Package' " "and select Repository as Package Source." msgstr "" "转到 软件包 , 点击“导入软件包并选择仓" "库 作为 软件æº." #, python-format msgid "" "Go to Packages, click 'Import Package' " "and select Repository as Package Source." msgstr "" "转到 软件包, 点击 '导入软件包' 并选择仓" "库 作为 软件æº." msgid "HTTP/HTTPS URL of the bundle file." msgstr "Bundle 文件的 HTTP/HTTPS URL。" msgid "HTTP/HTTPS URL of the package file." msgstr "软件包文件的 HTTP/HTTPS URL。" msgid "Heat Orchestration stack name" msgstr "Heat 编排栈的åç§°" msgid "Heat Orchestration stack%(forloop.counter)s name" msgstr "Heat 编排栈 %(forloop.counter)s çš„åç§°" 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 "" "如果软件包ä¾èµ–其他的软件包或特定的 Glance 镜åƒï¼Œé‚£ä¹ˆå®ƒä»¬ä¹Ÿä¼šä»Ž 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 "" "如果软件包ä¾èµ–其他的软件包或特定的 Glance 镜åƒï¼Œé‚£ä¹ˆå®ƒä»¬ä¹Ÿä¼šä»Ž Murano 仓库中" "获å–并安装。" msgid "Image" msgstr "镜åƒ" msgid "Image Title" msgstr "é•œåƒæ ‡é¢˜" msgid "Image Type" msgstr "镜åƒç±»åž‹" msgid "Image successfully marked" msgstr "é•œåƒæ ‡è®°æˆåŠŸ" msgid "Images" msgstr "镜åƒ" msgid "Import Bundle" msgstr "导入 Bundle" msgid "Import Package" msgstr "导入软件包" msgid "Importing package {0} failed. Reason: {1}" msgstr "导入软件包 {0} 失败,原因是:{1}" msgid "Info" msgstr "ä¿¡æ¯" msgid "Instance name" msgstr "实例åç§°" msgid "Instance%(forloop.counter)s name" msgstr "实例 %(forloop.counter)s çš„åç§°" msgid "Invalid metadata for image: {0}" msgstr "é•œåƒ {0} çš„å…ƒæ•°æ®æ— æ•ˆ" msgid "Invalid murano image metadata" msgstr "无效的 murano 镜åƒå…ƒæ•°æ®ã€‚" msgid "Invalid value of 'murano_nets' option" msgstr "“murano_netsâ€çš„值无效" msgid "It is forbidden to upload files larger than {0} MB." msgstr "ç¦æ­¢ä¸Šä¼ è¶…过 {0} MB 大å°çš„æ–‡ä»¶ã€‚" msgid "KeyWord" msgstr "关键字" msgid "Last operation" msgstr "上次æ“作" msgid "Latest Deployment Log" msgstr "上次部署日志" msgid "License" msgstr "许å¯è¯" msgid "Logs" msgstr "日志" msgid "Logs (Created, Message)" msgstr "æ—¥å¿—ï¼ˆåˆ›å»ºã€æ¶ˆæ¯ï¼‰" msgid "Manage" msgstr "管ç†" msgid "Manage Components" msgstr "管ç†ç»„ä»¶" msgctxt "Package requirements" msgid "Manifest file" msgstr "Manifest 文件" msgid "Mark Image" msgstr "标记镜åƒ" msgid "" "Mark an image with Murano specific metadata to be added to the selected " "image." msgstr "用 Murano标记镜åƒå¹¶æŒ‡å®šå¢žåŠ åˆ°å·²é€‰æ‹©é•œåƒçš„元数æ®" msgid "Marked Images" msgstr "已标记的镜åƒ" msgid "Modify Package" msgstr "修改软件包" msgid "Modifying package failed" msgstr "修改软件包失败" msgid "NO ENVIRONMENTS" msgstr "没有环境" msgid "Name" msgstr "åç§°" msgid "Name of the bundle." msgstr "Bundle åç§°" #, python-format msgid "Network of '%s'" msgstr "%s的网络" msgid "Next" msgstr "下一步" msgid "Next Page" msgstr "下一页" msgid "No availability zones available" msgstr "没有å¯ç”¨åŒºåŸŸ" msgid "No categories available" msgstr "没有å¯ç”¨çš„目录" msgid "No components" msgstr "没有组件" msgid "No images available" msgstr "没有å¯ç”¨é•œåƒ" msgid "No keypair" msgstr "没有密钥对" msgid "No license" msgstr "没有许å¯è¯" msgid "No recent activity to report at this time." msgstr "当剿—¶é—´æ²¡æœ‰æœ€è¿‘的活动报告" msgid "No requirements" msgstr "æ²¡æœ‰è¦æ±‚" msgid "No volumes available" msgstr "没有å¯ç”¨å·ã€‚" msgid "None" msgstr "æ— " msgid "Not in domain" msgstr "ä¸åœ¨åŸŸä¸­" msgid "Note" msgstr "æç¤º" msgid "" "OpenStack Networking (Neutron) is not available in current environment. " "Custom Network Settings cannot be applied" msgstr "OpenStack 网络æœåŠ¡ï¼ˆNeuron)当å‰çŽ¯å¢ƒä¸å¯ç”¨ã€‚无法应用自定义的网络设置。" msgid "Operation is forbidden by murano-api server." msgstr "æ“作被 murano-api æœåŠ¡ç¦æ­¢ã€‚" msgid "Optional" msgstr "å¯é€‰" msgid "Overview" msgstr "概览" msgid "Package Bundle Source" msgstr "软件包 Bundle æº" msgid "Package Count" msgstr "软件包数目" msgid "Package Details" msgstr "软件包详情" msgid "Package Name" msgstr "软件包åç§°" msgid "Package Source" msgstr "软件æº" msgid "Package Tags" msgstr "软件包标签" msgid "Package URL" msgstr "软件包 URL" msgid "Package Version" msgstr "软件包版本" msgid "" "Package creation failed.Reason: Can't find Package name from repository." msgstr "软件包创建失败,原因是:无法从仓库里找到该软件包。" msgid "Package creation failed.Reason: {0}" msgstr "软件包创建失败,原因是:{0}" msgid "Package foo uploaded" msgstr "上传包foo" msgid "Package modified." msgstr "软件包已修改。" msgid "Package name in the repository, usually a fully qualified name" msgstr "仓库中软件包的å称,通常是完整的åç§°" msgid "Package or Class with the same name is already made public" msgstr "相åŒåå­—çš„è½¯ä»¶åŒ…æˆ–ç±»å·²ç»æ˜¯å…¬æœ‰çš„" msgid "Package parameters successfully updated." msgstr "å·²æˆåŠŸæ›´æ–°è½¯ä»¶åŒ…å‚æ•°" msgid "Package version" msgstr "软件包版本" msgid "Package with id foo_package_id is not found" msgstr "id为foo_package_id的包没有找到" msgid "Package with id {0} is not found" msgstr "没有å‘现 ID 为 {0} 的软件" msgid "Package with specified name already exists" msgstr "å·²ç»å­˜åœ¨ç›¸åŒå称的软件包" msgid "Package {0} already registered." msgstr "已注册软件包 {0}" msgid "Package {0} upload failed. {1}" msgstr "软件包 {0} 上传失败。{1}" msgid "Package {0} uploaded" msgstr "已上传软件包 {0}" msgid "Packages" msgstr "软件包" msgid "Packages should contain:" msgstr "软件包应该包括:" msgid "Please confirm your password" msgstr "请确认您的密ç " msgid "Please supply a bundle name" msgstr "请æä¾›ä¸€ä¸ª Bundle åç§°" msgid "Please supply a bundle url" msgstr "请æä¾›ä¸€ä¸ª Bundle URL" msgid "Please supply a package file" msgstr "请æä¾›ä¸€ä¸ªè½¯ä»¶åŒ…文件" msgid "Please supply a package name" msgstr "请æä¾›ä¸€ä¸ªè½¯ä»¶åŒ…åç§°" msgid "Please supply a package url" msgstr "请æä¾›ä¸€ä¸ªè½¯ä»¶åŒ… URL" msgid "Previous Page" msgstr "上一页" msgid "Provide comma-separated list of words, associated with the package" msgstr "æä¾›ä¸ŽåŒ…相关的一组è¯ï¼Œç”¨é€—å·åˆ†éš”" msgid "Provide desired name for a new category" msgstr "给新集åˆå–一个åç§°" msgid "Public" msgstr "公有" msgid "Quick Deploy" msgstr "快速部署" msgid "Ready" msgstr "就绪" msgid "Ready to configure" msgstr "å¾…é…ç½®" msgid "Ready to deploy" msgstr "待部署" msgid "Recent Activity" msgstr "最近的活动" msgid "Repository" msgstr "仓库" msgid "Requested object is not found on murano server." msgstr "无法在 murano æœåŠ¡å™¨ä¸Šæ‰¾åˆ°æ‰€è¯·æ±‚çš„å¯¹è±¡ã€‚" msgid "Requested operation conflicts with an existing object." msgstr "请求的æ“作和已存在的对象冲çªã€‚" msgid "Requirements" msgstr "需求" msgid "Retype your password" msgstr "釿–°è¾“入密ç " msgid "Running" msgstr "è¿è¡Œä¸­" msgid "Running with errors" msgstr "è¿è¡Œä¸­åŒæ—¶æœ‰é”™è¯¯" msgid "Running with warnings" msgstr "è¿è¡Œä¸­åŒæ—¶æœ‰è­¦å‘Š" msgid "Select Application" msgstr "选择应用" msgid "Select Image" msgstr "选择镜åƒ" #, python-format msgid "Select a valid choice. %(value)s is not one of the available choices." msgstr "选择一个有效的选项。%(value)s䏿˜¯å¯ç”¨é€‰é¡¹ä¹‹ä¸€ã€‚" msgid "Select an image registered in Glance Image Services." msgstr "在 Glance é•œåƒæœåŠ¡ä¸­é€‰æ‹©ä¸€ä¸ªå·²æ³¨å†Œçš„é•œåƒã€‚" msgid "" "Select an image type supported by Murano. Choose 'Custom type' to enter type " "manually." msgstr "选择一个 Murano 支æŒçš„镜åƒç±»åž‹ã€‚é€‰æ‹©â€œè‡ªå®šä¹‰ç±»åž‹â€æ¥æ‰‹åŠ¨è¾“å…¥ã€‚" msgid "Select one or more categories for a package." msgstr "为软件包选择一个或多个分类。" msgid "Select volume" msgstr "选择å·" msgid "Services (Name, Type)" msgstr "æœåŠ¡ï¼ˆåç§°ã€ç±»åž‹ï¼‰" msgid "Set up for identifying a package." msgstr "为软件包设置标示。" msgid "Show Details" msgstr "显示详情" msgid "Something went wrong during package downloading" msgstr "下载软件包的时候å‘生错误" msgid "Sorry, this environment doesn't exist anymore" msgstr "对ä¸èµ·ï¼Œè¿™ä¸ªçŽ¯å¢ƒä¸å†å­˜åœ¨" msgid "" "Sorry, you can't add application right now. The environment is deploying." msgstr "对ä¸èµ·ï¼Œæ‚¨çŽ°åœ¨ä¸èƒ½æ·»åŠ åº”ç”¨ç¨‹åºã€‚当å‰çŽ¯å¢ƒæ­£åœ¨éƒ¨ç½²ã€‚" msgid "Sorry, you can't delete service right now" msgstr "对ä¸èµ·ï¼Œæ‚¨çŽ°åœ¨ä¸èƒ½åˆ é™¤æœåŠ¡" msgid "Specified title already in use. Please choose another one." msgstr "指定的å称已被使用,请更æ¢" msgid "Specifying a category helps to filter applications in the catalog" msgstr "指定一个分类有助于在目录中进行过滤" msgid "Started Deleting Component" msgid_plural "Started Deleting Components" msgstr[0] "开始删除组件" msgid "Started Deleting Environment" msgid_plural "Started Deleting Environments" msgstr[0] "开始删除环境" msgid "Started deploying Environment" msgid_plural "Started deploying Environments" msgstr[0] "开始部署环境" msgid "Status" msgstr "状æ€" msgid "Step {0}" msgstr "步骤 {0}" msgid "Successful" msgstr "æˆåŠŸ" msgid "Tags" msgstr "标签" msgid "Tenant Name" msgstr "租户åç§°" msgid "The '{0}' application successfully added to environment." msgstr "第 '{0}' 个应用程åºå·²æˆåŠŸæ·»åŠ åˆ°çŽ¯å¢ƒä¸­ã€‚" 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 "" "除éžå•独设置,å¦åˆ™è¿™ä¸ªçŽ¯å¢ƒä¸­çš„åº”ç”¨çš„è™šæ‹Ÿæœºä¼šè‡ªåŠ¨åŠ å…¥è¿™ä¸ªç½‘ç»œã€‚é€‰æ‹©â€œæ–°å»ºâ€ä¼šåˆ›" "建一个网络,å­ç½‘çš„ IP 段是从这个工程默认的 Murano 路由器的å¯ç”¨èµ„æºä¸­åˆ†é…的。" #, python-format msgid "" "The bundle is going to be installed from %(murano_repo_url)s repository." msgstr "" "将从 %(murano_repo_url)s 这个仓库安装 Bundle。" msgid "The environment name field cannot be empty." msgstr "环境的åç§°ä¸èƒ½ä¸ºç©º" #, python-format msgid "" "The package is going to be imported from %(murano_repo_url)s repository." msgstr "" "将从 %(murano_repo_url)s 这个仓库导入软件包。" msgid "" "The password must contain at least one letter, " "one number and one special character" msgstr "密ç å¿…须包å«è‡³å°‘一个字é¢ã€ä¸€ä¸ªæ•°å­—和一个特殊字符" msgid "The request data is not acceptable by the server" msgstr "请求的数æ®ä¸èƒ½è¢«æœåŠ¡å™¨æŽ¥å—" msgid "There are no applications in the catalog. You can import apps from" msgstr "这个目录中没有应用程åºã€‚ä½ å¯ä»¥ä»Žä»¥ä¸‹åœ°æ–¹å¯¼å…¥ï¼š" #, python-format msgid "" "There are no applications in the catalog. You can import apps from %(display_repo_url)s." msgstr "" "目录中没有应用. ä½ å¯ä»¥ä»Ž " "%(display_repo_url)s导入." msgid "There are no applications matching your criteria." msgstr "没有和您的æ¡ä»¶åŒ¹é…的应用程åºã€‚" msgid "There was an error communicating with server" msgstr "与æœåŠ¡å™¨é€šä¿¡å‡ºé”™" msgid "There was an error initialising this field." msgstr "åˆå§‹åŒ–该域时出错" msgid "" "This Application requires encryption, please contact your administrator to " "configure this." msgstr "此应用需è¦åŠ å¯†ï¼Œè¯·è”系您的管ç†å‘˜è¿›è¡Œé…置。" msgid "" "This action cannot be undone. Any resources created by this environment will " "have to be released manually." msgstr "这个动作ä¸èƒ½æ’¤æ¶ˆã€‚这个环境创建的所有资æºéœ€è¦æ‰‹åŠ¨é‡Šæ”¾ã€‚" msgid "Time Finished" msgstr "计时结æŸ" msgid "Time Started" msgstr "计时开始" msgid "Time updated" msgstr "时间已更新" msgid "Title" msgstr "标题" msgid "Toggle Active" msgid_plural "Toggle Active" msgstr[0] "切æ¢è‡³Active状æ€" msgid "Toggle Enabled" msgstr "切æ¢å¯ç”¨" msgid "Toggle Public" msgid_plural "Toggle Public" msgstr[0] "切æ¢è‡³Public状æ€" msgid "Toggled Active" msgid_plural "Toggled Active" msgstr[0] "已切æ¢è‡³Active状æ€" msgid "Toggled Public" msgid_plural "Toggled Public" msgstr[0] "已切æ¢è‡³Public状æ€" msgid "Topology" msgstr "拓扑" msgid "" "Trying to add {0} image to glance. Image will be ready for deployment after " "successful upload" msgstr "正在å°è¯•æŠŠé•œåƒ {0} 加入到 Glance 中。上传æˆåŠŸåŽé•œåƒå°±å¯ä»¥ç”¨æ¥éƒ¨ç½²" msgid "" "Trying to add {0}, {1} image to glance. Image will be ready for deployment " "after successful upload" msgstr "正在å°è¯•æŠŠé•œåƒ {0}, {1} 加入到 Glance 中。上传æˆåŠŸåŽé•œåƒå°±å¯ä»¥ç”¨æ¥éƒ¨ç½²" msgid "Type" msgstr "类型" msgctxt "Package requirements" msgid "UI definition folder" msgstr "UI 定义文件夹" msgid "UNKNOWN" msgstr "未知" msgid "URL" msgstr "URL" msgid "Unable to abandon an environment {0} due to: {1}" msgstr "无法丢弃环境 {0},原因是:{1}" msgid "Unable to communicate to glare-api server." msgstr "无法与glare-apiæœåŠ¡å™¨é€šä¿¡ã€‚" msgid "Unable to communicate to murano-api server." msgstr "无法和 murano-api æœåŠ¡é€šä¿¡ã€‚" msgid "Unable to create environment {0} due to: {1}" msgstr "无法创建环境 {0},原因是:{1}" msgid "Unable to delete category" msgstr "无法删除目录" msgid "Unable to delete environment {0} due to: {1}" msgstr "无法删除环境 {0},原因是:{1}" msgid "Unable to delete package in murano-api server" msgstr "无法在 murano-api æœåŠ¡å™¨ä¸Šåˆ é™¤è½¯ä»¶åŒ…" msgid "Unable to deploy. Try again later" msgstr "无法部署,请ç¨åŽé‡è¯•" msgid "Unable to download package." msgstr "无法下载软件包" msgid "Unable to get list of categories" msgstr "无法获å–目录列表" msgid "Unable to mark image" msgstr "无法标记镜åƒ" msgid "Unable to modify package" msgstr "无法修改软件包" msgid "Unable to remove metadata" msgstr "无法删除元数æ®" msgid "Unable to remove package." msgstr "无法移除软件包" msgid "Unable to retrieve availability zones." msgstr "无法获å–å¯ç”¨åŒºåŸŸ" msgid "Unable to retrieve deployment history." msgstr "无法获å–å¼€å‘历å²ã€‚" msgid "Unable to retrieve details for service" msgstr "æ— æ³•èŽ·å–æœåŠ¡è¯¦æƒ…" msgid "Unable to retrieve list of deployments" msgstr "无法获å–部署列表" msgid "Unable to retrieve list of images" msgstr "无法获å–镜åƒåˆ—表" msgid "" "Unable to retrieve list of services. This environment is deploying or " "already deployed by other user." msgstr "æ— æ³•èŽ·å–æœåŠ¡åˆ—è¡¨ã€‚è¿™ä¸ªçŽ¯å¢ƒæ­£åœ¨éƒ¨ç½²æˆ–è€…å…¶ä»–äººå·²ç»éƒ¨ç½²äº†" msgid "Unable to retrieve package details." msgstr "无法获å–软件包详情" msgid "Unable to retrieve project list." msgstr "无法获å–工程列表" msgid "Unable to retrieve public images." msgstr "无法获å–公共镜åƒã€‚" msgid "Unable to retrieve snapshot list." msgstr "无法获å–快照列表。" msgid "Unable to retrieve volume list." msgstr "无法获å–å·åˆ—表。" msgid "Unavailable" msgstr "ä¸å¯ç”¨" msgid "Unknown" msgstr "未知" msgid "Update" msgstr "æ›´æ–°" msgid "Update Environment" msgid_plural "Deploy Environments" msgstr[0] "更新环境" msgid "Update Image" msgstr "更新镜åƒ" msgid "Update Metadata" msgstr "更新元数æ®" msgid "Update This Environment" msgstr "更新此环境" msgid "Updated" msgstr "已更新" msgid "Updated Environment" msgid_plural "Deployed Environments" msgstr[0] "更新环境" msgid "Uploading package failed. {0}" msgstr "上传软件包失败。{0}" msgid "Used for identifying and filtering packages." msgstr "ç”¨æ¥æ ‡ç¤ºå’Œè¿‡æ»¤è½¯ä»¶åŒ…。" msgid "Validation Error occurred" msgstr "å‘生验è¯é”™è¯¯" msgid "Version" msgstr "版本" msgid "Version of the package (optional)." msgstr "软件包版本(å¯é€‰ï¼‰ã€‚" msgid "You are not allowed to change this properties of the package" msgstr "您ä¸è¢«å…许修改这个软件包的属性" msgid "You are not allowed to delete this package" msgstr "您ä¸è¢«å…许删除这个软件包" msgid "You are not allowed to make packages public." msgstr "ä½ æ— æƒå…¬å¼€åŒ…。" msgid "You are not allowed to perform this operation" msgstr "您ä¸è¢«å…许执行这个æ“作" msgid "" "You'll have to configure each package installed from this bundle separately." msgstr "你需è¦ä¸º Bundle 中的æ¯ä¸ªè½¯ä»¶åŒ…å•独进行设置。" msgid "{0}{1} don't match" msgstr "{0}{1}ä¸åŒ¹é…" murano-dashboard-5.0.0/muranodashboard/locale/zh_CN/LC_MESSAGES/djangojs.po0000666000175100017510000000372713245511125026335 0ustar zuulzuul00000000000000# Andreas Jaeger , 2016. #zanata # Wu Han , 2016. #zanata # Wu Han , 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 \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 "你的密ç åº”该至少有" murano-dashboard-5.0.0/muranodashboard/locale/ko_KR/0000775000175100017510000000000013245511556022412 5ustar zuulzuul00000000000000murano-dashboard-5.0.0/muranodashboard/locale/ko_KR/LC_MESSAGES/0000775000175100017510000000000013245511556024177 5ustar zuulzuul00000000000000murano-dashboard-5.0.0/muranodashboard/locale/ko_KR/LC_MESSAGES/django.po0000666000175100017510000010124013245511141025767 0ustar zuulzuul00000000000000# Andreas Jaeger , 2016. #zanata # Eunseop Shin , 2016. #zanata # Ian Y. Choi , 2016. #zanata # Ian Y. Choi , 2017. #zanata # minwook-shin , 2017. #zanata # Ian Y. Choi , 2018. #zanata msgid "" msgstr "" "Project-Id-Version: murano-dashboard VERSION\n" "Report-Msgid-Bugs-To: https://bugs.launchpad.net/openstack-i18n/\n" "POT-Creation-Date: 2018-02-11 01:33+0000\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "PO-Revision-Date: 2018-02-11 07:17+0000\n" "Last-Translator: Ian Y. Choi \n" "Language-Team: Korean (South Korea)\n" "Language: ko-KR\n" "X-Generator: Zanata 3.9.6\n" "Plural-Forms: nplurals=1; plural=0\n" #, python-format msgid "" "\n" " %(used)s + %(other_used)s of %(quota)s MB used\n" " " msgstr "" "\n" " %(quota)s 중 %(used)s + %(other_used)s MB 사용ë¨\n" " " #, python-format msgid "" "\n" " %(used)s + %(other_used)s of %(quota)s used\n" " " msgstr "" "\n" " %(quota)s 중 %(used)s + %(other_used)s 사용ë¨\n" " " #, python-format msgid "%s: random subnet" msgstr "%s: ëžœë¤ ì„œë¸Œë„·" msgid "-" msgstr "-" msgid "80 characters max." msgstr "최대 80 문ìžìž…니다." msgid "A local zip file to upload" msgstr "업로드할 로컬 zip 파ì¼" msgid "Abandon Environment" msgid_plural "Abandon Environments" msgstr[0] "환경 í기" msgid "Abandoned Environment" msgid_plural "Abandoned Environments" msgstr[0] "íê¸°ëœ í™˜ê²½" msgid "Active" msgstr "Active" msgid "Add" msgstr "추가" msgid "Add Application" msgstr "ì‘ìš© 프로그램 추가" msgid "Add Application Category" msgstr "ì‘ìš© 프로그램 카테고리 추가" msgid "Add Category" msgstr "카테고리 추가" msgid "Add Component" msgstr "구성요소 추가" msgid "Add Murano Metadata" msgstr "Murano 메타ë°ì´í„° 추가" msgid "Add New" msgstr "새로 추가" msgid "Add new category to the application catalog." msgstr "ì‘ìš© 프로그램 ì¹´íƒˆë¡œê·¸ì— ìƒˆ 카테고리를 추가하십시오." msgid "Add to Env" msgstr "í™˜ê²½ì— ì¶”ê°€" msgid "Adding application to an environment failed." msgstr "ì‘ìš© í”„ë¡œê·¸ëž¨ì„ í™˜ê²½ì— ì¶”ê°€í•˜ì§€ 못했습니다." msgid "All" msgstr "모ë‘" msgid "Allows adding additional information about a package." msgstr "íŒ¨í‚¤ì§€ì— ëŒ€í•œ 정보를 추가할 수 있습니다." msgid "" "Allows to hide a package from the catalog. (Applies to package dependencies)" msgstr "카탈로그ì—서 패키지를 숨길 수 있습니다(패키지 종ì†ì„±ì— ì ìš©)." msgid "" "An environment is a collection of applications that are meant to operate " "under similar conditions." msgstr "í™˜ê²½ì€ ë¹„ìŠ·í•œ ì¡°ê±´ì—서 ìž‘ë™í•´ì•¼ 하는 ì‘ìš© í”„ë¡œê·¸ëž¨ì˜ ì½œë ‰ì…˜ìž…ë‹ˆë‹¤." msgid "An external http/https URL to load the bundle from." msgstr "로드할 ë²ˆë“¤ì´ ìžˆëŠ” 외부 http/https URL입니다." msgid "An external http/https URL to load the package from." msgstr "로드할 패키지가 있는 외부 http/https URL입니다." msgid "App Catalog" msgstr "앱 카탈로그" msgid "App Category:" msgstr "앱 카테고리:" msgid "App category" msgstr "앱 카테고리" msgid "Application Categories" msgstr "ì‘ìš© 프로그램 카테고리" msgid "Application Category" msgstr "ì‘ìš© 프로그램 카테고리" msgid "Application Details" msgstr "ì‘ìš© 프로그램 세부 사항" msgid "Application Package" msgstr "ì‘ìš© 프로그램 패키지" msgid "Application default security group" msgstr "ì‘ìš© 프로그램 기본 보안 그룹" msgid "Application Components" msgstr "ì‘ìš© 프로그램 êµ¬ì„±ìš”소" msgid "Applications" msgstr "ì‘ìš© 프로그램" msgid "Author" msgstr "작성ìž" msgid "Auto" msgstr "ìžë™" msgid "Back" msgstr "뒤로" msgid "Browse" msgstr "íƒìƒ‰" msgid "Browse Local" msgstr "로컬 찾아보기" msgid "Bundle Name" msgstr "번들 ì´ë¦„" msgid "Bundle URL" msgstr "번들 URL" msgid "Bundle creation failed.Reason: Can't find Bundle name from repository." msgstr "" "ë²ˆë“¤ì„ ìƒì„±í•˜ì§€ 못했습니다. ì´ìœ : 저장소ì—서 번들 ì´ë¦„ì„ ì°¾ì§€ 못했습니다." msgid "Bundle creation failed.Reason: {0}" msgstr "ë²ˆë“¤ì„ ìƒì„±í•˜ì§€ 못했습니다. ì´ìœ : {0}" msgid "Bundle successfully imported." msgstr "ë²ˆë“¤ì„ ì„±ê³µì ìœ¼ë¡œ 가져왔습니다." msgid "Bundle's full name." msgstr "ë²ˆë“¤ì˜ ì „ì²´ ì´ë¦„입니다." msgid "Can not get logo for {0}." msgstr "{0}ì˜ ë¡œê³ ë¥¼ 가져올 수 없습니다." msgid "Can not get supplier logo for {0}." msgstr "{0}ì˜ ê³µê¸‰ ì—…ì²´ 로고를 가져올 수 없습니다." msgid "Cancel" msgstr "취소" msgid "Categories" msgstr "카테고리" msgid "Category Name" msgstr "카테고리 ì´ë¦„" msgid "Category {0} created." msgstr "{0} 카테고리가 ìƒì„±ë˜ì—ˆìŠµë‹ˆë‹¤." msgid "Check Keystone configuration of murano-api server." msgstr "murano-api ì„œë²„ì˜ Keystone êµ¬ì„±ì„ í™•ì¸í•˜ì‹­ì‹œì˜¤." msgid "Choose a Zip archive to upload into the catalog." msgstr "ì¹´íƒˆë¡œê·¸ì— ì—…ë¡œë“œí•  Zip ì•„ì¹´ì´ë¸Œë¥¼ ì„ íƒí•˜ì‹­ì‹œì˜¤." msgid "Choose a name for the environment" msgstr "í™˜ê²½ì˜ ì´ë¦„ ì„ íƒ" msgctxt "Package requirements" msgid "Classes definition folder" msgstr "í´ëž˜ìФ ì •ì˜ í´ë”" msgid "Click to create new environment" msgstr "í´ë¦­í•˜ì—¬ 새 환경 ìƒì„±" msgid "Completed with warnings" msgstr "경고와 함께 완료" msgid "Component" msgstr "구성 요소" msgid "Component Details" msgstr "구성요소 세부 사항" msgid "Component List" msgstr "구성요소 목ë¡" msgid "Component Logs" msgstr "구성요소 로그" msgid "Components" msgstr "구성요소" msgid "Configuration" msgstr "구성" msgid "Configure Application" msgstr "ì‘ìš© 프로그램 구성" msgid "Confirm password" msgstr "암호 확ì¸" msgid "Could not retrieve latest status for the {0} environment" msgstr "{0} í™˜ê²½ì˜ ìµœì‹  ìƒíƒœë¥¼ 검색할 수 ì—†ìŒ" msgid "" "Couldn't find any apps, required for this field.\n" "Tried: {fqns}" msgstr "" "ì´ í•„ë“œì— í•„ìˆ˜ì¸ appì„ ì°¾ì„ ìˆ˜ 없습니다.\n" "시ë„: {fqns}" msgid "" "Couldn't initialise glance v1 client, therefore could not make the following " "images public: {0}" msgstr "" "glance v1 í´ë¼ì´ì–¸íŠ¸ë¥¼ 초기화할 수 없어, ë”°ë¼ì„œ ë‹¤ìŒ ì´ë¯¸ì§€ë¥¼ 공용으로 만들 " "수 없습니다: {0}" msgid "Couldn't update environment. Reason: This name is already taken." msgstr "í™˜ê²½ì„ ì—…ë°ì´íŠ¸í•  수 없습니다. ì´ìœ : ì´ ì´ë¦„ì€ ì´ë¯¸ 사용ë˜ì—ˆìŠµë‹ˆë‹¤." msgid "Couldn't update package {0} parameters. Error: {1}" msgstr "패키지 {0} 매개 변수를 ì—…ë°ì´íŠ¸í•  수 없습니다. 오류: {1}" msgid "Create" msgstr "ìƒì„±" msgid "Create Env" msgstr "환경 ìƒì„±" msgid "Create Environment" msgstr "환경 ìƒì„±" msgid "Create New" msgstr "새로 ìƒì„±" msgid "Create a title for an image." msgstr "ì´ë¯¸ì§€ì˜ ì œëª©ì„ ìƒì„±í•˜ì‹­ì‹œì˜¤." msgid "Created" msgstr "ìƒì„±ë¨" msgid "Custom Type" msgstr "사용ìžì •ì˜ íƒ€ìž…" msgid "" "Default network is either not specified for this project, or specified " "incorrectly, please contact administrator." msgstr "" "기본 네트워í¬ê°€ ì´ í”„ë¡œì íŠ¸ì— ì§€ì •ë˜ì–´ 있지 않거나 잘못 지정ë˜ì—ˆìŠµë‹ˆë‹¤. 관리" "ìžì—게 문ì˜í•˜ì‹­ì‹œì˜¤." msgid "" "Defines whether or not a package can be used by other tenants. (Applies to " "package dependencies)" msgstr "" "다른 테넌트ì—서 패키지를 사용할 수 있는지 ì •ì˜í•©ë‹ˆë‹¤(패키지 종ì†ì„±ì— ì ìš©)." msgid "Delete Category" msgid_plural "Delete Categories" msgstr[0] "카테고리 ì‚­ì œ" msgid "Delete Component" msgid_plural "Delete Components" msgstr[0] "구성요소 ì‚­ì œ" msgid "Delete Environment" msgid_plural "Delete Environments" msgstr[0] "환경 ì‚­ì œ" msgid "Delete Metadata" msgid_plural "Delete Metadata" msgstr[0] "메타ë°ì´í„° ì‚­ì œ" msgid "Delete Package" msgid_plural "Delete Packages" msgstr[0] "패키지 ì‚­ì œ" msgid "Delete failure" msgstr "ì‚­ì œ 실패" msgid "Deleted Category" msgid_plural "Deleted Categories" msgstr[0] "ì‚­ì œëœ ì¹´í…Œê³ ë¦¬" msgid "Deleted Metadata" msgid_plural "Deleted Metadata" msgstr[0] "ì‚­ì œëœ ë©”íƒ€ë°ì´í„°" msgid "Deleted Package" msgid_plural "Deleted Packages" msgstr[0] "ì‚­ì œëœ íŒ¨í‚¤ì§€" msgid "Deleting" msgstr "삭제중" msgid "Deploy Environment" msgid_plural "Deploy Environments" msgstr[0] "환경 ë°°í¬" msgid "Deploy This Environment" msgstr "ì´ í™˜ê²½ ë°°í¬" msgid "Deploy failure" msgstr "ë°°í¬ ì‹¤íŒ¨" msgid "Deploy started" msgstr "ë°°í¬ê°€ 시작ë¨" msgid "Deployed Components" msgstr "ë°°í¬ëœ 구성요소" msgid "Deploying" msgstr "ë°°í¬ì¤‘" msgid "Deployment Details" msgstr "ë°°í¬ ì„¸ë¶€ 사항" msgid "Deployment History" msgstr "ë°°í¬ ì´ë ¥" msgid "Deployment Logs" msgstr "ë°°í¬ ë¡œê·¸" #, python-format msgid "Deployment with id %s doesn't exist anymore" msgstr "IDê°€ %sì¸ ë°°í¬ê°€ ë” ì´ìƒ 존재하지 않ìŒ" msgid "Deployment with id foo_deployment_id doesn't exist anymore" msgstr "foo_deployment_id idì— í•´ë‹¹í•˜ëŠ” ë°°í¬ê°€ ë” ì´ìƒ 존재하지 않습니다" msgid "Deployments" msgstr "ë°°í¬" msgid "Description" msgstr "설명" msgid "Details" msgstr "세부 사항" msgid "Download Package" msgstr "패키지 다운로드" msgid "Drop Components here" msgstr "ì—¬ê¸°ì— êµ¬ì„±ìš”ì†Œ 놓기" msgid "Enabled" msgstr "사용ë¨" msgid "" "Enter a complex password with at least one letter, one number and one " "special character" msgstr "" "최소 í•˜ë‚˜ì˜ ë¬¸ìž, í•˜ë‚˜ì˜ ìˆ«ìž ë° í•˜ë‚˜ì˜ íŠ¹ìˆ˜ 문ìžë¥¼ í¬í•¨í•˜ëŠ” 복합 암호를 ìž…ë ¥" #, python-format msgid "Enter a dict with choices and values. Got %(value)s." msgstr "ì„ íƒ ì‚¬í•­ê³¼ 값으로 ì‚¬ì „ì„ ìž…ë ¥í•˜ì‹­ì‹œì˜¤.%(value)sì„ ë°›ì•˜ìŠµë‹ˆë‹¤." msgid "Enter a password" msgstr "암호 ìž…ë ¥" msgid "Enter an image type supported by Murano." msgstr "Muranoì—서 ì§€ì›í•˜ëŠ” ì´ë¯¸ì§€ ìœ í˜•ì„ ìž…ë ¥í•˜ì‹­ì‹œì˜¤." msgid "Environment" msgstr "환경" msgid "Environment Default Network" msgstr "환경 기본 네트워í¬" msgid "Environment Deployment History" msgstr "ë°°í¬ ì´ë ¥ 환경" msgid "Environment Name" msgstr "환경 ì´ë¦„" msgid "Environment name must contain at least one non-white space symbol." msgstr "환경 ì´ë¦„ì—는 ê³µë°±ì´ ì•„ë‹Œ 기호가 하나 ì´ìƒ í¬í•¨ë˜ì–´ì•¼ 합니다." #, python-format msgid "Environment with id %s doesn't exist anymore" msgstr "IDê°€ %sì¸ í™˜ê²½ì´ ë” ì´ìƒ 존재하지 않ìŒ" msgid "Environment with specified name already exists" msgstr "ì§€ì •ëœ ì´ë¦„ì˜ í™˜ê²½ì´ ì´ë¯¸ 있ìŒ" msgid "Environments" msgstr "환경" #, python-format msgid "" "Error fetching the environment. The page may be rendered incorrectly. " "Reason: %s" msgstr "" "í™˜ê²½ì„ ê°€ì ¸ 오는 중 오류가 ë°œìƒí–ˆìŠµë‹ˆë‹¤. 페ì´ì§€ê°€ 잘못 ë Œë”ë§ ë  ìˆ˜ 있습니" "다. ì´ìœ : %s" msgid "Error test_error_message occurred while installing package bar" msgstr "패키지 바를 설치하는 ë„중 test_error_message 오류가 ë°œìƒí•˜ì˜€ìŠµë‹ˆë‹¤" msgid "Error {0} occurred while installing images for {1}" msgstr "{1}ì˜ ì´ë¯¸ì§€ë¥¼ 설치하는 ë™ì•ˆ {0} 오류가 ë°œìƒí•¨" msgid "Error {0} occurred while installing package {1}" msgstr "{1} 패키지를 설치하는 ë™ì•ˆ {0} 오류가 ë°œìƒí•¨" msgid "Error {0} occurred while parsing package {1}" msgstr "{1} 패키지를 구문 ë¶„ì„하는 ë™ì•ˆ {0} 오류가 ë°œìƒí•¨" msgid "Error {0} occurred while setting image {1}, {2} public" msgstr "ì´ë¯¸ì§€ {1}, {2} ê³µìš©ì„ ì„¤ì •í•˜ëŠ” ë™ì•ˆ {0} 오류 ë°œìƒ" msgctxt "Package requirements" msgid "Execution plans folder" msgstr "실행 ê³„íš í´ë”" msgid "FQN" msgstr "FQN" msgid "Failed" msgstr "실패함" msgid "Failed to create environment" msgstr "í™˜ê²½ì„ ìƒì„±í•˜ëŠ” ë° ì‹¤íŒ¨" msgid "Failed to get list of flavors." msgstr "flavor 목ë¡ì„ ê°€ì ¸ì˜¤ëŠ”ë° ì‹¤íŒ¨í•˜ì˜€ìŠµë‹ˆë‹¤." msgid "Failed to modify the package. {0}" msgstr "패키지를 수정하지 못했습니다. {0}" msgid "File" msgstr "파ì¼" msgid "Filter" msgstr "í•„í„°" msgid "Find in a selected category" msgstr "ì„ íƒí•œ 카테고리ì—서 찾기" 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 "" "첫 번째 기호는 ë¼í‹´ 문ìžë‚˜ 밑줄ì´ì–´ì•¼ 합니다. ë‹¤ìŒ ê¸°í˜¸ëŠ” ë¼í‹´ 문ìž, 숫ìž, ë°‘" "줄, @ 기호, # 기호 ë˜ëŠ” $ ê¸°í˜¸ì¼ ìˆ˜ 있습니다." msgid "Foo" msgstr "Foo" msgid "Fully qualified package name." msgstr "완전한 패키지 ì´ë¦„입니다." #, python-format msgid "" "Go to Packages , click 'Import Package' " "and select Repository as Package Source." msgstr "" " 패키지 로 ì´ë™í•˜ì—¬ '패키지 가져오기'를 " "í´ë¦­í•˜ê³  저장소를 패키지 소스로 ì„ íƒí•©ë‹ˆë‹¤." #, python-format msgid "" "Go to Packages, click 'Import Package' " "and select Repository as Package Source." msgstr "" " Packages 로 ì´ë™í•˜ì—¬, '패키지 가져오" "기'를 í´ë¦­í•˜ê³  Package Source로 Repository 를 ì„ íƒí•˜ì‹­ì‹œì˜¤." msgid "HTTP/HTTPS URL of the bundle file." msgstr "번들 파ì¼ì˜ HTTP/HTTPS URL입니다." msgid "HTTP/HTTPS URL of the package file." msgstr "패키지 파ì¼ì˜ HTTP/HTTPS URL입니다." msgid "Heat Orchestration stack name" msgstr "Heat Orchestration ìŠ¤íƒ ì´ë¦„" msgid "Heat Orchestration stack%(forloop.counter)s name" msgstr "Heat Orchestration stack%(forloop.counter)s ì´ë¦„" 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 "" "패키지가 다른 íŒ¨í‚¤ì§€ì— ì¢…ì†ë˜ë©°/ë˜ê±°ë‚˜ 특정 글랜스 ì´ë¯¸ì§€ê°€ 필요한 경우 " "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 "" "패키지가 다른 íŒ¨í‚¤ì§€ì— ì¢…ì†ë˜ë©°/ë˜ê±°ë‚˜ 특정 글랜스 ì´ë¯¸ì§€ê°€ 필요한 경우 " "murano 저장소ì—서 함께 설치ë©ë‹ˆë‹¤." msgid "Image" msgstr "ì´ë¯¸ì§€" msgid "Image Title" msgstr "ì´ë¯¸ì§€ 제목" msgid "Image Type" msgstr "ì´ë¯¸ì§€ 유형" msgid "Image successfully marked" msgstr "ì´ë¯¸ì§€ì— 성공ì ìœ¼ë¡œ 표시ë¨" msgid "Images" msgstr "ì´ë¯¸ì§€" msgid "Import Bundle" msgstr "번들 가져오기" msgid "Import Package" msgstr "패키지 가져오기" msgid "Importing package {0} failed. Reason: {1}" msgstr "{0} 패키지를 가져오지 못했습니다. ì´ìœ : {1}" msgid "Info" msgstr "ì •ë³´" msgid "Instance name" msgstr "ì¸ìŠ¤í„´ìŠ¤ ì´ë¦„" msgid "Instance%(forloop.counter)s name" msgstr "Instance%(forloop.counter)s ì´ë¦„" msgid "Invalid metadata for image: {0}" msgstr "ì´ë¯¸ì§€ì˜ 올바르지 ì•Šì€ ë©”íƒ€ë°ì´í„°: {0}" msgid "Invalid murano image metadata" msgstr "올바르지 ì•Šì€ murano ì´ë¯¸ì§€ 메타ë°ì´í„°" msgid "Invalid value of 'murano_nets' option" msgstr "올바르지 ì•Šì€ 'murano_nets' 옵션 ê°’" msgid "It is forbidden to upload files larger than {0} MB." msgstr "{0}MB보다 í° íŒŒì¼ì„ 업로드할 수 없습니다." msgid "KeyWord" msgstr "키워드" msgid "Last operation" msgstr "마지막 ì¡°ìž‘" msgid "Latest Deployment Log" msgstr "최신 ë°°í¬ ë¡œê·¸" msgid "License" msgstr "ë¼ì´ì„¼ìФ" msgid "Logs" msgstr "로그" msgid "Logs (Created, Message)" msgstr "로그 (만들어ì§,메시지)" msgid "Manage" msgstr "관리" msgid "Manage Components" msgstr "구성요소 관리" msgctxt "Package requirements" msgid "Manifest file" msgstr "매니페스트 파ì¼" msgid "Mark Image" msgstr "ì´ë¯¸ì§€ 표시" msgid "" "Mark an image with Murano specific metadata to be added to the selected " "image." msgstr "ì„ íƒí•œ ì´ë¯¸ì§€ì— 추가할 Murano 특정 메타ë°ì´í„°ë¡œ ì´ë¯¸ì§€ë¥¼ 표시하십시오." msgid "Marked Images" msgstr "í‘œì‹œëœ ì´ë¯¸ì§€" msgid "Modify Package" msgstr "패키지 수정" msgid "Modifying package failed" msgstr "패키지를 수정하는 ë° ì‹¤íŒ¨" msgid "NO ENVIRONMENTS" msgstr "í™˜ê²½ì´ ì—†ìŒ" msgid "Name" msgstr "ì´ë¦„" msgid "Name of the bundle." msgstr "ë²ˆë“¤ì˜ ì´ë¦„입니다." #, python-format msgid "Network of '%s'" msgstr "'%s'ì˜ ë„¤íŠ¸ì›Œí¬" msgid "Next" msgstr "다ìŒ" msgid "Next Page" msgstr "ë‹¤ìŒ íŽ˜ì´ì§€" msgid "No availability zones available" msgstr "가역 êµ¬ì—­ì„ ì‚¬ìš©í•  수 ì—†ìŒ" msgid "No categories available" msgstr "사용 가능한 카테고리가 ì—†ìŒ" msgid "No components" msgstr "구성요소가 ì—†ìŒ" msgid "No images available" msgstr "사용 가능한 ì´ë¯¸ì§€ê°€ ì—†ìŒ" msgid "No keypair" msgstr "키페어가 ì—†ìŒ" msgid "No license" msgstr "ë¼ì´ì„¼ìŠ¤ê°€ ì—†ìŒ" msgid "No recent activity to report at this time." msgstr "현재 ë³´ê³ í•  최신 활ë™ì´ 없습니다." msgid "No requirements" msgstr "ìš”êµ¬ì‚¬í•­ì´ ì—†ìŒ" msgid "No volumes available" msgstr "사용 가능한 ë³¼ë¥¨ì´ ì—†ìŠµë‹ˆë‹¤." msgid "None" msgstr "ì—†ìŒ" msgid "Not in domain" msgstr "ë„ë©”ì¸ì— ì—†ìŒ" msgid "Note" msgstr "참고" msgid "Number of Instances" msgstr "ì¸ìŠ¤í„´ìŠ¤ 수" msgid "Number of VCPUs" msgstr "VCPU 개수" msgid "" "OpenStack Networking (Neutron) is not available in current environment. " "Custom Network Settings cannot be applied" msgstr "" "OpenStack 네트워킹(Neutron)ì€ í˜„ìž¬ 환경ì—서 사용할 수 없습니다. ì‚¬ìš©ìž ì •ì˜ " "ë„¤íŠ¸ì›Œí¬ ì„¤ì •ì„ ì ìš©í•  수 없습니다." msgid "Operation is forbidden by murano-api server." msgstr "murano-api 서버ì—서 ì¡°ìž‘ì´ ê¸ˆì§€ë˜ì—ˆìŠµë‹ˆë‹¤." msgid "Optional" msgstr "ì„ íƒ ì‚¬í•­" msgid "Overview" msgstr "개요" msgid "Package Bundle Source" msgstr "패키지 번들 소스" msgid "Package Count" msgstr "패키지 개수" msgid "Package Details" msgstr "패키지 세부 사항" msgid "Package Name" msgstr "패키지 ì´ë¦„" msgid "Package Source" msgstr "패키지 소스" msgid "Package Tags" msgstr "패키지 태그" msgid "Package URL" msgstr "패키지 URL" msgid "Package Version" msgstr "패키지 버전" msgid "" "Package creation failed.Reason: Can't find Package name from repository." msgstr "" "패키지를 ìƒì„±í•˜ì§€ 못했습니다. ì´ìœ : 저장소ì—서 패키지 ì´ë¦„ì„ ì°¾ì„ ìˆ˜ 없습니" "다." msgid "Package creation failed.Reason: {0}" msgstr "패키지를 ìƒì„±í•˜ì§€ 못했습니다. ì´ìœ : {0}" msgid "Package foo uploaded" msgstr "foo 패키지가 업로드ë¨" msgid "Package modified." msgstr "패키지가 수정ë˜ì—ˆìŠµë‹ˆë‹¤." msgid "Package name in the repository, usually a fully qualified name" msgstr "ì €ìž¥ì†Œì— ìžˆëŠ” 패키지 ì´ë¦„(ì¼ë°˜ì ìœ¼ë¡œ 완전한 ì´ë¦„)" msgid "Package or Class with the same name is already made public" msgstr "ì´ë¦„ì´ ê°™ì€ íŒ¨í‚¤ì§€ë‚˜ í´ëž˜ìŠ¤ê°€ ì´ë¯¸ ê³µìš©ì´ ë¨" msgid "Package parameters successfully updated." msgstr "패키지 매개 변수가 성공ì ìœ¼ë¡œ ì—…ë°ì´íЏë˜ì—ˆìŠµë‹ˆë‹¤." msgid "Package version" msgstr "패키지 버전" msgid "Package with id foo_package_id is not found" msgstr "foo_package_id idì— í•´ë‹¹í•˜ëŠ” 패키지를 ì°¾ì„ ìˆ˜ 없습니다" msgid "Package with id {0} is not found" msgstr "IDê°€ {0}ì¸ íŒ¨í‚¤ì§€ê°€ ì—†ìŒ" msgid "Package with specified name already exists" msgstr "ì§€ì •ëœ ì´ë¦„ì˜ íŒ¨í‚¤ì§€ê°€ ì´ë¯¸ 있ìŒ" msgid "Package {0} already registered." msgstr "{0} 패키지가 ì´ë¯¸ 등ë¡ë˜ì—ˆìŠµë‹ˆë‹¤." msgid "Package {0} upload failed. {1}" msgstr "{0} 패키지를 업로드하지 못했습니다. {1}" msgid "Package {0} uploaded" msgstr "{0} 패키지가 업로드ë¨" msgid "Packages" msgstr "패키지" msgid "Packages should contain:" msgstr "íŒ¨í‚¤ì§€ì— í¬í•¨ë˜ì–´ì•¼ 하는 ë‚´ìš©:" msgid "Please confirm your password" msgstr "암호 확ì¸" msgid "Please supply a bundle name" msgstr "번들 ì´ë¦„ 제공" msgid "Please supply a bundle url" msgstr "번들 URL 제공" msgid "Please supply a package file" msgstr "패키지 íŒŒì¼ ì œê³µ" msgid "Please supply a package name" msgstr "패키지 ì´ë¦„ 제공" msgid "Please supply a package url" msgstr "패키지 URL 제공" msgid "Previous Page" msgstr "ì´ì „ 페ì´ì§€" msgid "Provide comma-separated list of words, associated with the package" msgstr "패키지와 ê´€ë ¨ëœ ì½¤ë§ˆë¡œ êµ¬ë¶„ëœ ë‹¨ì–´ 목ë¡ì„ 제공합니다" msgid "Provide desired name for a new category" msgstr "새 ì¹´í…Œê³ ë¦¬ì˜ ì›í•˜ëŠ” ì´ë¦„ 제공" msgid "Public" msgstr "공용" msgid "Quick Deploy" msgstr "빠른 ë°°í¬" msgid "Ready" msgstr "준비ë¨" msgid "Ready to configure" msgstr "구성 준비 완료" msgid "Ready to deploy" msgstr "ë°°í¬ ì¤€ë¹„ 완료" msgid "Recent Activity" msgstr "최신 활ë™" msgid "Repository" msgstr "저장소" msgid "Requested object is not found on murano server." msgstr "ìš”ì²­ëœ ì˜¤ë¸Œì íŠ¸ê°€ murano ì„œë²„ì— ì—†ìŠµë‹ˆë‹¤." msgid "Requested operation conflicts with an existing object." msgstr "ìš”ì²­ëœ ì¡°ìž‘ì´ ê¸°ì¡´ 오브ì íŠ¸ì™€ ì¶©ëŒí•©ë‹ˆë‹¤." msgid "Requirements" msgstr "요구사항" msgid "Retype your password" msgstr "암호 다시 ìž…ë ¥" msgid "Running" msgstr "실행중" msgid "Running with errors" msgstr "실행 중 오류 ë°œìƒ" msgid "Running with warnings" msgstr "실행 중 경고 ë°œìƒ" msgid "Select Application" msgstr "ì‘ìš© 프로그램 ì„ íƒ" msgid "Select Image" msgstr "ì´ë¯¸ì§€ ì„ íƒ" #, python-format msgid "Select a valid choice. %(value)s is not one of the available choices." msgstr "" "유효한 ì„ íƒ ì‚¬í•­ì„ ì„ íƒí•˜ì‹­ì‹œì˜¤. %(value)s는 사용 가능한 ì„ íƒ ì‚¬í•­ 중 하나가 " "아닙니다." msgid "Select an image registered in Glance Image Services." msgstr "글랜스 ì´ë¯¸ì§€ ì„œë¹„ìŠ¤ì— ë“±ë¡ëœ ì´ë¯¸ì§€ë¥¼ ì„ íƒí•˜ì‹­ì‹œì˜¤." msgid "" "Select an image type supported by Murano. Choose 'Custom type' to enter type " "manually." msgstr "" "Muranoì—서 ì§€ì›í•˜ëŠ” ì´ë¯¸ì§€ ìœ í˜•ì„ ì„ íƒí•˜ì‹­ì‹œì˜¤. '사용ìžì •ì˜ íƒ€ìž…'ì„ ì„ íƒí•˜ì—¬ " "ì§ì ‘ 입력하세요." msgid "Select one or more categories for a package." msgstr "íŒ¨í‚¤ì§€ì˜ ì¹´í…Œê³ ë¦¬ë¥¼ 하나 ì´ìƒ ì„ íƒí•˜ì‹­ì‹œì˜¤." msgid "Select volume" msgstr "볼륨 ì„ íƒ" msgid "Services (Name, Type)" msgstr "서비스 (ì´ë¦„, 유형)" msgid "Set up for identifying a package." msgstr "패키지를 ì‹ë³„하ë„ë¡ ì„¤ì •í•˜ì‹­ì‹œì˜¤." msgid "Show Details" msgstr "세부 사항 표시" msgid "Something went wrong during package downloading" msgstr "패키지 다운로드 ì¤‘ì— ë¬¸ì œ ë°œìƒ" msgid "Sorry, this environment doesn't exist anymore" msgstr "죄송합니다. ì´ í™˜ê²½ì´ ë” ì´ìƒ 존재하지 않습니다." msgid "" "Sorry, you can't add application right now. The environment is deploying." msgstr "" "죄송합니다. 현재 ì‘ìš© í”„ë¡œê·¸ëž¨ì„ ì¶”ê°€í•  수 없습니다. í™˜ê²½ì„ ë°°í¬ ì¤‘ìž…ë‹ˆë‹¤." msgid "Sorry, you can't delete service right now" msgstr "죄송합니다. 현재 서비스를 삭제할 수 없습니다." msgid "Specified title already in use. Please choose another one." msgstr "ì§€ì •ëœ ì œëª©ì´ ì´ë¯¸ 사용 중입니다. 다른 ì œëª©ì„ ì‚¬ìš©í•˜ì‹­ì‹œì˜¤." msgid "Specifying a category helps to filter applications in the catalog" msgstr "카테고리를 지정하면 카탈로그ì—서 ì‘ìš© í”„ë¡œê·¸ëž¨ì„ í•„í„°ë§í•  수 있ìŒ" msgid "Started Deleting Component" msgid_plural "Started Deleting Components" msgstr[0] "구성요소 ì‚­ì œ 시작" msgid "Started Deleting Environment" msgid_plural "Started Deleting Environments" msgstr[0] "환경 ì‚­ì œ 시작" msgid "Started deploying Environment" msgid_plural "Started deploying Environments" msgstr[0] "환경 ë°°í¬ ì‹œìž‘" msgid "Status" msgstr "ìƒíƒœ" msgid "Step {0}" msgstr "{0}단계" msgid "Successful" msgstr "완료" msgid "Tags" msgstr "태그" msgid "Tenant Name" msgstr "테넌트 ì´ë¦„" msgid "The '{0}' application successfully added to environment." msgstr "'{0}' ì‘ìš© í”„ë¡œê·¸ëž¨ì„ ì„±ê³µì ìœ¼ë¡œ í™˜ê²½ì— ì¶”ê°€í–ˆìŠµë‹ˆë‹¤." 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 "" "개별ì ìœ¼ë¡œ 구성하지 ì•Šì€ ê²½ìš°, ì´ í™˜ê²½ì— ìžˆëŠ” ì‘ìš© í”„ë¡œê·¸ëž¨ì˜ VMì€ ê¸°ë³¸ì ìœ¼" "로 ì´ ë„·ì— ê²°í•©ë©ë‹ˆë‹¤. '지금 ìƒì„±'ì„ ì„ íƒí•˜ë©´ ì´ í”„ë¡œì íŠ¸ì˜ ê¸°ë³¸ Murano ë¼ìš°" "í„°ì— ì‚¬ìš© 가능한 IP ì¤‘ì— ì„œë¸Œë„·ì´ í• ë‹¹ëœ IP ë²”ìœ„ì— ìžˆëŠ” 새로운 네트워í¬ë¥¼ ìƒ" "성합니다." #, python-format msgid "" "The bundle is going to be installed from %(murano_repo_url)s repository." msgstr "" "%(murano_repo_url)s ì €" "장소ì—서 ë²ˆë“¤ì´ ì„¤ì¹˜ë©ë‹ˆë‹¤." msgid "The environment name field cannot be empty." msgstr "환경 ì´ë¦„ 필드는 비울 수 없습니다." #, python-format msgid "" "The package is going to be imported from %(murano_repo_url)s repository." msgstr "" "%(murano_repo_url)s ì €" "장소ì—서 패키지를 가져옵니다." msgid "" "The password must contain at least one letter, " "one number and one special character" msgstr "" "암호ì—는 최소 í•˜ë‚˜ì˜ ë¬¸ìž, í•˜ë‚˜ì˜ ìˆ«ìž ë° í•˜" "ë‚˜ì˜ íŠ¹ìˆ˜ 문ìžê°€ 있어야 함" msgid "The request data is not acceptable by the server" msgstr "ìš”ì²­ëœ ë°ì´í„°ëŠ” 서버ì—서 허용ë˜ì§€ 않ìŒ" msgid "There are no applications in the catalog. You can import apps from" msgstr "ì¹´íƒˆë¡œê·¸ì— ì‘ìš© í”„ë¡œê·¸ëž¨ì´ ì—†ìŠµë‹ˆë‹¤. ì•±ì„ ê°€ì ¸ì˜¬ 수 있습니다." #, python-format msgid "" "There are no applications in the catalog. You can import apps from %(display_repo_url)s." msgstr "" "ì¹´íƒˆë¡œê·¸ì— ì‘ìš© í”„ë¡œê·¸ëž¨ì´ ì—†ìŠµë‹ˆë‹¤. %(display_repo_url)sì—서 ì•±ì„ ê°€ì ¸ì˜¬ 수 있습니다." msgid "There are no applications matching your criteria." msgstr "ê¸°ì¤€ì— ë§žëŠ” ì‘ìš© í”„ë¡œê·¸ëž¨ì´ ì—†ìŠµë‹ˆë‹¤." msgid "There was an error communicating with server" msgstr "ì„œë²„ì™€ì˜ í†µì‹ ì—서 오류가 있었습니다" msgid "There was an error initialising this field." msgstr "해당 필드를 초기화하는 ë° ì˜¤ë¥˜ê°€ ë°œìƒí•˜ì˜€ìŠµë‹ˆë‹¤." msgid "" "This Application requires encryption, please contact your administrator to " "configure this." msgstr "" "ì´ ì‘ìš© 프로그램ì—는 암호화가 필요합니다. 관리ìžì—게 문ì˜í•˜ì—¬ 구성하십시오." msgid "" "This action cannot be undone. Any resources created by this environment will " "have to be released manually." msgstr "" "ì´ ìž‘ì—…ì€ ì·¨ì†Œí•  수 없습니다. ì´ í™˜ê²½ì—서 ìƒì„±í•œ 모든 리소스를 수ë™ìœ¼ë¡œ í•´ì œ" "해야 합니다." msgid "Time Finished" msgstr "ì™„ë£Œëœ ì‹œê°„" msgid "Time Started" msgstr "ì‹œìž‘ëœ ì‹œê°„" msgid "Time updated" msgstr "ì—…ë°ì´íŠ¸í•œ 시간" msgid "Title" msgstr "제목" msgid "Toggle Active" msgid_plural "Toggle Active" msgstr[0] "활성 토í´" msgid "Toggle Enabled" msgstr "í† ê¸€ì´ ì‚¬ìš©ë¨" msgid "Toggle Public" msgid_plural "Toggle Public" msgstr[0] "공용 토글" msgid "Toggled Active" msgid_plural "Toggled Active" msgstr[0] "활성 토글" msgid "Toggled Public" msgid_plural "Toggled Public" msgstr[0] "공용 토글" msgid "Topology" msgstr "토í´ë¡œì§€" msgid "Total RAM" msgstr "ì´ RAM" msgid "" "Trying to add {0} image to glance. Image will be ready for deployment after " "successful upload" msgstr "" "ê¸€ëžœìŠ¤ì— {0} ì´ë¯¸ì§€ë¥¼ 추가하려고 합니다. ì—…ë¡œë“œì— ì„±ê³µí•˜ê³  나면 ì´ë¯¸ì§€ë¥¼ ë°°í¬" "í•  준비가 ë©ë‹ˆë‹¤." msgid "" "Trying to add {0}, {1} image to glance. Image will be ready for deployment " "after successful upload" msgstr "" "ê¸€ëžœìŠ¤ì— {0}, {1} ì´ë¯¸ì§€ë¥¼ 추가하려고 합니다. ì—…ë¡œë“œì— ì„±ê³µí•˜ê³  나면 ì´ë¯¸ì§€" "를 ë°°í¬í•  준비가 ë©ë‹ˆë‹¤." msgid "Type" msgstr "타입" msgctxt "Package requirements" msgid "UI definition folder" msgstr "UI ì •ì˜ í´ë”" msgid "UNKNOWN" msgstr "알 수 ì—†ìŒ" msgid "URL" msgstr "URL" msgid "Unable to abandon an environment {0} due to: {1}" msgstr "ë‹¤ìŒ ì´ìœ ë¡œ ì¸í•´ {0} í™˜ê²½ì„ í기할 수 ì—†ìŒ: {1}" msgid "Unable to communicate to glare-api server." msgstr "glare-api 서버와 통신할 수 없습니다." msgid "Unable to communicate to murano-api server." msgstr "murano-api 서버와 통신할 수 없습니다." msgid "Unable to create environment {0} due to: {1}" msgstr "ë‹¤ìŒ ì´ìœ ë¡œ ì¸í•´ {0} í™˜ê²½ì„ ìƒì„±í•  수 ì—†ìŒ: {1}" msgid "Unable to delete category" msgstr "카테고리를 삭제할 수 ì—†ìŒ" msgid "Unable to delete environment {0} due to: {1}" msgstr "ë‹¤ìŒ ì´ìœ ë¡œ ì¸í•´ {0} í™˜ê²½ì„ ì‚­ì œí•  수 ì—†ìŒ: {1}" msgid "Unable to delete package in murano-api server" msgstr "murano-api 서버ì—서 패키지를 삭제할 수 ì—†ìŒ" msgid "Unable to deploy. Try again later" msgstr "ë°°í¬í•  수 없습니다. ë‚˜ì¤‘ì— ë‹¤ì‹œ 시ë„하십시오." msgid "Unable to download package." msgstr "패키지를 다운로드할 수 없습니다." msgid "Unable to get list of categories" msgstr "카테고리 목ë¡ì„ 가져올 수 ì—†ìŒ" msgid "Unable to mark image" msgstr "ì´ë¯¸ì§€ì— 표시할 수 ì—†ìŒ" msgid "Unable to modify package" msgstr "패키지를 수정할 수 ì—†ìŒ" msgid "Unable to remove metadata" msgstr "메타ë°ì´í„°ë¥¼ 삭제할 수 ì—†ìŒ" msgid "Unable to remove package." msgstr "패키지를 제거할 수 없습니다." msgid "Unable to retrieve availability zones." msgstr "가용 êµ¬ì—­ì„ ê²€ìƒ‰í•  수 없습니다." msgid "Unable to retrieve deployment history." msgstr "ë°°í¬ ì´ë ¥ì„ 검색할 수 없습니다." msgid "Unable to retrieve details for service" msgstr "서비스 세부 ì‚¬í•­ì„ ê²€ìƒ‰í•  수 ì—†ìŒ" msgid "Unable to retrieve list of deployments" msgstr "ë°°í¬ ëª©ë¡ì„ 검색할 수 ì—†ìŒ" msgid "Unable to retrieve list of images" msgstr "ì´ë¯¸ì§€ 목ë¡ì„ 검색할 수 ì—†ìŒ" msgid "" "Unable to retrieve list of services. This environment is deploying or " "already deployed by other user." msgstr "" "서비스 목ë¡ì„ 검색할 수 없습니다. 다른 사용ìžê°€ ì´ í™˜ê²½ì„ ë°°í¬ ì¤‘ì´ê±°ë‚˜ ì´ë¯¸ " "ë°°í¬í–ˆìŠµë‹ˆë‹¤." msgid "Unable to retrieve package details." msgstr "패키지 세부 ì‚¬í•­ì„ ê²€ìƒ‰í•  수 없습니다." msgid "Unable to retrieve project list." msgstr "프로ì íЏ 리스트를 검색할 수 없습니다." msgid "Unable to retrieve public images." msgstr "공용 ì´ë¯¸ì§€ë¥¼ 검색할 수 없습니다." msgid "Unable to retrieve snapshot list." msgstr "스냅샷 목ë¡ì„ 검색 í•  수 없습니다." msgid "Unable to retrieve volume list." msgstr "볼륨 목ë¡ì„ 검색 í•  수 없습니다." msgid "Unavailable" msgstr "사용할 수 ì—†ìŒ" msgid "Unknown" msgstr "알 수 ì—†ìŒ" msgid "Update" msgstr "ì—…ë°ì´íЏ" msgid "Update Environment" msgid_plural "Deploy Environments" msgstr[0] "환경 ì—…ë°ì´íЏ" msgid "Update Image" msgstr "ì´ë¯¸ì§€ ì—…ë°ì´íЏ" msgid "Update Metadata" msgstr "메타ë°ì´í„° ì—…ë°ì´íЏ" msgid "Update This Environment" msgstr "ì´ í™˜ê²½ì„ ì—…ë°ì´íŠ¸í•©ë‹ˆë‹¤" msgid "Updated" msgstr "ì—…ë°ì´íЏë¨" msgid "Updated Environment" msgid_plural "Deployed Environments" msgstr[0] "ì—…ë°ì´íŠ¸ëœ í™˜ê²½" msgid "Uploading package failed. {0}" msgstr "패키지를 업로드하지 못했습니다. {0}" msgid "Used for identifying and filtering packages." msgstr "패키지 ì‹ë³„ ë° í•„í„°ë§ì— 사용ë©ë‹ˆë‹¤." msgid "Validation Error occurred" msgstr "í™•ì¸ ì˜¤ë¥˜ ë°œìƒ" msgid "Version" msgstr "버전" msgid "Version of the package (optional)." msgstr "패키지 버전(ì„ íƒ ì‚¬í•­)입니다." msgid "You are not allowed to change this properties of the package" msgstr "íŒ¨í‚¤ì§€ì˜ ì´ íŠ¹ì„±ì„ ë³€ê²½í•  수 없습니다." msgid "You are not allowed to delete this package" msgstr "ì´ íŒ¨í‚¤ì§€ë¥¼ 삭제할 수 ì—†ìŒ" msgid "You are not allowed to make packages public." msgstr "패키지를 공개 í•  수 없습니다." msgid "You are not allowed to perform this operation" msgstr "ì´ ìž‘ì—…ì„ ìˆ˜í–‰í•  수 없습니다." msgid "" "You'll have to configure each package installed from this bundle separately." msgstr "ì´ ë²ˆë“¤ì—서 ì„¤ì¹˜ëœ ê° íŒ¨í‚¤ì§€ë¥¼ 개별ì ìœ¼ë¡œ 구성해야 합니다." msgid "{0}{1} don't match" msgstr "{0}{1}ì´(ê°€) ì¼ì¹˜í•˜ì§€ 않ìŒ" murano-dashboard-5.0.0/muranodashboard/locale/ko_KR/LC_MESSAGES/djangojs.po0000666000175100017510000000441713245511125026336 0ustar zuulzuul00000000000000# Andreas Jaeger , 2016. #zanata # Eunseop Shin , 2016. #zanata # Ian Y. Choi , 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 \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 "암호는 최소 다ìŒê³¼ 같아야 합니다" murano-dashboard-5.0.0/muranodashboard/locale/id/0000775000175100017510000000000013245511556022001 5ustar zuulzuul00000000000000murano-dashboard-5.0.0/muranodashboard/locale/id/LC_MESSAGES/0000775000175100017510000000000013245511556023566 5ustar zuulzuul00000000000000murano-dashboard-5.0.0/muranodashboard/locale/id/LC_MESSAGES/django.po0000666000175100017510000007740713245511125025401 0ustar zuulzuul00000000000000# suhartono , 2016. #zanata # suhartono , 2017. #zanata # suhartono , 2018. #zanata msgid "" msgstr "" "Project-Id-Version: murano-dashboard VERSION\n" "Report-Msgid-Bugs-To: https://bugs.launchpad.net/openstack-i18n/\n" "POT-Creation-Date: 2018-01-18 05:49+0000\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "PO-Revision-Date: 2018-01-18 01:38+0000\n" "Last-Translator: suhartono \n" "Language-Team: Indonesian\n" "Language: id\n" "X-Generator: Zanata 3.9.6\n" "Plural-Forms: nplurals=1; plural=0\n" #, python-format msgid "" "\n" " %(used)s + %(other_used)s of %(quota)s MB used\n" " " msgstr "" "\n" " %(used)s + %(other_used)s dari %(quota)s MB used\n" " " #, python-format msgid "" "\n" " %(used)s + %(other_used)s of %(quota)s used\n" " " msgstr "" "\n" " %(used)s + %(other_used)s dari %(quota)s used\n" " " #, python-format msgid "%s: random subnet" msgstr "%s: random subnet" msgid "-" msgstr "-" msgid "80 characters max." msgstr "80 karakter max." msgid "A local zip file to upload" msgstr "Sebuah file zip lokal untuk meng-upload" msgid "Abandon Environment" msgid_plural "Abandon Environments" msgstr[0] "Abandon Environment" msgid "Abandoned Environment" msgid_plural "Abandoned Environments" msgstr[0] "Abandoned Environment" msgid "Active" msgstr "Active (aktif)" msgid "Add" msgstr "Add (timbah)" msgid "Add Application" msgstr "Add Application (tambahkan aplikasi)" msgid "Add Application Category" msgstr "Add Application Category (timbah kategori aplikasi)" msgid "Add Category" msgstr "Add Category (timbah kategori)" msgid "Add Component" msgstr "Add Component (tambahkan komponen)" msgid "Add Murano Metadata" msgstr "Menambahkan metadata Murano" msgid "Add New" msgstr "Add New (tambah baru)" msgid "Add new category to the application catalog." msgstr "Menambahkan kategori baru untuk katalog aplikasi." msgid "Add to Env" msgstr "Add to Env (Tambahkan ke Env)" msgid "Adding application to an environment failed." msgstr "Menambahkan aplikasi untuk lingkungan gagal." msgid "All" msgstr "All (semua)" msgid "Allows adding additional information about a package." msgstr "Mengizinkan menambahkan informasi tambahan tentang paket." msgid "" "Allows to hide a package from the catalog. (Applies to package dependencies)" msgstr "" "Mengizinkan untuk menyembunyikan paket dari katalog. (Berlaku untuk paket " "dependensi)" msgid "" "An environment is a collection of applications that are meant to operate " "under similar conditions." msgstr "" "Lingkungan adalah kumpulan aplikasi yang dimaksudkan untuk beroperasi di " "bawah kondisi yang sama." msgid "An external http/https URL to load the bundle from." msgstr "URL http/https eksternal untuk memuat bundel dari." msgid "An external http/https URL to load the package from." msgstr "URLhttp/https eksternal untuk memuat paket dari." msgid "App Catalog" msgstr "App Catalog" msgid "App Category:" msgstr "App Category:" msgid "App category" msgstr "Kategori aplikasi (App)" msgid "Application Categories" msgstr "Application Categories (kategori aplikasi)" msgid "Application Category" msgstr "Application Category (categori aplikasi)" msgid "Application Details" msgstr "Application Details (rincian aplikasi)" msgid "Application Package" msgstr "Application Package (paket aplikasi)" msgid "Application default security group" msgstr "Grup keamanan default aplikasi" msgid "Application Components" msgstr "Application Components" msgid "Applications" msgstr "Applications (aplikasi)" msgid "Author" msgstr "Author (penulis)" msgid "Auto" msgstr "Auto" msgid "Back" msgstr "Back (kembali)" msgid "Browse" msgstr "Browse (melihat-lihat)" msgid "Browse Local" msgstr "Browse Local (browse lokal)" msgid "Bundle Name" msgstr "Bundle Name (nama bundle)" msgid "Bundle URL" msgstr "Bundle URL (URL bundle)" msgid "Bundle creation failed.Reason: Can't find Bundle name from repository." msgstr "" "Pembuatan bundle failed.Reason: Tidak dapat menemukan nama bundle dari " "repositori." msgid "Bundle creation failed.Reason: {0}" msgstr "Pembuatan bundle failed.Reason: {0}" msgid "Bundle successfully imported." msgstr "Bundel berhasil diimpor." msgid "Bundle's full name." msgstr "Nama lengkap bundel ini." msgid "Can not get logo for {0}." msgstr "Can not get logo for {0}." msgid "Can not get supplier logo for {0}." msgstr "Can not get supplier logo for {0}." msgid "Cancel" msgstr "Cancel (membatalkan)" msgid "Categories" msgstr "Categories (kategori)" msgid "Category Name" msgstr "Category Name (nama kategori)" msgid "Category {0} created." msgstr "Kategori {0} dibuat." msgid "Check Keystone configuration of murano-api server." msgstr "Periksa konfigurasi server murano-api Keystone." msgid "Choose a Zip archive to upload into the catalog." msgstr "Pilih arsip Zip untuk meng-upload ke katalog." msgid "Choose a name for the environment" msgstr "Pilih nama untuk lingkungan" msgctxt "Package requirements" msgid "Classes definition folder" msgstr "Folder definisi kelas" msgid "Click to create new environment" msgstr "Klik untuk membuat lingkungan baru" msgid "Completed with warnings" msgstr "Selesai dengan peringatan" msgid "Component" msgstr "Component (komponen)" msgid "Component Details" msgstr "Component Details (rincian komponen)" msgid "Component List" msgstr "Daftar komponen" msgid "Component Logs" msgstr "Component Logs (jejak rekam komponen)" msgid "Components" msgstr "Components (komponen)" msgid "Configuration" msgstr "Configuration (konfigurasi)" msgid "Configure Application" msgstr "Configure Application (aplikasi konfigurasi)" msgid "Confirm password" msgstr "Konfirmasi kata sandi" msgid "Could not retrieve latest status for the {0} environment" msgstr "tidak bisa mengambil status terbaru untuk lingkungan {0}" msgid "" "Couldn't find any apps, required for this field.\n" "Tried: {fqns}" msgstr "" "Tidak bisa menemukan aplikasi, diperlukan untuk field ini.\n" "Tried: {fqns}" msgid "" "Couldn't initialise glance v1 client, therefore could not make the following " "images public: {0}" msgstr "" "Tidak bisa menginisialisasi glance v1 client, karena itu tidak bisa membuat " "publik image berikut: {0}" msgid "Couldn't update environment. Reason: This name is already taken." msgstr "Tidak bisa memperbarui lingkungan. Alasan: Nama ini sudah diambil." msgid "Couldn't update package {0} parameters. Error: {1}" msgstr "Tidak dapat memperbarui paket {0} parameter. Kesalahan: {1}" msgid "Create" msgstr "Create (membuat)" msgid "Create Env" msgstr "Buat Env" msgid "Create Environment" msgstr "Create Environment (buat lingkungan)" msgid "Create New" msgstr "Create New (membuat baru)" msgid "Create a title for an image." msgstr "Buat judul untuk image." msgid "Created" msgstr "Created" msgid "Custom Type" msgstr "Custom Type (tipe kustom)" msgid "" "Default network is either not specified for this project, or specified " "incorrectly, please contact administrator." msgstr "" "Jaringan default tidak ditentukan untuk proyek ini, atau tidak ditentukan " "dengan benar, hubungi administrator." msgid "" "Defines whether or not a package can be used by other tenants. (Applies to " "package dependencies)" msgstr "" "Mendefinisikan apakah sebuah paket dapat digunakan atau tidak oleh penyewa " "(tenant) lainnya. (Berlaku untuk paket dependensi)" msgid "Delete Category" msgid_plural "Delete Categories" msgstr[0] "Delete Category" msgid "Delete Component" msgid_plural "Delete Components" msgstr[0] "Delete Component" msgid "Delete Environment" msgid_plural "Delete Environments" msgstr[0] "Delete Environment" msgid "Delete Metadata" msgid_plural "Delete Metadata" msgstr[0] "Delete Metadata" msgid "Delete Package" msgid_plural "Delete Packages" msgstr[0] "Delete Package" msgid "Delete failure" msgstr "Hapus kegagalan" msgid "Deleted Category" msgid_plural "Deleted Categories" msgstr[0] "Deleted Category" msgid "Deleted Metadata" msgid_plural "Deleted Metadata" msgstr[0] "Deleted Metadata" msgid "Deleted Package" msgid_plural "Deleted Packages" msgstr[0] "Deleted Package" msgid "Deleting" msgstr "Deleting (penghapusan)" msgid "Deploy Environment" msgid_plural "Deploy Environments" msgstr[0] "Deploy Environment" msgid "Deploy This Environment" msgstr "Kerahkan di lingkungan ini" msgid "Deploy failure" msgstr "Pengerahan gagal" msgid "Deploy started" msgstr "Mengerahkan mulai" msgid "Deployed Components" msgstr "Deployed Components (komponen yang dikerahkan)" msgid "Deploying" msgstr "Deploying (pengerahan)" msgid "Deployment Details" msgstr "Deployment Details (rincian pengerahan)" msgid "Deployment History" msgstr "Deployment History (sejarah pengerahan)" msgid "Deployment Logs" msgstr "Deployment Logs (jejak rekam pengerahan)" #, python-format msgid "Deployment with id %s doesn't exist anymore" msgstr "Pengerahan dengan id %s tidak ada lagi" msgid "Deployment with id foo_deployment_id doesn't exist anymore" msgstr "Pengerahan dengan id foo_deployment_id tidak ada lagi " msgid "Deployments" msgstr "Deployments (pengerahan)" msgid "Description" msgstr "Description (deskripsi)" msgid "Details" msgstr "Details (rincian)" msgid "Download Package" msgstr "Download Package (download paket)" msgid "Drop Components here" msgstr "Jatuhkan komponen disini" msgid "Enabled" msgstr "Enabled (aktif)" msgid "" "Enter a complex password with at least one letter, one number and one " "special character" msgstr "" "Masukkan password yang kompleks dengan setidaknya satu huruf, satu nomor dan " "satu karakter khusus" #, python-format msgid "Enter a dict with choices and values. Got %(value)s." msgstr "Masukkan dict dengan pilihan dan nilai. Memperoleh %(value)s." msgid "Enter a password" msgstr "Masukan kata sandi" msgid "Enter an image type supported by Murano." msgstr "Masukkan tipe image yang didukung oleh Murano." msgid "Environment" msgstr "Environment (lingkungan)" msgid "Environment Default Network" msgstr "Environment Default Network (jaringan default lingkungan)" msgid "Environment Deployment History" msgstr "Environment Deployment History" msgid "Environment Name" msgstr "Environment Name (nama lingkungan)" msgid "Environment name must contain at least one non-white space symbol." msgstr "" "Nama lingkungan harus mengandung setidaknya satu simbol non-white space." #, python-format msgid "Environment with id %s doesn't exist anymore" msgstr "Lingkungan dengan id %s tidak ada lagi" msgid "Environment with specified name already exists" msgstr "Lingkungan dengan nama yang ditentukan sudah ada" msgid "Environments" msgstr "Environments (lingkungan)" #, python-format msgid "" "Error fetching the environment. The page may be rendered incorrectly. " "Reason: %s" msgstr "Kesalahan mengambil environment Halaman itu mungkin salah. Reason: %s" msgid "Error test_error_message occurred while installing package bar" msgstr "Kesalahan test_error_message terjadi pada saat menginstal paket bar" msgid "Error {0} occurred while installing images for {1}" msgstr "Kesalahan {0} terjadi ketika menginstal image untuk {1}" msgid "Error {0} occurred while installing package {1}" msgstr "Kesalahan {0} terjadi ketika menginstal paket {1}" msgid "Error {0} occurred while parsing package {1}" msgstr "Kesalahan {0} terjadi saat mengurai paket {1}" msgid "Error {0} occurred while setting image {1}, {2} public" msgstr "Kesalahan {0} terjadi saat pengaturan image {1}, {2} publik" msgctxt "Package requirements" msgid "Execution plans folder" msgstr "Folder rencana eksekusi" msgid "FQN" msgstr "FQN" msgid "Failed" msgstr "Failed (gagal)" msgid "Failed to create environment" msgstr "Gagal membuat lingkungan" msgid "Failed to get list of flavors." msgstr "Gagal mendapatkan daftar flavor." msgid "Failed to modify the package. {0}" msgstr "Gagal untuk memodifikasi paket. {0}" msgid "File" msgstr "File" msgid "Filter" msgstr "Filter (saringan)" msgid "Find in a selected category" msgstr "Cari dalam kategori yang dipilih" 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 "" "Simbol pertama harus huruf latin atau garis bawah. Simbol berikutnya dapat " "huruf latin, angka, garis bawah, tanda, angka tanda atau tanda dolar" msgid "Foo" msgstr "Foo (contoh)" msgid "Fully qualified package name." msgstr "Nama paket berkualifikasi lengkap." #, python-format msgid "" "Go to Packages , click 'Import Package' " "and select Repository as Package Source." msgstr "" "Buka Packages , klik 'Import Package' " "dan pilih Repository sebagai Package Source." #, python-format msgid "" "Go to Packages, click 'Import Package' " "and select Repository as Package Source." msgstr "" "Buka Packages, klik 'Import Package' dan " "pilih Repository sebagai Package Source." msgid "HTTP/HTTPS URL of the bundle file." msgstr "URL HTTP/HTTPS dari file bundel." msgid "HTTP/HTTPS URL of the package file." msgstr "URL HTTP/HTTPS dari file paket." msgid "Heat Orchestration stack name" msgstr "Nama stack Heat Orchestration" msgid "Heat Orchestration stack%(forloop.counter)s name" msgstr "Heat Orchestration stack%(forloop.counter)s nama" 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 "" "Jika paket tergantung pada paket lain dan / atau membutuhkan glance image " "tertentu, mereka akan diinstal dengan mereka dari repositori 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 "" "Jika paket tergantung pada paket lain dan / atau membutuhkan image glance " "tertentu, mereka akan diinstal dengan itu dari repositori murano." msgid "Image" msgstr "Image (image)" msgid "Image Title" msgstr "Image Title (judul gambar)" msgid "Image Type" msgstr "Image Type (tipe image)" msgid "Image successfully marked" msgstr "Image berhasil ditandai" msgid "Images" msgstr "Images (image)" msgid "Import Bundle" msgstr "Import Bundle (import bundle)" msgid "Import Package" msgstr "Import Package (import paket)" msgid "Importing package {0} failed. Reason: {1}" msgstr "Mengimport paket {0} failed.Reason: {1}" msgid "Info" msgstr "Info (informasi)" msgid "Instance name" msgstr "Nama instance" msgid "Instance%(forloop.counter)s name" msgstr "Nama instance%(forloop.counter)s" msgid "Invalid metadata for image: {0}" msgstr "metadata tidak valid untuk image: {0}" msgid "Invalid murano image metadata" msgstr "metadata image murano tidak valid" msgid "Invalid value of 'murano_nets' option" msgstr "Nilai yang tidak valid dari opsi 'murano_nets'" msgid "It is forbidden to upload files larger than {0} MB." msgstr "Hal ini dilarang untuk meng-upload file yang lebih besar dari {0} MB." msgid "KeyWord" msgstr "KeyWord (kata kunci)" msgid "Last operation" msgstr "Operasi terakhir" msgid "Latest Deployment Log" msgstr "Latest Deployment Log (catatan pengerahan terbaru)" msgid "License" msgstr "License" msgid "Logs" msgstr "Logs (catatan)" msgid "Logs (Created, Message)" msgstr "Logs (Created, Message)" msgid "Manage" msgstr "Manage (kelola)" msgid "Manage Components" msgstr "Manage Components (mengelola komponen)" msgctxt "Package requirements" msgid "Manifest file" msgstr "file manifest" msgid "Mark Image" msgstr "Mark Image (menandai image)" msgid "" "Mark an image with Murano specific metadata to be added to the selected " "image." msgstr "" "Menandai image dengan Murano metadata spesifik yang akan ditambahkan ke " "image yang dipilih." msgid "Marked Images" msgstr "Image ditandai" msgid "Modify Package" msgstr "Modify Package (memodifikasi paket)" msgid "Modifying package failed" msgstr "Memodifikasi paket gagal" msgid "NO ENVIRONMENTS" msgstr "NO ENVIRONMENTS (tidak ada lingkungan)" msgid "Name" msgstr "Name (nama)" msgid "Name of the bundle." msgstr "Nama bundle" #, python-format msgid "Network of '%s'" msgstr "Jaringan of '%s'" msgid "Next" msgstr "Next (berikutnya)" msgid "Next Page" msgstr "Next Page (halaman selanjutnya)" msgid "No availability zones available" msgstr "Tidak ada zona ketersediaan tersedia" msgid "No categories available" msgstr "Kategori tidak tersedia" msgid "No components" msgstr "Tidak ada komponen" msgid "No images available" msgstr "Tidak ada image yang tersedia" msgid "No keypair" msgstr "Tidak ada pasangan kunci" msgid "No license" msgstr "Tidak ada lisensi" msgid "No recent activity to report at this time." msgstr "Tidak ada aktivitas terbaru melaporkan saat ini." msgid "No requirements" msgstr "Tidak ada persyaratan" msgid "No volumes available" msgstr "Tidak ada volume yang tersedia" msgid "None" msgstr "None (tak satupun)" msgid "Not in domain" msgstr "Tidak dalam domain" msgid "Note" msgstr "Note (catatan)" msgid "Number of Instances" msgstr "Jumlah Instance" msgid "Number of VCPUs" msgstr "Jumlah VCPU" msgid "" "OpenStack Networking (Neutron) is not available in current environment. " "Custom Network Settings cannot be applied" msgstr "" "OpenStack Networking (Neutron) tidak tersedia di lingkungan saat ini. Custom " "Network Setting tidak dapat diterapkan" msgid "Operation is forbidden by murano-api server." msgstr "Operasi dilarang oleh server murano-api." msgid "Optional" msgstr "Optional (opsional)" msgid "Overview" msgstr "Overview (ikhtisar)" msgid "Package Bundle Source" msgstr "Package Bundle Source (sumber bundle paket)" msgid "Package Count" msgstr "Package Count (jumlah paket)" msgid "Package Details" msgstr "Package Details (rincian paket)" msgid "Package Name" msgstr "Package Name (nama paket)" msgid "Package Source" msgstr "Package Source (sumber paket)" msgid "Package Tags" msgstr "Package Tags (tags paket)" msgid "Package URL" msgstr "Package URL (URL paket)" msgid "Package Version" msgstr "Package Version (versi paket)" msgid "" "Package creation failed.Reason: Can't find Package name from repository." msgstr "" "Pembuatan paket failed.Reason: Tidak dapat menemukan nama paket dari " "repositori." msgid "Package creation failed.Reason: {0}" msgstr "Pembuatan paket failed.Reason: {0}" msgid "Package foo uploaded" msgstr "Paket foo diunggah" msgid "Package modified." msgstr "Paket dimodifikasi." msgid "Package name in the repository, usually a fully qualified name" msgstr "Nama paket di repositori, biasanya nama yang memenuhi syarat" msgid "Package or Class with the same name is already made public" msgstr "Paket atau kelas dengan nama yang sama sudah dibuat publik" msgid "Package parameters successfully updated." msgstr "Parameter paket berhasil diperbarui." msgid "Package version" msgstr "Versi paket" msgid "Package with id foo_package_id is not found" msgstr "Paket dengan id foo_package_id tidak ditemukan" msgid "Package with id {0} is not found" msgstr "Paket dengan id {0} tidak ditemukan" msgid "Package with specified name already exists" msgstr "Paket dengan nama yang ditentukan sudah ada" msgid "Package {0} already registered." msgstr "Paket {0} sudah terdaftar." msgid "Package {0} upload failed. {1}" msgstr "Paket {0} gagal mengunggah. {1}" msgid "Package {0} uploaded" msgstr "Paket {0} diunggah" msgid "Packages" msgstr "Packages (paket)" msgid "Packages should contain:" msgstr "Paket harus berisi:" msgid "Please confirm your password" msgstr "Silahkan konfirmasi kata sandi" msgid "Please supply a bundle name" msgstr "Silahkan menyediakan nama bundel" msgid "Please supply a bundle url" msgstr "Silahkan menyediakan url bundel" msgid "Please supply a package file" msgstr "Silahkan menyediakan file paket" msgid "Please supply a package name" msgstr "Silahkan menyediakan nama paket" msgid "Please supply a package url" msgstr "Silahkan menyediakan url paket" msgid "Previous Page" msgstr "Previous Page (halaman sebelumnya)" msgid "Provide comma-separated list of words, associated with the package" msgstr "" "Berikan daftar kata-kata yang dipisahkan koma, yang terkait dengan paket" msgid "Provide desired name for a new category" msgstr "Memberikan nama yang diinginkan untuk kategori baru" msgid "Public" msgstr "Public (publik)" msgid "Quick Deploy" msgstr "Quick Deploy (pengerahan cepat)" msgid "Ready" msgstr "Ready (siap)" msgid "Ready to configure" msgstr "Siap untuk mengkonfigurasi" msgid "Ready to deploy" msgstr "Siap untuk mengerahkan" msgid "Recent Activity" msgstr "Recent Activity (aktivitas terbaru)" msgid "Repository" msgstr "Repository (repositori)" msgid "Requested object is not found on murano server." msgstr "Objek yang diminta tidak ditemukan di server murano." msgid "Requested operation conflicts with an existing object." msgstr "Operasi yang dimintan konflik dengan objek yang ada." msgid "Requirements" msgstr "Requirements (persyaratan)" msgid "Retype your password" msgstr "Ketik ulang kata sandi Anda" msgid "Running" msgstr "Menjalankan" msgid "Running with errors" msgstr "Berjalan dengan kesalahan" msgid "Running with warnings" msgstr "Berjalan dengan peringatan" msgid "Select Application" msgstr "Pilih aplikasi" msgid "Select Image" msgstr "Pilih image" #, python-format msgid "Select a valid choice. %(value)s is not one of the available choices." msgstr "" "Pilih pilihan yang valid. %(value)s bukan salah satu pilihan yang tersedia." msgid "Select an image registered in Glance Image Services." msgstr "Pilih image yang terdaftar di layanan Glance Image." msgid "" "Select an image type supported by Murano. Choose 'Custom type' to enter type " "manually." msgstr "" "Pilih tipe image yang didukung oleh Murano. Pilih 'Custom type' untuk " "memasukkan tipe secara manual." msgid "Select one or more categories for a package." msgstr "Pilih satu atau lebih kategori untuk paket." msgid "Select volume" msgstr "Pilih volume" msgid "Services (Name, Type)" msgstr "Services (Name, Type)" msgid "Set up for identifying a package." msgstr "Diatur untuk mengidentifikasi paket." msgid "Show Details" msgstr "Tampilkan rincian" msgid "Something went wrong during package downloading" msgstr "Ada yang salah selama men-download paket" msgid "Sorry, this environment doesn't exist anymore" msgstr "Maaf, lingkungan ini tidak ada lagi" msgid "" "Sorry, you can't add application right now. The environment is deploying." msgstr "" "Maaf, Anda tidak dapat menambahkan aplikasi sekarang. Lingkungan ini sedang " "mengerahkan." msgid "Sorry, you can't delete service right now" msgstr "Maaf, Anda tidak dapat menghapus layanan sekarang" msgid "Specified title already in use. Please choose another one." msgstr "Judul yang ditentukan sudah digunakan. Silakan pilih yang lain." msgid "Specifying a category helps to filter applications in the catalog" msgstr "Menentukan kategori membantu untuk menyaring aplikasi di katalog" msgid "Started Deleting Component" msgid_plural "Started Deleting Components" msgstr[0] "Started Deleting Component" msgid "Started Deleting Environment" msgid_plural "Started Deleting Environments" msgstr[0] "Started Deleting Environment" msgid "Started deploying Environment" msgid_plural "Started deploying Environments" msgstr[0] "Started deploying Environment" msgid "Status" msgstr "Status (status)" msgid "Step {0}" msgstr "Step {0}" msgid "Successful" msgstr "Successful (sukses)" msgid "Tags" msgstr "Tags" msgid "Tenant Name" msgstr "Tenant Name" msgid "The '{0}' application successfully added to environment." msgstr "Aplikasi '{0}' berhasil ditambahkan ke lingkungan." 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 "" "Aplikasi VM dalam lingkungan ini akan bergabung dengan jaringan ini secara " "default, kecuali dikonfigurasi secara individual. Pilih 'Create New' akan " "menghasilkan Jaringan baru dengan Subnet memiliki rentang IP yang " "dialokasikan diantara jaringan yang tersedia untuk Murano Router secara " "default dari proyek ini" #, python-format msgid "" "The bundle is going to be installed from %(murano_repo_url)s repository." msgstr "" "Bundel akan diinstal dari repositori %(murano_repo_url)s." msgid "The environment name field cannot be empty." msgstr "Kolom nama lingkungan tidak boleh kosong." #, python-format msgid "" "The package is going to be imported from %(murano_repo_url)s repository." msgstr "" "Paket ini akan diimpor dari repositori %(murano_repo_url)s." msgid "" "The password must contain at least one letter, " "one number and one special character" msgstr "" "Password harus mengandung setidaknya satu huruf, satu nomor dan satu " "karakter khusus" msgid "The request data is not acceptable by the server" msgstr "Permintaan data tidak diterima oleh server" msgid "There are no applications in the catalog. You can import apps from" msgstr "Tidak ada aplikasi di katalog. Anda dapat mengimpor aplikasi dari" #, python-format msgid "" "There are no applications in the catalog. You can import apps from %(display_repo_url)s." msgstr "" "Tidak ada aplikasi di katalog. Anda dapat mengimpor aplikasi dari %(display_repo_url)s." msgid "There are no applications matching your criteria." msgstr "Tidak ada aplikasi yang cocok dengan kriteria Anda." msgid "There was an error communicating with server" msgstr "Terjadi kesalahan saat berkomunikasi dengan server" msgid "There was an error initialising this field." msgstr "Terjadi kesalahan inisialisasi field ini." msgid "" "This Application requires encryption, please contact your administrator to " "configure this." msgstr "" "Aplikasi ini memerlukan enkripsi, hubungi administrator Anda untuk " "mengkonfigurasi ini." msgid "" "This action cannot be undone. Any resources created by this environment will " "have to be released manually." msgstr "" "Tindakan ini tidak bisa dibatalkan. Sumber daya yang diciptakan oleh " "lingkungan ini harus dirilis secara manual." msgid "Time Finished" msgstr "Waktu selesai" msgid "Time Started" msgstr "Waktu dimulai" msgid "Time updated" msgstr "Waktu diperbarui" msgid "Title" msgstr "Title (judul)" msgid "Toggle Active" msgid_plural "Toggle Active" msgstr[0] "Toggle Active" msgid "Toggle Enabled" msgstr "Toggle diaktifkan" msgid "Toggle Public" msgid_plural "Toggle Public" msgstr[0] "Toggle Public" msgid "Toggled Active" msgid_plural "Toggled Active" msgstr[0] "Toggled Active" msgid "Toggled Public" msgid_plural "Toggled Public" msgstr[0] "Toggled Public" msgid "Topology" msgstr "Topology (topologi)" msgid "Total RAM" msgstr "RAM total" msgid "" "Trying to add {0} image to glance. Image will be ready for deployment after " "successful upload" msgstr "" "Coba untuk menambahkan image {0} untuk glance. Image akan siap untuk " "pengerahan setelah upload sukses" msgid "" "Trying to add {0}, {1} image to glance. Image will be ready for deployment " "after successful upload" msgstr "" "Coba untuk menambahkan image {0}, {1} untuk glance. Image akan siap untuk " "pengerahan setelah upload sukses" msgid "Type" msgstr "Type (tipe)" msgctxt "Package requirements" msgid "UI definition folder" msgstr "Folder definisi UI" msgid "UNKNOWN" msgstr "UNKNOWN (tidak diketahui)" msgid "URL" msgstr "URL" msgid "Unable to abandon an environment {0} due to: {1}" msgstr "Tidak dapat meninggalkan lingkungan {0} disebabkan oleh: {1}" msgid "Unable to communicate to glare-api server." msgstr "Tidak dapat berkomunikasi dengan server glare-api." msgid "Unable to communicate to murano-api server." msgstr "Tidak dapat berkomunikasi ke server murano-api." msgid "Unable to create environment {0} due to: {1}" msgstr "Tidak dapat menciptakan lingkungan {0} disebabkan oleh: {1}" msgid "Unable to delete category" msgstr "Tidak dapat menghapus kategori" msgid "Unable to delete environment {0} due to: {1}" msgstr "Tidak dapat menghapus lingkungant {0} disebabkan oleh: {1}" msgid "Unable to delete package in murano-api server" msgstr "Tidak dapat menghapus paket di server murano-api" msgid "Unable to deploy. Try again later" msgstr "Tidak dapat mengerahkan. Coba lagi nanti" msgid "Unable to download package." msgstr "Tidak dapat men-download paket." msgid "Unable to get list of categories" msgstr "Tidak dapat mendapatkan daftar kategori" msgid "Unable to mark image" msgstr "Tidak dapat menandai image" msgid "Unable to modify package" msgstr "Tidak dapat memodifikasi paket" msgid "Unable to remove metadata" msgstr "Tidak dapat menghapus metadata" msgid "Unable to remove package." msgstr "Tidak dapat menghapus paket." msgid "Unable to retrieve availability zones." msgstr "Tidak dapat mengambil zona ketersediaan." msgid "Unable to retrieve deployment history." msgstr "Tidak dapat mengambil riwayat penerapan" msgid "Unable to retrieve details for service" msgstr "Tidak dapat mengambil rincian untuk layanan" msgid "Unable to retrieve list of deployments" msgstr "Tidak dapat mengambil daftar pengerahan" msgid "Unable to retrieve list of images" msgstr "Tidak dapat mengambil daftar image" msgid "" "Unable to retrieve list of services. This environment is deploying or " "already deployed by other user." msgstr "" "Tidak dapat mengambil daftar layanan. Lingkungan ini sedang mengerahkan atau " "sudah digunakan oleh pengguna lain." msgid "Unable to retrieve package details." msgstr "Tidak dapat mengambil rincian paket." msgid "Unable to retrieve project list." msgstr "Tidak dapat mengambil daftar proyek." msgid "Unable to retrieve public images." msgstr "Tidak dapat mengambil image publik." msgid "Unable to retrieve snapshot list." msgstr "Tidak dapat mengambil daftar snapshot." msgid "Unable to retrieve volume list." msgstr "Tidak dapat mengambil daftar volume." msgid "Unavailable" msgstr "Unavailable (tidak tersedia)" msgid "Unknown" msgstr "Unknown (tidak diketahui)" msgid "Update" msgstr "Update (memperbarui)" msgid "Update Environment" msgid_plural "Deploy Environments" msgstr[0] "Update Environment" msgid "Update Image" msgstr "Perbarui image" msgid "Update Metadata" msgstr "Update Metadata (perbarui metadata)" msgid "Update This Environment" msgstr "Perbarui lingkungan ini" msgid "Updated" msgstr "Updated" msgid "Updated Environment" msgid_plural "Deployed Environments" msgstr[0] "Updated Environment" msgid "Uploading package failed. {0}" msgstr "Upload paket gagal. {0}" msgid "Used for identifying and filtering packages." msgstr "Digunakan untuk mengidentifikasi dan paket penyaringan." msgid "Validation Error occurred" msgstr "Kesalahan validasi terjadi" msgid "Version" msgstr "Version" msgid "Version of the package (optional)." msgstr "Versi paket (opsional)." msgid "You are not allowed to change this properties of the package" msgstr "Anda tidak diperbolehkan untuk mengubah sifat dari paket" msgid "You are not allowed to delete this package" msgstr "Anda tidak diizinkan untuk menghapus paket ini" msgid "You are not allowed to make packages public." msgstr "You are not allowed to make packages public." msgid "You are not allowed to perform this operation" msgstr "Anda tidak diizinkan untuk melakukan operasi ini" msgid "" "You'll have to configure each package installed from this bundle separately." msgstr "" "Anda harus mengkonfigurasi setiap paket yang diinstal dari bundel ini secara " "terpisah." msgid "{0}{1} don't match" msgstr "{0}{1} don't match" murano-dashboard-5.0.0/muranodashboard/locale/id/LC_MESSAGES/djangojs.po0000666000175100017510000000411713245511125025722 0ustar zuulzuul00000000000000# suhartono , 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 \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" murano-dashboard-5.0.0/muranodashboard/locale/en_GB/0000775000175100017510000000000013245511556022357 5ustar zuulzuul00000000000000murano-dashboard-5.0.0/muranodashboard/locale/en_GB/LC_MESSAGES/0000775000175100017510000000000013245511556024144 5ustar zuulzuul00000000000000murano-dashboard-5.0.0/muranodashboard/locale/en_GB/LC_MESSAGES/django.po0000666000175100017510000007475513245511125025762 0ustar zuulzuul00000000000000# Andi Chandler , 2016. #zanata # Andi Chandler , 2017. #zanata # Andi Chandler , 2018. #zanata msgid "" msgstr "" "Project-Id-Version: murano-dashboard VERSION\n" "Report-Msgid-Bugs-To: https://bugs.launchpad.net/openstack-i18n/\n" "POT-Creation-Date: 2018-01-18 05:49+0000\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "PO-Revision-Date: 2018-01-18 05:03+0000\n" "Last-Translator: Andi Chandler \n" "Language-Team: English (United Kingdom)\n" "Language: en-GB\n" "X-Generator: Zanata 3.9.6\n" "Plural-Forms: nplurals=2; plural=(n != 1)\n" #, python-format msgid "" "\n" " %(used)s + %(other_used)s of %(quota)s MB used\n" " " msgstr "" "\n" " %(used)s + %(other_used)s of %(quota)s MB used\n" " " #, python-format msgid "" "\n" " %(used)s + %(other_used)s of %(quota)s used\n" " " msgstr "" "\n" " %(used)s + %(other_used)s of %(quota)s used\n" " " #, python-format msgid "%s: random subnet" msgstr "%s: random subnet" msgid "-" msgstr "-" msgid "80 characters max." msgstr "80 characters max." msgid "A local zip file to upload" msgstr "A local zip file to upload" msgid "Abandon Environment" msgid_plural "Abandon Environments" msgstr[0] "Abandon Environment" msgstr[1] "Abandon Environments" msgid "Abandoned Environment" msgid_plural "Abandoned Environments" msgstr[0] "Abandoned Environment" msgstr[1] "Abandoned Environments" msgid "Active" msgstr "Active" msgid "Add" msgstr "Add" msgid "Add Application" msgstr "Add Application" msgid "Add Application Category" msgstr "Add Application Category" msgid "Add Category" msgstr "Add Category" msgid "Add Component" msgstr "Add Component" msgid "Add Murano Metadata" msgstr "Add Murano Metadata" msgid "Add New" msgstr "Add New" msgid "Add new category to the application catalog." msgstr "Add new category to the application catalogue." msgid "Add to Env" msgstr "Add to Env" msgid "Adding application to an environment failed." msgstr "Adding application to an environment failed." msgid "All" msgstr "All" msgid "Allows adding additional information about a package." msgstr "Allows adding additional information about a package." msgid "" "Allows to hide a package from the catalog. (Applies to package dependencies)" msgstr "" "Allows to hide a package from the catalogue. (Applies to package " "dependencies)" msgid "" "An environment is a collection of applications that are meant to operate " "under similar conditions." msgstr "" "An environment is a collection of applications that are meant to operate " "under similar conditions." msgid "An external http/https URL to load the bundle from." msgstr "An external http/https URL to load the bundle from." msgid "An external http/https URL to load the package from." msgstr "An external http/https URL to load the package from." msgid "App Catalog" msgstr "App Catalogue" msgid "App Category:" msgstr "App Category:" msgid "App category" msgstr "App category" msgid "Application Categories" msgstr "Application Categories" msgid "Application Category" msgstr "Application Category" msgid "Application Details" msgstr "Application Details" msgid "Application Package" msgstr "Application Package" msgid "Application default security group" msgstr "Application default security group" msgid "Application Components" msgstr "Application Components" msgid "Applications" msgstr "Applications" msgid "Author" msgstr "Author" msgid "Auto" msgstr "Auto" msgid "Back" msgstr "Back" msgid "Browse" msgstr "Browse" msgid "Browse Local" msgstr "Browse Local" msgid "Bundle Name" msgstr "Bundle Name" msgid "Bundle URL" msgstr "Bundle URL" msgid "Bundle creation failed.Reason: Can't find Bundle name from repository." msgstr "Bundle creation failed.Reason: Can't find Bundle name from repository." msgid "Bundle creation failed.Reason: {0}" msgstr "Bundle creation failed.Reason: {0}" msgid "Bundle successfully imported." msgstr "Bundle successfully imported." msgid "Bundle's full name." msgstr "Bundle's full name." msgid "Can not get logo for {0}." msgstr "Can not get logo for {0}." msgid "Can not get supplier logo for {0}." msgstr "Can not get supplier logo for {0}." msgid "Cancel" msgstr "Cancel" msgid "Categories" msgstr "Categories" msgid "Category Name" msgstr "Category Name" msgid "Category {0} created." msgstr "Category {0} created." msgid "Check Keystone configuration of murano-api server." msgstr "Check Keystone configuration of murano-api server." msgid "Choose a Zip archive to upload into the catalog." msgstr "Choose a Zip archive to upload into the catalogue." msgid "Choose a name for the environment" msgstr "Choose a name for the environment" msgctxt "Package requirements" msgid "Classes definition folder" msgstr "Classes definition folder" msgid "Click to create new environment" msgstr "Click to create new environment" msgid "Completed with warnings" msgstr "Completed with warnings" msgid "Component" msgstr "Component" msgid "Component Details" msgstr "Component Details" msgid "Component List" msgstr "Component List" msgid "Component Logs" msgstr "Component Logs" msgid "Components" msgstr "Components" msgid "Configuration" msgstr "Configuration" msgid "Configure Application" msgstr "Configure Application" msgid "Confirm password" msgstr "Confirm password" msgid "Could not retrieve latest status for the {0} environment" msgstr "Could not retrieve latest status for the {0} environment" msgid "" "Couldn't find any apps, required for this field.\n" "Tried: {fqns}" msgstr "" "Couldn't find any apps, required for this field.\n" "Tried: {fqns}" msgid "" "Couldn't initialise glance v1 client, therefore could not make the following " "images public: {0}" msgstr "" "Couldn't initialise glance v1 client, therefore could not make the following " "images public: {0}" msgid "Couldn't update environment. Reason: This name is already taken." msgstr "Couldn't update environment. Reason: This name is already taken." msgid "Couldn't update package {0} parameters. Error: {1}" msgstr "Couldn't update package {0} parameters. Error: {1}" msgid "Create" msgstr "Create" msgid "Create Env" msgstr "Create Env" msgid "Create Environment" msgstr "Create Environment" msgid "Create New" msgstr "Create New" msgid "Create a title for an image." msgstr "Create a title for an image." msgid "Created" msgstr "Created" msgid "Custom Type" msgstr "Custom Type" msgid "" "Default network is either not specified for this project, or specified " "incorrectly, please contact administrator." msgstr "" "Default network is either not specified for this project, or specified " "incorrectly, please contact administrator." msgid "" "Defines whether or not a package can be used by other tenants. (Applies to " "package dependencies)" msgstr "" "Defines whether or not a package can be used by other tenants. (Applies to " "package dependencies)" msgid "Delete Category" msgid_plural "Delete Categories" msgstr[0] "Delete Category" msgstr[1] "Delete Categories" msgid "Delete Component" msgid_plural "Delete Components" msgstr[0] "Delete Component" msgstr[1] "Delete Components" msgid "Delete Environment" msgid_plural "Delete Environments" msgstr[0] "Delete Environment" msgstr[1] "Delete Environments" msgid "Delete Metadata" msgid_plural "Delete Metadata" msgstr[0] "Delete Metadata" msgstr[1] "Delete Metadata" msgid "Delete Package" msgid_plural "Delete Packages" msgstr[0] "Delete Package" msgstr[1] "Delete Packages" msgid "Delete failure" msgstr "Delete failure" msgid "Deleted Category" msgid_plural "Deleted Categories" msgstr[0] "Deleted Category" msgstr[1] "Deleted Categories" msgid "Deleted Metadata" msgid_plural "Deleted Metadata" msgstr[0] "Deleted Metadata" msgstr[1] "Deleted Metadata" msgid "Deleted Package" msgid_plural "Deleted Packages" msgstr[0] "Deleted Package" msgstr[1] "Deleted Packages" msgid "Deleting" msgstr "Deleting" msgid "Deploy Environment" msgid_plural "Deploy Environments" msgstr[0] "Deploy Environment" msgstr[1] "Deploy Environments" msgid "Deploy This Environment" msgstr "Deploy This Environment" msgid "Deploy failure" msgstr "Deploy failure" msgid "Deploy started" msgstr "Deploy started" msgid "Deployed Components" msgstr "Deployed Components" msgid "Deploying" msgstr "Deploying" msgid "Deployment Details" msgstr "Deployment Details" msgid "Deployment History" msgstr "Deployment History" msgid "Deployment Logs" msgstr "Deployment Logs" #, python-format msgid "Deployment with id %s doesn't exist anymore" msgstr "Deployment with id %s doesn't exist anymore" msgid "Deployment with id foo_deployment_id doesn't exist anymore" msgstr "Deployment with id foo_deployment_id doesn't exist any more" msgid "Deployments" msgstr "Deployments" msgid "Description" msgstr "Description" msgid "Details" msgstr "Details" msgid "Download Package" msgstr "Download Package" msgid "Drop Components here" msgstr "Drop Components here" msgid "Enabled" msgstr "Enabled" msgid "" "Enter a complex password with at least one letter, one number and one " "special character" msgstr "" "Enter a complex password with at least one letter, one number and one " "special character" #, python-format msgid "Enter a dict with choices and values. Got %(value)s." msgstr "Enter a dict with choices and values. Got %(value)s." msgid "Enter a password" msgstr "Enter a password" msgid "Enter an image type supported by Murano." msgstr "Enter an image type supported by Murano." msgid "Environment" msgstr "Environment" msgid "Environment Default Network" msgstr "Environment Default Network" msgid "Environment Deployment History" msgstr "Environment Deployment History" msgid "Environment Name" msgstr "Environment Name" msgid "Environment name must contain at least one non-white space symbol." msgstr "Environment name must contain at least one non-white space symbol." #, python-format msgid "Environment with id %s doesn't exist anymore" msgstr "Environment with id %s doesn't exist any more" msgid "Environment with specified name already exists" msgstr "Environment with specified name already exists" msgid "Environments" msgstr "Environments" #, python-format msgid "" "Error fetching the environment. The page may be rendered incorrectly. " "Reason: %s" msgstr "" "Error fetching the environment. The page may be rendered incorrectly. " "Reason: %s" msgid "Error test_error_message occurred while installing package bar" msgstr "Error test_error_message occurred while installing package bar" msgid "Error {0} occurred while installing images for {1}" msgstr "Error {0} occurred while installing images for {1}" msgid "Error {0} occurred while installing package {1}" msgstr "Error {0} occurred while installing package {1}" msgid "Error {0} occurred while parsing package {1}" msgstr "Error {0} occurred while parsing package {1}" msgid "Error {0} occurred while setting image {1}, {2} public" msgstr "Error {0} occurred while setting image {1}, {2} public" msgctxt "Package requirements" msgid "Execution plans folder" msgstr "Execution plans folder" msgid "FQN" msgstr "FQN" msgid "Failed" msgstr "Failed" msgid "Failed to create environment" msgstr "Failed to create environment" msgid "Failed to get list of flavors." msgstr "Failed to get list of flavours." msgid "Failed to modify the package. {0}" msgstr "Failed to modify the package. {0}" msgid "File" msgstr "File" msgid "Filter" msgstr "Filter" msgid "Find in a selected category" msgstr "Find in a selected category" 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 "" "First symbol should be Latin letter or underscore. Subsequent symbols can be " "Latin letter, numeric, underscore, at sign, number sign or dollar sign" msgid "Foo" msgstr "Foo" msgid "Fully qualified package name." msgstr "Fully qualified package name." #, python-format msgid "" "Go to Packages , click 'Import Package' " "and select Repository as Package Source." msgstr "" "Go to Packages , click 'Import Package' " "and select Repository as Package Source." #, python-format msgid "" "Go to Packages, click 'Import Package' " "and select Repository as Package Source." msgstr "" "Go to Packages, click 'Import Package' " "and select Repository as Package Source." msgid "HTTP/HTTPS URL of the bundle file." msgstr "HTTP/HTTPS URL of the bundle file." msgid "HTTP/HTTPS URL of the package file." msgstr "HTTP/HTTPS URL of the package file." msgid "Heat Orchestration stack name" msgstr "Heat Orchestration stack name" msgid "Heat Orchestration stack%(forloop.counter)s name" msgstr "Heat Orchestration stack%(forloop.counter)s name" 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 "" "If packages depend upon other packages and/or require specific glance " "images, those are going to be installed with them from Murano repository." 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 "" "If the package depends upon other packages and/or requires specific glance " "images, those are going to be installed with it from Murano repository." msgid "Image" msgstr "Image" msgid "Image Title" msgstr "Image Title" msgid "Image Type" msgstr "Image Type" msgid "Image successfully marked" msgstr "Image successfully marked" msgid "Images" msgstr "Images" msgid "Import Bundle" msgstr "Import Bundle" msgid "Import Package" msgstr "Import Package" msgid "Importing package {0} failed. Reason: {1}" msgstr "Importing package {0} failed. Reason: {1}" msgid "Info" msgstr "Info" msgid "Instance name" msgstr "Instance name" msgid "Instance%(forloop.counter)s name" msgstr "Instance%(forloop.counter)s name" msgid "Invalid metadata for image: {0}" msgstr "Invalid metadata for image: {0}" msgid "Invalid murano image metadata" msgstr "Invalid murano image metadata" msgid "Invalid value of 'murano_nets' option" msgstr "Invalid value of 'murano_nets' option" msgid "It is forbidden to upload files larger than {0} MB." msgstr "It is forbidden to upload files larger than {0} MB." msgid "KeyWord" msgstr "KeyWord" msgid "Last operation" msgstr "Last operation" msgid "Latest Deployment Log" msgstr "Latest Deployment Log" msgid "License" msgstr "Licence" msgid "Logs" msgstr "Logs" msgid "Logs (Created, Message)" msgstr "Logs (Created, Message)" msgid "Manage" msgstr "Manage" msgid "Manage Components" msgstr "Manage Components" msgctxt "Package requirements" msgid "Manifest file" msgstr "Manifest file" msgid "Mark Image" msgstr "Mark Image" msgid "" "Mark an image with Murano specific metadata to be added to the selected " "image." msgstr "" "Mark an image with Murano specific metadata to be added to the selected " "image." msgid "Marked Images" msgstr "Marked Images" msgid "Modify Package" msgstr "Modify Package" msgid "Modifying package failed" msgstr "Modifying package failed" msgid "NO ENVIRONMENTS" msgstr "NO ENVIRONMENTS" msgid "Name" msgstr "Name" msgid "Name of the bundle." msgstr "Name of the bundle." #, python-format msgid "Network of '%s'" msgstr "Network of '%s'" msgid "Next" msgstr "Next" msgid "Next Page" msgstr "Next Page" msgid "No availability zones available" msgstr "No availability zones available" msgid "No categories available" msgstr "No categories available" msgid "No components" msgstr "No components" msgid "No images available" msgstr "No images available" msgid "No keypair" msgstr "No keypair" msgid "No license" msgstr "No licence" msgid "No recent activity to report at this time." msgstr "No recent activity to report at this time." msgid "No requirements" msgstr "No requirements" msgid "No volumes available" msgstr "No volumes available" msgid "None" msgstr "None" msgid "Not in domain" msgstr "Not in domain" msgid "Note" msgstr "Note" msgid "Number of Instances" msgstr "Number of Instances" msgid "Number of VCPUs" msgstr "Number of vCPUs" msgid "" "OpenStack Networking (Neutron) is not available in current environment. " "Custom Network Settings cannot be applied" msgstr "" "OpenStack Networking (Neutron) is not available in current environment. " "Custom Network Settings cannot be applied" msgid "Operation is forbidden by murano-api server." msgstr "Operation is forbidden by murano-api server." msgid "Optional" msgstr "Optional" msgid "Overview" msgstr "Overview" msgid "Package Bundle Source" msgstr "Package Bundle Source" msgid "Package Count" msgstr "Package Count" msgid "Package Details" msgstr "Package Details" msgid "Package Name" msgstr "Package Name" msgid "Package Source" msgstr "Package Source" msgid "Package Tags" msgstr "Package Tags" msgid "Package URL" msgstr "Package URL" msgid "Package Version" msgstr "Package Version" msgid "" "Package creation failed.Reason: Can't find Package name from repository." msgstr "" "Package creation failed.Reason: Can't find Package name from repository." msgid "Package creation failed.Reason: {0}" msgstr "Package creation failed.Reason: {0}" msgid "Package foo uploaded" msgstr "Package foo uploaded" msgid "Package modified." msgstr "Package modified." msgid "Package name in the repository, usually a fully qualified name" msgstr "Package name in the repository, usually a fully qualified name" msgid "Package or Class with the same name is already made public" msgstr "Package or Class with the same name is already made public" msgid "Package parameters successfully updated." msgstr "Package parameters successfully updated." msgid "Package version" msgstr "Package version" msgid "Package with id foo_package_id is not found" msgstr "Package with id foo_package_id is not found" msgid "Package with id {0} is not found" msgstr "Package with id {0} is not found" msgid "Package with specified name already exists" msgstr "Package with specified name already exists" msgid "Package {0} already registered." msgstr "Package {0} already registered." msgid "Package {0} upload failed. {1}" msgstr "Package {0} upload failed. {1}" msgid "Package {0} uploaded" msgstr "Package {0} uploaded" msgid "Packages" msgstr "Packages" msgid "Packages should contain:" msgstr "Packages should contain:" msgid "Please confirm your password" msgstr "Please confirm your password" msgid "Please supply a bundle name" msgstr "Please supply a bundle name" msgid "Please supply a bundle url" msgstr "Please supply a bundle URL" msgid "Please supply a package file" msgstr "Please supply a package file" msgid "Please supply a package name" msgstr "Please supply a package name" msgid "Please supply a package url" msgstr "Please supply a package URL" msgid "Previous Page" msgstr "Previous Page" msgid "Provide comma-separated list of words, associated with the package" msgstr "Provide comma-separated list of words, associated with the package" msgid "Provide desired name for a new category" msgstr "Provide desired name for a new category" msgid "Public" msgstr "Public" msgid "Quick Deploy" msgstr "Quick Deploy" msgid "Ready" msgstr "Ready" msgid "Ready to configure" msgstr "Ready to configure" msgid "Ready to deploy" msgstr "Ready to deploy" msgid "Recent Activity" msgstr "Recent Activity" msgid "Repository" msgstr "Repository" msgid "Requested object is not found on murano server." msgstr "Requested object is not found on murano server." msgid "Requested operation conflicts with an existing object." msgstr "Requested operation conflicts with an existing object." msgid "Requirements" msgstr "Requirements" msgid "Retype your password" msgstr "Retype your password" msgid "Running" msgstr "Running" msgid "Running with errors" msgstr "Running with errors" msgid "Running with warnings" msgstr "Running with warnings" msgid "Select Application" msgstr "Select Application" msgid "Select Image" msgstr "Select Image" #, python-format msgid "Select a valid choice. %(value)s is not one of the available choices." msgstr "Select a valid choice. %(value)s is not one of the available choices." msgid "Select an image registered in Glance Image Services." msgstr "Select an image registered in Glance Image Services." msgid "" "Select an image type supported by Murano. Choose 'Custom type' to enter type " "manually." msgstr "" "Select an image type supported by Murano. Choose 'Custom type' to enter type " "manually." msgid "Select one or more categories for a package." msgstr "Select one or more categories for a package." msgid "Select volume" msgstr "Select volume" msgid "Services (Name, Type)" msgstr "Services (Name, Type)" msgid "Set up for identifying a package." msgstr "Set up for identifying a package." msgid "Show Details" msgstr "Show Details" msgid "Something went wrong during package downloading" msgstr "Something went wrong during package downloading" msgid "Sorry, this environment doesn't exist anymore" msgstr "Sorry, this environment doesn't exist any more" msgid "" "Sorry, you can't add application right now. The environment is deploying." msgstr "" "Sorry, you can't add application right now. The environment is deploying." msgid "Sorry, you can't delete service right now" msgstr "Sorry, you can't delete service right now" msgid "Specified title already in use. Please choose another one." msgstr "Specified title already in use. Please choose another one." msgid "Specifying a category helps to filter applications in the catalog" msgstr "Specifying a category helps to filter applications in the catalogue" msgid "Started Deleting Component" msgid_plural "Started Deleting Components" msgstr[0] "Started Deleting Component" msgstr[1] "Started Deleting Components" msgid "Started Deleting Environment" msgid_plural "Started Deleting Environments" msgstr[0] "Started Deleting Environment" msgstr[1] "Started Deleting Environments" msgid "Started deploying Environment" msgid_plural "Started deploying Environments" msgstr[0] "Started deploying Environment" msgstr[1] "Started deploying Environments" msgid "Status" msgstr "Status" msgid "Step {0}" msgstr "Step {0}" msgid "Successful" msgstr "Successful" msgid "Tags" msgstr "Tags" msgid "Tenant Name" msgstr "Tenant Name" msgid "The '{0}' application successfully added to environment." msgstr "The '{0}' application successfully added to environment." 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 "" "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" #, python-format msgid "" "The bundle is going to be installed from %(murano_repo_url)s repository." msgstr "" "The bundle is going to be installed from %(murano_repo_url)s repository." msgid "The environment name field cannot be empty." msgstr "The environment name field cannot be empty." #, python-format msgid "" "The package is going to be imported from %(murano_repo_url)s repository." msgstr "" "The package is going to be imported from %(murano_repo_url)s repository." msgid "" "The password must contain at least one letter, " "one number and one special character" msgstr "" "The password must contain at least one letter, " "one number and one special character" msgid "The request data is not acceptable by the server" msgstr "The request data is not acceptable by the server" msgid "There are no applications in the catalog. You can import apps from" msgstr "There are no applications in the catalogue. You can import apps from" #, python-format msgid "" "There are no applications in the catalog. You can import apps from %(display_repo_url)s." msgstr "" "There are no applications in the catalogue. You can import apps from %(display_repo_url)s." msgid "There are no applications matching your criteria." msgstr "There are no applications matching your criteria." msgid "There was an error communicating with server" msgstr "There was an error communicating with server" msgid "There was an error initialising this field." msgstr "There was an error initialising this field." msgid "" "This Application requires encryption, please contact your administrator to " "configure this." msgstr "" "This Application requires encryption, please contact your administrator to " "configure this." msgid "" "This action cannot be undone. Any resources created by this environment will " "have to be released manually." msgstr "" "This action cannot be undone. Any resources created by this environment will " "have to be released manually." msgid "Time Finished" msgstr "Time Finished" msgid "Time Started" msgstr "Time Started" msgid "Time updated" msgstr "Time updated" msgid "Title" msgstr "Title" msgid "Toggle Active" msgid_plural "Toggle Active" msgstr[0] "Toggle Active" msgstr[1] "Toggle Active" msgid "Toggle Enabled" msgstr "Toggle Enabled" msgid "Toggle Public" msgid_plural "Toggle Public" msgstr[0] "Toggle Public" msgstr[1] "Toggle Public" msgid "Toggled Active" msgid_plural "Toggled Active" msgstr[0] "Toggled Active" msgstr[1] "Toggled Active" msgid "Toggled Public" msgid_plural "Toggled Public" msgstr[0] "Toggled Public" msgstr[1] "Toggled Public" msgid "Topology" msgstr "Topology" msgid "Total RAM" msgstr "Total RAM" msgid "" "Trying to add {0} image to glance. Image will be ready for deployment after " "successful upload" msgstr "" "Trying to add {0} image to Glance. Image will be ready for deployment after " "successful upload" msgid "" "Trying to add {0}, {1} image to glance. Image will be ready for deployment " "after successful upload" msgstr "" "Trying to add {0}, {1} image to Glance. Image will be ready for deployment " "after successful upload" msgid "Type" msgstr "Type" msgctxt "Package requirements" msgid "UI definition folder" msgstr "UI definition folder" msgid "UNKNOWN" msgstr "UNKNOWN" msgid "URL" msgstr "URL" msgid "Unable to abandon an environment {0} due to: {1}" msgstr "Unable to abandon an environment {0} due to: {1}" msgid "Unable to communicate to glare-api server." msgstr "Unable to communicate to glare-api server." msgid "Unable to communicate to murano-api server." msgstr "Unable to communicate to murano-api server." msgid "Unable to create environment {0} due to: {1}" msgstr "Unable to create environment {0} due to: {1}" msgid "Unable to delete category" msgstr "Unable to delete category" msgid "Unable to delete environment {0} due to: {1}" msgstr "Unable to delete environment {0} due to: {1}" msgid "Unable to delete package in murano-api server" msgstr "Unable to delete package in murano-api server" msgid "Unable to deploy. Try again later" msgstr "Unable to deploy. Try again later" msgid "Unable to download package." msgstr "Unable to download package." msgid "Unable to get list of categories" msgstr "Unable to get list of categories" msgid "Unable to mark image" msgstr "Unable to mark image" msgid "Unable to modify package" msgstr "Unable to modify package" msgid "Unable to remove metadata" msgstr "Unable to remove metadata" msgid "Unable to remove package." msgstr "Unable to remove package." msgid "Unable to retrieve availability zones." msgstr "Unable to retrieve availability zones." msgid "Unable to retrieve deployment history." msgstr "Unable to retrieve deployment history." msgid "Unable to retrieve details for service" msgstr "Unable to retrieve details for service" msgid "Unable to retrieve list of deployments" msgstr "Unable to retrieve list of deployments" msgid "Unable to retrieve list of images" msgstr "Unable to retrieve list of images" msgid "" "Unable to retrieve list of services. This environment is deploying or " "already deployed by other user." msgstr "" "Unable to retrieve list of services. This environment is deploying or " "already deployed by other user." msgid "Unable to retrieve package details." msgstr "Unable to retrieve package details." msgid "Unable to retrieve project list." msgstr "Unable to retrieve project list." msgid "Unable to retrieve public images." msgstr "Unable to retrieve public images." msgid "Unable to retrieve snapshot list." msgstr "Unable to retrieve snapshot list." msgid "Unable to retrieve volume list." msgstr "Unable to retrieve volume list." msgid "Unavailable" msgstr "Unavailable" msgid "Unknown" msgstr "Unknown" msgid "Update" msgstr "Update" msgid "Update Environment" msgid_plural "Deploy Environments" msgstr[0] "Update Environment" msgstr[1] "Deploy Environments" msgid "Update Image" msgstr "Update Image" msgid "Update Metadata" msgstr "Update Metadata" msgid "Update This Environment" msgstr "Update This Environment" msgid "Updated" msgstr "Updated" msgid "Updated Environment" msgid_plural "Deployed Environments" msgstr[0] "Updated Environment" msgstr[1] "Deployed Environments" msgid "Uploading package failed. {0}" msgstr "Uploading package failed. {0}" msgid "Used for identifying and filtering packages." msgstr "Used for identifying and filtering packages." msgid "Validation Error occurred" msgstr "Validation Error occurred" msgid "Version" msgstr "Version" msgid "Version of the package (optional)." msgstr "Version of the package (optional)." msgid "You are not allowed to change this properties of the package" msgstr "You are not allowed to change this properties of the package" msgid "You are not allowed to delete this package" msgstr "You are not allowed to delete this package" msgid "You are not allowed to make packages public." msgstr "You are not allowed to make packages public." msgid "You are not allowed to perform this operation" msgstr "You are not allowed to perform this operation" msgid "" "You'll have to configure each package installed from this bundle separately." msgstr "" "You'll have to configure each package installed from this bundle separately." msgid "{0}{1} don't match" msgstr "{0}{1} don't match" murano-dashboard-5.0.0/muranodashboard/locale/en_GB/LC_MESSAGES/djangojs.po0000666000175100017510000000405713245511125026303 0ustar zuulzuul00000000000000# Andi Chandler , 2016. #zanata # Andi Chandler , 2017. #zanata msgid "" msgstr "" "Project-Id-Version: murano-dashboard 4.0.0.0rc2.dev17\n" "Report-Msgid-Bugs-To: https://bugs.launchpad.net/openstack-i18n/\n" "POT-Creation-Date: 2017-09-26 03:23+0000\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "PO-Revision-Date: 2017-10-11 12:34+0000\n" "Last-Translator: Andi Chandler \n" "Language-Team: English (United Kingdom)\n" "Language: en-GB\n" "X-Generator: Zanata 3.9.6\n" "Plural-Forms: nplurals=2; plural=(n != 1)\n" msgid " 1 capital letter" msgstr " 1 capital letter" msgid " 1 digit" msgstr " 1 digit" msgid " 1 non-capital letter" msgstr " 1 non-capital letter" msgid " 1 special character" msgstr " 1 special character" msgid " 7 characters" msgstr " 7 characters" msgid "An error occurred. Please try again later." msgstr "An error occurred. Please try again later." msgid "Cancel" msgstr "Cancel" msgid "Create" msgstr "Create" msgid "Loading" msgstr "Loading" msgid "New" msgstr "New" msgid "Passwords do not match" msgstr "Passwords do not match" msgid "Show less" msgstr "Show less" msgid "Show more" msgstr "Show more" msgid "There was an error submitting the form. Please try again." msgstr "There was an error submitting the form. Please try again." msgid "Unable to edit component metadata." msgstr "Unable to edit component metadata." msgid "Unable to edit environment metadata." msgstr "Unable to edit environment metadata." msgid "Unable to retrieve component metadata." msgstr "Unable to retrieve component metadata." msgid "Unable to retrieve environment metadata." msgstr "Unable to retrieve environment metadata." msgid "Unable to retrieve the packages." msgstr "Unable to retrieve the packages." msgid "Unable to run action." msgstr "Unable to run action." msgid "Waiting for a result" msgstr "Waiting for a result" msgid "Working" msgstr "Working" msgid "Your password should have at least" msgstr "Your password should have at least" murano-dashboard-5.0.0/muranodashboard/locale/tr_TR/0000775000175100017510000000000013245511556022437 5ustar zuulzuul00000000000000murano-dashboard-5.0.0/muranodashboard/locale/tr_TR/LC_MESSAGES/0000775000175100017510000000000013245511556024224 5ustar zuulzuul00000000000000murano-dashboard-5.0.0/muranodashboard/locale/tr_TR/LC_MESSAGES/django.po0000666000175100017510000007147213245511125026033 0ustar zuulzuul00000000000000# iÅŸbaran akçayır , 2017. #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: 2017-05-25 01:33+0000\n" "Last-Translator: iÅŸbaran akçayır \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: 1495486491.000000\n" msgid "-" msgstr "-" msgid "80 characters max." msgstr "Azami 80 karakter" msgid "A local zip file to upload" msgstr "Yüklenecek yerel zip dosyası" msgid "Abandon Environment" msgid_plural "Abandon Environments" msgstr[0] "Ortamı Terk Et" msgstr[1] "Ortamları Terk Et" msgid "Abandoned Environment" msgid_plural "Abandoned Environments" msgstr[0] "Ortam Terk Edildi" msgstr[1] "Ortamlar Terk Edildi" msgid "Active" msgstr "Etkin" msgid "Add" msgstr "Ekle" msgid "Add Application" msgstr "Uygulama Ekle" msgid "Add Application Category" msgstr "Uygulama Kategorisi Ekle" msgid "Add Category" msgstr "Kategori Ekle" msgid "Add Component" msgstr "BileÅŸen Ekle" msgid "Add Murano Metadata" msgstr "Murano Metaverisi Ekle" msgid "Add New" msgstr "Yeni Ekle" msgid "Add new category to the application catalog." msgstr "Uygulama kataloÄŸuna yeni kategori ekle." msgid "Add to Env" msgstr "Ortama Ekle" msgid "Adding application to an environment failed." msgstr "Uygulamanın bir ortama eklenmesi baÅŸarısız." msgid "All" msgstr "Hepsi" msgid "Allows adding additional information about a package." msgstr "Bir paket hakkında ek bilgi eklemeye izin verir." msgid "" "Allows to hide a package from the catalog. (Applies to package dependencies)" msgstr "" "Bir paketi katalogdan gizlemeye izin verir. (Paket bağımlılıkları için de " "geçerlidir)" msgid "" "An environment is a collection of applications that are meant to operate " "under similar conditions." msgstr "" "Ortam, benzer durumlarda çalışmak üzere bir araya gelen uygulamalar " "koleksiyonudur." msgid "An external http/https URL to load the bundle from." msgstr "Paket yığınının yükleneceÄŸi harici http/https URL." msgid "An external http/https URL to load the package from." msgstr "Paketin yükleneceÄŸi harici bir http/https URL." msgid "App Catalog" msgstr "Uygulama KataloÄŸu" msgid "App Category:" msgstr "Uygulama Kategorisi:" msgid "App category" msgstr "Uygulama kategorisi" msgid "Application Categories" msgstr "Uygulama Kategorileri" msgid "Application Category" msgstr "Uygulama Kategorisi" msgid "Application Details" msgstr "Uygulama Ayrıntıları" msgid "Application Package" msgstr "Uygulama Paketi" msgid "Application default security group" msgstr "Uygulama öntanımlı güvenlik grubu" msgid "Application Components" msgstr "Uygulama BileÅŸenler" msgid "Applications" msgstr "Uygulamalar" msgid "Author" msgstr "Yazar" msgid "Auto" msgstr "Oto" msgid "Back" msgstr "Geri" msgid "Browse" msgstr "Gözat" msgid "Browse Local" msgstr "Yerele Gözat" msgid "Bundle Name" msgstr "Paket Yığın İsmi" msgid "Bundle URL" msgstr "Paket Yığını URL'si" msgid "Bundle creation failed.Reason: Can't find Bundle name from repository." msgstr "Paket yığını oluÅŸurma baÅŸarısız. Sebep: Yığın ismi depodan bulunamadı." msgid "Bundle creation failed.Reason: {0}" msgstr "Paket yığını oluÅŸturma baÅŸarısız.Sebep: {0}" msgid "Bundle successfully imported." msgstr "Paket yığını baÅŸarıyla içe aktarıldı." msgid "Bundle's full name." msgstr "Paket yığınının tam ismi." msgid "Can not get logo for {0}." msgstr "{0} için amblem alınamadı." msgid "Can not get supplier logo for {0}." msgstr "{0} için saÄŸlayıcı amblemi alınamıyor." msgid "Cancel" msgstr "İptal" msgid "Categories" msgstr "Kategoriler" msgid "Category Name" msgstr "Kategori İsmi" msgid "Category {0} created." msgstr "Kategori {0} oluÅŸturuldu." msgid "Check Keystone configuration of murano-api server." msgstr "Murano-api sunucusunun Keystone yapılandırmasını kontrol edin." msgid "Choose a Zip archive to upload into the catalog." msgstr "KataloÄŸa yüklenecek Zip arÅŸivi seçin." msgid "Choose a name for the environment" msgstr "Ortam için bir isim seçin" msgctxt "Package requirements" msgid "Classes definition folder" msgstr "Sınıf tanımlamaları dizini" msgid "Click to create new environment" msgstr "Yeni ortam oluÅŸturmak için tıklayın" msgid "Completed with warnings" msgstr "Uyarılarla tamamlandı" msgid "Component" msgstr "BileÅŸen" msgid "Component Details" msgstr "BileÅŸen Ayrıntıları" msgid "Component List" msgstr "BileÅŸen Listesi" msgid "Component Logs" msgstr "BileÅŸen Sistem Günlüğü" msgid "Components" msgstr "BileÅŸenler" msgid "Configuration" msgstr "Yapılandırma" msgid "Configure Application" msgstr "Uygulamayı Yapılandır" msgid "Confirm password" msgstr "Parolayı onayla" msgid "Could not retrieve latest status for the {0} environment" msgstr "{0} ortamı için en son durum alınamadı" msgid "" "Couldn't find any apps, required for this field.\n" "Tried: {fqns}" msgstr "" "Bu alan için gerekli bir uygulama bulunamadı.\n" "Denenen: {fqns}" msgid "" "Couldn't initialise glance v1 client, therefore could not make the following " "images public: {0}" msgstr "" "Glance v1 istemcisi baÅŸlatılamadı, yani ÅŸu imajlar açık hale getirilemiyor: " "{0}" msgid "Couldn't update environment. Reason: This name is already taken." msgstr "Ortam güncellenemedi. Sebep: Bu isim alınmış." msgid "Couldn't update package {0} parameters. Error: {1}" msgstr "{0} paketi parametreleri güncellenemedi. Hata: {1}" msgid "Create" msgstr "OluÅŸtur" msgid "Create Env" msgstr "Ortam OluÅŸtur" msgid "Create Environment" msgstr "Ortam OluÅŸtur" msgid "Create New" msgstr "Yeni OluÅŸtur" msgid "Create a title for an image." msgstr "İmaj için bir baÅŸlık oluÅŸtur." msgid "Created" msgstr "OluÅŸturuldu" msgid "Custom Type" msgstr "Özel Tür" msgid "" "Defines whether or not a package can be used by other tenants. (Applies to " "package dependencies)" msgstr "" "Bir paketin diÄŸer kiracılarca kullanılıp kullanılmayacağını tanımlar. (Paket " "bağımlılıkları için de geçerlidir)" msgid "Delete Category" msgid_plural "Delete Categories" msgstr[0] "Kategoriyi Sil" msgstr[1] "Kategorileri Sil" msgid "Delete Component" msgid_plural "Delete Components" msgstr[0] "BileÅŸeni Sil" msgstr[1] "BileÅŸenleri Sil" msgid "Delete Environment" msgid_plural "Delete Environments" msgstr[0] "Ortamı Sil" msgstr[1] "Ortamları Sil" msgid "Delete Metadata" msgid_plural "Delete Metadata" msgstr[0] "Metaveriyi Sil" msgstr[1] "Metaveriyi Sil" msgid "Delete Package" msgid_plural "Delete Packages" msgstr[0] "Paketi Sil" msgstr[1] "Paketleri Sil" msgid "Delete failure" msgstr "Silme baÅŸarısız" msgid "Deleted Category" msgid_plural "Deleted Categories" msgstr[0] "Kategori Silindi" msgstr[1] "Kategoriler Silindi" msgid "Deleted Metadata" msgid_plural "Deleted Metadata" msgstr[0] "Metaveri Silindi" msgstr[1] "Metaveri Silindi" msgid "Deleted Package" msgid_plural "Deleted Packages" msgstr[0] "Paket Silindi" msgstr[1] "Paketler Silindi" msgid "Deleting" msgstr "Siliniyor" msgid "Deploy Environment" msgid_plural "Deploy Environments" msgstr[0] "Ortamı YerleÅŸtir" msgstr[1] "Ortamları YerleÅŸtir" msgid "Deploy This Environment" msgstr "Bu Ortamı Uygula" msgid "Deploy failure" msgstr "YerleÅŸim baÅŸarısız" msgid "Deploy started" msgstr "YerleÅŸtirme baÅŸlatıldı" msgid "Deployed Components" msgstr "YerleÅŸtirilen BileÅŸenler" msgid "Deploying" msgstr "YerleÅŸtiriliyor" msgid "Deployment Details" msgstr "YerleÅŸtirme Ayrıntıları" msgid "Deployment History" msgstr "YerleÅŸtirme GeçmiÅŸi" msgid "Deployment Logs" msgstr "YerleÅŸtirme Günlükleri" #, python-format msgid "Deployment with id %s doesn't exist anymore" msgstr "%s kimlikli yerleÅŸtirme artık mevcut deÄŸil" msgid "Deployment with id foo_deployment_id doesn't exist anymore" msgstr "foo_deployment_id kimliÄŸine sahip yerleÅŸim artık mevcut deÄŸil" msgid "Deployments" msgstr "YerleÅŸtirmeler" msgid "Description" msgstr "Açıklama" msgid "Details" msgstr "Ayrıntılar" msgid "Download Package" msgstr "Paket İndir" msgid "Drop Components here" msgstr "BileÅŸenleri buraya bırak" msgid "Enabled" msgstr "Etkin" msgid "" "Enter a complex password with at least one letter, one number and one " "special character" msgstr "" "En az bir harf, bir sayı ve bir özel karakter içeren karmaşık bir parola " "girin" msgid "Enter a password" msgstr "Bir parola girin" msgid "Enter an image type supported by Murano." msgstr "Murano tarafından desteklenen bir imaj türü girin." msgid "Environment" msgstr "Ortam" msgid "Environment Default Network" msgstr "Ortam Öntanımlı Ağı" msgid "Environment Name" msgstr "Ortam İsmi" msgid "Environment name must contain at least one non-white space symbol." msgstr "Ortam ismi en az bir boÅŸluk dışı sembol içermeli." #, python-format msgid "Environment with id %s doesn't exist anymore" msgstr "%s kimlikli ortam artık mevcut deÄŸil" msgid "Environment with specified name already exists" msgstr "Belirtilen isimde ortam zaten mevcut" msgid "Environments" msgstr "Ortamlar" msgid "Error test_error_message occurred while installing package bar" msgstr "Bar paketi yüklenirken test_error_message hatası oluÅŸtu" msgid "Error {0} occurred while installing images for {1}" msgstr "{1} için imajlar yüklenirken {0} hatası oluÅŸtu" msgid "Error {0} occurred while installing package {1}" msgstr "{1} paketi yüklenirken {0} hatası oluÅŸtu" msgid "Error {0} occurred while parsing package {1}" msgstr "Paket {1} ayrıştırılırken {0} hatası oluÅŸtu" msgid "Error {0} occurred while setting image {1}, {2} public" msgstr "İmaj {1}, {2} açık hale getirilirken hata {0} oluÅŸtu" msgctxt "Package requirements" msgid "Execution plans folder" msgstr "Çalıştırma planları dizini" msgid "FQN" msgstr "FQN" msgid "Failed" msgstr "BaÅŸarısız" msgid "Failed to create environment" msgstr "Ortam oluÅŸturma baÅŸarısız" msgid "Failed to modify the package. {0}" msgstr "Paket deÄŸiÅŸtirme baÅŸarısız. {0}" msgid "File" msgstr "Dosya" msgid "Filter" msgstr "Süzgeç" msgid "Find in a selected category" msgstr "Seçili kategoride bul" 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 "" "İlk sembol latin harf veya alt çizgi olmalı. Takip eden semboller latin " "harf, sayı, alt çizgi, @ iÅŸareti, sayı iÅŸareti veya dolar iÅŸareti olabilir" msgid "Foo" msgstr "Foo" msgid "Fully qualified package name." msgstr "Tam donanımlı paket ismi." #, python-format msgid "" "Go to Packages , click 'Import Package' " "and select Repository as Package Source." msgstr "" " Paketlere gidin, 'Paket İçe Aktar'a " "tıklayın ve Paket Kaynağı olarak Depo seçin." #, python-format msgid "" "Go to Packages, click 'Import Package' " "and select Repository as Package Source." msgstr "" "Paketlere gidin, 'Paket İçe Aktar'a " "tıklayın ve Paket Kaynağı olarak Depo seçin." msgid "HTTP/HTTPS URL of the bundle file." msgstr "Paket yığın dosyasının HTTP/HTTPS URL'si." msgid "HTTP/HTTPS URL of the package file." msgstr "Paket dosyasının HTTP/HTTPS URL'si." msgid "Heat Orchestration stack name" msgstr "Heat Orkestrasyon yığın ismi" msgid "Heat Orchestration stack%(forloop.counter)s name" msgstr "Heat Orkestrasyon yığın%(forloop.counter)s ismi" 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 "" "Paketler baÅŸka paketlere ve/veya belirli glance imajlarına bağımlıysa, " "bunlar da murano deposundan yüklenecektir." 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 "" "Paket baÅŸka paketlere ve/veya belirli glance imajlarına bağımlıysa, bunlar " "da beraberinde murano deposundan yüklenecek." msgid "Image" msgstr "İmaj" msgid "Image Title" msgstr "İmaj BaÅŸlığı" msgid "Image Type" msgstr "İmaj Türü" msgid "Image successfully marked" msgstr "İmaj baÅŸarıyla iÅŸaretlendi" msgid "Images" msgstr "İmajlar" msgid "Import Bundle" msgstr "Paket Yığınını İçe Aktar" msgid "Import Package" msgstr "Paketi İçe Aktar" msgid "Importing package {0} failed. Reason: {1}" msgstr "{0} paketinin içe aktarımı baÅŸarısız. Sebep: {1}" msgid "Info" msgstr "Bilgi" msgid "Instance name" msgstr "Sunucu ismi" msgid "Instance%(forloop.counter)s name" msgstr "Sunucu%(forloop.counter)s ismi" msgid "Invalid metadata for image: {0}" msgstr "İmaj için geçersiz metaveri: {0}" msgid "Invalid murano image metadata" msgstr "Geçersiz murano imaj metaverisi" msgid "Invalid value of 'murano_nets' option" msgstr "'murano_nets' seçeneÄŸi geçersiz deÄŸere sahip" msgid "It is forbidden to upload files larger than {0} MB." msgstr "{0} MB'dan büyük dosyaların yüklenmesine izin verilmiyor." msgid "KeyWord" msgstr "AnahtarKelime" msgid "Last operation" msgstr "Son iÅŸlem" msgid "Latest Deployment Log" msgstr "Son YerleÅŸtirme Günlük Kaydı" msgid "License" msgstr "Lisans" msgid "Logs" msgstr "Günlük Kayıtları" msgid "Manage" msgstr "Yönet" msgid "Manage Components" msgstr "BileÅŸenleri Yönet" msgctxt "Package requirements" msgid "Manifest file" msgstr "Manifesto dosyası" msgid "Mark Image" msgstr "İmajı İşaretle" msgid "" "Mark an image with Murano specific metadata to be added to the selected " "image." msgstr "" "Seçili imaja eklenmek üzere Murano'ya özel metaverisi olan bir imaj iÅŸaretle." msgid "Marked Images" msgstr "İşaretli İmajlar" msgid "Modify Package" msgstr "Paketi DeÄŸiÅŸtir" msgid "Modifying package failed" msgstr "Paket deÄŸiÅŸtirme baÅŸarısız" msgid "NO ENVIRONMENTS" msgstr "ORTAM YOK" msgid "Name" msgstr "İsim" msgid "Name of the bundle." msgstr "Paket yığını ismi." #, python-format msgid "Network of '%s'" msgstr "'%s' Ağı" msgid "Next" msgstr "Sonraki" msgid "Next Page" msgstr "Sonraki Sayfa" msgid "No availability zones available" msgstr "Kullanılabilir kullanılırlık bölgesi yok" msgid "No categories available" msgstr "Kullanılabilir kategori yok" msgid "No components" msgstr "BileÅŸen yok" msgid "No images available" msgstr "Kullanılabilir imaj yok" msgid "No keypair" msgstr "Anahtar çifti yok" msgid "No license" msgstr "Lisans yok" msgid "No recent activity to report at this time." msgstr "Åžu an raporlanacak yakın tarihli etkinlik yok." msgid "No requirements" msgstr "Gereksinim yok" msgid "None" msgstr "Hiçbiri" msgid "Not in domain" msgstr "Alanda deÄŸil" msgid "Note" msgstr "Not" msgid "" "OpenStack Networking (Neutron) is not available in current environment. " "Custom Network Settings cannot be applied" msgstr "" "OpenStack Ağı (Neutron) mevcut ortamda kullanılabilir deÄŸil. Özel AÄŸ " "Ayarları uygulanamıyor" msgid "Operation is forbidden by murano-api server." msgstr "Murano-api sunucu iÅŸleme izin vermiyor." msgid "Optional" msgstr "İsteÄŸe baÄŸlı" msgid "Overview" msgstr "Genel Görünüm" msgid "Package Bundle Source" msgstr "Paket Yığını Kaynağı" msgid "Package Count" msgstr "Paket Sayısı" msgid "Package Details" msgstr "Paket Ayrıntıları" msgid "Package Name" msgstr "Paket İsmi" msgid "Package Source" msgstr "Paket Kaynağı" msgid "Package Tags" msgstr "Paket Etiketleri" msgid "Package URL" msgstr "Paket URL'si" msgid "Package Version" msgstr "Paket Sürümü" msgid "" "Package creation failed.Reason: Can't find Package name from repository." msgstr "Paket oluÅŸturma baÅŸarısız. Sebep: Depodan Paket ismi bulunamıyor." msgid "Package creation failed.Reason: {0}" msgstr "Paket oluÅŸturma baÅŸarısız. Sebep: {0}" msgid "Package foo uploaded" msgstr "Paket foo yüklendi" msgid "Package modified." msgstr "Paket deÄŸiÅŸtirildi." msgid "Package name in the repository, usually a fully qualified name" msgstr "Depodaki paket ismi, genellikle tam donanımlı bir isim" msgid "Package or Class with the same name is already made public" msgstr "Aynı isimde Paket veya Sınıf zaten açık yapılmış" msgid "Package parameters successfully updated." msgstr "Paket parametreleri baÅŸarıyla güncellendi." msgid "Package version" msgstr "Paket sürümü" msgid "Package with id foo_package_id is not found" msgstr "foo_package_id kimliÄŸine sahip paket bulunamadı" msgid "Package with id {0} is not found" msgstr "{0} kimliÄŸine sahip paket bulunamadı" msgid "Package with specified name already exists" msgstr "Belirtilen isimde paket zaten mevcut" msgid "Package {0} already registered." msgstr "Paket {0} zaten kaydedilmiÅŸ." msgid "Package {0} upload failed. {1}" msgstr "Paket {0} yüklemesi baÅŸarısız. {1}" msgid "Package {0} uploaded" msgstr "Paket {0} yüklendi" msgid "Packages" msgstr "Paketler" msgid "Packages should contain:" msgstr "Paketler ÅŸunları içermeli:" msgid "Please confirm your password" msgstr "Lütfen parolanızı onaylayın" msgid "Please supply a bundle name" msgstr "Lütfen bir paket yığını ismi saÄŸlayın" msgid "Please supply a bundle url" msgstr "Lütfen bir paket yığını url'si saÄŸlayın" msgid "Please supply a package file" msgstr "Lütfen bir paket dosyası saÄŸlayın" msgid "Please supply a package name" msgstr "Lütfen bir paket ismi saÄŸlayın" msgid "Please supply a package url" msgstr "Lütfen bir paket url'si saÄŸlayın" msgid "Previous Page" msgstr "Önceki Sayfa" msgid "Provide comma-separated list of words, associated with the package" msgstr "Paketle iliÅŸkili, virgülle ayrılmış kelimelerin listesini verin" msgid "Provide desired name for a new category" msgstr "Yeni bir kategori için istediÄŸiniz ismi girin" msgid "Public" msgstr "Açık" msgid "Quick Deploy" msgstr "Hızlı YerleÅŸim" msgid "Ready" msgstr "Hazır" msgid "Ready to configure" msgstr "Yapılandırmaya hazır" msgid "Ready to deploy" msgstr "YerleÅŸime hazır" msgid "Recent Activity" msgstr "Son Etkinlik" msgid "Repository" msgstr "Depo" msgid "Requested object is not found on murano server." msgstr "İstenen nesne murano sunucuda bulunamadı." msgid "Requested operation conflicts with an existing object." msgstr "İstenen iÅŸlem var olan bir nesne ile çakışıyor." msgid "Requirements" msgstr "Gereksinimler" msgid "Retype your password" msgstr "Parolanızı tekrar girin" msgid "Running" msgstr "Çalışıyor" msgid "Running with errors" msgstr "Hatalarla çalışıyor" msgid "Running with warnings" msgstr "Uyarılarla çalışıyor" msgid "Select Application" msgstr "Uygulama Seçin" msgid "Select Image" msgstr "İmaj seçin" msgid "Select an image registered in Glance Image Services." msgstr "Glance İmaj Servislerinde kayıtlı bir imaj seç." msgid "" "Select an image type supported by Murano. Choose 'Custom type' to enter type " "manually." msgstr "" "Murano tarafından desteklenen bir imaj türü seçin. Türü elle girmek için " "'Özel tür' seçin." msgid "Select one or more categories for a package." msgstr "Paket için bir ya da fazla kategori seçin." msgid "Set up for identifying a package." msgstr "Paketi tanımlamak için ayarla." msgid "Show Details" msgstr "Ayrıntıları Göster" msgid "Something went wrong during package downloading" msgstr "Paket indirirken bir ÅŸeyler yanlış gitti" msgid "Sorry, this environment doesn't exist anymore" msgstr "Üzgünüm, bu ortam artık mevcut deÄŸil" msgid "" "Sorry, you can't add application right now. The environment is deploying." msgstr "Üzgünüm, ÅŸu an uygulama ekleyemezsiniz. Ortam açılıyor." msgid "Sorry, you can't delete service right now" msgstr "Üzgünüm, ÅŸu an servis silemezsiniz" msgid "Specified title already in use. Please choose another one." msgstr "Belirtilen baÅŸlık kullanımda. Lütfen baÅŸka bir tane seçin." msgid "Specifying a category helps to filter applications in the catalog" msgstr "Bir kategori belirtmek katalogdaki uygulamaları süzmeyi kolaylaÅŸtırır" msgid "Started Deleting Component" msgid_plural "Started Deleting Components" msgstr[0] "BileÅŸen Silinmesi BaÅŸlatıldı" msgstr[1] "BileÅŸenlerin Silinmesi BaÅŸlatıldı" msgid "Started Deleting Environment" msgid_plural "Started Deleting Environments" msgstr[0] "Ortam Silme BaÅŸlatıldı" msgstr[1] "Ortamların Silinmesi BaÅŸlatıldı" msgid "Started deploying Environment" msgid_plural "Started deploying Environments" msgstr[0] "Ortamın yerleÅŸtirilmesi baÅŸlatıldı" msgstr[1] "Ortamların yerleÅŸtirilmesi baÅŸlatıldı" msgid "Status" msgstr "Durum" msgid "Step {0}" msgstr "Adım {0}" msgid "Successful" msgstr "BaÅŸarılı" msgid "Tags" msgstr "Etiketler" msgid "Tenant Name" msgstr "Kiracı İsmi" msgid "The '{0}' application successfully added to environment." msgstr "'{0}' uygulaması baÅŸarıyla ortama eklendi." 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 "" "Bu ortamdaki uygulamaların sanal makineleri tek tek yapılandırılmazlarsa " "öntanımlı olarak bu aÄŸa baÄŸlanacaklar. 'Yeni OluÅŸtur' seçmek, bu proje için " "öntanımlı Murano Yönlendirici için kullanılabilir olanlar arasından IP " "aralığı atanmış Alt AÄŸa sahip yeni bir AÄŸ oluÅŸturur" #, python-format msgid "" "The bundle is going to be installed from %(murano_repo_url)s repository." msgstr "" "Bu paket yığını " "%(murano_repo_url)s deposundan yüklenecek." msgid "The environment name field cannot be empty." msgstr "Ortam ismi alanı boÅŸ olamaz." #, python-format msgid "" "The package is going to be imported from %(murano_repo_url)s repository." msgstr "" "Paket %(murano_repo_url)s deposundan içe aktarılacak." msgid "" "The password must contain at least one letter, " "one number and one special character" msgstr "Parola en az bir harf, bir sayı ve bir özel karakter içermeli" msgid "The request data is not acceptable by the server" msgstr "İstek verisi sunucu tarafından kabul edilebilir deÄŸil" msgid "There are no applications in the catalog. You can import apps from" msgstr "Katalogda uygulama yok. Uygulamaları ÅŸuradan içe aktarabilirsiniz" #, python-format msgid "" "There are no applications in the catalog. You can import apps from %(display_repo_url)s." msgstr "" "Katalogda uygulama yok. " "%(display_repo_url)s adresinden uygulamaları içe aktarabilirsiniz." msgid "There are no applications matching your criteria." msgstr "Kıstaslarınızla eÅŸleÅŸen uygulama yok." msgid "There was an error communicating with server" msgstr "Sunucu ile iletiÅŸimde bir hata oluÅŸtu" msgid "There was an error initialising this field." msgstr "Bu alanın ilklendirilmesinde bir hata oluÅŸtu." msgid "" "This action cannot be undone. Any resources created by this environment will " "have to be released manually." msgstr "" "Bu eylem geri alınamaz. Bu ortam tarafından oluÅŸturulan tüm kaynakların elle " "serbest bırakılması gerekecek." msgid "Time Finished" msgstr "Bitme Zamanı" msgid "Time Started" msgstr "BaÅŸlama Zamanı" msgid "Time updated" msgstr "Zaman güncellendi" msgid "Title" msgstr "BaÅŸlık" msgid "Toggle Active" msgid_plural "Toggle Active" msgstr[0] "EtkinliÄŸi DeÄŸiÅŸtir" msgstr[1] "EtkinliÄŸi DeÄŸiÅŸtir" msgid "Toggle Enabled" msgstr "EtkinliÄŸi DeÄŸiÅŸtir" msgid "Toggle Public" msgid_plural "Toggle Public" msgstr[0] "Açıklığı DeÄŸiÅŸtir" msgstr[1] "Açıklığı DeÄŸiÅŸtir" msgid "Toggled Active" msgid_plural "Toggled Active" msgstr[0] "Etkinlik DeÄŸiÅŸtirildi" msgstr[1] "Etkinlik DeÄŸiÅŸtirildi" msgid "Toggled Public" msgid_plural "Toggled Public" msgstr[0] "Açıklık DeÄŸiÅŸtirildi" msgstr[1] "Açıklık DeÄŸiÅŸtirildi" msgid "Topology" msgstr "Topoloji" msgid "" "Trying to add {0} image to glance. Image will be ready for deployment after " "successful upload" msgstr "" "{0} imajı glance'e eklenmeye çalışılıyor. İmaj baÅŸarıyla yüklendikten sonra " "yerleÅŸtirme için hazır olacak" msgid "" "Trying to add {0}, {1} image to glance. Image will be ready for deployment " "after successful upload" msgstr "" "{0}, {1} imajı glance'e eklenmeye çalışılıyor. İmaj baÅŸarıyla yüklendikten " "sonra yerleÅŸtirme için hazır olacak" msgid "Type" msgstr "Tür" msgctxt "Package requirements" msgid "UI definition folder" msgstr "UI tanımlamaları dizini" msgid "UNKNOWN" msgstr "BİLİNMEYEN" msgid "URL" msgstr "URL" msgid "Unable to abandon an environment {0} due to: {1}" msgstr "Bir ortam {0} terk edilemedi, sebep: {1}" msgid "Unable to communicate to glare-api server." msgstr "Glare-api sunucusu ile iletiÅŸim kurulamadı." msgid "Unable to communicate to murano-api server." msgstr "Murano-api sunucusu ile iletiÅŸim kurulamadı." msgid "Unable to create environment {0} due to: {1}" msgstr "{0} ortamı oluÅŸturulamadı, sebep: {1}" msgid "Unable to delete category" msgstr "Kategori silinemiyor" msgid "Unable to delete environment {0} due to: {1}" msgstr "{0} ortamı ÅŸu sebepten silinemedi: {1}" msgid "Unable to delete package in murano-api server" msgstr "Murano-api sunucusundaki paket silinemiyor" msgid "Unable to deploy. Try again later" msgstr "Dağıtım yapılamıyor. Daha sonra tekrar deneyin" msgid "Unable to download package." msgstr "Paket indirilemiyor." msgid "Unable to get list of categories" msgstr "Kategori listesi alınamıyor" msgid "Unable to mark image" msgstr "İmaj iÅŸaretlenemiyor" msgid "Unable to modify package" msgstr "Paket deÄŸiÅŸtirilemiyor" msgid "Unable to remove metadata" msgstr "Metaveri kaldırılamıyor" msgid "Unable to remove package." msgstr "Paket kaldırılamıyor." msgid "Unable to retrieve availability zones." msgstr "Kullanılırlık bölgeleri alınamıyor." msgid "Unable to retrieve details for service" msgstr "Servis ayrıntıları alınamıyor" msgid "Unable to retrieve list of deployments" msgstr "YerleÅŸtirmelerin listesi alınamıyor" msgid "Unable to retrieve list of images" msgstr "İmaj listesi alınamadı" msgid "" "Unable to retrieve list of services. This environment is deploying or " "already deployed by other user." msgstr "" "Servis listesi alınamıyor. Bu ortam yerleÅŸtiriliyor ya da baÅŸka bir " "kullanıcı tarafından zaten yerleÅŸtirilmiÅŸ." msgid "Unable to retrieve package details." msgstr "Paket ayrıntıları alınamıyor." msgid "Unable to retrieve project list." msgstr "Proje listesi alınamadı." msgid "Unable to retrieve public images." msgstr "Açık imajlar alınamıyor." msgid "Unavailable" msgstr "Kullanılmaz" msgid "Unknown" msgstr "Bilinmeyen" msgid "Update" msgstr "Güncelle" msgid "Update Environment" msgid_plural "Deploy Environments" msgstr[0] "Ortamı Güncelle" msgstr[1] "Ortamları Güncelle" msgid "Update Image" msgstr "İmajı Güncelle" msgid "Update Metadata" msgstr "Metaveriyi Güncelle" msgid "Update This Environment" msgstr "Bu Ortamı Güncelle" msgid "Updated" msgstr "Güncellendi" msgid "Updated Environment" msgid_plural "Deployed Environments" msgstr[0] "Ortam Güncellendi" msgstr[1] "Ortamlar Güncellendi" msgid "Uploading package failed. {0}" msgstr "Paketin yüklenmesi baÅŸarısız. {0}" msgid "Used for identifying and filtering packages." msgstr "Paketlerin tanınması ve süzülmesi için kullanılır." msgid "Validation Error occurred" msgstr "DoÄŸrulama Hatası oluÅŸtu" msgid "Version" msgstr "Sürüm" msgid "Version of the package (optional)." msgstr "Paket sürümü (isteÄŸe baÄŸlı)." msgid "You are not allowed to change this properties of the package" msgstr "Paketin bu özelliklerini deÄŸiÅŸtirme yetkiniz yok" msgid "You are not allowed to delete this package" msgstr "Bu paketi silme yetkiniz yok" msgid "You are not allowed to make packages public." msgstr "Paketleri açık hale getirme yetkiniz yok." msgid "You are not allowed to perform this operation" msgstr "Bu iÅŸlemi yapmaya izniniz yok" msgid "" "You'll have to configure each package installed from this bundle separately." msgstr "" "Bu paket yığınıyla kurulmuÅŸ her paketi ayrı ayrı yapılandırmanız gerekecek." msgid "{0}{1} don't match" msgstr "{0}{1} eÅŸleÅŸmiyor" murano-dashboard-5.0.0/muranodashboard/locale/tr_TR/LC_MESSAGES/djangojs.po0000666000175100017510000000407113245511125026357 0ustar zuulzuul00000000000000# iÅŸbaran akçayır , 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 \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ı" murano-dashboard-5.0.0/muranodashboard/locale/fr/0000775000175100017510000000000013245511556022014 5ustar zuulzuul00000000000000murano-dashboard-5.0.0/muranodashboard/locale/fr/LC_MESSAGES/0000775000175100017510000000000013245511556023601 5ustar zuulzuul00000000000000murano-dashboard-5.0.0/muranodashboard/locale/fr/LC_MESSAGES/django.po0000666000175100017510000004727313245511125025412 0ustar zuulzuul00000000000000# Andreas Jaeger , 2016. #zanata # Gérald LONLAS , 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 \n" "Language-Team: French\n" "Language: fr\n" "X-Generator: Zanata 3.9.6\n" "Plural-Forms: nplurals=2; plural=(n > 1)\n" msgid "-" msgstr "-" msgid "80 characters max." msgstr "80 caractères max." msgid "A local zip file to upload" msgstr "Fichier Zip local à envoyer" msgid "Abandon Environment" msgid_plural "Abandon Environments" msgstr[0] "Abandonner l'environnement" msgstr[1] "Abandonner les environnements" msgid "Abandoned Environment" msgid_plural "Abandoned Environments" msgstr[0] "Environnement abandonné" msgstr[1] "Environnements abandonnés" msgid "Active" msgstr "Active" msgid "Add" msgstr "Ajouter" msgid "Add Application" msgstr "Ajouter une application" msgid "Add Application Category" msgstr "Ajouter une catégorie d'application." msgid "Add Category" msgstr "Ajouter une catégorie" msgid "Add Component" msgstr "Ajout d'un composant" msgid "Add Murano Metadata" msgstr "Ajouter des métadonnées Murano" msgid "Add New" msgstr "Ajouter un nouveau" msgid "Add new category to the application catalog." msgstr "Ajouter une nouvelle catégorie au catalogue de l'application" msgid "Add to Env" msgstr "Ajouter à l'Env" msgid "Adding application to an environment failed." msgstr "Échec de l'ajout de l'application à l'environnement." msgid "All" msgstr "Tout" msgid "An external http/https URL to load the bundle from." msgstr "Une URL externe HTTP/HTTPS à partir de laquelle charger le bundle." msgid "An external http/https URL to load the package from." msgstr "Une URL externe HTTP/HTTPS à partir de laquelle charger le paquet." msgid "App Catalog" msgstr "Catalogue d'application" msgid "App Category:" msgstr "Catégorie de l'app :" msgid "App category" msgstr "Catégorie de l'app" msgid "Application Categories" msgstr "Catégories d'application." msgid "Application Category" msgstr "Catégorie de l'application" msgid "Application Details" msgstr "Détails de l'application" msgid "Application Package" msgstr "Package d'application" msgid "Application Components" msgstr "Composants d'applications" msgid "Applications" msgstr "Applications" msgid "Author" msgstr "Auteur" msgid "Auto" msgstr "Auto" msgid "Back" msgstr "Retour" msgid "Browse" msgstr "Parcourir" msgid "Browse Local" msgstr "Parcourir le dossier local" msgid "Bundle creation failed.Reason: {0}" msgstr "La création du bundle a échoué. Raison : {0}" msgid "Cancel" msgstr "Annuler" msgid "Categories" msgstr "Catégories" msgid "Category Name" msgstr "Nom de la catégorie" msgid "Category {0} created." msgstr "Catégorie {0} créée." msgid "Check Keystone configuration of murano-api server." msgstr "Vérifiez la configuration de Keystone du service murano-api." msgid "Choose a Zip archive to upload into the catalog." msgstr "Choisir une archive Zip pour l'envoi dans le catalogue." msgid "Choose a name for the environment" msgstr "Choisir un nom pour cet environnement" msgid "Click to create new environment" msgstr "Cliquer pour créer un nouvel environnement" msgid "Completed with warnings" msgstr "Terminé avec avertissements" msgid "Component" msgstr "Composant" msgid "Component Details" msgstr "Détails du composant" msgid "Component List" msgstr "Liste des composants" msgid "Component Logs" msgstr "Journaux du composant" msgid "Components" msgstr "Composants " msgid "Configuration" msgstr "Configuration" msgid "Configure Application" msgstr "Configurer l'application" msgid "Confirm password" msgstr "Confirmer le mot de passe" msgid "Couldn't update package {0} parameters. Error: {1}" msgstr "Impossible de mettre à jour les paramètres du paquet {0}. Erreur : {1}" msgid "Create" msgstr "Créer" msgid "Create Env" msgstr "Créer un Env" msgid "Create Environment" msgstr "Créer un environnement" msgid "Create New" msgstr "Créer un nouveau" msgid "Create a title for an image." msgstr "Créer un titre pour une image" msgid "Created" msgstr "Créé" msgid "Custom Type" msgstr "Type personnalisée" msgid "Delete Category" msgid_plural "Delete Categories" msgstr[0] "Supprimer la catégorie." msgstr[1] "Supprimer les catégories." msgid "Delete Component" msgid_plural "Delete Components" msgstr[0] "Supprimer un composant" msgstr[1] "Supprimer les composants" msgid "Delete Environment" msgid_plural "Delete Environments" msgstr[0] "Supprimer l'environnement" msgstr[1] "Supprimer les environnements" msgid "Delete Metadata" msgid_plural "Delete Metadata" msgstr[0] "Supprimer les Metadata" msgstr[1] "Supprimer les Metadata" msgid "Delete Package" msgid_plural "Delete Packages" msgstr[0] "Supprimer le package" msgstr[1] "Supprimer les packages" msgid "Delete failure" msgstr "Échec de la suppression" msgid "Deleted Category" msgid_plural "Deleted Categories" msgstr[0] "Catégorie supprimée." msgstr[1] "Catégories supprimées." msgid "Deleted Metadata" msgid_plural "Deleted Metadata" msgstr[0] "Metadata en cours de suppression" msgstr[1] "Metadata en cours de suppression" msgid "Deleted Package" msgid_plural "Deleted Packages" msgstr[0] "Supprimer le package" msgstr[1] "Supprimer les packages" msgid "Deleting" msgstr "Suppression en cours" msgid "Deploy Environment" msgid_plural "Deploy Environments" msgstr[0] "Déployer l'environnement" msgstr[1] "Déployer les environnements" msgid "Deploy This Environment" msgstr "Déployer cet environnement" msgid "Deploy failure" msgstr "Échec du déploiement" msgid "Deploy started" msgstr "Déploiement commencé" msgid "Deployed Components" msgstr "Composants déployés" msgid "Deploying" msgstr "En cours de déploiement" msgid "Deployment Details" msgstr "Détail des déploiements" msgid "Deployment History" msgstr "Historique du déploiement" msgid "Deployment Logs" msgstr "Journaux de déploiement" #, python-format msgid "Deployment with id %s doesn't exist anymore" msgstr "Déploiement avec l'ID %s n'existe plus" msgid "Deployments" msgstr "Déploiements" msgid "Description" msgstr "Description" msgid "Details" msgstr "Détails" msgid "Download Package" msgstr "Télécharger le paquet" msgid "Drop Components here" msgstr "Déposer les composants ici" msgid "Enabled" msgstr "Activé" msgid "" "Enter a complex password with at least one letter, one number and one " "special character" msgstr "" "Entrer un mot de passe complexe comprenant au moins une lettre, " "un nombre et un caractère spécial" msgid "Enter a password" msgstr "Entrez un mot de passe" msgid "Environment" msgstr "Environnement" msgid "Environment Name" msgstr "Nom de l'environnement" #, python-format msgid "Environment with id %s doesn't exist anymore" msgstr "Environnement avec l'ID %s n'existe plus" msgid "Environments" msgstr "Environnements" msgid "FQN" msgstr "FQN" msgid "Failed" msgstr "Échec" msgid "Failed to create environment" msgstr "Échec de la création de l'environnement " msgid "File" msgstr "Fichier" msgid "Filter" msgstr "Filtrer" msgid "Find in a selected category" msgstr "Trouvé dans la catégorie sélectionnée" msgid "Foo" msgstr "Foo" #, python-format msgid "" "Go to Packages , click 'Import Package' " "and select Repository as Package Source." msgstr "" "Aller à Paquets , cliquez sur 'Importer " "un paquet' et sélectionnez Dépôt en tant que source de paquet." 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" murano-dashboard-5.0.0/muranodashboard/locale/cs/0000775000175100017510000000000013245511556022012 5ustar zuulzuul00000000000000murano-dashboard-5.0.0/muranodashboard/locale/cs/LC_MESSAGES/0000775000175100017510000000000013245511556023577 5ustar zuulzuul00000000000000murano-dashboard-5.0.0/muranodashboard/locale/cs/LC_MESSAGES/django.po0000666000175100017510000007117313245511125025404 0ustar zuulzuul00000000000000# Lenka Husáková , 2016. #zanata # Stanislav Ulrych , 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-22 08:52+0000\n" "Last-Translator: Stanislav Ulrych \n" "Language-Team: Czech\n" "Language: cs\n" "X-Generator: Zanata 3.9.6\n" "Plural-Forms: nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2\n" msgid "-" msgstr "-" msgid "80 characters max." msgstr "Max. 80 znaků" msgid "A local zip file to upload" msgstr "Lokální soubor zip pro nahrání" msgid "Abandon Environment" msgid_plural "Abandon Environments" msgstr[0] "Opustit prostÅ™edí" msgstr[1] "Opustit prostÅ™edí" msgstr[2] "Opustit prostÅ™edí" msgid "Abandoned Environment" msgid_plural "Abandoned Environments" msgstr[0] "ProstÅ™edí opuÅ¡tÄ›no" msgstr[1] "ProstÅ™edí opuÅ¡tÄ›na" msgstr[2] "ProstÅ™edí opuÅ¡tÄ›na" msgid "Active" msgstr "Aktivní" msgid "Add" msgstr "PÅ™idat" msgid "Add Application" msgstr "PÅ™idat aplikaci" msgid "Add Application Category" msgstr "PÅ™idat aplikaÄní kategorii" msgid "Add Category" msgstr "PÅ™idat kategorii" msgid "Add Component" msgstr "PÅ™idat komponentu" msgid "Add Murano Metadata" msgstr "PÅ™idat Murano metadata" msgid "Add New" msgstr "PÅ™idat nový" msgid "Add new category to the application catalog." msgstr "PÅ™idat novou kategorii do katalogu aplikací." msgid "Add to Env" msgstr "PÅ™idat do prostÅ™edí" msgid "Adding application to an environment failed." msgstr "PÅ™idávání aplikace do prostÅ™edí selhalo." msgid "All" msgstr "VÅ¡e" msgid "Allows adding additional information about a package." msgstr "Umožňuje pÅ™idávání dodateÄných informací o balíÄku." msgid "" "Allows to hide a package from the catalog. (Applies to package dependencies)" msgstr "Umožňuje skrýt balíÄek v katalogu. (Platí pro závislosti balíÄku)" msgid "" "An environment is a collection of applications that are meant to operate " "under similar conditions." msgstr "" "ProstÅ™edí je soubor aplikací, které jsou urÄeny k provozu za podobných " "podmínek." msgid "An external http/https URL to load the bundle from." msgstr "Externí http/https URL, z které nahrát bundle." msgid "An external http/https URL to load the package from." msgstr "Externí http/https URL, z které nahrát balíÄek." msgid "App Catalog" msgstr "Katalog aplikací" msgid "App Category:" msgstr "AplikaÄní kategorie:" msgid "App category" msgstr "AplikaÄní kategorie" msgid "Application Categories" msgstr "Kategorie aplikací" msgid "Application Category" msgstr "Kategorie aplikací" msgid "Application Details" msgstr "Detaily aplikace" msgid "Application Package" msgstr "BalíÄek aplikací" msgid "Application Components" msgstr "Aplikace Komponenty" msgid "Applications" msgstr "Aplikace" msgid "Author" msgstr "Autor" msgid "Auto" msgstr "Automaticky" msgid "Back" msgstr "ZpÄ›t" msgid "Browse" msgstr "Prohlížet" msgid "Browse Local" msgstr "Procházet lokální" msgid "Bundle Name" msgstr "Název bundle" msgid "Bundle URL" msgstr "Bundle URL" msgid "Bundle creation failed.Reason: Can't find Bundle name from repository." msgstr "" "VytvoÅ™ení bundlu selhalo. Důvod: Nelze najít název bundlu v repositáři." msgid "Bundle creation failed.Reason: {0}" msgstr "VytvoÅ™ení bundlu selhalo. Důvod: {0}" msgid "Bundle successfully imported." msgstr "Bundle úspěšnÄ› importován." msgid "Bundle's full name." msgstr "Celý název bundlu." msgid "Cancel" msgstr "ZruÅ¡it" msgid "Categories" msgstr "Kategorie" msgid "Category Name" msgstr "Název kategorie" msgid "Category {0} created." msgstr "Kategorie {0} vytvoÅ™ena." msgid "Check Keystone configuration of murano-api server." msgstr "Zkontrolovat konfiguraci Keystone pro murano-api server." msgid "Choose a Zip archive to upload into the catalog." msgstr "Zvolte archiv zip pro nahrání do katalogu." msgid "Choose a name for the environment" msgstr "Zvolte název prostÅ™edí" msgctxt "Package requirements" msgid "Classes definition folder" msgstr "Složka definic tříd" msgid "Click to create new environment" msgstr "KliknÄ›te pro vytvoÅ™ení nového prostÅ™edí" msgid "Completed with warnings" msgstr "DokonÄeno s varováním" msgid "Component" msgstr "Kompomenta" msgid "Component Details" msgstr "Detaily kompoment" msgid "Component List" msgstr "Seznam komponent" msgid "Component Logs" msgstr "Logy komponent" msgid "Components" msgstr "Komponenty" msgid "Configuration" msgstr "Konfigurace" msgid "Configure Application" msgstr "Nastavit aplikaci" msgid "Confirm password" msgstr "Potvrdit heslo" msgid "Could not retrieve latest status for the {0} environment" msgstr "Nelze naÄíst aktuální stav pro prostÅ™edí {0}" msgid "" "Couldn't find any apps, required for this field.\n" "Tried: {fqns}" msgstr "" "Nelze nalézt žádné aplikace, vyžadovány pro toto pole.\n" "VyzkouÅ¡eno: {fqns}" msgid "" "Couldn't initialise glance v1 client, therefore could not make the following " "images public: {0}" msgstr "" "Nelze inicializovat klienta glance v1, proto nebylo možné zveÅ™ejnit " "následující obrazy: {0}" msgid "Couldn't update environment. Reason: This name is already taken." msgstr "Nelze aktualizovat prostÅ™edí. Důvod: Název již je používán." msgid "Couldn't update package {0} parameters. Error: {1}" msgstr "Nebylo možné aktualizovat parametry balíÄku {0}. Chyba: {1} " msgid "Create" msgstr "VytvoÅ™it" msgid "Create Env" msgstr "VytvoÅ™it prostÅ™edí" msgid "Create Environment" msgstr "VytvoÅ™it prostÅ™edí" msgid "Create New" msgstr "VytvoÅ™it nový" msgid "Create a title for an image." msgstr "VytvoÅ™it název pro obraz." msgid "Created" msgstr "VytvoÅ™eno" msgid "Custom Type" msgstr "Vlastní typ" msgid "" "Defines whether or not a package can be used by other tenants. (Applies to " "package dependencies)" msgstr "" "UrÄete, zda je balíÄek možné použít jinými tenanty. (Platí také pro " "závislosti balíÄku)" msgid "Delete Category" msgid_plural "Delete Categories" msgstr[0] "Smazat kategorii" msgstr[1] "Smazat kategorie" msgstr[2] "Smazat kategorie" msgid "Delete Component" msgid_plural "Delete Components" msgstr[0] "Smazat komponent" msgstr[1] "Smazat komponenty" msgstr[2] "Smazat komponenty" msgid "Delete Environment" msgid_plural "Delete Environments" msgstr[0] "Smazat prostÅ™edí" msgstr[1] "Smazat prostÅ™edí" msgstr[2] "Smazat prostÅ™edí" msgid "Delete Metadata" msgid_plural "Delete Metadata" msgstr[0] "Odstranit metadata" msgstr[1] "Odstranit metadata" msgstr[2] "Odstranit metadata" msgid "Delete Package" msgid_plural "Delete Packages" msgstr[0] "Smazat balíÄek" msgstr[1] "Smazat balíÄky" msgstr[2] "Smazat balíÄky" msgid "Delete failure" msgstr "Mazáni selhalo" msgid "Deleted Category" msgid_plural "Deleted Categories" msgstr[0] "Kategorie smazána" msgstr[1] "Kategorie smazány" msgstr[2] "Kategorií smazány" msgid "Deleted Metadata" msgid_plural "Deleted Metadata" msgstr[0] "Metadata odstranÄ›ny" msgstr[1] "Metadata odstranÄ›ny" msgstr[2] "Metadata odstranÄ›ny" msgid "Deleted Package" msgid_plural "Deleted Packages" msgstr[0] "BalíÄek smazán" msgstr[1] "BalíÄky smazány" msgstr[2] "BalíÄky smazány" msgid "Deleting" msgstr "Mazání" msgid "Deploy Environment" msgid_plural "Deploy Environments" msgstr[0] "Nasadit prostÅ™edí" msgstr[1] "Nasadit prostÅ™edí" msgstr[2] "Nasadit prostÅ™edí" msgid "Deploy This Environment" msgstr "Nasadit toto prostÅ™edí" msgid "Deploy failure" msgstr "Nasazení selhalo" msgid "Deploy started" msgstr "Nasazení zahájeno" msgid "Deployed Components" msgstr "Komponenty nasazeny" msgid "Deploying" msgstr "Nasazování" msgid "Deployment Details" msgstr "Podrobnosti nasazení" msgid "Deployment History" msgstr "Historie nasazení" msgid "Deployment Logs" msgstr "Logy nasazení" #, python-format msgid "Deployment with id %s doesn't exist anymore" msgstr "Nasazení s id %s již neexistuje" msgid "Deployment with id foo_deployment_id doesn't exist anymore" msgstr "Nasazení s id foo_deployment_id již neexistuje" msgid "Deployments" msgstr "Nasazení" msgid "Description" msgstr "Popis" msgid "Details" msgstr "Detaily" msgid "Download Package" msgstr "Stáhnout balíÄek" msgid "Drop Components here" msgstr "Zde vložte komponenty" msgid "Enabled" msgstr "Povoleno" msgid "" "Enter a complex password with at least one letter, one number and one " "special character" msgstr "" "Zadejte bezpeÄné heslo s alespoň jedním písmenem, jedním Äíslem a jedním " "speciálním znakem" msgid "Enter a password" msgstr "Zadat heslo" msgid "Enter an image type supported by Murano." msgstr "Zvolte typ obrazu, který je podporovaný v Murano." msgid "Environment" msgstr "ProstÅ™edí" msgid "Environment Default Network" msgstr "Výchozí síť prostÅ™edí" msgid "Environment Name" msgstr "Název prostÅ™edí" msgid "Environment name must contain at least one non-white space symbol." msgstr "Název prostÅ™edí musí obsahovat alespoň jeden ne-prázdný symbol" #, python-format msgid "Environment with id %s doesn't exist anymore" msgstr "ProstÅ™edí s id %s již neexistuje" msgid "Environment with specified name already exists" msgstr "ProstÅ™edí s daným jménem už existuje" msgid "Environments" msgstr "ProstÅ™edí" msgid "Error test_error_message occurred while installing package bar" msgstr "PÅ™i instalaci balíÄku bar nastala chyba test_error_message" msgid "Error {0} occurred while installing images for {1}" msgstr "Chyba {0} nastala pÅ™i instalaci obrazů pro {1}" msgid "Error {0} occurred while installing package {1}" msgstr "PÅ™i instalaci balíÄku {1} nastala chyba {0}" msgid "Error {0} occurred while parsing package {1}" msgstr "Chyba {0} nastala pÅ™i parsování balíÄku {1}" msgid "Error {0} occurred while setting image {1}, {2} public" msgstr "PÅ™i nastavování obrazu {1} na veÅ™ejný {2} nastala chyba {0}" msgctxt "Package requirements" msgid "Execution plans folder" msgstr "Složka plánů provedení" msgid "FQN" msgstr "FQN" msgid "Failed" msgstr "Selhalo" msgid "Failed to create environment" msgstr "Nelze vytvoÅ™it prostÅ™edí" msgid "Failed to modify the package. {0}" msgstr "Nelze zmÄ›nit balíÄek. {0}" msgid "File" msgstr "Soubor" msgid "Filter" msgstr "Filtr" msgid "Find in a selected category" msgstr "Nalézt ve zvolené kategorii" 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 "" "První symbol by mÄ›lo být písmeno nebo podtržítko. Následné symboly mohou být " "písmeno, Äíslo, podtržítko, zavinÃ¡Ä a dolar" msgid "Foo" msgstr "Foo" msgid "Fully qualified package name." msgstr "PlnÄ› kvalifikovaný název balíÄku." #, python-format msgid "" "Go to Packages , click 'Import Package' " "and select Repository as Package Source." msgstr "" "PÅ™ejít do BalíÄky, kliknÄ›te 'Import " "balíÄků' a vyberte Repositář jako Zdroj balíÄku. " msgid "HTTP/HTTPS URL of the bundle file." msgstr "HTTP/HTTPS URL pro soubor bundle." msgid "HTTP/HTTPS URL of the package file." msgstr "HTTP/HTTPS URL pro soubor balíÄku." msgid "Heat Orchestration stack name" msgstr "Název Heat stack orchestrace " msgid "Heat Orchestration stack%(forloop.counter)s name" msgstr "Heat orchestration stack%(forloop.counter)s název" 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 "" "Pokud jsou balíÄky na sobÄ› závislé a / nebo vyžadují specifické glance " "obrazy, tak budou nainstalovány z murano repozitáře" 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 "" "Pokud balíÄek závisí na jiném balíÄku a/nebo vyžaduje specifické glance " "obrazy, tak bude s ním nainstalován z murano repozitáře." msgid "Image" msgstr "Obraz" msgid "Image Title" msgstr "Název obrazu" msgid "Image Type" msgstr "Typ obrazu" msgid "Image successfully marked" msgstr "Obraz úspěšnÄ› oznaÄen" msgid "Images" msgstr "Obrazy" msgid "Import Bundle" msgstr "Importovat bundle." msgid "Import Package" msgstr "Importovat balíÄek" msgid "Importing package {0} failed. Reason: {1}" msgstr "Importování balíÄku {0} selhalo. Důvod: {1}" msgid "Info" msgstr "Informace" msgid "Instance name" msgstr "Název instance" msgid "Instance%(forloop.counter)s name" msgstr "Název instance%(forloop.counter)s " msgid "Invalid metadata for image: {0}" msgstr "Neplatná metadata pro obraz: {0}" msgid "Invalid murano image metadata" msgstr "Neplatná metadata obrazu murano." msgid "Invalid value of 'murano_nets' option" msgstr "Neplatná hodnota parametru 'murano_nets' " msgid "It is forbidden to upload files larger than {0} MB." msgstr "Je zakázáno nahrávání souborů vÄ›tších než {0} MB." msgid "KeyWord" msgstr "KlíÄové slovo" msgid "Last operation" msgstr "Poslední operace" msgid "Latest Deployment Log" msgstr "Log posledního nasazení" msgid "License" msgstr "Licence" msgid "Logs" msgstr "Logy" msgid "Manage" msgstr "Spravovat" msgid "Manage Components" msgstr "Spravovat komponenty" msgctxt "Package requirements" msgid "Manifest file" msgstr "Soubor Manifest" msgid "Mark Image" msgstr "OznaÄit obraz" msgid "" "Mark an image with Murano specific metadata to be added to the selected " "image." msgstr "" "OznaÄte obraz se specifickými metadaty Murano pro pÅ™idání do zvoleného obrazu" msgid "Marked Images" msgstr "OznaÄené obrazy" msgid "Modify Package" msgstr "Upravit balíÄek" msgid "Modifying package failed" msgstr "Úprava balíÄku selhala" msgid "NO ENVIRONMENTS" msgstr "ŽÃDNà PROSTŘEDÃ" msgid "Name" msgstr "Název" msgid "Name of the bundle." msgstr "Název bundle." #, python-format msgid "Network of '%s'" msgstr "SítÄ› '%s'" msgid "Next" msgstr "Další" msgid "Next Page" msgstr "Další strana" msgid "No availability zones available" msgstr "Zóny dostupnosti nejsou k dispozici" msgid "No categories available" msgstr "Žádné dostupné kategorie" msgid "No components" msgstr "Žádné komponenty" msgid "No images available" msgstr "Žádné obrazy nejsou dostupné" msgid "No keypair" msgstr "Žádný klíÄ" msgid "No license" msgstr "Bez licence" msgid "No recent activity to report at this time." msgstr "Žádné nedávné aktivity." msgid "No requirements" msgstr "Bez požadavků" msgid "None" msgstr "Žádný" msgid "Not in domain" msgstr "Není v doménÄ›" msgid "Note" msgstr "Poznámka" msgid "" "OpenStack Networking (Neutron) is not available in current environment. " "Custom Network Settings cannot be applied" msgstr "" "Služba sítí OpenStack (Neutron) je nedostupná v souÄasném prostÅ™edí. Vlastní " "nastavení sítÄ› nelze použít" msgid "Operation is forbidden by murano-api server." msgstr "Operace je zakázána murano-api serverem." msgid "Optional" msgstr "Volitelné" msgid "Overview" msgstr "PÅ™ehled" msgid "Package Bundle Source" msgstr "Zdroj bundle balíÄku" msgid "Package Count" msgstr "PoÄet balíÄků" msgid "Package Details" msgstr "Detaily balíÄku" msgid "Package Name" msgstr "Název balíÄku" msgid "Package Source" msgstr "Zdroj balíÄků" msgid "Package Tags" msgstr "Tagy balíÄku" msgid "Package URL" msgstr "URL balíÄku" msgid "Package Version" msgstr "Verze balíÄku" msgid "" "Package creation failed.Reason: Can't find Package name from repository." msgstr "" "Vytváření balíÄku selhalo. Důvod: Nelze najít název balíÄku v repositáři." msgid "Package creation failed.Reason: {0}" msgstr "Vytváření balíÄku selhalo. Důvod: {0}" msgid "Package foo uploaded" msgstr "BalíÄek foo nahrán" msgid "Package modified." msgstr "BalíÄek zmÄ›nÄ›n." msgid "Package name in the repository, usually a fully qualified name" msgstr "Název balíÄku v repozitáři, obvykle plnÄ› kvalifikovaný název" msgid "Package or Class with the same name is already made public" msgstr "BalíÄek nebo Třída s tímto názvem již byly zveÅ™ejnÄ›ny" msgid "Package parameters successfully updated." msgstr "Parametry balíÄku úspěšnÄ› aktualizovány." msgid "Package version" msgstr "Verze balíÄku" msgid "Package with id foo_package_id is not found" msgstr "BalíÄek s id foo_package_id nenalezen" msgid "Package with id {0} is not found" msgstr "BalíÄek s id {0} nenalezen" msgid "Package with specified name already exists" msgstr "BalíÄek s tímto názvem již existuje" msgid "Package {0} already registered." msgstr "BalíÄek {0} je již registrován." msgid "Package {0} upload failed. {1}" msgstr "Nahrávání balíÄku {0} selhalo. {1}" msgid "Package {0} uploaded" msgstr "BalíÄek {0} nahrán" msgid "Packages" msgstr "BalíÄky" msgid "Packages should contain:" msgstr "BalíÄky by mÄ›ly obsahovat:" msgid "Please confirm your password" msgstr "PotvrÄte prosím vaÅ¡e heslo" msgid "Please supply a bundle name" msgstr "Prosím zadejte název bundle." msgid "Please supply a bundle url" msgstr "Prosím zadejte url bundle." msgid "Please supply a package file" msgstr "Prosím zadejte soubor balíÄku" msgid "Please supply a package name" msgstr "Prosím zadejte název balíÄku" msgid "Please supply a package url" msgstr "Prosím zadejte url balíÄku" msgid "Previous Page" msgstr "PÅ™edchozí strana" msgid "Provide comma-separated list of words, associated with the package" msgstr "PoskytnÄ›te Äárkami oddÄ›lený seznam slov asociovaných s balíÄkem" msgid "Provide desired name for a new category" msgstr "Zadejte požadovaný název pro novou kategorii" msgid "Public" msgstr "VeÅ™ejné" msgid "Quick Deploy" msgstr "Rychlé nasazení" msgid "Ready" msgstr "PÅ™ipraven" msgid "Ready to configure" msgstr "PÅ™ipraveno ke konfiguraci" msgid "Ready to deploy" msgstr "PÅ™ipraven k nasazení" msgid "Recent Activity" msgstr "Poslední aktivita" msgid "Repository" msgstr "Repozitář" msgid "Requested object is not found on murano server." msgstr "Požadovaný objekt nebyl nalezen na murano serveru." msgid "Requested operation conflicts with an existing object." msgstr "Požadovaná operace je v rozporu s již existujícím objektem." msgid "Requirements" msgstr "Požadavky" msgid "Retype your password" msgstr "PÅ™epiÅ¡te své heslo" msgid "Running" msgstr "Běží" msgid "Running with errors" msgstr "Beží s chybami" msgid "Running with warnings" msgstr "Běží s varováním" msgid "Select Application" msgstr "Vyberte aplikaci" msgid "Select Image" msgstr "Vyberte obraz" msgid "Select an image registered in Glance Image Services." msgstr "Vyberte obraz registrovaný v Glance Image Services." msgid "" "Select an image type supported by Murano. Choose 'Custom type' to enter type " "manually." msgstr "" "Zvolte typ obrazu, který je podporovaný v Murano. Zvolte 'Vlastní typ' k " "zadání vlastního typu." msgid "Select one or more categories for a package." msgstr "Vybrat jednu Äi více kategorií pro balíÄek." msgid "Set up for identifying a package." msgstr "Nastavení pro identifikaci balíÄku." msgid "Show Details" msgstr "Zobrazit detaily" msgid "Something went wrong during package downloading" msgstr "NÄ›co se pokazilo pÅ™i stahování balíÄku" msgid "Sorry, this environment doesn't exist anymore" msgstr "Omlouváme se, toto prostÅ™edí již neexistuje." msgid "" "Sorry, you can't add application right now. The environment is deploying." msgstr "" "Omlouvám se, momentálnÄ› nemůžete pÅ™idat aplikaci. ProstÅ™edí se vytváří." msgid "Sorry, you can't delete service right now" msgstr "Omlouvám se, momentálnÄ› nelze smazat službu" msgid "Specified title already in use. Please choose another one." msgstr "Tento název je již používán. Prosím zvolte jiný název." msgid "Specifying a category helps to filter applications in the catalog" msgstr "Specifikace kategorie pomáhá pÅ™i filtrování aplikací v katalogu." msgid "Started Deleting Component" msgid_plural "Started Deleting Components" msgstr[0] "SpuÅ¡tÄ›no mazání komponenty" msgstr[1] "SpuÅ¡tÄ›no mazání komponent" msgstr[2] "SpuÅ¡tÄ›no mazání komponent" msgid "Started Deleting Environment" msgid_plural "Started Deleting Environments" msgstr[0] "SpuÅ¡tÄ›no mazání prostÅ™edí" msgstr[1] "SpuÅ¡tÄ›no mazání prostÅ™edí" msgstr[2] "SpuÅ¡tÄ›no mazání prostÅ™edí" msgid "Started deploying Environment" msgid_plural "Started deploying Environments" msgstr[0] "Nasazování prostÅ™edí spuÅ¡tÄ›no" msgstr[1] "Nasazování prostÅ™edí spuÅ¡tÄ›no" msgstr[2] "Nasazování prostÅ™edí spuÅ¡tÄ›no" msgid "Status" msgstr "Stav" msgid "Step {0}" msgstr "Krok {0}" msgid "Successful" msgstr "ÚspěšnÄ›" msgid "Tags" msgstr "Tagy" msgid "Tenant Name" msgstr "Název tenanta" msgid "The '{0}' application successfully added to environment." msgstr "Aplikace '{0}' byla úspěšnÄ› pÅ™idána do prostÅ™edí." 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 "" "VM aplikací v tomto prostÅ™edí se automaticky pÅ™ipojí k této síti, pokud není " "nakonfigurováno jinak. Volbou \"VytvoÅ™it nový\" vygeneruje novou síť s " "podsítí, která má rozsah IP alokovaný z dostupných pro výchozí router Murano " "v tomto projektu. " #, python-format msgid "" "The bundle is going to be installed from %(murano_repo_url)s repository." msgstr "" "Bundle bude instalován z " "%(murano_repo_url)s repositáře." #, python-format msgid "" "The package is going to be imported from %(murano_repo_url)s repository." msgstr "" "BalíÄek bude importován z " "%(murano_repo_url)s repositáře." msgid "" "The password must contain at least one letter, " "one number and one special character" msgstr "" "Heslo musí obsahovat alespoň jedno písmeno, jedno Äíslo a jeden speciální " "znak." msgid "The request data is not acceptable by the server" msgstr "Požadovaná data nejsou pÅ™ijatelná serverem" msgid "There are no applications in the catalog. You can import apps from" msgstr "V katalogu nejsou žádné aplikace. Aplikace můžete importovat z" #, python-format msgid "" "There are no applications in the catalog. You can import apps from %(display_repo_url)s." msgstr "" "V katalogu nejsou žádné aplikace. Aplikace můžete importovat z %(display_repo_url)s." msgid "There are no applications matching your criteria." msgstr "Žádné dostupné aplikace odpovídající vaÅ¡im kritériím." msgid "There was an error communicating with server" msgstr "PÅ™i komunikaci se serverem nastala chyba." msgid "There was an error initialising this field." msgstr "PÅ™i inicializaci tohoto pole nastala chyba." msgid "" "This action cannot be undone. Any resources created by this environment will " "have to be released manually." msgstr "" "Toto nelze vrátit zpÄ›t. VeÅ¡keré zdroje vytvoÅ™ené tímto prostÅ™edím budou " "muset být uvolnÄ›ny ruÄnÄ›." msgid "Time Finished" msgstr "ÄŒas ukonÄení" msgid "Time Started" msgstr "ÄŒas zahájení" msgid "Time updated" msgstr "ÄŒas aktualizován" msgid "Title" msgstr "Název" msgid "Toggle Active" msgid_plural "Toggle Active" msgstr[0] "Aktivovat" msgstr[1] "Aktivovat" msgstr[2] "Aktivovat" msgid "Toggle Enabled" msgstr "PÅ™epnutí povoleno" msgid "Toggle Public" msgid_plural "Toggle Public" msgstr[0] "ZveÅ™ejnit" msgstr[1] "ZveÅ™ejnit" msgstr[2] "ZveÅ™ejnit" msgid "Toggled Active" msgid_plural "Toggled Active" msgstr[0] "Aktivováno" msgstr[1] "Aktivováno" msgstr[2] "Aktivováno" msgid "Toggled Public" msgid_plural "Toggled Public" msgstr[0] "ZveÅ™ejnÄ›no" msgstr[1] "ZveÅ™ejnÄ›no" msgstr[2] "ZveÅ™ejnÄ›no" msgid "Topology" msgstr "Topologie" msgid "" "Trying to add {0} image to glance. Image will be ready for deployment after " "successful upload" msgstr "" "Přídávání obrazu {0} do glance. Obraz bude pÅ™ipraven k nasazení po úspěšném " "nahrání." msgid "" "Trying to add {0}, {1} image to glance. Image will be ready for deployment " "after successful upload" msgstr "" "PÅ™idávání obrazu {0}, {1} obraz do glance. Obraz bude pÅ™ipraven k nasazení " "po úspěšném nahrání." msgid "Type" msgstr "Typ" msgctxt "Package requirements" msgid "UI definition folder" msgstr "Složka UI definic" msgid "UNKNOWN" msgstr "NEZNÃMÉ" msgid "URL" msgstr "URL" msgid "Unable to abandon an environment {0} due to: {1}" msgstr "ProstÅ™edí {0} nelze opustit kvůli: {1}" msgid "Unable to communicate to glare-api server." msgstr "Nelze komunikovat s glare-api serverem." msgid "Unable to communicate to murano-api server." msgstr "Nelze komunikovat s murano-api serverem." msgid "Unable to create environment {0} due to: {1}" msgstr "Nelze vytvoÅ™it prostÅ™edí {0} kvůli: {1}" msgid "Unable to delete category" msgstr "Nelze smazat kategorii" msgid "Unable to delete environment {0} due to: {1}" msgstr "ProstÅ™edí {0} nelze smazat kvůli: {1}" msgid "Unable to delete package in murano-api server" msgstr "Nelze smazat balíÄek na serveru murano-api" msgid "Unable to deploy. Try again later" msgstr "Nelze nasadit. Zkuste pozdÄ›ji" msgid "Unable to download package." msgstr "BalíÄek nelze stáhnout" msgid "Unable to get list of categories" msgstr "Nelze získat seznam kategorií" msgid "Unable to mark image" msgstr "Obraz nelze oznaÄit" msgid "Unable to modify package" msgstr "Nelze upravit balíÄek" msgid "Unable to remove metadata" msgstr "Nelze odstranit metadata" msgid "Unable to remove package." msgstr "BalíÄek nelze odstranit." msgid "Unable to retrieve availability zones." msgstr "Nelze získat zóny dostupnosti." msgid "Unable to retrieve details for service" msgstr "Nelze získat podrobnosti pro službu" msgid "Unable to retrieve list of deployments" msgstr "Nelze získat seznam nasazení" msgid "Unable to retrieve list of images" msgstr "Nelze získat seznam obrazů" msgid "" "Unable to retrieve list of services. This environment is deploying or " "already deployed by other user." msgstr "" "Nelze získat seznam služeb. Toto prostÅ™edí se nasazuje nebo je již nasazeno " "jiným uživatelem." msgid "Unable to retrieve package details." msgstr "Nelze získat detaily balíÄku." msgid "Unable to retrieve project list." msgstr "Nelze získaz seznam projektů." msgid "Unable to retrieve public images." msgstr "Nelze získat veÅ™ejné obrazy." msgid "Unavailable" msgstr "Nedostupné" msgid "Unknown" msgstr "Neznámé" msgid "Update" msgstr "Aktualizovat" msgid "Update Environment" msgid_plural "Deploy Environments" msgstr[0] "Aktualizovat prostÅ™edí" msgstr[1] "Nasadit prostÅ™edí" msgstr[2] "Nasadit prostÅ™edí" msgid "Update Image" msgstr "Aktualizovat obraz" msgid "Update Metadata" msgstr "Aktualizovat metadata" msgid "Update This Environment" msgstr "Aktualizovat toto prostÅ™edí" msgid "Updated" msgstr "Aktualizováno" msgid "Updated Environment" msgid_plural "Deployed Environments" msgstr[0] "ProstÅ™edí aktualizováno" msgstr[1] "ProstÅ™edí nasazena" msgstr[2] "ProstÅ™edí nasazena" msgid "Uploading package failed. {0}" msgstr "Nahrávání balíÄku selhalo. {0}" msgid "Used for identifying and filtering packages." msgstr "Použito pro identifikaci a filtraci balíÄků." msgid "Validation Error occurred" msgstr "DoÅ¡lo k chybÄ› ověření" msgid "Version" msgstr "Verze" msgid "Version of the package (optional)." msgstr "Verze balíÄku (volitelné)." msgid "You are not allowed to change this properties of the package" msgstr "Nemáte oprávnÄ›ní mÄ›nit tyto vlastnosti balíÄku" msgid "You are not allowed to delete this package" msgstr "Nemáte oprávnÄ›ní smazat tento balíÄek" msgid "You are not allowed to perform this operation" msgstr "Nemáte oprávnÄ›ní provést tuto operaci" msgid "" "You'll have to configure each package installed from this bundle separately." msgstr "" "Každý balíÄek nainstalovaný z tohoto bundlu bude nutné nakonfigurovat zvlášť." msgid "{0}{1} don't match" msgstr "{0}{1} se neshodují" murano-dashboard-5.0.0/muranodashboard/locale/cs/LC_MESSAGES/djangojs.po0000666000175100017510000000410013245511125025723 0ustar zuulzuul00000000000000# Stanislav Ulrych , 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 \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Ä›" murano-dashboard-5.0.0/muranodashboard/locale/ru/0000775000175100017510000000000013245511556022033 5ustar zuulzuul00000000000000murano-dashboard-5.0.0/muranodashboard/locale/ru/LC_MESSAGES/0000775000175100017510000000000013245511556023620 5ustar zuulzuul00000000000000murano-dashboard-5.0.0/muranodashboard/locale/ru/LC_MESSAGES/django.po0000666000175100017510000011210513245511125025414 0ustar zuulzuul00000000000000# Aleksey Alekseenko <9118250541@mail.ru>, 2016. #zanata # Alexander , 2016. #zanata # Andreas Jaeger , 2016. #zanata # Irina Kochetova , 2016. #zanata # Yulia Ryndenkova , 2016. #zanata # Artem , 2017. #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: 2017-02-06 09:03+0000\n" "Last-Translator: Artem \n" "Language-Team: Russian\n" "Language: ru\n" "X-Generator: Zanata 3.9.6\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 "-" msgstr "-" msgid "80 characters max." msgstr "макÑимум 80 Ñимволов" msgid "A local zip file to upload" msgstr "Zip файл" msgid "Abandon Environment" msgid_plural "Abandon Environments" msgstr[0] "Забыть об Окружении" msgstr[1] "Забыть об ОкружениÑÑ…" msgstr[2] "Забыть об ОкружениÑÑ…" msgid "Abandoned Environment" msgid_plural "Abandoned Environments" msgstr[0] "Окружение Забыто" msgstr[1] "ÐžÐºÑ€ÑƒÐ¶ÐµÐ½Ð¸Ñ Ð—Ð°Ð±Ñ‹Ñ‚Ñ‹" msgstr[2] "ÐžÐºÑ€ÑƒÐ¶ÐµÐ½Ð¸Ñ Ð—Ð°Ð±Ñ‹Ñ‚Ñ‹" msgid "Active" msgstr "Ðктивный" msgid "Add" msgstr "Добавить" msgid "Add Application" msgstr "Добавить Приложение" msgid "Add Application Category" msgstr "Добавить категорию приложений" msgid "Add Category" msgstr "Добавить Категорию" msgid "Add Component" msgstr "Добавить Компонент" msgid "Add Murano Metadata" msgstr "Добавить Метаданные Murano" msgid "Add New" msgstr "Добавить новый" msgid "Add new category to the application catalog." msgstr "Добавить новую категорию в каталог." msgid "Add to Env" msgstr "Добавить в окружение" msgid "Adding application to an environment failed." msgstr "Произошла ошибка при добавлении Ð¿Ñ€Ð¸Ð»Ð¾Ð¶ÐµÐ½Ð¸Ñ Ð² окружение." msgid "All" msgstr "Ð’Ñе" msgid "Allows adding additional information about a package." msgstr "ПозволÑет добавить дополнительную информацию о пакете." msgid "" "Allows to hide a package from the catalog. (Applies to package dependencies)" msgstr "" "ПозволÑет Ñкрыть пакет из каталога. (Так же применÑетÑÑ Ðº завиÑимоÑÑ‚Ñм " "пакета)" msgid "" "An environment is a collection of applications that are meant to operate " "under similar conditions." msgstr "" "Окружение Ñто ÐºÐ¾Ð»Ð»ÐµÐºÑ†Ð¸Ñ Ð¿Ñ€Ð¸Ð»Ð¾Ð¶ÐµÐ½Ð¸Ð¹, которые работают в одинаковых уÑловиÑÑ…." msgid "An external http/https URL to load the bundle from." msgstr "Внешний http/https URL, Ñ ÐºÐ¾Ñ‚Ð¾Ñ€Ð¾Ð³Ð¾ будет Ñкачан bundle." msgid "An external http/https URL to load the package from." msgstr "Внешний http/https URL Ñ ÐºÐ¾Ñ‚Ð¾Ñ€Ð¾Ð³Ð¾ будет Ñкачан пакет." msgid "App Catalog" msgstr "Каталог приложениÑ" msgid "App Category:" msgstr "ÐšÐ°Ñ‚ÐµÐ³Ð¾Ñ€Ð¸Ñ ÐŸÑ€Ð¸Ð»Ð¾Ð¶ÐµÐ½Ð¸Ñ:" msgid "App category" msgstr "ÐšÐ°Ñ‚ÐµÐ³Ð¾Ñ€Ð¸Ñ Ð¿Ñ€Ð¸Ð»Ð¾Ð¶ÐµÐ½Ð¸Ñ" msgid "Application Categories" msgstr "Категории Приложений" msgid "Application Category" msgstr "Категории Приложений" msgid "Application Details" msgstr "Детали ПриложениÑ" msgid "Application Package" msgstr "Пакет ПриложениÑ" msgid "Application Components" msgstr " Компоненты" msgid "Applications" msgstr "ПриложениÑ" msgid "Author" msgstr "Ðвтор" msgid "Auto" msgstr "Ðвто" msgid "Back" msgstr "Ðазад" msgid "Browse" msgstr "ПроÑмотр" msgid "Bundle Name" msgstr "Ð˜Ð¼Ñ bundle" msgid "Bundle URL" msgstr "URL Bundle" msgid "Bundle creation failed.Reason: Can't find Bundle name from repository." msgstr "" "Ðе удалоÑÑŒ Ñоздать Bundle. Ðе удалоÑÑŒ найти Bundle Ñ Ñ‚Ð°ÐºÐ¸Ð¼ именем в " "репозитории." msgid "Bundle creation failed.Reason: {0}" msgstr "Ðе удалоÑÑŒ Ñоздать Bundle. Причина: {0}" msgid "Bundle successfully imported." msgstr "Bundle уÑпешно импортирован." msgid "Bundle's full name." msgstr "Полное Ð¸Ð¼Ñ Bundle." msgid "Cancel" msgstr "Отменить" msgid "Categories" msgstr "Категории" msgid "Category Name" msgstr "Ð˜Ð¼Ñ ÐšÐ°Ñ‚ÐµÐ³Ð¾Ñ€Ð¸Ð¸" msgid "Category {0} created." msgstr "ÐšÐ°Ñ‚ÐµÐ³Ð¾Ñ€Ð¸Ñ {0} Ñоздана." msgid "Check Keystone configuration of murano-api server." msgstr "Проверьте наÑтройки Keystone Ð´Ð»Ñ Ñервера murano-api." msgid "Choose a Zip archive to upload into the catalog." msgstr "Выберите ZIP архив, Ñодержащий пакет, Ð´Ð»Ñ Ð·Ð°Ð³Ñ€ÑƒÐ·ÐºÐ¸" msgid "Choose a name for the environment" msgstr "Выберите Ð¸Ð¼Ñ Ð´Ð»Ñ Ð¾ÐºÑ€ÑƒÐ¶ÐµÐ½Ð¸Ñ" msgctxt "Package requirements" msgid "Classes definition folder" msgstr "Каталог Ñодержащий Ð¾Ð¿Ñ€ÐµÐ´ÐµÐ»ÐµÐ½Ð¸Ñ ÐºÐ»Ð°ÑÑов" msgid "Click to create new environment" msgstr "Создать новое окружение" msgid "Completed with warnings" msgstr "Завершено Ñ Ð¿Ñ€ÐµÐ´ÑƒÐ¿Ñ€ÐµÐ¶Ð´ÐµÐ½Ð¸Ñми" msgid "Component" msgstr "Компонент" msgid "Component Details" msgstr "Детали Компонентов" msgid "Component List" msgstr "СпиÑок компонентов" msgid "Component Logs" msgstr "Журнал Компонентов" msgid "Components" msgstr "Компоненты" msgid "Configuration" msgstr "КонфигурациÑ" msgid "Configure Application" msgstr "ÐаÑтроить Приложение" msgid "Confirm password" msgstr "Подтвердите пароль" msgid "Could not retrieve latest status for the {0} environment" msgstr "Ðе удалоÑÑŒ получить поÑледний ÑÑ‚Ð°Ñ‚ÑƒÑ Ð´Ð»Ñ Ð¾ÐºÑ€ÑƒÐ¶ÐµÐ½Ð¸Ñ {0}" msgid "" "Couldn't find any apps, required for this field.\n" "Tried: {fqns}" msgstr "" "Ðе найдено каких-либо приложений, необходимых Ð´Ð»Ñ Ñтого полÑ\n" "Попытка: {fqns}" msgid "" "Couldn't initialise glance v1 client, therefore could not make the following " "images public: {0}" msgstr "" "Ðевозможно инициализировать glance v1 клиент, и поÑтому не удалоÑÑŒ Ñделать " "Ñледующие образы публичными: {0}" msgid "Couldn't update environment. Reason: This name is already taken." msgstr "Ðе удалоÑÑŒ обновить окружение. Причина: Ð¸Ð¼Ñ ÑƒÐ¶Ðµ занÑто." msgid "Couldn't update package {0} parameters. Error: {1}" msgstr "Пакет {0} не удалоÑÑŒ обновить. Ошибка: {1}" msgid "Create" msgstr "Создать" msgid "Create Env" msgstr "Создать окружение" msgid "Create Environment" msgstr "Создать Окружение" msgid "Create New" msgstr "Создать новую" msgid "Create a title for an image." msgstr "Выберите Ð¸Ð¼Ñ Ð´Ð»Ñ Ð¾Ð±Ñ€Ð°Ð·Ð°" msgid "Created" msgstr "Создан" msgid "Custom Type" msgstr "ПользовательÑкий тип" msgid "" "Defines whether or not a package can be used by other tenants. (Applies to " "package dependencies)" msgstr "" "ОпределÑет может ли пакет быть иÑпользован из других тенантов. (Так же " "применÑетÑÑ Ðº завиÑимоÑÑ‚Ñм пакета)" msgid "Delete Category" msgid_plural "Delete Categories" msgstr[0] "Удалить Категорию" msgstr[1] "Удалить Категории" msgstr[2] "Удалить Категории" msgid "Delete Component" msgid_plural "Delete Components" msgstr[0] "Удалить компонент" msgstr[1] "Удалить компоненты" msgstr[2] "Удалить компоненты" msgid "Delete Environment" msgid_plural "Delete Environments" msgstr[0] "Удалить Окружение" msgstr[1] "Удалить ОкружениÑ" msgstr[2] "Удалить ОкружениÑ" msgid "Delete Metadata" msgid_plural "Delete Metadata" msgstr[0] "Удалить Метаданные" msgstr[1] "Удалить Метаданные" msgstr[2] "Удалить Метаданные" msgid "Delete Package" msgid_plural "Delete Packages" msgstr[0] "Удалить Пакет" msgstr[1] "Удалить Пакеты" msgstr[2] "Удалить Пакеты" msgid "Delete failure" msgstr "Ðе удалоÑÑŒ удалить" msgid "Deleted Category" msgid_plural "Deleted Categories" msgstr[0] "ÐšÐ°Ñ‚ÐµÐ³Ð¾Ñ€Ð¸Ñ Ð£Ð´Ð°Ð»ÐµÐ½Ð°" msgstr[1] "Категории Удалены" msgstr[2] "Категории Удалены" msgid "Deleted Metadata" msgid_plural "Deleted Metadata" msgstr[0] "Метаданные Удалены" msgstr[1] "Метаданные Удалены" msgstr[2] "Метаданные Удалены" msgid "Deleted Package" msgid_plural "Deleted Packages" msgstr[0] "Пакет Удален" msgstr[1] "Пакеты Удалены" msgstr[2] "Пакеты Удалены" msgid "Deleting" msgstr "Удаление" msgid "Deploy Environment" msgid_plural "Deploy Environments" msgstr[0] "Удалить Окружение" msgstr[1] "Удалить ОкружениÑ" msgstr[2] "Удалить ОкружениÑ" msgid "Deploy This Environment" msgstr "Развернуть Ñто Окружение" msgid "Deploy failure" msgstr "Ðе удалоÑÑŒ развернуть" msgid "Deploy started" msgstr "Развертывание началоÑÑŒ" msgid "Deployed Components" msgstr "Развернутые Компоненты" msgid "Deploying" msgstr "Развёртывание" msgid "Deployment Details" msgstr "Детали РазвертываниÑ" msgid "Deployment History" msgstr "ИÑÑ‚Ð¾Ñ€Ð¸Ñ Ð Ð°Ð·Ð²ÐµÑ€Ñ‚Ñ‹Ð²Ð°Ð½Ð¸Ð¹" msgid "Deployment Logs" msgstr "Журнал РазвертываниÑ" #, python-format msgid "Deployment with id %s doesn't exist anymore" msgstr "Развертывание Ñ id %s больше не ÑущеÑтвует" msgid "Deployment with id foo_deployment_id doesn't exist anymore" msgstr "Развертывание Ñ Ð˜Ð” foo_deployment_id больше не ÑущеÑтвует" msgid "Deployments" msgstr "РазвертываниÑ" msgid "Description" msgstr "ОпиÑание" msgid "Details" msgstr "Детали" msgid "Download Package" msgstr "Скачать Пакет" msgid "Drop Components here" msgstr "Перетащите компоненты Ñюда" msgid "Enabled" msgstr "Включен" msgid "" "Enter a complex password with at least one letter, one number and one " "special character" msgstr "" "Введите Ñложный пароль, Ñодержащий как минимум одну букву, одно чиÑло, и " "один Ñпециальный Ñимвол." msgid "Enter a password" msgstr "Введите пароль" msgid "Enter an image type supported by Murano." msgstr "Введите тип образа, поддерживаемый Murano" msgid "Environment" msgstr "Окружение" msgid "Environment Default Network" msgstr "Сеть по умолчанию Ð´Ð»Ñ ÐžÐºÑ€ÑƒÐ¶ÐµÐ½Ð¸Ñ " msgid "Environment Name" msgstr "Ð˜Ð¼Ñ ÐžÐºÑ€ÑƒÐ¶ÐµÐ½Ð¸Ñ" msgid "Environment name must contain at least one non-white space symbol." msgstr "Ð˜Ð¼Ñ Ð¾ÐºÑ€ÑƒÐ¶ÐµÐ½Ð¸Ñ Ð´Ð¾Ð»Ð¶Ð½Ð¾ Ñодержать Ñ…Ð¾Ñ‚Ñ Ð±Ñ‹ один не пробельный Ñимвол." #, python-format msgid "Environment with id %s doesn't exist anymore" msgstr "Окружение Ñ id %s больше не ÑущеÑтвует" msgid "Environment with specified name already exists" msgstr "Окружение Ñ ÑƒÐºÐ°Ð·Ð°Ð½Ð½Ñ‹Ð¼ именем уже ÑущеÑтвует" msgid "Environments" msgstr "ОкружениÑ" msgid "Error test_error_message occurred while installing package bar" msgstr "Произошла ошибка test_error_message во Ð²Ñ€ÐµÐ¼Ñ ÑƒÑтановки пакета bar" msgid "Error {0} occurred while installing images for {1}" msgstr "Во Ð²Ñ€ÐµÐ¼Ñ ÑƒÑтановки образов Ð´Ð»Ñ {1} возникла ошибка {0}" msgid "Error {0} occurred while installing package {1}" msgstr "Произошла ошибка {0} во Ð²Ñ€ÐµÐ¼Ñ ÑƒÑтановки пакета {1}" msgid "Error {0} occurred while parsing package {1}" msgstr "Во Ð²Ñ€ÐµÐ¼Ñ Ð°Ð½Ð°Ð»Ð¸Ð·Ð° пакета {1} возникла ошибка {0}" msgid "Error {0} occurred while setting image {1}, {2} public" msgstr "Произошла ошибка {0} во Ð²Ñ€ÐµÐ¼Ñ ÑƒÑтановки публичноÑти образа {1}, {2}" msgctxt "Package requirements" msgid "Execution plans folder" msgstr "Ð”Ð¸Ñ€ÐµÐºÑ‚Ð¾Ñ€Ð¸Ñ Ñ Ð¿Ð»Ð°Ð½Ð°Ð¼Ð¸ иÑполнениÑ" msgid "FQN" msgstr "FQN" msgid "Failed" msgstr "Ðе удалоÑÑŒ" msgid "Failed to create environment" msgstr "Ðу удалоÑÑŒ Ñоздать окружение" msgid "Failed to modify the package. {0}" msgstr "Ðе удалоÑÑŒ модифицировать пакет. {0}" msgid "File" msgstr "Файл" msgid "Filter" msgstr "Фильтровать" msgid "Find in a selected category" msgstr "Ðайти в выбранной категории" 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 "" "Первым Ñимволом должна быть латинÑÐºÐ°Ñ Ð±ÑƒÐºÐ²Ð° или подчеркивание. ПоÑледующими " "Ñимволами могут быть латинÑкие буквы, чиÑла, подчеркиваниÑ, Ñимволы @, â„–, $" msgid "Foo" msgstr "Foo" msgid "Fully qualified package name." msgstr "Полное Ð¸Ð¼Ñ Ð¿Ð°ÐºÐµÑ‚Ð°." #, python-format msgid "" "Go to Packages , click 'Import Package' " "and select Repository as Package Source." msgstr "" "Перейдите в Пакеты , нажмите 'Импорт " "Пакета' и выберите Репозиторий в качеÑтве ИÑточника Пакета." #, python-format msgid "" "Go to Packages, click 'Import Package' " "and select Repository as Package Source." msgstr "" "Перейдите в Пакеты , нажмите 'Импорт " "Пакета' и выберите Репозиторий в качеÑтве ИÑточника Пакета." msgid "HTTP/HTTPS URL of the bundle file." msgstr "HTTP/HTTPS URL bundle файла." msgid "HTTP/HTTPS URL of the package file." msgstr "HTTP/HTTPS URL файла пакета." msgid "Heat Orchestration stack name" msgstr "Ð˜Ð¼Ñ Ñтека Heat" msgid "Heat Orchestration stack%(forloop.counter)s name" msgstr "Ð˜Ð¼Ñ Ñтека Heat %(forloop.counter)" 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 "" "ЕÑли пакеты завиÑÑÑ‚ от других пакетов и/или иÑпользуют Ñпецифичные glance " "образы, они также будут уÑтановлены из Ñ€ÐµÐ¿Ð¾Ð·Ð¸Ñ‚Ð¾Ñ€Ð¸Ñ 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 "" "ЕÑли пакет завиÑит от других пакетов и/или иÑпользует Ñпецифичные glance " "образы, они также будут уÑтановлены из Ñ€ÐµÐ¿Ð¾Ð·Ð¸Ñ‚Ð¾Ñ€Ð¸Ñ murano." msgid "Image" msgstr "Образ" msgid "Image Title" msgstr "Ð˜Ð¼Ñ Ð¾Ð±Ñ€Ð°Ð·Ð°" msgid "Image Type" msgstr "Тип Образа" msgid "Image successfully marked" msgstr "Образ уÑпешно маркирован" msgid "Images" msgstr "Образы" msgid "Import Bundle" msgstr "Импортировать Bundle" msgid "Import Package" msgstr "Импортировать Пакет" msgid "Importing package {0} failed. Reason: {1}" msgstr "Пакет {0} не удалоÑÑŒ загрузить. Причина: {1}" msgid "Info" msgstr "ИнформациÑ" msgid "Instance name" msgstr "Ð˜Ð¼Ñ Ð¸Ð½ÑтанÑа" msgid "Instance%(forloop.counter)s name" msgstr "Ð˜Ð¼Ñ Ð¸Ð½ÑтанÑа %(forloop.counter)" msgid "Invalid metadata for image: {0}" msgstr "Ðе корректные метаданные у образа: {0}" msgid "Invalid murano image metadata" msgstr "Ðе корректные murano мета-данные образа" msgid "Invalid value of 'murano_nets' option" msgstr "Ðе корректное значение опции 'murano_nets'" msgid "It is forbidden to upload files larger than {0} MB." msgstr "ÐÐµÐ»ÑŒÐ·Ñ Ð·Ð°Ð³Ñ€ÑƒÐ¶Ð°Ñ‚ÑŒ файлы размером больше {0} MB." msgid "KeyWord" msgstr "Ключевое Слово" msgid "Last operation" msgstr "ПоÑледнÑÑ Ð¾Ð¿ÐµÑ€Ð°Ñ†Ð¸Ñ" msgid "Latest Deployment Log" msgstr "ПоÑледнее развертывание" msgid "License" msgstr "ЛицензиÑ" msgid "Logs" msgstr "Журнал Ñобытий" msgid "Manage" msgstr "Управление" msgid "Manage Components" msgstr "Управление Компонентами" msgctxt "Package requirements" msgid "Manifest file" msgstr "Файл манифеÑта" msgid "Mark Image" msgstr "Маркировать образ" msgid "" "Mark an image with Murano specific metadata to be added to the selected " "image." msgstr "Отметьте образ Murano-метаданными, чтобы добавить выбранный образ." msgid "Marked Images" msgstr "Маркированные образы" msgid "Modify Package" msgstr "Модифицировать Пакет" msgid "Modifying package failed" msgstr "Ðе удалоÑÑŒ изменить пакет" msgid "NO ENVIRONMENTS" msgstr "Ðет окружений" msgid "Name" msgstr "ИмÑ" msgid "Name of the bundle." msgstr "Ð˜Ð¼Ñ bundle." #, python-format msgid "Network of '%s'" msgstr "Сеть \"%s\"" msgid "Next" msgstr "Далее" msgid "Next Page" msgstr "Ð¡Ð»ÐµÐ´ÑƒÑŽÑ‰Ð°Ñ Ð¡Ñ‚Ñ€Ð°Ð½Ð¸Ñ†Ð°" msgid "No availability zones available" msgstr "Ðи одной зоны доÑтупноÑти не доÑтупно" msgid "No categories available" msgstr "Ðи одной категории не доÑтупно" msgid "No components" msgstr "Ðет компонентов" msgid "No images available" msgstr "Ðи одного образа не доÑтупно" msgid "No keypair" msgstr "Без ключей" msgid "No license" msgstr "Ðет лицензии" msgid "No recent activity to report at this time." msgstr "Ð’Ñ‹ еще не иÑпользовали приложений." msgid "No requirements" msgstr "Ðет завиÑимоÑтей" msgid "None" msgstr "Ðет" msgid "Not in domain" msgstr "Ðе принадлежит домену" msgid "Note" msgstr "Внимание" msgid "" "OpenStack Networking (Neutron) is not available in current environment. " "Custom Network Settings cannot be applied" msgstr "" "Сетевой ÑÐµÑ€Ð²Ð¸Ñ Newtron не доÑтупен в текущем окружении. Выбор Ñетевых " "наÑтроек не может быть применен" msgid "Operation is forbidden by murano-api server." msgstr "ÐžÐ¿ÐµÑ€Ð°Ñ†Ð¸Ñ Ð·Ð°Ð¿Ñ€ÐµÑ‰ÐµÐ½Ð° Ñервером murano-api." msgid "Optional" msgstr "ÐеобÑзательно" msgid "Overview" msgstr "Обзор" msgid "Package Bundle Source" msgstr "ИÑточник Ð´Ð»Ñ Bundle" msgid "Package Count" msgstr "КоличеÑтво Пакетов" msgid "Package Details" msgstr "Детали Пакета" msgid "Package Name" msgstr "Ð˜Ð¼Ñ ÐŸÐ°ÐºÐµÑ‚Ð°" msgid "Package Source" msgstr "ИÑточник Пакета" msgid "Package Tags" msgstr "Теги Пакета" msgid "Package URL" msgstr "URL Пакета" msgid "Package Version" msgstr "ВерÑÐ¸Ñ ÐŸÐ°ÐºÐµÑ‚Ð°" msgid "" "Package creation failed.Reason: Can't find Package name from repository." msgstr "" "Ðе удалоÑÑŒ Ñоздать пакет. Пакет Ñ Ñ‚Ð°ÐºÐ¸Ð¼ именем не найден в репозитории." msgid "Package creation failed.Reason: {0}" msgstr "Ðе удалоÑÑŒ Ñоздать пакет. Причина: {0}" msgid "Package foo uploaded" msgstr "Пакет foo загружен" msgid "Package modified." msgstr "Пакет модифицирован." msgid "Package name in the repository, usually a fully qualified name" msgstr "Ð˜Ð¼Ñ ÐŸÐ°ÐºÐµÑ‚Ð° в репозитории, обычно полноÑтью Ñпецифицированное имÑ" msgid "Package or Class with the same name is already made public" msgstr "Пакет или КлаÑÑ Ñ Ñ‚Ð°ÐºÐ¸Ð¼ же именем уже ÑвлÑетÑÑ Ð¿ÑƒÐ±Ð»Ð¸Ñ‡Ð½Ñ‹Ð¼" msgid "Package parameters successfully updated." msgstr "Параметры пакета уÑпешно обновлены." msgid "Package version" msgstr "ВерÑÐ¸Ñ Ð¿Ð°ÐºÐµÑ‚Ð°" msgid "Package with id foo_package_id is not found" msgstr "Пакет Ñ Ð˜Ð” foo_package_id не найден" msgid "Package with id {0} is not found" msgstr "Пакет Ñ id {0} не найден" msgid "Package with specified name already exists" msgstr "Пакет Ñ Ñ‚Ð°ÐºÐ¸Ð¼ именем уже ÑущеÑтвует" msgid "Package {0} already registered." msgstr "Пакет {0} уже зарегиÑтрирован." msgid "Package {0} upload failed. {1}" msgstr "Пакет {0} не удалоÑÑŒ загрузить. {1}" msgid "Package {0} uploaded" msgstr "Пакет {0} загружен" msgid "Packages" msgstr "Пакеты" msgid "Packages should contain:" msgstr "Пакеты должны Ñодержать:" msgid "Please confirm your password" msgstr "ПожалуйÑта подтвердите пароль" msgid "Please supply a bundle name" msgstr "ПожалуйÑта укажите Ð¸Ð¼Ñ bundle" msgid "Please supply a bundle url" msgstr "ПожалуйÑта укажите url bundle" msgid "Please supply a package file" msgstr "ПожалуйÑта укажите файл пакета" msgid "Please supply a package name" msgstr "ПожалуйÑта укажите Ð¸Ð¼Ñ Ð¿Ð°ÐºÐµÑ‚Ð°" msgid "Please supply a package url" msgstr "ПожалуйÑта укажите url пакета" msgid "Previous Page" msgstr "ÐŸÑ€ÐµÐ´Ñ‹Ð´ÑƒÑ‰Ð°Ñ Ð¡Ñ‚Ñ€Ð°Ð½Ð¸Ñ†Ð°" msgid "Provide comma-separated list of words, associated with the package" msgstr "Введите разделенный запÑтыми ÑпиÑок Ñлов, ÑвÑзанных Ñ Ð¿Ð°ÐºÐµÑ‚Ð¾Ð¼" msgid "Provide desired name for a new category" msgstr "Выберите Ð¸Ð¼Ñ Ð´Ð»Ñ Ð½Ð¾Ð²Ð¾Ð¹ категории" msgid "Public" msgstr "Публичный" msgid "Quick Deploy" msgstr "БыÑтрое развертывание" msgid "Ready" msgstr "Готов" msgid "Ready to configure" msgstr "Готов к наÑтройке" msgid "Ready to deploy" msgstr "Готов к развёртыванию" msgid "Recent Activity" msgstr "Ðедавние приложениÑ" msgid "Repository" msgstr "Репозиторий" msgid "Requested object is not found on murano server." msgstr "Запрашиваемый объект не найден Ñервером murano." msgid "Requested operation conflicts with an existing object." msgstr "Ð—Ð°Ð¿Ñ€Ð¾ÑˆÐµÐ½Ð½Ð°Ñ Ð¾Ð¿ÐµÑ€Ð°Ñ†Ð¸Ñ ÐºÐ¾Ð½Ñ„Ð»Ð¸ÐºÑ‚ÑƒÐµÑ‚ Ñ ÑущеÑтвующим объектом." msgid "Requirements" msgstr "ТребованиÑ" msgid "Retype your password" msgstr "Подтвердите пароль" msgid "Running" msgstr "Запущенный" msgid "Running with errors" msgstr "Запущенный Ñ Ð¾ÑˆÐ¸Ð±ÐºÐ°Ð¼Ð¸" msgid "Running with warnings" msgstr "Запущенный Ñ Ð¿Ñ€ÐµÐ´ÑƒÐ¿Ñ€ÐµÐ¶Ð´ÐµÐ½Ð¸Ñми" msgid "Select Application" msgstr "Выбрать Приолжение" msgid "Select Image" msgstr "Выберите образ" msgid "Select an image registered in Glance Image Services." msgstr "Выберите образ, зарегиÑтрированный в Glance" msgid "Select one or more categories for a package." msgstr "Выберите одну или более категорий Ð´Ð»Ñ Ð¿Ð°ÐºÐµÑ‚Ð°." msgid "Set up for identifying a package." msgstr "УÑтановите, чтобы идентифицировать пакет." msgid "Show Details" msgstr "Показать детали" msgid "Something went wrong during package downloading" msgstr "Произошла ошибка при загрузке пакета." msgid "Sorry, this environment doesn't exist anymore" msgstr "Это окружение больше не ÑущеÑтвует" msgid "" "Sorry, you can't add application right now. The environment is deploying." msgstr "Ð’Ñ‹ не можете добавить приложение ÑейчаÑ. Окружение развертываетÑÑ." msgid "Sorry, you can't delete service right now" msgstr "К Ñожаление невозможно удалить ÑÐµÑ€Ð²Ð¸Ñ Ð² данный момент" msgid "Specified title already in use. Please choose another one." msgstr "Указанный заголовок уже иÑпользуетÑÑ. Выберите другой." msgid "Specifying a category helps to filter applications in the catalog" msgstr "Указание категории помогает фильтровать Ð¿Ñ€Ð¸Ð»Ð¾Ð¶ÐµÐ½Ð¸Ñ Ð² каталоге" msgid "Started Deleting Component" msgid_plural "Started Deleting Components" msgstr[0] "Ðачато удаление компонента" msgstr[1] "Ðачато удаление компонентов" msgstr[2] "Ðачато удаление компонентов" msgid "Started Deleting Environment" msgid_plural "Started Deleting Environments" msgstr[0] "Ðачато удаление ОкружениÑ" msgstr[1] "Ðачато удаление Окружений" msgstr[2] "Ðачато удаление Окружений" msgid "Started deploying Environment" msgid_plural "Started deploying Environments" msgstr[0] "Ðачато развертывание ОкружениÑ" msgstr[1] "Ðачато развертывание Окружений" msgstr[2] "Ðачато развертывание Окружений" msgid "Status" msgstr "СтатуÑ" msgid "Step {0}" msgstr "Шаг {0}" msgid "Successful" msgstr "УÑпешый" msgid "Tags" msgstr "Теги" msgid "Tenant Name" msgstr "Ð˜Ð¼Ñ ÐŸÑ€Ð¾ÐµÐºÑ‚Ð°" msgid "The '{0}' application successfully added to environment." msgstr "Приложение '{0}' уÑпешно добавлено в окружение." 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 "" "Виртуальные Машины Ñтого Ð¾ÐºÑ€ÑƒÐ¶ÐµÐ½Ð¸Ñ Ð±ÑƒÐ´ÑƒÑ‚ приÑоединÑтьÑÑ Ðº Ñтой Ñети по " "умолчанию, кроме Ñлучаев когда они наÑтроены индивидуально. При выборе " "\"Создать Ðовую\" будет Ñоздана Ð½Ð¾Ð²Ð°Ñ cеть Ñ Ð¿Ð¾Ð´Ñетью, Ñодержащей диапазон " "IP адреÑов, доÑтупных маршрутизатору Murano по-умолчанию." #, python-format msgid "" "The bundle is going to be installed from %(murano_repo_url)s repository." msgstr "" "Bundle будет уÑтановлен из Ñ€ÐµÐ¿Ð¾Ð·Ð¸Ñ‚Ð¾Ñ€Ð¸Ñ %(murano_repo_url)s." msgid "The environment name field cannot be empty." msgstr "Поле Ñ Ð¸Ð¼ÐµÐ½ÐµÐ¼ Ð¾ÐºÑ€ÑƒÐ¶ÐµÐ½Ð¸Ñ Ð½Ðµ может быть пуÑтым." #, python-format msgid "" "The package is going to be imported from %(murano_repo_url)s repository." msgstr "" "Пакет будет уÑтановлен из Ñ€ÐµÐ¿Ð¾Ð·Ð¸Ñ‚Ð¾Ñ€Ð¸Ñ %(murano_repo_url)s." msgid "" "The password must contain at least one letter, " "one number and one special character" msgstr "" "Пароль должен Ñодержать как минимум одну букву, одно чиÑло, и один " "Ñпециальный Ñимвол" msgid "The request data is not acceptable by the server" msgstr "Сервер не в ÑоÑтоÑнии принÑть данные запроÑа" msgid "There are no applications in the catalog. You can import apps from" msgstr "Ðет приложений в каталоге. Ð’Ñ‹ можете импортировать их по адреÑу" #, python-format msgid "" "There are no applications in the catalog. You can import apps from %(display_repo_url)s." msgstr "" "Ðет приложений в каталоге. Ð’Ñ‹ можете импортировать их по адреÑу %(display_repo_url)s." msgid "There are no applications matching your criteria." msgstr "Ðет приложений, которые подходÑÑ‚ под критерии поиÑка." msgid "There was an error communicating with server" msgstr "При взаимодейÑтвии Ñ Ñервером возникла ошибка" msgid "There was an error initialising this field." msgstr "При инициализации Ñтого Ð¿Ð¾Ð»Ñ Ð²Ð¾Ð·Ð½Ð¸ÐºÐ»Ð° ошибка." msgid "" "This action cannot be undone. Any resources created by this environment will " "have to be released manually." msgstr "" "Это дейÑтвие невозможно отменить. Любые реÑурÑÑ‹ Ñтого Ð¾ÐºÑ€ÑƒÐ¶ÐµÐ½Ð¸Ñ Ð½ÐµÐ¾Ð±Ñ…Ð¾Ð´Ð¸Ð¼Ð¾ " "будет оÑвободить в ручном режиме." msgid "Time Finished" msgstr "Ð’Ñ€ÐµÐ¼Ñ ÐºÐ¾Ð½Ñ†Ð°" msgid "Time Started" msgstr "Ð’Ñ€ÐµÐ¼Ñ Ð½Ð°Ñ‡Ð°Ð»Ð°" msgid "Time updated" msgstr "Ð’Ñ€ÐµÐ¼Ñ Ð¾Ð±Ð½Ð¾Ð²Ð»ÐµÐ½Ð¸Ñ" msgid "Title" msgstr "Заголовок" msgid "Toggle Active" msgid_plural "Toggle Active" msgstr[0] "Изменить ÐктивноÑть" msgstr[1] "Изменить ÐктивноÑть" msgstr[2] "Изменить ÐктивноÑть" msgid "Toggle Enabled" msgstr "Изменить ÐктивноÑть" msgid "Toggle Public" msgid_plural "Toggle Public" msgstr[0] "Изменить ПубличноÑть" msgstr[1] "Изменить ПубличноÑть" msgstr[2] "Изменить ПубличноÑть" msgid "Toggled Active" msgid_plural "Toggled Active" msgstr[0] "ÐктивноÑть Изменена" msgstr[1] "ÐктивноÑть Изменена" msgstr[2] "ÐктивноÑть Изменена" msgid "Toggled Public" msgid_plural "Toggled Public" msgstr[0] "ПубличноÑть Изменена" msgstr[1] "ПубличноÑть Изменена" msgstr[2] "ПубличноÑть Изменена" msgid "Topology" msgstr "ТопологиÑ" msgid "" "Trying to add {0} image to glance. Image will be ready for deployment after " "successful upload" msgstr "" "ДобавлÑÑŽ образ {0} в glance. Образ будет доÑтупен Ð´Ð»Ñ Ñ€Ð°Ð·Ð²ÐµÑ€Ñ‚Ñ‹Ð²Ð°Ð½Ð¸Ñ Ð¿Ð¾Ñле " "уÑпешной загрузки" msgid "" "Trying to add {0}, {1} image to glance. Image will be ready for deployment " "after successful upload" msgstr "" "ДобавлÑÑŽ образ {0}, {1} в glance. Образ будет доÑтупен Ð´Ð»Ñ Ñ€Ð°Ð·Ð²ÐµÑ€Ñ‚Ñ‹Ð²Ð°Ð½Ð¸Ñ " "поÑле уÑпешной загрузки" msgid "Type" msgstr "Тип" msgctxt "Package requirements" msgid "UI definition folder" msgstr "Каталог Ñодержащий Ð¾Ð¿Ñ€ÐµÐ´ÐµÐ»ÐµÐ½Ð¸Ñ UI" msgid "UNKNOWN" msgstr "Ðе извеÑтно" msgid "URL" msgstr "URL" msgid "Unable to abandon an environment {0} due to: {1}" msgstr "Ðе удалоÑÑŒ забыть об окружении {0} по причине: {1}" msgid "Unable to communicate to glare-api server." msgstr "Ðе удалоÑÑŒ ÑоединитьÑÑ Ñ Ñервером glare-api." msgid "Unable to communicate to murano-api server." msgstr "Ðе удалоÑÑŒ ÑоединитьÑÑ Ñ Ñервером murano-api." msgid "Unable to create environment {0} due to: {1}" msgstr "Ðе удалоÑÑŒ Ñоздать окружение {0} по причине: {1}" msgid "Unable to delete category" msgstr "Ðе удалоÑÑŒ удалить категорию" msgid "Unable to delete environment {0} due to: {1}" msgstr "Ðе удалоÑÑŒ удалить окружение {0} по причине: {1}" msgid "Unable to delete package in murano-api server" msgstr "Ðе удалоÑÑŒ удалить пакет Ñ Ñервера murano-api" msgid "Unable to deploy. Try again later" msgstr "Ðе удалоÑÑŒ развернуть окружение. Попробуйте Ñнова." msgid "Unable to download package." msgstr "Ðе удалоÑÑŒ Ñкачать пакет." msgid "Unable to get list of categories" msgstr "Ðе удалоÑÑŒ получить ÑпиÑок категорий" msgid "Unable to mark image" msgstr "Ðе удалоÑÑŒ маркировать образ" msgid "Unable to modify package" msgstr "Ðе удалоÑÑŒ изменить пакет" msgid "Unable to remove metadata" msgstr "Ðе удалоÑÑŒ получить метаданные" msgid "Unable to remove package." msgstr "Ðе удалоÑÑŒ удалить пакет." msgid "Unable to retrieve availability zones." msgstr "Ðе удалоÑÑŒ получить ÑпиÑок зон доÑтупноÑти" msgid "Unable to retrieve details for service" msgstr "Ðе удалоÑÑŒ получить детали ÑервиÑа" msgid "Unable to retrieve list of deployments" msgstr "Ðе удалоÑÑŒ получить ÑпиÑок развертываний" msgid "Unable to retrieve list of images" msgstr "Ðе удалоÑÑŒ получить ÑпиÑок образов" msgid "" "Unable to retrieve list of services. This environment is deploying or " "already deployed by other user." msgstr "" "Ðе удалоÑÑŒ получить ÑпиÑок ÑервиÑов. Это окружение развертываетÑÑ Ð¸Ð»Ð¸ уже " "развернуто другим пользователем" msgid "Unable to retrieve package details." msgstr "Ðе удалоÑÑŒ получить ÑвойÑтва пакета." msgid "Unable to retrieve project list." msgstr "Ðе удалоÑÑŒ получить ÑпиÑок проектов" msgid "Unable to retrieve public images." msgstr "Ðе удалоÑÑŒ получить публичные образы." msgid "Unavailable" msgstr "ÐедоÑтупно" msgid "Unknown" msgstr "ÐеизвеÑтно" msgid "Update" msgstr "Обновить" msgid "Update Environment" msgid_plural "Deploy Environments" msgstr[0] "Обновить Окружение" msgstr[1] "Развернуть Окружение" msgstr[2] "Обновить Окружение" msgid "Update Image" msgstr "Обновить Образ" msgid "Update Metadata" msgstr "Обновить метаданные" msgid "Update This Environment" msgstr "Обновите Ñто окружение" msgid "Updated" msgstr "Обновлен" msgid "Updated Environment" msgid_plural "Deployed Environments" msgstr[0] "Окружение обновлено" msgstr[1] "Окружение развернуто" msgstr[2] "Окружение обновлено" msgid "Uploading package failed. {0}" msgstr "Ðе удалоÑÑŒ загрузить пакет. {0}" msgid "Used for identifying and filtering packages." msgstr "ИÑпользуетÑÑ Ð´Ð»Ñ Ð¸Ð´ÐµÐ½Ñ‚Ð¸Ñ„Ð¸ÐºÐ°Ñ†Ð¸Ð¸ и фильтрации пакетов." msgid "Validation Error occurred" msgstr "Произошла ошибка валидации" msgid "Version" msgstr "ВерÑиÑ" msgid "Version of the package (optional)." msgstr "ВерÑÐ¸Ñ ÐŸÐ°ÐºÐµÑ‚Ð° (необÑзательно)" msgid "You are not allowed to change this properties of the package" msgstr "Вам не разрешено менÑть ÑвойÑтва Ñтого пакета" msgid "You are not allowed to delete this package" msgstr "Вам не разрешено удалить Ñтот пакет" msgid "You are not allowed to perform this operation" msgstr "Вам не разрешено выполнÑть Ñту операцию" msgid "" "You'll have to configure each package installed from this bundle separately." msgstr "" "Вам надо будет Ñконфигурировать каждый пакет из Ñтого bundle индивидуально." msgid "{0}{1} don't match" msgstr "{0}{1} не Ñовпали" murano-dashboard-5.0.0/muranodashboard/locale/ru/LC_MESSAGES/djangojs.po0000666000175100017510000000531113245511125025751 0ustar zuulzuul00000000000000# Aleksey Alekseenko <9118250541@mail.ru>, 2016. #zanata # Andreas Jaeger , 2016. #zanata # Yulia Ryndenkova , 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 "Ваш пароль должен ÑоÑтоÑть по крайней мере из" murano-dashboard-5.0.0/muranodashboard/locale/de/0000775000175100017510000000000013245511556021775 5ustar zuulzuul00000000000000murano-dashboard-5.0.0/muranodashboard/locale/de/LC_MESSAGES/0000775000175100017510000000000013245511556023562 5ustar zuulzuul00000000000000murano-dashboard-5.0.0/muranodashboard/locale/de/LC_MESSAGES/django.po0000666000175100017510000010117113245511141025355 0ustar zuulzuul00000000000000# Robert Simai , 2016. #zanata # Robert Simai , 2017. #zanata # Reik Keutterling , 2018. #zanata # Robert Simai , 2018. #zanata msgid "" msgstr "" "Project-Id-Version: murano-dashboard VERSION\n" "Report-Msgid-Bugs-To: https://bugs.launchpad.net/openstack-i18n/\n" "POT-Creation-Date: 2018-02-13 06:13+0000\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "PO-Revision-Date: 2018-02-13 12:26+0000\n" "Last-Translator: Reik Keutterling \n" "Language-Team: German\n" "Language: de\n" "X-Generator: Zanata 3.9.6\n" "Plural-Forms: nplurals=2; plural=(n != 1)\n" #, python-format msgid "" "\n" " %(used)s + %(other_used)s of %(quota)s MB used\n" " " msgstr "" "\n" " %(used)s + %(other_used)s von %(quota)s MB verwendet\n" " " #, python-format msgid "" "\n" " %(used)s + %(other_used)s of %(quota)s used\n" " " msgstr "" "\n" " %(used)s + %(other_used)s von %(quota)s verwendet\n" " " #, python-format msgid "%s: random subnet" msgstr "%s: zufälliges Subnetz" msgid "-" msgstr "-" msgid "80 characters max." msgstr "80 Zeichen max." msgid "A local zip file to upload" msgstr "EIne lokale Zip-Datei zum Hochladen" msgid "Abandon Environment" msgid_plural "Abandon Environments" msgstr[0] "Verlasse Umgebung" msgstr[1] "Verlasse Umgebungen" msgid "Abandoned Environment" msgid_plural "Abandoned Environments" msgstr[0] "Verlassene Umgebung" msgstr[1] "Verlassene Umgebungen" msgid "Active" msgstr "Aktiv" msgid "Add" msgstr "Hinzufügen" msgid "Add Application" msgstr "Applikation hinzufügen" msgid "Add Application Category" msgstr "Anwendungskategorie hinzufügen" msgid "Add Category" msgstr "Kategorie hinzufügen" msgid "Add Component" msgstr "Komponente hinzufügen" msgid "Add Murano Metadata" msgstr "Füge Murano Metadaten hinzu" msgid "Add New" msgstr "Neu hinzufügen" msgid "Add new category to the application catalog." msgstr "Füge neue Kategorie zum Applikationskatalog hinzu." msgid "Add to Env" msgstr "Zur Umgebung hinzufügen" msgid "Adding application to an environment failed." msgstr "" "Hinzufügen einer Anwendung zu einer bestehenden Umgebung ist fehlgeschlagen." msgid "All" msgstr "Alle" msgid "Allows adding additional information about a package." msgstr "Erlaubt das Hinzufügen zusätzlicher Informationen über ein Paket." msgid "" "Allows to hide a package from the catalog. (Applies to package dependencies)" msgstr "" "Erlaubt das Verstecken eines Paketes vom Katalog (in Bezug auf " "Paketabhängigkeiten)." msgid "" "An environment is a collection of applications that are meant to operate " "under similar conditions." msgstr "" "Eine Umgebung ist eine Sammlung von Applikationen, die unter ähnlichen " "Bedingungen arbeiten sollen." msgid "An external http/https URL to load the bundle from." msgstr "Eine externe http/https URL zum Laden des Bündels." msgid "An external http/https URL to load the package from." msgstr "Eine externe http/https URL zum Laden des Pakets." msgid "App Catalog" msgstr "Applikationskatalog" msgid "App Category:" msgstr "App Kategorie:" msgid "App category" msgstr "App Kategorie" msgid "Application Categories" msgstr "Anwendungskategorien" msgid "Application Category" msgstr "Applikationskategorie" msgid "Application Details" msgstr "Applikationsdetails" msgid "Application Package" msgstr "Applikationspaket" msgid "Application default security group" msgstr "Applikations-Standardsicherheitsgruppe" msgid "Application Components" msgstr "Applikationen Komponenten" msgid "Applications" msgstr "Anwendungen" msgid "Author" msgstr "Autor" msgid "Auto" msgstr "Auto" msgid "Back" msgstr "Zurück" msgid "Browse" msgstr "Blättern" msgid "Browse Local" msgstr "Lokales Browsen" msgid "Bundle Name" msgstr "Bündelname" msgid "Bundle URL" msgstr "Bündel URL" msgid "Bundle creation failed.Reason: Can't find Bundle name from repository." msgstr "" "Bündelerstellung fehlgeschlagen. Grund: Konnte Bündelname nicht im " "Repository finden." msgid "Bundle creation failed.Reason: {0}" msgstr "Bündelerstellung fehlgeschlagen. Grund: {0}" msgid "Bundle successfully imported." msgstr "Bündel erfolgreich importiert." msgid "Bundle's full name." msgstr "Vollständiger Bündelname." msgid "Can not get logo for {0}." msgstr "Kann Logo für {0} nicht abrufen." msgid "Can not get supplier logo for {0}." msgstr "Kann Anbieterlogo für {0} nicht abrufen." msgid "Cancel" msgstr "Abbrechen" msgid "Categories" msgstr "Kategorien" msgid "Category Name" msgstr "Kategoriename" msgid "Category {0} created." msgstr "Kategorie {0} erstellt." msgid "Check Keystone configuration of murano-api server." msgstr "Überprüfen Sie die Keystone-Konfiguration des Murano-API-Servers." msgid "Choose a Zip archive to upload into the catalog." msgstr "Wählen Sie ein Zip-Archiv zum Hochladen in den Katalog." msgid "Choose a name for the environment" msgstr "Wähle einen Namen für die Umgebung" msgctxt "Package requirements" msgid "Classes definition folder" msgstr "Klassendefinitionsverzeichnis" msgid "Click to create new environment" msgstr "Zum erstellen einer neuen Umgebung anklicken" msgid "Completed with warnings" msgstr "Abgeschlossen mit Warnungen" msgid "Component" msgstr "Komponente" msgid "Component Details" msgstr "Kompontenten-Details" msgid "Component List" msgstr "Komponentenliste" msgid "Component Logs" msgstr "Komponenten-Logs" msgid "Components" msgstr "Komponenten" msgid "Configuration" msgstr "Konfiguration" msgid "Configure Application" msgstr "Applikation konfigurieren" msgid "Confirm password" msgstr "Passwort bestätigen" msgid "Could not retrieve latest status for the {0} environment" msgstr "Konnte den letzten Status für {0} Umgebung nicht abrufen" msgid "" "Couldn't find any apps, required for this field.\n" "Tried: {fqns}" msgstr "" "Konnte keine App's finden, die für dieses Feld erforderlich sind.\n" "Probierte: {fqns}" msgid "" "Couldn't initialise glance v1 client, therefore could not make the following " "images public: {0}" msgstr "" "Der Glace v1 Klient konnte nicht initialisiert werden. Daher können die " "folgenden Abbilder nicht auf öffentlich gesetzt werden: '{0}'" msgid "Couldn't update environment. Reason: This name is already taken." msgstr "" "Umgebung konnte nicht aktualisiert werden. Grund: der Name wird bereits " "verwendet." msgid "Couldn't update package {0} parameters. Error: {1}" msgstr "Konnte Parameter des Paketes {0} nicht aktualisieren. Fehler: {1}" msgid "Create" msgstr "Erstellen" msgid "Create Env" msgstr "Umgebung erzeugen" msgid "Create Environment" msgstr "Umgebung erstellen" msgid "Create New" msgstr "Neu erstellen" msgid "Create a title for an image." msgstr "Titel für ein Abbild erstellen." msgid "Created" msgstr "Erstellt" msgid "Custom Type" msgstr "Benutzerspezifischer Typ" msgid "" "Default network is either not specified for this project, or specified " "incorrectly, please contact administrator." msgstr "" "Das Standardnetzwerk für dieses Projekt wurde entweder nicht angegeben oder " "ist falsch. Bitte kontaktieren Sie den Administrator." msgid "" "Defines whether or not a package can be used by other tenants. (Applies to " "package dependencies)" msgstr "" "Definiert, ob ein Paket von anderen Mandanten benutzt werden kann (in Bezug " "auf Paketabhängigkeiten)." msgid "Delete Category" msgid_plural "Delete Categories" msgstr[0] "Lösche Kategorie" msgstr[1] "Lösche Kategorien" msgid "Delete Component" msgid_plural "Delete Components" msgstr[0] "Lösche Komponente" msgstr[1] "Lösche Komponenten" msgid "Delete Environment" msgid_plural "Delete Environments" msgstr[0] "Lösche Umgebung" msgstr[1] "Lösche Umgebungen" msgid "Delete Metadata" msgid_plural "Delete Metadata" msgstr[0] "Lösche Metadaten" msgstr[1] "Lösche Metadaten" msgid "Delete Package" msgid_plural "Delete Packages" msgstr[0] "Paket löschen" msgstr[1] "Pakete löschen" msgid "Delete failure" msgstr "Fehler beim Löschen" msgid "Deleted Category" msgid_plural "Deleted Categories" msgstr[0] "Gelöschte Kategorie" msgstr[1] "Gelöschte Kategorien" msgid "Deleted Metadata" msgid_plural "Deleted Metadata" msgstr[0] "Gelöschte Metadaten" msgstr[1] "Gelöschte Metadaten" msgid "Deleted Package" msgid_plural "Deleted Packages" msgstr[0] "Gelöschtes Paket" msgstr[1] "Gelöschte Pakete" msgid "Deleting" msgstr "Löschen" msgid "Deploy Environment" msgid_plural "Deploy Environments" msgstr[0] "Stelle Umgebung bereit" msgstr[1] "Stelle Umgebungen bereit" msgid "Deploy This Environment" msgstr "Stelle diese Umgebung bereit" msgid "Deploy failure" msgstr "Fehler beim Bereitstellen" msgid "Deploy started" msgstr "Bereitstellung gestartet" msgid "Deployed Components" msgstr "Bereitgestellte Komponenten" msgid "Deploying" msgstr "Bereitstellen" msgid "Deployment Details" msgstr "Bereitstellungsdetails" msgid "Deployment History" msgstr "Bereitstellungsverlauf" msgid "Deployment Logs" msgstr "Bereitstellungslogs" #, python-format msgid "Deployment with id %s doesn't exist anymore" msgstr "Bereitstellung mit id %s existiert nicht mehr" msgid "Deployment with id foo_deployment_id doesn't exist anymore" msgstr "Bereitstellung mit ID foo_deployment_id existiert nicht mehr" msgid "Deployments" msgstr "Bereitstellungen" msgid "Description" msgstr "Beschreibung" msgid "Details" msgstr "Details" msgid "Download Package" msgstr "Paket herunterladen" msgid "Drop Components here" msgstr "Lassen Sie Komponenten hier fallen" msgid "Enabled" msgstr "Aktiviert" msgid "" "Enter a complex password with at least one letter, one number and one " "special character" msgstr "" "Geben Sie ein komplexes Passwort mit mindestens einem Buchstaben, einer " "Ziffer und einem Sonderzeichen ein" #, python-format msgid "Enter a dict with choices and values. Got %(value)s." msgstr "" "Geben Sie ein dict mit Auswahlmöglichkeiten und Werten an. Habe %(value)s." msgid "Enter a password" msgstr "Geben Sie ein Passwort ein" msgid "Enter an image type supported by Murano." msgstr "Geben Sie einen von Murano unterstützten Abbildtyp an." msgid "Environment" msgstr "Umgebung" msgid "Environment Default Network" msgstr "Umgebungsstandardnetzwerk" msgid "Environment Deployment History" msgstr "Bereitstellungshistorie der Umgebung" msgid "Environment Name" msgstr "Umgebungsname" msgid "Environment name must contain at least one non-white space symbol." msgstr "Umgebungsname muss wenigstens ein Nichtleerzeichen enthalten." #, python-format msgid "Environment with id %s doesn't exist anymore" msgstr "Umgebung mit id %s existiert nicht mehr" msgid "Environment with specified name already exists" msgstr "Die Umgebung mit dem angegebenen Namen existiert bereits" msgid "Environments" msgstr "Umgebungen" #, python-format msgid "" "Error fetching the environment. The page may be rendered incorrectly. " "Reason: %s" msgstr "" "Fehler beim Abruf der Umgebung. Die Seite wird möglicherweise falsch " "dargestellt. Grund: %s" msgid "Error test_error_message occurred while installing package bar" msgstr "Fehler test_error_message bei der Installation des Paketes bar" msgid "Error {0} occurred while installing images for {1}" msgstr "Fehler {0} aufgetreten beim Installieren der Abbilder für {1}" msgid "Error {0} occurred while installing package {1}" msgstr "Fehler {0} beim installieren des Pakets {1}" msgid "Error {0} occurred while parsing package {1}" msgstr "Fehler {0} aufgetreten beim analysieren des Paketes {1}" msgid "Error {0} occurred while setting image {1}, {2} public" msgstr "Fehler {0} beim setzen des Abbildes {1}, {2} auf öffentlich" msgctxt "Package requirements" msgid "Execution plans folder" msgstr "Ausführungsplanverzeichnis" msgid "FQN" msgstr "FQN" msgid "Failed" msgstr "Fehlgeschlagen" msgid "Failed to create environment" msgstr "Erstellen der Umgebung fehlgeschlagen" msgid "Failed to get list of flavors." msgstr "Fehler beim Abruf der Variantenliste." msgid "Failed to modify the package. {0}" msgstr "Konnte das Paket nicht modifizieren. {0}" msgid "File" msgstr "Datei" msgid "Filter" msgstr "Filter" msgid "Find in a selected category" msgstr "Finde in den ausgewählten Kategorien" 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 "" "Das erste Zeichen sollte ein lateinischer Buchstabe oder ein Unterstrich " "sein. Darauffolgende Zeichen dürfen lateinische oder numerische Zeichen " "sein, sowie Unterstrich, das at-Symbol, das Nummernzeichen oder das " "Dollarzeichen." msgid "Foo" msgstr "Foo" msgid "Fully qualified package name." msgstr "Vollständig qualifizierter Paketname." #, python-format msgid "" "Go to Packages , click 'Import Package' " "and select Repository as Package Source." msgstr "" "Gehe zu Pakete , Klick 'Paketimport' und " "wähle Repository als Paketquelle." #, python-format msgid "" "Go to Packages, click 'Import Package' " "and select Repository as Package Source." msgstr "" "Gehen Sie auf Pakete, kklicken Sie " "'Importiere Pakete' wählen Sie Repository als Paketquelle." msgid "HTTP/HTTPS URL of the bundle file." msgstr "HTTP/HTTPS URL der Bündeldatei." msgid "HTTP/HTTPS URL of the package file." msgstr "HTTP/HTTPS URL der Paketdatei." msgid "Heat Orchestration stack name" msgstr "Heat-Orchestrierung Stack-Name" msgid "Heat Orchestration stack%(forloop.counter)s name" msgstr "Heat-Orchestrierung Stack%(forloop.counter)s Name" 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 "" "Wenn Pakete von anderen abhängen und/oder spezielle Glance Abbilder " "benötigen, dann werden diese vom Murano Repository heruntergeladen." 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 "" "Wenn das Paket von anderen Paketen abhängt und/oder spezielle Glance " "Abbilder benötigt, dann werden diese vom Murano Repository heruntergeladen." msgid "Image" msgstr "Abbild" msgid "Image Title" msgstr "Abbildtitel" msgid "Image Type" msgstr "Abbildtyp" msgid "Image successfully marked" msgstr "Abbild erfolgreich markiert" msgid "Images" msgstr "Abbilder" msgid "Import Bundle" msgstr "Bündelimport" msgid "Import Package" msgstr "Paketimport" msgid "Importing package {0} failed. Reason: {1}" msgstr "Import des Paketes {0} fehlgeschlagen. Grund: {1}" msgid "Info" msgstr "Info" msgid "Instance name" msgstr "Instanzname" msgid "Instance%(forloop.counter)s name" msgstr "Instanz%(forloop.counter)s Name" msgid "Invalid metadata for image: {0}" msgstr "Falsche Metadaten für Abbild: {0}" msgid "Invalid murano image metadata" msgstr "Ungültige Murano-Abbildmetadaten" msgid "Invalid value of 'murano_nets' option" msgstr "Ungültiger Wert für Option 'murano_nets'" msgid "It is forbidden to upload files larger than {0} MB." msgstr "Es ist verboten, Dateien grösser als {0} MB hochzuladen." msgid "KeyWord" msgstr "Schlüsselwort" msgid "Last operation" msgstr "Letzte Operation" msgid "Latest Deployment Log" msgstr "Letztes Bereitstellungslog" msgid "License" msgstr "Lizenz" msgid "Logs" msgstr "Logs" msgid "Logs (Created, Message)" msgstr "Logs (Erstellt, Nachricht)" msgid "Manage" msgstr "Verwalten" msgid "Manage Components" msgstr "Verwalte Komponenten" msgctxt "Package requirements" msgid "Manifest file" msgstr "Manifestdatei" msgid "Mark Image" msgstr "Markiere Abbild" msgid "" "Mark an image with Murano specific metadata to be added to the selected " "image." msgstr "" "Markiere ein Abbild mit Murano-spezifischen Metadaten, um es zum " "ausgewählten Abbild hinzuzufügen." msgid "Marked Images" msgstr "Markierte Abbilder" msgid "Modify Package" msgstr "Modifiziere Paket" msgid "Modifying package failed" msgstr "Modifizierung des Pakets fehlgeschlagen" msgid "NO ENVIRONMENTS" msgstr "KEINE UMGEBUNGEN" msgid "Name" msgstr "Name" msgid "Name of the bundle." msgstr "Name des Bündel." #, python-format msgid "Network of '%s'" msgstr "Netzwerk gehört zu '%s'" msgid "Next" msgstr "Nächste" msgid "Next Page" msgstr "Nächste Seite" msgid "No availability zones available" msgstr "Keine Verfügbarkeitszonen verfügbar" msgid "No categories available" msgstr "Keine Kategorien verfügbar" msgid "No components" msgstr "Keine Komponenten" msgid "No images available" msgstr "Keine Abbilder verfügbar" msgid "No keypair" msgstr "Kein Schlüsselpaar" msgid "No license" msgstr "Keine Lizenz" msgid "No recent activity to report at this time." msgstr "Derzeit sind keine jüngsten Aktivitäten vorhanden" msgid "No requirements" msgstr "Keine Anforderungen" msgid "No volumes available" msgstr "Keine Datenträger verfügbar" msgid "None" msgstr "Kein" msgid "Not in domain" msgstr "Nicht in Domain enthalten" msgid "Note" msgstr "Notiz" msgid "Number of Instances" msgstr "Anzahl der Instanzen" msgid "Number of VCPUs" msgstr "Anzahl der VCPUs" msgid "" "OpenStack Networking (Neutron) is not available in current environment. " "Custom Network Settings cannot be applied" msgstr "" "OpenStack Netzwerkdienst (Neutron) ist in dieser Umgebung nicht verfügbar. " "Benutzerdefinierte Netzwerkeinstellungen können nicht angewendet werden" msgid "Operation is forbidden by murano-api server." msgstr "Diese Operation wird vom Murano-API-Server nicht erlaubt." msgid "Optional" msgstr "Optional" msgid "Overview" msgstr "Übersicht" msgid "Package Bundle Source" msgstr "Paketbündelquelle" msgid "Package Count" msgstr "Paketanzahl" msgid "Package Details" msgstr "Paketdetails" msgid "Package Name" msgstr "Paketname" msgid "Package Source" msgstr "Paketquelle" msgid "Package Tags" msgstr "Paketschlagwörter" msgid "Package URL" msgstr "Paket-URL" msgid "Package Version" msgstr "Paketversion" msgid "" "Package creation failed.Reason: Can't find Package name from repository." msgstr "" "Paketerstellung fehlgeschlagen. Grund: Konnte Paketname nicht im Repository " "finden." msgid "Package creation failed.Reason: {0}" msgstr "Paketerstellung fehlgeschlagen. Grund: {0}" msgid "Package foo uploaded" msgstr "Paket foo hochgeladen" msgid "Package modified." msgstr "Paket modifiziert" msgid "Package name in the repository, usually a fully qualified name" msgstr "Paketname im Repository, normalerweise mit vollem Domänennamen" msgid "Package or Class with the same name is already made public" msgstr "Paket oder Klasse mit demselben Namen wurde schon veröffentlicht" msgid "Package parameters successfully updated." msgstr "Paketparameter erfolgreich aktualisiert." msgid "Package version" msgstr "Paketversion" msgid "Package with id foo_package_id is not found" msgstr "Paket mit ID foo_package wurde nicht gefunden" msgid "Package with id {0} is not found" msgstr "Paket mit ID {0} wurde nicht gefunden" msgid "Package with specified name already exists" msgstr "Ein Paket mit dem angegebenen Namen existiert bereits" msgid "Package {0} already registered." msgstr "Paket {0} ist schon registriert." msgid "Package {0} upload failed. {1}" msgstr "Paket {0} hochladen fehlgeschlagen. {1}" msgid "Package {0} uploaded" msgstr "Paket {0} hochgeladen" msgid "Packages" msgstr "Pakete" msgid "Packages should contain:" msgstr "Pakete sollen enthalten:" msgid "Please confirm your password" msgstr "Bitte bestätigen Sie Ihr Passwort" msgid "Please supply a bundle name" msgstr "Bitte einen Bündelnamen angeben" msgid "Please supply a bundle url" msgstr "Bitte eine Bündel-URL angeben" msgid "Please supply a package file" msgstr "Bitte geben Sie eine Paketdatei an" msgid "Please supply a package name" msgstr "Bitte geben Sie einen Paketnamen an" msgid "Please supply a package url" msgstr "Bitte geben Sie eine Paket-URL an" msgid "Previous Page" msgstr "Vorherige Seite" msgid "Provide comma-separated list of words, associated with the package" msgstr "Komma-separierte Liste von Wörtern im Zusammenhang mit dem Paket" msgid "Provide desired name for a new category" msgstr "Geben Sie den gewünschten Namen für eine neue Kategorie an" msgid "Public" msgstr "Öffentlich" msgid "Quick Deploy" msgstr "Schnell-Bereitstellung" msgid "Ready" msgstr "Fertig" msgid "Ready to configure" msgstr "Fertig zum Konfigurieren" msgid "Ready to deploy" msgstr "Fertig zum Bereitstellen" msgid "Recent Activity" msgstr "Jüngste Aktivitäten" msgid "Repository" msgstr "Repository" msgid "Requested object is not found on murano server." msgstr "Das angeforderte Objekt wurde auf dem Murano-Server nicht gefunden." msgid "Requested operation conflicts with an existing object." msgstr "" "Die angeforderte Operation führt zu einem Konflikt mit einem bestehenden " "Objekt." msgid "Requirements" msgstr "Anforderungen" msgid "Retype your password" msgstr "Geben Sie das Passwort erneut ein" msgid "Running" msgstr "Läuft" msgid "Running with errors" msgstr "Läuft mit Fehler" msgid "Running with warnings" msgstr "Läuft mit Warnungen" msgid "Select Application" msgstr "Anwendung auswählen" msgid "Select Image" msgstr "Abbild auswählen" #, python-format msgid "Select a valid choice. %(value)s is not one of the available choices." msgstr "Treffen Sie eine gültige Auswahl. %(value)s ist nicht verfügbar." msgid "Select an image registered in Glance Image Services." msgstr "Wählen Sie ein im Glance-Abbilddienst registriertes Abbild." msgid "" "Select an image type supported by Murano. Choose 'Custom type' to enter type " "manually." msgstr "" "Wählen Sie einen von Murano unterstütztes Abbild. Wählen Sie " "'Benutzerspezifischer Typ' um den Typ manuell einzugeben." msgid "Select one or more categories for a package." msgstr "Wählen Sie eine oder mehrere Kategorien für ein Paket." msgid "Select volume" msgstr "Datenträger auswählen" msgid "Services (Name, Type)" msgstr "Dienste (Name, Typ)" msgid "Set up for identifying a package." msgstr "Einrichtung zur Identifizierung eines Paketes." msgid "Show Details" msgstr "Zeige Details" msgid "Something went wrong during package downloading" msgstr "Beim herunterladen des Pakets ist ein Fehler aufgetreten" msgid "Sorry, this environment doesn't exist anymore" msgstr "Sorry, diese Umgebung existiert nicht mehr" msgid "" "Sorry, you can't add application right now. The environment is deploying." msgstr "" "Sie können derzeit keine Anwendung hinzufügen. Die Umgebung wird " "bereitgestellt." msgid "Sorry, you can't delete service right now" msgstr "Sorry, Sie können den Dienst jetzt nicht löschen" msgid "Specified title already in use. Please choose another one." msgstr "Der angegebene Titel existiert schon. Bitte wählen Sie einen anderen." msgid "Specifying a category helps to filter applications in the catalog" msgstr "" "Die Angabe einer Kategorie hilft beim Filtern von Applikationen im Katalog" msgid "Started Deleting Component" msgid_plural "Started Deleting Components" msgstr[0] "Löschung Komponente gesartet" msgstr[1] "Löschung Komponenten gestartet" msgid "Started Deleting Environment" msgid_plural "Started Deleting Environments" msgstr[0] "Lösche Umgebung gestartet" msgstr[1] "Lösche Umgebungen gestartet" msgid "Started deploying Environment" msgid_plural "Started deploying Environments" msgstr[0] "Bereitstellung Umgebung gestartet" msgstr[1] "Bereitstellung Umgebungen gestartet" msgid "Status" msgstr "Status" msgid "Step {0}" msgstr "Schritt {0}" msgid "Successful" msgstr "Erfolgreich" msgid "Tags" msgstr "Schlagwörter" msgid "Tenant Name" msgstr "Mandantenname" msgid "The '{0}' application successfully added to environment." msgstr "Die Anwendung '{0}' wurde erfolgreich zur Umgebung hinzugefügt." 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 "" "Die VMs der Anwendungen in dieser Umgebung werden diesem Netzwerk " "hinzugefügt, sofern sie nicht individuell konfiguriert wurden. Wählen Sie " "\"Neu erstellen\", um ein neues Netzwerk mit einem Subnetz zu erstellen, " "dessen IP-Bereich aus dem durch den Murano-Router des Projektes " "bereitgestellten Bereich stammt." #, python-format msgid "" "The bundle is going to be installed from %(murano_repo_url)s repository." msgstr "" "Das Bündel wird installiert vom %(murano_repo_url)s Repository." msgid "The environment name field cannot be empty." msgstr "Das Feld des Umgebungsnamens kann nicht leer sein." #, python-format msgid "" "The package is going to be imported from %(murano_repo_url)s repository." msgstr "" "Das Paket wird importiert von %(murano_repo_url)s Repository." msgid "" "The password must contain at least one letter, " "one number and one special character" msgstr "" "Das Passwort muss mindestens einen Buchstaben, eine Ziffer und ein " "Sonderzeichen enthalten" msgid "The request data is not acceptable by the server" msgstr "Die angeforderten Daten werden vom Server nicht akzeptiert." msgid "There are no applications in the catalog. You can import apps from" msgstr "" "Es sind keine Applikationen im Katalog. Sie können Apps importieren von" #, python-format msgid "" "There are no applications in the catalog. You can import apps from %(display_repo_url)s." msgstr "" "Es sind keine Applikationen im Katalog. Sie können Apps importieren von %(display_repo_url)s." msgid "There are no applications matching your criteria." msgstr "Es gibt keine Applikationen, die mit Ihren Kriterien übereinstimmen." msgid "There was an error communicating with server" msgstr "Es trat ein Fehler in der Kommunikation mit dem Server auf" msgid "There was an error initialising this field." msgstr "Ein Fehler in der Initialisierung dieses Feldes ist aufgetreten." msgid "" "This Application requires encryption, please contact your administrator to " "configure this." msgstr "" "Diese Applikation erfordert Verschlüsselung. Bitte kontaktieren Sie zur " "Konfiguration Ihren Administrator." msgid "" "This action cannot be undone. Any resources created by this environment will " "have to be released manually." msgstr "" "Diese Aktion kann nicht rückgängig gemacht werden. Alle von dieser Umgebung " "erstellten Ressourcen müssen manuell freigegeben werden." msgid "Time Finished" msgstr "Zeit beendet" msgid "Time Started" msgstr "Zeit gestartet" msgid "Time updated" msgstr "Zeit aktualisiert" msgid "Title" msgstr "Titel" msgid "Toggle Active" msgid_plural "Toggle Active" msgstr[0] "Umschalten auf aktiv" msgstr[1] "Umschalten auf aktiv" msgid "Toggle Enabled" msgstr "Umschalten aktiviert" msgid "Toggle Public" msgid_plural "Toggle Public" msgstr[0] "Umschalten auf öffentlich" msgstr[1] "Umschalten auf öffentlich" msgid "Toggled Active" msgid_plural "Toggled Active" msgstr[0] "Umgeschaltet auf aktiv" msgstr[1] "Umgeschaltet auf aktiv" msgid "Toggled Public" msgid_plural "Toggled Public" msgstr[0] "Umgeschaltet auf öffentlich" msgstr[1] "Umgeschaltet auf öffentlich" msgid "Topology" msgstr "Topologie" msgid "Total RAM" msgstr "RAM gesamt" msgid "" "Trying to add {0} image to glance. Image will be ready for deployment after " "successful upload" msgstr "" "Versuche {0} Abbild zu Glance hinzuzufügen. Das Abbild wird fertig zur " "Bereitstellungung sein, nachdem es erfolgreich hochgeladen wurde" msgid "" "Trying to add {0}, {1} image to glance. Image will be ready for deployment " "after successful upload" msgstr "" "Versuche {0}, {1} Abbild zu Glance hinzuzufügen. Das Abbild wird fertig zur " "Bereitstellung sein, nachdem es erfolgreich hochgeladen wurde" msgid "Type" msgstr "Typ" msgctxt "Package requirements" msgid "UI definition folder" msgstr "UI Definitionsverzeichnis" msgid "UNKNOWN" msgstr "UNBEKANNT" msgid "URL" msgstr "URL" msgid "Unable to abandon an environment {0} due to: {1}" msgstr "Konnte Umgebung {0} nicht verlassen wegen: {1}" msgid "Unable to communicate to glare-api server." msgstr "Kann nicht mit dem Glare-API-Server kommunizieren." msgid "Unable to communicate to murano-api server." msgstr "Mit dem Murano-API-Server kann nicht kommuniziert werden." msgid "Unable to create environment {0} due to: {1}" msgstr "Konnte Umgebung {0} nicht erstellen wegen: {1}" msgid "Unable to delete category" msgstr "Kategorie kann nicht gelöscht werden" msgid "Unable to delete environment {0} due to: {1}" msgstr "Konnte Umgebung {0} nicht löschen wegen: {1}" msgid "Unable to delete package in murano-api server" msgstr "Konnte Paket im Murano-API-Server nicht löschen" msgid "Unable to deploy. Try again later" msgstr "Konnte nicht bereitstellen. Bitte später nochmal versuchen" msgid "Unable to download package." msgstr "Paket kann nicht heruntergeladen werden." msgid "Unable to get list of categories" msgstr "Konnte Liste der Kategorien nicht abrufen" msgid "Unable to mark image" msgstr "Konnte Abbild nicht markieren" msgid "Unable to modify package" msgstr "Konnte Paket nicht modifizieren" msgid "Unable to remove metadata" msgstr "Konnte Metadaten nicht entfernen" msgid "Unable to remove package." msgstr "Konnte Paket nicht entfernen." msgid "Unable to retrieve availability zones." msgstr "Verfügbarkeitszonen können nicht abgerufen werden." msgid "Unable to retrieve deployment history." msgstr "Bereitstellungshistorie kann nicht abgerufen werden." msgid "Unable to retrieve details for service" msgstr "Konnte Dienstedetails nicht abrufen" msgid "Unable to retrieve list of deployments" msgstr "Konnte Liste der Bereitstellungen nicht abrufen" msgid "Unable to retrieve list of images" msgstr "Konnte Liste der Abbilder nicht abrufen" msgid "" "Unable to retrieve list of services. This environment is deploying or " "already deployed by other user." msgstr "" "Konnte Liste der Dienste nicht beziehen. Diese Umgebung wird gerade oder " "wurde von einem anderen Benutzer bereitgestellt." msgid "Unable to retrieve package details." msgstr "Paketdetails können nicht abgerufen werden." msgid "Unable to retrieve project list." msgstr "Konnte die Projektliste nicht abrufen" msgid "Unable to retrieve public images." msgstr "Öffentliche Abbilder können nicht abgerufen werden." msgid "Unable to retrieve snapshot list." msgstr "Konnte Liste der Schattenkopien nicht abrufen." msgid "Unable to retrieve volume list." msgstr "Liste der Datenträger kann nicht abgerufen werden." msgid "Unavailable" msgstr "Nicht verfügbar" msgid "Unknown" msgstr "Unbekannt" msgid "Update" msgstr "Aktualisieren" msgid "Update Environment" msgid_plural "Deploy Environments" msgstr[0] "Aktualisiere Umgebung" msgstr[1] "Stelle Umgebungen bereit" msgid "Update Image" msgstr "Aktualisiere Abbild" msgid "Update Metadata" msgstr "Metadaten aktualisieren" msgid "Update This Environment" msgstr "Aktualisiere diese Umgebung" msgid "Updated" msgstr "Aktualisiert" msgid "Updated Environment" msgid_plural "Deployed Environments" msgstr[0] "Umgebung aktualisiert" msgstr[1] "Umgebungen bereitgestellt" msgid "Uploading package failed. {0}" msgstr "Hochladen des Pakets fehlgeschlagen. {0}" msgid "Used for identifying and filtering packages." msgstr "Benutzt zur Identifizierung und Filterung von Paketen." msgid "Validation Error occurred" msgstr "Validierungsfehler aufgetreten" msgid "Version" msgstr "Version" msgid "Version of the package (optional)." msgstr "Version des Pakets (optional)." msgid "You are not allowed to change this properties of the package" msgstr "Sie sind nicht zum Ändern dieser Eigenschaften des Paketes berechtigt" msgid "You are not allowed to delete this package" msgstr "Sie haben keine Berechtigung zum Löschen dieses Paketes" msgid "You are not allowed to make packages public." msgstr "Sie haben keine Berechtigung zum veröffentlichen von Paketen." msgid "You are not allowed to perform this operation" msgstr "Sie haben keine Berechtigung zum Durchführen dieser Operation" msgid "" "You'll have to configure each package installed from this bundle separately." msgstr "" "Sie müssen jedes Paket, welches von diesem Bündel separat installiert wurde, " "einzeln konfigurieren." msgid "{0}{1} don't match" msgstr "{0}{1} stimmen nicht überein" murano-dashboard-5.0.0/muranodashboard/locale/de/LC_MESSAGES/djangojs.po0000666000175100017510000000430213245511125025712 0ustar zuulzuul00000000000000# Frank Kloeker , 2016. #zanata # Robert Simai , 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 \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:" murano-dashboard-5.0.0/muranodashboard/locale/pt_BR/0000775000175100017510000000000013245511556022413 5ustar zuulzuul00000000000000murano-dashboard-5.0.0/muranodashboard/locale/pt_BR/LC_MESSAGES/0000775000175100017510000000000013245511556024200 5ustar zuulzuul00000000000000murano-dashboard-5.0.0/muranodashboard/locale/pt_BR/LC_MESSAGES/django.po0000666000175100017510000006211113245511141025773 0ustar zuulzuul00000000000000# Andreas Jaeger , 2016. #zanata # André Franciosi , 2018. #zanata msgid "" msgstr "" "Project-Id-Version: murano-dashboard VERSION\n" "Report-Msgid-Bugs-To: https://bugs.launchpad.net/openstack-i18n/\n" "POT-Creation-Date: 2018-02-13 06:13+0000\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "PO-Revision-Date: 2018-02-15 01:57+0000\n" "Last-Translator: André Franciosi \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 "80 characters max." msgstr "Máximo 80 caracteres" msgid "A local zip file to upload" msgstr "Um arquivo zip local para enviar" msgid "Abandon Environment" msgid_plural "Abandon Environments" msgstr[0] "Abandonar Ambiente" msgstr[1] "Abandonar Ambientes" msgid "Abandoned Environment" msgid_plural "Abandoned Environments" msgstr[0] "Ambiente Abandonado" msgstr[1] "Ambientes Abandonados" msgid "Active" msgstr "Ativo" msgid "Add" msgstr "Adicionar" msgid "Add Application" msgstr "Adicionar Aplicação" msgid "Add Application Category" msgstr "Adicionar Categoria de Aplicação." msgid "Add Category" msgstr "Adicionar categoria" msgid "Add Component" msgstr "Adicionar componente" msgid "Add Murano Metadata" msgstr "Adicionar Metadados do Murano" msgid "Add New" msgstr "Adicionar Novo" msgid "Add new category to the application catalog." msgstr "Adicionar nova categoria ao catálogo de aplicações." msgid "Add to Env" msgstr "Adicionar ao ambiente" msgid "Adding application to an environment failed." msgstr "Falha ao adicionar a aplicação a um ambiente." msgid "All" msgstr "Todos" msgid "Allows adding additional information about a package." msgstr "Permite adicionar informações adicionais sobre o pacote." msgid "" "Allows to hide a package from the catalog. (Applies to package dependencies)" msgstr "" "Permite esconder um pacote do catálogo. (Se aplica as dependências do pacote)" msgid "" "An environment is a collection of applications that are meant to operate " "under similar conditions." msgstr "" "Um ambiente é uma coleção de aplicações que devem operar sob condições " "semelhantes." msgid "An external http/https URL to load the bundle from." msgstr "Uma URL http/https externa de onde o conjunto será carregado." msgid "An external http/https URL to load the package from." msgstr "Uma URL http/https externa de onde o pacote será carregado." msgid "App Category:" msgstr "Categoria de Aplicativos:" msgid "App category" msgstr "Categoria de aplicação" msgid "Application Categories" msgstr "Categoria de Aplicações" msgid "Application Category" msgstr "Categoria da Aplicação" msgid "Application Details" msgstr "Detalhes da aplicação" msgid "Application Package" msgstr "Pacote da aplicação" msgid "Application Components" msgstr "Aplicação Componentes" msgid "Applications" msgstr "Aplicações" msgid "Author" msgstr "Autor" msgid "Auto" msgstr "Auto" msgid "Back" msgstr "Voltar" msgid "Browse" msgstr "Procurar" 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 %(murano_repo_url)s repository." msgstr "" "O conjunto será instalado do repositório %(murano_repo_url)s." #, python-format msgid "" "The package is going to be imported from %(murano_repo_url)s repository." msgstr "" "O pacote será importado do repositório %(murano_repo_url)s." 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 "There was an error communicating with server" msgstr "Ocorreu um erro ao se comunicar com o servidor" 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 glare-api server." msgstr "Não foi possível se comunicar com o glare-api server." 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" murano-dashboard-5.0.0/muranodashboard/locale/pt_BR/LC_MESSAGES/djangojs.po0000666000175100017510000000424613245511141026335 0ustar zuulzuul00000000000000# Andreas Jaeger , 2016. #zanata # André Franciosi , 2018. #zanata msgid "" msgstr "" "Project-Id-Version: murano-dashboard VERSION\n" "Report-Msgid-Bugs-To: https://bugs.launchpad.net/openstack-i18n/\n" "POT-Creation-Date: 2018-02-13 06:13+0000\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "PO-Revision-Date: 2018-02-15 01:55+0000\n" "Last-Translator: André Franciosi \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 " 1 capital letter" msgstr "1 letra maiúscula" msgid " 1 digit" msgstr "1 dígito" msgid " 1 non-capital letter" msgstr "1 letra minúscula" msgid " 1 special character" msgstr "1 caracter especial" msgid " 7 characters" msgstr "7 caracteres" msgid "An error occurred. Please try again later." msgstr "Um erro ocorreu. Por favor tente novamente mais tarde." msgid "Cancel" msgstr "Cancelar" msgid "Create" msgstr "Criar" msgid "Loading" msgstr "Carregando" msgid "New" msgstr "Novo" msgid "Passwords do not match" msgstr "As senhas não conferem" msgid "Show less" msgstr "Mostre menos" msgid "Show more" msgstr "Mostre mais" msgid "There was an error submitting the form. Please try again." msgstr "Ocorreu um erro enviando o formulário. Por favor tente novamente." msgid "Unable to edit component metadata." msgstr "Não é possível editar os metadados do componente." msgid "Unable to edit environment metadata." msgstr "Não é possível editar os metadados do ambiente." msgid "Unable to retrieve component metadata." msgstr "Não é possível recuperar os metadados do componente." msgid "Unable to retrieve environment metadata." msgstr "Não é possível recuperar os metadados do ambiente." msgid "Unable to retrieve the packages." msgstr "Não é possível recuperar os pacotes." msgid "Unable to run action." msgstr "Não foi possível executar a ação." msgid "Waiting for a result" msgstr "Aguardando um resultado" msgid "Working" msgstr "Trabalhando" msgid "Your password should have at least" msgstr "Sua senha deve ter no mínimo" murano-dashboard-5.0.0/muranodashboard/images/0000775000175100017510000000000013245511556021413 5ustar zuulzuul00000000000000murano-dashboard-5.0.0/muranodashboard/images/views.py0000666000175100017510000000705613245511125023124 0ustar zuulzuul00000000000000# 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') murano-dashboard-5.0.0/muranodashboard/images/urls.py0000666000175100017510000000171713245511125022752 0ustar zuulzuul00000000000000# 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'), ] murano-dashboard-5.0.0/muranodashboard/images/panel.py0000666000175100017510000000136713245511125023065 0ustar zuulzuul00000000000000# 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' murano-dashboard-5.0.0/muranodashboard/images/__init__.py0000666000175100017510000000000013245511125023504 0ustar zuulzuul00000000000000murano-dashboard-5.0.0/muranodashboard/images/forms.py0000666000175100017510000001261013245511125023105 0ustar zuulzuul00000000000000# 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.conf import settings 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 images by project owner filter_project = getattr(settings, 'MURANO_IMAGE_FILTER_PROJECT_ID', None) if filter_project: project_ids = [filter_project] if request: project_ids.append(request.user.tenant_id) images = filter( lambda x: getattr(x, 'owner', None) in project_ids, list(images)) # 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')) title = forms.CharField(max_length="255", label=_("Title")) type = forms.ChoiceField( label=_("Type"), choices=_metadata.items(), initial='custom', widget=forms.Select(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 murano-dashboard-5.0.0/muranodashboard/images/tables.py0000666000175100017510000000520113245511125023227 0ustar zuulzuul00000000000000# 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,) murano-dashboard-5.0.0/muranodashboard/catalog/0000775000175100017510000000000013245511556021560 5ustar zuulzuul00000000000000murano-dashboard-5.0.0/muranodashboard/catalog/views.py0000666000175100017510000007113313245511125023266 0ustar zuulzuul00000000000000# 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 castellan.common import exception as castellan_exception 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 novaclient import exceptions as nova_exceptions from openstack_dashboard.api import nova from openstack_dashboard.usage import quotas 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 try: attributes = service.extract_attributes() except castellan_exception.KeyManagerError as e: msg = _("This Application requires encryption, please contact " "your administrator to configure this.") messages.error(self.request, msg) LOG.exception(e) raise attributes = helpers.insert_hidden_ids(attributes) storage = attributes.setdefault('?', {}).setdefault( consts.DASHBOARD_ATTRS_KEY, {}) storage['name'] = app_name attributes['?']['resourceUsages'] = self.aggregate_usages( self.init_usages()[1:]) 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_flavors(self): try: flavors = nova.flavor_list(self.request) except nova_exceptions.ClientException: message = _("Failed to get list of flavors.") exceptions.handle(self.request, message) LOG.exception(message) flavors = [] def extract(flavor): info = flavor._info return {k: v for (k, v) in info.items() if k != 'links'} flavors = [extract(f) for f in flavors] self.storage.extra_data['flavors'] = flavors return json.dumps(flavors) def get_flavor_usages(self, form): selected_flavor = form.cleaned_data['flavor'] for flavor in self.storage.extra_data['flavors']: if flavor['name'] == selected_flavor: return {'ram': flavor['ram'], 'vcpus': flavor['vcpus'], 'instances': 1} def init_usages(self): stored_data = self.storage.extra_data step_usages = stored_data.get('step_usages') if step_usages is None: step_usages = [ collections.defaultdict(dict) for step in self.steps.all ] stored_data['step_usages'] = step_usages environment_id = self.kwargs.get('environment_id') environment_id = utils.ensure_python_obj(environment_id) if environment_id is not None: session_id = env_api.Session.get(self.request, environment_id) client = api.muranoclient(self.request) all_services = client.environments.get( environment_id, session_id).services env_usages = self.aggregate_usages(map( lambda svc: svc['?'].get('resourceUsages', {}), all_services)) else: env_usages = collections.defaultdict(dict) step_usages.insert(0, env_usages) return step_usages def process_step(self, form): data = super(Wizard, self).process_step(form) region = form.region or self.request.user.services_region step_usages = self.init_usages() if 'flavor' in form.cleaned_data: usages = self.get_flavor_usages(form) step_usages[self.steps.step0 + 1][region].update({ 'ram': usages['ram'], 'vcpus': usages['vcpus'], 'instances': usages['instances'] }) else: step_usages[self.steps.step0 + 1][region].update({ 'ram': 0, 'vcpus': 0, 'instances': 0 }) return data def update_usages(self, form, context): data = self.init_usages() usages = quotas.tenant_quota_usages(self.request).usages region = self.request.user.services_region inf = float('inf') def get_usage(group, name, default): return usages.get(group, {}).get(name, default) context.update({ 'usages': { 'maxTotalInstances': get_usage('instances', 'quota', inf), 'totalInstancesUsed': get_usage('instances', 'used', 0), 'maxTotalCores': get_usage('cores', 'quota', inf), 'totalCoresUsed': get_usage('cores', 'used', 0), 'maxTotalRAMSize': get_usage('ram', 'quota', inf), 'totalRAMUsed': get_usage('ram', 'used', 0), }, 'other_usages': {}, 'flavors': self.get_flavors(), 'contexts': ['', 'info', 'success'] }) for step in range(self.steps.step0 + 1): def sum_usage(context_key, data_key): if context_key not in context['other_usages']: context['other_usages'][context_key] = 0 context['other_usages'][context_key] += \ data[step][region].get(data_key, 0) sum_usage('totalInstancesUsed', 'instances') sum_usage('totalCoresUsed', 'vcpus') sum_usage('totalRAMUsed', 'ram') return context @staticmethod def aggregate_usages(steps): result = collections.defaultdict(dict) for step in steps: for region, region_usages in six.iteritems(step): for metric, value in six.iteritems(region_usages): if metric not in result[region]: result[region][metric] = 0 result[region][metric] += value return result 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, }) with helpers.current_region(self.request, form.region): context = self.update_usages(form, context) 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) murano-dashboard-5.0.0/muranodashboard/catalog/tabs.py0000666000175100017510000001204513245511125023057 0ustar zuulzuul00000000000000# 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) murano-dashboard-5.0.0/muranodashboard/catalog/urls.py0000666000175100017510000000332213245511125023111 0ustar zuulzuul00000000000000# 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[^/]+)$', views.switch, name='switch_env'), urls.url(r'^add/(?P[^/]+)/(?P[^/]+)/' r'(?P[^/]+)/(?P[^/]+)$', wizard_view, name='add'), urls.url(r'^add/(?P[^/]+)/(?P[^/]+)$', views.deploy, name='deploy'), urls.url(r'^quick-add/(?P[^/]+)$', views.quick_deploy, name='quick_deploy'), urls.url(r'^details/(?P[^/]+)$', views.AppDetailsView.as_view(), name='application_details'), urls.url(r'^images/(?P[^/]*)', views.get_image, name="images"), urls.url(r'^supplier-images/(?P[^/]*)', views.get_supplier_image, name="supplier_images") ] murano-dashboard-5.0.0/muranodashboard/catalog/panel.py0000666000175100017510000000140213245511125023220 0ustar zuulzuul00000000000000# 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' murano-dashboard-5.0.0/muranodashboard/catalog/__init__.py0000666000175100017510000000000013245511125023651 0ustar zuulzuul00000000000000murano-dashboard-5.0.0/muranodashboard/catalog/forms.py0000666000175100017510000000327113245511125023255 0ustar zuulzuul00000000000000# 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] } murano-dashboard-5.0.0/muranodashboard/tests/0000775000175100017510000000000013245511556021310 5ustar zuulzuul00000000000000murano-dashboard-5.0.0/muranodashboard/tests/unit/0000775000175100017510000000000013245511556022267 5ustar zuulzuul00000000000000murano-dashboard-5.0.0/muranodashboard/tests/unit/common/0000775000175100017510000000000013245511556023557 5ustar zuulzuul00000000000000murano-dashboard-5.0.0/muranodashboard/tests/unit/common/test_net.py0000666000175100017510000001657313245511125025764 0ustar zuulzuul00000000000000# Copyright (c) 2016 AT&T 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. import mock import testtools from horizon import exceptions from muranodashboard.common import net class TestNet(testtools.TestCase): def setUp(self): super(TestNet, self).setUp() mock_request = mock.Mock() mock_request.user.tenant_id = 'foo_tenant_id' self.mock_request = mock_request mock_env_patcher = mock.patch.object(net, 'env_api', autospec=True) mock_env = mock.Mock() mock_env.configure_mock(name='foo') self.mock_env_api = mock_env_patcher.start() self.mock_env_api.environments_list.return_value = [mock_env] self.addCleanup(mock.patch.stopall) @mock.patch.object(net, 'neutron', autospec=True) def test_get_available_networks_with_filter_one(self, mock_neutron): foo_mock_network = mock.Mock(router__external=False, id='foo-network-id', subnets=[mock.Mock(id='foo-subnet-id')]) foo_mock_network.configure_mock(name='foo-network') bar_mock_network = mock.Mock() # Will be excluded by test_filter. bar_mock_network.configure_mock(name='bar-network') mock_neutron.network_list_for_tenant.return_value = [ foo_mock_network, bar_mock_network ] test_filter = '^foo\-[\w]+' result = net.get_available_networks(self.mock_request, filter=test_filter, murano_networks='include') expected_result = [ (('foo-network-id', 'foo-subnet-id'), "Network of 'foo'"), (('foo-network-id', None), "Network of 'foo': random subnet"), ] self.assertEqual(expected_result, result) mock_neutron.network_list_for_tenant.assert_called_once_with( self.mock_request, tenant_id='foo_tenant_id') self.mock_env_api.environments_list.assert_called_once_with( self.mock_request) @mock.patch.object(net, 'neutron', autospec=True) def test_get_available_networks_with_filter_none(self, mock_neutron): foo_mock_network = mock.Mock(router__external=False, id='foo-network-id', subnets=[mock.Mock(id='foo-subnet-id')]) foo_mock_network.configure_mock(name='foo-network') bar_mock_subnet = mock.Mock( id='bar-subnet-id', name_or_id='bar-subnet', cidr='255.0.0.0') bar_mock_network = mock.Mock(router__external=False, id='bar-network-id', name_or_id='bar-network', subnets=[bar_mock_subnet]) bar_mock_network.configure_mock(name='bar-network') mock_neutron.network_list_for_tenant.return_value = [ foo_mock_network, bar_mock_network ] test_filter = '^[\w]+\-[\w]+' result = net.get_available_networks(self.mock_request, filter=test_filter, murano_networks='include') expected_result = [ (('foo-network-id', 'foo-subnet-id'), "Network of 'foo'"), (('foo-network-id', None), "Network of 'foo': random subnet"), (('bar-network-id', 'bar-subnet-id'), 'bar-network: 255.0.0.0 bar-subnet'), (('bar-network-id', None), "bar-network: random subnet"), ] self.assertEqual(expected_result, result) mock_neutron.network_list_for_tenant.assert_called_once_with( self.mock_request, tenant_id='foo_tenant_id') self.mock_env_api.environments_list.assert_called_once_with( self.mock_request) @mock.patch.object(net, 'neutron', autospec=True) def test_get_available_networks(self, mock_neutron): foo_subnets = [ type('%s-subnet' % k, (object, ), {'id': '%s-subnet-id' % k, 'cidr': '255.0.0.0', 'name_or_id': '%s-name-or-id' % k}) for k in ('fake1', 'fake2')] bar_subnets = [ type('fake3-subnet', (object, ), {'id': 'fake3-subnet-id', 'cidr': '255.255.0.0', 'name_or_id': 'fake3-name-or-id'})] foo_network = type('FooNetwork', (object, ), { 'router__external': False, 'id': 'foo-network-id', 'subnets': foo_subnets, 'name': 'foo-network-name', 'name_or_id': 'foo-network-name-or-id', }) bar_network = type('BarNetwork', (object, ), { 'router__external': False, 'id': 'bar-network-id', 'subnets': bar_subnets, 'name': 'bar-network-name', 'name_or_id': 'bar-network-name-or-id', }) mock_neutron.network_list_for_tenant.return_value = [ foo_network, bar_network, ] result = net.get_available_networks( self.mock_request, filter=None, murano_networks='exclude') expected_result = [ ((foo_network.id, foo_subnets[0].id), '%s: %s %s' % ( foo_network.name_or_id, foo_subnets[0].cidr, foo_subnets[0].name_or_id)), ((foo_network.id, foo_subnets[1].id), '%s: %s %s' % ( foo_network.name_or_id, foo_subnets[1].cidr, foo_subnets[1].name_or_id)), ((foo_network.id, None), '%s: random subnet' % foo_network.name_or_id), ((bar_network.id, bar_subnets[0].id), '%s: %s %s' % ( bar_network.name_or_id, bar_subnets[0].cidr, bar_subnets[0].name_or_id)), ((bar_network.id, None), '%s: random subnet' % bar_network.name_or_id), ] self.assertIsInstance(result, list) self.assertEqual(len(expected_result), len(result)) for choice in expected_result: self.assertIn(choice, result) mock_neutron.network_list_for_tenant.assert_called_once_with( self.mock_request, tenant_id='foo_tenant_id') self.mock_env_api.environments_list.assert_called_once_with( self.mock_request) @mock.patch.object(net, 'LOG', autospec=True) @mock.patch.object(net, 'neutron', autospec=True) def test_get_available_networks_except_service_catalog_exception( self, mock_neutron, mock_log): mock_neutron.network_list_for_tenant.side_effect = \ exceptions.ServiceCatalogException('test_exception') result = net.get_available_networks(self.mock_request) self.assertEqual([], result) mock_log.warning.assert_called_once_with( 'Neutron not found. Assuming Nova Network usage') mock_neutron.network_list_for_tenant.assert_called_once_with( self.mock_request, tenant_id='foo_tenant_id') murano-dashboard-5.0.0/muranodashboard/tests/unit/common/test_utils.py0000666000175100017510000000514113245511125026323 0ustar zuulzuul00000000000000# 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. try: import cPickle as pickle except ImportError: import pickle import mock import testtools import yaql from muranodashboard.common import utils from muranodashboard.dynamic_ui import yaql_expression class TestUtils(testtools.TestCase): def test_parse_api_error(self): test_html = '

    Foo Header

    Foo Error ' self.assertEqual('Foo Error', utils.parse_api_error(test_html)) def test_parse_api_error_without_body(self): test_html = '' self.assertIsNone(utils.parse_api_error(test_html)) class TestCustomPickler(testtools.TestCase): def setUp(self): super(TestCustomPickler, self).setUp() self.custom_pickler = utils.CustomPickler(mock.Mock()) self.assertTrue(hasattr(self.custom_pickler.dump, '__call__')) self.assertTrue(hasattr(self.custom_pickler.clear_memo, '__call__')) def test_persistent_id(self): yaql_obj = mock.Mock(spec=yaql.factory.YaqlEngine) self.assertEqual('filtered:YaqlEngine', self.custom_pickler.persistent_id(yaql_obj)) def test_persistent_id_with_wrong_obj_type(self): self.assertIsNone(self.custom_pickler.persistent_id(None)) class TestCustomUnpickler(testtools.TestCase): def setUp(self): super(TestCustomUnpickler, self).setUp() self.custom_unpickler = utils.CustomUnpickler(mock.Mock()) self.assertTrue(hasattr(self.custom_unpickler.load, '__call__')) if 'noload' in dir(pickle.Unpickler): self.assertTrue(hasattr(self.custom_unpickler.noload, '__call__')) def test_persistent_load(self): result = self.custom_unpickler.persistent_load('filtered:YaqlEngine') self.assertEqual(yaql_expression.YAQL, result) def test_persistent_load_with_wrong_obj_type(self): e = self.assertRaises(pickle.UnpicklingError, self.custom_unpickler.persistent_load, None) self.assertEqual('Invalid persistent id', str(e)) murano-dashboard-5.0.0/muranodashboard/tests/unit/common/__init__.py0000666000175100017510000000000013245511125025650 0ustar zuulzuul00000000000000murano-dashboard-5.0.0/muranodashboard/tests/unit/categories/0000775000175100017510000000000013245511556024414 5ustar zuulzuul00000000000000murano-dashboard-5.0.0/muranodashboard/tests/unit/categories/__init__.py0000666000175100017510000000000013245511125026505 0ustar zuulzuul00000000000000murano-dashboard-5.0.0/muranodashboard/tests/unit/categories/test_views.py0000666000175100017510000001206213245511125027155 0ustar zuulzuul00000000000000# Copyright (c) 2016 AT&T 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 mock import testtools from muranodashboard.categories import tables from muranodashboard.categories import views class TestCategoriesView(testtools.TestCase): def setUp(self): super(TestCategoriesView, self).setUp() self.categories_view = views.CategoriesView() self.categories_view._prev = False self.categories_view._more = False mock_request = mock.Mock() self.categories_view.request = mock_request self.assertEqual(tables.CategoriesTable, self.categories_view.table_class) self.assertEqual('categories/index.html', self.categories_view.template_name) self.assertEqual('Application Categories', self.categories_view.page_title) mock_horizon_utils = mock.patch.object(views, 'utils').start() mock_horizon_utils.get_page_size.return_value = 2 self.addCleanup(mock.patch.stopall) def test_has_prev_data(self): self.assertFalse(self.categories_view.has_prev_data(None)) def test_has_more_data(self): self.assertFalse(self.categories_view.has_more_data(None)) @mock.patch.object(views, 'api', autospec=True) def test_get_data(self, mock_api): """Test that get_data works.""" mock_client = mock_api.muranoclient(mock.Mock()) mock_client.categories.list.return_value = [ 'foo_cat', 'bar_cat' ] self.categories_view.request.GET.get.return_value = 'foo_marker' result = self.categories_view.get_data() expected_categories = ['bar_cat', 'foo_cat'] expected_kwargs = { 'filters': {}, 'marker': 'foo_marker', 'sort_dir': 'asc', 'limit': 3 } self.assertEqual(expected_categories, result) self.assertTrue(self.categories_view.has_more_data(None)) self.assertFalse(self.categories_view.has_prev_data(None)) self.categories_view.request.GET.get.assert_called_once_with( tables.CategoriesTable._meta.prev_pagination_param, None) mock_client.categories.list.assert_called_once_with( **expected_kwargs) @mock.patch.object(views, 'api', autospec=True) def test_get_data_with_more_results(self, mock_api): """Test that get_data with pagesize smaller than result size works.""" mock_client = mock_api.muranoclient(mock.Mock()) mock_client.categories.list.return_value = [ 'foo_cat', 'bar_cat', 'baz_cat' ] self.categories_view.request.GET.get.return_value = 'foo_marker' result = self.categories_view.get_data() # Only two results should have been returned, with categories reversed. expected_categories = ['bar_cat', 'foo_cat'] expected_kwargs = { 'filters': {}, 'marker': 'foo_marker', 'sort_dir': 'asc', 'limit': 3 } self.assertEqual(expected_categories, result) self.assertTrue(self.categories_view.has_more_data(None)) self.assertTrue(self.categories_view.has_prev_data(None)) self.categories_view.request.GET.get.assert_called_once_with( tables.CategoriesTable._meta.prev_pagination_param, None) mock_client.categories.list.assert_called_once_with( **expected_kwargs) @mock.patch.object(views, 'api', autospec=True) def test_get_data_with_desc_sort_dir(self, mock_api): """Test that get_data with sort_dir = 'desc' works.""" mock_client = mock_api.muranoclient(mock.Mock()) mock_client.categories.list.return_value = [ 'foo_cat', 'bar_cat' ] self.categories_view.request.GET.get.side_effect = [None, 'bar_marker'] result = self.categories_view.get_data() expected_categories = ['foo_cat', 'bar_cat'] expected_kwargs = { 'filters': {}, 'marker': 'bar_marker', 'sort_dir': 'desc', 'limit': 3 } self.assertEqual(expected_categories, result) self.assertFalse(self.categories_view.has_more_data(None)) self.assertTrue(self.categories_view.has_prev_data(None)) self.categories_view.request.GET.get.assert_has_calls([ mock.call(tables.CategoriesTable._meta.prev_pagination_param, None), mock.call(tables.CategoriesTable._meta.pagination_param, None) ]) mock_client.categories.list.assert_called_once_with( **expected_kwargs) murano-dashboard-5.0.0/muranodashboard/tests/unit/environments/0000775000175100017510000000000013245511556025016 5ustar zuulzuul00000000000000murano-dashboard-5.0.0/muranodashboard/tests/unit/environments/test_topology.py0000666000175100017510000001601313245511125030276 0ustar zuulzuul00000000000000# Copyright (c) 2016 AT&T 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. import json import mock from muranodashboard.environments import consts from muranodashboard.environments import topology from openstack_dashboard.test import helpers class TestTopology(helpers.APITestCase): def setUp(self): super(TestTopology, self).setUp() self.mock_request = mock.Mock() @mock.patch.object(topology, 'reverse') @mock.patch.object(topology, 'pkg_cli') def test_get_app_image_with_package(self, mock_pkg_cli, mock_reverse): mock_package = mock.Mock(id='test_pkg_id') mock_pkg_cli.app_by_fqn.return_value = mock_package mock_reverse.return_value = '/foo/bar/baz' url = topology.get_app_image(self.mock_request, 'test_app_fqn') self.assertEqual('/foo/bar/baz', url) mock_reverse.assert_called_once_with( "horizon:app-catalog:catalog:images", args=('test_pkg_id',)) @mock.patch.object(topology, 'pkg_cli') def test_get_app_image_without_package(self, mock_pkg_cli): mock_pkg_cli.app_by_fqn.return_value = None for status in (consts.STATUS_ID_DEPLOY_FAILURE, consts.STATUS_ID_DELETE_FAILURE): url = topology.get_app_image(self.mock_request, 'test_app_fqn', status) self.assertEqual('/static/dashboard/img/stack-red.svg', url) url = topology.get_app_image(self.mock_request, 'test_app_fqn', consts.STATUS_ID_READY) self.assertEqual('/static/dashboard/img/stack-green.svg', url) for status in (consts.STATUS_ID_PENDING, consts.STATUS_ID_DEPLOYING, consts.STATUS_ID_DELETING, consts.STATUS_ID_NEW): url = topology.get_app_image(self.mock_request, 'test_app_fqn', status) self.assertEqual('/static/dashboard/img/stack-gray.svg', url) def test_get_environment_status_message(self): for status, expected_msg in ( (consts.STATUS_ID_DEPLOYING, 'Deployment is in progress'), (consts.STATUS_ID_DEPLOY_FAILURE, 'Deployment failed')): mock_entity = mock.Mock(status=status) in_progress, status_msg =\ topology._get_environment_status_message(mock_entity) self.assertTrue(in_progress) self.assertEqual(expected_msg, status_msg) def test_get_environment_status_message_misc_status(self): mock_entity = mock.Mock(status='foo_status') in_progress, status_msg =\ topology._get_environment_status_message(mock_entity) self.assertTrue(in_progress) self.assertEqual('', status_msg) def test_get_environment_status_message_in_progress(self): for status, expected_msg in ( (consts.STATUS_ID_PENDING, 'Waiting for deployment'), (consts.STATUS_ID_READY, 'Deployed')): mock_entity = mock.Mock(status=status) in_progress, status_msg =\ topology._get_environment_status_message(mock_entity) self.assertFalse(in_progress) self.assertEqual(expected_msg, status_msg) fake_entity = {'?': {'status': status}} in_progress, status_msg =\ topology._get_environment_status_message(fake_entity) self.assertFalse(in_progress) self.assertEqual(expected_msg, status_msg) def test_truncate_type(self): self.assertEqual('foo', topology._truncate_type('foo', 4)) self.assertEqual('...bar', topology._truncate_type('foo.bar', 4)) self.assertEqual('foo.bar', topology._truncate_type('foo.bar', 7)) @mock.patch.object(topology, 'loader') @mock.patch.object(topology, 'pkg_cli') def test_render_d3_data(self, mock_pkg_cli, mock_loader): mock_pkg_cli.app_by_fqn.return_value = None mock_loader.render_to_string.return_value = 'test_env_info' fake_services = [ { '?': { 'id': 'test_service_id', 'status': consts.STATUS_ID_READY, 'type': 'io.murano.resources.foo', }, 'name': 'foo', 'instance': { '?': { 'id': 'test_instance_id', 'type': 'io.murano.resources.bar', }, 'assignFloatingIp': True, 'extra': [{'name': 'bar'}], 'name': 'bar', 'ipAddresses': ['127.0.0.1'] } }, { '?': { 'id': 'test_alt_service_id', 'status': consts.STATUS_ID_PENDING, 'type': 'test_service_type', }, 'name': 'baz', 'instance': { '?': { 'id': 'test_alt_instance_id', 'type': 'test_instance_type', }, 'assignFloatingIp': False, 'name': 'qux', 'required_by': 'test_service_id' }, } ] mock_environment = mock.Mock( id='test_env_id', status=consts.STATUS_ID_READY, services=fake_services) mock_environment.configure_mock(name='test_env_name') expected_env = { 'id': 'test_env_id', 'in_progress': False, 'info_box': 'test_env_info', 'name': 'test_env_name', 'status': 'Deployed' } expected_node_ids = ( 'External_Network', 'test_service_id', 'test_instance_id', 'test_alt_service_id', 'test_alt_instance_id') result = topology.render_d3_data(self.request, mock_environment) result = json.loads(result) self.assertIsInstance(result, dict) self.assertIn('environment', result) self.assertIn('nodes', result) for key, val in expected_env.items(): self.assertEqual(val, result['environment'][key]) for node_id in expected_node_ids: self.assertIn(node_id, [node['id'] for node in result['nodes']]) def test_render_d3_data_without_environment(self): self.assertIsNone(topology.render_d3_data(self.request, None)) # Test without environment.services mock_env = mock.Mock(services=None) self.assertIsNone(topology.render_d3_data(self.request, mock_env)) murano-dashboard-5.0.0/muranodashboard/tests/unit/environments/test_tables.py0000666000175100017510000011277713245511125027712 0ustar zuulzuul00000000000000# Copyright (c) 2016 AT&T 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 import http as django_http import mock import testtools from horizon import tables as hz_tables from muranoclient.common import exceptions as exc from muranodashboard.environments import consts from muranodashboard.environments import tables from muranodashboard.packages import consts as pkg_consts class TestEnvironmentTables(testtools.TestCase): def test_check_row_actions_allowed(self): actions = mock.Mock() actions.table.data = None self.assertFalse(tables._check_row_actions_allowed(actions, "")) actions.table.data = ["test"] actions.allowed.return_value = False self.assertFalse(tables._check_row_actions_allowed(actions, "")) actions.allowed.return_value = True self.assertTrue(tables._check_row_actions_allowed(actions, "")) @mock.patch('muranodashboard.environments.api.deployments_list') def test_environment_has_deployed_services(self, deployments_list): deployments_list.return_value = False self.assertFalse(tables._environment_has_deployed_services('', '')) mock_deployment = mock.Mock() mock_deployment.description = {'services': 'service'} deployments_list.return_value = [mock_deployment] self.assertTrue(tables._environment_has_deployed_services('', '')) mock_deployment.description = {'services': None} self.assertFalse(tables._environment_has_deployed_services('', '')) @mock.patch('muranodashboard.environments.api.environment_get') def test_add_application_allowed(self, env_get): self.add_application = tables.AddApplication() self.add_application.table = mock.Mock() self.add_application.table.kwargs.get.return_value = "env_id" env_get.return_value = {'status': 'good', 'version': '1'} self.assertTrue(self.add_application.allowed("test", "test")) @mock.patch('muranodashboard.environments.tables.reverse') def test_add_application_get_link_url(self, reverse): self.add_application = tables.AddApplication() reverse.return_value = "reversed url" self.add_application.table = mock.Mock() self.add_application.table.kwargs = {'environment_id': 'id'} self.assertEqual('reversed url?next=reversed url', self.add_application.get_link_url()) def test_create_environment_allowed(self): self.create_environment = tables.CreateEnvironment() self.create_environment.table = mock.Mock() self.create_environment.table.data.return_value = True self.assertTrue(self.create_environment.allowed("test", "test")) @mock.patch('muranodashboard.environments.api.environment_create') def test_create_environment_action(self, create): self.create_environment = tables.CreateEnvironment() self.create_environment.action("", "") self.assertTrue(create.called) @mock.patch('muranodashboard.environments.tables.reverse') @mock.patch('muranodashboard.environments.tables.exceptions') def test_create_environment_action_fail(self, exceptions, reverse): self.create_environment = tables.CreateEnvironment() exceptions.handle.return_value = "" self.create_environment.action("", "") self.assertTrue(reverse.called) def test_delete_environment_allowed_with_environment(self): self.delete_environment = tables.DeleteEnvironment() test_environment = mock.Mock() test_environment.status = "test" self.assertTrue(self.delete_environment.allowed("", test_environment)) def test_delete_environment_action_present(self): self.assertEqual('Delete Environment', tables.DeleteEnvironment.action_present(1)) self.assertEqual('Delete Environments', tables.DeleteEnvironment.action_present(2)) def test_delete_environment_action_past(self): self.assertEqual('Started Deleting Environment', tables.DeleteEnvironment.action_past(1)) self.assertEqual('Started Deleting Environments', tables.DeleteEnvironment.action_past(2)) @mock.patch('muranodashboard.environments.tables.' '_check_row_actions_allowed') def test_delete_environment_allowed_without_environment(self, row_actions): self.delete_environment = tables.DeleteEnvironment() row_actions.return_value = True self.assertTrue(self.delete_environment.allowed("", "")) @mock.patch('muranodashboard.environments.api.environment_delete') def test_delete_environment_action(self, delete): self.delete_environment = tables.DeleteEnvironment() self.delete_environment.action("", "") self.assertTrue(delete.called) @mock.patch('muranodashboard.environments.tables.reverse') @mock.patch('muranodashboard.environments.tables.exceptions') def test_delete_environment_action_fail(self, exceptions, reverse): self.delete_environment = tables.DeleteEnvironment() exceptions.handle.return_value = "" self.delete_environment.action("", "") self.assertTrue(reverse.called) def test_abandon_environment_is_allowed_with_environment(self): self.abandon_environment = tables.AbandonEnvironment() test_environment = mock.Mock() test_environment.status = "test" self.assertTrue(self.abandon_environment.allowed("", test_environment)) test_environment.status = "pending" self.assertFalse(self.abandon_environment.allowed("", test_environment)) @mock.patch('muranodashboard.environments.tables.' '_check_row_actions_allowed') def test_abandoin_environment_is_allowed_without_environment(self, actions): self.abandon_environment = tables.AbandonEnvironment() actions.return_value = True self.assertTrue(self.abandon_environment.allowed("", "")) @mock.patch('muranodashboard.environments.api.environment_delete') def test_abandon_environment_action(self, delete): self.abandon_environment = tables.AbandonEnvironment() self.abandon_environment.action("", "") self.assertTrue(delete.called) @mock.patch('muranodashboard.environments.tables.reverse') @mock.patch('muranodashboard.environments.tables.exceptions') def test_abandon_environment_action_fail(self, exceptions, reverse): self.abandon_environment = tables.AbandonEnvironment() exceptions.handle.return_value = "" self.abandon_environment.action("", "") self.assertTrue(reverse.called) def test_abandon_environment_action_present(self): self.assertEqual('Abandon Environment', tables.AbandonEnvironment.action_present(1)) self.assertEqual('Abandon Environments', tables.AbandonEnvironment.action_present(2)) def test_abandon_environment_action_past(self): self.assertEqual('Abandoned Environment', tables.AbandonEnvironment.action_past(1)) self.assertEqual('Abandoned Environments', tables.AbandonEnvironment.action_past(2)) @mock.patch('muranodashboard.environments.tables.' '_get_environment_status_and_version') def test_delete_service_is_allowed(self, status): self.delete_service = tables.DeleteService() status.return_value = 'test', 'test' self.assertTrue(self.delete_service.allowed("", "")) @mock.patch('muranodashboard.environments.tables.api.service_delete') def test_delete_service_action(self, delete): self.delete_service = tables.DeleteService() self.delete_service.table = mock.Mock() self.delete_service.table.kwargs.get.return_value = "test" self.delete_service.table.data = [{'?': {'id': 'service_id'}}] self.delete_service.action("", 'service_id') self.assertTrue(delete.called) @mock.patch('muranodashboard.environments.tables.reverse') @mock.patch('muranodashboard.environments.tables.exceptions') def test_delete_service_action_fail(self, exceptions, reverse): self.delete_service = tables.DeleteService() exceptions.handle.return_value = "" self.delete_service.action("", "") self.assertTrue(reverse.called) def test_delete_service_action_present(self): self.assertEqual('Delete Component', tables.DeleteService.action_present(1)) self.assertEqual('Delete Components', tables.DeleteService.action_present(2)) def test_delete_service_action_past(self): self.assertEqual('Started Deleting Component', tables.DeleteService.action_past(1)) self.assertEqual('Started Deleting Components', tables.DeleteService.action_past(2)) @mock.patch('muranodashboard.environments.tables.' '_environment_has_deployed_services') def test_deploy_environment_is_allowed_with_environment(self, deployed): self.deploy_environment = tables.DeployEnvironment() deployed.return_value = True test_environment = mock.Mock() test_environment.status = "pending" self.assertTrue(self.deploy_environment.allowed("", test_environment)) deployed.return_value = False self.assertTrue(self.deploy_environment.allowed("", test_environment)) test_environment.status = "test" self.assertFalse(self.deploy_environment.allowed("", test_environment)) @mock.patch('muranodashboard.environments.tables.' '_check_row_actions_allowed') def test_deploy_environment_is_allowed_without_environment(self, actions): self.deploy_environment = tables.DeployEnvironment() actions.return_value = True self.assertTrue(self.deploy_environment.allowed("", "")) @mock.patch('muranodashboard.environments.tables.api.environment_deploy') def test_deploy_environment_action(self, deploy): self.deploy_environment = tables.DeployEnvironment() self.deploy_environment.action("", '') self.assertTrue(deploy.called) @mock.patch('muranodashboard.environments.tables.reverse') @mock.patch('muranodashboard.environments.tables.exceptions') def test_deploy_environment_action_fail(self, exceptions, reverse): self.deploy_environment = tables.DeployEnvironment() exceptions.handle.return_value = "" self.deploy_environment.action("", "") self.assertTrue(reverse.called) @mock.patch('muranodashboard.environments.tables.' '_get_environment_status_and_version') @mock.patch('muranodashboard.environments.tables.' '_environment_has_deployed_services') def test_deploy_this_environment_allowed_with_environment(self, deployed, status): self.deploy_environment = tables.DeployThisEnvironment() deployed.return_value = True status.return_value = consts.STATUS_ID_READY, "version" self.deploy_environment.table = mock.Mock() self.deploy_environment.table.kwargs = {'environment_id': 'id'} self.assertFalse(self.deploy_environment.allowed(None, None)) self.assertEqual('Update This Environment', self.deploy_environment.verbose_name) deployed.return_value = False self.assertFalse(self.deploy_environment.allowed(None, None)) self.assertEqual('Deploy This Environment', self.deploy_environment.verbose_name) status.return_value = "", 0 self.deploy_environment.table.data = None self.assertFalse(self.deploy_environment.allowed(None, None)) status.return_value = "", 0 self.deploy_environment.table.data = 'data' self.assertTrue(self.deploy_environment.allowed(None, None)) @mock.patch('muranodashboard.environments.tables.reverse') @mock.patch('muranodashboard.environments.tables.messages') @mock.patch('muranodashboard.environments.tables.api.environment_deploy') def test_deploy_this_environment_single(self, mock_deploy, mock_messages, reverse): self.deploy_environment = tables.DeployThisEnvironment() data_table = mock.Mock() data_table.kwargs = {'environment_id': 'id'} mock_deploy.side_effect = None self.deploy_environment.single(data_table, None, None) self.assertTrue(mock_messages.success.called) @mock.patch('muranodashboard.environments.tables.reverse') @mock.patch('muranodashboard.environments.tables.messages') @mock.patch('muranodashboard.environments.tables.api.environment_deploy') def test_deploy_this_environment_single_exception(self, mock_deploy, mock_messages, reverse): self.deploy_environment = tables.DeployThisEnvironment() data_table = mock.Mock() data_table.kwargs = {'environment_id': 'id'} mock_deploy.side_effect = Exception("test") self.assertRaises(BaseException, self.deploy_environment.single, data_table, None, None) def test_deploy_environment_action_present_deploy(self): self.assertEqual('Deploy Environment', tables.DeployEnvironment.action_present_deploy(1)) self.assertEqual('Deploy Environments', tables.DeployEnvironment.action_present_deploy(2)) def test_deploy_environment_action_past_deploy(self): self.assertEqual('Started deploying Environment', tables.DeployEnvironment.action_past_deploy(1)) self.assertEqual('Started deploying Environments', tables.DeployEnvironment.action_past_deploy(2)) def test_deploy_environment_action_present_update(self): self.assertEqual('Update Environment', tables.DeployEnvironment.action_present_update(1)) self.assertEqual('Deploy Environments', tables.DeployEnvironment.action_present_update(2)) def test_deploy_environment_action_past_update(self): self.assertEqual('Updated Environment', tables.DeployEnvironment.action_past_update(1)) self.assertEqual('Deployed Environments', tables.DeployEnvironment.action_past_update(2)) def test_show_environment_services(self): self.show_environment_services = tables.ShowEnvironmentServices() self.assertTrue(self.show_environment_services.allowed("", "")) @mock.patch.object(tables, 'reverse') def test_get_service_details_link(self, mock_reverse): mock_service = mock.MagicMock(environment_id='foo_env_id') mock_service.__getitem__.return_value = {'id': 'foo_service_id'} mock_reverse.return_value = 'test_url' url = tables.get_service_details_link(mock_service) self.assertEqual('test_url', url) mock_reverse.assert_called_once_with( 'horizon:app-catalog:environments:service_details', args=('foo_env_id', 'foo_service_id')) def test_get_service_type(self): test_datum = { '?': { consts.DASHBOARD_ATTRS_KEY: { 'name': 'foo_name' } } } self.assertEqual('foo_name', tables.get_service_type(test_datum)) class TestUpdateEnvironmentRow(testtools.TestCase): def setUp(self): super(TestUpdateEnvironmentRow, self).setUp() self.mock_data_table = mock.Mock() foo_column = mock.Mock(status='foo_status') foo_column.configure_mock(name='foo_column') bar_column = mock.Mock(status='bar_status') bar_column.configure_mock(name='bar_column') self.mock_data_table.columns = { 'foo_column': foo_column, 'bar_column': bar_column } self.mock_data_table._meta.status_columns = [ 'foo_column', 'bar_column' ] self.mock_data_table.attrs = {} self.mock_datum = mock.Mock(status='foo_status') self.addCleanup(mock.patch.stopall) def test_update_environment_row(self): data_table = tables.UpdateEnvironmentRow( self.mock_data_table, self.mock_datum) self.assertEqual('foo_status', data_table.attrs['status']) @mock.patch.object(tables, 'api') def test_get_data(self, mock_api): mock_api.environment_get.side_effect = None mock_api.environment_get.return_value = 'test_environment' data_table = tables.UpdateEnvironmentRow(self.mock_data_table) environment = data_table.get_data(None, 'foo_environment_id') self.assertEqual('test_environment', environment) mock_api.environment_get.assert_called_once_with( None, 'foo_environment_id') @mock.patch.object(tables, 'api') def test_get_data_except_http_not_found(self, mock_api): mock_api.environment_get.side_effect = exc.HTTPNotFound data_table = tables.UpdateEnvironmentRow(self.mock_data_table) with self.assertRaisesRegexp(django_http.Http404, None): data_table.get_data(None, 'foo_environment_id') @mock.patch.object(tables, 'api') def test_get_data_except_exception(self, mock_api): mock_api.environment_get.side_effect = Exception('foo_error') data_table = tables.UpdateEnvironmentRow(self.mock_data_table) with self.assertRaisesRegexp(Exception, 'foo_error'): data_table.get_data(None, 'foo_environment_id') class TestUpdateServiceRow(testtools.TestCase): def setUp(self): super(TestUpdateServiceRow, self).setUp() self.addCleanup(mock.patch.stopall) def test_update_service_row(self): update_service_row = tables.UpdateServiceRow(None) self.assertTrue(update_service_row.ajax) @mock.patch.object(tables, 'api') def test_get_data(self, mock_api): mock_api.service_get.return_value = 'foo_env' update_service_row = tables.UpdateServiceRow(mock.Mock()) update_service_row.table.kwargs = {'environment_id': 'foo_env_id'} update_service_row.get_data(None, 'foo_service_id') mock_api.service_get.assert_called_once_with( None, 'foo_env_id', 'foo_service_id') class TestUpdateName(testtools.TestCase): def setUp(self): super(TestUpdateName, self).setUp() self.addCleanup(mock.patch.stopall) @mock.patch.object(tables, 'policy') def test_allowed(self, mock_policy): expected_policy_rule = (("murano", "update_environment"),) update_name = tables.UpdateName() update_name.allowed(None, None, None) mock_policy.check.assert_called_once_with( expected_policy_rule, None) @mock.patch.object(tables, 'api_utils') def test_update_cell(self, mock_api_utils): mock_api_utils.muranoclient().environments.update.side_effect = None mock_datum = mock.Mock(id='foo_datum_id') update_name = tables.UpdateName() cell_value = 'foo_cell_value' result = update_name.update_cell(None, mock_datum, None, None, new_cell_value=cell_value) self.assertTrue(result) mock_api_utils.muranoclient().environments.update.\ assert_called_once_with('foo_datum_id', name='foo_cell_value') @mock.patch.object(tables, 'messages') def test_update_cell_except_value_error(self, mock_messages): expected_error_message = "The environment name field cannot be empty." update_name = tables.UpdateName() for cell_value in (None, ''): with self.assertRaisesRegexp(ValueError, expected_error_message): update_name.update_cell(None, None, None, None, new_cell_value=cell_value) mock_messages.warning.assert_called_once_with( None, expected_error_message) mock_messages.warning.reset_mock() @mock.patch.object(tables, 'LOG') @mock.patch.object(tables, 'messages') @mock.patch.object(tables, 'api_utils') def test_update_cell_except_http_conflict(self, mock_api_utils, mock_messages, mock_log): mock_api_utils.muranoclient().environments.update.side_effect =\ exc.HTTPConflict mock_datum = mock.Mock(id='foo_datum_id') expected_error_message = "Couldn't update environment. "\ "Reason: This name is already taken." update_name = tables.UpdateName() cell_value = 'foo_cell_value' with self.assertRaisesRegexp(ValueError, expected_error_message): update_name.update_cell(None, mock_datum, None, None, new_cell_value=cell_value) mock_api_utils.muranoclient().environments.update.\ assert_called_once_with('foo_datum_id', name='foo_cell_value') mock_messages.warning.assert_called_once_with( None, expected_error_message) mock_log.warning.assert_called_once_with(expected_error_message) @mock.patch.object(tables, 'exceptions') @mock.patch.object(tables, 'api_utils') def test_update_cell_except_exception(self, mock_api_utils, mock_exceptions): mock_api_utils.muranoclient().environments.update.side_effect =\ Exception update_name = tables.UpdateName() result = update_name.update_cell(None, None, None, None, None) self.assertFalse(result) mock_exceptions.handle.assert_called_once_with(None, ignore=True) class TestUpdateEnvMetadata(testtools.TestCase): def test_update_env_meta_data(self): kwargs = {'datum': 'foo_datum'} update_env_meta_data = tables.UpdateEnvMetadata(**kwargs) self.assertEqual("update_env_metadata", update_env_meta_data.name) self.assertEqual("Update Metadata", update_env_meta_data.verbose_name) self.assertFalse(update_env_meta_data.ajax) self.assertEqual("pencil", update_env_meta_data.icon) self.assertEqual({ "ng-controller": "MetadataModalHelperController as modal"}, update_env_meta_data.attrs) self.assertTrue(update_env_meta_data.preempt) self.assertEqual("foo_datum", update_env_meta_data.datum) def test_get_link_url(self): update_env_meta_data = tables.UpdateEnvMetadata() update_env_meta_data.session_id = 'foo_session_id' update_env_meta_data.attrs = {} result = update_env_meta_data.get_link_url(mock.Mock(id='foo_env_id')) self.assertEqual("javascript:void(0);", result) lindex = update_env_meta_data.attrs['ng-click'].find('{') rindex = update_env_meta_data.attrs['ng-click'].rfind('}') + 1 attrs = ast.literal_eval(update_env_meta_data.attrs['ng-click'] [lindex:rindex]) expected_attrs = { 'environment': 'foo_env_id', 'session': 'foo_session_id' } update_env_meta_data.attrs['ng-click'] =\ update_env_meta_data.attrs['ng-click'][:lindex] +\ update_env_meta_data.attrs['ng-click'][rindex + 2:] self.assertEqual("modal.openMetadataModal('muranoenv', true)", update_env_meta_data.attrs['ng-click']) for key, val in expected_attrs.items(): self.assertEqual(val, attrs[key]) def test_allowed(self): update_env_meta_data = tables.UpdateEnvMetadata() allowed_statuses = ( consts.STATUS_ID_READY, consts.STATUS_ID_PENDING, consts.STATUS_ID_DELETE_FAILURE, consts.STATUS_ID_DEPLOY_FAILURE, consts.STATUS_ID_NEW) disallowed_statuses = ( consts.STATUS_ID_DEPLOYING, consts.STATUS_ID_DELETING) for status in allowed_statuses: env = mock.Mock(status=status) self.assertTrue(update_env_meta_data.allowed(None, env)) for status in disallowed_statuses: env = mock.Mock(status=status) self.assertFalse(update_env_meta_data.allowed(None, env)) @mock.patch.object(tables, 'api') def test_update(self, mock_api): mock_api.Session.get_if_available.return_value = 'foo_session_id' update_env_meta_data = tables.UpdateEnvMetadata() datum = mock.Mock() datum.id = 'foo_env_id' update_env_meta_data.session_id = None update_env_meta_data.update(None, datum) self.assertEqual('foo_session_id', update_env_meta_data.session_id) mock_api.Session.get_if_available.assert_called_once_with( None, 'foo_env_id') class TestEnvironmentsTable(testtools.TestCase): @mock.patch.object(tables, 'reverse') @mock.patch.object(tables, 'policy') def test_environments_table(self, mock_policy, mock_reverse): mock_reverse.return_value = 'test_url' mock_env = mock.Mock(id='foo_env_id') envs_table = tables.EnvironmentsTable(None) self.assertEqual(tables.EnvironmentsTable.get_env_detail_link.__name__, envs_table.columns['name'].get_link_url.__name__) mock_policy.check.return_value = False url = envs_table.columns['name'].get_link_url(mock_env) self.assertIsNone(url) self.assertFalse(mock_reverse.called) mock_policy.check.return_value = True url = envs_table.columns['name'].get_link_url(mock_env) self.assertEqual('test_url', url) mock_reverse.assert_called_once_with( "horizon:app-catalog:environments:services", args=('foo_env_id',)) class TestUpdateMetadata(testtools.TestCase): def setUp(self): super(TestUpdateMetadata, self).setUp() update_metadata = tables.UpdateMetadata() self.assertEqual("update_metadata", update_metadata.name) self.assertEqual("Update Metadata", update_metadata.verbose_name) self.assertFalse(update_metadata.ajax) self.assertEqual("pencil", update_metadata.icon) self.assertIsNone(update_metadata.session_id) def test_get_link_url(self): """Test get_link_url. Because the dictionary part of ``attrs[ng-click]`` may have different key orders, extract it, convert it to an actual dict, and check that each value in the dict matches the expected value. """ update_metadata = tables.UpdateMetadata() update_metadata.table = mock.Mock() update_metadata.table.kwargs = {'environment_id': 'foo_env_id'} update_metadata.session_id = 'foo_session_id' test_service = { '?': { 'id': 'foo_service_id' } } result = update_metadata.get_link_url(test_service) self.assertEqual("javascript:void(0);", result) lindex = update_metadata.attrs['ng-click'].find('{') rindex = update_metadata.attrs['ng-click'].rfind('}') + 1 attrs = ast.literal_eval(update_metadata.attrs['ng-click'] [lindex:rindex]) expected_attrs = { 'environment': 'foo_env_id', 'session': 'foo_session_id', 'component': 'foo_service_id' } update_metadata.attrs['ng-click'] =\ update_metadata.attrs['ng-click'][:lindex] +\ update_metadata.attrs['ng-click'][rindex + 2:] self.assertEqual("modal.openMetadataModal('muranoapp', true)", update_metadata.attrs['ng-click']) for key, val in expected_attrs.items(): self.assertEqual(val, attrs[key]) @mock.patch.object(tables, 'api') def test_allowed(self, mock_api): update_metadata = tables.UpdateMetadata() mock_api.environment_get.return_value = mock.Mock( status=consts.STATUS_ID_READY) mock_table = mock.Mock() mock_table.kwargs = {'environment_id': 'foo_env_id'} update_metadata.table = mock_table self.assertTrue(update_metadata.allowed(None)) mock_api.environment_get.return_value = mock.Mock( status=consts.STATUS_ID_DEPLOYING) self.assertFalse(update_metadata.allowed(None)) mock_api.environment_get.assert_called_with(None, 'foo_env_id') @mock.patch.object(tables, 'api') def test_update(self, mock_api): mock_api.Session.get_if_available.return_value = 'foo_session_id' update_metadata = tables.UpdateMetadata() update_metadata.table = mock.Mock() update_metadata.table.kwargs = {'environment_id': 'foo_env_id'} update_metadata.session_id = None update_metadata.update(None, None) self.assertEqual('foo_session_id', update_metadata.session_id) mock_api.Session.get_if_available.assert_called_once_with( None, 'foo_env_id') class TestServicesTable(testtools.TestCase): def test_get_object_id(self): test_datum = {'?': {'id': 'foo'}} services_table = tables.ServicesTable(None) self.assertEqual('foo', services_table.get_object_id(test_datum)) @mock.patch.object(tables, 'pkg_api') def test_get_apps_list(self, mock_pkg_api): foo_app = mock.Mock() foo_app.to_dict.return_value = {'foo': 'bar'} baz_app = mock.Mock() baz_app.to_dict.return_value = {'baz': 'qux'} mock_pkg_api.package_list.return_value = ( [foo_app, baz_app], True ) services_table = tables.ServicesTable(None) services_table.request = None services_table._more = False expected = [{'foo': 'bar'}, {'baz': 'qux'}] result = services_table.get_apps_list() for entry in expected: self.assertIn(entry, result) mock_pkg_api.package_list.assert_called_once_with( None, filters={'type': 'Application', 'catalog': True}) @mock.patch.object(tables, 'api') def test_actions_allowed(self, mock_api): services_table = tables.ServicesTable(None) mock_api.environment_get.return_value = mock.Mock( status=consts.STATUS_ID_READY) services_table.kwargs = {'environment_id': 'foo_env_id'} self.assertTrue(services_table.actions_allowed()) mock_api.environment_get.return_value = mock.Mock( status=consts.STATUS_ID_DEPLOYING) self.assertFalse(services_table.actions_allowed()) mock_api.environment_get.assert_called_with(None, 'foo_env_id') @mock.patch.object(tables, 'catalog_views') def test_categories_list(self, mock_catalog_views): mock_catalog_views.get_categories_list.return_value = [] services_table = tables.ServicesTable(None) services_table.request = None self.assertEqual([], services_table.get_categories_list()) mock_catalog_views.get_categories_list.assert_called_once_with(None) @mock.patch('horizon.tables.actions.LinkAction.get_link_url') @mock.patch.object(tables, '_get_environment_status_and_version') @mock.patch.object(tables, 'reverse') @mock.patch.object(tables, 'api') def test_get_row_actions( self, mock_api, mock_reverse, mock_get_env_attrs, _): mock_api.extract_actions_list.return_value = [ {'name': 'foo_bar', 'title': 'Foo Bar', 'id': 'foo_id'}, {'name': 'baz_qux', 'title': 'Baz Qux', 'id': 'baz_id'} ] mock_reverse.return_value = 'test_url' mock_get_env_attrs.return_value = (consts.STATUS_ID_READY, None) mock_api.Session.get_if_available.return_value = 'session_id' services_table = tables.ServicesTable(None) services_table.kwargs = {'environment_id': 'foo_env_id'} mock_datum = mock.MagicMock() service = {'?': {'id': 'comp_id'}} mock_datum.__getitem__.side_effect = lambda key: service[key] actions = services_table.get_row_actions(mock_datum) custom_actions = [] self.assertGreater(len(actions), 0) for action in actions: if action.__class__.__name__ == 'CustomAction': custom_actions.append(action) custom_actions = sorted(custom_actions, key=lambda action: action.name) self.assertEqual(2, len(custom_actions)) self.assertEqual('baz_qux', custom_actions[0].name) self.assertEqual('Baz Qux', custom_actions[0].verbose_name) self.assertEqual('foo_bar', custom_actions[1].name) self.assertEqual('Foo Bar', custom_actions[1].verbose_name) @mock.patch.object(tables, '_get_environment_status_and_version') @mock.patch.object(tables, 'reverse') @mock.patch.object(tables, 'api') def test_get_row_actions_with_no_action_allowed_status( self, mock_api, mock_reverse, mock_get_env_attrs): mock_api.extract_actions_list.return_value = [ {'name': 'foo_bar', 'title': 'Foo Bar', 'id': 'foo_id'}, {'name': 'baz_qux', 'title': 'Baz Qux', 'id': 'baz_id'} ] mock_reverse.return_value = 'test_url' mock_get_env_attrs.return_value = (consts.STATUS_ID_DEPLOYING, None) services_table = tables.ServicesTable(None) services_table.kwargs = {'environment_id': 'foo_env_id'} mock_datum = mock.MagicMock() actions = services_table.get_row_actions(mock_datum) self.assertEqual([], actions) def test_get_repo_url(self): services_table = tables.ServicesTable(None) self.assertEqual(pkg_consts.DISPLAY_MURANO_REPO_URL, services_table.get_repo_url()) @mock.patch.object(tables, 'reverse') def test_get_pkg_def_url(self, mock_reverse): mock_reverse.return_value = 'test_url' services_table = tables.ServicesTable(None) self.assertEqual('test_url', services_table.get_pkg_def_url()) mock_reverse.assert_called_once_with( 'horizon:app-catalog:packages:index') class TestShowDeploymentDetails(testtools.TestCase): @mock.patch.object(tables, 'reverse') def test_get_link_url(self, mock_reverse): mock_reverse.return_value = 'test_url' mock_deployment = mock.Mock( id='foo_deployment_id', environment_id='foo_env_id') expected_kwargs = {'environment_id': 'foo_env_id', 'deployment_id': 'foo_deployment_id'} show_deployment_details = tables.ShowDeploymentDetails() url = show_deployment_details.get_link_url(mock_deployment) self.assertEqual('test_url', url) mock_reverse.assert_called_once_with( 'horizon:app-catalog:environments:deployment_details', kwargs=expected_kwargs) def test_allowed(self): show_deployment_details = tables.ShowDeploymentDetails() self.assertTrue(show_deployment_details.allowed(None, None)) class TestEnvConfigTable(testtools.TestCase): def test_get_object_id(self): env_config_table = tables.EnvConfigTable(None) self.assertEqual('foo', env_config_table.get_object_id({ '?': {'id': 'foo'}})) class TestDeploymentHistoryTable(testtools.TestCase): def setUp(self): super(TestDeploymentHistoryTable, self).setUp() deployment_history_table = tables.DeploymentHistoryTable( mock.Mock()) columns = deployment_history_table.columns self.assertIsInstance(columns['environment_name'], hz_tables.WrappingColumn) self.assertIsInstance(columns['logs'], hz_tables.Column) self.assertIsInstance(columns['services'], hz_tables.Column) self.assertIsInstance(columns['status'], hz_tables.Column) self.assertEqual('Environment', str(columns['environment_name'])) self.assertEqual('Logs (Created, Message)', str(columns['logs'])) self.assertEqual('Services (Name, Type)', str(columns['services'])) self.assertEqual('Status', str(columns['status'])) self.assertTrue(columns['status'].status) self.assertEqual(consts.DEPLOYMENT_STATUS_DISPLAY_CHOICES, columns['status'].display_choices) @mock.patch.object(tables, 'template') def test_get_deployment_history_services(self, mock_template): mock_template.loader.render_to_string.return_value = \ mock.sentinel.rendered_template test_description = {'services': [ {'name': 'foo_service', '?': {'type': 'foo/bar', 'name': 'foo_service'}}, {'name': 'bar_service', '?': {'type': 'baz/qux', 'name': 'bar_service'}} ]} mock_deployment = mock.Mock(description=test_description) result = tables.get_deployment_history_services(mock_deployment) self.assertEqual(mock.sentinel.rendered_template, result) expected_services = { 'services': { 'bar_service': 'baz', 'foo_service': 'foo' } } mock_template.loader.render_to_string.assert_called_once_with( 'deployments/_cell_services.html', expected_services) murano-dashboard-5.0.0/muranodashboard/tests/unit/environments/test_rest_api.py0000666000175100017510000001257713245511125030243 0ustar zuulzuul00000000000000# Copyright (c) 2016 AT&T 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. from django import http import mock from muranodashboard.api.rest import environments from openstack_dashboard.test import helpers PARAM_MAPPING = { 'None': None, 'True': True, 'False': False } @mock.patch.object(environments, 'api') @mock.patch.object(environments, 'env_api') class TestComponentsMetadataAPI(helpers.APITestCase): def setUp(self): super(TestComponentsMetadataAPI, self).setUp() self.request = mock.Mock(body='{"foo": "bar"}', DATA={"foo"}) self.request.GET = dict(PARAM_MAPPING) self.components_metadata = environments.ComponentsMetadata() self.addCleanup(mock.patch.stopall) def test_get(self, mock_env_api, mock_api): mock_sess = mock.Mock() mock_component = mock.Mock() test_response = http.HttpResponse( "Foobar metadata response.", content_type="text/plain") mock_component.to_dict.return_value = { '?': { 'metadata': test_response } } mock_env_api.Session.get_or_create_or_delete.return_value = mock_sess mock_api.muranoclient().services.get.return_value = mock_component response = self.components_metadata.get( self.request, 'foo_env', 'foo_component') self.assertEqual(test_response, response) self.assertEqual(b"Foobar metadata response.", response.content) mock_env_api.Session.get_or_create_or_delete.assert_called_once_with( self.request, 'foo_env') mock_api.muranoclient().services.get.assert_called_once_with( 'foo_env', '/foo_component', mock_sess) def test_get_empty_response(self, mock_env_api, mock_api): mock_env_api.Session.get_or_create_or_delete.return_value = mock.Mock() mock_api.muranoclient().services.get.return_value = None result = self.components_metadata.get( self.request, None, 'foo_component') self.assertEqual(200, result.status_code) self.assertEqual(b'{}', result.content) def test_post_updated(self, mock_env_api, mock_api): mock_sess = mock.Mock() mock_env_api.Session.get_or_create_or_delete.return_value = mock_sess self.request.body = '{"updated": true}' self.components_metadata.post(self.request, 'foo_env', 'foo_component') mock_env_api.Session.get_or_create_or_delete.assert_called_once_with( self.request, 'foo_env') mock_api.muranoclient.assert_called_once_with(self.request) mock_api.muranoclient().services.put.assert_called_once_with( 'foo_env', '/foo_component/%3F/metadata', True, mock_sess) @mock.patch.object(environments, 'api') @mock.patch.object(environments, 'env_api') class TestEnvironmentsMetadataApi(helpers.APITestCase): def setUp(self): super(TestEnvironmentsMetadataApi, self).setUp() self.request = mock.Mock(body='{"foo": "bar"}', DATA={"foo"}) self.request.GET = dict(PARAM_MAPPING) self.envs_metadata = environments.EnvironmentsMetadata() self.addCleanup(mock.patch.stopall) def test_get(self, mock_env_api, mock_api): mock_sess = mock.Mock() http_response = http.HttpResponse( "Foobar metadata response.", content_type="text/plain") test_response = { '?': { 'metadata': http_response } } mock_env_api.Session.get_or_create_or_delete.return_value = mock_sess mock_api.muranoclient().environments.get_model.return_value =\ test_response response = self.envs_metadata.get(self.request, 'foo_env') self.assertEqual(http_response, response) self.assertEqual(b"Foobar metadata response.", response.content) mock_env_api.Session.get_or_create_or_delete.assert_called_once_with( self.request, 'foo_env') mock_api.muranoclient().environments.get_model.assert_called_once_with( 'foo_env', '/', mock_sess) def test_get_empty_response(self, mock_env_api, mock_api): mock_env_api.Session.get_or_create_or_delete.return_value = mock.Mock() mock_api.muranoclient().environments.get_model.return_value = None result = self.envs_metadata.get(self.request, 'foo_env') self.assertEqual(200, result.status_code) self.assertEqual(b'{}', result.content) def test_post(self, mock_env_api, mock_api): mock_sess = mock.Mock() mock_env_api.Session.get_or_create_or_delete.return_value = mock_sess self.request.body = '{"updated": true}' self.envs_metadata.post(self.request, 'foo_env') expected_patch = { "op": "replace", "path": "/?/metadata", "value": True } mock_api.muranoclient().environments.update_model.\ assert_called_once_with('foo_env', [expected_patch], mock_sess) murano-dashboard-5.0.0/muranodashboard/tests/unit/environments/__init__.py0000666000175100017510000000000013245511125027107 0ustar zuulzuul00000000000000murano-dashboard-5.0.0/muranodashboard/tests/unit/environments/test_views.py0000666000175100017510000006370113245511125027565 0ustar zuulzuul00000000000000# Copyright (c) 2016 AT&T 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 from django.conf import settings from django import http from django.utils.translation import ugettext_lazy as _ import mock import sys import testtools from horizon import conf from muranoclient.common import exceptions as exc from muranodashboard.environments import forms as env_forms from muranodashboard.environments import tables as env_tables from muranodashboard.environments import tabs as env_tabs from muranodashboard.environments import views @mock.patch.object(views, 'exceptions') @mock.patch.object(views, 'api') class TestIndexView(testtools.TestCase): def setUp(self): super(TestIndexView, self).setUp() self.index_view = views.IndexView() self.index_view.request = mock.Mock() self.assertEqual(env_tables.EnvironmentsTable, self.index_view.table_class) self.assertEqual('environments/index.html', self.index_view.template_name) self.assertEqual('Environments', self.index_view.page_title) def test_get_data(self, mock_api, mock_exc): mock_api.environments_list.return_value = ['foo_env', 'bar_env'] environments = self.index_view.get_data() self.assertEqual(['foo_env', 'bar_env'], environments) def test_get_data_exception_communication_error(self, mock_api, mock_exc): mock_api.environments_list.side_effect = exc.CommunicationError self.index_view.get_data() mock_exc.handle.assert_called_once_with( self.index_view.request, 'Could not connect to Murano API ' 'Service, check connection details') def test_get_data_exception_http_internal_server_error( self, mock_api, mock_exc): mock_api.environments_list.side_effect = exc.HTTPInternalServerError self.index_view.get_data() mock_exc.handle.assert_called_once_with( self.index_view.request, 'Murano API Service is not responding. ' 'Try again later') def test_get_data_exception_http_unauthorized_error( self, mock_api, mock_exc): mock_api.environments_list.side_effect = exc.HTTPUnauthorized self.index_view.get_data() mock_exc.handle.assert_called_once_with( self.index_view.request, ignore=True, escalate=True) class TestEnvironmentDetails(testtools.TestCase): def setUp(self): super(TestEnvironmentDetails, self).setUp() mock_request = mock.Mock() mock_request.user.service_catalog = None mock_token = mock.MagicMock() mock_token.tenant.__getitem__.return_value = 'foo_tenant_id' mock_request.session = {'token': mock_token} mock_tab_group = mock.Mock() self.env_details = views.EnvironmentDetails() self.env_details.request = mock_request self.env_details.tab_group_class = mock_tab_group self.env_details.kwargs = {'environment_id': 'foo_env_id'} self.assertEqual('services/index.html', self.env_details.template_name) self.assertEqual('{{ environment_name }}', self.env_details.page_title) self.addCleanup(mock.patch.stopall) @mock.patch.object(views, 'reverse_lazy') @mock.patch.object(views, 'api') def test_get_context_data(self, mock_api, mock_reverse_lazy): setattr(settings, 'MURANO_USE_GLARE', False) mock_env = mock.Mock() mock_env.configure_mock(name='foo_env') mock_env.id = 'foo_env_id' mock_deployment = mock.Mock(id='foo_deployment') mock_reverse_lazy.return_value = 'foo_redirect_url' mock_api.environment_get.return_value = mock_env mock_api.deployments_list.return_value = [mock_deployment] mock_api.deployment_reports.return_value = [] context = self.env_details.get_context_data() expected_context = { 'tab_group': self.env_details.tab_group_class(), 'tenant_id': 'foo_tenant_id', 'environment_name': 'foo_env', 'poll_interval': conf.HORIZON_CONFIG['ajax_poll_interval'], 'actions': mock.ANY, 'url': 'foo_redirect_url', 'view': self.env_details } for key, val in expected_context.items(): self.assertEqual(val, context[key]) self.assertNotIn('__action_show', context['actions']) self.assertNotIn('__action_deploy', context['actions']) @mock.patch.object(views, 'reverse_lazy') @mock.patch.object(views, 'exceptions') @mock.patch.object(views, 'api') def test_get_context_data_except_exception( self, mock_api, mock_exceptions, mock_reverse_lazy): mock_reverse_lazy.return_value = 'foo_redirect_url' mock_api.environment_get.side_effect = Exception expected_msg = "Sorry, this environment doesn't exist anymore" self.env_details.get_context_data() mock_exceptions.handle.assert_called_once_with( self.env_details.request, expected_msg, redirect='foo_redirect_url') @mock.patch.object(views, 'reverse_lazy') @mock.patch.object(views, 'exceptions') @mock.patch.object(views, 'api') def test_get_tabs_except_http_exception( self, mock_api, mock_exceptions, mock_reverse_lazy): mock_reverse_lazy.return_value = 'foo_redirect_url' mock_api.deployments_list.side_effect = exc.HTTPException expected_msg = "Unable to retrieve list of deployments" result = self.env_details.get_tabs(None) self.assertEqual(self.env_details.tab_group_class(), result) mock_exceptions.handle.assert_called_once_with( self.env_details.request, expected_msg, redirect='foo_redirect_url') self.env_details.tab_group_class.assert_any_call(None, logs=[]) @mock.patch.object(views, 'api') class TestDetailServiceView(testtools.TestCase): def setUp(self): super(TestDetailServiceView, self).setUp() self.detail_service_view = views.DetailServiceView() self.detail_service_view.kwargs = { 'service_id': 'foo_service_id', 'environment_id': 'foo_env_id' } self.mock_request = mock.Mock(GET={}) self.mock_request.user.service_catalog = None self.mock_request.is_ajax.return_value = True self.mock_request.horizon = { 'async_messages': [('tag', 'msg', 'extra')] } self.detail_service_view.request = self.mock_request self.assertEqual(env_tabs.ServicesTabs, self.detail_service_view.tab_group_class) self.assertEqual('services/details.html', self.detail_service_view.template_name) self.assertEqual('{{ service_name }}', self.detail_service_view.page_title) @mock.patch('horizon.tables.views.MultiTableMixin.get_context_data') @mock.patch.object(views, 'reverse') def test_get_context_data( self, mock_reverse, mock_get_context_data, mock_api): mock_service = mock.MagicMock() mock_service.configure_mock(name='foo_service_name') mock_env = mock.Mock() mock_env.configure_mock(name='foo_env_name') mock_api.service_get.return_value = mock_service mock_api.environment_get.return_value = mock_env mock_reverse.return_value = 'foo_reverse_url' mock_get_context_data.return_value = {} context = self.detail_service_view.get_context_data() expected_context = { 'service': mock_service, 'service_name': 'foo_service_name', 'environment_name': 'foo_env_name', 'custom_breadcrumb': [ ('foo_env_name', 'foo_reverse_url'), (_('Applications'), None) ] } for key, val in expected_context.items(): self.assertEqual(val, context[key]) self.assertEqual(mock_service, self.detail_service_view.service) self.assertEqual(mock_service, self.detail_service_view._service) mock_api.service_get.assert_any_call( self.mock_request, 'foo_env_id', 'foo_service_id') mock_api.environment_get.assert_any_call( self.mock_request, 'foo_env_id') @mock.patch.object(views, 'exceptions') def test_get_data_except_http_unauthorized(self, mock_exc, mock_api): mock_api.service_get.side_effect = exc.HTTPUnauthorized self.detail_service_view.get_data() mock_exc.handle.assert_called_once_with(self.mock_request) @mock.patch.object(views, 'reverse') @mock.patch.object(views, 'exceptions') def test_get_data_except_http_forbidden(self, mock_exc, mock_reverse, mock_api): mock_api.service_get.side_effect = exc.HTTPForbidden mock_reverse.return_value = 'foo_redirect_url' self.detail_service_view.get_data() mock_exc.handle.assert_called_once_with( self.mock_request, _('Unable to retrieve details for service'), redirect='foo_redirect_url') def test_get_tabs(self, mock_api): result = self.detail_service_view.get_tabs(self.mock_request) self.assertIsInstance(result, env_tabs.ServicesTabs) class TestCreateEnvironmentView(testtools.TestCase): def setUp(self): super(TestCreateEnvironmentView, self).setUp() mock_request = mock.Mock(session={}) mock_request.GET = {'next': 'next_foo_url'} mock_request.user.service_catalog = None self.create_env_view = views.CreateEnvironmentView() self.create_env_view.submit_url = 'foo_reverse_url' self.create_env_view.request = mock_request self.assertEqual(env_forms.CreateEnvironmentForm, self.create_env_view.form_class) self.assertEqual('create_environment_form', self.create_env_view.form_id) self.assertEqual(_('Create Environment'), self.create_env_view.modal_header) self.assertEqual('environments/create.html', self.create_env_view.template_name) self.assertEqual(_('Create Environment'), self.create_env_view.page_title) self.assertEqual('environment', self.create_env_view.context_object_name) self.assertEqual(_('Create'), self.create_env_view.submit_label) self.assertEqual('foo_reverse_url', self.create_env_view.submit_url) @mock.patch('muranodashboard.environments.forms.net') def test_get_form(self, mock_net): mock_net.get_available_networks.return_value = None form = self.create_env_view.get_form() self.assertIsInstance(form, env_forms.CreateEnvironmentForm) self.assertEqual('next_foo_url', self.create_env_view.request.session['next_url']) @mock.patch.object(views, 'reverse_lazy') @mock.patch.object(views, 'reverse') def test_get_success_url(self, mock_reverse, mock_reverse_lazy): mock_reverse.return_value = 'foo_reverse_url' mock_reverse_lazy.return_value = 'foo_reverse_lazy_url' self.create_env_view.request.session['next_url'] = 'foo_next_url' self.assertEqual('foo_next_url', self.create_env_view.get_success_url()) del self.create_env_view.request.session['next_url'] self.create_env_view.request.session['env_id'] = 'foo_env_id' self.assertEqual('foo_reverse_url', self.create_env_view.get_success_url()) self.assertNotIn('env_id', self.create_env_view.request.session) mock_reverse.assert_called_once_with( "horizon:app-catalog:environments:services", args=['foo_env_id']) self.assertEqual('foo_reverse_lazy_url', self.create_env_view.get_success_url()) mock_reverse_lazy.assert_called_once_with( 'horizon:app-catalog:environments:index') @mock.patch.object(views, 'reverse') @mock.patch.object(views, 'api') class TestDeploymentDetailsView(testtools.TestCase): def setUp(self): super(TestDeploymentDetailsView, self).setUp() self.mock_request = mock.Mock(session={}, GET={}) self.mock_request.user.service_catalog = None self.deployment_details_view = views.DeploymentDetailsView() self.deployment_details_view.request = self.mock_request self.deployment_details_view.kwargs = { 'deployment_id': 'foo_deployment_id', 'environment_id': 'foo_env_id' } self.assertEqual(env_tabs.DeploymentDetailsTabs, self.deployment_details_view.tab_group_class) self.assertEqual(env_tables.EnvConfigTable, self.deployment_details_view.table_class) self.assertEqual('deployments/reports.html', self.deployment_details_view.template_name) self.assertEqual('Deployment at {{ deployment_start_time }}', self.deployment_details_view.page_title) def test_get_context_data(self, mock_api, mock_reverse): mock_env = mock.Mock() mock_env.configure_mock(name='foo_env_name') mock_api.muranoclient().deployments.list.return_value = [] mock_api.environment_get.return_value = mock_env mock_api.get_deployment_start.return_value = 'foo_deployment_start' mock_reverse.return_value = 'foo_reverse_url' context = self.deployment_details_view.get_context_data() expected_context = { 'environment_id': 'foo_env_id', 'environment_name': 'foo_env_name', 'deployment_start_time': 'foo_deployment_start', 'custom_breadcrumb': [ ('foo_env_name', 'foo_reverse_url'), (_('Deployments'), None) ] } for key, val in expected_context.items(): self.assertEqual(val, context[key]) mock_api.environment_get.assert_called_once_with( self.mock_request, 'foo_env_id') mock_api.get_deployment_start.assert_called_once_with( self.mock_request, 'foo_env_id', 'foo_deployment_id') def test_get_deployment(self, mock_api, mock_reverse): mock_api.get_deployment_descr.return_value = 'foo_deployment_descr' self.deployment_details_view.environment_id = 'foo_env_id' self.deployment_details_view.deployment_id = 'foo_deployment_id' self.assertEqual('foo_deployment_descr', self.deployment_details_view.get_deployment()) mock_api.get_deployment_descr.assert_called_once_with( self.mock_request, 'foo_env_id', 'foo_deployment_id') @mock.patch.object(views, 'exceptions') def test_get_deployment_negative(self, mock_exc, mock_api, mock_reverse): mock_reverse.return_value = 'foo_reverse_url' self.deployment_details_view.environment_id = 'foo_env_id' self.deployment_details_view.deployment_id = 'foo_deployment_id' for exception in [exc.HTTPInternalServerError, exc.HTTPNotFound]: mock_api.get_deployment_descr.side_effect = exception deployment = self.deployment_details_view.get_deployment() self.assertIsNone(deployment) mock_api.get_deployment_descr.assert_called_with( self.mock_request, 'foo_env_id', 'foo_deployment_id') mock_exc.handle.assert_called_with( self.mock_request, _("Deployment with id foo_deployment_id " "doesn't exist anymore"), redirect='foo_reverse_url') def test_get_logs(self, mock_api, _): mock_api.deployment_reports.return_value = ['foo_log'] self.deployment_details_view.environment_id = 'foo_env_id' self.deployment_details_view.deployment_id = 'foo_deployment_id' self.assertEqual(['foo_log'], self.deployment_details_view.get_logs()) @mock.patch.object(views, 'exceptions') def test_get_logs_negative(self, mock_exc, mock_api, mock_reverse): mock_reverse.return_value = 'foo_reverse_url' self.deployment_details_view.environment_id = 'foo_env_id' self.deployment_details_view.deployment_id = 'foo_deployment_id' for exception in [exc.HTTPInternalServerError, exc.HTTPNotFound]: mock_api.deployment_reports.side_effect = exception logs = self.deployment_details_view.get_logs() self.assertEqual([], logs) mock_api.deployment_reports.assert_called_with( self.mock_request, 'foo_env_id', 'foo_deployment_id') mock_exc.handle.assert_called_with( self.mock_request, _("Deployment with id foo_deployment_id " "doesn't exist anymore"), redirect='foo_reverse_url') def test_get_tabs(self, mock_api, _): mock_api.get_deployment_descr.return_value = 'foo_deployment_descr' mock_api.deployment_reports.return_value = ['foo_log'] result = self.deployment_details_view.get_tabs(self.mock_request) self.assertIsInstance(result, env_tabs.DeploymentDetailsTabs) class TestJSONView(testtools.TestCase): @mock.patch.object(views, 'api') def test_get(self, mock_api): mock_api.load_environment_data.return_value = "{'foo': 'bar'}" mock_request = mock.Mock() kwargs = {'environment_id': 'foo_env_id'} result = views.JSONView.get(mock_request, **kwargs) self.assertIsInstance(result, http.HttpResponse) self.assertEqual(b"{'foo': 'bar'}", result.content) mock_api.load_environment_data.assert_called_once_with(mock_request, 'foo_env_id') class TestJSONResponse(testtools.TestCase): def test_init(self): kwargs = {'content_type': 'json'} json_response = views.JSONResponse(**kwargs) self.assertIsInstance(json_response, views.JSONResponse) self.assertEqual(b'{}', json_response.content) json_response = views.JSONResponse(content='foo', **kwargs) self.assertIsInstance(json_response, views.JSONResponse) self.assertEqual(b'"foo"', json_response.content) class TestStartActionView(testtools.TestCase): @mock.patch.object(views, 'reverse') @mock.patch.object(views, 'api') def test_post(self, mock_api, mock_reverse): mock_api.action_allowed.return_value = True mock_reverse.return_value = 'foo_reverse_url' mock_request = mock.Mock() result = views.StartActionView.post( mock_request, 'foo_env_id', 'foo_action_id') self.assertIsInstance(result, views.JSONResponse) self.assertEqual(b'{"url": "foo_reverse_url"}', result.content) mock_api.run_action.assert_called_once_with( mock_request, 'foo_env_id', 'foo_action_id') mock_api.action_allowed.return_value = False result = views.StartActionView.post( mock_request, 'foo_env_id', 'foo_action_id') self.assertIsInstance(result, views.JSONResponse) self.assertEqual(b'{}', result.content) mock_api.action_allowed.assert_called_with(mock_request, 'foo_env_id') class TestActionResultView(testtools.TestCase): def test_is_file_returned(self): test_result = {'result': {'?': {'type': 'io.murano.File'}}} self.assertTrue(views.ActionResultView.is_file_returned(test_result)) def test_is_file_returned_negative(self): self.assertFalse( views.ActionResultView.is_file_returned({})) def test_compose_response(self): response = views.ActionResultView.compose_response('foo') self.assertIsInstance(response, http.HttpResponse) self.assertEqual(b'"foo"', response.content) self.assertTrue(response.has_header('Content-Disposition')) self.assertTrue(response.has_header('Content-Length')) self.assertEqual('attachment; filename=result.json', response['Content-Disposition']) def test_compose_response_is_exc(self): response = views.ActionResultView.compose_response('foo', is_exc=True) self.assertIsInstance(response, http.HttpResponse) self.assertEqual(b'"foo"', response.content) self.assertTrue(response.has_header('Content-Disposition')) self.assertTrue(response.has_header('Content-Length')) self.assertEqual('attachment; filename=exception.json', response['Content-Disposition']) def test_compose_response_is_file(self): base64_encoding = None if sys.version_info[0] == 2: base64_encoding = base64.b64encode(bytes('foo_base_64')) elif sys.version_info[0] == 3: base64_encoding = base64.b64encode(bytes('foo_base_64', 'UTF-8')) test_result = { 'filename': 'filename.foo', 'mimeType': 'foo_mime_type', 'base64Content': base64_encoding } response = views.ActionResultView.compose_response( test_result, is_file=True) self.assertIsInstance(response, http.HttpResponse) self.assertEqual(b'foo_base_64', response.content) self.assertTrue(response.has_header('Content-Disposition')) self.assertTrue(response.has_header('Content-Length')) self.assertEqual('attachment; filename=filename.foo', response['Content-Disposition']) @mock.patch.object(views, 'api_utils') def test_get(self, mock_api_utils): mock_api_utils.muranoclient().actions.get_result.return_value =\ {'result': 'foo_result', 'foo': 'bar'} mock_request = mock.Mock() action_result_view = views.ActionResultView() result = action_result_view.get( mock_request, 'foo_env_id', 'foo_task_id', 'poll') self.assertIsInstance(result, views.JSONResponse) self.assertEqual(b'{"foo": "bar"}', result.content) mock_api_utils.muranoclient().actions.get_result.\ assert_called_once_with('foo_env_id', 'foo_task_id') @mock.patch.object(views, 'api_utils') def test_get_with_compose_response(self, mock_api_utils): mock_api_utils.muranoclient().actions.get_result.return_value =\ {'result': 'foo_result', 'isException': False} mock_request = mock.Mock() action_result_view = views.ActionResultView() result = action_result_view.get( mock_request, 'foo_env_id', 'foo_task_id', None) self.assertIsInstance(result, http.HttpResponse) self.assertEqual(b'"foo_result"', result.content) mock_api_utils.muranoclient().actions.get_result.\ assert_called_once_with('foo_env_id', 'foo_task_id') @mock.patch.object(views, 'api_utils') def test_get_without_polling_result(self, mock_api_utils): mock_api_utils.muranoclient().actions.get_result.return_value = None mock_request = mock.Mock() action_result_view = views.ActionResultView() result = action_result_view.get( mock_request, 'foo_env_id', 'foo_task_id', None) self.assertIsInstance(result, views.JSONResponse) self.assertEqual(b'{}', result.content) mock_api_utils.muranoclient().actions.get_result.\ assert_called_once_with('foo_env_id', 'foo_task_id') class TestDeploymentHistoryView(testtools.TestCase): def setUp(self): super(TestDeploymentHistoryView, self).setUp() self.deployment_history_view = views.DeploymentHistoryView() self.mock_request = mock.Mock() self.deployment_history_view.request = self.mock_request self.deployment_history_view.environment_id = mock.sentinel.env_id self.assertEqual(env_tables.DeploymentHistoryTable, self.deployment_history_view.table_class) self.assertEqual('environments/index.html', self.deployment_history_view.template_name) self.assertEqual(_('Deployment History'), self.deployment_history_view.page_title) @mock.patch.object(views, 'api', autospec=True) def test_get_data(self, mock_env_api): mock_env_api.deployment_history.return_value = \ [mock.sentinel.deployment_history] result = self.deployment_history_view.get_data() self.assertEqual([mock.sentinel.deployment_history], result) @mock.patch.object(views, 'exceptions', autospec=True) @mock.patch.object(views, 'api', autospec=True) def test_get_data_except_http_unauthorized(self, mock_env_api, mock_exceptions): mock_env_api.deployment_history.side_effect = \ exc.HTTPUnauthorized self.assertEqual([], self.deployment_history_view.get_data()) mock_exceptions.handle.assert_called_once_with(self.mock_request) @mock.patch.object(views, 'exceptions', autospec=True) @mock.patch.object(views, 'reverse', autospec=True) @mock.patch.object(views, 'api', autospec=True) def test_get_data_except_http_forbidden(self, mock_env_api, mock_reverse, mock_exceptions): mock_env_api.deployment_history.side_effect = \ exc.HTTPForbidden mock_reverse.return_value = mock.sentinel.redirect_url self.assertEqual([], self.deployment_history_view.get_data()) mock_reverse.assert_called_once_with( 'horizon:app-catalog:environments:services', args=[mock.sentinel.env_id]) mock_exceptions.handle.assert_called_once_with( self.mock_request, _('Unable to retrieve deployment history.'), redirect=mock.sentinel.redirect_url) murano-dashboard-5.0.0/muranodashboard/tests/unit/environments/test_tabs.py0000666000175100017510000005351113245511125027357 0ustar zuulzuul00000000000000# Copyright (c) 2016 AT&T 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 mock import testtools from django.conf import settings from django.utils.translation import ugettext_lazy as _ from muranoclient.common import exceptions as exc from muranodashboard.environments import tables from muranodashboard.environments import tabs class TestOverviewTab(testtools.TestCase): def setUp(self): super(TestOverviewTab, self).setUp() self.overview_tab = tabs.OverviewTab(None) self.assertEqual(_('Component'), self.overview_tab.name) self.assertEqual('_service', self.overview_tab.slug) self.assertEqual('services/_overview.html', self.overview_tab.template_name) @mock.patch.object(tabs, 'heat_api') @mock.patch.object(tabs, 'nova_api') @mock.patch.object(tabs, 'consts') def test_get_context_data(self, mock_consts, mock_nova_api, mock_heat_api): mock_consts.STATUS_DISPLAY_CHOICES = [('foo_status_id', 'foo_status')] foo_mock_instance = mock.Mock(id='foo_instance_id') foo_mock_instance.configure_mock(name='foo-instance-name') bar_mock_instance = mock.Mock(id='bar_instance_id') bar_mock_instance.configure_mock(name='bar-instance-name') baz_mock_instance = mock.Mock(id='baz_instance_id') baz_mock_instance.configure_mock(name='baz-instance-name') mock_nova_api.server_list.side_effect = [ ([foo_mock_instance], False), ([bar_mock_instance], False), ([baz_mock_instance], False) ] mock_heat_api.stacks_list.side_effect = [ ( [mock.Mock(id='foo_stack_id', stack_name='foo_stack_name')], False, False ), ( [mock.Mock(id='bar_stack_id', stack_name='bar_stack_name')], False, False ), ( [mock.Mock(id='baz_stack_id', stack_name='baz_stack_name')], False, False ) ] mock_request = mock.Mock() mock_request.session = {'django_timezone': 'UTC'} expected_service = { 'service': collections.OrderedDict([ ('Name', 'foo_service_data_name'), ('ID', 'foo_service_data_id'), ('Type', 'Unknown'), ('Status', 'foo_status'), ('Domain', 'foo_domain'), ('Application repository', 'foo_repository'), ('Load Balancer URI', 'foo_uri'), ('Floating IP', 'foo_floatingip'), ('Instance', {'name': 'foo-instance-name', 'id': 'foo_instance_id'}), ('Stack', {'id': 'foo_stack_id', 'name': 'foo_stack_name'}), ('Instances', [ {'name': 'bar-instance-name', 'id': 'bar_instance_id'}, {'name': 'baz-instance-name', 'id': 'baz_instance_id'}]), ('Stacks', [ {'id': 'bar_stack_id', 'name': 'bar_stack_name'}, {'id': 'baz_stack_id', 'name': 'baz_stack_name'}]) ]) } def service_data_side_effect(*args, **kwargs): if args[0] == 'instances': return [ { 'status': 'bar_status_id', 'id': 'bar_service_data_id', 'name': 'instance-name', 'openstackId': 'bar_instance_id' }, { 'status': 'baz_status_id', 'id': 'baz_service_data_id', 'name': 'instance-name', 'openstackId': 'baz_instance_id' } ] elif args[0] == 'instance': return { 'name': 'instance-name', 'openstackId': 'instance_id' } elif args[0] == '?': return { 'status': 'foo_status_id', 'id': 'foo_service_data_id', } service_data = mock.MagicMock() service_data.__getitem__.side_effect = service_data_side_effect service_data.configure_mock(name='foo_service_data_name') service_data.domain = 'foo_domain' service_data.repository = 'foo_repository' service_data.uri = 'foo_uri' service_data.floatingip = 'foo_floatingip' self.overview_tab.tab_group = mock.Mock() self.overview_tab.tab_group.kwargs = { 'service': service_data } result = self.overview_tab.get_context_data(mock_request) self.assertIsInstance(result, dict) self.assertIn('service', result) self.assertEqual(expected_service, result) self.assertEqual(3, mock_nova_api.server_list.call_count) self.assertEqual(3, mock_heat_api.stacks_list.call_count) mock_nova_api.server_list.assert_any_call(mock_request) mock_heat_api.stacks_list.assert_any_call(mock_request, sort_dir='asc') @mock.patch.object(tabs, 'heat_api') @mock.patch.object(tabs, 'nova_api') def test_get_context_data_find_stack_has_more(self, mock_nova_api, mock_heat_api): foo_mock_instance = mock.Mock(id='foo_instance_id') foo_mock_instance.configure_mock(name='foo-instance-name') mock_nova_api.server_list.side_effect = [ ([foo_mock_instance], False) ] mock_heat_api.stacks_list.side_effect = [ ( [mock.Mock(id='bar_stack_id', stack_name='bar_stack_name')], True, False ), ( [mock.Mock(id='foo_stack_id', stack_name='foo_stack_name')], False, False ) ] mock_request = mock.Mock() expected_service = { 'service': collections.OrderedDict([ ('Name', 'foo_service_data_name'), ('ID', 'foo_service_data_id'), ('Type', 'Unknown'), ('Status', ''), ('Domain', 'Not in domain'), ('Application repository', 'foo_repository'), ('Load Balancer URI', 'foo_uri'), ('Floating IP', 'foo_floatingip'), ('Instance', {'name': 'foo-instance-name', 'id': 'foo_instance_id'}), ('Stack', {'id': 'foo_stack_id', 'name': 'foo_stack_name'}), ]) } expected_mock_calls = [ mock.call(mock_request, sort_dir='asc'), mock.call(mock_request, sort_dir='asc', marker='bar_stack_id') ] def service_data_side_effect(*args, **kwargs): if args[0] == 'instance': return { 'name': 'instance-name', 'openstackId': 'instance_id' } elif args[0] == '?': return { 'status': 'foo_status_id', 'id': 'foo_service_data_id', } service_data = mock.MagicMock() service_data.__getitem__.side_effect = service_data_side_effect service_data.configure_mock(name='foo_service_data_name') service_data.domain = None service_data.repository = 'foo_repository' service_data.uri = 'foo_uri' service_data.floatingip = 'foo_floatingip' self.overview_tab.tab_group = mock.Mock() self.overview_tab.tab_group.kwargs = { 'service': service_data } result = self.overview_tab.get_context_data(mock_request) self.assertIsInstance(result, dict) self.assertIn('service', result) self.assertEqual(expected_service, result) # Test whether the expected number of calls were made. 1 call should # be made to nova_api but 2 should be made to heat_api, since that # call is recursive. self.assertEqual(1, mock_nova_api.server_list.call_count) self.assertEqual(2, mock_heat_api.stacks_list.call_count) mock_nova_api.server_list.assert_any_call(mock_request) self.assertEqual(expected_mock_calls, mock_heat_api.stacks_list.mock_calls) class TestServiceLogsTab(testtools.TestCase): def setUp(self): super(TestServiceLogsTab, self).setUp() self.service_logs_tab = tabs.ServiceLogsTab(None) self.assertEqual(_('Logs'), self.service_logs_tab.name) self.assertEqual('service_logs', self.service_logs_tab.slug) self.assertEqual('services/_logs.html', self.service_logs_tab.template_name) self.assertFalse(self.service_logs_tab.preload) @mock.patch.object(tabs, 'api') def test_get_context_data(self, mock_api): mock_api.get_status_messages_for_service.return_value = ['foo_report'] mock_request = mock.Mock() self.service_logs_tab.tab_group = mock.Mock() self.service_logs_tab.tab_group.kwargs = { 'service_id': 'foo_service_id', 'environment_id': 'foo_environment_id' } reports = self.service_logs_tab.get_context_data(mock_request) self.assertEqual({'reports': ['foo_report']}, reports) mock_api.get_status_messages_for_service.assert_called_once_with( mock_request, 'foo_service_id', 'foo_environment_id') class TestEnvLogsTab(testtools.TestCase): def setUp(self): super(TestEnvLogsTab, self).setUp() self.env_logs_tab = tabs.EnvLogsTab(None) self.assertEqual(_('Logs'), self.env_logs_tab.name) self.assertEqual('env_logs', self.env_logs_tab.slug) self.assertEqual('deployments/_logs.html', self.env_logs_tab.template_name) self.assertFalse(self.env_logs_tab.preload) def test_get_context_data(self): mock_report = mock.Mock(created='1970-01-01T12:34:00') self.env_logs_tab.tab_group = mock.Mock() self.env_logs_tab.tab_group.kwargs = { 'logs': [mock_report] } mock_request = mock.MagicMock() mock_request.session = {'django_timezone': 'UTC'} reports = self.env_logs_tab.get_context_data(mock_request) mock_report.created = '1970-01-01 12:34:00' self.assertEqual({'reports': [mock_report]}, reports) class TestLatestLogTab(testtools.TestCase): def test_allowed(self): mock_request = mock.MagicMock() mock_request.session = {'django_timezone': 'UTC'} mock_report = mock.Mock(created='1970-01-01T12:34:00') tab_group = mock.Mock() tab_group.kwargs = {'logs': [mock_report]} latest_logs_tab = tabs.LatestLogsTab(tab_group, request=mock_request) self.assertEqual(_('Latest Deployment Log'), latest_logs_tab.name) mock_report.created = '1970-01-01 12:34:00' self.assertEqual([mock_report], latest_logs_tab.allowed(mock_request)) class TestEnvConfigTab(testtools.TestCase): def setUp(self): super(TestEnvConfigTab, self).setUp() mock_tab_group = mock.Mock(kwargs={}) self.env_config_tab = tabs.EnvConfigTab(mock_tab_group, None) self.assertEqual(_('Configuration'), self.env_config_tab.name) self.assertEqual('env_config', self.env_config_tab.slug) self.assertEqual((tables.EnvConfigTable,), self.env_config_tab.table_classes) self.assertEqual('horizon/common/_detail_table.html', self.env_config_tab.template_name) self.assertFalse(self.env_config_tab.preload) def test_get_environment_and_configuration_data(self): self.env_config_tab.tab_group = mock.Mock() self.env_config_tab.tab_group.kwargs = { 'deployment': { 'services': ['foo_service'] } } result = self.env_config_tab.get_environment_configuration_data() self.assertEqual(['foo_service'], result) class TestEnvironmentTopologyTab(testtools.TestCase): def setUp(self): super(TestEnvironmentTopologyTab, self).setUp() self.env_topology_tab = tabs.EnvironmentTopologyTab(None) self.assertEqual(_('Topology'), self.env_topology_tab.name) self.assertEqual('topology', self.env_topology_tab.slug) self.assertEqual('services/_detail_topology.html', self.env_topology_tab.template_name) self.assertFalse(self.env_topology_tab.preload) @mock.patch.object(tabs, 'api') def test_allowed_true(self, mock_api): self.env_topology_tab.tab_group = mock.Mock() self.env_topology_tab.tab_group.kwargs = { 'environment_id': 'foo_env_id' } mock_api.load_environment_data.return_value =\ '{"environment": {"status": "foo_status"}}' # d3 data self.assertTrue(self.env_topology_tab.allowed(None)) mock_api.load_environment_data.assert_called_with(None, 'foo_env_id') @mock.patch.object(tabs, 'api') def test_allowed_false(self, mock_api): self.env_topology_tab.tab_group = mock.Mock() self.env_topology_tab.tab_group.kwargs = { 'environment_id': 'foo_env_id' } mock_api.load_environment_data.return_value =\ '{"environment": {"status": null}}' # d3 data self.assertFalse(self.env_topology_tab.allowed(None)) mock_api.load_environment_data.assert_called_with(None, 'foo_env_id') @mock.patch.object(tabs, 'api') class TestEnvironmentServicesTab(testtools.TestCase): def setUp(self): super(TestEnvironmentServicesTab, self).setUp() test_kwargs = {'environment_id': 'foo_env_id'} mock_tab_group = mock.Mock(kwargs=test_kwargs) self.mock_request = mock.Mock() self.env_services_tab = tabs.EnvironmentServicesTab(mock_tab_group, self.mock_request) self.assertEqual(_('Components'), self.env_services_tab.name) self.assertEqual('services', self.env_services_tab.slug) self.assertEqual((tables.ServicesTable,), self.env_services_tab.table_classes) self.assertEqual('services/_service_list.html', self.env_services_tab.template_name) self.assertFalse(self.env_services_tab.preload) def test_get_services_data(self, mock_api): mock_api.services_list.return_value = ['foo_service'] services_data = self.env_services_tab.get_services_data() self.assertEqual(['foo_service'], services_data) self.assertEqual('foo_env_id', self.env_services_tab.environment_id) @mock.patch.object(tabs, 'reverse') @mock.patch.object(tabs, 'exceptions') def test_get_services_data_except_http_forbidden( self, mock_exc, mock_reverse, mock_api): mock_api.services_list.side_effect = exc.HTTPForbidden mock_reverse.return_value = 'foo_reverse_url' services_data = self.env_services_tab.get_services_data() self.assertEqual([], services_data) expected_msg = _( 'Unable to retrieve list of services. This environment ' 'is deploying or already deployed by other user.') mock_exc.handle.assert_called_once_with( self.mock_request, expected_msg, redirect='foo_reverse_url') mock_reverse.assert_called_once_with( 'horizon:app-catalog:environments:index') @mock.patch.object(tabs, 'reverse') @mock.patch.object(tabs, 'exceptions') def test_get_services_data_except_internal_server_and_not_found_errors( self, mock_exc, mock_reverse, mock_api): mock_reverse.return_value = 'foo_reverse_url' expected_msg = "Environment with id foo_env_id doesn't exist anymore" for exception_cls in (exc.HTTPInternalServerError, exc.HTTPNotFound): mock_api.services_list.side_effect = exception_cls services_data = self.env_services_tab.get_services_data() self.assertEqual([], services_data) mock_exc.handle.assert_called_with( self.mock_request, expected_msg, redirect='foo_reverse_url') mock_reverse.assert_called_with( 'horizon:app-catalog:environments:index') @mock.patch.object(tabs, 'exceptions') def test_get_services_data_except_http_unauthorized( self, mock_exc, mock_api): mock_api.services_list.side_effect = exc.HTTPUnauthorized self.env_services_tab.get_services_data() mock_exc.handle.assert_called_once_with(self.mock_request) def test_get_context_data(self, _): setattr(settings, 'MURANO_USE_GLARE', True) expected_context = { 'MURANO_USE_GLARE': True, 'table': mock.ANY, 'services_table': mock.ANY } context = self.env_services_tab.get_context_data(self.mock_request) for key, val in expected_context.items(): self.assertEqual(val, context.get(key)) @mock.patch.object(tabs, 'api') class TestDeploymentTab(testtools.TestCase): def setUp(self): super(TestDeploymentTab, self).setUp() test_kwargs = {'environment_id': 'foo_env_id'} mock_tab_group = mock.Mock(kwargs=test_kwargs) self.mock_request = mock.Mock() self.deployment_tab = tabs.DeploymentTab(mock_tab_group, self.mock_request) self.assertEqual(_('Deployment History'), self.deployment_tab.name) self.assertEqual('deployments', self.deployment_tab.slug) self.assertEqual((tables.DeploymentsTable,), self.deployment_tab.table_classes) self.assertEqual('horizon/common/_detail_table.html', self.deployment_tab.template_name) self.assertFalse(self.deployment_tab.preload) @mock.patch.object(tabs, 'policy') def test_allowed(self, mock_policy, _): mock_policy.check.return_value = True self.assertTrue(self.deployment_tab.allowed(self.mock_request)) mock_policy.check.assert_called_once_with( (("murano", "list_deployments"),), self.mock_request) def test_get_deployments_data(self, mock_api): mock_api.deployments_list.return_value = ['foo_deployment'] deployments = self.deployment_tab.get_deployments_data() self.assertEqual(['foo_deployment'], deployments) mock_api.deployments_list.assert_called_once_with( self.mock_request, 'foo_env_id') @mock.patch.object(tabs, 'reverse') @mock.patch.object(tabs, 'exceptions') def test_get_deployments_data_except_http_forbidden( self, mock_exc, mock_reverse, mock_api): mock_api.deployments_list.side_effect = exc.HTTPForbidden mock_reverse.return_value = 'foo_reverse_url' self.deployment_tab.get_deployments_data() mock_exc.handle.assert_called_once_with( self.mock_request, _('Unable to retrieve list of deployments'), redirect='foo_reverse_url') mock_reverse.assert_called_once_with( "horizon:app-catalog:environments:index") @mock.patch.object(tabs, 'reverse') @mock.patch.object(tabs, 'exceptions') def test_get_deployments_data_except_http_server_error( self, mock_exc, mock_reverse, mock_api): mock_api.deployments_list.side_effect = exc.HTTPInternalServerError mock_reverse.return_value = 'foo_reverse_url' self.deployment_tab.get_deployments_data() mock_exc.handle.assert_called_once_with( self.mock_request, "Environment with id foo_env_id doesn't exist anymore", redirect='foo_reverse_url') mock_reverse.assert_called_once_with( "horizon:app-catalog:environments:index") class TestEnvironmentDetailsTabs(testtools.TestCase): @mock.patch.object(tabs, 'api') def test_init(self, mock_api): mock_api.load_environment_data.return_value =\ '{"environment": {"status": "foo_status"}}' mock_request = mock.Mock(GET={}) mock_request.session = {'django_timezone': 'UTC'} mock_logs = mock.Mock(created='1970-01-01T12:34:00') mock_logs.created = '1970-01-01 12:34:00' env_details_tabs = tabs.EnvironmentDetailsTabs( mock_request, environment_id='foo_env_id', logs=[mock_logs]) self.assertEqual('environment_details', env_details_tabs.slug) self.assertEqual( (tabs.EnvironmentServicesTab, tabs.EnvironmentTopologyTab, tabs.DeploymentTab, tabs.LatestLogsTab), env_details_tabs.tabs) self.assertTrue(env_details_tabs.sticky) class TestServicesTabs(testtools.TestCase): def test_init(self): mock_request = mock.Mock(GET={}) services_tabs = tabs.ServicesTabs(mock_request) self.assertEqual('services_details', services_tabs.slug) self.assertEqual( (tabs.OverviewTab, tabs.ServiceLogsTab), services_tabs.tabs) self.assertTrue(services_tabs.sticky) class TestDeploymentDetailsTabs(testtools.TestCase): def test_init(self): mock_request = mock.Mock(GET={}) deployment_details_tabs = tabs.DeploymentDetailsTabs(mock_request) self.assertEqual('deployment_details', deployment_details_tabs.slug) self.assertEqual((tabs.EnvConfigTab, tabs.EnvLogsTab,), deployment_details_tabs.tabs) self.assertTrue(deployment_details_tabs.sticky) murano-dashboard-5.0.0/muranodashboard/tests/unit/environments/test_forms.py0000666000175100017510000000550413245511125027553 0ustar zuulzuul00000000000000# Copyright (c) 2016 AT&T 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 mock import testtools from django.utils.translation import ugettext_lazy as _ from muranoclient.common import exceptions as exc from muranodashboard.environments import forms as env_forms class TestCreateEnvForm(testtools.TestCase): def setUp(self): super(TestCreateEnvForm, self).setUp() self.mock_request = mock.MagicMock() self.data = { 'name': 'test', 'net_config': 'Nova' } @mock.patch.object(env_forms, 'ast') @mock.patch.object(env_forms, 'api') @mock.patch('muranodashboard.common.net.get_available_networks') def test_handle(self, mock_net, mock_api, mock_ast): mock_net.return_value = None test_env_form = env_forms.CreateEnvironmentForm(self.mock_request) self.assertEqual([((None, None), _('Unavailable'))], test_env_form.fields['net_config'].choices) self.assertEqual(env_forms.net.NN_HELP, test_env_form.fields['net_config'].help_text) self.assertTrue(test_env_form.handle(self.mock_request, self.data)) mock_api.environment_create.assert_called_once_with(self.mock_request, self.data) mock_ast.literal_eval.assert_called_once_with('Nova') @mock.patch.object(env_forms, 'LOG') @mock.patch.object(env_forms, 'ast') @mock.patch.object(env_forms, 'api') @mock.patch('muranodashboard.common.net.get_available_networks') def test_handle_error(self, mock_net, mock_api, mock_ast, mock_log): mock_net.return_value = None test_env_form = env_forms.CreateEnvironmentForm(self.mock_request) self.assertEqual([((None, None), _('Unavailable'))], test_env_form.fields['net_config'].choices) self.assertEqual(env_forms.net.NN_HELP, test_env_form.fields['net_config'].help_text) mock_api.environment_create.side_effect = exc.HTTPConflict msg = _('Environment with specified name already exists') self.assertRaises(exc.HTTPConflict, test_env_form.handle, self.mock_request, self.data) mock_ast.literal_eval.assert_called_once_with('Nova') mock_log.exception.assert_called_once_with(msg) murano-dashboard-5.0.0/muranodashboard/tests/unit/environments/test_api.py0000666000175100017510000005356113245511125027204 0ustar zuulzuul00000000000000# Copyright (c) 2016 AT&T 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. import mock from muranoclient.common import exceptions as exc from muranoclient.v1 import client from muranodashboard.common import utils from muranodashboard.environments import api as env_api from muranodashboard.environments import consts from openstack_dashboard.test import helpers class TestEnvironmentsAPI(helpers.APITestCase): def setUp(self): super(TestEnvironmentsAPI, self).setUp() self.mock_client = mock.Mock(spec=client) self.mock_request = mock.MagicMock() self.mock_request.session = {'django_timezone': 'UTC'} self.env_id = 'foo_env_id' self.session_id = 'foo_session_id' self.service_id = 'foo_service_id' self.deployment_id = 'foo_deployment_id' self.addCleanup(mock.patch.stopall) @mock.patch.object(env_api, 'api', autospec=True) @mock.patch.object(env_api, 'LOG', autospec=True) def test_get_status_messages_for_service(self, mock_log, mock_api): mock_client = mock_api.muranoclient(mock.Mock()) mock_client.deployments.list.return_value = [ mock.Mock(id='foo_deployment_id'), mock.Mock(id='bar_deployment_id') ] mock_client.deployments.reports.side_effect = [ [mock.Mock(text='foo_text', created='1970-01-01T12:23:00')], [mock.Mock(text='bar_text', created='1970-01-01T15:45:00')], ] expected_result = '\n1970-01-01 12:23:00 - foo_text\n' \ '1970-01-01 15:45:00 - bar_text\n' expected_reports_mock_calls = [ mock.call('foo_env_id', 'bar_deployment_id', 'foo_service_id'), mock.call('foo_env_id', 'foo_deployment_id', 'foo_service_id') ] result = env_api.get_status_messages_for_service( self.mock_request, self.service_id, self.env_id) self.assertEqual(expected_result, result) mock_client.deployments.reports.assert_has_calls( expected_reports_mock_calls) mock_client.deployments.list.assert_called_once_with('foo_env_id') self.assertTrue(mock_log.debug.called) @mock.patch.object(env_api, 'api', autospec=True) def test_environment_update(self, mock_api): env_name = "test_env" env_api.environment_update(self.mock_request, self.env_id, env_name) env_api.api.muranoclient.assert_called_once_with(self.mock_request) @mock.patch.object(env_api, 'api', autospec=True) def test_environment_list(self, mock_api): env_api.environments_list(self.mock_request) env_api.api.muranoclient.assert_called_with(self.mock_request) env_api.api.handled_exceptions.assert_called_with(self.mock_request) @mock.patch.object(env_api, 'api', autospec=True) @mock.patch.object(env_api, 'LOG', autospec=True) def test_environment_create(self, mock_log, mock_api): parameters = { 'name': 'test_env', 'defaultNetworks': 'test_net' } env_api.environment_create(self.mock_request, parameters) env_api.api.muranoclient.assert_called_with(self.mock_request) self.assertTrue(mock_log.debug.called) @mock.patch.object(env_api, 'api', autospec=True) @mock.patch.object(env_api, 'LOG', autospec=True) def test_environment_delete(self, mock_log, mock_api): env_api.environment_delete(self.mock_request, self.env_id) env_api.api.muranoclient.assert_called_with(self.mock_request) (mock_log.debug. assert_called_once_with('Environment::{0} '.format('Delete', self.env_id))) @mock.patch.object(env_api, 'api', autospec=True) @mock.patch.object(env_api, 'LOG', autospec=True) def test_environment_deploy(self, mock_log, mock_api): env_api.environment_deploy(self.mock_request, self.env_id) self.assertTrue(env_api.api.muranoclient.called) self.assertTrue(mock_log.debug.called) @mock.patch.object(env_api, 'api', autospec=True) @mock.patch.object(env_api, 'LOG', autospec=True) def test_action_allowed(self, mock_log, mock_api): result = env_api.action_allowed(self.mock_request, self.env_id) self.assertTrue(result) env_api.api.muranoclient.assert_called_with(self.mock_request) self.assertTrue(mock_log.debug.called) @mock.patch.object(env_api, 'api', autospec=True) @mock.patch.object(env_api, 'LOG', autospec=True) def test_service_create(self, mock_log, mock_api): parameters = { '?': { 'type': 'test.Service' } } env_api.service_create(self.mock_request, self.env_id, parameters) env_api.api.muranoclient.assert_called_with(self.mock_request) mock_log.debug.assert_called_with('Service::Create {0}' .format(parameters['?']['type'])) @mock.patch.object(env_api, 'api', autospec=True) @mock.patch.object(env_api, 'Session', autospec=True) @mock.patch.object(env_api, 'LOG', autospec=True) def test_service_delete(self, mock_log, mock_session, mock_api): mock_response = mock.Mock(status_code=200) mock_client = mock_api.muranoclient(mock.Mock()) mock_client.services.delete.return_value = mock_response mock_session.get_or_create_or_delete.return_value = self.session_id result = env_api.service_delete(self.mock_request, self.env_id, self.service_id) self.assertEqual(mock_response, result) env_api.api.muranoclient.assert_called_with(self.mock_request) mock_session.get_or_create_or_delete.assert_called_with( self.mock_request, 'foo_env_id') mock_client.services.delete.assert_called_once_with( 'foo_env_id', '/foo_service_id', 'foo_session_id') mock_log.debug.assert_called_with( 'Service::Delete '.format('foo_service_id')) @mock.patch.object(env_api, 'services_list', autospec=True) @mock.patch.object(env_api, 'LOG', autospec=True) def test_service_get(self, mock_log, mock_services_list): mock_services_list.return_value = [{'?': {'id': 'foo_service_id'}}] result = env_api.service_get(self.mock_request, self.env_id, 'foo_service_id') self.assertEqual({'?': {'id': 'foo_service_id'}}, result) mock_services_list.assert_called_once_with( self.mock_request, 'foo_env_id') mock_log.debug.assert_called_with( 'Return service detail for a specified id') def test_extract_actions_list(self): service = { '?': { 'test': 'test' } } result = env_api.extract_actions_list(service) self.assertEqual([], result) @mock.patch.object(env_api, 'api', autospec=True) def test_run_action(self, mock_api): env_api.run_action(self.mock_request, self.env_id, 'foo_action_id') env_api.api.muranoclient.assert_called_with(self.mock_request) @mock.patch.object(env_api, 'api', autospec=True) @mock.patch.object(env_api, 'LOG', autospec=True) def test_deployment_reports(self, mock_log, mock_api): env_api.deployment_reports(self.mock_request, self.env_id, self.deployment_id) env_api.api.muranoclient.assert_called_with(self.mock_request) self.assertTrue(mock_log.debug.called) @mock.patch.object(env_api, 'api', autospec=True) def test_get_deployment_start(self, mock_api): mock_client = mock_api.muranoclient(mock.Mock()) mock_client.deployments.list.return_value = [] result = env_api.get_deployment_start(self.mock_request, self.env_id, self.deployment_id) self.assertIsNone(result) mock_client.deployments.list.return_value = [ mock.Mock(id='foo_deployment_id', started='1970-01-01T12:34:00') ] result = env_api.get_deployment_start(self.mock_request, self.env_id, self.deployment_id) self.assertEqual('1970-01-01 12:34:00', result) mock_client.deployments.list.assert_has_calls([ mock.call('foo_env_id'), mock.call('foo_env_id') ]) @mock.patch.object(env_api, 'api', autospec=True) def test_get_deployment_description(self, mock_api): mock_client = mock_api.muranoclient(mock.Mock()) mock_client.deployments.list.return_value = [] result = env_api.get_deployment_start(self.mock_request, self.env_id, self.deployment_id) self.assertIsNone(result) mock_client.deployments.list.return_value = [ mock.Mock(id='foo_deployment_id', description='foo_descr') ] result = env_api.get_deployment_descr(self.mock_request, self.env_id, self.deployment_id) self.assertEqual('foo_descr', result) mock_client.deployments.list.assert_has_calls([ mock.call('foo_env_id'), mock.call('foo_env_id') ]) @mock.patch('muranodashboard.environments.api.topology.json.dumps', autospec=True) @mock.patch('muranodashboard.environments.api.topology._environment_info', autospec=True) @mock.patch.object(env_api, 'environment_get', autospec=True) def test_load_environment_data(self, mock_env_get, mock_env_info, mock_dump): mock_env_info.return_value = 'services/_environment_info.html' mock_dump.return_value = self.env_id result = env_api.load_environment_data(self.mock_request, self.env_id) self.assertTrue(mock_env_get.called) self.assertEqual(self.env_id, result) self.assertTrue(mock_dump.called) @mock.patch.object(env_api, 'deployments_list', autospec=True) def test_update_env_return_ready_status(self, mock_deployments_list): mock_deployments_list.return_value = [] mock_env = mock.Mock(id='foo_env_id', services=[], version=1, status=consts.STATUS_ID_PENDING) result = env_api._update_env(mock_env, self.mock_request) self.assertEqual(mock_env, result) self.assertEqual(consts.STATUS_ID_READY, result.status) self.assertFalse(result.has_new_services) @mock.patch.object(env_api, 'deployments_list', autospec=True) def test_update_env_return_new_status(self, mock_deployments_list): mock_deployments_list.return_value = [] mock_env = mock.Mock(id='foo_env_id', services=[], version=0, status=consts.STATUS_ID_READY) result = env_api._update_env(mock_env, self.mock_request) self.assertEqual(mock_env, result) self.assertEqual(consts.STATUS_ID_NEW, result.status) self.assertFalse(result.has_new_services) @mock.patch.object(env_api, 'Session', autospec=True) @mock.patch.object(env_api, 'packages_api', autospec=True) @mock.patch.object(env_api, 'api', autospec=True) def test_services_list(self, mock_api, mock_pkg_api, mock_session): mock_env = mock.Mock(version=0) mock_env.services = [ {'?': {'id': 'foo_service_id', 'name': 'foo', 'type': 'foo_type'}}, {'?': {'id': 'bar_service_id', 'name': 'bar', 'type': '/3@bar_type'}, 'updated': 'bar_time'}, ] mock_foo_pkg = mock.Mock() mock_foo_pkg.configure_mock(name='foo_pkg') mock_bar_pkg = mock.Mock() mock_bar_pkg.configure_mock(name='bar_pkg') mock_session.get.return_value = 'foo_sess_id' mock_client = mock_api.muranoclient(mock.Mock()) mock_client.environments.get.return_value = mock_env mock_client.environments.last_status.return_value = { 'foo_service': mock.Mock(text='foo'*100, updated='foo_time'), 'bar_service': None } mock_pkg_api.app_by_fqn.side_effect = [mock_foo_pkg, mock_bar_pkg] expected_pkg_calls = [ mock.call(self.mock_request, 'foo_type', version=None), mock.call(self.mock_request, 'bar_type', version='3') ] result = env_api.services_list(self.mock_request, 'foo_env_id') self.assertIsInstance(result, list) for obj in result: self.assertIsInstance(obj, utils.Bunch) mock_session.get.assert_called_once_with( self.mock_request, 'foo_env_id') mock_client.environments.get.assert_called_once_with( 'foo_env_id', 'foo_sess_id') mock_client.environments.last_status.assert_called_once_with( 'foo_env_id', 'foo_sess_id') mock_pkg_api.app_by_fqn.assert_has_calls(expected_pkg_calls) @mock.patch.object(env_api, 'LOG', autospec=True) @mock.patch.object(env_api, 'Session', autospec=True) @mock.patch.object(env_api, 'packages_api', autospec=True) @mock.patch.object(env_api, 'api', autospec=True) def test_services_list_except_http_not_found(self, mock_api, mock_pkg_api, mock_session, mock_log): mock_env = mock.Mock(version=0) mock_env.services = [ {'?': {'id': 'foo_service_id', 'name': 'foo', 'type': 'foo_type'}} ] mock_foo_pkg = mock.Mock() mock_foo_pkg.configure_mock(name='foo_pkg') mock_session.get.return_value = 'foo_sess_id' mock_client = mock_api.muranoclient(mock.Mock()) mock_client.environments.get.return_value = mock_env mock_client.environments.last_status.side_effect = exc.HTTPNotFound mock_pkg_api.app_by_fqn.side_effect = [mock_foo_pkg] expected_pkg_calls = [ mock.call(self.mock_request, 'foo_type', version=None) ] result = env_api.services_list(self.mock_request, 'foo_env_id') self.assertIsInstance(result, list) for obj in result: self.assertIsInstance(obj, utils.Bunch) mock_session.get.assert_called_once_with( self.mock_request, 'foo_env_id') mock_client.environments.get.assert_called_once_with( 'foo_env_id', 'foo_sess_id') mock_client.environments.last_status.assert_called_once_with( 'foo_env_id', 'foo_sess_id') mock_log.exception.assert_called_once_with( 'Could not retrieve latest status for the foo_env_id environment') mock_pkg_api.app_by_fqn.assert_has_calls(expected_pkg_calls) @mock.patch.object(env_api, 'services_list', autospec=True) def test_service_list_by_fqns(self, mock_services_list): self.assertEqual([], env_api.service_list_by_fqns(None, None, [])) mock_services_list.return_value = [ {'?': {'type': 'foo/bar'}}, {'?': {'type': 'baz/qux'}} ] result = env_api.service_list_by_fqns( self.mock_request, 'foo_env_id', ['foo']) self.assertEqual([{'?': {'type': 'foo/bar'}}], result) class TestEnvironmentsSessionAPI(helpers.APITestCase): def setUp(self): super(TestEnvironmentsSessionAPI, self).setUp() self.mock_client = mock.Mock(spec=client) self.mock_request = mock.MagicMock() self.env_id = 'foo_env_id' self.addCleanup(mock.patch.stopall) @mock.patch.object(env_api, 'api', autospec=True) def test_get_or_create(self, mock_api): self.session = env_api.Session() result = self.session.get_or_create(self.mock_request, self.env_id) self.assertIsNotNone(result) env_api.api.muranoclient.assert_called_once_with(self.mock_request) def test_set(self): session_id = 11 self.session = env_api.Session() result = self.session.set(self.mock_request, self.env_id, session_id) self.assertIsNone(result) @mock.patch.object(env_api, 'create_session', autospec=True) @mock.patch.object(env_api, 'api', autospec=True) @mock.patch.object(env_api, 'LOG', autospec=True) def test_gcd(self, mock_log, mock_api, mock_create_session): mock_client = mock_api.muranoclient(mock.Mock()) mock_session_data = mock.Mock(state=consts.STATUS_ID_READY) mock_client.sessions.get.return_value = mock_session_data mock_create_session.return_value = 'bar_sess_id' self.mock_request.session = {'sessions': {'foo_env_id': 'foo_sess_id'}} result = env_api.Session.get_or_create_or_delete(self.mock_request, 'foo_env_id') self.assertEqual('bar_sess_id', result) mock_client.sessions.get.assert_called_once_with( 'foo_env_id', 'foo_sess_id') self.assertNotIn('foo_env_id', self.mock_request.session) mock_log.debug.assert_called_once_with( 'The existing session has been already deployed. Creating a new ' 'session for the environment foo_env_id') mock_create_session.assert_called_once_with( self.mock_request, 'foo_env_id') @mock.patch.object(env_api, 'api', autospec=True) @mock.patch.object(env_api, 'LOG', autospec=True) def test_gcd_with_active_session(self, mock_log, mock_api): mock_client = mock_api.muranoclient(mock.Mock()) mock_session_data = mock.Mock(state='foo_bar_state') mock_client.sessions.get.return_value = mock_session_data self.mock_request.session = {'sessions': {'foo_env_id': 'foo_sess_id'}} result = env_api.Session.get_or_create_or_delete(self.mock_request, 'foo_env_id') self.assertEqual('foo_sess_id', result) mock_client.sessions.get.assert_called_once_with( 'foo_env_id', 'foo_sess_id') self.assertNotIn('foo_env_id', self.mock_request.session) mock_log.debug.assert_called_once_with( 'Found active session for the environment foo_env_id') @mock.patch.object(env_api, 'create_session', autospec=True) @mock.patch.object(env_api, 'api', autospec=True) @mock.patch.object(env_api, 'LOG', autospec=True) def test_gcd_with_new_session(self, mock_log, mock_api, mock_create_session): mock_create_session.return_value = 'bar_sess_id' result = env_api.Session.get_or_create_or_delete(self.mock_request, 'foo_env_id') self.assertEqual('bar_sess_id', result) mock_log.debug.assert_called_once_with('Creating a new session') mock_create_session.assert_called_once_with( self.mock_request, 'foo_env_id') @mock.patch.object(env_api, 'create_session', autospec=True) @mock.patch.object(env_api, 'api', autospec=True) @mock.patch.object(env_api, 'LOG', autospec=True) def test_gcd_except_http_forbidden(self, mock_log, mock_api, mock_create_session): mock_client = mock_api.muranoclient(mock.Mock()) mock_client.sessions.get.side_effect = exc.HTTPForbidden mock_create_session.return_value = 'bar_sess_id' self.mock_request.session = {'sessions': {'foo_env_id': 'foo_sess_id'}} result = env_api.Session.get_or_create_or_delete(self.mock_request, 'foo_env_id') self.assertEqual('bar_sess_id', result) mock_client.sessions.get.assert_called_once_with( 'foo_env_id', 'foo_sess_id') self.assertNotIn('foo_env_id', self.mock_request.session) mock_log.debug.assert_called_once_with( 'The environment is being deployed by other user. ' 'Creating a new session for the environment foo_env_id') mock_create_session.assert_called_once_with( self.mock_request, 'foo_env_id') @mock.patch.object(env_api, 'api', autospec=True) def test_get_if_available(self, mock_api): mock_client = mock_api.muranoclient(mock.Mock()) mock_client.sessions.get.return_value = mock.Mock( state='foo_bar_state') self.mock_request.session = {'sessions': {'foo_env_id': 'foo_sess_id'}} result = env_api.Session.get_if_available( self.mock_request, 'foo_env_id') self.assertEqual('foo_sess_id', result) mock_client.sessions.get.assert_called_once_with( 'foo_env_id', 'foo_sess_id') @mock.patch.object(env_api, 'api', autospec=True) def test_get_if_available_with_none_returned(self, mock_api): mock_client = mock_api.muranoclient(mock.Mock()) mock_client.sessions.get.return_value = \ mock.Mock(state=consts.STATUS_ID_READY) self.mock_request.session = {'sessions': {'foo_env_id': 'foo_sess_id'}} result = env_api.Session.get_if_available( self.mock_request, 'foo_env_id') self.assertIsNone(result) mock_client.sessions.get.assert_called_once_with( 'foo_env_id', 'foo_sess_id') @mock.patch.object(env_api, 'api', autospec=True) def test_get_if_available_except_http_forbidden(self, mock_api): mock_client = mock_api.muranoclient(mock.Mock()) mock_client.sessions.get.side_effect = exc.HTTPForbidden self.mock_request.session = {'sessions': {'foo_env_id': 'foo_sess_id'}} result = env_api.Session.get_if_available( self.mock_request, 'foo_env_id') self.assertIsNone(result) mock_client.sessions.get.assert_called_once_with( 'foo_env_id', 'foo_sess_id') murano-dashboard-5.0.0/muranodashboard/tests/unit/dynamic_ui/0000775000175100017510000000000013245511556024410 5ustar zuulzuul00000000000000murano-dashboard-5.0.0/muranodashboard/tests/unit/dynamic_ui/test_versions.py0000666000175100017510000000325713245511125027672 0ustar zuulzuul00000000000000# 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 testtools from muranodashboard.dynamic_ui import version class TestVersions(testtools.TestCase): def setUp(self): super(TestVersions, self).setUp() self.original = version.LATEST_FORMAT_VERSION version.LATEST_FORMAT_VERSION = '2.4' def tearDown(self): version.LATEST_FORMAT_VERSION = self.original super(TestVersions, self).tearDown() def test_get_latest_as_semver(self): latest = version.get_latest_version() self.assertEqual(2, latest.major) self.assertEqual(4, latest.minor) self.assertEqual(0, latest.patch) def test_exact_match(self): version.check_version('2.4') def test_older_in_family(self): version.check_version('2.1') def test_oldest_in_family(self): version.check_version('2') def test_newer(self): self.assertRaises(ValueError, version.check_version, '2.5') def test_uncompatible_old(self): self.assertRaises(ValueError, version.check_version, '1.6') def test_uncompatible_new(self): self.assertRaises(ValueError, version.check_version, '3.0') murano-dashboard-5.0.0/muranodashboard/tests/unit/dynamic_ui/test_yaql_functions.py0000666000175100017510000001173113245511125031054 0ustar zuulzuul00000000000000# 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. import mock import re import testtools from castellan.common import exception as castellan_exception from castellan.common.objects import opaque_data from muranodashboard.dynamic_ui import helpers from muranodashboard.dynamic_ui import yaql_functions class TestYAQLFunctions(testtools.TestCase): def test_generate_hostname(self): self.assertEqual( yaql_functions._generate_hostname('foo-#', 1), 'foo-1') self.assertEqual( yaql_functions._generate_hostname('foo-#', 22), 'foo-22') def test_generate_hostname_random(self): random = yaql_functions._generate_hostname('', 3) self.assertTrue(bool(re.match(r'^\w{14}$', random))) def test_repeat(self): context = {} result = yaql_functions._repeat(context, 'foo_template', 2) self.assertEqual(['foo_template', 'foo_template'], [x for x in result]) def test_name(self): context = mock.MagicMock() context.get_data.__getitem__.return_value = \ {'application_name': 'foo_app'} self.assertEqual('foo_app', yaql_functions._name(context)) def test_ref(self): parameters = { '#foo_template': { '?': { 'id': None } } } mock_service = mock.Mock(parameters=parameters) context = {'?service': mock_service} result = yaql_functions._ref(context, 'foo_template') self.assertIsInstance(result['?']['id'], helpers.ObjectID) def test_ref_with_id_only(self): object_id = helpers.ObjectID() parameters = { '#foo_template': { '?': { 'id': object_id } } } mock_service = mock.Mock(parameters=parameters) context = {'?service': mock_service} result = yaql_functions._ref(context, 'foo_template') self.assertIsInstance(result, helpers.ObjectID) self.assertEqual(object_id, result) def test_ref_with_evaluate_template(self): templates = { 'foo_template': { '?': { 'id': helpers.ObjectID() } } } mock_service = mock.Mock(parameters={}, templates=templates) context = {'?service': mock_service} result = yaql_functions._ref(context, 'foo_template') self.assertIsInstance(result, helpers.ObjectID) def test_ref_return_none(self): mock_service = mock.Mock(parameters={'#foo_template': 'foo_data'}) context = {'?service': mock_service} result = yaql_functions._ref(context, 'foo_template') self.assertIsNone(result) @mock.patch('muranodashboard.dynamic_ui.yaql_functions.settings') @mock.patch('muranodashboard.dynamic_ui.yaql_functions._oslo_context') @mock.patch('muranodashboard.dynamic_ui.yaql_functions.key_manager') @mock.patch('muranodashboard.dynamic_ui.yaql_functions.identity') def test_encrypt_data(self, mock_identity, mock_keymanager, mock_oslo_context, _): mock_service = mock.Mock(parameters={'#foo_template': 'foo_data'}) context = {'?service': mock_service} secret_value = 'secret_password' mock_auth_context = mock.MagicMock() mock_oslo_context.RequestContext.return_value = mock_auth_context yaql_functions._encrypt_data(context, secret_value) mock_keymanager.API().store.assert_called_once_with( mock_auth_context, opaque_data.OpaqueData(secret_value)) def test_encrypt_data_not_configured(self): mock_service = mock.Mock(parameters={'#foo_template': 'foo_data'}) context = {'?service': mock_service} self.assertRaises(castellan_exception.KeyManagerError, yaql_functions._encrypt_data, context, 'secret_password') @mock.patch('muranodashboard.dynamic_ui.yaql_functions.identity') @mock.patch('muranodashboard.dynamic_ui.yaql_functions.settings') def test_encrypt_data_badly_configured(self, mock_settings, _): mock_service = mock.Mock(parameters={'#foo_template': 'foo_data'}) context = {'?service': mock_service} mock_settings.KEY_MANAGER = {} self.assertRaises(castellan_exception.KeyManagerError, yaql_functions._encrypt_data, context, 'secret_password') murano-dashboard-5.0.0/muranodashboard/tests/unit/dynamic_ui/test_helpers.py0000666000175100017510000000403613245511125027460 0ustar zuulzuul00000000000000# 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. import testtools from muranodashboard.dynamic_ui import helpers class TestHelper(testtools.TestCase): def test_to_str(self): names = ['string', b'ascii', u'ascii', u'\u043d\u0435 \u0430\u0441\u043a\u0438'] for name in names: self.assertIsInstance(helpers.to_str(name), str) def test_int2base(self): for x in range(30): self.assertEqual("{0:b}".format(x), helpers.int2base(x, 2)) self.assertEqual("{0:o}".format(x), helpers.int2base(x, 8)) self.assertEqual("{0:x}".format(x), helpers.int2base(x, 16)) def test_camelize(self): snake_name = "snake_case_name" camel_name = helpers.camelize(snake_name) self.assertEqual("SnakeCaseName", camel_name) def test_explode(self): not_string = 123456 explode_int = helpers.explode(not_string) self.assertEqual(123456, explode_int) string = "test" explode_str = helpers.explode(string) self.assertEqual(['t', 'e', 's', 't'], explode_str) def test_insert_hidden_ids(self): app_dict = {'?': { 'type': 'test.App', 'id': '123' } } app_list = [1, 2, 3, 4] dict_result = helpers.insert_hidden_ids(app_dict) list_result = helpers.insert_hidden_ids(app_list) self.assertEqual('test.App', dict_result['?']['type']) self.assertEqual(app_list, list_result) murano-dashboard-5.0.0/muranodashboard/tests/unit/dynamic_ui/__init__.py0000666000175100017510000000000013245511125026501 0ustar zuulzuul00000000000000murano-dashboard-5.0.0/muranodashboard/tests/unit/dynamic_ui/test_services.py0000666000175100017510000003107113245511125027640 0ustar zuulzuul00000000000000# Copyright (c) 2015 Mirantis, Inc. # Copyright (c) 2016 AT&T 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. import collections from django import forms import mock import semantic_version from yaql.language import factory from muranodashboard.catalog import forms as catalog_forms from muranodashboard.catalog import views as catalog_views from muranodashboard.dynamic_ui import forms as service_forms from muranodashboard.dynamic_ui import services from openstack_dashboard.test import helpers class TestService(helpers.APITestCase): def setUp(self): super(TestService, self).setUp() self.application = {'?': {'type': 'test.App'}} factory = helpers.RequestFactoryWithMessages() self.request = factory.get('/path/for/testing') self.request.session = {} def test_service_field_hidden_false(self): """Test that service field is hidden When Service class instantiated with some field having `hidden` attribute set to `false` - the generated form field should have widget different from `HiddenInput`. Bug: #1368120 """ ui = [{ 'appConfiguration': {'fields': [{'hidden': False, 'type': 'string', 'name': 'title'}]} }] service = services.Service(cleaned_data={}, version=2, fqn='io.murano.Test', application=self.application, forms=ui) form = next(e for e in service.forms if e.__name__ == 'appConfiguration') field = form.base_fields['title'] self.assertNotIsInstance(field.widget, forms.HiddenInput) def test_service_field_hidden_true(self): """Test hidden widget `hidden: true` in UI definition results to HiddenInput in Django form. """ ui = [{ 'appConfiguration': {'fields': [{'hidden': True, 'type': 'string', 'name': 'title'}]} }] service = services.Service(cleaned_data={}, version=2, fqn='io.murano.Test', application=self.application, forms=ui) form = next(e for e in service.forms if e.__name__ == 'appConfiguration') field = form.base_fields['title'] self.assertIsInstance(field.widget, forms.HiddenInput) def test_init_service_with_kwargs_and_version_coercion(self): kwargs = {'foo': 'bar', 'baz': 'qux', 'quux': 'corge'} service = services.Service(cleaned_data={}, version=semantic_version.Version('2.3.0'), fqn='io.murano.Test', application=self.application, **kwargs) for key, val in kwargs.items(): self.assertTrue(hasattr(service, key)) self.assertEqual(val, getattr(service, key)) self.assertEqual(1, len(service.forms)) for key, val in kwargs.items(): self.assertTrue(hasattr(service.forms[0].service, key)) self.assertEqual(val, getattr(service.forms[0].service, key)) def test_init_service_except_value_error(self): with self.assertRaisesRegexp(ValueError, 'Application section is required'): services.Service(cleaned_data={}, version=2, fqn='io.murano.Test', application=None) def test_extract_attributes(self): cleaned_data = { services.catalog_forms.WF_MANAGEMENT_NAME: { 'application_name': 'foobar' } } templates = {'t1': 'foo', 't2': 'bar', 't3': 'baz'} service = services.Service(cleaned_data=cleaned_data, version=semantic_version.Version('2.3.0'), fqn='io.murano.Test', application=self.application, templates=templates) attributes = service.extract_attributes() expected = {'?': {'type': 'test.App', 'name': 'foobar'}} self.assertIsInstance(attributes, dict) self.assertEqual(expected['?']['type'], attributes['?']['type']) self.assertEqual(expected['?']['name'], attributes['?']['name']) self.assertEqual('foobar', service.application['?']['name']) def test_get_and_set_cleaned_data(self): cleaned_data = { catalog_forms.WF_MANAGEMENT_NAME: { 'application_name': 'foobar' } } engine_factory = factory.YaqlFactory() engine = engine_factory.create() expr = engine('$') service = services.Service(cleaned_data={}, version=semantic_version.Version('2.3.0'), fqn='io.murano.Test', application=self.application) service.set_data(cleaned_data) result = service.get_data(catalog_forms.WF_MANAGEMENT_NAME, expr) expected = {'workflowManagement': {'application_name': 'foobar'}} self.assertEqual(expected, result) # Test whether passing data to get_data works. service.set_data({}) cleaned_data = cleaned_data[catalog_forms.WF_MANAGEMENT_NAME] result = service.get_data( catalog_forms.WF_MANAGEMENT_NAME, expr, data=cleaned_data) self.assertEqual(expected, result) def test_get_apps_data(self): result = services.get_apps_data(self.request) self.assertEqual({}, result) self.assertEqual(self.request.session['apps_data'], result) @mock.patch.object(services, 'pkg_api') def test_import_app(self, mock_pkg_api): mock_pkg_api.get_app_ui.return_value = { 'foo': 'bar', 'application': self.application } mock_pkg_api.get_app_fqn.return_value = 'foo_fqn' service = services.import_app(self.request, '123') self.assertEqual(services.Service, type(service)) self.assertEqual('bar', service.foo) self.assertEqual(self.application, service.application) @mock.patch.object(services, 'pkg_api') def test_condition_getter_with_stay_at_the_catalog(self, mock_pkg_api): mock_pkg_api.get_app_ui.return_value = { 'foo': 'bar', 'application': self.application } service = services.Service(cleaned_data={}, version=semantic_version.Version('2.2.1'), fqn='io.murano.Test', application=self.application) form = service_forms.ServiceConfigurationForm() form.service = service form.base_fields = {'stay_at_the_catalog': True} wizard = catalog_views.Wizard() wizard.kwargs = {'drop_wm_form': True} wizard.form_list = collections.OrderedDict({ '123': form }) kwargs = {'app_id': '123'} result = services.condition_getter(self.request, kwargs) self.assertIn('Step 1', result) self.assertIsNotNone(result['Step 1']) result = result['Step 1'](wizard) self.assertTrue(result) self.assertNotIn('stay_at_the_catalog', form.base_fields) @mock.patch.object(services, 'pkg_api') def test_condition_getter_with_application_name(self, mock_pkg_api): mock_pkg_api.get_app_ui.return_value = { 'foo': 'bar', 'application': self.application } service = services.Service(cleaned_data={}, version=semantic_version.Version('2.1.9'), fqn='io.murano.Test', application=self.application) form = service_forms.ServiceConfigurationForm() form.service = service form.base_fields = {'application_name': 'foo_app_name'} wizard = catalog_views.Wizard() wizard.kwargs = {'drop_wm_form': False} wizard.form_list = collections.OrderedDict({ '123': form }) kwargs = {'app_id': '123'} result = services.condition_getter(self.request, kwargs) self.assertIn('Step 1', result) self.assertIsNotNone(result['Step 1']) result = result['Step 1'](wizard) self.assertTrue(result) self.assertNotIn('application_name', form.base_fields) @mock.patch.object(services, 'pkg_api') def test_condition_getter_with_form_hidden(self, mock_pkg_api): mock_pkg_api.get_app_ui.return_value = { 'foo': 'bar', 'application': self.application } service = services.Service(cleaned_data={}, version=semantic_version.Version('2.1.9'), fqn='io.murano.Test', application=self.application) form = service_forms.ServiceConfigurationForm() form.service = service wizard = catalog_views.Wizard() wizard.kwargs = {'drop_wm_form': True} wizard.form_list = collections.OrderedDict({ '123': form }) kwargs = {'app_id': '123'} result = services.condition_getter(self.request, kwargs) self.assertIn('Step 1', result) self.assertIsNotNone(result['Step 1']) result = result['Step 1'](wizard) self.assertFalse(result) @mock.patch.object(services, 'pkg_api') def test_get_app_forms(self, mock_pkg_api): mock_pkg_api.get_app_ui.return_value = {'Application': {}} kwargs = {'app_id': '123'} result = services.get_app_forms(self.request, kwargs) self.assertIsNotNone(result) result = next(iter(result)) self.assertEqual('Step 1', result[0]) self.assertEqual(service_forms.DynamicFormMetaclass, type(result[1])) def test_service_type_from_id(self): match_id = 'aaa123-345' non_match_ids = ['aaa', '-aaa', 'aaa123-aaa'] result = services.service_type_from_id(match_id) self.assertEqual('aaa123', result) for non_match_id in non_match_ids: result = services.service_type_from_id(non_match_id) self.assertEqual(non_match_id, result) @mock.patch.object(services, 'import_app') def test_get_app_field_description(self, mock_import_app): form = service_forms.ServiceConfigurationForm() mock_field = mock.Mock(description_title='test_title', description='test_description') mock_field.widget.is_hidden = False form.base_fields = {'foo': mock_field} mock_import_app.return_value = mock.Mock(forms=[form]) result = services.get_app_field_descriptions(self.request, '123', 0) self.assertEqual(2, len(result)) descriptions, no_field_descriptions = result self.assertEqual([('foo', 'test_title', 'test_description')], descriptions) self.assertEqual([], no_field_descriptions) @mock.patch.object(services, 'import_app') def test_get_app_field_description_with_hidden_field(self, mock_import_app): form = service_forms.ServiceConfigurationForm() mock_field = mock.Mock(description_title='test_title', description='test_description') mock_field.widget.is_hidden = True form.base_fields = {'foo': mock_field} mock_import_app.return_value = mock.Mock(forms=[form]) result = services.get_app_field_descriptions(self.request, '123', 0) self.assertEqual(2, len(result)) descriptions, no_field_descriptions = result self.assertEqual(['test_description', 'test_title'], sorted(no_field_descriptions)) self.assertEqual([], descriptions) murano-dashboard-5.0.0/muranodashboard/tests/unit/dynamic_ui/test_yaql_expression.py0000666000175100017510000000361113245511125031241 0ustar zuulzuul00000000000000# Copyright (c) 2016 AT&T # # 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 testtools from muranodashboard.dynamic_ui import yaql_expression class TestYaqlExpression(testtools.TestCase): def setUp(self): super(TestYaqlExpression, self).setUp() yaql = "$foo" string = "test" self.yaql_expr = yaql_expression.YaqlExpression(yaql) self.str_expr = yaql_expression.YaqlExpression(string) def test_overloading(self): self.assertEqual("test", self.str_expr.__str__()) self.assertEqual("$foo", self.yaql_expr.__str__()) self.assertEqual("YAQL(test)", self.str_expr.__repr__()) self.assertEqual("YAQL($foo)", self.yaql_expr.__repr__()) def test_expression(self): self.assertEqual("$foo", self.yaql_expr.expression()) self.assertEqual("test", self.str_expr.expression()) def test_match(self): self.assertFalse(self.str_expr.match(12345)) self.assertFalse(self.str_expr.match(self.str_expr._expression)) self.assertTrue(self.yaql_expr.match(self.yaql_expr._expression)) self.assertFalse(self.yaql_expr.match("$!")) # YaqlLexicalException self.assertFalse(self.yaql_expr.match("$foo(")) # YaqlGrammarException def test_evaluate(self): self.assertEqual("test", self.str_expr.evaluate()) self.assertIsNone(self.yaql_expr.evaluate()) murano-dashboard-5.0.0/muranodashboard/tests/unit/dynamic_ui/test_forms.py0000666000175100017510000001757213245511125027155 0ustar zuulzuul00000000000000# Copyright (c) 2016 AT&T 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. import collections import mock import testtools from yaql.language import contexts as yaql_contexts from django import forms as django_forms from muranodashboard.dynamic_ui import fields from muranodashboard.dynamic_ui import forms from muranodashboard.dynamic_ui import yaql_expression class TestAnyFieldDict(testtools.TestCase): def test_missing(self): any_field_dict = forms.AnyFieldDict() result = any_field_dict.__missing__(['foo', 'bar']) self.assertEqual('DynamicSelect', result.__name__) class TestDynamicUiForm(testtools.TestCase): def test_collect_fields_process_widget(self): test_spec = { 'type': 'text', 'name': 'foo_spec', 'widget_media': { 'js': 'foo.js', 'css': 'foo.css' }, 'widget_attrs': {'foo': 'bar'} } result = forms._collect_fields([test_spec], 'foo_form', None) self.assertIsInstance(result, list) self.assertEqual(1, len(result)) self.assertEqual('foo_spec', result[0][0]) self.assertIsInstance(result[0][1], fields.CharField) field = result[0][1] self.assertTrue(hasattr(field, 'widget')) self.assertTrue(hasattr(field.widget, 'Media')) self.assertEqual('foo.js', field.widget.Media.js) self.assertEqual('foo.css', field.widget.Media.css) self.assertIn('foo', field.widget.__dict__['attrs']) self.assertEqual('bar', field.widget.__dict__['attrs']['foo']) def test_collect_fields_parse_spec_with_yaql_expression(self): mock_yaql = mock.Mock(spec=yaql_expression.YaqlExpression) test_spec = { 'type': 'choice', 'name': 'foo_spec', 'validators': [{'expr': mock_yaql}] } result = forms._collect_fields([test_spec], 'foo_form', None) self.assertIsInstance(result, list) self.assertEqual(1, len(result)) self.assertEqual('foo_spec', result[0][0]) self.assertIsInstance(result[0][1], fields.ChoiceField) field = result[0][1] self.assertTrue(hasattr(field, 'validators')) self.assertIn('expr', field.validators[0]) self.assertIsInstance(field.validators[0]['expr'], fields.RawProperty) self.assertEqual(mock_yaql, field.validators[0]['expr'].spec) class TestDynamicFormMetaclass(testtools.TestCase): def test_new(self): test_dict = { 'name': 'foo_form', 'field_specs': [{'type': 'text', 'name': 'foo_spec'}], 'service': 'foo_service', } form = forms.DynamicFormMetaclass('foo_form', (), test_dict) self.assertEqual('foo_form', form.__name__) self.assertEqual('foo_service', form.service) self.assertIsInstance(form.declared_fields, collections.OrderedDict) self.assertIn('foo_spec', form.declared_fields) self.assertIsInstance(form.declared_fields['foo_spec'], fields.CharField) class TestUpdatableFieldsForm(testtools.TestCase): def setUp(self): super(TestUpdatableFieldsForm, self).setUp() self.form = forms.UpdatableFieldsForm(None) self.assertEqual('required', self.form.required_css_class) def test_update_fields(self): mock_password_field = mock.Mock(spec=fields.PasswordField) mock_password_field.confirm_input = True mock_password_field.has_clone = False mock_password_field.original = True mock_password_field.required = True mock_password_field.update = mock.Mock() mock_password_field.initial = 'foo_initial' mock_password_field.get_clone_name.return_value = 'bar_password_field' mock_clone_password_field = mock.Mock( spec=fields.PasswordField, required=True) mock_password_field.clone_field.return_value = \ mock_clone_password_field self.form.fields = collections.OrderedDict({ 'foo_password_field': mock_password_field}) self.form.update_fields() self.assertEqual(2, len(self.form.fields)) self.assertIn('foo_password_field', self.form.fields) self.assertIn('bar_password_field', self.form.fields) self.assertEqual(mock_password_field, self.form.fields['foo_password_field']) self.assertEqual(mock_clone_password_field, self.form.fields['bar_password_field']) mock_password_field.get_clone_name.assert_called_once_with( 'foo_password_field') self.assertTrue(mock_password_field.get_clone_name.called) self.assertTrue(mock_password_field.update.called) class TestServiceConfigurationForm(testtools.TestCase): def setUp(self): super(TestServiceConfigurationForm, self).setUp() mock_service = mock.Mock() mock_service.update_cleaned_data.return_value = \ {'foo': 'bar', 'baz': 'qux'} self.form = forms.ServiceConfigurationForm( initial={'app_id': 'foo_app'}) self.form._errors = [] self.form.validators = [] self.form.cleaned_data = {'foo': 'bar', 'baz': 'qux'} self.form.service = mock_service self.assertEqual('foo_app_%s', self.form.auto_id) self.assertIsInstance(self.form.context, yaql_contexts.Context) @mock.patch.object(forms, 'LOG', autospec=True) def test_clean(self, mock_log): password_field = mock.Mock(spec=fields.PasswordField) password_field.enabled = True password_field.confirm_input = True foo_field = mock.Mock() foo_field.enabled = False foo_field.postclean.return_value = 'post_foo' self.form.fields = {'foo': foo_field, 'password': password_field} result = self.form.clean() expected_cleaned_data = {'foo': 'post_foo', 'baz': 'qux'} for key, val in expected_cleaned_data.items(): self.assertEqual(val, result[key]) # NOTE(felipemonteiro): mock.ANY is being used in the assertions # below, rather than `{'foo': 'bar', 'baz': 'qux'}` because # `cleaned_data[name] = value` in clean() appears to also change the # dict that was passed in to mock objects in previous lines of code. foo_field.postclean.assert_called_once_with(self.form, 'foo', mock.ANY) password_field.compare.assert_called_once_with('password', mock.ANY) mock_log.debug.assert_called_once_with( "Update 'foo' data in postclean method") self.form.service.update_cleaned_data.assert_called_with( mock.ANY, form=self.form) def test_clean_except_validation_error(self): mock_expr = mock.Mock() mock_expr.evaluate.return_value = False test_validator = {'expr': mock_expr, 'message': 'Foo Error'} self.form.validators = [test_validator] with self.assertRaisesRegexp(django_forms.ValidationError, 'Foo Error'): self.form.clean() self.form.service.update_cleaned_data.assert_called_once_with( {'foo': 'bar', 'baz': 'qux'}, form=self.form) mock_expr.evaluate.assert_called_once_with( data={'foo': 'bar', 'baz': 'qux'}, context=self.form.context) def test_clean_with_errors(self): self.form._errors = ['foo_error'] self.assertEqual({'foo': 'bar', 'baz': 'qux'}, self.form.clean()) murano-dashboard-5.0.0/muranodashboard/tests/unit/dynamic_ui/test_fields.py0000666000175100017510000010650313245511125027266 0ustar zuulzuul00000000000000# Copyright (c) 2016 AT&T 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. from django.core import exceptions from django.core import validators as django_validator from django import forms from django.utils.translation import ugettext_lazy as _ import mock import testtools from muranodashboard.dynamic_ui import fields class TestFields(testtools.TestCase): def setUp(self): super(TestFields, self).setUp() self.request = mock.Mock() self.request.user.service_region = None self.request.is_ajax = mock.Mock(side_effect=False) self.addCleanup(mock.patch.stopall) @mock.patch.object(fields, 'LOG') def test_fields_with_initial_request(self, mock_log): test_initial = { 'request': 'foo_request', 'foo': 'bar' } self._test_fields_decorator_with_initial_request(test_initial) mock_log.debug.assert_called_once_with( "Using 'request' value from initial dictionary") @fields.with_request def _test_fields_decorator_with_initial_request(self, request, **kwargs): self.assertEqual('foo_request', request) self.assertEqual({'foo': 'bar'}, kwargs) @mock.patch.object(fields, 'LOG') def test_fields_with_request(self, mock_log): test_request = { 'foo': 'bar' } self._test_fields_decorator_with_request({}, request=test_request) mock_log.debug.assert_called_once_with("Using direct 'request' value") @fields.with_request def _test_fields_decorator_with_request(self, request, **kwargs): self.assertEqual({'foo': 'bar'}, request) self.assertEqual({}, kwargs) @mock.patch.object(fields, 'LOG') def test_fields_except_validation_error(self, mock_log): with self.assertRaisesRegexp(forms.ValidationError, "Can't get a request information"): self._test_fields_decorator_with_validation_error({}, request=None) mock_log.error.assert_called_once_with( "No 'request' value passed neither via initial dictionary, nor " "directly") @fields.with_request def _test_fields_decorator_with_validation_error(self, request, **kwargs): pass def test_make_yaql_validator(self): mock_validator_property = mock.MagicMock() mock_validator_property.__getitem__().spec.evaluate.return_value = True mock_validator_property.get.return_value = 'foo_message' validator_func = fields.make_yaql_validator(mock_validator_property) self.assertTrue(hasattr(validator_func, '__call__')) validator_func('bar') mock_validator_property.__getitem__().spec.evaluate.\ assert_called_once_with(context=mock.ANY) def test_make_yaql_validator_except_validation_error(self): mock_validator_property = mock.MagicMock() mock_validator_property.__getitem__().spec.evaluate.return_value =\ False mock_validator_property.get.return_value = 'foo_message' validator_func = fields.make_yaql_validator(mock_validator_property) self.assertTrue(hasattr(validator_func, '__call__')) e = self.assertRaises(forms.ValidationError, validator_func, 'bar') self.assertEqual('foo_message', e.message) def test_get_regex_validator(self): validator = django_validator.RegexValidator() test_expr = { 'validators': [ validator ] } result = fields.get_regex_validator(test_expr) self.assertEqual(validator, result) def test_get_regex_validator_except_error(self): for error in (TypeError, KeyError, IndexError): mock_expr = mock.MagicMock() mock_expr.__getitem__.side_effect = error result = fields.get_regex_validator(mock_expr) self.assertIsNone(result) def test_wrap_regex_validator(self): def _validator(value): pass func = fields.wrap_regex_validator(_validator, None) func(None) self.assertTrue(hasattr(func, '__call__')) def test_wrap_regex_validator_except_validation_error(self): def _validator(value): raise forms.ValidationError(None) with self.assertRaisesRegexp(forms.ValidationError, 'foo'): func = fields.wrap_regex_validator(_validator, 'foo') func(None) @mock.patch.object(fields, 'glance') def test_get_murano_images(self, mock_glance): foo_image = mock.Mock(murano_property=None) foo_image.murano_image_info = '{"foo": "foo_val"}' bar_image = mock.Mock(murano_property=None) bar_image.murano_image_info = '{"bar": "bar_val"}' mock_glance.image_list_detailed.return_value = [ [foo_image, bar_image], None ] murano_images = fields.get_murano_images(self.request) mock_glance.image_list_detailed.assert_called_once_with(self.request) self.assertEqual({"foo": "foo_val"}, foo_image.murano_property) self.assertEqual({"bar": "bar_val"}, bar_image.murano_property) expected_images = [] foo_image.murano_property = {"foo": "foo_val"} bar_image.murano_property = {"bar": "bar_val"} expected_images.extend([foo_image, bar_image]) self.assertEqual(expected_images, murano_images) @mock.patch.object(fields, 'exceptions') @mock.patch.object(fields, 'LOG') @mock.patch.object(fields, 'glance') def test_murano_images_except_exception(self, mock_glance, mock_log, mock_exceptions): mock_glance.image_list_detailed.side_effect = Exception murano_images = fields.get_murano_images(self.request) self.assertEqual([], murano_images) self.assertTrue(mock_log.error.called) mock_exceptions.handle.assert_called_once_with( self.request, _("Unable to retrieve public images.")) @mock.patch.object(fields, 'messages') @mock.patch.object(fields, 'LOG') @mock.patch.object(fields, 'glance') def test_murano_images_except_value_error(self, mock_glance, mock_log, mock_messages): foo_image = mock.Mock(murano_property=None) foo_image.murano_image_info = "{'foo': 'foo_val'}" mock_glance.image_list_detailed.return_value = [ [foo_image], None ] murano_images = fields.get_murano_images(self.request) self.assertEqual([], murano_images) mock_log.warning.assert_called_once_with( "JSON in image metadata is not valid. Check it in glance.") mock_messages.error.assert_called_once_with( self.request, _("Invalid murano image metadata")) def test_choice_get_title(self): choice = fields.Choice('test_title', True) self.assertEqual('test_title', fields._get_title(choice)) self.assertIsNone(fields._get_title(None)) def test_choice_disable_non_ready(self): choice = fields.Choice('test_title', True) self.assertEqual({}, fields._disable_non_ready(choice)) choice = fields.Choice('test_title', False) self.assertEqual({'disabled': 'disabled'}, fields._disable_non_ready(choice)) @mock.patch.object(fields, 'env_api') @mock.patch.object(fields, 'pkg_api') def test_make_select_cls_update(self, mock_pkg_api, mock_env_api): mock_pkg_api.app_by_fqn.return_value =\ mock.Mock(fully_qualified_name='foo_class_fqn') mock_pkg_api.apps_that_inherit.return_value = [ mock.Mock(fully_qualified_name='foo_class_fqn'), mock.Mock(fully_qualified_name='bar_class_fqn') ] expected_choices = [ ('', 'Foo'), ('foo_app_id', 'foo_app_name'), ('bar_app_id', 'bar_app_name') ] foo_app = mock.MagicMock() foo_app.__getitem__.return_value = {'id': 'foo_app_id'} foo_app.configure_mock(name='foo_app_name') bar_app = mock.MagicMock() bar_app.__getitem__.return_value = {'id': 'bar_app_id'} bar_app.configure_mock(name='bar_app_name') mock_env_api.service_list_by_fqns.return_value = [foo_app, bar_app] dynamic_select_cls = fields.make_select_cls('foo_class_fqn') self.assertIsNotNone(dynamic_select_cls) self.assertEqual('DynamicSelect', dynamic_select_cls.__name__) dynamic_select = dynamic_select_cls(empty_value_message='Foo') dynamic_select.update({}, self.request, environment_id='foo_env_id') self.assertTrue( hasattr(dynamic_select.widget.add_item_link, '__call__')) self.assertEqual(expected_choices, dynamic_select.choices) self.assertIsNone(dynamic_select.initial) mock_pkg_api.app_by_fqn.assert_called_once_with( self.request, 'foo_class_fqn') mock_env_api.service_list_by_fqns.assert_called_once_with( self.request, 'foo_env_id', ['foo_class_fqn', 'bar_class_fqn'] ) @mock.patch.object(fields, 'env_api') @mock.patch.object(fields, 'pkg_api') def test_make_select_cls_update_2_choices(self, mock_pkg_api, mock_env_api): mock_pkg_api.app_by_fqn.return_value =\ mock.Mock(fully_qualified_name='foo_class_fqn') mock_pkg_api.apps_that_inherit.return_value = [] expected_choices = [ ('', 'Foo'), ('foo_app_id', 'foo_app_name') ] foo_app = mock.MagicMock() foo_app.__getitem__.return_value = {'id': 'foo_app_id'} foo_app.configure_mock(name='foo_app_name') mock_env_api.service_list_by_fqns.return_value = [foo_app] dynamic_select_cls = fields.make_select_cls('foo_class_fqn') dynamic_select = dynamic_select_cls(empty_value_message='Foo') dynamic_select.update({}, self.request, environment_id='foo_env_id') self.assertEqual(expected_choices, dynamic_select.choices) self.assertEqual('foo_app_id', dynamic_select.initial) mock_pkg_api.app_by_fqn.assert_called_once_with( self.request, 'foo_class_fqn') mock_env_api.service_list_by_fqns.assert_called_once_with( self.request, 'foo_env_id', ['foo_class_fqn'] ) @mock.patch.object(fields, 'env_api') @mock.patch.object(fields, 'pkg_api') def test_make_select_cls_update_no_matching_classes(self, mock_pkg_api, mock_env_api): mock_pkg_api.app_by_fqn.return_value = None mock_pkg_api.apps_that_inherit.return_value = [] mock_env_api.service_list_by_fqns.return_value = [] expected_choices = [('', 'Foo')] dynamic_select_cls = fields.make_select_cls('foo_class_fqn') dynamic_select = dynamic_select_cls(empty_value_message='Foo') dynamic_select.update({}, self.request, environment_id='foo_env_id') self.assertEqual(expected_choices, dynamic_select.choices) self.assertIsNone(dynamic_select.initial) mock_pkg_api.app_by_fqn.assert_called_once_with( self.request, 'foo_class_fqn') mock_env_api.service_list_by_fqns.assert_called_once_with( self.request, 'foo_env_id', []) @mock.patch.object(fields, 'reverse') @mock.patch.object(fields, 'env_api') @mock.patch.object(fields, 'pkg_api') def test_make_select_cls_update_make_link(self, mock_pkg_api, mock_env_api, mock_reverse): mock_pkg_api.app_by_fqn.return_value = None mock_pkg_api.apps_that_inherit.return_value = [] mock_env_api.service_list_by_fqns.return_value = [] mock_reverse.return_value = 'foo_url' dynamic_select_cls = fields.make_select_cls('foo_class_fqn') dynamic_select = dynamic_select_cls(empty_value_message='Foo') dynamic_select.update({}, self.request, environment_id='foo_env_id') result = dynamic_select.widget.add_item_link() self.assertEqual('', result) mock_pkg = mock.Mock(fully_qualified_name='foo_class_fqn') mock_pkg.configure_mock(name='foo_class_name') mock_pkg_api.app_by_fqn.return_value = mock_pkg dynamic_select.update({}, self.request, environment_id='foo_env_id') result = dynamic_select.widget.add_item_link() expected = '[["foo_class_name", "foo_url"]]' self.assertEqual(expected, result) @mock.patch.object(fields, 'env_api') @mock.patch.object(fields, 'pkg_api') def test_update_clean(self, mock_pkg_api, mock_env_api): mock_pkg_api.app_by_fqn.return_value = None mock_pkg_api.apps_that_inherit.return_value = [] mock_env_api.service_list_by_fqns.return_value = [] dynamic_select_cls = fields.make_select_cls('foo_class_fqn') dynamic_select = dynamic_select_cls(empty_value_message='Foo') dynamic_select.form = mock.Mock() dynamic_select.required = False dynamic_select.choices = [('value', '')] self.assertEqual('value', dynamic_select.clean('value')) class TestRawProperty(testtools.TestCase): def test_finalize(self): class Control(object): def __init__(self): self.value = None @property def prop(self): return self.value @prop.setter def prop(self, value): self.value = value @prop.deleter def prop(self): delattr(self, 'value') mock_service = mock.Mock() mock_service.get_data.side_effect = ['foo_value'] raw_property = fields.RawProperty('prop', 'foo_spec') props = raw_property.finalize( 'foo_form_name', mock_service, Control) ctl = Control() result = props.fget(ctl) self.assertEqual('foo_value', result) props.fset(ctl, 'bar_value') self.assertEqual('bar_value', ctl.prop) props.fdel(ctl) self.assertNotIn('prop', ctl.__dict__) class TestCustomPropertiesField(testtools.TestCase): def setUp(self): super(TestCustomPropertiesField, self).setUp() test_validator_1 = mock.MagicMock(__call__=lambda: None) test_validator_2 = { 'expr': { 'validators': [django_validator.RegexValidator()] } } test_validator_3 = { 'expr': fields.RawProperty(None, None) } kwargs = { 'validators': [ test_validator_1, test_validator_2, test_validator_3 ] } for arg in fields.FIELD_ARGS_TO_ESCAPE: kwargs[arg] = 'foo_' + arg custom_props_field = fields.CustomPropertiesField(**kwargs) for arg in fields.FIELD_ARGS_TO_ESCAPE: self.assertTrue(hasattr(custom_props_field, arg)) self.assertEqual('foo_{0}'.format(arg), getattr(custom_props_field, arg)) self.assertEqual(3, len(custom_props_field.validators)) def test_clean(self): mock_form = mock.Mock() mock_form.cleaned_data = 'test_cleaned_data' custom_props_field = fields.CustomPropertiesField() custom_props_field.form = mock_form custom_props_field.enabled = True self.assertEqual('foo', custom_props_field.clean('foo')) custom_props_field.enabled = False self.assertEqual('foo', custom_props_field.clean('foo')) def test_finalize_properties(self): finalize_properties = fields.CustomPropertiesField.finalize_properties kwargs = { 'foo_raw_property': fields.RawProperty('foo_key', 'foo_spec') } mock_service = mock.Mock() result = finalize_properties(kwargs, 'foo_form_name', mock_service) self.assertIsNotNone(result) result = finalize_properties({}, 'foo_form_name', mock_service) self.assertIsNotNone(result) class TestPasswordField(testtools.TestCase): def setUp(self): super(TestPasswordField, self).setUp() self.password_field = fields.PasswordField(None) self.password_field.original = True self.password_field.required = True self.addCleanup(mock.patch.stopall) def test_get_clone_name(self): self.assertEqual('foo-clone', fields.PasswordField.get_clone_name('foo')) def test_compare(self): test_form_data = {'name': 'foo', 'name-clone': 'foo'} result = self.password_field.compare('name', test_form_data) self.assertIsNone(result) def test_compare_except_validation_error(self): test_form_data = {'name': 'foo', 'name-clone': 'bar'} self.assertRaises(forms.ValidationError, self.password_field.compare, 'name', test_form_data) def test_deepcopy(self): self.password_field.error_messages = None test_memo = {} result = self.password_field.__deepcopy__(test_memo) self.assertIsInstance(result, fields.PasswordField) self.assertGreater(len(test_memo.keys()), 0) self.password_field.error_messages = ['foo_error', 'bar_error'] test_memo = {} result = self.password_field.__deepcopy__(test_memo) self.assertIsInstance(result, fields.PasswordField) self.assertGreater(len(test_memo.keys()), 0) self.assertEqual(['foo_error', 'bar_error'], result.error_messages) def test_clone_field(self): self.assertFalse(self.password_field.has_clone) result = self.password_field.clone_field() self.assertIsInstance(result, fields.PasswordField) self.assertFalse(result.original) self.assertEqual('Confirm password', result.label) self.assertEqual('Please confirm your password', result.error_messages['required']) self.assertEqual('Retype your password', result.help_text) class TestFlavorChoiceField(testtools.TestCase): def setUp(self): super(TestFlavorChoiceField, self).setUp() self.requirements = { 'min_vcpus': 1, 'min_disk': 100, 'min_memory_mb': 500, 'max_vcpus': 5, 'max_disk': 5000, 'max_memory_mb': 16000 } kwargs = { 'requirements': self.requirements } self.flavor_choice_field = fields.FlavorChoiceField(**kwargs) self.flavor_choice_field.choices = [] self.flavor_choice_field.initial = None self.assertEqual(kwargs['requirements'], self.flavor_choice_field.requirements) self.request = {'request': mock.Mock()} self.tiny_flavor = mock.Mock() self.tiny_flavor.configure_mock(id='id1', name='m1.tiny') self.small_flavor = mock.Mock() self.small_flavor.configure_mock(id='id2', name='m1.small') self.medium_flavor = mock.Mock() self.medium_flavor.configure_mock(id='id3', name='m1.medium') self.addCleanup(mock.patch.stopall) @mock.patch.object(fields, 'nova') def test_update(self, mock_nova): """"Test if flavor with any invalid requirement is excluded.""" mock_nova.novaclient().flavors.list.return_value = [ self.tiny_flavor, self.small_flavor, self.medium_flavor ] expected_choices = [ ('id3', 'm1.medium'), ('id2', 'm1.small') ] valid_requirements = [ ('vcpus', 2), ('disk', 101), ('ram', 501) ] invalid_requirements = [ ('vcpus', 0), ('vcpus', 6), ('disk', 99), ('disk', 5001), ('ram', 499), ('ram', 16001) ] for req in valid_requirements: for flavor in (self.small_flavor, self.medium_flavor): setattr(flavor, req[0], req[1]) for invalid_req in invalid_requirements: for valid_req in valid_requirements: if invalid_req[0] != valid_req[0]: setattr(self.tiny_flavor, valid_req[0], valid_req[1]) setattr(self.tiny_flavor, invalid_req[0], invalid_req[1]) self.flavor_choice_field.update(self.request) self.assertEqual(expected_choices, self.flavor_choice_field.choices) self.assertEqual('id3', self.flavor_choice_field.initial) @mock.patch.object(fields, 'nova') def test_update_without_requirements(self, mock_nova): mock_nova.novaclient().flavors.list.return_value = [ self.tiny_flavor, self.small_flavor, self.medium_flavor ] del self.flavor_choice_field.requirements expected_choices = [ ('id3', 'm1.medium'), ('id2', 'm1.small'), ('id1', 'm1.tiny') ] self.flavor_choice_field.update(self.request) self.assertEqual(expected_choices, self.flavor_choice_field.choices) self.assertEqual('id3', self.flavor_choice_field.initial) class TestKeyPairChoiceField(testtools.TestCase): def setUp(self): super(TestKeyPairChoiceField, self).setUp() self.request = {'request': mock.Mock()} self.addCleanup(mock.patch.stopall) @mock.patch.object(fields, 'nova') def test_update(self, mock_nova): foo_keypair = mock.Mock() bar_keypair = mock.Mock() foo_keypair.configure_mock(name='foo') bar_keypair.configure_mock(name='bar') mock_nova.novaclient().keypairs.list.return_value = [ foo_keypair, bar_keypair ] key_pair_choice_field = fields.KeyPairChoiceField() key_pair_choice_field.choices = [] key_pair_choice_field.update(self.request) expected_choices = [ ('', _('No keypair')), ('foo', 'foo'), ('bar', 'bar') ] self.assertEqual(sorted(expected_choices), sorted(key_pair_choice_field.choices)) class TestSecurityGroupChoiceField(testtools.TestCase): def setUp(self): super(TestSecurityGroupChoiceField, self).setUp() self.request = {'request': mock.Mock()} self.addCleanup(mock.patch.stopall) @mock.patch.object(fields, 'neutron') def test_update(self, mock_neutron): mock_neutron.security_group_list.return_value = [ mock.Mock(name_or_id='foo'), mock.Mock(name_or_id='bar') ] security_group_choice_field = fields.SecurityGroupChoiceField() security_group_choice_field.choices = [] security_group_choice_field.update(self.request) expected_choices = [ ('', _('Application default security group')), ('foo', 'foo'), ('bar', 'bar') ] self.assertEqual(sorted(expected_choices), sorted(security_group_choice_field.choices)) class TestImageChoiceField(testtools.TestCase): def setUp(self): super(TestImageChoiceField, self).setUp() self.request = {'request': mock.Mock()} self.addCleanup(mock.patch.stopall) @mock.patch.object(fields, 'get_murano_images') def test_update(self, mock_get_murano_images): mock_get_murano_images.return_value = [ # Test successful control flow. mock.Mock(id='foo_image_id', murano_property={ 'title': 'foo_image_title', 'type': 'png'}, status='active'), # Test whether second continue statement works. mock.Mock(id='bar_image_id', murano_property={ 'title': 'foo_image_title', 'type': 'jpg'}, status='active') ] image_choice_field = fields.ImageChoiceField() image_choice_field.image_type = 'png' image_choice_field.choices = [] image_choice_field.update(self.request) self.assertEqual(("", _("Select Image")), image_choice_field.choices[0]) self.assertEqual("foo_image_id", image_choice_field.choices[1][0]) self.assertIsInstance(image_choice_field.choices[1][1], fields.Choice) # Test whether first continue statement works. mock_get_murano_images.return_value = [ mock.Mock(murano_property={ 'title': 'bar_image_title', 'type': None}, status=None) ] image_choice_field.image_type = '' image_choice_field.choices = [] image_choice_field.update(self.request) expected_choices = [("", _("No images available"))] self.assertEqual(expected_choices, image_choice_field.choices) class TestNetworkChoiceField(testtools.TestCase): def setUp(self): super(TestNetworkChoiceField, self).setUp() self.network_choice_field = fields.NetworkChoiceField( filter=None, murano_networks='exclude', allow_auto=True) self.request = {'request': mock.Mock()} self.addCleanup(mock.patch.stopall) @mock.patch.object(fields, 'net') def test_update(self, mock_net): mock_net.get_available_networks.return_value = [ (('foo', 'foo'), _('Foo')) ] expected_choices = [ ((None, None), _('Auto')), (('foo', 'foo'), _('Foo')) ] self.network_choice_field.update(self.request) self.assertEqual(expected_choices, self.network_choice_field.choices) mock_net.get_available_networks.assert_called_once_with( self.request['request'], None, 'exclude') def test_to_python(self): self.assertEqual({'foo': 'bar'}, self.network_choice_field.to_python('{"foo": "bar"}')) self.assertEqual((None, None), self.network_choice_field.to_python(None)) class TestVolumeChoiceField(testtools.TestCase): def setUp(self): super(TestVolumeChoiceField, self).setUp() self.request = {'request': mock.Mock()} self.addCleanup(mock.patch.stopall) @mock.patch.object(fields, 'cinder') def test_update(self, mock_cinder): foo_vol = mock.Mock() bar_snap = mock.Mock() baz_snap = mock.Mock() foo_vol.configure_mock(name='foo_vol', id='foo_id', status='available') bar_snap.configure_mock(name='bar_snap', id='bar_id', status='available') baz_snap.configure_mock(name='baz_snap', id='baz_id', status='error') mock_cinder.volume_list.return_value = [foo_vol] mock_cinder.volume_snapshot_list.return_value = [bar_snap] volume_choice_field = fields.VolumeChoiceField() volume_choice_field.choices = [] volume_choice_field.update(self.request) expected_choices = [ ('', _('Select volume')), ('foo_id', 'foo_vol'), ('bar_id', 'bar_snap') ] self.assertEqual(sorted(expected_choices), sorted(volume_choice_field.choices)) @mock.patch.object(fields, 'cinder') def test_update_withoutsnapshot(self, mock_cinder): foo_vol = mock.Mock() bar_vol = mock.Mock() baz_snap = mock.Mock() foo_vol.configure_mock(name='foo_vol', id='foo_id', status='available') bar_vol.configure_mock(name='bar_vol', id='bar_id', status='error') baz_snap.configure_mock(name='baz_snap', id='baz_id', status='available') mock_cinder.volume_list.return_value = [foo_vol] mock_cinder.volume_snapshot_list.return_value = [baz_snap] volume_choice_field = fields.VolumeChoiceField(include_snapshots=False) volume_choice_field.choices = [] volume_choice_field.update(self.request) expected_choices = [ ('', _('Select volume')), ('foo_id', 'foo_vol') ] self.assertEqual(sorted(expected_choices), sorted(volume_choice_field.choices)) @mock.patch.object(fields, 'cinder') def test_update_withoutvolume(self, mock_cinder): foo_vol = mock.Mock() baz_snap = mock.Mock() foo_vol.configure_mock(name='foo_vol', id='foo_id', status='available') baz_snap.configure_mock(name='baz_snap', id='baz_id', status='available') mock_cinder.volume_list.return_value = [foo_vol] mock_cinder.volume_snapshot_list.return_value = [baz_snap] volume_choice_field = fields.VolumeChoiceField(include_volumes=False) volume_choice_field.choices = [] volume_choice_field.update(self.request) expected_choices = [ ('', _('Select volume')), ('baz_id', 'baz_snap') ] self.assertEqual(sorted(expected_choices), sorted(volume_choice_field.choices)) @mock.patch.object(fields, 'exceptions') @mock.patch.object(fields, 'cinder') def test_update_except_snapshot_list_exception(self, mock_cinder, mock_exceptions): foo_vol = mock.Mock() bar_vol = mock.Mock() foo_vol.configure_mock(name='foo_vol', id='foo_id', status='available') bar_vol.configure_mock(name='bar_vol', id='bar_id', status='error') mock_cinder.volume_list.return_value = [foo_vol] mock_cinder.volume_snapshot_list.side_effect = Exception volume_choice_field = fields.VolumeChoiceField(include_volumes=True, include_snapshots=True) volume_choice_field.choices = [] volume_choice_field.update(self.request) expected_choices = [ ('', _('Select volume')), ('foo_id', 'foo_vol') ] self.assertEqual(sorted(expected_choices), sorted(volume_choice_field.choices)) mock_exceptions.handle.assert_called_once_with( self.request['request'], _('Unable to retrieve snapshot list.')) @mock.patch.object(fields, 'exceptions') @mock.patch.object(fields, 'cinder') def test_update_except_volume_list_exception(self, mock_cinder, mock_exceptions): bar_snap = mock.Mock() bar_snap.configure_mock(name='bar_snap', id='bar_id', status='available') mock_cinder.volume_list.side_effect = Exception mock_cinder.volume_snapshot_list.return_value = [bar_snap] volume_choice_field = fields.VolumeChoiceField(include_volumes=True, include_snapshots=True) volume_choice_field.choices = [] volume_choice_field.update(self.request) expected_choices = [ ('', _('Select volume')), ('bar_id', 'bar_snap') ] self.assertEqual(expected_choices, volume_choice_field.choices) mock_exceptions.handle.assert_called_once_with( self.request['request'], _('Unable to retrieve volume list.')) @mock.patch.object(fields, 'exceptions') @mock.patch.object(fields, 'cinder') def test_update_except_exception(self, mock_cinder, mock_exceptions): mock_cinder.volume_list.side_effect = Exception mock_cinder.volume_snapshot_list.side_effect = Exception volume_choice_field = fields.VolumeChoiceField(include_volumes=True, include_snapshots=True) volume_choice_field.choices = [] volume_choice_field.update(self.request) expected_choices = [ ('', _('No volumes available')) ] expected_calls = [ mock.call(self.request['request'], _('Unable to retrieve volume list.')), mock.call(self.request['request'], _('Unable to retrieve snapshot list.')) ] self.assertEqual(expected_choices, volume_choice_field.choices) mock_exceptions.handle.assert_has_calls(expected_calls) class TestAZoneChoiceField(testtools.TestCase): @mock.patch.object(fields, 'nova') def test_update(self, mock_nova): mock_nova.novaclient().availability_zones.list.return_value = [ mock.Mock(zoneName='foo_zone', zoneState='foo_state'), mock.Mock(zoneName='bar_zone', zoneState='bar_state') ] request = {'request': mock.Mock()} a_zone_choice_field = fields.AZoneChoiceField() a_zone_choice_field.choices = [] expected_choices = [ ("bar_zone", "bar_zone"), ("foo_zone", "foo_zone") ] a_zone_choice_field.update(request) self.assertEqual(expected_choices, a_zone_choice_field.choices) @mock.patch.object(fields, 'exceptions') @mock.patch.object(fields, 'nova') def test_update_except_exception(self, mock_nova, mock_exc): mock_nova.novaclient().availability_zones.list.side_effect = Exception request = {'request': mock.Mock()} a_zone_choice_field = fields.AZoneChoiceField() a_zone_choice_field.choices = [] expected_choices = [ ("", _("No availability zones available")) ] a_zone_choice_field.update(request) self.assertEqual(expected_choices, a_zone_choice_field.choices) mock_exc.handle.assert_called_once_with(request['request'], mock.ANY) class TestBooleanField(testtools.TestCase): def test_boolean_field(self): class Widget(object): def __init__(self, attrs): self.attrs = attrs boolean_field = fields.BooleanField(widget=Widget) self.assertIsInstance(boolean_field.widget, Widget) self.assertEqual({'class': 'checkbox'}, boolean_field.widget.attrs) self.assertFalse(boolean_field.required) boolean_field = fields.BooleanField() self.assertIsInstance(boolean_field.widget, forms.CheckboxInput) self.assertEqual({'class': 'checkbox'}, boolean_field.widget.attrs) self.assertFalse(boolean_field.required) class TestDatabaseListField(testtools.TestCase): def setUp(self): super(TestDatabaseListField, self).setUp() self.database_list_field = fields.DatabaseListField() self.addCleanup(mock.patch.stopall) def test_to_python(self): self.assertEqual([], self.database_list_field.to_python(None)) self.assertEqual(['foo', 'bar'], self.database_list_field.to_python('foo ,bar ')) def test_validate(self): valid_value = ['a123', '_123', 'a123_$#@'] result = self.database_list_field.validate(valid_value) self.assertIsNone(result) def test_validate_except_validation_error(self): invalid_value = ['123abc'] expected_error = "First symbol should be latin letter or underscore. "\ "Subsequent symbols can be latin letter, numeric, "\ "underscore, at sign, number sign or dollar sign" e = self.assertRaises(exceptions.ValidationError, self.database_list_field.validate, invalid_value) self.assertEqual(expected_error, e.message) class TestErrorWidget(testtools.TestCase): def test_render(self): error_widget = fields.ErrorWidget() error_widget.message = 'foo_message' result = error_widget.render("'foo_name'", None) self.assertEqual("
    foo_message
    ", result) murano-dashboard-5.0.0/muranodashboard/tests/unit/packages/0000775000175100017510000000000013245511556024045 5ustar zuulzuul00000000000000murano-dashboard-5.0.0/muranodashboard/tests/unit/packages/test_tables.py0000666000175100017510000003554013245511125026731 0ustar zuulzuul00000000000000# Copyright (c) 2016 AT&T 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 mock import testtools from django.utils.translation import ugettext_lazy as _ from muranoclient.common import exceptions as exc from muranodashboard.packages import tables class TestImportPackage(testtools.TestCase): def setUp(self): super(TestImportPackage, self).setUp() self.import_package = tables.ImportPackage() self.assertEqual('upload_package', self.import_package.name) self.assertEqual('Import Package', self.import_package.verbose_name) self.assertEqual('horizon:app-catalog:packages:upload', self.import_package.url) self.assertEqual(('ajax-modal',), self.import_package.classes) self.assertEqual('plus', self.import_package.icon) self.assertEqual((('murano', 'upload_package'),), self.import_package.policy_rules) @mock.patch.object(tables, 'api') def test_allowed(self, mock_api): mock_api.muranoclient().categories.list.return_value = ['foo_cat'] self.assertTrue(self.import_package.allowed(None, None)) mock_api.muranoclient().categories.list.return_value = None self.assertFalse(self.import_package.allowed(None, None)) class TestDownloadPackage(testtools.TestCase): def setUp(self): super(TestDownloadPackage, self).setUp() self.download_package = tables.DownloadPackage() self.assertEqual('download_package', self.download_package.name) self.assertEqual('Download Package', self.download_package.verbose_name) self.assertEqual((('murano', 'download_package'),), self.download_package.policy_rules) self.assertEqual('horizon:app-catalog:packages:download', self.download_package.url) def test_allowed(self): self.assertTrue(self.download_package.allowed(None, None)) @mock.patch.object(tables, 'reverse') def test_get_link_url(self, mock_reverse): mock_reverse.return_value = 'foo_reverse_url' mock_app = mock.Mock(id='foo_app_id') mock_app.configure_mock(name='FOO APP') result = self.download_package.get_link_url(mock_app) self.assertEqual('foo_reverse_url', result) mock_reverse.assert_called_once_with( 'horizon:app-catalog:packages:download', args=('foo-app', 'foo_app_id')) class TestToggleEnabled(testtools.TestCase): def setUp(self): super(TestToggleEnabled, self).setUp() self.mock_request = mock.Mock() self.toggle_enabled = tables.ToggleEnabled() self.toggle_enabled.request = self.mock_request self.assertEqual('toggle_enabled', self.toggle_enabled.name) self.assertEqual('Toggle Enabled', self.toggle_enabled.verbose_name) self.assertEqual('toggle-on', self.toggle_enabled.icon) self.assertEqual((('murano', 'modify_package'),), self.toggle_enabled.policy_rules) def test_action_present(self): self.assertEqual('Toggle Active', tables.ToggleEnabled.action_present(1)) self.assertEqual('Toggle Active', tables.ToggleEnabled.action_present(2)) def test_action_past(self): self.assertEqual('Toggled Active', tables.ToggleEnabled.action_past(1)) self.assertEqual('Toggled Active', tables.ToggleEnabled.action_past(2)) @mock.patch.object(tables, 'LOG') @mock.patch.object(tables, 'api') def test_action(self, mock_api, mock_log): self.toggle_enabled.action(self.mock_request, 'foo_package_id') mock_api.muranoclient().packages.toggle_active.assert_called_once_with( 'foo_package_id') mock_log.debug.assert_called_once_with('Toggle Active for package ' 'foo_package_id.') @mock.patch.object(tables, 'reverse') @mock.patch.object(tables, 'messages') @mock.patch.object(tables, 'exceptions') @mock.patch.object(tables, 'LOG') @mock.patch.object(tables, 'api') def test_action_except_http_forbidden(self, mock_api, mock_log, mock_exc, mock_messages, mock_reverse): mock_api.muranoclient().packages.toggle_active.side_effect = \ exc.HTTPForbidden mock_reverse.return_value = 'foo_reverse_url' expected_msg = _('You are not allowed to perform this operation') self.toggle_enabled.action(self.mock_request, 'foo_package_id') mock_api.muranoclient().packages.toggle_active.assert_called_once_with( 'foo_package_id') mock_log.exception.assert_called_once_with(expected_msg) mock_messages.error.assert_called_once_with(self.mock_request, expected_msg) mock_exc.handle.assert_called_once_with(self.mock_request, expected_msg, redirect='foo_reverse_url') mock_reverse.assert_called_once_with( 'horizon:app-catalog:packages:index') class TestTogglePublicEnabled(testtools.TestCase): def setUp(self): super(TestTogglePublicEnabled, self).setUp() self.mock_request = mock.Mock() self.toggle_public_enabled = tables.TogglePublicEnabled() self.toggle_public_enabled.request = self.mock_request self.assertEqual('toggle_public_enabled', self.toggle_public_enabled.name) self.assertEqual('share-alt', self.toggle_public_enabled.icon) self.assertEqual((('murano', 'publicize_package'),), self.toggle_public_enabled.policy_rules) def test_action_present(self): self.assertEqual('Toggle Public', tables.TogglePublicEnabled.action_present(1)) self.assertEqual('Toggle Public', tables.TogglePublicEnabled.action_present(2)) def test_action_past(self): self.assertEqual('Toggled Public', tables.TogglePublicEnabled.action_past(1)) self.assertEqual('Toggled Public', tables.TogglePublicEnabled.action_past(2)) @mock.patch.object(tables, 'LOG') @mock.patch.object(tables, 'api') def test_action(self, mock_api, mock_log): self.toggle_public_enabled.action(self.mock_request, 'foo_package_id') mock_api.muranoclient().packages.toggle_public.assert_called_once_with( 'foo_package_id') mock_log.debug.assert_called_once_with( 'Toggle Public for package foo_package_id.') @mock.patch.object(tables, 'reverse') @mock.patch.object(tables, 'messages') @mock.patch.object(tables, 'exceptions') @mock.patch.object(tables, 'LOG') @mock.patch.object(tables, 'api') def test_action_except_http_forbidden(self, mock_api, mock_log, mock_exc, mock_messages, mock_reverse): mock_api.muranoclient().packages.toggle_public.side_effect = \ exc.HTTPForbidden mock_reverse.return_value = 'foo_reverse_url' expected_msg = _('You are not allowed to perform this operation') self.toggle_public_enabled.action(self.mock_request, 'foo_package_id') mock_api.muranoclient().packages.toggle_public.assert_called_once_with( 'foo_package_id') mock_log.exception.assert_called_once_with(expected_msg) mock_messages.error.assert_called_once_with(self.mock_request, expected_msg) mock_exc.handle.assert_called_once_with(self.mock_request, expected_msg, redirect='foo_reverse_url') mock_reverse.assert_called_once_with( 'horizon:app-catalog:packages:index') @mock.patch.object(tables, 'reverse') @mock.patch.object(tables, 'messages') @mock.patch.object(tables, 'exceptions') @mock.patch.object(tables, 'LOG') @mock.patch.object(tables, 'api') def test_action_except_http_conflict(self, mock_api, mock_log, mock_exc, mock_messages, mock_reverse): mock_api.muranoclient().packages.toggle_public.side_effect = \ exc.HTTPConflict mock_reverse.return_value = 'foo_reverse_url' expected_msg = _('Package or Class with the same name is already made ' 'public') self.toggle_public_enabled.action(self.mock_request, 'foo_package_id') mock_api.muranoclient().packages.toggle_public.assert_called_once_with( 'foo_package_id') mock_log.exception.assert_called_once_with(expected_msg) mock_messages.error.assert_called_once_with(self.mock_request, expected_msg) mock_exc.handle.assert_called_once_with(self.mock_request, expected_msg, redirect='foo_reverse_url') mock_reverse.assert_called_once_with( 'horizon:app-catalog:packages:index') class TestDeletePackage(testtools.TestCase): def setUp(self): super(TestDeletePackage, self).setUp() self.mock_request = mock.Mock() self.delete_package = tables.DeletePackage() self.delete_package.request = self.mock_request self.assertEqual('delete_package', self.delete_package.name) self.assertEqual((('murano', 'delete_package'),), self.delete_package.policy_rules) def test_action_present(self): self.assertEqual('Delete Package', tables.DeletePackage.action_present(1)) self.assertEqual('Delete Packages', tables.DeletePackage.action_present(2)) def test_action_past(self): self.assertEqual('Deleted Package', tables.DeletePackage.action_past(1)) self.assertEqual('Deleted Packages', tables.DeletePackage.action_past(2)) @mock.patch.object(tables, 'LOG') @mock.patch.object(tables, 'api') def test_delete(self, mock_api, mock_log): self.delete_package.delete(self.mock_request, 'foo_package_id') mock_api.muranoclient().packages.delete.assert_called_once_with( 'foo_package_id') mock_log.exception.assert_not_called() @mock.patch.object(tables, 'reverse') @mock.patch.object(tables, 'exceptions') @mock.patch.object(tables, 'LOG') @mock.patch.object(tables, 'api') def test_delete_except_http_not_found(self, mock_api, mock_log, mock_exc, mock_reverse): mock_api.muranoclient().packages.delete.side_effect = exc.HTTPNotFound mock_reverse.return_value = 'foo_reverse_url' expected_msg = _('Package with id foo_package_id is not found') self.delete_package.delete(self.mock_request, 'foo_package_id') mock_api.muranoclient().packages.delete.assert_called_once_with( 'foo_package_id') mock_log.exception.assert_called_once_with(expected_msg) mock_exc.handle.assert_called_once_with(self.mock_request, expected_msg, redirect='foo_reverse_url') mock_reverse.assert_called_once_with( 'horizon:app-catalog:packages:index') @mock.patch.object(tables, 'reverse') @mock.patch.object(tables, 'exceptions') @mock.patch.object(tables, 'LOG') @mock.patch.object(tables, 'api') def test_delete_except_http_forbidden(self, mock_api, mock_log, mock_exc, mock_reverse): mock_api.muranoclient().packages.delete.side_effect = exc.HTTPForbidden mock_reverse.return_value = 'foo_reverse_url' expected_msg = _('You are not allowed to delete this package') self.delete_package.delete(self.mock_request, 'foo_package_id') mock_api.muranoclient().packages.delete.assert_called_once_with( 'foo_package_id') mock_log.exception.assert_called_once_with(expected_msg) mock_exc.handle.assert_called_once_with(self.mock_request, expected_msg, redirect='foo_reverse_url') mock_reverse.assert_called_once_with( 'horizon:app-catalog:packages:index') @mock.patch.object(tables, 'reverse') @mock.patch.object(tables, 'exceptions') @mock.patch.object(tables, 'LOG') @mock.patch.object(tables, 'api') def test_delete_except_exception(self, mock_api, mock_log, mock_exc, mock_reverse): mock_api.muranoclient().packages.delete.side_effect = Exception mock_reverse.return_value = 'foo_reverse_url' expected_log_msg = _('Unable to delete package in murano-api server') self.delete_package.delete(self.mock_request, 'foo_package_id') mock_api.muranoclient().packages.delete.assert_called_once_with( 'foo_package_id') mock_log.exception.assert_called_once_with(expected_log_msg) mock_exc.handle.assert_called_once_with(self.mock_request, _('Unable to remove package.'), redirect='foo_reverse_url') mock_reverse.assert_called_once_with( 'horizon:app-catalog:packages:index') class TestModifyPackage(testtools.TestCase): def setUp(self): super(TestModifyPackage, self).setUp() self.modify_package = tables.ModifyPackage() self.assertEqual('modify_package', self.modify_package.name) self.assertEqual(_('Modify Package'), self.modify_package.verbose_name) self.assertEqual('horizon:app-catalog:packages:modify', self.modify_package.url) self.assertEqual(('ajax-modal',), self.modify_package.classes) self.assertEqual('edit', self.modify_package.icon) self.assertEqual((('murano', 'modify_package'),), self.modify_package.policy_rules) def test_allowed(self): self.assertTrue(self.modify_package.allowed(None, None)) murano-dashboard-5.0.0/muranodashboard/tests/unit/packages/__init__.py0000666000175100017510000000000013245511125026136 0ustar zuulzuul00000000000000murano-dashboard-5.0.0/muranodashboard/tests/unit/packages/test_views.py0000666000175100017510000015257613245511125026625 0ustar zuulzuul00000000000000# Copyright (c) 2016 AT&T 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. from django.core.files import storage from django import http from django.utils.translation import ugettext_lazy as _ import mock from horizon import exceptions as horizon_exceptions from muranoclient.common import exceptions as exc from muranodashboard.packages import consts as packages_consts from muranodashboard.packages import forms from muranodashboard.packages import tables from muranodashboard.packages import views from openstack_dashboard.test import helpers class TestPackageView(helpers.APITestCase): def setUp(self): super(TestPackageView, self).setUp() fake_response = {'status_code': 200} self.mock_request = mock.Mock(return_value=fake_response) self.addCleanup(mock.patch.stopall) @mock.patch('muranodashboard.packages.views.api.muranoclient') def test_download_package(self, mock_client): mock_client().packages.download.return_value = {} response =\ views.download_packge(self.mock_request, 'test_app_name', None) expected_response = { 'Content-Type': 'application/octet-stream', 'Content-Disposition': 'filename=test_app_name.zip' } self.assertIsInstance(response, http.HttpResponse) for key, val in expected_response.items(): self.assertIn(key.lower(), response._headers) self.assertEqual((key, val), response._headers[key.lower()]) @mock.patch.object(views, 'exceptions') @mock.patch.object(views, 'reverse') @mock.patch.object(views, 'LOG') @mock.patch('muranodashboard.packages.views.api.muranoclient') def test_download_package_except_http_exception(self, mock_client, mock_log, mock_reverse, mock_exc): mock_client().packages.download.side_effect = exc.HTTPException mock_reverse.return_value = 'test_redirect' views.download_packge(self.mock_request, 'test_app_name', None) mock_log.exception.assert_called_once_with( 'Something went wrong during package downloading') mock_reverse.assert_called_once_with( 'horizon:app-catalog:packages:index') mock_exc.handle.assert_called_once_with( self.mock_request, 'Unable to download package.', redirect='test_redirect') def test_is_app(self): mock_wizard = mock.Mock() mock_step_data = mock.MagicMock() mock_step_data.__getitem__().type = 'Application' mock_wizard.storage.get_step_data.return_value = mock_step_data self.assertTrue(views.is_app(mock_wizard)) mock_step_data.__getitem__().type = 'Non-Application' mock_wizard.storage.get_step_data.return_value = mock_step_data self.assertFalse(views.is_app(mock_wizard)) class TestDetailView(helpers.APITestCase): def setUp(self): super(TestDetailView, self).setUp() self.detail_view = views.DetailView() fake_response = {'status_code': 200} self.mock_request = mock.Mock(return_value=fake_response) self.detail_view.request = self.mock_request self.detail_view.kwargs = {'app_id': 'foo'} self.assertEqual('packages/detail.html', self.detail_view.template_name) self.assertEqual('{{ app.name }}', self.detail_view.page_title) self.addCleanup(mock.patch.stopall) @mock.patch('muranodashboard.packages.views.api.muranoclient') def test_get_context_data(self, mock_client): mock_client().packages.get.return_value = 'test_app' context = self.detail_view.get_context_data() self.assertIn('app', context) self.assertIn('view', context) self.assertIsInstance(context['view'], views.DetailView) self.assertEqual('test_app', context['app']) @mock.patch.object(views, 'exceptions') @mock.patch.object(views, 'reverse') @mock.patch('muranodashboard.packages.views.api.muranoclient') def test_get_context_data_except_exception(self, mock_client, mock_reverse, mock_exc): mock_client().packages.get.side_effect = Exception mock_reverse.return_value = 'test_redirect' self.detail_view.get_context_data() mock_exc.handle.assert_called_once_with( self.mock_request, 'Unable to retrieve package details.', redirect='test_redirect') class TestModifyPackageView(helpers.APITestCase): def setUp(self): super(TestModifyPackageView, self).setUp() self.modify_pkg_view = views.ModifyPackageView() fake_response = {'status_code': 200} self.mock_request = mock.Mock(return_value=fake_response, META=[]) self.modify_pkg_view.request = self.mock_request self.modify_pkg_view.kwargs = {'app_id': 'foo'} self.assertEqual(forms.ModifyPackageForm, self.modify_pkg_view.form_class) self.assertEqual('packages/modify_package.html', self.modify_pkg_view.template_name) self.assertEqual('Modify Package', self.modify_pkg_view.page_title) self.addCleanup(mock.patch.stopall) @mock.patch('muranodashboard.packages.views.api.muranoclient') def test_get_initial(self, mock_client): mock_package = mock.Mock() mock_client().packages.get.return_value = mock_package expected_result = { 'package': mock_package, 'app_id': 'foo' } result = self.modify_pkg_view.get_initial() for key, val in expected_result.items(): self.assertEqual(val, result[key]) def test_get_context_data(self): mock_form = mock.Mock(return_value=type( 'FakeForm', (object, ), {'initial': { 'package': type( 'FakeFormInner', (object, ), {'type': 'test_type'} ) }} )) self.modify_pkg_view.get_form = mock_form expected_context = { 'app_id': 'foo', 'type': 'test_type', 'form': mock_form } context = self.modify_pkg_view.get_context_data(form=mock_form) self.assertIn('view', context) self.assertIsInstance(context['view'], views.ModifyPackageView) for key, val in expected_context.items(): self.assertIn(key, context) self.assertEqual(val, context[key]) self.modify_pkg_view.get_form.assert_called_once_with() class TestImportPackageWizard(helpers.APITestCase): def setUp(self): super(TestImportPackageWizard, self).setUp() fake_response = {'status_code': 200} self.mock_request = mock.MagicMock(return_value=fake_response, META=[]) self.import_pkg_wizard = views.ImportPackageWizard() self.import_pkg_wizard.request = self.mock_request all_cleaned_data = { 'enabled': False, 'is_public': True, 'package': 'test_package', 'import_type': 'test_import_type', 'url': 'test_url', 'repo_version': 'test_repo_version', 'repo_name': 'test_repo_name', 'tags': 'foo,bar,baz,qux' } self.import_pkg_wizard.get_all_cleaned_data = lambda: all_cleaned_data self.import_pkg_wizard.steps = mock.Mock(current='upload') self.assertIsInstance(self.import_pkg_wizard.file_storage, storage.FileSystemStorage) self.assertEqual('packages/upload.html', self.import_pkg_wizard.template_name) self.assertEqual({'add_category': views.is_app}, self.import_pkg_wizard.condition_dict) self.assertEqual('Import Package', self.import_pkg_wizard.page_title) self.addCleanup(mock.patch.stopall) def test_get_form_initial(self): self.mock_request.GET = { 'url': 'test_url', 'repo_name': 'test_repo_name', 'repo_version': 'test_repo_version', 'import_type': 'test_import_type' } expected_dict = { 'foo': 'bar', 'url': 'test_url', 'repo_name': 'test_repo_name', 'repo_version': 'test_repo_version', 'import_type': 'test_import_type' } self.import_pkg_wizard.initial_dict = {'upload': {'foo': 'bar'}} initial_dict = self.import_pkg_wizard.get_form_initial('upload') for key, val in expected_dict.items(): self.assertIn(key, initial_dict) self.assertEqual(val, initial_dict[key]) def test_get_context_data(self): mock_form = mock.Mock() self.import_pkg_wizard.storage = mock.Mock( extra_data={'extra': 'data'}) self.import_pkg_wizard.prefix = 'test_prefix' expected_result = { 'form': mock_form, 'modal_backdrop': 'static', 'extra': 'data', 'murano_repo_url': 'http://apps.openstack.org', 'wizard': { 'steps': self.import_pkg_wizard.steps, 'form': mock_form } } result = self.import_pkg_wizard.get_context_data(form=mock_form) self.assertIn('view', result) self.assertIsInstance(result['view'], views.ImportPackageWizard) for key, val in expected_result.items(): if isinstance(val, dict): for key_, val_ in val.items(): self.assertEqual(val_, val[key_]) else: self.assertEqual(val, result[key]) @mock.patch.object(views, 'exceptions') @mock.patch.object(views, 'glance') @mock.patch.object(views, 'reverse') @mock.patch.object(views, 'api') def test_done(self, mock_api, mock_reverse, mock_glance, mock_exc): mock_storage = mock.MagicMock() mock_storage.get_step_data().__getitem__.return_value =\ mock.Mock(id='test_package_id') mock_storage.get_step_data().get.side_effect = [ [mock.Mock(id='test_dep_pkg_id', fully_qualified_name='fqn')], [{'id': 'test_image_id'}] ] self.import_pkg_wizard.storage = mock_storage self.import_pkg_wizard.form_list = {} self.import_pkg_wizard.done({}) expected_api_mock_calls = [ mock.call('test_dep_pkg_id', {'enabled': False, 'is_public': True}), mock.call('test_package_id', {'enabled': False, 'is_public': True, 'tags': ['foo', 'bar', 'baz', 'qux']}), ] mock_api.muranoclient().packages.update.assert_has_calls( expected_api_mock_calls) mock_reverse.assert_called_once_with( 'horizon:app-catalog:packages:index') mock_glance.glanceclient().images.update.assert_called_once_with( mock.ANY, is_public=True) mock_storage.get_step_data.assert_any_call('upload') mock_storage.get_step_data().get.assert_any_call('dependencies', []) mock_storage.get_step_data().get.assert_any_call('images', []) mock_exc.handle.assert_not_called() @mock.patch.object(views, 'reverse') @mock.patch.object(views, 'glance') @mock.patch.object(views, 'LOG') @mock.patch.object(views, 'messages') @mock.patch.object(views, 'api') def test_done_except_murano_client_exception(self, mock_api, mock_messages, mock_log, *args): mock_storage = mock.MagicMock() mock_storage.get_step_data().__getitem__.return_value =\ mock.Mock(id='test_package_id') mock_storage.get_step_data().get.side_effect = [ [mock.Mock(id='test_dep_pkg_id', fully_qualified_name='fqn')], [{'id': 'test_image_id'}] ] mock_api.muranoclient().packages.update.side_effect = [ Exception("murano client error message."), None ] self.import_pkg_wizard.storage = mock_storage self.import_pkg_wizard.form_list = {} self.import_pkg_wizard.done({}) expected_msg = "Couldn't update package {0} parameters. Error: {1}"\ .format('fqn', 'murano client error message.') mock_log.warning.assert_called_once_with(expected_msg) mock_messages.warning.assert_called_once_with( self.mock_request, expected_msg) @mock.patch.object(views, 'api') @mock.patch.object(views, 'reverse') @mock.patch.object(views, 'glance') @mock.patch.object(views, 'LOG') @mock.patch.object(views, 'messages') def test_done_except_glance_init_exception(self, mock_messages, mock_log, mock_glance, *args): """Test null glance client with installed images throws exception.""" mock_storage = mock.MagicMock() mock_storage.get_step_data().__getitem__.return_value =\ mock.Mock(id='test_package_id') mock_storage.get_step_data().get.side_effect = [ [mock.Mock(id='test_dep_pkg_id', fully_qualified_name='fqn')], [{'id': 'foo_image_id', 'name': 'foo_image_name'}, {'id': 'bar_image_id', 'name': 'bar_image_name'}] ] mock_glance.glanceclient.return_value = None self.import_pkg_wizard.storage = mock_storage self.import_pkg_wizard.form_list = {} self.import_pkg_wizard.done({}) expected_msg = "Couldn't initialise glance v1 client, therefore "\ "could not make the following images public: {0}"\ .format('foo_image_name bar_image_name') mock_log.warning.assert_called_once_with(expected_msg) mock_messages.warning.assert_called_once_with( self.mock_request, expected_msg) @mock.patch.object(views, 'api') @mock.patch.object(views, 'reverse') @mock.patch.object(views, 'glance') @mock.patch.object(views, 'LOG') @mock.patch.object(views, 'messages') def test_done_except_glance_update_exception(self, mock_messages, mock_log, mock_glance, *args): mock_storage = mock.MagicMock() mock_storage.get_step_data().__getitem__.return_value =\ mock.Mock(id='test_package_id') mock_storage.get_step_data().get.side_effect = [ [mock.Mock(id='test_dep_pkg_id', fully_qualified_name='fqn')], [{'id': 'test_image_id', 'name': 'test_image_name'}] ] glance_exception = Exception("glance client error message") mock_glance.glanceclient().images.update.side_effect =\ glance_exception self.import_pkg_wizard.storage = mock_storage self.import_pkg_wizard.form_list = {} self.import_pkg_wizard.done({}) expected_msg = "Error {0} occurred while setting image {1}, {2} "\ "public".format(glance_exception, "test_image_name", "test_image_id") mock_log.exception.assert_called_once_with(expected_msg) mock_messages.error.assert_called_once_with( self.mock_request, expected_msg) @mock.patch.object(views, 'glance') @mock.patch.object(views, 'reverse') @mock.patch.object(views, 'LOG') @mock.patch.object(views, 'exceptions') @mock.patch.object(views, 'api') def test_done_except_http_forbidden(self, mock_api, mock_exc, mock_log, mock_reverse, _): mock_storage = mock.MagicMock() mock_storage.get_step_data().__getitem__.return_value =\ mock.Mock(id='test_package_id') mock_storage.get_step_data().get.side_effect = [ [mock.Mock(id='test_dep_pkg_id', fully_qualified_name='fqn')], [{'id': 'test_image_id', 'name': 'test_image_name'}] ] mock_api.muranoclient().packages.update.side_effect = [ None, exc.HTTPForbidden ] mock_reverse.return_value = 'test_redirect' self.import_pkg_wizard.storage = mock_storage self.import_pkg_wizard.form_list = {} self.import_pkg_wizard.done({}) expected_msg = "You are not allowed to change this properties of the "\ "package" mock_log.exception.assert_called_once_with(expected_msg) mock_exc.handle.assert_called_once_with( self.mock_request, expected_msg, redirect='test_redirect') @mock.patch.object(views, 'glance') @mock.patch.object(views, 'reverse') @mock.patch.object(views, 'LOG') @mock.patch.object(views, 'exceptions') @mock.patch.object(views, 'api') def test_done_except_http_exception(self, mock_api, mock_exc, mock_log, mock_reverse, _): mock_storage = mock.MagicMock() mock_storage.get_step_data().__getitem__.return_value =\ mock.Mock(id='test_package_id') mock_storage.get_step_data().get.side_effect = [ [mock.Mock(id='test_dep_pkg_id', fully_qualified_name='fqn')], [{'id': 'test_image_id', 'name': 'test_image_name'}] ] mock_api.muranoclient().packages.update.side_effect = [ None, exc.HTTPException ] mock_reverse.return_value = 'test_redirect' self.import_pkg_wizard.storage = mock_storage self.import_pkg_wizard.form_list = {} self.import_pkg_wizard.done({}) expected_msg = 'Modifying package failed' mock_log.exception.assert_called_once_with(expected_msg) mock_exc.handle.assert_called_once_with( self.mock_request, 'Unable to modify package', redirect='test_redirect') @mock.patch.object(views, 'reverse') @mock.patch.object(views, 'exceptions') @mock.patch.object(views, 'LOG') def test_handle_exception(self, mock_log, mock_exc, mock_reverse): mock_exception = mock.Mock() mock_exception.details = '{"error": {"message": "test_error_message"}}' mock_reverse.return_value = 'test_redirect' self.import_pkg_wizard.request = self.mock_request self.import_pkg_wizard._handle_exception(mock_exception) expected_msg = 'Uploading package failed. {0}'\ .format('test_error_message') mock_log.exception.assert_called_once_with(expected_msg) mock_exc.handle.assert_called_once_with( self.mock_request, expected_msg, redirect='test_redirect') mock_reverse.assert_called_once_with( 'horizon:app-catalog:packages:index') @mock.patch.object(views, 'json') def test_handle_exception_except_value_error(self, mock_json): mock_json.loads.side_effect = ValueError('test_error_message') original_e = ValueError('original_error_message') setattr(original_e, 'details', 'error_details') with self.assertRaisesRegexp(ValueError, 'original_error_message'): self.import_pkg_wizard._handle_exception(original_e) @mock.patch.object(views, 'glance') @mock.patch.object(views, 'messages') @mock.patch.object(views, 'api') @mock.patch.object(views, 'muranoclient_utils') def test_process_step(self, mock_murano_utils, mock_api, mock_messages, _): mock_package = mock.Mock() mock_package.manifest = {'FullName': 'foo'} mock_original_package = mock.Mock() mock_original_package.file.return_value = 'foo_file' mock_package.requirements.return_value =\ {'foo': mock_original_package, 'bar': mock.Mock()} mock_murano_utils.ensure_images.return_value = [ {'id': 1, 'name': 'foo'}, {'id': 2, 'name': 'bar'} ] mock_murano_utils.Package.from_file.return_value = mock_package mock_dependency_package = mock.Mock() mock_result_package = mock.Mock(id='result_package_id') mock_api.muranoclient().packages.create.side_effect = [ mock_dependency_package, mock_result_package ] mock_form = mock.Mock() mock_form.cleaned_data = { 'import_type': 'upload', 'package': mock.Mock(file='test_package_file') } mock_form.data = { 'dependencies': ['dep1', 'dep2'], 'images': ['img1', 'img2'] } step_data = self.import_pkg_wizard.process_step(mock_form) for expected_key in ('images', 'dependencies', 'package'): self.assertIn(expected_key, step_data) self.assertIn({'id': 1, 'name': 'foo'}, step_data['images']) self.assertIn({'id': 2, 'name': 'bar'}, step_data['images']) self.assertEqual([mock_dependency_package], step_data['dependencies']) self.assertEqual(mock_result_package, step_data['package']) self.assertEqual(2, mock_murano_utils.ensure_images.call_count) mock_murano_utils.Package.from_file.assert_called_once_with( 'test_package_file') mock_api.muranoclient().packages.create.assert_any_call( mock.ANY, {'foo': 'foo_file'}) mock_package.requirements.assert_called_once_with( base_url=packages_consts.MURANO_REPO_URL) mock_messages.success.assert_any_call( self.mock_request, 'Package bar uploaded') mock_messages.success.assert_any_call( self.mock_request, 'Package foo uploaded') @mock.patch.object(views, 'reverse') @mock.patch.object(views, 'LOG') @mock.patch.object(views, 'muranoclient_utils') def test_process_step_except_from_file_exception( self, mock_murano_utils, mock_log, mock_reverse): mock_reverse.return_value = 'test_redirect' mock_form = mock.Mock() mock_form.cleaned_data = { 'import_type': 'upload', 'package': mock.Mock(file='test_package_file') } errors = (('(404) test_404_error', "Package creation failed." "Reason: Can't find Package name " "from repository."), ('random error message', "Package creation failed.Reason: " "random error message")) for error_message, expected_error_message in errors: exception = Exception(error_message) exception.message = error_message mock_murano_utils.Package.from_file.side_effect = exception with self.assertRaisesRegexp(horizon_exceptions.Http302, None): self.import_pkg_wizard.process_step(mock_form) mock_log.exception.assert_called_once_with(expected_error_message) mock_reverse.assert_called_once_with( 'horizon:app-catalog:packages:index') mock_log.exception.reset_mock() mock_reverse.reset_mock() @mock.patch.object(views, 'exceptions') @mock.patch.object(views, 'messages') @mock.patch.object(views, 'reverse') @mock.patch.object(views, 'LOG') @mock.patch.object(views, 'api') @mock.patch.object(views, 'muranoclient_utils') def test_process_step_except_package_create_exception( self, mock_murano_utils, mock_api, mock_log, mock_reverse, mock_messages, mock_exc): mock_murano_utils.Package.from_file.side_effect = None mock_reverse.return_value = 'test_redirect' mock_package = mock.Mock() mock_package.manifest = {'FullName': 'foo'} mock_murano_utils.Package.from_file.return_value = mock_package mock_form = mock.Mock() mock_form.cleaned_data = { 'import_type': 'upload', 'package': mock.Mock(file='test_package_file') } mock_form.data = { 'dependencies': [], 'images': [] } # Test that first occurrence of exception is handled. mock_package.requirements.return_value =\ {'foo': mock.Mock(), 'bar': mock.Mock()} expected_error_message = _( "Error test_error_message occurred while installing package bar") mock_api.muranoclient().packages.create.side_effect = [ Exception('test_error_message'), mock.Mock(id='test_package_id') ] self.import_pkg_wizard.process_step(mock_form) mock_messages.error.assert_any_call( self.mock_request, expected_error_message) mock_messages.success.assert_any_call( self.mock_request, _('Package foo uploaded')) mock_log.exception.assert_called_once_with(expected_error_message) mock_log.exception.reset_mock() mock_messages.reset_mock() # Test that second occurrence of exception is handled. mock_package.requirements.return_value =\ {'foo': mock.Mock(), 'bar': mock.Mock()} expected_error_message = 'Uploading package failed. {0}'.format('') mock_api.muranoclient().packages.create.side_effect = [ mock.Mock(id='test_package_id'), Exception ] self.import_pkg_wizard.process_step(mock_form) mock_log.exception.assert_called_once_with(expected_error_message) mock_exc.handle.assert_called_once_with( self.mock_request, expected_error_message, redirect='test_redirect') mock_reverse.assert_called_once_with( 'horizon:app-catalog:packages:index') @mock.patch.object(views, 'exceptions') @mock.patch.object(views, 'messages') @mock.patch.object(views, 'reverse') @mock.patch.object(views, 'LOG') @mock.patch.object(views, 'api') @mock.patch.object(views, 'muranoclient_utils') def test_process_step_except_http_conflict( self, mock_murano_utils, mock_api, mock_log, mock_reverse, mock_messages, mock_exc): mock_murano_utils.Package.from_file.side_effect = None mock_reverse.return_value = 'test_redirect' mock_form = mock.Mock() mock_form.cleaned_data = { 'import_type': 'upload', 'package': mock.Mock(file='test_package_file') } mock_form.data = { 'dependencies': [], 'images': [] } # Test that first occurrence of HTTPConflict is caught. mock_package = mock.Mock() mock_package.manifest = {'FullName': 'foo'} mock_package.requirements.return_value =\ {'foo': mock.Mock(), 'bar': mock.Mock()} mock_murano_utils.Package.from_file.return_value = mock_package expected_error_message = "Package bar already registered." mock_api.muranoclient().packages.create.side_effect = [ exc.HTTPConflict, None ] self.import_pkg_wizard.process_step(mock_form) mock_log.exception.assert_any_call(expected_error_message) mock_messages.warning.assert_called_once_with( self.mock_request, expected_error_message) mock_log.exception.reset_mock() mock_messages.warning.reset_mock() # Test that second occurrence of HTTPConflict is caught. mock_package.requirements.return_value =\ {'foo': mock.Mock(), 'bar': mock.Mock()} mock_murano_utils.Package.from_file.return_value = mock_package expected_error_message = _( "Package with specified name already exists") mock_api.muranoclient().packages.create.side_effect = [ None, exc.HTTPConflict ] self.import_pkg_wizard.process_step(mock_form) mock_log.exception.assert_any_call(expected_error_message) mock_exc.handle.assert_any_call( self.mock_request, expected_error_message, redirect='test_redirect') @mock.patch.object(views, 'exceptions') @mock.patch.object(views, 'reverse') @mock.patch.object(views, 'LOG') @mock.patch.object(views, 'api') @mock.patch.object(views, 'muranoclient_utils') def test_process_step_except_http_internal_server_error( self, mock_murano_utils, mock_api, mock_log, mock_reverse, mock_exc): mock_murano_utils.Package.from_file.side_effect = None mock_reverse.return_value = 'test_redirect' mock_package = mock.Mock() mock_package.manifest = {'FullName': 'foo'} mock_package.requirements.return_value =\ {'foo': mock.Mock(), 'bar': mock.Mock()} mock_murano_utils.Package.from_file.return_value = mock_package mock_form = mock.Mock() mock_form.cleaned_data = { 'import_type': 'upload', 'package': mock.Mock(file='test_package_file') } mock_form.data = { 'dependencies': [], 'images': [] } expected_error_message = "Uploading package failed. {0}"\ .format('test_500_error_message') exception = exc.HTTPInternalServerError( details='{"error": {"message": "test_500_error_message"}}') mock_api.muranoclient().packages.create.side_effect = [ mock.Mock(id='test_package_id'), exception ] self.import_pkg_wizard.process_step(mock_form) mock_log.exception.assert_called_once_with(expected_error_message) mock_exc.handle.assert_called_once_with( self.mock_request, expected_error_message, redirect='test_redirect') mock_reverse.assert_called_once_with( 'horizon:app-catalog:packages:index') @mock.patch.object(views, 'exceptions') @mock.patch.object(views, 'reverse') @mock.patch.object(views, 'muranodashboard_utils') @mock.patch.object(views, 'LOG') @mock.patch.object(views, 'api') @mock.patch.object(views, 'muranoclient_utils') def test_process_step_except_http_exception( self, mock_murano_utils, mock_api, mock_log, mock_dashboard_utils, mock_reverse, mock_exc): mock_murano_utils.Package.from_file.side_effect = None mock_reverse.return_value = 'test_redirect' mock_package = mock.Mock() mock_package.manifest = {'FullName': 'foo'} mock_package.requirements.return_value =\ {'foo': mock.Mock(), 'bar': mock.Mock()} mock_murano_utils.Package.from_file.return_value = mock_package mock_dashboard_utils.parse_api_error.return_value = 'test_reason' mock_form = mock.Mock() mock_form.cleaned_data = { 'import_type': 'upload', 'package': mock.Mock(file='test_package_file') } mock_form.data = { 'dependencies': [], 'images': [] } exception = exc.HTTPException( details='{"error": {"message": "test_error_message"}}') mock_api.muranoclient().packages.create.side_effect = [ mock.Mock(id='test_package_id'), exception ] self.import_pkg_wizard.process_step(mock_form) mock_log.exception.assert_any_call('test_reason') mock_exc.handle.assert_called_once_with( self.mock_request, 'test_reason', redirect='test_redirect') mock_reverse.assert_called_once_with( 'horizon:app-catalog:packages:index') def test_get_form_kwargs(self): kwargs = self.import_pkg_wizard.get_form_kwargs('add_category') self.assertEqual({'request': self.mock_request}, kwargs) mock_storage = mock.Mock() mock_storage.get_step_data().get.return_value = 'test_package' self.import_pkg_wizard.storage = mock_storage kwargs = self.import_pkg_wizard.get_form_kwargs('modify') self.assertEqual({'request': self.mock_request, 'package': 'test_package'}, kwargs) class TestImportBundleWizard(helpers.APITestCase): def setUp(self): super(TestImportBundleWizard, self).setUp() fake_response = {'status_code': 200} self.mock_request = mock.MagicMock( name='mock_request', return_value=fake_response, META=[]) self.import_bundle_wizard = views.ImportBundleWizard() self.import_bundle_wizard.request = self.mock_request self.import_bundle_wizard.storage = mock.Mock( name='mock_storage', extra_data={'foo': 'bar'}) self.import_bundle_wizard.steps = mock.Mock(name='mock_steps') self.import_bundle_wizard.steps.current = 'upload' self.import_bundle_wizard.prefix = 'test_prefix' self.import_bundle_wizard.initial_dict = {'upload': {'foo': 'bar'}} self.import_bundle_wizard.get_form_step_data =\ lambda f: 'test_step_form_data' self.assertEqual('packages/import_bundle.html', self.import_bundle_wizard.template_name) self.assertEqual('Import Bundle', self.import_bundle_wizard.page_title) self.addCleanup(mock.patch.stopall) def test_get_context_data(self): mock_form = mock.Mock(initial={'package': mock.Mock(type='test_type')}) context = self.import_bundle_wizard.get_context_data(form=mock_form) expected_instances = { 'view': views.ImportBundleWizard, 'wizard': dict } expected_context = { 'foo': 'bar', 'form': mock.ANY, 'hide': True, 'modal_backdrop': 'static', 'murano_repo_url': 'http://apps.openstack.org', 'view': mock.ANY, 'wizard': mock.ANY } for key, val in expected_context.items(): self.assertIn(key, context) self.assertEqual(val, context[key]) for key, val in expected_instances.items(): self.assertIsInstance(context[key], val) def test_get_form_initial(self): self.import_bundle_wizard.request.GET = { 'url': 'test_url', 'name': 'test_name', 'import_type': 'test_import_type' } initial_dict = self.import_bundle_wizard.get_form_initial('upload') expected_dict = { 'foo': 'bar', 'url': 'test_url', 'name': 'test_name', 'import_type': 'test_import_type' } for key, val in expected_dict.items(): self.assertIn(key, initial_dict) self.assertEqual(val, initial_dict[key]) @mock.patch.object(views, 'api') @mock.patch.object(views, 'muranoclient_utils') def test_process_step(self, mock_murano_utils, mock_api): mock_bundle = mock.Mock() mock_bundle.package_specs.return_value = [ {'Name': 'foo_spec', 'Version': '1.0.0', 'Url': 'www.foo.com'}, {'Name': 'bar_spec', 'Version': '2.0.0', 'Url': 'www.bar.com'} ] mock_package = mock.Mock() mock_foo_dependency = mock.Mock(name='foo_package') mock_bar_dependency = mock.Mock(name='bar_package') mock_package.requirements.return_value = { 'foo_package': mock_foo_dependency, 'bar_package': mock_bar_dependency } mock_murano_utils.to_url.return_value = 'test_url' mock_form = mock.Mock() for import_type in ('by_url', 'by_name'): mock_form.cleaned_data = { 'import_type': import_type, 'url': 'test_url', 'name': 'test_form_name' } mock_murano_utils.Bundle.from_file.return_value = mock_bundle mock_murano_utils.Package.from_location.return_value = mock_package step_data = self.import_bundle_wizard.process_step(mock_form) self.assertEqual('test_step_form_data', step_data) mock_murano_utils.Bundle.from_file.assert_called_once_with( 'test_url') mock_murano_utils.Package.from_location.assert_any_call( 'foo_spec', version='1.0.0', url='www.foo.com', base_url=packages_consts.MURANO_REPO_URL, path=None ) mock_murano_utils.Package.from_location.assert_any_call( 'bar_spec', version='2.0.0', url='www.bar.com', base_url=packages_consts.MURANO_REPO_URL, path=None ) mock_api.muranoclient().packages.create.assert_any_call( {}, {'foo_package': mock_foo_dependency.file()}) mock_api.muranoclient().packages.create.assert_any_call( {}, {'bar_package': mock_bar_dependency.file()}) mock_murano_utils.reset_mock() mock_api.reset_mock() @mock.patch.object(views, 'reverse') @mock.patch.object(views, 'messages') @mock.patch.object(views, 'LOG') @mock.patch.object(views, 'muranoclient_utils') def test_process_step_except_from_file_exception( self, mock_murano_utils, mock_log, mock_messages, mock_reverse): mock_reverse.return_value = 'test_redirect' mock_form = mock.Mock( cleaned_data={'import_type': 'by_url', 'url': 'foo_url'}) e_404 = Exception('(404)') e_404.message = '(404)' e = Exception('foo') e.message = 'foo' errors = ((e_404, "Bundle creation failed.Reason: Can't find Bundle " "name from repository."), (e, "Bundle creation failed.Reason: foo")) for exception, expected_error_message in errors: mock_murano_utils.Bundle.from_file.side_effect = exception with self.assertRaisesRegexp(horizon_exceptions.Http302, None): self.import_bundle_wizard.process_step(mock_form) mock_log.exception.assert_called_once_with(expected_error_message) mock_messages.error.assert_called_once_with( self.mock_request, expected_error_message) mock_reverse.assert_called_once_with( 'horizon:app-catalog:packages:index') for mock_ in (mock_log, mock_messages, mock_reverse): mock_.reset_mock() @mock.patch.object(views, 'messages') @mock.patch.object(views, 'LOG') @mock.patch.object(views, 'muranoclient_utils') def test_process_step_except_from_location_exception( self, mock_murano_utils, mock_log, mock_messages): mock_form = mock.Mock( cleaned_data={'import_type': 'by_url', 'url': 'foo_url'}) mock_bundle = mock.Mock() mock_bundle.package_specs.return_value = [ {'Name': 'foo_spec', 'Version': '1.0.0', 'Url': 'www.foo.com'}, {'Name': 'bar_spec', 'Version': '2.0.0', 'Url': 'www.bar.com'} ] mock_murano_utils.Bundle.from_file.return_value = mock_bundle mock_murano_utils.Package.from_location.side_effect = Exception('foo') self.import_bundle_wizard.process_step(mock_form) for spec in ('foo_spec', 'bar_spec'): expected_error_message = 'Error foo occurred while parsing '\ 'package {0}'.format(spec) mock_log.exception.assert_any_call(expected_error_message) mock_messages.error.assert_any_call( self.mock_request, expected_error_message) @mock.patch.object(views, 'messages') @mock.patch.object(views, 'LOG') @mock.patch.object(views, 'api') @mock.patch.object(views, 'muranoclient_utils') def test_process_step_except_http_conflict( self, mock_murano_utils, mock_api, mock_log, mock_messages): mock_form = mock.Mock( cleaned_data={'import_type': 'by_url', 'url': 'foo_url'}) mock_bundle = mock.Mock() mock_bundle.package_specs.return_value = [ {'Name': 'foo_spec', 'Version': '1.0.0', 'Url': 'www.foo.com'}, {'Name': 'bar_spec', 'Version': '2.0.0', 'Url': 'www.bar.com'} ] mock_foo_dependency = mock.Mock(name='foo_package') mock_bar_dependency = mock.Mock(name='bar_package') mock_package = mock.Mock() mock_package.requirements.return_value = { 'foo_package': mock_foo_dependency, 'bar_package': mock_bar_dependency } mock_murano_utils.Bundle.from_file.return_value = mock_bundle mock_murano_utils.Package.from_location.return_value = mock_package mock_api.muranoclient().packages.create.side_effect = exc.HTTPConflict self.import_bundle_wizard.process_step(mock_form) for dep in ('foo_package', 'bar_package'): expected_error_message = 'Package {0} already registered.'\ .format(dep) mock_log.exception.assert_any_call(expected_error_message) mock_messages.warning.assert_any_call( self.mock_request, expected_error_message) @mock.patch.object(views, 'messages') @mock.patch.object(views, 'LOG') @mock.patch.object(views, 'api') @mock.patch.object(views, 'muranodashboard_utils') @mock.patch.object(views, 'muranoclient_utils') def test_process_step_except_http_exception( self, mock_murano_utils, mock_dashboard_utils, mock_api, mock_log, mock_messages): mock_form = mock.Mock( cleaned_data={'import_type': 'by_url', 'url': 'foo_url'}) mock_bundle = mock.Mock() mock_bundle.package_specs.return_value = [ {'Name': 'foo_spec', 'Version': '1.0.0', 'Url': 'www.foo.com'}, {'Name': 'bar_spec', 'Version': '2.0.0', 'Url': 'www.bar.com'} ] mock_foo_dependency = mock.Mock(name='foo_package') mock_bar_dependency = mock.Mock(name='bar_package') mock_package = mock.Mock() mock_package.requirements.return_value = { 'foo_package': mock_foo_dependency, 'bar_package': mock_bar_dependency } mock_murano_utils.Bundle.from_file.return_value = mock_bundle mock_murano_utils.Package.from_location.return_value = mock_package mock_api.muranoclient().packages.create.side_effect =\ exc.HTTPException('foo') mock_dashboard_utils.parse_api_error.return_value = 'foo' self.import_bundle_wizard.process_step(mock_form) for dep in ('foo_package', 'bar_package'): expected_error_message = 'Package {0} upload failed. foo'\ .format(dep) mock_log.exception.assert_any_call(expected_error_message) mock_messages.warning.assert_any_call( self.mock_request, expected_error_message) @mock.patch.object(views, 'messages') @mock.patch.object(views, 'LOG') @mock.patch.object(views, 'api') @mock.patch.object(views, 'muranoclient_utils') def test_process_step_except_package_create_exception( self, mock_murano_utils, mock_api, mock_log, mock_messages): mock_form = mock.Mock( cleaned_data={'import_type': 'by_url', 'url': 'foo_url'}) mock_bundle = mock.Mock() mock_bundle.package_specs.return_value = [ {'Name': 'foo_spec', 'Version': '1.0.0', 'Url': 'www.foo.com'}, {'Name': 'bar_spec', 'Version': '2.0.0', 'Url': 'www.bar.com'} ] mock_foo_dependency = mock.Mock(name='foo_package') mock_bar_dependency = mock.Mock(name='bar_package') mock_package = mock.Mock() mock_package.requirements.return_value = { 'foo_package': mock_foo_dependency, 'bar_package': mock_bar_dependency } mock_murano_utils.Bundle.from_file.return_value = mock_bundle mock_murano_utils.Package.from_location.return_value = mock_package mock_api.muranoclient().packages.create.side_effect =\ Exception('foo') self.import_bundle_wizard.process_step(mock_form) for dep in ('foo_package', 'bar_package'): expected_error_message = 'Importing package {0} failed. '\ 'Reason: foo'.format(dep) mock_log.exception.assert_any_call(expected_error_message) mock_messages.warning.assert_any_call( self.mock_request, expected_error_message) @mock.patch.object(views, 'messages') @mock.patch.object(views, 'LOG') @mock.patch.object(views, 'reverse') def test_done(self, mock_reverse, mock_log, mock_messages): mock_reverse.redirect = 'test_redirect' result = self.import_bundle_wizard.done([]) self.assertIsInstance(result, http.response.HttpResponseRedirect) expected_message = 'Bundle successfully imported.' mock_log.info.assert_any_call(expected_message) mock_messages.success.assert_called_once_with( self.mock_request, expected_message) mock_reverse.assert_called_once_with( 'horizon:app-catalog:packages:index') class TestPackageDefinitionsView(helpers.APITestCase): def setUp(self): super(TestPackageDefinitionsView, self).setUp() self.pkg_definitions_view = views.PackageDefinitionsView() mock_token = mock.MagicMock() mock_token.__getitem__.return_value = 'foo_token_id' self.mock_request = mock.MagicMock( name='mock_request', GET={'sort_dir': 'asc'}, session={'token': mock_token}) self.pkg_definitions_view.request = self.mock_request self.original_get_filters = self.pkg_definitions_view.get_filters self.pkg_definitions_view.get_filters = lambda opts: opts self.assertEqual(self.pkg_definitions_view.table_class, tables.PackageDefinitionsTable) self.assertEqual(self.pkg_definitions_view.template_name, 'packages/index.html') self.assertEqual(self.pkg_definitions_view.page_title, 'Packages') self.assertFalse(self.pkg_definitions_view.has_more_data(None)) self.assertFalse(self.pkg_definitions_view.has_prev_data(None)) mock_horizon_utils = mock.patch.object(views, 'utils').start() mock_horizon_utils.get_page_size.return_value = 123 self.addCleanup(mock.patch.stopall) @mock.patch.object(views, 'pkg_api') def test_get_data_with_same_tenants(self, mock_pkg_api): mock_package = mock.Mock( id='foo_package', owner_id='test_tenant', tenant_name=None) mock_pkg_api.package_list.return_value =\ ([mock_package], True) mock_tenant = mock.MagicMock() mock_tenant.__getitem__.side_effect = [ 'test_tenant', 'foo_tenant_name' ] self.pkg_definitions_view.request.user.is_superuser = False self.pkg_definitions_view.request.session['token'] =\ mock.Mock(tenant=mock_tenant) packages = self.pkg_definitions_view.get_data() self.assertEqual([mock_package], packages) self.assertEqual('foo_tenant_name', mock_package.tenant_name) self.assertTrue(self.pkg_definitions_view.has_prev_data) mock_pkg_api.package_list.assert_any_call( self.mock_request, marker=None, filters={'include_disabled': True, 'sort_dir': 'desc'}, paginate=True, page_size=123) mock_pkg_api.package_list.assert_any_call( self.mock_request, marker='foo_package', filters={'include_disabled': True, 'sort_dir': 'desc'}, paginate=True, page_size=0) @mock.patch.object(views, 'pkg_api') def test_get_data_with_different_tenants(self, mock_pkg_api): mock_package = mock.Mock(id='foo_package', tenant_name='test_tenant') mock_pkg_api.package_list.return_value =\ ([mock_package], True) self.pkg_definitions_view.request.user.is_superuser = False self.pkg_definitions_view.request.session['token'] =\ mock.MagicMock(__getitem__='alt_test_tenant') packages = self.pkg_definitions_view.get_data() self.assertEqual([mock_package], packages) self.assertEqual('UNKNOWN', mock_package.tenant_name) self.assertTrue(self.pkg_definitions_view.has_prev_data) mock_pkg_api.package_list.assert_any_call( self.mock_request, marker=None, filters={'include_disabled': True, 'sort_dir': 'desc'}, paginate=True, page_size=123) mock_pkg_api.package_list.assert_any_call( self.mock_request, marker='foo_package', filters={'include_disabled': True, 'sort_dir': 'desc'}, paginate=True, page_size=0) @mock.patch.object(views, 'keystone') @mock.patch.object(views, 'pkg_api') def test_get_data_as_superuser(self, mock_pkg_api, mock_keystone): mock_keystone.tenant_list.side_effect = None super_user_tenant = mock.Mock(id='super_tenant_id') super_user_tenant.configure_mock(name='super_tenant_name') mock_keystone.tenant_list.return_value = ([super_user_tenant], False) mock_package = mock.Mock( id='foo_package', owner_id='super_tenant_id', tenant_name=None) mock_pkg_api.package_list.return_value =\ ([mock_package], True) mock_tenant = mock.MagicMock() mock_tenant.__getitem__.side_effect = [ 'test_tenant', 'foo_tenant_name' ] self.pkg_definitions_view.request.user.is_superuser = True self.pkg_definitions_view.request.session['token'] =\ mock.Mock(tenant=mock_tenant) packages = self.pkg_definitions_view.get_data() self.assertEqual([mock_package], packages) self.assertEqual('super_tenant_name', mock_package.tenant_name) self.assertTrue(self.pkg_definitions_view.has_more_data) @mock.patch.object(views, 'exceptions') @mock.patch.object(views, 'keystone') @mock.patch.object(views, 'pkg_api') def test_get_data_except_keystone_exception(self, mock_pkg_api, mock_keystone, mock_exc): mock_keystone.tenant_list.side_effect = Exception super_user_tenant = mock.Mock(id='super_tenant_id') super_user_tenant.configure_mock(name='super_tenant_name') mock_keystone.tenant_list.return_value = ([super_user_tenant], False) mock_package = mock.Mock( id='foo_package', owner_id='super_tenant_id', tenant_name=None) mock_pkg_api.package_list.return_value =\ ([mock_package], True) mock_tenant = mock.MagicMock() mock_tenant.__getitem__.side_effect = [ 'test_tenant', 'foo_tenant_name' ] self.pkg_definitions_view.request.user.is_superuser = True self.pkg_definitions_view.request.session['token'] =\ mock.Mock(tenant=mock_tenant) packages = self.pkg_definitions_view.get_data() self.assertEqual([mock_package], packages) self.assertIsNone(mock_package.tenant_name) self.assertTrue(self.pkg_definitions_view.has_more_data) mock_exc.handle.assert_called_once_with( self.mock_request, "Unable to retrieve project list.") @mock.patch.object(views, 'pkg_api') def test_get_data_desc_order(self, mock_pkg_api): mock_package = mock.Mock( id='foo_package', owner_id='test_tenant', tenant_name=None) mock_pkg_api.package_list.return_value =\ ([mock_package], True) mock_tenant = mock.MagicMock() mock_tenant.__getitem__.side_effect = [ 'test_tenant', 'foo_tenant_name' ] self.pkg_definitions_view.request.GET = {'sort_dir': 'desc'} self.pkg_definitions_view.request.user.is_superuser = False self.pkg_definitions_view.request.session['token'] =\ mock.Mock(tenant=mock_tenant) packages = self.pkg_definitions_view.get_data() self.assertEqual([mock_package], packages) self.assertEqual('foo_tenant_name', mock_package.tenant_name) self.assertTrue(self.pkg_definitions_view.has_more_data) mock_pkg_api.package_list.assert_any_call( self.mock_request, marker=None, filters={'include_disabled': True, 'sort_dir': 'asc'}, paginate=True, page_size=123) mock_pkg_api.package_list.assert_any_call( self.mock_request, marker='foo_package', filters={'include_disabled': True, 'sort_dir': 'asc'}, paginate=True, page_size=0) def test_get_context_data(self): mock_form = mock.Mock(initial={'package': mock.Mock(type='test_type')}) self.pkg_definitions_view.table = 'foo_table' context = self.pkg_definitions_view.get_context_data(form=mock_form) expected_context = { 'table': 'foo_table', 'tenant_id': mock.ANY, 'packages_table': 'foo_table', 'form': mock.ANY, 'view': mock.ANY } for key, val in expected_context.items(): self.assertEqual(val, context[key]) self.assertIsInstance(context['view'], views.PackageDefinitionsView) def test_get_filters(self): mock_filter_action = mock.Mock() mock_filter_action.is_api_filter.return_value = True self.pkg_definitions_view.table = mock.Mock() self.pkg_definitions_view.table._meta_._filter_action =\ mock_filter_action self.pkg_definitions_view.table.get_filter_field.return_value =\ 'test_filter_field' self.pkg_definitions_view.table.get_filter_string.return_value =\ 'test_filter_string' self.pkg_definitions_view.get_filters = self.original_get_filters test_filters = {'foo': 'bar'} filters = self.pkg_definitions_view.get_filters(test_filters) expected_filters = { 'test_filter_field': 'test_filter_string', 'foo': 'bar' } for key, val in expected_filters.items(): self.assertEqual(val, filters[key]) murano-dashboard-5.0.0/muranodashboard/tests/unit/packages/test_forms.py0000666000175100017510000003003213245511125026574 0ustar zuulzuul00000000000000# Copyright (c) 2016 AT&T 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. import mock from django import forms as django_forms from muranoclient.common import exceptions as exc from muranodashboard.packages import consts from muranodashboard.packages import forms from openstack_dashboard.test import helpers class TestImportBundleForm(helpers.APITestCase): def test_clean_form(self): import_bundle_form = forms.ImportBundleForm() expected = {'import_type': 'by_name', 'name': 'test_form_name'} import_bundle_form.cleaned_data = expected cleaned_data = import_bundle_form.clean() self.assertEqual(expected, cleaned_data) def test_clean_form_except_validation_error(self): import_bundle_form = forms.ImportBundleForm() for attr in ['name', 'url']: import_bundle_form.cleaned_data = { 'import_type': 'by_{0}'.format(attr), attr: None} expected_error_msg = 'Please supply a bundle {0}'.format(attr) with self.assertRaisesRegexp(django_forms.ValidationError, expected_error_msg): import_bundle_form.clean() class TestImportPackageForm(helpers.APITestCase): def setUp(self): super(TestImportPackageForm, self).setUp() self.import_pkg_form = forms.ImportPackageForm() fields = self.import_pkg_form.fields self.assertEqual( 'Optional', fields['repo_version'].widget.attrs['placeholder']) def test_clean_package(self): size_in_bytes = (consts.MAX_FILE_SIZE_MB - 1) << 20 mock_package = mock.Mock(size=size_in_bytes) self.import_pkg_form.cleaned_data = { 'package': mock_package } cleaned_data = self.import_pkg_form.clean_package() self.assertEqual(mock_package, cleaned_data) def test_clean_package_exception_validation_error(self): size_in_bytes = (consts.MAX_FILE_SIZE_MB + 1) << 20 mock_package = mock.Mock(size=size_in_bytes) self.import_pkg_form.cleaned_data = { 'package': mock_package } expected_error_msg = 'It is forbidden to upload files larger than {0} '\ 'MB.'.format(consts.MAX_FILE_SIZE_MB) with self.assertRaisesRegexp(django_forms.ValidationError, expected_error_msg): self.import_pkg_form.clean_package() def test_clean_form(self): expected = { 'import_type': 'by_name', 'repo_name': 'test_repo_name' } self.import_pkg_form.cleaned_data = expected cleaned_data = self.import_pkg_form.clean() self.assertEqual(expected, cleaned_data) def test_clean_form_except_validation_error(self): tuples = ( ('upload', 'package', 'file'), ('by_name', 'repo_name', 'name'), ('by_url', 'url', 'url') ) for _tuple in tuples: self.import_pkg_form.cleaned_data = { 'import_type': '{0}'.format(_tuple[0]), _tuple[1]: None} expected_error_msg = 'Please supply a package {0}'.\ format(_tuple[2]) with self.assertRaisesRegexp(django_forms.ValidationError, expected_error_msg): self.import_pkg_form.clean() class TestUpdatePackageForm(helpers.APITestCase): def setUp(self): super(TestUpdatePackageForm, self).setUp() mock_package = mock.MagicMock( tags=['bar', 'baz', 'qux'], is_public=False, enabled=True, description='quux') mock_package.configure_mock(name='foo') fake_response = {'status_code': 200} self.mock_request = mock.MagicMock(return_value=fake_response, META=[]) kwargs = {'request': self.mock_request, 'package': mock_package} self.update_pkg_form = forms.UpdatePackageForm(**kwargs) def test_set_initial(self): # set_initial was already called by forms.UpdatePackageForm(**kwargs) self.assertEqual('foo', self.update_pkg_form.fields['name'].initial) self.assertEqual('bar, baz, qux', self.update_pkg_form.fields['tags'].initial) self.assertEqual(False, self.update_pkg_form.fields['is_public'].initial) self.assertEqual(True, self.update_pkg_form.fields['enabled'].initial) self.assertEqual('quux', self.update_pkg_form.fields['description'].initial) class TestModifyPackageForm(helpers.APITestCase): def setUp(self): super(TestModifyPackageForm, self).setUp() mock_package = mock.MagicMock( type='Application', tags=['bar', 'baz', 'qux'], is_public=False, enabled=True, description='quux', categories=['c1', 'c2']) mock_package.configure_mock(name='foo') self.kwargs = {'initial': {'package': mock_package}} fake_response = { "status_code": 200, "text": '{"foo": "bar"}', } self.mock_request = mock.MagicMock(return_value=(fake_response)) with mock.patch('muranodashboard.api.muranoclient') as mock_client: mock_categories = [] for cname in ['c3', 'c4']: mock_category = mock.Mock() mock_category.configure_mock(name=cname) mock_categories.append(mock_category) mock_client().categories.list.return_value = mock_categories self.modify_pkg_form = forms.ModifyPackageForm(self.mock_request, **self.kwargs) def test_init(self): self.assertEqual( [('c3', 'c3'), ('c4', 'c4')], self.modify_pkg_form.fields['categories'].choices) for key in ('c1', 'c2'): self.assertIn(key, self.modify_pkg_form.fields['categories'].initial) self.assertEqual( True, self.modify_pkg_form.fields['categories'].initial[key]) @mock.patch.object(forms, 'exceptions') @mock.patch.object(forms, 'reverse') @mock.patch('muranodashboard.api.muranoclient') def test_init_except_http_exception(self, mock_client, mock_reverse, mock_exceptions): mock_client().categories.list.side_effect = exc.HTTPException mock_reverse.return_value = 'test_redirect' self.modify_pkg_form = forms.ModifyPackageForm(self.mock_request, **self.kwargs) mock_reverse.assert_called_once_with( 'horizon:app-catalog:packages:index') mock_exceptions.handle.assert_called_once_with( self.mock_request, 'Unable to get list of categories', redirect='test_redirect') @mock.patch('muranodashboard.api.muranoclient') def test_handle(self, mock_client): mock_client().packages.update.return_value = {'status_code': 200} test_data = {'tags': 't1 ,t2 ,t3'} self.modify_pkg_form.initial['app_id'] = 'test_app_id' result = self.modify_pkg_form.handle(self.mock_request, test_data) self.assertEqual({'status_code': 200}, result) mock_client().packages.update.assert_called_once_with( 'test_app_id', {'tags': ['t1', 't2', 't3']}) @mock.patch.object(forms, 'exceptions') @mock.patch.object(forms, 'reverse') @mock.patch('muranodashboard.api.muranoclient') def test_handle_except_http_forbidden(self, mock_client, mock_reverse, mock_exceptions): mock_client().packages.update.side_effect = exc.HTTPForbidden mock_reverse.return_value = 'test_redirect' test_data = {'tags': 't1 ,t2 ,t3'} self.modify_pkg_form.initial['app_id'] = 'test_app_id' self.modify_pkg_form.handle(self.mock_request, test_data) mock_reverse.assert_called_once_with( 'horizon:app-catalog:packages:index') mock_exceptions.handle.assert_called_once_with( self.mock_request, 'You are not allowed to perform this operation', redirect='test_redirect') @mock.patch.object(forms, 'exceptions') @mock.patch.object(forms, 'reverse') @mock.patch('muranodashboard.api.muranoclient') def test_handle_except_http_conflict(self, mock_client, mock_reverse, mock_exceptions): mock_client().packages.update.side_effect = exc.HTTPConflict mock_reverse.return_value = 'test_redirect' test_data = {'tags': 't1 ,t2 ,t3'} self.modify_pkg_form.initial['app_id'] = 'test_app_id' self.modify_pkg_form.handle(self.mock_request, test_data) mock_reverse.assert_called_once_with( 'horizon:app-catalog:packages:index') mock_exceptions.handle.assert_called_once_with( self.mock_request, 'Package or Class with the same name is already made public', redirect='test_redirect') @mock.patch.object(forms, 'exceptions') @mock.patch.object(forms, 'reverse') @mock.patch('muranodashboard.api.muranoclient') def test_handle_except_exception(self, mock_client, mock_reverse, mock_exceptions): e = Exception() setattr(e, 'details', '{"error": {"message": "test_error_message"}}') mock_client().packages.update.side_effect = e mock_reverse.return_value = 'test_redirect' test_data = {'tags': 't1 ,t2 ,t3'} self.modify_pkg_form.initial['app_id'] = 'test_app_id' self.modify_pkg_form.handle(self.mock_request, test_data) mock_reverse.assert_called_once_with( 'horizon:app-catalog:packages:index') mock_exceptions.handle.assert_called_once_with( self.mock_request, 'Failed to modify the package. {0}'.format('test_error_message'), redirect='test_redirect') class TestSelectCategories(helpers.APITestCase): def setUp(self): super(TestSelectCategories, self).setUp() fake_response = { "status_code": 200, "text": '{"foo": "bar"}', } self.mock_request = mock.MagicMock(return_value=(fake_response)) self.kwargs = {'request': self.mock_request} with mock.patch('muranodashboard.api.muranoclient') as mock_client: mock_categories = [] for cname in ['c1', 'c2']: mock_category = mock.Mock() mock_category.configure_mock(name=cname) mock_categories.append(mock_category) mock_client().categories.list.return_value = mock_categories self.select_categories_form = forms.SelectCategories(**self.kwargs) def test_init(self): self.assertEqual( [('c1', 'c1'), ('c2', 'c2')], self.select_categories_form.fields['categories'].choices) @mock.patch.object(forms, 'exceptions') @mock.patch.object(forms, 'reverse') @mock.patch('muranodashboard.api.muranoclient') def test_init_except_http_exception(self, mock_client, mock_reverse, mock_exceptions): mock_client().categories.list.side_effect = exc.HTTPException mock_reverse.return_value = 'test_redirect' forms.SelectCategories(**self.kwargs) mock_reverse.assert_called_once_with( 'horizon:app-catalog:packages:index') mock_exceptions.handle.assert_called_once_with( self.mock_request, 'Unable to get list of categories', redirect='test_redirect') murano-dashboard-5.0.0/muranodashboard/tests/unit/packages/test_api.py0000666000175100017510000002046413245511125026227 0ustar zuulzuul00000000000000# Copyright (c) 2016 AT&T 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. import mock from six import PY3 from muranoclient.v1 import client from muranodashboard.api import packages from openstack_dashboard.test import helpers def mock_next(obj, attr, value): if PY3: setattr(obj.__next__, attr, value) else: setattr(obj.next, attr, value) class MagicIterMock(mock.MagicMock): if PY3: __next__ = mock.Mock(return_value=None) else: next = mock.Mock(return_value=None) class TestPackagesAPI(helpers.APITestCase): def setUp(self): super(TestPackagesAPI, self).setUp() self.packages = ['foo', 'bar', 'baz'] self.mock_client = mock.Mock(spec=client) self.mock_client.packages.filter.return_value = self.packages packages.api = mock.Mock() packages.api.muranoclient.return_value = self.mock_client self.mock_request = mock.Mock() self.addCleanup(mock.patch.stopall) def test_package_list(self): package_list, more = packages.package_list(self.mock_request) self.assertEqual(self.packages, package_list) self.assertFalse(more) self.mock_client.packages.filter.assert_called_once_with(limit=100) def test_package_list_with_paginate(self): package_list, more = packages.package_list(self.mock_request, paginate=True, page_size=1) # Only one package should be returned. self.assertEqual(self.packages[:1], package_list) self.assertTrue(more) self.mock_client.packages.filter.assert_called_once_with(limit=2) self.mock_client.packages.filter.reset_mock() package_list, more = packages.package_list(self.mock_request, paginate=True, page_size=2) # Only two packages should be returned. self.assertEqual(self.packages[:2], package_list) self.assertTrue(more) self.mock_client.packages.filter.assert_called_once_with(limit=3) def test_package_list_with_filters(self): package_list, more = packages.package_list(self.mock_request, marker='test_marker', sort_dir='test_sort_dir') self.assertEqual(self.packages, package_list) self.assertFalse(more) self.mock_client.packages.filter.assert_called_once_with( limit=100, marker='test_marker', sort_dir='test_sort_dir') def test_apps_that_inherit(self): setattr(packages.settings, "MURANO_USE_GLARE", False) apps = packages.apps_that_inherit(self.mock_request, 'test_fqn') self.assertEqual([], apps) setattr(packages.settings, "MURANO_USE_GLARE", True) apps = packages.apps_that_inherit(self.mock_request, 'test_fqn') self.assertEqual(self.packages, apps) self.mock_client.packages.filter.assert_called_once_with( inherits='test_fqn') def test_app_by_fqn(self): self.mock_client = MagicIterMock(spec=client) mock_next( self.mock_client.packages.filter(), 'return_value', self.packages[0] ) packages.api.muranoclient.return_value = self.mock_client self.mock_client.reset_mock() setattr(packages.settings, "MURANO_USE_GLARE", True) app = packages.app_by_fqn(self.mock_request, 'test_fqn', version='1.0') self.assertIsNotNone(app) self.assertEqual(self.packages[0], app) self.mock_client.packages.filter.assert_called_once_with( fqn='test_fqn', catalog=True, version='1.0') def test_app_by_fqn_except_stop_iteration(self): self.mock_client = MagicIterMock(spec=client) mock_next( self.mock_client.packages.filter(), 'side_effect', StopIteration ) packages.api.muranoclient.return_value = self.mock_client self.mock_client.reset_mock() setattr(packages.settings, "MURANO_USE_GLARE", True) app = packages.app_by_fqn(self.mock_request, 'test_fqn', version='1.0') self.assertIsNone(app) self.mock_client.packages.filter.assert_called_once_with( fqn='test_fqn', catalog=True, version='1.0') def test_make_loader_cls(self): loader = packages.make_loader_cls() self.assertIsNotNone(loader) self.assertIn("Loader", str(loader)) @mock.patch('muranodashboard.common.cache._load_from_file', return_value=None) @mock.patch('muranodashboard.common.cache._save_to_file') def test_get_app_ui(self, *args): mock_get_ui = packages.api.muranoclient().packages.get_ui mock_get_ui.return_value = 'foo_ui' ui = packages.get_app_ui(None, 'foo_app_id') mock_args = [arg for arg in mock_get_ui.call_args] self.assertEqual(ui, 'foo_ui') mock_get_ui.assert_called_once_with('foo_app_id', mock.ANY) self.assertEqual('Loader', mock_args[0][1].__name__) @mock.patch('muranodashboard.common.cache._load_from_file', return_value=None) @mock.patch('muranodashboard.common.cache._save_to_file') def test_get_app_logo(self, *args): mock_get_app_logo = packages.api.muranoclient().packages.get_logo mock_get_app_logo.return_value = 'foo_app_logo' app_logo = packages.get_app_logo(None, 'foo_app_id') self.assertEqual(app_logo, 'foo_app_logo') mock_get_app_logo.assert_called_once_with('foo_app_id') @mock.patch('muranodashboard.common.cache._load_from_file', return_value=None) @mock.patch('muranodashboard.common.cache._save_to_file') def test_get_app_supplier_logo(self, *args): mock_get_supplier_logo = packages.api.muranoclient().packages. \ get_supplier_logo mock_get_supplier_logo.return_value = 'foo_app_supplier_logo' app_supplier_logo = packages.get_app_supplier_logo(None, 'foo_app_id') self.assertEqual(app_supplier_logo, 'foo_app_supplier_logo') mock_get_supplier_logo.assert_called_once_with('foo_app_id') @mock.patch('muranodashboard.common.cache._load_from_file', return_value=None) @mock.patch('muranodashboard.common.cache._save_to_file') def test_get_app_fqn(self, *args): mock_app = mock.Mock(fully_qualified_name='foo_app_fqn') mock_get_app = packages.api.muranoclient().packages.get mock_get_app.return_value = mock_app app_fqn = packages.get_app_fqn(None, 'foo_app_id') self.assertEqual(app_fqn, 'foo_app_fqn') mock_get_app.assert_called_once_with('foo_app_id') @mock.patch('muranodashboard.common.cache._load_from_file', return_value=None) @mock.patch('muranodashboard.common.cache._save_to_file') def test_get_service_name(self, *args): mock_app = mock.Mock() mock_app.configure_mock(name='foo_app_name') mock_get_app = packages.api.muranoclient().packages.get mock_get_app.return_value = mock_app app_service_name = packages.get_service_name(None, 'foo_app_id') self.assertEqual(app_service_name, 'foo_app_name') mock_get_app.assert_called_once_with('foo_app_id') @mock.patch('muranodashboard.common.cache._load_from_file', return_value=None) @mock.patch('muranodashboard.common.cache._save_to_file') def test_get_package_details(self, *args): mock_app = mock.Mock() mock_get_app = packages.api.muranoclient().packages.get mock_get_app.return_value = mock_app app_details = packages.get_package_details(None, 'foo_app_id') self.assertEqual(app_details, mock_app) mock_get_app.assert_called_once_with('foo_app_id') murano-dashboard-5.0.0/muranodashboard/tests/unit/__init__.py0000666000175100017510000000000013245511125024360 0ustar zuulzuul00000000000000murano-dashboard-5.0.0/muranodashboard/tests/unit/images/0000775000175100017510000000000013245511556023534 5ustar zuulzuul00000000000000murano-dashboard-5.0.0/muranodashboard/tests/unit/images/__init__.py0000666000175100017510000000000013245511125025625 0ustar zuulzuul00000000000000murano-dashboard-5.0.0/muranodashboard/tests/unit/images/test_views.py0000666000175100017510000001633213245511125026301 0ustar zuulzuul00000000000000# Copyright (c) 2016 AT&T 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 import mock import testtools from horizon import exceptions from muranodashboard.images import tables from muranodashboard.images import views class TestMarkedImagesView(testtools.TestCase): def setUp(self): super(TestMarkedImagesView, self).setUp() mock_request = mock.Mock(horizon={'async_messages': []}) self.images_view = views.MarkedImagesView(request=mock_request) self.images_view._prev = False self.images_view._more = False self.assertEqual(tables.MarkedImagesTable, self.images_view.table_class) self.assertEqual('images/index.html', self.images_view.template_name) self.assertEqual('Marked Images', self.images_view.page_title) mock_horizon_utils = mock.patch.object(views, 'utils').start() mock_horizon_utils.get_page_size.return_value = 2 self.addCleanup(mock.patch.stopall) def _get_mock_image(self, prefix): image_info = {} if prefix: image_info = { "title": "{0}_title".format(prefix), "type": "{0}_type".format(prefix) } mock_image = mock.Mock(**{'murano_image_info': json.dumps(image_info)}) return mock_image def test_has_prev_data(self): self.assertFalse(self.images_view.has_prev_data(None)) def test_has_more_data(self): self.assertFalse(self.images_view.has_more_data(None)) @mock.patch.object(views, 'glance', autospec=True) def test_get_data(self, mock_glance): """Test that get_data works.""" foo_mock_image = self._get_mock_image('foo') bar_mock_image = self._get_mock_image('bar') # Filtered out by forms.filter_murano_images. mock_image_to_filter = self._get_mock_image(None) mock_glance_client = mock.Mock() mock_glance_client.images.list.return_value = [ foo_mock_image, bar_mock_image, mock_image_to_filter] mock_glance.glanceclient.return_value = mock_glance_client self.images_view.request.GET.get.return_value = 'foo_marker' result = self.images_view.get_data() expected_images = [bar_mock_image, foo_mock_image] expected_kwargs = { 'filters': {}, 'marker': 'foo_marker', 'sort_dir': 'asc' } self.assertEqual(expected_images, result) self.assertTrue(self.images_view.has_more_data(None)) self.assertTrue(self.images_view.has_prev_data(None)) mock_glance_client.images.list.assert_called_once_with( **expected_kwargs) self.images_view.request.GET.get.assert_called_once_with( tables.MarkedImagesTable._meta.prev_pagination_param, None) mock_glance.glanceclient.assert_called_once_with( self.images_view.request, "2") @mock.patch.object(views, 'glance', autospec=True) def test_get_data_with_desc_sort_dir(self, mock_glance): """Test that sorting in descending order works.""" foo_mock_image = self._get_mock_image('foo') bar_mock_image = self._get_mock_image('bar') mock_glance_client = mock.Mock() mock_glance_client.images.list.return_value = [ foo_mock_image, bar_mock_image] mock_glance.glanceclient.return_value = mock_glance_client self.images_view.request.GET.get.return_value = None result = self.images_view.get_data() expected_images = [foo_mock_image, bar_mock_image] expected_kwargs = { 'filters': {}, 'sort_dir': 'desc' } self.assertEqual(expected_images, result) self.assertFalse(self.images_view.has_more_data(None)) self.assertFalse(self.images_view.has_prev_data(None)) mock_glance_client.images.list.assert_called_once_with( **expected_kwargs) self.images_view.request.GET.get.assert_has_calls([ mock.call(tables.MarkedImagesTable._meta.prev_pagination_param, None), mock.call(tables.MarkedImagesTable._meta.pagination_param, None) ]) mock_glance.glanceclient.assert_called_once_with( self.images_view.request, "2") @mock.patch.object(views, 'glance', autospec=True) def test_get_data_with_more_results(self, mock_glance): """Test that extra results are not included in return value.""" foo_mock_image = self._get_mock_image('foo') bar_mock_image = self._get_mock_image('bar') extra_mock_image = self._get_mock_image('baz') # Extra result. # Filtered out by forms.filter_murano_images. mock_image_to_filter = self._get_mock_image(None) mock_glance_client = mock.Mock() mock_glance_client.images.list.return_value = [ foo_mock_image, bar_mock_image, extra_mock_image, mock_image_to_filter] mock_glance.glanceclient.return_value = mock_glance_client self.images_view.request.GET.get.return_value = 'foo_marker' result = self.images_view.get_data() # Extra result not included, and result should be reversed. expected_images = [bar_mock_image, foo_mock_image] expected_kwargs = { 'filters': {}, 'marker': 'foo_marker', 'sort_dir': 'asc' } self.assertEqual(expected_images, result) self.assertTrue(self.images_view.has_more_data(None)) self.assertTrue(self.images_view.has_prev_data(None)) mock_glance_client.images.list.assert_called_once_with( **expected_kwargs) self.images_view.request.GET.get.assert_called_once_with( tables.MarkedImagesTable._meta.prev_pagination_param, None) mock_glance.glanceclient.assert_called_once_with( self.images_view.request, "2") @mock.patch.object(views, 'reverse', autospec=True) @mock.patch.object(views, 'glance', autospec=True) def test_get_data_except_glance_image_list_exception(self, mock_glance, mock_reverse): """Test that glance_v1_client.images.list exception is handled.""" mock_glance_client = mock.Mock() mock_glance_client.images.list.side_effect = Exception() mock_glance.glanceclient.return_value = mock_glance_client mock_reverse.return_value = 'foo_reverse_url' self.images_view.request.GET.get.return_value = None e = self.assertRaises(exceptions.Http302, self.images_view.get_data) self.assertEqual('foo_reverse_url', e.location) mock_glance.glanceclient.assert_called_once_with( self.images_view.request, "2") mock_reverse.assert_called_once_with( 'horizon:app-catalog:catalog:index') murano-dashboard-5.0.0/muranodashboard/tests/unit/images/test_forms.py0000666000175100017510000000456313245511125026275 0ustar zuulzuul00000000000000# Copyright (c) 2016 AT&T 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 mock import testtools from django.utils.translation import ugettext_lazy as _ from muranodashboard.images import forms class TestImagesForms(testtools.TestCase): def setUp(self): super(TestImagesForms, self).setUp() metadata = '{"title": "title", "type": "type"}' self.mock_img = mock.MagicMock(id=12, murano_image_info=metadata) self.mock_request = mock.MagicMock() @mock.patch.object(forms, 'LOG') def test_filter_murano_images(self, mock_log): mock_blank_img = \ mock.MagicMock(id=13, murano_image_info="info") images = [mock_blank_img] msg = _('Invalid metadata for image: {0}').format(images[0].id) self.assertEqual(images, forms.filter_murano_images(images, self.mock_request)) mock_log.warning.assert_called_once_with(msg) images = [self.mock_img] self.assertEqual(images, forms.filter_murano_images(images)) murano_meta = '{"title": "title", "type": "type"}' mock_snapshot_img = mock.MagicMock( id=14, murano_image_info=murano_meta, image_type='snapshot') images = [mock_snapshot_img] self.assertEqual([], forms.filter_murano_images(images, self.mock_request)) class TestMarkImageForm(testtools.TestCase): def setUp(self): super(TestMarkImageForm, self).setUp() self.mock_request = mock.MagicMock() self.mark_img_form = forms.MarkImageForm(self.mock_request) @mock.patch.object(forms, 'glance') def test_handle(self, mock_glance_api): data = { 'title': 'title', 'image': 'id', 'type': 'type' } self.mark_img_form.handle(self.mock_request, data) self.assertTrue(mock_glance_api.image_update_properties.called) murano-dashboard-5.0.0/muranodashboard/tests/unit/catalog/0000775000175100017510000000000013245511556023701 5ustar zuulzuul00000000000000murano-dashboard-5.0.0/muranodashboard/tests/unit/catalog/__init__.py0000666000175100017510000000000013245511125025772 0ustar zuulzuul00000000000000murano-dashboard-5.0.0/muranodashboard/tests/unit/catalog/test_views.py0000666000175100017510000007270513245511125026454 0ustar zuulzuul00000000000000# Copyright (c) 2016 AT&T 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 mock import testtools from django.conf import settings from django.forms import formsets from django import http from django.utils.translation import ugettext_lazy as _ from horizon.forms import views as horizon_views from muranoclient.common import exceptions as exc from muranodashboard.catalog import tabs as catalog_tabs from muranodashboard.catalog import views try: from urlparse import urlparse except ImportError: from urllib.parse import urlparse class TestCatalogViews(testtools.TestCase): def setUp(self): super(TestCatalogViews, self).setUp() self.mock_request = mock.MagicMock(session={}) self.env = mock.MagicMock(id='12', name='test_env', status='READY') self.addCleanup(mock.patch.stopall) def test_is_valid_environment(self): mock_env1 = mock.MagicMock(id='13') valid_envs = [mock_env1, self.env] self.assertTrue(views.is_valid_environment(self.env, valid_envs)) @mock.patch.object(views, 'env_api') def test_get_environments_context(self, mock_env_api): mock_env_api.environments_list.return_value = [self.env] self.assertIsNotNone(views.get_environments_context(self.mock_request)) mock_env_api.environments_list.assert_called_with(self.mock_request) @mock.patch.object(views, 'api') def test_get_categories_list(self, mock_api): self.assertEqual([], views.get_categories_list(self.mock_request)) mock_api.handled_exceptions.assert_called_once_with(self.mock_request) mock_api.muranoclient.assert_called_once_with(self.mock_request) @mock.patch.object(views, 'env_api') def test_create_quick_environment(self, mock_env_api): views.create_quick_environment(self.mock_request) self.assertTrue(mock_env_api.environment_create.called) @mock.patch.object(views, 'pkg_api') def test_get_image(self, mock_pkg_api): app_id = 13 result = views.get_image(self.mock_request, app_id) self.assertIsInstance(result, http.HttpResponse) (mock_pkg_api.get_app_logo. assert_called_once_with(self.mock_request, app_id)) mock_pkg_api.reset_mock() mock_pkg_api.get_app_logo.return_value = None result = views.get_image(self.mock_request, app_id) self.assertIsInstance(result, http.HttpResponseRedirect) (mock_pkg_api.get_app_logo. assert_called_once_with(self.mock_request, app_id)) @mock.patch.object(views, 'pkg_api') def test_get_supplier_image(self, mock_pkg_api): app_id = 13 result = views.get_supplier_image(self.mock_request, app_id) self.assertIsInstance(result, http.HttpResponse) (mock_pkg_api.get_app_supplier_logo. assert_called_once_with(self.mock_request, app_id)) mock_pkg_api.reset_mock() mock_pkg_api.get_app_supplier_logo.return_value = None result = views.get_supplier_image(self.mock_request, app_id) self.assertIsInstance(result, http.HttpResponseRedirect) (mock_pkg_api.get_app_supplier_logo. assert_called_once_with(self.mock_request, app_id)) @mock.patch.object(views, 'pkg_api') @mock.patch('muranodashboard.dynamic_ui.services.version') @mock.patch('muranodashboard.dynamic_ui.services.pkg_api') def test_quick_deploy_error(self, services_pkg_api, mock_version, views_pkg_api): mock_version.check_version.return_value = '0.2' app_id = 'app_id' self.assertRaises(ValueError, views.quick_deploy, self.mock_request, app_id=app_id) (services_pkg_api.get_app_ui. assert_called_once_with(self.mock_request, app_id)) views_pkg_api.get_app_fqn.assert_called_with(self.mock_request, app_id) @mock.patch.object(views, 'shortcuts') @mock.patch.object(views, 'get_available_environments') @mock.patch.object(views, 'http_utils') def test_switch(self, mock_http_utls, mock_get_available_environments, mock_shortcuts): mock_http_utls.is_safe_url.return_value = True self.mock_request.GET = {'redirect': 'redirect_to_foo'} mock_env = mock.Mock(id='foo_env_id') mock_get_available_environments.return_value = [mock_env] mock_shortcuts.redirect.return_value = 'foo_redirect' result = views.switch(self.mock_request, 'foo_env_id', redirect_field_name='redirect') self.assertEqual('foo_redirect', result) self.assertEqual(mock_env, self.mock_request.session['environment']) mock_shortcuts.redirect.assert_called_once_with('redirect_to_foo') mock_shortcuts.redirect.reset_mock() mock_http_utls.is_safe_url.return_value = False result = views.switch(self.mock_request, 'foo_env_id', redirect_field_name='redirect') self.assertEqual('foo_redirect', result) self.assertEqual(mock_env, self.mock_request.session['environment']) mock_shortcuts.redirect.assert_called_once_with( settings.LOGIN_REDIRECT_URL) @mock.patch.object(views, 'env_api') def test_get_next_quick_environment_name(self, mock_env_api): # Test whether non-matching name is not returned. match_env = mock.Mock() match_env.configure_mock(name='quick-env-123') non_match_env = mock.Mock() non_match_env.configure_mock(name='quick-env-foo') mock_env_api.environments_list.return_value = [ match_env, non_match_env ] result = views.get_next_quick_environment_name(self.mock_request) self.assertEqual('quick-env-124', result) # Test whether matching name with biggest number is returned. non_match_env.configure_mock(name='quick-env-124') mock_env_api.environments_list.return_value = [ match_env, non_match_env ] result = views.get_next_quick_environment_name(self.mock_request) self.assertEqual('quick-env-125', result) @mock.patch.object(views, 'api') def test_cleaned_latest_apps(self, mock_api): foo_app = mock.Mock(id='foo_id') bar_app = mock.Mock(id='bar_id') mock_api.muranoclient().packages.filter.return_value = [ foo_app, bar_app ] self.mock_request.session['latest_apps'] = ['foo', 'bar'] expected_params = { 'type': 'Application', 'catalog': True, 'id': 'in:foo,bar' } result = views.cleaned_latest_apps(self.mock_request) self.assertEqual([foo_app, bar_app], result) self.assertEqual(collections.deque(['foo_id', 'bar_id']), self.mock_request.session['latest_apps']) mock_api.muranoclient().packages.filter.assert_called_once_with( **expected_params) class TestLazyWizard(testtools.TestCase): @mock.patch.object(views.LazyWizard, 'http_method_names', new_callable=mock.PropertyMock) def test_as_view_except_type_error(self, mock_http_method_names): mock_http_method_names.return_value = ['patch'] # Test that first occurrence of type error is thrown. kwargs = {'patch': ''} expected_error_msg = "You tried to pass in the {0} method name as a "\ "keyword argument to LazyWizard(). "\ "Don't do that.".format("patch") e = self.assertRaises(TypeError, views.LazyWizard.as_view, None, **kwargs) self.assertEqual(expected_error_msg, str(e)) # Test that second occurrence of type error is thrown. kwargs = {'foobar': ''} expected_error_msg = "LazyWizard() received an invalid keyword "\ "'foobar'" e = self.assertRaises(TypeError, views.LazyWizard.as_view, None, **kwargs) self.assertEqual(expected_error_msg, str(e)) @mock.patch.object(views.LazyWizard, 'dispatch') def test_as_view(self, mock_dispatch): form = mock.Mock() form.__name__ = 'test_form' form.base_fields = {'foo': None, 'bar': None} formset = formsets.formset_factory(form) mock_request = mock.Mock() mock_request.session = {} mock_initforms = mock.Mock(return_value=[formset]) kwargs = {'app_id': 'foo'} view = views.LazyWizard.as_view(mock_initforms) self.assertTrue(hasattr(view, '__call__')) view(mock_request, **kwargs) mock_initforms.assert_called_once_with(mock_request, kwargs) mock_dispatch.assert_called_once_with(mock_request, **kwargs) class TestWizard(testtools.TestCase): def setUp(self): super(TestWizard, self).setUp() self.wizard = views.Wizard() self.wizard.storage = mock.MagicMock() self.wizard.storage.extra_data.__getitem__().name = 'foo_app' self.wizard.kwargs = { 'do_redirect': 'redirect_to_foo', 'environment_id': 'foo_env_id', 'app_id': 'foo_app_id' } self.wizard.request = mock.Mock() self.wizard.request.META = {} self.wizard.request.session = {'quick_env_id': 'quick_foo_env_id'} self.assertEqual('services/wizard_create.html', self.wizard.template_name) self.assertEqual(False, self.wizard.do_redirect) self.assertEqual('Add Application', self.wizard.page_title) def test_get_prefix(self): kwargs = {'foo': 'bar'} prefix = self.wizard.get_prefix(None, **kwargs) self.assertIsInstance(prefix, str) def test_get_form_prefix(self): self.wizard.steps = mock.Mock() self.wizard.steps.step0 = 'foo_step' self.wizard.steps.all.index.return_value = 'bar_step' self.assertEqual('foo_step', self.wizard.get_form_prefix()) self.assertEqual('bar_step', self.wizard.get_form_prefix(step='bar')) @mock.patch.object(views, 'messages') @mock.patch.object(views, 'LOG') @mock.patch.object(views, 'env_api') @mock.patch.object(views, 'reverse') def test_done(self, mock_reverse, mock_env_api, mock_log, mock_messages): mock_service = mock.Mock() mock_service.service.extract_attributes.return_value = {'foo': 'bar'} mock_created_service = mock.Mock() setattr(mock_created_service, '?', {'id': 'foo_service_id'}) mock_env_api.service_create.return_value = mock_created_service self.wizard.done([mock_service]) mock_reverse.assert_any_call( "horizon:app-catalog:environments:index") mock_reverse.assert_any_call( "horizon:app-catalog:environments:services", args=('quick_foo_env_id',)) mock_env_api.service_create.assert_called_once_with( self.wizard.request, 'quick_foo_env_id', mock.ANY) expected_message = "The 'foo_app' application successfully added to "\ "environment." mock_log.info.assert_called_once_with(expected_message) mock_messages.success.assert_called_once_with( self.wizard.request, expected_message) @mock.patch.object(views, 'exceptions') @mock.patch.object(views, 'env_api') @mock.patch.object(views, 'reverse') def test_done_except_http_forbidden(self, mock_reverse, mock_env_api, mock_exceptions): mock_reverse.return_value = 'foo_url' mock_service = mock.Mock() mock_service.service.extract_attributes.return_value = {'foo': 'bar'} mock_env_api.service_create.side_effect = exc.HTTPForbidden self.wizard.done([mock_service]) expected_error_msg = _("Sorry, you can't add application right now. " "The environment is deploying.") mock_exceptions.handle.assert_called_once_with( self.wizard.request, expected_error_msg, redirect='foo_url') @mock.patch.object(views, 'exceptions') @mock.patch.object(views, 'LOG') @mock.patch.object(views, 'env_api') @mock.patch.object(views, 'reverse') def test_done_except_exception(self, mock_reverse, mock_env_api, mock_log, mock_exceptions): mock_reverse.return_value = 'foo_url' mock_service = mock.Mock() mock_service.service.extract_attributes.return_value = {'foo': 'bar'} mock_env_api.service_create.side_effect = Exception self.wizard.done([mock_service]) expected_error_msg = _('Adding application to an environment failed.') mock_log.exception(expected_error_msg) mock_env_api.environment_delete.assert_called_once_with( self.wizard.request, 'quick_foo_env_id') mock_exceptions.handle.assert_called_once_with( self.wizard.request, expected_error_msg, redirect='foo_url') def test_create_hacked_response(self): self.wizard.request.META = { horizon_views.ADD_TO_FIELD_HEADER: 'foo_field_id' } response = self.wizard.create_hacked_response( 'foo_obj_id', 'foo_obj_name') self.assertIsInstance(response, http.HttpResponse) self.assertEqual(b'["foo_obj_id", "foo_obj_name"]', response.content) self.assertTrue(response.has_header('X-Horizon-Add-To-Field')) self.assertEqual('foo_field_id', response['X-Horizon-Add-To-Field']) def test_create_hacked_response_empty_response(self): response = self.wizard.create_hacked_response(None, None) self.assertIsInstance(response, http.HttpResponse) self.assertEqual(b'', response.content) @mock.patch.object(views, 'utils') def test_get_form_initial(self, mock_utils): mock_utils.ensure_python_obj.return_value = 'foo_env_id' self.wizard.initial_dict = {'foo_step': 'foo'} # Test whether correct dict entry is returned. result = self.wizard.get_form_initial('foo_step') self.assertEqual('foo', result) mock_utils.ensure_python_obj.assert_called_once_with('foo_env_id') # Test whether init_dict returned because key not found. result = self.wizard.get_form_initial('bar_step') expected = { 'request': self.wizard.request, 'app_id': 'foo_app_id', 'environment_id': 'foo_env_id' } for key, val in expected.items(): self.assertEqual(val, result[key]) # Test whether alternate env_id is used. mock_utils.ensure_python_obj.return_value = None result = self.wizard.get_form_initial('bar_step') expected = { 'request': self.wizard.request, 'app_id': 'foo_app_id', 'environment_id': 'quick_foo_env_id' } for key, val in expected.items(): self.assertEqual(val, result[key]) @mock.patch.object( views, 'nova', mock.MagicMock(side_effect=views.nova_exceptions.ClientException)) def test_get_flavors(self): result = self.wizard.get_flavors() self.assertEqual('[]', result) views.nova.flavor_list.assert_called_once_with(self.wizard.request) @mock.patch.object(views, 'nova') @mock.patch.object(views, 'quotas') @mock.patch.object(views, 'services') @mock.patch.object(views, 'api') def test_get_context_data(self, mock_api, mock_services, mock_quotas, mock_nova): mock_api.muranoclient().environments.get().name = 'foo_env_name' mock_services.get_app_field_descriptions.return_value = [ 'foo_field_descr', 'foo_extended_descr' ] mock_nova.flavor_list.return_value = [ type('FakeFlavor%s' % k, (object, ), {'id': 'fake_id_%s' % k, 'name': 'fake_name_%s' % k, '_info': {'foo': 'bar'}}) for k in (1, 2) ] form = mock.Mock() app = mock.Mock(fully_qualified_name='foo_app_fqn') app.configure_mock(name='foo_app') self.wizard.request.GET = {} self.wizard.request.POST = {} self.wizard.storage.extra_data.get.return_value = app self.wizard.steps = mock.Mock(index='foo_step_index', step0=-1) self.wizard.prefix = 'foo_prefix' self.wizard.kwargs['do_redirect'] = 'foo_do_redirect' self.wizard.kwargs['drop_wm_form'] = 'foo_drop_wm_form' context = self.wizard.get_context_data(form) expected_context = { 'type': 'foo_app_fqn', 'service_name': 'foo_app', 'app_id': 'foo_app_id', 'environment_id': 'foo_env_id', 'environment_name': 'foo_env_name', 'do_redirect': 'foo_do_redirect', 'drop_wm_form': 'foo_drop_wm_form', 'prefix': 'foo_prefix', 'wizard_id': mock.ANY, 'field_descriptions': 'foo_field_descr', 'extended_descriptions': 'foo_extended_descr' } for key, val in expected_context.items(): self.assertIn(key, context) self.assertEqual(val, context[key]) mock_api.muranoclient().environments.get.assert_called_with( 'foo_env_id') mock_services.get_app_field_descriptions.assert_called_once_with( self.wizard.request, 'foo_app_id', 'foo_step_index') mock_nova.flavor_list.assert_called_once_with(self.wizard.request) @mock.patch.object(views, 'nova') @mock.patch.object(views, 'quotas') @mock.patch.object(views, 'env_api') @mock.patch.object(views, 'utils') @mock.patch.object(views, 'services') @mock.patch.object(views, 'api') def test_get_context_data_alternate_control_flow( self, mock_api, mock_services, mock_utils, mock_env_api, mock_quatas, mock_nova): form = mock.Mock() app = mock.Mock(fully_qualified_name='foo_app_fqn') app.configure_mock(name='foo_app') mock_api.muranoclient().environments.get().name = 'foo_env_name' mock_api.muranoclient().packages.get.return_value = app mock_services.get_app_field_descriptions.return_value = [ 'foo_field_descr', 'foo_extended_descr' ] mock_utils.ensure_python_obj.return_value = None mock_env_api.environments_list.return_value = [] mock_nova.flavor_list.return_value = [ type('FakeFlavor%s' % k, (object, ), {'id': 'fake_id_%s' % k, 'name': 'fake_name_%s' % k, '_info': {'foo': 'bar'}}) for k in (1, 2) ] self.wizard.request.GET = {} self.wizard.request.POST = {'wizard_id': 'foo_wizard_id'} self.wizard.storage.extra_data = {} self.wizard.steps = mock.Mock(index='foo_step_index', step0=0) self.wizard.steps.all = [] self.wizard.prefix = 'foo_prefix' context = self.wizard.get_context_data(form) expected_context = { 'type': 'foo_app_fqn', 'service_name': 'foo_app', 'app_id': 'foo_app_id', 'environment_id': None, 'environment_name': 'quick-env-1', 'do_redirect': mock.ANY, 'drop_wm_form': mock.ANY, 'prefix': 'foo_prefix', 'wizard_id': 'foo_wizard_id', 'field_descriptions': 'foo_field_descr', 'extended_descriptions': 'foo_extended_descr' } for key, val in expected_context.items(): self.assertIn(key, context) self.assertEqual(val, context[key]) self.assertEqual(app, self.wizard.storage.extra_data['app']) mock_api.muranoclient().packages.get.assert_called_once_with( 'foo_app_id') mock_api.muranoclient().environments.get.assert_called_once_with() mock_services.get_app_field_descriptions.assert_called_once_with( self.wizard.request, 'foo_app_id', 'foo_step_index') mock_nova.flavor_list.assert_called_once_with(self.wizard.request) class TestIndexView(testtools.TestCase): def setUp(self): super(TestIndexView, self).setUp() self.index_view = views.IndexView() self.assertEqual(6, self.index_view.paginate_by) self.assertEqual(_("Browse"), self.index_view.page_title) self.assertIsNone(self.index_view._more) self.index_view.request = mock.Mock() self.index_view.request.GET = { 'category': 'foo_category', 'search': 'foo_search', 'order_by': 'foo_col', 'sort_dir': 'desc', 'marker': 'foo_marker' } def test_get_object_id(self): mock_datum = mock.Mock(id='foo_datum_id') self.assertEqual('foo_datum_id', views.IndexView.get_object_id(mock_datum)) def test_get_marker(self): mock_datum = mock.Mock(id='foo_datum_id') self.index_view.object_list = [mock_datum] self.assertEqual('foo_datum_id', self.index_view.get_marker()) self.index_view.object_list = [] self.assertEqual('', self.index_view.get_marker()) def test_get_query_params(self): expected = { 'search': 'foo_search', 'order_by': 'foo_col', 'sort_dir': 'desc' } query_params = self.index_view.get_query_params(internal_query=False) for key, val in expected.items(): self.assertEqual(val, query_params[key]) expected['type'] = 'Application' query_params = self.index_view.get_query_params(internal_query=True) for key, val in expected.items(): self.assertEqual(val, query_params[key]) @mock.patch.object(views, 'pkg_api') def test_get_queryset(self, mock_pkg_api): mock_pkg_api.package_list.return_value = ( ['bar_pkg', 'foo_pkg'], False) self.index_view.paginate_by = 123 packages = self.index_view.get_queryset() self.assertEqual(['foo_pkg', 'bar_pkg'], packages) self.assertFalse(self.index_view._more) mock_pkg_api.package_list.assert_called_once_with( self.index_view.request, filters=mock.ANY, paginate=True, marker='foo_marker', page_size=123, sort_dir='desc', limit=123) def test_get_template_names(self): self.assertEqual(['catalog/index.html'], self.index_view.get_template_names()) def test_has_next_page(self): self.index_view.request.GET = {'sort_dir': 'asc'} self.index_view._more = False self.assertFalse(self.index_view.has_next_page()) @mock.patch.object(views, 'pkg_api') def test_has_next_page_with_api_query(self, mock_pkg_api): mock_pkg_api.package_list.return_value = (['foo'], False) self.index_view.get_marker = lambda: 'foo_marker' result = self.index_view.has_next_page() self.assertTrue(result) mock_pkg_api.package_list.assert_called_once_with( self.index_view.request, filters=mock.ANY, paginate=True, marker='foo_marker', page_size=1) def test_has_prev_page(self): self.index_view._more = False self.index_view.request.GET = {'sort_dir': 'desc'} self.assertFalse(self.index_view.has_prev_page()) self.index_view.request.GET = {'sort_dir': 'asc', 'marker': 'foo'} self.assertTrue(self.index_view.has_prev_page()) def test_paginate_queryset(self): result = self.index_view.paginate_queryset({}, 3) self.assertEqual((None, None, {}, None), result) def test_get_current_category(self): self.assertEqual('foo_category', self.index_view.get_current_category()) @mock.patch.object(views, 'reverse') def test_current_page_url(self, mock_reverse): mock_reverse.return_value = 'foo_curr_url' self.index_view.get_marker = lambda: 'foo_marker' result = self.index_view.current_page_url() result_parts = urlparse(result).query.split('&') expected = "sort_dir=desc&marker=foo_marker&search="\ "foo_search&order_by=foo_col".split('&') self.assertEqual('foo_curr_url', urlparse(result).path) self.assertEqual(sorted(expected), sorted(result_parts)) @mock.patch.object(views, 'reverse') def test_prev_page_url(self, mock_reverse): mock_reverse.return_value = 'foo_prev_url' self.index_view.get_marker = lambda i: 'foo_marker' result = self.index_view.prev_page_url() result_parts = urlparse(result).query.split('&') expected = "sort_dir=desc&marker=foo_marker&search="\ "foo_search&order_by=foo_col".split('&') self.assertEqual('foo_prev_url', urlparse(result).path) self.assertEqual(sorted(expected), sorted(result_parts)) @mock.patch.object(views, 'reverse') def test_next_page_url(self, mock_reverse): mock_reverse.return_value = 'foo_next_url' self.index_view.get_marker = lambda: 'foo_marker' result = self.index_view.next_page_url() result_parts = urlparse(result).query.split('&') expected = "sort_dir=asc&marker=foo_marker&search="\ "foo_search&order_by=foo_col".split('&') self.assertEqual('foo_next_url', urlparse(result).path) self.assertEqual(sorted(expected), sorted(result_parts)) @mock.patch.object(views, 'reverse') @mock.patch.object(views, 'get_environments_context') @mock.patch.object(views, 'cleaned_latest_apps') @mock.patch.object(views, 'get_categories_list') def test_get_context_data(self, mock_get_categories_list, mock_cleaned_latest_apps, mock_get_environments_context, mock_reverse): mock_get_categories_list.return_value = [ 'foo_category', 'bar_category' ] mock_cleaned_latest_apps.return_value = ['foo_app', 'bar_app'] mock_get_environments_context.return_value = {} mock_reverse.return_value = 'foo_url' mock_token = mock.Mock(tenant={'id': 'foo_tenant_id'}) setattr(settings, 'MURANO_USE_GLARE', True) self.index_view.request.session = {'token': mock_token} self.index_view.object_list = [] context_data = self.index_view.get_context_data() expected = { 'ALL_CATEGORY_NAME': 'All', 'MURANO_USE_GLARE': True, 'categories': ['foo_category', 'bar_category'], 'current_category': 'foo_category', 'display_repo_url': 'http://apps.openstack.org/#tab=murano-apps', 'is_paginated': None, 'latest_list': ['foo_app', 'bar_app'], 'no_apps': False, 'object_list': [], 'page_obj': None, 'paginator': None, 'pkg_def_url': 'foo_url', 'search': 'foo_search', 'tenant_id': 'foo_tenant_id', 'view': self.index_view } for key, val in expected.items(): self.assertEqual(val, context_data[key]) mock_reverse.assert_called_once_with( 'horizon:app-catalog:packages:index') class TestAppDetailsView(testtools.TestCase): def setUp(self): super(TestAppDetailsView, self).setUp() self.app_details_view = views.AppDetailsView() self.app_details_view.request = mock.Mock(GET={}) self.app_details_view.request.user.service_catalog = ['foo_service'] self.assertEqual(catalog_tabs.ApplicationTabs, self.app_details_view.tab_group_class) self.assertEqual('catalog/app_details.html', self.app_details_view.template_name) self.assertEqual('{{ app.name }}', self.app_details_view.page_title) self.assertIsNone(self.app_details_view.app) @mock.patch.object(views, 'api') def test_get_data(self, mock_api): mock_api.muranoclient().packages.get.return_value = 'foo_app' kwargs = {'application_id': 'foo_app_id'} app = self.app_details_view.get_data(**kwargs) self.assertEqual('foo_app', app) self.assertEqual('foo_app', self.app_details_view.app) mock_api.muranoclient().packages.get.assert_called_with( 'foo_app_id') @mock.patch.object(views, 'api') @mock.patch.object(views, 'get_environments_context') def test_get_context_data(self, mock_get_environments_context, mock_api): mock_api.muranoclient().packages.get.return_value = 'foo_app' mock_get_environments_context.return_value = {} context = self.app_details_view.get_context_data() expected = {'app': 'foo_app'} for key, val in expected.items(): self.assertEqual(val, context[key]) @mock.patch.object(views, 'api') def test_get_tabs(self, mock_api): mock_api.muranoclient().packages.get.return_value = 'foo_app' kwargs = {'application_id': 'foo_app_id'} tabs = self.app_details_view.get_tabs(self.app_details_view.request, **kwargs) self.assertIsInstance(tabs, catalog_tabs.ApplicationTabs) expected_kwargs = { 'application': 'foo_app', 'application_id': 'foo_app_id'} for key, val in expected_kwargs.items(): self.assertEqual(val, tabs.kwargs[key]) murano-dashboard-5.0.0/muranodashboard/tests/unit/test_api.py0000666000175100017510000001324213245511125024445 0ustar zuulzuul00000000000000# Copyright (c) 2016 AT&T 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. import mock from django.conf import settings from muranoclient.v1 import client from muranodashboard import api from openstack_dashboard.test import helpers class TestApi(helpers.APITestCase): def setUp(self): super(TestApi, self).setUp() factory = helpers.RequestFactoryWithMessages() self.request = factory.get('/path/for/testing') self.addCleanup(mock.patch.stopall) @mock.patch.object(api, 'LOG') def test_handled_exceptions(self, mock_log): handled_exceptions = ( (api.exc.CommunicationError, 'Unable to communicate to murano-api server.'), (api.glance_exc.CommunicationError, 'Unable to communicate to glare-api server.'), (api.exc.HTTPUnauthorized, 'Check Keystone configuration of murano-api server.'), (api.exc.HTTPForbidden, 'Operation is forbidden by murano-api server.'), (api.exc.HTTPNotFound, 'Requested object is not found on murano server.'), (api.exc.HTTPConflict, 'Requested operation conflicts with an existing object.'), (api.exc.BadRequest, 'The request data is not acceptable by the server'), (api.exc.HTTPInternalServerError, 'There was an error communicating with server'), (api.glance_exc.HTTPInternalServerError, 'There was an error communicating with server'), ) for (exception, expected_message) in handled_exceptions: try: with api.handled_exceptions(self.request): raise exception() except exception: pass mock_log.exception.assert_called_once_with(expected_message) mock_log.exception.reset_mock() @mock.patch.object(api, 'LOG') def test_handled_exceptions_with_details(self, mock_log): exceptions_with_details = ( (api.exc.HTTPInternalServerError, 'There was an error communicating with server'), (api.glance_exc.HTTPInternalServerError, 'There was an error communicating with server') ) for (exception, expected_message) in exceptions_with_details: try: with api.handled_exceptions(self.request): raise exception(details='test_details') except exception: pass mock_log.exception.assert_called_once_with(expected_message) mock_log.exception.reset_mock() @mock.patch.object(api, 'msg_api') @mock.patch.object(api, 'exceptions') def test_handled_exceptions_with_message_already_queued(self, mock_exc, mock_msg_api): mock_message = mock.Mock( message='Unable to communicate to murano-api server.') mock_msg_api.get_messages.return_value = [mock_message] try: with api.handled_exceptions(self.request): raise api.exc.CommunicationError() except api.exc.CommunicationError: pass mock_exc.handle.assert_called_once_with(self.request, ignore=True) @mock.patch.object(api, 'exceptions') def test_handled_exceptions_with_ajax_request(self, mock_exc): async_messages = [('test_tag', 'Unable to communicate to murano-api server.', 'test_extra')] mock_request = mock.MagicMock() mock_request.is_ajax.return_value = True mock_request.horizon.__getitem__.return_value = async_messages try: with api.handled_exceptions(mock_request): raise api.exc.CommunicationError() except api.exc.CommunicationError: pass mock_exc.handle.assert_called_once_with(mock_request, ignore=True) self.assertTrue(mock_request.is_ajax.called) self.assertTrue(mock_request.horizon.__getitem__.called) def test_muranoclient(self): muranoclient = api.muranoclient(self.request) self.assertIsNotNone(muranoclient) self.assertEqual(client.Client, type(muranoclient)) @mock.patch.object(api, 'LOG') @mock.patch('openstack_dashboard.api.base') def test_muranoclient_override_endpoints(self, mock_base, mock_log): mock_base.url_for = mock.MagicMock( side_effect=api.exceptions.ServiceCatalogException) setattr(settings, 'MURANO_USE_GLARE', True) setattr(settings, 'MURANO_API_URL', None) setattr(settings, 'GLARE_API_URL', None) muranoclient = api.muranoclient(self.request) self.assertIsNotNone(muranoclient) self.assertEqual(client.Client, type(muranoclient)) self.assertEqual(2, mock_log.warning.call_count) mock_log.warning.assert_any_call( 'Murano API location could not be found in Service ' 'Catalog, using default: http://localhost:8082') mock_log.warning.assert_any_call( 'Glare API location could not be found in Service ' 'Catalog, using default: http://localhost:9494') murano-dashboard-5.0.0/muranodashboard/tests/functional/0000775000175100017510000000000013245511556023452 5ustar zuulzuul00000000000000murano-dashboard-5.0.0/muranodashboard/tests/functional/DeployingApp/0000775000175100017510000000000013245511556026045 5ustar zuulzuul00000000000000murano-dashboard-5.0.0/muranodashboard/tests/functional/DeployingApp/Classes/0000775000175100017510000000000013245511556027442 5ustar zuulzuul00000000000000murano-dashboard-5.0.0/muranodashboard/tests/functional/DeployingApp/Classes/mock_muranopl.yaml0000666000175100017510000000064113245511125033167 0ustar zuulzuul00000000000000Namespaces: =: io.murano.apps std: io.murano Name: DeployingApp Extends: std:Application Properties: name: Contract: $.string().notNull() Methods: testAction: Usage: Action Body: - sleep(3) - $this.find(std:Environment).reporter.report($this, 'Completed') deploy: Body: - sleep(30) - $this.find(std:Environment).reporter.report($this, 'Follow the white rabbit') murano-dashboard-5.0.0/muranodashboard/tests/functional/DeployingApp/UI/0000775000175100017510000000000013245511556026362 5ustar zuulzuul00000000000000murano-dashboard-5.0.0/muranodashboard/tests/functional/DeployingApp/UI/mock_ui.yaml0000666000175100017510000000713013245511125030667 0ustar zuulzuul00000000000000#Note: it is a fake application, it isn't intended to be deployed Version: 2 Application: ?: type: io.murano.apps.DeployingApp name: $.appConfiguration.name Forms: - appConfiguration: fields: - name: title type: string required: false hidden: true description: >- Fields with different types are presented in this step - name: domain type: string required: false label: String - Domain Name description: >- Requirements: only A-Z, a-z, 0-9, (.) and (-) and should not end with a dash. Note: Only first 15 characters or characters before first period is used as NetBIOS name, min/max length are defined minLength: 2 maxLength: 255 validators: - expr: regexpValidator: '^([0-9A-Za-z]|[0-9A-Za-z][0-9A-Za-z-]*[0-9A-Za-z])\.[0-9A-Za-z][0-9A-Za-z-]*[0-9A-Za-z]$' message: >- Only letters, numbers and dashes in the middle are allowed. Period characters are allowed only when they are used to delimit the components of domain style names. Single-level domain is not appropriate. Subdomains are not allowed. - expr: regexpValidator: '(^[^.]+$|^[^.]{1,15}\..*$)' message: >- NetBIOS name cannot be shorter than 1 symbol and longer than 15 symbols. - expr: regexpValidator: '(^[^.]+$|^[^.]*\.[^.]{2,63}.*$)' message: >- DNS host name cannot be shorter than 2 symbols and longer than 63 symbols. helpText: >- Just letters, numbers and dashes are allowed. A dot can be used to create subdomains - name: name type: string label: Application Name description: >- Requirements: Just A-Z, a-z, 0-9, dash and underline are allowed, min/max value are defined. minLength: 2 maxLength: 12 regexpValidator: '^[-\w]+$' errorMessages: invalid: Just letters, numbers, underscores and hyphens are allowed. helpText: Just letters, numbers, underscores and hyphens are allowed. - name: integer type: integer required: false label: Integer - Instance Count description: >- Integer field, min/max value are provided minValue: 1 maxValue: 100 helpText: Enter an integer value between 1 and 100 - name: adminPassword type: password required: false label: Password - Administrator password descriptionTitle: Passwords description: >- Requirements: at least one letter in each register, a number and a special character. Password length should be a minimum of 7 characters. - name: recoveryPassword type: password required: false label: Recovery password - bindedApps: fields: - name: DeployingApp type: io.murano.apps.DeployingApp required: false - instanceConfiguration: fields: - name: flavor type: flavor label: Instance flavor required: false - name: osImage type: image imageType: linux label: Instance image - name: availabilityZone type: azone label: Availability zone required: false murano-dashboard-5.0.0/muranodashboard/tests/functional/base.py0000666000175100017510000006564613245511125024751 0ustar zuulzuul00000000000000# 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 import json import logging import os import six import six.moves.urllib.parse as urlparse import testtools import time import uuid from glanceclient import client as gclient from keystoneauth1.identity import v3 from keystoneauth1 import session as ks_session from keystoneclient.v3 import client as ks_client from muranoclient.common import exceptions as muranoclient_exc from muranoclient.glance import client as glare_client import muranoclient.v1.client as mclient from oslo_log import handlers from oslo_log import log from selenium.common import exceptions as exc from selenium import webdriver from selenium.webdriver.common import action_chains import selenium.webdriver.common.by as by from selenium.webdriver.support import expected_conditions as EC from selenium.webdriver.support import ui import config.config as cfg from muranodashboard.tests.functional import consts from muranodashboard.tests.functional import utils logger = log.getLogger(__name__).logger logger.level = logging.DEBUG logger.addHandler(handlers.ColorHandler()) class UITestCase(testtools.TestCase): @classmethod def setUpClass(cls): auth = v3.Password(user_domain_name='Default', username=cfg.common.user, password=cfg.common.password, project_domain_name='Default', project_name=cfg.common.tenant, auth_url=cfg.common.keystone_url) session = ks_session.Session(auth=auth) cls.keystone_client = ks_client.Client(session=session) cls.auth_ref = auth.get_auth_ref(session) cls.service_catalog = cls.auth_ref.service_catalog if utils.glare_enabled(): glare_endpoint = "http://127.0.0.1:9494" artifacts_client = glare_client.Client( endpoint=glare_endpoint, token=cls.auth_ref.auth_token, insecure=False, key_file=None, ca_file=None, cert_file=None, type_name="murano", type_version=1) else: artifacts_client = None cls.murano_client = mclient.Client( artifacts_client=artifacts_client, endpoint_override=cfg.common.murano_url, session=session) cls.url_prefix = urlparse.urlparse(cfg.common.horizon_url).path or '' if cls.url_prefix.endswith('/'): cls.url_prefix = cls.url_prefix[:-1] def setUp(self): super(UITestCase, self).setUp() # Enables zip files to be automatically saved to disk, without opening # a browser dialog. fp = webdriver.FirefoxProfile() fp.set_preference("browser.download.folderList", 2) fp.set_preference("browser.download.manager.showWhenStarting", False) fp.set_preference("browser.download.dir", os.getcwd()) fp.set_preference("browser.helperApps.neverAsk.saveToDisk", "application/octet-stream") self.driver = webdriver.Firefox(firefox_profile=fp) self.addCleanup(self.driver.quit) self.driver.maximize_window() self.driver.get(cfg.common.horizon_url + '/app-catalog/environments') self.driver.implicitly_wait(30) self.addOnException(self.take_screenshot) self.log_in() self.switch_to_project(cfg.common.tenant) def tearDown(self): super(UITestCase, self).tearDown() for env in self.murano_client.environments.list(): self.remove_environment(env.id) def gen_random_resource_name(self, prefix=None, reduce_by=None): random_name = str(uuid.uuid4()).replace('-', '')[::reduce_by] if prefix: random_name = prefix + '_' + random_name return random_name def remove_environment(self, environment_id, timeout=180): self.murano_client.environments.delete(environment_id) start_time = time.time() while time.time() - start_time < timeout: try: self.murano_client.environments.get(environment_id) time.sleep(1) except Exception: # TODO(smurashov): bug/1378764 replace Exception to NotFound return raise Exception( 'Environment {0} was not deleted in {1} seconds'.format( environment_id, timeout)) @classmethod def create_user(cls, name, password=None, email=None, tenant_id=None): if tenant_id is None: projects = cls.keystone_client.projects.list() tenant_id = [project.id for project in projects if project.name == cfg.common.tenant][0] cls.keystone_client.users.create(name, domain='default', password=password, email=email, project=tenant_id, enabled=True) else: cls.keystone_client.users.create(name, domain='default', password=password, email=email, project=tenant_id, enabled=True) roles = cls.keystone_client.roles.list() role_id = [role.id for role in roles if role.name == 'Member'][0] users = cls.keystone_client.users.list() user_id = [user.id for user in users if user.name == name][0] cls.keystone_client.roles.grant(role_id, user=user_id, project=tenant_id) @classmethod def delete_user(cls, name): cls.keystone_client.users.find(name=name).delete() def get_tenantid_by_name(self, name): """Returns TenantID of the project by project's name""" tenant_id = [tenant.id for tenant in self.keystone_client.projects.list() if tenant.name == name] return tenant_id[0] def add_user_to_project(self, project_id, user_id, user_role=None): if not user_role: roles = self.keystone_client.roles.list() role_id = [role.id for role in roles if role.name == 'Member'][0] if not user_id: user_name = cfg.common.user users = self.keystone_client.users.list() user_id = [user.id for user in users if user.name == user_name][0] self.keystone_client.roles.grant(role_id, user=user_id, project=project_id) def switch_to_project(self, name): projects_xpath = ("//ul[contains(@class, navbar-nav)]" "//li[contains(@class, dropdown)]") name_xpath = ("//a//span[contains(@class, dropdown-title)" "and normalize-space(text())='{0}']".format(name)) btn_xpath = "//a[contains(@class, dropdown-toggle) and @href='#']" projects_list = self.driver.find_element_by_xpath(projects_xpath) if projects_list.text != name: if 'open' not in projects_list.get_attribute('class'): projects_list.find_element_by_xpath(btn_xpath).click() projects_list.find_element_by_xpath(name_xpath).click() self.wait_for_alert_message() # else the project is already set def take_screenshot(self, exception): """Taking screenshot on error This decorators will take a screenshot of the browser when the test failed or when exception raised on the test. Screenshot will be saved as PNG inside screenshots folder. """ name = self._testMethodName logger.error('{0} failed'.format(name)) screenshot_dir = './screenshots' if not os.path.exists(screenshot_dir): os.makedirs(screenshot_dir) filename = os.path.join(screenshot_dir, name + '.png') self.driver.get_screenshot_as_file(filename) def log_in(self, username=None, password=None): username = username or cfg.common.user password = password or cfg.common.password self.fill_field(by.By.ID, 'id_username', username) self.fill_field(by.By.ID, 'id_password', password) self.driver.find_element_by_xpath("//button[@type='submit']").click() murano = self.driver.find_element_by_xpath(consts.AppCatalog) if 'collapsed' in murano.get_attribute('class'): murano.click() def log_out(self): user_menu = self.driver.find_element( by.By.XPATH, "//ul[contains(@class, 'navbar-right')]") user_menu.find_element( by.By.XPATH, ".//span[@class='user-name']").click() user_menu.find_element(by.By.PARTIAL_LINK_TEXT, 'Sign Out').click() def fill_field(self, by_find, field, value): self.check_element_on_page(by_find, field) self.wait_element_is_clickable(by_find, field) self.driver.find_element(by=by_find, value=field).clear() self.driver.find_element(by=by_find, value=field).send_keys(value) def get_element_id(self, el_name, sec=10): el = ui.WebDriverWait(self.driver, sec).until( EC.presence_of_element_located( (by.By.XPATH, consts.AppPackages.format(el_name)))) path = el.get_attribute("id") return path.split('__')[-1] def select_and_click_action_for_app(self, action, app): self.check_element_on_page( by.By.XPATH, "//*[@href='{0}/app-catalog/catalog/{1}/{2}']".format( self.url_prefix, action, app)) self.wait_element_is_clickable( by.By.XPATH, "//*[@href='{0}/app-catalog/catalog/{1}/{2}']".format( self.url_prefix, action, app)).click() def go_to_submenu(self, link): element = self.wait_element_is_clickable(by.By.PARTIAL_LINK_TEXT, link) element.click() self.wait_for_sidebar_is_loaded() def check_panel_is_present(self, panel_name): self.assertIn(panel_name, self.driver.find_element_by_xpath( ".//*[@class='page-header']").text) def navigate_to(self, menu): el = self.wait_element_is_clickable( by.By.XPATH, getattr(consts, menu)) if 'collapsed' in el.get_attribute('class'): el.click() def select_from_list(self, list_name, value, sec=10): locator = (by.By.XPATH, "//select[contains(@name, '{0}')]" "/option[@value='{1}']".format(list_name, value)) el = ui.WebDriverWait(self.driver, sec).until( EC.presence_of_element_located(locator)) el.click() def check_element_on_page(self, method, value, sec=10): try: ui.WebDriverWait(self.driver, sec).until( EC.presence_of_element_located((method, value))) except exc.TimeoutException: self.fail("Element {0} is not present on the page".format(value)) def check_element_not_on_page(self, method, value, sec=3): self.driver.implicitly_wait(sec) present = True try: self.driver.find_element(method, value) except (exc.NoSuchElementException, exc.ElementNotVisibleException): present = False self.assertFalse(present, u"Element {0} is present on the page" " while it should't".format(value)) self.driver.implicitly_wait(30) def check_alert_message(self, message, sec=10): locator = (by.By.CSS_SELECTOR, 'div.alert-dismissable') try: ui.WebDriverWait(self.driver, sec).until( EC.presence_of_element_located(locator)) except exc.TimeoutException: self.fail("Alert is not present on the page") self.assertIn(message, self.driver.find_element(*locator).text) def create_environment(self, env_name, by_id=False): if by_id: self.driver.find_element_by_id( 'environments__action_CreateEnvironment').click() else: self.driver.find_element_by_css_selector( consts.CreateEnvironment).click() self.fill_field(by.By.ID, 'id_name', env_name) self.driver.find_element_by_id(consts.ConfirmCreateEnvironment).click() self.wait_for_alert_message() def delete_environment(self, env_name, from_detail_view=False): if not from_detail_view: self.select_action_for_environment(env_name, 'delete') self.driver.find_element_by_xpath(consts.ConfirmDeletion).click() self.wait_for_alert_message() def edit_environment(self, old_name, new_name): el_td = self.driver.find_element_by_css_selector( 'tr[data-display="{0}"] '.format(old_name) + 'td[data-cell-name="name"]') el_pencil = el_td.find_element_by_css_selector( 'button.ajax-inline-edit') # hover to make pencil visible hover = action_chains.ActionChains(self.driver).move_to_element(el_td) hover.perform() el_pencil.click() # fill in inline input el_inline_input = self.driver.find_element_by_css_selector( 'tr[data-display="{0}"] '.format(old_name) + 'td[data-cell-name="name"] .inline-edit-form input') el_inline_input.clear() el_inline_input.send_keys(new_name) # click submit el_submit = self.driver.find_element_by_css_selector( 'tr[data-display="{0}"] '.format(old_name) + 'td[data-cell-name="name"] .inline-edit-actions' + ' button[type="submit"]') el_submit.click() # there is no alert message def select_action_for_environment(self, env_name, action): element_id = self.get_element_id(env_name) more_button = consts.More.format('environments', element_id) self.driver.find_element_by_xpath(more_button).click() btn_id = "environments__row_{0}__action_{1}".format(element_id, action) self.driver.find_element_by_id(btn_id).click() def wait_for_alert_message(self, sec=5): locator = (by.By.CSS_SELECTOR, 'div.alert-success') logger.debug("Waiting for a success message") ui.WebDriverWait(self.driver, sec).until( EC.presence_of_element_located(locator)) def wait_for_alert_message_to_disappear(self, sec=10): # The alert message pops up directly over the delete environment # button, causing click issues. So we must wait for the alert message # to completely disappear before clicking the button. locator = (by.By.CSS_SELECTOR, 'div.alert-success') logger.debug("Waiting for a success message to disappear") ui.WebDriverWait(self.driver, sec).until( EC.invisibility_of_element_located(locator)) def wait_for_error_message(self, sec=20): locator = (by.By.CSS_SELECTOR, 'div.alert-danger > p') logger.debug("Waiting for an error message") ui.WebDriverWait(self.driver, sec, 1).until( EC.presence_of_element_located(locator)) return self.driver.find_element(*locator).text def wait_element_is_clickable(self, method, element, sec=10): return ui.WebDriverWait(self.driver, sec).until( EC.element_to_be_clickable((method, element))) def wait_for_sidebar_is_loaded(self, sec=10): ui.WebDriverWait(self.driver, sec).until( EC.presence_of_element_located( (by.By.CSS_SELECTOR, "nav#sidebar a.active"))) time.sleep(0.5) @contextlib.contextmanager def wait_for_page_reload(self, sec=10): old_page = self.driver.find_element_by_tag_name('html') yield ui.WebDriverWait(self, sec).until(EC.staleness_of(old_page)) class PackageBase(UITestCase): @classmethod def setUpClass(cls): super(PackageBase, cls).setUpClass() cls.packages = [] cls.mockapp_id = cls.upload_package( "MockApp", {"categories": ["Web"], "tags": ["tag"]}) cls.postgre_id = cls.upload_package( "PostgreSQL", {"categories": ["Databases"], "tags": ["tag"]}) cls.hot_app_id = cls.upload_package( "HotExample", {"tags": ["hot"]}, hot=True) cls.deployingapp_id = cls.upload_package( "DeployingApp", {"categories": ["Web"], "tags": ["tag"]}, hot=False, package_dir=consts.DeployingPackageDir) @classmethod def upload_package(cls, name, data, **kwargs): package = utils.upload_app_package(cls.murano_client, name, data, **kwargs) cls.packages.append(package) return package @classmethod def tearDownClass(cls): super(PackageBase, cls).tearDownClass() # In case dynamically created packages are deleted at test level, # ignore not found errors below. for package in cls.packages: try: cls.murano_client.packages.delete(package) except muranoclient_exc.HTTPNotFound: pass class ImageTestCase(PackageBase): @classmethod def setUpClass(cls): super(ImageTestCase, cls).setUpClass() glance_endpoint = cls.service_catalog.url_for(service_type='image') cls.glance = gclient.Client('2', endpoint=glance_endpoint, session=cls.keystone_client.session) def setUp(self): super(ImageTestCase, self).setUp() self.image_title = self.gen_random_resource_name('default-image', 15) self.image = self.upload_image(self.image_title) def tearDown(self): super(ImageTestCase, self).tearDown() self.glance.images.delete(self.image.id) @classmethod def upload_image(cls, title): try: murano_property = json.dumps({'title': title, 'type': 'linux'}) image = cls.glance.images.create(name='TestImage', disk_format='qcow2', container_format='bare', is_public='True', murano_image_info=murano_property) image_data = six.BytesIO(None) cls.glance.images.upload(image['id'], image_data) except Exception: logger.error("Unable to create or update image in Glance") raise return image def select_and_click_element(self, element): self.driver.find_element_by_xpath( ".//*[@value = '{0}']".format(element)).click() class FieldsTestCase(PackageBase): def check_js_error_message_is_present(self, error_message): self.driver.implicitly_wait(2) self.driver.find_element_by_xpath( consts.JsErrorMessage.format(error_message)) self.driver.implicitly_wait(30) def check_js_error_message_is_absent(self, error_message): self.driver.implicitly_wait(2) try: self.driver.find_element_by_xpath( consts.ErrorMessage.format(error_message)) except (exc.NoSuchElementException, exc.ElementNotVisibleException): logger.info("Message {0} is not" " present on the page".format(error_message)) self.driver.implicitly_wait(30) def check_error_message_is_present(self, error_message): self.driver.find_element_by_xpath(consts.ButtonSubmit).click() self.driver.find_element_by_xpath( consts.ErrorMessage.format(error_message)) def check_error_message_is_absent(self, error_message): self.driver.find_element_by_xpath(consts.ButtonSubmit).click() self.driver.implicitly_wait(2) try: self.driver.find_element_by_xpath( consts.ErrorMessage.format(error_message)) except (exc.NoSuchElementException, exc.ElementNotVisibleException): logger.info("Message {0} is not" " present on the page".format(error_message)) self.driver.implicitly_wait(30) class ApplicationTestCase(ImageTestCase): def delete_component(self, component_name=None): if component_name: component_id = self.get_element_id(component_name) btn = self.wait_element_is_clickable( by.By.ID, 'services__row_{0}__action_delete'.format( component_id)) else: btn = self.wait_element_is_clickable(by.By.CSS_SELECTOR, consts.DeleteComponent) btn.click() el = self.wait_element_is_clickable(by.By.LINK_TEXT, 'Delete Component', sec=30) el.click() self.wait_for_alert_message() def select_action_for_package(self, package_id, action, sec=10): if action == 'more': el = self.wait_element_is_clickable( by.By.XPATH, "//tr[@data-object-id='{0}']" "//a[@data-toggle='dropdown']".format(package_id)) el.click() ui.WebDriverWait(self.driver, sec).until(lambda s: s.find_element( by.By.XPATH, ".//*[@id='packages__row_{0}__action_download_package']". format(package_id)).is_displayed()) else: self.driver.find_element_by_xpath( ".//*[@id='packages__row_{0}__action_{1}']". format(package_id, action)).click() def check_package_parameter(self, selector, column, value): columns = {'Tenant Name': 3, 'Active': 4, 'Public': 5} column_num = str(columns[column]) column_element = self.driver.find_element_by_xpath( "//tr[{0}]/td[{1}]".format(selector, column_num)) self.assertTrue(column_element.text == value, "'{0}' column doesn't contain '{1}'".format(column, value)) def check_package_parameter_by_id(self, package_id, column, value): selector = '@data-object-id="{0}"'.format(package_id) self.check_package_parameter(selector, column, value) def check_package_parameter_by_name(self, package_name, column, value): selector = '@data-display="{0}"'.format(package_name) self.check_package_parameter(selector, column, value) def modify_package(self, param, value): self.fill_field(by.By.ID, 'id_{0}'.format(param), value) self.driver.find_element_by_xpath(consts.InputSubmit).click() self.wait_for_alert_message() def add_app_to_env(self, app_id, app_name='TestApp', env_id=None): self.navigate_to('Browse') self.go_to_submenu('Browse Local') if env_id: action = 'add' app = '{0}/{1}'.format(app_id, env_id) else: action = 'quick-add' app = app_id self.select_and_click_action_for_app(action, app) field_id = "{0}_0-name".format(app_id) self.fill_field(by.By.ID, field_id, value=app_name) self.wait_element_is_clickable(by.By.XPATH, consts.ButtonSubmit).click() self.wait_element_is_clickable(by.By.XPATH, consts.InputSubmit).click() self.select_from_list('osImage', self.image.id) if env_id: # If another app is added, then env_id is passed in. In this case, # the 'Next' followed by 'Create' must be clicked. self.check_element_on_page(by.By.CSS_SELECTOR, consts.NextWizardSubmit) self.wait_element_is_clickable( by.By.CSS_SELECTOR, consts.NextWizardSubmit).click() self.check_element_on_page(by.By.CSS_SELECTOR, consts.CreateWizardSubmit) self.wait_element_is_clickable( by.By.CSS_SELECTOR, consts.CreateWizardSubmit).click() self.wait_element_is_clickable(by.By.ID, consts.AddComponent) self.check_element_on_page(by.By.LINK_TEXT, app_name) else: # Otherwise, only 'Create' needs to be clicked. self.check_element_on_page(by.By.CSS_SELECTOR, consts.CreateWizardSubmit) self.wait_element_is_clickable( by.By.CSS_SELECTOR, consts.CreateWizardSubmit).click() self.wait_for_alert_message() def execute_action_from_table_view(self, env_name, table_action): """Executes an action like Deploy or Delete from the table view. Does not handle clicking on the confirmation modal that may appear. Scenario: 1. Checks for the table drop-down button and then clicks it. 2. Checks for the table drop-down menu to appear. 3. Checks for the ``table_action`` button and then clicks it. """ self.check_element_on_page( by.By.XPATH, consts.TableDropdownBtn.format(env_name)) dropdown_btn = self.driver.find_element( by.By.XPATH, consts.TableDropdownBtn.format(env_name)) dropdown_btn.click() self.check_element_on_page(by.By.XPATH, consts.TableDropdownMenu.format(env_name)) self.check_element_on_page( by.By.XPATH, consts.TableDropdownAction.format(env_name, table_action)) action_btn = self.driver.find_element( by.By.XPATH, consts.TableDropdownAction.format(env_name, table_action)) action_btn.click() class PackageTestCase(ApplicationTestCase): @classmethod def setUpClass(cls): super(ApplicationTestCase, cls).setUpClass() cls.archive_name = "ToUpload" cls.alt_archive_name = "ModifiedAfterUpload" cls.manifest = os.path.join(consts.PackageDir, 'manifest.yaml') cls.archive = utils.compose_package(cls.archive_name, cls.manifest, consts.PackageDir) def tearDown(self): super(PackageTestCase, self).tearDown() for package in self.murano_client.packages.list(include_disabled=True): if package.name in [self.archive_name, self.alt_archive_name]: self.murano_client.packages.delete(package.id) @classmethod def tearDownClass(cls): super(ApplicationTestCase, cls).tearDownClass() if os.path.exists(cls.manifest): os.remove(cls.manifest) if os.path.exists(cls.archive): os.remove(cls.archive) murano-dashboard-5.0.0/muranodashboard/tests/functional/__init__.py0000666000175100017510000000000013245511125025543 0ustar zuulzuul00000000000000murano-dashboard-5.0.0/muranodashboard/tests/functional/config/0000775000175100017510000000000013245511556024717 5ustar zuulzuul00000000000000murano-dashboard-5.0.0/muranodashboard/tests/functional/config/config.py0000666000175100017510000000372313245511125026535 0ustar zuulzuul00000000000000# 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 from oslo_config import cfg murano_group = cfg.OptGroup(name='murano', title="murano configs") MuranoGroup = [ cfg.StrOpt('horizon_url', default='http://127.0.0.1/horizon', help="murano dashboard url"), cfg.StrOpt('user', default='admin', help="keystone user"), cfg.StrOpt('password', default='pass', help="password for keystone user"), cfg.StrOpt('tenant', default='admin', help='keystone tenant'), cfg.StrOpt('keystone_url', default='http://localhost:5000/v2.0/', help='keystone url'), cfg.StrOpt('murano_url', default='http://127.0.0.1:8082', help='murano url'), cfg.IntOpt('items_per_page', default=20, help='items per page displayed'), cfg.StrOpt('packages_service', default='murano', help='murano packages service, either "murano" or "glare"'), ] def register_config(config, config_group, config_opts): config.register_group(config_group) config.register_opts(config_opts, config_group) path = os.path.join(os.path.dirname(__file__), "config.conf") if os.path.exists(path): cfg.CONF([], project='muranodashboard', default_config_files=[path]) register_config(cfg.CONF, murano_group, MuranoGroup) common = cfg.CONF.murano murano-dashboard-5.0.0/muranodashboard/tests/functional/config/__init__.py0000666000175100017510000000000013245511125027010 0ustar zuulzuul00000000000000murano-dashboard-5.0.0/muranodashboard/tests/functional/config/config.conf.sample0000666000175100017510000000027613245511125030312 0ustar zuulzuul00000000000000[murano] horizon_url = http://127.0.0.1/horizon murano_url = http://127.0.0.1:8082 user = WebTestUser password = swordfish tenant = WebTestProject keystone_url = http://127.0.0.1:5000/v2.0/ murano-dashboard-5.0.0/muranodashboard/tests/functional/utils.py0000666000175100017510000001106113245511125025155 0ustar zuulzuul00000000000000# 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 import logging import os import yaml import zipfile from oslo_log import log from selenium.webdriver.support import wait import config.config as cfg from muranodashboard.tests.functional import consts log = log.getLogger(__name__).logger log.setLevel(logging.DEBUG) log.addHandler(logging.StreamHandler()) MANIFEST = {'Format': 'MuranoPL/1.0', 'Type': 'Application', 'Description': 'MockApp for webUI tests', 'Author': 'Mirantis, Inc', 'UI': 'mock_ui.yaml'} class ImageException(Exception): message = "Image doesn't exist" def __init__(self, im_type): self._error_string = (self.message + '\nDetails: {0} image is ' 'not found,'.format(im_type)) def __str__(self): return self._error_string def upload_app_package(client, app_name, data, hot=False, package_dir=consts.PackageDir, **manifest_kwargs): try: if not hot: manifest = os.path.join(package_dir, 'manifest.yaml') archive = compose_package(app_name, manifest, package_dir, **manifest_kwargs) else: manifest = os.path.join(consts.HotPackageDir, 'manifest.yaml') archive = compose_package(app_name, manifest, consts.HotPackageDir, hot=True, **manifest_kwargs) package = client.packages.create(data, {app_name: open(archive, 'rb')}) return package.id finally: os.remove(archive) os.remove(manifest) def compose_package(app_name, manifest, package_dir, require=None, archive_dir=None, hot=False, **kwargs): """Composes a murano package Composes package `app_name` with `manifest` file as a template for the manifest and files from `package_dir`. Includes `require` section if any in the manifest file. Puts the resulting .zip file into `acrhive_dir` if present or in the `package_dir`. """ with open(manifest, 'w') as f: fqn = 'io.murano.apps.' + app_name mfest_copy = MANIFEST.copy() mfest_copy['FullName'] = fqn mfest_copy['Name'] = app_name if hot: mfest_copy['Format'] = 'Heat.HOT/1.0' else: mfest_copy['Format'] = '1.0' mfest_copy['Classes'] = {fqn: 'mock_muranopl.yaml'} if require: mfest_copy['Require'] = require if kwargs: mfest_copy.update(kwargs) f.write(yaml.dump(mfest_copy, default_flow_style=False)) name = app_name + '.zip' if not archive_dir: archive_dir = os.path.dirname(os.path.abspath(__file__)) archive_path = os.path.join(archive_dir, name) with zipfile.ZipFile(archive_path, 'w') as zip_file: for root, dirs, files in os.walk(package_dir): for f in files: zip_file.write( os.path.join(root, f), arcname=os.path.join(os.path.relpath(root, package_dir), f) ) return archive_path def compose_bundle(bundle_path, app_names): """Composes a murano bundle. """ bundle = {'Packages': []} for app_name in app_names: bundle['Packages'].append({'Name': app_name}) with open(bundle_path, 'w') as f: f.write(json.dumps(bundle)) def glare_enabled(): return cfg.common.packages_service == "glare" class wait_for_page_change(object): """Waits until current page change in a browser.""" def __init__(self, driver, timeout=30): self.driver = driver self.timeout = timeout def __enter__(self): self.old_page = self.driver.find_element_by_tag_name('html') def page_has_loaded(self): new_page = self.driver.find_element_by_tag_name('html') return new_page.id != self.old_page.id def __exit__(self, *_): wait.WebDriverWait(self.driver, timeout=self.timeout).until( lambda _: self.page_has_loaded() ) murano-dashboard-5.0.0/muranodashboard/tests/functional/consts.py0000666000175100017510000001422213245511125025330 0ustar zuulzuul00000000000000# 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 PackageDir = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'MockApp') HotPackageDir = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'HotApp') DeployingPackageDir = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'DeployingApp') CategorySelector = "//a[contains(text(), '{0}')][contains(@class, 'dropdown-toggle')]" # noqa EnvAppsCategorySelector = "//*[contains(@id, 'envAppsCategoryBtn')]" App = "//div[contains(@class, 'app-list')]//h4[contains(text(), '{0}')]" Component = "//div[contains(@id, apps_carousel)]//div[contains(text(), '{0}')]" MockAppDescr = "//div[h4[contains(text(), 'MockApp')]]/p" AppPackages = u"//tr[@data-display='{0}']" AppDetail = "//dl[dt[contains(text(), 'Name')]]/dd" TagInDetails = "//div[contains(@class, 'app-meta')]//ul//li[strong[contains(text(), 'Tags')]]" # noqa TestImage = "//tr[td[contains(text(), '{0}')]]" DeleteImageMeta = TestImage + "//td//button[contains(text(), 'Delete Metadata')]" # noqa ImageMeta = "//dl//div[dt[contains(text(), 'murano_image_info')]]/dd" More = "//tr[contains(@id, '{0}__row__{1}')]//a[contains(@class, dropdown-toggle) and @href='#']" # noqa Status = "//td[contains(text(), '{0}')]" EnvStatus = "//tr[contains(@data-display, '{0}')]/td[contains(text(), '{1}')]" CellStatus = "//td[contains(@class, 'status_{0}')]" Row = "//tr[contains(@id, 'services__row__{0}')]" ErrorMessage = '//span[contains(@class, "help-block") and contains(text(), "{0}")]' # noqa JsErrorMessage = '//div[contains(@class, "alert-danger") and contains(text(), "{0}")]' # noqa EnvAppsCategory = "//div[contains(@class, 'draggable_app')]//div[contains(text(), '{0}')]" # noqa PackageCategory = "//select[@name='add_category-categories']/option[text()='{0}']" # noqa DatabaseCategory = "select[name='add_category-categories'] > option[value='Databases']" # noqa CategoryPackageCount = "//tr[contains(@data-display, '{0}')]/td[contains(text(), '{1}')]" # noqa Action = '//a[contains(@class, "murano_action") and contains(text(), "testAction")]' # noqa HotFlavorField = '//div[contains(@class, "has-error")]//input' EnvCheckbox = "//tr[contains(@data-display, '{0}')]/td[contains(@class, 'multi_select_column')]//div//label" # noqa NewEnvRow = "table#environments thead tr.new_env" TableSorterByName = "table#environments thead th.tablesorter-header[data-column='1']" # noqa ServiceDetail = "//dd[contains(text(), '{0}')]" ServiceType = "//table[@id='services']//tbody//tr//td[2][contains(text(), '{0}')]" # noqa TableDropdownBtn = "//tr[contains(@data-display, '{0}')]//a[contains(@class, "\ "'dropdown-toggle')]" TableDropdownMenu = "//tr[contains(@data-display, '{0}')]//div[contains("\ "@class, 'open')]" TableDropdownAction = "//tr[contains(@data-display, '{0}')]//button[contains("\ "text(), '{1}')]" # Buttons ButtonSubmit = ".//*[@name='wizard_goto_step'][2]" InputSubmit = "//input[@type='submit']" NextWizardSubmit = 'div.modal-footer input[value="Next"]' CreateWizardSubmit = 'div.modal-footer input[value="Create"]' ConfirmDeletion = "//div[@class='modal-footer']//a[contains(text(), 'Delete')]" # noqa ConfirmAbandon = "//div[@class='modal-footer']//a[contains(text(), 'Abandon')]" # noqa UploadPackage = 'packages__action_upload_package' ImportBundle = 'packages__action_import_bundle' CreateEnvironment = ".add_env .btn" DeployEnvironment = "services__action_deploy_env" DeleteEnvironment = "//button[contains(@id, 'action_delete')]" DeployEnvironments = ".btn#environments__action_deploy" DeployEnvironmentsDisabled = ".btn#environments__action_deploy[disabled]" DeleteEnvironments = ".btn#environments__action_delete" DeleteEnvironmentsDisabled = ".btn#environments__action_delete[disabled]" AbandonEnvironment = "//button[contains(text(), 'Abandon Environment')]" AbandonEnvironments = ".btn#environments__action_abandon" AbandonEnvironmentsDisabled = ".btn#environments__action_abandon[disabled]" ConfirmCreateEnvironment = 'confirm_create_env' AddComponent = "services__action_AddApplication" AddCategory = "categories__action_add_category" DeleteCategory = "//tr[td[contains(text(), '{0}')]]//button[contains(@id, 'action_delete')]" # noqa NextBtn = "//tfoot//tr//td//a[contains(@href,'?marker')]" PrevBtn = "//tfoot//tr//td//a[contains(@href,'prev_marker')]" DeleteComponent = ".btn[id^='services__row_'][id$='__action_delete']" DetailDropdownBtn = "form.detail-actions-form a.dropdown-toggle" DetailDropdownMenu = "ul.dropdown-menu" DeploymentHistoryLogTab = "//ul[contains(@id, 'environment_details')]//"\ "a[contains(text(), 'Latest Deployment Log')]" EnvComponentsTab = "//ul[contains(@id, 'environment_details')]//"\ "a[contains(text(), 'Components')]" DeploymentHistoryLogs = "div#environment_details__env_logs div.reports.logs "\ "div.report-info" PackageFilterDropdownBtn = 'div.table_search > div.themable-select.dropdown >'\ ' button' PackageFilterTypeBtn = "a[data-select-value='{0}']" PackageFilterInput = 'input[name="packages__filter_packages__q"]' PackageFilterBtn = "packages__action_filter_packages" # Panels AppCatalog = "//*[@id='main_content']/nav//a[contains(text(), 'App Catalog')]" # noqa Browse = AppCatalog + "/following::a[contains(text(), 'Browse')]" Manage = AppCatalog + "/following::a[contains(text(), 'Manage')]" Applications = AppCatalog + "/following::a[contains(text(), 'Applications')]" # noqa AlertInfo = "//*[contains(@class, 'alert-info')][contains(text(), '{0}')]" # Modals ModalDialog = ".modal-dialog" murano-dashboard-5.0.0/muranodashboard/tests/functional/MockApp/0000775000175100017510000000000013245511556025004 5ustar zuulzuul00000000000000murano-dashboard-5.0.0/muranodashboard/tests/functional/MockApp/Classes/0000775000175100017510000000000013245511556026401 5ustar zuulzuul00000000000000murano-dashboard-5.0.0/muranodashboard/tests/functional/MockApp/Classes/mock_muranopl.yaml0000666000175100017510000000061213245511125032124 0ustar zuulzuul00000000000000Namespaces: =: io.murano.apps std: io.murano Name: MockApp Extends: std:Application Properties: name: Contract: $.string().notNull() Methods: testAction: Usage: Action Body: - sleep(3) - $this.find(std:Environment).reporter.report($this, 'Completed') deploy: Body: - $this.find(std:Environment).reporter.report($this, 'Follow the white rabbit') murano-dashboard-5.0.0/muranodashboard/tests/functional/MockApp/UI/0000775000175100017510000000000013245511556025321 5ustar zuulzuul00000000000000murano-dashboard-5.0.0/muranodashboard/tests/functional/MockApp/UI/mock_ui.yaml0000666000175100017510000000711113245511125027625 0ustar zuulzuul00000000000000#Note: it is a fake application, it isn't intended to be deployed Version: 2 Application: ?: type: io.murano.apps.MockApp name: $.appConfiguration.name Forms: - appConfiguration: fields: - name: title type: string required: false hidden: true description: >- Fields with different types are presented in this step - name: domain type: string required: false label: String - Domain Name description: >- Requirements: only A-Z, a-z, 0-9, (.) and (-) and should not end with a dash. Note: Only first 15 characters or characters before first period is used as NetBIOS name, min/max length are defined minLength: 2 maxLength: 255 validators: - expr: regexpValidator: '^([0-9A-Za-z]|[0-9A-Za-z][0-9A-Za-z-]*[0-9A-Za-z])\.[0-9A-Za-z][0-9A-Za-z-]*[0-9A-Za-z]$' message: >- Only letters, numbers and dashes in the middle are allowed. Period characters are allowed only when they are used to delimit the components of domain style names. Single-level domain is not appropriate. Subdomains are not allowed. - expr: regexpValidator: '(^[^.]+$|^[^.]{1,15}\..*$)' message: >- NetBIOS name cannot be shorter than 1 symbol and longer than 15 symbols. - expr: regexpValidator: '(^[^.]+$|^[^.]*\.[^.]{2,63}.*$)' message: >- DNS host name cannot be shorter than 2 symbols and longer than 63 symbols. helpText: >- Just letters, numbers and dashes are allowed. A dot can be used to create subdomains - name: name type: string label: Application Name description: >- Requirements: Just A-Z, a-z, 0-9, dash and underline are allowed, min/max value are defined. minLength: 2 maxLength: 12 regexpValidator: '^[-\w]+$' errorMessages: invalid: Just letters, numbers, underscores and hyphens are allowed. helpText: Just letters, numbers, underscores and hyphens are allowed. - name: integer type: integer required: false label: Integer - Instance Count description: >- Integer field, min/max value are provided minValue: 1 maxValue: 100 helpText: Enter an integer value between 1 and 100 - name: adminPassword type: password required: false label: Password - Administrator password descriptionTitle: Passwords description: >- Requirements: at least one letter in each register, a number and a special character. Password length should be a minimum of 7 characters. - name: recoveryPassword type: password required: false label: Recovery password - bindedApps: fields: - name: MockApp type: io.murano.apps.MockApp required: false - instanceConfiguration: fields: - name: flavor type: flavor label: Instance flavor required: false - name: osImage type: image imageType: linux label: Instance image - name: availabilityZone type: azone label: Availability zone required: false murano-dashboard-5.0.0/muranodashboard/tests/functional/sanity_check.py0000666000175100017510000047663713245511125026511 0ustar zuulzuul00000000000000# 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 multiprocessing import os import re import shutil import SimpleHTTPServer import SocketServer import tempfile import time import unittest import uuid import zipfile from selenium.common import exceptions from selenium.webdriver.common import by from selenium.webdriver.common.keys import Keys from selenium.webdriver.support import ui from muranoclient.common import exceptions as muranoclient_exc from muranodashboard.tests.functional import base from muranodashboard.tests.functional.config import config as cfg from muranodashboard.tests.functional import consts as c from muranodashboard.tests.functional import utils class TestSuiteSmoke(base.UITestCase): """This class keeps smoke tests which check operability of main panels""" def test_smoke_environments_panel(self): self.navigate_to('Applications') self.go_to_submenu('Environments') self.check_panel_is_present('Environments') def test_smoke_applications_panel(self): self.navigate_to('Browse') self.go_to_submenu('Browse Local') self.check_panel_is_present('Browse') def test_smoke_images_panel(self): self.navigate_to('Manage') self.go_to_submenu('Images') self.check_panel_is_present('Marked Images') def test_smoke_package_definitions_panel(self): self.navigate_to('Manage') self.go_to_submenu('Packages') self.check_panel_is_present('Packages') class TestSuiteEnvironment(base.ApplicationTestCase): def test_create_delete_environment(self): """Test check ability to create and delete environment Scenario: 1. Create environment 2. Navigate to this environment 3. Go back to environment list and delete created environment """ self.navigate_to('Applications') self.go_to_submenu('Environments') self.create_environment('test_create_del_env') self.go_to_submenu('Environments') self.delete_environment('test_create_del_env') self.check_element_not_on_page(by.By.LINK_TEXT, 'test_create_del_env') def test_edit_environment(self): """Test check ability to change environment name Scenario: 1. Create environment 2. Change environment's name 3. Check that renamed environment is in environment list """ self.navigate_to('Applications') self.go_to_submenu('Environments') self.create_environment('test_edit_env') self.go_to_submenu('Environments') self.edit_environment(old_name='test_edit_env', new_name='edited_env') self.go_to_submenu('Environments') self.check_element_on_page(by.By.LINK_TEXT, 'edited_env') self.check_element_not_on_page(by.By.LINK_TEXT, 'test_edit_env') def test_edit_environment_to_empty(self): """Test gives warning message if change environment name to empty Scenario: 1. Create environment 2. Change environment's name to empty 3. Check warning message appear """ self.navigate_to('Applications') self.go_to_submenu('Environments') self.create_environment('test_edit_env') self.go_to_submenu('Environments') self.edit_environment(old_name='test_edit_env', new_name='') warning_message = 'The environment name field cannot be empty.' self.check_alert_message(warning_message) def test_create_env_from_the_catalog_page(self): """Test create environment from the catalog page Scenario: 1. Go to the Browse page 2. Press 'Create Env' 3. Make sure that it's possible to choose just created environment """ self.navigate_to('Browse') self.go_to_submenu('Browse Local') self.driver.find_elements_by_xpath( "//a[contains(text(), 'Create Env')]")[0].click() self.fill_field(by.By.ID, 'id_name', 'TestEnv') self.driver.find_element_by_xpath(c.InputSubmit).click() self.wait_for_alert_message() self.check_element_on_page( by.By.XPATH, "//div[@id='environment_switcher']/a[contains(text(), 'TestEnv')]") def test_create_and_delete_environment_with_unicode_name(self): """Test check ability to create and delete environment with unicode name Scenario: 1. Create environment with unicode name 2. Navigate to this environment 3. Go back to environment list and delete created environment """ unicode_name = u'$yaql \u2665 unicode' self.navigate_to('Applications') self.go_to_submenu('Environments') self.create_environment(unicode_name) self.go_to_submenu('Environments') self.delete_environment(unicode_name) self.check_element_not_on_page(by.By.LINK_TEXT, unicode_name) def test_check_env_name_validation(self): """Test checks validation of field that usually defines environment name Scenario: 1. Navigate to Catalog > Environments 2. Press 'Create environment' 3. Check a set of names, if current name isn't valid appropriate error message should appear """ self.navigate_to('Applications') self.go_to_submenu('Environments') self.driver.find_element_by_css_selector(c.CreateEnvironment).click() self.driver.find_element_by_id(c.ConfirmCreateEnvironment).click() error_message = 'This field is required.' self.driver.find_element_by_xpath( c.ErrorMessage.format(error_message)) self.fill_field(by.By.ID, 'id_name', ' ') self.driver.find_element_by_id(c.ConfirmCreateEnvironment).click() error_message = ('Environment name must contain at least one ' 'non-white space symbol.') self.driver.find_element_by_xpath( c.ErrorMessage.format(error_message)) def test_environment_detail_page_with_button(self): """Test check availability of delete button in environment detail Scenario: 1. Create environment 2. Go to the environment detail page 3. Check that 'Delete Environment' button is in environment detail """ # uuid.uuid4() generates random uuid env_name = str(uuid.uuid4()) self.navigate_to('Applications') self.go_to_submenu('Environments') self.create_environment(env_name) delete_environment_btn = c.DeleteEnvironment self.check_element_on_page(by.By.XPATH, delete_environment_btn) def test_delete_environment_from_detail_view(self): """Test delete environment from detail view. Scenario: 1. Create environment. 2. Go to the environment detail page. 3. Explicitly wait for alert message to disappear, because it hovers directly over Delete Environment Button. 4. Check that the environment was deleted. """ env_name = str(uuid.uuid4()) self.navigate_to('Applications') self.go_to_submenu('Environments') self.create_environment(env_name) self.wait_for_alert_message_to_disappear() self.wait_element_is_clickable(by.By.XPATH, c.DeleteEnvironment)\ .click() self.check_element_on_page(by.By.CSS_SELECTOR, c.ModalDialog) self.delete_environment(env_name, from_detail_view=True) self.go_to_submenu('Environments') self.check_element_not_on_page(by.By.LINK_TEXT, env_name) def test_abandon_environment_from_detail_view(self): """Test abandon environment from detail view. Scenario: 1. Create environment. 2. Add app to environment. 3. Go to the Applications > Environments page. 4. Select checkbox corresponding to environment under test. 5. Click 'Deploy Environments' button. 6. Click on the environment link to go to detail view. 7. Click the drop-down button. 8. Click the 'Abandon Environment' button. 9. Click the confirmation button in the modal. 10. Check that the environment is no longer present in table view. """ self.add_app_to_env(self.mockapp_id) self.navigate_to('Applications') self.go_to_submenu('Environments') self.check_element_on_page(by.By.LINK_TEXT, 'quick-env-1') self.check_element_on_page(by.By.XPATH, c.Status.format('Ready to deploy')) self.driver.find_element( by.By.XPATH, c.EnvCheckbox.format('quick-env-1')).click() self.driver.find_element(by.By.CSS_SELECTOR, c.DeployEnvironments)\ .click() self.wait_for_alert_message() self.check_element_on_page(by.By.XPATH, c.EnvStatus.format('quick-env-1', 'Ready'), sec=90) self.driver.find_element(by.By.LINK_TEXT, 'quick-env-1').click() self.check_element_on_page(by.By.CSS_SELECTOR, c.DetailDropdownBtn) self.driver.find_element_by_css_selector(c.DetailDropdownBtn)\ .click() self.check_element_on_page(by.By.CSS_SELECTOR, c.DetailDropdownMenu) self.driver.find_element(by.By.XPATH, c.AbandonEnvironment).click() self.check_element_on_page(by.By.CSS_SELECTOR, c.ModalDialog) self.driver.find_element(by.By.XPATH, c.ConfirmAbandon).click() self.wait_for_alert_message() self.check_element_not_on_page(by.By.LINK_TEXT, 'quick-env-1') def test_new_environment_sort(self): """Test check that environment during creation is not sorted Scenario: 1. Create two environments. 2. Add app to one of them and deploy it. 3. Start creating new environment. 4. Check that row with new environment is present in the table head. 5. Sort rows by name and check it again. 6. Sort rows in other direction and check it again. """ self.navigate_to('Applications') self.go_to_submenu('Environments') self.create_environment('quick-env-1') self.add_app_to_env(self.deployingapp_id) self.driver.find_element_by_id('services__action_deploy_env').click() self.go_to_submenu('Environments') self.driver.find_element_by_id( 'environments__action_CreateEnvironment').click() self.fill_field(by.By.ID, 'id_name', 'quick-env-3') self.check_element_on_page(by.By.CSS_SELECTOR, c.NewEnvRow) self.driver.find_element_by_css_selector(c.TableSorterByName).click() self.check_element_on_page(by.By.CSS_SELECTOR, c.NewEnvRow) self.driver.find_element_by_css_selector(c.TableSorterByName).click() self.check_element_on_page(by.By.CSS_SELECTOR, c.NewEnvRow) self.check_element_on_page(by.By.XPATH, c.EnvStatus.format('quick-env-2', 'Ready'), sec=90) def test_new_environments_in_one_moment(self): """Test check that only one environment can be created in one moment Scenario: 1. Go to environments submenu. 2. Create one environment. 3. Go to environments submenu. 4. Press create new environment button. 5. Press create new environment button again. 6. Check that only one form for creating environment is created. """ self.go_to_submenu('Environments') self.create_environment('temp_environment') self.go_to_submenu('Environments') self.driver.find_element_by_id( 'environments__action_CreateEnvironment').click() self.driver.find_element_by_id( 'environments__action_CreateEnvironment').click() new_environments = self.driver.find_elements_by_class_name('new_env') self.assertEqual(len(new_environments), 1) def test_env_status_new_session_add_to_empty(self): """Test that environments status is correct in the new session Scenario: 1. Create environment. 2. Add app to environment. 3. Check that env status is 'Ready to deploy'. 4. Log out. 5. Log in. 6. Check that env status is 'Ready to configure'. """ self.add_app_to_env(self.mockapp_id) self.navigate_to('Applications') self.go_to_submenu('Environments') self.check_element_on_page(by.By.XPATH, c.EnvStatus.format('quick-env-1', 'Ready to deploy')) self.log_out() self.log_in(cfg.common.user, cfg.common.password) self.navigate_to('Applications') self.go_to_submenu('Environments') self.check_element_on_page(by.By.XPATH, c.EnvStatus.format('quick-env-1', 'Ready to configure')) def test_env_status_new_session_add_to_not_empty(self): """Test that environments status is correct in the new session Scenario: 1. Create environment. 2. Add app to environment. 3. Deploy environment. 4. Add one more app to environment. 5. Check that env status is 'Ready to deploy'. 6. Log out. 7. Log in. 8. Check that env status is 'Ready'. """ self.add_app_to_env(self.mockapp_id) self.driver.find_element_by_id('services__action_deploy_env').click() self.check_element_on_page(by.By.XPATH, c.Status.format('Ready'), sec=90) self.navigate_to('Applications') self.go_to_submenu('Environments') self.check_element_on_page(by.By.LINK_TEXT, 'quick-env-1') env_id = self.get_element_id('quick-env-1') self.add_app_to_env(self.mockapp_id, 'TestApp1', env_id) self.go_to_submenu('Environments') self.check_element_on_page(by.By.XPATH, c.EnvStatus.format('quick-env-1', 'Ready to deploy')) self.log_out() self.log_in(cfg.common.user, cfg.common.password) self.navigate_to('Applications') self.go_to_submenu('Environments') self.check_element_on_page(by.By.XPATH, c.EnvStatus.format('quick-env-1', 'Ready')) def test_env_status_new_session_remove_from_one(self): """Test that environments status is correct in the new session Scenario: 1. Create environment. 2. Add app to environment. 3. Deploy environment. 4. Remove app from environment. 5. Check that env status is 'Ready to deploy'. 6. Log out. 7. Log in. 8. Check that env status is 'Ready'. """ self.add_app_to_env(self.mockapp_id) self.driver.find_element_by_id('services__action_deploy_env').click() self.check_element_on_page(by.By.XPATH, c.Status.format('Ready'), sec=90) self.delete_component() self.check_element_not_on_page(by.By.LINK_TEXT, 'TestApp') self.navigate_to('Applications') self.go_to_submenu('Environments') self.check_element_on_page(by.By.XPATH, c.EnvStatus.format('quick-env-1', 'Ready to deploy')) self.log_out() self.log_in(cfg.common.user, cfg.common.password) self.navigate_to('Applications') self.go_to_submenu('Environments') self.check_element_on_page(by.By.XPATH, c.EnvStatus.format('quick-env-1', 'Ready')) def test_env_status_new_session_remove_from_two(self): """Test that environments status is correct in the new session Scenario: 1. Create environment. 2. Add two apps to environment. 3. Deploy environment. 4. Remove one app from environment. 5. Check that env status is 'Ready to deploy'. 6. Log out. 7. Log in. 8. Check that env status is 'Ready'. """ self.add_app_to_env(self.mockapp_id) self.navigate_to('Applications') self.go_to_submenu('Environments') self.check_element_on_page(by.By.LINK_TEXT, 'quick-env-1') env_id = self.get_element_id('quick-env-1') self.add_app_to_env(self.mockapp_id, 'TestApp1', env_id) self.driver.find_element_by_id('services__action_deploy_env').click() self.go_to_submenu('Environments') self.check_element_on_page(by.By.LINK_TEXT, 'quick-env-1') self.driver.find_element(by.By.LINK_TEXT, 'quick-env-1').click() self.check_element_on_page(by.By.XPATH, c.Status.format('Ready'), sec=90) self.delete_component() self.go_to_submenu('Environments') self.check_element_on_page(by.By.XPATH, c.EnvStatus.format('quick-env-1', 'Ready to deploy')) self.log_out() self.log_in(cfg.common.user, cfg.common.password) self.navigate_to('Applications') self.go_to_submenu('Environments') self.check_element_on_page(by.By.XPATH, c.EnvStatus.format('quick-env-1', 'Ready')) def test_latest_deployment_log(self): """Test latest deployment log for a deployed environment. Scenario: 1. Create environment. 2. Add app to environment. 3. Go to environment detail page. 4. Check that 'Latest Deployment Tab' is present. 5. Check the logs for the expected output. """ self.add_app_to_env(self.mockapp_id) self.navigate_to('Applications') self.go_to_submenu('Environments') self.check_element_on_page(by.By.LINK_TEXT, 'quick-env-1') self.driver.find_element(by.By.LINK_TEXT, 'quick-env-1').click() self.check_element_not_on_page(by.By.XPATH, c.DeploymentHistoryLogTab) self.check_element_on_page(by.By.ID, c.DeployEnvironment) self.driver.find_element_by_id(c.DeployEnvironment).click() self.check_element_on_page( by.By.XPATH, c.EnvStatus.format('TestApp', 'Ready')) self.check_element_on_page(by.By.XPATH, c.DeploymentHistoryLogTab) self.driver.find_element_by_xpath(c.DeploymentHistoryLogTab).click() self.check_element_on_page(by.By.CSS_SELECTOR, c.DeploymentHistoryLogs) # It might be possible for the log lines to be out of order, so remove # all non-alphabet and non-spaces from each line then sort them to # guarantee following assertions always pass. logs = self.driver.find_elements_by_css_selector( c.DeploymentHistoryLogs) text_only_logs = list( map(lambda log: re.sub('[^a-zA-Z ]+', '', log.text), logs) ) text_only_logs.sort() self.assertEqual(3, len(text_only_logs)) self.assertIn('Action deploy is scheduled', text_only_logs[0]) self.assertIn('Deployment finished', text_only_logs[1]) self.assertIn('Follow the white rabbit', text_only_logs[2]) def test_latest_deployment_log_multiple_deployments(self): """Test latest deployment log for environment after two deployments. Scenario: 1. Create environment. 2. Add app to environment. 3. Go to environment detail page. 4. Check that 'Latest Deployment Tab' is present. 5. Check the logs for the expected output. 6. Add another app to the environment. 7. Update the environment. 8. Check that 'Latest Deployment Tab' is present. 9. Check the logs for the expected output. Should differ from the first deployment. """ self.add_app_to_env(self.mockapp_id) self.navigate_to('Applications') self.go_to_submenu('Environments') self.check_element_on_page(by.By.LINK_TEXT, 'quick-env-1') self.driver.find_element(by.By.LINK_TEXT, 'quick-env-1').click() self.check_element_not_on_page(by.By.XPATH, c.DeploymentHistoryLogTab) self.check_element_on_page(by.By.ID, c.DeployEnvironment) self.driver.find_element_by_id(c.DeployEnvironment).click() self.check_element_on_page( by.By.XPATH, c.EnvStatus.format('TestApp', 'Ready')) self.check_element_on_page(by.By.XPATH, c.DeploymentHistoryLogTab) self.driver.find_element_by_xpath(c.DeploymentHistoryLogTab).click() self.check_element_on_page(by.By.CSS_SELECTOR, c.DeploymentHistoryLogs) logs = self.driver.find_elements_by_css_selector( c.DeploymentHistoryLogs) text_only_logs = list( map(lambda log: re.sub('[^a-zA-Z ]+', '', log.text), logs) ) text_only_logs.sort() self.assertEqual(3, len(text_only_logs)) self.assertIn('Action deploy is scheduled', text_only_logs[0]) self.assertIn('Deployment finished', text_only_logs[1]) self.assertIn('Follow the white rabbit', text_only_logs[2]) # Click on the Components Tab because add_app_to_env will check for # specific elements under that tab when env_id != None. self.driver.find_element_by_xpath(c.EnvComponentsTab).click() self.navigate_to('Applications') self.go_to_submenu('Environments') self.add_app_to_env(self.mockapp_id, 'TestApp1', self.get_element_id('quick-env-1')) self.check_element_on_page(by.By.ID, c.DeployEnvironment) self.driver.find_element_by_id(c.DeployEnvironment).click() self.navigate_to('Applications') self.go_to_submenu('Environments') self.check_element_on_page(by.By.XPATH, c.Status.format('Ready'), sec=90) self.driver.find_element(by.By.LINK_TEXT, 'quick-env-1').click() self.check_element_on_page(by.By.XPATH, c.DeploymentHistoryLogTab) self.driver.find_element_by_xpath(c.DeploymentHistoryLogTab).click() self.check_element_on_page(by.By.CSS_SELECTOR, c.DeploymentHistoryLogs) logs = self.driver.find_elements_by_css_selector( c.DeploymentHistoryLogs) text_only_logs = list( map(lambda log: re.sub('[^a-zA-Z ]+', '', log.text), logs) ) text_only_logs.sort() self.assertEqual(4, len(text_only_logs)) self.assertIn('Action deploy is scheduled', text_only_logs[0]) self.assertIn('Deployment finished', text_only_logs[1]) self.assertIn('Follow the white rabbit', text_only_logs[2]) self.assertIn('Follow the white rabbit', text_only_logs[3]) def test_filter_component_by_name(self): """Test filtering components by name. Test checks ability to filter components by name in Environment Details page. Scenario: 1. Create 4 packages with random names (rather than using default packages, because MockApp is the name of a package, but all package descriptions contain 'MockApp', complicating testing). 2. Navigate to 'Applications' > 'Environments' panel 3. For each name in ``apps_by_name``: a. Set search criterion in search field to current name. b. Hit Enter key and verify that expected components are shown and unexpected components are not shown. """ packages = [] apps_by_name = [] field_name = '#envAppsFilter > input.form-control' for i in range(4): app_name = self.gen_random_resource_name('app_name', 8) description = self.gen_random_resource_name('description', 8) metadata = {"categories": ["Web"], "tags": ["tag"]} manifest_kwargs = {'Description': description} pkg_id = utils.upload_app_package(self.murano_client, app_name, metadata, **manifest_kwargs) apps_by_name.append(app_name) packages.append(pkg_id) self.addCleanup(self.murano_client.packages.delete, pkg_id) self.navigate_to('Applications') self.go_to_submenu('Environments') env_name = self.gen_random_resource_name('env', 9) self.create_environment(env_name) for app_name in apps_by_name: self.fill_field(by.By.CSS_SELECTOR, field_name, app_name) self.driver.find_element_by_css_selector(field_name).send_keys( Keys.ENTER) self.check_element_on_page(by.By.XPATH, c.Component.format(app_name)) for app_name in set(apps_by_name) - set([app_name]): self.check_element_not_on_page(by.By.XPATH, c.Component.format(app_name)) def test_filter_component_by_description(self): """Test filtering components by description. Test checks ability to filter components by description in Environment Details page. Scenario: 1. Create 4 packages with random descriptions (rather than using default applications, because they all contain the same description). 2. Navigate to 'Applications' > 'Environments' panel 3. For each description in ``apps_by_description.keys()``: a. Set search criterion in search field to current description. b. Hit Enter key and verify that expected components are shown and unexpected components are not shown. """ packages = [] apps_by_description = {} field_name = '#envAppsFilter > input.form-control' for i in range(4): app_name = self.gen_random_resource_name('app_name', 8) description = self.gen_random_resource_name('description', 8) metadata = {"categories": ["Web"], "tags": ["tag"]} manifest_kwargs = {'Description': description} pkg_id = utils.upload_app_package(self.murano_client, app_name, metadata, **manifest_kwargs) apps_by_description[app_name] = description packages.append(pkg_id) self.addCleanup(self.murano_client.packages.delete, pkg_id) self.navigate_to('Applications') self.go_to_submenu('Environments') env_name = self.gen_random_resource_name('env', 9) self.create_environment(env_name) for app_name, app_description in apps_by_description.items(): self.fill_field(by.By.CSS_SELECTOR, field_name, app_description) self.driver.find_element_by_css_selector(field_name).send_keys( Keys.ENTER) self.check_element_on_page(by.By.XPATH, c.Component.format(app_name)) other_apps = set(apps_by_description.keys()) -\ set([app_description]) for app_name in other_apps: self.check_element_not_on_page( by.By.XPATH, c.Component.format(app_description)) def test_filter_component_by_tag(self): """Test filtering components by tag. Test checks ability to filter components by tag in Environment Details page. Scenario: 1. Navigate to 'Applications' > 'Environments' panel 2. For each tag in ``apps_by_tag.keys()``: a. Set search criterion in search field to current tag. b. Hit Enter key and verify that expected components are shown and unexpected components are not shown. """ self.navigate_to('Applications') self.go_to_submenu('Environments') env_name = self.gen_random_resource_name('env', 9) self.create_environment(env_name) apps_by_tag = { 'tag': ['DeployingApp', 'MockApp', 'PostgreSQL'], 'hot': ['HotExample'] } all_apps = ['DeployingApp', 'HotExample', 'MockApp', 'PostgreSQL'] field_name = '#envAppsFilter > input.form-control' for tag, name_list in apps_by_tag.items(): self.fill_field(by.By.CSS_SELECTOR, field_name, tag) self.driver.find_element_by_css_selector(field_name).send_keys( Keys.ENTER) for name in name_list: self.check_element_on_page(by.By.XPATH, c.Component.format(name)) for name in set(all_apps) - set(name_list): self.check_element_not_on_page(by.By.XPATH, c.Component.format(name)) def test_filter_component_by_tag_overflowing_carousel(self): """Test that filtering components that overflow carousel are present. When multiple results are returned, they extend past the carousel's main view. Check that results that extend beyond the main view exist. Scenario: 1. Create 15 packages with random names but with the same tag (so that all 15 packages are returned when filtering by tag). 2. Navigate to 'Applications' > 'Environments' panel. 3. Filter by the tag. 4. For each app in ``apps_by_name``: a. Verify that all created packages are shown, even those that extend beyond carousel's view. """ packages = [] apps_by_name = [] field_name = '#envAppsFilter > input.form-control' tag = 'foo_tag' for i in range(15): app_name = self.gen_random_resource_name('app_name', 8) metadata = {"categories": ["Web"], "tags": [tag]} pkg_id = utils.upload_app_package(self.murano_client, app_name, metadata) apps_by_name.append(app_name) packages.append(pkg_id) self.addCleanup(self.murano_client.packages.delete, pkg_id) self.navigate_to('Applications') self.go_to_submenu('Environments') env_name = self.gen_random_resource_name('env', 9) self.create_environment(env_name) self.fill_field(by.By.CSS_SELECTOR, field_name, tag) self.driver.find_element_by_css_selector(field_name).send_keys( Keys.ENTER) for app_name in apps_by_name: self.check_element_on_page(by.By.XPATH, c.Component.format(app_name)) def test_deploy_env_from_table_view(self): """Test that deploy environment works from the table view. Scenario: 1. Create environment. 2. Add one app to the environment. 3. Deploy the environment from the table view. 4. Log out. 5. Log in. 6. Check that the environment status is 'Ready'. """ self.add_app_to_env(self.mockapp_id) self.navigate_to('Applications') self.go_to_submenu('Environments') self.check_element_on_page(by.By.LINK_TEXT, 'quick-env-1') self.execute_action_from_table_view('quick-env-1', 'Deploy Environment') self.log_out() self.log_in() self.navigate_to('Applications') self.go_to_submenu('Environments') self.check_element_on_page(by.By.XPATH, c.EnvStatus.format('quick-env-1', 'Ready')) def test_abandon_env_from_table_view(self): """Test that abandon environment works from the table view. Scenario: 1. Create environment. 2. Add one app to the environment. 3. Deploy the environment from the table view. 4. Log out. 5. Log in. 6. Go to the Applications > Environments page. 7. Abandon the environment from the table view. 8. Click the confirmation in the modal. 9. Check that the environment is no longer present on the page. """ self.add_app_to_env(self.mockapp_id) self.navigate_to('Applications') self.go_to_submenu('Environments') self.check_element_on_page(by.By.LINK_TEXT, 'quick-env-1') self.execute_action_from_table_view('quick-env-1', 'Deploy Environment') self.log_out() self.log_in() self.navigate_to('Applications') self.go_to_submenu('Environments') self.check_element_on_page(by.By.XPATH, c.EnvStatus.format('quick-env-1', 'Ready')) self.execute_action_from_table_view('quick-env-1', 'Abandon Environment') self.check_element_on_page(by.By.XPATH, c.ConfirmAbandon) self.driver.find_element(by.By.XPATH, c.ConfirmAbandon).click() self.wait_for_alert_message() self.check_element_not_on_page(by.By.LINK_TEXT, 'quick-env-1') class TestSuiteImage(base.ImageTestCase): def test_mark_image(self): """Test check ability to mark murano image Scenario: 1. Navigate to Images page 2. Click on button "Mark Image" 3. Fill the form and submit it """ self.navigate_to('Manage') self.go_to_submenu('Images') self.driver.find_element_by_id( 'marked_images__action_mark_image').click() self.select_from_list('image', self.image.id) new_title = 'RenamedImage ' + str(time.time()) self.fill_field(by.By.ID, 'id_title', new_title) self.select_from_list('type', 'linux') self.select_and_click_element('Mark Image') self.check_element_on_page(by.By.XPATH, c.TestImage.format(new_title)) def test_check_image_info(self): """Test check ability to view image details Scenario: 1. Navigate to Images page 2. Click on the name of selected image, check image info """ self.navigate_to('Manage') self.go_to_submenu('Images') self.driver.find_element_by_xpath( c.TestImage.format(self.image_title) + '//a').click() self.assertIn(self.image_title, self.driver.find_element(by.By.XPATH, c.ImageMeta).text) def test_delete_image(self): """Test check ability to delete image Scenario: 1. Navigate to Images page 2. Select created image and click on "Delete Metadata" """ self.navigate_to('Manage') self.go_to_submenu('Images') self.driver.find_element_by_xpath( c.DeleteImageMeta.format(self.image_title)).click() with self.wait_for_page_reload(): self.driver.find_element_by_xpath(c.ConfirmDeletion).click() self.wait_for_alert_message() self.check_element_not_on_page(by.By.XPATH, c.TestImage.format(self.image_title)) def test_delete_multiple_images_one_by_one(self): """Test ability to delete multiple images one by one. Scenario: 1. Create 3 randomly named images. 2. Navigate to Images page. 3. For each randomly created image: a. Select current image and click on "Delete Metadata". Each image is deleted separately. """ def _try_delete_image(image_id): try: self.glance.images.delete(image_id) except muranoclient_exc.HTTPNotFound: pass default_image_title = self.image_title image_titles = [] for i in range(3): image_title = self.gen_random_resource_name('image') image_titles.append(image_title) image = self.upload_image(image_title) self.addCleanup(_try_delete_image, image.id) self.navigate_to('Manage') self.go_to_submenu('Images') # Check each checkbox in the table and delete each image one by one. for image_title in image_titles: self.wait_element_is_clickable( by.By.XPATH, c.DeleteImageMeta.format(image_title)).click() with self.wait_for_page_reload(): self.driver.find_element_by_xpath(c.ConfirmDeletion).click() self.check_element_not_on_page( by.By.XPATH, c.TestImage.format(image_title)) self.check_element_on_page( by.By.XPATH, c.TestImage.format(default_image_title)) class TestSuiteFields(base.FieldsTestCase): def test_check_domain_name_field_validation(self): """Check domain name validation Test checks that validation of domain name field works and appropriate error message appears after entering incorrect domain name Scenario: 1. Navigate to Environments page 2. Create environment and start to create MockApp service 3. Set "a" as a domain name and verify error message 4. Set "aa" as a domain name and check that error message didn't appear 5. Set "@ct!v3" as a domain name and verify error message 6. Set "active.com" as a domain name and check that error message didn't appear 7. Set "domain" as a domain name and verify error message 8. Set "domain.com" as a domain name and check that error message didn't appear 9. Set "morethan15symbols.beforedot" as a domain name and verify error message 10. Set "lessthan15.beforedot" as a domain name and check that error message didn't appear 11. Set ".domain.local" as a domain name and verify error message 12. Set "domain.local" as a domain name and check that error message didn't appear """ self.navigate_to('Browse') self.go_to_submenu('Browse Local') self.select_and_click_action_for_app('quick-add', self.mockapp_id) field_id = self.mockapp_id + "_0-domain" self.fill_field(by.By.ID, field_id, value='a') self.check_js_error_message_is_present( "Only letters, numbers and dashes in the middle are " "allowed. Period characters are allowed only when they " "are used to delimit the components of domain style " "names. Single-level domain is not " "appropriate. Subdomains are not allowed.") self.check_error_message_is_present( 'Ensure this value has at least 2 characters (it has 1).') self.fill_field(by.By.ID, field_id, value='aa') self.check_error_message_is_absent( 'Ensure this value has at least 2 characters (it has 1).') self.fill_field(by.By.ID, field_id, value='@ct!v3') self.check_js_error_message_is_present( 'Only letters, numbers and dashes in the middle are allowed.') self.check_error_message_is_present( 'Only letters, numbers and dashes in the middle are allowed.') self.fill_field(by.By.ID, field_id, value='active.com') self.check_js_error_message_is_absent( 'Only letters, numbers and dashes in the middle are allowed.') self.check_error_message_is_absent( 'Only letters, numbers and dashes in the middle are allowed.') self.fill_field(by.By.ID, field_id, value='domain') self.check_js_error_message_is_present( 'Single-level domain is not appropriate.') self.check_error_message_is_present( 'Single-level domain is not appropriate.') self.fill_field(by.By.ID, field_id, value='domain.com') self.check_js_error_message_is_absent( 'Single-level domain is not appropriate.') self.check_error_message_is_absent( 'Single-level domain is not appropriate.') self.fill_field(by.By.ID, field_id, value='morethan15symbols.beforedot') self.check_js_error_message_is_present( 'NetBIOS name cannot be shorter than' ' 1 symbol and longer than 15 symbols.') self.check_error_message_is_present( 'NetBIOS name cannot be shorter than' ' 1 symbol and longer than 15 symbols.') self.fill_field(by.By.ID, field_id, value='lessthan15.beforedot') self.check_js_error_message_is_absent( 'NetBIOS name cannot be shorter than' ' 1 symbol and longer than 15 symbols.') self.check_error_message_is_absent( 'NetBIOS name cannot be shorter than' ' 1 symbol and longer than 15 symbols.') self.fill_field(by.By.ID, field_id, value='.domain.local') self.check_js_error_message_is_present( 'Period characters are allowed only when ' 'they are used to delimit the components of domain style names') self.check_error_message_is_present( 'Period characters are allowed only when ' 'they are used to delimit the components of domain style names') self.fill_field(by.By.ID, field_id, value='domain.local') self.check_js_error_message_is_absent( 'Period characters are allowed only when ' 'they are used to delimit the components of domain style names') self.check_error_message_is_absent( 'Period characters are allowed only when ' 'they are used to delimit the components of domain style names') def test_check_app_name_validation(self): """Test checks validation of field that usually defines application name Scenario: 1. Navigate to Catalog > Browse 2. Start to create Mock App 3. Check a set of names, if current name isn't valid appropriate error message should appears """ self.navigate_to('Browse') self.go_to_submenu('Browse Local') self.select_and_click_action_for_app('quick-add', self.mockapp_id) self.fill_field(by.By.NAME, '0-name', value='a') self.check_error_message_is_present( 'Ensure this value has at least 2 characters (it has 1).') self.fill_field(by.By.NAME, '0-name', value='@pp') self.check_js_error_message_is_present( 'Just letters, numbers, underscores and hyphens are allowed.') self.check_error_message_is_present( 'Just letters, numbers, underscores and hyphens are allowed.') self.fill_field(by.By.NAME, '0-name', value='AppL1') self.driver.find_element_by_xpath(c.ButtonSubmit).click() self.wait_element_is_clickable(by.By.XPATH, c.ButtonSubmit) def test_check_required_field(self): """Test required fields Test checks that fields with parameter 'required=True' in yaml form are truly required and can't be omitted Scenario: 1. Navigate to Catalog > Browse 2. Start to create MockApp 3. Don't type app name in the 'Application Name' field that is required and click 'Next', check that there is error message 4. Set app name and click 'Next', check that there is no error message """ self.navigate_to('Browse') self.go_to_submenu('Browse Local') self.select_and_click_action_for_app('quick-add', self.mockapp_id) self.driver.find_element_by_xpath(c.ButtonSubmit).click() self.check_error_message_is_present('This field is required.') self.fill_field(by.By.NAME, "0-name", "name") self.driver.find_element_by_xpath(c.ButtonSubmit).click() self.wait_element_is_clickable(by.By.XPATH, c.ButtonSubmit) def test_password_validation(self): """Test checks password validation Scenario: 1. Navigate to Catalog > Browse 2. Start to create MockApp 3. Set weak password consisting of numbers, check that error message appears 4. Set different passwords to Password field and Confirm password field, check that validation failed 5. Set correct password. Validation has to pass """ self.navigate_to('Browse') self.go_to_submenu('Browse Local') self.select_and_click_action_for_app('quick-add', self.mockapp_id) self.fill_field(by.By.NAME, "0-name", "name") self.fill_field(by.By.NAME, '0-adminPassword', value='123456') self.check_js_error_message_is_present( 'The password must contain at least one letter') self.check_error_message_is_present( 'The password must contain at least one letter') self.driver.find_element_by_xpath(c.ButtonSubmit).click() self.fill_field(by.By.NAME, "0-adminPassword-clone", value='P@ssw0rd') self.check_js_error_message_is_present('Passwords do not match') self.check_error_message_is_absent('Passwords do not match') self.fill_field(by.By.NAME, '0-adminPassword', value='P@ssw0rd') self.check_js_error_message_is_absent('Passwords do not match') self.driver.find_element_by_xpath(c.ButtonSubmit).click() self.wait_element_is_clickable(by.By.XPATH, c.ButtonSubmit) class TestSuiteApplications(base.ApplicationTestCase): def test_check_transitions_from_one_wizard_to_another(self): """Test checks that transitions "Next" and "Back" are not broken Scenario: 1. Navigate to Catalog > Browse 2. Start to create MockApp 3. Set app name and click on "Next", check that second wizard step will appear 4. Click 'Back' and check that first wizard step is shown """ self.navigate_to('Browse') self.go_to_submenu('Browse Local') self.select_and_click_action_for_app('quick-add', self.mockapp_id) self.fill_field(by.By.NAME, "0-name", "name") self.driver.find_element_by_xpath(c.ButtonSubmit).click() self.driver.find_element_by_id( 'wizard_{0}_btn'.format(self.mockapp_id)).click() self.check_element_on_page(by.By.NAME, "0-name") def test_check_ability_create_two_dependent_apps(self): """Test using two dependent apps Test checks that with using one creation form it is possible to add two related apps in one environment Scenario: 1. Navigate to Catalog > Browse 2. Start to create MockApp 3. Set app name and click on "Next" 4. Click '+' and verify that creation of second app is possible """ self.navigate_to('Browse') self.go_to_submenu('Browse Local') self.select_and_click_action_for_app('quick-add', self.mockapp_id) self.fill_field(by.By.NAME, "0-name", "app1") self.driver.find_element_by_xpath(c.ButtonSubmit).click() self.driver.find_element_by_css_selector( 'form i.fa-plus-circle').click() self.fill_field(by.By.NAME, "0-name", "app2") def test_creation_deletion_app(self): """Test check ability to create and delete test app Scenario: 1. Navigate to 'Catalog' > Browse 2. Click on 'Quick Deploy' for MockApp application 3. Create TestApp app by filling the creation form 4. Delete TestApp app from environment """ self.navigate_to('Browse') self.go_to_submenu('Browse Local') self.select_and_click_action_for_app('quick-add', self.mockapp_id) self.fill_field(by.By.NAME, '0-name'.format(self.mockapp_id), 'TestA') self.driver.find_element_by_xpath(c.ButtonSubmit).click() self.driver.find_element_by_xpath(c.InputSubmit).click() self.select_from_list('osImage', self.image.id) self.driver.find_element_by_xpath(c.InputSubmit).click() self.wait_element_is_clickable(by.By.ID, c.AddComponent) self.check_element_on_page(by.By.LINK_TEXT, 'TestA') self.delete_component() self.check_element_not_on_page(by.By.LINK_TEXT, 'TestA') def test_filter_by_name(self): """Test checks that 'Search' option is operable. Scenario: 1. Navigate to 'Catalog > Browse' panel 2. Set search criterion in the search field (e.g 'PostgreSQL') 3. Click on 'Filter' and check result """ self.navigate_to('Browse') self.go_to_submenu('Browse Local') self.fill_field(by.By.CSS_SELECTOR, 'input.form-control', 'PostgreSQL') with self.wait_for_page_reload(): self.driver.find_element_by_id('apps__action_filter').click() self.check_element_on_page(by.By.XPATH, c.App.format('PostgreSQL')) self.check_element_not_on_page(by.By.XPATH, c.App.format('DeployingApp')) self.check_element_not_on_page(by.By.XPATH, c.App.format('HotExample')) self.check_element_not_on_page(by.By.XPATH, c.App.format('MockApp')) def test_filter_by_tag(self): """Test filtering by tag. Test checks ability to filter applications by tag in Catalog page. Scenario: 1. Navigate to 'Catalog' > 'Browse' panel 2. For each tag in ``apps_by_tag.keys()``: a. Set search criterion in search field to current tag. b. Click on 'Filter' and verify that expected applications are shown and unexpected applications are not shown. """ self.navigate_to('Browse') self.go_to_submenu('Browse Local') apps_by_tag = { 'tag': ['DeployingApp', 'MockApp', 'PostgreSQL'], 'hot': ['HotExample'] } all_apps = ['DeployingApp', 'HotExample', 'MockApp', 'PostgreSQL'] for tag, name_list in apps_by_tag.items(): self.fill_field(by.By.CSS_SELECTOR, 'input.form-control', tag) with self.wait_for_page_reload(): self.driver.find_element_by_id('apps__action_filter').click() for name in name_list: self.check_element_on_page(by.By.XPATH, c.App.format(name)) for name in set(all_apps) - set(name_list): self.check_element_not_on_page(by.By.XPATH, c.App.format(name)) @unittest.skipIf(utils.glare_enabled(), "Filtering apps by description doesn't work with GLARE; " "this test should be fixed with bug #1616856.") def test_filter_by_description(self): """Test filtering by description. Test checks ability to filter applications by description in Catalog page. Before beginning scenario, the test creates an app & package w/ a randomly generated description (because otherwise all descriptions would be identical). Scenario: 1. Navigate to 'Catalog' > 'Browse' panel 2. For each description in ``apps_by_description.keys()``: a. Set search criterion in search field to current description. b. Click on 'Filter' and verify that expected applications are shown and unexpected applications are not shown. """ app_name = self.gen_random_resource_name('app_name', 8) description = self.gen_random_resource_name('description', 8) metadata = {"categories": ["Web"], "tags": ["tag"]} manifest_kwargs = {'Description': description} pkg_id = utils.upload_app_package(self.murano_client, app_name, metadata, **manifest_kwargs) self.navigate_to('Browse') self.go_to_submenu('Browse Local') apps_by_description = { 'MockApp for webUI tests': ['DeployingApp', 'MockApp', 'PostgreSQL', 'HotExample'], description: [app_name] } all_apps = ['DeployingApp', 'HotExample', 'MockApp', 'PostgreSQL', app_name] for description, name_list in apps_by_description.items(): self.fill_field(by.By.CSS_SELECTOR, 'input.form-control', description) with self.wait_for_page_reload(): self.driver.find_element_by_id('apps__action_filter').click() for name in name_list: self.check_element_on_page(by.By.XPATH, c.App.format(name)) for name in set(all_apps) - set(name_list): self.check_element_not_on_page(by.By.XPATH, c.App.format(name)) self.murano_client.packages.delete(pkg_id) def test_filter_by_category(self): """Test filtering by category Test checks ability to filter applications by category in Catalog page Scenario: 1. Navigate to 'Catalog'>'Browse' panel 2. Select 'Databases' category in 'App Category' dropdown menu 3. Verify that PostgreSQL is shown 4. Select 'Web' category in 'App Category' dropdown menu 5. Verify that MockApp is shown """ self.navigate_to('Browse') self.go_to_submenu('Browse Local') self.driver.find_element_by_xpath( c.CategorySelector.format('All')).click() self.driver.find_element_by_partial_link_text('Databases').click() self.check_element_on_page(by.By.XPATH, c.App.format('PostgreSQL')) self.driver.find_element_by_xpath( c.CategorySelector.format('Databases')).click() self.driver.find_element_by_partial_link_text('Web').click() self.check_element_on_page(by.By.XPATH, c.App.format('MockApp')) def test_check_option_switch_env(self): """Test checks ability to switch environment and add app in other env Scenario: 1. Navigate to 'Catalog>Environments' panel 2. Create environment 'env1' 3. Create environment 'env2' 4. Navigate to 'Catalog>Browse' 5. Click on 'Environment' panel 6. Switch to env2 7. Add application in env2 8. Navigate to 'Catalog>Environments' and go to the env2 9. Check that added application is here """ self.navigate_to('Applications') self.go_to_submenu('Environments') self.create_environment('env1') self.go_to_submenu('Environments') self.check_element_on_page(by.By.LINK_TEXT, 'env1') self.create_environment('env2', by_id=True) self.go_to_submenu('Environments') self.check_element_on_page(by.By.LINK_TEXT, 'env2') env_id = self.get_element_id('env2') self.navigate_to('Browse') self.go_to_submenu('Browse Local') self.driver.find_element_by_xpath( ".//*[@id='environment_switcher']/a").click() self.driver.find_element_by_link_text("env2").click() self.select_and_click_action_for_app( 'add', '{0}/{1}'.format(self.mockapp_id, env_id)) self.fill_field(by.By.NAME, '0-name', 'TestA') self.driver.find_element_by_xpath( c.ButtonSubmit).click() self.driver.find_element_by_xpath(c.InputSubmit).click() self.select_from_list('osImage', self.image.id) self.driver.find_element_by_xpath(c.InputSubmit).click() self.driver.find_element_by_xpath(c.InputSubmit).click() self.check_element_on_page(by.By.LINK_TEXT, 'TestA') def test_check_progress_bar(self): """Test that progress bar appears only for 'Deploying' status Scenario: 1. Navigate to Catalog>Browse and click MockApp 'Quick Deploy' 2. Check that for "Ready to deploy" state progress bar is not seen 3. Click deploy 4. Check that for "Deploying" status progress bar is seen """ self.navigate_to('Browse') self.go_to_submenu('Browse Local') self.select_and_click_action_for_app('quick-add', self.mockapp_id) field_id = "{0}_0-name".format(self.mockapp_id) self.fill_field(by.By.ID, field_id, value='TestApp') self.driver.find_element_by_xpath(c.ButtonSubmit).click() self.driver.find_element_by_xpath(c.InputSubmit).click() self.select_from_list('osImage', self.image.id) self.driver.find_element_by_xpath(c.InputSubmit).click() self.check_element_on_page(by.By.XPATH, c.Status.format('Ready to deploy')) self.check_element_on_page(by.By.XPATH, c.CellStatus.format('up')) self.driver.find_element_by_id('services__action_deploy_env').click() self.check_element_on_page(by.By.XPATH, c.Status.format('Ready'), sec=90) self.check_element_on_page(by.By.XPATH, c.CellStatus.format('up')) def test_check_overview_tab(self): """Test check that created application overview tab browsed correctly Scenario: 1. Navigate to Catalog>Browse and click MockApp 'Quick Deploy' 2. Click on application name to go to the detail page """ app_name = 'NewTestApp' self.add_app_to_env(self.mockapp_id, app_name) self.driver.find_element_by_link_text(app_name).click() self.check_element_on_page( by.By.XPATH, "//dd[contains(text(), {0})]".format(app_name)) def test_ensure_actions(self): """Checks that action is available for deployed application Scenario: 1. Navigate to Catalog>Browse and click MockApp 'Quick Deploy' 2. Click deploy 3. Wait 'Ready' status 4. Click on application 5. Check that defined action name is in the list of app 'actions' """ self.add_app_to_env(self.mockapp_id) self.driver.find_element_by_id('services__action_deploy_env').click() self.check_element_on_page(by.By.XPATH, c.Status.format('Ready'), sec=90) el = self.wait_element_is_clickable(by.By.XPATH, c.More.format('services', '')) el.click() self.driver.find_element_by_xpath(c.Action).click() self.driver.find_element_by_css_selector('.modal-close button').click() self.check_element_on_page(by.By.XPATH, "//*[contains(text(), 'Completed')]") def test_check_info_about_app(self): """Test checks that information about app is available Scenario: 1. Navigate to 'Catalog>Browse' panel 2. Choose some application and click on 'More info' 3. Verify info about application """ self.navigate_to('Browse') self.go_to_submenu('Browse Local') self.select_and_click_action_for_app('details', self.mockapp_id) self.assertEqual('MockApp for webUI tests', self.driver.find_element_by_xpath( "//div[@class='app-description']").text) self.driver.find_element_by_link_text('Requirements').click() self.driver.find_element_by_class_name('app_requirements') self.driver.find_element_by_link_text('License').click() self.driver.find_element_by_class_name('app_license') def test_check_topology_page(self): """Test checks that topology tab is available, displays correctly Scenario: 1. Navigate to Catalog>Browse and click MockApp 'Quick Deploy' 2. Click deploy 3. Wait 'Ready' status 4. Click on 'Topology' tab 5. Check that status is 'Waiting for deployment' is displayed 6. Check that app logo is present on page """ self.add_app_to_env(self.mockapp_id) self.driver.find_element_by_link_text('Topology').click() self.assertEqual( 'Status: Waiting for deployment', self.driver.find_element_by_css_selector('#stack_box > p').text) self.check_element_on_page(by.By.TAG_NAME, 'image') def test_check_deployment_history(self): """Test checks that deployment history tab is available, logs are ok Scenario: 1. Navigate to Catalog>Browse and click MockApp 'Quick Deploy' 2. Click deploy 3. Wait 'Ready' status 4. Click on 'Deployment History' tab 5. Click 'Show Details' button 6. Click 'Logs' button 7. Check that app deployment message is present in logs """ self.add_app_to_env(self.mockapp_id) self.driver.find_element_by_id('services__action_deploy_env').click() self.check_element_on_page(by.By.XPATH, c.Status.format('Ready'), sec=90) self.wait_element_is_clickable( by.By.PARTIAL_LINK_TEXT, 'Deployment History').click() self.wait_element_is_clickable( by.By.PARTIAL_LINK_TEXT, 'Show Details').click() self.wait_element_is_clickable( by.By.PARTIAL_LINK_TEXT, 'Logs').click() self.assertIn('Follow the white rabbit', self.driver.find_element_by_class_name('logs').text) def test_check_service_name_and_type(self): """Test checks that service name and type are displayed correctly Scenario: 1. Navigate to Catalog>Browse and click MockApp 'Quick Deploy' 2. Check service name and type 3. Navigate to service details page 4. Check service name and type there 5. Navigate back to the environment details page 6. Click deploy 7. Wait 'Ready' status 8. Check service name and type 9. Navigate to service details page 10. Check service name and type """ self.add_app_to_env(self.mockapp_id) name = 'TestApp' type_ = 'MockApp' self.check_element_on_page(by.By.XPATH, c.ServiceType.format(type_)) self.driver.find_element_by_link_text(name).click() self.check_element_on_page(by.By.XPATH, c.ServiceDetail.format(name)) self.check_element_on_page(by.By.XPATH, c.ServiceDetail.format(type_)) self.check_element_not_on_page(by.By.XPATH, c.ServiceDetail.format( 'Unknown')) self.go_to_submenu('Environments') self.driver.find_element_by_link_text('quick-env-1').click() self.driver.find_element_by_id('services__action_deploy_env').click() self.check_element_on_page(by.By.XPATH, c.Status.format('Ready'), sec=90) self.check_element_on_page(by.By.XPATH, c.ServiceType.format(type_)) self.driver.find_element_by_link_text(name).click() self.check_element_on_page(by.By.XPATH, c.ServiceDetail.format(name)) self.check_element_on_page(by.By.XPATH, c.ServiceDetail.format(type_)) self.check_element_not_on_page(by.By.XPATH, c.ServiceDetail.format( 'Unknown')) def test_hot_application(self): """Checks that UI got hot app is rendered correctly Scenario: 1. Navigate to Catalog>Browse and click Hot app 'Quick Deploy' 2. Check for YAQL validator 3. Check that app is added to the environment """ self.navigate_to('Browse') self.go_to_submenu('Browse Local') self.select_and_click_action_for_app('quick-add', self.hot_app_id) field_id = "{0}_0-name".format(self.hot_app_id) self.fill_field(by.By.ID, field_id, value='TestHotApp') self.driver.find_element_by_xpath(c.ButtonSubmit).click() self.fill_field(by.By.CSS_SELECTOR, 'input[id$="flavor"]', value='testFlavor') self.driver.find_element_by_xpath(c.InputSubmit).click() self.check_element_on_page(by.By.XPATH, c.HotFlavorField) self.fill_field(by.By.CSS_SELECTOR, 'input[id$="flavor"]', value='m1.small') self.driver.find_element_by_xpath(c.InputSubmit).click() self.check_element_on_page(by.By.LINK_TEXT, 'TestHotApp') def test_deploy_mockapp_remove_it_and_deploy_another_mockapp(self): """Checks that app is not available after remove and new app deployment Scenario: 1. Navigate to Environments 2. Create new environment 3. Navigate to Catalog>Browse and click MockApp 'Add to Env' 4. Fill the form use environment from step 2 and click submit 5. Click deploy environment 6. Wait 'Ready' status 7. Click Delete Application in row actions. 8. Navigate to Catalog>Browse and click MockApp 'Add to Env' 9. Fill the form use environment from step 2 and new app name and click submit 10. Click deploy environment 11. Check that the first application created in step 5 is not in the list 12. Click Delete Application in row actions. """ # uuid.uuid4() generates random uuid env_name = str(uuid.uuid4()) # range specifies total amount of applications used in the test app_names = [] for x in range(4): # In case of application some short name is needed to fit on page app_names.append(str(uuid.uuid4())[::4]) self.navigate_to('Applications') self.go_to_submenu('Environments') self.create_environment(env_name) self.go_to_submenu('Environments') self.check_element_on_page(by.By.LINK_TEXT, env_name) env_id = self.get_element_id(env_name) for idx, app_name in enumerate(app_names): # Add application to the environment self.navigate_to('Browse') self.go_to_submenu('Browse Local') self.select_and_click_action_for_app( 'add', '{0}/{1}'.format(self.mockapp_id, env_id)) self.fill_field(by.By.NAME, '0-name'.format(self.mockapp_id), app_name) self.driver.find_element_by_xpath(c.ButtonSubmit).click() self.driver.find_element_by_xpath(c.InputSubmit).click() self.select_from_list('osImage', self.image.id) self.driver.find_element_by_xpath(c.InputSubmit).click() self.driver.find_element_by_xpath(c.InputSubmit).click() self.wait_element_is_clickable(by.By.ID, c.AddComponent) self.check_element_on_page(by.By.LINK_TEXT, app_name) # Deploy the environment with all current applications self.driver.find_element_by_id(c.DeployEnvironment).click() # Wait until the end of deploy self.check_element_on_page(by.By.XPATH, c.Status.format('Ready'), sec=90) # Starting form the second application will check # that previous application is not in the list on the page if idx: self.check_element_not_on_page(by.By.LINK_TEXT, app_names[idx - 1]) self.delete_component() # To ensure that the very last application is deleted as well for app_name in app_names[-1::]: self.check_element_not_on_page(by.By.LINK_TEXT, app_name) @unittest.skip("This test gives sporadic false positives." "To be fixed in bug #1611732") def test_deploy_several_mock_apps_in_a_row(self): """Checks that app works after another app is deployed Scenario: 1. Navigate to Environments 2. Create new environment 3. Navigate to Catalog>Browse and click MockApp 'Add to Env' 4. Fill the form and use environment from step 2 and click submit 5. Click deploy environment 6. Wait 'Ready' status 7. Click testAction in row actions. 8. Wait 'Completed' status 9. Repeat steps 3-6 to add one more application. 10 Execute steps 7-8 for each application in the environment """ # uuid.uuid4() generates random uuid env_name = str(uuid.uuid4()) # range specifies total amount of applications used in the test app_names = [] for x in range(4): # In case of application some short name is needed to fit on page app_names.append(str(uuid.uuid4())[::4]) self.navigate_to('Applications') self.go_to_submenu('Environments') self.create_environment(env_name) self.go_to_submenu('Environments') self.check_element_on_page(by.By.LINK_TEXT, env_name) env_id = self.get_element_id(env_name) for idx, app_name in enumerate(app_names, 1): # Add application to the environment self.navigate_to('Browse') self.go_to_submenu('Browse Local') self.select_and_click_action_for_app( 'add', '{0}/{1}'.format(self.mockapp_id, env_id)) self.fill_field(by.By.NAME, '0-name'.format(self.mockapp_id), app_name) self.driver.find_element_by_xpath(c.ButtonSubmit).click() self.driver.find_element_by_xpath(c.InputSubmit).click() self.select_from_list('osImage', self.image.id) self.driver.find_element_by_xpath(c.InputSubmit).click() self.driver.find_element_by_xpath(c.InputSubmit).click() self.wait_element_is_clickable(by.By.ID, c.AddComponent) self.check_element_on_page(by.By.LINK_TEXT, app_name) # Deploy the environment with all current applications self.driver.find_element_by_id(c.DeployEnvironment).click() # Wait until the end of deploy self.wait_element_is_clickable(by.By.ID, c.AddComponent) # For each current application in the deployed environment for app_name in app_names[:idx]: # Check that application with exact name is in the list # and has status Ready row_id = self.get_element_id(app_name) row_xpath = c.Row.format(row_id) status_xpath = '{0}{1}'.format(row_xpath, c.Status.format('Ready')) self.check_element_on_page(by.By.XPATH, status_xpath, sec=90) # Click on the testAction button for the application buttons_xpath = c.More.format('services', row_id) el = self.wait_element_is_clickable(by.By.XPATH, buttons_xpath, sec=30) el.click() action_xpath = '{0}{1}'.format(row_xpath, c.Action) menu_item = self.wait_element_is_clickable(by.By.XPATH, action_xpath, sec=60) menu_item.click() # And check that status of the application is 'Completed' status_xpath = '{0}{1}'.format(row_xpath, c.Status.format('Completed')) self.check_element_on_page(by.By.XPATH, status_xpath, sec=90) # Delete applications one by one for app_name in app_names: self.delete_component(app_name) self.check_element_not_on_page(by.By.LINK_TEXT, app_name) class TestSuiteAppsPagination(base.UITestCase): def setUp(self): super(TestSuiteAppsPagination, self).setUp() self.apps = [] # Create 30 additional packages with applications for i in range(100, 130): app_name = self.gen_random_resource_name('app', 4) tag = self.gen_random_resource_name('tag', 8) metadata = {"categories": ["Web"], "tags": [tag]} app_id = utils.upload_app_package(self.murano_client, app_name, metadata) self.apps.append(app_id) def tearDown(self): super(TestSuiteAppsPagination, self).tearDown() for app_id in self.apps: self.murano_client.packages.delete(app_id) def test_apps_pagination(self): """Test check pagination in case of many applications installed.""" self.navigate_to('Browse') self.go_to_submenu('Browse Local') packages_list = [elem.name for elem in self.murano_client.packages.list()] # No list of apps available in the client only packages are. # Need to remove 'Core library' and 'Application Development Library' # from it since it is not visible in application's list. packages_list.remove('Core library') packages_list.remove('Application Development Library') apps_per_page = 6 pages_itself = [packages_list[i:i + apps_per_page] for i in range(0, len(packages_list), apps_per_page)] for i, names in enumerate(pages_itself, 1): for name in names: self.check_element_on_page(by.By.XPATH, c.App.format(name)) if i != len(pages_itself): self.driver.find_element_by_link_text('Next Page').click() # Wait till the Next button disappear # Otherwise 'Prev' buttion from previous page might be used self.check_element_not_on_page(by.By.LINK_TEXT, 'Next Page') # Now go back to the first page pages_itself.reverse() for i, names in enumerate(pages_itself, 1): for name in names: self.check_element_on_page(by.By.XPATH, c.App.format(name)) if i != len(pages_itself): self.driver.find_element_by_link_text('Previous Page').click() class TestSuitePackages(base.PackageTestCase): @classmethod def setUpClass(cls): super(TestSuitePackages, cls).setUpClass() suffix = str(uuid.uuid4())[:6] cls.testuser_name = 'test_{}'.format(suffix) cls.testuser_password = 'test' email = '{}@example.com'.format(cls.testuser_name) cls.create_user(name=cls.testuser_name, password=cls.testuser_password, email=email) @classmethod def tearDownClass(cls): cls.delete_user(cls.testuser_name) super(TestSuitePackages, cls).tearDownClass() def test_modify_package_name(self): """Test check ability to change name of the package Scenario: 1. Navigate to 'Packages' page 2. Select package and click on 'Modify Package' 3. Rename package """ self.navigate_to('Manage') self.go_to_submenu('Packages') self.select_action_for_package(self.postgre_id, 'modify_package') self.fill_field(by.By.ID, 'id_name', 'PostgreSQL-modified') self.driver.find_element_by_xpath(c.InputSubmit).click() self.wait_for_alert_message() self.check_element_on_page(by.By.XPATH, c.AppPackages.format( 'PostgreSQL-modified')) self.select_action_for_package(self.postgre_id, 'modify_package') self.fill_field(by.By.ID, 'id_name', 'PostgreSQL') self.driver.find_element_by_xpath(c.InputSubmit).click() self.check_element_on_page(by.By.XPATH, c.AppPackages.format( 'PostgreSQL')) def test_modify_package_add_tag(self): """Test that new tag is shown in description Scenario: 1. Navigate to 'Packages' page 2. Click on "Modify Package" and add new tag 3. Got to the Catalog page 4. Check, that new tag is browsed in application description """ self.navigate_to('Manage') self.go_to_submenu('Packages') self.select_action_for_package(self.postgre_id, 'modify_package') self.fill_field(by.By.ID, 'id_tags', 'TEST_TAG') self.modify_package('tags', 'TEST_TAG') self.navigate_to('Browse') self.go_to_submenu('Browse Local') self.select_and_click_action_for_app('details', self.postgre_id) self.assertIn('TEST_TAG', self.driver.find_element_by_xpath( c.TagInDetails).text) def test_download_package(self): """Test check ability to download package from repository. Scenario: 1. Navigate to 'Packages' page. 2. Select PostgreSQL package and click on "More>Download Package". 3. Wait for the package to download. 4. Open the archive and check that the files match the expected list of files. """ self.navigate_to('Manage') self.go_to_submenu('Packages') self.select_action_for_package(self.postgre_id, 'more') self.select_action_for_package(self.postgre_id, 'download_package') time.sleep(3) # Wait for file to download. downloaded_archive_file = 'postgresql.zip' expected_file_list = ['Classes/mock_muranopl.yaml', 'manifest.yaml', 'UI/mock_ui.yaml'] if os.path.isfile(downloaded_archive_file): zf = zipfile.ZipFile(downloaded_archive_file, 'r') self.assertEqual(sorted(expected_file_list), sorted(zf.namelist())) os.remove(downloaded_archive_file) # Clean up. else: self.fail('Failed to download {0}.'.format( downloaded_archive_file)) def test_check_toggle_enabled_package(self): """Test check ability to make package active or inactive Scenario: 1. Navigate to 'Packages' page 2. Select some package and make it inactive "More>Toggle Active" 3. Check that package is inactive 4. Switch to 'Browse' page 5. Check that application is not available on the page 6. Navigate to 'Packages' page 7. Select the same package and make it active "More>Toggle Active" 8. Check that package is active 9. Switch to 'Browse' page 10. Check that application now is available on the page """ self.navigate_to('Manage') self.go_to_submenu('Packages') self.select_action_for_package(self.postgre_id, 'more') self.select_action_for_package(self.postgre_id, 'toggle_enabled') self.wait_for_alert_message() self.check_package_parameter_by_id(self.postgre_id, 'Active', 'False') self.navigate_to('Browse') self.go_to_submenu('Browse Local') # 'Quick Deploy' button contains id of the application. # So, it's possible to definitely check whether it's in catalog or not. btn_xpath = ("//*[@href='{0}/app-catalog/catalog/quick-add/{1}']" "".format(self.url_prefix, self.postgre_id)) self.check_element_not_on_page(by.By.XPATH, btn_xpath) self.navigate_to('Manage') self.go_to_submenu('Packages') self.select_action_for_package(self.postgre_id, 'more') self.select_action_for_package(self.postgre_id, 'toggle_enabled') self.wait_for_alert_message() self.check_package_parameter_by_id(self.postgre_id, 'Active', 'True') self.navigate_to('Browse') self.go_to_submenu('Browse Local') self.check_element_on_page(by.By.XPATH, btn_xpath) def test_check_toggle_enabled_multiple_packages(self): """Test check ability to make multiple packages active or inactive. Scenario: 1. For each package in `package_ids`: a. Navigate to 'Manage>Packages' page. b. Toggle it inactive via "More>Toggle Active". c. Check that the package is inactive. d. Switch to the 'Browse' page. e. Check that the application is not available on the page. 2. Now that all packages are inactive, check that alert message "There are no applications in the catalog" is present on page. 3. For each package in `package_ids`: a. Navigate to 'Manage>Packages' page. b. Toggle it active via "More>Toggle Active". c. Check that the package is active. d. Switch to the 'Browse' page. e. Check that the application is not available on the page. 4. Check that the previous alert message is gone. """ package_ids = [self.hot_app_id, self.mockapp_id, self.postgre_id, self.deployingapp_id] for package_id in package_ids: self.navigate_to('Manage') self.go_to_submenu('Packages') self.select_action_for_package(package_id, 'more') with self.wait_for_page_reload(): self.select_action_for_package(package_id, 'toggle_enabled') self.wait_for_alert_message() self.check_package_parameter_by_id(package_id, 'Active', 'False') self.navigate_to('Browse') self.go_to_submenu('Browse Local') quick_deploy_btn = ( "//*[@href='{0}/app-catalog/catalog/quick-add/{1}']" .format(self.url_prefix, package_id)) self.check_element_not_on_page(by.By.XPATH, quick_deploy_btn) self.check_element_on_page(by.By.XPATH, c.AlertInfo.format( 'There are no applications in the catalog.')) for package_id in package_ids: self.navigate_to('Manage') self.go_to_submenu('Packages') self.select_action_for_package(package_id, 'more') with self.wait_for_page_reload(): self.select_action_for_package(package_id, 'toggle_enabled') self.wait_for_alert_message() self.check_package_parameter_by_id(package_id, 'Active', 'True') self.navigate_to('Browse') self.go_to_submenu('Browse Local') quick_deploy_btn = ( "//*[@href='{0}/app-catalog/catalog/quick-add/{1}']" .format(self.url_prefix, package_id)) self.check_element_on_page(by.By.XPATH, quick_deploy_btn) self.check_element_not_on_page(by.By.XPATH, c.AlertInfo.format( 'There are no applications in the catalog.'), sec=0.1) def test_check_toggle_public_package(self): """Test check ability to make package public or non-public. Scenario: 1. Add default user to alternative project called 'service' 2. Navigate to 'Packages' page 3. Select some package and make it active "More>Toggle Public" 4. Check that package is public 5. Switch to the alt project and check that the application is available in the catalog 6. Switch back to default project 7. Select the same package and inactivate it "More>Toggle Public" 8. Check that package is not public 9. Switch to the alt project and check that the application is not available in the catalog """ default_project_name = self.auth_ref.project_name service_project_name = 'service' service_prj_id = self.get_tenantid_by_name(service_project_name) self.add_user_to_project(service_prj_id, self.auth_ref.user_id) # Generally the new project will appear in the dropdown menu only after # page refresh. But in this case refresh is not necessary. self.navigate_to('Manage') self.go_to_submenu('Packages') self.select_action_for_package(self.postgre_id, 'more') with self.wait_for_page_reload(): self.select_action_for_package(self.postgre_id, 'toggle_public_enabled') self.wait_for_alert_message() self.check_package_parameter_by_id(self.postgre_id, 'Public', 'True') # Check that application is available in other project. self.switch_to_project(service_project_name) self.navigate_to('Browse') self.go_to_submenu('Browse Local') # 'Quick Deploy' button contains id of the application. # So it is possible to definitely determine if it is in catalog or not. btn_xpath = ("//*[@href='{0}/app-catalog/catalog/quick-add/{1}']" "".format(self.url_prefix, self.postgre_id)) self.check_element_on_page(by.By.XPATH, btn_xpath) self.switch_to_project(default_project_name) self.navigate_to('Manage') self.go_to_submenu('Packages') self.select_action_for_package(self.postgre_id, 'more') with self.wait_for_page_reload(): self.select_action_for_package(self.postgre_id, 'toggle_public_enabled') self.wait_for_alert_message() self.check_package_parameter_by_id(self.postgre_id, 'Public', 'False') # Check that application now is not available in other project. self.switch_to_project(service_project_name) self.navigate_to('Browse') self.go_to_submenu('Browse Local') self.check_element_not_on_page(by.By.XPATH, btn_xpath) def test_check_toggle_public_multiple_packages(self): """Test check ability to make multiple packages public or non-public. Scenario: 1. Add default user to alternative project called 'service'. 2. Navigate to 'Manage>Packages' page. 3. For each package in `package_ids`: a. Toggle it public via "More>Toggle Public". b. Check that the package is public. 4. Switch to the alt project and check that each application is available in the catalog and that each application has the public ribbon over its icon. 5. Switch back to default project. 6. For each package in `package_ids`: a. Toggle it non-public via "More>Toggle Public". b. Check that the package is not public. 7. Switch to the alt project and check that each application is not available in the catalog and that the "no applications" alert message appears in the catalog. """ default_project_name = self.auth_ref.project_name service_project_name = 'service' service_prj_id = self.get_tenantid_by_name(service_project_name) self.add_user_to_project(service_prj_id, self.auth_ref.user_id) package_ids = [self.hot_app_id, self.mockapp_id, self.postgre_id, self.deployingapp_id] self.navigate_to('Manage') self.go_to_submenu('Packages') for package_id in package_ids: self.select_action_for_package(package_id, 'more') with self.wait_for_page_reload(): self.select_action_for_package(package_id, 'toggle_public_enabled') self.wait_for_alert_message() self.check_package_parameter_by_id(package_id, 'Public', 'True') self.switch_to_project(service_project_name) self.navigate_to('Browse') self.go_to_submenu('Browse Local') self.check_element_on_page(by.By.CSS_SELECTOR, 'img.ribbon') ribbons = self.driver.find_elements_by_css_selector('img.ribbon') self.assertEqual(4, len(ribbons)) for ribbon in ribbons: self.assertTrue(ribbon.get_attribute('src').endswith('shared.png')) for package_id in package_ids: quick_deploy_btn = ( "//*[@href='{0}/app-catalog/catalog/quick-add/{1}']" .format(self.url_prefix, package_id)) self.check_element_on_page(by.By.XPATH, quick_deploy_btn) self.switch_to_project(default_project_name) self.navigate_to('Manage') self.go_to_submenu('Packages') for package_id in package_ids: self.select_action_for_package(package_id, 'more') with self.wait_for_page_reload(): self.select_action_for_package(package_id, 'toggle_public_enabled') self.wait_for_alert_message() self.check_package_parameter_by_id(package_id, 'Public', 'False') self.switch_to_project(service_project_name) self.navigate_to('Browse') self.go_to_submenu('Browse Local') self.check_element_on_page(by.By.XPATH, c.AlertInfo.format( 'There are no applications in the catalog.')) for package_id in package_ids: quick_deploy_btn = ( "//*[@href='{0}/app-catalog/catalog/quick-add/{1}']" .format(self.url_prefix, package_id)) self.check_element_not_on_page(by.By.XPATH, quick_deploy_btn, sec=0.1) def test_modify_description(self): """Test check ability to change description of the package Scenario: 1. Navigate to 'Packages' page 2. Select package and click on 'Modify Package' 3. Change description """ self.navigate_to('Manage') self.go_to_submenu('Packages') self.select_action_for_package(self.mockapp_id, 'modify_package') self.modify_package('description', 'New Description') self.navigate_to('Browse') self.go_to_submenu('Browse Local') self.assertEqual('New Description', self.driver.find_element_by_xpath( c.MockAppDescr).text) def test_upload_package(self): """Test package uploading via Packages view. Skips category selection step. """ self.navigate_to('Manage') self.go_to_submenu('Packages') self.driver.find_element_by_id(c.UploadPackage).click() el = self.driver.find_element_by_css_selector( "input[name='upload-package']") el.send_keys(self.archive) self.driver.find_element_by_xpath(c.InputSubmit).click() # No application data modification is needed self.driver.find_element_by_xpath(c.InputSubmit).click() self.driver.find_element_by_xpath(c.InputSubmit).click() self.wait_for_alert_message() self.check_package_parameter_by_name(self.archive_name, 'Active', 'True') self.check_package_parameter_by_name(self.archive_name, 'Public', 'False') self.check_package_parameter_by_name(self.archive_name, 'Tenant Name', cfg.common.tenant) def test_upload_package_modify(self): """Test package modifying a package after uploading it.""" self.navigate_to('Manage') self.go_to_submenu('Packages') self.driver.find_element_by_id(c.UploadPackage).click() el = self.driver.find_element_by_css_selector( "input[name='upload-package']") el.send_keys(self.archive) self.driver.find_element_by_xpath(c.InputSubmit).click() pkg_name = self.alt_archive_name self.fill_field(by.By.CSS_SELECTOR, "input[name='modify-name']", pkg_name) label = self.driver.find_element_by_css_selector( "label[for=id_modify-is_public]") label.click() label = self.driver.find_element_by_css_selector( "label[for=id_modify-enabled]") label.click() self.driver.find_element_by_xpath(c.InputSubmit).click() self.driver.find_element_by_xpath(c.InputSubmit).click() self.wait_for_alert_message() self.check_element_on_page( by.By.XPATH, c.AppPackages.format(pkg_name)) self.check_package_parameter_by_name(pkg_name, 'Public', 'True') self.check_package_parameter_by_name(pkg_name, 'Active', 'False') def test_package_share(self): """Test that admin is able to share Murano Apps Scenario: 1. Hit 'Modify Package' on any package 2. Mark 'Public' checkbox 3. Hit 'Update' button 4. Verify, that package is available for other users """ self.navigate_to('Manage') self.go_to_submenu('Packages') self.select_action_for_package(self.mockapp_id, 'modify_package') self.driver.find_element_by_css_selector( "label[for=id_is_public]" ).click() self.driver.find_element_by_xpath(c.InputSubmit).click() self.wait_for_alert_message() self.log_out() self.log_in(self.testuser_name, self.testuser_password) self.navigate_to('Manage') self.go_to_submenu('Packages') self.check_element_on_page( by.By.XPATH, c.AppPackages.format('MockApp')) def test_upload_package_detail(self): """Test check ability to view package details after uploading it.""" self.navigate_to('Manage') self.go_to_submenu('Packages') self.driver.find_element_by_id(c.UploadPackage).click() el = self.driver.find_element_by_css_selector( "input[name='upload-package']") el.send_keys(self.archive) self.driver.find_element_by_xpath(c.InputSubmit).click() # No application data modification is needed self.driver.find_element_by_xpath(c.InputSubmit).click() self.driver.find_element_by_xpath(c.InputSubmit).click() self.wait_for_alert_message() pkg_name = self.archive_name self.check_element_on_page(by.By.LINK_TEXT, pkg_name) self.driver.find_element_by_xpath( "//a[contains(text(), '{0}')]".format(pkg_name)).click() self.assertIn(pkg_name, self.driver.find_element(by.By.XPATH, c.AppDetail).text) @unittest.skipIf(utils.glare_enabled(), "GLARE backend doesn't expose category count") def test_add_pkg_to_category_non_admin(self): """Test package addition to category as non admin user Scenario: 1. Log into OpenStack Horizon dashboard as non-admin user 2. Navigate to 'Packages' page 3. Modify any package by changing its category from 'category 1' to 'category 2' 4. Log out 5. Log into OpenStack Horizon dashboard as admin user 6. Navigate to 'Categories' page 7. Check that 'category 2' has one more package """ # save initial package count self.navigate_to('Manage') self.go_to_submenu('Categories') web_pkg_count = int(self.driver.find_element_by_xpath( "//tr[contains(@data-display, 'Web')]/td[2]").text) database_pkg_count = int(self.driver.find_element_by_xpath( "//tr[contains(@data-display, 'Databases')]/td[2]").text) # relogin as test user self.log_out() self.log_in(self.testuser_name, self.testuser_password) self.navigate_to('Manage') self.go_to_submenu('Packages') self.select_action_for_package(self.postgre_id, 'modify_package') sel = self.driver.find_element_by_xpath( "//select[contains(@name, 'categories')]") sel = ui.Select(sel) sel.deselect_all() sel.select_by_value('Web') self.driver.find_element_by_xpath(c.InputSubmit).click() self.addCleanup(self.murano_client.packages.update, self.postgre_id, {"categories": ["Databases"]}, operation='replace') self.wait_for_alert_message() self.log_out() self.log_in() # check packages count for categories self.navigate_to('Manage') self.go_to_submenu('Categories') self.check_element_on_page( by.By.XPATH, c.CategoryPackageCount.format( 'Web', web_pkg_count + 1)) self.check_element_on_page( by.By.XPATH, c.CategoryPackageCount.format( 'Databases', database_pkg_count - 1)) def test_add_pkg_to_category_without_count(self): """Test package addition to category as non admin user Scenario: 1. Log into OpenStack Horizon dashboard as non-admin user 2. Navigate to 'Packages' page 3. Navigate to package details page 4. Check that 'category 1' is among package's categories 5. Navigate to 'Packages' page 6. Modify any package by changing its category from 'category 1' to 'category 2' 7. Navigate to package details page 8. Check that 'category 2' is among package's categories 9. Check that 'category 1' is not among package's categories """ # relogin as test user self.log_out() self.log_in(self.testuser_name, self.testuser_password) self.navigate_to('Manage') self.go_to_submenu('Packages') self.driver.find_element_by_link_text("MockApp").click() self.check_element_on_page(by.By.XPATH, c.ServiceDetail.format( 'Web')) self.go_to_submenu('Packages') self.select_action_for_package(self.mockapp_id, 'modify_package') sel = self.driver.find_element_by_xpath( "//select[contains(@name, 'categories')]") sel = ui.Select(sel) sel.deselect_all() sel.select_by_value('Databases') self.driver.find_element_by_xpath(c.InputSubmit).click() self.addCleanup(self.murano_client.packages.update, self.mockapp_id, {"categories": ["Web"]}, operation='replace') self.wait_for_alert_message() self.driver.find_element_by_link_text('MockApp').click() self.check_element_on_page(by.By.XPATH, c.ServiceDetail.format( 'Databases')) self.check_element_not_on_page(by.By.XPATH, c.ServiceDetail.format( 'Web')) def test_category_management(self): """Test application category adds and deletes successfully Scenario: 1. Navigate to 'Categories' page 2. Click on 'Add Category' button 3. Create new category and check it's browsed in the table 4. Delete new category and check it's not browsed anymore """ self.navigate_to('Manage') self.go_to_submenu('Categories') self.driver.find_element_by_id(c.AddCategory).click() self.fill_field(by.By.XPATH, "//input[@id='id_name']", 'TEST_CATEGORY') self.driver.find_element_by_xpath(c.InputSubmit).click() self.wait_for_alert_message() delete_new_category_btn = c.DeleteCategory.format('TEST_CATEGORY') self.driver.find_element_by_xpath(delete_new_category_btn).click() self.driver.find_element_by_xpath(c.ConfirmDeletion).click() self.wait_for_alert_message() self.check_element_not_on_page(by.By.XPATH, delete_new_category_btn) def test_sharing_app_without_permission(self): """Tests sharing Murano App without permission Scenario: 1) Login as admin; 2) Identity -> Users: Create User: User Name: Test_service_user Primary Project: service Enabled: Yes 3) Login to Horizon as an 'Test_service_user'; 4) Catalog -> Manage -> Packages: Import Package check the public checkbox is disabled 5) Try to modify created package and check the public checkbox is disabled. 6) Delete new package """ service_prj_name = 'service' new_user = {'name': 'Test_service_user', 'password': 'somepassword', 'email': 'test_serv_user@email.com'} try: self.delete_user(new_user['name']) except Exception: pass # Create new user in 'service' prj service_prj_id = self.get_tenantid_by_name(service_prj_name) self.create_user(tenant_id=service_prj_id, **new_user) # login as 'Test_service_user' self.log_out() self.log_in(new_user['name'], new_user['password']) # Import package self.navigate_to('Manage') self.go_to_submenu('Packages') self.driver.find_element_by_id(c.UploadPackage).click() el = self.driver.find_element_by_css_selector( "input[name='upload-package']") el.send_keys(self.archive) self.driver.find_element_by_xpath(c.InputSubmit).click() public_checkbox = self.driver.find_element_by_id('id_modify-is_public') active_checkbox = self.driver.find_element_by_id('id_modify-enabled') # check the Public checkbox is disabled self.assertTrue(public_checkbox.get_attribute("disabled")) if not active_checkbox.is_selected(): active_checkbox.click() self.driver.find_element_by_xpath(c.InputSubmit).click() self.driver.find_element_by_xpath(c.InputSubmit).click() self.wait_for_alert_message() self.check_element_on_page( by.By.XPATH, c.AppPackages.format(self.archive_name)) package = self.driver.find_element_by_xpath( c.AppPackages.format(self.archive_name)) pkg_id = package.get_attribute("data-object-id") self.select_action_for_package(pkg_id, 'modify_package') is_public_checkbox = self.driver.find_element_by_id('id_is_public') # check the Public checkbox is disabled self.assertTrue(is_public_checkbox.get_attribute("disabled")) self.driver.find_element_by_xpath(c.InputSubmit).click() self.wait_for_alert_message() # Clean up self.select_action_for_package(pkg_id, 'more') self.select_action_for_package(pkg_id, 'delete_package') self.driver.find_element_by_xpath(c.ConfirmDeletion).click() self.log_out() self.log_in() self.delete_user(new_user['name']) def test_filter_package_by_name(self): """Test filtering by name. Test checks ability to filter packages by name in Packages page. Scenario: 1. Navigate to 'Manage' > 'Packages' panel. 2. Set the search type to 'Name'. 3. For each name in ``packages_by_name``: a. Set search criterion in search field to current name. b. Click on 'Filter' and verify that expected packages are shown and unexpected packages are not shown. """ self.navigate_to('Manage') self.go_to_submenu('Packages') packages_by_name = ['DeployingApp', 'HotExample', 'MockApp', 'PostgreSQL'] if not utils.glare_enabled(): packages_by_name.extend( ['Application Development Library', 'Core library']) self.wait_element_is_clickable( by.By.CSS_SELECTOR, c.PackageFilterDropdownBtn).click() self.wait_element_is_clickable( by.By.CSS_SELECTOR, c.PackageFilterTypeBtn.format('name')).click() for package_name in packages_by_name: self.fill_field(by.By.CSS_SELECTOR, c.PackageFilterInput, package_name) with self.wait_for_page_reload(): self.driver.find_element_by_id(c.PackageFilterBtn).click() self.check_element_on_page(by.By.PARTIAL_LINK_TEXT, package_name) for other_package in set(packages_by_name) - set([package_name]): self.check_element_not_on_page(by.By.PARTIAL_LINK_TEXT, other_package, sec=0.1) def test_filter_package_by_type(self): """Test filtering by type. Test checks ability to filter packages by type in Packages page. Scenario: 1. Navigate to 'Manage' > 'Packages' panel. 2. Set the search type to 'Type'. 3. For each type in ``packages_by_type.keys()``: a. Set search criterion in search field to current type. b. Click on 'Filter' and verify that expected packages are shown and unexpected packages are not shown. """ self.navigate_to('Manage') self.go_to_submenu('Packages') all_packages = ['Application Development Library', 'Core library', 'DeployingApp', 'HotExample', 'MockApp', 'PostgreSQL'] packages_by_type = { 'Library': ['Application Development Library', 'Core library'], 'Application': ['DeployingApp', 'HotExample', 'MockApp', 'PostgreSQL'] } self.wait_element_is_clickable( by.By.CSS_SELECTOR, c.PackageFilterDropdownBtn).click() self.wait_element_is_clickable( by.By.CSS_SELECTOR, c.PackageFilterTypeBtn.format('type')).click() for package_type, package_list in packages_by_type.items(): self.fill_field(by.By.CSS_SELECTOR, c.PackageFilterInput, package_type) with self.wait_for_page_reload(): self.driver.find_element_by_id(c.PackageFilterBtn).click() for package_name in package_list: self.check_element_on_page(by.By.PARTIAL_LINK_TEXT, package_name) for other_package in set(all_packages) - set(package_list): self.check_element_not_on_page(by.By.PARTIAL_LINK_TEXT, other_package, sec=0.1) @unittest.skipIf(utils.glare_enabled(), "Filtering apps by description doesn't work with GLARE; " "this test should be fixed with bug #1616856.") def test_filter_package_by_keyword(self): """Test filtering by keyword. Test checks ability to filter packages by keyword in Packages page. Scenario: 1. Upload 4 randomly named packages, with random descriptions and tags, where each tested keyword is `pkg_description`,`pkg_tag`. 2. Navigate to 'Manage' > 'Packages' panel. 3. Set the search type to 'KeyWord'. 4. For each keyword in ``packages_by_keyword.keys()``: a. Set search criterion in search field to current keyword. b. Click on 'Filter' and verify that expected packages are shown and unexpected packages are not shown. """ self.navigate_to('Manage') self.go_to_submenu('Packages') package_names = [] package_ids = [] packages_by_keyword = {} for i in range(4): pkg_name = self.gen_random_resource_name('package') pkg_description = self.gen_random_resource_name('description', 8) pkg_tag = self.gen_random_resource_name('tag', 8) pkg_data = { 'description': pkg_description, 'tags': [pkg_tag] } packages_by_keyword[pkg_description + "," + pkg_tag] = pkg_name package_names.append(pkg_name) package_id = self.upload_package(pkg_name, pkg_data) package_ids.append(package_id) self.addCleanup(self.murano_client.packages.delete, package_id) self.wait_element_is_clickable( by.By.CSS_SELECTOR, c.PackageFilterDropdownBtn).click() self.wait_element_is_clickable( by.By.CSS_SELECTOR, c.PackageFilterTypeBtn.format('search'))\ .click() for keyword, package_name in packages_by_keyword.items(): self.fill_field(by.By.CSS_SELECTOR, c.PackageFilterInput, keyword) with self.wait_for_page_reload(): self.driver.find_element_by_id(c.PackageFilterBtn).click() self.check_element_on_page(by.By.PARTIAL_LINK_TEXT, package_name) for other_package in set(package_names) - set([package_name]): self.check_element_not_on_page(by.By.PARTIAL_LINK_TEXT, other_package, sec=0.1) def test_delete_package(self): """Test delete package. Test checks ability to delete package successfully. Scenario: 1. Upload randomly named package. 2. Navigate to 'Manage' > 'Packages' panel. 3. Delete the dynamically created package and afterward check that the package is no longer present on the page. """ def _try_delete_package(pkg_id): try: self.murano_client.packages.delete(pkg_id) except muranoclient_exc.HTTPNotFound: pass pkg_name = self.gen_random_resource_name('package') pkg_description = self.gen_random_resource_name('description', 8) pkg_tag = self.gen_random_resource_name('tag', 8) pkg_data = { 'description': pkg_description, 'tags': [pkg_tag] } pkg_id = utils.upload_app_package(self.murano_client, pkg_name, pkg_data) self.addCleanup(_try_delete_package, pkg_id) self.navigate_to('Manage') self.go_to_submenu('Packages') self.select_action_for_package(pkg_id, 'more') self.select_action_for_package(pkg_id, 'delete_package') with self.wait_for_page_reload(): self.driver.find_element_by_xpath(c.ConfirmDeletion).click() self.check_element_not_on_page(by.By.PARTIAL_LINK_TEXT, pkg_name, sec=0.1) def test_delete_multiple_packages(self): """Test delete multipe packages. Test checks ability to delete multiple packages successfully. Scenario: 1. Upload 3 randomly named packages. 2. Navigate to 'Manage' > 'Packages' panel. 3. Delete each dynamically created package and afterward check that the package is no longer present on the page. """ def _try_delete_package(pkg_id): try: self.murano_client.packages.delete(pkg_id) except muranoclient_exc.HTTPNotFound: pass packages = [] for i in range(3): pkg_name = self.gen_random_resource_name('package') pkg_description = self.gen_random_resource_name('description', 8) pkg_tag = self.gen_random_resource_name('tag', 8) pkg_data = { 'description': pkg_description, 'tags': [pkg_tag] } pkg_id = utils.upload_app_package(self.murano_client, pkg_name, pkg_data) packages.append({'name': pkg_name, 'id': pkg_id}) self.addCleanup(_try_delete_package, pkg_id) self.navigate_to('Manage') self.go_to_submenu('Packages') for package in packages: self.select_action_for_package(package['id'], 'more') self.select_action_for_package(package['id'], 'delete_package') with self.wait_for_page_reload(): self.driver.find_element_by_xpath(c.ConfirmDeletion).click() self.check_element_not_on_page(by.By.PARTIAL_LINK_TEXT, package['name'], sec=0.1) @unittest.skipIf(utils.glare_enabled(), "GLARE backend doesn't expose category package count") def test_correct_number_of_packages_per_category_in_catalog(self): """Tests correct number of packages per category in catalog view. Scenario: 1) Dynamically create one randomly named package per category in ``default_categories``. 2) Navigate to Browse > Browse Local page. 3) Check that all packages appear under 'All' category. 4) For each category in ``default_categories``: a) Click on the category dropdown selector. b) Scroll down to the current category. c) Check that the category link has the correct package count. d) Click on the category link. e) Check that the correct packages are present on page and that other packages are not present. """ self.navigate_to('Manage') self.go_to_submenu('Packages') # The list of default categories that appears in Browse>Browse Local. # Each category is associated with a count: a count of n indicates a # category which appears in the dropdown as "Category (n)". default_categories = [ ('Application Servers', 1), ('Big Data', 1), ('Databases', 2), ('Key-Value Storage', 1), ('Load Balancers', 1), ('Message Queue', 1), ('Microsoft Services', 1), ('SAP', 1), ('Web', 3) ] packages_by_cat = { 'All': ['DeployingApp', 'MockApp', 'HotExample', 'PostgreSQL'], 'Databases': ['PostgreSQL'], 'Web': ['DeployingApp', 'MockApp'] } for category in default_categories: pkg_name = self.gen_random_resource_name('package', 8) packages_by_cat.setdefault(category[0], []).append(pkg_name) packages_by_cat['All'].append(pkg_name) pkg_data = {"categories": [category[0]]} package_id = self.upload_package(pkg_name, pkg_data) self.addCleanup(self.murano_client.packages.delete, package_id) self.navigate_to('Browse') self.go_to_submenu('Browse Local') category_link_name = 'All' next_page_link = '//a[text()="Next Page"]' category_link = '//a[contains(text(), "{0}")]' package_title = '//div[contains(@class, "description")]' \ '/h4[contains(text(), "{0}")]' # Check that the correct number of packages are present for 'All'. all_packages = [] next_btn = self.driver.find_element_by_xpath(next_page_link) # Only look for the next button for up to 3 seconds. self.driver.implicitly_wait(3) while next_btn.is_displayed: package_titles = self.driver.find_elements_by_css_selector( 'div.description > h4') all_packages.extend([p.text for p in package_titles]) # The last page will not contain the next button; ignore the error. try: next_btn = self.driver.find_element_by_xpath(next_page_link) except exceptions.NoSuchElementException: next_btn.is_displayed = False else: with self.wait_for_page_reload(): next_btn.click() # Reset implicitly wait to default time. self.driver.implicitly_wait(30) self.assertEqual(sorted(packages_by_cat['All']), sorted(all_packages)) # Check that the correct number of packages are present per category. for pos, category in enumerate(default_categories): category, package_count = category self.check_element_on_page(by.By.XPATH, c.CategorySelector.format( category_link_name)) el = self.driver.find_element_by_xpath( c.CategorySelector.format(category_link_name)) el.click() el.send_keys(Keys.DOWN) # Scroll onto the dropdown. cat_link = category_link.format(category) self.check_element_on_page(by.By.XPATH, cat_link) el = self.driver.find_element_by_xpath(cat_link) self.assertIn('({0})'.format(package_count), el.text) for i in range(pos): el.send_keys(Keys.DOWN) # Scroll down far enough to see el. with self.wait_for_page_reload(): el.click() for cat, packages in packages_by_cat.items(): if cat == 'All': continue if cat == category: for package in packages: self.check_element_on_page( by.By.XPATH, package_title.format(package)) else: for package in packages: self.check_element_not_on_page( by.By.XPATH, package_title.format(package), sec=0.1) category_link_name = category class TestSuiteRepository(base.PackageTestCase): _apps_to_delete = set() def _compose_app(self, name, require=None): package_dir = os.path.join(self.serve_dir, 'apps/', name) shutil.copytree(c.PackageDir, package_dir) app_name = utils.compose_package( name, os.path.join(package_dir, 'manifest.yaml'), package_dir, require=require, archive_dir=os.path.join(self.serve_dir, 'apps/'), ) self._apps_to_delete.add(name) return app_name def _compose_bundle(self, name, app_names): bundles_dir = os.path.join(self.serve_dir, 'bundles/') shutil.os.mkdir(bundles_dir) utils.compose_bundle(os.path.join(bundles_dir, name + '.bundle'), app_names) def _make_pkg_zip_regular_file(self, name): file_name = os.path.join(self.serve_dir, 'apps', name + '.zip') with open(file_name, 'w') as f: f.write("I'm not an application. I'm not a zip file at all") def _make_non_murano_zip_in_pkg(self, name): file_name = os.path.join(self.serve_dir, 'apps', 'manifest.yaml') with open(file_name, 'w') as f: f.write("Description: I'm not a murano package at all") zip_name = os.path.join(self.serve_dir, 'apps', name + '.zip') with zipfile.ZipFile(zip_name, 'w') as archive: archive.write(file_name) def _make_zip_pkg(self, name, size): file_name = os.path.join(self.serve_dir, 'apps', 'images.lst') self._compose_app(name) # Create file with size 10 MB with open(file_name, 'wb') as f: f.seek(size - 1) f.write('\0') # Add created file to archive (the archive already has files). zip_name = os.path.join(self.serve_dir, 'apps', name + '.zip') with zipfile.ZipFile(zip_name, 'a') as archive: archive.write(file_name) def setUp(self): super(TestSuiteRepository, self).setUp() self.serve_dir = tempfile.mkdtemp(suffix="repo") def serve_function(): class Handler(SimpleHTTPServer.SimpleHTTPRequestHandler): pass os.chdir(self.serve_dir) httpd = SocketServer.TCPServer( ("0.0.0.0", 8099), Handler, bind_and_activate=False) httpd.allow_reuse_address = True httpd.server_bind() httpd.server_activate() httpd.serve_forever() self.p = multiprocessing.Process(target=serve_function) self.p.start() def tearDown(self): super(TestSuiteRepository, self).tearDown() self.p.terminate() for package in self.murano_client.packages.list(include_disabled=True): if package.name in self._apps_to_delete: self.murano_client.packages.delete(package.id) self._apps_to_delete.remove(package.name) shutil.rmtree(self.serve_dir) def test_import_package_by_url(self): """Test package importing via url.""" pkg_name = "dummy_package" self._compose_app(pkg_name) self.navigate_to('Manage') self.go_to_submenu('Packages') self.driver.find_element_by_id(c.UploadPackage).click() sel = self.driver.find_element_by_css_selector( "select[name='upload-import_type']") sel = ui.Select(sel) sel.select_by_value("by_url") el = self.driver.find_element_by_css_selector( "input[name='upload-url']") el.send_keys("http://127.0.0.1:8099/apps/{0}.zip".format(pkg_name)) self.driver.find_element_by_xpath(c.InputSubmit).click() # No application data modification is needed self.driver.find_element_by_xpath(c.InputSubmit).click() self.driver.find_element_by_xpath(c.InputSubmit).click() self.wait_for_alert_message() self.check_element_on_page( by.By.XPATH, c.AppPackages.format(pkg_name)) def test_import_package_from_repo(self): """Test package importing via fqn from repo with dependent apps.""" pkg_name_parent = "PackageParent" pkg_name_child = "PackageChild" pkg_name_grand_child = "PackageGrandChild" self._compose_app(pkg_name_parent, require={pkg_name_child: ''}) self._compose_app(pkg_name_child, require={pkg_name_grand_child: ''}) self._compose_app(pkg_name_grand_child) self.navigate_to('Manage') self.go_to_submenu('Packages') self.driver.find_element_by_id(c.UploadPackage).click() sel = self.driver.find_element_by_css_selector( "select[name='upload-import_type']") sel = ui.Select(sel) sel.select_by_value("by_name") el = self.driver.find_element_by_css_selector( "input[name='upload-repo_name']") el.send_keys("{0}".format(pkg_name_parent)) self.driver.find_element_by_xpath(c.InputSubmit).click() self.driver.find_element_by_xpath(c.InputSubmit).click() self.driver.find_element_by_xpath(c.InputSubmit).click() self.wait_for_alert_message() pkg_names = [pkg_name_parent, pkg_name_child, pkg_name_grand_child] for pkg_name in pkg_names: self.check_element_on_page( by.By.XPATH, c.AppPackages.format(pkg_name)) def test_import_bundle_by_url(self): """Test bundle importing via url.""" pkg_name_one = "PackageOne" pkg_name_two = "PackageTwo" pkg_name_parent = "PackageParent" pkg_name_child = "PackageChild" self._compose_app(pkg_name_one) self._compose_app(pkg_name_two) self._compose_app(pkg_name_parent, require={pkg_name_child: ''}) self._compose_app(pkg_name_child) bundle_name = 'PackageWithPackages' self._compose_bundle(bundle_name, [pkg_name_parent, pkg_name_one, pkg_name_two]) self.navigate_to('Manage') self.go_to_submenu('Packages') self.driver.find_element_by_id(c.ImportBundle).click() sel = self.driver.find_element_by_css_selector( "select[name='upload-import_type']") sel = ui.Select(sel) sel.select_by_value("by_url") el = self.driver.find_element_by_css_selector( "input[name='upload-url']") el.send_keys( "http://127.0.0.1:8099/bundles/{0}.bundle".format(bundle_name)) self.driver.find_element_by_xpath(c.InputSubmit).click() self.wait_for_alert_message() pkg_names = [pkg_name_parent, pkg_name_child, pkg_name_one, pkg_name_two] for pkg_name in pkg_names: self.check_element_on_page( by.By.XPATH, c.AppPackages.format(pkg_name)) def test_import_bundle_from_repo(self): """Test bundle importing via fqn from repo.""" pkg_name_parent = "PackageParent" pkg_name_child = "PackageChild" pkg_name_grand_child = "PackageGrandChild" pkg_name_single = "PackageSingle" self._compose_app(pkg_name_single) self._compose_app(pkg_name_parent, require={pkg_name_child: ''}) self._compose_app(pkg_name_child, require={pkg_name_grand_child: ''}) self._compose_app(pkg_name_grand_child) bundle_name = 'PackageWithPackages' self._compose_bundle(bundle_name, [pkg_name_parent, pkg_name_single]) self.navigate_to('Manage') self.go_to_submenu('Packages') self.driver.find_element_by_id(c.ImportBundle).click() sel = self.driver.find_element_by_css_selector( "select[name='upload-import_type']") sel = ui.Select(sel) sel.select_by_value("by_name") el = self.driver.find_element_by_css_selector( "input[name='upload-name']") el.send_keys("{0}".format(bundle_name)) self.driver.find_element_by_xpath(c.InputSubmit).click() self.wait_for_alert_message() pkg_names = [pkg_name_parent, pkg_name_child, pkg_name_grand_child, pkg_name_single] for pkg_name in pkg_names: self.check_element_on_page( by.By.XPATH, c.AppPackages.format(pkg_name)) def test_import_file_regular_zip_file(self): """Import regular zip archive. Scenario: 1. Log in Horizon with admin credentials. 2. Navigate to 'Packages' page. 3. Click 'Import Package' and select 'File' as a package source. 4. Choose very big zip file. 5. Click on 'Next' button. 6. Check that the package is successfully added and check that it exists in the packages table. """ pkg_name = self.gen_random_resource_name('pkg') self._make_zip_pkg(name=pkg_name, size=1) self.navigate_to('Manage') self.go_to_submenu('Packages') self.driver.find_element_by_id(c.UploadPackage).click() sel = self.driver.find_element_by_css_selector( "select[name='upload-import_type']") sel = ui.Select(sel) sel.select_by_value("by_name") el = self.driver.find_element_by_css_selector( "input[name='upload-repo_name']") el.send_keys("{0}".format(pkg_name)) self.driver.find_element_by_xpath(c.InputSubmit).click() # Wait for successful upload message to appear. self.wait_for_alert_message() # Click next, then create. self.driver.find_element_by_xpath(c.InputSubmit).click() self.driver.find_element_by_xpath(c.InputSubmit).click() self.check_element_on_page( by.By.XPATH, c.AppPackages.format(pkg_name)) def test_import_package_by_invalid_url(self): """Negative test when package is imported by invalid url.""" pkg_name = self.gen_random_resource_name('pkg') self._compose_app(pkg_name) self.navigate_to('Manage') self.go_to_submenu('Packages') # Invalid folder self.driver.find_element_by_id(c.UploadPackage).click() sel = self.driver.find_element_by_css_selector( "select[name='upload-import_type']") sel = ui.Select(sel) sel.select_by_value("by_url") el = self.driver.find_element_by_css_selector( "input[name='upload-url']") el.send_keys("http://127.0.0.1:8099/None/{0}.zip".format(pkg_name)) self.driver.find_element_by_xpath(c.InputSubmit).click() self.wait_for_error_message() # HTTP connect error self.driver.find_element_by_id(c.UploadPackage).click() sel = self.driver.find_element_by_css_selector( "select[name='upload-import_type']") sel = ui.Select(sel) sel.select_by_value("by_url") el = self.driver.find_element_by_css_selector( "input[name='upload-url']") el.send_keys("http://127.0.0.2:12345/apps/{0}.zip".format(pkg_name)) self.driver.find_element_by_xpath(c.InputSubmit).click() self.wait_for_error_message(sec=90) # Invalid app name self.driver.find_element_by_id(c.UploadPackage).click() sel = self.driver.find_element_by_css_selector( "select[name='upload-import_type']") sel = ui.Select(sel) sel.select_by_value("by_url") el = self.driver.find_element_by_css_selector( "input[name='upload-url']") el.send_keys( "http://127.0.0.1:8099/apps/invalid_{0}.zip".format(pkg_name)) self.driver.find_element_by_xpath(c.InputSubmit).click() self.wait_for_error_message() self.check_element_not_on_page( by.By.XPATH, c.AppPackages.format(pkg_name)) def test_import_package_by_invalid_name(self): """Negative test when package is imported by invalid name from repo.""" pkg_name = self.gen_random_resource_name('pkg') self._compose_app(pkg_name) pkg_to_import = "invalid_" + pkg_name self.navigate_to('Manage') self.go_to_submenu('Packages') self.driver.find_element_by_id(c.UploadPackage).click() sel = self.driver.find_element_by_css_selector( "select[name='upload-import_type']") sel = ui.Select(sel) sel.select_by_value("by_name") el = self.driver.find_element_by_css_selector( "input[name='upload-repo_name']") el.send_keys("{0}".format(pkg_to_import)) self.driver.find_element_by_xpath(c.InputSubmit).click() self.wait_for_error_message() self.check_element_not_on_page( by.By.XPATH, c.AppPackages.format(pkg_to_import)) def test_import_non_zip_file(self): """"Negative test import regualr file instead of zip package.""" # Create dummy package with zip file replaced by text one pkg_name = self.gen_random_resource_name('pkg') self._compose_app(pkg_name) self._make_pkg_zip_regular_file(pkg_name) self.navigate_to('Manage') self.go_to_submenu('Packages') self.driver.find_element_by_id(c.UploadPackage).click() sel = self.driver.find_element_by_css_selector( "select[name='upload-import_type']") sel = ui.Select(sel) sel.select_by_value("by_name") el = self.driver.find_element_by_css_selector( "input[name='upload-repo_name']") el.send_keys("{0}".format(pkg_name)) self.driver.find_element_by_xpath(c.InputSubmit).click() err_msg = self.wait_for_error_message() self.assertIn('File is not a zip file', err_msg) self.check_element_not_on_page( by.By.XPATH, c.AppPackages.format(pkg_name)) def test_import_invalid_zip_file(self): """"Negative test import zip file which is not a murano package.""" # At first create dummy package with zip file replaced by text one pkg_name = self.gen_random_resource_name('pkg') self._compose_app(pkg_name) self._make_non_murano_zip_in_pkg(pkg_name) self.navigate_to('Manage') self.go_to_submenu('Packages') self.driver.find_element_by_id(c.UploadPackage).click() sel = self.driver.find_element_by_css_selector( "select[name='upload-import_type']") sel = ui.Select(sel) sel.select_by_value("by_name") el = self.driver.find_element_by_css_selector( "input[name='upload-repo_name']") el.send_keys("{0}".format(pkg_name)) self.driver.find_element_by_xpath(c.InputSubmit).click() err_msg = self.wait_for_error_message() self.assertIn("There is no item named 'manifest.yaml'", err_msg) self.check_element_not_on_page( by.By.XPATH, c.AppPackages.format(pkg_name)) @unittest.skipIf(utils.glare_enabled(), 'GLARE allows importing big files') def test_import_big_zip_file_negative(self): """Import very big zip archive. Scenario: 1. Log in Horizon with admin credentials 2. Navigate to 'Packages' page 3. Click 'Import Package' and select 'File' as a package source 4. Choose very big zip file 5. Click on 'Next' button 6. Check that error message that user can't upload file more than 5 MB is displayed """ pkg_name = self.gen_random_resource_name('pkg') self._make_zip_pkg(name=pkg_name, size=10 * 1024 * 1024) # import package and choose big zip file for it self.navigate_to('Manage') self.go_to_submenu('Packages') self.driver.find_element_by_id(c.UploadPackage).click() sel = self.driver.find_element_by_css_selector( "select[name='upload-import_type']") sel = ui.Select(sel) sel.select_by_value("by_name") el = self.driver.find_element_by_css_selector( "input[name='upload-repo_name']") el.send_keys("{0}".format(pkg_name)) self.driver.find_element_by_xpath(c.InputSubmit).click() # check that error message appeared error_message = ("Error: 400 Bad Request: Uploading file is too " "large. The limit is 5 Mb") self.check_alert_message(error_message) self.check_element_not_on_page( by.By.XPATH, c.AppPackages.format(pkg_name)) @unittest.skipUnless(utils.glare_enabled(), 'Without GLARE backend murano restricts file size') def test_import_big_zip_file(self): """Import very big zip archive. Scenario: 1. Log in Horizon with admin credentials 2. Navigate to 'Packages' page 3. Click 'Import Package' and select 'File' as a package source 4. Choose very big zip file 5. Click on 'Next' button 6. Check that error message that user can't upload file more than 5 MB is displayed """ pkg_name = self.gen_random_resource_name('pkg') self._make_zip_pkg(name=pkg_name, size=10 * 1024 * 1024) # import package and choose big zip file for it self.navigate_to('Manage') self.go_to_submenu('Packages') self.driver.find_element_by_id(c.UploadPackage).click() sel = self.driver.find_element_by_css_selector( "select[name='upload-import_type']") sel = ui.Select(sel) sel.select_by_value("by_name") el = self.driver.find_element_by_css_selector( "input[name='upload-repo_name']") el.send_keys("{0}".format(pkg_name)) self.driver.find_element_by_xpath(c.InputSubmit).click() # no additional input needed self.driver.find_element_by_xpath(c.InputSubmit).click() self.driver.find_element_by_xpath(c.InputSubmit).click() self.wait_for_alert_message() self.check_element_on_page( by.By.XPATH, c.AppPackages.format(pkg_name)) def test_import_bundle_when_dependencies_installed(self): """Test bundle import if some dependencies are installed. Check that bundle can be imported if some of its dependencies are already installed from repository. """ pkg_name_parent_one = self.gen_random_resource_name('pkg') pkg_name_child_one = self.gen_random_resource_name('pkg') pkg_name_grand_child = self.gen_random_resource_name('pkg') pkg_name_parent_two = self.gen_random_resource_name('pkg') pkg_name_child_two = self.gen_random_resource_name('pkg') self._compose_app(pkg_name_parent_one, require={pkg_name_child_one: ''}) self._compose_app(pkg_name_child_one, require={pkg_name_grand_child: ''}) self._compose_app(pkg_name_grand_child) self._compose_app(pkg_name_parent_two, require={pkg_name_child_two: ''}) self._compose_app(pkg_name_child_two) bundle_name = self.gen_random_resource_name('bundle') self._compose_bundle(bundle_name, [pkg_name_parent_one, pkg_name_parent_two]) utils.upload_app_package(self.murano_client, pkg_name_grand_child, {"categories": ["Web"], "tags": ["tag"]}) utils.upload_app_package(self.murano_client, pkg_name_child_two, {"categories": ["Web"], "tags": ["tag"]}) self.navigate_to('Manage') self.go_to_submenu('Packages') self.driver.find_element_by_id(c.ImportBundle).click() sel = self.driver.find_element_by_css_selector( "select[name='upload-import_type']") sel = ui.Select(sel) sel.select_by_value("by_name") el = self.driver.find_element_by_css_selector( "input[name='upload-name']") el.send_keys("{0}".format(bundle_name)) self.driver.find_element_by_xpath(c.InputSubmit).click() self.wait_for_alert_message() pkg_names = [pkg_name_parent_one, pkg_name_child_one, pkg_name_grand_child, pkg_name_parent_two, pkg_name_child_two] for pkg_name in pkg_names: self.check_element_on_page( by.By.XPATH, c.AppPackages.format(pkg_name)) class TestSuitePackageCategory(base.PackageTestCase): def _import_package_with_category(self, package_archive, category): self.go_to_submenu('Packages') self.driver.find_element_by_id(c.UploadPackage).click() el = self.driver.find_element_by_css_selector( "input[name='upload-package']") el.send_keys(package_archive) self.driver.find_element_by_xpath(c.InputSubmit).click() self.driver.find_element_by_xpath(c.InputSubmit).click() # choose the required category self.driver.find_element_by_xpath( c.PackageCategory.format(category)).click() self.driver.find_element_by_xpath(c.InputSubmit).click() self.wait_for_alert_message() # To wait till the focus is swithced # from modal dialog back to the window. self.wait_for_sidebar_is_loaded() def setUp(self): super(TestSuitePackageCategory, self).setUp() # add new category self.category = str(uuid.uuid4()) self.navigate_to('Manage') self.go_to_submenu('Categories') self.driver.find_element_by_id(c.AddCategory).click() self.fill_field( by.By.XPATH, "//input[@id='id_name']", self.category) self.driver.find_element_by_xpath(c.InputSubmit).click() self.wait_for_alert_message() if not utils.glare_enabled(): # GLARE backend doesn't expose category package count self.check_element_on_page( by.By.XPATH, c.CategoryPackageCount.format(self.category, 0)) # save category id self.category_id = self.get_element_id(self.category) def tearDown(self): super(TestSuitePackageCategory, self).tearDown() # delete created category self.murano_client.categories.delete(self.category_id) @unittest.skipIf(utils.glare_enabled(), "GLARE backend doesn't expose category package count") def test_add_delete_category_for_package(self): """Test package importing with new category and changing the category. Scenario: 1. Log into OpenStack Horizon dashboard as admin user 2. Navigate to 'Categories' page 3. Click on 'Add Category' button 4. Create new category and check it's browsed in the table 5. Navigate to 'Packages' page 6. Click on 'Import Package' button 7. Import package and select created 'test' category for it 8. Navigate to "Categories" page 9. Check that package count = 1 for created category 10. Navigate to 'Packages' page 11. Modify imported earlier package, by changing its category 12. Navigate to 'Categories' page 13. Check that package count = 0 for created category """ # add new package to the created category self._import_package_with_category(self.archive, self.category) # Check that package count = 1 for created category self.go_to_submenu('Categories') self.check_element_on_page( by.By.XPATH, c.CategoryPackageCount.format(self.category, 1)) # Modify imported earlier package by changing its category self.go_to_submenu('Packages') package = self.driver.find_element_by_xpath( c.AppPackages.format(self.archive_name)) pkg_id = package.get_attribute("data-object-id") self.select_action_for_package(pkg_id, 'modify_package') sel = self.driver.find_element_by_xpath( "//select[contains(@name, 'categories')]") sel = ui.Select(sel) sel.deselect_all() sel.select_by_value('Web') self.driver.find_element_by_xpath(c.InputSubmit).click() self.wait_for_alert_message() # Check that package count = 0 for created category self.go_to_submenu('Categories') self.check_element_on_page( by.By.XPATH, c.CategoryPackageCount.format(self.category, 0)) def test_add_delete_category_for_package_without_count(self): """Test package importing with new category and changing the category. Scenario: 1. Log into OpenStack Horizon dashboard as admin user 2. Navigate to 'Categories' page 3. Click on 'Add Category' button 4. Create new category 5. Navigate to 'Packages' page 6. Click on 'Import Package' button 7. Import package and select created 'test' category for it 8. Navigate to package details page 9. Check that new category is among package's categories 10. Navigate to 'Packages' page 11. Modify imported earlier package, by changing its category 12. Navigate to package details page 13. Check that new category is not among package's categories """ # add new package to the created category self._import_package_with_category(self.archive, self.category) self.go_to_submenu('Packages') self.driver.find_element_by_link_text(self.archive_name).click() self.check_element_on_page(by.By.XPATH, c.ServiceDetail.format( self.category)) # Modify imported earlier package by changing its category self.go_to_submenu('Packages') package = self.driver.find_element_by_xpath( c.AppPackages.format(self.archive_name)) pkg_id = package.get_attribute("data-object-id") self.select_action_for_package(pkg_id, 'modify_package') sel = self.driver.find_element_by_xpath( "//select[contains(@name, 'categories')]") sel = ui.Select(sel) sel.deselect_all() sel.select_by_value('Web') self.driver.find_element_by_xpath(c.InputSubmit).click() self.wait_for_alert_message() self.go_to_submenu('Packages') self.driver.find_element_by_link_text(self.archive_name).click() self.check_element_on_page(by.By.XPATH, c.ServiceDetail.format( 'Web')) self.check_element_not_on_page(by.By.XPATH, c.ServiceDetail.format( self.category)) def test_filter_by_new_category(self): """Filter by new category from Catalog>Browse page Scenario: 1. Log into OpenStack Horizon dashboard as admin user 2. Navigate to 'Categories' page 3. Click on 'Add Category' button 4. Create new category and check it's browsed in the table 5. Navigate to 'Packages' page 6. Click on 'Import Package' button 7. Import package and select created 'test' category for it 8. Navigate to "Catalog>Browse" page 9. Select new category in "App category" dropdown list """ self._import_package_with_category(self.archive, self.category) self.navigate_to('Browse') self.go_to_submenu('Browse Local') self.driver.find_element_by_xpath( c.CategorySelector.format('All')).click() self.driver.find_element_by_partial_link_text(self.category).click() self.check_element_on_page( by.By.XPATH, c.App.format(self.archive_name)) def test_filter_by_category_from_env_components(self): """Filter by new category from Environment Components page Scenario: 1. Log into OpenStack Horizon dashboard as admin user 2. Navigate to 'Categories' page 3. Click on 'Add Category' button 4. Create new category and check it's browsed in the table 5. Navigate to 'Packages' page 6. Click on 'Import Package' button 7. Import package and select created 'test' category for it 8. Navigate to 'Environments' page 9. Create environment 10. Select new category in 'App category' dropdown list 11. Check that imported package is displayed 12. Select 'Web' category in 'App category' dropdown list 13. Check that imported package is not displayed """ self._import_package_with_category(self.archive, self.category) # create environment env_name = str(uuid.uuid4()) self.navigate_to('Applications') self.go_to_submenu('Environments') self.create_environment(env_name) self.go_to_submenu('Environments') self.check_element_on_page(by.By.LINK_TEXT, env_name) # filter by new category self.select_action_for_environment(env_name, 'show') self.driver.find_element_by_xpath(c.EnvAppsCategorySelector).click() self.driver.find_element_by_partial_link_text(self.category).click() # check that imported package is displayed self.check_element_on_page( by.By.XPATH, c.EnvAppsCategory.format(self.archive_name)) # filter by 'Web' category self.driver.find_element_by_xpath(c.EnvAppsCategorySelector).click() self.driver.find_element_by_partial_link_text('Web').click() # check that imported package is not displayed self.check_element_not_on_page( by.By.XPATH, c.EnvAppsCategory.format(self.archive_name)) def test_add_existing_category(self): """Add category with name of already existing category Scenario: 1. Log into OpenStack Horizon dashboard as admin user 2. Navigate to 'Categories' page 3. Add new category 4. Check that new category has appeared in category list 5. Try to add category with the same name 6. Check that appropriate user friendly error message has appeared. """ self.navigate_to('Manage') self.go_to_submenu('Categories') self.driver.find_element_by_id(c.AddCategory).click() self.fill_field( by.By.XPATH, "//input[@id='id_name']", self.category) self.driver.find_element_by_xpath(c.InputSubmit).click() error_message = ("Error: Requested operation conflicts " "with an existing object.") self.check_alert_message(error_message) @unittest.skipIf(utils.glare_enabled(), "GLARE backend doesn't expose category count, " "'Delete category' button is always displayed with GLARE") def test_delete_category_with_package(self): """Deletion of category with package in it Scenario: 1. Log into OpenStack Horizon dashboard as admin user 2. Navigate to 'Categories' page 3. Add new category 4. Navigate to 'Packages' page 5. Import package and select created category for it 6. Navigate to "Categories" page 7. Check that package count = 1 for created category 8. Check that there is no 'Delete Category' button for the category """ # add new package to the created category self._import_package_with_category(self.archive, self.category) # Check that package count = 1 for created category self.go_to_submenu('Categories') self.check_element_on_page( by.By.XPATH, c.CategoryPackageCount.format(self.category, 1)) self.check_element_not_on_page( by.By.XPATH, c.DeleteCategory.format(self.category)) def test_list_of_existing_categories(self): """Checks that list of categories is available Scenario: 1. Navigate to 'Categories' page 2. Check that list of categories available and basic categories ("Web", "Databases") are present in the list """ self.go_to_submenu("Categories") categories_table_locator = (by.By.CSS_SELECTOR, "table#categories") self.check_element_on_page(*categories_table_locator) for category in ("Databases", "Web"): category_locator = (by.By.XPATH, "//tr[@data-display='{}']".format(category)) self.check_element_on_page(*category_locator) class TestSuiteCategoriesPagination(base.PackageTestCase): def setUp(self): super(TestSuiteCategoriesPagination, self).setUp() self.categories_to_delete = [] # Create at least 5 more pages with categories for x in range(cfg.common.items_per_page * 5): name = self.gen_random_resource_name('category') category = self.murano_client.categories.add({'name': name}) self.categories_to_delete.append(category.id) def tearDown(self): super(TestSuiteCategoriesPagination, self).tearDown() for category_id in self.categories_to_delete: self.murano_client.categories.delete(id=category_id) def test_category_pagination(self): """Test categories pagination in case of many categories created """ self.navigate_to('Manage') self.go_to_submenu('Categories') categories_list = [elem.name for elem in self.murano_client.categories.list()] # Murano client lists the categories in order of creation # starting from the last created. So need to reverse it to align with # the table in UI form. Where categories are listed starting from the # first created to the last one. categories_list.reverse() categories_per_page = cfg.common.items_per_page pages_itself = [categories_list[i:i + categories_per_page] for i in range(0, len(categories_list), categories_per_page)] for i, names in enumerate(pages_itself, 1): for name in names: self.check_element_on_page(by.By.XPATH, c.Status.format(name)) if i != len(pages_itself): self.driver.find_element_by_xpath(c.NextBtn).click() # Wait till the Next button disappear # Otherwise 'Prev' button from the previous page might be used self.check_element_not_on_page(by.By.XPATH, c.NextBtn) pages_itself.reverse() for i, names in enumerate(pages_itself, 1): for name in names: self.check_element_on_page(by.By.XPATH, c.Status.format(name)) if i != len(pages_itself): self.driver.find_element_by_xpath(c.PrevBtn).click() class TestSuiteMultipleEnvironments(base.ApplicationTestCase): def test_create_two_environments_and_delete_them_at_once(self): """Test check ability to create and delete multiple environments Scenario: 1. Create two environments 2. Navigate to environment list 3. Check created environments 4. Delete created environments at once """ self.navigate_to('Applications') self.go_to_submenu('Environments') self.create_environment('test_create_del_env_1') self.go_to_submenu('Environments') self.create_environment('test_create_del_env_2', by_id=True) self.go_to_submenu('Environments') self.driver.find_element_by_css_selector( "label[for=ui-id-1]").click() self.driver.find_element_by_css_selector( c.DeleteEnvironments).click() self.driver.find_element_by_xpath(c.ConfirmDeletion).click() self.wait_for_alert_message() self.check_element_not_on_page(by.By.LINK_TEXT, 'test_create_del_env_1') self.check_element_not_on_page(by.By.LINK_TEXT, 'test_create_del_env_2') def test_deploy_two_environments_at_once(self): """Test check ability to deploy multiple environments Scenario: 1. Add two apps to different environments 2. Navigate to environment list 3. Check created environments 4. Deploy created environments at once """ self.add_app_to_env(self.mockapp_id) self.add_app_to_env(self.mockapp_id) self.navigate_to('Applications') self.go_to_submenu('Environments') self.driver.find_element_by_css_selector( "label[for=ui-id-1]").click() self.driver.find_element_by_css_selector( c.DeployEnvironments).click() # check statuses of two environments self.check_element_on_page(by.By.XPATH, c.EnvStatus.format('quick-env-1', 'Ready'), sec=90) self.check_element_on_page(by.By.XPATH, c.EnvStatus.format('quick-env-2', 'Ready'), sec=90) def test_abandon_two_environments_at_once(self): """Test check ability to abandon multiple environments Scenario: 1. Add two apps to different environments 2. Navigate to environment list 3. Check created environments 4. Deploy created environments at once 5. Abandon environments before they are deployed """ self.add_app_to_env(self.deployingapp_id) self.add_app_to_env(self.deployingapp_id) self.navigate_to('Applications') self.go_to_submenu('Environments') self.driver.find_element_by_css_selector( "label[for=ui-id-1]").click() self.driver.find_element_by_css_selector( c.DeployEnvironments).click() self.go_to_submenu('Environments') self.driver.find_element_by_css_selector( "label[for=ui-id-1]").click() self.driver.find_element_by_css_selector( c.AbandonEnvironments).click() self.driver.find_element_by_xpath(c.ConfirmAbandon).click() self.wait_for_alert_message() self.check_element_not_on_page(by.By.LINK_TEXT, 'quick-env-1') self.check_element_not_on_page(by.By.LINK_TEXT, 'quick-env-2') def test_check_necessary_buttons_are_available(self): """Test check if the necessary buttons are available Scenario: 1. Create 4 environments with different statuses. 2. Check that all action buttons are on page. 3. Check that "Deploy Environments" button is only clickable if env with status "Ready to deploy" is checked 4. Check that "Delete Environments" button is only clickable if envs with statuses "Ready", "Ready to deploy", "Ready to configure" are checked 5. Check that "Abandon Environments" button is only clickable if env with status "Ready", "Deploying" are checked """ self.navigate_to('Applications') self.go_to_submenu('Environments') self.create_environment('quick-env-1') self.go_to_submenu('Environments') self.check_element_on_page(by.By.XPATH, c.EnvStatus.format('quick-env-1', 'Ready to configure')) self.check_element_on_page(by.By.CSS_SELECTOR, c.DeleteEnvironments) self.add_app_to_env(self.mockapp_id) self.go_to_submenu('Environments') self.check_element_on_page(by.By.XPATH, c.EnvStatus.format('quick-env-2', 'Ready to deploy')) self.check_element_on_page(by.By.CSS_SELECTOR, c.DeployEnvironments) self.add_app_to_env(self.mockapp_id) self.driver.find_element_by_id('services__action_deploy_env').click() self.check_element_on_page(by.By.XPATH, c.Status.format('Ready'), sec=90) self.go_to_submenu('Environments') self.check_element_on_page(by.By.XPATH, c.EnvStatus.format('quick-env-3', 'Ready')) self.check_element_on_page(by.By.CSS_SELECTOR, c.AbandonEnvironments) self.add_app_to_env(self.deployingapp_id) self.driver.find_element_by_id('services__action_deploy_env').click() self.go_to_submenu('Environments') self.check_element_on_page(by.By.XPATH, c.EnvStatus.format('quick-env-4', 'Deploying')) env_1_checkbox = self.driver.find_element_by_xpath( c.EnvCheckbox.format('quick-env-1')) env_2_checkbox = self.driver.find_element_by_xpath( c.EnvCheckbox.format('quick-env-2')) env_3_checkbox = self.driver.find_element_by_xpath( c.EnvCheckbox.format('quick-env-3')) # select 'Ready to deploy' env env_2_checkbox.click() # check that Deploy is possible self.check_element_on_page(by.By.CSS_SELECTOR, c.DeployEnvironments) self.check_element_not_on_page(by.By.CSS_SELECTOR, c.DeployEnvironmentsDisabled) # add 'Ready to configure' env to selection env_1_checkbox.click() # check that Deploy is no more possible self.check_element_on_page(by.By.CSS_SELECTOR, c.DeployEnvironmentsDisabled) # add 'Ready' env to selection env_3_checkbox.click() # check that Delete is possible self.check_element_on_page(by.By.CSS_SELECTOR, c.DeleteEnvironments) self.check_element_not_on_page(by.By.CSS_SELECTOR, c.DeleteEnvironmentsDisabled) # add 'Deploying' env to selection env_4_checkbox = self.driver.find_element_by_xpath( c.EnvCheckbox.format('quick-env-4')) env_4_checkbox.click() # check that Delete is no more possible self.check_element_on_page(by.By.CSS_SELECTOR, c.DeleteEnvironmentsDisabled) # check that Abandon is not possible self.check_element_on_page(by.By.CSS_SELECTOR, c.AbandonEnvironmentsDisabled) # unselect all envs but 'Deploying' and 'Ready' env_1_checkbox.click() env_2_checkbox.click() # check that Abandon is now possible self.check_element_on_page(by.By.CSS_SELECTOR, c.AbandonEnvironments) self.check_element_not_on_page(by.By.CSS_SELECTOR, c.AbandonEnvironmentsDisabled) self.check_element_on_page(by.By.XPATH, c.EnvStatus.format('quick-env-4', 'Ready'), sec=90) murano-dashboard-5.0.0/muranodashboard/tests/functional/HotApp/0000775000175100017510000000000013245511556024645 5ustar zuulzuul00000000000000murano-dashboard-5.0.0/muranodashboard/tests/functional/HotApp/template.yaml0000666000175100017510000000076313245511125027344 0ustar zuulzuul00000000000000heat_template_version: 2013-05-23 description: > Hello world HOT template that just defines a single server. Contains just base features to verify base HOT support. parameters: flavor: type: string description: Flavor for the server to be created default: m1.small constraints: - allowed_values: [ m1.small, m1.medium, m1.large ] resources: server: type: OS::Nova::Server properties: image: cirros-0.3.4-x86_64-uec flavor: { get_param: flavor } murano-dashboard-5.0.0/muranodashboard/tests/settings.py0000666000175100017510000000447713245511125023530 0ustar zuulzuul00000000000000# 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 socket SECRET_KEY = 'HELLA_SECRET!' DATABASES = {'default': {'ENGINE': 'django.db.backends.sqlite3', 'NAME': 'test'}} from horizon.test.settings import * # noqa socket.setdefaulttimeout(1) DEBUG = False TEMPLATE_DEBUG = DEBUG TESTSERVER = 'http://testserver' MESSAGE_STORAGE = 'django.contrib.messages.storage.cookie.CookieStorage' TEST_RUNNER = 'django_nose.NoseTestSuiteRunner' NOSE_ARGS = ['--nocapture', '--nologcapture', '--cover-package=muranodashboard'] EMAIL_BACKEND = 'django.core.mail.backends.locmem.EmailBackend' SESSION_ENGINE = 'django.contrib.sessions.backends.cache' OPENSTACK_ADDRESS = "localhost" OPENSTACK_ADMIN_TOKEN = "openstack" OPENSTACK_KEYSTONE_URL = "http://%s:5000/v2.0" % OPENSTACK_ADDRESS OPENSTACK_KEYSTONE_ADMIN_URL = "http://%s:35357/v2.0" % OPENSTACK_ADDRESS OPENSTACK_KEYSTONE_DEFAULT_ROLE = "Member" # Silence logging output during tests. LOGGING = { 'version': 1, 'disable_existing_loggers': False, 'handlers': { 'null': { 'level': 'DEBUG', 'class': 'logging.NullHandler', }, }, 'loggers': { 'django.db.backends': { 'handlers': ['null'], 'propagate': False, }, 'horizon': { 'handlers': ['null'], 'propagate': False, }, 'novaclient': { 'handlers': ['null'], 'propagate': False, }, 'keystoneclient': { 'handlers': ['null'], 'propagate': False, }, 'neutronclient': { 'handlers': ['null'], 'propagate': False, }, 'nose.plugins.manager': { 'handlers': ['null'], 'propagate': False, } } } murano-dashboard-5.0.0/muranodashboard/tests/test_utils.py0000666000175100017510000000322013245511125024050 0ustar zuulzuul00000000000000# 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 testtools from muranodashboard.common import utils class BunchTests(testtools.TestCase): def test_get_attr(self): obj = utils.Bunch(one=10) self.assertEqual(10, obj.one) def test_get_item(self): obj = utils.Bunch(two=15) self.assertEqual(15, obj['two']) def test_in(self): obj = utils.Bunch(one=10) self.assertIn('one', obj) def test_iteration(self): obj = utils.Bunch(one=10, two=15) sorted_objs = sorted([o for o in obj]) self.assertEqual([10, 15], sorted_objs) def test_set_attr(self): obj = utils.Bunch() obj.one = 10 self.assertEqual(10, obj['one']) def test_set_item(self): obj = utils.Bunch() obj['two'] = 20 self.assertEqual(20, obj['two']) def test_del_attr(self): obj = utils.Bunch(one=10) del obj.one self.assertNotIn('one', obj) def test_del_item(self): obj = utils.Bunch(two=20) del obj['two'] self.assertNotIn('two', obj) murano-dashboard-5.0.0/muranodashboard/tests/__init__.py0000666000175100017510000000000013245511125023401 0ustar zuulzuul00000000000000murano-dashboard-5.0.0/muranodashboard/tests/test_tabs.py0000666000175100017510000001032013245511125023640 0ustar zuulzuul00000000000000# 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 mock from muranodashboard.catalog import tabs from openstack_dashboard.test import helpers class TestLicenseTab(helpers.APITestCase): @mock.patch('muranodashboard.catalog.tabs.services') def test_license(self, mock_services): """Check that a license is returned.""" # Fake the services.get_app_forms() call. m = mock.Mock() m.base_fields = { 'license': mock.Mock( description='Lorem ipsum dolor sit ' 'amet, consectetur adipiscing elit.') } mock_services.get_app_forms.return_value = [('', m)] # Fake an application object, needed when instantiating tabs. app = mock.Mock() app.id = 1 group = tabs.ApplicationTabs(self.request, application=app) l = group.get_tabs()[2] # Should return the license description l._get_license() self.assertEqual( 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.', l.app.license) @mock.patch('muranodashboard.catalog.tabs.services') def test_no_license(self, mock_services): """Check that no license is returned.""" # Fake the services.get_app_forms() call. m = mock.Mock() m.base_fields = {} mock_services.get_app_forms.return_value = [('', m)] # Fake an application object, needed when instantiating tabs. app = mock.Mock() app.id = 1 group = tabs.ApplicationTabs(self.request, application=app) l = group.get_tabs()[2] # Should return the license description l._get_license() self.assertEqual('', l.app.license) class TestRequirementsTab(helpers.APITestCase): @mock.patch('muranodashboard.catalog.tabs.services') def test_requirements(self, mock_services): """Check that requirements are returned.""" m = mock.Mock() m.base_fields = { 'flavor': mock.Mock(requirements={ 'min_disk': 10, 'min_vcpus': 2, 'min_memory_mb': 2048, 'max_disk': 25, 'max_vcpus': 5, 'max_memory_mb': 16000, }) } mock_services.get_app_forms.return_value = [('', m)] app = mock.Mock() app.id = 1 group = tabs.ApplicationTabs(self.request, application=app) r = group.get_tabs()[1] # Should return the requirements list used by the template file. r._get_requirements() self.assertIn('Instance flavor:', r.app.requirements) flavor_req = r.app.requirements[1] self.assertIn('Minimum disk size: 10 GB', flavor_req) self.assertIn('Minimum vCPUs: 2', flavor_req) self.assertIn('Minimum RAM size: 2048 MB', flavor_req) self.assertIn('Maximum disk size: 25 GB', flavor_req) self.assertIn('Maximum vCPUs: 5', flavor_req) self.assertIn('Maximum RAM size: 16000 MB', flavor_req) @mock.patch('muranodashboard.catalog.tabs.services') def test_no_requirements(self, mock_services): """Check that no requirements are returned.""" m = mock.Mock() m.base_fields = {} mock_services.get_app_forms.return_value = [('', m)] app = mock.Mock() app.id = 1 group = tabs.ApplicationTabs(self.request, application=app) r = group.get_tabs()[1] # Should return an empty requirements list r._get_requirements() self.assertListEqual([], r.app.requirements) murano-dashboard-5.0.0/muranodashboard/tests/test_fields.py0000666000175100017510000001066713245511125024173 0ustar zuulzuul00000000000000# 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.core import exceptions from muranodashboard.dynamic_ui import fields from openstack_dashboard.test import helpers class TestFlavorField(helpers.APITestCase): def setUp(self): """Set up the Flavor Class and novaclient.""" super(TestFlavorField, self).setUp() class FlavorFlave(object): def __init__(self, id, name, vcpus, disk, ram): self.name = name self.vcpus = vcpus self.disk = disk self.ram = ram self.id = id novaclient = self.stub_novaclient() novaclient.flavors = self.mox.CreateMockAnything() # Set up the Flavor list novaclient.flavors.list().MultipleTimes().AndReturn( [FlavorFlave('id1', 'small', vcpus=1, disk=50, ram=1000), FlavorFlave('id2', 'medium', vcpus=2, disk=100, ram=2000), FlavorFlave('id3', 'large', vcpus=3, disk=750, ram=4000)]) def test_no_filter(self): """Check that all flavors are returned.""" self.mox.ReplayAll() # No requirements, should return all flavors f = fields.FlavorChoiceField() initial_request = {} f.update(initial_request, self.request) self.assertEqual([ ('id3', 'large'), ('id2', 'medium'), ('id1', 'small') ], f.choices) def test_multiple_filter(self): """Check that 2 flavors are returned.""" self.mox.ReplayAll() # Fake a requirement for 2 CPUs, should return medium and large f = fields.FlavorChoiceField(requirements={'min_vcpus': 2}) f.update({}, self.request) self.assertEqual([('id3', 'large'), ('id2', 'medium')], f.choices) def test_single_filter(self): """Check that one flavor is returned.""" self.mox.ReplayAll() # Fake a requirement for 2 CPUs and 200 GB disk, should return medium f = fields.FlavorChoiceField( requirements={'min_vcpus': 2, 'min_disk': 200}) initial_request = {} f.update(initial_request, self.request) self.assertEqual([('id3', 'large')], f.choices) def test_no_matches_filter(self): """Check that no flavors are returned.""" self.mox.ReplayAll() # Fake a requirement for 4 CPUs, should return no flavors f = fields.FlavorChoiceField(requirements={'min_vcpus': 4}) initial_request = {} f.update(initial_request, self.request) self.assertEqual([], f.choices) class TestPasswordField(helpers.TestCase): def test_valid_input(self): f = fields.PasswordField('') for char in f.special_characters: if char != '\\': password = 'aA1111' + char self.assertIsNone(f.run_validators(password)) def test_short_input(self): f = fields.PasswordField('') password = 'aA@111' self.assertRaises(exceptions.ValidationError, f.run_validators, password) def test_input_without_special_characters(self): f = fields.PasswordField('') password = 'aA11111' self.assertRaises(exceptions.ValidationError, f.run_validators, password) def test_input_without_digits(self): f = fields.PasswordField('') password = 'aA@@@@@' self.assertRaises(exceptions.ValidationError, f.run_validators, password) def test_input_without_lowecase(self): f = fields.PasswordField('') password = 'A1@@@@@' self.assertRaises(exceptions.ValidationError, f.run_validators, password) def test_input_without_uppercase(self): f = fields.PasswordField('') password = 'a1@@@@@' self.assertRaises(exceptions.ValidationError, f.run_validators, password) murano-dashboard-5.0.0/muranodashboard/dashboard.py0000666000175100017510000000214513245511125022443 0ustar zuulzuul00000000000000# 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) murano-dashboard-5.0.0/muranodashboard/templatetags/0000775000175100017510000000000013245511556022640 5ustar zuulzuul00000000000000murano-dashboard-5.0.0/muranodashboard/templatetags/custom_filters.py0000666000175100017510000000230213245511125026243 0ustar zuulzuul00000000000000# 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 import template from django.template import defaultfilters from six.moves.urllib import parse as urlparse register = template.Library() @register.filter(name='is_checkbox') def is_checkbox(field): return isinstance(field.field.widget, forms.CheckboxInput) @register.filter(name='firsthalf') def first_half(seq): half_len = len(seq) / 2 return seq[:half_len] @register.filter(name='lasthalf') def last_half(seq): half_len = len(seq) / 2 return seq[half_len:] @register.filter(name='unquote') @defaultfilters.stringfilter def unquote_raw(value): return urlparse.unquote(value) murano-dashboard-5.0.0/muranodashboard/templatetags/jsonify.py0000666000175100017510000000140113245511125024661 0ustar zuulzuul00000000000000# 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 import template import json register = template.Library() @register.filter(name='jsonify') def jsonify(value): return json.dumps(value) murano-dashboard-5.0.0/muranodashboard/templatetags/__init__.py0000666000175100017510000000137513245511125024751 0ustar zuulzuul00000000000000# 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. # NOTE(zhurong): import horizon filters for murano-dashboard, So # murano-dashboard templates can use horizon register filters, such as # parse_isotime, timesince_or_never, etc from horizon.utils import filters # noqa murano-dashboard-5.0.0/HACKING.rst0000666000175100017510000000055013245511125016565 0ustar zuulzuul00000000000000Murano 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 murano-dashboard-5.0.0/tox.ini0000666000175100017510000000475313245511141016311 0ustar zuulzuul00000000000000[tox] envlist = py35,py27,pep8 minversion = 1.6 skipsdist = True [testenv] usedevelop = True install_command = pip install -c{env:UPPER_CONSTRAINTS_FILE:https://git.openstack.org/cgit/openstack/requirements/plain/upper-constraints.txt?h=stable/queens} -U {opts} {packages} setenv = VIRTUAL_ENV={envdir} NOSE_WITH_OPENSTACK=1 NOSE_OPENSTACK_COLOR=1 NOSE_OPENSTACK_RED=0.05 NOSE_OPENSTACK_YELLOW=0.025 NOSE_OPENSTACK_SHOW_ELAPSED=1 DJANGO_SETTINGS_MODULE=muranodashboard.tests.settings passenv = http_proxy HTTP_PROXY https_proxy HTTPS_PROXY no_proxy NO_PROXY deps = -r{toxinidir}/requirements.txt -r{toxinidir}/test-requirements.txt http://tarballs.openstack.org/horizon/horizon-master.tar.gz http://tarballs.openstack.org/heat-dashboard/heat-dashboard-master.tar.gz commands = {toxinidir}/manage.py test muranodashboard --settings=muranodashboard.tests.settings [testenv:pep8] sitepackages = False commands = flake8 [testenv:py27-ocata] install_command = pip install -chttps://git.openstack.org/cgit/openstack/requirements/plain/upper-constraints.txt?h=stable/ocata {opts} {packages} deps = -r{toxinidir}/requirements.txt -r{toxinidir}/test-requirements.txt http://tarballs.openstack.org/horizon/horizon-stable-ocata.tar.gz [testenv:venv] commands = {posargs} [testenv:cover] commands = {toxinidir}/tools/cover.sh {posargs} [testenv:pyflakes] deps = flake8 commands = flake8 [testenv:eslint] deps = -r{toxinidir}/test-requirements.txt passenv = * commands = nodeenv -p npm install npm run lint [testenv:releasenotes] commands = sphinx-build -a -E -W -d releasenotes/build/doctrees -b html releasenotes/source releasenotes/build/html [testenv:docs] commands = python setup.py build_sphinx [testenv:makemessages] commands = pybabel extract -F babel-django.cfg -o muranodashboard/locale/django.pot -k gettext_noop -k gettext_lazy -k ngettext_lazy:1,2 -k ugettext_noop -k ugettext_lazy -k ungettext_lazy:1,2 -k npgettext:1c,2,3 -k pgettext_lazy:1c,2 -k npgettext_lazy:1c,2,3 muranodashboard pybabel extract -F babel-djangojs.cfg -o muranodashboard/locale/djangojs.pot -k gettext_noop -k gettext_lazy -k ngettext_lazy:1,2 -k ugettext_noop -k ugettext_lazy -k ungettext_lazy:1,2 -k npgettext:1c,2,3 -k pgettext_lazy:1c,2 -k npgettext_lazy:1c,2,3 muranodashboard [flake8] show-source = true builtins = _ exclude=build,.venv,.git,.tox,dist,doc,*lib/python*,*egg,tools,horizon,settings.py,*/local/*,functional_tests murano-dashboard-5.0.0/murano_dashboard.egg-info/0000775000175100017510000000000013245511556021777 5ustar zuulzuul00000000000000murano-dashboard-5.0.0/murano_dashboard.egg-info/SOURCES.txt0000664000175100017510000003476413245511556023701 0ustar zuulzuul00000000000000.coveragerc .eslintrc .zuul.yaml AUTHORS CONTRIBUTING.rst ChangeLog HACKING.rst LICENSE README.rst babel-django.cfg babel-djangojs.cfg karma.conf.js manage.py package.json requirements.txt setup.cfg setup.py test-requirements.txt tox.ini doc/source/conf.py doc/source/index.rst doc/source/_static/.placeholder doc/source/_theme/theme.conf functional_tests/collect_results.sh functional_tests/env_pkg_prepare.sh functional_tests/generate_html_report.py functional_tests/post_test_hook.sh functional_tests/pre_test_hook.sh functional_tests/run_test.sh functional_tests/templates/empty.template functional_tests/templates/report.template murano_dashboard.egg-info/PKG-INFO murano_dashboard.egg-info/SOURCES.txt murano_dashboard.egg-info/dependency_links.txt murano_dashboard.egg-info/not-zip-safe murano_dashboard.egg-info/pbr.json murano_dashboard.egg-info/requires.txt murano_dashboard.egg-info/top_level.txt muranodashboard/__init__.py muranodashboard/dashboard.py muranodashboard/exceptions.py muranodashboard/middleware.py muranodashboard/version.py muranodashboard/views.py muranodashboard/api/__init__.py muranodashboard/api/packages.py muranodashboard/api/rest/__init__.py muranodashboard/api/rest/environments.py muranodashboard/api/rest/packages.py muranodashboard/catalog/__init__.py muranodashboard/catalog/forms.py muranodashboard/catalog/panel.py muranodashboard/catalog/tabs.py muranodashboard/catalog/urls.py muranodashboard/catalog/views.py muranodashboard/categories/__init__.py muranodashboard/categories/forms.py muranodashboard/categories/panel.py muranodashboard/categories/tables.py muranodashboard/categories/urls.py muranodashboard/categories/views.py muranodashboard/common/__init__.py muranodashboard/common/cache.py muranodashboard/common/fields.py muranodashboard/common/net.py muranodashboard/common/utils.py muranodashboard/common/widgets.py muranodashboard/conf/murano_policy.json muranodashboard/dynamic_ui/__init__.py muranodashboard/dynamic_ui/fields.py muranodashboard/dynamic_ui/forms.py muranodashboard/dynamic_ui/helpers.py muranodashboard/dynamic_ui/services.py muranodashboard/dynamic_ui/version.py muranodashboard/dynamic_ui/yaql_expression.py muranodashboard/dynamic_ui/yaql_functions.py muranodashboard/environments/__init__.py muranodashboard/environments/api.py muranodashboard/environments/consts.py muranodashboard/environments/forms.py muranodashboard/environments/panel.py muranodashboard/environments/tables.py muranodashboard/environments/tabs.py muranodashboard/environments/topology.py muranodashboard/environments/urls.py muranodashboard/environments/views.py muranodashboard/images/__init__.py muranodashboard/images/forms.py muranodashboard/images/panel.py muranodashboard/images/tables.py muranodashboard/images/urls.py muranodashboard/images/views.py muranodashboard/local/__init__.py muranodashboard/local/enabled/_50_dashboard_catalog.py muranodashboard/local/enabled/_51_muranodashboard.py muranodashboard/local/enabled/_60_panel_group_browse.py muranodashboard/local/enabled/_63_panel_murano_catalog.py muranodashboard/local/enabled/_70_panel_group_manage.py muranodashboard/local/enabled/_71_panel_murano_packages.py muranodashboard/local/enabled/_72_panel_murano_images.py muranodashboard/local/enabled/_73_panel_murano_categories.py muranodashboard/local/enabled/_80_panel_group_applications.py muranodashboard/local/enabled/_81_panel_applications_environments.py muranodashboard/local/local_settings.d/_50_murano.py muranodashboard/locale/cs/LC_MESSAGES/django.po muranodashboard/locale/cs/LC_MESSAGES/djangojs.po muranodashboard/locale/de/LC_MESSAGES/django.po muranodashboard/locale/de/LC_MESSAGES/djangojs.po muranodashboard/locale/en_GB/LC_MESSAGES/django.po muranodashboard/locale/en_GB/LC_MESSAGES/djangojs.po muranodashboard/locale/fr/LC_MESSAGES/django.po muranodashboard/locale/id/LC_MESSAGES/django.po muranodashboard/locale/id/LC_MESSAGES/djangojs.po muranodashboard/locale/ja/LC_MESSAGES/django.po muranodashboard/locale/ja/LC_MESSAGES/djangojs.po muranodashboard/locale/ko_KR/LC_MESSAGES/django.po muranodashboard/locale/ko_KR/LC_MESSAGES/djangojs.po muranodashboard/locale/pt_BR/LC_MESSAGES/django.po muranodashboard/locale/pt_BR/LC_MESSAGES/djangojs.po muranodashboard/locale/ru/LC_MESSAGES/django.po muranodashboard/locale/ru/LC_MESSAGES/djangojs.po muranodashboard/locale/tr_TR/LC_MESSAGES/django.po muranodashboard/locale/tr_TR/LC_MESSAGES/djangojs.po muranodashboard/locale/zh_CN/LC_MESSAGES/django.po muranodashboard/locale/zh_CN/LC_MESSAGES/djangojs.po muranodashboard/packages/__init__.py muranodashboard/packages/consts.py muranodashboard/packages/forms.py muranodashboard/packages/panel.py muranodashboard/packages/tables.py muranodashboard/packages/urls.py muranodashboard/packages/views.py muranodashboard/static/app/core/metadata/metadata.service.spec.js muranodashboard/static/app/murano/murano.module.js muranodashboard/static/app/murano/murano.module.spec.js muranodashboard/static/app/murano/murano.service.js muranodashboard/static/app/murano/murano.service.spec.js muranodashboard/static/muranodashboard/css/catalog.css muranodashboard/static/muranodashboard/css/checkbox.css muranodashboard/static/muranodashboard/css/components.css muranodashboard/static/muranodashboard/css/deployments.css muranodashboard/static/muranodashboard/css/hide_app_name.css muranodashboard/static/muranodashboard/css/packages.css muranodashboard/static/muranodashboard/css/reports.css muranodashboard/static/muranodashboard/css/support_placeholder.css muranodashboard/static/muranodashboard/images/ext-net.png muranodashboard/static/muranodashboard/images/icon.png muranodashboard/static/muranodashboard/images/murano_srv.png muranodashboard/static/muranodashboard/images/murano_srv_red.png muranodashboard/static/muranodashboard/images/shared-sm.png muranodashboard/static/muranodashboard/images/shared.png muranodashboard/static/muranodashboard/js/add-select.js muranodashboard/static/muranodashboard/js/draggable-components.js muranodashboard/static/muranodashboard/js/environments-in-place.js muranodashboard/static/muranodashboard/js/external-ad.js muranodashboard/static/muranodashboard/js/horizon.muranotopology.js muranodashboard/static/muranodashboard/js/import_bundle_form.js muranodashboard/static/muranodashboard/js/load-modals.js muranodashboard/static/muranodashboard/js/mixed-mode.js muranodashboard/static/muranodashboard/js/more-less.js muranodashboard/static/muranodashboard/js/murano.tables.js muranodashboard/static/muranodashboard/js/passwordfield.js muranodashboard/static/muranodashboard/js/submit-disabled.js muranodashboard/static/muranodashboard/js/support_placeholder.js muranodashboard/static/muranodashboard/js/triStateCheckbox.js muranodashboard/static/muranodashboard/js/upload_form.js muranodashboard/static/muranodashboard/js/validators.js muranodashboard/templates/catalog/_app_license.html muranodashboard/templates/catalog/_app_requirements.html muranodashboard/templates/catalog/_overview.html muranodashboard/templates/catalog/add_app.html muranodashboard/templates/catalog/app_details.html muranodashboard/templates/catalog/app_tile.html muranodashboard/templates/catalog/categories.html muranodashboard/templates/catalog/env_switcher.html muranodashboard/templates/catalog/index.html muranodashboard/templates/catalog/quick_deploy.html muranodashboard/templates/categories/_add.html muranodashboard/templates/categories/_packages.html muranodashboard/templates/categories/add.html muranodashboard/templates/categories/index.html muranodashboard/templates/common/_detail_header.html muranodashboard/templates/common/_form_fields.html muranodashboard/templates/common/tri_state_checkbox/base.html muranodashboard/templates/common/tri_state_checkbox/input.html muranodashboard/templates/deployments/_cell_reports.html muranodashboard/templates/deployments/_cell_services.html muranodashboard/templates/deployments/_logs.html muranodashboard/templates/deployments/index.html muranodashboard/templates/deployments/reports.html muranodashboard/templates/environments/_create.html muranodashboard/templates/environments/_data_table.html muranodashboard/templates/environments/create.html muranodashboard/templates/environments/index.html muranodashboard/templates/images/_mark.html muranodashboard/templates/images/index.html muranodashboard/templates/images/mark.html muranodashboard/templates/packages/_detail.html muranodashboard/templates/packages/_import_bundle.html muranodashboard/templates/packages/_modify_package.html muranodashboard/templates/packages/_package_params.html muranodashboard/templates/packages/_upload.html muranodashboard/templates/packages/detail.html muranodashboard/templates/packages/import_bundle.html muranodashboard/templates/packages/index.html muranodashboard/templates/packages/modify_package.html muranodashboard/templates/packages/upload.html muranodashboard/templates/services/_application_info.html muranodashboard/templates/services/_data_table.html muranodashboard/templates/services/_detail_topology.html muranodashboard/templates/services/_environment_info.html muranodashboard/templates/services/_logs.html muranodashboard/templates/services/_network_info.html muranodashboard/templates/services/_overview.html muranodashboard/templates/services/_service_list.html muranodashboard/templates/services/_unit_info.html muranodashboard/templates/services/_wizard_create.html muranodashboard/templates/services/app_tile_small.html muranodashboard/templates/services/details.html muranodashboard/templates/services/index.html muranodashboard/templates/services/wizard_create.html muranodashboard/templatetags/__init__.py muranodashboard/templatetags/custom_filters.py muranodashboard/templatetags/jsonify.py muranodashboard/tests/__init__.py muranodashboard/tests/settings.py muranodashboard/tests/test_fields.py muranodashboard/tests/test_tabs.py muranodashboard/tests/test_utils.py muranodashboard/tests/functional/__init__.py muranodashboard/tests/functional/base.py muranodashboard/tests/functional/consts.py muranodashboard/tests/functional/sanity_check.py muranodashboard/tests/functional/utils.py muranodashboard/tests/functional/DeployingApp/Classes/mock_muranopl.yaml muranodashboard/tests/functional/DeployingApp/UI/mock_ui.yaml muranodashboard/tests/functional/HotApp/template.yaml muranodashboard/tests/functional/MockApp/Classes/mock_muranopl.yaml muranodashboard/tests/functional/MockApp/UI/mock_ui.yaml muranodashboard/tests/functional/config/__init__.py muranodashboard/tests/functional/config/config.conf.sample muranodashboard/tests/functional/config/config.py muranodashboard/tests/unit/__init__.py muranodashboard/tests/unit/test_api.py muranodashboard/tests/unit/catalog/__init__.py muranodashboard/tests/unit/catalog/test_views.py muranodashboard/tests/unit/categories/__init__.py muranodashboard/tests/unit/categories/test_views.py muranodashboard/tests/unit/common/__init__.py muranodashboard/tests/unit/common/test_net.py muranodashboard/tests/unit/common/test_utils.py muranodashboard/tests/unit/dynamic_ui/__init__.py muranodashboard/tests/unit/dynamic_ui/test_fields.py muranodashboard/tests/unit/dynamic_ui/test_forms.py muranodashboard/tests/unit/dynamic_ui/test_helpers.py muranodashboard/tests/unit/dynamic_ui/test_services.py muranodashboard/tests/unit/dynamic_ui/test_versions.py muranodashboard/tests/unit/dynamic_ui/test_yaql_expression.py muranodashboard/tests/unit/dynamic_ui/test_yaql_functions.py muranodashboard/tests/unit/environments/__init__.py muranodashboard/tests/unit/environments/test_api.py muranodashboard/tests/unit/environments/test_forms.py muranodashboard/tests/unit/environments/test_rest_api.py muranodashboard/tests/unit/environments/test_tables.py muranodashboard/tests/unit/environments/test_tabs.py muranodashboard/tests/unit/environments/test_topology.py muranodashboard/tests/unit/environments/test_views.py muranodashboard/tests/unit/images/__init__.py muranodashboard/tests/unit/images/test_forms.py muranodashboard/tests/unit/images/test_views.py muranodashboard/tests/unit/packages/__init__.py muranodashboard/tests/unit/packages/test_api.py muranodashboard/tests/unit/packages/test_forms.py muranodashboard/tests/unit/packages/test_tables.py muranodashboard/tests/unit/packages/test_views.py playbooks/legacy/murano-dashboard-sanity-check/post.yaml playbooks/legacy/murano-dashboard-sanity-check/run.yaml releasenotes/notes/.placeholder releasenotes/notes/abstract-base-class-fix-7cb06a0924b973f3.yaml releasenotes/notes/add-encrypt-data-function-73f0407bf1427040.yaml releasenotes/notes/add-package-details-126fe8cbefcd0229.yaml releasenotes/notes/bp-murano-dynamic-ui-custom-realtime-validation-2151342003f6a385.yaml releasenotes/notes/bug-1405788-2c8b2708e3bfc63f.yaml releasenotes/notes/bug-1579220-0a3fe23ac8f249ee.yaml releasenotes/notes/bug-1650406-4e4a3bdcfcc5718a.yaml releasenotes/notes/create-deployment-history-all-env-view-7610fd4301604b45.yaml releasenotes/notes/dashboard-rename-split-650ba2f7d4f846c2.yaml releasenotes/notes/display_repo_url-47c3cb0b45c2d68d.yaml releasenotes/notes/extend-flavor-requirements-d007f54c68c571ad.yaml releasenotes/notes/filter-in-package-definition-d463e434c856a412.yaml releasenotes/notes/fix_type_format-798a646071b1104b.yaml releasenotes/notes/fixed-network-mode-for-envs-0af7dad3bee9957b.yaml releasenotes/notes/glance-glare-aa81451d785591ed.yaml releasenotes/notes/glance-v2-wanring-b7ef3e3ce0ce6ce1.yaml releasenotes/notes/images-filter-project.yaml-081bffde1b91057f.yaml releasenotes/notes/import-horizon-filters-af5dcf0720502567.yaml releasenotes/notes/ips-display-topology-5a45876dafc637eb.yaml releasenotes/notes/manage-multiple-envs-e587c2e9432e39d7.yaml releasenotes/notes/password-checking-780dc07fa1d9926a.yaml releasenotes/notes/python3-cae8a08d96696550.yaml releasenotes/notes/reload-empty-env-10165198e8384b08.yaml releasenotes/notes/reorganize-dashboard-settings-11733b5c1003154b.yaml releasenotes/notes/safeloader-cve-2016-4972-82523879a6c3b1a5.yaml releasenotes/notes/show-resource-91a1f73cdb5d74ab.yaml releasenotes/notes/single_request_latest_apps-4f6add404ab07c15.yaml releasenotes/notes/status-session-b06786d470910080.yaml releasenotes/notes/topology-icon-fix-6572c069d127ed95.yaml releasenotes/notes/ui-definition-parameters-c9d2cb3a7da7459b.yaml releasenotes/notes/update_password_field-21a3b60658de3575.yaml releasenotes/notes/volume-selection-ui-element-d7ac69eceea3e584.yaml releasenotes/source/conf.py releasenotes/source/index.rst releasenotes/source/liberty.rst releasenotes/source/mitaka.rst releasenotes/source/newton.rst releasenotes/source/ocata.rst releasenotes/source/pike.rst releasenotes/source/unreleased.rst releasenotes/source/_static/.placeholder releasenotes/source/_templates/.placeholder tools/cover.sh tools/post_install.shmurano-dashboard-5.0.0/murano_dashboard.egg-info/dependency_links.txt0000664000175100017510000000000113245511553026042 0ustar zuulzuul00000000000000 murano-dashboard-5.0.0/murano_dashboard.egg-info/not-zip-safe0000664000175100017510000000000113245511475024225 0ustar zuulzuul00000000000000 murano-dashboard-5.0.0/murano_dashboard.egg-info/requires.txt0000664000175100017510000000042113245511553024371 0ustar zuulzuul00000000000000pbr!=2.1.0,>=2.0.0 beautifulsoup4>=4.6.0 Django<2.0,>=1.8 django-formtools>=1.0 iso8601>=0.1.11 six>=1.10.0 python-muranoclient>=0.8.2 pytz>=2013.6 PyYAML>=3.10 yaql>=1.1.3 castellan>=0.16.0 oslo.log>=3.36.0 semantic-version>=2.3.1 Babel!=2.4.0,>=2.3.4 django-babel>=0.5.1 murano-dashboard-5.0.0/murano_dashboard.egg-info/pbr.json0000664000175100017510000000005613245511553023453 0ustar zuulzuul00000000000000{"git_version": "75b3caa", "is_release": true}murano-dashboard-5.0.0/murano_dashboard.egg-info/top_level.txt0000664000175100017510000000002013245511553024516 0ustar zuulzuul00000000000000muranodashboard murano-dashboard-5.0.0/murano_dashboard.egg-info/PKG-INFO0000664000175100017510000000603713245511553023077 0ustar zuulzuul00000000000000Metadata-Version: 1.1 Name: murano-dashboard Version: 5.0.0 Summary: The Murano Dashboard Home-page: https://docs.openstack.org/murano/latest/ Author: OpenStack Author-email: openstack-dev@lists.openstack.org License: Apache License, Version 2.0 Description-Content-Type: UNKNOWN Description: ======================== 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 `_ * `Wiki `_ * `Code Review `_ * `Sources `_ * `Documentation `_ Platform: UNKNOWN Classifier: Development Status :: 5 - Production/Stable Classifier: Framework :: Django Classifier: Topic :: Internet :: WWW/HTTP Classifier: Environment :: OpenStack Classifier: Intended Audience :: Developers Classifier: Intended Audience :: Information Technology Classifier: Intended Audience :: System Administrators Classifier: License :: OSI Approved :: Apache Software License Classifier: Operating System :: OS Independent Classifier: Operating System :: POSIX :: Linux Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.5 murano-dashboard-5.0.0/functional_tests/0000775000175100017510000000000013245511556020361 5ustar zuulzuul00000000000000murano-dashboard-5.0.0/functional_tests/run_test.sh0000777000175100017510000000147713245511125022566 0ustar zuulzuul00000000000000DEST=${DEST:-/opt/stack/new} DASHBOARD_DIR=$DEST/murano-dashboard function start_xvfb_session() { export VFB_DISPLAY_SIZE='1280x1024' export VFB_COLOR_DEPTH=16 export VFB_DISPLAY_NUM=22 export DISPLAY=:${VFB_DISPLAY_NUM} fonts_path="/usr/share/fonts/X11/misc/" # Start XVFB session sudo Xvfb -fp "${fonts_path}" "${DISPLAY}" -screen 0 "${VFB_DISPLAY_SIZE}x${VFB_COLOR_DEPTH}" & } function run_nosetests() { local tests=$* export NOSETESTS_CMD="$(which nosetests)" $NOSETESTS_CMD -s -v \ --with-xunit \ --xunit-file="$WORKSPACE/logs/test_report.xml" \ $tests } function run_tests() { sudo rm -f /tmp/parser_table.py sudo pip install "selenium<3.0.0,>=2.50.1" cd $DASHBOARD_DIR/muranodashboard/tests/functional run_nosetests sanity_check } murano-dashboard-5.0.0/functional_tests/templates/0000775000175100017510000000000013245511556022357 5ustar zuulzuul00000000000000murano-dashboard-5.0.0/functional_tests/templates/report.template0000666000175100017510000001361113245511125025423 0ustar zuulzuul00000000000000 Test Report

    Test Report

    Summary: success — {{ stats.success }}, skip — {{ stats.skip }}, error — {{ stats.error }}, failure — {{ stats.failure }}
    View Artifacts | View Full Log {% for class, group in report.items() %} {% for test in group.tests %} {% endfor %} {% endfor %}
    Test Group/Test case Status Count Success Failure Error Skip
    {{ class }} {{ group.result }} {{ group.stats.total }} {{ group.stats.success }} {{ group.stats.failure }} {{ group.stats.error }} {{ group.stats.skip }}
    {{ test.name }} {% if test.screenshot %} (screenshot) {% endif %}
    {{ test.result }}
    Total {% if stats.failure + stats.error > 0 %}failure{% else %}success{% endif %} {{ stats.total }} {{ stats.success }} {{ stats.failure }} {{ stats.error }} {{ stats.skip }}
    murano-dashboard-5.0.0/functional_tests/templates/empty.template0000666000175100017510000000572013245511125025250 0ustar zuulzuul00000000000000 Test Report

    Test Report

    Summary: unable to run tests
    View Artifacts | View Full Log murano-dashboard-5.0.0/functional_tests/post_test_hook.sh0000777000175100017510000000070113245511125023754 0ustar zuulzuul00000000000000#!/bin/bash XTRACE=$(set +o | grep xtrace) set -o xtrace DEST=${DEST:-/opt/stack/new} DASHBOARD_DIR=$DEST/murano-dashboard source $DASHBOARD_DIR/functional_tests/collect_results.sh source $DASHBOARD_DIR/functional_tests/run_test.sh echo "#Run murano-dashboard functional test" set +e start_xvfb_session run_tests EXIT_CODE=$? set -e echo "Collect the test results" do_collect_results echo "Kill Xvfb" sudo pkill Xvfb exit $EXIT_CODE $XTRACE murano-dashboard-5.0.0/functional_tests/pre_test_hook.sh0000777000175100017510000000032713245511125023561 0ustar zuulzuul00000000000000#!/bin/bash DEST=${DEST:-/opt/stack/new} DASHBOARD_DIR=$DEST/murano-dashboard source $DASHBOARD_DIR/functional_tests/env_pkg_prepare.sh XTRACE=$(set +o | grep xtrace) set -o xtrace prepare_packages sync $XTRACE murano-dashboard-5.0.0/functional_tests/generate_html_report.py0000666000175100017510000001161613245511125025143 0ustar zuulzuul00000000000000#!/usr/bin/python # 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 __future__ import with_statement import jinja2 import lxml.etree as et import uuid import sys import os import re if not __name__ == "__main__": sys.exit(1) if not len(sys.argv) >= 3: sys.exit(1) if not os.path.exists(sys.argv[1]): sys.exit(1) LOG_LINE_PATTERN = "^(?P20[0-9]{2}\-[0-9]{2}\-[0-9]{2}) (?P