././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1696417899.8851812 murano-16.0.0/0000775000175000017500000000000000000000000013122 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/.coveragerc0000664000175000017500000000017300000000000015244 0ustar00zuulzuul00000000000000[run] branch = True source = murano omit = .tox/* murano/tests/* [paths] source = murano [report] ignore_errors = True ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/.stestr.conf0000664000175000017500000000010400000000000015366 0ustar00zuulzuul00000000000000[DEFAULT] test_path=${OS_TEST_PATH:-./murano/tests/unit} top_dir=./ ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/.zuul.yaml0000664000175000017500000000700400000000000015064 0ustar00zuulzuul00000000000000- project: queue: murano templates: - check-requirements - openstack-cover-jobs - openstack-python3-jobs - periodic-stable-jobs - publish-openstack-docs-pti - release-notes-jobs-python3 check: jobs: - murano-rally-task - murano-tempest-api - murano-tempest-cfapi - murano-grenade - murano-tempest-api-ipv6-only gate: jobs: - murano-tempest-api - murano-tempest-api-ipv6-only - job: name: murano-rally-task voting: false parent: rally-task-murano irrelevant-files: &murano-irrelevant-files - ^(test-|)requirements.txt$ - ^setup.cfg$ - ^doc/.*$ - ^.*\.rst$ - ^releasenotes/.*$ - ^murano/tests/.*$ - ^contrib/.*$ - ^tools/.*$ timeout: 7800 vars: devstack_plugins: rally-openstack: https://opendev.org/openstack/rally-openstack rally_task: rally-jobs/task-murano.yaml required-projects: - openstack/rally-openstack - job: name: murano-tempest-base parent: devstack-tempest irrelevant-files: *murano-irrelevant-files timeout: 7800 required-projects: &base_required_projects - openstack/heat - openstack/murano - openstack/murano-dashboard - openstack/python-heatclient - openstack/python-muranoclient - openstack/tempest - openstack/murano-tempest-plugin vars: &base_vars devstack_plugins: murano: https://opendev.org/openstack/murano heat: https://opendev.org/openstack/heat devstack_services: tempest: true s-account: false s-container: false s-object: false s-proxy: false devstack_localrc: TEMPEST_PLUGINS: "/opt/stack/murano-tempest-plugin" KEYSTONE_ADMIN_ENDPOINT: true tempest_test_regex: application_catalog tox_envlist: all - job: name: murano-tempest-api parent: murano-tempest-base - job: name: murano-tempest-api-ipv6-only parent: devstack-tempest-ipv6 description: | Murano devstack tempest tests job for IPv6-only deployment timeout: 7800 irrelevant-files: *murano-irrelevant-files required-projects: *base_required_projects vars: *base_vars - job: name: murano-tempest-cfapi parent: murano-tempest-base voting: false vars: devstack_services: murano-cfapi: true tempest_test_regex: service_broker - job: name: murano-grenade parent: grenade voting: false irrelevant-files: *murano-irrelevant-files required-projects: - opendev.org/openstack/grenade - opendev.org/openstack/heat - opendev.org/openstack/murano - opendev.org/openstack/murano-dashboard - opendev.org/openstack/python-heatclient - opendev.org/openstack/python-muranoclient - opendev.org/openstack/murano-tempest-plugin - opendev.org/openstack/heat-tempest-plugin vars: grenade_localrc: RUN_HEAT_INTEGRATION_TESTS: False devstack_plugins: murano: https://opendev.org/openstack/murano heat: https://opendev.org/openstack/heat devstack_services: tempest: true s-account: false s-container: false s-object: false s-proxy: false h-api: true h-api-cfn: true h-eng: true heat: true tempest_plugins: - murano-tempest-plugin tempest_test_regex: ^murano_tempest_tests\.tests\.scenario\.application_catalog\.test_deployment tox_envlist: all ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417899.0 murano-16.0.0/AUTHORS0000664000175000017500000002334500000000000014201 0ustar00zuulzuul0000000000000098k <18552437190@163.com> 99cloudML <99cloudml@99cloudMLdeAir.lan> Aaron-DH Akanksha Aleksandr Kholkin Alexander Adamov Alexander Saprykin Alexander Shlykov Alexander Tivelkov Alexey Deryugin Alexey Galkin Alexey Khivin Anastasia Kuznetsova Andrea Frittoli Andreas Jaeger Andrew Pashkin Andrew Pashkin Andrey Kurilin Andy Botting Angus Salkeld Ankur Rishi Aqsa Artem Akulshin Artem Tiumentcev AvnishPal Bertrand Lallau Bo Wang Boden R Brent Roskos Brian Tully Béla Vancsics Cao Xuan Hoang Chandan Kumar Chandan Kumar Chetna Khullar Choe, Cheng-Dae Christian Berendt Claudiu Belu Corey Bryant David Moreau-Simard David Purcell David Purcell David Purcell Deepak Dmitrii Dovbii Dmitry Teselkin Dmytro Dovbii Doug Hellmann Duan Jiong Duong Ha-Quang Ekaterina Chernova Ekaterina Fedorova Ellen Batbouta Elod Illes Erik Olof Gunnar Andersson Felipe Monteiro Filip Blaha Flavio Percoco Florian Walker Gage Hugo Georgiy Okrokvertskhov Georgy Okrokvertskhov Gerry Buteau Ghanshyam Mann Guoqiang Ding Guy Paz Gyorgy Szombathelyi Hangdong Zhang Henar Muñoz Frutos Hervé Beraud Hidekazu Nakamura Hironori Shiina Hongbin Lu Huangsm Ian Wienand Igor Yozhikov Ilya Popov Jacek Tomasiak James E. Blair Jeremy Liu Jeremy Stanley Jesus Perez Jose Phillips Julian Sy Julien Vey Kirill Zaitsev Kirill Zaitsev Konstantin Snihyr LeopardMa Li Xipeng Lin Yang LiuNanke Longgeek Luigi Toscano Lujin Luo M V P Nitesh MStolyarenko Madhuri Kumari Madhuri Kumari Madhuri Kumari Marc Koderer Marga Millet Margarita Shakhova Maria Zlatkova Michael Krotscheck Michal Gershenzon Mikhail Dubov Monty Taylor Nam Nguyen Hoai Namrata Natasha Beck Nguyen Hung Phuong Nguyen Van Trung Nicolas Nikolai Starodubtsev Nikolay Starodubtsev Nikolay Starodubtsev Olena Logvinova OlgaGusarenko Olivier Lemasle Omar Shykhkerimov OpenStack Release Bot PRAMOD KUMAR SINGH Paul Bourke Radek Pospisil Ravi Shekhar Jethani Robert Collins Robin Naundorf Roman Vasilets Ronald Bradford Rongze Zhu Rui Yuan Dou Ruslan Kamaldiniv Ruslan Kamaldinov Ryan Peters Sam Morrison Sam Pilla Samantha Blanco Samuel Pilla Sean McGinnis Serg Melikyan Serg Melikyan Sergey Kolekonov Sergey Lukjanov Sergey Melikyan Sergey Murashov Sergey Reshetnyak Sergey Vilgelm Sharma-Ritika Shilla Saebi Snihyr Kostyantyn Sofiia Andriichenko Stan Lagun Stan Lagun Stanislav Lagun Steve McLellan Steve Wilkerson Swapnil Kulkarni (coolsvap) Takashi Kajinami Tetiana Lashchova Thierry Carrez Thomas Goirand Thomas Goirand Timur Nurlygayanov Timur Sufiev TimurNurlygayanov Tin Lam Tin Lam Tony Breeds Tony Xu Tovin Seven Vahid Hashemian Valerii Kovalchuk Van Hung Pham Victor Araujo Victor Ryzhenkin Victor Stinner Vijayaguru Guruchave Viktor Ryzhenkin Vu Cong Tuan Wriju Bhattacharya Yosef Hoffman Yushiro FURUKAWA ZhiQiang Fan Zhongyue Luo akhiljain23 bhagyashris bhavani.cr chao liu chenaidong1 darla.ahlert deepakmourya devray dineshbhor dommgifer earthmant ellen emashkin enthurohini gaofei gaoyl gecong1973 gengchc2 ghanshyam ghanshyam howardlee hparekh huoliang jaugustine jeremy.zhang kbespalov lei-zhang-99cloud leizhang lidong lingyongxu liumk liushuobj liyingjun liyingjun ljhuang lvdongbing melissaml noa npraveen35 ol7435 olehbaran ondrej.vojta oshykhkerimov pandatt pengyuesheng ricolin ricolin shangxiaobj shashi.kant shihanzhang smurashov stoneliu sunqingliang6 sunshuai syed ahsan shamim zaidi tamilhce varshak05 venkatamahesh venkatamahesh visitor vryzhenkin wangqi wangzhh wangzihao wu.chunyang xiangxinyong xpress yuhui_inspur yushangbin zhang.lei zhangyanxian zhangyanxian zhangyifan zhu.rong zhulingjie zhurong ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/CONTRIBUTING.rst0000664000175000017500000000112700000000000015564 0ustar00zuulzuul00000000000000The source repository for this project can be found at: https://opendev.org/openstack/murano Pull requests submitted through GitHub are not monitored. To start contributing to OpenStack, follow the steps in the contribution guide to set up and use Gerrit: https://docs.openstack.org/contributors/code-and-documentation/quick-start.html Bugs should be filed on Launchpad: https://bugs.launchpad.net/murano For more specific information about contributing to this repository, see the murano contributor guide: https://docs.openstack.org/murano/latest/contributor/contributing.html ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417899.0 murano-16.0.0/ChangeLog0000664000175000017500000032053500000000000014704 0ustar00zuulzuul00000000000000CHANGES ======= 16.0.0 ------ * Imported Translations from Zanata * Fix bandit * Imported Translations from Zanata * Update master for stable/2023.1 15.0.0 ------ * Fix tox 4 compatibility * Imported Translations from Zanata * Update master for stable/zed * Switch to 2023.1 Python3 unit tests and generic template name * Fix murano-test-runner help output test * Fix compatibility with Python 3.10 * Replace abc.abstractproperty with property and abc.abstractmethod * Fix compatibility with oslo.db 12.1.0 14.0.0 ------ * Imported Translations from Zanata * Update python testing as per zed cycle teting runtime * Drop lower-constraints.txt and its testing * tests: Remove use of 'oslo\_db.sqlalchemy.test\_base' * Add Python3 zed unit tests * Tenant to project migration for RequestContext * Update master for stable/yoga 13.0.0.0rc1 ----------- * Remove the unused paste * Fix the exceptions import * Update the ssl config * Update the murano.conf * Update the openrc info * Remove the warning description * Update murano-api description * Update Testing Guidelines * Update the Linux Image required package * Update System requirements * Update the murano\_agent description * Update the prepare\_lab * Fix the error python version * Fix devstack job failures * Add tools to tempest job irrelevant-files * Add Python3 yoga unit tests * Fix Engine.execute() in func tests * Update master for stable/xena 12.0.0 ------ * Revert "Use fnmatch from oslo.utils" * Replace deprecated import of ABCs from collections * Fix broken unit test * Enable tls-proxy support for test jobs * [community goal] Update contributor documentation * Add missing font for PDF doc generation * Add Python3 xena unit tests * Update master for stable/wallaby 11.0.0 ------ * [goal] Deprecate the JSON formatted policy file * Use common rpc pattern for all services * Fix lower-constraints * Imported Translations from Zanata * Imported Translations from Zanata * Fix 'method object has no attribute \_\_yaql\_function\_\_' on py3 * Remove six * Imported Translations from Zanata * Add Python3 wallaby unit tests * Update master for stable/victoria 10.0.0 ------ * Murano testing to Ubuntu Focal * Add native grenade zuul v3 jobs * Fix cidr format error about ip\_address in allowed\_address\_pairs * Fix requirements-check job * Fix murano ci UT error * Murano api add monkey patch * Switch from unittest2 compat methods to Python 3.x methods * Use uwsgi binary from path * Fix versions api by using webob correctly * Always reset cfg.CONF when starting the wsgi app * Stop to use the \_\_future\_\_ module * Remove retired congress * Cap jsonschema 3.2.0 as the minimal version * Switch to newer openstackdocstheme and reno versions * Fix pep8 error * Fix hacking min version to 3.0.1 * Cleanup py27 support * Monkey patch original current\_thread \_active * Bump default tox env from py37 to py38 * Add py38 package metadata * Add Python3 victoria unit tests * Update master for stable/ussuri 9.0.0.0rc1 ---------- * Use unittest.mock instead of third party mock * Remove six usage murano/contrib * Remove six murano/api * Remove six murano/packages * Remove six murano/engine * Remove six murano/dsl * Remove six usage * Remove six murano/db * Remove six murano/hacking * Remove six murano/common * Remove six murano/policy * Add contrib to irrelevant-files * Sync heat-translator and tosca-parser version * Sync python-glanceclient version * Sync python-magnumclient * Clean muranoartifact py2 support * Cleanup py27 support * Update hacking for Python3 * Drop unittest2 usage * Imported Translations from Zanata * Eventlet monkey patching should be as early as possible * Add --procname-prefix for uwsgi murano-api * [ussuri][goal] Drop python 2.7 support and testing * Imported Translations from Zanata * Update master for stable/train 8.0.0 ----- * Update semantic\_version to 2.8.2 and remove multiattach in volume template * PDF documentation build * Blacklist eventlet 0.21.0,0.23.0,0.25.0 * Fix docs build * Add ip\_version for NeutronNetwork subnet * [train][goal] set default dns for IPv6 env * Remove the deadcode * Fix IPV6 rabbit host * Update api-ref location * [train][goal] Define new 'murano-tempest-api-ipv6-only' job in gate * Bump to hacking 1.1.0 * Bump the openstackdocstheme extension to 1.20 * Blacklist sphinx 2.1.0 (autodoc bug) * Add Python 3 Train unit tests * Add local bindep.txt * Replace git.openstack.org URLs with opendev.org URLs * Add deps for api-ref * Update the murano-rally-task job's irrelevant files * Uncap Bandit * Cap Bandit below 1.6.0 and update Sphinx and jsonschema requirement * Correct the environment template clone error message * Remove the unused DISCOVER\_DIRECTORY param * OpenDev Migration Patch * Dropping the py35 testing * Bump psycopg lower-constraint to 2.7 * Replace openstack.org git:// URLs with https:// * Update master for stable/stein 7.0.0.0rc1 ---------- * Grenade: add the Grenade Heat plugin too * Fix py37 unit test error * add python 3.7 unit test job * Using trustor's session to delete the trust * Add grenade job * stop murano-api process * Remove the unused experimental job * Fix contract violation for User resource * Fix pep8 F821 error * Fix pep8 E731 error * Fix pep8 E402 * use get\_rpc\_transport to obtain an RPC transport instance * python3 ValueError object has no attribute message * Fix unittest no such option None in group DEFAULT error * Change openstack-dev to openstack-discuss * update minversion to 2.0 in tox.ini * Title overline too short * fix docs url * Add framework for murano-status upgrade check * Fix test-release-openstack-python3 job failed * Increment versioning with pbr instruction * Update the URL in doc * Use templates for cover and lower-constraints * Use standard cover tox env * add python 3.6 unit test job * switch documentation job to new PTI * Remove install-guide-jobs * import zuul job settings from project-config * Fix py37 tests failed due to async * Update reno for stable/rocky 6.0.0 ----- * Fix Configuration Guide sample and policy conf can not found * Docs: Autogenerate config documentation * Remove congress and mistral functional integration tests * murano-congress-devstack job use native Zuulv3 * Using native Zuulv3 jobs * Update py27-ocata to py27-queens * Remove the unused run\_tests.sh script * Add irrelevant-files for murano-congress-devstack job * [docs]Deploy murano api under WSGI server * Optimize murano admin\_troubleshooting * Remove set GLARE\_API\_URL for horizon * Refactor murano Installation Guide * Sanitizer some sensitive logs information 6.0.0.0b3 --------- * Use V3 auth\_url * Add Release Notes url to README * Switch to using stestr * Follow the new PTI for document build * fix typos * add a link to release notes in README file * [ci] Use zuul v3 native job for Rally * The original env should be {}, otherwise it can not be dealt with by heat client * Fix Error: attribution() takes exactly 3 arguments (2 given) * fix a grammatical error * fix tox python3 overrides * Method, decrypt\_data, uses internal Barbican endpoint * Update auth\_uri option to www\_authenticate\_uri * correct the plural spelling of "object" * fix document error, "is" should be "are" * Trivial: Update pypi url to new url 6.0.0.0b1 --------- * uncap eventlet * Remove use of deprecated term, Usage: Action * Add form validator example to documentation * Allow port security to be disabled * Fix Contract on Project property, extra * Updated from global requirements * Enable mutable config in Murano * add lower-constraints job * Updated from global requirements * Add default configuration files to data\_files * Updated from global requirements * Updated from global requirements * Make test allowed\_pattern type same as the hot example and fix the gate * Update links in README * Updated from global requirements * Updated from global requirements * Imported Translations from Zanata * Add the missing title of Configuration Guide * Remove empty file * Updated from global requirements * Imported Translations from Zanata * Imported Translations from Zanata * use . instead of source * Update reno for stable/queens 5.0.0.0rc1 ---------- * Zuul: Remove project name * Replace curly quotes with straight quotes 5.0.0.0b3 --------- * Updated from global requirements * Updated from global requirements * Option to configure pip source for murano-agent * Updated from global requirements * Fix wrong url in how\_to\_contribute.rst * Murano-engine side implementation of agent message signing * Updated from global requirements * Remove old job names * Follow Zuul v3 naming conventions * Configure murano dashboard selenium tests configuration * Remove python-dev from list of preinstalled packages * Support shared IP address range * Imported Translations from Zanata * Fix the deprecated usage of "get\_transport" * Updated from global requirements * Updated from global requirements 5.0.0.0b2 --------- * Remove murano-tempest-plugin entry from setup.cfg * Document error * Updated from global requirements * Add README for remove murano\_tempest\_tests from murano repo * Remove murano\_tempest\_tests from murano repo * Minor updates to encryption docs * Murano tempest job with separate tempest plugin * Updated from global requirements * Remove the unused install venv scriptes * Fix murano-congress-devstack-dsvm job * Correct misspelling in Murano doc * Fix the incorrect documentation indent * Fix the gate failed * Updated from global requirements * Zuul: add file extension to playbook path * Add legacy murano-dsvm-functional job * Remove setting of version/release from releasenotes * Remove separate firstapp build * Updated from global requirements * Updated from global requirements * Move legacy jobs to project * Updated from global requirements * Do not use “-y” for package install * Updated from global requirements * Use secure path join * Fix rally-dsvm-murano-task job failed * Updated murano-cfapi-paste.ini with http\_proxy\_to\_wsgi 5.0.0.0b1 --------- * Updated from global requirements * Log message * Format string sequence error * Remove download murano images from app.openstack.org * Updated from global requirements * Updated from global requirements * Replace keystoneclient with keystoneauth * Updated from global requirements * List environments api document * [Trivialfix]Fix typos in murano * Code optimization * Environment configuration API * Updated from global requirements * Remove fallback to [keystone\_authtoken] * Update and replace http with https for doc links * Api document error * Remove unused param * Updated from global requirements * Fix to use "." to source script files * Updated from global requirements * Implement environment audit reports * Added Tempest API client methods and Tempest tests for sessions and deployments * Updated from global requirements * Remove pbr warnerrors * Updated from global requirements * Deploy murano-api via uwsgi * Ensure assigned-ips output is present in Heat template with Nova Network * Add securityGroups param to NovaNetwork joinInstance method in Core Library * Update reno for stable/pike * Using oslo generator config file generate murano config 4.0.0.0rc1 ---------- * Added Tempest tests for publicizing a package * Added Tempest negative tests for environments * Fix Murano API in Installation Guide * Refine skip messages * Remove install-guide env which is no longer effective * Update the documentation link for doc migration * Handle available volume client versions * Use consistent session options * Add configuration guide * Updated from global requirements * Fix murano\_auth usage 4.0.0.0b3 --------- * Add idempotent\_id decorators to murano\_tempest\_tests * Update murano policies documentation * Updated from global requirements * Add decryptData yaql function to murano engine * Move admin\_index.rst to index.rst * Update testtools.testcase.attr to decorators.attr * Make cinder volume attachments available * Init the orchestration client from config (part2) * Updated from global requirements * Init the orchestration client from config * Updated from global requirements * Update Documentation link in README * Adapt murano documentation for new standards * Mark doc warnings as errors * Unskip test\_deploy\_app\_with\_volume\_creation test * Make murano user has admin role * Add dsl\_iterators\_limit config option * Use default credentials if None has been passed * Add Apache License Content in index.rst * Use openstackdocstheme everywhere * Skip cinder volume creation test * Remove long-ago deprecated show\_categories * Updated from global requirements * Use tempest.test.BaseTestCase for murano tempest tests * Add policy sample generation * Add widget for volume selection * Fix html\_last\_updated\_fmt for Python3 * Add 'rm -f .testrepository/times.dbm' command in testenv * Remove murano default policy.json * Updated from global requirements * Policy in code for actions/static actions * Add WSGI support script for murano API * docs: Add search package API to api-ref * docs: Add (static) actions API to api-ref * Updated from global requirements * Policy in code for categories * Updated from global requirements * Policy in code for deployments 4.0.0.0b2 --------- * Policy in code for packages * Updated from global requirements * Policy in code for environment templates * Updated from global requirements * Remove dead code * Policy in code * Updated from global requirements * Updated murano-apste.ini with http\_proxy\_to\_wsgi * Updated from global requirements * Updated from global requirements * Updated from global requirements * External object IDs couldn't be used in template() contracts * Replace assertRaisesRegexp with assertRaisesRegex * Updated from global requirements * Remove enforce\_type=True from oslo.config set\_override * Modify Default Domain * Using run\_process instead of screen\_it * Rename py27-mitaka to py72-ocata * Refactor existing context attributes to base class * Update rename environment API endpoint * Updates environment api-ref * Updated from global requirements * Replace six.iteritems() with .items() * Make get\_token\_client\_session get auth\_uri from murano\_auth section * Updated from global requirements * Updates package api-ref * Add bandit job to the pep8 gate for Murano * Allow users to assign a security group to an app * Stop using aliases for creds manager * Add environment templates to api-ref * Add deployments to api-ref * Add sessions to api-ref * Install devstack with murano on Python 3 * Add categories to api-ref * Fix config type of run\_service\_broker\_tests to Boolean * Fix doc generation for Python3 * Added opportunity import packages without tags * Make murano auth with murano\_auth section instend of keystone\_authtoken * Adding bandit.yaml configuration to bandit * Add mising timeout to ApplicationCatalogClient * Changed admin pass to include secret marker as per bandit linting error 109 * Add \`nosec\` for Bandit issue 506 in resource\_manager.py * Add \`--skip B104\` for Bandit issues * Make CinderVolume attachment info available 4.0.0.0b1 --------- * Updated from global requirements * Change cred provider to enforce dynamic config * Optimize the link address * Add packages to api-ref * Adding bandit target to \`\`tox.ini\`\` * Set access\_policy for messaging's dispatcher * Fix doc build if git is absent * Updated from global requirements * Updated from global requirements * Install Guide: Fix RST * Remove log translations * Updated from global requirements * Updated from global requirements * Replaces uuid.uuid4 with uuidutils.generate\_uuid() * Imported Translations from Zanata * Add environments to api-ref * Fix some reST field lists in docstrings * [devstack] Remove all the dashboard config files * Use HostAddressOpt for opts that accept IP and hostnames * Replace assertEqual(None,v) with assertIsNone(v) * Create official murano install guide * Updated from global requirements * Updated from global requirements * Fix a typo * Updated from global requirements * Adds TLS/SSL Version Support to Murano Engine * Replace yaml.load() with yaml.safe\_load() * Agent initialization was fixed * Updated from global requirements * [Fix gate] Update oslo.messaging constructors following recent change * Updated from global requirements * Renames wait\_for\_volume\_status in tempest tests for consistency * Updated from global requirements * Correct some typos in doc\_files * Remove skip test from murano engine package loader unit test * Update reno for stable/ocata * Updated from global requirements * Fixes cfapi unit test conflicting with other tests 3.2.0 ----- * Fix murano-api docs * Fixed opportunity run muranoagent twice * Remove support for py34 * cors: update default configuration * Use https instead of http for git.openstack.org * Prepare for using standard python tests * Remove heat as enable\_service in devstack * Make use of already registered group * Increase unit test coverage for Cfapi * Fixes various Cfapi bugs * 'io.murano.apps.' is not used to download packages * Updated from global requirements 3.1.0 ----- * Updated from global requirements * Fix typos * v1 execution plan template processing was fixed * Marking rabbitmq password config property as secret * Fixes various documentation typos in Murano automated tests description * Exclude build dir for flake8 test * Add debug to tox enviroment * Updated from global requirements * Allows fetching of deployments from all environments * Increase unit test coverage for Common WSGI * Update response code descriptions in API spec * Let admin can delete user's environment * Correct the wrong calling 'getExternalNetworkIdForRouter' * Fix a typo * Increase unit test coverage for Common Auth Utils * Initial commit for murano api-ref * Updated from global requirements * Fixes TypeError thrown by parallel\_select in DSL Helpers * Remove ALLOWED\_HOSTS using murano devstack plugin * Removes unnecessary utf-8 encoding * Developing Murano Packages 101 * Increase unit test coverage for DSL Helpers * Updated from global requirements * Use test-config phase for configuring murano tempest * Updated from global requirements * Use assertIn() to replace asserTrue(\* in \*) * ExistingNeutronNetwork didn't return IPs for the instance * Updated from global requirements * Deleting dates from dictionaries to skip it in assert * Use assertIsNone(...) instead of assertIs(None, ...) * Add log translation marker * Updated from global requirements * modify print information * Modify variable's usage in Log Messages * Updated from global requirements * Replace six iteration methods with standard ones * Translate info-level log messages for LOG.error * Murano can now properly attach VMs to shared networks * Fixed SharedIp class * Fix removes date\_time items from dictionaries * Always declare agent RabbitMQ queues * 2 forgotten files from https://review.openstack.org/#/c/401327 * Reduce the amount of smoke tests from 61 to 40 * Show team and repo badges on README * Updated from global requirements * Fix syntax errors of exception print * Fix syntax errors of the comments * Add Nova anti-affinity rules * Ability to retrieve current/owner user/project * Revert "Update devstack keystone\_authtoken config to fit with keystone v3" * Multi-region support for WindowsInstance * HeatStack async mode fix * Don't purge random stacks * Fix a typo * Configuration is now properly applied to new nodes * Fix typos in cover.sh * Updated from global requirements * [Docs] Restructuring * Add murano-pkg-check to the test-requirements.txt * Increase unit test coverage for common wsgi * Use method ensure\_tree from oslo.utils * [Docs] Restructuring * Replace oslo\_utils.timeutils.isotime * Updated from global requirements * Fill ALLOWED\_HOSTS using murano devstack plugin * Updated from global requirements * Order the packages for parameter search * Typo fix in murano * Documentation for Parameters and ParametersSource * Fix typos in class\_templ.rst & test\_catalog.py * OpenStack typo * Updated from global requirements * Fix debug output for SSLMiddleware X-Forwarded-Proto * Updated from global requirements * Updated from global requirements * Increase unit test coverage for common engine and instance statistics API * Move test with cinder backup creation to the IsolatedAdmin suite * Fixed Shellcheck's warnings in murano-agent * Unblocking the gate * Update devstack keystone\_authtoken config to fit with keystone v3 * Increase unit test coverage for Version Negotiation API * Increase test coverage for Schema Generation API * Increase unit test coverage for Stats API * Repair cinder integration tests * Update .coveragerc after the removal of openstack directory * Added LICENSE to application development library * Increase unit test coverage for Heat Stack * Improve first app builds * Updated from global requirements * Fix TypeError being throw by wait\_ready in Engine System Agent * Increase unit test coverage for Agent API * Remove getRegion() calls from network initializers * Updated from global requirements * Fix statservice save db error * Increase unit test coverage for db services environments and engine system * Increase unit test coverage for engine system * Increase unit test coverage for Engine Package Loader * [docs] Update API spec with info about environment model API * [docs] Add info about show env model and edit env model commands * Fix TypeError being thrown by six.reraise in Engine Package Loader * Improve tests documentation markup * Fix a typo in multi\_region.rst * Increase unit test coverage for CFConnections API * Increase unit test coverage for Manage CLI * Increase unit test coverage for murano heat stack and yaql * Enable release notes translation * Updated from global requirements * [Docs] Add skeleton for My first Murano app guide * Initialize environments with empty metadata * Add environment edit API * MetadataAware mixin added to Core Library * [docs] fix typo manual installation * Fixes Statistics class create referencing invalid attributes for models.ApiStats * GC unit test coverage increased * Increase unit test coverage for DB Instances API * Fix TypeError being thrown by delete\_environment\_from\_space in cf\_connections * Increase unit test coverage for base Utils * Increase unit test coverage for External Context Middleware * Increase unit test coverage for mqclient * Filter enviroment list by project id * Metadata storage in object model * Increase unit test coverage for common.app\_loader * Increase unit test coverage for Stat Service API * Increase unit test coverage for Common Server API * Correctly release CinderVolumes * Fix typo and make docstring more clear * Increase unit test coverage for Schemas API * Increase unit test coverage for Sessions API * Allow to PUT an empty object model * Updated from global requirements * Increase unit test coverage for hacking checks * [Docs] [networking] section for multi-region deployment * Increase unit test coverage for Services Actions API * Async HeatStack::push * Fix indentation in ReplicationGroup class * Increase unit test coverage for murano heat stack * Increase unit test coverage for test fixture * Mark .testr.conf as not-executable * Using assertIsNotNone() instead of assertNotEqual(None) and assertIsNot(None) * Change assertTrue(isinstance()) by optimal assert * Increase unit test coverage for dsl session\_local\_storage * Fix invalid SessionState.deploying reference in services actions * Increase unit test coverage for Hot Package API * Refactoring of Instance::ipAddresses retrieving * Prevent unnecessary stack.push in Instance::releaseResources * Increase unit test coverage for services api * Adds menuselection syntax in enduser guide, quick start section * Increase unit test coverage for Package Base API * Increase unit test coverage for exceptions in Packages API * Increase unit test coverage for common utils * Change assertTrue(isinstance()) by optimal assert * Fixes CFServiceInstance.to\_dict calls wrong class * Increase unit test coverage for murano db and db services * MuranoPL garbage collection system overview * Fix the python3.4 TypeError * Fix menuselection syntax in enduser-guide * Fix typo * Revises json to JSON, yaml to YAML and yaql to YAQL in documents * Use sys.maxsize instead of sys.maxint * Updated from global requirements * Fix copying file before checking it exists in hot package * Increase unit test coverage for Load Utils API * Murano bindings to Glance Metadef API * Call addGroupingress() in init of NeutronSecurityGroupManager * Increase unit test coverage for MPL Package API * Fix a grammatical error * Fix removes duplicated phrase from the document * [Docs] Document Multi-Region Support * [docs] Add info about template() contract * Updated from global requirements * Add quotes to versions * Add hacking rule for using assertIsNone instead of assertEqual(None,\*\*\*) * Fix some typos in devstack readme * Install all apps from ./meta directory on devstack * Remove unused config.CONF * [Docs] Migration guide to newton for application developer * Documentation on App development framework * Remove networking config section from devstack settings * Use fnmatch from oslo.utils * Updated from global requirements * Add missing ":command:" markup for the command * Remove unnecessary setUp * [Core Library] Add getInstanceIpList to NovaNetwork * Increase unit test coverage for dsl attribute\_store and context\_manager * Increase unit test coverage for system agent * Replace retrying with tenacity * Increase unit test coverage for Static Actions API * Increase unit test coverage for Env Templates API * Increase unit test coverage for Actions API * Increase unit test coverage for Catalog API * Increase unit test coverage for api deployments * Convert =0 version specs to ==0 specs * Move getRegion() in CinderVolume to deploy() method * Update docs with installation of io.murano.applications * Remove the unused images * Fix a typo in utils.py * Updated from global requirements * [messaging] Using get\_notification\_transport() * [Docs] Murano plug-ins * Increase unit test coverage * Remove default=None for config options * Add unit versioning test suite * Updated from global requirements * Update reno for stable/newton * Fixes ObjectDestroyedError on StackTrace object * Move session.finish() to 'finally' block 3.0.0.0rc1 ---------- * Forces applications to be deleted before Heat stack * Prevent logging of result of resources.string() method call * Prevent executor finalization until exception are not handled * Small fixes in Core Library * [docs] Conventional changes and formatting fixes * Serialization of destruction dependencies * GC.isDoomed() and GC.isDestroyed() functions were added * Serialization of destroyed objects * Raise exception on call of method of destroyed object * Pass receiver to :GC.subscribeDestruction explicitly * On-demand garbage collection * Break cyclic references in DSL * Cleanup, clarify newton release-notes * [Doc] Add auth params to core library import instruction * Fix multiple errors in network configuration function * Add joinedNetworks property to Instance class * Use openstack-client variables in openstack CLI calls * [Docs] Update screenshots * Better detect and configure murano-agent pre-installed on image * Updated from global requirements * Adjust region name for network selection in devstack * Make tests compose packages in temporarily directory * [Docs] Add info about using abstract dependencies between applications * Use default region\_name parameter in clients * Create glare client in functional tests * [messaging] Using get\_notification\_transport() * Move misplaced reno notes in place * Fix NetworkExplorer creation * Fix adds notes about replacements of items between % signs * Fixes invalid expression in app dev framework * Set the desired hostname to a instance using murano instead of Murano Pattern * Add \_\_ne\_\_ built-in function for murano * Object's properties deserialize was fixed * Fix for Wrong reference in glare docs * ObjectStore parent lookup with temporary id was fixed 3.0.0.0b3 --------- * Updated from global requirements * Type name resolution was fixed * Increase description column length in task table * An initial commit for MuranoPL garbage collector * MasterSlave\* classes were deleted * Alive objects from Objects and ObjectsCopy were merged * Migrate JSON schema generator to new framework * Update clients with multi-region support * Update app dev framework with multi-region support * New framework for contracts * TestFixture mocks were updated * firstOrDefault() was replaced with first(null) * Replace functions 'Dict.get' and 'del' with 'Dict.pop' * Updated from global requirements * TrivialFix: Remove cfg import unused * modify the home-page info with the developer documentation * Correct murano reraising of exception * Update Readme with correct Doc URL * Install all dashboard/panel files from murano-dashboard * OpenstackSecurityConfigurable was renamed * Allow network driver selection override * Fix some typos in wsgi.py & catalog.py * Updated from global requirements * Remove pot files * Adds more replica provider primitives * Migration of replication to the template() contract * Clean imports in code * Update the UI test running documentation * Change Murano API detection in tests and fix tests itself * [docs] Add instruction to add policy files to horizon * Use upper constraints for all jobs in tox.ini * Base Application classes for App Development Framework * Extracted replication capabilities from ServerGroup * Updated from global requirements * Revert "Fix config group for SSL in tempest test" * Fix config group for SSL in tempest test * Fixes congress integration * SoftwareComponent hierarchy refactoring * Improved Server naming and provisioning reporting * Renamed Instance\* to Server\* * AppDevFramework: Server Replication reporting * [Docs] Moves network configuration to admin-guide * Refactor 'wait\_for\_environment\_deploy' function * Use glare urls explicitly in devstack plugin * Add tempest versioning test suite * Support for multi-regional apps was added * Do not serialize Config properties * Don't put non-initialized objects into ObjectStore * template() contract function was introduced * Updated from global requirements * Refactor merge\_dicts helper function tests * Add multiple api workers * Trivial: Add python identifiers in setup.cfg * Linux.runCommand() and Linux.putFile() are now non-synchronized * Changes type format in object model * Fixed reporting in app development framework * [docs] Remove "meta-class" term * Updated from global requirements * Add missing parenthesis * Add deployment murano tempest test * Make service broker work with GLARE again * Fix a typo in the rst file * Move .init call into separate load phase * Get rid of type origins * Add py2.7 and py3.4 identifiers in setup.cfg * Add cinder volumes attachment tests * Updated from global requirements * [docs] Fix formatting in installation guide * Add base and packages for tempest murano scenario tests * Nested new() were not using 2-phase load * Duplicate objects were instantiated for redeclared properties * Updated from global requirements * [Docs]Update the Murano service broker install guide * Fix string interpolation to delayed by logging * Add Python 3.5 classifier and venv * Use assertIn, assertNotIn and assertIsNotNone * Do not override credential provider in tenant isolation tests * [Docs] Move user and appdev guides from draft to main docs * SoftwareComponent implementation * MuranoPL forms implementation * Respect per region RabbitMQ configs in Agent[Listener] * Updated from global requirements * Concurrent Execution Control * InstanceGroup and InstanceProvider hierarchy * Event Notification pattern implemented * Replication classes of Application Development Framework library * TestFixtureWithEnvironment class for TestRunner * dump() function added to DSL * Ability to instantiate object graph * Refactor tempest utils readlines() to read() * Fix typo in package loader logging * Use assertEqual() instead of assertDictEqual() * Fix used package versions for stable inheritance * Refactoring of the ObjectStore passing in DSL * [Docs] Change FQNs of murano apps and add examples of FQNs * [Docs] Add information about Config property usage * Use devstack for service broker use separate paste and db * Replace OpenStack LLC with OpenStack Foundation * Enable static action tests with glare backend * Add tempest GLARE sanity check tests * Initial implementation of artifacts client in tempest plugin * [Docs] Clarify how to run API and engine in separate terminal * Fix cfapi test job * Devstack install murano-dashboard with murano RBAC policy * Updated from global requirements * [Docs]Update the Murano Dashboard install guide * Fixed owner usage for dict-based new() function * Updated from global requirements 3.0.0.0b2 --------- * [Docs] Add parameter resources-dir for hot-packages * Updated from global requirements * Add py27-mitaka tox target * Updated from global requirements * Use keystoneauth1 lib for authentication helpers * fix a spelling mistake: applcation should be application * Implement API call and RPC call for static actions * Could not invoke static methods from Python * [Docs] Add info about 2.2.0 version of execution plan template format * Add filter based on 'in' operator * Use upper-constraints in tox test environments * [docs] Added murano gerrit dashboard * Do not use stevedore namespase as a prefix for class FQNs * Use the absolute path for the session backend * Use multiprocessing.cpu\_count instead of psutil.NUM\_CPUS * Improve cover job output * Updated from global requirements * Improved Test-Runner's output * Test-runner now properly invokes setUp/tearDown methods * Added equality handlers for MuranoObjectInterface * Update namespaces for MagnumBay application * Include objects created with new() into the ObjectStore * Allows Spec::transform treat generators as list * Convert script line endings for the target OS * Improve \_\_init\_\_ detection * Fix error in Magnum-app * Amended reno note about booting from cinder volumes * [Docs] Add a reference to the article about multi regions * Use Murano-specific Horizon settings when devstack install * Get package service backend when package import in devstack * [Plugins] Updated from global requirements * Add metadata to the result of action serialization * Implement new syntax for action declaration * Fix issue with deployment with cloudbase-init on Windows * Updated from global requirements * Use SafeLoader to load yaml files * Added a muranoPL-specific override of 'call' * Refactor API params tuples to sets * Format logs in devstack setup * Including a description field for environment and environment templates * Updated from global requirements * [docs] Add information about TestSuiteMultipleEnvironments * Use a correct config option in example plugin * Add io.murano namespace to demo app for example plugin * Use murano CLI for importing core library in devstack * Remove version from example and heat-translator plugins * Fix missing parenthesis in \_getSubnetworks call * Fix application template update * Two-phase Instance deployment support * Fixed a bug when Heat Stack Update was called without template changes * Finish porting unit tests to Python 3 * Port API v1 unit tests to Python 3 * Fixed inability to deploy if security groups are disabled * Port test\_agent and test\_catalog to Python 3 * Implement meta-classes for UI hints * Add timeout to the methods of Linux class * Fix defaultGroupName of SecurityGroupManager Class * Devstack add compile message for murano-dashboard * Change to tempest stable API * Reorder release notes to put master on top * [Docs] Add an example about usage of static methods in contracts * Updated from global requirements * Update version of source files in cloudify example app * Update obsolete way of initialize tempest client manager * Update functional test \_get\_stack * Updated from global requirements * [Docs] Fix typos * Updated \*\_domain\_id to "Default" * Magnum plugin: import exceptions robustly * fix typo oslo.messaginga * Fixing application template deletion * Fix mismatch description in env template api doc * Updated from global requirements 3.0.0.0b1 --------- * Updated from global requirements * [devstack] Pass MURANO\_USE\_GLARE as bool, not as str * Update version of cloudify client in cloudify plugin requirements * Move service broker api to the top level * Generate separate db for murano service broker * Replace obsolete way of get creds in tempest tests * Documentation to use sub-templates in HOT packages * Updated from global requirements * [Docs] Replace names and images * [Admin Guide] System requirements * Added package references to generated UIs * Forced flush of tmp file to disk on ui retrieval * Support GLARE configuration in Murano tempest plugin * Fix up Assignment table in MuranoPL Reference * Remove unnecessary executable permissions * [Docs] Add info about manifest format to versioning docs * Updated from global requirements * [docs] Fix small typo in docs about automated tests * Updated from global requirements * [Trivial] Fix mismatch string format argument * Add models and migration for new service broker db * Increase status report messages time resolution * Correct wrong import statement in Magnum App * Updated from global requirements * Trivial: ignore openstack/common in flake8 exclude list * [docs] Add info about sanity\_check suits * Updated from global requirements * [Docs] Install diskimage-builder by pip * [Murano Docs] Versioning * Allow insecure SSL communications with RabbitMQ * Modified docstrings to comply with pep8 H405 style check * Change incorrect murano-agent bin file location * Updated from global requirements * Remove explicit version from magnum plugin * Support GLARE installation in devstack Murano plugin * [Murano Docs] Cinder Volume supporting * Pass [rabbitmq]/ca\_certs file to murano-spawned instance * Skip test\_migrations on Python 3 * Revert the destroy execution order * [Trivial] Remove unnecessary executable privilege * [Murano Docs] Murano Contributor rules * Fix tempest.conf generation * Support for \*args/\*\*kwargs was added to the MuranoPL * Deployment reports datetime DB insertion was fixed * Incorrect Method param inheritance was fixed * [Docs] Add limitations of deployment murano-agent by cloud-init * Fix typo in word "settings" in docs about using glare * Fix inaccuracies in docs about murano apps tests * Fixes race condition in HeatStack * Fixed a bug in \_get\_glare\_url * Updated from global requirements * Fix base64 on Python 3 in yaql\_functions.py * Port cloudfoundry/test\_cfapi.py to Python 3 * Port test\_plugin\_loader to Python 3 * Fix tox -e py34 * Imported Translations from Zanata * [Murano Docs] Edits * Use unified decorator for retries * Updated from global requirements * [Murano Docs] Remove articles * [Murano docs] Publish CLI section * Switch to using get\_notification\_listner * Change clusterip field description in docs about Dynamic UI * Add ability to configure home region in murano devstack installation * Do not force admin credentials in service broker test suite * Remove tempest-lib from test-requirements * Increased the size of TEXT columns to store large object models * 'GetPackageUI' API can now be called even if Glare is used * Updated from global requirements * Add help string for cfapi opts * Updated from global requirements * Heat stack deletion for HOT/TOSCA packages was fixed * [Murano Docs] Install murano-agent by cloud-init * [Murano Docs] Manage environment templates * Add documentation about bundles * Define context.roles with base class * [docs] Add stable branch backporting documentation * Updated from global requirements * [docs] Documentation about using Glare Artifact Repository * [docs] Fixed cfapi CLI command sections * Track status report timestamp * Fix Heat Resource Leak in LinuxMuranoInstance * Fix links for samples in heat-transtator plugin * Destroy orphan objects * Don't use bash eval for Linux.runCommand * Add note about big files download * [Murano Docs] Set environment variables * Fix case-sensitive filter value in cfapi * Docs about murano applications debugging * Do not upgrade packages with cloud-init * Fix typos in Murano files * Clean up the configure network doc * Revert "Use unified retrying decorator" * Use unified retrying decorator * Document changes in Dynamic UI * Adds a test for logo validation * Handling of ephemeral methods was fixed * Removed warnings from building docs * [doc] Fix typo in directory name * [doc] Fix formatting for argument * Also package murano\_tempest\_tests * Remove unnecessary packages.rst * [doc] Fix malformed table error * Use \_LW for deprecation warning * Update reno for stable/mitaka 2.0.0.0rc1 ---------- * Upgrade bind function to use murano actions * [cfapi] Use muranoclient to access murano packages * Fix error on deleting Magnum App * Document MuranoPL Metadata * Removes ability to alter defaults for child objects * Add descriptions for policy rules * Do not wait for MessageHandlingServer * App to deploy Magnum Bay * Updated from global requirements * Documentation for static/extension method/properties * Import and deploy TOSCA CSAR packages * Deprecate packages\_opts conf group * Delay load of package Meta * Modified the wrong note * register the config generator default hook with the right name * [Murano Docs] Install murano client * [Admin Guide] SSL configuration * Meta values evaluation was fixed * Documentation for reflection capabilities in MuranoPL * [Admin Guide] Deploy murano * Spelling mistake corrected * Add 'delete-application-from-env-template' in spec doc * Updated from global requirements * Moved CORS middleware configuration into oslo-config-generator * Fix static method parameter validation * Improve logo validation during package uploading * Makes config() function be available to Core Library only 2.0.0.0b3 --------- * Added property to associate environment with OS region * Extension methods were introduced to MuranoPL * [Murano Docs] Fix description for bundle-import * Py3 compatibility fixes * Support of MuranoPL extended metadata was added * [Murano Docs] [CLI] Intro * [Docs] Add info about two new properties of Instance class * Identity v3 support to external context middleware * [Murano docs] fix command name in enduser-guide * [doc]Update the compose.rst images path * Add roles to RequestContext.to\_dict if they're not there * Do not use list lenght checks in Env-Templates test suite * Fix the entry point for oslo-config generator * Update tempest plugin after tempest-lib deprecation * Added links to murano-specs documentation * [Murano Docs]/[docs] Replace all instances of "tenant" with "project" * Fix clobbered releasenote * Fix incorrect ICMP rule in SecurityGroup * Remove unused pngmath Sphinx extension * Rename glance to glare * Ability to have several MuranoPL classes in single YAML file * Use precompiled regex for yaql expression detection * Use more generic "type" name instead of "murano\_class" * Support unversioned keystone endpoints * [doc]Improve the Murano client doc * Updated from global requirements * Update docs, to reflect correct place for driver config option * [Core-Library] Fix the name of variable in Instance method * Remove obsolete tests for logging * Added [murano-test-runner] venv in tox * Strengthen validation of receiver type for methods * Namespace resolution was incorrect for empty namespace * Refactoring of smart-types defined in DSL * Allow static methods to detect calls on object * Import of static and classmethod Python methods * [Murano Docs] Multi-region support * Updated from global requirements * Removes unnecessary import * Heat stack was not always deleted * Fix tests that broke after release of oslo.config 3.8 * Add multiple engine workers * Updated from global requirements * Major refactoring of how OS clients are created and managed * Operations on reflected entities * Updated from global requirements * Enable pep8 to check files in directory tools * Support for static methods/properties * Basic reflection capabilities were added to MuranoPL * Migration to yaql 1.1 * Remove incorrectly used "# flake8: noqa" * Use eventlet.monkey\_patch() before any libs are loaded * [docs]Update the package-import version parameter * Updated from global requirements * New operator 'is' was added * [Murano Docs] Log in to murano-spawned instance * [Murano docs] Managing packages -edits * Update documentation to use openstack-client * Improve grammar in reno notes * Versioning for class configs * Add the max length check for environment update * Add test for update environment with invalid name * Don't use list lenght check in Repository test suite * Updated from global requirements * Remove KEYSTONE\_CATALOG\_BACKEND variable usage * Update glare definitions path * [Murano Docs] Fix Windows 2012 R2 and 2008 R2 image links * Fix adding unit test module * Removes unused posix\_ipc requirement * [Core-Library] Add ability to boot instance from volume * Updated from global requirements * Updated from global requirements * Replace HTTP 415 return code to HTTP 406 when it's correct * cleanup of manual rst file murano docs * cleanup of specification murano docs * Fixed confirmInput parameter name in Dynamic UI docs * Fix incorrect result of listing templates * Remove xfail in service broker negative tests * [Murano Docs] [CLI] Remove manage apps * [doc] Add documentation on package loaders and package cache * [Core-Library] Reset all out properties during releasing * Updated from global requirements * Skip description from service transformation * [Murano Docs] Extend Contract section * Use uppercase 'S' in OpenStack * Fix spelling typos * [package\_cache] Lock usage\_mem\_lock based on package id * [Core-Library] Increase format version * copy-editing of debug-tips murano file * fixed typo on client rst file * [Core-Library] Add 'direction' argument to '\_addGroup' method * [Murano Docs] Fix indentation level in UI definition example * Remove old API tests in favor of tempest plugin * [Core-Library] Delete redundant expression from Instance deploy method * Add environment templates test suite to the tempest plugin * Add categories management test suite to the tempest plugin * [Murano Docs] [CLI] Deploying * Allow murano to run under windows * Update \_get\_tags function to prevent race condition * Move load\_packages\_from from engine section to packages\_opts section * Cleanup .lock files after package deletion from cache * Add service management tests suite to the tempest plugin * Add session management test suite to the tempest plugin * Add environment management test suite to the tempest plugin * Add missing whitespace to error message * Attempt deleting stale packages from cache at download time * Add application catalog repository tests to the tempest plugin * Updated from global requirements * [Core-Library] Fix the way of getting DNS in describe() method * Updated from global requirements * [Murano Docs] [CLI] Manage categories * Remove cap for pip version * [Doc] Update YAQL repository link * Updated from global requirements * API hanged on a termination signal * testtools was moved to main requirements file * [docs]Rename 'Package Definitions' to 'Packages' * Introducing ConfLangInstance * Fix import order of modules * Allow package cache to persist on disc * [Core-Library] Add ability to specify direction and ethetype for groups * Updated from global requirements * "notification\_driver" from group "DEFAULT" is deprecated 2.0.0.0b2 --------- * Cap pip to <8 for dsvm job * Python3: Add support for raise and urlparse * Install murano-agent by cloud-init * [Docs] Update Dymanic UI specification * Fix using map() for python2,3 compatibility * [docs] Add murano test runner information * Fix migrations to maintain compatibility with sqlite * Fixes attribute store for MuranoClassReference types * Updated from global requirements * Python3: Use six.moves for py2 compatibility * Python3: Replace basestring by six.string\_types * [Doc] Fix directory name containing execution plan template * Add package check for package name length * Updated from global requirements * Python3:Replace iter.next() with next(iter) * Fix python 2 and 3 compatibility issue with six * Python3: Replace dict.itervalues with six.itervalues * Python3: Replace dict.iterkeys with six.iterkeys * [docs] Update to use openstack CLI instead of glance(v1) * Python3: Keep compatibility for urllib.urlencode * Imported Translations from Zanata * Updated from global requirements * Broken owner parameter for getAttr/setAttr was fixed * Python3: Fix using dictionary keys() as list * Python3: Replace dict.iteritems with six.iteritems * [Doc] Fix 'Murano packages structure' link * py3: Replaces xrange() with six.moves.range() * make enforce\_type=True in CONF.set\_override * [Murano Docs] [CLI] Manage environments * Support for Cinder volumes was added * Fix the parameter order in method assertEqual * Correct some spelling mistake in guide * Correct filename * Updated from global requirements * Replace deprecated keystoneclient...exceptions * Fix error in app io.murano.apps.demo.DemoApp * [test-runner] Refactor mock unit-tests * Replace deprecated library function os.popen() with subprocess * Replace assertTrue(isinstance()) by optimal assert * Modify filter by 'Name' in Package Definition * Use assertTrue/False instead of assertEqual(T/F) * Updated from global requirements * Updated from global requirements * Imported Translations from Zanata * user\_id column was widened to support domain users * Replace unicode with six.text\_type * Add tagging fuctionality for heat stacks * [test-runner] Create new executer on each test case * Replace tenant\_id with project\_id in auth\_utils * Fix test class property access in mocks * [Murano Docs] Introduction to the User Guide * Missing blankspace in debug in murano.common.statservice * Drop MANIFEST.in - it's not needed with PBR * [docs] Add the step to add passenv in tox.ini * Adjust '410 Gone' exception in service broker negative tests * Use murano client for getting final status of environment * Change LOG.warn to LOG.warning * Wait for state progress of Heat stack upon delete * Updated from global requirements * [Murano Docs] Describe app migrating to L * [test-runner] Put TestFixture class check to the right place * [Murano Docs] Adds Using CLI section to End User Guide * Update buiding windows image article * Add mutable default arguments hacking check * Update the README.rst * [AppDev Guide] MuranoPL Core library * change the repo stackforge to openstack and github to cgit * Remove arguments "{}" and "[]" in function definitions * Updated mock release note and mock-index maxdepth * [docs] Add example of delete item from the list * fix typo in doc/source/image\_builders/upload.rst * Updated from global requirements * [murano-test-runner] Mark 'package' as required parameter * Stop using WritableLogger() which is deprecated * [mocking-machinery] Add original method function * [Murano Docs] Fix links on client docs page * Add release note for fip on multiple networks fix * Updated from global requirements * Pass environment variables of proxy to tox * [mocking-machinery] Add inject YAQL functions * [Murano Docs] Add an intro to apps section * Remove libs and libs usage from murano and murano hooks * Updated from global requirements * Fix spelling error for db migration file name * Updated from global requirements * Added CORS support to Murano * Add an option to filter packages by 'id' in API * Improve public network detection algorithm * Do not wait for MessageHandlingServer * Synced requirements with global requirements * Updated from global requirements * Modify describe method of core-library networking classes * Fix Linux.runCommand method * Add MockContextManager * Fix alignment in log messages * [Murano Docs] Add an intro to env section * Remove version from setup.cfg * Add OneOf smart type * Updated from global requirements * Raise proper exception in ext context middleware 2.0.0.0b1 --------- * [Admin Guide] Policy enforcement * HOT outputs were merged * Fix debug log message * Functional tests for Chef and Puppet examples * [Murano Docs] Remove logs section * Include default region for multiregion testbed * [cfapi] Add multiple tests for cfapi service * [Murano Docs] Browse component details * Glare Plugin Package version removed * Updated from global requirements * [Murano Docs] Add info to deploy an env * Replacing application\_catalog with application-catalog * Updated from global requirements * Remove iso8601 dependency * Documentation for package type plugins * Force releasenotes warnings to be treated as errors * [Murano Docs] Review an environment * Remove unused bunch class * [docs] Update error message * Add verbosity control for Murano-test-runner * Add binding test for cfapi * Add negative test for cfapi last\_status route * [cfapi] Prevent code 500 if instance, environment or service doesn't exists * Skip package section from input parameters * Add provision and deprovision test for cfapi * Fix method comment typo * Remove 'not in global requirenments' section * Public environment template * Fix logging\_setup call * Updated from global requirements * Get scoped token from ext\_context middleware * Update functional tests due to tempest update * [docs] Fix unit tests location * [docs] Remove py26 from tox targets * Fixed broken assignment to dictionary using integer key * Updated from global requirements * [Murano Docs] Edit an environment * Fix Example of net-config filename * Update help message of test-runner to same format * [Murano Docs] Create an environment * Rework service broker authorization process * Updated from global requirements * Documentation for Cloudify plugin and example application * Add reno for release notes management * Add middleware for external requests * [docs] Introduction minor edit * Fix TypeError set param from 'null' to some value * Add new dsl exception for better error reporting * Add functional api tests for categories * Initial commit for service broker tests in tempest plugin * Improved reporting for EADDRINUSE error * Set the port to use oslo\_config PortOpt * remove default=None for config options * Use detail instead of explanation for 415 errors * Adding Cloudify apps library and Example App * Adding the Cloudify Plugin Files * Drop types module usage * Remove obsolete names checks * [documentation] Deletion of the articles rewritten * [test-runner] Show help on error * Use assertIsNone instead of assertEqual(None, \*\*\*) * Banish all 'witches' in murano repository * Move variables from plugin.sh to settings * Update default devstack MURANO\_REPO\_URL * Fix test execution result status * Move cfapi opts to global variables view * Add murano-cfapi endpoint during devstack installation * Updated from global requirements * [Murano Docs] Deployment logs * Return 403 instead of 401 HTTP Response * Move enabling services to plugin settings * FIP was not assigned to revived Instance * Import packages from murano-apps in Devstack * [documentation] Introduction and User Guide publishing * [doc] Improve devstack plugin installation docs * Use default readthedocs theme * Adjust tempest config file path * Remove libs and extras.d usage from gate hooks * Package type plugins support was added * Change in Murano Devstack installation * Updated from global requirements * [doc] Add CLI deployment instructions section * Check dashboard symlinks during horizon plugin installation * Add murano service broker in devstack installation * No need to enable services when plugin is activated * Remove unused tests from project * Updated from global requirements * Streamline test\_simple\_software\_configuration * [Docs] Add murano agent description * Improve message for publish package conflict * Updated from global requirements * Log request headers in debug mode * Open Mitaka development 1.0.0 ----- * Fix a typo in db/catalog/api.py * Fix the delete package give the error with """ * Add test for simple software configuration * Adds plugin dependency for plugin demo app * Initial commit for service broker documentation * Add Apache 2.0 license info to core library * Fixed a typo in example plugin config * Introduce MuranoPL/1.1 and 1.2 * New middleware to handle ssl termination proxies * Updated from global requirements * Use migrate command instead of syncdb * Increased the number of environments per tenant * Updated from global requirements * Fix typos in Murano PL Reference * Fix a typo in Quickstart guide * Fix typos in developer documentation * [Murano Docs] Application topology * Change the way devstack configures murano-dashboard * Do not try to delete server from deleted Heat stack * Makes conditional expressions consistent with yaql 1.0 * [Docs] Describe token expiration in troubleshooting * Incorrect yaql engine was used for MuranoPL/1.0 * [test-runner] Register test-runner system class automatically * Return full structure of std info in Linux class methods * Fix obsolete imports in functional tests * Updated from global requirements * [test runner] Support --config-file option * Retry-on-deadlock added to modify package db api * Replace deprecated tempest config option * Customization of default\_dns in Devstack * Add dependent application test * MuranoPL logging tutorial added * [docs] Add commands to register murano in Keystone * [Intro] Architecture and use cases sections editing and rewording * Updated from global requirements * Make test\_package\_is\_not\_provided less strict * Python method results were not converted to immutable values * Updated from global requirements * Made service broker to handle asynchronous request * Add 'acquired\_by' property to Environment resource * Fix building documentation on readthedocs * Add 'How to login into instance' section to the new docs * Test hot package files with sets instead of lists * Number of yaql functions caused AmbiguousFunctionException * Fix get\_sessions state parameter not working * Fix a typo for environments error msg * Fixed "ERROR: Malformed table" in RST * Add troubleshooting section for a new documentation * Fix order of arguments in assertEqual * Add prefix 'test' to unit test module names * Fix typos in app development guide * Enable H233: Python 3.x incompatible use of print operator * Keyword validation regexp was fixed * Add test for resources deallocation * [Murano Docs] Add Deploy an env HowTo * Specification of which property/argument violated contract was added * Murano waited indefinitely for UPDATE\_COMPLETE stack status * Fix the run\_test unchecked pep8 W503 and E402 errors * Add missing import of '\_' in template\_applications * Release resources allocated to the Instance when it gets deleted 1.0.0.0rc1 ---------- * Fix minItems value for tags in json-schema * Fix link in the documentation * Remove mistral libs installation from test-hooks * Fix agent error message * Change ignore-errors to ignore\_errors * Function caller was incorrect when called from Parallel block * Updated from global requirements * Show public packages for non-admin users * Fix three typos * Fix an error with wrong argument * Remove H904 rule from ignore list * Updated from global requirements * Support unicode characters in Applications or Packages's filter * Added the home-page value with openstack.org * Updated from global requirements * Support for Unicode strings in MuranoPL was fixed * Fix race condition with router creation * Improve the Murano Documentation * Delete the unused LOG configure code 1.0.0.0b3 --------- * Updated from global requirements * Added the support of Glance Artifact Repository * Log refactoring close to new logging spec * Fixed incorrect MuranoPL names for some of Python-based methods * Murano Policy Based Modification Documentation * Update response codes to expected ones * Add id to sort keys parameters while pagination * Environment modify actions introduced * Convert api internal TypeError to HTTPBadRequest * Updated from global requirements * Remove legacy network-related code * Add cd, export and enable service command in doc * Updated from global requirements * ID references made model not able to load in 2 passes * Improve error message in case no environment name * Use default devstack functions for create murano in devsstack environment * Fixes incorrect log format string * Fix typos and plugin samples link * Fix the location of unattend template * Apply yaql conventions to \_\_init\_\_ parameters * yaql context versioning * Fix pylint errors 'unused variable' * Fix murano devstack libs and plugin * Restores back plugin support * Version-aware YAML loading * Glance MuranoPackage Artifact * Package versioning * Temporarily skip tests with dependant applications * Add support for more Cloud Foundry API calls * Fix silently overwrite user specified content type * Updated from global requirements * Cloud Foundry Service Broker API initial commit * Introduce test-runner for MuranoPL test packages * Add tenant\_id check for {env\_id}/lastStatus api * Abandon environment on each exception during removal * Updated from global requirements * Remove tethering between func and api tests and use venv for tempest * Fix the Download link for VirtIO * Adds version info to ApplicationPackage * Move congress/mistral integration related tests to separated dir * Add support for heat environments * Updated from global requirements * BOM symbol removed from Agent-v1.template * Run compress command for dashboard in devstack * Fixed the typo's in multiple files * Logging API for MuranoPL * Simple instance configuration * Improve functional engine tests cleanup * Removing unused dependency: discover * Namespace resolution error was fixed * Fixes Congress model validation * Type cast error in engine * Updated from global requirements * Add port type on port option * Enforced AUTO replacement\_policy for sharedIp port * Updated from global requirements * Fixed the SharedIp class * Make commands in install manual copy-paste-able * Add logging.conf to gitignore * Migration to yaql 1.0 * Updated from global requirements * Update the tox.ini for build docs * Concatenate environment ID to stack name * [Murano Docs] Add an application HowTo * Default Region Configuration Property * Fixing support for package download API * Updated from global requirements * Update the gitingore file * Updated from global requirements * Updated from global requirements * Return x-openstack-request-id header to the caller * Inherit RequestContext from oslo\_context's RequestContext * Updated from global requirements * Typo leaded to FIP not being assigned for nova-network * Add category list pagination support * Attempt to make pylint output more useful * Added info about client and murano-apps trackers to CONTRIBUTING.rst * default\_dns from config was never used * Add support for heat files * Do not confuse terminal stack statuses * Add HelloReporter application to rally-jobs * [Murano Docs] Add Delete an application HowTo * Updated from global requirements * Fix minor typos in murano example plugin * Making murano components aware of openstack ID * Postpone setting fipAssigned attribute until stack successful push * Update the gitingore file * Perform tests tagging * Add dummy test-app and test for it * Prevent 500 when requesting deployments of a deleting env * Attempt to fix congress client initialization * Use oslo.log library instead of system logging module * Remove openstack.common package * Warn user about unsupported content type * Fix incorrect format syntax in app\_loader * Check session validity during env show api call * Updated from global requirements * [End User guide] QuickStart, delete app * Fix sample logging config 1.0.0.0b2 --------- * Add possibilty to delete environment with incorrect .destroy method * Include original ObjectsCopy/Attributes in exception\_result * System class was forgotten in core library manifest * Remove duplicated check env code * Removes early creation of Heat stack * Updated from global requirements * Fix logs loss after switching to oslo\_log * [Docs] Add policy file description * Delete ability to manage packages by name * Separate load\_app from api starter * Introduces combined class loader * murano.cmd.api: Unable to import 'paste' * Updated from global requirements * Fix typo in error message * Improve Murano PL docs * [Murano Docs] Add Delete an app HowTo * Describe 'UI Network Selection' in the docs * Check tenant id during abandoning of an env * Allow setting eventlet.wsgi.MAX\_HEADER\_LINE * Remove entry of 'deprecated' log level * Restrict environment template name length to 255 * Move congress-specific test functions to separate class * Introduce refactored tests * Restrict environment name length to 255 * Make tools/cover.sh executable * Remove hardcoded dns from config file * Handle ValueError from hot\_package * [Murano Docs] Add managing packages HowTo * Use setup\_develop in devstack libs and plugin for devstack * Switch to oslo\_log * Improve the docs on UI Definitions format versions * [Murano docs] Quickstart partial * [Murano docs] Adds bigger screenshots * Fix error message in case category name is too long * Declare the CONF variable * Switch to oslo.service * Use Custom networks as primary if no others exist * [Murano Docs] Add Searching for an app HowTo * Added script for unit tests coverage job * Remove unnecessary pass-statement * Move load\_paste\_app to the place it should be * Remove the wrong assert\_is\_called checking * Remove all vim modelines * Allows congress to fetch environments from all tenants * Hide TrustId in log to tighten up security * Organize imports in correct order * [Murano Docs] replace screenshots * Remove vim headers and shebang * Reload logging options when receiving SIGHUP 1.0.0.0b1 --------- * Update from global-requirements * Improve dsl exception readability * Update version for Liberty * Fix inconsistency of error message and validation when create env * Add CLI reference 1.0.0a0 ------- * Document murano actions * [Murano Docs] Adds ref links to manage\_applications * Do not store session db in tmp directory * Modify environment delete API endopoint to have "abandon" feature * Enable trusts by default * Make use of devstack external plugin * Fix table with block constructs in muranoPl doc * Fix urls in docs for image builders * [Murano Docs] Fixes a typo in the Import an app package section * Increase default timeout for agents response to 1 hour * Update from global requirements * Migrations: ignore mysql fk cheks for any mysql driver * Add new content to Murano introduction article * [Murano Docs] Add importing an app package HowTo * Clean-up openstack.common * Attempt to make pylint output more useful * Add note to docs that hash is not used in images.lst * Use oslo.policy instead of incubated version * [Murano docs] MuranoPL section review * [Murano Docs] Remove old versions of articles * Switch from MySQL-python to PyMySQL * [Murano docs] Examples section review * Murano docs: Intro, key features, target users * cleanup murano docs in the image\_builders folder * murano install docs improvements * murano documentation and cleanup * Update docs about Require section * Add support for 'boolean' HOT parameter * corrected typos throughout murano source articles * Add MURANO\_REPO\_URL customization support for devstack * Improve exception handling during enviroment editing * Murano docs: navigation restructure * Add unique constraint to environment table * Update meta folder readme * Refactor test plugin app * Fix incorrect method when creating unicode env * Add Examples and Use-Cases to app developer guide * App Dev Guide: Murano packages * Appdev guide migration * 'Execution plan template' section * App Dev Guide: MuranoPL section * App Dev Guide: Hot Packages * Removed CLI tests from repo * Adds FAQ content to the Murano docs * Step-by-Step * Add Rally jobs related files to Murano * Update from global-requirements.txt and fix cli tests * Installation docs: install core library as public package * Use catalog=True for package\_loader queries * Murano docs structure * Better ordering of HOT parameters in generated UI form * Improved error reporting for HOT applications * Fix for HOT parameter types conversion * Fix cleanup\_duplicates PluginLoader method * Remove unused dependency on lockfile * Execute pre/post deployment hooks on GC * pselect exited on first exception * Fixed YAQL tag leakage to YAML loader * Add missed tests for migrations * Test db api pagination and ordering * Update from global-requirements.txt * Add stevedore to Murano Requirements * Fixed rules for Congress integration tests * Drop use of 'oslo' namespace package * Add missing RabbitMQ user parameter to Murano lib in devstack * Streamline and simplify package filtering api * Updated YAQL requirement to >= 0.2.6 * Fixes Heat stack rollback on failed stack update * Update project README file * Remove obsolete system class definitions * Remove murano-manage usage from the documentation * Adding lintstack to support pylint gate job * Fix bug with murano-dashboard cleanup in devstack * Open Liberty development 2015.1.0rc1 ----------- * [devstack] Automatically enable required service in devstack * File key is wrongly created when the File Type is downloadable * Update devstack scripts to reflect repo rename * Update docs to reflect repo rename * Update .gitreview file to reflect repo rename * Add method in Agent class to allow message handling extensions * Fix for cross-tenant package and class isolation * Show example of setting requirements to flavor field * Policy enforcement - add cleaning action rules * Cleanup action policies will be created manually as described in this documentation * A Galera-compliant database locking solution * Enable OpenStack Theme for docs * Adds describe method to Network implementations * Murano documentation update * Log exceptions, raised during api * Forbid setting is\_public via querystring * Update default policy settings * [devstack] Install Murano as plugin for Horizon * Remove filter for tasks to show logs for all actions * Set template for docs * Allow any admins to perform actions on packages * Nova Network support * Restricted text search on packages to text fields * Policy enforcement 'services' relationship * Add package\_count to Category objects * Fix installation document * Safely encode yaql expressions to support unicode expressions * Update muranodashboard installation * Cannot create environment from env-template * Update Image Builder documentation * Fixed wrong super call in models.TimestampMixin * Removed redundant code from models module * Fixed 500 error in get\_result API handler * Fixes instance FIP assignment * Bump python-muranoclient version in requirements * Provide Identity version for negative tests * Updated from global requirements * Fixes action result serialization * Computes transitive relationships in murano model * Fix EP building if app doesn't have files in template 2015.1.0b3 ---------- * Initial implementation of Plugable Classes * Fixes incorrect handling of <...> in execution plans * Updates k8s DIB element * Functional tests for environment template functionality * Configurable environment's default network config * [DIB] Kubernetes: added installation of flannel and haproxy * Fix error with package tags type * Use python from venv for subunit-trace * Fix pep8 issues in imports * Environment Template. Create an environment from a template * File Downloable Feature * Environment Template API documentation Targets https://blueprints.launchpad.net/murano/+spec/environment-template * Fixed gate-murano-congress-dsvm job * Added iniset for policy\_enforcer if Congress policies are enabled * Modified Mistral Workflow due to syntax change * move configure\_network post-config to extra phase * Fixes agent call may hanged upon action call * fix typo * Reworked functional tests for Murano * Implement category management API * Makes exception\_traceback optional for exception\_result * Remove deprecated Kubernetes binary "kubecfg" from DIB element * Update API policy * Adds API to obtain action result * Return 409 status instead of 500 if package exists * Extension API for environment templates * Ignoring properties with None value in Murano->Congress mapping * Fixes environment owner in congress mapping * Remove test skipping * Mistral integration functional tests - scripts * Environment Template entity and its service * [DIB-elements] Kubernetes binaries added to PATH * Remove wsgiref from requirements.txt * Functional test for Murano Mistral integration * Update from global requirements * Add document describing v0.5->Juno app migrating * Fixed logic of determining environment status * Changed status constant PENDING -> pending * Kubernetes DIB element: fixed critical bug with application path * Include missing log string format specifier * Fix Policy Enforcement Test App * Fix for testclass TestRepositoryNegativeForbidden * Use oslo.i18n for translation * Policy enforcement functional tests - scripts * Fix inaccuracies in dashboard manual installation * DIB elements for Docker * DIB elements for Kubernetes * Improve exception message 2015.1.0b2 ---------- * Policy enforcement functional tests * Fix for tempest tests * Added tenant check in sessions API resource * Added auth customization in API tests * Update dashboard installation section in docs * Changed models to match migrations * Add timeouts to murano-agent calls * Fix for DSVM gate * Resolve and enable H702 PEP8 rule * Resolve and enable H307 pep8 rule * Resolve and enable E265 pep8 issue * Resolve H305 pep8 issue * Enable and resolve some PEP8 issues * Adds ability to join instances to existing Neutron networks * Add class that runs Mistral workflow * Remove 'murano\_metadata\_url' from config options * Documentation for policy enforcement * Reverted router\_id to router in NeutronNetwork * Add availabilityZone property to Instance class * Initial implementation of policy enforcement point * Changes replacement policy for Neutron ports to AUTO * Use pretty-tox for better test output * Refactor db migration tests * Configure Murano network in devstack * Fix rest\_client import for Tempest based tests * Fix name of the properties for networking resources * Remove usage of FloatingIPAssociation * Adds congress client to client manager * Add Mistarl client to murano * Log details about finished deployment * Fix usage of oslo.utils.uuidutils 2015.1.0b1 ---------- * Drop unused dependencies * Adds per-class configs * Updated from global requirements * Removed unused file from openstack/common * Removed deprecated run\_tests.sh * Removed unused uml-generator from contrib * Drop ordereddict from requirements * Replace anyjson with oslo.serialization * Use oslo.serialization * Use oslo.utils * Change the package 'description' field type * Remove unused imports from genconfig * Update from oslo incubator * Fix Murano app names * Workflow documentation is now in infra-manual * Update information about murano sample config file * Removed outdated init scripts * Added generated sample config to gitignore * Remove py26 from tox targets * Merged duplicating unit tests for actions * Update from global-requirements * Update DB migration tests in respect to new release of Alembic * Adds aggregate function to MuranoPL (YAQL) * Fix dvsm gate * Add tests for tenant isolation * Remove skipping tests on resolved bugs * Change name new\_name generation * Change application index default sort from 'created' to 'name' * Remove #noqa from gettextutils imports * Fix documentation errors and warnings * Use Keystone trusts to get fresh token * Remove unused functional from murano/common/wsgi.py * Fix method lock release upon exception * Update from global requirements * Use oslo.config generator in murano * Environment in delete failed state was in progress forever * Extract version definition to a separate file * Add check on CLI environment actions * Sync with latest Tempest changes * Update article about functional tests * Fix defects in API specification * Add information about network configuration * Add information about 'update\_settings.sh' script * Add initial information about debugging * Extend installation guide * Update manual launching guide * Exceptions get muted in Try-Finally * Open Kilo development 2014.2.rc2 ---------- * Updated from global requirements * Missing validation for environment name in API * No error's reported if heat stack creation/update fails * Fixes typo in JsonPatch UT * Fixes stealing of agent responses in some cases * Do not check config on pep8 * Runtime properties may no longer have default value * Creates a router if one doesn't exist * Use only specific router for created networks * $list.skip($start).take($count) throw YAQL exception * Updated from global requirements * Don't rely on OS::Nova::Server.addresses * Fix concurrency issue with HeatStack.push * keystoneclient.middleware -> keystonemiddleware * Throw macro caused syntax error exception * Remove Contract on customUserData * Remove setup.sh * Allow signalTransport option for sw deployments * Make engine tests based on python murano client * Add two new functions to manipulate with lists 2014.2.b3 --------- * Restore auth\_uri config option * Remove partial clean-up in Instance.destroy * Fix race condition when two Instances are deployed * Move wsgi module to murano/common * Add status reports to HOT packages * Fix issue with Default for Runtime props in HeatSWConfigInstance * Fix deployment failure detection * Provide a description for syntax error * Fix property initialization * Enable H202, H402, H404 rules * Apply fault middleware * Don't treat stack missing as failure during delete * Adds REST API endpoint for action execution * Improve logging in sessions.py * Migrate to oslo.db * Fixed issue with loading yaml files * Fix agent.prepare() when agents disabled * Don't hide exception messages during package load * Application base class didn't define deploy method * Fix occasional deletion failure * Fix check for floating ip in func tests * Fix pep H101(TODO) and H231(py3 exceptions) * Set next version to 2014.2 * Fixes silent deletion of environments * LHS assignment expressions didn't work with non-trivial indexation * Make apache restarts a little more forgiving * Correct InstanceNotifier yaml stub * Allow strings in heat SW config configSection * Add openstack libs to config checker * Reduce number of API requests during deploy * Allow murano-agent to be disabled * Unit tests for MuranoPL execution result serializer * Add sample logging.conf * Allow software config at deploy * Add cleanup method in devstack scripts * Define Murano API URL explicitely * Add simple smoke tests for Murano CLI client * Make default branches independent * Fixed race in DB migration testing * Default is\_public to false * Fixes to unit tests around policies * Add caller information to Begin execute log entry * Move and rename functional tests * Use with\_variant method for dialects db types * Fix the concurrency issue agent queue creation and VM agent * Added Murano config checks * Fix the issue with multiple agent call 2014.2.b2 --------- * Fix tests which checks stack deletion * Add version command to murano-db-manage * Make sure supplier logo saved on dashboard upload * Handle null JSON blobs * Add failure messages in engine functional tests * Fixes name generation for Heat stack * Inherit api tests from tempest base test class * Adds Continue macro to MuranoPL * Unit tests for macro blocks * Add new requirements install in setup.sh script * Fixed incorrect information on Python frames in MuranoPL stack traces * Unit tests for exception handling in MuranoPL * Fix issue with incorrect model on MySQL * Unit tests for engine's YAQL functions * Fix DB migration script * Delete Heat stack when environment is deleted * Fix stack inconsistency after app deletion * Improve dsl testing framework foundation * Add 'userdata\_format' to Server heat template * Add optional fields to packages for supplier info * Unit tests for MuranoPL assignment expressions * Add --log-file to each daemon init script * Fix issues with Heat template updating * Fixed issues with port checks timeout * Fix syntax error in Environment.yaml * Fix setup.sh to import core library each time * Use random name for Heat stack name instead of environment's name * YAQL version updated to 0.2.3 * Add lib-pqdev to Ubuntu prereqs in documentation * Unit tests for method and property access * Unit tests for contracts * Code refactoring and improvements for MuranoPL testing mini-framework * Updated from global requirements * Fix pep8 issues * Update docs to reflect change from murano-api to murano * Add articles about Automated Tests * Add testresources in test-requirements * Add port checks after deployment success * MuranoPL testing mini-framework * fix db-sync execution in Murano setup script * Added coveragerc * Added DB migrations on Alembic * Change how actions are stored in Object Model * Return and Break were broken * MuranoPlException was referenced from incorrect module * Support specifying \`\`sort\_dir\`\` key in packages.search call 2014.2.b1 --------- * Deprecate run\_tests.sh * Adds ability to throw/catch/rethrow exceptions in MuranoPL * Bump pythonclient version to 0.5.2 * Maintain virtual MuranoPL stack trace * Add policy checks to API * Add heat\_template\_version to network fragments * Make sys:Resources class use resources belonging to its owner * Install python client from repo * Introduce a SharedIp object for Clustering * Make categories optional parameter * Fix issue with incorrect uploading packages * Fixed endless recursion loop when super() called in base class * Add API unit tests * Renames 'Workflow' to 'Methods' * Automatically call MuranoPL initialize/destroy methods * Improve method resolution rules for multiple inheritance * Ensure that all the Instance's networks are created prior to joining * Move Neutron networking implementation to Core Library * Add new deployment tests * add pid directory deletion in murano setup script * Fixes Python 2.6 compatibility in HeatStack class * Fixed VM instance tracking in MuranoPL core Instance class * Migrate unit test to testtools * Correct property accessibility for classes in Instance hierarchy * Declare a Runtime property for StatusReporter in Environment * Reorganize documentation index page * Make environment name unique within a tenant * Subsequent stack updates for Heat have proper set of parameters * New instances can now join Networks created in previous deployments * Add support for actions in engine * Fix issue with package loader * Make packages API work on Windows * Restore identity of upcasted MPL object on method call * Add default for getAttr * Preliminary support for HOT packages * Drop unused dependencies * Cleanup tox.ini * Allow package\_get by name * Refactor api.catalog.search() method to provide 'next\_marker' value * Make DELETE request return 404 for nonexistant package * Update dynamic ui fields docs, fix some indents * Fix incorrect import after rename * Import networking library with devstack * Add initial tests for application deployments * Rename muranoapi to murano * Code update * Update gitreview to match repo rename * Renamed the PublicIp and set it to addresses in instance snippet * Send message\_id as property * Add rmq message logging * Removed version prefix from the endpoint urls * Add rabbitmq section config * Add more info to dynamic UI devdoc article * Adds an explanation of the engine workflow * Add API specification * Add new articles to documentation * Add more classes to Instance inheritance hierarchy * debug level logs should not be translated * Fix credentials passed to tempest * Add documentation about Murano PL system classes * Use HOT instead of CFN heat template format * Add notification\_driver to config in devstack * Fixed python2.6 compat for RFCSysLogHandler * Allow import-package to delete & re-import * Allow AgentExceptions to be logged properly * Fix a bug when the same method could be run concurrently by 2 threads * Add docs for App Catalog: * Log unhandled exceptions during task execution * Basic Security Groups implementation * Add KeyName to Instance heat template * Increase length of 'name' column in package table * Add fix for compatibility with SQLalchemy 0.7.9 * Added functionality to assign FloatingIP addresses * Add custom String type to support collation * Fix issue with user permission on package deletion * Implemented AdvNetworking scenarios via Neutron * Added MuranoPL infrastructure for advanced networking scenarios * Added NetworkExplorer engine object class * Add negative tests for murano repository * Add negative tests for murano repository * Add include\_disabled param to pkg search * Fix teh gate * Delete ActiveDirectory application * Changed tests for murano repository * Revert "Remove hardcoded foreign keys from db.catalog.api" * Add basic reporting to engine * Uncomment tag import * Fix extracting services from deployment.description * Added ipAddresses property to Instance * Get function added * Fix issue with getting heat outputs * Fix issue with keystone * Fixed name of the log file on VM * Update ActiveDirectory UI to the last changes * Fix murano-manage import-package * Launch murano-engine with Murano services * Add file limit for a package archive during upload * Set a proper name for murano config section * Added developer documentation * Fixed incorrect instantiation of Resources class * Heat stack could remain even if all applications were removed * Wrong Usage for dns\_ip property in AD PrimaryController * Typo in HeatStack API * DirectoryPackageLoader skipped all package directories * AD was broken because of missing Body key * Rename Type to Usage for MuranoPL properties * Added a CLI command to manage categories * Bump python-muranoclient version to 0.5.0 * Use io.BytesIO instead of sqlalchemy.byte\_buffer * Preserve keys in object's system area * Configure rabbit vhost for Murano * replace reference to .\_items w/ call to .items() * Add CPU info to stats * Fix class FQNs for Active Directory in Dynamic UI * Fix missing function error in install\_venv * Replace 'applications' in deployments data with 'services' * Add 'cp' of murano-dashboard lib file in devstack README * Default MuranoPL function argument value was evaluated incorrectly * Fix issue with hostname * Setup doc build infrastructure * Fix some options set by devstack * Get rid of murano-common * Add murano-dashboard to devstack * Fix engine results processing * Handle unicode strings in merge-dict * Typo in murano-api.conf * Devstack scripts update * Update requirements.txt due to changes in global-requirements.txt * Fix for package import * Fixes DB migrations on SQLite * Add tag assignment during manifest parsing * Support packages in dsl and engine * Remove hardcoded foreign keys from db.catalog.api * Fix wrong indentation in sample UI definition * Update dynamic UI sample - according to the new format * Remove deep dependency from muranoapi * Billing statistics improved * Fix search in tags and categories * Resolve issue with InstanceTracked instantiation * io.murano.Environment used without namespace * Removed dependency from kombu.five * Use plural for class\_definition * Fixed mysql muranoUser setup, add time sync * Add migration for inserting default categories * Catch DBDuplicateEntry exception * Resolve issue with uploading package * Make DB scheme be compatible with MySQL, PostgreSQL and SQLite * Add excpicit comparison instead of using in * Added collation to FQN field to solve MySQL error 1071 * Added 'destroy' method that is called on deleted instances * Improve GET packages API call * Make fully\_qualified a unique parameter * Fix bug with session during deploy * Removed unused muranoapi/contrib * Correct --unittest-only test behavior * Added CLI command to import packages * Fix Murano gate job * Add API entry for Statistics * Update for api and engine services installation * Requirements bumped to global-requirements.txt * Fix parentheses in log translation * Change the way of updating class definition table * Add MuranoPL Testing Framework * Remove auto-create db option from sample config * Make logger inside wsgi a bit more informative * Improve repository API * Collection of MuranoPL instance statistics for billing purposes * statistics renamed to request\_statistics * Add DELETE repository API call * Implement repository API GET methods * Refactor murano functional tests * Added explicit db-sync command * Implement upload call to the repository API * Adoption of pre-0.5 environment API to MuranoPL format * Use native oslo.messaging notification dispatcher * UML Generator script * Resolve issues with circular dependency * Implement package search in repository API * Initial commit for repository API support * Add repository API tests * Added initial version of murano application package parser * Devstack scripts update * Add db backend to support metadata-repository * Merged in murano-common * Add MuranoPL Engine * Add services tests * Added sessions tests * Added environments tests * Lead log spelling to the one style * Set up logs in config.py in appropriate way * Initial MuranoPL implementation v2 * Added base for Tempest tests in dvsm jobs * Added Devstack integration * Add versioning support * Add per API call statistics * Add Statistics Collection loop * Initial MuranoPL implementation * Use oslo.messaging * Fix and enable import checks * Synchronized Openstack Common * Remove copyright from empty files * Enable H102 "License headers" * Run hacking and flake checks for real * Update gitignore * Fixed issue with requirements * Remove oslo.uuidutils from muranoapi * Update context from Oslo * Update README with actual info * Return setup scripts in the consistent state * Remove 'openstack-' prefix in service name * Organize setup scripts * Add tempest integration tests * Cherry-pick the following commits from release-0.4 * Add new setup script and SysV inits * Cherry-pick the following commits from release-0.4: * Fix dev version package spec in requirements.txt * Support getting and updating network\_info of environment * Add common log and configuration places * Cherry-pick from release-0.3: * Enable HA on declared queues * Don't delete environment in case there are problems with RabbitMQ * Fixed the component name for tarballs * Update requirements to match havana's ones * Fixed https://bugs.launchpad.net/murano/+bug/1244118 * Remove d2to1 dependency * Resolved issue with MySQL * Cherry-pick from release-0.2 * Add murano-api to PYTHONPATH * RabbitMQ SSL parameters were not passed to MqClient for certain types of operations * Fixed log output, MRN-922 * Porting changes from Release 0.2 * Cherry-pick following change-ids from release-0.2 * Moved keystone config to conf file * Upgraded prerequisites * Resolved issue MRN-751 * Resolved bug MRN-719 * Resolved MRN-704 * Fixed a bug with error and warning state count * API now gets and handles conductor exceptions * Fixed MRN-680 * Resolved bug MRN-699 * Add info about ssl configuration to murano-api.conf * Resolve MRN-682 * Updated to muranocommon 0.2 with SSL support Change-Id: Ibf4ec63332ab1073bda7d1a1cd102cf514284188 * Migrated to Murano Common * Increment environment * lastStatus introduced * Fixed injectinit function * Modified pip search, garbage deleted * Deployment description is not hidden anymore * Fix jsonschema version in pip-requires * Added non existent environment verification * Removed extra param from config * Resolved MRN-576 * Resolved issue with session delete * Corrections in edge-case output in Deployments API * Deployment logs fetching * Remove obsolete code * Added ability to remove services * New API v2.0 * fixed python setup workflow * Fix MRN-582. Env version incrementation added * Added Traverse Helper * Move to openstack.common.db * Change oslo.config dependacy declaration * Updated to latest OpenStack Common * Cherry-pick all changes from release-0.1 branch * Resolved Bug #1186804 * Added MANIFEST.in and LICENSE * Fixed Environment::Get * Fix import in systemservices * Support for ASP.NET apps git-based deployment * Fix issue with deploy API * Resolve issues with requestion list of services * Resolved issues with Environment::Get * Handle some divergence cases * New way of session handling * Resolved issue with logging * Resolve issues with service deletion * Changed output messages * Support for ASP.NET apps git-based deployment * Add CentOS setup script and modified ubuntu script * Cleaned-up request.context code * Add setup.sh to project murano-api * Fixed issue with service deletion * Resolve issue with circular dependecies * Add .gitreview file * Rename misc files * Rename configs * KEERO-315 - Fix all occurrences of old names (keero, glazier) in REST API * Fix name of the entrypoint stript * Removed all projects except Glazier Api * Small fixes for unit tests * Fixed issue with length of new name * Fixed small issue * Fixed issue with names * Fixed issue with names * Fixed issue with names * Fixed issue with names * Fixed issue with names * Fixed issue with names * Fixed issue with names * Fixed small issue with new names * Fixed issue with horizon component installation * Fixed issue with horizon component installation * Fixed issue with horizon component installation * Finished converting API Specification * Fixed small issue with setup.py * Fixed issue with renaming of the tabula component * Fixed issue with renaming of the tabula component * Added part of API Specification * Tabula renamed to dashboard * Tabula renamed to dashboard * Finished documentation for API * Finished documentation for API * Renamed Portas to API * Renamed Portas to API * Added tox for webUI tests * Added license to documentation of Portas Client * Small fix * Fixed api interface names to environments * Fixed api interface names to environments * Renamed and licensed python-glazierclient * Fixed api interface names to environments * Fixed api interface names to environments * Renamed documentation project * Fixed licenses for tabula and tests. Fixed name of tabula project * Fixed licenses for tabula and tests. Fixed name of tabula project * Fixed a few small issues * Pass all RabbitMQ settings from conductor to Agent * Initialization of tox for conductor and portas. Add new webUI automated tests * Licenses added * A lot of changes were made * Issue with figures * Main Documentation Project * Fixed ignore file for python-portasclient * Documentation for UI * Documentation for Python PortasClient * Forgot man pages * Documentation for Portas Project * Send token when deleting environment * Fixed https://mirantis.jira.com/browse/KEERO-227 * Fixed issue with sessions * Fixed issue with sessions * Experiments * Experiments * Experiments * Experiments * Experiments * Add logging to WebUI * Add initial files for unit tests * Fixed issues with sessions Added logging * Added unit tests for client. Coverage 66% * Rename RabbitMQ username field Removed use\_ssl option from settings * Fix running install\_venv.py * Updated python-portasclient * PEP8 compliance * Fixed unit tests * Added ability to add services to environment * bug fix * bug fix * Fixed index bug * Finalize UI * Finalize UI * Experiments * Experiments * Experiments * Experiments * Experiments * Experiments * Experiments * Experiments * Experiments * Experiments * Fix PEP8 errors * Experiments * Experiments * Experiments * Experiments * Experiments * Experiments * Experiments * Experiments * Fix issue with statuses * Experiments * Experiments * Experiments * Experiments * Experiments * Experiments * Experiments * Experiments * Experiments * Experiments * Experiments * Experiments * Fixed issue with sessions * Naming conventions: use name instead of id for instance names * Heat auto-discovery, keero-linux-keys -> keero-keys * Experiments with UI * typo * Scoped tokens * Experiments with UI * Experiments with UI * Experiments with UI * Experiments with UI * Experiments with UI * Experiments with UI * Experiments with UI * Fix issue for result of deleted environments * Fix merge issue * Merged * Fixed small issue * Fixed small issue * Fixed small issue * Fixed small issue * Fixed small issue * Fixed small issue * Fixed small issue * Fixed small issue * Fixed small issue * Fixed small issue * Fixed small issue * Fix name of the variable * Fixed small issue * Send token when deleting environment * Removed unneeded binding * Fixed small issue * Fixed small issue * Fixed small issue * Fixed small issue * Fixed small issue * Fixed small issue * Fixed small issue * Fixed small issue * Fixed small issue * Fixed small issue * Fixed small issue * Fixed small issue * Fixed small issue * Fixed small issue * Fixed small issue * Added tabs for services * forgotten file * Fixed issue with activeDirectory deletion * Add support for reports filtering * Added tabs for services * Fixed small issue * Fixed small issue * Fixed small issue * Fixed small issue * Fixed small issue * Fixed small issue * Added dropdown list with list of services * Added initial version of tabs for services * Added initial version of tabs for services * Added initial version of tabs for services * Fixed issues with templates * Fixed issue with installation * Fixed issue with installation * PEP-8 * Fixed issue with incorrect import * logging and bug-fixes * fixed VM initialization script * Evironment/Units deletion, bug-fixes * Fixed UI issues * Remove service deletion button * Finished installable UI * Removed old code * Naming conventions changed * Updated OpenStack Common (Fixed issue with notifier package) * Use Heat REST API instead of command-line * Added support for setup.py Added localization and documentation skeletons PEP8 Fixes, optimized imports * #KEERO-222 Remove random part of unit name * Send Env to Conductor for deletion * Hot fix for WebUI tests * Fixed WebUI tests. Added new tests * #KEERO-220 Send X-Auth-Token to Conductor * Added initial unit tests for RestAPI service * Fixed all pep8 * Fixed automated tests for web UI * Fixed automated tests for WebUI. Added class Page with simple objects * Code to start\stop keero components using devstack functions * Fixed: changed the run mode for install venv script * Added deployment script for automated tests * All Cloudbase-Init plugins disabled except UserDataPlugin * Userdata script updated to support computer renaming functionality * Entry point script renamed 2d27f4f5054f34982ed67da2bf4b35c8ac1558d3 * Issues #195 and #199 * README and guide for conductor * Fix and unit test for issue: https://mirantis.jira.com/browse/KEERO-219 * Added unit tests for REST API client. Fixed pep8 * New devstack scripts added * Old devstack scripts removed * Write-Host replaced by Write-Log * Fixed typo * Sync * Sync * Sync * Cloned horizon and added our dashboard * Removed obsolete code Removed projects: [windc, windcclient] * Added tests for REST API. Fixed issues with Web UI * Added tests for REST API. Fixed issues with Web UI * Merged iteration3 branch to master. Fixed pep8 * Fixed small issues with UI * Added lst fixes for demo * Fix another issue with environments list * Fix another issue with services * Fix issue with getting list of environments * Added progress bars for services. Fixed few issues for demo * Fix issue with ack on results * Add part of service id to unit name * Add ability to get status for Environments and Sessions * Added password-secure checks for UI, fix usability issues for demo * ExecutionPlanGenerator DSL processor * Updated workflow elements to reflect new name changes and to fix typos * ExecutionPlanGenerator DSL processor * Updated workflow elements to reflect new name changes and to fix typos * Another Issue with sessions * Added progress bar to Web UI. Fixed pep8 errors * Change behaviour of viewing services * Issue with session * No ack is needed when auto\_ack set to True * Resolved issue with reports from orchestration engine * FIx issue with fields created & updated * Fixed issue with logging * Added deploy button for data centers in UI. Fixed templates for services * Queues should be durable * Add debug logging for controllers * Sync before tag * Fixed issue with empty services list * Added new API for Web UI * typos * Fixed length of names * Fixed instance namings * Added WebServer and AD * Workflows, ExecutionPlanGenerator, Reporting, UserData, conductor improvements * Removed obsolete file * Bug with Session * Added Session operations * Added Environments CRUD operations * Issue with deleting Environment * Removed obsolete files * Added initial version for python-portasclient * Issue with port for RabbitMQ * Function updated to return IPv4 addresses only * Typo * Explicit import of module DnsServer added * Function to install IIS added * Code to return DNS listening IPs from DC added * WebServer API Added WebServer API Small refactoring * Fix issues with queues * Added units name generation * Write results from orchestration engine * Active Directory API * Remove obsolete service table * Enable session deployment * Reports from orchestration engine Added ability to store and expose reports from orchestration engine * Cleaned up API * Added support for reading messages from RabbitMQ asynchronously * Typo * userdata.py fixed * Finished Task KEERO-111. Added base UI tests * Hot fix: Fixed pep8 for Dashboard * Finished Task: KEERO-117. Added new UI wizard for Create Services Action * Userdata plugin with minimal MIME support added * User data sample file added * Extra functions moved to NotCoreFunctions.ps1 file in order to remove them in the future * Functions to work with Base64 strings added * Functions to work with Zip files added * Modified files from cloudbase-init added * Fixed pep8. Fixed deployment script * Added support for session checking * Most part of Session API * Removed obsolete code * Added new Session model and migration Fixed issues with previous models * Initial conductor implementation * Added deployment script and automated tests * Small PEP8 fixes * Fixed small issues with parameters. It is required fix * Added remove method for environments Also slightly updated routes table * Finished environments api * Only environments from same tenant as users should be shown * Remove unnecessary blocks of code * When new DC is adding tenant\_id is added as param * Fix issues with context * Moved utils.py from WindDC * Small changes to .gitignore Removed global .gitignore Added .gitignore to WindowsAgent project * Update added files * Added support for keystone-auth * Updated initial version of portas-api * Initial version of portas-api * Simple function to update agent config added * Simple function for working with templates added * Function to retrieve meta data opject from config drive added * localrc updated * Files to automate devstack installation added * Fixed small issues with WebUI * asd * test.commit * test.commit * Log functions updated * Stop-Execution modified * Removed obsole line * Removed obsolete file Added .gitignore file * Added reference JSON for Active Directory * Fixed urls for dashboard * Fixed Web UI for demo * Files removed * Windows PowerShell module added * Unattended installation files added * Execution plan files added * windc iteration2 * Added WebUI for correct configuration of new service AD * Resolved issue with datacenter id * Resolved issue with datacenter id * Fixed many small issues * Fixed typo * Fixed KEERO-89 * Fixed issue with data centers * Added services functions to client. Need to be tested * [KEERO-83] Windows Agent: Ability to reboot machine after execution plan is executed * [KEERO-83] Windows Agent: Typo fixes + sample values in config * [KEERO-83] Windows Agent initial implementation * Added operations for chef. They might be remove if we decide to not use chef * Fixed small issues * Fixed KEERO-85 * Fixed issue with virtual environment SQLAlchemy library * Added library libsqlite3-dev to virtual environment for windc client * Added new functional to dashboard, fixed small issues * Added windc API client, sync repo with dev box * Added new files * Updated design. Removed extra code * 1. Added support of CloudFormation templates. Made a simple interface to build template. Stan can work here to redesign template.py 2. Added calls of drivers. Now heat is called from cmd instead of client. Should be rewritten. 3. ActiveDirectory makes a static template. Need to rewrite this with working with actual parameters * Added additional fields for Domain Controller * Added simple form for configuration Domen Controllers and IIS Servers * Fixed small problems with links and titles on pages * Fixed small problems with links and titles on pages * Added initial project for horizon dashboard * 1. Added builders support. Each builder is a class dynamically loaded from ./windc/core/builders folder. The class name should be the same as module file name. 2. Updated core/api.py to support datacenter and service creation with extra parameters which are not defined by model explicitly. 3. Added event based approach for the windows environment change. Now when user submits a request to API the core updates database and initiates a new event which defined scope (datacenter, service, VM) and action (add, modify, delete). This event and data will be iterated over all registered builders. Each builder can use this event and data to plan some modification * 1. Fixed issue with main file start ./bin/windc-api 2. Added router to Route /datacenters/ and /services/ URLs 3. Added stubs for windc/core/api. 4. Fixed start-up process for service ------------------------------------------------- Now it is working service which will reply for curl http://localhost:8181/tenant\_id/datacenters/ curl http://localhost:8181/tenant\_id/datacenters/dc\_id/services curl http://localhost:8181/tenant\_id/datacenters/dc\_id/services/service\_id * Initial version of the Windows DataCenter project. It is openstak-skeleton based * Unattended files added * Initial empty repository ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/HACKING.rst0000664000175000017500000000100700000000000014716 0ustar00zuulzuul00000000000000Style Commandments ================== Read the OpenStack Style Commandments https://docs.openstack.org/hacking/latest/ Murano Specific Commandments ---------------------------- - [M318] Change assertEqual(A, None) or assertEqual(None, A) by optimal assert like assertIsNone(A) - [M322] Method's default argument shouldn't be mutable. - [M323] Python 3: do not use dict.iteritems. - [M324] Python 3: do not use dict.iterkeys. - [M325] Python 3: do not use dict.itervalues. - [M326] Python 3: do not use basestring. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/LICENSE0000664000175000017500000002363700000000000014142 0ustar00zuulzuul00000000000000 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. ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1696417899.8851812 murano-16.0.0/PKG-INFO0000664000175000017500000000460000000000000014217 0ustar00zuulzuul00000000000000Metadata-Version: 1.2 Name: murano Version: 16.0.0 Summary: Murano API Home-page: https://www.openstack.org/software/releases/mitaka/components/murano Author: OpenStack Author-email: openstack-discuss@lists.openstack.org License: Apache License, Version 2.0 Description: ======================== Team and repository tags ======================== .. image:: https://governance.openstack.org/tc/badges/murano.svg :target: https://governance.openstack.org/tc/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. Project Resources ----------------- * `Murano Official Documentation `_ * Project status, bugs, and blueprints are tracked on `Launchpad `_ * Additional resources are linked from the project `Wiki `_ page * `Python client `_ License ------- Apache License Version 2.0 http://www.apache.org/licenses/LICENSE-2.0 Release Notes ------------- Release Notes may be found here: https://docs.openstack.org/releasenotes/murano Platform: UNKNOWN Classifier: Development Status :: 5 - Production/Stable Classifier: Environment :: OpenStack Classifier: Intended Audience :: Developers Classifier: Intended Audience :: Information Technology Classifier: License :: OSI Approved :: Apache Software License Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: Implementation :: CPython Classifier: Programming Language :: Python :: 3 :: Only Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.8 Requires-Python: >=3.8 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/README.rst0000664000175000017500000000226200000000000014613 0ustar00zuulzuul00000000000000======================== Team and repository tags ======================== .. image:: https://governance.openstack.org/tc/badges/murano.svg :target: https://governance.openstack.org/tc/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. Project Resources ----------------- * `Murano Official Documentation `_ * Project status, bugs, and blueprints are tracked on `Launchpad `_ * Additional resources are linked from the project `Wiki `_ page * `Python client `_ License ------- Apache License Version 2.0 http://www.apache.org/licenses/LICENSE-2.0 Release Notes ------------- Release Notes may be found here: https://docs.openstack.org/releasenotes/murano ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1696417899.6571803 murano-16.0.0/api-ref/0000775000175000017500000000000000000000000014445 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1696417899.6851804 murano-16.0.0/api-ref/source/0000775000175000017500000000000000000000000015745 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/api-ref/source/conf.py0000664000175000017500000001501600000000000017247 0ustar00zuulzuul00000000000000# -*- 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 documentation build configuration file, created by # sphinx-quickstart on Sat May 1 15:17:47 2010. # # 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 sys extensions = [ 'os_api_ref', 'openstackdocstheme' ] html_theme = 'openstackdocs' html_theme_options = { "sidebar_mode": "toc", } # openstackdocstheme options openstackdocs_repo_name = 'openstack/murano' openstackdocs_bug_project = 'murano' openstackdocs_bug_tag = 'api-ref' # 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('../../')) sys.path.insert(0, os.path.abspath('../')) sys.path.insert(0, os.path.abspath('./')) # -- General configuration ---------------------------------------------------- # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones. # 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. copyright = u'2016-present, OpenStack Foundation' # 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' # 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 = False # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'native' # -- Options for man page output ---------------------------------------------- # Grouping the document tree for man pages. # List of tuples 'sourcefile', 'target', u'title', u'Authors name', 'manual' # -- Options for HTML output -------------------------------------------------- # The theme to use for HTML and HTML Help pages. Major themes that come with # Sphinx are currently 'default' and 'sphinxdoc'. # html_theme_path = ["."] # html_theme = '_theme' # 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'] # 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 = 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, 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 = 'muranodoc' # -- 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', 'Murano.tex', u'OpenStack Application Catalog API Documentation', u'OpenStack Foundation', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of # the title page. # latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. # latex_use_parts = False # 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 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/api-ref/source/index.rst0000664000175000017500000000022400000000000017604 0ustar00zuulzuul00000000000000================================== OpenStack Application Catalog APIs ================================== .. toctree:: :maxdepth: 1 v1/index ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1696417899.6851804 murano-16.0.0/api-ref/source/v1/0000775000175000017500000000000000000000000016273 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/api-ref/source/v1/actions.inc0000664000175000017500000000502100000000000020424 0ustar00zuulzuul00000000000000.. -*- rst -*- ========================== Actions and Static Actions ========================== A Murano action is a type of MuranoPL method. The differences between a regular MuranoPL method are: * Action is executed on deployed objects. * Action execution is initiated by API request: you do not have to call the method manually. Thus, Murano actions allow performing any operations on objects, like: * Getting information from the VM, like a config that is generated during the deployment * VM rebooting * Scaling A list of available actions is formed during the environment deployment. Following deployment completion, you can call the action asynchronously. Murano engine generates a task for every action thereby allowing the action status to be tracked. Execute action ============== .. rest_method:: POST /environments/{environment_id}/actions/{action_id} Execute action on deployed environment. Request Parameters ------------------- .. rest_parameters:: parameters.yaml - environment_id: env_id_url - action_id: action_id_url Response Codes -------------- .. rest_status_code:: success status.yaml - 200 .. rest_status_code:: error status.yaml - 401 - 403 - 404 Response Parameters ------------------- .. rest_parameters:: parameters.yaml - X-Openstack-Request-Id: request_id - task_id: task_id Response Example ---------------- .. literalinclude:: samples/execute-action-response.json :language: javascript Get Action Result ================= .. rest_method:: GET /environments/{environment_id}/actions/{task_id} Retrieve action result for action executed on deployed environment. Request Parameters ------------------- .. rest_parameters:: parameters.yaml - environment_id: env_id_url - task_id: task_id_url Response Codes -------------- .. rest_status_code:: success status.yaml - 200 .. rest_status_code:: error status.yaml - 401 - 403 - 404 Execute static action ===================== .. rest_method:: POST /actions Execute static action. Static methods can be called if they are exposed by specifying Scope: Public in the MuranoPL object and the result of its execution will be returned. Request Example --------------- .. literalinclude:: samples/static-action-request.json :language: javascript Response Codes -------------- .. rest_status_code:: success status.yaml - 200 .. rest_status_code:: error status.yaml - 401 - 403 - 404 Response Example ---------------- .. literalinclude:: samples/static-action-response.json :language: javascript ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/api-ref/source/v1/categories.inc0000664000175000017500000000546300000000000021123 0ustar00zuulzuul00000000000000.. -*- rst -*- ========== Categories ========== In Murano, applications can belong to a category or multiple categories. Administrative users can create and delete categories as well as list available categories and view details for a particular category. List categories =============== .. rest_method:: GET /catalog/categories Retrieve list of all available categories in the Application Catalog. Response Codes -------------- .. rest_status_code:: success status.yaml - 200 .. rest_status_code:: error status.yaml - 401 Response Parameters ------------------- .. rest_parameters:: parameters.yaml - X-Openstack-Request-Id: request_id - categories: all_categories - id: category_id - name: category_name - updated: updated - created: created - package_count: package_count Response Example ---------------- .. literalinclude:: samples/category-list-response.json :language: javascript Show category details ===================== .. rest_method:: GET /catalog/categories/{category_id} Show details for a category. Request Parameters ------------------ .. rest_parameters:: parameters.yaml - category_id: category_id_url Response Codes -------------- .. rest_status_code:: success status.yaml - 200 .. rest_status_code:: error status.yaml - 401 - 404 Response Parameters ------------------- .. rest_parameters:: parameters.yaml - X-Openstack-Request-Id: request_id - id: category_id - name: category_name - updated: updated - created: created - packages: category_packages - package_count: package_count Response Example ---------------- .. literalinclude:: samples/category-show-response.json :language: javascript Create Category =============== .. rest_method:: POST /catalog/categories Add a new category to the Application Catalog. Response Codes -------------- .. rest_status_code:: success status.yaml - 200 .. rest_status_code:: error status.yaml - 401 - 409 Request Parameters ------------------ .. rest_parameters:: parameters.yaml - name: category_name Response Parameters ------------------- .. rest_parameters:: parameters.yaml - X-Openstack-Request-Id: request_id - id: category_id - name: category_name - updated: updated - created: created - package_count: package_count Response Example ---------------- .. literalinclude:: samples/category-create-response.json :language: javascript Delete Category =============== .. rest_method:: DELETE /catalog/categories/{category_id} Remove an existing category from the Application Catalog. Request Parameters ------------------ .. rest_parameters:: parameters.yaml - category_id: category_id_url Response Codes -------------- .. rest_status_code:: success status.yaml - 200 .. rest_status_code:: error status.yaml - 401 - 403 - 404 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/api-ref/source/v1/deployments.inc0000664000175000017500000000262600000000000021337 0ustar00zuulzuul00000000000000.. -*- rst -*- =========== Deployments =========== Deployments track environments that have been deployed, either successfully or otherwise. Each deployment contains the following information: * A "Class: Environment" object (io.murano.Environment) with a name. Each "Class: Environment" object defines an environment in terms of the deployment process and groups all Applications and their related infrastructures together. * An object (or objects) referring to networks that exist. * A list of Applications (e.g. io.murano.apps.linux.Telnet). Each Application contains, or otherwise references, anything it requires. The Telnet example has a property called ``instance`` whose contract states it must be of type ``io.murano.resources.Instance``. In turn, the Instance has properties it requires (like a ``name``, a ``flavor``, or a keypair name, ``keyname``). List deployments ================ .. rest_method:: GET /deployments List deployments for all environments for the current tenant (project). Response Codes -------------- .. rest_status_code:: success status.yaml - 200 .. rest_status_code:: error status.yaml - 401 Response Parameters ------------------- .. rest_parameters:: parameters.yaml - X-Openstack-Request-Id: request_id - deployments: deployments Response Example ---------------- .. literalinclude:: samples/deployments-list-response.json :language: javascript ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/api-ref/source/v1/environments.inc0000664000175000017500000001605100000000000021520 0ustar00zuulzuul00000000000000.. -*- rst -*- ============ Environments ============ An environment is a set of logically connected applications that are grouped together for easy management. By default, each environment has a single network for all its applications, and the deployment of the environment is defined in a single heat stack. Applications in different environments are always independent from one another. An environment is a single unit of deployment. This means that you can not only deploy an environment that contains a single application but an environment that contains multiple applications. List environments ================= .. rest_method:: GET /environments Get a list of existing Environments Response Codes -------------- .. rest_status_code:: success status.yaml - 200 .. rest_status_code:: error status.yaml - 401 - 403 Request Parameters ------------------ .. rest_parameters:: parameters.yaml - all_tenants: all_tenants - tenant: tenant Response Parameters ------------------- .. rest_parameters:: parameters.yaml - X-Openstack-Request-Id: request_id - environments: environments - status: env_status - created: created - updated: updated - name: env_name - description_text: env_description - tenant_id: tenant_id - version: env_version - id: env_id Response Example ---------------- .. literalinclude:: samples/environments-list-response.json :language: javascript Create environment ================== .. rest_method:: POST /environments Creates an environment. Response Codes -------------- .. rest_status_code:: success status.yaml - 200 .. rest_status_code:: error status.yaml - 400 - 401 - 403 - 409 Request Parameters ------------------ .. rest_parameters:: parameters.yaml - name: env_name_request Request Example --------------- .. literalinclude:: samples/environment-create-request.json :language: javascript Response Parameters ------------------- .. rest_parameters:: parameters.yaml - X-Openstack-Request-Id: request_id - id: env_id - name: env_name - description_text: env_description - created: created - updated: updated - tenant_id: tenant_id - version: env_version - services: services - acquired_by: acquired_by Response Example ---------------- .. literalinclude:: samples/environment-create-response.json :language: javascript Rename environment ================== .. rest_method:: PUT /environments/{env_id} Renames an environment. Response Codes -------------- .. rest_status_code:: success status.yaml - 200 .. rest_status_code:: error status.yaml - 400 - 401 - 403 - 404 - 409 Request Parameters ------------------ .. rest_parameters:: parameters.yaml - env_id: env_id_url - name: env_name_update Request Example --------------- .. literalinclude:: samples/environment-update-request.json :language: javascript Response Parameters ------------------- .. rest_parameters:: parameters.yaml - X-Openstack-Request-Id: request_id - id: env_id - name: env_name - description_text: env_description - created: created - updated: updated - tenant_id: tenant_id - version: env_version - services: services - acquired_by: acquired_by Response Example ---------------- .. literalinclude:: samples/environment-update-response.json :language: javascript Show environment details ======================== .. rest_method:: GET /environments/{env_id} Shows details for an environment. Response Codes -------------- .. rest_status_code:: success status.yaml - 200 .. rest_status_code:: error status.yaml - 401 - 403 - 404 Request Parameters ------------------ .. rest_parameters:: parameters.yaml - env_id: env_id_url Response Parameters ------------------- .. rest_parameters:: parameters.yaml - X-Openstack-Request-Id: request_id - id: env_id - name: env_name - description_text: env_description - created: created - updated: updated - tenant_id: tenant_id - version: env_version - services: services - acquired_by: acquired_by Response Example ---------------- .. literalinclude:: samples/environment-show-response.json :language: javascript Delete environment ================== .. rest_method:: DELETE /environments/{env_id} Remove specified Environment. Response Codes -------------- .. rest_status_code:: success status.yaml - 200 .. rest_status_code:: error status.yaml - 401 - 403 - 404 Request Parameters ------------------ .. rest_parameters:: parameters.yaml - env_id: env_id_url - abandon: abandon Response Parameters ------------------- This request does not return anything in the response body. Get environment model ===================== .. rest_method:: GET /environments/{env_id}/model/{path} Get an Environment model. Response Codes -------------- .. rest_status_code:: success status.yaml - 200 .. rest_status_code:: error status.yaml - 401 - 403 - 404 Request Parameters ------------------ .. rest_parameters:: parameters.yaml - env_id: env_id_url - path: env_model_path Response Parameters ------------------- .. rest_parameters:: parameters.yaml - X-Openstack-Request-Id: request_id - defaultNetworks: env_default_networks - region: env_region - regions: regions - name: env_name - services: services - ?: env_model Response Example ---------------- .. literalinclude:: samples/environments-model-response.json :language: javascript Update environment model ======================== .. rest_method:: PATCH /environments/{env_id}/model/ Update an environment model. Response Codes -------------- .. rest_status_code:: success status.yaml - 202 .. rest_status_code:: error status.yaml - 400 - 401 - 403 - 404 - 409 Request Parameters ------------------ .. rest_parameters:: parameters.yaml - env_id: env_id_url Request Example --------------- .. literalinclude:: samples/environment-model-update-request.json :language: javascript Response Parameters ------------------- .. rest_parameters:: parameters.yaml - X-Openstack-Request-Id: request_id - defaultNetworks: env_default_networks - region: env_region - regions: regions - name: env_name - services: services - ?: env_model Response Example ---------------- .. literalinclude:: samples/environments-model-response.json :language: javascript Get environment last status =========================== .. rest_method:: GET /environments/{env_id}/lastStatus Get the last status for the environment for each service in the environment. Response Codes -------------- .. rest_status_code:: success status.yaml - 200 .. rest_status_code:: error status.yaml - 400 - 401 - 403 - 404 Request Parameters ------------------ .. rest_parameters:: parameters.yaml - env_id: env_id_url Response Parameters ------------------- .. rest_parameters:: parameters.yaml - X-Openstack-Request-Id: request_id - lastStatuses: env_last_status Response Example ---------------- .. literalinclude:: samples/environment-last-status-response.json :language: javascript ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/api-ref/source/v1/index.rst0000664000175000017500000000052300000000000020134 0ustar00zuulzuul00000000000000:tocdepth: 2 #################################### OpenStack Application Catalog API v1 #################################### .. rest_expand_all:: .. include:: actions.inc .. include:: categories.inc .. include:: deployments.inc .. include:: environments.inc .. include:: packages.inc .. include:: sessions.inc .. include:: templates.inc ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/api-ref/source/v1/packages.inc0000664000175000017500000002266600000000000020560 0ustar00zuulzuul00000000000000.. -*- rst -*- ======== Packages ======== In Murano, each application, as well as the UI form for application data entry, is defined by packages. Package Structure ================= The structure of the Murano application package is predefined. The application package root folder should contain the following: * ``manifest.yaml`` file is the application entry point. .. note:: The filename is fixed, so do not use any custom names. * ``Classes`` folder contains MuranoPL class definitions. * ``Resources`` folder contaisn execution plan templates as well as the ``scripts`` folder with all the files required for an application deployment located inside it. * ``UI`` folder contains the dynamic UI YAML definitions. * ``logo.png`` file (optional) is an image file associated with your application. The logo appears in the Application Catalog within Murano Dasboard. .. note:: There are no special limitations regarding an image filename. However, if it differs from the default ``logo.png``, specify it in an application manifest file. * ``images.lst`` file (optional) contains a list of images required by an application. .. note:: A bundle is a collection of packages. In the Community App Catalog, you can find such bundles as ``container-based-apps``, ``app-servers``, and so on. The packages in the Application Catalog are sorted by usage. List Packages ============= .. rest_method:: GET /v1/catalog/packages Get a list of packages Response Codes -------------- .. rest_status_code:: success status.yaml - 200 .. rest_status_code:: error status.yaml - 400 - 401 - 403 Request Parameters ------------------ .. rest_parameters:: parameters.yaml - catalog: catalog - marker: marker - limit: limit - order_by: order_by - type: pkg_type_query - category: category - fqn: fqn - owned: owned - id: pkg_id_query - include_disabled: include_disabled - search: search - class_name: class_name - name: pkg_name_query Response Parameters ------------------- .. rest_parameters:: parameters.yaml - X-Openstack-Request-Id: request_id - packages: packages - updated: updated - class_definitions: class_definitions - id: pkg_id - fully_qualified_name: fully_qualified_name - is_public: is_public - name: pkg_name - type: pkg_type - supplier: pkg_supplier - description: description - author: author - created: created - enabled: enabled - tags: tags - categories: package_categories - owner_id: owner_id Response Example ---------------- .. literalinclude:: samples/packages-list-response.json :language: javascript Upload package ============== .. rest_method:: POST /v1/catalog/packages Upload a package to the application catalog. .. note:: Though specifying categories is optional, it is recommended that you specify at least one. It helps to filter applications in the catalog. Response Codes -------------- .. rest_status_code:: success status.yaml - 200 .. rest_status_code:: error status.yaml - 400 - 401 - 403 - 409 Request Parameters ------------------ .. rest_parameters:: parameters.yaml - categories: package_categories - is_public: is_public - file: pkg_file Response Parameters ------------------- .. rest_parameters:: parameters.yaml - X-Openstack-Request-Id: request_id - updated: updated - class_definitions: class_definitions - id: pkg_id - fully_qualified_name: fully_qualified_name - is_public: is_public - name: pkg_name - type: pkg_type - supplier: pkg_supplier - description: description - author: author - created: created - enabled: enabled - tags: tags - categories: package_categories - owner_id: owner_id Response Example ---------------- .. literalinclude:: samples/package-create-response.json :language: javascript Download package ================ .. rest_method:: GET /v1/catalog/packages/{package_id}/download Download a package. Response Codes -------------- .. rest_status_code:: success status.yaml - 200 .. rest_status_code:: error status.yaml - 400 - 401 - 403 Request Parameters ------------------ .. rest_parameters:: parameters.yaml - package_id: pkg_id_url Response Parameters ------------------- This request does not return anything in the response body. :language: javascript Show package details ==================== .. rest_method:: GET /v1/catalog/packages/{package_id} Shows details for a package. Response Codes -------------- .. rest_status_code:: success status.yaml - 200 .. rest_status_code:: error status.yaml - 400 - 401 - 403 - 404 Request Parameters ------------------ .. rest_parameters:: parameters.yaml - package_id: pkg_id_url Response Parameters ------------------- .. rest_parameters:: parameters.yaml - X-Openstack-Request-Id: request_id - updated: updated - class_definitions: class_definitions - id: pkg_id - fully_qualified_name: fully_qualified_name - is_public: is_public - name: pkg_name - type: pkg_type - supplier: pkg_supplier - description: description - author: author - created: created - enabled: enabled - tags: tags - categories: package_categories - owner_id: owner_id Response Example ---------------- .. literalinclude:: samples/package-show-response.json :language: javascript Update package ============== .. rest_method:: PATCH /v1/catalog/packages/{package_id} Update a package. List of allowed changes:: { "op": "add", "path": "/tags", "value": [ "foo", "bar" ] } { "op": "add", "path": "/categories", "value": [ "foo", "bar" ] } { "op": "remove", "path": "/tags" } { "op": "remove", "path": "/categories" } { "op": "replace", "path": "/tags", "value": ["foo", "bar"] } { "op": "replace", "path": "/is_public", "value": true } { "op": "replace", "path": "/description", "value":"New description" } { "op": "replace", "path": "/name", "value": "New name" } Response Codes -------------- .. rest_status_code:: success status.yaml - 202 .. rest_status_code:: error status.yaml - 400 - 403 - 404 - 409 Request Parameters ------------------ .. rest_parameters:: parameters.yaml - package_id: pkg_id_url Request Example --------------- .. literalinclude:: samples/package-update-request.json :language: javascript Response Parameters ------------------- .. rest_parameters:: parameters.yaml - X-Openstack-Request-Id: request_id - updated: updated - class_definitions: class_definitions - id: pkg_id - fully_qualified_name: fully_qualified_name - is_public: is_public - name: pkg_name - type: pkg_type - supplier: pkg_supplier - description: description - author: author - created: created - enabled: enabled - tags: tags - categories: package_categories - owner_id: owner_id Response Example ---------------- .. literalinclude:: samples/package-update-response.json Delete package ============== .. rest_method:: DELETE /v1/catalog/packages/{package_id} Remove specified Environment. Response Codes -------------- .. rest_status_code:: success status.yaml - 200 .. rest_status_code:: error status.yaml - 401 - 403 - 404 Request Parameters ------------------ .. rest_parameters:: parameters.yaml - package_id: pkg_id_url Response Parameters ------------------- This request does not return anything in the response body. :language: javascript Search for packages =================== .. rest_method:: GET /v1/catalog/packages Search for packages in application catalog. Non-admins, by default, can view packages that belong to their project as well as public packages: packages which belong to other projects but which have been tagged as public by an admin. Admins can search for packages across all projects. Response Codes -------------- .. rest_status_code:: success status.yaml - 200 .. rest_status_code:: error status.yaml - 400 - 401 - 403 Request Parameters ------------------ .. rest_parameters:: parameters.yaml - filters: pkg_filters Response Parameters ------------------- Returns the list of packages matching the search criteria. Get UI definition ================= .. rest_method:: GET /v1/catalog/packages/{package_id}/ui Retrieve UI definition for an application. Response Codes -------------- .. rest_status_code:: success status.yaml - 200 .. rest_status_code:: error status.yaml - 400 - 401 - 403 - 404 Request Parameters ------------------ .. rest_parameters:: parameters.yaml - package_id: pkg_id_url Response Parameters ------------------- Returns the entire UI definition for the package, if the logo has a UI definition. Below is an example of a very basic UI definition:: Version: 2.2 Forms: - appConfiguration: fields: - name: license type: string description: Apache License, Version 2.0 hidden: false required: false Get logo ======== .. rest_method:: GET /v1/catalog/packages/{package_id}/logo Retrieve application logo. Response Codes -------------- .. rest_status_code:: success status.yaml - 200 .. rest_status_code:: error status.yaml - 400 - 401 - 403 - 404 Request Parameters ------------------ .. rest_parameters:: parameters.yaml - package_id: pkg_id_url Response Parameters ------------------- Returns the binary logo data for the package, if the package has a logo. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/api-ref/source/v1/parameters.yaml0000664000175000017500000004065000000000000021327 0ustar00zuulzuul00000000000000# variables in header request_id: description: | A unique ID for tracking service request. The request ID associated with the request by default appears in the service logs. in: header required: true type: UUID # variables in path abandon: description: | Indicates how to delete environment. ``True`` is used when just database must be cleaned. ``False`` is used if all resources used by environment must be destroyed. in: path required: false default: false type: boolean action_id_url: description: | The UUID of the action to be executed on the deployed environment. in: path required: true type: string category_id_url: description: | The UUID of the category. in: path required: true type: string env_id_url: description: | The UUID of the environment. in: path required: true type: string env_model_path: description: | Allows to get a specific section of the model, for example ``defaultNetworks``, ``region`` or ``?`` or any of the subsections. in: path required: false type: string env_name_update: description: | A name for the environment. Name must be at least one non-white space symbol. in: path required: true type: string pkg_id_url: description: | The UUID of the package. in: path required: true type: string service_id_url: description: The UUID of a service belonging to an environment template. in: path required: true type: string session_id_url: description: | The UUID of the session. in: path required: true type: string task_id_url: description: | The UUID of the task associated with an action executed on a deployed environment. in: path required: true type: string template_id_url: description: | The UUID of the environment template. in: path required: true type: string template_is_public_url: description: | Indicates whether public environment templates are listed or not. The following options are possible: - ``True``. Public environments templates from all projects are listed. - ``False``. Private environments templates from current project are listed. - ``empty``. All project templates plus public templates from all projects. are listed in: path required: false default: false type: boolean # variables in query all_tenants: description: | Indicates whether environments from all projects are listed. ``True`` environments from all projects are listed. Admin user required. ``False`` environments only from current project are listed (default like option unspecified). in: query required: false default: false type: boolean catalog: description: | If ``false`` (default) - search packages, that current user can edit (own for non-admin, all for admin). If ``true`` - search packages, that current user can deploy (i.e. his own + public). in: query required: false default: false type: boolean category: description: | Allows to filter by categories. in: query required: false type: string class_name: description: | Search only for packages, that use specified class. in: query required: false type: string fqn: description: | Allows to filter by fully qualified name. in: query required: false type: string include_disabled: description: | Include disabled packages in the result. in: query required: false default: false type: boolean limit: description: | When present the maximum number of results returned will not exceed the specified value. The typical pattern of limit and marker is to make an initial limited request and then to use the ID of the last package from the response as the marker parameter in a subsequent limited request. in: query required: false type: string marker: description: | A package identifier marker may be specified. When present only packages which occur after the identifier ID will be listed in: query required: false type: string order_by: description: | Allows to sort packages by ``fqn``, ``name``, ``created``. Created is default value. in: query required: false type: string owned: description: | Search only from packages owned by current project. in: query required: false default: false type: boolean pkg_filters: description: | The filters that you want to use to search for packages in the application catalog. If no filters query parameter is specified, the application catalog API returns all packages allowed by the policy settings. By using filters parameter, the API returns only the requested set of packages that meet the filters. The list of filters includes: * limit: the maximum number of packages to return * type: the package type * id: the package id * category: the package category * tag: the package tag * class_name: the package class name * fqn: the package fully qualified name * name: the package name in: query required: false type: string pkg_id_query: description: | Allows to filter by package id. in: query required: false type: string pkg_name_query: description: | Allows to filter by package name. in: query required: false type: string pkg_type_query: description: | Allows to filter package by type, e.g. ``application``, ``library``. in: query required: false type: string search: description: | Gives opportunity to search specified data by all the package parameters and order packages. in: query required: false type: string tenant: description: | Indicates environments from specified tenant are listed. Admin user required. in: query required: false type: string # variables in body acquired_by: description: | The session that is currently `deploying` the environment. Returns the `first` session id that is in ``DEPLOYING`` state for the environment. in: body required: true type: string all_categories: description: | All categories available in the application catalog. in: body required: true type: array author: description: | The author of the package. in: body required: true type: string category_id: description: | The UUID of the category. in: body required: true type: string category_name: description: | The name of the category. in: body required: true type: string category_packages: description: | The list of packages associated with a package. Each package returned includes its ``id``, ``fully_qualified_name``, and ``name``. in: body required: true type: array class_definitions: description: | The class_definitions of the package. in: body required: true type: array created: description: | The date and time when the resource was created. The date and time stamp format is `ISO 8601 `_: :: CCYY-MM-DDThh:mm:ss±hh:mm For example, ``2015-08-27T09:49:58-05:00``. The ``±hh:mm`` value, if included, is the time zone as an offset from UTC. in: body required: true type: string deployments: description: | The list of deployments for either the current environment or all environments for the current tenant (project). The following APIs control whether deployments by environment or by project are returned: * ``/deployments``: Returns all deployments for a project. * ``/environments/{env_id}/deployments``: Returns all deployments for an environment in a project. in: body required: true type: array description: description: | The description of the package. in: body required: true type: string enabled: description: | Whether the package is browsed in the Application Catalog. in: body required: true type: boolean env_default_networks: description: | The default networking information of the environment. The information includes the ``name`` of the network, along with the ``type`` and ``id`` of the network, contained in the ``?`` property. An example ``defaultNetworks`` object looks like:: "defaultNetworks": { "environment": { "internalNetworkName": "net_two", "?": { "type": "io.murano.resources.ExistingNeutronNetwork", "id": "594e94fcfe4c48ef8f9b55edb3b9f177" } }, "flat": null } in: body required: true type: object env_description: description: | The description of the environment. in: body required: true type: string env_id: description: | The UUID of the environment. in: body required: true type: string env_last_status: description: | Shows the most recent status of the environment for each service in the environment. The response object includes detailed information by ``service_id``. in: body required: true type: object env_model: description: | The ``?`` section of the environment, containing information about the environment model, including its ``type``, ``id`` and associated ``metadata``. in: body required: true type: object env_name: description: | A name for the environment. Name must be at least one non-white space symbol and less than 256 characters long. in: body required: true type: string env_name_request: description: | A name for the environment. Name must be at least one non-white space symbol. in: body required: true type: string env_region: description: | Current region of the environment. in: body required: true type: string env_status: description: | Current status of the environment. The available statuses are: * **Ready to configure**. When the environment is new and contains no components. * **Ready to deploy**. When the environment contains a component or multiple components and is ready for deployment. * **Ready**. When the environment has been successfully deployed. * **Deploying**. When the deploying is in progress. * **Deploy FAILURE**. When the deployment finished with errors. * **Deleting**. When deleting of an environment is in progress. * **Delete FAILURE**. You can abandon the environment in this case. in: body required: true type: string env_version: description: | Current version. in: body required: true type: int environments: description: | A list of ``environment`` object. in: body required: true type: array fully_qualified_name: description: | The fqn of the package. in: body required: true type: string is_public: description: | Whether the package is shared for other projects. in: body required: true type: boolean networking: description: | Current network of the environment. in: body required: true type: string owner_id: description: | The owner id of the package. in: body required: true type: string package_categories: description: | The categories associated with the package. in: body required: true type: array package_count: description: | The number of packages associated with the category. in: body required: true type: integer packages: description: | A list of ``package`` object. in: body required: true type: array pkg_file: description: | The upload package file. in: body required: true type: object pkg_id: description: | The UUID of the package. in: body required: true type: string pkg_name: description: | The name of the package. in: body required: true type: string pkg_supplier: description: | The supplier info of the package. in: body required: true type: object pkg_type: description: | The type of the package. in: body required: true type: string regions: description: | Detailed region information for the cloud environment. in: body required: true type: object services: description: | A list of ``service`` objects. in: body required: true type: array session_id: description: | The UUID of the session. in: body required: true type: string session_state: description: | The current state of the environment. When a session is first opened for the environment the state is ``opened``. in: body required: true type: string session_user_id: description: | The UUID of the session owner. in: body required: true type: string session_version: description: | The version of the session. It is tied to the version of the environment, so that only sessions whose version matches that of the environment can be deployed. in: body required: true type: integer tags: description: | The tags of the package. in: body required: true type: array task_id: description: | The UUID of the task associated with an action executed on a deployed environment. in: body required: true type: string template_description: description: | The environment template description. in: body required: true type: string template_id: description: | The UUID of the environment template. in: body required: true type: string template_is_public: description: | Indicates whether an environment template is public or not. - ``True``. The environment template is public. Can be cloned. - ``False``. The environment template is private. in: body required: true type: boolean template_name: description: | The name of the environment template. Only alphanumeric characters are allowed. in: body required: true type: string template_service: description: | Detailed information about the ``service`` to be added to the environment template. The ``service`` includes virtual resources and application information. The virtual resources information is specified inside the ``instance`` object property. Application information is specified inside the body of the ``service`` object. The ``instance`` object properties include: - ``assignFloatingIp``. Whether to assign a floating IP to the VM. - ``keyname``. The key name of a key pair for the VM. - ``image``. The image to be used to provision the VM. - ``flavor``. The flavor to be used to provision the VM. - ``?``. An object which includes the ``type`` of the server. An example ``instance`` looks like:: { "assignFloatingIp": "true", "keyname": "mykeyname", "image": "cloud-fedora-v3", "flavor": "m1.medium", "?": { "type": "io.murano.resources.LinuxMuranoInstance", "id": "ef984a74-29a4-45c0-b1dc-2ab9f075732e" } } In addition, the ``service`` should also include the following: - ``name``. The ``name`` of the application. - ``?``. An object that includes the ``type`` and ``id`` of the application. An example ``type`` is: "io.murano.resources.LinuxMuranoInstance". - ``port``: The port to be used by the application. The value must be greater than 0 and less than 65536 (although formatted as a string). The entire ``service`` looks like:: { "instance": { "assignFloatingIp": "true", "keyname": "mykeyname", "image": "cloud-fedora-v3", "flavor": "m1.medium", "?": { "type": "io.murano.resources.LinuxMuranoInstance", "id": "ef984a74-29a4-45c0-b1dc-2ab9f075732e" } }, "name": "orion", "?": { "type": "io.murano.apps.apache.Tomcat", "id": "54cea43d-5970-4c73-b9ac-fea656f3c722" }, "port": "8080" } in: body required: true type: object template_services: description: | The list of environment template ``service`` objects. in: body required: true type: array template_version: description: | The current version of the environment template. in: body required: true type: integer templates: description: | The list of templates. in: body required: true type: array tenant_id: description: | The UUID of the tenant. A tenant is also known as a project. in: body required: true type: string updated: description: | The date and time when the object was updated. The date and time stamp format is `ISO 8601 `_: :: CCYY-MM-DDThh:mm:ss±hh:mm For example, ``2015-08-27T09:49:58-05:00``. The ``±hh:mm`` value, if included, is the time zone as an offset from UTC. in: body required: true type: string ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1696417899.6971805 murano-16.0.0/api-ref/source/v1/samples/0000775000175000017500000000000000000000000017737 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/api-ref/source/v1/samples/category-create-response.json0000664000175000017500000000026200000000000025544 0ustar00zuulzuul00000000000000{ "id": "ce373a477f211e187a55404a662f968", "name": "category_name", "created": "2013-11-30T03:23:42Z", "updated": "2013-11-30T03:23:44Z", "package_count": 0 }././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/api-ref/source/v1/samples/category-list-response.json0000664000175000017500000000074400000000000025261 0ustar00zuulzuul00000000000000{ "categories": [ { "id": "0420045dce7445fabae7e5e61fff9e2f", "updated": "2014-12-26T13:57:04", "name": "Web", "created": "2014-12-26T13:57:04", "package_count": 1 }, { "id": "3dd486b1e26f40ac8f35416b63f52042", "updated": "2014-12-26T13:57:04", "name": "Databases", "created": "2014-12-26T13:57:04", "package_count": 0 } ] }././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/api-ref/source/v1/samples/category-show-response.json0000664000175000017500000000056100000000000025263 0ustar00zuulzuul00000000000000{ "id": "b308f7fa8a2f4a5eb419970c827f4466", "updated": "2015-01-28T17:00:19", "packages": [ { "fully_qualified_name": "io.murano.apps.ZabbixServer", "id": "4dfb566e69e6445fbd4aea5099fe95e9", "name": "Zabbix Server" } ], "name": "Web", "created": "2015-01-28T17:00:19", "package_count": 1 }././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/api-ref/source/v1/samples/deployments-list-response.json0000664000175000017500000000360700000000000026010 0ustar00zuulzuul00000000000000{ "deployments": [ { "updated": "2014-05-15T07:24:21", "environment_id": "744e44812da84e858946f5d817de4f72", "description": { "services": [ { "instance": { "flavor": "m1.medium", "image": "cloud-fedora-v3", "?": { "type": "io.murano.resources.Instance", "id": "ef729199-c71e-4a4c-a314-0340e279add8" }, "name": "xkaduhv7qeg4m7" }, "name": "teslnet1", "?": { "_26411a1861294160833743e45d0eaad9": { "name": "Telnet" }, "type": "io.murano.apps.linux.Telnet", "id": "6e437be2-b5bc-4263-8814-6fd57d6ddbd5" } } ], "defaultNetworks": { "environment": { "name": "test2-network", "?": { "type": "io.murano.lib.networks.neutron.NewNetwork", "id": "b6a1d515434047d5b4678a803646d556" } }, "flat": null }, "name": "test2", "?": { "type": "io.murano.Environment", "id": "744e44812da84e858946f5d817de4f72" } }, "created": "2014-05-15T07:24:21", "started": "2014-05-15T07:24:21", "finished": null, "state": "running", "id": "327c81e0e34a4c93ad9b9052ef42b752" } ] }././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/api-ref/source/v1/samples/environment-create-request.json0000664000175000017500000000002500000000000026122 0ustar00zuulzuul00000000000000{"name": "env_name"} ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/api-ref/source/v1/samples/environment-create-response.json0000664000175000017500000000044700000000000026300 0ustar00zuulzuul00000000000000{ "status": "ready", "updated": "2017-04-27T15:36:02", "created": "2017-04-27T15:36:02", "tenant_id": "cca37eef752244d99945a4123f30ff79", "acquired_by": null, "services": [], "version": 0, "description_text": "", "id": "a2977db57398401aba5804ef2211a2a3", "name": "env_name" }././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/api-ref/source/v1/samples/environment-last-status-response.json0000664000175000017500000000107700000000000027321 0ustar00zuulzuul00000000000000{ "lastStatuses": { "66563e45-4d0a-451e-8138-7bc773b0607d": { "updated": "2017-03-09T07:31:51", "task_id": "1267d8dfcf2144f9a31f0f033defa0fd", "level": "info", "text": "Unable to install ApacheHttpServer on node-1 due to The murano-agent did not respond within 3600 seconds", "created": "2017-03-09T07:31:51", "entity_id": "66563e45-4d0a-451e-8138-7bc773b0607d", "entity": null, "details": null, "id": "4f93ae1f73294bf1a58cbc59fffe6238" } } } ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/api-ref/source/v1/samples/environment-model-update-request.json0000664000175000017500000000011400000000000027236 0ustar00zuulzuul00000000000000[{ "op": "replace", "path": "/defaultNetworks/flat", "value": true }] ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/api-ref/source/v1/samples/environment-show-response.json0000664000175000017500000000171200000000000026011 0ustar00zuulzuul00000000000000{ "status": "ready", "updated": "2017-04-27T15:36:02", "created": "2017-04-27T15:36:02", "tenant_id": "cca37eef752244d99945a4123f30ff79", "acquired_by": null, "services": [ { "instance": { "flavor": "m1.medium", "image": "cloud-fedora-v3", "name": "exgchhv6nbika2", "ipAddresses": [ "10.0.0.200" ], "?": { "type": "io.murano.resources.Instance", "id": "14cce9d9-aaa1-4f09-84a9-c4bb859edaff" } }, "name": "rewt4w56", "?": { "status": "ready", "_26411a1861294160833743e45d0eaad9": { "name": "Telnet" }, "type": "io.murano.apps.linux.Telnet", "id": "446373ef-03b5-4925-b095-6c56568fa518" } } ], "version": 0, "description_text": "", "id": "a2977db57398401aba5804ef2211a2a3", "name": "env_name" }././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/api-ref/source/v1/samples/environment-update-request.json0000664000175000017500000000003500000000000026142 0ustar00zuulzuul00000000000000{"name": "env_name_changed"} ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/api-ref/source/v1/samples/environment-update-response.json0000664000175000017500000000045700000000000026320 0ustar00zuulzuul00000000000000{ "status": "ready", "updated": "2017-04-27T16:01:29", "created": "2017-04-27T15:33:55", "tenant_id": "cca37eef752244d99945a4123f30ff79", "acquired_by": null, "services": [], "version": 0, "description_text": "", "id": "f199275420ff4e938e0307b0cf68374d", "name": "env_name_changed" }././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/api-ref/source/v1/samples/environments-list-response.json0000664000175000017500000000131500000000000026166 0ustar00zuulzuul00000000000000{ "environments": [ { "status": "ready", "updated": "2014-05-14T13:02:54", "networking": {}, "name": "test1", "created": "2014-05-14T13:02:46", "tenant_id": "726ed856965f43cc8e565bc991fa76c3", "version": 0, "id": "2fa5ab704749444bbeafe7991b412c33" }, { "status": "ready", "updated": "2014-05-14T13:02:55", "networking": {}, "name": "test2", "created": "2014-05-14T13:02:51", "tenant_id": "726ed856965f43cc8e565bc991fa76c3", "version": 0, "id": "744e44812da84e858946f5d817de4f72" } ] } ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/api-ref/source/v1/samples/environments-model-response.json0000664000175000017500000000645600000000000026326 0ustar00zuulzuul00000000000000{ "defaultNetworks": { "environment": { "internalNetworkName": "net_two", "?": { "type": "io.murano.resources.ExistingNeutronNetwork", "id": "594e94fcfe4c48ef8f9b55edb3b9f177" } }, "flat": null }, "region": "RegionTwo", "name": "new_env", "regions": { "": { "defaultNetworks": { "environment": { "autoUplink": true, "name": "new_env-network", "externalRouterId": null, "dnsNameservers": [], "autogenerateSubnet": true, "subnetCidr": null, "openstackId": null, "?": { "dependencies": { "onDestruction": [{ "subscriber": "c80e33dd67a44f489b2f04818b72f404", "handler": null }] }, "type": "io.murano.resources.NeutronNetwork/0.0.0@io.murano", "id": "e145b50623c04a68956e3e656a0568d3", "name": null }, "regionName": "RegionOne" }, "flat": null }, "name": "RegionOne", "?": { "type": "io.murano.CloudRegion/0.0.0@io.murano", "id": "c80e33dd67a44f489b2f04818b72f404", "name": null } }, "RegionOne": "c80e33dd67a44f489b2f04818b72f404", "RegionTwo": { "defaultNetworks": { "environment": { "autoUplink": true, "name": "new_env-network", "externalRouterId": "e449bdd5-228c-4747-a925-18cda80fbd6b", "dnsNameservers": ["8.8.8.8"], "autogenerateSubnet": true, "subnetCidr": "10.0.198.0/24", "openstackId": "00a695c1-60ff-42ec-acb9-b916165413da", "?": { "dependencies": { "onDestruction": [{ "subscriber": "f8cb28d147914850978edb35eca156e1", "handler": null }] }, "type": "io.murano.resources.NeutronNetwork/0.0.0@io.murano", "id": "72d2c13c600247c98e09e2e3c1cd9d70", "name": null }, "regionName": "RegionTwo" }, "flat": null }, "name": "RegionTwo", "?": { "type": "io.murano.CloudRegion/0.0.0@io.murano", "id": "f8cb28d147914850978edb35eca156e1", "name": null } } }, "services": [], "?": { "type": "io.murano.Environment/0.0.0@io.murano", "_actions": { "f7f22c174070455c9cafc59391402bdc_deploy": { "enabled": true, "name": "deploy", "title": "deploy" } }, "id": "f7f22c174070455c9cafc59391402bdc", "name": null } } ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/api-ref/source/v1/samples/execute-action-response.json0000664000175000017500000000006200000000000025401 0ustar00zuulzuul00000000000000{ "task_id": "9e60318629ef47378b583825e7d282b7" }././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/api-ref/source/v1/samples/package-create-response.json0000664000175000017500000000210400000000000025317 0ustar00zuulzuul00000000000000{ "class_definitions": [ "com.example.apache.ApacheHttpServer" ], "description": "The Apache HTTP Server Project is an effort to develop and maintain an\nopen-source HTTP server for modern operating systems including UNIX and\nWindows NT. The goal of this project is to provide a secure, efficient and\nextensible server that provides HTTP services in sync with the current HTTP\nstandards.\nApache httpd has been the most popular web server on the Internet since\nApril 1996, and celebrated its 17th birthday as a project this February.\n", "tags": [ "HTTP", "Server", "WebServer", "HTML", "Apache" ], "updated": "2017-04-06T07:54:40", "is_public": false, "id": "10f3e349bca9432abd673319195eed2b", "categories": [], "name": "Apache HTTP Server", "created": "2017-04-06T07:54:40", "author": "Mirantis, Inc", "enabled": true, "supplier": {}, "fully_qualified_name": "com.example.apache.ApacheHttpServer", "type": "Application", "owner_id": "c0f6e4cf1bfc48aba587e709b58c9f28" } ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/api-ref/source/v1/samples/package-show-response.json0000664000175000017500000000210400000000000025034 0ustar00zuulzuul00000000000000{ "updated": "2017-04-06T08:22:11", "description": "The Apache HTTP Server Project is an effort to develop and maintain an\nopen-source HTTP server for modern operating systems including UNIX and\nWindows NT. The goal of this project is to provide a secure, efficient and\nextensible server that provides HTTP services in sync with the current HTTP\nstandards.\nApache httpd has been the most popular web server on the Internet since\nApril 1996, and celebrated its 17th birthday as a project this February.\n", "tags": [ "HTTP", "Server", "WebServer", "HTML", "Apache" ], "class_definitions": [ "com.example.apache.ApacheHttpServer" ], "is_public": false, "categories": [], "name": "Apache HTTP Server", "created": "2017-04-06T08:22:11", "author": "Mirantis, Inc", "enabled": true, "id": "979637f39a7245cebeabc99e6aa01666", "supplier": {}, "fully_qualified_name": "com.example.apache.ApacheHttpServer", "type": "Application", "owner_id": "c0f6e4cf1bfc48aba587e709b58c9f28" } ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/api-ref/source/v1/samples/package-update-request.json0000664000175000017500000000013500000000000025172 0ustar00zuulzuul00000000000000[ { "path": "/is_public", "value": true, "op": "replace" } ] ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/api-ref/source/v1/samples/package-update-response.json0000664000175000017500000000210300000000000025335 0ustar00zuulzuul00000000000000{ "updated": "2017-04-06T08:28:22", "description": "The Apache HTTP Server Project is an effort to develop and maintain an\nopen-source HTTP server for modern operating systems including UNIX and\nWindows NT. The goal of this project is to provide a secure, efficient and\nextensible server that provides HTTP services in sync with the current HTTP\nstandards.\nApache httpd has been the most popular web server on the Internet since\nApril 1996, and celebrated its 17th birthday as a project this February.\n", "tags": [ "HTTP", "Server", "WebServer", "HTML", "Apache" ], "class_definitions": [ "com.example.apache.ApacheHttpServer" ], "is_public": true, "categories": [], "name": "Apache HTTP Server", "created": "2017-04-06T08:22:11", "author": "Mirantis, Inc", "enabled": true, "id": "979637f39a7245cebeabc99e6aa01666", "supplier": {}, "fully_qualified_name": "com.example.apache.ApacheHttpServer", "type": "Application", "owner_id": "c0f6e4cf1bfc48aba587e709b58c9f28" } ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/api-ref/source/v1/samples/packages-list-response.json0000664000175000017500000001363700000000000025227 0ustar00zuulzuul00000000000000{ "packages": [ { "updated": "2017-03-30T08:35:03", "description": "Library of base class to develop scalable Applications with MuranoPL\n", "tags": [], "class_definitions": [ "io.murano.applications.tests.TestPoolReplicaProvider", "io.murano.applications.SingleServerApplication", "io.murano.applications.tests.TestSoftwareComponent", "io.murano.applications.SoftwareComponent", "io.murano.applications.tests.TestEvents", "io.murano.applications.CloneReplicaProvider", "io.murano.applications.PoolReplicaProvider", "io.murano.applications.Event", "io.murano.applications.SingleServerGroup", "io.murano.applications.TemplateServerProvider", "io.murano.applications.MultiServerApplication", "io.murano.applications.ReplicationGroup", "io.murano.applications.OpenStackSecurityConfigurable", "io.murano.applications.Configurable", "io.murano.applications.tests.TestMockedServerFactory", "io.murano.applications.tests.TestCompositeReplicaProvider", "io.murano.applications.tests.TestRoundrobinReplicaProvider", "io.murano.applications.ServerReplicationGroup", "io.murano.applications.CompositeReplicaProvider", "io.murano.applications.tests.TestReplication", "io.murano.applications.CompositeServerGroup", "io.murano.applications.RoundrobinReplicaProvider", "io.murano.applications.ServerGroup", "io.murano.applications.ServerList", "io.murano.applications.Installable", "io.murano.applications.ReplicaProvider", "io.murano.applications.MultiServerApplicationWithScaling" ], "is_public": true, "categories": [], "name": "Application Development Library", "created": "2017-03-30T08:35:03", "author": "Mirantis, Inc.", "enabled": true, "id": "b0298c205235410fba047f4af8df0eb0", "supplier": {}, "fully_qualified_name": "io.murano.applications", "type": "Library", "owner_id": "c0f6e4cf1bfc48aba587e709b58c9f28" }, { "updated": "2017-03-30T08:35:07", "description": "Core MuranoPL library\n", "tags": [ "MuranoPL" ], "class_definitions": [ "io.murano.Exception", "io.murano.system.MetadefBrowser", "io.murano.metadata.forms.Hidden", "io.murano.system.NeutronSecurityGroupManager", "io.murano.system.AgentListener", "io.murano.Environment", "io.murano.system.SecurityGroupManager", "io.murano.resources.ConfLangInstance", "io.murano.resources.HeatSWConfigLinuxInstance", "io.murano.test.TestFixture", "io.murano.resources.MetadataAware", "io.murano.SharedIp", "io.murano.File", "io.murano.resources.LinuxUDInstance", "io.murano.configuration.Linux", "io.murano.resources.ExistingNeutronNetwork", "io.murano.resources.LinuxMuranoInstance", "io.murano.Object", "io.murano.system.Logger", "io.murano.metadata.engine.Synchronize", "io.murano.test.DummyNetwork", "io.murano.resources.CinderVolume", "io.murano.metadata.Title", "io.murano.Project", "io.murano.system.Resources", "io.murano.metadata.forms.Section", "io.murano.resources.Network", "io.murano.system.MistralClient", "io.murano.resources.CinderVolumeBackup", "io.murano.system.NetworkExplorer", "io.murano.system.DummySecurityGroupManager", "io.murano.resources.WindowsInstance", "io.murano.CloudResource", "io.murano.CloudRegion", "io.murano.system.Agent", "io.murano.resources.Instance", "io.murano.resources.Volume", "io.murano.system.InstanceNotifier", "io.murano.metadata.ModelBuilder", "io.murano.system.HeatStack", "io.murano.resources.LinuxInstance", "io.murano.metadata.Description", "io.murano.metadata.engine.Serialize", "io.murano.resources.ExistingCinderVolume", "io.murano.resources.HeatSWConfigInstance", "io.murano.system.StatusReporter", "io.murano.Application", "io.murano.test.TestFixtureWithEnvironment", "io.murano.system.AwsSecurityGroupManager", "io.murano.StackTrace", "io.murano.resources.NovaNetwork", "io.murano.metadata.forms.Position", "io.murano.metadata.HelpText", "io.murano.resources.NeutronNetworkBase", "io.murano.User", "io.murano.resources.InstanceAffinityGroup", "io.murano.resources.NeutronNetwork", "io.murano.resources.CinderVolumeSnapshot" ], "is_public": true, "categories": [], "name": "Core library", "created": "2017-03-30T08:35:07", "author": "murano.io", "enabled": true, "id": "5b6c8d7cd0694a7ebb7525ae62357740", "supplier": {}, "fully_qualified_name": "io.murano", "type": "Library", "owner_id": "c0f6e4cf1bfc48aba587e709b58c9f28" } ] } ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/api-ref/source/v1/samples/session-create-response.json0000664000175000017500000000034000000000000025407 0ustar00zuulzuul00000000000000{ "created": "2017-04-06T07:54:40", "updated": "2017-04-06T07:54:40", "environment_id": "744e44812da84e858946f5d817de4f72", "state": "opened", "version": 0, "id": "257bef44a9d848daa5b2563779714820" }././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/api-ref/source/v1/samples/session-show-response.json0000664000175000017500000000043100000000000025125 0ustar00zuulzuul00000000000000{ "id": "4aecdc2178b9430cbbb8db44fb7ac384", "environment_id": "4dc8a2e8986fa8fa5bf24dc8a2e8986fa8", "created": "2013-11-30T03:23:42Z", "updated": "2013-11-30T03:23:54Z", "user_id": "d7b501094caf4daab08469663a9e1a2b", "version": 0, "state": "deploying" }././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/api-ref/source/v1/samples/static-action-request.json0000664000175000017500000000013700000000000025063 0ustar00zuulzuul00000000000000{ "className": "ns.Bar", "methodName": "staticAction", "parameters": {"myName": "John"} }././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/api-ref/source/v1/samples/static-action-response.json0000664000175000017500000000001500000000000025224 0ustar00zuulzuul00000000000000"Hello, John"././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/api-ref/source/v1/samples/template-add-app-request.json0000664000175000017500000000063300000000000025441 0ustar00zuulzuul00000000000000{ "instance": { "assignFloatingIp": "true", "keyname": "mykeyname", "image": "cloud-fedora-v3", "flavor": "m1.medium", "?": { "type": "io.murano.resources.LinuxMuranoInstance", "id": "ef984a74-29a4-45c0-b1dc-2ab9f075732e" } }, "name": "orion", "?": { "type": "io.murano.apps.apache.Tomcat", "id": "54cea43d-5970-4c73-b9ac-fea656f3c722" }, "port": "8080" }././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/api-ref/source/v1/samples/template-add-app-response.json0000664000175000017500000000140000000000000025600 0ustar00zuulzuul00000000000000{ "updated": "2017-04-26T19:41:58", "created": "2017-04-26T19:33:10", "tenant_id": "cca37eef752244d99945a4123f30ff79", "services": [ { "instance": { "assignFloatingIp": "true", "keyname": "mykeyname", "image": "cloud-fedora-v3", "flavor": "m1.medium", "?": { "type": "io.murano.resources.LinuxMuranoInstance", "id": "ef984a74-29a4-45c0-b1dc-2ab9f075732e" } }, "name": "orion", "?": { "type": "io.murano.apps.apache.Tomcat", "id": "54cea43d-5970-4c73-b9ac-fea656f3c722" }, "port": "8080" } ], "version": 0, "description_text": "", "is_public": false, "id": "64670f5ada0848408734b2985f5cbb92", "name": "test_application" }././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/api-ref/source/v1/samples/template-clone-request.json0000664000175000017500000000005200000000000025226 0ustar00zuulzuul00000000000000{ "name": "cloned_env_template_name" }././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/api-ref/source/v1/samples/template-clone-response.json0000664000175000017500000000037500000000000025404 0ustar00zuulzuul00000000000000{ "updated": "2015-01-26T09:12:51", "name": "cloned_env_template_name", "created": "2015-01-26T09:12:51", "tenant_id": "00000000000000000000000000000001", "version": 0, "is_public": false, "id": "aa9033ca7ce245fca10e38e1c8c4bbf7", }././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/api-ref/source/v1/samples/template-create-env-request.json0000664000175000017500000000004200000000000026156 0ustar00zuulzuul00000000000000{ "name": "environment_name" }././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/api-ref/source/v1/samples/template-create-env-response.json0000664000175000017500000000040300000000000026325 0ustar00zuulzuul00000000000000{ "environment_id": "aa90fadfafca10e38e1c8c4bbf7", "name": "environment_name", "created": "2015-01-26T09:12:51", "tenant_id": "00000000000000000000000000000001", "version": 0, "session_id": "adf4dadfaa9033ca7ce245fca10e38e1c8c4bbf7", }././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/api-ref/source/v1/samples/template-create-request.json0000664000175000017500000000004300000000000025371 0ustar00zuulzuul00000000000000{ "name": "env_template_name" }././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/api-ref/source/v1/samples/template-create-response.json0000664000175000017500000000044200000000000025542 0ustar00zuulzuul00000000000000{ "updated": "2014-05-14T13:02:55", "networking": {}, "name": "test2", "created": "2014-05-14T13:02:51", "tenant_id": "123452452345346345634563456345346", "version": 0, "is_public": true, "description_text": "", "id": "744e44812da84e858946f5d817de4f72" }././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/api-ref/source/v1/samples/template-list-apps-response.json0000664000175000017500000000126600000000000026220 0ustar00zuulzuul00000000000000[ { "instance": { "assignFloatingIp": "true", "keyname": "mykeyname", "image": "cloud-fedora-v3", "flavor": "m1.medium", "?": { "type": "io.murano.resources.LinuxMuranoInstance", "id": "ef984a74-29a4-45c0-b1dc-2ab9f075732e" } }, "name": "tomcat", "?": { "type": "io.murano.apps.apache.Tomcat", "id": "54cea43d-5970-4c73-b9ac-fea656f3c722" }, "port": "8080" }, { "instance": "ef984a74-29a4-45c0-b1dc-2ab9f075732e", "password": "XXX", "name": "mysql", "?": { "type": "io.murano.apps.database.MySQL", "id": "54cea43d-5970-4c73-b9ac-fea656f3c722" } } ]././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/api-ref/source/v1/samples/template-show-response.json0000664000175000017500000000154700000000000025266 0ustar00zuulzuul00000000000000{ "updated": "2014-05-14T13:02:55", "networking": {}, "name": "test2", "created": "2014-05-14T13:02:51", "tenant_id": "123452452345346345634563456345346", "services": [ { "instance": { "assignFloatingIp": "true", "keyname": "mykeyname", "image": "cloud-fedora-v3", "flavor": "m1.medium", "?": { "type": "io.murano.resources.LinuxMuranoInstance", "id": "ef984a74-29a4-45c0-b1dc-2ab9f075732e" } }, "name": "orion", "?": { "type": "io.murano.apps.apache.Tomcat", "id": "54cea43d-5970-4c73-b9ac-fea656f3c722" }, "port": "8080" } ], "version": 0, "is_public": true, "description_text": "", "id": "744e44812da84e858946f5d817de4f72" }././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/api-ref/source/v1/samples/template-update-app-request.json0000664000175000017500000000072300000000000026173 0ustar00zuulzuul00000000000000{ "instance": { "assignFloatingIp": "true", "keyname": "mykeyname", "image": "cloud-fedora-v3", "flavor": "m1.medium", "?": { "type": "io.murano.resources.LinuxMuranoInstance", "id": "ef984a74-29a4-45c0-b1dc-2ab9f075732e" } }, "name": "orion", "port": "8080", "?": { "type": "io.murano.apps.apache.Tomcat", "id": "54cea43d-5970-4c73-b9ac-fea656f3c722" } }././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/api-ref/source/v1/samples/template-update-app-response.json0000664000175000017500000000072000000000000026336 0ustar00zuulzuul00000000000000{ "instance": { "assignFloatingIp": "true", "keyname": "mykeyname", "image": "cloud-fedora-v3", "flavor": "m1.medium", "?": { "type": "io.murano.resources.LinuxMuranoInstance", "id": "ef984a74-29a4-45c0-b1dc-2ab9f075732e" } }, "name": "orion", "?": { "type": "io.murano.apps.apache.Tomcat", "id": "54cea43d-5970-4c73-b9ac-fea656f3c722" }, "port": "8080" }././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/api-ref/source/v1/samples/templates-list-response.json0000664000175000017500000000142300000000000025435 0ustar00zuulzuul00000000000000{ "templates": [ { "updated": "2014-05-14T13:02:54", "networking": {}, "name": "test1", "created": "2014-05-14T13:02:46", "tenant_id": "726ed856965f43cc8e565bc991fa76c3", "version": 0, "is_public": false, "description_text": "", "id": "2fa5ab704749444bbeafe7991b412c33" }, { "updated": "2014-05-14T13:02:55", "networking": {}, "name": "test2", "created": "2014-05-14T13:02:51", "tenant_id": "123452452345346345634563456345346", "version": 0, "is_public": true, "description_text": "", "id": "744e44812da84e858946f5d817de4f72" } ] }././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/api-ref/source/v1/sessions.inc0000664000175000017500000000647400000000000020647 0ustar00zuulzuul00000000000000.. -*- rst -*- ============================= Environment Configuration API ============================= Since Murano environments are available for local modification by different users and from different locations, it's therefore necessary to store local modifications somewhere. Thus, sessions were created to satisfy this requirement. After a user adds applications to an environment, a new session can be created. A session can be deployed only once. .. note:: Multiple sessions can be opened for one environment simultaneously, but only one session can be deployed at a time. Only the first session that is deployed will be deployed, while the other ones will become invalid, no longer capable of being deploying. Once an environment is in ``deploying`` or ``deleting`` status, a new session for the environment cannot be opened. Configure Environment / Open Session ==================================== .. rest_method:: POST /environments/{env_id}/configure Creates a new configuration session for environment ``env_id``. Response Codes -------------- .. rest_status_code:: success status.yaml - 200 .. rest_status_code:: error status.yaml - 401 - 403 Request Parameters ------------------ .. rest_parameters:: parameters.yaml - env_id: env_id_url Response Parameters ------------------- .. rest_parameters:: parameters.yaml - X-Openstack-Request-Id: request_id - created: created - updated: updated - environment_id: env_id - state: session_state - version: session_version - id: session_id Response Example ---------------- .. literalinclude:: samples/session-create-response.json :language: javascript Deploy session ============== .. rest_method:: POST /environments/{env_id}/sessions/{session_id}/deploy Start deployment of a murano environment session. Response Codes -------------- .. rest_status_code:: success status.yaml - 200 .. rest_status_code:: error status.yaml - 401 - 403 Request Parameters ------------------ .. rest_parameters:: parameters.yaml - env_id: env_id_url - session_id: session_id_url Get Session Details =================== .. rest_method:: GET /environments/{env_id}/sessions/{session_id} Start deployment of a murano environment session. Response Codes -------------- .. rest_status_code:: success status.yaml - 200 .. rest_status_code:: error status.yaml - 401 - 403 Request Parameters ------------------ .. rest_parameters:: parameters.yaml - env_id: env_id_url - session_id: session_id_url Response Parameters ------------------- .. rest_parameters:: parameters.yaml - X-Openstack-Request-Id: request_id - id: session_id - environment_id: env_id - created: created - updated: updated - user_id: session_user_id - version: session_version - state: session_state Response Example ---------------- .. literalinclude:: samples/session-show-response.json :language: javascript Delete Session ============== .. rest_method:: DELETE /environments/{env_id}/sessions/{session_id} Delete the session ``session_id``. Response Codes -------------- .. rest_status_code:: success status.yaml - 200 .. rest_status_code:: error status.yaml - 401 - 403 Request Parameters ------------------ .. rest_parameters:: parameters.yaml - env_id: env_id_url - session_id: session_id_url ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/api-ref/source/v1/status.yaml0000664000175000017500000000340000000000000020477 0ustar00zuulzuul00000000000000################# # Success Codes # ################# 200: default: | Request was successful. 201: default: | Resource was created and is ready to use. 202: default: | Request was accepted for processing, but the processing has not been completed. A 'location' header is included in the response which contains a link to check the progress of the request. 204: default: | The server has fulfilled the request by deleting the resource. 300: default: | There are multiple choices for resources. The request has to be more specific to successfully retrieve one of these resources. 302: default: | The response is about a redirection hint. The header of the response usually contains a 'location' value where requesters can check to track the real location of the resource. ################# # Error Codes # ################# 400: default: | Some content in the request was invalid. resource_signal: | The target resource doesn't support receiving a signal. 401: default: | User must authenticate before making a request. 403: default: | Policy does not allow current user to do this operation. 404: default: | The requested resource could not be found. 405: default: | Method is not valid for this endpoint. 409: default: | This operation conflicted with another operation on this resource. duplicate_zone: | There is already a zone with this name. 500: default: | Something went wrong inside the service. This should not happen usually. If it does happen, it means the server has experienced some serious problems. 503: default: | Service is not available. This is mostly caused by service configuration errors which prevents the service from successful start up. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/api-ref/source/v1/templates.inc0000664000175000017500000002516100000000000020771 0ustar00zuulzuul00000000000000.. -*- rst -*- ===================== Environment Templates ===================== An environment template specifies a set of virtual resources and application information that can be deployed on top of OpenStack by translation this information into an application-ready environment. Environment templates can be customized, created, deleted and modified by users. Environment templates can be instantied as many times as the user desires. For example, the user can have different deployments from the same environment template: one for testing and another for production. The workflow for the creation and the instantiation of the environment template is as follows: #. Creation of the environment template (including application information) #. Transformation of the environment template into the environment (creation of the environment and session and adding applications to the environment) #. Deployment of the environment on top of Openstack Each environment template consists of services, which specify the application information. Each service includes information about the applications that will be installed (e.g. Tomcat), including application properties like the Tomcat port. Additional information pertaining to the virtual server may be specified, if applicable, such as keyname, flavor, image, etc. The following is an example of an environment template:: { "name": "env_template_name", "services": [ { "instance": { "assignFloatingIp": "true", "keyname": "mykeyname", "image": "cloud-fedora-v3", "flavor": "m1.medium", "?": { "type": "io.murano.resources.LinuxMuranoInstance", "id": "ef984a74-29a4-45c0-b1dc-2ab9f075732e" } }, "name": "tomcat", "port": "8080", "?": { "type": "io.murano.apps.apache.Tomcat", "id": "54cea43d-5970-4c73-b9ac-fea656f3c722" } } ] } List environment templates ========================== .. rest_method:: GET /templates Get a list of environment templates. Response Codes -------------- .. rest_status_code:: success status.yaml - 200 .. rest_status_code:: error status.yaml - 401 Request Parameters ------------------ .. rest_parameters:: parameters.yaml - is_public: template_is_public_url Response Parameters ------------------- .. rest_parameters:: parameters.yaml - X-Openstack-Request-Id: request_id - templates: templates - created: created - updated: updated - name: template_name - tenant_id: tenant_id - version: template_version - description_text: template_description - is_public: template_is_public - id: template_id Response Example ---------------- .. literalinclude:: samples/templates-list-response.json :language: javascript Create environment template =========================== .. rest_method:: POST /templates Create an environment template. Response Codes -------------- .. rest_status_code:: success status.yaml - 200 .. rest_status_code:: error status.yaml - 401 - 409 Request Parameters ------------------ .. rest_parameters:: parameters.yaml - name: template_name - is_public: template_is_public Request Example --------------- .. literalinclude:: samples/template-create-request.json :language: javascript Response Parameters ------------------- .. rest_parameters:: parameters.yaml - X-Openstack-Request-Id: request_id - created: created - updated: updated - name: template_name - tenant_id: tenant_id - version: template_version - description_text: template_description - is_public: template_is_public - id: template_id Response Example ---------------- .. literalinclude:: samples/template-create-response.json :language: javascript Get environment template details ================================ .. rest_method:: GET /templates/{env_temp_id} Get details for the environment template ``env_temp_id``. Response Codes -------------- .. rest_status_code:: success status.yaml - 200 .. rest_status_code:: error status.yaml - 401 - 404 Request Parameters ------------------ .. rest_parameters:: parameters.yaml - env_temp_id: template_id_url Response Parameters ------------------- .. rest_parameters:: parameters.yaml - X-Openstack-Request-Id: request_id - created: created - updated: updated - name: template_name - services: template_services - tenant_id: tenant_id - version: template_version - description_text: template_description - is_public: template_is_public - id: template_id Response Example ---------------- .. literalinclude:: samples/template-show-response.json :language: javascript Delete environment template =========================== .. rest_method:: DELETE /templates/{env_temp_id} Delete the environment template ``env_temp_id``. Response Codes -------------- .. rest_status_code:: success status.yaml - 200 .. rest_status_code:: error status.yaml - 401 - 404 Request Parameters ------------------ .. rest_parameters:: parameters.yaml - env_temp_id: template_id_url Add application to environment template ======================================= .. rest_method:: POST /templates/{env_temp_id}/services Create a new application for environment template ``env_temp_id``. Response Codes -------------- .. rest_status_code:: success status.yaml - 200 .. rest_status_code:: error status.yaml - 401 - 404 Request Parameters ------------------ .. rest_parameters:: parameters.yaml - env_temp_id: template_id_url - service: template_service Request Example --------------- .. literalinclude:: samples/template-add-app-request.json :language: javascript Response Parameters ------------------- .. rest_parameters:: parameters.yaml - X-Openstack-Request-Id: request_id - updated: updated - created: created - tenant_id: tenant_id - services: template_services - version: template_version - description_text: template_description - is_public: template_is_public - id: template_id - name: template_name Response Example ---------------- .. literalinclude:: samples/template-add-app-response.json :language: javascript Delete application from an environment template =============================================== .. rest_method:: DELETE /templates/{env_temp_id}/services/{service_id} Delete an application from an environment template. Response Codes -------------- .. rest_status_code:: success status.yaml - 200 .. rest_status_code:: error status.yaml - 401 - 404 Request Parameters ------------------ .. rest_parameters:: parameters.yaml - env_temp_id: template_id_url - service_id: service_id_url List application details for environment template ================================================= .. rest_method:: GET /templates/{env_temp_id}/services List all the applications for the specified environment template ``env_temp_id``. Response Codes -------------- .. rest_status_code:: success status.yaml - 200 .. rest_status_code:: error status.yaml - 401 - 404 Request Parameters ------------------ .. rest_parameters:: parameters.yaml - env_temp_id: template_id_url Response Parameters ------------------- - X-Openstack-Request-Id: request_id - updated: updated - created: created - tenant_id: tenant_id - services: template_services - version: template_version - description_text: template_description - is_public: template_is_public - id: template_id - name: template_name Response Example ---------------- .. literalinclude:: samples/template-list-apps-response.json :language: javascript Update application for an environment template ============================================== .. rest_method:: PUT /templates/{env_temp_id}/services/{service_id} Delete an application from an environment template. Response Codes -------------- .. rest_status_code:: success status.yaml - 200 .. rest_status_code:: error status.yaml - 401 - 404 Request Parameters ------------------ .. rest_parameters:: parameters.yaml - env_temp_id: template_id_url - service_id: service_id_url - service: template_service Request Example --------------- .. literalinclude:: samples/template-update-app-request.json :language: javascript Response Parameters ------------------- .. rest_parameters:: parameters.yaml - X-Openstack-Request-Id: request_id - service: template_service Response Example ---------------- .. literalinclude:: samples/template-update-app-response.json :language: javascript Create environment from environment template ============================================ .. rest_method:: GET /templates/{env_temp_id}/create-environment Create an environment from the environment template ``env_temp_id``. Response Codes -------------- .. rest_status_code:: success status.yaml - 200 .. rest_status_code:: error status.yaml - 401 - 404 - 409 Request Parameters ------------------ .. rest_parameters:: parameters.yaml - env_temp_id: template_id_url - name: env_name Request Example --------------- .. literalinclude:: samples/template-create-env-request.json :language: javascript Response Parameters ------------------- .. rest_parameters:: parameters.yaml - X-Openstack-Request-Id: request_id - environment_id: env_id - name: env_name - created: created - tenant_id: tenant_id - version: env_version - session_id: session_id Response Example ---------------- .. literalinclude:: samples/template-create-env-response.json :language: javascript Clone environment template ========================== .. rest_method:: GET /templates/{env_temp_id}/clone Clones an environment template from one tenant into another. .. note: In order to clone an environment template, the template *must* be public. Response Codes -------------- .. rest_status_code:: success status.yaml - 200 .. rest_status_code:: error status.yaml - 401 - 404 - 409 Request Parameters ------------------ .. rest_parameters:: parameters.yaml - env_temp_id: template_id_url - name: template_name Request Example --------------- .. literalinclude:: samples/template-clone-request.json :language: javascript Response Parameters ------------------- .. rest_parameters:: parameters.yaml - X-Openstack-Request-Id: request_id - environment_id: env_id - name: env_name - created: created - tenant_id: tenant_id - version: env_version - session_id: session_id Response Example ---------------- .. literalinclude:: samples/template-clone-response.json :language: javascript ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/bandit.yaml0000664000175000017500000001710700000000000015255 0ustar00zuulzuul00000000000000 ### This config may optionally select a subset of tests to run or skip by ### filling out the 'tests' and 'skips' lists given below. If no tests are ### specified for inclusion then it is assumed all tests are desired. The skips ### set will remove specific tests from the include set. This can be controlled ### using the -t/-s CLI options. Note that the same test ID should not appear ### in both 'tests' and 'skips', this would be nonsensical and is detected by ### Bandit at runtime. # Available tests: # B101 : assert_used # B102 : exec_used # B103 : set_bad_file_permissions # B104 : hardcoded_bind_all_interfaces # B105 : hardcoded_password_string # B106 : hardcoded_password_funcarg # B107 : hardcoded_password_default # B108 : hardcoded_tmp_directory # B109 : password_config_option_not_marked_secret # B110 : try_except_pass # B111 : execute_with_run_as_root_equals_true # B112 : try_except_continue # B201 : flask_debug_true # B301 : pickle # B302 : marshal # B303 : md5 # B304 : ciphers # B305 : cipher_modes # B306 : mktemp_q # B307 : eval # B308 : mark_safe # B309 : httpsconnection # B310 : urllib_urlopen # B311 : random # B312 : telnetlib # B313 : xml_bad_cElementTree # B314 : xml_bad_ElementTree # B315 : xml_bad_expatreader # B316 : xml_bad_expatbuilder # B317 : xml_bad_sax # B318 : xml_bad_minidom # B319 : xml_bad_pulldom # B320 : xml_bad_etree # B321 : ftplib # B401 : import_telnetlib # B402 : import_ftplib # B403 : import_pickle # B404 : import_subprocess # B405 : import_xml_etree # B406 : import_xml_sax # B407 : import_xml_expat # B408 : import_xml_minidom # B409 : import_xml_pulldom # B410 : import_lxml # B411 : import_xmlrpclib # B412 : import_httpoxy # B501 : request_with_no_cert_validation # B502 : ssl_with_bad_version # B503 : ssl_with_bad_defaults # B504 : ssl_with_no_version # B505 : weak_cryptographic_key # B506 : yaml_load # B601 : paramiko_calls # B602 : subprocess_popen_with_shell_equals_true # B603 : subprocess_without_shell_equals_true # B604 : any_other_function_with_shell_equals_true # B605 : start_process_with_a_shell # B606 : start_process_with_no_shell # B607 : start_process_with_partial_path # B608 : hardcoded_sql_expressions # B609 : linux_commands_wildcard_injection # B701 : jinja2_autoescape_false # B702 : use_of_mako_templates # (optional) list included test IDs here, eg '[B101, B406]': tests: # (optional) list skipped test IDs here, eg '[B101, B406]': skips: [B104] ### (optional) plugin settings - some test plugins require configuration data ### that may be given here, per-plugin. All bandit test plugins have a built in ### set of sensible defaults and these will be used if no configuration is ### provided. It is not necessary to provide settings for every (or any) plugin ### if the defaults are acceptable. #any_other_function_with_shell_equals_true: # no_shell: [os.execl, os.execle, os.execlp, os.execlpe, os.execv, os.execve, os.execvp, # os.execvpe, os.spawnl, os.spawnle, os.spawnlp, os.spawnlpe, os.spawnv, os.spawnve, # os.spawnvp, os.spawnvpe, os.startfile] # shell: [os.system, os.popen, os.popen2, os.popen3, os.popen4, popen2.popen2, popen2.popen3, # popen2.popen4, popen2.Popen3, popen2.Popen4, commands.getoutput, commands.getstatusoutput] # subprocess: [subprocess.Popen, subprocess.call, subprocess.check_call, subprocess.check_output, # utils.execute, utils.execute_with_timeout] #execute_with_run_as_root_equals_true: # function_names: [ceilometer.utils.execute, cinder.utils.execute, neutron.agent.linux.utils.execute, # nova.utils.execute, nova.utils.trycmd] #hardcoded_tmp_directory: # tmp_dirs: [/tmp, /var/tmp, /dev/shm] #linux_commands_wildcard_injection: # no_shell: [os.execl, os.execle, os.execlp, os.execlpe, os.execv, os.execve, os.execvp, # os.execvpe, os.spawnl, os.spawnle, os.spawnlp, os.spawnlpe, os.spawnv, os.spawnve, # os.spawnvp, os.spawnvpe, os.startfile] # shell: [os.system, os.popen, os.popen2, os.popen3, os.popen4, popen2.popen2, popen2.popen3, # popen2.popen4, popen2.Popen3, popen2.Popen4, commands.getoutput, commands.getstatusoutput] # subprocess: [subprocess.Popen, subprocess.call, subprocess.check_call, subprocess.check_output, # utils.execute, utils.execute_with_timeout] #password_config_option_not_marked_secret: # function_names: [oslo.config.cfg.StrOpt, oslo_config.cfg.StrOpt] #ssl_with_bad_defaults: # bad_protocol_versions: [PROTOCOL_SSLv2, SSLv2_METHOD, SSLv23_METHOD, PROTOCOL_SSLv3, # PROTOCOL_TLSv1, SSLv3_METHOD, TLSv1_METHOD] #ssl_with_bad_version: # bad_protocol_versions: [PROTOCOL_SSLv2, SSLv2_METHOD, SSLv23_METHOD, PROTOCOL_SSLv3, # PROTOCOL_TLSv1, SSLv3_METHOD, TLSv1_METHOD] #start_process_with_a_shell: # no_shell: [os.execl, os.execle, os.execlp, os.execlpe, os.execv, os.execve, os.execvp, # os.execvpe, os.spawnl, os.spawnle, os.spawnlp, os.spawnlpe, os.spawnv, os.spawnve, # os.spawnvp, os.spawnvpe, os.startfile] # shell: [os.system, os.popen, os.popen2, os.popen3, os.popen4, popen2.popen2, popen2.popen3, # popen2.popen4, popen2.Popen3, popen2.Popen4, commands.getoutput, commands.getstatusoutput] # subprocess: [subprocess.Popen, subprocess.call, subprocess.check_call, subprocess.check_output, # utils.execute, utils.execute_with_timeout] #start_process_with_no_shell: # no_shell: [os.execl, os.execle, os.execlp, os.execlpe, os.execv, os.execve, os.execvp, # os.execvpe, os.spawnl, os.spawnle, os.spawnlp, os.spawnlpe, os.spawnv, os.spawnve, # os.spawnvp, os.spawnvpe, os.startfile] # shell: [os.system, os.popen, os.popen2, os.popen3, os.popen4, popen2.popen2, popen2.popen3, # popen2.popen4, popen2.Popen3, popen2.Popen4, commands.getoutput, commands.getstatusoutput] # subprocess: [subprocess.Popen, subprocess.call, subprocess.check_call, subprocess.check_output, # utils.execute, utils.execute_with_timeout] #start_process_with_partial_path: # no_shell: [os.execl, os.execle, os.execlp, os.execlpe, os.execv, os.execve, os.execvp, # os.execvpe, os.spawnl, os.spawnle, os.spawnlp, os.spawnlpe, os.spawnv, os.spawnve, # os.spawnvp, os.spawnvpe, os.startfile] # shell: [os.system, os.popen, os.popen2, os.popen3, os.popen4, popen2.popen2, popen2.popen3, # popen2.popen4, popen2.Popen3, popen2.Popen4, commands.getoutput, commands.getstatusoutput] # subprocess: [subprocess.Popen, subprocess.call, subprocess.check_call, subprocess.check_output, # utils.execute, utils.execute_with_timeout] #subprocess_popen_with_shell_equals_true: # no_shell: [os.execl, os.execle, os.execlp, os.execlpe, os.execv, os.execve, os.execvp, # os.execvpe, os.spawnl, os.spawnle, os.spawnlp, os.spawnlpe, os.spawnv, os.spawnve, # os.spawnvp, os.spawnvpe, os.startfile] # shell: [os.system, os.popen, os.popen2, os.popen3, os.popen4, popen2.popen2, popen2.popen3, # popen2.popen4, popen2.Popen3, popen2.Popen4, commands.getoutput, commands.getstatusoutput] # subprocess: [subprocess.Popen, subprocess.call, subprocess.check_call, subprocess.check_output, # utils.execute, utils.execute_with_timeout] #subprocess_without_shell_equals_true: # no_shell: [os.execl, os.execle, os.execlp, os.execlpe, os.execv, os.execve, os.execvp, # os.execvpe, os.spawnl, os.spawnle, os.spawnlp, os.spawnlpe, os.spawnv, os.spawnve, # os.spawnvp, os.spawnvpe, os.startfile] # shell: [os.system, os.popen, os.popen2, os.popen3, os.popen4, popen2.popen2, popen2.popen3, # popen2.popen4, popen2.Popen3, popen2.Popen4, commands.getoutput, commands.getstatusoutput] # subprocess: [subprocess.Popen, subprocess.call, subprocess.check_call, subprocess.check_output, # utils.execute, utils.execute_with_timeout] #try_except_continue: {check_typed_exception: false} #try_except_pass: {check_typed_exception: false} ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/bindep.txt0000664000175000017500000000055000000000000015124 0ustar00zuulzuul00000000000000# This is a cross-platform list tracking distribution packages needed for install and tests; # see https://docs.openstack.org/infra/bindep/ for additional information. libpq-dev [platform:dpkg] mysql-client [platform:dpkg] mysql-server [platform:dpkg] postgresql postgresql-client [platform:dpkg] # PDF Docs package dependencies tex-gyre [platform:dpkg doc] ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1696417899.6571803 murano-16.0.0/contrib/0000775000175000017500000000000000000000000014562 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1696417899.6571803 murano-16.0.0/contrib/elements/0000775000175000017500000000000000000000000016376 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1696417899.6971805 murano-16.0.0/contrib/elements/docker/0000775000175000017500000000000000000000000017645 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/contrib/elements/docker/README.md0000664000175000017500000000005400000000000021123 0ustar00zuulzuul00000000000000This element install Docker on Ubuntu/CentOS././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1696417899.6971805 murano-16.0.0/contrib/elements/docker/install.d/0000775000175000017500000000000000000000000021535 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/contrib/elements/docker/install.d/56-docker0000775000175000017500000000072400000000000023165 0ustar00zuulzuul00000000000000#!/bin/bash set -eu if [ -e /etc/lsb-release ]; then if [ -e /usr/lib/apt/methods/https ]; then apt-get update apt-get install apt-transport-https fi apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys 36A1D7869245C8950F966E92D8576A8BA88D21E9 echo "deb https://get.docker.com/ubuntu docker main" > /etc/apt/sources.list.d/docker.list apt-get update apt-get -y install lxc-docker else yum -y install docker fi././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1696417899.6971805 murano-16.0.0/contrib/elements/kubernetes/0000775000175000017500000000000000000000000020545 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/contrib/elements/kubernetes/README.md0000664000175000017500000000006100000000000022021 0ustar00zuulzuul00000000000000This element installs Kubernetes on Ubuntu/CentOS././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/contrib/elements/kubernetes/element-deps0000664000175000017500000000000600000000000023046 0ustar00zuulzuul00000000000000docker././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1696417899.6971805 murano-16.0.0/contrib/elements/kubernetes/install.d/0000775000175000017500000000000000000000000022435 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/contrib/elements/kubernetes/install.d/57-kubernetes0000775000175000017500000000417200000000000024767 0ustar00zuulzuul00000000000000#!/bin/bash install-packages curl wget linux-libc-dev git gcc libc6-dev bridge-utils haproxy SVC_ROOT=/opt/bin ETCD_LATEST_VERSION=$(curl https://github.com/coreos/etcd/releases/latest | awk -F'"' '{ print $2 }' | awk -F'/' '{ print $8 }') ETCD_LATEST_URL="https://github.com/coreos/etcd/releases/download/${ETCD_LATEST_VERSION}/etcd-${ETCD_LATEST_VERSION}-linux-amd64.tar.gz" KUBE_LATEST_VERSION=$(curl https://github.com/GoogleCloudPlatform/kubernetes/releases/latest | awk -F'"' '{ print $2 }' | awk -F'/' '{ print $8 }') KUBE_LATEST_URL="https://github.com/GoogleCloudPlatform/kubernetes/releases/download/${KUBE_LATEST_VERSION}/kubernetes.tar.gz" mkdir -p ${SVC_ROOT} pushd ${SVC_ROOT} # Install latest etcd wget -O ${SVC_ROOT}/etcd-latest.tar.gz $ETCD_LATEST_URL tar xzvf ${SVC_ROOT}/etcd-latest.tar.gz rm -f ${SVC_ROOT}/etcd-latest.tar.gz mv ${SVC_ROOT}/etcd-${ETCD_LATEST_VERSION}-linux-amd64/etcd ${SVC_ROOT}/ mv ${SVC_ROOT}/etcd-${ETCD_LATEST_VERSION}-linux-amd64/etcdctl ${SVC_ROOT}/ rm -rf ${SVC_ROOT}/etcd-${ETCD_LATEST_VERSION}-linux-amd64 # Install latest kubernetes wget -O ${SVC_ROOT}/kubernetes-latest.tar.gz $KUBE_LATEST_URL tar xzvf ${SVC_ROOT}/kubernetes-latest.tar.gz rm -f ${SVC_ROOT}/kubernetes-latest.tar.gz tar xzvf ${SVC_ROOT}/kubernetes/server/kubernetes-server-linux-amd64.tar.gz mv ${SVC_ROOT}/kubernetes ${SVC_ROOT}/kubernetes-latest cp ${SVC_ROOT}/kubernetes-latest/server/bin/* ${SVC_ROOT}/ rm -rf ${SVC_ROOT}/kubernetes-latest # Install Go wget -O go.tar.gz https://storage.googleapis.com/golang/go1.4.2.linux-amd64.tar.gz tar xzvf go.tar.gz mv ${SVC_ROOT}/go /usr/local/go export PATH=$PATH:/usr/local/go/bin # Build flannel git clone https://github.com/coreos/flannel flannel pushd ${SVC_ROOT}/flannel ${SVC_ROOT}/flannel/build popd cp ${SVC_ROOT}/flannel/bin/flanneld ${SVC_ROOT}/flanneld rm -rf ${SVC_ROOT}/flannel # Update system PATH sed -i 's/PATH="/PATH="\/opt\/bin:\/opt\/go\/bin:/g' /etc/environment wget -O confd https://github.com/kelseyhightower/confd/releases/download/v0.7.1/confd-0.7.1-linux-amd64 mv confd /usr/local/bin/confd chmod +x /usr/local/bin/confd mkdir -p /etc/confd/{conf.d,templates} popd././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1696417899.6971805 murano-16.0.0/contrib/glance/0000775000175000017500000000000000000000000016013 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1696417899.6971805 murano-16.0.0/contrib/glance/muranoartifact/0000775000175000017500000000000000000000000021032 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/contrib/glance/muranoartifact/__init__.py0000664000175000017500000000121500000000000023142 0ustar00zuulzuul00000000000000# 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 muranoartifact.v1 import package VERSIONS = [package.MuranoPackage] ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1696417899.6971805 murano-16.0.0/contrib/glance/muranoartifact/v1/0000775000175000017500000000000000000000000021360 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/contrib/glance/muranoartifact/v1/__init__.py0000664000175000017500000000000000000000000023457 0ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/contrib/glance/muranoartifact/v1/package.py0000664000175000017500000000300100000000000023317 0ustar00zuulzuul00000000000000# 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 glance.common.glare import definitions class MuranoPackage(definitions.ArtifactType): __endpoint__ = 'murano' type = definitions.String(allowed_values=['Application', 'Library'], required=True, mutable=False) author = definitions.String(required=False, mutable=False) display_name = definitions.String(required=True, mutable=True) enabled = definitions.Boolean(default=True) categories = definitions.Array(default=[], mutable=True) class_definitions = definitions.Array(unique=True, default=[], mutable=False) inherits = definitions.Dict(default={}, properties=definitions.Array(), mutable=False) keywords = definitions.Array(default=[], mutable=True) logo = definitions.BinaryObject() archive = definitions.BinaryObject() ui_definition = definitions.BinaryObject() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/contrib/glance/setup.cfg0000664000175000017500000000132200000000000017632 0ustar00zuulzuul00000000000000[metadata] name = murano_artifact_plugin description = An artifact plugin for murano packages author = Alexander Tivelkov author-email = openstack-discuss@lists.openstack.org python-requires = >=3.6 classifier = Development Status :: 3 - Alpha License :: OSI Approved :: Apache Software License Programming Language :: Python Programming Language :: Python :: Implementation :: CPython Programming Language :: Python :: 3 :: Only Programming Language :: Python :: 3 Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 Intended Audience :: Developers Environment :: Console [entry_points] glance.artifacts.types = MuranoPackage = muranoartifact:VERSIONS ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/contrib/glance/setup.py0000664000175000017500000000145500000000000017532 0ustar00zuulzuul00000000000000# Copyright 2011-2012 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. import setuptools # all other params will be taken from setup.cfg setuptools.setup(packages=setuptools.find_packages(), setup_requires=['pbr'], pbr=True) ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1696417899.6571803 murano-16.0.0/contrib/packages/0000775000175000017500000000000000000000000016340 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1696417899.6971805 murano-16.0.0/contrib/packages/EncryptionDemo/0000775000175000017500000000000000000000000021277 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1696417899.6971805 murano-16.0.0/contrib/packages/EncryptionDemo/Classes/0000775000175000017500000000000000000000000022674 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/contrib/packages/EncryptionDemo/Classes/EncryptionDemo.yaml0000664000175000017500000000047500000000000026525 0ustar00zuulzuul00000000000000Namespaces: =: com.paul std: io.murano res: io.murano.resources Name: EncryptionDemo Extends: std:Application Properties: my_password: Contract: $.string() Methods: deploy: Body: - $reporter: $this.find(std:Environment).reporter - $reporter.report($this, decryptData($.my_password)) ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1696417899.6971805 murano-16.0.0/contrib/packages/EncryptionDemo/UI/0000775000175000017500000000000000000000000021614 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/contrib/packages/EncryptionDemo/UI/ui.yaml0000664000175000017500000000033100000000000023112 0ustar00zuulzuul00000000000000Application: ?: type: com.paul.EncryptionDemo my_password: encryptData($.instanceConfiguration.my_password) Forms: - instanceConfiguration: fields: - name: my_password type: string ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/contrib/packages/EncryptionDemo/manifest.yaml0000664000175000017500000000027100000000000023771 0ustar00zuulzuul00000000000000FullName: com.paul.EncryptionDemo Type: Application Description: Simple app to demonstrate Murano encryption Author: Paul Bourke Classes: com.paul.EncryptionDemo: EncryptionDemo.yaml ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1696417899.6611803 murano-16.0.0/contrib/plugins/0000775000175000017500000000000000000000000016243 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1696417899.6971805 murano-16.0.0/contrib/plugins/cloudify_plugin/0000775000175000017500000000000000000000000021437 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/contrib/plugins/cloudify_plugin/LICENSE0000664000175000017500000002363600000000000022456 0ustar00zuulzuul00000000000000 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. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/contrib/plugins/cloudify_plugin/README.rst0000664000175000017500000000256000000000000023131 0ustar00zuulzuul00000000000000Murano Plugin for Cloudify ~~~~~~~~~~~~~~~~~~~~~~~~~~ Cloudify is a TOSCA-based open-source cloud orchestration engine by GigaSpaces Technologies. This plugin extends Murano with support of Cloudify TOSCA package format. TOSCA packages can be deployed on Cloudify Manager deployed at configurable location. Plugin registers `Cloudify.TOSCA/1.0` format identifier. Installation ------------ Installation of the plugin is done using any of Python package management tools. The most simple way is by saying `pip install .` from the plugin's directory (or `pip install -e .` for development) Also location of Cloudify Manager (engine server) must be configured in murano config file. This is done in `[cloudify]` section of murano.conf via cloudify_manager setting. For example: .. code-block:: ini [cloudify] cloudify_manager = 10.10.1.10 Murano engine must be restarted after installation of the plugin. Requirements ------------ All Cloudify TOSCA application require `org.getcloudify.murano` library package to be present in Murano catalog. The package can be found in `cloudify_applications_library` subfolder. Demo application ---------------- There is a demo application that can be used to test the plugin. It is located in `nodecellar_example_application` subfolder. Follow instructions at `nodecellar_example_application/README.rst` to build the demo package. ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1696417899.6971805 murano-16.0.0/contrib/plugins/cloudify_plugin/cloudify_applications_library/0000775000175000017500000000000000000000000027547 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1696417899.6971805 murano-16.0.0/contrib/plugins/cloudify_plugin/cloudify_applications_library/Classes/0000775000175000017500000000000000000000000031144 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000021400000000000011452 xustar0000000000000000118 path=murano-16.0.0/contrib/plugins/cloudify_plugin/cloudify_applications_library/Classes/CloudifyApplication.yaml 22 mtime=1696417875.0 murano-16.0.0/contrib/plugins/cloudify_plugin/cloudify_applications_library/Classes/CloudifyApplicat0000664000175000017500000000502200000000000034322 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. Namespaces: =: org.getcloudify.murano std: io.murano csys: io.murano.extensions.cloudify Name: CloudifyApplication Extends: std:Application Methods: .init: Body: - $._client: new(csys:CloudifyClient, app => $this) - $._environment: $.find(std:Environment).require() describe: updateOutputs: Arguments: - outputs: Contract: $.string().notNull(): $ deploy: Body: - If: not $.getAttr(deployed, false) Then: - $info: $.describe() - $._environment.reporter.report($this, 'Checking for TOSCA package') - $._client.publishBlueprint($info.entryPoint) - $._client.createDeployment($info.inputs) - $._environment.reporter.report($this, 'Waiting for deployment initialization') - $._client.waitDeploymentReady() - $._environment.reporter.report($this, 'Installing {0}'.format(name($this))) - $._client.executeWorkflow(install) - $outputs: $._client.waitDeploymentReady() - For: outputName In: $outputs.keys() Do: - $output: $outputs[$outputName] - $._environment.reporter.report($this, $output) - $label: $output.get(description, $outputName) - $value: $output.value - $msg: '{0}: {1}' - $._environment.reporter.report($this, $msg.format($label, $value)) - $.updateOutputs($outputs) - $._environment.reporter.report($this, 'Installation complete') - $.setAttr(deployed, true) .destroy: Body: - If: $.getAttr(deployed, false) Then: - $info: $.describe() - $._client.waitDeploymentReady() - $._environment.reporter.report($this, 'Uninstalling {0}'.format(name($this))) - $._client.executeWorkflow(uninstall) - $._client.waitDeploymentReady() - $._client.deleteDeployment() - $._environment.reporter.report($this, 'Uninstallation complete') - $.setAttr(deployed, false) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/contrib/plugins/cloudify_plugin/cloudify_applications_library/manifest.yaml0000664000175000017500000000155300000000000032245 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. Format: MuranoPL/1.2 Type: Library FullName: org.getcloudify.murano Name: Cloudify applications Description: > Cloudify Murano integration support library Author: Trammell Tags: - Cloudify Classes: org.getcloudify.murano.CloudifyApplication: CloudifyApplication.yaml Require: io.murano.plugins.cloudify: 0././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1696417899.7011805 murano-16.0.0/contrib/plugins/cloudify_plugin/murano_cloudify_plugin/0000775000175000017500000000000000000000000026214 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/contrib/plugins/cloudify_plugin/murano_cloudify_plugin/__init__.py0000664000175000017500000000000000000000000030313 0ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/contrib/plugins/cloudify_plugin/murano_cloudify_plugin/cfg.py0000664000175000017500000000134300000000000027326 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # 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 oslo_config import cfg def init_config(conf): opts = [ cfg.StrOpt('cloudify_manager', required=True) ] conf.register_opts(opts, group='cloudify') return conf.cloudify ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/contrib/plugins/cloudify_plugin/murano_cloudify_plugin/cloudify_client.py0000664000175000017500000000604600000000000031750 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT 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 threading import time import cloudify_rest_client import cloudify_rest_client.exceptions as cloudify_exceptions from murano.dsl import dsl from oslo_config import cfg as config from yaql.language import specs from yaql.language import yaqltypes import cfg CONF = config.CONF archive_upload_lock = threading.Lock() class CloudifyClient(object): @specs.parameter('app', dsl.MuranoObjectParameter('io.murano.Application')) def __init__(self, app): cloudify_manager = self.CONF.cloudify_manager self._client = cloudify_rest_client.CloudifyClient(cloudify_manager) self._blueprint_id = '{0}-{1}'.format(app.type.name, app.type.version) self._deployment_id = app.id self._application_package = app.package @specs.parameter('entry_point', yaqltypes.String()) def publish_blueprint(self, entry_point): global archive_upload_lock if self._check_blueprint_exists(): return path = self._application_package.get_resource(entry_point) with archive_upload_lock: try: self._client.blueprints.upload( path, self._blueprint_id) except cloudify_exceptions.CloudifyClientError as e: if e.status_code != 409: raise def _check_blueprint_exists(self): try: self._client.blueprints.get(self._blueprint_id) return True except cloudify_exceptions.CloudifyClientError as e: if e.status_code == 404: return False raise @specs.parameter('parameters', dict) def create_deployment(self, parameters=None): self._client.deployments.create( self._blueprint_id, self._deployment_id, parameters) def delete_deployment(self): self._client.deployments.delete(self._deployment_id) def wait_deployment_ready(self): while True: executions = self._client.executions.list(self._deployment_id) if any(t.status in ('pending', 'started') for t in executions): time.sleep(3) else: deployment = self._client.deployments.get(self._deployment_id) return deployment.outputs @specs.parameter('name', yaqltypes.String()) @specs.parameter('parameters', dict) def execute_workflow(self, name, parameters=None): self._client.executions.start(self._deployment_id, name, parameters) @classmethod def init_plugin(cls): cls.CONF = cfg.init_config(CONF) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/contrib/plugins/cloudify_plugin/murano_cloudify_plugin/cloudify_tosca_package.py0000664000175000017500000001323700000000000033256 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # 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 yaml from murano.common.helpers import path from murano.packages import exceptions from murano.packages import package_base RESOURCES_DIR_NAME = 'Resources/' class YAQL(object): def __init__(self, expr): self.expr = expr class Dumper(yaml.SafeDumper): pass def yaql_representer(dumper, data): return dumper.represent_scalar(u'!yaql', data.expr) Dumper.add_representer(YAQL, yaql_representer) class CloudifyToscaPackage(package_base.PackageBase): def __init__(self, format_name, runtime_version, source_directory, manifest): super(CloudifyToscaPackage, self).__init__( format_name, runtime_version, source_directory, manifest) self._entry_point = manifest.get('EntryPoint', 'main.yaml') self._generated_class = None self._generated_ui = None @property def classes(self): return self.full_name, @property def requirements(self): return { 'org.getcloudify.murano': '0' } @property def ui(self): if not self._generated_ui: self._generated_ui = self._generate_ui() return self._generated_ui def get_class(self, name): if name != self.full_name: raise exceptions.PackageClassLoadError( name, 'Class not defined in this package') if not self._generated_class: self._generated_class = self._generate_class() return self._generated_class, '' def _generate_class(self): inputs, outputs = self._get_inputs_outputs() class_code = { 'Name': self.full_name, 'Extends': 'org.getcloudify.murano.CloudifyApplication', 'Properties': self._generate_properties(inputs, outputs), 'Methods': { 'describe': self._generate_describe_method(inputs), 'updateOutputs': self._generate_update_outputs_method(outputs) } } return yaml.dump(class_code, Dumper=Dumper, default_style='"') @staticmethod def _generate_properties(inputs, outputs): contracts = {} for name, value in inputs.items(): prop = { 'Contract': YAQL('$.string().notNull()'), 'Usage': 'In' } if 'default' in value: prop['Default'] = value['default'] contracts[name] = prop for name in outputs.keys(): contracts[name] = { 'Contract': YAQL('$.string()'), 'Usage': 'Out' } return contracts def _generate_describe_method(self, inputs): input_values = { name: YAQL('$.' + name) for name in inputs.keys() } return { 'Body': [{ 'Return': { 'entryPoint': self._entry_point, 'inputs': input_values } }] } @staticmethod def _generate_update_outputs_method(outputs): assignments = [ {YAQL('$.' + name): YAQL('$outputs.get({0})'.format(name))} for name in outputs.keys() ] return { 'Arguments': [{ 'outputs': { 'Contract': { YAQL('$.string().notNull()'): YAQL('$') } } }], 'Body': assignments } def _get_inputs_outputs(self): entry_point_path = path.secure_join( self.source_directory, RESOURCES_DIR_NAME, self._entry_point) with open(entry_point_path) as blueprint: data = yaml.safe_load(blueprint) return data.get('inputs') or {}, data.get('outputs') or {} def _generate_application_ui_section(self, inputs, package_name=None, package_version=None): section = { key: YAQL( '$.appConfiguration.' + key) for key in inputs.keys() } section.update({ '?': { 'type': self.full_name } }) if package_name: section['?']['package'] = package_name if package_version: section['?']['classVersion'] = package_version return section @staticmethod def _generate_form_ui_section(inputs): fields = [ { 'name': key, 'label': key.title().replace('_', ' '), 'type': 'string', 'required': True, 'description': value.get('description', key) } for key, value in inputs.items() ] return [{ 'appConfiguration': { 'fields': fields } }] def _generate_ui(self): inputs, outputs = self._get_inputs_outputs() ui = { 'Version': '2.2', 'Application': self._generate_application_ui_section( inputs, self.full_name, str(self.version)), 'Forms': self._generate_form_ui_section(inputs) } return yaml.dump(ui, Dumper=Dumper, default_style='"') ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1696417899.7011805 murano-16.0.0/contrib/plugins/cloudify_plugin/nodecellar_example_application/0000775000175000017500000000000000000000000027645 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/contrib/plugins/cloudify_plugin/nodecellar_example_application/LICENSE0000664000175000017500000002363600000000000030664 0ustar00zuulzuul00000000000000 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. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/contrib/plugins/cloudify_plugin/nodecellar_example_application/README.rst0000664000175000017500000000124400000000000031335 0ustar00zuulzuul00000000000000Nodecellar Example Application ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Nodecellar is an example application with a Node front end and Mongo database backend. To test this application with the Murano Cloudify plugin, the following steps need to be executed: `git clone https://github.com/cloudify-cosmo/cloudify-nodecellar-example.git Resources` `cd Resources` `git checkout tags/3.2.1` After the above steps are completed, the packages need to be zipped and uploaded to the Murano catalog as normally done for Murano applications. You can follow instructions from `here `_ to quickly bring up the environment for the application. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/contrib/plugins/cloudify_plugin/nodecellar_example_application/logo.png0000664000175000017500000014550100000000000031321 0ustar00zuulzuul00000000000000PNG  IHDRvIO/ pHYs.#.#x?v OiCCPPhotoshop ICC profilexڝSgTS=BKKoR RB&*! J!QEEȠQ, !{kּ> H3Q5 B.@ $pd!s#~<<+"x M0B\t8K@zB@F&S`cbP-`'{[! eDh;VEX0fK9-0IWfH  0Q){`##xFW<+*x<$9E[-qWW.(I+6aa@.y24x6_-"bbϫp@t~,/;m%h^ uf@Wp~<5j>{-]cK'Xto(hw?G%fIq^D$.Tʳ?D*A, `6B$BB dr`)B(Ͱ*`/@4Qhp.U=pa( Aa!ڈbX#!H$ ɈQ"K5H1RT UH=r9\F;2G1Q= C7F dt1r=6Ыhڏ>C03l0.B8, c˱" VcϱwE 6wB aAHXLXNH $4 7 Q'"K&b21XH,#/{C7$C2'ITFnR#,4H#dk9, +ȅ3![ b@qS(RjJ4e2AURݨT5ZBRQ4u9̓IKhhitݕNWGw Ljg(gwLӋT071oUX**| J&*/Tު UUT^S}FU3S ԖUPSSg;goT?~YYLOCQ_ cx,!k u5&|v*=9C3J3WRf?qtN (~))4L1e\kXHQG6EYAJ'\'GgSSݧ M=:.kDwn^Loy}/TmG X $ <5qo</QC]@Caaᄑ.ȽJtq]zۯ6iܟ4)Y3sCQ? 0k߬~OCOg#/c/Wװwa>>r><72Y_7ȷOo_C#dz%gA[z|!?:eAAA!h쐭!ΑiP~aa~ 'W?pX15wCsDDDޛg1O9-J5*>.j<74?.fYXXIlK9.*6nl {/]py.,:@LN8A*%w% yg"/6шC\*NH*Mz쑼5y$3,幄'L Lݛ:v m2=:1qB!Mggfvˬen/kY- BTZ(*geWf͉9+̳ې7ᒶKW-X潬j9(xoʿܔĹdff-[n ڴ VE/(ۻCɾUUMfeI?m]Nmq#׹=TR+Gw- 6 U#pDy  :v{vg/jBFS[b[O>zG499?rCd&ˮ/~јѡ򗓿m|x31^VwwO| (hSЧc3- cHRMz%u0`:o_FlIDATxpTם~ĸmlm XpVΘ REѠaf|ϡ95qO%:>ܹS{rGaO%"Fj".c5Bc;4Gq$w[$ђk_U]1HkwOsFGG^sn#,GX! `9B@r#,GX! `9B@r#,GX! `9B@r#,GX! `9B@r#,GX! `9B@rG?tlT+)}Gn~7'PqbrCEGT)ɡ !qM/JXR$ L! n]ۤKu,J}SJJ B@0r92.ܮ8L$=L!G&3}qͮ0(`"qـ@Dj}Ƅ L$v>PwG&I;2 `B@pS#, .. 0 B@p&yL.6':`bvQ{;CLy !p'B@0^R00B@0^..gaI]9x1 Jt;_.H:(cbfZԝq`C K,.ME`B@PXdեָv@Aܢc K`HKC3a| P5Iˎg;CZ2 If3rgT! (Zx< urûR a @Ǵa9L?;*7 *idg(AS9qTr " (B@ J*; ʄH8^\* a`&@Ij\0fH[" ( B@ ٽXrä(|W]  ! +#KRmLh, 1dśGxg Ғ{Gҩ>_$GJIS#1`h9 O 2r)?IHX5` I`F:~xkZfL]Zg"(]P>j,$,H OygƸ9CtAEz|;rtЂt8C@E邪 ւ%Bng`LQQ9)Pt t{ty.?(;@I)PB@-F-*xAIW lgX E(! Fe`i;3aI*>;*B_S8[.Ɩ ˝!vY)NkoܙjWǽEƲz|.M]x TZX7]*͒ݰH1]o5-"TZ)MQ3wkf3sb@%]3]Q[\^"3M1+0JJ|Krgz˻!<:*+eX{2 ^![% TJLpup$o klAߜߵklfx ?57d|%{Y3w/;7*wplx{TܿN*G*TT̐Ɛ `K.>ƶH(El@(%7a2rg[ iN2L Ryg]B@Lfr4<[(NX%,^t' l*0ŲP8 v%Jkliv(JTp~կQjby0U,;Gcn72޸-rSi1V' @9f+#r46/) ?n13*] R {(%G(NݺסK˄PfZ(y:, ! Tv0r Kfžu/)FEy>"#7xRrCcɭ-b@KT>t`-ƙ‚.)w'{w*,7 ge_T\M 1I?\% " /&i_~VEJ3-e݁q޹sS I%Y

1&A=r7&pzr ]*Hz`v Yݒ]nX)oҪn5+.7ziVR1&@$G̢tERP/-7PRJ,$wWi0}kDJ& 4bvb޵PP=tCQb&̑V0 t=f["tuޑI+;;?kDvzǵ;$S0&Aݢtz7IA`2`]jHk>*rg/&e1;Ŗ3A CbT}n e?$B^I?I^Rc䂬{M\ G fom挎 C~ !iy+.+\hP f1ߗܽӒ^{+%!J%?#UL@@5uA# Z-ղ_KIzE[n fMWR>QtPmTu¯:+crg0rG=Rg+;ށ ˭+ dǻTeT($00E!,7!  nF[$ 0,2{[D`$B@)v*xKad@[/R t-pn`!Qsߘ /)W3pL Pݤ%p"݂%3ԾgQJB!pSٿـ[n x?DꦩR2}آ:fr vcB@7ӕaF8yX|x)-]b@! [9?&;?,fQb!{X, Lj 91ЦsA\&yNK̀Kʭm h ^r+_,D06OJw2؉`Ҳ{#0Smxl^ϣ{>! 74@ɞVf6: zX !T3 v*%bdgᅰ hz}HXP-Qٹ`{nٹ? -<XP-6̈}q|X;΀\ 9؁Mr 0=yc@i{.@ Sv.mʭ{$*fERE p`,,rT! #)w@cHXPi ٷi>ˀqd߲ʘTH! l)-ogX%-@*_*).wv- (+@IʪAA`X, _#TmbX VFKSa ""TJز[`0,  F"TJ²<ΐRkE(zD tʮJiRk-+);] -! bg!G*aE.YRG6ـ;JiKP $@)7 gnqd-P{dOΒ`씻O-b6 ! gˀrξA{ V59Pn_82o)b6 쑑ڢ] #SX,f P pBnk) >A2b PMc6`T, k0 1|PN,f P] Ȓ`ش?`%,;ff,@#0ig(qd p;Jg~>p“p]Xņ%ـ`8B@@[p /ɞddG0PbG #& qǼZM\/ny$9%F>| OJs#+@5½ce,6)׬vٱJgw^D}~^Tf'oI&#{żGTR H14rh}| aA`r&8}{\OjCn%B@CTn}%;%ؖ~0bs;v&o{Yx21pZ@iZ7zo otII푴n% n\/V-5 ] bEiN =f?7__%$x6iq_$]c3{JD?TF(AJ/k;&Gniy(t%q=qXq? k٧3# TI%EX7,wOnn B8ÈI3SP:Paa߰C&Hʝ}2>?&Scg?@(1B@vI%];/Fy !Tpg~(-B@(%=tmBEPg ! ƮK]b2LO?g190@i@K:*w,3;^2 Ihl/7tMIwѩ`J1Pz0{ _XWiQϟqw]e@3-X _m}y#HT6#8@ͣ `7L3 W_*ph@@b֟Rt{rgKkQ0[4P.pw1o7&=>m?AS5N(B@>a/6쑖C(gKPu-+~c 2"\LΛa3tG p|6٧1q$mk2T! rai}A 26 Ea(#B@p ]`Ik>k%.@5G신!`^Y-)nʊ*.KA 9d3gFGwHP^4a1ct 1~7C W?b?@(3B@Au m0gy*'PCo"E@y`'CJg~T08"@]XAKtơ(`ϯ[\ ,&w_;]In؉ nI;J `X9t`eSR2}4#`}Ē*L-M p~Pl_]"8t`$X[R =T! [D+㶇rtvB1Ib?.%m 1v B@~;0LW`2t`Oc8w !g Q3s.`(ij8C C H.m1 T! ?1;i5a5+@.ˀp:QzW-nbyt!p$w#2ћm5qjeQQe_!tr~!n4/z;In^7qO YH|)CN1PYt7)3҆ Ot#qnwgP*g.-7\,??qvYTn.Lsz|Sc67J1PYLF8=%Wcȝ ;&]n(@0`qߏ4{J{rg K.A BzK1Pys"˝M(#wTr7P- 9ii:D82rrC(}V=@0IBT+ 0k7I;@}R5We `ep9hl_.)Iv EһE!0$$h-X,ӱ T 3T[Xnx?d4d5i/+R T! j;*ʈ̞#Pcp@5u] 7{4V`26 0@Հ()wnQ%J r4@}}SQ_*g>RRӤB@V*;r?o/n (2kˑ@pJn 3bp"PI.G쿍bYTC]Ha?@2B@+ ǚ3(9Հs+P@u1LQCΫUp@ ;o7 F-RrT! ri,T f?@0! r} q KPn;Ty)@9ټ Df8aVUnCF F P.e~LIOCTbp3(,=LWg>T\Xw0! r蒝KޛenlQaP6 JCvVN ؛fP8{3S0! R jI; 1/sAVy:Nѵ`B@O-Ndq hgA(eǔ Mm0|"G8E9>ˎ')@'} ̑UY6@)t3/n UmO3|0<,] f!0[aU [WQ{oZ!E C`:dO1~n|݁)Ijv90f#*{fn0[~0@;40/9 !0&aH>n{,l9lbH<f**)aɱ-,aH).7 g;"0S]LBPl׮2yo"0q|uOR`&?b U j Lذ`odq۝{3 !fGܒpCP; p ! J-7 ,5L?`ٳ 0] lOa"P);UeI=fa2gxT #P)B!L vɞY)Q eGe>(*ɑ:[inZ1ٵ쐥%wTZZ ˦JX Eb81A GTCfvӍ` m )GmxyS4X 6 ˮYעx_7MTCfR`>ٵXJ|hYu]k.XU*p[웎n$,;&fX]we|IȝbV&,ˈPvs=0M+ ZZ".7Y(FX9'L%zMYS1a%uXvLk:vcvʷgSRR]hFX-ܙQË jpJ$^*p;5')%; t2j~pJ|w{[;5!L&)fFaja}1 |dzvF l ۑ[(7)gAMfË8(πu,@B@wA`a ]ؒ2 1OPb ! 4bLCVاm'QmT#f/ͣ L">7%.C;fH:T Ֆ1쩄,@)B@j{n~ehO AJb*)Qy>TˁLv6\aXd\0|Dfr?Ӆq׆rU7#L(fT !0x F0f$fbC6snߋɞͯ3ץ}d:GTIl;cnWvٽ<`jeo%=J*eٹY`B@k* N Umq'@%` B@pnH([B`1NX ˎ+-wV/ƋjNMh[c=/.Q1lPpB1 VS`B@=YƟ$Pp=b4`F/^7]t3X%(4GUmTLST(2d'V _BpgFòwn ! x>W=&`b4}Fy "PT߷OT P@G#A7aq2,F ^7a|K9AG#At *Sv*D s8m@lw@ޫXTOc ތ =`eA][*"S.-`[7)w& rdțQ1LPpmˀ&t' e.Q1L=8L{{TPMQaH"}CTwi4ts@h2=aw T[@$>蔻v+CC n`b24P1a3*¤ -M"ǿGH7}1C =brbx?8b@%ܰ;)7! lqY`lv;H[%]r 6.m')#@)DEvIȝ+H( @1Oɍ*FX]Z,3sD! X~FG7aU9o])G֪_o|_Tz~U,u͠E3[FnUw}sVGJOI) A6~`$jw G;T5]zջ\-NLz'@! 7~njL% j K'TÑ|>fزra1n:E ! \q}0z.1CF DJɀB sFh6V3r URNp;B@ n" ̢qG5W\񵄨ZTdiݨ%eg 려Q5` l?*AUMD q3%d~;;:JWܕ#7t ~.#,w\Yrr|~$.IktV"L&K#,wFnE!%iM~kt86} I(>P0~`՘/ƹ[r)Xb ]ȿ%wKr7w:#.!91cjv }t7ärZҵq{ºuFcEr (@Xr w{{$.NэUy{NqC_~n5 NWXP4 QBT &:Q':LJUGE8S;Dж2ˀ1;I_(&nq0 e>~) ~%/G:IVsG7C }%1KFQm!xYWQQQ8-@`B@n>? !)GuvUar}`)B@#w&uHKaKw(nI?rߪ?W-:L`ؓK,w?;(pÒ#F,F`:7/ɎNaI0[Bv/ Xk5Ŗ ,bӻ [p,1v00dpp9rHR ]b4[fK,j{c-{f'%$nK:J75%wo蚘K! #(R;GeϬ"L,-7p9%)Mk,:`9B@ӕ]A`w\܈Äs1fT_L :SkE_1ˮ )v#0 ˝}B A쓽@uvO?2p0,}pM޲J s֚(?G`7:,: eϧ 2r[  x?}״w\}^X @Z7fÒ.Ⱦ%}i{w뇟}&=8h{yn׿aA 2rg'l3L@3HzIvm.A 1*]v\} RSnN3X ]@̥ B1.!%f2QWa ̆#⧍"DA3\ Tmׅ PǖA J/&@\R`@)Ѯ`{nX$*AT76+ +m,|2 !Pʛ~㋉ 3Ppw؛faٷ 8#*g8JyCa WXБ!EoxLJi-R;#0psK,Yw`"6.wCvrw9G:T݁I-;g~ EA΀kaga81oٸ`|eN6a.2^B(C`xO݁]lt}!P5+(&7ig+,74cO)ظ`s#P. a@a?Pb ne>[Y! r)߮Cр?ˀa-Y! rtqPLn0SZQowX ^m^,@@R ]rUv3r$mT%rgp-~v2@%씔46+0̩`Xb,^Sp Tր߄fs*NX2Afu:`?3 V.0B@ zѨwyT-$7 IdoJػn%do%S*[K$ea10F.G{t mT^]0pL;SX{ufXڛSaؤ ,w@k,w>DB,l%n6bLXtػnr,@pB@ՒB&XO)g@zwX`)=p <(RnOӻr𹄂E.0 B@նCnCL] r(]2tU,K8[M 𹄂"`bbD..L%Wۧ`\KT]cۺƖY3$^"#w/@Iͣ ‘q#,w[nI?:o}r:t|OC|I"v L 6 ƽG(QRs'=|,]?b;N.(! Ӥ%Tq*׷i`{E_9;=CGw W$t2}ͨ`Tc_vH_<]jA|`0(! S%KiI}?n֣bjKK)/OGZTnIzb0B@&Kz_ +/.NwX)-ic݁NΙ97] Pר WFψe` @UPr.Q4U,D"J%Y\1!7vN@")@`Ғ63xa3w){L! ?I g.1{ 3;=] 0#&)B+1MoVVLnpHacI>0+[4[%%5LB(C)Eh<Kf_TR_Bgf.6aK t\@rg8 ;U0,7 1& { wtsbJ_A4,wvV?7.1+b΁0qs١+@)FiJz~|.l *O8^BpEa߰ :b;(!B@6)G/*]B Zaa/K'Gp%F6I*rª\8^T 1 O]bTt(5B@6JKZ!)EW 3`ܰpT~ݯ_heAVܙQt8ǧ-53֭_.R{N! [4ġ+`75,L؞qD%`Pf[>)#wS  -8.W<{N PN"d+sA+;0> &UΝ 0]2-;D*@d H;oe_xƂD[T>ʎ@*t Ӳ{%Ϣ B"v *@P9rlan?;ɝevA`aZ;մ5_Ι2k挎 @@tM.ZjsT%u9o)wEW;w Ҙ  LNyU0@ilf[ܙmr  ˝%Y;,fKRlEd]Jisb+TNZn*/;7&)%SהIn4YUr` X|כ}b1#i*03ބ\RrCS״' ku %IPqeVeeq( 8Euv)g+; ׈S׌gN%- 0CXuF .N].KPsj*0ިܪ1bޣ}:rCBy>@x1g}A18I:~*_A݁rrVVltGP-B>n]"d0A ɝ\+q 3{abaqłoX TQ+ 0s=Vȭ%HV!j> 47~;2K i]b/P-iKt0! =w an6J{}T5+a I[Ubw4g~>-7|*{ p `:B@(n !%;elϳO?}޹vx@n{7)7ZH03Px- B@f{[2rܴ P:);MW!hHfΠ[,7x6@)t 挎 @@tbKj;ʮ[ҏػ[.6GݥUPq.?-)frRF쭒x_|v٧-f13y=ngHNMajR,ǻ̯Pg٧OyiU-ˁ&Kc$7KI;!wypx ٪2f90ytVFaA`\ғCJnw>éQND t˝)6`GngarC9+-7;}_o̮OVhm@phݏk, IZ`*5+݈;r>GcA_Q+{o`JIڑܿb3uVhmm]7O>d׿t`W+ 8߷nF$\`r46c{֭PL |7pLAf@ZZZ}˳lw?i$)ϟ%KNw}ϟ?rJ]f"ż7qz m{UX3Zqy5k:|>??:/^,XC== <ބfQ6I.2JK(CT]2rÿ]a-mm|~k50{_=ho?9ӳ/ؙL84]H$wiomyˁg@3: :V')AwxCWC>ZQO]; fU ~Ȝ ͆l?+V Lő;sj00FgR9+V[j/_pc@p{>쳌(05B@P8 a`.2b/LjCc?9~|{.u.-~fgGVdٲeۿկ^d;ͥ 4$%0#iwUKK˶h4z3~rN8tߦ~2 ˑ[n Ks˗ľl;}wLiO&Y̨Skצl>|>?MP.[l$f"7r.=')wCWsO"L&LOإe۩SN8 1ի_gr`0{IG@%.!@ІƗfCA<ܸ,\4LKK˶|qFJ}r5Ojjjn.\?{xɒS,X=se=d^a4Ӕq˄t $)wj׿ylpp=@"ڵkuq#T{sܼ\.gLf~$EQ-:yP࡞TL&09It J$)?-mm?I e~[]~ڵ{\.7po 'Ò"K F.]zmBx`p''=f?`Z<.09Bjmm]~5}Xd2p&i /D"|kj~tkفaE7/ɝpw---/ϡ7܎p^~kW/] |fP6]%igB>czp @e$;](݂ۤte INSkkW٥KzzgDκ7{Ppҥmg?=N/@8rgw%],pχ~0mAXBw…e_Y4<T-*Ò ήOSrCUOW:V{;L&d{~g;= ;>+~̰c0#־xlhl|'Ǐor@L.wlp𙚚턁ۤuL0Vc3,?0##3e]@Ҳ968fx0P/=b  7i߀n Yf˚߾&f na-'~Rc +FGc3`f-+B?9~Xh4|Մ3>ywaM`|?KMSt?_Dվ70(- /z'WMr~:2fzlk[ZZvχ 1#L&{-mmӴd2azlr&u3|(ӧO__ \zH$_lP(tッMkK[[m>*I7nܨW]v77n@Ug/顡!^PR|~αg$0ӧ[:tub6-(sҲ}|W˗dg0} [ZZJ@5\mmgϜ){E+$I?j_5#@mCc}}}0\0y)p4u GFF*կ^?Wم ~@!`}}W1T tRܴ6Bц md2Fnv\q o7S'˙xSVodd$0ƴPh鏼F' ijmm]ɓ?  677w?fLmG^>ڵk&wׯ_ 3j&`KK˶@@OҖ֮8ɨfL@*r"QSSssٲe746|lpH$?~xɒS7ۅW^]1{̱PO+䞨"_C@[E"g.t-Zt;7yܓ-콐Zݼ%ٷ){kO];`J{N8q(ܗU[(=68, U FQ%KN-Xޛ{%UN7~߹vZ{_~!A%׿n2k֬I3 %1[:Ѓ ~L&Sҙ|zCp\---~GWs>`y6 ͛MhҥKQpM/=rfmm|衿;y8ʟV{9W˗ @L \SSs-;T<lii600u[;-]/^r;e/xmikuWsϯsxyLgU4ן[|p'/BO7/~!ͣ` {/\pRfK[[O65557׬Y>Pǡ##Fp}[ZZ]r`x`^2dK[[k'Oܖ':>344՛{exx\3---X__. 3hK[[ &oHEB aCDQgssL&೜>|~FQ^$|~k Ne׭[wpddd;f!Ӑ868l&YںaTҥK }nnnjkB²_¿=><<+646~&0=e B /vLCC,5árٸif0K-ٿKڏ KH3=rL&xssN@6O ʡ,!Skצ2Lo0_8I&ׯ_Jnnn8yD:! fqJa Jnik}?+?u@4u677`#Hp4|ry~9֮d2.; Ft% 777w 7Cp' 1: 6vVh^Ph_{{{QC]}͍!Irʢ^n}h4xɒSt}?7`-mm|~7?U>Z~$)ɄrPSSss…ǿ/Z{{_YF|KAҲ}zwry;8f2&ۦ7hY` .Z[[Ϸ^~KW\Ytry\.<뿢Ѩx >~Iޏ`CY Gy`׮]kwd2 f ÒVIj/|_.fgK[[׿޻~ҥKynS& g2I B+W<>f*ӧߴ|s:<js=C vO4Z߿x _d$*sM86|~ȧH$bŊZx_r 5Zϝ[Pss[Dv8땤\_k&ŽϞ9Xf۸i1TdqӦ92ϴ;Fʕ+_xLQ.޲I P+W<%? ,|hJzxɒUDT{|%`JoK[[8|7c> jDx5/^g͖/P7!K/~5S^Ku_ri0L%K~Uq7[ Hۼm__i<Hzܣ>Wy~{n3g"&j 3eȷhSN2`N)~nKdžsܼI!<߿֭;@lhl|9^*ۍ~4e~|3.^eJ[jjjn:{aZG>?YFo7y555=߿ϴVL P__&oj~]868LPf2gjjj~K/9BMMMScbfw{ϯ4=Oϝ]:2:y֮MnWcl2>r aM^GnU?1-e6p9MSfBp8-.!M^4ɚ5kt֭;x'ɛ@6A7\.7#FlGjٲeMr<|Miʕ+VN_5ϱ0mܴŚ a_Jg hmm]FGWL&>Lub}}9Scb1 kiiٖfCGF)C@/KRmmN'GѫGsn+"raݺu &{jTooﱠW{f0ѲHۦ!fyw٧1:'MnܴEfן=$}M^9CCCH#0Ma'ٿ3+IwR 4s==ihR`f0&'Ǐ&zl ss 2LYLf͚4-1iRAcώIC@Kf&Snxɟ3+@lD" 7Q^LCu ӿoJ[lд`䙩0+TچƗg&6Y?L@äI"K~ل!sի`6xȑAn644^z%f(-mmo|fCc j_y(R]?1-ei/^) r*׏93g"?9~DKkkN*z: ˃Z6EoW"HޔP :՚`,mDohLXSSsp'j׭^} |oCcRjiips*I|ׯ)G˯_Е+Wټt:9uTז>sg [j/^쒵L&L\''LhW Q) /4⾨tj|PGr9(eU7r/YrjѢEݡP࡞\NL_Ahmm]Q>\{/_^crl6:s̫Î@;75BҴwnܸQzwR`({j^Sq{<7$3 B4~?0-k֬ǡ777Xn])3d\-[ݔ 0*3s7ؔzɿ+IW\YTXϾ) ÿnFFFܼoX oˀ  ?[POš\3KVzz.a8dTe)Bp8۹dtS@ɽ12֚w{#~(4'N777{a)NSfZ^@I7y HMA7HPh(W/nܴ?8{Ŕ)=A˖-ׯ_2N/pҥ1mLf20o;M^^awȑj)m@CҖsJj. [jO>3mgr&?N>&p^WuWg ~eʌf =IJHۥ9f  0[V&9S@*e]9s3}Lr&!3 H{ʕE&< 8s~״6[pvFFFFi2y9?vׅ^#֭[MO ﭷzޔ[K׮\uSRzu^lٲ졒QQ 0I[DD"|98?t&1ު.]7eU?)%k/_^cJ[_2"qbCCÏLiQp…3-ͱgkjjnЖӧOǂ<&F9~.! 2iW}}Roڊ m5 ;o.FfQY_ӕlhl|9c|>?{KB@eҒRn0eUWJҖU?\f Iǽ,v~6! zgQ(iހ&@P]rߘЎxYڒdApu}& =! 2rӊ;Fu鹸r&j.Ĉ[o-3Og4ӱ|rc&Y QSSs#/0"裏|+ zZZZO( bDqȑ( B>PYs ҵkMh){Y__+H$_v9---:>?4⺰rC==_-T Ĥ xA{'MhIي+Ʉv :L'%K~hfbٲeMiKP {?6-XT?\St]reQD1ŋڄv/ P՛ F"0Szz.ן3-A(Ҳ-͆LhK4u^)zT0kl?ϝ`4{܄%|rF&\L)TeȷMiBy睿0-{VhÄP֔O(o5,N ߖZS BcVw͍FO3~2ᅺӵQEuS*WAr/Ў |֚5kфv\ qoR _2b& 7LÄ {ˌYwBj_}^ѨCU`@) ,]Zېk xׅ,9HJPOES@S[:[bs?߸q9bPP`$cMիWW0@ ,xʣ> Sb[쟘Җ{lo%EA0eyLʕ+ rZZZR__+H$oB[l*vddĈ f+X|]S@1>䓥n)c) vL`|1M<Jm @iJ[Qf}w)2>KFLzz.)3 vZ{۰pL)rʕcC>}Mi˃=Q9w#>p6s&?!L(b$CQ@9T 3a>’T__Xl 0 3Mw[xj_*w}m0ae N;Li˻~K T\S67eg>v~{bFV{<:&s~lmm]oJAH${{)rƍzƄʫT6ۂ ^v(xT ʘP`=MhG.qӦ؇&bfdB n{T *{CR6PPN5@/~CSf1BcVϕ{W#L&Ϳ|3O/m͚5i!7mzєZǹhѢ0 `${^aK[[m`BEry?5-~+bj\Ɍf|~NKK6&s7z~[@?nA@imm]̙ m[:[ s%s*yj0ŋ/ @cB;T ĤY˗/_6̩̕hv5ki d>))=d)zP Gy mKфv ,X rʿi p' LHftߘҖeȷMi˧!P zB1D;MOCPh sRjL)4ab z7o=e޻~4L)4!W ܛo Bp[&-!EΚШϯ) pC==ϙ lf!ڲf͚)A nMhK&gJ\.7osss myw#i-!I$0S7nܘO/f;M)r9jaK[[)+KѨ{ܴsfD 5aϟ_5\v^0)Kp]L)22284 Ȋ;&/w?yZ0a 30ܸQ`?#TKT[[ÄvT@H5 H( 244 ҄f\& W<,#{$h1-L*r&)44mIp>oi ۂ ^vg 3l3}}}0@e,Zm0|~hB;*Q Ĥ¼S˗wxxJ@s=W݆\.7fG5W l  x >!ϝ]3:p~L=rnfs!ҥKҴFӟOk Li&Vf^8 ]d4Zpu l/ں3oU?ӹqSN\.7冀kS<`zxɒSn hq+SL (Gw{/L%? ?sK&o>yD)U T}w΄vlܴEFիgB;Lٟ7ۅ*qȑL.Cf))AV HAQ!Ie;uTOm x~޻~0ŋP6D"<#1ի+TP/neRsoǥY_6 c=obb0hZle@b.]2fhiif›|PyM.?~%0)KUlT+9~?0jjjnqP!e˶x, Ʉ vrMh… G :>c݆|>?"=^zuք;ǯu){YxNX__ă8qe#f XL^*O+L K%i…g4ܸqc>Jy `I0$˗/oB;Ο?rͥKƄc B>Pχ˵;L<\.7̙3cѢEݶ\0;7mzфPhSe=TSl6 Es]Ĕ!|~l >}:fB\~<7u63g"֭;SS_( _҈*LX8I$Juzsބ}.Lcr>cq<0wـ444>,@p##hmm]̙ mYtcDľzN\&1el R$:~}!ɳ%_} (R=/^2- ?$#rO>d)P:---ێ >COL̔0$&xImcܙ#g9 amJ[ν.F՞?~ mD" \~KҲm``{L .L, ޹,\ Ph~>fjS*{ɟv=nB!IL&̬z/ȲeNG .^6\vu՞:u˔sB$s]V;X ĤAsg-[݄͗'fCjkkfJ[OHe7iOKtUCg9cggK[[k'OjYb?fL!|ӔA<7~83gμJXjgsu{zqӦһ_rLiU BåKC8;p[&gxx"!AS{ńw+֔B&M:!+>& )( imm]S?X644}UOooh4Ԧ~կZSZ7mzѴPĤF9sфs.W/ ,չoڇԯŇEz;~~)Cu2% \_gБ#G L$cФ9hiiϯu FMm4K^3,0`,DmR{r<(=@(N8BN>7;w-c?T?#/ԭ^A9ߣZ677wD" >_|֤!foK[[SL{Y&7/YrʄvKs:iZgDZ6 LIB\MȔ=Ken)իe|u^ں>^=߿bb֑UܖNGyOЇ ,0b}.gbB"Hƽ! 7D"ػZ[[׿{Jlٲ&~ ]/ן3 x ,)mycdd;g.Sn8K\^;y?5lik[9̌)9L*b31?#/Zx}׭[w@?I'} pzL^i^cw{^ry<ƴl;r `y8K5 AvϞ97`LW/_oBBgm+>ͦR̛h4zYyHۙ2}"ÆVFF>6^Cc^<;ʤdn{FQX4VX*EDwwU**R Ґ wdzB'Umc1 2 anz<̌ߘ2@`v%x#K… ?r ˁ z;[@斖R@zͰ .{f 9PBl6P7x<;B2gw1 Pbv?^3>_^oj3wʸ7^gVz4uP_:GojJel=#I$J(|%¥mUU/r2O\x<^S&cVJdx~{}MM ӧL0xn?"si+(IBduժUGht ju?` ,dzn D"mC?djD"[kt"\_d*O2\75]C7pG=ѨLVH${ k]]ϨYNg+//yxHoJ,kЂdmײX>s8Tl ~j}SM<ώwOF*2K]YYleF#|% ~ xn1W\/vw:?8׷Om3#__Qz>(VbXC xfM-/zN+^d!"v|i{=Moz}1=׹Kg^fM'P0xPã4Tx_^} jkk{; ~P|F~ezX(|H+UU߳T8Q[[뛚.|p)C!I!rwũ';Rf՚D"ۍPobr &?sM^aZ@f;WTUP:+3{b@ po>[j@EQKss. [k'''sJz)j俙1;D%kX羰%I!kZS˖-;t}_/go~W._X饗@KRTY4uDсMMUVݵ_rpppۡ`P† 4 ~nT曳*++/e{hA3g8z}-.ׁ}}x\ZuN'=3>>^-`@'DE| ZIs<5!lV5K'+**-Z4p{/s]nn!駝_LO[rje"P~?"tk5/^gH{;CCᡡ쩯ZRe{|CMڎm\Ob=un֓nZb^o_!aL9o}/{6Q'[k]w߼xbsGth1 hnn~hJa@m^!Uq~M̬\rJ![0UQQ1<߻`}V?/\yڵogB\zu `H>ߘyrrrWFX—J\{{m6dՊWTTSB<ȼ^oTʫM~&y뛚F9 v x(ʞ5k^grw_ΫW/\`d@4hZ'N/\QQ1dɒ?s=gm`>3727g{| L҉u~:А=eꙘ>AheZfL]ڵk˵a|3--c:(8~B#݌WR^Wm^4õlI3*;98z_O qc{f (2Ͳ@ـw2cYCY")D xL}GfAUU Y<7Mۖ61rWe@!Eэ6m?=#x.tajB~NvbV}}:;;`R7Z)jmmm l =@ c ikk;FMn廍ohxlZ.cQ@!|c7n[>:n1A wD՚r?5Q{nPϕG|+3[Z0bsK,P豆cJhzذaÓ}fٻTbX#@٭^ڰA2>bA42P(@(@!x=OMJ8j#VKflr=2n~46IFN|eUҥKQ3E Px546fkRUu: 4c{률ځ!LP @s#xBM 6SxIQ'Ș|hGڞz*l 8~X m!h\,9sjB>{=-ZD`HꋪӝOd|czE|ެ# !69rR2nKDcCw͸)(n-xv?;dL֊k xP-Lriimb;3`!9_F^olxh~K uN<^F:6H{{'8(8,llEE0WcsKRw'ӟ#X BTU1}r@.( L&3iŐi |ɄZZ[{>?81*m\%K@-N7>0Q\Zl&__tyFeohx4`PsG].PCDb1˃壪2B['1ng}GM^$xG6sYB۰z4pP렱X|hkk;FF7<G必 rGKkk"X<>Kr? GQ-.l3gɘYjy}SS R-]nfMv8佑Hd;?K(ة'z|$x䏞knn--r3_|(<^o2(3ZK~!p 7zwdr}krᡡBC~gr'e˵Dl]n,Yr=p@(3˖/M}*htc**p:75%:éT?ͩ'r ?6-mmmTUp:kbES;׷/LKx{$܈ MD"X%nP {Aϴ`ƓӗH$r9ͼ͆H&wMſw***Q?`ϱիW?GK݈ ^d2ykn!!7è-.~=Ηr"8wO{vbF5R}bI~ f^R{g}Ky=6PmUpw ! !ɉo=L&XTuuޗJ>116X(L}}hR(<D(:^w=rFN>x<^Ҳ;O:vリH(5KqQ /6˶f_LFr !q]pQNpA UZ,[dz}DD([z2rBKア(3k֮r|ダh@>66ϣTLϴdu_vwNNNx̙' H֭ݲwޱl@|cv}DX̰حtj_KSSS._/̬\rjŊ,Y~ya-uRTUS}}AF-sk O~ϴel6ۆbNχL=B-k\ !-\h.I]p=RZ_b/T&O++M$H[Fm2UkזqBtkU+VhѢ-^d=_z`ZS˖-;oQ_,\^'p||[CCV67S YhAd… 63ǹw/2z+BOTUB3HfvSSS?3EfoTEEEF.\SmJ3|z'lX7>>^!J6dɒ++**1F [Z{jjj˗"_A zzۯRk׮=|ҥ xlI;>4Rm6ۄ,qMMᡡ;:z m$?iB\t/K{r77_ 1vp˜ٛƏm\g);Vkj… 1o^ mkwwm*ڮ=իWd Զۇ+**-A ,erB+B@_^r[\n c)M&27w߻,݂y<_~%|N j`A@h4UUN&ޭ;(~mmm"{Bs [\d:di;#?~,e#lvwݵCCVYʳzh#}|AYbZSne)KCckHf #]nnYQef U2CHH477Gd jkwwh,婩yVA@ST eYԔ'h]9}Yʲjժ#Hv<ώD"PUUUCw4>>G74Cd hpp!YR]]A@V,^oVA@d)K-%><4d,V5 *![tOd)KCckHFGFTVV>O d2@477ǁ CKߑ,555"C7]d(f'hY@@~pΠdE:;矐b"ÿI?7!n"x9:-ثcBt !}$}g"wwes߸i)LM߇s7Z \?Fy.wK?FʛBCf^=r~ʕeڵ4<钸lB։ &]msx>-y%/g.ڠmm{ 2 an;;g|Dߕy03۹c2c opp2CUp?9h/AqY{ o "!!w&!ĻB6!{W'3ڠ^ΡܲobTIrNQo)'т60T/kDu's{ dƾ1jLJu&H磏>,eYQU-|e'%-/s9NHK@= ;{pN@e$v0LKfz26`vw׎P> C)s3[lmqd{61L\3E/A 3S軺clLNN8JPj%Z$9$f7E.{ ނz mrgG6E65s }WO"Cܹd(ӡ` -? wVKd!_$_NݜL@O r~m~!alwJtocz~ eyn?2(yD=VGvL}i|Cd۔֧uf]2ϰ\F O󙡵G@A!>zPk.U c~!W0M( .Tr(2SYY<-_8_ oKMb6#N:dyB]`.b nȧqnexot1`BP^o/c9]tMK,m]kh< (`|j0nK 06A?\/r Hmm퓴H wlvG˄'[rY;^OSi潵ks(RٮyO~Ƅ/P z)P|2ɋ}_|M{Y?%U? cm^,fﻅk۟26.&?뛚RX|lh^@ mӄ( 4,q<$n%rHdɨzLqq!5,8p3]cJnkwwm CY֮]CC4Js+ w k7L*MUuf!Ǧ:ich)~w}Wʾ cclBMNNXr: " `E:vo$8ی=eob-'i.L %7re(Gssoi#P'fuz!qɋ2ͤ22G(2j=mroSgOwa~؄x<;Rr(2lݴH!8_ŕhDeT=^I^67D*]cJ棏> 娯h#(ҥlb$20pVdGg[:-uc9f06&h XzsFqp0e/{Չ2۲Y @qe8+r$gkiBM7Vm4E(<>G-DߕڭybNimYn=؄ܹd(ǚki"a90K/üuEoR|(+?b6i_[kJBQ!D{+K]EQfZx d`c~ w"2_6 @._@h"` P^]Y. 06&#2Z\}dhg٨%sW rwe06&}}%ojJHqcD:L*~!y/]]} %q2cժUGh"c90oI&r8w /vw YK]UUEL@0m) *aSwAW2!8P,~!ě2J]]QٌIK7JKuW_rPr4776 %@J:C5Yyf $LpKzdw<"e`]+嵗w06!-C9-_( P|gu\m6%2(B*`'{;EnLYYPt/w'ɒ'^o) `Nn6]Xl "S·rh9=u]=Q[&Hb"#ʱzh!&U'fj]"勅Τ+یEn{cI-3:d16AboVh}aZS@(-R:|Nqۿq4ac6gR ]ڜKFUHgG(lnP4?NRe.GCckFis9-xZ^:,A6(~7&!Ğ,! :N` E3rK]EQfZ9h `9L/z jm63U K̗jBt_$ӆ1*;&G' hnn~(T?&Cy\fԉ<+3A 6eJ5+ XזMy'ғMsJ5>m{,]9(m? u…Pg( Rbe333Εݸ=z?@6-#B3X `0+2gJ]UU5@@8z}xhZr74Ckȃ \|'2z'!2:2H`Gz}1ZC W_}!L.(u9֗h 0˗.}eZP0x֐ A@x뭷jcXC!TǗ_iim/u9*++5S633C-r`G08 `p#A@G08 `p#A@G08 `p#A@G08 `p#A@G08 `p#A@G08 `p#A@G08 `p#A@G0?e(A1IENDB`././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/contrib/plugins/cloudify_plugin/nodecellar_example_application/manifest.yaml0000664000175000017500000000160100000000000032335 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. Format: Cloudify.TOSCA/1.0 Type: Application FullName: org.getcloudify.muranoapps.examples.NodeCellar EntryPoint: singlehost-blueprint.yaml Name: Node Cellar sample app Description: > A sample application built with Backbone.js, Twitter Bootstrap, Node.js, Express, and MongoDB Author: Trammell Tags: - TOSCA - Cloudify - Sample ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/contrib/plugins/cloudify_plugin/requirements.txt0000664000175000017500000000003200000000000024716 0ustar00zuulzuul00000000000000cloudify-rest-client>=3.2 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/contrib/plugins/cloudify_plugin/setup.cfg0000664000175000017500000000100200000000000023251 0ustar00zuulzuul00000000000000[metadata] name = io.murano.plugins.cloudify description = Murano-Cloudify integration plugin summary = Plugin to deploy Tosca packages via Cloudify Manager with Murano author = Trammell author-email = trammell@gigaspaces.com [files] packages = murano_cloudify_plugin [entry_points] io.murano.plugins.packages = Cloudify.TOSCA/1.0 = murano_cloudify_plugin.cloudify_tosca_package:CloudifyToscaPackage io.murano.extensions = cloudify.CloudifyClient = murano_cloudify_plugin.cloudify_client:CloudifyClient ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/contrib/plugins/cloudify_plugin/setup.py0000664000175000017500000000145500000000000023156 0ustar00zuulzuul00000000000000# Copyright 2011-2012 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. import setuptools # all other params will be taken from setup.cfg setuptools.setup(packages=setuptools.find_packages(), setup_requires=['pbr'], pbr=True) ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1696417899.7011805 murano-16.0.0/contrib/plugins/magnum_plugin/0000775000175000017500000000000000000000000021105 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/contrib/plugins/magnum_plugin/LICENSE0000664000175000017500000002363600000000000022124 0ustar00zuulzuul00000000000000 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. ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1696417899.6571803 murano-16.0.0/contrib/plugins/magnum_plugin/magnum-app/0000775000175000017500000000000000000000000023147 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1696417899.7011805 murano-16.0.0/contrib/plugins/magnum_plugin/magnum-app/com.intel.magnum.plugin.MagnumApp/0000775000175000017500000000000000000000000031503 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1696417899.7011805 murano-16.0.0/contrib/plugins/magnum_plugin/magnum-app/com.intel.magnum.plugin.MagnumApp/Classes/0000775000175000017500000000000000000000000033100 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000022200000000000011451 xustar0000000000000000124 path=murano-16.0.0/contrib/plugins/magnum_plugin/magnum-app/com.intel.magnum.plugin.MagnumApp/Classes/MagnumBayApp.yaml 22 mtime=1696417875.0 murano-16.0.0/contrib/plugins/magnum_plugin/magnum-app/com.intel.magnum.plugin.MagnumApp/Classes/Mag0000664000175000017500000000520100000000000033525 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. Namespaces: =: com.intel.magnum.plugin std: io.murano Name: MagnumBayApp Extends: std:Application Properties: name: Contract: $.string().notNull() baymodel: Contract: $.class(MagnumBaymodel).notNull() nodeCount: Contract: $.int().check($ > 0) masterCount: Contract: $.int().check($ > 0) discoveryUrl: Contract: $.string() timeout: Contract: $.int().check($ >= 0) Methods: .init: Body: - $._environment: $.find(std:Environment).require() - Try: - $._magnum: new('io.murano.extensions.mirantis.magnum.Magnum', $._environment) Catch: With: 'murano.dsl.exceptions.NoPackageForClassFound' Do: Throw: PluginNotFoundException Message: 'Plugin for interaction with Magnum is not installed' .destroy: Body: - $bayId: $.getAttr(bayId, null) - $._magnum.deleteBay($bayId) - $msg: format('Magnum bay {0} is deleted', $.name) - $._environment.reporter.report($this, $msg) - $.baymodel.delete() deploy: Body: - $baymodelId: $.baymodel.create() - $msg: format('Creating Magnum bay {0}', $.name) - $._environment.reporter.report($this, $msg) - $params: name: $.name baymodel_id: $baymodelId node_count: $.nodeCount master_count: $.masterCount discovery_url: $.discoveryUrl bay_create_timeout: $.timeout - Try: - $bayId: $._magnum.createBay($params) Catch: - As: e Do: - $formatString: 'Error: {0}' - $._environment.reporter.report_error($, $formatString.format($e.message)) - Rethrow: - $.setAttr(bayId, $bayId) - $bayStatus: $._magnum.getBayStatus($bayId) - If: $bayStatus = "CREATE_FAILED" Then: - $msg: 'Magnum bay create failed' - $._environment.reporter.report_error($this, $msg) - Throw: MagnumBayCreateFailed Message: $msg - $msg: format('Magnum bay {0} is created', $.name) - $._environment.reporter.report($this, $msg) ././@PaxHeader0000000000000000000000000000022400000000000011453 xustar0000000000000000126 path=murano-16.0.0/contrib/plugins/magnum_plugin/magnum-app/com.intel.magnum.plugin.MagnumApp/Classes/MagnumBaymodel.yaml 22 mtime=1696417875.0 murano-16.0.0/contrib/plugins/magnum_plugin/magnum-app/com.intel.magnum.plugin.MagnumApp/Classes/Mag0000664000175000017500000000654200000000000033536 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. Namespaces: =: com.intel.magnum.plugin std: io.murano Name: MagnumBaymodel Properties: name: Contract: $.string().notNull() imageId: Contract: $.string().notNull() flavorId: Contract: $.string() masterFlavorId: Contract: $.string() keypairId: Contract: $.string().notNull() externalNetworkId: Contract: $.string().notNull() fixedNetwork: Contract: $.string() coe: Contract: $.string().notNull().check($ in list(kubernetes, swarm, mesos)) dnsNameServer: Contract: $.string() dockerVolumeSize: Contract: $.string() labels: Contract: $.string() httpProxy: Contract: $.string() httpsProxy: Contract: $.string() noProxy: Contract: $.string() networkDriver: Contract: $.string() volumeDriver: Contract: $.string() tlsDisabled: Contract: $.bool() public: Contract: $.bool() registryEnabled: Contract: $.bool() Methods: .init: Body: - $._environment: $.find(std:Environment).require() - Try: - $._magnum: new('io.murano.extensions.mirantis.magnum.Magnum', $._environment) Catch: With: 'murano.dsl.exceptions.NoPackageForClassFound' Do: Throw: PluginNotFoundException Message: 'Plugin for interaction with Magnum is not installed' create: Body: - $msg: format('Creating Magnum baymodel {0}', $.name) - $._environment.reporter.report($this, $msg) - $params: name: $.name image_id: $.imageId keypair_id: $.keypairId external_network_id: $.externalNetworkId coe: $.coe flavor_id: $.flavorId master_flavor_id: $.masterFlavorId fixed_network: $.fixedNetwork dns_nameserver: $.dnsNameServer network_driver: $.networkDriver docker_volume_size: $.dockerVolumeSize labels: $.labels http_proxy: $.httpProxy https_proxy: $.httpsProxy no_proxy: $.noProxy volume_driver: $.volumeDriver tls_disabled: $.tlsDisabled public: $.public registry_enabled: $.registryEnabled - Try: - $baymodelId: $._magnum.createBaymodel($params) Catch: - As: e Do: - $formatString: 'Error: {0}' - $._environment.reporter.report_error($, $formatString.format($e.message)) - Rethrow: - $.setAttr(baymodelId, $baymodeId) - $msg: format('Magnum baymodel is created {0}', $.name) - $._environment.reporter.report($this, $msg) - Return: $baymodelId delete: Body: - $baymodelId: $.getAttr(baymodelId, null) - $._magnum.deleteBaymodel($baymodelId) - $msg: format('Magnum baymodel {0} is deleted', $.name) - $._environment.reporter.report($this, $msg) ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1696417899.7011805 murano-16.0.0/contrib/plugins/magnum_plugin/magnum-app/com.intel.magnum.plugin.MagnumApp/UI/0000775000175000017500000000000000000000000032020 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/contrib/plugins/magnum_plugin/magnum-app/com.intel.magnum.plugin.MagnumApp/UI/ui.yaml0000664000175000017500000001643200000000000033327 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. Version: 2 Templates: baymodel: ?: type: com.intel.magnum.plugin.MagnumBaymodel name: $.baymodelConfiguration.name imageId: $.baymodelConfiguration.imageId keypairId: $.baymodelConfiguration.keyPair externalNetworkId: $.baymodelConfiguration.externalNetworkId coe: $.baymodelConfiguration.coe flavorId: $.baymodelConfiguration.flavorId.norm() masterFlavorId: $.baymodelConfiguration.masterFlavorId.norm() networkDriver: $.baymodelConfiguration.networkDriver.norm() fixedNetwork: $.baymodelConfiguration.fixedNetwork.norm() dnsNameServer: $.baymodelConfiguration.dnsNameServer.norm() dockerVolumeSize: $.baymodelConfiguration.dockerVolumeSize labels: $.baymodelConfiguration.labels.norm() volumeDriver: $.baymodelConfiguration.volumeDriver.norm() httpProxy: $.baymodelConfiguration.httpProxy.norm() httpsProxy: $.baymodelConfiguration.httpsProxy.norm() noProxy: $.baymodelConfiguration.noProxy.norm() tlsDisabled: $.baymodelConfiguration.tlsDisabled public: $.baymodelConfiguration.public registryEnabled: $.baymodelConfiguration.registryEnabled Application: ?: type: com.intel.magnum.plugin.MagnumBayApp name: $.appConfiguration.name nodeCount: $.appConfiguration.nodeCount masterCount: $.appConfiguration.masterCount discoveryUrl: $.appConfiguration.discoveryUrl.norm() timeout: $.appConfiguration.timeout baymodel: $baymodel Forms: - appConfiguration: fields: - name: name type: string label: Bay Name description: >- Enter a desired name for the application. Just A-Z, a-z, 0-9. - name: nodeCount type: integer label: Node Count initial: 1 required: false description: >- Enter desired no. of node counts. This node count specifies no. of minion node created in bay. - name: masterCount type: integer label: Master Node Count initial: 1 required: false description: >- Enter desired no. of master node counts. This master node count specifies no. of master node created in bay. - name: discoveryUrl type: string label: Discovery URL required: false description: >- Specifies custom discovery url for node discovery. - name: timeout type: integer label: Timeout initial: 0 required: false description: >- The timeout for bay creation in minutes. Set to 0 for no timeout. The default is no timeout. - baymodelConfiguration: fields: - name: name type: string label: Baymodel Name description: >- Enter a desired name for the application. Just A-Z, a-z, 0-9. - name: imageId type: image imageType: linux label: Instance Image initial: linux description: >- Select a valid image for the application. Image should already be prepared and registered in glance. - name: keyPair type: keypair label: Key Pair description: >- Select a Key Pair to control access to instances. You can login to instances using this KeyPair after the deployment of application. - name: externalNetworkId type: string label: External Network description: >- Select an External Network to assign IPs to bay nodes. - name: coe type: string label: Container Orchestration Engine initial: kubernetes description: >- Select Container Orchestration Engine type to be created. - name: flavorId type: flavor label: Bay Flavor required: false description: >- Specify the nova flavor id to use when launching the bay. - name: masterFlavorId type: flavor label: Master Flavor required: false description: >- Specify the nova flavor id to use when launching the master node of the bay. - name: networkDriver type: string label: Network Driver initial: flannel required: false description: >- Specify the network driver name for instantiating container networks. - name: fixedNetwork type: string label: Fixed Network required: false description: >- Specify the private Neutron network name to connect to this bay. - name: dnsNameServer type: string label: DNS Name Server initial: 8.8.8.8 required: false description: >- Specify the DNS nameserver to use for this bay. - name: dockerVolumeSize type: integer label: Docker Volume Size required: false description: >- Specify the number of size in GB for the docker volume to use. - name: labels type: string label: Labels required: false description: >- Arbitrary labels in the form of key=value pairs to associate with a bay. Specify in format . - name: volumeDriver type: string label: Volume Driver required: false description: >- Specify the volume driver name for instantiating container volume. - name: httpProxy type: string label: HTTP Proxy required: false description: >- Specify the http_proxy address to use for nodes in bay. - name: httpsProxy type: string label: HTTPS Proxy required: false description: >- Specify the https_proxy address to use for nodes in bay. - name: noProxy type: string label: No Proxy required: false description: >- Specify the no_proxy address to use for nodes in bay. - name: tlsDisabled type: boolean label: TLS Disabled required: false initial: false description: >- Specify true to disable TLS in the bay. - name: public type: boolean label: Public required: false initial: false description: >- Specify true to make bay public. - name: registryEnabled type: boolean label: Registry Enabled required: false initial: false description: >- Specify true to enable docker registry in the bay. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/contrib/plugins/magnum_plugin/magnum-app/com.intel.magnum.plugin.MagnumApp/logo.png0000664000175000017500000002107700000000000033160 0ustar00zuulzuul00000000000000PNG  IHDRZZ8AtEXtSoftwareAdobe ImageReadyqe<xiTXtXML:com.adobe.xmp yvk]IDATx]xTeeJ24CCH ".M  ˏ H* S'doNɔL D3̽{y{w#lGrrrY,VRRj5KIIӱcZh4^?|_cIR&H<}QիWڵk$0VlXmEEEm۶ !+>Iׯ|VXv4R͘F%a-lpHTv˗/YzNj/^sƍhk׮5j C ԠAx)[624J s L%FSH%,i{Z\?F%K/X`uvv? :uhǍ7d J-|䂁Y Hu7 p|.Q{K${)deUV-Ztr@ӹǎ;lРAc ll!=[zS -l+Eo0zur)K̙} ͛ך:uHbU*Um"~.aY\71YJҤ9MShKKXo߾ki;uTo \Mx?ʾ[¾].4JE$F`f#r6q {~[~ӧ/ܽ{p_}|߾} ɷ=%= 70ndJ~Dnݺ/gΜ9׮]'4l;cT ߸oakvuV_P-1R;U^|Bpۥd1c*t~^{dK[fOl=_hAI^AV zW8zEIYd{HִMj!I>\Js߮#FJMMCo9k&zG1å,9LZ1`$-{Qj ][dk pvэ .rmD _=eʔI&J9X)+ۘ: `b#Xl}NǾVP88{ und/ydSfϞ=АG+++-hd4$Tp֨8ֻX=~>4|9WjICIDk'̝;e˖m Dy:J.$Q 6_$uu`f*<Mmvl1Fiƞ={5j @(XbŬgyf|/,KYstm/١F6x웦7!%5EE7f̘g1I䱉r!YI=2q+w7sHOY]_.Lj,SNNo X}`],%I~nӦMSFSWAUZW'QO4WN上F&JuDS*Cj bU1SQ++áx~ԩScnecz6(Pf~A +8G i=,H 醵$|jT,ߝB9A+$290,hH*8YlzSHCJ PR*〔SJ{=}ZquT3 Osq C@GEE)_$8gB’GMꗋF@6Y-[<)xNpXj[1N89Gǟ^Wh>KVNC[!0_vT! t͓! lT)v-XiF3wk}d_EQTV- a-9k!jFp+Fy"6 8]vp?o]JxA (! 0>NKKkO>5rex W;JQF5aaJ9`aJ,jldcx<+G} K5xP)c%';vt"'U `%\$TQ8YѾG @#G'( w̝1%eE`u}W,݀V(h.$!_/O5y;^PnRX"*.,O#j* 5 LzTM.ݲ I-Bx]Km\n:.攗3>T*i"BhRG,!K rٔ}z*""")$C@g E$ߴ6}5nB@Y~N\5<|i3r7!̱R/1=NhVJ>H\%0L)$!QH6JX%} |oZ5䰬Źl<6|v1 qwC޷Rgׁ`*@\RxLJ_yۅ$sZ'E*_lpy,e6!Y$ў@2gvsLe-ؠ^A׀cPqMy!06;}L)7L,11QӢExfNd$^3GfO~,5QwӺr>bDPE! #'ze?kMyN ˓ڞnoڷXpr - 2ؔ+.ظgYh E n2@Zk]˷scdbGUNǚ5kVOK^7%e *@? E,_Ž LP` 9[ SخģgjQ^׷ls;M֬| x~.ʼ!ɯVZTv:ǡ< w.h2He.93b>87ԯwu@^Aֵ- v=BE 먕 IF6s8/m*S0Xw$'ijGW_}5oY+#ƍ~A׬no t?ޟ%չ{SD7Z+(6B~|'_z $JR`хg-=FaD;XIFY!r'8!%pg(ޣG7Ȼj2Q^2eI>olTIN WGs }ٞΝ׸qc-޽{@?Ν;GgyG,wls#yMΌgE ת|Xgl/@c{tP?qT#|g22k4Ƒ#G.2hdA R!# @m [j`4(drk#0KJժcɈ?X%8YƂ {w" huDe˖k:<>izT?2\B˷Mt-VZvNUЄ]wVp&qzVlرcDzݻ2dF!ڹs ~^דq+8*9Д~1=*x2:4b[ԒBPߐR@04|0M9N*jV܂8~UV`' AAMv?ղhSՅ$ Aҍтpw.ܨ߅ KApICX%;7s̉3fwDlÆ {1*}OB ۟b5z^O8(!nB3#.jN+9ID~sH~ZD{:g㚳6lDEc͋7>G8:СC'_x'HGcyPH[/@ݳLBǮH%e=y`fw(AAד\?'ul"E6C9cu:A&lYR^K TVy/ȼ:~p= l  ۠uLEr_V3nݺɚ?:bb#..Nwʯᵷ@YVBQ9ŲXc*#{ &;Μ9nϼ}z3?ˢRfv@F`X,t愹o>E]͵ 0`(<^CbDi ]J\"98s Oްay 14ѷQzŐ9_9TGs2W7>h4A"GiaBolٲ-:GXصkׯ͚5Sl2SKdЩkfY]F{f;kt=3.0.0 # Apache-2.0 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/contrib/plugins/magnum_plugin/setup.cfg0000664000175000017500000000104100000000000022722 0ustar00zuulzuul00000000000000[metadata] name = murano.plugins.magnum description = Plugin to deploy a Magnum Bay to run docker containers on it. summary = This plugin uses python-magnumclient to deploy a Magnum Bay. You can deploy any of Kubernetes, Swarm and Mesos cluster with it. Just specify 'coe' to deploy cluster of your choice and run containers on it. author = Madhuri Kumari author-email = madhuri.kumari@intel.com [files] packages = magnum_plugin [entry_points] io.murano.extensions = mirantis.magnum.Magnum = magnum_plugin:MagnumClient ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/contrib/plugins/magnum_plugin/setup.py0000664000175000017500000000145500000000000022624 0ustar00zuulzuul00000000000000# Copyright 2016-2017 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. import setuptools # all other params will be taken from setup.cfg setuptools.setup(packages=setuptools.find_packages(), setup_requires=['pbr'], pbr=True) ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1696417899.7011805 murano-16.0.0/contrib/plugins/murano_exampleplugin/0000775000175000017500000000000000000000000022476 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1696417899.6611803 murano-16.0.0/contrib/plugins/murano_exampleplugin/example-app/0000775000175000017500000000000000000000000024707 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1696417899.7011805 murano-16.0.0/contrib/plugins/murano_exampleplugin/example-app/io.murano.apps.demo.DemoApp/0000775000175000017500000000000000000000000032027 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1696417899.7051804 murano-16.0.0/contrib/plugins/murano_exampleplugin/example-app/io.murano.apps.demo.DemoApp/Classes/0000775000175000017500000000000000000000000033424 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000021700000000000011455 xustar0000000000000000121 path=murano-16.0.0/contrib/plugins/murano_exampleplugin/example-app/io.murano.apps.demo.DemoApp/Classes/DemoApp.yaml 22 mtime=1696417875.0 murano-16.0.0/contrib/plugins/murano_exampleplugin/example-app/io.murano.apps.demo.DemoApp/Classes/D0000664000175000017500000000176100000000000033537 0ustar00zuulzuul00000000000000Namespaces: =: io.murano.apps.example.plugin std: io.murano res: io.murano.resources sys: io.murano.system Name: DemoApp Extends: std:Application Properties: name: Contract: $.string().notNull() instance: Contract: $.class(res:Instance).notNull() Methods: initialize: Body: - $._environment: $.find(std:Environment).require() deploy: Body: - If: not $.getAttr(deployed, false) Then: - $._environment.reporter.report($this, 'Creating VM ') - $securityGroupIngress: - ToPort: 22 FromPort: 22 IpProtocol: tcp External: true - $._environment.securityGroupManager.addGroupIngress($securityGroupIngress) - $.instance.deploy() - $resources: new(sys:Resources) - $._environment.reporter.report($this, 'Test VM is installed') - $.host: $.instance.ipAddresses[0] - $.user: 'root' - $.setAttr(deployed, true) ././@PaxHeader0000000000000000000000000000022400000000000011453 xustar0000000000000000126 path=murano-16.0.0/contrib/plugins/murano_exampleplugin/example-app/io.murano.apps.demo.DemoApp/Classes/DemoInstance.yaml 22 mtime=1696417875.0 murano-16.0.0/contrib/plugins/murano_exampleplugin/example-app/io.murano.apps.demo.DemoApp/Classes/D0000664000175000017500000000036000000000000033531 0ustar00zuulzuul00000000000000Namespaces: =: io.murano.apps.example.plugin res: io.murano.resources Name: DemoInstance Extends: - res:LinuxMuranoInstance - ImageValidatorMixin Methods: deploy: Body: - $.validateImage() - $.super($.deploy()) ././@PaxHeader0000000000000000000000000000023300000000000011453 xustar0000000000000000133 path=murano-16.0.0/contrib/plugins/murano_exampleplugin/example-app/io.murano.apps.demo.DemoApp/Classes/ImageValidatorMixin.yaml 22 mtime=1696417875.0 murano-16.0.0/contrib/plugins/murano_exampleplugin/example-app/io.murano.apps.demo.DemoApp/Classes/I0000664000175000017500000000213700000000000033542 0ustar00zuulzuul00000000000000Namespaces: =: io.murano.apps.example.plugin res: io.murano.resources std: io.murano Name: ImageValidatorMixin Extends: - res:Instance Properties: requiredType: Contract: $.string().notNull() Methods: validateImage: Body: - $environment: $.find(std:Environment).require() - Try: - $glance: new('io.murano.extensions.mirantis.example.Glance', $environment) Catch: With: 'murano.dsl.exceptions.NoPackageForClassFound' Do: Throw: PluginNotFoundException Message: 'Plugin for interaction with Glance is not installed' - $glanceImage: $glance.getById($.image) - If: $glanceImage = null Then: Throw: ImageNotFoundException Message: 'Image with specified Id was not found' - If: $glanceImage.meta = null Then: Throw: InvalidImageException Message: 'Image does not contain Murano metadata tag' - If: $glanceImage.meta.type != $.requiredType Then: Throw: InvalidImageException Message: 'Image has unappropriate Murano type' ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1696417899.7051804 murano-16.0.0/contrib/plugins/murano_exampleplugin/example-app/io.murano.apps.demo.DemoApp/UI/0000775000175000017500000000000000000000000032344 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000020500000000000011452 xustar0000000000000000111 path=murano-16.0.0/contrib/plugins/murano_exampleplugin/example-app/io.murano.apps.demo.DemoApp/UI/ui.yaml 22 mtime=1696417875.0 murano-16.0.0/contrib/plugins/murano_exampleplugin/example-app/io.murano.apps.demo.DemoApp/UI/ui.yam0000664000175000017500000000517100000000000033475 0ustar00zuulzuul00000000000000Version: 2 Application: ?: type: io.murano.apps.example.plugin.DemoApp name: $.appConfiguration.name instance: ?: type: io.murano.apps.example.plugin.DemoInstance name: generateHostname($.instanceConfiguration.unitNamingPattern, 1) flavor: $.instanceConfiguration.flavor image: $.instanceConfiguration.osImage requiredType: $.appConfiguration.requiredType assignFloatingIp: $.appConfiguration.assignFloatingIP keyname: $.instanceConfiguration.keyPair Forms: - appConfiguration: fields: - name: name type: string label: Application Name initial: Demo description: >- Enter a desired name for the application. Just A-Z, a-z, 0-9, dash and underline are allowed - name: requiredType type: string label: Required MuranoImage Type initial: linux description: >- Enter a value to be matched against 'type' field of MuranoImage metadata - name: assignFloatingIP type: boolean label: Assign Floating IP description: >- Select to true to assign floating IP automatically initial: false required: false widgetMedia: css: {all: ['muranodashboard/css/checkbox.css']} - instanceConfiguration: fields: - name: title type: string required: false hidden: true description: Specify some instance parameters on which the application would be created - name: flavor type: flavor label: Instance flavor description: >- Select registered in OpenStack flavor. Consider that application performance depends on this parameter. required: false - name: osImage type: image imageType: linux label: Instance image description: >- Select a valid image for the application. Image should already be prepared and registered in glance. - name: keyPair type: keypair label: Key Pair description: >- Select a Key Pair to control access to instances. You can login to instances using this KeyPair after the deployment of application. required: false - name: availabilityZone type: azone label: Availability zone description: Select availability zone where the application would be installed. required: false - name: unitNamingPattern label: Hostname type: string required: false ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/contrib/plugins/murano_exampleplugin/example-app/io.murano.apps.demo.DemoApp/logo.png0000664000175000017500000007041300000000000033502 0ustar00zuulzuul00000000000000PNG  IHDRI pHYs.#.#x?v OiCCPPhotoshop ICC profilexڝSgTS=BKKoR RB&*! J!QEEȠQ, !{kּ> H3Q5 B.@ $pd!s#~<<+"x M0B\t8K@zB@F&S`cbP-`'{[! eDh;VEX0fK9-0IWfH  0Q){`##xFW<+*x<$9E[-qWW.(I+6aa@.y24x6_-"bbϫp@t~,/;m%h^ uf@Wp~<5j>{-]cK'Xto(hw?G%fIq^D$.Tʳ?D*A, `6B$BB dr`)B(Ͱ*`/@4Qhp.U=pa( Aa!ڈbX#!H$ ɈQ"K5H1RT UH=r9\F;2G1Q= C7F dt1r=6Ыhڏ>C03l0.B8, c˱" VcϱwE 6wB aAHXLXNH $4 7 Q'"K&b21XH,#/{C7$C2'ITFnR#,4H#dk9, +ȅ3![ b@qS(RjJ4e2AURݨT5ZBRQ4u9̓IKhhitݕNWGw Ljg(gwLӋT071oUX**| J&*/Tު UUT^S}FU3S ԖUPSSg;goT?~YYLOCQ_ cx,!k u5&|v*=9C3J3WRf?qtN (~))4L1e\kXHQG6EYAJ'\'GgSSݧ M=:.kDwn^Loy}/TmG X $ <5qo</QC]@Caaᄑ.ȽJtq]zۯ6iܟ4)Y3sCQ? 0k߬~OCOg#/c/Wװwa>>r><72Y_7ȷOo_C#dz%gA[z|!?:eAAA!h쐭!ΑiP~aa~ 'W?pX15wCsDDDޛg1O9-J5*>.j<74?.fYXXIlK9.*6nl {/]py.,:@LN8A*%w% yg"/6шC\*NH*Mz쑼5y$3,幄'L Lݛ:v m2=:1qB!Mggfvˬen/kY- BTZ(*geWf͉9+̳ې7ᒶKW-X潬j9(xoʿܔĹdff-[n ڴ VE/(ۻCɾUUMfeI?m]Nmq#׹=TR+Gw- 6 U#pDy  :v{vg/jBFS[b[O>zG499?rCd&ˮ/~јѡ򗓿m|x31^VwwO| (hSЧc3- cHRMz%u0`:o_Ff6IDATxw$eUif:MټK;AEQA#?s33 ($Q"AA]2`g3fw6LU3;=ӡvgf٭~>heB!BB!xB!P !B'BB!xB!O! ٯȄ:B!b$\}~spBb8 p*/#R#> p5-B!5j BHi8>SB\VӽB񝋧asB!gpGF3B!ħxJü&g SB!s'K^>B|gKϖBgK B!>3ZoBꗑLk7S/M;ɚ~<8 y=mB?vc=ؼ}OoY8d.F/ǜBꋭ{SwO<֏NۼQ'`ێXl؛8u @o|L=i^x':E`kLAwsM`Z4h4_'4+;B (:Pwu]]@C` ˴ ?Do+@!m5'!B@8,,xMӐ5-: yDox WBmH8>)!P(߱'"@* Z/˲r)BBo_?)VtӦ;%46$JlK{X<,'xB!d]Xlvt%9챘d'"fqJS)hZ4X_Q !=[e0O~Nqꡐ#&۵eb |:-aF"72d<ݿ ol?y koMkO@)B*+kc81d1C>u㛚$NK8h=qolȤ`pع!A h~xMBپ.3k7 I9Sĺp[*48x yC{8,>HhhE"ϲz}/-p&xNuD$[iVGS)q kW®%"!5EQ/|&B<3/s65 Ϙ!"p|simwMk7 e;v;׸O'2u=F`F74Ĝ77Kmk/ >`\0O(BJfEV,Zl1rvkok|k]&!B!'?!߳v ]MO'R:}X8k$ tO[ߚ*&!t:}mwfe=,C74P !-hqvB$0k&.HĩD^kW]ՒG'My;zѾS !5۰hqY)h̖8w|vWk*$_D/v[:UMQh)B&Hѐ|DuwUVg]U5:!Rl|=kz3Q !kvctvM$ٳxJS!vGU"Zk'>߾:2l*vxB!dX#Dہٳčiiڥ3 ɧR&%Envj<O!dL!ɒ9CWtUH>WF3Rqy osO!m^,Zv rNkiq'@:%fmqpϩ|%MfvxP !nٲ}-^޾J2].^А_KŨZ4)}}@k|8d@B7xB_6m݅Eנotuqs:JsoS=]]~p`̀{)Ron @4YsWlkt*[ ۻ@5ٻp0=BWb 2RS>uפS"N ^ Ӕ8z.4(R'H50k NeʫdNSJܛ(}*vrTw o:]1q%,?04$YcաSI @2iq_R myRO&Zu9t-O(B^FBK#xR1A+JC.RŢgWcݹU>=f]R1}৵'elqB'SvGuM2YuZSΚ{4[:R"ko}m۝V`qJܛw:]˒}=8PI9]s]Ӡ ]kuݳ7Ӻ硥 K,ƶm۰~:^f6][\Փs--vqo/H +2`x vxRnau ^.zYw/"KXv-4MG,C~z*=:,wM kSI®%솎h4t}Xa3n|/Cפc$>U`[9CCCt=iwK/bnLO܋ry>)d2]ݰxp?믻94+"꺎P8bϪBEsž{sM"_soj/?;+;[>HEMf(dPH;O= 'pUWm]7Dut^ӊ;w [>>~_Zv ̟7iopӧ"6[D ^ ;EOw@g:Cx$yiX~6mC7G$+aFmE(7h{܆ח`\HD;,ElJb R .ۉ*9nO7|; #$®4=5Eae? {ܹ٬ZFsDa',5b*!0M Y:x2kC`tD#q7tǾ^{-'n8=uyn?!cYY3iL8]{{{8. t'R)dLqϙ >p9nPX]ItAynv+| \{G^q}o-Kx'vᚐ8_"nVas{.q/uk{c#5洄Qa6 ӤKy?%N6 딳pOk)wݞht<7{[uWR""c:]N9 ڤ2Rlؼ g:C.;k:>_GϦ袋 F.$q*d3o.Nimar9x~] |Cś&k`n_EqSBN:^3?|dvZ%QyA=ڵ+=mmN tqnqO$׫+7҄=?*i_ ͷwNǻ?pv[122P(ݰS(|<;ݻw;{YN!w;C)\*YkZkNcۉu~+WB} l;{ǝd{v8CfK]quW:ջ1AўZ w.5{ \O0B.qSxܵ1.+Vx eyϙ0yiǽWi$h|+Ş={uw f.̘`T3@" sf;;v Mg,Su ÷wvcWD1nql'###yeh[ZDxb113toi!`8I$ |9x`y/=GeWUh^K:wR]:$ḟɎituɵ+Π!#^T |=:WIJ4O t ڀYqzƼ{ݝIupvEy$:H،i"l,xC'?3'tSl}l /\womP _0_򳁁D!XO;цqk]cf5F%N{߽M>Dq7 H%b\sYkZual ٽRXYwO&uwV7 ``GKWn4,4V'tټ<!\mS"No&w&U76"^hwh_K^zlfWSwV)tkdd$o=:тuw%νِ;x]k}ؿm߱Z^v2 `ՠKcSl}V~w{G 78'/,߄Y0,@t..;Pd\1]Z]]uwߝF# gmg#׉O<=Zy1d2y{4ꔢU\w0rxpAހ"O't<溞e2nKR]Cm\wWM=TĎ~&QB'6eec7VB00KD\i s{" I>ףw<бek7pR3f76Jͷq$۩{c^k(2c`sA; fvZmmNh>亻. <@& #5aMy4U5`S^-qXL{*_V[[&WhxC CSW-G< \)?e60:tNƼ, vGBK(.]ǽn@g^wOMds`xÐʬϔ¿@~e^,x;x_qgٜgqohp\e.`g E`gMSx(.'b`` uC8?zY:R{[%K{_7nvΟK'dO md)l6Ep۝5PĩuVqj{,-q^`F`+Xر7Fќj3BX_`>+v셡f`F8|{{%WgZ`"owȯ7sC|,C 00ՖF'kTw q'S? czGMB:BpHG8d 6 ׆A;pIah 螞_νJbWm6m~p͵W[֞D)-_ tCz[ k5^l v=wwh>F2.qv|R W>{oxؽ;HhEE ]nzܻQG*KҀEIR%`.4 ITxZZw'хտ xmsbړH4FG† ;4_ 춠;ay)..;Pki]N\#W ئFv6^Y$BW[x ᰎH8[WWް5h[%LYT^kFIitef55xWmlqwWcʒ\!gᐁ8ɘH0v;v}tԩtt{0-/4osOŽN-qVWY^| 6|_5tќl@$B$"H8$a'uu <ɉ5x OF2L~hMxW[.q W"xpo'*."}j=bvCnNxBq}d4M b RNxwךW⮪14_^yWl|?ٚ@:˹h@DN'xl[&SY40}6;4efK^;@g[DLKùs;5#jL._ C0sF~yUЦE~|exY?pB4"=]{$˒pCבK#"𜾕n) ZGnY[/5|'hO?#!;o#t)$Q#5,L-TXKǯ֍^@JNTh^%,.GBʽ'}5O&ࠈO,Y_*$hIKNqc1 E4|kt]'U/.DN0(Vx'J^vX,s,AޗP wlHD~-[f%8q.!hT&M*4< ,+֕No4y" MFC^M>04WE:͕H1O%c,aEtf1 G%),b @{гxyGbX~!aĺTAhޝ5|~sIsk麮t.ΐ< ,.ܨp4t wv:a )o'SNa'X>BNqq{?]{ᶷD-ahx8>ݜN ҉C& (y122ᰄqMv:w->', 7660td3&4-]=rD\DxW®\{2)?ot5@,qY>x8.ުp{k YmsLTh>1֭. pŕΝ㋻iyr"Hw;lN<2L^ooɤ]%~"p|sS",Wd] a<xLaWSAwb],&Tb]".ɉ-|)-_~~Ts?ciI'E$^tcUfH '"8$:%B+HTpX~Jz$>h4XDŹ*4{͓OI2ݽؚc |F ވk$p<S&n 寳f7"x IOo?.D\o5jrhYV{ǥb$r{X7Q~淀E/us8VS߃V% )xdjѨ#BJ Mu({ls|x??wb](L$֩dW8hw_SHIZ)wt |M)x:l6'.qoi//fǧRNv|,vw8~,R[{ ^=mP)ܑw$H89 *h' `uOy=8VSIէGB2bP\TFFFF㛛e[VKX8ޝ^gohpzikÚ$ <,yq~KȽ/N5q'5ّv6Ɉr c{}Ux}ad!vJd276'ܻ+iQ%``E.-Kg8Kz;(.+xiМמN;;sʡ&7DBG{-xR :Yxe`xL08VsX76/rL[bMm* Օߣ ]sJ)q.'1HF 7edjL7ab%=k`^1wx <[, Qp24dF {$,>mӜD:{)U&^W溻SO(E_D oť 4l'RzvhOh;R%&m\:%+ώwW+̎W⢶V$M"fGwC~Vm/e9u]74:dƦg 78P xn` k4)LCC~v|moBW >W!NSo/Z}ahQ{ab]sڹtEYT6g0Yqs~nuLD׫OZ]D,M-wv >*tFx=-_x`DL&WN՛WXb`1ΙP/[|;JnF~W)=-*B祭Xn뤄5ʽV鵵bJwh>rɤXNojwwic,qz;OȄ{2n|C8>*tuvw8~*U*UR`[dw?q#pSnA}Wg8{W.w஻:xR'o6DZ[ ]2qwo{+\gWMagyM6u[LԶCw: Vmcd:aZBXxB\{($Y]v6wo{sWok\ZoN'n`Ŋ|5mz/ ӽK};ŚO(ڞv]DNwmoU U ]Pkg?9Ƚ6MӓY{oh:=Imqw wNhi*? mN9Ba,i]"*VoMūjDs<#PȓTMZ[E#ͫVt׿ stHd _kkVj(Pv7vWV.6.-v\{U*%9D3щmqr78| ^d:@ d>QifEŹ5noUm{sWh&Gr(uyeݽ[޼R<ŝPikq&wjp3V8I=stno^V.Ϙ|VFVAS0}-.u1_޷-yN'k =f{+DDOvo{St0~VNceG]|-=dr{Owwb]?#%M꘼Gw|r5NqcUW8/%ӈSdݵϨW"I¨i'޻ŽlWcRGN'$oQ  S\ܾй(aWmoS~YCN;٣vޗk5S9?jDmk)pJ+o-Ua 2JK(&"52O ]ntڃ>Q.~.hyLL.I'}T]-c5ko};|=LG&𜤖v++(u]zZwUFTmLā7m\x0"عC]P&/.Lch~zn},qh`BETNz𖷈xd3#fhhDm46dΝ9N;I]gWGk1VPI\v2\5M}hxǁ G,"_IOā7 xہ;>9wO:|*崃M$:{<& N(d!n1_ݡۑn8Ht:B2.>nǜvQ_ *.x?;N <)ث u_?'$BL vF} 8_m'c@{XK{MR_k <  }a~ /JƛoDζ6r{ϕ;=U}~U_jOW:g,9sw;Eu[ }y\j' = \|{W5ݽd>8(%h=?;x{XҞ꼩jPP{8?\;`JDGFd=^Ovym.4r漽^ރ( p%n_c2?a1}Vs|kR곷WݍHHB?ySIY_0;ߗ{oj~ރw?1e+-/&g06zt|wU7 ,^1FOWU~t]> \w>̛ǞuN6N8>`s8vwzr6 q"нw>׿eB'}](۶7$g?;aR]9sSN^X*D""扤|L׽ LMln#qEke~K5W{˗\<(Hu+A#cd8XED"6u{2Y|{Pf //ޝ7#xRGW x'*jܬRIR{{oO8m- /Kq']t}m/t6G>^ Fx==pXjGvY&[ܓpz(EgPK,R5+g`hXXJ|#[)ӝpK]Kk=曚RǸ>K p3fa%;R.OI]6o=C,yo dOzW?twKquM9f-WU}R]-e~eT;l` \|C0m^o,I+tAmpy21JlBI-]v0M`kW4ptx~?G~%<$ܻZ{FewڳG]+I:x]!)|Lq*an~t#6wH [:mq*s!}pPG?]^?B; <&kx%_}+ȩw̝ x"g+?O7K DB}0𧛥;XyB/;Q5`~}?]nwu!`V`ou]=opyCΕFzH|߁&McD;a i\ϑmۀ\{ݛ{w RVpӟ (֚ͻ]U}QKkaRk_u?c?oܧN(OiW2o k~׀>@WWU;H_'~N]˜ñOpJׯU$0U^CԄX8olԘ^?Yp[ߜPI]A(ظID7}kqgxZ-}JVY|,pƍ?>ڣ/28\~k~xkmٱyk=-/ uG> vBv~4 ײw QRIai׊׫샫y%۷\Hb-{Tm{}ٲ(TI+2& &k_"p"X}m.}8Yqx=(4HWeW84MAm>+`{ %'FZn֡.9 CCUWel_%7]Yϗٳcv"Ol'x&,Z@E~YߺW_Yv k/ycj <s@Vj]>kկGY'K'{9[?9"D :դi],P?9ओD|Wi /F24qgivx{^5O |ug]*u]LU9{r)xBjy̺D~:iI};p'6ISu)ᇁ!qF!pWؼ$޲gye?b<` <>]*8}] @.|w/p|wAN|K996o~3'?^)w?;z6a4MX!ɰߺ ץe1~*\ƛdvzRK7_sO9J^W~6 ,] |?v5aOa;P!N{DW+9vi'tSN&)5yɲV@O9H3þ.=ؽ;_']󤇧S߫fA E޲$e:" ye?--9O:xBJo "(bf)fŝXB|"> ݉l6zJ`3E|!~Íw=sӊxBWZlM `YVNk약f:yG~ql*%Ar~; !z <>.[^y˲02|3пE/[.^}5_ =Aw:9*<^W-Few _FG߹S1hju~y-;?Z<?04|k!7duP ~wFM9 ]ס:2L _*p"nr]m{Of@1A9WnX,5K@롺WuO(O? ̳@.``ޭ[/^y%_Du`9tGBO _QQ'k~ h:p;DkO?#{{5irYsaO]v)ի%m ;ˉwkɶ+䲾9B'tu=8˲rkW˚ǿ"\wpϧuwlB|*\)ZY㬅esa&Lӄe>y{7E>R@fEx& A.{XM'}/;AT0FFFr"j_˖WDg pokb/=%#Tv 牼i//w'_5v^zps8V '/#/-*4_~K`sO~f%4W. 3ɞOj:-Ͽ{ڷKj%WH|YHh>I'A}G`Ek&_B p.\x |g]*Mo/ˀ?V ֤flAz T$LI]Y3mv*D"9,)(󳟋pD>[{Z]?Y~!zRrJU޲'.VWr~a) E5+`IGH:x}JVej^]oYկWo\~xq{wK e{(2p6<窞jN?T gT|6+=ޯ6_ CwЉQ <Z50 b1 Lg 3R^_4wMb)TZ)Jx *qț&#տnzpلs^XʵRa O]{, CCC+ɧS_?zM~_6o68םXW)aˮñ_`N^^}"h\Dzv7##m緁l9ڠv)%=F 7E$\ ,e?dfaE@c#:kX{_X \p00;ΏdlzFsOK,xMF144l6lV%(2W(vW_ JXw& ~_tٵ0 D\!)3 |B8)?цB!O%Փˮ՘ñO)By%mgxT!5k .vv͉y߻~O($xMW"?00˲W'_'ĺb¾q[N/3|JVeSyXj ? /] \+'þpϻehXMWQ5-o,]v F6e˗Ltcpusuݓ^.cj |.ۏЀ\fK+6nnS/Lˮq .}\BխhO@},FF'~y!08P:l/?')΂ͽ2;ό.M(-I`\3罚XljFxRs+߱G"\IL& ˻^lB->@\{)aw&wd}lo?CZOa Љ{cGe!xxk;/8]v-[4X5{Nk5piWs&yay<<zs2ཨ1Nzx ;O8xq3ch <^`!}w]8^1;fؘñ_d)Z/l3J_kñO8rХh3O(ŰOIJO j<ŏk]1c5gl.LCkO/ ’)?^j3).e690=Kd³iknͮ+Ǿl 2ǛC(^$ML cKMXy )lF浦`Od]zSc#N'v ݻD^-Sjel>< 8:<TOaᵍ1g,^S)~J7Λ2ݎ?5L>@%|e"kYY:u3 9eSx>%?AG8ӳ YBs(xRL ־yKթ) vΩ,g,XQXex T7eLKK~K9فcqò MK`2( iZظ~1c6nژ7S`S,yܥ-V(tϘ(6_'&SiૂiY0- Ēgqn͉=iqwgZiR2efք1cw믿ޮ4eU8ɎchMӔvĉ''{fƄ5=78ONI/-`:aT4ov 0F]6W-9=E&A"˖ބ}12E4%ܲM5t:xӛing:3x!*~=˲r7[̢+x?dM #Y |)5Aۯ=ز, 2}d-dM v2/mr0M ٌ_x5c}+.\`YZEBz\9sܹ%cn C6c8:xRyjYD&k/\~k0My)'[AMǐnZ&W[G&k Sb)cٳl6&-GòLUPz\NBx[% GT#dYXso}%teM,nZ\xEx1G̚2VBqcϹwӄifa~yK1͊:xRU2xySg> ʾ :. ˛7Y|ӟ/<Wd2&? S}߿Tfi?gl\u]8cz0tCEl7]v.Gk 2 Ι~лӳ&49,OV}<3ryaM[Bp D0-:y74fø+.ĺ͖K/`B!h_w1 ӟBF]7U7]v-kGFp% jCCd2Y;躆!f|]<~\f{Mss\v%߽Nl&>t[q˟SO>;4ĺ="|*,?Ϗg6|{A ]נiK6Vw5wwXc6tr {~3_y7 ᐎd"}[6Gy[ȫIspܼSyd\tѯqMW/ly{+Žj W^5q塐pǾ 8,l9ظaW^j]`}#]6ϭ]v9(iY%"kf1{l{yYz5x|#d@q0/QmOQ7 >zg_, \p/k;y{%ڸ.[7]q7stj_p9ĕp9FFfB*+x |E^$do:B!&|$Z\|ꫯBDE$qKGϓMoC=({^84M̙=/xeU`~ g\H>'jqꃔ9Mj CG< }࿰qOŸ|3vzͥZe^ .RnwwvөN;t|ӻp-~AJ5sMc,x |ݼhsJ/o»>bu<xEx饗fZq0YKX9iMZY3qo> '̙wy[#cvv <BMנk: C9{<Gu 1ohk`G:H|NxkCss fϙ׿ غe^]?zQ,[tˮ1/^>O'BKƳrM !=dGHڵ7Ê_B&q˝!zx !:xB!P !'B !B'BdB!B!>pBHvB {p^ g<B 43A!$;y R6y B ^8@y3[`=n>B|̆Wx|.!hW CA!<񒃿!-5f C־wW!zMgBK ?gFa3B!ćgg(8 !gt@/|N! i|;$ٮ !0`v[tP^ *3C!\ճ[\YV!a׳aZ< t >;B<5='qžQo!A^׳q'[pzB!rgZ k-|!x^ݸ׫~! /y"pڍ;=}O."RclM<~~BlM!q1rXXi+K!T%Tݴ/fvK )C!TU`^O@S' l'BH h[/(]H#R~? }$Rf_O@ x.EB!e5[[I8>ٗH!Ӭ5 $MB!S0EGGB)k|D"2!=f|qw3K!>.;xo|g/O!D N/Y!Zs)yY>ׄRd 4!xww$O>i\C!>?\- {Is-dB }Cr ^}Wn,-!gl~=YLEH9A2 !ն_2)S׮\l; '6x A[vS}(@zj,g}B!c Ok!cW_H'BB!xB!P ! 1: raise AmbiguousNameException(name) elif len(images) == 0: return None else: return GlanceClient._format(images[0]) def get_by_id(self, imageId): image = self._client.images.get(imageId) return GlanceClient._format(image) @staticmethod def _format(image): res = {"id": image.id, "name": image.name} if hasattr(image, "murano_image_info"): res["meta"] = json.loads(image.murano_image_info) return res @classmethod def init_plugin(cls): cls.CONF = cfg.init_config(CONF) @staticmethod @session_local_storage.execution_session_memoize def create_glance_client(region): LOG.debug("Creating a glance client") params = auth_utils.get_session_client_parameters( service_type='image', conf=CONF, region=region) return glanceclient.Client(CONF.images.api_version, **params) class AmbiguousNameException(Exception): def __init__(self, name): super(AmbiguousNameException, self).__init__("Image name '%s'" " is ambiguous" % name) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/contrib/plugins/murano_exampleplugin/murano_exampleplugin/cfg.py0000664000175000017500000000146600000000000030051 0ustar00zuulzuul00000000000000# 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 oslo_config import cfg def init_config(conf): opts = [ cfg.IntOpt('api_version', default=2), cfg.StrOpt('endpoint_type', default='publicURL') ] conf.register_opts(opts, group="images") return conf.images ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/contrib/plugins/murano_exampleplugin/requirements.txt0000664000175000017500000000005100000000000025756 0ustar00zuulzuul00000000000000python-glanceclient>=3.1.0 # Apache-2.0 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/contrib/plugins/murano_exampleplugin/setup.cfg0000664000175000017500000000130000000000000024311 0ustar00zuulzuul00000000000000[metadata] name = murano.plugins.example description = Example Plugin to extend collection of MuranoPL system classes summary = An example Murano Plugin demonstrating extensibility of MuranoPL classes with code written in Python. This particular plugin uses python-glanceclient to call OpenStack Images API to list available images and return their ids to caller. Anther available method allows to get murano-related metadata from image with a given id. author = Alexander Tivelkov author-email = ativelkov@mirantis.com [files] packages = murano_exampleplugin [entry_points] io.murano.extensions = mirantis.example.Glance = murano_exampleplugin:GlanceClient ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/contrib/plugins/murano_exampleplugin/setup.py0000664000175000017500000000145500000000000024215 0ustar00zuulzuul00000000000000# Copyright 2011-2012 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. import setuptools # all other params will be taken from setup.cfg setuptools.setup(packages=setuptools.find_packages(), setup_requires=['pbr'], pbr=True) ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1696417899.7051804 murano-16.0.0/contrib/plugins/murano_heat-translator_plugin/0000775000175000017500000000000000000000000024312 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/contrib/plugins/murano_heat-translator_plugin/README.rst0000664000175000017500000000172500000000000026006 0ustar00zuulzuul00000000000000============================= OASIS TOSCA Plugin for Murano ============================= This is a plugin for Murano to support the OASIS standard for TOSCA. The feature currently supported by this plugin is importing Murano application definition archives of TOSCA CSARs into Murano application catalog. ********** How To Use ********** In order to make use of this plugin it has to be installed first, in the same Python environment that Murano is running, using the pip command (i.e., run *pip install .* from inside the plugin folder). At a minimum, the plugin requires version *0.2.0* of the *TOSCA-Parser PyPI package*. Two sample Murano application definition archives are provided in unzip format: * hello_world * wordpress In order to import the corresponding archives refer to *README.rst* inside each sample folder to generate the archives first. The archives then will be ready to be imported into Murano application catalog via Murano command line or Murano UI. ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1696417899.7051804 murano-16.0.0/contrib/plugins/murano_heat-translator_plugin/plugin/0000775000175000017500000000000000000000000025610 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/contrib/plugins/murano_heat-translator_plugin/plugin/__init__.py0000664000175000017500000000000000000000000027707 0ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/contrib/plugins/murano_heat-translator_plugin/plugin/cfg.py0000664000175000017500000000144200000000000026722 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # 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 oslo_config import cfg def init_config(conf): opts = [ cfg.IntOpt('api_version', default=2), cfg.StrOpt('endpoint_type', default='publicURL') ] conf.register_opts(opts, group="heat_translator") return conf.heat_translator ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/contrib/plugins/murano_heat-translator_plugin/plugin/csar_package.py0000664000175000017500000004563000000000000030575 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # 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 shutil import sys import yaml import zipfile from murano.packages import exceptions from murano.packages import package_base from toscaparser.common import exception as csar_exception from toscaparser.prereq import csar from toscaparser.tosca_template import ToscaTemplate from translator.hot.tosca_translator import TOSCATranslator CSAR_RESOURCES_DIR_NAME = 'Resources/' CSAR_FILES_DIR_NAME = 'CSARFiles/' CSAR_ENV_DIR_NAME = 'CSAREnvironments/' class YAQL(object): def __init__(self, expr): self.expr = expr class Dumper(yaml.SafeDumper): pass def yaql_representer(dumper, data): return dumper.represent_scalar(u'!yaql', data.expr) Dumper.add_representer(YAQL, yaql_representer) class CSARPackage(package_base.PackageBase): def __init__(self, format_name, runtime_version, source_directory, manifest): super(CSARPackage, self).__init__( format_name, runtime_version, source_directory, manifest) self._translated_class = None self._source_directory = source_directory self._translated_ui = None @property def classes(self): return self.full_name, @property def requirements(self): return {} @property def ui(self): if not self._translated_ui: self._translated_ui = self._translate_ui() return self._translated_ui def get_class(self, name): if name != self.full_name: raise exceptions.PackageClassLoadError( name, 'Class not defined in this package') if not self._translated_class: self._translate_class() return self._translated_class, '' def _translate_class(self): csar_file = os.path.join(self._source_directory, 'csar.zip') shutil.copy(csar_file, self.get_resource(self.full_name)) if not os.path.isfile(csar_file): raise exceptions.PackageClassLoadError( self.full_name, 'File with class definition not found') csar_obj = csar.CSAR(csar_file) try: csar_obj.validate() except csar_exception.ValidationError as ve: raise exceptions.PackageFormatError('Not a CSAR archive: ' + str(ve)) translated = { 'Name': self.full_name, 'Extends': 'io.murano.Application' } csar_envs_path = os.path.join(self._source_directory, CSAR_RESOURCES_DIR_NAME, CSAR_ENV_DIR_NAME) validate_csar_parameters = (not os.path.isdir(csar_envs_path) or not os.listdir(csar_envs_path)) tosca = csar_obj.get_main_template_yaml() parameters = CSARPackage._build_properties(tosca, validate_csar_parameters) parameters.update(CSARPackage._translate_outputs(tosca)) translated['Properties'] = parameters hot = yaml.load(self._translate('tosca', csar_obj.csar, parameters, True)) files = CSARPackage._translate_files(self._source_directory) template_file = os.path.join(self._source_directory, CSAR_RESOURCES_DIR_NAME, 'template.yaml') with open(template_file, 'w') as outfile: outfile.write(yaml.safe_dump(hot)) translated.update(CSARPackage._generate_workflow(hot, files)) self._translated_class = yaml.dump(translated, Dumper=Dumper, default_style='"') def _translate(self, sourcetype, path, parsed_params, a_file): output = None if sourcetype == "tosca": tosca = ToscaTemplate(path, parsed_params, a_file) translator = TOSCATranslator(tosca, parsed_params) output = translator.translate() return output @staticmethod def _build_properties(csar, csar_parameters): result = { 'generatedHeatStackName': { 'Contract': YAQL('$.string()'), 'Usage': 'Out' }, 'hotEnvironment': { 'Contract': YAQL('$.string()'), 'Usage': 'In' } } if csar_parameters: params_dict = {} for key, value in (csar.get('parameters') or {}).items(): param_contract = \ CSARPackage._translate_param_to_contract(value) params_dict[key] = param_contract result['templateParameters'] = { 'Contract': params_dict, 'Default': {}, 'Usage': 'In' } else: result['templateParameters'] = { 'Contract': {}, 'Default': {}, 'Usage': 'In' } return result @staticmethod def _translate_param_to_contract(value): contract = '$' parameter_type = value['type'] if parameter_type in ('string', 'comma_delimited_list', 'json'): contract += '.string()' elif parameter_type == 'integer': contract += '.int()' elif parameter_type == 'boolean': contract += '.bool()' else: raise ValueError('Unsupported parameter type ' + parameter_type) constraints = value.get('constraints') or [] for constraint in constraints: translated = CSARPackage._translate_constraint(constraint) if translated: contract += translated result = YAQL(contract) return result @staticmethod def _translate_outputs(csar): result = {} for key in (csar.get('outputs') or {}).keys(): result[key] = { "Contract": YAQL("$.string()"), "Usage": "Out" } return result @staticmethod def _translate_files(source_directory): source = os.path.join(source_directory, 'csar.zip') dest_dir = os.path.join(source_directory, CSAR_RESOURCES_DIR_NAME, CSAR_FILES_DIR_NAME) with zipfile.ZipFile(source, "r") as z: z.extractall(dest_dir) csar_files_path = os.path.join(source_directory, CSAR_RESOURCES_DIR_NAME, CSAR_FILES_DIR_NAME) return CSARPackage._build_csar_resources(csar_files_path) @staticmethod def _build_csar_resources(basedir): result = [] if os.path.isdir(basedir): for root, _, files in os.walk(os.path.abspath(basedir)): for f in files: full_path = os.path.join(root, f) relative_path = os.path.relpath(full_path, basedir) result.append(relative_path) return result @staticmethod def _translate_constraint(constraint): if 'equal' in constraint: return CSARPackage._translate_equal_constraint( constraint['equal']) elif 'valid_values' in constraint: return CSARPackage._translate_valid_values_constraint( constraint['valid_values']) elif 'length' in constraint: return CSARPackage._translate_length_constraint( constraint['length']) elif 'in_range' in constraint: return CSARPackage._translate_range_constraint( constraint['in_range']) elif 'allowed_pattern' in constraint: return CSARPackage._translate_allowed_pattern_constraint( constraint['allowed_pattern']) @staticmethod def _translate_equal_constraint(value): return ".check($ == {0})".format(value) @staticmethod def _translate_allowed_pattern_constraint(value): return ".check(matches($, '{0}'))".format(value) @staticmethod def _translate_valid_values_constraint(values): return '.check($ in list({0}))'.format( ', '.join([CSARPackage._format_value(v) for v in values])) @staticmethod def _translate_length_constraint(value): if 'min' in value and 'max' in value: return '.check(len($) >= {0} and len($) <= {1})'.format( int(value['min']), int(value['max'])) elif 'min' in value: return '.check(len($) >= {0})'.format(int(value['min'])) elif 'max' in value: return '.check(len($) <= {0})'.format(int(value['max'])) @staticmethod def _translate_range_constraint(value): if 'min' in value and 'max' in value: return '.check($ >= {0} and $ <= {1})'.format( int(value['min']), int(value['max'])) elif 'min' in value: return '.check($ >= {0})'.format(int(value['min'])) elif 'max' in value: return '.check($ <= {0})'.format(int(value['max'])) @staticmethod def _format_value(value): if isinstance(value, str): return u"{}".format(value) return str(value) @staticmethod def _generate_workflow(csar, files): hot_files_map = {} for f in files: file_path = "$resources.string('{0}{1}')".format( CSAR_FILES_DIR_NAME, f) hot_files_map['../{0}'.format(f)] = YAQL(file_path) hot_env = YAQL("$.hotEnvironment") copy_outputs = [] for key in (csar.get('outputs') or {}).keys(): copy_outputs.append({YAQL('$.' + key): YAQL('$outputs.' + key)}) deploy = [ {YAQL('$environment'): YAQL( "$.find('io.murano.Environment').require()" )}, {YAQL('$reporter'): YAQL( "new('io.murano.system.StatusReporter', " "environment => $environment)")}, { 'If': YAQL('$.getAttr(generatedHeatStackName) = null'), 'Then': [ YAQL("$.setAttr(generatedHeatStackName, " "'{0}_{1}'.format(randomName(), id($environment)))") ] }, {YAQL('$stack'): YAQL( "new('io.murano.system.HeatStack', $environment, " "name => $.getAttr(generatedHeatStackName))")}, YAQL("$reporter.report($this, " "'Application deployment has started')"), {YAQL('$resources'): YAQL("new('io.murano.system.Resources')")}, {YAQL('$template'): YAQL("$resources.yaml('template.yaml')")}, YAQL('$stack.setTemplate($template)'), {YAQL('$parameters'): YAQL("$.templateParameters")}, YAQL('$stack.setParameters($parameters)'), {YAQL('$files'): hot_files_map}, YAQL('$stack.setFiles($files)'), {YAQL('$hotEnv'): hot_env}, { 'If': YAQL("bool($hotEnv)"), 'Then': [ {YAQL('$envRelPath'): YAQL("'{0}' + $hotEnv".format( CSAR_ENV_DIR_NAME))}, {YAQL('$hotEnvContent'): YAQL("$resources.string(" "$envRelPath)")}, YAQL('$stack.setHotEnvironment($hotEnvContent)') ] }, YAQL("$reporter.report($this, 'Stack creation has started')"), { 'Try': [YAQL('$stack.push()')], 'Catch': [ { 'As': 'e', 'Do': [ YAQL("$reporter.report_error($this, $e.message)"), {'Rethrow': None} ] } ], 'Else': [ {YAQL('$outputs'): YAQL('$stack.output()')}, {'Do': copy_outputs}, YAQL("$reporter.report($this, " "'Stack was successfully created')"), YAQL("$reporter.report($this, " "'Application deployment has finished')"), ] } ] destroy = [ {YAQL('$environment'): YAQL( "$.find('io.murano.Environment').require()" )}, {YAQL('$stack'): YAQL( "new('io.murano.system.HeatStack', $environment, " "name => $.getAttr(generatedHeatStackName))")}, YAQL('$stack.delete()') ] return { 'Workflow': { 'deploy': { 'Body': deploy }, 'destroy': { 'Body': destroy } } } @staticmethod def _translate_ui_parameters(tosca, title): result_groups = [] used_inputs = set() tosca_inputs = tosca.get('topology_template').get('inputs') or {} fields = [] properties = [] for input in tosca_inputs: input_value = tosca_inputs.get(input) if input_value: fields.append(CSARPackage._translate_ui_parameter( input, input_value)) used_inputs.add(input) properties.append(input) if fields or properties: result_groups.append((fields, properties)) rest_group = [] properties = [] for key, value in tosca_inputs.items(): if key not in used_inputs: rest_group.append(CSARPackage._translate_ui_parameter( key, value)) properties.append(key) if rest_group: result_groups.append((rest_group, properties)) return result_groups @staticmethod def _translate_ui_parameter(name, parameter_spec): translated = { 'name': name, 'label': name.title().replace('_', ' ') } parameter_type = parameter_spec['type'] if parameter_type == 'integer': translated['type'] = 'integer' elif parameter_type == 'boolean': translated['type'] = 'boolean' else: # string, json, and comma_delimited_list parameters are all # displayed as strings in UI. Any unsupported parameter would also # be displayed as strings. translated['type'] = 'string' label = parameter_spec.get('label') if label: translated['label'] = label if 'description' in parameter_spec: translated['description'] = parameter_spec['description'] if 'default' in parameter_spec: translated['initial'] = parameter_spec['default'] translated['required'] = False else: translated['required'] = True constraints = parameter_spec.get('constraints') or [] translated_constraints = [] for constraint in constraints: if 'length' in constraint: spec = constraint['length'] if 'min' in spec: translated['minLength'] = max( translated.get('minLength', -sys.maxsize - 1), int(spec['min'])) if 'max' in spec: translated['maxLength'] = min( translated.get('maxLength', sys.maxsize), int(spec['max'])) elif 'range' in constraint: spec = constraint['range'] if 'min' in spec and 'max' in spec: ui_constraint = { 'expr': YAQL('$ >= {0} and $ <= {1}'.format( spec['min'], spec['max'])) } elif 'min' in spec: ui_constraint = { 'expr': YAQL('$ >= {0}'.format(spec['min'])) } else: ui_constraint = { 'expr': YAQL('$ <= {0}'.format(spec['max'])) } if 'description' in constraint: ui_constraint['message'] = constraint['description'] translated_constraints.append(ui_constraint) elif 'valid_values' in constraint: values = constraint['valid_values'] ui_constraint = { 'expr': YAQL('$ in list({0})'.format(', '.join( [CSARPackage._format_value(v) for v in values]))) } if 'description' in constraint: ui_constraint['message'] = constraint['description'] translated_constraints.append(ui_constraint) elif 'allowed_pattern' in constraint: pattern = constraint['allowed_pattern'] ui_constraint = { 'expr': { 'regexpValidator': pattern } } if 'description' in constraint: ui_constraint['message'] = constraint['description'] translated_constraints.append(ui_constraint) if translated_constraints: translated['validators'] = translated_constraints return translated @staticmethod def _generate_application_ui(groups, type_name, package_name=None, package_version=None): app = { '?': { 'type': type_name } } if package_name: app['?']['package'] = package_name if package_version: app['?']['classVersion'] = package_version for i, record in enumerate(groups): section = app.setdefault('templateParameters', {}) for property_name in record[1]: section[property_name] = YAQL( '$.group{0}.{1}'.format(i, property_name)) return app def _translate_ui(self): tosca = csar.CSAR(os.path.join(self._source_directory, 'csar.zip'))\ .get_main_template_yaml() groups = CSARPackage._translate_ui_parameters(tosca, self.description) forms = [] for i, record in enumerate(groups): forms.append({'group{0}'.format(i): {'fields': record[0]}}) translated = { 'Version': 2.2, 'Application': CSARPackage._generate_application_ui( groups, self.full_name, self.full_name, str(self.version)), 'Forms': forms } return yaml.dump(translated, Dumper=Dumper, default_style='"') ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/contrib/plugins/murano_heat-translator_plugin/requirements.txt0000664000175000017500000000010600000000000027573 0ustar00zuulzuul00000000000000heat-translator>=2.0.0 # Apache-2.0 tosca-parser>=2.0.0 # Apache-2.0 ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1696417899.6611803 murano-16.0.0/contrib/plugins/murano_heat-translator_plugin/sample/0000775000175000017500000000000000000000000025573 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1696417899.7051804 murano-16.0.0/contrib/plugins/murano_heat-translator_plugin/sample/hello_world/0000775000175000017500000000000000000000000030105 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/contrib/plugins/murano_heat-translator_plugin/sample/hello_world/README.rst0000664000175000017500000000140000000000000031567 0ustar00zuulzuul00000000000000================================================================= Build Murano Application Definition Archive from hello_world CSAR ================================================================= In order to build a Murano application definition archive from the hello_world CSAR and the corresponding logo and manifest files, from inside the hello_world folder run following commands: 1. Download archive from https://github.com/openstack/heat-translator/raw/0.4.0/translator/tests/data/csar_hello_world.zip 2. Rename it to 'csar.zip' 3. *zip csar_helloworld_murano_package.zip csar.zip logo.png manifest.yaml* The resulting file *csar_helloworld_murano_package.zip* is the application definition archive that can be imported into the Murano application catalog. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/contrib/plugins/murano_heat-translator_plugin/sample/hello_world/logo.png0000664000175000017500000026577400000000000031600 0ustar00zuulzuul00000000000000PNG  IHDR,,y}u OiCCPPhotoshop ICC profilexڝSgTS=BKKoR RB&*! J!QEEȠQ, !{kּ> H3Q5 B.@ $pd!s#~<<+"x M0B\t8K@zB@F&S`cbP-`'{[! eDh;VEX0fK9-0IWfH  0Q){`##xFW<+*x<$9E[-qWW.(I+6aa@.y24x6_-"bbϫp@t~,/;m%h^ uf@Wp~<5j>{-]cK'Xto(hw?G%fIq^D$.Tʳ?D*A, `6B$BB dr`)B(Ͱ*`/@4Qhp.U=pa( Aa!ڈbX#!H$ ɈQ"K5H1RT UH=r9\F;2G1Q= C7F dt1r=6Ыhڏ>C03l0.B8, c˱" VcϱwE 6wB aAHXLXNH $4 7 Q'"K&b21XH,#/{C7$C2'ITFnR#,4H#dk9, +ȅ3![ b@qS(RjJ4e2AURݨT5ZBRQ4u9̓IKhhitݕNWGw Ljg(gwLӋT071oUX**| J&*/Tު UUT^S}FU3S ԖUPSSg;goT?~YYLOCQ_ cx,!k u5&|v*=9C3J3WRf?qtN (~))4L1e\kXHQG6EYAJ'\'GgSSݧ M=:.kDwn^Loy}/TmG X $ <5qo</QC]@Caaᄑ.ȽJtq]zۯ6iܟ4)Y3sCQ? 0k߬~OCOg#/c/Wװwa>>r><72Y_7ȷOo_C#dz%gA[z|!?:eAAA!h쐭!ΑiP~aa~ 'W?pX15wCsDDDޛg1O9-J5*>.j<74?.fYXXIlK9.*6nl {/]py.,:@LN8A*%w% yg"/6шC\*NH*Mz쑼5y$3,幄'L Lݛ:v m2=:1qB!Mggfvˬen/kY- BTZ(*geWf͉9+̳ې7ᒶKW-X潬j9(xoʿܔĹdff-[n ڴ VE/(ۻCɾUUMfeI?m]Nmq#׹=TR+Gw- 6 U#pDy  :v{vg/jBFS[b[O>zG499?rCd&ˮ/~јѡ򗓿m|x31^VwwO| (hSЧc3-bKGD pHYs+tIME 9 IDATxweWu%J]9TgVw+K$!B۲M4 m160m,'@2JsN_}«z-DKOzsY{&crL19&crL19&8nۂ[ftv6DP FXP5~VKjyg']&P((0cNFRp( c?̄B~f%ɿk3[>{0D@`aG4㜬-Y3qB:=_5%뵒aS)z@P[ }?}>/|/n59A#쌂 &30GfvF  `P'' \X |E㺇_yZ|y?cHd07P(ʦWs86(djX6 O^?xG?qb;.a[FMqceɜYEv?D0~F:gJMVwr*2~A5]g9&bQ-H#y&ɵ&Ϸ|6 3Yl? ʧ (B16B4j!#`u_͏v֓o;CytKU*!S x1a4 5p )sj~AX/{.9(qE_` b+!0_C6` _gzǦJ)1m[q鍧utOQY=ۅ)EċxrL19UT9b!j0Q e*y>sM`Fbo\E Ql*5 :w;2M4v&}hRF`ڼN,;oqp36<A#"]{.{ptgF2XW{u63uFXv,9{ei4;i&6<,;w64"<͈ M9'1VT^Ҹ2TG1wYϘnޤ79&jf7f-ƌ]QNE"ݫ5țި =۝FjYM19Z5V.eYD[w "Dq0qbx,UШP^S˖Te]Ka7QF|ǞmGwxPP,{ٯfhzs{-b0Rx o ^? ۍrGQy2Puk޺ k VR be8u8i? %d>7{z{k-Ap!`4v^PizG݌~ qryx"oF.OR?}Jk0cNׄ?ߌ{0H26Ra˳p`W?zLĭJm~IA#gz'νtYL{ A06݋Zz pmKO͘5oj'dn6iu7u>6Da铬U,^>n E?b0lk Qih;4 wpYG˒3{񚷜2Z'@ bj>?}/: S4pʹNc5 zw`^{Ru'$stW ==iԷ'-MQHGW [VWC*'9A/J+i[(%8 VQL`Lw'ڻ(|G.$ܩ양j.^sdJ +Oa{轛48vd?1xER+ 폮{>Z\~4V>~"!0rĄ VV`Lss|aƚ8EpB[ޯ-e$~ (̻ m+ߏҊw#8 *O)[2eםwUh(!5D|fRHcЙM01 ܅m/x*mE\5 ^3MĔr#Ox x.Z2_<㧖 r)iZP(;8ҵ>sv^ ^?"ńtH^xc~4zL)oEyՇQX(.Jߋ;[> ;t ѺK.zi8SQsxz,r;eq.!gRVCY|6rtלv\֎ _"(žm}8l#22\ڇő&A#igG{GlPO;{>Vak:vo;ҵ zAƔv!yW?j^ w"P)(,-P!Si:y#;Ǘ17}~>7|I Zo,-OKl_JJS@¸V%E|E;Ʊ'Q, 5ca'7SְۧO^TFjv01|ѝ6 H {12Z+_+!?C2#l[wwEߡa U 926BhzBF{;Ar[ ׯBN]Ջ)ڏ~y1ً.;޻Tԃˆ C?sw,P׮Ē0uf'fϟK YxWl괟]'X*$Ag=80} 彗ܦ,μp yt;FdHⰎ`%=`AemȲ$+_!t ΠTuӳ陽1HƂrZE˼$"'j'(I 0±#'|jE'v6jfYFWǯozKc^* ]D1r{񝨌ZY=s#VVy=h!:QUImVp߭9Iǵg;quk|U/fS*uk8bHlrTZ,xɷOo~bkӋI]k#YtEa[.]f a|0FxV=V'.geZR!Dz5sOF(^7xŕ[W4/+hD8dژ[y+[D)] {ۅXqĊ 8Aо3Qq͍X<AۏbVby+~--Á.C/ E'Km\sPj+A#@{g JE#?0wT Р8(xO5cN'zYɷC{4D!:|Bx=tV tzshbӌgϷUm`cF cFf{T2Kw4uR\5ط7}y$+b#IJ HK>YXK={wyR^i){;F@\\n ';:/CGe\;˗cg۟CR[QAneoؽzVKH/j&?FgFGNL* 7 O8 svN@vc}z$AZ1ʘ[,Li 8(IFv4~\n|{[m~?×.|w_}흥.鰘"%>~2gʳ^ Hn<˭:n]pU<\>EEfW,8Hixud,v]Y!J7g=P ?1}&T|=lk/7Yq$OXOwGwtѡ*nӏl[p: O%6Ca_LDQg8Q(A! qHkT_=K}n/xy9Dcڬ h1'?{_912x|w>󣧭r4 +ə諄hxFFxg6q+~ml}Yoyl:ٌ)Š3aXVSPOR;8-xWr[~:F[c{4Ĥgޢ(a~1 Tyvxϴaw^{o}&N;ἓ 3Шؿ(+0e(#{ {aYc?|57K> SgvXPj+OIW33 _ފ\7N4g+NWayj>0ľX0E_ "BsZ.F/K'>ྷZ s/(u+/iuJ"xtvR4bdC{p͏,8#Fr I0 "D:|߃Xb(7Z! #ăB˕7>73:᫰^ {b7vm=Hr>A+oXRfxoF-_ Eݹ~f̝ yʇ_yZcXBAI NaGFo AK]UkgL[JJ) >(B^ p<E*#59bar yÃvͩ &2\4ȌAQGaA;Uׯ==40[ח8fYds=ZPmQaݢ6K)}34#BkVXu"˭U|H릝0BgWλ㾷B>xE=_aۆؽ~M 0)IpPח,)4${2\y+7-*6'XJUk$9^~bW>TH|{(gqcĆ2ZUo:oyV^enB΋,n:^Y$r^BGW05㾙O:[?x9>ŷbҙ(}4!4_2p'ЦQ" Sq%O>y1׹xw|I>% Y2 X5.Lm8I(ςsC@ZJӎRC' S^[~]w1 LO_=Y#:$?'YC; D(co~YƴZO)h(|W H{F>Ga_y;6?|!‘q%MW$*3/ցaJO;Wc1ϹuNF!cƜv_sn. M;}_ns!]e.cE鳻0=qc/?]=m-.sݛ:wTq͍0o{?6=Jz HcIN!qumEqBY3nI6ʚsE8Q=zB OGyӵg]νtN;w|ӗn.ұF-@?N#rgJ5Tt`]=Y\)zyqCSGk(|i:Cpr `Jk kG*q4"7OiEq3{jgW>~@@?a\-Į5mV~SQ@WOKX'UBO^- sN19&Njo&\qN:cr{kx\M19~M]B~Wacre&UoIMcrLa\'6LwQB6_'_ I7srLAPG> bLCBlc04Xjyrö1\%?Gv a&?%pӣW#tY $"e 4 tJZ{eMI NWE>ð+uQ Q\%?QӴ0Yu꟔&Q$@NfPLSM*Xm<';fO_28fJ_zkwN31X_e ,Vi|3;2H'2i(Mdž53Iڣϧ^0 ӄ؟$W_bj,]$w<'*ƨ/'2#t3i32CIq[mX+~MrnTш,YK@ohF-8=,dXh"ˁWIa\9[[A#qlN>/4O<״qM:L/踌>@9B!a55XճleN."v:9dt"lLr|imeG_Ӏ6F3bqbe@J35J#-K$/=bڙԍ girpbv2͌g4a-6Ffb\ ;1mínơ36^}&m7Y4l/r`qt>cdw49& {&VBn UE#5VkZ5{W;90E7D8rqU-_/vPi=+m5O}kfXF3PTd2-i*h//qi'ҍa_aI:md,zܒmeA6M/8Ш J&[fYjEN&)[b5MM3p q!!a (zb ԨE\}KOd ̞{ܚϮN碁%nDؙFr8,SN&5H_p w<_ 團3 #7w/95e G NKfV^81\-3D $bEc伮04i5]@]q26t=xH{: I0sW`>&MsR菘}6Wbu"."PH?bCTJiyt*|qNY ҁ5eV$6M262w*2 R%͌u8%=g3 U܄ϮK51rZ\n~畷y4rvN3#"4_&v0h:F Mq̳8Pnk |ͯ΀.m2yD-HԄ}I(b"п/`^;qi`YҮw޿q 2|j7]pg$ꓣp"WIH0l 0qhhDyE3T燔׌Om&M+N2W9'rܛ ǐHM=Mc-gK=m7IPP Y*FD+bNX9$9PX6\7bp.Fl@nѱMHgZL'繜Ts=[Fk T NbRP2KE ԂDD8>lB|8qR \Iu9nLk.Ɣkm5Ʀ[Cqŧ[]D$!AusjV ɘޔ*%~h'&P XLyGU5ڳ02 0d<)8lĕ='6(tQ2\&!7yH8bf dY:^?T[g3@OB# i͓IDg2Np3,b}u }sxB-AK\r_EIqOsBH:t w#vÈt97HlKk,wMM/d a6бIS gL;0i@L2M[2XqX?N@e-B¸ X|Ͱks音>ѥǑ47#_l$^hAI%Iۅ"RH7{&sDM )áa$SA{'-zi$Ba;x8khQFII4ܲ0JFwDFzvٜc]kn",^#YJTDMtK$ ~db?}0"HjNK;gu9af!:#WxWNV5K2V\4NDS@ò9XҖ)nx$ʱ!q/8ճʊAW)#F`#} ~ r@ X+3q=;1A&IdxsUP" EJI4 MsW) $'FŢ9=@7!1 Y;F87#IC3P&inH&A,֝WٿcTiuɁtGBia< Hwf;lIR AY37ua[8$bgz:]Ug6B-;mboᘬNefLPcNlEamaA\ʐ&r܄܄Xr2MD;&(dJΨL!ۘh(]VӍ`!.r V>Qqb3[VfSsz5]eg&iS_>rΨ -ND zKI;5f?b-39fչ22||Vn=r_G~cd' 2jyebW >YH rE!dMa (sJ_hM,4v1rjZ IDATFg"-ºWEҰ#L]x餗G7I"7+H-s8GR?z# L&κ{l (|.>$".ޜX!^269Ԣd <'kIԜ<7 -/̀E2@5 "3 6H0%`Q-G R'̒cfp9?ĆI!"N&GA[vsbwl(X2e(-.*cddS`}DFsFO$j@v,psWP/ke&aaJHԮ:P<< I6OQݛ6wY?DE wYd뙲5be2YQ,gKB2+(֤Hd=<G j܀rt|3, R,ȕ9fj|V@L Ѓ98.d h>|Y~"-Lti^ j9ĜGgsQRߔʃ2B2+`Ȝ _/Rz-pb-eQБ(4jIs-M;)l2YVL"8YEr !ʹ&%"zF󹌅L{ .`#bkHjd  (aA;2]%v1;z3#2viT*r3Jahev?$Hs!KO11OBz HIu@ec6UbC#Ť&O&X#̹EA02IY<#ɍ\M伔8*dI4gbٰ\Z9\KIfJ!KCqg>&3 m+(#jUjc+H'Mj 9WE~'1$ Y/d$^6&R<É%0%lm^##sQjLcʌbtYI7ʯEVI "'ڱ%#!Œ29٨6tzLldF!J/lJّlD5UVE[/q5)lMe8z AZYgF]LD,Rй1^ cQ%Ýi<0bQ\yr9ϩ 28d;(좙d:2"3ZR;ڦ—TsfYHKִIUX.kT [#aY"7"G[|aMcOl&2˂'2CFNl WMj{X'%dMo>ҽ kID2N̦KXɞzHl!o;Y_bXzI"Py2J7dyLߕe?h &!RFN8/A摑^BjvRWs鞥J kɯ"I+ڬYHZ $%.2\7)o"^Ъa쑺6gh9Y}t=$-#ZèAFSeEsKN ΆHw$ C c]Fz;2j3td X[$6zF;#4, d䐒e@XӮvM#gv_rI8~%PKyɊ&8X$sNV}^!4S!\Hs=iuyl5nS+mD@ld$д[ٻQ0ЬPI]YE5עJ$zDԯIa,F(˲dr e{$%h)0x= 6Rer&8\LsAwe8efGͦ0YfK%ٖ6XᨏZAۛ1=+d0VzYXNY!i-dZt)'8EIvO<l,d ږȞIokkd0ѩ\\>U9Hh7pac%%wsI] z zzf ߻ǰ7L"IIuUȹ&̓:ۅ5b]|b] 盔,HdH>.% wQhi Q f^ MCJߕ f ,c4Иn^x\u5"cT  z,5¥s1 cr25&c!Bn0̆>ͩT$ηiq̠ffS^OGZXVps4>w&y1 T\V91Z5fCIQ-rLwrl024d6E!6[8 +)h4$ Cͺǚ*6$YMm2df00!1|Ĉ0kjj) փ"äY{]Dɂ}rcE3˒Bml<@ /k꺭,זzgZaJCwgY/E>lcUezvUMٺCnZA֠BYwk.Kqf)/CZӭ.,=TkF(+BE2Wۮse__λ96̽#{W\qaC% x*'SYzg}ZVvW%ru)0 Pʖ y/8"ɏȮ"b@E ?5&][Hd&FHОLَvMSw̌G%›r,4B 'L).1VD&dpޤ DI}\8so܂q&}|ˎ1#[@"螮C0TfC 0f@&p}]7<98 }]Il0K&;GH EMy:青McH!k3(fLB1w1(i6R)p®߉FWQ^P"O\D -凅as@O1DvX6/@(4B)U̜:9QŴp.yEyT}|j!m,křJDʎ OF檥++]9l;h7V_"FyXxɂf~j };t M";/.<ƅgl0of?<Z3(`v`ۮ987,υAܤ}t`1v ^"B\iKcթ{;B@)FذPD*> zvc߁8pAP% [w+s >lTy$E?e{0wJP N y7c{p!BK@"Hvp ߒfU-Tcl%4L'6A 79ϒ Hfu~Pk[C`)u 瞱+jSnL׍B!uSZ<+,sf w0g5x^AK֖% ZԦq W9;k2FdV CLk`ƒ= \Ll=qcSqj"F榮7V-.}j؝;V؅OهQL09mbAH!`.K3-A j]p>ߺ1tךݿg5~z)|Di@a1"p4 ?=*o/1mK`y(jAab!Zf3 s6b3c!GA6e *ڪx+ƖqSnR U{43AmBat%sVnkb~~QfS@RaWkV`󶣸Xy1* >=\3LM49b 3eN݃3P=]gGpnSϞۦ\2 ^U{FX3GwV?.8zoIM |j.)xK2Rl%MDd'h\};hkZ?.뀌GA T^K1! k4W4\eg^4v:'Q(/Ţyزn l3 BIt2^->~u,onn=GX5 80#(b+֨mBSbΌc(NwW^<㡵_0([M(dQbC:\)ƄӨ5:g <=QK<#nv$ YzBkx|HT.bI3ATh((<o{n1xØ1uH:A:/>?zʳVF!"f%ߖrԢ'+:w8RAv\יܛ[jþH$ "%%Ylv-g"&f&f;&1=Yz"۶QLj_H$BTַdG˼U-)J(D{w}{.w+'qڤl:K;&ۻiqx=?uoɲ5Z+09=/DkpfM=VuD"AW<}vnrw(钘>fːYy٪K{ez `U ^ߧ@6Y҉&j6gn5b~cGytDbkJ0oh_|"?gٺyp{&"IU5GMuL ?#GT@ɗ {O||/& Ƙ 2 Cz>U*°hu4N4G诺 26YE*L,F kzU5Kw-2tXӏ;$I>Zj?<|q<=ˏyD:$&Ƙ-%jP ~ؿxY>0&.eZ%/w;S]<?ac-ᇯS2p(&Ο[uCF}r˝N-&>pǞ%&nЍ$7Ѽ#IM&6*}+ !]q06Mm)lV)G"9u~xXؘ?|?vd!MӭMP8:tRrc=NPmk'2q6Sr.@؃:=Ç3/|E4h%6bFN$2NaRRlX]WHP ;6G6O?^"{$&J!Rx0޼,|˨,2 cLU9"[+Wy=',XP6|7ySt<#q1rfx͞Wï8fkݯf.q䶋t8[zZEZ+6oFzSFLM9H\|YDwwX GЏ{Ķiв@U~/!UwJ[d8C).#ƖGc OlmR@!z h &M:mvˏ<|UwW;(+khl){iva*SeOgYDr3‘ę̈v]a߮+ R/#XXf,Won̛Rn 4&Sc ۷ w/k5]V_H+Őu|ʶ, {NR7JsU) ڡT.HvM) BO[^<$"1zhZ iP(UgK"-aѲZDxը}OqǾ V톢Y^=ɘYJB wܤٳ]=i1­;;||/BO~٫:UT$Gb,<Lxxzwm>?^RRЎxx2Ni҈jQX|#{vV3tPwCv'{GR% #*k];/|;_x̏?k*GߜѺPY=/~^S6 V?rTVzXPQx=>ޘO_{ Hȿ1( a@)u[1t^ Hя|8 1+؎HW~KCBBwnM<≏HQcnWASJxdĿdg_O1ZCU?iydSũT59\ctH%kY5}Z;?<ʧ*HǛYY1ɱ.GjOI"#YZly]|cTvYDxϽ{t:RWR7b$ NuZ*I(-2ؙKۄ$=3?: YV CAv)D{[ qbǗ?2$B9u_ɻ@rw΂-4؋.jeᫀPI%^u]G BK: |0[LیZ BBMhdS V/{MOx*$!Z7>:7Xk IDATIlLbzonIzIm:1ٺiWD|EP2'vۍͯW,wOCvt/ܝ!6 S]~{L[ա֖u>nIljKވ :,;gشp`Qn#IhU=\ٌ(o6 awaQn#)~r" ||I3C"R݌gb]k K k 6 eJA9y:u!J8r l˴FC3o_W:c0 t 6ny xh5&1̧/^S5Nt8|k3%&-)ͅnz}?"JZUϿ19_7ixMM=l"K-W|K.bj4:ϵ1~_X IRmu;E9,Y:jYmZۖʫݰ97)ѦkS3d>3%f6jvZH `&JVoT 's!פN0z43 ySlJkژD*D늗O37Ń',w s *$KU+OVtĺօPq+V̮1'Ï>:kksz$IXQJ9WEcl[UKhTJeUzDM)#ӻ (©" 6Zyh Rp[5PKɊf(NVkٲa65٩w"ޚvpp*ͮt4Xe-X5ͺoZ(|F2X${F)FgBP~݊u^lgZy&VF7Pn*XHVPJoFfCҍ,oUK˖垥Pc+ NqJ W'ƺܲJ՛[4|k Mh)(eeceXQ6iQG[x90AlKk-0}՘b\p٢T! &C l{^đgHVX\a8}y4lr,n°oG;ĦOb FRKNΝs,w%}A-?0"Uxtl廪MQ^b) , ft^Qvd[7k7-Zv|3O&R\(R:`R-~bNݷȲ{o_;/wh}$M&\Ν_V*{ a@)[{%Lz8syx6`e?ϵeǚ8j;]9cPX Ss-į1|nn5AO])Ke*%WK)c 5 /JbӘ@q'2Wf%̼ӚvMu ֘tDmEXڦR2wݑJɬĽ$dkz|9OWŐŠ5v6#8TF{ BM˛饝%/3\^;ѬE)Z_cL_Y+*k>ݨG?i!ʖ>fٰnqŬZh5-䗗v.߁5z=+륕<|-61JYR,Y)NJOeG5Ÿ|O3 t3ūiUX+ֿs޸SX3?=yy.ΰЛ/I(κÔu3RJ̅撏(txm[cxd oya.H"O~Cx1FttJ\/Vn\fásʕ鄳WS$ԭxrl A/|Rkn862ӚĤXYs%$vAyʐ"#pɻvTk-ZBBxf/>!}\<3_5{z+Nrz8zk X +>fTTF{uXLGX:W #2ar,s_Ia{t-^ICsT~Fl0-`ܛbe?J8ε6Z>~Q, N_Xkqca~ڭy>y*ɳ8+ӛ: 0h-(]Vga:a'}:䫴y&੘z z ;n|d+?R⡬ʗR">[P)_xx#dJNX Z|:@O*ȧӖ:XoZgko&\Lkmba:mc WeI\gg+0[vݒ26Sqy 0BEбwm [m\S.HU[%Ar7jq+5f>¡D@sɰMu-%*El23&13tM9N}$eʝh hΜrt!X75FX]vku}EC^əR_ b2hQfeOi@Ŕ04Y`,FKJ)[)0QpW*}b <$0o}*6 R[/A:& R˂51ҮD\() U,dab7s%2_? Ӳ4fzV2=&,VN)- ȶ!Ur^qBH)e-US 38I%MYxicebm: 맖G: V(.W>H5 V1\8~_gix+e'6"1)sg_ٻ#- eƗ˳,.ΗM~}/{Y\܀擲87-=ǔ2P*#m8x S Ҁy`gM!RY:Ŗ im.b߆ ,WfJOy*(qTG]JQ6~6c敇E;1 mAorF''ٹmh]/;M?*w[KѶ,smŖ ׌aSmyRJe 2\JPVqI 7_ %x:WS c%8ͰLĀ*JB{Kf|X(11į1fg⯟ :vrez]Zi sś'᪘‘Dn~O' &Li IAa%tvR ٸZv%"ɔy*"ED7aǶowV_41OBhisKA"Ly垃~X80eHYZUS/@wKE0p5̪#'+E/JVVdF*K~<=f|6ɟ.8io5@۬jm 6.p+;8t{r-6}q_OK?X\7y&ΝWN'RAr7:u@5dU,\V0P"h5Hu\t/-Q$UZiU(|y%9l)5Lq'Ξe3}@;V3dLDʸ*z2BP.ThrScmF%UQ5\~IUTU6E)ro]ZOT[Zػ mH0&`* 'Is} w0Y,$`fÑK ?ѯ*3ءC.ݹcKw729δ|8 )Ř&q2 t1uqtVv"C\ZZ;^?>旧¬kM 7vG]$k IT!U|4u E G۫ :RUsIGYuM]f޶Jg@@T`Òܮ"cɶr/H)oUm E3xQQ*|b3;|o I2֎'>E&1IiIټ4 4' -: f@(dHyʡ.XvmvgLjb8eaC8wŃFL=z&tFtG^zmg)Mz}>IrDžbFMqR+o68[^F$ָ9s%nS5e(*҄LbDeYW eI,VYɚr,d)A?˯w?s4@V1Ћ o_/}zQ NjFqfPSf=eriXog,C_Y 7s6Fce6[kW R4 RiQhډxr=GA7S"g2@d25ŖA#q<R-*ʐXՉfMϼX_!JMZ¥de|gdT#bՍQSJ`5IX;T< * qlKj=K CrϰksWnF*UR6O3X&1b MQWo;(x gH+fсxQ)vy+$#"ZDI:YalTZoJI6Q:+Mۨ< fDeN瑏FK73#̉j݋6&Pl^Fe^JB-h\J6JyوQ&8wi|űN7G7Xذn'}V:1VT~GQ R)Cz`,dhV;E1wi&&ƝtTq7jVr8H#&FYH%5 #!аֳx^=չ%%^:0WQK I-w]復!6aR!yߏ܃oNDh9CI8O2sQYsptRJZoYx^4je ¦uLQIq-y͎ݢ:7A4hYX|㕽̃I,Xd|Q(5D|+ $"ܶMmD=۷#LXS [.=^MTH9R1pSĬHm֊(|ZGї_ZnX† бܳJ"Ъ%uy%1!jvӲFO|%vFA`D- FDI@/-I~GG>Wbvmy0kf~2F8g(YuD(lX\qCHCŸ>:e7%󆆨LM5dؘRi'2>wq`\Ƅhi^D1r \/qGnSc2nf'P!08sY_'RY-%o4WkG5VUuNfI!^MLQ)=:xz-Bq8Kiq pvm$KHL Z&b- ej I ]z2.3\>[%IjI*Vu&j?ftzs`Uo R׬_?pۮe^z-]AbTɱZml93ܱ&%o RʂZ2*XLVN:닢&AO eY&ldvFIM#!"u6oH$00(tJ %WLjX&&oӋltQ'>T-t ax,nͩ 2}ӧt8{ǿe`G`|K'Z&{$~f$>;-+{78\0U(`eӥ{K J׾~xi O=؊o`œ.7*w{~bKpϻ^]Fb*[,kKDIFsM t7.neMARqqY[pO%kjeHunS"Fxn{yرŰ{k\8 tKÐTr|3]˱O,vM*cQAX vǬCIus|ws-hmT*]tjgT!M0&&ILyQܧo:e:]~&gh7W"nce bj#N4/nb!oUzFb0ʀ5$6!11mٗ'JR+TKF^55}"$q%s$+S;= _}>ƴ.Jr-ySG$Ѽ뎓l]ςnߺ8goԹͼvfoenn#r.Qy7QXn#6p|7- CpS w:tZ)fCF맗T -Ĉn/2+u3#w)1⳴(8+K]l^Q6fM(eѺԅ[?H8Z 1֭9yB_z]zוfX w6"goY ĉXl6uTe^Z<*EؘDD^k2^/`2)Ĥ5>'4wU)JxgVMeD%p۞sEQRL0N`a 6>pkצ8z|Ͽt$nB65"#_#N4W_C˜s{$% >_9|@wz4̑E,hi<No`R۴~tKq$L.[ʐג}zcI:&Y֦Y[89 7&_lٴGY41ɜ~|ue&vS'Fhػw:O?I7!)76սw6oYE£,@Ĉpr=R/a+\ud*J l'92B`jZ xXNH*[\6\i,+;Ŏ_u O#>G|f[ =$%H3 e6V n-l8MYЋ1I?5Aع*>*J<դh.NH3l8J2{ ϦrΚ ;KVu4.K,'JXq+1wW]tDKb +78`и7]K0)!L&xs(r3Už٪tG#RQ.%CP,.8u~ê>ˡ=YNkuJJIDBFhŕ-;//ҏ굯\dw6ۣÅ˓taj}217KM/rr['2xRPV)5lް\뚬P\gu:m<6+-ߵDf&ur7(䔇RlumEg-o(^=qcyn@/'q>ZQ^]j?W ?".q^JfhnDRj Ciq.w*%eDNGAWi,:J׽w%١[)\{A8wa ݠc]c"Cbv4x'(Iz?cͰU-PcbF00 G$ŞQy~s:k{VV>OUytq֍KLujhJAApU&ys썝,C=x}-@P*JWV*Yr8Wkmq/щgD ʠۮq\{?5w]]/35L7꤇M . B]Š *l,K1P9[Uj):Xx]F_u&'RKZ%g":Qi懁qF㖝mCD?w:R#G\aikyM;lE>GlDZ?"/SٳH՞4[(o;)k,t+p_j\QBVDh?y^푄N~h|t)/wutJ1NAY.2?Fp+SWO̪)$ڄ^+pAgON:Ξ6@?S=wic$*8L2L/ǿLm x9EN* T^fú%>6N§Xq RlPXc |_j+t§QYvjc^8 u:ȸ,$&ѓ W#nUO}7[$$^ƆWW kESC RA3Jtx'~NbEêEPLM@M_:*|X>+O=Þ]gӍzȟ K |r ǟ}f[;XYŷ" tPTOt0O|`m{3k7&|h^ Pl r'< /3,tZ]XTM>;W.v3O}o[G2X#heؾ _s'VE7OYX<0G8}gذMX8DcFJ)lc"\Dc𵞉Ɣz&O߇SL5szi5^mÑt}L'+ *Q1\*N#ER"DVߑS&v_daiÔ?qq}7{]ۿʉSxaN c|Hy%ž]+~ ZXW O~+F$WEvx2?rezS˵61l؏WyAvť /[$RE,{vŽwNjuV5/DF'S~&8RYv);~~زou~s/_c4 8Ql<}w=GOթO.\|ꩈf!,*$@?zOM?NB mlAv-biY<Fh9o6m"`(flqU\ ˥\WjL_-5]Wl/TI䯿q+Ԗ-F<]׸pioxhX7cÆvDG(e4)[aHTlWM<:V4"7xV6?MR3465Î-{^ԹIMogqi%Yn=(C#Xxc!<ɾf :O,Gm>~n1OƵOx3>5qf8-lZd7ښeJ>aaIx>n6ji? ,z`7ï$uRz}M+ `x7~9τ08Qt ܑp&XZ6t~S,RldE'xN,ϰ[w\g(EfQ uv#*e/_[QOQMLMB+@:}\-pz834#nӡӽBb,1^I$f#> o7A۩vI4~/i6zT)f[wfHT/i'|%O*5MwAe]y66x-*kAv3;4Q\6h t]7 _y"Khxi5 Vu]Q\V*3O2#/ J6şY0s&9g{Ėu>T['VeDFN#eoA+{Fkr]ׁ{/uB7HA$H$ZԲ8ce[%9Z3ђ$y,xD[3%"5b7g~ ުnJ4W?~sgo®1yH%fC|mx͍KمOK1X9PTBpv"7[@S*qaT֘F<ԥxBiL2\$n]\I<ʈL*]M3FiXN@Y_%p*)VG%g(sIb."d |s)VD1G7rℱYk*' o{>L6X_ua)^{xf[ʊCJ>[MffPotְ1[n]s*P{GsL-S^Xj0Hrvjt@Ա]X[[>s=|WA+0g!<~}0Blb$EdzНQ}rxtpD|ބs])])V6TD\ m H|F /uDwqmϑq)'1Cq%/i0+l]Xڗ8Z=cq`TXw5}t#6]ĪXw`t j.k+Uu.T?I<˽jnh||Z 2*-N&~`x$`/]4̟.mQ@hk2UݨK+w||ioXDڝҽh^<St#J.2DNق~7?ė3!g:@uZ3Tqm F%]>뱴'[?C+Xu>g\]y[)| q ;tF1 EEWTD w ZU"<|;q|fŨՅsRJU?)ϕTj9e=|S~ t,G0|d+_{$GlbĦqŽh=3DFag Z+Ѝ?lFM=3D'#J'(q"r^UXY5í~wGX;ظ^/A F<3k^KVׁu:fX.Z@ED"H|===M ~'#paZHt}@X5˶ iI#Vd4l Ni3L2wwt.G0A.Ӊ¾rsZ bCY4 IDATy yY$FrJ' ս1d J| ;V#n8"Zd_xiJЋfЉKL.* MpsxNޅbn D[O_3 GMGul]8{A *Ruݳ*#$O~ t"5NZ6MFdywQFŢĒBJI H#9JL\<`((Uٕ ~%TeuэGxe0ff& {#iqcŢ,6 Y قA^4Xu*qY52=D4qC 8q`8.7R~Wa2a| ~R,i$ 4hh=3NUWBvXPT^:x× 0S'e"`f>> ~Uk 6я5H ʁ*J]k\u?ŋGQ`~krM X^H@D(/CWdS:u}VG2SUШ.KXZٓо ϊS!GBIJB"֒']̞,Ob2;5H%`tG;qNe|QfcCXݰxJ b$Q=3 ^0ފx fqZp\*1Q8Q'r.8>t-W__so} kε1 ij9>XY'Qq Y@?G7rPEU}Ɵ/MLzL[Ǐ} _yΰ)E0)0<|wȭk~axhp91v_zq<=/q< d;"ARu#aE+r`Jݶ}W꺨Ԥ1a7n YN5|S6 (G%!81A(]PrbE{Mn #xAȈYAI!TY҆e]3ˬ N%ZwgГ|ߛر >хUg˫ H!nq_/0ڂae 1E h e*l+ۇ58cO]cx-# ϸ yָ>"Rcw^]V`. Gs>"<#zRWn8hP6neNw~E} b~uO]*EX]gYxi?B'_$$%Q3a<_t̰bTUϭNA]ac m# 3iB5KG{x.jy )fWh~d>F:؞ ./`2#ke[Y}ލ;|*}F)k 0;;Sc.CD/O-%Lɳ}"jiRr gYƩS;O+SX(3bP,':& `*WT̘X uWNdӜ\WSDXYq鯍gغaf \  o#π% fhAcqO\ٺ#NrYs:ɹ1҈ϲl5d}K7q ؙa\١][*8jQF1g [|\̼Mqv1żp"DUmAX+J6GaJRU[۾XnhvEʙeX RUב#g-Vk.*%ejbUDe C1R3f8 F;ZGͩ#1a &y,ؘ#PoKmνQAzG !ϧG9:-rYA\* *oH¡Ap *F_ VJ]LlI4B+3[*7*KYwk&8ѱ,Xboh5 y mO~em؛5 hLW ZultGlz0:)UɩKН= CFxMA>QUJ]@A 8b2@P**]_*dAYKaJ7 %VNSKH];4s@iO\baiVL;7S-7QjnMeZ}-[Ks5Rq%56QĺWhM)R˹6ȡP$RS!ca"pYb02p5Da umCvf̺,+)2vftJQFSdQ.YHbIaǚL۞3%-Vpp>pAl\]<Pu ګe)qL$_k80>'p>̓ \̨ⰰ"NIW8aa3VVe樊1' EXY!CB:zY#șD LqYT/r9eP*L{%oIzpLèX4!+VTR|Elsu}'Hzh5)OL>]iduU1XaDX$]]+3BT.(]TJ aU;F.Nsg.NJTS6p1lZ:*{-!!ɶgVuUEk9Y71e+6T9|TqeTAn=l"`!ϞMѳK 2\ 4;i!W ï\v<؁~pJ7xk j@yA0k,I9t fibM +E fIS^<=E"g4#Ѳ绨J+vq uK=nFIecb,`ŗrɸ V Wb~,sؿ& 3.qJ^jL5A*aʗUkV0,I¨W'e](H`7̔oԢ`7G [-?$m[ 񦔫l pb"*_8M#?V5ND @XV@ ahQeM"3*;!iG,750Bdvrs) ; ~Dd}8yYH9*I π#?V& y; ^>z~\EFҊJ<-SF ף8eH|Z| d eMzKtWAft|%!0T%AFBY䏕hLld)eA03pR(ĵTeTJ"̬dM,Tz{"*XPib [.^uB{d .]HKܲ>~!)9@ Xq/.i3B<7a'`J!ذ>란Q5U+$>LS/Oe?svpLjJ $qZIyԀR6mǖC I`($ >62܌-j\/@V PlR}Ҧ=au~Vyq47{6;P kѕ ~?OIGAhQI/C|'n=}qM\^l[g8D+<&ՒQep{[Epj3%F lD~9ac]"{m' Uo P]D(-PU؄CטlVWsHł`j-B/jsnt)q _Vlh !?mWކhȶ`N͕, 6%x Q8/sxJdToVy!Y'"Evȇlا<>uI첰|Mx"ڰХFii`{azS{Ddec3#{Z^^Pb$-)\Jhs~sZ  fGSܔmFޡ[NT^̟Pk+_S." Հ(g\S'@h>sQ/8״l9 bZ0 @AlGTs4~4}ySDDY_K6%߮IԠRֿYOMOߛZqVM[텙P3 . TkojC{_3qW4 \/Udj ^h"+HU #85J`_5v\R/9̬-$Fky<j9r s[kORkiFXm ڤcUr|.,jBiߛ=xsg[ɶĿfa$_9,G%&֭N5},lō%h=ꒀmF:$9)7igZrBBe]ZoVnV( bj 05TP s K\p(aur@L{i#QʹЦ_)A X 3P |!sXdif!cQ䝪J@s0:4/j4XI8&2$".vܞ MZQ}=x̞7X˲$j Sܲi(aL[~&Ykޫ'x9W%4Q>Z6d3m~t8Z  ;WT1domlqBKǹty^Nͬt3g 'FIoJB5CԿARVe%S@k9eB ;w>/ 1<7hB$&BBՈ'3 T#@KT&q4&(@k 6cX-潴')u a`AZrr .{7n2$,+e e IVwZWj LS-7!]h ؉`zZb@Z) IDATE@k {S9_>& )X؇Rڄ 9˲TYdr8.j(jR YO~N--|w* s8RW0b~(>ՠ7I-QU[=f#y6X#"l@|- {]qEE݉–sKKVHꙆ? 5MۋݛC TSbVeKc@d̿TRH<9-Dǩ( 4+}B `ViD]/q=84jhsoH:?2Xy;+|9V8(Q<?)YbXbWVz!jZ2$W5IoNHWgRe@C˓$M>Y+RE"?eת_Up{"-Ĥ,nmXH,$BJ}_'M;1DF Qò9Ҁf!T';ā# 4u<3n!&##YHb"6${x e/7ׇтEɵA֖$b/ρ>,σCTHѭmdPӆ7W)9$sȚ/jsSB\[]J2h̔<ðd" .MU{V0^f*Pe-5[apM@j`ג)fq#h(qw9 k0!彥Z_|7hH0 #HYҼ!%d- 6%(ЦA'A+c!]qRSˎLΣ6 {8W,X\ugDÞ X 6,ZH!d&uf_CaYQfb;P"AD`yER>;0H2 -B]bR6%L"8xS*fZ Y^a&Bz%/L 4_sb X(IZVG%*OW5Y.ZtXW4zʼn:BhIYNme1 LұYjWf. UÀEKt KJPy` v\^yޑl5+񸑾2M4U#-/yYdWnYe9`!46ښ=JÃÄ=)/O9aZ26 1`}{7{M#%ۯMЛЛ3զz&ک)?1gIg2C`NS\̃w 46꟯cNy^X8͒ %,(xg߰g >Hed#]8FJSBMYAHlNRU" h?Ϧ8o_ž rC~Ia<ºPiAiArs. h"ϜAz&Qm.nc.Il%pAK6NER1*OكsZu%A@y6_;%5jo캧m:bl{E[켮SW)?ٜasd-ǃ>g*L>ۥkl¸۰>2"92'DKt)BJtMZ<#43t.L `Apj/Qc-hCx%|BU3Z32fq`"Rpa-M,NN>1ue$*I5s\=݀ Nbs@՗;NZ'\ϛ׹]1o.EqͥAz,S#TI`ck-HeU&PL\"ດXC,@2xކ_7~/]wWZƗ L zUq˅nsD]X;+q&PF PD!>4E &TH절C$w53k5sL [:H7{wDs=P% Wz>p_P8lH3|B*Sى%mAbR]*1}_[w,?&$iBSXVM0Q C4 ʇz%yl^ a@'AzlbI^$pXrp/,mƸ]o]e4 kdSWG~l}%ac4iڧO8iA'܂V).qQ%!ᚱHϛ.X]!oGwhNlQl>OƱp30]k^tp /܀½ xG9-RÀq4]r96"T2Kre9d#I&NQSڌf@BQxw Yn?Y%zQ.qmAװy77zdFcb|=}?ؽetzpŇ1: iR(XG)=:j+!ByN20Q ^K#8Hp|)@4q! 'ЍfaTXwp+}/J: ц(NR|7>unnW73}wwGb#}6Ev7Z xy'&v%vޫ1LOamSI5Cnz,Z^sEK=1oB}t?^0.*ڎ+)ȿEczh9Ŗ V _ʣxf4P D +/V0U]zv{ U EX70"lbm8ЏfqV:<]f<?G' Q>{oLyDj9([A!a9CFĵjBlkzylS8,~)@g`(#.7k.:>?z :3PACÁT F*[s*4,c~Iϴ?dH@b) "1֦8;Z$R R ,f0qhrG(MP-8\-\?^3ꄆraLw<FMBe9ap*4ş'q_? S "A:0*"QW0?7ȲzxX__!m  <17.02!_ @Ȋ}pʦ5#7PZdbGs(3Ca6vt5nx૏o"B{YtbՃV-@N1O>W ]wj+o|9bQ5YX0l9)A=]!N;鄱|,c~l /e:Zw qкx/>ci9 vm_Cx[,d q9#(\vs;vpU e\9ȣxK,;YzChi/sézP :x@C#] ytNaa1MFX:ڽ[hܽ c*tL Ciϲ2v79GK9N<=7p|߆} f݆6?D8:΁.gp-}(.}^*cl-`sV|x3+_?D+J5a/ܷ>} O_אt"҅BC ~`1VpfsW% """w>޷mEgpoqe&E0636{U 22G^:'ro+I';Ow1N "݃Qڍ蘰QŚr 4Esf2l]?ݡ1a:ɰ<‘Ocf̢7DM"'CK2!J`:1!.cyr]Ag@f \Ap3XLYp$ h -qS9ե ,bn!Wz t+`=Sֱaſ=ǽ{hڇ=D`C)sajsF1\p&*"َmWtMkN }4]WnA ct}u0|7GwsbR|Lbc}8ƘN3|#S>Wn7̂,Ao'^A?5Cl:CK_Ճ >_|4N0e FŰASm?k0U.:ƩXYZeWo uUn? _gB׼cWg˚)V6pjMzw0MS]FG L570B 7|xnJxgR PJ!V]9yytХJ EKmlaPԎȠ =ϏAu #[eN Wc2TxWG~杸V!n=U% R6  4Ǒs Cшrr,ONbi2V<2;*X)WpU /?}opv[cB5X:}ÿt=zeY;/ɃX<<ũX>bc1D~< Mi>HgH)UbW#Kڧ {~'ߎ9n2{A<#BJ<VYO܇$.f;^8y }{[/~g~[1x h-mWuÛxRW+8.YPP!n[ Vg|]LqȖ#] /շ j2NqIA}/a*~!6 iS GHSc=?ۛ0cQ };-X=Ⱦ5,NO00US0 D`{Wwե ^:R;" "'p;?fF:x_8D)w~ni0gy氣Ory IDATU߿KpgFA{ma!VȊN3v2KO5nxVl[Q(>Gwaiy2P=}O*co>֝C\?]E)  1[3> ITmL׿m1֐ lΞ^7z?֧16 utaTűpb[jjQip]=8MqWDJ#d6Z7K81]ӭWO#%4D$4Μ\tuW޼c ^Ƶoީğލ3˧:sh>YDy_Gҁ#>PJaulKgNx:^qT!,;+/jjgM*R"h2TU1JC_=@)Pk0Se| (Ph-koP?`#hvm~4^4D \n^WVDWvpg1=(mrΐsJU8n>uZH63w~lk[$gCL') r5lZ@p>IPAès+_q][7f/"'&@7m~<>{Ct bhN_9~Wk?Lgf~}bՅEsxE"7af57 ;T ynx #zxO݊OC3j'iG.2yRǿ4^|0fggH#R q̬z/fLuVxOĩH33Gs4EX?C_~;a/`um  axd.zFCkxcbϥdl5< #=ĺ8á1/êmjVA5 η fn&"D*RMá k@ IlT [w`m 8q x10ފv -ER '' afG mQСXZYtԂ4׹x Af2lPȦy3`S8&hq|d8VQD;j׶+_[2pޫع{lG߅y{Yxo:|7ĩ}) 4G%w؍{ 3D*r%AW 4cغc x/ۂYld͇qO=0 hX /b%$CF o|:R;xWaRY( G.t;l+ww\n0!^>4>8 vVJ__=Yҙ%Q(Hl>d tԨT/Ņe^@ܒu=0S}"y.Sۍ}7lkhîkL^3]f3Tm 澶AKeoVXZZA,mYZQI sIwÎ~6Go,8]Cš1zTP"7+w/瘌Jf+˜ l~Mk;?5."<M 8bh+:V!ӌq6c7ayiODhyS3zhixA|S*Tǘ#LV"Ε \ƈ: Zj X]bj5KW!\k,-$A It8~y|Ɩ|&om/amS`7Gkbt) oB2j̠#;߽zfz3o+7p[P0G0tx!F$dSƬx$h4jM͠O!Be AZ!T>' I]-31&q8s_Ί5W) g_9pg sx]@f1ȚTAGS|jWdA'HoFs0r 6¬{dI\7ef#fVQ2,˟W!מ;& "BBTTLOYNm[7gI&l["txnQ>OƱ#h{CLҍ쿇NK/{ 3{> D$=՚y-O-o$I1t4yJ|"@ "@FeP3GX|vs'V1pђ$>.90d%5K( mOF 6ϲ"B(({=_DPJwߋ v,"CH 24עtMdM]1zAE-'BTQ'}p64kaH͜Fh[4 ƞ2kwSWOax"tb.i7dfO_FɇTEm _PY., 5RN` (Td/g©mCxJPeva+0jE¡5նH[ *ʄV׽Rg!}[8 TCyA'uXLJ#L!֖~a}P0NN|w~gh/lЇ@ =FQVc] jH!!%lAexQ~IR |_]0)!j,anw(C<$!l4D6ֲy\]JY:s%COݙ,>^ťki_(_S+V$>/ލo I_ NcuWs*g/“j ~2ט͛1cUT$*4V{Vh-x#-doaXA@u$#ɃW:DG!^@16ti,l#ܬ5vކzPVƌWՄ4FJ1439x2"?yC"g>ɋ%q>>:@`6r:x9 /?x>B:ehv]{ģr7{Ru c m\q{Hcl83_^i90nG~Sl}EXbDP2(5^(eՌzfkM\] o"ߣ]U %wm{3'2E!Q=D1M!懿 G}D:.VۂS/azk i&2e7~5<9HW®"h3X]^2>?ʥSX^(1uWOQ'!XC\{l3#gQ>tK{Eo(!UtJ Gk\@K{{>kA^!bYc$z NAHwd<cn`}Lo ]Kډ/GTd=I5dЛxѣxSh^&90TFd&U|3&O  xrzm^ 5)wKC@7}t#/ٹ lߵ?oh'$C$z i(H33LAJsGiNjHx W0|ޅxlF84@4/pM]oe?ĉWdFn2|l3Rffo\Vd``R1&ο$)38C M"¾uNRSj\W5ZzB`1NLR߅ax&̩{ɇ'#"B:h)l۵ 7F$EsO )b= Ow 5~Wi,[1lN6o-_}Ww;&D4]p09mޜ[z Sz/ݵt{/U6p$dnt]en"ke!ͳ/qơ 0R~ `F @ZBz>fv_//B=x$ Q>4)?@'xo/G]t`>܋y"\y~ϿQ/V8{tr'y k+[p°pUKU+.» |8 e ij8=ulAwT.XWrZQ<2{M>ؽw?؊:ӏ1Z Ac{on/agsx ?]5Ɲn `/m?q)-jEɏ}wm=S=aem#=l,td7_މ !>0\јlhЬY4ոS@4v^`߭m$:ŕS ~~]Sxg1?{?^{!n 1 c`J &(pkxx{v;~'"{.^К0yn0w8 |a<@! G|Gqۏ_ 3lݎ:a%SvE oL^SJ苧_@;Xى* 33/n`~wuVxCP2AFJ nfdgsá5/0p0{1ݝ<1b=6kgsY 'xO1(5n˰m46.ٍ?/wrLc|*Du 7S}psX9Gv([oU|V; liaش. C\sⲅ [wʛ5F !Cz մS?/zoDov)kfJ>E\z,v6>pZkC?+l`v|LG VCOMjQ_8vG} _{)Ύ!fvuÄvn?8tq}f@2IVuVS|I`i3TZ#Qǡ\||/2y/FIiyxFԶ!)}?ws$ݎC SxmEPC}(ǀT@AkIvcQGF!. y\nb?rIBB#=;>N& Rz*]r?X__37F: $s9,-yvt)_ ]m1IS?=O |*95J?}Y*j-~YATf?Y;SBH27~}0@93WOaz{`6G;,n%=`~泐zNSrzDwGkJAy4fDUJ_?JF #^=4^~Tt[yavwhh/檵'>9| `]llEneYD1^Ooq epof` ȫ pݟ{BqNk4v^AoisG???pIjZO8Sʓ^U.\7Wѝ2]!q">tTF9U.3hF0, xSiJ9 CuR=տ+/h4: BTr2q_x`g+O!89FթF RTS Yk"?qS|q4'&W=Dj-gA*D|^Q9$N2z݌6xGqg ޳)JDC|?=_z}T4/:("MSk_jTtI-ex/gXq{l5֌(u`H!Piߋg8wj9U1r IDATg_9$<1 /•&ƪUg%yfIMa:؃[歸]زcsۧa>N.Czemmtlvjތ^g>u *Ο.z N/#{Vlx 'W“GR)< y=;҅/|HR}G;pzwvl=݊A'cѯErx l!ABoq/ě޹156.B%pC]0Kz-CJڝ[gk-lkWn|>l33q(Of9kxx뇰 띵fxgŬ;nSn qŁuVm @ysǗܡxR,`tH3̱/F؈W'_ "9Nžt( W_c0Y YG#:uɀ3*CK;0XL" elLVqS*:TC "~Ux?ǵہKsNn3GpyGO3+@_fL;;܊?P `4<qnp 1h{\ ]J16eI,΀:3!(ld42tVt9 'XÙcX?a:p%1߾BOFHD=LE[1^ny"fA9)$C^B5#x8#)'dQvڋp':lJDS|3`. b>왁R J)qZ0* )̅1lŔ S<"OH V'簸q0>:sႄ'}/1Hlqτ1oC۟$hsXxGǑ`0lÖ^Lpꅙ 5,OcaxX-`!ZA3Nl`*܎7c.X fiهG4: -b~ ̷b:؊X4oh90 1k#d.g0lL# X!R=A9,)FR sGW3Bg1lt<;Ճ'V G&9G 8O3Guqrņ?3ZUb|S*D7Fh`F]`àH=3,z#1w0D@W-z Cf"A%A~R)O $Ƕityt9i2ddUSnK(N3eM>P$>"(X&lJOh)3H-ɇa"̭K.et-Ȅ_ק6({yY0eRMjB.EfI[J˥;nT{{gU #]֝fx9[e=6W2Mnn fyKͨ8^Wަ|hfinfCX8<.2F>YnnX;f2ja]~h0xʃ$Cot'¶sptvbn98|^?W:{H_=nĨ4BK#eB'gTFާ,Jꇝ-9{J$.lMlhא䪭Vÿsc[]sc bX՞7A%O_[bD%vJR+"pummP EĸË\VѠjT *7#BtTblU!?¦ڂ.Tz}ņo*5;m|xt얜mO!VNIc\`Tl=S3V,d-7+SZ=nP"A,jHS'O`N`y Yt]oE&TⵃU*n̩ZB1;\l?+vjH*޺ Ɋ% kiXA^ɲZ/*r*#^c+aI{M(cJ,PBd_y͖j,@dˮ$ Q׸O2Sr%ŭ%غ(AM[ߣ NPn[rmfz)Hv|b*pl*%[q)ouP1lqy=@ "f *pU0=sŕ  VMkVK6ANuUq.W- )>FB5$$g˨c$c.9 Ҷwr L_&q/u෬VN[?!Npr(MP4 8HʝQ c++# ;6!}3LNֿC'Λ}\5dq.cxj!tt-r0m+j' nhԿj7Fk k,&WD6:NM%ncn nQmaUf T~3V]i 0i tל*ޤt 6qθv{/Ң@J6[62u|l*6u4-%U['݉{U[VJoCgD_M>F v4d3$Kpg F]x4vYMr3>x "M'ZGpYGzu c=( 4gƴnץdamAl 6YvUZhAJ]o8AXm;ІuG51a5/%gɁ* c8Yw$A,`IQGS+FN(J *Բb< 97$Q6ݡq:5eWfT66b]{S vlp c \=0J1NDžV=+.<0LQn"EPfULg[l7 ^X]@YUƜ@/b}alLtde yFōLk&\d' Z,evGRe':luHPtd sE5+JNB}+@)0ՉID,XyYj3;n'ʔOK-CQ[%_FtڸYe zw>ƅ[ؠS5ڈ3%Ud7rgj0!1}T[fpg(AY$%OV/Ħ j̵˥>B}DŒU>0]R1Ѻ>)A<4C(ߖ8@֒,)HV7@9w.ǎ C/iݹfk ˺(j Vp[jxNjȠ J<:S? sjVT2 T^%`mKQcVUVDhGYq@.*^S(h 654Rlr#;^!m|^QDfw1_(uK]CO@QX%z^{l,{(citҧLOl~ wB^wWXB?]'8sl41A[FAK 4 L/ln;Ph_u ˗kpZK)1 Ñ)`h Q}Jᱸ~>SSrY;^6~\rIk_i"n,_VFgv[+9fװ Tfx3αr.MM IUQ{u v9nNj H&13#jm'0W7GP+gSa&ؠTvPQ6-XN*0Dn*zx Xiقbn M0^ IDATXbm H9H.J̶E#L_TTMޓɆF<\N}ݩ£ڄAnVz\,gV7QS.+{i SZ}SqezdLJݦͳN8t%zǎQ&E:wL˖*?lCҶ@}#妹%i.GղLqTF 28#J)si+7vn}ak;l1Jh1lAp& u{=x(nWc=Շ gJFgb>4ĸ?WF[@zeG,<1ĸ7;NIt]81 b Op`OH7aH8K|\r,b0L0o{O!c fzQdC+d|~]ۯ/]Hy,.Ji D/Q'研2<+?a;|v x! Ͽ}_?YA*3?hbAxո+vCʜP\'xOq Wɀ^O!dG/1=\)}XPcwXl2Fj 1Sy:×NɘB:1YN?wb"zFM ~"^ :1ϩa? v\S<),%؃/ 0&:e3!a0$HxbjlѴҘL4IdAlp.i a\s཮+=$[1Zx3+>bcܟএmc"cv5C|؊[l בK&8p0.NQ*3nC%<_y rE 6FøPK;`9!XD~7eZ^n8\#6 =m㺆3Vcr[>;,*'.e! {oB^VCw=Rq۰_2Y"iv5n]>G I!:eCX4AkU3Ở$[޽y b!MRL_UqG^=='}/oć~6xWp\ GYT& SKOǠ7ďm8HS]4!.v 5lFx+!t8sX82mREWCϮ`m%R`2G3JD9,NM`.tZ^6QC)<#m7~)÷>SN/'tsGj*J@p9r8Qv%R rb\9].+Tfaܼɨt&c?ޕ.բT7W T͒+nq}@&({!Clq>?YJ(e9ʟK#0nm `|8Hbs:8>s7s1LEh1YSADxCKcÔ?>0 0Yڒ1#{G2(Oѻ_8n,P_|7F1~W?X dQDkC'g?_p;%#<JwdOK$ݛEc8".iR3޿ {O|т/B2 )@5XC۟Fp{T0G➿{5h" 1|E|6ܻ90d8#7n'\#]\DNw WȚ>%([UmT8z35ׅaNwqe.,*Dc_7*K*DsF*:p<򲯊BR>1^%3g;^remhLfBCqIlgn$x-fc u!R@k}ACp{h%G4')n48W6 PJb=:EӾ{ip{_OI _JP! *&կJaXҽNY}`@s f -JUD8aR=Aw[]L%8x˸ˏ`uxG(F-1[<\L0W0I:)H18PuΏNv;YcG {גpG֜.RüCeG UI?* 3kt[^($k3JSŔ•(9[`Zt-4ʛM*_f/(;hB <_DCǛE(+@isز*Y}DdO~1y0:620 uW6q ;ub=9:4 0Ž8fZGXtaC9rZT˶MNA(|_l3,?]xHȢ!֏"EOUox1YЬa"Ch0N0ٜEUc/DkJYxL$rC 5xϮ̊=V,tX?b߫aOYRGpxfL=pTo,~> +fvW CPI˞1$Iv%!B&q=Uot#(˪(1t n ;p8ZCƈ#W&)=l1ǐU*&AtlO`1/ڥ+ zqtkqJc!R1*GbH uUX#i{b$ȢF!'DZPA\yQT %wkE#(Ι YVY၇Ƒ2Lnncf8f}}""?d$zABUw0J3ǡ ;֡H(U%W؉#;ۘ9]#bָ;u.cU䘿Xj8;,$K7?cEN`O3,gp<{ kfkR!D[D!j/E$1F9G '?`0\('<h-5a<%L .SXM Sc?(N13@2h@+ >ܸUa vTן.u}=JḚ7_EYPxI#QR3BP?,r5^@Բ+Fx(c;p2p {@%5ʡH\Yvh 8"| {%aò BͦuZ Z]H` ܀6k^fC*CU2N3bζnkujEGPaT8q,n@ >L_<>̌Qrp0Ai"hzۧ`AF*Qq3Ңc>LXW D*B%&ba(&ݴ j9TXBI)h /|&GOhC'8Cxא &4ǡU51=7~7qU&C(mNmK@^M F ^9P`vsb,ybK¦@x޹fSIHp_#%R,-n8JxLRh#5_eN_ףבIt]])[Ń K_dbʂd_Tܴ"V) E Rd+?w%ۢO|{.fDjٙPK|" rQa>jUSui ]-Xp_+`|`9m%0s4r]RS5.Peߘ.Aݕ1N޻O&҈TT eؽPArIc9?AL XJlpl2" ,@D `TզSw%Ȅ/RLu _CFegsqcG/wc!F*vRf8gGS͎=1@XHt3e(9pz;M-H}{{SU$CiNz1w=ʑuu}1?jKw`ܘA$ \;3ǍeOan N=P]d]ƅ)5?8ft^FSdf.])"#o`*v렻SKl8r-8&C^ v ȡKgL"c8)wޭuT퉷'4>=J8>'>x[XEQy'jV[(g(9?pwDVcӮ)̸DCEX ߢqm=$"vnkK" *_ IꓻOJnd2cʢDž %\e\{a.p>#G70XONHv'fwXzNm܇{1Z+RxjX71(A^!B } * 9Nj  ɛڶKVt`rm`X{ v['TroL<; a5L'3<hH<'˪yH4QlmW uIy'ƹ'pTWxq CHU3јp]PQ IDAT<'񾟺 G?Gk@ct`t& /)x"N`ȠV+|2#p|vϊ=eAӬ!d*!|Q^Y mt"d} ~|ׯl,K= fFYV/>q5bh2u-{)N~Wtov:U}j6Q݌7T7WB#5Q "#5<.Kٚ,JL9%~_¿_uդq&P 4Ew"/%>H n(&wpZ`^>Lc7)[5XgqݐDE+{ B}?]tj} \-![C0^b r gϾkn"123T"C7$fLXDxw =*v{P8+9ǬœpHcV`R!S(@!Hgekxo|~ظc 0iȆ .ǿ,_gPE331OUP\`oaRlV\cSgn`w:&m\cLs<߯c+6=ݛS(\1(=Adx37pMnvr4=sGH2=@k3޶LdFIzOxO>~eIG"ag/ч/9=lttT1f{K1/!93K|@r}ZR:p/)*0čQ\u%s(ef'z6.R{-diWٽW)j{ܗ4L8=l-6y [ehbДYE("+8iPo"n/b^L0HGށ+K92Q3MI"D&K:{+(QCE=6fA2 Y;{#>?} 8XCST綡L%t (a'ǰo`Wjeu",5H7La8xy)HQj&v_4er@2 Hdi LMf0faUHWrKͱZR&")ƴK0HxDeMQuPB=7FX\cVB+a'i q,0/w 35BV!08ch ]lbryRJ04+u 9qt5bMqRheqB,@JAqs$:EG0*! ,@ Aʪ@ٿH0wܔ!24S0 Tq|+f=7JBE23s m F1vgMQDN2ԍ JYj|+CHL=@ V} 4f˥іwfSh=l9=FB7J@!EHp-gYBǖ MS$@VˣwʥJsI$ePZ^l+Y10!N1V $ALJ0VDZ@VTMfeR@+E9@Hh 6X"A`^͠Ia`DC5kr0?oQV!5C fN3hcdfhh "TT0 (,9 f=ӾT4t QrbH 4E0Tb`F(#>WR:08l[CQ1i JC0hHPJ{Jp`uYiַ `7] 4o:C $i8;uѱtF tώ~v izw9"DD Wt2L[I%@:TF Lh5b"(J={E:;y]-wu}i%vZJ[P F֖bgPS1|%&Qn=GFfwҞd`Ҩklr;?M .쵑5<~&M9؁B;[ΒZ&I24"$iwLD@2aѱY>NSEEBLWyE1oԍ~^l^WnT`Vv/.2^di+H8Lk0'qPvȿ.FaX$P8 }]8mOvuj0rwj2?;?3!2̚ˀ,YِE+h 97 !% `Rʙ#AkBNafp]܏ ve;:I*LX+tqvkW`{M,)fE590]n>5q.&؄"qX* <&@7kóLqc&lw N :628A˼\CjuI9jB~;S&Z؀Ddgu)IfN , g  `K~[>t0ْA=ۚ*K'4_,.IK2ff'W.r14r /@; tvz3Ȧ͂Q9s7n|ˍQ{#+'_L{sW_y\Ԗ:Nn`  (vH2丒  tB7( v7̧'y[|3Q67bPG?r& ])Z39i+S 1%$Yw\܏R9 ;wn㙉 BDfئʐ#Eۙ Y-0Jdw,AnI{]F$l$lsd7ܓwJcp@5ߗ.gÂKf+qEOgq]xlU\-GqFffV`a D];HNV黃]VVA yۥCV#lg?aOo %2k1 bQ= rC\tB!1,F+]#O] I< q q˧㸝<97$$3v=G7rѣ8U#I٦AXH(hNU\ŵjRlޤ~  BYO]sͫywٚBL#T956E^kէȝt dIpN sK:]-r,f';F$([w #.\=}*f͖dr'pɩkU: ݔ\nANĶ7RzI㣞8392:^U'Fކ8KE@!65 ( ۖ,mEqfFFQX`[9<җܜ_Ƣ>_͒&Pu+\?p#Rʨ>=^iFĥcs!_d: 1-KEJʞUZ _*5;1xlYItTir[/ ǂa#opK7!l<XӲ {pR`xܺ[S(g o=MȃzX;7?Rgq[P8`Հ"!j=kgN]s&*h;b\uf ,% 4pH@*ARMZf [K HǼ'nT(bzSzn*RjdI0Ndp'U'wYW+1^ VwZHd}+$u Ź.}`R5_c9*G|QTˋr꼜\P X&*P{ɨ M(уAQA `0EpOU(̐P-.VEՄQ(3TP7+W%/7j l2(׼VB"q@|M@p(_ZLs}F\k{{;fSZ4w.o~2VYT7[IDAT_nф‰lXk(_l`|m6PϛknUm&-;w>妭 mE󙋯XlͶtMgH\VϼwXlBl=1H"h"@ VI&OA ͍\b%K0 IENDB`././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/contrib/plugins/murano_heat-translator_plugin/sample/hello_world/manifest.yaml0000664000175000017500000000043000000000000032574 0ustar00zuulzuul00000000000000Author: OASIS TOSCA TC Description: Template for deploying a single server with predefined properties. Format: TOSCA.CSAR/1.1.0 FullName: io.murano.apps.generated.CsarHelloWorld Name: csar_hello_world Tags: - TOSCA-CSAR-generated Template: tosca_helloworld.yaml Type: Application ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1696417899.7051804 murano-16.0.0/contrib/plugins/murano_heat-translator_plugin/sample/wordpress/0000775000175000017500000000000000000000000027623 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/contrib/plugins/murano_heat-translator_plugin/sample/wordpress/README.rst0000664000175000017500000000137400000000000031317 0ustar00zuulzuul00000000000000=============================================================== Build Murano Application Definition Archive from wordpress CSAR =============================================================== In order to build a Murano application definition archive from the wordpress CSAR and the corresponding logo and manifest files, from inside the wordpress folder run this command: 1. Download archive from https://github.com/openstack/heat-translator/raw/0.4.0/translator/tests/data/csar_single_instance_wordpress.zip 2. Rename it to 'csar.zip' 3. *zip csar_wordpress_murano_package.zip csar.zip logo.png manifest.yaml* The resulting file *csar_wordpress_murano_package.zip* is the application definition archive that can be imported into the Murano application catalog. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/contrib/plugins/murano_heat-translator_plugin/sample/wordpress/logo.png0000664000175000017500000026577400000000000031316 0ustar00zuulzuul00000000000000PNG  IHDR,,y}u OiCCPPhotoshop ICC profilexڝSgTS=BKKoR RB&*! J!QEEȠQ, !{kּ> H3Q5 B.@ $pd!s#~<<+"x M0B\t8K@zB@F&S`cbP-`'{[! eDh;VEX0fK9-0IWfH  0Q){`##xFW<+*x<$9E[-qWW.(I+6aa@.y24x6_-"bbϫp@t~,/;m%h^ uf@Wp~<5j>{-]cK'Xto(hw?G%fIq^D$.Tʳ?D*A, `6B$BB dr`)B(Ͱ*`/@4Qhp.U=pa( Aa!ڈbX#!H$ ɈQ"K5H1RT UH=r9\F;2G1Q= C7F dt1r=6Ыhڏ>C03l0.B8, c˱" VcϱwE 6wB aAHXLXNH $4 7 Q'"K&b21XH,#/{C7$C2'ITFnR#,4H#dk9, +ȅ3![ b@qS(RjJ4e2AURݨT5ZBRQ4u9̓IKhhitݕNWGw Ljg(gwLӋT071oUX**| J&*/Tު UUT^S}FU3S ԖUPSSg;goT?~YYLOCQ_ cx,!k u5&|v*=9C3J3WRf?qtN (~))4L1e\kXHQG6EYAJ'\'GgSSݧ M=:.kDwn^Loy}/TmG X $ <5qo</QC]@Caaᄑ.ȽJtq]zۯ6iܟ4)Y3sCQ? 0k߬~OCOg#/c/Wװwa>>r><72Y_7ȷOo_C#dz%gA[z|!?:eAAA!h쐭!ΑiP~aa~ 'W?pX15wCsDDDޛg1O9-J5*>.j<74?.fYXXIlK9.*6nl {/]py.,:@LN8A*%w% yg"/6шC\*NH*Mz쑼5y$3,幄'L Lݛ:v m2=:1qB!Mggfvˬen/kY- BTZ(*geWf͉9+̳ې7ᒶKW-X潬j9(xoʿܔĹdff-[n ڴ VE/(ۻCɾUUMfeI?m]Nmq#׹=TR+Gw- 6 U#pDy  :v{vg/jBFS[b[O>zG499?rCd&ˮ/~јѡ򗓿m|x31^VwwO| (hSЧc3-bKGD pHYs+tIME 9 IDATxweWu%J]9TgVw+K$!B۲M4 m160m,'@2JsN_}«z-DKOzsY{&crL19&crL19&8nۂ[ftv6DP FXP5~VKjyg']&P((0cNFRp( c?̄B~f%ɿk3[>{0D@`aG4㜬-Y3qB:=_5%뵒aS)z@P[ }?}>/|/n59A#쌂 &30GfvF  `P'' \X |E㺇_yZ|y?cHd07P(ʦWs86(djX6 O^?xG?qb;.a[FMqceɜYEv?D0~F:gJMVwr*2~A5]g9&bQ-H#y&ɵ&Ϸ|6 3Yl? ʧ (B16B4j!#`u_͏v֓o;CytKU*!S x1a4 5p )sj~AX/{.9(qE_` b+!0_C6` _gzǦJ)1m[q鍧utOQY=ۅ)EċxrL19UT9b!j0Q e*y>sM`Fbo\E Ql*5 :w;2M4v&}hRF`ڼN,;oqp36<A#"]{.{ptgF2XW{u63uFXv,9{ei4;i&6<,;w64"<͈ M9'1VT^Ҹ2TG1wYϘnޤ79&jf7f-ƌ]QNE"ݫ5țި =۝FjYM19Z5V.eYD[w "Dq0qbx,UШP^S˖Te]Ka7QF|ǞmGwxPP,{ٯfhzs{-b0Rx o ^? ۍrGQy2Puk޺ k VR be8u8i? %d>7{z{k-Ap!`4v^PizG݌~ qryx"oF.OR?}Jk0cNׄ?ߌ{0H26Ra˳p`W?zLĭJm~IA#gz'νtYL{ A06݋Zz pmKO͘5oj'dn6iu7u>6Da铬U,^>n E?b0lk Qih;4 wpYG˒3{񚷜2Z'@ bj>?}/: S4pʹNc5 zw`^{Ru'$stW ==iԷ'-MQHGW [VWC*'9A/J+i[(%8 VQL`Lw'ڻ(|G.$ܩ양j.^sdJ +Oa{轛48vd?1xER+ 폮{>Z\~4V>~"!0rĄ VV`Lss|aƚ8EpB[ޯ-e$~ (̻ m+ߏҊw#8 *O)[2eםwUh(!5D|fRHcЙM01 ܅m/x*mE\5 ^3MĔr#Ox x.Z2_<㧖 r)iZP(;8ҵ>sv^ ^?"ńtH^xc~4zL)oEyՇQX(.Jߋ;[> ;t ѺK.zi8SQsxz,r;eq.!gRVCY|6rtלv\֎ _"(žm}8l#22\ڇő&A#igG{GlPO;{>Vak:vo;ҵ zAƔv!yW?j^ w"P)(,-P!Si:y#;Ǘ17}~>7|I Zo,-OKl_JJS@¸V%E|E;Ʊ'Q, 5ca'7SְۧO^TFjv01|ѝ6 H {12Z+_+!?C2#l[wwEߡa U 926BhzBF{;Ar[ ׯBN]Ջ)ڏ~y1ً.;޻Tԃˆ C?sw,P׮Ē0uf'fϟK YxWl괟]'X*$Ag=80} 彗ܦ,μp yt;FdHⰎ`%=`AemȲ$+_!t ΠTuӳ陽1HƂrZE˼$"'j'(I 0±#'|jE'v6jfYFWǯozKc^* ]D1r{񝨌ZY=s#VVy=h!:QUImVp߭9Iǵg;quk|U/fS*uk8bHlrTZ,xɷOo~bkӋI]k#YtEa[.]f a|0FxV=V'.geZR!Dz5sOF(^7xŕ[W4/+hD8dژ[y+[D)] {ۅXqĊ 8Aо3Qq͍X<AۏbVby+~--Á.C/ E'Km\sPj+A#@{g JE#?0wT Р8(xO5cN'zYɷC{4D!:|Bx=tV tzshbӌgϷUm`cF cFf{T2Kw4uR\5ط7}y$+b#IJ HK>YXK={wyR^i){;F@\\n ';:/CGe\;˗cg۟CR[QAneoؽzVKH/j&?FgFGNL* 7 O8 svN@vc}z$AZ1ʘ[,Li 8(IFv4~\n|{[m~?×.|w_}흥.鰘"%>~2gʳ^ Hn<˭:n]pU<\>EEfW,8Hixud,v]Y!J7g=P ?1}&T|=lk/7Yq$OXOwGwtѡ*nӏl[p: O%6Ca_LDQg8Q(A! qHkT_=K}n/xy9Dcڬ h1'?{_912x|w>󣧭r4 +ə諄hxFFxg6q+~ml}Yoyl:ٌ)Š3aXVSPOR;8-xWr[~:F[c{4Ĥgޢ(a~1 Tyvxϴaw^{o}&N;ἓ 3Шؿ(+0e(#{ {aYc?|57K> SgvXPj+OIW33 _ފ\7N4g+NWayj>0ľX0E_ "BsZ.F/K'>ྷZ s/(u+/iuJ"xtvR4bdC{p͏,8#Fr I0 "D:|߃Xb(7Z! #ăB˕7>73:᫰^ {b7vm=Hr>A+oXRfxoF-_ Eݹ~f̝ yʇ_yZcXBAI NaGFo AK]UkgL[JJ) >(B^ p<E*#59bar yÃvͩ &2\4ȌAQGaA;Uׯ==40[ח8fYds=ZPmQaݢ6K)}34#BkVXu"˭U|H릝0BgWλ㾷B>xE=_aۆؽ~M 0)IpPח,)4${2\y+7-*6'XJUk$9^~bW>TH|{(gqcĆ2ZUo:oyV^enB΋,n:^Y$r^BGW05㾙O:[?x9>ŷbҙ(}4!4_2p'ЦQ" Sq%O>y1׹xw|I>% Y2 X5.Lm8I(ςsC@ZJӎRC' S^[~]w1 LO_=Y#:$?'YC; D(co~YƴZO)h(|W H{F>Ga_y;6?|!‘q%MW$*3/ցaJO;Wc1ϹuNF!cƜv_sn. M;}_ns!]e.cE鳻0=qc/?]=m-.sݛ:wTq͍0o{?6=Jz HcIN!qumEqBY3nI6ʚsE8Q=zB OGyӵg]νtN;w|ӗn.ұF-@?N#rgJ5Tt`]=Y\)zyqCSGk(|i:Cpr `Jk kG*q4"7OiEq3{jgW>~@@?a\-Į5mV~SQ@WOKX'UBO^- sN19&Njo&\qN:cr{kx\M19~M]B~Wacre&UoIMcrLa\'6LwQB6_'_ I7srLAPG> bLCBlc04Xjyrö1\%?Gv a&?%pӣW#tY $"e 4 tJZ{eMI NWE>ð+uQ Q\%?QӴ0Yu꟔&Q$@NfPLSM*Xm<';fO_28fJ_zkwN31X_e ,Vi|3;2H'2i(Mdž53Iڣϧ^0 ӄ؟$W_bj,]$w<'*ƨ/'2#t3i32CIq[mX+~MrnTш,YK@ohF-8=,dXh"ˁWIa\9[[A#qlN>/4O<״qM:L/踌>@9B!a55XճleN."v:9dt"lLr|imeG_Ӏ6F3bqbe@J35J#-K$/=bڙԍ girpbv2͌g4a-6Ffb\ ;1mínơ36^}&m7Y4l/r`qt>cdw49& {&VBn UE#5VkZ5{W;90E7D8rqU-_/vPi=+m5O}kfXF3PTd2-i*h//qi'ҍa_aI:md,zܒmeA6M/8Ш J&[fYjEN&)[b5MM3p q!!a (zb ԨE\}KOd ̞{ܚϮN碁%nDؙFr8,SN&5H_p w<_ 團3 #7w/95e G NKfV^81\-3D $bEc伮04i5]@]q26t=xH{: I0sW`>&MsR菘}6Wbu"."PH?bCTJiyt*|qNY ҁ5eV$6M262w*2 R%͌u8%=g3 U܄ϮK51rZ\n~畷y4rvN3#"4_&v0h:F Mq̳8Pnk |ͯ΀.m2yD-HԄ}I(b"п/`^;qi`YҮw޿q 2|j7]pg$ꓣp"WIH0l 0qhhDyE3T燔׌Om&M+N2W9'rܛ ǐHM=Mc-gK=m7IPP Y*FD+bNX9$9PX6\7bp.Fl@nѱMHgZL'繜Ts=[Fk T NbRP2KE ԂDD8>lB|8qR \Iu9nLk.Ɣkm5Ʀ[Cqŧ[]D$!AusjV ɘޔ*%~h'&P XLyGU5ڳ02 0d<)8lĕ='6(tQ2\&!7yH8bf dY:^?T[g3@OB# i͓IDg2Np3,b}u }sxB-AK\r_EIqOsBH:t w#vÈt97HlKk,wMM/d a6бIS gL;0i@L2M[2XqX?N@e-B¸ X|Ͱks音>ѥǑ47#_l$^hAI%Iۅ"RH7{&sDM )áa$SA{'-zi$Ba;x8khQFII4ܲ0JFwDFzvٜc]kn",^#YJTDMtK$ ~db?}0"HjNK;gu9af!:#WxWNV5K2V\4NDS@ò9XҖ)nx$ʱ!q/8ճʊAW)#F`#} ~ r@ X+3q=;1A&IdxsUP" EJI4 MsW) $'FŢ9=@7!1 Y;F87#IC3P&inH&A,֝WٿcTiuɁtGBia< Hwf;lIR AY37ua[8$bgz:]Ug6B-;mboᘬNefLPcNlEamaA\ʐ&r܄܄Xr2MD;&(dJΨL!ۘh(]VӍ`!.r V>Qqb3[VfSsz5]eg&iS_>rΨ -ND zKI;5f?b-39fչ22||Vn=r_G~cd' 2jyebW >YH rE!dMa (sJ_hM,4v1rjZ IDATFg"-ºWEҰ#L]x餗G7I"7+H-s8GR?z# L&κ{l (|.>$".ޜX!^269Ԣd <'kIԜ<7 -/̀E2@5 "3 6H0%`Q-G R'̒cfp9?ĆI!"N&GA[vsbwl(X2e(-.*cddS`}DFsFO$j@v,psWP/ke&aaJHԮ:P<< I6OQݛ6wY?DE wYd뙲5be2YQ,gKB2+(֤Hd=<G j܀rt|3, R,ȕ9fj|V@L Ѓ98.d h>|Y~"-Lti^ j9ĜGgsQRߔʃ2B2+`Ȝ _/Rz-pb-eQБ(4jIs-M;)l2YVL"8YEr !ʹ&%"zF󹌅L{ .`#bkHjd  (aA;2]%v1;z3#2viT*r3Jahev?$Hs!KO11OBz HIu@ec6UbC#Ť&O&X#̹EA02IY<#ɍ\M伔8*dI4gbٰ\Z9\KIfJ!KCqg>&3 m+(#jUjc+H'Mj 9WE~'1$ Y/d$^6&R<É%0%lm^##sQjLcʌbtYI7ʯEVI "'ڱ%#!Œ29٨6tzLldF!J/lJّlD5UVE[/q5)lMe8z AZYgF]LD,Rй1^ cQ%Ýi<0bQ\yr9ϩ 28d;(좙d:2"3ZR;ڦ—TsfYHKִIUX.kT [#aY"7"G[|aMcOl&2˂'2CFNl WMj{X'%dMo>ҽ kID2N̦KXɞzHl!o;Y_bXzI"Py2J7dyLߕe?h &!RFN8/A摑^BjvRWs鞥J kɯ"I+ڬYHZ $%.2\7)o"^Ъa쑺6gh9Y}t=$-#ZèAFSeEsKN ΆHw$ C c]Fz;2j3td X[$6zF;#4, d䐒e@XӮvM#gv_rI8~%PKyɊ&8X$sNV}^!4S!\Hs=iuyl5nS+mD@ld$д[ٻQ0ЬPI]YE5עJ$zDԯIa,F(˲dr e{$%h)0x= 6Rer&8\LsAwe8efGͦ0YfK%ٖ6XᨏZAۛ1=+d0VzYXNY!i-dZt)'8EIvO<l,d ږȞIokkd0ѩ\\>U9Hh7pac%%wsI] z zzf ߻ǰ7L"IIuUȹ&̓:ۅ5b]|b] 盔,HdH>.% wQhi Q f^ MCJߕ f ,c4Иn^x\u5"cT  z,5¥s1 cr25&c!Bn0̆>ͩT$ηiq̠ffS^OGZXVps4>w&y1 T\V91Z5fCIQ-rLwrl024d6E!6[8 +)h4$ Cͺǚ*6$YMm2df00!1|Ĉ0kjj) փ"äY{]Dɂ}rcE3˒Bml<@ /k꺭,זzgZaJCwgY/E>lcUezvUMٺCnZA֠BYwk.Kqf)/CZӭ.,=TkF(+BE2Wۮse__λ96̽#{W\qaC% x*'SYzg}ZVvW%ru)0 Pʖ y/8"ɏȮ"b@E ?5&][Hd&FHОLَvMSw̌G%›r,4B 'L).1VD&dpޤ DI}\8so܂q&}|ˎ1#[@"螮C0TfC 0f@&p}]7<98 }]Il0K&;GH EMy:青McH!k3(fLB1w1(i6R)p®߉FWQ^P"O\D -凅as@O1DvX6/@(4B)U̜:9QŴp.yEyT}|j!m,křJDʎ OF檥++]9l;h7V_"FyXxɂf~j };t M";/.<ƅgl0of?<Z3(`v`ۮ987,υAܤ}t`1v ^"B\iKcթ{;B@)FذPD*> zvc߁8pAP% [w+s >lTy$E?e{0wJP N y7c{p!BK@"Hvp ߒfU-Tcl%4L'6A 79ϒ Hfu~Pk[C`)u 瞱+jSnL׍B!uSZ<+,sf w0g5x^AK֖% ZԦq W9;k2FdV CLk`ƒ= \Ll=qcSqj"F榮7V-.}j؝;V؅OهQL09mbAH!`.K3-A j]p>ߺ1tךݿg5~z)|Di@a1"p4 ?=*o/1mK`y(jAab!Zf3 s6b3c!GA6e *ڪx+ƖqSnR U{43AmBat%sVnkb~~QfS@RaWkV`󶣸Xy1* >=\3LM49b 3eN݃3P=]gGpnSϞۦ\2 ^U{FX3GwV?.8zoIM |j.)xK2Rl%MDd'h\};hkZ?.뀌GA T^K1! k4W4\eg^4v:'Q(/Ţyزn l3 BIt2^->~u,onn=GX5 80#(b+֨mBSbΌc(NwW^<㡵_0([M(dQbC:\)ƄӨ5:g <=QK<#nv$ YzBkx|HT.bI3ATh((<o{n1xØ1uH:A:/>?zʳVF!"f%ߖrԢ'+:w8RAv\יܛ[jþH$ "%%Ylv-g"&f&f;&1=Yz"۶QLj_H$BTַdG˼U-)J(D{w}{.w+'qڤl:K;&ۻiqx=?uoɲ5Z+09=/DkpfM=VuD"AW<}vnrw(钘>fːYy٪K{ez `U ^ߧ@6Y҉&j6gn5b~cGytDbkJ0oh_|"?gٺyp{&"IU5GMuL ?#GT@ɗ {O||/& Ƙ 2 Cz>U*°hu4N4G诺 26YE*L,F kzU5Kw-2tXӏ;$I>Zj?<|q<=ˏyD:$&Ƙ-%jP ~ؿxY>0&.eZ%/w;S]<?ac-ᇯS2p(&Ο[uCF}r˝N-&>pǞ%&nЍ$7Ѽ#IM&6*}+ !]q06Mm)lV)G"9u~xXؘ?|?vd!MӭMP8:tRrc=NPmk'2q6Sr.@؃:=Ç3/|E4h%6bFN$2NaRRlX]WHP ;6G6O?^"{$&J!Rx0޼,|˨,2 cLU9"[+Wy=',XP6|7ySt<#q1rfx͞Wï8fkݯf.q䶋t8[zZEZ+6oFzSFLM9H\|YDwwX GЏ{Ķiв@U~/!UwJ[d8C).#ƖGc OlmR@!z h &M:mvˏ<|UwW;(+khl){iva*SeOgYDr3‘ę̈v]a߮+ R/#XXf,Won̛Rn 4&Sc ۷ w/k5]V_H+Őu|ʶ, {NR7JsU) ڡT.HvM) BO[^<$"1zhZ iP(UgK"-aѲZDxը}OqǾ V톢Y^=ɘYJB wܤٳ]=i1­;;||/BO~٫:UT$Gb,<Lxxzwm>?^RRЎxx2Ni҈jQX|#{vV3tPwCv'{GR% #*k];/|;_x̏?k*GߜѺPY=/~^S6 V?rTVzXPQx=>ޘO_{ Hȿ1( a@)u[1t^ Hя|8 1+؎HW~KCBBwnM<≏HQcnWASJxdĿdg_O1ZCU?iydSũT59\ctH%kY5}Z;?<ʧ*HǛYY1ɱ.GjOI"#YZly]|cTvYDxϽ{t:RWR7b$ NuZ*I(-2ؙKۄ$=3?: YV CAv)D{[ qbǗ?2$B9u_ɻ@rw΂-4؋.jeᫀPI%^u]G BK: |0[LیZ BBMhdS V/{MOx*$!Z7>:7Xk IDATIlLbzonIzIm:1ٺiWD|EP2'vۍͯW,wOCvt/ܝ!6 S]~{L[ա֖u>nIljKވ :,;gشp`Qn#IhU=\ٌ(o6 awaQn#)~r" ||I3C"R݌gb]k K k 6 eJA9y:u!J8r l˴FC3o_W:c0 t 6ny xh5&1̧/^S5Nt8|k3%&-)ͅnz}?"JZUϿ19_7ixMM=l"K-W|K.bj4:ϵ1~_X IRmu;E9,Y:jYmZۖʫݰ97)ѦkS3d>3%f6jvZH `&JVoT 's!פN0z43 ySlJkژD*D늗O37Ń',w s *$KU+OVtĺօPq+V̮1'Ï>:kksz$IXQJ9WEcl[UKhTJeUzDM)#ӻ (©" 6Zyh Rp[5PKɊf(NVkٲa65٩w"ޚvpp*ͮt4Xe-X5ͺoZ(|F2X${F)FgBP~݊u^lgZy&VF7Pn*XHVPJoFfCҍ,oUK˖垥Pc+ NqJ W'ƺܲJ՛[4|k Mh)(eeceXQ6iQG[x90AlKk-0}՘b\p٢T! &C l{^đgHVX\a8}y4lr,n°oG;ĦOb FRKNΝs,w%}A-?0"Uxtl廪MQ^b) , ft^Qvd[7k7-Zv|3O&R\(R:`R-~bNݷȲ{o_;/wh}$M&\Ν_V*{ a@)[{%Lz8syx6`e?ϵeǚ8j;]9cPX Ss-į1|nn5AO])Ke*%WK)c 5 /JbӘ@q'2Wf%̼ӚvMu ֘tDmEXڦR2wݑJɬĽ$dkz|9OWŐŠ5v6#8TF{ BM˛饝%/3\^;ѬE)Z_cL_Y+*k>ݨG?i!ʖ>fٰnqŬZh5-䗗v.߁5z=+륕<|-61JYR,Y)NJOeG5Ÿ|O3 t3ūiUX+ֿs޸SX3?=yy.ΰЛ/I(κÔu3RJ̅撏(txm[cxd oya.H"O~Cx1FttJ\/Vn\fásʕ鄳WS$ԭxrl A/|Rkn862ӚĤXYs%$vAyʐ"#pɻvTk-ZBBxf/>!}\<3_5{z+Nrz8zk X +>fTTF{uXLGX:W #2ar,s_Ia{t-^ICsT~Fl0-`ܛbe?J8ε6Z>~Q, N_Xkqca~ڭy>y*ɳ8+ӛ: 0h-(]Vga:a'}:䫴y&੘z z ;n|d+?R⡬ʗR">[P)_xx#dJNX Z|:@O*ȧӖ:XoZgko&\Lkmba:mc WeI\gg+0[vݒ26Sqy 0BEбwm [m\S.HU[%Ar7jq+5f>¡D@sɰMu-%*El23&13tM9N}$eʝh hΜrt!X75FX]vku}EC^əR_ b2hQfeOi@Ŕ04Y`,FKJ)[)0QpW*}b <$0o}*6 R[/A:& R˂51ҮD\() U,dab7s%2_? Ӳ4fzV2=&,VN)- ȶ!Ur^qBH)e-US 38I%MYxicebm: 맖G: V(.W>H5 V1\8~_gix+e'6"1)sg_ٻ#- eƗ˳,.ΗM~}/{Y\܀擲87-=ǔ2P*#m8x S Ҁy`gM!RY:Ŗ im.b߆ ,WfJOy*(qTG]JQ6~6c敇E;1 mAorF''ٹmh]/;M?*w[KѶ,smŖ ׌aSmyRJe 2\JPVqI 7_ %x:WS c%8ͰLĀ*JB{Kf|X(11į1fg⯟ :vrez]Zi sś'᪘‘Dn~O' &Li IAa%tvR ٸZv%"ɔy*"ED7aǶowV_41OBhisKA"Ly垃~X80eHYZUS/@wKE0p5̪#'+E/JVVdF*K~<=f|6ɟ.8io5@۬jm 6.p+;8t{r-6}q_OK?X\7y&ΝWN'RAr7:u@5dU,\V0P"h5Hu\t/-Q$UZiU(|y%9l)5Lq'Ξe3}@;V3dLDʸ*z2BP.ThrScmF%UQ5\~IUTU6E)ro]ZOT[Zػ mH0&`* 'Is} w0Y,$`fÑK ?ѯ*3ءC.ݹcKw729δ|8 )Ř&q2 t1uqtVv"C\ZZ;^?>旧¬kM 7vG]$k IT!U|4u E G۫ :RUsIGYuM]f޶Jg@@T`Òܮ"cɶr/H)oUm E3xQQ*|b3;|o I2֎'>E&1IiIټ4 4' -: f@(dHyʡ.XvmvgLjb8eaC8wŃFL=z&tFtG^zmg)Mz}>IrDžbFMqR+o68[^F$ָ9s%nS5e(*҄LbDeYW eI,VYɚr,d)A?˯w?s4@V1Ћ o_/}zQ NjFqfPSf=eriXog,C_Y 7s6Fce6[kW R4 RiQhډxr=GA7S"g2@d25ŖA#q<R-*ʐXՉfMϼX_!JMZ¥de|gdT#bՍQSJ`5IX;T< * qlKj=K CrϰksWnF*UR6O3X&1b MQWo;(x gH+fсxQ)vy+$#"ZDI:YalTZoJI6Q:+Mۨ< fDeN瑏FK73#̉j݋6&Pl^Fe^JB-h\J6JyوQ&8wi|űN7G7Xذn'}V:1VT~GQ R)Cz`,dhV;E1wi&&ƝtTq7jVr8H#&FYH%5 #!аֳx^=չ%%^:0WQK I-w]復!6aR!yߏ܃oNDh9CI8O2sQYsptRJZoYx^4je ¦uLQIq-y͎ݢ:7A4hYX|㕽̃I,Xd|Q(5D|+ $"ܶMmD=۷#LXS [.=^MTH9R1pSĬHm֊(|ZGї_ZnX† бܳJ"Ъ%uy%1!jvӲFO|%vFA`D- FDI@/-I~GG>Wbvmy0kf~2F8g(YuD(lX\qCHCŸ>:e7%󆆨LM5dؘRi'2>wq`\Ƅhi^D1r \/qGnSc2nf'P!08sY_'RY-%o4WkG5VUuNfI!^MLQ)=:xz-Bq8Kiq pvm$KHL Z&b- ej I ]z2.3\>[%IjI*Vu&j?ftzs`Uo R׬_?pۮe^z-]AbTɱZml93ܱ&%o RʂZ2*XLVN:닢&AO eY&ldvFIM#!"u6oH$00(tJ %WLjX&&oӋltQ'>T-t ax,nͩ 2}ӧt8{ǿe`G`|K'Z&{$~f$>;-+{78\0U(`eӥ{K J׾~xi O=؊o`œ.7*w{~bKpϻ^]Fb*[,kKDIFsM t7.neMARqqY[pO%kjeHunS"Fxn{yرŰ{k\8 tKÐTr|3]˱O,vM*cQAX vǬCIus|ws-hmT*]tjgT!M0&&ILyQܧo:e:]~&gh7W"nce bj#N4/nb!oUzFb0ʀ5$6!11mٗ'JR+TKF^55}"$q%s$+S;= _}>ƴ.Jr-ySG$Ѽ뎓l]ςnߺ8goԹͼvfoenn#r.Qy7QXn#6p|7- CpS w:tZ)fCF맗T -Ĉn/2+u3#w)1⳴(8+K]l^Q6fM(eѺԅ[?H8Z 1֭9yB_z]zוfX w6"goY ĉXl6uTe^Z<*EؘDD^k2^/`2)Ĥ5>'4wU)JxgVMeD%p۞sEQRL0N`a 6>pkצ8z|Ͽt$nB65"#_#N4W_C˜s{$% >_9|@wz4̑E,hi<No`R۴~tKq$L.[ʐג}zcI:&Y֦Y[89 7&_lٴGY41ɜ~|ue&vS'Fhػw:O?I7!)76սw6oYE£,@Ĉpr=R/a+\ud*J l'92B`jZ xXNH*[\6\i,+;Ŏ_u O#>G|f[ =$%H3 e6V n-l8MYЋ1I?5Aع*>*J<դh.NH3l8J2{ ϦrΚ ;KVu4.K,'JXq+1wW]tDKb +78`и7]K0)!L&xs(r3Už٪tG#RQ.%CP,.8u~ê>ˡ=YNkuJJIDBFhŕ-;//ҏ굯\dw6ۣÅ˓taj}217KM/rr['2xRPV)5lް\뚬P\gu:m<6+-ߵDf&ur7(䔇RlumEg-o(^=qcyn@/'q>ZQ^]j?W ?".q^JfhnDRj Ciq.w*%eDNGAWi,:J׽w%١[)\{A8wa ݠc]c"Cbv4x'(Iz?cͰU-PcbF00 G$ŞQy~s:k{VV>OUytq֍KLujhJAApU&ys썝,C=x}-@P*JWV*Yr8Wkmq/щgD ʠۮq\{?5w]]/35L7꤇M . B]Š *l,K1P9[Uj):Xx]F_u&'RKZ%g":Qi懁qF㖝mCD?w:R#G\aikyM;lE>GlDZ?"/SٳH՞4[(o;)k,t+p_j\QBVDh?y^푄N~h|t)/wutJ1NAY.2?Fp+SWO̪)$ڄ^+pAgON:Ξ6@?S=wic$*8L2L/ǿLm x9EN* T^fú%>6N§Xq RlPXc |_j+t§QYvjc^8 u:ȸ,$&ѓ W#nUO}7[$$^ƆWW kESC RA3Jtx'~NbEêEPLM@M_:*|X>+O=Þ]gӍzȟ K |r ǟ}f[;XYŷ" tPTOt0O|`m{3k7&|h^ Pl r'< /3,tZ]XTM>;W.v3O}o[G2X#heؾ _s'VE7OYX<0G8}gذMX8DcFJ)lc"\Dc𵞉Ɣz&O߇SL5szi5^mÑt}L'+ *Q1\*N#ER"DVߑS&v_daiÔ?qq}7{]ۿʉSxaN c|Hy%ž]+~ ZXW O~+F$WEvx2?rezS˵61l؏WyAvť /[$RE,{vŽwNjuV5/DF'S~&8RYv);~~زou~s/_c4 8Ql<}w=GOթO.\|ꩈf!,*$@?zOM?NB mlAv-biY<Fh9o6m"`(flqU\ ˥\WjL_-5]Wl/TI䯿q+Ԗ-F<]׸pioxhX7cÆvDG(e4)[aHTlWM<:V4"7xV6?MR3465Î-{^ԹIMogqi%Yn=(C#Xxc!<ɾf :O,Gm>~n1OƵOx3>5qf8-lZd7ښeJ>aaIx>n6ji? ,z`7ï$uRz}M+ `x7~9τ08Qt ܑp&XZ6t~S,RldE'xN,ϰ[w\g(EfQ uv#*e/_[QOQMLMB+@:}\-pz834#nӡӽBb,1^I$f#> o7A۩vI4~/i6zT)f[wfHT/i'|%O*5MwAe]y66x-*kAv3;4Q\6h t]7 _y"Khxi5 Vu]Q\V*3O2#/ J6şY0s&9g{Ėu>T['VeDFN#eoA+{Fkr]ׁ{/uB7HA$H$ZԲ8ce[%9Z3ђ$y,xD[3%"5b7g~ ުnJ4W?~sgo®1yH%fC|mx͍KمOK1X9PTBpv"7[@S*qaT֘F<ԥxBiL2\$n]\I<ʈL*]M3FiXN@Y_%p*)VG%g(sIb."d |s)VD1G7rℱYk*' o{>L6X_ua)^{xf[ʊCJ>[MffPotְ1[n]s*P{GsL-S^Xj0Hrvjt@Ա]X[[>s=|WA+0g!<~}0Blb$EdzНQ}rxtpD|ބs])])V6TD\ m H|F /uDwqmϑq)'1Cq%/i0+l]Xڗ8Z=cq`TXw5}t#6]ĪXw`t j.k+Uu.T?I<˽jnh||Z 2*-N&~`x$`/]4̟.mQ@hk2UݨK+w||ioXDڝҽh^<St#J.2DNق~7?ė3!g:@uZ3Tqm F%]>뱴'[?C+Xu>g\]y[)| q ;tF1 EEWTD w ZU"<|;q|fŨՅsRJU?)ϕTj9e=|S~ t,G0|d+_{$GlbĦqŽh=3DFag Z+Ѝ?lFM=3D'#J'(q"r^UXY5í~wGX;ظ^/A F<3k^KVׁu:fX.Z@ED"H|===M ~'#paZHt}@X5˶ iI#Vd4l Ni3L2wwt.G0A.Ӊ¾rsZ bCY4 IDATy yY$FrJ' ս1d J| ;V#n8"Zd_xiJЋfЉKL.* MpsxNޅbn D[O_3 GMGul]8{A *Ruݳ*#$O~ t"5NZ6MFdywQFŢĒBJI H#9JL\<`((Uٕ ~%TeuэGxe0ff& {#iqcŢ,6 Y قA^4Xu*qY52=D4qC 8q`8.7R~Wa2a| ~R,i$ 4hh=3NUWBvXPT^:x× 0S'e"`f>> ~Uk 6я5H ʁ*J]k\u?ŋGQ`~krM X^H@D(/CWdS:u}VG2SUШ.KXZٓо ϊS!GBIJB"֒']̞,Ob2;5H%`tG;qNe|QfcCXݰxJ b$Q=3 ^0ފx fqZp\*1Q8Q'r.8>t-W__so} kε1 ij9>XY'Qq Y@?G7rPEU}Ɵ/MLzL[Ǐ} _yΰ)E0)0<|wȭk~axhp91v_zq<=/q< d;"ARu#aE+r`Jݶ}W꺨Ԥ1a7n YN5|S6 (G%!81A(]PrbE{Mn #xAȈYAI!TY҆e]3ˬ N%ZwgГ|ߛر >хUg˫ H!nq_/0ڂae 1E h e*l+ۇ58cO]cx-# ϸ yָ>"Rcw^]V`. Gs>"<#zRWn8hP6neNw~E} b~uO]*EX]gYxi?B'_$$%Q3a<_t̰bTUϭNA]ac m# 3iB5KG{x.jy )fWh~d>F:؞ ./`2#ke[Y}ލ;|*}F)k 0;;Sc.CD/O-%Lɳ}"jiRr gYƩS;O+SX(3bP,':& `*WT̘X uWNdӜ\WSDXYq鯍gغaf \  o#π% fhAcqO\ٺ#NrYs:ɹ1҈ϲl5d}K7q ؙa\١][*8jQF1g [|\̼Mqv1żp"DUmAX+J6GaJRU[۾XnhvEʙeX RUב#g-Vk.*%ejbUDe C1R3f8 F;ZGͩ#1a &y,ؘ#PoKmνQAzG !ϧG9:-rYA\* *oH¡Ap *F_ VJ]LlI4B+3[*7*KYwk&8ѱ,Xboh5 y mO~em؛5 hLW ZultGlz0:)UɩKН= CFxMA>QUJ]@A 8b2@P**]_*dAYKaJ7 %VNSKH];4s@iO\baiVL;7S-7QjnMeZ}-[Ks5Rq%56QĺWhM)R˹6ȡP$RS!ca"pYb02p5Da umCvf̺,+)2vftJQFSdQ.YHbIaǚL۞3%-Vpp>pAl\]<Pu ګe)qL$_k80>'p>̓ \̨ⰰ"NIW8aa3VVe樊1' EXY!CB:zY#șD LqYT/r9eP*L{%oIzpLèX4!+VTR|Elsu}'Hzh5)OL>]iduU1XaDX$]]+3BT.(]TJ aU;F.Nsg.NJTS6p1lZ:*{-!!ɶgVuUEk9Y71e+6T9|TqeTAn=l"`!ϞMѳK 2\ 4;i!W ï\v<؁~pJ7xk j@yA0k,I9t fibM +E fIS^<=E"g4#Ѳ绨J+vq uK=nFIecb,`ŗrɸ V Wb~,sؿ& 3.qJ^jL5A*aʗUkV0,I¨W'e](H`7̔oԢ`7G [-?$m[ 񦔫l pb"*_8M#?V5ND @XV@ ahQeM"3*;!iG,750Bdvrs) ; ~Dd}8yYH9*I π#?V& y; ^>z~\EFҊJ<-SF ף8eH|Z| d eMzKtWAft|%!0T%AFBY䏕hLld)eA03pR(ĵTeTJ"̬dM,Tz{"*XPib [.^uB{d .]HKܲ>~!)9@ Xq/.i3B<7a'`J!ذ>란Q5U+$>LS/Oe?svpLjJ $qZIyԀR6mǖC I`($ >62܌-j\/@V PlR}Ҧ=au~Vyq47{6;P kѕ ~?OIGAhQI/C|'n=}qM\^l[g8D+<&ՒQep{[Epj3%F lD~9ac]"{m' Uo P]D(-PU؄CטlVWsHł`j-B/jsnt)q _Vlh !?mWކhȶ`N͕, 6%x Q8/sxJdToVy!Y'"Evȇlا<>uI첰|Mx"ڰХFii`{azS{Ddec3#{Z^^Pb$-)\Jhs~sZ  fGSܔmFޡ[NT^̟Pk+_S." Հ(g\S'@h>sQ/8״l9 bZ0 @AlGTs4~4}ySDDY_K6%߮IԠRֿYOMOߛZqVM[텙P3 . TkojC{_3qW4 \/Udj ^h"+HU #85J`_5v\R/9̬-$Fky<j9r s[kORkiFXm ڤcUr|.,jBiߛ=xsg[ɶĿfa$_9,G%&֭N5},lō%h=ꒀmF:$9)7igZrBBe]ZoVnV( bj 05TP s K\p(aur@L{i#QʹЦ_)A X 3P |!sXdif!cQ䝪J@s0:4/j4XI8&2$".vܞ MZQ}=x̞7X˲$j Sܲi(aL[~&Ykޫ'x9W%4Q>Z6d3m~t8Z  ;WT1domlqBKǹty^Nͬt3g 'FIoJB5CԿARVe%S@k9eB ;w>/ 1<7hB$&BBՈ'3 T#@KT&q4&(@k 6cX-潴')u a`AZrr .{7n2$,+e e IVwZWj LS-7!]h ؉`zZb@Z) IDATE@k {S9_>& )X؇Rڄ 9˲TYdr8.j(jR YO~N--|w* s8RW0b~(>ՠ7I-QU[=f#y6X#"l@|- {]qEE݉–sKKVHꙆ? 5MۋݛC TSbVeKc@d̿TRH<9-Dǩ( 4+}B `ViD]/q=84jhsoH:?2Xy;+|9V8(Q<?)YbXbWVz!jZ2$W5IoNHWgRe@C˓$M>Y+RE"?eת_Up{"-Ĥ,nmXH,$BJ}_'M;1DF Qò9Ҁf!T';ā# 4u<3n!&##YHb"6${x e/7ׇтEɵA֖$b/ρ>,σCTHѭmdPӆ7W)9$sȚ/jsSB\[]J2h̔<ðd" .MU{V0^f*Pe-5[apM@j`ג)fq#h(qw9 k0!彥Z_|7hH0 #HYҼ!%d- 6%(ЦA'A+c!]qRSˎLΣ6 {8W,X\ugDÞ X 6,ZH!d&uf_CaYQfb;P"AD`yER>;0H2 -B]bR6%L"8xS*fZ Y^a&Bz%/L 4_sb X(IZVG%*OW5Y.ZtXW4zʼn:BhIYNme1 LұYjWf. UÀEKt KJPy` v\^yޑl5+񸑾2M4U#-/yYdWnYe9`!46ښ=JÃÄ=)/O9aZ26 1`}{7{M#%ۯMЛЛ3զz&ک)?1gIg2C`NS\̃w 46꟯cNy^X8͒ %,(xg߰g >Hed#]8FJSBMYAHlNRU" h?Ϧ8o_ž rC~Ia<ºPiAiArs. h"ϜAz&Qm.nc.Il%pAK6NER1*OكsZu%A@y6_;%5jo캧m:bl{E[켮SW)?ٜasd-ǃ>g*L>ۥkl¸۰>2"92'DKt)BJtMZ<#43t.L `Apj/Qc-hCx%|BU3Z32fq`"Rpa-M,NN>1ue$*I5s\=݀ Nbs@՗;NZ'\ϛ׹]1o.EqͥAz,S#TI`ck-HeU&PL\"ດXC,@2xކ_7~/]wWZƗ L zUq˅nsD]X;+q&PF PD!>4E &TH절C$w53k5sL [:H7{wDs=P% Wz>p_P8lH3|B*Sى%mAbR]*1}_[w,?&$iBSXVM0Q C4 ʇz%yl^ a@'AzlbI^$pXrp/,mƸ]o]e4 kdSWG~l}%ac4iڧO8iA'܂V).qQ%!ᚱHϛ.X]!oGwhNlQl>OƱp30]k^tp /܀½ xG9-RÀq4]r96"T2Kre9d#I&NQSڌf@BQxw Yn?Y%zQ.qmAװy77zdFcb|=}?ؽetzpŇ1: iR(XG)=:j+!ByN20Q ^K#8Hp|)@4q! 'ЍfaTXwp+}/J: ц(NR|7>unnW73}wwGb#}6Ev7Z xy'&v%vޫ1LOamSI5Cnz,Z^sEK=1oB}t?^0.*ڎ+)ȿEczh9Ŗ V _ʣxf4P D +/V0U]zv{ U EX70"lbm8ЏfqV:<]f<?G' Q>{oLyDj9([A!a9CFĵjBlkzylS8,~)@g`(#.7k.:>?z :3PACÁT F*[s*4,c~Iϴ?dH@b) "1֦8;Z$R R ,f0qhrG(MP-8\-\?^3ꄆraLw<FMBe9ap*4ş'q_? S "A:0*"QW0?7ȲzxX__!m  <17.02!_ @Ȋ}pʦ5#7PZdbGs(3Ca6vt5nx૏o"B{YtbՃV-@N1O>W ]wj+o|9bQ5YX0l9)A=]!N;鄱|,c~l /e:Zw qкx/>ci9 vm_Cx[,d q9#(\vs;vpU e\9ȣxK,;YzChi/sézP :x@C#] ytNaa1MFX:ڽ[hܽ c*tL Ciϲ2v79GK9N<=7p|߆} f݆6?D8:΁.gp-}(.}^*cl-`sV|x3+_?D+J5a/ܷ>} O_אt"҅BC ~`1VpfsW% """w>޷mEgpoqe&E0636{U 22G^:'ro+I';Ow1N "݃Qڍ蘰QŚr 4Esf2l]?ݡ1a:ɰ<‘Ocf̢7DM"'CK2!J`:1!.cyr]Ag@f \Ap3XLYp$ h -qS9ե ,bn!Wz t+`=Sֱaſ=ǽ{hڇ=D`C)sajsF1\p&*"َmWtMkN }4]WnA ct}u0|7GwsbR|Lbc}8ƘN3|#S>Wn7̂,Ao'^A?5Cl:CK_Ճ >_|4N0e FŰASm?k0U.:ƩXYZeWo uUn? _gB׼cWg˚)V6pjMzw0MS]FG L570B 7|xnJxgR PJ!V]9yytХJ EKmlaPԎȠ =ϏAu #[eN Wc2TxWG~杸V!n=U% R6  4Ǒs Cшrr,ONbi2V<2;*X)WpU /?}opv[cB5X:}ÿt=zeY;/ɃX<<ũX>bc1D~< Mi>HgH)UbW#Kڧ {~'ߎ9n2{A<#BJ<VYO܇$.f;^8y }{[/~g~[1x h-mWuÛxRW+8.YPP!n[ Vg|]LqȖ#] /շ j2NqIA}/a*~!6 iS GHSc=?ۛ0cQ };-X=Ⱦ5,NO00US0 D`{Wwե ^:R;" "'p;?fF:x_8D)w~ni0gy氣Ory IDATU߿KpgFA{ma!VȊN3v2KO5nxVl[Q(>Gwaiy2P=}O*co>֝C\?]E)  1[3> ITmL׿m1֐ lΞ^7z?֧16 utaTűpb[jjQip]=8MqWDJ#d6Z7K81]ӭWO#%4D$4Μ\tuW޼c ^Ƶoީğލ3˧:sh>YDy_Gҁ#>PJaulKgNx:^qT!,;+/jjgM*R"h2TU1JC_=@)Pk0Se| (Ph-koP?`#hvm~4^4D \n^WVDWvpg1=(mrΐsJU8n>uZH63w~lk[$gCL') r5lZ@p>IPAès+_q][7f/"'&@7m~<>{Ct bhN_9~Wk?Lgf~}bՅEsxE"7af57 ;T ynx #zxO݊OC3j'iG.2yRǿ4^|0fggH#R q̬z/fLuVxOĩH33Gs4EX?C_~;a/`um  axd.zFCkxcbϥdl5< #=ĺ8á1/êmjVA5 η fn&"D*RMá k@ IlT [w`m 8q x10ފv -ER '' afG mQСXZYtԂ4׹x Af2lPȦy3`S8&hq|d8VQD;j׶+_[2pޫع{lG߅y{Yxo:|7ĩ}) 4G%w؍{ 3D*r%AW 4cغc x/ۂYld͇qO=0 hX /b%$CF o|:R;xWaRY( G.t;l+ww\n0!^>4>8 vVJ__=Yҙ%Q(Hl>d tԨT/Ņe^@ܒu=0S}"y.Sۍ}7lkhîkL^3]f3Tm 澶AKeoVXZZA,mYZQI sIwÎ~6Go,8]Cš1zTP"7+w/瘌Jf+˜ l~Mk;?5."<M 8bh+:V!ӌq6c7ayiODhyS3zhixA|S*Tǘ#LV"Ε \ƈ: Zj X]bj5KW!\k,-$A It8~y|Ɩ|&om/amS`7Gkbt) oB2j̠#;߽zfz3o+7p[P0G0tx!F$dSƬx$h4jM͠O!Be AZ!T>' I]-31&q8s_Ί5W) g_9pg sx]@f1ȚTAGS|jWdA'HoFs0r 6¬{dI\7ef#fVQ2,˟W!מ;& "BBTTLOYNm[7gI&l["txnQ>OƱ#h{CLҍ쿇NK/{ 3{> D$=՚y-O-o$I1t4yJ|"@ "@FeP3GX|vs'V1pђ$>.90d%5K( mOF 6ϲ"B(({=_DPJwߋ v,"CH 24עtMdM]1zAE-'BTQ'}p64kaH͜Fh[4 ƞ2kwSWOax"tb.i7dfO_FɇTEm _PY., 5RN` (Td/g©mCxJPeva+0jE¡5նH[ *ʄV׽Rg!}[8 TCyA'uXLJ#L!֖~a}P0NN|w~gh/lЇ@ =FQVc] jH!!%lAexQ~IR |_]0)!j,anw(C<$!l4D6ֲy\]JY:s%COݙ,>^ťki_(_S+V$>/ލo I_ NcuWs*g/“j ~2ט͛1cUT$*4V{Vh-x#-doaXA@u$#ɃW:DG!^@16ti,l#ܬ5vކzPVƌWՄ4FJ1439x2"?yC"g>ɋ%q>>:@`6r:x9 /?x>B:ehv]{ģr7{Ru c m\q{Hcl83_^i90nG~Sl}EXbDP2(5^(eՌzfkM\] o"ߣ]U %wm{3'2E!Q=D1M!懿 G}D:.VۂS/azk i&2e7~5<9HW®"h3X]^2>?ʥSX^(1uWOQ'!XC\{l3#gQ>tK{Eo(!UtJ Gk\@K{{>kA^!bYc$z NAHwd<cn`}Lo ]Kډ/GTd=I5dЛxѣxSh^&90TFd&U|3&O  xrzm^ 5)wKC@7}t#/ٹ lߵ?oh'$C$z i(H33LAJsGiNjHx W0|ޅxlF84@4/pM]oe?ĉWdFn2|l3Rffo\Vd``R1&ο$)38C M"¾uNRSj\W5ZzB`1NLR߅ax&̩{ɇ'#"B:h)l۵ 7F$EsO )b= Ow 5~Wi,[1lN6o-_}Ww;&D4]p09mޜ[z Sz/ݵt{/U6p$dnt]en"ke!ͳ/qơ 0R~ `F @ZBz>fv_//B=x$ Q>4)?@'xo/G]t`>܋y"\y~ϿQ/V8{tr'y k+[p°pUKU+.» |8 e ij8=ulAwT.XWrZQ<2{M>ؽw?؊:ӏ1Z Ac{on/agsx ?]5Ɲn `/m?q)-jEɏ}wm=S=aem#=l,td7_މ !>0\јlhЬY4ոS@4v^`߭m$:ŕS ~~]Sxg1?{?^{!n 1 c`J &(pkxx{v;~'"{.^К0yn0w8 |a<@! G|Gqۏ_ 3lݎ:a%SvE oL^SJ苧_@;Xى* 33/n`~wuVxCP2AFJ nfdgsá5/0p0{1ݝ<1b=6kgsY 'xO1(5n˰m46.ٍ?/wrLc|*Du 7S}psX9Gv([oU|V; liaش. C\sⲅ [wʛ5F !Cz մS?/zoDov)kfJ>E\z,v6>pZkC?+l`v|LG VCOMjQ_8vG} _{)Ύ!fvuÄvn?8tq}f@2IVuVS|I`i3TZ#Qǡ\||/2y/FIiyxFԶ!)}?ws$ݎC SxmEPC}(ǀT@AkIvcQGF!. y\nb?rIBB#=;>N& Rz*]r?X__37F: $s9,-yvt)_ ]m1IS?=O |*95J?}Y*j-~YATf?Y;SBH27~}0@93WOaz{`6G;,n%=`~泐zNSrzDwGkJAy4fDUJ_?JF #^=4^~Tt[yavwhh/檵'>9| `]llEneYD1^Ooq epof` ȫ pݟ{BqNk4v^AoisG???pIjZO8Sʓ^U.\7Wѝ2]!q">tTF9U.3hF0, xSiJ9 CuR=տ+/h4: BTr2q_x`g+O!89FթF RTS Yk"?qS|q4'&W=Dj-gA*D|^Q9$N2z݌6xGqg ޳)JDC|?=_z}T4/:("MSk_jTtI-ex/gXq{l5֌(u`H!Piߋg8wj9U1r IDATg_9$<1 /•&ƪUg%yfIMa:؃[歸]زcsۧa>N.Czemmtlvjތ^g>u *Ο.z N/#{Vlx 'W“GR)< y=;҅/|HR}G;pzwvl=݊A'cѯErx l!ABoq/ě޹156.B%pC]0Kz-CJڝ[gk-lkWn|>l33q(Of9kxx뇰 띵fxgŬ;nSn qŁuVm @ysǗܡxR,`tH3̱/F؈W'_ "9Nžt( W_c0Y YG#:uɀ3*CK;0XL" elLVqS*:TC "~Ux?ǵہKsNn3GpyGO3+@_fL;;܊?P `4<qnp 1h{\ ]J16eI,΀:3!(ld42tVt9 'XÙcX?a:p%1߾BOFHD=LE[1^ny"fA9)$C^B5#x8#)'dQvڋp':lJDS|3`. b>왁R J)qZ0* )̅1lŔ S<"OH V'簸q0>:sႄ'}/1Hlqτ1oC۟$hsXxGǑ`0lÖ^Lpꅙ 5,OcaxX-`!ZA3Nl`*܎7c.X fiهG4: -b~ ̷b:؊X4oh90 1k#d.g0lL# X!R=A9,)FR sGW3Bg1lt<;Ճ'V G&9G 8O3Guqrņ?3ZUb|S*D7Fh`F]`àH=3,z#1w0D@W-z Cf"A%A~R)O $Ƕityt9i2ddUSnK(N3eM>P$>"(X&lJOh)3H-ɇa"̭K.et-Ȅ_ק6({yY0eRMjB.EfI[J˥;nT{{gU #]֝fx9[e=6W2Mnn fyKͨ8^Wަ|hfinfCX8<.2F>YnnX;f2ja]~h0xʃ$Cot'¶sptvbn98|^?W:{H_=nĨ4BK#eB'gTFާ,Jꇝ-9{J$.lMlhא䪭Vÿsc[]sc bX՞7A%O_[bD%vJR+"pummP EĸË\VѠjT *7#BtTblU!?¦ڂ.Tz}ņo*5;m|xt얜mO!VNIc\`Tl=S3V,d-7+SZ=nP"A,jHS'O`N`y Yt]oE&TⵃU*n̩ZB1;\l?+vjH*޺ Ɋ% kiXA^ɲZ/*r*#^c+aI{M(cJ,PBd_y͖j,@dˮ$ Q׸O2Sr%ŭ%غ(AM[ߣ NPn[rmfz)Hv|b*pl*%[q)ouP1lqy=@ "f *pU0=sŕ  VMkVK6ANuUq.W- )>FB5$$g˨c$c.9 Ҷwr L_&q/u෬VN[?!Npr(MP4 8HʝQ c++# ;6!}3LNֿC'Λ}\5dq.cxj!tt-r0m+j' nhԿj7Fk k,&WD6:NM%ncn nQmaUf T~3V]i 0i tל*ޤt 6qθv{/Ң@J6[62u|l*6u4-%U['݉{U[VJoCgD_M>F v4d3$Kpg F]x4vYMr3>x "M'ZGpYGzu c=( 4gƴnץdamAl 6YvUZhAJ]o8AXm;ІuG51a5/%gɁ* c8Yw$A,`IQGS+FN(J *Բb< 97$Q6ݡq:5eWfT66b]{S vlp c \=0J1NDžV=+.<0LQn"EPfULg[l7 ^X]@YUƜ@/b}alLtde yFōLk&\d' Z,evGRe':luHPtd sE5+JNB}+@)0ՉID,XyYj3;n'ʔOK-CQ[%_FtڸYe zw>ƅ[ؠS5ڈ3%Ud7rgj0!1}T[fpg(AY$%OV/Ħ j̵˥>B}DŒU>0]R1Ѻ>)A<4C(ߖ8@֒,)HV7@9w.ǎ C/iݹfk ˺(j Vp[jxNjȠ J<:S? sjVT2 T^%`mKQcVUVDhGYq@.*^S(h 654Rlr#;^!m|^QDfw1_(uK]CO@QX%z^{l,{(citҧLOl~ wB^wWXB?]'8sl41A[FAK 4 L/ln;Ph_u ˗kpZK)1 Ñ)`h Q}Jᱸ~>SSrY;^6~\rIk_i"n,_VFgv[+9fװ Tfx3αr.MM IUQ{u v9nNj H&13#jm'0W7GP+gSa&ؠTvPQ6-XN*0Dn*zx Xiقbn M0^ IDATXbm H9H.J̶E#L_TTMޓɆF<\N}ݩ£ڄAnVz\,gV7QS.+{i SZ}SqezdLJݦͳN8t%zǎQ&E:wL˖*?lCҶ@}#妹%i.GղLqTF 28#J)si+7vn}ak;l1Jh1lAp& u{=x(nWc=Շ gJFgb>4ĸ?WF[@zeG,<1ĸ7;NIt]81 b Op`OH7aH8K|\r,b0L0o{O!c fzQdC+d|~]ۯ/]Hy,.Ji D/Q'研2<+?a;|v x! Ͽ}_?YA*3?hbAxո+vCʜP\'xOq Wɀ^O!dG/1=\)}XPcwXl2Fj 1Sy:×NɘB:1YN?wb"zFM ~"^ :1ϩa? v\S<),%؃/ 0&:e3!a0$HxbjlѴҘL4IdAlp.i a\s཮+=$[1Zx3+>bcܟএmc"cv5C|؊[l בK&8p0.NQ*3nC%<_y rE 6FøPK;`9!XD~7eZ^n8\#6 =m㺆3Vcr[>;,*'.e! {oB^VCw=Rq۰_2Y"iv5n]>G I!:eCX4AkU3Ở$[޽y b!MRL_UqG^=='}/oć~6xWp\ GYT& SKOǠ7ďm8HS]4!.v 5lFx+!t8sX82mREWCϮ`m%R`2G3JD9,NM`.tZ^6QC)<#m7~)÷>SN/'tsGj*J@p9r8Qv%R rb\9].+Tfaܼɨt&c?ޕ.բT7W T͒+nq}@&({!Clq>?YJ(e9ʟK#0nm `|8Hbs:8>s7s1LEh1YSADxCKcÔ?>0 0Yڒ1#{G2(Oѻ_8n,P_|7F1~W?X dQDkC'g?_p;%#<JwdOK$ݛEc8".iR3޿ {O|т/B2 )@5XC۟Fp{T0G➿{5h" 1|E|6ܻ90d8#7n'\#]\DNw WȚ>%([UmT8z35ׅaNwqe.,*Dc_7*K*DsF*:p<򲯊BR>1^%3g;^remhLfBCqIlgn$x-fc u!R@k}ACp{h%G4')n48W6 PJb=:EӾ{ip{_OI _JP! *&կJaXҽNY}`@s f -JUD8aR=Aw[]L%8x˸ˏ`uxG(F-1[<\L0W0I:)H18PuΏNv;YcG {גpG֜.RüCeG UI?* 3kt[^($k3JSŔ•(9[`Zt-4ʛM*_f/(;hB <_DCǛE(+@isز*Y}DdO~1y0:620 uW6q ;ub=9:4 0Ž8fZGXtaC9rZT˶MNA(|_l3,?]xHȢ!֏"EOUox1YЬa"Ch0N0ٜEUc/DkJYxL$rC 5xϮ̊=V,tX?b߫aOYRGpxfL=pTo,~> +fvW CPI˞1$Iv%!B&q=Uot#(˪(1t n ;p8ZCƈ#W&)=l1ǐU*&AtlO`1/ڥ+ zqtkqJc!R1*GbH uUX#i{b$ȢF!'DZPA\yQT %wkE#(Ι YVY၇Ƒ2Lnncf8f}}""?d$zABUw0J3ǡ ;֡H(U%W؉#;ۘ9]#bָ;u.cU䘿Xj8;,$K7?cEN`O3,gp<{ kfkR!D[D!j/E$1F9G '?`0\('<h-5a<%L .SXM Sc?(N13@2h@+ >ܸUa vTן.u}=JḚ7_EYPxI#QR3BP?,r5^@Բ+Fx(c;p2p {@%5ʡH\Yvh 8"| {%aò BͦuZ Z]H` ܀6k^fC*CU2N3bζnkujEGPaT8q,n@ >L_<>̌Qrp0Ai"hzۧ`AF*Qq3Ңc>LXW D*B%&ba(&ݴ j9TXBI)h /|&GOhC'8Cxא &4ǡU51=7~7qU&C(mNmK@^M F ^9P`vsb,ybK¦@x޹fSIHp_#%R,-n8JxLRh#5_eN_ףבIt]])[Ń K_dbʂd_Tܴ"V) E Rd+?w%ۢO|{.fDjٙPK|" rQa>jUSui ]-Xp_+`|`9m%0s4r]RS5.Peߘ.Aݕ1N޻O&҈TT eؽPArIc9?AL XJlpl2" ,@D `TզSw%Ȅ/RLu _CFegsqcG/wc!F*vRf8gGS͎=1@XHt3e(9pz;M-H}{{SU$CiNz1w=ʑuu}1?jKw`ܘA$ \;3ǍeOan N=P]d]ƅ)5?8ft^FSdf.])"#o`*v렻SKl8r-8&C^ v ȡKgL"c8)wޭuT퉷'4>=J8>'>x[XEQy'jV[(g(9?pwDVcӮ)̸DCEX ߢqm=$"vnkK" *_ IꓻOJnd2cʢDž %\e\{a.p>#G70XONHv'fwXzNm܇{1Z+RxjX71(A^!B } * 9Nj  ɛڶKVt`rm`X{ v['TroL<; a5L'3<hH<'˪yH4QlmW uIy'ƹ'pTWxq CHU3јp]PQ IDAT<'񾟺 G?Gk@ct`t& /)x"N`ȠV+|2#p|vϊ=eAӬ!d*!|Q^Y mt"d} ~|ׯl,K= fFYV/>q5bh2u-{)N~Wtov:U}j6Q݌7T7WB#5Q "#5<.Kٚ,JL9%~_¿_uդq&P 4Ew"/%>H n(&wpZ`^>Lc7)[5XgqݐDE+{ B}?]tj} \-![C0^b r gϾkn"123T"C7$fLXDxw =*v{P8+9ǬœpHcV`R!S(@!Hgekxo|~ظc 0iȆ .ǿ,_gPE331OUP\`oaRlV\cSgn`w:&m\cLs<߯c+6=ݛS(\1(=Adx37pMnvr4=sGH2=@k3޶LdFIzOxO>~eIG"ag/ч/9=lttT1f{K1/!93K|@r}ZR:p/)*0čQ\u%s(ef'z6.R{-diWٽW)j{ܗ4L8=l-6y [ehbДYE("+8iPo"n/b^L0HGށ+K92Q3MI"D&K:{+(QCE=6fA2 Y;{#>?} 8XCST綡L%t (a'ǰo`Wjeu",5H7La8xy)HQj&v_4er@2 Hdi LMf0faUHWrKͱZR&")ƴK0HxDeMQuPB=7FX\cVB+a'i q,0/w 35BV!08ch ]lbryRJ04+u 9qt5bMqRheqB,@JAqs$:EG0*! ,@ Aʪ@ٿH0wܔ!24S0 Tq|+f=7JBE23s m F1vgMQDN2ԍ JYj|+CHL=@ V} 4f˥іwfSh=l9=FB7J@!EHp-gYBǖ MS$@VˣwʥJsI$ePZ^l+Y10!N1V $ALJ0VDZ@VTMfeR@+E9@Hh 6X"A`^͠Ia`DC5kr0?oQV!5C fN3hcdfhh "TT0 (,9 f=ӾT4t QrbH 4E0Tb`F(#>WR:08l[CQ1i JC0hHPJ{Jp`uYiַ `7] 4o:C $i8;uѱtF tώ~v izw9"DD Wt2L[I%@:TF Lh5b"(J={E:;y]-wu}i%vZJ[P F֖bgPS1|%&Qn=GFfwҞd`Ҩklr;?M .쵑5<~&M9؁B;[ΒZ&I24"$iwLD@2aѱY>NSEEBLWyE1oԍ~^l^WnT`Vv/.2^di+H8Lk0'qPvȿ.FaX$P8 }]8mOvuj0rwj2?;?3!2̚ˀ,YِE+h 97 !% `Rʙ#AkBNafp]܏ ve;:I*LX+tqvkW`{M,)fE590]n>5q.&؄"qX* <&@7kóLqc&lw N :628A˼\CjuI9jB~;S&Z؀Ddgu)IfN , g  `K~[>t0ْA=ۚ*K'4_,.IK2ff'W.r14r /@; tvz3Ȧ͂Q9s7n|ˍQ{#+'_L{sW_y\Ԗ:Nn`  (vH2丒  tB7( v7̧'y[|3Q67bPG?r& ])Z39i+S 1%$Yw\܏R9 ;wn㙉 BDfئʐ#Eۙ Y-0Jdw,AnI{]F$l$lsd7ܓwJcp@5ߗ.gÂKf+qEOgq]xlU\-GqFffV`a D];HNV黃]VVA yۥCV#lg?aOo %2k1 bQ= rC\tB!1,F+]#O] I< q q˧㸝<97$$3v=G7rѣ8U#I٦AXH(hNU\ŵjRlޤ~  BYO]sͫywٚBL#T956E^kէȝt dIpN sK:]-r,f';F$([w #.\=}*f͖dr'pɩkU: ݔ\nANĶ7RzI㣞8392:^U'Fކ8KE@!65 ( ۖ,mEqfFFQX`[9<җܜ_Ƣ>_͒&Pu+\?p#Rʨ>=^iFĥcs!_d: 1-KEJʞUZ _*5;1xlYItTir[/ ǂa#opK7!l<XӲ {pR`xܺ[S(g o=MȃzX;7?Rgq[P8`Հ"!j=kgN]s&*h;b\uf ,% 4pH@*ARMZf [K HǼ'nT(bzSzn*RjdI0Ndp'U'wYW+1^ VwZHd}+$u Ź.}`R5_c9*G|QTˋr꼜\P X&*P{ɨ M(уAQA `0EpOU(̐P-.VEՄQ(3TP7+W%/7j l2(׼VB"q@|M@p(_ZLs}F\k{{;fSZ4w.o~2VYT7[IDAT_nф‰lXk(_l`|m6PϛknUm&-;w>妭 mE󙋯XlͶtMgH\VϼwXlBl=1H"h"@ VI&OA ͍\b%K0 IENDB`././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/contrib/plugins/murano_heat-translator_plugin/sample/wordpress/manifest.yaml0000664000175000017500000000050100000000000032311 0ustar00zuulzuul00000000000000Author: OASIS TOSCA TC Description: 'TOSCA simple profile with wordpress, web server and mysql on the same server. ' Format: TOSCA.CSAR/1.1 FullName: io.murano.apps.generated.CsarWordpress Name: csar_wordpress Tags: - TOSCA-CSAR-generated Template: Definitions/tosca_single_instance_wordpress.yaml Type: Application ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/contrib/plugins/murano_heat-translator_plugin/setup.cfg0000664000175000017500000000120000000000000026124 0ustar00zuulzuul00000000000000[metadata] name = io.murano.plugins.oasis.tosca description = Heat-Translator Plugin for Murano summary = This plugin enables import and deployment of OASIS TOSCA application specifications in Murano. The plugin makes use of tosca-parser pypi library for package imports and heat-translator pypi library for package deployment. Deployment support is not yet added and will come soon. author = Vahid Hashemian author-email = vahidhashemian@us.ibm.com [files] packages = murano_heat-translator_plugin [entry_points] io.murano.plugins.packages = TOSCA.CSAR/1.1.0 = plugin.csar_package:CSARPackage ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/contrib/plugins/murano_heat-translator_plugin/setup.py0000664000175000017500000000145500000000000026031 0ustar00zuulzuul00000000000000# Copyright 2011-2012 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. import setuptools # all other params will be taken from setup.cfg setuptools.setup(packages=setuptools.find_packages(), setup_requires=['pbr'], pbr=True) ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1696417899.7051804 murano-16.0.0/devstack/0000775000175000017500000000000000000000000014726 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/devstack/README.rst0000664000175000017500000000264600000000000016425 0ustar00zuulzuul00000000000000==================== Enabling in Devstack ==================== #. Download DevStack_:: git clone https://opendev.org/openstack/devstack cd devstack #. Edit ``local.conf`` to enable murano and heat devstack plugin:: > cat local.conf [[local|localrc]] enable_plugin murano https://opendev.org/openstack/murano #Enable heat plugin enable_plugin heat https://opendev.org/openstack/heat #. If you want Murano Cloud Foundry Broker API service enabled, add the following line to ``local.conf``:: enable_service murano-cfapi #. If you want to use Glare Artifact Repository as a storage for packages, add the following line to ``local.conf``: .. code-block:: ini enable_service g-glare For more information on how to use Glare Artifact Repository, see :ref:`glare_usage`. #. (Optional) To import Murano packages when DevStack is up, define an ordered list of packages FQDNs in ``local.conf``. Make sure to list all package dependencies. These packages will by default be imported from the murano-apps git repository. Example:: MURANO_APPS=com.example.apache.Tomcat,org.openstack.Rally You can also use the variables ``MURANO_APPS_REPO`` and ``MURANO_APPS_BRANCH`` to configure the git repository which will be used as the source for the imported packages. #. Install DevStack:: ./stack.sh .. _DevStack: https://docs.openstack.org/devstack/latest/ ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1696417899.7051804 murano-16.0.0/devstack/files/0000775000175000017500000000000000000000000016030 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/devstack/files/apache-murano-api.template0000664000175000017500000000140400000000000023053 0ustar00zuulzuul00000000000000Listen %PUBLICPORT% WSGIDaemonProcess murano-api processes=1 threads=10 user=%USER% display-name=%{GROUP} %VIRTUALENV% WSGIProcessGroup murano-api WSGIScriptAlias / %MURANO_BIN_DIR%/murano-wsgi-api WSGIApplicationGroup %{GLOBAL} WSGIPassAuthorization On AllowEncodedSlashes On = 2.4> ErrorLogFormat "%{cu}t %M" ErrorLog /var/log/%APACHE_NAME%/murano_api.log CustomLog /var/log/%APACHE_NAME%/murano_api_access.log combined = 2.4> Require all granted Order allow,deny Allow from all ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1696417899.7051804 murano-16.0.0/devstack/files/debs/0000775000175000017500000000000000000000000016745 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/devstack/files/debs/murano0000664000175000017500000000000400000000000020163 0ustar00zuulzuul00000000000000zip ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1696417899.7051804 murano-16.0.0/devstack/files/rpms/0000775000175000017500000000000000000000000017011 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/devstack/files/rpms/murano0000664000175000017500000000000400000000000020227 0ustar00zuulzuul00000000000000zip ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/devstack/plugin.sh0000775000175000017500000005656500000000000016604 0ustar00zuulzuul00000000000000#!/usr/bin/env bash # Plugin file for Murano services # ------------------------------- # Dependencies: # ``functions`` file # ``DEST``, ``DATA_DIR``, ``STACK_USER`` must be defined # Save trace setting XTRACE=$(set +o | grep xtrace) set -o xtrace # Support entry points installation of console scripts if [[ -d $MURANO_DIR/bin ]]; then MURANO_BIN_DIR=$MURANO_DIR/bin else MURANO_BIN_DIR=$(get_python_exec_prefix) fi MURANO_AUTH_CACHE_DIR=${MURANO_AUTH_CACHE_DIR:-/var/cache/murano} # Toggle for deploying Murano-API under under a wsgi server MURANO_USE_UWSGI=${MURANO_USE_UWSGI:-True} MURANO_UWSGI=$MURANO_BIN_DIR/murano-wsgi-api MURANO_UWSGI_CONF=$MURANO_CONF_DIR/murano-api-uwsgi.ini if [[ "$MURANO_USE_UWSGI" == "True" ]]; then MURANO_API_URL="$MURANO_SERVICE_PROTOCOL://$MURANO_SERVICE_HOST/application-catalog" else MURANO_API_URL="$MURANO_SERVICE_PROTOCOL://$MURANO_SERVICE_HOST:$MURANO_SERVICE_PORT" fi HORIZON_URL="http://${KEYSTONE_SERVICE_HOST}/dashboard" DASHBOARD_FUNCTIONAL_CONFIG_DIR=$MURANO_DASHBOARD_DIR/muranodashboard/tests/functional/config # create_murano_accounts() - Set up common required murano accounts # # Tenant User Roles # ------------------------------ # service murano admin function create_murano_accounts() { if ! is_service_enabled key; then return fi create_service_user "murano" "admin" get_or_create_service "murano" "application-catalog" "Application Catalog Service" get_or_create_endpoint "application-catalog" \ "$REGION_NAME" \ "$MURANO_API_URL" \ "$MURANO_API_URL" \ "$MURANO_API_URL" if is_service_enabled murano-cfapi; then get_or_create_service "murano-cfapi" "service-broker" "Murano CloudFoundry Service Broker" get_or_create_endpoint "service-broker" \ "$REGION_NAME" \ "$MURANO_SERVICE_PROTOCOL://$MURANO_SERVICE_HOST:$MURANO_CFAPI_SERVICE_PORT" \ "$MURANO_SERVICE_PROTOCOL://$MURANO_SERVICE_HOST:$MURANO_CFAPI_SERVICE_PORT" \ "$MURANO_SERVICE_PROTOCOL://$MURANO_SERVICE_HOST:$MURANO_CFAPI_SERVICE_PORT" fi } function mkdir_chown_stack { if [[ ! -d "$1" ]]; then sudo mkdir -p "$1" fi sudo chown $STACK_USER "$1" } function configure_murano_rpc_backend() { # Configure the rpc service. iniset_rpc_backend muranoapi $MURANO_CONF_FILE DEFAULT # TODO(ruhe): get rid of this ugly workaround. inicomment $MURANO_CONF_FILE DEFAULT rpc_backend if [[ $SERVICE_IP_VERSION == 6 ]]; then iniset $MURANO_CONF_FILE rabbitmq host "$HOST_IPV6" else iniset $MURANO_CONF_FILE rabbitmq host "$HOST_IP" fi iniset $MURANO_CONF_FILE rabbitmq login $RABBIT_USERID iniset $MURANO_CONF_FILE rabbitmq password $RABBIT_PASSWORD # Set non-default rabbit virtual host if required. if [[ -n "$MURANO_RABBIT_VHOST" ]]; then iniset $MURANO_CONF_FILE DEFAULT rabbit_virtual_host $MURANO_RABBIT_VHOST iniset $MURANO_CONF_FILE rabbitmq virtual_host $MURANO_RABBIT_VHOST fi } function configure_murano_glare_backend() { # Configure Murano to use GlARe application storage backend iniset $MURANO_CONF_FILE engine packages_service 'glare' if is_service_enabled murano-cfapi; then iniset $MURANO_CFAPI_CONF_FILE cfapi packages_service 'glare' fi iniset $MURANO_CONF_FILE glare url $GLANCE_SERVICE_PROTOCOL://$GLANCE_GLARE_HOSTPORT iniset $MURANO_CONF_FILE glare endpoint_type $GLARE_ENDPOINT_TYPE echo -e $"\nexport MURANO_PACKAGES_SERVICE='glare'" | sudo tee -a $TOP_DIR/openrc echo -e $"\nexport GLARE_URL='$GLANCE_SERVICE_PROTOCOL://$GLANCE_GLARE_HOSTPORT'" | sudo tee -a $TOP_DIR/openrc } function restart_glare_service() { # Restart GlARe service to apply Murano artifact plugin if is_running glance-glare; then echo_summary "Restarting GlARe to apply config changes" stop_process g-glare run_process g-glare "$GLANCE_BIN_DIR/glance-glare --config-file=$GLANCE_CONF_DIR/glance-glare.conf" echo "Waiting for GlARe [g-glare] ($GLANCE_GLARE_HOSTPORT) to start..." if ! wait_for_service $SERVICE_TIMEOUT $GLANCE_SERVICE_PROTOCOL://$GLANCE_GLARE_HOSTPORT; then die $LINENO " GlARe [g-glare] did not start" fi else echo_summary "GlARe service wasn't started yet. It will start in usual way." fi } function install_murano_artifact_plugin() { # Provide support of Murano artifacts type to GlARe setup_package $MURANO_DIR/contrib/glance -e } function is_murano_backend_glare() { is_service_enabled g-glare && [[ "$MURANO_USE_GLARE" == "True" ]] && return 0 return 1 } function configure_murano_networking { # Use keyword 'public' if Murano external network was not set. # If it was set but the network is not exist then # first available external network will be selected. local ext_net=${MURANO_EXTERNAL_NETWORK:-'public'} local ext_net_id=$(openstack --os-cloud=devstack-admin \ --os-region-name="$REGION_NAME" network list \ --external | grep " $ext_net " | get_field 1) # Try to select first available external network if ext_net_id is null if [[ ! -n "$ext_net_id" ]]; then ext_net_id=$(openstack --os-cloud=devstack-admin \ --os-region-name="$REGION_NAME" network list \ --external -f csv -c ID | tail -n +2 | tail -n 1) fi # Configure networking options for Murano if [[ -n "$ext_net" ]] && [[ -n "$ext_net_id" ]]; then iniset $MURANO_CONF_FILE networking external_network $ext_net_id iniset $MURANO_CONF_FILE networking create_router 'true' else iniset $MURANO_CONF_FILE networking create_router 'false' fi if [[ -n "$MURANO_DEFAULT_ROUTER" ]]; then iniset $MURANO_CONF_FILE networking router_name $MURANO_DEFAULT_ROUTER fi if [[ -n "$MURANO_DEFAULT_DNS" ]]; then iniset $MURANO_CONF_FILE networking default_dns $MURANO_DEFAULT_DNS fi } # Entry points # ------------ # configure_murano() - Set config files, create data dirs, etc function configure_murano { mkdir_chown_stack "$MURANO_CONF_DIR" # Generate Murano configuration file and configure common parameters. oslo-config-generator --config-file $MURANO_DIR/etc/oslo-config-generator/murano.conf --output-file $MURANO_CONF_FILE cp $MURANO_DIR/etc/murano/murano-paste.ini $MURANO_CONF_DIR cleanup_murano iniset $MURANO_CONF_FILE DEFAULT debug $MURANO_DEBUG iniset $MURANO_CONF_FILE DEFAULT use_syslog $SYSLOG # Format logging if [ "$LOG_COLOR" == "True" ] && [ "$SYSLOG" == "False" ] && [ "$MURANO_USE_UWSGI" == "False" ] ; then setup_colorized_logging $MURANO_CONF_FILE DEFAULT else # Show user_name and project_name instead of user_id and project_id iniset $MURANO_CONF_FILE DEFAULT logging_context_format_string "%(asctime)s.%(msecs)03d %(levelname)s %(name)s [%(request_id)s %(user_name)s %(project_name)s] %(instance)s%(message)s" fi iniset $MURANO_CONF_FILE DEFAULT home_region $REGION_NAME # Murano Policy Enforcement Configuration if [[ "$MURANO_ENABLE_MODEL_POLICY_ENFORCEMENT" == "True" ]]; then iniset $MURANO_CONF_FILE engine enable_model_policy_enforcer $MURANO_ENABLE_MODEL_POLICY_ENFORCEMENT fi # Murano Api Configuration #------------------------- # Setup keystone_authtoken section configure_auth_token_middleware $MURANO_CONF_FILE $MURANO_ADMIN_USER $MURANO_AUTH_CACHE_DIR # Setup murano_auth section configure_auth_token_middleware $MURANO_CONF_FILE $MURANO_ADMIN_USER $MURANO_AUTH_CACHE_DIR murano_auth iniset $MURANO_CONF_FILE murano_auth www_authenticate_uri $KEYSTONE_AUTH_URI configure_murano_rpc_backend # Configure notifications for status information during provisioning iniset $MURANO_CONF_FILE oslo_messaging_notifications driver messagingv2 # configure the database. iniset $MURANO_CONF_FILE database connection `database_connection_url murano` # Configure keystone auth url iniset $MURANO_CONF_FILE keystone auth_url $KEYSTONE_SERVICE_URI # Configure Murano API URL iniset $MURANO_CONF_FILE murano url "$MURANO_API_URL" # Configure the number of api workers if [[ -n "$MURANO_API_WORKERS" ]]; then iniset $MURANO_CONF_FILE murano api_workers $MURANO_API_WORKERS fi # Configure the number of engine workers if [[ -n "$MURANO_ENGINE_WORKERS" ]]; then iniset $MURANO_CONF_FILE engine engine_workers $MURANO_ENGINE_WORKERS fi if is_murano_backend_glare; then configure_murano_glare_backend fi if [ "$MURANO_USE_UWSGI" == "True" ]; then write_uwsgi_config "$MURANO_UWSGI_CONF" "$MURANO_UWSGI" "/application-catalog" fi } # set the murano packages service backend function set_packages_service_backend() { if is_murano_backend_glare; then MURANO_PACKAGES_SERVICE='glare' else MURANO_PACKAGES_SERVICE='murano' fi } # configure_murano_cfapi() - Set config files function configure_murano_cfapi { # Generate Murano configuration file and configure common parameters. oslo-config-generator --config-file $MURANO_DIR/etc/oslo-config-generator/murano-cfapi.conf --output-file $MURANO_CFAPI_CONF_FILE cp $MURANO_DIR/etc/murano/murano-cfapi-paste.ini $MURANO_CONF_DIR configure_service_broker } # install_murano_apps() - Install Murano apps from repository murano-apps, if required function install_murano_apps() { if [[ -z $MURANO_APPS ]]; then return fi # clone murano-apps only if app installation is required git_clone $MURANO_APPS_REPO $MURANO_APPS_DIR $MURANO_APPS_BRANCH set_packages_service_backend # install Murano apps defined in the comma-separated list $MURANO_APPS for murano_app in ${MURANO_APPS//,/ }; do find $MURANO_APPS_DIR -type d -name "package" | while read package; do full_name=$(grep "FullName" "$package/manifest.yaml" | awk -F ':' '{print $2}' | tr -d ' ') if [[ $full_name = $murano_app ]]; then pushd $package zip -r app.zip . murano --os-username $OS_USERNAME \ --os-password $OS_PASSWORD \ --os-tenant-name $OS_PROJECT_NAME \ --os-auth-url $KEYSTONE_SERVICE_URI \ --murano-url $MURANO_API_URL \ --glare-url $GLANCE_SERVICE_PROTOCOL://$GLANCE_GLARE_HOSTPORT \ --murano-packages-service $MURANO_PACKAGES_SERVICE \ package-import \ --is-public \ --exists-action u \ app.zip popd fi done done } # configure_service_broker() - set service broker specific options to config function configure_service_broker { iniset $MURANO_CFAPI_CONF_FILE DEFAULT debug $MURANO_DEBUG iniset $MURANO_CFAPI_CONF_FILE DEFAULT use_syslog $SYSLOG #Add needed options to murano-cfapi.conf iniset $MURANO_CFAPI_CONF_FILE cfapi tenant "$MURANO_CFAPI_DEFAULT_TENANT" iniset $MURANO_CFAPI_CONF_FILE cfapi bind_host "$MURANO_SERVICE_HOST" iniset $MURANO_CFAPI_CONF_FILE cfapi bind_port "$MURANO_CFAPI_SERVICE_PORT" iniset $MURANO_CFAPI_CONF_FILE cfapi auth_url "$KEYSTONE_SERVICE_URI" # configure the database. iniset $MURANO_CFAPI_CONF_FILE database connection `database_connection_url murano_cfapi` # Setup keystone_authtoken section configure_auth_token_middleware $MURANO_CFAPI_CONF_FILE $MURANO_ADMIN_USER $MURANO_AUTH_CACHE_DIR } function prepare_core_apps() { cd $MURANO_DIR/meta for i in */ do pushd ./"$i" zip -r ../"${i%/}.zip" * popd done } function remove_core_apps_zip() { rm -f $MURANO_DIR/meta/*.zip } # init_murano() - Initialize databases, etc. function init_murano() { configure_murano_networking # (re)create Murano database recreate_database murano utf8 $MURANO_BIN_DIR/murano-db-manage --config-file $MURANO_CONF_FILE upgrade create_murano_cache_dir } # create_murano_cache_dir() - Part of the init_murano() process function create_murano_cache_dir { # Create cache dirs sudo install -d -o $STACK_USER $MURANO_AUTH_CACHE_DIR } # init_murano_cfapi() - Initialize databases, etc. function init_murano_cfapi() { # (re)create Murano database recreate_database murano_cfapi utf8 $MURANO_BIN_DIR/murano-cfapi-db-manage --config-file $MURANO_CFAPI_CONF_FILE upgrade } function setup_core_library() { prepare_core_apps set_packages_service_backend murano --os-username admin \ --os-password $ADMIN_PASSWORD \ --os-tenant-name admin \ --os-auth-url $KEYSTONE_SERVICE_URI \ --os-region-name $REGION_NAME \ --murano-url $MURANO_API_URL \ --glare-url $GLANCE_SERVICE_PROTOCOL://$GLANCE_GLARE_HOSTPORT \ --murano-packages-service $MURANO_PACKAGES_SERVICE \ package-import $MURANO_DIR/meta/*.zip \ --is-public remove_core_apps_zip } # install_murano() - Collect source and prepare function install_murano() { install_murano_pythonclient git_clone $MURANO_REPO $MURANO_DIR $MURANO_BRANCH setup_develop $MURANO_DIR if is_murano_backend_glare; then install_murano_artifact_plugin fi } function install_murano_pythonclient() { # For using non-released client from git branch, need to add # LIBS_FROM_GIT=python-muranoclient parameter to localrc. # Otherwise, murano will install python-muranoclient from requirements. if use_library_from_git "python-muranoclient"; then git_clone_by_name "python-muranoclient" setup_dev_lib "python-muranoclient" # Installing bash_completion for murano sudo install -D -m 0644 -o $STACK_USER {${GITDIR["python-muranoclient"]}/tools/,/etc/bash_completion.d/}murano.bash_completion fi } # start_murano() - Start running processes, including screen function start_murano() { if [ "$MURANO_USE_UWSGI" == "True" ]; then run_process murano-api "$(which uwsgi) --procname-prefix murano-api --ini $MURANO_UWSGI_CONF" else run_process murano-api "$MURANO_BIN_DIR/murano-api --config-file $MURANO_CONF_DIR/murano.conf" fi run_process murano-engine "$MURANO_BIN_DIR/murano-engine --config-file $MURANO_CONF_DIR/murano.conf" } # stop_murano() - Stop running processes function stop_murano() { # Kill the Murano screen windows if [ "$MURANO_USE_UWSGI" == "True" ]; then disable_apache_site murano-api restart_apache_server fi stop_process murano-api stop_process murano-engine } # start_service_broker() - start murano CF service broker function start_service_broker() { run_process murano-cfapi "$MURANO_BIN_DIR/murano-cfapi --config-file $MURANO_CONF_DIR/murano-cfapi.conf" } # stop_service_broker() - stop murano CF service broker function stop_service_broker() { # Kill the Murano screen windows stop_process murano-cfapi } function cleanup_murano() { # Cleanup keystone signing dir sudo rm -rf $MURANO_KEYSTONE_SIGNING_DIR if [[ "$MURANO_USE_UWSGI" == "True" ]]; then remove_uwsgi_config "$MURANO_UWSGI_CONF" "$MURANO_UWSGI" fi } function configure_murano_tempest_plugin() { # Check tempest for enabling if is_service_enabled tempest; then echo_summary "Configuring Murano Tempest plugin" # Set murano service availability flag iniset $TEMPEST_CONFIG service_available murano "True" if is_service_enabled murano-cfapi; then # Enable Service Broker tests if cfapi enabled and set murano-cfapi service availability flag iniset $TEMPEST_CONFIG service_available murano_cfapi "True" iniset $TEMPEST_CONFIG service_broker run_service_broker_tests "True" fi if is_service_enabled g-glare; then # TODO(freerunner): This is bad way to configure tempest to # TODO see glare as enabled. We need to move it out to tempest # TODO of glance repo when glare become official OS API. iniset $TEMPEST_CONFIG service_available glare "True" fi if is_murano_backend_glare; then iniset $TEMPEST_CONFIG application_catalog glare_backend "True" fi if [[ "$TEMPEST_MURANO_SCENARIO_TESTS_ENABLED" == "True" ]]; then if is_service_enabled cinder; then iniset $TEMPEST_CONFIG application_catalog cinder_volume_tests "True" fi if [[ "$TEMPEST_MURANO_DEPLOYMENT_TESTS_ENABLED" == "True" ]]; then iniset $TEMPEST_CONFIG application_catalog deployment_tests "True" iniset $TEMPEST_CONFIG application_catalog linux_image "$CLOUD_IMAGE_NAME" fi fi fi } #### lib/murano-dashboard # Dependencies: # # - ``functions`` file # - ``DEST``, ``DATA_DIR``, ``STACK_USER`` must be defined # - ``SERVICE_HOST`` # ``stack.sh`` calls the entry points in this order: # # - install_murano_dashboard # - configure_murano_dashboard # - cleanup_murano_dashboard . $TOP_DIR/lib/horizon # Defaults # -------- HORIZON_CONFIG=${HORIZON_CONFIG:-$HORIZON_DIR/openstack_dashboard/settings.py} HORIZON_LOCAL_CONFIG=${HORIZON_LOCAL_CONFIG:-$HORIZON_DIR/openstack_dashboard/local/local_settings.py} # Set up default repos MURANO_DASHBOARD_REPO=${MURANO_DASHBOARD_REPO:-${GIT_BASE}/openstack/murano-dashboard.git} MURANO_DASHBOARD_BRANCH=${MURANO_DASHBOARD_BRANCH:-master} # Set up default directories MURANO_DASHBOARD_DIR=$DEST/murano-dashboard MURANO_PYTHONCLIENT_DIR=$DEST/python-muranoclient MURANO_DASHBOARD_CACHE_DIR=${MURANO_DASHBOARD_CACHE_DIR:-/tmp/murano} MURANO_REPOSITORY_URL=${MURANO_REPOSITORY_URL:-'http://apps.openstack.org/api/v1/murano_repo/liberty/'} # Entry points # ------------ # configure_murano_dashboard() - Set config files, create data dirs, etc function configure_murano_dashboard() { configure_local_settings_py configure_dashboard_functional_config restart_apache_server } function configure_dashboard_functional_config() { cp $DASHBOARD_FUNCTIONAL_CONFIG_DIR/config.conf.sample $DASHBOARD_FUNCTIONAL_CONFIG_DIR/config.conf sed -e "s%\(^\s*horizon_url\s*=\).*$%\1 $HORIZON_URL%" -i "$DASHBOARD_FUNCTIONAL_CONFIG_DIR/config.conf" sed -e "s%\(^\s*murano_url\s*=\).*$%\1 $MURANO_API_URL%" -i "$DASHBOARD_FUNCTIONAL_CONFIG_DIR/config.conf" sed -e "s/\(^\s*user\s*=\).*$/\1 $OS_USERNAME/" -i "$DASHBOARD_FUNCTIONAL_CONFIG_DIR/config.conf" sed -e "s/\(^\s*password\s*=\).*$/\1 $OS_PASSWORD/" -i "$DASHBOARD_FUNCTIONAL_CONFIG_DIR/config.conf" sed -e "s/\(^\s*tenant\s*=\).*$/\1 $OS_PROJECT_NAME/" -i "$DASHBOARD_FUNCTIONAL_CONFIG_DIR/config.conf" sed -e "s%\(^\s*keystone_url\s*=\).*$%\1 $KEYSTONE_SERVICE_URI/v3%" -i "$DASHBOARD_FUNCTIONAL_CONFIG_DIR/config.conf" echo_summary "Show the DASHBOARD_FUNCTIONAL_CONFIG" cat $DASHBOARD_FUNCTIONAL_CONFIG_DIR/config.conf } function configure_local_settings_py() { local horizon_config_part=$(mktemp) mkdir_chown_stack "$MURANO_DASHBOARD_CACHE_DIR" if is_murano_backend_glare; then # Make Murano use GlARe only if MURANO_USE_GLARE set to True and GlARe # service is enabled local murano_use_glare=True else local murano_use_glare=False fi if [[ -f "$HORIZON_LOCAL_CONFIG" ]]; then sed -e "s/\(^\s*OPENSTACK_HOST\s*=\).*$/\1 '$HOST_IP'/" -i "$HORIZON_LOCAL_CONFIG" fi # Install Murano as plugin for Horizon ln -sf $MURANO_DASHBOARD_DIR/muranodashboard/local/enabled/*.py $HORIZON_DIR/openstack_dashboard/local/enabled/ # Install setting to Horizon ln -sf $MURANO_DASHBOARD_DIR/muranodashboard/local/local_settings.d/*.py $HORIZON_DIR/openstack_dashboard/local/local_settings.d/ # Install murano RBAC policy to Horizon ln -sf $MURANO_DASHBOARD_DIR/muranodashboard/conf/murano_policy.json $HORIZON_DIR/openstack_dashboard/conf/ # Change Murano dashboard settings sed -e "s/\(^\s*MURANO_USE_GLARE\s*=\).*$/\1 $murano_use_glare/" -i $HORIZON_DIR/openstack_dashboard/local/local_settings.d/_50_murano.py sed -e "s%\(^\s*MURANO_REPO_URL\s*=\).*$%\1 '$MURANO_REPOSITORY_URL'%" -i $HORIZON_DIR/openstack_dashboard/local/local_settings.d/_50_murano.py sed -e "s%\(^\s*'NAME':\).*$%\1 os.path.join('$MURANO_DASHBOARD_DIR', 'openstack-dashboard.sqlite')%" -i $HORIZON_DIR/openstack_dashboard/local/local_settings.d/_50_murano.py echo -e $"\nMETADATA_CACHE_DIR = '$MURANO_DASHBOARD_CACHE_DIR'" | sudo tee -a $HORIZON_DIR/openstack_dashboard/local/local_settings.d/_50_murano.py } # init_murano_dashboard() - Initialize databases, etc. function init_murano_dashboard() { # clean up from previous (possibly aborted) runs # create required data files local horizon_manage_py="$HORIZON_DIR/manage.py" $PYTHON "$horizon_manage_py" collectstatic --noinput $PYTHON "$horizon_manage_py" compress --force $PYTHON "$horizon_manage_py" migrate --noinput # Compile message for murano-dashboard cd $MURANO_DASHBOARD_DIR/muranodashboard $PYTHON "$horizon_manage_py" compilemessages restart_apache_server } # install_murano_dashboard() - Collect source and prepare function install_murano_dashboard() { echo_summary "Install Murano Dashboard" git_clone $MURANO_DASHBOARD_REPO $MURANO_DASHBOARD_DIR $MURANO_DASHBOARD_BRANCH setup_develop $MURANO_DASHBOARD_DIR } # cleanup_murano_dashboard() - Remove residual data files, anything left over from previous # runs that a clean run would need to clean up function cleanup_murano_dashboard() { echo_summary "Cleanup Murano Dashboard" # remove all the pannels we've installed, also any pyc/pyo files for i in $(find $MURANO_DASHBOARD_DIR/muranodashboard/local/enabled -iname '_[0-9]*.py' -printf '%f\n'); do rm -rf $HORIZON_DIR/openstack_dashboard/local/enabled/${i%.*}.* done rm $HORIZON_DIR/openstack_dashboard/local/local_settings.d/_50_murano.* rm $HORIZON_DIR/openstack_dashboard/conf/murano_policy.json } # Main dispatcher if is_service_enabled murano; then if [[ "$1" == "stack" && "$2" == "install" ]]; then echo_summary "Installing Murano" install_murano if is_service_enabled horizon; then install_murano_dashboard fi elif [[ "$1" == "stack" && "$2" == "post-config" ]]; then echo_summary "Configuring Murano" configure_murano create_murano_accounts if is_service_enabled horizon; then configure_murano_dashboard fi if is_service_enabled murano-cfapi; then configure_murano_cfapi fi elif [[ "$1" == "stack" && "$2" == "extra" ]]; then echo_summary "Initializing Murano" init_murano if is_service_enabled horizon; then init_murano_dashboard fi start_murano if is_murano_backend_glare; then restart_glare_service fi if is_service_enabled murano-cfapi; then init_murano_cfapi start_service_broker fi # Give Murano some time to Start sleep 3 setup_core_library # Install Murano apps, if needed install_murano_apps elif [[ "$1" == "stack" && "$2" == "test-config" ]]; then configure_murano_tempest_plugin fi if [[ "$1" == "unstack" ]]; then stop_murano if is_service_enabled murano-cfapi; then stop_service_broker fi cleanup_murano if is_service_enabled horizon; then cleanup_murano_dashboard fi fi fi # Restore xtrace $XTRACE ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/devstack/settings0000664000175000017500000000643400000000000016520 0ustar00zuulzuul00000000000000# Settings needed for the Murano plugin # ------------------------------------- # Set up default repos MURANO_REPO=${MURANO_REPO:-${GIT_BASE}/openstack/murano.git} MURANO_BRANCH=${MURANO_BRANCH:-master} # Variables, which used in this function # https://github.com/openstack-dev/devstack/blob/master/functions-common#L500-L506 GITREPO["python-muranoclient"]=${MURANO_PYTHONCLIENT_REPO:-${GIT_BASE}/openstack/python-muranoclient.git} GITBRANCH["python-muranoclient"]=${MURANO_PYTHONCLIENT_BRANCH:-master} GITDIR["python-muranoclient"]=$DEST/python-muranoclient # Set up default directories MURANO_DIR=$DEST/murano MURANO_FILES_DIR=$MURANO_DIR/devstack/files MURANO_CONF_DIR=${MURANO_CONF_DIR:-/etc/murano} MURANO_CONF_FILE=${MURANO_CONF_DIR}/murano.conf MURANO_CFAPI_CONF_FILE=${MURANO_CONF_DIR}/murano-cfapi.conf MURANO_DEBUG=$(trueorfalse True MURANO_DEBUG) MURANO_ENABLE_MODEL_POLICY_ENFORCEMENT=$(trueorfalse False MURANO_ENABLE_MODEL_POLICY_ENFORCEMENT) # Set up murano service endpoint MURANO_SERVICE_HOST=${MURANO_SERVICE_HOST:-$SERVICE_HOST} MURANO_SERVICE_PORT=${MURANO_SERVICE_PORT:-8082} MURANO_SERVICE_PROTOCOL=${MURANO_SERVICE_PROTOCOL:-$SERVICE_PROTOCOL} # Set up settings for service broker API MURANO_CFAPI_SERVICE_PORT=${MURANO_CFAPI_SERVICE_PORT:-8083} MURANO_CFAPI_DEFAULT_TENANT=${MURANO_CFAPI_DEFAULT_TENANT:-admin} # Set up default service user for murano MURANO_ADMIN_USER=${MURANO_ADMIN_USER:-murano} MURANO_KEYSTONE_SIGNING_DIR=${MURANO_KEYSTONE_SIGNING_DIR:-/tmp/keystone-signing-muranoapi} # Set up murano networking settings MURANO_DEFAULT_ROUTER=${MURANO_DEFAULT_ROUTER:-''} MURANO_EXTERNAL_NETWORK=${MURANO_EXTERNAL_NETWORK:-''} DEF_MURANO_DEFAULT_DNS='8.8.8.8' if [[ "$SERVICE_IP_VERSION" == 6 ]]; then DEF_MURANO_DEFAULT_DNS='2001:4860:4860::8888' fi MURANO_DEFAULT_DNS=${MURANO_DEFAULT_DNS:-${DEF_MURANO_DEFAULT_DNS}} # Choose applications for installation MURANO_APPS=${MURANO_APPS:-''} MURANO_APPS_DIR=$DEST/murano-apps MURANO_APPS_REPO=${MURANO_APPS_REPO:-${GIT_BASE}/openstack/murano-apps.git} MURANO_APPS_BRANCH=${MURANO_APPS_BRANCH:-master} # MURANO_RABBIT_VHOST allows to specify a separate virtual host for Murano services. # This is not required if all OpenStack services are deployed by devstack scripts # on a single node. In this case '/' virtual host (which is the default) is enough. # The problem arise when Murano installed in 'devbox' mode, allowing two or more # devboxes to use one common OpenStack host. In this case it's better devboxes # use separated virtual hosts, to avoid conflicts between Murano services. # This couldn't be done using existing variables, so that's why this variable was added. MURANO_RABBIT_VHOST=${MURANO_RABBIT_VHOST:-''} # Settings needed for the Murano Tempest Plugin installation TEMPEST_DIR=$DEST/tempest TEMPEST_CONFIG_DIR=${TEMPEST_CONFIG_DIR:-$TEMPEST_DIR/etc} TEMPEST_CONFIG=$TEMPEST_CONFIG_DIR/tempest.conf TEMPEST_MURANO_SCENARIO_TESTS_ENABLED=$(trueorfalse True TEMPEST_MURANO_SCENARIO_TESTS_ENABLED) TEMPEST_MURANO_DEPLOYMENT_TESTS_ENABLED=$(trueorfalse False TEMPEST_MURANO_DEPLOYMENT_TESTS_ENABLED) # GlARe variables # Glance Artifact Repository endpoint type for Murano communications. # Public by default. GLARE_ENDPOINT_TYPE=${GLARE_ENDPOINT_TYPE:-publicURL} enable_service murano enable_service murano-api enable_service murano-engine ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1696417899.7091806 murano-16.0.0/devstack/upgrade/0000775000175000017500000000000000000000000016355 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/devstack/upgrade/resources.sh0000775000175000017500000000113500000000000020726 0ustar00zuulzuul00000000000000#!/bin/bash set -o errexit source $GRENADE_DIR/grenaderc source $GRENADE_DIR/functions source $TOP_DIR/openrc admin demo set -o xtrace function create { # add later : } function verify_noapi { # currently no good way : } function verify { # add later : } function destroy { # add later : } # Dispatcher case $1 in "create") create ;; "verify_noapi") verify_noapi ;; "verify") verify ;; "destroy") destroy ;; "force_destroy") set +o errexit destroy ;; esac ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/devstack/upgrade/settings0000664000175000017500000000061600000000000020143 0ustar00zuulzuul00000000000000register_project_for_upgrade murano register_db_to_save murano devstack_localrc base enable_plugin murano https://opendev.org/openstack/murano devstack_localrc target enable_plugin murano https://opendev.org/openstack/murano devstack_localrc base enable_service murano-api murano-engine devstack_localrc target enable_service murano-api murano-engine BASE_RUN_SMOKE=False TARGET_RUN_SMOKE=False ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/devstack/upgrade/shutdown.sh0000775000175000017500000000104400000000000020566 0ustar00zuulzuul00000000000000#!/bin/bash set -o errexit source $GRENADE_DIR/grenaderc source $GRENADE_DIR/functions # We need base DevStack functions for this source $BASE_DEVSTACK_DIR/functions source $BASE_DEVSTACK_DIR/stackrc # needed for status directory source $BASE_DEVSTACK_DIR/lib/tls source $BASE_DEVSTACK_DIR/lib/apache MURANO_DEVSTACK_DIR=$(dirname $(dirname $0)) source $MURANO_DEVSTACK_DIR/settings source $MURANO_DEVSTACK_DIR/plugin.sh set -o xtrace stop_murano # sanity check that service is actually down ensure_services_stopped murano-api murano-engine ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/devstack/upgrade/upgrade.sh0000775000175000017500000000375100000000000020351 0ustar00zuulzuul00000000000000#!/usr/bin/env bash # ``upgrade-murano`` echo "*********************************************************************" echo "Begin $0" echo "*********************************************************************" # Clean up any resources that may be in use cleanup() { set +o errexit echo "********************************************************************" echo "ERROR: Abort $0" echo "********************************************************************" # Kill ourselves to signal any calling process trap 2; kill -2 $$ } trap cleanup SIGHUP SIGINT SIGTERM # Keep track of the grenade directory RUN_DIR=$(cd $(dirname "$0") && pwd) # Source params source $GRENADE_DIR/grenaderc # Import common functions source $GRENADE_DIR/functions # This script exits on an error so that errors don't compound and you see # only the first error that occurred. set -o errexit # Upgrade murano # ============== # Get functions from current DevStack source $TARGET_DEVSTACK_DIR/stackrc source $TARGET_DEVSTACK_DIR/lib/apache source $TARGET_DEVSTACK_DIR/lib/tls source $(dirname $(dirname $BASH_SOURCE))/settings source $(dirname $(dirname $BASH_SOURCE))/plugin.sh # Print the commands being run so that we can see the command that triggers # an error. It is also useful for following allowing as the install occurs. set -o xtrace # Save current config files for posterity [[ -d $SAVE_DIR/etc.murano ]] || cp -pr $MURANO_CONF_DIR $SAVE_DIR/etc.murano # Install the target murano install_murano # calls upgrade-murano for specific release upgrade_project murano $RUN_DIR $BASE_DEVSTACK_BRANCH $TARGET_DEVSTACK_BRANCH # Migrate the database murano-db-manage upgrade || die $LINO "DB migration error" start_murano # Don't succeed unless the services come up ensure_services_started murano-api murano-engine set +o xtrace echo "*********************************************************************" echo "SUCCESS: End $0" echo "*********************************************************************" ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1696417899.7091806 murano-16.0.0/doc/0000775000175000017500000000000000000000000013667 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/doc/requirements.txt0000664000175000017500000000037200000000000017155 0ustar00zuulzuul00000000000000# doc build requirements sphinx>=2.0.0,!=2.1.0 # BSD sphinxcontrib-httpdomain>=1.3.0 # BSD reno>=3.1.0 # Apache-2.0 openstackdocstheme>=2.2.1 # Apache-2.0 os-api-ref>=1.4.0 # Apache-2.0 oslo.config>=6.8.0 # Apache-2.0 oslo.policy>=3.6.0 # Apache-2.0 ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1696417899.7091806 murano-16.0.0/doc/source/0000775000175000017500000000000000000000000015167 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1696417899.7091806 murano-16.0.0/doc/source/_templates/0000775000175000017500000000000000000000000017324 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/doc/source/_templates/sidebarlinks.html0000664000175000017500000000046100000000000022665 0ustar00zuulzuul00000000000000

Useful Links

{% if READTHEDOCS %} {% endif %} ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1696417899.7131805 murano-16.0.0/doc/source/admin/0000775000175000017500000000000000000000000016257 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/doc/source/admin/admin_troubleshooting.rst0000664000175000017500000001624500000000000023420 0ustar00zuulzuul00000000000000.. _admin-troubleshooting: =============== Troubleshooting =============== Log location ~~~~~~~~~~~~ By default, logs are sent to stdout. Consider how to set up the log files. Murano API + Engine ------------------- To define a file where to store logs, use the ``log_file`` option in the :file:`murano.conf` file. You can provide an absolute or a relative path. To enable a detailed log file configuration, set up :file:`logging.conf`. The example is provided in :file:`etc/murano` directory. The log configuration file location is set with the ``log_config_append`` option in the murano configuration file. Murano applications ------------------- Murano applications have a separate logging handler and a separate file where all logs from application definitions should be provided. Open the :file:`logging.conf` file and check the ``args: ('applications.log',)`` option in the ``handler_applications`` section. Verify that ``log_config_append`` is not empty and set to the :file:`logging.conf` location. Issues during configuration ~~~~~~~~~~~~~~~~~~~~~~~~~~~ If any issues occur, first of all verify the following: * All murano components have consistent versions: murano-dashboard and murano-engine should use the same or compatible python-muranoclient version. Dependent component versions can be found in :file:`requirements.txt` file. * The database is synced with code by running: .. code-block:: console murano-db-manage --config-file murano.conf upgrade **Failed to execute `murano-db-manage`** * Make sure the ``--config-file`` option is provided. * Check `connection` parameter in the provided configuration file. It should be a `connection string `_. * Check that MySQL or PostgreSQL (depending of what you provided in the connection string) Python modules are installed on the system. **Applications panel is not seen in horizon** * Make sure that the following files are copied to the ``openstack_dashboard/local/enabled`` directory, and _50_murano.py is copied to ``openstack_dashboard/local/local_settings.d`` directory. * _50_dashboard_catalog.py * _51_muranodashboard.py * _60_panel_group_browse.py * _63_panel_murano_catalog.py * _70_panel_group_manage.py * _71_panel_murano_packages.py * _72_panel_murano_images.py * _73_panel_murano_categories.py * _80_panel_group_applications.py * _81_panel_applications_environments.py * Check that murano data is not inserted twice in the settings file and as a plugin. **Applications panel can be browsed, but 'Unable to communicate to murano-api server.' appears** If you have murano registered in keystone, verify the endpoint URL is valid and service has *application-catalog* name. If you do not want to register the murano service in keystone, just add ``MURANO_API_URL`` option to the horizon local setting. Issues during deployment ~~~~~~~~~~~~~~~~~~~~~~~~ Besides identifying errors from log files, there is another and more flexible way to browse deployment errors -- directly from UI. When the *Deploy Failed* status appears, navigate to :menuselection:`Environment Components` and click the :guilabel:`Latest Deployment Log` tab. You can see steps of the deployment and the one that failed would have red color. **while scanning a simple key in "", line 32, column 3: ...** There is an error in the YAML file format. Before uploading a package, validate your file in an online YAML validator like `YAMLint `_. Later `validation tool `_ to check package closely while uploading will be added. **NoPackageForClassFound: Package for class io.murano.Environment is not found** Verify that murano core package is uploaded. If not, the content of the ``meta/io.murano`` folder should be zipped and uploaded to Murano. **[keystoneclient.exceptions.AuthorizationFailure]:** **Authorization failed: You are not authorized to perform the requested action. (HTTP 403)** The token expires during the deployment. Usually the default standard token lifetime is one hour. The error occurs frequently as, in most cases, a deployment takes longer than that or does not start right after a token is generated. Workarounds: * Use trusts. Only possible in the v3 version. Read more in the `official documentation `_ or `here `_. Do not forget to check the corresponding heat and murano settings. Trusts are enabled by default in murano and heat since Kilo release. In murano, the corresponding configuration option is located in the ``engine`` section: .. code-block:: ini [engine] ... # Create resources using trust token rather than user's token (boolean # value) use_trusts = true If your Keystone runs v2 version, see the solutions below. * Make logout/login to compose a new token and start the deployment again. Would not help for long deployment or if the token lifetime is too small. * Increase the token lifetime in the keystone configuration file. **The murano-agent did not respond within 3600 seconds** * Check transport access to the virtual machine: verify that the router has a gateway. * Check the RabbitMQ settings: verify that the agent has valid RabbitMQ parameters. Go to the spawned virtual machine and open :file:`*/etc/murano/agent.conf` on the Linux-based machine or :file:`C:\\Murano\\Agent\\agent.conf` on the Windows-based machine. Additionally, you can examine agent logs that by default are located at :file:`/var/log/murano-agent.log` The first part of the log file contains reconnection attempts to the RabbitMQ since the valid RabbitMQ address and queue have not been obtained yet. * Verify that the ``driver`` option in ``[oslo_messaging_notifications]`` group is set to ``messagingv2``. **murano.engine.system.agent.AgentException** The agent started the execution plan but something went wrong. Examine agent logs (see the previous paragraph for the logs placement information). Also, try to manually execute the application scripts. **[exceptions.EnvironmentError]: Unexpected stack state NOT_FOUND or UPDATE_FAILED** An issue with heat stack creation, examine the heat log file. Try to manually spawn the instance. If the reason of the stack creation fail is ``no valid host was found``, there might be not enough resources or something is wrong with the nova-scheduler. **Router could not be created, no external network found** Find the ``external_network`` parameter in the ``networking`` section of the murano configuration file and verify that the specified external network does exist through Web UI or by executing the :command:`openstack network list --external` command. **Deployment log in the UI contains incomplete reports** Sometimes logs contain only two messages after the application deployment. There are no messages provided in applications themselves: .. code-block:: console 2015-09-21 11:14:58 — Action deploy is scheduled 2015-09-21 11:16:43 — Deployment finished successfully To fix the issue, set the ``driver`` option in the :file:`murano.config` file to ``messagingv2``. ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1696417899.7131805 murano-16.0.0/doc/source/admin/appdev-guide/0000775000175000017500000000000000000000000020631 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/doc/source/admin/appdev-guide/app_debugging.rst0000664000175000017500000000540100000000000024156 0ustar00zuulzuul00000000000000.. _app-debugging: ================================ Application developer's cookbook ================================ If you have not written murano packages before, start from the existing :ref:`Step-by-Step ` guide. It contains general information about murano packages development process. Additionally, see the :ref:`MuranoPL reference `. Load applications from a local directory ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Normally, whenever you make changes to your application, you have to package it, re-upload the package to the API, and delete the old package from the API. This makes developing and testing murano applications troublesome and time-consuming. Murano-engine provides a way to speed up the edit-upload-deploy loop. This can be done with the ``load_packages_from`` option. Murano-engine examines any directories mentioned in this option before accessing the API. Therefore, you do not even need to package the application into a ZIP archive and any changes you make are instantly available to the engine, if you do not plan to check or change the application UI. To check your application's appearance in the OpenStack dashboard, upload the application for the first run. Additionally, re-upload the package using the OpenStack dashboard or CLI each time you update the application UI. To load an application from a local directory, modify the ``load_packages_from`` parameter in murano config ``[engine]`` section. .. code-block:: console [engine] ... load_packages_from = /path/to/murano/applications ... .. note:: The murano-engine scans the directory structure and seeks application manifests. Therefore, you can point the ``load_packages_from`` parameter to a cloned version of the murano-apps repository. Deploy environment using CLI ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ The standard way to deploy an application in murano is by using the murano dashboard (OpenStack dashboard plug-in). However, if the OpenStack dashboard is not available or some sort of automation is required, murano provides the capability to deploy environments through CLI. It is a powerful tool that allows users and application developers make arbitrary changes to apps object-model. This can be useful in early stages of application development to experiment with different object models of an application. You can read more about it in :ref:`Deploying environments using CLI ` Application unit test framework ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ An application unit test framework was created to make development process easier. With this framework you can check different scenarios of application deployment without running real deployments. For more information about application unit tests, see :ref:`Application unit tests `. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/doc/source/admin/appdev-guide/app_development_framework.rst0000664000175000017500000011576000000000000026634 0ustar00zuulzuul00000000000000.. _app-development-framework: ================================= Application development framework ================================= Application development framework is a library that helps application developers to create applications that can be scalable, highly available, (self)healable and do not contain boilerplate code for common application workflow operations. This library is placed into the Murano repository under the ``meta/io.murano.applications`` folder. To allow your applications to use the code of the library, zip it and upload to the Murano application catalog. Framework objectives -------------------- The library allows application developers to focus on their application-specific tasks without the real need to dive into resource orchestration, server farm configuration, and so on. For example, on how to install the software on the VMs, how to configure it to interact with other applications. Application developers are able to focus more on the software configuration tools (scripts, puppets, and others) and care less about the MuranoPL if they do not need to define any custom workflow logic. The main capabilities the library provides and its main use-cases are as follows: * Standard operations are implemented in the framework and can be left as is * The capability to create multi-server applications and scale them * The capability to create composite multi-component applications * The capability to track application failures and recover from them * The capability to define event handlers for various events Quickstart ---------- To use the framework in your application, include the following lines to the ``manifest.yaml`` file: .. code-block:: yaml Require: io.murano.applications: Create a one-component single-server application ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ **To create a simple application deployed on a single server**: #. Include the following lines to the code of the application class: .. code-block:: yaml Namespaces: =: my.new.ns apps: io.murano.applications Name: AppName Extends: apps:SingleServerApplication #. Provide an input for the application ``server`` property in your ``ui.yaml`` file: .. code-block:: yaml Application: ?: type: my.new.ns.AppName server: ?: type: io.murano.resources.LinuxMuranoInstance name: generateHostname($.instanceConfiguration.unitNamingPattern, 1) flavor: $.instanceConfiguration.flavor ... Now you already have the app that creates a server ready for installing software on it. #. To create a fully functional app, add an installation script to the body of the ``onInstallServer`` method: .. code-block:: yaml Methods: onInstallServer: Arguments: - server: Contract: $.class(res:Instance).notNull() - serverGroup: Contract: $.class(apps:ServerGroup).notNull() Body: - $file: sys:Resources.string('installScript.sh') - conf:Linux.runCommand($server.agent, $file) #. Optional. Add other methods that handle certain stages of the application workflow, such as ``onBeforeInstall``, ``onCompleteInstallation``, ``onConfigureServer``, ``onCompleteConfiguration``, and others. For details about these methods, see the :ref:`Software components ` section. Create a one-component multi-server application ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ **To create an application that is intended to be installed on several servers**: #. Make it inherit the ``MultiServerApplication`` class: .. code-block:: yaml Namespaces: =: my.new.ns apps: io.murano.applications Name: AppName Extends: apps:MultiServerApplication #. Instead of the ``server`` property in ``SingleServerApplication``, provide an input for the ``servers`` property that accepts the instance of one of the inheritors of the ``ServerGroup`` class. The ``ui.yaml`` file in this case may look as follows: .. code-block:: yaml Application: ?: type: my.new.ns.AppName servers: ?: type: io.murano.applications.ServerList servers: - ?: type: io.murano.resources.LinuxMuranoInstance name: "Server-1" flavor: $.instanceConfiguration.flavor ... - ?: type: io.murano.resources.LinuxMuranoInstance name: "Server-2" flavor: $.instanceConfiguration.flavor ... #. Define the custom logic of the application in the handler methods, and it will be applied to the whole app, exactly like with ``SingleServerApplication``. Create a scalable multi-server application ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ **To provide the application with the ability to scale**: #. Make the app extend the ``MultiServerApplicationWithScaling`` class: .. code-block:: yaml Namespaces: =: my.new.ns apps: io.murano.applications Name: AppName Extends: apps:MultiServerApplicationWithScaling #. Provide the ``ui.yaml`` file: .. code-block:: yaml Application: ?: type: my.new.ns.AppName servers: ?: type: io.murano.applications.ServerReplicationGroup numItems: $.appConfiguration.numNodes provider: ?: type: io.murano.applications.TemplateServerProvider template: ?: type: io.murano.resources.LinuxMuranoInstance flavor: $.instanceConfiguration.flavor ... serverNamePattern: $.instanceConfiguration.unitNamingPattern The ``servers`` property accepts instance of the ``ServerReplicationGroup`` class, and in turn it requires input of the ``numItems`` and ``provider`` properties. After the deployment, the ``scaleOut`` and ``scaleIn`` public methods (actions) become available in the dashboard UI. For a working example of such application, see the ``com.example.apache.ApacheHttpServer`` package version 1.0.0. Library overview ---------------- The framework includes several groups of classes: ``replication.yaml`` Classes that provide the capability to replicate the resources. ``servers.yaml`` Classes that provide instances grouping and replication. ``component.yaml`` Classes that define common application workflows. ``events.yaml`` Class for handling events. ``baseapps.yaml`` Base classes for applications. As it is described in the :ref:`Quickstart` section, the application makes use of the Application development framework by inheriting from one of the base application classes, such as ``SingleServerApplication``, ``MultiServerApplication``, ``MultiServerApplicationWithScaling``. In turn, these classes are inheritors of the standard ``Application`` class and the ``SoftwareComponent`` class. The latter class binds all of the framework capabilities. The ``SoftwareComponent`` class inherits both ``Installable`` and ``Configurable`` classes which provide boilerplate code for the installation and configuration workflow respectively. They also contain empty methods for each stage of the workflow (e.g. ``onBeforeInstall``, ``onInstallServer``), which are the places where application developers can add their own customization code. The entry point to execute deployment of the software component is its ``deployAt`` method which requires instance of one of the inheritors of the ``serverGroup`` class. It is the object representing the group of servers the application should be deployed to. The application holds such an object as one of its properties. It can be a single server (``SingleServerGroup`` subclass), a prepopulated list of servers (``ServerList`` subclass) or a list of servers that are dynamically generated in runtime (``ServerReplicationGroup`` subclass). ``ServerReplicationGroup`` or, more precisely, one of its parent classes ``ReplicationGroup`` controls the number of items it holds by releasing items over the required amount and requesting creation of the new items in runtime from the ``ReplicaProvider`` class which acts like an object factory. In case of servers, it is ``TemplateServerProvider`` which creates new servers from the given template. Replication is done during the initial deployment and during the scaling actions execution. Framework detailed description ------------------------------ This section provides technical description of all the classes present in the application development library, their hierarchy and usage. Scaling primitives ~~~~~~~~~~~~~~~~~~ There is an ability to group similar resources together, produce new copies of the same resources or release the existing ones on request. Now it is implemented for instances only, other resources may be added later. The following is the hierarchy of classes that provide grouping and replication of resources: :: +-------+ | +-------+ | | +--------+ +------------------+ +-----------------+ | | | | | | | | +-+ | Object <--------+ ReplicationGroup +--------> ReplicaProvider | +-+ | | | | | +--------+ +---+--------------+ +-+--------+------+ ^ ^ ^ | | | | +------------------+-----+ | | | | | +-------+ | | CloneReplicaProvider | | | +-------+ | | + other | | | | +----------+ | +------------------------+ | | | | | | | +-+ | Instance | | | +-+ | | | +----+-----+ | | | | | +-----+-------+ | | | | | | | ServerGroup | | +---------------+--+ | | | | Template | +-----^-------+ +---+----------+ | Server +--+ | | Server +-------> Provider | | +------------+ Replication | +-----+------------+ +---+ | Group | | | | +--------------+ +---+---other---+ | | | +---------------+ **ReplicationGroup** A base class which holds the collection of objects generated in runtime in its ``items`` output property and contains a reference to a ``ReplicaProvider`` object in its ``provider`` property which is used to dynamically generate the objects in runtime. Input properties of this class include the ``minItems`` and ``maxItems`` allowing to limit the number of objects it holds in its collection. An input-output property ``numItems`` allows to declaratively change the set of objects in the collection by setting its size. The ``deploy()`` method is used to apply the replica settings: it drops the objects from the collection if their number exceeds the number specified by the ``numItems`` or generate some new if there are not enough of them. The ``scale()`` method is used to increase or decrease the ``numItems`` by some number specified in the ``delta`` argument of the method, but in range between ``maxItems`` and ``minItems``. **ReplicaProvider** A class which does the object replication. The base one is abstract, its inheritors should implement the abstract ``createReplica`` method to create the actual object. The method accepts the ``index`` parameter to properly parametrize the newly created copy and optional ``owner`` parameter to use it as an owner for the newly created objects. The concrete implementations of this class should define all the input properties needed to create new instances of object. Thus the provider actually acts as a template of the object it generates. **CloneReplicaProvider** An implementation of ``ReplicaProvider`` capable to create replicas by cloning some user-provided object, making use of the ``template()`` contract. **PoolReplicaProvider** Replica provider that takes replicas from the prepopulated pool instead of creating them. **RoundrobinReplicaProvider** Replica provider with a load balancing that returns replica from the prepopulated list. Once the provider runs out of free items it goes to the beginning of the list and returns the same replicas again. **CompositeReplicaProvider** Replica provider which is a composition of other replica providers. It holds the collection of providers in its ``providers`` input property. Its ``ReplicaProvider`` method returns a new replica created by the first provider in that list. If that value is `null`, the replica created by the second provider is returned, and so on. If no not-null replicas are created by all providers, the method returns null. This provider can be used to have some default provider with the ability to fall back to the next options if the preferable one is not successful. Servers replication ~~~~~~~~~~~~~~~~~~~ **ServerGroup** A class that provides static methods for deployment and releasing resources on the group of instances. The ``deployServers()`` static method accepts instance of ``ServerGroup`` class and a list of servers as the parameters and deploys all servers from the list in the environment which owns the server group, unless server is already deployed. The ``releaseServers()`` static method accepts a list of servers as the parameter and consequentially calls ``beginReleaseResources()`` and ``endReleaseResources()`` methods on each server. **ServerList** A class that extends the ``ServerGroup`` class and holds a group of prepopulated servers in its ``servers`` input property. The ``deploy()`` method calls the ``deployServers()`` method with the servers defined in the ``servers`` property. The ``.destroy()`` method calls the ``releaseServers()`` method with the servers defined in the ``servers`` property. **SingleServerGroup** Degenerate case of a ``ServerGroup`` which consists of a single server. Has the ``server`` input property to hold a single server. **CompositeServerGroup** A server group that is composed of other server groups. **ServerReplicationGroup** A subclass of the ``ReplicationGroup`` class and the ``ServerGroup`` class to replicate the ``Instance`` objects it holds. The ``deploy()`` method of this group not only generates new instances of servers but also deploys them if needed. **TemplateServerProvider** A subclass of ``ReplicaProvider`` which is used to produce the objects of one of the ``Instance`` class inheritors by creating them from the provided template with parameterization of the hostnames. The resulting hostname looks like 'Server {index}{groupName}'. May be passed as ``provider`` property to objects of the ``ServerReplicationGroup`` class. **other replica providers** Other subclasses of ``ReplicaProvider`` may be created to produce different objects of ``Instance`` class and its subclasses depending on particular application needs. Classes for grouping and replication of other kinds of resources are to be implemented later. .. _software-components: Software Components ~~~~~~~~~~~~~~~~~~~ The class to handle the lifecycle of the application is the ``SoftwareComponent`` class which is a subclass of ``Installable`` and ``Configurable``: :: +-----------+-+ +-+------------+ | | | | | Installable | | Configurable | | | | | +-----------+-+ +-+------------+ ^ ^ | | | | +-+---------------+-+ | | | SoftwareComponent | | | +-------------------+ The hierarchy of the ``SoftwareComponent`` classes is used to define the workflows of different application lifecycles. The general logic of the application behaviour is contained in the methods of the base classes and the derived classes are able to implement the handlers for the custom logic. The model is event-driven: the workflow consists of the multiple steps, and most of the steps invoke appropriate `on%StepName%` methods intended to provide application-specific logic. Now 'internal' steps logic and their 'public' handlers are split into the separate methods. It should improve the developers' experience and simplify the code of the derived classes. The standard workflows (such as Installation and Configuration) are defined by the ``Installable`` and ``Configurable`` classes respectively. The ``SoftwareComponent`` class inherits both these classes and defines its deployment workflow as a sequence of Installation and Configuration flows. Other future implementations may add new workflow interfaces and mix them in to change the deployment workflow or add new actions. **Installation** workflow consists of the following methods: :: +----------------------------------------------------------------------------------------------------------------------+ | INSTALL | | | | +------------------------------+ +---------------+ | | +------------------------------+ | +---------------+ | | | +------------------------------+ | | +---------------+ +---------------+ | | +----------------------+ | | | | | | | | | | | | | | | | | checkServerIsInstalled | +-+ +----> beforeInstall +----> installServer | +-+ +----> completeInstallation | | | | +-+ | | | +-+ | | | | +------------------------------+ +------+--------+ +------+--------+ +-----------+----------+ | | | | | | +----------------------------------------------------------------------------------------------------------------------+ | | | | | | | | | v v v onBeforeInstall onInstallServer onCompleteInstallation .. list-table:: :widths: 10 10 40 :header-rows: 1 * - Method - Arguments - Description * - **install** - ``serverGroup`` - Entry point of the installation workflow. Iterates through all the servers of the passed ServerGroup and calls the ``checkServerIsInstalled`` method for each of them. If at least one of the calls has returned `false`, calls a ``beforeInstall`` method. Then, for each server which returned `false` as the result of the ``checkServerIsInstalled`` calls the ``installServer`` method to do the actual software installation. After the installation is completed on all the servers and if at least one of the previous calls of ``checkServerIsInstalled`` returned `false`, the method runs the ``completeInstallation`` method. If all the calls to ``checkServerIsInstalled`` return `true`, this method concludes without calling any others. * - **checkServerIsInstalled** - ``server`` - Checks if the given server requires a (re)deployment of the software component. By default checks for the value of the attribute `installed` of the instance. May be overridden by subclasses to provide some better logic (e.g. the app developer may provide code to check if the given software is pre-installed on the image which was provisioned on the VM). * - **beforeInstall** - ``servers``, ``serverGroup`` - Reports the beginning of installation process, sends notification about this event to all objects which are subscribed for it (see *Event notification pattern* section for details) and calls the public event handler ``onBeforeInstall``. * - **onBeforeInstall** - ``servers``, ``serverGroup`` - Public handler of the `beforeInstall` event. Empty in the base class, may be overridden in subclasses if some custom pre-install logic needs to be executed. * - **installServer** - ``server``, ``serverGroup`` - Does the actual software deployment on a given server by calling an ``onInstallServer`` public event handler (with notification on this event). If the installation completes successfully sets the `installed` attribute of the server to `true`, reports successful installation and returns `null`. If an exception encountered during the invocation of ``onInstallServer``, the method handles that exception, reports a warning and returns the server. The return value of the method indicates to the ``install`` method how many failures encountered in total during the installation and with what servers. * - **onInstallServer** - ``server``, ``serverGroup`` - An event-handler method which is called by the ``installServer`` method when the actual software deployment is needed.It is empty in the base class. The implementations should override it with custom logic to deploy the actual software bits. * - **completeInstallation** - ``servers``, ``serverGroup``, ``failedServers`` - It is executed after all the ``installServer`` methods were called. Checks for the number of errors reported during the installation: if it is greater than the value of ``allowedInstallFailures`` property, an exception is raised to interrupt the deployment workflow. Otherwise the method emits notification on this event, calls an ``onCompleteInstallation`` event handler and then reports the successful completion of the installation workflow. * - **onCompleteInstallation** - ``servers``, ``serverGroup``, ``failedServers`` - An event-handler method which is called by the ``completeInstallation`` method when the component installation is about to be completed. Default implementation is empty. Inheritors may implement this method to add some final handling, reporting etc. **Configuration** workflow consists of the following methods: :: +----------------------------------------------------------------------------------------------------------------------+ | CONFIGURATION | | +-----------------+ | | | | | | | +---------------+ +-----------------+ | | | +---------------+ | +-----------------+ | | | +------------v--+ +---------------+ | | +--------------+ +-----------------+ | | +-----------------------+ | | | | | | | | | | | | | | | | | | | checkCluster\ +---> checkServer\ | +-+---> preConfigure +---> configureServer | +-+---> completeConfiguration | | | | IsConfigured | | IsConfigured +-+ | | | +-+ | | | | +------------+--+ +---------------+ +------+-------+ +--------+--------+ +-----------+-----------+ | | | | | | | | | | | | | | +----------v----------+ | | | | | | | | | | | | | getConfigurationKey | | | | | | | | | | | | | +---------------------+ | | | | | | | | | +----------------------------------------------------------------------------------------------------------------------+ | | | | | | v v v configureSecurity, onConfigureServer onCompleteConfiguration onPreConfigure .. list-table:: :widths: 10 10 40 :header-rows: 1 * - Method - Arguments - Description * - **configure** - ``serverGroup`` - Entry point of the configuration workflow. Calls a ``checkClusterIsConfigured`` method. If the call returns `true`, workflow exits without any further action. Otherwise for each server in the ``serverGroup`` it calls ``checkServerIsConfigured`` method and gets the list of servers that need reconfiguration. The ``preConfigure`` method is called with that list. At the end calls the ``completeConfiguration`` method. * - **checkClusterIsConfigured** - ``serverGroup`` - Has to return `true` if the configuration (i.e. the values of input properties) of the component has not been changed since it was last deployed on the given server group. Default implementation calls the ``getConfigurationKey`` method and compares the returned result with a value of `configuration` attribute of ``serverGroup``. If the results match returns `true` otherwise `false`. * - **getConfigurationKey** - None - Should return some values describing the configuration state of the component. This state is used to track the changes of the configuration by the ``checkClusterIsConfigured`` and ``checkServerIsConfigured`` methods. Default implementation returns a synthetic value which gets updated on every environment redeployment. Thus the subsequent calls of the ``configure`` method on the same server group during the same deployment will not cause the reconfiguration, while the calls on the next deployment will reapply the configuration again. The inheritors may redefine this to include the actual values of the configuration properties, so the configuration is reapplied only if the appropriate input properties are changed. * - **checkServerIsConfigured** - ``server``, ``serverGroup`` - It is called to check if the particular server of the server group has to be reconfigured thus providing more precise control compared to cluster-wide ``checkClusterIsConfigured``. Default implementation calls the ``getConfigurationKey`` method and compares the returned result with a value of `configuration` attribute of the server. If the results match returns `true` otherwise `false`. This method gets called only if the ``checkClusterIsConfigured`` method returned `false` for the whole server group. * - **preConfigure** - ``servers``, ``serverGroup`` - Reports the beginning of configuration process, calls the ``configureSecurity`` method, emits the notification and calls the public event handler ``onPreConfigure``. This method is called once per the server group and only if the changes in configuration are detected. * - **configureSecurity** - ``servers``, ``serverGroup`` - Intended for configuring the security rules. It is empty in the base class. Fully implemented in the ``OpenStackSecurityConfigurable`` class which is the inheritor of ``Configurable``. * - **onPreConfigure** - ``servers``, ``serverGroup`` - Public event-handler which is called by the ``preConfigure`` method when the (re)configuration of the component is required. Default implementation is empty. Inheritors may implement this method to set various kinds of cluster-wide states or output properties which may be of use at later stages of the workflow. * - **configureServer** - ``server``, ``serverGroup`` - Does the actual software configuration on a given server by calling the ``onConfigureServer`` public event handler. Before that reports the beginning of the configuration and emits the notification. If the configuration completes successfully calls the ``getConfigurationKey`` method and sets the `configuration` attribute of the server to resulting value thus saving the configuration applied to a given server. Returns `null` to indicate successful configuration. If an exception encountered during the invocation of ``onConfigureServer``, the method will handle that exception, report a warning and return the current server to signal its failure to the ``configure`` method. * - **onConfigureServer** - ``server``, ``serverGroup`` - An event-handler method which is called by the ``configureServer`` method when the actual software configuration is needed. It is empty in the base class. The implementations should override it with custom logic to apply the actual software configuration on a given server. * - **completeConfiguration** - ``servers``, ``serverGroup``, ``failedServers`` - It is executed after all the ``configureServer`` methods were called. Checks for the number of errors reported during the configuration: if it is greater than set by the ``allowedConfigurationFailures`` property, an exception is raised to interrupt the deployment workflow. Otherwise the method emits notification, calls an ``onCompleteConfiguration`` event handler, calls the ``getConfigurationKey`` method and sets the `configuration` attribute of the server group to resulting value and then reports successful completion of the configuration workflow. * - **onCompleteConfiguration** - ``servers``, ``serverGroup``, ``failedServers`` - The event-handler method which is called by the ``completeConfiguration`` method when the component configuration is finished at all the servers. Default implementation is empty. Inheritors may implement this method to add some final handling, reporting etc. The ``OpenStackSecurityConfigurable`` class extends ``Configurable`` by implementing the ``configureSecurity`` method of the base class and adding the empty ``getSecurityRules`` method. .. list-table:: :widths: 10 10 40 :header-rows: 1 * - Method - Arguments - Description * - **getSecurityRules** - None - Returns an empty dictionary in default implementation. Inheritors which want to add security rules during the app configuration should implement this method and make it return a list of dictionaries describing the security rules with the following keys: * FromPort (port number, e.g. 80). * ToPort (port number, e.g. 80). * IpProtocol: (string, e.g. 'tcp'). * External: (boolean: `true` means that the inbound traffic to the given port (or port range) may originate from outside of the environment; `false` means that only the VMs spawned by this or other apps of the current environment may connect to this port). * Ethertype: (optional, can be 'IPv4' or 'IPv6'). * - **configureSecurity** - ``servers``, ``serverGroup`` - Gets the list of security rules provided by the ``getSecurityRules`` method and adds security group with these rules to the Heat stacks of all regions which the component's ``servers`` are deployed to Consider the following example of this class usage: .. code-block:: yaml Namespaces: =: com.example.apache apps: io.murano.applications Name: ApacheHttpServer Extends: - apps:MultiServerApplicationWithScaling - apps:OpenStackSecurityConfigurable Methods: getSecurityRules: Body: - Return: - ToPort: 80 FromPort: 80 IpProtocol: tcp External: true - ToPort: 443 FromPort: 443 IpProtocol: tcp External: true In the example above, the ``ApacheHttpServer`` class is configured to create a security group with two security rules allowing network traffic over HTTP and HTTPS protocols on its deployment. The ``SoftwareComponent`` class inherits both ``Installable`` and ``Configurable`` and adds several additional methods. .. list-table:: :widths: 10 10 40 :header-rows: 1 * - Method - Arguments - Description * - **deployAt** - ``serverGroup`` - Binds all workflows into one process. Consequentially calls ``deploy`` method of the ``serverGroup``, ``install`` and ``configure`` methods inherited from the parent classes. * - **report** - ``message`` - Reports a ``message`` using environment's reporter. * - **detectSuccess** - ``allowedFailures``, ``serverGroup``, ``failedServers`` - Static method that returns `true` in case the actual number of failures (number of ``failedServers``) is less than or equal to the ``allowedFailures``. The latter can be on of the following options: `none`, `one`, `two`, `three`, `any`, 'quorum'. `any` allows any number of failures during the installation or configuration. `quorum` allows failure of less than a half of all servers. Event notification pattern ~~~~~~~~~~~~~~~~~~~~~~~~~~ The ``Event`` class may be used to issue various notifications to other MuranoPL classes in an event-driven manner. Any object which is going to emit the notifications should declare the instances of the ``Event`` class as its public Runtime properties. You can see the examples of such properties in the ``Installable`` and ``Configurable`` classes: .. code-block:: yaml Name: Installable Properties: beforeInstallEvent: Contract: $.class(Event).notNull() Usage: Runtime Default: name: beforeInstall The object which is going to subscribe for the notifications should pass itself into the ``subscribe`` method of the event along with the name of its method which will be used to handle the notification: .. code-block:: yaml $event.subscribe($subscriber, handleFoo) The specified handler method must be present in the subscriber class (if the method name is missing it will default to ``handle%Eventname%``) and have at least one standard (i.e. not ``VarArgs`` or ``KwArgs``) argument which will be treated as ``sender`` while invoking. The ``unsubscribe`` method does the opposite and removes object from the subscribers of the event. The class which is going to emit the notification should call the ``notify`` method of the event and pass itself as the first argument (``sender``). All the optional parameters of the event may be passed as varargs/kwargs of the ``notify`` call. They will be passed all the way to the handler methods. This is how it looks in the ``Installable`` class: .. code-block:: yaml beforeInstall: Arguments: - servers: Contract: - $.class(res:Instance).notNull() - serverGroup: Contract: $.class(ServerGroup).notNull() Body: - ... - $this.beforeInstallEvent.notify($this, $servers, $serverGroup) - ... The ``notifyInParallel`` method does the same, but invokes all handlers of subscribers in parallel. Base application classes ~~~~~~~~~~~~~~~~~~~~~~~~ There are several base classes that extend standard ``io.murano.Application`` class and ``SoftwareComponent`` class from the application development library. **SingleServerApplication** A base class for applications running a single software component on a single server only. Its ``deploy`` method simply creates the ``SingleServerGroup`` with the ``server`` provided as an application input. **MultiServerApplication** A base class for applications running a single software component on multiple servers. Unlike ``SingleServerApplication``, it has the ``servers`` input property instead of ``server``. It accepts instance of on of the inheritors of the ``ServerGroup`` class. **MultiServerApplicationWithScaling** Extends ``MultiServerApplication`` with the ability to scale the application by increasing (scaling out) or decreasing (scaling in) the number of nodes with the application after it is installed. The differences from ``MultiServerApplication`` are: * the ``servers`` property accepts only instances of ``ServerReplicationGroup`` rather than any ``ServerGroup`` * the additional optional ``scaleFactor`` property accepts the number by which the app is scaled at once; it defaults to 1 * the ``scaleOut`` and ``scaleIn`` public methods are added Application developers may as well define their own classes using the same approach and combining base classes behaviour with the custom code to satisfy the needs of their applications. ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1696417899.7171805 murano-16.0.0/doc/source/admin/appdev-guide/app_migrating/0000775000175000017500000000000000000000000023452 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/doc/source/admin/appdev-guide/app_migrating/app_migrate_to_juno.rst0000664000175000017500000000510500000000000030232 0ustar00zuulzuul00000000000000.. _app_migrate_to_juno: Migrate applications from Murano v0.5 to Stable/Juno ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Applications created for murano v0.5, unfortunately, are not supported in Murano stable/juno. This document provides the application code changes required for compatibility with the stable/juno murano version. Rename *'Workflow'* to *'Methods'* ---------------------------------- In stable/juno the name of section containing class methods is renamed to *Methods*, as the latter is more OOP and doesn't cause confusion with Mistral. So, you need to change it in *app.name/Classes* in all classes describing workflow of your app. For example: .. code-block:: yaml Workflow: deploy: Body: - $._environment.reporter.report($this, 'Creating VM') Should be changed to: .. code-block:: yaml Methods: deploy: Body: - $._environment.reporter.report($this, 'Creating VM') Change the Instance type in the UI definition 'Application' section ------------------------------------------------------------------- The Instance class was too generic and contained some dirty workarounds to differently handle Windows and Linux images, to bootstrap an instance in a number of ways, etc. To solve these problems more classes were added to the *Instance* inheritance hierarchy. Now, base *Instance* class is abstract and agnostic of the desired OS and agent type. It is inherited by two classes: *LinuxInstance* and *WindowsInstance*. - *LinuxInstance* adds a default security rule for Linux, opening a standard SSH port; - *WindowsInstance* adds a default security rule for Windows, opening an RDP port. At the same time WindowsInstance prepares a user-data allowing to use Murano v1 agent. *LinuxInstance* is inherited by two other classes, having different software config method: - *LinuxMuranoInstance* adds a user-data preparation to configure Murano v2 agent; - *LinuxUDInstance* adds a custom user-data field allowing the services to supply their own user data. You need to specify the instance type which is required by your app. It specifies a field in UI, where user can select an image matched to the instance type. This change must be added to UI form definition in *app.name/UI/ui.yaml*. For example, if you are going to install your application on Ubuntu, you need to change: .. code-block:: yaml Application: ?: instance: ?: type: io.murano.resources.Instance to: .. code-block:: yaml Application: ?: instance: ?: type: io.murano.resources.LinuxMuranoInstance ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/doc/source/admin/appdev-guide/app_migrating/app_migrate_to_kilo.rst0000664000175000017500000000715100000000000030220 0ustar00zuulzuul00000000000000.. _app_migrate_to_kilo: Migrate applications to Stable/Kilo ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ In Kilo, there are no breaking changes that affect backward compatibility. But there are two new features which you can use since Kilo. 1. Pluggable Pythonic classes for murano ---------------------------------------- Now you can create plug-ins for MuranoPL. A plug-in (extension) is an independent Python package implementing functionality which you want to add to the workflow of your application. For a demo application demonstrating the usage of plug-ins, see the ``murano/contrib/plugins/murano_exampleplugin`` folder. The application consist of the following components: * An `ImageValidatorMixin` class that inherits the generic instance class (``io.murano.resources.Instance``) and adds a method capable of validating the instance image for having an appropriate murano metadata type. This class may be used as a mixin when added to inheritance hierarchy of concrete instance classes. * A concrete class called `DemoInstance` that inherits from `io.murano.resources.LinuxMuranoInstance` and `ImageValidatorMixin` to add the image validation logic to a standard, murano-enabled and Linux-based instance. * An application that deploys a single VM using the `DemoInstance` class if the tag on the user-supplied image matches the user-supplied constant. The **ImageValidatorMixin** demonstrates the instantiation of plug-in provided class and its usage, as well as handling of exception which may be thrown if the plug-in is not installed in the environment. 2. Murano mistral integration ----------------------------- The core library has a new system class for mistral client that allows to call Mistral APIs from the murano application model. The system class allows you to: * Upload a mistral workflow to mistral. * Trigger the mistral workflow that is already deployed, wait for completion and return the execution output. To use this feature, add some mistral workflow to ``Resources`` folder of your package. For example, create file `TestEcho_MistralWorkflow.yaml`: .. code-block:: yaml version: '2.0' test_echo: type: direct input: - input_1 output: out_1: <% $.task1_output_1 %> out_2: <% $.task2_output_2 %> out_3: <% $.input_1 %> tasks: my_echo_test: action: std.echo output='just a string' publish: task1_output_1: 'task1_output_1_value' task1_output_2: 'task1_output_2_value' on-success: - my_echo_test_2 my_echo_test_2: action: std.echo output='just a string' publish: task2_output_1: 'task2_output_1_value' task2_output_2: 'task2_output_2_value' .. And provide workflow to use the mistral client: .. code-block:: yaml Namespaces: =: io.murano.apps.test std: io.murano sys: io.murano.system Name: MistralShowcaseApp Extends: std:Application Properties: name: Contract: $.string().notNull() mistralClient: Contract: $.class(sys:MistralClient) Usage: Runtime Methods: initialize: Body: - $this.mistralClient: new(sys:MistralClient) deploy: Body: - $resources: new('io.murano.system.Resources') - $workflow: $resources.string('TestEcho_MistralWorkflow.yaml') - $.mistralClient.upload(definition => $workflow) - $output: $.mistralClient.run(name => 'test_echo', inputs => dict(input_1 => input_1_value)) - $this.find(std:Environment).reporter.report($this, $output.get('out_3')) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/doc/source/admin/appdev-guide/app_migrating/app_migrate_to_liberty.rst0000664000175000017500000001745300000000000030742 0ustar00zuulzuul00000000000000.. _app_migrate_to_liberty: Migrate applications to Stable/Liberty ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ In Liberty a number of useful features that can be used by developers creating their murano applications were implemented. This document describes these features and steps required to include them to new apps. 1. Versioning ------------- Package version ``````````````` Now murano packages have a new optional attribute in their manifest called `Version` - a standard SemVer format version string. All MuranoPL classes have the version of the package they contained in. To specify the version of your package, add a new section to the manifest file: .. code-block:: yaml Version: 0.1.0 .. If no version specified, the package version will be equal to *0.0.0*. Package requirements ```````````````````` There are cases when packages may require other packages for their work. Now you need to list such packages in the `Require` section of the manifest file: .. code-block:: yaml Require: package1_FQN: version_spec_1 ... packageN_FQN: version_spec_N .. `version_spec` here denotes the allowed version range. It can be either in semantic_version specification pip-like format or as partial version string. If you do not want to specify the package version, leave this value empty: .. code-block:: yaml Require: package1_FQN: '>=0.0.3' package2_FQN: .. In this case, the last dependency *0.x.y* is used. .. note:: All packages depend on the `io.murano` package (core library). If you do not specify this requirement in the list (or the list is empty or even there is no `Require` key in package manifest), then dependency *io.murano: 0* will be automatically added. Object version `````````````` Now you can specify the version of objects in UI definition when your application requires specific version of some class. To do this, add new key `classVersion` to section `?` describing object: .. code-block:: yaml ?: type: io.test.apps.TestApp classVersion: 0.0.1 .. `classVersion` of all classes included to package equals `Version` of this package. 2. YAQL ------- In Liberty, murano was updated to use `yaql 1.0.0`. The new version of YAQL allows you to use a number of new functions and features that help to increase the speed of developing new applications. .. note:: Usage of these features makes your applications incompatible with older versions of murano. Also, in Liberty you can change `Format` in the manifest of package from *1.0* to *1.1* or *1.2*. * **1.0** - supported by all versions of murano. * **1.1** - supported by Liberty+. Specify it, if you want to use features from *yaql 0.2* and *yaql 1.0.0* at the same time in your application. * **1.2** - supported by Liberty+. A number of features from *yaql 0.2* do not work with this format (see the list below). We recommend you to use it for new applications where compatibility with Kilo is not required. Some examples of *yaql 0.2* features that are not compatible with the *1.2* format ``````````````````````````````````````````````````````````````````````````````````` * Several functions now cannot be called as MuranoObject methods: ``id(), cast(), super(), psuper(), type()``. * Now you do not have the ability to compare non-comparable types. For example "string != false" * Dicts are not iterable now, so you cannot do this: ``If: $key in $dict``. Use ``$key in $dict.keys()`` or ``$v in $dict.values()`` * Tuples are not available. ``=>`` always means keyword argument. 3. Simple software configuration -------------------------------- Previously, you always had to create execution plans even when some short scripts had to be executed on a VM. This process included creating a template file, creating a script, and describing the sending of the execution plan to the murano agent. Now you can use a new class **io.murano.configuration.Linux** from murano `core-library`. This allows sending short commands to the VM and putting files from the ``Resources`` folder of packages to some path on the VM without the need of creating execution plans. To use this feature you need to: * Declare a namespace (for convenience) .. code-block:: yaml Namespaces: conf: io.murano.configuration ... .. * Create object of ``io.murano.configuration.Linux`` class in workflow of your application: .. code-block:: yaml $linux: new(conf:Linux) .. * Run one of the two feature methods: ``runCommand`` or ``putFile``: .. code-block:: yaml # first argument is agent of instance, second - your command $linux.runCommand($.instance.agent, 'service apache2 restart') .. or: .. code-block:: yaml # getting content of file from 'Resources' folder - $resources: new(sys:Resources) - $fileContent: $resources.string('your_file.name') # put this content to some directory on VM - $linux.putFile($.instance.agent, $fileContent, '/tmp/your_file.name') .. .. note:: At the moment, you can use this feature only if your app requires an instance of ``LinuxMuranoInstance`` type. 4. UI network selection element ------------------------------- Since Liberty, you can provide users with the ability to choose where to join their VM: to a new network created during the deployment, or to an already existing network. Dynamic UI now has a new type of field - ``NetworkChoiseField``. This field provides a selection of networks and their subnetworks as a dropdown populated with those which are available to the current project (tenant). To use this feature, you should make the following updates in the Dynamic UI of an application: * Add ``network`` field: .. code-block:: yaml fields: - name: network type: network label: Network description: Select a network to join. 'Auto' corresponds to a default environment's network. required: false murano_networks: translate .. To see the full list of the ``network`` field arguments, refer to the UI forms :ref:`specification `. * Add template: .. code-block:: yaml Templates: customJoinNet: - ?: type: io.murano.resources.ExistingNeutronNetwork internalNetworkName: $.instanceConfiguration.network[0] internalSubnetworkName: $.instanceConfiguration.network[1] .. * Add declaration of `networks` instance property: .. code-block:: yaml Application: ?: type: com.example.exampleApp instance: ?: type: io.murano.resources.LinuxMuranoInstance networks: useEnvironmentNetwork: $.instanceConfiguration.network[0]=null useFlatNetwork: false customNetworks: switch($.instanceConfiguration.network[0], $=null=>list(), $!=null=>$customJoinNet) .. For more details about this feature, see :ref:`use-cases ` .. note:: To use this feature, the version of UI definition must be **2.1+** 5. Remove name field from fields and object model in dynamic UI --------------------------------------------------------------- Previously, each class of an application had a ``name`` property. It had no built-in predefined meaning for MuranoPL classes and mostly used for dynamic UI purposes. Now you can create your applications without this property in classes and without a corresponding field in UI definitions. The field for app name will be automatically generated on the last management form before start of deployment. Bonus of deleting this - to remove unused property from muranopl class that is needed for dashboard only. So, to update existing application developer should make 3 steps: #. remove ``name`` field and property declaration from UI definition; #. remove ``name`` property from class of application and make sure that it is not used anywhere in workflow #. set version of UI definition to **2.2 or higher** ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/doc/source/admin/appdev-guide/app_migrating/app_migrate_to_newton.rst0000664000175000017500000001140000000000000030564 0ustar00zuulzuul00000000000000.. _app_migrate_to_newton: Migrate applications to Stable/Newton ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ In Newton a number of useful features that can be used by developers creating their murano applications were implemented. Also some changes are not backward compatible. This document describes these features, how they may be included into the new apps and what benefits the apps may gain. 1. New syntax for the action declaration ---------------------------------------- Previously, for declaring action in MuranoPL application, following syntax was used: .. code-block:: yaml methodName: Usage: Action This syntax is deprecated now for packages with FormatVersion starting from 1.4, and you should use the `Scope` attribute: .. code-block:: yaml methodName: Scope: Public For more information about actions in MuranoPL, see :ref:`actions`. 2. Usage of static methods as Action ------------------------------------ Now you can declare static method as action with `Scope` and `Usage` attributes .. code-block:: yaml methodName: Scope: Public Usage: Static For more information about static methods in MuranoPL, see :ref:`static_methods_and_properties`. 3. Template contract support ---------------------------- New contract function ``template`` was introduced. ``template`` works similar to the ``class`` in regards to the data validation but does not instantiate objects. The template is just a dictionary with object model representation of the object. It is useful when you do not necessarily need to pass the actual object as a property or as a method argument and use it right away, but rather to create new objects of this type in runtime from the given template. It is especially beneficial for resources replication or situations when object creation depends on some conditions. Objects that are assigned to the property or argument with ``template`` contract will be automatically converted to their object model representation. 4. Multi-region support ----------------------- Starting from Newton release cloud resource classes (instances, networks, volumes) can be explicitly put into OpenStack regions other than environment default. Thus it becomes possible to have applications that make use of more than one region including stretching/bursting to other regions. Each resource class has got new ``regionName`` property which controls its placement. If no value is provided, default region for environment is used. Applications wanting to take advantage of multi-region support should access security manager and Heat stacks from regions of their resources rather than from the environment. Regions need to be configured before they can be used. Please refer to documentation on how to do this: :ref:`multi_region`. Changes in the core library ``````````````````````````` `io.murano.Environment` class contains `regions` property with list of `io.murano.CloudRegion` objects. Heat stack, networks and agent listener are now owned by `io.murano.CloudRegion` instances rather than by `Environment`. You can not get `io.murano.resources.Network` objects from `Enviromnent::defaultNetworks` now. This property only contains templates for `io.murano.CloudRegion` default networks. The proper way to retrieve `io.murano.resources.Network` object is now the following: .. code-block:: yaml $region: $instance.getRegion() $networks: $region.defaultNetworks 5. Changes to property validation --------------------------------- `string()` contract no longer converts to string anything but scalar values. 6. Garbage collection --------------------- New approach to resource deallocation was introduced. Previously murano used to load ``Objects`` and ``ObjectsCopy`` sections of the JSON object model independently which cause for objects that were not deleted between deployments to instantiate twice. If deleted objects were to cause any changes to such alive objects they were made to the objects loaded from ``ObjectsCopy`` and immediately discarded before the deployment. Now this behaviour is changed and there are no more duplicates of the same object. Applications can also make use of the new features. Now it is possible to perform on-demand destruction of the unreferenced MuranoPL objects during the deployment from the application code. The ``io.murano.system.GC.GarbageCollector.collect()`` static method may be used for that. Also objects obtained ability to set up destruction dependencies to the other objects. Destruction dependencies allow to define the preferable order of objects destruction and let objects be aware of other objects destruction, react to this event, including the ability to prevent other objects from being destroyed. Please refer to the documentation on how to use the :ref:`Garbage Collector `. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/doc/source/admin/appdev-guide/app_migrating.rst0000664000175000017500000000075200000000000024210 0ustar00zuulzuul00000000000000.. _app_migrating: ======================================= Migrating applications between releases ======================================= This document describes how a developer of murano application can update existing packages to make them synchronized with all implemented features and requirements. .. toctree:: :maxdepth: 1 app_migrating/app_migrate_to_juno app_migrating/app_migrate_to_kilo app_migrating/app_migrate_to_liberty app_migrating/app_migrate_to_newton ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/doc/source/admin/appdev-guide/app_unit_tests.rst0000664000175000017500000002075200000000000024432 0ustar00zuulzuul00000000000000.. _app-unit-tests: ====================== Application unit tests ====================== Murano applications are written in :ref:`MuranoPL `. To make the development of applications easier and enable application testing, a special framework was created. So it is possible to add unit tests to an application package and check if the application is in actual state. Also, application deployment can be simulated with unit tests, so you do not need to run the murano engine. A separate service that is called *murano-test-runner* is used to run MuranoPL unit tests. All application test cases should be: * Specified in the MuranoPL class, inherited from `io.murano.test.testFixture `_ This class supports loading object model with the corresponding `load(json)` function. Also it contains a minimal set of assertions such as ``assertEqual`` and etc. Note, that test class has the following reserved methods are: * *initialize* is executed once, like in any other murano application * *setUp* is executed before each test case * *tearDown* is executed after each test case * Named with *test* prefix .. code-block:: console usage: murano-test-runner [-h] [--config-file CONFIG_FILE] [--os-auth-url OS_AUTH_URL] [--os-username OS_USERNAME] [--os-password OS_PASSWORD] [--os-project-name OS_PROJECT_NAME] [-l [ [ ...]]] [-v] [--version] [ [ ...]] positional arguments: Full name of application package that is going to be tested List of method names to be tested optional arguments: -h, --help show this help message and exit --config-file CONFIG_FILE Path to the murano config --os-auth-url OS_AUTH_URL Defaults to env[OS_AUTH_URL] --os-username OS_USERNAME Defaults to env[OS_USERNAME] --os-password OS_PASSWORD Defaults to env[OS_PASSWORD] --os-project-name OS_PROJECT_NAME Defaults to env[OS_PROJECT_NAME] -l [ [ ...]], --load_packages_from [ [ ...]] Directory to search packages from. Will be used instead of directories, provided in the same option in murano configuration file. -v, --verbose increase output verbosity --version show program's version number and exit The fully qualified name of a package is required to specify the test location. It can be an application package that contains one or several classes with all the test cases, or a separate package. You can specify a class name to execute all the tests located in it, or specify a particular test case name. Authorization parameters can be provided in the murano configuration file, or with higher priority ``-os-`` parameters. Consider the following example of test execution for the Tomcat application. Tests are located in the same package with application, but in a separate class called ``io.murano.test.TomcatTest``. It contains ``testDeploy1`` and ``testDeploy2`` test cases. The application package is located in the */package/location/directory* (murano-apps repository e.g). As the result of the following command, both test cases from the specified package and class will be executed. .. code-block:: console murano-test-runner io.murano.apps.apache.Tomcat io.murano.test.TomcatTest -l /package/location/directory /io.murano/location -v The following command runs a single *testDeploy1* test case from the application package. .. code-block:: console murano-test-runner io.murano.apps.apache.Tomcat io.murano.test.TomcatTest.testDeploy1 The main purpose of MuranoPL unit test framework is to enable mocking. Special :ref:`yaql` functions are registered for that: `def inject(target, target_method, mock_object, mock_name)` ``inject`` to set up mock for *class* or *object*, where mock definition is a *name of the test class method* `def inject(target, target_method, yaql_expr)` ``inject`` to set up mock for *a class* or *object*, where mock definition is a *YAQL expression* Parameters description: **target** MuranoPL class name (namespaces can be used or full class name in quotes) or MuranoPL object **target_method** Method name to mock in target **mock_object** Object, where mock definition is contained **mock_name** Name of method, where mock definition is contained **yaql_expr** YAQL expression, parameters are allowed So the user is allowed to specify mock functions in the following ways: * Specify a particular method name * Provide a YAQL expression Consider how the following functions may be used in the MuranoPL class with unit tests: .. code-block:: yaml Namespaces: =: io.murano.test sys: io.murano.system Extends: TestFixture Name: TomcatTest Methods: initialize: Body: # Object model can be loaded from JSON file, or provided # directly in MuranoPL code as a YAML insertion. - $.appJson: new(sys:Resources).json('tomcat-for-mock.json') - $.heatOutput: new(sys:Resources).json('output.json') - $.log: logger('test') - $.agentCallCount: 0 # Mock method to replace the original one agentMock: Arguments: - template: Contract: $ - resources: Contract: $ - timeout: Contract: $ Default: null Body: - $.log.info('Mocking murano agent') - $.assertEqual('Deploy Tomcat', $template.Name) - $.agentCallCount: $.agentCallCount + 1 # Mock method, that returns predefined heat stack output getStackOut: Body: - $.log.info('Mocking heat stack') - Return: $.heatOutput testDeploy1: Body: # Loading object model - $.env: $this.load($.appJson) # Set up mock for the push method of *io.murano.system.HeatStack* class - inject(sys:HeatStack, push, $.heatOutput) # Set up mock with YAQL function - inject($.env.stack, output, $.heatOutput) # Set up mock for the concrete object with mock method name - inject('io.murano.system.Agent', call, $this, agentMock) # Mocks will be called instead of original function during the deployment - $.env.deploy() # Check, that mock worked correctly - $.assertEqual(1, $.agentCallCount) testDeploy2: Body: - inject(sys:HeatStack, push, $this, getStackOut) - inject(sys:HeatStack, output, $this, getStackOut) # Mock is defined with YAQL function and it will print the original variable (agent template) - inject(sys:Agent, call, withOriginal(t => $template) -> $.log.info('{0}', $t)) - $.env: $this.load($.appJson) - $.env.deploy() - $isDeployed: $.env.applications[0].getAttr(deployed, false, 'com.example.apache.Tomcat') - $.assertEqual(true, $isDeployed) Provided methods are test cases for the Tomcat application. Object model and heat stack output are predefined and located in the package ``Resources`` directory. By changing some object model or heat stack parameters, different cases may be tested without a real deployment. Note, that some asserts are used in those example. The first one is checked, that agent call function was called only once as needed. And assert from the second test case checks for a variable value at the end of the application deployment. Test cases examples can be found in :file:`TomcatTest.yaml` class of the Apache Tomcat application located at `murano-apps repository `_. You can run test cases with the commands provided above. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/doc/source/admin/appdev-guide/cinder_volume_supporting.rst0000664000175000017500000001024600000000000026513 0ustar00zuulzuul00000000000000.. _cinder_volume_supporting: Cinder volume support ~~~~~~~~~~~~~~~~~~~~~ Cinder volume is a block storage service for OpenStack, which represents a detachable device, similar to a USB hard drive. You can attach a volume to only one instance. In murano, it is possible to work with Cinder volumes in several ways: * Attaching Cinder volumes to murano instance * Booting from Cinder volume Below both ways are considered with ApacheHttpServer application as an example. For more information about Cinder volumes, see `Manage Cinder volumes `_. Attaching Cinder volumes ------------------------ Several volumes can be attached to the murano instance. Consider an example that shows how to attach a created volume to the instance (next, in the *Booting from Cinder volume* section, we are going to boot from a volume created by us). **Example** #. In the OpenStack dashboard, go to :guilabel:`Volumes` to create a volume. #. Modify the ``ui.yaml`` file: .. code-block:: yaml .... Application: .... instance: .... volumes: $.volumeConfiguration.volumePath: ?: type: io.murano.resources.ExistingCinderVolume openstackId: $.volumeConfiguration.volumeID .... An existing Cinder volume can be initialized with its ``openstackId`` and can be attached with its ``volumePath``. These parameters come here from modified ``Forms`` section of the ``ui.yaml`` file: .. code-block:: yaml .... Forms: - appConfiguration: .... - instanceConfiguration: .... - volumeConfiguration: fields: - name: volumeID type: string label: Existing volume ID description: Put in existing volume openstackID required: true - name: volumePath type: string label: Path description: Put in volume path to be mounted required: true Therefore, create a ZIP archive of the built package and upload it to murano. Attach created application to the environment. Enter its openstackId (which can be found in OpenStack dashboard) and path for mounting. For example, you can fill the latter with ``/dev/vdb`` value. After the application is deployed, verify that the volume is attached to the instance in the OpenStack dashboard :guilabel:`Volumes` tab. Alternatively, see the topology of the ``Heat Stack``. Booting from Cinder volume -------------------------- You can create a volume from an existing image. The example below shows how to create a volume from an image and use the volume to boot an instance. **Example** It is possible to create a volume through the Heat template, instead of the OpenStack dashboard. For this, modify the ``ui.yaml`` file: .. code-block:: yaml .... Templates: customJoinNet: .... bootVolumes: - volume: ?: type: io.murano.resources.CinderVolume size: $.instanceConfiguration.volSize sourceImage: $.instanceConfiguration.osImage bootIndex: 0 deviceName: vda deviceType: disk .... Application: .... instance: .... blockDevices: $bootVolumes .... The example above shows that the ``Templates`` section now has a ``bootVolumes`` field, which is stored in the changed ``Application`` section. Pay attention that ``image`` property should be deleted from ``Application`` to avoid defining both image and volume to boot. The ``size`` and ``sourceImage`` properties come in ``Templates`` from the changed ``Forms`` section of the ``ui.yaml`` file: .. code-block:: yaml .... Forms: - appConfiguration: .... - instanceConfiguration: fields: .... - name: volSize type: integer label: Size of volume required: true description: >- Specify volume size which is going to be created from image .... After sending this package to murano you can boot your instance from the volume by chosen image. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/doc/source/admin/appdev-guide/developer_index.rst0000664000175000017500000000063600000000000024544 0ustar00zuulzuul00000000000000.. _developer-guide: Application Developer Guide ~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. toctree:: :maxdepth: 1 step-by-step/step_by_step exec_plan hot_packages murano_pl murano_packages murano_bundles app_migrating app_unit_tests cinder_volume_supporting multi_region examples use_cases app_development_framework app_debugging garbage_collection encrypting_properties ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/doc/source/admin/appdev-guide/encrypting_properties.rst0000664000175000017500000000377300000000000026033 0ustar00zuulzuul00000000000000.. _encrypting-properties: ================================= Managing Sensitive Data in Murano ================================= Overview -------- If you are developing a Murano application that manages sensitive data such as passwords, user data, etc, you may want to ensure this is stored in a secure manner in the Murano backend. Murano offers two `yaql` functions to do this, `encryptData` and `decryptData`. .. note:: Barbican or a similar compatible secret storage backend must be configured to use this feature. Configuring ----------- Murano makes use of Castellan_ to manage encryption using a supported secret storage backend. As of OpenStack Pike, Barbican_ is the only supported backend, and hence is the one tested by the Murano community. To configure Murano to use Barbican, place the following configuration into `murano-engine.conf`:: [key_manager] auth_type = keystone_password auth_url = username = password = user_domain_name = Similarly, place the following configuration into `_50_murano.py` to configure the murano-dashboard end:: KEY_MANAGER = { 'auth_url': '/v3', 'username': '', 'user_domain_name': '', 'password': '', 'project_name': '', 'project_domain_name': '' } .. note:: Horizon config must be valid Python, so the quotes above are important. Example ------- `encryptData(foo)`: Call to encrypt string `foo` in storage. Will return a `uuid` which is used to retrieve the encrypted value. `decryptData(foo_key)`: Call to decrypt and retrieve the value represented by `foo_key` from storage. There is an example application available in the murano repository_. .. _Castellan: https://opendev.org/openstack/castellan .. _Barbican: https://opendev.org/openstack/barbican .. _repository: https://opendev.org/openstack/murano/src/branch/master/contrib/packages/EncryptionDemo ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/doc/source/admin/appdev-guide/examples.rst0000664000175000017500000000527000000000000023205 0ustar00zuulzuul00000000000000.. _examples: ======== Examples ======== .. list-table:: :header-rows: 1 :widths: 30 70 :stub-columns: 0 :class: borderless * - Application name - Description * - | `Zabbix Agent`_ - Zabbix Agent is a simple application. It doesn't deploy a VM by itself, but is installed on a specific VM that may contain any other applications. This VM is tracked by Zabbix and by its configuration. So Murano performs the Zabbix agent configuration based on the user input. The user chooses the way of instance tracking - HTTP or ICMP that may perform some modifications in the application package. It is worth noting that application scripts are written in Python, not in Bash as usual. This application does not work without Zabbix server application since it's a required property, determined in the application definition. * - | `Zabbix Server`_ - Zabbix Server application interacts with Zabbix Agent by calling its setUpAgent method and providing information about itself: IP and hostname of VM on which the server is installed. Server installs MySQL database and requests database name, password and some other parameters from the user. * - | `Docker Crate`_ - This is a good example on how difficult logic may be simplified with the inheritance that is supported by MuranoPL. Definition of this app is simple, but the opportunity it provides is fantastic. Crate is a distributed database, in the Murano Application catalog it looks like a regular application. It may be deployed on Google Kubernetes or regular Docker server. The user picks the desired option while filling in the form since these options are set in the UI definition. The form field has a list of possible options:: ... type: - com.mirantis.docker.kubernetes.KubernetesPod - com.mirantis.docker.DockerStandaloneHost Information about the application itself (docker image and port that is needed to be opened) is contained in the getContainer method. All other actions for the application configuration are located at the DockerStandaloneHost definition and its dependencies. Note that this application doesn't have a filename:Resources folder at all since the installation is made by Docker itself. .. Links: .. _`Zabbix Agent`: https://github.com/openstack/murano-apps/tree/master/ZabbixAgent/package .. _`Zabbix Server`: https://github.com/openstack/murano-apps/tree/master/ZabbixServer/package .. _`Docker Crate`: https://github.com/openstack/murano-apps/tree/master/Docker/Applications/Crate/package ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/doc/source/admin/appdev-guide/exec_plan.rst0000664000175000017500000001544600000000000023333 0ustar00zuulzuul00000000000000.. _exec_plan: ======================= Execution plan template ======================= An execution plan template is a set of metadata that describes the installation process of an application on a virtual machine. It is a minimal executable unit that can be triggered in Murano workflows and is understandable to the Murano agent, which is responsible for receiving, correctness verification and execution of the statements included in the template. The execution plan template is able to trigger any type of script that executes commands and installs application components as the result. Each script included in the execution plan template may consist of a single file or a set of interrelated files. A single script can be reused across several execution plans. This section is devoted to the structure and syntax of an execution plan template. For different configurations of templates, please refer to the :ref:`Examples ` section. Template sections ~~~~~~~~~~~~~~~~~ The table below contains the list of the sections that can be included in the execution plan template with the description of their meaning and the default attributes which are used by the agent if any of the listed parameters is not specified. ================== =================================================== Section name Meaning and default value ================== =================================================== FormatVersion a version of the execution plan template syntax format. Default is ``1.0.0``. **Optional** Name a human-readable name for the execution plan to be used for logging. **Optional** Version a version of the execution plan itself, is used for logging and tracing. Each time the content of the template content changes (main script, attached scripts, properties, etc.), the version value should be incremented. This is in contrast with ``FormatVersion``, which is used to distinguish the execution plan format. The default value is ``0.0.0``. **Optional** Body string that represents the Python statement and is executed by the murano-agent. Scripts defined in the Scripts section are invoked from here. **Required** Parameters a dictionary of the ``String->JsonObject`` type that maps parameter names to their values. **Optional**. Scripts a dictionary that maps script names to their script definitions. **Required** ================== =================================================== .. _format_version: FormatVersion property ~~~~~~~~~~~~~~~~~~~~~~ ``FormatVersion`` is a property that all other depend on. That is why it is very important to specify it correctly. FormatVersion 1.0.0 (default) is still used by Windows murano-agent. Almost all the applications in murano-apps repository work with FormatVersion 2.0.0. New features that are introduced in Kilo, such as Chef or Puppet, and downloadable files require version 2.1.0 or greater. Since FormatVersion 2.2.0 it is possible to enable Berkshelf. It requires Mitaka version of agent. If you omit the ``FormatVersion`` property or put something like ``<2.0.0``, it will lead to the incorrect behaviour. The same happens if, for example, ``FormatVersion=2.1.0``, and a VM has the pre-Kilo agent. Scripts section ~~~~~~~~~~~~~~~ Scripts are the building blocks of execution plan templates. As the name implies those are the scripts for different deployment platforms. Each script may consists of one or more files. Those files are script's program modules, resource files, configs, certificates etc. Scripts may be executed as a whole (like a single piece of code), expose some functions that can be independently called in an execution plan script or both. This depends on deployment platform and executor capabilities. Scripts are specified using ``Scripts`` attribute of execution plan. This attribute maps script name to a structure (document) that describes the script. It has the following properties: **Type** the name of a deployment platform the script is targeted to. The available alternative options for version>=2.1.0 are ``Application``, ``Chef``, ``Puppet``, and for version<2.1.0 is ``Application`` only. String, required. **Version** the minimum version of the deployment platform/executor required by the script. String, optional. **EntryPoint** the name of the script file that is an entry point for this execution plan template. String, required. **Files** the filenames of the additional files required for the script. Thus, if the script specified in the ``EntryPoint`` section imports other scripts, they should be provided in this section. The filenames may include slashes that the agent preserve on VM. If a filename is enclosed in the angle brackets (<...>) it will be base64-encoded. Otherwise, it will be treated as a plain-text that may affect line endings. In Kilo, entries for this property may be not just strings but also dictionaries (for example, ``filename: URL``) to specify downloadable files or git repositories. The default value is ``[]`` that means that no extra files are used. Array, optional. **Options** an optional dictionary of type ``String->JsonObject`` that contains additional options for the script executor. If not provided, an empty dictionary is assumed. Available alternatives are: ``captureStdout``, ``captureStderr``, ``verifyExitcode`` (raise an exception if result is not positive). As Options are executor-dependent, these three alternatives are available for the Application executor, but may have no sense for other types. ``captureStdout``, ``captureStderr`` and ``verifyExitcode`` require boolean values, and have True as their default values. Dictionary, optional. Please make sure the files specified in EntryPoint and Files sections exist. .. needs checking, commenting it for now Files section ~~~~~~~~~~~~~ Files is an execution plan's entry that describes files that are passed as the part of the execution plan template. This is a dictionary that maps file ID to a document describing the file. It has the following attributes: **Name** the filename; may include slashes to specify files located in nested folders. The root directory is the ``Resources/scripts`` directory. **BodyType** is one of the following: * ``Text``: Body attribute contains string content of the file * ``Base64``: Body attribute contains base64 encoded string content of the (binary) file **Body** contains file data or valid file reference ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/doc/source/admin/appdev-guide/faq.rst0000664000175000017500000000677100000000000022145 0ustar00zuulzuul00000000000000.. _faq: === FAQ === **There are too many files in Murano package, why not to use a single Heat Template?** To install a simple Apache service to a new VM, Heat Template is definitely simpler. But the Apache service is useless without its applications running under it. Thus, a new Heat Template is necessary for every application that you want to run with Apache. In Murano, you can compose a result software to install it on a VM on-the-fly: it is possible to select an application that can run under Apache dynamically. Or you can set a VM where Apache is installed as a parameter. This way, the files in the application package allow to compose compound applications with multiple configuration options. For any single combination you need a separate Heat Template. **The Application section is defined in the UI form. Can I remove it?** No. The ``Application`` section is a template for Murano object model which is the instruction that helps you to understand the environment structure that you deploy. While filling the forms that are auto-generated from the UI.yaml file, object model is updated with the values entered by the user. Eventually, the Murano engine receives the resulted object model (.json file) after the environment is sent to the deploy. **The Templates section is defined in the UI form. What's the purpose?** Sometimes, the user needs to create several instances with the same configuration. A template defined by a variable in the ``Templates`` section is multiplied by the value of the number of instances that are set by the user. A YAQL ``repeat`` function is used for this operation. **Some properties have Usage, others do not. What does this affect?** ``Usage`` indicates how a particular property is used. The default value is ``In``, so sometimes it is omitted. The ``Out`` property indicates that it is not set from outside, but is calculated in the class methods and is available for the ``read`` operation from other classes. If you don't want to initialize in the class constructor, and the property has no default value, you specify ``Out`` in the ``Usage``. **Can I use multiple inheritance in my classes?** Yes. You can specify a list of parent classes instead of a single string in the regular YAML notation. The list with one element is also acceptable. **There are FullName and Name properties in the manifest file. What's the difference between them?** ``Name`` is displayed in the web UI catalog, and ``FullName`` is a system name used by the engine to get the class definition and resolve the class interconnections. **How does Murano know which class is the main one?** There is no ``main`` class term in the MuranoPL. Everything depends on a particular object model and an instance class representing the instance. Usually, an entry-point class has exactly the same name as the package FullName, and it uses other classes. **What is the difference between $variable and $.variable in the class definitions?** By default, ``$`` represents a current object (similar to ``self`` in Python or ``this`` in C++/Java/C#), so ``$.variable`` accesses the object field/property. In contrast, ``$variable`` (without a dot) means a local method variable. Note that ``$`` can change its value during execution of some YAQL functions like select, where it means a current value. A more safe form is to use a reserved variable ``$this`` instead of ``$``. ``$this.variable`` always refers to an object-level value in any context. ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1696417899.7171805 murano-16.0.0/doc/source/admin/appdev-guide/figures/0000775000175000017500000000000000000000000022275 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/doc/source/admin/appdev-guide/figures/chef_server.png0000664000175000017500000010147300000000000025304 0ustar00zuulzuul00000000000000PNG  IHDR;js sBITOtEXtSoftwareShutterc IDATx}Xw/7LHM$@I\"EX(` Zhx-z~sYaιw+^һp,*ЊTVRrKHD!?ʣyLf&3 Llzzzlll@ȓ˲dvZPP !dIB(!J,B!XBĩV O^GMA3X!!!4V<)c|ܸ2LԒ<WĖEg5.J,BaZo z\:5DXU Mw }6'_˯?0sw]s#Ͽ߾:uvM't.>WآY+_uHwٿm}'(H]t!}Ʈ\4+~8ϵޫL 5]Kq騑yŞU̍Յp1*mearKjjBUừk-5֫z=%=ފTkW$;Dݙ9e-z#DsVoN-Pv, x3uˆfKzcdկ?Ϛ3*rj`A@Bz _ⶕiL͉K7n9[3K`8;3qNޜ::Jrhmݵ~5$2>UvD>ӷm<F}9uW֜#K<ρs9-z`<o27"U^,X|z N>霖U7eq-?Zl-k{}on\!CY{qIqZevxMw>SvY;,2.WH=Og8k… rAGsoo?\wH3x eeh鐑P퓰H" Nq`Wðe[|Fnf V|vpߖƜc!M']|bn9g0]ܛ_oe3 C6oܺo wSNKGW66Q|vp_6 b}G=ؚZy]W"A&C9sV7Z-̙#ʫKl'gqs%"^-E$[?P}_xBɜ7`U;X+s]#7 7Huš..3ȻL1`F|DcwHfg|7 }<ϱ^᥊zjnaE-`yQ/뛫뭍JmLJ~|nxkt?w[;=He|o~s5=Α QY%gRw0knD.pϟ+ȕ_1̕Q&œ,ooj; ]Jاn![wW\{xBуg ?o"5{jfP\M~4+lMRuH׬pej.<_䍳yD`=,0` {] F0tLyoXug*##0`U+߲ {|?':9=eT`Z(sy#0zױB|,7p#`?hXe|w'zeYF  N0c%Vݒ7%켸w+Zd5lWnڔ7/cG(< }(epl68{ +7%\.cUes+ޜc\+K %ӘX=.ƣ= ;ǰ!"@P 8=uJ{+}_#\6ר5W,GLt\'t.NXwڲޝ5 ݧb&t\ HE([]Xo5j&Z#Fq>BGڋ&:86uv8/7vR:ʼv3n7geW;}\K}o2>^Ɏ] tt,\Gݵj C X҅.r:I]p 0XӅ+t=f #^Ar.;z̘5 ]|*'thqGL' [S]Nq#8^^e+`]^ˍ(8u"10OKe̜9`-sŌs"geɩr'uwލ޶qpa[ӵމ`yN_.YZ᜙^a8$o~S2!Ws<|wͮ=y{VfFUiy[,Y})-{6= so$<~m Ɛ-9[6Kk*8{,ZȹeTWwwe `;F^C;w$.ߋEY',@򂷽4<5>ݸ3kޛ11_/n5H.Ss\bh(+;eeNGrKY1wT/ ?qOX(\QX.=֩9j;K|]zV,k2]k{?:fF NI.xww̥6%^G;5S{ ʚ&bĽT5 9ͥ{v)™0"P)%Y-TW)5&ڕ {,!zZL# yj6DQ`8wV؅+j؏PE5SK{"kSI5 \%oI QȄO덕v=WJ,J X_\A@m¤WB&pEX"B(!PbB!XB%y.>3L%!O. -ǣJ<ekkK@XB%!BE!By8555 B g'egSBDB(!J,B!XB%!BE!B"B(!PbB!XB%!J,B!db&?Ҳ?p?5v?̗PrLB?3k:砫2\wCD\:$B1 t~aLH랲g4܁䗝p}CfXɬΩ]{ߘą².&5YUAA3ߤrkVߍmpgŬYEلd6sM 'ZcY.04 TY3;8቉aN"XZO]hҝҲÊ+ W|in:R1ݚߨ4{WԳK05YS`c -)5\ܾMz U 5 Y'TCY0 :$L.#>06" 3v3t&e,߽;k|- OŬpz%tu$7/0;B2k=@ϩ٧&5\1cwrpNB`hb EB;.9`~T|;;5A^KZ;TiHW{oN֨U)*EuٺmI2!C%My^yy)Z?8W/ ݟ|0Vglɒ qږ}{QE(x3JcHjF,tX@XaAB 1,*R k~0[7*PV) RAa3ȖM'tE%ɢ6*,?R^Ug'ggȄC=02 $8G-tmf2^F' 7CL0U5Æ QbSmn1%B7`6BEjUqiX=1 󎇞./*72I7:¢BFO4,xQQi@ $^Ac/CFAL&\b &<h!J,B!B%!BE!PbB"B(!J,B!XB%!BE!I,O?l1?z؛pBѺfV䦥G~tB!XSg4wNLHN!CAܭ:YXUkN8lR.,k;脕9q>V ~Sع,L` ~w?I(+(T;F^ӽu76t{'?>p1ji#=an3}B5zeAEkV`a j aݺw ~zNM IDATrtTcýzԧrH `n :Y\nPu4h)|^]?5Q?c{ݧٻj\BȄKf$"hעc}˵@dIk:f,ƙ$@tipJRW'usN2-uGX.7h0kT8P`=yi>Xfs{]Sv1#"4~|Ph nC7?We\>w V6h9Wz ѡD%A4[+׽7ڄ2vtG>#^=P5qPC'F`҃gv p `=zAqEIB㫲2X8]QiQ#\ <)~ 9.xәqV@!d"'5"q\VphX`wKC_ D< b!i-;prg{s7!|Z4Z/1ft\gSIVJlY%M#L3cC+4>%Hy Ǔ%7& kՖ8U`.i}HAI##%dtr`JсIUa#/L㌩<_>$>0pyby[*90px$!Xfb 9Vm\YT4Id Ȅ:E˓ =j._]ZO&sJ^jƪ{Lo/~AaaaAn&e]QfNI4Ï>J 딕W%[YlLȘ 1ybX ,Rʄ#  O{yXwD9b͝u% ^ #@ {Āj%MC)%cgAQp @̍:lޟ%N44&*PlO-W6PEdfV!ٱZ7t Uh~Ā:Me` 44 `C]x(WlU>; z,<8W֥jh*SC=>ZQ^郡bhT987Gɘj\i7*0?so)Hk܆DAaPZLKv]? ybkc0=:v&(~PJeg*:CD>|5uUTo*U౲-,kve]0/T8$.!MjliUGI%%% RԪ,]iux4ѵ;7jmK8rMte{P\ BTvpa\QT4Cw#K[YՖb>)/ܟr'f0A;N{:ˎaຮܺ#֮NJ5֝T{{]xȊZ .-U$KKőü F^(\%TY""F^g;)#R 2KچLI.Ygml&d2Yg)uEu, I,nԎ -jz]x*kPi`G\C{DBYRPu%J( K_Gߢ*  4E anc'_\0V oSjM01,/)ibGcWVYEENvXo ,X:yI#3//Peo?=o^Vm(^p,M MQ,=#lvV͕uu~/e4M - 2>y}Դjt^A~n kU,X'dJ2;!qK mJ^ar ?.Hdn(W+Skmc_EEuay<u*mARM(ʉn0;6RRT5[&B5y>̜̈{:f/KWWWV)MءVIr>0Z\2Y>uՕuroA:;es)JˌZRŘH Y˾CXF VPێab 4ab3Jв_sŃDBgs{z%X~6MB2fE'( XjĝC)%!dW7eֱ}HaF^B~j9EMmr\>A21+]Sux}OyE~yA&yA]"tݓwBL/shՁե:pnVڰkn]~A?u#gu\[vӗFΜ w[, Ϳ}gL_7wk˿rX/L3guO9WFiTGB.o ^N8uAcroM7MK= ڞkw6\ ŲW^xJM<;pxs L oinY^wq0i*m[KXD~9PK=,4\m~X~Pk{!xZg*5!d%E#hL{\ÛlzXƝ>y4#pThnfkM;odMl;&S88<; q S/؊|#<[Jy/9 :2B2aGE-]9*=^u|`fopv"dcuQsɫ=r^o _gMm#O4ܺeܺ LOd7|bi[eC-)ZW{^~ eZJE(sM~SgΝ90[gN]mjz::i/L׎/6_^k8.>E]U.oa3MNRQ}F<[튦=_7FRg'~ Wub8u>=wY5߅7Y<xVVp몋Nģt q&~߾}eYGI&q\aLbcc$VZ~_~_S_zojU+.]goDa4jأoRw 0YVi,^SWO8+-T}צ=~#srZۖ/O6y()̦[9͉W:Qd=GL,֋,>d6j)\%]"dvfJ,BQ !C"ݟr$O+mѳeNtJb0XV&5&;Xnu}׸[f;Z?%>bLh!.&`Y G<~cl=}6/efY3pc9|~`Oz8suYxl fo9[BF%޺곗XxWe!i >+yOO@xhl7:D D#I5+ruL:yhf;=R^6,n"xs=K| Z-+4€_מhtoAaBg䩍"WW 31%!Ϛ }gwX;[7o6[Z&t|eL~fz&ģsSƫy]ЫVk1zJ>^*XNE>Fbu :7g} kT_:qx1`O0'y2]W{Xu㡜26z׾ c gEhmY^#3g˽qo&fl,qf-'=X<[qUC(I;3vZ2bIlN"/wLڵoۛpnGeH'J2 6|>P'@3'ekr\#!earsfcakj㹮xKq^aEa1㽧4|bxkvnZcN5nc"ѱb=bp}{7gm9`Qd)m#AAA?}/IS|M} Y`cc2x]G <ыS&e0O"uv"n8#]/0]a 翲|:m^__͛7QE%]fYoxu&Mr,qr߼m0^U}oXFA}]{VߢdupUv4ݬ'u] ptS\1(n-JHօ8q baͧ2Am8+WH5lpig2CsW]@zLBhTEv`Y]kG`n)@daW];i`}uڙ\:Mp^}W߼m @4es $݋p}{ OKA66SuLGFG___}ӥosqR K3Q6m&;9['g'3[rܟ"j2acxK^0]p$[p]jp\X  c(M;W4+xJӡpR>ŗ:vak~+7]K[.CNsf͙.=f"?> 5AMc]{8, ڸĉ+g;h8vIz z{: U`c?gf`E&_4?|xx;2ڙPb KߍkKu]=/ߣ:uYcv`'rM ]n~7=8"'! t&L\,Lm p| -sNww_o 6=6}ur_9$F=f\B~lbqf54kN5\Xdg~rSLXzsV47kM3Z5|yݬҏ{?iNB\1,p] `d1>,v~tOi5AN\൤5a.66ۮ[D̖{Xw?OB2AI*qA -E lŮP]ѳEE.=ҋtkcEm޵PoPQ$*H20 kyLfd^f${@g:my5x<;@!tg;VaTd0\-x’V\dWoNlyi ( puc0u6J&fMsAB$g]gΪn/mzSWK,?sخ]5&!d:u撶!vvvV\lmV+Nf@`oO3 3VNCx#Ÿ,'3Bf$/(kO|pՊ`:M ڷ|:]:l<]g[ݱ{_[;m4WXw*y|a@:Վ_xJ(:[dX@o[q*>Ke* ^qj 5>.JZ_ғ#mf} `)C'[_R 3Vg9>c2ۓJ11Fik<8_ZN1FTtփ% pxY`l#=n.̴Z(B6Yζo5L ˎA=99X:3MƳ?4[,p#hhmX0mҘol;ʓ|o%C&XqT*"R LyTo~_ @]0x´392,ٕ"VZNJ.aؚ " Օu@zNu盁P+q;Lw1SזQẂ10 @zL-/Q== hxRA<+4E"Ч 2J8UQXuEFuq>J)*)•B*/ z.bb!J+0XO '{Fj48yQK~?v N%]&MW++r2yۤodEo|$W\b.;6⍹Ks4t3Np^s@ypN:FDDD^^M j! ̟ ER[Z9s1ɑkj{( f~#1guRrvDDeҤIkfq҃˧M}cSDףR؈ ]To@4/=q٫TRͬ H]}=yLjjaJ̈p='*EOjX,fl6sDzŋ.]4h >Pk+nZ@=IA$  Āۭggg$ wNopխ V P;㰌% ݰ?kr_*1<ׇX=&|o6zПyz$O`, (x@ՙo2n],4J8`n#- vڲ xzO[em9 4-5۬ = zH7< #(Z]<3Ek.]K4'|r&}˚'>&k|b|ҿJ  !+ruA+m[#xj14gWqP0wa9bI` IR=L6;Gd8t+&Pa1#nn(7HR }XD4~̨G[߲CpԙNDӼڮ}!+$1~̨[S78٬oÞ o4~CGﰼdSDhpϡ̍gDh*+N'+[_RaSJyGEL{.b'*t eIeO舉"{a"螷c8蛆XLܚڞիu z|½ oX[$G%[O`b!$9W\nM**Μ?n U6_U9{y eX{hW|E<熮ԗ49KCe=rrUOwX^r8xf <񒃽$(9ka5\jߛcw欟!P|$*Qq^I R&W->U]"J̈mK,'DP ՞((%|b(/&H6(ӽH!bh 0^%*~]pP.N,\K,\߾(4`i,.5a vf*W'ab!TdT5Z@xIΎw\Z;cZB{d_?5ܣy(d: $ ZBHO6rSfg.ORNIڸ*kH^At~NljWMHVj33c#yE&{tjbg2뱒?I W:beԉ5#iƁsKb24Hz; N]~l>3%[7dFl}1f<wMX/"Kupb$=ssP3sҷtY\vc,hoXlYk[k㇙]SW?Vǚ| >ҳM;r;O}eR46n@&4@?o/O< j~|{F+ ]l67/a GSg<.s;Gc0zNiFZAOߪcutgַ]u؍#/xbzF#BXXdn Qokmmk |~;}FG|.͈nt_nttd=`ab!yC-ݱj_]斮 ;7X=IC҇QrI{AAXCg65\^B9QR7`WCa~.l6 Bb,DF]kͥS]ůNomkxy)ζ3N7 ]bv60kb.ڢEA[6~=VsaЖSYB`OuuB5L1dK~7ߧ<=+Jƣ H)8IOCw IDATuuuI+.j@+)Qty(oնEXuY;oDDDDDD]`V׃s=X2 V1u~/y{_TFDD nk=փ#""ވlM@[0꒢eKޙFEw+ԟذlnԩSNkoo!}fk*ﮦ[2.H@(Eݻ.)1(X3M)}}P|xﯠ.௛.7ԡFvnIVhNꁭMɩXUXZZZXL@W01[:{BWRa,$$ֲ=nAg,_&K+23K@W[q7|}KOm^Wdr3w@>ʰ53w 9"&FZ_m%޴ u5* jY|A.(V'Y ?X4Jk =LI19bT*."I FU QcⲬ WR/(%*}xJ dA1܂ mLV$V{&nkV1SDKj(BAC9$9>cNT(C#畗_r~β$*:IL EThi?/:HLYEgŬ|/0_Z`h5Д2va.7LnjKi"S"}xESaauT\‚Ch#P=-.6Mخ({wpJj09.X|Os;tZkꚚ hA:充{/@y7eX@G؂`ߺ5nuV]|J,UYX`8(c#;q}IW/]A q>jc#rlp (t =S$ +B3CSMċڻu g0p>$R$q'\wǘ673׋HrӤ*iF'~C7u7 j k-MhT^>EJ %MqKz_qv7~Rg|y?Bgt vٴzS[ڢ1!4wwvݔ¤E "ά|- NƳjP,Bh?S} Yqi5EW&HED@jᚉ*FW.TRA+n%G5Cw6n ɚR2.R T{KWtB$H< c=Ai=qٖ@(Wj;xx|887!C&Gpk@@NR8m3W;mAF[ gWxxGLrkcj[O]n h׮8Pt]ۍNcb\=E8(" AbUQik] ѕQQ@Z1' l]azUQsR` T 0\Ƹ"UnnN7Eߎkrkb$xb~XF:+\yo41z ,=e{PVx@P_͎0{ztLSjt;:Ǜ.,/=kî&W1s3BO=5D^3RǓ$$N\>;2.5A@*ԥT(e]9q/kuJ0"D^%;,rb'&)9>%{}JQqҭ9)PJ\B/+450fBh9q{D9hS;A>eo?n{%]WGM?j @`æ/?d<ߨqk;yxߌٰ?wkpV b0"nW=݊ߕ,zo.tЮWQZz(<*6[' /tvh:TluVW[S߃lQ>i8S`lpSَ;,o;|G_ NnbيBXz6z52@XWv ͅ7| t( ubVX6pp۽(-[׺)au\`VBAZl6fXxb``#ܥK SC!X,mmmC 2E!lB!Bab!B!0B!L,BX!&B!B !B!0Bab!BΏLnԜ0wO@&YxN07.#}A!۬!/ )!s;yn|VKFOpq=° "=@Z,G X~WY4=gzUJ_P>dc|++UKr_t ޸h,."M[Y,wlL,W"G;`MM&"nU 5wAS57黟ڪ[{c{vԟgJerc7<pBhm3 `nu`AZZr+ā`JG˶oxَ_E=pr~ն?5+ H (BbzݯK P^ b#0Ѓ`s0<-sKG9ӔQ*\QUR_x%і}hfПF Xu`/Uv@B7XAe?x}α&R-B ƲqG ˲<{nzq L5;Yq +d:K}#oXߘ LkXОWP6N0ww 99n܄ŏzݏZ֟ԩٞSy<OX @ nΒǗVYM;/ GfTIVj@0uKZ@4s 3AU<A!@abVKO';աGE/v沉-,_wjo\b` Xvvv|>qQb -[ZyC%d,cf`\!X,.,н&zR~Gx=B!L,B !&B!4` X,:aٌJR۝0BASSFUUU~~~X8FݍְUMCŸBA N8Mq2E99WmZWu_42,)55zrjӇ9s@HƄ,No{f*ɬ]Dmzldꮭ'RRlu۪l;4fUXĢsr6=D#_^ִ)z^uڞϳUӳUɯ%:NlLHOvwд5z^U}e$%FS՛2cX}+#+^mYB765k׾D`K@K9ڱiC*(nSbspns:8-?[o+0r쬌\nޜurّi$m:W5#cZ$ϒHȜ寍FȃȜuPWie Y'YsҘ0?sMz=jڴ1zkFƧU y7+ulJڝB}4yVITK<̛c3VP݌śhE%s} TB&ƽuWfvUaAeS7"u{L`g }y5bɻ ٖFO..ޤ2oϲ7k6b<޺Վכtׂs{Q!3{5k A_}/pq7ge^Y={,s?m`>8"O} mYōUI#EY;rM={Hl[S}O5/3k7mݱifӦuUYBs,@˱Mс=gM2uի~Uﭣg-W=cVL&8zPެ촯w8:9kM{\GVٺu+rv9'kѴ46l֭w{\whrw޳gڰ촯<â%Ŷ5{3YOnh/*[A}2}u#U*++ɁN]YYmHO,MHFd W@N?| H='S 0tLbVaC\{UjwB9-m`GX' @JJ8%G c{Yղ;;Z3\95=>CI$5ݱ/E{=&:ߓ੯^bnnꛔNi$ȱ[t IWWwƽX âe{h-sEB``=XA7o5*UPJo>թ>6Sa88SW4 *o.ڪJ'9 K`ӆu5ʹBBhnڪK¦{CմXy름E-k -$3usB6I4 j=ղayzX]k0THW~5R}Drdt WF7~k`Gs+4ӗFP[<^Nz|$EzJ5HޔKViȊ!DWsc[8AID7" ^9S@I(,CWсUĬ'X<] 4ƾr>\o o)~~܂Ǝ^샍u~)~Ra:oR@0nEa\= %,=.znH&K@KtOznڞua#u%gtl՛r%ID4/[e~wYr˚[eȑсLs#={`!<-1!c=i+*宝%=¼ϱcZ7J&@7q=k$%!ƛ>5yi9wdܹմQ'h>C:h=ssz#zmNV( [=3(5+ 哼6|wU ڤ*-S:5y~]?Q9c:7:huygvw'Z ӘÇr$z5yfKXr4uҲMGihMZ 9ӻ8#Fϒ2O tx1-Ms@G9ˏ;iOVڦ{Ǻmdq?|a I_L_M|"y8gYvc`o\r]@YOMޚX7sk>㑺i>]ʚ|&mzHHDX'IYy$qYraoJ$/+ynkYՄ_h} }o?mOh?Xؐ]N-ؚ4y9~Y[B\s␦9{ʈ^k%Y Ιr|grLy Ls83ϛ_ƽb6zMAd ~wbӄ99Mcl. C"aoU I5MBBB/KTL}"ŸK^gg'{ھ.Ǝprrh᣿ooGjZ,l69cYŋhs.]4hУY?4-ջ7o\MPr7O~{q?si2WH[6Κw=yӟp u9sVkvO+[/׼UUU=vb 2Ez;_S"@!L: :B!B !B!0Bab!BX!0B!L,B7c)oX|S^`jXfl8eً/> B, zfPxXLAίSXA`b!ɩjbQ TZNNNXuxba, * ÈvQabhƢ@鉫8::uIF/H #8zbL&!%0Ћ[rttloogYƍlj B }:&zqݵ!tB!L,B !&B!B=S0hBOIENDB`././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/doc/source/admin/appdev-guide/figures/chef_server_form.png0000664000175000017500000014707600000000000026340 0ustar00zuulzuul00000000000000PNG  IHDRL-9sBITOtEXtSoftwareShutterc IDATxwXW3^(,PQDXibIbIQQ{(XPQPA)" {/|\w آ=y׽ޙr2Tpp0ׯ&B7I,>zH$uޝzQ߾}q B-KKK+,,X}BDXX"!TTT`,BX,X!B$wB!0A!"!B BcB!EB!B!"!X!BcBa,B򪪪LPuhB!!OJJJ"!0$;" D@,?}r_ƱMUpII ԁRP !7Ԭ!&ሾ~ii2Uh搟ť+.<#&lY [W8jB!'Ojkk_a׮];[+шC/̍~*>}VGr7[@Q^~&M>>>vvv۶m}CNNNvvvoz)))vvv3fxv/VSS:tx4"_000x@^FRt:j|ٚxC~?m!1ޭJg+/++tRTTTaaaCCj='Nɟ*--]~=0֭{}Ϛ5 ƍn:<Px욚ccc{{ɓ'wͭw߾}aaajjj,Y\\|@`hh8`)SAPr4mnnc@ [mbb!KѴʿceƚOJ$oyxA<={}8S5;wn׮]^]]M>Mvmmm:DJXH$4M{w֭Νp8 nRV~9C   ---H$,r׾+VHKb1EQ+**@IIIGGG DMӋ-e"UUUׯ_700xU7vvv,?477KUU!##UVM2/*ȎyKyz?Om G[<IaIdWkAcG utɝ;w̙C~P,gϞȑ#VVV}VWW@޽Qԛi"(00PMM/88xܸqos\dw JJJZhH$8pEHCٳGIIMD<}i5k477sQPP ?֭[jhhYP*++["+˅#/s_"aHKD7sn&_e5ɦW}pR!++k޽_]V٦(ȑ#"_}arüpmmaFM=]۪իWD#F8pVE377߼yٳ`UUUo(lD"/::Z O>"rʁ2 !ދ!*HNNKL\RH8"Uk~cCTUʳl@(hxTViZLh`sIXlooGڵk0_566jhh 0`̙7.[,22LJC=|dʔ)jVV7SN:goo˗ K?v5x`/Q777%%Hzѱc***޽yyy'NPSS:th|%$$.'$$9rرcYYY kΜ9#GMΎFGG)(( 8/x899DiaΟ?e---ϟ/ ((ҥK?nhhPVVӧ#_۷o@jj*iIKK7oř3gd['N/))idȑ}42{o߾%%%|>ٲe흤1?pvv~ч ,))Zvm_~Y^^>h 3h{c233ݻ_|.r;w6 VX1}N[nUVV(..))) ZZZѣٺsŋ999-ڳgO.]nܸ!+WlذKKK]%%%-Yutt,++w^LL̢Eϟabyytt9ӧ?Sx<\rݺu6.+o榯?ܼ7|In93~W^ǎ۷o4h~YYYbbbBBI8l0e<<<Ƚ?RSSSWWWHCݹs$6"{^ |E_xS7< ;EF\ƍDDl޼NKKKNN^dɓ'A Æ 366>}40d6lՕ\p v-Bvcuuʕ+IY==G]^"_\o} A@xٳ@.]t^e%Uw0gܬEYzsZz +=g5Ͽj-:XaVVϷkll[|СC333|>dժU|>)..N||>?+ٺu+yy5>QVVFJ=<< ߿O ?S>]MC  39sFZbgg'1_l*}/ϟ0a0-]m}ܚIɐ!C|{eet-[oFZB&/|~]]yy)>`7ѣّ4ISS!::ZZgxx8>|xKK )sΕ;dį[eYV(oD=fgg|/R$BH4i$>"ҥKpWWW>sgԨQ|>ё>Ȏ do477{zz8q333 ,-z*3fP($%ϟ'*=ȱg jjj:^'B/[0LVVV||)/;cv>#x6Lf<]ajeM ʪʯ,FuB2AGGGEc/^ӦMիpȐ!NNNͷn"%a~Ȑ! HAvvvG̙3y%KDۘttuuI"44F9sȭ 4cƌGo>9rd׮]MJJ'''r螚*[طo_ٗѧdpGdddAd G*]w#"!!u3&&&_ۃrssMLL  {#w|' Yd_jiib6)** }Zr H^UUeر0(,,'kkk+++ee円9ԩSLukӿ ro#%Ԯ,ߖˁ:rk5tp.Mlg.PxhUeee 7bdLWѡoN.bD9gZ,5Otp]0nj'CfXOOOxr$NݼysϞ=/:yBZ%UVMN䰰0P.;̫׏O" <-[sEDyXa?;]?%'>^z VZ^YP\*yq3g*((\׍7lj?SSS:$%% B%1bYKKˏ?f{&$$Ny#56mxa`ffFhj2jLÇ߿ߑ E"##cccB+iԓhR?B?܉_Xu7_9~ 54f={(#I( #?h񣁂}7c'u112hgJnݺ-]%KS1Vm455MHH ߩܙ#e:_&ɑ_lSK+K*NؐJJ#~`dI)iťe}z07߅ӧOp8GTTTTTVMM ٫W;vH[ZZ.Zh޽sε' x}466;::97H!DVAAARR4c4k֬իW_-׳k̙CLJJz䉚… ;߼cbbbbb( 7ofԩϟ}veee^rrr~駜{,[TIIb̙:::_uu\2##ƍ  ߯;wngWUU3X[[ٳ~otuuh6mD&/鸗;^d%%%~~~]TUUkkkKJJHORooiӦI|֭[.\hggghhX^^/?s/~Q===㯥~7uww8UA2{85CWue( mtUW{ucv|-SL:tŋJJJ&N8rHq$gϞMMMgÇwhjjW_M2e nݺ޽{߾}ݻw_xqN<ّ 񉉉 rݻw>}qqq%%%<=ܹs׮]"VVV;rH``X,OZW2sL##+226lɒ%oqr;v8{ɔ0VVV3g$|055ݱcSRRzk.ćFEE-]TMMm͚5vmvҥ˟~M.ۣGyM{͛73* :2d!zyPWU5ZY>l G`|('M4̴B6nB!Ѹ B!B!0A!X!B Ba,B!EB!0A!"!B BcB!EB!B!"!X!BcBa,B! ܢ" !P,^@!?hB!B!0A!X!B Ba,B!EB!0A!"!B BcB!EB!B!"1+1B!E6@(b]}9EJ B2e^V?(?z8nA!ymx jHC5 B!E:)r*Rq /[ U]TTtً/GFF[XXp6[reaaPY!t IDAT*=zh^tuu ##_wKlaUUw}ح[.]@ii?0L޽nT1PUJ<βQf]յTx "zSwݻw,;j(MMԨ(H4k֬RffC/^YZZr8x#tPhh;k!Bc7J |~>}a??766=wQ2iҤY'OZ"o߮ן:ujϞ=<ѣkג<ʦM6ndkڵkaaaDDlyii_`kkEӧccc͛wʕWWݻ={͍|ؼ}zUUU@ѣG'Oҥ˷~~HPYYYVVfcc#@߾}u([㓛gمɝL2EQQ1000,,lԩ<߿~Ϟ=uuuK,pB^^g!ێEx<ޒ%KlmmϜ9/lذ!==EH򠶶vɿ򋆆yGeaa!jkk[|uX\WWWSS#-LLLWWWcccWWW@jmmJo]!˲'Nljj -5k֊+zE6ҿ:;;[YYY[[,deeecc0LEEÇLMM?Cbcc8C!^ ן;w.˲)))>>>7ntppOHHHLL(v̙-)X,n]avy+VIiٳ͘RYYp"##AZZ&I̴fnn>pwӇP]__OJ$ty--- fI E-ɖܺu4x!Bo;HLLٳaffǏ555O>mڴ{YYYYXXok0`ԩSϞ={immm7o4QUUdff:;;SSRRn޼I^>y$ fمG?8WW>(((qBțjQTT=z(z ]]]MMͫW~w>p8$ܬ5a„~IvԩS ûufoo?k֬۷o۷O[[Ȉ,peccciɋ644@߾}mmmSRR?>i$ {ݽ{oJEEe…/_>|m;3 BB(`(a翯U{ !n\$ h )r:KBt," DeYeI_q#ϣ X( gp!6!} ÂaeY%qF"CE44RyjXM`8^UUuuWa]v7447nx$ "F0 ˰ fX b0 a ?z,MQ@S@C.Mp& &VΆ޺u+%% tҿOOO#ބ۷o:u>ظ%Kó9~8rH fSJ2" V$a% 3,Òp\09лXCSg!P<X."N uV\Ir)(((((pss[nyk ,Y,??…%%%\.gϞ"ɓ'G޸q##3ȳKŲ,ÀXŠEV F hq 6F*@$RSS3CCïŅ555'O H$[n}~~~!b7otvv#>Ze aA$aմ>:\k*w4B︒IJ#ф~j*4hb)z]F~SS#GJ˵|7!!!V/HᴳLYYYrr2Mׯ'~vrJJJ9*D"a{S7]?BV^<"  ÊV@Rqsj4&Bʬ{KS 4MNvăl "`.۫W/r޽{رLaz_,ihh8uԝ;w x<^nƎ;uTy;wNk`ȑ[li3sܹscƥN8Qfii=tP'Oܹsŋy朜~m͚5B˦r{KII)(($^;v/%!?XO))**4{d/j 8k9²__|Ѻ0""b}f͚\\\bbb"H>qqqrUVVVVV-IJJZd@ ׯcYYٽ{bbb-Z4|iNԹrJ555[[[CΞ=[;wo3B,#'B"*Gᙀ{ 9Y `bb73"0~x(((VK3~rWmmm\xZZZN>=sL77+V={VFRisaYҥKzѢE:d[jhh899bWbװg2,H{L°$͙455\GG}}}xޱ߿ϲbWSS{I]]]V{{zxxԄl۶O?MJJ.@ H$([hmm!}8l0e¤fȑ$zz/EӴBNjI***7ɾTPPXL^4ɺu|{aaKܤJHH/JW'f^B===UNtPNN΋2i%yU@hh(4o!BN__رm۶O^ ""㍍fffƆԒradGtĩS:tȑ6WZZ d IcJppܓCCC߮>ԌNLL411Gcky;w޽{ŊbxSWWݝa[t D8f͚6,X7F6QQQ˖-cYvȑaÌȼ%u/_.s.;bĈǏ3 #7׏;!9rdŊ)))[nh.--eY.^xܹoMOO ,˦ZSU㏛7o355%%%$r4hК5k<oӦM/޳g͛7___8}h^Wa,Bޱcǂoݺѣ*GD>>>!!! Θ1c޼yvŋ tuuF";؀|}}}||bbb\n=<,]Kk(^FWT'֐Z&hĥKS4MN0lEH$ !ԁ@ہ$ +a@$a[EV bazkzr,ytU8<(CqgX2KFk뼓^ۍl1i* ō~|:##3ŕV45$c^FMO6oefzPZܞ)>Y0B{gOK Z%>U\{Pv}0B5⾞iaX`(`i)0eaa)`XT$w *MMS 4šC~.ށ_M["X5/?I zuQâuW?nPW/T}_|^5#+=z;lz`X.)YXaI7 "FD4E,"GQTǧ0<WB:5k- T~f|zmNE"`L}O}뮙[@*Dr [lo/GYHÂ5~ 9ufJ?i_r8"" {*Êh5SWq~nif[ʭԊ AD?9QPX#UUda'shj.{ʦ3Ū =l4 QaWmnfL#a,2I ,`zc(}ֈCfF#Cȳvz>;M?;C?N=?VbnGcd,{>r8A$aygYwUEjXwUm,#r8T9{5/h(qS+ohJCS6#r,|\&ͧNW~YfJZeFVfΠ}!yx[J +EfuU9мmOuQTY}s~Uݶ>thoŮfwc_od^=м~aȥt[$ 'Dp)`X)aidv)}2♆; %}E4M=kSe;:H@$wCdܝ3S?˟QEP$SzuQmj3ד0TK1Lc׌~ sI6q67c_HMA~QW*.5`ޙjZ~YtVE{5\zv Tx3GeTK y=R{j%?)tQ1TE>STXpS5Eι{yUBIV]xfxexDp-E(Գ"k %g+D@_] YS "?f|3 (m LRv)?fZ/-:Sģg;Kj+9" fY->$kY] PRHݬYNuC,ymOs0CaySss[$$+~?_U@"ǡ90, ` @Qm3cwE#]<=ǽ;=U;C-Ǖ,T=!`Z7˓rĵ2VkvQEQ 56OCR#G# {~mG5U,h(T5ϱ.F24zCCW56KF{aYE.0_gzXAJ.ėI|;sDtp/h(ϞtI4yk=ɝK=w Z&qxFWH *pc$8c/&x)C$aiH**r4\RuJ{ws}3WEVYвNrȳqh`h ׮f;kiQ* /-* I톍 VT5.'|5A;n~H: !̋ BCGqR2+DHY(?i,8t@J阢 E]a!Wm_Hު Y*eA9:EG`χodwf1\~^qO}]LI7-fTV$[RFt[r:hkRX!~nwc̷|ӧ~޽{Kr8۷wVUU~zϬ_~qww;vZaoV4=w̶iٷ B:pBdd9;6sLNՓ2quvv{tM<~~~x& /or( !"/V^^j*eeepss;zhmmtR__rKKYf)**I$;;ɓ'gdd8pBCCSRR֮]+" mFjjEEѣ]4gh,YbnnR[[NQӧccc\2yd{{{vZXX"[_=zH,{ڴiqqqN8qbRRR~~ RkkYfq8J IDAT{uE]Ӧ !T"ɓ'I K,qww.2dh8|pRRٳg̘ڽ{wqtt\`AW='@]]ݱc8ҥKuttΜ9SZZÇljjyXXإK?S337nDEE """<==޷onݺ666ʖō;;%%Ҫ]]]wzww=z$&&E!y!@]]eFakkkllcǎʼ%RSS***<FZ6|w,**qFEE˲%%%iF-H^n B‚QFL֭[w>w\ EQo&o:ujWdnn"!!!&L1bDPP?)'Hjjjjj) ***ZZZD"{Ŏ?>%%͛X,?544֭[Fx=BTmmmWW-[\xʕ+[ljӧO }||"""^eH:D dee%$$@IIIssYlllii/!C$322Ο?OJ:"]]]|iE,⪪+Wp8@P^^=Bwc1cztttTT[̙ceeu'ObWÇݹsիgڵkhhhnn.5444-2l0//O6ScIc(**?^,߿_IIiҤIb7A...͓hAAAoy3JJJL2z貲ߘ1cd;uӧ윜:sv_nݾ}Y 555==67ڵkm2hӕ+W>#77777?822^uر c;;fi vvv .tpIVQQaggG>Bo===/io߾7ۓ'O|}}ΝKᑓ [[KK˵k^|}}}cclD"}aÆ˗/O?ݻWի#""\]]ȧ~C~~>˲sٲeKSS˲eee ,X~=˲|>_(J+A^ZZvpaV^^. ۵tM6H'*$B͛E"Q޽'uijj*))-_ㅅ:::۷oWRRD{ ?\ſ[^^M|>ٲe)))۶m[,3g/ɓ{yy!]]]{1rL B2 p&I-))&222.../_&3gO555w @p…*<.**jٲe!eeaÆ᪐ &&&[WSSDijjZ[[wVVVDe:[3f@QTmm횚^ cii߼y9^. BHGG'99N>|xڴi^^^.]oYx666,KrXhQ/bAA&~Xaƌ$)''EJJJNNȑ#oVRREikkO4ի EYY9--N۷OZZZCCɓ'\.ky͛W\\_ܹsDnMMM֏?h_\@@Ν;;񫮮$,KWWW3 H^H$D"5~XBHFF^=!%%_]x؇HRRRǎⴵBeɒ%xPgC[ZZ_>~xٳg D"QOOè8!!D"8עr/[__yeee&yKA 3m\=F+?W z(XW^jmmݾ};J"}ժUɒN "a> GGQQQrH&̉b$pvȑ'O S(`ss<-gעSzTRN\=Z?:##СCx8B( 2,,D"=zTKwK.]o.:;pWbccedd>G]]]MMm/_?~ |O 8pСׯ_vڬY>mذa 3 ,_fْ~jMM͙3gvsC_ #,/ n8ܔsppvZIIqqq1n@ <<>"--\n/j}}YիW***P__/ 555uttN:%ρnܸ!TXXf͚ѣG鹸455ܹs޽;wZH΂/J/w`wh_M```zz:JL"%3&6%%٥nooOKK)ȴ .= \R[[;00P (**%$$ D"Q\\drEGGRc\.W ੜ ^x( 9233NjjUWWtԶ%0od.\~~PPкu""">ɂ ZZZ͛4_]fϞ0a~Z[ޛ7ovssSQQYv˗/?_rDbXX… wܹym۶ڵ ?vaddaԩSw$i(mm{{{h'Y&22Rc{Xo=yd׮]QQQ[dd$DZz5^\b˗/_z5ϗZhlHOӦM f͚Ǐ׋D_u?#>>QTsss=]}ijjٳL&r83gJKK[[[H}EFFH$2 oooXL&.]K"$&C аZpassH$4hP|z[GB<P(trrϯ IHHLgx,fQTMKKkǎPsxW^}khkkkkkׇp#"L&khhޟ=| b@ #ZZZP!/sѰKKK0$DRTTX%}@]NND  شo~Xb7d2 <7v@455/]B݃XGrjkk fϞqK.UUU >|۶m| //#~#GTVVWUU kkV[[[SSp6mdjjJJJJJJÇ߼yJ]]]DDDYYϷ'HpQ]P(<5stttrrrAAAaa{nݺu̙e˖]z`̞={ĉGAرIII/_ˮR(+W;vl֬YD8qѣYYY4 /u~3gxU[&L@-_|TUUv[[[]]B(11B50^HOO EPZZZB\.W  2!TPP .SV666yyy޽Dъfff !H uE [[B''' "//}vӧg̘1|ooo6q5kDFFvرc.]O?DCnٲ!m۶]v9;;#6lusQF},N `M"ܷod A˩LMMUWWONN^~ɓ'MLLX,dr'!X,͛L2y;v۷o/_!4k֬O4e;wnwoܸ1o޼ &̚5hqUnDyH$JNNvpp077wpp8rH$B2t++/vа<%%Kz[[/bkk;yuO}kaavroooKKˍ7 nk!TXX`0vX!dooSccc}}}%577D%&&fdd,X@WW!1dȐ3ٴid 99YRFF&..FihhXZZxbڴizzznDϞ=cX!--L&o޼Jggg? gッ.bڵkÇwn3gI$RXXX+VVV'Or<077w}mO5`dd7X>2BKC@ &&3f ޿WRR~$TѣGڦ7NXVVvСwމb>ϋihh Bx9^ t`ĬEḤkȑ۷o·t|`pppO4zh[III/_8qDtt4Nyׇ]455+++\ 000dE$ͽjժׯ=zx7oVUUTVV4iR?߫#0^ 444xzzٳ~655UUUѣ 6>})Ckk+B]t !붶?۷3g; ;vܻwoǎٳgD?K>|>ܿ?Lnllh>=@ii… O< X>ʕ+l٢rܹsΩEDDrHXXΝ;nݪeBO5o}X,,QPP;w*(''U|>yyyPSSSDHD$_!S _x}R/633377ǫVJTVVFq8R]]:"pԩաmݺ BCp```͘1_~D"QMMNKax! M:Cac6eʔ숈#GSt7o޽{'--}}}}WWOǮJˏ1ɓ'999t:ŋ$c) Bݻ#F+bz~~;wH$ _' ,HKKKIIAL23##ʕ+<+Va?~WΟ?/''gjj wb1<-ZWŋ?׭[`0vrpp/ݕ+W:Ϩe9rf߾}pן={틝gժU8<$?&&fСJJJ_n/_aÆ^FTOzXak֬پ}Z;w|CC_~EEE%$$DIIJ_UUU***QQQT*yyy`ӦM][nEGGŋ;99EGG}vϞ=x@+VbYYիW19s,Yĉx|ȻwV\bŊiӦ}x`lذAWWFĤ444TTTD"==-[EDD|kkks_r޽ʕ+^ھ _3;;y!Bӷmfjj|1|1'Oܽ{wPPNzX,>rȢE|>_|ݻbqXXجYZZZD"y\P(;wnVVX,>pOKKX,d+vX3g[ZZ޼yS,;¢U,XXXyݻ666b֭[&Mp8uuu"s|>ԴB,w{`睃:sssںu+N}m- BaQ|_'Ǐ߿L&cVTT~qcRT"{Pvvh|>H3L0!djjJ&x kkk1PQQy!dwr`/444z7oB@xsXXXzz:BHQQqx,!!!$233{*'oʪ,##7@|Qt:}ҥQQQU?nݺf`4? SNrʬY|||Ou֘==ӧ믒20 ??UV B(yy&"8mڴe˖-Y;pB%%qƝ;w!K"*m۶k.gggц z\Tڿ}.*:iҤ˗/#ox<fQT&%%b+YYYYYYiiihqmmmmmm~Xd| 2 Cb@PWWG&B_/f/--  HUUU(A H^$A49!P"Xb@,x\.wǎÇ;w.B5))۷$ɉ@ ?H(4CVV!t޽+Wϙ3G[[+VE RRRmTZZz]P~~/m&##b2iiiǏ?zH$ @, >}c<vvv$INNnҤIBkkk999dggHFE Ə/ ߼yXΞ=)bd_jjjVWW#jkkt:NTUU,sbHN8QCCCAP$/) Cxʪs4ϗx8]gb>/--.|hBCCBB ***!&ٯ_?N8p BD"tIX,1c\WYbgEEB򲱱A%&&2]LMP===g͚씔__m6Ypd?;z練M>!dffv5###*zA" FTTԄ +++;h |vڕJ]\\drvvv``֭[>)Bk~ٳ[[[ϟ?F&Oǜ/^ HOܒ)))sss?~<ڳgP(444tqqAijjΙ3'11YKK맟~EfUVV'''+(($,KOyyyhhheeyhh,ڷoߝ;w}@@{V^JJJqFzzzM:9rի'O}A-YƦٹd2 <_Io߮l2ww |d[VVv]v NNNx255555k !doorlll}N577D%&&fdd,X@WW!z& ƍpQF$00033iƍXyyyаadee(:O3 xFq,:ܓM6Ig̘Kd:"<b`ĬEḤkȑ۷ot-[lD"Q[[[OO!cƌ̙3'NXl+W$fffڵK("\\\"##qC\\ӳ#d2B޽{>{|~vv+uD(;wgP(<O$m4@,&$$"rssmmmt:BBEEÇxӧ^t:~ c_ BHWWW L2oIv?>@PSS0aƒjkkKJJD˗BFZZ&dXdmmӓ죤T__y󲲲RSSutt>,BH$"B$ w`TWWK,BPII铊X𭈈蒨 ypv"$iƍ+V HIZZZ>l67p8eee Pbb$c_!7o޼sN]]L֣FB>}ہh>o޼Zn]ݻfb񗬾{ 8pڵQ\\Ç݋o&((hŚׯ_ojj255h B=6l̙3<ESSݻwL&.KOO_lù}vPPYBBʕ+E"щ'ߗgϞrǏp8ǎ{S^^KKK΁B(;;;33S]]}Μ9NNNl6ѣ߿XQQ6jժٳg?~<**J^^>4{tO7n+Wĭ,b%  ECCcSN#UUՙ3g~ѱMMMǎ#Hk֬QQQINN`ٵ...MMM'OdXgp8nw*E>M[[BHAA}'Lb2Æ 311ѹ{&BHYYI&QK.}affe&{U__`$v%""?`vvviiZ JųPTT*mNȧ100 SN޽[WWGLt>F?P(x222Æ ٷo2zG+ꪨp8~^SJ^NMz/`nP@,E "bE@|Yx<f% YT*FIIIAm5<{ꕬ4T4߸6}}ӧO EOO!8dȐֶvǏw?..… b`œ&M9lذx ^ZPP@&fWbdee544d2S|dddX,;Ѯ]RSSt L ܺu'SRRRXXvĉ.\|񀀀ӧO~*}م "l6D" I@">_,RYYZpa@@ŒS^^ZYYinn*++bw@`oop޽իW#nܸ.9!???CCCЈ#L<%2y}$$$#._iӦe˖OYYYǏ݀MLLjjjkkkLfZZZOJPO2EFF[7n܈TWWA3L;w7mի8yy[;!aBK.-**VVV{|>W.''ӿ"QNN***B@!+ɓC Aƺu벳CCClْ4rHk׮Χ֭;|pAA޴iԺ +W:88h|U^^Bh[l8p`||qo˵k״|R>bŊG8 "G۷oBd2۷׿X,VHHȻwjkkBݖ!gll ]?bA ߝg܌R\ IDATUUeOOO777QVVݻO2 KKK[[[cc㌌ yyy碡epܹscccW\iccm>|||mmmkjj^xo߾=jԨ_rc```FIqƩS~A,,,ƎPSS#I:tX,~~cŊNNN hll\f̙3qSq ܽ4uuuA:::!x[ZZœ9s̙R\\cffK>gΜyf}}}kk3B ѣB 0k׮8pD$8qg%pBHNNB#999Ce; @,`ĬEḤkȑ۷omN Çs[ZZ*((,^ݻw#P9rd~n޼YWWG&q.Î;$`|>Bs;99ᔤ˗/{xx8q"::Um?rt <==oyzzVWWt*xQiiiyy N>+2ԩSqqq1c1`ggWRRmiiBO<ضw~~~ekk+B]t !붶Svܹp–"'+WܲeʹsΝ;!aaa;wܺuO/;{yy}%pCm۶zjŊsUUU<2w޼yƉ 544LJJ*))h>=%%%Gk5 v}=꥕qDü<&)544,---,,_~d2E")SG+..e2N[[[O>---=k֬ >\QQ1wQFܸq 1E+W=H$>{,%%ϼ3f @8|"QpRz~ fϞmbb"KJJy<޸q>#<<,,LAAٳgϟ_|X,.,,/Õ }K9sΝ[d֭[NzQPXQQѥPOOo?~1,,,33!tW^={VZZz͚5#G>:>>ӧ8 H111֛7oKOO%Ֆ-[͇k!TTT4b*SM>z@***~$y +HKK:ᔗ))) <4BhСxA---6"H&&&۱cGHHHVVP(ġ%yɯ_r`oN8@ hjjhjj1B]]!4j("ի6YYYå}𡥥%pIC444h4ZVV֦Mvгgpے1;ѣq Fqq$133&LSh囚D"Qk%SKR8Fٿ?NDxBÿ#,"Q$ImlllllD"QYYٙ3g\kSS8<;u/EEq//^|'xfNuttCL$rϟx"Njjj2*^IѣGNP(\.1$$#FչÇ'%%1LPadd&a>i+D*,,ģ./_6772C777|YYY<<"00P] &j`` -,,pdv [h$9wLgeơBiر]z4x<J}$ӧ666ݖVYY^F`MMMRƎ[\\\^^l6'dbbR\\MMMD'$|>*z='k׎;vر `_~b1 Ɋ}CJJJJKK@e_ @`08ithXYh7oVPP /Y,FFF_\iiiӧO}jjjgϞ߭[¤$" iVaP/-,,(994OYtӧO=<<ݻ^3&&&fРACȘ>}:۷aƏߪU+77ŋ?~U4ӳC#FPUU]~_5lذѣGkjj5 HDD-[=zϘ1cnݺ5cƌ>}c/K~}ø   N"t,P,He?eP<<&;+J_Vanc_;|5֊5"iNqmO3-UqNY>D,"Q{yU8' hd2٥K^ZXXr \\\:wLDLJJ jѢ;멫۶mۓ'O ̙#/߷o_JJʴi_cvZDD¢EՉʕ+?Gjn^D*[yq/ mc%'smeDI}fѣ}ii}ڵٳg>tttDB0&&5PSKI=` GSRR""WW;v)((طo_aaavƍX^^}=Dbcc[Qbb;w/^f[%%%R͛~~~Oh޼ĉ555(66ʕ+͛7411!۷o>|Ʀۡ|>?55566V 1v[hҳgO##111@]]DtRtI˸8>?fsss"jJƻ0.nDԵkW&''!!e˖W^%m۶=:)))11Ã&Nֶ8ݺu˗6lhӦMϞ=?~y"JOO駟wADvd>>>jjjLm{ן3gE"ћ:;;xG/,//9Ό3_PPT[RRRPPQ^^w^@YZZz"jb"MR]]MDjjj0tP"+**zY]ԬY3"l|r+ihh899ۗ}!"aÆnظ޾SN`y&аO>LLyZ߾}䅊s̙2eQd2ً/oD!CUWWwad=?>>ofccs& ѣDŽ ؤ~v ={ 666õiǺF /Jl}}}b|BCC>l``r₂/^|,i1c̞=z5556m_Y̌bWEEzjU---#F|DEEeggϞ=x˖-YYYDtL>~O$M<oȐ!wNIIaJn޼ɭMo땓sa==3㦾"zْ%Krrr,Y,֯_UX<`ӧQDDŋl+??Æ H$Ν322Zb_uŶmۮZsݹsgTTTIII۶mΝ۱cG"_jUeeKKKUUUO>s\##_~vcbbFծ]/^ܼysNN?ݹ\Ql٢`pUUŋwޝ)Sn޼| }k4ZZZ}XjUTTԱcVZU\\jjjfffO<-,, t҇3JQVVv"z9NJJ}SYk׮Dtڵ/ܹtؘSPuuGnܸA{g߫Qs-r ۶mwÇ[XX$$$0!cΜ9qqqK,Yhё#G""":u KDqqq=>>>>-ZHLL|;{^| :::3vX///fѣGw޵qtttuu 7SٽY>jԨQF1zzz6l`}}}_~c2l6[maaQolXYYYXX0Kyݺu֭[C֖SѺu=:((X`pܹ3gʷo\P(,,_(766^  `sœ@6sZYYYXXB!3K,30//{=wԩDLm(N=ztƍcƌ޽;999FDLTTTx<3bFEJuuu-p8 //K.xO|<666l6~~~L.)++7n\N/_L2_~gշn 6mZrr2w><0Ɋ+!47iӦ-\ɓLIDDٳggΜ9f̘ݻwMaz}TTT4iΟyݻ{yy=?xرaaa6l;v˗/RocccsgϞ͟??L8y$3=33Ɔ9r[ngQLLLQQSn_ӿKK{1_27ʕ޼y̙3Dckѕ+W2@ϝ;7,,,$$dرuc6mڢE!} IDAT=zQ]]UV >CBBBW\xbCC(;chhpK.YFSSsҥ/dK,)++bFktt~)99yȑo}82bտ\miio߾}ɒ%MyPC>~8##C~JVVVѣ_|~0Ouux<^hhhnnÇ/^E^?~XUU~̙x|8,ߩ8wU( ._>ǚ/" qpuuuզ#-dܹ;w|ⅶڵk|H$ڴiSbbX,633[`kվ|244l6k׮3gμsNXXXyy-[q$666::zǎ111gΜ111-:{lvvvEEEhh[*H֭[w]]]__ŋ?~Ν;k֬)++#"___ww/ϯsƍl6*))-lll(,,L"̛7/==}Օi̙\.FhҤI? ]f ͛עE'N*((ݻW~la„7r\.۽{ϟŲUQQaXݻwl6ؘ奢BDGNHHD/F7~wblْobbblllbbl6Ք''' 3f$ WWW"jٲSlllCM~BBBFF hzD0b>ODiii,KSSGs[,wފD&tܹAdddl%%%gϾv Gr6_|y朜.+QVVfl63|ƍ[neʕ /iii1_jhh7omT"4ryr@0x`KP-ڻwoHHHAAOF~L ijjö5888,YÇbɓ'D4ydfD4P,Ṹ8qb֭\:::p8;vܺuSLi-3w())ym6]UUUbvߞӬY3ccΦtfoAdfΚ5ظ2COII Sr^mɗZxϟ߿?a۷OA< FMPPPttt~~|m۶ŋqqqM522k֬]MMMkkQmm#G\\\yU++ .TUUI$E"֭[aaaRRԄdeeկԈtQ(--MII!"HC:::2읍 (""[no5p+W$$$ 4ջug"J111f``0em?sNSSy͙3gժUd񕕕D=wvXlYhh'988󞿵3gLKK?~|tt4RUU]~5kLC߿ڵkթS[,xQTT? 0Fb ,Ys,IM>TգG-Zю;^z֚>HҬ/^ٳba\>f͚L2eDNDտk^̙#ddd1>00Ӝ,?|ܹs_xyŬ,UÄ VXaii "ڹs&LpfhOOO{{{77͛7K$"JKKstttvv?~iiÇmlln޼IDMpp0w$f֭Rt~8YXX͛O<` .[N~>}$$$ڵDb .kbbsNMMFND"(!!aӦM[l $o߾}رt?-[vݻwݻwƍnݒ'-Yj͚5[ne.!x<"6mZDOO_~ܹs~ZZZ2C>\O-##cС6d.ٌ9rȐ!m۶p8!!!:;;[[[9sʕ+9;vl(d###55}hтҥ"fٳgO>ݻq$IQQQVVsl6޽{޽{AAA+V(((PTTԩʕ+\… |e\\ǙND&L߿?]|N>]AAgڵ E6mDo _Gnݚ+D"X,b``@D\.-[r߿/ "MLL̙믿QCCC7otҵk׆Qf-Zdgg׵k۷o߾}f4h۶m7o޼sNΝ^z>+**zJޥVZ1jjj@x7hStvvvvv:ujeeƍ \r-.--,oJ ۼystt5^֭[7oNNN}̙GΜ9333&"[[;wITV"]ZZZ|kkw֍,̼:&K1?544***(//o@X۶msmH$ E"Q^^ƍQϟ3{ܹ399prrP9CQQ177711<Խ{weee O|X,^bEJJʊ+ ׭[wʕ}˳|}}݋w |?@lY(44O>](s +WDD4lذ+V?̌DK.miӦ.\ҥKk֬\tͨQ"##Ǐj*wwɓ'Κ5Çg611Yt)jJOOO,wڕ""HEE~ QF͜9ƍ?sVV f7Njkk~QFs2##[jCc N '/R\\dGGGֶO> pמ*ؔϟѣ>iCKK I<|?ӧ"# 'N(++g?Dgϖf2,''֭[s XEݻ7ݦMM6=yGuuu#Gdփ OڵK tLJy[/_̌3;1cƲeUهgϞ^ry枞&&&Dt"UUU[[[7776cǎq8vڍ1"---22r馦Ǐ?|ǎˌŋ eeecX׮]>v옧gǎw]ݻߡ;_D,|299ZlID111ׯ_4h;w=*?$99yȐ!VVVqqq,'"___?k,F}Zn}!`bbTڥK"dCr{Y3004hEFFD"777f` //ԩS@&I$f *3TYY0cn򐚚ͮLH$͚5>|xll, Zj秭sQfioMMѣGk׮m۶O|e˖#G_\n# πd@@dޞUmҥMӨ)((_(ɦO^EDDW~.\XTTyyy666b_v͛7QQQcΞwիĉ%qqqcǎ |֮]{O]3v)7җ&%%EFF@wfiiy"""~sQJJJT[[&2>>y*< ><...!!aĉ7oްaޖ,# &\rr 7n\FF}Æ 7n\zz:xxx :4,,L,<[UUekkT8rȤ/_Μ9s^^^+Va֭[7jԨ5kH$=:&&NVUUѤIY[#WWWwРA ^FLLkhh|{=uڴicSSS.]4i$f=z?~~mvee͛ϟ?x`>лw˗۷/??oH@T*edJKK۾}mN<|rʡCbcchݺu^^^G=tPeeevvݭ[͛۷qxK.-jݺ#Gɓp"x 6l OLLuTQQ9sH__?!!!##[XXݼy_y}Tz kkkŸqHQQq/^$"emmm``@D׮]sttTWW'"//F:\ZZ^ٶmۅ 6zGfXL""www7hРF:׹sg&:uLc#_KN__Kbg Yd|>_KKKWWY_ r2LCCeԩ_m۶ݺu1/֬Y~z3fX[[/Zh޽!!!>}zݗ/_.H_s̙޸qG7ihh.++kݺkoՔ^Ӿ}{fӧƍswwWVVn2ttt|СC?tИUV=}T~@ J@ QN UWWW]]mjjȪUzZm6%%4&Qnݺu&<;k,ccc///cck׮ B===kk7oz{{3())a6 fC]]]~/6xm 111>|ʕ+) ^ZfTq䡧~aCzbŊXeemʳ?w܀;|;YD 0AW>;%%%UUB@`hh;P^^nddT]]}aD"N<e2H$_xۻvJD]tٴiS]] Y[[߿=r䈋kMwe˖- |gm2eʰabbbLanޙ7oSxԩ~q83gt޽~<庹[n„ &Mr]\\c!# IDATlח.]뫯ֲHIII-߼0 _&ŋϒE|||.\奭={l߾=55uС<OUUu<oL[[[wwwy5ju!33sL˖- e,]_Fb5h;kk?aÆ^z^=o߾z""D0y䢢"SSS-3=z֭hҤIÆ >N7:up""(==]oǏL>.|}Nkݰ055{07EKlٲ7n8qq Oy",""˗c@zJJYYٹscb}hd2ّ#G޽b<==[n-Hbbb%7(DXlllxaÆu|ڵk+**/_j3"h"77[nѩSutt(++3g\lÇfgg/ZK.o0f55 輵Z<+**:t0|p"u떣#33qN:ijjX,//ѣGŲb4֣h 'Mtҥf͚?~ԩS> 啕"D+ݨ0o=~[oAKK o7D"p|rMMtܹnjjjv]]]YYµ|+[w3_zR)iٲ%s;եKD"eff9r)LOO2ĉf(.+ RiC;E>|r| O>555+W$"555fۆ d23_ղeK  L2;jЍ7:wb=X,}v׮]q*3YEEYYE,kkkq~.|>,UZZ u)++Âd]]ʼe/_MMM^^^EE.|JD$ AII .|||#Yp          Yυ%ps5@dd@@dd@@dd@@1UUUeeeBP*~Q|>{}Y¢ʺk(nd^d2޵-/Ǒ[Wr8K$X/#Fߔ/ōgbRTTq8EE lŧu |Sjkk w|~(\{d/;gT 8qA YEYYE><Ṣ>l{հi&oo*̵l L2z*>A?]sDU5Ghmm-JrS3yW\\%KdD1W]mIxxQݛ8:Ƹ^VQZ(pe$s5l`T7>uL5[&(dL*fff +++":r뵴:ZZZ&&&mڴ9\(ZS"jg*_I[v[$٘ػՏ{(s4 v7m6m'eZH.=x=G[Sw zDEڵk9H$J#F'"ssM6ձcLj(==]GGҒǴK 6iw^ϭ o2]|P"Ȉ%/JÒzR5yִ: Ve5by ,̞=ښΝ;므Ht{q8P,S^^f544XX#":X2"buzۧO7۷/''gɒ%|>?++kҥDYZZ!YNN!us۷755}_UU'I/FЮ6Pj>34(+#yʹٷi݂r!E'gؼV[|HLLlժUUUsIRHԹs"JNN^ls`[j5f̘ 6TWW>׉]̙hxf*ev:&er//eL] 4~ږc95WB"\F2>CDbLEQㅰd2ނxq?_;t뫥Ӎ7d2 oڴbeffn۶BGG¢EOޔ۴iwWh5A=eTU/)&$?XKݑ H,"%{03CM6&hR_F)OʖQM v)Kz#ȗf/f\ YEYYEEY>^;j(H$I$UEd~/ лX,JϿX,.|+~XKuuu_D"SU`Mt2y xTUU BTEukkk7e䦰nŧ *뾖Ӯ^o]A hYEEYYEEYY)"Y>"dl.\NJJ‰Oʕ+nݺݿ_OOOII0 |\"رcϟ?oժʠ*IENDB`././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/doc/source/admin/appdev-guide/figures/logo.png0000664000175000017500000010106400000000000023745 0ustar00zuulzuul00000000000000PNG  IHDR?1gAMA asRGB cHRMz&u0`:pQ<bKGD pHYs  ~IDATx]|SNҦIۤ-Pt@q0Sn6`ƀp*ԛ4=C_{Ws^MhoH'@I`H #0HhH H H`@Є&`L @ H  MhH H H`@Є&`L @ H  MhH H H`@Є&`L @ H  MhH H H`@Є&`L @ H  MhH H H`@Є&`L @ H  MhH0H`@ WꊺꪺVAo&,nQ1 @zp_CW?z}}}KZ! ?jfVw]=+W.]fzj3'46T@ ԪVkgm ƭ~YuiW++/]*+PRrLQљ<)?WTtbi+W++UUҥ[ Mh1nL_1asTWWϜ9s/^r%Ed M$}2voh55z9?~rʕ .%%%EEEDDƦJ0ٳg.]79l@Q =WZxAX M _QQv0`3B =LKK+--@! '`| KWYM)/a ` m&Zzp燖rYYF6)rVSK\Dj&11_2(68BZ1XA75>&R{ ^,-A;_\|6?6?-BI10.vPl m-oevvÿxZqw-zp5944'.:?1(%$-,#,#<3<3"+"+?ɮŭRY)W% vG XB痔T'n!IY$٭+/^7C"BB‚U*/#>/ddddffT[K|`@aYXdX|nnn0csssaWTUUE+ nk_l䘘@n킶iWDFFøD&%FjjjzzzVVأd 763Y`1aO>_~WjX,{%Hb U&/Vsll|XXFvTP. m`VÐ\ P-OBP `ZB'am E I%vھ}~8vXpp0R "i[7ˆtRkT*=#wOu :9Բcӣ|}hb G^N86!ēo&zӆg{3@BUSAg'pSC@q_?aSmmo3fx>dDEǷT@X W@XEI9qqafwN57=;{m#|Nm`֮;Dg?ĻSLӞz[ Q*pOL,LN*NM)MO}+ssY^xbi) Yd΀lx@& rww3fvر˗/u=%WE, ~Z>j7Nۈ?%YK-7 J=E#]oS{*O4fXpp 04HaR3 \N- fsΏqn`Z`%aaacٳ_W(O?}||#T+^nzݫ1sKzjeBV˭W:dY}ֶrD]~X+Mph4!a,iiin3s;HЙ=g `x!g`QX>L&kժիa;8>::gYon?2fZ$2Y>:~ڔg}^Eusw_L:!(0%,?;66'!sunNm~޹"ȹD/Ĺ%~Z@X C0! !,!85@U?}OB 6I֢6%Ͽj|4&2岬wo08 ŠtV䴪ηg4@]i}a}n}f2HZ/)HHڃC^P@`0c#EGG'''ZUHgJ[}M ݍaMGX YYYX@*ϟl&B~$b־"f6`#Y{q'-Y Y0ʮLk2vez PP4b|0@CV]z}r}B}L26TT9?vOa_@gH@-xx8XNXG@+ܔnO{*K& ΄Ƿ~ 9*çX}g12C%;JDW<(ŢT+ei 7Yg70qE+Fh`Bbx0@2DZ *UjJ~ 2[yC]^u}:pڕapBHPpDD8!5523gZ%v˨Qm"џ\0L.+Z7^ue/d=%@ť%}Uމڻ(ઠC~P~g4c4GiW>c?v?d!ql I[;?fMK`օjOemZz&m1@ҿT],q>Unshs?ȥnC^& 84$&K6(**-# ^#N Wlv _Yb* aw)XPifU6mUWZ.-&?G=U*Utt44#G fdsYc2pi_7ZXqp:! `*9$ CJhrrrHX߆/B]?Lkm ʞ-L:5#1 Xf1%r6)mmbb(] 051h*J^NS>חȁYR8Ka7} E8`X$ x%ΜT!3pέfrȼ>?9{@YŬ OasZ|`Y/zTvu`~IS%ig }66Ȳia$]L# u?ӗ-*+q ؗ6 P) (5;}*/o?_?б Pj!ch9ް>A!(a(:c`)2e%( O/CQ,?\N 1:3节hE.n[7+N\xzOS'.'w ͯϴdݡ#C3$8j,~U.yNyϥ?Qlg/7c76[ }%5'm*El"3Fc `=c" 7%6Jl\f 7'3d1瀣k v0|bm1)H{3󳟙g~?3O{zj_g|2'qE/^4<pw!^^a>>Q*UVg_n9 r t cuȇnD\a=cw ^TOOW:h#|=<ܽ<|Wg:N̘%2 n]צduNy<7? 3d ӝ۟쮂v_][/80V9?M:oyO/X|xQEɻ-&^xˢH${le٘6O^}[=qc5-ZKtjO c߾+訳7(##(Q)ƃ <^Llr{tRaʿ+s- d}ڈJWQQEݨSߡXܢ11ooOwwWS=0s&ϰ2'y;\Dc6;cn9dBܪ#R[I-M611v5yv95MD}p Pua}ewVڱ@ FZV[|YY:#zFsV}r^޾hk^d%WcX>u˖#Y<|b5W%}$D$/+]<߈'.a97:qqQ &.9H:6 鼂PI3P:GHcvq~, MNN600=1@*A~`Y}q?yyyy{{R7؆@DzGk3'LLpI&$ҶumIƒ6dߔ~_ sobdKCСؾ (U) J=U?m08f]Z58+Vࢴ.I_bW,Ez~u^] "&mX wSԭOqy⦅:kv5}Z8H%K=4'N{zDA;ogSjLJ_'^h2b4r4jU4Co[_xHp&u #'je6=@vZfd)c,"vpf~sY;꽵~6R MT.QٺAp 5֫+lW./R%_m=FcjٷX}V#3VJ>+}s gϟz 1,`E-_zbEX$qE䁫W%ĭI$,uRELYguT+Z):\|'GF+߫>~B {&;ߏ`4e,*;66/!09,#"3 >_В M$$e3L7O8fZ[`}hF۪9;l _n#V-?_z@ $@PMgMCz62|Hom X P k[, V)=mNc񼓤TdiJ-D$?Fvgµ@XԖ۩ʔ~ wtlRX/& PcDV03e2Ō ToѦAܚVF=~ҡ޾Aڮ}C֍Z7vjGC{=W~y} ͚!М7 9Kz  z' E}4`3YK<#11yB̵Z4Z"tWJ.6bn4(٫rټ@iSh$u SVFUUUEThg}%bX(pA-O9f2`K [jűfM Pi$Xʬ d04X+`M86viؽg^mmz}._Y[ޱIv 9 $J`h qo3n\f8v Rȕ"bC~X\4׷Y4(~Cm7 LC7V6SfWgZd$9*S*O 5r\gI=#=Y+J:1}l&l Peg--dI34X3@}]m+Fmuۀoݮ_ۦmkV7myœOY>7j϶ LP608(4<,*"; ȠB"z3Y C^l0=tL նP Kb| hc, t~~~۾]X˭(>fBQDX)_ : ζα)tI=`c[7n#W%"qHIb5HřˋlVYfZ`36~&L j& ʍ u jEo-jLj6 ]@4j hthzcFtlա鉎W^XYo^xHАHHF'd7g#GҰ2ʐ'_MU-皡qyPr7I0Ғ %ŌSnQBv LAAm>[Syr}~m{wEH: ^rJFQu61;;n%&Xe+MCIp>YhcTȷ-]GHzɍf{`hq[nI_/}/HMJjZFO?#l@ps3X|^6իI;'|QE[6+AW,z ^l[gA ضƶA۴Ρ}k[=ЦkkNmX׻m]_G聺mҮnX:ԏpmdG:4>ړvnB_I[0%j1wWoFL&$$&3>qMӗ ҍ4=ІD\/9S Rby>gْLER&12iΖ)^nm뜹YsyLC`չ9dg)S"C.m6/}ޒn%fd+!v|XUs-N ,Mml2L˲.ɾ=/ShVH\chY>,6Z7oC훆6kƑ{SSG?4sӸκgB׫oz\ᯟ%n_rHWS̄qZf)6#"g>.`J`e^O=pD7 U"*w݆Ƅ+ȇTuZofɿX2K2Wf u]cfY_yUB"b%sPjWf] -P'X/+l%]lD0XdcJ$Rd'HzN _'sx7*,{՛W^ =|mnձ=>][4nQΜϫ?ݡW{nv{k[Nv{=׳uo׽}۳^{GKm~m*WWa^tI zΉel5gʬ!/n.B B SXSTVJ[Qti=C<͖R14N|1ȠSj')Mu@k&YVkݫmP#1` ݡ+lḓ`:8kGj5K dSN>Z%}nd3ְ$"sI W[ON+NHZGK"Kb[Ngc۠CC4 o8W*M]$x;?~^M8^{ݩnowo|G=tlw㧽>E/7}4i}fqv;c#zlg^CNsuSܴnL洧CVO+-Fq9]p{ED؀s4BbPi`q𽿿/%kO֬@]sV>Fg9g,Y m jxނ9fZI,qfXB>B HeR,K$V,`Ї+|PE?k1Vo8ZOz@1݆~GSj4g\y׺]{ 0}ګ:/{7NL #W ju4[?TinЦ-Cuۇv zH]#w=zi3g^y#_{|VމO,H=8b굔<R 7{p:$<+0`AA Ï=~ȡ谜+.0 a0&YoP 0 ;4x m;Dv:Bgl*/X)6L]NcpݢMa3Vqt̢ewQ"R3b|Ai!nkeڪP{;|ڇ}A/tzsDwWW}tO7~ =Ms1&Я!+ 4m`aÙa!t;v ca,o2l~ף``caeXWX`o `z6&1 ?'&*xp1y@qE kFؖ5 pF0Y9oK7h#`7B\@R$Wlu &;{CScv:izur<ۭMSVN]]uvUS;(uh 0\!rJZ[-%bHn!"Eaq#%}l3'' rXHeOb=S'=rѷrvڟ<;o^Y2Au+4ڸnXFvogֺnz#f&(86]spb"< @+-m0seQRBbdxSv@92 EYL3ݤGl!\c}|^6>nq~ ptԢ(,lf#Hej9<.>Sϖ%(HJ̎M j}}O]Nm;=I54ʫU S'6ŰDule$VCcˬ7PLp2啶UYͫZ멵VS1>g:-_=cun ĸClE˒Φݡ`ݜ׍XH;?ɀ1]Z@p׺0kNuo:տݍz@pG=}ԣw+gG=>a%-#uO{ҧ=>eTuS}3'?wRMmw)=ʚۯlI-PlPㆡ \[?[ԯZë6"bKRrr^(a@0km$ߩnN cXDqqq a77{t|mYm('eJ$1 oWТ2R9ݙ/*F~i6gew.&e ٩^bz|Ԡ j@ LrG7~ 44)u3xP `? ~Ha= XhѹgzThcbb`?WVVb? XDzչԌe &U} J7`Ȯ3 ZU:9@E+&7zC?~nkZܳV8odrL0@kpp h>; e$M.PMKwA0, 9z6OΑ5ߋ9YH… d{ ---!!!22288T/0xSaK#ъP]3N̲f{3:}⮰?\B$N]؊*ǟ,HN\D0`g@Q `F4|݄+DoÃm ڸʀ o6\X#=nӮgH{Z  KLL~nFUw\5eERR_h(؀ITpRQRed%4//.sZٮng l,4մsүſN_LV# ^On?q__;ݨ4Ƭt=4`WsREGG@&TTTP6S$NoǕd"ә<㜊,?/Oɀ=n옑bebbR %J|01V|uI|y***OU63{MrS#S45_˻}[kY˷Z%ƺ`ysw&)fܡ Anmk?ֱNuOuөn\级;_{83_v{[kNuotحQ^ kWIW}Mmφ)Ipf$~$IՃ\ׯ԰nPa6 $M{PkӘȜX&vVFM^<_6{W9? {-"=;<ꀒsrrZ&K1MOSQתF|WƖܡo2p_J-Ja_Ft5ݹ/,yw/Y~U9U B7@݉ a*[_5<1~yp0s&cQ ;Gpաgƻ >K)[, T+wНGs,Fc*9Օ5;eg!(irV)Sm ZZܼmrYŊ;bXL^[c 9lYgm 8y_x¬-T郋6^xdg]_ ?I&Eu|!.<~|$x znu6ؕ}Y'&9cE⇇aN!a r[;/`Sukpi(a04LXf(88PnyBք|B|NI`S)eNUmf ft,RǴF? PT^>!{'\WZ.ƁIF] qUҷ0y,L2@Xb,7m0Nf4tSl'Q}<--cwBښ'o~|M?0ucQ4x XM \TczJLpEzdFrrr||[XX"Wdeed!.%$dfDE&iQ*Uw]|Rs5pk|׸/]&8gf ?-?MQ%ϢX f2 Lя ̯ Io'An0 KTy:HlӢb0d&$&2!aA!煖 8GSqN$r8=wP}uu%5 km~>#cAJh,?>o.YL,yg_*YsҺepnc_Ҭ?UezcH$A 0@y[ Ɓ|LiS x :0}q#azZO7U`PHdtk !S(#?җfx~Kr50K+أT4sœ:~ }M9w V{k\G=r _3pw?ڜˊdϥg7]m9w9}^e Ec,~B^}kZ̢u!{bGLUMzD?Ճ9][mܖJ;bGƞonkxkNto;5g.Nzv>03`XjC1*l t+*yEЊ˺N8{Kv9ZγVJpMY9&ST  gPu(C LGY5w|8lb^=vkJ"y겤KR~X4uq̥wN XU6l 8kBwNmIד&Z3|Nu:ַk kmkgԩUCFҢ0 nq62xtuOw=ۥ o{}5byK&ng]=/7*FQ0ԙ pIi.,3'eQ?/2OE8oqU`pePH̄&p3e k 0\-:o.,MO+JI.HJOL̋ˍˊΌ\KNeuBPuL?P]?/ QfvW_g'^^o!^\7I'D `.k2qZ@yh[,=or}qUO:vܳծukUף͵^ixб~PwxOTKc~ȹ yѪƖ~Bd*Q,۷]sxaX~a0ٱ1T*QLI/.`KMyS^f*rŗ˩RiKB 62/)], шZ42^bTDE L`|` 8;cBx˔W 9%"X%3<<<$,~P^֞<owM1/_:wFLO^8q咤KbX}c?wlB83`#jfVjT2>b9U}Wzp'~cwakPgar2P `܄18Lg~ok?~}@$| S@\:Ox:Ъ0~q}q~4"I A\bx1a@ӶiTSWʆ)tT_\[ :4:"8|ӄuqq{`000/B4SA$''!jON^_U# #V>F=fԪU'Ny?PT`;Xc .3F6@u`:kܒ%b111:t݌$ɏ?9#G1L){h݇|of_8D pTTs `X 7!h@8wrCS %‚L)@XM@؊s[[Z-주q?VeDE],-B?o(@*xmrryd Py4=;5Vn)W$EDf`^nm1i$^|3N͉cw#.zw|{㠕]ɴQ_ FWa˃1F܆ťo"2zsFc0)`ޔpYgP)@y󩾇=f~b(9n͚50MU6#F@hahB2劣#O@s0aԡлe/40wosדAOƏߋND0ZcӁ~@@@HHx?CzV5#|'PÎ zT~ f^q=1z 6 Fˆc8j(жm[???Fie@ሿ+wǠO Ür4\R1?L`8wqDF0ǯ4ŬQBH;bcCbYaдipZ 5Ζ-+s: ݷ :(~1;:Ey؅ W+))%,,˳ )118hH|Eo8Zb-=|qQ_L:;6,#LA> 9?d2ʄb#wSIx^rfcyÒCCɸ'˸]UNt[|TLqQ!%>`:. GFR)aI! Al8o^#nExr~BczUQ\O1 mzddjx8z7>G7YoLS2``G<r)C[@ѓϙa<SaT5|ezd3OQS}NW3~{z{{㎴cE'k0 l$:'')q/G/W.$_$2qÈSjϞ= GY.+W4j4XXǠ*A 2x%"___X #ڳgٳw^'O1XXd|bM#h<F9pep)-4y_~ qw Zdv0qYt'OhU1;v;w͘1cڵN¸R)I0Xs 6@{8u-.PDE "w`}$yA_FgTy}'/?;L:uÆ X| ɥR@/;sτ!#X4d[ aij>C9+3HhXiwY,hX09,#7 |6m$bT۱ïӦ@ @s |x{1dWΛ7ovrч7zo_yfQgݻ@^Ɯ>z"Õ1E)ɕٵ^+2M h Æ:ZIoeZ[Z<5tc]%ėe;CfƠ\=il0I!kWV!+D&y5Hß_}Q)OBAu]f% V'MD K j腧'$"mݨSNx~\ ϱcǰP͛@0z}rrrrss [ ( !%Hn6v4B0 6 P\\Ka1&3 9s>06R4\1R7nOm8BtZhIP̙ct`Zܹ8xSĉgpBxsESVNsn\N$Lclv=ׇx8::w+Il\ ;w` rMtG`ހ@$ga,;v+K3fX~0N5kPX*LU<1XY0 ` lCF-[JZn f2OM6A)DGS:" n) 3m_b.T\]]j[`M7 9R`1Ǭ7p} ;ܭ! [`=phvEv+۹Ǔ 6L  0wMH>H8rpS `joQp҇,^TXFx ,.]p_ٳ?YOa׮[`;d nՏ.73 ,,,4z )T*XPs ޮJ1JX$ ܂vFɭvQ֭}Țܹs>@P߮sEY̆spp'sgvODy#~9m-ZulտJFoŢ^z H47=2@"+857& {?f`׮Y%~_$}z -""?)$$-"<5<,>P d^Sl=<|ΔɾǁeSS(4ZQJJiOx#'~ `.JBcϏѯ0 #np>;#s`W]I߾6n#..`;ѣQ>h~XB sjwyƝP1f!$1X +3~RRpXGupYظxGݻw;? t!.`zgycaB\ad4?I/cFhC*TQ{xx$'.Pt.\cǎf~5n8sDj6c@In` WN[nC 9诠cHX,\&C:F*1c>+{yym޼ CO z+!e$O(ހ_~1o@簆ıxcf7/[ TZp 6?N096l޳g`]LK70 bM,C>%a8k4\?Pc;$)9h„ d^R ,Uj}B(^~~~,`a 1LsС%K`cDШP~a: (Ҟ ΓD73,& qFkhԩĉ1e$$80Ll₥EX9ِ^|E| (ŋ`0I-?4־!` #`3OtX kG(⛰3@N0(Lm 5͗#N~~P/^OG`1@ B y@@4]E˗/O ws?fX2d\xLlj' kS1ƺ35 EU!0|QQP W\2}f d?jUXl}C:"COqk\7xB@K./^}QF=Ð)mccA ň+cǚ :<}tՀ\OXC,(. >҇@;[c $< B/`믿%^/"RE7<~ǴuBgEv7CoW^dkrI#}}CCY"Ç)`4-e>>ԫQLN.ލhssb#L<< FA066II<㉋EU#џ)׳m[@I<P]8X_u$^Gp/ボ'Yt->vќ8M<-{!Җ}#; 1&75-1~! `Z慖7z C;wJeAXpXm0X Y:W6t a/ Dz\ "10 …hoS=L ;P3`Tѯۊa:t(q29o'"ZĊ̽w A?r&@ #uS Ǥb.LdH!kf@;V&d?zbkk{=]j$=`1\6s.|\t)G̸Bp1`-rRB"= 2?~`}M% 0 Ŋpc¸`Ebi ln錎 -czm_͛7c,MqGAh]СðaÀe'L` eA5dSxlǣۣ+P,}\?(3d= {A:t-XѴ 䞈];0laayf(Xukonٯ,pU@qZuL(VzdMN8G05ޤPoo_gg{8ZYB{k۶\ܲ=e@}:tPc{kO߱z5 >+|x@tH &{n}ժ< D٨B[jyŤy ,aO&᷁1lq\Voe!1[,ݻoj5f|QbfMh~$%<8qMtBbE,AC2T !* YI[>! ˞6L[o!DayoMb~ݺu[@A&CB_C >Itۭ[iӦ1$| $G)l#WSZH0(BБ[t:~IxojTtYzg͟?$\: az@baa^{eƹjՊؘI&Pǘ0+<sX^2(St<+L]F`}a!`dp={lذaڵ`- ?S TZZ2ȬwwwҤpA:!\Yo:5G͛1ƐL0bX)^z5Qi`~<A-x@p ( ?_aR*X -Wn:99bt G//p ab25O!*r\&njo`S9tCgF/.lQØrT }6)yv1HTTO?]yޓk:~ǟG@!DiI)M)(Z( , mJBΛ7όí1khOfn)U#V̻vK5>=œX qS,,xcoooX2EaDz@w02JDtAVXJZt Jc@`:?CS5M؇@˖-ؼy39޽.lVDri88(LtgىYgs8 h c j |ǏyɓUx{|{c@3sL1~f}5 !~v3DǶϚu07>]ÔP@ymۚc(?۽n)ܻ}`ǵ n\䏥V4$a'hpg xkyWW j02~Zar20 .': Rp__*ZViC XoչBG0 z\7B_<$_>~ލmx`[6Lac𥉉Y110q;Sq (ޔ &w EHiP#,*EW@S2 d¥ Ԍ Z`f\v(1իWw .gpGHS (B8skѢEP)pJEq ?!d-6C;w9s&KW\ tF]&s dˆL VKN56CTCKcN dخ6?`8x~nǖ7:jx)XpOAwbQ&@` Sa|w_ rBO1^) %ƋTAbn,jJDc//^ ` H ̹& i^6Cf0(Q.X7csoR{,En0 ̟6p.`O?7x~Μ9!/0\TVN[g8ӊ^9d$Og}~ڲe˱08|kر]tAwHp .n rˀu#F05/qR`r- .f53Xxx%[du{EyIbMM(7iu3u4x~;\N )j=5t(p0NU0 xzHb[i ;H$|m=W̝{EC٪@{^yGI5)%, pޏc 84 0]Ύ͌Ðw!V^w^PhfT&TŅ/gFK.يSSssƺC_wHJ͈(8*/lq#,:o&bzf ,ESޠФ'O6Pcƌij|й ʅqT/;(a_eFPq| SX7 D0}1֭[z# ^PTL4W^o Cπ) A*Uor :V 7>(4u裏Ό3 EobMkx5c HN'T炒Shڵ[y }!w;J"7 4FQ$S(Y4`<h4[a8 oׯ_IwXUH͸ߔ1d$"ӀCWȿ~ <rJ.8Z{cwqtRQ:E;ӂ2XU{]j̙3g ­]\\1Nl)846̓`Ua:H~S( #Y~KؔzAu^PGb<nD7o޼vc W^Ԙ)L1~H63eߡS.A4_ )je奲R0Q0Ze6u LWTȂBPRr_B5 K:%ixd<Ʋ͍KH`eFGiݽ(7ܡdw[w9=2 wϯgOs)0(P/P8nTgp}|U^%X4 /'ic#WlaAarrBP`GK鴈pt`%u }٢Lx*\E)^ebL R|=7/[.^p*ɥD@ b:ǣz?CKsu5-Kߴ"4]" 8 ruHa (%?=& @T~#MdciJ5_,H +osŷ9C#S3:2:2W}hcKO$4FCg`hZP/\]nM 1^-Pp-[ζmexşL\:laWS׬.g/RFd\c#`6&GSٚvlQ-gJa:=aCmsR{2a/[Jᷖji(UY)bi@SqaܭqKhy)>_ܔkj3jn4t4z:(oY3tqkcXuD,ҽgnN'/8 z[Ի,<0%c jTЩ$ej|ϩ'یP(IJ8E/N9s}A#Ez+ᴐٹ"yD`8䚆(`q1 {6ɭ;R33j 52Momjlo$w[:^նq(kR6`J{]_4*+g2ʟSOd\@eMNNK@ _JQ;h|U:-GeQ|r޼=PANNٱrzddQJ /VitտgvȵdXAt8?aPCHϧ wVSMf "< QWVݧg ӐW'y搏Jaa!x@"~D{ek8i9p-FG9'j %%g k,͛͠bAչlr1βdJJ!K,q.C-# $@J9o-LwJ|0ΘCQ⟥ $ 90@-.sMzB`L @ EIDATH  MhH H H`@Є&`L @ H  M $@ $0@ @,@ $@$ $` M $@ $0@ @,@ $@$ $` M $@ $0@ @,@ $@$ $` ~F]R%tEXtdate:create2014-04-22T10:37:26+04:00ԊM%tEXtdate:modify2014-04-22T10:37:00+04:00"ltEXtjpeg:colorspace2,uU tEXtjpeg:sampling-factor1x1,1x1,1x1pIENDB`././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/doc/source/admin/appdev-guide/figures/step_1.png0000664000175000017500000012726600000000000024214 0ustar00zuulzuul00000000000000PNG  IHDRZ*sBITOtEXtSoftwaregnome-screenshot> IDATxy\M眻RN"Z60X2Ovc {R)h$$i!훺խTy4׹==9!V\ 4M4]3+|OB ??"~HcB}x3'(4M BHDQ%!jjA$fY,VxJ"H|~n݆LyF!Eaawܹ%//O޽{V4N訡f+!`ZZZVVV%%%L&tyi Yݻ6YsB(CCCHTPdEHdkkܙD!A#FgɊiCC$B!%IEOY|B!Hס4+P(l!B迮@ ]&IQEQ Bٕ@&+HwE!jvO!B-t?Q͝+B!H!B- F*!j0RA!P%5K!Be*SA!PSNcB@eT*^5w~B!F&df!B3u*HX XZB!w M̯o!B!*B!Z TB!r1i(!&l佹sƝ"Ȫ6hkj䉃)6eEF߮}"%.dH2noq\&rk?J=}bfTMVo[zxsuA-^XbJM yCּgw!8_p륙 G&:MzNN ^Ğˁ0d' 9fæ̲7רS:x{ЛĤ|qASz`#9B ֠H`dM(  (wT墒b^让z@8n\P0V:xfo>e)x`fD6K}d>.~$^%zs65T(*8 a &˫YEPf1[8w␞4YE^?y/#;oW{{\Έ[bSd;kןhp c'&,=~o?t~Dok5!^M-Z@e3 tg\_ܞInI,*ue&{_?y>f~Ѕ/ 3h㭌dIJkɷ7kt5 [G \u]JTGG MȰH 3|wlMwȰZw1-e9#hȎԊ@Ԡtt[ͯQrl +Y:QO5o\z-`5!@P2;s-NX{b*'[6|?N;, $[u kN[ӃxTX=B͌RSx[ 519Oe>R!;~yа'M#rք&Ed?،6%&l$ i{aܕ#ESvVg1Kvݢ~ZTWI]Eo@q#3gk O~CxL"6#H dz9 f|&_'5(Z6.-7mL OL0۴܌BI tTDwoV`Z(usg{0~cڝRUf o[s\z:M$AП]n]NRsG@aڷUUuh1-\.K<[z %\Ƚ%´gߋ˵Ɗrlty*(~[͙%9o ]zt* ?惜&wzuH+lu]?GgE_#e56͏w᥵ q6z4Q?vVL˜?/h,(}s[vZ7LtKנbW$]7Ɠ0Iԭ R e$A/g>AQd!L%MquC3J sy`ˊSc]փWc,F rVuڶquz|K΅\uf Y̚?SVk IpdkIJt:A@"Ѧ fJU/SZω{VYTҫrPdߏ{>SpafiHj ٥r)Lv˽+N ysaX~;Qa8j+#F~K{w[D__u8\N3>|$4¦Q_NWqJɸ}W ~ٮ__bАǡAO _肗74IRmWbnW٘rGc<}_<;tC[J{h_OMA? .;/\z;ip@P6jPz[٬{v8-{Z 'M1M6kb/ԡdZe\4蝐/wos+,A]- ~~DIgT~`{{<_k|J^80=Dk]f7cqP׬TQjd(Р~*PbH%栝NK7L3d e3e@v|Vݟ?ɸE˖-[ȭ)0doԭdTGٿR @r:M^㳅!ǭQ&E S"ZvLk6x?Fsy }nMlpE?s}R"} 5,tx{Aj$j8#h?rH0&YQǓbqà=d<Ѷ4@?{-wU4![ԩi8sOd2@S4@c-O^;XzU'@mӌ3r]+f4.Kx}~ÇN$(;Y[z\@@T3m9JlM ;1&)_l6m5e*H,n`244E3XWzzz符"F˒k}lsFpD( .XRԶ ?XR*x }M>Y)Y~3tS&|/PMT0GgeR zI9ϒ8UתLVSya8KGhx:uA]hL铲7؁-PS2KJ*@WՒ紡_ſdT棐sR޿K ]G7!4`֡dXetVMEHWy6<Q'oo5)/yTZt3a"[:V3l;s\ >|!)iZ~[mFK@U[CnG-|ɽJۨ dbi56(3l!d4iasԒP(~ÇǏ`llvfzz3$M[ܞ#G޷w/Ibb}1ӂQw5rĀ%I͍;L,HeE ٗ/dPZKY]J (HP2a'),VJ)D|e )*'0?$I}) >3{TM#,6UOy?6RLpU9مP[ϡ @IӐE{9u;0:@5Q֧ƛ%SBsÖ蓙Ժې݆8,Zw3z6d|}KE@n-`h>\^33hRzz@rk |5e6,S " Fbllll)ɗ/_;ve.[aϞ>sں>򋂃<|Lm7*G L(ӥ⒘KGNLg#aD$ b*]oD  bW_VJ\ݼowd1 lirCn<(q>y -='O %T֮`2%$S+)*?3]MՎZw13nX͊+Bq_lӣBL gFN\dR T2a3/=JZtwUk?fknН2NPY `)+KhD* L↫X,榬&eddNxMOr"b*q~fӑ0#=\j$k:N~$df`˳cBZq卖.Npu( bX6Qq ˭=ljCPǒK!QC) o[\c ?kV X'k2Z u?ǏVMD(&)Fێmg'J D" o$Pkm_b<`AO(ʈsC{ԺE7DF59nDoSբ0[A%7:6<~+uCpIa>:jGfSIcDͫlץJNЖw4ÿ8ځX.[;>\e|ȉ6ݴܘOߵ{Iz7dYgdos˗3M T1?&'"O1dA3> &368]p5cۡv~wM%IGxz|ıy`D<@GD899I$DbN8QO&լkQ5̺Lb^Yy|JFI]Ϥ[k$Ox[TiӪIm) ψ}ETX}k+ckmwh:s[ݩIAO+u3kP+O{8]v [ȴ{Z2_t>L#Qoͪo݊AuLV׍ 2#? O*mզiZU]2$,]zCm׾}ʓ@+V6;|" }\0?ۚk pzKʶ%u(zh;#+ IDAT_L(im@joPzxI$15vUH4d(eTxPۇUKmbCeo$Ri@53pJ*ayOx&x׶ Z+B!e_ B(rWclng.HKݦV B_mvi1H>.\uﰸwo%VicgxRBA B!P G!P˅ B!Z.7iPB(Ru*B!P Ç/Q˖4wfB!`***i#B}=QB #B\ B2ׯ+!BUT:w\@![B!raB/iIsf!Bf`x 35]sg!B f^BxHB B!P"aб3B!Ts;3!BUB\ BH!B-F*!j0RA!P˅ B!Z.TB!r5b"?=:~YlS'EP6,v⒆oE 89V%L 8qd.k1h̬M>)}QIĎQvQ*1C;"jB!T":dʊ}ELќ]_5;pEu#765ĜZ5f{άwnoWD3* YA'oX!jƊT(Ik~}J Ȕ1?.g"ۇ kK i743' _,nsAotK6? ޏI>u$Ec䜉 T5F5-X¢73S]N#@źn5g 4/6[NvN^OLJg+^ "&4BB!X hד :u05@O}*[+ ޿XʺE>j)m+]o~qS@[ƯT!Z=sVʟJ!&9W7n8vI\M+-86ӎx\y*ŇRl{iu]i7G 't}̓wI*]~;nIYËXAN!P5ZaV+d n;g~bcfI7 ,9kO]yr .=$FUm>UKmIBS7./*G:jxj"*/CşLpT^z+N@[PUYjΚ .Rq)aUwޜ37L0H!j8faYne9@Be_9~5ov*ibqZ~ɫ`FD @6e0@e -N˧AhA 5b(KGws8߸2$?5$/FpAӪPT5Җ- [LN/T۲ۃo9{)ZF7B!XPp$K#[uU%w5#:CQ !Tl:bTAL2nd&\$a _|`0u+Lkq]U`u1gaܐyJZ*]97 Tчt6qKl@Z weŜηuSƁ!Pi*=-8o^ɳgD洕I2x/xwX&0!ʵZq`C$G8#'Tz4R;/nȤ̜o|*:+tbThv|穼Ϯ{1@܋Mztz"*a]vTʥi)))))yFB5Za8GRs P65W} Y9ڄcߣ܋K>gWo"2N9VHhTw:@Z-㈮g~Wl* Umy{WE!P%I"H$uĉf:a;1Z`IQr]θndB&$Z``B!T#B\7J{4w.j`Ϫ3B'a B!Z.TB!raB #B\ BH!B-F*!j0RA!P,)pA!ļׯDp+m]]B!P%lA!P˅ B!Z.TB!raB #B\ BH!B-F*!j0RA!P˅ B!Z.TB!raB #B\$bn BUC*b$͝BH^e Ć B0LuH$yA! +RB!raBG*+K&o|C}*ĩ7;88lz\@Wy]%orp}4JkFs14]h̃oJ/:\ؤ@!ͷTIf1>a9 lD*ZY֫Wwiʺ}7Nnio xbѷ,B! 1J\K'B20$?EyFVFNPrREŞ1%g=tid MFaǩaEN7 k aF]5]κ>N/d6tꬱ=4X„sqZ$3,5dʍ>ѩ\BW˃||; &q: 9uyxᐟWe=vf!SO]QnZexB$ !jN߰N. J)Sǚ(M\fqGƠc(?:INɛ{y{<_}#G[fyLfłn2=njĮ\cw ;\19Mb╩ݵ.>]cz!dY5-aki֐ljr e ] L}KqgiHÛ~Uu%,_i򲿲1 !jѾ] ]y':;1b^?HضmV/ӊQұd{~Syy65^Jg^2Ydw7 J\9U;u.ǬTUɩ\COizGCmk&e߳w,ù;hkK\@u1O[w٧ƽKQ\x˺>r..[s+h CnZ~)%#.I,INE#.J* g A!7!ЏE*t~w@c$@FǀM#ur-kTą<> M@Es}\"|VIAzC״ 5YJG!-PT h<h ]s`O_)Ñ-le`0HBAUl>hAn&@v OJR B?oP9awHG7GARLkk&CdCUKj $H󾙭Y}<ƒif\_ZZYd(i+dzer*zi7n- LYea)|{NȊz"g4iϝnȵ-5dņ)ysҚF18wr?W(# e|`SEHY٩H޿lY8l-ϧ<] '|6/qyrBș:nX:Ҥ0kAmm[9HTBNNND"X'N| WܩK"/#nQAmBM BI$1}j|ڻ-G5*_8B5T> PRK)m9BiIE'D!CPiH.*iaKiYrP#1 h4P!Y VBZD*&1 ]:ܥt -հ-;DiB@@] +oT* P4)s~/fhЅ/ 3h㭌dIJkɷ7kt=5jRшS>GB1Mm۳-tL&6nZ}B`9wR/O6$ϸ?J{YC %yٛ@NMcNwh[l5@6sZ0?O@AW0"2U,ksn;|evǵ۴n)`uhJ2-̿ze7#|S:~,|G)}?* T~}ctKנbW${uc@ԭ%ϟfl'/W<'J\7kӵYbA}?G8# X jfT>!⩄ӻQxUL3M5|]fpu& @J@-JՃJ[PQ Lpy=ғOBܿiT+W JW$=|`w{/Anw` ^g.usNH߹. KKbАǡAO b< @J7 `41ty>_T S˸}W ~ٮ__~AGDiqi.P_ͪj$+樥MA}(l"]@v[ zv& ?{|_Ne z{Ϯ3g|z:4#'@k0l- Y":/UۃǗ 60 d.޽#H޲=Xj0HN@v|@y-~;cZV$)q_k 8~%:FJm 0?%UNί)h7_ʑji P>ϟ`aƃ}#sVaԯ$9NΞ0me7S=ERO=_JGa@+F7%Z?]1 ԯ)j0bt_+t3'%ҁ a1#[R*l㌤ǝG.P] @?{9Z,]Q@o9G*C{ym-if sOkGWK+% ϐE4-Cru6ޥg˨j!ɛTu1vߒ+_ܫ@ͨ #!MPj/2FXl ~m~ VJzsU9م&Ph3.{FT֌)Tl(9u䘵ߜia~N HBjNR|gOf~k(RnCZwhu۟wِ--=yUO`[^|8ggФ`>@7OG* B߹ S!7d8<  IOJjä˚vL&T~f, 4bfܮڛCW$ױgzU$Ifk=ʼn};IL5J7}rGIB.Hڏtmym&vV(@#l5&`W~vpp(.LU?r67f_DqQEӂ KH(+NZay_E{Ј r: kk:2#?K'$@E_)N }Un-y@S48'~u܀;q)~ ڣ=k?]ym^>γN0}K>wzڽNϢ])!U0wH OJRqhIL 3U BZ IDAT([jnf/N!|w}Ǧ;w] k6Vf,wW}q QRu}jk@?"kJ@D'mmV#l1֚n'ٶ=k Zz wCj!w]ܖ_;Lr{L@ ӂcg-}#Ljk.=kz-*B׺&ч"NN>L\;8c97-?2F&Dd`Y#'v,+NVytտ 췟gΛ1yѹ$ڟ9/qBTfk 9F #R߸$|$r"4oIˌ_?5=Qn{ ˚O{ݘ wڿ\ËL8V*ݷ!}GqU on8qZ2Եݰ.maU3`.uqx y[ԲMÇX0kpM vg|U[Rlv Iulז% KWIܧV&B%I,jDjX&Iy?ظ|9wѤp}LcB||"r@qCxD?goKSb߽%ұ7lzu6k@c佫o /^rr/#FNMt}~MfW͗8wQT%r 2D D]۩+Q!|ܒe+jm7 \b^S1l;i2,&P@tC8tOv1l筀k5vˁvA]\vN&5S&kܮXAkӔzLZgWCR=Lokcɠ&:"5Iz(ȭmPR.Q;.}7q)K(qޥ= ,Vo^Sa]v=w)M6ppQxWS%5]^E^-[ gwol=kwzOwbs~RSoåե{{";#7 ztRiғ1iBVkLjq@D~3!E܇IgcCY_ldKP~ͧ%9'8th@R^$J,a[)=uKAf nnw4-%*av/ :O(NvZ"P4:[;tvZ?3Qv%VWaqY&v\5J CoŰ)_HD}lSwaMd]L !@Ae5""D vłOY{ֵ^AEAW׆(EO@J>ge2s3ɝ;3tf-ş>!2kҾUJI.|vς\]YqIP.߯GX~{Uz4s'b{wƦI8 3 u]7?x&0]mKN'l}4{?KئuhL.<]QYwG5K=oETvfU̜85x^ܳ19bBɩA?Vj5vv.mm {Rh{ 99ON*jѽBJtt,g;,W*3;t9rNB8]"l$ ~ivo_[=o LYec3sja>&gdg b9xKKV^cf؞M6#n!P-WTD kҠty6 _:qCMs[J_92ȶew_zCi/IcӦ፴A>B̈́9?ַoK@m˴Y-S$5qP66}riXy"9G5wt?9@m)%cɫb]l) E&LB_ ((cǎ]qJ(P2*h(wXeL4p'N}KtK#Gkrlr|rX40 -&fL`  ʻsGg^esF ]ufP BЊ#׫9awuՎU1Nsj5 #>]AA|v{[T<[ɱ!|/2B}اB{UA*j7!L% g N#ЏS!  qdžPmFAIQ!}L(Q*BY|J|B}* B_Lڮ S!};$wT " A$R}Bhi"%C2H$b &+#Ti 0Hp2a?ij %U &*]3b 0`Dg7ȏ"5"~ umD6L`~ h  h I`AӴ95Gŷ< P"Ǵ & bj1GAUt-hA$F:RRJ4(ic hwåܢi]c;-%>;'Nᤤ#i9M9.?n2+~-8v1!?~!=Ocwˢ- ~пS)Ъq+oENo3jw+v_Z)@<>~G٠;2a2is[8;`Xzy˙ד*H(b̺/jwMSfF.E8hM?1]4c]Ln@SB[v`u%g[դm`TOLލ(gYpλ7$X I _)/Wp` Q=aW(3{gl?7'o ndwF·vsY_Y3ηck>ٽOJP?93ߥG_ "hi|X֝w1AgBFvwy,+D.yge*RS&ttςv4hzkӤ^n|>ߥЛ~CH+/T+nÞE/]IJk{@|N}&n"Ԋb$1^h{R׀gv$wXIޟn5yUA3=:q?SY*G,bWE ,Hjb _W Qs0A! G!}sLueލ=(|'oe6^<1ueȥFZq,"RTҹlqd[֙럊 9umލM_9VL-ds<]1ySQO@HST?'@/r{sFl >8"иzav4(n\ݭ.Ks9ʔ a#V!-[צ-:B pαLPx#N*Ʊ֣Ohf3)S/t!V6e^խޯnLJ)E6ВIO}@,~"F%C`??˧ůa B*R/ƌX Ul]AH! VWH\n]kpȶqaT6,㺆L$ r2${UKI.ymڊ425R` [:-<~467搸Mw ǩi}m\]q^aɯikOLˊy^Zl=:]̚i˗/_2Z**JoWG;>ΐ~ v:6اU Py/N^m4nӻxՉ&LвCc~m캅< w=2ul?hD-Vw<5gQ^ EzcLo:[M}Gphʕ ͤua6_LTݯm6?Yve+"LVSw̧ \@zMZP -uK_^Z|'+^m\VɈ%VP{eEekӖZmtlH*O_d&(6}iU>99M&m]$!m:b.oFREQڱcGuǃBwg\tϑ#ˣ]_?(00P.3 azB5Bpģ5)Ai16@L!T鸬r B5A!Pͅ B!j.TB!TsՆq*3fM-9~ܶ^I.[˒u"%8w![]a 'g:)([۬IAC[@jղۃ"tpڂۖҴB!*6d*zWoWt# ںUĴbACę17lܼ~[xBZՖ&xRRަn#gh%97bZ8o#Ȍ|,>{7Z-o2(C'AGt*}Cl-ߺWa+&J}wҨ\Re#BժR/ BaN;{jiU2Ǟ[;L/jhȁm&[Eo~l⩭Doέ^ndɗo6plF̥MnTplsZv(ueNn\vMN~MZs-DanBUҧR *6󚩙o`ʱF-dFih7҉Bm۶ hK-fœwN۟C۷%IXA!GREQڱcǏ'3 B_/00P.3 Uٟ T*!B5U0ipPmTfs5 .C!ͪpJE]&΀m줴waqR̍ p<"O8#=Mӻ-,,ЏL|3H8BcLS2}1vn.vӟ=>\沆9*LLay=|z!%*[0zCϵ϶&Q}aKB?+dKv&U]%ϗu }#aߙp cw/}gJa.nr]lCPIGOc"OyO>,,ܺʂAhײkrm`ޱė c|`ta}V{(D_I輋1.?kÎVuoji`K(Iv0spMމ`Qô-jݝ剷#[ܥPwRe!"OAb%r Xp,GԳ[2 "skchY5 XJ[K/*JBhx׺> TJD7_[n tΗn d-͌壸M>h7VRH +DiG%E}2LGwtҔS2mlFf|'0މ~xح%217||$M\Ǣd^NPۘMlG5kn:SP b6J{`yڍm廨DK>_tWWGY meߖ;mֲTm_0x ⠶r>GuGGyݟ8wgms㷯?8ueV,  IDAT$T铏& !h|ʼn5u2-[{nP:Yjק<įb- ?u~k.ږN#jNR၁w$Jr:UYJ^= +8s=ay }G Ef,=hʢat‡d~nc#~qK"]k`kb2?kO(,3>2XKYagr8-m9 ;{5\]zFĻqׄtQ_0X҄NK,9 [)LIp;W MMs.!Ѫ#lMq e2"*Үoxш=zw1uS3\*s[ɨG4ybƧ"@!pUv9)3"i9~GXTıv(yIv?Nq/|OBxoUyΈaۇ~T|Q~..VWQ !?z\^m}u=^O\~*V>1,l2ʼsk;)a }C/ܸs3}ɲ-!ySQش0SAJ**D &Rd%vXx#<6? Z,+zWo5g$D.zi9TgN?1L p]Hʔ<7HڈzS%ݖE+Lp s+m\a_&RqX] OgzpkT4LL JձQ~}?u0s {XĴ/ndk9>f  }F'6[ք]f.{(v}C_ͨwQcO:⹅2C[ :M#o1plΚ*O&LqC8ztЀffzFSH>{!Va}v>mJ!Kյ^H6UX4yRe);H%:q)U0cֵ!׀‚ 9 aڀliad.@'mYb4x F=+6|)A %eP2&-*rL-$HB%H02r(88h\q6e84}N`ꚰUze!7TNR$~o@ף(i>% ئƥ"چ˯3[]!]K ml3'3 Upo[xi>sm:9nd7^)---[Qe1~RKO p!U%- ˵O*4ҫe\א$ 3dϼR5^2ض4RFV?d-X¬e{6-ݲJ02-ި.Fzє*h75Gis.Ɨ{ (c|+Y>" [!RQW{$|w߬S)4Rib Yjr$8}zG {ᵇ(B}G&N #Q@0Xjg:b_4"y'=2-/+ O,z1jCk+TB?C4Uc&"w'C3 >N=_ޤ}l`h]x׷1ԗR뮌b?9N&`nJE?\bsq\3I;ޖ8ڵ(\W$aŎ\z=mȘɧbuWv[/יb DµYcHu/y\΍7a3biH&˔!G1: _Cl V1<F#9~ fu4,FϞxkzWW>.j86kb&n 2t=mX{t~f۶V6԰}FT43âDzpr{ۓ=Q_sRlCk{B!B?H!ʿ8%<&7NH1u8vƽ|;R&4<ǩtPmW 8N!1:];VwZ B>P]|}#eO!B5f*!0SA!Pͅ B!j.TB!Tsa =y~ASe}CH˽1{o{mɋ^緛x5SEKE%$ђ+|\g}]:HZ5tbowg~g+gBgj'ÉJB%>v^a_3P08y_NqW:;t ‹׊\9XyzԭI^oWέ;#Z(ůN_oꞈ[WvZF̞u8gԖw B+kvl;P wgtq|ho~[/9hXʼm2P?93ߥG_ jk8ljґ1?[ʈ=sW[s>7F}@% 1; Z|)ACy5lu_²؝(*֦I|Kq7S)L1;5#\}CNOmF_^/.i%:i|HC٭&O7j9hF_G''3iDchh,|_н=sï.&Z2+u'],~[[R?2+;`X1 }_>߫}P5|&ՔSaKV$j B(3"i9~GXTıvHU@ly*cng+5L,J:7wy#,rK:sSZ2 ݺO.s. 7+;Z\_8'Pi׷?h8"ڨ WVڭ/-nص]Pi7Mݑcʁv״nѣG/2wnʼBۺe]"u L#zab}~(}Ov?Nq/|OkX͚:BNE]7wc%3eH*E)s[ɨG4ybƧ"@!pUsm:9nd7^9ySW鰆Z!QcFlJ0c˚ :˧nᐿ}Eu_za\YwgZk^y'zZ J Ps,[fڅqC2yGDHJjsr#\HGap 65I2U,nM2Z}$_JE$eHt=Z4GlSc5) TNM.H^a ե_r=p.ی@eul뾈)bQ. mmP;y|lÖ?j+mߵZn:ަn#ghQB0y<&_۹7Ib>Ǣǐq,%97bZ8W"7o]aATpu uI{=M{Y-ߺWa+&J1SA523jѬcwr%EzU(Nl@> tj&2J3犞i&i7/P.: Z$:4R$:Y\,WEaZYpgg)b{.%?0L9M4,xৠ#HPfF6y˷3Fՙv`vS4D ?`f/ԭۀ2ND K~ޖ6=&?@美![jȦֺ91"Ť4~M6м4~lkQzV(Ohr&XŔ.\}9irAEPmT*~5|ׯ.ڹuRN&e_cLx:ضJv`Vݛzo۽-[EɗloleU=R=wv׮}|mQ6 ;ݲw&7(o~tm{w3X\8'd&%z)ʠʼnQodU;Z.XzlB}xHFRt+t˵w~_q"el}m_mr56[sYRS- /Fy'=2-/+ O,z:?@+_E>y a܄% S;U' kyv~|4wJ:ѺBHd fTT7g3ofbbbbbbVߣO_ WqsŔ_:,XT*,'T|_j_~m6e_I q|.K*^~6A+,^1!F$Xt5}q8lK ͽg>]fekҪZJ.5ܦAI6Wx,tv+= ~" Ǿ*N#.N0_O-l]X3DFQM0\LTWɤ;\Gl^ŴD@\18u=VGD}iZsl Ѳw6g9s;fMV4H}]79U]$%0l_Ey\KLڨQ fu kr*jr.EQbvjqiz77WxAG{Sxj AM䔱ϊ#ؠt{@tusŌ2:q oZ@%=Qo=`bg*꼠{/;:] r.H4&HL8w![ NM_4v\\g򬉿M?uvν+'2o5uNZLW4m׿#Ь֖)M+Sm_t\Gv"B迠Ʃ=w0P*;w!O}r?ެu+k {j-Wp]E"BKq*Uy> g>No~5VDӒgHԩԢPB*TPW3|g Nme cռל1ʹZE"B?Hf*uT8AplLYݥ*jQ!ЏQug34¢ʂA!PPu}*[~Xifn]e BVLg2BP$:ԋ3 gjmb7C?!B 3PW֌ؚwv7s 2wՖܩčQ_d]LV>n>Dcwuq&EL|~;c4-2{{9_UxhP B(3"i9~GXTıvHU@P)a }C/ܸs3%s3FH+_F7kqL>=ׅKN >Z3Pu!gOp5gYLtU| i>]FoO\~D}6in'] [3FMt wYByE/Ma;S;NTTߥЛb/U,vg.|*#ź^mΝLEF"`ŗ.غIG X\SLwhbyf5mP?2+;`X]4ʚq[f~"T"=.],8RXf<J+ÆBߗ+%RPtjzhR6[  9ݽDKPܭ]Kǯ/,㗾}}Σz||UB(g(gO5+2432eLueލ=Qp6Gv?Nq/|OkXJly*cng+ 9umލM_5Y$ު#ö!L]WZv<%lAoWvpO2PDl3XX>fV?Z:vME) Ե]soM_BNF]?:7>-j.jKeD.5rc!?^͝}^?`#ܒ\Z3*TZ<9>P>+tbΆ߳fS~QAs]4C\ h۷.]j_CF|X|tYh Ԥ*|g7w1޷ny)[Wo+q }JCP$E?[&ڳӗsad yRVӻ;,Zb04:ug=C}s=Cje6psg(sd GiÅr4o65Ex}Ա֣433r2E 2%apwKvD *B4*vy}7uƷu0KGxL\CN׭.&DY"I\ &Mdld:*dT7v@+2*b虍\X Wuf9oډ1 -8sX44 r)2k H(gN0,<8߶C|t?rn tY@e;+Ωu5)?Xx3Ŷg8,HPY 2NҕC0h .DŽSE7kq+IvQ6 eûHD2]l-x~|e,l" ]q*aa箽הpe`SܿΝ5"Q la7Z|ncEfu nɿ-)'rצWR߳-NNLJS ܦ}~I6ox,d;vMe7S{ڰM,E˪}lr.4nƳOI{t~f۶V6EY#onc(q)DEek4.'{.]?~m܊6"My\Gg7&l)~yۅV.coMgE]f~ev`(/n.]3~A_F((&o߾!зɿ;{U4sJc5ʿ]5ȑ# `0$Ib B!j.TB!TsB x!Bf*!0SA!Pͅ B!j.2E^! Bi@~=B!6RH&<Y SD{|)xTK޽n|UWm<5Kߝw ^Qz,֎9;|g#<"T 'VP.&ȼEudžOvlw*5DgWߨےGfcݮɳO+{=ݳ:]$ 4-LX,-6i޸cC(ݎS#gb{-wlO(ߟ ݕobX"OnO{GGgRyػ 0Y @M!wqMwPj]jDTEժ8P[_WѪuZԅQEQhEPI AG>WGɋ].zw 9{g( "">qG_j˧x]UK$h|n왣ֺkn<8u:~]hG- ]KNl3?XsΨ#aF0bIZ$ʏ߾っfxm;#r󮑶c{<*spCB)?ݒQ 2{Uu9_ {GG+xUt0eX\fYg{ gkR{\mm::PuM>u2N73k>1DVVU<~jZҧzUzl{

ľ=+qUV1hMMJJ XZ_Y:|hU76q=&2կ%kqi~c 1a@۰«?2HVQaQP8|dǞ~-0!]wB&fT򹃇dlȕ1XX&]Y}~I{Vd,o/x8$fn# ߼dl1Mۿ'+αM ,֍UmW_Ry\{c! n.lR>*Y>7]x+I:j|o*ݫcԍ>K=tD̿6}7 ^v+ ^Hسu~@_Q{_7 >x1~OD$\xrܫ͙jӥ#'}*'r6nĎKy߇|#iU_1noՑ3ֱS_<囱{;xW h|gyX M"%XSSu0Lz;/MLRybs|߇}HDBֵ{nħu}U*ՀbRDA}UM΋JE"&O{*d|j% ;wE֜N YW7wJŮ3$"(k!S,;|M~ .D9WN'h nUT'_12o{`;{cgkS3j}'mc'>_oR};`oy%f{ѹգ{<W<ݩ[z\,Firouؒs|.aSN!wO0|_ɗ Įfu9_ij" # ԲJm+YzrΞ*ʣSy"?~' ٸi~w it8Cַäujjꋐ bSIGKz9Yr\:3-JTO[{Tk;> 1P\2jA-g|*=6?!n;[OYb**M\(/ֻ0ua!:uڱiSS-[uВjʹ81=z}pzyWϙ1j=?O0/_ArµGDdng1rJv6f8wS&{.Q@ԅc-20b;È%'^] ZXy)I+F/ֺ|biW;kw&Χ4vp߱ՃLrxsY"xqWHH;::uxfw-8*d3uqG~YH"c'qa]zr܏xd:^?ۉu/VҢ?zrW뛴x҄?`Hb'sȡKU*Tɮ]-n5uYsDQT55y{Xc]#7GNV8 ×E}d' DTcAͮ:9jܒFTuk`d=ngc_U%9Q>njWޛn>] ,5>?V2w|ʱm*͎#;߯y/G}T֑ q59U)}7O܌K?%f,On`kLj܄Q?VLj%'g 5/UtKsw_-mS݆# /9 f*:9[Y3''YԤw'Mh*`@BgۛfNr8dAK~ ;vƑD5;~5\bkڴ{iD  "2&系e6Y"̈D^jq́OL WtSq?sv "֬~/Ƹqb¹sיعX3/emܶX#*n]|l^+|ou{!$joF-;xH-\bZ~m re9OwL=fiΕ=gG'"rYيJV#1JGW,QYMQT^4Pf2.j%D.zFcb%2Dd>۷laBSY(n|:.#>ԹǶj* v=ܗH̽/zBn8at)7t"seNLrXߘ!<}d^jͲ|+Xbē"{p?Erŋ/^x!rNS9=>ZcF 9{?HW,l5Jo3~kL^XiȐFNy9^:be&|2\WZͫM.HɈbm>NOO:~Q= hp7ln IDATdU[~ϮnqM/(QeVlDߜ;pbֶuȚ>1{[Ne҅g,9t9ߟvfʤ3?EY.sh7&$Vz qSNx qrT死ͤǰmc$yM~{=}C~w>gGFowwc}ٺݠypW\LyKg6֗sn4_Ƌ_0<qτq{-}1uJsFX])Ƕ]P>LhRoYw&3_9e̫gs .oYdz3Wuy<gߪWka㘈e8tTE9Wa(\\_Pl: ܓeƬg옷#ݯ @Ih*am89m]fcXsݶ1sj7&$Vz qS)2邚s8s?Ig,~X\a*w?'yeg@Yyzfb?Ԑ1ic7ZG0<yW?z5s% FT>$ e3xt/j[ ȩk-8|Wr;kPof-; {n(<<6lό{~w]| yyL7KyGaw p@@N88e҅%S(Gr|###e҅҅҅oüumT{RüV6>1xI'/c8{T{ ,+_d+adѤUCSBSBSb_\W@" 󚗍xR<¾2O:|233iGyvFm&Q >lwEhĚ 0,Q###5A0tNm۶5"\&MDQ,| nݺq qo޺uŋA`KueEQyW\׆x{ WLAE_SuDTBXRѠo IDATxw\g]Y" KEQhUZTZWmkjվպUuԉ@D".!S{Ⱥ{# h }S<{w:GB!Aě :K@aJ F*!RACuPBӴ|%o1*P%L!BMf W"&LDb(HB! IfX70E* ÇVB!(--}յkע\.I2da@ 2en"Bf \\\***bbbLlZ(B{Cmլ"Baaa!e EQPcǶvB!"bĈD,) Xhh"Beee%JeaIezB!hiiU`!vB!+)) QA0)0!BN6ܘVݙ!BaBQ}S B1LA!Pa B!( SB!FU(@YB! K6!BmKB6 B͑L6!Bmi)X,VB]udB4 e!BHvE!Pa B!ڨ6 @T@@ I #p-84")L *:-]L8zKZTi?6+ĿgmU>B[go0ʿ2{ؚL]7wU-9];^|_AWFͯ-:ROIv'ŷ^0Lst֫vki ']]D'yLy33s:yB(AR>w"Sr,M#S33n'2`8B0`RdM( mE^IT͢^'.ٻq eNtٓo53Co9?r3hO  OMlA4*[P鱏cZrt%筩+n ɯZ@(#yF'O}ߪyV0 @IbQZ<p R44YUҜ^7+y[s&QU*>2Áw/Xoz~]8Ջ^鵶 @w;}Jx)rƸ{IKOtn{6k r l6DSٰ$whLb96?d̨,;Cf¶8,юE[Ca+=8w﹈TUX!vM?C>}KxOY}UZtj@ȡ  hR hjg*'`+yz#6\8fkgcӾӈ[xPt}ї&%bг5nY"yݦ|㒹MΕOah[`` }7z秔=^Y 0 ذ.xNjNLߡs訐@5|c{N /y_GђbIե^RZ(nM S y|+,ɓbWddN\b6æ8l&I(()Yş3 pC߼!f]2E7k(h`.]Ǐ:ޑ /dE=]!`' arh<jȨ&^Kn(J2XB4-..#?+ejyknbBNVlK2BT\~tK$@!k9?s.'EQYY-a $I~8u16gZ;0 x bcc*L+)fOnǹѕ֙5BrxQ\L]?(n߬k|o>h!3X.Y(_ t|>W6ԓ|+j}iO>?d3YU8+xue쑽f]ͯL}|>a[e _"C޽v=XY% KcH>4h¼òōLVWGEzoa0>whéuVA ^x1N|~φy~LhJ:/-UC{SK69eP?ϗB*s(T2)d+s _$f@'hJ K+@TT(˫M~yS{n~gw8m+w6H=5'9 BMCrxZ\*^&%f hF8MBkۜ)z^ Ժj@ ^f6|m_+U.yu' x.\|%iM_\5v*N~Gv~iJ42[/j}PQ'zm e*ӯe`sU$ػg6߽tkL,8)!ϋA֥J[WѤ_wKFBD p Ks2cnYT480ҋ8<ϯ,#{NnxrvOSNωzZԘҫuPޏŮ5k$#''4c5k5C^~՛4o mu`"I;A"1C 2H9SKh}UⒸ;yl+??z ee<&m(K~WM(J:o7t/9ƙT<޽t_Dߵ;Ou^UN.sQߘ `ؼ~`b֍?4~D iSu{'֌2οtrSNֵ7P]oui@m_q9Z|NX;~ 1HYwDtɳn]IRn$ c]5ۗO L aѥ냊a\`xo։ §{֞OOm8 5T>'\e,3jPxkcfuUzuN&#J/]&6f6sZG6j] 5SNL]S#fLzR}S$4%J*(m 6]jI~Y],fjX;thn=TDp+]M}?0؁c qHKsJxF<#gGEpXvdgv$lѿc@慃!"5`@~KzKZ0ػogf :vn\- EWeCuv5Q'A;Kڳ @s Kz{NewuN&j nŸ G@aEt՛ڿՂ$P1h)'@VMug Mu܄ş-Czŋ/B\MsԖ$ˤ 7!WJ8 kV?~zO؝Ώ(JTuO󶿠$&xB긮ghKPǷ>=u2 *ٸҫ{v_RU޾pߎM('IϮ>CJonI@0UI؆sk~3A½Mٱ)m?m(X'kv)GuC44(`bEB"#M6I'עkNro7fhD?X`9eJW5pa:fL'4ݲ&Αf} >xXZL?{Ȋ.zB @9 wVI˜tZWc~eQtAsv=YHܸq󀻡q@KxG-B\Eo#O@k4E(8l{b9JN.MrTwmnu%G9['>z|ӇJ`il9 -ߨT~7:JK4M ,$t,l;AA$L&-͘$D"sn߾=aVVV/fffN6]*\9rm[qq)Ϗv# Joaj I)X/8v8Ź<X7N/LveeRUY+UkF8y3A4ϯ:2{;[ې_[*uŒ U[ wTq̕#N_ ɫ)PU FMHA !Td.d`2278lL\ ; X6\ףA9qO\;7WK^\I-D/jB@4ڨ 8:lXZfju FRllll]͛iOv7xÇo?zxȑ];w߸Q*(wᆰjہnq8 Шnӿ#܏)XѧkM/p&x,b-I5*` J89S7Ճ{)uN,&@DƟW $ ^g!@ԩmb[@ &h\L( lhw?ٱUǎV:K, kEj- + [ssÏ$k=-'U[_?q2(nȩSKͪ;U1u\+w}/@wD{(a5 Ք'fŦ) LₗD"qqq#H8Ρ)(X*Dܜ{{{ PJM74{ <brp^# x,^:$H qR@DK 15MJsⳄPL%MQ ){|;&<*KaJ`rչ T^'R&վqms HeMeFeiNTQbb1hj2^O̬;׺vKrIY<ߝgn=ՠ&EP߇G[bH) B*x 9}L7(P1l 4E)=U1t_V+7pj۸G#}HiK ={lؼ%K;w.ZhYr/]4y@WaÇ띻vuڕfe}阯pJ7ZPO4A|06’ fbSz) zvUZ.'W;TJkVyf8\X9*cA[#*({)@v( P ð{W+)n'Ԟ@>rK٤.Pd*dGÏCKw=翨z;%AI4jRR،r/ mlǚ@wH51L!I(zԩ ϙ7osٸyCofͯMÆM|٦-[ƺ $pH?. }qkkn[>Lw5F<HWQZH# rƵ' sj7JZUa"g=|}V^YV}PeϏm3x\UTsb_z7; )W'_$3v zp5 1LV/ n[vfVG}SGvQe@AbFyU.O޸;z7n(mã<⾝;wy*<ᘩv%H!+9GH6qro^JteZ`Og-RYb=z@^XM!?wH5HL&S";fD($h5$AA U6I#Hbۖ8oѩ <.߹&Q((4I`8nïC+ Q+^3*bIn#zj%\Z sGߩ˨dx;|l{CIrW+QTa@jѕ|s7g罼{kz-%O99//RN3!badː^s4,ۭZ{ifH`;}\˄W_϶ZΤ]a#ت Sϕ-XP<'{UM#n@3g}#,};PjD?bKg>~Th c}. Pl&0UXM랑~x$#'SZj0-w~~X[~[`nV:܌G!Y^Z9DZ5 &߸ǩYqR)QV[~l19-h9->rM.D? =lr7n+_Ws,juPW~,٪ױZ֓yzlPF0O}M7]Ko`NٶЬ d d/E]'jb-GeT=:H3/ED~CL:?=_j"=Vosuʄ.~ף1%k_rx;sScNT.6pjΪ@h8?>)lkUfnq%'U?k>'9[{?FĜ9sR):p%MhQ;LEB%Iq'19kbг{G9I Iͯ`jtePJ%Y=ΐhb}n/=y/Rݒ ɏ HH-5M:7Skid*ofi۽g'ru>;o|~Ӥ<GoW &Stw&TѶ[[ڻ.O*mޭon\`rM[Rz̼֟UB+ҨUd@{v IDATBn??0žg&Ʀ66fZW(>lŌ*J SD0Mdq-K7?{XȵPWYF{t~FSon2XO[0!BS& 04Jƒo?(64*:0 h `=?B!ޭa H,:/"=x1hi.IOۚ%. BM6 VNuSW_EfJ/fx‡"Bľ)!BJWo #B0!Bmۛi6B)U)B!FBQ B Shif!B.ffN$;=Uk!Bfݻ4M9A! KMؼsB!T ]*X B!P-8!Bm)!j0LA!Pa B!( SB!FaB6 BQLeN)(Q6lofcTiғxҮ[fDtEFd¾+H{t4Xaj.L@yX% 64=_mه%'!Bͦ)UMwtp;b-I{YA+g0'-1uYH'L[Eq4o@WFf% k xTc(3'FrVB}ڔT")+`YTHP2xU*R-‹]׎ iYĴRV7젯J|kQU,"UM,,41D iP[B֔#f ;.aZDU2gzIx|/ ߛu!aC-g^sy"He5/T]],E 7Bt1ejpřq )E@,m5Qg;6о[GMH :B$1&X Ҕ cn:ؤILr,:ZY33ѱ'@|~YEw…TãffZkWSPh}i+,\~XnjVxlÿNYD鶣NZ4]ZD0xM-GBPRm jlWLDt M 4XjEą #{c15K-7`h7QehFeut!-^:=n05# -p)hF(}rʯX j++\9i5J9SaU B!|J Sh6))-.L~)fʕK$SiD凤 X ;VcAHJCyV;YR*+Rt>oOkiVg\Q,`VT@e0@ek+(AV+wHڏ7ܸgVRBOriaܓD] uI"Ԕ[PC09LQlhiETahq. L ,6CY1 q Sqqs5I Ks^J5ګ3h)իWUWqYlsZZҞGfʔjZf>b߸>J+o !Nm4ԕln](S1 =hHjO/zpfP%8*=bwt)jGv~rq& ޹X+M]cԲo~&w9uuϯ?/iai6X}ޚ5%#xC@{Bc'1(}}#vv/˫uey8 !RD+[ٰnec)%yi~94bsZiiN@ks2;kwfXeBZ޵uB/?C˾ӎ,s29 /|g'uQw\~9Ԯ`iiHi;{s"G{O҅&i0su+Yiډ ;m_ubB)u @l4m'N_ ]zNpx{k̫x$,Sl]ZAdnZ4Q lk3ڽ3bIJ2KƶX@p`8cB)tJR2r?eG=uAE*(~e v4P??hxm 5=uBiwU4x@vϺkqT|Hc&-MJkua3o mZjFȭ`+2K 53^f{hAJl.VUA B!6Хj!UT!Pk!l@IlppT! J |.,ZXaF:=}bFYԄa0hɪ/txrrpN//an껑֥Oo>u?{we p&6ikm! {ۂT/>,6vXz%mF!3gT*eXxo[)j]!Ժ͛'UG]?ϔ~AтaJ脮o!Bm M@cx!JZ*L+tiG_F @ȢxH!)?L/Ph&"ipB#d1 AtuBW+4V ޯMHhZ.:ziQj{Y8B ,j !)9L]kB4P4P4MS|Bm= T!q $$A4PUBT!e)ub*Fh@J׊T(1!Ԛ*$hY,Fa4I  @ ޻"_"hikeBUuWZQsT(@)@0d5*5.tu'zT䑏Q$T+]wu U ZEUs$A A4M4@T'aVgr?c,٥wq뭃-_Zt&YM='NoaDϣ W;N컦*N8fglee8tLwG=kJrϝȤ\K̪@n8"(-LEVQ" Dd(b.ʓť"JY[D ih&S--BCerdRO/f┶PtZN6  鱏}n-9OϮ\5$j <9}F+O}FPU(C-Y:57joy2ТY=5 I HtU@y<3ƽ)o`[/ _ 1jC4zhӞ A'epA>ڱhkH>?̝bBR={clIyaJuMո) R$4/kv2f2L۵j(Ey0ĊKII$AjQhPz"v ^E  536W7a3i?tj(ʡzp1l,zl~tP־-Ր>J\+ч)/sDl60 HJpYmxi'WKAS4!Mi>e޷ CuwONF!L3]v"z[HujQNOpzyzV_Žda.XAfrThu(}X ()ES$T(y;Ez{?Cm{Q!5@_=luټMz`k $ϬOv2o;s/2>1)V6jSNE>||u,ϘE/-tEr]G|H Cf $yO>zfx\Tu:t;f4jO1 O h{  )x˫M~uF[,=6y]\|u{Wl{Vzj80{\KV7#uzM_=ϬlPȺazҷA'H{t"`xn=z;ۦub鞵S/]1GNwZhAǖ8sw/V$+K<.A v!>~`E{tj5|`/pU[3kS_:`UzY~Buq_q'}fb?@~9Dne`cf37حE"gGQ+*U(rFٴ-.yu_@:G:Q;f:1 ֣+Y)gTQdݪ&ŋ@nB. =p<@{]-xL6/ ~[e|g=&2i|kMH}w{aS{S!Hv#=f׫݉M/_eDEiQ4o :No*֌|ʹ3c]'jW^4y]=jPg[uy\M/JVyr<ѣ;F7DMF̙3G*X4y-YdQ$H(Z,RZ(+$􎻅fp͛7O,3;4Zٳ->J5(TaW>Y="c )u.}4G\>/s%fLV /-觡A7F|B<ۉoRK_\u5 r Ly GtJeSƨ~kS5 mbi )X@ z%)/i,⚩Yҳ;2]zf,knmK~g>@~7oR/YT7ZQK)գM1 BͧQ#ȍD>z(NkL{3r7چ@pnyg׾F@fTfݟQ6 aWRDrodOJ*I Fhg`~'|+˕ÃC@n^_fSRs:رrn_s;#cA[#Zr5Y);)EBZN;^4ľzH~㻓rz|qaU!UȚ?_K6l. 3k<{hẒb;@pkivө#錵<]5N˘.0 IDAT:˞t!@g;m ߼bKkWP+[{:k*=у j3L>`-7EquԥyV{mKⷍ S.[t,'j`o_yCOiJ3Qo}kӾv/嫥ػTAw]YUN M9zؼy8p`W=#NwL˄W_϶ZΤ]a#ت Sϕ-XLxWW.ֳWS96l0W'C]Bt8ؤ w )uϭ+1ZBgH;{k*h.~;f.LIzLw= 4G5nPs46ETG*-Ff_U$uzg=z Nv՜EzpZa"+rx]5 :{١ǧ6>>%Kv[k^_^[p{[ ۙWzp:&r].sWչqw alyD]\l7+MǮ?[x$7]7;YlsM(8V#;u" HIhCjCaJݻ@e@$T|~D mkf 3.OBJ\^Z,jS-b2D *BJ҄>Ց{Qa2dw$"u?t~'57;OD]s]冐jE=TPof&4rQaǥ8 9M}H1 Bqզ3t {NJhM˾~E /,wr},S'sn}o=v>x\ss}H#}B]A)MK)P깃z=="| .Z*LBA6`=G2켹>S>z9,Qj;j##[k}S?}Pg"F)wB%'9MA;j!B %jS[F!eQ~Rl{f;{ M~mZɚ{>MKiPX ")]!z۝EF GO$TA$ dA}QNm Q$8cm!;aЇ2L!~ڭ%ЧCaJe/ $A kZ~e (nVTDP "{""-"nPQPQq  e-]i?e^&{r&18 |jT/?nd`|)6e`;i4bcccc0ik& J R/?ooJa4 h19ҨDAi!/\,]V&J2^;T Gba@$I-6>4 -ϔSa8p +E AO+S0$  #IuE6lB&de(U.Pʎ£ h8б/5+{U?uo  uc$aF$F` j I $C`_V~u=KRv"{L-ڿmV3naX@7ŢG[8lvj!_ 0FqO,:԰=Y< \~ArJ,;W,۫RViYkLIꝨakT[_-lO]£ȿNeYA6wQCΘ+'گ >*eXLoz_D F4l+a7bv/.[c;t爎4Yֳ7SjW&SMDtW~ WB=?`֘."KLKgSKJdZ\x5&Srg]3eѡއFPQjPu`w-T_U@&3N*} H#C"!um9Hq C{5:g!cҸy”B˪ Bħ[ŬF6@B@xmdTŘQeg2r.Ǐos{UbzYT^$\AE 1:U*ʞJҫZ"w0,_n$Nyp)zwyrS6%-VϏS«RE)1bՏ빣rEȝ2_}GrMlGQzI?]()./Ә 1VS z٨Ks A( o)_U/MT_T vZܒJSMIS_\|_2@Z)$-$+> YǞ<<\`@a ^f.k2uv'p5[y^K]L>8tR5y@ 2KX*7i*C5MVyEɢȏ^>ߞcp4ESѫ7:W-r:!U\-KoWTDrղo7hKM[65+)";-1S² ə"@ P:.T_s mm0B/jj;cޥdes8%ߡU dS[{o5eFҋ >?~dp۷_K*:.gn_6YXʟ'M hthz.S}SRpu[s!kFQ=TiKIiR"$*jLL<@LiJz]«jZ:sTuU֞L*c3SmЌ{TY%ֽ BkTh9o؛ Rn=[CN3UN6s1 C'f \hf3yOoMnn#ISr 5`M>>3td5=N/z (p=Gy/om`h3}w+Y0{"%#ΰU'4gBzmIjC -;+*uaUSwӢqӬ`ռz[ Së-U!E=6|:GU{o D\)P\cw=7Vezc~d:'yzz`0v c@鿼JU'+.? H$r sAb^{o1퇷F5 W2A?BK/A~Ah&ʫr_1ho  *SAiP  H#Ue8TggY{__ۭ;ދ~I`UHuvv_Pw.ܝL\ '@~Y*4c4dn~ؒe}{,a 2Etxᅭ\4^"Pi9o|W-Or`ŇbI<jD.ZyWWL;&$'9L܉,5]9t(qbmС/K$=!QoҊ:m\'6dVHdOQZurh}3FiؽEMpih"V3'Yҁ,\5$ ɏ }I:،|dRYU5i>ڻ4WVKT  HBx|(غCw:5NÓ>`7/Sxh\vCt~~>ֈ w>: dc\b˩OgVmRnƻ+Vͯݕ7@@otjP)joRds!h%L*o ѣaFE2`H=j7;#<|W?ioTiŷbڹIB:. tЧɬgNLKKfޝl5}=oqFm}~[S"R5˯%dpِҵ P5eK[%I{q1ތmUE!Z+tW||9f+}e_874-+WH C䤟*}\M;f)˵)0f3N;3ftA~u8d1$HcȮƼgw[p ȊOT7mCR5&-,`0vˡ^ӽ6Jn8æmN@,z8]lGq+.M9l5t @-Gţ\:+{w erެHxE5+!m2vB@K~;5twڏ>DV!K7gޱOb rn?l442J=+Tt6:)7rp󡄔eҫ @aMq6ώ80-L9y suv]+rkglM?}#tQkgly!8"ef@1az'[5u2~K0 RwJmƣZ/ NgcbO$'\>r5z8>uAyŏ7n0j@;GWbât^(0Ï_C!tZ8Nxe+Y z{G9Nh-sajvښ``5QN>$pKy̑qE}-H. 0C+몷0{Qͩ6;I@k$e q'_:)z3 XR[E]zkg}o-8]2DNsv|G<,au5ޫ23 uX)S{k GzORmyw}g.sZ?åAWa&E䗲aX7SY,zRK ŬF}Lcu/ELS%24UIaIr;kb>{MGIqynrO(,Ukn2xTYݪ!eHck1$tdrfhj(Sfv4lלKŖnUFet˗ZգENkv C*`d=z86^{Oy}S_Ԡ/ku !yJ N0jA0O9ֻ /JkAEUUU̺8AW2E(ikkkiihNg5|JH$C6mtt= >V@\.WWWWMMNco24v奥y' ȯWeJqqɤhLA@&#'OҪǠAShID5 K$A?Z=]aQA.dAA)T  H2AAF ) 4RLAABe LVX")&: AP /Bgo=_/n~VoϹ fWbڻ[=#?Uc-Sğάt]k}Z5Kf+#(;za(묛v_A&<(WYq8fv/I|x4-};=(*>!J ̝kDOzɝ<րe9r`>^SE,3!a{a߹Do(F#A) w-w}5),Sn=jhc-//?*"NuSYs,3!~a뙨34mI#4`+kY[A##?5cʻ(?>3g%ǪߘeodcfN.)`+W-^H՚$3f˔6]9?mY(ͿkD1ucx+}QDX{s:K{EߖT-(qE\^9h0n埨D;sd4'VM;ں8{}oKsN8r1{pON>.ɨJQORnܫ c{ļϋdT3Bv|"3b#-SȒWY=G83PUbiojN]˦svvs+UTH)Qco'WP^DvSm?iۢ?<7}c@31}KʾO1e%F{%liVPP:tnUn&RLI`h_@rkpiC}(#ÌՇvճa=LܞBk([u:%*ֵ~_-G >KK]xL2N+>akה8dhsG6۰Vϭ5/5% ~('l8"0kg0Œcڒ7Ki w[Mg`PRf)(T{7Eީ>;/>Lɀ8R[lOG][^|(?_XGȐ6;m~!1s0]C}8*3E1az'[\ᴃ"MWq W|E Qc l{!`TK^5wZaMq6ώװZ%rn?l442J=+a'q#uiXMnϋJ8*R^{殾SHȉzGO~^Ԫ TQ`?lMCCQэ4, >C1SiȲe$ukϤ̦R&FXxvn?NC?S.Fƣo1a tfRqݩDѵNW(T"~㓠h̏K#-9Oj9E2K6MS;kf[au[fu4l޳[Ƙ^ܫ ^C,1PU7vDB OGƷ>Dճlԡu 3tةjptJ35U F8.Ng^f&O5+v_co^+:o~'c+uZIk:|[A]I. iI&lUeS 8z4fjVddž<|:6Y>+]eEE&( Rxj|96zX0Awoz:hB>I[Ka#RË^\xvP[:G{[cR܆FR}dt&t6s^ IB1|v _g;RU]i e*fWP K %&,b"3Z-D q Q=uK_C( #+/!o؎o;sQ &ARPWf$(,=,,]*3ӷ,e0(jlJp}-: G;(HB<,ťjM>]+(4zWFNUJ0cC&`fLHf@ּ5Ie0o Co. 9>'L9/!D|UIyC(>剋YMXeo1mݹ S\ l-WnjPޚv3-FJqٖKw<~l|#ݽVRFXBiT[L] ]0,a~ӕTɂRЩ4+CnJk+~Iƕj6R +0uc"r)f@}~;+څ]\4֭OH'$bngH /aD~? Ѳ E}&-/2ޱ뷱ӒB)_@O-^^zu<c`ѕVX^nT $ -(Q2<34 4eϩQ*ӯt5Y>/{ ,R^uMn+:}׋-Q*=F~٪ Sh ޿BFI򷷀$i3MX|vN4,][rj.,ZtP1\ NMYL.鲿l41Jw%0f RWzp &,QbY$*gXY$(a ZPOŴ8&5}1VIBf)^~ίx^d?A#K+59(4akTOpP* -8'! `WpMS5;r]^ĐR=L²$Y9o@4ZGN,?zz~{n Q̠ }nZ]FzTgiWj׬eP3C5egi(oXFx4Oik)f vgSN^|){mciY2e@3E Vd~9DRΓlk:ڄ (P5+ ZfC>|V;%S SR}:|.I. u"1qaɍwŹſ|\p 9:: $A /vi$ꮕuz #s& 0ݖ=0~idՉ2u1^ ɒwj[qUKVy}{~TqnO|՞^GMn2AL)=ŷܺ>@JCEM'rfd+ฎ--62EyNA/>M+LhL2sN>(Ņ K\JhbTF=&]{$*^v j`+ ~eThbgw<~j1?VS򛡵nܩP3Dƻayy_>U[K~̸S]u OK;Dn^PrKYy&C %܈dҺH,(SմW2C4 ^llll}y٬Ν;kii $wR:mIgf; zܱ -N[x_i:f,v1}/'a`s}SX,VV\nff&AMAꉒ^ާ4MMMȈe"EYZIQʵv=Oc&|&ݚ2lOgQ OOҥx"P.TTiщ:f ?"65ƬnƠHTmίqlӍoGbBb>LZ;%xg-˝ TdgRFN"eȺ.9u2 _5OOω'zyy I6`ꗥ$IJRκWB9ᓭ%4փĉHc*%{ }Ur`w"IDOEwwmVac^=|IR(d黭CXxzۺsgc\Qe}{y+0g|K_r靐$IiZk}}ܳϊDǪr{swzR~!ގ =l*(zST>wN4GO'ܼOw'Z{ʧbHFs+NeZպd&ݙ3j҂7'Xv2f|A AD=S*ڊiݗ:gCBeGTKu8qiЕ>4"cKUVTd(auhb 2o_eg@#nxe4i*-gqR@LudͤOJ ʆ'S) iMOv4c+hk"p:23y2AN/la8Yhwr[0Ƙ +=tKyqUuS۩HoG$j3e15ZJU70β5;դ#ٱ!xoV=CrZWϲQ!}GY-ՖUZ(`bc8Z-zt7bupծ8_@dET) ؒSlT O@ԸT7t|A r^y(]]fDa& -,fX")1ת%,(|; +|q=Yu0Ѥ ؖKw<~l|#ݽ~m(,Ukn*:̛@)=@t˂jA/Ӕ]L_Ktb)՛DƥbVS#Vٔ0mu 7繊jqUVWPQpf՘jSHI25ZrꞍjQvTR`P H!ˏ^>ߞcpp amy;Sj4U=edaŦ mQlQ{Ff!_YћW2޲U_7SqS?'H}pdy&Y|fKHL @#bXX, IDAT誳Z=yx2>7!S%J5` ~wLnUa3cuNG[d5|aԠ6Ue1[vV 1mF$KBP02<1ct&o魉U0L] Ǧ0td}GHusUc}OVZ2YJ1(Uj_k7٠ꨖ`A0ݻw7t0 H=SF-o69g["ʡ) 4RLAAB}Ai,AAAP  H#AA)T  H2AAF )_v^Dz[wJ*}.I;n{66AJуVh:f,}OjA‹:A4DޣtX%:ML:n=tWC|lڬ!HQJx'kN[b p2Q @dl>ĆX9yʮ@>~ܾ\ ܓӭˢ#/y2Rru^]8#^$)`+W-Dz8!9fi{YVA6(߈t ɚrRʴToPTT2)**UTTTTTTUU:0A,DV!K7gޱOb rn?l442J=+aݹu۾~_u?}ͭ|{ \'GD.EA;GmuƿWyQ2;<:O7Ef@1az;ٷ.vҵUS(N;rnWz$1xؚf:F袶/VgLi-BEUUkeMAAY7O2`T]3gu8)I@ҚhVlcoD˗ |~,FKCI.%LLkХ +VgYϚfLCjؐGM!znSr @>Ox?K)W Toeǀ*e49i_]}Q 24X")04L4VMX?0VS zٰxo7r.Ǐos{ժg5Fc!c3KHi +0uc"KS\.R ct6| 0'L9/lnUBl[gShҏg֬ Op[c57zt…I~wtpۜTenfe߫Pji,hz<ŠM %,s/Gݿ&gX?s߁~G.Wjr~Q[JI 3d瀚'݄ ,PQY$ˏ3@j=z8܄L2(z z=C O'BKG`a yz㦯3J3|Eӳui/웜ۂlP -U='M h; &p{֞[& 5e&&~t|}P &+BJj 2#xgEc?h6-5 5·k< z,S$ٷ8Vd4gK+6 tf /톎am̎}[P^@ = [E9}.cjd??t Gbh+PhWz;L ΰ_yB:\FԷ9to9γ8ަ[U+ O0r-ɛ|zki^8E̜~_V5ZzbN5k@N4vawSwjBPP85ll `A0ݻw>ROvYsh |uʻx;U|::{wϐnj5[$ hA\ps-o-*?7Ei4 )t 4V>MQlSm8gr"p{_M+fqx飔7`de@F1U ,Y[@*d?<0e* /ST  HVerQ-li 6Ǟ^]U5{ZhH'E%5*}Rx$52Fwӣ S0*%t>_S,쭓ufIi }Shݼ 2ݠdbbâyz=O<ܺoG%Ⱦs7{wӯs1E8)#e݆ ȟm94: .bHToϾX-]Ml >_{,%;}7~?{k0v˾MmܸB[FG&-0̗ŨPAAH$.'A:бUW:AA.wwwTZv, ɲQν%ڨFAA?S,SYw 9c'Y*AATLa6ĆAA  H#AA)T  H2AAF ) 4RLAA E"R~zDtUUU###&б G2E(ikkkiihvm"2a\nnn^Ν]"(//Çmڴݕ3 ꪩt G]qܓ'O^ ILKK311}!Ԡ>ʔb&IP 5ɓ---zA8$IT D"i(^/0T R}q I#sLS_X#9uYL{cOc{y'uV; |/v*I;nAfٺOPΐQ$G+9c+Hu=NɯP2\ 7Lfxxlr (C'9oKq:0Aƈa\cF흱b٫;$^-ǭ)CkuVCL;Q)X;a}:EʾHBH-IQKfI%JD!,ݞd˾GEvN,-s*\s?{s}gμg杙V_7:ioxa9%IܢBV  3+CVۙ2goO)Y0&o>uyȝ*/9y.tl4s}&kbp"@X(Ґ5^J.1dr~9kAū蘳>pSyW 36!_nk:dLȐL&1 .-LSϕ^h1fbgW d>`idp|1N/?D?jȧB^q s#&ܫ+[XxiX5Xgۄ^}ՈL$Zy+u`۩w\!v0'U Fδ]>LO;AJqLd\ywX3=9)ofkׂ SWg[0ۭmr]dcY g.?LB 7\p_8A3Ǭd59xϥ. NY^YDⱫɉ-> {#s;т'6`CՓ݋wƾdϵ7Sr`d1k?tpnXVJ4SOGVoOU fRfXvrGxfūKntg/tN}p/N  X1w]A=e/mٛG8yw$sCg{VqUVUI^MYR>IVǤ[iyZd onoqdO c/ü2xZ3NTRv8ʇɨհЉ}ʮ&eq@Pp‡NFiR7$II8;}΄C<# I'V~HzW+m\; %v,>t& -O-[v$>!M1f2o4!@VwuP‚5"pk W₝UzaAB@ܬm~SO8x0'+!O_Ľֳ}ɻ/$QdӍRpUZ)H~"/(J {udBBT'-t}HYMMJ?W;HPt*EpN-< H=W Q?FL]fGyf@5>54d($Z+)YꛋEUBYڸJI\d c=gsNg//&[xFx{tMdD_ =Luz;tB!]aHYGnT}nݨM[UO‰[ܠaZY}RufO4i/O %IR'O}iADȫJtI18.5df"oL: }/Z%Hm~JZU+X @S$G^ryN%ĸ|/2Mt9Y1 L!%H$\DH4j4:DbܟhTs@yy-ywy'fs9GGEܽ)fQ+]0|ZV̭`h3j4 jpL2_𕏳.e؁$~NbŘd9N8TWt`|HSDkd$1)1` H0jn"4BBS[G;)P\)b<-[Q--`]Q4(50:M"Y5M*=;|zyJAsNcx$xŋ4(JW/` ,aU7\q]|AA, >R]p8 L=5P2tC =;ǩ~:d;%8[tmiiG~Ұ  >f;-*"돔" ,,P󢐧߉MN'Zgܨ*q&{׌ Ni+tdf:jL!+m>UaT TgFT1A4t'z(.Z?.CH’'}udi *IQ,{waܢE<ǁE m~ޏp?(貖Α%wdU"E#FտɦE]>&ME $A!X(I.rU9欝F}xز(ġ.C0FgViLU8AQ򙫮|i1=sEf=?YQ *csfOu9]=/wE[OzP)bǗ//_"$6{~ђ2LR78n^Ð(#'%AE>V8-:WRe4ֹo9Z[-3׃z8ܪuG?.,.y;3ZG$?ln^_>-_ OI,Y.~|j*73y`;NXsE qT)I-L?r6L>ı ;c^?DN?\i]OP{g_bfYE^zޤRICV6U~y'x!LQׅ^ d_ߛ$-YSuMhU^;'a焥Rb0Tw#KjtM!VZPȝ/-t5OcBzp^U_kh>UrW\Msՙ;da:9F[L!5z*c7Ecn5=}Ebj'ǚx]s< C*E" 'x{WJaKC %,,q}*gʴuR*GAQ*˞D}EWc2}rpYxPq͎[7+8m?LW}H;ob\w.F(;`x>tsXZ}zd֑9~ܱ=0nޭO^}QX ͷE;}.ڐ=;mo 5`jjSd4qƽ{8{peih2H'Mn_r)۸ު f`'~KYo~+uɸ]B#k@988|2L&I$R>dE=;tv3*뻷#W=rgbZඋ Nc㜬/:wH"ЦLw#!t}uhSKx|3iK'@ۓy,\|BPaρ#vlXjq<’kzX"aaG ;sU<7x={ :ff{ٳw_wvub,twrr&͓I߇4QB!;3Hxyʗ3Wޞ>6`[.XxYHW1mMGuuxtP%IJkÎio%dasn/ 9t*?_owP*C{%w3Y 'G]/kBAeaiu޹15ǡ w7 Nh'vfy}>2,uU<3s]&&jxgMNC'i_T~eY/|!S)hWނ0NZ,k80Fw֎HJo¿M?n/=|LdIqn`8|e;JE}%7uWP 3^ANO=gĮ/rtZ/]vcq=z̺xMd0v=wl,+;VV ]e7Ppy[_9  Mk}$և$7tiY cb wojlj#&~ǘ6FfHv}Uiyw8HͧvVe:*do<AaJy%7ϤS:0 [Q zRM]&vFs AisSz#mt$[+ Ve ^rq蘀&#An47*8?'dQ*b֭v'>6dqMse.1=ݒ=kng[*^I&҈ykn#AMs=).66.1YЍ A0}ŀ_]7=H6HM$4AV'ȍi|w Lޱ<ϵ15\] gP9q̪zO^?ސd[Zxxuvsݺ9\xL1cOt۝~QkTfO2_]t8CVGaL])O+(69&h:aOFͱge}BT&Y -Tv6T1nB_#]En [oU{*I )q]BTv]!]b&SUZVuZH*W_&gp!ʳh]]Fi3hr= xln,$%by.d6:N>wŖUN[6] k 15۵;Q%Aqa^cSqxXFRLG9+'K=u&$B7Nr2sVHFMMXJ8Vɯf'kZ;[tmjґnre5̳[`3ouV35uOm@R(a??J 1CIbRb$0F`LcǾ#s/?/d5$q _]!ʨ9;)PAP[>8*fEѯ=]}`Yn0a4 01 71Դ%YuXu*iJݕhW#u@E #숐F%zer$dү}`\Ó.__FSDJ-i"{@B^_![@TѪJ5T9-o[UVV|.k jKF~ہ;R03F&P0L(l#8Aj+..n(jKRT >nԑAb-,¨|_cJxd)%YFZ+@y/ŨU㇪.>>iV:'`X!YZY{'L/AY.Rx_j[2eo?4 [ŽuDؑ)AD}wY>cKy.4 [3x_sj.%IrQak1F5(Ƚ&"U>yɕd0*n?"! 8W~/ :FV,q;1NUKII8z 4-555 `Z&[\\2-6 qT)I-L?rҺhϾ̲I°iyA~'~AxNe02EįLmw۸=U[ug(%eU{,лM?UQi;Nc:Bo$uF1ɾ~Lz4Ѿ^ȺkJfIIC83:4=M|WR},?A6FPj TVEp+`] DoijA# wZm>βx<7oBBCF&5555vsy?Lٳ'Fnj$y&Z<CaSg]mDu%v0]/Ϝ`)D7/ܺu$\k'SvU^ۧP{S UnQyǢ1ՙ7ՕF^pku4dy:*"qwbf.݌ ۥEbTaaTk&$LB[A+Ht7qԓ&1I w5'}jL&$*&&ֵkזi AQw3z>^-+zw*tmz&< |ţ*IRZCvN4QTٽЁOA[` h>  MwѧG  _ SAi0AA6 SAi0AA6 SiC9v&RY m  Fa }`=s{ӑLJ#Ύ>j`&ShGr!r#gY{jsTK9\D?jȧ5/^u4`2Z8nY^~  :x٧NRs;~Y큄8ydU1) '}ٙP(AQRНޫ#Bw?a rWxq ecWe~2JI\'c 6 H) QwVsX4J\cL7=$t8Nִv,!&Ԥ#3:+6u GwW6i,KSîJN_1*-m܌-mu+uA_ HfWHh2j~+;)Pk}9ޗcZWP)_RSONCS[=c5xs9GGE,T(a ,¨|_cJx>x-=|ܨ#N[6nǷ0VeQA@uftMUi]df:jL!+mzQ0; B jK,KߛTZdR24[~"v%[BӶzwo|f)tiyA~'~Ax>KB2]D䷡a yufv'S(!yvxĥ'O.ݖQEPUnff]s޾xH&K+'ɔK~޻R -OA~foo/(JXXXk _{+魺pmyg@֎ Ad2L&Hl (̋J{$cڅ1!Ҷ0}j)Sz])KsKL}߄_3eʬ8ͫNgS8{n^D?aF]; . ߤf_ZJWl8$:雎(US~LT%9ookǁ Hrλ k; Ʃ*ns~ba_qAAaYj|{Ye]UC+?i6esYoS3q4ҁ,͜6sQzv@|.qƁ_gجZ~4T^xRٌc?UFEY @TQSPǼ*XɾT kR$xx+޼/~mgﰡÄ5M{ 1|5ݷqJ'-Zj&+c<bkeh"]9" [\2.gT0>\ObEׯdMݕlDꍻ恖nXXᝣ!!A15iN5cb"LUށ tp͗JōI3ס>yx?={iZϘƾf ˖Yvp++j=1:vWpqY 1h %l~ֵ~T.)`A"GBagCqɭ̩]{w5ՕqX6 :V$sߟ ~ӨCKDWؗyo>tp>=/ )K#LbŠ>UOnp˓[f4 ,"P7Hs^W1JˉG,P՟gdcA/ja0- wܑxyfGiofb2DJR#.vYpQ@ƾ1)r1#6#\B#T~I/^#߻s&t=wY ݷ+0dXG='wݍ޴H*#~5M]>'}˩K\L~#q3GShA^ MLP:A5X^p6*l@n2g))Op]@w"B嚉Ԯ&}]-{VF=$!/BZ!0#JhQ{|e?yĉ'^|Irz׻Err5)l:lPǴ -U9uDyR#Et2AAƺiY{QFAs=,lX7s]tAAZ4_pxus4JAA)u}֎AAN  Qh  H) Qh  H) Qh H92LU²tL&SN !<ٙY۰QW?,:/ tJ,y.f1du>vX p;E&K+Ս,FN3gV#}3]Vk+.dsok'|2M2Y] ?֯ǖԒykq-tCruuuNN-" BHKKhig==`a{jygj-DP:%\$/يW 1̟ѫę%Le^  `p[-aً;-6 ֤@geo{_kR1!u͇ :s[dlZkO1;Lajm/n V'}yYk-Rc|&aYV D}fkCVJ|ERVVN 'o4Y/b{MJuv|0I-k}f#^rg7s6vtߓa#Ԭº}amY.ޑ9aWc/o$4,E΋tp~:E،iJmo\dD~:oLK Srrrddd( l!ðO>iiix.uxiF~$WݗetZ}>s|ˤHǿf  RJR{;&VjMRrTnR_!T Ȫ*S%)Y$Yn֊͜d>l@gE1 o a-Ev;VY}ƙ!tr +.^D#z݌YkwFyRV\YGO >R} f]4lb?Lֺf us<7U>&rWlly|Gq 7$9@2#lʝ c傄kc5"v]QvcZ᎝oAntiXjEIAwD.ԉƬ`i q0Z&AWZ A&sGyo2!xMlaE9/_u6z jMe膰zgwStvJp6a,~r޳t0|s\3.8tUeqꨵ2 bZw@"TUp~TP8PzSvF. Ri0e4M^gܑ*,c7y ! IL^U]Cc$kIeF$vڲq;_3,,ɽU&З JFZG,ruY k4n}E5E`.9|S'ه1UZi|hpy1~&vݐ H#w0]?S JV/;3JJOD߸{ls0Lz$}^˅DRE]^+B/pr4ZSXp޹U^]X\,)tw,Kg4I~4ݼ r˿|H [j qu趱*dLR]LԚi~qH9i/D>PTk]I^G ,ei`o˞6 ɛx;/d2犒k'TRvl6mf8z%90h*1NܭQmVnʠ,LDYL@r%rX<]]_lLuXf(A*aaG ;A!|F) (I;mfz_߽=b>]̕`ڥnZ.R>iBKӂ:L9pN> K-nANrpuB0}ŀL-7m0#pߟpo06O?:Znʟ;{ԖoP9EQ8s.(Adu-K^u1,}X2]7rӉ)҄#(u`۩w\F&`>r2L}˹W :\and{lϵ15\(*D?jȧB!Y_K&s拙U8Qal;B:)8oĚI (Nb5;֣ޯe dyna$] gPo|u|Wo-߾2"*D#=3 Wg[0ۭQm-myRYAwt|ag<Bĸ#0fJbBϗ[ y$ '1gUTWZiEIAwzLHuJ]q<}VPlrLt,zÞ 6UkJjG0U]Ƥ$>dgB*An 2A" {`OQ)Idlqߝ.ohav aw~24R  fͲ•`gߋGHa&qDO-[v$>!M1fz*~t- 5=q 3)aNWCV5v4LBO <~"kRk֓_Day&3VV ]e7Ppy[vpAR8Y٢|oS,!B%˄.DyvL7=$t8UU+kݴPaW%T6v]nƿT ;qnO._nH\9X}β:JR,,wBfSQxI80.De[nE^ZMa(oh^?v AHM>Z{Q \CC5^1)&[ksSJۏY~èmGG͊]6I Yuty82ZA~UNKfwx]}`Yn0atK}y1&%Y} Z9 MuFM4 ñŜ#"k YJzS=k忮/)}'(ϯj~HPӖd}5O'$DIqTHh~kH5^fUXO.5d!e70Rݩ MEw]]AauBU]l}Bco^;-!,IZzfxTdni=1UU5;`RJ^N%$Ş oQbTyMBAoe$7/D:v~V%CEW8N~I09yFeN׆?lNXYTD!5z`"dH 2ڑ Pqo|2Wg=Z8#JH0nae6MZ޽ǧG)æ|[CjRYJFRbhmdec~iQ嗷q{"P=JLfU?8t^}qGһ jK,KߛT 1kqO3 Y#XSPEҬ<&꿏$"Nyo[;AHO5545o^O1 zӄ5w9Fݮ/e`:qEnKeTEն۱,?hE䜷/I&#|(x2~+OP71M; j#9AywJ#^yshP<F3$5͏yufv'SwƏ&\'i הPSy_I3Lq>sj:::T*Տ  <SSA\XHJzn4\pY/em<;PAy5|>L&dDRa$*o c= =R(dgv{niA(hʝZ;A%к:lqY<:$5a2(_gdc4֍Ai!ook_}a&9rrhJ %&%%UVV֡C#RSSSSo7]IKKX< 0E]]͛7BPAA8C1g^ jjj- H=-1La0={:"H%.055)LQWW=zX׮][-AA" Πa hHO>|Z ܟ6p;Ei7Tj' fP+_܊_B}A/Huo5#U~v=DZ.`>=m5!wbO;.m֗8?YT)l {ylBQg}, ̡/fVݯ:p΅š2aIYY999yEFhooqZdMyX.AɭN?'|slua)^޹y_MLz\and{l8E:^ ^koillbus>nu5ijJ\+uYϳ3]~QYgW C<1u'N^AnLKψStMlgdNh]9s̚؋~NV#\a7~#k%ZyS66&LQD>-­ 3I9?ԃ)&:Y^wu{W$,7*%)ry-3=H{SI&+߃ ٙWgp)@XqCއL(s$7UljRNUSa3sD%i| 88aT 8x0'+!ODž75IH:,8Wll_8w g|G/t{1kE;$8SDzW+m\; %v,>t&+uAQRНޫ#BwPH0 ?>Q따dG+<8]N2+MJIG2Ʌ:4LAzEZ?waW%T6v]nƿk`~j;r/݁qZ2L&Sy@Lk4v"!#$i$q-ɒ%d(ɚ%;ٲK%$dhߴ7#r^rGsys{Μ%3|ϑ=Tt$.c7!ӘD_PZ +he[XhK|q}I~lEY5S@b{ͷ e6kT…Wy ]L֜|b7E^}ePĤMTw2XݵR/KSe^)[(w4^OJBƺ3+K܍ȉ7 n gFv_5@IQ|f/ y[ߖ#-Cצ ~p>cɻ9wӠGy%#Bj!E1JfFex#,cEѝÕHh22ڀ$ȾL^KGo\1`NJ/`ZZ1qiqGT%mJm$꺫*ŨbB'~-ۢj=: U + hSWCCWW)2l^!$d$Ha$"Š|Kl} &&NC0XA3#(Ϯbhk2jCiYr|뇏 ) TlZu5;S2*SCO;@N+aט~ɭV₇Vi(ׯ7O߽l*HCL.ȫ,pjٴEg3`ҽ)RS9B4 %?֭% Alͨ]Pѻ+0cqy;g 1kf=:lȪVzymquYZ=/:ژCLi:X2P;m7P$@ ]T.6~j:VLAC )Y4dh8~k~T<`J&ф\-9d|S\-~snJNo.l{]ZZ45pqkIb$VoE1}КD7ݾ9zѢ_KBc(v>qfZgXvOVy+\^q@#LmX{">G  ߡٔÃnf{x >@sCkȰaCiR??:p?-6^ֵ.%I{vQ$U9=J4  ɼӋWt,L7;i8+vr:&#rC6_ N'=vfRf$n<L1Sn=K/JN3ɴ_ 1os(]gO1נVWDoƽ3ҏ.\]ch=}W)sI_p?zY*sB&HERo:d u߼鳿=Y[@{uмƱ9uKGN/Fۺ48 P̙2-Ysa?p?_%@f]z DWynԲn&z| IJ)"Ԍ{k@ŧA>pzn۩'ܩih'4[- IDATy}$=}&}:pqTߑF T(zy}O[ք%{O?05B_;a}iǍׂ^N`R޿\`.º߲ߊ[jwPr+[IC.Uc| |YhEOl<430c*]h)}"NI2Rb bKK݆%Q+WlΜ_HUix3Tm' %xۆn.6od[ Ayz6.[6)]W-:S%ҭ.=G5[FRSS!4$ n X2簾rqOnCi+V%L5G׷awAzؼy$yW݌{;@Fsd59xԳj_,Q^ 79X%*ذV.!_98la5V AIF5ޚ@jw,  I$ 02@|Hh+rUbP5XMV` ck 'Ț}t?RthG*JsNȱ97!6*aЕO H[IHHH$w4Ҙ*;/Uٽ+g?kZ >_s NW-/54@EQv$ECKЇyנܯ3Ʉ-H菟g`CV$)%)RA?M 'CeM-a4Y$I̗"%OK~'%/HęEA e ^Ž_$Y 7QUpR$ PWˈy9 YuCKRmS¤m?F[+%Nb\f' ;1GfLuΐ'p!E̮œBHe>|RѦĆե.EücGZ+=m˶~ ?\jXy]fǾUWG9_mBA:ȴ}{/]wiȏ {);cd79ֻ7P:]ܷq* Ἄ#եVʹKumfo\f,#?&& չ yýWRl1c:im1QRTӪmJ8> M%#Ic=upg! 3ãڔ#Gw0Q,AwuILXxk3=Ol$ޑ5GTE'pHdHX{> MA>#5~N ȿܹs|~ݵ)3o}}phf_Q/S^>s=HtV H&n5DOMP6|_6i^qL&i:+wgtf2w+?yod[n]# d?_gdZ$@ˏ?~(4=̳7du^jK?nc?a+a3Y֭{O' q\%:ȵ)i]-@$e ; T&qn^_+Ѧ},nܨ:ZF)"܏ ɷ,]NpMl wpU_}Y+!4`CgK XGw(Rg`zL_͙Lu qc2N|U>󧛝4v:XªQ7¯7A,\KXQCOAl#?^e27l~0|R ;LLǃ?K=Ȭop$zs̡n+%d}W[S ;b 9 d2:q;{&WDT4W~m)ie=?,S/k?lfFMaV%֦LSֆ3^wc_(cL/+EaCF]93`IKkY6~a.QCK~{U?ڒ? %xEyꇋ-o/ B$v4?ol.a^F p`5V6 NM/ǜ̦_+ ``gn_j?6WAe JSJ cvN0~Nf JQOz v.w7Mern5;ΕW']t:Jon[[m}Rي7xs`:coYnHJ`z+-?>S: XhѢE]M( *aΆKϫ9Ns3|ߏ—B:ԁ7bN=J}3; **M?C7Vrgݡ}劀fTQ Z_Yij½uc& ƪɹݎc{+CƫhlpNZTic#Q|>+Fqљ<>em*D- \d]rrbRGc HTž̔'%%%%=b(gfJaap{GgE>蠧kBebv}n>@Ql)+.M,&ո,a-S^N%){Nנ0jO䓚)yh|5ُ3l¤@͈>ťհ>=(09>C\`5 Mڼ Bw ~ /A1iIMAJ>Sn@fU"Hx ٨Y<0pԠIblH7!+IRQ´.i DʾV -n)-1O#x6s+@Nv올'-) kk$ospi @D\()Ϋyu|)&皎 84%69s/I+dtR{hrY3`(U15/Ѵ,Fk5Fd{} &#]c@R8(轜OnnщDKA(JLSBJj%RQȲ>(T)]NS/ KaeAM]W <ЕfU HVׯ6}- if=1cI*c aT%mJm{$1Z'}%*MP H;ݿ$;u⢨) I ?ªdU kzI.G7¼/o xfԚ/rme2|AKth5,Wޫx_1Hג(JYDo t1j eIQvPqN˺vcszEU=ͬIpvAf5C_[5Ѡ4UAdQ5ݵʏGEqq?Pa/ɾ1_A}rcA{ΥIIIIII BYշc!7q9JvenJ}Vhf.us?. +13$2BL3ݎ16 L#OMRuS oٿ{u5: QJكko <ϑx;9. opYi(Oǿ**)~7edz7W% =VVz~F"n9' I[f4v@4aot%N0gb0TF)SY Pp<')OXwȣ' 0F79hd5GV I>FY0aҢtEo Sܳ)cERu]wn-ik1r-;eL=ƭ}V>$E◌0悋˶<ө/?2X%& .m!Ak5hB2tEk>\>'G5݅W\p>~D&?gAuu )t[w7.Ob'X7UEjίxVQQP(/BqN@nnN{s#]cc[͛i k8ٓޱ m+ʔ*uuuF&Q $INN.33c$V'h(Vb@_q -FAad2YRRw,o`jYA:dAA:(T  A2AA ) tPLA$>j8c?a)\V7X\Կu7+a3Ym?LO`DdV7?NVhv0<&뒓7|T<޺p_萕F.-]pE4q+6U~m(VAсN5>U)N JrԄ5v~MkFN),kdMMzGC$R*ztRܒdQ7UI&IbNWjNz^Y hk Y?0Qo=W/ʫ)Ӂ-`Hb2H s6]d10 Ӊ^\ۨ=~fDHjG(|fO"4aὋɚ.O4V!"[ceNvm'ECijpLu,OaVeZb64met3)^U6py ݖO1^RޞV2Z=1bVςqSTXy1lD_ *Dz 3HCjE kyDҌsPƹyUZU|I/VWAŧ/-b__x'eއٟ0oHY\8'/e&&_2u.di%F&fWI4U{K{[(i4`X',cw~_kHW) zjhj=: Un+(Ϯbhk2jiZĽ4!,b ]iV^ :o!7Yoߒ.?lϑ3>2ys4pRBXUP#Um**uD]*TH %#BjrYj˛ Gtu-5yƜ#e\"nK5J`E^0*ʹ-AсA $%ER,`EJ;u&EP3윟YWjif3͵5 Ssu0Fg} i r"Q;+QdPdx: ZXeU ,n@ "E!xQ\ܰA*I q#$nω{i,*{Y!4"j #5F="ɢktk&Im}N%YetEZvbD.OxbJZSCO;@N+a4]FrO yrf*+- nK5JJ0T'[41 <yK\)TFLy58# ѥɒRTA _{]?O.GXC_ sT9i+J<q@ĎqfZgHb[4b[ w,ӋPʹ?=cz߲!޼./ei7+(f][^.Pi ) dFqdfh?\d5#X0iqYn}sM H1 icI³ F۹lHZg Yo ]08 IDATSܳ) jmcs{HR!aMDA<"^έݱE+H0PHR9Tj?FU|(wU$Jr>}h 2D*yqy1s~]7(?x})EO+zXS$=L7Ԑ|[9h%񣢮T(xM®Ƃi(R%_hj@zDC󓓓+;e%,ww]j /"4w\>OC2]Xf )2 j4hHZxfLdO! u2HJ]z+u!FTioT5P5ڟX[PBI\ʒ:f:67Tn]ڟͬuAAAӤvǚEA :%  6)ekAA_ͦ  A2AA ) tPLAABe  ԯ(S0 7Z A_QTTT8p/--m@Oef~~~aa!!x2 A_q{7ѣG삂P zD rrr=zh i.]v5}!   HAA:(T  A2AA )nJtd2^_kD}օd2&q󧛝4v:X(534rZ^>xy,rduv&W'6]lpIbdo\31/6N492ªQ7¯E[|n$m9t,# w 6n?rSeB6 T/5I lм+[x,.NL&-4\"i, RzfYn_P+ag 89v3+D b^`dwr3קG8[2FLYR ,}tx{h:;!ew_|絳/-f5pnV90OlT -kd2 yn ؂n~opu<1jmaVne_wŀAv/̵*'ޭv-piVof"5Y 봋"^zиql1^P HG!|y $@(}+6ԼٻdWŴ9lsc/]#3{h~7;&c2F97Z *c6աK+fT}=&k:9LA9=')78Jb6mQw$<6'vE5cE4q+6Ug}U4܎_c2@T'\aɺ?w%,zj&$**]&f7l~V5Oމ:5_9|s礚t^N%){N!sDmEJ\T6_?T㢲䇅ZLn ҲO;9 LKϨu,f8NS1W$YG=ljWtnFUAqH5JD/ݵqJw<ٙKY ;g3&O=q&iik澛v(pWMK"'] <^[}A'Ŭ·E yO>-CwVX?nAD[0'k=Q &}:]M {?m&H¼2! iH ZDEӛ~[GV}_$Ǵ7cB_E2^mb`6k~a]r:sGՐ 4P"L͚9z`ÝtNywoə_6WS[ela$ ZPP!uO=AoҋӘ^ό{'k;ͶדucYϏ/g^N_f,q3K̦#@ӟ6o.-,4=e #'^YbTX\Z5lUj@e EQ] KK.q+{8$R[$Hϋ Q #_lkrf2ॷgU@<5 @á1}M?mB%02@Q"F@Qi%_Jh}5h u6D9#P$z" ck ']H(ٚn# P0~JSbCl39=5zm4NI!ˢUښڗhZ)L֨ Oz|dYEM`$A=ߗ`Z:ҕxYL^zQXZCg̙1RuRdžSt):gUIi[e FUV6FZy}t}OtaF~ܞ$1C7}9*S鰄XO-a~2Iqa\mrzq~xY kƨrEVM4'D`&E|qrϵ\-J6tա✂)y|eSUFIÙ"]U;Ѧۊ,*PR7Bd,̵L_j!I(($Ar02]zaʁkK`4EÑ<_yM~IR\-~snJNo.lgHަ;&Xغ=Y0Mʹ?=cgmG M3vO9;YWL!a[QK7߅ȅ")h5R@6a$`BJ9rAA{el sλ۝nsԆ +aݽ/%IP_ GiCuM2֦M$/lI4_;I]oьs|wh6A#Fy-z%ËO@p^Eh6N=۷F`>%Z$bgD1ѧA4J(m҅ э܈m 불}Ν{5o-|nfϦ#h]gl]XddP5IFgajֳ;:  HGN  {@e  *SAP  HAA:(T >X K'j|K_#/+9Cg`I+8%$ovp L1οCfA] Ux߶hZBu:k&s;Dw5춾5 |>ʜijA✬;Y2 w>pDv}Ae NɸhI+^>u7~}W#du7։L 4l?iW37v>n(kZWpT|䑹Q+]mϒ)~a.QCK b^`dwr3#rxiF{^;8:n36-kd2>)%boM0/Qk#nx8cw+ז%<7zQ~IlY;8dZN>R$SקG8[2FLYu-$ fOs0gr]3’.V-JV\^b;z挱`2-&F[vZ4vc'Ɍ_bZ{( 9Z¿6|ҁrbD 3r?АWTyz&:$]zRևWz y=ٳӮ/Bݞ0`?%}Ae 톓 ɺ 1_.ܗk96:dѫ bx z}zPDLx"j>[P{ɨ _Mf5Ӹ8:ᚐ`wG߈=tI:Sv!*ٴ1F}ޑبC>U$"lJwcՊ܃nG]?xtƂ^N%){N]~w ytZ:sy-JnT;yrMܹst%ת Qq|[ p͸a^GJGlsmf*E\BHOJJJJzVQRUEY>0i/Xح -=nK7> Sݞ0`?% ڗg'Ce ,ei+/lꉲB~~t*Nzʪ?V+m#Z"D"ޮ{.MQyBӦ޾~lEY5S{ 0Nvm'%hhcݙ%%Bj3|ϑ=Tt$.c fFv_5@IQ|f/ y5IMڼ Bw ~ WxbUd/=,R?h]ې}$zY*Jօv-fA^̕GvP4m rosk>^.Nwɾf 0%1N?n!8 t9ɒjnf0mSH 톢*QyZRBa~v 'g!E1׾qK9.ţV:Ϙ3c,V״rn\W-Ʒ>U ThX&ɒD!5(w~_kHW☘U mMFm4-Zk(@m+_[a ]iV^%P (6~ 0QWDETd9N543Ϻ}cuֶCw` ~)ii3zN =J+;@B=.}_?LrO yrf*UU6>)] h8sXߑ-)" iQJc}]T;zβ3~|N1MeTׅ58Hb܌'I0adYUi gdV39-J<@7,Ӛk nxMn:Gk" ܪ +v[EJcVN?q<~*Gzx{]lgV i~"ElBH ^ֵsՓh5vwٯo3ٸ9n5GkMԓ}LU]L)ElLt_uIAڍPlFiic[.sgfpE^eٗS˦-:)Hf(+^}▼8RW%횿 nF' L!IqHӦ(G+% X\ӛ/QH5\!|*#'MxEO>cEZ0׾Zc^Z:IDAT*iH=췗uUϊT9*Aٻ>ӿq{[Nܩw&.IWpo'gz*hb в7OklH|OWMfm3v o~GGM wOzՅ~)-Ͻe[6-~UKvBhf ziTˋWV!5hUok&]}wu 4qgwj`ؕ%_*еgFuʹvjzف;}JB||lv_ɑPwX]nБbucF o;p_j6QRZwn"ď|k8ФG'ݶY}TT9҆n0_ S3&p~ڃ[~mάXz)*&;voz$_%_ޑ4cperUPOΧwvjNQSJZZ2o, Oɰ69 I+Nu|ZTy2OPOBwd,5lo:7g&Ek=ɍMsaSoJz㗷W\cX5,] ?>pO wgKƒb qBgޅsdYi%;;3gZ} NzO*Vu\ GX=Y9G͚/Y$=n8tiz;8hEVl 5Txٵܟo9ӢnSxő-ճcׇV[GOREjm1xƤicд~_%wb^ OEo7йGbJ'cm 9Ÿz5ҕT=W( 11{2L)3lnBn۵w 9M0l'Unŗݕzڤ[~e iСDow >v+MELELELELELELELELELELELELELELELELELELELELELELELELELELELe*{~\BLb?.M`P`P`P`P`P`P`P`P`P`P`P`P`P`P&owkHuUh.\BeIQL$Uy㹯.?]PiFEdYMy8U6u)?,]n{Q]t]wL+'^lX|}m|}4-==mפuPv뺪W>-Vb^2BeժL\:WCiBtN-(6˻f9T M^r_1 YUVI       .pGBݥo Z1^Qñorg.x䰦#:K.gnK[uSíK~c7-LGL@7n#tgL2.$fތ԰MZw|}jW<ڦuԳ'x`)!d{@P"(AwcS4S?|+3_t/O|Vٓ~dq`L'^H+BgcG8[X~Jwp7T!pzT of]4~Kww\BlRPn2jL(Yv{Ǯ7/?0-U6k)2vOvonB;[ÇrڟMYzfՄWV<5x&Y?:JO.:de/_E V$uIs{P!#Zy:\,e9LK$߀Ϫ M@OVSZs"aҢ|%G8u1#"Xz@ !c__p_Ef_ e$pXԲqGgO%x|o漩Nܞb]0 Qt&5'yEL@ { N?祹?Tm`R@h|]“vηQo KÎ} !Bx;Usם_Tz.5$R@dHL{\' u!${9kdXIQL"R[/HUΓT|rEL@Ik+a& ~ oPO[:ùlcd,[JB8]D0=Ƥ¾93pڌ]ݠNWVeeѲo}|W|sSd!*za(3cgYYBy-ZLnz'or509:U$9֦ \C[ ]ŪP"v'}325@xS?Nnb[:XgZJK˰ȑyΜə$mU&^8Hw伳<[f(ܺTrr^L7G d==o=poVpV I:tf911θ\za\i#vnuܹ֙s^>v+       7IR;zziȵJ\Ⱥ!IUt2x3WnbM@( #)U=ixlr9]םNL|1$lG1#˲A򃿝+^$IA)*VSd$Lfon\۽n""""),IRueDr;oa6Hdl 0l 04gBFT>+e,{ -#t"IIENDB`././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/doc/source/admin/appdev-guide/figures/structure.png0000664000175000017500000033652300000000000025057 0ustar00zuulzuul00000000000000PNG  IHDRkqIDATxG<VJI%"(  @@UUFEBG03i({F32222222222222222222222+=nm-{f˕J; eS~{7Bh4B(| pbbb\ LMO,dH C1+ Ƭ(uȺ=כzV=]?lԎw>o2IDAT9Ҷ`JARDEQE5* *Fc1`QU*"( " Hmݝ?sfnEO̜3+6pٳnj^E'Cr4c D H(rIg yuץF%YutlB"DK)G$zɑZ|s?|o7͙ g 0} a-/Iʁ`DH;f<5n/_O9V`s` U dOXLL=wm%_^7']]7` 0p wѴoXjib)2HA&PVD59XBRTf6])mS` 0P~딡ᢒ+LAi+꩜8 Q*"r#O̻n:eT#* 9$V ,ꁐg_Ck n 0`@+O,./'8 i+uJ"hqMx }Y eMysx"wPqHq7'$3 "TU5Yo|BW˛` 0*8Қ+f2m,>Nkvb/êz]T!hk}TL|_ -λ aҘ]~ܒ!V7` 0. p$>SdN!I4\\tK1Ya,u7\YEd:ZEkP` y}܊19 1OZeM;5m>"VtD{jWϩKg0` k8h½3DUH°2`lWYx y-.MIUJcWvbFA55o'OnS1쪿ϣ#LP(9u0` k8(}X|d9~UD߈Pȏx;ۃWquGp wN IsQO8LBXFawҖSg` 0~eQ$ iVyC_e6*ǨX]hNOSM]H|Msͅ'QЩ'g& #mJ%'b2` 0tp_pP܈FL mAIBsuZ! ,mVU7S! ?tik}'cW*]ut/ix+ 䝱ii1—tޝ?\0` p>5p!m"].u^HdVs+2Zb6cn1@G(n.Sk)*QJIS` 01 ُLӯGuN*2q%[禓՟7iߠ@AW^S<'>sMcj1^uucpl>ʤ+,`̚41` t*gφ\SłVNE?I-hfmiZ Ǝ 8&0ACK[%h5ѷF"/*cZ[L$J Zκ!@<* UhͷEl_'Pd}v n 0`]@ /?i,5 TGSX*M@.}fA#%֪9N]h"߳1\4*+WƼGǒH&,7Ùz+;vbu3?s\r!2_c()2(?ZB 4|NB#v1k,7` 0.Ä{mmOTk[ˬ|w̐nKhyoFJHVe[#hy >o5{ ,]ւգ x=GM&xd܈(m6Z[ KzbqxOOne7bcyL}` 0Хtpv\40o2$QTa͜ط,OZP[NgEE9`w ]ۆ?!0 akIuilKoEؾ7'%s9 ;q͌r\~Q‹U!ϣ8M: i"b R0` t:&S%}iv߫jӼz vyÐN6]U{D)JД㆖!;;Po0k"Ί{ w"nw9;殽'_hs3P+uW'ғ2NJRDU+7` 0@a>VXsD>TurQU3KC!pޔXiS3L{[W}Brm ֻr]?D($R n 0`<KF̌%-G&X]]"(qޕ[OZw x- ZlUmq; 2Y.!t:+ s/ ۷GZ *Iatu?m4xOf6$cN2U۝Vlnec`gM1` KjFe̶prJdMÀ. ܇N}!$sf쒑)a~t-ϟ:fKVNQ]i̺-ܽ{ڲ˛;ɀ" bkrnk !\` ?{"e8sffgg]wTLU4 3   ,0DRAC]Uݝٙ9#<㼟g=ܝ~;29 +½STK؆88񲃊$!fLYwD?ŅK-|yi ߽G{KA"FQa먌-Iw-w[E]GiB)n/$^qxVr&g 0`@w{#B) rg2]vDr-_m-b+z>,ŮkɇMwzF{Ln },yfKNaU.HlYZj&5mb]FKX` 00tM+yIGsd֝)MZdTGw y+ glB6T@OciVtH{2Rdwf^,ڳ.na} ;*VI{"y\ky+` 0%^ IɒmH\t+ޝ'w%VCZ[woܼ͑9:K~p_C\;h쎨S#%1* 7W2TF?Xͭ"vG'۞Slnpx1` ]reoi7'L$nwQ"PO]NE{(*66[I wH2ISMz$\qs>`(hsI2"M2쮢c J`ʇP@3q=]w>=߹f4TN"2` ]S@S lŧdNv%(E˪j1='BV8*ܾ{dv-l=|(d24pQMuZPRz`Po7uuO2e:LwN1:v*y6QX&` 00tQ`(T3`jw粫=q.D؞\=P:Bte^SBOnU˖qŤ4*Ie 2IF>v#m~?/]fZg:i}zF?z1U iqCu;)@cJD3!6ni1.fU{M[I#H݌TGumuSѮ@Ox1` ] Px f)L$UzN00\tR蹕?6#pZB(q8V@w0i 岄/UZ}"2u+“H}ctԈ;|ԣ*Z{0` `vp$qQ=@8g%UTciݺtOxB/ȸQt{( |T*P^**&uZW+ @$[}I#.H"h=R)NxL,֭3Z;1` 0Y kܵ7T8 HLdN:]amj5ϡY /o 4[4WEK4-O,nb00u#F}*B J*.DYUN:&S0`s @ya%y "4cɅ`[bhx>n<*6RahPgZXƪ#oTpD08 k$ 3),]-wy} VO@DQuAjCQN7D5U6NC]yQ^ SacÝb 05n d~y( $Lm9Q fE$[{+gsm-^K2ZJW]B5Q>LC^ > m܃ܥt'xrNQ  Bݦ_1=aWNӌ;;!XW1*&F^ 0Kn~;D2jBQ@m9Hflլq݈V;:>'UKr hFc zu.z&Bivj'<=R@=J-vE~Qqs07κGb.}DX,<U0`  vÔv݃(& #X&.|ZԃaDHUi+^O2γ v:PEn|c䂴DiqWayEv@çαOnyL7'_Y` 07@IDAT=3vnUȃ4A@$"  D @D$@J@RBi5H+ZJҶ{s8ٳZl~lΝ8fΙgry;GH8, yIQ"+j]I.A]xpgM[Ƿ ]~C꟮"uw.>$` 0%E^d$ "De[$Be$IP[(mļ%Z>:ac!~ }X%4z/ +P6RGTRB@k.{0` "eo$=GvZkDCnEB!V@wB$å"YmJzD],I_w2Տb!*uF[}U;ق^q[P Vd!%9W ` 00,½ZrdQ4 \: jXVGRĪ_0w7 DztH5:< >>!J[btZ_GlqP셞|^%0` `ᾭA'd{BաG0.3'9""օ.hGu7^P,-NoxgFu뺏?3}* w%K7o*` 00,}Μn!BBLp`fd bp JK(+K0SY`֍1݊F#5p5y<"OH H%[bTdAI(9@…X "3$ء` 00dJ9#J!5mlDSn5XdYZ(ʏDV:.h>r$Ĺ1-l)o9d%F-Qg/ue=~H)!\-!`]p0` |%ܩqrKNzJ4t"<܀ƺIve-FĂH aIX[GYab8T*ڟI??5rQïp #UF'q2_P` 0`;:{79e?UF5 uT{}m@Cy/m(.al3ƦbeD:FǕ챩VxGjt7ku{i"L5pRM\@{O/@;c(n = 0` ?tmL3#R<О0EKn_x݁G&‰4J<4ʤ<0kln_YSW_:--6S'wL+r=*#<9To-0` &_S,W/p ,2 ˋIt%&c##@~jxJp7,ϞmEU4!_('_@'Uw+L~W> 0` 5kT6n} i(zDq>({u܂Fy@*$@Bۂہ{PbKK5qm2>G-胇X ;/՛Go@5q#<%~9neKgߗ<0\` 00d~w gƧӝ$u7PAREkKIpd B0{T"XؙhD@x#[%絊Ү*K:Tbuc!+ͣX.X!P*{۷` 00 p9si5hl[JrODKۺ=zdsmp E@z0evwu[Q<ԪOwM[`̌ 7DieƀFT<}>PN@eWt>]CG}\` 0F>p:׺GWqXsB!\tSQ, Mo2A>UC ٍduZ*;7Ç^z,T}ɡWQׇJ,0N2S z)9-V`eq' 0`# [㿚Rw AozBT42S0pG{W}u.shn![7 ^ɛ`),c}.y Zu~҉ܶ6*Q"QH;9psN=|8=Q(Tf R=jqZ]聩=^` 00 pfΚ3:i"A-kprTGK w8!$= "4PE!BV,4N <V#70ء/Dp lÅO^G4K%Tj8*[`0Ĺ+"^7Uga 02[ts tJ#׎2Ke'}|EVP.ЛGuQˢ!\u\u ݟypܩ%j(#;TGٍ-FM$k6TĂ]Ea /Vպ9 0` `TOrNq:VFU a%3 _rgnLQt]Weh&LT<Ѥ]i DHY_UzZ$_ s' K4N eOG!ª3&pa 02+v'5\ZCvcL1`U6b]D֙0.,zgg+WQAfb8ERN*ng+nvWE&Ap_ґw vu .hӇm` ZK(,{5Ć:Loԧv;حJl ,4j 'xzD6dD! 'ؙqbQ.DTnv2s=&}lуt^u|oI{Gty0` `λKڿlͱ`wGBTUž{Rzbn{-XuTN.ɽ"\ w3)ON)3D:>0{$` 0` ﷢^rOlvO&B: 82)PHR"^Q(;Y"6"E49CXވlKp;q7}`DT^?G۞P/$` 07欼{v$ 8:L,3ddd^vn&]1ž `,42&])SӯԨO!~1pꏳxp%e 02{M/i18I38 8(6yӣH8bKC0bIU K aيؑu! YO\7_q:Vف ` 0C5?veOz2+ho AJ55JKw-D _ĢBEቌ$a`ӡeHDv4v޶8ZAXW3b˟ w/x5O` 0FtOh:s;΁K*P\XiI:A$"nZ+`C-n |;$,62mtYw|y.0`){pG{;x7߸y*N;r YqpLTg7[k/Y^wJc-MIgu*:7aU[ 2̱ap˻w` 0Ho-}Kگ<19pG\$afR{*7&n4x=]ȲP^㈚axڪ>Ɖ,C. e19uF( WY5oYj_q\` 00[=n[8X:/>ƃ/ta=_vw{RXj3XdIQ¶lQd1NXⅺ.VAҗ4n[7lW=\` 0ك/<㳮*uùlUי$A.t\zK)iёo!#VwnW\~V~w+غn{i֝۟__<0`  h_._,IWo{ uiEݑJ Aϊm՚cU׫"Ԑ  )7vV?uqslE?>A` 00dF=(ş˿'Omj\;5ϘHS:R/L4jRd{pLD1۔\J3{2OV?]yJx09=f1bW+kmWl6B  @ RTTTUTUjjf渭*UAG/ 54i27q=g `OtJj/MT4-QǞn~®E/9˻<"G#rM)ٯ|y+EG(x /X:wbP@(X wB,;! @Bq bP@"IDATM'ImӶm[ AAPPU/UDEUU.UUTUUUUP UUt*  @K6i&MrεNz eﵲs̙LW{ϞX`X`X_/; , , v; , ,a` , , ll1,(i, cƌ r?90,"9`pN!Dkx`X_MӶinRٳ)p$uH jߔOqqq:y^X,W[SSCd6ܴ}O@@ k\{ck֬je2… 騣uE3<{yy9ZgffR->ӎ;hŊtLJ;w. 8Q/۸qmʱM&~qIgÆ ԣGz P||<[+u-[gڲe UVVR>}bcRl]MBk0Z&3Cq)EQZ)K~;o[c˳|DG>ߺu&`C<4Mn:vHsOjK/GyD/>#,޽{u]G999gYkv~ ФIgiܝN'ztqIhK.~z Ҁe˖Q~ <=cRoQ~ԫ>DwR ڎ%%%E!:mcR 5ru 'H}ֵkWH2"YYYt'K='OLwfĈ6nDQx^<+s)m󤧧˾E//} ]v8I{!9֮]+g~Jm|`tY*h٢ߘvBsNBn&NnD{;z;Z- ,qlvV(8eLIւµ^{=6.p{ןG-rKo6띎u',-شteouʪ6l iӦѼy$: ~'Tz*x4qD .@"sz@7SJ$ԩS'zw[nJ܏D?DO5lذF8G*"DE{믿 0p'@@馛g\@4ن B>͟?_ꑑApƎK uRRRg*kOᆪoV@Cw"n#RYY=R_t^ϖ8a[O?- x 6eG?bL,X@1#r„ tYgI 8APƸqdK9stBc;}dy=/!r'}zӋ/(DŽvsC߽/X  s?SvizY@~ʕa\kטQ}ʱl;R{aF^nݺ>CYqɱv1ٕH;裥C7ZCoDQ?!A8P6/l_8p܉`"`pgg۲sĚL z>t!#:,LY4eRRrzOs <UݪKg\UQ੮_XY\Vy⪢/U]NN8]בW2뉂:iSqD!LiTܮvZ UyװQ%nWU"rUl‚9 W;/&/DV`tJ,DDLp7s̑a,ԍ*0` 9ʀX@3`yѠs<G=}HerDe# >\"xFt DIT('&$ wQ: ܏".4V GN}mN*N:I{NNt: Թ' 5` ,@ 5gd5tG !4bBdRLV1-;ۅJ~;QQ8Q4ppNOs˱sU35Dp4G(}Lq,lfslBRZz|RZF. cUԩWyՉgh>}YSt{ɶ+~_tt_C0ja^^?J7}W2A7$"-2gY, MIŢOb4` 32# (T3exDE+e{ ר2"(#LYRx;lt/fkw}7l`H!FȆHA.7r*]DR<@=h3 _~䜣TԾR#ehr|}SӎWg+o4XGDz*?Xra_-&XS7)FfS]!+@Z~IPAm3"@|? 0[%߲oZ3-EəL -!]h{>'^Y]v]\%`-f Ym)!94OzI&zx֕k I4yIAnv"茴I~=va h#EMDnj#a_Ѓ\cD5H/Q2Վ#\#gt5ŋ4?`* 5Ǹ;r-ȋF;QGP"W\qLq<ک|HG9i,ƅXg<#Un8 / ugʙDE01oy&egqFCD1+M;fF˟jHpF(7r;ID1"p@`\z|Gt ]Op D:bl(]ۍJW馛d#p "eCah3FXh}Q@$ώ3R(Qq ]jNhmH"0k EjhdtJT>]Ө!t3Dz.pZ ܏q8# 0U5{Mtӵh}ꏂ  Jn,<&ں1 4݈uC] H=Vc膵zS[/@*7W1}]ԿuN@v;:U9/, , \`pg'ʼt?0]4DEDTzD=:dDFïʭ[+D؎],᳧NvR9`X8,`pg*[a59vBPٍi/TYdْF6{{ٻӚȬExIP-iZ ֖+"* B)g"cMc@ސ کbi|nmkɥmt&~p9Ѫ, Nwlr8=nϨ#-"rU,&SdKJք6d$K|g$IN ;h@2wXjKpdH"qD#7P'n7)]-~;Y`X *[ ܿ%$XcCO /KZJ l%wⒺQRJN1 9$LqT[Q,"EFaFݸx4FeԿːi3P2#@?pMC|P$ti`Xh0L''VWdUZZ$s G^R֓r%K\G$c&Qp N.")ڎ00Œ ~NDPG!A^8 4MyH, ,@8KCi l1#&hwb]s+Di"\|w"=~R#`>⚆ %E7&)9&H:; , 4SYC*3xSRb5۬UKOE"S]B-O%K|7& fvNQy7P$HS(yXb{7]d" qz)[mmAX`XY r\7l'[bta'ENh\V0XMR0衔k'JO&ͻHDxI(NE ֍np. EXYd-yEe%ް_, , |`pg ^)~zjzF-V5t;꧊x[{o!\[)1hgv $"-PFo +)tj pp|WQh  ʈ,"H!$jaC`X` ,!`:QpXՊij[ػPDGC7FXlUϣظC֤vOք0@)X[NzY"1Xmmo8&Bc>*#v|ݕS۱[H~, iog4inݶP@UEQTP@@(LP`DPutlsnֶM6skҶkt8 %gd`r8PHkO~uY.'-{%yP%b̓1B=K5uJBHfT2HiOZ3/M `ok/|ykž#ټT5>9  #@ wPntBզu44i1iY'Gbl;J@O%5-:7(f+$A1 yғ] zIm>|<,oe-w\`x[U#=Πk: q'@ wP1 2?~&A7y_|jtnkkSCAP@@_r&[sHpW/FkT\.t&Ky_Wl -6 -6/e^$*ې(\I_Lдx0"-.Uij\\@@e0MO{@TCn@KE;elVX'fefL'o9܀dvL1 @ })_~I `-u,˟)z#u~e !*Y^ܶ+L4Uѹ 5pCR{|D0dPr/t:{ @  N* em5"n`n"㨯CY:nW=Sd@F T"tzV3"9” c/kdNBL-['!:PZaćʺ, D,AF TbhYSw B D//"-r,hS,L/"74C*fE2C`R@ne"IDAT߶=ysαm;vMҊ"`Q*D6& BIĐIL m im1:(ڵkۦM&ms9;$I Ejs9S#s?I V5sBX|OφoB ?`9nzA<}5) ntƉ[}J~ 0m$N'-_IlklgHvo14'1Kb\&:6 1[y "-g~ 'fa`r ]%3YFf̩Omv|A`?жR<|DP>#Os |5O!  8I(Op}}i+X(piBk#%96%93bSu`GNا4Lu 6o4 )"̵գ%qL<#>q1͌Np'"ؙ݂YwI&g XiEO`ix,> 8=Sm'>́, ̭%(<_;!C3 ϩ:Bs"gM 0mV66'}"߿ n^ʡbl&?q6#k;C+3;[~  oܧxG۠8->2썊kFb_Axf;Qkn}^F[JG9jFLn(\pcRiَ[Oc`Pp'Y}?͇; 9.d8Z8 3{6m`'Q+yAh/͛wcȇh_C2 ?}M2CsIUV 4 >w-bK]Ke3՛X+<ǁc#@74FTY9sa=Fx"6n F:4o1Zony:XXez܅P\[a2*k*D@@0(ؼOeY=#DYP$gv |MQ4hFD u,8vv"茍̰O$#d- Yx+ކTr1Fax"X>\'}\zP.=QVt|N w [}=2oʤ[E, |!\pKdJOtQk he$䓧q׿&U9CېH_Hf?ZD'Me&1D%4@C0۾o0&   `Vo5.oHպ+z6*<5hذB# .PU9uNCyIL>M"|W[FGEcE/ 9~1N`wodFG3 uUg][at/]?pF$)1BVbHFe4Vϵ໕F9LT?:O'< .z@O/zkp!~==>CS,?>kf'E   Y@f=#WqCYZp]QQ nFJ<}6$IB{Hd5tAzg ,F!>6 z?ߋ[?ϭԳyx,{_^Ƭr4u   Y @f%ԏtZ^X:˜Ay~ZT2AT/D7l xD`Bu$)d[TKnV`VzzwlαOZMDMg®7>| hGe1@{ 4:MeuV.Z`^Pp'  Wlww3Y궬Ѯ~1$Cb2ġ|ϩFፋ Kg<8{soBed.eQ)vGOFs բC5){`AW'R0=Ҙ#aJc*rhh- 0k삳cvebR95IZt6K Tg4n-\wm8ՃQEK"׹"jX-±ÒEsQled-߭nhBΚKxaZU/V `nPp' ~T>֭PWYx՘[9aG7cbj2 S:D5<NG]hp )$8?@;kg\jt0ߙh7e$\)\=ذj`AUPlmÎе-E}؍x+:AVTEq`?<j1LkqhϋT2-~ܴ.k܄vXR3 @@@s ;+;by0xuxdYmcq~pYq`VJKt3V$ q( ϳa?x&P϶rUѱ` ZcR|"Ht ;̸FLc}(3ic{sXgS m @@@;W̟Q*6ff#,u~{vk4CjQ8}ϕ”!ܨ|X|yQZ=d2 ֒ef`Vtq]+Dg=xUH%[;a.1HQ ӲQ.W" TZC=B 0x 6/+RO֜_t՗uLz.\ׄ־s% uJ dK |fFm)4Nx^J#Dù @ @""Z1,GK6u_R Y+8.a|Md3ʹp'  @`bq 0ǻ݆k%o2RU<) mAez 0 $E&(2| -AKn4h͵7 +r BV2a(G2=v$0k=zZ# f՛Khƃ   Y @`}@ yut2B]ᗫ +RJQ47C;`nPp' @tE]}^x"RQ4V]ea֏ ڋc=!N%' XU=[(7B;kZc   9 @_ڎ޵V.~Hh0XJL]T=7.MfN}?ڎXb5ؒ(NqpXm2{aL@?xmf݄$B"@QV:jZihhSU*LH$E"I!&y j4vo9Lv9/qscqJwX&@N7o ӶTNB[;;\!24L߹'\ `1[/rڵob@ lx>y(;~AꚈ:aJRl'Ȁ\\Fu/֕dg3 l x^nɸ͡]ZYz:EAn.+.x̔G m0/UiJHU!j]ͼz7t;6m:vvZ -0/&)a sOF 3`p'eA\<rA]C.G3A#CɨL`0 =.= W-K8s& &` >; `Fxz.P*{pvzTJdb1[UrVld (X[ 5Ѿ@OHLTuBckC:Z ;;Pϵ)hZ8:V//*+hIə/- ,; 9U#U=VSe2x(&hەʘFL  @}9w.Ju:dn݊M'cTBlKxTxEξG-}ܜDˈ GFc+n :wLn6qd:2uZB^ 0>pgSSQáV 奥8a?U~>RVF^Vd2<[q9#Bx?" ʪoeM2msFng `wp\[b70wR)KAdbD$DG#r<_c ڷDž#Gmd$L P:~<;~ӧcӔ)8s'%] u*ҏ_} Wl,eeXGH?&ΧcyTXPzX\B?B*'@ _w.~zDFbܹR}ouL\57>nxTc^jc:-Bz\UʈeIl @T݆ @ իgOO쌋Cɢ? F7- biˍK6~<\֕qcE/Ζ&KGq׮Xh?T $|&`R8iUwEWN>WF iRˑ8}*+~m) ߁x  6 p$Wo2Aag@쌰?Sw4)˗K}̆ w8RGÆ1s33q|6i>>:ӧCSRLݥw̛':4t!w7>eŨV_Z0xbdl݊={w!˥ω%9J+_`0Ɔ/C9@ :BZ+FFIn8q#\ CqTW2b4iU >кKAVȴs|EG^621QQR  ,(hݯJ[˖!5>o~3f7$D 5 ~~2k`2[t4BCR11߶}Ǐ>C޽LF49WV&CP6| @`pe6m૑[{ &m6qhXLLXJ۽MOvoC8UZP4ł:*\ 9E@ U(<>d"R]]=h6@H4u99*t(  DQ<Chjm  `2}0<@[t:B霙t d*"1v@&i222їPdg~J/%%)$cX IYl 9\%@pF㢿3 bF s}yti qh!oKx>V̍Zy=z6CYYc0`HaX ?ETvVU?R'29Р{VxQ$ ;st:#vb<w@QѼH98x`.RZ-S`MvQ0[9.x8 @prr\D]55MmeeNg~RYu$5&P,c Cd u tM[tj R>`fsMKKtWJ "jdQJNT(XL&)cQ@OD_< uI"QhIigyE18e;hgNݓ8;5>y"Km`IDAT{s}o۶m (JE RU-)E UJ!PTB`Ͷm۶~|ݮݧAu7v^|9|55@vKx̟QL`2fs Ҵ:6nMg3 A&S\g3<()ICY47`.z+Suy,eo\k}ƞfBPP]P]v A v@P!7 r5jSiW\gy)+uFOH~L9wE[g$ gU )ĺK5lԽ?01(* sE @pNglͥ'eL@|ktАTty'lK6aM).l){cH)(-d,8S3a훴ji\C6z3L]ekqCo~y[@p.@P()eKhv#k(W1tW:{iq[v)):ܧĒ̔{~xgj]9 jAm.cfNLLcvn͚$ڥ[ݕ{mȉi*"(g E @pN=CBe\]H=e/1' , u5menGFidC}],\n+Cy3B`ih-w x}p8'=}zt @w@P2˦l n4 IIUӚ:M܅ХԼR^~Nw1Zpۏx]w! ]wd@V:[U'<8"K4 qu` i/:n_02ɒ@yqW( )U?@&-껜f m9f;GKJlҎiXxk1nѪc˂]/K4@shpCwA]q<蚬/Pq+'`\Ey(5. fghRX衭,]`UXbV4L->$M[&蹫ootF| @5iAvZ ,e˖v$-- +xꩧ@zj<^ڵ M4W_Żヒ3ڒ%K0tJ{aa!RRR*zW_}5."A\ׯɓ'c̙qZ8PTϚ5 ۷ǝwyu]DŽ ļrqW( )pxwƌqQ‚JKഖbM%{` ChdO EK@m޹c(NĽEpC8FARAtFɵ0ntOuЌ4{/hî "9NOdZF";̙3&M Xx\x0A"MYY$}PP$%%a #DzmbOWhCއo>y_%qg}ذa9⹍`}1Xx|sX6 a ?~|e:wܷ{zA:tj|IO?;dy_sV|RT]= @P T_5jRVs$J7! sPD6D>ziAPFnk3SKuw4Z!$30j_ нNhe\3ˎ= {JeGFlඍ[6|6샔7|SD1bNϘ1C"KcѣXp!4hl.ӦMñcǰ|r ƍ$ . C{xGУGflܸQ[.y\z"b{qqU&E֭ED? _{?3qoFx ŦAGQqޛroQ/#$$Ќ'''qqq IK L_~o+M)">(vV\nݺys i 1N vZ,Z>СC[`ߜ Ec/Dzȑؿ[||Ӂ ilݺ8]{=z4!]b_.8ϟ?_8#nFĜ}osy"+ Fֱؓ?9)(2O-D׽ C\(Pz,%ZQMtryL`8_NDKꕗ}ȿ;n[ltрF6pkԑĒ vmΟ6mxH3#۶mCNwFoII.A #$RBk2J(8]|3f E%fYubE޿gϞUIbq d?IզAGhĈN2JNĔXu]I?1aH4y?$pWw* _)+aHx=7:;S[(?,v8~F9)cG[U=k8C \}2&شi(/tJx#?q 9~Ҥ3):pʪ" @nXZu?i]pRFo6 aqÚ>+ye4Q[+'!k0FMJ̻s fѱ* I2%b͚52$[gPB"(? }L$d$Y@oݺUD|B:/~]Ԅ3ȼtB|Xr܌Js$#|@%t^##2~-rʌJpDl!Fr$+33͛7:'{wgw o]*jfN)YxʞJ;{P]P(5 CYsԉjUtc*6.d F $^d| ݑcF(e+A&ɸfԯk'@<(6'oçf.rjI(wFc)o0,$"QgT IoVAhIԨ+IbH)E}%Q&e$?F}=I4;ŋh-4Z,`ěe83$X$?,>5$$h\rXB|ďğX1z$FO$+/u\|1;Cd$VoqәZxo׮]\IKAl4ʔuGuso|^oM6s~M)P( `Yb^[/|U_͑i0krҾUC\֭pَe; ="䞻,<+-h,j'L&XעFBoV />qA;0K4cԕ;QJB=(yF(ӗ01JAJ2,Y_|a4Zi$H`?xYXGJjH`b=&$Rd4o cwfu9>ݷqn.Li3ST&&&2-t6Hy11gt^ۡsh\XPZ%Mj]llʪ" @QfG9sM&4}O8B #8#0ṗyB )1FR)JmL e4$\XO4FkzoebqW( @}4痄܈ZzU$.7 5EH݆ߋ 9;Vo΁ӞG~,6a辤\\sΔId|6n-!aU&ʝewur2" @qZ>wJ ZƏ6`C/xzng1,FNbqQ}nʅ˞- n{2@;,^h+,j /PvNOQ+ e@w@P,M},n䐘pIaóW ܥf+9[wK].~CQA_Dw9r( [^V ROKf7Sn-bMjRnDm{u@:M2 Rn\Zc.`ؔ)G@P(N#I=ϣCFxx_^xU?LSZ^3\ [O@Dec; sW]3BLgAbl˖ Os~nIL(̚Ì"'c̶7rsi̐Yb)4*P( cDG !~kξ櫅76KnKqdkZ^=p ML/Ûp]=lHlH3 e7o䙹0CgSU3w<=x Ѷm[2)199Yd= s!i}aB ﰏs&/W9Wr#4d^rfCn9fCT/;s*4u3gH)CbĆ)2goh̙Ct^8^OXc8G̱ηLiܭ04MpFP]P( Iۿ|;&czWRɍE'^];8^sDS7sqlfz(TS.'|"v:ٳLĖsoI3gCNLhLGI 8%wQenbNK/ :sLA?Cgd9IIl9SrC(\:ei .}=wby.}cJH9c t,HoၔNs3!8g.wnTE,X{bI& X+6jC@ 뮻NIYw6e{Ǐmsć:Æ N bsEܡ,@P(N+ւ'nfs@7tWC5C?Qy62}]a;BzBqđ XSu„ "JL wz%"$̣mXHI&I_yweę۷ "JbJܚɒ%"6^t ?MH|iO>]!Cƺt$ܠyLp6P]P([J̸݉  C˩w^W6y7yMSk!HՓI7]Whq㮖ԉݔ( bDnޗI?%Ar$t\֭['~:ROrM [rpf̘QH;6܌eF%$5: rRigE ^j HDnkhDD}aH't^ѼVߒn]wqqU_%Y(,Xj%є8R.AHIj)0fcYf şSl.$Ϟ=[h˿[u}4s)z,K袋Ԇrp[A'N( 7nGϨ:5;v><S@6h@tj9.uji}[3kU1hӦӉ!&}T} ͹ );EqW( ?sRR*Nb nYi?)~UB 7#/' 1Q05INu VҎ3(:'1$PB"J]℆[Rc΅r7Pf;> >AÅt % 8E,rƞHP5$#FvFɅ1p1oDݩ=gҥK} hENǣÔF/o0u<)K(P( 3S'N~=qi&S KnӰ\da)Y*̢K"WDQ~ZB5J;ln6k@@ !!$fOF9t( 3 " @8<9/.^_wipr`) vK*v+"":PlvXV=Rw .-vko:!am.8nM D(L(,W9 mT8ZI$˶mNĉ$ $I$@ axRZNP S m&hm -mhҤ)!q$c۶mY$IjwsV-ɶCMufdV>ΥǹH@s==.X<LϦ&"@2ܖ~{eH9K0|ўݷeĿvG$؜^?Y Xe 0Ӟ3B@owG T c; 1[;_t4 f"vj̦(F2=pT;PTy2qa j#6i \jD͙ 3L/g:.m ]:@@Z[% }/LoZ*b2R( nȈ5b@xJ1hyg,' /JH*zzۛ@0H@"3Z]SV.V , [ %#HbaT?d=c&H K+ڐJGI/ߣa:&|ì2>,Z.@X N `<5Orz>-J*t"&ASI˂Dj2 &xp7GIydrv4 oJ+8;&޿VUGѶ5,T=kBV!%@@z'0{vYk$ ),t\S\쥆 *dj, 5> மnȈGbZ^ChoAWnTT:`wW!igm'зn4P!!@>S6l~g-~UzIV̐(9 6$b=ib-HAH݋D '`yPPhA_K:RohɜVF=’,dcP3^wIv!yHƽR!1$ D.8Q-(6 ftv$@V'w|3m(Zlpz +LF" ketMC2([a(,bdR@wk45Cz} VB2Җq[f3 vhIc(y*#;֮>ۦ՞tP-ԸlH% Mַ?l]XڧJ7Y@>n{w^T\X/+X1d&93H&0$MMAO++ ɄHqs,gصGp};H;L3T!`-k G w0!Yͭwn`V@p=[ڃT)>_wG{ߌY }?7kuSRO(^jzn"Z^ԇ-]=}}!qT8L7O#w+5k}2K!vݝƬHĺ!VȊ0HD[!Yl(-/Ez[:H%ԍL 2DɤY15a?td (-/ԀkGfSfx*#=DY2';] $ ʋ0:':lno]8յ)hH}M߄'Ўh}}OZ-Y['L]ТWూ `<]3WB]7w;hq_ǾvYD$q_`EsMw)I=A•hkHdj2 -bubłXb<9։nA䚥{g#ڕ˘bcX`Ӧ=4WL^# $ pU%\Vs9h@,g EbZ0ToU2ֻ*cy$u56i];?).4)NAVT6si'q7L0 Ia`w- -@U %=s/nWǓ/߱#-^Coj`>v-nWCNgJE!--V'"$c݈؝EZP/[m0eqzD'x*/$$odM$ `UrfO̙V8FY;:!Vg۞uvFAl)S!p 30]LwHJ n&(KKեյ~b&C~_K;zv5w&\!Gu0ڵx4>dQt=D< Q`+SqDa'Dn~V`}=S?Jw91v56 (-G͑p'ŐqH~.e3  pϾljlTs dBj`L;+0T:0d !BLH->w~M7jfF`CKRrʧ,>oUwC;.κ@]@F7h|Z:ZZtX$v%( x*=S"m0sOLT ۥ` V0*o<@&s~ H1pYՕg\Lt[׹c߫;{w!!_Qu\,j%ff735l#GF01cvW3DiŴ(`hfX)1|#Ggb+c~=,s6汝Sf`E^g ! Q\ٳ||kݮ VŘJg8\%j}RIGw5lDM{P!Ko]_xe8Lw^A>o;SO|I],WYSzjo tWG{?ڻa'$bbEpĠe0Cpg w~JRp>ds>f20Lql,s74M|HEahF.[@(~=o=eE*Z9BNm4"[t19%b̫T8pMZl6[X9 w0bz\KFZTE-)p xQ3.bJ__ nW#)W_m}c[cļ* U`FO[Ҙ#`Pilh-|1KFyYGП+v+?;T"zFO$0@5=~uM}agD!ڛgq׃!CD8\$ 6wv{TVz1E]M@W*LvY \œk2E"!}ڔ+.]hk[:Z{oo&!SʦB ЁajPMnTLcOG"G.3r'Xd@U~=d+$ V!N>j醡KF5kط@?{뺧PХ'c"CtrڅȈe) !Z}4gCcCE:FI1PwP!c"ft^"No$ X0.ٔ`Ê9TWВ݈vC=J^YXk_k.Yu.n9Km~_@75`<>wKo޲b؇ZbrE;ˌ)q-\u[>̞0]@ w0n{?<Ⱦ^Q,16J+dsFħ ا11uQ6u)D4x.ƴFXCEvrzT&ždRxLtCV![+`VA0X:aص,A!>4:N4xj"H nFov$|c1@t隂ˮ:]8'e\w:FE(0> ^"Z'IAɏ $ʕASAS!s;=nMbeI'޳!1рX QBV& }D )œ c; a2vQsdS6}:˳ |D/{6"ТZXS(O@]7Wn_*ƺg橅l)WXG@K'Hq`v|򪂛njT!3zH |N@]{ObX]s!*5P1ѷ3Ps-fD3˝_|,O`yڃw 6c9?6n1,? `h;pX֮pgA;㱯.)+j&3~9Ӆ0imxiv[>l|  w0n?Zpr"bwQ<𔬄sXzdk&cL .M۳,+# !`){!>Djx񹎐PY# `˖ m1LZeF@/Wy9MkҤHC]XD<  w0n6C]@eknaT3)<: ͇gb<'Cvx ZL&2$=%磈msy޾u߷Gw`[Ƈt,2Yf_#6=@|Z7Jp8VM]RPlEa!d|+IP(̴)VX1]JW5۷_lc<>L,$ W7:jvShj|MY Tp&@ofrzV-Rqu"1i^x]YofO`FjvTG.H$؏I` 3NLw8zTO?ilj۶ߡ  w0bدz"WzJ^s: Ӯɗ2IU|<*0.p=~X0q=[?hTolĈ~v;>gR:쫧l`ض3KKvFaBvcȘ3ǵw;v<*(N6Hq~[O]Ir 6~H:}K# ^gj6[ _dw)gÅ,$s? sY5boC˘6.痗ibL,?[Z.ۼy9N=Op Nc!#ވl&>`RI){<]Z =僞송3+uFܣp6;2Rry:D4N18\V͘H 0fy>`*5 63qg6t$we)|ꑏ2XkB@dbz!Z2 iuu&K:uH8|nڹlCVX`--̡pgAtv"d -?5Cb"KV"ԅ)aMn mɸ| dw\bʷoGNK9Ff̘\ a@;@$!*).&8?'^r'.W e-w0 G$6LhE4hr-#pf GI]#I{hN1#Ðՠ+$R0>WS  !8`9Κmmɷ].ּqUIq`U2 a:/_5og͂##Z5#I~c{r]>S@R{*\'lvCѦP7Җmnn_.BuPӛu j*?ylͨbHho(.fz, .@!P"0f͂A̼JhbŒ?[/Ɨ> ^q%Kg*lsk8JK?h9;v`#ĉ8 cr3 O9% rz, @W_CKnNЍ֭Hל A[:ߪ^@FdKjkT@beTZg}vZ h&ۇqpffbҥV[o<Gχ]θq5ھ޽)}ZI8[rŸl (nNTՁ *).`sU7;M.\;"nl&NV jJ"-k"XjwrfTj)BH<~3:Ixt0 28ъmقSO_Z\l|D{K4j99eQm\M^yg8o ;60t($@;̛-.Ͳ{A^#mj斖،Xo'|UV5VaSc% u4?4JIOʡ͆kWVݫVቑ#1m6!Ǜ"IB!m۳650  u`aq5i'Hz) +np$ʭᝡǾt:Yb&t˵:![Y.Guܿ"[i7@ wXalP"2;wT1ZYF_#1jxdmMDdsV2{.L1=Wffk-Y9y'{q-+Ú9s~apYgYT0 #ݝp$y UztU!N\/:xI'h{K{ݸ mDw2s^h ZCW(c);>?x0%NԜ j RWn;zmo_̚:z ^άva3NKxvz_u,{ɬދwߍc`ϦTɲqI=@$*). lr↰v[Ybܽ;iKaLj 7҆S0mڴ''O ½-0CӧO/nh%m8Wlڴikt7riӦf۶m͛5 ZF/򔒒jԩS !P$TbX@wv40(Hxӣ$aWFB>|bǾ.E8[-!(ם0""VX'^W&d{Δ8=a4<NqH%.ivóqr*8=% PG}tu󃒒3JIIImGmZ ,IیG6}i-Ϻx, nZ(5Iڜvh8uN[r{އwPyFKg%U3gb}hk8/G[Nԯua\ynYb/o HNTS]y,.^p |2;H{eNA= oŏY*U 3D Wϙ1 !F/Ⓖ<^1EE襧(@;|nҖsp!"CS#t`o-q$כ7껭55Bz |>N0 2Gjfnut%:x@hgSqZj)g8@^@|=jtu9g<]@%Yaqewtvlþ: y ں`D;Q%:^`X9/&ERG ֯`ꐺd~/5xqޥWQCP w )U- S zrXa>hj8.MQ{h { `Fz: g-.,(`4a19K"#FLlCyyK{J:Ԧ@=3tP@H ==Cr=[WY^^HCFXr]kJ:i-w޶&-ߊ(T2df@m$ HU5B^ BDZ0'ZaxD!ɰQ!FxTJc )4"NP|Y]]Umyx(@;,oU1= Ar6ˉpsNXԋhnEftX&a6NAhςSE @k3GSuHY%y˲ L/ĎP (25 X(*/m uq^}Oj2( s*@(@;4-CeeE M-̋nKjZF- -#8(E"ŬɫDJoѴt lMVB>}ShrWjSTAE`9 ,ǀE J/>B;p#%׏ rqEHyJ9I4 jjs8jg0 N;m!SR@20þλzzeh CAcI`MCWeu9`w *I+)躂::/ 7yu_ +/Ev`)k )<$^ēùf.kNY=ee-{U]ݫgdd:,=O n.s!EA,k[Aﲚ $݃3J@.*)0aZbF|8rŠu'[i(R`'A#Gasd"@:w?b* Pz([+xގGȻ ֡wHnͪH&#`2{cCgFѣ+\vڊN%;Ǎ1GMKC(R'UPKTht,8 N?=@/ @>*)dV u 59ᠩ1(fyHm ,n3@rp8p "+a(rr Dx[ë=ه+s0D1'[̛omo65cW";#GF.X}"I'RYyF:är `R<G+1]ת%U̴9|/Zi`5 F,tMx!t8U!e9VetÀaH V%fd]!FP %. *97s͉V'xIS&P_uQWBطkM"4NFm 'BQu5nTzp9f]<ݹsY\Nt5c@Euu^QG( @2ЧE;i9,`! " Evb:8E,\ UfO'8QH0;陿kX4*mZo 2o(ڭ\w).&(N@XκjCA@PPNͺconNPxQ s.' k^4DIyE0;#1E,CU`8gT |nO˝FZ8Gb8կ6i"f,qɈy@KAR@P@7@;fCw{&߉ ='u9| $bYG9&crifY%)ͨJb~0 W]gZ)5*AWE<;?FF=p9w7HCMɩsTlf|n( (@;ԒWP xWޛIOBTQP B$.v(sDU\I#`9Zbjp“ pȏDUP[kl~T)|lͥ!aȱXi]9RQOP@P) w ;0 Ԥ3Nt/p! vw(20 )h0媪4"tMA8TE",a M#Ph@t-. oZܾaw"ѦG Mb2t=/ꩈ( (@;-<a, o[ 1"sɞryanp`EHB(N3e0~'ƀ"ʭiNk')&! @f+jm+|r+uo@(R-:MNNO( PNtw QB{n7CTNj;2@!:X v{6EY <Dc@ZFoh rkdTAVA232 :z"Re̛wC FCFW94 ^B5,, [x(,0< Cs'ꯢU^VrhQovZ4^'NÛ@; 0&/pk}XomQ7bMz@2+ Y})gV0DΟ݀GYĞi^jh=1}Z"a&SvgᅗMNJcť%Y?䅳S@  w@~ gz]M&yrxlL=A`wPL*fVgawEBQL\b& }S׃6j͝ ]+ 8Er8eϜqwTvu* p N%x?(Jw6GRr,=V|&RA,,o Rh^0xn E~xKQ/}wMܻ)=<3ڦk67K4ez<eWieEܙƋa#* b i>ߩť~{h!L @@x%x=O?SVpE+2Jf|" A0eh^SQZA2B`eY@ Xbvj0k閞uڌ!3M I2?/gCKF@-@+ QIv6sU+ Kʝp  Ã#ȍewe/14lo4hGzZBsMj %|RL usiM7H"&$ z5^^9r{ӭ7;f&ͪ&VMxn39}10Y,S\n %R#7ApPCZ,d#AQUxj  w@>,j^/3Z6uRҰpB4]+U@~B*qho|C~4\ohFeT]LRި"A1@[n9S7  N5!ҺxC"e=[Hީܕ*3c̃9N UjuDŽ`_SB5 ME>&S,KVGśOv>%@j:O(b@,FĂ)<:O)`E/(P=;p,&f[fuά%E:#F N@ R@4n9{2M|=KZ."dE kγ8<: 0lSaRZ's>[uĜPSp'P;SᏪ3An{bkbQF]3i A"  k,Jy7?莺[iH@j"?_BzzF7B 0jD(FGr zCYS;0.YYx']r)NÚk;(Җ-ꜿY"ivp>K7ͭfg.^6]%Vr0+"RR/ۖm#Լf\>^<]rx<>hC{qDiF I`Yccbls[9dl4apMp' #cJ{lfl14b~@E X27o4ghA^9ϞGM&vs;LEc[lRjrbjpx5~N kYRp9RlHGw]W  W wmFuٞi)er$ EE5T 7w%NcrC}-P[P dE (E>]y=2/^Fv}d4 H_m[&V/]yؽrғ֘P,!8N?v-^6]F07ʩLD!4 dT7 ;[v#`qY+ᯨ z1 fϱivө7  W wY,3Qk-ƴQ;yȊFDQ(x7r`C lRQ.2-=Eͫ = ♀g\ OCaQ>ܮʌޗR7{+膀2H@5@\@F""ZdytѢ`oꚆ53f໷F!) __C LIK=>>88ߧ]>qe yT;SƍԡқL(Xzk'ݻ-[ZX(w~nL@.ZD7nOҒ%tAm;vЅTT+DJ߰N&%?$,8i(*O>OwkH;(Eq +%U9RY?<?NYYr6t~. _ff%;[R]k0дr܍F*iqH9r.=tReRoyie> *fC#?c^*~{rF*RNQqko𢧌gڌxT.ݫ"ե(]jt&)T>H$0 b0?45;dls W|(,>Uo#,KU7ªm5$/VwM@!\K+oTytj]sfz}7.*h.n<;UVѷ} guzDZSO5U8-cŒl@}@Cp@[ 2g,x*-eqk38KTV.Ebʻ[5dP`tb H_RI0e5n ; Pp z)m;-Dq7욊<xD'w?pJ8õFFM&ukZ*Z5;DR*·d;8; zZkZw qbYD118F(gUj[ UzIJJ Pz?w?w<S|P{$@p@ G!:|PzӗTRPwFH,҉#.ki7X5ut)[~f2t*#uЃR\; f>}KOhu?рR_rs B<zOΝjܣ+utUUOvc-[uSSUv_:όLH}MnnΙԯM!J977EXt\|FCBk[ x w^gJʹ3v/ދ6mz,&Q .2hPA #osiuZ k|iC#)MYm5 Cz3Uߎw@p@bcS.&'~%? 4t@NiB; ?v@,АkŝRy6zxT}BGM륕}VA@lY/ .U!?2{b``JE}'+tW䜵qdM^VhLAJ,*ݽP) xu^z99/9pv~~NNlBRl,V8<ygkO p/޹4@c r  n:ϐZmǎz=Jb YRXqU̼u+ݞD%%Ӏ^JhLU*ƣ]$;0tf"CII-d:AS3"jZ`N4(&gE rYv=F)*r=iT<&@p@ MD-ON_WlNS2̀Z-Ei4ՒBf:xQ,KpUDK/X[924ĠM nZukŹ%x@p@!+&bߥ))FR9a`ZRP*/لXV0 S̲BAȺ²lDZ{+P>Ex9ddl(Ƞ 2;@X O`w/"Z:' `nTFR*|Ta<0ȷx,Ӌf.f !AWL.x#--Zb1( #LZma:TRq Q9JRV گbN%WUpn{bA.Q>ǝqrDԢ)&&v0иi ^JV;0騿@]Z2TյPo48'GloTU ,f{JJVB4ŰsnmBf P|< MBz:ݐ^z=u@ZH HT}a0)n_dS[,Gx}{G;;4&MCppNzܞCf3E=(M.)={,{crl2훏W"uֆ;ia1iP> &(v8plx~.]F!59?OL )+= y2 u诨(4uOf))K(66i@P))ݻ|hDZ .5K8qG.8]Rx@Uw/+4Æz+0p Yo ; LJj|sVjo=HHKkF됐=5Y[0ȑVKس01 99Z-2 @Ct[ tin}99LU5UZz$h-]Z b7`- kؕMvA TT5SMS3338]tj8r?36S >i E\`ӝ:n$_~rhZX[[鐓7zgffjժd=풒=>>fE*;PQ ⨋K#[/<~T*|͚\T231: 0` =|QQhذ!F`n9fBBagkׯvըQ11𠻯J >q .\2ѱޱc\<8::аL-sss!H`ffƅT* ˱(**tKKKBTBV‚2칄DL>7U˖<Hvvvnm`h4 +++fvG&YWG'YYYEBV*UڶiۧP(xYY;Y2 u~XYOGVN~A r^3fې|6x0:vgYY }56 ''~z" w@ ޖUWܘh2bb@,.*²K!ѫwo?wÎ:#HKKǟ~Z..y&Bݻu"S` qakܺu  &Üٳ1%4W\uL8ǍQV-^p.N۶k]p{#!?o77'T|Ч<7ǝw߸~=0#Gd2K$ylڄ..hݶ-z˗/ۘ3kzUvO:?B4y}oڄCqDŋQF t DQ #}ѣmo֬zM X7ٹll}WWW\t ˗.E5''D?Y^{xĉXz5Ud}@^ώhi9i/ wzH@@ESKew܉+fa…Xd ۶IvƎ*83!>h@ oס0\.|¸522χ1fbYf.Y pDGõvmL<'Ne6+?p w&oܸU+VYӦٻ7l݊aʼ111nnj:|#G&Ι_)/;wիY~cx8mE?Ӧa5}"CD_ѣQ"Yf͜ 60?^]F&OFyؼe -Y5cFMƏ]3w.dGEѪz" w@n۶mx`6^0>6oeq1~g.3LT6~X֚e=<ׯZ+Цuk?v/nٲe5աng,{ˮ>>WVpѢ2_&`6s&6fcJyCXI;z( `d;H@@(uUL~I̸-drvo=22FFHIIe٢%÷u%$~ r >cnj?vFK|% &x&%!))l ֯W/dV܃ň/4FxeVflق3fX; TYuH  @ZiZK @e@@* @$%GE LN\˱: HJbK*;"_9))!YfhWWJ bsrONx ũ4 /C}9;wptl!z'޻_PCx22tMJZ9a([[SS1 xjkR\̓-H w@xYjC"#Jc9;]yB^'II hO߉6  N Uz 6ֶFK33~4WJaINNޯ2Y"9yċ' N ;_۾zuĞ&Mu5ƦfSSXTv qX+%?{7AAE40x6: Vc=;L^ϪT[1WItXY'ny:xyx p' ǧ  wZԿe'CÆ,,ԔJ#Po0+ R_PP7N@p' \z ǏͳBuZK$V,SF[ HCVotF[{Z}ET7N, /; r<3Rձ LYY!@"FHT*]T RiqV0f%GTLJuN!  w@xzfȸ \e [qUq13ndb,b0266X 3P~UjA@VT5MFcB D'fAgJNGP  wo ;wctSv fSL:~~5 %P1ZXa`W4D"!Llm@~PTBáBͣE9B!##I5Z'@ ]KLb)ef>9q,-9( `t6ep " L* 1^H xcp'z`CWue%bL\z~wGժ<_WҳRrde.=?^17LAHMO lh@ wlkwvx |R$֍ i= R/X{Uߡ VQ ^P5jE#jͯY~G+dEBιWJOHNʾ)co>' P@ @u /9 <#΅G=GF$`<[2c)G%3,Qw~0K^Nڹĝs眿_[_@ 8(2$qj$qSWXgtPI q߳Y`jc2z9={ڸSKwl*=h;7v#Sj۾GG!dט+ Y-!;"sdO3u<)ʓsbĮǗHkX<1IdX; rVU3+Y.9NGFFN.w @o1v;)gPК]OxMN{xn4\oB lfdS~m;thJ9^t +]fX+D2Ouyɶ$2HX u-_G:6L}!wf N>-Zc,-h~}\a6:n8ꪫ(++E~]vt駷XCѳ>d{>>jk@x rɓo'ɴi&zhĉ)ؼy3=3{Ǯ]]?LcƌN:%i(''N81δ4ڶmc H &v俢} q[tB4XOӐ'Hm%OCg0fSpO:;>xr1#)jx}F=˜)YJ8?I]s% a 4Lͪb">S6 fIW3\|x, |x82xZ,lS0 CF<?ڴiC#G$N'y<;[oz}6)b222A >WZp^ ڱl{iرԾ}{zGsamlh0u,bjm&&HZK9^GYYmvs$۸qp2{֓[@,i-(;ݠ3s kIw^W'7J`8Ew!pؐdtE(6p4ȲS|&=>˞'ܿ(TgoqPA=@N0k&l۷}'mFD7t5Pnn.=t饗l;Y3g uq 2x]w :t YvoAA>3:s /*=z49NYݶmF+kt Ef{Ȑ!oMݻwoVdcC|AAf{Ig_~ZᇩGbh_~E}8͵:ܠ]CSO=%2֯"@xAݹs ?_|1e(@ddʕt)cܹ"0޸q#-^XZHC aW62?5($&LK.䰸8T~G4Xp}?1]VAӧ (1Lm@m"XA?Yf _  A>|$s tNj(;;[0wdۑ= 0G_|I!Yz$3 %680 HK,38C)xR>?~ Aps9G/UWW*S ``/,yưv0Jl8`է $ @k@w ~ݩc=e:}[wzd,%DCR$'Y&H?:T"TSuq YZdoh3BzһwoeFW5-lth@f˖-ԭ[7q2l]vMGzԨQB3x`junNfثA_Æ n ]Pa—V׮ռ3dO }w&b$q7[eʸ/skE'{caRB*Kz &+3N`Dfʔ)"KYdq!J!{dƱ`O m l27ok_^M=m}')R]DXMTScu ڶճ5d', NwPmUw ..C댍 Y S wyC*)tD=]1zK_Nu!LY\A0Ba| x.%tI$$.C矋{c3(9FY76`j_aL8 ?th `,;4 x'd6ɲ٧v yLo0I,#Em' NbnnA]9u=DYF[j;G:d1hs>cb51qD!g)jNkm!PB$M f$qWoܹYf>IOqI",jG}ɹk:12۝,ȻNow /pk-VɸS¹1I|D?h@]TYY꥽8ϝ !MU.NP$r0BS[`EEEq袋2H``_H>}D' 2B7~$jAA2G?n[hIR`A @_eU#Q 1( ~C++RDž6 Mbx%4#|@UN0/O\SAs9">6bNQsC wU}_{37nhZBq'A=rѢ- v<(ͪaðƀ3L`p@  5 ԩR֛v`a,bIg;QQk {EqR)EYܝ <2,'%NRSy"Hb[ۀ65Ӵ5 qYvgiFm}"YSQNҢ@: Fc4 @ @w n׎!.T=dv&'w"rW-!Andu"Yl9dDFRŤ\n DGPѴLv%82$3%RB$M H$V $bfUe6dNSדTr0)*rWᵔtz'g#[F7 !.,6*R)2 JB8&y&( r6D=fLlud] H$V $vh9i,V#6 ;ok- chK(!7չ6@:Ey{J>G^")og9Qp'~4dd^jb=zIג'N:.!oK$ @ hH.8`T)Uݾ0(K ۪IW\N))tQͮ9TS2 eF{2ɘ֝ c)]ND+'(5}ҫrA.'2%@ H$I% t}jNvSc2kbb`!ksA5HK5]T[L;H WPصKvBw-[Η$ @ O 0(ij錧n-2I)P2,K5A+׵Gyǐ^DuUCL#a8G]U+p'i @ ]p@X\UNW6yj4i#',-8[gI<k $`]UNCȨבRH!wK+  c/4 @ Z$qh[sX.FOShĚ jJFYCESЅ l ymDJe:#E\% @ hH.8<>x3% f[ %\+g%u,ҳ{I 5"13:=/iyYA H$$^Q[;rܕanG,wK\85%i~VKBk&+ZAW)sHoΡP]:PsNi~dܽ^_xs U H$ $P`C7f7^f0eHWx$IIfZ!oNȬO'9B!W J1u*WI$ @j]pPtVktoho=[Q:v!k~g2ZjI_[mcӛ)[U*H$ @@w AO|Zt8ˏȔ |5{db_r R9 Ť3 q' T^U H$ $ JK3f&N ӰiN je t=ZUiŸǹ*vFym `g Srk+3 @ hH.8miŲOmz={fMld)O/6O6Іi(^ZwfP0L`)$@ H$I%%WﮞorҮ$"`Iytj~=5먺t=RCA"eaz;uzmy5IH$ U `Ey͒ܿ [| I q 2,/&ͣ7d⡊s)#JZmFf}kFQ휙ݶ- CTUDhD$ L F `1U%   -mwݙydN۶BQT'4ٝ.9ʭ`rn)n:F4w8,AiWkkR5IgꁪQ1Z&}҇,9zUF Z<߆R[QxBE,BQ%?p&VYCKoC20 _B#;/r74VsB2}[iC@5"BCx#<`Z?~g63΍h۹ MMԌG)n$es \'DC8dU,q1/~L,aU̎ ==1Ә& c 5HwF:a,XO;5N3z `\5 Ppe疡2 -wBz+;V];7b|z.=@ pׇ$CUeH]}c,`fiYwAp00hh  XRi+b+DFQ0uL0 MC໨s1]G2y*\υmvµMxN aO !}\Ά;1Jy:оg4N:0v%6J,Ea>xPz0D`UZ^z3@Z~aFQuz,{JGnՐ0 "%/H^T.#Up\Gx y,OIĀ# ,t5 gUceh*>W}OE~>W{uzB@@0(Q 0eIƗzsr,h; a.\3*)ˀĐHV!U5 \ {# srkۊHdQ /-zg3/C, 0ҦxNG;@B]SFwEAp+ H!DwwjNEK#~zfwtY < T&TUeD؏!fУOyg~Q)ퟡz-UcewsG_r?ή@@@@zOݺXLq, J=\׫3DxplUe {D׵ b+\ǂk@ ҇Hl"ig۶i10 ;Z]}{KdzgKhKeʥ lg S   N9s{jGή]+7$=*$9֝oaQHϩ -@R1G9L|o l˂Qjq(mdzUkS;ֽy:$M(@ ;(QaJ@@@ hn {#`qKO1lF&'W(3&mأh!X{Kb')IXz>Y2D1NiҚra+Zq'NG";fi7<0h=K NE?N p,+VbEjIޔ0:5) ( ["[FNnYMCUܢ檦AիDwv MOLr A ]Auk:q 8ڻvpOk<paN@@pPp'r,4M8g).Պb! =0]_e5#v[-=BR2|^q$QW?A`W*pfg 9ؠqeei`vsJ=j@@@@qм[msOg_-kҭK_f~zq@3p<נ k?gssKƟfs EܔkӻHJ*~@b3K:wųHeU-(uFË> W@WȷlB"q"DNJez75 lDZM0( ×=q@{e-[VZ~+O2jҦuYe 䒤BV3dYbtE Qx\VHg˲Q)8&,3"S;5I10Q|?7akB+y   c;>O퀵ќ%Ϝ2Q(SUy2^*.v}/) !+$0@feYCszEQ_US?mۢ&Ru :7}Z$gAO61vJ}Re8b99؇;-a@@@G;Hf <ו)̋kaNE!ZUpI%bV5EoxIRq~jY]yK42Փy  &#»UC7eQoo;ܵ!jy4>"3 Fwʕ\iф;A%UYT)|Kk9|@s.|ϵ{9ht1_]Kf&BMk pK}=Q.K]7@3e  8(d܅ʗmQ)s$UC8Npppt-[clS&j3tM' eSRIE)MmFU5 DeȊϳC\Rסq kݹ Йy׸v 4z `Pp'8Dka,]kos/|<}f".ݗ* 9D)M Ơki-:ʸ.+д,t]d*T҂VD^#]. ~O`w, @W{獨N*sN]F](QkIn-ehz@ñ .i(vl@.Uddȱ)zۖ e2aO`z;@@@# ;o`}N뇺շFgB5L5H܋fyT*ծXV: 9p9JYfO9[TcvZJ@@0B(C/D+@/<cҘeɫ |,jH25 m{6#Bћ~ss^Q@@@#;i||23{`h `y_ Y#m6Z] cX^\]` 9t09TxNXaS(Ļ@"?n6S̫Òrl St46~` 9B&ecOi'ÈebjifX}7Q}c*.l<;  jbk EQZH@k8 86H* YYyؖ5RFoa0 `:;u}ח<1g+e #dpi IG|*l+ I׫Onq #2SE}8Gz}7 `z{K*ս8 c1.VWzj$K`jyh|y+\QX.i4ehd(2 `.Y''IIPl M&Rh4禩)$T.Y]+Z-_3=RSF8[ho %K""(7ԡfj(+((&JJ\քcJRLޔlB+Xwl @7Z^v$SN'LHw;%ҋ j*/l:UU+&&~ښ/.<`I2Lsg$8iiSd}L|~qޣi)"b%m.(RTd8`zٺNȨ3ˆ%11˴ ׯiQ;M?/ w1,1,1,1,1,1,1,1933LIAᡊJJZjm(ZZ"(hJVTۖZJU X!IX=ٓDJkeM2}=};gx8 ?,` 0b,` 088Apg 0(0`  w` 0b,` 088Apg 0(0`  w` 0b,` 088Apg 0(0`  w` 0b,;FDmv  F U]Ȳ[vѲiea 0 wZ|R>Dy}x/i.GLSz/0!lڠA]0` @`@@x]7ҫܿ}j0٤YNml&M:Ŏ_0.8` 0(_Ox{TE!@`[CbA-+NӣG&q0`w<3'WN]_P]\׉z4-6(DF.0Fw&;r¼60;`7d 鞐B_fIQq62ՍN 0 ;`~MQ4w=OhDU=%ԜF,d .%JfYQchD q=1`w& b `[(_LIw]ď"{R]D&1;Ϙ2s;h222jX,i9:] ǪDv'fRb5jpl3U4mi@@@%9` `2i3 w[-ϋ0U)5JgStjr($BՖ{K!ŢD5ĪnVt6RU O`f42[;`pRb+d42TR EFD~ZNtQv23d2d65"sNR̪Mbjz\&E]TTUI` zڃy-s,+9_A@7L:ֹ˪H5RT!;7&sUH!`/~U 5:u{B)66V]'_|AժUHJHHcӟ*VH:"ukÆ B>r X,X@> =7/++l"^?Nuk.Сm{=;F~-^]߱cڿ?l6|2uؑK.ђ%KK.~1iMEQXb,(JMU(+JXs2K :r"BȠi\) yT,F-)c?$` 0?-f3ת*^ϔq?M[ Rj!q~ ]W4+A1-Fkԥu\~S;DǸqD4p@OhΝpBڵgtt4͚5#*Gw}W%#G͛_nݺ`A裏 {iԩy =sL!^?s!z!CǏ ’/Ј# sBA 2D ;,8Nɓ'SLLeÇiӦe@ / *!_T)ի?%("|ԢE ]8g͚5)"".]Jiiiy;EBq){' *8מ={ɓbL {wbccg\!7 |1>$ 'NmC諴a *C"с gҤI"j׮]^6u!a-[911cP\\ƘWXiA&Lt]#YpuDG`_}HR>q>$+H_ՠPٲe<ùH;wzAÆ ciԨ`U}œEܤI1+q0;PdGFFzed\ѐ_Uρ.FNv'EAUȢq`Q}Wn5R Zϟ6O"*B%(C~+WB!L@&hy4z;N~T8V,fWlCn~\=uDpS3.+2awPRo1ZJ?]HZ _z%!PQ9Cr'^'q~pC^BrUM6/>C`"+ \) -Vg?lqn;0/1h ۶m'qO?-l(XᅤFX@RU$P믿.\ @ $_24*IIIbE"_o}'HA`.`u1^oÆ ER ?> HFyÊ%` WCsޮ p|'4L Bܪ=I.Rӳir"SiT! F#-&c2DAcꔫ⊉гHy0BX/^'YҢT!!J ')!`^؋v!6UikigA,^X!{-X? .!dza?*XNdpGz!쳲d~ ~/BxD"1B^r_\ z;GD…  H6BUdž]Ho*u$XX3Bȱb^a>Vkڂsb\ e _P >^ *a `}|6!,`%0 w+z33(+F@2-odLNr:\e'A@jQ5q(;:\.jH`g8.kҤ`x b[ Q*'|<<%W8" xFu"J$,#B++`Ae Z!`Ox8>z&Ĩ:1Ja{ O9D2پ+/E|ZT`,b>xeEo D„;!Ǝ?/,&ZH(`A~^cLh[z \3X [KϞ=}^ص,XɓuǼSNPՆ'#I\uƂ9wH }=8'\CX~~q=:w,ʨ=-.\쮾[(b/SvvxXMݓ]y-.r:&9<'ҕ́d0hmwPre2 a6[t=v\`TO]`3@%^n Yb BXoōx1dTձMV!!!Re7Br2c O8?G FH`}E۰4H3*QEؤuvSe{8e͔d 9ϟOi8x\>bg-*`clFB s``% 1#Ώgc}M$$I64mSiնժP(PZhAUZTnZЖR(BA ڶhѶm$I즛lvgλcfs'm4ɺ}wf9z{;+?V!Ltpp ?p12zUDqwes~ fBY%G?E<_sd n_ ~^~ a? xWof_z Y+<WXy^ysbѮ ?.Y۟bj @pe<-Zf;۩M+SE.%cY]RV+ϛ[]]W"0MOC\p綝8ѢbY Z b$c1Bή[ݷX;>tF8׃E!Eou, ]?|/wgqs﯏/ݎ3?Hq5j,ԃ}?T󐿓Fo3:7/K_wx.v\c_1RЭ7|>/gT5ɎRSۮCECFfS^KKynib Qsdҝ6mUnLe&=W_v2/F/养\cXc\ : \?鷮i/kSWW1eWٻo'ߓL&MEoc'fQ(rsp9T6URG=gyb`^SMFL`iB[CaUS콓9猽wDUJjhDHިX!h ` P#O[չfkWWW۶T\b{qͮl; nY@=pfɋD8MM$bڳo&ƦhzzHx[%h(exvP8qvWn^5L|X;( D'uuwZ_B9]SFZNs. 7<5-u}jv`b {R###0鋋' Ugˋ[uoj¶p\ o1]wHjW>'YVU&(Y ruj2, MN诱$޽}OH+y!es%& ?VP? +{&<qג݉n Z(lsQ7TD#ӛ./{OTp(Yz'Cp4(2l(_Nv[j0B$]*+H,>87[`†-vp}x閙q6dUVU:P +322r055Eӓ3Uc?Y?Gmn셏b408@㓔^H{0HI뉏F ~@W)kyx Z8新-ȋKKL|(D]]FF(VuW Y˲7zP#l#C}E=(AW8FPejh yǤ돑g+Mc PG  @9sµ8%q{c {S%uknn+L3+^غ}g?Å?ePJ|@WYƍXC{H cpl1Q7K[x*<3nen6C$!;@$ e \T*^cJXeJh!x`+p@qSJДT +AT*E?x ԪP'coDPd{d&>el_!vp5ޘ4=П ~e PG @- uocim[mijK Iz^͛7&ɇ@ d#[]ۚb*Mi.dWiR:HceC;3,yVBj;`p@Jy8rw3Li{9,RBmMH˥[aҟ -e2e!+gͭdO+v8e0i@ɮ4-°3\ RTO7A]+CO5J/37-7^|U *l$ TQ;@6l,HZݥՇpԺ]ZJ6kjl @:z(on'% O0 iY",$Ebm(-ۂ)LZCCC_ ؋Q*do@WL&_|8Q!|)n3L4\%'4T`%PkB8ҩ[D{F6'v߄צi>7 &`Y!}0p`hh۷a@-F%RkDt# ~ifB3 p@q !FF`spwjŜu3M3h+UY{$+8v8ñVU8`bW*[6Xl*UUM33{\o;j8i~t\׮a`T@pF5~rrww"VW͎DrɏV5˼hLvK\[\7)**/nd2L"A6z"m޶dDe2[O$<受/2ãd<.Am2a4Js&Sa{7n ,,l&\%8*6P(~T/C8W$2^j(SZ-nFFcLTa.fL"^Pn`޼y!!!cUʏ?#dM =)Z蔔K͛?R`/6e@J 04@LFuFI*5$8o͏k4,>zSH&36P2ybO415'7EN0j>@7bFZ9ᅮc , <p@G}ޅ [0L1q^1ђ]nn]r4:do഻11-7 T< n ,,l &Ml>~R *yyyT\\L|'n$tR*TN3^:rc/~N:tC W4ήV(߾}ݻGzjJMM'R|ׯ_'VKu֭P3޿Fرcq$Nɵz/x{ܹs;ISõָzLVVݸqԤI/]D...~"LT*j֬؈FVs8oݺ5i4rss#{{{umݺN*±lmڴ)sݻdS۶mgϞ,?kٲX:Ν;'Nq4mڔ xL"6F%)IK.%WWWq,#F/}-3jۻ)wTQ׳vwlZh\AEѰaD֭q7W5&%%t& :f͚EB'۷P' 3f}^׮]iD%wa}ʕ"_ VXA4n88p effj;o->>F-ϕ}///krޚ< V4B$Ym_p%jXs6Q?vf&-_p;6"t9by۷A}ԩS'}/|?;wS-DH =$:#޽{{t}Až =U ;RDV?@% b7Z+ǏܧOW-ǎ!͛ʼn3_:B߷o_EOŋE`:6^6c7M@);* 0hHLydUjZϭ87߸qc9s=o-**JlsժU_G|@7nf\&S=1 ;^L#W]UWqtt,w^l"'*͛7/vի'^rnU}ƍ}ڵ_J<*2m1a/~-G~~ @j3Ǐ+M0Ayu:% Wu$a%'@pYS>}v'=_һ]s7yƒ1s=@\5[(v#[l)=:>!ͷ'+p/imm̵kDk̜9sJ~O=O6xB[/j>k[c >0x 痵>99fZ2=򩪼E%5yfZK5@qlr [ U>ବdTpp #?zȬc\`zN+.n@zU!@^O# 4B<;* ;#c0IjnI*j0PXvv|NNNw2PQ F')Th EFed\՝zBh%<I>-3O?jBGG zk)?߸,+k?!Py^^D^i2~lgV^ bw997~)(C:F  n ||"OEZ ޹BAK=fL"''>@-|TA]yhw~PZ-kܔK(*eYYL!%.?O_V!ghu1/qC)-m 1(@?[6((  h" ! JJ"QUMtnS@qa ǚƵe; ŸbxÕ kkY>?Ӛe--۲[D3o}YVS=i.xc,fK,Ƒr()ý@qo-J}G|_{\rC;+n[òBݍ6޸ur)z9u{{cV 9ƅ勐00pY |(z{qzvC!U?pBnupw8>?=6ؓ54dNY}*w}mqN~̥\" ?%K wP,Vק& w) w) w) #Ap22kϹ\& wJ @J @J @J @J ý;@ dH d :0( w) w) w) w) P;% %^p222v۶mm TRp(I@(UDM!8BB "QB"l~}ݷ1cۺ\:v]ouu\n[ptpg 0`E XD 0` w` 0Q;` 0G0` 0@!0` p`` 0Q;` 0G 0`E X3` pp` ~qd`WksRPi٣s}4?)1ž^kyo` ;`yPZeKw#Hz&Jïg p~fۣέs90`w BiRPP 陛(JBY1Y5{]n~sb˸8ZI8` 072LJ{=wwRfY0({tjkb6` 0`yeTڥW(aDJDU*9s[9mfѢE&` 0M6|ד*,]lt+З@K,^eOnWmnr8퉨͉01K5&)NQ00<pKBR2]) ЏLXZ-e˗M>XT%xmӺ!v{42 W*҅f  wn0@u&Q[f!|22x3d+KZL*qHosȈӹeصT(Cy0@v3b ;`yZQݝl5bdY2SxáQzT]'!US(L_X+/>b51)))|}}زt5 ,MLL$UU) XmjF.\Pbw/~Klj_`p(&/'"0=& gez-RSm.+]MJ&]3l!?&jdkd9j"ɪCo22}γѭjGm-i5iҤ@3dyO 6/\(yDѡCbǗL O>.]JիWڵkԥ9(B}駷棏>ZjQf͊c;`ĈJll, wPX3/d%]B^PJƌSp!(]*?s9=n/^kՌEqls.p)WƇ4=:.]D fsr.]O>TD -.@nX3GX2kG!VeimFfE#ȆsN "fêx+Aa[Gqqqf14/رc!SOիi߾}Cw}G=j*۸q>裢ҁV\I^^^Էo_aUXldDCu֭X8/R}vڶm:`:ue"DǏ}7o,87jHϽIz _ƍm֬YC?EFF#<"g/BgϞS\/M|﮻={ЦMUhX2>>^ŋmF~-ܹ\B?0EGGwKhU\YGI ŤW^BԴiS7G8x ͝;^J+V~ K/DÇUV ۷ҥKSǎ5Ma̚5Kpi˖-t9qnT1/^,+WwN_~EĉERg6FEEO?MժUSÆ Ÿŝ =ڊ1>gx޽y1׭['ePP[=1113Gٝ$Ɩn#NR{yYxOMM#J&lr~s%2HSH`Mޑ}BQ#&B B^zTdI6Spp0nݚx:t`8!Cڵ8^BBM>]$/_SN̙3wؾsBdoAh" wTx׆ І>s挨\N8Cw}Bb"#U ?d6?b^Tf!(!dUnlA4CBB"0P&09I& +d׆kE`& KQYfMt?\Wpu>Cs'B?Ч9H$gk$2pW⳩Ss#QCM~XB#v 4/[#j7;0C.cMg}V_$cqvD(٣!}";\XI;S DU0<Ȕz{@?ZEV/ Gf,ʈTIUtJ'{WĤ(~֐6vϟ?֯_/*X%FV#nd'VFUF~G2ejC#E1yNb= ij K0g2 ! @ATsAeR$\XZ껱m +Bw ,T2#P7ҭ[7Zt)矮oq~y@;{=amAr)=ֲx" c X4UZc؇-q ao䁾@;OInWۉ0;;`@7do;"6C'%,^Xe[X^!_ o>4Qm!!` QbU6YayaAehҨ)SF+RRcH1 >;us\+|8*ɨD*=zr.xQFݻW煍^UiLXX *UDvT-WQGyC ə3gD:u$l@!Rq?r $Yj>tMQA<[ZkIKJ> ~ \ @ #dT)qx/~cqKz.Ќ3Dc1޸diӦ /?`` ),#{0+W)rQÄ?wewM@&@|:(ݮ]3LtE%l"d"+*~fokzwm2oͭ>aeL`00GXkh&^q \ 7Vi15B 3*sE!B!Q Gl-NP}qQ,)hBT?ėL0y!!Q1$2ݎ;\xqcHcz)8"9`)΃DL}H9,n:Qjpv8˅N Hd5>vwˀv҄)a# *C;nSxK)'.Z "H0_۶m)nڵn,1 .~y<_`{Ld6#?wl&^8>ڌw$HL-?/VA y0o` ;`y;*(^2scrKfkit%J6YL*`b5j"// c%Ki ЮA 7C"= X2dk6"ø+!NJ1VO"(xˀ妸0` \Č9rb|@#:xI ;׈VKmYsGY%D&JfvYw.;^%Kԥljr0`E D8gim7EU,Uq((ПJәSg)-ߵ:&*NֆEЕ+L?dTX]Pz]I &?3ImۻmV *( (""  ( %( *("VVm6M;l$FMrm's?{woΝ{]{FOo1nCEѽv'NMR)08wwߥɴ1CCC]Y=(٘WD>؎wcc"s{Uw!X,v;!b{qؙ*wYU;KAwB{]˪TfF]+(NJ=כ3C_(JRis1pe)$@PW[F'$UM 㜚P;9Hdո;c;Icq$;ZvWi=G @a@!lrJbT"V7uBӵllϬT?8b(gz* uNQVW$]) N3|F/׸eJi1@  q=.@!gLw"ՙDCN4'W8JȨE\ wf( 5ɉIrC͖4S2IYY5V*|$g՚u猘Pe;fHJ{vJvsWqxٮyU+YΣHS3㹽Y۞ub4Jepsj ;P!t׸P8LpKm>`z*b FcscfȎeTG\[j;+vtwr)%fσQm'gHӔ+( @*l+Ҭo786h@- q*;N{Lv,y1h4]qJ$vjnS] @cN53?vP*ɛ#-5@Jyw< E;C^s9cȫqgƽ4}ΆH$" %H= xE9HP*T;C!J$N2?S3j=9b T=/eRe]( o!X0$@Ryy7ӳ~ TdIk{iס'RWN?RF H&@w0_ L,ik5_D+?k)yȩ/3c/&Mv'y`pcl!q@A⒭bIRv:0F?~ٳ]( d3w7D q*&jESgӟ?|t{s ް>ɱסS\hfxwkyTP%H IrF :7g[L05uY[iWX+Zj&唭0*D [ u򮖳Mas>wRWE2ea%f:J*'`|#w`@q 76bLiR IZ#=$Xn|P:'~Nq5羈Ngo@@բTӇY1;`3}${#}pDX\>}dpI(7w)A"B|98 $@w ;Q@{Bx8@уKpZw$i``!n \ǝ9mp<%^Vro1T88*łXd-QZ+% +6hru(TU$a!%4S53)r;948o7.svd0У&M(<1xRE8'FsR=ca XE\\_X~w?v ptjMaD(pS{8 eܼyvI$n7uXxR9n'o3 :n`ҥMo`VV,dsr.#j)]0=>@{\2Bs$66<oذ6lHŇ|{g͚EVnk64& ]{9;˅_;ڵO>5>}:իW:|ͥ{k@ӦM#GOxf$`5kRڵߟ 7hЀ4 ߳>??t g6&:,M@pUKɩ,T*Ej3;v_:DzJޞd27%M@[>~k`Ux4@9zSņ*`[+WP˖-)99JyII _]wuu{n8ӧ5jԈZ[X< ,Ju@z}-3*;6;vXAN[LqXs n `&NcJ@_b!XƌgY.  8kma|Μ9DT3Vug6Y+Nc۶mI *鬝={ ޴iS{3?sLUzD"l @9 majuNeŔV!ga͇޽{ӧ|iz`88؉Ix,ܳg;oK)h0鶠^8^+-:i0#+'/.&fEEFUn#*7DUYZxvW^\\LVԔ5qh96k -(MS,Ad e?.l_tzfVY K50a^<^/`U999~)ը101'hN;. Ohf^ _hĈSnCll$b_ |HOD3=ҟ(*$OTXzwX#G=~\{J1U?zyU~ԩS:!`Z7= a[;:BH$8b` :PPq O"7@p `Q;j .;{vM\{{nJ%sP `-11t![ [9e]pDy6J7R* #[,ӡBM)5^٠фS^' 9maXTj?ֳTjP*D"lr!'!OAVQ/8zh6SDKJ(d^#c.(XM>>xwHDC(㤔h0XqY#Bap2!?9xf$8 f2يDAzq{qFF6h߾xV FO=kљ'TR" YHHD2n4uv7 9$H./!77#/T<;/H!f@pNpضi2U $PI$=p$I 2 2# 2 @'IDAT/M=mss^jF2Ր ",2+*,R²RQ(( 2 " ˲ʤ2MUuێ;ζ,EݑK:wcq$pxw 2; (" Çc0APDAq ==0(@dwAPDAq@Ad>28;Ad>L8;AdwAPDAq@Ž!`pݖ~h@-iuesSkeŭ꒒pFOQc_0LF^߀ @N(@?|4NJ%gJ_.|o%IL:I ,*{18&i(YD00&+ʋހ _"(vrI^ uQOyi$ K0yi3/6TjQ/ao`$ED$I*;~''u{ @*HA?/rsK$~H s#`X,D;G@q uMwl₝TɞnQ B$dQk\],KXӵ3(NO>`zM24̀//_$???Rs}&OLf|BNNN>C3uO8Wc2aER9 \?r@q LVyB $6g%K-X$RzAXM_iȟg233nܸAtuO [ׯ>R1&Md]>%KP``ݵغ\\\а|fq8F-o7>,,f͚/ct+&fpl9{F-o5B`dN~Vr~gD_nTb!3)H!r&A8%S3ovԘal;3}y>=۷CUUUH^|V\IB  |~~>m߾ygSpXfEmV+++tv(((޼yKutU,vn̙3Yf%|Et=>.vg5Ϟ=߼y3xoOHH{Ҹq6g22fZf /rvO׮]nάNlz7`;w^p!EDDӧJ[[ Zb;HbͶ"0rP0nhϋ>χYavf9++/ߵk/,WĔ^\kjj(77֮]K7oeYVYqga\"##iܹ|l[np^ץKB3]o &۶-G̙3)::߿O ۷]gΔʷDRRii)_BCC_rPw(ݶm[hۂ"(Q%AH1Q4@J""@[MLH&bHB 1$ @L*F@Ҷvwg#33m.tɲ3gg3 #pg MR&n7 Xəɦ=rs‘sàPTXPyu"A7(o s n[S_8۷ҥKUw…Ѭ3bԨQJ$#{ :DnG^R%궦8^zdA,>~h8њO K`g7D/ӦMSud!̙ǀ,<d!K ^~DWxȸ#>~m{GVV n?~q@~ؗ8` 04H-RDPE<dT]Qow6.ab@ @׮]Sݻw}W8 eVd1 L< w3G YW>?|Ȭ* 0,SÝxSII %(69T,6( t|*cVD>(Жo@{%KmV#FФIpwbbYG6ȸCvdKXFtBxGΝ*돪*N@qdy >@Vi;s >[nU-[i.G?u*3 [3(`C);\Ry!9` 0tȘ;bJjnnu 3{ߞ2- c"DPKzՏhҢI>zˇwe*UטLΝ(۶mSU^0Bرc* D-<-JxO;?zg;}"!QGێc2#*-Od3f1cF'#Gɓ'4k,uF5dusuҘЊdҍN`Q;>w'`sqG8\1@d靀O> 1IՉq0@F`@dl\mBpSfZےXmS#TVZB`}5Ƹ@VQۋ5yurX͛?ɼߏ,/ moe'` tg, Jڰm>& TTqӠ" JBuR/09gTwIBEET+.H,_iڜU_V-VC@9H`w w o:%Ε|j Sqq]O {6SM~j SR8bZb΂OBMl7iɸ/G{92`{ `p1_ `SND{tX?dHv(S!:}ZT$i tjëW#W,ꑗKE9/_qM+1`{ 6mb0E(9bL 뵣 aUT/FDe1Ո1("% NXY^IJ2#Zlnn4[Gy9OM{3pEM;` ts,H@LBYe :} 6t_p9)'Wӵk7)qnXzthdg^ \K&qPSC?'9` 0n { g1$vKj2^0'*ddQaqARxwr]n vdޱj0BBCek%WYjo Fz=35` spO /x iWqleiV'oҴhWTYvܬ:Hzu}W-5gA;i2s444q[`{ @-@RI+p~C;aExO_JׯHĪ5!ۥe*JTVVJyɫH/|` 0iX3&&CC1SQy2*(MAHPV^nA..Z]j%o` `ڵkRNKrRΎH)WWW`{Ŋ~$P |*++W%4ΧB|ZUUenjyYP 6I)MB76l85kּ&#[]]}~o%)C^jճO2MuիW?iItxzjTǮ${^iJukT"#-Jc{&fY(~f 02 ~#:ܫ.>7nM@(i(ETUU}E){J@G"~#Dt̴nݺ/+xayV]L<ɄlCG:Qt%_`- DݻRg` 0n;`i];[X~* ;1{tI=)uqZT[+Z#n+/*|eAa_x֝ZfPK'+oyݝ\ʟ0X3&iB?;KL=XhMPlsml+tU2ɫ"3BrY` pg MDP"Z˪̭P*YD.\p('c'~*+/<BS zef 0YX3&LuKF{IYI$QĮ.N+u&YJW _?0jhO,` 0YX3&i{ܝPY('77XLL͉t۹y={۟O]@|2` wꦣ.9E0s*͜ec/(Ш-BG_}zS0Ғjv('R~> K;r[|` wpʫ#:xV5ى(+;'dkƯBg`4O:` ;`@lˎքNT qK>]'ߟ c";"NCӴцa!I2<ho43  HV ݸot"FqϞ$Iuuѝt]W+555j5 H s oLhv7` >04HۻRY^. wiׅѴBYkkv; > 0@`@8Y򞥽3uXAFB)ɸJ>"aGh;` spg MQІaPSCc4㎒aRkKKDSMPAaBln•Sw ۞xoxS` 0L0xMMI!Oojh  {Y~`N])8::*cgb͊6$!`01b3;+f XhX;HfR a!;RR8r I~ri~v6CZ&5Ad4 IH!/W֑`mgoodGv56-z̲kɛL/·%4?w#'L,G7CLMf<u | u70|'&gKjc>fy`y`Fb84{wAkwaMf"s8HˣDcfNwL*m0ZUiD =/~O ^p"<˻+0x팄a 3$'jCмੋ VЬ!YACp@C l?@&N|!w4OdJ 2CD=ll_}d/o^ekM;sR^&WGH'??I`˩m~:6AjuO-W(6 ,*ZX/Ez`2 j0D bqVVA(b2јL` @p$'kJfZ- hH-dD~v,JfVw"Zh216=]4g)8؈`u>vr:ݐ*[.' Qj)`8WEET^?94;"jF8A.GdGV(@TSG.-s^Kz`.mkvN'dWqƆ99#i〧OS@@&.=ػidF޲RvP2JK6edl$?l+: @ɞ^Jej\^>66fZ: BĆ]7 @>*UIWW/{ -<4i͜9z!Dc5G{{2dWh.C iE.qt qKk PV$5jL&UVVu$҅ hРAt'{{m0,KLIUhHLtVՒ{Iғ'O֭[TPP__w֍oTt "777 !ooo/Rrr2M2iƌd4@>B='O3S~Ȧ!rrr(!!ڵk?۱cG7op?rHIm0Q2  @rZmhVZ]sQ>sZp!ݻܹCÆ e˖QXX;233[˗/hھ};={vEk׮sܹzVs1Zr%uޝCk֬EFF1rY0gg%**zg+Z.pԎDt#o@pLD[KD6tP#___Zj7ɉ )((|6^VVg٬xjj*M<Ϛ5kqSN¿Φruu3EN,< [l'~RI7o)o|Ѩ4 i>, `'TYSռ,ŲzmFӦMe.]E>3^YϱY|ݻws![[[Ǐ͛iv˂#Gq,Rov VI#  ;{p6=a }0/g{+Wԍ@~Jp)##4zh^JeNzcVTTPnn.}ԨQt~jSdo4Fɞ$ wH ݽ`iݺugݩ|Ŋt=ҥK:u*?ʹ_z״k׮:yДf|-'=|nr{b7 -ee:cƌ7DGjβn)ZAwH ׷L}(X.cd6ΞR8P7ѰS!fr`m;  ިw8ڿxC~4|Ϗp `}; PYꪹtxe98 -'q`c! ZR򷯷7<yTJ OKqò1| `Bwwek.W q%Om0tE"Ѣ\=(.^?{q>2 zC0~!Yl:mEںzz.͵|xJN[Is-˒ğY4My F_1+;#\7:ⅅ~ijʸmYRRw8TʝNIy0g]HDF uJa? l>!">ĈО[%vnv{i#=nv$^QGUIzԲd4e4e8 k!75 p`gH46H#MM.u9Cvo1DJSX9"}4+fY"O/'(.08>_HDC"I|zT pzWc _zcRQa,[Ӭc{ ph`Mr w"b)HD p R ;1@@ w"b)HD p R ;1@@ w"b)HD p R]ٷmu]HH@ gXt^(IENDB`././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/doc/source/admin/appdev-guide/figures/structure.vdx0000664000175000017500000061171500000000000025073 0ustar00zuulzuul000000000000001.36590.3152.66291.09280.68290.1575-0.01.36590.3150.68290.15750.68290.1575-0.0#000000#0000001.00.010.0125#0000001040.00312510#000000Arial0.138888888888888900.00.00.0-1.00.00.0100.00.15751.36590.15750.682950.00.682950.3151.36590.3150.682950.15750.682950.15750.0#000000#0000001.00.010.00625#00cccc1000.0031251111110001.09260.31471.36570.15720.01.00.01.0NURBS(1.0,3,0,0,0.9106,0.999,0.0,1.0,0.9999,0.7768,0.0,1.0)1.0926-3.0E-40.01.00.01.0NURBS(1.0,3,0,0,0.9999,0.2235,0.0,1.0,0.9106,-0.001,0.0,1.0)0.2732-3.0E-41.0E-40.15720.01.00.01.0NURBS(1.0,3,0,0,0.0898,-0.001,0.0,1.0,1.0E-4,0.2235,0.0,1.0)0.27320.31470.01.00.01.0NURBS(1.0,3,0,0,1.0E-4,0.7768,0.0,1.0,0.0898,0.999,0.0,1.0)1.09260.31471images.lst1.00121.00122.43726.8740.50060.5006-0.0#000000#0000001.00.010.0125#0000001040.0031251100.00.50061.00120.50060.50060.00.50061.00121.00121.00120.50060.50060.50060.50060.0111111iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAYAAADDPmHLAAAgAElEQVR4nO2daZBc13men3P32/vsA4AgNi7gIhIkTUmh6BAuWatlexw5FS+RBakC25FdMfIjyU8QVU7KrrhiqhwvMeIyaEmJaLssSKDlTbaGkmVRpBiBErhhGc5gMFvPTE/vt++eH909093TPZilB6SlflEX3X373HNPn+897/ed7557B3rooYceeuihhx566KGHHnrooYceeuihhx566KGHHnrooYceeuihhx566KGHHnr4PoF4qxvQirNnhg5E48lTB+68f8wwIgc9z80WcpmLb77x3XFg/OTpxefe6jZ+P+FtRYCzZ4YOHH345y8++iO/nNJ1DYIFCDMQpAGYu36NuekJioXsxRsTr18slwoXgYs9UmwfylvdgBac+qHjv5JSRBbf8UGYCOluhPoIhAX2HL6fPQdXIEgfA/dYIZdhbnqC8fuukc8uXVyYmboI7Dopzp4ZOg08Wft4ERivbRdPnl6c2q3z7gbeVgrwpT/60Hc+/LO/fozAAsKWb0MQGkKKg2QihAQUIViEcAUR5rArFnPT18ik55ibvsbc9MQqIegSKc6eGfpjTY+fuP+Rd3Pn/Y8QT/Yzd/0ay+lZ5qYnWFman8xnl8dr5xw/eXrx5Z2eczfxtiLA+Bd+OXzP+34GwkbjNxNBNH2Wq2SQEyAZCEoQVkkhwiWAJuMsp2cp5le2TYr6yB/75Gfp6zNBiiBJElCAsLoJKk1EnJ95M7uyOD9ulYt1Qryt3NXbhgBnzww98cgTnxp/x8OPs2p0QfV9qxjQmSCICEKOgxRFCBtBBYJlCJcQeNTdxnJ6lrnr18gszkEDIehAirNnhp4Axh96/Bd58JFjhN587Xyids4kQh5ASCpC0iFcAYqIMAvAcnp2dcukZ8dr6jROlRS5bXfcDvF2IsDHP/Qz/+PcyJ6B2p5Gw69jAJ1IEDb8LwCEDlIEISUQklwbpVkIi1XFYC24nJuuqoVjV6CFFMC5/uE7D/7kx/47QeXF2tlE51elHyElQIojSRoIB0ERwhwCu+G8k+SyK5O55YXxpYWpcaoEvGVu4+1EgN/+2Km/OCWRqdmv0Yg0uYVwC4Ro/By61wkDG0kZRigjSNptCOECVi2OWAFYVYe66yjmV9D0GD/58d/DVK9A6NJkbAGiqSvbEEIoICUQSl+NjAKBBVQAgRAahXy+phDXWVmaHn/ztb8/D5zbTYV42xDgz373XV8d+9ivHw+D+m8NG15ajSva7GPDz4G3DEGW+k8WyhBCilXfiyjISYQUQUgB4NakO4vAx65Y2M4IpiEIgyyEPuBB6NTe18/XbHgh1u9r6nIpXg1q5RhCjiGkGELIIOkIEcW2Hf728790ceHGd47vFgmapoFnzwwlgTEgyy32TZHY0LHQzwJedcfqiA9X+yxcR4bmz6KDMoRBGfxlViuSEwih10YyhOEKYbCydojQEVISIe9HSDFkrYIpzUJQrI10CYQGaCAgxAf8KhlCDwiq511tTge1CHKEQb7KpdW2pRBSEkm/E90Y5LEPnDr2hT/6+Ang05voxi1jlQBP/8ae3z581z2nHvnhn8DzfKauvMJz75i/ODN5ebxUyJ3f5Xl1cnjvXSmCMgBhoy6FnWKBViVo5xhCCD1Cb4GqUUTNuDHC0GG9ktQPcwiDQtWWTadpJBsN7+ttlEHIDa0JqipB0FB3naYtilAnhp8h9FfwvUVE/DEGhgcAjrPbBBjdkzw1NOgz/cZfEU3sZe9tI9x534PH9A/+62Nz16+deumxCVaWF8bffOO75+n+/PZY/9A+QtxqR4aNoVy7kb15+Q/cBcCtfVIQcoIwdNr4vrDN23aKUt3dVo1aiVvfGfotBURDcFMv30KMsEDozyGEB9UgdFewSoDMUjF7+4G+VBg4FFbKlHLTZBaiqFoKMzbIXfc/iBFJHn/8/R89Pjd9jW+983p2eWFmfGbqSp0QO8mAHY9GFcLAadh186BPtHxufhcS+nkIq0EWQoAcp6a3hGG7WKJul7C5ynZlRUOR+n9hO0UJEI0KsHpgm5hAtOwL8szdeLX1h3cVqwQoFu2nclnryWTKhNAj8D0Cv4Rnp7GtWQorcWRtAM1IkeobYGTvwZRumGOlYm5sbnqCbzw8M5memRxfWpipE2Ir8cPBvoEBQm+ZzqO70QjV92vEaPiu9jYMbAjyrElsHBGGNUluqFM0HFc/Y9iGcE22bc1NhB3KVb8LV9u3VqhtgBg275MkB3DgVigA8NT0VOZUMrUv1Voo8IoEXhG3Modd1Cnnkqj6CIo2iBFNsXf/UQ7ddeygqiknltOzJ+auX+O5d8xfvPy9F+tk2DB+2H/kncdCv1Dzy/WR3Xmqt/p/fcSJhu9DIPQJ/dxqSSEMBBKENuss1GZ0t1WcJrcgGtrZWq51RsD6kY1oHyACQjQcFzrMTU9CNSjfFawS4OTpxdzZM0Pni0X7RCymdzwgDGw8O41nV6/QlbIJNGMvijGKHtmLpg9yx32jqJp07F9W4wdeemyChZnJ8zNTV8ZpEz/EEgPHQj9Ds4Gb3zWNStGyr8Vnh0Gh6ndF7ScKpUauljrbTik32t9aqs3IhvUGbxnZq6+CdfvW+GOC0OkfPgywblB2C00kPntm6MDwSHzyrqMj26tMqKjGHlRzL3r0DjQjhaYLVDVAVd3VHHl69np2durK+aWFmXGAH3rPh84dve8QhOF647cau+V70fI5DO2azIvqJpmNlbRp9fp9awrUSIJ2iR7aju715ToQo+1xNWIIEz1xFDD56rN/MHntleeeBM53e2q+TsXOnhn64x9614EThqHuuHJJSaCZ+zHid6NHj6DpGqpqI8sVZMlZzcsPDQ9gaH5DtLzNqD/0qwRAVLtcqA2d3lq8hWCr6GDojkbsYMzWKL9DQmijOuXIO5C1IYQUpZDLMnXlBS6//Oz5TPryqW5ddm5HgB2pwEZQ9VGMxH0Ysbsw4neiyCVkuYwsCgjhEvgBgVfBdzIEbmtASNPnMGww4Goo7jaUlQGp6ZimH7sZA7UL1FrKiKayHeppPG6zdQoBqAhlAEndg6SOImkHQJh89fx/nrx26fyxbqiB3Lrjwng5995HleN7b0serF7q7B4Cv4hTfpNy9tsUFr9GpbyA68ig3AfyIZAHkNQhVHMfavRBJO02EAaEbjWoCxuzbfXsW/29y9oMgdqrDwS1aV1APUPXXIZanqEeTDZ/t1a+4TjRqUxrnZsr21ymuf1hUCBw5wncGYRkImmHSPQfSr3+0mfmL4yXv7Vxj98cnVYEPTl7Izd++8H+ndbfEWFgYeVexsq9zMrMMyjaIGbifqL97yaSfAhZ1ZB0F8m4HyEUAt/CznyewL5ar6H22ngZRjS8NIyypiCs9r6hzGo8HzbWtIGcr5vDrz/vhnWuCxJv7kbCoIBb/DqS8QADwwcAjtEFrFMAgAvj5akffad6fHRPousq0AmBX8YpT1Jc/kdWZv+cUubbeK6Nat6NrA4iJA018jCBM0HoplkdLaLNiGod2bC5kdikAl2qc8tq0f681Smui6wdwHFKfPebT5+/MF5+jh2io3Vd139q9sZbtk4Bu3SN5etPM/nSJ6iUJgED8NH7fw5JHWVV0sP65q+9EgA+IUFt8wnDgLCxTP09a+Wr9fnNdXWsM2hTZ7DWrnqdrfXU20trnfU2VusMW867VqdDZr57eaGOBDh5evGL6XRhsmtn2iYCv8j1i79aJYGIISQNY/jfISnDa8Zqa8Q2xqx39DojtpKoxYht61w7b7jOiH4bYjS2sQMxmgxdb5/fRApJ0bEri1BdTbRjbKjvFct9cmE+TxiGbbeb5Em6hjUSTINIIoSGMfxJhDrSeSSGjZ3ZzohBkyGbjEjjKOxsoPaje61Me2IEtKrKerVo3NbKCMmAIE9m/vWu9W3bGKCOC+Pll9/7qHpi795k+0xU21zoziGagqoqwtChlHme5J4xJCmCEBZK5F4CN03oLnX0n2tJnXZ+vvV1ozLt6qyX3cBvbxgLrJVpqrPDrEBSEij6IJOXn2fsF1/4BF3ATSO8SsV9amGhRQXq/zoow+Y2qsm9Nlun+p3KHFMXf4VKeQ6kUYQwMAd/GnPo51DM+6qJn5ZRu07y2a5atHMjdbVo40Z2QS2qaxrzZNJdyQEBm7sx5NxiuvDk0HBsTQU2Lf1ig0/h+qo2ytjWUClcZuo7n2Lkjv9IavSDEOaQzSiyvgd4H74zj29fx7eu4TuzdLrsGjbtE2uDft3UbhNTNtEy9WsoJ9bVt/68ze3rUGcoqvdChGUgHG/XN9vBhi4A4MJ42X7/v9CNRMI4ruuNfOms/1vyDNtwI0HgUFj6Gpkbf4pdToPUh2I8gCQPIilxZG0QNXIANXYfsjqAkPXqsrDa1cb2Mswm3cgmJX9DV7O9KaqiDyApUb72V386eWG8/PRm+upm2OytYU/dmM6euicxuqoC7dffVbEpgdhwtG+GXCG+WyA79yzZuWcBiPQ9TGLwCSJ9D2PG7kcEyyjyKIq5gJ54gMDL41Vu4DtL+M4y1TWBDQ3omlqslWub9m2ts+G8TUmtFrUQsoZdsaBLMwDYJAHql4pLRftENKoBW/ACrVg1/EZG3h65SpmXKGVeAgGSEicx9ATR1MMkho4jyxaSfB1NGYEwD0LFd1bwKrP4znzLtYdGUjQ2eu11rfmtxNhE7n+bbkRIOpm5mQ16YOvYys2hT87P5U8cPjJw85KrEE3EBxr6tsOCCrpDLt8tsDL7LCuzVXUwY3cSHz5OYugJzNg+CJaQ5TSyfmd1RAYhXmUS357Bs96gupSstUU3UYtVYqz38etjga2rhSQb2PZboAAAJ08vTp09w7m9+5IndL0xdNjAiYuwoQPWY/tphPXTxOYK15PLKlzGKlwmfe0PkZQYieHjxPrq6uAjwiKqshc15gMqvruEb0/iFV8gcG9s0PqWX7FLQaeQDQAy6bk2bdk+thSCnT0z9ODAYOTiocMbq0DrJfjtYSNibbrkpmDE7yJZV4f4HbVI26V6j4KKZ12isnSW+rL17WG9G1lrePN37dyIpPZj9j3CN//hSzz2kS90LQOz5Yr++L+OfPX+B0aPK3KbFMKWa9vaAd0h1sbnldUYyeHj9O39cWJ9D1JdlCnjOzewZv9LtxrQ0o5mtWj3nWrsQ0/ez19+/g/4yCe/1bWe2PKlPs8LnlyYLzQnhDZMDLHB1i693GHbcuJpi+etTb18t0Bm5gLXXvxF5q/9EWACCrI6jJb8YLf6vYb6eRsTRu2vEwh5dZ3meDdbsGUCnDy9+NxiujTuecFNOrlzZ29k7HXZxt0mF53JNX/1f7E49X+pGiaPGjsGovOC2e6hlRj+agwwNz3R1TNt62K/5wXnlpZKWxqRGxq7neFvFbluoiBzV/4Az6sAHgIHLXZvVw2wWUhypOs5ANgmAU6eXnx6abE0uRUJv+kIprZtwrjbPt82yOW5BRYnPwciVVWByH4Qt/7RSpKSIJOe7X692z3Qtv0nM0vlTXT8Bp28qZG4DRVZd56dkSv95ufwfAnQECJAix7umgE2CyGpXc8BwA4IcPL04tPz88XJzY6k9iNx8yN6vbE3SawukMtz8qTf/CwoVflXI4duqQpISgLofg4AdkAAAMfxz2Wz1qZHVKNBdmzkjc6xBSNv9rwLE5/DCyLVy9CSektVQEjVezRs26Lbt+nvdMXnU8tLVvZmBl8V4bYdvr0g71aTy3PzLEx8DpTqYtxbqQJrCvA2igGgepGoWHSeKhad5lHWYvCuGfmtIFcDsRaufeYtUYG6AtBl/w87VwCAp9ILpTU70EUjN2yrld1qcjVU6Ll5Fq599parQF0Bup0DgC4Q4OTpxVyp5J6zHW/TAdfmfTTNxOJWkqs9qebeAhUQkrorOQDojgIAPLmYLm/T2LRT8dpXzRJ8a8nVvh7PyTN/9TO3VAV2KwcAXSLAydOLU9msfc5x/A6dz7ptw47eodFbDbyOXOyMXHNX/wTPv3UqsFs5AOieAgA8tZi2Ohh7a75384Zefy5uMoJ3TqwQzykw/drv3hIVaMkBdP1JIV0jwMnTiy8Xi864ZujIiry1jt7Q8M3ueVvE6hK5GlVr7sqfUKn4qyogGwe71ZVNqM8ACrkMu/EI2a7e+en74ZOZTIW+wX0cOPpvGD7wPrTI8MYGIWzoWNp0fD3a3+a2i+SafvV/rqqAFj3M9FSBxYUi5ZLTtn+2g7oClIq5XXlO0E2XhW8FF8bLU+99p3J8ZEQ/6FYmICyQHLyX5NAjyHIUzy3hezbNkdfOV/Q0YV1lovNXO0Qp9xrDhz6GIpeRhEXg+6wszmCVHfLZCq5bjYkURW5++NMWoOjDyNoAL39r/Pk/+3K6K0vBG9H1e789NziXXigihECIERan/o7c3F+iKln2HHqc/Ud/msTgOxCSvjqSgiAgCLYzwtcH9Z0UZEcq0k61alVff2VNBQb33YuiatSpZlkuKxmLuZkcmeUSpaKN7wVteq0zZK26/C67vPD2VwCo3k/4vndrJ/buS6aEsIn33w0ijlVYIpd+gcCdIRofon/Po0QSRxCyhlPJEQbumu9twk3uEej2sG533g5rUAFK2dcYObymAmHoYxUWagOA6qsk8P0Ax/Gxyi6OU30eshBws+cvqOZ+SsUKr7z0j5/vxvMAWtF1AgB8+HEjq+vKWCQicCoFfG8JI9ZHcuA+wkCnkHmDwvIlQm+OSGKIvpFHiCQOQwi2tbzml2vYQNV3gE3cfLLJ83hugcHbfgr8qxjRfrLpy4QEqySQpOpDq4QAIQnCMMR1A2zbx7F9giBEkgSStP6ERvIYmfQsV1556Xw3HgnTil0hwIXx8ss/+k7lxMhoPKWZw0hSCit/g3LhOkKyiaYOEY0fwPdCSitXKWVfRxJZYsl99I8+gpk4hGPn8JxSVbrrFe/CotNukKu48hqjh38BRWmjAlKDEjSpAki1uCDwA1zXx3V8wqAaFNWVQYvdxdz0BFNXX/mNC+Plqa23bmPsCgEAPvzDZsqMqMdVpULg5zHj+zGjt1WNnruObc2jaDKx1GGMyB4cq0Ix8wpW8RqKXCHZfwd9o4+iR0bwnCKuW1ynDFVs3mIbltwhuTw3z+D+mgpE+sktXqF6R2/z6G90C/X3Up0YVB8U6fsBnhsgawPosduZuvIKc9MTT10YLy9stZU3w64R4Cd+JHrRc/1fHhiMGEb0Dkq5CWxrHlmRiCQOYpiD2FaF4so1fC9bJUP/PahqCquYJ7/4Ep49g6I4pIbuITV0DFk2cd0SgWfXiLDeat0Vic3VJqipwJGPN6hAgFVMbzj6V/dJLd/XyygRjPgBLl/6Nh/91Hf//VZ/2mawawS4MF62P/CYbsTj+nFZLmPG9mOYQ/iuRzF7GdctoGg6sdQRZDlOpVykmHmdkAqaYZLoP4oQEcr5ZXJLFwm8eQxTo3/0QZKD7wCkavAY+lsz+jY1fzOlPCfP0O3/Cvyr6JE+8ktXIQw2MfrbKIQQmPHbUc0hXv3OP2X/z/kbv7mVn7lZ7Pa1zKdmZ/OnYjEtVS5MAgJFidA3+ii+W8Eq3iBb+C6yaqAZ/cSSD+FUHKzCFIWVGYxIAiPWR6J/H3bFIrf4JuHiZYxogljiCP2jH8Su2BSz0xQyVwl8u0MzGu7CWedC1u3Y9m1scxN/zsEHfg1THUVW5kkNH2Vl/nvNN3vUmlJzDPUPzZGnqB1SO65cLOza08J3TQFgVQXmbdsfS6Wq69qDwMWxlnDdbM3ohxBCoVKap1xcIAwtzPhtmNERfE9QLsxRLi0gyT5mfD+R2G34tkspO0EpewURLhGND9I/+hCR+EFCwLYytJvGtcPOJhTr54dNKmD2UVi6CgQto782O9hg9AsBkb57kJUI//SV8xcvjJef2VFTO2BXCQC1vMC7tMl8vjJm6DKqVjtlCL5n4VQyhIGDGbsdMzZKGPiU89epWCtIsiCSOIBhDuHZDsXcJE5lBVlViCQOoJvDOOUCpdwEVnEKVSkRS+5hYM/DmPEDuHYe16n+abitG3oTR7QpUlh5lT13nkCtxQKEPpVSuir3UqOBq36+re+vvddjt1Mu7V4OAG4BAaBKgg++xzi3vGyllpdKB4MgMFRVRlaqU50wDPCcHE5lBSSZaPIwuhbF8xyK2Qk8t4SkaERTR9C0WDVeWLmG5xVR9Rix1BFUNUq5WCC/fAm3MoeqOiQHjzCw52F0cxjPLVWnlU3Yxvhvc0jrLs/JM3zgo+BfRYv0UcxcY3VGsMnRLyRBbPBhMulZLl/69hcvjJef33pjt/Vzdh9nzwx9HBiLx9WxVF+E/n5zNQlSTbFWU6+KmsCIDOE7ZUr5aVy3jKJFULUURmQIzy5Qyk/jBza6GUfTE5jRUVy7gFVawrWXMGP9GNFB9OghQqLklqfIZ67h2oUttXmrqfz3fPTrmOoriDBNduESufSlLY1+IaD/wBiXL32b8b985kd+6cxSXQE2E45sGrdEAVpxYbz88oXx8jMfeMw4l8/Zk4vp4tGK5aUUVULTlNXbowPfxrYyOE4ePTpKNL4fSQisYppycRY/sDGje4nE94EfYJXSlArzhLjoZpJE6hBhIFPO36CYnSD0F4hEdAb2PEBq8F5CBI6dJwi81bZtbZLQ+UvXzjK4/8eRwik0M1VTgXDTo18zBldzADNT1z797HOrOYB1YeNO8JYoQDucPTP0IHBKVaWxwaFIKpUy0DSFIAyq2bHaxR8kGcMcQZYFVilLpbQAkoyqRTEiI8iSwK1kKJcWkRUV3Yyhm8PIksCxstjWIsgKZjSJEbsdzdiDVa5QyE6TW56o5hhg6/ngNnhs7K+JmhNILJNLXyK/+OqmR78W2UNs6F089+VnOP7Tf10fqBuN/m0pw9uGAI1YcxHaWDJlkEpV78hdvSoXVK/HybKJERmC0KNcmMOxc8hqBFWLY0ZHEKGNVVzEtpfRjASaHsOIDCHCAKu4gG1nUPUoZjSJFtmLoo1ilS1yyxPkM2/u6DcIYPTwj3PPu34VTbxI4DvMXfkyhO5NR78QAjN5N2byKF9+5g+zP/aJb47SfI2TNp8bsWkyvC0JUMfZM0MHgDFJ4lQioR/s7zcwTYWg5ZIwYYhm9GMYSVynQjk/gR+EKJqJZqSIRIfwnSJWcRrP99GMGJoRxzAHIahQzM3hOivo0X7MaB+auRdZ7ccqWSzOvoxVWurYxo07UPDOj/w5ifgKirRAfvEVCsuv3nT0CyGI9N2PHj/CM//7t77+M//hlQ/TfgEcN9nf+LqN9r+N0Ogikkk9lUxqqKq0bn0AhJixfaiKgmMtYZUWCYWCqpmY0WFUNYJnZ6hYaYJQoBkxdD2BYfbhu0WK+Tk8r4wZ68OIDqKZexBSimJ+mczC61TWkWHjJMPIgQ9y96O/RlT/fwS+w8LEXzWrQJvRLwmIDb8HRR/k9//bf/ryp3596d/ScQUkbPBdp+9X8ZYEgdvBhfHywoXx8hfP/33pN3/0XdrkyopNqeQdhRBVlWpRepUErp2nYmUJkIjED2CYKQK3QrkwjV3JEwJG7DZMs5/AtSgXF7DKGYIwwIgMEk/uIwwE5dwNCrlpQm8RTQsYGLmL1PC9yIqB61j4XoelXw2cKGavMrT//aiagqI4EAY41mLn0V9zC3rsCKVimRe+8fXzf/MN61tUF+902uSbfN9YptpRNfyzIUAj6rOIDz1unisWvcls1j7qOEFKlgWKItVWBoUEvotTyVKp5JAUg1jyEKpm4laWKRfTOHYJZJ1o4nYMPYJbKVHKT+M4VpUMsT1EowP4ToVyfpZSYRaCDGbEYGjvffQN300YCpxKnmD1D16th+cUSA49gaEvo+pJrPybiNX1AmuGlxpUwex7kEx6lpdeePHv/v556yI3N/JmSFD/+zqr+GfjAm6GuotQFDEWjyupWExBkUVtuVh99We1g83YCLoeIXDzlIvpWrxgoBlJItEBAreEVZjB811UI46imZhmPyKoULGy2DVCRRP9aOYImjFKuVQmv3KD7NJUwzWJte596L2/T/+gj6EXKGZepZR5vXn0N8wMJFkjsedDXL70bX7vdz77s5/+TO6bbE7uO5XxgQJNf8W6in+WCtAOdRfxxX8o/eb73m1M5vMeFcs/CqAoNUOEIUEY4jolKtYKrudhxvZiRuJIhFjFNFZpGdd3MKKjxBJ7EKFHpZSmXM7iBwGyahJL3o6hq1SKKxRWJqiUF1DkErHkAAOjdxNJ3AZApby2jM918sQH3k3ELKJoSSqFNxGELenh6ntF70eL7GfqyiuMf+31z7z8hpNh86O8tZwD5OkQDH7fKEA7NM4iTFM+GI1I6PraX+ds7HhFjRCNDSJLPhUri1VaQlYjKFqESGQQVZWwy0tY5QxIKroZR9WimGaCwC1Sys/gej5GrK865TSHkZUU5WKZ9OwlrGKGB574LYZGFCIRm9LKa1jZ19vmBVRzD5H+R+s5gPvYfvCXAYob9dH3NQEaUXcRssxYNCKnIqZAlsXqZddGA2h6nGi8n9B3sIrTOE6ArBloWgwzOohMCaucw7ZWkLU4qm5imH1oqoprFyjnr4NsoplJVD2KHtkDIo6sHyA59CiDg8sEvkN25m8h9FoWiwj0+F3o8bu58Pmzsz/xyX/qNAXciAQuMEftL09vhFv/tKO3CLW7aj4BfOLsmaGP5wuMaZoYi5gCw5CQJVaJULHy2JUCQkA0sZ9IQiXwChTz81ilZRQtgqbHSQ7vAy9HqZBmpbCEZiaRFY1o31FkKcSxMpQykxSzC5jRJIo+Qd/wMRw3iqaCkbiDSv71dcnd+t1AmaWVOUBlaz5/BVigjb9vh++bGGArqM8ifuxfRs5V7HCyWAqOen6YIgyRZRruIQDHLlCxcriuRzR5gGg0CUGFcnEeq1zAcX2MyAjxxAgSPnZpifhcYakAAAPeSURBVFIxi+M5IGmY8b1ETQPHymEVlwEbxTiCGfGQtSROcbI6I2i4UqjH7kSSI/zNF//i4leet77B5vw+wCywyBYygT8wCtAOtb+/+2ng02fPDD1oWeEpWQ7GDF2kolEJpcFFBIFDLjO16iKSA/ciYVMqzJHPTFBSosiqTiR+EFWRcO2lan6htIKmR1G0GLHIXnJL38FMPoRhJIlEVfT4EezCGw2tEiAUltOzFMvBAms22kgFSsAEYG21D36gCdCIVhdRKvtjisJYNNLBRVh5kATR6CCpRITAK2AV06wsZVHUCKpqEk3diSI5ONYypewSlhJF1aMsz3wFWfkAkWgcI34Ez7pBGFi1OABkNYlTWWIlHyyy3gXQ8nmBqvE7JyI2wA+kC7gZ6i7iI09Ez9k2k6VSeNTz1lxEWL9XIQTXKWGVcjiOhx7dQzzRhyQ8KuXlaqDoOCBHSaRuQ9c0HGsZ21pB0YbRzQS6aSAkFa8yX00DKyZa9DBTV17h2b/+3pdem3AXaT/N84GrwHV2sEagpwAbYJ2LqISn5II/pusiFY0IFKXBRYQOhewsBSFQVYNo8hCa4mOVsliFGaxyFllWMSOjSGFANv0iupkinjyIFtmPU7wMgYUkRwBw7AoTN9xl2geBeeB7VJM7O8IPzDSwm6hfrlZVxiKGQDcEslRf8ds8tdSNGPF4CkKHcnEB23GQ5AiSJNO353EO3n2M1EA/rjWNnXsZxbwNI3mMv/vC07z/58c/ynrjXwcuUU3r7hg9BdgGTp5efBp4+uyZoQM5NxwTxfCUrnPQNAS6Vl/WXV32XSnnqVgFhBBEYwOkYgahX6KQWyC/dIkbqkks8RCqWVWBugIUS3ar/3eAi1QJ0DX0CLADtLqISiU8JUvhmGGQMk2BIlcFtj69K+SXKBYEsqwQjR+iVJinlJ9j4UaKfYfuQYvdtVr39NT0Emv2WQaeZxceEdMjQJewfhYRjqlqOGYaYOj1O39DRH1KuTKDEILCylXmrhsM7zuIau4nDFwAlpdLdf9/BXiBanav6+j6AyJ6qLqIk6cXf8p1OZgvcGpxOZzM5gJsO6g9DCMg8Kubba1QLqSZnazmAoSkMnnlEktZ/03gq8A32CXjQy8IvGVYvRYhUXURBsg1F6FoMeKpAzz+4V9g7vo1vvKlz1x88eWVD/zO5/Lp3W5XjwBvARpnEaYBugZmdJj40F288tI/ngdOnDy9mLsVbekR4C1E/XJ1bQM4V5th9NBDDz300EMPPfTQQw899NBDDz300EMPPfTQQw899NBDDz300EMPPfTQQw899NBDDz1sCf8f8xls1NFsOQgAAAAASUVORK5CYII=0.4380.4384.85266.36090.2190.219-0.0#000000#0000001.00.010.0125#0000001040.0031251100.00.2190.4380.2190.2190.00.2190.4380.4380.4380.2190.2190.2190.2190.0111111iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAPgUlEQVR4XuWaW4xmx1W2n1X7+74+zYzHM/bYZowJzsjBcWY8B8+h52A7vooEImgGIRIHwQVIKBKWcscFuUAgcRtAwAURigQkQtANEpGChGR8nrEdx+P2iYzj+Ld/Oz7EnrHn1P31t3e99Hxd2ktV+obu6bFAFkvaXV2nXd96ax2rtknif5Ie+MuneOcnH4XT712obt6yodepwvg3//DeD3Di+VfeDD973WRsIh8bbd68iVF0WQAe/OFZ7xBgfCwkAYimEZ1OOAGclPh3M2aREIYQJhBOqQ8QAhBeej+GaHETSHDg5ksAXMso6nB54uYtExkG+ILeXrT5j/E+KPshRlE3zYHFQTywsBB/V+KYYBbAZAjhvAuVi5e7Y2ACjej33V81AE4XB/JlBNjlBQKBLOvzseUkg1BVdGXUVUPd2AxwDDSrTFpAmO89ZGAIA3mPDHD0EU5mloEKEFaQ1Xz3rewuNiRnvpyC5KWZEQyqYPR6FZEGYAZxtB1rPlEkhkVbCgMKaZPa8REDcXkaDYCjrpIJuSSY+f9SoRJZPT3mg828w4JRdSrGexWM9wBmBEcz8A3KH0MLhPdF1K4lOeJrAgCp0Pm8lEa0uRp4vZggM+R6TUyAVCFQxYYwOQYwAxxtxVggEoNSKx1CkAFULrxWAEpmCh2WlQp+uUXlY7AWGUWQJcOFQZIoRdEJUCUQJI5K1i5hyvbHgRRIahtVjFs7AFagkQoTOPJyUJR22OUeB893TWleFESJGEXTRIiRYEY3QGdyWR0MHVUC8h8e+jAkN1nohy+Q1ihN5hq8QIGmcFEwjJdefpU6RqpQYQ6AE0M3x53bb6OuI6dOvcYo2rbtUwwGNa+/8RYbN25gYmqcBqPbMVg/xuDcwgyyYxKzv3bPxvivz30UfmnHhjhqlyXfGYmrA0Cjpprv6u23f5pRVOqGBFUV0nhlniAqEhsRI9x0443UdUO3AmugFnSDYevH6Z9dmBE6Bjb7i0vM/+Of/ks49sAXI4B7FRUiL7gaL4DKavlyUPF4zUa/SpbNTiAgoImiHjRDprsVdEPaoWCMbxhPNkFHEfzqA78S/+kbzwVfDiSPMoUQoLV7gdK1CROYCoZV+npDMu8vvIWsHYXS+5BldiAYCYRlW2BAFYypjROZd7iw4YxJ0D6ojFPcWqxJBVpUBRiyvHPu+VOUtGPHbQDMzZ1iLTQ53qOyzTRA1wAMQ9QRMGPdtROcPzM/VIff/K17Z7/9nR/2vvSlzywiMpWTJeYLwzQ5+cssLj5PXb+2cjL0by+c0c/dtI4LA2EGEu7NcGmwwhtg5lvu81Ab0EJEGBCbyODS069ZmK/p9xf57LbrqBtRCwDqCHXUsGzShpw9fRGJY8DsX/zdi92v3n/HwCNGXBUMTPDFPdcBcOut8MEHX+Ps2W+sGApnVjRz5cJjfgNH3V2efKwDhuUWIgosYGbDhwCNIi7+lmxAUokAlYGZcc3mqVYdvnr/Zwd/9r2zQZ4Fgo1O3GIEaXClNsBAZMgCHo8XE+R9hTHOG2QBEjCk2CHIChtgyQawXK+gk5jbeN1UGzb/3hfWR2dYnjqXtpwG0JVlg2WwZ6k8efKlodWuOhVXQgZEibqu2XvXdiTxyo9e4+KFBSbHx6hsS2EDoI6CBIwhLMIgLoNw+qfnZzAdQzb7R9/+2/D1L38lCjKXu+ZASBJZZiahJPe7dn62bTdSX1uBvLPcCw+qLMBtt/088/M1cXGwzGRUigPAMMwSCCQQDEjGcdP165ZAODcjcezr939l9vf//pvhT778O1ESwNrjgAiAkUuui1UUnqJCYl6gYmwLpgGW+esoMAKSEXAx74ZRNsCSDRgC07rJwCUQ1iebwK9fYv6VN09WaXVkWhMATuamUFKWfhrmDBqZ3kl5fpCGlW7WsaqMyfEuwdpAaIQNsGQDcBAqCAabtqxD4jtIR7dtvbNxm2VrVIGYmJJBKv2UwHj66TlWS7t330FVVSvO2bntRirbQMOQsbSeFTaAZANEJ6kICBtKwhRLh60ziGMyzborWgMAEXyHJAzLDknu2rujzISx4kzMbYaKObkaNHXDYr9mvq55aO5tzpw5x6AWdROJTcTMqKpACIFOFRgb6zAxOcbkxBhTUz3GlkqrwAhs3DzJmfcvDhMo0OzaJcAJ4UyYDMwDDloGLU+hi5RVJlcHASaIaa6BBWPQiMXGCN0xLNYERWQRNQwBgUifyIX5Gj6cB4MmRszg+s3r2XLDRiamJqg3jnH2w/4M2DHE7NIJN/d9ZsOVASAJZdtriWGBDCE8RDSszMJw5pADZKIkLAU4VgWqXoduFCJQ1c2QwVgJtVKo7CyiipEmNrz77ke89dZpxid63HzL9UytG+PC+XpGxp4Xn3n9ufs+s72p5wNmqwagCGdTRQmVRx95Cic4ML2Lbre31P4k/x0duXs/TdPwxOPf5+OiTRu3UIUO1WDAwmLNK6feYnyiy9at11PX1R//wp4bfuO7z7w9P37DTRd/+nJY3cXIPz/zvm695RrmB8K9oEAOofC55tuycpkotgISaZrIoI4sLgzo92sW+gNirUtgDR8BJvPQPABqjWDKKGuaWkOpuTi/wPqpcTZvvu7Vqh7bS/9M/8//euPFp2a+xrnBt5C0khcAZUwakos8AGqDktQvMAdDngUgLGM/1RBCMoxAIBKqik5HjAliNxKbaghOjEKSv9XMQTfBEICqjb8mpybodrvI7NP9D9/Wj+P6ePvWE+Gdbe/GF1+2FVXAgxxnErcFwnBjSMGaipMaYfg7DZm/iyjvMyMEI1SBECOmCrOYwm2BGPanGcsgeKZG0zTJnoAwqqpDf6HmhQ9+Up/vTqpz7QY6X9gTqhu2xVXZAIEzL0Oe9nLi+DMMBgM63Q4oN2h1EzHg0JG9zF9c4OmnTrJWuuNzd9BE8Z8vvXz5+GHnDiLQS+xYSMmV2dL6Nc1HoXl7bFGbOpP2M5+6U2femVhZAjyrk0eEcuu+f3o3BRW6DgLGJyY4cs90EVmag6qkBhFijNR1gyKEyojNcr1rgZ27dgIiBMMl0VeqLIAp5Q5GVByWQpw5N9Cm86ZaFTf1r2Fq8+ZVJkMIr4ILn/nfQp/xPiRKYBxZwFAWFxi0ZwMCsECvZwBEmc8Lltb02MIM1EQwaBQxkvWJEJCZumZqsIU+22+bWkUyJCCL/sozgfwAxKFSOy/LGDIwAHes3j5kxH11UnoIhg2fQKgMc1yX+0yJmwAEglWEEJIqaNghA7OGoIo3+u+tLhKMgHyn8bjGOPG4xwH7Du6hssDxx5/m46Cde3YSDJ79wXOshnbdtRMzIRkhgJKnwkCkOnXoqFIc6zUW65UBUPStlYsxshT4HN6bxf2SOHBon6fE5vf8Zpn7c3fp19vFNdey29u1dxeVAQYxetit6O9UzI/rJTCM2KiVAMOqdrQqG+/3AxBXdINq+bb8bFAgT4YdJOT8y4hplKfG8uNRgUyQwJCcqZAMmjzmoKrcjVIFVzVz20MUUYBElKgiy8kU1sNYrImRhUaDix82qwuFETEmps0w1LYDI2J7w329A5k4bBOp1O4SgAhmRBnBRJTa7wdEMnBpvgVzGwUpfFJCDoIAgRq1TqshNrEhRkU6FVh3lbmAhItwcT54/NGnkMRqyfX1TiYmxnn4wcfo9XoJMIYxxZH7DjFYHPD9J5/lSmjfob0eFUrILOFhRLEMUSRYqBpVC3SDbCUAsqMvDzn9bn768D5ktCKO5elv6fOtkJK77zucgLU0TAjo9npMH9mPmRFjDruZyjglBZMGFrEIBE/Tlaa35iyKvoxw7c1XEAhlip/7XqDVc5Snv7nrSEC2wJTXZiIntwkQnNPyggJDkjvUNNRIeUNl6cygamR1E+u++gOjd+MtWsW9gFBbmh9qeL/re5Yn5NfTKlIfEFImDWme5V+j+OOeorylNWFJCpXWMFIsYYEQAmBYx5rAYtPp9WNTfxSn3j+3sgTQio1BJUT+A088+iRXQzt2b2dyapLjj5wYJjGhCiseYS+NG2Z4u/fvptfr8uiDjw2ZPHTvwTY+wdSqrAUwoH9hfmC9ismuNeu3Vrz62vxqjKBaEXfj4q7vwJEDgPDuQkoh9xRWpMsCDd8zjZEmI9c0PN+X2xI3LYIjnz/s80jrx7SACQhI0B+sbyZ77xDP9nj7R6f1wlwT4a6VVCD/mClB7GKp9BhIhqu8qwGpzG5X23l+aoCBxx1FqGuJOQnyq3b/TQLJ8DmpTgALVHFSZ6uu/v//u0YvfPe9+N7J11dzL5BeHMl1U77NwkC50XRvJP8WCOX95UcWcosthORcRgA5ww5ummeeosU0SDL8BCtSnT7Fc2yPz765N54/dz3j61itF5CjLdqdlMGJh59Y1t0QKCnG5UOM6SMHmb94kbkfzA39vI/1cYc/fwQQj//HYwB+W7xKEuLgPYeKr0lzaf3W39wfAfgDgN++slNhSS7K8ujtwN3TAKM0Htw7MD45wb7D+8moiAgBDi3ps78iH2uWxgnX+SzXVNqYMpLlqj6R8U9N5boWszNAc1Vx7CnzZGEI11NZPtYrVjLR5gRidMwgv4xBI14prfl2OF9OHgy6qpuKI3PlSRPlR5VCuEGUCiESTuabLS+hvKjxCBWzgnEZYiRdWSQonGJi7vgjj6GoK9LX0sUevPcwGDzh+t/2fW7XDmLT8NLci23f5W3AYUhznUQ0rf1uUMW9nsgj0um7DwPKJSOVyrJoc1eXZY9en14CwhhtAqYv2QYlACQYfcqW1sg9lgkkWyMAvlPIV2yZEEqMe1xurevDAXMXmDFZkkb/j6NaqJaywQ562Y+u4kPJURLkCLvfxsuck7Ka/DpXaK1l5bcHsAKIIZiPXbME+KKU5uTEw49nOrfn4D663d5S+2OMJhf14w/5mKsjf6+UJ4pOa7AB5cT+Qs34WMdbDPbfcwjAdR+Q/xiw4grAs2QOLI1BgLna4IkM7vJx1Rt91YhB3gc0jYjRA6E1q4BwGtQRLF/IVFyDleLvkpTmlp5OXje51/GxWS5ijv9odTSo6+iSuQoEwmqMIDiy3mdElBuizLe7WDiaXsVAbeltpf2R1x3s8mDFlp/YyF2p83EVKmBeixIhQgMoqgQHjQ6MR8c47teL9svXbYW1sNFGVWuPA1xeYxREoWCYDEUN2z4JZFyNFxBUnUBTxwyITwJVnYC4ikAIicmeLT0dPnm0RjdoZjcCPWDrX33vdebm3uETTc7XNPAWsCgpY8okjZpwC3ArHz8FIP4vzP2xpDdwygH4v0z/BSjKoeHVWVh6AAAAAElFTkSuQmCC3.62940.34634.77844.06911.81470.1732-0.03.62940.34631.81470.17321.81470.1732-0.0#000000#0000001.00.010.0125#0000001040.00312510#4d4d4dArial0.13888888888888890#4d4d4dArial0.13888888888888890#4d4d4dArial0.13888888888888890#4d4d4dArial0.13888888888888890#4d4d4dArial0.13888888888888890#4d4d4dArial0.13888888888888890#4d4d4dArial0.13888888888888890#4d4d4dArial0.13888888888888890#4d4d4dArial0.138888888888888900.00.00.0-1.00.00.0000.00.173153.62940.173151.81470.01.81470.34633.62940.34631.81470.173151.81470.173150.0#000000#0000001.00.010.00625#ffffff1000.0031251111110001.0E-40.31530.03140.34650.01.00.01.0NURBS(1.0,3,0,0,0.0,0.9593,0.0,1.0,0.0038,1.0006,0.0,1.0)3.59820.34653.62950.31530.01.00.01.0NURBS(1.0,3,0,0,0.9962,1.0006,0.0,1.0,1.0,0.9593,0.0,1.0)3.62950.03153.59823.0E-40.01.00.01.0NURBS(1.0,3,0,0,1.0,0.0404,0.0,1.0,0.9962,9.0E-4,0.0,1.0)0.03143.0E-41.0E-40.03150.01.00.01.0NURBS(1.0,3,0,0,0.0038,9.0E-4,0.0,1.0,0.0,0.0404,0.0,1.0)1.0E-40.31530an application entry point. The file name is fixed. 0.96610.96611.10819.51690.48310.4831-0.0#000000#0000001.00.010.0125#0000001040.0031251100.00.483050.96610.483050.483050.00.483050.96610.96610.96610.483050.483050.483050.483050.0111111iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAYAAADDPmHLAAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAAgY0hSTQAAeiYAAICEAAD6AAAAgOgAAHUwAADqYAAAOpgAABdwnLpRPAAAAAZiS0dEAP8A/wD/oL2nkwAAAAlwSFlzAAALEwAACxMBAJqcGAAAQbZJREFUeNrtvWm0JVd1JvjtExH33jdlvpyUqZSERjRZAwUWCIMtgTGuFghbLlAhBgnKuLDLpso2NDaYqja97HK3Vxl3F7VsY+MuELSFxSAshjJgMRuQEBIIAZLQkMpUZr4c33yniDi7f5xpnxPxXqZm1uq6uV7ee+OeOHHi7G/v/e19hiBmxv98/f/3pZ7uBvzP19P7yp/uBhz3a+F3ZjFZ7YDKTqiJzyTmExSyrZpUFxpboYhArrD7oMRn9/14Xtq+s/3M5qP7TQMAM6APK1IjrevDTHQwI3oAuj6Ifj6H2T9feLq77Hhe9JPpAt6tRssHnpnnuIgU/ZxS6mIA21jRdlJqEyiDEawTKK3znn6m5FoKXsiAfU8/p33kgJGU1zW4rucJOADoQ1rje6z5q5nCXehu/zHwv2n8hL1+ggBw9QSGmy7VpF5GjJ+ForOoU2y2zTRvThlZHIMCyH6mRMOj720AYPFbKmgtvvqLmncS3915zCAwoCgxPAyMq6N1zfeT0l9TJT6Dhflv4eQbB093jwM/AQAYr/7aRXmRvYJq/lek6GJ0CwIYqAGuGZGmK/dfKiy2IHBAsP/RehYB8fnROyEAwAkYiFwBs63KtpFJlBfvrEEZWWAwMKiYwd9jwserenRzZ+qDdz2d/f+0AOCjH706u+qK6ZfkSv07VtmLqaumoQGUDGaycnLaWwNcA1Sbz9AIJttqnBScFDrZ3+hYPCABE7MRKIAGB4iAQOIcAljBuCf7574z2fsAiBgoyPw8rFZI8xcr5r+46bMr//SqV91YP9WyeGoB8J03F/XZ43+pFL0Fmfp56mQKJYO17UxyHV7Zv9K+c/idpN8nY46lqZeWAAognRwD2q0AEJt6cawVAAlXYG5aDCYLiALgHKAcBhwAuDbN6ijwuNbQfIvW1Xuz+374j3jO7eVTJZKnDADVyrUvzpR6B0i9hDo5MNZW2525LQGMzDvV8BpMwncTmsIkSjScggvwb+L89V6pmfdmXPSR5ALp79ExHbsLwFqCAqAuQIWxENDGKnQz8KgCWP9Tzfwn+fT1X3wq5PLkA2D+umegw/8JWfY6FKqLkQazsryrAngEYAygTvy4Ei5bAgChjBeqEsJNAICkjugnKVj3g9Rk91tqFVLhOq23YHbHo/N8+Gj/MgAdC4bMuAcFoABQ8wil/jAq+t+x6YO7n0zxPLkAWHrt6znL/pi62SkYW1NPZAU/AHgMf+epdkvt9/IWv62n4Q2Tj6Ssa4f9yv5DLPy0b7jtuBacVJp/aQ1sOZ2eY8uqDqAmAOQmmlCwrqHarcvqXdnsDR96skT05ABg4apZTb3/rDqdN4OguGQY8lUCegBwCSYGRQKWJh/xO6Ua32bmEyvQZvIbmCAhINEPxEKo4sVCwABiS2C/u3LOGvjfdfM8byUcAAsDBCoMEAoCWGtdVu9TPHwnZm9aeKJF9cQD4PCrn81d9V7q5j+DsWX1rI3G6yGCxrcIM7IC4nubCY+0nLC2C1jD98vgofHiNYQv3iVh9MfTcDGxBg4kyW9G/AxoDQaBqGeBoAw/MNbgGzTSb8HWj9zxRIrrCQVAPf/qV6tMvZe62VYeuQ4bAXXf3Hhk1i2Dd2EZJZbAHYt8v9DyBhlMzg0HY3PvhLduJJB89wKG+C1JDklz3wBD4gqk+Zf1yz/KgGzCcAQA1CXwqD6sy+ot2daPfuSJktkTNhikj/7rt2UFfZAy2soDDXANrlaActnE8WknN5Iva/2WxujuY5KNS49HPpmTz22XlufJwxxnfdN2Ha/+NCIMaYLilDODwFwB1TK4WgZQgwc1KKOtWSf/oD78qrc9UXJ7AizA1Zk+Qn9IXfUu0gzWCuAKXK8AXIEkmyeh7cc0+baTIl4ABIsBNDN9sr40AZSUi46lZE9+aAkLU8D5sYC0TItLkMe5paxmW0PgE4QMyKcA5CBik1+q9B9hM/0h8PiSR4/PAnz06kwf1X+iuvQuqtkouh6AqyUQl0mHU7u2yI6JOpeS766IDp+jfHwitNbrtTK7uL7UWrTVkSaFGveRaHZajqnZdgYYLGyA7zWwLsHjJaAegDVAmoGOepc+rP8PfPTq7PGI8HEAgEi/WL9bdbL/FZUGaw2uV8FjZ/KF8FvDojXi7RQQqR9NBbmmL9WBeTdMt0gne5+r4/rXvJ5oqwQjkoE+r+VJnek9+7osJRI9R67fyBBpLlfB1SpYM1AyVJfepi+v3t1i6o779ZgBoA//yu9Srv7ApHIZqPqgqh/119pEew3Nku/c0un+cxsjdwXSq1phe4GkAzfOfIsRwYY2J/yhrT28ngzaeEyLJWIbDXg7IF2LbV/VB6o+WGugYlCh/kAf/KW3PlY5PiYOUB/4pVerIr+eSBWsGVz3gXpk4noyt0DenyfhmGrJ20ehHsL3tlCvjQ/47wl/aL1jmQA6lrDcoZQ4JhaoUSaNANYjryyKcQAAAxxlExnEbL9qQHVB+SRIEZi51FV9bbb9Hx51dPCoAVDOXfncvFN8loi2cA0j/GpoRuUEySMpKHvcpP7TDF9K8lJBr5P7bx0DANYnf+uFgBIAMjMogdAWDooDUuASLLqlDs3R7yyAZcy/9sCIrI9zP1kPlE2CcoA1H6kG1RXFyZ+67dHI89G5gLkrtmVK/Q3ltIUrDa5WgXIYtMreHFlIs+xI1+3MBtnS/0Yja4gUbF0ZHSsU9GXbyCLWOQbEPtxyilbht1mDRPhsJZqOIFJcn0xTOEJo4J9mLK3LKYfgsg8uNShXW7KC/gZzV2x7kgDwbqWR/6mayC/ikQZXA6Ac+BsncScsGsrOnAkBkWT6guw3yVwqZIRObJA0AQL/J7WRY8FJTY8AmQjY8YfoUi1tSClDC+FlwfMDNuJ2xQTQNakNwLZA3QdXQ/CohppQF2mt/hR493HL9bgL1vu/81oq6DoMa6AeB+Gn/ZK8KD3WxowbISAHwUS+tAUEkRVJLtAQeAtAUtPaWieStqClPRrJzbVYBKfNVjmi4tpXT96SinuPbk8oEACUfaAaAyOG6qjr6r23vfZ45Xp8HGD/LzyD1cTXqVCn8LgEj1fhU7sgOysHIpEj07skeCDB5LodaVbRb3GKWHxH+NrAbBsfiBJE6/l86e/lMYrf1xot9EJOfb4UfkICAYTwNNTHTuAik2lmnQkeAHGOq1c7F0NAZwqqU4DHes8Iwxf2TvzCMYeSj8sCaO78IfXUKVzW4HEf4BohlBL94UKhRmO9cwjT6Fi6AllHYoYbZlbO4JUdLLW7xbKsSSyo5bdm+9uTRG1uow0EMifSvGcP0cQYNaxEYyKqFFINjPvgcQ3qqVM6uvjD45HtMS1Atfd/+fmsyD9LoI4eD4BqgChF65i7CszeaLjBFgnWzra8JzUuQojIfFuaGGiNFFrH/NMBISFoyTfS09sEGHU6kmNthA9CMLGrYHseRQBwrkPKVSOEgK4eCUhxXIu2WHJN+QRUtweGHtcVX5Gf9D9uWU++61uA+6/oZip7FxXU4bI0jN9rOVreBQm0xNB8ZN8BrZMsWEpEkjNxgVZC10KOUs1sHFtDi7HW8ba2JlwiOk9WFdpHsMK3302fBHCwEH6ErNZJKCnoYC0qbGRQggrVyYD/CFzRfcwAqCfqKynny3hUg8v+GmY1EaIIBTm5aa/PDeFx+F+i3guxJf/fJuiGoFIGnwCojRA2ND4BSXpuIwUszklkGX437q+R8vXNDNeIbFljlDPpe5iUsS4H4JEG5fRz9e76yscGgF0v6inQ70IR8XgI1GKiats4uDyetDHVHPbERipf6JCG5nNyrTQKQFx/3AZXTqMNeK3coOHfk7p9PTpuowRMS10ctVkUt4rCHq/CUqT9Kq1PFEKLeqsSPB4BSpFS9LvY9aLeowZAnRUvpZwu5VEFVKM1blS2NdaM0PC4M8L9mQ4kW4xS/9YI0dx5cgCmrQNSk464IK/3p5NzRRu0uE6kgaJeLYXLYG7LIQAuH8BSkTxFkfWKUcMGIU3bk9xpOQKPSlCOS+useOmjBMDVmSJ6MzIiLodA7YacOXrzDVvLLUTviNBM/qPU/KTTYYXSuLZYrNGmbWtGBSmaWgQtz4suzYhi/ai6VAgcm28PjrTzjLl3SUKOMoMtfRl1fqIg4lrEAOoaXI6AjEhp/DrQPmzcCoDy4fmLoOhyjGqgHEc3zI2LUuPeG+nZyISaz+w6qCEwWf9aYWWSPmwxq0HwMoUrhoflXyOHI4HUluCRoGqbFcyRH3cnUUOoOuJITX+egnuN9YprfS9HwKgCMlyGhw9cdNwAyIivoY6a1OMRuK4j5MrRqmZGTt5I2qGCAGpLEhOlCP3qQJJagvR60twjblOrwBwI2s5vuY+UuXmwSAsUC4BZgNsLLlwjNJdB2vam09rokpL4xs1oRDjJPZrrM7iuoUdjUIcmK52/pk3WzTzA/Vds4KK8lTrqXL0sJneoJO5PZ/XKuD2N7+1vZIeKAftZnK9AQE5hdS1k3UkugCGyjeIakjP7aQBN/0iFMgs1S9gdEsiuS5S9HH+Eu6Rb+dso26K9NQO1blpNlmBIrBsAYm0Jo7QCOuYha2YGBRg1AypDNjMNrvQ9VK4+D2d9a0neU3ODiM7guciKs3k0BuvKCEu5xjlBuAaooNYsfzPhSDT2b27Z9iJHfaUKBV0yDuwbYd9cif4wlncqbIp+lBIi/7Z9e45tW7vYMJub5lUWTwXhnh+s4LP/NI8HHxxi27YCL3jeBrzkRbMRLzH3Y+GaE1Br7H1kiL37SozHHLXF+3EYzdNM6BbAtq0K27Z0MT2tzIqfksG1yfmTFFqk5Y4orzFI1UI3Il5CiLY7oLoGj0qgU5xd6d7zcuAL6wKg0tmVeRdKr45MRQQwk5mMCG2zeElq0ws+EQzbFpEoJ3wB2XGBL35pCX/5gSO498ER+n2NMp1OGLByXLOfCMDUlMLUZIaXXD6F33zTDuw8uQco4PZvL+O6X38Qcwcq5Dmh1oy//u9H8I637sBvvflEsxbV9yyDMsJdd63gz947hzu/38dqXwdOvNaLgSwDJiYIU5MZLjqvixf97DRe/MJp7DixAJUMraXplv3VsPeJtBN+5UCT8hAHR67B4xGyyUKpUr0cCQBiF3D/FV1djL6pcvoXemnJmC5SYuJuSwqYhAuIZvS0ze6BN/1EBCoI//iFJbz57XMYjBhFTi1rPWOBH+/kN80Aa8ZwxLjsBZP48N+ciW5H4Zo3PoAvf2MFE71gnaoamJki3HzDmTj7nEmYlUwAZYR9+0d45bUP4P4Hx+h0jOs6nhl4jgswmyBKM+PUkwr8m2tmce3VmzA1paDLFlKXgoGtWSfhAoTVYEsMo7kWkcUwWpzNzoArupNO6z4f+OzIXTW20dnKOYrYmn9tVl4LdLHzqSz8ZUT4pK9LkGqBxp6oMLhi/N1NSxiOgYmeQp4T8lwhy8SfouhPHedfnhGKQmF6KsO37xji9u+sYv/cCHf9cIiJXmYFaf6KnDC/qPG1b6z61dsAAwXw95+Yx/0PlZiczGzbju/6WaaQ5wpFodDrKUxOZNh/sMa733MYb/wPe/HQQ0OoHLJjIaQK789dX8pgxuMkhNAsIw+vKO4EDR6WAPjs8UMr50iRRwCoa3UxCjWlx2MBskBYKDIvbaxfhGfSgrXm5gFdMXY9UqNTKChl/qRgHsufq8f/ZQpECoeOMHbvLjEat5RRCooU9uwfA5URBgHAuMaPHxwiy7JG+cfStqJQ6HUzfO22Id7w23N4aNcYKpcuE2haADT7DnGfxhPjDH8JyTXrncdjUAdTWa0uXhMAIHoBdG1sYirAxFLF8XoaViWNDnbR3yQByHLCeeduRFXj0Ql1nb/Wju/kOOXkaQxGyrq0lmtkCit9ZVap2/sZD4Gj82Rd0xPTJqUIEz2F+3dVeOefHEZ/tbIbmUiFSgeFEPUtcaBcnCiY/B6JsKoBrQHiF6wBAFIgfQFqbcaWG5mmOLlBDL+KRVqKOJmjW5CsQ10EvPHaM7F16wSGwxr1MYDw6K0BgTVw0s4pXHDBRtS1XrssAURFBNaqYiwtE7LsibFK8livp/C1b49ww00rZhORNMZ3/eUziMGoM1yYKJJpkdtNBs8YRvhVDQAXQmyp4qOA1R9efsLEBJ8Ea/4bCymZTSzvLbxZ3k1s5rJ4FDsy6D67xkdj8aZOPapw6bNqfOSGF+OD19+PH/7wCHbtWjWrXxLL2PbKFGE8rg2w1yRmGq977VmY3DQGa0KWZWYkLinvhAxLHolMlJLnGZTKkGXtF0hJKzNQljWqilEUWRoJR6+iAD7yqT5e9bJJzEwpE7dHYairk0XqIQkJRb7ahZbsyF8UQDAwrkB5Z+fqDy8/Yep8zEUAoHJ0IneKHRjXEXJIKRESO2G7kE/6oCTca2zAAAEO23Ai6OVD+KmdNf70j8/Bar+D5dUMYeuYZNq3CEGVyvHd7+7DW9/6VZQlQ6mkw4gwGtV49nN24DWv3AwMHoZLQMncUhCkALFwY2Ylu5jmbsuyvYd3vOM5eO5zdxrrAmA0LvHwwyv4+j/P4ctf2oPFxTHynCIv6l6dgvDQIxrfumOMl76oZzZLSVPDoguda3IkOhRhMYnUnS+msDkwjCuwKnYQ9IlAAoCs09madbin+878k0/4EGBm87Cdo+YSQMzmuBOLOE1AL7EEzqIEweqVo8DqPKaKDqaKPIApnfnjZJARluYZ73/ffozGQJGnVIagNWPzlgn8p3eej+liv5/CuL4FaG5BR+R8edMCkMpx5olLOO/0Ghg7kGd49k9N4qorz8Ud3zsNb33bbdi3r+8tSJqZHI2BHz1Y46WXyUs3SV8s/FAuxmzgCDEfMFXVlUZWoNep+AR3fQ8A1Z06GbwMrrXoaQJDw1EFv+sa/BRGn9M3KVISlsHkEPyNRNlDwS2EVdDl2Mxu9RdzIKDoY83AH/3ZAu64a4BeL0OregF4y29dgIvPWwEv90HdMEWt3QIo709JcJ/gw6khPKIM9WgFWO1Dj4WrtL3z7PN34N//1jPxjj/4vrUg7C2JqyvPCfc+YJZ5NcJoAQJXnrzgXZ4hsRjCOjSiB5McAdPUSQ0ADIfZicWENmTBze/ziGIxB5TEbBZr2sjGqcTiXHjeEEBAoVEuQeRuVroHEu+yHphU7t/f2MfNnx9jciJv9f3DkcZVv3w6Xv+qKfDKgwArfwmlVKsFUIo9B5B56sDm4xPYukfDp5ruiplBC/vxwou34IQTJnDkyKjVihQM7N2vUVccXKsHASJAhSjAhXchPG+OPopKnPXVRr5lSTsc3fUAKEtsR1csooyWY7mLByZKlveDKMziZpily26gh6w2eUtgC65hAQRpiADobpq6CnfcMcZ7PzBAp9NOsKqKcc45m/C2f38K1OAhaB0sFTmBtswbzTI2LsCGTybKAZSwAG0AaMxikmycCdPZEk7a2cGRI2Vre1WWAJ3T+swBNzgorRNDlHGAcOeulTvQjHFN2ydTADDUBmgdX9w4f7CElTdhohXKlaEgLFHW34Q7FMW4CS9wg0lSG0CgHDh6uMIf/ddVrKwaAhX4prmO1oypqRzv+v3zsG1mH3R/HCIee2trWQBH9kIa3d7RMQAQBCf6SPRfWdYY9GufQQzns22PgSazTtLccWbPCToasXQmPz4tygtEDWYGtEbNaoM7HKIATd3GrBVNYWte/zIcgSQ4mI2ZJY4tudsWjgKbhdsKNuowN17gwh95QTMaWVXAf3nfCPc9BHQ7gZNIdl7XGr/2a2fjec9aBi8uJZbHlD+2C9BB3SxgjmkBtBBIVCmwb67E/rkaRdGckMPM0AxMTxXwK4bYxPkhYRaGjZ39jQd+ELmDJhgTMJh6/EzhMBqYUR5WmYiTNYxf9+QjJIQjBffCg6/DyNUeV0mjZdjoz43GWH0Zygkfv2mMz35ZY3Ki2ZFEwHCocfnlO3Hd1RPA4oMmUkk7CcfLAZwR4OMDQOJ7CQBlDF0DN9xcY1SabGLbq64ZZ501gSyroGvr7wXTd+1gl29x1+I42ZMy/sgNyHcDVi93MRxcC7RYsqYoBoEHuWABLjy0RNEs/0r9FEDOmkCATBI+AH782Yd/DJUD37mzxl/eoO2YQbMTq4pxxpkb8Pu/sxPF8CHjyXxPSVCqdcJAhvIWILgkkwQyiSAZBTgAKAVAse0etu0B9u3X+H8/qfGPX2P0upk4L41YGOee2QWyEihjjYaVs+ly02Eux88u4ogmhIj+lX5fvjRHYA0A0EqgSrD3KE/dNM/mjYNt0NbyWj9O0p+7OlwOwdciyoiyKiccPMD407/RWB3QGn4f6E0Q3vbbp+PE2X3QK2XSTtmpa1uATFkSCJm+dqN77XkAgPCRT1X4yrcY2q7vJ2bs3Q/ctwtYXGEUufLBT/rSmrFxNsdFZwOodEieOubveZbLuAoX7S1BfI8xAZRRQFK2AQC/i7UQvidpAkk+McNxGWEVSItyZLeIVZYnOKh4JWchr5AvJgWMx8B/vZ7x4B5Ct+PmE4juJ0JVa7zhdc/AC7zfd/eR9LZt5vouAPHDQ7B2FOBet32XLXe2GssEpcyEENfmtN3uVVWMCy+YxLmnM/RAC04V+sa4A7eKSJC+aPAoAUR63Jd3LiBYJAGAPBT2AltD4xnBnCMkOLzlFHwhRZ9P77hT29SDCJQBH/sUcMs3Cb1uTPbcazxmPP/SrXjD1R3w4iPWLMpElmg2u8u5gZ+4jFLmphh1uGe2qWOlWgUIAJ1CXiTB3BoJKiePTi/Da35lGrmeMyskmCNdawhTnhyZeSkmNgKMFu+mBDGIXYSBnXAhy/BjRhgLPEQIgg/Yjgj5HhcxiASLjAOjJJG7UUDlwO3fU/jAP2QoOmH1ucAHqgo46eQO3vqWbegOHzRTrKR1iqxX6Kwsy0HQLRbAhGQxi2bBAdYX6LFe6bnDEeOaV2/FCy4YQs+XPtXulcQ3Q3srgNTvR2AQJ7WAg0Q51p0mADQmYhR5mi/YvfvNS7glm+fpghs7sOSQQ0LGCTwe9bODPDlw8BDw3g/nGI4IRdHiPxnodBR+89d34pSNe6FXxiHkCyMnoYN884xPb0sFK2W5iASAllHA8Qn2WC+tGeOS8fIrt+LXX1uA5x8J7B6pYNl/jAW8js9vA0cUOgIaLQCoMQ1wZnwEkXEBcIJbw6H66CAhXN7mku9IKOv7rWly+RkZMSgFjMeEv/z7Dh7al6PT5agagyvCeMz416/chhc/ZwW8sIQoWZGOhgVJmTAwU2ZCRQqADCAX7orT6BgAOF5QMBuXNTOT4XWv3IrX/7JC9+iD0JVojJftWoLEmsJnR8B9RU522qzD8FYkQ80TLQDgaZhHmoQQyPSAcAMpHYiyTSEiiPrd/kSO4IhMEXkzzbazgZs+38VXbi/Q67oTg78mMp148bNmcN0rFXhxzkcbcEknCQQ5oGQvrUiBlCF38pVp+FSwDKUCCWwXdl0jzPCNOscc1xrodgmzsxme89Mz+KVXzOD8U1bAc7tQlxUIyiqdmCrfiOeFUL2VQqThURoY8IrLOgwVm/7OUWOqBQDYCHAHka2IhC3Ts4JVRR1PotVxZ4RUMYsSNnRkQHUYt97VwfWf6SLPqbXDtQa2nVDgP/zGDKbKXdC1DjyiVfFFO6yQlE3eNEcDRVSiYxC52UXy5TT1vPMnsHlzDjcL2L2UAqanc8xuynHuOQXOO7vA1qkBsLgb9Z5Fcfem8Z6ktQk9mZnVSO4giMN9Zjf7ynFCf37XyLoJgCkA02A953PcXqRypo/0/0pcsYUFt/1m5KM9f2CYkOnAgQx/8dEpjEuFIg+dKdk6A/g3123GmVvmoBdHLuEQCUXkYxDnpC0HWMsFNMYCYN2SQpY11rhAaxP5vP7qaTz3kq6YzQO4gQdyD73qLwLLi9BHhp40+zBPyDDWdCdkYZFlckdaqkTwfusdhhg+ttrKk6jaSOBYTwD5LMzmhBb5Ih0bLINYpeMzbu7q5BL04Vg6rhMhwWTQRiPCX3xiBvsO5+gWJqkkNY4IGI0YL3/5Rrz0kkXw/ALQtqyRWtbluY51mFXrAcCuWhJJGKUUMtUEABl/AizsB/avgqvAgN25WrN9CISDnxNFbO6piR00nkvUyvQFcNZi/bIvWINpBnXdRgI5R0UbQVBhFMpPfXIIsg1zmifYvRNAm6TlaKKYyW6wooCPf2ka3/p+D91Ch4kZvqMJdc141rMm8BuvrUH9OfPgLf8AigRU7oM1fdBkLTpDwXIAanEBfpKTHQyy96YIDQ7gJ2e4ftDaPxIh0hUATMrSqGCyZTrdfVqT9HlQhMgksqoREFisDQxuKixDU6gxjZpbE0HAkDdhgroAjwFNZr26CogOMJDCT82/dMbiGNvIgkPSSGXAPbu6uOkrMygKIxwpePfKMsL27Rk+/akF1MMphLECRClr6f6ne4wtmzSesaPE9s0huUOKQNxc3ZMRzIQMuRqcjZCJuBGJ+MEgkT2MI1A3eBNG9qQaeIF5oQLRAE9ARnKydAOiDiF8N17gw3AbcxN1MKyno01tIgAMeAN6atLuCCIaR0qE/Bysvmf0Hu7CCXP4TlYDdHAnJplDuPGWjVgdZugUzdBJguCLXxziC3oCoEms9SKfCDK5/SIHJnsaP33+CL961aLX5jYXQHKgSgyIOQLYbgGcb7acxloeR+ycL3Yanvr78LskeUJhWomhfYmxB1mOZH1WXm4AiLMpDOoJyH2NwqRQMMZ1D5XagoLnE24hWJGjAe6iPp2LEDJ6gMiowd0FWeLH2LW/ix893EOnCBpJa+Rcu93m8bSs0UyJTsJgpPCFWwsUivCcCysT/ikKIHb379yCDoMB7AEQA0Yp6yadBfBKGk/Fjvy8q1X48Thsky4glIlAgKgSPxVMjqNFeQFvycz1S2zEuMohB9TDpFBoVFxgjE0oso7dFEoIjhksJkCwnEYtyaGMGEKrAkH0oGHsOVBguZ+h22mPItYCw1qvdPq2e030gO/c28PJOwd2WFe1cwCqg+8kRkZAr2eAlJJAZqAoCNNTFG3/0prLp2A1GiGdLCeBQGsInwM4A+EUIJT1yPOowIhnUGmKUuuN5eEr9Sym1RS4nrcnO9WEZZEEglgQ4uqP9reRCRkbLiZTvMBW2UiBoqd1PHrhH6usAlBrBdUxOYaqasKNAWzZCKiMUZemrXnG2DgTtreNAUPo5ISpnvYAiIQv5NsUZKrtskysuQ3hR+Y+gIrcAy/kOVq4snwaq+Wk3YAjoFmwLqPd/XoKldpixvH9RUWjNMzTKmwDomnJWngfGbZoR64ETEvGaacqzM4qMMdLqdJFGMf6O1ZZrQkbZhQuPk+j17UATspkWYZn7BjDbINrMmiZqnHyCbUlgsk1mbBxA7B1Q2kebu6ijlRQ6aNr2oZufTmO+8iBxe0+JpI7blc1fxrk0nCO2kJMqNUm9Mc53IhMEwAwPmWsOxhis3lwoeaWhlsfo+VcdQ68woZFrTfqH9XDqGvGaSeOcMUvGg0rSzNOIP+A5rH1/uys59AH2swp6PYUrvqlDk7dtohLnpNhPDZ1O2FWFeHEnQoXn1uBx9rMbAaDyxovvKTCzIyym0KQv8VxCfzs8wkT+UjILBZUWMwh+k6vI/g0vo/2AwjnMlhgheM/iL52BFVNYqCnMa6VX5/gXmFSKIyL1iAsVxsxnW+yj35TiEmcKx0uxirObPmpZME/CNMUwja91Merf34BZ5y1Fd/4psZDD2j0B83JGuYsaqMJUYmoZQxs3Khwzrk5fu5nM5wzux98eAXXvHyEwwtT+MHdY5QlmxBzR4Y3XaewubuEuk+e2LEGTtu0gGtftwl/dwOwvKyhtRmhvOxFXVzxgiPQS6UFK8Q9uq/SLbpIIeEA0bu0nokVSChEPAIYlwnhpf3e2YSlQdcnFWVHRhzAJbdWqkmMe5uRq0NAPRK9KkkhhUkhlhS6xaOsOSBNgsA1TmYRH9mPS3cOcOkbN2GketCUiaST0DoHAjH7qA0AsnyGGh29AiweQX3A7I20qdyL3/vN03HfI5OYOwhsmGGcfTpj02gP9NGRSH5ZpVtexUsuOoALfupEPLALGI4IO3cAZ+9Ygtp/IMw/TJM3Ul8iAshr/CbPCfX5HcTc3dnfWdbXxjUsgCjrocQGrI5UWBUlmhiTQDJzYiousFRvxJZsC7jaG5M3OeKgk++2jCT7JqvM7kOcPbSupN53BJibD2v8otEaF59xOJeaAAhzS0jMaGPUWkcArPsjFPt+jAs3zeDCEztmyfTRFdTDUVSfj9MB1HMHsGNyGTvOnDThwmAIvXvZjgJK6+aEID60bZqxruBjAJA4BB9lJBFC6qYlOIotWBp2UNV2yDvBaMMCuI2blsoZzHY3QamjYD2wGm3NmBIJn2QvfB80JAYgjC3YAyqAwHUil3UQfKzQMRBa3AGLcp6Kpuu23ceqRn14PqDUL3YF5JQyKa96pQ8srwogUyxoyO/y5DXMvJezEKoEnuBODohhxDHRek+wheBZA2oKtdqIxb7plzbXKvIArpMBxYxR3cFyvQGzna3gwR74oE+OvcdDb4iGe9mGZxTMliF1QFgyRqFDJXKly/E2S3R045pJr0Y9DGE5EJ9HSTl5zbaxDglCqanp9dbV/Ph6fudEzZ4isPiNo3MSwYMTohgAwExQxTYsDTKMKkJWOMMa91cggSoA2/XB/HgGMxOboPJFcLkIVo5orSUA24hoto+YEwjnDsSiUl+fuLD3JrYjJfdsrCuU2iebQ83OapRpwUwjOdPit9c8Jr6nw9Sy2ULbwzprUS1HFUWXa5RrCN9alXwWNc1gftUOujk5gKBE8NfiAoKFHtUdLJZT2NzZZh4Rpyu4hZ+JBw5vDMsN2Gsdkxi5s8iWoIjvikyyKe3Y6GsLUBovKW3BGQhh2puvyw3CSLeUAq3ZlFhQLVzANy/wAZ++FeXZ9QnLsQT7vpavh/wcyrEVK3VOwEJfY1RlyPJAq4liUhkDQCKFAEWMo+MpTGfT6HS2Qg/3+97wTwWMLHKb2ZMdKR40BY761lcmOguN39YQsq8oKSNN+Voa6lxaq4QhcdlO4hrfOQBLClCkzV3IHJoQNDfUlZh72e40BHTn2mtR7wSMdQdHV8yUM9c1JoO7hguoq1pTZl1BDTC5EbscR0dT2DGx2T63dhHCxofJo5EgRAekx5nNMLOfPBLug2xHEZuh6EixG2hpwwLH15ezaWi989IDabxOLQJHEGrqamTFa0QKTutjytDi51sndsQADKuINZBtBOWbcGRhjKpWyN0GVwgcoK61h3wAgObSic2tpQeZlbmL4x6m8x6mu9uh6wHAYy9AtnFwFJl5ly410t2lNb92kIitgEiAwVllRjAYbhjadVQYkk5HacIH61Ds1zbSug6auLXSpplnea1YwIFnsuCHdotcUR97oIg6U96SmnvfFPtdM0BdqN6JWB7WWOyXINWFm9Mo09+1hn/8S1gYovWqY4iKyO/9o2x28NBgEr2pCll3J3i4G2HygvPzkhyKREDEOoUplEvCyZm/sAtZRNLDBJ0AFodWOXomtT5JeQrO2RRsNPMJ/lhkvtNKGEHo3suwT1MQyKTL/e9iPx9p03ULoNqGfyOSJ4+5AwrU24lK5zg4bzK4foN3DzqbC6nrVVe9V5/VwXBBielY8cYnjFGd4dCgC2TToM6OuHEO8m7cgzk+3jBrCMf9+IDpDBPratN5bhBJ4seh3b275L+LhZ1Gsg6PoQHbmTku7y4/A2k+nmUZuHaI33XahiAcsp85rRNC8G6AR2q9GCdp9hHHwmc0Bp6oux3IZ3BwYQWjsoYCeSCG5XCGfA8Gw4WGBajK6hBRF2EzZyN8BYCVmVe3OCrQVTk2T2wGdAkeHzQlhKtNdEtYBeH3vfaKkpF7CGrF1lrEgxgh2eMpqY0cXMhFwie5VIP5ogFxJhA2rYxm0ApXknKR4FaEm+HkfhraKz7LYo0y7ntsaZqRQEADdbaBOltxdHEFSytDZKrwM5m8Ejt5kkI1rg83ADAYjh/W6ELZ+VJ+ur8jEAQwEQ71C3RUjenuCQDX4PGR4IflDCBnLlMXEHnLNveAaJsAV698xp4HjLD40g8HjyBn3woASfA5vOlkzqNocVuUKV2Gh6U3721CTQUeNT728ZHgk7o8sbVWqrMFqrsDy/0BDi2sINhuc89m87bAAUAKo6ryj5T1ABiV+sCgX9aKVMaoo2ScEvKqWWFuJccpMyW6nR3G1JXzEEQgUjkf2niMUEuPik5IHqkunGqAsfMFAmzpadHaRCkkQpg5KzNv4mz2grVxebQhZowCFiOdfnhWgizV9FSovgHc+HnN+B9G+FRsBnV3YjgqMXd4EVozMmVSvsqafafACma39X6/rAflaL+7Bc8Bdh+YO1BWejG389wUxblj5w4yBZQ1Yd+SeVfdnaBic+zrXWO1OFbDP8Il+NDEJ0dITzrK+Uf3XiP4Uc8dQmeyXd8gx+iJ3WXY9mEyiYLDHDvv3xH/xiw5AcymhZIjOIFFPAHNvmnpI3YK0ygvweOEvwXUOwllWWPv4XmUlfbL3kPYFxJqRIxMZRjXvLB739KBBgD+9kM3z9Uac1me+VDQPyKHhBmBWcwxrBT2LmqUGqDuiUCxNXRkSv6ibJXVHFFUksCIkCVTtEOnJZ3oySQHMpUKQFtiBgcMnQggEbQWv8v2aTTKt4M+aZ9vp47ew+QOjold1CdJncU2UHcnylLjkUNHMRqX9lFL5BWVrNBJBUDkuYLWfOBvb/zcnJO7dwG33nr/8vLq8N4tk/n5ZKFBmuy+OYYTgAwonH8elIS98zVO2qhQdHeAUYDHc0aIJKbSxk4SgSNYnylWElHkVd3J0kUcK6PD7YdlVVGRtfK8UuuSY85VsajD3VfDjjsFEDWsSQA5eZcnMMAK6G6H6mxFWdXYe/gIBqMxMsr8OI6yAnJcOgCCkGU5lhZH99566/3Lruqwuo+ZH5k7eie5TZTsqhiPJBFGmBU25mKDMeORhTHGZQ3V3QqaOBWgLsB2f3rNMaobGiQ1B3aNnTDNci4hUm1INVJ0bKv1aDueanFb+eSagBnviHyzs15osQLia2SdOP4cuQeEurQG0AUmngHV3YZRWeKRg0cw6I+Q2dA9PGxNys3yAcdoVIY9B5fuZLF+PRoL2Lt/5fbVc2bLPKOi0lL4Ruh+2p+7GDGgGMOxxp6FMXZuBCY709ATp4NH+03a2A/3CjKXbjgRaabbZRSCUVs9E8YgaDSjMfQaETCZUfKmaG1thzwv0cRUsxvnuDawOCTdoCgblU8+syjEDOQbQb2dUKqD1cEQ+w4fxXhcI1NB+MRkzL113c4hODllmcLqoC73H1y5Xd5OlEf9yGdvvXt5pZzL8ywaFnYZJe9jKJysLGEcjzX2HBliYXUMUgWod4pNGGWIEi8S7dKXt02AlNrq+aLwmd6/JxMrU78pSVubX47qEmX0GmVdY8TvHP05n95yv+mz/6I2QdystiZ/B9TEKSBVYGF5GY/MHcJ4bBa4EIDMp3itvHwGN+znTAQUmcLSajn3yc/dcbeUeWQBvvrVb+8/uvjS23du6pxSktV4t44Pdlsgsht+KLMRogEEuUEG7JsfYjDW2Lahi6x7ApBPg4cHgGpZjAY6RRT5gnT0z/tPGTa6JJLVLR8exhpGaT0uhEyXH/gBGNkeXpsWCMX31knOhpaxM7ec1/DtyTHvIwDkG0Dd7aB8ArqucXD+CI4urIAIyEhZ5QuCVyA7LscWHAEYRObBF0eXRt/5/Ffv3i9aEFsAZq7u/OGeL5VV0G4nYIChbMDtWKZb7+ESRZmdWXV0eYQ9h1cNO80nQJPPAE2cAlBPaIUWHKFNy5IOivyjsBK1sBY6KGfqd1knWgoO1sT3PTczxbIO2TxpAaJwDzHvSdsb3YuOL6BrQPVAE6eYPssnMBqOsHv/ARyZXwSR3bGExLxHAhTbBBYRFIdt8MiCQYFQ1sD37jvwJWau1gQAAHz6i/d89ch8/2CeC/9C7HeBdxeTRNAAgj1YMgX0hxUePrSCI0sDoxzdWdDUGUBvJ0AdG0sLtwCddGbyLjsuXYARkSjz3ecafGjIDZcSx+tJXTr5S91J3VJnRAglwRMWygqb5f1pbYjzxEmgqTNAnc1gZhyZX8Cuffux2h8gU8oL0ymcfJKt++7Jn7MQbMz//OLo4D9+ffdXUnk3APC5L91275655W90LA/wIIDMCcQhhlKOhbIvnyuGrjXmjq5iz8ElrA5GAGUmUpg6A6p3EpDZnclqHRIqMlZue9BzW/ztTHEj/kaz83WbcIFm4ma9P2Gh2qKSaBUUPEfwFk8zSGu7ydYk0DvZ9El3GwgZVldXsXvvfswdPAxdaeRKQVkL7CIyZad6KJBVQo4GX8PWCWa30t0HV7/x6S/deV8q78baQGYevvPf/cubLzpz5mUZUVHbG1WKvM+HNT2y9xUQT2qEISIAY3U4Rn9ujI1TXWzeMIFetwC6W4BiFlyvAuWi4Qhcwee609Rv9EqcdDIzuXXPomhLO1ENS/4hz3FfE78d1Rn1XAtnCKgJmzQQWOWgfBooZkH5FIhyABrDwQBH5uexuLwCBhuWbxULclTPkXHFIeMHxwccMKyCKoX+qKpu+fbuTzHzIJV38+nhAE48cdNpn3rva24++5SpC4eDAbguwVxCVyW0LsF1CXAFXZdgXdtHzGtolilYjdqadud3da2hFLBxqodNMxYIUNYfl6BqxaxGqvtm/iFq3+FMfraAGCSSdC/5bT3ANLNB678cLpib9UkCJy/jNB8MogygHMgmQPk0OJsGZR0/Z2A0GmJ+cRELy0u2j6xuKyF0RTasU3ZjCkfylH8HEVRWgKiAyjqgrIvexCR+vH9095W/f8uV+/fP70pvLW+737m5hd1f/87Dnzxr57kXKjLsHyym8pMZHvbLteyom0IY99YI352sSBG01phfWsXiyiqmJrqYnZ7EZK+DPCvAnc0ANoG5BvQIVA+MhaiGIGgw1yCIh1rKDaQcQCxxZKIWLKxH76n57gd22OyY4moQzxOIiCoBQAaQ3VVKdaGyKSCbBLIugMznVKqqwmq/j8XFRSz3V82jepVCRlKbXTinoDjO8ysxwcOl7oPvt09HIbMd/Ve/O3fT3NzCnjZZtwKAmfX5z9z2yZc8b+frTj1h4vSRNo8q9H1O8R4R2v8Q+gswGz5ruz+gG3NXdidxZo3llQGWVwbodjJMT3YxMzGJbrdAnmXgbArIpgBssVygAukSzBWgxyCuwFwbDeMalAjVr2FoE2wEghaTESQd/Bo1SzLcM4hzK/QOmAqQKsCU2/2OnJAYVVVjNBpieWUFKysrGI6GYACKMjN27/mVECaH0T3JvXzL3Tlkt76xiqmI0S0UHj60+tD7b7r7k8zc+szzVgAAwI/uP/yDW765+xNveMU5byVLNjTcticiIIYDA/vfXaZQcwwUdv8oPG+IoTEajTAYDHBELaJbdNDrdjA10cVEr4siz6HyDMQdcNaNxWj9PfHxmnaX3WsBhTfxoo41qwtA8kJg540t+AHUdYWqLNEf9NFf7WM4HGA4GkEz++cV+8UzLm6H1HJBumVaPjhDI2wmfwuKQuJOM/DFO/Z/4kcPL/5grR5ZEwDMPDrjlK03XvacHb/8zJOmzhwMSotIKUiASdtciA4PFtHmd7/pmt1syi3T86SZLKDI7N/HWmM4HGIwHGB+wXRSt5Oj0ynQyQt0ux10igJZnpt9e+yun54mScIYDsRSdCYbciIni+PuXOnfJRjIGgbD6g2pr8GaUVUVRqMRxuMhxuMxxuMxhqMhtK6t8NyDqsO+BhT9BeH6wR0nbOcC4CZ2kM8GBhcQhrJ7RYb7960+8H///b03MnOy8PE4AAAADz1y5K6Pf+HeD//2NRf/x0xB1X67MvtHzgJY388CGBymYriZMspyBfN7SMiY4zZ5kZFItmgMBkMMBv3gjgk+JqbM5Coyu8ebmz4WBvRbhNmw+OKHRNvbGINfpwdAa8NHdF2hrmvUtQ7Wkcz0K2WnZplFIe7duVIW5hux8P0/cdxFA5HQ5bnmX6YIo5HWH//Kwx9+aG7prvVkvC4AmHlIRB978SU7f+HS87f8TL9i4YSM6TVTxxjaCpA1/DFHBp1pJWbzCD9tiCTbRRl+3gXI1OOshuUR8nEpDEBXNSpoYKRtO7UHm3wL+/05QEhaGgtaWgMHa3Jz0/wStlDSC8NqHbkkjc2YhVlUDLciyEzRtk8mkULmIFQ5+4qY7YrexFow/OMK3ZZ3bvIHKaBXKNx678K3/ujD93yMmYfryfh49sC+5z0fuuP9h+cHC0XhGh7MleKQ/PEZKWcgYJ5NEdBqGq+s1itm83QNCsOZGUSGi2AnNoZwKIPJSuYAcmWyjrki+6eQK4VMEXIF5BkhI/kbUGTyO9k65LtCoRQKZTJoeUbISaEghZzcZ0KmFHICcpjPCuTbnomEmMmimjZnpH3CLEPw18qVCZoHN/GGICxFZCEYisknh1zc38mBw4vjhfd87MfvB3DPsYSbH6sAM1dE9LlfvGTnjddecea/zWCic6ssIfRieKRr5faElFxAKqjTKKGkIE8OrfW35zhrEDonWi/qtnORMbmdHhyWiYfIhAXhczRBnhaZfQ6g9gcjD5MSwUA9nNYqRw6JvAXxTB+hXJr7aroCYz3gQOWAAUtASUOpDGDg5m/M3fjpWw99Ls37PyYAWBDsm5wsrr/gjA0XXnr+5uf3B6UgghpkHnjixOAfHGmNefT0cbE/dhAsw5UMEQICrgDyw78mKjYux/GQaIWtAEk0AdW5LVemLWEkg4IWzhAlGF3CUYAZbaGarYu8GxHPYYbrE4rLROTO3E9kARIu4O5soqNw64+Xvvn2D9x/PTPvOx7ZHvdjEAaD6o7f+6s7/uqRg6t7e4XbPpWF1lizxTZnbTy6B4cbvnSdZcw8+ZBFLhdQ7MozMmseM2tGXd2Zcm4E1hTK0Un3PZzjzotGMG1nKsTzH6Nr298zV47cnsPw52VAZP79+Qh1kQvPLCl2ShLmVIh+ce1js49CWsYv8tSBIvYKwiOHBnt/728f+KvBoLrjeOV63ABg5sFtPzj4+f/ykR/89eqgXO1kbvaJFpzAEjeSHUumDLE/FoDBotO0zWDp8MfynFBGgQVILIew3zOywnLfOQg3cwK3dThQxUAIgAhATf/MNSK+AjLtQJgyF8x8cBs+Vgdi4HEq/HhI3pl6+fAHsvfRyQj9gV79s0/u+evb7lv6fFvOf63XcbkAAYI5IvrYmdt7O//tFae+KSdkfgWBNclmWrVzCeZOzNNCNBgaRNqzef8IdEeaRIgVlhg4ZxKF8NZasHC8Ngx0vQy3KCQ2+/6hi4L3yzrCzfpT3Ik+GpGLlNxM6XCuWFnkXIkgyBCXSRZBheZAZAWdq3Eu1B50Vjcziz3rD3zxwIff9z8OfoyZ5/AoXo8KAPZ179v/+u7rT5wttvzKC7a9kslNigmd48gbeYIXHmniuJNZ/WuIjV8S4OTAgSi6/9nzB+0dNEdgENhwgo9WiwhCGLEMJMIn2Qhf3oHZy8mX1/ZwCA1BIZXrGLAUPiXgEFcWoDDWM+UgbsmbeQim2Un9k986ctPbP7DregD3PlphPmoAMHNNRLe/8T13vn/D5LMnfuFZsy8ra0Bb7Q8DQiKN7kbFHDnkQBZdNtC/e0vir2e1W7TBw0p2nYzPWZTlRieH31oOtpYXz0vgQNT8Z3vcA4OloGwZGx5Qgga/R5BrOydlRPOUsCxKEboZ4QvfXfjMG//bj98P4Dtr5fvXez2KZ2FFIBhXFf75mj/53vu+/L35z3XyYL7NZIfg49zYgfPncCNVzuxz8JM+7IKcjiZIofOh3ncGwmX8urYkNBA/xe7PXstfL/X5sjw8mQ08RvAKBNLppmhFJC4dk5f5M4h75ODrfYJHSNzVl2ydgAxANyN8+e7lz13z5/e9r6rwz+ule9d7tc4HOO6TiTZOdrIX3fB7573pFy6afVmlNaraTKgzZt1l6ORqHCNpl+oNM2p1OO7GHr0ZcOXiqTrxqCyL5EFsH0ShyFc3J5y6Y83yadRIog6z8VWw9WH/AitqhskQ+uV2NsNPIbCToz7xPsgq5BCUSTplRLjl+0ufueb/2vX+/rj+EjMvPmYZPh4AWBBsyoAXfvB3znn9K567+SqlkJelIXx+eZXz3SL/7x+VpnWYZOmSPmKKNAtyGWZ8oilsmWkKmQZEM4HaYn8kAAh00/5MCEvNOfqpUZ1YVx8edRfAQD7eVXBDpkQuhgi/u3SyB4AtURQZaqbqU99euOm6/7brQzXwdWaef1zye7wAsDe5AcDPvOeNp1117eVbXzPdy6aHYzNW7wXot6XRfs48W/egxXRoaSUCIKzAIcAieIKMQuB/l8JtF3njPta6QV6jgCWLIh/op2W5zSf9pkxuyRaUtwxG0O7ZBWH5tmH91gpYy9ErMqwMeOVDX5v/u9/90N6bAHyDmZcet+yeCACYG6dJAJe88UVbL3vnq3Ze+4zNnTOH49oTPhYrbd30ErLTyPykSe8aZFkOs4adVgN2uZSQhK/Dnh9ztqY0G3vdCRPhowC5QjrUHbSaPW9hMTXbCFzsxI+g+W5wy2BAwY3qyK3v463pFXodhd1Hygf++BMHP/SBry58GcC3mbn/hMjtiQKAbXgHwAXnntz76b9806mvvuTMicsJoFIMk7IWgmQLBQ7DwVqLp2DaocLI91sAuQUZIZ8gXIOP2xN73RbrU1oADTMve6ixT2UsZgGAZNqGm1dPIomrrEXg4Pt9WSJ0MpMiuu2B0Zd/4//Z95F79g9vB3A3M4+fMJk9kQAQQDgdwLPf+6sn/+Irn7fpX22ZVptHY41a1yG6Fy7Amf1A/Jw1CGPFLMw868TEOyviz5dSJGEN4sBRvuJeEJofx58yK9NCDENO2wvcBXiuGY7Q+bVcghDaY0qZvP6RFT76sdsWP/6W6w98DsAdzPzQEy6rJwMA5j5pE4B/cdlPTT3rP1+985cvOqX3/E7B+dAkDaxrsJqtA4uXhC5s1iS4QET0rFCdBXHfXUzZktQJuJDJBUn7zXcj62OQPnEw8ETp320E4LM7Qcjw/l8QPiL0CoWyouquvcNvvvPGQ5/8yj2D7wK48/GSvaccABYEBYCzAZz/9leccMmvXjb78pM35edlxBhXWph/wC26ZG0F6KaU+/SyjRqYwzRlNxJoz/cJJpEpbE38iO9tQvVybBW+zXY6nx+5DZ/OMY/fJZEfjKICu5zKEkIFhU5uRkD2HK1/9N+/tvSZ//MzR28D8EMA9zFziSfp9aQCwF+EaDOA82ansrP+4Mptz7vqp6deumM2P7NQwLjUqOVyKrih3sZivMgCyJ1/WLssYhCIU3wzMYG8MYhIIIRFJ7PDtjUjIquXcIM0qpSpA05yACR4gavQzeuzHKGbZyhrYN+SfuAf7lz9/B9/euHWhdX6fgA/YuajT7psngoAWBAQgJMAnL1tJjvtd35x9uJXPGvDZadsyc6bKKhTaY2ykkzfkT1J7uBzBPJhVf4Rav68xtXR0P+W0E7gJ17EnJ4X/WaBYwt64uen5lpQ+GdhkJlplBP6Ixo/Ml//6Obv9r/y5/+0/L1Dy/UuAPcB2MtPkWCeMgD4CxJlMEA4A8BJb3jhhme+5vlTl5y3vfOs2Qna2S2AWjOq2loGrcNgDALR8xrvxhlaNnz2nG2NFycabeoN/p+THc2ijZbTcICDmXeuwF08sw+gzhRhVAPzA973o7nquzfc2v/2B77R/zGAvQAehBH8o87nPy55PNUA8Bc2vbUdwKkAdpy5tdhx9fMmz/r5cycuPG1r9szZCd4+06Upp+2azZPGNAtTTyaXINm/CyelcKJ8ERxblwQvnEDp/xSb+shCJAEHgewcR/e4WbOCamWM1YU+H9h1tP7xLfeMvn/j7YP7Hzis5wDMAXgYwIGnSuMbcni6ABA1gmgawA4AOwFsLjLMvvyiie3PP6M49bmnFc88aVP2jG6GmYkObZzMMVlkgf0HwQdJeNn4vE6crYtfcoNI1yDEHoO5ca6zSvLMqmb0S/SHFRaHJZYfWdJ7vv1wdd83Hxg//Om7xwfKGgsAjgLYB2COmVee9r7/SQBA1CCiGQCzALYC2AJgBsD0WSeo6Qt2dmZP3Uwbzjkh37ZlmmZmezSTK85nujSNZFgnucsYEQBiSkhYx1MgpJtE/QxeHvNKVaFaGOrlI6u8fO+B+tDDR/XS3XPVwv0H9QqAFQDLAI4AOAxggZmX8RP0+okDQNQ44yZmAEwDmLLv0wAmYWeGw8ijeJqaWMKgo7J/fRihrwBYte/LT5d5P57XTzQA1my0IZIFDAAUHtvMpifiVcEkpSsA5VNN4J6I1/8HBq3Cgi6UwmIAAAAldEVYdGRhdGU6Y3JlYXRlADIwMTUtMDUtMDhUMTE6NTA6NTUrMDA6MDBXfw+IAAAAJXRFWHRkYXRlOm1vZGlmeQAyMDE1LTA1LTA4VDExOjUwOjU1KzAwOjAwJiK3NAAAAABJRU5ErkJggg==8.511.01.01.00.50.50.50.5111.01.03.31320.35244.62022.78231.65660.1762-0.03.31320.35241.65660.17621.65660.1762-0.0#FFFFFF#0000000.00.010.00625#ffffff1000.010#4d4d4dArial0.13888888888888890#4d4d4dArial0.13888888888888890#4d4d4dArial0.13888888888888890#4d4d4dArial0.13888888888888890#4d4d4dArial0.13888888888888890#4d4d4dArial0.13888888888888890#4d4d4dArial0.13888888888888890#4d4d4dArial0.13888888888888890#4d4d4dArial0.138888888888888900.00.00.0-1.00.00.0000.00.17623.31320.17621.65660.01.65660.35243.31320.35241.65660.17621.65660.17620.0#ffffff#0000000.00.010.00625#ffffff1000.01111110an image to be used as an application logonullnull[]nullnull0null5solid0nullnullnullnullfitnull100nullnullnullnull[]nullnullnull2.58810.63816.45597.44381.29410.3191-0.02.58810.63811.29410.31911.29410.3191-0.0#FFFFFF#0000000.00.010.00625#ffffff1000.010#4d4d4dArial0.13888888888888890#4d4d4dArial0.13888888888888890#4d4d4dArial0.13888888888888890#4d4d4dArial0.13888888888888890#4d4d4dArial0.13888888888888890#4d4d4dArial0.13888888888888890#4d4d4dArial0.13888888888888890#4d4d4dArial0.13888888888888890#4d4d4dArial0.13888888888888890#4d4d4dArial0.13888888888888890#000000Arial0.138888888888888900.00.00.0-1.00.00.0000.00.319052.58810.319051.294050.01.294050.63812.58810.63811.294050.319051.294050.319050.0#ffffff#0000000.00.010.00625#ffffff1000.01111111contains all the script files required for an application deployment nullnull[]nullnull0null5solid0nullnullnullnullfitnull100nullnullnullnull[]nullnullnull2.66246.8743.66376.8741.00120.03.1636.8740.50060.0-0.00.00.11250.12370.00.00.0-0.00.0125#8080801000.003125221101600.12370.00.00.00.12370.0Reposition Text1000.00.01.00120.0121.10819.72911.10814.01210.05.7171.10816.87060.02.8585-0.00.00.11250.05.01070.00.0-0.00.01875#8080801000.003125221101600.05.01070.00.00.05.0107Reposition Text1000.05.7170.00.0121.10818.39362.16188.41321.0538-0.01961.63498.40340.5269-0.0098-0.00.00.11250.1302-0.01720.00.0-0.00.01875#8080801000.003125221101600.1302-0.01720.00.00.1302-0.0172Reposition Text1000.0-0.01961.05380.0121.03881.03882.45598.43240.51940.5194-0.0#b2b2b2#0000000.00.010.0#0000001000.01100.00.51941.03880.51940.51940.00.51941.03881.03881.03880.51940.51940.51940.51940.01nullnull[]1nullnullnull00solid0nullnullnullnull/imageProxy/cdn3.iconfinder.com/data/icons/vista-general/128/folder.pngnull0100nullnullnull[]nullfolder, open (128x128)nulliconfindernull1.10816.8742.16186.8741.05380.01.63496.8740.52690.0-0.00.00.11250.13020.00.00.0-0.00.01875#8080801000.003125221101600.13020.00.00.00.13020.0Reposition Text1000.00.01.05380.0121.00121.00122.43726.8740.50060.5006-0.0#b2b2b2#0000000.00.010.0#0000001000.01100.00.50061.00120.50060.50060.00.50061.00121.00121.00120.50060.50060.50060.50060.01nullnull[]1nullnullnull00solid0nullnullnullnull/imageProxy/cdn3.iconfinder.com/data/icons/vista-general/128/folder.pngnull100nullnullnull[]nullfolder, open (128x128)nulliconfindernull1.62510.38832.47386.2950.81260.1942-0.01.62510.38830.81260.19420.81260.1942-0.0#8feaea#0000000.00.010.00625#00cccc1000.010#000000Arial0.1666666666666666600.00.00.0-1.00.00.0100.00.194151.62510.194150.812550.00.812550.38831.62510.38830.812550.194150.812550.194150.0#8feaea#0000000.00.010.00625#00cccc1000.01111111Resourcesnullnull[]nullnull0null3solid0nullnullnullnullfitnull0100nullnullnullnull[]nullnullnull1.62510.38422.47387.83630.81260.1921-0.01.62510.38420.81260.19210.81260.1921-0.0#8feaea#0000000.00.010.00625#00cccc1000.010#000000Arial0.1666666666666666600.00.00.0-1.00.00.0100.00.19211.62510.19210.812550.00.812550.38421.62510.38420.812550.19210.812550.19210.0#8feaea#0000000.00.010.00625#00cccc1000.01111111Classesnullnull[]nullnull0null3solid0nullnullnullnullfitnull100nullnullnullnull[]nullnullnull1.10815.27722.10935.27721.00120.01.60875.27720.50060.0-0.00.00.11250.12370.00.00.0-0.00.01875#8080801000.003125221101600.12370.00.00.00.12370.0Reposition Text1000.00.01.00120.0121.00121.00122.41215.33480.50060.5006-0.0#b2b2b2#0000000.00.010.0#0000001000.01100.00.50061.00120.50060.50060.00.50061.00121.00121.00120.50060.50060.50060.50060.01nullnull[]1nullnullnull00solid0nullnullnullnull/imageProxy/cdn3.iconfinder.com/data/icons/vista-general/128/folder.pngnull0100nullnullnull[]nullfolder, open (128x128)nulliconfindernull1.63250.41472.47754.74260.81620.2074-0.01.63250.41470.81620.20740.81620.2074-0.0#8feaea#0000000.00.010.00625#00cccc1000.010#000000Arial0.1666666666666666600.00.00.0-1.00.00.0100.00.207351.63250.207350.816250.00.816250.41471.63250.41470.816250.207350.816250.207350.0#8feaea#0000000.00.010.00625#00cccc1000.01111111UInullnull[]nullnull0null3solid0nullnullnullnullfitnull100nullnullnullnull[]nullnullnull1.10814.05212.66244.05211.55440.01.88524.05210.77720.0-0.00.00.11250.1920.00.00.0-0.00.01875#8080801000.003125221101600.1920.00.00.00.1920.0Reposition Text1000.00.01.55440.0121.48340.34372.66292.30350.74170.1718-0.01.48340.34370.74170.17180.74170.1718-0.0#e6e6e6#0000000.00.010.00625#00cccc1000.010#000000Arial0.138888888888888900.00.00.0-1.00.00.0100.00.171851.48340.171850.74170.00.74170.34371.48340.34370.74170.171850.74170.171850.0#e6e6e6#0000000.00.010.00625#00cccc1000.01111111logo.pngnullnull[]nullnull0null3solid0nullnullnullnullfitnull0100nullnullnullnull[]nullnullnull1.10812.76952.66242.76951.55440.01.88522.76950.77720.0-0.00.00.11250.1920.00.00.0-0.00.01875#8080802000.003125221101600.1920.00.00.00.1920.0Reposition Text1000.00.01.55440.0121.63910.37582.66293.59190.81960.1879-0.01.63910.37580.81960.18790.81960.1879-0.0#e6e6e6#0000000.00.010.00625#00cccc1000.010#000000Arial0.138888888888888900.00.00.0-1.00.00.0100.00.18791.63910.18790.819550.00.819550.37581.63910.37580.819550.18790.819550.18790.0#e6e6e6#0000000.00.010.00625#00cccc1000.01111111manifest.yamlnullnull[]nullnull0null3solid0nullnullnullnullfitnull100nullnullnullnull[]nullnullnull1.36590.3152.66291.09280.68290.1575-0.01.36590.3150.68290.15750.68290.1575-0.0#e6e6e6#0000000.00.010.00625#00cccc1000.010#000000Arial0.138888888888888900.00.00.0-1.00.00.0100.00.15751.36590.15750.682950.00.682950.3151.36590.3150.682950.15750.682950.15750.0#e6e6e6#0000000.00.010.00625#00cccc1000.01111111images.lstnullnull[]nullnull0null3solid0nullnullnullnullfitnull0100nullnullnullnull[]nullnullnull1.10814.09011.10811.56730.02.52271.10812.82870.01.2614-0.00.00.11250.02.21110.00.0-0.00.01875#8080802000.003125221101600.02.21110.00.00.02.2111Reposition Text1000.02.52270.00.0121.10811.54452.58481.54451.47680.01.84641.54450.73840.0-0.00.00.11250.18240.00.00.0-0.00.01875#8080802000.003125221101600.18240.00.00.00.18240.0Reposition Text1000.00.01.47680.0123.66377.3873.66376.36090.01.02613.66376.8740.00.5131-0.00.0125#9999991000.00312522111121111000.01.02610.00.00.01.00.01.0NURBS(1.0,3,1,1,0.0,0.5131,0.0,1.0,0.0,0.5131,0.0,1.0)3.66377.3874.66497.3871.00120.04.16437.3870.50060.0-0.00.00.11250.12370.00.00.0-0.00.0125#8080801000.003125221101600.12370.00.00.00.12370.0Reposition Text1000.00.01.00120.0123.66376.36094.66496.36091.00120.04.16436.36090.50060.0-0.00.00.11250.12370.00.00.0-0.00.0125#8080801000.003125221101600.12370.00.00.00.12370.0Reposition Text1000.00.01.00120.0120.75090.75094.797.3870.37550.3755-0.0#b2b2b2#0000000.00.010.0#0000001000.01100.00.375450.75090.375450.375450.00.375450.75090.75090.75090.375450.375450.375450.375450.01nullnull[]1nullnullnull00solid0nullnullnullnull/imageProxy/cdn3.iconfinder.com/data/icons/vista-general/128/folder.pngnull0100nullnullnull[]nullfolder, open (128x128)nulliconfindernull2.24610.38194.85175.90311.12310.191-0.02.24610.38191.12310.1911.12310.191-0.0#f7f4f4#0000000.00.010.00625#00cccc1000.010#000000Arial0.138888888888888900.00.00.0-1.00.00.0100.00.190952.24610.190951.123050.01.123050.38192.24610.38191.123050.190951.123050.190950.0#f7f4f4#0000000.00.010.00625#00cccc1000.01111111execution_plan.templatenullnull[]nullnull0null3solid0nullnullnullnullfitnull0100nullnullnullnull[]nullnullnull1.59750.39814.85176.87340.79880.199-0.01.59750.39810.79880.1990.79880.199-0.0#96f8f8#0000000.00.010.00625#00cccc1000.010#000000Arial0.138888888888888900.00.00.0-1.00.00.0100.00.199051.59750.199050.798750.00.798750.39811.59750.39810.798750.199050.798750.199050.0#96f8f8#0000000.00.010.00625#00cccc1000.01111111scriptsnullnull[]nullnull0null3solid0nullnullnullnullfitnull0100nullnullnullnull[]nullnullnull3.79560.35264.79558.67151.89780.1763-0.03.79560.35261.89780.17631.89780.1763-0.0#FFFFFF#0000000.00.010.0#8080801000.010#4d4d4dArial0.13888888888888890#4d4d4dArial0.13888888888888890#4d4d4dArial0.13888888888888890#4d4d4dArial0.13888888888888890#4d4d4dArial0.13888888888888890#4d4d4dArial0.138888888888888900.00.00.0-1.00.00.0000.00.17633.79560.17631.89780.01.89780.35263.79560.35261.89780.17631.89780.17630.0#FFFFFF#0000000.00.010.0#8080801000.01111110contains MuranoPL class definitions (*.yaml files)nullnull[]nullnull0null5solid0nullnullnullnullfitnull100nullnullnullnull[]nullnullnull3.06660.35514.43335.44741.53330.1776-0.03.06660.35511.53330.17761.53330.1776-0.0#FFFFFF#0000000.00.010.00625#ffffff1000.010#4d4d4dArial0.13888888888888890#4d4d4dArial0.13888888888888890#4d4d4dArial0.13888888888888890#4d4d4dArial0.13888888888888890#4d4d4dArial0.138888888888888900.00.00.0-1.00.00.0000.00.177553.06660.177551.53330.01.53330.35513.06660.35511.53330.177551.53330.177550.0#ffffff#0000000.00.010.00625#ffffff1000.01111110contains dynamic UI yaml definitionsnullnull[]nullnull0null5solid0nullnullnullnullfitnull0100nullnullnullnull[]nullnullnull3.62940.34634.77844.06911.81470.1732-0.03.62940.34631.81470.17321.81470.1732-0.0#FFFFFF#0000000.00.010.00625#ffffff1000.010#4d4d4dArial0.13888888888888890#4d4d4dArial0.13888888888888890#4d4d4dArial0.13888888888888890#4d4d4dArial0.13888888888888890#4d4d4dArial0.13888888888888890#4d4d4dArial0.13888888888888890#4d4d4dArial0.13888888888888890#4d4d4dArial0.13888888888888890#4d4d4dArial0.138888888888888900.00.00.0-1.00.00.0000.00.173153.62940.173151.81470.01.81470.34633.62940.34631.81470.173151.81470.173150.0#ffffff#0000000.00.010.00625#ffffff1000.01111110an application entry point. The file name is fixed. nullnull[]nullnull0null5solid0nullnullnullnullfitnull0100nullnullnullnull[]nullnullnull1.82380.36743.88811.60260.91190.1837-0.01.82380.36740.91190.18370.91190.1837-0.0#FFFFFF#0000000.00.010.00625#ffffff1000.010#4d4d4dArial0.13888888888888890#4d4d4dArial0.13888888888888890#4d4d4dArial0.13888888888888890#4d4d4dArial0.138888888888888900.00.00.0-1.00.00.0000.00.18371.82380.18370.91190.00.91190.36741.82380.36740.91190.18370.91190.18370.0#ffffff#0000000.00.010.00625#ffffff1000.01111110lists images if requirednullnull[]nullnull0null5solid0nullnullnullnullfitnull0100nullnullnullnull[]nullnullnull0.4380.4384.85266.36090.2190.219-0.0#b2b2b2#0000000.00.010.0#0000001000.01100.00.2190.4380.2190.2190.00.2190.4380.4380.4380.2190.2190.2190.2190.01nullnull[]1nullnullnull00solid0nullnullnullnull/imageProxy/cdn4.iconfinder.com/data/icons/Basic_set2_Png/64/document.pngnull100nullnullnull[]nulldocument, file, paper (64x64)nulliconfindernull0.49750.49752.66562.76160.24870.2487-0.0#b2b2b2#0000000.00.010.0#0000001000.01100.00.248750.49750.248750.248750.00.248750.49750.49750.49750.248750.248750.248750.248750.01nullnull[]1nullnullnull00solid0nullnullnullnull/imageProxy/cdn4.iconfinder.com/data/icons/Basic_set2_Png/64/document.pngnull0100nullnullnull[]nulldocument, file, paper (64x64)nulliconfindernull0.49750.49752.66564.07190.24870.2487-0.0#b2b2b2#0000000.00.010.0#0000001000.01100.00.248750.49750.248750.248750.00.248750.49750.49750.49750.248750.248750.248750.248750.01nullnull[]1nullnullnull00solid0nullnullnullnull/imageProxy/cdn4.iconfinder.com/data/icons/Basic_set2_Png/64/document.pngnull100nullnullnull[]nulldocument, file, paper (64x64)nulliconfindernull0.49750.49752.66241.54450.24870.2487-0.0#b2b2b2#0000000.00.010.0#0000001000.01100.00.248750.49750.248750.248750.00.248750.49750.49750.49750.248750.248750.248750.248750.01nullnull[]1nullnullnull00solid0nullnullnullnull/imageProxy/cdn4.iconfinder.com/data/icons/Basic_set2_Png/64/document.pngnull100nullnullnull[]nulldocument, file, paper (64x64)nulliconfindernull0.96610.96611.10819.51690.48310.4831-0.0#b2b2b2#0000000.00.010.0#0000001000.01100.00.483050.96610.483050.483050.00.483050.96610.96610.96610.483050.483050.483050.483050.01nullnull[]1nullnullnull00solid0nullnullnullnullhttps://cdn2.iconfinder.com/data/icons/Qetto___icons_by_ampeross-d4njobq/128/zip (2).pngnull100nullnullnull[]nullzip (128x128)nulliconfindernull././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/doc/source/admin/appdev-guide/garbage_collection.rst0000664000175000017500000001163600000000000025175 0ustar00zuulzuul00000000000000.. _garbage_collection: ===================================== Garbage collection system in MuranoPL ===================================== A garbage collection system (GC) manages the deallocation of resources in murano. The garbage collection system implementation is based on the execution of special ``.destroy()`` methods that you may define in MuranoPL classes. These methods contain logic to deallocate any resources that were allocated by MuranoPL objects. During deployment all objects that are not referenced by any other object and that are not present in the object model anymore is deleted by GC. * The ``.destroy()`` methods are executed for each class in the class hierarchy of the object that has this method. Child classes cannot prevent parent classes ``.destroy`` from being called and cannot call base classes implementation manually * ``.destroy()`` methods for class hierarchy are called in reversed order from that of ``.init()`` - starting from the actual object type and up to the `io.murano.Object` class * If object `Bar` is owned (directly or indirectly) by object `Foo` then `Bar` is going to be destroyed before `Foo`. There is a way for `Foo` to get notified on `Bar`'s destruction so that it can prepare for it. See below for details. * For objects that are not related to each other the destruction order is undefined. However objects may establish destruction dependency between them to establish the order. * Unrelated objects might be destroyed in different green threads. * Any exceptions thrown in the ``.destroy()`` methods are muted (but still logged). Destruction dependencies may be used to notify `Foo` of `Bar`'s destruction even if `Bar` is not owned by `Foo`. If you subscribe `Foo` to `Bar`'s destruction, the following will happen: * `Foo` will be notified when `Bar` is about to be destroyed. * If both `Foo` and `Bar` are going to be destroyed in the same garbage collection execution, `Bar` will be destroyed before `Foo`. Garbage collector methods ~~~~~~~~~~~~~~~~~~~~~~~~~ Murano garbage collector class (``io.murano.system.GC``) has the following methods: ``collect()`` Initiates garbage collection of unreferenced objects of current deployment. Usually, it is called by murano ``ObjectStore`` object during deployment. However, it can be called from MuranoPL code like ``io.murano.system.GC.collect()``. ``isDestroyed(object)`` Checks if the ``object`` was already destroyed during a GC session and thus its methods cannot be called. ``isDoomed(object)`` Can be used within the ``.destroy()`` method to check if another object is also going to be destroyed. ``subscribeDestruction(publisher, subscriber, handler=null)`` Establishes a destruction dependency from the ``subscriber`` to the object passed as ``publisher``. This method may be called several times with the same arguments. In this case, only a single destruction dependency will be established. However, the same amount of calls of ``unsubscribeDestruction`` will be required to remove it. The ``handler`` argument is optional. If passed, it should be the name of an instance method defined by the caller class to handle the notification of ``publisher`` destruction. The following argument will be passed to the ``handler`` method: ``object`` A target object that is going to be destroyed. It is not recommended persisting the reference to this object anywhere. This will not prevent the object from being garbage collected but the object will be moved to the "destroyed" state. This is an advanced feature that should not be used unless it is absolutely necessary. ``unsubscribeDestruction(publisher, subscriber, handler=null)`` Removes the destruction dependency from the ``subscriber`` to the object passed as ``publisher``. The method may be called several times with the same arguments without any side effects. If ``subscribeDestruction`` was called more than once, the same (or more) amount of calls to ``unsubscribeDestruction`` is needed to remove the dependency. The ``handler`` argument is optional and must correspond to the handler passed during subscription if it was provided. Using destruction dependencies ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ To use direct destruction dependencies in your murano applications, use the methods from MuranoPL ``io.murano.system.GC``. To establish a destruction dependency, call the ``io.murano.system.GC.subscribeDestruction`` method in you application code: .. code-block:: console .init: Body: - If: $.publisher Then: - sys:GC.subscribeDestruction($.publisher, $this, onPublisherDestruction) In the example above, ``onPublisherDestruction`` is a `Foo` object method that will be called when `Bar` is destroyed. If you do not want to do something specific with the destroyed object omit the third parameter. The destruction dependencies will be persisted between deployments and deserialized from the objects model to murano object. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/doc/source/admin/appdev-guide/hot_packages.rst0000664000175000017500000001200100000000000024005 0ustar00zuulzuul00000000000000.. _hot-packages: ============ HOT packages ============ .. _compose_package: Compose a package ~~~~~~~~~~~~~~~~~ Murano is an Application catalog which intends to support applications defined in different formats. As a first step to universality, support of a heat orchestration template was added. It means that any heat template could be added as a separate application into the Application Catalog. This could be done in two ways: manual and automatic. Automatic package composing --------------------------- Before uploading an application into the catalog, it should be prepared and archived. A Murano command line will do all preparation for you. Just choose the desired Heat Orchestration Template and perform the following command: :: murano package-create --template wordpress/template.yaml Note, that optional parameters could be specified: :--name: an application name, copied from a template by default :--logo: an application square logo, by default the heat logo will be used :--description: text information about an application, by default copied from a template :--author: a name of an application author :--output: a name of an output file archive to save locally :--full-name: a fully qualified domain name that specifies exact application location :--resources-dir: a path to the directory containing application resources .. note:: To performing this command python-muranoclient should be installed in the system As the result, an application definition archive will be ready for uploading. Manual package composing ------------------------ Application package could be composed manually. Follow the 5 steps below. * *Step 1. Choose the desired heat orchestration template* For this example `chef-server.yaml `_ template will be used. * *Step 2. Rename it to template.yaml* * *Step 3. Prepare an application logo (optional step)* It could be any picture associated with the application. * *Step 4. Create manifest.yaml file* All service information about the application is contained here. Specify the following parameters: :Format: defines an application definition format; should be set to ``Heat.HOT/1.0`` :Type: defines a manifest type, should be set to ``Application`` :FullName: a unique name which will be used to identify the application in Murano Catalog :Description: text information about an application :Author: a name of an application author or a company :Tags: keywords associated with the application :Logo: a name of a logo file for an application Take a look at the example: .. code-block:: yaml Format: Heat.HOT/1.0 Type: Application FullName: com.example.Chef-Server Name: Chef Server Description: "Heat template to deploy Open Source CHEF server on a VM" Author: Kate Tags: - hot-based Logo: logo.png * *Step 5. Create a zip archive, containing the specified files:* ``template.yaml``, ``manifest.yaml``, ``logo.png`` `Browse` page looks like: .. image:: figures/chef_server.png The configuration form, where you can enter template parameters, will be generated automatically and looks as follows: .. image:: figures/chef_server_form.png After filling the form the application is ready to be deployed. Hot packages with nested Heat templates --------------------------------------- In Murano HOT packages it is possible to allow Heat nested templates to be saved and deployed as part of a Murano Heat applications. Such templates should be placed in package under '/Resources/HotFiles'. Adding additional templates to a package is optional. When a Heat generated package is being deployed, if there are any Heat nested templates located in the package under '/Resources/HotFiles', they are sent to Heat together with the main template and params during stack creation. These nested templates can be referenced by putting the template name into the ``type`` attribute of resource definition, in the main template. This mechanism then compose one logical stack with these multiple templates. The following examples illustrate how you can use a custom template to define new types of resources. These examples use a custom template stored in a ``sub_template.yaml`` file .. code-block:: yaml heat_template_version: 2015-04-30 parameters: key_name: type: string description: Name of a KeyPair resources: server: type: OS::Nova::Server properties: key_name: {get_param: key_name} flavor: m1.small image: ubuntu-trusty Use the template filename as type --------------------------------- The following main template defines the ``sub_template.yaml`` file as value for the type property of a resource .. code-block:: yaml heat_template_version: 2015-04-30 resources: my_server: type: sub_template.yaml properties: key_name: my_key .. note:: This feature is supported Liberty onwards.././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/doc/source/admin/appdev-guide/multi_region.rst0000664000175000017500000001125700000000000024066 0ustar00zuulzuul00000000000000.. _multi_region: Multi-region application ~~~~~~~~~~~~~~~~~~~~~~~~ Since Newton release, Murano supports multi-region application deployment. All MuranoPL resource classes are inherited from the ``io.murano.CloudResource`` class. An application developer can set a custom region for ``CloudResource`` subclasses deployment. Set a region for resources -------------------------- **To set a region for resources:** #. Specify a region for ``CloudResource`` subclasses deployment through the ``regionName`` property. For example: .. code-block:: yaml Application: ?: type: com.example.apache.ApacheHttpServer enablePHP: $.appConfiguration.enablePHP ... instance: ?: type: io.murano.resources.LinuxMuranoInstance regionName: 'CustomRegion' ... #. Retrieve ``io.murano.CloudRegion`` objects: .. code-block:: yaml $region: $.instance.getRegion() $regionName: $region.name $regionLocalStack: $region.stack $regionDefaultNetworks: $region.defaultNetworks As a result, all region-local properties are moved from the ``io.murano.Environment`` class to the new :ref:`cloud-region` class. For backward compatibility, the ``io.murano.Environment`` class stores region-specific properties of default region, except the ``defaultNetworks`` in its own properties. The ``Environment::defaultNetworks`` property contains templates for the ``CloudRegion::defaultNetworks`` property. Through current UI, you cannot select networks, flavor, images and availability zone from a non-default region. We suggest using regular text fields to specify region-local resources. Networking and multi-region applications ---------------------------------------- By default, each region has its own separate network. To ensure connectivity between the networks, create and configure networks in regions before deploying the application and use ``io.murano.resources.ExistingNeutronNetwork`` to connect the instance to an existing network. Example: .. code-block:: yaml Application: ?: type: application.fully.qualified.Name ... instance_in_region1: ?: type: io.murano.resources.LinuxMuranoInstance regionName: 'CustomRegion1' networks: useEnvironmentNetwork: false useFlatNetwork: false customNetworks: - ?: type: io.murano.resources.ExistingNeutronNetwork regionName: 'CustomRegion1' internalNetworkName: 'internalNetworkNameInCustomRegion1' internalSubnetworkName: 'internalSubNetNameInCustomRegion1' instance_in_region2: ?: type: io.murano.resources.LinuxMuranoInstance regionName: 'CustomRegion2' networks: useEnvironmentNetwork: false useFlatNetwork: false customNetworks: - ?: type: io.murano.resources.ExistingNeutronNetwork regionName: 'CustomRegion2' internalNetworkName: 'internalNetworkNameInCustomRegion2' internalSubnetworkName: 'internalSubNetNameInCustomRegion2' ... Also, you can configure networks with the same name and use a template for the region networks. That is, describe ``io.murano.resources.ExistingNeutronNetwork`` only once and assign it to the ``Environment::defaultNetworks::environment`` property. The environment will create ``Network`` objects for regions from the ``ExistingNeutronNetwork`` template. Example: .. code-block:: console OS_REGION_NAME="RegionOne" openstack network create OS_REGION_NAME="RegionTwo" openstack network create # configure subnets #... # add ExistingNeutronNetwork to environment object model murano environment-create --join-net-id # also it is possible to specify subnet from murano environment-create --join-net-id --join-subnet-id Additionally, consider the ``[networking]`` section in the configuration file. Currently, ``[networking]`` settings are common for all regions. .. code-block:: ini [networking] external_network = %EXTERNAL_NETWORK_NAME% router_name = %MURANO_ROUTER_NAME% create_router = true If you choose an automatic neutron configuration, configure the external network with identical names in all regions. If you disable the automatic router creation, create routers with identical names in all regions. Also, the ``default_dns`` address must be reachable from all created networks. .. note:: To use regions, first configure them as described in :ref:`multi-region`. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/doc/source/admin/appdev-guide/murano_bundles.rst0000664000175000017500000000406500000000000024405 0ustar00zuulzuul00000000000000.. _murano-bundles: ============== Murano bundles ============== A bundle is a collection of packages. In the Community App Catalog, you can find such bundles as ``container-based-apps``, ``app-servers``, and so on. The packages in the Application Catalog are sorted by usage. You can import bundles from the catalog using Dashboard or CLI. You can read about this in :ref:`Managing applications ` and :ref:`Using CLI `. Specific information about *bundle-import* command can be found at :ref:`Murano command-line client `. Bundle structure ~~~~~~~~~~~~~~~~ Bundle description is a JSON structure, that contains list of packages in the bundle and bundle version. Here is the example: .. code-block:: javascript { "Packages": [ { "Name": "com.example.apache.ApacheHttpServer", "Version": "" }, { "Name": "com.example.apache.Tomcat", "Version": "" } ], "Version": 1 } .. ``Name`` is a required parameter and should contain package fully qualified name. ``Version`` is not a mandatory parameter. Version for package entry specifies the version of the package to look into :ref:`Murano package repository `. If it is specified, murano client would look for a file with that version specification in murano repository (for example ``com.example.MyApp.0.0.1.zip`` for com.example.MyApp of version 0.0.1). If the version is omitted or left blank client would search for ``com.example.MyApp.zip``. Create local bundle ~~~~~~~~~~~~~~~~~~~ However, you may need to create a local bundle. You may need it if you want to setup your own :ref:`Murano package repository `. To create a new bundle, perform the following steps: #. Navigate to the directory with the target packages. #. Create a ``.bundle`` file. List all the required packages in ``Packages`` section. If needed, specify the bundle version in the ``Version`` section. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/doc/source/admin/appdev-guide/murano_packages.rst0000664000175000017500000000030300000000000024516 0ustar00zuulzuul00000000000000.. _murano-packages: =============== Murano packages =============== .. toctree:: :maxdepth: 1 muranopackages/package_structure muranopackages/dynamic_ui muranopackages/repository ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1696417899.7211804 murano-16.0.0/doc/source/admin/appdev-guide/murano_pl/0000775000175000017500000000000000000000000022625 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/doc/source/admin/appdev-guide/murano_pl/actions.rst0000664000175000017500000000676000000000000025030 0ustar00zuulzuul00000000000000.. _actions: ============== Murano actions ============== Murano action is a type of MuranoPL method. The differences from a regular MuranoPL method are: * Action is executed on deployed objects. * Action execution is initiated by API request, you do not have to call the method manually. So murano action allows performing any operations on objects: * Getting information from the VM, like a config that is generated during the deployment * VM rebooting * Scaling A list of available actions is formed during the environment deployment. Right after the deployment is finished, you can call action asynchronously. Murano engine generates a task for every action. Therefore, the action status can be tracked. .. note:: Actions may be called against any MuranoPL object, including ``Environment``, ``Application``, and any other objects. .. note:: Now murano doesn't support big files download during action execution. This is because action results are stored in murano database and are limited by approximately 10kb size. To mark a method as an action, use ``Scope: Public`` or ``Usage: Action``. The latter option is deprecated for the package format versions > 1.3 and occasionally will be no longer supported. Also, you cannot use both ``Usage: Action`` and ``Scope: Session`` in one method. The following example shows an action that returns an archive with a configuration file: .. code-block:: yaml exportConfig: Scope: Public Body: - $._environment.reporter.report($this, 'Action exportConfig called') - $resources: new(sys:Resources) - $template: $resources.yaml('ExportConfig.template') - $result: $.masterNode.instance.agent.call($template, $resources) - $._environment.reporter.report($this, 'Got archive from Kubernetes') - Return: new(std:File, base64Content => $result.content, filename => 'application.tar.gz') List of available actions can be found with environment details or application details API calls. It's located in object model special data. Take a look at the following example: Request: ``http://localhost:8082/v1/environments//services/`` Response: .. code-block:: json { "name": "SimpleVM", "?": { "_26411a1861294160833743e45d0eaad9": { "name": "SimpleApp" }, "type": "com.example.Simple", "id": "e34c317a-f5ee-4f3d-ad2f-d07421b13d67", "_actions": { "e34c317a-f5ee-4f3d-ad2f-d07421b13d67_exportConfig": { "enabled": true, "name": "exportConfig" } } } } ============== Static actions ============== Static methods (:ref:`static_methods_and_properties`) can also be called through the API if they are exposed by specifying ``Scope: Public``, and the result of its execution will be returned. Consider the following example of the static action that makes use both of static class property and user's input as an argument: .. code-block:: yaml Name: Bar Properties: greeting: Usage: Static Contract: $.string() Default: 'Hello, ' Methods: staticAction: Scope: Public Usage: Static Arguments: - myName: Contract: $.string().notNull() Body: - Return: concat($.greeting, $myName) Request: ``http://localhost:8082/v1/actions`` Request body: .. code-block:: json { "className": "ns.Bar", "methodName": "staticAction", "parameters": {"myName": "John"} } Responce: .. code-block:: json "Hello, John" ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/doc/source/admin/appdev-guide/murano_pl/class_templ.rst0000664000175000017500000011147100000000000025672 0ustar00zuulzuul00000000000000.. _class_templ: Common class structure ~~~~~~~~~~~~~~~~~~~~~~ Here is a common template for class declarations. Note, that it is in the YAML format. .. code-block:: yaml :linenos: Name: class name Namespaces: namespaces specification Extends: [list of parent classes] Properties: properties declaration Methods: methodName: Arguments: - list - of - arguments Body: - list - of - instructions Thus MuranoPL class is a YAML dictionary with predefined key names, all keys except for ``Name`` are optional and can be omitted (but must be valid if specified). Class name ---------- Class names are alphanumeric names of the classes. Traditionally, all class names begin with an upper-case letter symbol and are written in PascalCasing. In MuranoPL all class names are unique. At the same time, MuranoPL supports namespaces. So, in different namespaces you can have classes with the same name. You can specify a namespace explicitly, like `ns:MyName`. If you omit the namespace specification, ``MyName`` is expanded using the default namespace ``=:``. Therefore, ``MyName`` equals ``=:MyName`` if ``=`` is a valid namespace. Namespaces ---------- Namespaces declaration specifies prefixes that can be used in the class body to make long class names shorter. .. code-block:: yaml Namespaces: =: io.murano.services.windows srv: io.murano.services std: io.murano In the example above, the ``srv: Something`` class name is automatically translated to ``io.murano.services.Something``. ``=`` means the current namespace, so that ``MyClass`` means ``io.murano.services.windows.MyClass``. If the class name contains the period (.) in its name, then it is assumed to be already fully namespace qualified and is not expanded. Thus ``ns.Myclass`` remains as is. .. note:: To make class names globally unique, we recommend specifying a developer's domain name as a part of the namespace. Extends ------- MuranoPL supports multiple inheritance. If present, the ``Extends`` section shows base classes that are extended. If the list consists of a single entry, then you can write it as a scalar string instead of an array. If you do not specify any parents or omit the key, then the class extends ``io.murano.Object``. Thus, ``io.murano.Object`` is the root class for all class hierarchies. .. _class_props: Properties ---------- Properties are class attributes that together with methods create public class interface. Usually, but not always, properties are the values, and reference other objects that have to be entered in an environment designer prior to a workflow invocation. Properties have the following declaration format: .. code-block:: yaml propertyName: Contract: property contract Usage: property usage Default: property default Contract ++++++++ Contract is a YAQL expression that says what type of the value is expected for the property as well as additional constraints imposed on a property. Using contracts you can define what value can be assigned to a property or argument. In case of invalid input data it may be automatically transformed to confirm to the contract. For example, if bool value is expected and user passes any not null value it will be converted to ``True``. If converting is impossible exception ``ContractViolationException`` will be raised. The following contracts are available: +-----------------------------------------------------------+-------------------------------------------------------------------------------------------------+ | Operation | Definition | +===========================================================+=================================================================================================+ | | $.int() | | an integer value (may be null). String values consisting of digits are converted to integers | +-----------------------------------------------------------+-------------------------------------------------------------------------------------------------+ | | $.int().notNull() | | a mandatory integer | +-----------------------------------------------------------+-------------------------------------------------------------------------------------------------+ | | $.string() | | a string. If the value is not a string, it is converted to a string | | | $.string().notNull() | | +-----------------------------------------------------------+-------------------------------------------------------------------------------------------------+ | | $.bool() | | bools are true and false. ``0`` is converted to false, other integers to true | | | $.bool().notNull() | | +-----------------------------------------------------------+-------------------------------------------------------------------------------------------------+ | | $.class(ns:ClassName) | | value must be a reference to an instance of specified class name | | | $.class(ns:ClassName).notNull() | | +-----------------------------------------------------------+-------------------------------------------------------------------------------------------------+ | | $.template(ns:ClassName) | | value must be a dictionary with object-model representation of specified class name | | | $.template(ns:ClassName).notNull() | | +-----------------------------------------------------------+-------------------------------------------------------------------------------------------------+ | | $.class(ns:ClassName, ns:DefaultClassName) | | create instance of the ``ns:DefaultClassName`` class if no instance provided | +-----------------------------------------------------------+-------------------------------------------------------------------------------------------------+ | | $.class(ns:Name).check($.p = 12) | | the value must be of the ``ns:Name`` type and have the ``p`` property equal to 12 | +-----------------------------------------------------------+-------------------------------------------------------------------------------------------------+ | | $.class(ns:Name).owned() | | a current object must be direct or indirect owner of the value | +-----------------------------------------------------------+-------------------------------------------------------------------------------------------------+ | | $.class(ns:Name).notOwned() | | the value must be owned by any object except current one | +-----------------------------------------------------------+-------------------------------------------------------------------------------------------------+ | | [$.int()] | | an array of integers. Similar to other types. | | | [$.int().notNull()] | | +-----------------------------------------------------------+-------------------------------------------------------------------------------------------------+ | | [$.int().check($ > 0)] | | an array of the positive integers (thus not null) | +-----------------------------------------------------------+-------------------------------------------------------------------------------------------------+ | | [$.int(), $.string()] | | an array that has at least two elements, first is int and others are strings | +-----------------------------------------------------------+-------------------------------------------------------------------------------------------------+ | | [$.int(), 2] | | an array of ints with at least 2 items | | | [$.int(), 2, 5] | | an array of ints with at least 2 items, and maximum of 5 items | +-----------------------------------------------------------+-------------------------------------------------------------------------------------------------+ | | { A: $.int(), B: [$.string()] } | | the dictionary with the ``A`` key of the int type and ``B`` - an array of strings | +-----------------------------------------------------------+-------------------------------------------------------------------------------------------------+ | | $ | | any scalar or data structure as is | | | [] | | any array | | | {} | | any dictionary | +-----------------------------------------------------------+-------------------------------------------------------------------------------------------------+ | | { $.string().notNull(): $.int().notNull() } | | dictionary string -> int | +-----------------------------------------------------------+-------------------------------------------------------------------------------------------------+ | | A: StringMap | | the dictionary with the ``A`` key that must be equal to ``StringMap``, and other keys be | | | $.string().notNull(): $ | | any scalar or data structure | +-----------------------------------------------------------+-------------------------------------------------------------------------------------------------+ | | $.check($ in $this.myStaticMethod()) | | the value must be equal to one of a member of a list returned by static method of the class | +-----------------------------------------------------------+-------------------------------------------------------------------------------------------------+ | | $.check($this.myStaticMethod($)) | | the static method of the class must return true for the value | +-----------------------------------------------------------+-------------------------------------------------------------------------------------------------+ In the example below property ``port`` must be int value greater than 0 and less than 65536; ``scope`` must be a string value and one of 'public', 'cloud', 'host' or 'internal', and ``protocol`` must be a string value and either 'TCP' or 'UDP'. When user passes some values to these properties it will be checked that values confirm to the contracts. .. code-block:: yaml Namespaces: =: io.murano.apps.docker std: io.murano Name: ApplicationPort Properties: port: Contract: $.int().notNull().check($ > 0 and $ < 65536) scope: Contract: $.string().notNull().check($ in list(public, cloud, host, internal)) Default: private protocol: Contract: $.string().notNull().check($ in list(TCP, UDP)) Default: TCP Methods: getRepresentation: Body: Return: port: $.port scope: $.scope protocol: $.protocol The ``template`` contract does the same validation as the ``class`` contract, but does not require the actual object to be passed as a property or argument. Instead it allows to create an object from the given template later. Also you can exclude some of the properties from validation and provide them later in the body of the method. Consider the following example: .. code-block:: yaml Namespaces: =: io.murano.applications res: io.murano.resources std: io.murano Name: TemplateServerProvider Properties: template: Contract: $.template(res:Instance, excludeProperties => [name]).notNull() serverNamePattern: Contract: $.string().notNull() threshold: Contract: $.int().check($ > 0) Methods: createReplica: Arguments: - index: Contract: $.int().notNull() - owner: Contract: $.class(std:Object) Body: - If: $index < $this.threshold Then: - $template: $this.template - $template.name: $this.serverNamePattern.format($index) - $template['?'].name: format('Server {0}', $index) - Return: new($template, $owner) Else: - Return: null In the example above the class has the ``template`` property that is validated by the ``template`` contract. It holds the template of the object of the ``Instance`` class or its inheritor. In the ``createReplica`` method ``template`` is used to dynamically create instances in runtime considering some conditions and customizing the ``name`` property of an instance, as it was excluded from validation. You still can pass an actual object to the property or argument with the ``template`` contract, but it will be automatically converted to its object model representation. .. _property_usage: Property usage ++++++++++++++ Usage states the purpose of the property. This implies who and how can access it. The following usages are available: .. list-table:: :header-rows: 1 :widths: 20 80 :stub-columns: 0 :class: borderless * - | Value - | Explanation * - | In - | Input property. Values of such properties are obtained from a user and cannot be modified in MuranoPL workflows. This is the default value for the Usage key. * - | Out - | A value is obtained from executing MuranoPL workflow and cannot be modified by a user. * - | InOut - | A value can be modified both by user and by workflow. * - | Const - | The same as ``In`` but once workflow is executed a property cannot be changed neither by a user nor by a workflow. * - | Runtime - | A property is visible only from within workflows. It is neither read from input nor serialized to a workflow output. * - | Static - | Property is defined on a class rather than on an instance. See :ref:`static_methods_and_properties` for details. * - | Config - | A property allows to have per-class configuration. A value is obtained from the config file rather than from the object model. These config files are stored in a special folder that is configured in the ``[engine]`` section of the Murano config file under the ``class_configs`` key. The usage attribute is optional and can be omitted (which implies ``In``). If the workflow tries to write to a property that is not declared with one of the types above, it is considered to be private and accessible only to that class (and not serialized to output and thus would be lost upon the next deployment). An attempt to read the property that was not initialized results in an exception. Default +++++++ Default is a value that is used if the property value is not mentioned in the input object model, but not when it is set to null. Default, if specified, must conform to a declared property contract. If Default is not specified, then null is the default. For properties that are references to other classes, Default can modify a default value of the referenced objects. For example: .. code-block:: yaml p: Contract: $.class(MyClass) Default: {a: 12} This overrides default for the ``a`` property of ``MyClass`` for instance of ``MyClass`` that is created for this property. Workflow -------- Workflows are the methods that describe how the entities that are represented by MuranoPL classes are deployed. In a typical scenario, the root object in an input data model is of the ``io.murano.Environment`` type, and has the ``deploy`` method. This method invocation causes a series of infrastructure activities (typically, a Heat stack modification) and the deployment scripts execution initiated by VM agents commands. The role of the workflow is to map data from the input object model, or a result of previously executed actions, to the parameters of these activities and to initiate these activities in a correct order. Methods ------- Methods have input parameters, and can return a value to a caller. Methods are defined in the Workflow section of the class using the following template:: methodName: Scope: Public Arguments: - list - of - arguments Body: - list - of - instructions Public is an optional parameter that specifies methods to be executed by direct triggering after deployment. .. _method_arguments: Method arguments ++++++++++++++++ Arguments are optional too, and are declared using the same syntax as class properties. Same as properties, arguments also have contracts and optional defaults. Unlike class properties Arguments may have a different set of Usages: .. list-table:: :header-rows: 1 :widths: 20 80 :stub-columns: 0 :class: borderless * - | Value - | Explanation * - | Standard - | Regular method argument. Holds a single value based on its contract. This is the default value for the Usage key. * - | VarArgs - | A variable length argument. Method body sees it as a list of values, each matching a contract of the argument. * - | KwArgs - | A keywrod-based argument, Method body sees it as a dict of values, with keys being valid keyword strings and values matching a contract of the argument. Arguments example: .. code-block:: yaml scaleRc: Arguments: - rcName: Contract: $.string().notNull() - newSize: Contract: $.int().notNull() - rest: Contract: $.int() Usage: VarArgs - others: Contract: $.int() Usage: KwArgs .. method_body: Method body +++++++++++ The Method body is an array of instructions that get executed sequentially. There are 3 types of instructions that can be found in a workflow body: * Expressions, * Assignments, * Block constructs. .. method_usage: Method usage ++++++++++++ Usage states the purpose of the method. This implies who and how can access it. The following usages are available: .. list-table:: :header-rows: 1 :widths: 20 80 :stub-columns: 0 :class: borderless * - | Value - | Explanation * - | Runtime - | Normal instance method. * - | Static - | Static method that does not require class instance. See :ref:`static_methods_and_properties` for details. * - | Extension - | Extension static method that extends some other type. See :ref:`extension_methods` for details. * - | Action - | Method can be invoked from outside (using Murano API). This option is deprecated for the package format versions > 1.3 in favor of ``Scope: Public`` and occasionally will be no longer supported. See :ref:`actions` for details. The ``Usage`` attribute is optional and can be omitted (which implies ``Runtime``). Method scope ++++++++++++ The ``Scope`` attribute declares method visibility. It can have two possible values: * `Session` - regular method that is accessible from anywhere in the current execution session. This is the default if the attribute is omitted; * `Public` - accessible anywhere, both within the session and from outside through the API call. The ``Scope`` attribute is optional and can be omitted (which implies ``Session``). Expressions +++++++++++ Expressions are YAQL expressions that are executed for their side effect. All accessible object methods can be called in the expression using the ``$obj.methodName(arguments)`` syntax. +-----------------------------------------+----------------------------------------------------------------+ | Expression | Explanation | +=========================================+================================================================+ | | $.methodName() | | invoke method 'methodName' on this (self) object | | | $this.methodName() | | +-----------------------------------------+----------------------------------------------------------------+ | | $.property.methodName() | | invocation of method on object that is in ``property`` | | | $this.property.methodName() | | +-----------------------------------------+----------------------------------------------------------------+ | | $.method(1, 2, 3) | | methods can have arguments | +-----------------------------------------+----------------------------------------------------------------+ | | $.method(1, 2, thirdParameter => 3) | | named parameters also supported | +-----------------------------------------+----------------------------------------------------------------+ | | list($.foo().bar($this.property), $p) | | complex expressions can be constructed | +-----------------------------------------+----------------------------------------------------------------+ Assignment ++++++++++ Assignments are single key dictionaries with a YAQL expression as a key and arbitrary structure as a value. Such a construct is evaluated as an assignment. +------------------------------+---------------------------------------------------------------------------------+ | Assignment | Explanation | +==============================+=================================================================================+ | | $x: value | | assigns ``value`` to the local variable ``$x`` | +------------------------------+---------------------------------------------------------------------------------+ | | $.x: value | | assign ``value`` to the object's property | | | $this.x: value | | +------------------------------+---------------------------------------------------------------------------------+ | | $.x: $.y | | copies the value of the property ``y`` to the property ``x`` | +------------------------------+---------------------------------------------------------------------------------+ | | $x: [$a, $b] | | sets ``$x`` to the array of two values: ``$a`` and ``$b`` | +------------------------------+---------------------------------------------------------------------------------+ | | $x: | | structures of any level of complexity can be evaluated | | | SomeKey: | | | | NestedKey: $variable | | +------------------------------+---------------------------------------------------------------------------------+ | | $.x[0]: value | | assigns ``value`` to the first array entry of the ``x`` property | +------------------------------+---------------------------------------------------------------------------------+ | | $.x: $.x.append(value) | | appends ``value`` to the array in the ``x`` property | +------------------------------+---------------------------------------------------------------------------------+ | | $.x: $.x.insert(1, value) | | inserts ``value`` into position 1 of the array in the ``x`` property | +------------------------------+---------------------------------------------------------------------------------+ | | $x: list($a, $b).delete(0) | | sets ``$x`` to the list without the item at index 0 | +------------------------------+---------------------------------------------------------------------------------+ | | $.x.key.subKey: value | | deep dictionary modification | | | $.x[key][subKey]: value | | +------------------------------+---------------------------------------------------------------------------------+ Block constructs ++++++++++++++++ Block constructs control a program flow. They are dictionaries that have strings as all their keys. The following block constructs are available: +---------------------------+---------------------------------------------------------------------------------------+ | Assignment | Explanation | +===========================+=======================================================================================+ | | Return: value | | Returns value from a method | +---------------------------+---------------------------------------------------------------------------------------+ | | If: predicate() | | ``predicate()`` is a YAQL expression that must be evaluated to ``True`` or ``False``| | | Then: | | | | - code | | The ``Else`` section is optional | | | - block | | One-line code blocks can be written as scalars rather than an array. | | | Else: | | | | - code | | | | - block | | +---------------------------+---------------------------------------------------------------------------------------+ | | While: predicate() | | ``predicate()`` must be evaluated to ``True`` or ``False`` | | | Do: | | | | - code | | | | - block | | +---------------------------+---------------------------------------------------------------------------------------+ | | For: variableName | | ``collection`` must be a YAQL expression returning iterable collection or | | | In: collection | evaluatable array as in assignment instructions, for example, ``[1, 2, $x]`` | | | Do: | | | | - code | | Inside a code block loop, a variable is accessible as ``$variableName`` | | | - block | | +---------------------------+---------------------------------------------------------------------------------------+ | | Repeat: | | Repeats the code block specified number of times | | | Do: | | | | - code | | | | - block | | +---------------------------+---------------------------------------------------------------------------------------+ | | Break: | | Breaks from loop | +---------------------------+---------------------------------------------------------------------------------------+ | | Match: | | Matches the result of ``$valExpression()`` against a set of possible values | | | case1: | (cases). The code block of first matched case is executed. | | | - code | | | | - block | | If no case matched and the default key is present | | | case2: | than the ``Default`` code block get executed. | | | - code | | The case values are constant values (not expressions). | | | - block | | | | Value: $valExpression() | | | | Default: | | | | - code | | | | - block | | +---------------------------+---------------------------------------------------------------------------------------+ | | Switch: | | All code blocks that have their predicate evaluated to ``True`` are executed, | | | $predicate1(): | but the order of predicate evaluation is not fixed. | | | - code | | | | - block | | | | $predicate2(): | | | | - code | | | | - block | | | | Default: | | The ``Default`` key is optional. | | | - code | | | | - block | | If no predicate evaluated to ``True``, the ``Default`` code block get executed. | +---------------------------+---------------------------------------------------------------------------------------+ | | Parallel: | | Executes all instructions in code block in a separate green threads in parallel. | | | - code | | | | - block | | | | Limit: 5 | | The limit is optional and means the maximum number of concurrent green threads. | +---------------------------+---------------------------------------------------------------------------------------+ | | Try: | | Try and Catch are keywords that represent the handling of exceptions due to data | | | - code | or coding errors during program execution. A ``Try`` block is the block of code in | | | - block | which exceptions occur. A ``Catch`` block is the block of code, that is executed if | | | Catch: | an exception occurred. | | | With: keyError | | Exceptions are not declared in Murano PL. It means that exceptions of any types can | | | As: e | be handled and generated. Generating of exception can be done with construct: | | | Do: | ``Throw: keyError``. | | | - code | | | | - block | | | | Else: | | The ``Else`` is optional block. ``Else`` block is executed if no exception occurred.| | | - code | | | | - block | | | | Finally: | | The ``Finally`` also is optional. It's a place to put any code that will | | | - code | be executed, whether the try-block raised an exception or not. | | | - block | | +---------------------------+---------------------------------------------------------------------------------------+ Notice, that if you have more than one block construct in your workflow, you need to insert dashes before each construct. For example:: Body: - If: predicate1() Then: - code - block - While: predicate2() Do: - code - block .. _object-model: Object model ------------ Object model is a JSON serialized representation of objects and their properties. Everything you do in the OpenStack dashboard is reflected in an object model. The object model is sent to the Application catalog engine when the user decides to deploy the built environment. On the engine side, MuranoPL objects are constructed and initialized from the received Object model, and a predefined method is executed on the root object. Objects are serialized to JSON using the following template: .. code-block:: json :linenos: { "?": { "id": "globally unique object ID (UUID)", "type": "fully namespace-qualified class name", "optional designer-related entries can be placed here": { "key": "value" } }, "classProperty1": "propertyValue", "classProperty2": 123, "classProperty3": ["value1", "value2"], "reference1": { "?": { "id": "object id", "type": "object type" }, "property": "value" }, "reference2": "referenced object id" } Objects can be identified as dictionaries that contain the ``?`` entry. All system fields are hidden in that entry. There are two ways to specify references: #. ``reference1`` as in the example above. This method allows inline definition of an object. When the instance of the referenced object is created, an outer object becomes its parent/owner that is responsible for the object. The object itself may require that its parent (direct or indirect) be of a specified type, like all applications require to have ``Environment`` somewhere in a parent chain. #. Referring to an object by specifying other object ID. That object must be defined elsewhere in an object tree. Object references distinguished from strings having the same value by evaluating property contracts. The former case would have ``$.class(Name)`` while the later - the ``$.string()`` contract. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/doc/source/admin/appdev-guide/murano_pl/core_lib.rst0000664000175000017500000002646600000000000025153 0ustar00zuulzuul00000000000000.. _core_lib: MuranoPL Core Library ~~~~~~~~~~~~~~~~~~~~~ Some objects and actions can be used in several application deployments. All common parts are grouped into MuranoPL libraries. Murano core library is a set of classes needed in each deployment. Class names from core library can be used in the application definitions. This library is located under the `meta `_ directory. Classes included in the Murano core library are as follows: **io.murano** - :ref:`object` - :ref:`application` - :ref:`security-group-manager` - :ref:`environment` - :ref:`cloud-region` **io.murano.resources** - :ref:`instance` - :ref:`network` **io.murano.system** - :ref:`logger` - :ref:`status-reporter` .. _object: Class: Object ------------- A parent class for all MuranoPL classes. It implements the ``initialize``, ``setAttr``, and ``getAttr`` methods defined in the pythonic part of the Object class. All MuranoPL classes are implicitly inherited from this class. .. seealso:: Source `Object.yaml `_ file. .. _application: Class: Application ------------------ Defines an application itself. All custom applications must be derived from this class. .. seealso:: Source `Application.yaml `_ file. .. _security-group-manager: Class: SecurityGroupManager --------------------------- Manages security groups during an application deployment. .. seealso:: Source `SecurityGroupManager.yaml `_ file. .. _cloud-region: Class: CloudRegion ------------------ Defines a CloudRegion and groups region-local properties .. list-table:: **CloudRegion class properties** :widths: 10 35 7 :header-rows: 1 * - Property - Description - Default usage * - ``name`` - A region name. - ``In`` * - ``agentListener`` - A property containing the ``io.murano.system.AgentListener`` object that can be used to interact with Murano Agent. - ``Runtime`` * - ``stack`` - A property containing a HeatStack object that can be used to interact with Heat. - ``Runtime`` * - ``defaultNetworks`` - A property containing user-defined Networks (``io.murano.resources.Network``) that can be used as default networks for the instances in this environment. - ``In`` * - ``securityGroupManager`` - A property containing the ``SecurityGroupManager`` object that can be used to construct a security group associated with this environment. - ``Runtime`` .. seealso:: Source `CloudRegion.yaml `_ file. .. _environment: Class: Environment ------------------ Defines an environment in terms of the deployment process and groups all Applications and their related infrastructures. It also able to deploy them at once. Environments is intent to group applications to manage them easily. .. list-table:: **Environment class properties** :widths: 10 35 7 :header-rows: 1 * - Property - Description - Default usage * - ``name`` - An environment name. - ``In`` * - ``applications`` - A list of applications belonging to an environment. - ``In`` * - ``agentListener`` - A property containing the ``io.murano.system.AgentListener`` object that can be used to interact with Murano Agent. - ``Runtime`` * - ``stack`` - A property containing a HeatStack object in default region that can be used to interact with Heat. - ``Runtime`` * - ``instanceNotifier`` - A property containing the ``io.murano.system.InstanceNotifier`` object that can be used to keep track of the amount of deployed instances. - ``Runtime`` * - ``defaultNetworks`` - A property containing templates for user-defined Networks in regions (``io.murano.resources.Network``). - ``In`` * - ``securityGroupManager`` - A property containing the ``SecurityGroupManager`` object from default region that can be used to construct a security group associated with this environment. - ``Runtime`` * - ``homeRegionName`` - A property containing the name of home region from `murano` config - ``Runtime`` * - ``regions`` - A property containing the map `regionName` -> `CloudRegion` instance. - ``InOut`` * - ``regionConfigs`` - A property containing the map `regionName` -> `CloudRegion` config - ``Config`` .. seealso:: Source `Environment.yaml `_ file. .. _instance: Class: Instance --------------- Defines virtual machine parameters and manages an instance lifecycle: spawning, deploying, joining to the network, applying security group, and deleting. .. list-table:: **Instance class properties** :widths: 10 35 7 :header-rows: 1 * - Property - Description - Default usage * - ``regionName`` - Inherited from ``CloudResource``. Describe region for instance deployment - ``In`` * - ``name`` - An instance name. - ``In`` * - ``flavor`` - An instance flavor defining virtual machine hardware parameters. - ``In`` * - ``image`` - An instance image defining operation system. - ``In`` * - ``keyname`` - Optional. A key pair name used to connect easily to the instance. - ``In`` * - ``agent`` - Configures interaction with the Murano agent using ``io.murano.system.Agent``. - ``Runtime`` * - ``ipAddresses`` - A list of all IP addresses assigned to an instance. Floating ip address is placed in the list tail if present. - ``Out`` * - ``networks`` - Specifies the networks that an instance will be joined to. Custom networks that extend :ref:`Network class ` can be specified. An instance will be connected to them and for the default environment network or flat network if corresponding values are set to ``True``. Without additional configuration, instance will be joined to the default network that is set in the current environment. - ``In`` * - ``volumes`` - Specifies the mapping of a mounting path to volume implementations that must be attached to the instance. Custom volumes that extend ``Volume`` class can be specified. - ``In`` * - ``blockDevices`` - Specifies the list of block device mappings that an instance will use to boot from. Each mapping defines a volume that must be an instance of ``Volume`` class, device name, device type, and boot order. Either the ``blockDevices`` property or ``image`` property must be specified in order to boot an instance - ``In`` * - ``assignFloatingIp`` - Determines if floating IP is required. Default is ``False``. - ``In`` * - ``floatingIpAddress`` - IP addresses assigned to an instance after an application deployment. - ``Out`` * - ``securityGroupName`` - Optional. A security group that an instance will be joined to. - ``In`` .. seealso:: Source `Instance.yaml `_ file. .. _instance-resources: Resources +++++++++ Instance class uses the following resources: **Agent-v2.template** Python Murano Agent template. .. note:: This agent is supposed to be unified. Currently, only Linux-based machines are supported. Windows support will be added later. **linux-init.sh** Python Murano Agent initialization script that sets up an agent with valid information containing an updated agent template. **Agent-v1.template** Windows Murano Agent template. **windows-init.sh** Windows Murano Agent initialization script. .. _network: Class: Network -------------- The basic abstract class for all MuranoPL classes representing networks. .. seealso:: Source `Network.yaml `_ file. .. _logger: Class: Logger ------------- Logging API is the part of core library since Liberty release. It was introduced to improve debuggability of MuranoPL programs. You can get a logger instance by calling a ``logger`` function which is located in ``io.murano.system`` namespace. The ``logger`` function takes a logger name as the only parameter. It is a common recommendation to use full class name as a logger name within that class. This convention avoids names conflicts in logs and ensures a better logging subsystem configurability. Logger class instantiation: .. code-block:: yaml $log: logger('io.murano.apps.activeDirectory.ActiveDirectory') .. list-table:: **Log levels prioritized in order of severity** :widths: 10 35 :header-rows: 1 * - Level - Description * - CRITICAL - Very severe error events that will presumably lead the application to abort. * - ERROR - Error events that might not prevent the application from running. * - WARNING - Events that are potentially harmful but will allow the application to continue running. * - INFO - Informational messages highlighting the progress of the application at the coarse-grained level. * - DEBUG - Detailed informational events that are useful when debugging an application. * - TRACE - Even more detailed informational events comparing to the DEBUG level. There are several methods that fully correspond to the log levels you can use for logging events. They are ``debug``, ``trace``, ``info``, ``warning``, ``error``, and ``critical``. Logging example: .. code-block:: yaml $log.info('print my info message {message}', message=>message) Logging methods use the same format rules as the YAQL :command:`format` function. Thus the line above is equal to the: .. code-block:: yaml $log.info('print my info message {message}'.format(message=>message)) To print an exception stacktrace, use the :command:`exception` method. This method uses the ERROR level: .. code-block:: yaml Try: - Throw: exceptionName Message: exception message Catch: With: exceptionName As: e Do: - $log.exception($e, 'something bad happen "{message}"', message=>message) .. note:: You can configure the logging subsystem through the ``logging.conf`` file of the Murano Engine. .. seealso:: * Source `Logger.yaml `_ file. * `OpenStack networking logging configuration `_. .. _status-reporter: Class: StatusReporter --------------------- Provides feedback feature. To follow the deployment process in the UI, all status changes should be included in the application configuration. .. seealso:: Source `StatusReporter.yaml `_ file. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/doc/source/admin/appdev-guide/murano_pl/metadata.rst0000664000175000017500000002431400000000000025143 0ustar00zuulzuul00000000000000.. _metadata: MuranoPL Metadata ~~~~~~~~~~~~~~~~~ MuranoPL metadata is a way to attach additional information to various MuranoPL entities such as classes, packages, properties, methods, and method arguments. That information can be used by both applications (to implement dynamic programming techniques) or by the external callers (API consumers like UI or even by the Murano Engine itself to impose some runtime behavior based on well known meta values). Thus, metadata is a flexible alternative to adding new keyword for every new feature. Work with metadata includes the following cases: * Defining your own metadata classes * Attaching metadata to various parts of MuranoPL code * Obtaining metadata and its usage Define metadata classes ----------------------- Define MuranoPL class with the description of arbitrary metadata. The class that can be used as metadata differs from the regular class: * The ``Usage`` attribute of the former equals to ``Meta``, while the ``Usage`` attribute of the latter equals to ``Class``. The default value of the ``Usage`` attribute is ``Class``. * Metadata class has additional attributes (``Cardinality``, ``Applies`` and ``Inherited``) to control how and where instances of that class can be attached. Cardinality +++++++++++ The ``Cardinality`` attribute can be set to either ``One`` or ``Many`` and indicates the possibility to attach two or more instances of metadata to a single language entity. The default value is ``One``. Applies +++++++ The ``Applies`` attribute can be set to one of ``Package``, ``Type``, ``Method``, ``Property``, ``Argument`` or ``All`` and controls the possible language entities which instances of metadata class can be attached to. It is possible to specify several values using YAML list notation. The default value is ``All``. Inherited +++++++++ The ``Inherited`` attribute can be set to ``true`` or ``false`` and specifies if there is metadata retained for child classes, overridden methods and properties. The default value is ``false``. Using of ``Inherited: true`` has the following consequences. If some class inherits from two classes with the same metadata attached and this metadata has ``Cardinality: One``, it will lead to emerging of two metadata objects with ``Cardinality: One`` within a single entity and will throw an exception. However, if the child class has this metadata attached explicitly, it will override the inherited metas and there is no conflict. If the child class has the same meta as its parent (attached explicitly), then in case of ``Cardinatity: One`` the meta of the child overrides the meta of the parent as it is mentioned above. And in case of ``Cardinatity: Many`` meta of the parent is added to the list of the child's metas. Example +++++++ The following example shows a simple meta-class implementation: .. code-block:: yaml Name: MetaClassOne Usage: Meta Cardinality: One Applies: All Properties: description: Contract: $.string() Default: null count: Contract: $.int().check($ >= 0) Default: 0 ``MetaClassOne`` is defined as a metadata class by setting the ``Usage`` attribute to ``Meta``. The ``Cardinality`` and ``Applies`` attributes determine that only one instance of ``MetaClassOne`` can be attached to object of any type. The ``Inherited`` attribute is omitted so there is no metadata retained for child classes, overridden methods and properties. In the example above, ``Cardinality`` and ``Applies`` can be omitted as well, as their values are set to default but in this case the author wants to be explicit. The following example shows metadata class with different values of attributes: .. code-block:: yaml Name: MetaClassMany Usage: Meta Cardinality: Many Applies: [Property, Method] Inherited: true Properties: description: Contract: $.string() Default: null count: Contract: $.int().check($ >= 0) Default: 0 An instance (or several instances) of ``MetaClassMany`` can be attached to either property or method. Overridden methods and properties inherit metadata from its parents. Attach metadata to a MuranoPL entity ------------------------------------ To attach metadata to MuranoPL class, package, property, method or method argument, add the ``Meta`` keyword to its description. Under the description, specify a list of metadata class instances which you want to attach to the entity. To attach only one metadata class instance, use a single scalar instead of a list. Consider the example of attaching previously defined metadata to different entities in a class definition: .. code-block:: yaml Namespaces: =: io.murano.bar std: io.murano res: io.murano.resources sys: io.murano.system Name: Bar Extends: std:Application Meta: MetaClassOne: description: "Just an empty application class with some metadata" count: 1 Properties: name: Contract: $.string().notNull() Meta: - MetaClassOne: description: "Name of the app" count: 1 - MetaClassMany: count: 2 - MetaClassMany: count: 3 Methods: initialize: Body: - $._environment: $.find(std:Environment).require() Meta: MetaClassOne: description: "Method for initializing app" count: 1 deploy: Body: - If: not $.getAttr(deployed, false) Then: - $._environment.reporter.report($this, 'Deploy started') - $._environment.reporter.report($this, 'Deploy finished') - $.setAttr(deployed, true) The ``Bar`` class has an instance of metadata class ``MetaClassOne`` attached. For this, the ``Meta`` keyword is added to the ``Bar`` class description and the instance of the ``MetaClassOne`` class is specified under it. This instance's properties are ``description`` and ``count``. There are three meta-objects attached to the ``name`` property of the ``Bar`` class. One of it is a ``MetaclassOne`` object and the other two are ``MetaClassMany`` objects. There can be more than one instance of ``MetaClassMany`` attached to a single entity since the ``Cardinality`` attribute of ``MetaClassMany`` is set to ``Many``. The ``initialize`` method of ``Bar`` also has its metadata. To attach metadata to the package, add the ``Meta`` keyword to ``manifest.yaml`` file. Example: .. code-block:: yaml Format: 1.0 Type: Application FullName: io.murano.bar.Bar Name: Bar Description: | Empty Description Author: author Tags: [bar] Classes: io.murano.bar.Bar: Bar.yaml io.murano.bar.MetaClassOne: MetaClassOne.yaml io.murano.bar.MetaClassMany: MetaClassMany.yaml Supplier: Name: Name Description: Description Summary: Summary Meta: io.murano.bar.MetaClassOne: description: "Just an empty application with some metadata" count: 1 Obtain metadata in runtime -------------------------- Metadata can be accessed from MuranoPL using reflection capabilities and from Python code using existing YAQL mechanism. The following example shows how applications can access attached metadata: .. code-block:: yaml Namespaces: =: io.murano.bar std: io.murano res: io.murano.resources sys: io.murano.system Name: Bar Extends: std:Application Meta: MetaClassOne: description: "Just an empty application class with some metadata" Methods: sampleAction: Scope: Public Body: - $._environment.reporter.report($this, typeinfo($).meta. where($ is MetaClassOne).single().description) The ``sampleAction`` method is added to the ``Bar`` class definition. This makes use of metadata attached to the ``Bar`` class. The information about the ``Bar`` class is received by calling the ``typeinfo`` function. Then metadata is accessed through the ``meta`` property which returns the collection of all meta attached to the property. Then it is checked that the meta is a ``MetaClassOne`` object to ensure that it has ``description``. While executing the action, the phrase "Just an empty application class with some metadata" is reported to a log. Some advanced usages of MuranoPL reflection capabilities can be found in the corresponding section of this reference. By using metadata, an application can get information of any type attached to any object and use this information to change its own behavior. The most valuable use-cases of metadata can be: * Providing information about capabilities of application and its parts * Setting application requirements Capabilities can include version of software, information for use in UI or CLI, permissions, and any other. Metadata can also be used in requirements as a part of the contract. The following example demonstrates the possible use cases for the metadata: .. code-block:: yaml Name: BlogApp Meta: m:SomeFeatureSupport: support: true Properties: volumeName: Contract: $.string().notNull() Meta: m:Deprecated: text: "volumeName property is deprecated" server: Contract: $.class(srv:CoolServer).notNull().check(typeinfo($).meta. where($ is m:SomeFeatureSupport and $.support = true).any()) Methods: importantAction: Scope: Public Meta: m:CallerMustBeAdmin Note, that the classes in the example do not exist as of Murano Mitaka, and therefore the example is not a real working code. The ``SomeFeatureSupport`` metadata with ``support: true`` says that the ``BlogApp`` application supports some feature. The ``Deprecated`` metadata attached to the ``volumeName`` property informs that this property has a better alternative and it will not be used in the future versions anymore. The ``CallerMustBeAdmin`` metadata attached to the ``importantAction`` method sets permission to execute this method to the admin users only. In the contract of the ``server`` property it is specified that the server application must be of the ``srv:CoolServer`` class and must have the attached meta-object of the ``m:SomeFeatureSupport`` class with the ``support`` property set to ``true``. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/doc/source/admin/appdev-guide/murano_pl/reflection.rst0000664000175000017500000001412100000000000025510 0ustar00zuulzuul00000000000000.. _reflection: Reflection capabilities in MuranoPL. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Reflection provides objects that describes MuranoPL classes and packages. The first important function is ``typeinfo`` . Usage: .. code-block:: yaml $typeInfo: typeinfo($someObject) Now ``$typeInfo`` variable contains instance of type of ``$someObject`` (``MuranoClass`` instance). MuranoPL provide following abilities to reflection: .. _types_reflection: Types ----- .. list-table:: :header-rows: 1 :widths: 20 80 :stub-columns: 0 :class: borderless * - Property - Description * - ``name`` - name of MuranoPL class * - ``version`` - version (`SemVer`_) of MuranoPL class. * - ``ancestors`` - list of class ancestors * - ``properties`` - list of class properties. See :ref:`properties_reflection` * - ``package`` - package information. See :ref:`package_reflection` * - ``methods`` - list of methods. See :ref:`methods_reflection` * - ``type`` - reference to type, which can be used as argument in engine functions *Example* .. code-block:: yaml - $typeInfo: typeinfo($) ... # log name, version and package name of this class - $log.info("This is "{class_name}/{version} from {package}", class_name => $typeInfo.name, version => str($typeInfo.version), package => $typeInfo.package.name)) - $log.info("Ancestors:") - For: ancestor In: $typeInfo.ancestors Do: #log all ancestors names - $log.info("{ancestor_name}", ancestor_name => $ancestor.name) # log full class version - $log.info("{version}", version => str($typeInfo.version)) # create object with same class - $newObject = new($typeInfo.type) .. _properties_reflection: Properties ---------- Property introspection ++++++++++++++++++++++ .. list-table:: :header-rows: 1 :widths: 20 80 :stub-columns: 0 :class: borderless * - Property - Description * - ``name`` - name of property * - ``hasDefault`` - boolean value. `True`, if property has default value, `False` otherwise * - ``usage`` - `Usage` property's field. See :ref:`property_usage` for details * - ``declaringType`` - type - owner of declared property Property access +++++++++++++++ .. list-table:: :header-rows: 1 :widths: 20 80 :stub-columns: 0 :class: borderless * - Methods - Description * - ``$property.setValue($target, $value)`` - set value of ``$property`` for object ``$target`` to ``$value`` * - ``$property.getValue($target)`` - get value of ``$property`` for object ``$target`` *Example* .. code-block:: yaml - $typeInfo: typeinfo($) ... # select first property - $selectedPropety: $typeInfo.properties.first() # log property name - $log.info("Hi, my name is {p_name}, p_name => $selectedProperty.name) # set new property value - $selectedProperty.setValue($, "new_value") # log new property value using reflection - $log.info("My new value is {value}", value => $selectedProperty.getValue($)) # also, if property static, $target can be null - $log.info("Static property value is {value}, value => $staticProperty.getValue(null)) .. _package_reflection: Packages -------- .. list-table:: :header-rows: 1 :widths: 20 80 :stub-columns: 0 :class: borderless * - Property - Description * - ``types`` - list of types, declared in package * - ``name`` - package name * - ``version`` - package version *Example* .. code-block:: yaml - $typeInfo: typeinfo($) ... - $packageRef: $typeInfo.package - $log.info("This is package {p_name}/{p_version}", p_name => $packageRef.name, p_version => str($packageRef.version)) - $log.info("Types in package:") - For: type_ In: $packageRef.types Do: - $log.info("{typename}", typename => type_.name) .. _methods_reflection: Methods ------- Methods properties ++++++++++++++++++ .. list-table:: :header-rows: 1 :widths: 20 80 :stub-columns: 0 :class: borderless * - Property - Description * - ``name`` - method's name * - ``declaringType`` - type - owner of declared method * - ``arguments`` - list of method's arguments. See :ref:`arguments_reflection` Method invoking +++++++++++++++ .. list-table:: :header-rows: 1 :widths: 20 80 :stub-columns: 0 :class: borderless * - Methods - Description * - ``$method.invoke($target, $arg1, ... $argN, kwarg1 => value1, ..., kwargN => valueN)`` - call ``$target``'s method $method with ``$arg1``, ..., ``$argN`` positional arguments and ``kwarg1``, .... ``kwargN`` named arguments *Example* .. code-block:: yaml - $typeInfo: typeinfo($) ... # select single method by name - $selectedMethod: $typeInfo.methods.where($.name = sampleMethodName).single() # log method name - $log.info("Method name: {m_name}", m_name => $selectedMethod.name) # log method arguments names - For: argument In: $selectedMethod.arguments Do: - $log.info("{name}", name => $argument.name) # call method with positional argument 'bar' and named `baz` == 'baz' - $selectedMethod.invoke($, 'bar', baz => baz) .. _arguments_reflection: Method arguments ---------------- .. list-table:: :header-rows: 1 :widths: 20 80 :stub-columns: 0 :class: borderless * - Property - Description * - ``name`` - argument's name * - ``hasDefault`` - `True` if argument has default value, `False` otherwise * - ``declaringMethod`` - method - owner of argument * - ``usage`` - argument's usage type. See :ref:`method_arguments` for details .. code-block:: yaml - $firstArgument: $selectedMethod.arguments.first() # store argument's name - $argName: $firstArgument.name # store owner's name - $methodName: $firstArgument.declaringMethod.name - $log.info("Hi, my name is {a_name} ! My owner is {m_name}", a_name => $argName, m_name => $methodName) .. Links: .. _`SemVer`: http://semver.org ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/doc/source/admin/appdev-guide/murano_pl/statics.rst0000664000175000017500000001423400000000000025035 0ustar00zuulzuul00000000000000.. _static_methods_and_properties: Static methods and properties ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ In MuranoPL, static denotes class methods and class properties (as opposed to instance methods and instance properties). These methods and properties can be accessed without an instance present. Static methods are often used for helper methods that are not bound to any object (that is, do not maintain a state) or as a convenient way to write a class factory. Type objects ------------ Usually static methods and properties are accessed using `type object`. That is, an object that represents the class rather than class instance. For any given class `foo.Bar` its type object may be retrieved using any of the following ways: * Using ``ns:Bar`` notation considering that `ns` is declared in `Namespaces` section (and it is `foo` in this case), * Using ``:Bar`` syntax if `Bar` is in the current namespace (that is, what ``=:Bar`` would mean if ``=`` was a valid namespace prefix), * Using ``type()`` function with a fully qualified class name: ``type('foo.Bar')``, * By obtaining a type of class instance: ``type($object)`` (available for packages with format version starting from `1.3`), * Through reflection: ``typeinfo($object).type``. No matter what method was used to get type object, the returned object will be the same because there can be only one type object per class. All functions that accept type name, for example ``new()`` function, also accept type objects. Accessing static methods and properties --------------------------------------- Static methods can be invoked using one of the two ways: * Using `type object`: ``ns:Bar.foo(arg)``, ``:Bar.foo(arg)``, and so on, * On a class instance similar to normal methods: ``$obj.foo(arg)``. Access to properties is similar to that: * Using `type object`: ``ns:Bar.property``, ``:Bar.property``, and so on, * On a class instance: ``$obj.property``. Static properties are defined on a class rather than on an instance. Therefore, their values will be the same for all class instances (for particular version of the class). Declaration of static methods and properties -------------------------------------------- Methods and properties are declared to be static by specifying ``Usage: Static`` on them. For example: .. code-block:: yaml Properties: property: Contract: $.string() Usage: Static Methods: foo: Usage: Static Body: - Return: $.property Static properties are never initialized from object model but can be modified from within MuranoPL code (i.e. they are not immutable). Static methods also can be executed as an action from outside using ``Scope: Public``. Within static method `Body` ``$this`` (and ``$`` if not set to something else in expression) are set to type object rather than to instance, as it is for regular methods. Static methods written in Python -------------------------------- For MuranoPL classes entirely or partially written in Python, all methods that have either ``@staticmethod`` or ``@classmethod`` decorators are automatically imported as static methods and work as they normally do in Python. .. _extension_methods: Extension methods ~~~~~~~~~~~~~~~~~ Extension methods are a special kind of static methods that can act as if they were regular instance methods of some other type. Extension methods enable you to "add" methods to existing types without modifying the original type. Defining extension methods -------------------------- Extension methods are declared with the ``Usage: Extension`` modifier. For example: .. code-block:: yaml Name: SampleClass Methods: mul: Usage: Extension Arguments: - self: Contract: $.int().notNull() - arg: Contract: $.int().notNull() Body: Return: $self * $arg Extension method are said to extend some other type and that type is deducted from the first method argument contract. Thus extension methods must have at least one argument. Extension methods can also be written in Python just the same way as static methods. However one should be careful in method declaration and use precise YAQL specification of the type of first method argument otherwise the method will become an extension of any type. To turn Python static method into extension method it must be decorated with ``@yaql.language.specs.meta('Usage', 'Extension')`` decorator. Using extension methods ----------------------- The example above defines a method that extends integer type. Therefore, with the method above it becomes possible to say ``2.mul(3)``. However, the most often usage is to extend some existing MuranoPL class using ``class()`` contract. If the first argument contract does not have ``notNull()``, then the method can be invoked on the ``null`` object as well (like ``null.foo()``). Extension methods are static methods and, therefore,can be invoked in a usual way on type object: ``:SampleClass.mul(2, 3)``. However, unlike regular static methods extensions cannot be invoked on a class instance because this can result in ambiguity. Using extension lookup order ---------------------------- When somewhere in the code the ``$foo.bar()`` expression is encountered, MuranoPL uses the following order to locate bar() ``implementation``: * If there is an instance or static method in ``$foo``'s class, it will be used. * Otherwise if the current class (where this expression was encountered) has an extension method called ``bar`` and ``$foo`` satisfies the contract of its first argument, then this method will be called. Normally, if no method was found an exception will be raised. However, additional extension methods can be imported into the current context. This is done using the ``Import`` keyword on a class level. The ``Import`` section specifies either a list or a single type name (or type object) which extension methods will be available anywhere within the class code: .. code-block:: yaml Name: MyClass Import: - ns:SomeOtherType - :ClassFomCurrentContext - 'io.murano.foo.Bar' If no method was found with the algorithm above, the search continues on extension methods of all classes listed in the ``Import`` section in the order types are listed. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/doc/source/admin/appdev-guide/murano_pl/versioning.rst0000664000175000017500000001610000000000000025540 0ustar00zuulzuul00000000000000.. _versioning: Versioning ~~~~~~~~~~ Versioning is an ability to assign a version number to some particular package (and, in turn, to a class) and then distinguish packages with different versions. Package version --------------- It is possible to specify a version for packages. You can import several versions of the same package simultaneously and even deploy them inside a single environment. To do this, you should use Glare as a storage for packages. But if you're going to keep only the latest version API is still good enough and both FormatVersion and Version rules will still be there. For more information about using Glare, refer to :ref:`glare_usage`. To specify the version of your package, add a new section to the manifest file: .. code-block:: yaml Version: 0.1.0 .. It should be standard SemVer format version string consisting of 3 parts: ``Major.Minor.Patch`` and optional SemVer suffixes ``[-dev-build.label[+metadata.label]]``. All MuranoPL classes have the version of the package they are contained in. If no version is specified, the package version is *0.0.0*. .. note:: It is impossible to show multiple versions of the same application in murano dashboard: only the last one is shown if the multiple versions are present. Package requirements -------------------- In some cases, packages may require other packages for their work. You need to list such packages in the `Require` section of the manifest file: .. code-block:: yaml Require: package1_FQN: version_spec_1 ... packageN_FQN: version_spec_N .. ``version_spec`` here denotes the allowed version range. It can be either in semantic_version specification pip-like format or as a partial version string. If you do not want to specify the package version, leave this value empty: .. code-block:: yaml Require: package1_FQN: '>=0.0.3' package2_FQN: .. In this case, version specification is equal to *0*. .. note:: All packages depend on the `io.murano` package (Core Library). If you do not specify this requirement in the list (or the list is empty, or there is no ``Require`` key in the package manifest), then dependency *io.murano: 0* will be automatically added. Object version -------------- You can specify the version of the objects in UI definition when your application requires a specific version of some class. To do this, add a new key ``classVersion`` to section ``?`` describing the object: .. code-block:: yaml ?: type: io.test.apps.TestApp classVersion: version_spec .. Side-by-side versioning of packages ----------------------------------- In some cases it might happen that several different versions of the same class are simultaneously present in a single environment: * There are different versions of the same MuranoPL class inside a single object model (environment). * Several class versions encounter within class parents. For example, class A extends B and C and class C inherits B2, where B and B2 are two different versions of the same class. The first case, when two different versions of the same class need to communicate with each other, is handled by the fact that in order to do that there is a ``class()`` contract for that value. ``class()`` contract validates object version against package requirements. If class A has a property with contract $.class(B), then an object passed in this property when upcasted to B must have a version compatible with requirement specification in A's package (requesting B's package). For the second case, where a single class attempts to inherit from two different versions of the same class engine (DSL), it attempts to find a version of this class which satisfies all parties and use it instead. However, if it is impossible, all remained different versions of the same class are treated as if they are unrelated classes. For example: classA inherits classB from packageX and classC from packageY. Both classB and classC inherit from classD from packageZ; however, packageX depends on the version 1.2.0 of packageZ, while packageY depends on the version 1.3.0. This leads to a situation when classA transitively inherits classD of both versions 1.2 and 1.3. Therefore, an exception is thrown. However, if packageY's dependency would be just "1" (which means any of the 1.x.x family), the conflict would be resolved and the 1.2 would be used as it satisfies both inheritance chains. Murano engine is free to use any package version that is valid for the spec. For example, one application requires packageX with version spec < 0.3 and another package with the spec > 0. If both packages are get used in the same environment and the engine already loaded version 0.3 it can still use it for the second requirement even if there is a package with version 0.4 in the catalog and the classes from both classes are never interfere. In other words, engine always tries to minimize the number of versions in use for the single package to avoid conflicts and unnecessary package downloads. However, it also means that packages not always get the latest requirements. .. _ManifestFormat: Manifest format versioning -------------------------- The manifests of packages are versioned using *Format* attribute. Currently, available versions are: `1.0`, `1.1`, `1.2` and `1.3`. The versioning of manifest format is directly connected with YAQL and version of murano itself. The short description of versions: ================== =========================================================== Format version Description ================== =========================================================== **1.0** supported by all versions of murano. Use this version if you are planning to use *yaql 0.2* in your application **1.1** supported since Liberty. *yaql 0.2* is supported in legacy mode. Specify it, if you want to use features from *yaql 0.2* and *yaql 1.0.0* at the same time in your application. **1.2** supported since Liberty. Do not use *yaql 0.2* in applications with this format. **1.3** supported since Mitaka. *yaql 1.1* is available. It's recommended specifying this format in new applications, where compatibility with older versions of murano is not required. **1.4** supported since Newton. Keyword ``Scope`` is introduced for class methods to declare method's accessibility from outside through the API call. ================== =========================================================== UI forms versioning ------------------- UI forms are versioned using Format attribute inside YAML definition. For more information, refer to :ref:`corresponding documentation`. Execution plan format versioning -------------------------------- Format of an execution plan can be specified using property ``FormatVersion``. More information can be found :ref:`here`. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/doc/source/admin/appdev-guide/murano_pl/yaml.rst0000664000175000017500000000322700000000000024325 0ustar00zuulzuul00000000000000.. _yaml: YAML ~~~~ YAML is an easily readable data serialization format that is a superset of JSON. Unlike JSON, YAML is designed to be read and written by humans and relies on visual indentation to denote nesting of data structures. This is similar to how Python uses indentation for block structures instead of curly brackets in most C-like languages. Also YAML may contain more data types as compared to JSON. See http://yaml.org/ for a detailed description of YAML. MuranoPL is designed to be representable in YAML so that MuranoPL code could remain readable and structured. Usually MuranoPL files are YAML encoded documents. But MuranoPL engine itself does not deal directly with YAML documents, and it is up to the hosting application to locate and deserialize the definitions of particular classes. This gives the hosting application the ability to control where those definitions can be found (a file system, a database, a remote repository, etc.) and possibly use some other serialization formats instead of YAML. MuranoPL engine relies on a host deserialization code when detecting YAQL expressions in a source definition. It provides them as instances of the YaqlExpression class rather than plain strings. Usually, YAQL expressions can be distinguished by the presence of $ (the dollar sign) and operators, but in YAML, a developer can always state the type by using YAML tags explicitly. For example: .. code-block:: yaml :linenos: Some text - a string $.something() - a YAQL expression "$.something()" - a string because quotes are used !!str $ - a string because a YAML tag is used !yaql "text" - a YAQL expression because a YAML tag is used ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/doc/source/admin/appdev-guide/murano_pl/yaql.rst0000664000175000017500000000427600000000000024336 0ustar00zuulzuul00000000000000.. _yaql: YAQL ~~~~ YAQL (Yet Another Query Language) is a query language that was also designed as a part of the murano project. MuranoPL makes an extensive use of YAQL. A description of YAQL can be found `here `_. Simply speaking, YAQL is the language for expression evaluation. The following examples are all valid YAQL expressions: ``2 + 2, foo() > bar(), true != false``. The interesting thing in YAQL is that it has no built in list of functions. Everything YAQL can access is customizable. YAQL cannot call any function that was not explicitly registered to be accessible by YAQL. The same is true for operators. So the result of the expression 2 * foo(3, 4) completely depends on explicitly provided implementations of "foo" and "operator_*". YAQL uses a dollar sign ($) to access external variables, which are also explicitly provided by the host application, and function arguments. ``$variable`` is a syntax to get a value of the variable "$variable", $1, $2, etc. are the names for function arguments. "$" is a name for current object: data on which an expression is evaluated, or a name of a single argument. Thus, "$" in the beginning of an expression and "$" in the middle of it can refer to different things. By default, YAQL has a lot of functions that can be registered in a YAQL context. This is very similar to how SQL works but uses more Python-like syntax. For example: :code:`$.where($.myObj.myScalar > 5`, :code:`$.myObj.myArray.len() > 0`, and :code:`$.myObj.myArray.any($ = 4)).select($.myObj.myArray[0])` can be executed on :code:`$ = array` of objects, and result in another array that is a filtration and projection of a source data. .. note:: There is no assignment operator in YAQL, and ``=`` means comparison, the same what ``==`` means in Python. As YAQL has no access to underlying operating system resources and is fully controllable by the host, it is secure to execute YAQL expressions without establishing a trust to the executed code. Also, because functions are not predefined, different methods can be accessible in different context. So, YAQL expressions that are used to specify property contracts are not necessarily valid in workflow definitions. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/doc/source/admin/appdev-guide/murano_pl.rst0000664000175000017500000000076000000000000023362 0ustar00zuulzuul00000000000000.. _murano-pl: ================== MuranoPL Reference ================== To develop applications, murano project refers to Murano Programming Language (MuranoPL). It is represented by easily readable YAML and YAQL languages. The sections below describe these languages. .. toctree:: :maxdepth: 1 murano_pl/yaml murano_pl/yaql murano_pl/class_templ murano_pl/core_lib murano_pl/reflection murano_pl/statics murano_pl/metadata murano_pl/versioning murano_pl/actions././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1696417899.7211804 murano-16.0.0/doc/source/admin/appdev-guide/muranopackages/0000775000175000017500000000000000000000000023631 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/doc/source/admin/appdev-guide/muranopackages/dynamic_ui.rst0000664000175000017500000006544000000000000026515 0ustar00zuulzuul00000000000000.. _DynamicUISpec: Dynamic UI definition specification ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ The main purpose of Dynamic UI is to generate application creation forms "on-the-fly". The Murano dashboard does not know anything about applications that will be presented in the catalog and which web forms are required to create an application instance. So all application definitions should contain an instruction, which tells the dashboard how to create an application and what validations need to be applied. This document will help you to compose a valid UI definition for your application. The UI definition should be a valid YAML file and may contain the following sections (for version 2.x): * **Version** Points out the syntax version in use. *Optional* * **Templates** An auxiliary section, used together with an Application section to help with object model composing. *Optional* * **Parameters** An auxiliary section for evaluated once parameters. *Optional* * **ParametersSource** A static action name (ClassName.methodName) to call for additional parameters. *Optional* * **Application** Object model description passed to murano engine and used for application deployment. *Required* * **Forms** Web form definitions. *Required* .. _DynamicUIversion: Version ------- The syntax and format of dynamic UI definitions may change over time, so the concept of *format versions* is introduced. Each UI definition file may contain a top-level section called *Version* to indicate the minimum version of Murano Dynamic UI platform which is capable to process it. If the section is missing, the format version is assumed to be latest supported. The version consists of two non-negative integer segments, separated by a dot, i.e. has a form of *MAJOR.MINOR*. Dynamic UI platforms having the same MAJOR version component are compatible: i.e. the platform having the higher version may process UI definitions with lower versions if their MAJOR segments are the same. For example, Murano Dynamic UI platform of version 2.2 is able to process UI definitions of versions 2.0, 2.1 and 2.2, but is unable to process 3.0 or 1.9. Currently, the latest version of Dynamic UI platform is 2.3. It is incompatible with UI definitions of Version 1.0, which were used in Murano releases before Juno. .. note:: Although the ``Version`` field is considered to be optional, its default value is the latest supported version. So if you intent to use applications with the previous stable murano version, verify that the version is set correctly. Version history ~~~~~~~~~~~~~~~ +---------+-------------------------------------------------------------------+-------------------+ | Version | Changes | OpenStack Version | +=========+===================================================================+===================+ | 1.0 | - Initial Dynamic UI implementation | Icehouse | +---------+-------------------------------------------------------------------+-------------------+ | 2.0 | - *instance* field support is dropped | Juno, Kilo | | | - New *Application* section that describes engine object model | | | | - New *Templates* section for keeping reusable pieces of Object | | +---------+-------------------------------------------------------------------+-------------------+ | 2.1 | - New *network* field provides a selection of networks and | Liberty | | | their subnetworks as a dropdown populated with those which are | | | | available to the current tenant. | | +---------+-------------------------------------------------------------------+-------------------+ | 2.2 | - Now *application name* is added automatically to the last | Liberty | | | service form. It is needed for a user to recognize one | | | | created application from another in the UI. Previously all | | | | application definitions contained the *name* property. So to | | | | support backward compatibility, you need to manually remove | | | | *name* field from class properties. | | +---------+-------------------------------------------------------------------+-------------------+ | 2.3 | - Now *password* field supports ``confirmInput`` flag and | Mitaka | | | validator overloading with single ``regexpValidator`` or | | | | multiple *validators* attribute. | | +---------+-------------------------------------------------------------------+-------------------+ | 2.4 | - Parameters and ParametersSource sections were added | Ocata | | | - ref() YAQL function were added to Application DSL | | | | - YAQL expressions can be used anywhere in the form definition | | | | - choice control accepts choices in dictionary format | | +---------+-------------------------------------------------------------------+-------------------+ Application ----------- The Application section describes an *application object model*. The model is a dictionary (document) of application property values (inputs). Property value might be of any JSON-serializable type (including lists and maps). In addition the value can be of an object type (another application, application component, list of components etc.). Object properties are represented either by the object model of the component (i.e. dictionary) or by an object ID (string) if the object was already defined elsewhere. Each object definition (including the one in Application itself) must have a special ``?`` key called ``object header``. This key holds object metadata most important of which is the object type name. Thus the Application might look like this: .. code-block:: yaml Application: ?: type: "com.myCompany.myNamespace.MyClass" property1: "string property value" property2: 123 property3: key1: value1 key2: [1, false, null] property4: ?: type: "com.myCompany.myNamespace.MyComponent" property: value However in most cases the values in object model should come from input fields rather than being static as in example above. To achieve this, object model values can also be of a `YAQL ` expression type. With expressions language it becomes possible to retrieve input control values, do some calculations and data transformations (queries). Any YAML value that is not enclosed in quote marks and conforms to the YAQL syntax is considered to be a YAQL expression. There is also an explicit YAML tag for the YAQL expressions: ``!yaql``. So with the YAQL addition ``Application`` section might look like this: .. code-block:: yaml Application: ?: type: "com.myCompany.myNamespace.MyClass" property1: $.formName.controlName property2: 100 + 20 + 3 property3: !yaql "'KEY1'.toLower()'": !yaql "value1 + '1'" key2: [$parameter, not true] property4: null When evaluating YAQL expressions ``$`` is set to the forms data (list of dictionaries with cleaned validated forms' data) and templates and parameters are available using $templateName ($parameterName) syntax. See below on templates and parameters. YAQL comes with hundreds of functions bundled. In addition to that there are another four functions provided by murano dashboard: * **generateHostname(pattern, index)** is used for a machine hostname template generation. It accepts two arguments: name pattern (string) and index (integer). If '#' symbol is present in name pattern, it will be replaced with the index provided. If pattern is an empty string, a random name will be generated. * **repeat(template, times)** is used to produce a list of data snippets, given the template snippet (first argument) and number of times it should be reproduced (second argument). Inside that template snippet current step can be referenced as *$index*. * **name()** returns current application name. * **ref(templateName [, parameterName] [, idOnly])** is used to generate object definition from the template and then reference it several times in the object model. 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. Templates --------- It is often that application object model contains number of similar instances of the same component/class. For example it might be list of servers for multi-server application or list of nodes or list of components. For such cases UI definition markup allow to give the repeated object model snippet a name and then refer to it by the name in the application object model. Such snippets are placed into ``Templates`` section: .. code-block:: yaml Templates: primaryController: ?: type: "io.murano.windows.activeDirectory.PrimaryController" host: ?: type: "io.murano.windows.Host" adminPassword: $.appConfiguration.adminPassword name: generateHostname($.appConfiguration.unitNamingPattern, 1) flavor: $.instanceConfiguration.flavor image: $.instanceConfiguration.osImage secondaryController: ?: type: "io.murano.windows.activeDirectory.SecondaryController" host: ?: type: "io.murano.windows.Host" adminPassword: $.appConfiguration.adminPassword name: generateHostname($.appConfiguration.unitNamingPattern, $index + 1) flavor: $.instanceConfiguration.flavor image: $.instanceConfiguration.osImage Then the template can be inserted into application object model or to another template using ``$templateName`` syntax. It is often case that it is used together with ``repeat`` function to put several instances of template. In this case templates may use of ``$index`` variable which will hold current iteration number: .. code-block:: yaml Application: ?: type: io.murano.windows.activeDirectory.ActiveDirectory primaryController: $primaryController secondaryControllers: repeat($secondaryController, $.appConfiguration.dcInstances - 1) It is important to remember that templates are evaluated upon each access or ``repeat()`` iteration. Thus if the template has some properties set to a random or generated values they are going to be different for each instance of the template. Another use case for templates is when single object is referenced several times within application object model: .. code-block:: yaml Templates: instance: ?: type: "io.murano.resources.LinuxMuranoInstance" image: myImage flavor: "m1.small" Application: ?: type: "com.example.MyApp" components: - ?: type: "com.example.MyComponentType1" instance: ref(instance) - ?: type: "com.example.MyComponentType2" instance: ref(instance) In example above there are two components that uses the same server instance. If this example had ``$instance`` instead of ``ref(instance)`` that would be two unrelated servers based on the same template i.e. with the same image and flavor, but not the same VM. Parameters and ParametersSource ------------------------------- Parameters are values that are used to parametrize the UI form and/or application object model. Parameters are put into ``Parameters`` section and accessed using ``$parameterName`` syntax: .. code-block:: yaml Parameters: param1: "Hello!" Application: ?: type: "com.example.MyApp" stringProperty: $param1 Parameters are very similar to Templates with two differences: #. Parameter values are evaluated only once per application instance at the very beginning whereas templates are evaluated on each access. #. Parameter values can be used to initialize UI control attributes (e.g. initial text box value, list of choices for a drop down etc.) However the most powerful feature about parameters is that their values might be obtained from the application class. Here is how to do it: #. In one of the classes in the MuranoPL package (usually the main application class define a static action method without arguments that returns a dictionary of variables: .. code-block:: yaml Name: "com.example.MyApp" Methods: myMethod: Usage: Static Scope: Public Body: # arbitrary MuranoPL code can be used here Return: var1: value1 var2: 123 #. In UI definition file add .. code-block:: yaml ParametersSource: "com.example.MyApp.myMethod" The class name may be omitted. In this case the dashboard will try to use the type of Application object or package FQN for that purpose. The values returned by the method are going to be merged into Parameters section like if they were defined statically. Forms ----- This section describes markup elements for defining forms, which are currently rendered and validated with Django. Each form has a name, field definitions (mandatory), and validator definitions (optionally). Note that each form is split into 2 parts: * **input area** - left side, where all the controls are located * **description area** - right side, where descriptions of the controls are located Each field should contain: * **name** - system field name, could be any * **type** - system field type Currently supported options for **type** attribute are: * *string* - text field (no inherent validations) with one-line text input * *boolean* - boolean field, rendered as a checkbox * *text* - same as string, but with a multi-line input * *integer* - integer field with an appropriate validation, one-line text input * *choice* - drop-down list of variants. Each variant has a display string that is going to be displayed to the user and associated key that is going to be a control value * *password* - text field with validation for strong password, rendered as two masked text inputs (second one is for password confirmation) * *clusterip* - specific text field, used for entering cluster IP address (validation for valid IP address syntax) * *databaselist* - specific field, a list of databases (comma-separated list of databases' names, where each name has the following syntax first symbol should be latin letter or underscore; subsequent symbols can be latin letter, numeric, underscore, at the sign, number sign or dollar sign), rendered as one-line text input * *image* - specific field, used for filtering suitable images by image type provided in murano metadata in glance properties. * *flavor* - specific field, used for selection instance flavor from a list * *keypair* - specific field, used for selecting a keypair from a list * *azone* - specific field, used for selecting instance availability zone from a list * *network* - specific field, used to select a network and subnet from a list of the ones available to the current user * *securitygroup* - specific field, used for selecting a custom security group to assign to the instance * *volume* - specific field, used for selecting a volume or a volume snapshot from a list of available volumes (and volume snapshots) * any other value is considered to be a fully qualified name for some Application package and is rendered as a pair of controls: one for selecting already existing Applications of that type in an Environment, second - for creating a new Application of that type and selecting it Other arguments (and whether they are required or not) depends on a field's type and other attributes values. Most of them are standard Django field attributes. The most common attributes are the following: * **label** - name, that will be displayed in the form; defaults to **name** being capitalized. * **description** - description, that will be displayed in the description area. Use YAML line folding character ``>-`` to keep the correct formatting during data transferring. * **descriptionTitle** - title of the description, defaults to **label**; displayed in the description area * **hidden** whether field should be visible or not in the input area. Note that hidden field's description will still be visible in the descriptions area (if given). Hidden fields are used storing some data to be used by other, visible fields. * **minLength**, **maxLength** (for string fields) and **minValue**, **maxValue** (for integer fields) are transparently translated into django validation properties. * **choices** - a choices for the ``choice`` control type. The format is ``[["key1", "display value1"], ["key2", "display value2"]]``. Starting from version 2.4 this can also be passed as a ``{key1: "display value1", key2: "display value2"}`` * **regexpValidator** - regular expression to validate user input. Used with *string* or *password* field. * **errorMessages** - dictionary with optional 'invalid' and 'required' keys that set up what message to show to the user in case of errors. * **validators** is a list of dictionaries, each dictionary should at least have *expr* key, under that key either some `YAQL `_ expression is stored, either one-element dictionary with *regexpValidator* key (and some regexp string as value). Another possible key of a validator dictionary is *message*, and although it is not required, it is highly desirable to specify it - otherwise, when validator fails (i.e. regexp doesn't match or YAQL expression evaluates to false) no message will be shown. Note that field-level validators use YAQL context different from all other attributes and section: here *$* root object is set to the value of field being validated (to make expressions shorter). .. code-block:: yaml - name: someField type: string label: Domain Name validators: - 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 Using of *regexpValidator* and *validators* attributes with *password* field was introduced in version 2.3. By default, password should have at least 7 characters, 1 capital letter, 1 non-capital letter, 1 digit, and 1 special character. If you do not want password validation to be so strong, you can override it by setting a custom validator or multiple validators for password. For that add *regexpValidator* or *validators* to the *password* field and specify custom regexp string as value, just like with any *string* field. *Example* .. code-block:: yaml - name: password type: password label: Password descriptionTitle: Password description: >- Please, provide password for the application. Password should be 5-50 characters long and consist of alphanumeric characters regexpValidator: '^[a-zA-Z0-9]{5,50}?$' * **confirmInput** is a flag used only with password field and defaults to ``true``. If you decided to turn off automatic password field cloning, you should set it to ``false``. In this case password confirmation is not required from a user. * **widgetMedia** sets some custom *CSS* and *JavaScript* used for the field's widget rendering. Note, that files should be placed to Django static folder in advance. Mostly they are used to do some client-side field enabling/disabling, hiding/unhiding etc. * **requirements** is used only with flavor field and prevents user to pick unstable for a deployment flavor. It allows to set minimum ram (in MBs), disk space (in GBs) or virtual CPU quantity. Example that shows how to hide items smaller than regular *small* flavor in a flavor select field: .. code-block:: yaml - name: flavor type: flavor label: Instance flavor requirements: min_disk: 20 min_vcpus: 2 min_memory_mb: 2048 * **include_snapshots** is used only with the volume field. ``True`` by default. If ``True``, the field list includes available volumes and volume snapshots. If set to ``False``, only available volumes are shown. * **include_subnets** is used only with network field. ``True`` by default. If ``True``, the field list includes all the possible combinations of network and subnet. E.g. if there are two available networks X and Y, and X has two subnets A and B, while Y has a single subnet C, then the list will include 3 items: (X, A), (X, B), (Y, C). If set to ``False`` only network names will be listed, without their subnets. * **filter** is used only with network field. ``None`` by default. If set to a regexp string, will be used to display only the networks with names matching the given regexp. * **murano_networks** is used only with network field. ``None`` by default. May have values ``None``, ``exclude`` or ``translate``. Defines the handling of networks which are created by murano. Such networks usually have very long randomly generated names, and thus look ugly when displayed in the list. If this value is set to ``exclude`` then these networks are not shown in the list at all. If set to ``translate`` the names of such networks are replaced by a string ``Network of %env_name%``. .. note:: This functionality is based on the simple string matching of the network name prefix and the names of all the accessible murano environments. If the environment is renamed after the initial deployment this feature will not be able to properly translate or exclude its network name. * **allow_auto** is used only with network field. ``True`` by default. Defines if the default value of the dropdown (labeled "Auto") should be present in the list. The default value is a tuple consisting of two ``None`` values. The logic on how to treat this value is up to application developer. It is suggested to use this field to indicate that the instance should join default environment network. For use-cases where such behavior is not desired, this parameter should be set to ``False``. *Network* field and its specific attributes (*include_subnets*, *filter*, *murano_networks*, *allow_auto*) are available since version 2.1. Before that, there was no way for the end user to select existing network in the UI. The only way to change the default networking behavior was the usage of networking.yaml file. It allows to override the networking setting at the environment level, for all the murano environments of all the tenants. Now you can simple add a *network* field to your form definition and provide the ability to select the desired network for the specific application. *Example* .. code-block:: yaml - instanceConfiguration: fields: - name: network type: network label: Network description: Select a network to join. 'Auto' corresponds to a default environment's network. murano_networks: translate Besides field-level validators, form-level validators also exist. They use **standard context** for YAQL evaluation and are required when there is a need to validate some form's constraint across several fields. *Example* .. code-block:: yaml Forms: - appConfiguration: fields: - name: dcInstances type: integer hidden: true initial: 1 required: false maxLength: 15 helpText: Optional field for a machine hostname template - name: unitNamingPattern type: string label: Instance Naming Pattern required: false maxLength: 64 regexpValidator: '^[a-zA-Z][-_\w]*$' errorMessages: invalid: Just letters, numbers, underscores and hyphens are allowed. helpText: Just letters, numbers, underscores and hyphens are allowed. description: >- Specify a string that will be used in a hostname instance. Just A-Z, a-z, 0-9, dash, and underline are allowed. - instanceConfiguration: fields: - name: title type: string required: false hidden: true descriptionTitle: Instance Configuration description: Specify some instance parameters based on which service will be created. - name: flavor type: flavor label: Instance flavor description: >- Select a flavor registered in OpenStack. Consider that service performance depends on this parameter. required: false - name: osImage type: image imageType: windows label: Instance image description: >- Select valid image for a service. Image should already be prepared and registered in glance. - name: availabilityZone type: azone label: Availability zone description: Select an availability zone, where service will be installed. required: false validators: # if unitNamingPattern is given and dcInstances > 1, then '#' should occur in unitNamingPattern - expr: $.appConfiguration.dcInstances < 2 or not $.appConfiguration.unitNamingPattern.bool() or '#' in $.appConfiguration.unitNamingPattern message: Incrementation symbol "#" is required in the Instance Naming Pattern Control attributes might be initialized with a YAQL expression. However prior to version 2.4 it only worked for forms other than the first. It was designed to initialize controls with values input on the previous step. Starting with version 2.4 this limitation was removed and it become possible to use arbitrary YAQL expressions for any of control fields on any forms and use parameter values as part of these expressions. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/doc/source/admin/appdev-guide/muranopackages/package_structure.rst0000664000175000017500000000215300000000000030077 0ustar00zuulzuul00000000000000.. _package_structure: Package structure ~~~~~~~~~~~~~~~~~ The structure of the Murano application package is predefined. An application could be successfully uploaded to an application catalog. The application package root folder should contain the following: **manifest.yaml** file is an application entry point. .. note:: the filename is fixed, do not use any custom names. **Classes** folder contains MuranoPL class definitions. **Resources** folder contains execution plan templates and the **scripts** folder with all the files required for an application deployment located in it. **UI** folder contains the dynamic UI YAML definitions. **logo.png** file (optional) is an image file associated to your application. .. note:: There are no any special limitations regarding an image filename. Though, if it differs from the default ``logo.png``, specify it in an application manifest file. **images.lst** file (optional) contains a list of images required by an application. Here is the visual representation of the Murano application package structure: .. image:: ../figures/structure.png ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/doc/source/admin/appdev-guide/muranopackages/repository.rst0000664000175000017500000000332000000000000026600 0ustar00zuulzuul00000000000000.. _repository: Murano package repository ~~~~~~~~~~~~~~~~~~~~~~~~~ Murano client and dashboard can install both packages and bundles of packages from murano repository. To do so you should set MURANO_REPO_URL settings in murano dashboard or MURANO_REPO_URL env variable for the CLI client, and use a respective command to import the package. These commands automatically import all the prerequisites required to install the application along with any images mentioned in the applications. Setting up your own repository ------------------------------ It is fairly easy to set up your own murano package repository. To do so you need a web server that would serve 3 directories: * /apps/ * /bundles/ * /images/ When importing an application by name, the client appends any version info, if present to the application name, ``.zip`` file extension and searches for that file in the ``apps`` directory. When importing a bundle by name, the client appends ``.bundle`` file extension to the bundle name and searches it in the bundles directory. A bundle file is a JSON or a YAML file with the following structure: .. code-block:: json {"Packages": [ {"Name": "com.example.ApacheHttpServer"}, {"Version": "", "Name": "com.example.Nginx"}, {"Version": "0.0.1", "Name": "com.example.Lighttpd"} ] } Glance images can be auto-imported by the client, when mentioned in ``images.lst`` inside the package. Please see :ref:`step-by-step` for more information about package composition. When importing images from the ``image.lst`` file, the client simply searches for a file with the same name as the name attribute of the image in the ``images`` directory of the repository. ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1696417899.7251806 murano-16.0.0/doc/source/admin/appdev-guide/step-by-step/0000775000175000017500000000000000000000000023165 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/doc/source/admin/appdev-guide/step-by-step/configure-step1.png0000664000175000017500000006203600000000000026715 0ustar00zuulzuul00000000000000PNG  IHDR*sRGBiTXtXML:com.adobe.xmp 761 245 j@IDATx]E- I,Di PDEQOE?{; (E# H{&fݻwf>S̙s<̩y߿ ݻN͓M-mK۲i," " " " ]@MMMѢg,V4 bֶ_D@D@D@D@D;H52L:e:/;ObiL{1Y}+e9MynR!n.ݖt%^D~ BҴ-ͳMAD@D@D@D@zTg糂=b?aMWc^p1=Mv_NMi^D@D@D@D@2TħH<1Zץ|&M{4΋|_i&ۺuܹv<]\?" " " " "C dE/Z}} iOr~. ŬNusαSZ~ziSE@D@D@D@Dlbs̱nz!2dH\ }ߞ]_ tK_RM@`$?ꨣ?Ë 6z,>;NCCS," " " " $t:Ǿ/w¹+46ZX5{.tC رæMyv@cݮS;mk֬ɛSAׯ_o{C" " " " "  Fky|NuiŸ1c| {}jll}TСCmԨQQ#]о6kO-鼋l43͋Onީ./my|ߑ qtwV," " " " "P9Y#d5/:zuaAt kT3O𸵣%Zn-Smh;B;]WjD>dCghYD@D@D@D@D\og4g"g3,L]D@D@D@D@DR-ŴOӤGˋt|Q," " " " "Ptw1i 9;zLB=.։Tnɕy鼯{YeƎlFZhB׵E%YD@D@D@D@ qF?&NhuuulٲŞx իr!V__nOv:ijjj[, |>6466 3gYj=immo3;J!" " " " -شiSh;vy }?n;w۷S混uɛ iRw.SjmM^jjǗ8J/" " " " {Xl!=`jϺTv֭Yˎ[2-{%?)Ow(>MS|mm]J6}6OE@D@D@D@QFڵk#Νk۶m&}Cʞ΋KQƎuV߻o=냻iW|͞^Rj3~R|r{GG}4xvGs=Gʇ鍊n9SJuU?:SmC,ɓ裏>͙3'_pֻw;沕['W]z!{b/Ծ<;Kh]۷E/*~}/~Bqxc>8$H;0khh+M%V{^ ]1AE?{4X*q)S*擟?LyZ'}|} lVN8F׽KR&~WGWU|=y=nާ?{^4h??i\T=M)g>g ]zW~3gpI7ZGq1=KFK;wkӧOg_ۅ'ߞg/PǺk>A7;xYnO>dxeE~)" `4sSs=k|_A:BܸqSO< /VR`ht\XѯL>nK~>_=2}}m>Nb3g-1\w|N%G9w.ۺ1-W_}u^sҾoƓ;wNt(!u@q2 o~ ?۪ť֭[osVtgч/;+7mj.[t fʃ rD~6}V͛7/Æ ˯_~}fn:6 'ԌT/-.\>Ntz}pn0^~T۱sB&[߰'zk~j}'/9*UW%yG?Q3:*|'tt'^hw[Æ Ej՞o ?Y:4n-u>l\"$G̅x`u-|<>?_rӟTj1+X!%|  ;^)%\0_N(lO8m 0:ĨŎ僲\s꿏v9)]S2[r9õ ty1sP5AX_i{Z\[+1L R3Z;_(Zbm%Ý a֬Y{%.oswyCOCZYҴ|K4Ϸz<W/BdK˵durX|~VՅn_|8ëlGM[段ӏFjk7o]ބ^=.ᅇZv޳sc߽yIxK˂ D^$٥^%l/k_Z|E4 I'dxe]/J^&BϷmƋ<ĥG?QxE,.*^Jַb>/&`ngcqI=z?y7ݗ4ˋ?ք v}iyw_}nLDZ7`nu{] tܹw;vlw?묳q~s]97^xa\|ӛ_yKmD1ct}/fZ r=p`4s .d_~yywGQ-fX.p脝wyqR8g,Y#̍o|viuċuβ :(;믏<]#Rk߂ <)!Cʪ=]ڏo|N=!>ppUWŲ) ^7Rg};7%i<ɣ#H~sϛ) O|qXikST4np>Es:~8!o5/})2ho `%p}t\>[BS)1|G7f̘ȀsG~z;h=vs0ewa1nkYW>,B{&-V1bvݽ.iâS,_>Ig c3` ?]gu5cՅ}kױ[m>q])p3g )`I{Bk_Z"WRЁ$؇>o}kl#G(DCEZ1K뼓O! q㾂xbsF0>:Bc-x>Ík=^BqlDܸS}щ@yg.|qx2>0:?0aWfϞߵdfe!ocsnt?{r}(/. >~s5Fp|X^WO~Cp!zȃ b'p.87z>n#u^{e~yx(w&Yj! :t \HƂpEl@;Nz,+X!7w\nz}cu47DŽN'E0aB#+( 7`a"e@/ۧO?=a{*[+boPC8ꨣS 湡v/O>9C$C B ]|%AlKӟt.yxnrq]`D'acq%L6-.UK+;'/\?b_FGW6<31;`;/}iLF'Љ|3L)8<" 0ӑۥq,1>ej-0 7sSN9%η>16I}9^Ӂb& s1#B=@B.1ȸ+mFB_t^kveA^uD.('َMRyO[J̐<+69l^c{sN>>o膳M_l혩Ǵ;nY,a_; fr3pw{T59 b N2%{o^eBlzCCF4T^l|4cTY)Owyg޺(139 +ǭtE8sy'. pBQ_,ޑ*X.C[aM4){B4ʏ SM@0/Ovq%򕯌_zr 'M&=ŷ[ʹθn<_[X/Q#pNy/-xZV;\wُN!Eu'u ,\wu:p׆܃\pW&~#xlS3<3XAEGۂO̰TюCyy@;׼55 ˜{Xo65=m{2ci,5Η鸆ŦByQ^3 {x{Q(b=fox7~cn&!!}UnY;Gۉi?憸 Ar  4/,\4POn1 ._6fط%Nݑ@ݺ֞e(' (:0" ?@"Olߖ~ *l(wl>ԊU| 6 GueJ c7θSR_g#x2AIH]E" }?Ύߓ?\__g mgB<"GiHUJǁ<7ǏՖKyZNM;l&͓_J\c4p-yHUQ:]~O 8OpPɬoƕn`bD*6--m {-w)˕rPˎfRӛ>{yОG';tعom7ÍF.iSB[tIiR~(3uNJ߯_{{9󟲚Wڄq[.|Gil_~kwx2i̬i%V(>[Qc%=i`1! ͤ>X 2]v}{Ɩ|қ 5,tܭyX_ .l]'~Ĕd\)+d!Ȧg!7B 7G\8?ʯ:aSiWUPgб}ذR9>|Y) E~CҤ Cg_c;0s~˸vmtF>k6Mv==|n_^;t|:u!]{R{玢"tGo 3;W""CUSo8{a#ۡ_o !^ld˝n \j7iV>ŵPnT}sr_ L:Wi<RWҷ,~ ^:HMz:u} `]do!5Al򳾣};&' n<Zslo_6}r?X56uա,;.>$޲x>^4"PI#5~vCOڿ{ydxUvcc|;ҏbK>3{wvh-h ;LF~Ϗ-7^|/>yoV53z 6f_P:Q ^r >-[|XG:j,~}d:L "az+?}(p=Q-xD>3t3<{ ȇY#vrK(4 ~EGPĂ'OFS8G鍙X} w˷ iiQy\N;4e%ߖyܯ8wL-y\-܅#Òz}%.qZ7o7?=}ӖSlo!"Ǚ/1N:#>>Fl+c< ~sMcEeKUJE.aj> mh.Oh3ҡ8s^:xe/mu#Юu%ԥ=珎!QCU׷,G)q{RWα?æL-ӎ;xh=t/˝#mƝ6*L4}liIgźmq[O8[%|tl76: h4faen>X <EnnB+䆋v0Ǟ~017:M''KFޟ` ۸`ͪĵLG\BAktl I"hx -[5vG:0N?"\xޡ; ZMW}RRpSUoNB+:/t+ݾã_zNeYʽj^Rx-pCx批<~ZS |yreZCl1cI}*Ō~YfF|}w R\~4"=|4 ?yo[OD@D@>J[* +KzɕJUX՞i\;q[ c+T>%֮>g}" " " " "P$h" " " " "PribU|rT4(D~9ԴT1*>9*C@"jGD@D@D@D@D~MD@D@D@D@! _5#" " " " UL@"O&" " " " /*& _'GErHCM@ȯⓣ@9$ˡVEݼ~uǮ**"" " " "Й;:vw欶Zl 5vF$il}ֻ>0Ck;wacCtZ" " " "  wJP<3^w}gM, Bj#ХD֭[;{キu?pyaҍO-X|ӓ̺mݹڪ@ Ί+mȑ-_9sӧOGyfO>h?hG7MFއ~xܗy{l_2xy=/x6lv饗4St6<h ?+2믿޾dz}~'l}s =n۴i=oN2eOfۼyseҥkIC0>h߽{wtԁ+W}rYgC=˂WRt) F!cX_ǎgɍ;ϋ^>þ:ؤaĈ'`a'p\aǵ+=.:ـg"Ё cѧ@lܸva^_XD@D@D@D@ R"޽{ڵkmٲeyA/=y,r?ng\_L*5jT~hN1AӟT`׿W71d;iz߯PLXy@5hΝWBK.,XyT({nk_ܟ~E١Cڸq#/O!9w4*y͚5q=c\n8m gyfwxvEn@[Szh/[~P6N̞>bP*G{00}ee>k֬糔Lc^iD`_R"8??X?!CG>1cFCo׼paD/2>>dש0gާcShc?/>yϋO}Be43aA^l1>*݄t:Oו;_܈8V #/gky@eڳ]#irۨq.9 /{)e/{YFwxp\s?w3x׽.~w0z0y)m<؏4?'}khex^:ßbe'hB1ɓ}C(_|1>)Xzy=<#ƷQN: 7#L : 3 |yrΠis9z8^*4b42#4>}-]q[Nse6)]5P}@Vyڰ^-2+b9}$xG?j~_WurL+O}ʮ:%ƧW FC 1?i? mg΋k~f_q{xN%t2MNFCCfxeM=\hAclGS #xtB~Ď#Ql}FbÆ QV")G6಄+*ߤO~; \#b1pu(s-ñם]1<5TƞWK4c~e(hmon V+F8TF3ZnxĠD@zE0BpM ?аŘ?! X߸ie\yhuqfXlcU;"XpbF{@$nHX'M7[`zqb%ytAqiӦل ȷR-}< L \N?/ n~,bee9h֍Nҵ^)L>RCDž2%SOnn ~:q<1I\x)s5<'CSHXtYq:> _o+Y|^JD~3ν+İf,K̳:g q)&˭ " ]B>vrf.6h ,.ղF!qExl(E\~߈ރB_

|N -BD~G$h>d%PX o?h|pb!l.?k1mo.2EBꦑM2beJ㣅3gLWEa X.A.PaOҬ6ܧ=¾8g]sCwhgԛy=w\cqo 3?bh/=TY,';ß`g=Lwǘa&15k+]]i[FFmctfvp@T#@qA`">ǭϰl ޱQc>C||x: >m bq! u! uSy5[K+ņ+qAb+Ϻ_zѢ_O`cdlUs|j1#{~B9ê();1yY$~(Ym#}5?0+oA3OGǰaБEی/dYG -ýORy֖D{'mɫ-eb$ -+%-ך?>š{2eʆ}q8&:Pts]pfӶt>.];,7pCeݖSRU,={:tioh҉D,{bye}v9=fkm)O Evx/F\dE{2q]b/ϗB/yG)9Je]_*ؚr Tȏ XAbǂbN=k#kO}|:˅D@D`_yp6[LV6Cr/x"qǡFo tєtO%czE>q%''1KLǦV" ] h.j|tՉmdKt7qV/G%:'pهXq'vA/~6DDh;ͣ˽{ɻGYmeձW" " ]@u|Xb„Bapvm#" =mmm m!cT{Ƃ"#IDATe:@ TU@)9Y/(@O!m{œN@Tb(PZD@D 4AhfʢS/ [D@  wNzoے\iE@D* >B?(٠ " " ' }qM5S-@O =ᬫ" "P>lZWR{@W'`NVvک" " &P"J-!|D@D{hFvګv" " m#P"mPj' $@7! MN!" " " " N@"I(nB@"HUCD@D@D@D@DP," " " " ݄D~798|'XD@D@D@D@ nr"U p[n5>k׮||ccc\GN;v}TZ{QՙOq&3Do߾yqb߹sիPV" " " " " Nݻwo wbB:E@DPO," eJӆcDeS;@&-dh0vs‹@GJOޕ>~H[D@@zYeVD@D@:@prcUmbVNꪫM@UH@oc[Y^XUuSaD@D@*OzD>.9͊YWdS)(Gr- 3qONTy=T<}CzD~R_| r]U4)fE@D FG8F[C*k.Lꤳ?F|=6.L[E2 %?MxYfMD@D.3趓) w>5e.Mψ%"  <>@loI>n:jb@usX;|nn;mљC66612Ƹ`PEE@D@HD>7-`MU]X>Κ8{:,m*?G>56zp4l}qW%uwM͹MP@ Eh= ?G|D>/\2KU#!sTEnvu,r#:bn,:F*@ zS9fv/Egw#mCmȭ_MjD~4Tx"[rtѺ}n:!I9b,g.A::=h(w9|lCfs_^8*K'Q4 x tj[ata9f\K:I ncnGb}h\ǘ#vCD~eDž~B;[\fXCv7a>[wD@DZ i!D3'ು6F@? #" " @U|^B?M0Ӣ{[:npMxfHhWD@ t-obSrMZHM¾7tWQEU'av^'wǐ&ҒtE{hm.YY<q%{)@sU))b3˜{l5ٷ>. &jD ٣oڵ53\w" " @Պ|7hriefiYD@& }kC " " jV!z͋@!7w"" " D ÊTD~ő*C\_Gȯ8Re(" " " " K@"s" " " " "PqG E@D@D@D@Ds Hw.]D@D@D@D@*N@"Ht.寣@ HW2% ߹ut8#U" " " " "й$;." " " " ' _qPD@D@D@D@:D~E@D@D@D@D$+T@\:TD~ő*C\_Gȯ8Re(" " " " K/Yݴ> P;v>(!" " " " "PCM@ȯⓣ@9$ˡ}D@D@D@D@D HWQD@D@D@D@DP>" " " " "P$h" " " " "PribU|rT4(D~9ԴT1*>9*C@"jGD@D@D@D@D~MD@D@D@D@! _5#" " " " UL@"O&" " " " /g'#" " " " "a[pYvUlY>6l0?~ 4hIDGD@D@D@D@:1YjUuQU!gG>'+Z ԧD~5AD@D@D@z(\tSHwJu.FX{G"ψb&$ɉT5D@D@D@D@D H; " " " " "MHwjwE@D@D@D@D&'RBh=cج;wef '" " " ">Ϸv-4hӦMqmۚo~5: ]9Hw峧@%пg(̝;yvFfA]hAsk{Ί+{}y[2̛7Ϯ*{'zV~K_YfD~gױE@D@D@D@&pg؎;l̙{}(>h?ngug?իWӭO>vBꫯSO=&L`|+c^^zr!{UVHw3r4#P[[k/{oUV5ۆ~Ȑ!ӦM{jjj/ٳgǎb@}}92~Y߾}ٕRaUVH xvo(}}W؅YzV_,MwXgQuLy{}_+O+>7 u l2N~x;%?]@_D@D@D@z:^zE]lH;.\h Iؼys|v͚5q|_vgڅ^h 7t +|TvH`6cƌ<%\b/ӟ}+_;ƍgߢ՞m+^awyg}wI&'>迟ϰ\qC01Byo~xY~{dƍ6`(;`vZهaĠ#FX]]]~x̼OyBaâG#Sce˖@W U>7'.]g>ut/wrUN(D~LD@D@D@D@ rTN(D~LD@D@D@D@ rTN(D~LD@D@D@D@ rTN;jOڝED@D@D@zԝBG";]Ut1Ǐ_ob.X\ԧD~5AD@D@D@z(zQGو#GS~A}!WC!TKast@eRD@D@D@D@:D~gױE@D@D@D@D555p(e)" " " " "Jh"؁+qbyk@Jy_(1 oJ!" " " " "V-ie ra:Ѽ@}ښ&/*;;@Z(@%twQ_XY_,֋@1]l}igbi@ S_.Džȶ۷ڤu" " " " " e@cᾎا%w q}}=裞L@YM֬kK"?S=H߾}k1VD@D@D@D@Dhlv%i.]yKyo<]}՞bv@[h&:! $޽{wlo6m4ׯ_ " " " " "P-[_o :fBb>.~DgDbSmmm>|͞=^۱cڵ+Nt҉<}yN6l` *JW=c+6lv%Km16,{ŠKE Dp=MU|c45S^m&XKYtO]VW;cq:BZ›y˞Ğ&$H'&m:y2)OEEOF[i;zzm}S'Y5XpX)q('gnbbw1~zǣrˤ':ͼ>@~cɖ_riƄ4vL؅}6NgSrlK"źfiZą֥5/" ݕemMh7G7dm{r{rfuK ِ3[-_p lI6r` /5]1ϳ/;zY!l䘉6e[[W/OFmeKsMN[a'16n\m6mw {Z%|0S=ee,9l2{r}a9C6|93x{޾ږ,_diұgڴс yJ'c:m׭=m÷q ِ>eJ!xM[g]Fژl܄޶tx6K~,X}q厩P2eJLM)rR4CmLtn&FOgc_eבR\;Η]ܧiz1|t]K6/k˘)6_]ю,k١#Aʵ:H[^g?!?f7EntcN=/mKfEK7êOz5L> #fXƟ\\6Ssٝ75ԚU6b^M.:+ 159s`fԩqjRBJ"=kgi̼Ogޗ=tW fm>;bḙ&}_ľiK\r=onlG7wn|e5V'ۡOkm}4#9&Yb=cvO&aZQY]E~G(ˁ0uavβ);lPZkl8k@c͛lӦ߂'m7;2i_,wv]e+v ~UO3ldRDZSٰ3mm ۼbiL7nq6nP5{7aݽpthb1n6qd ہSͶVٴ/ OjL[M!*mTwg؄෴1 OHq̎͟7>/yE~'~x=X,_ӆ*䓖SΚhF >O\hrQ1iy2]gw 9iROc<=첯W," =[ʇi}8Sgfۊml\Ч!F| +f[֗Yd/l#l`/ f9{U,@fcDzB-a9Zl֘{l9n-mKpG" үywc΄swB} 3|s7E:}tqn 6lpq Z==`&ܳKsy%Ӗ>lᡴ{!S{e[&aևNTΒIf Kmui[J-=@}>/6;,ÑB!4}xK;rŢ~`=Nk>iC<#᠁s Am\4^.\cF]O4lp Pjaӈ87.INm:0e=~ma6~vQ֯OJoi܎M+ܜ7pPX9yv}lG#nlmۦ;v-^۰aՍnEHsGT;'kg Y4g:AsOLPFq'ٔ["{jda8:~X&]~g(OL-ܸ,MjVHvd|RdxyS~y8GqDX|RQtoY .ɜ8N.O%O4wI">{>ޗ@!24/ϳObitg[a"n{!Yà{lֆE6o16J]Ǖ6e\_ۼm-^,>D}%;5Sgyɻl\|Ye- 758 253 e)@IDATx]5L^BTZee_Y\vqwiR(T{<2GfzNZ䟓܂K.d!`!`@FRo!`!`#V C0 C0 *@Qya)\-LC0 C0 ʇ@AAA$:gboB"1 C0 CeE3&y%/K!`!`!P&D|ȣϲ0-# Nv7aع!`!`! =W>lIJE&DI5 C0 C0+^KIBb:9ngW;!`!`dD>yr]:Z^At5;!`!` !G&ϙ(0/z 8WPGsO:tk熀!`!`@UC $dgS76Y؇4DDSqع!`!`!PPBNp^gzOs'8PyxsuQ C0 C0 ʎ@Hü#?x y&t[%4J#Ã/ZH.]*+WիW?'sgGC0 C0 C`}B JPVZRn8kpʆItT!͓^z{-]tڵk0!`!`!`G`ɒ%2|peРAҠAOCR$#\I5!!rq C0 C0 C P^{5y7qIC}2G<$>zMϞ=+Xv4 C0 C0J s86\;ʿk{'C ;z w_kGC0 C0 C(pm8d0:+ /B<Ԁb ֭L C0 C0 CcõʽC&ѳ9sxw!5`ϗ5jDõkC0 C0 C(plvՅs#Qq/<jj-uH9ߪUgc!`!`e@Æ YfC؇!'vWֲ]Rnݜ0!`!`"pB=zԮ][:v(ժUGd9rT^]6xc)*ʈV#E&t=i 4АԇkUVɻ c/6י;),,?Cvf=KC0 C0 C -b yWr>|\R/_ϻv Ȭo%IhSZQ2ɯç,g!`!`6m@Uޣ^H9̙9S)S=>t#n29/,&Ekd4+7:F*m0 C0 C ЬY3;wn 1b,[kԬYS6l4;)NΩI㱰ZըUU3Ҭq=q=2i3(=eg/eСF 7޽{ˮ*y2X`H}GQkL[Scdjw-[Eҹsg|*??CF @>-+<'T3 \xuympGljg}&?>T8뭷d޼yҴiSw}K@V"1vQ%/|Ehi` ΠжOINߗ5iҤTľۯNhTmZg%{\ _~߿z^V7r-V N0/J]+bAիh#?9?FK+՝%*N eQĿEwX8D ǏV,X Td֬?o׮] ' 4hR 84mTʊf<>Ndƌرc515_z*oFAxw 2 W)/~∟A :d",W8"Ie1,v H/6šTߏl]BsD{MZP T䚟\HPeEk㽄&\_؞&KeK;bi"ov>Qu ۲3]a*BjK7oܻ #Q.-m()~\۳hP.jl#W4BFLfBe+{VǓD̍wJ nܸq"'xb@J=)qanThHO>d>vD Q,{XG̅h0T*cicAEbaу>naqorM74cnԢE yꩧ{i{W",.4C=Tvzk{DKHZN=Ԕnۯ>^l7ɷvꕲ9w*,LC [l0HO<_Tm(B7|t!~A"4ւ9B^PEH^¬]F=c>ɓ'{ ~8 e]}=.\+h, ;/E#LrN.(L)s Ti]ڏ?>b (^kf8O5'_|/Kʝ54%/3v P:\yǹm~P\sE_'믿+4(O],w~bakq>-;=L[~"ԯ.ȷFweb6NZCuYޙLy K' jc@H6xE @9SCz0pm6-%JȵGRˎ:⡚di1c/~Z 4V 7\"maXW\qģ [C#=#S\1ܴ~2eq9] 4.B^{rB`ÀnwF=4G 傖X١/DžPKYD!>ȖU,X9e 4]DpOy>~8/M{ү:H@) A6K.X핒z,z Թ?\Ly;|[x PĠ8s\Otc q~iT >m8I-Tsavh?]֭7m Cpx+O xos}~o>yq/ɞ݊&ܻ[cƘ?%LKs@VFm'L 40hpGQgid?swܣN&t0-kxCRH+ uD x)/Z}<$Ne6>pN'Ϲ -3thZf^N^F0N$KV]w屢@Fd6y`.p ~kSSgM0'^i)֙ /Ѭ"ݻwCKJfQfX~raEeԩ~ |~Gxg䁁`i3ufKQK"fHeTi]Wʈ"aAkuK >iJ'C!N;KHT_ʒ7F4jOS_hتl")MzF2@Q 0@ΣaΥ*٫l3cNЇf0:I֏Ş[^2$wP6-fl\|U8_d,;UX){\zҢǞ%~w?tnRһ]465dX;к 16fXv4{cv‹M¬Ewa |V XxDWGCNr=<ؿ"2ƔxZhh!tt;`N8F_/,bf`D́ҥAjy:u佀njK3)Bd7CF%WpGGwqPV?Z-]lK}taۍ%vCq(V8znnq .% !C(SuAD¬j0? :A ?-Dx M/Ji]5+a ̌Q7T0Qr;fJ#ٻwƔEMŸA$Aۀ4R#^f;餓|4u¥i+:;@GӰpG|&% ,&dm{ym/^;K0ã%eZ{f1ip*״h1}sav^9@Q_PG[eJy}rg/mygO?PYb<[2cGcn}aez3QSC`>а tJ&°d(!6|,6`:ϲ9Ff"F En(i z^ZGloC;z} **Uʋ@#ad V~A3YzO9 mC|4('%=Dӏ=DB34A~j>̆:~anXg>233x]1gz60]9|G%ڇHX͏ƕxN4 dP7_u  uI%lWG:T r*\'jbY!LT豬y(o A[@;D;̬FR?NȺYZ 5-T75/MZ4߉1}sav^y`ŒhH5{}>[WI/t ]O= ydE1aerA{ʅo|t~L:C&}O O!,b:5նE;b 6Vn4DZDE[f5lԭj9#$,DWX҈MgWt8?skEē#vp =afL1 GgLI_$3=5y7W?QˋrIj0h1:mZwID4EӚLˢcUbMa8,w7ѠXWQrhڒ]CD qfuC-ȔE~#} Po_|փFWO';?3x e!J9 DNT,<*c@ \?=ymk4zAV:~iL `jƎR5OݥFWc]_AްY(Ҫ&6N3Z'0hC<*~wƭ,A=kFBRLt[RՇĭ > Sl+m)Gfi~Y׮f(tQip29Z3 e@{u1q|7h|H4bXqxh㨉dఉݕ"x =ͻ;eIkS֋] 8Jvln[nl[^[w!3_wLo|hJa޴݀pѬ%?`;vjLdg\I:s3H+k{=cgQB>]NE(uk,^}|6@ݥ&J[~JSۖEMtbiӢqer̵g17)tQՖ>\}S9ؽdlJ}D~G$O O?HxsٶOržFIeFʻ_h 4jh?%!lݥG{:$&`B!,ڃlah!_tO@Bv<̂ b=6Y f1[{mC:BPu{R%:=$;[Cnnl12ja nQ3$OMvαm 3L 8[ _!B1(cJPnc, % 2w08Q7_|. u& h9զuw,ieW%_澕h!@lY ojj rIc`3a~FyG1m4dG&|i7u7U3RZuAϖ{ = 0Dn#Ǹ؂SO>ٿq ]+BlRc0H w1 g5}A*Q3_hKS3 E5.~.QemzKAd̔y8 Me•Mw^ w1o,\V oF/q_fmCwq3 6tV2@ZF+HG+ɡӂL@J#.YL0$Fۯ @r!a:Sm0qQkkL6;W3 e1̳?G s$kZ#W:Hd*SPhE@'.TI=zt$WŠރ@th%ql)Ge]%`&t4`3Au-C4@0` ]8(3i@21i0M[>#}&%^_A0S E!s-E5j4|c}J`A1>[G4}͠w~wY %0XI4Ki:KS~G54L^4i02=Z3 Auk 9# D&p^'92Bבw_e{f>ꈎ ɇ&.G`*”.R7rTHg$Z}n=g!6^:jُ騱N/W٦/t䔸>3Su['hQAa2T*G`bYy= ZSSwAڝ1d'WaGg`UEe66jFuc K;RBxgxA!`zY౮/,LK=L!X'Ǣ|}<99W('4** ˋJvݪkVD%#46|S.z&y_&aC =l1kݻ6ة3C0 ʏ@i}+eڐRsZ:LseW ld13&#7!`| w1_5R?ed)1 C pb tT٘;nb0 Ե3;7 u~M>4r!ziGC0 C -اM9lrmtulg 2y9ya #>,K7!X¬&6z]C0 C S*>ӄ;C0 C0 C`}B6Zj0 C0 C"`ľe0 C0 C`}BTږWC0 C0 **[1C0 C0 #Si[^ C0 C0,FlZ C0 C0'دOmy5 C0 CEk3 C0 CX0b>0 C0 C"`ľe0 C0 C`}BTږWC0 C0 **[1C0 C0 #Si[^ C0 C0,FlZ C0 C0'W.W5kUP8RZ ˫!`!`!Pa?~9(W7}Ed!`!`F[ 0 C0 C0iz1!`!`!PdM[n]I C0 C0 C ['[̽!`!`!B$!`!`"`>[̽!`!`!B$!`!`"`>[̽!`!`!B$!`!`"`>[̽!`!`!B$!`!`"`>[̽!`!`!B$!`!`"`>[̽!`!`!B$!`!`"`>[̽!`!`!B$!`!`"`>[}Xf:oYò C0 C0b@E"9ҺaMy쫉Rz5٠V5~FƘY!`!`T=W2-^Jnzw4>Yr\{ƲcFynK!`!`SVR"iS\HUk| oH})_FCYɤ9Ke;Yr`)6 CQFɴiӤUVҡC0?:3fL2E6tSUߥe˖ұcRӠAR]K.]iӦ~YxlRTn5ojNSFjʛ7r/);1!S?M˼%+Aw 7z5m;5Ln:skO~qaE2~6tmYW.8\y=t"Yttl^GԭI ek7wy+*Zډ!`e@دq*ry_^F0ĂyW}9ҟK㣏>:K;VOp]wt_y%p >~] ;uI.w-ۗf]5*M\ |liA i'UoφJE!>\ezJ>rۣ7\1rz۬{>'<(-,˰y|>ݘrc}Ƚ!`@#'z#^ҭ[7ygO.u'O~Af̘_ff䧟~wy9ܝ4j;m:6ݺ79i ;s\e&c=״^ ;Vq켤{CԃdZɃo" VM](?3PlrTa'K/8|f&{l C(v~\X2P%Gu˞v;z믿A.\(;찃\|RNI)'蘺{fɪU>@fϞ-7}W>lyW^A?# &Dhqѣ\Rz)yd֬Yҹsgw-Y9{֭^zI.q^fM{dvD><YPe7.LZhL?S~r뭷z?}뮻Nׯ/˗/G}T>cYd7K*(tg޺UGv0zd3s9clںuL7a2srg F>v}3ih?:ۛlڦ&&=97v%ed =6n"ԔngJ}gsXrfͽ3-^;r?k yqșn$ ktqg΢ҰNuc&rNm_NS?~ROڡLD3, :_d0{ gxOt5|.t0uOdJ͈9eK 9n N;l0٠fiRZ:yRy͙PpumkA7S@Y4S$jkn{EJ}CT*=f$|'sԮ]ۓi1>|'ِО={zM7e;*A%cS='m۶;NƎ=ϟyyg=ɿ;7 ,{mƇIjakNN:$LwF--ZjLJN;4;w'Qwx@#&:G}|7cyn+(5'7; !`ƫ@/}m Pڸ-h! kYwu$qnMcd,mrrȐ!CO{fcN8{g`go6avná@ s^g߷gSEx,\rhL7V74*g088߿y?6-Gt vvx{v7hgLwwKNK76a.ܬ97[{7!of|DGzުQ6wުߚgs*>g೷w;n:r$1iK~:{ u3zǩ/u3 e8wvnvgO"&GnRvw-JW^&!PT :T4i<'$ 엎t=Wwzd&`ٲe!>~{IJ`ܨQ{,oO/8G&LSӣFN=ĉK"s1Œg|o enݘ+L~A(Cz:v?UNÍ`=n0q3Uq9#s 0+ȫtt"|Ȳ!~ěI؅rʎm4Ö[l{ӻ3GTP|Ҝen$h俅3a<&h'=]܂cc~#ũv1^,tF>c?'g(A1c~&!~ y뷎$p{pC h1;Q,w_)MÆnGl*0pG A/[m2k!hБ⵨^+{!%۳v9UlP;FX\E7AS}֮ ;`Lf(;s;E1&*z%LA5 m1n=tvo͛|P$&tS9= ̥(KL]@G{4Nr'Z.W_}~Qރ`#؉c]:\%suZjI_Ǽ<]tT@ΉY?{U"_FL&Mjg$`㎆E pub/#9sb v.]M~xUwYO@YoȆnm ^$sc;ml1Ù&NC}k-wvMbxhxhC0YQR=mU9olY,tdӱe#ȎT?,Es߶q-2oK'tgk#ў2o7!mX|Ar Yegs "i@0c㮻-9Ğj׿M\ m9nCc P7f>Ė, E 6 *fΜagl#,E4K/ԟK qX|g0M_E63žv ,Bcjz5, C^;^{n_͟1AUv4 tf.b w-_}*GU nA+LO0QRcs;xȲUo K\njeM]qH=vzʵn)}ݢXH3nW ϶l_Ž6,EϮ<@1G1=mإYM^Bٞ!I UH'nL?\d֢?g m#W}npYC1贝ڦj C "PpWLB8b7G%EBbĨbnGw&&jNz&iy&yb'l[Hb[9D6D[(N=2=[$MYmaU LL"o˪]@k2ԑv{gCF%-淾?mw&!Wg7y2i u( *BY [`ĶI?  ,I+[ J1kb̽7 C O7ou\&5qR0jZ(~MVw&~]5QdV9&ztR-$aڎCB7t&?-& C*UVn'T6:ŋj;٦ wj\@IDATgQ]G{nxV՜e)KːGJ>vJBh(!=s(KQ{ 2 ʅ@,Β!SswmNZ0+,CuϷu۴MГ{|͝0 C"/bOO:/onSL=w`ZCuss71 C!#hQNIbX!=o3iL C0 <Gįrg]^}4ޟtj 0 |G8s<ٲ*U.vt]c;{-}!`@~i=Y/̻ wĞ#_:ۋ=`>5 sJmr}rx!`UuI5gҴ.t=,@Ja2 C >-ui8w\k汯p<|ٯCݻ@j%Iy7g!`%V:cyBGS(Z{q={lQWr) 0uy}uFXHk3c8rK!`D =(~BeD{rliXf CXͣCA[3>\~}GC02F KBqt'7^O n&!`/h,&mN/8X> C02C ?::V$.S31 C`}E6fo*P B64kհ|!PZz.oS܋}a,Q8Rm%z=h^`ٕ!`1E [!`+60i= 0:v(-[ѣGKe 7)}SL H.]rz*mZ°*|2uTy4_~E:t 4(p*wiժ4iҤ3U/^,`6Hj]Ƶ&HÆ S%K͛'Nm۶F%|f7#8A𱏮H~N CXv }/A~I "97;x8ѓA<%C04hخ:m䠃GKoNoƏ/g}WN9Ox0j( 䧟~O#S8 Y .!#A/XΝ'UZɄ < z}o;dW_-1ٳcB#s'P=vmѸq?|1s 75> 0@8! &srgRawN "lǔ{!X{A-0gC&l$Y? sٲeo?k|azǼPB>j;ewu3ϔiӦɈ#|ϱeuoqh{Lr|6`^"oA]y啉V{ٱJՊI|6 Q_4gj8Nꮺ*?N$!zSO2?S~Ax'҆;`%W_c  < _ǀup =0{yg|M@nFiѢ@_uO4ay,ћBs9!KHiK.ݻ $mɒ%|Cy /ӂ'z»f eʁJhg!w@(!H*L?6rH_v0^}{CU >~3Lΐde(az7KޢfIy9>0!RH$y~Pۮ];_[d )}oO@E6 ޣkFXÓNR"(xπI^T4ȫVQ߿#F5U-Leׯ{pL%M6ϧ c~*kZlpܔ=@FHjb˗. 3[20 \4hZD@;Dܞ{E5$M6RV-iڴ'8)+Ģw^So6~7^+,뭷E!a@D 2pФD <F+U& ŲIE+3l7+v2`a&*->A?M4uJ*yAڭ[7_נ֨Q#'BCyO>>HجYL4E)֭[{<rG9Ae&|ChA~"0FzPvm 0ԩ L d/TB0cCbht5n )o#H2J&%pANQdnݺ̤0c2:* bЃ@y43ՙw5}"3f6L$UF {`:O{Td 9,U#eۣ/o=#5jՕ=vR'%V.o> o+:Yϛjr+ͫ;?;:[.2oi^Kߨugo_?wro5~ UNC2'փK-gۿxL+ҲҴݦ._3˗e%͑1а_U+̗!`Au2{:lWZGj@zCa$"<3Fzq*mD hIIPj֬w a̤Yf>QٳgC03s tJ ANK씙Q E R_&;B=@ SW^z1;a^SatErGYfH^'N#' [urش笉ýfF-״̙y f8;þ@mƳYbL\r|1 C @k昅q*@:!Zd-)&U-|3j6ϸnܸq!U B7U~~>]H4wtl!Sm!c)G8+!>#"R=$2a1&CQr.TOw]v$VTJT ? SL$L][bj=cL yf te,hF1^c{gfeP!ǖ0! F ( $?KS#\,F%݉ʑ3r0?ީ3gGӉ?y`}6aC/Qu<(xu`ϙucf*zOUP:j_x~RohLhB^ l_Y-<{f oR[Lt0ӠYܾ kY1Iϑz0kKVGLqF;@!&2*쉝N ,LCvLYwWxڲC=ԓt{C!￿W+z(` $ T0/Pf[gm:zAǀŪØŖb.-Œ iü,ѨDmx28-svbd"/A@.M/Tŧ ݡp=kƛD&e >#U?&V 0bDZNtdLǰHٰ` 3q O?r0bH.y%&yŧЫ%jC\Ch**hI[/ZPX_ ;UG}aF"FuF#Q3hz+ukd0U#39?FLgRe-k\n@]#q!ZNTsɊ5rϷsCNgOP8,E߸o_A:KZuoEB׍='o^o{]]6_;3=μǓ~/(z4ﴙ|a}c}Ʉ߾Vݶ;ff_>MNF;-=0d' d3tyD)ܯ8v? Kd. CR!7?vh$Hj"5 Fqq]4&g6SKйC +KQ͛3ȧa}=mDasAIMv^/Bd.-Qɮlf]u_nMdq)?SRa&8B *) ӏT5dGnC!ЄSW|G5TPG Թu) (Q.@£yw eka` =i;9ǹH{ 2䫧jE}AI})2=f{7[@bFwAAA4nGZ;ؙVnqm7g+TS}?yV| jҼcolM CgLJ8DJb2Rd?D-\6$ti ݦ:u*gu:KN{0U^%NxLFeÈ'"Q7^O7*>: T9NtMzҒM'|݃ nN =Fz\,X<5٤Sw6{/_ڞXgWԭ+_$5ܖ _W+sU&]96@Uد?g9]W`BۛY%JC=C" %FA9Wj9"EL%A31ʼnN9UD+2DQ;U[:wBʅ-M=7 CX`$d!P슃N0 kג|! ygcL^ܪ\J%`n C _m ; 0Bke!P^ U<,!`@j_)!`kػ+*ŋ4S뺚X!.@m u00 C bMۢ9gSBc@<НFķћe|䫧|픝D2=b\QRtg\ዝÇфƛoA~"?cK">W >Ԟq]-R$N C(ghi }XHrZ>g}6<@ITq>V˔J*)K{t//b*'4Q_億ƛo %u?Cn|ҳ!Wgt3{W8r.61 C`}E6P5m,· 믗{WrK9s;QkIƍeرrI'V[m%:t:<@ਣ_`̓X C ]8$'F#?wjȒ821 C`@66ZErb~2`VH=ҴiSyoM&xH=Ҿ}{'tvwQ.R*'O>[vi'>tRFo$?Woh"ev:Hu]^{p;8AzJ:m 'U80 bM7$|ug}&zg!ӧO;O(wqqC9D񸿳:K "^xp KG}$ Lï*hZR; s9#}ᄒ\n8EQ-4;aOǝ3twǯ|xǏuo߾r)?w7l0.u:ǟى!.b_fVȥcui:A66rO[lY"-Z :uj>#'sUW]b 0o ~[^uOT!C_-gy7Zr s==="oRH=z􈛫@`! z- In +ʴid֤I'+瑛<.Hٳ_R_~eN8Q@lf|0+TCfx?ԋ.( *~pMKtt's̑.L y+= vrN~y.]wŀgM7G# 0bp OۮÿKlaGC`] ?Ğ^У.--rj,G/1)!`Thihhhi ii}SJKO5YH;t{Ȉ#m'n/UzNb}Go.aUt%ц eF, .̚5˧/O @ m/~-ݻs=Ү]kDKvSNmé<YfFO;ꭄd3[~Z@p)k̭ԝm۶iӭ~(gfc|s2PA QթS0`d`Bƌ crl0aAdM %z⣞0f7o._|'& G l=}!-#[:u߶/MY.7|>[f.^U`Y!`TMT-j6sOu\r_68řyBߧOde]{nj P(b{XgCvnTyE]_uXcfY-U=?7S$;w&;-+Wžju9S;?!`=f.hiF>w/`=֭[˸qJD5 /: 7|s6]}5 Q\csj lʼnl!_`!hxsybEN7U~ACt T0Ay\PT~G4|d?VҸ4-[`Jֵ &zlҤ'}T6m-,f>&O ^?pX7r}Ń=4#:d'@!ຑu/uTrpTw35\oWjU-펵 0:`u2ԁXkbmo\G[GGm]["m`=(_FLa0CdJѠ=?s{CzϞQ  VQGY "Q޽JhV! & aL4TMwc?wђCN!Q_tGC46ΟYu:U<ޯQ@Ex% 0˛!Q>P6,CIUV;γI7C` $U9g0k< ~ h!|S&,E`o51fFHE< Xƞ^6{~_Q4HlٖK*pq|w#Bfb@eB@I9M G~(5hk_ʱ.`ab裂y"N ]wr5! Xt.4hmyb3$4؏ M5lȑxH=XHe`d 4UՍ]YE4ݔ?Lʝw]q , fq"!͘P`Ϡ0 F3,4Z:tN+q= NCzc`XWʺѽr11Z8)GsR9oq o_Х_g~Lb/`@#kXٟ<&1s<-WxUqnbB+`|{Bk@DX5cNÀ$a0BhfUХT`➴f+aBlH^T$p! !IG}Fؙz},R4ߪRqG:9?:#䞎'8FꋕSޞh]zQWdu$آ0 CЖ*5{exz("7f6cޞ,_V1{LgCS;`Dgr.t$78RT#&u4L QS33 +-'T"0vn@{2w\?5gxFGn{)b0 j#n0 J@&DZ+]!`F o}0 *@\e_erd1 C`U C0 C0 C {g0 C0 C0#yW$ C0 C0 C {g0 C0 C0#yW$ C0 C0 C {g0 C0 C0#yW$ C0 C0 C {g0 C0 C0#yW$ C0 C0 C {g0 C0 C0#yW$ C0 C0 C {g0 C0 C0#yW$ C0 C0 C {g0 C0 C0#yW$ C0 C0 C {g0 C0 C0#yW$ C0 C0 C {g0 C0 C0#yW$ C0 C0 C {g0 C0 C0#yW$ C0 C0 C {g0 C0 C0lS4yl{C0 C0 C(g&[.$Y!`!`!-f-b0 C0 CCaX C0 C0 l0b-b0 C0 CCaX C;8]ZdkN\#@JM$&SNge2LIgA2I9؎$:úVV{VZ2lI3s>K$@$@$@IHHHH` _o D$@$@$@$@IHHHH` _o D$@$@$@$@IHHHH` _o D$@$@$@$@IHHHH` _o D$@$@$@$@IHHHH` _o D$@$@$@$@[$@$@$@$@phhh@OO&''g7ZHNNjmn9xPFWPuqI^n-X-|)(#  XD?~|Y zMIWWAٴiSՇFbldSxa)l!_ENZ͕Kq =;&  %_^ +Wc4<8ZmY]/~\۟zʮ4aƵ!>u> , ͆pDGGVk.;vL/IOO?l}x_<͝1"i^/AXJw!e|?|CV [xu 16ߘ{}.R7#5x7G;8ᄚ#&um-;nSwOO. /-OT1q\RBXl@hD$@$@$@W8D˱~Kz)͛u?}?zoUUUaϞ=gyk׮ŋ/JzΝ>D)G+!?=A xxWpmC7%?]^p| n~x@t ?{Q#nveVd]2^@R8>   & Ko*++ų{{{xG=PTKrS)QP.ݛ|cGZvIHHH`)1.QzY//A<+Q9rD x9WQQ?ZI;4Gȍ7}8p@/Yĺly)a$8z˗;D*MxXmo)Ymkew ՗eVW{o.~_vhmV nFK,=99Y?`! p2111ľc_$@$@$@$SO)D?*v~iQ.zMKa/85 L sȈeE X1P; ѩm80xԏR f|tk..m/RsdG˜ (麀QL{7gUK:a5Ц- ,s:L"789 _K]^ʫIA~v9c=HHHH POHHHHB} PoHHHHB} PoHHHHB} }C13xH+1ȼeB/p1o   I 999$̼_ѡIy3P/&}M$@$@$2NuАxe^y&\1{|e2 eI} "p8i&444˖zԋyrG_ǞgըntZuڕqbN ՘0ՠ6IHHV$+rf"nz}2׆\HHHH* !p 6=     D_"gl\I'f$    'ir-g39V#     i˝3M~ڈ5m`L$@$@$@$@$Dwÿ4xF;aHHHHH`Dw*ˎ$@$@$@$@$@KkU ao$iÿ2$@$@$@$@$@ G`[d$xɹNHHHHH`DcϦM昫 JnGUUՔi     +$ [f,oh7]v.mHHHHH$ [UKU66eeco*Xccccqq+6&    '֢Ek!]ߝԔxFsN={wJJJ33$@$@$@$@$@sFuu5vލcǎ!))i x2k'Mļ) kRRRPYYGb||!mHH>FMLjS !?\'Q}Llܘ%8܅WM8Yۏ$ziGzkԑTuq-igrM/[aMuݰǦQ?|QHz苰c2!)Ia\=k@(0Z诫L$lش3T 4`bX%66:ay[0fR@$@+PG+zқP:rsr.z~ZϡɍĴU@\F$2QXxwvs1GCm=Z;]DlBpn$;ːp5!5Xe`zƃ QpCj;8: eپqU[ 6iKȀ3 2})f'[9m/] -EqE(O8n,cw#r]Qy5n. cX8F^q\ I"Lzh2{.5O+X1D `55Kl1isZGړ9]`y {S&"MKg`MlXIH`eEv$$M"=g0LW>r(rbֺͅBOXƉ=z`KVHɄՆ3;7(1=WAk `@T ΚCOf rm8>uij' tNOH2=m(ȹv\b:Ǡ_b<aѤbuboG3 퍍,NG⢶Tͦ.La_[[ 9f pSsw.s6$B gklĢMlļlO[zIDAT9k_Z؛B9a16/F\[o:[<$@$<4 2 -2 a8^KZMelzz8u9O`ezΩ Y[ / |a&l.L.7=ylןknGat+#-xݓkp̓N\;ԘiÇ5=sbmePKuHvm noVga0w;#@.Yw_eI|^ U8֧կGmg uiPSSkCEEEQ@f~s a !`2e7)x^M~/|[%- ݚr3Ħ-$@$LU~Zĸ,@|ZnщچNKX`H(?S ʰ\*V40U˳':zB4}Ip(#6^Ǫ! å,)J@[-KVЃS'Ng,^O{Z vi+L UW385|x #}:HzG!Fؤ={!!)a͝L)yGE!7#^]Ӗ3on:r'⬆{ZWC ¢Q)] 2շUqodrC]3w?ktqqL? ;9 soz&m%mpɛIs&- ִ)IK[ol+e6C$@$R 56`P}:rP4 7hDejWj+:ԅa@{ZݥNe"QeuCPԉPhDuZ*bG;ԅ7މ(Lh\Ц0:4 DbE-Qc}y@uל"XWc ޘ!\9gaT![yY!&J#?LsO'C+UGijFțXliSߠ\^JƦ@l3bK=Ӧi?o @BCuf*6!Ǭ1O(ag1X9QϖûqGe A56,lo䦫R;p;G23jiΛ8J,O zoD ߚX=VzhڨZۖ=4ul>gΕ])4,LlBUK5\N͸#sKe#"ޤ5mKs $@$ v4nL}gu^;Cb.>BMX3@5oO714:]rsUƆf36߀{OjW"RU]̄84BÛFQz{÷ 7@m=5طυM\?‘jaOJ֩nn/+JGDR9lCV^2&zѡסXծcDr>nv~CnW֨]qPn!GZNԭYloƶ ,A\Ǥ eY rLۗgCi.ˋ @zYy<Kp kJp [Kmb f@ $ GIN ؋B|bܙ+!6O.Nbd3ư,s9G0) mo0 B"HX |^d\3\:3>rJ}=/4y|P."2D6aG2eys܍y֚`g domo=a\o*3F 7kXk8 e:v _C~Z@sp@(q L+ r!Y`X A)X 6J {p4p@7º/ x!!4h! b8 #1H2d DC"HR@j_i҃F"kTTEM +ꃆq44GejA izE_C1f``[`X vk*֋ `q"NǙ `<|^v*ĿhBH"df ݄c## ~,\*b=C|L"HZ$+')&IIIHWH}dE>فHN! K}+gaw(5 .+) ST)fOJ%RA9H9KGyh8YHByŇjTKu*UL]MCmަh4-V@[M=}P+*(q*U)5(]Qzl<]H\eS?**7UTQT^P}FR3U P-S۩vF1R.Yz:QL=D=KTzDU'4zÔaaf`|;goq]^s&KYYy]S+@+[kV}m\R{,gƫ_2;:N\::CzAyu1XzYzNO`0sv栁A`AazF#Wt FmF(dl20yojfhܴYYY=syy5 EnK2ӲjlŷbcMvXXߴ~}kKC5GcB&W&&nxˉܩ鋳sKKMWuhUnn Z>;v#cIfxvMziJm~2bqYY|,||}7߯/ P  xhX8475.fn'6d0%t~h{5,62Qe09Xq/$RBGݏ6Ώm2qrOcctcg}&ny8-A9ajBmDIJޤ I.%k'RH) )SL8o7M=t9OPq$/3;]J INq6q^pY ~',3,ygL*+8k[=#995A}3{z7 ÄEh@s: ?Judl9sVyVX\|.gno{Z ZNhXsrrɑSEZZNg~63'w ;{\3>{o~E׋/5t:uc]] ].7uu7L9yW] vz7nݜz9_)3|w=½* 768 460 AiDOT(!T@IDATxtUՙ0TDbE#IYB]#o`JhP2i!}G2f 2 t /ΊdQ ՚ZPȀX5h"\89s97}r=g}ngg?r! B@!. #Y*)B@!  HGB@! D@ jlB@! @B@!  dPcKUB@! (B@! @ [*B@! D> B@! 2(RU! B@! ! B@!ADȠƖ ! B@+_~%:;;q)r! B@Lࢋ.!CСCk[)gΜヒGI9B@! @ k |W8rZZZ\pQ?pEzR! B@!НBx`_k_ƎqYXiiρ #Gb"}l|B@! @?$>ٳ8~8:::l"t7A= ɓ'k+4x`=g_=QAS! B@O,竮yF࣏>ӧ[oÆ ;E3 gm.BC !¿AB@! 0Jj<޹s5a._PP`'.޽{g˕W^iJׅYK! B@6,;u5\r n6˂|?iCss0?/#$! B@F@lFώtxqW*%K/d ~MYW?ԒB@! +o,}]w7I)oZ[[1b3F>B@! =K@WX>q![p ` +S|ᇸˌ º +v26-덍dFB@! ,o o2wUNXx n2IB@! BPf@&0 s7O7d B@! Cu{V7d B@! X=oTWLhuB@! @ c^*.B@!DV: ! B@d,Q2B@! H@LluB@! @ c^*.B@!DV: ! B@d,Q2B@! H@LluB@! @ c^*.B@!DV: ! B@d,Q2B@! H@LluB@! @ c^*.B@!DV: ! B@d,Q2B@! H@LluB@! @ c^*.B@!DV: ! B@d,Q2B@! H@LluB@! @ c^*.B@!DV: ! B@d,Q2B@! H@LluB@! @ c^*.B@!DV: ! B@d,Q2B@! H@Llٳga|>sG |>n| x/}ϙ3g蜮ʃ#UAv!C̴:sbu&ʡy'lGl߬X "c%,Ԃ!EW=~~Q':/<cx6܍p#u!oV?!F v#*/z9:F©?ꆶk~v%~#01׿#aΩYRB@VȀ2tvv6>G$ƍ_o|(iR{ekk+׿|Vԋ/Hõ^kݗƹ3QO}zjLJ@(,?,{,ȳ rss{w5DS.#5\C69^ɞ{N›oz 2zM<8f+EH'.j> ~v+kn l( f@K/EvĒ(],fΝرc>c@ eeeMy.rcʔ);*px} *N^^M53~m۶gO׈MT><" N'c͡pȇ8YKs1aL sxBb^h~m#h 梋.0fxJFlbk~ɬH?Jk(נ;;pae~o|Hi@!`&}Xz:ԉCoCvhr.k>eQY&߅D*ۯẎ~h|h<6馛8`3 *СC8o6>l:[QzKKR[5X1,|n~Zv)x,lQ [K1){/Ek#Rk#}W]$"պ3jME`3Z,bߧ\ ֗"ai~hq/ƽQ4E:%B4cnj66 zW~[q7v<p饗g Xy{ȑ#KC#% j$/@uN!k!jm眥N;ŞQS:C=lmBEiNԙQ䷒7ezV`k`f8H-+4זy%Ux_#hùj/U"c4X=X߾}aleԩP/>sYq*#<>8ƍOndTnJ1 YqGPbTb$~_QzY 3g~ _{n\{&@,g|ޕ66r9 ?'EZ̮G9l#wIl*żx16+W0N΄f41<YRRf~g9ƠFg9FKk7cMhXXB~jiv`W"aVԑ~6 @,ޱ6DT Z0͹AU8m| $_De+,NcbxSxY;h \tE9NbV$V$sG\|QsK`Ŋ&V ?>lH"6@Bn㓆s0+M+<۬:߹8܏#r}K U0KH1=h{X~)HA65AW"Hk1k-lzEYdO#ڶ(cx/7G^T7bf}>l"yU3V"򅴈\ek—cR} I! ˚PWX,G; f`aSELc=V2׿0<ܓYPJ+sك>=Ryҍ VE({ΐmQ"g! = KHd1C_#Fk΃0A{1=M>Z[*.yEk⑱N%F*kVGx^ gĜUvdEQ\{8|V |B?wmxLx]Ŧf? Ry1Zlt î] Dk bFZ\ ݥlsMdCB2M3XB"Υɚ {$j$DƑueB5oCQo_Eُc$cVhC٤UGpGk SBpєF` ?o-&?0b݉ț ߍxxbVwmkL 4i#vsGm7ޢQq!1$)W,"35b[ 6ߪӻ mZ_-~FM{~<?sZkA/(vIzA\7|92i,ǒ1-Pb9Z="lҫx#a xyh߃ų:I!?|(=gukPqe@#W{9L}Ǵ*O`=nfB+;o5 K32u+ǙMh3c30ۯŠE o}l<AjFWOKRϋIyYP^>Mcy9rpCik0Q?='oCX(BC@^֖C`/@ʴ+6"T/.j7OS B`z9TbmFSp  )@fVhkbqs9LD?R@۱G)k^<B(ERi- y&T86sUHh}b-˕Bo(.8A=wľm)PT}|d r-&OG.IݡvYLJ7-_H]DSI]^$ꛜ d*f_eΧ=p`?+! @$ @/k6QDe]'D[~gWCv7?V|p ퟟ60؇rrq D8=<=L)n(؁#4|9_ಱ1٦VRB@*O~;v s^xmW?{q8Qq:#ȑ#q9+́ŮjF`:&vi苀?L\/Bf]VYQ;4%1 _!_ן;)̖9[o nI̘{p8V7ؘ%rpf=x5sҤID0@\.&}.V4Gj3Q! ( 8y7xo!0w僅Mvɻ.,, s#G9ΰaÌ8pxξ,2J?V,<PVVFTYKF_|W^*?wy27.S,$[1aUugOg8|2og}#ಱbupSe3!zy-fW\q`K/ʹ98ɓq]w?t%L Ёu`#(DMc^K CmB@!л ڃMLX x<|=[nk.Ot w}G?/ >k#3*m Hѕw%oB;׏ʹT9O$}r K^΂;B@Qz3Sg[qv;f&^f=,XrXCL`ؖg»lij lbikxJ1cz1?uV6~s4ȳ!hCۙ#洺T!; ځ~6O4al^ʟ!CX97.*ëbWg^3W樰ɜRdWan*MISis4깜B@!D ꒧B@! !xV! B@Qz)B@! z(=^B@! =A@.y ! B@" @lB@! @O'KB@! BC%[! B@!D ꒧B@! !xV! B@Qz)B@! z(=^B@! =A@.y ! B@" @lB@! @O'KB_3hlȐ!u; 58smnZ!ILo;t"D'A%!]K:B@s Ei%vXٕn _ 8n.*4lp\$I ά`}ĄHk;P #`u'V!OԣBs8wPB!;wH$p7JB stqLj\TUEޡQ^ߨx )D:AX<L3%l\z)#{<3+g)3̻yL7lryЉsgja}W穂M% @m8)MBm;؜j|(T7Ƥ~> Sݺ3&#Ek^L혺* %+i.)P.SG2715^|||@?&pp6fe +"0ml?9 @7%[gaȐNJH.:ONAgRj%p*54(2Kb//ҒB-Lsun(԰ϏMϔ! ugYQVh6цi5xF_>I8ԉ8q4B1tHd6|tߨ9>$҂7 WQΎvFˢ24dg#;GPmI*dVJ*bY ?D[ aFcLvK&wf9CY1bH .cLUmF':mb9lȱL} JT S o|5~3VVzoZ[?Izgԏw4x>;GюG`cF96#q.|lVUW3\;#G]1b6.ôNŊƧP7m% N4O#Fo\GrDІj tWe9G[ß9FΕd hcxÓri`+Piv{6|Ս/+# Rm6zQs XR^9v @'5khi+oz n? 5+⌋V`vn[5XCk Ӱ^&ؽq5~^e'<[sq"6?ښ@8"^vjCݣs͚[M![KW`o`i!|?_^Z1ہ65!%(6B9tFQ;[q7EΚ+1'(_ZoDTl?BZW9X3myeS;L~'XH w+g o&=)7֞/+iq$Zܟ+Z<1{(vM@ѶZBƥ~~^w D T mtԄЊ[1$VT䛱ϔON*ڂθ}3Pt9Xe͝cf}ls.#zD*0%<SX bk0{}Of-5ye$(5A33.b,&!?Ruh>ՅK.Zyeh\窵-%+6`^Uo/=׵&PEzފK;ON3[\ZÌ[uNt& ϔM I]‚Uq)`pȱ~e {_B}7Zf䒹y0Lw™ᑩ|(hY{ eU{'b(N޶mU_0桴|- ?n0K=@#jRXSh։moIEbo0M Y;g/*f݇kGP)ODit:XeI(!xBFSyrUtf#V>e;|])ŝ׏ƙcoc˓hEU,v|JϮ;aS =k&O!aQYxf*$%v Tht_c<|F?zm[=HМZ9y^Geˣ(!o Yga 6Wt4fξ?#BaK24Fsq3xh6I)l²a Ժܕ5-F[Ŧk`ľj( z{ 䡬{r֩w/#)[IXP~|ǥQcxi{=궨fۨbwCEwS!x2rާʐRW=3Kpޟ%rޱg!@9Aoq̀P4.6cXc ءPdTf3{_hD`e"pim8I׾lTGwW_gI6^z).J1v xNK$\pPJ5gkvl$a.&lgkӌsK7Y҅PVrIxJR4 R3x;Xe yAfnj!i~Xp4*%6YRl^=0gp \yXi5Y vJz |˛ U.ߩ$c:20\\h֒y33h4BY5Ն쭙)pmYY}vD3]ri)T1ZTIk +riͤd.PJf._8[mw,gt\u)Xi"L5_y%~uMŠ*ikԆmH iv #l^f88كz0v.gt?D ԰G7u#~o* k:TW]Lw"}jXizlɻ^>x2"w;:vQG2ǬMK2(ǎÛoi|>n׿u3`Y:/+gZz뭷0n8.Æ Kg96Bsd3օCHBs`y_1-6!M업ث?-d;ߢ-ToAy?v; X%̐Y4.cPcKaZH|%~&۩լM HW_Uax pÌrL#pdOfLG[.)MpG<<,u\x6pH3(Lf M7{|49Of{Yѕ)Z?ITYK[ծh<"+mxx~X)-@T3838G~Uu[wmK漦|k6h]K؜NVT߄+Wd'H#MQ% SŸ&9lL5#0H uOG:S(#Ǘ M+T26[_bQ.dL}{tk/q-&e T4QLfgCˡ -Տ˦:'ވǍuz4 rV7;1eY|zϺ^ {=\@ UatL.7NcɻB QbI٩S.RQyԚG3t( 5E9 H4 ---l|oqo~6mq.v~m۶{Ǫe]GFH q4\ÿ\-GCMh WGx3sVŒ"r^eu7yÑGOxyR{8^+׽D@ykf&FW߂Mo,/[C&=eȱ&Em=&`kfo}VǫZdᙞqSoSI uIQhGiEg8ts& ZZ4j^ U\;.8p_QQ%97ʿMhĊҧͫdWX^JY^s]D*5x ONx՝|DHChnnÇʀB^^Ək+qѣF:ۇ#*|8s_W snmm=/ @w/"D{f'Dwڹkr䗑+J?>b yȻ&d \5;rj>-#h#py gCn8M7f75uhENk ꏪS^8׊ÐBSOCAjId  +iԽ{1%a:o@45ʹb %B.@Ez%GD8"ž>ɶ| It5B5rPHҩDoM#[Nj/MMcgZ=/lx5ш@[zybG̀R}/h92 MMd~Bԃuf gkmu1<˟+GwڅdAWÙe ~h1ـ1]@_+*CLrSI33cϱyYAʣJvlS¼Q RT Ц8L $E_/vK們~J˚;*Q*גmͮ ,lxPYVZQ^'4Rz \rN/W1K Rr':܉2McXh 9`{Le2ġ» Q0Dl ncf.f]tEPkF>̓m=6Eko_ڎ'ɄMw]ɥ ]1o7b݃ؾMQnQ3(ihxVy̙3Ƈ4h5fh #*K.z|hO>2aCa3U? &n%!/4/ :`׵&:ԑǏpAh~mh|+>{AqĝGÈ)^ zYu2`쥈SG c,Cg=t/@7M']vl\SnV!l ӌc 9.V$zsf`3(҇& -Jb/TG'3GiѴѪ>r \#n"a.,  o._rjj qrv/捷n}dmWra } 5Uʫ!BjOsNvgMw(Ɨjڗc5K8 Li ݷn!*~D6<YP7 .l¶W_c?ou]5Wiq:*-% @t趿n%8{o>to"w%RǞ̊z#R.^DK٬2G'%DȞ7$8*a(iK|гRBŨ߹c( [K8Wt Fn$(7Kr~#P.1ѮB* e՗M‰87ߋ+R(aA!bUC7.FE RD8"p$~~ עj-&&P,5 K%4@ݺ[ף ϔOz_X7nj}#7n@Ю3?eG[HՙyuuW O {}vyAI+/^a!M)ĉ1j(w={v gsV,TZ}5- (َCY a œJ6/p>FdΫ[UTؗ>\Thh45 թN)se+DEU)sZ:{0(榱E%Wwv׫cqd={_4+(ô ā6zkv(Ccr{JČRD n[7_EGцkhõvMe(y8($Dr u K]U U/?#,o*6|y,s3>Tӏ>\t@IDAT f;6cII U鞾@{3z}o5 \v e mꫯZ]8i5j;rboAΝ3]x{ pVy[P4RyF Vq9i?^*?>'{G+ o0e+d-R5A&@L9^h>m@%yzA>;oXs4_eTftgWG+oӶDݞ߭pLl3Eԣ~RƛIuߝo`ETVj%ty$G0-gСaO fdExQ`חbRΖu)Õl@Et:/n]4fi (ӬQ*d+Qm\> rm+X6KเFR*vZSpw+7P)*3$3nϺ*hOHUO6˫qdT;T>@w9=Ӎ+Gǎ_\edr{-ٺY*WvtmĂb,UXm _zcY"OIe$8mxUBLe?0Nu-Ğg77"|GWVZlh+!iJ9>YTNc, b*`QƬ)ף2Gc) }Бɶ6[L{/὆}K1M@oߎyxᇍ͠x6@*1⍥x4>gM:ȓ6EȪqz]\ON͟{gtW9 J7eէ) "+p,MR B@ 2Bg*j4v͂> |G1NgDW]uqΡM+lW"Ώ(B;/vcayA4>${Ϭfbn<\myQ[oeu:3t,;^&} Jk)ܙPChh, u}9 ! @f >o|!8Ă9ٰpElςD"{GM)phnfaFrô m+߄B VBfM7|8 MC>LEXUB' tEý*#:8HWpv%>g%=;qj HU 㠱 E}K۟b֏'ml!=L'{{|B@!ЍDH3h {as6a>+_l { JM @SSUF՘k0%LL+^gEbhEʵB@!Ч抦70'avn?to,?ln{ƙGy Y]|GOH_|UʵB@!З֊%̻MX;eɇӛfMػ"yt|,X"^x؏9rTB@!v(iNTשes0nX³(r! B; fh8T𞎯S@-gwlF y$˵B@! R! @*<% zZO54 $B@!(6t^*ttB@! QRW(i|$!B@ -DXؾ}὇7~}~Cyљ:u񜿫8^8qF`*Wq.20QF`.㫲xN`]x^xxH5Ys+f?:׹+w! B@$B@D(%7b%}+cƌ!CH?Ǐo6akĹ ܹsƞ=pxaבIDFtonwU 1{=|'W|6 999gĈ*وcnj<83gD<)ϛqٹmxff>߱5ț%Ÿےfgg'B@! @DHTXW5 xƎ,ز^O>貊׼0ò#*_|UgϞ[Tx(4h˿~*U\FU>u7-oy=>}:Gmg%Lg{fN7c?qD`>nus^^^R/Ͻ YoȐvPxf r޷C=He#k 2N=H_Ol܏hÑ!aFY6r!@kx6ѣGqwߍ#|͛Cq<>T.UqU>v(!VU8x( /b,3\W+j-;#;^ЅdRVF5(huw#PNL-om4g]8Jav [kEE4D%B@@n]IDe.͟h&u#3Q>F`xbIMv$ٺs%{yX1Il9 T|w*mxX7 ܶojYWs\b]2\t&/xb:;_<<|qBLRfu[<}W>)=JVg 6<&CI|iqKlٍՏܳ>EQ1{iZ;ASͣ:J*P5ˬE@_> a*zKC vbu?[-T_`^pS$ѯ:jW!}r-J] "r|fx^ *jZ?o/w3A@hgH36;a41Ҧ?ޛ7˞JgOn9#U(/c=G[/|3Pt9ƨ@7}}85o `Aiw##T7`Rr$#ϔMu [jQyd+q+69Ch:a1 iĥ>m{a҆p947.Ƃ:3\ⵞxݵdWJ%MdtuneQlX=^f <|𥴶'l|ACȽ\iqyĺGg!oz8.+Ì[u F+WrDȈfJ hV5j?k1ܽ`~Y`.l2%EYG+a P>TmxE9%UHNy*l_RDty @g1}*+)xøyWPY%p`y`8i.7{qv@npic_[OxhŒ\Cp[ḋChGhkڀc.Fh q_gf &gpr)z+QzkH@% T@6V WG#;>KHr!/C~4.#< MAI8Rr9`#ׅb̒w\cKιH<p5j(~;$iXe?q;-Ԑux`[K,$7OXH35aA>:B 8{' EfR()UOȚ1xU]'"X׎`6, EX c:0;4$Y[h+be7N3 g"&5kVA~sc͐O&{OaҶj)+W!R<2[MolJ];rͿ`x{$2JX![[ZB!onAy6gסYa!Hi$"/ѝK#Dm Tݵs,la z旤LJXӂ+_K\]i/^/1kdi˪iqs^ش1vB^kRv㫞>"`yͤȪOW+!2)kdeMR3gf@Jg yuTm8F]eoZh'oGpɳ4)3oB5҂kS)v1Byg'i]y'~>/@yj$к ˅0ռEC.@_" @_j-)8yhEIqE[$(T ;J6"giFb1=Moؤh{AI#7H*Qs?B=++]N^ z±wwu!q7/3f+ 3zyNز*lnJLbp5oDl곙Lh}3B)w:)1џSM8F|˪ҋc;ogMӒ~WfRR!;;AJ!/9>_.ݐnBߠ.,)ObI0+ Yd)M!KrN15K'3>Q[K\5!LWڂ:#%ơ8%'| ٸ7cTZ΃0!>svʇ.<ձ֢,5Fe3QmN}ڱ 6ҹ(g)]o wy8havE؎nJ(" QMSJEy"/,ʖcdkᄚ1uAQ Kpۉ:أwͳG]O;83YTfYO{ߧ-GT3$HXU%/Yie$x:2".Kxާ+9yDR+ ͽ[-6KJ^z?4vOy ֻ`ލ]3Z]M PW@-_@/" @/j ) ?d}2;ZX;n£ w6:Ұ5筄uyaY 7{j-:.LgbTB¨մqo^wKs3{pxS%;5/|BQ|t$>K )X\yۈr]]9{wDf@+pr)x'kKC=^x2Gz7ǻ<F/kWh{nI+WZܕ2ٜJ.^O@aϝA& 2wB`f!xCjDpod$Ew# []/!7XB{ F2ZE]g+ҧ敓]uGt#n>Gn@W'qDf}Ed35fWmcG̱wز9L n‚݊B<Y!6bV`{`Z6sڈ@_XV97:S]2G]Wq . &d&[d Nn~^Ŧ7,Y<y:0NWQaͳstfmߺkN`cofhl6A\;zN~Y&չhsʹ9SFUxnj^6-o>mm]XB}|c#W T1 {ݎ]So97K-l>:F Yy/%^䌺MpyH9.ˁh# |D (mE#uŝ=vqZ,2 `qnEcM{C$8QhѾɥcd(O/ͨ8@2ylcQ{\(6RqM7x6[49Rqmх9:[P[2a(v w}Zvb~h&eW ִsec߹ ܕ􅸎ZQ][JAQlە]~/ܘnG}TQ^\2 ? m6su=JYՠxKjJ_\u @Bƕ^_\e} _c+#P6o&KK1msǝF^W7ҮNQYNíGG }$h,jm{uE4 [ Q`+1o91X̚ϼm;[v׵u4{YB?~` 8[0 Q5 $mSABT ځ֎[T{ 蹣3}I4g;~uV΀/fLm$kmq̂bƢ;LrZh}+EvGխ84kB;){En,xc=~GU'bOQZ0PiYEV2 ^B @S8 !kP\ Pm- \rymŋ<Â6юNB(\ޒǝM/6cD{g8u+A Re4adNV|2P "JNp{P94LY iS?}    E oHHHH`0'O$@$@$@$0PZޒ LqT`IHHH*Sk[    )N > "@`j7{K$@$@$@$0 P'   ZLfoIhҩ2ni%1Խ"%yOay|S*Ԩ0,VLX}ϤSHtJKU]̡Tڪ3&oG>lpTad'HC&> [[m-Ob{"ߋ]^X#zl !0xv=ojyU|S4_ L&˗/Rs{נ&xG/߃f#TD;{-5̼$++~A(-v e|4M&Q, 8 ~B2*ލJ;:DS{Y=phB}ZWE*Of IO Bv`-ہT< '+,R ٶ] @~Y&̎I& *eN%gCOzch%Vbøp-u71~wJAlcEMXm7߰12.2sL̆s&sM 鳮u׿9oh̚3r E"PT,%8}3Ǫ XZQWаx1=]vM-,C-1Rˡ;کɲC8Nh+IMS+$uvKl$ĜfMZsIRƅsb5RB6mG˪~$ySuWԉW-f_ #C]X-Z[o-Šncv_qA,SԆu+lM(h^6YO6H妩9Pj=g>q6o:©xDVQUWxG#@ zc@ 8W'~?1l~u_f+oyԴݖiIPrs :nm/x* V2rkרW  ɽDZ8J{N4%'ᗍmvr Of>u:FUO4[)ܵr-`$8BAO7qGrE5a7Upj\9ėnYJ;X@Xa޳8'OE bvJ,Ω8ˑ*d Ib.˯ G.q߅FI\=/+įax4'!m.m[oNHӸ"imEv<ּT c0e]%:#VY D D'⪦=WX޴lv2uM֔pvh@ ^ 5Ə݊X MJyw4\2Nt?ζ@&g&U}uJO`sg~-odmГ&/go~t]u-ϒȽUX_m0q_IYt?8V4lZ]"^awj%*!>M^nI4~]t=2&פpu;)(6VU۫oĭ+Ϲ;Gƻi̝v />wvy7w.0jɸ7̏S'~wFQ6bo&n]|OvQy @s$= aqwM4?ƼW>gf0⣗d7Կ Xjf x75_ @m.3&N'C9G ]XX56~ ~ xx,8;R޻K N[E#|ۛ҅qt6u7jy, Mkw"G @䆄 "I@@ nUBp:ZR/gl sFS ujT4l:)3(:[i2h05q!ޫThRW#UuCv!Lb]qu9ڝɚzq=FY"V!cJyHV)xIVWBoG?51zf/[l7$r>cu?6׎+Wrvp7-Q:N&j:J 5-{G8[+~k@L<F-=ފʒ3xfDVhyA,T:fM@sDz;G7 ]7O2GdۊwyBc2vk=j~mt2\nfiôVVKwdlMˣH.b=_xA'@ C@ (HJ+MϠOVےf1)uרB[N K*f]kVUit6DjcUMV8rέz!1bmsVc4lWN~\P-q%Ȍ/m[qsWF{F̢#Zio|>h8,f2/͆|dcz gzh ü&$=6G< IpKXsbșǧxMj^nXȗ5<,͹r.mSQӄncvĩ2EK@bT̹z9edlw)j1LZp:yMI0Hl" DC $tD,m9qQw?ʫx؎D¿ ,(^uU[__Nc\@W U~Ɠarx vHK$eMrbgn.-&T L͊sk[zTI<|W;;O9֡sk~,l8l1Q!mXlݦe]xs0`$į0YKqqJBHչi}ե籫vL~!+W$}T?Fl! D<[p/ Uxug ?KZbeVA[ERqxD[o wq m^ULӡw^1 Y0rj<"VZvKz[%e!W9{_kBu7c[?Sg': 7G[=ӥ-_cu[ 袸}>y=1aka auhD]Q݀DW%Cs7Bcd+|L|E&@ Ƒ@D 3FKGNub6k7KQ; DzjbUexZm.M!<>@LĶ?fGlv_šAǡW;;+#{uS SvxIj3s!ܜw^؏bGyM|{g̩z,kކ'Rt8 7w-snR/_v'4mV k UDzdSx@Ys-۹hl?KFy([ {Zt!U$@P $0fvXϴh\m ,6\x^eVz,,S @L zpwL8[!Qkd=Y3:0>9m1 # yi!qoĕA@%&IkWJ3Kȡإ{+iquLwhWz`DИK4^#ïܳDZSxܽ>w}7vcH]PWu3| G$UT:2l Di,jT\ @Ec;6Qj04W q^i9; FV]/wXy6!'`㐦q~)!Q>>,A$Y;"Mh4pXD;Q#r>ͫJ9`f$9Ӄ{yFZXmoPHJ_t}CMr(HO벪s0EHs6g>_ait f떜uq!3V7328}gnPWT{%֖Dz!=(LL KN&a8s.vp9 Ŀ[ #$&`SH`ᰘ{=Y|*OX=$ c[Հ{Y ~ 4 G [0L'a hBG1!BE˷⨮4> Y1ȌqKPoUZ_Cr@kzvQUhmP  j]7 ~7fҝį`Ȏhsg᭗_GL <|SOom+M~FqÀ9GF*#?맿gtvak_!7xrHV܆973 [1>gHVVG [+S?woHbH5[yfqqW8%qL+5?\a+ix / ڻݗVhY#DlFak$ڙSs@ P࠰I$yc!]WD&=-[@ [PLU6PǼ[aLu}?4CдͲ X)[޳Wz9ādxv>)Ǽ uԘ 奴 9(X.fd'ֵ7'!h>ZK$bh_H\}DSrY%շvlőGf6ٌ!Q! >9a ;6O fIDATmf9pjSjayeocX삥ٸηPJLw;cF9+H D`tB[ 9`8a3vU]@^ܗ<`̓~qpuiw<9 VǾ:Ճҹ)du4q_ *,VVwHe$ _fs/TtSqgu7f)+ZKi}ؕWivw:@*+|282z^JVKA)ǺH~;OAdE~s!&(ja)naf>t܉>muC_2г?;sX8:" JSŶ߀uˊeTݔחŰcSYM wt~3>lyBlC[i?L `plP @LS80O`Qm%Uذ+Xe3{@_50OhF|2EkwƸirՆwN41JgD%lRi'ack1Ǹ^7[Yx֋9mC>0k-~YLb<Ǿwߪ@$)y# |PȇӐ B #v{cG O~wgm߯:cFt8y5eG`Sܲ m_U$@$P*bR Ɏ՗j}/:ʄ:Aɟ@VZgeWqVOnsB$@$0*k s,_Pp~ ęr5a 2ᆅ]$_19SGG~w3cލ7R! bPL,HHHH"N @#    bPL,HHHH"N @#    bPL,HHHH"N @#    bPL,HHHH"N @#    bPL,HHHH"N @#    bPL,HHHH"N @#    bPL,HHHH"N @#    bPL,HHHH"N @#    bPL,HHHH"N @#    bPL,HHHH"N @#    bPL,HHHH"N @#    bPL,HHHH"N @#    bPL,HHHH"N @#    bPL,HHHH"N @#    bPL, C=P0r^ƥ\}z^U$@$@$06?W>9IDATtoH$ $HD+T"BEXP+F-9^,O\h/`EZ^Olj֦у VG$${ٝ?&;;_3C @f+yfc㾮)*pz!ﯧZ CzJ&*K˟j Yhbtjm]Rq <`h[msa( [мk@ i,`[ӪZylY|zJs<> jL{:U_n`0?@V"@h%;@I/[RJuK5ru}@ 7A9/kΩuK¿^Q@#w#4+@ -C'w'^(!W?iYd9|VU^Hk&=.HLo9ܘ^SJinYiHǾOi't^3RF۵vҥߌ=룣Jj_`WP_[+k9IH ۉ-Cx  $~8ڼ+gKrLm{hQYW?м.wJʾbr,jj!!]A TA?pҲC݀Rdk:5| NPf2Z[;UG>?(քQkgyql7ɬ_:h ~ʋ:5_țپI}C AVJȿls k&W5~!  :/F%}G? uuqGw+;ʅ_o H*BU婕_7CCܢ8-#z+i2 Iso yAMkލF>knsZ[)UCm8sVo9&|a6Nfo%mmg].]#HgiR<{jSs b 6~!7 /)sW`*?:ķ\sL]ue޽k9/o!%zJ}eU眼NY)Os6r?Iw}/ix4_x)` P/kYY2;9O5l֡xb.[enKy1@PP2 Ga{faϟOސx^m;p/@{*lfoԵ V\)uQ. G_O:I >wT#Hޘ+.n.B_~@hֽ:"Ku;F~yu?UvV]0 4EN|yBj\靧\ɳw9rF|YOm㟫"ef/ΔzeJ]9s}mKiٲ2{Y*U=&yA+( P w*@b Vr=NӄU" W7cuV`.̀I[wuu!úE6}>L;#ϫ;#Ԋo[.7s0 @ Zf@F»;Sې73-̊֝^V7 صyb[0LK^ҷ[t>U.xK{poMx{sF6{,طf̍ˤ}m~+l\ƽRܞT.z.N'@" DX `6FFכAn뽲o[HTW<U}OI6?tD&r+jlYsCǪeOw>,=mfh+?'3_T}N{{SO_}iR4 j%#2 LM +[)j|kxE2$M; aalrɊ  =W8a]ׅ/#qZp_{6 B_/z^ǩ.=+RZ4>guzz!mAW6~ÆUS)Ͼt@6x &W"fշ ZݞJ-,prr`ۗpɨ.:st 33 aa|jzCtNdZo,n+?.lAǏ$%?KRJY+`V}WAE42ClF׶r.rIqy~ {3W.oc0=.Յgl^|]5!u9%K=iYϺI sYFۀ7J7_${]~eycw! ˷|~ʋ2odUw6͇nw9Ulɗ֝3CՓ':y _@ ,V'P\+&ydȐK:x~Ruj5K Gt\JRAWNݬ3o+m%SFd[{Hz@Xs~aE[e{dy\CixG@ x 3X|uի F.n/a64Z^ڥJv.ӫɑ5R%trWjz DΞ3;]Tio-7@ 8@@H ,@@gz4@@*@H(? G@@Y, @@ @@pV7KC@@ p@@ 8@@H ,@@gz4@@*@H(? G@@Y, @@ @@pV7KC@@ p@@ 8@@H ,@@gz4@@*@H(? G@@Y, @@ @@pV7KC@@ p@@ 8@@H ,@@gz4@@*@H(? G@@Y, @@ @@pV7KC@@ p@@ 8@@H ,@@gz4@@*@H(? G@@Y, @@ @@pV7KC@@ p@@ 8@@H ,@@gz4@@*@H(? G@@Y, @@ @@pV7KC@@ p@@ 8@@H ,@@gz4@@*@H(? G@@Y, @@ @@pV7KC@@ p@@ 8@@H ,@@gz4@@*@H(? G@@Y, @@ @@pV7KC@@ p@@ 8@@H ,@@gz4@@*@H(? G@@Y, @@ @@pV7KC@@ p@@ 8@@H ,@@gz4@@*@H(? G@@Y, @@ @@pV7KC@@ p@@ 8@@H ,@@gz4haɊ oa["8%*۷:Hvv8訖C@ F?mEh=zϬ@hbPfC@x߮];ҥ_FFF@ƍoC #D+p9~ٳg999N = Ncǎ._~oB Ze%nݺUtڵt1Qt, Ddؿq_~ҹsd`[`6/!'N]vwzꕐuP@(#@R ޽[?R ɱl.M?}uvaF ~Q':9m5 8딀ynEhN{uAf#@h6q@,xfI%`[:Gʌ d܂o}F--Kвk8Bh#S#/`[whfSX @܈q @ 6N, lV)nf!7bfE $)f#в-k0 )925E |Gf(@h;UYH!č'ynIz ,@_mB qLyn@ Na&`Rq#fI*`[@l6-Kвk8Bh#S#/`[whfSX @܈q @ 6N, lĉj*ٱc8p@*** /keҦMf(*@UUlذA֯_/'O=2`몥Θ.--]ӢS ynQ6p*|WpB)++gφ^z=#Ǐ9N?TZ%ſZ+Eӯ3Ɖ+҉We,zoRvPd'ؠX 6x ;N[ge쫥ӍQm3ul\@yy,]T^us/jsl*mx<}=KH\9R)e[*k/*[*ֹG㏭juWɈo =c_`" kmeM5ynMuL12i@$\kHm"SlDUKyՒ;Pg=K妥2pYu 7%|,447xø;wsxuؘi9 K.*{#*?5 ɔ- џ[59wT;nJ`N 3-c @#>#6m;w.˓_|QڵkRI F*RX/RVt=[OQ FX@R%U*WWNF5~24 LFt+7032 ) ϟ%K˭~ӟuKJJ{;F&y(3]0]nztP%{|,\Ps.L̸9zlPL1 s+؊7x<9#_ {Sd?ȤFܢ.s@pc) (x#GĉEWsuY﫺uVy뭷G1q$o ܷjQ(Ceڥ28ƛ~Jy`,P%OkeW o\zư}{FKJJ| rP发R+%S$6NYR5A^+!:)7AfCw u.:>f8[V`R{n[ȏ磲V<ӓM]!%d~TA{Aun=Aj\8΅Zu.dgKn. vjFMq[w"u1_hBx} yuu`{iСg]wрxu^a1~:eY:IdЉb\l~Q~2;.)(~V>B-V<|:To)οyW+uG7ɲ9e%LkH/F^`a|N7R_eޣ}w3w,bnL*(EoqFkH` {x>D]7 /K kMUEcxUheKhA7' qcb?ej0\\TS|}?k]@AYZ0Ft%qțeI`+jy}wiP/" #@3W=4T}! Cӟg?iH?^8^(|TYO-j`Z|@Ys~PU8 ,Q\du\wwoO( (Xyg?H^`'R/t!?ZJ?#&سPP%oz|':JǸ- #fR]իWڳz߹sgd+3fQU T@FC3@^i|:?. VwŪrnݳK*S*ckg{R^>J~E=4*+'wPRM]`m6}F-ys$ؔeq\3p.Py7.hFjm/9#ᳲQ|1- ÆQ@ Q :|eee׾5av˝wil46:uJ>S>o<}JNWW%Gtu5sXrjZn[DV>t_"d'+sEu-fl |nRk9/,2P玞.EJmU[ŕKf`\uՓgLrb{ܣEyuv8m3wuRF3Ԫ.J2-0Fg ee9Rw_ĸد,SY2z*B:]&%G- _Hx ݅~E/޽{VwFxtE{tq̹{`RVw>[&,߼TDJfpMP P ,qIgaIV.YME{,Fds',3lb39UM</ <oˡC~g[=܇{'pNTƬm|X7I;1W5, hEL1/iZ mE4e<_/٧`2r^poo@x +'h")n:tݺuw}]¥í BfU7 _UW~aɺ3_ <bηX 5;UwyƸ^\n R躅rc"l݀X0r˪ZQmaYHUrT}w``o]iߙ,#56z27n/F]ly?]kߎ[ˡnSU+cOʬ_WseIJQKt]epTݟe8/8mz=y ֶ[ |rKu WNO3yn3 W7n|֖͞=;u:ӏ<|ٮ8FJQ ^LoЙz]Fs? N:?Ԫbtr|;n! ֆ}o!:SiҳTH*X٧ vV-|`+,RwY&wYrUԕqĦ{7 )iӦW?O_%u|DSUq&`+G |$x7^2qֵU x z^Ff{kۭЈܧ[drTЎ9^_E D|0"$N ^`ԩ~zktՆ .dtK7u{.lW>{ )sBu L{WU/F%9AzP¾FPw O l`*x>‚mBlNBJS2[W ȏܘ1cqz)_-ІE䍀Bj6DŽn`ٱx] ǥ"=)} >aϴkTGMֶ[1繽1w>BlÑs@ bBpN ^^>D P[~PX.]BO^_] WO97~A+7U=dk`uA{/jUvb.|ʹ\= D'NwBo@]Е%#ƕ Z Z}M,4u |ASwN d4r"գjqMÈgT 3@Me_Mkxd`7uFPTcҵ2_ 9ثy i7>DŽ THƐ,nUCUۖE#&x*{o*{]Y=I}VD.=}to33lm$Μ/c/Q|[С1zZ78xaLqv˾qeR4d:-[GUkW"}01@cD7`h-"?ςVRWx^~#Qua1ŠuۥLz(Rzl:-buC/Wmcl`6?Ȁ=ΕDP1 2 )MٳǨ.;vj(w+œ$w뮔ʥSůkrUM(ߨ&+O\]ԯzɲ5U%fZ-oZRh\퍐$V!S9f5 &uRÆv,Ku;[U‘Oڢ XuPּr]7zҭdbsA.6Vhy~xl'Rh̹E >"@sWSOWmwy'eĈF]=ⷿ 7;pDLmWO*guUw^I??LNmwNjo@K}_^`bzﶛۧ  >RLE9n1BWꫯJ߾}CMp[_zġSȯ'_mujyFՃ&yaB= Jǫ[7bW]vx{_'T"|;[&* u<ȿTYlk.$!w{`.]9Ymh3]~8i0f*[n3X`=T mV>Ш@x]_wio lnUFF=ZE 0QnÅD}PPq\4ϧer>FJȧkp{U}TwHнY>s wJ>% й L# g3UN8+Kuc u)ìg/^1 )Zc\UVk>ݽ>s"N'CJVz3J= >:nT?e~Z}L-~Q'{?<= ʶgmqSճ!biӲ v!X!Xc@٩6:E![h?&{aHv!پ]){#oY=W KQyVwB> 2f^;ydK3¿~v]1#]$oE 4L@  zͷonO~t_Ot}ã}c|GUW)W=Xە:UQ!NyxDz? u$g)'$W,RO%r?y{Кۖۉ1hj<#xfsF^nM9k֬L>]e e,[7ܱ2hF t;Lja!;=nQOG7nQ 0sB{>*PzZ9n\2}ILڿ}:g_X݆X>wh^)r5 Զ~EzF&"z4yn=wЛw^9sq?MJII{GtOB F|<}dIGɚ 1F{R4p=XnWMg-۠[' O=+BkT[#?SGFkO휺dقT۬F;쳠O]zuyx}, zLs>cJ?mA]|*5H6?{9tB<D ଀@oQMMʕ+^W_-)\sW=x~ZtMe)e+2hkֵZWG?f!{=c_~E]d~TꥦU!G\%:Y1XNU7KQ֍$[OFPRΥ Og{JN;4&"ݤai[=,tZͣƿ#itYm3lSF1|)RX]=kJ<0 ?#@sp*Z__/|q7RQQ!^xkVc;M.`Rwl޼؞/X׾M뤶pjOh/,9NG0ẹAĄ -cI u駟6.pҸMwZfvyU6Δo<'%\6- F& 4$if!"pXT;G=` >9G1e,ϗe6vBBj[@ȃ@Ͼ`M/`RN%˪o{-%nz; atCG/O=|_"w1gh2@Q2 `R-`g-J<uಲ$ Y|rnYH!$g'`[@2M&@h2JfB ,VE Т\Vd $O6 )lus@QƜ@MFɌZYH!*("Zԁ"dɹf!yn;ʘ34(Q 0 )X%`[@:pYYU{>9,s0- ~GsF& 4%3jf!vآs@hQ.+@ u'v@r:~E (c d&dF-@,Zb[yn-eeHVݻwe lw@p6zٴiH߾}Yf m޼٘$???Ic/E-|$S 4y!N߿_jjjD_ܹd$L0z܊N8!nrW^jK jw2 \N:%ǎv_]cXLX@_]ߺu={Vv*;vlVf;Xh.wtҥ_FFFsYE& 4%3Jr3gǍt__15'@s 4+:9|Պ2 ^@{!iiin] nB М?RUUe}[ tߡCnvWu&#  $l"  @@$ $Nf@@0  $ v2  )@0%xG@@ ID@@L);  I @H&"  ` L @@H@d6@@S`J  @`'  Sw@@@;MD@@ #  $l"  @@$ $Nf@@0  $ v2  )@0%xG@@ ID@@L,mڴsyG@@t8wlڴɘ:???D7IZH&.zAIjj* 4A@@]͛7n槈| gΜ.L:t@h  :TUUg}&ru]#'ȑ#G$77WvJa  ]@cǎ޽{{rW}#m۶Iǎ  tعs:uJW^q@mmF;r/@@]@Wu;#Gm>8wt"_|1w"Df4@@={Ǎ+"@uu|FKK/ԨJ @@ZckVĨ^#Rt=v=9q   CzٳFZ]ƩW@ߪظq ةr@@Z!C8Z>kjjd{N_~Ӂ[^aC@@~]ɓҾ}{k8,*,c znS #P>}$333@@d8}|uu5J'95uE;wSj,@@tȉ'.EsD]KdHW 4g @@,U{zxsy5yh.z   @ Є!  Z@ݵl  @   jvײa   M  @ ]ˆ!  (@4a  Vjw-  @ Є!  Z@ݵl  @   jvײa   M  @ ]ˆ!  (@4a  Vjw-  @ Є!  Z@ݵl  @   jvײa   M  @ ]ˆ!  (@4a  Vjw-  @ Є!  Z@ݵl  @   j?l>7AIENDB`././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/doc/source/admin/appdev-guide/step-by-step/hello-world-screen-1.png0000664000175000017500000003645500000000000027553 0ustar00zuulzuul00000000000000PNG  IHDRӞ?sRGBiTXtXML:com.adobe.xmp 761 194 ⟁y;>IDATx\Te?Obj &'"[;id[Ħ&nt_S^VXٻH]$wi,?Ԗ6/F)N&䤠L{=戟zgy<}<9(@ P(@#&\ P(@ P@`(@ P=LA~۠\ P(@ 0>@ P(@& mP(@ Ps(@ P@`6(W(@ P P(@ P 0aC P(@(@ P( ա(@ P|(@ Pa {P(@ PA> P(@ 0=lru(@ P( (@ PzA:(@ P`}(@ P=LA~۠\ P(@ 0>@ P(@& mP(@ Ps(@ P@`6(W(@ P P(@ P 0aC P(@(@ P( ա(@ P|(@ Pa {P(@ PA> P(@ 0=lru(@ P( (@ PzA:(@ P`}(@ P=LA~۠\ P(@ 0>@ P(@& mP(@ Ps(@ P@`6(W(@ P P(@ P 0aC P(@(@ P( ա(@ P|(@ Pa {P(@ PA> P(@ 0=lru(@ P( (@ PzA:(@ PiB`@֫Pjvuuu=`&=|4[.~3||{^Z{LJtRYLLmo%[o`fjM P(phӒoFN,8z(*+˰-} *c 0禣Zpij}yԠ0̈́jۥUNoƜ4w`$_Φ(@ P@wiBvpc~;f#׃ߓP rJ%YiXgr*"Ct3X;gM,^>2\b-i(@ P讀6-R{cʒ0ȼ @ CNDI؊b`0ԟ$V(`sL$22g潕Bmta0 )Pa bҫBQx[~9oPʊʬ,DYӮ[ 6ݜXȤUX 4孌i0#rNO5d ""b%[{%6'E6jUu @a֦bXXV#(@ Pu%iFs(Ku9X$*z6捩gEcJ dBy0gqxV,E@S Oǀ)6g#eh%3i (s֏\T58f+9-  -CmrQWX"1^řȳXp".xbDE-#B]Ӭ6c8 Kd͛v#L) q$3?_YqJk:RL0. LNFPZdM}My@ P(p \ _s~2tiPϢt Kawt[a)ݬI9Vox}R_A"IAm̲rŠJ,{ٙK?]K;*ф'`d$<"9R-A~ vo*z9r_-m:l,sti!cѿ<iҰ [`5C\ qfD!hR-}ZDMc#6ujLF P(p h9 G$ 7)H$U!11&u=^Ɲ#gp4mϺγ/K"ݔ5s/$"DFm&Z uF/_`m/ɧ7%Xˠ=Ƈ#Ws rAkAΉvz7=ѻsϿ(@ Pל--!=]g11͂e!iKQTTvnAq8FU!ѬĪA:]3J!|S{]nCO1#B x4n%n5c|R4!7KB&k1{E!μ۱>b{ IΒ_9+=R(@ \J(.ދ]Z&gB #m/>[` zV22Gc_)f2E jv\(ƪgNE8,HK˃!f&cL7oyDXG1*"n,ߵ:Xc8JCea,HNEBT(@ Pח҈ɠyijkaP!PXN0Z"d԰8gu=s^Q^);] 3:|C@h(,y0ǚlbd`WÓR8MZ+ IPƃBlQG@h8ˍlC3%"x k{6ũ:) dЇb.vlND P(p Wk6?/_UCM :OotajIdq풿Ԁ;cz;^oy}f֫2OHVôxte̱HX/ ׻eȒR(@ \G_S Jm2U/AnR+p4:o,o/݊_A<jv 6퉮|t7w, T¦Ov[ lv]7 W뒲j]_]UsψPㅺr^Jj 2f5N5GdaNZ`vT;+'PS(j^yfN憐30,0lDƬ)nv3D @m[i|ޮ#k{cAZV\+<'aY?M|xjZ݂dw"齺JԔI6;tڬJJfj (8zW̰BaXz{],(߹ղeKlx!#wDzN]+y2 I㘐qլpW>i[db ;Ի&c]."pEg.HC:UbUL$ 2# KaҭK4!1ر1!ϑ[ٸ:b{" +qߝ':q_qV8+ 3fbeT$D(qBH `U1ÿZÁ8_yeç73:R%5yΕsVPNj9oG@RGYoMeVLvb[ޖȥ/3 \,4gXv[)"#zHZ,|fYI$Ș+F!θ' [O7S#1VRk,ȗ 富,ڱ !Jŀ4vd,m!1vt0Kf] tp1\c9r4^7ߧYe{ y,rk8|!EHDzBQy<%kDTJV %Q5X ?e^c(^1,fLN Id?q`(uC5+`NEyy9"''L#˲VE?/+ϛ ɉ(ȇF=cՌ ªg0!y  DĦɹ`!2OgZ+"Yd/Z"!&\Ny9]WKq׀@'qLxěd#=AE0h(G׺LSr  R`j㊑vyj𓯋"$ĎJ;.1V0)>HW[~ XۥthÆҧKv{to셕)@*v2d%H9eQ[}G@T܈[PO{VypkH;K>mjQιr;$() #1dnՠB9W\q/5=A<a VG%_IC`X.\c G\0.Εi1|~︘GŊM;`(6_X~~m(0kEi9kcQQ  !9j|zLYA~ ~h&`Ge!|q;f#4,lS*gB_G#wO,7W*}c^s8أ<"C:Wr}FBOcU8BM٦:c co`+@r!R呲e6u„r g3~`=E8v,  _< &P#*Mc|h9ctU)t S_i^xߊIҚ/m-v]ֹ*&-Y%ZBܗ#kFў-W餡94햮)u=%=xoF$%!8~#GA@?v*sMFmV5 O;>;s-=سg7kS/ t(`9{ F 忂z\Μk2 -L=E7%46ϖVv 4cl( Ӗ|bBt_,ڽud6W7;QUŷk*8j i]w[ZZ{[ W]6˵DFr9{ h©Y8@Zi{bHLl+|furUe3mʎӑ~(K#p- 1٘C {E:ܪll("~Z"rxߤ0[\S~xxYHUXyxcp{?E}0iR0w%{b=%?ځMUv~97ԷL01[˕_ϞLˁclgLCdf5}QQgCMlpg/k7g: ) VoԭhGzXHL WaR/kiD}#0{jtБԒ1qQGcx<#*J=VĠ/߆IzT@oi\Š(tMe(n(ǜ q-"\fpФ%߳c_j B5^A3|.Šhȫt,#!ˑS͞jcֿ .~ r+tGoOšYMM%ߝ@9pѹƷyr\dJbҝM.nXm5 K ijcLamk`cR^J9| k-r/ixj:ق='1QtӪtNDer_Ei8.̙+^˳2+jIޡ Wcn ;^{UϯG>Oc>1c1#>:V)F" STf{Gq:l2ũc< HظH2(CX Fz;p)L9rMi൛T~ Mi]MGt >7N[U^*[ -[nVsH4`zB/aQAl5{e' e Q/3VUG,/˕2lϊUYwg`*a?6Fnz\ hZaf܅xۅs5Vix9ձ׷+iZWy^:cokG׍cqiq<Քsp#2珂UQ^➦%c6'WKZq_~}۔ WE;?zz53D6;UdhG(cOyД|m:%7h`<3ys $nw;+S/&ۙO5piΠ%[~.mkpynPM}XL< 7w7`:]7/mيg^i&`޲ȓCm m= S,Iep( ׭-&O"yfmX[,C6;&`ن..^7y8fyS/`GeGda9X6 {mSW[ǵRC j=Z4o7bt;UQڳX CVBԅ*yzPnM%|Bɡx_>[yoTPN†"n ?9ynS<'Ps_Z+UU:~^#06 >YqфĹ.n6HJX&ccF$?kcop8gblǃ/@VV9,ܴ־ap <; T`wQxWgY<ڔ-kH>MH~eIH㕩*+22T.re 7IƶT=< czF6sSᖋgؐwDH)b5#Т@Y \>47r78N ??~>--C'_㱻!nLT!_>t =⏉JbGJҿ`\yzs}7SB?/A] kg_I9vx#~Լˣ HО cT 3n^'Gasm1sw~QvmFy}Xg`5g_1p|~c`{8"icL}DO#R`CJB;u쟿5JLe{:-B,|X{k\\Oicڽw*a\̻w)k89JVqV&)/K}K}f> rQLDծ??sZ/z\zKRM7k@ ૳r ޸ uu%Zr~ج?zv _ƥ%c7b;o`\ n^uJ[c = WG@ j7 _ ?÷eYC)-+xj<:LrtS(9g k6}nGBu\FrG`pNiןc4KQ /2}w5k4܀;Jם;ݞ0KZ՛gBǕ(fhY2m+'kos(BںoFT߹;1T:뿏*G/f1vr<:q_tz\+ڶv /yP%6K筟n6Jp18r u7&nc)o9q5Lv]OVP. 8P 15νzd 'G{^^rSp.6:@[ 7 cq2_EoWb DoGr1`+”e!Wby1ԠRqYu! h758@-46]y@)5vq5TZD/B`͚gMI<y bA"H坔56q=r E8WZOg/a~OUn s#()ODm=N'a>t4M%lJw*ǒ8Re`| 퐁-u/*#Wo돭 <: /HM ^Y->r<=o$~ȖyMm&̖;o?\!!uꢗU<($[G>4KV,Z Xt{ >x~Nvj[__񛈉2AVQW_c6vx\"7s.<,&sNJ('waB9ӻ|t (a9 S 1'a줚dAX> /vAi1s~f~fǯA|6涷Z/i=}1-yO> r<pgQS_3AyYa£$=x0 'b$ru<׬s$GQ/ G}4(p;r\ 8(Z ԕA Rߜ/9>}\ǔ`N k05j߉/`o}j+Qy?<}V{ny@>pOʵvLպrïU]1Н? Uj6i=^7tZaA P:x{˳yjuˑnE{KwKhx>#|V_7; (W:H2\b-i(@ P讀6-R{cʒ0ȼ @ CNDI؊b`0ԟ$V(`sL$22g潕Bmta0 )Pa bҫBQx[~9oPʊʬ,DYӮ[ 6ݜXȤUX 4孌i0#rNO5d ""b%[{%6'E6jUu @a֦bXXV#(@ Pu%iFs(Ku9X$*z6捩gEcJ dBy0gqxV,E@S Oǀ)6g#eh%3i (s֏\T58f+9-  -CmrQWX"1^řȳXp".xbDE-#B]Ӭ6c8 Kd͛v#L) q$3?_YqJk:RL0. LNFPZdM}My@ P(p \ _s~2tiPϢt Kawt[a)ݬI9Vox}R_A"IAm̲rŠJ,{ٙK?]K;*ф'`d$<"9R-A~ vo*z9r_-m:l,sti!cѿ<iҰ [`5C\ qfD!hR-}ZDMc#6ujLF P(p h9 G$ 7)H$U!11&u=^Ɲ#gp4mϺγ/K"ݔ5s/$"DFm&Z uF/_`m/ɧ7%Xˠ=Ƈ#Ws rAkAΉvz7=ѻsϿ(@ Pל--!=]g11͂e!iKQTTvnAq8FU!ѬĪA:]3J!|S{]nCO1#B x4n%n5c|R4!7KB&k1{E!μ۱>b{ IΒ_9+=R(@ \J(.ދ]Z&gB #m/>[` zV22Gc_)f2E jv\(ƪgNE8,HK˃!f&cL7oyDXG1*"n,ߵ:Xc8JCea,HNEBT(@ Pח҈ɠyijkaP!PXN0Z"d԰8gu=s^Q^);] 3:|C@h(,y0ǚlbd`WÓR8MZ+ IPƃBlQG@h8ˍlC3%"x k{6ũ:) dЇb.vlND P(p Wk6?/_UCM :OotajIdq풿Ԁ;cz;^oy}f֫2OHVôxte̱HX/ ׻eȒR(@ \G_S Jm2U/AnR+p4:o,o/݊_A< 1413 188 y`I@IDATx |5@EXK#gUӊk\#je&n#v6K~Lְ( b4+Bb$hOȱ?3x^939sf>;9kĀ      ZRI@@@@@W0;     DhcSU@@@@@0     DhcSU@@@@@0     DhcSU@@@@@0     DhcSU@@@@@0     DhcSU@@@@@0     DhcSU@@@@@0     DhcSU@@@@@0     DhcSU@@@@@0     DhcSU@@@@@0     DhcSU@@@@@0     DhcSU@@@@@0     DhcSU@@@@@0     DhcSU@@@@@0     DhcSU@@@@@0     D!mD,U:TVVSK5[5g;S<+<ژ/- eEJyluz(,m'(@@@@@mk)=YzHwziL7uV>aMhpӆ<ʵ~vh}MtpM&2Ѝg+"=     F5U/=?ZvR3!o*pԖ5uh<[Nzx+C[ ptǘ]_{餺?cQƖ`     p:R_U[iO*潜SN4R$.ӪKgr-KSZF}l~gCECfRRvaMzwZ2֋Mn!u%hVvyDOCm\V]Ƹ55.N+]cs+^$_du}T l,-z Oe]T] _Vjy    8%AzyJhH->1K=QfQ.IEyJ KGKҕ@ 4ZkپH)m hƤ+7;U~K7D(vݲܽRdn:HU(wlEN+V/ Oo_K=43O1Iə+ H5e 9Tuu\Y3X`]{8-Q1(\kf^7-X3+U2kR5pF2'IJLi `z/r>h>&      ѱ⨯_au~rKpDE?ԗrFfߎ^}>_`ݥB_4 h".gTqӂ][Ws,;/ FgjuEwp-ͷrw_4:8%5X7_tqG_slk8ur媾ԡB=CCَ|qYnWݮXiՊJ-먡CaAڼ)VBoTڽ)XeI:m:s(?r9=@̜h+>[^3T     @}j'zvۚ(dur$9/x7ߚ$N     И_،9*4,o2o^!*/($4Lր릱D syU0kF)iZ1~&{1m>6?/5b}94Ex- bGdQ@@@@@@a:NP >rcS-(`nNV0{/o__S=,nҒVU]vk~.N񳳳lzn?awL`@@@@@N?p:X U[zJԬo겟LcjN))\k7&Zm(V J2@@@@@ |K§@@@@@@     #@P8x55E@@@@h)>    -ikSW@@@@zA     $@P86uE@@@@ (     LikSW@@@@zA     $@P86uE@@@@ (     LikSW@@@@zA     $@P86uE@@@@ (     LikSW@@@@zA     $@P86uE@@@@ (     LikSW@@@@zA     $ҖzJ3KUV̄ <ϟ[Y2橲P=xPe%,Ql-}T:5Q4_^a  7"Q*)Pn=5p׻+Y׫rG!a mU|k#ud%^UKђJ QhXNa-\{y!5ojϸmww6Euur_uG'Et8ݽ!N.WnަK{8M0@@ kԑJ]]Vk-E*_̵IZ1V!dL)2IodUX2}cy0 &PM?>(-`sЂ7^U, ?|@EJZuY)6}[^MAauXC?]lN;_PRڶ*T1 ?4^I*ԱQf|;*v~ziWU?~_ZC?p<= @8EZǦ(1Bm~ =4;_*xVƳ Q٢kV?5${XTu8k Q.N]tVK.OO.V ?Gy<D)S8*6!Oz=(-Z]_g %ԧYQ4IZq2㔽qھf⢣t(*/ P_a;n}}{c4rW54fI^HR\\NU\5k4*)CkƂ1@@u%ݭB;UFhȐ;f/cThSv e絣lYQv==-ifY+ kv_j"Ҫ5yT^Cn՚ x+-n-rQCghTbUǬr2f%ccJ^/ѺٯO'EiT,rf^iץtvFY\m"7S+4fv[Ƣ4j \{}kbpV8&N4+CqqFNQP8B Eۥε:nw%*+ߚ[˼I#پL{:ktQOYRJTfيt2.+~~yHҘ**P}IF8RF 7EFoJ+Wǫl<]f_EEsvLګOw'mݺzgj}ǣoj@@;,z+q0SgT q.]p[kukcl{vgkӇme)JJ!ګJ]S8˷h^IM}y$ /S~V~kԽgw{~~գIgh1QLU=>C^8וNe ʔ7;Q 0iф᚞!|r[[uޢe͞Jz4iɆ +sBcC| %%&irw*3/5)H L1nK߲c~|ˆ+[Io"wm "oCp؇~TQ# t9e2kggepvҽK3@i-!r_^ZuTo9eQ7#b_eڲ #/  '/P^]{H>{M r5ZJ5#w1u^:SU/}cūuW_+Z$NO5IJ&زLC ܳ>׫KQA#?5POz=h~mF#jk1|]Eλ Pݥj-_ε+MM?{pg՟U0'|]%E֗Qc{Xw5tf{= ߺ}6~vOXLkoiA>-Rw(̷P/SxMg C3;lH_W嬱6荗(mZ"  Z.Wc;4352s[w8(  /KVhܸ1 G߁iCVR8z&.^g;՗n0OlU-b&X@)spGn>cHؗ{|iAXiҜ\kSqn_iZ-ۦ=ֵ3Twe7>l+ 㾉v} ڰar̰;|+4WX¸'pln]kwnuv+܊ԏ߆9q-1a&eiMڝ@O(Uz n))'Fm/gʺAQdRF6@ϞIhہʫɟ9FSORhaܩ#tM Q#  ''p^\@7Zka_-x^8kt˼Y=tTul<=MCi'MC/aˏPJ/2V34 0Q)k7bTuwi߮b/[Ԭ]Oҍc4'4ydCzިI5T_>!&>ZZ[w(SRAf;%b]|aig맽nn/81k7/[J5[rn)*KQBhz޶M5N p5%yTZLͦw8ZI/vf[vk.u7f[UѴXknpTl掭@64@k m_ w~h##:saXiTs<(  7XareC}HMJa ?ܮ2{ j}yyV/ŧź[ܝ|Z[(W)S4gON,?@:FȺK?yShw? >#\re0{y+ǘJ+j2ID/M}i35 Gس^000T_ZsdQှrmp{\ut3m" *T+ L L] D Xuiʭqx|cM@ܝ`p/R7is3Wos¾G_G}@@d|-δ:;Į8uj*πwV4Lz Y8~icMmԥjj3lXK` ><ѧ_J~zj'?ocUA+}kD@@XԱ}k_s~KG1Vpnz*    (Ц#uF@@@@ڳA(;     J­#9     О G@@@@@V n%@@@@@,@P=o=ʎ    Rp+H    gyQv@@@@@!ͥ/..nn6@@@@@ڙ-     @[چ28~xS     h)7EF@@@@NVʱ      ÍF@@@@@ (|r,    CpQd@@@@@d !    Pp;h@@@@8Y'+r     @; (7EF@@@@NVʱ      ÍF@@@@@ (|r,    CpQd@@@@@d !    Pp;h@@@@8Y'+r     @; (7EF@@@@NVmAc㟊ߦܩ?n;򕥥j΄;?Ѭ?fG+ZqDܲyk`%s5TV _ ߔ;5yz+ziSz6}iSZ5(oR}rj`sSSjrABv5hO{@=yKߩwV^429anm:5{/m*f "Z۶|~%oy^SeU S}P} u[vG)ղ=9zZKF>^tD^sLK5RiG[lB}OW.z^4٦XRD_hqvأ>+2䞸 G 2g6W__yҵrb-TZWxZuڥzZKF>+zEnRyc+v+#6i  DڔXsgj\%[O\N7~xJJM&@^_H5-X )=grA󑮵Oxm ˻cUOk}=)\uߵy?U6-yבY;wYXٺfk03ĩSN(+ gnP0^_KNTOOU CܫoO+AcjkC¥NN!Ο'<>_0 FNw]ウ;d_J#uso#Y:jPVOR@ kwUhUvJOI g}k4^buenjzNvnf5Gmƛh[P8O^eG+U_/uXkO;=Fa17y/^]ttdQ}wc6xo[VJ:'|E;9uVviZo6kΚOukׁq xvi4x`'tw`-y<6O[] xv,=< הXw.='Z]#Tl]aÞ;UzMO+S'Wclc#3ůՅNyn#n`+_={5oſahywVkV]{[: |<.T^!u'YNrnzf9u~\In}tXaγ@krkʔUupzoyzsW_Uw^Z=lV/jV  _='|);9QSiN^M+ڏv>R;w aZNTY"n닖mT5Dd%Ms"p*gNَWX5]O,KŮZדz-7hL^$OsZoTooTo/!g'*7ks]Y7~_{+7RMx~{jڈK<ֿ&Y&T?{:Z^uٺsm1~n/rf/7\wBu׬"4Uzf6WU_iYUDV J-*ֲӃ?TsuӇ|aZy.JLsJzdڹ;n^+X,sQ&d.fff*u ;Mc(݂rҔ~ݰp襃4Bs^_ʫwUYtay;͂53#C]k&yinWٖHuUGӔf?k?Czל%K4sѬH+7Ƥ(#s&/Vz(-sOONaleO^SV-_ Ϲ kzF椎Ѷ3ǂøĎu^{5u}y`mnJUlc}V.X?^Mnн`&-䜝Y0wrejO_yyYimJysR-}ov1g;e (9Z2YHsӼ9W TCo驷.s{r[gdV.U.E%__;f[Ai@7]~]i^m\εTJvM3f\ގ]OEb;l250yu5\Ժ&9qHcSX։3}sMnL:6?go~ѧB˾[#{;gk=_ktܟh]oi5?gFG[/%]A^\7zs?w܎!6MƺGpM{R^lNn4rk.Kw}PXSie 1ZnWGߡ=/?f')s9Yhej&\ᆭС-e.`\%>NY<9TN\avfTᑟ"}}Dk#6@ nY>w3VE+H \ K-m~*SM;X7߼'}[?}- eW醱|`G@);?aaOTO/sfΉ7y,˰sE)jăiv;Ei9vc6nTsrn[ N74vb=Oǫgzl^xk7|^MUۜ%.хΡ?rN VZGZlAU/j-tMb;z?7ZǸ 1Zjro-[޸N@j~5ȚN]o}귁W:>/xy3)psvicki޷[` =/_ 2w\ò PsI\6Vb+s؜a؝u'ⓝ6SBzF^Ȁ 4ӗ13,~4C*+ WPD͝5sx`Y;/dcY~| m)|;IxnW6MzX”y.}v&ŷ~\;mRaٹ#l-pbh @V)m0^LsF2XgA_JNO Z&mB%Ϟ*wH-6WNg_?\i!k{-t8a6zYW>Gt9  ЎܧmSIp#}yR>UDxUq9nh2P"MV/ۉ5nc\ Lhk,;bZxu^[6|H'f%֙XR𦽹~yEEݡLVA2BadT<;qkl7R[GY4_[^ZY0bJk}֞kuq]f 1rLF}hdOu~4KqRF0RJHW] ]7eBOi$0ީ{o{6~xu-À 4 ^iV\R.>'?jxSi9WtK[ƏŲv8jPD& >&UZ\{\=8Wϯe7{MSRlQ)MU_ꭈ5_X??OWwh?縟gF],pGgw՝}b{R_|~D?RA5W&igGE[K;< p ;i`/\u/uk.uQFZgǚsB\?Y W?_˙R)wO<:\f:ixC5l- &?wvt;?WJ>jqB^}ؼN/Ao qا{l Tq1{T oȁ׵zvcm_g[&h]tp,)|BK_gh<8LYqN;p]kM?V&:p`^7M9vf1Ψ -W>,,-O/~B KljY]Xّydn4wk->]فʜiuQJ4hEmZ]_uTf3~?Kr/wf۱t_Vˀ@B^@@B.TS;}i(pwnci͞+V5K4Ѓ:T@{hEM-Ot}ّ @wت,qУ]|]5BvJ}cke:`xmsޖ'teܝ+X4eƔZ־ 0@m0E3V:)~l=Wu8Kw;uijW=s]#}:.g趕ax3h`7_8JܩݫfD -X`oe˝W3,6tlCX"n.֔ܙ*)cJveN5D~'kuc27)ֿo7:u2`ܲV4'h={}?ї= @vdW9Qi!:fiL7%<7GNv(,dUwHgzN}-RbroLwX }ۦ5o6tQY}w_m;N i?2I[ͷ!^@@&glM]xAyio]칢okĔ!V{uϭ7Q&#>NS[݉WS Y-ykm~Z8-Gw[Niܫ%h=zrߢLWk׀iw{~7ښIZW|mdפie@93ǏJTLxvKLJ| 5:LupFZ4JWaYtn%ZDu֗zQF@[&੨g*?[|μl^FJֺQV=rCz|ARebǥJ;ubp};ib^BXVN]ۃsc`YI^˧򫀑tnu2jZO*WZ_V  dM[4{vsמiy#W* F>KM[4q}aw>qX ]yr-`shX#׌M y |Ae ; f,nQ[OP8)3  Pn{OMoD{eH|[*_O1o̷Ձr?oQbh'뙷kظt [&jPL@@切g_ҟyKb^{0 @mMM@@@@@     @L@@@@@A     @L@@@@@A     @L@@@@@A     @L@@@@@A     @L@@@@@A     @L@@@@@A     @L@@@@@A     @L@@@@@@H`}56i     T $IDATmhe     V@@@@h֣     @+      @{ (ܞeG@@@@Z)@P`$G@@@@ڳA(;     J­#9     О G@@@@@V n%@@@@@,@P=o=ʎ    R &(*U'wzee:uTCoU{<%ojγ__B.wmëٙ>B[VdGkh*Yw^GoB"1@@@@h жZ7{n_T]JxS^gu_Sn|v4X&xY9mȣ\ggjaOD i*l2 x}Z"#    mh[SUғh![Ui)5sM HmYSVͳ+7l2 WپLwYnN:Ikl !    Cm-%:^Un=l;N-N2z:IQQQ4/q-ײ8ed()7/*:A6wj0Y8QQJ^o1k֨(%e֤wE+cn=YWfeg+!GT?heieK{ZSb[5<6Nq;޻oNҘEF1_G%ֻ_י*9PEeN+jޛaau뭖     @=SzDJ{ޢ/CD۳Uj%/Qt]]ĻtHyeC390KtX h\SОC5JHxXwXʕ5ѵw+~%B˕=afuSڂ%ʜ1I;R/ֻy-u:_gd*3}@9 "c     И [_*Q*GtP:_cM})W9n*lء*E Jϭ]*'J`V +~6LE;-hj8z>'bK^mD|&_'X{a k|+G[}Es ?Q^uNWjxU:wͶ#f<"/ʼnRKTrj1`M(aU{HM;qZX4!    @Nv<:ӂ1tjJg+55QnpNU:"Z5CF (:*&i`J,f(ʴn| 3>nzMRt"DezVSxO]mk\ecUNwUz5#r~Zl3wZvŕ-9kzn>[:Pxk,Dgk5s`S-il!B]i-q/qh=~\M֎9%Z)ېWYRQ0wiYuuyE@@@@hsP%bo:n7({VnK̴@XaKP|H(,gulunh:9&j֪-~{ ݿ)&S'*7;[K*)<>G-vUf^ѲZ1i͛b(TIFݛҊ[ԭCf3s/̉нC*ۿE<5 nzH    pipbgi݌#4}rv:?I#3>"GІqn0ؙ="y.oqMW9YQS{CdL"V+wJ&LsOLܙ{toLZ!jzY=$3>R_jJj[ȘXEv󬟺+"3U){:CV'M 1xN}3Ht!     8Ӽ<^B+#FuJrBBd nK0'\-fjR>u#헸LmGso_)7JXݟCYa!Vq!KE@@@@4v n<#<65܂ KNk RE<&-ne@YE~e槎4?;;˦v^N@@@@@ CZ 7]\U?=NMZ.4vhiᔒ5Zz\=]qcՆbp$C@@@@N$(|+@@@@@     <g[SS@@@@@     @0 R86uE@@@@ (     LikSW@@@@zA     $@P86uE@@@@ (     LikSW@@@@zA     $@P86uE@@@@ (     LikSW@@@@zA     $@P86uE@@@@ (     LikSW@@@@zA     $@P86uE@@@@ (     LikSW@@@@zA     $@P86uE@@@@6wɀIENDB`././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/doc/source/admin/appdev-guide/step-by-step/new-env-1.png0000664000175000017500000025375000000000000025424 0ustar00zuulzuul00000000000000PNG  IHDRs3sRGBiTXtXML:com.adobe.xmp 1907 649 HW@IDATxxU2 %KDTEQQ,ªkY+bVVE,5H'H'N& .'yio.yΩg(       5Jv A@@@@@@pA@@@@@@@j0%@@@@@@@`.@@@@@@@ ̭o SB@@@@@@@@@@@@@j0%@@@@@@@`.@@@@@@@ ̭o SB@@@@@@@@@@@@@j0ؒ]#      QOkghgZNAuPZ*u%]~x`Qx髍4IF}:ߟ.h      !+֏[T5Lpl6Uz/-HE'5VXaMlP^*=> ;ޒ3<΃=z@@@@@8 վnײYߡ4rU2EK:i3H6«M0Zҳk_^򰸇      PuVu;ӭMLLmlWIԈ1~A+^XPDL X;޺9mkBkh'׎OL"S-&/Y67sor<\l[=#0KC瀀.@@@@@@I`Yn4K6 VlW%pukuj =8RZ&je4g@-M6-;W~Lh"C\ҵbl#>1~bZu~Z3ekگN+PgL@@@@@@;;dY[RMcp؂S2rdOnܡ'j3qb5g3tk=vmVzceWjSb4uf 86Z_SY9Fvd**̥&7@v] ؀b[ \,B"=/8u'x      ph\.vmUnճa&PkN-fdȵfڌ]{o[٫\q{͒ɵ'[yfffo1EA`':Kt{/@nVVwF@@@@@8 }!!!~tLf΂^ogf:ui}/FGxӯ=NaA&[wWzB͹ي˛S8N0YlNw\߾8F<Ƚ罕402GS]٫YyMLA@@@@@8lL((c߭U϶QڐY, oEzjMo'=t?m%;7-9z엛׮]9\ P@iܫj` >܊M#     1̵>tA;Mz.Wx7m=bJѿښ&,/i9ξZk7~FMZ;FNq&л!9K["KA4wܚ-m\C@@@@@8zj}ijd M/7=r/Xgt、 q`JƗӲs}4rrrUinKW޼+##' @@@@@@@XXXcgn=Q!* fNM"[bݯ/<2`nvQm@@@@@@[[!ުn[#p {@@@@@@DC=JyLK`~@@@@@8 ܠ{L&      ys'B@@@@@@#@`&       p G@`ռzOkG}xx$.:럗xڊ6}t/gnwsk}TL$KjtG7nZޜMRd65p=EQ~V@p* 8;q&L[!Tp>?{8U kRs]fjs<-[I{ƨw[Ք\e﫭@_.gyP|^Mg,ОO+]Gߖ     [Ƅ@J'ͅTjeJ3MRWܩ)%jpJɿS} W 7[ ƍ>ꬿ]wMߥ'ikckgk;t\sgjª]2NЇkӎJ]Z}[K3Tg^ݤ2kjSq/{dZrM/\{~eʭj     F%FT@ $zvM?խ+[" Snle50>C;y`+^-x"{]#3 K9zpv:|ZF?hK~N."MeIp8葎yuBݓ;S*K US,wZ B`_him,߽eymJ[2qK'sݏ~<      P7!P9t}liXlz^]5ծv?Ul*aݮ{4&ro];yS:69R{tWzHu^g;?W:m:Qs,pv⏚̓h{ʚ3FMW3ԋsP<>5빧eLs\^5KW>.87X'ya=L:E&<^䴫avҼ5S;KN`y[GE樧{~ ׹.5snz6oeDk݂zI m7ޯۇу_籟z_}~bVҢ^-= }G]#{YI;}>}&97/_։uYW M-%zuwOߩ6JttAzݕZԠ쓿uNƚ[muqL}m@i˷i^j޸I {7ԝZSx-eo٪?йji ~h=:Bm%;:ӕǗxӾ8㗞S/|E;UtyX +w-V6m:EOWSMzfA;#tc#dǏW+* wn թ]_'`n{enZSfhHdL{7V;SjgNtkyIuŧʆ37-6Gj ?sϩ@@@@@*28[x=WE>Y}`i+S򷱚2yFYoD%kmԲ+M wn}eM/-/<J\hꍣL W6/ާ5wD=>.NqoLcNP#{5ԭSSk-3]8rjREoOkiJLo[/k}iܧ4}iVs &q5:i\5|Kewsgs~N.7Ei] mP{pQ&=aM2Y┆v`zwnr˪[=y.ePc_|Q8@'ݫ&hz35L 7L2afZ12JI~-Y~O4ِ;;GaXO_ևl9.u4XSߛ6ˬ#3k;W-ڦB#tC:Z&(xh5ؖӏݯ6)3rݺX=3RΚ{o=cuK }K%ϜԿn f{vhyμo [ izƈi@5{kъIOM&X/^<feۥܤ%ZOmVzŨSj2Sk܋3q]u5#B gf*םiW@5I1Z`f|cίelj,ӓ/@䪫4OV/']ﬨ     p%@?{ӌULԆwa]͟=]78{&$xX]H_g ORӜKzuuͺY}&؎8~1Gӥ'e2js bܛcͧ15ct^L(Lto/F,W@z]n6xA}h5sYM8K;w*k M uwJ$-XGv/q,Ymt@Su߿:A(";EjtH+\;Z-0OъU喙~J>ԫ63T]OE::jPmG%o4>Ol8iZ<󧠁F^p?\цe/iOd;H}?h+.ܲ8nKܾyZu^?9۹P@@@@@ [85YXiexzٍZ v0#A;@3aSikjf)\L [h'Vzph_[X:Nan`nAq:kᾳz{Egɞ]{.MuՅzLǴQߒ%sշ&밥n+dR$~GMH嬚gUJk bڶB˫TaÜjsbOs}I^c~}5oc-7>]_k:X@s&*LC\FߢISum@.\}ԧK,g3Ztfh+jAAPѓ[ 6 z_#mrOKm{x|3l1&l9Pի]4Ul}`ɥ}4[9UKiPޥl!;Q6[4ٱE$&( )sym6{ Z٣h51lީ`I     $*~[8)K?Y0X#ABibz_i_^ESNɟ'X{'d+'ޞ`A*āF@)&=2%[\-]T;EWjmٚ:l}Z)rP_BiMsLM506\Gʺ͊p!'><IOA͋%s*UGJ= v&/9}&.cf߾=BzjvC==}ٯׄo3=:z+8MvG^ot_,[Sxm<ν0~%{ܕ:1BءK]&6Lg9~Ϡ/` *V&ul{W*,xm. - > ljWɯQje%k)8'G%/vo5,] o;/FRyO4JY>_?o ĵm$u㗙P$W{?_)U֟\_wؾacaC@@@@@MG  pE;R{4o֭q2Bg;K+%eMҰ>m A-4Aĥz:}acAEsWo\?6~fYfϭZ8Ag)`p2S?uJ?Kԛ^/6d()i5{ۦEhZ/47۵,iVc6dۏkog=E]KI |BK~okY/^sտjժx-_X=QON7F剧WRj덅(G>fQ[v;ݜ֫?SBR.X~Zjק .#@@@@@Jp]; Ps,2q*jB5jf_3J Ln eR'S]{&QCvSRk7*'*8nۧ]f) "{g}ua=AM>>kO}LޔqsNAѺM<uvٌ0qȿLVigж E'z.9?RT+ygg<^hC&ݠSDZC߾jrf;KW~si{ꥩjuGNփ4]|Ϩ{ި?Y'KܮAߺٵ-+h^`%OSl*ҟ=|pN4{ԪN`2kDh@K白h&3O\[M ҵP{ :l {Erj)l[ʸW/$;LeG'[w.ئt*6Z&^bcq6=qef}Ӎ7ҘS sLh^{Uz^0˼kGeԇj=OխlK3>~Tj7P6S@ghzT <rӝlp'Pό.E]՝lKu3>ݦOO[f6cLr柛jv Sdh(;=E&˞g5 ncmKjjaӴ=V`,d~. @@@@@@F`x}y9a[^е=3t{b>qd;iZq)O     -y9˶fnO۫mTDtefLQHzv+ u٫tњxmP&g`fkNCל\{it ]z譟?CګizR]IlԎ. g/ }:!      5Kҙ߬4Etvt\H}hҷ&޼85 ҃vzW 佽+M8;&ۼ^Ե8 Qj[&X\^vԤNᴷ?Par D8(.`dW@@@@@@& T:;wyX]L7<8@Cr[l'4R:ANMd&Kmдm@m} p <4\[Vz.}\؀M;%-&@@@@@@~J/l_}H}KSݚ㖂kd;6S[^-=$٧zaNPXVOQ#c @@@@@@7{ڽr}= tNju뭳,\A>eN9z7 @=T b3Ưլ\C@@@@@@FTz>k۴#-y7iɺsf,I{̞,^[Z)-][lmR=dWfפo9r"[      @tf]aG P[t;7pmoΞQVٴqfa/m]j{ifQ1H3  lݝ]"#@niR\C@@@@@iLٟI^ٹ&Zz 5؛{P7Fl},9rrrUd=r><      %Tkkߣ5]*\¹:/`M0g 3@@@@@@@O2y TC|T!      a=|x)       @@@@@@@܃K       ~ O8!      Su0;o@@@@@@gs3K+Gaz*QjذׯZjUؐ`nDT@@@@@@8Z6Y;g nx2uϝeNNOO͛f͚U?,WHD@@@@@@U`'vdjȑRHHX/Z*!     bG@_n@L/ҷEqEA=۔EEz l:Vvdw8v^8I,/JN# =>ikI/igE&_]?Z̸3_XhU4h١ap@@@@@ڦD9(} Tsy{Sjܵ3lJxbS/NݓVGT[|ę 4+1H.֝sSNF{ܫVQꖆLWʔGÝj ^_mixr`fH{(/zxJyFn{~ZE62~Cc[1[:NTms%w+mK=յsޤ)a M_|DF?P,kۦiOjԀz[xJg^rs@S33=iw233d $A͘PԖoe      pD oޮ s&SƷS`ajqu"[V/Ӟ$I?+i\嶿RFܟT+=?y-YWys^תnT%>WHQ#f+mmBO[_]:пh_D6_S#T ms5xآhj>8jfR;_}LcIshȐ&=jryE{_9 :m70J.X7N1 T 3g&UcС֭[7\5^V%! -ݰZbؖ\k6iԙP|bOky(Y ,t$y(;ЅcWv*()= _o)QkMosʬ]q@@@@@C$<'dX{v|Νv(]2-Dr=+bĹُl\I/%?Vm_~8S*d5sƬ:&~OLofE쟵1JI:&+vjJ8F[WvNv&Y7ыKJ5&ad6)'ڹ$$.zM;Vdvd=EvM,Z:S۸N|IEE]566V\<J dV{:N߽|" _/W-udiB=AikT[3v[w]ޫvTd"cOB7 u3i\1/ RU"$Yn}`5 Rk܌o ٣4|p.:g     h 5yRyRPCoٛ _ղ,nmM~ZLsQ2[&nR)f5dswkwUs'{T{vnn]}-\y❽]ZnLbTy+mY~)!γyԂ=xݛw)wT3ۊe?YVۉz: o^/zr'NFU(s9v"| ןՒ"7t4wo7dMU7O?AS-݋~äHW&{wM_uzi'j@)wOaT}5qD6-^yF+ի. KE]lGs5\_S@@@@@@!fl%-٠LH0uͺ* /Hm15B j>Ѫ[^Qph*{gJ''rotꘔYlT;{F9\};/+VIJެ'RUzE:uKs-R z4kl8_IE'4@"D=n=6+2K^( j]R<un+z7 x~ MVK3rOh 7+l妟|r_tp@@@@@?ڑؼ:|BӾ՟jJ73Rͨzwީ&;`wrT+\aJ tTnr/^MG],Z\rrv}G3<6RI͚=_TN.S`E.;|tdpҕY)Ⱥ-ڢrg{^}|V١ Uȵ,|'lt5U mErcG~1eL7B~ WxO"kN(;qoma-ϛ!R ][<«RU"%]_I۴wŒ"I޺X<'|v[e=q.8G@@@@@y6IjMhֳGTظbr+jʪce.WgZhecz֞QF=.ZrMXOl{[X̲;>?TTK)w)dϝd}ڼ{s)wWJ|쳫*>?Vgy Ɩ>:ݦA~$aS6oEZ]IHEJXs.oS._LuxSI%NM%b^3۔U.i Ε\fK!     Pu!Z^庋5BNo߰:v{֪ yF5~r7 Ju,8-~C+ojsmWuC zJ3\+k&cSoԖˆ#9߻Sk|w2׷4*n?aa1u& {[oK΍3&d\/0AM4_LuK]WnOpDVhw4HF78\X#@@@@@@^ gOMvK-ߡԪ;WŁEߕf7KVŸE6y{.      phgFA,Xq5sII =g^GMbl귔YΊϛ]TQUM)      P@Y PuU7jcq1ZNOOG&NenwsaQGdڐJkEe~ݺ_~MC4Z<7^SgQ6ꍗ}fl ) @@@@@@$P`Fk*}ĪKH1A'.dnZq>d>E՝+k 7ۯ9`tEL's;ulNIIO ;*G*<i3@d(X/E5,@@@@@8**x\Cf =mmgZl8Lkg8TU<):86~DSJTQz!ڴ+ϭOА~Ӌ6֢WJ1%ckژ &LӅ_3bK#cABa5{43U}4n"7MR?54mIq_"m9A@@@@@joyv2K!G:m]i'.&o=bjcin=G@IDAT7W4TptkβmsR1jet;[EZCpь%[igӚiTvK:*.&B BչY|E׆>=Yto ôrkdUV:ir?whW ʫxl8pAg)Dsogv/ Ƚü;Gя蛕ϲovlc+SNm)cbħosa'4{Tˇ<ℑztnjQƙr۔E(|#"    R޾\ժP&Wl\i^fy ~iL fzu^mַW躩(*̥ hTK_xbcٞg=S}o3Xl%$ tr—.5Euֳg*U۩&9 ,K;)~K.W=aX3&*X;lFM͹v bޔ!/.Mo^)5S`Ř|syk&e3xKk9z;&_PFPF/Q1CЭۥ\}_K, QnU؁K5]^Q+JwOB#:o\\G@@@@@z׾TK_ &\BLt%$$(:::7 jxxJٳlZL&| yKb\5wo jلҫ}=ٷ]GZdƼR?oأZ&2'P[vg+dL۫d nL 79[5e&]tR}'w^9?~::6~,&S>'FlSb.Ȣעk0ЮAѶg! ЫYpz2QD}9}V/[a-՛      4jjY\ቕU܊Gҥ=lqp5lPMz-^{W0̎fN~,-Lc5Υ}jޒg]ؖE%ݰ#SgzYb,|uN˟bb`m֎-`=1K6F]٫33kR2{y9N&O r x_s"DC}!}B.uM3-k>qvPN`¢բUKhXv40-Y2̏YFhĴ%GMWb@?jJ1' i@%O4_Qݺ26F~ pTB@@@@8Jj7:w:J xj+aTAۯ*̢K :̵K,\-!.^/- }9YR~}u뙥}ͬݑ&utaFN\{sEei[zE%d f~N+]ߧ~ܯ&ve[jn zb:KHT:`^3      P@{6U&ᚳ,IM 9Mkg("إjR7vgrkj:7)sonBDKq1ν7QM]{.Zb Е9NF{,[oӗ;dvnadf>S&}T63G:׿ۤioڨzǷ#ܑWIc]>쟓<- 7tνܹ=h=~       PS2W" ,KM|'w h&sY2flpNͫSo^~_e6ʿeݴ+K:'m3ΫezfIf÷oALofnCi:6 %cٻgAT|@%5m)"VLtr~Zۚkjٚr,"s-ѵL PHTHA=w30 zܹs}ߋ3=kـ` ^

]{e^fx>\>2nn qtn`u{ Co򵮷]o ʫmHx̷Փ NV{}ah ~9UOH;z~8Q b׷l-<roEvu67z@wfaɡ(@ P(@ P(@ PKs˸o<|#oe(dx.h통;ҵyOy\\VeS{}YH?cD5ʪatkD #zȼ*/~7_E[XF yuSڽ Tak*sΊ膹_ذ_ݑ_dB /׆(@ P(@ P(@ P.I-FOHPVmi^Qj(қ"$OU!(@ P(@ P(@ P(@)`ncc)@ P(@ P(@ PP%Il2K?~M6Ef~]g}="={С\]]롰(@ P(@ P(@ sIb |EvA#˃ m۶M(@ P(@ P(@ Ppr!PXX__q }||(@ P(@ P(@ PIskRuS .^&M\a)fmuԹb(@ P(@ P(@ P5)`nMn P(@ P(@ P(@ PU`0p,F P(@ P(@ P(@ P&̭I]M P(@ P(@ P(@ P V(@ P(@ P(@ P(@ Ԥ5˺)@ P(@ P(@ P(@ TQ*±(@ P(@ P(@ P(@`0&uY7(@ P(@ P(@ P(@* 0[E8(@ P(@ P(@ P(P ֤.(@ P(@ P(@ P(PE*c1 P. ` H3rf{cpvuֶ"IXsNmyz"Mס9vnW̟bf4?a,&y?}>v(3QwL KSa]ѵ;Ӆ (M\&P(@ P(@ P@:s* TX2u6>ڴ`H< j|yvR9^ٱ^8wm9FdX9,ډ5iƫ?s<CUTŻc\)⸩ƓGO=IpBlv+%EG!!t& P(@ P(@ P@$E-+p@xߤW1C58| tspuk/9WWKnq_zޥ8?/,;֥^誯B\ԿZZĭ BY4uz4[ϣcg| PG wF P(@ P(@ Pp^-q 3'(P.Cpe [莁#Gb? /W]nÛ"a;Ϊ Wޜbd_ބLk(Fjo>H0O?-$H?$Tm_Ԗ|#v88 E@%.{n6 h'ݏCmGo&وoajxr|R(mS4Sk*/<}_D Ou Y V`0Lg1e"Sik/?tYEq}G=~۵kDgu܁-̹/u=FytƱl_뎏2oY=pSs})i9Zkdzo8>[OÌ1Fc-`ړZE;6'^ NYkK#6?;% 58$u}"r`W}d O 7SkS`sSkE_DMGVvkeM s؉A; ?_f'K%(L%5{A [a 61Hx^"mupC+Xg:-O_U,x;ڏ(?K O?ˆ+ b=,JT5+I c?Ťeq>ؑ!g{w+4Pͧ;ZNEhfnMYf~L@;Q{ N]_XSq<۽1%H6BZv`_N#ظ;[IVuݎ/\0\I QIha-,vYJx\9@qjo`s"̓{>xl8nbVۗrpK;$H*Y_-# ޣQH2ɶDfl'qpS`G1l9O eջvғ-no.Kg=Dp Hj<Jߤ@#Pn[Z} XE .|3nhp"J{"46m!ѿsJ~ߓ2:ځ9+mb^]bT7|SSbf)wN.Z}k'sf(@ P(@ P(@ \@1Դ@ɅK؝)sOm ޝ3?>nҺ8WP֚?$*ª2Id7|t$N%{{'='|_{)`[Z _ݵ W JomO`/ 7Iv$Q$[\Taf&ZQ|e} zeګ6k¤t4F>$ָoxtփ$aӤ`Qzhp$7Xڛïr=S/aR J.ޥeOgSWfKOmP,缇^:{2Zղjgq!RU|6^,$&*.mpu&&.4qp"iC>ݱ>Z t#&+ -^M2+Q퐤BM.z #]S%N|(@ P(@ P(@ \@it檫bBl /|r[yU˪B.U:]P__S)ciU 4ýzItbXHZ9ZsBuebpVVzE پmv,^E%&-'c-kUYǴJA)pX8R\gZJf ǰQ/O߮G K)M ngT}1\t_%o{"fcssk3 tiٕ(*^͕~+mPmoY%)-$A_UZ&{@K,^+luZ}0L`{sUZpDtgVQTPքrqQQrF|(q[}rJ }uhK M.]ޥ^ީl|O P(@ P(@ P`\{zEz ӫ]Zybݮ_XBV #>dU5֫}2ףn^q<}?tQ7-s-fP'O鸿wkO8ܴ^AY`;om犵¢ov"S30+|<̗CX8W_OMpr{Md+/ODh$,} ձlmٲ.IJKC|5~g<[-ccܡں~xi-sڶmĭYCӏuU>cO҇o-fs]Ke^l^TtkTwl}X/S^{A2?͇t7N%u$wwJ>xa_I+ΘWc}ڢݞ2aJ]KةL&(@ P(@ P(@ TY%IU.͂\t~ " X%AW6섚[ oSOHO}QFY 2%7"oDFܿkvU K. ;mbG~ ^X ?  0J[/36H ~Z9Wkl9pFKU韏ӂK}N>S8%ߗ3$[Ѿf?UO@ =?J9~G6"S;2rZZipYɨM7n3G=QCSEw/8|S_\p|QuI[dl肴| 7 pǎ*shBu⚠Ⴢ_bڳx+wu]zXh,,bg^/utt-[YM2Ѯv-Օk}]Ir*(@ P(@ P(@ (S .^wZܻ7ZyQbIVU\U=rR Wʪ` yn`Z630Wz %k}-?)?J/\=Mk1 ڦwvzﶓ^ ~AxP+L޽ 0Cs5dv{n׿>OfiJկ~P $d5, FJodH~Rx5k!W$[JZ7IQ~IKmKkUZ<μ6ޚQ3<*kZePui<$_T2_2"Qm:Vy@k꽣ch_n(@ P(@ P(а̽NS~-M˦Z Weho0oTAN-=|gW;V\K WWO" E'JζXUF֩oڪX jT9x?k_YyFk0r(ێv}PL 4Aψ`jK3gj*7U> Z5wu#{k^:շ3R(@ P(@ P vŭVCWTfj۪< l[2Z͑n/\^ mM6ueq&UVAe-(ц=$xUܾ [czit{ e߷g7iu3=x3\Z¸{~'mm:]'G P(@ P(@ P0[GN6C <vո4nvIU0ԩldORAz?6'Š0y 5w2$=ўM Ϭҡ_CӫrzwnZ_2gn3^rf(@ P(@ P(@ P@ 0[עJ5qʱvև)[ vo:CWmGZ٭T xm}֞:udkjXqK[/A~gS:W{ol?b?_pݩys`6ɷ_$JRZve(@ P(@ P(@ P@]htIR]hPΗhsoM'X WԜ*ϫ:%Da[5uCS_n?-- U.ς'sU{(@ P(@ P(@ P |䭡66̵ԼU !{UWj(@ P(@ P(@ PuJqj C P(@ P(@ P(@ P(@ P(@ P(@ P(@ AsIa(@ P(@ P(@ P(@ 0k(@ P(@ P(@ P@`06(@ P(@ P(@ P(@ P(@ P(@ P(@ AsIa(@ P(@ P(@ P(@ 0k(@ P(@ P(@ P@pmb(`Zh`:Wiiil/(@ P(@ P(@ PuD -itIes1(@ P(@ P(@ P(@ ԪYUn(@ P(@ P(@ Ps :\(@ P(@ P(@ PjUZ(@ P(@ P(@ P(@ 8'`sNE P(@ P(@ P(@ PV̭Un(@ P(@ P(@ Ps :\(@ P(@ P(@ PjUZ(@ P(@ P(@ P(@ 8'`sNE P(@ P(@ P(@ PV̭Un(@ P(@ P(@ Ps :\(@ P(@ P(@ PjUZ(@ P(@ P(@ P(@ 8'`sNE P(@ P(@ P(@ PV̭Un(@ P(@ P(@ Ps :\(@ P(@ P(@ PjUVƝQ(@ P(@ P@qq1Μ9ֲy(PhٲVӻ%Ivk(@ P(@ T Pon_0pr5(@ A"۷OaߴiS4jԨM(@PԂ!^zݽv`nQ7ωLksglj 'jsQ0۰Z=cǎaڏ ^O=6jHff&̙`= 5kȧNY s7hjl۷cƍ{믿FPPUC;oڴI n޼Z>=s aѢEزeV \&#t=(@ P(@ P(`8r9@U/Y=Vlpy%wy'Ǝn?sZJI~ƌ{*+\-ܻwodggkd]tI|W^Z_57ٳ'/_#XOvZ4n?8}]mkU]-G77O&9r*F'-i6mg-5:Zw; r1{>Ū?Ab$ ?Fg(@ P(@ P(@ P 0[gOװ͙zԪ Ci|M7i*ȫ7pYf]t-܂~+ܹ%%%8p6>Kbʔ)(,,]wNU@Ykm혻r=,Gxf [6 u ӿCbb* S17ѺkIccwF#fhݮmR$'LμT/yA~ ]cuO-(@ P(@ P(@`0$Y mݺUZG%U"m* <.&&F ~Z^=8ՂGUZZ%5Gӧ [19[W! [ނ (Y#Sv״me}6D [Q~AhuvmŇKamf.X|uY7VQ(@ P(@ PS/^F{U%5/ e;뿀 k~Տ &%%/nrՓwڐɪeбcGt]˯zߪ`nZZ7|3vڥͳzꪤfזU/_W_ݫVx [N{}'KRJV|̺G"..+}b@hwK|4\sVGKyXM 70 018 XS.% s̛Yw\]ul(@ P(@ PA \GC8rơv@IDATɫsP:#7UĈR_Vaw,B P:P1ٳgcРA1bnuӦMhEkz-RV{K/_~w4imb޼yۦMm[5zyڵ+T_ӧOǚ5ka===:6U9uK[&8EctL] 2jAN9 O ?U-b$[gWZ6̄ ep/ˍ7mBQ%.o ys Q"ig{%e 'Z߇wd0 G ayžφ!^ Ŝi1*,U)'>һ1e!猜 WWxoap;y-y9BկڡgMk_~;6^R _1:ʯKdOW. Z8_)@ P@r9 <2q)* id805&Lo^v',Ƭ+-C ۖB"gCT ~h!ۼ Ґf/̸ŴbܴX5 $I]wϓ+ vɄXb] z?<\$g{>k KH 5h7 L[!Fu8Tg ^)g=Wx-ZϔqsސWˤP9Sͧ_4N"I;XPǢų0Vk{ 9SǠcu(=Ge~W*U 5q(Pl{BCQm=d|ʯ:Lt${ߋ9C8MH[}I0f~_#iqQ@ݎ^mnݻΚTӣ v ?,G\gWq[jeȵ-̽rty?CuDaH׀GZq+A]Dz4 Re[$b:Te\}K޵*G^@Xr e#@*?C'+NmôL˜/gݏqXz2ܴ  &.bxX70 NC"%e;onؿcIY0rU1o E 䪭y)q=ܩ^hLvU5&9:weS2El鉘]nhJ{3ń!"5cR= Pw M!]/+Sy SE--ơÁSŲUwl2vNMES4ŁI[~5(@ TE|6 V䪺UplX}dskRuS fͭTq!r3 zn6lފI܉!!.eݦTMeMi3nux\Yn݌BL]_f$8=iV#I X17W\"nE.]trf1 T6cx̊9Q֦ʭ "~f9$,6ܺ>D\- %8eyk?M.X$W٪X.gz Lknp, ǹU 7R7*5Y3^(,}maw  φ!sYژonrg5MLO7o4bVܿf|$X?ljr' 97$&!"} 9^۷(@ P5) #N|n~Wc&\3uKgf?&5|ySF :u\qӑX(@ P'3p dffj?gkEP$.#/h;\7S@oBnDщPTL(peǥKp뭷BMY6*걄9{~ԍVPMs8Uk>dӱsĄkzN6ut Kl@m Lj1|KDLB%ƘV\.,]43 a僕~z,ND]:{?7bK1̰ƼaJX_~cVkd,QzgF(&<dmױ͋b}`x椵'68N!"[N^n.+Ҿה\'>h7߲VK.V7B *|s:cR2E -S'6gcܡwNǟi3Zzw2Tӆ !(p r.N=珈A=Ay h\i*NSDKBHG]jJLK/Ng\dڂ!d}2By@`2M7ܑcM)hg8)R~LXn@K1F1ɩg:<\{49h%i-S!%]Ç#O=3$"eۖEaԕi&xa,b+NEzJ6 U\*n)TyL{_D-Oj(}G+Yʨ-ڰ7Oc!~\q;}]s\ `|D P*;0p@acrR=J{xky.=%wv\涽Kirq(@ T*Э[7Ù3ϴAA7 )@S>'j %%{y+xO hzkSV?XKTo /6\QqOmpeH-ʏ6ILJ<ݑ/vؿ'`M*-f[Y|\ngӷ"0m `}w-HT=+dj9֐H$xqMd:oSk8󋥎Ѹ1-+}zwcl5.OflojX^酶Si'd8[ե`2x^z^b_?2|C PmD\)Kڇm?\-Ow<wMM3 ꄙ0MkcܼDj/yW zE{.tblsј?oclO\&}a3s%WRދ;Ww|.mbYIV#e4scy,&HOwL@~۲LS7[IY_{;o|IyrVNy R樜+Aj#Z|7h=] hycUw}x'!6;H'g!hͺ|ImcZo4PjIࢺ} TH>6k)pO@= l(@ P5رcg/{֭òe˴j~agfyTf)@++֗33\>۔[>+j9^&֦n(SFlY"CNp+\ֱ9Yֿe[W[(lTeu~ڡ8=qr~'SZG2G/8Y_ShYH}aT|6fmU\.nf̳̉nml2_tz?ڭ1{H%7dT?KP^Tb=_)@ P# aHHd\.GFHNAx_ݼQ&XaID|&揇ׯW3S6&OpLlE^\0x9 &'l Yiq>ܲ>"e,NPkuiۆ*\[K|Oiy1\b@ Zi[Ϛc ~e:DtG>k>"$kN7QK̕X,6D!vǖ5 G΢:yPh<]b3/S<7N7A]rcF Pn{Gv}b>;ػw/o.3E`0@:^nɒ%xWѤI:y FQj,V԰k*_υ:V:V(Ke~7zUú˗SgVr.7[W&c>̣˺Ȝ86k1o0:k|qDkWޙ}Z;כٕA _ΊIҞFmiErZo'=sX_c¼5zp[BP(-?ߖv3> %eKU[ߡgOlW ;` f.9H?|?ځm>-غlݍj9+(@ \N ,tA4,(S ml͊@Ϳɢz`+Z$w6:y[FR`ڔ_P!,vJYlg@`B5\; wW#߲j\C74Ϋ^?5Cdz%_1NC6@=.ПddGd?M#dž"~'_*K2(@ PfWRڦŵ] {m7kImScw͡H7ך (ഀfFBBk߿-Z@pp0"""kaƍxl@W͌@]\d}XcmLVOKP6?j;.r<-" >ŭ\uAP,酜xuPP=` |F!N$JJ#~nWEa]uviVzsm {טO^!n+纮}K\R&u5g)ך+<IJZf{;XܫwnڍHu_95@>ZLYؒ?Cw|<e> _V?0j3KK /7[1r0s\(@ f6DLO>~H7+>ݍ`.+HQU*o\>:|k_+#kG>Fߌ" 8C~6-|1݈3\juY;} s1KSO%8s\nƶݪ=dak*PWJ:^RuIr~CraK4_|wlLJbtXVV>+̑Ȕ-Fv2>&2yu53  n0\M PN B*ؿz]vYpkaw[zA_:T\]Ҹ:D@nO;੧/ɓfIpO@0m63Uu j--%} ^g{V7YI+gY~|ڪT/i)V>[6|n^m+PId ߊ[r"ζW+y܉HKS`y/mvE!%%Ƴ˶Yo,_(m4 @]:ګהn{`h^Ά hJk{unv<2Id8D}1ep6ힽE:yRY_>gfdj4쏞0?e@T4<$-1+ҳe7XfKo%$0Y!bVX> S$"YV7#'F%H~9m+#j+xfhaq,J_n}Tz?7C-/% P(@ P և6R2֞QA!#/\u(}L.58^8/(7^ #WZγy^145@P9Z |I0HLSlZC=_"fbך,7ʦШYxnXSLb.Ϛy)ϼt}_JmЙ_sHM Ō"m?o("|yѬM(p v%,$,}k ֧~,‹SB??=ocqUvJ)| DjišQ30w@>+r#U&e%cƨ9͋xЈ6 !5I 0?\q5t4ۀEη!0MPK(PCiLȏJ{~l*ϙ.]Pִ0 _-a6_RW0x-̟c E~/".Cd)"夼w!s:Rϑe;o Tmm }f>-uuA4~c[~9Oڊ~; P(@ PE%Il'(P݈3ZR|䜃Qd*eiKU]š{<'d.r7sF+ye{u2rII?Ϭ:.js%GI3nl՚A }IrY{WUW;+(@ T wy ÆqI2b9`(!ldwÔ^cN6U5՘33S7q.Cp5}-ݹs'5OmrUcl(@JPv]j?׃9)gιj9jsyx'?N% |1NWrxݡ e_ZeYq9UQWהklT|U|u:+{ݳ2 PU QSw `pG'j31[검wwDG 7njov4T\]`ۮkϽS(@JMp &n(@h֬N>6m\eM,N Pכ|P]T;(@ P(@ Piii񁯯o<Ksrrp9`)@ Pfo>tZX5sTFŋڃ>իݯ+*`q13(@ P(@ P ۩СZlYT=rϜ9Ǐ#00Vo4ܫGN P~ ks~[M P&H?ͭu"sQb P(@ P4|sZހC P(@ P* VEe(@ P(@ P(@ P(@ ԰@S(@ P(@ P(@ P@̭P(@ P(@ P(@ PiskZS(@ P(@ P(@ PU@c P(@ P(@ P(@ P5-`nM ~ P(@ P(@ P(@ PU`0 h,B P(@ P(@ P(@ P̭ia_'D; P(@ P(@ P(@ 8+`RWon`3p P(@ P(@ P(@'K.\ O&ۺ%`ߴn.N>mjB:WӺ׳H?]M]ѭuqz"׈έ=ͺBfI#7]8_'!b7A˯I*@Ѥ{M57g$%r:14lِjX/]ϣ5?vnnnhڴ)ШQk(@ PwQǟ$F*!!@BbED,xzN;ϳyz`GſYN)C IH# $ٝ IvdugyOw@@@(`n ^7wu&:=$51h}k]vj]-,&@; _⻋EёM]E?5yՒGzf$'&O7L`wuu➹mV/Z<'wMLLejE 2o.p̍cB+@@@@@s y17K5SUYfd_VqcsgkfjV'-\t I.lF6O =@4 0_vH_: "[j }˓Gx<Ғ+]};Go-Cjw4K u~l@GϹ~sOB@@@@\c3E7ϵa[X%z Cz%%cE)x4GaVǘ5knW=vSPx 4)E?4WG3ՠ}5S-k/OxDɲ}r]_hԀoaudIm vCj@y(Gv"    @g t'˒m& Ru:Ɵ,)&z)&[f_Q)K?!_6W˿?ma뾾ATzGJCQezߘuziz&*EeQB^w3;mߞV ?*      @`ަu R\QoYu=9G.-答 MS`?s$tݳoGU,.zs^j=ufԭ:{zHiuKB;M.0oFM!^q_Ƚ~R+%$@@@@@@ tSdt~B m:jEd]QTm`bYS 'Q{}M=1uOΠnu[ &:GXu5`o6Z"'ӌej`w˝- K wglG@@@@@u;:VX9lO[SzZg{ ph3<00]+oeVYj۷[ʌ֤Ax?@@@@@@0t~Uԙ@s5siLM-ߠ@<0%%%߹;I@@@@@`84!OwVR E)c78&kC%      @ ̍CD@ TUUICCƊ~m_mmJRN      ] 2誴 +@w+'U[#uM#\pɨ8o^     Q(@07 :]Fؾ}"9"Vl_-ϞiΎ8@@@@@B-P?  "11&=aN${F@@@@FFFQ 5ҥKYĺ_nGĎTڈ     @H憔@8ظ@     b.b @G8wvmG@@@@|h! fظsedn7     t~ . |0MN9|w 2JEUMD[(v     . % nظv%Q?x7WxL8J+/ ~@@@@@ ǟ#@8S ວ<".;C*G@@@@@ <@OH 3&*7zP;b׺Pdxž@@@@@H IG !OHict}YJn)8IMluGGu      s#6@vZFz{#'<;aaL0 =X ː%ղpU,\] G|`}nL&     H`n':t'r0759^۳y]vFYS,}._8ٵ~oKIE\{9v~|"̽汯,뙑@@@@@b(;]GF@-=Loy~'K7T[\F::̴yC|r}ka}s<垹.r@@@@@\Q ;z]/~^ |Nΰ> K*k]e[Z7dPt),>[+?,7<[N?$8b%+-Q~>ru8RݬC@@@@@ FQ -v&z[F+ dQA8X<~x'Wf< 3erŢS7;ӏ*dJQV2i|yq<#     LG~#@T hWe<4?o/INJ(^YF1glWϗlk*\U`@@@@@( o!pGzJ:*Bѳo}٫+aݼ8v4 MrZ%CS1i @@@@@>tE Oْg1|L6.ZUVm*'[+_.kU0^{myYIqqd@@@@@( n#@t owď+e@n·+\eJnf׹Z«k=qH      F$@N/ӴA2kq-la rYzԹ$+#z\m=E.3[mO̕     *@07ZHjk뼶ed @@@@@ذaVCkYOm'̳;u[[ @@@@@{bV\)^z :4 F!o ;@r;vȬY-ղmg5S)Ֆ2     XE@IDATڌ:sÇzH,YV 91;@ @s:Ŀ[nΗ`㏧[͗ss\{47vN8'yxF@@@@L+WlFm7J*B <UUUyf 0﵂ T-;O/)*2U됂R)z&E&|=uCCGj _ z{|+[^`+qy捯Dr>~LqngWu_$@@@@@"\`Ȑ!gftK:B-ms][n֜O`}dĠ|ʐsb7f<Dzh&I4sk^15&[TVmdiʋF9G.9QNW)Takj~^m-Wיí/?d`V:ƙJR+ʭ-V ;^dkĄxϯL@9Ӛ?5@|/@@@@@ g =F-5r[[äV k, HV&ç8qDҳQ')V=*RJJJ$?O    DL(J4QK9B>岎eu[ۛTk2Gbb\q!R[[.     !w W;A07$JYw=׏o̞q1 FC^>h!!yAh     "+wYw@N"zիߜiDҚrYuwn"iV[RSScMqmALl    Du]' ;!2\7nL>̮#1C5TjTtpzzz5 l@@@@_}̚58 G WC0WHt" Μ9SƎ+g}vHF0y   b     Щ4;{l9eʔ)aܰ0+P[[+a)ܰ0"J`nD    ag Fƾ+@aA@@@@@"B 1qi       )@0ӃW       @D̍@#@@@@@@@OB@@@@@@"B`nD      x        s#0@ <<#!?hs='?ܳ'Ol͞=[^x mϹ|)6m &_ʻ_vni͘1ãޖa      D2  >\ʆЈ#dR]]mモ~>;O?${%     D#bZBC@x@[پ}virKQQs=vZIJJ/ ƍeʕrJn7ްeȑϝ;Wjkkz5W_z:u5os1r)HMM\y2sL+_~5 /2o(ka{%ڶѣG;c/'|R6l cǎnAvay|V]쳏\{b ^zUπ/!~*6o,,lkV__ & . ^p2k,R˲O>_|Qm&ᄏhjpzڴi˙gڵzy1ٳĸֱ     @{02=:9sNcY@ u]Vg[nEtಲ2W*yꩧ@򩧞*ٸq 3S5pqqL}GeTL@3iYh@ Wyy衇_~6ٺu?m?#niBx +y뭷>fX_U4655+cƌgyF>`р}_z3SLw\R4nMK.O{! ,~jkB+Ы޽@GiV,      s*@ |Wr 'HlldffZIAO<{50TM::VG&$$X#_5`T z<[ܗ_~)'|Ijj5RWG4x{Z#puD QiiirG[.ZHtԬN騣p֤AJMGNNK=@Huՠpu{V֨Q zArZVVd f k vɒ%Hbej*۷u OzS,1cws& >XF<#     Lԁ^@:3 ՇIQC[J:߷&Mj@SΤ˚gOyYYY}u[ Fk{mn{[#wǏqZKSbbbNPgWY ,k5i@ܙ /W]R4`᯿p`ue X~㨙vO6 ;=˳     @k#ٚm@(1bu\/+/hO{GGȚ: ёZ!b[}]kYGjWGjT:ؙS}d/^lWu:ɪѮׯ&U֞n:|k k̈g^{Yetb/*-Gug jcIq {};799YZ&~      @[=E>}uo tIִ]wuWokVӚ7Ǯ }+J>     aRd#@N-IlJJǴ1;Q_ 0ڧ|u-%-#]Gjy z`W5a}>uT^ pT-zuje퇯ѿǾ5~%%%I     h@'/`gΜi/v}r_ ӠtkۮAdgpYġ j_ jGנZ=Ͻ@@@@@%@0ח y @tI~v     "@07B@Ѡcƌnz     x       s#0@@@@@@@S`@@@@@@@ F       s==x      D܈8 4@@@@@@ +@@@@@@@ "Fa       \O^!      !@07"@@@@@@@<zz @@@@@@qh       )@0ӃW       @D̍@#@@@@@@@OB@@@@@@"B`nD      x        s#0@@@@@@@S`@@@@@@@ F       s==x      D܈8 4@@@@@@ +R 66VvVG*wܓ@@@@@B/7ftj"۩2CQ@y= @@@@@ 1Yԅޙ=       A 027(. #       g      %@07(. #       g      %@07(. #       g      %@07(. #       g      %@07(. #       g      %@07(. #       g      %@07(. #       g      %@07(. #       g      %@07(. #       g      %@07(. #       g      %4@ JJJHdǎMr{yPD6o\畟%zyWn)5 S3^UeRaW~Jzd_]E֯!9xoMSҤ{~?J),X镟*kp .){W~*ٸƻ|br|Ml\ܫĤ.ң@a^ Iҳ ٰjW~Bb7+N֭XOC5K_?pWS~W~a߾A]>6!(}]ȫ8=xWeͲ^1gn^YZtW~LL2+_3ngOZ^'m˞=.{n7'۞S]I=՞xq'=^zi݊_ޞm?7}ٓ}gO~=Yؓ~^scOy5K{ϣ~.I?'ޞ|{WIWz޲'=_y˞|E{IϷz޵'=yמ|u{I/{ÞB7I{ɞH쩤pTmg[wgO%LRK7mb{5ҵ{OI_V\([6oΕnݽncOZV-%ERVў-Y9+XJ6xgeKל^jF6YȞ^=5G!+˥hj{Kjcnq͑l>w5G\5D9q Q_W+W.rOH^x{͡-cOݢ156X#|7ر\C,q c!5%?ۻe;t9L1956(5J5J!S{OI9'}"C?z'=yȞ63>!5Dks=Sϣk #gFi5G~,q |?0߿=lO=oO9J5iY:n}Oؓ=aOwa>',v7I{-B}5Dg"k9">~B̞L)~55w>~c~HA_sE9:A^skr]5Gz fJJHV3eg{}l=? K a+`3rW@Ws|@.ly}4ki|qqwyZ۳i&Vt ;غW{W^k}+ϹuZ:^s۟}p9rZ>{OhY_y?W>yƖy_)#W4*뫼h1?Wy_yy_]rwL#}<ՠ]篿=T^U<.}RySjgs}+Ϲu~l坕ٟ<^O&]o[]_g|KuZ9^}7;"_M*l~+7+}߽צ~_}*0uS>C} A?l=f-gsj^߻~g=C;ϳWoM:PzTWWK~G: b ?N]m.e:s+)>G煣-@@@@@:SQVjf `f/]3$=Oa-'۲k@[K6Ntl     -HFZؤCu>L4%@ c0Z     @Ƥt$vE[@@@@@8 qޛ@@@@@@HE} &-֞ei@@@@@:@֪.&t\  ^E׈>H      @{ l\BM`n4m      F`n9T4@@@@@@I`n4m      FaZJC@@@@}]YJ 7Jrr镗/=z`OT   @ps4 %-#RA@@` hY˼7^-![VK}}mv9׾vm#C*)ܸI*kđ&&¯n@@h@brJkx05#@$ WErh  ?tZ\9"h\_i}-}UV0Ī+Q$389ehv*Jx>7ք -yD#6_HaAdĠo` W%.2|Q_w]>8쒓d맏ޗ[$=o|ѕ2"`FږGM[z Rf=ٯH1K/ ܀j 'Wnloj9hrʔTɁUs2eE<~Ւ{\~$U_KϿ,P* Hjj :x9ܩS'Ͼۀxd?  jG pNxP  I>czOxd+qhW]&q}ޅRYQ!/yF~U U2履"48=~*yuf1W;˵GN~t_7\+?93L>S\rXnOrmt3ry4"c7>Z<e4yMub;*ʭίəO]*W=Rn ?Gj 7f[>rޟ5~F^mqFZ~~wcyL}>yI/uu\2j,c<;v]=Si̫J7%ܣ> >/SY~Zb-f @@ D=sf9/@,}@@4M7J%6.NV\!+{{u#G.!ÆIR__ߊȈ6nWT! &UKz .rgSr0]3[K 3{f͹"*jD#eGrlSiX&k4ri͉7;6T_=L$?~T&aM5-l.Sʑt\dv8\w9Gfw1zόn)`o~{?F!  fK~}DSr":M_@@@SG^vj5#o *_}yoW^x9^|tb+{*YA}j%'uYrڭpd'YkZfC )uJ8׊ګi ),k>YKoo~ 72lȖS\3r/}@גY-ۻhiKYu Ss97%s5-V{-彩8 2㞹f%}45Jۭ|N쬮}~glﷀl   K]K@@@cJ.)ŧ/_y6jwy'eҳg$%'_oMr{q(\~?V5jۍ5[sX3UdjݰѬuZQY"?neVmΊvԴէq a3m羔 WYČ|}hUIm֛@tzщ[U&?5,oLzJ0C.3m'^^ u[Yl?5sWIrWÿ5ͽ'Ȫ/G&/N, /x|Y57u}yljyЬ+SjO<̓;={34᛫ t-XB@@]']gϞ]RE$@@"Q &&F~52``gG+c޴tӍȣ= N1rtpF2z>] @ʴfȟ$uXl31T.e9k2Ӥ29[κe9_m_/{zKn8Pke]5u)u 2M_o17rMRf?<]2JnصG>%ǟC6y6rk"898h<ĚOM!䞗I]8BλP&Uf*ʣN7H9N@Ps^iZ:Nf3j  H fIh:@qYt @@c ?{C^|ڔ䠉ʉ.pfX*ǎ$e[\sxˡr{\Xflq? fۍf_ HN얶o͛+$efJFNw@+Ӣݴ [5*); XYԚcg۔?MRRR>T˽IL"=ʯ*Vꕯѳ |i~RDW~C}[+ yz7כx;Ma^`b8C߾A]>6!(}]ȫ8=xWNfBX3x7|Xg}fugHk6F3=.{n7'۞S]I=՞xq'=^z`/|=a|W?}O}ӆUˤƞ-= $|ƞ돗Xk>G\ړ~soOyϽ=D+=ؓeOz=SϣO=S5R]Ş-9H |=yݞ|u{ ް' {kP_C^wؓ^ou=ɞzFkIɞzI)U"%&hk^={Y5GX^$k~^[gwkd\q\FsUH!̹v9S92\{^C0K}\CĘk!z-3#5k1`9kJ'kK{kVv'fkW{ C>9{ 5j}\C9Oؓ~7[ĵvkH"9>NR;k_:S 2=:btjd v     @1-xC &:`v,327@(!      u8u       Jc3@@@@@@@ sCK       @+@@@@@@@P  .u#       J86C@@@@@@B)@07ԍ      R`n+ @@@@@@PR7       Jc3@@@@@@@ sCK       @+@@@@@@@P  .u#       J86C@@@@@@B)@07ԍ   eWT@IDAT   R B PZZZ@@@@@@ v&0i(       VY+7;C@@@@@@ @@@@@@@  +7;C@@@@@@ @@@@@@@  +7;C@@@@@@ @@@@@@@  +7;C@@@@@@ @@@@@@@  +7;C@@@@@@ @@@@@@@  +7;C@@@@@@ @@@@@@@  +7;C@@@@@@ @@@@@@@  +7;C@@@@@@ @@@@@@@  +7;C@@@@@@ @@@@@@@  +7;C@@@@@@ @@@@@@@  +7;C@@@@@@ @@@@@@@  +7;C@@@@@@ @@@@@@@  +7;C@@@@@@pVR       @]]X[9;@rrdeeYzäJR@@@@@@ŋK^^ x[ v>m۶YAu%11q$S"       +$--Mrrrߘ-:@QQTTTȀvGS"       @yy5"7-٢3 T$ D2      )2rhQP\$ D2       @!       D2       @!       D2   UYP1M[֤Bg+f1q%3MpdzB*)ۈ51!y2&w-o,c6qWHiŠJmhG9}p\<\>{!   p ߯{v)T =՟#      Ё@UUnVvmA&ieVnZYTiUu+<'F111JL֖=r5;Ab@@@@@@cǚ+ybo+.UqCz~\Q`nY,tj}gƾ]7+mC_)m_uӵ7鯍]?%w@@@@@@IKRD"Uǟa.CfSHTͷ*3?%4{"=.eDK; rU]6eгt7-^Hڒ      X`۶mzw5S 75U* ]9JK.Չ+Ye㋚%J]-*۲ Lp>ų\r~PMG2al%S֖e@VݲǺj,$       pZvСC̩ז<S1UcnmڞJo_o^U:YS'LjMIl{Ry^]em[42-bܴ%5eT-*(VITy7ir?&ivVf v\U5$Oq-^*Lf$̞  Mo6 nVQZ8rZi5Hj5ȃ5e$N׌USM)ffjڤfzpZǕ\@@@@@@??^n޽***Ϙ1CkY?~~NNk,KטCzui2AH9~dZ&n~s S31e`q]8rxgьpn}C sP5e&a&d׸en 'N>qiw7 Zj2'LzU-^Mǭu{zꖹ.O33oZpMQjbYz۩LnKi}qe      mVcǎմi:έ] <'s!jz9/.PIydJ?0~ u{hFu7ߛUM7ܬӓ5o־UVn{7iJVoNhP~o)[+Pu.Mغe?v 6dܼnn>z&;Z:/2f1Q;?r      *֗vi?ԱcXw) Vު/_ok^tj6b j1Β/f~DӢա+^֫bSɳ朤o+:!DsӕikmeM,7ͭu=[=-s}u{Kv{2V߾8Lfޗnԫz:aիX ;+o**VCYǗ+='K埰C햶[isU:jN.Ne <      A8sd=vk_Z%ԿX&5?֗4L3RTYv0@eV+^ WGg|_-1GjIz&+*1[5NZrܫ̼*ba];u\eFm֯mz.h&*gxF)׹Th8SUiIjGz4=.#Mg{&-zXهQV9IJu*fLϵ:}n?}M&J~"yJ4}0! @@@@@@xuM9 ޺]j4-ICBQ[OM!#2u54^vyXN..$'аֽ-M]3wCBQ=Z{6u؆|; ޶[&Gtr%4      VnB><Ţ,:fV;ԥ˕ +y/9s-L       @W/WJʹ:srD@@@@@@~3UMROpzTZ        =       p=M>       =       p=M>      0\c(9<UQPaWzfC+3@crzʬu:nҵ8* #      -@3vbSMGmԶ۔HϛU 5垭!KXڽd*Ѿ'֨F%+0WI4OdB?kI>`       p2t$85L3@Qfv!׾M!KՍV ڶ2MQQQlUmԦDe)-3/*&Y[ j)ћGTT7TnkbN6TŨ-=yB?VYZa}y,UA_alncbJ-MLԖ} m\)6Z3̹Mf_~m7),PkTeeצ2Wn3XV--ư[wWHig@@@@@8y'oHz )Z7QCk:fjo=*~[kJ\+jJz#&d)9TRCYFI+xޖUr[BJUD)UZ>NNz *gy(~F<3N])gA"RGʔIYa}~yMWI*M*VYM? Q)bieFi}<%E[vCa,啎Rr,ݲ!#?EPjjdQɚec -yդmT],9NO*9s]u0     &@0707B4JG4??"ɮ':|𐆟n5AǒxOw&0\=8Mj J?%km?[q督 /6Tn~;M`U][%&fz[В4at`j+l\2J)F3jJO'&}:dv=ޡ/hSF@@@@@@Ձ@\: :^/%;i}@YYre+~C愑 @ƛ.[>S6$tN[ԖwcQ"Yoe׶{\ SRVC~OѷmŒYjzC4_}Y&+Bk*^e5$y9މngkyzag{+5:_)3)ޓ>"Vk/K"     '!@ܓcQNVEqw.  YKUYY?ܤr$&jib]{=Q]rrjՓTZm9k 54] oLMkkR ޖ;_>2릭4e]Fk_7>֑yoy[^v9|* ]16:,;A| u_Nto0v%S̔{ˋ      @`ssc)N՛r].UUQŎrmXӏq~csO4*)~TmgnK!feLՊmڶRBE%&YS;t\g+va:4Thm;MW瓇FcQÆF]yO~S8kwSE~::BƎ9en].)'GwM:YP*RņG][q}xT5~wm;5;N7$s< رA_8N]̅MeT+6?vm}j[ѣҩ @kmZf76^[2S]GGpN{Yv̶oY*Es Jݴ s;a#XkT_J,sjײ=8{_u$+?m{, prIg.P_"7~wmUaoo?^pf9PgUaۄkF v4˥*-9qwtmض6%qSĀsj.L@z%ΒwX?n{9ڋ<NF`Жw=WkTCn>TQ]n+%+ЯNSZ(1T;r8.}V^@O>9G iB  $:CBlSt7F9wpѷrm^G8Qt^!fuȞ`/ś”eJ3-s̤:А/LB@`|A}\:΍+uv > J jmX>gŋ Tkֹe<ׁ LKTTT?fN֕3'5k%IҕL941iڶBEi1DmS߬|ct5W)*̫8<7!cj={Ey;&Z &gqê2I-:-]1T$6淖EhiQY5 '++&jk 4Ke7yE⢣;kd 45iNdM_v$P=o-f;?GٛyQd'eVeMjPyQx+Q&-U~Żt۲\{Z%.UwPkn1.F|v)11QK*1*]sUV}~tvf-ڴ:FLԞn'ި/MkR sҴwڧfHL1iZٛ+8 -0fLs2Oݦ(41Aͳi>Ϳb^7}[VhNbV+jYA{LuRe]ʊ UEZ4TT9vH+,9[4%d!Bqެ"_TSP>h2ϙ@F55erz/>qӇ眯aߊՕmɊ0J$ݪmr6:E).sdlsqu") \| $ N%\kЃKbL˼0])tմH ,%e(16 J]R?>2:?|:4TB۔LG"㔱,MЙ XV 1Xd\śC2e&ܢOis7^~{Tl"MY\woe;>Fee5t("Yg:fj0GyNrthz_^6kҖfPfj(23Lj++v}L PeCmCͰӑ\^@ZZn}9^JTcWTgQ\Ї# 85Oɾu9X ~Ŝ4hY)ީh펴fdi_'0ٶqSo_sGOrGڼaD~ UZ-en>1XM~XBWӎAgܫjb}ttz3@ϟaQO5g2MN?XΝuúXEB%d3ڟq_|)|q {SrD Qj C~ݝŹOSt5Qۦ{xm}Xŏ..UOgg:檕#s4-I\s[;9/ 80&۵.?\p%}[-kh~+zDˢ " ڸU}yg.N-OR㬅2 jn`@BVlwXXrk5'{b﹞"7K4~U^Us M֔ (_/ۄ8w 9g䝿J %/t۲NxOUe_R.i;O֨+=wݪT|s{[cc6߽L 뷫Ht0ߥ!b֔+"9+Tv5@3oBENeZPcA  @W%;gk^O롤HO˴\ϯI>~Bv/QZJMTi.cOն+w,n~p> =?NZf}x\(xPO<-˛[ӎAxs'5U*]h_{ U99 U1S͍6*nx4i0CtmsFDG*\k8sl;7svybN8:皂π /pIKMK<ω!dZn,)U^^k6f',HM֥,3nr:Tv'៝uny}lsΚguUȅ{ȳ7xo4+*5W?U՟<lM#Gܪ>r+jkWKL nں_kbuٹiunmFlz\Q+ڤo> {ky)?;m`=!M%/NѼf]嫵PBfNɓwT54J5d5KoZW^8}܁WiV׾1PmyhmblS_G>ohRO//z^~y5+؅/ޅIlxy \`2dg#9"\;ZJѪ>lǵr#P˒#O==TlzyEnO=nʻXݘ)'U.ν?Yң'{qYOe@>_/lHc.\)39M#7}wjw>|y>Z 񿕤Ot|5/8 /IJ,Pj0&z8Z;05[wrw;`}ɏtv e%&z~ze/в;pB&(nm^,}a:.:Agj3x~sT5js Q)krvڮU%dF{)^詀K3hAB5]Fk-y'{[#~F ͯ~ (21WwWܑ)y-ߛWx@wZs\~V[ ǓC)ḟoy"0 [}w.[9[<5s2phA`wn_Zkf#+U&_!}e}Lta=:f.vQ]'[k=s> m3}?n%*YW@>hG^vk]f_k_2t,Ϲ|=Bs6烞׫<ϩX_?AgYًt m=kM>(s/~]WCSs{^HˣC9  pUkwn7h-/oiOc{mN[ӻ<_n/k \A @߹|@IZ*Q3RW#Kzxz2ǷOa&Ui޿bC1"J4%i*6%}kmRqњ{u)#J[*Ͻn7 9s 'ôWxϋic,YKUn_-5*zʹ*jo5G =e,M6o<ݷ= ]~NcU,6x.jn{<B{%Hobm}->xsO26mY~]/~qLf0]sk vʱx>vz.#ͭPv%hVa1[DZ祫uKoo;afV)9*4R\J:Tem I'{ſ扼A8ikS'fc8Tϲ ?ѣN7"LS0^eWq=REηA4N"_׍ϕJV"d?Ѧ>|zPtW Sft iϹiyO?2-PxaJ3j f\i]Y׷ƶһC+~ow0zZ? |ˢ(8Iг칧o9zڝ NFL4KMU^Y>}_W˓`G,g.9 =ܨl=<BNj1X`zj1M6qy|aݻ"ӯ=M\C')l|9sNO 72Nk6&iil@k}v%F 75[VMeA5;gi֙^SST5ڢ gZѵMxe!X:ջ/QQTfji[psn ,k%J 7ۅ29~6ik=syF~qlk_| Ѕ@O]mw8-ݺJV+5e؁%?cf XlO*6ًӦ6)+| 7]:a/>inN" [U>Ӌ>c:9zcLFrjSqF@A"Үj"nnZ+7X|*qM{Pm.N8bL/|ϼ<=<_*w8g p56/C62XynW]nO˕8[koBO+Č]gr4Cc:]P)Ub5443WCֶjlԑO4dlj4[bm;! l:9_K[)tJ enD$@ۇÿǍiպXWk>_DF n{]kWSc[SK@A&`Z{ ;QlMY@@ {Z| T@@@@@@h-֣!      sOO:       ma@@@@@@8u@<IDAT@@@@@@m(       p:=>       Q@@@@@@@t {:| @@@@@@h#fQ8tF@@@@@/qu[-s%"       g _,%"@G'Nh2@@@@@@СC]#ZvKD@@@@@@@9%"       했       @ sJD@@@@@@ - @@@@@@@ 攈      t+@0["       /@0)@@@@@@V`nD$@@@@@@@_`nS"       ЭnH   @pҵR5Kv\m3ڹ咻A@@E`n0S   86TCZ&K35Ґ9e"f"  A tb @@@@31:6hźt?mYgޭ͕wR7k/3}8wkf}27><~g;-/׮}3;7{Yy=oٻqqrdSgV/UguzlLofjsoJRcIyfSVY}_sg|Vwa2  >$+@@@Nȉqi7ϼӮ:oBI%"*1Vsqo?Qrf]~Oo(= v߮xVke%\g^tOQ/dk˞2z]O?Ba*>%NVN͛Lj򊊔sύzvmn'^%Zȓݥ;Ϙu.w0i4ܦ#:kUޡ̹#U,[/M&ޮ{\6)NkןVzP    Aa%S@@@NO~trlCلwlxVn~sw.} J?B [id[u7.խ?sꞄx}Q p>uCkrlKMHSiQ=|(ݱiiö}:.j9֧+Kuݭ?PVV,YtE;ۺhbM Uf]/Kׂ.y]cR??.>p.h;gRu7*Txo_3:MUL   O`nl@@@S&pOL#Q[non ٴsoBBǬ輖 ufY6 S7Y}M<MR⮿^3g^t#Ct_Qsa넧K扷i]|Nsj>$@@ 5   L t~`5UjKWMMB4m_Fnvo3a#_LVֽ_\x{HxC޶cFFJ+jzGNۊon}^1뭷TSIVsRefiZOȻtލv(u*qڜmx_,@@@ ۗ   i$0vw-U 5Jzf2XnZZK벙cհ9]kwWC;*]LO~Pg}GqXoᾎϼY3M95ot6I׍ׄwwc+t݅Q؛4BW0].[Γsef쉎&OpL%ϟҗTSHK-훛    Їu!&Y!   pz "=loayiвT=vcyl(o?QwVܥz̞;*ʴU[-_3텚_2W򰑊hj{kz<$;kͺ1ݡ'Zəւg=$ &|tlZGMK_kx{){YRsr6sCzss,y5+給hۦ%M\o23=   pg '  @0]1   ":Fny&m"zƏjyzNwL4٣;۞gfjDwnS]k]GtP6iu\:gf  =:Ժ=s}   a:"bҚޅ;zO#Lt] 1688 556 (mx@IDATx xفa # Fh jJFԨhDy|F+*Uj)u*XqyR *S)`Aa(H YrN6Js]ə3s9f?E,      TQH      -@@@@@@@ZEb@@@@@@L       T-.#     `@@@@@@jq@@@@@@      @0U      8@@@@@@%P&PnnSXXX줯E ?>2F@@@@@Iُfjw^lR[VpppU tQK. Z@@@@@@L}xTΝSoNN`SNUi,     4j=ĉ o=k­lժcł     Ԇ@L'Om2kP:FֱbA@@@@@jCZhe"     4.LxZ@@@@@@0!      @ ԸE@@@@@]S     4.LxZ@@@@@@0!      @ ԸE@@@@@]S     4.zinA֯Z_*@j}N_]s.Թ^Sו( 8Xu]i8?9,G+:to.]Ы>MV7>ګ4.tTPhsEIq{ܥ7h#۫~W /ºtu7yrzKGL)֬Q\fjTzkI    4Mz|͚?٨#4&fY_zLϼ Z,=҈8^ p;w2k]v=Cj~9mGkXbqVfoOخ+G9ĥ |hoݐ3,i kr*>vL8 ;uлw&y5u&,ܥkI@@@@@Ru>˿/W?na7c,yC\g[0+8x:ҵ`Ǟ]9M AKI:?CK7޻,o7@{a_~mvrK/4+v6?2˯ /^C?TC[($،     P@^/F*xp'lZF^wur9:h'Z|Z jܭip F=}?kwhM^>|u-0iVV~,MLѾe__|]_I]b }}L+[}ꓯ~L4 kgZzY{d㗷kP.3?lGsgpm|}Z*EI 0lEkIs'ߙCa15HV}#iCQ&p|g\ {+;Ub{gefmW5PJQkXcjO_:35{a٭iJ!ӭv9+r41P^SNO{DŽDz4:Ђ#ukB=KM(TC ꡤ#uc|{les葉aou{٥:_7n5B"aX]L1;>ͰKzh=5J;R?[8rHڣzesumK:|R1[Q֝67}۴ 焾,*4&G'DŁC&TxhÆ+9"}q@[usX{Ӟj6{yaK#/Mi֒mvrRǯL}QGvVUv9M46ϗDwΊ:VT +cM . )ɴ#1f.I7v7]1:'sLܳK;j|F~^     g""{&%T#o}ʇMW_D鞛.Wރ4/Oqojm6uуsfVoCյ ]w߃vYݥk>0ziCjh~>6A3~U6Og>Vz]k +֧6v&XֱfWZ5ʄS[ >RY='Jãzݣ=uC`Vz`hύyR.zj\Wߣ&2@1/[DXq^Ȏյ͹Ϗ.MءKȶbd?=7,D2 |3#W :W=5wL׺Ľzu[{ӌ͔kl?/IS6?Y9e>3J,H,BZ(R1WSMԧoWh?XKlȰ0ϸZ{Njgƭs';,EW 0FomuèvEgY9*~i\6z:kƏF4JD++ [V{}tR?-Puߔ~&E;kT(ƚs*4sq&Pu9     P]c=-#Vt4׊8rx=t71K$nFΗdڽ+k˿%] 4+!CGL0݋Վs+m@fO9uI?n +t=={n*gpWػtݑ[/+-kKNQ8j E 5VFS<娽}>VV@5=ZK3zĝהu9C6>{J+-yZN@>00mk2a Jo~fMJn`\kճ *ߌok$1Kqpw!VXשYn=o=B{xĎkC9N:DS][W~f=yLtnxޞ8]Z[!U@ >_?Z+X`:vƽib]kb.5%sٹg'84}6~[8Gs!tQUVZw}cV+~sUnaK7Viq£oӳ7?/[uygװ*WR,½ >ZM2~D3E1#ڙi kM4>+*z%^<&?~:JK?a,[Oœ'tΐ>Ǹ@3`z^NbBΥEcҌWE@@@@@ft-j$UEJlG@@@@DnG0I7>KOmzޥN<{Is&Efq5Aw/,TjVTliTaSN[( cU^ ):VAKWEzMREy>͔w~w'}2#LZ%-',e~jiE1Xҗ-;jP4}%ͳtkJ;ucZ` KI3 Q=x;Ep@;[Q)*9Szﶮ +a0m Lֶ: M;N&eϱuvW=     @i>{3]UfJXt%Y=Lxe?-LŴ}po_NSW3&ʩJ]ZWkEx+ Ybڅ_)LzSKyzz@Zyޗe''!_ƨcae^9fy{hyҗǥ[tASG='m@i;(\JsdgyUN?ل    T Vд~?ә-v42tQtGuڅUAZ~@6hnFin.yvP \}a'ŏatv.aa`u:Cj unRnQ:wvڝnyu6C@@@@_;q볇u>E^}v@@@@@@N6/Y6@@@@@@|th      05B@@@@@@,@!چ     4@L $@@@@@@! `jG!      PS<(4 @@@@@hѡm      @  MB@@@@@|th      05B@@@@@@,@!چ     4@괩m۶INzUfff=@@@@@@1 DFFVf?½@@@@@@@S-      @*a/      @)L@x      P}؋      PJS)"     T.@r"      T        `܇      0-      @*a/      @)L@x      P}؋      PJS)"     T.@r"     (      u!ĉ 4@7ov?AAA]]~R'5Q    P[}|)CB 5,PPP[[lf͚p  PO^^}\] TT  u%`]pP.]nuUuS\\l!ܻw_'_ k P_Ծ}{u*ChpCd{FTT  u%`fRxx[p2l@8z:tPK h֍w.LuL   P')kƾX}/݊#MQɓjѢES:}F8;Q P@@k8r4/, T$s*a; дC@Ӧ.WnnF@@7Sޏ @@@@@@|5?W>VO]7KGu;3u>Β:]w(T]޶kQ55.d<<綣}uPV JQJѲk6̖u\ D=0III 8udтi\V)ˆlڅ    4 'O*++KGsOĽ^wE饪ҕ[3e` y7>?(ud~WIĦVҒr3eƃ9J~nEzM5BS4&˜)Hp aV!   N**2뗞夂ܮ߳Kv=9 onE:f4i\=3Ǐݻw׌3t7XŨlxO+\?VI Zdb n`Vs}ռU+*:|Vº)*dE`Bچ+(#s̫x.:s}L'iuTԮ9MUD.rKnY2UWk)|4w?Qrz Ь1F=9i;oJĮuhCX{PҘ jUŊ2 mmy?II*[Oh,]\?J L/:1I3z}_\T&4a>z9ϥ<YApɝ$B79WĥMOX;5.;5Ư{†\`j97)ѧkK-Ϻ'V^2s,Ѩɖ_VЖւ~Z\hzҵGgݬn.jq(Dyen=kE)W+W6 u/ؽ\W]3ZR5fVVsuM~خ,R_ѱHZC&Sz>GH@*sn?g%ƒ+y+\*(^t[@&&0|;4f͞=[͛76oެ{G_t饗jРA N4j@r>+ַ6MN}ky/IKa-W{o4'S[x)z{o7u53 .Y)rb66,] \4fv,]۵4#qJ->msKV4=au4\3)mA .YYSL}3ʗxK4nة/rt;K~z`%}d/\'&И2%koNz&M=vvV\|_WI(7dfᳵGO)a|\1+E3Q?̏}%c]sRy=Ue~r[npӆIZ{Y@[^Y忮hݶjߢicQoxtZ4Lٟ.7+z{v#luZם[ @ϫͅ٧{f}:}OMUܸJ869۬YbX@NG //O}v~\ړ!ZJgmyQzՑG'?݌ Y2Uk׮ߒ*OXvNՃICuהje*-}<lg+;dK^$SD%-M /<>8G5g3L`VkdM9ŘYW6}J OlO{%gJw =S P ƽT_&$.JzRŻAݕtɫL?ךc[hGA+cUϩL͍ L]9Oɿ^n&%` Ǔڴ1o/hBwwXK<{;â57o&jL^8ww>]y-mO?dΡTT%{Zwʏw Ԧ囹''ef͵Se?3gxTt6kn 3#'ܭκߜIgS  @.ܭ}iϞ=ρCg}/PL'ovP'-{LB}ϴH CSVV, ضm~G]| *d-XB$߯/LϬ%T?$қmזkjL|P\)*!w~O/ZnWnaٟeB%l ̹Y#nQc`)+ہܽ;=_x\׏0@p Em֒Y{L`#,ryʵe(E ҰaVqޯןvLJ\\w3f ة |̞H;ur (<'$$Lî'fYVxq5|(qQ)<Z~(Ysn6A.:|~tnI0όJNY9XݢbKpVeVڿO$wULTy~Qº Ӝ!}7ײ;[5 5dDZJ]A$m(fjW5?1^C5?1O|V2KV5]Etdoђ,UTRdr͝I31^sl2lGoWSjɺ2׼LT7|.LXS'Ƹkٚ:wO;f˲-+uKvI]lgev:{],xsUv;me(\qLw%к/|UgRP,Jݔ|nc _j~;wyE@NuSNwŎ꫾7o?\%aātsXtk8E?ui43nFT5]yu_1hE"wԹ`FvpްXӊ  $NS|{se'PmB.)+S@*ݻ駟!6 +WXwZpK4ȡo):j?\y"gR?pA إFhg/y5jb~0_3x=KSCwm~u Pc͏׾0E17hˆTq|mӕU2˙W:dLE^ WCl?OEk?TxzWc%>vЮwTTnLj/d)x K2o>kKϩ|UFIiD; >PQڼ8\ 1\W蹜m{JkM=_.XsvԈ~a}#|@L0EѷRO?<,;5s3PKg&s:K[ޜOho_S̲)&Mҕ~oߘ==Ɍ rwOfSJʟXJO_ZS7x_1y:Vfz [4ud>wkdk*=Όڞ6,I4{W㫻BJUz~ k[ūwyt0Wqn  PG'-_mf][3)Ϻa5oJ!TE[o_W#1;\rpf)J6lȝ*ݰs=?NZ֌[\H^0Ҫi2uhvٽS떿n@m#3u dcg;V l̕Zf;dmqS`p2~d=QOp1"LXm-w/FϝX\r.!<鵷כخ7g`kwŮ=Y$[s04jݓͳ#?=#Q&I *ӑ@1/'ckǦLmD+-[hf8)/ )*^h"=coP&]KX 5137[Y)3%gRm'F4[͍GNȝ;[tr/w;|.p+L~u93XyM\ ӄ5rHusk3}d{T:bܡjw]6~KbȴO3Rвr>ݳ_ffz틧ΕSH7WxoU|4JEJWE%ñ&&iaC*;P-Zۻh>~^:A9GjVnv?ߠu4S<5׽v>jr A@T9i1A$ĥRc㎍GJ%6/yUy3ϭm?PK}J4tߡ2+^ݫGإ.ҴtwBړյGq-dO輭*,u?)4]ǎ[wG[a; c^oilۮg)oDrP53*,,A7ZE*[%uO;s||y~֭sd(dNǬa:E:\ jE7YYsy=({ٳ>:rzUY<,#Ԧ0]?M&E}]YirפH;oߦsGꉙn{>/M!3gdI*h, `، PSAApE>u+CPA•5>8 u/@A~f@X$/ꗿdz5sI}pdvXF_+.Y/zϋqo^o[rS)9ǵy|do=|ejK2KtNuz+˹WOPźtp)wG/ e*B"/S+cΊ{fPr\Ù9/Sݙn;fdZWy.v7_7,yM3w;*S!ɸ 9z`*s4̴8 ,W{%bF{ZT&m9s4iZeMSy_/'ܫyV-.a1%:o)C@Lk 0ahn0JbEǬOv]xFR2Q%7߫ on$^qSTH?]9ԼIKgz6[}iODLOg+3Lg۾U_3$t7EzpUuoj!Ϊo_It}dkuax]m^n2wb'&L]ٻj7ǚm޹wz =TҖ?_avo_gcC#⺓@UϩQ'*';u6ef+ל'7jyDuU x-M j1^rskݛs5i\xg^U"?I7mU9ֽ9_s\S),J=+, Ԡ@Ά;oNzSpQJF&)Eo}R2OL8Uc%ٙNDpUM:DcN߭sWkj5%Gwi/{\);v\NQ+Pf'o6kPfSM[oW@u_TF%jɳu$缀Ǟo4ޙye8{N접1y7t&j%Z*ZQNh?:G/8SznukԭL b-i5?SgiN(e%535hL+^X۳l{i/}A<0Wu5t* HJ눛kfu/sJf\s<{NC,y%'=I q^#zRٺ+byݿ3=aq5o$=+ìf=V *:/\ f,R)o W lC@vXM+xg%L0W(y3َfw6}:ݕ5ԜUMݻШ)Z0Xf.P$ԹCoqN=+~mKxBY+_$L뻗PJNF sW2E7Մ(v*iÚlF,rO-_ӟn#vZ]2W.JQ"7}Hi![Riβ4%'wW}]ݲdrm+y&3J,|B hW%{YC@@hu`?Sו#4punLWڬ=A O%Y߬͗]/{y;^UfBe梃k}Oʬ2OhUT^xnQgR"/wܗ w=彚STHd>XSI RRNIi/c.eۛe.o]ڤ$ӇP]yB=DsZaC4w֝ I=ݿ"ONwK7QcͳX@kQk6v͏lYzm4fTpN-m<_UMZqgEE lVa!%â7Ӷ]cFQL'(C:G͏Y;ӌ(v|&hM[ulҧc9gF <Qek`    @m 4H{FrU T:t@@hD_EEUZtP{BºiHSKM ;B?6jX@.???A8cw.` |uo]fnЧ)98F@&!`]h;. }h'DIuԱczj"4Tw.f?.*@@@233ժU+>kU+?;;[ǎSddd2 @I h֭ڵڷo_gw7)d:42'O7ٳG PpppSS  ԕu-##C]tQvAȥCi޽߿|)cC= 5+PXXhͰn8qf 4@F'мys;=(((NON@@J1_p/uu\@@.Lg7      @ z T      pV `:'A@@@@@j_SS      pV `:'A@@@@@j_SS      pV `:'A@@@@@j_SS      pV `:'A@@@@@j_ ~G?~\yyy*,,P;Ԫ@PPZl-ZYfZ#    MY x69r.07,p8 S6m     8.ȥ`Ϗwz%pIAeLZ@@@@h\g}źl.5"p:ֿs߻@@@@@EGpN JF 。ڃ    g@0>!     %@@@@@@h遣      @} `/yE@@@@@Fzh6      P_Kz@@@@@@F*@8     ԗ@@}ULx pO WPPPx7u@@@@@@üߤ=vxcx9UGN4HskΝcLrN     @m `ma?ɓ'|VMi3JYK18:gs@@@@@zLFOŖ@nnڴi/} 5k 4i8@@@@@ F0Յ2uT(PTT-Zh׾C*L؀v} n@-)     ԭ ϩϿ^@@@@@!x7d{xfklLmm'G@@@@ULKUo<#zXI     p 4g[N?O;Ɵ]`g,(r(:_gͪ:k!     P*xמ-uͨ-ze&϶^iLϋm G@@@@hqop R&_=DVP{I0{ST-Vp@@@@@QJ}=6@`Ppn*L{wEis2jcCUaQ7e"     05@ 󺷷msXoΑO\uJZ{2j"A@a5Qe      05ģr^6|O~6}Qؓ:+ZFזoi˷-" jF@@@@@LgA<Ty)ybtZ8MQ:֪^uE%=P> &9+     MTLM7njTM4Oȝ~߭vuN['me+w9J^[_ʱA7ꡠ?kX=V|s*+}     ۈV٨~]Z_vo2=N{{I;d%xi=U´42dM^6}sL[r)MFꪡ]߃{H*wz^@@@@@hjLԎx5*룻yZrAt?}{_5 I]'Yy_4vʤ#2'Ϣ:{`򰰂     D05к~Z ѕZ2N@]ү}4= >tVO [?ZiM3zOą&tjeD~=r;#/ٻ(? pp_"ZDE(EEQAbxVET@-QųU_Eΐ@Bg6lf$m ӒU'f+G/kC4>֦©^_QV      P0U]jG[Ik4)J]Y鳯~m\T~ڸӬ?_e^v }as>*l}B;@@@@@r,7g?ϡ?!_oF/_\ྡR n +ycoPilNTN}+UfS'     F`ժUzէOk׮X_L\z衇d?~wu޽XHeD VЯv): aEr/j4YJ@@@@@؋`,d]s5j߾}kon].YU;wԳ>5kc^.*eO8־^@@@@@(a &x%vw {L .,^?MϪ_~%Yf 폟pJ߫6&E^_ݻtԣ.Rȶ~8զUWū*wzp^xxp54/(ԣ!zx,/ 5ۖ=u\gM{3ozykw{ -     \]vRŭl&Ӛ5kriׯ矩;#!U ?6U ѥOvuN_U[#҂@{5ͽ)l6{tAM[rz ʖKm-usN=*/mmFhJ߼ *=v)^ݡ3B2@@@@@J-ciǎ:e3w^ oo^TrxqSRy6:Jrjzy7/J[e>7ynQ/ O˽˳Ow]OmZ6NVoMpՑKVյ rxM|ciaAKx     @c4ֳhm+tiow~`7_A4իZ#JLծ][XT`y[xUZ6lؠTw.     Pbf2dED^q d/g3X./\>U,p5rU*?tÅ'+''4F     P*Wp_&k ) 8 M}?+MǖqD;uj׼,0FB@@@@@X G5;C͚5 /ZY"H9 j6ۭ~Vl7A[n񒒒*,i@@@@J#q}*e+o&kd޽.2X)Dx\V X%e\ںEb`C6    [>L>7Y%A 2?^]t_\%k)X*f      ɓugO>ʲL6%&&` J@ Tq5#E@@@@+S` .       P:W DL^      Qc@@@@@@ `#     {#@io@@@@@*g      `5A@@@@@* |KgΜ'[O_>^cǎ ϊx޻wo/oɚ1cF`K/ ڵku '?w}zWC=7ѣG7^=lC@@@@@ x:l+w:t7\jձcG\R[nW_UV`ԵkK@@@@@(q(۷GѼyk:묳x@˗/WRR*/X7hΜ9Zz,Y?^כoMrHorrrz-_Wnn8/gy;sw#;m@~׿۫nݺ>}Qeffq^`Yfvaה)Sj*uE#GΝ;=?ث#ԭުŋ{ckzZns*!!AcƌQ-{k?6hk-[n^`+г>UqFϲe˖L^zI;vٸZ0a j<7mT*U m      " }+E^ –o׿(,pzӧkԨQ%6m䕵M7ݤSz˷^{޽ fZp稣[ny'4k,o6 X*,f ~Yp˶Yky/{1O.۶m{7ޫW/y6 mٲEO?X;d?V-g㶀/" Ș_-?gA2 YKuYӦMӉ'( EK6?cndǫÂzwuWpSx㱠,-ZHs򔑑?Â=gyg͚5^nԨg,tꩧzm       @iJӮ}>lU\Y)))^fY`sjKY=.K'm` ʊ.[ vY-Y.nWo<D  Yɂtli`3_8Z“͂~N~Dv"+״iSo9=ݳf YdY袋4zho;ϐ-5׳gO/ugz-aAhiРA5tPߍ7Rp^{A` 7oFVzyv#["bi׮w?SN9_4YY`nϸuw{ E׻-gA0[В,fYTv_{3l&wo[_rw-ǔpkٲe#     P(J;]*epZZlI,0b  լYi{cr=x:[/fg%+g3g7Yy;=m&/>X}mv_F+o} ۗX,#,XڱX 6f@@@@@G &@þ,H1~xCGydb+_Ar(۾[`+Uql,{\r2u Y~u@@@@@pL<)`N;4o9쨘;@@@@@ʥryX~P6ksE_15"     @\zL@@@@@@JTS8      P0cF@@@@@@ T4     =Lec@@@@@@D0(?#     eOS;f@@@@@(QL%O      @ T=F@@@@@JTS8      P0cF@@@@@@ T4     =Lec@@@@@@D0(?#     eOS;f@@@@@(QL%O      @ T=F@@@@@JTS8      P0cF@@@@@@ T4     =Lec@@@@@@D0(?#     eO*W;w#C@`n{     @ oak֬,LwQ3FK~= @@@@@*/dS|Ԍ     ;r?1     >4     5Le_@@@@@@0y@@@@@@ `*kG"     %,@#     eMSY;b@@@@@(aL%|h@@@@@(k      @ `*@      @Y T֎E@@@@@JXS G@@@@@ʚv/      PJ<      P0#F@@@@@@ T@@@@@@&@1     >4     5Le_@@@@@@0y@@@@@@ $e7lؠteeeiΝR_7h"s_""VzߤyD~据|ZD~:u]ԈMZjyD~5h""J_,"F:jجeD-Z4"zj*"?;+SkҖD'լƩFgoҚ#jT#seikI(峷joFԓTCMh={VKD~jz@ۈ9ZjIjꠈZxaD~Bjj޺}D\SD~JmspDyOv>yO~?7d'{?ҟn{>Osş>W>s˟>>sџ>>osן>w>sݟ>^ wß]'{diÚڲi?{giZW~c򍚩v׮RujTQS%mim^6"?Acթ(">demڼ!]WU^C4la6O@u673'SQ]s5 }ը/56]嚣{6*k^CڕQ!fQ!rhEnU%ف" {a[o?O6E6Fع]C,r Q]C+k~F/5뿍ß8eGkJ5Jq_Ci?i?oTڮ9>>sȟ֮pQ!kZQ!}}~?5ǺUi%ATwnj(uh'kywޟ5Ǧ "{)u\! vN vNSpE{O?U-{^sfߋD^$k26'&k3*m[5e{mQh龷(5Ga(k2E!96k(u5Grkf͚jذՋ~,/#?qv34Y +KCD;Z^pۢ7`e{,ly{YK5m9jZ>]w^F+-h"{W]OD?Z^hۢ/hy=m+x:1\Um̱ +|•/l<1޶o Xgvv>DxeˏZ#E+-v/gh)vXu\'<͠H7/x%jUhy&mk4;ꊖ1[v<^{s&VZ>Fc3V [1w]H| =ϵs"n/Z^'Z]򬬥x%vERN)jQ.*cZF-hbtk5ZyhE+FU?fnv#vGCD`0 ]qraf1s~كCz;w|,1FU,Ǭ?Lg֭JM6B(GO*Dz)G,8ߵ)f,rtcլuCx     lVUYV:P QbB**di\pq2@@@@@J@*Tz0Jy ݽBBGp) Y      %Q!L]׷#     @4h<@@@@@@=Hسb-ը]|      @ l ]Za5\!g0\&!!     E%zbOEH2T,cD@@@@@KSqR/      PN0˰@@@@@@H(@@F wv-m֬YkYT5nҤh*@@@BBjԮT  s>wޜJNm۶*77W; VףNfZef)!Z /N[   /kejJ;]*S=.ή^P  '`v.U/Cp7k򗸼}^-C'9 @IDAT8NфoBGO\x7HR4wzx[uW'`_fkM:P6Q^#:f]=j{zlaP"  P~ԪV~?2r   @Y%gq\I^rq}7xA̭16/=\>m^7Ք-@9ЇU}u^||.A!KiRt[o>Dr+(  Or#B@@l L|a-[[nC&?;L!W穽ˢErJaި]])V VI>i&&8COLz5_nœX59\:QGzzɺطjשRb 5ZՃ'ľ?  eYG@@ʝ-=}355ZJ@w>:K?:VCûvnףQjܠILT0pLGZ[ѧUl)OxXo~\M|O[n;S&OVMm-g^?-4[雗gE[:_rĚ(c=4u׿ÎۯĔ4ZzȽ5W}3Kxk53?-H+>yR^Y8]>K3 Q~Szr&%7s W ;Y_p}i>S=;]Ycu5W넶a@9iz@k5n}y}ۓ|??-(S߬7&-T/|?_Vj{YҾj@=ߚ|co7{@@JJ{0<"  E=wޮ=[v=z']vygîեŗ]-[KG1K\qHj;32=s4S4e۵prAWwߧ#ow:.1{U=S}5ZvKI][(Ke-xtU^`RN&ensw:5ktS_M6%6FSGb~xjn^NohdzKE%l @(7clrM{|(M4WϺAׇ+soq3P^FC[w=Y =bZ:}w]=  _"܃B.53CCB@@ ]ZZQ*Utbvx睷B%;olu<0w_Ejwڰ~rssb85ҷ(5jjI#S.={nٺˏ ._&)|ghki[ʼn#̫k=OC-?I"%2dk~^pIk+W\X/ XjV ]7s3B͟p^OilnhW_RtC|vl+a@@k]`^pcۓz^x-;ʥJܸ!ӣyԍ?nuwvqF@@(v `J_쇄  @iK۷RJRrTf08mbDUZmUՄھ=7PWZN.Uo5'*П;/%Y?jWW` yVnvJ!g]^KEX' VnKMZ4o=Oh|h{;IaNNX0Uw vkn~}w}[;eVo5ez.W*v{0IիYB??O#.'>Ic_tDHYvϷ 3x}d  Yළ~*B " 1"  P4k~SԶ]{}6c ktV~Em"进ogc@@(OK4  @EHLJR߳QtܡG 7_ &K.iqzl{5=lݲY6iuZG}4A`~]4W`B:@I9K4g"<^ޔ跜fRx{ܮmX0(=4>Y#me|VBy\[_/+qnͯh׊ ͺ'qdzF1NulbNi橩jE Mc1n2Łӗ~Sn|W3>5.c_/_lG@@"GbEV#!  > ؒx7lYvp3ۚw];YォO]|fj6-hskы]b`@@x(=h7{}}5kB+Ih̅j\`bt]y:sc}4\({y8z~Sꂧ:)n `TKjv y3zkI6 CuڴjVk mwQV_Mf {w<{nk$*HS C\[w/谣j駁\ұAʛQૂu\wåI/>6N|Z\OfTF|ܳK}w3@@rSZɲ  QRJn-j ]5b}{(oƒj''EO{VkD=I56۳ioD[`m#mF%"VKTEn׊ #VS#rs]"Vuߑ_DWIHPj_"Wm}cD=+WQ:Fe?ϏȯTZ)"2~[}5٢OJvxF;Yӆ÷Y_dݍ۟l6n2Os'4We͟xq_rsWȷ3Y;?>sݟ'{a7]s5]ou?]w]XɮgƟzɮɮɟ#+c&\]5;';B_sdkKըz-wѼUD6]嚣{6zQGFQZD^gU^Cڕ3ןf3ן5Ga!vkEQ!*kvv-`d+H5AT;8_ɍß {QkkKkJ'fkWkVv^s{Sq_sC(!}N}?}}mssdo9O/wƟ*}Q{bh[ؿydSa9ߧTڿSo_9{4Ŏ([-횣!JիVQ:0=*Ly;vBF"      [d$\JLbYTLe{@@@@@@*N(i@@@@@(Wd0      @ `*~cZ@@@@@@ʕru8       Pߘ@@@@@@r%@\N     7@@@@@@\ `*W      /@i@@@@@(Wd0      @ `*~cZ@@@@@@ʕru8       Pߘ@@@@@@r%@\N     7@@@@@@\ $f47n,Lq"     Quy.@@@@@@|,%      @|L}؊       %      @|L}؊       %      @|L}؊       %      @|L}؊       %      @|L}؊       %      @|L}؊       %      @|L}؊       %      @|L}؊       %      @|L}؊       %      @|L}؊       %      @|L}؊       %      @|L}؊       %      @|ي    T<۷kÆ ϶m*# T^]~U>\ JXvwN3x    Td-X@͛7 UT~;v+VP2\ Q;C$5    Thŋvjذav`ӵen:s%Dr&|{0Ex    Z ##ÛT|-gFx\ yyvG/k@@@@DUS 휰s#`tInMYz\>i=n#PU@=ߙT&@@@@@ l۶-4[{uԙ:})^I){^ƨo>&d?+M7ݤe˖Yg땩34uovܪUƄ4䬞Y-1K_3kV䵐7Ł~TMY}K,ⴍʎ޶4ח f߻v6d,~% Җ*#    E ON$<^})o-1\^y)!S^^۞dk8l2@3޽E+'iPaл5_ux^&psU ^~Otg{83|?֥S24jpM9C_kZQj5\3nӢiKC2?6U۝]/?;eը>?FNCg!Jv1`@@@@@ dffgѓO>OѽK>(_ɦ/ҐQ#5|HK}95 +>5 UݻwW׳GjuA`l͝6Z]vwۻjȻc1&^f̫*\ʤt P׺nve^=9q)0%{K.2RO?>2}ٚ!P~踷eVvvkQhP6Ng׵P4/0`;Ӟ޵ѯ)4˖=@mzơ4 ;Rs9zvXU^ YpƌׯL۷f͏ fF?ntw 6,zA8G=>TgsO{ho^?Z=ݱclǿi4;W6q7uA57lXzΩE1=NZ?H+fo|zvڵcrXmOӞ1w{矷6oyHyNqs歲m;     7_|Ə $-Z(TMx?Y8骓;7!'wf7^˶C4qt=6O l/TtAI:vL-p5:EA]Ү-z|T?r_XO,7]uDMz65x.yr%f>HNуjD{=]W_NtK-ӗܲkSխL􉷩Ǔ48,~m.3UݦI'C}47`;c'Խ&k &-9FYq𙮏 4zdM*<ހf=T_u[ mNvA.6!*y_vܹg]K:@wooe$UΗ5헞kMcB)wNpXX6!iԕ;؎M?\4Zn8]32}:wKN>ϼlֽo<;[+u_-U^{N\{M3h>r$]C v=eGy1SVZ1c?Î%EVhnloҮ[|Oame=6IАwϦ/ї_ܳuX{t;ƽ<^dh}:Uo~F1C2'#4bDgԈ)Z:Jhqw~Mދ.>j꼣zjaɟ<<Vvz 7˯:N4QC{w+9d/eIjGy#4x{6$<     8c5qD g F&MxFԑ=nKF8J&뛑=UËtUor=4i\/|hדz ܋}7iqVfT#v_֕ՃgKy˶C nfL%FʍCa_7KoMjdd3G\8 6L5Þdk/.80U'&vڑJwlSWF{R͢]:]FQ$oCi *=2C r/'wFm/E_WFwG]9s]ջg.*4qڭS碝 QsL9G/3=6;<7g6E$N?nZB/[+wiĘ!jwg"ځn5=f]_ҍ/]"vuoA=p T\-tD?/o?ny<5h$Rzlkqr     ~AgnI{0%ѽR؋5Z*]$7ceE'*]/\ܤ]Ujý{DIpy;W.v'V~ޞۘɵR3ύizvL{lt=>;pN]njҪݝ|G}(;#    [ ^ZTnݘ^+뚪s?ѿ\j,R]]sӥ궴k]peH`=SDJP_ߩQFhք`'u1 럟WѨ!RjMɮjF̈?&)Yz: pz1RKн[n҈i}}ouNPً(PW??Qk d#o\ƞXY,A^RTr1IkβIzuh`X^= :T͂ۓdYcݱ NiWݫaTŷpu.*GJ!einji?]aLp]d[XW4%c c Ì!SE͒|T4QQA>03i|/{=9s9;Nj| ]E.SQƼn3n<@; JZƦSCKBִtbeL9IeRk3e|ʳ>1%i€CtOi˭i;;=KzR-/R~#|0ka^L (@ P(@ P(@.ߣk׮wɈ|qcpu0tqF *\} &e#K\c{!* `w&)'iQ97g۫jmoYcg{딞; cƌA J/emXMQd@~|s'Mޮq[V*&QqSŏE_NqɵͿsoWY2>w|W()o#\N P(@ P(@ PP* =Wg,.o"MJcU&5UH\clmU4uΜ9lʮwYB<+qXg1e#vLĥIҸ$*kwnA +;i L|O P(@ P(@ P"ڬ+&Mj欅ϪGb@װi(nU/B1r'1ܵiߵ_N P(@ P(@ P0ݹ q)sE/0^bOw;@ P(@ P(@ P(P]l`.IC P(@ P(@ Pjjݤ(@ P(@ P(@ P%d<(@ P(@ P(@ P|M P(@ P(@ Pbu+jrL(džcVM"L P(@ P(@ h … 5ڀ;_V@9&cvb&{{cSi~(@ P(@ Pj@˖-q);w=jғC9cB96l'+|kpv޿Z7d*=)@ P(@ P(@ d"rkj2EQFzR3bw]cS(@ P(@ P(@ +!%b P(@ P(@ P(@[60j=(@ P(@ P(@ P@l`*(@ P(@ P(@ Pld(@ P(@ P(@ P \"(@ P(@ P(@ P`S(@ P(@ P(@ +r(@ P(@ P(@ PV L|O P(@ P(@ P(P%b P(@ P(@ P(@[60j=(@ P(@ P(@ P@l`*(@ P(@ P(@ Pld(@ P(@ P(@ P \"(@ P(@ P(@ P`S(@ P(@ P(@ +r(@ P(@ P(@ PV L|O P(@ P(@ P(P%b P(@ P(@ P(@[60j=(@ P(@ P(@ P@rCT&1@.\Ե Ҹ5z^>Py67;ΙXke% 030;`[yW)"((%oi8C()iأDkʈ@6i]fIߴXرY X^jH P(@ P(@ PA40Idm?KW#JÉ#<B$!yy4HŔޗ%JnRP\4 m:&jʺe'E")9 Cp|{ ۊ~YO8/ڂH?>a1HtcJSĨT <'22p檹ʚWޘTSEeޙ<B#Gk> ATjcD,YI8P㸒u1:G!.z)Ss$- C 1|3iq(@ P(@ P(@ P.$s$\ў$oսrQy2 d$mwg8}* ?Gk~T214} 0~hsSx?A!T=/ 0Xat#v$%H;dlb^[{U0uho=/ Lض"M}xkY pWdDQyAmzcya_7ܛCxڧ%> 䋼p@iduFޣ0NJy.Anhm(BO]`(@ P(@ P(@ P@>&F?* !:X:;"r#<| u=.m\dxOTO/(k4hb}U]՛8J P(@ P(@ Pt[Ӄk s;8Dt y tlY)""yNȖi'Iv&Sj^p 5IOo[0R\A҉i9?NDz>JNp?Bi{ >QuNSRRzOMy"FHI9<_)@ P?o[@IDAT(@ P(@ P@ T{2^֞mسgvl݈y0pJAW&q?ACzO&L(,Ľ(%OM9X $ˬQ~JQF'_ù=XD O:@]"%n ̟ӆC.IOjc3w!XbA}ۇm#PF琗s9;iVv(@ P(@ P(@ TiZ']hj/5bOoOėQ|3|ڰ,]=aʴS)| w$$cBTi^H:tԷ gIj*}  R3 >.S8K] xDžcF ,ٲ XN}RJ91}1H_`y%(@ P(@ P(@ P.Z7dn6WwwH,؆aputlr00J S>][aL_̸mC>A2ۈ]c߽1~P{-XF!rڂΖ1GQ%|iR2~r QrU P(@ P(@ P(PeM6U\1txU2KKVal3x5ޜY~l\R6܎cq(|-åԇd~%uD P(@ P(@ P;SY9:O G7{ǘlCFnSv3g#l؁C9*$[e0 P(@ P(@ P(pg ܡ Lw&SE P(@ P(@ P(&(@ P(@ P(@ P*#h1,(@ P(@ P(@ P{0(@ P(@ P(@ P{0U΋)@ P(@ P(@ P@`S?@ P(@ P(@ P*'y14(@ P(@ P(@ Pl`(@ P(@ P(@ P@T9/(@ P(@ P(@ P5^ L5 (@ P(@ P(@ P*(@ P(@ P(@ P (@ P(@ P(@ P`S(@ P(@ P(@ x60C(@ P(@ P(@ Prl`CS(@ P(@ P(@/(@ P(@ P(@ TN Lbh P(@ P(@ P(PTP(@ P(@ P(@ r^ M P(@ P(@ Pjj!@ P(@ P(@ P(P960U΋)@ P(@ P(@ P@`S?@ P(@ P(@ P*'`\p硍[oQ0. vi* ƫ{y4\J@]:.eR[xc=3Za5_Ѩ>X%[V/$lzyp8q1(p _53/?dd'Iӯ9ip; M 4Zon@>;?Aweڇйeu{}m/{60n=L'vnL*zjLF ջK:C,Wt#@cx~2IGgOu@po)@ y[%_D.@ړaf% L <}(*"q2 (p~Rk5\ԨР:k}xL>fGpn@u B;u&vPAS1̂kY`sIgtƶ=n@O%eHݙ~^ڼ5_?<~: 0xHM'' /pK0q>2% {c,___߰`,xXNj $0-4ݵρHܕcٻ!ݟ_?Y#۲o(pk\y1#FF[u#-0~o.RRb7'1Æ Â1/83!v^CDȨDde<{)N(@8OZɳ ZWy  ^0`H[dZc0lv;,C_RnSI;8l'y) Cڶ,:"XYL9sv,q]An+\Wyd^|'!lZ6`ƨR)`Yo(@;Ek+g`Fs}6 9A|Mʓyؾ Q,xHrd:|] 幡1 bs:9rwzF Tɤ|,7/Flg \ž,)GHxTLס2*^;rzĝ׈6%\ʐyyɘOTRud漹uvNjW%neIk>A* P 1X,8>ɗ "\I噇pIg"7gHwxD6hYw=-S?".B)# :pJ+Y/s2Wzb GNׁvJ拽HeiAS>RO+i;R撲X8̏Liq!H:eō֎# sW ܚ&ߗ`A x6RϵR##?M>OB{t.|U> AU2ȡ,S37G;aEGbڔiq_Y{IxYi7 /%5ezw?\O:g+'B;.:P-XꝞ2m<E(@ P0x jn<)ayEESZ<7{CDb:| zFnЧ7/y5b KR/1ZvȰ0D/Ya,M-(Pίݮ.M;ʍgD`" Qًct7Ϝ)HW}$45JGzrw\;Ib~Z\m8ßMԨR_@IKMr|Azͱ{\PdIt])` -ɯHm;ͫ-9|c[Ԇ]|'!w:N/-Y$s}s E_`H}\?=ߨ97ל7flIQ#Ī`Oe?|[),8YZB VKk~ Loihju%2|r6,R}dc߉Er5D»/Öi97׭dD P+2QTlòb/Pz٣I!QZ&Fnb lER*lEcx(SKyo@ZRNa /.dl4HckI}FaI!3<~dAY鼞ZZ2}*|L_% e6%(@ P|r…cfFDCf.Iz]lYWKEZ\W8U pۍu ť[3r,7áYa1&w^(w|N‚:֔H-I PvQε["(rvLEwv??=hESzI48sh9?DWt*-װgv:/:ƌZuHm|z7A=r|bqeg+(H}uER*v˒0?aSOy8pDF{pzL/̓6+GR8o,ԫ_o G YcؼQR #)A=RRũ6-Xz8Wj7xvFy]rAW{6#Vkp^V^^0 %=BY%oCN2L6Y/s,쉰Nl|9"BԅK(C 8NT}^6ɐƤsG=ڸpmn(yNܹ K"_("-(@ P0b݌1R>3a?j'%+ JEϒƥ Fbtl Ҷl) Th36ž<=<_9<]|q̨,|~"\-++W^qL;qnĺO0ITpMPlELa5M8x਺M2ku2N})NA/fVcR6`%np>v̘@ P6¼ 8~l7',AZ\h[69.{/]Zj <܌ iIxM.-I>H^0qMZ<6Żw'|4L!U爆xe|8Y[ nq=(@ :1djfC81ǁvy]q~vᑑ4 rP(8+Un-69|`]/= F P kX-T.~|rgkѥAGT@Vs}ݗ{f={"1#A.|9 c:ywxPn3PV2Q,L^0p <@^ qR. o1tz8'kmѶX-'w/+ Gӟ#NFπg ilَ5gq/^?K,[ PPrWemz{-KZAӢFaIpݓ$$e6p7~p񘗑hSBXy8y,;6 H)KZ2%{<30Q:|d|KFAΏr~^r~KO)!Qo܁wG8vkId̘"u8=Kj~V2͝+B|'=9NoEei$oyf,NP14ۻýh$G*,ǒAhbY1ы^:N2]EӫU&C]!%/K Pq75byM̞>>M H~C-vnLYHKjjnVDKɦP9?=8otm?ôsrSNQ>d0u+C `^f9B{{^'^)s-6 ;}%GrI@RmC9r7D|U+8mˉq_/Ln8%Ð6l4œ[? -3~(@ P0`Y4|a)OWc s^-}m/v$r (@*s)@ P(@ P(@ P Ծ(@ P(@ P(@ P@`S޹(@ P(@ P(@ Ptt\(@ P(@ P(@ L60{M P(@ P(@ PnZ L7M)@ P(@ P(@ P@`S޹(@ P(@ P(@ Ptt\(@ P(@ P(@ L60{M P(@ P(@ PnZP5333+a)@ P(@ P(@ PR6m8L9{09 P(@ P(@ P(@{ndoyEEEfs(@ P(@ P(@ PL^z=p(@ P(@ P(@ P=60S< P(@ P(@ P(@l`rH(@ P(@ P(@ PdO(@ P(@ P(@ P ! P(@ P(@ P(@ `=Σ(@ P(@ P(@ Pp(&4\@ P(@ P(@ P(`O`o&Q(@ P(@ P(pg (.ƕ7x;3LU1U^ 4@FZj Ryl`Q(@ P(@ P Ҫ掺b*](1vڵ-((ӧQXX͛t""(@ P(@ P(@ P \r-j{x}HnݿE777x{{ŋU60U+S(@ P(@ Pn<Ԫ]n[U d2UiT%>L P(@ P(@ Pjjw=(@ P(@ P(@ PU`S2(@ P(@ P(@ Pyl`y9(@ P(@ P(@ TI LU(@ P(@ P(@ P ToSULl9{O^s:XPr~Oױ}qu\|eqi,.={H}v nX\25m& ;u01.ZX@ BK %dZ<.MChޔEXXtIOAt3LCx(!zE߿yF|͋Omw'g(6[ڦ:t,ZN!ƓX!E,4e3T.M& ڼP:~#Җ.)g `q4hFE) lߊdD/ƓX4k:M^#՗dU^#rͥkQ(PꀺP&#܅^ L׋xT_S6Z8t^S~֝ÅRQ1ZZHs_˺*?"+On}e^E\ȹu/$g%PW"PpI)8xB: 5dI!v3 ^)ϻr~!)kq^ 됲p7Vr)Xh5(8KS"[k+ĽȆlSrڧŸGRr޺G Zq c!>: ű׫V=Xtڔ@72]GF|~8eFl1"0׶I!4r)8l]V_X3k mqk>W^O)HSv[(@ P@hj}v4 DGyu<6]Bt# $iސ hT !Mdmzד d} SRUGitj))D϶.|>e:cR[Q!m42 >խ]t]zFՑԺuX2pR8dnȄx|%yvjPx+7OAYCWm븢 =t0#@OqHMw6vͅlt MzlW3wcƥ啶=(P2ڄ?JFOgҢ1,4_| :ygk&9x(֛MD`obb/(Qn0a)06NxRRx$~3 !O*W=ީ~=)JϚ;_+`*&m'1CWe?bM&L)E[/n(@ :2^"pԺ4[(4h5\ ::RpyJ,m7+'.A??4k^RcK~]ŗ1{~,,b(Q7^c{{e>leM5=УSZ)8,z`piGRfa"2}d| -GrMT1䝤K'c\e4.H5-AgLe}R4=E9YFb"D+SD_YZJ;YO{+o&ex43LJ<֫=sq2{ "gCqzZrl;K~~͎}wٚo0AڝQ7k:3].X/߫rLfj `*&ꁧf,R!R2 ?Qp$Pd6S/8m89F+t7K8b k:49)꼧-J85g( Zk" 2\1b么V{rm=cZn~Zc; )&JNDxqIOZz.Gǿ\VHHJ99'$ϊ븹G0g\1c~R1{t(n[vKR>/fK`PD?ir mͻFN#ZuAa T^?-e7+ d6&N(@ 2jؾ(޿Eoſ;Σ nOYz9Ed/BPhP?LRzM{? ܳh}~ 4.)}Sv j+C~hz>A׵4{=4{}6ڪ=>#pߜa[ MKD!CNGF'甒և̄Z48\_zVmYh<İǡr}DFWzS)T3Ɩ\l{IUpiZc^F:t9xs1i8@{Oaܗ:xڸ c-hW]zȘC8(why/xctbp:VHy%Te Xy6^mk,?Ү>xS&TaԘl)CTɲäAa8bc1ش4#}2_9I<qˣ1kXiY8Xv˫#x7?tXkAIh2jbb0c& =sa; &\F|D65Q]cB<&֨֎50'3d{EC,Uf.{IYv`T݈J$Pҗ=K3ǹ]=)qe؆4l޼Yf|]*W`P|u5M{4ͮ%b>skhI{sov&塟x0d_'x]/o0DcC6fEbw혋Rvْq(`+î\yȓg]<\<)x$Ö9Xs&tĚK28gyK.$(ĬAO ߮XzzΞ/ sk6:!P«]E6Ne?em89/g^/MX4sb> D_N0g}]l'u_*Z! נ'Q5MhuM(@;W:LIdёbơf"{vx[}\ZAj%¹ 31}pDd5UR/JG%|(Zl.Hͣ_{ξ.r6EcN:=[MqsqpF?"wq6qwCݖqi\d}{'@|S?@yTm{>4|zc@0EWQzw6q6y[MÆ |2eJmALe.#lpy t>6gBVuTW̸P?={+x$rǶ2ԃ2\׏xC>m+_<)؄ChsbFzi/œ-m_IOf8sAԆ0Y4\[񳞔˗RzC*5 3||Ez,_(N@;n*ba^0-流C2Lcn/F(G[i`|1d9>KEHk0m{ivEz"nߴ[Rsx}ccn6b!njK(P@bS1=T!W\8owÌ$܍yqJ3#t,oj.X G>vHpxs% um^r'4t,: '#}KQ%cC10fH Z( !Q|627!4y'>dَsOKl|C;Kz~+6p;rhsV-BNhawg~yVd#ΒZv3q_OS(;^X|~RWm')~s0^vNmQ9bBە/4kL~(VW4oކZ~ GV0\q8 ľg 䆜CW&"]2cGJOV~xg>ڦκ$Yֻ2ONF;{4h EǍ 'Thq/L'10dJ:oĉ'퍰0)#I-W6;Gmo+ä%'^Ӛcr_oݲ+JbPijZgi.#RAp_{Z ϼ=4rYPOu?y@z5]eyf l.\'μKzǸwrmdEMwCcwiߕS PPbs'+C6>P6j#Ait<qvP)uL{s0>Ƒ?=VIS(trϳC9v㷣:ôRC?)-$Q)c-\=;{Qw,fHh6M}2KǪyr9enۼ$ݥ#~=,]W PB(G &zmvԈ['GajKIRm@,=֟$ZWVo4uF:TbA~;Rx J>lϐd[ p9"^V˦bUIqK8WCVڬ98n>"e|kIixCsʶn\˷lzݩF%]vXdIi\Q6mZ}qͥ73[+1%uhH)Ցbh3,oJ}r6R|Js92 x;thÜ@KoJ1|UR2%M]j%v{dx._S+K8<%;S @RyVDڢ??0D3ΪǬ,''DT1hſpR^_ 0D6&NEj[+vV@UԢ"ɚϩLץi[ ݙ{)7K>l^:˯ԞX9`MZ<pft l oQ(n}rQYcՎpβ͡r};u#&u x[a`O*q]HQO9<{u,UcNr>+}=RU>XsHR(-qnnZ/OM"E#J&N OߘA}&J9y[gKHDk)-@s=xX|Nz?:5ǣ#D/y$ikq_SMI)5{ji& ʌJyG n2PZ&`x4Dٍ~O֗a(d:9Ko\r29,+ZVX:_$PA5֒l $$# ~km =xrhl[ft!ֿ2:Ń(t龓)+kE_)@ Pw<}Xm5`T.RÑOZ \FqQ(%M7HKQw= &G Ѷ\YT;6ݷd(4) HC cv&nX T{y⩀.PMIǙ3ǰ~QQ@֫ITWJOAcX4y$))R|w'eS"pD9c(ꥧ} {_wK>}i=Tϲ y߿B<&c ܒa*S(# ۩rM6N_4Yި5H1b)j~7e=c lJ~lً\{5 +)!X pmFF@I91Թ)jzD갬Je*^Ԣ_ 1FLR\"^˕rY*&?#hU_e5=J5s-+Tδ-Y#wCYKU9ګCAĥ_ |ș`Ot<%J;"yI.ÚC]I>g,]"uq_ې{ռtuMj(@;U `hz BwQ3r򿥱Fi쑇eo\W7D J>9 jI &pyq"3D; xP2 ~&-eh_W2 L~M߬uZ?T[IyUhn0ʘ{O?hNʩ dٛKeQ\Z+jo CKq U42-Z5.)iUu[[-d+9\4>mnJz'PWbNoq5O_Iu5jIRfmZxQeԨ 4 R߀K}HS6Fi;$qOcGZ0"GkyvAxb̠]7>"II 5ao,67e@Uǔ:v{;9JM(H„]3rGıZ [W}<_ӃfMt~}zKL j*ϲyK}kz %g$ 1Z?ts%' PlsUrWecx%ydXPCN;&7-Fu!aN (>"iY2ԩ叏xuEO(p1>LCrN='`^R6um-ys+ { n1X0= C\%a#fvvj␾ZV{ജi+lRS{۔sVsdhg!櫏ks\^bTM#qjiD(K+XvlBξPT]ByLA|UkvR^;t83NMCZZՠ$RdMQ(p \ވTӍ=~)7t)" +vǑ7:ΖN( C׎_BoWc;y=x% (p9%W-?ı+`l\\ۑU?8y1m>і_?'2,ooZyLpf^Py&5Q39V-3MՏP$ veKfv>DruC=_dBIZps{wdUyUVXƩHK -5t&JHڕ4S`ֿlRKM3i):vD%:38 cG215Q0!$AYe]>g{ٳw]Zn8О[N:\.āTUI51fw֞PRv|K?b@JSlcݷq'bmܵJݑQcHzյ<ґ?h?b!M 7$;+/_k̯G:\KNA|٩AumN9&\scBz9^uTY|#Mq֊?N*n_}HR]%}+㿾u8{1wHhKKG8#[#+ }RVNO Yϗi*->wϤS=K9IuIu~G @[-Yxװ7scTm{^)vwEi˻,iW̪nj7v$͠5_G!͈ʷM nۀԳyZoZyNOǴi9N;K3ᆲfZC* @ @ @8=zt["" @ @ @rLT#@ @ @( TF @ @ @@9 r* @ @ @T`H @ @ @S9 @ @ @* H0UQ@ @ @ PNJ^~JE @ @ @ ɓ'W ob @ @ @%+K @ @ зS>J  @ @ @J$J@ @ @ @- ԷR @ @ @  @ @ @} H0 @ @ @D@. @ @ @@L}(%@ @ @(`*K @ @ зS>J  @ @ @JjJ۾+ZvcGaTǺ!W5@?g`\lUlXͱz @ @ @2Y[p^̝xd5cձF F'SA MO>0Nn+$:q8G.rg8F @ @8VwS93h|\zף>"K4֖GZ Kcܶxpac,]66t7̋ڝkYG}}C,mٜw~>l/k_nY[ΖUGyOxGظ1~8Gcό+4FSScL:dHZ길~alͪ-6is]t&z6,\7?ܽ {Iv  @ @ @ $z=1cjoW}S-_%֬V'"=77WF5tXqp1BƜūc]X>Z_$7c&ZcGghl֝SwF}f{rK[uժzݱ.cvl_{(T7Xui[۷ǯ^J@uv̸֛c޼t˩Eu΅M3S͈E9mrbt}Ѽ|~̝u%꘾9WΏx9,XӗukWW{ @ @ @KI0FK]>>La(^{^|(nt,w ^t̘[Mk\. \pe5a @ @ @{Mdc )2O;AtK_YK,u1O9%ⵈ9iy픲(=%=&tv=:XXѺ%hNӡuV>eFGML4㚉Gʢnb|05;;x`gYes x9ˌ&\e}3;8uLݨNsvy  @ @ @o j޳;ќ&}=5)bߎ_W[l~P,o:7Z5vLJʓnYz:;UmMqCٲiM ,wV|)Mb/Z;ON4'Kl5gǴ4C艟.\1Ȱ=z<4{*mZ7u'lw,//eYtNW @ @ @Sέb֧bF˝  tņ,Խx܋MsBJJ|T^jkbLVQ낸mM㑻.Zӡ ;y{vmxcZ /MzujಘcժQH.O ~l];[ǰؔ&IdܭؼcO۽-)-yE @ @l̰nY^hY>Ŋ&k"QaioVGiN,<:{QsIUΞO.Xʊg//n8. ;x'o[bOk:kVLھ1Zoi/m9&ƕfD늗sl%[{%8*{ *E[{z7\:#lQt/b#kUb%mm)TqCҩ @ @ @! TL ԦdPi|ZѺ`UZ%azԌOm?_]ز3b} 9KYqcMqUUsⲷ8Tl瘲6 @ @ @1 pL`f=8⢳?{}q|\=srfxdG7sgM,65V @ @ @Mc4tlb @ @ @@ĉ @ @ @`ʣ% @ @ @ & @ @ @@>3y&@ @ @TS_ @ @ @$y&@ @ @TS_ @ @ @$y&@ @ @TS_ @ @ @$y&@ @ @TS_ @ @ @$y&@ @ @TS_ @ @ @$y&@ @ @TS_ @ @ @$y&@ @ @TS_ @ @ @$y&@ @ @TS_ @ @ @$y&@ @ @TS_ @ @ @$y&@ @ @TS_ @ @ @$y&@ @ @TS_ @ @ @$y&@ @ @TS_ @ @ @$y&@ @ @TS_ @ @ @$y&@ @ @TS_ @ @ @$y&@ @ @TS_ @ @ @$y&@ @ @TS_ @ @ @$y&@ @ @TS_ @ @ @$y&@ @ @TS_ @ @ @$y&@ @ @TS_ @ @ @$y&@ @ @TS_ @ @ @$y&@ @ @TS_ @ @ @$y&@ @ @TS_ @ @ @$y&@ @ @TS_ @ @ @$y&@ @ @TS_ @ @ @$y&@ @ @TS_ @ @ @$y&@ @ @TS_ @ @ @$y&@ @ @TS_ @ @ @-ݫ8vIENDB`././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/doc/source/admin/appdev-guide/step-by-step/new-env-3.png0000664000175000017500000022337200000000000025423 0ustar00zuulzuul00000000000000PNG  IHDRY sRGBiTXtXML:com.adobe.xmp 1113 642 6@IDATx \TU2 Kefib-֛-{z|^[ԲlQlS[%L3M5qGٷϹ̰#i`{ޙ$0Q(@ P(@ \@*(@ P(@ P,(@ P(@ P@ 0R(@ P(@ 0k O׻cS(@ PUu'.d!)#BаAѪ*:hj_rLCڸ:U (@ P(@ PT*YyϤVa\0fѥI;)o{cp{ۮ7;N@kݼ]q[=8owA=Nk-S(@ PA*YԾ{xKE,!}b7l]5 Qz6L(OEc'kp \ rZ6 P(@ P@,;MΑW`X!S>-wGx8 /{ Vސށ#r Z-Gd:h=dyX(^KK>@{ } T3FNZxEP(@ P@M \v%=ǀeFsOGv ;o;;]SWx( A$WhzD0N_̆N4j8*#-*ծCCe_<0 ;(@ P(@j T+GdثC !@YJ,).-W@wQeOħqIaG/=gԋ8 53U=[ 䘚(@ P(` ) .T)yɣ/d %f ,3\һ%93βWЉ^,ˀi@rTo"[QlT (@ P(@ +>I֞8]ڐ̼KȨo;kNV\On<6X5ZŞ5+JYo(@ P5. KB ؐ>zu1֮ZϷXÒê{!5kGeC5Ч1ӢDMY~*.=j+[CKJdc xпu&V5#-F\5db聘eT9 IjH*EUXr소,p2^Elje\ ڛUmM?,R/5'&"ZW\K~¶bWy`*vîY(@ P5*X(`S;~vWXlSOg A*i{6l/gZ}FMYŮ5p E&՘*m'Ia=ΙKΑxzr]Ud1m5sӪ]c]ٽ Bj%R%'@2o3.l4SN}!V~\N$(@ P \}Um(wF TU | c0b[=Lb{29CfVvk^$ZYy|`8kbZ'cscv݉s_=ƿ^}irVOËUx!_;[kc,=!y?܏[oqĂv7&_8خk6/z/#/w\FdE/̸0R/.Lr .$Do^Wg.#T"&gFz`臍KSb($we͘;c&6Ū%٘:7ӓsg`NG~NǔFPYU:v>I)Ać.&3 _SCh`Mx({7 C~y-=@:~ y=[ctl,]獌 Xůc8&Wu&x6B]@>e6 䀱G`K2) Х)nc!M@_/3> othYpWxK s)wQ F/k{Y9=pMqoG7mع8>^`n oWt74/Yu~"6})~[o Hۺ:r!p=[7NǬj(@ Pb؈9(P;;+0;'bDl1fhsy8t좴 7<5áD/r1: H-,㡒%(#TC>ݞߝX֯8)<%;!SiG͇-/rǩD|y"hQs#Z7$˭.|}Z.dE Uz-PtO0Au~\>C[_o᥯Fx0t&=1ɦ#)1 =0%(.+ct\3GY+w 1m.OGP3_ Rq8^nC1P|#¯KYyϙո﨟>za'OwM*C?cIưn!=li5VbzǮv+~TIߑ~rq y WTϿdvf>j>FzVKԭY72:zcۦxu dasGxm厮].鍱hcvO0zcE!d uA*.;/AaZ_p c\?4E/JG^-#qQCc0bf 0֘7_VV]wA׻'XV)jg#nl,aK/:ބ1aI:}@.Ay^\l\ɟ(@ P:2QuO !8#TSڃջ[fn{w5mnVsIEvc$!b3+ezkHWӼōriJ! p8KԶӠN\Y2ܵx^WqƜ1&U-6ff\W_Fmګh-Y> SŠ*Rrofn/W@w#nZJy1 !&t5/-o|m/YOos,ȫTcW7=!AZ <aRX,. ǷI]#ϫ< V~ Zء_PqBj. PWl+. -[~X';F!/~!Ct! ir0HF%:ż\|5 Sز1GxhKȏϑz~ciJd\!S`L ]; 2*I럒ewxyyVĵ8p^悙0Nˏ"O|1cgϼ4S(@ .`/sq-(PR"I_U2# klwOƪOôHĪGdb H/WsDb7\ўm_tPwe2SKxK1a$ =u.,?Odse9*jVmJ9GLoBfԱz.f8Bl°=cH88PFrK[`g5v 6S!ӿ3|ke>9}$|Z*3w4\^ O;_LsGہ$s)vDX삶E^Gud-U[c׺aƋX#.Y3^TTI[ تpe% V|-3.5٩VEp\2Ik]N6g-_n3a*s^np6D񾲯mKgjCZ~Qɡ Rahdg;܌QYX/מ|^=]^CQĬ'ŏR.OwuNw b!T]%-IūۢԸWt[.0 "X8i-X&] [+eVdy݄'g}cqڶOIND\|f`Xmmej>'R4PoNE e$TP&mŧ(QI'Go ShlG'M;%CԬ8`Uڵ`|El_mK P'|_`Mm45dc/)"^7'#^NsvTXl6JU4Qe;0~6d(0*d TnE P( @ =~{%n؂z߆׷M'p8i]JB)0!N+OU+^Z2U{:1-Bc\+zgFc›̟3}R0&ơ;q[Z5<K_c0orx>'ǫO4P4cl7 ӦJ9\X^ /V|< f>1ލ}ďjV%,|Dwpõ2? <싇 YsCDD]­hzIHGhfQmom hI瀧nBXvo `/ӢX']ۼ i_:~nN#'Sf]yd2FӨ&/wIvjд'__|Ƶ2ǯLxYbnzcӶ~+/9pPd=q۔ H]Xk=ː^9 킌˶c۶\r;(@ P .I#@Rn\#$yzJf:GW8ޫeF-EÿaѦz8V:*̔9.ttUAdgBoU[o[zUXΊ$bP~eV;r^ 3@teO}< 65 U!T/K) \F^U\tZぢUGoecok)͔jMu]yҏ;\*n%dHKվ'נ吧e]@˪'?)_ST>[<})_f|qvfʫ1'(@ PٟV)@Z. OR3Sx#K:UW hOE0WPYuV쳴 gK/(^w)+ձĴCj^j"&LH,aNW"ızfr!_њh y*(d WYݜW6Sv(@ PKR&N \ٞ,zD#q_UC\$o8c!:\7&;v_q݀;= (@ PW^A+o=PZW6R&(@ P(@ # M(@ P(@ P TV(@ P(@ P@9 M(@ P(@ P TV(@ P(@ P@9 M(@ P(@ P TV(@ P(@ P@9 M't:?{(@ P( KWKA%P(@ P$LUi Uu> P(@ PYYj֓)z{:ւ(@ P(P<\Tb6UVRMlM\OȪ*YW=x3l>TOx(@ P(pdYW<|Q;ِ_A^q$ǒsZQmy($q(rלd(@ P(@ Ԩ@,52*/b`.֮MZnmeT(!Т58 d -%x(@ P.SಟzOT 쉆rj_,5,c琔-1e` 90c1_C,ssCcOhZ7)Y8N>/FYG$2$cB@M3QL SE"vG0o&AP)'R _$zɸBQA O[k:k(@ P(@ \p!uSzsaZy;-K_S|gD e OBCg)8gp#)MN^\MUa%p 6?M(:qXo(@ P(@ P Kc{EbԲZ7OKŔG=޹&of$83Xv%* ymӾ sETR=ݼ\G P(@ Pe  oX&MŌTeÐ]Vb?bkTE>#p`X`X~i`狰79uw/LU>p~;,Gœzw n ?\cz.{h (@ P(@ ؀Zl$ uO ;l9G\ۍggoĺJ6*?q/"Q[+N,_: m:!9}o>h)n`om_cW[-khGQD? Y5J,=ڲj.Q(@ PjsF t9>OGXR6Lҽ___9Jc¸~o]u1" cr`vy*paN ((@ Pl@A8 lBȑ,W"%}0dkhiUI qj]9{6h<^&XH(([ iK9l)cAԆ18esb,2p(@ P0b#'͠@r֡WqobE OߒX6)Fޏy+I-rA&]pS0iHeɉهYޘ?qʬW $Lb0h5eh.Q(@ P \Y_N+&1,^v{SxP}f-ga1gkU yO[Y-0C,ގfQUS_,3l+Ac (@ PlFA9l*ULK-6-f˲KIxfYn+p0-\!i+ƘZ}edhEc"tТIv-7q(@ P X=֚P 8*8yX?  е۵&S"2[ #/Q[q:nt bL;Ww-(@ PlLÅl섰9SL{Nb[1sFXͱjX%8m-tt94A\3n=g/;-/ CoVR(@ P;$[h @ TKc21Wg$j+ֲL2<)<-IF[Fk0ZhOv!@{|8VԾđ?w}F1L(@ Pj,\(G wϜOOxvO9Fe8n2s{w,6l]{ks"\fʍ*?g1; Q(@ P.`Aȹf4ahj{Z5ix%. \mr*trqkO2Rн;  O1jw-w[SWҬpת(CŲjtVfnXG -m5Q(@ Pi:iv qw;cYnh퉭ǒ>n3-==,ÊN;O#+7| 7G;\Z$`_{lbycCq }ͭYy2 I9njnlfNH<M ٢:Cd}{I|;;{!G2O'q!=~@o憜|"5TA&uwЮKxø(<,9^~;jԦn]&`Έ}"?I0{ se^yT^͖֙=GwY ,3륰\>],{l)w(@ PlZZ=YH ,HTaVZLI'sI@D>8 Oy봈 -s۵MqB6p?X wK0_hcSo;iYK dokZO+um°t9 /!k ]C-߉,-a]Ƅ"|`HetޥOOGsC3o)Lhn\TR>{л'zI6F/驢ι}QCMYڱrt{*.ꉻu8)7/(1Tȴݭ]oyPst]VV+e)r(^; P(@ PFA zqt-`ޖ+ׇz͓@EIrUoH>c8 3 Ԙףޫ)dş瑞c4<.Ql9\%Ǒ61٠'SZ-1% Q;GW&Y~Ԭ6.u역&WSqA,yyʋzv|GTԾLIL)O4,{fі3wsgd?Np3mczrH@QQX iJ$$+nF =h[&U'|*le#hOQ񞸐)[a۲Kh07*Zhf}wGd"[;巣IX|Q,^,j]5n-OMz*)  ijCy'(@ P.Р?ͧ%z V%\'Kvks%*cJcGkyZz65YmezRuGu"i,uJGwe[5l([1IΌ|leSL %0ԯhlfʂߏ%c g7ݞ r E!1أ6^-}]֧7*0&"e~.Rr|5S(@ P*-BBcoMwƄb.~4F=6NE;7Ҷgk"RL)/~J,q]*p1#zVr*=a-f-,k`zFecmP(@ P",ueh 6 B|}} hN+*ТzXX5M&m`TX]&U)93SܾpO %/ NgdH@˜'a,oH@-I(@ P# QE̓z8m&8FkÅԆm2_JVORU]i2j~> '{;*[QR֪'%gJGP#JwM w|l]k@W֫ZfbgqF\CܜѧsStuVbFx7l99ؕΜ xow kKΐES0dxS%o)ʡ3C%ksZ^%?Ej|FkƧ9ٗs.|`hx ik>Wg&N _{Cʵsxل@N9G< T_ 8N{_l}^ /_[?~d~4̪Ejhgsp\*VF%cmϔO8| kj[1??Dl5>]%.IgSd\f=Wx&nI)uWXZn'$aetƏщ,m܋D=Yd r]mNETV?G5ⓝ b_viҵkLz.Nc£8fuKu;\8p+(@ '[#[}:5L"2|~6w+CXE}6ΐ x%B/&;Ð-GD(Uc'mU2Yk-:m01o~; tS19+T%9CڲjO[i:{'Z7ıi\gT= 1j@[Ln3^'a?|~OׂYE  l{cMڣs.֭9QƮҎ#Cq( OQq"y;]XN<Ot^~!.5\WL)?r.\{kbc7f^58X!+WϠIǰxih3H G.'}8` J_'hwva@P<ݽd+ ,u ԿG˱xØu 讓Kq^;d|q|v[ Ǡe>Wg[OXz Skg-p. olqо2R߅qNJAU+^7{. \(㸌ߍ@s,_g A{|0ϖe\(@"BuL8(`-ϐ9'z">c.͋XK#q>^'򖔕qnCյ9NB#ɻ1kX }w #a{$XHãO?OqXm6JxR|"=r1`PPc>կ-X.bOc(m䵕J)za=Zh˽'5 ۃ4YAג7aV{EWH4d^C^&76S;ta,cӧdMTX%G%. |p\s넛ZkyUJ9b m愼9ïM8ssim0 9Cr[ ޽ɭk84ʍ'? wC׽Ec<٭QQ }w&:A?> %aM0<_WrĜ%c^#>6J rͼ5J|0uD 騳;JoX\:0%+6V'弌ᾕG=6AZK!d9vUyC"}~z#rΤعCChLf!0'luvr;z|&bI}'(HȐZ 3Vw kWPqFunZ,ލS[_F F]4Hk(@ P> TJz"O8L)Zvo:"]JF㙻ڦ#FNNbF) r =C=<$hAeY/ɋNw!>յi[4oY'P =4=L#E9dž 4 G.aŽ ƀ.&Mcv+#a ת6낞]@;"=ҒSؤ}at#E_w[4ǛCй7&^nwp$/շ +JNj>^Wnz8!=#]>LxsXޜ\*NYخ&|j#$OO;cr X[xӕrT:rf_ M 8r ( ٶj L<6@M\2~Sw[T9_2fP_S-c n @{qs+fj^G+_B3o/$m)Woi>H +K ݺ4ҍ< *7UZw +Tͦi 7*7Hz kByXqݣv¬i׬_W 5ǻw 8!D%V>Ч Zkw83IP-DDt4*7w+! =[[Vjrr1S^%oW,<Чo{ NLk{AQ4|qFoS3l>kep{LVSID PGP'4/_`KK2Vcs6Uj]zg>U[[ 8&~˖{Hދ ~F]( -LW1{b%H)вqa|-싿:t0Nvns$ jGk41hhA9h?=vNZSN[Yϭj2p&(^] ѩ_o>oTGty?BUO2!9 Fwp3{35r㭝aMJٍЏrJ*:}67.Iӹ"dˤ@7t[c .Y(9q"Rl]ۇE 0zHH9#dek2 Y$9C񭓓*2iDEq܄9M8`&WΗLaڃ ʂWaH'yog<'ȊØRYrSaEUqf+ʥO/;0ɮlv䜎ЯUoԾ>$z +z #zS퐤ʻƴ\]Rǣ aqZ%)'_&Vip%fx='$h냃`H2*I'b?Ο +TZ!0Mת^0i8aE7f$6Hfim&V5!|̮5\a}r|S#\d^!d_X[%Vp\q¿:;tR[o(@ P)Փˇmoz-}B Z:_V)u13s֜D+[G7K@cw`xE7ghO3\HM޸(yS&Ii`6-ˢZg Rwb$&tQ*|ZQB)w?@yrޞPXo~2vv޲J%W]Zl<|܋rAVr _~-pgVΫ>r1Q͓bŎr*7lh*37DF\>&Mz/?6f Æ %Z(ZdRr t([pk)I MR6l6?sߝsv3{}~w|?+{(d~ė@4yl"KBܽjj|aqT|?-SZB; (vͰo(t I.%U:ɯ@ Mؐ Ι3n+KN`6W#,0Mh$]̴H,Ps]G|;.Le1:ol$ϣm.WYn'@CWUS(Q)טQh>_WjHicR\&C|.Z}V4ş JXER%~]я{c+S7Dcźqx@WN5e9#2aɿPŏ~c7S~w8={d$w5G0!ٹ%ny F /y8(>q9-^kZy(уG-h?>O>2WZ9%bx?ݸDǩ*h[X(k'P)[;гC1Rz_͉[I-|Q\*C38Y^K$䩳)?&c~n !od2&+v9wѿkXdQ=~L: ڑx5NQ2G_%=XK'|jo| K"۫?[d[1Ӄ5C &b}QE©yD#19KBoNk+]K,Mo S.Mn8 olY-t,Sn|5SȲFV-4glt2>[ +|fO=eFzxD$@ -YIv_RGʼn}hぎ-<Ѯp{ē7/ٕ,P˃^=-zBG-AZQ<~K zƒ-Y 'FVPOrKY1oN-ߝziV1=z7 >8N- ]u!S#:tn;ñ ğo ЖDՖkg=ixt^:+~xt|֎ xU>xR{Ȗ94Dz_0~=1XQ:?ο?} {>5eu8n|ieݴZ~8׫~PKvV7㙇w}lv<J_ D>_Ӭ&TB(,>턯6}/%ڿ `[}gU w@NH[]d`(L{yA6=me EЗ.6uFC~S%=U[{D< AI8{R2a5S|1bAJ9Ռi, ?mcz`m/GLAwelsoW.$zAP\Mp ]mTL@C5xJ{o&tlAY2]hzd\~kbE>y*QL݋c{f9>3kZ͞xCXx21$fF{וyrZɲ;egSv/CAXחgBG9/4̔Cy?65wGz^|!e^kFg>:I YZ_*Z,,( @=$ s t7p!!GK ##|_.R"r+ZMx9j4jO}_KWU=n q"Z6F,\Q mk%CJ|) 6:j>CR>}%vo2/%z&<0-K}>JB&_z eIT25.sV8ޭp'o퀟EycCo,yE<99AAA5EɁYWڳܸpNat9g<<tv91+_EKƕl4c*5 poܸFn~kvJv q8Jz$x@ߣ_9X4 BB Mԓd歿&0yɒq2wtl^^tcv0]F$bnb6\xm3=-LJRgJeTj꟰f~I5,&Y3>uw(+  B{†Jgѥ?lalg DW'D ,JhQ,CD郎Y;[D ea3ybeU?0YP6jEJ]>ܹh b& jY+W~GX,0a!%Ӕ?"K\-6,M@Qx n둏@)w*//0\ ,Z1xwx,jvxeȎٲ^PE-9KK,,yplj #.҆#|gK˒ט%4s εe3R(?m u?Mw:i˛|J|6o^~$ΈK٦zj^{Sstqdd8&טk5 88GspD{M:`זq&21(%C>x;ZWSuvwCMpMpo[A}Y; t^b9See.|Me?\|?{q/R,oןYdOIHfPddYޖOQy,;,ia,d/=q]`Qj),FMNNeI9)GI/YkZ~rP^T(-]9'ħkWY|VĒ潉"\My[Zf(1WӜκ*ع9>,M+h]n@$@$@$@$@$@$ PdY-QNe-2^\ $jf%@, OӔBxᰖܩ'f d,k[B͹rث|<6=NE;[mOe(m|j"%بAo˖Yˮ8(/ $@$@$@$@$@$@$PQ]j>WRAY̨eBJ8lP>] QuNj9U&n5l:߱fߠj\5`IHHHHT\jr4[+ SÎ 'Yl$@$@$@$@$@$@$@K"N ;F$@$@$@$@$@$@$PPdOž 8,,;5 @}"@>J$@$@$@$@$@$@$(8԰c$@$@$@$@$@$@$@E4[+ SÎ 'Yl$@$@$@$@$@$@$@K"N ;F$@$@$@$@$@$@$PPdOž 8,,;5 @}"@>J$@$@$@$@$@$@$\g C(((@ff:Cնs^nnnt U*B$PEIII8uQ[F4FHHHHHH viBcwئw/^ġG[npwwQCZTKTヒ]v! @I)ߎٳgWv'      JP_]4Wm(QHKPPP۠Rm*nf̜9(}ѣ&ƨ%J_&lٲZ>%(gM|-Z[jŒQ #$p%(Gnn/F1 Cr!eU J1TV)8w7n4h|AMPٸq#V\S'}YvmPb / ++ Jٳ'?{)Jx޽f%|W׮]l2( e'`͚5prr~a+gVr+|FF$"U0K[)@[0PxI9Jo֌r7p-bE 솰;㾈P( $@$@$@$@$@"@ZXhE !JPٴiƌUۻwo]/J,ꪫ6m9(R:vk?N8}j?# 1dm)_W,YӧOZ즛nJSz?P-b=샑Kwԁ)"66OVwG؜A1iDUk2vb}OMSiy`ڒM:K/{D$@$@$@ E5;Z?8`QҲeKMdQʣOuNH&M,_}f6jĴi4qÚgP2si-JwӇ)ؖ1G`*3)0"ĮGF`mۗosçBd+sAXø4k|Gvm{c`m#j ӤE'\iHHH 3Rԡ(WE_}~z{{CY(EY,]۷G.]ZE,j"壥O>6ʏlQA-1RpUPV1RF9Ub_Qu_~}lڤ9h,s .&wS,}+X6=f!tD̖8}~ ""| |y݌ OYtG%Pu}fHHHH0~0n(80,E e,PnV|k(խ[֜***(VZiTTZNԬY3t JFLh֬YPsNFZ`t?/O٢])!{XoLb␸x-R'@@bE O|-N(_rJUh2?ي҂+\<<릊pq߷"dK;gϞG~>GF=/%]=\?uGsw&gbyAv 01jIQv %O [KQ.RS zz5uL WWx4_ɉYyŢlddySPIEj흔k_~ͣ/)/JmkRUO+¶h|'   :!2wTc01"n{Z']t#?*]H2PT5nJPZfDmPwsec)ºi0'^V%,IXhlIpwc!I)/?ubQ?tÔ COU-'.&p |4As|ku4Xl$:e1ƌI6/Eؽ DYa^"dz,^?c ͹e)Bagxnvtvc3pI.VFBOvoS(6m o%P!Pdgk L\"ۿl;&+/c7m9c7"'.^@\ƶ;O 1O&Qk (c7,ׯ eg< *w2 XrF'w߰ â70вV<{ֺ3Gޠl3x/` WͶjb uv>aڪm?~<@'7#'P}Wik򃩯0m8ְ1wj."^l5;Y%'O*)qEmmO`QO+K`Ae>A$r!@D/.奅l, ʞ("0m%GU-WJ?̮Y{`S,Z4B#Y=\0&"R 1s0|VpMsz~H,2lU\|5 ,XJѮj99`###J ,*[J,d"l]ͽ$_\`SFh;_1ݮba=, -ϸW{me܏mZ^L~7"^mJ4B߮;@ '|jNwW3x=`DLH!*bܨ`#>?dׂ&΋Ʀ-۰%c̛f?R6sV,%cFjX,m۶( XY)_F}aUScٲ M4' =ZrQ`4D/ݩ^E2~dIl.fa %l lAFcٚ|62x h]u*GMeԴEؠXoZ /N¢M2m2w5>T^SɈm%u(u6|0/:[q/@SV~H ǤN FTRf 6,B~!rG_٪W`^^;.Ǯ5/R|Pm!"e1>*YG$@$@$PB={SɎ]lvX͇Y;ZsQ$1c\lRۄY? \VX_ݬanS\Ӆ2^\BC4^'%t\<^[ZneČo?[S%b^+cs2>#؁^^~: |BýOPs tg=B_vXZb*M=-_' 3/xaĤVGo`YfpB(yʑls]`G9ǿ|=УXs-ACG3( ^xE ĺO%]'q'fB`,8!A>1y~nSFp۶~EZ3*|M{$jݚq ƴEF_0zdak(x?D2CSc ި̤oG!nw2Vf#Mvq۴i1u e Sõ0毳$&& fPQ,_u+05b(֙*@ҷ"bx,^N6NY&K׌K1rOHD^0*,N1y^ݻUch?%`Lp9<*LSd|D e6rq= )":͜阽8 3/8+61F!_Saǫ[MHIX#aadM&HHL&'jy}{ĞZW!nqx7M”&)]PoClHaw־Z;IqW9W 6q8VR|]גBV?dQڦ8-;T\fJ)B)sj=W2<=#xyYsAȐP {wbDߓ*-9n2e)zw WB~m5?=v3S猪ˋ(ҦK4/ѨSPDxTZGM%Ԛ]+a֫{ے#\6Hs"$HܬA/#F;h;v:cq̀yY\}K@IDATvz<=ѴX`r t=W|U<   "WFzE`G-71(;%Z7c1*#oJnak/h?N -!Y":vɦ)˷`#!Tl!zrŸbf'78L6uعl&ڇMˍð|\o_: Sݜ{nxy[o"MƊad\ pge՜j|7ba׺ZiTʽ^Wmb !4h kOG!G **]?gQyE%6D$Prr?4-:W +zEUYX?ե-~搾q|"fٰ^!{P?M?>XD}9"f,/ITl˯sT7Ӛk$o#+-s}ނV*d_Ojk ?gϜXI_nZ-O0/jQgDدb9S2D^a>i(?U.Ikh7FM|C M+ a}hۊ*q\SlЫew[fh^p|A%fm41S2x5X1/X^"œiЭ3J uT rFKc\WٽUKo׵pxW۩@ !tff Ak'A̔qh=F̜&V+KNY+ e"OnXnI}hLռZoRbOo5;,)a7,-LY-+N$Zr*ݬ+˜}ٓ0Y!B;.l{_XaѦX &.MVBO7߲MZ2M- F|Ә缶@/~n>]de,c5_D'Ž V7kzٻ⊗̹Yަ9*պv\ir@ɂEЮV]ފdy1Ck Nxqx:ixv|ǔ6SHHH& ^MW!G Nb0)XnN 1mjVRTQ3*#}Vլ^-oOq}S50qkЪ8nDd7m1/SySo]] y$EwkecKSV>{ԏΩwS. nƍ?0= #~kn0.4H+[cܦEٌD\ 9xD"hc!E:IH!P$2! ,5lym#%6fzu&~6>kz +nǢUXrFoEp0ݙE ^ZdP ,|n}xLuV`݋hG5I}kO4cɦ^*%"zk=x81~teeO)p"ng4Vw2ſ٩/¨~mWJG{ |?.?1}l~z2ĕnɺuw&Nǜ9s0>A⨯+ TL@_^h% *Om}uBR!n=nE#[Qv ,HVGы_A4h&r4k/ܿ^dm]gA +Nt%,YO^#XDGWC jybm"Ľi#T]^-MZڏ1j ZEh~bRgx/u5u ;Ž3ɹn<],5Չڭ"Ke$pY=qCz[]bñG,CعnF>]lPìf=(t! ICr,%BIW"qcjU\пRn/=;2k0ix\V+~B֗&%Z϶Ν{mo?'әI['gMHURTݫ?ivBT -v&0IPq`41̿(8Xޙ">PEs|OZȕ$uwfɍᲨL yzJxL: ufc([h,7iJȪN]6W~M!a>Z&M>"[k`<[帩ΠYHH~c DeX|i-ܟ mf0|r5Vb*O+x`)]R\)[)ޭY )\׮iwN;|R_&[9bBpװnwj ? %%`LSGcxl,[Q/{u3z8i#c?Hw%65Dm/ Cc: .0tWT\ޭ*'(k@ySQͱ: Z'k40Sdz|_;MVY3{"L_i֧M\bwbSʔӕUGvҼBHnBHX1 /AðX#U֏S0;bhM[T 'ٱ=U-qfտdFBT=>3cl-{WwAs-Me˜mQ1VxǢ[S~nص(Nf<"L7 јa o/Y'RS~|MHPoܫ YUlGGf1%v1&^,;4  ?m/o>c(_u#fV^$g9:/qa;fL6"?2-='Η"ĉw#6;o=4-Lbeh%aNXgwQtOcW|YˎC\JoFLUo#z`ֿlZ_d2]wX rxU|%@Fq2Sv".N{ "#b"_Funʖ5[Ebjf|نQxjd̊^(X1s_F“/ :43jFEc+(ٵxrF慮Cā oc^wkr3gX )$K^[\xfco Z[qK%c⯦d"*{MyMeD _08Yx,Cj 7iTl Vcعѱ"k~W'>"5Q 2i%`6~9狱ϧ[> #W  5!3>BŒ[ 1 Raϵ sguh.G>>=,˨eʟLzW(~L +KX+hY[Ti_CpL*wsw{s /K>U,K\2_DA0aT/]U:Jg|0ɅԕoKvx0ŜDŽRexƁ_ֺ/WQ?$\H@ 7&3/OԬ:D.El>=5_?6Z ]ԋf#+爋el~陋򑝕j!ɫߡ|(|yz{_˖Z^WEL(Ym-p}yh/ΪgP˼;3LN\u@WOq,5tM2d-9UZJ"Ya${5,RK֐W.eL&  '׼80.2j5Qav*vh6E{X/~sC8ԯdHLŻON~޶!5t[?o_JvXxpSPO\_ª35WѿH_^ "_*净 ehiF,3xj(c5%\]͆KYSTgx+Zͳ2  6‚:~dhjw6*WgaX,:;ZČXTD`O՘RH:˾br $@$@$@$@$@$@@!z'ZnmlggC$;n=?ѓYQ܈Uq䰺 _g;dU^أ1%[`%,4'0(*AT\aETnHHt^>H?c^|֥'ȫ<(R Z, {9zO &F—GشU-gL%   pXθx"{}59:WN$@$@$@$@$@$@@rr8WbTeMjӂgdsN/$@$@$@$@$@$@$@ @vpQم4Cho<KMZEA$@$@$@$@$@$@ @AA233OOOj/77;EJHHHHHHH:.jWO$@$@$@$@$@$@$P#(FVB$@$@$@$@$@$@$ PdiWO$@$@$@$@$@$@$P#(FVB$@$@$@$@$@$@$ PdiWO$@$@$@$@$@$@$P#(FVB$@$@$@$@$@$@$ PdiW@z8jHHHHHH Pdm¬ߡ,ْރg:T      OfFPxN*tsFv^Fŗ~{CNAգ,-bEydp{#kgWSo 3w|#iJxGkdC=}[UQ_C e(,XݵW#eJAҥKp&Rdq9a/HHH.?>AS1T١_Yc/|)-9ڻ~l]H7e5 Gr R|ri(%4;[:`TH+ng^'ˈFzy{PSS?eѢ4)ݢr5S%K,XH^P]3 PdMKh>RNAV)MN|El= ?)3QNԲ͉QdH$H sKԣ>75#Dp2Of1pQbM& dHovx*|mTV876^˷nCMTBmBIaP}Xϐ5@}!2S' @]brzQ,ܵEnJ`Ađl>Ь+|S{+Dt"YqTPmp&gULBm T˄,x}ҲJ(?26^{i*Ԁ frB܀Pp$@$@$@$@$@$@5DG*"ReנBMP-A>I:q7&;|iEYJ z͉P+ݱ6Y,_s[Pֹ"~v8zTHxߋKcy!Ǖ89V hkH8v       @K;B{˖g&tbSV+vRj'"VYKZc_o 4btF *M2F- ڲ?SZfԲ?׊^ݦ "u X,/nO`@qS$~_N@i+Gjנ/!{sѮ2gp^riڂZOS.i+[  ,Ws|$@$@$@$@$@$PwhR 뛂},{"+1sV_[%~pk7k_-C6 VnF{2ݬV`h.aD@7Y-'jmP|Xؖҏ[^b(\3 @h􇄺mUmH>Ԯl{­n }3٧n__kn{H@ff&ڷo_Ͳ=   pH\.RvNFegYa ,v0HHHHHH(T @}#"8991T.\///xxPToK$@$@$@ c]7IH ={V.6!'|-Xjُ¹pvvk*b)    CY6" MҥKXu^qRf#vG"-{JA˖-ϓ$@$@$@$@$#@$@u@ 77͚5C[i.^a4     zF,l] *B4nT,cpww& TE*cA G#' &   hϝvHjU”$   =Yj-k& $\>h;E$@$@$@$`@Ӷ Tl\1k t˯hjkTV1V@$@$@$@$@$@YH'g uicw=cn66j;R6 O$@$@$@$(8$@$P\\*T;B +a]*neR]\*&j'X9 Ts 1 @%V-Rb;]'dsPQA6f$@$@$@$@$PYY*KIHpu,wՁ-|IiYhۢ Y 䌐 d. @M(:価Wͭ6 zu;[@35AԶ9RO31:mٖ_~ֲ7'zG=*RjHHHHHPd+lH@Y!]yk@S'/Oaݗ4ADe}c w'lus>HZGboJ.^KK GϦ݋$@$@$@$@ ՃIbIH& (^cMmy*_~69ecYQ"k;\Jr~[H#E #$@$@$@$@Ez0I" $'{5m[{iM)kv.oFW{↮-J62JNO݅nUי'   pl{uaHH 8"\һ%x8kmٛEm}GS7k|w :IuY=ux;vF^'gS,2N$@$@$@$@J" E$@Dɾp1VFEP&Ʊed᳆rcWDFNeqFN(]SyU9    /(ԗb?IHrQS#sſ>cT//2/Zz]Cw`oƱQeTKǟדQK #    pdYyv7 w5|%*0rpW<%,+2'[&Yo<89'yMr 2!    G'@g# &H91UC;p7y 9gslOX Z-ߕ^FiӮem[{ߍ sÞ @hk?'nCS&Z[c-}IGp,*<2.[/,'n2 }{["oe ݫLA$@uF@*1O~hכHlh%6ioQn7iS/YA,j߮5Ǝ 5r⼆4#$@$@$@$@$(8԰c$@$PK,W_:|$ _lke j#.k,-'~![iU^݄IY屨Q    ph^.t82s A9;5Bfh *^[؝VHϾyc Jb[ F^Ački3>vF x侻l#siއ57#tZ4ߴu#G׬H > $@$@$@$@$POTZdytzm{?aϖ2Exzm[Z6KO`Ao8GJ5r֎\Ƚ&BAT WL,]z!t.{Ky9xЧg0SO&OCFfu*ٟ3HHHH@EշA]cjh?ܺh|tFHfcmq!i++]hqs`_.#FN&kmcgTsƯ#?=<<֑xan;r!n>t F7E+Iӌ @ b5޸+>!|?+;̼B _oOW'<*3wU~j~e@ewwѬ|;C'& jk;4maL7vŝើm4\!MRNSKO@6QZ:4o~K̈́K o8Kbu -J`yW{ҥK0`@@C~\뭢ՈU68BE,`*n"VJ9 Crܦ&~l~ $c{ \^mFJ n /okn-3)0g%SG7/5xzD֊H|;aҐ~ n2((yX(kA`x/?MhQ^tE]WZXҾw}'N҆T, eߪӞe= 1C‹fZE8M ۺ֤,-%YP6BEXv( DVOv^N~YrwC7Cy&EeGQzH+DvdFBL #1\BSF-C7db.^C*Yc_SaHHHHj@) n릦[e5'zp"N\v7].\Dfnh'-Ś@-ޣޕbeK =]- JxQ-:N?S-W* IpPBbv7,ZJXj/KgL컲*[TIHHHHLFXE,NNiWT-0F| UQ6W=o˕odᜬnʣ lycQ%jN{7qEFn~<[_I׃: $Xچo:u0~eؓv,] ,nn[@TS#]ϻ:k*[~넶 v9śSPO|XQUkkij[R~RCǨТI|l?CmoZ #i[bQNqU}6vs*  \*lʳ`Rg䂌ȡv^|dg 8ʝ1jQ~ZӹZGY^qHeġ흴~q@*1\ ,j(ٻL}M4L6-0 $X˿cw6 +{ c[ϩ2*ߴ9զ,jT 8(FHZTQK*D6\b *3\;dgt3dr#ecQVXڂ%-- ]a܁dee]vp1Մ1C1.xt[ 2a"`m=e Vnݯj}<?KRcTArCO(o } ~|Nj*-ꍵZ~%|l&<=/˘4dff}b1 @mJe5zebO%Y ,UKiT :Z|wE#//O[҃PjpC!"Ybr9ybRxY PT9-ǒ_c_ ?M;7OX 9"s9yڻ^.lP7;;~ C%fKӣO+SO$@$@$@$@$P"{i+KHs#Dw zdڒYd+lڴ 6lEc00 ,ݻjV) 1H-Yf+vRS4!9ّHΛy%#dQSo\i ww(C(|hmʥ15mgi&wVQV@$@$@$@$@w:_zݼ2hԗ(((YkƍZ#GYq=%`. | |0'lNQEOcǞߵxι8馉2?O~ƋOk[ e7>+YآDR Eh6|(|lqv E5F 7'fIHHHZҰPr$py 8pBѝwމZ^嵞 oootiyNy=}Swjv v'(L"  hB v&9O@h>HH rCD(xhTŊEŢ*bRQѦ "XcQV,REA $d&M !l<dgc̺O3Z8ux8|'seq_G@@@B$KT A"p|$ZLnUJxy+MOSP؟q?{ QֿVƵdC@@AӅaR wc=FoBou:[߿_Gu.l/rڵK'M^ xB8](  p T ߞ# F 8%-^z6lWj: 6yfω66F`   Md"VJ)`gԩ.s {Z+d9yWՑ!.@8$YN:D ؋ŝ-d9%lO dp  f.|{#pNUd#  ^ ">@@@ ,Q1L   ^ ">@@@ ,Q1L   ^ ">@@@ ,Q1L   ^ ">@$ ̝;W'O>IK'dc -ƏZv{Nӕ[~;]pgV^^ӯ|Ùy;mof?Wnn _|ѩ?ЙSO?͛,B,YצMծ]Ygҥz畝;ɑF֡Ctb2d^ymݺU:uȑ#uQ###/õfg|A-ZhѢEј1cԴiS{n[Ng6H9|r]tENrӴiӜ.X&'';3jx 9rD?dێMK&MR߾} 6m=~ig6lJ*,  #L3gS('_0nOe|Hǎ$4f͚ѣG˞崵I~X3fp ޾5gΜ"]zNr"Xi\rs*̮]$ԩS5| `I 0 lk׿^7|r!'ޡCޭ[7=28:p^z%'c}e~ۤիe/tNű"-^X;v̙3uW&"Ow /Բe˜W_}Չ_vپ}ۮWSg 뮻;},  #@jvKO?UϞ=Ure%%%9 ;#~8Olƞb?$-v6}QjUgMdl-7v$n-'|nMUTQbb3 )ؤX3`;ˢFUWX!;Ğc 7$fl"<`nݺU M\ؙ)v&MFĊ]Z }N)tyv˖-vw98LԱ3lr&HV\̼ n:5kOv Ygk`ԎMٺ`BgؤŮc˹̔ /  #BgX PN6)bO {M\Cyb, [B}(׿_4 {ֹKhYgU-{{}MۺоCc2age]V'/[7MP1  _WavYIcI6Qc/vkO&Yl̞Fd*45k:vhRhW3=@@*@ᧁ P+<I&Ok8 JΰҥKٙ'GN6ܶ3Tl̰ ^՞,Oű)t)c׳3Flb!4/{;;^\֞"t)  ~J y4    '(Ldu@@@@ dq   Ir@ @@@@$ @@@@H"]    $Yx     @@@@ c@@@(,H    I    @9d)D@@@@H@@@@A$K9     @    P$Y.@@@@,<@@@@r Rt   d1   Ir@ @@@@$ @@@@bʡ@)gܹS999:zj$7 [/{޶9>Y:A}{MMajMsgi֍a 5TaӰi akRFarhǦuaj^a9ھimX}|B79'>`o\V_=A;mUKPsj ?{H[ׯ_M ? [ka1UոE#|m*1jryaG|>>3JLi6#>m\޾r5mGaT\EM[Va*WVrvaaղJ*)a¶빋mos]l<6.wowk]uui]Ŏ7we]6F|wyذzx;w7sxkwgv|wq|3KwGt|{wwwzb_WžwWu]}rzh_žEwu]}uzn_žuw}]}p~dߗž%wٳ}dvW;w}]0Fh_jowV߻ݍjkgڮ}w'թZg m:bueߞڹ]ZgURaޝ[kUG6 6]Y]w)c} q0{vn^_knfa=8d;"sT3zᘣvKcXZ(1DJs:.a3vH\c¶>_ҵ/mycI/n*3@Ei>"_L8NH틋Glow}:zi_#l?)_\< 2bi ʥHX"SNm7TdeJjo6\\H}E qY1택r3ŕ틉,.?=F*R9@}LؠBX9$Q+{F%mV /˅ P<ʶVo+VRo7   ^ *C?B.-  .\ ˉ.  DI(,BE F   P,eQc@@@@%@](_S~|Bo    rǀ;lGA@@*I>   ,    @E RGC@@@S"sJF@@@Ça:m߾MժUWMTA( @ @ T`5jUc@@*M:/\߿O5kҡC۵W;)K+/G۷PvO15T$;QSц @ve.󺬈   ߏukS{.tf5ɗ=q !hդ%jEwVѢ?ꏓ8 Y z/@x @9d)D@@8iK\8zNBߪfZ:vbYvN}TDKfw^YUrN~W&,ݥYWBBv"w|Hv1?=Ĥ\!  TlQ6l#/q3Nzz]w^JkCC5n$`+q5{S_OԴ)fFH ?yp7 [}>g,Z^[ (@G@@(`VӟJ={©;K6O^{e?ڑm7UC9̜n(ۤYfo@]\_-]KVju: 32+}y8=3sJ}~Q뮾C6٭|gsgk;ӍO9_҄}V]nP=ꍹ݅znC֡hi˾,lI ^W#vr62&M]Q+t誖!@y4?-ԥ}Mdg 9 C%B'K~@@NT?[.Я+:wQ_ [zK|H>p@{}^}6t?ӅZ~ƜNt9h8Ll7jW۟#`}-0 IïMu]rփze(?^.9$|dڙ^)R^N VKR-se32 I5Up׏ь?rO8n'iȴtCUǿ?Y-iKӡ4vߍhc+L{55{"BB8_@@ ضM[UkQ ;Q`twWyiί~tm< l_$>rb>7ӁA9])ʹ0JM)7Q[t'P#/?R.mI 0.!Igש$X,-o>гCAof/'b:M5\潏i 6Ր4>>&@TcϿ>.Xt5mIْo7;ӄK>z>SWoIa   5*&b/|[R%U\E| Ucef2׺vtW5>{>{ϟ'tUs<1u:聑}+XkskjHjCe'/mѵ/> Z~3G_Q/'#-蜜S~4mX~s;%uJ߷ZXZk'o5TÉ57х& _kɗ_+Z7ZB_$Y    p 4hH7WҲu}(Ctsm(_ܩ|_-ZڜU+3U^}U\9Z:([&R 5OwH[cEF3#dY]ƜRRa7U_1cN)H4՝ O=[?}Ӝ:󧁗֎Ua^῿ocA4} \pk!h &aGi=ޟRb7vZyQ_,5[ҳ{Z3EtԔDU`Wr@@(xw4[3#PbVUk~5?EڥlsOLxժTtVJz槌h‚&&Wgk(~Ϭl\7Z )i}_v6?AmZq&`eܟ"l*Sr}KL4?_]R*r@@@EhοCj튏f~I^I|YRJNG4G@@@+@;g@BBl%po  TlJGMػ!RO~:`   I r҉ѡ#E vo۬}{;g7hZgپEv՟Uj$Vgi7Bz}{wn=]jU'>k6۳3>n:nX}֮ڷ{GX}RuvfR]l]ݥ;Uvծ0X#w6g#wv 1p;vŎcw޷״6ڦ}YڵucX}B$i4`~ܼ>zbMm,x_ŪPC4kֆ'$~ssh5aTiC9ڶ!}\5H>.lܰùu԰Y˰z;3mUaUaڼfeX}LlU5n&ޗoVkڟVӦ2Ĩɹm?+WQӖD]OUԴa .T[ W.X߬MV-Sɒ[_tdtڦ.v7.v~b=.v츹˦1f|3c3 JSwٺ[uWaVVo/y.b7byy.K|{wwwzb_WžwWu]}ri_Gž~QwٹeVFɪ^VX}=b_뺻 .¾o׎9N1=ްb7q{\.x7=^Mr9r5 5kcr80evXiw)1G9ؼݍ%cs^cs11Jus4žVcZżKymd^s%9{}@(J%DQ1}AY@@<)LO A!   @ = ^@@@@$Y<0   /@%ǐ=@@@@d    DIC@@@< @@    $Y @@@I !    @ d1d@@@@$Y<0   /@%ǐ=@@@@d    DIC@@@< @@    $Y @@@@b  wT!  xUQSq!   "B2Rĉ    !8@@@,2Rĉ    !8@@@,2Rĉ    !8@@@,2Rĉ    !8@@@,2Rĉ    !8@@@,2Rĉ    !8@@@,2Rĉ    !8@@@,2Rĉ    !8@@@,2Rĉ    !8@@@,2Rĉ    !8@@@,2Rĉ    !8@@@,2Rĉ    !8@@@,2Rĉ    !8@@@,2Rĉ    !8@@@,2Rĉ    !8@@@,2Rĉ    !8@@@,2Rĉ    !8@@@,2Rĉ    !8@@@,2Rĉ    !8@@@,2Rĉ    !8@@@,2Rĉ    !8@@@,2Rĉ    !8@@@,2Rĉ    !8@@@,2Rĉ    !8@@@,2Rĉ    !8@@@,2Rĉ    !8@@@,2Rĉ    !8@@@,2Rĉ    !8@@@,2Rĉ    !8@@@,2Rĉ    !8@@@,2Rĉ    !8@@@,2Rĉ    !8@@@,2Rĉ    !8@@@,2Rĉ    !8@@@,2Rĉ    !8@@@,2Rĉ    !8@@@,2Rĉ    !8@@@,2Rĉ    !8@@@h 8@%Q3]s)9r4ɩ&~=jZ8x>}13M.[]1b]>gv4E*x^b@@(3YJ Fs<#ۭ35L[N[ۓƩ]5`@mRI)[ݙY'GOӢݹe k\3^KݤLS1e1   PZ-@;?vz7NNW:dOoN[qMŖyN$zC7W, o'~ MUU"A   PfEu"J1񽓱Z6Zmһ*%%׵omf덡}:ev/K:@o|ծV,xI}}tUE5zg\Rީ)Nv[̙H-j/H-oWsV9;PطUnWUr%{_>sL4teP-Ծ0@&Kv:E)o ,w@@@ r 9;\⛩s[ik_ee}fW|Hgɳ>9]9WojR,ҁ]jSr Ys$LX{$4Vgi;~>BtKVt Kz[hL/8~ǏWQ5C5ѷd=nCӬY~ Զ$La,Mh#C4`\&pQح_3lWYQlwͭӕ6vV>.$uIi>T%vf6c/2ty$?WVzW*$GZ4sۘEYܜ~b&a2ډOf0uE6?MCn\u&ɒe8shr=DsϱJI`yRtEaƛ-=>6VK{A)JEmǸ O׀ձK3SfNրEhXcGw @@*@^$ D@v6ɀnTKIvwAkJsv2IG= ZאKͩBFkU5epa୆Wjb\b La9lҖg3^ 8}uoWd)Xϼ\e@ntmU|;^!ܸD\$v[?~R91%Qn2ILKqJ}D)?>d-B]|3~g.f.kZ9 @@<e.TFpv!)Y=EiUR(j /' [c=9LyŜAӶG}:.'b>t|k 'Cb~zpvJ1)exg[ۻ lŞ TxVMm۠ZfL{=sa;q[mtOݧuWrh^?zS6YhΙ1%&}_ _xPz!  )P)g殳\>'&)]&,Ԁv1UL|bic)6e+;N`dFSnx]b:5gN7Ͷ.P#̵]>Ҁ ,6iEO:.Y@@0b?Fa.g@LI\e\Sd aK]m""ML/eGⵃw-en"^g'Ӥ}¾e&¼#wzf:_.-̴qc"ܺW ߨ}*s  V$Kih   Dׅ"P   V$Kih   D *@@@@$YJ+F{@@@@ I(T!    RZ1#   HD@ @@@(IҊ@@@ @% U    @iHV    @,PB@@@J+@bG@@@"dB    PZ,=    A$K@@@@ d)@@@@$Y"P   V$Kih   D *@@@@$YJ+F{@@@@ I(T!   ) GonXS_Lync@\YK4OsW.Q 6~UZ\)"" ){lo(>k"RKJ#Dšd.Vt-5KOZtu4P}4%p#tAPofϐ}ɭͼ`UL )w;TM^8صl1,Ab6_\!,UO\A'PK UhIȒRzPjxfd^̃ȯVDdDq zVءi\/|GL%z+O~'K(Mo[31;[obj()v8d9k 1'w"MW)))ο=d>)w W#'d:T~HSv 龎_l-`X%횢KT)]ͲE qKa㎟g?S)36.n|}{Hċ-׋ޛ2T}Հ#7R]y9tVey(yɦؒB kӁ<;-fzo9I7LrxZeNi3~dzxF6S#"rSolI`֙5T )K g0Ŵ}f08=mP}. H)T0OT#9.9<\2oJ}iyFJ h3 \$icj΄eᒝHзy޽4GoW{M'^^4rf6ۡS>.׉8}$YN=[F뱗́e+[kFBj>a&/o4c}~24z`eT[qV|\;9Cg$ef%[nJߪ)OaGtZPZf:W̜y׋ Q02x|[7cCF9E;MV\{?Ȭt:E]Єy{O!]LD5󿙙vj٬WLQaN;tvK6oaj:k_$ N@Yo}.T4<[ԑZ@Ln-}g>LV%žfI#uf&A-]l_]gfݬ132Զztk<0ZKfh\M&ȷ}9!^ڦ=e;_Q 82@EHTڿ{0t+DtPp{ʠ4x)͘7o㷼VTA9GL[`z9|_C]^עol?Qˆjl(N{ݠ.v,(.Rh _;ws*^;E`s"p.c։+p.FO\V]hl?ZÛFh=y=mM3LS濦o)F4<=5z=qX<3m_&9R#5x_icPKOŋ}yxSɜu^p7S4y ~jGus62y6S&yojtvs2_g#/K}f銃"@YvPvS5lY2.0%SMfkjܸ6а3붒mQ:5My<1(mXvgb߃BW6xWoӕ~mCmOW5a_|y[tV|i몕{FXZ?͛efQflІЧvӥ J KZl3rFj_pP[55mgcJ33 iӳj?8ւ-ŗxk}&N?WGWf1]i=g6h\:nҬ`mF-tZyo9\(kF<٘A&|g~<ǻoJooipLùRӿ .H087L> >!r1 t(Y퓝Lm*;]6jX鰿zN_>t'5M&PڶoUpRIlojLJJɺw9ޖÑ?_ $FN_;k֭RfLVsUk_\)cFӈѓLl:DɇSga1s֣x{HBeiԷ -NϕIAM.h`^4\OԚ~<)I}xESX|W#n,diz%$sWoZJt͜P`78d9(@|ͳO)xs^~y䄎4sbs7k+YZRkBo!S59'M'XRz;Z.@ Scu/9'gNR۶6Is_kE7u<}3Wi $6H$IDATHqfepY̘\gIp9F=QZʴu,[nN-(!o%}TM=2\g_Lžt:Y[lNZ! BBiMN@ͲE8Yf*t]/7[ٹ>'*1W>Y %u}z7?`m`f{9_diSVOIISŃ{kEP>fg|VqK>eg/J~bXِ[\{X 7"pǻ/t\zm׹oK4& /˼G2 ڗ55'&_ s/>ݱ!2Mys*{9M*|M9FEW PۿxAtDIì?nɗLbt[ux =c=ޏ{d38_v8Asi,%=& `c= -%Lo  ܝks}ԦEÐY*_~0_g@M߆z"j@(/[uARBc M@@@@\r   d &    PV,ec=@@@@ D$K7@@@@ d)!   !$YB0   U$KYX@@@ M@@@@1e]@ Yn@@@U,Ig    p T:jʙ7^8|#6@@@3Y\ E@@@"@,j   H@   E$KYX@@@p dqp@@@(I     .    P,eQc@@@@%@]@@@@,$Yʢ:    K$     @YHEu@@@@Iw@@@@d)    .,."   e R5A@@@\U~o #GEAF]SC>u_&ѤwTUޢI_-h}lZKVK6k_Ec+kg5mar5Q`<k'@< IM>K}ο -ۚ-QbL&V~xʹ =IVW?#[x@uH:}>bbbWnq  P 5db삤<ۙK\J9.);4|Z1zuH8F&` }sD,D3Ydb! *Gr5+t9v -ozv'>]R3QWu>Ο۾O/NЁݙ ^[R-Utw`7LFsV߯'&/K_miv[T仝zeůͲ`/ !pWtd+46Cv.M2 _~0Mz|퓂ow_J^kaT[;W]O-&=gkQ}[XxDY1oJlz,X|Z |{RcN9g7߼YX'9pmi b yyQ0懾&f/Vq}WКT fTqd/Pf@;凌{3vSzyvo6Q"ߥ~oՁj54YߴK4W*)uɹ5o=ki直d81[?7QX_rP0mwWԻڗD2FzT}Gv <4c8KUrww86$Qx]/طC99'_3ie:k2">=w ׁ?)c~-NR;7tǨ?ۻ;? *  RT0Ol@ԸB#)aiZCf&tSjp<2e(V#E"/ hHdL y!$[HysϹ\9w9zhX ?#Vk MH'+kƍ:RlH"cԾg*]Y/Y[ sUq.PsQV$Pm6l(Vee`XwHM;;0^W% _z7)Jݧ?>ڀB/NwDh,Ewد'ӕ0z۞s^ڸFUi}i*x_OgnR,Ye;/7莉U9^Fr*;R^iV?PN|hsb@rb:ǒtBv^v񸽫8h8ZmiB`|o/=yXo!C2T,[TPY`֣$TK~߉6.awo7js쵨ΎJBte}Ч!M~bwЀ;;h}6`PGKH/­tX;FָNXZ.-kKU~@y2*mxm]ѧ{!{+8Iu܂&K/XeO.YoVrwy _1Iޗ'Q~ Xb+e Kl}Tq}UֱJZ⒣k`Ԓ[tQ,aeblHgͩA5iT<^!D8+K4mgBڛvu5aBUvD'Hs.u,UK^3E!]|X ddgsu]o᝶ҩz}B,X6d)'e՝kXheߊKz(Gh5;1ѾV-S'd8tЄ ;|BW#lۧmί׽ڇpJC{bד6"vw.QxՍ_ 3j"32]ZvƇS 8Z:Zؾxɷ_ۜ 35vDf<=5.jyFDW=!nujzso-;p'j32ecjw]9ڸ+u$ mHQj5ME5 5sDXs.9þ|`gw^q&.~^y7c R)csqUKXFY֜W؆0ͬ*_?ve|.}\Kc(ԽvXdozocHGs ZB lDI)аai8aGuIUi_vQ /P4}5zxrxg>/s4,hQۋ¢KrrEL x @}s8g-JѲLl6 \ 2ֆY& \#dPm1;BLg9P=<%%:ߪIt {*҆ Жٖ̐N[h%:umv {uQ=PWj^u|C>l/ԃ6]kF|wlTu;zip /a}~Lk^ݣ0]!OU0jZO/0Ns^XT|>V^ NK/~N[__e}%%ڗ\qIiZ_,_y\m%Y(ޙjꯟ'J ??qq7Gm=`6-b%i\gAL57 YO]%%<454mh`яʼn)ze':[)v~Dkxv5[\[󕟟zCp W-ZMk׾ھan(wCt mBM㾚dV/@Xi`kp)uh:jۜ~&ѧ=TtYE!Ow~qWeЯ_->_| ̛l](u u!i'%CoV;{;nQ˔F^BR(,vІ|nrNbYQ #jy^}YC83>gV̀H6qsY n6n>A]3^CFV> `8auhLv9Q#D~rz,(*K'e[GPn-C +o{2i",b7 jz70 pgF0pRNu{T+ckLN_nnw0Z8cS8Gd(GJ\^~ɻrͫ=2J7/[yžU{mYIsiK>ٷUrrIF@@*OM#?g@;+=8J:nf'١lD}t:gzH;kq'4Vө\[&dO}uMo!  'Ks3Hfp~QX^ڰ[N "vT:.f*&&BgR-E436epv:iS*#&&Vi9y6f9[b(S)ZV9V(1XFC+ vei'׊nv:\[═S*=qm935aAQ,u13)@O3U`]:Qؙn9Q:%  9,mON[@e_ڻiTSIG3.L) s}&STҏtPVfgÔLys*w2U+ LoS@]+כir^tnCy*^RU t2PY,Z=j~pe}+5rfrs4`)(A꺄Fӌg(1qn)hʫNZ ;0ԙfR'[hrܪ]eO%CzhWٹqa3e86]>qzdsh?||\t,8/Īץ,D)Ѡ,a5:bkȅN0GBfV}bd)6X=2z{|*jTY#z]Btk uYǖܮD}%&ū(E )e&"~GRA5jdC@@V в$/Sn U$rovhRS62BqSW(lPұ9K<Usbc׺^+"Ûī*% 74LkzRei'>/C Ʃk>UmԾzvZr  g=Y?F@H)\Wu&>F².!k/*=sRE,VݸFU^m5{\Y/s߸ Ո{33Y涨zz8!:R "V'1IuXsjzؒhl\i;MzZT['g@@hY_v gT-e˻[rI6'^?u,i>] %čӊ-{y;Xz:z)ț.lܟk&UF[Wy\E[Os'=zQ 4o/ӄ%\ѷXcec f5aeuP6Ci+ɘj)RɁmzbz椾 \   sY@,pb#kr{{M衱6)}wʙ -w iNBTmΙ򬍠?vNW)-#S]UP:)f#GGy?B62Tތt+ USsСenOg=Vfů֣8_{y[f5ӿ;2A=S5+3-\?P>kgƖZ5U@bC @@ڦ9_6Nh~nWhxx|)Z{F%^QiOpY2)/U;Pf}9[w^ v'Au辁l^W̭lnuJ:1~"N#WZja(]@@@ 459\ڌ'"u[ksg[#,pRjN FOJw/f=|v3M 8ajA1^#E6i9f ,   @D0#Ɋx]۵8|5wz]ßh^f*&O;Vc?!VQf׃@@@ di@@@@zP=(lB@@@*@bG@@@ R @@@@ di@@@@zԃ&@@@@Y*F~@@@@, @@@hA@@@G IENDB`././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/doc/source/admin/appdev-guide/step-by-step/part1.rst0000664000175000017500000004634600000000000024763 0ustar00zuulzuul00000000000000 Part 1: Creating your first Application Package ----------------------------------------------- All tutorials on programming languages start with a "Hello, World" example, and since Murano provides its own programming language, this guide will start the same way. Let's do a "Hello, World" application. It will not do anything useful yet, but will provide you with an understanding of how things work in Murano. We will add more logic to the package at later stages. Now let's start with the basics: Creating package manifest ~~~~~~~~~~~~~~~~~~~~~~~~~ Let's start with creating an empty Murano Package. All packages consist of multiple files (two at least) organized into a special structure. So, let's create a directory somewhere in our file system and set it as our current working directory. This directory will contain our package: .. code-block:: shell $ mkdir HelloWorld $ cd HelloWorld The main element of the package is its `manifest`. It is a description of the package, telling Murano how to display the package in the catalog. It is defined in a yaml file called ``manifest.yaml`` which should be placed right in the main package directory. Let's create this file and open it with any text editor: .. code-block:: shell $ vim manifest.yaml This file may contain a number of sections (we will take a closer look at some of them later), but the mandatory ones are ``FullName`` and ``Type``. The ``FullName`` should be a unique identifier of the package, the name which Murano uses to distinguish it among other packages in the catalog. It is very important for this name to be globally unique: if you publish your package and someone adds it to their catalog, there should be no chances that someone else's package has the same name. That's why it is recommended to give your packages Full Names based on the domain you (or the company your work for) own. We recommend using "reversed-domain-name" notation, similar to the one used in the world of Java development: if the `yourdomain.com` is the domain name you own, then you could name your package ``com.yourdomain.HellWorld``. This way your package name will not duplicate anybody else's, even if they also named their package "HelloWorld", because theirs will begin with a different domain-specific prefix. ``Type`` may have either of two values: ``Application`` or ``Library``. ``Application`` indicates the standard package to deploy an application with Murano, while a ``Library`` is bundle of reusable scenarios which may be used by other packages. For now we just need a single standalone app, so let's choose an ``Application`` type. The ``Description`` is a text attribute, providing detailed info about your package. Enter these values and save the file. You should have something like this: .. code-block:: yaml FullName: com.yourdomain.HelloWorld Type: Application Description: | A package which demonstrates development for Murano by greeting the user. This is the minimum required to start. We'll add more manifest data later. Adding a class ~~~~~~~~~~~~~~ While `manifests` describe Murano packages in the catalog, the actual logic of packages is put into `classes`, which are plain YAML files placed into the ``Classes`` directory of the application package. So, let's create a directory to store the logic of our application, then create and edit the file to contain the first class of the package. .. code-block:: shell $ mkdir Classes $ vim Classes/HelloWorld.yaml Murano classes follow standard patterns of object-oriented programming: they define the types of the objects which may be instantiated by Murano. The types are composed of `properties`, defining the data structure of objects, and `methods`, containing the logic that defines the way in which Murano executes the former. The types may be `extended`: the extended class contains all the methods and properties of the class it extends, or it may override some of them. Let's type in the following YAML to create our first class: .. code-block:: yaml :linenos: Name: com.yourdomain.HelloWorld Extends: io.murano.Application Methods: deploy: Body: - $reporter: $this.find('io.murano.Environment').reporter - $reporter.report($this, "Hello, World!") Let's walk through this code line by line and see what this code does. The first line is pretty obvious: it states the name of our class, ``com.yourdomain.HelloWorld``. Note that this name matches the name of the package - that's intentional. Although it is not mandatory, it is strongly recommended to give the main class of your application package the same name as the package itself. Then, there is an ``Extends`` directive. It says that our class extends (or inherits) another class, called ``io.murano.Application``. That is the base class for all classes which should deploy Applications in Murano. As many other classes it is shipped with Murano itself, thus its name starts with `io.murano.` prefix: `murano.io` domain is controlled by the Murano development team and no one else should create packages or classes having names in that namespace. Note that ``Extends`` directive may contain not only a single value, but a list. In that case the class we create will inherit multiple base classes. Yes, Murano has multiple inheritance, yay! Now, the ``Methods`` block contains all the logic encapsulated in our class. In this example there is just one method, called ``deploy``. This method is defined in the base class we've just inherited - the ``io.murano.Application``, so here we `override` it. ``Body`` block of the method contains the implementation, the actual logic of the method. It's a list of instructions (note the dash-prefixed lines - that's how YAML defines lists), each executed one by one. There are two instruction statements here. The first one declares a `variable` named ``$reporter`` (note the ``$`` character: all the words prefixed with it are variables in Murano language) and assigns it a value. Unlike other languages Murano uses colon (``:``) as an assignment operator: this makes it convenient to express Murano statements as regular YAML mappings. The expression to the right of the colon is executed and the result value is assigned to a variable to the left of the colon. Let's take a closer look at the right-hand side of the expression in the first statement: .. code-block:: yaml - $reporter: $this.find('io.murano.Environment').reporter It takes a value of a special variable called ``$this`` (which always contains a reference to the current object, i.e. the instance of our class for which the method was called; it is same as ``self`` in python or ``this`` in Java) and calls a method named ``find`` on it with a string parameter equal to 'io.murano.Environment'; from the call result it takes a "reporter" attribute; this value is assigned to the variable in the left-hand side of the expression. The meaning of this code is simple: it `finds` the object of class ``io.murano.Environment`` which owns the current application and returns its "reporter" object. This ``io.murano.Environment`` is a special object which groups multiple deployed applications. When the end-user interacts with Murano they create these `Environments` and place applications into them. So, every Application is able to get a reference to this object by calling ``find`` method like we just did. Meanwhile, the ``io.murano.Environment`` class has various methods to interact with the "outer world", for example to report various messages to the end-user via the deployment log: this is done by the "reporter" property of that class. So, our first statement just retrieved that reporter. The second one uses it to display a message to a user: it calls a method "report", passes the reference to a reporting object and a message as the arguments of the method: .. code-block:: yaml - $reporter.report($this, "Hello, World!") Note that the second statement is not a YAML-mapping: it does not have a colon inside. That's because this statement just makes a method call, it does not need to remember the result. That's it: we've just made a class which greets the user with a traditional "Hello, World!" message. Now we need to include this class into the package we are creating. Although it is placed within a ``Classes`` subdirectory of the package, it still needs to be explicitly added to the package. To do that, add a ``Classes`` section to your manifest.yaml file. This should be a YAML mapping, having class names as keys and relative paths of files within the ``Classes`` directory as the values. So, for our example class it should look like this: .. code-block:: yaml Classes: com.yourdomain.HelloWorld: HelloWorld.yaml Paste this block anywhere in the ``manifest.yaml`` Pack and upload your app ~~~~~~~~~~~~~~~~~~~~~~~~ Our application is ready. It's very simplistic and lacks many features required for real-world applications, but it already can be deployed into Murano and run there. To do that we need to pack it first. We use good old zip for it. That's it: just zip everything inside your package directory into a zip archive, and you'll get a ready-to-use Murano package: .. code-block:: shell $ zip -r hello_world.zip * This will add all the contents of our package directory to a zip archive called ``hello_world.zip``. Do not forget the ``-r`` argument to include the files in subdirectories (the class file in our case). Now, let's upload the package to murano. Ensure that your system has a murano-client installed and your OpenStack cloud credentials are exported as environmnet variables (if not, sourcing an `openrc` file, downloadable from your horizon dashboard will do the latter). Then execute the following command: .. code-block:: shell $ murano package-import ./hello_world.zip Importing package com.yourdomain.HelloWorld +----------------------------------+---------------------------+---------------------------+-----------+--------+-----------+-------------+---------+ | ID | Name | FQN | Author | Active | Is Public | Type | Version | +----------------------------------+---------------------------+---------------------------+-----------+--------+-----------+-------------+---------+ | 251a409645d1444aa1ead8eaac451a1d | com.yourdomain.HelloWorld | com.yourdomain.HelloWorld | OpenStack | True | | Application | | +----------------------------------+---------------------------+---------------------------+-----------+--------+-----------+-------------+---------+ As you can see from the output, the package has been uploaded to Murano catalog and is now available there. Let's now deploy it. Deploying your application ~~~~~~~~~~~~~~~~~~~~~~~~~~ To deploy an application with Murano one needs to create an `Environment` and add configured instances of your applications into it. It may be done either with the help of user interface (but that requires some extra effort from package developer) or by providing an explicit JSON, describing the exact application instance and its configuration. Let's do the latter option for now. First, let's create a json snippet for our application. Since the app is very basic, the snippet is simple as well: .. code-block:: json [ { "op": "add", "path": "/-", "value": { "?": { "name": "Demo", "type": "com.yourdomain.HelloWorld", "id": "42" } } } ] This json follows a standard json-patch notation, i.e. it defines a number of operations to edit a large json document. This particular one `adds` (note the value of ``op`` key) an object described in the ``value`` of the json to the `root` (note the ``path`` equal to ``/-`` - that's root) of our environment. The object we add has the `type` of ``com.yourdomain.HelloWorld`` - that's the class we just created two steps ago. Other keys in this json parameterize the object we create: they add a `name` and an `id` to the object. Id is mandatory, name is optional. Note that since the id, name and type are the `system properties` of our object, they are defined in a special section of the json - the so-called `?-header`. Non-system properties, if they existed, would be defined at a top-level of the object. We'll add them in a next chapter to see how they work. For now, save this JSON to some local file (say, ``input.json``) and let's finally deploy the thing. Execute the following sequence of commands: .. code-block:: shell $ murano environment-create TestHello +----------------------------------+-----------+--------+---------------------+---------------------+ | ID | Name | Status | Created | Updated | +----------------------------------+-----------+--------+---------------------+---------------------+ | 34bf673a26a8439d906827dea328c99c | TestHello | ready | 2016-10-04T13:19:12 | 2016-10-04T13:19:12 | +----------------------------------+-----------+--------+---------------------+---------------------+ $ murano environment-session-create 34bf673a26a8439d906827dea328c99c Created new session: +----------+----------------------------------+ | Property | Value | +----------+----------------------------------+ | id | 6d4a8fa2a5f4484fbc07740ef3ab60dd | +----------+----------------------------------+ $ murano environment-apps-edit --session-id 6d4a8fa2a5f4484fbc07740ef3ab60dd 34bf673a26a8439d906827dea328c99c ./input.json This first command creates a murano environment named ``TestHello``. Note the `id` of the created environment - we use it to reference it in subsequent operations. The second command creates a "configuration session" for this environment. Configuration sessions allow one to edit environments in transactional isolated manner. Note the `id` of the created sessions: all subsequent calls to modify or deploy the environment use both ids of environment and session. The third command applies the json-patch we've created before to our environment within the configuration session we created. Now, let's deploy the changes we made: .. code-block:: shell $ murano environment-deploy --session-id 6d4a8fa2a5f4484fbc07740ef3ab60dd 34bf673a26a8439d906827dea328c99c +------------------+---------------------------------------------+ | Property | Value | +------------------+---------------------------------------------+ | acquired_by | 7b0fe7c67ede443da9840adb2d518d5c | | created | 2016-10-04T13:39:34 | | description_text | | | id | 34bf673a26a8439d906827dea328c99c | | name | TestHello | | services | [ | | | { | | | "?": { | | | "name": "Demo", | | | "status": "deploying", | | | "type": "com.yourdomain.HelloWorld", | | | "id": "42" | | | } | | | } | | | ] | | status | deploying | | tenant_id | 60b7b5f7d4e64ff0b1c5f047d694d7ca | | updated | 2016-10-04T13:39:34 | | version | 0 | +------------------+---------------------------------------------+ This will deploy the environment. You may check for its status by executing the following command: .. code-block:: shell $ murano environment-show 34bf673a26a8439d906827dea328c99c +------------------+-----------------------------------------------------------------------------+ | Property | Value | +------------------+-----------------------------------------------------------------------------+ | acquired_by | None | | created | 2016-10-04T13:39:34 | | description_text | | | id | 34bf673a26a8439d906827dea328c99c | | name | TestHello | | services | [ | | | { | | | "?": { | | | "status": "ready", | | | "name": "Demo", | | | "type": "com.yourdomain.HelloWorld/0.0.0@com.yourdomain.HelloWorld", | | | "_actions": {}, | | | "id": "42", | | | "metadata": null | | | } | | | } | | | ] | | status | ready | | tenant_id | 60b7b5f7d4e64ff0b1c5f047d694d7ca | | updated | 2016-10-04T13:40:29 | | version | 1 | +------------------+-----------------------------------------------------------------------------+ As you can see, the status of the Environment has changed to ``ready``: it means that the application has been deployed. Open Murano Dashboard, navigate to Environment list and browse the contents of the ``TestHello`` environment there. You'll see that the 'Last Operation' column near the "Demo" component says "Hello, World!" - that's the reporting made by our application: .. image:: hello-world-screen-1.png This concludes the first part of our course. We've created a Murano Application Package, added a manifest describing its contents, written a class which reports a "Hello, World" message, packed all of these into a package archive and uploaded it to Murano Catalog and finally deployed an Environment with this application added. In the next part we will learn how to improve this application in various aspects, both from users' and developers' perspectives. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/doc/source/admin/appdev-guide/step-by-step/part2.rst0000664000175000017500000004067300000000000024761 0ustar00zuulzuul00000000000000Part 2: Customizing your Application Package -------------------------------------------- We've built a classic "Hello, World" application during the first part of this tutorial, now let's play a little with it and customize it for better user and developer experience - while learning some more Murano features, of course. Adding user input ~~~~~~~~~~~~~~~~~ Most deployment scenarios for cloud applications require user input. It may be various options which should be applied in software configuration files, passwords for default administrator's accounts, IP addresses of external services to register with and so on. Murano Application Packages may define the user inputs they expect, prompt the end-users to pass the values as these inputs, so that they may utilize these values during application lifecycle workflows. In Murano user input is defined for each class as `input properties`. `Properties` are object-level variables of the class, they may be of different kinds, and the `input properties` are the ones which are expected to contain user input. See :ref:`class_props` for details on other kinds of them. To define properties of the class you should add a ``Properties`` block somewhere in the YAML file of that class. .. note:: Usually it is better to place this block after the ``Name`` and ``Extends`` blocks but before the ``Methods`` block. Following this suggestion will improve the overall readability of your code. The ``Properties`` block should contain a YAML dictionary, mapping the names of the properties to their descriptions. These descriptions may specify the kind of properties, the restrictions on the type and value of the property (so-called `contracts`), provide default value for the property and so on. Let's add some user input to our "Hello, World" application. Let's ask the end user to provide their name, so the application will greet the user instead of the whole world. To do that, we need to edit our ``com.yourdomain.HelloWorld`` class to look the following way: .. code-block:: yaml :linenos: :emphasize-lines: 5-8 Name: com.yourdomain.HelloWorld Extends: io.murano.Application Properties: username: Usage: In Contract: $.string().notNull() Methods: deploy: Body: - $reporter: $this.find('io.murano.Environment').reporter - $reporter.report($this, "Hello, World!") On line 6 we declare a property named ``username``, on line 7 we specify that it is an input property, and on line 8 we provide a contract, i.e. a restriction on the value. This particular one states that the property's value should be a string and should not be null (i.e. should be provided by the user). .. note:: Although there are a total of 7 different kinds of properties, it turns out that the input ones are the most common. So, for input properties you may omit the ``Usage`` part - all the properties without an explicit usage are considered to be input properties. Once the property is declared within the ``Properties`` block, you may access it in the code of the class methods. Since the properties are object-level variables they may be accessed by calling a ``$this`` variable (which is a reference to a current instance of your class) followed by a dot and a property name. So, our ``username`` property may be accessed as ``$this.username``. Let's modify the ``deploy`` method of our class to make use of the property to greet the user by name: .. code-block:: yaml Methods: deploy: Body: - $reporter: $this.find('io.murano.Environment').reporter - $reporter.report($this, "Hello, " + $this.username + "!") OK, let's try it. Save the file and archive your package directory again, then re-import your zip-file to the Murano Catalog as a package. You'll probably get a warning, since the package with the same name already exists in the catalog (we imported it there in the previous part of the tutorial), so murano CLI will ask you if you want to update it. In production it is better to make a newer version of our application and thus to have both in the catalog, but for now let's just overwrite the old package with the new one. But you cannot deploy it with the old json input we used in the previous part: since the property's contract has that ``.notNull()`` part it means that the input should contain the value for the property. If you attempt to deploy an application without this value, you'll get an error. So, let's edit the ``input.json`` file we created in the previous part and add the value of the property to the input: .. code-block:: json :linenos: :emphasize-lines: 11 [ { "op": "add", "path": "/-", "value": { "?": { "name": "Demo", "type": "com.yourdomain.HelloWorld", "id": "42" }, "username": "Alice" } } ] Save the json file and repeat the steps from the previous part to create an environment, open a configuration session, add an application and deploy it. Now in the 'Last Operation' of Murano Dashboard you will see the updated reporting message, containing the username: .. image:: hello-world-screen-2.png :width: 100% Adding user interface ~~~~~~~~~~~~~~~~~~~~~ As you can see in all the examples above, deploying applications via Murano CLI is quite a cumbersome process: the user has to create environments and sessions and provide the appropriate json-based input for the application. This is inconvenient for a real user, of course. The CLI is intended to be used by various external automation systems which interact with Murano via scripts, but the human users will use Murano Dashboard which simplifies all those actions and provides a nice interface for them. Murano Dashboard provides a nice interface to create and deploy environments and manages sessions transparently for the end users, but when it comes to the generation of input JSON it can't do it out of the box: it needs some hints from the package developer. By having hints, Murano Dashboard will be able to generate nicely looking wizard-like dialogs to configure applications and add them to an environment. In this section we'll learn how to create these UI hints. The UI hints (also called `UI definitions`) should be defined in a separate YAML file (yeah, YAML again) in your application package. The file should be named ``ui.yaml`` and placed in a special directory of your package called ``UI``. The main section which is mandatory for all the UI definitions is called ``Application``: it defines the object structure which should be passed as the input to Murano. That's it: it is equivalent to the JSON ``input.json`` we were creating before. The data structure remains the same: ?-header is for system properties and all other properties belong inside the top level of the object. The ``Application`` section for our modified "Hello, World" application should look like this: .. code-block:: yaml :linenos: Application: ?: type: com.yourdomain.HelloWorld username: Alice This input is almost the same as the ``input.json`` we used last time, except that the data is expressed in a different format. However, there are several important differences: there are not JSON-Patch related keywords ("op", "path" and "value") - that's because Murano Dashboard will generate them automatically. Same is true for the missing ``id`` and ``name`` from the ?-header of the object: the dashboard will generate the id on its own and ask the end-user for the name, and then will insert both into the structure it sends to Murano. However, there is one problem in the example above: it has the ``username`` hardcoded to be Alice. Of course we do not want the user input to be hardcoded: it won't be an input then. So, let's define a user interface which will ask the end user for the actual value of this parameter. Since Murano Dashboard works like a step-by-step wizard, we need to define at least one wizard step (so-called `form`) and place a single text-box control into it, so the end-user will be able to enter his/her name there. These steps are defined in the ``Forms`` section of our ui definition file. This section should contain a list of key-value pairs. Keys are the identifiers of the forms, while values should define a list of `field` objects. Each field may define a name, a type, a description, a requirement indicator and some other attributes intended for advanced usage. For our example we need a single step with a single text field. The ``Forms`` section should look like this: .. code-block:: yaml :linenos: Forms: - step1: fields: - name: username type: string description: Username of the user to say 'hello' to required: true This defines the needed textbox control in the ui. Finally, we need to bind the value user puts into that textbox to the appropriate position in our ``Application`` section. To do that we replace the hardcoded value with an expression of form ``$..``. In our case this will be ``$step1.username``. So, our final UI definition will look like this: .. code-block:: yaml :linenos: Application: ?: type: com.yourdomain.HelloWorld username: $.step1.username Forms: - step1: fields: - name: username type: string description: Username of the user to say 'hello' to required: true Save this code into your ``UI/ui.yaml`` file and then re-zip your package directory and import the resulting archive to Murano Catalog again. Now, let's deploy this application using Murano Dashboard. Open Murano Dashboard with your browser, navigate to "Applications/Catalog/Environments" panel, click the "Create Environment" button, enter the name for your environment and click "Create". You'll be taken to the contents of your environment: you'll see that it is empty, but on top of the screen there is a list of components you may add to it. If your Murano Catalog was empty when you started this tutorial, this list will contain just one item: your "Hello, World" application. The screen should look like this: .. image:: new-env-1.png :width: 100% Drag-n-drop your "com.yourdomain.HelloWorld" application from the list on top of the screen to the "Drop components here" panel beneath it. You'll see a dialog, prompting you to enter a username: .. image:: configure-step1.png :width: 100% Enter the name and click "Next". Although you've configured just one step of the wizard, the actual interface will consist of two: the dashboard always adds a final step to prompt the user to enter the name of the application instance within the environment: .. image:: configure-step2.png :width: 100% When you click "Create" button an instance of your application will be added to the environment, you'll see it in the list of components: .. image:: new-env-2.png :width: 100% So, now you may click the "Deploy this Environment" button and the application will greet the user with the name you've entered. .. image:: new-env-3.png :width: 100% Simplifying code: namespaces ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Now that we've learned how to simplify the user's life by adding a UI definition, let's simplify the developer's life a bit. When you were working with Murano classes in the previous part you probably noticed that the long class names with all those domain-name-based segments were hard to write and that it was easy to make a mistake: .. code-block:: yaml :linenos: Name: com.yourdomain.HelloWorld Extends: io.murano.Application Methods: deploy: Body: - $reporter: $this.find('io.murano.Environment').reporter - $reporter.report($this, "Hello, World!") To simplify the code we may use the concept of `namespaces` and `short names`. All but last segments of a long class name are namespaces, while the last segment is a short name of a class. In our example ``com.yourdomain`` is a namespace while the ``HelloWorld`` is a short name. Short names have to be unique only within their namespace, so they tend to be expressive, short and human readable, while the namespaces are globally unique and thus are usually long and too detailed. Murano provides a capability to abbreviate long namespaces with a short alias. Unlike namespaces, aliases don't need to be globally unique: they have to be unique only within a single file which uses them. So, they may be very short. So, in your file you may abbreviate your ``com.yourdomain`` namespace as ``my``, and standard Murano's ``io.murano`` as ``std``. Then instead of a long class name you may write a namespace alias followed by a colon character and then a short name, e.g. ``my:HelloWorld`` or ``std:Application``. This becomes very helpful when you have lots of class names in your code. To use this feature, declare a special section called ``Namespaces`` in your class file. Inside that section provide a mapping of namespace aliases to full namespaces, like this: .. code-block:: yaml Namespaces: my: com.yourdomain std: io.murano .. note:: Since namespaces are often used in all other sections of files it is considered good practice to declare this section at a very top of your class file. Quite often there is a namespace which is used much more often than others in a given file. In this case it would be beneficial to declare this namespace as a `default namespace`. Default namespace does not need a prefix at all: you just type short name of the class and Murano will interpret it as being in your default namespace. Use '=' character to declare the default namespace in your namespaces block: .. code-block:: yaml :linenos: :emphasize-lines: 2,5 Namespaces: =: com.yourdomain std: io.murano Name: HelloWorld Extends: std:Application Methods: deploy: Body: - $reporter: $this.find(std:Environment).reporter - $reporter.report($this, "Hello, World!") Notice that ``Name`` definition at line 5 uses the default namespace: the ``HelloWorld`` is not prefixed with any namespaces, but is properly resolved to ``com.yourdomain.HelloWorld`` because of the default namespace declaration at line 2. Also, because Murano recognizes the ``ns:Class`` syntax there is no need to enclose ``std:Environment`` in quote marks, though it will also work. Adding more info for the catalog ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ As you could see while browsing Murano Catalog your application entry in it is not particularly informative: the user can't get any description about your app, and the long domain-based name is not very user-friendly aither. This can easily be improved. The ``manifest.yaml`` which we wrote in the first part contained only mandatory fields. This is how it should look by now: .. code-block:: yaml :linenos: FullName: com.yourdomain.HelloWorld Type: Application Description: | A package which demonstrates development for Murano by greeting the user. Classes: com.yourdomain.HelloWorld: HelloWorld.yaml Let's add more fields here. First, you can add a ``Name`` attribute. Unlike ``FullName``, it is not a unique identifier of the package. But, if specified, it overrides the name of the package that is displayed in the catalog. Then an ``Author`` field: here you can put your name or the name of your company, so it will be displayed in catalog as the name of the package developer. If this field is omitted, the catalog will consider the package to be made by "OpenStack", so don't forget this field if you care about your copyright. When you add these fields your manifest may look like this: .. code-block:: yaml :linenos: FullName: com.yourdomain.HelloWorld Type: Application Name: 'Hello, World' Description: | A package which demonstrates development for Murano by greeting the user. Author: John Doe Classes: com.yourdomain.HelloWorld: HelloWorld.yaml You may also add an icon to be displayed for your application. To do that just place a ``logo.png`` file with an appropriate image into the root folder of your package. Zip the package directory and re-upload the file to the catalog. Then use Murano Dashboard and navigate to Applications/Catalog/Browse panel. You'll see that your app gets a logo, a more appropriate name and a description: .. image:: hello-world-desc.png :width: 50% So, here we've learned how to improve both the user's and developer's experience with developing Murano application packages. That was all we could do with the oversimplistic "Hello World" app. Let's move forward and touch some real-life applications. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/doc/source/admin/appdev-guide/step-by-step/part3.rst0000664000175000017500000007245000000000000024760 0ustar00zuulzuul00000000000000Part 3: Creating a Plone CMS application package ------------------------------------------------ If you've completed "Hello, World" scenarios in the previous parts and are ready for some serious tasks, we've got a good example here. Let's automate the deployment of some real application. We've chosen a "Plone CMS" for this purpose. Plone is a simple, but powerful and flexible Content Management System which can efficiently run on cloud. Its deployment scenario can be very simple for demo cases and can become really complicated for production-grade usage. So it's a good playground: in this part we'll create a Murano application to address the simplest scenario, then we will gradually add more features of production-grade deployments. .. note:: To learn more about Plone, its features, capabilities and deployment scenarios you may visit the `Official website of Plone Foundation `_. The goal ~~~~~~~~ Simplest deployment of Plone CMS requires a single server, or, in the case of OpenStack, a Virtual Machine, to run on. Then a software should be downloaded and configured to run on that server. So, as a bare minimum our Plone application package for Murano should automate the following steps: #. Provision a virtual machine in OpenStack (VM); #. Configure ths VM's network connectivity and security; #. Download a distribution of Plone from Internet to the virtual machine; #. Install the distribution and configure some of its parameters with user input. Preparation ~~~~~~~~~~~ First let's revisit what we've learned in previous parts and create a new application package with its manifest and create a class file to contain the logic of your app. Create a new directory for a package, call it ``PloneApp``. Create a ``manifest.yaml`` file as described in part 1 of this tutorial in the root of the package and fill it with data: name your package ``com.yourdomain.Plone``, set its type to ``Application``, give it a display name of "Plone CMS" and put your name as the author of the package: .. code-block:: yaml :linenos: FullName: com.yourdomain.Plone Name: Plone CMS Description: Simple Plone Deployment Type: Application Author: John Doe Then create a ``Classes`` sub directory inside your package directory and create a ``plone.yaml`` there. This will be your application class. At the top of this file declare a `Namespace` section: this will simplify the code and save time on typing long class names. Make your namespace (``com.yourdomain``) a default namespace of the file, also include the standard namespace for Murano applications - ``io.murano``, alias it as ``std``. Don't forget to include the ``Name`` of your class. Since you've declared a default namespace for a file you can name your class without a need to type its long part, just using the shortname. Also include the ``Extends`` section: same as in our "Hello, World" example this application will inherit the ``io.murano.Application`` class, but since we've aliased this namespace as well, it may be shortened to ``std:Application`` By now your class file should look like this: .. code-block:: yaml Namespaces: =: com.yourdomain std: io.murano Name: Plone Extends: std:Application We'll add the actual logic in the next section. Now, save the file and include it into the ``Classes`` section of your manifest.yaml, which should now look like this: .. code-block:: yaml :linenos: :emphasize-lines: 6-7 FullName: com.yourdomain.Plone Name: Plone CMS Description: Simple Plone Deployment Type: Application Author: John Doe Classes: com.yourdomain.Plone: plone.yaml You are all set and ready to go. Let's add the actual deployment logic. Library classes ~~~~~~~~~~~~~~~ Murano comes bundled with a so-called "Murano Core Library" - a Murano Package containing the classes to automate different scenarios of interaction with other entities such as OpenStack services or virtual machines. They follow object-oriented design: for example, there is a Murano class called ``Instance`` which represents an OpenStack virtual machine: if you create an object of this class and execute a method called ``deploy`` for it Murano will do all the needed system calls to OpenStack Services to orchestrate the provisioning of a virtual machine and its networking configuration. Then this object will contain information about the state and configuration of the VM, such as its hostname, ip addresses etc. After the VM is provisioned you can use its object to send the configuration scripts to the VM to install and configure software for your application. Other OpenStack resources such as Volumes, Networks, Ports, Routers etc also have their corresponding classes in the core library. Provisioning a VM ~~~~~~~~~~~~~~~~~ When creating your application package you can `compose` your application out of the components of core library. For example for an application which should run on a VM you can define an input property called ``instance`` and restrict the value type of this property to the aforementioned ``Instance`` class with a contract. Let's do that in the ``plone.yaml`` class file you've created. First, add a new namespace alias to your ``Namespaces`` section: shorten ``io.murano.resources`` as ``res``. This namespace of the core library contains all the resource classes, including the ``io.murano.resources.Instance`` which we need to define the virtual machine: .. code-block:: yaml :emphasize-lines: 4 Namespaces: =: com.yourdomain std: io.murano res: io.murano.resources Now, let's add an input property to your class: .. code-block:: yaml :linenos: Properties: instance: Usage: In Contract: $.class(res:Instance) Notice the contract at line 4: it limits the values of this property to the objects of class ``io.murano.resources.Instance`` or its subclasses. This defines that your application needs a virtual machine. Now let's ensure that it is provisioned - or provision it otherwise. Add a ``deploy`` method to your application class and call instance's deploy method from it: .. code-block:: yaml :linenos: Methods: deploy: Body: - $this.instance.deploy() That's very simple: you just access the ``instance`` property of your current object and run a method ``deploy`` for it. The core library defines this method of the ``Instance`` class in an `idempotent` manner: you may call it as many times as you want: the first call will actually provision the virtual machine in the cloud, while all the subsequent calls will no nothing, thus you may always call this method to ensure that the VM was properly provisioned. It's important since we define it as an input property: theoretically a user can pass an already-provisioned VM object as input, but you need to be sure. Always calling the ``deploy`` method is the best practice to follow. Running a command on the VM ~~~~~~~~~~~~~~~~~~~~~~~~~~~ Once the VM has been provisioned you may execute various kinds of software configuration scenarios on it to install and configure the actual application on the VM. Murano supports different types of software configuration tools to be run on a VM, but the simplest and the most common type is just a shell script. To run a shell script on a virtual machine you may use a `static method` ``runCommand`` of class ``io.murano.configuration.Linux``. Since this method is static you do not need to create any objects of its class: you can just do something like: .. code-block:: yaml - type('io.murano.configuration.Linux').runCommand($server.agent, 'sudo apt-get update') or, if we declare another namespace prefix .. code-block:: yaml Namespaces: ... conf: io.murano.configuration this may be shortened to .. code-block:: yaml - conf:Linux.runCommand($server.agent, 'sudo apt-get update') In this case ``$server`` should be a variable containing an object of ``io.murano.resources.Instance`` class, everything you pass as a second argument (``apt get update`` in the example above) is the shell command to be executed on a VM. You may pass not just a single line, but a multi-line text: it will be treated as a shell script. .. note:: The shell scripts and commands you send to a VM are executed by a special software component running on the VM - a `murano agent`. For the most popular distributions of Linux (Debian, Ubuntu, Centos, Fedora, etc.) it automatically gets installed on the VM once it is provisioned, but for other distribution and non-Linux OSes it has to be manually pre-installed in the image. See :ref:`Building Murano Image ` for details. Loading a script from a resource file ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Passing strings as a second argument of a ``runCommand`` method is convenient for short commands like the ``apt-get update`` shown in an example above. However for larger scripts it is not that useful. Instead it is preferable to load a script text from a file and run it. You can do that in Murano. For example, let's make a script which downloads, unpacks, installs and configures Plone CMS on our VM. First, create a directory called ``Resources`` inside your package directory. Then, create a file named ``install-plone.sh`` and put the following script there: .. code-block:: shell #!/bin/bash #input parameters PL_PATH="$1" PL_PASS="$2" PL_PORT="$3" # Write log. Redirect stdout & stderr into log file: exec &> /var/log/runPloneDeploy.log # echo "Update all packages." sudo apt-get update # Install the operating system software and libraries needed to run Plone: sudo apt-get install python-setuptools python-dev build-essential libssl-dev libxml2-dev libxslt1-dev libbz2-dev libjpeg62-dev # Install optional system packages for the handling of PDF and Office files. Can be omitted: sudo apt-get install libreadline-dev wv poppler-utils # Download the latest Plone unified installer: wget --no-check-certificate https://launchpad.net/plone/5.0/5.0.4/+download/Plone-5.0.4-UnifiedInstaller.tgz # Unzip the latest Plone unified installer: tar -xvf Plone-5.0.4-UnifiedInstaller.tgz cd Plone-5.0.4-UnifiedInstaller # Set the port that Plone will listen to on available network interfaces. Editing "http-address" param in buildout.cfg file: sed -i "s/^http-address = [0-9]*$/http-address = ${PL_PORT}/" buildout_templates/buildout.cfg # Run the Plone installer in standalone mode ./install.sh --password="${PL_PASS}" --target="${PL_PATH}" standalone # Start Plone cd "${PL_PATH}/zinstance" bin/plonectl start .. note:: As you can see, this script uses apt to install the prerequisite software packages, so it expects a Debian-compatible Linux distro as the VM operating system. This particular script was tested on Ubuntu 14.04. Other distros may have a different set of preinstalled software and thus require different additional prerequisites. The comments in the script give the needed explanation: the script installs all the prerequisites, downloads a targz archive with a distribution of Plone, unpacks it, edits the ``buildout.cfg`` file to specify the port Plone will listen at, then runs the installation script which is included in the distribution. When that script is finished, the Plone daemon is started. Save the file as ``Resources/install-plone.sh``. Now you may load its contents into a string variable in your class file. To do that, you need to use another static method: a ``string()`` method of a ``io.murano.system.Resources`` class: .. code-block:: yaml - $script: type('io.murano.system.Resources').string('install-plone.sh') or, with the introduction of another namespace prefix .. code-block:: yaml - $script: sys:Resources.string('install-plone.sh') But before sending this script to a VM, it needs to be parametrized: as you can see in the script snippet above, it declares three variables which are used to set the installation path in the VM's filesystem, a default administrator's password and a listening port. In the script these values are initialized with stubs ``$1``, ``$2`` and ``$3``, now we need to replace these stubs with the actual user input. To do that our class needs to define the appropriate input properties and then do string replacement. First, let's define the appropriate input properties in the ``Properties`` block of the class, right after the ``instance`` property: .. code-block:: yaml :linenos: :emphasize-lines: 6-18 Properties: instance: Usage: In Contract: $.class(res:Instance) installationPath: Usage: In Contract: $.string().notNull() Default: '/opt/plone' defaultPassword: Usage: In Contract: $.string().notNull() listeningPort: Usage: In Contract: $.int().notNull() Default: 8080 Now, let's replace the stub values in that script value we've loaded into the ``$script`` variable. This may be done using a ``replace`` function: .. code-block:: yaml - $script: $script.replace({"$1" => $this.installationPath, "$2" => $this.defaultPassword, "$3" => $this.listeningPort}) Finally, the resulting ``$script`` variable may be passed as a second argument of a ``runCommand`` method, while the first one should be the ``instance`` property, containing our VM-object: .. code-block:: yaml - conf:Linux.runCommand($this.instance.agent, $script) Configuring OpenStack Security ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ By now we've got code which provisions a VM and a script which deploys and configures Plone on it. However, in most OpenStack clouds this is not enough: usually all incoming traffic to all the VMs is blocked by default, so we need to configure security group of OpenStack to allow the incoming http calls to our VM on the port our Plone server listens at. To do that we need to use a ``securityGroupManager`` property of the ``Environment`` class which owns our application. That property contains an object of type ``io.murano.system.SecurityGroupManager``, which defines a ``addGroupIngress`` method. This method allows us to add a security group rule to allow incoming traffic of some type through a specific port within a port range. It accepts a list of YAML objects, each having four keys: ``FromPort`` and ``ToPort`` to define the boundaries of the port range, ``IpProtocol`` to define the type of the protocol and ``External`` boolean flag to indicate if the incoming traffic should be be allowed to originate from outside of the environment (if this flag is false, the traffic will be accepted only from the VMs deployed by the application in the same Murano environment). Let's do this in code: .. code-block:: yaml :linenos: - $environment: $this.find(std:Environment) - $manager: $environment.securityGroupManager - $rules: - FromPort: $this.listeningPort ToPort: $this.listeningPort IpProtocol: tcp External: true - $manager.addGroupIngress($rules) - $environment.stack.push() It's quite straightforward, just notice the last line. It is required, because current implementation of ``SecurityGroupManager`` relies on Heat underneath - it modifies the `Heat Stack` associated with our environment, but does not apply the changes to the actual cloud. To apply them the stack needs to be `pushed`, i.e. submitted to Heat Orchestration service. The last line does exactly that. Notifying end-user on Plone location ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ When the deployment is completed and our instance of Plone server starts listening on a provisioned virtual machine, the end user has one last question to solve: to find out where it is. Of course, the user may use OpenStack Dashboard to list all the provisioned VMs, find the one which has just been created and look for its IP address. But that's inconvenient. It would be much better if Murano notified the end-user on where to find Plone once it is ready. We may utilize the same approach we used in the previous parts to say "Hello, World" - call a ``report`` method of ``reporter`` attribute of the ``Environment`` class. The tricky part is getting the IP address. Class ``io.murano.resources.Instance`` has an `output property` called ``ipAddresses``. Unlike input properties the output ones are not provided by users but are set by objects themselves while their methods are executed. The ``ipAddresses`` is assigned during the execution of ``deploy`` method of the VM. The value is the list of ip addresses assigned to different interfaces of the machine. Also, if the ``assignFloatingIp`` input property is set to ``true``, another output property will be set during the execution of ``deploy`` - a ``floatingIpAddress`` will contain the floating ip attached to the VM. Let's use this knowledge and build a proper report message: .. code-block:: yaml :linenos: - $message: 'Plone is up and running at ' - If: $this.instance.assignFloatingIp Then: - $message: $message + $this.instance.floatingIpAddress Else: - $message: $message + $this.instance.ipAddresses.first() - $message: $message + ":" + str($this.listeningPort) - $environment.reporter.report($this, $message) Note the usage of ``If`` expression: it is similar to other programming languages, just uses YAML keys to define the "if" and "else" blocks. This code creates a string variable called ``$message``, initializes it with the beginning of the message string, then appends either a floating ip address of the VM (if it's set) or the first of the regular ips otherwise. Then it appends a listening port after a colon character - and reports the resulting message to the user. Completing the Plone class ~~~~~~~~~~~~~~~~~~~~~~~~~~ We've got all the pieces to deploy our Plone application, now let's combine them together. Our final class file should look like this: .. code-block:: yaml Namespaces: =: com.yourdomain std: io.murano res: io.murano.resources sys: io.murano.system Name: Plone Extends: std:Application Properties: instance: Usage: In Contract: $.class(res:Instance) installationPath: Usage: In Contract: $.string().notNull() Default: '/opt/plone' defaultPassword: Usage: In Contract: $.string().notNull() listeningPort: Usage: In Contract: $.int().notNull() Default: 8080 Methods: deploy: Body: - $this.instance.deploy() - $script: sys:Resources.string('install-plone.sh') - $script: $script.replace({ "$1" => $this.installationPath, "$2" => $this.defaultPassword, "$3" => $this.listeningPort }) - type('io.murano.configuration.Linux').runCommand($this.instance.agent, $script) - $environment: $this.find(std:Environment) - $manager: $environment.securityGroupManager - $rules: - FromPort: $this.listeningPort ToPort: $this.listeningPort IpProtocol: tcp External: true - $manager.addGroupIngress($rules) - $environment.stack.push() - $formatString: 'Plone is up and running at {0}:{1}' - If: $this.instance.assignFloatingIp Then: - $address: $this.instance.floatingIpAddress Else: - $address: $this.instance.ipAddresses.first() - $message: format($formatString, $address, $this.listeningPort) - $environment.reporter.report($this, $message) That's all, our class is ready. Providing a UI definition ~~~~~~~~~~~~~~~~~~~~~~~~~ Last but not least, we need to add a UI definition file to define a template for the user input and create wizard steps. This time both are a bit more complicated than they were for the "Hello, World" app. First, let's create the wizard steps. It's better to decompose the UI into two steps: the first one will define the properties of a Virtual Machine, and the second one the configuration properties of the Plone application itself. .. code-block:: yaml :linenos: Forms: - instanceConfiguration: fields: - name: hostname type: string required: true - name: image type: image imageType: linux - name: flavor type: flavor - name: assignFloatingIp type: boolean - ploneConfiguration: fields: - name: installationPath type: string - name: defaultPassword type: password required: true - name: listeningPort type: integer This is familiar to what we had on the previous step, however there are several new types of fields: while the types ``integer`` and ``boolean`` are quite obvious - they will render a numeric up-and-down textbox and checkbox controls respectively - other field types are more specific. Field of type ``image`` will render a drop-down list allowing you to choose an image for your VM, and the list of images will contain only the ones having appropriate metadata associated (the type of metadata is defined by the ``imageType`` attribute: this particular example requires it to be tagged as "Generic Linux"). Field of type ``flavor`` will render a drop-down list allowing you to choose a flavor for your VM among the ones registered in Nova. Field of type ``password`` will render a pair of text-boxes in a password input mode (i.e. hiding all the input with '*'-characters). The rendered field will have appropriate validation: it will ensure that the values entered in both fields are identical (thus providing a "repeat password" functionality) and will also enforce password complexity check. This defines the basic UI, but it is not particularly user friendly: when MuranoDashboard renders the wizard it will label appropriate controls with the names of the fields, but they usually don't look informative and pretty. So, to improve the user experience you may add additional attributes to field descriptors here. ``label`` attribute allows you to define a custom label to be rendered next to appropriate control, ``description`` allows you to provide a longer text to be displayed on the form as a description of the control, and, finally, an ``initial`` attribute allows you define the default value to be entered into the control when it is shown to the end-user. Modify the ``Forms`` section to use these attributes: .. code-block:: yaml :linenos: :emphasize-lines: 6-9,14-17,20-23,26-28,33-36,38-39,44-46 Forms: - instanceConfiguration: fields: - name: hostname type: string label: Host Name description: >- Enter a hostname for a virtual machine to be created initial: plone-vm required: true - name: image type: image imageType: linux label: Instance image description: >- Select valid image for the application. Image should already be prepared and registered in glance. - name: flavor type: flavor label: Instance flavor description: >- Select registered in Openstack flavor. Consider that application performance depends on this parameter. - name: assignFloatingIp type: boolean label: Assign Floating IP description: >- Check to assign floating IP automatically - ploneConfiguration: fields: - name: installationPath type: string label: Installation Path initial: '/opt/plone' description: >- Enter the path on the VM filesystem to deploy Plone into - name: defaultPassword label: Admin password description: Default administrator's password type: password required: true - name: listeningPort type: integer label: Listening Port description: Port to listen at initial: 8080 Now, let's add an ``Application`` section to provide templated input for our app: .. code-block:: yaml :linenos: Application: ?: type: com.yourdomain.Plone instance: ?: type: io.murano.resources.LinuxMuranoInstance name: $.instanceConfiguration.hostname image: $.instanceConfiguration.image flavor: $.instanceConfiguration.flavor assignFloatingIp: $.instanceConfiguration.assignFloatingIp installationPath: $.ploneConfiguration.installationPath defaultPassword: $.ploneConfiguration.defaultPassword listeningPort: $.ploneConfiguration.listeningPort Note the ``instance`` part here: since our ``instance`` input property is not a scalar value but rather an object, we are placing another object template inside the appropriate section. Note that the type of this object is not ``io.murano.resources.Instance`` as you could expect based on the property contract, but a more specific class: ``LinuxMuranoInstance`` in the same namespace. Since this class inherits the former, it matches the contract, but it provides a more appropriate implementation than the base one. Let's combine the two snippets together, we'll get the final UI definition of our app: .. code-block:: yaml :linenos: Application: ?: type: com.yourdomain.Plone instance: ?: type: io.murano.resources.LinuxMuranoInstance name: $.instanceConfiguration.hostname image: $.instanceConfiguration.image flavor: $.instanceConfiguration.flavor assignFloatingIp: $.instanceConfiguration.assignFloatingIp installationPath: $.ploneConfiguration.installationPath defaultPassword: $.ploneConfiguration.defaultPassword listeningPort: $.ploneConfiguration.listeningPort Forms: - instanceConfiguration: fields: - name: hostname type: string label: Host Name description: >- Enter a hostname for a virtual machine to be created initial: 'plone-vm' required: true - name: image type: image imageType: linux label: Instance image description: >- Select valid image for the application. Image should already be prepared and registered in glance. - name: flavor type: flavor label: Instance flavor description: >- Select registered in Openstack flavor. Consider that application performance depends on this parameter. - name: assignFloatingIp type: boolean label: Assign Floating IP description: >- Check to assign floating IP automatically - ploneConfiguration: fields: - name: installationPath type: string label: Installation Path initial: '/opt/plone' description: >- Enter the path on the VM filesystem to deploy Plone into - name: defaultPassword label: Admin password description: Default administrator's password type: password required: true - name: listeningPort type: integer label: Listening Port description: Port to listen at initial: 8080 Save this file as a ``ui.yaml`` in a ``UI`` folder of your package. As a final touch add a logo to the package - save the image below to the root directory of your package as ``logo.png``: .. image:: plone-logo.png :width: 100 The package is ready. Zip it and import to Murano catalog. We are ready to try it. Deploying the package ~~~~~~~~~~~~~~~~~~~~~ Go to Murano Dashboard, create an environment and add a "Plone CMS" application to it. You'll see the nice wizard with all the field labels and descriptions you've added to the ui definition file: .. image:: plone-simple-step1.png :width: 50% .. image:: plone-simple-step2.png :width: 50% After the app is added to the environment, click the "Deploy this environment" button. The deployment will take about 10 minutes, depending on the speed of the VM's internet connection and the amount of packages to be updated. When it is over, check the "Last operation" column in the environment's list of components near the Plone component. It should contain a message "Plone is up and running at ..." followed by ip address and port: .. image:: plone-ready.png :width: 50% Enter this address to the address bar of your browser. You'll see the default management interface of Plone: .. image:: plone-admin.png :width: 50% If you click a "Create a new Plone site" button you'll be prompted for username and password. Use ``admin`` username and the password which you entered in the Wizard. See `Plone Documentation `_ for details on how to operate Plone. This concludes this part of the course. The application package we created demonstrates the basic capabilities of Murano for the deployments of real-world applications. However, the deployed configuration of Plone is not of production-grade service: it is just a single VM with all-in-one service topology, which is not a scalable or fault-tolerant solution. In the next part we will learn some advanced features which may help to bring more production-grade capabilities to our package. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/doc/source/admin/appdev-guide/step-by-step/part4.rst0000664000175000017500000003332500000000000024757 0ustar00zuulzuul00000000000000Part 4: Refactoring code to use the Application Framework --------------------------------------------------------- Up until this point we wrote the Plone application in a manner that was common to all applications that were written before the application framework was introduced. In this last tutorial step we are going to refactor the Plone code in order to take advantage of the framework. Application framework was written in order to simplify the application development and encapsulate common deployment workflows. This gives things primitives for application scaling and high availability without the need to develop them over and over again for each application. When using the frameworks, an application developer only has to inherit the class that best suits him and provide it only with the code that is specific to the application, while leaving the rest to the framework. This typically includes: * instructions on how to provision the software on each node (server) * instructions on how to configure the provisioned software * server group onto which the software should be installed. This may be a fixed server list, a shared server pool, or a scalable server group that creates servers using the given instance template, or one of the several other implementations provided by the framework The framework is located in a separate library package ``io.murano.applications`` that is shipped with Murano. We are going to use the ``apps`` namespace prefix to refer to this namespace through the code. Step 1: Add dependency on the App Framework ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ In order to use one Murano Package from another, the former must be explicitly specified as a requirement for the latter. This is done by filling the ``Require`` section in the package's manifest file. Open the Plone's manifest.yaml file and append the following lines: .. code-block:: yaml Require: io.murano.applications: Requirements are specified as a mapping from package name to the desired version of that package (or version range). The missing value indicates the dependency on the latest ``0.*.*`` version of the package which is exactly what we need since the current version of the app framework library is 0. Step 2: Get rid of the instance ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Since we are going to have a multi-sever Plone application there won't be a single instance belonging to the application. Instead, we are going to provide it with the server group that abstracts the server management from the application. So instead of .. code-block:: yaml Properties: instance: Contract: $.class(res:Instance) we are going to have .. code-block:: yaml Properties: servers: Contract: $.class(apps:ServerGroup).notNull() Step 3: Change the base classes ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Another change that we are going to make to the main application class is to change its base classes. Regular applications inherit from the ``std:Application`` which only has the method ``deploy`` that does all the work. Application framework provides us with its own implementation of that class and method. Instead of one monolithic method that does everything, with the framework, the application provides only the code needed to provision and configure the software on each server. So instead of ``std:Application`` class we are going to inherit two of the framework classes: .. code-block:: yaml Extends: - apps:MultiServerApplicationWithScaling - apps:OpenStackSecurityConfigurable The first class tells us that we are going to have an application that runs on multiple servers. In the following section we are going to split out ``deploy`` method into two smaller methods that are going to be invoked by the framework to install the software on each of the servers. By inheriting the ``apps:MultiServerApplicationWithScaling``, the application automatically gets all the UI buttons to scale it out and in. The second class is a mix-in class that tells the framework that we are going to provide the OpenStack-specific security group configuration for the application. Step 4: Split the deployment logic ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ In this step we are going to split the installation into two phases: provisioning and configuration. Provisioning is implemented by overriding the ``onInstallServer`` method, which is called every time a new server is added to the server group. In this method we are going to install the Plone software bits onto the server (which is provided as a method parameter). Configuration is done through the ``onConfigureServer``, which is called upon the first installation on the server, and every time any of the application settings change, and ``onCompleteConfiguration`` which is executed on each server after everything was configured so that we can perform post-configuration steps like starting application daemons and reporting messages to the user. Thus we are going to split the ``install-plone.sh`` script into two scripts: ``installPlone.sh`` and ``configureServer.sh`` and execute each one in their corresponding methods: .. code-block:: yaml onInstallServer: Arguments: - server: Contract: $.class(res:Instance).notNull() - serverGroup: Contract: $.class(apps:ServerGroup).notNull() Body: - $file: sys:Resources.string('installPlone.sh').replace({ "$1" => $this.deploymentPath, "$2" => $this.adminPassword }) - conf:Linux.runCommand($server.agent, $file) onConfigureServer: Arguments: - server: Contract: $.class(res:Instance).notNull() - serverGroup: Contract: $.class(apps:ServerGroup).notNull() Body: - $primaryServer: $serverGroup.getServers().first() - If: $server = $primaryServer Then: - $file: sys:Resources.string('configureServer.sh').replace({ "$1" => $this.deploymentPath, "$2" => $primaryServer.ipAddresses[0] }) Else: - $file: sys:Resources.string('configureClient.sh').replace({ "$1" => $this.deploymentPath, "$2" => $this.servers.primaryServer.ipAddresses[0], "$3" => $this.listeningPort}) - conf:Linux.runCommand($server.agent, $file) onCompleteConfiguration: Arguments: - servers: Contract: - $.class(res:Instance).notNull() - serverGroup: Contract: $.class(apps:ServerGroup).notNull() - failedServers: Contract: - $.class(res:Instance).notNull() Body: - $startCommand: format('{0}/zeocluster/bin/plonectl start', $this.deploymentPath) - $primaryServer: $serverGroup.getServers().first() - If: $primaryServer in $servers Then: - $this.report('Starting DB node') - conf:Linux.runCommand($primaryServer.agent, $startCommand) - conf:Linux.runCommand($primaryServer.agent, 'sleep 10') - $otherServers: $servers.where($ != $primaryServer) - If: $otherServers.any() Then: - $this.report('Starting Client nodes') # run command on all other nodes in parallel with pselect - $otherServers.pselect(conf:Linux.runCommand($.agent, $startCommand)) # build an address string with IPs of all our servers - $addresses: $serverGroup.getServers(). select( switch($.assignFloatingIp => $.floatingIpAddress, true => $.ipAddresses[0]) + ':' + str($this.listeningPort) ).join(', ') - $this.report('Plone listeners are running at ' + str($addresses)) During configuration phase we distinguish the first server in the server group from the rest of the servers. The first server is going to be the primary node and treated differently from the others. Step 5: Configuring OpenStack security group ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ The last change to the main class is to set up the security group rules. We are going to do this by overriding the ``getSecurityRules`` method that we inherited from the ``apps:OpenStackSecurityConfigurable`` class: .. code-block:: yaml getSecurityRules: Body: - Return: - FromPort: $this.listeningPort ToPort: $this.listeningPort IpProtocol: tcp External: true - FromPort: 8100 ToPort: 8100 IpProtocol: tcp External: false The code is very similar to that of the old ``deploy`` method with the only difference being that it returns the rules rather than sets them on its own. Step 6: Provide the server group instance ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Do you remember, that previously we replaced the ``instance`` property with ``servers`` of type ``apps:ServerGroup``? Since the object is coming from the UI definition, we must change the latter in order to provide the class with the ``apps:ServerReplicationGroup`` instance rather than ``resources:Instance``. To do this we are going to replace the ``instance`` property in the Application template with the following snippet: .. code-block:: yaml servers: ?: type: io.murano.applications.ServerReplicationGroup numItems: $.ploneConfiguration.numNodes provider: ?: type: io.murano.applications.TemplateServerProvider template: ?: type: io.murano.resources.LinuxMuranoInstance flavor: $.instanceConfiguration.flavor image: $.instanceConfiguration.osImage assignFloatingIp: $.instanceConfiguration.assignFloatingIP serverNamePattern: $.instanceConfiguration.unitNamingPattern If you take a closer look at the code above you will find out that the new declaration is very similar to the old one. But now instead of providing the ``Instance`` property values directly, we are providing them as a template for the ``TemplateServerProvider`` server provider. ``ServerReplicationGroup`` is going to use the provider each time it requires another server. In turn, the provider is going to use the familiar template for the new instances. Besides the instance template we also specify the initial number of Plone nodes using the ``numItems`` property and the name pattern for the servers. Thus we must also add it to the list of our controls: .. code-block:: yaml Forms: - instanceConfiguration: fields: ... - name: unitNamingPattern type: string label: Instance Naming Pattern required: false maxLength: 64 initial: 'plone-{0}' description: >- Specify a string, that will be used in instance hostname. Just A-Z, a-z, 0-9, dash and underline are allowed. - ploneConfiguration: fields: ... - name: numNodes type: integer label: Initial number of Client Nodes initial: 1 minValue: 1 required: true description: >- Select the initial number of Plone Client Nodes Step 6: Using server group composition ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ By this step we should already have a working Plone application. But let's go one step further and enhance our sample application. Since we are running the database on the first server group server only, we might want it to have different properties. For example we might want to give it a bigger flavor or just a special name. This is a perfect opportunity for us to demonstrate how to construct complex server groups. All we need to do is to just use another implementation of ``apps:ServerGroup``. Instead of ``apps:ServerReplicationGroup`` we are going to use the ``apps:CompositeServerGroup`` class, which allows us to compose several server groups together. One of them is going to be a single-server server group consisting of our primary server, and the second is going to be the scalable server group that we used to create in the previous step. So again, we change the ``Application`` section of our UI definition file with even a more advanced ``servers`` property definition: .. code-block:: yaml servers: ?: type: io.murano.applications.CompositeServerGroup serverGroups: - ?: type: io.murano.applications.SingleServerGroup server: ?: type: io.murano.resources.LinuxMuranoInstance name: format($.instanceConfiguration.unitNamingPattern, 'db') image: $.instanceConfiguration.image flavor: $.instanceConfiguration.flavor assignFloatingIp: $.instanceConfiguration.assignFloatingIp - ?: type: io.murano.applications.ServerReplicationGroup numItems: $.ploneConfiguration.numNodes provider: ?: type: io.murano.applications.TemplateServerProvider template: ?: type: io.murano.resources.LinuxMuranoInstance flavor: $.instanceConfiguration.flavor image: $.instanceConfiguration.osImage assignFloatingIp: $.instanceConfiguration.assignFloatingIP serverNamePattern: $.instanceConfiguration.unitNamingPattern Here the instance definition for the ``SingleServerGroup`` (our primary server) differs from the servers in the ``ServerReplicationGroup`` by its name only. However the same technique might be used to customize other properties as well as to create even more sophisticated server group topologies. For example, we could implement region bursting by composing several scalable server groups that allocate servers in different regions. And all of that without making any changes to the application code itself! ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/doc/source/admin/appdev-guide/step-by-step/plone-admin.png0000664000175000017500000043220500000000000026104 0ustar00zuulzuul00000000000000PNG  IHDRϣ iCCPICC ProfileHWXS[R -)7AtЫt !ˢkAł]"E`ʺX&t}{Ο3s'3;sP rEfJj (@Pds$QpwyY^_hryH,\ 'cຜ|q!7^/k!A XW3x&!.bd*-&,a5GGW(x3ľ AN:8fG"s9H0*r7;/ReD@%!Wn/ҰD}G 0@  iv;r_hF 8Crq xx+O?l) VzX6 !VM}T,Kd!~)S`0[>U(HSb)iu3򠊆J WeJ]* *TzT)+%EY@@9H@y@yj:QU:_uaK]ZT[j u2UJ]AC=KK}C,i,ZVM;O{DFWsP WSTUR]EB_}zzQk}*l'4nk h54c4s5kӼ\eZSV7".z6QJ;\;KLvv8$::t:Òadat0>2?7j٨z;Z-={KS/X/[o^C}\VtFk]:{A,-F 1XFYFkNӍ}ka3sM~0VAS+DӅLQ<2֚5O0m^c~BB`ޢ⽥e:VVVV5Vi~U7m666[llQ[W[m5;NhŮ} aј1E5] (u/ǚMzlد9;i9E8-tjpzlqtBs qRj8޸]'.qmt&v;nGrKy'=?zyz;{Vyw1aen3cU=fݬg6Y_8 x6 * j N (4R:+l!,2luppNxux{ĜHjd|QQ⨆ 脈 k&<Eŀ51cb bH;r8qi%$Lh(MlLROT>9(<3elʜ4RZRIM:dr)3\?5giӎғfǰ39\w-+=,| >apUVXֶ1{rsssOD٢<yv%^ őD2ER_ 9-RkOҮ"ߢʢӓ9C4ee32 ř8d]s옋͘8ly=C]@Y ]a DD\r{mK¥\m\[ẕr+?;+ZWܺJck&]\[i.Wض^sCԆWmIVe@m~Vփ m]ΎUU;;v>ݕ_w.ehO޸M Ak5'o;tkǑ#G=Dĉ9S:V^|zL񙁳gu7Nk>ͦM"/\r|K>N^|ǕnWk[\[Vk<Ƿw܍o߼z+V{Gbǝۓowy~7{E@xPPa#GUǡNS]A]-tx"ygSӊgϪ;??Ǥ?z^+S/_WKJ+vƁ؁Gr /aG͟?=y/ _#>g =R١ %Dq_#~&xr q>Q6 XuqiJd8+bQ ah! }l'" 1502 936 Z=iDOT(o'Z@IDATxu?oz&!k =l`;г{zE<("b $ͦn&Ydfwd3y}~g=lBCUE޽{t%:t"@ @ @۾}hWQQBM ^ @ @ @@R9m"ķۼysu޿{ ]E @ @2R{2=ڕWeJJJ2.C @ @4]`nӦM{iiMs% @ @#nƍ{YYY  @ @ @'P{4hPJ @ @#nÆ 3ul @ @ @S3  @ @ @֯__=] c @ @D:xOx2dHK+C @ @ 0l @ @ @\ *O @ @i6  @ @ @@\'@ @ @i4  @ @ @ W{ @ @ @4{M @ @ =WA  @ @ @@= & @ @U@𞫠 @ @ @ M@a @ @* xUPy @ @ & xOðI @ @r*< @ @Ha$@ @ @ sT @ @ 0l @ @ @\ *O @ @i6  @ @ @@\'@ @ @i4  @ @ @ W{ @ @ @4{M @ @ =WA  @ @ @@= & @ @U@𞫠 @ @ @ M@a @ @* xUPy @ @ & xOðI @ @r*< @ @Ha$@ @ @ sT @ @ 0l @ @ @\ *O @ @i6  @ @ @@\'@ @ @i4  @ @ @ W{ @ @ @4{M @ @ =WA  @ @ @@= & @ @U@𞫠 @ @ @ M@a @ @* xUPy @ @ & xOðI @ @r*< @ @Ha$@ @ @ sT @ @ 0l @ @ @\ *O @ @i6  @ @ @@\'@ @ @i4  @ @ @ W{ @ @ @4{M @ @ =WA  @ @ @@= & @ @U@𞫠 @ @ @ M@a @ @* xUPy @ @ & xOðI @ @r*< @ @Ha$@ @ @ sT @ @ 0l @ @ @\ *O @ @i6  @ @ @@\'@ @ @i4  @ @ @ W{T~YۚWeؾ7]{78qPݧkP-^9OL/tbG  @ @ @Fow,CMOqި^䡥ѥ)n!@ @ @@\PeGOmn9e|Yk& ^];s @ @ {>[Xdwy)KǺ4Ե["[5gNnegkxq)bT2! @ @U@`lWع5u ѭSf^cG|di˲$`o ^VǗ\[+=I FN=6f9Z{MS/ZZPsλ @ @Q@GUscdzx̑ eeR\_3ʾ<Ͱ~KŔc3ցocݑA};v=[;xOup|YdɘqY+x  @ @y v5_|`q|ѵq]'$Q-˺̬Oّ״ɥ '%K4ov [w,@&fN6s|׽"xOu#zŧ_:$ @ @ G{1kzS\g ?~e/歊Y{_s3}|$|.nٛf+O}K{CcĔVMcY2k!w%Y1 38 /vEgSv>Tu @ @@cƄZp7ωwDdfLLM3޹0nY)T3U—ߗѳ6{T___JZcZ# ,^zbLԫι\4_ɤx!Mkvf}xԒ3猊K&e)xh3;ѕ}/3a=;ǟmjt:"N @ @,,(wxoͺ~ԡ=΃T{-OCo֓3G-7SS5w>fS֙ם?"xY ScX2qY:cH|Qѣ[ך!0EPqiCGOm;^4:>x}g  @ @ \lUl,ӆFvE4 ڲ=XzYOfYS/'d{+O=*^6Smj@O'o<6*nx3[&  @ @ 4s+EbFWjIϽbbu/.R35㮝%[VŖʺK|Q֩âWזͨi5T_~a+zѮ\1âؽu>0'AZK˧fy߿GzoVykz]sGӎ_we;bOU ]d5wsVWDIqѷGmiVLHڷGASY=ճzON*7qj1,?qkw/-hy9/t- XQYXs{j{v.?,^1L~ޖ/?K*).2(fM[v;xKPw8|i}wGN(N85ccq粭ےgdŦ}P&7yT{UݑZiʀ_x @ @8y[efΫg[zb5ӻQcG\}ײxxzOݻm->K7S/L6,zab ww,xdm<_^װjrsqƈFgʽ㹫[]sXj#%#۷k\wXބ;&dF7/5-itu䁧[,MTs]{ ߼`t +m @ @E) xmɜq=,3SڳK̽1rׯ歊&K,ڰ+&3Sa?2(2mxtܱYY=;DN_^U|&XTd^~Ĉ>%闷h}M|o+0/Č;w笎>>Te5ipqI꽼G1XqWVoe/̼16}'>7NdzW0)NִYME @ @ڠ=O7ݿ/\$l|U'DTȁeF^uŠKJń}~t$3SYL7̚r{޽oe.ÒR'ם=*]zx\=нSIGէ9^_qQ3w`b%= ݿxךq&_ _/|o  @ @-{E?"Y.o:uN[?jCݗ}mȸxd|-?<;Ud/K3uSԡ3d5?ɚ_?y֨8H޻/~Vh{/+/XTFj$K' ]:N5R5񱻞9T=!uWWd~~j_ܯgx-;S> @ @@t+5wԘ24ڥVzt_VU ZqzL*⵷<so;)ռTwؚ-9+wiSvӇ&kݺ>7T ݒ+ǟn=uUd zj͚K2iJ)icd̬}!=ǻ5iI{$AWk ?Jf120˷WutO}SǼA_U)[]循qquuZS_R_7 @ @E{Z}ψ.-X߽9ݚqxe,Η&q$N/ύVoK?Tւq3Om5̰W=>}ΨdbYxoݵ'a_ݻv&1"F{1O[3/k '>yJspE?|hgaeUUq7-Y>Uӿ14>um @ @( {nc}ڰq/έ`o?||ockg:%NAcO?~xFDgkKLl9qSE>;l?,Vgg>"YɌ7M\;싟>:U釫O+~tlFg_*M{;^_>Y~769p X!mzk6~|EPk4_ptCk7~ĪZKuNfϽ;9 @ @mY@𞧻-xOUkjca{j&E%k7tOݓ|?:>uw/ߜ51L\.ߞqmjO=2ޒ,S/x/!2~<5'Yk)wė,N|[ Wmn~,ް+c8#t'>pfƱvv8_yE㊓GeC @ @XwԘ24ڥnOLܕswU-៼f|14:uXڭ⵷<g Qi|.53O1w\1w|a1ypm'sW7^;39yHa֤VC0 ޯdRmzn{>q!@ @8y? ob޻wW:<;wt<lܑ<wYܿdkCuO=22umi~=;ǝoK  @ @ޏXOnL.+ň}]#\1âؽ`I~N XV7˜ V}3=I!xO sVǧ^Rgx欉ug;0wK3N}c佋O]cwfYrG^ @ @5ifέg68>z΅q˂MvGW6:Yޫg'URPT_(Yff蕇efRK~ӓk,s7~կgj ^9kQlޑ>iPZRǙ#.4fo۳?qk#ۦ^( @ @y 917yjWLMZZoxq`k֓3Gjc: ,_zb4Wf[_,K6X+dX 5>rȸ#d\Ύ=OUӧ?tb }$?q -cm -z˔(u!@ @ p |]>gP|q^:uBlMwݥm;D\ѯKmO.9r1\-xr%޿Ȭ+w/5{3ܱ}a꠸a֤G#xOucZsq fd]w<%;]<'@ @ PVճW'޳M-93cbtIlwaAܵhsT5q{j =6v뜭r_VDENRRvzrޱCA玉]cîO?:5W Rk[gə=_Vl?6ٙQ.3[|I2}K7^?~|}FR;MSƧ^C_ooyZ)7Lu @ @E# xo[9g$KdIޓ>;sd\ubުYgg/qR7,:w}r^祱a۾l#ʓGdy+ e=1eQ=>~:,u͙bdw쌧7UƁ֋?I ۫u c!%suk^{d(m55ޖTnʸsqE2c=?Mh,xOճ;Y7euCR'=wt٣b> ^Tw?-Zߙ.`Of->rƐxI#O&@ @ Ptқ殨~`كݚ.\yʠɃcDC+3k^2~d izI5mN{MqjG݇~QqRM^ܳ7ڰ-[=.e lX ӣv(,+cK@W*lM oR+o-\Gʱ%jf/\.Uo[߿trWu0Y)~lE<<,v͖=u.Mzw3NZ @ @@wg/U];wH'w8kTYtjR25CYQ-gQ_sUI5o[wƘaz5>L 98oLٱ: &@ @ =g¦W0{OKbǮЯGxŘ>qn2z1ows$hzce|/K;z虏6GO%3;v2  @ @ @-]5|9$>vиhܠ,| @ @E6ď/ڤٛe/4$zu7 @ @h9Z|doYX\&9+3}`<4tL+> @ @ @Z@F?ʷlO*c|w[T':G3g8_t~{E @ @@1 ދ @ @\@^pr  @ @ @@1 ދ @ @\@^pr  @ @ @@1 ދ @ @\@^pr  @ @ @@1 ދ @ @\@^pr  @ @ @@1 ދ @ @\@^pr  @ @ @@1 ދ @ @\@^pr  @ @ @@1 ދ @ @\@^pr  @ @ @@1 ދ @ @\@^pr  @ @ @@1 ދ @ @\@^pr  @ @ @@1 ދ @ @\@^pr  @ @ @@1 ދ @ @\@^pr  @ @ @@1 ދ @ @\@^pr  @ @ @@1 ދ @ @\@^pr  @ @ @@1 ދ @ @\@^pr  @ @ @@1 ދ @ @\@^pr  @ @ @@1 ދ @ @\@^pr  @ @ @@1 ދ @ @\@^pr  @ @ @@1 ދ @ @\@^pr  @ @ @@1 ދ @ @\@^pr  @ @ @@1 ދ @ @\@^pr  @ @ @@1 ދ @ @\@^pr  @ @ @@1 ދ @ @\@^pr  @ @ @@1 ދ @ @\@^pr  @ @ @@1 ދ @ @\@^pr  @ @ @@1 ދ @ @\@^pr  @ @ @@1 ދ @ @\@^pr  @ @ @@1 ދ @ @\@^pr  @ @ @@1 ދ @ @\@^pr  @ @ @@1 ދ @ @\@^pr  @ @ @@1 ދ @ @\@^pr  @ @ @@1 ދ @ @\@^pr  @ @ @@1 ދ @ @\@^pr  @ @ @@1 ދ @ @\@^pr  @ @ @@1 ދ @ @\@^pr  @ @ @@1 ދ @ @\@^pr  @ @ @@1 ދ @ @\@^pr  @ @ @@1 ދ @ @\@^pr  @ @ @@1 ދ @ @\@^pr  @ @ @@1 ދ @ @\@^pr  @ @ @@1 ދ @ @\@^pr  @ @ @@1 tw,YTC @ @+0~ہcu{n=O!@ @ @ s&@ @ @ ; @ @ @ 7{n~J @ @ @ { @ @ =7?  @ @ @@= @ @M@𞛟 @ @ @ C@a @ @& xOi @ @ ! xC @ @r4 @ @gp!@ @ @ sS @ @d38 @ @ @)M @ @2v @ @ @@n&@ @ @ ; @ @ @ 7{n~J @ @ @ { @ @ =7?  @ @ @@= @ @M@𞛟 @ @ @ C@a @ @& xOi @ @ ! xC @ @r4 @ @gp!@ @ @ sS @ @d38 @ @ @)M @ @2v @ @ @@n&@ @ @ ; @ @ @ 7{n~J @ @ @ { @ @ =7?  @ @ @@= @ @M@𞛟 @ @-@IDAT @ C@a @ @& xOi @ @ ! xC @ @r4 @ @gp!@ @ @ sS @ @d38 @ @ @)M @ @2v @ @ @@n&@ @ @ ; @ @ @ 7{n~J @ @ @ { @ @ =7?  @ @ @@= @ @M@𞛟 @ @ @ C@a @ @& xOi @ @ ! xC @ @r4 @ @gp!@ @ @ sS @ @@[w{kQ*vƞbo`Uu($_x!Bzk.ڷ萼w!:wl:DN$yޥS9ztv>ݺT-yŃP::$ @ @ȟbݎ]aشsOl+]{PʡNGinQڣk(=Š%1Wܻ{5+Jݨ  @ @,3bˎX|VY&y?~ :$S_ٯWJz&3. x( @ @o;bY1~<ʒcK{ǘb\>~@iJA{0^|^M@^lwx @ @Hl|x|Fa{Z{A*?mDY6L֥V{ @ @H q-+Pֳ$=8=Ț*9q,ޏ @ @-xuW? ŕ(XpԃY383mk@\'@ @hs>.]k)6wwסR4'^p @ @ImK^'pjCcd-[\#4 @ @q)ekۏoW;.0,;x#cF& @ @@]]w/^aui#rx ## x?v @ @$^x!X"Z*DŽ@-4".>qTo׎. x?@ @ @*$pcضGT>ݺ$ȸ$ M6 @ @@M3+b=MEǷ@inɣd/GC@~4ԵI @ $GWl?<4Vlrh@׎'Y'lnQhT.$@ @h g7l$sZzu*0a@xS0_E@𞋞 @ @-8xPɥq-CA \|xEsJy)l @ @J`Ϊ˹Kb݅jR;(Yo;y|2k {s\K @ 9VTZC LN~o Nt @ pZ9Ef{Ry8ihi@=z`&@ @ȟ\|zy*TVx͔[Xq @ @ڀʭ;Ǟʷ L'<ٷg  @ @/Y/^xuP+k.uڤ Y݋@SMr @ @@~˜dMw!.0sx'n_ @ @bX2~3tb1W;{ާGq;j k'@ @h+6t?xE"p,t>֔8cԠcxG 6[so9%Ⱦ}׿)xtuh@Uܸ'?#olj"N @Z(o-kai{o>6acz\{AZ#񃙽kTN{-8f*~;zrN>*]~!E кOf?l]6vmPܱC⟒^j ku*ʹƎFǼN[^+}ۑ]?"9YDsDںhv{cÞASf;v(1&PkGz={cÊGHlWzw~:mLYT`RYSǩaggSӱE7#N|/Rԩ3b3SϏN'$Mky/xM)J.e{樮% 9|2G@[U(8ouAlLfoy88gj80{D+/;b voF|zh,?ʼnoa-bTb@Q @ Y1+ xj\u\tJ/I~ʈbٰaζn^ӵq7ŕ玨9Єw{K)#;~[<% x?ױ[/; @ w?0@H{ƉÊttT{SZp]򫮊ͨk׮ vm ];{U4mbaΘbNa0cN@~ݲc~_ӷO  @@?<~5wI@ q>Lk@@NeVNJݿi\Lo]ք^}7>sObyE/}1o˟&@ 㬕/j$ @Mwŭo6^-xc[\^c[@ފNcU y{\?t˒x '@@ۡ3 @ W @k \|ȸ[ uQ{+ޘl$xrUз_[{{SmW5 . x/8  @ǕϞXw/Zu\` SFNg:xZ7x}uq'nŌ=?vnƱ{=c= @- gV.cB ]pLU'# xϏcZ G,7d1' 3 @7P  @mT@Foncֵ5PaU}*F~NE\sr} 3 @7P  @mP2mRQXv(nc!xoS.*T{;JGj}o^S  v7cdž%xoY @ xj\M\cz{+޷NOޯ%v } n]O{&$?'.?8‹ήs\lޭ;wxɽNe_3~We|FcxTϴ$j65cfώ{I}/bTm.vM:'OR @m n޲uI#P660}l' #U1Q+{iT +W=7~+q ym]}qzʹߎ^\{7ƎW:~xݵ7{9p-h|{~]|4x]!os[^W+{ŵiSToݕwz'~X%YOL,_+^55#zm88wM=;G.klvSH ,5f_w.#l˜_7\Pn>mi.N;>+*[ɸ?CN|>9ϪFU˯:ESo2U9ު:ctNn|sද}"NMMqkXg|\ܿOk{s4|?kkT{(e?eeNJ^#ZsiE폾9a^ukf6ghmh({S\xʻǔL{ J_}djFnvq؅o,ko"557v5[ՑmGL[K*xuɿK ?[E5̻_Xxfߋ7&L~ƯsL6t>ÙGZG%kǏ.lJg\ࣸ{-$$!oL"m"FQ|0Ő~~*w&ƏݺBoZx[YQsn~#,wDxdy4ޅHld8/wM= ߍo|w~cMeZSU+bnx HՆ 5&`8_YK)KeL`HdcΤ! &/}3|ۓye +_VFlJ٤ K(T)-[ο[W݉Ҕ{pf]%BƵ̔Ofŏ66wSY(R۱R,]?s(~V/V'`<⇾8p1^cL 0&=_?:O30x9ͅ &2W~7J@PbŌDg}+f6tu!Xu*uÌ!Eab#ƲX;.2#xE팥j\:<7w:仞v>ATR1՚$gim:Ԩ fSeZCFcrfG{e03 gOpDPKTÁcf#DsB6W*s^g`5w}g0S'27(WO' rA3(@c&W {)vy 0&TTyygf`#02\n"df^uŊ_"o}bV&m/#}Ѐez==X~oph錡/7<܎\tJVbNV/L(pI^bᔁsi6OKct;RuPOC` H`Gn:\ˆ u^`L Ekw3UIfxxXJbAw)+)HQN(Uk_8"Pv6;=:jJ?\""ىv6ܗr6}i%@<+>}GoTpSq ~cMeZ:X,#tcєGOk@nʻ`L h:Ux`"~aLLKjhXx7+#Z W$ ڪq]XI-.k"녡:+D9\d`2eoyNPˆZYG;;=XhJ|q8p˼}&{2{3R}BzBLH{ZSSDeQomUmX6/P=zhVv]%>Lϣ&z^W7o[WUqE Ed˲T-@ ݔmP;_CJ7K|0R i;wqo;Ru ʾɟ`L x"9\ܟ: ϵPD7XxǪ|YScIlmAM (nsXw)gkؘd/nDl\BhK ꋳA"yLYzaG@v;]gqhmpaP@ }6mxj+C/ek3 T@@WOk@zdnpRR۱Z,ԫpXylu 9+աr\}q.`L 0~N.|eLrrM7i8 >4#G% ub ^AތfG1̞2vU[BY> 6wJ1HhR7»j2V_'Uu%SO?YOQdݼFkHSڗ m.K~[t?|S!f3#Q:'] ?[O 1@vfV{7VꏢfQǿ3Za{ n⋧$/Tט`L Nwo?0&`i+>ǧOt j&mQ~6`YAvǖo-;;VL?v*׳WDu1MVxOg52lkd ]˨" ~U@HY~jce$bn&fMe,ouimv4I~[""joa6~%Vmg*&bͩjiG{^3j5&afQ`X[xW[i/Ŭ| `L 0\o vogLBBB_x,)''»/u_?-41׋wK.T{0»*x: Q}޵ eǶ`Z.Vm} Ȣyn=W~6J@=dɲA%2k;nlD@B]> u8zDT1kP•Ik@MY}mqNs4kOjvTPjUYw_uKa:VmI*`| 3W5pUm؟58hizxZ7 ra37FTu8cX]xn}q.ʛ`L`8_YKF0:6,ƜI#AZKMq/F 은%waE 0&.7eלݢBBBF/0Dx*>ӻJ/{z ~W vz /.C/^F&'gNA#APkMI/F 쭀U$ѴiձX"7^U}yz@[CixQVقe8blKz.̨W%Ϟ2&׺_1SPc.=4dCeV!i$g8*!r\aR_iW3~b͙H_ifm#ۮ 2̸uzvK 5E#}Vw 3&N "q=2,ɱHAc0*: Q/ }g ѽD{'QjG͎;4qX"Q~%Ǝ,\sM9/+EbS2Zg++y˭Dd^:MYdۖԟہy2q$k#gتvdO%ʊxT4ϰv>j_h}{<)r&5T(ꮊǠ>m۱Z,.詟>&Y]\iYڊt`$1=.2ёHDJ\ Ɛ- Q G}vfK{WZ;hn@}uֆ{V4u"s߃Q$l+K2 Tb»l*]l*m'iKceUY,PUzbMgRS~{^\,Ka^ձ3!VOȏ2,Qz/jGXK;B=MDX"q'2 5%ޭڋi9I\=ՠpG9etɤUEM+2S\~!$J^ՌaUh'ګThVEr`NO,.U&0_6 `L 0:P."}LNNI|P63@݌鳜!z!QfTZ]$‹3Y|w9| =O*# Țo5?hS0&&֎-|S$W{!hpOBFbKI$ǎ^vq $+D^_x! .ܫm^q[wJ3}>#M@Ǥvft\~,g7/Wc4iܹ(8PB~YC7V-tLV+ "WcJ靄ֳ$:)h <cn@nS0~'R Ƭz /sh֟I@Nz&kV9iB׍zGb٫ Tƪt~5EM_bkd쥔-xUV~qFvՂyv珕cd7RBfLTA .*>BQTה:Rz>tߋc%A^6>=#oeG_eL 0L'͑u q 1ĸL(e ^ibT19ޗٮX~O_dƋWK;.Yψoעhh3OjٖaN`&4p.Uuavˬu|4&ur5`On:֟rF5S, <$uT􅢅8Z  $fvڹS !6 {$vfWGQis{2*٠'d6SnWKόW`L (\ۀm'Ul'@BzeR{I3RAYZF.,c"؅.Htv~ %L3MډvE`OcUi זwo` z^ێ_"= m[ v + "}~7.w ۔a!iY8Tb^7"Đ< Q4DQy 3Ƞc/?HʁHkY8\fi7ڔ.>Tܬ*QT٬:i;VE<'b̩eȿ{"1h+?n -GC'vfez?' `L ,/JjGVBX%I}lB,* 4jLM&FD;_e)m$ qΈ2~ Uף2x92R#) & k|/ΐYy>3oapWyVJ{M)^~nֿ*Gs=­%Bq3s+g3NFJu9K\:DgO>O>!E=1n5V*ĖObA]S^Z~Y(i=h&SVՓ^J)؅K{R|њ3WmyQVզ'ۋl, > b,^It7T*<,Ob-8O QZɝgؾ,u[%~s`'׻Y\M|)Q\Ӭ:i;VE#Uq(z ,zJ|pY4 ?w`&e(;zI)٩dYwg:Y`Î;gv${p}ΤtLLJ@m;w_,|I|$owa;#,g߸ -IXEf?!XxK7][du=/~"7Sj|OXxD}/BO`(ہ3{ݫbycS̉CuWJ7l}@QɯylU}%^Y ǜS1:F57 8[q̍v$^.ʥzE=Yg3D}Ry-,1N u(*M=_Kg5X!]QBE{__> Ojm/w^=|vE@"ʪ4E楊6Sl1>(hG7QF[y&eæ5OaىBz:{4ߦ]9 ({ߎzT&a竸jœ]rʪA7+ctH6+,,dH|jqrsj݃xy08z,JV"ꨧX)#K ӱ?iŦZ\!N{FyRm~_/n!w|g&FҤf1 ~du[t'ijA1 WjQց6GiO;h0@XtJ~/ &V~>{/b՘3XבN/Э%BkLd@e#?Fzxe|Ew^][[%uSײM1˸4د9\@Y-d^ZDYUjfUs+hͅB7vP.v%:p~1{2:v"s)WDi3^=v~OK\IRh;MfaP=`W5EfQO۱R,z^XΖFJxb"ZWLxoTchcL 0%p7ʻoi31 @IDAT4ܗ: )&32ݕEyy7qf.4*eדnޛiJ";eGE .*RwI.N]Ӻ] 1^"njؔlFg>~lH»t?D*BVVew%dwifuWv} |頋5mY?m i/F# ^[$9p,oZײ|]T?SF Y ҆PSTI1G {ɋ:ΤSV\jzڑ6D~*ܸ +wW=s; 5g{rA45a jgE== *3g"ߞJOuj ?1"h!pX +񔷳zDW_,b6#=t٢7w:aF``Lz Y/4iX LIU(\όLw%ng{ ۍm׈w+QB6/Ef{, *ME& 2QC6;!N:TNFn7J15w fp,'5.?Tubۥkb8)y9M&%OVQqu]xq{GV}?Ó׳,RP>)A|LE!/cCYFW}s[1]8=?yڃڃeКlދo+;E^U_ډr .X/=s+?4G;QJ.WOm6_[ gP;Ǚ˶QS@vUϬ*>5=mKW|7bK?|V*lX _^$󂷗byl=F,k@G;co}V-s=?tc>]_?)'sظpso+,sy35&`}~]v G.\d$MISa\b$P s.t9~g*(Yy#P*$| M@qOSGネgL4 ̜Ӑ葔5"?Ւ/kʿ_Zʫ=o/ KeGMU3#:@41&! ܿ9=|5{#)$BScFct\$Yvu7*뽕;I9)u1ygMLz"{CC$p͕v\nw8`kZܢ| dX \?~3V0 QL 0s h KgL 0vB.\ L3h> 0&|xU|)b,1$g)K+\,ޅJ7w(f$(<}402ݧQ{6y٧ǐh# BX2E#0$q^,"#_dPukf{x[_5 ޞDw@r1L 0_ +A> 0&HvYe2+m'Q\n k3&0#%kaV௎IHOb'K{bL4e kqo4I/S="e>=i #deN ε]rKһ( N -}8 ^ ^`DQ\,Ƨ0& »TL&˱!v&v.5_.̫L 0&0, 8tòn^Ixb_=»S|wfӀ4h! x1|oHY\v,kgG2&L%»xp& ʢpS߶,.9YzdrNly_͜(Bxn{ u"2u"GIx7Dv7R{eoE|vՑwԦgԑW֘K3&Xxu&lRux_`$3Gs{QkZtfl`L@]Kj=9~"ާ$M0C{SUM!Yaذ)]xgO !\҆+wk%J}3:[,Sұ~AEk»Eo #Ȼ\c&K~[6% "._z|i~&6f[`L xIvc {-/M)OJGILKFdD8X|# 7(۽^i $7@x:Mg~-&Wm64;Xx7ȋAޣo/{,[`~'»ߑ`ò83,8+f(&'_}p3XSxGi1ԓwL&Tdx'8xOxϞ4鉞!wl(qGtw<}rgz 0;ὶY63PW;~j ž6 +M`L8k^;&|z8ܗDD4L vԊI_x ظ(LM,`LtUCV|ػzXx7$FGbJbXxWL 0P @ӡ0& @2&g^9^=xX`ݶokK38}X 0&t8s @Ǚ| H{iqd5qɉH/IukQKm1*1>!}FFRo5KdӈVix!¦'V=nGG?5?x| &`L 0"գ&nW5}DD,0vI|߃R,`L nU#5X!J> RcS$ { w.ߐ,\ZA6MGޓÑ)G NrlOk~u\mbOь?1.[8{T85e=L 0&`L 0&Pjj_J.V+w!&,)1XfMqI ]]ZS7IоV׌nfLr!Sid3qOV:c}wPN\8\čF}"NA8B;8/K"#ј_,'D»8U`L 0&`L 0&ђ MA 'L!=2:s)I VBC[|^G b-hwwIB휉iRFhDa.ln=!* G$`6ԃx:;kpԌo߾E` 0&`L 0&`&7Q0Z YٓiUaBFGwՆ6\#rM.{][OvՏR1lf>3m4jD vz^ZU-R!~zq QcéJ -`L 0&`L 0&0 |x/q~$V=x,w2[DrtI>ɘF;yk9[nnqtuNC3>YQ!MXJ.)ĆJ:ФI5+֊ZWlhh郎8Eť-#Y1 n[. 0&`L 0&`^xG8yW g7M{9ˌ TBq?: "C0Rz|&`L 0&`L M`kd@>ZdYpfKutzOhz$Ty3]=h&F)ӽ6Ya"ٷx)0$ G9R;CvS7V'Na#wIΈ`&ͤe3&`L 0&`L@#nÎbGa# wNG!=>F|`B:G% <,&5vUa/#&TUۀbzm#w rn9j8YDSSF'`qd1p=ESvUyr%7;4=4(rm!Hژ; O h/LރqL 0&`L 0&0$wPt0ݩ=4ɪ(=4v1阐h)^dw:(ӽ5Mͨ׀PM]d/!YxN <>2c: M8thE}Y `/VT5B/ssio>3S3'` A!n&`L 0&`L M[-f_қ..;Cq1HMޅK{\{wqSnk$ uBxonA -Y&OJN&ܜD^&Dq'/bei'?Fv[sWn}uX ;@`8G6aXx7 %`L 0&`L 0 T5`/D>`MQNڍn|,^ YdGAj{%2"Lvݻa${.IB{xN)c;$ZzܥTʼnd A>)TzbvEwQP*Y\ѠTGȋav|Q߰ `L 0&`L 0&@:s#KJO|']&]KMb|8$"tF NziTI$BdlZ65EP:.^$QX|,m&R{2".K,]T*YԴv$7`@/n ,1|nFXxώdL 0&`L 0&~?fp&З1.YϐM :6Z^LjcuNqQxdžK;M*D{#<ݝq_mmv\'kU`D.^ޅ΢W߿|03q XxNw`L 0&`L nUX.2Gx%FG!!:mOk]LfMLj PzIRN;/PKn./2 ]A]B'mb[Dْ} Ip')]d4e_[K^AOq zU2|'q)پ%`0&`L 0&`L9;wEj [Lxk*%JdK|dD.Hz ]KBx<) wJVX'aڝI1A ynVDt.QݖvRF}YH{` 0: ru`L 0&`L 0 pw#XrHҤtS#ѱbRS.2{{՝._I/_|8HYeJ MzDw!UÝtR}"_ !O4%RB=]4<`L 0&`L 0&DGqt5$tL,eKHdY{2MdDdB&[q.Υsq!;^q{.OdS{ wހuMhBXD$ %^X6cXd_v^a{kbL 0&`L 0& ۛxm+ȱy &<&mxKCƨx$C1_Hqřq.>佂ywgCJ5M6)ӽ[${i2Uё|E!с ~'qDz~zAއz1&`L 0&`&7Qj98h,v.&FE"{egXɷ9qH|Z!ja&tmĽf*)]X\mDSD4,,0=!?"GtY,ϕgL 0&`L 0&A@\.d&>1)M#&FJf*1;3ń0SܝijmíFqwm"!eLw}hu=uW> ;C. 0&`L 0&`^(]O^>80$.&VdLzB @$'жnR׻)|·ot]tVS ` !洗a݉.(7/q)AՃdwcL 0&`L 0&*ҰװGPvNfђ>*ǝlgz}CĴ^tW洓sLޤl[hQa=x{gM_||jVBn0&`L 0&`#޷JXw[dGebDHOX! UUqtuN&Qoi&\"ὺEr賈B׽o"Ut;'>v8y'X5nE. 0&`L 0&`^본^|_ .eĩe؈pʀN~lt^U߈4jMs iU|!&}~<<:/|aXx7`L 0&`L 7^}]f 얀 5eh)~_(dKt-V>n "s_FY-IS44ބ[j"ѽGd.2C !- -\P*n;q0&`L 0&`#|[#AWIR"#0&PRi=Q$.;"&N+{-˴t8JSrY4H!wLwD5=*#+aYxM`L 0&`L 0CѩFN·=2,ccI{fjR{"ew܇l"Ab::]4q"˽ wQhܛ;Ez7 뒗rbx9v{s=<Ǹ[5`]3*> 0&`L 0&`u5p -<݅.2?A=SB)[d E#ѽl-L KV;l$;iBU:Fp՗v.vrNA^ 37,Ƨ0&`L 0&`L@/W}o=3֩44p?- C8+Zno&^ JFd;EiP v2;X̽(pL 0&`L 0&u (N@#9dpH5IG!>ܛ><5z\GEu-IiRG ѝ>ѝ'OVfxy2RM :)L 0&`L 0&K໿| ZE@Ӓ 1-mƧa MI*Še8/NҝZwmmB`:Ż!x^=qq4,MgL 0&`L 0&#'%aWWD@̌'/OMCBr\ ÇuuGv;囨lh45?]rwİ9tᰯdl\>`L 0&`L 0&iabԸ0GΛI 4w{awPf9Es\׈nt|ݝ" G $<'»~v|&`L 0&`L 0&"p w:6IIRq4j ezig"{PY$e_׀Gǻ.k_|SiPXxώdL 0&`L 0&Wt?:6@z06>f~v;*qgMzuwQlDM,i/Q4]*4P|.S{Mk;Z:=$>9!mcL 0&`L 0& tN a&C8N ܗ a;#qDwQv[Z`,]##1&nw,B|kiCzPQیH*7(q3iA_࿇#)N@T{;=ehi58qנۏHr?LjZ_z?}69|eQ.Z:PzxY+X4#YGFҪS*8`L c,wyU =12d?۽w#Dw.&-F@]ap'QT$Œ_uS *j[Pmk9UE:4B6@u&սej[pգ ]])cF#{xMJGZLy 7Z"pXx}w0OΜ}au= 5M4 5EN_sPZ,bsJT`݆ XoIyΣp,-1Ė)xѹ? W Jb,Wbukgdk[%ˑ.ESѳloZ*3l1YkT9FR,Fԇ`L 0&?` wx)کFc$=Dh߬_f/mzrk;5Qۈ{$ Q?&n66OĔ$!wu$ \!So*MEf3=3lBzJ<.2k#lV?,BOvx?ؒ%ā[fc.aә 5PM6>*EuNaXG6DNn9\䚁\_^A0, 5=i{7-JF=k*GMδR,QyfDz|,`L 0&` Ī4*/ bHGfHGG!2<̧`pE&ݬ5Kv-ME!vI6c`TGaa)ofu4j M -e ]x#4h+;~¦Gkį+^A`M4*/ 𮟝L;,2g-v.;a|Yf\~% 4+o<3mJՕa}i}h`bD 'Yd._8xn[DўXx2-zU5PaXibDr> 0&0Gyurin!ߗ4$iBPp{&(muK*oeuCY$Kzw&Mƴ1),{_Mx>ܢL}ut E8ea"S͈f4i3:Hx"^ ^|׽ll61 }^x9#e<)KJچ%!c`ĺcf#@$\ toI+W^c?ބM lS!pb͛iGU]80)CmbTўXxk2-Qk>ҮhԳ֨roXՇ>n|`L 0@본^0FI1a!F3bRIɉx,M${$Xߪ廵{`Dw切NuOO7z(#~\b,>9a D7Bv{jkwl41l=RV~DKB|=At(N4uŎ{dS܆F{{0¼ђ&N _g؂%( SE[PfMH-;*H&y5!YN/JphEV_}_8ƍ.1әO΁߻Ru= ⁀mA)֒ǻzCzUUR,cDwe> 0&w $1՗"\=HxCJt4ꧦ}Gޔ}/\+5 hhD;9: OtM{2]{Qtdd`t|vO;"s],"=.wzA !K{#eokw!׵*w N &Qc4K`_) p\w1v]%/%MX՟=CkP| QqZqx'ŧ q4 x(7DU*Dy?CyoC"ܤsbΗQTւSki0#:s|f '5x?Ukɉ_gx b1/o>Q^k7p^oYKcO~ _ɛI887~:Pv -yS_LߥG{M9^wާJ B?\ t*N=$}|K=zSzkblw* 6jh1vo[ND<'Wxo|^WٞkR}ף"ݪ z_Emy:{O}w22He/ b<ةO{z ob߳H+ڐmAQeo ~zx_=5#2LO fʏ(*zJPG]yשiBF2:߯{-M#7+&6QP%wac`,*pC$:q{6I/wJǶc'z? hJ>d5&`Ld>z ^Gb!퉣bƥ"-!Nx2l-[ תp0݅͌4(Yąb\|=iĮ[z5w),Q]{ϵ* ZOEfԅ/y10!2`sIʄV4,@Zp596 /=OH,pcΚ~U agހ(m/?ؙ+5{ڍ@ѝXd~4ڪ'\sM?l-6u'ڰ';\oEƹE9y6isvGZħE)X;]LgЊ(úw u(/+;.æW*)տ ' Ԣ'w!bzWV͗0?c1V=k^W6ikplw6=ʣ06Ĩ%zSYɬzw؃.^lf dߤǜu{q]3G֔w`7OYqxFrvv,諾ҳ|qc@Ś8H"6Y/T%l\aj#e>)*hgۚ!mCbj`eтmMx:س֨rg Y`߻o5gG%,tӥ}\?{)f P'F!n? G"^:m(2Kb{_bxB }Exe8mi6pfƦ^#n7JO6iI\}^kQP)nNGW>/pm:S.4|9_bW?N\Oz㦨,dp^h5nǧĖ?--2,/PաX '?zt[E~}+908*|fj6:.;V͇ `!,2j~ Jo3033kL$ {Mr|Jm0-=-acRթ{y, =6߻s >wD$T:*YYgjByuRiAw>1i5'}1+z52TmN|m4$؛[Qjl6NRGdr^"=/{r8?k BHʩxn9}Jo0FϭQ_e/֔}PCضӹ-Z'^XeY]ؾ<FC&J6Y|];صt6[No99UHb[;vw`t]QIUIӟE ;"UؐTe B@! `7~춤~=߳(x%$k1}ڜmZ~Xb/nwpmxeQ:<쩶o4`P !lloh*-q< k |6f37;AmWZ[ȃ d )[ %o_`}!W9/@ݟ[4&}iEK6Z=-}DOȘ߆Y3AcY3qG 1_ˎ=_͙Z~TVcqxѲCEpw?'/'`|;ml¢;O [iu?W-گ|oJpKo݌u&"$'8in~.8UoAVI<Ϗ> 778=՚( B@!  ({A]# *ҼxD-i*&(<"~kqPBDd1c[E+{VJ"c,Z EiJOnj|\k5&`U$˭AGxkt ؜ .o(BDRY7m޷bԞ57V,}R#Y"`ՄeN[{[&1L.G(mDvyڢ&,ajg-{1jVQ`cf~mKfD‚]"͟kLLM#"1,. ݯbmuy1cUӘDlμM2v);h7L܃i[Ks}o>bxx{it;LuV)h~'/'`OH)-n&L'4^ ַ4M{Z\\Q0a}*j7C{Nn{͓d.0]kQa UB@! & »@è#Q55YxI?fA5⣋WqqSB7ۮ^i q?*E{J>e;ݦ*݋.ͿCpm܂="{l,m  k.FZ=0{W-b oE,6 .zsgAES_,ud  wkdo(QVkǻ~=Kޖ~giۮZ;%[hoRR ~|O8hX9ٻ(xݘ6u rGR{3XqU]1 Eo5}S$L6Pꦗpp5b4F{7o 8Ѷk,Y ~鋸hNm`A8|6vB,:u >]ziƉˉ:0O.7N3{cZm x?izvY&'Mx.8T#K6X2NR*7pԮ3Xͪ85}z"+B@! @@f,Ħ}W/Y¼,< )6 SB~Xi7f4˛3&crNV,D3[|O.gsכPN. =}Ͽ32Iv"nlg'Ysњ&gMc=GS obf@Ҽp1Uj"4ZT| 0`"ׂD:Fq\ɟ,nx( |4}ڀُ豖%KV/z%l&uX]p 8Jy5V=/>d+;aޅ {*-JPb (6 [,(˹m*>yzkO{ko8q9Q_ z(o:xïj|v'6fv 'bk:9T_hX, v3kP/Sי꣩gRxB@!& U8ӄw=mL:4{&+Q1ZӅ_oz2J;.>#$ZMscjN~e2jfOQs{isA%Ump+[o)䪱=v`̉,VDߋG?4h`skq(X][TP_ ))HЇ#x\ij}ij| ep!_am9SOGXq?=۩G5Tb${@#{uص|[1I:lS"k3dtmRRJc\g˹;qXTj=?󽱠1~m;W Li<ƺr ƃrX/`"ZH֡ N_Nԡx'0.*.< fGZNaA c=X=13ҩi^k<΄ wwcFlebi=ͩLE! e< {n 1XrT˦ӣxW&TjFeO >L͂8%XU7q{~q -|F'7rH>|}(Πŷv&MAZsyL\ѻ-\P* 4n+XrUcPn`JpIYiNxI&͝Oe1|ij"6fnߝ_wD8O} |g71cx/ނc?;,[HM}S=ڟP}6yҪ|$xQ~IP]/Uq'q{NMmGx^`Pc0|^bzx ODRY,¾(B/'P# {'5*9\&awq3 NbnŢdz ,{+08}S! %{Zt-љa ]xWKfOEl:¼ÅfL z #@»"i7π<3)OˤiJ09#T3z5bc+i9y.[@ZR"vCCۄCqn#WL9iKSiKߺ:IJGFAhdrr-Gt(坉1!aogVX=ލ⬥?Ou0Im0+z"l9]7Eٮ3x},oLG#c&`Gύxk{r/:v,k:eC}pxtOVLxC~.۰bt:Ik- )'/'`M1>,cMk/3zo{<ĵxX-/Wy%szT_̃pa1qX|;Z7Qub}y! B 𭷎x[;â>=Ī#N++hZD*+> Y)(M&fQ| RYδwu㺖 b՚v΄]-&Y҅11 >W@(æ}(h9sZd,2VCOE]IO^GkCO*RzVV͋^6ZG^[A^8~ΟQth~za=GVc÷4gc/>  `>!6o'Mc "~sa ,7D:1`'KtuӘc9clzK/M]zo˨lh52< gN=ot*_U :DxSRw"Q-/nNY9wF`5^XnKO8#KK&AEi}Yq."):ՂE6&VQ4'ËI1 b.>!þ6~ KM MTo꽌_:ݿRG==Pb_R }A.^+lc{V-l¶'FnXz z^,InИe)K>ܾ*.({ ]WE'éw@fEsTwn9EaQUahuXPubyׯ$L;6Z|W-L;+=ĉž Nnf݌{™zL_C}7۔ 68U;{'ȳYB@! n~z ((2tt}J(,, i)IHSc5MPV3uXpؚN$qL `Rv?uf_{.k^YNTCmԢCq2#W8a!(Z.?`؍{Ƽ庀 }/6ZhUŜҍizѽQ,_Vz]0\۟t܇,~lZxzz ~^u!Cąqz`.9! V8gn<>6}:JLCGfxׂqi91hc>l:Vىˉ:L$lChڽ [J\ug/;M#7 ;`#US3MIV+n4^z W,&!~nB˷O"Ź )pEz*-.|ux㰹3T^}V\Kx'6=*D":uEԎHEbOeEdgXL m`lD@]h@zT[Jgᛛ-e5N@}hzaS駑u v[ڝ ԜRQ+MyY^Ƿ)oS=-[\;!64 +wŽeE?:/fdQ1Ga';E:es rOބO_ k1B9d@ז؁8|Q}-:bq7|^>7e_N_a37āΓ@s{9|r!zTDkQF{±zXEgd=&wۘ;X;'Uͅcg3fʩvN^7! B`0|_du`T-u! *1޳h6t5-" t#~N`'>;-_Ljy,G_zܽ%1^#bAC,_O ~'.1^;0Fj[l|c?˶͋R࡞-:hkֳF쪜yaśP8̑j/kۻӸlP!;k%~Vi2&B@! Px9sr(6<{/RDS)1!+).Wbq8[:]pXVG8f=Z[TU?I:&SK~% ؕ~8F ntQh`vJxW={힜)4d$sO8lǨWM8}՛6҉ c/.@ܙQB"R 556ڒ|x|_Yleb8L;v5֠,yygpp@g'=xyqpz-mQ];AuɪS 5 B]w{z;ٌ2JrEcbu^>9n]+r-v1簮ܛwkdBvUZָSC2u5H+v5ϥ W=QUY F]f'xq639}>&.1I'qtysղ! 7pk]S MQ.wO鴜> IIa hlyΟuMv4YjDx@^LP^FxPpoB+yQlJ"ۦw Zͨ?5hN9в' 3F5!owZހ7POcC&gE>PJ@hdB@! &z Ia-AB@! ʫ !*] ƌ.WꙌz1&0Hwe26)*\E1]-M?.eB1*p[p'.ZQxfF'<)|{è;pB Sdi⻧OŜtENQpA?*ylTww/Uv5M,d( aSS0s̟YIٙ<و{b}S;'98Wl{|k#oRJCmȦp .)9N! \XcKm-! B`X#2<)r'0\;Mٻm)9>.YiHvt6Z\onEՍ&JsX~ w~QlN) I|hڸ$G}06_mlƥ(焂2D ݽIB(}jjwE+}/CaZn׻<1+ᝄ~k&V VeѳB@! p$uyZ4_¾|ǔ %Y! "pGkзhȭ~aUyZBFb<-^*E%MH1]I*]ٯ(AZ%TځFwsg\_TC^MDZoQ'b鱖&.2덨ޤr<#+z*o< wMLF'abŢG+]F%G7 t?:H˟"5ޣF'B@HH&peL}BeB@!0\ 4Q7 qDneOHd̨DGM .&u1 ߽τ#x&P*I~ShW'djK襮YH2o" uN?N  ~Z=?o(dM|&γo?%\इ' LTk ,79RC@wB@![xμ,fIHʡB@! /{RXx _ 7iNQ#/AE|kJxWQYG%Jkm^8߻2CdY(\&%\5M8y1qis;z)5~MpB5Z` ZZk3*9lJbV4y7~E -"G 1Unז-).»\B@! o^eU&m >G{1*:P! B`xW[ڇ79z%{czFy3] G Jtg7=eM]`^q>Hw ??jUEkBvا@'e)1ʇ~dRt^ F+/~&U ^dLpWyș a&#U(&=T{9#"*n0]κF|O`|z_@% »-(B@! B@!'p~p*Z#`䭾3ܕ(3DxO4@tx1eJtcb(Nea ^9:C(???wh[6>24! B@! B# Fr PU_്?J`,!uOE`*#??{fᒬ&Ewv<#Dez-t(S)0#7 LZFhޣl3'taXːB@! B@Gg*Ĺi98; J1W>#SITgf`S0&º]=n43|5.]Y1]M.Dh=ϧ)y>V%F̓^Oe5yL'rWfDUV g!kB@! B@! t'._zۑ>($!;%oL;]]Ҧ Wjt yȡ{ZV3fxVG%aYwcrNZw'iJ]B@! B@! UFpUXp=\c3G&b{!#gٷpsKs UupZ#)}cYt}BF>Yd"'F/.F竈EZHrXPe%6"OJ ! B@! B bЫ%󌸨BJt K\NJE3= W-5NEe¨(#ַWH"o%`3օI$_03\5#u$RG͢zz4g/i^"GC22 R&"p! B@! B@ > ?=K!O:S0Ld3*\ =} JDadvb`4t#'gJh 0mL&F$i}1VFޯuAw̘c&ї=E%`k xIDG22SGㅯ̏3HCօB@! B@!0x\:-Iat yLZ@qfLքx%kiR܆+ R-ec^XGR|ғ4cx%ǺT7sULj@P 1c8q#[hsz#gت& XDӗXiEq莏YwǑJB@! B@! M"dydPLdH 'cix2EY̨h.(~%W7F{'(m_Eū%^ lSEϠs1PD j>׋ هV<}#7"h~FOIf`fnIt-MxB ZH)F|+>"PȊB@! B@! |4&'@UZ䦥PlOǧ0(L5q_\*]Ee+%׵vmmD+֔&yR@Vim&U__^[Y)e3Qx)O'/5ٚ/>Eq~(k!{T$+~\>%v"PjB@! B@! @DZ){'1gEܑER`xvGqZGUUR-)WSxҎk:AQ^ ݌~WwU6"-?*9)~\SWQFk?JȮ9 M!Yomr~ cƘ ,FSSn,#e4ڵd  #z$дH}]ih+Ptp1![Mp^̘d1 mpC];{_ɏ_>(UPL$A! B@! O`/a@#{&ao4^]iºgQIVŌnV*On5>I]UdW3G'k`QźIURTOB69&c$㼂5:w[npWk=|1|x)1!Dx5! B@! B@ rF>_!/ԟMxzhғq9%*͗sݮ5۔٭|{5!^KLb|=B"qoZis~P^m֡w.*{9h)>g8q#O] WZDA,eg8 :$C}H0K#B@! B@! $؞kBl2h`̟1wGTҴ5ŰhxIj&-we17 Sh7:r:1B)J D@D7!Ϭ}#p1r-hcr8)[qO.=9N3Ǫf=Gkzҷl4[CXw~j|Y<7j*B@! B@$S7OU eþ-%2=fhJ<]|߾~=HozM14q8ϝ90.c4Rco38_׌.zw ^LK 6g:MW6GC@hI! B@! B@8@?؁p {VJf`1[`ݍv__^v3hwB8)s/M`d'? bYT~/}x6gg.}WB}"ʥ̀ p' {H! B@! B@8N=ΐdY.1injL|zDLJeWC챘Њ&tu;#dC3'iS,6>!yt0lZ%$9gUG$&}u1!C[B@! B@! NVY7"HIzZL>D&XDxXAu3 vT߄/TSnBkO?8G11owoݍ¬)ڛ)Fs jT51P?~77Kza`0ӱ@IDAT `T/YIH N@E"*غU˵moikSB7ZKzjuU/պU@ED-Ⱦٗ sΜ9L9If&MYy=33)uʫkdca_|V^#49-]OnTc6Ȉ>=eVώ{JjIȐn:+pra{.z7.3d@@@H6; ӟ u:U07[ih<@5{di$O)=Q.[dwYkt »Cv އΓ Y!['jdcPl8xLUHZ:{[,[{̒+ΕA={0!@5}V.rc1ihl͡ @Tiȿ.SGݏ   +ߑ'*ws[3}vJzfː<8 /gde%ŨjHyzRw\}ħ3Lws83އ!6PF#}zJNV۾1pR/*Ngk}O7:9X߿{tuwoɵy  @StT4A@^nev'8f!Oin ҧ{%z޹%Wgp)$[bw؋lw,i@ g@@I-LnNeJMV,ޫ{ КËR#G57eTgfHFzZπg+srXgְz;:=3CBw\)eduR\[)٢Z&t7X>wJ.5j]z]ffx+_=ooy  мlE@HJ^Iܶ/) Wjt䳵L \-?/C gg붎|83MH?.{cյR3NGtE %Z~K`?􂅹XܣQ$4榰ZBo kj[i9Ƹ]0z qޣ۴j {@ 88@H`%oMhBwgwf) :5{?X'&y.;#ê7guf{kĄ ڗzԟr ?;~R}ZfBZv]%w35}:k=O ̕=5|+ odoX??> uLW~xq)lk]FTN6E?m X-@ۨE{@< x&@@L'1&և _5QC4}Mt|wYiin>yVojO~lԐ>=͛tJ OTVA ;!pyв2RכpgEJh.TKdw;%sd^05q6jg\M~Z=GHUTMQ?n>˔iV C5X&xC@Z%@*.vF@JwoK&US&xׇ[eP|u9Ck[{nw'}sLצ5MМn5H6a|NO_05Mof7Z!{^o7ܫ5p?%YhH} ۵qƒa&6SnoLnǔ13߳<{Avuځ:뽏m4kH\L8%sl2_k|z!]{GM].3T}TRݣ#HA)@@@1kk)qlRqgm~4YkSx3=_o\i:d GMI C+XaO: u{e5R3 wVgj(pJsvfg;+|]4?3I˵kuq*i k|foiY):GG4 S=&;  I-ܺI= ͠L2g[3u -%߭2$23kepP:[L]w-5cl7%g&TPMIڴ١352cb벦퍦f8fv&h7?f3igVol#mvg&e{_/seP.~w>A  @LD $|rhҏ#eCx+h6;aCi'?v_4Zc{OX(?hZҏ#@ѻD$  xI  @ q@~'I?.3Р ϟ̇vwJ57fD @sO#%AOA  @LD mCNԥX ?`k63mHz`68{zMoZ$@";6b@o޽q@@ m';+hא][3XLylx* YV hagnjZқ+^ L/~FhGA  @LD 2Enof30 G9lt-9Gh&{ F ݄ §7A[J.uюo +NϕjGqI{I3 1c  2=)3T 5q;,73@pn-3M&Xwf[+t7a{p Ӟ5l(ѹ ֝ ٵ)4lחVnw+|wyݧQyӺ9̊ 3ui,N{ٟG Y_Ot{f{I3 1c  2_6eƓ93#]ӥ{6ϙu>gTu 3u9Cׅn3&7楞ϙnovi*5d6|5Nn=/ _DŽu>\o7^mh_cu99W~pTz+7h[Ι(>jT4@:/Fh{8@HVーRJ˫ oSdffz^v=_ 4x論{AnwigJnVUNo;6NlG&v+\pJ# ^(pfʛe:ј&\7{>B:+7y]uZX6[6'q'sWJ~b =zW =@1@H9W6'lMquLL)ˑZchAiHedLiSƾi7, M m8Bl 5twg'7CO7qk\ғx!;[5[T?Ka_:Z.xZZ^vGA  @LD m}s@2%GARGҚC{i:}TQ/-3CktZy Ƽ 7L9#ǵI٭O6%l“.dPͅ_]sǠB@cc jvB)%&߿h\+M3֝r)5B+@@#w.yÒ8Ugh.R8Lyҿg:ݳ%Kx'Pa{)23'+dujYmVM;mӄm: xQ:x'?OʗYDCk7GSSˣ t@|zG5ޓy$Fao |r_ss˼3 %W'fF.k@@k 9RYݵh4Q܌nүGϑ~={H=sZ&&Z>C3̳tUOncFͱjkpkOXL`<&tor~ZJOTHx?TQ#U[zin꫟Wn>&e^[qrМfYi6l 6"Cdc{+zSvT\Vo}ϕBiOZ.[>9Tz`t^l ~^.kx[4#}h'_Ai@x}^~ޔ9?x?h35JY]gd[{j|n~rGwe}67a̴x&e۟: իZgWJeM>OM V4ք:No]hhN6}{xh6a^ {$lq嫇"W_-ܒwOspkboW^)}h&z I6tq-2:{(?$P͟" ek}W$w_yg"#w Ud'\ dBnFa  I$ߓ=ZGLn~4x7|~Q`Tdj!=sK s5ls>|NVzCV{V5^xdm5;ޔN13ly0Z}a-mKk]/%׿ɾ]>tJ78̲FnD:Ѹ Zgf z]hWi M^]`:]t{^0V 'b Η;G {DT{1{~P^.'_䜑‚COL=On_[2{p{~@XP])?] _+r`:ѐ C0&Pn\8_{o_o=i @NX3Պt9?d6a^Sf 6ٵ̌']mkn˘3浹!kΐ}\fL-gs3A]Fڨ M3Mu>3VD7r76;(˼}vgs3&C{uֺ^p﫻[\t;pgS ƼgZ ݄߭\y6ynz)c`jF@:gγ{!0iPi1bsG {$,/_, TnxpCpYʓfȸ@y˝{%۫_sTEZCz] ^ ,xUrnh=G ˶n1ϕw!xe~'x?qڢe޿ϕ~lx|a+ I$Pr$qwU@Lmd&xwzZ63 xSNFLu]c:kϺ1.@n0&6:;\m?{9.77?ƴg_`j-[B1c5-?kT7܌l3nv_3Mg=[Mo6>C1<.p%3dL7 {ę{Bh~\n /[mo v[Z%߳Y#ȭ~|sEw$zLOIR:>Ƭ=ʬ3[Ve櫗-wdYIȁNœ3dT\2kY zÎKƳam7DZb"g1\F)[#+W2-8%DACicFurE2ܼ +{YSCXܾ_MiUڇpsD}ߥ7+xiȕ/lAj2̟S?w"Y[Q޳Kf9jk oL/#vD}4n'KVnO7S\X y냝2oK$5eP?{Lwп.ۻr^4t9k՗dĨ^%.Y$oQ_,ku qֆR3(5&1|C5XF@ {&ys۾G 8) 5Y6 W&y^;μzC6Fm[a?7/#f]jgVÜ F1g,ݾH`=1X[8.=De8[p/=L]M 滟/SsvrD!1z {Kd]^&Ž4Y )ݱQ>ٴCUk=˘'Ʉ ̡+/¡#~Ke݆Odco=}gΐ Ck\ud|n8v:.S3iLuIɞ@ (SϚ 'MN_hm@_xu +F}X $Mn=Vnrf@q c`8"tkun 讽_@]Bs޿z,BCxHkx`_K)M,N2+gIANvdv x7)O/qʈ edDz#}[wʭPUk9 ל-wZXe@. /R4HJ3N"#ítvm sn1&wCQ^Q [}Bɰe[=~䚁2ZWu%tJ6n1 k+=S?F5?VYԌzOZ͌Kf-̮}k\^avo*{y6>1p$ X3d$dCOp㝋 qT2//GV8Gu7dï QqC2yчO#`Wϖ/=!7W=ć:bY4BRGU U1_ ЏݫǞ=DGrmWఆK^ ʥprEeu- 9Wnrr~J=*oE?p0kGtrHtiop$,! @2 mnÚs @]7I=.sHSsw;ΎIa+"9n5kj COnb{oN+_?i1.'C> >Ǚs,Ҳ18HKLg>T&{%7Fyf<Ã%~*[\3s%CCJoԞm71lÉ7v {udr%c傁z":k6r 9Y#Oԛ:3+?}VkON#oL-E] m:mCUd1kS j0mm_ZfpxђEJ5&7͔IW^go`8fѢT kjGә1^$וs*U% [珕~K$B ~=֛+뺹7d 4Pnp̢=~Lx7kuDLL7*l>)U7zy䂡ˍoXD6M̿;K?|@L=yddg_L6>!Pty䏚)_>o5m;_fk0_. ~e.yW@Fʛu?oQW›fh":=vu]|L5X}'d˪Β/=l?B9s0ɨ-_Z&N>jXd ?_i+]ϻڇXGmoa!{ղȗs$IoȕB{6_sw0/yP?GHxc 2v[ϿY}9}黏+1kl^8Q(// ΁e⧬~:}Lnig@H^?~X"6Jsh ƴv8w uI7kN^/;[O #i]f)?1PS;ҬwH]ׂ1Z m.HauG5늳'ﭯLnGPQ\ẖnr4Ł֖A aEPzZ{[t]!q e̟J]oM2>^Z2DK89or2 !z3oۚ>IˆaYLi2E酞H&xjN ޫJdɢǬ߶PyU/™7 968#=r`;xy݋$KR VY*} 7H+ߕ=G#$S 1Іt9dzؖ/o ^F= @@ ~fydoR"@.3T}"@{Z"j}떣L[@孛=<3:m4KYl){ƻw};WczB[/;OoVjOvn" ٝN|Eg/gm]2;$IxfaݠtULљ|-R^Y/ꛛmpNB߿)7=KrK8F-&hz ]?g\a2Эr>jgOz_iR>HG|./nߑltn'[G^vLC٣yW{K䱕G%\Y0wr1c׾/o9 K_+g ֚ig@Hjz_%J$H"`JH?LGӛVF)~t9Moj*YRs;3͖+_9 5[0eV}sư&xyhaYZ#cۣ>GGx8 zj'gGw9%VeHV}>kBnښ&n8"B]r^ nY["QF >,K4EmchA|}|_9uQ>Hm~y ߣ:'I_CY{ &:ڊK~C>?Jn[tDϩi=>xy3KJx zs#zSM]#_./,^,tFrseMjǪb?Eo`y/3޳+33o@JGdɛktb ̿`18gأ=2O=< yfTAFn6YUo}ݪ릝z֋hQOtÜ +_!WE*$\]%qX%ρFYrLQN!^̖6m{ڍ2u?Y:WvFv{{eC5ЫA {1wCgK1:[V+ܟCW aZ﨟]Bo۶0V5l,bL ^/ۺA\hc gא6iS3//)7\8^͌st`{@@/s,E E|˂_-6m~ȫK6aoNZU ҥ|zeF}޴xDo0xo>_|R7?gK~MB?[;7y70.@@ m';~ l/~FÑ$QIX]ayG+ΖL Hc>nK0sO6[SgG7ż576nqp7?$x!-| _=OneK>f݄}sUlpz5xLDf@yeKt[CnT?U[wSg1sb9_="¢o}E vA{̷ @MWVKadG1E='iC&rуZʫwϔ/\h}D[al;M/t5ʿ֜ 0t yq2($ ܳz^XշEe{䂷tp&߿h̟тNcLvo 3姗~N9HZ]j&A~脅OiU~rBKcOoxWAӮ_`Kirw")ݾr6U}Z}r>ၹY5/>_no_ЄL֯xQ O4fpqh[;F(5cWxU=bG],w|B V7.֒<;%y3;2`UyCYg/+Z %{JQ1g :N{F&S.S޼{LrgVWu% rɽO5Jѕ!f-AŲ[%`f>-}ݫwr7n K )}Bi޻gg:m&!u$˜:ϩ_=}sO5>!⢇cؾYي9G 6_q}-u4?ԑD]] @IDATs+,m'1F/6TT*ϗq @F#y>z7BU˖G%CK^bCB.{/v5@TgiClӄ(1+;:x.>k'_-?;T yoбW˨Ifq"Y_8LuoHWXD@UmRb, $p!/3S`Aʟn. h~Vy6l74)za7F^.^"c9gOYHL->392<NўS3hS/X8_"Tz9%W 43}h-44Pb9OM g co:#|w]q21Np{`L#y_)0MYaW' ]J<`_ }ל-mIZ_t ji6О:6xӀ{ܦ2V_&Y kVZnBcSV\}lY`ƺ__3s=fi o7 Dh'oΛ!ѕ/> O~xhͅo1[r2UlFa7\~qEfF]vYziX#a=spQ۷|~L||8lV" @ =^! j]Adɢ93eh3 =eJ[3Yz94 h˷V>[!*_ƙrȞ2"QI-RS?[裡A{ ͔eh<{8<ts|}m)Ҳ7cr{k_ugkKsk?_Z!Oכ脎M)ӿ.>h7-;ף -_'s յន?;~[mvL @@h]ɯV`vFcd9|@y/@i1  @w2@@RX/˳붧-p픑r=z/}=@1@@ oDVl?c/6#@G 9Hn=>-kwP =@1@@?Rz{ t}_ @2G4 S=&;  @hePEu fhO~y9/)=ih=&xf@b $b@@ l;|\F}-<@k4Ypt]TuӴ׉{I3 1c  @+)^׊#Sdzq/ xM xf@b $b@@V mk<]Dݣ7#HA)@@@< |f{{{I3 1c  e.A  @LD  7\溌7R2ou7!i q @[o   +Cf$szw$Go G4 S=&;  t_ogm3sJ:G)##;5=z=@1@@VL~j'#&,t8G {/y[jG4 E{F([Y  @ =^!Q>=r;CX`T^rehA-\ {^yeZ@" \:4ύs$ xO7#   "#[*YL?@\qr]p +w$i@@@@ @Qfy hioN+i5dI]@@@@ )nc{*)03}2 !${" @@@F>evu3D3ar%irO$A "c@@@@X`g*%'(z )*1Jl]I @o]D@@@6g5iu2N螑.j:+ww1"   I"pV[]ܶ/IzL7IC)# ';M_P= 4   @ɡT*I I#eL8@@O    @ ȉVŮ*0o/p9odNq    Е8"on/)J2c=\0zLԷˌ&{ro@@@@'*OɚfdS %5X>?b #ѻK     W8(Pf(5p@ ';F@@@@$pd3ygAn&?,٧ A=✡Kv@@@@V`gdRY)Eӹo)%3Ck_&ӹx$@$    $ȇ{!ЄӇis`n1ꜥ#;Rs!   $ òNzV NѐElT=A    й&z MonZk%ٳ Ք;5ۓ q     ]E`[q)џ: EJO1 d[SEn씚2*@@@@vv|z8zRsiyU;1+#=e>.*H:Q95    @ˮc'e=^ag_㩤dzZ7R'C?{{6    @87i5?ʫJTȑjOZo]J3M]zST :y7     Z1dMT5HymT zTK>u wꔘ     x(@!&M!     @g@@@@@=Ĥ)@@@@@      4     ;@@@@@< x@@@@@ x3     b     |@@@@@PCLB@@@@@      {IS     @@@@@@Cw1i @@@@@w>     x(@!&M!     @g@@@@@=Ĥ)@@@@@      4     ;@@@@@< x@@@@@ x3     b     |@@@@@PCLB@@@@@      {IS     @@@@@@Cw1i @@@@@w>     x(@!&M!     @g@@@@@=Ĥ)@@@@@      4     ;@@@@@< x@@@@@ x3     b     |@@@@@PCLB@@@@@      {IS     @@@@@@Cw1i @@@@@w>     x(@!&M!     @g@@@@@=Ĥ)@@@@@      4     ;@@@@@< x@@@@@ x3     b     |@@@@@PCLB@@@@@      {IS     @@@@@@Cw1i @@@@@w>۶m%A@@@@:W`ѝہ$?;Go G4    .@[@G#      xwq@@@@@h@@@@@\.^      {|~     K @@@@@ xϏ@@@@@p 8x     @|q4     .w/@@@@@O=>?F@@@@@%@           ]@@@@@@ >8@@@@@     '@G#      xwq@@@@@h@@@@@g}࣪K! 5h-(,+bhwX%/y`\V-ϊv)RYX)5 b$B4H@ |;'373W2Ϲ|{fs{H!          Px&          wwHHHHHHHHHHH >ǫIHHHHHHHHHH@!@]          j          PPxWppHHHHHHHHHH#@=>~HHHHHHHHHH!          Px&          wwHHHHHHHHHHH >ǫIHHHHHHHHHH@!@]          j          PPxWppHHHHHHHHHH#@=>~HHHHHHHHHH!          Px&          wwHHHHHHHHHHH >ǫIHHHHHHHHHH@!@]          j          PPxWppHHHHHHHHHH#@=>~HHHHHHHHHH!          Px&          wwHHHHHHHHHHH >ǫIHHHHHHHHHH@!@]          j          PPxWppHHHHHHHHHH#@=>~HHHHHHHHHH!          Px&          wwHHHHHHHHHHH >ǫIHHHHHHHHHH@!@]          j          PPxWppHHHHHHHHHH#@=>~HHHHHHHHHH!          Px&          wwHHHHHHHHHHH >ǫIHHHHHHHHHH@!@]          j          PPxWppHHHHHHHHHH#@=>~HHHHHHHHHH!          Px&          wwHHHHHHHHHHH >ǫIHHHHHHHHHH@!@]          j          PPxWppHHHHHHHHHH#@=>~HHHHHHHHHH!          Px&          wwHHHHHHHHHHH >ǫIHHHHHHHHHH@!@]          j          PPxWppHHHHHHHHHH#@=>~HHHHHHHHHH!          Px&          wwHH5 \}G}s͸RX"މQi Y0"Ќoxfݏ~)A~]+[iMH.R}W[_ 1Qo,<pu]ioZi1:{qT#ڷo/ɸ־NLdq:oqHHHH(~ϛоi7P8q +7'#u1KH xO4 ׉~=oD2{$2`'r1/ 4x $Պ]ɬ6ob?dLYY$-|CΛ%+ơs6x}gL|$cȃ_ ؁6v\MEp Wj}ޫmo$G8Y6`G$@$@$@mwK\iNClU#+){ݑGuWq2GՓ~Y2Ֆ+m=Xx91y;wg:u[MVҔVaW8k 0}v?k_ Te۫|M5X=94{yH,_0 cZ0{u%(9 -~v%'xB^,CX1]k     V%@eoŰzܓ&ۑț^-~Г}6=M9h  h tU[:aY_㿰=x6ZIxw2g WM>~."KGtkǧY"t3ŵ/Fh]uuCT,[Nz÷sv÷)ό+,%)&+-7H ke%g` %gbq[Z'\UW85pVLH-?zlۉs#Q0G0቏+hu3e &dq݈HHHu]na']́mYg0c~nPH쎏ffeSG>.ͭUHK?(yFO,!- jS;#g ' ns1#-qL]&M eif 7xC) %7TlrOq     w[.3[#X[vc7]$// ^ w1:شhi$)eskUl"PyӋI0Ivkٺ70y'/@c}-5cJNm6 s'   B»-a l$j#oCk7>8oGfEwk BCp^|o?dRz_L^N-[p[5{uyсvꆨZMmS^$q ہjˍ>*xs ,DEӻ6JYg8C#6Z4HHH.; .#7l"Y1ho ~>F8|~&4xC[{bHZڗp 7sw ?J?Gy=p_fO{T!GNş>P=n nQ<\u bc==5b׾pN_%u }<~xoS+''3{ĖmiG,WIps?  j+k=)S-7|T/M;hDOy)2k{d 7gq/ 7zםk_ҒcXR뿗D+m5.ʽ9>=dLUS8pKٽ [Cv}l]W]}ȱpEoڥpގmcm9D~r?T]yK񶻑^#*J 3F {0[NKZpFYoPd<9W).H%=_ ͨ,ً*C$Ϡm qqz) O8֡Ŀخ̖cSBURqwBP~ wug/)v)Ⱥ> dh[3jǻDZ*=)#0.ɢ u8p3>DǷ#9 5K;K>e\j6څ=gN'; )-(*6֣ TםQb#J֎C?8όzxnwAwCM);GKAs ڿ\܇`]VRsF茦󗐐rr}쁌=w{j{.Gv=ґ;!pcy/}(PeZ`%{d95D>$^nC@?`;MIbP_<9uzOգt$º%}ax0>7Tg1Id\R}ê7HHHH\!@L»' P%̟4,ƂN޿yK{oB,QaHox`E>56s -UK/ OJXC6QO/.&etRK{?2J='cklY8hNZ Roƨ_`J鉣 G:¡wcPIX]!13-lK%FcY;]Qx9_Flra,ZYPج1yrI?kabixu|13RjVw+ʿ.C20`*rl(w,>_ 0 HSoƎS`%U3 Z1s!E󊲮 ذaXͮ3`L?gK,5V,䠴/)VQUlMƔ勑] (ZW의p4 C{; Eqخ53QEB)L0KXlMI1Zu:T~+"ڢ[1T~q'@; $$CfH!N#խ,)?Zv;yLWbJ^G\Goy %a'ڋ蝋A^Zu /Ť6,&.Aن*OاqYۻkE<^Gt{aIs1^'!1w斶关_&t*ȷ8MOffyx?g(*q6gtYnD Mq[ot"#JW[O)8#/XMo6|P_"WmT&ɲɹBb#fp==r"z.C7k*r&aF__[((Zٚ9 㲥7IOi2h$h{ M'FF|x}FH>I&Nĉo|P.x+xW0dd{'5QBތ~0 S]۾Ɉ;?}A<}0ndǫiuoĚvI%e6ؖR}1cj'Tx7ruJGJaEXv'{>|oW`R{ y{Հ,f׿iO~{8##F46כbdN7K^e(‹%k1wmUlRXN$Ul[e'bk?`^Y=&жXd@&L?\8dF.HL ZXX$|^|n)aRH9Gif1ozhKD-ra6UA&S-1G~fZ<.o\}Xm)o#1s{!-KgcFQEI.F= a7!6tA03i!& VNj暝zKMŊsF}HB<s%DPmox+$@$@$@$@/AzUxO1 rR{-!,"EU,2RDDe~Tb/ytHC; ~^$ݲ %Lf ܌&0{3z kV-~1{`x뱀'.GpT<Qi.ٜfN|' y۬wweت? 8&b絗3'$Ἷ[bN6ұkf7\[dOo~a!hO2 ^{dBm<]2^.qRÎ"CˆН.C-*:'8sU_*GtLh%˶zKJF QjG>/!aKj*p+ћnzkW6 =^<ρT3.v]0~2 hd]*_TQ+`-E !)^&HkdqUsM]ʶ{kˮʦܧ# Ky#+*a{\ \G4p ogvhUPGaKjrWi?u\}ąz/&Jj}@]ZE0ryHNZ},d#R;*7왆קi|FM0LxHHHH zޣgUJgk3[_dU[TXQ׹Dx$?\ѯmeV /|{Y>B=c[[ ꓓ(.ŞOfoX#O{yN6wD1tT z_ ס$5Ct3`%\$0s[(-}l~Sl~OM0^mE:2?t״s6_~&:+Xo;Jr2 elX>Ooۍ&N~d|X_<2u,`8~.$e?%lrŖo(~7d 'y~s6V"H`lnA017[":!z*f4q#.nYk FSoۑx#GKtGXhd7.>O CQ0"eXa5JаA)O/>94K\x>^B8HNUUY!qq_ 6I2ɳLRCg يƑiiq];L:*;!luԎcibSȓ)bFvѾa; @\(Dž/P =EU-|-Ƭ8dgwϟm,ZluDQc>|T:em# ’9oy/㊇t%$=|ț/.s1ʾlՏ#3TS?#|I#mfJ {Hc@\bq: $".TqռJxkKP-!@$MΙmȿE3Q{vCxWz!+ReHBYְTbȯ2cg`L nZ»&|^"/3Dzv8N wN+:|yj ]bלjl\b[XSiՖX1M)6v+61f_grZ{ODZ lkc0j[cxsOy]VF9HHHH @=xN{Y]b "Z&J |'Zan Z!<nG}Ln9~c$ki;T?Kbȿo!o-y"&Ju/>\B&#YHEΑm}6_!]xWC0wQBYM찉@YpapBM+> $܇7ݿPbL6c8@dtF@~w]W\_Y}ǾBBBzMMMHͼYiƌAh-G?Mn<;w8ݼ[]_@6I]a[8֭|Ÿе,ةWF, .*fQZd[6ÝӪ<]u]X!h{jzOt/T^VL|W=ELn*#dNi_Z؅o[n8y#+-}'V0Q ڌ07U|)fx67?    x Pxv}8MID^_BL{"_( j,f``ߞvFIZEݾ,ٿ&"L|[v"/OC nn2& B34o&B_h㙐0l׏D]7LFm.Cj؄w剑Xm=HQDu{읏6ON)gYSHO"=7W$._x Y3ǻנ8g ÿ|[7TO9GA ?R∏x|@e{8p?~H`!H }B@m,m4(*|yےGǏEX/Rҹw}G_,5P ڦ;o+t?1c">>*(mosHHHH tiXI Sfyl#;ވWBYq+~1^rX2bOT7a|] Sbgb7|8 '* v* Y8a-R'6l&zE| %ܸU!L["(R!m|H[H3Ibjţ|w= Krpdm.oČgҾ*2F>XZ#}|}hcD_30_2W\֗†sXz:kc-?v'Rd^4\>(VxZҿbo=6ĕAX=qM;JjɊ /3pfyEֺţkHpmg+R;F;}kWQ8d߰C6 @(njH[;1*K(}ƒ}\4J- i=uaX+#VP2Hyc;\sJCx!/|rkiU^/P*!̆K\%ۚJ?􁽎v:{Ӻ9/7gb~qcdgbBN/#nl>((ƋXܱ0")v1It6TWfb;T9z,bn؊?2;Gk*vu/:0gqtc3X'jYeEPOTle}O$@tZ M8N/iqF\1¿xb٭N wN$(} r?NTGWqݔ63v2'Bj֨!cSiHm鼑F # '/_B5;þwճ[G(u q4VnA⭾,I 58|BqOy<ԌV7F$@$@$@$ :vQ^T/v5Q2]w|1qohЋ:K8qf8`!> ~` ߈5"L j*'nPC-(_zůL xk;9{"<lVMMfEZl\͗J- ǦG'ݕ{M"k%<_+ej.IXdzǻH[ #`Mm !R:“W7&3'^/xWu-{ WF\Ye,CURWlUa\T &|4c)Xo)qfgyTd4WʏKT vyw\GZ.LO'<'P3#жp8L3Wo:•ñ\p!(_^KW DtY|r[WyZnx1C+Q%}Vm)mDbrN;^ƑYii1K0`LOa|eRȚRCj;%=>K'j,gŽQmoءsHHHH fcF|adcf-U?~(kԹ#1u`?&-x< gteXP3.bwm{™7ʑy?ԗV_zϜ🔷}o{r(<o+Uj||{y7=~ MBK\EZm\Z)~Aٵ않}YlM<{+drYwƦ~@_>f:/F lpbX]k_(ԨJs0b@@j+ǪEi~J@{KdѵEF# 0Jt[zDY4bf,So$Mq9lY5rx]ulxp/}Vy7lTKBIgY(rBر4҇cγc˦|7W<- "#fAUTCy//|uxe 5[mhk:>={ kakRh̙2"(߶ E-_wI#nIaEjHl4`4O2(~))݆6&䌞OCL/ /I1oX4cĚv61zÝ>孠L׿y'Xa^ߨݿ/n'e';#c\n,aά.    rG#U{wUOxsxANUo?}x9/P{y]xgjMX|o7u#٩zg ^nqQnC6_{$L;AV0j>"g06_F&h!kxR ip߮'sx>qLy~1Pn /#36^,4LWX_e3qotJƗhV#EW0{]\S7ȵE/9[E]u2K΀g|;#)K4Rdbij]3Fv!?؊ZV=5#U22RQ[ &Ú/kTWRg{W :]}?6J=CϷ#.'ҳs-\ٺJUY~kr&ĜZ»L_-XUߒ_8#+IZm`!PxJ&I܋.]К# dAb! W,>sԌi=Yޢ8\A8JFfgOLjF.v>A_cIZt:M Y0|\*(~)>47HHHHBMl't# )!՗&6:A1LqׅwQ%[pZHzNtXktW }h*|E8RRS] 0^ *5weJ[,li#{x°yVBXtf+q. s(6c_a@xcRaO =n/!uQk}βyK ]Փa/kWs(#A"Q8 ؽźUw#{l%ޙuu n!E4U`K"m̂ˋW_&w_gX.Ԯߝ>o=r>)p`p1Ӆo3k#YF F)IoaߛQfc2i]eQi:FLߨgC5)0`pHHH(]$3αw.9:}x)im4f,9ǜ5aۇ1gWm 6Gb=6m^䊈| <KCy8?BHN7U`Qkp[^Ġ>Cmp /ē)ꑽh81a/ح+~@?gt:&a®{uU}tsTu }VyztÓ|Rwq߀mӱz>rf2=UK*K$HNGbxn? -2Mp$\?xI,؝ k}e k}Jbԣ4(%dDXWÀd`FL@|CP_h>;,:գd_[K=sȵЊ~N]+5Ã`=?J،.μC'_jr$݈m]J﹤tdu96Z[}+IY6F3JZs n 7kp9_ :t@f$Up>t7&;_#[LL|oO?xw֝R5V|q:t57& 1J|A[YUWo;xuR*Z--p[麯p"pSZ|?MFZHc}-N|q_H:unM)8N2EtA&:5.⿲76-$UOAJggWF՜™ ':5 $$si/o%$xLNMm.,YW_i}4:uEj7⫇,u9%}? ]SC]nNؖX'o]I]#-EI E7diB/[BI@=j`SQH~)))h0_ `HHHH 6cƫZ@8ڭB XnKpLB nES5 | 4VcGq-18Wxj^r {DAqX{    PxaQj؜'XC{LSYt0;gjLcIHHHj%9󦍀Jt*ۚ}0z)HHHHHj&@jnkKx~.,ţM;#y /;/z͡aHHHH=فT9iHj:#v`v5{|,}ƪCsHHHHHU]%poǂ 1`^!0 kssXa5T2Rp`xg}"i}210; ݸel E$@$@$@$Pxo @HHHHHHHHHHޯdEHHHHHHHHHH mh 5C5Ӕ @[ @-m          fPxf!         h (V $@$@$@$@$@$@$@$@$@$@  LS"$@$@$@$@$@$@$@$@$@$@m fT|xz;_KX7@s}|!=?뿙,Xk o&rcLF䀟̾p-՚ZjͶS_`㻵p=Ǿ 4-! p$@KkPuZG齺Z9m}~L~&cȃj(}l҄L 5.=ReizYGB h5m}^\28DfIk7ĽeMm{(P-$@$@mw[G%I9 rN#Јښ:4%tDZvV:y8/swM_q۶u1% n>HBX_]vTL̕ghkZ|qdv Yw2:G-̇HHr2mcA fv@B,6]IHxo@eVe X`[w ?7ʺ=}  ?[0k?8xm7vH˷ޯ~j 7w\nC%x#?C;z;,XhV|A~)w>|v}qaG)Ǿ>@Ee5J/]oM,aكKHꑅR\W?YG?UtV=z6eQSZ#uMHrn3H˧ᔜť$d 4dgFWNwLWp,sCM#z@ҞRSqw< \_(^݊OH+H30.7W]u9J#>>ڥp=I!^ǻݥU)}qϰ/YWfqz>AW=ݝ _VGى ^IҶSl5o6}~sJȹ>:_unsQg46}RnYa 敖#VƭۗcVXeJYwa}ߖ0^^u2xrZܣX ;(9(w_?F^|cCո%cB'wu2{Gv)Ⱥ> Ϥׇ:,J>g}v0ƥd\ 9Y 6 \ye6J {M }F ymk߫-ߍ7܍*ww܃ArLYi閏;g{ ӹYz ;: N)w9lO5|/vdCJDoCWˋ!xyޏTIGSƸ; +DHC{>@m3k]h|gϢ!yYEU(ߕ[rO֯cce۲&-ھc7cF GsIM~/k-'>|0:u1oosl~<|x;*&tT}-蟜XqnonzΜwWOn'!i_Şbϛeu׾<=PKQuNɋV{vg+íF;}Kkrl{'c:$&o<ݞJ6x3qSl?vNN_:⑻zۓ i]nfTXUwyŜPff crEX%8[Vj ͆ bvURW2Ozc Ɋ=y8֮*#d  tMLL Y2XOMDⶵ(@RV&- 6AOjKFcA[!9/sR&LEN/zu۰hsAvc?+ ꭊf{KCnG:< m#)c8fL ʷ%S6mPӋm\!;q2V}9z[Ջ^V>6bޥl'.<\Bps8Q qjĆE(3zc愡](Q;ׯW'3~ &bKW~VxLpȣr+9Ήԅ'r*v`4E )݃)<M s8Jwwa^EفxF!V.2=u:<`+!X9hm=mފf(bL h*yP+9XC؉?y\S%cEv,N!^y_0ipyL/k ,}7G5wT+m:1\o߯cKMd-~;g{S/({ٿJc?.Ajf\9͙$ WLm`0ݽsKBƮz8ޓ"կ[6ֱ){X»=4 7b{XK2.,{Qіխ'>q?o< 1;2vg.-އa{̧N+~D.&վ0))w.D˟8 :;f܉,M|R܂ uK:=Bq h(8 * n.h;=r"z.C7k*I9b`6-|RF}ѮK7 ߙ[ViĶ|l$eqGTϝ»_[dcɰ;ۓ[乾O^A+_~8,K~4h_ /Jjp}^ܒ |ZX3KPT  I& w7c!Cr?mb;LY5&I=-e:2'w[}2!c#̶1KGq,|3/̧WaX~#v,Fsx=V{ oR1~ iމEE-(LJF^U_I>'RdqA4[銵_$-(0ͳbf/X~p}1sjJSe\4E㿱f;څ1l9גn{XC#aQl 7~+v܈ۃ*C'/d"~\Z~b8ތ+>V Cdt{p3'eklxڲW& +yh<(<;Wd9љw(:`bg3ְL-WkoJco 3cx"j>O ~#!&: YʄV(ՓWwP5tOOwvPjC1JLvjFWۿטN'3&@IDAT%D :CJkx0ཤ5Íz.H|!;xT<íO%y@"'ft=~ud(sJx C{³" a-OT?*C DIHڤYᮕ'x?wL.^p[v>Ql <ɾ' 6nk"t: IHmr{(?%\Ƌ,tƀ`-LA#;JJye\="P=?^ _R=2~yzaqBJ}4A_C'qBFfe١ŒGtD e\Xn0HyެYe0ޟöZٕߌ# c]q eKn1wo՗hb Mg*Յ "K =^:H0j\2ez! @SAno(Y~ MmYZۅŠn.xp;530y9 𮍕 [e^o776T6qml4U+*7 3[0.oFN&TOtY`>@ l*Pbw6 9 .rWްcܧ))OHC.ރanF{4(LP{~-[ri,ߤ-zkp^aecQpd]*a>a~ݲ xu3`w߄yoQi[KO9 gk00+1ڧc.ދi.x_rίw?#Qr8{R/wHoxRc^0!3-…Os~GbkO&x dR~x  E»5UI2 U7Rbď 5b^k+'w2c۞Q;*30 M,'4".Jq)qitS +,y wr9̝{ <ʝ?g{ΙwyO7x$gp=U8SCKC"NMrޫ<ˢ>Y3Nt!dYy:7,Vrԑ NRUFhE@7JPo>rya1abI=rX^l0B~1& ϑ2bKߥK3vnf}p!-VB-Ege5> "4c۳Zd9X!{Mјq!NlR;?fKy6̄%z"8do!7^?Ff뙑ػ:FF9bA2!T) G!>z鬞K>uRǣʱp|4^@a2qJJeALVzISC4phMѾO2xbJ go4#[srW~o ֙Eā>CQ纷]5,fh"x1*gp=x&*v7OZ]o-fG.BSQ,z'GZuE鷾 @,nDžۛ.Ex׉pH 3 G+xS9-X5\qw%鎝1, Cd7vS_9Lf:tPl4GX/9bbUF=mqrb;ÖG"F$0FR׿ׇY.W5}ܶt_rVRUU@1m+Un]zBݸhP[v՞UNG^}W2ZXY+e՜v krPX\;lˁt_$1>/Å!ubtQhòTa,c2=`qۂYso?9f$erT3S?Rfl}4:%+ͱ%t,+tVw4 ݕ|VvЗZ{DH\jS&#[?,H\T?UxWFF,KQRu#x\Uy&Islw:7]GM?ϐ4u' "@{P^p]FIg>tIg䒛\+dN3έ}6;'}ǁuĻ @|>v,JY(&nnu|V٘l9D 3/rr<[HK MHPo#_,΃> 'Z$M|NÙ Ruu)ϡ2,<'ESMwoĎf'k PE'#f諾ܴp;rLF߸},Gn®-wX3ܮ/()EDwi6qXj:T7gFAU[3(LW([ȩX._UjP9mqTuNyYsk?9f[+50gmװ3歍|g\5kEgE˵d=z{o;lU:/ZԶܞCRE;%ČEnHRP37d,-܍}ǻC Hxb"_rI7nN݌mU?pH Sg鋥].t "Ső65X qL}C8.l+ǔz|d|"|Lp.N]5G Δw#1nnFZ$ ,Vrr/[g$U ԍv{uIX  m$u p}ry0FUyogT@2 듅VCLi|+F\vsEm͍8m3"@jN5]⋫ \^h >bM !eՃ,Jk(. u{1LTC=KM<%yHNxnT[Ȱ] 'zHz'L#ڮT$!p~o s3n0m/{4AvO9c=2FB)KhZ->=2]Y0{6-.fl"[ Uk^q~0SM7ntDO$xn^pe,]GO USTV0VI1$c7ծ"YȿV;s<fGK\x;߮^JM?ϻ,Y! К/1޵$eӘ`+9ZƮwS8 KҌE@O@o#_rX.WLg;q5LtsC6=cls+S!Ζۮs G7݋'eqjY %tK5nx^B3TKhŵ{ cJI*}cо:RDZǴfDm?-SOn:~Fm2eGPa(+LU',-¢U6&%iX:><([nRK{;@|۲AH;%ˏڃk7q)\\eY\uj<5\M;0v>xfw@Gh=$ 棪)mqLEJr[[gpS5]= L%@{ȖSJG!Ѹr{`)F:(Ɗia8 +g*F+2cw6GsѰ~rl2 #CKC"U{ Z ŖڶԭR/vZx4; H}uhtv`rl^+LFzMURy]GGciu, KҌ%3qDQsf)Ab9oJףe/w4Z6bժM8ٵ+Μ遯owSU^oκiYL_IK{',Z곖;Mړ16ֱq^3W2ZG% 8qLYǨPrWi6Io^}uD5SRn1%IRGGGyOLt}?(uޖIקN^K+# o9V3 ^I3sVZhL<﹆O ^ч6aN ow=(o[6 @Dq]qBp3Yt,Jj솭3apȾ]QYh^υFO`“f6}Ƒ"+P3F`6_q?^w5Wn(R w_!V?ىr/:̺o&"z,z6G|nt0X_yzU1ʲkMVטcibq/)_ ȥ& 73%܌:Y 6ˇ)2}?_FߛWhS ǕJup ʫ6ZYy^JK$Lx/a1Vm-[wn,wXrfjy4e/.Q)gMX-LGtN2Rf5}=mڳ1>E]G }r}R,-Kc1}xɼDb2[h!zbx5+0{yvbChR+{Uh}HB}Z'IQbl1cl˝X>W؞I ;#Րls,;~Z=/0SLI;BOa,i-mؾ ~;{KO~u a#30OQ4㦖wH2![N}qyۆfZɲ anh؃Pe%7#UȴQ+&u] x}JB͘@(̝quji^o1<Պ>ġ+E`hh:KY{@B:Am$NQ)JLoce1W7,uc#`ê;7S-&5&mLi[()ENm5ֺj'p9Hd7혲 ۰XƺT 5F9>Aw8HKdmM;cX ;WaZsn8i?X5֭m fJH0lb궕U!an[6R?;f11iFɩIe\-?Y{ƈsww4fe ǔၱO};1|WV7sw}<(sWc_o^އ'פޗ&{VƾOSG_c]ϡjQcGqKOl*2c%„'ߵrG%: +(>yq׎D^2SME~^o0;֓_"3Np|"sKnoܾXN_;Ćb6;\%ݸ9oqՖzlzq8x9WŤoO9Y)$@$xA_Z|2&ɳQXv^ށC49;BI4b(ꪏ<+Dǟ8]@HIEő_d3KC髾xgc|44bi8 gHP$ZP\gÎjp!jV̴vA/>#☋tq8؏I!Ey8)njtJNEծ 1bıVm0 qa`6҇Q.;_ˣD{풙=3{K3C~#n}bY.Nx'.=SGzlSPFi3Ǟic`H1;GǼq31khz0jvG1 UMLSѐ3˽J҇$G=(j#]l2hzo90RpܯN^~F߷R␀Mt.(HI¨Of?YDƿbvoq u0E4vKʪ3$eXø:?;&Cש M/Mj[ rŃcnƿ( +㽱f 欩R珚<07HH  Gdž gSVWG6{]V,NɊ224]D\S ǍXK%9 >0Lk,"]p[{ >Tr@~u[Vh3o~1, %oe!n Y4lhrǎf\oXq #ݙDYq™ 1;2_7T#2;[2xP̴Wvm!V?3YvVV`=UCx}=Cuzs|ex5ʐ>Y^SvV+N%_0 skK}v@pḙ,E?ML =~CQ>**<Ź_J{*lV<;I)&)pHzo %{QN0sW&"r`P a1;Ӻ H3!D٦ԗgs.FH\wDC0Xlÿ/鞇n힫;7͐*N6`=x2@>=逎jcks_U>1}=׸1v}s!`ɫXa 6(-@}}-|mE5ߞ.͸Et?Տ|+rİgy>redY  9&@{ctSNΫ 5~_lޭ*21 rA_e4k^P{ ;UE0K*$dt[`}in5miYӻcB9lg|IȣP=kcq$ҰrjU ͇k_`!m8HFgF S1%xצDZZO_M{}v5zw 9ov/;.zދvv F+`1MX"R ?3ĒU# #!,em4~S)SV8d{5/yQF݃ w3X/I漹5߉ۂѲLB*=s >kOָ's pτqmkDloѫ.m6u(c{#ה~)f HFYBI\vfVK.<|5vk3dC$|vK8$` =jYg8" IST9x%a،nS1l*F r[&3L6`fSl,kMMWVKSw:t=.і ĞCͲ:G:qL6 c|]jX-«l۶^{wYw;ԍ3>/6{>o>]1A*+g۝/y#qw[osw0o!tf%FGƊ Dtw"Y:{W~! +aKS?)/+[tGϋ! aer>>$KxtEn=i>"#YѫzZC=4vT^-S;9C }zZ3PKڙ$Pڄɡv&YH8'6rgJőnjijYtsӽ]4ʂ+%D/oU0m-hl<.X iҞb'DdN~G"9 ܛ؈G۔mbMUU , 7ڨn|To;̺=:.}DQ->V+aԧsf?nmAә.]^ѣ^[[=}O[\տ̧?=7t,?QY󋜴x|Y޻S8/w{t?  0~aؙ$    G@ӬOD=ɃX>^>CY$e2  4tgŨ/ @Z=CHHHHHHHHHH2f1K$@$@$@$@$@$@$@$@$@$xOkP9          L#@{Y 5:vXrk$@$@$ph=yO؀3YC !sOX   8 U[qide%"Ⱥ uPhsP %‚iI uMw{OW-+u&Jm%'z Mصf\= 5|P?/a9^\K`ӌb $ LK$@$@$@$p 2v" 燓yCr+i뮆G0yK{}/$ 5[<✕_NAGY6   ![Vugpu 3]Ĥf|F:Rd {0%m?*˖ZQ3%WĤ&B   4t^w<u7Ƈ'q]߮([?I{Wc>kl~"':M~"\4ijw'h=0[qE9 eUthUS(X^Ë»-Fע0} ;_БY"Xa.=rHF9ٸiPlc;~^هvD]# aJ Lj6{;_|g^#qG1!aX+WQ.o #]D3vcQιhan)#D#ۦõxOp,עy!i?Rei:M?xFVgΜ*\eӢ6{W_[v7ap?g l]uy5ONw--E֡}fQ"Ž&Խ ܳ_I@lajr$~#'hKx ;D&}`׾ȥLIji<^xou]"vu+~>~кHsD/#An7 pMyma1rsƁ[t~J|~NOtv[S=^{EqmҗކC׼/䪋qqF0苟Ǥ|z\IIdq3?㝦V4.dat=/W]w;||Wqk v>/>wo#>.]}7Cy=vhoūG>ş~~[ĝGnGUp0R.Xt\ 1@zKV}`+┠>#O%7塇!IHH.tlmؿuV>}t8)>:d` v0#UBQl!{8cmĖUظ)ÙJ^䊁;Ipw^63;kqRQ9}!pfEgaf.g|q3)doŊ+G)s1yx?z{^ͧ~`R0tuR.[`@l Ey"^i}ł+OQSg`ަ $_DO@VLGKeM5R=)T+&pf<0nň\VKoq'SGB_Xikd[ QtxھnYlO*dYX,Iߦm4{8OPPR6nSBnCвw'z}ƥO4O%ۄWc6;񵣤tp~:ާs_m#!Y5m].9ی6H[96[ҖkAu&??^fbx158ꫭvew/Ow󝦻<'pܵ[@_sQ|wO#9o͘~A 63Ծ`k;tuD;eW`͟a싵{o?7\^ߋon9O7!7_=yK~4p댽ѥO %*e ,"bl1dIC^uCrrwכrK$@$@$p=d xPϙ8JA圱1/E."^ a֘eۿ[`f?}(I:4)hU)Dl*NImIs*'dűeSIlTY%)rtOvNBT~ jaɉW.|OͲx}ϫu78Fi `{5dL:ÒӞ9j91:ӆ/d۾ߚe ѕ}Oaj#EVdտ({xu@9~:ц+fb9 [ݝ ;WaQa9pl,k;y?msmC~`{ p<(P_W#c})3f,Iuq,y W^_ۃڟ휙'",SLvW0%sXi1S$|?yh/Qy`zAoON),;3YqߤfGHHH;tlRq/M.L|&4QpȘR3d{^OlF)byP4.cZ"#)]|oG":YVQbFOB֩xs3xz uU6ƪ0_ Qߞ5\)gO|Zv B.L^}A{qۗNbbsNMt0eR "uIX9k2&`1dI_k;딑A X/baFf w}E/ZMcES+Q:THxCGOYoe9S+*z^Tl,Fw%ĿĀ|q:7>k 0 VMK[F+yćR-DBhj SS@~T,8a{0G6b}Q QhLGZ4k+!Sdd6F⩋0y:6SFUP/)V:&ONQ)*KrˋXiŻ1lU)Է@yLGtjYykl6مPȘxҍ6˧ -4Vv𚲨vbd|,- {/K1\U[)}RP8S>g_suXӐ"~*qڎb6Y>i {{jPe$fI+'/,9K%g}YD[^\ ]xal޿-?ʩ;tO~Kmw>oA{:t[ȸA$@$@:C68"B\ɫ儹5=Fk[VlZ)C,U]K]h%;1'vQMs>6ՠbk¤^dvmáb0S_Q޶h٣痔I%]Q!2rdVE߬ e⺘8:|(ˊw۾3=M{MuղC3N{22`paoT%2UÍ(DvKl'_]eQ0tDECZSX4bh-VKN҇֏e?MΨ☉*T.DB~W R|%Wu+=l9p {o=%LHX*Uh" ?a~ GNgqJ<(;}"[MOb;THI2L kMVĶj?}s_mC;j7ʬCA* 7=33Ϡf~ڼ|plܵFߘ/ԟRd8KFߨ ֝(r?ǻzNFjm(Ûne;>?}:r cQ7/<>\:hw{*CXaX7uG 3G,k.#/#d PpČɮ꫖9v)Rw+Co*Gǻ;$@$@$p=dc+/e;_Q(c=)QZ%_k4rQExg|eT0ixRzQcKmL2%}:w"XehscKmo]>Q3Gp)eik1kSG9ۅ)'X~>6حGȂos|L>KCp4Jq,Nhߏ}x"/Ųƀs }dXFl[w AddSNw(R?f6 "鍬?w#c[3Iݷ6-qOIImM+>U8=|i/IDATq/XƂYgu&s}KmrO=$Xݫ*ǵ֧>Rڌ 3 `=>&Ͻ\2Wr_g98c=U}akd/V(7~vv+:\W'1G`~'1ڧGc2n9"߄sWp!gQ(/k7;{)I;zy(}##ՃY}= V$@$@$@:Ccnӧa飔.%(!W%.R(P^n~9 l!+l")hH'\NR3GoBb*1d5.fNl;,ˮ9\aױ6ڍMR>C첹 ˺gSC&ɂ#BNW៛$<;QAwmGa)"ٲ)OB3lsfY"\3Jˍu SU¨|> W^v= ۔!2Xd޲|5nIL>.Gm!G_箍h)yM_]8I,Q/G=1ם0}RϿm$yFx.:BXߪ76Z~J>mX6)C.hVC)fŲdq)q䷗,zGBqjNng9\3ā]qG3+}GE$"gp=֢y|]2t5aזƍ;Vc " JJQ155ޅZ;IZ(2D)3i"&h?О-(_lsU7˓Hk ~Eae65w` _6Q{:9nf嬑8KXm `vbƼљW,ŝB`u>/]g`x rm$椷#Wu(pI-KI]9vlQaQ~2@-QVT, :s]PuᵯGgAfFǻ "  ![=[ƪ#L_, uQCǨfI.fn-ؾF)k@TaxeS'r)*=2DN1ʦ Ci#ftzPȔes:M'LmSzF|jtƛ>13U_NOJv1ΐ1=#puʌ%.8utxHGH X~ oء~E/*3g셰S+`hntR@ߜL!ڸiD`^WG`$*#gu'}T_Za:Ko_]{,WcʅXu#&޺id~zW0f.L[ S1Al8l5U:g%{$ :\_,UCվ.l :{6-.ft>_ӼD]bl>=̳n%5XИwM;1vO4,C͸A1   TwyqKY<-, !IxWdOv0i148i/p C[plWCYV_jcm!ͭ~.++H~ AjuxĔ7BֈB`1緅%&̦Ȁz. 6Jsfژ߶f؀pKXj bO@sC6=clsw*S!NQK$SN'qL0 *2Ҭ=?v2 '?޾F PkAŜ5ֽxZ+\VJ7Ns]}^&uw}⥛aѪRYFVO[{psS!x,:\\Kޚu.ט8w_޴CA}v15}$$!Y5SԶO?W}p*bwboFsOb/M%ƻpp Zy5 @&=dk%㒷r3>'-*qVƎ)i†aX"Tʦ02dө [/Ϛy/#ˈgխ8 >㪣-J|MH1R?ƌ2$h–%ꉘ>~muVX Et3(wx/[|aX.`vs8>ݰ~,)_jMQy!r pK⸘2ѢI  ~-H.w8 1wՃRLvIPepkQsZ=JTڥǥ Q2XV￲ `íHa6nL)C{hMWi/H_1$ڏJ׹J{M FXlJ5\ykOr6!S|p@([m.YLvwx xW=nҮ[SWp&=]P⻿gW0/s?߶Fs#~{"> FS7vUMy}EA,Z I0ߎ}}' 8s]&~c߿}ˬssEN9yANǻ$@$@$pA=ds/> $hץ ;Wʹ2eܲѰ6nJTo48F",NKr?yxȨIPhǖ,ƽC3rp8iʆ ͳ?4܎%UlX)G n6Ɣmo`ϖȥ-TG(U0 sgGnTIg(qU8ԐOY-Wcn=ܮ(=^$xw48z3Ƹ}=Y+V+2e ӄPQdcfJAO͘ѥ2"ۂûc7;HVK#gCGBj*{TL sbs[~bڣ;!ڏkBj'lPAꞳn9wՔy0~va~kk(z25.Jas Ok;%ĉw?'ڒ[.wh*?>ыP1i!D*ۃ'm<3Z>6d \fr8*'X rdxoص m|EeK'ah8,   8ǁ>4h^2Ҫ!;Bl}žw{! TTF_(Dȯ9b'rP8DOGɡ68Rĕ_0_'řWTTESJ#b\SP{?o/YNMLGŸc7u+R>ᷱ[q31ktԡCudc|44bi֮691M'NI1n9(Th.u}q>m~sy Guѹǭ Ȏ.HvVr]Xm;,9ne9Z#CY=x5Z5.(,€ѻi9 |P׎R)@~#Z?;>~P8eCMz7jj\a1Tpkb461eɇÙhi738wq}P"֥UKsQf̅^'hs[wj&*DMct;34V@0wڱ}7fwKʪ$|e}RRˎc<_~D5q,; =FLôS7"z/vljcXqqF6]F;Op=xFt!f)$>!W!蛴]9Q)Tj{@nX;W{EPUCQ3eAhp1fsa Zedތk ka: P Tγ>g2 \!ZFP{/ vV+/V2 C∁/Nat%33oU&;;r+ݗtw_0GfݖڬSw7_W 4Ǖ$K}LTQwA/xNFm%Gu?~$E5]̮SV*u۴՛c7"+"#R}pU$XLv0 KlfA }-/vvbl4G*TOj9s_;F8 G"4mCs]NT=TIj}u S@ϒǿŗo, k+;qNߴ9ZOhwËe7`ip sodfRGwbcnƬYփ׹/ԪX>@6c9Bh9vᄍOy'WbfǖYx״1P \樬KꝒlw}ʖUap8bxHHH t8&Oj6ؼ՗Q0d &|cfV%|7yEߖ]x|jFWS,XЧV6[O7␋'`1M8,΋iWc6Uv>=] ϭX1iq=ḰP-ݹYN-1ї́q%Zᯥ~Ը֏"3a!οu6>\1fC/- xC`VP4JQBbF %Roi>\O*s>Nz l:]FEhvxjuqRF{`J.{?I.z}h m%Rw#~$vFFmy): `mA6D^Aand(]͏niCœ#\?H%,{ڸ3x}ȔheS"~slqw=ԕ-*ڏ({ ςjx=gxu*!VȢ#},OsP$y{y_0F]~9;SNI\r_Lk*N]uXm<L{ Wc/:OOMC^Zxռ>ۂmbK;B)9v,Obnƀ.q{>o>]1A*+g۝/y#qw[os=ݻ߿:o%sAbqcX}xLE`$N\ׄ!L{], JPf֘kI{X]ۑ7IHHtgZpNFY=+?ٝRzXjE>3Qǁye$Eee!-^ZpQ(/!o^;WmuqgoAc18=ZqQeE}Ju0wEGhij'I^5x~%>NGDȟp y=#qz=`̘H&_!uvȖ/Ӭ~LXm;,9r^;!#^Y?ކF?%uZvL5M0fޗ3'!pnBċ"fCKs}uxF˘HHH2a&*I$p>8,RR928ꀗ=P(,9IdKH Ќ hS5 |*D$@$@@Zv#Zk0lh?|.ÒDּ.-صqlc0w]e"?g;Yk\,]kr?}n#Z?Z/뎢A_g!  L&@{&[ :$THHHHHHHHHH ֣$@$@$@$@$@$@$@$@$@$@iG3 "         dtg; @=LBHHHHHHHHHH2l=N$@$@$@$@$@$@$@$@$@$vxO;P!          L&@{&[ :$THHHHHHHHHH ֣$@$@$@$@$@$@$@$@$@$@iG3 "         dtg; @=LBHHHHHHHHHH2l=N$@$@$@$@$@$@$@$@$@$vxO;P!          L&@{&[ :$THHHHHHHHHH ֣$@$@$@$@$@$@$@$@$@$@iG3 "         dtg; @=LBHHHHHHHHHH2l=N$@$@$@$@$@$@$@$@$@$vxO;P!          L&@{&[ :$THHHHHHHHHH ֣$@$@$@$@$@$@$@$@$@$@iG3 "         dtg; @=LBHHHHHHHHHH2l=N$@$@$@$@$@$@$@$@$@$vxO;P!          L&@{&[ :$THHHHHHHHHH ֣$@$@$@$@$@$@$@$@$@$@iG3 "         dtg; @=LBHHHHHHHHHH2l=N$@$@$@$@$@$@$@$@$@$vxO;P!          L&@{&[ :$THHHHHHHHHH ֣$@$@$@$@$@$@$@$@$@$@iG3 "         dtg; @=LBHHHHHHHHHH2l=N$@$@$@$@$@$@$@$@$@$vxO;P!          L&@{&[ :$THHHHHHHHHH ֣$@$@$@$@$@$@$@$@$@$@iG3 "         dtg; @=LBHHHHHHHHHH2l=N$@$@$@$@$@$@$@$@$@$vxO;P!          L&@{&[ :$THHHHHHHHHH ֣$@$@$@$@$@$@$@$@$@$@iG3 "         dtg; eEu hA'? bhs ` fg٦&@ @ (ދ@ @ @ PN @ @ (ދ@ @ @ PN @ @ (ދ@ @ @ PN @ @ (ދ@ @ @ PN @ @ (ދ@ @ @ PN @ @ (ދ@ @ @ PN @ @ |6ժp @ @ @zw]xmr @ @ @@xȀB @ @ @fv+k @ @ P@*cb(5\ @ @ @vQf0T\P @ @ P@nKd2iqi!@ @ @xfvW3>A  @ @ @$~8uH{t:U4)Y @ @ @xsu]fOٙhF@ @ @ h<vK )|7|~ځ @ @|@۶m:KkW,[b~N  @ @ ElW׹p8_.{<^.:!@ @ @&^ǻyGzM{^<9LXK[^XN'i F^AWr4#Qu钫J9%.{q54A^p?u5 W^ "Žk;7BP. B'E<F!O.Eƫqs! #(,± _uC-Pd{ i| Q8!h4⍈CEk O@(V" Z(ve4`|9j/D0@(«`<5MP pކt̖ \#$<$"5x$= 5"d0VE(+"JZC5!Pf4E@\HwP"ZCuD,. @(‹ "REfo;HsE,JE( p&,cۚncGSF,ݞ*nuvP#m= <[n{ăP8I$BqwN.]ZB1հ;šm-6fV68h"ûc"*s]&"+e] WE {ֹ 7A, U"ъ@H3gP%}"1!Z`Hag\5XAD%ي"P6|Nͽ`bqHc'9JS%8/23f՟x!Vm*bP85/imӧHV;wX\d"֐63IJK^$/W]3zYTp^d sݰw<ƸImOEH]$.2rC ăh[08~R~C (㴙Ox3h_-nXAje`B("U>psEuh#o҄K`FעBB /UId倦&V!gbm4DrRT"xZ"У $uxo_ ULBfܤHJ(Ūĵr]>9Lb1BQ!^:'H\}6; (+{"CCU2)c6A,2MB($bB$İ[;H, A% X!3Z1T=]DG{OmA /MBUF+φۛ;);+>˽اc(|v2gU R6_o";ibNlF++>GH2!+f}zKʴbqoL" G٫xP)}k?&Qdc&vN(d}"3_QUQ&֜ zIl!Gv@SëӣY!F[H;{5o}$Л q_6WTWPvEGPz5Bhٞ^ "j5zc).#WERSPxLbW`6x%B6Z&&र\O&[G(bѓ<H+ͳjBNrvfڹʅX&\$*DIOGpaE`$8Hë@(t4Z+x~VI\6 šǟŽIReV>^dOJ;B3IޢJ <9ث33@xTۇ|.Xe7i乖{X_EMibdBe1?dsv/quF¤/~׿۵HۗOk2u! ݙ?7>OѫPyf3uiܯxexD0ާ~hFGCQAEŽ_@dcU>l֧E8x#/"l E$_C(㺑zAEV Qv 7a9VX(fh$¿H\* 3=GP pքIHg q-FێW“۸=gv*OU-m;&a&rH?LoPxu`s}B"?W|K_:དྷ5B8'Nµ*K]((QEeP/`EW!0 @`6Bt7bQ*@(RyNwYܾNݭy:\rk *ZJ{E(=b>y= Uޚ6p>TbB}GVF_Q=7؉9 F9WqE+,<^5 E ^gbYQ 8%k0RWvQ(?1dvO374 E8&T33;\bz.=9]{LWP, 3=iz߻3%΄侌V]!ALP$]CJk?KܸPS,S*R< #;RXRc ׉Ncأp}\=K/y#/\?C~Xi \boDykȭl㤴-B1ocN0<߶ckQk{|ڼPQ\(~mO;3<#cmImTzPe|~q{j 1YkVMGȍ *OX`\!ؒڍvr(Xg?^6oQ2jc3J 5B?Z lQ{f-ЅClŽ\_ěhObi@}2֩;]磦 _{N C#޼ aG /-4g%YiY[<< *ڽѷ)p%^N4x>B!OΫob;֙{j;b(r*j"blhBvy3*jQ̐"=_A[Ei[.M~ ZK _U(*W"臬6nM+FFɪ2(MMz1z7/QRI XdP<&J(,zBWka(UB7cUpI;bj|B&RE(y>s7L%w ߋr{FT׋egA63+֤ibmhB I҄O:!y6/QyE7GaiCjA$R;CrR>G]DB!՚CPMK])ċ:vqݣWv/ HXz`e,> b!sVHHq+  lHXOښu = + ABNYS&6aMJ( !=%+(7˦4 xu[y>H4151 rff%|LͶ3DeگR{4Y0ՃgB5"H6KrO$BP1XRS,s}""FPT)%= ni36 b #uL wcG&-b47{B)G L" Z|)6b [5[2w( #Ä:wv%8ʳu Q5&|6DBrwQ%ۿ?-y%^D1C)iB1X*J؋EJ HۙyN Na}hs30+R(Ř'X@!XNTR$ %baM*K=jV$ Eb`v}:~׵!PH,DaY[yC0>Drs&7* )I@( EP9#ВֈNB V"^c}1{vRMkE pH0a #sNpD?h k+ZcVf FMk0)  1ZHQ0XIWΒ@(7"1 >֭l@(r2u Ú%BwıKqP$P4Ѩ24zs;s8 pfx2V^%mtDu < sTa=D(`(tslI7[f) X{HD'B%tTm'MnOsOʞ6%ϸ3t<<>7~q ={ͮ?&N!#}?/4Bv I PH'1<lC gb~!űؙfa2Pā=9_0xWf>cv U@ {p58 &E~UvG$6W0Y,i1@(F(aț͔?"OMS3n? ^\73AJyl5]3٧" dJ-/XyoGl^d?Iw.F{%]~]{V7 B q15ő?G3u1 e_voe:_?meLS(%tN,$tX9NȯÌ޸5BA( /F^:_/sI,#y?:sn<0B'Vs_%2 SW8"q]8h&EL6oFVG>szhxOsd>5Fn=z{1q!rw-侯<.Wzۿ2 ~b8!j:EZLlן FsHtZNMٗ~8_ۙ#ڢT`aY61{-*`1>8$>KPrG7e4'LėϵAz *)++'!|K9Cf)P2c(=/&oҨⳖSses֮ B878eœxL(X11]N:HxzoͱFwAH(9׉z ~h? pϔt0*M=C(kl'~o.1|ܺTfPUi&.!#+͖fG3m5?ooGx';1{9kbXi;Y= џ.sb$@+  nDjp82-ˬx~j[*;]M=/qB< u{zHL#KvOx!CeNOZ+G(ڷP13Qis-bu?4P3O&Ģmg"lC;#~ m`(EZrיخPw⩽<݌P ҎFw {v oB,Rޡ@EԘ~ 2BUg /Ě5]pM=]\ce[}/<#ewlv4wӠ#`Ö>JVT)7IENDB`././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/doc/source/admin/appdev-guide/step-by-step/plone-ready.png0000664000175000017500000012777300000000000026133 0ustar00zuulzuul00000000000000PNG  IHDR@c iCCPICC ProfileHWXS[R -)7AtЫt !ˢkAł]"E`ʺX&t}{Ο3s'3;sP rEfJj (@Pds$QpwyY^_hryH,\ 'cຜ|q!7^/k!A XW3x&!.bd*-&,a5GGW(x3ľ AN:8fG"s9H0*r7;/ReD@%!Wn/ҰD}G 0@  iv;r_hF 8Crq xx+O?l) VzX6 !VM}T,Kd!~)S`0[>U(HSb)iu3򠊆J WeJ]* *TzT)+%EY@@9H@y@yj:QU:_uaK]ZT[j u2UJ]AC=KK}C,i,ZVM;O{DFWsP WSTUR]EB_}zzQk}*l'4nk h54c4s5kӼ\eZSV7".z6QJ;\;KLvv8$::t:Òadat0>2?7j٨z;Z-={KS/X/[o^C}\VtFk]:{A,-F 1XFYFkNӍ}ka3sM~0VAS+DӅLQ<2֚5O0m^c~BB`ޢ⽥e:VVVV5Vi~U7m666[llQ[W[m5;NhŮ} aј1E5] (u/ǚMzlد9;i9E8-tjpzlqtBs qRj8޸]'.qmt&v;nGrKy'=?zyz;{Vyw1aen3cU=fݬg6Y_8 x6 * j N (4R:+l!,2luppNxux{ĜHjd|QQ⨆ 脈 k&<Eŀ51cb bH;r8qi%$Lh(MlLROT>9(<3elʜ4RZRIM:dr)3\?5giӎғfǰ39\w-+=,| >apUVXֶ1{rsssOD٢<yv%^ őD2ER_ 9-RkOҮ"ߢʢӓ9C4ee32 ř8d]s옋͘8ly=C]@Y ]a DD\r{mK¥\m\[ẕr+?;+ZWܺJck&]\[i.Wض^sCԆWmIVe@m~Vփ m]ΎUU;;v>ݕ_w.ehO޸M Ak5'o;tkǑ#G=Dĉ9S:V^|zL񙁳gu7Nk>ͦM"/\r|K>N^|ǕnWk[\[Vk<Ƿw܍o߼z+V{Gbǝۓowy~7{E@xPPa#GUǡNS]A]-tx"ygSӊgϪ;??Ǥ?z^+S/_WKJ+vƁ؁Gr /aG͟?=y/ _#>g =R١ %Dq_#~&xr q>Q6 XuqiJd8+bQ ah! }l'" 1246 320 iDOT(Aqr@IDATx tT}ߞ)8`8E)MKϊCQdU5P`bk[ &B٠xD>.`-JBhم封&tsXoHئkc+baߙ3m sl4/}s?y'n    Txd    @J@@@@x+*Y"    @:    @ ,@@@@ F@@@@@JJ    x     Po%@%K@@@@Q@@@@(%    ި    @[ P@@@@o@@@@J @d    7    % VTD@@@@u@@@@x+*Y"    @:    @ ,@@@@ F@@@@@JJ    x     Po%@%K@@@@Q@@@@(%    ި    @[ P@@@@o@@@@J @d    7    % VTD@@@@u@@@@x+*Y"    P[2D2rRҔ4cƴC\\gz " t7kgtHSAx@@@ xUj{=~~[=f/jZ|rPܦG[N^rnq{ND(Z<9?ު=o ?a[T|vlA @@@@n_-ެs=k4_:su<=y6/*$Kjx8a:\f7ibX]?P<689A<7Ѐ^M]!^=lEmZP-97ucTM̜ieݺ^z҇,͜òԋqaqO L権}h֬X*6;[ձޜЛsC_WrujSܓ(@蜗9 ]bBv~F.Y}JN~=8,:n_@@@#&P|kUѺy34Rj~غO(vnϊ Nڰ0-{m}ɥn}n<ʡNn/ұsͶ\Uze:%nM{M);oٛy˖zt&\ WLuԴ|o/΃kZv)Iڞ~#b= CG=qnx_Ԃu}Koc e2LU>;h/hyfN;orۦ66Hf8wbxxK^;?f+WO.@ߣ.["dX7 Vm䳩nlUٸω[?o!   p\p(z.>xY~>;VTIY$;/W\}=ZͥZEY`J}@_|);,3\1mҦT/4Vq]c:6۪}Z[rppͥI~eݗz 4i'{mg}85p7zVKΟ' HO#@@@p xbs{tF`S$NoO%_ٳᅧlxa=USGY/P顬:ӵF ]=f>2f|X?35tluzwpfO!s[䞏9 iryzS)l[ivy{B|V@ / /.7<<" @@@B`B޼'[3]r#) %NMrQ3j?|Fj%5PS\s6~57U6#7YZV]Y94EeY޶*mM-w-EcLj@:@_VlM:oΊlESggY8x   xqEǡjZ%ٜc}9r\-9x^-;ug'L+KMf>3_\E!u >>ԷÑ[P 4WKk^a輭j!BpD" mzukKjѨ,R/<6߷Bf6Û-վ4ur=OxK-ln ݽB{_}SUϙ6NcǛ   /oֹ5r*&mѣzj +1zAUe^c'Y3VӦZ|+:\=ج天8Eٛ3/[d@m[6HsvPj+.85d{9<UEf$uyZDUnױrU|o]a9 uBelmkla ܼ=G~w+7!w@@@@'poé"_~Y~]:})AݞjZ NE!W֫imf^O[sCM]ʺ_7t_jǮ\,uQN 'Ί mLVYߪMu)}'5}=VE_޼BcwTzi. nU|iV\G=?C3@3w+UM4=Q=O[-\׿3ҏk^4SN_XLs?8S}YeX @J୨ @b@@@*< Sk{sZ9Y!wlCF3CFݞfΫAm`QV{6׆gyjtX1uUj ̓A5" NȞq|tQ֦ƃ2?to/cG5mGR I!_U}WaSO;~ūcj@i6X]ҽOVmhiݞn3Up4WoGo;T9!   p48|=>:7cEs}ݴgj&g4yy--va=.:f7UqC`a{:U~8;6nf9Ѻ-o9G*j7VoXlL(23Uԓߚ?x dk׼[|O_nc~>{ͶyfEMOkCp ,M\G:-EYW#{JvtܸFsdbqOBԩz߃9{7Wt}x}($L&J$2mREJ&4HZS42 ݭMbf.g1; iÃ|+v끲Y.@@@&x+xSX4wetϩRV,]e+~-uU+ԓ̝+!O@@@@N 6nni?gַtD_묖y~תOsR=W/U;׆YZt#g@@@@` x۰;3[j%+f{mn1Γ>=n+rC@@@] y{xui*;ݖ|Uc>[X!3*jS`Vxμ@R"   dsƩ: ^+D6Ú3k|{%kG?}7rMy4[@@@@`Lw]mLJl    @ފ#9    x+D4    )@H0#    PBH    @ފ#9    x+D4    )@H0#    PBH    @ފ#9    x+D4    )@H0#    PBH    @ފ#9    x+D4    )@H0#    PBH    @ފ#9    x+D4    )@H0#    PBH    @ފ#9    x+D4    )@H0#    PBH    @ފ#9    x+D4    )@H0#    PBH    @ފ#9    x+D4    )@H0#    PBH    @ފ#9    x+D4    )@H0#    PBH    @ފ#9    x+D4    )@H0#    PBH    @ފ#9    x+D4    )@H0#    P@IoİI1͘)|=hƌiE $h;2mT Ή 0YÃK+/yGz衇95τr*rfPā u:" .wR}>-\scͪ}EW__bg-H\=ūw9>5ͧv Lk:K]ݸG- ]IO߹kQ[+u#4/N*p,3=?c&dP2% y>hb>w% @A% ]ߠ_Rʵ-) :KRxn ʽI'0|e7r[ୁۘI#0tQ U-VG_جڿjy~?$ۂ آ~C;.ek/{KK}J@ Vn׹ " 5.-xUpB "n"wNrMڻjzZ3q 836VgkCZ{:hQj4z?ph<I}>oEG %Wvsi?%#SoE1@;V`oR]ة١8R1/fQ 7|[+s=>}f>Pln:rW^޽1U9|`p^`,FM?W^nmw֡;3eg#aRڏhzXЫX?}׶z5gsy"0pvVlmz⒖=zgR{OkΐU;B})5,(3w{MotM9Se|)g긱WR KJ{ ]>ֺ[4x֡句^k?6 ~Z|P_?}sgNYh25@qĨ}޳mH.8)rVyZ8)s- |CzOՍ~vϘrd]@&8ޤiZ5ĀɗLOgA6 5Z2ۍt 6gOf~j?;Sc״nAR}.u%[G;D ϩxTlƕ 4Jͽ#9"NuIխsW˪[ZܨWxa:7ܪG_jZ>/BpY=)-HR_ܬQȜxFUgN-恋ٗ!W_|^~Y7ZKY_x6U,Ҵ},~?J`N6 zїd?߮??jiץ/W_|v}FdYy ׅp6˿q$սs_^9V4k5(lWUw?z"8zv}@I"poN#$}I_BC U.^GDnK-K!g7EO= jP.A!CҜ4ڡ;-%ַWn=f{ZV?i; "\垬h>5?LRq@%po㶹b)k;CAυlk6nh{╪,_%P8NJgOXe^*2 KkoV*\]M ⩇Ѓv d%[Zᦉ+ Z_g~ jmsmޒVo$t?ԼJ \V뭦m!X=]G)jOg団[O ޹'\_OL}smE?t}'oyo? z(W#*lTP[X:g!{D̔~~֮֎vQ}z:=խ7vz_vWݪ7Kɡį&ۉNmEM_=}2vlLN"xT_ffϏWia Y;6]ƴ.ݩSFDlpZSO>چ9 @7fsG@/4/3"zG}A=-}H'TVXx}L:5semyBJu֞uob&Ŷ2'rgGZH 60_I3'5J6y%|=xn NF`K59tцKlT@x:clW W!W퍗6q|) S k |t$6?Zov~J}طmIu|MsRۗ.S%{jm9=bG/Ι"rq tzb*ٚ1~88?4`тE*~>; Lm/#,^GB oZhόZlcٗIy޿^۬Wo<`EMCTqתl%ӣ>@$.N58q">/ȐE}ty @#poa+b}vΉ|5~D {+^U^C]>j"qQWkZO/٩OJx]1=Z rL>j3eV=_G^<%f{ 1z)},lڣ[=+fׇ]_}W_?}:tkq6DvenZkjLuhyzrEq?x{"0k.2)o['>/y^/E-w/ +0 ; PP{)VYg>`b3{e _C_Hۺ''_ᡦ5mڼ}J8Һ)Sl<>nNv`ǀL[<֣ڹη C V H%S;58ԴVg>gvm mմ)Ӭyp oyj)c7OxKM~\_[.P𪐹<^Kt뜳xpzxpzڰvLcYqoPx 7X9PT64Դvb!ۨ=/'6 L34|noe/ )07VCʂdIY)mqR/ _6(o 6w;G!gպJ:M\>^M+gw ">с7.=w|Q׳ξ^ndljis|`;Qb+.nlk߿St[gdqC؊z kfvT5;Ǵ3ȷO0 kyuXoU|Fύ|1L/LϾW6Y!J-yf=[Y[2jlz@TͻnVt cCev6tfqwAvE.Po|ɯզ𮌚MGo:G00o玬K/h!O!~ 8jg65qSH R`ovyTi+:ۊNiǴr3a8%f_BBXJɏ*f-yjlRz܍ 7/W䬊5ԫm;V]ع,"_K[^]{7>3?Hٰϟ_&gYlת*u{-Srx@gra@_ O 0@h&u[; }MOm>}:鲟:u|JSh\'>U6w}lm+LSFM \QC;>oD6:^/wiƝUh%crò\pJܶop 9sruVO?:4zqTsDZ:x5l}K zFR\qmީGVr_y]fډy^Su1moUCS=)~l~@Xjek=?i wN춅Ӽ뽚ڵ9>܃,$ LLZ)bf-eSeoi/ y0F`ް)%`}/\2Fg5<5.s~ ޜcǣ[Bnڑzk Zl伊Q6ai!dʵ-I-9uz~U 4bJևsZ/ n-1:*paI+*ԉ / 9/>B=6m C>{Uml -j<ݫ3F'~T^o]2=G*vbh{ҷY8\y,e;RYXwhUWc/Z.46W!pNx@ %P[5n:l]{2hemS)`_ j؄5ڭF'ίi͂T-0#C/ujWH_a]]bZm{\KNjT_kmhGsǩZ!Hgj[„ݳqǴy-9vuMokL k=˽'njBKgبwHO"n8=/tn7*a;Դ ^9t: LB[?U6#) Xvj߾ csYZOy\6WWjrL6{L P۬mcC$}_smt)*xfw" *ҳrBs.ډNtOJQ=GVJY|jGmx |68K]W;}8Wn ?=fx-qS @,P[)<14xKoTfN[B7)N6qHInJVM*NzlVlR~V6 lriJ 7ޔUiM=8Yb VthvYM*#@P )g5~j>OwgM&uzE)þ=אy -[Đ_zULJZ-iwoL>;+s[nL~p onK{:K/<םƞԬG;+m'z)L8$kL >obXoskO6E A.vs(T:[!%[F5;,5"ZkPl@P[siP  @"@m,jlsW kY6gSx @ / E A@@`\+?;&մy«X:(3 w@h-Xz=LJ BۭP$J`a9as5@\ >TV0 87l~/6\ƪx@ "jm5}JLQ @ &Y    Nۤ;e@@@@`2x g2"   L:oQ`@@@@ @m2%ʈ    0MSF@@@@&p(#     6NF@@@ &Y    Nۤ;e@@@@`2x g2"   L:oQ`@@@@ @m2%ʈ    0MSF@@@@&p(#     6NF@@@ &Y    Nۤ;e@|EgIDAT@@@`2x g2"   L:Iw0@@@@"@m ʁ    pG xN'    0QM3A9@@@@(wԁr0    NoS}!   5Ś"   NoS}!   5Ś"   NoS}!   5q@IDAT |UՕzA@ALel;bDjƒ1$:5@i 5+ P ,kdoS!J*IȵϹܐ~>_k}o>K$@$@$@$@$@$@$@$@$@o`b        Hobh        oqab         Hobh        oqab         Hobh        oqab         Hobh        oqab         Hobh        oqab         Hobh        oqab         Hobh        oqab         Hobh        oqab         Hobh        oqab         Hobh        oqab         Hobh        oqab         Hobh        oqab         Hobh        oqab         H@ >MDpq""h/K@߈Hq75ASNs:w 7ᤙ@ 4;{~t Oąݺ~q+пg Fh:YphHN̫#?T}{A 'wy. G@S:k.2^ RuF.|/R˵jwꎟM{Kg@*'FK{>uwDL4CC%FCFex%PFJŰ'{*D~Ż)J6ayA69K㭗XRZ <0 w)R36i'c (}^^Pjj @cxݴWƗb( y3g㎱C]Hq|_|\"RP~crJsrYߜ}v }v掓UpDƉƪ?%Ϩ a4{ᮢ2Ұi~+x| 33 &bӟTbd~{v67aHMܙ31>jv˃RJÊm1/p|6><=h&43u\m)~,un,+AJ8܀^BAshlܸϿ^ d L},qbs]vxк[O֋.AD%n=X\=sZH qN'Hܼ%(SPIoy"x+8ooVUuW^y SFjj65ہqك)]&VX!A#gm 0$-*0[`<ቖhmOjy>QPqo?Hv| n~ :뗬ci<ӍݶIn9z:ΝbAM{MJ$(KؑjdO)玩yK`XTtUuB*lۈa^kg. i]̜'EmooD>eDD\Vq㘯c].at:q\qo s6 Ɍr`i8m%)P]({6FRp(43'x딂 K ǖ=8\OJ2v@cC=x|" dS*H[ S-aboЋ_õtC10_wtmQ^f`ǂ_M >BԄ'xMK\Z׵hh=甀ܞ\\pE6Qo{QrλΩy+DP64.4~i`$>0K_A(&k҆645ƇI+h &.9>~ih9%\^ zGX1JV)f]KAn8 r ?DzXo~- AYy;.b>Yikg߆BL[js"s)bSK$tav:RU?WTDJ8=;&l3>cE-TY>s7ң ^h8H҇T11>uɏϿ_BŘ[4șvY* svk`ڔRJX+D3RS.XU#Xn;;7HdrZ{vWr0@'Ç`|T/?̱:ŝqiɦ8ya :I_BqjuWR&s c}wG${E@#b""֖iÇG>FڧI᭫;O>=Ev| 2Јz \8'P5 6#MHe&:>@{/sϚ#t3F?bU$:SwG![b7qa71 ,Ivvw-✾\PeџWR.ꎾ{}ӆ=sQ&t~O=z8A-z>kecw긼S:;,czoyQ(=693󖈘;x\)E} $lVRgS|T+HAO@+R}d?*<;D/gj|=ӊ&9&_yw%%$yeK8sKqrl,.׵%`a3m@q8B~Ŏރ9#atsP-m^=s0Ŕ}_ٞ<*>/$Mէ?TkNxXo_iV^|=݈ƨ"n臻3{^DPjv|%FG/9*~r\鳻飣xrˇxSy~ D E: =g=_4 ~w0b{1a)_T֬^=WC\8 ڄ?,- JIĸ$jŶ1 aaU ͨ 0g2ԁ®274U0/Lb .#|(G1/sDH~6Ǭ]O{5 JkbDkDpڝQþAV"pYZDeE9J[tVjߴ(_Pʖn+DJ>\ eMRVm ec6CbL9W7,C V[e" ,~O"ۢ*~g|#QOY,\[>o|T5[`0L<]{-z&VCw dLWX!0i]!t /x bf=5Ev[<[l鼥ex>lu/Jƛ,0 eQ}g![o*<%2e(ڔi? ,Xl<ȉՋ3E;ʚQ& ׅvQVfP5H}(CMƉ98͜4((ajsO1/G٤LW]֢* $cUX>jCDܻ|0.;]Yd²bT: Ӵr5q3F'Wnk-wM+Xy~/w13󭼻3;g4S65FټVκ4؞<-o>?CM<.][5EX?ºr@\jQZׂv/7=a>z6㩵wo|ݓʠ_*nX&OL3 Z/]|Qԧ3(M5RS1C&S5@#D-^y'uU6yFmyAdaM&@".s}fx߉qsز|IlYF*l4zr9$MXsde|ӏ{M(2Ϋjy|`$[ohLFOTTS)_[ܼ_M(ٿ`9N9&jҰ f|x!wԑo: N| z95J(qZg+[?xb#6gXeȻxЎ bED3FS3˚ǎ 6 ƾg' (Q.3dco <6SG.jd+&<V?jfn,DŽZ)Sl.6mjj[KA²g'g xEyq/ pCG@zGwWᤘ<>)夯NM6GO3<Þ=[3^I_L_cʿm5~ K 틉W_nbcG"ؐHGve=1}%{;5]NGPޯLveW聾|}#ijD5d%. O(+W`|0ԡE˸h {ⵋ(aؾ `7$M2c2a{1b@t<J(l z8 J 3NyU:W=oMxҢ3mş3eTM*v< 47]# ec|Uoa\5R;p-˫iO+F,.N?{uo΍(WI.w#oլFis34~jad."bٹ#O/2Ѵ&YZnEֹ[?P]FWy &#zBʑ=;QRX-w,X(s %/ٓ11>܍M΃$=P:liJ2y]Y97L;#ӫQU~+7߿~$`(-Y07Җo0>i̶g|gPlيn(X8ce9v5%t 3 ܼX%߹cwKOwk~MnLvj*2'c-W.MsOZvhbLbb![w磁'xSu.!/*[MZڹy;qUMx`le)RMF83'=tHMt҉E@m |!X z}PC59Z؜%W %rݏ|F[:d{.gOr)UU+vd=èk 0@'?#j>\;X4lʹ/x#"g #nw15CqܿX"j7| uy;)p)S0tv WbڐI#<'Pm%Ҿ-ވF@9A8gMfgi߭ʑEHQh,vi@ʦ ,㨚gRx\_1*nz HeO^ СS7_Zc9e @q秶OVģ]m8s+QGʹtS& /#Ϡmy8~T ?k@54SD !vKM\|]W-[5,-YX'Aè~Ē{Ilv-ø}p Rsb]׻8A8AQq *$J&o9. ŕ =tQ2ɰ)(6TR ~y?~̐/\_Ef+ZٶgH}5L03VPZ]4Ŵ]diH[n:񫟺Hf &x\giofY*}è.-h׈6{6+& ǙEؤ1=-ssD-}EoSӊayNq4PN_y9FNɚ0vCʗ'I0f[(}S>UYi(^-7ʻxDi4A|S6 Vv>мGjMvۉ`@%):ڰ, 2 c] ;^4a[VQm~*ƞ~LLgȉ/OXּg[v\22o]Sl]ښn݅?:-;IǰMNoDt =yOΞ-n!6Qds}񏘳N! AeC/DzD 7vِAX%ijl!"F<۾)E7+k@Y %y(P'^]0əteAm.HAO 8DzfA;6L=i׶GփM@]_ ,Y뻋ӏ;Bu.!_72^ڋ3YO&OTW[cH`B.-];Qc'{fP`%hsP& ʉuXNn@Ih:&4ozc<ҏ#F#E\pq[I7+YII/1VaOiT]"g(+no9ޖWO(ͪ>2u)D-?'v"-Mo8 v*ę6h˝V ֳU?#@틘)_DGo5xzceCEv!@O+#W^e4Oدo1uw\֞[8v)OJ@Oe …(9Nɠ?4~!jXՎ4~lA_⹿Lr o.Od\!svJ(ס(N =qҳ 8y΄Yfakowxvxmݩqxܞ$GlRjAm;ε`m8)/.WʼnqQ;⍯^D݁HU$v#.'{mUYKM~ħA㔞GA@]S"m ( sY#D0 Iqo+9WϬZw]J+òqhU!d #_A}d}(4IV=oD\%SK;V7oޕN|})gb͢k nqsDr X;ny$ju F)Zv&∦y3dӢ^ 75ȇ8x>|UEV'=M4΋-J%l[;i[$D踡L=h/`ݚ"yRKu.<jhWdwu|Uc3gM>1:y٢tn9zL7rH@lU)*QTo9]͸.BWG]:"OWY} 􅡁 N}vbsNplE획ԩ+{ED֓܎t^yt;+YTSAշBj|PFaT3ֈS%?Lsl%}:OONj<m3rVSĩCK?hZmo/ ,cN3qAgN<r¸hZ2?PnڥyXyQ\1ܓc.&`>73l=\57\OM/%ç!M{6ct~Y慏f@i}Dnjךк!y/5p1b rmLlVLVVP(_}kp?RәU].+Wg#+<&giCԋlj69O%vqk; ?"L[j]? vضxy*'5Z-fv!|q\kQX֫.{ѹ8WZh}$EQ?ͿVϭ)N3 ]&oő*(S?y yT"!^ݪQ&eDq_i+|܆{vVES9%0Oq4_qj)ڠ:0qLj;7AB$j"3rhf͚̻]/߄%[MMK×l8cp2߷+qĕXKڕ@ԉ q(TWmdL_edh[k(d{.N/=-8K"sogn%T*Fx4ע"B#d"Aҙ@9VD&QѴ| n%4&"[˲'/NѺiV_NlDM0mDF&jkdƚj E$νu—1QUTcþN$c'Ȼ-u1>mY;4Q!͌k2hT'r)k"]IyD_Ũj:p uγn_3fzт~/ QvjVv[T2~pC=i7+f˭ߜ^+OX?Jǹ4X>'W52~f< 6J;oVXCa40<ꩺG( z*["aFÎR@jf')9"8&l#a)^!cP (ś jm;n%2P+iW{ZkdKeי)8o&pКmۍ>4a _OTogG*\R$|e_zUa\"lՏdsFg#ӯ6(xΔs@MNa+pϓ}/|*woݚїv KC@Z2)ns󋣓F 6QY85m<*}?rFI~%N\cO]c[ʭQ FOᠹͭDsҼv+-j3W~:5uj}Is2\K,~Qw='~.` W"۟mODOK&0S3 v`Rޓ,|jfZ7) Uk&gʹBb[6ag=Bnm_"s*pHs1N̬4,dX#YFa΍`B'[|l.EJRF G>GS"_u-$>2Tùp~ɷ Nn8W7zT n@:XUM:K[ڹk)E=:j~\8O49=x5; W=}5iswY #7't`=ƇӚ>Fg…fT@y%¾0.>YvU49!<ޏlkuBNz)/%5(<ٵNݕ[;{yӎOڤMstMqt`j>w 5 EEOf%d 7X~W=Tdi+$V܃ݓ<=g܃Tܓ9jWJY6cwN9GS=e'͠/٦PͥŶ:WcM;%e[g 3PxXXkxr(GˏU%|lTܫ[]bv媢(; F"VU{d"5vOăۗ cUvKO-M<&zLl/dgض nSOQ%8o9ER !| 9#GuyY h=svNA3Ai?-g({kj3Ɯ)۞_n|fy3h;/}]`;dȃsY1dWό B"7jl' \3BƘչHp;\iY{ey7EM"uf|JOj7oV|4׆\_ :|ה~IWdžqB5'( W׵(^GG?gl|0^v?v'&u K`2ܙkHɤDHBw&!cEd`iUmSI3xp-oUTL83#E(oMmLp޿|fDNrW#E{gVY˟@/b ];\#Xv۴!7a(R{Oqݟ߫ʠq|:;ZlH h|J6D{uo᧋Ka;A3\dd敠`W]>7֛@"TGPgMi\ M`27#Wk6i^㟶C@]{3hOd<6<8TB8@Ғwo\cʈ©7{O_qd 49Ex :Rm~JS*BB('>Nx\Ukk:1ьe"}L)U66t1N}<es?3>9?0j+}oX')?L4,ygtKq{ўA_@W^?ОdK_]W -K7"pP #>cxSreswdSD*;-= 4zYi1Kb"W,B9R"D xHG=J܁kY{T-Y:ɲގXyh}cE"LCzomk0wi&hy8mٽ2rܶ^,(!lk]%k֚lEf?+xS@Bfǫů,AWPKS~A3Ni<-)QA;;~vDɼnSh5`DL BMCV Ec{OѸwπx)}b3꓉Uv%e|n:4bIFøEU֕봷js⹎ČMqĒ %P'{ZOi`2P?6ހqӗF'*S ]J=-e{.T]-r(CC}k{1 QAur_?W^yTC)0ǃ%2Dif7w4L&|OmjW=#S1%eԄx| İjmnF)YײuJv'V//>#Tzά;13w4f<*zx9|A1L˫~xO}Zg: >^^+(N Vª_2Qle~\mV|ojx|oc{٧1 (#k497y`^?>3rfh$ RYV>X磥ogh윛 L>y߶ѱXg .4[X Ffk>Pm:5;c Y Dvbʄdl5=5aXtK#HM4`z0Lٲ)FS^m-߭!Vڌz._Ґ7s;3_Y>BJ1:G*d[) m;ي()Ctm@en9"[8ocˊE(hiEJ*@?}Us_ S8m÷ NmOOHLTBZm^wy ea+ eK6 n[;)Jͭ9h1F_يpHuJ&kT}oF͝Fom ,ٱ>(-{qVjY%^Aie\@ ʱQ2WZ@;̺wʺB\y"eQM{~3sr u>ĘI'G"b[۵Uc*o.aq'-蹞6|oU>d-a\isl*x;{L?FqJȶK.Fsu^K3:DP'DݼSt!MUaSz.)zOOuUM8އMiNO\>OCK8f uuGpc4̻K@}RB&.# ?8%&?ݳϸmzH8quej't+ϷOy^?v\NH={#w0$+4ף=w-E|РyggtKrwwgc"Q 7{~QSЭ{w |?>ߎk]JNg I @~&#WJw| ap~j\{3s +^kˣQG9LYZ+3n u "{nqFv?fLlE-Զ#',= $%YMs-_'e Y(h{om",$nK[CrȕQrQyHH"@EIHH@{"wG{􎚳Er"8w(x;wϧShSX{-zjy寧bl~qֳ"/ M7HHH{*^K&H*Ű,gL̬M]YhoS_ `ضm]C  fP &  D@~d;C=`]I \SbҕJ[e +IDATrٲ ?,v<2ܛ=[L%Y< &@[rKG$@$@g=T>9:vW R9ݢ07 XW e88tՉC"'iHr ZXc h(xk- $% ޒYX(        Nނ,? @R-)"       h(xk- $% ޒYX(        Nނ,? @R-)"       h(xk- $% ޒYX(        Nނ,? @R-)"       h(xk- $% ޒYX(        Nނ,? @R-)"       h(xk- $% ޒYX(        Nނ,? @R-)"       h(xk- $% ޒYX(        Nނ,? @R-)"       h(xk- $% ޒYX(        Nނ,? @R-)"       h(xk- $% ޒYX(        Nނ,? @R-)"       hooucIHHHHHHHH휡g$@$@$@$@$@$@$@$@3@\i֍HHHHHHHHZoM KfgIHHHHHHHZoM KfgIHHHHHHHZoM KfgIHHHHHHHZoM KfgIHHHHHHHZoM KfgIHHHHHHHZoM KfgIHHHHHHHZoM KfgIHHHHHHHZoM KfgIHHHHHHHZoM KfgIHHHHHHHZoM KfgIHHHHHHHZoM KfgIHHHHHHHZ@ "a4#u r]vc\a%ӱsWt3xLr$CZ p ǁAHHHHHHH@ ZW6_P:rI< ]bWߘr;f]t$C9 :*0m9gNGUHDc8Gk$@$@$@$@$@$@$@3fMȒ/wJ|k\VMEwTE6 ^5Ӗ:H 44Ǣdk\KǓ/ŵ>V%BapjXgL3UEe $@$@$@$@$@$@$@$pΞ-4s;]65աw0˺.ůJڀz~|d~{qU+ da0H?QV[cޮIP\Fz3-%\'F        Ξ-}6^^;>ڽ%{"hl qfZO2ހ`o=S:!Eҭ%tGiՒzEQ^R-W]f'pQ7g0Jg=-i7)R#yl߄뵇jdO|/h BHWedex\*ۘ~4.*dI߃(Mh}`g.,ׯ~8 kwy5*W.Wƒ%e g.Vtqv5Q.},ز=EhN;wo-u{wSom-a6T{q]7v~u/nBmM#&}_V.       @r D1ܳRob~H{>53pS_C緃r-!8nx5(5>L욭횽m2D#cG 1e(ۜ-F֭wѰg -:a֒QmpL9z M}g]_> :s7|\M0{lAr(A$@$@$@$@$@$@$& $ .!۰٢4j5[V\Lp(W}KXQ]]7 %{vuߣu3Az(}.l.鸧n`^Ɓ=KVڂOnǬkC 7.2UE/Ssie3RI척}Xg ]&?fg<`=~--of|GӤ]^1=.<9W<`T2C;3 NjYBUa o)[ s®l-%^ĨQEe* Դp-k"aT%rqrp7:n\ٟ*y48FݽV/AFZr~),:-dd/f_7Y Ѽ{f}s]6ʊfcn\zmðhξ%0=l#u "k06X%T 7"xjuXߕ.I9Fky4 {7&/49^Z3mTw:Ѻ|ÓW@pL~bp9To;yD$@$@$@$@$@$@$pH:[[+Deh.5MJ\pOy*۱Px{ %]%7m.v֊05#@|g }MdjG_|yobW~7g¬Z&KOTyCZI#^:źtUA.шMЫriY>asվZs7wxX=>:jo`VkFD(3]lrW q*$s4|;,!>Lvjٜc;lw qj"i5ނoOc7-m=7'?.tU\$)QxʮH.6cJ7q^ mU4krf= @&t7G8؏'6brgB}NZX}-ɨV+n86"}{;V\;߱eFǩuQ(ҵЀuxqռF3or;wbfݵ uFj fRZlxzm7ͣhL^f~pHHHHHHH@!pVo1=:66흉^ns-rU|Ilr& !6 +\$1e(sr(e)*x 劳54*^Mg#NJ~q}X o׸+0 oX5f,7%*rjGW+k#,!lݝ.[wwiEP 5 zۭ6UxTMV]n5uHHHHHHVE[\_^5aHxUmVأ/>[4i¸TG[O?^b5#6TGI?o%k-w߽&3Ǫ`h۰xcn-nZ}k, rּvYW'K 7!/ƌ?հ7lxr-V# $T=nÃXѢ4jގGʰٸ)\kf!gO6fŻ ]WI* ZUv6ypq8%;XS:[n5ZNiѣp\"wJzAqJw5"IG+_:al@"Yi9ϛ75['DH$`$F8A])Fc8"ItDg᧕G0GuHI6xGqJJ{$e1HHHHHHH"F/|m;_7jSPkNFtx,շ~/T(ux|­Xk?lFi& O(x;g4) xf,# %?_>6*OJk'l^uWÇ+S&       Ks&xkĊQ7cizr˟}7-2YeDвf].a#       h@^SWnǬ$ _eӯ?+\FxűuSprfھ6>-nsIENDB`././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/doc/source/admin/appdev-guide/step-by-step/plone-simple-step1.png0000664000175000017500000041766200000000000027351 0ustar00zuulzuul00000000000000PNG  IHDR8 iCCPICC ProfileHWXS[R -)7AtЫt !ˢkAł]"E`ʺX&t}{Ο3s'3;sP rEfJj (@Pds$QpwyY^_hryH,\ 'cຜ|q!7^/k!A XW3x&!.bd*-&,a5GGW(x3ľ AN:8fG"s9H0*r7;/ReD@%!Wn/ҰD}G 0@  iv;r_hF 8Crq xx+O?l) VzX6 !VM}T,Kd!~)S`0[>U(HSb)iu3򠊆J WeJ]* *TzT)+%EY@@9H@y@yj:QU:_uaK]ZT[j u2UJ]AC=KK}C,i,ZVM;O{DFWsP WSTUR]EB_}zzQk}*l'4nk h54c4s5kӼ\eZSV7".z6QJ;\;KLvv8$::t:Òadat0>2?7j٨z;Z-={KS/X/[o^C}\VtFk]:{A,-F 1XFYFkNӍ}ka3sM~0VAS+DӅLQ<2֚5O0m^c~BB`ޢ⽥e:VVVV5Vi~U7m666[llQ[W[m5;NhŮ} aј1E5] (u/ǚMzlد9;i9E8-tjpzlqtBs qRj8޸]'.qmt&v;nGrKy'=?zyz;{Vyw1aen3cU=fݬg6Y_8 x6 * j N (4R:+l!,2luppNxux{ĜHjd|QQ⨆ 脈 k&<Eŀ51cb bH;r8qi%$Lh(MlLROT>9(<3elʜ4RZRIM:dr)3\?5giӎғfǰ39\w-+=,| >apUVXֶ1{rsssOD٢<yv%^ őD2ER_ 9-RkOҮ"ߢʢӓ9C4ee32 ř8d]s옋͘8ly=C]@Y ]a DD\r{mK¥\m\[ẕr+?;+ZWܺJck&]\[i.Wض^sCԆWmIVe@m~Vփ m]ΎUU;;v>ݕ_w.ehO޸M Ak5'o;tkǑ#G=Dĉ9S:V^|zL񙁳gu7Nk>ͦM"/\r|K>N^|ǕnWk[\[Vk<Ƿw܍o߼z+V{Gbǝۓowy~7{E@xPPa#GUǡNS]A]-tx"ygSӊgϪ;??Ǥ?z^+S/_WKJ+vƁ؁Gr /aG͟?=y/ _#>g =R١ %Dq_#~&xr q>Q6 XuqiJd8+bQ ah! }l'" 1458 824 9|iDOT($ ~@IDATxUۥ7D,* vAZꪯ]b`EWYVDQ?8a2Mn$LI3)8묳6X6md'xuQFA@@@@@x >|p &4hs1U]/     @\^nݻ]~7x     @T@wm[J     q;cӥ^@@@@@$P_l۷oD]@@@@@ ̟?S-x     @ 7UV-Ju.      6JO@@@@@5A@@@@H +@@@@@ ȎXP@@@@@DىB@@@@5A@@@@H +@@@@@ ȎXP@@@@@DىB@@@@5A@@@@H +@@@@@ ȎXP@@@@@DىB@@@@5A@@@@H +@@@@@ ȎXP@@@@@DىB@@@@5A@@@@H +@@@@@ ȎXP@@@@@DىB@@@@5A@@@@H +@@@@@ ȎXP@@@@@DىB@@@@5A@@@@"qFVZWQK N[     @ٌ3lɒ%V~}k߾իW/oڴfΜi-ZjY۶mQFi/_3ȮH]֍    T'NիWǷVzu޽{Zl'OlK./_PP`;wƍǧUU%v@@@@@r bM'vJ0;b_.%kmM&Ǻnk\WjU/}^xD@@@@" zb**GnݺYaa;:d]}1}7M[b TjT+ k-Է}7uj4@@@@@ + dҤI%5(N]JSRU%ٛl}5s q}6co*(]b}շ{6+( !    eSNMk-`v&Al\G#TLYvjy5tX΍k[5~U6UE6u:{ *7cni}4zi     @$?~[.)ݵkW7o^%-A4iRl~V co)cY҄uծW=gXkmԬUecUilWie jVS@@@@@ ׯX}ֲeˊXu| {ުv3m񝊥#4svj+sValp[׳L~h"{}Xz-mc;i]zΜ9?ڂ l…|rkݺut_ݺu+@@ DUVٌ3l6sLU ]v-ZfA@ TT0; AlgWo!oOr%Clƛ][Ջӈ+gے[X׈Oߡ][KV_mW~:;amԴgh[կ`%Kwޱ?~ᇰ;5jV[me XZUW7On/#JN:ִiSo￿SFPۅ^0)p:{c3OuL`6t$Ϲ}#ys*x^(uAqmv:u;APhRGb:_{衇>goÆ }?7W_}:s|o/38u/.ry|'p|ְaàY  @y֮G6xTaᖞؽgwzK7]#lW_d{r>m/[WdFrҍog;YI(=NL7,~s1VfLo&{LAevW8op o$̦ _0'0vX;묳wy΅%吢$ С81J9L.q#@B+ҦM2}Ft9a2ѝ^kW_ Q?LիWnGsJBѣz^^=s @(`v5ccu:X'/s}O(փoj˵Ew%rr&v[/+qveO?Tm)]we]t)y J+8N?7|3tKm} }?_$ɼc/Lص>?iv뮻nG8#/|@nVoJ7rHӟrtƍ;֮%JCT猧zHA@\%@kWXͽ֮foZOa<=' nne'Ho4ͅkغ|cX׏lݚ ^y9NU@33ҍ͕VOʁhTyc  ٦MlҤIIGj8:([JE<<-^uadpwkP31r"bFlDc9[k-=`#tޯ{6mVucĜ7/8qIݬVz5vꩧ뇀V KP:˂J1|nsY[{g۟;sK@9?nݺ袋lȐ! E&8}8Zli|wXX [weO>qdѿ{N3tْk#EI^}%.*=_IΝ{u:cs_O<k1b  / ɓMn2-޽{.\ XZ_;=$3'LZf~1Vywh`wttL{w_;mlKP_K7ݻwo3FK?/qTl=X}݄ ЭҫWs\F@Al/GN_a85?.+mzD[9uj.ܥyl-?El-![w^'n6lHS_;B fFJUrvmIsyA\n۷LʻFͺ=c;$Lo}ҷ9+OvȫQ8_ dשSԓYnm)-]Ei{> 7Bx%,|&"؝$+WLOL+iL@@D,=DQ fG.vq?iˏ]gޝ_ {|?b-yoƎؿh_njbr1Ǥu{9>}{Io)z(b&E?Lc\yu+~(G_E(_M(C ;t`͚IGUT_~Џ1ս]vks(Bگwܱ]SΜ9.\wuֱcY{c箈/svEW`„ cF37ݺu3+ʻ*ci֬YwE3IhˣOCǬ-Z8k;桭$#} d~?~B3uĈ sm\;p~=:xRLSj l路5f:5M3)_|}WIp!9   @@yݕG)@gxzdS [{d/]Wd0ɖO<w=Z=Y`{hgvj,ǵk:?p_^|2<>l3fO۶n4?ASc&Ϋr~[~yoo_~3IUGm;hmOvmȑ+U`P?6I[kk/:twǏwr<w 4{}}fS]q ӂ4|駟&̺{o ήj^x3y?=#jժ,S9Ƀ~+/'?(^t "[@֠9t.N oQN[oՙ$y b<餓 ^$T7,dRR"Al D+nW[n'ӢT,n`npu(z{'>gR*5h7HޢZo>|衇[㦥QTVOl dx+R/nӦcإKwR>3Ϣ(zΪ(yޒ`ZA*͠~%muWA=y@)%naz9f̘+##NQC}LS-V➷{J̴袐eqt '8xgP}^ށl P _:nB#*kAu>szW7Rǁ>8ӂ4(rNڄ~h)  +0iR#o;|Rw u fە2-rMc9'Ńubuukh}4uY֪n{o}7 6n殶+6XZlVuEu.Y[d?m`VۈAݭFIY TbIK׊zTEQYVU-, BPT.՝GuSY\oxKʗ d+u7ڇ~Y=8(jvP[u d.pd%u{R/,J`G嫮*m#;(-PP}T d+ugZpPp,G}*@>wyg\9NNz>\I3\ՁlNrgk [ߛ:&uU{A#w=RQ9_ d &וk}ƺ-C qtZD+8 g`e^# M@֬gX:kҤIH%i dw~;0g.=9H,VPzs]z^ӊ٫wS-R-Kz2+VCW]8(퇻\޽kcP [ԇG:Vnim/ä x꥜wKRﮰdغ_t+اc*DkK.ugZil뮄E hzKPޠwR W[omӦM3 + ?裁i3=;(Ty}}Olj>_[9/(]vmmat*zهJ|z_w& I믿ã*ٹ1w1=SF9;r* #-C$mwޠttJLj`q:So߾6Ί_<" &JgNqyUrn *X/G [90N;4Sx1 /Tsק.^] Uoko3gWoXрiOE߭ދ~')z4n.|k_ܒl] :Fr|V#JkF%~x ddy0YvNRť3֊,  :'7n\oa ~luPg* d ޒ/]aM{N֪~b:'/a#gߣo=`5ͦ.__[Y;4.'|?̈́xՋ_4HL +?JsTW^y%)A@` L>'v:('WcVR>LPBݻw&CJ:}V?T *A=hW/쇪~tQyz'|}(h/a}FlT*h?zTGiTno>dzm(G?0ϲCTd [мw]Nt )XGݢMa=uG.*:fQ|'iL݂W'ӳ) h:CqHovѸR;t*-oH)ŝfEBLZFy]N0[:36wiy  % sg؝Z6,`ة~7z$^ tSZvAk-=)C^d+֍ڣw(LM~:-6yΰK/c۪^PuT/?Q/>s+wy%VEFC߻Owr{WV`J, + {!$ ּ\eK4$WRuPMSytIIeLT ЫżQOgҠA'.p*ZV ooͯ1E3~/(_g_ׯrflw\A/tIu6ŢtX݂駟`Iy۸r8wxS0w=I*ΖTt.Jy.ͺ;If▫h(}~U{6v TNuT't_󽍣#Jk%wp}_wS[4ܿץ dk]svJzEsWw͕#  (]R۵ fG!:F6ʭ.*ߟaitKvn'c篱>nX]{+;GwѤG#yeslņ-IviU:5Y=i/0HAN]RQ@[4rW؈#fղ|IBoհ@R9MXaz?+p-}* eրG~m¦V ;ʡ/Q d31+'|wACN BQo-TФޒNTѭ`i]7Rp[au 7h^z9i ҸuorYv>Qz x`KX6?4dP)w֓[.@vymGy6ߎ(1|@=Es(ŇuKiZ^7?3S.w] _tQwc VfT  G@=u:Һ<>U0[p7U < %ҁl)hl?ey-;~zMWsNYf,Fk[h|J{ m5 =SҿY6aRP0VAY Ԭ̢-uQo7- ޖT@A%hL-|j?àiPT(TR|(뿝XT=>fSޢ 6.< 'nXQk7o;ꦋ> JUw= \w3De{A鰺tJ.7A2*%K:u!Dys[:^z^T2lbGoG d?# h}NV$Ewe-sUp]c>|NF][ž뢱"&t@@ Y@ t8+))+VpRW<&l;L| [;|[ l{Vk[75X:5oǟ/]WdQVW[҈h lXXA쿊,թ439SYEi3yGmwON:(A>-ali$缻|Џ d Ǧzئ >*h୷D1;.Vx뭁93O?=?蹮)U?a9ǃ֓jZq*]ITs6𞂲sL4P ۛ{<* N? Իú<;2wܹs ^%K6]\NO® -x{O~\l(s $hѝjRXX5p޻Q~h+-vקGhSO &$>םTlxD@d1;|VlX֑R-֫Ij֪^MW mު d]mܒ=$l,S֥]k kWX+>"'JP+F3U.kQ~OΤh=Zr)`@?=;1G\U T/]Rj%nw_o5n_J ??U?bZ+,(rSpuI=:v n<,@:uΚyQQI?58vm_6*UU }2?Xt(@T dwzwyO+@tAXSnݔUMmQ9_tdk>j ﯷ2uT0`y _섍^ζʹjIiy9  @~dU m+ۋ;.٫='ڠmZ ]cFoQWQF{ջR;+I;aBkP/1oQ]z{{s@R OݴiS7נ纭WzK/)%C"n.v>Sʛ.ͭ^A%,@Yڵ ϔzdz?ӪsX~7RϗwnJ;*9fKZ~Us;\Qٺ0[u1 ZR}lbGoGt"J-0;-8?aJ-nO n駟Nh»># @Vf(hr]lYQd6RĺZJ\5u,pݥq-ۡE]Ӷm۬N,yUcРھ?ti\%/^.KM >iYzR ;(ЦW|!( j߯JﮔLr#mT!W.4؂.ZejNao5(yӚTy?1oiݺhN+?8_rež޽{;ԓT9ҽ*ٹuA>mKwelڟ\ltwCt[ ʏF9*;G=?ם9:P   @~ du ;4X:śsiתQh@(|G94c,EA?߫vرvYggs fR+⥗^_M.".[BMob ;/ʵ6hme-=?4Aӏߋ/82OS~Pۇo_uS:LziAoڎ?~ztqJJc/FDo*ソ(nflVj/SPH%hЪ dw}*uٹQ55ν:;Ý l]N7.m:\s5)s'{'Im-K@@ȹ@v SPUԭ-[,wx1bD<O6͎;&EymEA\ d+o^{t?o]v2|V5PR:i`kW8yKP~񲴑pRvg+O30bEm3,IX`wz[8u{WRDe{A wu*7?͓TPG9cSZ ;׿ӃڰE=mQ8_qI ;(]K>}L̂R n [o;4O6˃>\`O5  @~D.b}^vH5Cj־ժWRd]l`26co+_Q=QJS~LVN^nQH oSǒ~8,AR#G\P0^PrРAv嗻$=B _4R( :9?viI`Ȑ!Ij&M' dW6 Vzovx/iGykmv{,izXvذav''6A= A)J=pZVyuk… 8N۷o´_~eB~ͧϊǺŽw/mD-aPӣ6ت5#(hGͻn:'U){{dk۶'zGҔ@vJmA@@LYnil V[X gݩ-__l}1ض]V-f'ON8!):<.lk֬q{Fk~~lwQGynWjMz/&̪|'[‚F餭:~cUt [O?ݜ,NZJً/=gu$TBo +k{nq}\w(% e)Ç7 d/Q?3-AIzՅoY\L(ଷ1tX݂λ΅ ^tMI[2TRtǁTej#ӽ<\mp( pfNwgyNz%w9clݥuX»s.ҤS'u/갡f(  )@5{A@ʊ햯ٝ X/'7,|4(x3ȟZF35]=QS-Z_]YH?XF{QC*=w^z F&MJYfNﴠEŒ/lѝ R) 4O>i-j+ͫz!*mϢEfS)얇~/Ǟ={]Rgw}uz>z4z8HU2qt}~_BU!mտ㪪ׂ΁RV0Ǝ;Ζn [ 5COd|Vus[oҥi   @ D6}힭[fvƻX!ʞ&/]ouk^mX/Fk;#/>mwsWusvmf{ OYaXзvjQ΋mm恽)' <86Ow hR97gmVzB){$Rmĉ G/绬AҠqAX2v=@w@VH7['=j?A>M¼E? j`@_9σFꕥJߡéz+Xa8-]>,-KYrw^\\ݔ)& BjA*۝ۯVy1g?E0m[tX݂UXyo~vI[oRE^'{AEt;[YTWpo6;m꟮ArNMwz(kI4O&%ڟ\k(7U}t*) Oݯ.0;O&lUj<WkwvtRRQ'濋L]ve9sI}@@l m۾ kXz],_?ȸkl[SV^l"yֳYm{6vn;6eZj6򄮶bF;IֽIXp/d7_ըf l(~P0{v-Z8A200aON:K-Teʔ)`QQ:uWG fR_I~cVF [i4P*n3 kO=ܶVp+;,D=:4T gPϠٕOcV1/%'uP WyPZ Wi3tLQGlܸqeu@/Q^Pp:nAj>c;ȿ=.0[]5k_ T'bpQؠ^u0hOu紿]4-w:lhm_UZ9P@jgݷÙNuP L۷oIi3f8wSh#5H#.(V2ږ eZdO?hX [=r ڵk紑zɫWb*I+Fb 멮W?`nٲMAQoeT`E??zf޼Z^Hzӫ!m-ErѣsA5>_M6'x4Kg})OPi޼ӛ] 0k 3 Q${I_t{OmcQiJ ]\HUԮc2+]o s *[AS}+M+U\N'|Zk#Y{˔:_ :*)/dbw}hTwdVu zStנzq{yã>y   dSx (}Kwg'vV56شOwp/vl9}oGL[T֬n Gukd4cű:V`"nTe˖y \ .'MI?(֧`zb2e  ƺ@LtبQ#{>KzRږL;+y +kPbl @=긕e)ꙫthpU nGoQp@~*» vjVݡTtX݂uǒʿ^IwU. Dnu*pVuPc ZFwIYٹN ;(˵6^^;TZ9PIlZwҋ)uzl*.})+ b]-@@@X= ngs'.+?mWieCo Ȇ3R(PͩEb zsC3l#6gX ֣ٖݼJr}9IFΩGa믿vA ֥A<5H#.(n2ڦb NVw륞 Hf`s P*uAPqW)޽ 'zzK]YȻ~ i_2 BҨՕyX>y]pdC ֫g(wiLNo1;,2' +QڞzyK.cIi`Lκ+lwuIuqHL.8ݟ]?Emay:w:lEs=.+|-(@xT4lO)|Mg\P[Di,zY@@ 2meLYa-V|]@IDATlco5^yiGuM[ v7 x>[^3!ʑުA-{ .tlTXjS9ժ^1 /^b`TPã> `7i$liM_b>(eM?2lK[KZyYA?₂n=*;*zY+=.\JT{_dʙ]wi`WXntJ;۴[dW.w3VJT;g50R*kQM}&T/o/W_9?.))xF 2Kw׫ DJYrUWxkuԶKwޠǏwz+]"E]NO\taI߹aEVGq[g{]6(՛SmǠ JMvKYr;@{4?BvQ:_ :J'=ow[~ujXjh^Ȧ4 Kc;.>6o]wڢXN-*h 7Y5eVLGzꇒ#J/zh*0OPC z|7kn>}qEx@Pް2V>iӦrg+h^Oj/^l-ޫzc[?RWΝ;17!D>&Mr|- TWZsoiժ:I}'.z*;\۫:Y   $@ ;Z}-W?vy%n<0i°a/LT'N      uQo!         uQo!    ӽiL@@@@R3R Wޜhmױ?nP==julVφuNe@@@@6b7joԥVjU[łs^ y     ̋p^n=5ʙݡYe@@@@ TJ?k=48oUҼflaޚԫ>@@@@@ }[1' 8x    @ Ȯ0ZVe@@@@&y3<o СCO>C7n\|{m{'~5jygj ܝ &;/ ;S' X 4A@Jqkn~l@ d6dȐ8V[meoV 7`oF|\`vZuU=5j/4ԩ@yN0;h>[Q1W,\nxsi\Nl}bWy|zLج ;hJC@|q|7?? dXe7hz裏I&שhޚ5ks=g:uJZ@vI„51!O @L绛9̣߭m! +@ ;ɉj]vzh˄ߞN"I޸vVX9nT;}^  )@ osJ dX{Nc[z_sαM67Q ;0.wn&6E[ 4UW]@Ff&K9{ <" Q k<8묳B .N;->W NY{z WVUm*"3gδ1c뢺JI{իWwzQ7h{[reSN9.ko [#=t~ӦM_N>H|C   @6tZ|7|@v~©7 )@  dX ,<:uؕW^i{V?;3V g}I;/RI[Z qsoȐ!vE9Ue~7NONU?n_*s=Ե@afw6kfM4q^+0=`J.]^p@^%\b J+U?".vyg'^ ,#+{ʔ)v'I?|k۶5-gx kݺu|]pk5_.  PK=UO_uxy|g`;ꫯc9yٳo6mYCY?{Jѡ[>x؝qz(<`>h|1VoTlnc=lKQZEh d+ nu?vXwyvg_+ /SO=y_|ن  SE9c+[M  ,;~rn6@C@v2EtxT  0VlIn՝}Ԡ4JQ-kߔzL\sM|ZP [U{=7n)zY&$U [:*vm HCA2}tl:-~e>E@B|wsl.>w9߭%D@ Ȏ^׭[紂u]-2h *:tHapB'Wz.iF+Q`o~ֺ3Y<@XTnj{K:^@?p矏O{뭷RE3 &8 $-i@@J(@6绛J=  $@ ;FR@I'd?ۣGS>젢"K,qg}o|gI>  E dsknr &@ ;e dD 6Rzu,{gJݡt"@_y[j_T+"n@۷w^A߶;+0=o [iR4[~ivmݗ!CQYlm؛E㎄zrjjI)  @6 8ML--fnf\@@( ȎFYNGiӟmz衇_ϱ#\rZ3֮]kMqYgٹ_|ͦUj֬i~ծ]yd+rLS?na}g:9oSjh͛/M]lDAv!^UV9'OL'  @ 8 dK/drK^   ٱF(ˉPjojӧV^i |薁W\ᾴ??V>Ƀ{x[{,C=djv_N| dk':y d>:wlhsԨQNn'W_}5a IM  &?|78v]ɦ X^AR\aR(E㳨СC' 6t Teرβ^Ϟ='tg | dka `t! dk;}޸qxX>+,,tޮ@6UW]eVV:(l# Y#?|7: Y˸eORrgv]t}͜9ӔVDAlo쪨꽮^ ]ڴi:k@@ jrc  @ 6f@@@@@ G@@@@@ m"    Y-@ ;#    /@ ;ۘ=D@@@@Z@vV7G@@@@r_@v1{    dn>*    oc@@@@jY|T@@@@}ٹ!     <     sC@@@@@ dguQy@@@@@ d~     @V      @ 6f@@@@@@5klҥj*[zmذ!# &PfMWկ_7nlu7@@@ƍm6o޼odv@'VZY6mZj+  9-M6ĉmŊVPPSF ^zN78; @qq{ [nι^/  @> e {̙NOZjY֭M*}F@U@k͝;֯_oݮ]l-   dO?oj׮VOlٞ#  @Alֿuٌ3oΒv  Ȼ@il…ִiSk޼lP$P=@@ n [c(^:[xsױchVZ!  @y7nsiNV bƌ  P@[lWZeSN5իWJ@@@ 3 d3ɡأG燍xTOl   @ (#{„ y.;   y&ٳVol   @WǏwvj]w͝cO@@S dS7v6 䬀ze+Rʩ٦f@@H@6iEpgW@@ dC+  &@ @v/ o`v@@ /dF@ m˞! l{  sY)@@< M ;?> \kQ@@3  9%@ ;A@@ M   Ss9@@(  9%@ {ssyV\\v6i8㌴gF'x-ZdrW^YfYZi&:{lɶ{؞{B+%S~͛7Ϟygs]vY_x9svm|Ac\KSO͛ˮW~&] G@[@fQ֯_6oǎ ] ۘ1cA2WnL8pM2Ů:;#SO=mܸ;0kٲe}v_[^ܗy8l0/mСv9ddKF;K?ڐ!C=cTw>M'*UEJLX޺vݐYR{͒cj"  9@.b;Smك.qfx޽{;%͟6SܧO+**G}vqǪBVoSzj5jdG9A ;sȮB-Ym="]vÎ # Y)@ {s>JԂR+R7ZV fEڵkaÆοL ٙЛ?ڵˤ+}ުzȮf6Ȯv [;,Yb|n{lX1@@'@ {3l߁Q/ dWjDW~JѪf]dGɲ;jU$]JohSՌ@vzSsȎh {vw8Q1U%C@"@ {^Y-[8'r{?`7.]ؑGi裏l\_}u ༿[ni6n8 z[m뮦T(t 'N^[T"B,U{ZE-]TK)m{[mR*QAACNUTRbj3wy;>77~׽9>g39@U\os 6,|_Ilф? x;=@_:;:}zp]wi(qmO< FVZ)|ӟ.h~w[n \ ,~n/blw wK-To;!CM74+V6?y!/Ŕ~H \C$ 6 295 8?$&lw0F;찃]{M/t~:nFLlСv߾PuG4L(UVYv/\s5.1ֹ.b_{ zhX|ŋYUW^y]tI)[nNsUW mktR΍  6N|I/oy;Z7xd>weJ{uJϩyڦf&a_Wd\-xk{^{5khV\qvJX_n-~l4/ds=RwM7A Yfóu]"K㙎֑f۬mpuJ6̑2z iE?ƛ{MNv^B0." " " L'^H1Ѿh]կ~e !D`;33z NJ#8"3&ۆ@yChcu6!z&~0QPN-A0wy]{S!pds:#Mlh6)E9os6~AE5٠AB B(.ίxԞwy?lf'xΎ8UW]囲)qo'+ p`v~ǦL(nC=X<|]:E<)@!\m1 ׳_W 0y)24!Aְ"}K^7_N;&?cYt-^ٴ5O;ʳ(na8üΛmhV#oI8z+]mƝ~incnEm!UsYt!.f; Q^m]m*iUH3mV6#u#eZ5?t,r!駟NgLBa%dwKEID@D@D@DHtWx6ۘW-xb&vuׅ^xjExc C0q?+}fI_<?v*>L8"`uQ^$ЊzziǸ{E⯧ŗE-i7j3nV͢kk/>WZi[V[-;/0hO_GG۴FBvG~K1 B5_Be_йʽN'wٶ3rW!R{b{Q\W֑FmexѺlVMkw_XS|YGc C}D!->k"mzr>{,@eLD@D@Dىg= ~[Fo/mrJÿ腖©Q̥^m/þڦQmoz_~fc8(jRvQQp͞ף^6zbc-ėN;.hv\om/[QSk N8-5Fϼ㋳m"mQp9K.Ķ0EضElG-]5D-.Mr}{1PClvҾkC}y_JId^0hGlm1HQj1zlwOEf{]YkQ0[۝>;Q|{n^ubG.5{Eג[4sk~މ!(7ζO<6u#mKNSG;,˟L6﫛k ;s/;y$;ڵzco*UH6j\è^VOJʃ'朦-vr5۵:ĦS/4!d2@fSִp m44ZykF&x =Bm1&[M?ݲŞ#@уW?Jsƶ[1<͜93ݭyF5|?Ńlo^XPU2m 9GǏm,o͢ki"ST:~s9h!z!ҥuto$dw4=oo"z[]S#lmagzyp!6GShVLfu RםyYV%tJ(+{&NqB>>|๗Oə@K￿ LH2# !1 ]'hyXUNS|>[&AG?|Yge)3<4s|i48ʇx(J#:SƁn^wN(p^gا}[Y%-?mT7%F~9t5x>&HZ~iE~mhz#!NYh;30Vb]Ruems*r%+<%~qZ|k *u^Պ6^]weeZ5?"t:PQ%͘^ԴkH!*q2:5F;U3R-.i ӸiCC΃'0~8 4"_<``y@xMn+B9񁙁kq44iDyȡq`@=69]&\;K?czLb?P?G*/66b,ul`b/ӑ~⢑~We`mۈ}Z|[dΊ|Y|ض9+OD@D@D@f/Qyŗz\81GQu5ԋw wܱF k7?[|;c2Zhf,l#ϲU91bܖz=l?`,3N|Ұ-uc9%ݪqHo%1jʍؕtsV_fiY5iyM=jxS"[L5}ӥ-:YE̺4.EqoM!W>tn]K έhE}z xpov]bdw$kcd+{M]Nn`љ:RͪWL,vLZyQf]Tn`]˳&Tf2cPT獶[?رŞW/$e{ dGq!uˋaٟ.Cy(KGCg qL~n~ѣ<]BvC">i\d=mg-OD@D@D@f/?*(dG={#o 6Vf1 lCT. be!0\d챣"MRC e'[ #u+L\\={@IDATKLE}  b!"Hʊ*!j*w~] efyfzPYZUӏ.Y) ~C i}[OȾ-[lE҅ktU,|E*3U/(.~c/u ktB6TX?뚵rʹoC:e+XtkU뺳,+Ӫwu\;S2B.d A5ƛ}˄l^1Z eX$d@ޚvgu1!$3=HkYC~Ʌl"s!;Jv@" !B6sqG{A1M +^ aiV+Js',/EvUWe/ᑍS / "sS N*,!E:O WOً>;U=?cƌ9/Đ'm;[ou2CdC&BpDSO*M,nheD񮊳eb}[OȞI.Ԛu[Ru!qs "cEB6 ~EsفCf?o7.8:zd LO 5sOzcYqESЕyٙ}?YA,M+fm,MTݨ+|B^̜7u|iUN1\,O[mU#B,E!?1jvz^{6BE=>NÎ Ne嚊'Gg϶5bWE#}ԯ4_eeB6D5'|"a!}_Sjv,XhUS!:mmp*"VQ7_e77KVUuZeV_- &|S#N;tCȦq-qh42Rl:3uNٖ ٜ?;/IS^*UH6JܙzeZ%?yL5eB'-|l? P`0#Da8=܅h^ ~ rg '2HB|G0p-ؠt 0hРpq%zdpc| f aW۴iBVstZ}h;.LǗ']S$~q,ώ|5>Ξ_YG0(8p@,z m<7G4HzFL Q` .D.D$RwתsnEHO3uZ9]׊ܙk`P[f\dElUڽ KL֯u_2 C8([2&/GB5?w%4x8|FF\SNCh$Y]8FQrQhcP=z̓(̼t0-#nw{szC2i`O>ӄ8*b;!r{/\|:4x~'7xɁCI5>ts119B> Xr|u +vg!3F8P% "*Z'" " " ![u@D@D@D@D@z d׎7x .^#B#4cOR'dCc}a9 2˺3Bn!#^oh9ƍgđCE0I مXRD@D@< } @/$ !lVGX62*8#ywOj[S0'D7~tt4-͋@Wݕ4 YŮ\@% !LD@D@D@D-!~]@$ !W.JD@D@D@D-!|F@Bvo+Q] ![BU$dňB!|0G<",*'" " " " $dwdBD@D@D@D@ \:@$ ![@D@D@D@D@z {_itE@D@D@D@z) {ieUjE@D@D@D@z3 {sֵA`E@D@D@D@z= {}%-VyjE@D@D@D@ FMULQBE@D@D@D@ PuץB)E@D@D@D@ Zr{yD@D@D@D@$ dE^{oD@D@D@D@. nוJ{eD@D@D@D@8 -m$d@}N~衇/7Zׯ_CBUR!gѣGň@_$G}4{aakYD@D@DW@~7?0<C`]}N~gŒ3ò.+!Tr]@"f|-\^H Nxǭ(+.ID@D@D@,+>B." " " " @)iӦӧۧÆ ->M୷ SNPrC K/t桋BO |nJDu0# /pX`lR o_‰X(|}B 5}RȦ`E@4)(" " " " " " " " " "0KHȞ%uRf Hn%$d:@$d7KJg vTD@D@D@D@D@D@D@D@D@DY%D@D@D@D@D@D@D@D@D@D@f  ٳN*" " " " " " " " " ", ͒~" " " " "0>}z0aBۅZ(ٲffcǎ mmmaÆvaȸr)" " " - !PJÞ{ea%&Mʖg̽|KoeXgu?[30~u)bB*njytyD׋.!ppvI'e=q/°~wɦ‰'뗭cرY:5N?Aȋéjy8D#<2lMubd_psϵc'N5!?%w} Qs s5Wͱկ&k6~ʁH 1zEDj0;b:b_nW^t΁O?= 4fs^q!SD@D@D@f[gۢSE@D@D@D@D@GloWZil]wՄuOUVY%L>=&za.Yh_f?l~cV̐ǃ:(ۚ ʂoa.R!#h/VDf1"7,t n!rSo⭧'=ܓ&M2t %;\ 2A@Bv(G]d 6"ailڷ.xR!; R$D ٿo-51'1.fxd-$d21 #AԞ|I ]׿ea,\}|J8 ;oFŋ~~1ǵ^e7+d68n\a]QeM7!.&p.>O wT&G*7:7|_>T߿[VO&" " " He+c:"d Ap-Y7c w}}P{駇 7W5&2!1A`BX3ϩonV=~x yaC߬ 6ۄ;7fj# cǎbQ>)k Oԣcq~x6S,uf{raCD@D@D@z  ٽ$u" " " " "1V )h,?CL3gfoM,qta?o,FP`zB Yzh@ !wBD@D@D@D@2 9q 툜mĉaeżzM##{/I\wuYNX>b ɩ> F _àA,, XOlBful'? FsFyΔ/| W_]W\e" " " Q Jİ&4@{xRyYgeB7^hq?7_†ǀlI{CL+4MctIk6޾,F6 [B,p]p-?7|sM,#Guqxp#co ILobJcOZj)[noVxAZō2!^Տ?x8jXPӟ2gw$jL>֧B6+g;t0(uۅp=,}  ͈@$ !W.JD@D@D@D/hG{ e]hH۱.z.(s95·o3Quk8蠃lsgl!/~,%be[xpGmHfvl͒gg nx6s/:ry}M`PO@Bv ^|K oAӡaAK" " " " "ЀpWoW^qĈcƌ'v," " "PK@Bv--@'pד'8lp6<N3 2sWODGy|7?yg3F{eø{_o8fayLD@D@D@D@D@D@D@D@nŭ@#7= /DԿ }?[vԐ_ 7"$ !;i\" <;>l}6^. " " " " " " " " "Е$dw%M%)W~?pc/Łx_!þ,F.`Z#" " " " " " " " -& !ŀ$d<@% !\B"t}RȞ>}z0aBV6 -P{ウeƎ`|cذaav=2r{X(;" " " " " " " " I!{s=3K,D4iRtIc7\qP{eø{_o8fayNW;@W)!N^{mV",-kZ]tדGm3V:" " " " " " " " "PMBv37n\Zde gyzC^| oCÖ-.K " " " " " " " "0 OBl\u^O@Bv,T暬^{0|@?ρڃ .[۱̳>n0eʔ/ێ[c5[l`w]x77&O>X`袋7<[O\/;/bxWoaH>O 7Εg;y;Z_F6l3ۯ#z&C?#önTw,F\=\;!yĉ;N]b@X}ݷ 90\s_*plf (өWt@?< I /y+XEF駟 TYD@D@D@D@D@D@D@D@D`# !;Y=!"KOboWZil]wՄuOUVY%L>=&za.Yh_f?l~cv##tP[*,g+ fo޻`KflģZzWƈ7L MgLƻ fx/=3|ٺ /^u[KxZ[_Wi*" " " " " " " " - ٱ ٔ+l ofV/!H0S0^{m3D4&$E[[yi_Y7pߤi!uY6N;w5 ,#"Q7M[dk?e]8l0!b]Nx#>S\#F8%Za@u]״ڬyGOjMX|ʦ+M1a„lPlo1sҽ~}G}k ?3=@=P+Co_|fLD@D@D@D@D@D@D@D@fwc  ?A 4LC#裏ocw/qtuCu`ڍ.K. a(5uC E(uwկb^F\Ftw{w-6[oeț r-3qPxv#8[~\%vW\y3>vaS=͝%X0ӬMGuTkB;܍x\̉gM87/}KhSq c^OX&g7yKl:Qpo=5<驯4iyh^D@D@D@D@D@D@D@D@f7c:5Z${-٬@LdBad`O~5B#4#r۽^OZ_jMEEuË<TYOdzCgL<CPF& r3֬z. itu~VsC9cxc]6v8 ;&7xc81"r##:bkmV&l q(º2nC,\L*d3"\}Q ;33M؏T07nun>g}<`鷾LD@D@D@D@D@D@D@D@fgc 3h#B6f̘a0#OjO?A&$F:Lby{ z95-vm*Ǐo޾!/ "|HafpqfՌPzd_uUaرY,j߇2E0ײxᩐzTs 1=ϦtBv?; aO;:{h|cu;qz!n&̙3ķF&L x`O\wuYNX>b ɩ> F _àA,, XOlBful'? FsFyM;S|?ME@D@D@D@D@D@D@D@D`v" !;VW <1-!02.huYЍ{ھC ᗰ!1&l^& opW6M;餓5\mO}eYlXM Z~`oX#G]FW7|72cӛǥZʖB6^ЄVqL-W?;,| z5S abOnS!|:EOp?ckׂ̎$dRJ!ܽ;n*~a;b_0|Es9F>8p`& n]w tmM"/~s@[ll<l: c/^xK,nlUVwM:O>d8Szߓ׫OI&xg{wދU~K{<^A ٽu 7z 2s܋8w^ |P7'aW,ݧ#{'o#vo,lV[mzne7鬳 ]tQ>N;f A."3輬9|uCeБC9jw|K_ O=T/v/gϊ Wicfŵqg?O$?!qa% ӭbҞΤ7OBvo(E]0gik7pð; ?|[µ^To܅Z(J{8|D_a5te7*/ߝ}o&_uo}[vyw:c_9ʳ;Wa7w]w ]R'#DB `e3fֹ*B6^zY@b=5\N:Xݹ:W&X{^w=ZBv򬗚ztH.:tCm'?YzN8<c$Ć2aC|z"NbUEȮ} \~gUW ĝIVݹ*;j]L.5 ^Bv֙ H.g-" " " " "0[pQ2K!CJN?tA#?<GXb%ZkF^xtWo$v4=? xD>gtM*⻅_|1vmӟbd6|gιqWڵ^Kbx.C Lw]xglA@Tbl/@IDATxy睁9GW_}gЭ ll*B6tMW|ԟ_=|8~?nۣF lISN<)'\s@(eɁkv>)_I6mL.f2ߧ䳙oT6?³:LȦ .;A{Ln̜9Ս6~8v02"o/wryp0@, 7`'E]}Qiiho d/mB7>/ds{/kW*ԫ|KF#\Vvuoi[C@Bvk*Ueҗ7;*W_?;3 jZ0>U8;D[F O>9;s Nq~7m}_lO֧3/i"$]xᅁ6?C_U3E t2^e ?pJf|u`.vr]W]uU67MJ+m_NAyvǰ^xyCp7qVUi>O(";cMftsvDx S:;?F9 :l =qyC#{ [o~:h'*΁} '˼#Am1(-Bhe]nygٖe9#mYz-͖YyYSկS+zu*f˄l]~`a谡m/mFߔ*uf ޳>]M{z~o u=G[Ye6R^vs0]{}s6e]-LgUii~f޽تRVM[G@Bv*e%R~ፈW2/?SycHce ƛeĒ"1LgZm›o#&x1O4ɮmDTzGj/y/.bnF#GF|o7c|r-me#fN8ю@KdC}72&̔0<ЩOs5W}9E>7F>hRwm0uT,8tKKFF4GpB3fyRO!0)^]aCTBLxo;4台}D~_;{u`~ JO>SM죎:*,袅Yku^'x<⁍h7I"rQtpy䢳ndU G:ph vi' xNcλ KʈAN!v6Ͽ{l}G2?QGۖf˿L:G.S~6j>NA-ڝs9IS~ר>pQ|BKΜuBdE8F$D4#cvml#?/jSGX_o 5c'g{)[_-󶣣m WEm tVSQw zvc"v-v>QͲWr_x}%dL2-Dvk=u?vHiimNN6VN7uI5i 3dVf'L9LD@D@D@D@D@mk8{9~h1@;UH&z8fBy S$TMQcgHgކǏ툐M)S| _`۾l㺿oxiǹ;Zg&~1J!,SN9QdcֶsiDEH!r vAN-\tX[hlv\*dW'ɭ,)jy^b '` ؽ4N f.:^0YboY`eCGX3V>n*d;PrD:7)ec)}Z&L[Vm!ʿlZkUkFȞ2eJį>mg8!VzeÎdyJVs1d+[yaOTȎ5Swl}Wի[[]fZF@Bv*a9 RmS6 E:]ȎalȲp4~a3E;ӖUm[dϷ1:[NqXw U*m7x`EF ٭1w#u!sc2 =UѽҔ[D (2^DOd8 M]>S)plywltիqvQz (*d_As6r!5љ" xI{^< BGBRC*9ZfqXaZٿ!Ϡ^A/ -R;o+ -BAzFCX7gU=Шl9gjm]uUCxeH8nv")g]v%ۭ)Z_-LBFQ[:[NeP+4 -˜EV+ -R2-u^M,u/ -Ҋ{.,Sk_Z)cʎ\ǥEZ]xjΛA*c+KI_ZȎqx73̦D@D@D@D@z.~X<.Bss}9/!_5~{>5!7wQ 3f0OZ<"/"^rb:⠁Yv獘TgXb'e/:o_ȳ&FmF=?_>{d3b`95rDV<i] ~T'ږum!/ʿlZkUnu{{ol,vem{Z#DaW_]'{*橇|P#:ꨰ;_L ݋s+~KI_Z}^~´i,.`kQ+uV FK/C+?i.`GLK\i{. rG/v=!$hS8R5=:@ez l@xD3z"Soq=H1HC W]uU$ R!.t/(ZOWRgemwfDz![QHO'_#@ {'jq6!ZlyaKɪujL&CʝP0|_d.zML]by#l]{֡.'sj} TȦNȎ8$ 3vC7:ɸi[Wvږ9*m Q ,jc:[g䩨sU;UF)5~'aMo0!?!D6ymu(:,b+l'1:CkFnoi>oZz}ZÏSmaJ/zJQD@D@D@D <.#N2eS b0?MdB|Es=5;8ۏد_|q&x!P!bQƀ |K.G8jzS>` aDLc_XxuŤ7-,&BOaֽWeS)m/1z%#ÈN !7aXhCU,pA,?|x "Sga _rl=+9[n _(;%$<"@ Jp !=PUt3*t>~%זVM xGg_0$8I^ڵ3[oิxsFP$XsUڲδ-^1ә:[Nq^#͘3b_%W~/L{ZeǪu;5r0q6N<7<[q_*ƄV>6:A]~ܗ8r-G#ia{#Z\Fq^1bwx#$!3n.doD(m$ ~Gq_QO=u+Wi:Ӷz_p\:ۙ: sVBvպ%y}3F'NYcPJxeB6ۺ6v{tVH:7:E_^' ")Athz|7#dxⷔe%'l> A])732CLFB0n-^!b„ @H[}SQn:w4=<£g77Jȑ D<{yePxvĈy}a!`x/x?f DmĉE]Լ]Y,Uzdx;D'jOȦ.ru렳t OA`D7ʅ;wpcގe_yl#"**isl㿮#_; (7]:k۬Y>Mg6ޚxhz@ZǓ6o.dSt!Sh# &~~v|@^ceWm|ʿ^ñUlg"dW0ŸWJ/џ ~J4GGv=Gt׿qٚw~O:&9++< @SFGloo)ZK pP ħC Xx%d)uB#QȽ⨀/..T9Wo9.U"B^C'oo,/xR3V5=g+*{+B`yISB,E@.{Gd{ IEԾ:Ⱦ @A l e?mz?]+lx{s^G/[q}D?{sӇ wL B3L _@;"ۛL{ :!ʟ @A l%e)oe9 !%^EeC3UH{^SEV= I¢y0ol 0ټ[º;wvUm@A  p,Cd'_!v pt%/|W!"b{rUW/E/\5\!^zi+}axss.{Kd6u}Cd A  .!FT{Y!wS @A }E`o1)/"4R @A`[GL@A  6ޜR% @A lc<7vA  @ہ@ML.@A  $E`1D @A  p"o@A #E`1D>jA@A  o-m#vq#~O}k)oA  ])"츼qk4 A  @#Dvbd" @ۈ@u"ZL@A  j-mՔ%W @A`(SDv< @A  p8NѤ@A C@u{]]ٓPDA  @BdeNA@A ++?Z""{@A  v %u&1=^ea4O@/f}_z>hA.#{@A  !$gt5nkA~߼-nnrL׭'&z5yMw;ݩ;s],_ՙnu[ugqiߛxE np>?W_UzUݜ=MozS?g}}yضn_oo[>c?䭧3>ڽ˻Kv?苺۞o {kZ|=Amns]vCd_?XH?; Y,fȏH_<ݸe/~~A=tx#6a A`C^AP9眃.]K^W~Ww~wG-}_]kk݋^#>#OwV,hsݳo})}7>{?O~˾lt¿l:rݶ覅nv;x,Z\cؓ \̈_;;RIIdjn|w__z-mU擙xӞ==vbEyV""Y___Ɔ,?s_9np, 3쉾Y~~~G⮔__>>ncVRSׇRqy~w꿇6ƌ$sKG0nC!FTE6yB`w8y)a/*zk_x.ݧ~N|<_MD%8ٿ$\p-Wx&7эIw==9\#yQ!'⊓^$tD?g#~W֢gsۢ&<>| |tggt:ʬ҇#?ۆvhm[֓hSR+̮"uSB qpI.D2(M#0t榿+e9~^W?|wυwSׇ )dn8Bd#{]"̳yLl6=.-$7aY晧wX,ܞ?Bd#7)/~D@J:5zȊg\"c7ۻ>-@i}E`Q;D\~5_5"tm>tX9{|J""{ y"b҅^\iw,t+!ߎanL"{ !.U@"/+YgUf%g=oc?cSmmo{L} /:Ĺ8ox10~[|-y9E뻾kU_US3XDLޏG\r>"pP;韺w=#Fǽ ٵH}ާ]'YK!Lh1ۧ;vUgTه y IF 'vV!E긨;F#g?ý,*@^ve+_>np%N0vZ{U ˼s"(P_;'}ZA ģE ׉<\+x~7C /D|"<+E ! #)|y2YtpmϿ{y)İ>~;H_?z׷o-`JiIK{H!x:ؖO}jCU;[9΄Ī6_sp\jB@bDAb'"8FdishYLQ~c>cu~S~SԾoVU7/56Z3K\_"o+I4>37Ub녕#//z; 9@HÖuYysAA}q.vto VӦ[>ya_oޖwwUg$xF&!o2Oz}^S:os69=t[~w]Uڅy>O߶ecs˲.\; Ty9=L/[,4iO18ojZD/?>3E>>Mv(p;UaW47ΕU "t;e06[oO63^~kzdBdSu_urמ=?O1~["/R=FAU|V3Əcbba24(ay[;/\V??Г)w6D Fk4ihD81ÐO3 b#L&;ԛyYEHnHhG4yX4 X#S?cK.:ɛ$ AS `%C~LA DzZmE洕9D\q( [e%m[" (O7%0N|ʧ6%Ime>.WuwӪ:Wi"p&CaL.o `ݫBrh-!e>qv[/-h\um}rG ]#?#KI:GPck c %gl6EIjX({.D\[h˰xyV.;[{{///"w:"%'>c¡nNn6UzЃz{}hOzGP#Q{{NW#wGuyCzskЮqsykE0o2O]dsV}vava糞O|b;sUٽuʲ. !7ww?_)SQ2s1A3ys&wjKJMⶩ|f>/'wa!?R-:ΩEDQ;e3袾*-m3̹ p}D6"b:Bn#My C 0Qr"+yXg(X3ATycd >No~޻6b hpfP>@LJ)NB- ^iL0䕉 AȏK[+bAh!p)Vop o%$U~A+G''I4XD6\yE !:=wlF:@Vk_My(2L}yE5V,<pK/^Ɉ&]Q?Fjʦ 5gO紕Ul噃(I+ \+#ik{BLCucG?,ni0Ю/KdO $-D;ɣCΪkWV9JA(#8,䟹_RzO3nǵB\}2PI-pXP#`"{q=k&%3Y A1"¹"U雾䋂{ <1}eEkA-!?{c?^Zd"yw?Ʋ)c c?jg!)"Xĩץ|o( vʃ1oil1G_'ey"*9KyinyT}~do3 [osu%Gdl  }`۲a`uĆKD5oBxUp"{n("6nWcjǩj3g8egO6S3{Kd3RL+H fTH`$^[y01Ni`zYEH<չ@D6C` O|ғW]N:J㑑J:o#08d<"NHK<3{yW^I}pa03.04 )ɨa$ X-)1yAH2yGU~yG6lƩg:H!uŀ7p2 oN^[ez4Ģ9uMV.O0.-"0Is~ 4mr1 L;j[ r fsk#s+""tbommyDh)3 m۞ @UHEc‹΄s˓ }Rbl+y-҆(5KyC|K²c+Nybҏ8!n;Wey:b%n/S<9b')Y7O"ۮHP;>Zo>=C9R:N-$%qq`\0_{+;]a?e g9Bdޯ.<8Z,o퇌'l ɲKsW/ 9Z[Vv}>9Ȯ6C;PH}nUu>vǩE͋ ٵiǣmf\sM8-͠Fv m5S[ASMx Ȑ~"cbEN|/!R-R|y EދT)hكLKk#{gpJ$H߄C'?+"<чGҘ0ZMxi{0=V~dzSk.# u.VXCeB0CP_W7p1D {1cs+?±H0/#c>c bP䙁ɠr3fyٗ'|{/o2L[E0hen[M[Xb9d)l!D{'SuٖYJM6UjUcyR)B~׷C814ԑӪ:o3ԅ~+dkE{:hj ,4EybĤb@d#=}G"y6ӯbF2FDHvۥ/o c MP{{yOu(/a' {\K,EB}ˢ+{~ ʞu4l;}po w<kq\:w@i:vؐ>gWy}*YD6S "MBt1 i0Haն‹l^dkce5}׼)KhSi#,h󀴷xh_ǔ9 ߪ1}m;|;'~% !iט2,9vԮ4FM92V qUqMQ_Dvan.z.sAtWXbIm@qm9yjup( Fc7*d׍9GN-?4:e~jն/98Vvú"5f/sXȞ.䭈E亅aȆ&˲HOg¡el"c5dy !o>NnEDZX\6SSDQ fբkWc継ed=/mD`ol|K`%@&Oϩ*uVΈ"Cm6cx Iúw숬|‰f"'2ì*D]&-:6gڮ %0"6!<Ő 4Xw}B 0yT`5\pApNy?$-Z<"8\T0a)R])uˆQx5 [V,x9A"m&zʺ Fdzm7Dnj nz ɟ,SDv c,f4FԽ V Žŋv۱s "䛸D=ghSJl\^֧6U&}C"{QhҶ~k `F]mIkL}\3ójn-n(K-::X}1iu'vXҟ{CA!B޼V=mp "9Eb;uo}${ƘhHF %ET.D{Dk, ò%+53EOؙ*{ף <!]H9'2&scD6F+7c}E"V[lx]\i_'OZD"9Taoݜ-;W"+SFyN!lUsܖevvzE  =ݷH{.9?uc3 mc+`l7ю[sG8?<)"{)6Ycn^Lj8W}8&Qٜ>92͌߂qD`o얨zc3*'a#lׄ [?0INicװ Av1ڟ8[OD"򼰯<@ߤPy ƼEn[lG<}ʑ^6m!ܣM d_11q/?o&Y%&^\]6hxU=Hlex-+|bnj摍,zdcnc(yXmu罅r'وl m'2{(pK7' _AGT@#{>Js.jΕi`:;u]ۋ3.뛆=O>Dvye88vJ;l#'x=vi9'Kd{bG4J:l q@/sCHad%=w;Cl9)xx-<ώt[plsxoߒƋ^N+]D7bWϞ:'i."sٞw1>[su "<x(L՘^چ'R[s-Kd׳Vm;TYi.9?"ձv?EDd<-c2$uʦpd]%NUG,晃o$cMnw m3c:߂qD`o"AЧErщ12%^O d} BPu-YX -g.qw*4v-ʠZ qH5FlFDÌȇp:M$A "b]D[˳TYVָ491'agpl`/"G9 (kL="%`3bEem*D6Bu[q n"C"[U ښY=[oI痑¹M)r)"ۄ]ZDC> IB:[Жcn['0YSe) ՞stDeʏ60ֵՊQhA#/SlDJ;\Hڨg{oNt+&]vYֳK-i%BCI6?Zέ1"t; f^nw֢lwf˱(-|$Bd L&VFTe=un+=BhR1xĸRO.cJp<}%pl$w;g 2=g+~8Ws򏄱B;t\{KDJ1yQ^["{>v(&2byZ$Cdf} l'xꋐcD4"UA/9&_xi\t,{v:o}ؿMl3y vA"=ĕ8ѯ{9$[<+?SD6ۢ]SDtl[vV"U[셻~M2%b7Ӧ껈lz5>ЁڑlV2D --p7 =v>,QbќcZ0Y"s:Dn,\Օ<;6&4U=9cpiE_dKzP57:㏀Ez>,];<.M`X̳P˔]يfk|\4-acG:3u3.g-0A6chh];ݓpc/JkUi 3Nƞl["[Hb񚎲!AsiSc[ 8ta mmV9Kh_vgS6!&,覝ʇG.VVaOWwnU ʜ_6\ 'k]ame*5/xDdm/TR cmLxˇPZ5VXz@<\ TXAdfw""6?:DtO}cDΓې!{K.C:&vF^|cc@V\%ҶuCЦ$qZQ1Ă$l/M r;v }WR/mpg0Q܋7ElV, fPCA ;uUk"<[~3輸Q#%H=`+k^Z A OVnmb`+mb΍]KlwCm }Nw"[%eSMO bW$Dd1;ʽ\ fjBnd*~HJna""uKfھG# y؎!ƒ pz@y "[Kwm59l<cwk[|BdA%oL!TҋwIBdRm,SX2D/FZž[7vBeJVA`olq=$CdNt"EM)qo KNg  F sS/<쒋KwZ"W_ 5 -r\ @A e["7)׭v=˨Q A  !Pup"wۍIE`ױ%!:) @A mA`o3<-| @A]GuK`\sMs+ DjB@A  O-};ݩ+wzwCn?}Bd~=$ @A  p?]{Wv%UJZ5 @A 3.~jB/ @A cٗ]vY׿Vꮺ>VvKb>$lcpA  #8loq[t}kMozsR @A  -._ޙ.wi[٭zn @A l)"7AO\/|ap97nw6M/"ۧ^8摽ɊHA  @!0$!~W}x;/ꖃ5W @A F`l7{ы^Oj|;w7OV[<وHA   ߓ6{yy7MN˗ @A  /{IdWW\g}I[yA  دzի#guVwvI@A  !D:|_޽5Sfݍnt3ZϞ% @;t\sM7 w[ߺo3LA@A  @躽&)I^{m! @A#pgtށ9!@A  n!DCQq A  @^젳vԉ=' @A  vٻW)QA  @A  @A`Sՙ @A  @A  vٻW)QA  @A  @A`Sՙ @A  @A  vٻW)QA  @A  @A`Sՙ @A  @A  v6@IDATEۇG 1P5>}.F K)o{=;{Z)" " " " ՕZ.*A`fzk^mRya6ﲔ[uC[Դ_QT7v_. q+Ge kWrQhFD@D@D ] OYa!nfBP۹G+wf˺Zogr_ =>Um;.//v-5:6EM3x1'lJKswm gF3" " " "P$ Ea" " " " "P oOq>kV;~Uv=lޤCڦuQȞ0ow)O_#pg?;H^VT#QaRD@D@D@D@ Sv=1cv #b_waz~w}3>Ʌ v^чHO[Fꭻ!e:9:4YkTg ]T9ȃ< i(O4}Ļ/rM->fuWaST=6 =gNZ-aPrŪ  !; m$01 q}ݜ )zU\:lny*-M&\auj/vX,~$#Nkkyw]4@>wEs޺I*ѱf&u9sh[m6\_fV&Ey nen?}l6MQwn^vMC&GFDm.ӬҦ[}#G3_n|Տ[;xv~~v\V Ȧ13ݔ뀺Q7 X}c\ 6nL_unYc6}s:9cϓz6HDr-;+6/#[irceTӢzy+;yL8QZi*" " "Py HȮeTCGmhLb$D^W.m)x}6!7Ƣ/"$HL|B٘rg>C,p Ë'_n^`Z_PƸAŃ֍׳_ދFo=g 綱a$ s~Wxm"!΄[7u0/+Estom[CeV;|=l!ci}6`woAQg3';8lQ$wlQSj@@Bv5(D]@!p^4W _l1)ky/l+d/ "pLvV] x#?"K,0 ُF+׊"ѸAzI ǻ8}᫕&eE?!__W׶ٿG;y>`6CȞ2c;~^з^uC_~W ǯBXwipIK"}mH|ݾxzVYN'Qoާ;oRyyƇ&!;y$dW߲ՕTBG5)'eP1#{v|4#+/F!1u"Q'>B!T_ Mw#,B~"G?n´Y{ڮ>q/k-X$G`բ%^ V1%1 r!"oC7lYZ䖷Dzp!_Qh#.GxGF 'O)7>Ǯ5{Sw?"f{'륝N'ؙuv5GTD@D@' ճ\uU" " " " N7~=O̕Ui۷};G(d]5 w[;vy2iΌLeI :&E8f]ZĢ;w~N=Z9Bk淆٢#< vbq _hNݺ۶XwW$f[.ю%l:BLˁ>:YBG"D@D@D@ է,u%" " " " U~Qf42;b׹Y]=m~<[N"d_ :-<0^ t1_>em/V'=َg-G]"AA,vc~~o4eoلl<o4YO:dvnuhDǎjZ<ޗ ;VJxH%4({$T'7^xV?r)B20&uGybAANؑg=>.|gvvc_}(n;v~Ϛ?1|Ǜ~۾cp3(ñMv,J&8BLO#!3W&4;?Lp?FFLc]#iل-"O3 C2~;wx"6b6zwy>ckr:{*daOq#Qu9l/ b]w ZVQxC6c&<G!d"X}~dZ$dWՉT2 xY銷p.+D;/B\ևB6(/5?6tx<󈙗ﹲCpƒo2ݯw/&W}2c;?J6!;%]l~yK{cY{WݲNOjO3Rl<}\9AAPk{ۿٶY||j2}~ jܸː() ]4`ѠXR&>2T{}E@D@D@D@*oPg,¼#bskqGoxW? Sݧ XĄr ٤sJŎP1 ~8#)~{y G?o-[#vt*fh9QfwÙ"_LJՋ9 'KP<-]ϋ @ oPf/v8o&ʅx,{ j埉!C[/ IsQ.:X'" " "P HȮE l{72 o\o^|fFLD؇Hx&ÀᴢFW {NMӺNpWKnH8=k~f@ 4K ?)~VBv(  o uͯb'#0Tq/e0KF'F>e ٳ)wxFz H=1K tL\WB{OHЮzv#`IOQGa!:;8Zd" " " ՋU*@(eAa2#^ '9q[umN߶lgK/9(![{h@euvbۿơN 7G^fae֑v(&{)d' 3[>I|D)w䁎uǼ&$yD+#OFLסmiBျ>?y2&)dϚ/?#>im>$$ҌN Z{H8K `|6=$mbs?X-k*" " "P HȮe+nzk{ya\mhQ,men:m]iE FOu=60>JQoGO=hwPNl(7Y&u(w&u|A `fnuںQm>~_Md:Yn(~YX-F6DžaKXҦٱc/#e1Xl1k;}te6,CMj OebBsXB6iqW`jr'mѭLhſǑLҾHݶwwoH}b27vGuTEq}6l4iWoڻwo7~xA*g4cժʭ<5R8p;:ЦM/ˋjO>q?s|}խ[7^̂F<2멧r;vtW|4h{QnnKzr;e|OY=8f ۹ Whoӌ,J#E{>曮Yff$pGof @bk?׿kx^3" " " " " " " " "PHȎJCBve1;C|.N8רQ#׭[7bD1ԙ }>wNVJG+E@D@D@D@D@D@D@D@D`QE] ;_|?`?x ephAD@D@D@D@D@D@D@D@$ QB_y晸0Zk- +8W#_ESNclĈ^sv'NtK,?nVsnЎ{饗x ׿[=XW~}RKm&^_~{ܸqɓ1aH\sMsD3䝼`}}_ҷo_~bJm~L{ 9V|<]vŵl2an„ ~P 2ĵoޭn5pڵx\rC~Cu>ly-z׫f 7->_sn%E?>|uG k7h ;wv뮻[g2K8>'r@u# !;*PFtE:ˋ{e߸qcw嗻u)[[nq<ۊ:u+mF~{?3Y׮]݃>73Pjve=}p3voK]ڵu ^wuq:e6F 쏰{i[f+ e6D {y睓R._&?9M:ss { 7>}Ļ5\{1 kX?w0?_V-w!#<@V/ٗzdyZa/F@BvTلeYƥVŃ᫬#^O?d"o6lׅB6B:G3E]vqx9sAeW^nwqGicqGuTo8]iΧMoO>dwm7wy [Y{s̞=3 ;zowK ;{c%dǸ4#" " " " " " " "PHȎ 4Mywbn۶2,:uj\nvG X&s={"1D&޹x׾ qkڴI `3 cD#|6\uUj,YFE8ƛwbW_}ݶ}Qw!O?b-f?[~}V `IGyċl#O?kݺ5yC3t7oloy Sbx[9s-\9Z B@Gf,[oMG[n9 w7!S.bGm&fWu- -bd4N$dGM&117o{ғ7+Cozyn3ZF͈_m*~aWy .5i҄Eo~;SmѝtI>L+Bv{w̙>66/0򆐎RdE:xan3z9آ;S܁C0bNټY7zhWy''/i3“$LB}X;a~E? ~w /nu"w{N͘13tPM>8K/cw3&d_0&ad_T7&diuh xqū?#r M6ˆɀ~'}yf$67^fلlWC#Ƿ~W!~g^0GE5Ë< zh G8YbI-x8S:+EΖ>aZNCڮ'4s}8C݉'4[M7)gg-fIRȦv1@u# !;*lB6Z2|" ۿlmx !Z>B6*!5L qw azلlDP‚v駻{/^_xW_u~db?2vXh\vZ-o1-!{ҤI뙐CNZ(dmvC 'qwa /{p4hⓛ뤐ݣG?HTg&d%=rC! Agzmпxj Ǜr SLA`B_>mv ф AƒB{ga?ܯ믏3(ag`fE_i:XME@D@D@D@D@D@D@D@Dhy ل (kkժU\g؎8Jdg}ֵo/&kӼB) |G~EK#FDq=tg}OǎvuW7glI_"j_{ecaԩ^e!OXfs;dz!fime/Hx&1!p4aXBnj$絭cNrdh FLS@@BvT%d3"|Gc=L=+!-0~I]Nlp 7ےӄLb0٬lbBׯ_u]nW㚐x!#s }fW]uUNxL)B6a98^{͵h"^ƓrN|Aw7 AʧCc0Q0rJ| 6x~ P//!:t0"תU}ޓڄPOt>v8̟/aCw6lG+4~wOCc2d_D~ݬYa\fe$M-$.O!Oޣ:*n3Uƍ{ڬo߾Xm=0޿v[۶۴iӦ(̺o.U&ozq pRK-kٲe嫯fN4)^AKz뭷Nnʸ\~ɓ'gL?ɓB&vh6@c0ևSnvmc$dG^B6uOx _>_y/V6,fB6a.nK3zabqcݻ/!f0B[z¤ 8ix]P*3f($ZkYk! `:t$5\c}}&L aE300n>vv֭.! -05R^Xe}x z8R>p_~[a;Or墬fro{뭷݄ K,ZhV[m5ۺuY'e-/BY_~'Ovz%tgqkӦtAe˖+G>3?:[jڷowխ[rf>\R̙nC9ķ/yv?_|5nuQ$:i& !rr'" " " " E;fϞ1 ڭ)d)>#z1rhۻwo?߾ @4|gXbī:17|{o*[u+gu߿Nsq5*S]矻O<͝;7.f]^ۯ{衇\.]q^pW^y9sfF͛7lʘ"PJN{uy^ɼSgܣ[fe|.~oz)uH.Jv_ovm7ױcGWvm7fs9^ryaRRϝ<~u"=\۫ry ՉcU wկ_m喾~vM>ݙ{K&:jUZQ~_[yM4q.kԨJe]^z%[nn6rCpaGq&M\r!%JzWl>wm{r]G 馛:BY5Y}WS ϕGȊR^Xʺl/˥(E L ֮]Pbly_<:r,/!Jܣ4{W1RZ|v*S]02ŋ.qyEGkԩh0<_L=z"yEiw&~. k߾;N8 ܧh1*sbpl*ь=h0K x&vm_V_Fݤ"& v\ڔ2{'&s?s3QƓG6Yfn4ݲ!))1ZQ1ң=8m*G>أ|׸ǒeo~^º,xo^zi/W( vBv1m@e 4|mØ!#^~Ub2jF {nB67HX{a͍>͞΍ˌW 8ă2EH#Ac.f`qOM.s2ip\g$ ~[Zf}H Eޅs"sO As#19< vG ~}4p_f"ʹQ c47FoiF695$-b˪El-gA'N\`L+ku̍:M sZee[{}Qln:pW\qE#sx3ԁHpl*F8pƶӎVlV_0>܇Q4y[{u@92ʈE"_G;DY&Ͷs\$Ǜĺ2^Dw~;Y>7Hz s#'R;GӨɟk:i=,uRkai=~#x>//}}mʣL˖ |8^V #;O\݈g)S9z TM.:4햍nKݷ{C3ix G2\ WI.KvL[}Tyx.#@cS qqF\]qnĆK6_BD^3ꪫ5f`7,G6^*$'j& zgbSlYqFb-;vwu%[y@,CxC9Sޔg0OD8HQ'#;s1~6ViuX6QWʏA@\]}^ûX)JpoSGHKf!eʡ)ukO}mw/m0LxyLx oZ=#_p,p$d6T?&ߵZAqx8<݅^+u ϋmriyH~ŲbYV x!q3SpSKJJEZnnݪdݯn ] 1y 1hԉ' C,d7Ls<]lz !=b; l/ұL"Cf$4k<ˠkGxǑ{{=!s0ic4([™l6qgđd@blcbTD]tG.F9 ٥+a}ᚬ 6p"\)ӆD_!BQV*ƥSNÍ}11Lf*,W)L?S?& DHbZAX30E+GƈH3/YXP. (2Cev.[{>T՘X!g':opZbk[|`PU8KНn:nbؿ?ftn՟E)WA ٻfxѧb>R^Mnڴi2lMENrr ᚯ1c׮]}lW"ǖx%db^fĊѣGlڴm[V$\Q܈-Q3_$ܟjB v\1B6b'ujZiL G3SK!6R&&,Vl.MyU>ĸByA y)9h2u{ŻԌH>xB#u -)d5IڄhdM5w|x_ND!nڄLieZglX+}`.¾ y`? ŶQ~c)l9+=` P:VcgNuoծۥKC׽s`- _gkUxS~xG0A&M~s0͚3mB* ~r_^fN"H>5g %1*`, p!GH1PjZy@IDAT--{Q}ᅇx>/|'l !+_!ȈyfB'eZ&6ŖiW5\ g">e{wƔM b`NBm  kޮ ;ALNgٔp V1uO[eSu駟|G 6H}gC"֭c0<*Bv)튵o6Db6+T~ap5%:^vB!q|U%(o#h3Ot|$磾Lg역MȔN\ߡmx#B<` Yu-](d2t@f2ʎ2!1ڀ(L?\C)li8l(TBf[?=/^3WwƯ~sm*!TdTD@D@DH &h3_ZhAwBJOm;- >'|a ry(G( B Yl\ibӳx;v^%b/рQoJPTLvyi: ²g5axdq6~eO);bcIOLl-+QQ&TK6nBhTV!8z2-1~:1 SCLF'+{GnRDdJqtE/hw^ľP.] |S <] Bl!7HK ٬;}a¨Jl_4`4f4x&$gB !|H'ucܱRkaVלּ?^_|qԑĽ۬J53!bۀ(L?XŶSB68!/~g{FnZ" 6]u*r*" " "Psd(CeNK̀lޢFib 7WSrb#| XLMߋL^C 荥p !^'͈J^ieIzQ7ad:3gY߳gOsxg,r لP10M^쉟LX˝uYLl-+(n6!4ՙk$Beiᨫ3]JYb3a̫LΕ [*e{{ !ـv]J־JĽLBP!{=S}I4أGݱcG?#䫂LvǺ{v}MȔV>ه{OC\¾3KWg ]lPe<֮=sJȆBձ'd>r{)M%ܑq?4KȮ:Y99B6ye3mպg`-7vlwH&njWj/ex'ԙ 4^-.LGR-w݇`@5"N{.6=Dv;0XI3Qx"e{Y}Ҧ^ /СhL} MЉ/if Ji엉MeE<60M TPM\N )ɴӖ?xG53yM1q饗5!󫮺jfK쳏 ާbTD]4Mk)>z2EJiW # it`pBlQB< g*O/ܟ"#4ҥg;a: 'EG:ӝ6br !|!b'KyBUuo*}V(C:4߄;;is~S(dGf!-=̔Ee@olLj]? Z(`KȮ\T@R6:yٞ4[:u׈yb$:h / \+{fD0^&s a px!gG 7y؇aO{ :wHG 7O\3e!ʆ xsO9xAr]eyuF1+k"F(;ߗ`fŶ+7ҵ4S 9/-ƌn6%Gƒ|~e6v'Jbf0Y3(/@,[;m!NgM9w) I0%v#s{] >T<@l8Owy'P. VrShc,i,K$ނ&2qR:87n(5$l3 # !'(<b˪"N bctPPg&1xB6|=h0:׈a*Lh-pS`C]4_1uXsw]E8%T S+U)$AGEEe;6a]l}(K\:Zqc و&ZMs6(N7m<3@gVj:n1%޿tp.|wBXg(fk"r>2_m-x6q/>/ԃЊ}dw\t`ӎO;B=~C!i)ic~e[J{@H:}1N%qB Ϗssr8i)n$dW:@V7hn7]yfMnD%C>}bA;T"C[o=CQx1Zh^mB8?D!?3C 4!Gڶmk)"x# .|.EooDDDB!>"Ƕ}FȡC! /KBC#ؗ_~{^s/P* ٰdR|æв|ȰXntt :Ct !]SO)۰A%Af&ds!Ý̸Zk-GfE"箈Yܯ_BԦS{:NĊiWrot@qөPHBl~ȒvѾD#n6m:^wu: yF gOJzΈftaв ٜ_;s3ϗZAzG-P1ҠP#,M15S R'6tPGe@lc}mSydWGeğjm9cPW]sᅷl"X'Ę4e)y+XBW4lC/l!Ba2cx)r_w"B~6a*[mŖUFbq}t @!T%ųL7Q/rY(dVĥ霢'_+,M;ܯ"*u.Ѯla"v+kgr{&|oh |y7S' )eM ZUmwP[Xm@*tRۃBϧ.+dzxAMfjgcpVgI !o-HȮ*^؟~%uYN_{]N<4![hD@D@D:BmFx:mKȮ_&" " "P d#Tbc3 ?0< Ugk!0e!P%A|.L$dg"" " Օ@]]KD@D@DGvu/{}O<}>1t1N b{"n3T_"RH.j-X]gp'(WV-KY L+5ʍ=u5nܸd?CVZ=zM2}g~~m<" " " O_~g'Zݖ[nzRm̙ot;bdbJR$due.7n8w;VVJf~m{I3s=~w-X)б" " " 3ܭOs3QG;Y3ۦ," " " " Տ*"d_5PBs_c+^z%7ߔЮ];wa-amM%dj}DeB؄YԡEΝ/߻}Cq}{=^X?߼yfD@D@D@Ç|ޭZeVmS B1B;?NEӂTI{dW6!w>.tZFED@D@D@"D;Ӎ?>5uq'xkذalJȎhFD@D@D@D@- %'Nt /*nڴinnȑޛ}nVr͚5?ۇ ɀݻww열W^y;vT*VƳ]vu5B#|;tV^y]a<֭ZnXc K*34 8s&1[kbRK-uڶm눑Ȇ%Ba뮻n+^zW_}x}8ù xmasi*" " "0~y睬8xβq2lJΊld!C[1اY2LWקO^zU,U[n6lz!1E]F-"zK/͚59 [nM0z衎CM7m&W_u_|Er>hפI!,r̙ lܹw}=p#/s5\!J3#/ZI]xᅱHneҴiSw).: x Z*GSy&Mn6$8 6MY"V< {{zlW^B6|KN:x<j<z|cPP~g^㵽{mӹNn+#f N]tq+>G{np [BvE1NK116l3 6ʿO&:}EwxY{4<Ft`|+QqYveSh?L$dW5YBOBv~u:Tk&!l*/Bk׎QSO= qqǕ`^{ lV^B692^zfN;Èv|҄y$^7tg?(cxɓ]w]v%>'|/,!פy mS ٥3 )dfG[TE1~Yk׮ݢb?/(B=+ _KȾͥu"BX![v~u**_y,@l?{[HZ" '=C~TN7Hoq"MhbBǃ!0s § B:ğy)]v"]'G#a߾}} O>9Lbn3<-xd#c] +.r/d|<~;bO'wC7r&70&lQFy/=z,p+LHG`ƫ;t }QM$s~,d[ d?ۓ߉oh:T," " "PmMw(h|y5YȆq=ǰ 8[moߏAy"D<|q, sOGwa￿&PxL 'aOs<#8p,^X)e~V0)4hvmK7xK0R0}Y%| GSMPs{Z5|6)q,Nx$:7yZdB'SLu+u81p&?%Xe2R_m8I O>ݧɻѰaC若i)Fv!uv](xr-}:h?߄ĹLF;1p^u&?m_6!{ +1Ly)>#`'Cl}"u>[]LˣNچBNO^||]k! -}M+dnt!;Mi!0Q4K.į󗇀cb.?4.{GP!< j N`I葤b!W\:cx[F#/|8| <!l IC;묳!BPzW|ܤzW;BIδc Cwuץs"dhr9#w}&ϟs9n=b!&kA+:5$I^Is=RƓӜj,䴔2^u,툒\s/+V'zmp;3'5`0Ms#N>+4zȋ'}Ʊa~){۸Gy_M3IB#$$_a4{bR^_q~ q-Z{ K kC&ڧO*ctx\Ŷ2mqB@N;{}~ttI^N2]aԴug_ 6O{:NZ{ڠvZ'iCi`TLaT%y*Ϸ.ː}e @M)Li"ʍvAj+Iy9@a"]^1<-6!iB60$B6/xˏI2b4B7^ /0!"{,O>IE@D@D@f`Sd!Cx0m֬2{1Մ{33a:DnG<~i.bAdz!jDBlϝx#<Ϝ9{ "Zc&:[n d<}ʋx!!, w<ƫORgxQ3 "oY_E76e3xow!7&/psXݍ!ERƤO!T##oP;o;=?P@I:G4eGǻXxR1m/yiqx˗}Z]LKŶ!{ŦmSye|utK> O H1#?,W>O"y;!ao^=kx?>|)/ #O4i|yM]|D@D@D/e!pryd&{d#$|f&rQ ±bp "b/,:h"{9%f"Hwm:-m9Bbq6sa%xo\t#ѕ,9B:\0ߞ9wȚ_n]='Fg p&#mp`[Zu_⁊` ܛll%_:i Ob(ՁO0f#Fx&̰P.T6wp`s#%>FB3AXo!.7/PȶkM$CP^s#Vcg],\r޴v"Zbې|ڽbۧl/ᚭn> 9lBv=y#O*Ə)i?1M։f><Wka:3K&mK]%?yGó" " "  كlj엯]{E1 /9<ɳԈ-IC$FAc{ _KaHoC;G`1i SK&׈j8l;hbO1\H3ۋr 1# Ǯmfшp3iP,@XLs@LF#\>VJB1-yNFfl@e>ilŹic9"3uzϹCxu(Q牙,b[Bʞw=8EdhLq(b>t*qq^#!9gBVd3vj$1ṋkCp0H)VOaރj@#+-_ =< sv41ޣ ۞rV H.gk{K9oZ]L_)u|۽Rۧl/an> 9l]Gli?CA~Iz q;Ώ296 %?x" 1xѼ,Q ttRI6M4P1OLF\abBg,|Y'qt0DG`iuu@H/&pd8fP!O3\%aEdiRx>u&r~? :gd](VZ&!/*_u7nu+skL8i9&RuÈBv2p9[}ZƉBЫ:cY\H _ d2 ;sncAbBvxd oB!BBIp 7l|@ 嘉O;`]l]+*tZG]Mӄl+C.ʋ)R'mCiM;WTL{Y=Lutaȹe l م T]k>E4hĪ3?<#׮]VSx6b9QxZ>mWȦ$ @c) >C@Bv\ENo,&m'Ek ِLً{6 AQ]0&̊XÚuŀ(quŜ"9*Y"0_ӡjyuawWWw߉/]*ϩj:+2?g|δR }ݸ(T*,;R]Sߗ~e)uj۴hA:p=GR/'?Rn~Ƥ"皚ʌPлuDBkO]_ts5sܯS5MM!B]uL[Mu@uF*WD_~SO @nBiZl\{Νsήĺ!>(Z+{RC\~,֯s[ ̉zjUi%~n<ϓ|߽l7vҦڿrRC +u5}?}Yg^R[M}d7ٵE@@ эm` S LmЂl*Nq=g 6 EAfWsZJZ蠅BZ2 ٯ*7֭[GSv'hPh9s\ioѕhᣦE{h\ak><Şcţrݯc5/-#Rh d{+g *ÐcS9 pԵUȶk.%)zitνk\Uhv^oUn6h_B,U9T~M>{>ZTPM7O<ٽ K*ǡֵ_1dZ:*Yn9:J5ۏ{[vs}o'm+-{/R/Tg^FJ]oM.n]7W[E@rlkq2?]+W?ZᨯgYԩz0K}UUzVXO>^q QVTBC-;d/&VI;\X׋BK՜epHk5R];tʩ DY5?c׋Uu5o {Q!g J 3qczjPZI/o~:iR@ .:40z)0S/;K׃γRuCmd]j0SV]E7LyR8EU7'h7XoSo ]r%H%8׿/Qi@V5P_GL&N. K*աWזݯ5ctAя; M ѯ_1%_{h!x'~δR rF$-g0-,;R?#IXg^@k3TÇT7ֹ 'udL?ھHS?l@@ lWN3lh=@ԇٹQqQ[Z X9U套^r+TXEJkpnz}7m[o= PWYaYLmVrJL:~u.E˪B!5mOǩI=s"n?~|ɮgW%dO1bDFo̚fmz,k}:VRJZk3D@} hdsV@@\J^~ZC =څ]>\Uכ#W=uJk}Xc ?ϕz4EMMTC-;ִbrMbJ Sܚ.S 1jҍ@zeܸq,AMu=Z^߃}uϣޫ95ȷOq߽l7{;irRC +u5}?}y\kD|^n ^usU@@Kt=6 ۻ*ܝ6mUޗem >GBhȩRjyu(QOh/躢砚j׷TШ zYzd!_ ;  Pi> s=ݮ#3XN;o@!|<)5Yl!dGzdd7KcD@.CkH5ke˖ĘA;7U@92B UdebhA1~#;bq9   P|K},=9@@hA=~*Sb?r  T}lMoԨ]4i9w    4 ۼyKrX@#`\A/n羶C::Ql@@\=辯*o9.c?̜ʋdؾ +Re?@@s߭ [eE\1ah]c;@@@ 4 {Kkw#d"  @ 7ZMAv6mlw-ZeQ-#  @ .~yvs[rhiL%xD@jWj [j6?u[Yώβ5@@'}@IDATH\,oow_Ҷ̙cKF\A Y!  W bkƸ Yf֪U+{w~3jk-/&  @:'~]AFvlֶJ&.V5yd@@#dBl-:/ 6@@YA:&|5^ڭmr󠷵RkVMC@?[ƍfON{G}~mJs   %`lss?;Z.fkof+.:Z5#.ba@@ e9,gn&Mž+-a{uRHyX=  Ԓ@ [_jwMjfi<" ?޳GK[s9ʉԿ#  _-ѣ=CC@\N[m& X=C@@,X@@@@@ m촅Y?     @Ye0     @i ~@@@@@ca@@@@@f     e d     i d-@@@@@ .@@@@@ N[#    %@] #    -@0G@@@@(K ,>F@@@@H[ ;ma֏     PAvY|,     Av¬@@@@@,X@@@@@ m촅Y?     @Ye0  C`ڴivglK/tƴ|1rHr+[~rw qlʔ)g۷j/ /'ہhڵ _W3f̰n)dmٲ~eFC֥K[eUL@@kϚ-!  Pg^zp np@ƴ|ڂ .l6jԨܝ-{yLwmvv5ؘ1cdz?wn^{^zRY/}]0`@8}9M4-":x  dg˚@@[>}؟]mݖ1._dWF7߸_ {z{lAxK./yD@Se  Tz iR q[ mرaiN:6lS w{n_|˜|ɦd믇ǹ뮻Z6mו뮻:T/~4i͚5+ fnm4^  @ɛF@@*JSN'ܧ}J b;sA+AvAXa3d: ?|{£ѣ]wuk  @:鸲V@@*B`Μ9;G?S륖Z}Q[lrof>=6sL+U_~y~m5Ll[n%\ +`oy? PRQeS4H?gs=;v^+͚5wݕY0agܹssζkJ xRAjVFG}䎣qƦ0mVN#V?<#\d8u믮6jk.c{~\AvySNnR\7njbJpl{馛\'~h[޽m/ݣza+4VkҤIxSB{kY}n][vo38vm7vo#cA%\ҭJ4P-u_)O<^ys1%  PAvx,  @ ge_nUV6~p`O>gp5p`t՚Q~7tSZ:_lj 5 gWmk5Ƶ~4limy$=?CruO s"c@@ }  .O~ÛoyFx K9*V[tD??*T]m\O]v% ~ ͛6ŗ]vߍ_{6stSO=NzWᛑ'Azz92j5Z\f;1_ꥮu6k,W2߶SGi*qRі]Fa=d?O*ֵJ+믟^# @J)Z@@R@3y晢v!zjwqo\+쭶ʕuк_uUzԛZ}dglGjO81Tj ilV5=Krr}?ؽ~]w<ϙBXPOiӦ sj}k\٪Q)!;V4@]fk  .: ;EBUUg~, ',dkI,Q>j`J6 }Av]s5[m 'OlrH8dvTqvG)Zqrsiܔ@ Nϖ5#  P'7xcF֭[,wH!?N9r)}²#Ѧ[;ե. z!)7N;+/i=Ȏz^ٽu3YaA,30dUכ<{\]Ǘ뱶l_{\PH\4@.@]݄)  ki2eJx ٵ7'z]tE-.B3fLXoyw 6o EO{2'*wn_ vo*tر֣GwCA_"۶m_ڗ_~i {ld 7.'i댝zQAwiÇ[g  -@]4   Pa+j\̙3o߾a" >?3wS\qo^}UݳgO4iRIiEqATcu%J ?k-ײ?*gYJi쐓' $.@8)+D@@?|@uQ^q׻=z]{~rcvRd/XFzg[lڃ:(|ց膁l_<<'믿U{k9Z񵳳fw/Zj)7'%\ҿt%JngK/6hzdk{&Ȏj@'@]{  @N~~߿m_z݋4:z}٭SN{\ ۯ?wyDžڵs4TNCPq~6&wyv=?ZiP9TC=kjݦTYu~neML=Nj#F7u%o3g 6W߼kܾ[#oG Ex @ Ȯ_狽E@@Nf̘T,GMU:ԭ[7Sh')|_i %@ޥK 55ӦMseEbGke״W>w\S-sm3 9ׅA@a d7Q#  Po>e]3 _!zx+.GuT"N; @E 4 {oQΫ >9-Z0 tӹs炗aF@@(]_~>}~-{^ HSLoСCu9U( . |4?\x&MLkfl^I+d!@@+ 1@a͚5N84$ pc@HSٯivuS/^zCPl ڑGie5״/ܖXb4F@@ FkQuURdE5 zسgO߿BWluC=7  .8p25 e}%   y Xu pM# P! .l\{T"+z6mjOؕ:ɘX/Fi+lý)l3f̰n7W֭Cq1k_|E{wDfxN'ƍ˨;_Khw 9wkK.=AUf@@@@@* ұcGSVJ UOvۭ̥^j7pC2ȘV/;}n6tS5jT]NA~wmgZqv/+Rm}͸r5ؘ1cv7ZnוDs=ސ!Clwv=uY.W@@@@ .T O;Zu<|7w}]vn-cZ]P7IUs9Ւ>aSOr/4 [gu<     "@znf78L߾}sJcꁪbjުicǎ KtɶfZ߇b7dke]6j:tE 7tuv|6mˊ},GancP_}/nr-E '     4 {aÆUAl{xکSN9&N>c'|cAv(x-5.+lbsG7;wnxt]v;|@@@@hAMO}-g0;_+NΙ346ӫW/O믿vZj){Gmݍ?ʓh޶mZkݲe˜z +o_3gδ'|&Olz ?3A-t\}q6`[eU`)h?WYh v7"4n^uUm{tSOs/ʄ2ձVط#8CY~{[ovK/mIZN믿ΏʚÝS V=uհ/%dk;_|q uF!<    #Рl첋͞=;Cۇ ێ*Wo={=I;~饗s5@T34 <{챦 qMʈ(Vc=>ݺwn_N~;g7 '`GuT\pB'Iwqixvq<"   T@埯ǵ~)4EClQj>f͚z*TB̑#Gz1+PMZz^ꡭp/20`~w=j*/^~]j+հ1,"q u4 hMM^{pV DV=cƌ{OX-KMvGH ;y=L@@@@;d>.>-o?܅ȵzip4pg_|{OA#N9{ウNuvBV WE۷~ndeFz 14ho,vmn~pu/GY/sYWJ ۷oo<@jaG:whf .]u_xprZAv*ozvUW蟼V5FԵOi    -lz>}}.,;=?pPafޛ sW?~QR 'Vm\/4U=jXTDazgB u5d4`?餓2BgmOǺ;# ajaj*-߻t#aȑy4&O׮]]:+%ȎG B袋c봂boJ@@@@2\}yZ 5Xz~7ȩޞiRꁻKܜBP)qWjT; U}Ѯ޲gܸqn@RرqnuTCT+l7p5k̽tA"V߃蠇 ruM6ܣʅpᴃ> jfSZ{9w-5٪~=#{Ȑ!Cݲ:+DTu*/yC    D\VJkDZxx/Sb^ֲ vij$9AeKL2uFė;ӶrK7 Tvav]w&5n؞z)6\ӦMclm TA8kWcه'N(k׿NeG?x;^ǫkK{a|둯jd+Z /M6M+u]̋ʌWIG'@@@@@ u /r7p^\+DUB'nq%2={A^V)*+裏㪱2"jӷI&.<@|>^s5ku\\i3nP?'<%:z_wuvfV/h*8JY [7o;6XRVmۺ>W oJm|H꘎o*;KN@@@@*C 2CJzJ(\UpܡCkѢE(uJʪoM)O?[o:m6cYLFƁzT]%H QL )Qn|1 hnݺRPI M7bVZi%G@@@@A6B >'j`țo:wzvos%D {K?3ý׍>37mW4 '     AB}z+ƎkW\qEs*5&XN\/ePm5M뮻7CDeP<ўەQ T9eʔtB@@@@ u fƍ˻[>lWS9Y=hР(wYfv '@@@@%@Ki"/wm&M2Ռ%X4@bĮ3Ftn^?~M6TRdEu7(zi.v#   ^ ޟ…TjB[l8XyU1vf@@@@@  2@@@@@h>=     A6     @E dWa@@@@@@@X,X`>;{UUU֩S'[il=W_=u]g}ֽ{T3fM7vO]Zh>Zvwۗ_~iM4O>ڷoo#GtvU9O\s͙3\sMvm3L/b?~ƴ%X}/uDy T5:n82e6hP-nz8O@^d׋N"  P~kvyWې!Cl饗ΘoSYge;S{wߵ]zWQF 7`ЧO:9Wu/z_~َ>hƎnlnҍ7hݺuock>.W_Y~OϰToeL{V[I'T|-|Z)w{o!up @ ȮD@@x6x`5k-;ka=zpAg}fꥦ?]uU^5 W^[kg_7߸K*dEɘ^;oIKck4. @ Ȯ=k  @ ㏶뮻mFtc+5kv}s tM6͕`u={6mT ڮʾƳ ͛g-[t5^~=n?t wSY\eTFWN|뭷lҤIn`ю;&lj'kQ];сz!moa7yd[j ΢;#<&馎IsBBRϫ׃>h\L.첶馛@_*k-< 7y*:o?nnqϓn OhUVFǯϙnis5}>^{5Y~Q1^4οJݨtz뭗k}nΓK-28GmCQOh u*w7gb7][q kB?K~;qu~ _,mv~j/ր?+ًYKVYejlyg?S4ۈ#\ȗz9~Oag* ;c6_~u5{kQG>Q(cE'iᦰ<)PRN41N)dW@웂)Q`)8}+e|ӧOw 4)ֿ:W_⯳FM7FFu sz_BB{"_Bd<*;smsB8r+ }ajgu}ia죏>+6`{T5\$n{|n %P5 . ՋM}_)ݠ}WD?wuWm\;0 M}LuG[1Nk5* ?SwމF|6sZӦMizRg/诛{;[BNeY7VtCu+@]l@@TBA=գR9|GwygףTl\\ nsz>7z'z~*r{.(#VԃU=yu,mݻwwŅ(>h R+\`ϗ_~nm) #|j Ͻ y*ױ`q/n1uc zf^ًTAjՎ;ޏzL.km0(IUnA{[ۍR:ϘEKAWy=#O|Zyn7ܾ|%%@UPn! n.T'nBp-Dv n\^ou͟xᶟx|OM -w\ g7ρXm|-b-"Ֆ TřsUAIe{wgAI xT'$߱]ֵAI&dƾرc'y?+~;81cƄғ\yMV~k{<"@'D4@@X|x?m>,9Cs*|U ~?e? z̺y>?M jXW LA*No8Ov@<5p*ॴ|MPv_\M>| i ւU Qr5 }/r!i5aRUPe-Dk>櫂UAKqAvY eM,6.*,ӹZj(ߵ\+ֽ[t,z92 [QnuoisLdI+wߍA؝~✃_d|Os+u*W jj/zT'mkT772Aɫ\Y~pNSg)W` f7#t xsC:g  !׿qhMdn~"NV8 JB\AB5xrzc+$7zi=\UAK6ɇ0zd} 6]|G睯7[دO i7z?ϻRZ ]Pf= bz]ly J'^)g3ߵ\Z O"st) ۯ7QA铲>I+5 ntiM| V}V7r5}G/7z}&F n5}rXs Ye_7O>y2.@Z(&@@MՓ}\N /}ZjjA8j٪j?7 -i9ջVԹsڐ!C\BgUߠU{>@/ >nRϳj*'=أM5U]QܔR#;q-(2,F.PF7[7,=| ~`纖4?WP;V$o>{Iڊ,Z#o٥KWO@t#8OX OG@@ i pĉb)p`\ gj S4O#4|)V58YM- àQ!B[4D2 +Ѐu\RAE-.1*U5 yA؂qӵ. AkI|+s zwA_APׂԄvOc=mڴNbZ [hnkyqv6:85P\cܵCe|>uM@_7>ZUPd~Ll~n }ܵkWYwA 7 'Լ5[\ג Ku:FTUŴR?KtR>FnjpMo) IC h    ~s.N zs|iOzk\Ȯꦍ?>{}\[0Zz!ݬ*)kг:*r|I9~bYhBW_B#W::q--kZV2*=մLڗQ 퓮|Mj"^g+HUn~Tp-dIDATMvI_""źi@ bT_gI7c4M!E O5߽c[M߽%#HNZg\ ~}.i|JM~rM*gK/sz4,jdW`o@@([@Y~YH z˅,j S4_ ;֗//t:BG˭|\!}w5T[WM7CuzyW0D <8|wͯ7: 85tdF Jŭbּ~i8_M߽٥:i}^߿ U;i@^},|Iܹ|-Mݸ}g:ԮAvz5@@jE GzFwB.5kVMa֓+Ⱦ:nn%QܴAլYGvG{ QBg}[z8G~rAd[B~|y5K4wf\omW=uS@ǧiov: M5KYFe؂:n r5ۯ =#AygPAu}Nnr-UAvɓ YTMXLτ?AɐjYb黷 'o\ l6/_91P_R\`7{3J@!@e dW`o@@HD@%|@c2eJ/7㏇AB&=h\Az`@=ݻ |/<ۗ͠QuL* [ſ0߿Q%Fng7zk_zCG jTǢ6_EagΝ??G>Ctm3g؜}kLy j|W縚BQB0Vr]yU{0W_U 4(g Ѝ.csׂ_G@7j]v )ߓ (X3fźY' C q7=X:;w޼B/tS@Rkܸbu5Rrk59 zǨ]A +RAA]lw뚧!@ dW9a@@HE ( {xw=S`J3W]vY[n2zagTA׻Wp%+BrUz^Q\OE6BP_(ԣTzJY&R]k]r%. J4ObkP/ssգz+Uכm{45[Z9/KhqFl}^w]rg}@;vXqm~Z3Ϙu'@]y=B@@ꝀJ( jZvJLPU(|i^qaa!W \M=hk^7}\0 F@%k Vr'Bs\ @v@@`0C{mvCfW.ՇVy{זXbJS b[\}Bu%P64f}gֺmc   @ ~ ?*iR zonO?t7`~X8W^q۷n;78j6 \i ֩پHB`zP4iDX@ *S 2 {  ;^{.bӠMURW^otATTATEU˘(p5׸A>ul͚5sn rL,4 ͩ@@@@:uٳ]lpT27oۦAt~t*m Ji^o~qн{wW>NHN ;9Kք     Av @@@@@ 9,Y     @ )J@@@@@dM     )d*@@@@@ NΒ5!     @*D@@@@HN ;9Kք     Av @@@@@ 9,Y     @ )J@@@@@dM     )d*@@@@@ NΒ5!     @*D@@@@HN ;9Kք     Av @@@@@ 9,Y     @ )J@@@@@dM     )d*@@@@@ NΒ5!     @*D@@@@HN ;9Kք     Av @@@@@ 9,Y     @ )J@@@@@dM     )d*@@@@@ NΒ5!     @*D@@@@HN ;9Kք     Av @@@@@ 9,Y     @ )J@@@@@dM     )d*@@@@@ NΒ5!     @*D@@@@HN ;9Kք     Av @@@@@ 9,Y     @ )J@@@@@dM     )d*@@@@@ NΒ5!     @*D@@@@HN ;9Kք     Av @@@@@ 9,Y     @ )J@@@@@}Uɭ5!     Av @@@@@ aJ$ @@@@@ N֓!    $,@0(C@@@@HV ;YOֆ     Av @@@@@ Yd=Y     @ :@@@@@ddm      d' @@@@@ N֓!    $,@0(C@@@@HV ;YOֆ     Av @@@@@ Yd=Y     @ :@@@@@ddm      d' @@@@@ N֓!    $,@0(C@@@@HV ;YOֆ     Av @@@@@ Yd=Y     @ :@@@@@ddm      d' @@@@@ N֓!    $,@0(C@@@@HV ;YOֆ     Av @@@@@ Yd=Y     @ :@@@@HN`Μ96c ={/VUUYS@FYfRKٲ.kZ6 Ȯ e    E |6}cڷooj+.pB1    Ԟ[oe}m-(кuk[s5k/Pe     P=KK}Me     Pܹs7(tvuYZlY[&ȮUn6    荝O2ޫ^ٕq @@@@^y7o,ۆnX{H]l @@@@ <3VUUoޫcFfmV{A]l @@@@ < |kuOk!    @>|:Av @@@@jY KAvp,    _ ~CqK@@@@HA ;VI*D@@@@!@]?Av8O%     @j $NU"    @ Ȯ ~'@@@@R N5Ud*@@@@@~d׏D]?{    )d* S@e     P?y"Ȯ牽D@@@@S@Ma)J@@@@d; 7ڷo?zӳgO[nο0I0] @@@@ f=~x{t;8[dEb~;0֭[iOZUUz֪U7WmH     El׶jXJ O9?s1ֹs}N dY/    hUy<;T~Xj} )Z-3DmQ~ݶ4IH[A%$H}Ah4G7tCeeXRK9wvƙم}o>s=ם%PE ƨQzudfdpD @ @L ㎋hjjq-RyAaAa G @ @G^{m,Y$[wzƌ1mڴ#7A޽{Hر#OcX;]f}L?ZZZbӦMY'C?8;W^pԩSh#Av^%@ @ @ xwމƢEbĈ=*ٛ7o5kDkkki7xcKҖ>Ը|ϲҒ&#G:V\[lÇw}],[~C(D  @ @i"_5xy޼y=d/gKwy1qlo&38p +3gNWfl/^8SN9%ϟ5k;^jUv~뭷ftfh'O΂_tE=ƛ ;O]m @ @ PE 櫯Kf3r#sNW_AO?>hVYgyf= O>d߿?fΜϝl߾=eB+"R@Ow /^;3:v[rwW Z!@ @ @HZdÆ 1hРH!C24;Ȟ4iRp _>֮])|of3,X/b|qܹs{ c @ @TQ?{쉳:+L 5\SLU+gu<@ 6뺴^vf%BLTNazo 7e @ @@-1G}ӺgqFdw}~/4NQNsGҖ?~|kk8&@ @ @@j%N|͎^hQ/L㦛nʖ8x`VָnllXf͚}5}vdڦM3f~IcAv' @ @@-ivc=c{n\uUGIG>И IK_/Y$vܙNc3,=Avo* @ @ PZ nٲ%V\=yZbdŊqyuָKK/fZu6nܸlvwv+ĺubf~Gnڴ)o7AoE @ @ @Jd~O>ɖIkY{BUVeޛ}1믿oV5*/"yH|c„ Ғ}l25qĘ7o^J{ҁ  @ @TO}eKsd/^iN:)1bDvi駟e]_|qVܜ}1?uԸ;?< SW^ӧOKatOۅ^X.KI= @ @}bqXzuר٩pǎ4!ƌi9vt_O[ZO?#G… / رcҥKcy6lXՒ @ @5'PAvZlY|嗙oTkƶmۺT>hРHoUؘbÆ f͚lɐǩwimmfl޽; o0`@vI0K/MMM2dHޓ >ޓQ @ @ ;d瀪I @ @C@]I](  @ @A@jM s@$ @ @! Ȯ$Ȯd @ @ 5&9j @ @d{d{2J @ @rd瀚CP5I @ @@}= =% @ @9s@͡IAv$@ @ @>񞌒 @ @9Ф ;TM @ @ PxOxOFI @ @@PshR&  @ @uE\NGY*bU}R @ @6n---}U+c=6LRՑʭ3 @ @&m۶hjj:% 8c„ U :#@ @ @h͛7u }1tЪB]Un @ @ PIJB1;= wg @ @裏b}*.B`Ĉ1iҤ"dS @ @* ]j5'@ @ @v={Dkkk_JRs11|3fL 6dʯs @ @$ Ȯ$ @ @ dʯs @ @$ Ȯ$ @ @ dʯs @ @$ Ȯ$ @ @ dʯs @ @$ Ȯ$ @ @ dʯs @ @$ Ȯ$ @ @ dʯs @ @$ Ȯ$ @ @ dʯs @ @$ Ȯ$ @ @ dʯs @ @$ Ȯ$ @ @ dʯs @ @$ Ȯ$ @ @ dʯs @ @$ Ȯ$ @ @ (:tP tN @ @(_ѣGU @ @*P_): @ @ @Jz @ @ @PҥCyųر: @ @ @ { @ @ @BJm1{֬*t0:'@ @ @c(53?S0{1O @ @ @U_s~vyr9M;?͝kB^N  @ @ @`ΦxfxQ*/f/-R֖hlfv /8-<r6lE{Y~_:vOYa(>OӬ-U(HSb)iu3򠊆J WeJ]* *TzT)+%EY@@9H@y@yj:QU:_uaK]ZT[j u2UJ]AC=KK}C,i,ZVM;O{DFWsP WSTUR]EB_}zzQk}*l'4nk h54c4s5kӼ\eZSV7".z6QJ;\;KLvv8$::t:Òadat0>2?7j٨z;Z-={KS/X/[o^C}\VtFk]:{A,-F 1XFYFkNӍ}ka3sM~0VAS+DӅLQ<2֚5O0m^c~BB`ޢ⽥e:VVVV5Vi~U7m666[llQ[W[m5;NhŮ} aј1E5] (u/ǚMzlد9;i9E8-tjpzlqtBs qRj8޸]'.qmt&v;nGrKy'=?zyz;{Vyw1aen3cU=fݬg6Y_8 x6 * j N (4R:+l!,2luppNxux{ĜHjd|QQ⨆ 脈 k&<Eŀ51cb bH;r8qi%$Lh(MlLROT>9(<3elʜ4RZRIM:dr)3\?5giӎғfǰ39\w-+=,| >apUVXֶ1{rsssOD٢<yv%^ őD2ER_ 9-RkOҮ"ߢʢӓ9C4ee32 ř8d]s옋͘8ly=C]@Y ]a DD\r{mK¥\m\[ẕr+?;+ZWܺJck&]\[i.Wض^sCԆWmIVe@m~Vփ m]ΎUU;;v>ݕ_w.ehO޸M Ak5'o;tkǑ#G=Dĉ9S:V^|zL񙁳gu7Nk>ͦM"/\r|K>N^|ǕnWk[\[Vk<Ƿw܍o߼z+V{Gbǝۓowy~7{E@xPPa#GUǡNS]A]-tx"ygSӊgϪ;??Ǥ?z^+S/_WKJ+vƁ؁Gr /aG͟?=y/ _#>g =R١ %Dq_#~&xr q>Q6 XuqiJd8+bQ ah! }l'" 1462 870 iDOT(R@IDATxT{uwBCqZ-(R/ز_@)PKɼL&y::d_='3ظl2۸qwqva@ @ @(oo 2^zVV-w:cq46p@߿yC @ @b^u{\AqVjU;ꨣ6.XA& @ @ @B;oj֬i 4$lw}ֲeli# @ @ ̞=.BwI&wwnc @ @ dFiuֵ/bnmm= @ @ #Fؽk5j԰snl֬Yl% @ @ l#0o<4hUR֯_1???H{ @ @ @16l>,oc5L@ @ @}C @ @ @ m!pX@ @ @Gcc;A @ @@blGa9 @ @ d%: @ @ @C @ @Ρ] @ @ H#DN@ @ @VGq[FDz  @ @  @ @@h @ @  li0 @ @ @YOc;{B @ @@v  @ @ ;0}-XWnmڴ }YD.  @ @@=~x[reloUTnݺ%nKԞ8q-^8<;ud 6-+ @ @ @^a}R^Ď^o 6kUͳ5XUz|? @ @ $J;6S;xPdW]ZzEY!Ek 싩mU6e:+p:2̪fۖjڎZUÊ  @ @@NP &lkU,Q[aMrVh+loiKmƼ"l[:vB&kZ @ @ bPɓ'U_DVJF٣GS: Ws Ź4^ĺnf2oUb P԰ST$xX(Kl3z7iin;e @ @_֬YV$nw̙(2J"٨QTJ}}N ۫ ;<{vUqb9c=۵m]kYJ'U+ Kks}aU&|@vN-n*5LB @ @^k׮?Y֮];k޼yiTq9#lYh`u pvHv\mXv}3k\;"whYԫ&81%N8B>mumuY~ymҥֲeKlL'jժU͠~@ ܯEqX¦Mf3gδӧXխ[ڶmk֬YJtͬ @(-q;DmoNsWޙ)|Iysֶ]ڱ~:u]3|-Z]]Oݲ}xvG٨+fMj!Y:'n/Z}]Ol̘1Qw++iVl6`^ڝrԩ+UzAPPP`5kִƍ_mC=dCki~ .[L$p9/G i\̼C`…vg&xpoW٧VvgK/d/bv 6tuv}EB_#Y#ק⮻/2kF7n=nׯ/OsgcN8ׯVe @i(iq;Dm!za{ vۓmBO>j}7uUc8sZ n}ul3qE Ka3$Jyn9ժ-*OLyWv=3ݩ]N=T߿UV-+do^u0;ꨣ+pW_7|3,7pC2f*QFgz`M6u_4)O@/<Μs[&eUi P%e[60wD/>"~?k@G˹A@쳏\2r?:~+)G#G5]vmY;-\ @JJFQ[¶D>1zAsn^˞[{]5^X`W LnX8`TIP 5;fKsܹv%^})Tɽk;w.v]\Dj a;ʳoz+Ŷ{D+22)[y1bDܡ~v- TvnA9_¶W(>Ñ3qJnk"6l]~IOG=z 4V.tNIZiJ9k3}b @(Hd=qj;IVaveDN쿽2)7ʎ^ `k6 8WqD'ڨfx%Tq%n(`I“yd?~m^@?bيl27&K(\bA: CsJqٱBr-CřB[hRX[xqbQ`endy+ G$^~)aOO{@ y睄LG:/}-^{zCQbʫ:vɄstx9#Λ'SO=k1n @' Q{ĉ&'LMݺuKxvɴ*N_;};ԳO@[Ļص_̲x%wvmf Aæ{j'٤P$(MJ^) 0S2v!lU]v׻> |0`7~ꭤB)&+UiO %P\aۿny&2hzY$~{䧑}ݧO{衇2xSnLK%2L 1%vжb{`Y!@6yjUe܏y m_Qmuߋm \mSgvw(ٻ1RuŕԉsufM4x`ꫯjߞ"/@DȽ[rv}ɩ5\\5,ʞx r-U{kRʫdI jn">CQ`%#?Ά0af͚&Ogya)]lߏN#F K. M\0zJbY*,ַoޚ4֎qƙ =m۶5 ǭ&W煎Q:*塩OOnwcŭmٲmf)-z $~mwvGM|U0v9Λ]ċ_y r:f̘^t,J'sVzfOqlkw Y¶کܹs,աC-+QEȆ0a[=~92|u&#oC#}]WhcuL/3.Ⱦ˄Mx @ @I^(ng}cxM[6e[ ~g}ھ{%zjWA{饗%uY?gyz왰<@_%ӓϨQiLMyM"W7F$MGy 8Ѝc}㊩⊸eaT)'e:r}Y\]vt-l?m]pn1 {/=cbŊئJa&Xqu>0 )/风:M(V4%MDsT/w5tL/4o)}mD~먷SyEbj?ab-~q&kDmەR1xierL(~0Bk_м?ܔLϳLG6ϊtvdZ=hРka|~8vZ~[-Ms%l{#]w1@ p!j{{6q;kK~/Mnmcϛ+!?:}'jbin~4ՆM-ֹaXJbBa,;`8o]a&Xa >yy)6H$HLO?%;RXf.ڿijVz뭦LM[<_+z 7Nvw'+$drGRɖ&!9,FN=x[~06XUiyj-JtW&$f nͷneعsgoQ~;:@2I[&ɹ+vK '`0܉_޴F]H 3v腍F[-mQei?pϥiӦWEYRW_zwj{-/E䢪 QR.MSxP竮*oc;,PX{+ \Uw&BbYI6OT¶sO?~gs~%4{UP(}UYpy ۹~MUa[/ʂMzU}o.{mϊqeoGܯE ]E#7 U|! v+_ԗK@=+/a[^<%b֫W2(=XQօK_Q(Q[`\Ω0&*2Sz'd^ao޸M8D%EL60J!&cǎ6eSh(㏇Ȇa\Xۢz[D/Žh 5j0 J3:_[^#uSoF]^?3m}9/Tdu(ߍlȖd¶^IoAw+Nt-d:Ovۭx' @lL'j{e3̪mY-l]|yX$aC;X& .\m_davN-4Os:͉pu;:Yê.2=*HaW7Y&Z% ƣV;Cqz^6'Ovc1j}p諶,9ha¶Dny6N1( aK&!YHz%/?A}{}Oީ~+\:Zm @H${ѣG'< KEmo}(\Vޖ¶ׅ -W͞;>䃉Kl+yާbZtzI'ԲTX3uP}AqTCn7[neI(Us%& lmT(\%Y~zQQ &e=`uZĭa’TFaWT?[Mkj?Zl- ]VVǮ]!x*?POQq{L笶; IE&iL'$le$%S?&.;Ȧd¶qnrg?d#SH`gy~륡,Sa[(5*/vaw[ou˽M @ 9{F׶QWo*q[vgqza{xl/cZW/OkrSTkg-M$4Pۮ=J^oS^Aݲ4yKz4N?MW秞zC_A-J–<*L޲:i^* n[)a;,. Mx hM /(SyBKnݺn٭Z[m qFDڠC19Q~K7v}$'4 QE -17Bj0 +_H)g >(MtQo(lKz Ï%%t?hJd7.j:VByyF^M/$RGY`ް! - V6E//~%SÒ<*K:xTrQ\yy|Ms.%J[6 In% $g}]/6 '-Q7*fyX=ɖ1W={[b-Z+5=v),( a[C{ o~ߍۑ-k:WaE#V^=7WAaDaU>2󬤄m>}w^&Oc&5*8 [' @@#S¶=31NH'JI;^j[լv|wulΊuhzPm$ֹ]]3k[zlyiNS%L &_PTA<S=o'x\[%lyx僟au%l%_ћ0"Xlovs=LV?*S}سwRɣFB44૯JۻտZ ^}U"wzwKj"}PD4Uu]ѯ~Υ5&i_ZۖnnT>ߎl_Ӊ(t W[[^۞5huN&Ga5rG# @*7 !luCp])wVۉ7d G18R(q;hAQFg,抳i3ŊxǪHvTcˁtM!0$p-t(Vozְ⚆g#lz袋V{\^a}%CՔoXٲۿ|t_ĈJQ"Oyoֽ{w_H$ K(ZvE4]خ}ps=]a[eutRvj%tNW({Wk&il ?ؔY@ J@ ۹wJ:&`VP͛PJC 2|ʔ)vG&(1 u,7IV]w5a /`]tcH:WJNy*)Jn3^`DϨp)QBC=zycz۽kq!eaCTۂe(=Na:W[=J8%(l2l+5=KzY #tg"lwiLBE% OWVc-\l3ݏGq_83@ Pdlm\^8|RR5?իfիlV?qEVN\n4y*S(p . W≦k$Mm'=HyVTu=ԅ„́ڥ^I r,nH(P+g3<>l1c)>LVN:)^5j< ,TQ l[YKfkS% Oߗk6ضrK{'GN8!|y a!MeAZն?~{vmeQ3#Fr(2(|zG#ۄ~Mꇒ\vElέ_S;’G5k\O+Vh5^堒}Ϝ9[lmڴ7|36HWezGߩXAQ6 @O k'-N7'zC +RYpVM휭ҵmg=ٵ~-8qs1 >*]Duj*7nbsZ:snFf$ :Ʈ}$/}D=5!x.)bKdž bJ4%ԃ{@uH4رc\uQbbFKUtLBZٲ0:mae~"[5s7'$ &y[&I*P#VHԎ|Ms.l+jgoweZ=Po氰trr:pLvtmbS˖-o >("J҂& @*7ٹ tkٲo={u?֮¶M1yMƙ< . q|3/^BM1aClPxP(o0K?̞y7n]Črɦi„ qE4iz}/ H| iC0wmy -AQ&ʖQm +O OyJIP.DmFƏWF篼JB⊦auaטs9'<9<ڤO%BE>hט'18JUAy0v#ymɻ:X>8(%0.ʼn6"~zL"{S+^i~˖Qm ++X Cuݺus@b~/`QO]c'_b-b ~}\vyqo{2m*5=唎5Ux)rAT>ߎySka@Ʉm],22\oyhkL&¶~kJ/Vum6=YR4".8L]r%=s:X@ P d}\φgznO_ޞpKtN`(VٱoOZձShbsWFα^Mjؓ!/m۷mgmĖYo|3m1]lٺ vȫ[֮h7;嗯[oobUvf%vR=؅$no֬Y3W4Ӄرcw,^8lS#\6lҤIB0‰HRfXzX~$N*8z ZqEӰ0qoY !!z&P-_ǯ?y"y}--%lG.ǧDt`H̾+$a cW>~>s³1yG+yFXi|1.ڮulC[ovַ]={i"[KF銁P@MeɆURg}6؝"wǨQlҥe%ihX⊦auaװ/253m%lcWbfKXK,b-m۶nɋ^^dK}Ʈ}/ʓ]h%X7oH4y3{RBaQM L*a޶'W+!?K()`^K=ܗPV9_>޺uM |gꨶ鬳2 M଄a^DHuI+h:WuS\TXC/bC%du,4AS]%P[E#l'5uۑHpIyݯǍs8#L%L6:%Sa[^JcUk^{74QQV\4.L_¶)Lbe7.yK'gX: uf:?jS?cH&OQy-]q__ {:L[Qؕ_]#L&;x/)).N&nJk*Ӄ^@8ʲi[ӹcJ: ҽ{wu zYd&P;Ņ Z1d/KBPQ^&~V>,~;$,{tm]l9"> EV} [ox'(/ ]4@IDAT_Q-R/@ T 9-l;~ve֬VpX'kxYj:I#o٣ڶ=Q(aU졟滉'TbӖ][ױmgիv$^|TW4 { gv* W"ϟƏçBj(4A>}b/b*&_F<%~EVڑGVoe-l{ W,xCVdbZ%T[5w"OyA$/tuCsIΙ`u^0R*Pl۟w L6ٰ_rWȑ`х^z'Kh57P< QT`֋6Lzy{Slߞ[E#l{gCgEoG~-(a{Μ9nI5Sy?>Äc-գc^_sB6mW3@ @ ׮Y+9 ZCG2ݠ^Ζ`Wj V'd-'cFcaC;"99ekUvSv'&wal¢5nFk[5wQvg\փDR)THR) 1> 5 %XJ&UM4qN;YÆ 0!0ߨ}ʔ)@ܯ_KpBSlRyjZZ+!SN%~|Bτ \>5[…֒AWYϿﲚuP GgϞ8,>GylHv;N~wCnD^=zy[uzk1wݺuWq>^nl[8W~;~-N]'O륳t/^DOuӨ6  @Za;YYFO6y-#\RGV+vS @ @Hv:($+W֢E 7u:>OTg.?e-4ʏ,{ @ @2@خ1( P7loۈ#+Q#mL(ki @ @vV, _ގ87V{F&Sl[oM:NJ(.3Zh.pt@ @rvvM>~]qHAS2$%TϪUN0h~)$V\X@ @ h_6l+HDM1k׮&e-4rh& @ @vH#;(W^iK,ɨA׷N;M"Yzpn(k@+!@ @(+JOrYC`ݺu6|p{aIX ح[~'l˚c!%O`vE%T|[wAYﯸe{@ @ LhQXr͞==?Y6mܿf͚Y^^^5R @ @@ @ @ "SEc!@ @ @as @ @rvNu @ @ m@ @ @)9]4 @ @9 @ @ @  lTwX@ @ @@ @ @ "SEc!@ @ @as @ @rvNu @ @ m@ @ @)9]4 @ @9 @ @ @  lTwX@ @ @@ @ @ "SEc!@ @ @as @ @rvNu @ @ m@ @ @)9]4 @ @9 @ @ @  lTwX@ @ @@ @ @ "SEc!@ @ @as @ @rvNu @ @ m@ @ @)9]4 @ @9 @ @ @ tAoy˩X@ @ @+es @ @rv- @ @ P) lWn!@ @ .;Z@ @ @R 3f yd<8h@ @ @E3ϴ|Cέ @ @*+ѱ  @ @ ! E;FK!@ @ Tfە9v@ @ @9Ha;;&C @ @+ss @ @rvvM @ @ P lW!@ @  4 @ @ @2@خ̽ϱC @ @A9i4 @ @@e&]{c @ @ sh2 @ @ La2>@ @ @  l`d@ @ @ve} @ @@@N @ @*3; @ @ $F!@ @ Tfە9v@ @ @9Ha;;&C @ @+ss @ @rvvM @ @ P lW!@ @  4 @ @ @2@خ̽ϱC @ @A9i4 @ @@e&]{c @ @ sh2 @ @ La2>@ @ @  l`d@ @ @ve} @ @@@N6C~ek ;؁[4϶&@ @ Dau&"0|Bѭ]}Hj  @ @+A's( |4ɦ,Xi'֖Y'lٷ=9b\yhWkZzi4:!@ @*)J6C`ev鋿ŪhհrDlY:\{mwЭ[yNx  @ @%EaHR* VENLkק@eA`ɪuN56{S jXKyW-` @^7 YKP$Y54 r ?[/Ӗr[Usۯw3WjhB @9sgq3όWĉھءxּyswrBJl)ѩS'߿\iO羋zlM81r<Ǖm۶ֹsgڵ*k_~vРAVv|Q'HYTrl@&0a۴ ԪV.گ٣oiz2&;lM{˺l|)iԲt%ݤűc>jVִ^|iN羋z\󖭵Wt:5whZ6ie<`/ثu]ۣ{ߓXL@ *oI'VZo/ ƍ~Vf|q&{1{GbU<3ֳgOwO>awzw;V8Oi8NmĈWngu{秵M:RcZ?К4){>dSwik׬=ۙ}ۓ#ū퟇vu0vL@r/?hk#ڡ[sUӏOo',]hyTK:]l?뛘cvcnύa۔+$Ly+cUMKQ^4i O.xvLڵvkY.;wTʢl#?vw};晀 @b@Ban7x#vQFLTta]w^o؟ؖoq _4 @I8e]ؘmTwG?3[ye|b9]Fa{v;C|KCؖ#s+V%nz-) @B@F.uT⫾aO>d, Jm}/^c{m5]&e˖?l .;ow߸eEIŶm%luz PYw: zf-]Uwo;gO?Ύ+nk!c;=f2S7p7S(|{˳3豭8|p4`5eq/k::aI:w%AΦ3@ʏ6vV(u]grH\o喸p {z*\QgE.j P ?m 0Z^~K%n7U՞;{;0ԅ{4vv>ɕ頰ݼ~ {̭? rKö/2Q>!@vz81rvp*~kݴiSSEAT|0:u}ڂ jժv[mpq y[t}G6fLaܺs9Ml7M;lٶh"Ud=WlgHϟo;4n::t`kً! 2$6mX߾}cʕ+mذa6~xgjլK.֭[7k\^YS}%2)?#~PxQFo?o>Uъ+_~1:uL2]a:GtN~xb+3 dL`zGG w+Ԭ~VITev}6o$t:ԥ&QRxЊO;Fhs%j]/S׿u7 ['sC{5m6wlnk ٯgxWz[<[8g }{^LkjVvkgoѶYP$QSW.7þæYesFiN]fcg-5]ۚ<啭ZJ:enhմNHYH56ػNP϶i_::+6lִ{zD}+lk~>^%ԧFED~^CPfkհdr*e b@؎%BE+r7q=I=skԨz*Q#)Sի=ӧۅ^hΣv&^ ܞG aL>L'U#Gڿo5+<k6|])'γ3gƕӌqCy:ZE"g_|BEt_zoѣGk62= @ c9"]7'GH٪s\:Ʉ>E%F+_놗>wubUʞ)y厧<'J))uuU w79"DCdžgfCFδ _xWժ^b,x~Qhگ' woY;I#aGGNعM~0ƴbK@$ Z]wm7K/Qg&¶>8trlQJ2Y}ģAa[/J`byvo;~ݝאC6>!@YCa;Mau֡bדm۶u==o^{$*z֢E ѣ+ʃ-{i >c]ݫ_u̙cتTbCuˊ*lK~衇Юz[O.(QɄmy_uU}GMwy_{v"AK(Z<]a{ܹ-mƍ]|-(j_UNugrb "I%IT˳w`w;WIώyv2q|x)(Q&o'N*=|&v0tjV[L\ 2t{9a;Y@>i 꺞={ڹk P$z և~Dv͛oaիWw%vi;ic0oVl{g 6tC|HW]Lu]1n-\odMUؖ0+V&q_wCh^Bb,:wl/bǣ:m%`%`T8+vuWO>؋yb^~a[gaS ϶f{G٤򨗇žx.}/=WJ H%~zf*n?}notmm+D#xGYbδ cKsQsz{fLlf~1SK,oΈ)3)ʠ6s80H??Ds Odh;g8;8a6-cm)&qֳ;Swk_LЮnudhm8iy[]VTHtΕ%N(Ƕ#l@~i X޶;Շzh[1%[ֳ7|39,%*!~J7 ѠAA.;sCxvQGywwZܳTǣrQ#<^] z>«;}]f[[qe HHǂ¶WoSHy+^z;\%}/dJqRmLŶ4?F @ !~gk Cܼu]k`/G/5;M?v1=mK't2D]K;&Yuq<Ma2dMVwVjZ3!n'ܳgӘ*!buŞK Es-3(lKo۸0+'~0cesbk'L @  l)l[~SAiOwZ W q-pzaKb5J R-pϊ+l{}*5-osYI i_ꥂɫ[bCb # {+ƶnoʃ؞=$ۊW~b+Ę¢ ͢X~?߬imkVqtq2չK@ʕv) 1c&T1//L ᅿH%VshV>&)+l3MvѲVZ٩sjwǵ$?<GxaP$mKv]w<+N_j?Εsoy wp4`(Q;0g"Y_:WJ([ۘ ?{ E.KK$H(z9=әQ g8s>3 ** I$}N ߃uOOׯ_@n] 6z [=`xF TbQdw…xfB\wuoZVؾ_m۶>tP1dvpx6B T<ӱC /P= 'xBj+,V%:W*{Q?HHH ;L],7S ք~f.nLF}38_YcDKb*v}/s~`ߔ`X}0 y-u.ľ>GwN?ot<.I&l0 NSs^Ū-LWIiDiB޺"]NxZv2ٹ=w۬fe   %@amҳ>[i|駫{/|WqLxfq{rРAb ]ݤIư7O>#yNmy_'B{lH7~zH:;n8?JAx:Ax߿;v` K#Uޞ\#vؿp{H+ԵMca5@ N+Ka eo%v1I6iH\/}1W&,f]זXbqzQ ]-?;O?͊ʀ^䘝;Iyst Iw.Y=|;}O\FqAw7ٲc3Yz18A {?2C,p1OsB>ׄMJdwJ6iXf.;Om05t 9waA` ZI_??c\ηc,{Ǯ%1Na;g2  ە$lËNx&-Z8 lĉZvC7O>SO%[9`|ʈ#t1aX Nۄ޼ys͒l)CܶXeȑҴi'Ke zlKOƶ2ۧ dOTxi|Z [f dr. oÀ qK,azqN6$4j#;F牼FM짿">| a%= GpN $Q+N~l,_zS$E?9ׅmx\gs,!  ZKv% 8c~wy';V^#3 {r)ңGWFK^3gδ"wzf#<8ǀ O?(?ȭG.]C^wq%w- 2%Da]v CQVnEKoE;W*sm dOt Ԇ3[^w0y=dvȉNk=1}]94`,Tm3r߇3G7PeG,snr:fp.lKOtP! ~uZخË8гgϖE@ԝ;wf͚%mqAhxEC7[r۩{ >\o*r:k,3g!vO*gɒ%2}tMzVDz"tl? t_;lc C8q'*PM0dbi(_ޱQ@]X*:%BYV8ޝ[5N[طzRZW" ˝*/#   fpic.ߗ; XTAڷl1RsXϮi^    HN DT:=y5$@$@$@$P S*v>Fl< T2Z/l@BvPuB}`-'   [Brfw58 @bvfٶloX[A)|?mKV-sJ$@$@$@$@${jmPtvӢ:@u2WG0#IHHH*@/.v"7jܸDn>zp9rHHHHr@ˉAuXbv,1[󠜛 (A#   :-#Ei#97hȽnn"[|ENHHHHHr@'  jHjtl :Bޑ W_6HHHj4x]^}pOfi--mXfCDe4     "Pm"E$)-B= zfj'XLFVf9O$@$@$@$PLƖʄgOҬ $/"Lk7teX}a2ZHNHHHHH@MԎxOQ*~Z +)(e YPx$   Hewe&0=Z7-Z!;P,v+rX @vjmHg$m -hٞn9x8nUs/$@$@$@$@E-0p. AlHpm#*Azl+)~ @n]6'#6!ۼqJ4'd.[^o'ry]%^F) @gL:gZ4xvp;7o/½r3rOi7)4     P;m땸N!&P{h34<^#H?StL'jIY$@$@$@$@;@[>~kOoo-nne"VEs>  ~ @.¶ Q D ҩi nQS]I$ k"/ X!/HHHHjuSe!Gи H*d<Uk|L#y!IS&p,GƏ¶b -l:7u6Ҁ+[oh[Y, V˪F$@$@$@$PS4mPO7/՗5UΏe6D nܑ${mSخ)gI$@$@$@$P a$hL!Z{5lR525® g<HHHNˠA~mc'tyr^ĩÝ$   !j kzaC61ۼK]bl)r'ˋ{7uNҳld4oPC I$@$@$@$VL_A-bw3I9_s)bwvzaI ۖMFx~?RV$     Pm1* Iֆ aۋ]O]v ;W\   H@X>Wc#M`o[of&    $PsmP!<e] ~kvUf T=X65 k~@І6BIXyA}[A$@$@$@$@$PMj 1m`ymo{cnZK`Ho $}؆ mhm<^sQ$@$@$@$@$j <ᱭ^Xv󈫭!ITDm/6"Ga߀ @a"(  =lxnw5<H f%A]9G @jym {ۋAjblc;&]J$@$PswAmcvaI hGm]/ @M$PkmŎv= lڶ#6p$k{m{DŽ-"  #KÐ81’7<7hlmvtQFIIIaI:mժMfx'eŲK>}jnf_d6mҢE 9ӲbMN}裏Ν;'ѣGk:H6l38q|ᇚg-5JZ2bsϕ b6e:u2w\iР\r%ҡC;N8Aڵk ,g}VG6o%~3a'tm6QJ_ꫯʌ3d]v]wݵҷc4iDt"={޽{0!MuB؆Ƕv6B{vg $@a;&& @N-l{GB!6:ׯOtUť m„ ұcGw}d:#_kV R ̑->RZZ*x/&ZD /\7xT"ۅ~g$mã>*>ԯ__}]),,^xAn6 7 phϗ?GId3ƍS1m'xB.N3[l?x:_H+W5m}{_[iocMÇ>L򽮎s1x`⋣~Sw:Qmf-P$8;'/7]O? UÙ[!  L!jv:D̫ -6mtnv6$E c=6idqbUDDǤR\\,=/*k]b|aoY@0޽ Q,1#ӧOaa;… !+lC?~l*̷lR 8mDGc^SSս?12|} ؖ[n)x'/t9:!l#$yjC?\FZ * o4y4mH+&"  %_و#늰} rUH֮$&: iuQ%(/ʦnΜ9S8]Co?:Oe@ǭ + Rw}ҬY3YrMWF?#s:m51,QԠDߩ cw-?n+C|].O%]Mviq|%?@v;NXV%;`"۾vRmǺ[z\T@a;URG$@$@UO¶3MH8c#@a_WUv= iҲzGu2˷&vlȶSNHHH2$NqQQ gP$"1Vqg-w4 nu]`})ȤICڃW-auȑ.b[oU. XN1 nI]W\q|W_RA' V q~82D:K6{8U C oCiX`CF8 :Wl#APCXboiq!>ErWˠAV;^H6,K|bԋ.H暘펷td;ҰW^yE˘g5ިWPy o1[a[G0PG>mxk{)Pj[X6y׬Y"Χ={jY=#RM| 8Fn7_w g}|+1؊sQmPwB.[xN_uU嶌s tmN >|;wFi ۙp E(xClGP!.0 0w8 ox8p r7}27oX&fຌk DZùg\7mTS( m6pwyxcqM;l0W D{76mP7Xe8k -Cf=rJ|Oqፁ`kSϓN:I= ΧzsCs<%3Oiv& glV}(lW=巉R>=++~  (l{:xa+][jLu]^bD#1 bUdtʱQF~;xDC, A lCpAoذ_6Lvea q!   ZDxaaib 2۳7}͔Q<^5BB kb"uf @xEuRDK6Oao w_',l/*0Bc;2 x۶]m4^t=3j3o8Hxw4ax{^AXHpẅ4lL}< oh4!p0)PjN}nӇ,Ọ+hrgꃂD`}o ACсk ,(lg؎9KZo>ěXayd7|sxueHt,}Z1xv6rZ=(lW=#{qvxd?%~lwwiա{X*K1:߸E嘿< eg7 +sB 6?5+%s;6a3ot፺Q/q#n&^3>N$lWhZ^M ۺ}Z sBV1WmM*D0Ϟ&7 yh7LSByN=eC:~trWns/h3'~P66m#)  1Ng"xGD^0A67a^o"ԈPӰ)l᱄l/Cf8SO=UXzcnzL!ba]C`xv6ivLsp8_(lWznҰ&ڔO_ٓ?[u!/Px@JI= d1az }BaQ -~ߠ <(Tbl'j{6ۏ'l#7KN[wGFSiInV] :^x$OIjnR- 2;hj3X)lHHrmX3{ʐ!Cbf@+m QxS=bn%"4⁚8&&f\d ouEZ 5"<*e!AX7uT}lŠ1L I3 #7p;cEg'|YLH$ZSv#V7XñF s;mK? u"͠ATu r1c Dj<ZH 0 B㚉kg2Nc6מdߛvU k4!,'X׼x^/Aq-7a;]Vb6CѫW/_pHղ6b gs۵&dsQPخBa{o D{F#Wl'AXf᭽ f2!<a5<} ۶p۳~\Z磌 ѻ}wEy m+xS'  =cM333M 2a: I.t.eΜ9WU(p:&4"H؆:ƻA|vFQ&]|-1E\DؐϞAtN&lc@DĜg?}q!<Jc%dxW/ݥaJb"fɄ,W mtiu ]w3e=ְ'6vR9xpvl$@$@$@9F¶w@tf*b&8j!lg̘!:1D\/BbyC D7;#Bq1<+trb fe傰 o` 7,v1`p>X"FmǙn7?خd5 1Bc7',lCഐx"!:Q'3na;SσN;*!T\s=W#@g<Uv ۙ^{!lC cŃ:k'2ukbi|+jL؆?mJokHHHrm d,aXٳg ھusYFax^{-*훅JH5JP h/Wqxe۶ @ D>ZΝ[H60gyF6|pU3tg*bl2 a;?Y[b< lԛ('8ŹqkxHM'OUAa;S\шXv6>/x[ gsz*[d6k׻qF}#@ao~X':|4oQ}IpUOVCpt鷇7a9{'{jv6ۦۨQo}/IHHmu:!GJ FضNcΝ5g8u< /)l 6Kt7o.1!l'kW Q q7xc%ׯUlkEUW]{^G. (WG}͛ ^{ C Zu/wL>]=GATDbxB@D3;ҍumM/lʦs'jguCcqo3X6ޞƗmW&\ ]v :P'7)opVn.]T!uAf;^{UMe vmk ~oXa3pJ욈Æ Ӈ  )̂vloZ,xut1xv6F{< ۙDuOvq̳7Hv]]L U@H |[BIzTSen}t ˛nߕ_'͜K@@O$l'k;6c ۨs9ztfbtnQg\&6*Gn >'ƽp" 2  ޱNgG&4oQ1ۅx//X=^ׇjʔ)=PD8XEڕ>}\p 馛Ѕ׾-F/ƿq,s ڞ |@Ϟ&{xFdzS̝B4iXo?;/Jdcw9xX3HHHCךDqNB)ȯ'?̻-ssS$ :) u|X3V![o*D x5;^wfR1A>묳tCKau%jW-l' aP?^c c/85Q b>C*ʘ |h `Ỵ_"7)iF!9 ފh' m&?ZJW li%q>-ćF fm۶`W1DxKEl7?Qc|o{0C{q.3z ۶yiժUTU l[ !-pu=pxs0|opƲLhIړ,a}oi@zꥂo9@<ñ>''t?8hXFtg{ 'l-\+Q.hRQ={Ԙu=FehC&Fv= XqFZ]dZ/שWvp# ,k\,mls),lvݍ,1Tf̗/M/[=L:lr¡d37Y~˜Ȝ b7;y4o۹]9awtZ3sA2pؕ1>?xwCr#pBmE  Ismxx5!Hj!$iRPO, \ u,XnݺE݃ډu-W;<^\lOen6+պ ^|_~C jwIK9uT}pa3N6e{ U𤆸m`j,lTe/u O֮Lx-HX~fTs6l(lWĨbh[$'5 y7'{%EO^y9GZw :d٪ dʺ|ף#[-'&S>}ىm57Bߠhl( .-!FkDŽ]t;2%*6oQcF^=mnؤ;Ҳ}ר偐jnn™Sj{6ۏ'l-++}B=ṍ+M 7UKZEؖm>,caUY2uz򤙫`FMz˶CNw#¶AHHr<(lRZrYIkGj1| ^{M0#@ _ !Ih$PS Pخda{ p#R{j /v|cDϫzy‰@AOl;LXl!n[ϟ! r[y F,C 4۳ťlHF;]c_E3жY!ҷW̋xۺlmQvL,5U֭jao7~Hzlx8C$@$@9Gv6(M,+Ny睧^_|,[LEz \ˤItL L{QZ9%Bv% Vl֬X"kB4jJQ<  ۹u<ؚ ( ^an} 笽$g5GF _3j<o8lܸnlzlL]7ԓ7-q/%.uE?\{:z?t5\ ]*)lW%mnHH#D 7%ZR_Oywu9ߥ[v76H$bhp.Mkؠ鵂I smxb4 PHrD?Cz;&gZkp 8qDiРGڶm3Ȕm ۙ;~9 > ΐ  9t0    ` EBa;SvY @%] PY% :!lP8^(ozK0Ig"   'ݥU$ ~¤ 3$ ERY# T&Z/l#vlPbm&a{RR\$ H*|Ywuo?f(147C$@$@'l{q]?$@$@$@$@5@c8lN᥍e7-v.k֓ @2m ?VmWnL#nZO"t>vHHHHH aB_!nSخ[    $Kض0$yv>m ym    H@K<᭍en8 ~^5Gu4HHHHvm1MЮl $cۉyn汍eI ;8HHHH$Pmi AaB'n;m'nCF,'   'K؆#=QvPFlhs y$@$@$@$@u@#3vt^fqmZ}q%z17=خ'=wHHHn ;mm;CH\ *lGn;]mzl׍3{I$@$@$@$P*a מv07qۋ-N.]NHHH K.pjv}ҎxjGDm%IN)lgEIHHHHR xab6z%Ϋ󈷭¶Qc(Qp_+'   'h#Hx <$<0$8Hpj g1H$@$@$@$@DV Nց#K7svwS "7]QخsՒ @N 0$Wۼ!dopvNWۼk?ʉ'믿gN&L ;v}7'5A$@$@$@$Paxi{kvCVm{N ul )aa8v"7 lԋMa)/;N` T>!l;Nyl $H܇ #cO/nHHH@X[6NBAq;F"Y~,\PBΝ+`PخHHHH K5[vj6mtZ,u4ζs$!pC؆w[Ky$@$@$@$spv}xwzaCVAEbڞ om/ƶH4r]7 u @n6С*hcj + m/$znc)[*-Kn4   Ep8sZҢQs+'\{1S;"dKkC`˄ >kdywu?;8ߖ˗{-ڵoV&O,ңG9y~_-Z$ƍcJ.]#;ϋ \SL9sH׮]w޲;K6mrHHHH%Pm4qቍ4xfcBi EwI}[PNa~   qOtסYӲ#*fC COnȼ7&zk<#_U~aOg-w47xc뮻|SNI&ˇ/\?p[oUV^L-ꫯAY$@$@$@$@$6Z!lBv=t^0 S7,G<]^DfvZ.J6m0mx,@$@$@$@$~]NT OޡP7/ 1Bّ "vm1W_}%t_=?Dixb?z_u7oL8Qرxnw-R{1L߾}nxkϘ1C^y6m|oM AnL=᭍e:Lo_VWHFԖҥA蘕HHHH W Zޅ[.זN4z4_k=sW6vt 6zh9r&;FF;L֭['CUO<q &Ç˄g%o."{ox5IHHHH )-lckq¶:Y R/"n#G’@vdh[Y, V˪F$@$@$@$PS4mPO7/՗5#G<xٞW6[ۻow6zl#,Dc=+P$:m^w~7,S    H@# Ov XFDÓh/ns,A>B*l{R'HHHH'H<3ſxWC:25܈7Om[M' jGmY֙Ƚ]i4a_/y믿ʵ^+C [-[vQQ>yvm2x"aJHӦMO>+HHHHH !lCGvX^`'pcjހSA+sG::Kz|eo$@$@$@$@2że/5rB4bĽǧvXp'l[Yz4pS¶bDH ϟ?_.91>~䩧޽{s=#HHHHH 1Z%lGѮ/H,qk5 @Ԏʲm ' ۲.HHHH KeBwОcnB!WVqk4 N#Do}MJm09嫯x{c xvg7|#tM1HHHH6v 7$赍U&JCF2qۼ#[ȅ"<~<^b>b8%   Ț@XGƌ>2O:C歍 1 ,R?J~ dMn\E6kS܏W6y|$3B|qۭH(j{q#镳tf}T [b#ZJN3fm۶i$@$@$@$@$)#lu:^ET:xuyA:i~z&k#W0O$DzLe #   Ȋݱy`m&bc݉YM`6p\?٭W%b.Eɜ9sk׮Һut3? $Pm+U.B5:-H|WҼZbF^H=+of8%   ay*I&rW~        Kvs=QBV>\ ʙ_Vꫯ<[飏>*>_3<#[lHD`ڴirqYN=T93x3DlF2t2d~ַ$L/O^$' ,{wߕoQC8sQGɈ#$???F$@$@$@$@$@$@$@$(l xx@HϞ=wTZ %@ Sa;F?̸qΓҨb[o/=z)SȄ ~ʇ<ƒ* @&@a;p>쳸׳>+oy wz%lO8QE>k!"mڴENI !yuLv^(pV.\P=Ǘ-[W^HaaaT:禛nu~ 4(* $@$@$@$@$@$@$@$@5B٣>Z.䒲4KNNQ*J؎4/\~衇bږgum VZo- 4$NIHHHHHHHj ۑ|rHS﯃͟?_׏{U I& H׿ٷo_jd_ɓ}ҧO9aÆ~cjƍK1Cw}׏}@k׮W^yOqޯhGi۶֋C~d3;( .ρJ-bV_&qXt`_!LmUx֬YzOepܰ?o+C=T0P΁>em-7ߔ+Vh8#680(8l&({p߰Fa&ׯzKl 8_p~<Ĺecꭈ)2dH8z_z%֭sz뭣p 0su k E,.us<> T ֈ}w䯿z1آY|?:W}?igqlذA~a?@9r@-ٟBUxG"~gK/UQl"0yb#袋TJڱcG뮻46f:Bs<!p'|25NrnQ186{챇_/(z8V\)UtW_s tIa-矗 .@0 bfTF&l.:x寈#<S:8o46e5wyl3w:c ^fmt+ n<-olzÓ+C"%{e]{E O?cƄs=W ZX!l:!#`2.3n6|2Mea!OTLA$GAD.]|Oi x[eJq]~˦nQᰠǶ(䊇*W]ulvn /P3<3*?p}Ȕv@0$ λ{\q`B#  Dץ}M@/XyHHH ѣ}:aa* s@4 EYºx6sar)&|5Tml*ǬDO,,lC>A07':Ґq=ƔF:B`ZlE1cƨoEr,l9+s9Gƍ! plGcl[pfAC ^ ~ HHH.`uhs_S!Un*d   꼰$oٲeJ*;MD_裏A /gm :*a9;̆ &ÇTp\b‚aah"c :cN=yA)1% Am` lLaO03B?MFmx3;t3/7f\rI9h8ou>_+vYLp˃ bV7~oO!#>%\&e  M8X:KS,w0$jF$@$@$꼰p \c=v6 bB4B{6l׺6'l#^0L- x lX`6 fa]x"2)Sh[y N6 ,rg}-7+Ĺixc ox  1iXضtˇ)mfGȎ& #F,cB !VKd|4m4Q#  ZEstF}x!L_mS   j&Pn*HPu6(_ī~x6B JAG]2b۠HDFX+ªc28 "3vo"6g#xp0aIu`^jȑi _: }bFѩ0/n  {C_+q(q0,H ӖSެ:m8[ +Wz"V94  K8X:K}Y*.uQQ}t vpJ$@$@IN ,2=cup%8+ja 3K6Mo7rcǎ#Bx pt3sΕ &.bsC4 qÖN m_Gvx^ðCZxC6i@-xP&fM!¶(|bKׯgL>\P!@AD  ͯJ3K{45+RK%JGM3ʤ(rQJmsJ?MR1ŔP 0AϾ{{o=Ϲ{Zk~ksw &]!lYI̶QNEIOg̘~J< @ Oo,`Yo`y/ GM-l_s56}d\%?9NrU~{cbskGuIK'L]k&a[nSsV@V>}b\iȊ: ,ćFa[|%^$ڲDI[>c5Zm&^M6أ^i\p_ok `:9fmkqAάeL,~@"۪3=^\ruJvϜ95NWj?鄩78CsGcؗJѣGy@@,}m/]Mf`2I,.!y t %.X ! aG"AL첋ؽ{&DRiveŢm&a[>L$I_z%?%nr7!jY9TEJ5E\RQ/&*Y\:pgs6MͨQBZH[+],cdQ,*%Ͳ@6tNXpRy+oȲSOM D_x~c\l܆d]oW5<] < XQ%X7c4:#1 t7M+lj7*Ԡ(J \(XQHn0kyKzί*  ,vg]S&ƊI׭} !Xz@:@ i]L Jd(SN9̙/l%JN2ݏ $lO?S3 r'"5AOwq&1%\BpZpUZ"ErqWn~WE}/2r[bqbtmɒ%VK9U:[{~ew[I>1x-nz)CTs,*؎eϟosM1֘yL])l s&ѿۺë)F^@@'@ʮb`g{ 8M)l7z?Ze 6vݮKYZKH;vğsNE*p(XJӛoi>[ʒZʺ\\:&E^>ZE.+7հ~{|B~%g/}Y5  l96,]xbRr @݇vܴNґnA@VVv/$^\TEi @BKa[A5=E\YO .W2I~@@ =!W@p@z(NE\~p @@~c[㇐@ Kmi @hK ڲ7ߴg}3N4 mq @=@ ۋ-e˖СCmn-Q[>{| 41(l+mYmw^z;vlӡ @͛篦7 v5 4 %lX.\hr=[6z @ hZa{ܹqĉFR@ HV I؞?7uԞ1z@ 8x0ydeM @YmKn9z@ &'¶^ImiiZ@@j[HN a;cL @6v~z@@ lzx @@@FnGC WyY@ 3mm@ K۹V:@ 'ͣ@rIa;J @ y @@. lrX @66 @%\+ @@FQ @$aS @p<  @ s9t  @NaaG v.NA @ l#l(@ \@)@ 8mm@ K۹V:@ 'ͣ@rIa;J @ y @@. l֋/~6cbGϘ1clĈ֫W6:ĪUl֬Y/l;r!QuUu,]fΜeN:餪vUfPCÇ7)ͨkhy/lɒ%Sn7Ǚu]v}ل 쓟dnEG TBaayB@ Pe˖٣>j/Ͽ/` !u-h@v>Ҧnj_lo/kU׬YcGu͝;7)~%ǝ9u Ai!_8j(>YMj>;/Kl-SOrjqMmᆵਫE_몇"ҫ>؅^@@Fngp TNࡇro/j%kZgu8zxCkqFa{ԩ6$-X-ZdWkG:,[oH,6mWq1;`I&SmMeˉ?OK/i]vHt8}ﳷza[-bzZۯ瞶rJ+[ּgv˗{9|]&oX;p}Y+ɥCB_ugy䟽o߾H~fGyxom5/ز;oV/v=i~ӟVv{{#V){i'_TC})c;2G?3o=c^ngveď\W  dy{l}/Cۗ^zn&?Ki\yGlȐ!& \7p@//^~+C\?٬q;qN[[n|WX=&JJ )￿mƾ lb1|Ǝ?Æ c^I˹";|o^p\&ӟ|}nO-xM<RR26[[%Z՘)m6i46FJFDϛ#;n8C(4mf "iY]"W.E$-o \D(uKM]lSB ȿ/*RyUB  R(o.cF6>*+QZ餓N;3͏8 Au \ͥ~h]wվhg駟v6% OTń?X7a_KmU~h/\MÇ .Ft@$Κ5My=GGq?zβI" T'?1-Z̜93Ir9`1T}9+FHIJqk^owqݢ TX5/Oy:i@cŊl҂駟 kŌ|+>} [ZZ⩲[G}ZT%hg#_Ʊl-1igwﭭm-hnJ8.Liqd޼yCM7j-^~i٤- Gfp 4mf & aK¥ *ڄ }7\)KrI?{YI\bdSZ* >l/q姒DU z2)BT"gI.V¦,e=*^%Yb\9 زy >uo۶{~DUo{Y /Ki%A-شj,Ե#vl[PoVn.뮻 `W*&l{ꩧL=>Xc<`쳏 OžGB̊a\?%^>d}mG%J'!?H@DzܩdМ9s\L-U;V 7OLB=z$&}ƾkO |ZS?du$=Jڭ|/l=ɇ9" dY˚Xc7|ɱXm5$jo >7Phl4/%T-cwԻ._3ҿկCRLbE PjkQOJzV&Fmof;RsJն@ տJV}/Ng!@@=,Y Uޭm]kZ:.QtR}W>$}Փ"?XVԍ &DI/5[âώ27AA/O|n,8`ׂ5ikҶzټ\ ΧPyX9AkjS~pg@[Al67y yo7)K/Dтk nc ?y6RKrKq'Gv> zq.a,VZJ݂:u\E$,[y]t)$ t땴;ɜ ^gXPh o(dg?a͵R'b;Okp7&kZkkaYM?j)b~-,6o $F{XjwXDn o%u o~$b@,#N?!@@#^oS׿lGISOB^K/ ńb|$,=Kp7 'W -'l]FY,]x^ 'Pp9EخZ (oӝbv1rZ`*UnRoZ-%DG%HZ/`uJT} ϛX.zUWJLJm;MKkX/y˓V ŒiNBLQ.7 > >[%>+IQ0V7[}q-ۭNޅd {xS$}U :3<暈3<]ɂKt~!6y s+(J>UjUSOB^KZa;-j ^jIx,ُ.Ė 1Ov\ nuf(va;x\!8-ticUk+ȵbIV*#|'ERBtd>cƌdpuVb%|{'xk%*Ӭbrrc˕X|E}_rr=-lWN&y[B *I"T AW1 .JeKΗc\K?j)\q!iy-@.k?NJkGz q^)h;zCJB.Fs@ Da:s+ݓ9-]@ P K.Tl6sʖ :[|ISP.C'իW{/񯧺[9+?VBɓ'{vU)S)V|s8uˋ-*&;袋sG؎rL>%UӏcZ>׼Td_I)@}Pp!LݻR?BTK"9A[6ʍSU-^.U3V_ 'ς8jvJO_K{Sߟqkgu9r-mȐ!ɵv~r5NK叾cjҏZȷ} Lv 2qS?9򧭘l .g-$@D۩M7 0 @@ 'u$xXxރ)`j D8tPϢW߰W]Q(k U+l+DW-;LȭrÂg+'lGbP=T`<ܓ.1)n#F opE"ZN0m# ϛo%oygL\w;: o ZH'6lXr8EXAܯXeb=m-9X9.02٭* x*R%֓=wW" csBϬ$Ea;~+OP9Wۯh[9qEE5H7YUB z7? J"y{ɷm,>Ө>-UŅJn9/;RCMEt RK)v^")u?[mؖ ζuQwa/8ErQ,:VrH?(݆#wA%kwḰI߯ FZIH_n}~W]/tjGe'[L^I>՗_ݏGwJF{xa+Ȱ4 aYF~B $ &@*@Z:-[5XejNʧQx ykG^K--./~cL8N:+ ֆR¶DrGbaBĠYa],-Y$GZgB駟. ?Wؿ ȕ,i>KcG؎,ks9a[~5FrJljѭ*bGG/.q"n|sQV;_*p2} Vd}qu4GuN S~quJl_zT؇vJ?=$@ WZBjĖ(hc WY2"rr*g @ Ōb)yw߂˥_VŒ%omOp?bE[OUvXw퇞o>K9Y,ŶAk|͛W:#l:Vb*+gYK X}WgTK]*K~s S88S_u=SsUӏZʤ?-iLNGq}{/~#=աধͽu"Eq9mOq@@g$1-6N?QۂD@ȶ"lm6!_żYXs1ɘ瞭 ., '+=\r]dBc:CP6OM:\eeq}&mk:~Ms+`֬Y.FRa[+roWv~IJo} Uy6/¶w{6DVI~Yc:Ȼ96Dc٤`6lY3܀haH}:#ӗX>mDƺǂ:H *H$Q_K]wHL26v.~d]h1P9s椋~#]nUP6-IEPp? lhQ&x$#sLA yVZezLPeIDAT)xŋbl=6xcz-<ַo߆5,=#߰{ucJ^ poG/|mmsn 0H .E<W^ԫ pi@l 2Ğ}Y º)PegT^׃m J{ڂ)>O>}=DGqނogOׯW)GX| .d⮀قyӳa@a6l3oN{45T`<&կ@xJALjOIA{R{SjvނE7|l|O>d 1q-jS:3VgڴiQwZա'A ⢅7S۸qL, LǏ:S{Jm0,#LI5#ͳmV$K1xٳ'NG1R^-JI=kG-eb;*,AL} ©1W8p^Ӝ̦Ff}5o43,&M"xdv`8@Xa)FNB & vK $HhP\(ԩS b^{W >B@6Y)a[%H:vIV6l0 .dZp`,X!QB£*a[%SۃW \~RTO?Tљ3g k* <؅Cc'a[ ;MkLZX| #mvmqD g$`^x&Q^СC-LڥBBp(^,w9׶vgհ\3xg c yտ`/Qb4>z>VƵ2zִx׭5m.W.';<ӢNG3-w(2#l@@--- s < @y'HLY9J;Pu*R"{[UY"|˲7TqQ{ԨQK_O?Xc:L;]tC[\帠Kodw*iS2vX+)SO-_zkADjRZؖ%RpsSoIL0ߜhZҏZʴZ7j5<OlmHjm/ d=yv@ PvI4\ P@1adf.@6vN?n @Gaql/-= 7y 8m& lWό 5fqW@ ` L@. lrX \@FĦS @a9@zV_|уN*'  ] l#lw׹I @@]Ga@ tkz8@ P+ZQ @ݟ6v @ah @@!C*̈́ @@خ!@ Г l#lJ[!@@+FEF@ 8=n`@ P J( @=6vϜ Ca@\ @@&݃/M @ lf@ t=}~@ PvQ, @@. l#lb" @ %%1 @Cisa;?@ ݫW/:uj* @@O&.X޽Wÿ'wڶϛ7VZe&L!A 䇀}'}AC~zGO @ V^h-[̆ncƌAnC Cyoc気t  @Ii]cG @V&Md뭷^>;J @ ЄVX/^ؖ.]ꯦ? ЄS.C Xb-X]ύ1FN#@ 41z|.ʚG>dֿkiiiiA!@@#zj[r+~D)#JOZ @ rZ5kؒ%Kr(A ,5jGJY- @@{^؎s{Ve# @@!7 2=gh) @&]52 @ @ @@W@J @ @&]52 @ @ @@W@J @ @&]52 @ @ @@W@J @ @&]52 @ @ @@W@J @ @&]52 @ @ @@W@J @ @&]52 @ @ @@W@J @ @&]52 @ @ @@W@J @ $tR9sfrAّGۙ3g͞=;6p8;]tzƏoou]g ,(^zѣmM7 &  vF>@ @G}9Fmdr;_~͘1#r5ؤIZw$?I8]w+wva[f7?Tt~vwWo߾6m4{LGg}>@Ka| @]F; s] [ow%]wa;qmSy;7 @nu#@ tOlܹI{o6lXr\Nw6ɘ1chrYSڮ$^{z!{饗 tynVp3wF @GaXR @zFuacf3mr~ʔ)vW&ǝwi@?1= @hRۏ<= w݆kgnɞ|I{O>6|pzMӁ ?ثjr:(߿[}l2=|&V[memD,׿z[t^~?OyDqlm}c<9ۇ?a?X/;ڟ8q7@뮻lŞuȐ!O|"+DV+Vw9H0`ym^r~O8˖ll7|#>BZ4s[o;,[h)%  d lgp @ Gn/x%\R_~n% J-Z$ɓ'UW]\ .p1:9Ύo~O~ҹ瞛X%oOS;cmɒ%t ;IzUrr)/o^z٩js̱snW*WRa[ZeIUmK^gs%;8 b-ҧ-Gm?KiQgM;qZx}]w-8B m @rJQ¶\ZȞiĈn},ATr}$Zb{󞪄~K_#<29X%nd|[߲=39[]K?r![o% <^y?a^(cҾ K{'nkAa-t x][4 !@A: @ 4J>r 7믿_bBCqgWv7.~7\oH0OAIs5ٳ]Ht: 7`#GNvE嫯n6ϫn]veq%¶2W[ '`I.R;0wg¶\r> 0)`Rn\dE;8_,X?~%YFE ~b;c @@{#u@ P$+ fV𝶊n/x`Z?_.zǺھxtI~9+J ӛo龵oZImSv5o>o/X;Q->zmi^,Wha[n^KԖbIEIi!>:7fSQ,뮻΃Ȅr! @(Ka,.B @h"sugI|ד˃G9asʕҤUjgر0+V#@J-*-q o{oG>3Mդ>x'iKrE/%-V\tE_ X_yB J lWB< @!lW{Ĉc_6;oV̊iw?Vl9DTn-gW3LO{5tu庪teܹZʭRxqС5hO׻e:ѣvC=}'VJֽvm6a„`Cvݻ1cO<~gΜiW^ym߾/>rJaÆ[oE}mĉ{N?}f'za|Mzf#to=\ۃ+#Nx,`@^~+B  L KZn݂9= *[oSLqFi޼nN[v,ͫBxڷo,LN?={ڊ+Dٲem6rH/ݲAmgFC{RRR2W'R" &r(p+6l#;톓)U+p ! Pr+ֵ-[̆jƍs׫]q6}G E`[3+[ޭ[iB}aEL_NT1}t}WlUVu'O쮧E6yS=]vkf#FuEZjN:Y"E"e=AF`;,4 d$@@@ |={;ǝ۱TNo\ ϓB@ T^cŶzj*S+Wn#7ntAiJ\)Գ[=xZ\^A}&웮SeUmͮΝ;vY~«zϝ;J(>}N>&C@#@]x%W@|[:i`qum%l6V\Bm! @b wy>κTR6|p_~po>uѿ@@ v f!@dzkFpR5+{Bl)%\Н=qpu~Oy  PP b}atA֫W/k޼yF˲%Kpnר׷ /2T;vlzA /ߏzF?fO¿%GձMwY߱ Ǐ=u; Ėfت+wqك>Jutb={bŊE--    @n l_]UVe:%%^ykذaϫ* d5׳Fw88o4}!J(a͚56mڸ 5}jn/ݻ @@@@H`{ƍvYgE[li+V3f7xVi}ו+Wno*r[E$_nrʙzkWlnߘnNoT-YC}qsYJSeTz!۹sgIJ '17    '?n#Fj^֭yn2,M4[ 4sS_g"vN\waǏؕ / }ךMp}VdKmyE@@@_ yrȐ!:ڱ7|c=zr!/Ė-[l„ ns4j8;x~UU+]}ٮw)S@]u>`k߾}m̘1O>\=?裠wʩynXNMM>[SzʝWC_Ӡ WꏟqVT)3gh=oР;j269?sw\8}:JNO+lݺuLznpЪU|ѢEz̺O.[۪U+ر3ֆl駟ښ5kt4PoeggiӦ{Mvs=MrLqttԩnZxQG9lkN;-"y=V[tzMx ^ķ&˘F@@@YU'g D;0W 5P S'o߾|r?+UW7@ Ff)dչȲe«i:;x`fP(_w }znfS So?U2EV 5ݬA4}]۽ꏇk?.}QY3 /_K9:9)4vrJAݺuge {Q ϽUqPJgotsrs'_݃zlU"LGz%a'|fs2!   @R*u@X=_lg矷^z)]v{Mp۷poVa4߶ojk=u|o9P12lz}mPf͚*W^yeDA`U7^zk駟n{o_}UD"!j|)9(-~zT/ܗQ2 fWvzvyjfUB%j%LWnZdN?_%TTJ%ԣ[aGTFM n>h7ת2pݰaCG_z܇o*M |t_͛7X_A7u~ym61c`ַp꭭VM$zGkt_U#7\CVWSuWsUỮ7W-Xψ;#<2x'?5gxN.6zćK״W 4@@@@H`[(bq~^{y޲jbTHGP {{F*wQN{U/P-d5`j`[eR=еM7sڵ4lzOG7E25-z[%[J~yOӇ у!jj*UrFy<>!jV/pMx:59|zF6jT~~U)&۷o,E 4o 0    IlLo*(TQܹ-X Xŗ/ 6g7 8l~g 7t5?d>J< 0|`رc#JLe`[ᇠߗ^<ܣ6lp5ÃD4bӦM# ZO{&ι`o_u^pnU2e tl`Uj|hggDu.C?7#4mҤk}n:HOT+HvPݝ BsQ 1].|OKqhzΫʈ(؎lkߖ.]j'Ov,(8WBh߾Dxy c[uN=TS/s=i3<*Ws߾+zr*2&zz0=X    9w=ϛ7/R|Qfؽ{tM) ͣ{\y}\ve V`1OYAFzUS nVi}i>qĈZ>Vp9Vߴ\U7fVo?9䓃US@ϚiӦgHC .[tx>ի܇:k4htpY豭sE<5SOQGֽO A|@@@@ $u-~ɮEVhݪU+W'꯿^Ça*wܾ}B<5aC -Zm^tR$Nr?Ƕ\1x]ZF=rUKyժU~^ti lkpI}Cq lM 5*QYN}ٳfstSZj[tpqe ϧ>PЇ >V?x^/Bl׿UF-`[׭[h    y#kgN:EP TWQ*U<`s=n=v@s"G+qJ UUClٲn'pB0ەЀ_|92W]uG-1d21a?jԨa U<ܫ;?mzc|l5%K\/v}S/%mbʪt <44@@@@ {s,MP@|g.W{}(0[;0Szgc"   vQ M]    @b l'Jڳ%N[υ#    F`; 3 ȑ#mҤIӇ    $vro@@@@Hx턿\     \uZ@@@@@ r     @r l'j@@L`k׮tlٲVn]kԨxǂXrnFϞ=cƌyٿ/;ꨣL  vΛG@@ Lω'hzjժejX~ɓ'[Z}q^ݻw۩g=c ڵ;ן~Ɋ)⦻wn'NnݺW_Do}y>Y3/,9 $@LwkE@H:ln>߹sX¦LbVr˚5kfCEF[o^y{gw#4]/lZw,Wv~\[@Q\g6@@x  @aUW]eԣ'z-뮻쬳Ίj+L5kl۶mVB]d |:<= D lGz@@B%`[^ϝ:ur=U֣O>yPK7Y3 @ l'# jB[/0#QpLU?pJ4imӦMx`zѮiz0ϟ?}][x`qǎ?ط~ܹƍP &S5;<].]>ML>g͚eK,z28RJ3^)ʖ.]j-Z-[FB=Ǎg)/SU\eb|t_{6m/5k4bŊ~~h6lO>_5VR%kذqV|`x&tm|-ZԃdɒVzu;cL/qus !@D@HRxm?BYmڵ~^N{'Mvٺulȑ.x۷ Bq̱wasNEnޏ?h]w]+WO?4bGyĶl1_oT6DklK/~уGΜ9nF[~}C(\W@_D d{}w@s {l_nFOdB3 /خ]b-N: /^<댹3f" @ l'#  @al3tAz*VS}{!˟Vo)S&wM-o~WƍOZL%PԎ=XܽzE;-Qۧ~]v Ҡ={^멷yL=gϞ|`t& Ucǎ0_~uq!zϷ^zߙ6/ݗ˗\pm  }:<ԓZ=?3}׼wK!b> L1~͛]Ou-W{SNu@!@Y   g_~G}ԕMǵO=:ָkLn>V蔔W[xfmQY۷0ܗ9+9L^ Uku?~| #FTB՞~ꩧ\>+Sy־{gEq}7p{7\OoS; /Ӭ痫'CetF@غu덢ꡠz4@@PUZÉs9>nР)>TMdPizGl\|n~?< 2ę&g+_`V=o(}mԨQ1cР;+p!t: åEz1 k[}KTFuKQF~UYgj܉>`{饗<\|С^_ѫ[|Wn]gx@@he˖_qx  NU=tj~>TXe2T(0qDhk[c_(WS}AilkDrǶJ(V 5-˞~V l+ l:?ޅѾu㨉_ y 75PczMe?t] UFu'>:(iu\lM  @Rޜ9sLK٣_+@@ D`z*Ѹq CA<>'ls1ֶm4Sת;]×P9 _#9̐^> ^ziMsqzbu]r3S= >y[-Oe>d^IݡCHDp/|ljje%؞2eM< `[pa`۱ę YH`{ɒ%U'+l  ? WzXPVm ֗3 QR*yQӀ nUB=|@ϭztz&5cʔꫯb1/`[~j| ~opA.],^A5({GlF8<}gvZԚ}{'2 `LZha-wy1ř YH`[HΜ9әJR\/m&ʣ6  @ P[=Ë/v'մiL{sܧOg@6{ꕫ-ur+־ fA k*?ꫯת`;O= 5X 6Ūm[zG}T/{mOˍ`[%F. ͛7wnM -E@6W֬Yc+WU vF @@)mP=vB70[vS6rHJOGVou]gQ=5dzkڷoo=Pz UcjG7=* :~*ꠣ^3ݻ;B=z=lb |C 23z꠿q9餓\);9ؚ=w\Seր)LViPl\hxYgEݡ}Ǹ,USO ѵ?t[FD7 ?7JZܹlv4^z%kٲ?[|sjz>[F^i@@ ,/TN 뿅F@HL O.M6y!<:OT=zoJ\7T?:'7m}0ptG=ҥKr#paRRRyet3 D $} "  .O4TN`vVnz74mVox5ݸqc޽{  ,`TVf@LSpsqg4j>JhPK 7^? 3 U]%Z~m7e8UT1Qr.P[dԣ^=-Zz}+̾kӔɍ`[ǔz4O4(Vy]jڄ B n:3V@@ $@MzD@ vKנꙫHmնd7Bl꙽rJ[jv9Q >}K҂cT~DWbŊkKv @@ Nm8VG@C`;1g  @V ܰ   P -@@Ȳ6v6D@ vA;   &  @ .7B@@  s1b  @ .x3B@@  sYb?  @ .PA@@ G sbg  @A .(w@@@  sb  @ .7S@@@  sb  @ l?GG@@ 7 sb  @ l=F@@  s!  @~l:D@@ o I(  @ l18C@@  qP  ,k֬3fؒ%Kիm}:u긟f͚YժUs`;Wy9  *@M G@%0uT{7m„ k׮t/Xbv'oZJw, Ύ"  P ! @B̛7>K;|µjղc=jԨa>iֶ 4H,;3Ƕ  lmrv  P>#{衇l۶mVzu[r;SO=zOnzmڴ)zK;ӴMN5휒d?  <mTrF   gG6֭m޼ن fM4Cرcm}ԵkW+[⋮tgE\?h.  %@MXO,g F^pz]TRK.$ իg_|޽;sV#G&ѭ ȯtJ  '@MxO-g ._[Ϟ=L2/[ƍ9s1uV5j}WOgxoq̙3.rWs3x/ b@@X``;_N@C`vۖ-[\O?>8N:ْ%KLaV ŚS/Vy7|j֬,w`;^1G@@ q iL@@@tȣ>ʎvmt1/7't=#h:&YmYc;@@ 6vJ9C@(P+RbE7PdZ7n}M:tl2KlÆ +X˖-eNl+  $6v<) AСCQF.VT{[֎;8S-mT.sڥ^j7x_W,   !@M*' K`vM7oGq*TNrڵ֫W/Szb0*WV;~ON-Z435`;SL  @B ll'I# @ (+m޼yr$<Н_݆nWpUT.. z I_69n#ή #  Pp ə! @زeN'|b%KtD:wlOp /^l)mݺu6j(2dZ]}pYnV^ 6  $6vb<% hԏ? pwԩSjԨzg+lɒ%A&@D֣GҥK^#vr3@@ 6vz 9@H\UVcƌ1x⮖joܹ϶J*Yge{UV-S9%~@@(x@@  {nHիm}5]jڵkʗl,E@@  ) @@\ Tv  @ &. " vz7@@ 6vAz9@@ s!  P C ! 9!@@@)@M]0L @Ȧv6@@,@M]ON @Ⱥv@@.@M]ПQ@Ȓv@@ &ND@ ؎W@@HmyZ9S@@ 8X@@ &NGE@2'@9'B@@  @@ l@@HXm}x9q@@ #tX  @b ll'# ll@@ 6v!x@@ vZ   PX ˳u  A@@P llA@@@'@M]j@#@c  @H`{ ͕! @R "EX֭ڃG@@0 $m=m4KMMƍ@@ 7ڜ9sdɒ 4@@(Il/\֬YcUVul癫@@ۋ- ~W^ @@Hl nj3gtwN[# |omkڴ)S&i@@H` uϖ,Yb+Wt_MmР$@@͛m޼y\խN:~   P:SUsQyTc[eI*Vhe˖%J%  #c۲emذzKEcF@@@@[`;n26@@@@@O     q lM     )@@@@@@ n@@@@@ ?Sc#    -@7     v~sl@@@@@&c@@@@@ O}     vdl     ϱ@@@@@ ؎ @@@@@S`;?96     @q     @~ l>F@@@@[`;n26@@@@@O     q lM     )@@@@@@ n@@@@@ ?Sc#    -@7     v~slrQilg |6Vb\<F@@@@ Κ#!}αguǼv @@@@ -Y@ ٔj?jU*mc/~(".W U)kg@@@@9휳dO3/tPxQSv枰{m 8T| @@@@I`;@c~Ya05(eO୚u뺬   ǰ@IDAT @A .wsB ӗnXhsVnN"E7.;f?ۧl4˙    @l'ʝ<Ȅv&X@@@@ rWR$<     "ʕ+_u [n&;4iR%\bժU Ɖwi 4:_1cl޼yg=9ԩc 6ƍDm wF@@@W 3fX׮]5kرcMK 6̚6mٳgVt}~N~{nw s=y::vmĉZdɒvUWghѢ&W_)xժT.k[^j}={;ŻNol 9lWuoN:C@@@@  Xڻ<Ǐ}'x=G}=m;vND}Z8    %@`{ʔ)6yྜyVJ}V'r29ݐ!CR$uֵO>9U ۭ[6zoܸѦNjkg?~Yv<{U6VtqxAsQT]B@@@@ 7lM`31l{vG\~FoѢ :4xW8    ,@`~-;vʕ#K=,Zƍgsεˊ/nUVVZTm~i߾]s5VlYC~{]f=k,7@~g-[6mڸ#Vt}gR@ׯo|o>zs1bD0vvqǹݯի/pׯ&Mؿ/S ږ,YVTz~ _3lk7o?>葞b_}U}oٲ&L`sq%JF`'xIMM5o?Sl֭q@@@@d b# aRJ^Qsα 5o^}`'|b< P~wg衇 z%n+b_-[,b=QH۷o_Sp[z5__e]nv[n?{-R۾;/ݼ ^zo2lk{VY~}oʔ)˃y Yfsm̙zPb    $v.*i0ӷի D{[/@+> Cu׫ںu ˕+zl+DMw}vis3lgeg϶{Ν;cEOTX6lfFjժ^}iZ-Zzu@᠃r=rl5F@@@@ c\o VVdIw'T/ݛnv`,裏L7*?]xn}]w4ȤI\_"Xb{Y5fhmۺP^>̭9{ rѣwo*ەj_O>Aur:VCX|;ꨣ쩧ro5z&UEȏ>hwd6p Ãjrj=myE@@@@ s!V _C,TzB!o Us[MwW =rHz}/]: d gϞnqt\!o۷owUoZM`ݷWu;ui?{MF{߲lʗ/v+߿ʟ'/)ϛz֯_?k׮^_unwuz(;3"    $v.z[[-z衮 j1v`Sƪ)VpݴkJ*zjzcWvtɯfJ+Ul~5@u ֌nzPW\fg'؎o{]W\a7 j*QAzu+Tܗ.vچy    $v.mJETBK/{z][FftvM+U8U%Rԩi^A@}`;6̞| `&4]nۺnVD~#˳ ?^.X-k}!    ۹lAZfw˾,I0uv2 6g͚e_|qnf'Əo0:`UOnn&LJ~|Mg[n^{O}vmnӦM_'WٱjiLA|x? yu5 ">f7X[OF?jU"    ۹l(ս3~    $v.۪ݭ[ |NSq }2e֗7on{?۝wԜ_j?־}`vfuaݳgO[bEp([\rȑgGnZ:XMgT&%%%Xwrf_W@@@@U`;m=L˖-Cڸq\/]q. /Ӵj8 E`[3+[_~i[n ӄU5mcOUjժ׻TSfu&N#RG UV} ѩS'Wkfgg@@@@dH`;/nk,^V^m Fj0r*; URJIF٭ VZ^ʾ:UV%T7N;wڵL2n^M]Νk閙d    $v"ܥB~w ÇW6;v    I&@d7 ^!Cg NMe=ڶmkuuUC=4oԨQA5i     y U+ׯ3&jӦS7 @@@@W`;y}~y:uꇗ-[֚4ib}9S5N@@@@ {s Tb/^<k     $v2m@@@@(ۅ&r      @2 l'Z@@@@@B @]n"    $v2m@@@@(ۅ&r      @2 l'Z@@@@@B @]n"    $v2m@@@@(ۅ&r      @2 l'Z@@@@@B @]n"    $֭[myf۲eر#@Hx%JXٲe-%%*UdeʔIk@@@ @ۻwe˖ʕ+c 1@@ !WnjղE&s   @R͙36nhE z/^܊+K@@ ]lΝ濉ʗ/o7v@@@ Il/Y.YըQU'˪l   ʭXRSSM=ԩ@@@ 6VO3g:J*zi6v=p@-j~on/vkڴ)5s "  Il/\֬Yc+WU U`;D dSCEzmk׺ի#9   M澚Z~}KIIq <@@Pp[͛mstP;c @@(IlO<`lҤCGF6 @@ nk I؞5k=u։a\  8 мysl6 @@׶ӧjӦM8@@$H`[_I-Q?.@ zmJЩl{! $6v>\9 jB}{8@@$ &N|@(ۅr]  ll  P( m@@p H]vԏOb2uۙbb%@@R``;!\N@ Ν;m˖-uV۶m1lkҥK[2elٲVxt!ӥa   /@M1 C`Ͷn:KMM%KZr܏zoڴugKII @   mB s  7nk׺2#5kk)Y|rV|4iH  @ &.43 )k֬qjnvժU&؎Gu@@H,mzb9[@(T*/b 7@dݺutm-rJ֨Q#6v8@@ &ND@)ի~{-?ʑ,Xw_PBv@  N``=\ $ʕ+m۶mV^l… tVz`?  :mBPsA  i2"*U}'Io߾}QnRJ\oݺu~zS9bŊucR1@@B!@ZlwVYF5m_HڲeF+]x_6mKڵksՓJ*VHjis4;c  %c?QFjTW^6k,7I&6`4Djjuϝ;6lh%Jpl;A@@P la?{=l&*:Cb/#Fܮcǎv'hz֭۷~ /]uճ Y=ߞW@@`o=gkѢEUávٲer}pV\4ָq4D@@@lQ?owv5k̅ӧO7yvUWE<\zO<+Sc|6 >CK.NylnzB)^d͛7^ѢE{ijZf'@@`/ ;R˖-#TI[nP[{[o?+a~<`;4  T`;{oث:3`[gՔT5jص^'|b}[WTXۤI쭷rF.? ^#oc} dF@֪U`uP馛\]bE;[Ի[o 6p=uTףۗjv  N`;5k<>W^I_UoN8:u䖫WݴSJ .\ǕW^,6lִiSk~B_}]#A{v+  X)SGP[4h bW~Rmڴ lL   P ^`+'o߾=6x`SY:;<7>۾kw}_5ᄈ7|J,i?^ʐT۶msύFo[O>G1= @Ȭ@`[m6 ۱ڪU\i#8"b0lXZC@@p lAz 5Rt=VZk׮+"(Y.]ʕ轎\ѣG| ۷v[8; v  Xv&6KwtiX  @ ΃`[O{g&L*UpzaZn}駦ؕ+W;BVpݾ}{Ӎնnj{v:7`h^,uC'#ܠ;wc9&uv @@ ۙ@b@@)@GUV?t_U/^WnU?RQŭ۪^޽h.>8XM{ŊnrAr䉾n=fx_L#  lg2@@H`;ɓ'oaiIF\IHg}wqi3Tjd׮]ֵk׈T^s=t.,?Zc M`{oB,G@@ ޹s߻^M4Wjoq-juݲeK3c;lꫯ`B nɚ5kL6 T(HW %Jdn dR`;P  i 9J?\gO>i+W4{+SҧO믿Nvi4M=*eҪU+۱ciۿ y[R"8q 53#<nj9o@@LlgU@@@ vۏ=-Zȅ cyp[Dpֿ[d\șgk37wOnvnvS=Xgٳg^7|[%nj!  lg2@@H`;;ΨݻW^V^u|~f͚W_>Ι3 䖩tI*ULMFe*cms{֩SDž˗/5kָ գdɒWm5mܼJ*J;KCքx0 @pۈ:"003 Qtp @vv> mN+J؇>HKl[vm6 =nΤ|G @>ۂ] P˗/^ߋHLٳlwQ!@ Pwmv.ZzAÇWwؑ=db%w @' l] @H^lYjϝ;_~G#O7vQ; @l vE @@tttd^bE477gt9OxKKK̜93{(wcccWqv @u' lM @hmmիWgK7.[o%I3Ӻڛ6m{#Fvn> @+`nhCԞ@ m/B @l v5 @?   @W@-خ߻ە @CZ@= @Թ`[]緸#@`{&@ mP]# 0Cp]2 @l B  @% Zj  @ H9T=xWK `!^W kw @]`K,֘3gN?>?vl @#7ҥKcĈل:WB @`h `>&OglCl/_k:JD @`h `{֭Ce^=4oWMOltux`=>/U @C6Nc3s=jcǎK&@ԟ͛'Ȗ:uj̚5. @C:N\LyiY'Ƙ1ci. P{mmme˖X~}H,yR{ @^N`H #V\~9( @jK Ԟ1cF466V @WvQ(VL4F P;7oޥ嚛];C @lM @ @) S_ @ @ P`l2 @ @ @ Ov&@ @ @e)@ @ @y 6 @ @- .L @ @S@  @ @ @lvd  @ @ @@ hGB @ @ @O>19 @ @E@]-# @ @ 'vD @ @" خ @ @蓀`OLN"@ @ @jlWH @ @I@'&' @ @ @@e$ @ @$  @ @ @Z2A @ @}lI @ @-qXjU֭[s=zt477Ǵib%G+tXxx¦74nD̛1!{-7%Lqm۽ @ @ 7Kʕ+sk_ï,0cƌ3g+glokn},~pk ?8=#w],-E@  @ @V`ɒ%vځTm}CHݩҶqjoÆ^Px*/վ#v~p^s^Mv^  @ @}0SPUtZ%gngƷX^<,êʇ^W::cGo?nvssClFa @ @R 6Ľ[8#c„ 'Vow]sgcqP{@YYG{{a- qƾ{gMVLk~J @ @ C5⦻WĈ1rJ l߶=ZẒfƇx`oU{vEyUN @ Wŋǖ-i&ƌnƪm1vlrЋ:;b1yTs*v.%@ @)hѢ5 Px AǍO^تIt㦍=3v.%@ @)_W>אk_/eV`Vu7nOʥo\5J @ S@S> kkv]T @ @.۵=r{/.W @ @u) خal{vb'@ @Kvm`ǯ s> @ @@] k{X۵=~^]  @ @R@]*خ+rŜO @ PVvm_l+| @ @l k{`\1 @ @ԥ`U]Wn9 @ @.۵=r{/.W @ @u) خal{vb'@ @Kvm`ǯ s> @ @@] v}7GGGGMMM1f̘:uj̙3'빃u%Ngu`5ۧvj9>zq>ulɚX} fgձ/{~=l㨺& @ @mY.;vĉϏiӦJOW_UUWECCC*Zy8ճk~q^ F55ƍώ]Ozne>n~v @ @,W~>Ӎ3o)H^|yƍK.$v}nveK¿]+{m[.=&76`h_>Z[[OqGDZm͚5gubv3fG#FV앂|]wݕ.=ztV`]kl?vk\qp4/ҭOͿ} 'neB/可\?U9y܈Xp}zWJK?RX|ckk{ J &'cF { ?hkȎßۜw/6lVg_'Ś!)O Ϟu[mSXwvhUL @ PKlFvm( /nܱcGLɪUbҤIqa{=h2/ܓ'O|#݆[n_WwtPW:/ ʛ⢋.ٳgglo޼9XbEVGZ.erYOqͣb t iq {eKqS-M:㲰|t!'G4w fu{%>{IDATP8'ߝ_wtFy(\1O'q;dBvo%>+N @ @+N˅"ZZZdÆ ,<ȬY~Ke˖ŗHSp=~b{xO}* ~s…N:)7m۶g?شiS9眓YBϜ93[*g?*N=ۉYʷiFuZ-mlS{^Gw*vR wG ,.p؞wB_myB~9d}uַ\zL ol|x`m @ @@U lCɖ 鹜;52%=-[?|Y>p !Ii}466,_zk5*LtN` ?-u7;% l'O'N?|ذuGGz L4cknZ3{K`zfF뎎[ ˙+0kY%Kswl߫ @ @I `;=DfS8=2_7)?> ?OcW_;riw ?Oa/W@`;S \ qŏz ~qqq/O{ןwDV_\wol^:[F`;N!ϟ_zi▖3IAy^[=}o> cEz ocȗ>01#6u;<_[Xo;-W) l  @ @YZĐO|"9OS&fu777gpc=/ٹ`;˲!霾nv*4<-k2|lɒiӦ=^_Bav%Ivx/@?|$z%k]rLF"@7/$Y:'/NLM>WOf;C-}; @ @A`;|ӟΖ&9T~_dˍPNf_7K,=`l_~yzR}}J3<3RbڵC./䒝6{^=F$iTXbdM\|Iܾv?{cq/M41ǾSd3T wV`[w @ @$Pv$iu .5kd3L[oO>d{񖷼gsZ;N'#Fx# -]4lG=ۉpgi^o헆gı6L|Nےg6f\GM5uN;N>xr|mI+w`~O @ 0lq 9lI#<%bJgl?gǢtwbDž^}]:chY<{^yْ$cC^vBYs-I2i܈̵4>)N7 _sV̌{G=1y~TºWXLɅx8:J#l%|$@ @LN8!ҫ疖iii?qme̙WuW\-~{ӵH/^?BR.(o3L3f8qbWwuW|>s9qGd/lo۶-X~}.K `{M<< Ul ׾{x`XbtY3~@%Gۧw̎M,,urT-/bY8zB+U]/:*R)_岸ݎUCZJ'm<|1 znu @ @&Wݭ/a֬YYxnuUyHٳ#̫V>C^g}6L]\So~iI3gfu^:[$y1[ִm/lR_WsӚ'|r? `;9zȔv˹f3,C&ݚN4NiK˚n܆ْ#[ڳ\zʾqڡS#.Ě-Cq[*=lWJV @ @5%P#ͨ;wn,XkFv6O~~{Y-ik^XhQtM敦::ilɲe˺fvݴVJv*o}+lm$I`lC%N ϞG v:fPua} ˋ5<}m(̸gc[[GOe{({zۘ%I 30" ;^,4ׯ;{d:[O+'خj  @ @jK`immffoݺ5NcǎVu벙XccccC M6ŤIb„ /9ڿ`{L 5C!+`- em @ @T@-U;lr5!خ @ @D˂P:۵3VzJ @ PAvqj WQ* ]!@ @O@@,کC];c @ @lWwlr5!خ @ @D˂P:۵3VzJ @ PAvqj WQ* ]!@ @O@@,کC];c @ @lWwlr5!خ @ @D˂P:۵3VzJ @ PAvqj WQ* ]!@ @O@@,کC];c @ @lWwlr5!خ @ @D˂P:۵3VzJ @ PAvqj WQ* ]!@ @O`ѢEٙ_oX`A5: q#vvr(5ݸic4|}.87o4W#!( @ @@XxqlٲF 3&ϟ?=տU-b옱8q@Vٮ tw-cZZe,-'b @ @+tXrVA1cF̙3g@ZxCq+bDӈ9j֭]ؾm{ƙG͌]S @ @``6l{VA8#c„ 7ŻPgg=0k{؀֯ t֭7+ }׿v`{'@ @8rjlb߿Gw,Mb(.;ZxqoOG76_EgG5s  @ @tXdI]w>Tzh:׶#.=qS-ك 7 a C /[Rxܞ^;bGێ쁞_8U4<1lW~@ @ Pepz%gj^w>@cCC}q/̢/NW-ܱ @ @@6nVš[%V B9zhnniӦ+Lu.]1=&^!{s8IFļׂySbδޮDݛ  @ @ @*N?š%i ?#""@ @ @Rv} @ @zv @ @ @@`T> @ @T`H  @ @ @T@]a @ @^@]C @ @ P* .հO @ @U/ خ!A @ @(83!: [ @ @ @8*D @ @ {q @ @Q@]O @ @ Ы`W @ @ @8*D @ @ `1: [g9@ @ @D@]% @ @ 7vߜE @ @U"71,ER% @ @ ]v{{{gZlF @ @U#:z)STk_ @ @Ě5ksύaÆEï `!@ @ @U+hѢꪫbȑѰp.j;c @ @ @oqECa뮻.ON @ @T>\pA֯I&l{я~:C @ @ @+;3F&L>P@sTIENDB`././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/doc/source/admin/appdev-guide/step-by-step/step_by_step.rst0000664000175000017500000000336000000000000026421 0ustar00zuulzuul00000000000000.. _step-by-step: Developing Murano Packages 101 ============================== Murano provides a very powerful and flexible platform to automate the provisioning, deployment, configuration and lifecycle management of applications in OpenStack clouds. However, the flexibility comes at cost: to manage an application with Murano one has to design and develop special scenarios which will tell Murano how to handle different aspects of application lifecycle. These scenarios are usually called "Murano Applications" or "Murano Packages". It is not hard to build them, but it requires some time to get familiar with Murano's DSL to define these scenarios and to learn the common patterns and best practices. This article provides a basic introductory course of these aspects and aims to be the starting point for the developers willing to learn how to develop Murano Application packages with ease. The course consists of the following parts: .. toctree:: :maxdepth: 2 part1 part2 part3 part4 .. #. Creating your first Application Package .. #. Adding User Interface to your package and other improvements .. #. Modifying the application to do something useful .. #. Adding scalability scenarios .. #. Learning some advanced stuff for production-grade deployments Before you proceed, please ensure that you have an OpenStack cloud (devstack-based will work just fine) and the latest version of Murano deployed. This guide assumes that the reader has a basic knowledge of some programming languages and object-oriented design and is a bit familiar with the scripting languages used to configure Linux servers. Also it would be beneficial to be familiar with YAML format: lots of software configuration tools nowadays use YAML, and Murano is no different. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/doc/source/admin/appdev-guide/use_cases.rst0000664000175000017500000002330300000000000023336 0ustar00zuulzuul00000000000000.. _use-cases: ========= Use-cases ========= Performing application interconnections ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Murano can handle application interconnections installed on virtual machines. The decision of how to combine applications is made by the author of an application. To illustrate the way such interconnection can be configured, let's analyze the mechanisms applied in WordPress application, which uses MySql. MySql is a very popular database and can be used in quite a number of various applications. Instead of the creation of a database inside definition of the WordPress application, it calls the methods from the MySQL class. At the same time MySQL remains an independent application. MySql has a number of methods: * ``deploy`` * ``createDatabase`` * ``createUser`` * ``assignUser`` * ``getConnectionString`` In the ``com.example.WordPress`` class definition the database property is a contact for the ``com.example.databases.MySql`` class. So, the database configuration methods can be called with the parameters passed by the user in the main method: .. code-block:: yaml - $.database.createDatabase($.dbName) - $.database.createUser($.dbUser, $.dbPassword) - $.database.assignUser($.dbUser, $.dbName) Any other methods of any other class can be invoked the same way to make the proposal application installation algorithm clear and constructive. Also, it allows not to duplicate the code in new applications. Abstract dependencies between applications ------------------------------------------ In the example above it is also possible to specify a generic class in the contract ``com.example.databases.SqlDatabase`` instead of ``com.example.databases.MySql``. It means that an object of any class inherited from ``com.example.databases.SqlDatabase`` can be passed to a parameter. In this case you should also use this generic class as a type for a field in the file ``ui.yaml``: .. code-block:: yaml Forms: - appConfiguration: fields: - name: database type: com.example.databases.SqlDatabase label: Database Server description: >- Select a database server to host the application`s database After that you can choose any database package in a drop-down box. The last place, which should be changed in the WordPress package to enable this feature, is manifest file. It should contain the full name of SQL Library package and optionally packages inherited from SQL library if you want them to be downloaded as dependencies. For example: .. code-block:: yaml Require: com.example.databases: com.example.databases.MySql: com.example.databases.PostgreSql: .. note:: To use this feature you have to enable Glare as a storage for your packages and a version of your murano-dashboard should be not older than newton. Using application already installed on the image ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Suppose you have everything already prepared on image. And you want to share this image with others. This problem can be solved in several ways. Let's use the `HDPSandbox `_ application to illustrate how this can be done with Murano. .. note:: An image may not contain murano-agent at all. Prepare an application package of the structure: :: |_ Classes | |_ HDPSandbox.yaml | |_ UI | |_ ui.yaml | |_ logo.png .. note:: The ``Resources`` folder is not included in the package since the image contains everything that user expects. So no extra instructions are needed to be executed on murano-agent. UI is provided for specifying the application name, which is used for the application recognition in logging. And what is more, it contains the image name as a deployment instruction template (object model) in the ``Application`` section: .. code-block:: yaml :linenos: Application: ?: type: com.example.HDPSandbox name: $.appConfiguration.name instance: ?: type: io.murano.resources.LinuxMuranoInstance name: generateHostname($.instanceConfiguration.unitNamingPattern, 1) flavor: $.instanceConfiguration.flavor image: 'hdp-sandbox' assignFloatingIp: true Moreover, the unsupported flavors can be specified here, so that the user can select only from the valid ones. Provide the requirements in the corresponding section to do this: .. code-block:: yaml requirements: min_disk: 50 (Gb) min_memory_mb: 4096 (Mb) min_vcpus: 1 After the UI form creation, and the HDPSandbox application deployment, the VM with the predefined image is spawned. Such type of applications may interact with regular applications. Thus, if you have an image with Puppet, you can call the ``deploy`` method of the Puppet application and then puppet manifests or any shell scripts on the freshly spawned VM. The presence of the logo.png should never be underestimated, since it helps to make your application recognizable among other applications included in the catalog. Interacting with non-OpenStack services ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ This section tells about the interaction between an application and any non-OpenStack services, that have an API. External load-balancer ---------------------- Suppose, you have powerful load-balancer on a real server. And you want to run the application on an OpenStack VM. Murano can set up new applications to be managed by that external load-balancer (LB). Let's go into more details. To implement this case the following apps are used: * ``LbApp``: its class methods call LB API * ``WebApp``: runs on the real LB Several instances of ``WebApp`` are deployed with each of them calling two methods: .. code-block:: yaml - $.loadBalancer.createPool() - $.loadBalancer.addMember($instance) # where $.loadBalancer is an instance of the LbApp class The first method creates a pool and associates it with a virtual server. This happens once only. The second one registers a member in the newly created pool. It is also possible to perform other modifications to the LB configuration, which are only restricted by the LB API functionality. So, you need to specify the maximum instance number in the UI form related to the ``WebApp`` application. All of them are subsequently added to the LB pool. After the deployment, the LB virtual IP, by which an application is accessible, is displayed. Configuring Network Access for VMs ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ By default, each VM instance deployed by ``io.murano.resources.Instance`` class or its descendants joins an environment's default network. This network gets created when the Environment is deployed for the first time, a subnet is created in it and is uplinked to a router which is detected automatically based on its name. This behavior may be overridden in two different ways. Using existing network as environment's default ----------------------------------------------- This option is available for users when they create a new environment in the Dashboard. A dropdown control is displayed next to the input field prompting for the name of environment. By default this control provides to create a new network, but the user may opt to choose some already existing network to be the default for the environment being created. If the network has more than one subnet, the list will include all the available options with their CIDRs shown. The selected network will be used as environment's default, so no new network will be created. .. note:: Murano does not check the configuration or topology of the network selected this way. It is up to the user to ensure that the network is uplinked to some external network via a router - otherwise the murano engine will not be able to communicate with the agents on the deployed VMs. If the Applications being deployed require internet connectivity it is up to the user to ensure that this net provides it, than DNS nameservers are set and accessible etc. Modifying the App UI to prompt user for network ----------------------------------------------- The application package may be designed to ask user about the network they want to use for the VMs deployed by this particular application. This allows to override the default environment's network setting regardless of its value. To do this, application developer has to include a ``network`` field into the Dynamic UI definition of the app. The value returned by this field is a tuple of network_id and a subnet_id. This values may be passed as the input properties for ``io.murano.resources.ExistingNeutronNetwork`` object which may be in its turn passed to an instance of ``io.murano.resources.Instance`` as its network configuration. The UI definition may look like this: .. code-block:: yaml Templates: customJoinNet: - ?: type: io.murano.resources.ExistingNeutronNetwork internalNetworkName: $.instanceConfiguration.network[0] internalSubnetworkName: $.instanceConfiguration.network[1] Application: ?: type: com.example.someApplicationName instance: ?: type: io.murano.resources.LinuxMuranoInstance networks: useEnvironmentNetwork: $.instanceConfiguration.network[0]=null useFlatNetwork: false customNetworks: switch($.instanceConfiguration.network[0], $=null=>list(), $!=null=>$customJoinNet) Forms: - instanceConfiguration: fields: - name: network type: network label: Network description: Select a network to join. 'Auto' corresponds to a default environment's network. required: false murano_networks: translate For more details on the Dynamic UI its controls and templates please refer to its :ref:`specification `. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/doc/source/admin/config-wsgi.rst0000664000175000017500000000515500000000000021233 0ustar00zuulzuul00000000000000Installing Murano API via WSGI ============================== This document is a guide to deploy murano using two WSGI mode uwsgi and mod_wsgi of Apache. Please note that if you intend to use mode uwsgi, you should install ``mode_proxy_uwsgi`` module. For example on deb-base system: .. code-block:: console # sudo apt-get install libapache2-mod-proxy-uwsgi # sudo a2enmod proxy # sudo a2enmod proxy_uwsgi .. end WSGI Application ---------------- The function ``murano.httpd.init_application`` will setup a WSGI application to run behind uwsgi and mod_wsgi Murano API behind uwsgi ----------------------- Create a ``murano-api-uwsgi`` file with content below: .. code-block:: ini [uwsgi] chmod-socket = 666 socket = /var/run/uwsgi/murano-wsgi-api.socket lazy-apps = true add-header = Connection: close buffer-size = 65535 hook-master-start = unix_signal:15 gracefully_kill_them_all thunder-lock = true plugins = python enable-threads = true worker-reload-mercy = 90 exit-on-reload = false die-on-term = true master = true processes = 2 wsgi-file = /murano-wsgi-api .. end Start murano-api: .. code-block:: console # uwsgi --ini /etc/murano/murano-api-uwsgi.ini .. end Murano API behind mod_wsgi -------------------------- Create ``/etc/apache2/murano.conf`` with content below: .. code-block:: ini Listen 8082 WSGIDaemonProcess murano-api processes=1 threads=10 user=%USER% display-name=%{GROUP} %VIRTUALENV% WSGIProcessGroup murano-api WSGIScriptAlias / %MURANO_BIN_DIR%/murano-wsgi-api WSGIApplicationGroup %{GLOBAL} WSGIPassAuthorization On AllowEncodedSlashes On = 2.4> ErrorLogFormat "%{cu}t %M" ErrorLog /var/log/%APACHE_NAME%/murano_api.log CustomLog /var/log/%APACHE_NAME%/murano_api_access.log combined = 2.4> Require all granted Order allow,deny Allow from all .. end Then on deb-based systems copy or symlink the file to ``/etc/apache2/sites-available``. For rpm-based systems the file will go in ``/etc/httpd/conf.d``. Enable the murano site. On deb-based systems: .. code-block:: console # a2ensite murano # systemctl reload apache2.service .. end On rpm-based systems: .. code-block:: console # systemctl reload httpd.service .. end ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/doc/source/admin/configure_cloud_foundry_service_broker.rst0000664000175000017500000001304500000000000027015 0ustar00zuulzuul00000000000000.. _configure_service_broker: ======================================= Murano service broker for Cloud Foundry ======================================= Service broker overview ----------------------- Service broker is a new murano component which implements `Cloud Foundry `_ Service Broker API. This lets users build 'hybrid' infrastructures that are services like databases, message queues, key/value stores, and so on. This services can be uploaded and deployed with murano and made available to Cloud Foundry apps on demand. The result is lowered cost, shorter timetables, and quicker access to required tools — developers can 'self serve' by building any required service, then make it instantly available in Cloud Foundry. Configure service broker ------------------------ Manual installation ~~~~~~~~~~~~~~~~~~~ If you use local murano installation, you can configure and run murano service broker in a few simple steps: #. Change into the murano directory: .. code-block:: console cd ~/murano/murano #. Generate the murano service broker config file. Murano service broker has a common config file for service broker API services. Using tox, generate a sample configuration file: .. code-block:: console tox -e gencfconfig #. Copy the configuration file for further modifications: .. code-block:: console cd ~/murano/murano/etc/murano ln -s murano-cfapi.conf.sample murano-cfapi.conf #. Edit ``murano-cfapi.conf``. Below is an example of the basic settings you may need to configure. .. note:: The example below uses the SQLite database. Edit the **[database]** section to use another database. .. code-block:: ini [DEFAULT] debug = true verbose = true ... [database] backend = sqlalchemy connection = sqlite:///murano_cfapi.sqlite ... [keystone_authtoken] www_authenticate_uri = 'http://%OPENSTACK_HOST_IP%:5000/v3' auth_host = '%OPENSTACK_HOST_IP%' auth_port = 5000 auth_protocol = http admin_tenant_name = %OPENSTACK_ADMIN_TENANT% admin_user = %OPENSTACK_ADMIN_USER% admin_password = %OPENSTACK_ADMIN_PASSWORD% ... [cfapi] tenant = %TENANT_NAME% bind_host = %HOST_IP% bind_port = 8083 auth_url = 'http://%OPENSTACK_HOST_IP%:5000/v3' .. note:: The ``bind_host`` IP should be in the same network as the Cloud Foundry instance. #. Create database tables for murano service broker: .. code-block:: console cd ~/murano/murano tox -e venv -- murano-cfapi-db-manage \ --config-file ./etc/murano/murano-cfapi.conf upgrade #. Launch the murano service broker API in a separate terminal: .. code-block:: console cd ~/murano/murano tox -e venv -- murano-cfapi --config-file ./etc/murano/murano-cfapi.conf .. note:: Run the command in a new terminal as the process will be running in the terminal until you terminate it, therefore, blocking the current terminal. Devstack installation ~~~~~~~~~~~~~~~~~~~~~ It is really easy to enable service broker in your devstack installation. You need simply update your ``local.conf`` with the following: .. code-block:: ini [[local|localrc]] enable_plugin murano https://opendev.org/openstack/murano enable_service murano-cfapi How to use service broker ------------------------- After service broker is configured and started you have nothing to do with service broker from murano side - it is an adapter which is used by Cloud Foundry PaaS. To access and use murano packages through Cloud Foundry, you need to perform following steps: #. Log in to Cloud Foundry instance via ssh. .. code-block:: console ssh -i @ #. Log in to Cloud Foundry itself. .. code-block:: console cf login -a https://api..xip.io -u -p #. Add murano service broker. .. code-block:: console cf create-service-broker http://:8083 #. Enable access to murano packages. .. code-block:: console cf enable-service-access .. warning:: By default, access to all services is prohibited. .. note:: You can use ``service-access`` command to see human-readable list of packages. #. Provision murano service through Cloud Foundry. .. code-block:: console cf create-service 'Apache HTTP Server' default MyApacheInstance -c apache.json .. code-block:: json { "instance": { "flavor": "m1.medium", "?": { "type": "io.murano.resources.LinuxMuranoInstance" }, "keyname": "nstarodubtsev", "assignFloatingIp": "True", "name": "", "availabilityZone": "nova", "image": "1b9ff37e-dff3-4308-be08-9185705dad91" }, "enablePHP": "True" } Known issues ------------ * `Hard to deploy complex apps `_ Useful links ------------ Here is the list of the links for Cloud Foundry documentation which you might need: #. `Cloud Foundry development version launcher `_ #. `How to manage Cloud Foundry service brokers `_ #. `Cloud Foundry CLI docs `_ ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1696417899.7251806 murano-16.0.0/doc/source/admin/deploy_murano/0000775000175000017500000000000000000000000021134 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/doc/source/admin/deploy_murano/configure_ssl.rst0000664000175000017500000000706500000000000024540 0ustar00zuulzuul00000000000000============= Configure SSL ============= Murano components can work with SSL. This section provides information on how to set SSL properly. Configure SSL for Murano API ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ To configure SSL for the Murano API service, modify the ``[ssl]`` section in ``/etc/murano/murano.conf``: .. code-block:: ini [ssl] cert_file = key_file = ca_file = .. list-table:: :widths: 10 25 :header-rows: 1 * - Parameter - Description * - ``cert_file`` - A path to the certificate file the server should use when binding to an SSL-wrapped socket. * - ``key_file`` - A path to the private key file the server should use when binding to an SSL-wrapped socket. * - ``ca_file`` - A path to the CA certificate file the server should use to validate client certificates provided during an SSL handshake. This parameter is ignored if the ``cert_file`` and ``key_file`` parameters are not set. Murano API starts using SSL automatically after you point to the HTTPS protocol instead of HTTP during the registration of the Murano API service in endpoints, modifying the ``publicurl`` argument to start with ``https://``. SSL for Murano API is implemented the same way as in any other OpenStack component. See `ssl python module `_ for details. Configure SSL for RabbitMQ ~~~~~~~~~~~~~~~~~~~~~~~~~~ All murano components communicate with each other using RabbitMQ. By default, all messages in RabbitMQ are not encrypted. You can encrypt this interaction with SSL. Configure each RabbitMQ exchange separately. Murano API <-> RabbitMQ <-> Murano engine ----------------------------------------- Modify the ``[default]`` section in the ``/etc/murano/murano.conf`` file: #. Enable SSL for RabbitMQ: .. code-block:: ini # connect over SSL for RabbitMQ (boolean value) rabbit_use_ssl = true #. Set the ``kombu`` parameters. Specify the paths to the SSL key file and SSL CA certificate in a regular ```` format without quotes or leave them empty to enable self-signed certificates: .. code-block:: ini # SSL version to use (valid only if SSL enabled). valid values # are TLSv1, SSLv23 and SSLv3. SSLv2 may be available on some # distributions (string value) kombu_ssl_version = # SSL key file (valid only if SSL enabled) (string value) kombu_ssl_keyfile = # SSL cert file (valid only if SSL enabled) (string value) kombu_ssl_certfile = # SSL certification authority file (valid only if SSL enabled) # (string value) kombu_ssl_ca_certs = Murano agent -> RabbitMQ ------------------------ To encrypt the communication between the murano agent and RabbitMQ, set ``ssl = True`` in the ``[rabbitmq]`` section of ``/etc/murano/murano.conf``: .. code-block:: ini [rabbitmq] ... ssl = True insecure = False If you want to configure the murano agent differently, you need to change the `default template `_ located in the murano core library. After you finish with the template modification, verify that you zip and re-upload the murano core library. Configure SSL for the Dashboard ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ If you do not plan to use self-signed certificates, no additional configurations are required. Just point your web browser to the URL starting with ``https://``. Otherwise, set the ``MURANO_API_INSECURE`` parameter to ``True`` in ``/etc/openstack-dashboard/local_settings.py``. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/doc/source/admin/deploy_murano/devstack.rst0000664000175000017500000000361700000000000023501 0ustar00zuulzuul00000000000000============================== Integrate murano with DevStack ============================== You can install murano with DevStack. The `murano/devstack`_ directory in the murano repository contains the files necessary to integrate murano with `DevStack`_. To install the development version of an OpenStack environment with murano, proceed with the following steps: #. Download DevStack: .. code-block:: console git clone https://opendev.org/openstack/devstack cd devstack #. Edit ``local.conf`` to enable murano DevStack plug-in: .. code-block:: console > cat local.conf [[local|localrc]] enable_plugin murano https://opendev.org/openstack/murano #. If you want to enable Murano Cloud Foundry Broker API service, add the following line to ``local.conf``: .. code-block:: ini enable_service murano-cfapi #. If you want to use Glare Artifact Repository as a strorage for packages, add the following line to ``local.conf``: .. code-block:: ini enable_service g-glare For more information on how to use Glare Artifact Repository, see :ref:`glare_usage`. #. (Optional) To import murano packages when DevStack is up, define an ordered list of FQDN packages in ``local.conf``. Verify that you list all package dependencies. These packages will be imported from the ``murano-apps`` git repository by default. For example: .. code-block:: ini MURANO_APPS=com.example.apache.Tomcat,com.example.Guacamole To configure the git repository that will be used as the source for the imported packages, configure the ``MURANO_APPS_REPO`` and ``MURANO_APPS_BRANCH`` variables. #. Run DevStack: .. code-block:: console ./stack.sh **Result:** Murano has installed with DevStack. .. Links .. _DevStack: https://docs.openstack.org/devstack/latest/ .. _murano/devstack: https://opendev.org/openstack/murano/src/branch/master/devstack ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/doc/source/admin/deploy_murano/install_manually.rst0000664000175000017500000002545200000000000025246 0ustar00zuulzuul00000000000000.. _install_manually: ======================= Install murano manually ======================= Before you install Murano, verify that you completed the following tasks: #. Install software prerequisites depending on the operating system you use as described in the System prerequisites section. .. TODO (OG): add ref to System prerequisites when it is ready #. Install tox: .. code-block:: console sudo pip install tox #. Install and configure a database. Murano can use various database types on back end. For development purposes, use SQLite. For production installations, consider using MySQL database. .. warning:: Murano supports PostgreSQL as well. Though, use it with caution as it has not been thoroughly tested yet. Before you can use MySQL database, proceed with the following: #. Install MySQL: .. code-block:: console apt-get install mysql-server #. Create an empty database: Replace %MURANO_DB_PASSWORD% with the actual password. For example, 'admin'. .. code-block:: console mysql -u root -p mysql> CREATE DATABASE murano; mysql> GRANT ALL PRIVILEGES ON murano.* TO 'murano'@'localhost' \ IDENTIFIED BY %MURANO_DB_PASSWORD%; mysql> exit; Install the API service and engine ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ #. Create a folder to which all murano components will be stored: .. code-block:: console mkdir ~/murano #. Clone the murano git repository to the management server: .. code-block:: console cd ~/murano git clone https://opendev.org/openstack/murano #. Create the configuration file. Murano has a common configuration file for API and engine services. #. Generate a sample configuration file using tox: .. code-block:: console cd ~/murano/murano tox -e genconfig #. Create a copy of ``murano.conf`` for further modifications: .. code-block:: console cd ~/murano/murano/etc/murano cp murano.conf.sample murano.conf #. Edit the ``murano.conf`` file. An example below contains the basic configuration. .. note:: The example uses MySQL database. If you want to use another database type, edit the ``[database]`` section correspondingly. Replace items in "%" with the actual values. For example, replace %RABBITMQ_SERVER_IP% with 127.0.0.1. So, the complete row with the replaced value will be rabbit_host = 127.0.0.1 .. code-block:: ini [DEFAULT] debug = true verbose = true rabbit_host = %RABBITMQ_SERVER_IP% rabbit_userid = %RABBITMQ_USER% rabbit_password = %RABBITMQ_PASSWORD% rabbit_virtual_host = %RABBITMQ_SERVER_VIRTUAL_HOST% ... [database] connection = mysql+pymysql://murano:%MURANO_DB_PASSWORD%@127.0.0.1/murano ... [keystone] auth_url = 'http://%OPENSTACK_HOST_IP%:5000' ... [keystone_authtoken] www_authenticate_uri = 'http://%OPENSTACK_HOST_IP%:5000' auth_host = '%OPENSTACK_HOST_IP%' auth_port = 5000 auth_protocol = http admin_tenant_name = %OPENSTACK_ADMIN_TENANT% admin_user = %OPENSTACK_ADMIN_USER% admin_password = %OPENSTACK_ADMIN_PASSWORD% ... [murano] url = http://%YOUR_HOST_IP%:8082 [rabbitmq] host = %RABBITMQ_SERVER_IP% login = %RABBITMQ_USER% password = %RABBITMQ_PASSWORD% virtual_host = %RABBITMQ_SERVER_VIRTUAL_HOST% [networking] default_dns = 8.8.8.8 # In case OpenStack neutron has no default # DNS configured [oslo_messaging_notifications] driver = messagingv2 #. Create a virtual environment and install murano prerequisites using **tox**. The virtual environment will be created under the ``tox`` directory. #. Install MySQL driver since it is not a part of the murano requirements: .. code-block:: console tox -e venv -- pip install PyMYSQL #. Create database tables for murano: .. code-block:: console cd ~/murano/murano tox -e venv -- murano-db-manage \ --config-file ./etc/murano/murano.conf upgrade #. Launch the murano API in a separate terminal: .. code-block:: console cd ~/murano/murano tox -e venv -- murano-api --config-file ./etc/murano/murano.conf .. note:: Run the command in a new terminal as the process will be running in the terminal until you terminate it, therefore, blocking the current terminal. #. Leaving the API process running, return to the previous console and import murano core library and other libraries from the `meta` directory: .. code-block:: console cd ~/murano/murano/meta/ for i in */; do pushd ./"$i"; zip -r ../../"${i%/}.zip" *; popd; done cd .. tox -e venv -- murano --os-username %OPENSTACK_ADMIN_USER% \ --os-password %OPENSTACK_ADMIN_PASSWORD% \ --os-auth-url http://%OPENSTACK_HOST_IP%:5000 \ --os-project-name %OPENSTACK_ADMIN_TENANT% \ --murano-url http://%MURANO_IP%:8082 \ package-import --is-public *.zip rm *.zip #. Launch the murano engine in a separate terminal: .. code-block:: console cd ~/murano/murano tox -e venv -- murano-engine --config-file ./etc/murano/murano.conf .. note:: Run the command in a new terminal as the process will be running in the terminal until you terminate it, therefore, blocking the current terminal. Register in keystone ~~~~~~~~~~~~~~~~~~~~ To make the murano API available to all OpenStack users, you need to register the Application Catalog service within the Identity service. #. Add the ``application-catalog`` service to keystone: .. code-block:: console openstack service create --name murano --description \ "Application Catalog for OpenStack" application-catalog #. Provide an endpoint for this service: .. code-block:: console openstack endpoint create --region RegionOne --publicurl 'http://%MURANO_IP%:8082/' \ --adminurl 'http://%MURANO_IP%:8082/' --internalurl 'http://%MURANO_IP%:8082/' \ %MURANO_SERVICE_ID% where ``MURANO-SERVICE-ID`` is the unique service number that can be found in the :command:`openstack service create` output. .. note:: URLs (``--publicurl``, ``--internalurl``, and ``--adminurl`` values) may differ depending on your environment. Install the murano dashboard ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ This section describes how to install and run the murano dashboard. #. Clone the repository with the murano dashboard: .. code-block:: console cd ~/murano git clone https://opendev.org/openstack/murano-dashboard #. Clone the ``horizon`` repository: .. code-block:: console git clone https://opendev.org/openstack/horizon #. Create a virtual environment and install ``muranodashboard`` as an editable module: .. code-block:: console cd horizon tox -e venv -- pip install -e ../murano-dashboard #. Prepare local settings. .. code-block:: console cp openstack_dashboard/local/local_settings.py.example \ openstack_dashboard/local/local_settings.py For more information, check out the official `horizon documentation `_. #. Enable and configure Murano dashboard in the OpenStack Dashboard: * For the Newton (and later) OpenStack installations, copy plug-in file local settings files, and policy files: .. code-block:: console cp ../murano-dashboard/muranodashboard/local/enabled/*.py \ openstack_dashboard/local/enabled/ cp ../murano-dashboard/muranodashboard/local/local_settings.d/*.py \ openstack_dashboard/local/local_settings.d/ cp ../murano-dashboard/muranodashboard/conf/* openstack_dashboard/conf/ * For the OpenStack installations prior to the Newton release, run: .. code-block:: console cp ../murano-dashboard/muranodashboard/local/_50_murano.py \ openstack_dashboard/local/enabled/ Customize local settings of your horizon installation, by editing the ``openstack_dashboard/local/local_settings.py`` file: .. code-block:: python ... ALLOWED_HOSTS = '*' # Provide your OpenStack Lab credentials OPENSTACK_HOST = '%OPENSTACK_HOST_IP%' ... DEBUG_PROPAGATE_EXCEPTIONS = DEBUG Change the default session back end from browser cookies to database to avoid issues with forms during the applications creation: .. code-block:: python DATABASES = { 'default': { 'ENGINE': 'django.db.backends.sqlite3', 'NAME': 'murano-dashboard.sqlite', } } SESSION_ENGINE = 'django.contrib.sessions.backends.db' #. (Optional) If you do not plan to get the murano service from the keystone application catalog, specify where the ``murano-api`` service is running: .. code-block:: python MURANO_API_URL = 'http://%MURANO_IP%:8082' #. (Optional) If you have set up the database as a session back end (this is done by default with murano local_settings file starting with Newton), perform database migration: .. code-block:: console tox -e venv -- python manage.py migrate --noinput Since a separate user is not required for development purpose, you can reply ``no``. #. Run Django server at ``127.0.0.1:8000`` or provide a different ``IP`` and ``PORT`` parameters: .. code-block:: console tox -e venv -- python manage.py runserver .. note:: The development server restarts automatically on every code change. **Result:** The murano dashboard is available at ``http://IP:PORT``. Import murano applications ~~~~~~~~~~~~~~~~~~~~~~~~~~ To fill the application catalog, you need to import applications to your OpenStack environment. You can import applications using the murano dashboard, as well as the command-line client. To import applications using CLI, complete the following tasks: #. Clone the murano apps repository: .. code-block:: console cd ~/murano git clone https://opendev.org/openstack/murano-apps #. Import every package you need from this repository by running the following command: .. code-block:: console cd ~/murano/murano pushd ../murano-apps/Docker/Applications/%APP-NAME%/package zip -r ~/murano/murano/app.zip * popd tox -e venv -- murano --murano-url http://%MURANO_IP%:8082 package-import app.zip **Result:** The applications are imported and available from the application catalog. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/doc/source/admin/deploy_murano/prerequisites.rst0000664000175000017500000001236500000000000024601 0ustar00zuulzuul00000000000000=================== System requirements =================== This section provides basic information about the murano environment system requirements. Additionally, it contains a description of the performance test scenario, which you may use to check if your hardware fits the requirements. To do this, run the test and compare the results with the baseline data provided. Software prerequisites ~~~~~~~~~~~~~~~~~~~~~~ Before you install murano, verify your system meets the following prerequisites. **Supported operating systems:** * Ubuntu Server * RHEL/CentOS * Debian **System packages for Ubuntu:** * gcc * python3-pip * python3-dev * libxml2-dev * libxslt-dev * libffi-dev * libpq-dev * python3-openssl * mysql-client **System packages for CentOS:** * gcc * python3-pip * python3-devel * libxml2-devel * libxslt-devel * libffi-devel * postgresql-devel * pyOpenSSL * mysql Hardware requirements ~~~~~~~~~~~~~~~~~~~~~ We recommend that your system meets the following hardware requirements: +------------+--------------------------------+----------------------+ | Criteria | Minimal | Recommended | +============+================================+======================+ | CPU | 4 core @ 2.4 GHz | 24 core @ 2.67 GHz | +------------+--------------------------------+----------------------+ | RAM | 8 GB | 24 GB or more | +------------+--------------------------------+----------------------+ | HDD | 2 x 500 GB (7200 rpm) | 4 x 500 GB (7200 rpm)| +------------+--------------------------------+----------------------+ | RAID | Software RAID-1 (use mdadm as | Hardware RAID-10 | | | it improves the read | | | | performance almost twice) | | +------------+--------------------------------+----------------------+ Other possible storage configurations: * 1x SSD 500+ GB * 1x HDD (7200 rpm) 500+ GB and 1x SSD 250+ GB (install the system onto the HDD and mount the SSD drive to the directory where the virtual machines images are stored) * 1x HDD (15000 rpm) 500+ GB Testing the performance ~~~~~~~~~~~~~~~~~~~~~~~ We have measured the time required to boot 1 to 5 instances of the Windows operating system simultaneously. You can use this data as the baseline to check if your system is fast enough. .. note:: Use *sysprepped* images for this test to simulate an instance first boot. To reproduce the performance test, proceed with the following steps: #. Prepare a Windows 2012 Standard (with GUI) image in the ``QCOW2`` format. This example uses the ``ws-2012-std.qcow2`` image. #. Verify that there are no KVM processes running on the host: .. code-block:: console ps aux | grep kvm #. Make 5 copies of the Windows image file: .. code-block:: console for i in $(seq 5); do \ cp ws-2012-std.qcow2 ws-2012-std-$i.qcow2; done #. Create the ``start-vm.sh`` script in the directory with the ``.qcow2`` files: .. code-block:: console #!/bin/bash [ -z $1 ] || echo "VM count not provided!"; exit 1 for i in $(seq $1); do echo "Starting VM $i ..." kvm -m 1024 -drive file=ws-2012-std-$i.qcow2,if=virtio -net user -net nic,model=virtio -nographic -usbdevice tablet -vnc :$i & done #. Start ONE instance using the command below (as root) and measure time between the instance launch and the moment when the Server Manager window displays. .. code-block:: console sudo ./start-vm.sh 1 To view the instance desktop, connect with VNC viewer to your host to VNC screen :1 (port 5901). #. Turn off the instance. You may simply kill all KVM processes by running: .. code-block:: console sudo killall kvm #. Start FIVE instances with the command below (as root) and measure time interval between ALL instances launch and the moment when the LAST Server Manager window displays. .. code-block:: console sudo ./start-vm.sh 5 To view VM's desktops, connect with VNC viewer to your host to VNC screens :1 thru :5 (ports 5901-5905). #. Turn off the instances. You may simply kill all KVM processes by running: .. code-block:: console sudo killall kvm Baseline data ------------- The table below provides the baseline data that was received in our test murano environment. +--------------------------+--------------------------+---------------------+ | | Boot ONE instance | Boot FIVE instances | +==========================+==========================+=====================+ | Avg. Time | 3m:40s | 8m | +--------------------------+--------------------------+---------------------+ | Max. Time | 5m | 20m | +--------------------------+--------------------------+---------------------+ **Avg. Time** Refers to the environment with the recommended hardware configuration **Max. Time** Refers to the minimal hardware configuration Host optimizations ------------------ You can improve your default KVM installation performance with the following optimizations up to 30%: * Change the default scheduler from **CFQ** to **Deadline** * Use **ksm** * Use **vhost-net** ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/doc/source/admin/deploy_murano.rst0000664000175000017500000000031400000000000021664 0ustar00zuulzuul00000000000000================ Deploying murano ================ .. toctree:: :maxdepth: 2 deploy_murano/prerequisites deploy_murano/devstack deploy_murano/install_manually deploy_murano/configure_ssl ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1696417899.7291806 murano-16.0.0/doc/source/admin/figures/0000775000175000017500000000000000000000000017723 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/doc/source/admin/figures/add-interface.png0000664000175000017500000012250100000000000023120 0ustar00zuulzuul00000000000000PNG  IHDR4?sBITOtEXtSoftwareShutterc IDATxQli(IUf3=puu{怎.bWJVG FH׸u6KGC@W2ZJ`4HW !4]>Rq13$Ca8q:鎻=$vꫯ뫯? ÑB!^޹s<Å#A1LA!+PC #!`00cB}1ʑϝ;w$X0سOo>} r*!B/h?ŋ.]M"e0`0?-̣ BtR)(pşy籕B!b_|͑l6_xGefk̐l6/'➌B'>(&$ɗ)(J+l6yMl6_mb&!O6La뿾tKb](In 򏫅rf;n.J:6>Bɓ'n~ǏY_aty]DupXt!qW|EMfF]dUs0 H- S/l c_S?] L M4 Ҿkb7ф*MAZ/@nm6Lʢ\BVfC!g/=zҥK.]o~\t]WB@X HAon)2 ]^wJabPat I](Ps{or mS;5Jzop⾽b"nr!p!7ٵr} _*w#V_Fѧ^.W]^]+q[ոF3i36l4ӣUWffB!}tdjΝmo>#l6[y/Ei ]hgLqJ9;Ci}rܒ0ånC{8X l]`QA5(k l[AH02n,H؃(v' .ņ#vZG+Ͷ|!B߿gVbyۿۿpR!/ 9^93F[?|7f9zSBH@*'FeL.V81cr$=\mv"wt:NW ~_So:5 otZ.&x+B}Ο??v zkzg;Yޮ*(@WB&`?q^ EFWꉟ@TV@jH[:éFi ZI4Cb!Џ(3JWx@U gCSN9KQT?01 m>尛JGR%_jI{!Bol=s mo4 I|4yrIwܫHJ(dE0W# 'QzΪ!B?Yh)z'96{W;qAWSPhoF3F*u%W1$0y]:ȝtx4%!)p;($I!BgKg+6XNzk' EfĪ8QU+¡{qLKQVzw+8ѤSՑFPR{tr-r>;CB^m`v(k'@Y=, NmrX<"Sji@vwoT8,Bڵ׮W^0C1ڥzBj>%tvz.(7rM፯b!](b @ؽ36+RЎĭ Y3Z 4ᚱl5 U" ,~%<1]΅D}v~g ج%/x̸ Bws~?igHIMCm 7Z ,l+Rf[Khax7V !Qf"buJNo-L][1pFz)g{xt@m6EH@nx'MTWܬl;]yIsd}B'5776*m2~Xs J)@ 契B-Ӧə;!Bg9d;zGglBFw^p“?<~G@!qݷ/b+ B(c# B(!!BcBxca B (!!BcBaB!1 B!0FA!!BcBaB!1 B!0FA!!Bo .\~z뭷9B!z[.\x"QB!taB/N[!Bg.F1"!: ^B!0FA!!B BaB!QB!0FA!!B BaB!QB!0FA!(!BОwBar~̃gi1!ЙsM P~/G—+BṂO|3|KS (!(g??uw_qG@!ETao`,*Ϙl6Y~|:_o'/_?z?r< B!t@Qlp~cs.% aYuJb9C.܇sg? p>wBa((t v++7nD]4 l6gJm6."=n)m6牕: +\4:} !99y" ,o?22!zc'`0ʗj-t;9yk6io|kb ]63z&05nWɚ +3]]]r0)Ҍ&fV,!(Ɵ[sTRX^v'Rl) yw(47Hɹ iKЫ om|&X QBl|W|q; ^ʥno&糖R /{A m ۙi[f]Q !('SR&WWPmVc26%HY{ߑ}P% YN`iaf%1$2"F!0F9IJsC\&Za"Ibv%[txjFPؿK ^ %Bf p&ƻMz'|!r9yti=av4@!^6ڻTYҶ-ld&K ,Ȃ[[%k\-ZVXmu7=Bg!FQU}׻q.]zwz=A?hH$"wj!)Y֪ߖnHc!tcUU?3^o2.\p9lzβo~_0a!uGVxG.ސ;TxR;(-MLmVfL}fZorٯ-N|?:тj9(_;_g Cw+\pMY(zat|$lv k&JjV槃svMޮo² n'gc{hX>_h*&8.7Ww$3P@W}wб))ofS2n!kW+@/̔ڕ`;-)^s .\xHa:8$(|„w%a`÷_vjjC]X_q*iZ3S+-.n0u3(-QK `)nH"X41ұhnnZI\)R5GsC5+6aGc>GT0F+yq?HQ|^6u](0%ؙPh;1< B}@ږUPk.M؃hЯiM!m j~t6ra9j?ryM4MI IK͡^2.o *PŶr\@OE#]o\BEIsűtye>Ƈ(!=%RH-OU;Jv$m5@77FZ#eU8,pΗ˥h&UzP:i -m=Sf(^Gد-ueTc(!Ð;m hU-6}DN;b6Էv}c- XERqǍ?|9U0hK#WP%?(!tBFtVpp>g=1XW+F}{3[$iT9V@vt-QgBT-0#(w(3L,*ZHaڶxҤ`Xh+4cG{((!rGJR{t u< a`]PZ ~EN0L^Fi$|DxV1Js6>62Ex_7AOH8w8-y?"L T?N'Yoq'Z/nqNzxmv);>I'䂼Gi˝e(Bw&^|j頛7? ߪp3KSG=g>bgy8ȵ g;5j?7JGss6Ηni!Z"K8`%HguZ2rZzʭZ)_Ufwe2'`"K>1ۺ[}:~ve/b-@q+Uغ:e08&4-2Amm݉2|v&+w+kNʽF9t_ #3emoR#!)Ń`Z zjq99x8J-0؍]~ 4Ss)R: }ov:U>[[TâoSlc6ّSN;0Zr"qVȭbr1[P"fHPb,[%LN9[hH$|qwΊ9gjI=) ] @0[HZ0LES!Han2#K Djl#Z F<6`eHBe]j5p8zBl.Yl"AK =`IV cJ}p dJtEwBژ)gtq$omtDLN3Fe%1ĵFrdz@Ҭ7[)I,D6fSy7җdRɞFgq|SLb)<TcLiܗF#" Qܳѝ0ߴF׵4pp|vMN# @sP-XCJsެ'jBhaٳw@ڸ6]y*IC {LCKsknP%QUDXkg*o/g-&@ a`{-N,T|j$:fW9[46_q쬑Eޖ{$wLR;yW2?ͤCav6{4WXԿATX[δ0FyPԵTg&_1JfYk) i)aFټ\Љ;5^/ b$M,%Hf3rERSlu:Sti;{8_(۵i쮅=FWYO,f)raU2le)]PŤ{ORF%51'Э& @ۼ& %b؏P<騹4ީEoe6S⑓dBqAS )b wV>xUK8_0d31=N-L4RHZuDh#qItL`9V =/ @jusn=@+7{5Ĭqz>L*uGkJV;xLJ&!;;b&[ 6e Pl,˚B]Lno 0r6|&]6B@7|]jERt; D9o' IDATm Mݔ c^c-drŚY6MzW!Q^s"5rfǰVqS6GiH,ƥr ( ( o!6ے#7)3ǟjB,[l-JUfWX 08$ +pt';lmGsC'@UUqJ;ca 5YM٥QRW ] ,3K)jS5G9=fײ JX}f  c6!fۿcF. '儅 )'׃f=bVt\y+%74r fg"oH`ƕFQ-VvD<^fdMּZ\fղ eRْ;6ȵlyڔVԫ90sMն^Sz_=;f0#)w9%Y_ Agh -yoYr#7.ϩA)`\>l+|7Ly̤)f1C)uق%t("9yd u %o:d镓)ɛxZ%(F9R b= ԕ[(''f ',6@Oݻ ŲVºw >V~N)|E^ltYP: ]'dǔ6~_s>_7 Y[tpQdt k>a ~R}=廜$ vխѺ*%)S7s1B M۲:|,ɠc_*#U t{q>G,N# r-\|R?]FlV;`P.rIJcZUDOӌ ]ũ'axrzBEbԴ_ h>hz-YP}N {Nޥ:ݬHgL!+wEݽ!K BOhѻr5%B(S Xto%@GVxJ[yr9=D\/ BGL>C2I:ˍB;f=I5 7A_U$I MȍZCUéqU6zOP:Lj|~ÔX9RFigB@+٪buH!čzB k zRY/̆B6`NT©¦$m\?ʅ3P@ٶ 1f]&l6[*}7v/ T<<ɮ#d̀.l]5؅%?0q3(Rvkxfo^`l`$(ӤANglk]9f-[Zrl6p EN_\v<;k*P0~39*6`݌XI0z6өc)>]gli*\}j&8v |O@2e񦣬0>ͣS ^>ͅc!.f6j|2~ld |!Sr?~r{Z ,bsIWzl _^kt:} .'I$I .^F2#|7!pE H HF)!|1Ңb!YyBaB!1 B!0FA!!B.]z 67B?.O}*IR% Í ˿.;BgܥKy l O9Fh4Bٛ$}5Sz=RoljE8_Ry|p9S "Kz'rG7n:֋J#q%,%)WrEGv#Q)VjzVMskzB F)w:xFr$"oYxsw<tUѺT.Dy=d&~#*b1kHGsb[>[a|1FA,8bzTױa)USA7;<k=P1 B;c o:9ƹ9Er#qӟW/O>w y"srs%L5JbAXJ-»T8yޗ*rYHݼVbq;yOl"քy?Q('X_ WNڵBWbӞsNgZ (b)sjSUT:x+1'L`u6~7axN߰`#FmgÛR39 B.y`99U,DhB?{Qd;IG`0x>cw,{4 > ޏNNFv`{1;ZΣ;`0xx'`n>XrS_ >swe]Kw%}G޿ ;_rٽPKP;n<nYG`0xxkZ}8܋NڵBFswwN{v%>XbK}Σ`0b=:`wN> ;[7 _zi%yS__/V3wwK.FBv҃.hV;3v½`sien=|n%o쁻;޿`YVwɹ{;/԰;wg/joMٽw sOj;>~#[A3];c[pIOXܣuݻGwvC5joMwOG}`(כKQ8 7JmezB~n] c)0oK ,E#ws~7bGH'he4Z u(Mu0$e+U"^@1rEIQ*.}fJ e\ǯ&n#eqPntd$R LeqDV:Rsz ͮST[gߏ(mJ{ Sڕ6zX (3qc'Ur<ˌLZf@>JT?rAKg"L^btB ]dﰸJjGHkRP)y KJ(rF۲ sbIV 3H$Ei'J:U: Ҷ%hrXd_ +(*退FJ"> $ ;ym*|Unl2FO+{z*d.e #n;tQr֫&I1_gK\tɽRUU+Pt+IJ" =NI*i7A2 Mʐh?iqϯU#HZ0FAvA~("]Y3R\WeHtA>%wDb\xQpV @̹+mY RzbW5'T+u+}'n\>^籡HS@>cHPqgXJ쎤P:T0)E@3o_RQa ݣgSu|g6" 47zRWj~#2aQް>Oݰ ?wsW"}BOPbbl dE١Oj7ЀzBg,zLru Tr.IR j8zz:ϟjH$zA{6%W)d$㞤RK٠/R鎯 iJ5uTr<quH*@$024 WCk" q_0R4qvR@j'H#f\A H(]g7c#rH7EAY1:Q@noT] ta 4,9v'AU$4P~'6eBM''dƷ[HU@iU6V}'ځ^TzGA޽Ծt2It(WUIκ4m`G?h_O^U0Gư=x_&O(6/cLxY//UMy 1Uz'S).|R \`BPL7\O]SڼIh/j_['صY 4a!;iv GS=޿Љܼ U>d,*@x@x4y=th $EEy~_nll!~ aö@?2.޻x"A!Y1 B!"쏂B?EXc35̣ BcB!QB!1 B!(R^OVB%\ϛLnl>!'Cb z~f 'jfySNk&<6.W&|R@"L^Q.Ci6^9lx^56}z`2MРnoOOǫ=b zCbctvo( LM]`fY@tЭ[gVDl)u_Jb9Sh@KKK +O`蝑lgGNq~"j)/ݹKj z3c#p,27ЭlKmD&V3 ?o8w$אGefra ȭbl62;N^fWK=épv4 n qXF<RZ Grl?Q"D+m/-l\Txyb8B~mn7qX![ ;F6Ԃ)I*F܃BMQhWtI`bǻB(Bz^d( ?~|MZL^?],6j.$S.> QzbS(en3rJ-D;] v2@v;2$I sr9c{*tl.nR3>U3ÐYۻct5_=Q&ܜ!^tڝ< vz1WUT]kڛI櫵R:] XCIЩTjH9bfȣ܊#b$HG` xn z#cgB]1 P 7\Dp}>ݰ UUN~4H+QQX6^Q!|$:efg^ ^Ps騕4|իTqEn2RCܤ~m>Y#3K+y`L=ym6m.E9 b˱)KXvm6KSb>1Z /ٵ0v%u:{te𫁾wCoxr"Dxg.SLh^S,k9yKmJ|`YvqB`5iOջ_fEb6[s/3nM:n<==o}|j_pvO߹+dg_t%pL;x;h1>?,YS l1 JTժJ8Pٰl6ޗ^ʍ|ģiR:9E5~Sk*Ħ=8sZWq+8_S,t3~wt#LSVOEKGk)'N8DKdYHt{U/;9O,q/6Ri#"zՄ8ɽ[1tڰPJ+=nB^igÛR393=R%ssGhei:6X_ WeNxs;yw8/*GŜR~%ycvo=av@Ŝә#߫(oa*Gg3ӐeV1=~lyv2`ঋZ}8܋N_Gwɹy{swqux{§v,{O{_3vZѺ׾xtk?ɣܝNGӑ '> ;[7` }V;Iqªݝa٩vJs> &7ݙOb0 v~Zٽ>su.~x5 iǏݝ2[#ɓXvjikgxԱ X]7i^ڏ)jks/:}65 IDATQ# ;bu]};k7w/[/t ߣF{yG>s`Όݱpog0_:޽;j5ѹC)K<ʛdŭZıIsUK'?wk[[E?GЏd8/J.Vp1$3PLզjpz EOii_7bG~"62U:e9ыʥ[-)q;8,qͺY dؠ1*%Q!iV%AI3I1:Y,U[=SFM;@Xhڧ1vt%:R1uKsĄGKRNڈz؆J]2ٿZ·=)|yc<+=JKio?3|lݜGvcbX}P5w#t\W.V\jnBqPԬ Mfd KmY"]p9V@J+ L-͑jȀm.3n3Y?>DK)]\`EwwefPSbY#1mx7{`IC4 M[Ejk]뻌\,*u03~ޣds o~y `n]/׀X])BT46?6TPSj9 Io vw2FLJ4 +J#ZWdM ²&k%Nq5k,LxQU?5:tؗcaH9VSjƚcIжjQnVJ̴S[TMՌQLffJ^EuYx<?Zz’%skX`0L>Lhwl%uTZA]D%-&1$\31}d.;;"ɣC b$$h ? $i%`V(0f1v@Se 2N0|0A⇃=g%CMɄ}ў'ESD魞&kr i1TR0>U_R eXʊFYjQ&2VܮWaa.kۢ%kH&I=O..HQA,G!)ͦ-} `0G`&͗lXnd |TtMBe9$M:[i ^ Oaeeh@+O ]3,,kRR9EЕܩѹh-VtӉK}>>%ͻm4Af%okJ+e1Ԧ)ViTG\L$mR+'0J"՜R(YߪX+'쮤RE @W~ [(*3:VRu;(ˤ.[s]rTd@2VI4 tӴBoN]&6;r`>O(.! νy~K;!;?[E|AgyCc+4`t@W|b%+۩uh7|]9LӁ>OTH#le!@T X?B3&!I(! v+ɴ?02oLg|UBPKY2q;-GBH@!!<ø;ZU *'6?ܪEĤ{{gQu;( y!Rff  e(a?ɯ2;swӃe 2kjL~ftu=g ^k_k,~ބo[ (~6H3`0zz߶bg`0/? ,P0_FO `0_r<v7s +Q0 `F`0_2t%)|e %Ykg^.[ GoZlj]L-}ڰf!Η.ٚ'˙ ۋ_2n[,-----i_?1=+QEJlQm> Sb$ȣh"mN7ES; t_ZӴyGyo|1`*U99HKP8Ӈ„OUЕL#|@I( mhy]y/yGsq(:hy [{5čh鐗GG3UYGKB+M0񠀸^47DB(%Z[?f|ySYcE۳yy&o[[3r&$"/.hmz1PʲQ:"@͊-㼱d$??)VOU@"b48VrgdY΄wCCQ%[\{BA+3\zƳ|lؒ5!2lՙx]̺=-Of嬜=,EOv̺8zhP3ViJ_<-joEZ~,8Q&,`xHʼn=eYD({xJ8 +gbSœla,uۚQͅ HVΦcUEO=ک (*B$L=RR| @RGv́FthPOSi[S2hh>QvhS㐠\'԰Y>0BxVeyjQ2-YZt,Lgz*-A5st(;/O qilU7l8i'1SIK,UxFs\wj?{;53VZE9տ&v?&_˃ޣWsou?Cu{~7Zy;Os)-^J9@u?-&'_r~`0݄5 7赭<# $diMQ7^Pm>-tAc @0KVP@/Z*N 9(0.HEΤ TgҊgݐˌ ]-99&fHlz)-s͐`v̆)+$ŘtF @Jf]mJfrHM#mX*J+P{sip1$iwBsFiZVH)aYXB+5R~'x6AfzPԦQÃ*( y9)n),:V`QJfEVD63V{XtFY(}+ǖ|K9ooo;l_?{ϤŜgύϥw>{~%W%Ou+vׯJ~0N^\Alǫ f 3fHƘZH0feuylJx\lG\b*U-hMdu9!t%f%Vt,6 H5hJ /=۴9bru5d6nNo-$ӷ(vC*!LP[Nrt%^9CąbE:h!9c͍"gۙڡHuҜ^0ݵWTi&n{CYfnYmFƪ+èbE;5Sr6EJMxb x6Q|߭ӿb~_sW ?߳}p1;_"/!c+m鷺Pc;& tc5jM@wwd UnjF9%|?mY<2h/ ზQ_HNCC 3~!ce XY'QZ09(pvm¸0ģCX=ux(Sc4ItB6sBEHӱcVm[h !~!x)sP0(i~!.ad7>BB ݷ>gItBb3ᨇYg ;zh;/w3T j~\oIR/<͗׍lHR^7)?{_!ǧ^~~} x_R7 ϯ.o{/:vljkc0_<Ʋ]K|gsF/SLsZU><+}ƭysIe~~cSkxNzŝ}LSy{bbjۚ{1ig'|/._w=qBHmʇ y@cc0( 71J.U$ScsSN+hJ4J]=!{~o^/ş9}汍Z_cZ{鞫P;}?ٽWӹMj}7+7/r/.ؐWc0m! >`&;TAߟa#_5<1܁13.}5EY1~k̇Dӷ7?>ēyV^Tݟ`î25 n+=>oq'W;a? s}o4W2;uo||n'񳕼1`0&c=| 5 `0 ^k `0X`0 ``0 k Њ̼㄄Ӌa5}>Qz Gx$6$Gꨪ^ېo5ΖIfJB|ulr&"YZ/Hկ='rZv%)hk s$9vr'=Dzgy}G-:y>\^V8V>ygxgcljG+Mj3f磣,ӄ6-H5 " IIDq4#ZycHycM֪r/  X |P:;/r>ۻvVB7@/Fx^JEzP!RfvolsqH7g ?:oGCZsjyc ωy'%G!EUZm|o()W^ {j3Cy@ͅ:|~j :`r!HϞt)h PNr)9D:4*x`7aJYr#9S9P0M,g'=jp$G4+D^jc9zl>F2^R@|6q 9zLrQHyiɮ3Pē, 53=ʼn=eYDlMm53ںcxCINLE8Fh({xJ8 !߉D23q'bX_;F~,8Q&,`Xj-eƇHdbα$'Jthb^&5?cӢVt meh~[*Eb2HNƊxFf'Pt[CN$cd2sz0:sF̔DMPґ68%˲=)x(/tc>w`3ˋ]:1$Xb(fCmX@@+fJÚB1SЧр i!TZ!]Ad` Vq1~&i8qb g3djIաR(ja ]g ș<=lf`*ID%R$ŘtF6՞ݢpZjPgJe{V;wEĐ@1=4TbA/ep΁>iuٻ]ȱY'ul90Nvd6Cd0>7C g3U߆1wTp0"1С 8Hr^^xE$ش24fDM:o͠LŲF2b$edc6[Ύ7ʰ3~c08ŏ~};9Е\,*u0c5+^YT ,T&k%̀ [mRS 5X>YXQк*tY*5Xx+T3h}HDTU5jA̔6z};jJXs,IQZd7طT0 VW= 8HhZ kM0ӠmaZ1X@V!OB< P=aI`F#8nV˿?!|G;VsQтk2sPJBu)VVTSu2@2g^ϱ ѡ1LV|0ZE3 (ʨhFzMU7#|j!~8x8sVY1Ċ!ДL9{rM=}2GoDSIfDcgÎ ]mNK1&(cA++ei6 HlXkTU &`M/jd224]( E 4FsV f=]Be9t㷪 Vi`KHH0tcbE:{œh\>wL]U eh@+O 5,,krˀ;U0 @2tx>Jzc1#X4r\$"2$e1]vKa-6 D"UDAZ!NSMCK[V6C 孠+ٱج;rvOREArJ2=e<lmө|@:yvkbttI>! BiJ; ewI1T-Z:.Ԁ a9᳓f[-4V`k8m@)xbv1i$ba X':q͌$<:6# Hu$|fl+Ʉ9>ioO$8i{et pcS2$=>Ѱi@1[A¨T' 4Fd\Ȓ: ;1Ѷd|#PH@Pvq;zvNc!4VnpZ؞c=aehU bw_DNR.-kܖƴ/V:56Fk޺1H94ģf A8`_Z ǚR~g*qK!!Mօ,ygc  H+VG $*"Kc0 l1`0{Q0 ``0 5 `0Q0 `F`0 5 `0 ( `0X`0TMr:_,f},b&3KtU޼l.'EΛnǟP"5 Q/ z!y}>lT9*zAzׁD)Q>YMlOzF`0O*=k^JLK;=p*6|NVQlB~ixgcۓ8ƳN~Dɋ/_|sQTCrF`0_B,( B7ivkyo(}+h۳ls\/D)=ktx2oz[m]q-/ܺ yEB0VV^͵bʋ|. |=ƒ+NJZ{ءuv_ Eb{P:+G/Jz=#J,$kF1ۋ|r/]]I|8U1 ~1_/yYVU hƪ"Wkƶ*ifN(d6ٞE/gB"/B钱 BDԇ/]nosi_^^y/> kc/`06y9pfhԯM 7nt9~Ѩvuz1Wߑ6Mtz&F}nt뗏Fޫop|wFt~@+F##n6.:s͔.:'57 kx%)7{pZc˺[j4qsF?}i}]s.gbh48q[j4Oy//cSF}=8~q܀Vo4w;=o h4n 8G.n:O|phaWߑK[Ƀ,{prު&=N7<8ykܸŀ5>Wo;vz[|8{tH__\3,:6W}#Nk.}×7\u{oh<9xn!gKFc,{=-56[Veg}~?|+W7n,/L me:-5#Q "% "+V_X{χgLv&Ɏ~ g3djI^LYH+UI0ot߀y^Fnj/Q籕` Hi3u6*Jdzl\I+3޿&fHlVP]\CP)=-zX *oI\/ep΁>0eM]2uIzx(Jz9)Y<> =`d6wIS2TۀADK)](+a[)<=wTDܶ -*N 9(0.\`Ew%ۅT'vG> ?&?on~j?U_{}΄ )*H?$Md&l ۞-+&(VbviPI&y+ Z>ȏZ9_Tɣ~ȋ ZS[?F$Ujejo[uPFUa X +Bj&砵B[ iPƂf4^U*Ii펝 ;mu02T)e1TRXwhJΑLlЋaʨ$hg 顡4gf[\"tXUP=nzK+5x̽~O~<܃hן4Ŋu:u>&?kݕTJ2OutMcUՠ Mho@Il-T˱PR1[;\zV4-FP" MOEiB%Ts1!eDbG  `e#Qxf^ЊqWU:՟DVr@Wa)a*gtsVH MZ9Q[Bim&W]ժrTd$lPLϧROۀ&cj!VPu}.$ltt2]٦֮~i;q=xf}&,=.߰$?{u%n/w-%!O,7" @ 鳒s;'˟= _zG[_q XBu"(qeti,;xb߫$5G 慃'B+fW s4ma}pQɟH$|+0蚉s@@م1$a0 Ḛ})e: fbXȸhmfzՐɸ%u>wb*ꉍCC<H+'6(HRvY+CmNH!5ds G= -4߶2pl0>ƚP[յ @N `C!i@c r2ƆB~>ƒfkh7F[ni:0 ~t$68vx6ksp۱"n/CW^yeyy??W= 0gJD&ݩJ&FN 9ȻQ|Maװ@`0+ Da_`6v~n Rt\J@τ}3жYˤ =f'uTNHs;3 u鱉Y.(J.(vpPs"VgU 938fi%`ua0Lg_:n#zc/0șVQ`SA5M5UމCECbGSNrPWLhE@M`)̎>ӻcb( /dbI`[7cH%U3 ZR)P M@+Dɘh!VM5ύ Ϭ6zf;2A4`0̧XbG,LfGt L.K Cl^NDS kAsrA!" PeUљmĈiPVn4 ]xJJm a7@) nY İFDzU0䰻GM><:%F8 HVq&::+q}&0HF:I)adIF/#8ܷI=څCCoadF6vf|qk( 2㖅`0̧`0 -Q0 f=z>}$D^LYNۺR+D\/'d>`IqmVkS/Fx$O^ ׃`0x"Wَ*˲8ƳOge=鐦2N˳E``JDz{yZ1)!xQJ7OB"("xsAh('DΗL>#!l.b~/c鰀bA5߼0&K*~ xxhH9}pbBbX8o,91:CUx!!kED"l`'78uh&={>g`aWߑKƵɃ΁KGo4#.zѼz1szޤ:\7:{h4צL7tHkzѸvW8[.w;Fc^)؃-k/W=u{pZ{?q<~˘>{3p\7np<}h4 _^Z^ #>/q|vŐ%V EU/ep΁>Y23b=.i7"bH*Œʈ.⠣62Ɉz弼` Hi3964+hLvyX3yY(fJ:aX3PV&z,)m%ltOUTЕ6sP@>RJ6D5H4u>cH:Pʭ74#"zAWa*i Z!'Dl魃>4MTrLAH+I,^8 sta`0/*@(U4VlGÆQ:bTUtVzAb2Yظi Z -Yx<?Zz’S)ԀZ]0ӠY1 ͼ4U3J`ZI3aVy\1 -FèV1;*OjTj<Е\,*u0ceMĴ #) 5?Nm,!.P6f"xwB\aюU``}BQfDcg7MDUVv)5a]gCk.U `f4{H.HQA,G!)ͦmU")UkFU"jMoIg7ĸNq %-&1$܊[5L*52I9=)yZ(Ю}LU#|j!~8x8s6[x(ViTr.QmVlB5۷*GYjfT@_ILͥg5JӲjq9N4sG9Ol'[|kim BXj1㩳G`0*(' Ƞ±pH+78CAs/P8 Gh (8~L0CMHH}8MЖы@ z,:AieaC8j0/<9%Fʡ!5NH, H i s׶k6LˑtH᱐@y38z61(C̄ vV**<2.Z'P GV+hX4 hIR[ui!=6gT0{q͏>H̽I>跾-rL! !^"b|- Y]=׾5¢Y^^4mqqww[RNIAUgy1`G%r͇zGz1{Fє)k׮V@8_0Qn?7IYC=xj b4-`Sb0   ~z:<`ww7@`s߁ +Q0 `F`0 5 `0 ( `F`0 `0 k;gzӵ΍|OX_rN2J`F\rO$._K7꟡9KWk^0 su/|iSOW]{n|zufRCL7]ʛӦޝʥ^x;=unFĠ߷s{ю7kyWޙ[\21O=ܾ]ݛMq̙wzdFsSӳe.g{\g^i na\陹Չׯ>gI_?ST/]5c/=R[$U<@`]~K:^~e]OboWQ};o`0Xƚū՞KH IDATw'^zA7ޙCO_;o=g{Lu=}]]|}/~gQz_`7=Wս\xs;c6xzi\uK=67ƕwߜKOoOߔ_sWmę)/Z~W0`0 ^fNSeez~NwO3Ы{LнggWF9üggtwVCmnnqǾf˼w !{ӓZA+Г&ڹ)\ummcXqj}羽.ڵoyqNz4gӽf)u=;zb`0GٮHZ5m-o2v5iyFm` {WX/֡{Gj]˵_RՖkGƮ]]7Z7VMH^IhٴAta{,^e]W}֕^wL+6uC}Yזя밳VFa0 5ʧvO^^nc~}n.ˁ;~51>Ye]v= L].X՛-׫e6wwDܻaeY†_J2oVli -//o-]&Sww C6d?`izwt׫7j}Y\/ww/{cycgyҥ|7L{z]mj нOޝkfɽ;޽^+a [\[ҤeFedwT~E'w@b^z}kg/W+o9w`0GL1{f7u v%+]uSwFt<ϚDd<v<ṉ޼KG[4S'(yq_yFmU `+\  !i~`|ut\~}U=,j7V](kVuT kS؍xj 3I8r(((((((2u" 'Gm~e`(mʧ?IEO޶u0rWID4~׾PQ?}?36ŕ7 f'7mË'1mhh-q@js?58~篎uI\cn{x]y7<з=/y((L{fs)Qv ߚ]sPM)١Ww mj_B|4 x1tbZCXl,|brRu =r/zG7oz^n_O%"ʝ<ǻp87y;## eC[GÑ# ^|/OZom:t2G١lv8 ۏQMǦ=G=Sh82|.=zyUUC^ |C*ŚQᓡcNWUCޛCٗnD6+ DT=~t衧l~L-/ȼe߮m>bHi|!W$yP๊TD%Rs+Wi4fL1й(و k5ЈĊ  TF+]*#U垣 sFs$)7zj**Ji(Rv8=gD<ګ k@r'Un3&Qq*ߏ4Й=a"q|`AčOWVS݇u:z #;Ɇ*shᶭ~h'~Оmg;ΐ۫KqT|53 io=Q$m=Ug/Wj2Km( O tK n>I;{>*ﶍ'govty4x";Tn10p9T׿5*=ý`>BFdd@F@Fdddg \.sF|ʕ> uF-[VZO8l6<#)3HqqqYYcu\R)ϟWEQ.\%Kp P\\9d2(p2xsu]{/A@@X@,Y"J1VVV9/%?p.]0!2 2 |DhJ#ARZ@Dzr0:8k< #-֫FR:EȄ;ۃO-:U?q"Azl#j?I0ƻFapOg `6{mW;,ѬOGջ4wSaکsC5{1+*tܡLدFznjwvyW~֫z{zSŃ]=> dzPjTJz搙vte)T6vx'd2f{Ըz܇֜jL8Ju KSkEHG]~U5:q(h8mr5E"ln5:bS"mS7q$gO"Եu?or4yZ$5N$*WDjotzWcca! h1Zq$FdW"I;nI}`81;u}z^5}fG#)lhu탱ysHLb tski5D$f1ֈHO).J&U( >ig(1-[\5stgnKfR""bFfiV #=EYfD$(VE)$kĈU{#iWR~_BDbgRVx0]dGqC"լ+}ʄ!􇦇?k_Vu&uS).LRdy7HMddf2A"͵2#"YbknTzjwv;$Tln1Ӻ*@GRG+fS}]=!V6Y`ܸEXf( Zg͚ FZE ѵDƵe$=[<-JCsv^L|q-ssdP5-;2e"=an9ڬ󘿽}mS;TKddIfZ2s}׹ƹU;SSgl5 p,cI<ir!S]͖`BqB.z$U-M2mbtݒDIia7mmQ0-vFDD<S+KkLԸ š5gLX ._wNo$ƣI[_,m];1u@|' 9\FL "&[q0`kj N&Ki(IK&HD䬑CtmLD<l kgrZ"l?IטWL7ǁx_fkSݤ]PCIX<>EЯ|^4loDJGTcIssnX$q.Dk}kg,'}ulv"TXH-NRWoi`T㭙Xo9g+93N䚖FQZ`ty\g>HǙyfZEr4zR` ,YOuv9`ij@iQn3ڡz|5N8lAFēFDWlPLq&O"nhRLk90GzԔyG"tH-3dI# [L[w$SAVF>GbwXĔ81iaX?Hl2#båyh s5;QfP ft$j`UN$L,ƌ$G:)"L"CSbD<-6;41L3w.1U"b,0!^"qn[DbrXx(RL`RMKk|K`uՆhu7[DD`r5:dxtV {k黹'QM[%FlqhFd5>%4SZj ٜ./gf"$EX*CHK䔄M'̤sNDD$% R4;̬;@Dx$-9.T4Oanl7pE̵M"]a&31tzA$rM~gnyhsX0{;qsN]iw.[@;3)b2"Yt(׈x:-5fx/=wPhMIOG::'&psIODDzϯӜxj0#iNDz2N &HL Fa"qprTɛgy:"rZ7t8دYknwٽ{ǎ;vhus+4gy&ܫND<Lld"4'H(g&Tf^nQYunD(DZ<낹ƬgLNt]<V{t/)(&!`T4bbTEHPL$ ޥz_Z,&3;[z;zܸkNt'g=(ؼm%韹f6LrAg=GFzHdj\m:YW퍦9qSg2`7p"A8=7BHJYo kfXmv -OUo,I.yw k*vy{&IfG*pDI:X~QnIk鼞S黄'p2hvtC$\}dqk-̭f ɃIZ{&pG{Zsu.gj_4 ,e0҉IP>03I8̩Pbgz"yh-I]|n#=ٯx`]ݭŃs~٪*{v^zҥ-2 ^t)2 2 2 ,ڌb0&''Qnerrh43ʊ+&&&PnebbB$sFYre.pB>G|>_]j=9`xGΟ??22Pwy`0]`03JW(@F@Fdd@F@F@Fdd@F@Fddd@F@Fdd@F@F@Fdd@F@FdddX/Qwk=((((((3J W>Occc/n P^^.˲`#s&Ȩ[XCS^bW詇VQ+PN>]VVnݺe˖-_till֭ML{翛+--]X:G#omu|XtYYYee (Dlٲʲt:}^'X\\Fh4fW~~⋵ Xyyyym6?glax0(_-/ٖ-[vh qe/|% 2 G(((or8õy'wUo:t^޻~ʊk{Zo4(O,kޗ=w}Q+Pl}楁 﫽 ږ{v$H=i{=Aٚ˽ȰNnz`˞8q걟j&G7z$(q|ﮎP"Kd0{y٘ؾi;CsãZ&~xdϞ'iTZհg]2pCOfJ}o4dK.爈&/fEo#SDeʷߊ#"*%%'[,% Kp{Lh*5-VTv<[/bea[GE.w鱧U7WF;d;ؿ?%"#Ͻ}G~y(8Lس0mS¿Yq|Ko)ݶ+N5 }ݻh~mZ#MN\ũƚe2'ήQ~@Dyb__>VRb[g ?m^13k2g~ncC]mխ?xWm_ɽ;L=PBDtlO(X|xpz`3TTpc&7|3ykF &Ռ kz[mϧRW6~ٰ@SD*fh;ʫө;7jk{g+rIDATZ&J𭣎\aȍj3&8GkJ׈>֑rvmXK'DDW>9w_҇Kv_j\jzg&&-Kǧo]kvjۏ grlEEi~8KzLD顣bĸ~4p(;gVVo6'~%"uou,q?RX{dF"K7|ŧُI$ #bU2zΆ"*{7Q,!0׎Tt:; R]IqYQavm۾jֿ;9ݵdzao*l}6Dރ/ڵٕ%GvLt{&G?V)Yj ">yџK~4E>6O~'9Q1">Zw()|y;~?Rnؿ6]ۯ^xs>99'AP UUU-]^ZZЇ)߽߽nի.]QQQN pҥK. p K SSS z*D{.2 ,Tccc ccc__9_c9ddjeŋ n6ҥK/^۝Cr8255DC>eb-z`2 ֭KӧOu}\u lx>Jf'蔗_OW}dX1r2;܏~k=((p2JQQ (0-V0XEEE( /LBQ cqTx<QAx뭷P_z-AG)**Zlӧ_u_˖-+$byec,5Lh8q`NMMY&&&.]9rB+W\A) >%Kscaٲe˗/gdFqq ˗._|e J,dɒ@RTTĖ,YR))ltugjjWc  |1eF#hɫoIENDB`././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/doc/source/admin/figures/deploy-log.png0000664000175000017500000015156300000000000022517 0ustar00zuulzuul00000000000000PNG  IHDR9kobKGD pHYs  tIME 4$0 IDATxw|lKo$$t, (bzz}{A* *R")$'[f?BB'|?`̞sf3g ME """""""UBrHCDDDDDDD"""""""R%8*{ee׋0|EEX"{dƽ{ UKGݱc U!""""""J 9L2rv ݎnns`́nc3lm60ll6la0 6"<ߏ9` j޴T,gYaUDDDDDDD*gN"ȵhM|8 li<.fLJLL?Sy!q٤f6caф: uN:Loa %6*JB 80w à,=;%*( P{38oK'"""""gaXVq@a5y92K_bvMǍ4q9e^w|AǺu0ai  33{.]ɓ,]ҭ[9A(]/d޽vn&Xp!?# bƍ,]0 ٳg:˗tٕD>4;z\XL ^DIrgL'sĻ!i2p0w̞81a` O[ǗJFw^ s'5;N5;֐!G>Q3ʿM9 ]{ρ;Xaa`` aN'YYp|tSذPaǚ5k0aaaaDEEyfC=D@@@233#))ٳgI˖-?vӸqc6l1cxG !.'RXXHBB[neܹТEq#7X{FڴÛ_j  GFנxߏY'{X?aC(N޴IAAt7c/C#c[R 9pD6Nԥ*"""""re{Lx2Q`3 N'<-* lK6ʌ]Mz~>XRRX'B|\ޠGʄ {`ԩ,Yŋӵk2330`-Z`ƍ;'gvs-ڵk?~<ӧO/QXXȝwIZX|9&Mb̙h7Eŗ<,꯾C[7cteMa 5<|3Ečx~~³c&㈍#އԲ-w>r5i+tl;t,0z;>ɚ~܏'<E6垞uqLGW*ӿN_ ]>gDR:x%#>2ͻ'x{-p˅5q)w#26kSRp|x^[~x5p8R28l>]nX[b8UVЬY3J:ݺucɒ%,]\p8hѢeѰaCHOOgԭ[ŋ\zyJfp8[6UFZ,&M0i$0srJR`p:1%Ncmq ?Ҳ?—R|ƻ?ߌb+\G9V͆F5)<\a'mo.\`vN{>'"޿?ao܉cKOqQ yL{>LOۏ㦷M,۾zo IB,^y{o}/zƒpϾ,G08Yn^&v8fR(hб?0ٓݎf#&7f`‚1- pq}U:"88EVVVekժU$$$Nvv6|rL4M\.^#ISAq=/= _Q:\<7b#~oy3ؽ'RCHF 6hӾ aǠ.`ӴYk wGek>5@x1kgұ1cԿ9aФs"G#mDԼO6#Ι7Ɍm\{wC\G :p(6ltxP?>כX,8v0wm_600ۍ+ |bccKO42LfÏn< +CÎ>r&}O氷Q8PX^/Îf/w}pvrx uVhI~VR]qx7!zeId V!IyćDU6P!"""""t faW;Vcf͚X|om':x-STT@LLLh GddIV1?alۂ{6.[` @俟Q#}5,iXn᪟AOK"|.seaq9'_ě߀Dz$ 2"Y11.G ":Y]<M'Q<s;i'«i(FLm=j>^͕whcnHa۾IAaU:1^e0wK]عs'ibHOOzݱcGI_x[ DDD}J+T[@ :u1WVL߽xbS{g /u |ٕGHV\џ?"aPKyǗ@Zzyl7TFD4"̄tEo)ǿ=ΈA5[˰DDDDDo 9RJR.) LwQqQxxٝD_kH[i2 XAzanI}(7nal۶L"##1c;w.oa/f߾}@Yx1FbСr2/{F\u0ssh+.+ r1@kNy̟ț1F0=I/Lv],X b &͘ƾO>$_-EW'dcQ҇y҇pW< 9oOpbv>Lx|stT^tMDDDDDސò,^/iySi6-\EnO?icby]^/ǍE@6dcS(¶f)VGto/dذaDDDI\\͛7/\PPs%11[r#խ[7lBRR:aaaӧOڶm[:G^?˛97_QX g|v%o?$^ cG4۷Uﮝ +A7m2)ކ-4ggB}AfL""""""rN?ϟL$5%=)ڵ?~ÝeY'9/|2sÓ+/ _aǃw}>N\,qv񋯏]g QZ57oNzz:.mҿ,\xKZZ 2%-.vrѪU+Zn0 l6 4(Sn]qfnZqT{_\կtn g\m dሊ&}4#K`h[(ڶ?t\gBt!:cs߮1.?]YZ3 "/fdс 0J.wN6Evx؃9]pabef` /!v{e+""""""rj9NIn?9uJCuD*cJǗC!)PvSCר 6twSK!T 9DDDDDDDJP!"""""""UBrHCDDDDDDD"""""""R%(*A!T 9DDDDDDDJP!"""""""UcΜ9*99ta9IeaYizx<n7* SHHCDDDDDDD"""""""R%(*A!T 9DDDDDDDJP!"""""""UBrHCDDDDDDDI!?ύak: u*+7DXE㘢͌{q$mIpEDDDDDDJJmiۦٵ}_yVfmԢ.C%"""""""G稴5>T~~ fmt/ 3f[jE/]MK KYʏata6ȕ.Ih7 ;.0dw15GD}_1ޭq&y2yNENpi4B%x c0ٴAi]ɚМHJH䔰H?cdCv1"""""r,?'YľMl ,&|'3q^!ArV1oqQRVSVtj=f~c!{c s [؏&lwM/5X猞M 2+s刽, 6OÏa˗񿉟+2UJ'_̅k>~Uv nxSA}]&.,8<9{Xp VS^ڝO܏[\N(;$ y-2ǪBNU ՍK|n)sw^IjeTĎ?N m^6?X:6^s Fb4dn"#  pꓲ_F4>L;f)~iޓge7xt}C`Em#?/tu*5,;pɐk/iMkp-W԰N{=k!4hݐ0`궠cSF#b KLjj>ZINd@|ͪe9nG1'u'Bh9ƙ*Z}o# iҺ3=>!@T8bʿؐAnef&1^|#UgJ=s!G99NPd E bqXپ#? w<247<%Qj甼Epˣ,5靨]؎~_K[鯹7*\DDDDrz>;~fŢdf\P~_|/lOK5Gxv8tg!?LsPf=0(`_ꟷUthCӲљ.j]ЋQq7S\s_{9.ǽ;Jpn GxN6J,Gq9|_[ ᮫.{,e;YǘMy?m,;k.;Oškl}Y~J/|v'NgsHX^]mW_f;NcSzڥkr+]F3`ptzOb1}jpQW 1gr5oLhۆo9 ܱY_dք<пbuxz;:9 0 CIɲ, ˲0MχvINNC'ė{dga(O?reϐD^;Inq^+S3?]nGBU]\I ] ~Cգh߮ -%ܓ1웕Er IDAT];_墰sonۛ fށ>|1x~s4wz0~@@eDـL=csU9f*?ދW [wx߭|u۶mۆ s`w#`Β({k64oK'e%dtoҽ\'e\„@XmڷCv9/pnVÈh?|1;%nzu P{?~e&uP3u>}S^RNj HKN w)/L=to) A5釭킃.7oLpѿxWM짺 Vrc{Nͨ]3{~a_," 0{I]Guxz:;:9 4T+`Z{\?f}0>| ASk>eY:}y CFƧ{!*)=$PoLYEf<#~7[&m;kylf6s=20͹Wu| T|GL_ŏ^݃_7ӹo{!/ϼ\4Ye>p4'墐7INu7H,XUtze_ 90 }xŃ˸{/,]{MLq@b $:1 "Pz%:{R@t\K"K:©p8~[eiFpk䳃6\;Rºk 3*ONEDDDN}9y$7=1*x YNNN.z_G3Iڑ “&@]!9[5:p)b'hpu_jsRڞI⩬2iNQދ:;{[3-$AIޒh=n梒a0צ> B}x8}έ}yra;;#lG^fqb"_S2tȽo)SV\ 7G°c2NLm[hdۀͺ_?Xn+=.NU PDDD!RՕ rO3o;Gfq.lDw.uLZrɊEֲ, 4BKxkOpCՈ7""""DD8_^z]NW| G^<4j'+3LxY":qC@Hf5lVLYpuN;e,'$y}57e\8¹Q 9Dx+#><% @£+OoD4=d`J/%)[;)K܀?mGi[)(c1l%w5F{P8},xN^5y:ü9A9sDM|’IqaF OCƤmEBY]~""""'rt00 }9YeaYizx<n7뾘Y}Q>RW] M1YD6]|; .fۄx0 n<ڊ=۶{hܨ |}~pϤtcӜ:񤛅;)F hQ0t5꾵jNL9(Be2 mÏ!^ĽͻR:ƍ YpLӖ\x FڡǴ*9ѐCE rrkSC(w/;-YqSrW[ԅş9Cn+nE![Y猜`K@gvǡY`}7/'K|°%ʼHTH%czv5vjDq!7 eٌl<ϭn/i?ԿsHF.pM\RpuQ4V~g>%4'tq++.\ų d|񘻖2IQ3K[9t|ͻ5č5';z(kY2gm voذ>Qgu(9wl&/cWFxQGl/&fuꦁ*+h.{)~9~e2Ϗ^C+]_y-r}Ӌ>+|-&޾ܬnr[UDDDDrTi~9եqt5'v>xW7nߍasX>l? [sQAraۙ2e6+r0ٔ^MCp~ ۩eg;cf݈fQNEd:Oq?Ƣ;`b[_GwF=-TjDK fvieb`?BYaUR~1G޼M G|Ą_3U׫t`+0g8;}<8#&ɘsYZPuw6ai`1)wCht˯XDDDRKǚCU1ў<8 f⸛}Em|pMcƚglJxC@㔾H+9IOnNVZ靠CDDDD\CDՌT|7}.2sB9IfЈ":+E藒],"g1{&?]?bm}hWdѼ6|5&ziPV BrIѧ996m{F'9^u!\33N˜mj"9"RyΝXMԉCLK-ƸQc2{%8]~#?aJ8Ύ']ӉhT@UJ 9&ox>u/0X0EK5O='U/W~GR{[|FΔ9ry*;3n/[Ƽ5Yh ZЖNEbW!"""""""TEI XZeIx1QvͿ2٬K;Qk=ۿPT,ڜGPBg^)?4)Zf*EKIMhʕ]k&'W-=եS!i],$g"6uM&|Y͉,6""""""ga}^"y+$ހ= Y+6f.䋏g6I6͉NͼCW2MxրlGZNw-RtEÆѐ~d[*Y!Sy~_ ]EX }5M^AR9SUJaofΚBoCQ4\;i,^/) g }r}70;`>u! ?9!S(ܻ"(;R(7v;b=W*F#U<:#5V!oZ M("""""rƪ.El.&`2-bGnZ.HlP_C a6;`ol[TZG{HDg>K,'p@7wХ:I\ `PHsYDDDDDLӝ,ی {˗.e]xV GpT0Ol|U}*|9CՠԦv9ÝӾDo 2{'5yxaVEu*SF2jwcP/g~wӼ&9+r'r TkٜҞA`B;mPf>;pif$u _#( 8DDDDDDFAAea4DNeYXi|>^/Mrr2:tP!"WT 9DDDDDDDJP!"""""""UBrHCDDDDDDD"""""""R%(*A!T 9DDDDDDDJp̙3G """""""g=G.]0 0T"'ɲ,,4M|>^ǃ&99Y$"""""r rru /&Ry-,o.73Jײ|]^HCD*$g_xs1ZC̪_o>__Q"=K15_Slp p3wXE|ßI)huVGO* ɧgŎlLl:nCQPĆϟ՟\ҫw̕)ďWrcпi e蛑|}6}kEn;2;?ځsL˭ͦ0dŤQG@ju|a:#8M>ld#<> _]bZ]ɭ>Df!e Xˋ:mZ?W'~bd,0chJnVzV ۱7ꯩH\?|΄>{j;pΘ:w>-ՈoR=lڲI/bkx{ԿZf~Q]Ue;>vB7qc\ SWvYC2S}Blrc>6j+bJBYď .&PMi}=M{A YR?3k[:W3ԔRw|r6aP 0 &9tMgò Z $peQnMgF 2؈LakJᏭym uY1_ϟI)d^ήl8 $^֜|fYVw3Fv 7iSջ &&~5hS ߝCcw ПQ6r~C Uk>l_0vt7ڛ9_Ȥ0l بY#pi|= %DDô}7[e2cZ^lԨL5=l(FN 9@(uO76`lnj{ @Nm$W62[Рy}v̞?m JuCxy^^>6>([-g<|s9s4m6Ģ*LYmV2E~MAs67K_ٵhw~ Z(%4kR6ˈؔ_>،.!^n{VL[֓(n_=1ZmgŊMFlTw4~ !L3~NvQ~sػekS,!돸)ur`?R7+x\Ѷ.!d~6όLKʞϛw{wNmdTȿf}BjpQ@&335 |eN Op%7N InjIP逜I|j-rM|pzY:w[m3P +Cx QWV\ifPo\~&S3)Q,K]W^F[D/ydꗜ- pw>xXSz1^ w]!ڽErX6g:KOe1׫Oho8o0O69dQ!&{bwuw-`֓ThI(, IDAT_ 4l/ӮcdOO<<.Z;7zv.朄5]#OlnXk4sUF|Y&vtaj'[L"/lP|OKu op'?Ws+tKH7B[ ^UN[_-XZ;sx<2`03Bh^ڟJE>Ryvޞ>0U;Ĵv4.ʷϰ0΄׏x૪}*e[+>NgtNQM|!VQb| SZ}a'?}v I-약%Vq^ұ5-q2}E?KxGJwN$t)Qs3qϠA\Tev:!f.M}RX`QO5dl<, Ykw]ƾ9C@w()U6}l'l̘ n(#_a©eg$MΧ:YfH+P|HAwMxܫf{C|DcEtvpSgdA4NcPeW4g?L"+w HPFM0$y?d\r<od+ǼL޼y[t i,PqMf;_:@mC |5v6)6_rH=A\ɥ0{=@qiaήr8:[+.2Qp-3JC=/2OxWФ0嚸e"Z/B&כ6tXڹfr4G^4 - -usq+hBS5>Z^OZ"׃ډyüh}pABRhC[{o/1Ϸ]]//WizXrEKф>;+>cc&`U@ƵzcStN~hBKہZ\r$kS P!* .spCF@1zsFwK㮲~tm2Vs\D)Z o6Yʵ@Hi_]6=Q[ޞ)pcp[ʮ@NbŀCoWQX&=yndi}QCW,vCE~S7\НI,J4]\Ǧx8x0ԫj?5^UTr#Wۇۺ^R]1z4`LZ$%$ܽJ.4OI1ϳt 6.%+9]=ZȚZڟJ}VXoX]BӹW,9Q(._^#;*Y+R 9elتUЗ(k@=ˤ˿jKOIQR? uaXw00aS`U,VL [RYY6'6iL: z݃>^Ir&򣶳z.Nϖhxp Y[u~"t]ah.1)a)է mV̶H!3gzpa1> [ʾF6.QJ͌h9K(<=o >"J;{H.Bk"|"^!-qKL~-ZX iA/J-ڳ|v.b-E )4LŻ"։YSw6?Oe#25YiБMy2j{ni5jZڟHi8M_^F4ޙ($+r׃ٟ6݇0 tk}y̤'++pmzJ]߰yދL{I:8*)MmZ˾BN.}ClѬ&8!aϊcra&$bo^ IwmCƞcdib굤qc#IJg&Z4.Sj(1n4d.Q$$h"$QS1L.O2W0S(ysWGV~cyG?Sp V1)5iKVUe  hI=GK]G"evoƛ_˙RM@/z1PѼ/536:02g٧Y}4 PuOn?ie%OW\'Ό=ܜB>rg;[}p%|QIn:g&>&oپYf &?J7{2+=Iml_3 gDXrd[4М==p˱0T~4?|F zsq~YTU\Mq_{zMޓ/{DBi+.(퉦bJ71 'AG40f}ơuk2¾;W.w:7=s)?!a%jZ?.ӛVw bPeW~lXew+awETp;1Y̘0g?H۹tli?n,{gq&Нg/;'<aPBN*@mS.3nM:@0 $`A뗸ꅗU@=C>_>eM/?~JwnZK Td-SZ5P[H֏aƗ;  'ѢJ}8JsXS81`ˇ|iVo/Q(OFLfr!V* )(8brJq2KX~1>#ٱikrϠMkźSh=^uMƦREnFDj%JЧ И]}n͖Sx lftLZ^N[:GpI8 XyuS~ ;7߭ݛYk7XƊ$x%~K2VxWyynWDnj,_-xn!.睹 esyac Fj*nFuD(r,te͛ $0f_Y J@Fc>.Ր{IɄG=mkҲ*JO& 5!m&eb{;SS-φt_LE#bC֭e[Wѳ@2% ڴuSYH8Kh3Y_( c kTZZN/]hLVRNSa>;3]&erPkD^>r$??8KX^z =fNE_mlת[_@Dɠ'*j]ғtI_LyKF* H< e1Y(~8_9רƎ{IJ/&#xrYRtq>6/ jcKhFə@Au-~MI+?淳Kq =EĕKlۿ.C,_BcSKPuj-b+_$n呀f]*'Hi/8aVv}xTX&cSjOڶs2ٰx@ a9vToGl9pllxE9n_EmGF=w'2s!,Ii:@׼v/YfI;Y8KFfPRY=G9j+0J*oK+f;~N:W^zt:ӧ[7d/מyf3QfR~?.U0R&67Xp\go9uvU9,Moe iJ/}6^xU:P;1"@S%Quy, bx#,)~~iknx+ Kp9d|%$曰mԈs8kZDf`<[z)wuL0}1F{ԴDi wwaSdWGbLx__uVZٱ1{l)!&]OQLBBq%4Ε}}?f7l>Z~=lpjo>ϗ[g3/Lw?޼6҇F{;gںi' >ՠ #ףu^u}N3/?&}*!8$kT3T8=E}OX, {zM@ʵ21ZeW1Uz9Shiugj<}I`YrB9Qt(S[خq!Ě ך^ШsFp=B`ߠ3MNr<PAܐXcδ t{ڌچqc[{0U9L/ r[ /ut (sF^jw`y(ԡ-W݂l9T\CF;4²9~4ɞ~/uu}(J?x{=/mɃ&+Y-q$5ԫ{B0U֘|bS g:([bl;rKOWꩵg@ųl!i wvd`?ZMy&GI,Ra7ρ2fܥ_u(![`VXU% ks.?}U {)9+o=έ+8ǛH k`Ke?|yS=UDu1͘fL&F z$BBB15)q3Fi¬Pr/Ai"#=‹1 [c΁{/1&dճi|ozqL{61h{Q 4a~=M_wcQJ N4eugYRx]\hoŌX֌y394~Cye^Qt ӛ,X&]Z<gV۫,\~[ G:<ƿ^#]fWmH$^!-q-!*LR$=~?!}]tk}EҔ}V>/a}4{O.n讞Nn'ӤgG4Y`"?b=+ciiSsf?dT@|l¤O.ҙ!B @!MP2w{Ё K|jm#}s] ylBk_Drt&?&ü%/X9oad{5V@DB.6mgmAYٚ0fr{|9KC!BTs>>}rpGq)pdzF"T+={a'g9}(Mk.nqӕg:;E *[aEaoU_~g81'k7&PO]='|K}ϖ 6b [WBqHC*$I!DM?A„ѤD%5!!BTRNB7B!B!D I!B!BB!B!D I!B!BB!B!D޽{DA!B!_+QxEc%e4β)׏>'9ҐQQ"IQXԇ]bytuoQbFv]r*!"AtӉ*9:䝋$N|lwE4h j*ƍW-mU!B)$- /ܮHc"Ϭa^[)g`8MrJ?*CBm7Md ÜP8vmv~kNDL&&5Ӭ[b[igq4GO؞I᫕ 96c'߶j~Zbܸq:#B!ĝPudk>9 SMի͔ЖsQA85{ٗvӟɓG1tXzxd=bp81{{D4@ BrU /Mpddm!}_!Bq5947ByqZ xnxZY~9jȉֿ6a8jsBJ;G:X̘F %.ΖUXQI- i"#xC#ԇ[i1jl<{E!Bq' 4;E&4,@0Kga -ZWBMQo\T^fsشaX˴7Z!B!4IrQkI<ɴQ5fŔFLԱdYgkb(Ո6Fm>̞pL!B!n/@\iֿ/bY[ ^ 1gEwT1[GS9aفA=yTԬ79РE=^"X[kO`ڒsn舛LZTlOX>NdgVʌ<3m4o=C\Vx9 VODZć 15:|ͫ##+O;n=;AML:za_ɹc[YI7 أ#]rT78鯰;ϖF4tbȉ%tw,[Fn_!B!nLfrQKN X1dL+>ho"/֦bjӘoG]#Y,CA:xMQ1xT ߚ/{nIV*Ļks~݌iv|_R:2"\aifR羦479o;6Vؒ멮F)X%1Ι J~m6[w/d/>&6tBǺf""$7V? n{?]]oAfr!B;MfrQK Y|󑩄;<%x4c,=QȨ`VZ1 rs{i$eASܱ.!u6c~qU`6jxa'5XaDZa"}٠½h:o19ɀȪ;V )=Y2J;{WK`<%u W!BBjk brLW&r8)/u^jt%(5Ե0OCJq4EN[-6>PVqDRosGMa|B!BaF+S+f >(#jE^LH4b㠺8r9J ^xh NWfm6_z@\n8o+ Fhv:4޸ߧ FRM<(l]H~Fe?}hyw [xC<63 O뀳n_!B!*FnWP:}<4oc\#[ <ǧiv<:ONc<܊xH2 ZoT%<>N0#@C ,4PjX\MTDJdld4aTq*'p2 M/&K KW$9<W>UܪUn}lstqG~aվ54F ;8gDVG䑚 x+JAtr7ş]PIB!BT$P/ _:&x92PTD;/0&l+ɑ3] 97W݋xgG8#7k f*P;1'$7OFy8,/.NEcoKH{/nDzqty؇vu8ZS9V>UT i~_^ma}|<(N˒,b] Ň2љ׳^5u$oz1Og׀!߲ !BqY ѷoZv[q_CkB|~}ݠ̯[s7n٠qiDvR#8\YU!~ʃuO#e_^՗yaem!BqvB@PH4Efٌdh4RRR`@דT+c$22ĝJfy5I;9Aލ/-ZCYL8y+B!£BjY}Q~rg(8Ÿ65䌆?槛/i/H'R5oB!Pe&UF39BBB$HDm !BqL!B!$!դ59B!ĝ&I!&2C!BKBQMd&B!$9\P-B!ĝ"OW Ut!B!n?@!d/B!DM !B!F$B!B!jIr!B!F$B!B!jIr!B!F$B!B!jIr!B!FPWݦJH}>ܚ ƽ4NUC1·n"-`2jUm G_pDS/{=f'a3(Ժ׀ U%x̪e_B!BH%9 #EX̜Qnfi6ƤS;ؑ{Y!a=|A p egX Ev4r2C[9_1+Ôs߷&b“^20&oq6iM?* +v}3XvCuϤWgNR/B!B!nU3\H0 p퀁W@~/OD$ڹ}7F_!\ŔKlCn=h_3]?bӔzuc;LK mƸӔnqV-QxwIf6Wo\qlLw7Xt%#&oS:cCCj37 #@Y>LD 9Gٗp }r,Y%L@Jk킭 PBFJ>FfY-V{|:\#US ;7[DLJ+%6A[PYJNmaoRqi&˥p"+\_+'Z<1t(!]iYx=g!B!Lcfƚ~}uT!xA2.qoaɗ4Kqwwc[H@kP7l?hs8VO=QM;+EG,*4*Ѹ\E{RY *+SӭIl594ep[vUM7*/veͰM:BX^-;Lğ:Kf"s'.e'_ig8z?6!B!KL#Gilj( _ rt]fVpx*~9F67ziLbNIh#=CP(oOQG@n~(t*/:tǖt8nER68?k4$]x3᧽p){\ғfZ2Nr*ǀ=6aԫp}Ocڟ-0Wn_Tn2'\8{)=U'&<΃#vX`܉9! %!B!w3EaaYPPT#di?'4@[E+ɍ'&[}ݭP.,ڀ)͢Ĩ"f3fɄh^')) B!BT@=8cNd{ (J#B!'I fNTCU(&#m0qXgU!B!wrܮ"B!wRB B!B@B!B!$!B!BAB!B!$!B!B!EQktQ B!w)!B!BAB!B!nv/B!BB!B!D I!B!BB!B!DQ䃑2+qۜ}2~ Z:PEMdn|_9'l[xvr')0e˴V3V"B!j!Dmd."3.M_p_&&B!0 !x.z} _ r4)N kvdh[άf6rv}U|.cct &xwĕ\eS?(<'3q:3pt9 ށ` xVes9,|:L>#/Ϲ*&eu8I>C!8^Cla *]C>aܯYdRPbiÙ}Ǹ`!L($6t=ac ֔ݠ= x$(f'ٲ)m eګqR@Q{ݽ2>[i:ġN$˟Sxf[ rlxelN/o\ dυ3LOףd JG/HI<cWqo6^!_|<$Oe<zk @oxs\lhҥ>[W׺9 'Q/:RG%)*Iȋ_a{v Z[yEidzlV*!Bqፄ@NGc_K|$KG|e)?XL_|u/o d=Iwv?œ]qN]1c䲡uO2v _gymx=Ѕϲ(2A* S|^Sk MrϨk[a=[v´q+"'ٳ}BL'e94M oxByݵuvbيaEө[ 1S^{ޚCg+{(zx#{+ /~"l!<.|גh0]9k]̒|9UB!$97s@UzKE I6me[i_%CIox~; Ko19>bj/v**{_7/CįZƁk݂PoTmw&Ѷ Jh]2eHץi. BDlCj,GA8kwd?)W8ȜO2M@aUlf@aKsElօt-KoE>a}hr회f@2RVqūH<}΢ݨgצWP~6![g}m-_L 3}}pt%#&9UB!$9vc{6Pꕿ5{ɗ]3ǴhCl:}EQl7#-_ҕ. 0̩KW2 pz={.e9Ʈ\g[_Y^!wy]5Ќ/OF jS/ƇU8+MLq0e}h Ѐ[aW.W/t+fRV8/ܲ(`۷obzrB!!f62(^F]@q< 9+[}|13:DKu\Lxg@>?Hl 0Y\î]} Q6'%Ҫ&2$x)n^ks+2 ~pxZ1Qϧ|Gr5g}, ;ȓOЦ,=B!Bd5eTD^#( eysяtϟnK݅a@ǙY`4f@PLe ٠\@Du.d[t96\7̽]a~0܁g LќNvB1W Gel߱gb9j:V-ayi:B!G8 &`P@ 'cNp㈋Oۄ⡋dxv',aF.qZYR0F*jpm:WUA?[evTބ\ll{QNP@c\J ǣȬ9ҍ[3ejVQ,?#)r !B r!2q D/)%eM@k^OKf4<]ZLHV30@*e|)IbX{6`FAt 5ڝw| l'wov^@M*=,3 6q<γZnt{l,ӟadsڏpc\5+B!$!HOw{ ^~\OӇPOSK3fҶ+bAO%nމ^q'v;WL%Gu}qs3ѬgTӁtyN pE:w[ZpTUmD`Ӄ:|!-7W+ҳm |t'sl·|w<a IDAT܏|H>yӇ˷|L;Rf|B!g'?GH?`.OO&2\y9ìA]ߦ;-G=†y# x7Vowc?>O-iS ׎u [(ftvMx~^ܑޏb`VBi9fqoZ:CKoއG YoAoLţf f_qw7xjRǾF t;P |3HE V{??Djwg[?d-F>XȮE)3#Ԉ?yh;X:.J= טE/2J}H>_yk~ĚC(͟y7-IB!" ʑenZ\zai>ѝ^(,cF Via^ڤ.R.9t]GQؠHp[s*چ9oJrXWEeS-y{zWE#ƽ'[)Z *QL\;;s6ֆ1 5STL1v91u#}/_fl2tav; {W(QK^LИZa;}M)d ){Hrk (4gEz?qb/SdF P~<6pix ?-RO?1<#-DФ}4{gEgĦFT1&oLhz@SC &_? ׅh3iu{"JHG9BĕY9v" ?h{{tx-^n"X?ZNeҝ;u.0S^+z<އ%}oB!9T dƻce.M]Jy5/tEQSS18ГpDƣ H}W׀Z?E'? 1.8,P'vZz#I1 o&a6Vsx26J~? n'0i{4{JB!Bٮx+ZB9hj5Qxw!؁cX}E}8bIY9}a+yW1ysiqط}rƙh)W? U4^J!B!Jm8-/o4:VvpH[WVJ.̥Ϟk0k[Vp }x˟̩/b ~OKh5_y'r0nM^UB!BqS4,sӛ7V"躎ȆB!Bq!B!B$!B!B9B!Bq_ B!B! B!B!}AB!B!/ pY^*!B!x`HCLrr2RLTUl !B! >sȓ'4B!"o q' @B!B !$ B!xHC!B! r!B!p}уw`KB!BK6iuM4A]ץȄ(׿VUUUQt:q:\pAM!B!r`0`0Si p`ƭkHQ^B!B{LU_ !'}#k]ׯ B!B׃iBd,}#i~IC!B!cf4!Dz-ɵ%B!Ϙ~J{tEB!BGB n!B!g wg#B!U@!B! r!B! A!B!B$!B!B9B!Bq_ B!B! B!B!}AB!B!/HCx`茑 &ģ߁]xJƍNDm$8Ŝ1ÝHS!B!9Dl:S0ʩ_xo)B!A": G6qZ:i3֤A;.f;2m8qoҍ%]Za?C%7X͜9rَOJ4څEPtQ{3g.Nr&ۮ$=5Ԉ?dySmwZW A/>CV^l]Wjtn5ɣ^`K)FקsvT B!Bd&2]ЦHաPXU^b˾\~$R9[2wʜѳ8avx7{^GXM)g? j,GdH\^} !<4)3w?Cce+DB!B r וlFj! TWmHѮu%B} )Ӝ.-p$ñs#jhM \<ڮ>Sޯ [X̘^; )v Ѹz`#CSp| ֎vbٶ@WPE5YMD<.l:J$tp>u ,B!B!i0{+lQh ]5v\lNB& 4+ׇRP8|d1% r IW޷BRj$_MOZ(O1EJl537~)rhڲ)սrB!B{ȎW( {7|aoB" zc\E\\= e4 -\)Kl[x bF# ⩞)dV^VW.anJ sWWP-2 x}e8oS.x[jܘ&ђ}l?mt:Sv_g(V mK׏bK{uӸqS5kFfh}$g]嗛i9-v #4'pf9s6I<2w=DO\$1s7=Y"[G='2$S{?̠U'@e[.FuϬsH/ѾEs7nk[N6K/y;c)];w߅B!AqWnew|aU#S^o--fpߗb|QW2w2Jx8aFB%oDbJ<6m1Ut腷8$Iq8 J ["%rabvyWfլ;j^Yr_/Lk8v~-ٻz*gNf)G:r*W~̂5mT/iygM8K-\nXZk@Nh,^+uOB!Aoqz~ROiC w0צƾ˄ hџ4{:៟ y*\]%x);}iXY|+vU!;QMcil6 J0qy!l~_r4 3z$c,d#_4݇ݭ(%v/j |jEs"ցghFkDpa/Od[޵A#kv3^r~9-gZ,t3u6]fӿqKFHV9s653ez+K}c9}%+OdždjKr=ʡc;IǶ-i١??NfSO3I~| -ZӰMĹ#5ciٖO={K G'oۿڷ}g{K.Fuyog|лC^~?Yw4b6}mq|ڍl'2v`O:w@zsFɻys_>~,c{1ǃ$fˬ.Hˉ?Өmxefc]ܵ+='eoyG{nޙSC{z[W3cX-BqOM-.dpDspx0SKZt5L1<ĐfՌ޽1+c`vRxҿP} N&}KO3(]l#>~R= _C?:vڊ3(̌Q'5uJm DN_Cރ94nzvBKjb4fnyRaN\[okRC(K&6\D&1+ ^zoS[_M `BZ5NHc/0 (ǥ8VVՃP1Qzwq3Vv%**5M݈Ƙ:-ᧂ\zy/TrG X1o1 ňl?q-!/s#[c>uk@~W:Tƅx9/w9?!B.Y!qn<+]'0cALəӫ2ͻt$0d'&,ᱷGn?+]OR7"5)T-4s~ʩ_\ʗk35jpizUm~3f;gXqmu1ӬZCѾ T֮KДl@h́l,ҕ_Zxk$\^T,lH1bFK%1~+lIq&딽jsToR X5k$%8uPBѭ} rJw)y; q)Ә$"Mh5^g)'[ŊnԊy{:2B!$!A2O2z??&=5|w8ĸE9 !kdyT ~lhA-4{(U *KS035~CwO0SQ#n /u(q1Ǯ~!~иzifWHA(Ҡ7}1-c=u1U4p}1*WQoB +gIO^)7t%d?Ț勪ɩ|t|ؘa3Pݕ#ݛ&q.cYOn4HϪd"Aj`%1]^o jXj2;Tw̼@AUt\ij)$ٳ^Bq/#x910n-O9g'ףC,ն~yg8_o¡;>gp% G5BHH!!ACQ&X+Wr">oJ'c`"wz&gԜ遢&lj|)Rֿ2kC^/_WM<]49k@'x}(VmLRq|")q7>j:.ͤXUV|s|V5bt'{ףq~~]u;'Jdq~+k/^QZ4~vwF7F۩P̄/CEyYmfm==F?Yr|2:W@*'B{8yqӴ6;3q~ZcO0#_7hQKx iq9]o1V˰wX0ۍ!U/-+Ws,A۬1os>IQ|P(UɅYz|[RS{? y| :ee.B陶zRJ4L@n9J\ =ŁgWZ~h$O LRd K4ϫBrU'_nP*@<+pS1}i>~%NvB z Pպާm#)b+8HIqha6QD&?[:: mϓ{sDpfeip t̀RP:5:O9.NN崜8Qo>^[m3GiRcNh<eGW}?'[^+:eqc1HWvKխ_",Y% CBx ޞlwGxXKA a;wjFCS!B%lp q>gA>%S`ػ_AY5KU,kaEϺwV6_Ms,+cFj_=> 1FhI܂UƢ](kGc)ίxKL.e_ڪX2vw !Ӱi"(7u>jJyj=o1=^'&znAeY'b:$oh{,c^shf*H,?>o6fwô*(`%;p U`ouc0p|JZf^._;4Ch}ٷbz# ޤ, ӠPJml}U%x/o_B]mSh,&/Ó_cjP3 }Zlk7`鉧~Ԟ!BHCq߃{+LTq2QP 7E1v "M*61c-Ë@A:sfCKKa[!&qU5C()YVtunY{L!h+VZBx/Ӷ>ljH}#YS_F%to ZZh1gf3^ʴ#=gv?!BB<a 6ڨ夼VЭ蠅%a@J 5a %Wpt@x%:T(ݓox]laLԋQ?,jW /4kwF7pA;!ӶNVW)CmϢTОB!x`J!dU úcf5n-u *9]\(xW9͈^Y-1DcIiw{yJ37y(+8bjX-0 Zw4-'`yG9i,)՟I=64I(R;~pzw\HpN+{^m׮FW=06a7 iG7a8$_;GJ}/? v!Ӷ;f"BA )z|LwO<YΚ0O}sC(?l8kA|{vl`4I_=d9YߠPgRR85.x뗣r!Ojq〗0<6WYod>r>kh̍>]yp Cڄ~?\>xcP>|8{UDFc0UDdO[_QkF?=Ϡ}6y#W {~-uG^the=b>w>_RGBC)(a \DWUjD&f^c~ _~t[aت'EwkDZˈi4bkPR_s)] נ 8zk?ng8&@oH^jÜx0w8Fo<G}e SGbzd4&sKߌ1kLʲ:-3ϵ8F4G 柀+RA=C{gև|Jy4~aZB!}EINNE6ut]G4\.NÁnԮ];˰k.ʕ+'!B!xȞB!B!/*K+0auV7L{:8eKLE!B!FNp G:B!BqwܹG~QLi^N&|U i{IZ|+/uGrx_G'X[Zf|©ۥ/*bp|z=e5S,?[>.zATꟙvcyr bPT^?ͦK9\-~?3'/ϋ Uxf)qn%ሳ=Ly 6yGod9I>/\ +3`ڼ|9k/dC~!B!g!*Ł$8|΅_D>ħ>x[YI[1/_7cEqf3s]u61^ޝ%bgbH 2jo/k]($~OhG So!a;Z'ݱ_#1UzS}"WAGHLxΑMQ`C? Q3.x =hs<`_w F}?>xux=$־}]go6fws^M`,ߡ6xG uc#s[{q̟Q+F'ʚչ#Վav8Zx,Y*_izW񿹤̙C@UhJ 25~_NPW޷8$8nPWE_8+ѱ^W1mqlN ʑ(^:ZR=9Umc8[Pyhp8x"J 8޺ pzqKIGy Vո<)Z'q:k3u*\B03({'`(/@h{YQ891tP__|- %فc[{hOnc6l>դ#&x]O2^?.u&͆:w qu)' ve[ÞD*9z{y|Rƾ󙁸gx,/x+:1Se'2/ju >!t-Ü[rM\esP҆Z>\CQ,D)/;.x*kFkWb3fjwNNjybYw{NL[aڧ a (yu8{#?v`4bk8Jʏ]*UקG5(Bwp[nH  ~)X5O>V<F1IRY/Uվ-Z.7S_,^]ET@ƻE'3K OҞ8xϠcC]{evVHCqd5>iZ|LvZ:Vh1^q31>aIp> WBP;86ÌdMIX'k%BTqBH!!bMZTRwE.b~6gLs]ss]sQãg:)O'oQc:l\Jȇyۈ9 5~E8^ʅ][h[>c8f7k@aݖIpuIUFh},@e[w/,xSq VM*X8%u?Ém5Ga VI|z)ƿgޮ0;ż/&q:֯p.<9.^jQhazI\TXn|<_(8 oWx)hyPofNX.Ԩ czP:=wQޯ I($83к+=L3k璭:}n߫~);i;Me&f]GK8n.HVXşN 'S [ ⱚUİ99?y%twlN`PW,\ zl߉88(""*rHI8~c'4MSG^׽}<  &c{bxuwRq|Yп<_q>3O>xqo{q|æcx4.6}2ZHB.0: ^ԇctǦcPMس0+ձ݊gCΛ_ǼUT&8n7;AL' W(^" 'hV[ƙ]s.Xe8"/WR)ywCX;q\;8q71ycAeU,syV ,1e% q=5VGp63kF õk~.@DNh#s1m'AUx%X a> K!)'kF| 21OAN6Ӑ5Cj81?OʁxgLC/\/$sΫFk 'UpTgRC/H̋Ğy4|WWm]{h˻c^!XԳ*z`V?m5[*coo oŽ0eaˋ3`>Wmc6g.i#|6ߌf4;!RlKpD6. muIDATv,f '{.Ѽ׺ߜxPOxPRSawrWw`! \ڔqWpF:'脊6:xޏ'Y<=z~ ߵ`Z)ڴI<X^XneXa`2O0, ˲0M`0H ؽ{7-[,6[ƍ+'o`,ClŘ{MuDDDDDO+9DDN#q._s= \UM1!"r윋 2`3OE9enu %ky)"""""'~]EDDDDDDD"""""""|.]!"""""""QAE *r, 6/`t=-˺̸%=۹Õt}?ӿ&~GawV4.M6iӆUAD_̃+>^ڂl哣Hִ4H|U{|ejIvmIKk}<֎5PҮd;{ ?ʌ`ЊCXdz}]W3- џ7vPBOTDDDDE oϳD|Ƒτn–K3sP{t2r- Hޞ,{>$, _S>>`ż4 ӖHW.18/ҝd6#Geۈ`']m ~/s2lIY k?<|ldUZ)q?Iѓ dzH_WRR"!"L7uHp8I>2Z$ʖ~lgcn܀S,XH`|$VnܺC>qwzxпHbL;-*$2-]ɈKHY9*;'Ϣݠpϫ¢p"2tuϭ9ZKF]ݛ2w@!#/ "|ANV&dOse~gK [Cc\^3^1eC~vcdכx?fܕӾ x `Оvڑ1l5tѾ :̾#q_^җWv?ncˮ-v"㛌׍u71?09Inى+^]3`L~]ړѫC^pJ1_w28^VCXTrly/=)k&ImyIHDDDT9ŜN^8+ tG|QX5<~Dvb=M)gC8Nsf0vߩf>9y ȸ=wsB+349mT?W9⩔B(U;pr 9 ^|ͼl;7k6o;ks|F7'(tx1w|;i KBwޖwXkQY5#gϖ*<І_dֳȝ;&4bIWMiK❱.Do⥥"Cf,+sx8 ƃgtیj_o%uf64ϟϼnҷk۽];X#__Ȣ)7yd`9j }ߜ}7á0Uw ر's$; o`ԈCX]ݤ֯y.U+gr}mc%ϠHDDDTV2Ky`ZU4|\q8_%P'I gn#ZӪ,]¬{#xsOl_0/[Ac ֑Igs9qj''`'*G'AL_5i $pԬݪ7-JvItrSZWg?/ k4e5qc{f:kleyd A}> ѥQ`W5T*T5_vptwƜ_r'`-X??-٘)p8+jVK_LuҤ! ⷉK`z)lUUvQ|$$sh~ fp^ n&7!UeK }KaӪ7_^TL4~;FKzn|Zv # +"c~nR 7T( ׌]\͉ ;!̚ lՇ0}׮W0O"\tlmGi]ʔfg)ҨaPmF#Gb]:&^bAA^qF9b#JqEPT ]?qlƦXx3xܜB,*-?bOnd}!b\8.<.X`k!+iX'6Hb$qɰ>g?vwc;2"(IE78 0-0KK!D2\$z.J= ;?lq k[EDJH{`7,1IR۴ˑ19D${J"xڐ#+%z[xi_,uVmo 9낆$9Bڠit>;/e+oB؊1eڟ6ˣ%._ǝ&C^A7 f iJq$ ی4GƺԼYəՉP4~  I-]ƏONDm )y7ۗ\~wT)д;gqdu 陵î (HnJ{'׿14V,.x|[((4*&0uY5ꓘ }l68'eRU'6-vp,f9FDDD/*ק-GMd{gGtW<ϾկnAk:73.kܚ-??;ICRRRRHID(lY+u?WEVbX2wň/+xpypDͲ(XUSB#'ޚ0g|@gL er|]"Z'n`]oU+ۀ.mLN{;ҦV +\;m.Cl۝a|"}CJLp s޲J3ndٺV![_ȦRҀfyn ίӐS7{jŇ52=ys$""rqY9 ˲0 cͿ37fǷh?rgR\qS}:1N%Ts0S.p.\ 3~m4p-|LL|^?ֆn\/q{҈vW$3jHO^68P>e?8pݗz>*M;terE]c/;'WbGAzWsCSbKu E*&,v9%,029," |~C˖-˽ ֭qJ+Mӹ^Ƀ7/!}8TE""""µfEADDNf.Ι e%8DDDD+--MQ9,4MoWrx^vڥlkw8vcԸTE""""QzZC+r\xݮ""""""*]+"""""""QAE *rHTPCDDDDDDD"""""""T"D9DDDDDDD$*!"""""""QAE *rHTPCDDDDDDD"""""""T"D9DDDDDDD$*!"""""""QAE *rHTPCDDDDDDD"""""""T"D9DDDDDDD$*!"""""""QAE(9!eQ DDDDD䔣"HS999۷OWt)ťDx4i֭[ټy"""""" 9DA RtD9DDDDDDD$*!"""""""QAE *rHTPCDDDDDDD"""""""T"D9DDDDDDD$*!"""""""QAE *rHTPCDDDDDDD"""""""T"D9DDDDDDD$*!"""""""QAE *rHTPCDDDDDDD"""""""T"D9DDDDDDD$*!"""""""QAE *rHT?A~~̉qIENDB`././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/doc/source/admin/figures/network-topology-1.png0000664000175000017500000011646300000000000024145 0ustar00zuulzuul00000000000000PNG  IHDRdSr-sBITOtEXtSoftwareShutterc IDATx{\Tu? 02Ƞ\TA.-tB3-ZV ]`n*&P)N\Aaaa\3M*uχȜ9s>܎B!GHU@!B(,B! B!7q8iqV'k6_j_lZؖn̉b&B>|-s:tp,l-mΎp:{$I#8sL3roJ?k#f,N!r;-[^9,q"@ rN'P+*~L-nðXWk !  8"H$ @,bX"H(@( @ BVH= 8P~"6q^>/ 4B!?Za"}Cԣs 8\}ipqӶ-T/EB!{XN[gX h|S N"ps A :;  dj--[0J%M$"B 8@a]Z'Yt2bkAӧ{=1z{L.tk6sLoow}Y_ɴ{fHO8z\]][KsvF://| @%K?/us:lሥYZ{v尘^^#׹|ߔNpYGlWJ%لB&fq K`rT* k$ƌj2xyy}9Nal6[gg'.3.)Jv;aBO˟\ںi3+0HJ{EslW]q[دBixdMy'{]/Q3+G»kJ1} [ڭ  0WΞ&rh4+V"56[g>o[TZ?;➟}0Ζ4>lxͳ*xdk*ȟnz4B"O=98szDsspN~v4F+ <==VTQk/YqKbq&zXe+mZ}bX6gڴA KF0Rv"~7@ Ipplw={^ԠɗϾ3Qhij%gg#%_\2nv?ȉrIK4%fdһP7N8G.X+bc׷X~}wc36QR$Bnȁ3YYis8/:v@p.#ok7+>8]maM}ZQޔ7}be iH?!ٸl {0pDžGHƆ^Ō{@y&kb5tJ0u_]`-6bc%zyIC;1`!L# ?y䳛Õc3\3iB!iXxPRbtсuv''zOӥ{AʂV+%n+9FΟ՜cB5]'n~' s&q a>uwv벴${꯯n۫1Y_nڴn{`r > d>nB!{X/^u/5v8jC_|ᴃ3uXO44wZY3jT>}O&ITIw}Ǐ7LÆ STMvK,p8f=W- vZ,l]@"J!8Z]w =":sa@K?{`ij7gd|eqAp:Y;^rZ|׭RLfA7trܣOMx*%GϦo]6^xxF#0A%!)f駁B!}ZZ3qvs:e {:9XPpŎp8t:Y+'k8g+/h5z0F_yJeKKK``Oď, fc fDϻg;.^24~oQ1qѰ z=0dxIP0$RIP0aB]el6לidOS&@dN`f 6V%JC" :,c;#X-_Ǎx~]%B!Ct1 Bnkk B^&Sf\fνbwDZVN"!Bw%F>>>4V=WC`9] pF!rE~~w>zqVtowN' >>YrL쓁)fu"BŞW%h) 7.za# H, #E#Ւ !Bn(? #r@$xz $ << : !B~aB!x !BEB!BaB!PX$B! !BܹsT B!}DnuPE@ѣ**!"*7zz3U!r3Er3=-U!BamL@!PX$4BH!B(,B! B!"!BH!BEB!BaB!PX$B! !Bxh ͌>Co#T>7szҷC:$F$wB!ajX7gH<1| UwL܅87QK&(,ތD&Rz[;.s!iXⰟ/*\ziiIEZc j ݜw>cdwˢ8r#J8a\7j`䄠%Dk[{1fgk)|L7L̜߇K>=ұޡo0攕*BM1Bw넉sN16OAI"6\ ӽLA~6"Bn߫e; [m;r rp9qc;)hw'SaٜN']B!d[ۊ MDNERXQvByW{DLtanV\}1+/nkN\2ݨhRo7!rSŮ .;LVJ]QO%ε;-_öcFF: P T3tanz7nxR`'B!F,:.wj'D*]ӥUfg+|}㉖jy-IRv0b͜?iuz !C|M89Hl[?#/Μ{o |``=v˰]Ǟ/q Qȶ[-yU6N ٳ/Lf͋ !ry>X*R¶b_WOƗ/Bȍ ȇxL-,!rφDΉ>Ct*B!Vs[_{B!$* B! !BEB!BaB!PX$Z~q=כ?}Hg#'?kl_{nwS0}1=={W-lveag4vt fݦn|%ocbFyBn[bG9? /d~NJ.~_3C%?|gy ZVFn9nCZ{ۇ3/1U9S>ЏMx e4,"u`o}"5#y~2!"!'˦]qu&>ţWxֿP۔@5ˀe\*͋_h|?'Rof_e0\@u 23i~fsu ^XܘA&EXEߕ6zús. 2)CPݛϼy {l}:BXn$)H%ݿ/P_Vu>ȧ,JO_0Ia= CO {uq3۰lk\5)܋[HP@fxX3|lUe7 kKxq$ta3G$0ݴQⳓ;tsdĊϿ+æ+}ŌsRtǤ{kS!V¯:3IQ;\oՑ1']GIttÓV'!DLj 0`?,>eu6~̲|낖m/l>ijwjӏ^۸yͶ陙wɕs^7)6cFq-JX>~ʀX, ]} .:\}%( Pu:7eǚ US־>x-l>i}_dqglYzfߤ8S[^yE:ͺ]Jx6\:Y+V?׏߭5U5[Sy\-?gPnq/cFꊕ~Ȧҋ8([#U{w}ѳtt;U\9G|t{';`+;9+wkV%;1&FspM#))B(,r0~{ fPN9es{LQSPquH@sRRY4:ǍA/m}o |dƧ?زc=be~]:7*#u,m{o `)dqEH{r&?)Sxb;lm~3b7)p"!fFЄٔB-~ }S=a-vKX0+cV j|sfZ h fWfV]&*frböA0`x)Sck $0ˇ?g􇫨xZ~׽A} #I5NZ)H,l:XxKcg-Rl *{G IDATP0,m򞬖@$>:<,tp hA6oX=3w]mUV#3.t>jS [`3[|ށ3\W&&J:3v^*[L_xkdMGL:ꎖݛ{GƨLAw>BL|p4B@5wگ7<`S>Cݩv݆X0AeI]摻[X&e./._#?{]'u19Ij檴GםP\唹a=]f{ tA@L5, j!{#?=մI̥S.;5fIVu cUI ņ7fw%$%\sg`QџW.ш?*LR Sw15?N]9Ԅ l=y ]Gpj޽9hľaSc3 ^S6&~q$~ɋ>ؔ]@.^U]g,~)W;(gxcOޱwoȼMa11L~z eEBMq\|zrs<̺M޹r]t\^.f$I'[bŅ!BЄ LEB~u0WaƎzŬXJMp!d`ƃgz(5+{|lqf{e^z-CQ74%ݸnhB!Ǎ !BEB!BaB!PX$B! !BEB!Bakw %B(, =BhQn=%oH͖Gp86t!Bq/ E5Dqc|t/^}/.8HyhGR;\ XpG{=sg> [6azk`=R|zNl(+MǞ|ڏ?JGʿɞMS¾# /]7tgƇr\dԗE"H$aB!IѰr7x_1]2ON*g!LRNTrH_^?v*{P1V`sb^\ESawKG \ѣvX; ]YAR0^J\{޹V*Gm_EB!PXءoOfozG/WBM~r]^|K_\]+)Hb[KkQ3Ohohq^#ߠ*h1C,dvc.HeсCɵnm-"!rۻ'/6pG]g4%-c=:jE"9ǫ;]G=Õb5`V-ƎVo' o >/~j'B!s,:Lv@$@1=±OngoHת뢇sІb7 cB.oۅ#'_yqLi' Bśdl`T^"--6@lTJߑLSi h$Oax$$LvoQ~|]ݵ^2|gN$*H!PXي/ Vw.ϾۓѓK Uj9{f˝Jnjj*Cm&U / $Rih ?9rMS 6Ir 1#W+k*qN/=ns%Iҏ!Ba+AkˏL1(c|E}΋i__^q?#'MއD_ b\ziSZY=Nf;u)`d)R~a[))B!o(|˾zqOOj3i_.>kWѿ2yi2M"g1_r@;330jwܳ&zoًu/QR$B~ .{*AIBHH-Np1U!BaB!HZ'B!"!BH!B(,B! B!"!BH!B(,B! B!"!BH!BEBU*zԩSFRu\2uԩSTPB~T=GbV_6D+e,^ %'٫+mbBYP"MX7&悔SNM)1 rEr0ؒw ;G]LHbSVZk.MϸBȵGv|P%#F|@uO!6G-v*bVߨ b%|4W*j Up6vޒ =gcXuˊ+WϞ ǧ/%U}zĭg6_gռ6L@жxE\Z3o_YuXZKHNh@<槨k em)4`fm;A{v>sZP۴'MD6)/8R5pU&<:1)9!:o=(@h`bTiI\QWxDWu4Z016;`O~aY׭W#|tw旺̨4YK#hؓZkWKWW_8\2'0r Q"XY$, ֭+j{)H[XwVWf줬!5XJ=Tjek7ՃsRu=f1T۸z~|f(5ِ?}=o׽:'qUAM?E06/lݾ2F}پgf7 PhSbڼ ROEgf-儐ZiҬ<زuJ7㳹(#%&.9i6$@Ű=@mT(f{/7WlH]SФ^ȫ }-P[7'(Gŵ]-0/XPimI#Yk \[2/6"@*J˘m{2bNXsc) @6|$+u] Ѧ̉^fOZW%'Ej+ea)۸ltﶤj We[ ,L𬤤j(ݓW\leٚ^(5^ȟQa1T,]{Ү>Zaي<:R"BTrX yePػ!;,-PG%$jT0ߙ[ZeYY P'ǩCaNʊ؅Zʊ iҬeFXJmi \JyvdDw}O&%ZcaYRRX >Qs5*Z)F+0"tlE߳OZS\*6JnڐJwtG )ya_+恻SUŞBjV涜!Lٹl4͒m<¬voL:!B6?۰hW -,,XT J4tkc]yB9xyNJ%Eނʾg*RRDCAƲ-zP'lږq҇LvwiVzˀpe3rc}z-}ƕ& JjOuy7ĮJG4}^ޙoken>QUrV]D0k9-ۏM/#IZX]{j"U: jU3LD\Yhq!+i-'/J3rYke]oWr]|}AE?Oq)5D%bY;5 ]}䨀H͊#ZHlPQˊȣiTD/MR,%{ ʊ$ZBa'>3vy5WI2>L0!/+X"<,5zs,X PĆ35v/&A!k5M1PѤ!ѮimGhW-fOZ>FrgN|>J;Kp הf 4W]C+u0LEHڝjewh(:bWvf3Vw}bS˖J]YQBaAbbrIF%j.\MWl鲘W5`X飍 BwG{&VB3] ֚~{yLQwf(NXPۼ~_Xp&$`LJ OC߉ܪ`uu 8f꺼 B]*+ F 3wT1]r_rw:jUg, ?+]iHsY1<9.~ ! 󽁕 ]GjՐcB j+tZSi]-Hyb(5`TQQAK9h{==7ƭ2>bCzhe$xBGߝiՅy2!ΊLTr4eEBAُ26rǓAAbwi^V]@Gov eȉ{3pV|=_w~Чyg/swx|tВRh}P0_{2?n=Sl^v Si:%ۓ(_8aiμ}FY+1?7 ZJCLa) }q5Gꚼh܃!qPDoN P+Nj6aK30@nRHWoN1f_W Z]{TFVxVw*icVkeYwe0z?k1ϮIU̠*(uu3 Eוqd5[؁㤤->faJHwVǦ򊄐nJeg_S3<)}LWS3x@T~Nv ~9oɗh"}Md.aK'=t?6i?78{ܪOO߲Ihƶvo*"\X7+B 5kx`+Kj]W4qB3+*Jj֔T='B=xf\]SR\cgVl@HL~p8#?P,  vx7sCQ! plk\[u K G5Үjl +Z0U_RY19"!␈E8Yݱ~^'LvIÃQ$L1*)P?T<#0y]æy;ˇ[d$&5%g =90Z}3sf-[::^!Dv֔?Ҩ׻s;9KBjK 2hZgUp~=.b8OT|{F7ӕ ^SR2,YKKjJL !y4{z#X?V 9F, qɶ;UWwc-bp{ˀ`JɾPAZhĽԑˏuHԣogc,-5mloިDMwc^߸t$ąr yH6N} ڞS^!Q CYEM\186/%#ͺqZUY9%+׭Ys溄ƺ"*J P\_ \oĺE'*ѵb+|7也5b&1o4Utg h# ݩUܙW;襉^c((U:W$PX:_5\L'SRsz'`/7X &f*yTonlm?e$@$9@4ҫg Fz u=ۘi3Cе};_t[z ֵj zŮ 02wHn"$f w% 5ĥeoLܧuZSQl0Kz&;{kSGE&"uisj =&Bb$Zr>]ȆlT?<:('9N%mKCC9/اZ'õR!`~s&*jH#RRB-yN^=PZu!i.Z~WF?ڝ 89)>!7m1B?7|bՕx*ܳ+je ͧ O߉=Twd q ]9. R*D{7P[RvXgybhhY٥?0lT}5. oK+?TpK]IqIYR XL̜5MMM%$E(XSq0o#rWe(F`fqI Ґpf_)k,@HF2X5* . "tҜy%Eu)ICMEqbWkʬkڞ4b%Gbf]>u,,k)[ըX/+ܙV%w‘zV[`%b#BTPYy1)Uq>WdYqGV[_xdIJbtZA`O^i- YWZp=`ط,vy!*Ɔ}[%xIҞ"jy, uKiCT0u;ײyC 4<9QUg0W$PXnχWy^}*ϡ`5l']XHÃBK ՗ pj p|HSWc.6hya5Tm! &o^:a[ȋN^M]MY9K<,'>g)6-0MM"z%Q̚2@muTڄh׼MbJu;Z9\r"t6"c,o]Y^ TQ+7\nXY60ʊݻ!z,izź2#X} ͼ2x ]a^Eon__7~2|b3ykKsKs{_͙>Cmir+-[%6~tuenYUqkl)gxؤdu-]2"!~6w_⻖Ow IDATL-C6;Pk)+3R=|$Z8x*"@/M[?T-sD1;xK8ԡ{4;!4a._{㷻oΙ7dai@|Nki{ο5QV[gU?+l[~pEwJP'hvn;+E 4]'6cO1Y߼p5m,\ZJ3 %IڼYչjMT$҈E;2g_F`sRuy3*X$sǁڴE{ׯ:<:|ּL9V`Gf(-_wN|ӕ6$.u7򊄐Lq-[x~E,-" ΢tgƇ7w~|}O Z^w7n8i]ɶz+Hw$΢7g:>)O+{M+,5VdO=1)VLʯhSI$] ^7?%:7+vD\k6'5m9"!"UXZp)CA(,5;o1 vnf]Fb {M&Hȇӧ!Wa)زD_ScX-P̚~4+Xkdm R%7rȆ4~ UªBѨeBy;Sw )I 5G J_=\\h0 {}T ԬHH!iDƶAgGܖc!?cE^Am4BaBnO9ڤ=GtZIZKJIlT Wkc._r!4B! &B! B!"!BH!B(,B! B!"!BH!B(,B! B!"!B! !BEB!BaB!PX$B! !BELU@nE&*BQ-B!~Q74!BH!B(,B! B!F=hAC/u ߾FNs=Ϗ^1jk[ )ẇK|}_[T>ci`>tC3wہB!jذ! kC,,^rIgGa,8vdqGIxy+B! &_ѩ{|zqFf3vtԷ|I[ϟTjd~l,ikdžCMv޾K'aO8:VǺrKa; JLQO`7?vT&;]O8@4.9SdTV>t#Nq5M%-ɰaO?<.O 8N׽g+tͭl$ü;<`xmjI/Ȥ/JwS'VU ?{}/l sfAQPi0(ْw zm6TmDT6ƦV~n-bN@EEA9:03ys2A\Zg͚f~k0c\?LgcQ+s]={Gٷ%j)zs8I#KUox1" >$D+fzhg{}))+okR)"fKd{s65,}?lTӧ54?"Q Ny.;eQI[ 4Z\c':ըOҝ'7չSW>lݵ4Bֈg3G<"!j ML)r."R8O2j~yVj?+-rlbֶ5(F"bѦƦI_akeu:]NK&EIOGe'-}ˏ0>0=:E3UE~wPG(L.oҬHÝOG+4G%Et]t~""%"a=*|J|eUNOgGE1aDӿÔ x6KnqKTZL  ̉OC\';I)"mlty1+>KSF_TTAcF>ŨWaTZ]iu-} o8 ("Uq5_3*""JFD-O?y"jjt{="DDB"*7KކVQdFٕ )Ufu ֒F6|7E~!i8^f`vZ?T_mn'"+EŽ:36Vz)$J"m֞-"DMƵy="6E$4.L?QD\c)T4JN%f+G-ZD LZg3Thi_8ֈ+ȍoD aSnP01&ߥ#j:zԺdhwvIYTq+$ (on-ʛu켼%MWey[nk5.P`@,릿駳VkoŊtv:MN9Do5tqgd˗-*s-/]KKDjqV3-*-Zpy='7h6Lk-///ߵ$Z"9K%/pեnkmgЋ>kvQ-!+Oj-OTUm-J&9DDYœJ):|r^~GFxuAp=GU֋EkDm q3b Ln<9<5 fn>'f/IRWW5_gK*sE[M'ptEGcϨ<CDD}eʿI ~PΞShk `Pln̥[ӳu z=/n_=vɣ GG'%4VU9mGkbRmivXhuK`c֮EMBe9E.VoɼE+3|ruvvz<l簰0. qc_(.?dh&gԚ53I#׮hE @,XXb"NJuul ~vZ?3BcsǙh#ىst"565'܊TI3^"",_Q?5ݞ#[RӰp\9\s5SoƬp\냝[m="~G#鵾]wJmxM0t/߾:ODiL{pc}m; gM`R|ߔGƌ/ZZGNpa OO`}̂f$ilx1lQ4)iIFD8 GnFf}/MvHhbʰIT*w]uj dr{?⽑:v/f1[Ŗmձ,]jժʃ[[DS[7gWk_v.U~DʹzyU)jժU?[o*m"{~_m VzicI|J #7;ByuWeu"'ޡ>e5\3U٨TJ&qSEwۍ]O}ݹyEDjɝ{wnn8vu7ܾo**\^@,s΅O/%RD$2U]`ϻ$";W}Љ;cU"J{u]%yUIL"")e:eMRb/T[\T_GvѨ|]6?eSֽ?uk?_=~Hh#󍁖:V㲵""qɦ8)""Q)q{$>j*w+*QLU•G*R?0kxd{.ZƁkTp_]WgLi WEDLw)-/GblԆH{SD'"'˜%i ΡP o;Pg[JEܿV1N> uG ; YKe&z=* Lz.YLQ+}hQGU&{hxX*.f|7ڄƥme~jʨUl{~wGԽcO')Fb"Os佺nڎWJ|ByeWDl“;qLLs%TsS#6Uܑ>t+mSsyi8*VQ'L۲Om8nӠ5~4eQ%" %vƖN7^lgؕ`044nc'fn{DD?9&B"[_ptf!ȿO{sODؿ+rňo~۶bHO<11R$rʬ->ncBƺ y ͷwʈ?*;Q[2=Yg1&vNj׏Ъ۽CӦD+wWfw*ɾWH `p9{#E{o.uϬ>1 {.<߈o;+ֵ=c"n|6mSȯ?.V͸#BDZ=%%18 |i"3'~}m"،Gf{b_WK¯+s8 [z*p"E s#wբy\6](Uc j*/D3ܮ88X{~^9omF[q}nb&!oxS6q[K.+(+(Z!"r מ f༜ VG|y74WW8{&.+\aQaN*ZtfA^p;"n ޭM"͋VVg)-߻w3m+әng_| vXEDY'zp,Z*}]K/2fqy^7/?xͫS5t\̢ h&^>{,`59"I:kfztQ^B0.5e%,Z&XK}߫ek4"E|^ZשGߵsW];WBe9E.VoɼE+30%L]RYS8| 24nkS%iԞxT4u#YI)LڸuGuނ 8.+/XQJ-/NwnϡC={@,⋢Ity6m_ZᅦE4 9~(C#1w= M/JNʺ/Kvf3?*(ꮥk^hZ0!"Ԣ%?KdR[1=gբO-\/bLLm9z^g|5BWXb"E 0X\pjjj:}tgg|nf />sw Owܹjl3gΌ9^b>}:..NVHoi5LHMMebgl#GcqX62dHcc#6CPwb__Ao" U*J24tGEP(P(GcO@ߞ,E `1xz|As%`5hnfƑE fgkKDDHS{:Z;<݊}z@OJMҋ?:5Zn LՏ/=lyx?;|fO<?{.Ew9L憄Wj{֔m[NCcǢ.?xy]./s־O+,,,,,|K^9K՞+>f:fs_'|+LsΙYrK),,b%?8e+VZLLД >?YwU*V_5kkFD۴/hGz0$jα{ R9= qȘjȰ!j툌 WWD5B2,5cBWTaګ^s+ #_ftbE(=vʻ.@MK_jbuK5iZ9;3geZ3GޏO>eȳy(g+?X~ݿ|8Kt%P~r༳Ɣ19+UL_uԳ>)gu4.+ufU{tkm-7,QK߬{pmUN IDAT8sܳberrChӻ~=I=4ocU,/[vN4̞傽+ XnZ+TE}J}1k$ٵO-RŤ_%YU+7Zr 7o/vZI7tôasGEM[K۔yI!DibhH+%Z?LqaaaaadWݺO][Էf=g?eÉC?[—Ӈ_85+)>}5o4:㿑;:oɢ ?蒊7ULA^^^ ~(ƾ\G3z{?WPݕKgn~y҃.~X`):Kvnj3God}o᭷7T*jЁ@|>`ڴiUUU=1psZYuhQ8 j(I5J[߶^=&q 5)Ervڧ55nOԄqۿutUrgLx1‡)"!~0jPӰ%KWu=m=׿2UU/^QCfM^\Tl>VPfiģo.?_>˰>(H[7Z=cO qcos@D-֋`PǢ{ǛǶʨ<*L]q<@Ni2Fs~ڈ5f23uHj?|l{`lC]RpWi)?rv!bcRik& -6+DTfc{sjl!D$b[g_0O[[Yh#C\eqEeb'^;dLfln2|FаHbcgy{FzbKbniUycqq""2gb7Z-eʴL!E?埾Rי}O&펽<]*=-+)Zں]"Is4"R0*27*A6EDnĥRD$da{pz{˟*⯫! 'E(i-)nW,"5ֶnWdfQ*"뺡aQE*]U[']OgN8CE/[$d|NnSm]*cR9\pkt6[SpMw %""u{?Zk[@k./SD$TQB(m,;t)7ِ/nqz[:CD^jQD Oqfغ011 soE1 yF UޣG^]I2KO ̉O;^:iݑlvCթDDD;e4uLIש$p`{u[Ɛ´w.ZC]"]K^);ߟϰ26)E|ǫ?%T%ME&ޥr}viхI5aqucsXS͓# *Xx:PU;/q/kq`np1$zlS99%Brtu"wҰ׽̤iU'޲{cxҐo_ڽ15jjW*aAVTr{Кhuggywj!Q㭳^<'*13Oa$}nΊSn?*O}G:K}c/lXLwxY{.lٯon)qTl>VPfgh~{qqSцMG.Y}"fd8 [.? ?.F'޺cF˩2z$$ś+Kk&<7S+^K;Nߘ% WQ j[qv_rW"<.!\-ik+md3kl#qXDSc2E|5w>R?ٷay{Fzb`kY'9#DD$.tt%("fdי[tu"i82DO]ե _8)rGIkimOO\{zG4˿/ZV_?a[SS|{_Ig7Y7wDŎUߎ nk?#,]zUF'-(s\yhQ- v[HTځIatp]Q$*%^:6+>?9\]gN8cYD7'ODtF)]'4^8|'dOƤFx\.[ijQYw-#3{v\FKCO`8 *DHGb[{iؖfR.~¼G8"';2KI:'nz~{[k Mra1߭?SOE&n` &HG]sk`gji,VQXx.;\%=%,W[1>X><9B:j[|"ڦku(s5xYU|Vc5F;PU;/q/kq`7.l[dz4]xO^:ᖑ8qhil𳚥刽e|7#OXDFNL\S#%;Tr;83&]8mKO%v܈P ǵ,:|֬76wV4Dnd?EWwבoE)&&>aXd_?i܆ [*/UXEDdz4p*%: ,*2[|CqWXzD /Ohb!ꈭÙV .CAro{5aS奊c"Nְ'G TvUEe{4'6EO[bJ?`9s{}?kYſe3(n!@Q V?G)~d)o'ϳc"Xw%q_>E?n_s(Cy @,}Z9 ADs>'"0Ht9 @,Xb"@,Xb"E @,XE @,Xb"E@DD|=Nbk߿壟C:=]?tF?,EgcK>1'xEKMkٔZu2eXzqۢ4<5I_yc_4^ٵ:Wm>11Zj\Rdys3ſ<=g{ceI`tYin.Sqjh]}`vV8wnyDDBJk7sK{ ҷM+t_Tzj髬 c[f<|`rә6j/@,3.;6Q)b+6t<ܭ)L W8ryZs0Q!QfcRDwCR2q"" seЋ&OCw~g~p{SZnGQ@ijWj@*DzEg qgu,8FKCq&.!vE!)Q:3GR-&"~ȷ Cթ+r|>+71(}{+!S[Z.16X;/q/s|5en4Շ!c3棾9S+=wJQoO p,7fF{+xgWF p"30u 3Ք"%Cʞl|P#h|"E @,Xb"EX\n |!*F8E!2L E ER7 @,X @,J `"r%@,hC a"&N'\WΒǖ}bNzbQDS_;eMC:=7RCM^Uz~GD\=l[Ccos@D-E/`a_D=5skN-ll~aL/j5ƆKێ)b ܻkuRt1ƅS⬩^9'e\u>^}fR괚Նisw\rf!yӷN uWux1YɦIg^'t ΍YuV8wnyDDNk|Yg."J;ZS6~/s{QTKUC+cGgfJO@9LnT\bi__W%)ciś=;4ZJm~ACzDoMVvH_;pb=DBBݿϴG9:5[&o:MO)M\mjJ-HihE&$sG]@gyK =mHܴX;/q/kO1!S[Z.1 "(8^=ퟰ>}H7xћ+6OnDASVEe䜩CՕZwdw( d_yhݑqIb% ́ۏ=pCGEŵM'+{E3´pMְRn·rfD7#oK,}$->uտ2bMy⦃B#xReOCh*)58G>+{f>tw']n8c@,Xb"E @,XE @,Xb"Eb"E qTv:ERt<wҋWq_sm--ke)7r;=7ZgS6U קӱ9 "S"-5-fSfWkiX2{j|^⫫<_cZql)C""FYb*#"溎!RjqyZÓoU<>"&k<8dU6mSKVБ)Þ{0&Qo?:R= jyg6|9qvi{Ֆn(=4g(Tvty%>~wCg^tEoYgj*.Q*ϵWW>'9¦F΂ <xM" y+9c"bCϥdDDT#'"2ekEDMqnE͎NTE>YfZo<7J%"}-1BEDgmъ*N-;rͫ6Dxz06ҋÞpPj3& 24twV 70吭%-.ND!FMHpZ__V1NK 0ND{F]'j7~C+N EbXJEncE'̤mi[ƹVeX^NSY:Ι U>p}F$+Dz<|*4fm︥x[73UF˫6'85.Q)i[ IL1?7):NzmtJcQۿcPy4:s2g{#>5qi1#β6ڞEY0a׌Nk(c[%""&UAf~񁾢Y0akN?k:O}"΂ T9])"!S4<+ccίq(?lLں`;?l]|[$TϘRzG):v\c|JCgٺ_1OƩ'_wyIY+E e T%[UVt!U/9B,][u"&Ec.,mZ%*Ca c%ЩR굲 DnVly;$n3 :~œ}?˸F(ؿn} 4@XaE@XaE@Xa ,@ r?[~GbcGP$aU>_3Eaa-b~iWzؾxGo6$Eae?'7,>VCkg}y  _7oY^?旗nͯf5Jl3>.|}|}~}Ͽ44*Dg%o߶^"a"*Dg|'~P3mׯÑSDa߬ F^_`C4`3{ܱWV "@;|z5,g^zSoͦ^~w~|]7p7jGglryeȼ`cEuE`歝 ӯ."o(ߪw[^o\CǸ@Xf[8?{kE`9HyͯzŻW_xBz ,ݷo[_ƿ!):l`[훷,/ϿyuK[o^^W3%6ٙ_v>SGN>}׋/Tp5lzX~o[wo^Cj\,jmݽz~޷|Ϛ}3,}Eayⷻ/mYToObϐc]S+C["p#F۱Y;BHw&hש*\#-{`-/I/gbizJ]`2&<3J%eku۶,n䫶e~X&)eJ+yr,MU ,'.5')uԲ,UPEa/xޥ׎cE|\^&ȧE#F5ɊͭS`SFCqy壏ݭ;?9Gڅ/7=oTuuBhn&k}lj񫟹uSOj?ݽO}w8a[vBuL7"p|]z|ٍV#kon}x$~XyًsA3-'v92*'Q`q;o_ܸZ?}z~BykG#ig#kEIE^~dNc][X\};ڔa*K"ʼn1 l3T~̇:ƚO^zK]q:r߱6,uy5&Ka74i톾 EQ mZm/:PtMmRMZ@XfcN5TEXiX"0Y'".иp4$(uHF\R2(GtV趾QI.x\;)"04gRNqq5!߿˴ +GFy`Ӽs,I^rjiRa@YsDzQ>fUQT\E]E~͍"0aQu]{鬳}$(Ӭ$u a"0xe E`&e E`T~rgU%;:HͰ, 7\APu:677777'ue5"!ި_=zhjk*k!hk%q`o,R48.Q# |^.?@,Ft;=MM)iM,&5 @pE`nPf漅E`7 lSI<5ǓBr 6U%>.<:0LKvڎR Ңj몽蜦;3wtv*6=c][=EBiEeEY7n؎ch@XpVu*=KZ)"0Tv*[o$j4WU6ȊH*U9©wmvGc%\E`u46(aX۠u˔mGyE`IEVvB6O+U|1Yk!0G|riNgHӏР<̭S`SWrsVi440MybiPaPmeŅE-}c4uᡳ"0dt/..|C }`O3qgS "!T3M3(ڦ&B3m440p]c5GqWlY(I (pƍ48 G=#T׶J3t:ᠳ jFnP fBV,Z*:H`A`,< TlaPUd[lχŮJCpӆg$qY&&TFiU6cnpY׵°N&g知a*@Xf4,Y&F Ugiktqeǥr G&޶!v}"Q pkaz7"I *$.LJ'gDg$0?wRm"NbԆNXtѴHiBZAuF쳦KETHs4͔Yf;++@,ihg?jvTT'h|Cg4e9-465)R@, 5&4SaQkU{ !֎=ï;(m{נJ~p(7?3mqp͡;5IHV"i75w=ڮ/(9haXߔf0mܻϮ[nٵ{xG7(,GsTˣ+yq E`|vj IE`,.lkܞŅ- @XfΝt~oyM p`"NvG^xW'N3 +;C쾝E\#aE@XaE@XaE" ,a3JIENDB`././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/doc/source/admin/figures/network-topology-2.png0000664000175000017500000012420500000000000024137 0ustar00zuulzuul00000000000000PNG  IHDRDsBITOtEXtSoftwareShutterc IDATx{\Tu? 02Ƞ\A.-t m֊jm`RvK2ݔ\6ڄ*N\A`aa~@nZi~>|sf35 B!4t !BB! !Bnﷹn8]v 0٢o:lF#z`mnX1maDB!eTWa9Xi:]a.+_p8lz,Ι>-r%LBuqMm'U[ ;vxE|>y<P[Yۧ7B I}q?#FR:!B~trЛX)#

</l|GXarǧ;BON`Y3L_ X<;\lx<!XYf3Bsaʙ3g>6vG}T*t믿>k,OOBwuٳ>ݻw <00ѣG?䚚Fb7Z3/am1?=~M%.6Z5~c;61:m-ͣx[<%:^$DzX(:1rzOB]À O8sp<>#G'ND)_ES+Ξ=pLSGa! PN:{Io7yjcƌq8eee;::~m^?qDvtt3f ?AAAvUUUܳZ~\W/n9-7 &xdf3>i?DA×J;=o)W|a͌ww{,m[6Zsy} C9շ7%0J}Mװe{OܟvDB!7A Y-B>_>o1|= $S/8}⩦Fs=`sv>O?D"O?g֬Y= $ , ovFDD/fsjjj```uu?  BO=Ԙ1cN80KCi{^juX,s66TO 6/ ӧ.>OF.~'9,?5}hQO׈ =)_:\hhXF>2BntpӃ/^dm61ӓ72k7oH`H /(1e*OxV)Sbg>~FI' Ä9 &x{{?~\&q9"er9ZXX n4':K;Pԓ9&kEgTzULne`j-^8z~YQTпC>9aZ[VVrz߫{qf?#ئW360qVxpnh0?v>҆M'7d'~_?DO#tЙL ONȌBBi;Y9V@]|7lˤFJyx ҥKRT,s9ƍkiiioookkq aX,ܳnȋX,ZēB(uWb0֖fcl=wo|77qXm-x'])2Gócriѥu[ѣLSQn3}mк7*&gRVt3ǥk7n5|,#sߚw-7NN=7wg+mz-Iٚ[[\1ܥ֬Wѿsi.!N =᰻Iasع9<6=n6g}:Դ~!HwKp=Wn{핎3_6bbkۼ'ƌ1bkZ |]SθVEcK\;$Є<·&FVi ϕc"NassQh_ѓ"Ct9fԿD,N#M$  +|،M!\t‣̺ xvA6Clv|97:zGHHospH6+xqhX_0WVymހ}RӶypko }F7-_ʞjy 9iᅢ!7$grK(,` _daEW}zA;Ci37Pjh66{i^QT*;ryqq1_׽{񶶶#F( ӧO?~_ץKrM, ٍΞM&'b<k;{wEcM%y5E+/ n㻻:Jtnɸ7f*o 9WfGcm'GΥo]xF3'L9 r3 C.p8Vn;sߕW 6(Ph v5ff::Gw-X"UoҚr`E"VL&3#FxO?˗/obxĈ---<wܡmkƍV5++iac( b9ţ|<^ױ5? }'^|\i bc#vw3[e\KL>@=>!BnwC5؛/^ljQ-;΋]$ ۬st]m] kgYM L, 7m fZ{)S ajC=;xuȑ=@$>}T*eYV,GDDDFFD"B0$$|0ƍ M1cϭAnthh|h(ܦd&Nvk/qS||D `n΃FkLpme&B-*w VXؾv6E s\z`=DB!?6j΅ )7\\q8̴(`5!s3ܸץ_{鴄Oܥ<77QO ~Jфt !rC ]SɓKe<1ps3 x|!BON!B~|:BtB!BB!N!B(B!'<<B!DCv: "_xH B!}Qh0~# B!NM$wt!P:!7s-$BtBn"fB!N!BB! !B(B!P:!BB!J'BtB!BB!Dx x·7yUtu^ësFwm@E#F%3VtYB!NԮ[ηl@89lTs:Uwl7gr1K)܌ނvIۻ.ҒB~tf=]P<#,&D"jnYoYeZۣ JӘ>mtDWۉ0b"dra\ٹjoĀ ࣽйǟ_yi3[u)R3WzVhi8WxFw.UഇW VNhɌP&7%Kw/~{H)Ppc+h؟06bv.3N'#~d; y-M6 <0#Q,fޡ<>x#Lq.%Y9*Y =eOt+8p&f3u BQC;C,qtvM|㎾FG q W2n$J@ 䉟<Xh4!z'ˇl!jx{Ϥ[u9rocahhˆM tenY많&CBn{:㟝˛4oXvѷlz}?莧c. ?u|T|\eGM{SdОme@7k@F~hB!wnܫȭXy=^׾Z !6'/Γ0V K6h3t!7>;g NV4B!ݸGŁ9@gB!ݣB!N!B(B! !BB1q]}XS?uOQֺwNu o6?t]T._'>a<\>]uBۗӳwr~W@WFxN`l/XXV kԎK*cF'S@nw̝o|bfW;7R\y)igeNܱsg*ăWm3܆-+#5~=ZkٽY3M4ZBh?H;>Yv-/ii(;p8TȂ"ɉveE !7g]iy##m{Vʏmre2NLԖ%2?\ 񫷳sWwFjzt7do9Pc0cyfMOo1%Mol^_7hE _Disk6읷uqc@~/ %pMmS٦P |>[L/7l \p|t[Y ӌa! {|x~Q{6tBGn9uWɪ'#eSХ}gi/<'B͛41/<3K{۬P6~wYP6ԧU-8w6!ajϫ/Wvh{]0Gdl~?FF#cu@GHMN{tS*D9*u VTyy|$nmG7o:<5mU}8 쐟,ÀҴp׿r]}]˙9_pBö緜2uϩ'75mYiFfRWw &Ew];r_2H00!wDž'kB/tL!B ;n\??7r 0 ƿ>dnxviɵN1jv:*2#33cю7wIPЪ=vJRb,--R @WcE;>:?~ O0(:o"%meomO[K{ZrhK}юb㌧22333hA֖ꮙ+W"Z˚u*Qܻ2333󅇽:ѷ^jNuR_{v2׮+<%+6.F_r-3"(Bڽ$3c^,!)`F~H(\OkzFsO~\-zXyHOͺc6<8wOXÿ-{ŢTrmfBj9'OPȼz.\t cyc_.|p 8!c'3!CؚgW꺅uBϙ q;>xN2PO #vU>.@CuV3IT]}3&& GVX@CLw릤y"NVƨ}DD11ޗswߖw :EzIpNq1M}xcA3N 't&>"-`=BϞ*pXմ)gFx璐A) }sɗw◇޽3e`T &u @*gL5&rgg0O?|k``Y3L,#w9e2COTk1>;߹dƃ32a&Y輀/j_UKg-~z#)56A*wm` ZX lD>(C,9wyOH'(nmS ,:Ung*M2,_*:v].*,kA >ܧ44Իz K24tVsCKfZ(daZ%ѕi-D> IDATmCu_`՚C3|mܙ(V># PwWqOipLS i9ZhYյgD_-n#d𶓃kY ?wy}M#\jLYo?b^溺K7`N  ^]]vwX&. !]L5mY)fN }$|2#nGwmpOi!b|ܽzdQ3Lh' /-@#v\L;3b-Pƾ0-@CE}S4B=F˖AB!`0XXl \MpÁ36)L0yIH!A199]ݣzz@<%-kNqMI*&B(BI+_sy] We=/>aoܙwtX@3/#E,_"On}HZ-@p `FV`I =cלYv&S&!J'3D\8v*E' %,ģi'/o<4I9m)}`L-"ū ~ZY?U 3N#DJVK[GDJnNфBaC } !OǍ=eO[.>cGܬq0 YGJخe|zlK rw0lw?:+Tru/R4!rZ{:FфBrB:B(B!g&B! !BB!P:!BtB!J'B!N!BB! !B(B!P:!BtBϖY:zڴiӦE18eڴiӦM__I爐,5k#i1+˹q Gˮ,\0%ߏKS欩OTo>mE!vBn}-?”iӦMK)5 Nm@/#7ilڟ)gdBV)*NP+zB(۽X@'%'JVg ή//Pgyr[rk [J (hj<[sO!7 [+ZU`n.ݖC^0\SY3(ձO;ӬɈ_^lRf44>mB(wd>q|(?Qvg\Ce}P}gznJU:.!9ʢ}F[--ҳ`fo;Q} c喔Լ:P߼'ƍ^57)/(3K sSʵ`u$e_S(~@J?Q}Wc^JUq}⬤9iz N[^v񜤬E;uFscqƢ*={' c9MqXsy+s˪zKsV>ʡb1<,Q_[_^*mg̓]ނM/ݛIWUoӚ)[4>Ɣk6VqWS&.F͖Es-3זNOyhtR,:%V / ܄)fRE턐0eY%:99cukqFJv U\rluau#{ꀺqت=W+7-cwoj J$?ٕrI]ԕU{\[(Ԯo#Yk P_:?6O+K聺5˙m{2Խňq8j2"0,H\m,HVjN9 Q\u.Wힴ\gͪKNRJa)ٙ_RTimI} dsӊML줤0?ج,ۓ_RlUlU卅kq(g'$ƪR%;L``1Wn\2Ұ &`g~Еedn]](SbU k+]eT;daq}z@WTpv٠c{‰4v tB͇qEzr6jY'8M7ȈU&cW/Z[f({cp1z(\AȭgKjͱ}*6?\OVi}Y̵%,(b3Db:*<<:>1qcJj~ۗJjrO^1;sk)l>sJno6'ݸōEYjܺ}BpulbRTڂrfcN-=NPWTi܆{$`*X,w=g4als\Ąجk@]^vAҞE~W9G3D9H81\gMU,kp}ϣ훜(0yǞծE::{]qq</)ЮNqa4ܞgdՙqRe]'E۹Cau(%˯/UK U, Fc:Y^X2,|EV{na^ed7\S4Aca\-(6oK q՝lvWDV0yev1קzZj3X",=}`]>ÿeUyEU_z@-ZKS#\ZPeLiBƊ~/>k='()ԗrq$B鄐;Drf ݾg!4:HJa+:I)^Ūp}8G(Ԫ0 Mi,lUE84fsĤ)lC`kF\tmY7v YU.tO^QI\qW4f#VvbSĖ'=DI\}f]mc9[P: ug֟5J뒻ʠ9'+o)S'ē#8?J'Vp U ^lɶLk+|mk0U0֕r}밬Y4t6̙'X1hXm5[#7FEfkZpG%vρyC=2>:`1T4#>:7a[ϖ=W)`tڱn$ʇ?L\~~I>=زMG0aꂙVOdž8(NЖԙ5`B7%hK$?{V5c5!2Cs>0aÖ}Ƃ*@ouNtOb0n00Hs7-,.tzv'V0fSWtVg4b=-^Lֿy:e7rUgnhbm[Kʶw7QAeiŦՉ,5@W 3Ur&2@,5\[Z ՔCva:g3T{E;vf%m8J22J߇"u: sO"p`^0g6 r&s\NЗW_(Ŋ 2k)9GܓCd}ãq Jk7$Q8!Nn^NQ{ǤI3]&1 3$(g'wtwRM7}rFCVvp-)w>1w&1t8TSһjބ?!-}kϤ5gxQsߟYSCT-z_WsY,n- }Ql7+Is xvMkFWel;( &Mt3];~e@]X[z.*OܣWbR`׮wR{µCp}k聓cZgTsE5q`TS]!RQ뺛cPr'*{پF7C:5 :--f.=.ufW8Q$&2'͌I3BdF.?iS(u`g m5@>pt֮c]6}e۳{e7`¹r5y2\}zEQ Ye\s8(>W˸'(Nue3WxU:V_ >]Gc 1_{"fN1 rdo r ;  jǏԏs b^ 9LJCߝ]Ȭmz'7$.D"RC˵߾xc}#ɾ">u 7u4q~Y_`q]^ {`*ʭ*S%oؼ:_ZŬ-gԕP$!U(4j\9]2cA ;EuK}gH6-3~۪8*âdՒ,,AP# IDATB؁-#A{j8zf)޽nEZ[2F6;9}[ME2SYFva㍺$I[ߖ<;Lٻ,TJXn=ä"VxGf0J`ə zoeͼ2@u N=1mg GzvF =;kp%m)ܑ<"Jq+6ݿ>ޏ&C~ Kqhs8`N^Z6G^ye1z-OOn| 0}sO\!tݰv>TGoĻljZ){l՟UUX=&yz'`U;= ɓOIe:7{4!=IW-H x$mrҸqcy:@p=-tB(2 S)fDJ'?BͨH\^bwϠ:gV*%?t$$*̵;KFNS[Zȍ(ڂ,ղ&H!7a5Ee:mI΀=YjE\`ZlLP$NgN!KH3uMHIU`l=R9Z:;}YchU/f=+2WP:!r=33 3g]j>̍+,èWmK !/~}: oOy FTRPH,V("CT&/[HMӈ!BͅFB! !BB!P:!BtB!J'B!N!BB! !B(B!P:!BtB!J'B!N!BB! !Bn7B:F'Bnoj5tc޹LՃE]vt!J'7Ǎ6~uK'WP9c}V ]I܌Qb0rnB(ܮD#+ev-n`;!8y>/jQ}'|};[Wlp̓ 뱽'k_ZڧC-V xޞK&oH?6ݾvG{wj8MJ%>DHֶ.]KCgjykEx?3?RSUՃ m)5D#Fk ,c4:8wh`\D%!P:>$=ln^_hK' ,3BIHGHID l5-r3&S2F`.0 g㢮`n 3 rAPXTL 2R}Ewv=NB[)IOZ+n%n' jc޸#(7A`ncQ+Pn|n|ޟy~?3Qw;[VF:j|-ꍤ7l9O@Xnrá|uuaI>@۬ƫYW;4I 9zP ^2NAh]WT4&S al#""Ϊ &yȰqfrk>{&yf`8 *;"%Қ2[L.rQ.TzW2y`{yCp|oݓ@ gu[J Jk m2;,6L"@"4.w]81[KN:h ;-۳D,o7 PԕGP`߀svAޓ [H͖MEnM_<2QRajvNG gw zd-G$6 4W+@0Vrx>D5/*""b:\/ :D֩+WwW->/~OJO+z'Qi:I=.UX7|p7\:?"vU8bR]KjseȪHۺzݓ9z@/Vv@0]~{b_ހ#ƯO\|3y*\0\r8^BDDtu2">MIgj*NT뇕* 0< Vڢ7!)* zOR@0 L\_g_sKˍc#!Zn^嚰c>/}-$Ra_Xڀh_K>>Knv3 ?/?$T̜1`O tޮCϿY1]yDҤp~] \|T"|z׉S}:ן߿BDD?;ԘL!IMg]L gҺCL}XhBDDL'tHu38~ZX/j~og9bdž?DDD=;DDDԽpT,11  11  111  11  111  11  nj_}}(]Tf]]RӋl< DDtB7+ viؼNޅyW5߼fNjZݢ9woWLo~whUzsi2+`oNIjZnѪ 0lN.ڜ>_UaGWRu͐uZ]zN$Ńc~uaV5ٹ٩r-˾ЮZ5Q~N SlPIqڐk=E|87uf,ݝ=]fwnnn塆NB"""/ M]a͐1o~"n~RPqcX*lZk{0yD7f-DԠ ~6qSم3h6Fa/;aOĪ)Iy6驉~<7DDtgnez E9~2Mfɸ{&8q=8mmUI=Zqh+JArb;^ZQ,voktM?-!>if=Ng zԅYsVlsO˃WfWt\fl) a S#Bz!C-۲lԊ[)~ŕ+N}iO\-|i1$Vk]]nonn#KRyyy'"BW&g?z?HBNT/6 jn+f6 1-PDkիlFh$9".1WcG+`yР5Ksg-[E+N|gUB#bV+WjL&+f*00|ȅI ;DHb_sQVZ_$S[Uh"zqqj~qq~9ZV;ku5~xIpҚ+Rwu 3fkZ]j᤮_Ph@Pj띈 ϟy9{g!S3>COm?cu$ }.G~lC3vlyyq''mRԊ7x:aަ5l^x饕+( ~]]7xτ_]w[iz7x婀­7ջ7xm¼ c}.ֻ7+=tgt}:U{\L"w"e=* |>:זyyAp|pP`iٺRPmhL72⛳wvfHfʘϓ|KYyy Oh8xtp.*ͿXXpșڱ"ЉM%9.*Ej#>rp֤*Dc h`BFw8(0PtbmD"vO8fشǦ}h(ufIQ1(_U>R4ONm܆H:KNOG%@=J+[6Eħ!dd>͍+ 5ϕKԕ d<L/HfH(m'D?HuCh47J}"ƶFP!B!uԛW1@$ blJ}"Nm:fӑo .'ѹ|ckÆX&RD*uj^Y|xDͿ\ 0WN*!"N3hܠJ8ۆ\ͯv0, w3  +DQ+l-06xpsl@y.ܵm-N(+)m+SԄj[ W.\2nkv8ʫh.;{[K PNxf^O^}bg JCM;LlwFϙ7}ٙ[3WN@3ى@_m͖Oz>8gڼ-k_̓N*ܶÜ0/~hғ1#MNmjX_M+,[m2QHX^)~R#YfxJ!---AΟ??x{Rð5y~fhVu\ڑSN7G<3ýӱߞ37}eNy=ƞ{_^|yw 7 Gu'y;-ܛuDDسCt<53m@Լ5g6_aY}|b0]ڻc'"سCDDDL'DDDDL'DDDtBDDDtBDDD=(]l_!VGk._*r I~R""v'nzC yWYDD3pIc߿(aLTЃk6."/bМIl oSXE^vqPx?"22*'G$ii&HbgSL _S@DDL'tč9qTj>O,lf uX6ve̕\0Gњ|,vuzY[-c[&.\|~G }viJoůWHrf@FHb "H.X=SwCްo\+8~~j|jaKٝ'a *x̹i듿X!3pzJfV4T")̆J;"%QlG;pf6ہx] )1 S):ĐhəJ_hdvT\.ϟuMDL'ԣI"篘sQjFRfJd+%jm$8~;Kc$@J  'f XD̍WYYbNkLZz(pCKnoV͊OX E.{pgOBP(hiia-Q'鄈鄈NN鄈鄈N~W,<%%%uuuNݠAT(AAA\"b:M=?`wt/^S'h4;wnȐ! (DtB=UIII``X,M%SZ{TO<:UCquB U\Qd0EDIx Q&oz M3v=q/mzg^? [cz\>f-c>#hiVwX59z3"سC2XVnm_b;,Sq3\ۙ:N=PmWگ责3-70W+y(=qݪ̹d]'Iz#ycƉk0HL'kĴqS;dmy>Z3XX.{o[+eߟ?7TX,7LHLd1uѵDXkH9X&702ft"r6.\=X'/y'VϿە3K5|O/l^xRhW/yX Txy G'ŒH,pDef(KNۦ͐:uC@7/5uN–Zv2f։Ȉ[S` JZޙPa ;#%0ff|SiӗYEHDL'DYN~&ɆMEHigTtۚOOފתgM?:zX\ hɳz_8/kkᷙ~[w 1^i)|w+ =;:I+sںr$꘩qXBޛzK>29ԬwmXP1{sfZ4gYz1*1,[^z0̹_̵ܜUOC-3O,ڠO^gX!i{PIEYo'^ːN:Ј >)Ъ 0p֚MaXEя&/7ݖ"  O9Y@Ŏ_hrjwB}ZPNmDb }0>.qԤY)iڍy$(HB&G` HҶqV)QSec2!\=gD!6(}OL'DtBԑ<慷R9FTF҅k/l@eت.XEo}x.n{9[ IDATM2-дMmWlwǣ ;f T^x-'YKW~pUkt (maE짆ujq{ړ(ĭMJl=ӷÑxKNnVs`]8`2UNiS{mc^ sʰuǫo3rxEK睇İWz>\ <)▮buּCo4 _5B#,w{CFJ~6QͺY{x'7\:|˗/_pko:|mSG_~Wheo^$?[Y@j{aqtQqy~{28^723fV85뱙f_DUnήlEHDL't8K,n&+eJN w=z㖬e:NK~Oݛr¼'\gS7DG^_;A7LGƅEGF?Ul~Kc̖~e~YVM\3x;KccI+Ҟ&^mb^DۋVYa!ʃ?JwM,Y}Ea+Ž;~_BXk~jn;n̙}qqHKDh܉!@s -ob|ȼ1cUUQKV 4W\z/Ϙq_~ը'Xj@ѯz_L~7tYvl]޵zW^xD->{r[I13 7$*c-םTGŒWlGSOԙwW(/(^. (DDtdS@RL%oj8TtםȶebfLX3gMGQytxWfȚ 7dκ~(fO_\I-xU/zo F;PUZE?Y#DUk_&2 h2.5ȱ^`izJN]`0$v3R_}1<6 JKp⮪-BGM}qҮb BDD)8K6#ڃ?(W9汭n26BGf+$ l?)}0$DPk(_kZODTɔh*9 uBol"qZ<Xjdpw0g L,MWƅtR5&U^ܑw1'zؤhP:jmpꔿЄk^TH?v#41ʄ=b#84ch]CEEdUMKTxFi9)o? U\m"6T욁{vN+yjǦו׍\3ڔSNe٣|zj:vPuʡ4qw=4,;x5!kj_+2l ˦L_SN aZ uSGEh9w:^Q ~c=0L=)ZRvW7P5mKv|lڇ%9uwgwmU iϟPQ'= a~&N–f(o*f NڳեNM}s}Hs5D-zt T! zʥ?-Zr$L8:Sf'Ok;?.,T5WLL-"p_v;^:̎$6O\W[jϭi>7D(5\=H Ê]0E]5y⒪`IC7=*#4Nb4Y; \.K + IsUʶs xáa[Y>oJYHMHСgW{PѕF/yX6*;U߂GF6vIı?Iycu:jү_?~z{k׮IRw]q=Յ&ǎ DZw!C( ~~fS]ʷ+ݺڵk.A$˃M鄈/>JDDDL'DDDDL'DDDtBDDDtBDDDL'DDDDL'DDDtBDDDtBDDDL'DDDDL'DDDDL'DDDtBDDDtBDDDL'DDDDL'DDDtBDDDtBDDDL'DDDDL'DDDDL'DDDtBDDDtBDDDL'DDDDL'DDDtB}1ba= uhb,[ǀBDDđMg[|C-==cL=?5[JEEnG촼qfG_@{|ߪU v]z L."YB܀|UmKO(ԾT52*Z3lP^P"""I~ENH 8 ^թs qR V2l% &kN5߯ʃb^Sʕ$۴${ji(UqܜOUpUmBV:7Ͻ5꠷g|Nj]Czk{ں&*uƬJ(B?څMcm~mRZHLٖU}1iG~e]D>$LmP>/\w I m] Uuz"&tl0ѽӳǝ*尀^md#JwJUsN}`B2(whS9M`N+eٻP+kHΣMʺr@Ժaz &;{E#ׄn_P8NY.rZ#sM}@G Z#Q˅C. [*F1.X7,m>U/r+=kV^Pmlg:Kّ,5;Pjd0.:/Q9N7;3NmSа`\{7:TR7, :Y[Z)gϿll=A´d5""Nӳo**ZԖV[fY?6g{UX A¤i3XY%/Zg N18mf7jV:lCيC{o2o;i aUZXV}Uxs5D-:QIl҂L&kgl eY'$_ŪGRMU#n}dTRU 0i^XQx]Vs;\:h{TrM.5)ͯe S9cC@dOK5ʺCNko(&Q~ҞSէ+ꛜ>^Sf#~nt 'NT^ ~"GNZ߿lSCW|d'<slOb\1%o*vJ kz U7:bLrbޑon@fM HF:uG6uFA7׹b0Ǧ{Mً&??}ZpzXir>o<|e샃լVayS _5'CVQZ;{zy{Ir_g (d۬3'  j65|sc~ }ޟpCgot ɭ>_m4pS^Y'2oM\+ÉL^ZDL'D9UkZgvvDtBD7F+;}^dIDL'Dt&gk5uDtBDDDtBDDDL'DDDDL'DDDDL'DDDtBDDDtBDDDL'DDDDL'DDDtBDDDtBDDDL'DDDDL'DDDtBDDDtBDDDtBDDDL'DDDDL'DDDtBDDDtBDDDL'DDDDL'DDDtBDDDtBDDDtBw٘_bӉf񚣏nͥ}b{~+ͬM,YZ{PI*:: yA6μ@yA1 #;cٓ-,edלv }%-UNJE~^$S:;t~퇥VWf׎2$T<4$ї{S=۶+ S|""Tw압9P'F_v5蒢7/]}ѱX1\^yaL_S0A´F""D8NF*|fj\-v_XIS;`7^{y kD shj4[>KyeM6SӱE8"Va/]Ywr.>.,3=%df/""b:Sů%"{za bQ/d6z}qqoP(X{DtBD,77755b?婩ZuHD]=;DJQQѲe~B4`X-[VTTj$Ŷ^%##"00PyOMnnnIIt$1Q1%887jsX u>hnZ333ӽѣnjyS DDL'DRSSwܹsN7X`1cXWD}pT,Qo~vG"""t:ʕ+ݯ322f;g.XqBD {vz@R>uʕG!"b:!s+{T( ,@[GOjjF7x y~}8ǎm^]j6WQ$e-P\|BT* ـw?dggo۶mƍ رcǏ ̙!nw`PWq:fYPwo= vѢEzY/eۈN{kllkόs^~鄈1cot:~uC!UrEEE]mXTTtS DD]m'DJ\\,[,..ΟjDDtBD`…tR\\\\\J`5QbQf͚;#׬Yj$ŶFfgg$wk'鄺 G"{TobݙBjZUAD={v订I'ZѾE;*ͬ ""tb+#r4Ui*mw;c6p;jm̕j+5(;,t_-:e ?WUs'?lT#E (DDtݸ+rJ3Ǘ_desGW|{bwZU4>~_:!I3}J5eklәF rmHpA]8j^A\H1OR2z",X\(qfǠQ'&M.)E;NekMEI_{-FدU>#fٓƌʞ][=R.R}~ .U\83tۀ""餶[>_G'SGIG=4$[3 X>1 U)>(:2\e pDTOP (/Bk@9y :]؜V Ī- ajj>gv@DD] ŀ_>potH3J O7xp&iY'DDD"a8[Of^^f>}C/1-5!ӱG!<=6<]co&VRMώ2h6VsafL{x/lt,P/*c[фNnC!_>gs?%An2兔?gM{%k.kԘ  U>ݬ9_p"Xydƫ򩑂ӕ^OtC&DDtyLn*i}aj~xeMJeOW6o}"J#ꃺ.i&ݕx?׺*>MDD?Jv|=J;a3ZDD'҉`Lݿfub"""b:!"""b:!""""""""""b:!"""b:!""""""""""b:!"""z^DDDԫ 2٬닋|8B#b:!"dn7婩ZuHt/p QEEE˖- bYlYQQ^ֲk.>P* l~jG&77]Bzz:ko*Nl6+ z=ZvܹK Şb}:dʕ?lʕ"##o*NWq\VRSS[nj!b:>@DDl,!Zl;!"9gk턈7XDL'DD*޹!r!""m'DԍM|3뇖"jV? -&-:ypC 97 u ]r>yz "{m'tWѤ{)k?q +zoҿ9<{v/U}[Ep;jmwbPשW E;*قB QH'N_ 'AC) (2,[c֥VYDDtB݄@but"+I޺?W"""+扽g +lA!"{㧌; ?fɩiϮ"ek@HRUSS`X+D ^MT diD{v.üľ}mێ[q@џO4,2Qorgm'UsT9&ώNrO6 ZwIBxݾ*Jtl?$H'溧%""{Ͽۿ8􊜻ϗD ED@os(fϱ/ؾg8kҞ__M)03G_~|SWy_/Kvo}S$KQ4>"m2R#t`dHᅷ7޹\9_k l`Ivj}H'fnRK=kzuߔ?&e@:6wjeٱg+Cqlg#Kb!W ; ? 3~_.ݾdhr7揮X[c`;otzO̾{.2<(-8,u)͝B!(0;G+}/~WvF""Dn29[H'w˻:t~L`{3'~\r0u NYR{>w\oß>-wOr?bO~'2)馌9@:flzn_H]6,M `OZte;9ZW4"9z\} s4(@:f~zƯμp4 rm(à&qhk[V?ᠴߔ~YO/:cN 3>oy;|' @k _߲zLot?^q G͇)Z?~ſc/?qbRt'ʒVZψ&b]."bFD䑿 0Kصs~ɶ/m];eb&E;<4ʥ:SK""GG!`WXs*Y6T'Jm%H'ni;oܿg-:'>&#O ?%,Nwͭ~'˯6qڶM=V/Ւ6:<>H~v.|w8ttnƫn1| NvE_~xx>CS"={P]i(7ϿGxιr۟ZiGn=IaϗΪvI_'vTy%f_9Zf+?cWߨЖ=+9Nl{t72\h$%]~]!Cj_wFY:%Xf_?ґ[~o8>֜fU5o{Eb)J `m;vU~C-`N6UUtrlk˞ڙ#9|D.G4ݴǠut`m؉xGq+ j 'uJ\סj$u93;(M;媦k2P 0W w~8iJơoN(6 RQfM,*Vv9Ǔo~Չi% 0Wʞ}_a7v+'H'OI=uU0*M7l14t`m~4߳4naL;@:0c3L9I{Mt6>;6e`P ]IRYӔEQN9]?<q\9e3MðO-S@:ٔaJ׃e֌4))ӸT }'|Q8{eOg)ӏР<H'6qߏgmDQ,6Vv,+M7N ,+; t`>ƾj$0WNr+^{@:pXyWVv*н `li^NNYQ\Ԕf6uXJkte웺f:~geÁsY^de]iha{a\tBbe8cra{oQ (H'6qUuhnXNP'XL5֑ĝGqaQe`I 0pzZ."9fa;qf6 pD5C7"JS@bw̕#w\S2zTa 0xU쪶[qNl%MhBw̝fy+{e&ug˦ȵIٱgI2ر 3!^\MlO끊XP;jl(Ǿ8 MKw\kTsc;H'6Rm4EoX#Ķ0&H'6Hڎ8Cd+ MGaں;մ: p&Pi,Z NM'YcW5xNGgyҤ Iӊ,(6dRZ!&~X @:d~B}$ ͊% ;Ϡ `n D8#U`v,$z'|SHN0 qS @34%]S[NV  0_'}SfĞ4¢w)nyĪB@I,(z'R(oFXLN˼hMf4]) 66raka:v~cNŻz2Nij5/LbR t`>gU,7,W%ry`1+83~Vk{a:>ֺo+Bw)ujײ҈Ф(`-[Gn\1eUE^0H'b<+I' +;\);)+;lh8I^6MzIKh@:pPV45sRb}'vlIW.Nlwuj4 t r:a$}'\)hsoN/ &M> \4tM r(znA lEsnԑV$]{ڪ5;jlInUESSiN\Mؚf)p8VB.rc4)=Ϊ#kc[ߏn8~T xm6Tks2ěmB>YU?͊7Vg9"@:08 e.9ŻCm/R"E5xnMdPt`64+J3`wz9UwhV8#[b2眤LTxgu*QlVBs/ Wv}?4oJJEQdyߧvl%Ny4mv{{޶MI1t}lz2nR o~]{t`v g߿i#'(on&v $ M) CNw,1gǬU麛3w̗K[uk)ʊB Nw\{[֕zqm~xpo,NT\Lۦ NiEfm(}thXnM6Ip Jw5Mۏ؆krA;6%]ȱ׷I0¡N$$5tm&N5Ķ.uܸ׬b@qC ueǻi;[bɩ)Dw|g[Nn SD/fIٴMƥr5]5+F/3wwo}IL;eqg quoQfeH'f{uGЕmDQ^ؤw >ݏGSf04Ox3۷m/n7&A,ֺϬ5&oI%L*Eҳztx @:0$$Oqn+ǬS(j%(ZN YҜ}/)jlHVN<>32)$ql!;h La%\RvznĬǽuI]V9-3 l;vU~ۧ.iU:)uܬL#ņ:iUMbo|D7N*C' J"DcNlF@1 +^MW#d-eD`+8CnfTYf_čpx-ßL@:4Vvtjj4NJ:D5 ;Θb^h{Dd웪qpBݠihq*l%Nvϊ5սoNZVŁk]|޽?)F/3Yeme2}9al;Hcǎ;6]S&t`7\fŞfP{ , vgȭYoYfž%̊cǎQ7~stnЌ1=ﭼꚜXfŦȬX`kw*㨫\nՄYfqtiX xj1+X J)MIהihVUQ׫}IQ5m}׵uF^:!:VB/Ïxco,ѝSҜBsҺȦqͳEgnבC xZM2/4 (jcGrMV q!ʼn.YX[u}SLǏlz MYg3d}Q(ʦ2M5¸(_`N1l/p"2MU8j z'8r͡ɋVBp/< m;,ErbsH'f|׍}K۱+ʋ zr*N*C' J"!2k~ ,-iXugby;Wj&®X`$`y`*2 2"=r5"n}'VBg8WtV) Sa+3a5{R5ꭸ}N_[ `-}k"_AqG%}UubTx8q銴TN&NH]- 0x2l ϧӴ=* t~\{vTYM'k&3o薱S>p]n<w:nTjw"rETiÀbxq~7-?=qz`0-rã˲siH8yئQ\}5qd+TyǵF>wh3'~itFW`B28v3ɫ.|Mm'pыe*]o;"nN3ڱ/7}_vl_b)Mnvݢ4>Z?ߵ;렼Gq;BwQ,mǎ +_{?/GUg߻KO^u!]AGJY~Qv~|fnkNH'  NtH'-S5!&IENDB`././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/doc/source/admin/figures/new-inst.png0000664000175000017500000017667300000000000022221 0ustar00zuulzuul00000000000000PNG  IHDRI詒bKGD pHYs  tIME 2 IDATxwtSM(5K4).bbCQPyUJ/^Kz M' Hy>!dwfy;ּy̭ފeYHc3g2 """""-cѮ)6lllQQQllllQQQllll!12G"I4M<gI0[v\K\س(M-T*u={m Ů-""""rq6;Jt!v`;N,?J9\X,""""gG",*m\T.>hke++ͯOch\ """""9gNA"^Z2>.ž1 ٺQDDDD-6O<@'P5 혝8oÎ"G昄sG{M|$)U[[DDDDr1""""" """"" """"" """""-""""-""""-"""""6AC9L&^[?`kl-SP0\+I/}q8y5ˢ:]e.ۣ0 ' ;O:cͺopoI\\9nHέq8hۥ7'1K0 / G;oFDDs.vLvZACClڱf'1t~$~ޖv+Ip㣘l 'id.L=Hvt9E?zm(z}W˽my):z CZ:p8sy)e.|}Փ7sSӭDocB/zp ۮqJ<8F';{˔%Xb- KxMfM£{6P>h(``e`ո 67[+_&r,ӥNbg93uGEfÔY}ʩcqE't<¨QXxԧ,68lG/_W:Lh(`T6/9 @GNg+$a_ 1 Z3Dr&J`_Y"Y nV*z=6+@0\lC~jG|!(G"jOn}*"l:7ҽUyJ6JcOL_~,+|Ƌ:sÁ̭}3f>]Lal g@GOƅqnt>zZ8p8ha'LA~Y\"bt>~ZwœNb}f+vEho=;]p[FcC*γ dI?9/Zs2&}i[Lx:)9t,*tApߋ_@4q2|f8tvMw9FAdfFܒBDAkwS/hfb ~dV|`K<7S>|>t:roP\ QνMO-lf$0ס?g9|r2i4mx{+ M<1Eָ!4)b+|]TRLSoαy N26,';f,hϘ NA/?{%|_1=I٠h}/uSX5Xٖ_NyĞfϪpl<=O﯋_$[e,Ncf1]+dWsR|/#:<3;gR~ѝ!3|dӌ'|r&+RI!MٟJ^~mVw _~ 洰y18/6bFWng^BnGYllG[[xt)3Ԣg*>UfTV;ٵ#7Vpe/M;wjt8""׈9s* h#26V okK9/'xӎ6j6?uW1һƁEK8e saYkƓcgtz.ϴS^#C2;XJvjɯְzzVLM|oJ궋Ļ~4{kZϪe+Xv!?L9 vx~xS[|8xo~O" 2/3nr֭Y˪?UX6-HlFsYl3-aO/\^wl/s~ʗ{.ή ƅEo:]ƉEXY8_~AFVc>J\\\Ǩp]|5g֬f݊9|=31t.ZB\cѥ p]>̪3z;ךTD""RpnTVGh^:i6vF `730O>~j@3N)"vGCI-6S'BJ[؋VqH{ )Ya `6,kPd|l;§r`0vaٱDEiׄ2^I}vv}'bɼbw߳À|A_ņ >t<R 8_/'ǹqONZE2 l?@biWٚ- b6|/o0@4)SpupG]_=gwpsKᕷ"P*Ϫx獮T?Ć1n8/IuF+'"3 E]ە6%g2숦H$ll~zӴ-gnnK8ϩP6ϨoV; >U4-v gOe}o#hxV e#PO #_@qH yY0! xbdm%9c I\+I!.q 9|@{Z}XZ$ƥC2 Owb e,{xy>/\+Wv#=>#%ѺM3ԯK뮣V uyvW2g YH=]_\͆3up%: ~Y@w?{ݩ4`_! 23{a./Ϟ]s;@#7ޗ]0u*+`OLn89@ucԗ^h"Po&zkwj!nci'+kW`Na{cE,ీ__J|`6 ' JI[ |iOcyX0!cۏe>+P%*De_t`N˴3Y?xٟu#;w"=}wLQ'CE+PpgwJ/J\9.u59#U d{22zGhמG%6֧6]n.b̝BᕛEk翏ڃCu!/ޕZB(`9_`-#.ZzU2r0bMyz}fKsEyY0!?W8&k"7P;]2e u٭2?eѱqɉEc|hD兟BƤ\&1fGSpѡ a4={Nݩ]>]/ql%ѫwuxIWEmlߙ@!ԏX~h(Kg2b?DivOz/֞ImwG~ebu͔NLɷ5{r[܍5{'p)cq% Etb幫ylӶxtw3l}1 j*Fӧzpśv1s[W%Yb5zC+6L' {'>ݛ:ҩEFbl LWq6yX&Ǽo|)ax:tkK*>Sfo# ~S-eSZgH:OUu?f?1iIxXV#M//xyq{,|T 3 Dvw~ǧ[ۿ7!nyD apk}kg1U=2_h4\1^Ű FƷQ߶NMbyOkyfiLc/ qSh֏6ÿ9Op.wۏ,7{ۭfCL~'[:Edž居b? wे$kvCL:qPﮛsr[}:}Hzk 5zrvV^h=ے [DD;cf7%.rwڄ0_Uxg|4d̉N^ >8Ƶ<,Shd4:_Ϥ-S󻁾o29ms0x(<^zId,Hy,㵁MG.5'CcHtŷJ"""`s\d~N?jJu,e gޠNOYKs:ޛRܲ>}m[&OeլXU: Sׅy !;9;%5eT+}to 5[rk'ش`U7v.@ȪÿiZ^%8^--?+ٰ)J:AnVGoфfči~N ɚ5[w8_z-R$crS? [Yz ūLjiDe.uV=(]:j֬KU/cXnUpkJ⡬\4ZPDDDDD[DDDDD[DDDD䪧1"""""H=""""" """"" """"" """""-""""-""""-""""" """"" """"" """"" """""-""""-""""-""""" """"" """"" """"" """""-""""-""""-""""" """"" """"" """""20bcvHlllQQQllllQQQllllQv|<֫3+Ƙ5 v]&{:Ћ^}fi<׫QQI<8UAuokPy9/V0kAբッ׬vy0aZӿ*Mndo"`)?o~1~V"@Îh&9pŹ]٬w"h^DN'-dsx<Ľ*e٘C~$ 'LlγkVfeޏgZ 7<>{G= 8IMecZK{0Jf6楷""""eՃ1'Ӡ~Eü/'3v-'7nwA2YYE P~w|jimoY=S}q^Ƨo}hr{WZWdpa;#9Fw'u&EQ`ڕɀw52ڨwK#|Us5ZPZƾ>?ESޤ(M?+&~6 KY8*ȥtymVwHis/=qjÉ0p==҄fG`P(flE)_$T74|>XQ }X3^.EQ[gQG}/ ,}Q,9Z@ OG(r6F(Ukց&~kY:*qjU(*kV3?am4>WVtn|a 컇ޢ-)]\^ ElFP@G&]_wZ}#wqoN ]cs>? xRrqE:`"X|4N! r'/RFt,ª 9p#Gw'@vsv*)'j G8y&RMZDDDD;s.: 5:Sv/.e77th4g/5慶e8<ќ{. DpY|pM4* Tw'H(Q{nbeA[e{7ݎU]'HS/Jx~ذPW IDATQ΂I_|k&qE 恗8e˶8l"䏤j.6?r-;sq}>=gwsZ7UR64kgp5q&d_F1 lRرSyONGTğb GTEDDDkW4}v*tz!)}l53 QTm?Ѯcr3r[--b?5c]tj{ v/-Y, r[˰;Q~WVWU6Լ"eF%/Ձ&`+ٜ'|[a^IŠ^]T""""sIo}Y "Wl:KmvK_|#"W]Τ*l\} } vJ$T-!#"W\66Jӄ6PDDsb1, cRvj7NTFID.SDv̕yƕͲRBvP.""W ̤eDc2gI}]ڜ-"rE4օmJ-fjJvJJƀkw6I;.cE 6 6{ˀeLr56T ݃quJ0)#B\ Ҝ[pԖ;h,wEҿƥl *CwRujvo1LR&WۤucDJaLk2ؒ²ra,l6 e0X0dRNu,(`gI= $9X;] ƸI3>d""YDY$Z'? v \6HWre5W2eRuːDgRJN]a,zuDvȇek;pYpͶY)+YiR QN{I I$v 1q.Œ6(UAyo%n&I:q.4N q}`53~5y0XBժը؁wBR,$6r7E:~ɼwh6;rY elQt>wGhiIByq9ID$0\; uMH$*yvko|6"Y46I(ޠO7(Ȏ eXL_n=)wY%WSD=TYɓ&}6I 8gTkFBBnSfLJoͨD%oQuvz@RQQۘ,ztM:]H"r%t7q:;9+\ȵ&yVs'mh3Di'{z"9J)[,;r3p8hjќgA8Z?ɒ8- )G~],67ҽUyJ6JcOL_~o™7Ёѓqq>E |~Xlj|.̰?m:?ׇ{仓Xي]ф.[wv94}X|οؾξLve̋ХMG[Lx:)9t,*tApߋ_@DI Ĥ^ JBXlkU ^ڃrct>D {h)9h$xyR66{"d?DKT,Vj>sSW/ߝelXҭ!NvX;{Sўw,ȃ9zW<7Km_+D<€KH>zB|x(kb͜E<5a +Qxt5x}+僕X*I%x~o`AiWfe\M~Q>Y eX"1v.ϋkv/ӵEvPʓo=&ϳOK_ K!uȜ,T 'ٶݟYemhnp#4o~O" sn3VE z峕 kj ˪GSF"X3;k׳v |% {CF,%;EkXz=+{@&>{7%uLJN?=5Vgղ[wnflj8o"̩D41 63JiG \&0ytqD~ޜq 4_Yo<hy Xih23;S 3h=?2 ;mp?|7.z͡2M`̒-c&004{j9~p1K2M3b 4]~5aΌ%MKϥ>}ٱ9o ooM_̡_i&004s* ?iM``ph}~hP N`_)h+ׅy]qrqyZ9/|<1lXְKV:.?,&n ~52֓ʝS &y;cL3km P}\p|{~;wmq6J9 _u|CcKtל<_#?Ds~;偄`GT;dW)B?E2YWɧ䛓<N2EGKD ťl_RYIm#hGJ$r6<&fyl4ьRiN24}XE®ֲnC_jz ]H2ى?bqTdoJa`Vq?o.F%Iܸ?a=ܿNZE2_u}l?r 7R' *ɢɽR ګW/lQ>p8 \p mX^_Fϰ20 wqm 굡^xfErG(Y8SqR4pKNqXy{|#wq|'}\p\p8rOJeѯOb@lQNHw|TɕB.9996a? |W{28qف# (r9uwbJP?2/30' t I+^<ܮ}Np&;؏D8 iz]Jĕ"bGt'Ѻ}>7\17w<ݽi΄aBmn"N;^ ye3+iH8#KeOm%wP'x\.5=6ŷ+s0s]#o,XE<KIf S2P;ɗS\HMÚ=0jT053P6jDv4&,kc> z{_me~<ˬ/Kǡ/8aulj0.<6vxEYleZѣCs`|7Z4]M|3svTN3eԡg@4E6NN,ÀG#:P,/z49:=Ho;^b6?l= Clt=DJV _Wl65FJx[iҐ{ӳS]{C7Rrqy(G5Dnz.xA|%c׺*,}]ĺB)*H/п<#)ꫯR~Y5ޗx Q6"eώT{C0)ˌg۠M͝ǖ1mAw7#Jܳ1MpY3N|7uS:ŚPCuOx29E~L KС[[V2{Q@m)֪8F2!~/}d7IkNvϜƲiz}y|י&lw~ǧ[ۿ7!nyD apk}kы"P* ?/wmY6,_|ٲlyB-GurYNcCsR˕GD&QJ9R\@R9 خK"}}뤔JzH-:@Ytt*帟_z\6gۧ:7a(6|3oVC[6(q6F@;qxn`mB/>Y:%D1MΗ3in#yq)LN! ? ^c?;Y0y' R-Kxm`=|/&xċ Ȥ62)%ע۰CWl w᧩P'}meYq+|8f~|ߤ?o"rMoRх߽%iV1rʫ|9w8{z3 b&UcR;Tr,2Q5S l_CSyk΀P6ľSD:})Y QOzC2$}-m"!2Rm_M\Ӡ1uz 6&<y !;9;%5eT]ҽ1lI%elȪÿiZ^uV6$cQ$VvS:8S ErGAƷSo?2T"~ƏϏ桛R~YYmR22</e\ؗ񼡱پ\ :IAkT`ɽ,:AtLMㆶ9x/;V⺶=DAQ.c,nFUO˿ x Z~7E^zczGzR9=Œ'eZѿz> {T7EEDDI1w 8] ]x!iM0&F:緗3WN"RXҾ\y/>OGv .nan%}M9=3oPG^bŧ UYFD$ m>9(\Haj`5O 7!t*eDDrPo4z7` VoKپXnMRttúM "oC{5ˡ}InmH)"" "Vʹ*vLrԿW߂RCfm4uc\E geRfIpBl!6ŨEDDrYD*  O;ifa%M#-"r*lէN16 krOgY1mi씓)!f-;#"rM6Is]H )J׆vmL2{{TK{_D>>> L.t4kB̃mYX` =.5Ky`wiDDKqqq,_M6&'veYA"`,J|vl[vbuP<ӷ4\{zzRaSOER.h KzIT)[DW2Ք1ɳ@ !QNa8Ue5}xfa2fRgK{(`'{d+)h;Ĥ4N+5ɫ0X("WNcjYn"c,l6 ;{T|xHN}͂41@`[VR(N:Y`Jӆ2 l`6 q/ryҮ\GKD./i:M{fr@m:i8[{p-"'㾌'+ilvRv%\&yKYڼ\vzRw6izMm"6ODD;p>$_ha|LUeY،qVݕtqw ""WD6!;[m)JV!%= ""rşL! tNYeYÕ-m a$ɽ&)TED5MOvڎ{$472?:2KEףu'xަ{&whɺ1=xgb;m_W= [8s}W/h[v>ղrQ6vnYɝ ԼL#[\ڤ*?ˤm; M.=3?È:Q/-1V^ OԈ뼀,_xFWt%& uirb!ͭmV-ԓ/rO]eHs}Az衇#Jn\'=C-N&W)MAwѴJI-MyuHGJ=FUq1y޼8q3.QVδr8hi %$rj<{W{zP~ p>}G|  K͎t|}ɽt~ݭ1D&wu`٘'#b g^7:D3zMR^͋М>}#4N1y#$L 7G6?7۔Ϧl~o77Mp~;{<3wl~8tξneyLH<ף}_/a8z{?c.~:Ѵ>C[Ǜ,בK<}ݲ9GҶ.~~LN1NqRNyzǕHӆmלi=W%|{_3ט='c3Kί|ִ 2w ?k<вyaxvzK&,ٿ#swN潍Q&ȯfM9̩/Va6D7ϛQƘm森͈1cֿfn)cM;M1'?r`Ӧ`LJo2퇅(cL摛ڛglG6ie8 IDAT&3G&>sTLP`SsWS&䋻M8clcƣvL2}Cg}c-Cڛa!Qٖl6tj>k1yry(V3|s<ٷcӽŽףl#1mgSMfoGA.g(_>&\neqL2]u37=Мq9Lp@iOghBL2?{pK_:(]b7FW4 ]ر5VIk{/j,aAQ5vv{̼{gv;wSҵqWVi;UsAxߪGwP{s2L:yTدOJ%gtR Ge{uN#еongZ eV˂ϠTB)zN*wwӁ!,9,N.UKjcG\C]׹k-7wm/~zW./ˊէ{AF$wRYE82IMf$ơ3qP)Q:.w``8Y;PN/zy̖mZi)܎•]V2[8R7q(Om{$(/M:ա˓8^2n*.92k,$ +zHys$M#q#hd7-HܕG˾>1`lw6>V#NMhZ|wRH$տ0-H'3žHexsv=CO޲]DV[ۆHyȬNjtz}\-u:i8SqwX;w ijfug\cK^%.Es=G=XJh-T`@2 Y^wd/_0J}ũ/WcoU>k-^ՇzCR܌vQƌ$L]anHJD 5LH-IUdMOLtxWJ*"dkJ"AdYH])&Y≞$)hbX=>JXgwogx[:])ȩ۷HcGg "n{SFŎ;Gj { } x X|-mC$A پ.Pwba$8Mn 7; !:Mͧ`낋bX YÔD$Cq Zl]rנV%%|bINX?lj7 )Z0%S<̊ 韊;[<{FVAddsp3IW=F!,}q;ŀu΄-XE<23 l0-2v!a\(f۫d̐z|awފOoMcKbX++EFR WYoڲ +`űvqֽ1d[m| Ad%P9c ,صyKf"tmJۼss'.'|ZToޏ1yJ|νSЦ+K_ W2woX.D{gw`Uo`FGft~:{?"4:#c\=R1+.a\ +gnZV96on}ȃfA/Oʩ~4Q%kBgoF2̯褆hAS*GLoRL$ׂ [A?ia1i_\4 AAA   [AAD-  "'(#-0;ywv G/0ѧIW  |UăfO-J3&G  ")#gN⋅p.L͎iS#m w/aog\'ekc}{  Qt?E!~BRKWٖeM9hHevX-])]%m7`A:?Q{NϜ +Y㭃oʩ~4-V!>N6l5qt)7Hyk+ obҲDX#yC'3)5w3F2&:Mej7G2%Wٖ=q9~OFz ȥߦ1eCRnbԭ\RQM{bMvsŤ@Eq7#AT@qEw4gJFJx{ #(#)SFsHM?u |u 6;2:VyH&iP%Ǒ b2A}{P7%.XVcf+'CW lOrEɟHY\3r29?3 OjۚCR*yƕm{yUv%( }꓋mBQɞ֗=@GƎGލqR<#M]uP.sQՍS8AҦ`T:T\w_ۮ.TSFW\"!ޚ ;/SsG m懷܇Z-GJ"}q(ﳪ{oDcs+YsXro7άCPjAFkd:TE^:-v| TXC;h$> 5&>mD,iRk$^(#Xд&Wi5к{8x5W5' Gw^%9~qj )773]]|rf?ȕ{Xk4c4A/m "f@rc)L33jVeP6Gi~?0wj}Ճ33bndZ# ,GErt"` FIJ:s(*dmK`` -l~DijkyR@bi$F/:BMT?Lp/ 奔 =؃ ]>֍\vMǣ浬2do؆c5BOqyhl;h#a Q?Ɉk6 g=,Yu73V?=vb}p |'5v*yp:Z WT EV'xQoU{ew XʾZLbڃl΃:ޏإ{s6c\?dE{'ӞS&dw^zsG=  JCRc(Pscsobޣ,ZR#r[J tŠjyi6>6$2@Ld<2Hyb+S 0jAq3F1dؕcW_r≎C;iQ 6V |I$荝K.QYc!Q\[*wwHCӚs$ڈJ,҃)poqLpZq^͓ށKdNuz{GCfo 8Fyiҩ,?~yr4ݏRmGZx ~1I>suo$,+A57y!녝BrkK:Yѿ[uYg,wU[9bՒ帚ջ1^FMoKcn`h$nF|F힤s}GO-ӱ+Ir k}Y4 iKg̤ƘI79vߦk%ǶA H}"ΕRlBX9pV:6u}?Ԏ8"vP[EjJ>n}`OM [ʭ1,Op&{-#NA["rLFFO+w p1JD]F{*$YTTG&!9RSx+CWk>^NHd"%*=/ig SF;So~ǎjswE ]I;MY5Yěm!d /'SɥcAlc'82K U)$AbAb&@ Q׫Gj=I >=gm "WZgi;chIdTYs?:#.bCJFlA;$yfSG*X|vm'ҝrй+VF뎰iU*EԞ6rq'(Ԭ#d]p?-˟GO]7=E]ߊS/CΙGTq NϞ!:Mͧ`넥׬f϶vT-XH.9kP/Cq $}4 '#+h@&Aj$MbޞiJ1<_gѾ}k2eei5X"q   |Db[AAD-  "AA`  `Ov>RɣG A |s 찶H< E/˦rN^^^"A |Hȕ+H$ g>޽{QH "(T[/F@Reu011]4 W}B$ׂ |5222D# %HDr-  [AA`  HAAA$؂   lAAAD-  "x2gtiH``0`ζ+ī:z;#֓ʬ_&p|dVEWO)z8urkx+S\`d59r /GrsR;'A1Ne9r7c߉5oT+ é@e& z?{XHSQ|@ '{W'xCTkֽBUH*rՠqwO5>R*7ZM~Ҁ5ف:>Uq(1VEoitkk"/ʓdy A᫑[s/ɍFRss=>V],^jB(wUz 94nJ.x^a 6u4z׶Ęx8ײ>~|O;blmrQiMJ`!tngk;/#:0.#YpZrw'5 9(u:4|^{,rQ5qO )773]]|rY̆[Nų oZ`D4Yս7[bXӱ9aߟUE$$ [\sn)'cPkfvxTq'ez(u fa9ru;s$F Yƣ^9Mƽ][ ?Ws59lWFꨵ7]'1*i,fqQۘM]4/P=d @yr߷]o],b DF?%'mMtJyjM5zʻْ::Qo>ߝT?fKߞ}t4U1lXHզhӌ%w35?ToܝYr,(?}`KP3=sfevsY 'E9w m:#ҏh2yߙʌ;yFmC`s=L=m9';-7 iE9|WFtc=,t7{wX?Ͻs6ٴ(%wx|^GK286ihxnjxDzB2kqKUÙ!0H0s g+fv+ˆQ#~#\&|76$ǞyTe`\'.*ުTQ[27lC{FUygp1ÓV?Ϧ!6&e964O1s=mL&r3pܺ/eetϵ9;$i;owΣd+?̹xNRXcw8cߒP`>Ğō\Mۙ d IDATT*c\vMǣΦbQdq:n\%.ξ j]cjSof@;`蚸{ 3͙&2[c_gq 2sbmߔZyMP?Ɉk6 g=,YuW8mٷG4IG9}a#V&4%iܬEm$^ؠ B+d4|={{Ž|-P; m܋jsf_;3 eW3)._ i /un{̴-,1Σcx\8`+oxFo[+ȅk׮-]tygP/i!kZ6""4I|J j1u*K_=CiƀԖ<0J!UyqvG$4r8X9%7Zz MN9D _GK!\x\,5e0_E85eކAzT%ꤣM v߽3h5b ވRABejɊr\M\c/vݦgҽurHQ 1vNh$nF|F힤ԵȃR] Rخ~ƃw?X xFtt¥:2ޟd[bd+ o]/$2[[r؀kҤS XCq,hʻs*81Jj9IQyTK䷑O4M5gIg}{6/6zc sdXH|p'ĈioqKҶJnHN5vr%nHJDX_, s`N1$&I_}tƼ@}OGi<3uB2FϜHL(rprn@K4Za.G9KL 02HΌ)# ROHZ75w#)֌°~t#1s&MSm{tZ~OLoʣA.x`!XT>HҙyQ- ꍟF ڴhEF.ost.FKJ4ݮm\$(ڭFH=l\BY SM2w|pezR)gRws7ŋ":frGkMp~/OM';_:sEIRdNGu \ZQAfPFT]9.|;Tm̉3e7+[҉0CάΐxnLWދ3V$~B녩P>SCӕ(C7y] OGBZ3,lG>0K;Di2<l]pqS_}M5ŀu΄-XE<23 l0 0YX)1yF|TͤSN2W> ǻq &-GymW*K#lPOK#@ ~2֙I쫱yb(T̜<&6>ۧټ^xYIn`Xw'̍13/{b4@:wbQ Y"qfw)1B(k s2|L/a -p&rl=?s,:sr!QgQV!~i)NaKPYBԲ8#3X*xӦ,Uɭԭ@1=hB- cC,ҵ)m#šRޏ1yJ|νSЦ+ؗ[NJM:{#Wry$·_Ej!lW6~ABcέˁ,t~:{?"4:2K#7Zb3ƫq}M'| Gqa4VG~㡱[#ȶ \ݎ"UNl@+PӋii;qq<87Ӣ"} )h3PXckeDùՋ9BiLlƊ3Y<>R75L[w`\=R1+`%ؖ%<:""Sp'wu+Ce +S #c)R,/<*yx^3{+4lZe)%Ij-!"\n So/U쾟*g250/6L8 EKR/eǜĨ,P#wY4?a G[ nl]ޅ?w<H3œd{Wå>>ԫV?~m eq!!;&fơO~fVo.qJ.8}?83Oǥbq)Kkoh2gTw3gߟrʉSQ'9p۝je^' eo++&w^J9Տ3JdM(D'%Js7BV9E}C&!d IOO\xɠ *'8M}E^9 \:!Y=>!(wUz)CAOVug<>6l@/K> 'qqqfKLk:6'-m:vM]_X;[sW`SP!qAx4|'qٽAp hҤS XCq,h ҌgDG')\ۨ!x=-;x^a}za'ڒh?#V-YZ PkeԮlA#Q'O6-H'3"Uⶐ 5[_m%(Pɬ(xPU}/ һ?m$Ӂ!vi9ymDV%epZn^8J8&7+Y,R6ޏ UGCk^6-8K, .Mmq(z﷥1Sk^uIͽbKn`W`  &&?&>xF w?:?'aSnHJ=ƯCK2r=Wސ<*7]vԶ1}렜m!d/Lr20jbI{Zk_WO2j@b&đ/bHJ^"rLFFO+]{ br͍T4I*=*[E|d^6U{h,,Gڏ{fXe"H["{,r}eAwkh;g]'}d&c 2/\2,;M./Q?Ќ1ŭPOK#@ ~2/,vR#`aK:xș?-zV$~B녩P>S%`bX 덼M';:Ғ,4!(s&l*!Ua`i0!:Mͧ`넥׬fQ2 %'p 6V(u}\pq~4  :R)B~>ԫV?~m =8MLmO.}HkV8}ys(ujbuoz7:1:;{m&Tr؈+Pm o-_`EsX7` ֓{̭!Y&F!w錬e$_0& z^{jTMtڗ2Çotonj~s#3}wngݱԾ2x}Χ$jZqAr fbɚPkuD; /?   lAAAKDAA#3؂   lAAA   [AA`  HAAA$؂ 1]fB¯A$؂   lA?/T?o»PYNA ^q MtJyjM5\IH@%F3HԹ:j-b͒֨_C;ӠyERnnf\|W'#HUgUlbM愝W^VE|7A /%Wb9dӸ,DŽc2vCX7wOg_&3~;i;e2A.9fcKYw|Wwg޽&"I8͏=g86:%cOf]M˟gɍE t/{QZ3-'g) &`tܰ_zP Ib|dBްPXƄsb< څRO- 껖GExwXKĠ$һ?m$Ӂ!vi92n7v.0GJfDsL@#q#hd73Aq+d9hKЦGs)֕}eռɵ-ZCejɊr\M\c/vݦo7ֿl6o'&%ܩ4q";S80a+~=%+QƳ?z$+'kpӗrzfe~|按HAwp1hp)h*׈iEObɫD"RϲHͰ6˜HH-\(b:gZR6)R C,k#kMpf;%6fȸawވOiΈ1,{ *勝&O%UIjaUS|_)\9y4@q5\nMbh҈8ʌP\YU,CF`[fBjWWLm̶ _ss'.'|ZTV[+S$έ^4J"/P)D%>ީe hӋU/fU \Js.LB#\Oλ_ȟ<']VFiھXQkWJXQJɍ!9-{֤ut8R ui~h{YUfY IDATh8]z&(*X+ņ{b/kc5Ʈ{w(((m)~ |j>xr̼3rߝ{o PYp:Cx!ssȩ)d%ٵ`Vچ53T4aK@?ҧZL3\t`vIzb\)}gxÈp&LLH m:`ȂWI Fvl -edߐ7N–#8p-xb&..MXPį3ᷛ:r};(Ĭ[ǴU ҍi0ڀl5kƴG79>_m 3;zۓL*==Y|%nv}#Oy?Wnr4D  +!! ԝVū:  !?`۔xVVM`$zb`H|pEq4ŮW?ñ;1D@ep5֚W`>sp%`@<=4^wB!o,r(Cuq##ӧ%Hgmؖr=+f35ML`5[B#<&;dyO tZ=Dͬ[6?'}!B! Ǡ*T+/ZV?%_ ~_#9i4*9V:pʔvQgmґ2wu.v1c !Nc:Y"SXNdo; ]QI !BO|z>ϫ,ɵB!%?UJpC;%B!/zB!@!BIB![!BIB!` !B! B!_OlFãG^BJ,)ABI͛7J^#H;rǏ#>yȒ% VVV҉B! ?ۘ쌉 BzB.*BBfoTR`kZˆ$XXXHG !Ŀ'Y"E޼y133CRI-^@rrrYgQgSSSrȁR$((KG !Ŀ'Rk!3!P($BgPKF<,*Q|_mը;&e7ը^TӎN~J? $\blz\Ozn4 Mj{qWeVwSƏќe+c_4]jZT3yY;0A!©lSAmxdKIɂؘ7cWRQǣK[>b~ wL "='ϧ -+kqϺ߁<[!^hCꥦLL(eQ#h[ZM$oم@:yM4HeQx4Gx@9yvqԢZWF|?k?IMPZ|LDb*Sڰ?5@¥qTW)y&Ԣ,:McMnS2j/ˎPm iD_ZfU(]*W;ҩg"&Vi67'Q9Pkgrm͐qxF![߮j5v`^;yrr>}.KCY}%6{>fvn5 2b[(ZH^Z͠k *S6K:z!=d}~2`4tDZDߦU)r$Cʬozؾ'6a֭W4HVF]2~#7s?]ƃ8jm1 ʫ)S]$*mikZcX=nL;մ=m'ұ^Eԥ*|Ԗ1[!';?ա9*Y;z64y}nUtnCլ kN*ψ9qy ?m۲4mׂ*8dOŦ[Vt `-[[Vu}ss!=-֋:Rkwb xn ̋S:7_ɠc8m]DXgn>b/#{Lg%ŪWl~JF>+.nژ5˩/O~>9OrI"zTdA@?/:3yPuz`a.Ɍ `l L}*WxȜӸ՜4tquR(I34,Lԁ==OxĽ\n>b/cF%kylݷ5帻h*iI?{%ڳαR\T~ Med瘮,β;` Lhk?dnݗsR;oeٔ% UVNMs7WvmehUiwža}Yq7%?49_^4>0]K%}vnL]xퟸ?<[fl c?CζT&11RFǟ.aB Y];`w/kKdB̯ª }(= \Jl7U=Aacd2T璥qƁ6;0퍕˞JLcj0AEsyz봘mҨYn.B۷≮Fynԕоcc;*xvl)lZ@n nӳ߳iwzjz馵JJ7 gt|UL>3hHRFGk[~ OKO-Oآ?-7d0mZqPb[-_q,H%/7EW>h y-*ۯh7+0D]+Փ#22e Op\LPJ[gP> אNDދY 3뼔vmCC߿]" RavCz$:wyƪQF5VYIWJs("brφՋ f٩P'xX'2-OaVȜR23b5"d!`쓏x2͎Ջ\t1P[F(J&Xdʗ(ҬW2ςcvJ,dJbSa1e͉w)piC\h4ɀ,3y2^aꄻ˶)uT?&b#R 3ca$٦Kyvӽ8XL1ueH- /%Y3/.[t<`m3ش0aJ ȍţYz:2#djݬ" 1 Y+JQ^ 3anN])DN5J9PjNп#'S) QC<1SIc.5V/YF( "aRajC teV aAXMI^&Ͱ8.uL!,c䂵IlTSmkZcξ8`LQ[Ld Ba<[vD DMi{kAd. *MKl~=+?m8=O}Q j Ði9(ݲD{>̟3G2.`ls ilվ~NQcԱ7#_kE_ :Pz=~U(]Ϗ#е`#¹ugl#XM)q,sOwl)|W0c)UWg3{g 4\Z; 0POԥ*ؗjDG,%^.1hЙڒ ER8V.xTނ| tj*[oY(gWNa}G} G2#O]ϻyȩL(qPSO]+7hBqDZOcg~-;jE@6NNQg{{YJͻҶ3w~i>Q zxz~|[Z{iut[Edޚ\+7JBF:ǚŷi[vQqt0u)ظ/>2څ`_ҥ[?7oڵ,)ׯD >'V3g#*(Pӯ&y䍴v1Z]}8nwW(i-2ˋ,'sfҪ{ۖ3#XHl$)T8{w}`,\=I3'a=ar:m 1[9U`+xZkCI3Ʋ5#%&y2N"2O#;*>^ܽs_L2sDgA;.Gѫ#4m)*_X~ЗR$ܗ*2~5@sys&mfŵr$Z]E΢pO~`-L3ugt&__3Q}j27V72òۉr;pB+Ho^D-FTIN-iu'+Pu{C9u{`p10}a0na2^P(3ӺM%p$89v͊aHyҢʐF]Ìv!%>(07ģ\@t\GyIH3(5BХS '(,۟y%³?`]vʾ6_Ic9:}w~p4 v ٰ{D 2g&>(;5UDtX6[bL"ZJ!BI?4u.MF*JaS@껧+;6B^@tБ?©`:Ny*? X|MUߣdCE6tNd ]Jc_<0S+:б}bn$sʔ9 L!rYϽˌyL k>Lnܟ(ćؿ9B'J@m w)ST͍U!7fpyoi剣]JOFT3'NP>%.[<{8`^_8'w5c$WlX7|K!si=V`% IDAT[ʝF|mmc*$NNQu{شΓPuiw镝z4>El!?j9), 5V/ %M~==l6Yp Yz1]cL.'SAB:Xr6GIXL7˰\ћڃ^ :]Y&/v[a1e͉|@a[NBSAix2͎Ջb4–z] iXx"[m(1N'٩VǁN\$dl`K/UG_aW4K2:6߿y_|BmllFKLLdϞ=L>#? NGxx8xxx|q7hЙڒ ER8V.xT$3oY;Z>=tr-z8k{ǖү,J~+A93=EB$ ()\qSnY_/VNXD]d`#8r.Gߺ̂| tj*[oY(gWNa}'ľT#ٳގ(Ps/YJ+ Sc-Qyk{0$*֊*?9Mr%JGF3a`Dbp,Hn~ W0ͫ0Tgc؅7danjқړ̦ikJ_~0|h#6Xۮt)6+o<_7'w 3NFŵ Qb68Ydfa;ԜS [h^Mڟ^ݽgy_C#1˂WI Fvl -e^Dzcܴ?`k9߇g7}Wh4BCC^BУ칝JsLF2y"dOʊyJ!ė,"F,ɜ*޶%L(<!Hw}l! ZLX!HH-B!pP!BIB![!BIB!` !B! B!` !.ONh1ck#z?\8߷1$zl#;4JY5juY6Ƹԥ|9~>9'B: BReHAcoh9blpM$MJsYC B B$_Nzؾ'6a֭nZx|ہq7$afе^JNXݥ w=#9(LZ˝$@Ρ=_!eV@ĊXʪ2ЧY;͐mPj*4T۬{մ=m'ұ^Eԥ*|'}aN?K2W=-'^~`ƌ 9A` 1*RJƫRc.:O>yrr>}.KCY}%Bl!.ou4?3yPuz`a.Ɍ ܺ/a箥twʬQȈ[Rѳ.1՜4tquR[:fyw[?{%ڳαR9u0d!Ů;^?UR+_*)ލz#{3z/Yc]/ES7Z.tl$Slߵ24.hH7fBIB|[Fa- r%ZjgOX5ۮrc:~OypsLȤzbbREXXzǢMĺESY:ٱiw oi剣]JO ߠrQ'_r[X >9<ӿUWͣ$grŏܹj:7 zL[5 bc- O5:#e'm5WMOsޟ*` !>_[!#fd h !x j~9ӢKälObvϊՋ fr2%Y{ akY7ő|unGM7=댔,1LycxQ7<]؟:ⲧHcIwyܦҙ*P"7} dΈ׈PؓÆh"yv?+l:;dM'fBIB| [RXk#\z&< +F( "oLO\=4cN҄pbc@ I7~k\5rXԜ)锭 GR$a6kAc1,gM!Wcx=5~ʽcK{[ng@brjl<{{4T`w` I:P*W.,')/Efg:8J֛<~ٕSXy_cFǹ z[zƙ[ӛwn^b&3%pN\4IVKV#5Iu2!ȀO`xwMvB̨>J\Rgn~kAz'BMY_b!_Zpg7#`i쯟WKVe 'ugYG̡WKt15cn_gҢWҵi lrlA(_o7ͩGgX9j6Gc,Mv%tlG4%p0Ȃo_Ya:\unixyU}5?.- d?9z#XGr~U)I{VlA3#l>6;Mi'Thy|zWʃL2ǯ-R{%B!ۧ["bDcOyϯ=}  U+q\>L"o'^n,eܷ.N)Y|En+xu#ʆAKqx/IWEINPϕA$;Ǹp{x%кeKZ÷Ig'3r;jT%l9G+w뮯b[xu>w7 EhЪ1jkl5EB!_jmƎ3p.OY)Q0*PZXJ +:8n Ā;KgFew]п=5J Wf '$0rY0rx/ .ǾciJ,ت9uu qB>~DRRb2uףz!-năi jFn3OI dn0!]ZҶz4'q ;WGƍO?OH6!6TڕfѽwЦ+J-BA>Qs!ǦsPjP'q{ZyR5LP҂Ge2bmN_Yh.:s'2Xs5IfS rQ,zk®(ՋrYNC{%֢lԻA0? UV3 @[~ה\aEOg8O0<_뢲ό0aew>?>y@lB!_X̎^Y^Q6'oAKV~mن&r5+*J_uJy{?{3v6/h$*\m>|_aKj_a~ vٚ5^0hnflNřRPm|YlH G[2' o=,q73F3)v{ C/JS!B|q .;2@o:[r>19/Z,UJgϞ9/l"5%<0V)KdMMi.H~ĝō87)U 0'o̗wـW(wyֵc_?6Cċ8etіVyr𐽛N T+)ʔ!'q)>+Yr%+8J2 ZpwFͫ)C7!yղ߂a|{WHdNtMJB!ćWqGqeńڮ !BlOn?S䣺 !BH-_#Xš7ԭYk!BY""B!G!B! B!` !B! B!Bl!37[|b>!/&qWX}/V5Hh[yaqΔQe83Hq98y|MJDFFJ/!ɖ-GrC22"(=8s^fnju^-ѩ6/YQmVƗA5r&K{V$^V.嗠D$.Ν22717ٵa+{ӣUAoа*"モe71}Q+7xfnKuӄR)]5C3ZwLznm`긅.s1vur|"''3fK!e8f{/ևBoQjO bۇD]\{lJZ}<.9̌6?ud͊L\ܟ/߿pݿSIŴhsֿ/KjcppDNeX8wnHΙq(l$(y&ΝJ^"XgxxnݺO<$\t-}ͅ췔l˃Y;kqj-4eJ+z^9ӛ}kxt4b"~XJ4CkٽdKlc[I>˙p/mB>+.ژZi_ |> ֍^8;Iqc)ij8'3N?WﺍA6}\1:~;FjL r:4omׅ!^j:uɢU\uT`6 rL[3 e`/WZBlQuتR&/\3Ged:1.^XJSU{<.oϤij<^61*,OcSDMa}]_ iW1[]箂T璥qƁ6;0HqiP5buJc_ܴ]oaϡy qpܨ+7}wTRشdYk5YMXu'÷ݦgg:\Ws+&I /yfyhӵ9L *-Fk}.[O92'"[ ^SCy'{{ź1:kx#/MD~sP(_}g,АleBSޘ_+h69[Ƣ5\- \|זH>^ZOF *wu% OKN$t8)2Վ$#m4ߝ>!,, }M߄ $m$hț7/fffT*I_>Nt]7LMMɖ-gPs+JuB#?1o.rgABXДvW 3ۆ42Ա#IrK69J9Z/IGl邇}|5W*MƮ]" RavCz$:wyƪQF5V^IXt$ QiEDR Vٰz,;dO{» wf":ZW%hx%u1Y3.g]:$ !vq3{>>/|d"B=pF( HPxɦ+"݀⮲е{bT%,¹0'uts#w}Ci60|J>M hRw b*,! 1B1`܄ 8 vwS}&} j99tyMEoi58-DNT( IDATp_pV}ٖcanrwюeyp=-g0 H!Ap*cQӿ>p-"/q+ԀaS#X6 ħԩIJyJv}/g)3/ma-8@G~ʜ#2Jvtk?vYVM3UhCx-19O_Gr&T7Tag3aD%\Ԟ |]r]!Yx8K Ôz{김ϮtoPT.:JZ}%-gxTnT~zb^dyOb6FpT<8̥0i?d7zR2qtYWssyr_.☻N'='Ic]G[DD|ɔUffIP̝1 z|& 0?B~?ЊEMCP1VrOw8GNpwQ\HݴXرi#hָ_З9TQSf(m?A9?Uҁ9n/+Ѽn#>OS8妜܌_u_gWmztŽ27`PzOx.sW t:?*UC8#fihРUV۝O O+Yԩ8z7b(&uHfDD$(uJt"mQK9\:;%4DnﳗFB9ou뙪Ɇ[9(`(`=Ɵ4;/#]bB<9㧳xT3/a`}YQMuxйF@I)p\LSQ|mWXz4/Nܱm0GSdu8¬?dt1QTnP`'6!81{> u>rqhH,XM,XP#"}q v)I@ǨbrJ:1q4 ]@Bk=ó]bs*5䁄}J_MFQ +Sޡ{ '^TRE$k]t??.]:sDD0 ,VF,x,߇)T,)pV M_R0轋?MGIvjo.du?}[($㚵bʑ;kp)^[{-7F2i֐Q⸓RlgQ{9q 7{:ͮt{l0~(D{e|g*EY/*6|Vbup&כ Ɛ3,,k_V}T=80w}.ۺT}+Fzeo}B\ x}ߘޞ,L#r$^lUjf3au2`O$86n'\ g++~ɘ.ͨe6xӮ_}Oyՙc]lzsaǀ n^^LvާMpuEz9m1̖\/}rt7s:/ ZFjaV7Vszoϝ] ֥87C.=Ɠ%<23C6.iպ1x}'ë1U쬟84YDaߕgؒMlSĽ7gcyL^O^kѣAEĦ c輭 cX֠&;/Pnh_nP+]$3VCo.c9\^g] s_AdWѨ4AFޥId.Dvy {sOܛ͔gi1%M`8ErVO ]ufa|0b?G!ݎ./v1`qf4FC C}I^FܙDL%azQℷ(s2*ȏxu%^# \-]CE#t}! щx)I7Q/Q nY|(U{^uͺ1\"|%\S_#k<537icmfģ%_+^=H("rO2dr3u|);2z:vepnDLK3}T'1ds 7MX^ v|w b*7e[m6mº3Q9[6-0y检uz4WO'a Xr:clˤtW\qUH:XTlz.ĭ\/xYf'עA끬iOZ6Q2=7ߚԟ&0gԔ.xJO1z;{NcuݬgY3aS]Ėר҂q#`E?mDi0v*- `6kҝTɔgygF-NL}Mz0?Jn$p<=7o1Wd9 ɒtgTT ܑU -87mt4h@ժUyrXe˱bԭ]H m?aĺ٘Z SDDDDRt>ь߄IFB0s4H[DDD@[DDD$+ժ{>$"rWd`Я1DΓƮ]8xO'"rWVCCCܹshD䎓pA*U(R$Pu=_ك O:E||FADjAAA-Z//mQ\sED{W \vNj4ª[sZ1^~g/7\>-ёihZ#qb\VL9ۙEDǞnh?֯'Gyk??>E.u|F› Dȭ>6Ճ>aVӂv,Zr աϐʔ*eH*O^RL`kQFɄ# ~ɘ.ͨe6xӮ_}lD7~bhƪ+p $0~Cuܣ:r2pa/C5m0'`&.5c5SѬM"cV*'<_Ò<bQ|u1ϺguU _OyVToؑ>l]>}7fAgpW{}vbc@T7Qu/o; %<|8VO{:kTN{jbk@k#~Ⱥ=j 5g_CȨ?mƋ 7coϨuQgS,rs(`ȕɪ}Lns>38>۶ |ƒͫ -P-5 f~qXN냤,CaOple[[)ݶcjrr޻l 6?'N/}gYNL0 +3h](>ZhE,8z+O=Y3}V>Ss^eܮ8ˌ=j%CM@٬#G}<{C{+Z g)ھ9=B2bHK[]y-Ħ]:9"SK1)c6mY?PrFeӿ9釬CALjk6q6}޽dF>0y眰f_LH!+-"rr\f[=}A%coׄq{sD0zQWsƙ7b %< Q@曶 (Ɛ}x]evvPuU vFPXLm#4s>}&} j9,,L3c.˹ۗvk|T'e= @:^sۥlRcNrntD~7}~wx{ N ѧ0 Yr,gY< .Yc+r3_6s[9 LD\^)B7ޠ ]t4DLX#Q 9gEm0pK[Τ񄵩.B3=ٿx{SXsU /FJm߭]aɔY9Kdj<F~3'%!Yx8K Ô6W{} בg?btcf2lU7eZw.E,>cǐ;\s+ Q7u:9?C3jJ m'=1؜#FOMZ?⛻PSw0=Z4h=>Uҁh9!Ufmc6uS4w.OFB=eD >Lߐ/0 ǤT6eS.b`XяumyJr ?2]:gJF2H}[ߜCvf4:<_ED$,x)FO(dH*-Vdg]#.4.^PHvx.;|Mk1èۢ >{,"~?$BR1Y+ImmN?1ϒ6$y?!Җ}hdZBٕt/zC+uf#"rI;ƪg33yBn~Z?[g`n|}?-nJ ;aWs:.l[:RҞ]ME?C+""o_"Ro_n p̀Z}Z:͘5?yNj`6 ߆iZ,陭X;>*{^f؜j|yԂ˾sy I@z4觫a6}e؍f=Z-Y2V񫫦fKsft<=B3z,`&mp%Yfkcf3[̤Isۤf:-EiG;WQD~T6GZ/ӥfoڕO\9v]i.~_͡k,9#4W{{Fw 4"7LVuf6V-D"פŔ߸&lZs51dz9-L\qku1nw4V>O=~=v㛍hv%|ڇgqD|9;С!ak݋auf>|8VO{:k8cŸ"YءϮ8ŞsiS3_F;2ooV szD:k,SѬM"cԜ+:(`3C+r4ٛΕ p+6FGD"7#Ÿ@V p21t҉ϱaLڟydnꁅlrL?Fa=럳Ed :>cdy0< Z.JV0l=TVƾ"/mX:&'˦xuwl5#h^صSq su)sR#Љn勱xonl7Cy<Լ:)ݲ Rɷtv8nxPS]~a-}Vm!l1OOf}-<Ölbmʜ_,9WCx<;twx?pj$9\> b.hę|2ml^3w8%k9.lbt8rV/N^렀-""|v&AHSp:q'Nbŀ6oq*5ÛZ/{` IP21IDAT/dss@Nν s*?$lb񹻩< *^w=B)V-fw߷rTJЬQ(7?Ҋg;?Oi<+괗)e'z<%A.9/}e^?m:}s">_-'ϿqB,>9@ӓ2=n*6/|ٳ~gJCwC7q9c퀃= C3S?zZC߬;F5c%}7}@PIj0q} (j1fŧ! zm`IǞ|6=oVLqsu=8-_}Gэ~~qSTaTH?1TjW W)(  aLc*֍A+Q/2M_o|iӭmz4T>/7}lxR"1c)+϶"N$3{w4Ͽܘ&O?Nxtb,^a(J=iZ~Xze!uh=AeMGY4Mܒp}͟S"ٴv #ʶnQO|.{m 'ͧ09"E<|J1Xu%`W.̓{vXCN˟'ƚD1LWx'E+1mqf4FC C}Ipfu ~|t/q}ZJe;xv28SHNo(f֗SɇM hRw b*,!ؘ*1B1R~Y\쟛|'"1S6o޶d`F0Z1rk]wn{o<ߘyys4'= @Q%apGLNnj@Vm%UQFPʋ?w }: Si+Ap%d^xrӐ䉇a n + %JѦM9z)o Lk6J;~Me23:aj=vn+/|o>gxp_& 0{Apn^(OĞ8SLɐyW39<7r=b/Ԓ 1$bܖ&Z(JѲA񛟺L`;XxoB..\6HHîQ oLň)ԩIJyJv}/g)3/?vYVM{MP;+^c!;bEב\ DLX#Q 9gEm3r6:YOgвwW=`Vk>c'1uՌϓ~4zݮ :RGL0.slL“J"l\V՟DIg_qqs~\+sk*`ȿ& _p(ְ5m#ns&8AZ5,qbkjޔ>Ha%+;t9}Gۚs=+r!Lh~wd\>762w6\,Ka"7ǝbll7x6b4vlgZ5nFWfq0efNjGqw0gԔ.xJO1z;{Nc9e9Lߐ/0 ǤT6!Ufm`Tw0=Z4h=>Uҁ9@3m*^v|s|^!9[/ݢC>\?g-vU7|j,NL_rr/SSDBZ0nrG6< NAqo4$PvY|1RFSnz;6t#""wxb~JXɢNoik,x)FOq[ߡ=""rr떕,vCE+p-9sE[DDs>)}!גs)L>{)n/VHi?HrQQQlls,"~?$C?}[(0㚵bʑ4*_ꏛ%mzD,>#{^~O? uvW;-""̭P Liv-"r/pa/C5m0'+OtDf7kL|6Ǯe&._͡k,9U=W_g32ǀ n^^LvQKxp֭@Vu0WӣpzC"YءϮ8Sf3Ea'cM|y嵬ާMpuz9m1Z]ր-$G~vyM3a1$1]Ql])|ŵO97O1k)hռɚ:_DEDomv&ybFI=Y3}V>Ss^eܮ8ˌ=j%CM@٬#G}< Z.JV0|R/epª#-#ɌmߕgؒMlS =  NqsvNCP!6VN>cvSlڲdѡdfe2t⽵[bl}b>ě[\Vl’*p>L?h!o˪ON`tkga%o<#1nd:\9%"-""YZY)7y¾x1Xlg L۫xn i='ձLSQ:-能ۤ,JR1N lET*֍A+Q/2M_o|iӭmz4T>/7}|cDcsRsW4emEɝOIgaWYNWk7+KU: YM[ YWC8v>Ðd7뎑.1=xA+❇yI&nOVu>/;` hiZ1@4f!ކhCp&ݐɕlĝITK8 oQJVv]vBt"EJx5L-KrTBƇ<tfrȻt*;dz؟N&0' 樉݋%k]c3YɷB1m)[ؓY݌ &>.qΟ!5c6^wiɸHžݠg'†e,͇$Sv;؅&7U|TVu'.-w"-""Qbd`F0Z1ljNuRs%(L>]Zn|'";QȜX;MIJ]0ryDfo؂"J>HRq|ii*^*_r:cl|?v,zc^N9Nt?̅?P8Eb*,!ؘ1g)U䳧H)ݑ{0r} cȀBz 7ȝJ[DDDT~zb^dyObdʬ%ۏx{y25#cZOJ6kC诳q-{)+Om̶`#%~Sh בg?btcf2lUtDIg_qq}-SfHbqlޔi݂e?'8w0m%r\ƯHs)Uag3aD%\Ԟ |a\H<.zOaxRv0nAusU)_qmMD{lUTw0=Z4h=>Uҁ9 ^Es)}Z4.#ax`D~M&?=&)zCO1r`XяumyJ{,ozw_t+E|p? """"=p xi\>Y1|ҭwt \9 ط [wr,wjnF  <[kEF)|m?\tHVo_.#ijp}#5Rn~x%şȱ0'zlM'Rq73qY.)ˏ61ϗdkO@/| fcmφ%rna X\xcfh!ΒFY%5zF(,z@gƶ gI֓U`M(zxx❖L\6[ Ƴlu\(Ңy vs#a}pkI%_VLbxxg: /7#21kO5INK<~k|H Od}7'.1U#۝!Ȁ͈F nTZel˹- fK`ӠtM$_!&Q n9KS3Xc:LfgD|KkYe2s~ા~"AQI8l?β:Ɓ1P~I_yпaiK2wH;)li &==*GC_u9t:5EDDDrjMWkܷݓc7\m)`(`(`(`ܵǽ[l<2(e4Ě~ύ5݊S[DDDvswwwwF.͖՚Xv:XӭmLW- F7GJH'Ʋx_,K """""7)vbXd/  6`` 6l0`N|Nw\L]r?6aIENDB`././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/doc/source/admin/index.rst0000664000175000017500000000066300000000000020125 0ustar00zuulzuul00000000000000.. index:: Murano Administrator Guide .. _admin-guide: Deploying Murano ~~~~~~~~~~~~~~~~ .. toctree:: :maxdepth: 2 deploy_murano prepare_lab murano_policies manage_packages manage_images manage_categories murano_repository murano_agent policy_enf using_glare.rst net_configuration configure_cloud_foundry_service_broker admin_troubleshooting appdev-guide/developer_index config-wsgi ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/doc/source/admin/manage_categories.rst0000664000175000017500000000012400000000000022443 0ustar00zuulzuul00000000000000.. _manage-categories: =================== Managing categories =================== ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/doc/source/admin/manage_images.rst0000664000175000017500000000020000000000000021556 0ustar00zuulzuul00000000000000.. _manage-images: =============== Managing images =============== Build an image ~~~~~~~~~~~~~~ Manage images ~~~~~~~~~~~~~ ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/doc/source/admin/manage_packages.rst0000664000175000017500000000542200000000000022102 0ustar00zuulzuul00000000000000.. _manage-packages: ================= Managing packages ================= Managing packages on engine side ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ To get access to the contents of murano packages, ``murano-engine`` queries ``murano-api``. However, it is also possible to specify a list of directories that may contain packages locally. This option is useful to speed up debugging and development of packages and/or to save bandwidth between the API and the engine. If local directories are specified, they are examined before querying the API. Local package directories ------------------------- To define a list of directories where the engine would look for package files, set the ``load_packages_from`` option in the ``engine`` section of the :file:`murano.conf` configuration file. This option can be set to a comma-separated list of directory paths. Whenever an engine needs to access a package, it would inspect these directories first, before accessing ``murano-api``. API package cache ----------------- If the package was not found in any of the ``load_packages_from`` directories, or if none were specified, then ``murano-engine`` queries API for package contents. Whenever ``murano-engine`` downloads a package from API, it stores and unpacks it locally. The engine uses the directory defined in the ``packages_cache`` option in the ``engine`` section of the :file:`murano.conf` configuration file. If it is not used, a temporary directory is created. The ``enable_packages_cache`` option in the same section defines whether the packages would persist on disk or not. When set to ``False``, each package downloaded from API is stored in a separate directory, that will be deleted after the deployment (or action) is over. This means that every deployment or action execution needs to download all the packages it requires, regardless of any packages previously downloaded by the engine. When set to ``True`` (default), the engine shares downloaded packages between deployments and action executions. This means that packages persist on disk and have to be eventually deleted. Therefore, whenever the engine requires a package and that package is not found locally, the engine downloads the package. Afterwards, it checks all the previously cached packages with the same FQN and same version. If the cached package is not required by any ongoing deployment, it gets deleted. Otherwise, it stays on disk until a new version is downloaded. .. note:: On UNIX-based operating systems, murano uses ``fcntl`` for IPC locks that support both shared and exclusive locking. On Windows, ``msvcrt`` is used. It does not support shared file locks. Therefore, enabling package cache mechanism under Windows might result in performance decrease, since only one process would be able to use one package at the same time. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/doc/source/admin/murano_agent.rst0000664000175000017500000001421200000000000021470 0ustar00zuulzuul00000000000000.. _murano-agent: ============ Murano agent ============ Murano easily installs and configures necessary software on new virtual machines. Murano agent is one of the main participants of these processes. Usually, it is enough to execute a single script to install a simple application. A more complex installation requires a deep script result analysis. For example, we have a cross-platform application. The first script determines the operation system and the second one calls an appropriate installation script. Note, that installation script may be written in different languages (the shell for Linux and PowerShell for Windows). Murano agent can easily handle this situation and even more complicated ones. So murano agent operates not with scripts, but with execution plans, which are minimum units of the installation workflow. Murano-agent on a new VM ~~~~~~~~~~~~~~~~~~~~~~~~ Earlier most of the application deployments were possible only on images with pre-installed murano agent. You can refer to :ref:`corresponding documentation ` on building an image with murano-agent. Currently murano-agent can be automatically installed by cloud-init. To deploy an application on an image with pre-installed cloud-init you should mark the image with Murano specific metadata. More information about preparing images can be found :ref:`here `. This type of installation has some limitations. The image has to have pre-installed python. Murano-agent is installed from PyPi so the instance should have connectivity with the Internet. Also it requires an installation of some python packages, e.g. python3-pip, python3-dev, python3-setuptools, python3-virtualenv, which are also installed by cloud-init. Interaction with murano-engine ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ First of all, communication between murano-agent and murano engine should be established. The communication is performed through AMQP protocol. This type of communication is preferable (for example, compared to SSH) because it is: * Durable * To establish the connection, there is no need to wait until the instance is spawned. Murano-agent, on its turn, does not need to wait for a murano-engine task. * Messages can be sent to RabbitMQ asynchronously. * The connection does not depend on network issues. And moreover, there is no way to physically connect to the virtual machine if floating IP is not set. * It is possible to reload the instance and change network parameters during the deployment. * Reliable If one instance of murano-engine fails in the middle of the deployment, another one picks up the messages from the queue and continue the deployment. Right after application author calls the :command:`deploy` method of the class, inherited from *io.murano.resources.Instance*, new murano-agent configuration file starts forming in accordance with the values specified in the ``[rabbitmq]`` murano configuration file section. A script that runs through cloud-init copies a new file to the right place during the instance booting. Execution plans and execution plan templates ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ It was already mentioned that murano-agent recognizes execution plans. These instructions contain scripts with all the required parameters The application package author provides the execution plan templates together with scripts code. During the deployment it is complemented with all required parameters (including user-input). For more information on execution plan templates, refer to :ref:`Execution plan template `. Take a look at the muranoPL code snippet. The``EtcdAddMember`` template expects *name* and *ip* parameters. The first line shows that these parameters are passed to the template, and the second one shows that the template is sent to the agent: .. code-block:: console - $template: $resources.yaml('EtcdAddMember.template').bind(dict( name => $.instance.name, ip => $.getIp() )) - $clusterConfig: $._cluster.masterNode.instance.agent.call($template, $resources) Beside the simple agent call, there is a method that enables sending an already prepared execution plan (not a template). The main difference between template and full execution plan is in the ``files`` section. Prepared execution plan contains file contents and name by which they are reachable. So it is not required to provide the resources argument: .. code-block:: console ..instance.agent.callRaw($plan) Also, there are ``instance.agent.call($template, $resources)`` and ``..instance.agent.sendRaw($plan)`` methods which have the same meaning but indicate the engine not to wait for the script execution result. The default agent call response time (with the corresponding method call) is set in murano configuration file and equals to one hour. Take a look at the ``engine`` section: .. code-block:: console [engine] # Time for waiting for a response from murano-agent during the # deployment (integer value) agent_timeout = 3600 .. note:: Murano-agent is able to run different types of scripts, such as powershell, python, bash, chef, and puppets. Moreover, it has a mechanism for extending supported formats and that is why murano agent is called ``unified`` To use puppet a deployment workflow, configure an execution plan as follows: #. Set correct version of format: ``FormatVersion >=2.1.0``. Previous formats does not support puppet execution. #. Use corresponding type In the script section, script item should have ``Type: Puppet`` #. Provide entry-point class Use puppet syntax ``EntryPoint: mysql::server`` .. note:: You can use scripts directly from git or svn repositories: .. code-block:: console Files: - mysql: https://github.com/nanliu/puppet-staging.git A script output is available in the murano-agent log file. This file is located on the spawned instance at :file:`/etc/murano/agent.conf` on a Linux-based machine, or :file:`C:\\Murano\\Agent\\agent.conf` on a Windows-based machine. You can also refer to murano-agent log if there is no connectivity with murano-engine (check if RabbitMQ settings are updated) or to track deployment execution. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/doc/source/admin/murano_policies.rst0000664000175000017500000001032400000000000022201 0ustar00zuulzuul00000000000000.. _murano_policies: =============== Murano Policies =============== Murano only uses 2 roles for policy enforcement. Murano allows access by default and uses the admin role for any action that involves accessing data across multiple projects in the cloud. .. glossary:: role:Member User is non-admin to all APIs. role:admin User is admin to all APIs. Sample File Generation ---------------------- To generate a sample policy.yaml file from the Murano defaults, run the oslo policy generation script:: oslopolicy-sample-generator \ --config-file etc/oslo-policy-generator/murano-policy-generator.conf \ --output-file policy.yaml.sample or using tox:: tox -egenpolicy .. note:: In previous OpenStack releases the default policy format was JSON, but now the `recommended format `_ is YAML. .. Merged File Generation ---------------------- This will output a policy file which includes all registered policy defaults and all policies configured with a policy file. This file shows the effective policy in use by the project:: oslopolicy-sample-generator \ --config-file etc/oslo-policy-generator/murano-policy-generator.conf List Redundant Configurations ----------------------------- This will output a list of matches for policy rules that are defined in a configuration file where the rule does not differ from a registered default rule. These are rules that can be removed from the policy file with no change in effective policy:: oslopolicy-list-redundant \ --config-file etc/oslo-policy-generator/murano-policy-generator.conf Policy configuration -------------------- Like each service in OpenStack, Murano has its own role-based access policies that determine who can access objects and under what circumstances. The default implementation for these policies is defined in the service's source code -- under :file:`murano.common.policies`. The default policy definitions can be overridden using the :file:`policy.yaml` file. On each API call the corresponding policy check is performed. :file:`policy.yaml` file can be changed without interrupting the API service. For detailed information on :file:`policy.yaml` syntax, please refer to the `OpenStack official documentation `_ With this file you can set who may upload packages and perform other operations. So, changing ``"upload_package": "rule:default"`` to ``"rule:admin_api"`` will forbid regular users from uploading packages. For reference: - ``"get_package"`` is checked whenever a user accesses a package from the catalog. default: anyone - ``"upload_package"`` is checked whenever a user uploads a package to the catalog. default: anyone - ``"modify_package"`` is checked whenever a user modifies a package in the catalog. default: anyone - ``"publicize_package"`` is checked whenever a user is trying to make a murano package public (both when creating a new package or modifying an existing one). default: admin users - ``"manage_public_package"`` is checked whenever a user attempts to modify parameters of a public package. default: admin users - ``"delete_package"`` is checked whenever a user attempts to delete a package from the catalog. default: anyone - ``"download_package"`` is checked whenever a user attempts to download a package from the catalog. default: anyone - ``"list_environments_all_tenants"`` is checked whenever a request to list environments of all tenants is made. default: admin users - ``"execute_action"`` is checked whenever a user attempts to execute an action on deployment environments. default: anyone .. note:: The package upload wizard in Murano dashboard consists of several steps: The "upload_package" policy is enforced during the first step while "modify_package" is enforced during the second step. Package parameters are modified during package upload. So, please modify both policy definitions together. Otherwise it will not be possible to browse package details on the second step of the wizard. Default Murano Policies ----------------------- .. literalinclude:: ../_static/murano.policy.yaml.sample ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/doc/source/admin/murano_repository.rst0000664000175000017500000000027400000000000022614 0ustar00zuulzuul00000000000000.. _murano-repository: ================= Murano repository ================= Use an existing repository ~~~~~~~~~~~~~~~~~~~~~~~~~~ Set up a custom repository ~~~~~~~~~~~~~~~~~~~~~~~~~~ ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/doc/source/admin/net_configuration.rst0000664000175000017500000000746500000000000022542 0ustar00zuulzuul00000000000000===================== Network configuration ===================== Murano may work in various networking environments and is capable of detecting the current network configuration and choosing appropriate settings automatically. However, some additional actions are required to support advanced scenarios. Nova-network support ^^^^^^^^^^^^^^^^^^^^ Nova-network is the simplest networking solution, which has limited capabilities but is available on any OpenStack deployment without the need to deploy any additional components. When a new murano environment is created, murano checks if a dedicated networking service, for example, neutron, exists in the current OpenStack deployment. It relies on the Identity service catalog for that. If such a service is not present, murano automatically falls back to nova-network. No further configuration is needed in this case, all the VMs spawned by Murano will be joining the same network. Neutron support ^^^^^^^^^^^^^^^ If neutron is installed, murano enables its advanced networking features that give you the ability to avoid configuring networks for your application. By default, it creates an isolated network for each environment and joins all VMs needed by your application to that network. To install and configure the application in a newly spawned virtual machine, murano also requires a router to be connected to the external network. Automatic neutron configuration +++++++++++++++++++++++++++++++ To create the router automatically, provide the following parameters in the configuration file: .. code-block:: ini [networking] external_network = %EXTERNAL_NETWORK_NAME% router_name = %MURANO_ROUTER_NAME% create_router = true To figure out the name of the external network, run :command:`openstack network list --external`. During the first deployment, the required networks and router with a specified name will be created and set up. Manual neutron configuration ++++++++++++++++++++++++++++ To configure neutron manually, follow the steps below. #. Create a public network. #. Log in to the OpenStack dashboard as an administrator. #. Verify the existence of external networks. For this, navigate to :menuselection:`Project > Network > Network Topology`. #. Check the network type in network details. For this, navigate to :menuselection:`Admin > Networks` and see the :guilabel:`Network name` section. Alternatively, run the :command:`openstack network list --external` command using CLI. #. Create a new external network as described in the `OpenStack documentation `_. .. image:: figures/network-topology-1.png :alt: Network Topology page :width: 630 px #. Create a local network. #. Navigate to :menuselection:`Project > Network > Networks`. #. Click :guilabel:`Create Network` and fill in the form. #. Create a router. #. Navigate to :menuselection:`Project > Network > Routers`. #. Click :guilabel:`Create Router`. #. In the :guilabel:`Router Name` field, enter *murano-default-router*. If you specify a name other than *murano-default-router*, change the following settings in the configuration file: .. code-block:: ini [networking] router_name = %SPECIFIED_NAME% create_router = false #. Click :guilabel:`Create router`. #. Click the newly created router name. #. In the :guilabel:`Interfaces` tab, click :guilabel:`Add Interface`. #. Specify the subnet and IP address. .. image:: figures/add-interface.png :alt: Add Interface dialog :width: 630 px #. Verify the result in :menuselection:`Project > Network > Network Topology`. .. image:: figures/network-topology-2.png :alt: Network Topology page :width: 630 px ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/doc/source/admin/policy_enf.rst0000664000175000017500000000357700000000000021154 0ustar00zuulzuul00000000000000.. _policy_enf: ================================= Policy enforcement using Congress ================================= Policies are defined and evaluated in the Congress_ project. The policy language for Congress is Datalog. The congress policy consists of the Datalog rules and facts. Examples of policies are as follows: * Minimum 2 GB of RAM for all VM instances. * A certified version for all Apache server instances. * Data placement policy: all database instances must be deployed at a given geographic location enforcing some law restriction on data placement. These policies are evaluated over data in the form of tables (Congress data structures). A deployed Murano environment must be decomposed to the Congress data structures. The decomposed environment is sent to Congress for simulation. Congress simulates whether the resulting state violates any defined policy: deployment is aborted in case of policy violation. Murano uses two predefined policies in Congress: * ``murano_system`` contains rules and facts of policies defined by the cloud administrator. * ``murano`` contains only facts/records reflecting the resulting state after the deployment of an environment. Records in the ``murano`` policy are queried by rules from the ``murano_system`` policy. The Congress simulation does not create any records in the ``murano`` policy, and only provides the feedback on whether the resulting state violates the policy or not. As a part of the policy guided fulfillment, you need to enforce policies on a murano environment deployment. If the policy enforcement fails, the deployment fails as well. .. _Congress: https://wiki.openstack.org/wiki/Congress This section contains the following subsections: .. toctree:: :maxdepth: 2 policy_enforcement/policy_enf_setup policy_enforcement/policy_enf_rules policy_enforcement/policy_enf_dev policy_enforcement/policy_enf_modify ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1696417899.7291806 murano-16.0.0/doc/source/admin/policy_enforcement/0000775000175000017500000000000000000000000022143 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/doc/source/admin/policy_enforcement/policy_enf_dev.rst0000664000175000017500000001337300000000000025671 0ustar00zuulzuul00000000000000.. _policyenf_dev: Murano policy enforcement internals ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ This section describes internals of the murano policy enforcement feature. Model decomposition ------------------- The data for the policy validation comes from the models of Murano applications. These models are transformed to a set of rules that are processed by Congress. There are several *tables* created in murano policy for different kinds of rules that are as follows: * ``murano:objects(object_id, parent_id, type_name)`` * ``murano:properties(object_id, property_name, property_value)`` * ``murano:relationships(source, target, name)`` * ``murano:connected(source, target)`` * ``murano:parent_types(object_id, parent_type_name)`` * ``murano:states(environment_id, state)`` **murano:objects(object_id, parent_id, type_name)** This rule is used for representation of all objects in Murano model, such as environment, application, instance, and other. Value of the ``type`` property is used as the ``type_name`` parameter: .. code-block:: yaml name: wordpress-env '?': {type: io.murano.Environment, id: 83bff5ac} applications: - '?': {id: e7a13d3c, type: com.example.databases.MySql} The model above transforms to the following rules: * ``murano:objects+("83bff5ac", "tenant_id", "io.murano.Environment")`` * ``murano:objects+("83bff5ac", "e7a13d3c", "com.example.databases.MySql")`` .. note:: The owner of the environment is a project (tenant). **murano:properties(object_id, property_name, property_value)** Each object may have properties. In this example we have an application with one property: .. code-block:: yaml applications: - '?': {id: e7a13d3c, type: com.example.databases.MySql} database: wordpress The model above transforms to the following rule: * ``murano:properties+("e7a13d3c", "database", "wordpress")`` Inner properties are also supported using dot notation: .. code-block:: yaml instance: '?': {id: 825dc61d, type: io.murano.resources.LinuxMuranoInstance} networks: useFlatNetwork: false The model above transforms to the following rule: * ``murano:properties+("825dc61d", "networks.useFlatNetwork", "False")`` If a model contains list of values, it is represented as a set of multiple rules: .. code-block:: yaml instances: - '?': {id: be3c5155, type: io.murano.resources.LinuxMuranoInstance} networks: customNetworks: [10.0.1.0, 10.0.2.0] The model above transforms to the following rules: * ``murano:properties+("be3c5155", "networks.customNetworks", "10.0.1.0")`` * ``murano:properties+("be3c5155", "networks.customNetworks", "10.0.2.0")`` **murano:relationships(source, target, name)** Murano application models may contain references to other applications. In this example, the WordPress application references MySQL in the ``database`` property: .. code-block:: yaml applications: - '?': id: 0aafd67e type: com.example.databases.MySql - '?': id: 50fa68ff type: com.example.WordPress database: 0aafd67e The model above transforms to the following rule: * ``murano:relationships+("50fa68ff", "0aafd67e", "database")`` .. note:: For the ``database`` property we do not create the ``murano:properties+`` rule. If we define an object within other object, they will have relationships between them: .. code-block:: yaml applications: - '?': id: 0aafd67e type: com.example.databases.MySql instance: '?': {id: ed8df2b0, type: io.murano.resources.LinuxMuranoInstance} The model above transforms to the following rule: * ``murano:relationships+("0aafd67e", "ed8df2b0", "instance")`` There are special relationships of ``services`` from the environment to its applications: ``murano:relationships+("env_id", "app_id", "services")`` **murano:connected(source, target)** This table stores both direct and indirect connections between instances. It is derived from ``murano:relationships``: .. code-block:: yaml applications: - '?': id: 0aafd67e type: com.example.databases.MySql instance: '?': {id: ed8df2b0, type: io.murano.resources.LinuxMuranoInstance} - '?': id: 50fa68ff type: com.example.WordPress database: 0aafd67e The model above transforms to the following rules: * ``murano:connected+("50fa68ff", "0aafd67e")`` # WordPress to MySql * ``murano:connected+("50fa68ff", "ed8df2b0")`` # WordPress to LinuxMuranoInstance * ``murano:connected+("0aafd67e", "ed8df2b0")`` # MySql to LinuxMuranoInstance **murano:parent_types(object_id, parent_name)** Each object in murano has a class type. These classes may inherit from one or more parents. For example, ``LinuxMuranoInstance > LinuxInstance > Instance``: .. code-block:: yaml instances: - '?': {id: be3c5155, type: LinuxMuranoInstance} The model above transforms to the following rules: * ``murano:objects+("...", "be3c5155", "LinuxMuranoInstance")`` * ``murano:parent_types+("be3c5155", "LinuxMuranoInstance")`` * ``murano:parent_types+("be3c5155", "LinuxInstance")`` * ``murano:parent_types+("be3c5155", "Instance")`` .. note:: The type of an object is also repeated in its parent types (``LinuxMuranoInstance`` in the example) for easier handling of user-created rules. .. note:: If a type inherits from more than one parent, and these parents inherit from one common type, the ``parent_type`` rule is included only once in the common type. **murano:states(environment_id, state)** Currently only one record for environment is created: * ``murano:states+("uugi324", "pending")`` ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/doc/source/admin/policy_enforcement/policy_enf_modify.rst0000664000175000017500000000704500000000000026401 0ustar00zuulzuul00000000000000Using policy for the base modification of an environment ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Congress policies enables a user to define modification of an environment prior to its deployment. This includes: * Adding components, for example, monitoring. * Changing and setting properties, for example enforcing a given zone, flavors, and others. * Configuring relationships within an environment. Use cases examples: * Installation of the monitoring agent on each VM instance by adding a component with the agent and creating relationship between the agent and instance. * Enabling a certified version to all Apache server instances: setting the version property to all Apache applications within an environment to a particular version. These policies are evaluated over data in the form of tables that are Congress data structures. A deployed murano environment must be decomposed to Congress data structures. The further workflow is as follows: * The decomposed environment is sent to Congress for simulation. * Congress simulates whether the resulting state requires modification. * In case the modification of a deployed environment is required, Congress returns a list of actions in the YAML format to be performed on the environment prior to the deployment. For example: .. code-block:: yaml set-property: {object_id: c46770dec1db483ca2322914b842e50f, prop_name: keyname, value: production-key} The example above sets the ``keyname`` property to the ``production-key`` value on the instance identified by ``object_id``. An administrator can use it as an output of the Congress rules. * The action specification is parsed in murano. The given action class is loaded, and the action instance is created. * The parsed parameters are supplied to the action ``__init__`` method. * The action is performed on a given environment (the ``modify`` method). .. _base_mod_rules: Creating base modification rules -------------------------------- This example illustrates how to configure the rule enforcing all VM instances to deploy with a secure key pair. This may be required in a production environment. .. warning:: Before you create rules, configure your OpenStack environment as described in :ref:`policyenf_setup`. **Procedure:** #. To create the ``predeploy_modify`` rule, run: .. code-block:: console congress policy rule create murano_system 'predeploy_modify(eid, obj_id, action):-murano:objects(obj_id, pid, type), murano_env_of_object(obj_id, eid), murano:properties(obj_id, "keyname", kn), concat("set-property: {object_id: ", obj_id, first_part), concat(first_part, ", prop_name: keyname, value: production-key}", action)' The command above contains the following information: .. code-block:: console predeploy_modify(eid, obj_id, action) :- murano:objects(obj_id, pid, type), murano:objects(eid, tid, "io.murano.Environment"), murano:connected(eid, pid), murano:properties(obj_id, "keyname", kn), concat("set-property: {object_id: ", obj_id, first_part), concat(first_part, ", prop_name: keyname, value: production-key}", action) Policy validation engine checks the ``predeploy_modify`` rule. And the Congress engine evaluates the rules referenced inside this rule. .. note:: The ``production-key`` key pair must already exist, though you can use any other existing key pair. #. Deploy the environment. Instances within the environment are deployed with the specified key pair. .. seealso:: * :ref:`policy_enf_rules` ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/doc/source/admin/policy_enforcement/policy_enf_rules.rst0000664000175000017500000000614100000000000026240 0ustar00zuulzuul00000000000000.. _policy_enf_rules: Creating policy enforcement rules ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ This article illustrates how you can create policy enforcement rules. For testing purposes, create rules that prohibit the creation of instances with the flavor with over 2048 MB of RAM following the procedure below. **Procedure:** #. Verify that you have configured your OpenStack environment as described in :ref:`policyenf_setup`. #. To create the ``predeploy_errors`` rule, run: .. code-block:: console congress policy rule create murano_system "predeploy_errors(eid, obj_id, msg) :- murano:objects(obj_id, pid, type), murano:objects(eid, tid, \"io.murano.Environment\"), murano:connected(eid, pid), murano:properties(obj_id, \"flavor\", flavor_name), flavor_ram(flavor_name, ram), gt(ram, 2048), murano:properties(obj_id, \"name\", obj_name), concat(obj_name, \": instance flavor has RAM size over 2048MB\", msg)" The command above contains the following information: .. code-block:: console predeploy_errors(eid, obj_id, msg) :- murano:objects(obj_id, pid, type), murano:objects(eid, tid, "io.murano.Environment"), murano:connected(eid, pid), murano:properties(obj_id, "flavor", flavor_name), flavor_ram(flavor_name, ram), gt(ram, 2048), murano:properties(obj_id, "name", obj_name), concat(obj_name, ": instance flavor has RAM size over 2048MB", msg) Policy validation engine checks the ``predeploy_errors`` rule, and rules referenced within this rule are evaluated by the Congress engine. In this example, we create the rule that references the ``flavor_ram`` rule we create afterwards. It disables flavors with RAM more than 2048 MB and constructs the message returned to the user in the ``msg`` variable. In this example we use data from policy **murano** which is represented by ``murano:properties``. There are stored rows with decomposition of model representing murano application. We also use built-in functions of Congress: * ``gt`` stands for 'greater-than' * ``concat`` joins two strings into one variable #. To create the ``flavor_ram`` rule, run: .. code-block:: console congress policy rule create murano_system "flavor_ram(flavor_name, ram) :- nova:flavors(id, flavor_name, cpus, ram)" This rule resolves parameters of flavor by flavor name and returns the ``ram`` parameter. It uses the ``flavors`` rule from ``nova`` policy. Data in this policy is filled by the ``nova`` datasource driver. #. Check the rule usage. #. Create an environment with a simple application: - Select an application from the murano applications. - Create a ``m1.medium`` instance, which uses 4096 MB RAM. .. image:: ../figures/new-inst.png :alt: Create new instance :width: 100 % #. Deploy the environment. Deployment fails as the rule is violated: environment is in the ``Deploy FAILURE`` status. Check the deployment logs for details: .. image:: ../figures/deploy-log.png :alt: Deployment log :width: 100 % .. seealso:: * :ref:`base_mod_rules` ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/doc/source/admin/policy_enforcement/policy_enf_setup.rst0000664000175000017500000001214400000000000026246 0ustar00zuulzuul00000000000000.. _policyenf_setup: Setting up policy enforcement ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Before you use the policy enforcement feature, configure Murano and Congress properly. .. note:: This article does not cover Murano and Congress configuration options useful for Murano application deployment, for example, DNS setup, floating IPs, and so on. **To enable policy enforcement, complete the following tasks:** #. In Murano: * Enable the ``enable_model_policy_enforcer`` option in the ``murano.conf`` file: .. code-block:: ini [engine] # Enable model policy enforcer using Congress (boolean value) enable_model_policy_enforcer = true * Restart murano-engine. #. Verify that Congress is installed and available in your OpenStack environment. See the details in the `Congress official documentation `_. #. `Install the congress command-line client `_ as any other OpenStack command-line client. #. For Congress, configure the following policies that policy enforcement uses during the evaluation: * ``murano`` policy It is created by the Congress` murano datasource driver, which is a part of Congress. Configure it for the OpenStack project (tenant) where you plan to deploy your Murano application. Datasource driver retrieves deployed Murano environments and populates Congress' murano policy tables. See :ref:`policyenf_dev` for details. Remove the existing ``murano`` policy and create a new ``murano`` policy configured for the ``demo`` project, by running: .. code-block:: console # remove default murano datasource configuration, because it is using 'admin' project. We need 'demo' project to be used. openstack congress datasource delete murano openstack congress datasource create murano murano --config username="$OS_USERNAME" --config tenant_name="demo" --config password="$OS_PASSWORD" --config auth_url="$OS_AUTH_URL" * ``murano_system`` policy It holds the user-defined rules for policy enforcement. Typically, the rules use tables from other policies, for example, murano, nova, keystone, and others. Policy enforcement expects the ``predeploy_errors`` table here that is available on the ``predeploy_errors`` rules creation. Create the ``murano_system`` rule, by running: .. code-block:: console # create murano_system policy openstack congress policy create murano_system # resolves objects within environment openstack congress policy rule create murano_system 'murano_env_of_object(oid,eid):-murano:connected(eid,oid), murano:objects(eid,tid,"io.murano.Environment")' * ``murano_action`` policy with internal management rules. These rules are used internally in the policy enforcement request and stored in a dedicated ``murano_action`` policy that is created here. They are important in case an environment is redeployed. .. code-block:: console # create murano_action policy openstack congress policy create murano_action --kind action # register action deleteEnv openstack congress policy rule create murano_action 'action("deleteEnv")' # states openstack congress policy rule create murano_action 'murano:states-(eid, st) :- deleteEnv(eid), murano:states( eid, st)' # parent_types openstack congress policy rule create murano_action 'murano:parent_types-(tid, type) :- deleteEnv(eid), murano:connected(eid, tid),murano:parent_types(tid,type)' openstack congress policy rule create murano_action 'murano:parent_types-(eid, type) :- deleteEnv(eid), murano:parent_types(eid,type)' # properties openstack congress policy rule create murano_action 'murano:properties-(oid, pn, pv) :- deleteEnv(eid), murano:connected(eid, oid), murano:properties(oid, pn, pv)' openstack congress policy rule create murano_action 'murano:properties-(eid, pn, pv) :- deleteEnv(eid), murano:properties(eid, pn, pv)' # objects openstack congress policy rule create murano_action 'murano:objects-(oid, pid, ot) :- deleteEnv(eid), murano:connected(eid, oid), murano:objects(oid, pid, ot)' openstack congress policy rule create murano_action 'murano:objects-(eid, tnid, ot) :- deleteEnv(eid), murano:objects(eid, tnid, ot)' # relationships openstack congress policy rule create murano_action 'murano:relationships-(sid, tid, rt) :- deleteEnv(eid), murano:connected(eid, sid), murano:relationships( sid, tid, rt)' openstack congress policy rule create murano_action 'murano:relationships-(eid, tid, rt) :- deleteEnv(eid), murano:relationships(eid, tid, rt)' # connected openstack congress policy rule create murano_action 'murano:connected-(tid, tid2) :- deleteEnv(eid), murano:connected(eid, tid), murano:connected(tid,tid2)' openstack congress policy rule create murano_action 'murano:connected-(eid, tid) :- deleteEnv(eid), murano:connected(eid,tid)' ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/doc/source/admin/prepare_lab.rst0000664000175000017500000001261600000000000021273 0ustar00zuulzuul00000000000000======================== Prepare a lab for murano ======================== This section provides basic information about lab's system requirements. It also contains a description of a test which you may use to check if your hardware fits the requirements. To do this, run the test and compare the results with baseline data provided. .. _system_prerequisites: System prerequisites ~~~~~~~~~~~~~~~~~~~~ Supported operating systems --------------------------- * Ubuntu Server 16.04 LTS or higher * RHEL/CentOS 7.4 or higher **System packages are required for Murano** *Ubuntu* * gcc * python3-pip * python3-dev * libxml2-dev * libxslt-dev * libffi-dev * libpq-dev * python3-openssl * mysql-client Install all the requirements on Ubuntu by running:: sudo apt-get install gcc python3-pip python3-dev \ libxml2-dev libxslt-dev libffi-dev \ libpq-dev python3-openssl mysql-client *CentOS* * gcc * python3-pip * python3-devel * libxml2-devel * libxslt-devel * libffi-devel * postgresql-devel * pyOpenSSL * mysql Install all the requirements on CentOS by running:: sudo yum install gcc python3-pip python3-devel libxml2-devel \ libxslt-devel libffi-devel postgresql-devel pyOpenSSL \ mysql .. _lab_requirements: Lab requirements ---------------- +------------+--------------------------------+-----------------------+ | Criteria | Minimal | Recommended | +============+================================+=======================+ | CPU | 4 core @ 2.4 GHz | 24 core @ 2.67 GHz | +------------+--------------------------------+-----------------------+ | RAM | 8 GB | 24 GB or more | +------------+--------------------------------+-----------------------+ | HDD | 2 x 500 GB (7200 rpm) | 4 x 500 GB (7200 rpm) | +------------+--------------------------------+-----------------------+ | RAID | Software RAID-1 (use mdadm as | Hardware RAID-10 | | | it will improve read | | | | performance almost two times) | | +------------+--------------------------------+-----------------------+ `Table: Hardware requirements` There are a few possible storage configurations except the shown above. All of them were tested and were working well. * 1x SSD 500+ GB * 1x HDD (7200 rpm) 500+ GB and 1x SSD 250+ GB (install the system onto the HDD and mount the SSD drive to folder where VM images are) * 1x HDD (15000 rpm) 500+ GB Test your lab host performance ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ We have measured time required to boot 1 to 5 instances of Windows system simultaneously. You can use this data as the baseline to check if your system is fast enough. You should use sysprepped images for this test, to simulate VM first boot. Steps to reproduce test: #. Prepare Windows 2012 Standard (with GUI) image in QCOW2 format. Let's assume that its name is ws-2012-std.qcow2 #. Ensure that there is NO KVM PROCESSES on the host. To do this, run command: .. code-block:: console ps aux | grep kvm #. Make 5 copies of Windows image file: .. code-block:: console for i in $(seq 5); do \ cp ws-2012-std.qcow2 ws-2012-std-$i.qcow2; done #. Create script start-vm.sh in the folder with .qcow2 files: .. code-block:: console #!/bin/bash [ -z $1 ] || echo "VM count not provided!"; exit 1 for i in $(seq $1); do echo "Starting VM $i ..." kvm -m 1024 -drive file=ws-2012-std-$i.qcow2,if=virtio -net user -net nic,model=virtio -nographic -usbdevice tablet -vnc :$i & done #. Start ONE instance with command below (as root) and measure time between VM's launch and the moment when Server Manager window appears. To view VM's desktop, connect with VNC viewer to your host to VNC screen :1 (port 5901): .. code-block:: console sudo ./start-vm.sh 1 #. Turn VM off. You may simply kill all KVM processes by .. code-block:: console sudo killall kvm #. Start FIVE instances with command below (as root) and measure time interval between ALL VM's launch and the moment when LAST Server Manager window appears. To view VM's desktops, connect with VNC viewer to your host to VNC screens :1 thru :5 (ports 5901-5905): .. code-block:: console sudo ./start-vm.sh 5 #. Turn VMs off. You may simply kill all KVM processes by .. code-block:: console sudo killall kvm Baseline data ~~~~~~~~~~~~~ The table below provides baseline data which we've got in our environment. +----------------+--------------------------+---------------------+ | | Boot 1 instance | Boot 5 instances | +================+==========================+=====================+ | Avg. Time | 3m:40s | 8m | +----------------+--------------------------+---------------------+ | Max. Time | 5m | 20m | +----------------+--------------------------+---------------------+ ``Avg. Time`` refers to the lab with recommended hardware configuration, while ``Max. Time`` refers to minimal hardware configuration. Host optimizations ~~~~~~~~~~~~~~~~~~ Default KVM installation could be improved to provide better performance. The following optimizations may improve host performance up to 30%: * change default scheduler from ``CFQ`` to ``Deadline`` * use ``ksm`` * use ``vhost-net`` ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/doc/source/admin/using_glare.rst0000664000175000017500000001050500000000000021311 0ustar00zuulzuul00000000000000.. _glare_usage: ===================================== Using Glare as a storage for packages ===================================== DevStack installation --------------------- #. Enable Glare service in DevStack To enable the Glare service in DevStack, edit the ``local.conf`` file: .. code-block:: console $ cat local.conf [[local|localrc]] enable_service g-glare #. Run DevStack: .. code-block:: console $ ./stack.sh **Result** Glare service is installed with DevStack. You can find logs in ``g-glare`` screen session. #. Install the ``muranoartifact`` plug-in from ``murano/contrib`` .. code-block:: console $ cd $DEST/murano/contrib/glance/ $ sudo pip install -e . #. Restart ``Glare`` #. Set Glare as packages service in murano-engine. For this, edit the ``[engine]`` section in the ``murano.conf`` file. By default, ``murano.conf`` is located in the ``/etc/murano`` directory .. code-block:: ini [engine] packages_service = glare #. Restart ``murano-engine`` .. note:: You also can use ``glance`` as a value of the ``packages_service`` option for the same behaviour #. Enable Glare in ``murano-dashboard``. For this, modify the following line in the ``_50_murano.py`` file .. code-block:: python MURANO_USE_GLARE = True By default, the ``_50_murano.py`` file is located in ``$HORIZON_DIR/openstack_dashboard/local/local_settings.d/``. #. Restart the ``apache2`` service. Now ``murano-dashboard`` will retrieve packages from Glare. #. Log in to Dashboard and navigate to :menuselection:`Applications > Manage > Packages` to view the empty list of packages. Alternatively, use the :command:`murano` command. #. Use ``--murano-packages-service`` option to specify backend, used by :command:`murano` command. Set it to ``glare`` for using ``Glare`` .. note:: You also can use ``glance`` as value of ``--murano-packages-service`` option or environment variable ``MURANO_PACKAGES_SERVICE`` for same behaviour + View list of packages: .. code-block:: console $ . {DEVSTACK_SOURCE_DIR}/openrc admin admin $ murano --murano-packages-service=glare package-list +----+------+-----+--------+--------+-----------+------+---------+ | ID | Name | FQN | Author | Active | Is Public | Type | Version | +----+------+-----+--------+--------+-----------+------+---------+ +----+------+-----+--------+--------+-----------+------+---------+ + Importing ``Core library`` .. code-block:: console $ cd $DEST/murano/meta/io.murano/ $ zip io.murano.zip -r * $ murano --murano-packages-service=glare package-import \ --is-public /opt/stack/murano/meta/io.murano/io.murano.zip Importing package io.murano +--------------------------------------+--------------+-----------+-----------+--------+-----------+---------+---------+ | ID | Name | FQN | Author | Active | Is Public | Type | Version | +--------------------------------------+--------------+-----------+-----------+--------+-----------+---------+---------+ | 91a9c78f-f23a-4c82-aeda-14c8cbef096a | Core library | io.murano | murano.io | True | | Library | 0.0.0 | +--------------------------------------+--------------+-----------+-----------+--------+-----------+---------+---------+ Set up Glare API entrypoint manually ------------------------------------ If you do not plan to get Glare service from keystone application catalog, specify where g-glare service is running. #. Specify Glare URL in ``murano.conf``.It is http://0.0.0.0:9494 by default and can be changed by setting `bind_host` and `bind_port` options in the ``glance-glare.conf`` file. .. code-block:: ini [glare] url = http://: #. Specify Glare URL in the Dashboard settings file, ``_50_murano.py`` : .. code-block:: python GLARE_API_URL = 'http://:' #. Set the ``GLARE_URL`` environment variable for python-muranoclient. Alternatively, use the ``--glare-url`` option in CLI. .. code-block:: console $ murano --murano-packages-service=glare --glare-url=http://0.0.0.0:9494 package-list ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1696417899.7291806 murano-16.0.0/doc/source/cli/0000775000175000017500000000000000000000000015736 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/doc/source/cli/index.rst0000664000175000017500000000031200000000000017573 0ustar00zuulzuul00000000000000======================== Murano CLI Documentation ======================== In this section you will find information on Murano's command line interface. .. toctree:: :maxdepth: 1 murano-status ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/doc/source/cli/murano-status.rst0000664000175000017500000000363600000000000021322 0ustar00zuulzuul00000000000000============= murano-status ============= ---------------------------------------- CLI interface for Murano status commands ---------------------------------------- Synopsis ======== :: murano-status [] Description =========== :program:`murano-status` is a tool that provides routines for checking the status of a Murano deployment. Options ======= The standard pattern for executing a :program:`murano-status` command is:: murano-status [] Run without arguments to see a list of available command categories:: murano-status Categories are: * ``upgrade`` Detailed descriptions are below: You can also run with a category argument such as ``upgrade`` to see a list of all commands in that category:: murano-status upgrade These sections describe the available categories and arguments for :program:`murano-status`. Upgrade ~~~~~~~ .. _murano-status-checks: ``murano-status upgrade check`` Performs a release-specific readiness check before restarting services with new code. For example, missing or changed configuration options, incompatible object states, or other conditions that could lead to failures while upgrading. **Return Codes** .. list-table:: :widths: 20 80 :header-rows: 1 * - Return code - Description * - 0 - All upgrade readiness checks passed successfully and there is nothing to do. * - 1 - At least one check encountered an issue and requires further investigation. This is considered a warning but the upgrade may be OK. * - 2 - There was an upgrade status check failure that needs to be investigated. This should be considered something that stops an upgrade. * - 255 - An unexpected error occurred. **History of Checks** **7.0.0 (Stein)** * Sample check to be filled in with checks as they are added in Stein. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/doc/source/conf.py0000664000175000017500000001131100000000000016463 0ustar00zuulzuul00000000000000# 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 os import sys on_rtd = os.environ.get('READTHEDOCS', None) == 'True' # 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('../../')) sys.path.insert(0, os.path.abspath('../')) 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 = ['sphinx.ext.autodoc', 'sphinx.ext.doctest', 'sphinx.ext.todo', 'sphinx.ext.coverage', 'oslo_config.sphinxconfiggen', 'oslo_config.sphinxext', 'oslo_policy.sphinxext', 'oslo_policy.sphinxpolicygen', 'sphinx.ext.viewcode', 'sphinxcontrib.httpdomain',] if not on_rtd: extensions.append('openstackdocstheme') # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # The suffix of source filenames. source_suffix = '.rst' # The master toctree document. master_doc = 'index' # openstackdocstheme options openstackdocs_repo_name = 'openstack/murano' openstackdocs_pdf_link = True openstackdocs_bug_project = 'murano' openstackdocs_bug_tag = '' config_generator_config_file = '../../etc/oslo-config-generator/murano.conf' sample_config_basename = '_static/murano' policy_generator_config_file = [ ('../../etc/oslo-policy-generator/murano-policy-generator.conf', '_static/murano'), ] # Set the default Pygments syntax highlight_language = 'python' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. exclude_patterns = ['specification/murano-repository.rst', 'specification/murano-api.rst', 'murano_pl/builtin_functions.rst', 'install/configure_network.rst', 'articles/ad-ui.rst', 'articles/telnet.rst'] # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. show_authors = False # -- Options for HTML output --------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. if not on_rtd: #TODO(efedorova): Change local theme to correspond with the theme on rtd pass # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". html_title = 'Murano' html_theme = 'openstackdocs' # Custom sidebar templates, maps document names to template names. html_sidebars = { 'index': ['sidebarlinks.html', 'localtoc.html', 'searchbox.html', 'sourcelink.html'], '**': ['localtoc.html', 'relations.html', 'searchbox.html', 'sourcelink.html'] } # 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'] # -- Options for LaTeX output ------------------------------------------------- # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass # [howto/manual]). latex_documents = [ ('index', 'doc-murano.tex', u'Murano Documentation', u'OpenStack Foundation', 'manual'), ] latex_domain_indices = False latex_elements = { 'makeindex': '', 'printindex': '', 'preamble': r'\setcounter{tocdepth}{3}', 'maxlistdepth': '10', } # Disable usage of xindy https://bugzilla.redhat.com/show_bug.cgi?id=1643664 latex_use_xindy = False # Disable smartquotes, they don't work in latex smartquotes_excludes = {'builders': ['latex']} ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1696417899.7291806 murano-16.0.0/doc/source/configuration/0000775000175000017500000000000000000000000020036 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/doc/source/configuration/config-options.rst0000664000175000017500000000064400000000000023532 0ustar00zuulzuul00000000000000========================================================= Configuration options for the Application Catalog service ========================================================= The following options can be set in the ``/etc/murano/murano.conf`` config file. .. only:: html A :doc:`sample configuration file ` is also available. .. show-options:: :config-file: etc/oslo-config-generator/murano.conf ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/doc/source/configuration/index.rst0000664000175000017500000000025600000000000021702 0ustar00zuulzuul00000000000000=================== Configuration Guide =================== .. only:: html .. toctree:: :maxdepth: 1 config-options sample_config sample_policy ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/doc/source/configuration/sample_config.rst0000664000175000017500000000100100000000000023366 0ustar00zuulzuul00000000000000=========================== Murano Configuration Sample =========================== The following is a sample murano configuration for adaptation and use. It is auto-generated from murano when this documentation is built, so if you are having issues with an option, please compare your version of murano with the version of this documentation. .. only:: html The sample configuration can also be downloaded in `file form <../_static/murano.conf.sample>`_. .. literalinclude:: ../_static/murano.conf.sample ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/doc/source/configuration/sample_policy.rst0000664000175000017500000000223500000000000023432 0ustar00zuulzuul00000000000000==================== Murano Sample Policy ==================== .. warning:: JSON formatted policy file is deprecated since Murano 11.0.0 (Wallaby). This `oslopolicy-convert-json-to-yaml`__ tool will migrate your existing JSON-formatted policy file to YAML in a backward-compatible way. .. __: https://docs.openstack.org/oslo.policy/latest/cli/oslopolicy-convert-json-to-yaml.html The following is a sample murano policy file that has been auto-generated from default policy values in code. If you're using the default policies, then the maintenance of this file is not necessary, and it should not be copied into a deployment. Doing so will result in duplicate policy definitions. It is here to help explain which policy operations protect specific murano APIs, but it is not suggested to copy and paste into a deployment unless you're planning on providing a different policy for an operation that is not the default. If you wish build a policy file, you can also use ``tox -e genpolicy`` to generate it. The sample policy file can also be downloaded in `file form <../_static/murano.policy.yaml.sample>`_. .. literalinclude:: ../_static/murano.policy.yaml.sample ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1696417899.7331805 murano-16.0.0/doc/source/contributor/0000775000175000017500000000000000000000000017541 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/doc/source/contributor/contributing.rst0000664000175000017500000000344000000000000023003 0ustar00zuulzuul00000000000000============================ So You Want to Contribute... ============================ For general information on contributing to OpenStack, please check out the `contributor guide `_ to get started. It covers all the basics that are common to all OpenStack projects: the accounts you need, the basics of interacting with our Gerrit review system, how we communicate as a community, etc. Below will cover the more project specific information you need to get started with Murano. Communication ~~~~~~~~~~~~~ * IRC channel #murano at OFTC * Mailing list (prefix subjects with ``[murano]`` for faster responses) http://lists.openstack.org/cgi-bin/mailman/listinfo/openstack-discuss Contacting the Core Team ~~~~~~~~~~~~~~~~~~~~~~~~ Please refer the `murano Core Team `_ contacts. New Feature Planning ~~~~~~~~~~~~~~~~~~~~ murano features are tracked on `Launchpad `_. Task Tracking ~~~~~~~~~~~~~ We track our tasks in `Launchpad `_. If you're looking for some smaller, easier work item to pick up and get started on, search for the 'low-hanging-fruit' tag. Reporting a Bug ~~~~~~~~~~~~~~~ You found an issue and want to make sure we are aware of it? You can do so on `Launchpad `_. Getting Your Patch Merged ~~~~~~~~~~~~~~~~~~~~~~~~~ All changes proposed to the murano project require one or two +2 votes from murano core reviewers before one of the core reviewers can approve patch by giving ``Workflow +1`` vote. Project Team Lead Duties ~~~~~~~~~~~~~~~~~~~~~~~~ All common PTL duties are enumerated in the `PTL guide `_. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/doc/source/contributor/contributor_index.rst0000664000175000017500000000035400000000000024036 0ustar00zuulzuul00000000000000.. index:: Murano Contributor Guide .. _contributor-guide: Contributor Guide ~~~~~~~~~~~~~~~~~ .. toctree:: :maxdepth: 2 how_to_contribute dev_guidelines plugins dev_env testing doc_guidelines stable_branches././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/doc/source/contributor/dev_env.rst0000664000175000017500000000012600000000000021720 0ustar00zuulzuul00000000000000.. _dev-env: ======================= Development environment ======================= ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/doc/source/contributor/dev_guidelines.rst0000664000175000017500000000216000000000000023260 0ustar00zuulzuul00000000000000.. _dev-guidelines: ====================== Development guidelines ====================== Conventions ~~~~~~~~~~~ High-level overview of Murano components ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Coding guidelines ~~~~~~~~~~~~~~~~~ There are several significant rules for the Murano developer: * Follow PEP8 and OpenStack style guidelines. * Do not import functions. Only module imports are accepted. * Make commits as small as possible. It speeds up review of the change. * Six library usage rule: use it only when really necessary (for example if existing code will not work in python 3 at all). * Mark application name in the 1st line of commit message for murano-apps repository, i.e. [Apache] or [Kubernetes]. * Prefer code readability over performance unless the situations when performance penalty can be proven to be big. * Write Py3-compatible code. If that's impossible leave comment. Rules for MuranoPL coding style: * Use camelCase for MuranoPL functions/namespaces/variables/properties, PascalCase for class names. * Consider using ``$this`` instead of ``$`` where appropriate. Debug tips ~~~~~~~~~~ ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/doc/source/contributor/doc_guidelines.rst0000664000175000017500000000014000000000000023243 0ustar00zuulzuul00000000000000.. _doc-guidelines: ======================== Documentation guidelines ======================== ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/doc/source/contributor/how_to_contribute.rst0000664000175000017500000000057200000000000024034 0ustar00zuulzuul00000000000000.. _how_to_contribute: ================= How to contribute ================= .. TODO add a brief intro: - Intended audience. - How to start developing? - How a new-comer can contribute? - Communication channels - Useful links for an OpenStack contributor - consider the context of https://docs.openstack.org/sahara/latest/contributor/how-to-participate.html ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1696417899.7331805 murano-16.0.0/doc/source/contributor/plugins/0000775000175000017500000000000000000000000021222 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/doc/source/contributor/plugins/manage_plugins.rst0000664000175000017500000000700000000000000024742 0ustar00zuulzuul00000000000000.. _manage_plugins: Creating a Murano plug-in ------------------------- Murano plug-in is a setuptools-compliant python package with ``setup.py`` and all other necessary files. For more information about defining stevedore plug-ins, see `stevedore documentation `_. The structure of the demo application package +++++++++++++++++++++++++++++++++++++++++++++ The package must meet the following requirements: * It must be a ZIP archive. * The root folder of the archive must contain a ``manifest.yaml`` file. * The manifest must be a valid YAML file representing key-value associative array. * The manifest should contain a *Format* key, that is, a format identifier. If it is not present, "MuranoPL/1.0" is used. Murano uses the *Format* attribute of the manifest file to find an appropriate plug-in for a particular package type. All interactions between the rest of Murano and package file contents are done through the plug-in interface alone. Because Murano never directly accesses files inside the packages, it is possible for plug-ins to dynamically generate MuranoPL classes on the fly. Those classes will be served as adapters between Murano and third-party systems responsible for deployment of particular package types. Thus, for Murano all packages remain to be of MuranoPL type though some of them are "virtual". The format identifier has the following format: ``Name/Version``. For example, ``Heat.HOT/1.0``. If name is not present, it is assumed to be ``MuranoPL`` (thus ``1.0`` becomes ``MuranoPL/1.0``). Version strings are in SemVer three-component format (major.minor.patch). Missing version components are assumed to be zero (thus 1.0 becomes 1.0.0). Installing a plug-in -------------------- To use a plug-in, install it on murano nodes in the same Python environment with murano engine service. To install a plug-in: #. Execute the plug-in setup script. Alternatively, use a package deployment tool, such as pip: .. code-block:: console cd plugin_dir pip install . #. Restart murano engine. After that, it will be possible to upload and deploy the applications that use the capabilities that a plug-in provides. Plug-in versioning ------------------ Plug-ins located in Murano repository have the same version as Murano. Therefore, to use a specific version of such plug-in, checkout to this version. Then specify the version of plug-in classes in your application's manifest file as usual: .. code-block:: yaml Require: murano.plugins.example: 2.0.0 It should be standard SemVer format version string consisting of three parts: Major.Minor.Patch. For more information about versioning, refer to :ref:`versioning`. .. note:: Enable Glare to use versioning. Organization ------------ Documentation +++++++++++++ Documentation helps users understand what your plug-in does. For plug-ins located in the Murano repository, create a ``README.rst`` file in the main folder of the plug-in. The ``README.rst`` file may contain information about the plug-in and an installation guide. Code ++++ The code of your plug-in may be located in the following repositories: * Murano repository. In this case, the plug-in should be located in the ``murano/contrib/plugins`` folder. * A separate repository. In this case, create your own project. Bugs ++++ All bugs for specific plug-ins are reported in their projects. Bugs related to plug-ins located in Murano repository should be reported in the `Murano `_ project. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/doc/source/contributor/plugins/murano_plugins.rst0000664000175000017500000002044500000000000025023 0ustar00zuulzuul00000000000000.. _muranopl_extensions: MuranoPL extension plug-ins ~~~~~~~~~~~~~~~~~~~~~~~~~~~ Murano plug-ins allow extending MuranoPL with new classes. Therefore, using such plug-ins applications with MuranoPL format, you access some additional functionality defined in a plug-in. For example, the Magnum plug-in, which allows murano to deploy applications such as Kubernetes using the capabilities of the Magnum client. MuranoPL extension plug-ins can be used for the following purposes: * Providing interaction with external services. For example, you want to interact with the OpenStack Image service to get information about images suitable for deployment. A plug-in may request image data from glance during deployment, performing any necessary checks. * Enabling connections between murano applications and external hardware For example, you have an external load balancer located on a powerful hardware and you want your applications launched in OpenStack to use that load balancer. You can write a plug-in that interacts with the load balancer API. Once done, add new apps to the pool of your load balancer or make any other configurations from within your application definition. * Extending Core Library class functionality, which is responsible for creating networks, interaction with murano-agent, and others For example, you want to create networks with special parameters for all of your applications. You can just copy the class that is responsible for network management from the Murano Core library, make the desired modification, and load the new class as a plug-in. Both classes will be available, and it is up to you to decide which way to create your networks. * Optimization of frequently used operations. Plug-in classes are written in Python, therefore, the opportunity for improvement is significant. Murano provides a number of optimization opportunities depending on the improvement needs. For example, classes in the Murano Core Library can be rewritten in C and used from Python code to improve their performance in particular use cases. .. _package_type_plugins: MuranoPL package type plug-ins ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ The only package type natively supported by Murano is MuranoPL. However, it is possible to extend Murano with support for other formats of application definitions. TOSCA CSARs and HOT templates are the two examples of alternate ways to define applications. Package types plug-ins are normal Python packages that can be distributed through PyPI and installed using :command:`pip` or its alternatives. It is important that the plug-in be installed to the same Python instance that is used to run Murano API and Murano Engine. For multi-node Murano deployments, plug-ins need to be installed on each node. To associate a plug-in with a particular package format, it needs to have a special record in `[entry_points]` section of setup.cfg file: .. code-block:: ini io.murano.plugins.packages = Name/Version = namespace:Class For example: .. code-block:: ini [entry_points] io.murano.plugins.packages = Cloudify.TOSCA/1.0 = murano_cloudify_plugin.cloudify_tosca_package:CloudifyToscaPackage This declaration maps particular pair of format-name/version to Python class that implements Package API interface for the package type. It is possible to specify several different format names or versions and map them to single or different Python classes. For example, it is possible to specify .. code-block:: ini [entry_points] io.murano.plugins.packages = Cloudify.TOSCA/1.0 = murano_cloudify_plugin.cloudify_tosca_package:CloudifyToscaPackage Cloudify.TOSCA/1.1 = murano_cloudify_plugin.cloudify_tosca_package:CloudifyToscaPackage Cloudify.TOSCA/2.0 = murano_cloudify_plugin.cloudify_tosca_package:CloudifyToscaPackage_v2 .. note:: A single Python plug-in package may contain several Murano plug-ins including of different types. For example, it is possible to combine MuranoPL extension and package type plug-ins into a single package. Tooling for package preparation ------------------------------- Some package formats may require additional tooling to prepare package ZIP archive of desired structure. In such cases it is expected that those tools will be provided by plug-in authors either as part of the same Python package (by exposing additional shell entry points) or as a separate package or distribution. The only two exceptions to this rule are native MuranoPL packages and HOT packages that are built into Murano (there is no need to install additional plug-ins for them). Tooling for those two formats is a part of python-muranoclient. Package API interface reference ------------------------------- Plug-ins expose API for the rest of Murano to interact with the package by implementing `murano.packages.package.Package` interface. Class initializer: `def __init__(self, format_name, runtime_version, source_directory, manifest):` * **format_name**: name part of the format identifier (string) * **runtime_version**: version part of the format identifier (instance of semantic_version.Version) * **source_directory**: path to the directory where package content was extracted (string) * **manifest**: contents of the manifest file (string->string dictionary) **Note**: implementations must call base class (`Package`) initializer passing the first three of these arguments. Abstract properties that must be implemented by the plug-in: `def full_name(self):` * Fully qualified name of the package. Must be unique within package scope of visibility (string) `def version(self):` * Package version (not to confuse with format version!). An instance of `semantic_version.Version` `def classes(self):` * List (or tuple) of MuranoPL class names (FQNs) that package contains `def requirements(self):` * Dictionary of requirements (dependencies on other packages) in a form of key-value mapping from required package FQN string to SemVer version range specifier (instance of semantic_version.Spec or string representation supported by Murano versioning scheme) `def package_type(self):` * Package type: "Application" or "Library" `def display_name(self):` * Human-readable name of the package as presented to the user (string) `def description(self):` * Package description (string or None) `def author(self):` * Package author (string or None) `def supplier(self):` * Package supplier (string or None) `def tags(self):` * List or tags for the package (list of strings) `def logo(self):` * Package (application) logo file content (str or None) `def supplier_logo(self):` * Package (application) supplier logo file content (str or None) `def ui(self):` * YAML-encoded string containing application's form definition (string or None) Abstract methods that must be implemented by the plug-in: `def get_class(self, name):` * Returns string containing MuranoPL code (YAML-encoded string) for the class whose fully qualified name is in "name" parameter (string) `def get_resource(self, name):` * Returns path for resource file whose name is in "name" parameter (string) Properties that can be overridden in the plug-in: `def format_name(self):` * Canonical format name for the plug-in. Usually the same value that was passed to class initializer `def runtime_version(self):` * Format version. Usually the same value that was passed to class initializer (semantic_version.Version) `def blob(self):` * Package file (.zip) content (str) PackageBase class ----------------- Usually, there is no need to manually implement all the methods and properties described. There is a `murano.packages.package.PackageBase` class that provides typical implementation of most of required properties by obtaining corresponding value from manifest file. When inheriting from PackageBase class, plug-in remains responsible for implementation of: * `ui` property * `classes` property * `get_class` method This allows plug-in developers to concentrate on dynamic aspects of the package type plug-in while keeping all static aspects (descriptions, logos and so on) consistent across all package types (at least those who inherit from `PackageBase`).././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/doc/source/contributor/plugins.rst0000664000175000017500000000064600000000000021762 0ustar00zuulzuul00000000000000.. _plugins: =============== Murano plug-ins =============== Murano plug-ins help to extend the capability of murano. There are two types of murano plug-ins which serve different purposes: * Extend murano Core Library by implementing additional functionality. * Add new package type classes. This section contains the following topics: .. toctree:: :maxdepth: 2 plugins/murano_plugins plugins/manage_plugins././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/doc/source/contributor/stable_branches.rst0000664000175000017500000000601400000000000023413 0ustar00zuulzuul00000000000000.. _stable_branches: ============================== Backporting to stable/branches ============================== Since murano is a big-tent OS project it largely follows the `OpenStack stable branch guide `_ Upstream support phases ~~~~~~~~~~~~~~~~~~~~~~~ #. Phase I (first 6 months): All bugfixes (which meet the stable port criteria, described in OS stable branch policy) are appropriate #. Phase II (6-12 months): Only critical bugfixes and security patches are acceptable #. Phase III (more than 12 months): Only security patches are acceptable In order to accept a change into $release it must first be accepted into all releases back to master. There are two notable exceptions to the support phases rule: - murano-apps repository: We recognise, that murano apps have different lifecycle than main murano repository. Most of the time new apps are being written for already released versions of murano, not for master. Having a rich collection of apps is one of the goals of murano-apps repository, therefore we accept backports of apps and app features to previous release branches. This is done on a case by case basis and should be discussed with PTL and Murano core members on IRC or Mailing List. However we believe, that submitting an app to stable branch only means that author of the patch is not going to support the app. Therefore for the app to get backported it still has to be first accepted to master and all subsequent releases. - murano core library patches: Murano Core Library is an app, that provides core functionality and classes for other murano apps. It shares a lot of properties of regular murano apps and the rationale behind allowing backports of MuranoPL code from master to stable branches is basically the same: low regression risks during upgrades, high adoption impact. However since core library is much more sensitive app, backports to it should be taken more seriously and should be discussed on IRC and Mailing List and receive PTL's approval. These two exceptions do not mean, that we're free to backport any code from master to stable branches. Instead they show, that murano team recognises the importance of these two areas of murano project and treats exceptions to those slightly more liberally than to other parts of murano project. Bug nomination process ~~~~~~~~~~~~~~~~~~~~~~ Whenever you file a bug, or see a bug, that you think is eligible for backporting in stable branch nominate it for the corresponding series. If bug reporter does not nominate the bug for eligible branch — this is done by murano bug supervisor during triaging/confirmation process. In case it is not clear whether the bug is eligible or not or if you do not have permissions to nominate a bug for series you can set `$release-backport-potential` tag (for example `liberty-backport-potential`). Murano team is holding bi-weekly meetings on IRC (as part of regular community meetings) to triage and nominate bugs for stable backports. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/doc/source/contributor/testing.rst0000664000175000017500000000043600000000000021753 0ustar00zuulzuul00000000000000.. _testing: ======= Testing ======= Testing guidelines ~~~~~~~~~~~~~~~~~~ Continuous Integration service ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ UI testing ~~~~~~~~~~ Tempest tests ~~~~~~~~~~~~~ Automated testing machinery ~~~~~~~~~~~~~~~~~~~~~~~~~~~ CI design --------- CI jobs ------- ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1696417899.7331805 murano-16.0.0/doc/source/first-app/0000775000175000017500000000000000000000000017074 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/doc/source/first-app/Before_the_start.rst0000664000175000017500000000015500000000000023106 0ustar00zuulzuul00000000000000================ Before the start ================ What you need ------------- Deploy Murano ------------- ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/doc/source/first-app/Debugging_and_troubleshooting_your_murano_app.rst0000664000175000017500000000021200000000000031144 0ustar00zuulzuul00000000000000============================================= Debugging and troubleshooting your Murano app ============================================= ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/doc/source/first-app/Develop_murano_app_for_plone.rst0000664000175000017500000000255200000000000025514 0ustar00zuulzuul00000000000000============================ Develop Murano app for Plone ============================ Develop standalone Plone Murano app (single VM) ----------------------------------------------- Plone server requirements ~~~~~~~~~~~~~~~~~~~~~~~~~ Define host VM requirements ........................... Host VM operatting system requirements ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Host VM hardware resources requirements ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Define preinstalled software and libraries requirements ....................................................... Define what the PloneServerApp should do ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Create and debug sh-script that fully deploys the Plone server on a single VM ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Create Murano package for your app ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Upload and deploy your Murano app to OpenStack cloud ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Develop cluster Plone Murano app (multi VM) ------------------------------------------- Develop basic server-client Murano app ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Add load-balancing to the Plone cluster ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Add scalability to the Plone cluster ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Add self-healing to the Plone cluster ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/doc/source/first-app/Publish_your_murano_app_in_the_application_catalog.rst0000664000175000017500000000063700000000000032144 0ustar00zuulzuul00000000000000================================================== Publish your Murano app in the application catalog ================================================== Join the OpenStack community ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Prepare testing environment ~~~~~~~~~~~~~~~~~~~~~~~~~~~ Contribute your code to Murano-apps ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Contribute your code to App-catalog ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/doc/source/first-app/README.rst0000664000175000017500000000207700000000000020571 0ustar00zuulzuul00000000000000This directory contains the "My first Murano App getting started guide" tutorial. The tutorials work with an application that can be found in the `openstack/murano-apps `_ repository. Prerequisites ------------- To build the documentation, you must install the Graphviz package. /source ~~~~~~~ The :code:`/source` directory contains the tutorial documentation as `reStructuredText `_ (RST). To build the documentation, you must install `Sphinx `_ and the `OpenStack docs.openstack.org Sphinx theme (openstackdocstheme) `_. When you invoke tox, these dependencies are automatically pulled in from the top-level :code:`test-requirements.txt`. You must also install `Graphviz `_ on your build system. The document is build as part of the docs build, for example using:: tox -e docs /samples ~~~~~~~~ The code samples in this guide are located in this directory. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/doc/source/first-app/What_is_the_use_case.rst0000664000175000017500000000007700000000000023737 0ustar00zuulzuul00000000000000==================== What is the use case ==================== ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/doc/source/first-app/What_you_will_learn.rst0000664000175000017500000000007400000000000023636 0ustar00zuulzuul00000000000000=================== What you will learn =================== ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/doc/source/first-app/Who_is_this_guide_for.rst0000664000175000017500000000010200000000000024121 0ustar00zuulzuul00000000000000===================== Who is this guide for ===================== ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/doc/source/first-app/index.rst0000664000175000017500000000065400000000000020742 0ustar00zuulzuul00000000000000========================================= My first Murano App getting started guide ========================================= .. include:: README.rst Contents ~~~~~~~~ .. toctree:: :maxdepth: 3 Who_is_this_guide_for What_is_the_use_case What_you_will_learn Before_the_start Develop_murano_app_for_plone Debugging_and_troubleshooting_your_murano_app Publish_your_murano_app_in_the_application_catalog ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/doc/source/index.rst0000664000175000017500000000531300000000000017032 0ustar00zuulzuul00000000000000=============================== Welcome to Murano Documentation =============================== **Murano** is an open source OpenStack project that combines an application catalog with versatile tooling to simplify and accelerate packaging and deployment. It can be used with almost any application and service in OpenStack. Murano project consists of several source code repositories: * `murano`_ -- the main repository. It contains code for Murano API server, Murano engine and MuranoPL. * `murano-agent`_ -- the agent that runs on guest VMs and executes the deployment plan. * `murano-dashboard`_ -- Murano UI implemented as a plugin for the OpenStack Dashboard. * `python-muranoclient`_ -- Client library and CLI client for Murano. .. note:: `Administrator Documentation`, `Contributor Documentation`, and `Appendix` are under development at the moment. .. Links .. _murano: https://opendev.org/openstack/murano/ .. _murano-agent: https://opendev.org/openstack/murano-agent/ .. _murano-dashboard: https://opendev.org/openstack/murano-dashboard/ .. _python-muranoclient: https://opendev.org/openstack/python-muranoclient/ Introduction to Murano ~~~~~~~~~~~~~~~~~~~~~~ .. toctree:: :maxdepth: 1 reference/overview_index Using Murano ~~~~~~~~~~~~ Learn how to use the Application Catalog directly from the Dashboard and through the command-line interface (CLI), manage applications and environments. The screenshots provided in this guide are of the Liberty release. .. toctree:: :maxdepth: 1 user/quickstart/quickstart user/user_index Installation ~~~~~~~~~~~~ .. toctree:: :maxdepth: 2 install/index Configuration ~~~~~~~~~~~~~ .. toctree:: :maxdepth: 2 configuration/index CLI Guide ~~~~~~~~~ .. toctree:: :maxdepth: 2 cli/index Administrator Documentation ~~~~~~~~~~~~~~~~~~~~~~~~~~~ Learn how to manage images, categories, and repositories using the Murano client. .. toctree:: :maxdepth: 1 admin/index First App Guide ~~~~~~~~~~~~~~~ A guide for developing your first Murano application. .. toctree:: :maxdepth: 1 first-app/index Application Developer Documentation ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Learn how to compose an application package and get it ready for uploading to Murano. .. toctree:: :maxdepth: 1 admin/appdev-guide/developer_index admin/appdev-guide/faq Contributor Documentation ~~~~~~~~~~~~~~~~~~~~~~~~~ * If you are a new contributor to Murano please refer: :doc:`contributor/contributing` .. toctree:: :maxdepth: 1 contributor/contributing contributor/contributor_index Other Documentation ~~~~~~~~~~~~~~~~~~~ .. toctree:: :maxdepth: 1 reference/appendix/appendix_index reference/appendix/articles/articles_index ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1696417899.7371807 murano-16.0.0/doc/source/install/0000775000175000017500000000000000000000000016635 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/doc/source/install/common_prerequisites.rst0000664000175000017500000000461700000000000023653 0ustar00zuulzuul00000000000000Prerequisites ------------- Before you install and configure the Application Catalog service, you must create a database, service credentials, and API endpoints. #. To create the database, complete these steps: Murano can use various database types on the back end. For development purposes, SQLite is enough in most cases. For production installations, you should use MySQL or PostgreSQL databases. .. warning:: Although murano could use a PostgreSQL database on the back end, it wasn't thoroughly tested and should be used with caution. .. * Use the database access client to connect to the database server as the ``root`` user: .. code-block:: console $ mysql -u root -p .. * Create the ``murano`` database: .. code-block:: mysql CREATE DATABASE murano; .. * Grant proper access to the ``murano`` database: .. code-block:: mysql GRANT ALL PRIVILEGES ON murano.* TO 'murano'@'localhost' IDENTIFIED BY 'MURANO_DBPASS'; .. Replace ``MURANO_DBPASS`` with a suitable password. * Exit the database access client. .. code-block:: mysql exit; .. #. Source the ``admin`` credentials to gain access to admin-only CLI commands: .. code-block:: console $ . admin-openrc .. #. To create the service credentials, complete these steps: * Create the ``murano`` user: .. code-block:: console $ openstack user create --domain default --password-prompt murano .. * Add the ``admin`` role to the ``murano`` user: .. code-block:: console $ openstack role add --project service --user murano admin .. * Create the murano service entities: .. code-block:: console $ openstack service create --name murano --description "Application Catalog" application-catalog .. #. Create the Application Catalog service API endpoints: .. code-block:: console $ openstack endpoint create --region RegionOne \ application-catalog public http://:8082 $ openstack endpoint create --region RegionOne \ application-catalog internal http://:8082 $ openstack endpoint create --region RegionOne \ application-catalog admin http://:8082 .. .. note:: URLs (publicurl, internalurl and adminurl) may be different depending on your environment. .. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/doc/source/install/enable-ssl.rst0000664000175000017500000001231600000000000021417 0ustar00zuulzuul00000000000000.. Copyright 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. ================= SSL configuration ================= Murano components are able to work with SSL. This section will help you to configure proper settings for SSL configuration. HTTPS for Murano API ==================== SSL for the Murano API service can be configured in the *ssl* section in ``/etc/murano/murano.conf``. Just point to a valid SSL certificate. See the example below: :: [ssl] cert_file = PATH key_file = PATH ca_file = PATH - *cert\_file* Path to the certificate file the server should use when binding to an SSL-wrapped socket. - *key\_file* Path to the private key file the server should use when binding to an SSL-wrapped socket. - *ca\_file* Path to the CA certificate file the server should use to validate client certificates provided during an SSL handshake. This is ignored if cert\_file and "key\_file" are not set. .. note:: The use of SSL is automatically started after pointing to an HTTPS protocol instead of HTTP, during the registration of the Murano API service endpoints (Change publicurl argument to start with \https://). .. SSL for Murano API is implemented like in any other OpenStack component. This is because Murano uses the ssl python module; more information about it can be found `here`_. .. _`here`: https://docs.python.org/2/library/ssl.html SSL for RabbitMQ ================ All Murano components communicate with each other via RabbitMQ. This interaction can be encrypted with SSL. By default, all messages in Rabbit MQ are not encrypted. Each RabbitMQ Exchange should be configured separately. **Murano API <-> Rabbit MQ exchange <-> Murano Engine** Edit ssl parameters in default section of ``/etc/murano/murano.conf``. Set the ``rabbit_use_ssl`` option to *true* and configure the ssl kombu parameters. Specify the path to the SSL keyfile and SSL CA certificate in a regular format: /path/to/file without quotes or leave it empty to allow for self-signed certificates. :: # connect over SSL for RabbitMQ (boolean value) #rabbit_use_ssl=false # SSL version to use (valid only if SSL enabled). valid values # are TLSv1, SSLv23 and SSLv3. SSLv2 may be available on some # distributions (string value) #kombu_ssl_version= # SSL key file (valid only if SSL enabled) (string value) #kombu_ssl_keyfile= # SSL cert file (valid only if SSL enabled) (string value) #kombu_ssl_certfile= # SSL certification authority file (valid only if SSL enabled) # (string value) #kombu_ssl_ca_certs= **Murano Agent -> Rabbit MQ exchange** In the main murano configuration file, there is a section named *rabbitmq*, which is responsible for setting up communication between Murano Agent and Rabbit MQ. Just set the *ssl* parameter to True to enable ssl. :: [rabbitmq] host = localhost port = 5672 login = guest password = guest virtual_host = / ssl = True If you want to configure Murano Agent in a different way, change the default template. It can be found in the Murano Core Library, located at *https://opendev.org/openstack/murano/src/branch/master/meta/io.murano/Resources/Agent-v1.template*. Take a look at the appSettings section: :: The desired parameter should be set directly to the value of the key that you want to change. Quotes need to be kept. Thus you can change "rabbitmq.ssl" and "rabbitmq.port" values to make Rabbit MQ work with this exchange differently than the default Murano Engine way. .. note:: After modification, don't forget to zip and re-upload the core library. .. SSL for Murano Dashboard ======================== If you are not going to use self-signed certificates, additional configuration does not need to be done. Just prefix https in the URL. Otherwise, set *MURANO_API_INSECURE = True* in Horizon's config file. You can find it in ``/etc/openstack-dashboard/local_settings.py``. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/doc/source/install/from-source.rst0000664000175000017500000001610000000000000021626 0ustar00zuulzuul00000000000000Install Murano from Source ~~~~~~~~~~~~~~~~~~~~~~~~~~ This section describes how to install and configure the Application Catalog service for Ubuntu 16.04 (LTS) from source code. .. include:: common_prerequisites.rst Install the API service and Engine ---------------------------------- #. Create a folder which will hold all Murano components. .. code-block:: console mkdir ~/murano .. #. Clone the murano git repository to the management server. .. code-block:: console cd ~/murano git clone https://opendev.org/openstack/murano .. #. Set up the murano config file Murano has a common config file for API and Engine services. First, generate a sample configuration file, using tox .. code-block:: console cd ~/murano/murano tox -e genconfig .. And make a copy of it for further modifications .. code-block:: console cd ~/murano/murano/etc/murano ln -s murano.conf.sample murano.conf .. #. Edit ``murano.conf`` with your favorite editor. Below is an example which contains basic settings you likely need to configure. .. note:: The example below uses SQLite database. Edit **[database]** section if you want to use any other database type. .. .. code-block:: ini [DEFAULT] debug = true verbose = true transport_url = rabbit://%RABBITMQ_USER%:%RABBITMQ_PASSWORD%@%RABBITMQ_SERVER_IP%:5672/ ... [database] connection = mysql+pymysql://murano:MURANO_DBPASS@controller/murano ... [keystone] auth_url = http://%OPENSTACK_KEYSTONE_ENDPOINT% ... [keystone_authtoken] project_domain_name = Default project_name = %OPENSTACK_ADMIN_PROJECT% user_domain_name = Default password = %OPENSTACK_ADMIN_PASSWORD% username = %OPENSTACK_ADMIN_USER% auth_url = http://%OPENSTACK_KEYSTONE_ENDPOINT% auth_type = password ... [murano] url = http://%YOUR_HOST_IP%:8082 [rabbitmq] host = %RABBITMQ_SERVER_IP% login = %RABBITMQ_USER% password = %RABBITMQ_PASSWORD% virtual_host = %RABBITMQ_SERVER_VIRTUAL_HOST% [networking] default_dns = 8.8.8.8 # In case openstack neutron has no default # DNS configured .. #. Create a virtual environment and install Murano prerequisites. We will use *tox* for that. The virtual environment will be created under *.tox* directory. .. code-block:: console cd ~/murano/murano tox .. #. Create database tables for Murano. .. code-block:: console cd ~/murano/murano tox -e venv -- murano-db-manage \ --config-file ./etc/murano/murano.conf upgrade .. #. Open a new console and launch Murano API. A separate terminal is required because the console will be locked by a running process. .. code-block:: console cd ~/murano/murano tox -e venv -- murano-api --config-file ./etc/murano/murano.conf .. #. Import Core Murano Library. .. code-block:: console cd ~/murano/murano pushd ./meta/io.murano zip -r ../../io.murano.zip * popd tox -e venv -- murano --murano-url http://localhost:8082 \ package-import --is-public io.murano.zip .. #. Open a new console and launch Murano Engine. A separate terminal is required because the console will be locked by a running process. .. code-block:: console cd ~/murano/murano tox -e venv -- murano-engine --config-file ./etc/murano/murano.conf .. Install Murano Dashboard ======================== Murano API & Engine services provide the core of Murano. However, your need a control plane to use it. This section describes how to install and run Murano Dashboard. #. Clone the murano dashboard repository. .. code-block:: console $ cd ~/murano $ git clone https://opendev.org/openstack/murano-dashboard .. #. Clone the ``horizon`` repository .. code-block:: console $ git clone https://opendev.org/openstack/horizon .. #. Create a virtual environment and install ``muranodashboard`` as an editable module: .. code-block:: console $ cd horizon $ tox -e venv -- pip install -e ../murano-dashboard .. #. Prepare local settings. .. code-block:: console $ cp openstack_dashboard/local/local_settings.py.example \ openstack_dashboard/local/local_settings.py .. For more information, check out the official `horizon documentation `_. #. Enable and configure Murano dashboard in the OpenStack Dashboard: * For Newton (and later) OpenStack installations, copy the plugin file, local settings files, and policy files. .. code-block:: console $ cp ../murano-dashboard/muranodashboard/local/enabled/*.py \ openstack_dashboard/local/enabled/ $ cp ../murano-dashboard/muranodashboard/local/local_settings.d/*.py \ openstack_dashboard/local/local_settings.d/ $ cp ../murano-dashboard/muranodashboard/conf/* openstack_dashboard/conf/ .. * For the OpenStack installations prior to the Newton release, run: .. code-block:: console $ cp ../murano-dashboard/muranodashboard/local/_50_murano.py \ openstack_dashboard/local/enabled/ .. Customize local settings of your horizon installation, by editing the :file:`openstack_dashboard/local/local_settings.py` file: .. code-block:: python ... ALLOWED_HOSTS = '*' # Provide OpenStack Lab credentials OPENSTACK_HOST = '%OPENSTACK_HOST_IP%' ... DEBUG_PROPAGATE_EXCEPTIONS = DEBUG .. Change the default session back end-from using browser cookies to using a database instead to avoid issues with forms during the creation of applications: .. code-block:: python DATABASES = { 'default': { 'ENGINE': 'django.db.backends.sqlite3', 'NAME': 'murano-dashboard.sqlite', } } SESSION_ENGINE = 'django.contrib.sessions.backends.db' .. #. (Optional) If you do not plan to get the murano service from the keystone application catalog, specify where the murano-api service is running: .. code-block:: python MURANO_API_URL = 'http://%MURANO_IP%:8082' .. #. (Optional) If you have set up the database as a session back-end (this is done by default with the murano local_settings file starting with Newton), perform database migration: .. code-block:: console $ tox -e venv -- python manage.py migrate --noinput .. #. Run the Django server at 127.0.0.1:8000 or provide different IP and PORT parameters: .. code-block:: console $ tox -e venv -- python manage.py runserver .. .. note:: The development server restarts automatically following every code change. .. **Result:** The murano dashboard is available at http://IP:PORT. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/doc/source/install/get_started.rst0000664000175000017500000000151000000000000021671 0ustar00zuulzuul00000000000000==================================== Application Catalog service overview ==================================== The Application Catalog service consists of the following components: ``murano`` command-line client A CLI that communicates with the ``murano-api`` to publish various cloud-ready applications on new virtual machines. ``murano-api`` service An OpenStack-native REST API that processes API requests by sending them to the ``murano-engine`` service via AMQP. ``murano-agent`` service The agent that runs on guest VMs and executes the deployment plan, a combination of execution plan templates and scripts. ``murano-engine`` service The workflow component of Murano, responsible for the deployment of an environment. ``murano-dashboard`` service Murano UI implemented as a plugin for the OpenStack Dashboard. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/doc/source/install/import-murano-apps.rst0000664000175000017500000000231700000000000023144 0ustar00zuulzuul00000000000000.. Copyright 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. Applications need to be imported to fill the catalog. This can be done via the dashboard or via CLI: 1. Clone the murano apps repository. .. code-block:: console cd ~/murano git clone https://opendev.org/openstack/murano-apps .. 2. Import every package you need from this repository, using the command below. .. code-block:: console cd ~/murano/murano pushd ../murano-apps/Docker/Applications/%APP-NAME%/package zip -r ~/murano/murano/app.zip * popd tox -e venv -- murano --murano-url http://:8082 package-import app.zip ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/doc/source/install/index.rst0000664000175000017500000000124400000000000020477 0ustar00zuulzuul00000000000000=========================== Application Catalog service =========================== .. toctree:: :maxdepth: 2 get_started.rst install.rst verify.rst next-steps.rst The Murano Project introduces an application catalog to OpenStack, enabling application developers and cloud administrators to publish various cloud-ready applications in a browsable categorized catalog. Cloud users -- including inexperienced ones -- can then use the catalog to compose reliable application environments with the push of a button. This chapter assumes a working setup of OpenStack following the `OpenStack Installation Tutorial `_. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/doc/source/install/install-api.rst0000664000175000017500000000535200000000000021611 0ustar00zuulzuul00000000000000.. Copyright 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. Install Murano API ~~~~~~~~~~~~~~~~~~ This section describes how to install and configure the Application Catalog service for Ubuntu 16.04 (LTS). .. include:: common_prerequisites.rst Install and configure components -------------------------------- #. Install the packages: .. code-block:: console # apt-get update # apt-get install murano-engine murano-api #. Edit ``murano.conf`` with your favorite editor. Below is an example which contains basic settings you likely need to configure. .. note:: The example below uses SQLite database. Edit **[database]** section if you want to use any other database type. .. .. code-block:: ini [DEFAULT] debug = true verbose = true transport_url = rabbit://%RABBITMQ_USER%:%RABBITMQ_PASSWORD%@%RABBITMQ_SERVER_IP%:5672/ ... [database] connection = mysql+pymysql://murano:MURANO_DBPASS@controller/murano ... [keystone] auth_url = http://%OPENSTACK_KEYSTONE_ENDPOINT% ... [keystone_authtoken] project_domain_name = Default project_name = %OPENSTACK_ADMIN_PROJECT% user_domain_name = Default password = %OPENSTACK_ADMIN_PASSWORD% username = %OPENSTACK_ADMIN_USER% auth_url = http://%OPENSTACK_KEYSTONE_ENDPOINT% auth_type = password ... [murano] url = http://%YOUR_HOST_IP%:8082 [rabbitmq] host = %RABBITMQ_SERVER_IP% login = %RABBITMQ_USER% password = %RABBITMQ_PASSWORD% virtual_host = %RABBITMQ_SERVER_VIRTUAL_HOST% [networking] default_dns = 8.8.8.8 # In case openstack neutron has no default # DNS configured .. #. Populate the Murano database: .. code-block:: console # su -s /bin/sh -c "murano-db-manage upgrade" murano .. note:: Ignore any deprecation messages in this output. Finalize installation --------------------- #. Restart the Application Catalog services: .. code-block:: console # service murano-api restart # service murano-engine restart ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/doc/source/install/install-dashboard.rst0000664000175000017500000000421000000000000022757 0ustar00zuulzuul00000000000000.. Copyright 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. Install Murano Dashboard ======================== Murano API & Engine services provide the core of Murano. However, your need a control plane to use it. This section describes how to install and run Murano Dashboard. #. Install OpenStack Dashboard, the steps please reference from `OpenStack Dashboard Install Guide `__. #. Install the packages: .. code-block:: console # apt install python-murano-dashboard #. Edit the ``/etc/openstack-dashboard/local_settings.py`` file to customize local settings of your envi .. code-block:: python ... OPENSTACK_HOST = '%OPENSTACK_HOST_IP%' OPENSTACK_KEYSTONE_DEFAULT_ROLE = '%OPENSTACK_ROLE%' ... .. Change the default session back end-from using browser cookies to using a database instead to avoid issues with forms during the creation of applications: .. code-block:: python DATABASES = { 'default': { 'ENGINE': 'django.db.backends.sqlite3', 'NAME': 'murano-dashboard.sqlite', } } SESSION_ENGINE = 'django.contrib.sessions.backends.db' .. #. (Optional) If you do not plan to get the murano service from the keystone application catalog, specify where the murano-api service is running: .. code-block:: python MURANO_API_URL = 'http://%MURANO_IP%:8082' .. Finalize installation --------------------- #. Restart the Apache service: .. code-block:: console # service apache2 restart ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/doc/source/install/install-network-config.rst0000664000175000017500000000461500000000000023775 0ustar00zuulzuul00000000000000.. Copyright 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. ===================== Network Configuration ===================== Murano may work in various networking environments and is capable of detecting the current network configuration and choosing the appropriate settings automatically. However, some additional actions are required to support advanced scenarios. Nova network support ==================== Nova Network is the simplest networking solution, which has limited capabilities but is available on any OpenStack deployment without the need to deploy any additional components. For more information about Nova Network, see ``__. When a new Murano Environment is created, Murano checks if a dedicated networking service (i.e. Neutron) exists in the current OpenStack deployment. It relies on Keystone's service catalog for that. If such a service is not present, Murano automatically falls back to Nova Network. No further configuration is needed in this case; all the VMs spawned by Murano will join the same network. Neutron support =============== If Neutron is installed, Murano enables its advanced networking features that give you the ability to not care about configuring networks for your application. By default, Murano will create an isolated network for each environment and attach all VMs needed by your application to that network. To install and configure applications in just-spawned virtual machines, Murano also requires a router connected to the external network. Automatic Neutron network configuration ======================================= To create a router automatically, provide the following parameters in the config file: .. code-block:: ini [networking] external_network = %EXTERNAL_NETWORK_NAME% router_name = %MURANO_ROUTER_NAME% create_router = true .. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/doc/source/install/install.rst0000664000175000017500000000171500000000000021041 0ustar00zuulzuul00000000000000.. _install: Install and configure ~~~~~~~~~~~~~~~~~~~~~ This section describes how to install and configure the Application Catalog service, code-named murano, on the controller node. This section assumes that you already have a working OpenStack environment with at least the following components installed: Identity service, Image service, Compute service, Networking service, Block Storage service and Orchestration service. See `OpenStack Install Guides `__. Note that installation and configuration vary by distribution. Currently, this installation guide is tailored toward Ubuntu environments, but can easily be adapted to work with other types of distros. .. note:: Fedora support wasn't thoroughly tested. We do not guarantee that murano will work on Fedora. .. .. toctree:: :maxdepth: 2 install-api.rst install-dashboard.rst from-source.rst install-network-config.rst enable-ssl.rst ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/doc/source/install/next-steps.rst0000664000175000017500000000073200000000000021503 0ustar00zuulzuul00000000000000.. _next-steps: Next steps ~~~~~~~~~~ Your OpenStack environment now includes the Murano service. Import Murano Applications -------------------------- .. include:: import-murano-apps.rst Additional Resources -------------------- #. To add additional services, see ``__. #. If you would like to add glare as the storage service for packages, see: ``__. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/doc/source/install/verify.rst0000664000175000017500000000106400000000000020674 0ustar00zuulzuul00000000000000.. _verify: Verify operation ~~~~~~~~~~~~~~~~ Verify operation of the Application Catalog service. .. note:: Perform these commands on the controller node. #. Source the ``admin`` project credentials to gain access to admin-only CLI commands: .. code-block:: console $ . admin-openrc #. List service components to verify successful launch and registration of each process: .. code-block:: console $ openstack service list | grep application-catalog | 7b12ef5edef848fc9200c271f71b1307 | murano | application-catalog |././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1696417899.7371807 murano-16.0.0/doc/source/reference/0000775000175000017500000000000000000000000017125 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1696417899.7371807 murano-16.0.0/doc/source/reference/appendix/0000775000175000017500000000000000000000000020735 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/doc/source/reference/appendix/appendix_index.rst0000664000175000017500000000022400000000000024464 0ustar00zuulzuul00000000000000Appendix ~~~~~~~~ .. toctree:: :maxdepth: 2 murano_concepts tutorials rest_api_spec cli_ref glossary articles/articles_index ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1696417899.7371807 murano-16.0.0/doc/source/reference/appendix/articles/0000775000175000017500000000000000000000000022543 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/doc/source/reference/appendix/articles/articles_index.rst0000664000175000017500000000064200000000000026274 0ustar00zuulzuul00000000000000Miscellaneous ~~~~~~~~~~~~~ **Background Concepts for Murano** .. toctree:: :maxdepth: 1 workflow **Tutorials** .. toctree:: :maxdepth: 1 image_builders/index test_docs **Guidelines** .. toctree:: :maxdepth: 1 guidelines **Gerrit review dashboard** .. toctree:: :maxdepth: 1 murano_gerrit_dashboard **API specification** .. toctree:: :maxdepth: 1 specification/index ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/doc/source/reference/appendix/articles/guidelines.rst0000664000175000017500000000433400000000000025431 0ustar00zuulzuul00000000000000====================== Development Guidelines ====================== Coding Guidelines ----------------- For all the code in Murano we have a rule - it should pass `PEP 8`_. To check your code against PEP 8 run: :: tox -e pep8 .. seealso:: * https://pep8.readthedocs.org/en/latest/ * https://flake8.readthedocs.org * https://docs.openstack.org/hacking/latest/ Blueprints and Specs -------------------- Murano team uses the `murano-specs`_ repository for its blueprint and specification (specs) review process. See `Launchpad`_ to propose or see the status of a current blueprint. Testing Guidelines ------------------ Murano has a suite of tests that are run on all submitted code, and it is recommended that developers execute the tests themselves to catch regressions early. Developers are also expected to keep the test suite up-to-date with any submitted code changes. Unit tests are located at ``murano/tests``. Murano's suite of unit tests can be executed in an isolated environment with `Tox`_. To execute the unit tests run the following from the root of Murano repo on Python 3.x: :: tox -e py3.x Documentation Guidelines ------------------------ Murano dev-docs are written using Sphinx / RST and located in the main repo in ``doc`` directory. The documentation in docstrings should follow the `PEP 257`_ conventions (as mentioned in the `PEP 8`_ guidelines). More specifically: 1. Triple quotes should be used for all docstrings. 2. If the docstring is simple and fits on one line, then just use one line. 3. For docstrings that take multiple lines, there should be a newline after the opening quotes, and before the closing quotes. 4. `Sphinx`_ is used to build documentation, so use the restructured text markup to designate parameters, return values, etc. Documentation on the sphinx specific markup can be found here: Run the following command to build docs locally. :: tox -e docs .. _PEP 8: http://www.python.org/dev/peps/pep-0008/ .. _PEP 257: http://www.python.org/dev/peps/pep-0257/ .. _Tox: http://tox.testrun.org/ .. _Sphinx: http://sphinx.pocoo.org/markup/index.html .. _murano-specs: http://opendev.org/openstack/murano-specs .. _Launchpad: http://blueprints.launchpad.net/murano ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1696417899.7411807 murano-16.0.0/doc/source/reference/appendix/articles/image_builders/0000775000175000017500000000000000000000000025516 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/doc/source/reference/appendix/articles/image_builders/index.rst0000664000175000017500000000022500000000000027356 0ustar00zuulzuul00000000000000.. _building_images: ===================== Building Murano Image ===================== .. toctree:: :maxdepth: 2 windows linux upload ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/doc/source/reference/appendix/articles/image_builders/linux.rst0000664000175000017500000000257100000000000027414 0ustar00zuulzuul00000000000000=========== Linux Image =========== At the moment the best way to build a Linux image with the murano agent is to use disk image builder. .. note:: Disk image builder requires sudo rights The process is quite simple. Let's assume that you use a directory ~/git for cloning git repositories: .. code-block:: console export GITDIR=~/git mkdir -p $GITDIR Clone the components required to build an image to that directory: .. code-block:: console cd $GITDIR git clone https://opendev.org/openstack/murano git clone https://opendev.org/openstack/murano-agent Install diskimage-builder .. code-block:: console sudo pip install diskimage-builder Install additional packages required by disk image builder: .. code-block:: console sudo apt-get install qemu-utils curl python3-tox Export paths where additional dib elements are located: .. code-block:: console export ELEMENTS_PATH=$GITDIR/murano/contrib/elements:$GITDIR/murano-agent/contrib/elements Build Ubuntu-based image with the murano agent: .. code-block:: console disk-image-create vm ubuntu murano-agent -o murano-agent.qcow2 If you need a Fedora based image, replace 'ubuntu' to 'fedora' in the last command. It'll take a while (up to 30 minutes if your hard drive and internet connection are slow). When you are done upload the murano-agent.qcow2 image to glance and play :) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/doc/source/reference/appendix/articles/image_builders/upload.rst0000664000175000017500000000466000000000000027542 0ustar00zuulzuul00000000000000.. _upload_images: ======================== Upload image into glance ======================== To deploy applications with murano, virtual machine images should be uploaded into glance in a special way - *murano_image_info* property should be set. 1. Use the OpenStack client image create command to import your disk image to glance: .. code-block:: console openstack image create --public \ > --disk-format qcow2 --container-format bare \ > --file --property .. Replace the command line arguments to openstack image create with the appropriate values for your environment and disk image: * Replace **** with the local path to the image file to upload. E.g. **ws-2012-std.qcow2**. * Replace **** with the following property string * Replace **** with the name that users will refer to the disk image by. E.g. **ws-2012-std** .. code-block:: text murano_image_info='{"title": "Windows 2012 Standard Edition", "type": "windows.2012"}' .. where: * **title** - user-friendly description of the image * **type** - murano image type, see :ref:`murano_image_types` 2. To update metadata of the existing image run the command: .. code-block:: console openstack image set --property .. * Replace **** with murano_image_info property, e.g. * Replace **** with image id from the previous command output. .. code-block:: text murano_image_info='{"title": "Windows 2012 Standard Edition", "type": "windows.2012"}' .. .. warning:: The value of the **--property** argument (named **murano_image_info**) is a JSON string. Only double quotes are valid in JSON, so please type the string exactly as in the example above. .. .. note:: Existing images could be marked in a simple way in the horizon UI with the murano dashboard installed. Navigate to *Applications -> Manage -> Images -> Mark Image* and fill up a form: * **Image** - ws-2012-std * **Title** - My Prepared Image * **Type** - Windows Server 2012 .. After these steps desired image can be chosen in application creation wizard. .. _murano_image_types: Murano image types ------------------ .. list-table:: :header-rows: 1 * - Type Name - Description * - windows.2012 - Windows Server 2012 * - linux - Generic Linux images, Ubuntu / Debian, RedHat / Centos, etc * - cirros.demo - Murano demo image, based on CirrOS .. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/doc/source/reference/appendix/articles/image_builders/windows.rst0000664000175000017500000001214100000000000027741 0ustar00zuulzuul00000000000000MS Windows image builder for OpenStack Murano ============================================= Introduction ------------ This repository contains MS Windows templates, powershell scripts and bash scripted logic used to create qcow2 images for QEMU/KVM based virtual machines used in OpenStack. MS Windows Versions ------------------- Supported by builder versions with en_US localization: * Windows 2012 R2 * Windows 2012 R2 Core * Windows 2008 R2 * Windows 2008 R2 Core Getting Started --------------- Trial versions of Windows 2008 R2 / 2012 R2 used by default. You could use these images for 180 days without activation. You could download evaluation versions from official Microsoft website: * `[Windows 2012 R2 - download] `_ * `[Windows 2008 R2 - download] `_ System requirements ~~~~~~~~~~~~~~~~~~~ * Debian based Linux distribution, like Ubuntu, Mint and so on. * Packages required: ``qemu-kvm virt-manager virt-goodies virtinst bridge-utils libvirt-bin uuid-runtime samba samba-common cifs-utils`` * User should be able to run sudo without password prompt! .. code-block:: console sudo echo "${USER} ALL = NOPASSWD: ALL" > /etc/sudoers.d/${USER} sudo chmod 440 /etc/sudoers.d/${USER} * Free disk space > 50G on partition where script will spawn virtual machines because of ``40G`` required by virtual machine HDD image. * Internet connectivity. * Samba shared resource. Configuring builder ~~~~~~~~~~~~~~~~~~~ Configuration parameters to tweak: ``[default]`` * ``workdir`` - place where script would prepare all software required by build scenarios. By `default` is not set, i.e. script directory would used as root of working space. * ``vmsworkdir`` - must contain valid path, this parameter tells script where it should spawn virtual machines. * ``runparallel`` - *true* of *false*, **false** set by default. This parameter describes how to start virtual machines, one by one or in launch them in background. ``[samba]`` * ``mode`` - *local* or *remote*. In local mode script would try to install and configure Samba server locally. If set to remote, you should also provide information about connection. * ``host`` - in local mode - is 192.168.122.1, otherwise set proper ip address. * ``user`` - set to **guest** by default in case of guest rw access. * ``domain`` - Samba server user domain, if not set `host` value used. * ``password`` - Samba server user password. * ``image-builder-share`` - Samba server remote directory. MS Windows install preparation: ``[win2k12r2]`` or ``[win2k8r2]`` - shortcuts for 2012 R2 and 2008 R2. * ``enabled`` - *true* of *false*, include or exclude release processing by script. * ``editions`` - standard, core or both(space used as delimiter). * ``iso`` - local path to iso file By default ``[win2k8r2]`` - disabled, if you need you can enable this release in *config.ini* file. Run --- Preparation ~~~~~~~~~~~ Run ``chmod +x *.sh`` in builder directory to make script files executable. Command line parameters: ~~~~~~~~~~~~~~~~~~~~~~~~ ``runme.sh`` - the main script * ``--help`` - shows usage * ``--forceinstall-dependencies`` - Runs dependencies install. * ``--check-smb`` - Run checks or configuration of Samba server. * ``--download-requirements`` - Download all required and configures software except MS Windows ISO. * ``--show-configured`` - Shows configured and available to use MS Windows releases. * ``--run`` - normal run Experimental options: ^^^^^^^^^^^^^^^^^^^^^ * ``--config-file`` - Set configuration file location instead of default. Use cases --------- All examples below describes changes in ``config.ini`` file 1. I want to build one image for specific version and edition. For example: version - **2012 R2** and edition - **standard**. Steps to reach the goal: * Disable ``[win2k8r2]`` from script processing. .. code-block:: ini [win2k8r2] enabled=false - Update ``[win2k12r2]`` with desired edition(**standard**). .. code-block:: ini [win2k12r2] enabled=true editions=standard * Execute ``runme.sh --run`` 2. I want to build two images for specific version with all supported by script editions. For example: **2012 R2** and editions - **standard** and **core**. Steps to reach the goal: * Disable `[win2k8r2]` from script processing. .. code-block:: ini [win2k8r2] enabled=false * Update ``[win2k12r2]`` with desired editions(**standard** and **core**). .. code-block:: ini [win2k12r2] enabled=true editions=standard core * Execute ``runme.sh --run`` 3. I want to build two images for all supported by script versions with specific editions. For example: versions - **2012 R2** and **2008 R2** and edition - **core**. Steps to reach the goal: * Update ``[win2k8r2]`` with desired edition(**core**). .. code-block:: ini [win2k8r2] enabled=true editions=core * Update ``[win2k12r2]`` with desired edition(**core**). .. code-block:: ini [win2k12r2] enabled=true editions=core * Execute ``runme.sh --run`` ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/doc/source/reference/appendix/articles/multi_region.rst0000664000175000017500000000403000000000000025767 0ustar00zuulzuul00000000000000.. _multi-region: ============================= Support for OpenStack regions ============================= Murano supports multi-region deployment. If OpenStack setup has several regions it is possible to choose the region to deploy an application. There is the new option in the murano configuration file: * `home_region` - default region name used to get services endpoints. The region where murano-api resides. Now murano has two possible ways to deploy apps in different regions: 1. Deploy an application in the current murano region. 2. Associate environments with regions. Deploy an app in the current region =================================== Each region has a copy of murano services and its own RabbitMQ for api to engine communication. In this case application will be deployed to the same region that murano run in. .. seealso:: :ref:`multi_region` Associate environments with regions =================================== Murano services are in one region but environments can be associated with different regions. There are two new properties in the class `io.murano.Environment`: * `regionConfigs` - a dict with RabbitMQ settings for each region. The structure of the agentRabbitMq part of the dict is identical to [rabbitmq] section in the `murano.conf` file. For example: .. code-block:: yaml regionConfigs: RegionOne: agentRabbitMq: host: 192.1.1.1 login: admin password: admin User can store such configs as YAML or JSON files. These config files must be stored in a special folder that is configured in [engine] section of `murano.conf` file under `class_configs` key and must be named using %FQ class name%.json or %FQ class name%.yaml pattern. * `region` - region name to deploy an app. It can be passed when creating environment via CLI: .. code-block:: console murano environment-create environment_name --region RegionOne If it is not specified a value from `home_region` option of `murano.conf` file will be used. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/doc/source/reference/appendix/articles/murano_gerrit_dashboard.rst0000664000175000017500000000663500000000000030173 0ustar00zuulzuul00000000000000Murano Gerrit Dashboard ======================= Description ----------- If you would like to contribute to murano by reviewing patches to murano-related projects — you can use this gerrit dashboard, or create your own using `Gerrit Dash Creator `__ URL --- :: https://review.opendev.org/#/dashboard/?foreach=%28project%3A%5E.%2A%2F.%2Amurano.%2A+OR+project%3Aopenstack%2Fyaql%29+NOT+label%3AWorkflow%3C%3D%2D1+NOT+label%3ACode%2DReview%3C%3D%2D2+status%3Aopen&title=Murano&My+Patches=owner%3Aself&You+are+a+reviewer%2C+but+haven%27t+voted+in+the+current+revision=NOT+label%3ACode%2DReview%3C%3D2%2Cself+reviewer%3Aself+NOT+owner%3Aself&Need+Feedback=NOT+label%3ACode%2DReview%3C%3D2+NOT+label%3AVerified%3C%3D%2D1+NOT+owner%3Aself&Passed+Jenkins%2C+No+Negative+Feedback=label%3ACode%2DReview%3E%3D1+NOT+label%3ACode%2DReview%3C%3D%2D1+AND+NOT+label%3AVerified%3C%3D%2D1+NOT+owner%3Aself+NOT+reviewer%3Aself+limit%3A50&Maybe+Review%3F=NOT+owner%3Aself+NOT+reviewer%3Aself+limit%3A25&My+%2B1s=label%3ACode%2DReview%3D1%2Cself+limit%3A25&Need+final+%2B2=label%3ACode%2DReview%3E%3D2+NOT+label%3ACode%2DReview%3C%3D%2D1+NOT+label%3AVerified%3C%3D%2D1+NOT+label%3ACode%2DReview%3C%3D2%2Cself+NOT+owner%3Aself+limit%3A25&My+%2B2s=label%3ACode%2DReview%3D2%2Cself+limit%3A25 `View this dashboard `__ Configuration ------------- :: [dashboard] title = Murano description = Murano Review Inbox foreach = (project:^.*/.*murano.* OR project:openstack/yaql) NOT label:Workflow<=-1 NOT label:Code-Review<=-2 status:open [section "My Patches"] query = owner:self [section "You are a reviewer, but haven't voted in the current revision"] query = NOT label:Code-Review<=2,self reviewer:self NOT owner:self [section "Need Feedback"] query = NOT label:Code-Review<=2 NOT label:Verified<=-1 NOT owner:self [section "Passed Jenkins, No Negative Feedback"] query = label:Code-Review>=1 NOT label:Code-Review<=-1 AND NOT label:Verified<=-1 NOT owner:self NOT reviewer:self limit:50 [section "Maybe Review?"] query = NOT owner:self NOT reviewer:self limit:25 [section "My +1s"] query = label:Code-Review=1,self limit:25 [section "Need final +2"] query = label:Code-Review>=2 NOT label:Code-Review<=-1 NOT label:Verified<=-1 NOT label:Code-Review<=2,self NOT owner:self limit:25 [section "My +2s"] query = label:Code-Review=2,self limit:25 ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1696417899.7411807 murano-16.0.0/doc/source/reference/appendix/articles/specification/0000775000175000017500000000000000000000000025363 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/doc/source/reference/appendix/articles/specification/index.rst0000664000175000017500000000026400000000000027226 0ustar00zuulzuul00000000000000=========================== Murano API v1 specification =========================== .. toctree:: :maxdepth: 1 overview murano-api murano-repository murano-env-temp././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/doc/source/reference/appendix/articles/specification/murano-api.rst0000664000175000017500000015707300000000000030202 0ustar00zuulzuul00000000000000========== Murano API ========== Glossary ======== .. _glossary-environment: * **Environment** The environment is a set of applications managed by a single project (tenant). They could be related logically with each other or not. Applications within a single environment may comprise of complex configuration while applications in different environments are always independent from one another. Each environment is associated with a single OpenStack project. .. _glossary-sessions: * **Session** Since murano environments are available for local modification for different users and from different locations, it's needed to store local modifications somewhere. Sessions were created to provide this opportunity. After a user adds an application to the environment - a new session is created. After a user sends an environment to deploy, a session with a set of applications changes status to *deploying* and all other open sessions for that environment become invalid. One session could be deployed only once. * **Object Model** Applications are defined in MuranoPL object model, which is defined as a JSON object. The murano API doesn't know anything about it. * **Package** A .zip archive, containing instructions for an application deployment. * **Environment-Template** The environment template is the specification of a set of applications managed by a single project, which are related to each other. The environment template is stored in an environment template catalog, and it can be managed by the user (creation, deletion, updating). Finally, it can be deployed on OpenStack by translating into an environment. Environment API =============== +----------------------+------------+-------------------------------------------+ | Attribute | Type | Description | +======================+============+===========================================+ | id | string | Unique ID | +----------------------+------------+-------------------------------------------+ | name | string | User-friendly name | +----------------------+------------+-------------------------------------------+ | created | datetime | Creation date and time in ISO format | +----------------------+------------+-------------------------------------------+ | updated | datetime | Modification date and time in ISO format | +----------------------+------------+-------------------------------------------+ | tenant_id | string | OpenStack project ID | +----------------------+------------+-------------------------------------------+ | version | int | Current version | +----------------------+------------+-------------------------------------------+ | networking | string | Network settings | +----------------------+------------+-------------------------------------------+ | acquired_by | string | Id of a session that acquired this | | | | environment (for example is deploying it) | +----------------------+------------+-------------------------------------------+ | status | string | Deployment status: ready, pending, | | | | deploying | +----------------------+------------+-------------------------------------------+ **Common response codes** +----------------+-----------------------------------------------------------+ | Code | Description | +================+===========================================================+ | 200 | Operation completed successfully | +----------------+-----------------------------------------------------------+ | 403 | User is not authorized to perform the operation | +----------------+-----------------------------------------------------------+ List environments ----------------- *Request* +----------+----------------------------------+----------------------------------+ | Method | URI | Description | +==========+==================================+==================================+ | GET | /environments | Get a list of existing | | | | Environments | +----------+----------------------------------+----------------------------------+ *Parameters:* * `all_tenants` - boolean, indicates whether environments from all projects are listed. *True* environments from all projects are listed. Admin user required. *False* environments only from current project are listed (default like option unspecified). * `tenant` - indicates environments from specified tenant are listed. Admin user required. *Response* This call returns a list of environments. Only the basic properties are returned. :: { "environments": [ { "status": "ready", "updated": "2014-05-14T13:02:54", "networking": {}, "name": "test1", "created": "2014-05-14T13:02:46", "tenant_id": "726ed856965f43cc8e565bc991fa76c3", "version": 0, "id": "2fa5ab704749444bbeafe7991b412c33" }, { "status": "ready", "updated": "2014-05-14T13:02:55", "networking": {}, "name": "test2", "created": "2014-05-14T13:02:51", "tenant_id": "726ed856965f43cc8e565bc991fa76c3", "version": 0, "id": "744e44812da84e858946f5d817de4f72" } ] } Create environment ------------------ +----------------------+------------+--------------------------------------------------------+ | Attribute | Type | Description | +======================+============+========================================================+ | name | string | Environment name; at least one non-white space symbol | +----------------------+------------+--------------------------------------------------------+ *Request* +----------+----------------------------------+----------------------------------+ | Method | URI | Description | +==========+==================================+==================================+ | POST | /environments | Create new Environment | +----------+----------------------------------+----------------------------------+ * **Content-Type** application/json * **Example** {"name": "env_name"} *Response* :: { "id": "ce373a477f211e187a55404a662f968", "name": "env_name", "created": "2013-11-30T03:23:42Z", "updated": "2013-11-30T03:23:44Z", "tenant_id": "0849006f7ce94961b3aab4e46d6f229a", "version": 0 } Update environment ------------------ +----------------------+------------+--------------------------------------------------------+ | Attribute | Type | Description | +======================+============+========================================================+ | name | string | Environment name; at least one non-white space symbol | +----------------------+------------+--------------------------------------------------------+ *Request* +----------+----------------------------------+----------------------------------+ | Method | URI | Description | +==========+==================================+==================================+ | PUT | /environments/ | Update an existing Environment | +----------+----------------------------------+----------------------------------+ * **Content-Type** application/json * **Example** {"name": "env_name_changed"} *Response* **Content-Type** application/json :: { "id": "ce373a477f211e187a55404a662f968", "name": "env_name_changed", "created": "2013-11-30T03:23:42Z", "updated": "2013-11-30T03:45:54Z", "tenant_id": "0849006f7ce94961b3aab4e46d6f229a", "version": 0 } +----------------+-----------------------------------------------------------+ | Code | Description | +================+===========================================================+ | 200 | Edited environment | +----------------+-----------------------------------------------------------+ | 400 | Environment name must contain at least one non-white space| | | symbol | +----------------+-----------------------------------------------------------+ | 403 | User is not authorized to access environment | +----------------+-----------------------------------------------------------+ | 404 | Environment not found | +----------------+-----------------------------------------------------------+ | 409 | Environment with specified name already exists | +----------------+-----------------------------------------------------------+ Get environment details ----------------------- *Request* Return information about the environment itself and about applications, including this environment. +----------+----------------------------------+-----------------------------------+----------------------------------+ | Method | URI | Header | Description | +==========+==================================+===================================+==================================+ | GET | /environments/{id} | X-Configuration-Session (optional)| Response detailed information | | | | | about Environment including | | | | | child entities | +----------+----------------------------------+-----------------------------------+----------------------------------+ *Response* **Content-Type** application/json :: { "status": "ready", "updated": "2014-05-14T13:12:26", "networking": {}, "name": "quick-env-2", "created": "2014-05-14T13:09:55", "tenant_id": "726ed856965f43cc8e565bc991fa76c3", "version": 1, "services": [ { "instance": { "flavor": "m1.medium", "image": "cloud-fedora-v3", "name": "exgchhv6nbika2", "ipAddresses": [ "10.0.0.200" ], "?": { "type": "io.murano.resources.Instance", "id": "14cce9d9-aaa1-4f09-84a9-c4bb859edaff" } }, "name": "rewt4w56", "?": { "status": "ready", "_26411a1861294160833743e45d0eaad9": { "name": "Telnet" }, "type": "io.murano.apps.linux.Telnet", "id": "446373ef-03b5-4925-b095-6c56568fa518" } } ], "id": "20d4a012628e4073b48490a336a8acbf" } Delete environment ------------------ *Request* +----------+----------------------------------+----------------------------------+ | Method | URI | Description | +==========+==================================+==================================+ | DELETE | /environments/{id}?abandon | Remove specified Environment. | +----------+----------------------------------+----------------------------------+ *Parameters:* * `abandon` - boolean, indicates how to delete environment. *False* is used if all resources used by environment must be destroyed; *True* is used when just database must be cleaned *Response* +----------------+-----------------------------------------------------------+ | Code | Description | +================+===========================================================+ | 200 | OK. Environment deleted successfully | +----------------+-----------------------------------------------------------+ | 403 | User is not allowed to delete this resource | +----------------+-----------------------------------------------------------+ | 404 | Not found. Specified environment doesn`t exist | +----------------+-----------------------------------------------------------+ Environment configuration API ============================= Multiple :ref:`sessions ` could be opened for one environment simultaneously, but only one session going to be deployed. First session that starts deploying is going to be deployed; other ones become invalid and could not be deployed at all. User could not open new session for environment that in *deploying* state (that's why we call it "almost lock free" model). +----------------------+------------+-------------------------------------------+ | Attribute | Type | Description | +======================+============+===========================================+ | id | string | Session unique ID | +----------------------+------------+-------------------------------------------+ | environment\_id | string | Environment that going to be modified | | | | during this session | +----------------------+------------+-------------------------------------------+ | created | datetime | Creation date and time in ISO format | +----------------------+------------+-------------------------------------------+ | updated | datetime | Modification date and time in ISO format | +----------------------+------------+-------------------------------------------+ | user\_id | string | Session owner ID | +----------------------+------------+-------------------------------------------+ | version | int | Environment version for which | | | | configuration session is opened | +----------------------+------------+-------------------------------------------+ | state | string | Session state. Could be: open, deploying, | | | | deployed | +----------------------+------------+-------------------------------------------+ Configure environment / open session ------------------------------------ During this call a new working session is created with its ID returned in response body. Notice that the session ID should be added to request headers with name ``X-Configuration-Session`` in subsequent requests when necessary. *Request* +----------+----------------------------------+----------------------------------+ | Method | URI | Description | +==========+==================================+==================================+ | POST | /environments//configure | Creating new configuration | | | | session | +----------+----------------------------------+----------------------------------+ *Response* **Content-Type** application/json :: { "id": "257bef44a9d848daa5b2563779714820", "updated": datetime.datetime(2014, 5, 14, 14, 17, 58, 949358), "environment_id": "744e44812da84e858946f5d817de4f72", "ser_id": "4e91d06270c54290b9dbdf859356d3b3", "created": datetime.datetime(2014, 5, 14, 14, 17, 58, 949305), "state": "open", "version": 0L } +----------------+-----------------------------------------------------------+ | Code | Description | +================+===========================================================+ | 200 | Session created successfully | +----------------+-----------------------------------------------------------+ | 401 | User is not authorized to access this session | +----------------+-----------------------------------------------------------+ | 403 | Could not open session for environment, environment has | | | deploying status | +----------------+-----------------------------------------------------------+ | 404 | Not found. Specified environment doesn`t exist | +----------------+-----------------------------------------------------------+ Deploy session -------------- With this request all local changes made within the environment start to deploy on OpenStack. *Request* +----------+---------------------------------+--------------------------------+ | Method | URI | Description | +==========+=================================+================================+ | POST | /environments//sessions/| Deploy changes made in session | | | /deploy | with specified session_id | +----------+---------------------------------+--------------------------------+ *Response* +----------------+-----------------------------------------------------------+ | Code | Description | +================+===========================================================+ | 200 | Session status changes to *deploying* | +----------------+-----------------------------------------------------------+ | 401 | User is not authorized to access this session | +----------------+-----------------------------------------------------------+ | 403 | Session is already deployed or deployment is in progress | +----------------+-----------------------------------------------------------+ | 404 | Not found. Specified session or environment doesn`t exist | +----------------+-----------------------------------------------------------+ Get session details ------------------- *Request* +----------+---------------------------------+---------------------------+ | Method | URI | Description | +==========+=================================+===========================+ | GET | /environments//sessions/| Get details about session | | | | with specified session_id | +----------+---------------------------------+---------------------------+ *Response* :: { "id": "4aecdc2178b9430cbbb8db44fb7ac384", "environment_id": "4dc8a2e8986fa8fa5bf24dc8a2e8986fa8", "created": "2013-11-30T03:23:42Z", "updated": "2013-11-30T03:23:54Z", "user_id": "d7b501094caf4daab08469663a9e1a2b", "version": 0, "state": "deploying" } +----------------+-----------------------------------------------------------+ | Code | Description | +================+===========================================================+ | 200 | Session details information received | +----------------+-----------------------------------------------------------+ | 401 | User is not authorized to access this session | +----------------+-----------------------------------------------------------+ | 403 | Session is invalid | +----------------+-----------------------------------------------------------+ | 404 | Not found. Specified session or environment doesn`t exist | +----------------+-----------------------------------------------------------+ Delete session -------------- *Request* +----------+---------------------------------+----------------------------------+ | Method | URI | Description | +==========+=================================+==================================+ | DELETE | /environments//sessions/| Delete session with specified | | | | session_id | +----------+---------------------------------+----------------------------------+ *Response* +----------------+-----------------------------------------------------------+ | Code | Description | +================+===========================================================+ | 200 | Session is deleted successfully | +----------------+-----------------------------------------------------------+ | 401 | User is not authorized to access this session | +----------------+-----------------------------------------------------------+ | 403 | Session is in deploying state and could not be deleted | +----------------+-----------------------------------------------------------+ | 404 | Not found. Specified session or environment doesn`t exist | +----------------+-----------------------------------------------------------+ Environment model API ===================== Get environment model --------------------- +----------+-------------------------------------+------------------------+--------------------------+ | Method | URI | Header | Description | +==========+=====================================+========================+==========================+ | GET | /environments//model/ | X-Configuration-Session| Get an Environment model | | | | (optional) | | +----------+-------------------------------------+------------------------+--------------------------+ Specifying allows to get a specific section of the model, for example `defaultNetworks`, `region` or `?` or any of the subsections. *Response* **Content-Type** application/json .. code-block:: javascript { "defaultNetworks": { "environment": { "internalNetworkName": "net_two", "?": { "type": "io.murano.resources.ExistingNeutronNetwork", "id": "594e94fcfe4c48ef8f9b55edb3b9f177" } }, "flat": null }, "region": "RegionTwo", "name": "new_env", "regions": { "": { "defaultNetworks": { "environment": { "autoUplink": true, "name": "new_env-network", "externalRouterId": null, "dnsNameservers": [], "autogenerateSubnet": true, "subnetCidr": null, "openstackId": null, "?": { "dependencies": { "onDestruction": [{ "subscriber": "c80e33dd67a44f489b2f04818b72f404", "handler": null }] }, "type": "io.murano.resources.NeutronNetwork/0.0.0@io.murano", "id": "e145b50623c04a68956e3e656a0568d3", "name": null }, "regionName": "RegionOne" }, "flat": null }, "name": "RegionOne", "?": { "type": "io.murano.CloudRegion/0.0.0@io.murano", "id": "c80e33dd67a44f489b2f04818b72f404", "name": null } }, "RegionOne": "c80e33dd67a44f489b2f04818b72f404", "RegionTwo": { "defaultNetworks": { "environment": { "autoUplink": true, "name": "new_env-network", "externalRouterId": "e449bdd5-228c-4747-a925-18cda80fbd6b", "dnsNameservers": ["8.8.8.8"], "autogenerateSubnet": true, "subnetCidr": "10.0.198.0/24", "openstackId": "00a695c1-60ff-42ec-acb9-b916165413da", "?": { "dependencies": { "onDestruction": [{ "subscriber": "f8cb28d147914850978edb35eca156e1", "handler": null }] }, "type": "io.murano.resources.NeutronNetwork/0.0.0@io.murano", "id": "72d2c13c600247c98e09e2e3c1cd9d70", "name": null }, "regionName": "RegionTwo" }, "flat": null }, "name": "RegionTwo", "?": { "type": "io.murano.CloudRegion/0.0.0@io.murano", "id": "f8cb28d147914850978edb35eca156e1", "name": null } } }, services: [] "?": { "type": "io.murano.Environment/0.0.0@io.murano", "_actions": { "f7f22c174070455c9cafc59391402bdc_deploy": { "enabled": true, "name": "deploy", "title": "deploy" } }, "id": "f7f22c174070455c9cafc59391402bdc", "name": null } } +----------------+-----------------------------------------------------------+ | Code | Description | +================+===========================================================+ | 200 | Environment model received successfully | +----------------+-----------------------------------------------------------+ | 403 | User is not authorized to access environment | +----------------+-----------------------------------------------------------+ | 404 | Environment is not found or specified section of the | | | model does not exist | +----------------+-----------------------------------------------------------+ Update environment model ------------------------ *Request* +----------+--------------------------------+------------------------+-----------------------------+ | Method | URI | Header | Description | +==========+================================+========================+=============================+ | PATCH | /environments//model/ | X-Configuration-Session| Update an Environment model | +----------+--------------------------------+------------------------+-----------------------------+ * **Content-Type** application/env-model-json-patch Allowed operations for paths: * "" (model root): no operations * "defaultNetworks": "replace" * "defaultNetworks/environment": "replace" * "defaultNetworks/environment/?/id": no operations * "defaultNetworks/flat": "replace" * "name": "replace" * "region": "replace" * "?/type": "replace" * "?/id": no operations For other paths any operation (add, replace or remove) is allowed. * **Example of request body with JSON-patch** .. code-block:: javascript [{ "op": "replace", "path": "/defaultNetworks/flat", "value": true }] *Response* **Content-Type** application/json See *GET* request response. +----------------+-----------------------------------------------------------+ | Code | Description | +================+===========================================================+ | 200 | Environment is edited successfully | +----------------+-----------------------------------------------------------+ | 400 | Body format is invalid | +----------------+-----------------------------------------------------------+ | 403 | User is not authorized to access environment or specified | | | operation is forbidden for the given property | +----------------+-----------------------------------------------------------+ | 404 | Environment is not found or specified section of the | | | model does not exist | +----------------+-----------------------------------------------------------+ Environment deployments API =========================== Environment deployment API allows to track changes of environment status, deployment events and errors. It also allows to browse deployment history. List Deployments ---------------- Returns information about all deployments of the specified environment. *Request* +----------+------------------------------------+--------------------------------------+ | Method | URI | Description | +==========+====================================+======================================+ | GET | /environments//deployments | Get list of environment deployments | +----------+------------------------------------+--------------------------------------+ | GET | /deployments | Get list of deployments for all | | | | environments in user's project | +----------+------------------------------------+--------------------------------------+ *Response* **Content-Type** application/json :: { "deployments": [ { "updated": "2014-05-15T07:24:21", "environment_id": "744e44812da84e858946f5d817de4f72", "description": { "services": [ { "instance": { "flavor": "m1.medium", "image": "cloud-fedora-v3", "?": { "type": "io.murano.resources.Instance", "id": "ef729199-c71e-4a4c-a314-0340e279add8" }, "name": "xkaduhv7qeg4m7" }, "name": "teslnet1", "?": { "_26411a1861294160833743e45d0eaad9": { "name": "Telnet" }, "type": "io.murano.apps.linux.Telnet", "id": "6e437be2-b5bc-4263-8814-6fd57d6ddbd5" } } ], "defaultNetworks": { "environment": { "name": "test2-network", "?": { "type": "io.murano.lib.networks.neutron.NewNetwork", "id": "b6a1d515434047d5b4678a803646d556" } }, "flat": null }, "name": "test2", "?": { "type": "io.murano.Environment", "id": "744e44812da84e858946f5d817de4f72" } }, "created": "2014-05-15T07:24:21", "started": "2014-05-15T07:24:21", "finished": null, "state": "running", "id": "327c81e0e34a4c93ad9b9052ef42b752" } ] } +----------------+-----------------------------------------------------------+ | Code | Description | +================+===========================================================+ | 200 | Deployments information received successfully | +----------------+-----------------------------------------------------------+ | 401 | User is not authorized to access this environment | +----------------+-----------------------------------------------------------+ Application management API ========================== All applications should be created within an environment and all environment modifications are held within the session. Local changes apply only after successful deployment of an environment session. Get application details ----------------------- Using GET requests to applications endpoint user works with list containing all applications for specified environment. A user can request a whole list, specific application, or specific attribute of a specific application using tree traversing. To request a specific application, the user should add to endpoint part an application id, e.g.: */environments//services/*. For selection of specific attribute on application, simply appending part with attribute name will work. For example to request application name, user should use next endpoint: */environments//services//name* *Request* +----------------+-----------------------------------------------------------+------------------------------------+ | Method | URI | Header | +================+===========================================================+====================================+ | GET | /environments//services/ | X-Configuration-Session (optional) | +----------------+-----------------------------------------------------------+------------------------------------+ **Parameters:** * `env_id` - environment ID, required * `app_id` - application ID, optional *Response* **Content-Type** application/json :: { "instance": { "flavor": "m1.medium", "image": "cloud-fedora-v3", "?": { "type": "io.murano.resources.Instance", "id": "060715ff-7908-4982-904b-3b2077ff55ef" }, "name": "hbhmyhv6qihln3" }, "name": "dfg34", "?": { "status": "pending", "_26411a1861294160833743e45d0eaad9": { "name": "Telnet" }, "type": "io.murano.apps.linux.Telnet", "id": "6e7b8ad5-888d-4c5a-a498-076d092a7eff" } } Create new application ---------------------- Create a new application and add it to the murano environment. Result JSON is calculated in Murano dashboard, which is based on `UI definition `_. *Request* **Content-Type** application/json +----------------+-----------------------------------------------------------+------------------------------------+ | Method | URI | Header | +================+===========================================================+====================================+ | POST | /environments//services | X-Configuration-Session | +----------------+-----------------------------------------------------------+------------------------------------+ :: { "instance": { "flavor": "m1.medium", "image": "clod-fedora-v3", "?": { "type": "io.murano.resources.Instance", "id": "bce8308e-5938-408b-a27a-0d3f0a2c52eb" }, "name": "nhekhv6r7mhd4" }, "name": "sdf34sadf", "?": { "_26411a1861294160833743e45d0eaad9": { "name": "Telnet" }, "type": "io.murano.apps.linux.Telnet", "id": "190c8705-5784-4782-83d7-0ab55a1449aa" } } *Response* Created application returned **Content-Type** application/json :: { "instance": { "flavor": "m1.medium", "image": "cloud-fedora-v3", "?": { "type": "io.murano.resources.Instance", "id": "bce8308e-5938-408b-a27a-0d3f0a2c52eb" }, "name": "nhekhv6r7mhd4" }, "name": "sdf34sadf", "?": { "_26411a1861294160833743e45d0eaad9": { "name": "Telnet" }, "type": "io.murano.apps.linux.Telnet", "id": "190c8705-5784-4782-83d7-0ab55a1449a1" } } +----------------+-----------------------------------------------------------+ | Code | Description | +================+===========================================================+ | 200 | Application was created successfully | +----------------+-----------------------------------------------------------+ | 401 | User is not authorized to perform this action | +----------------+-----------------------------------------------------------+ | 403 | Policy prevents this user from performing this action | +----------------+-----------------------------------------------------------+ | 404 | Not found. Environment doesn't exist | +----------------+-----------------------------------------------------------+ | 400 | Required header or body are not provided | +----------------+-----------------------------------------------------------+ Update applications ------------------- Applications list for environment can be updated. *Request* **Content-Type** application/json +----------------+-----------------------------------------------------------+------------------------------------+ | Method | URI | Header | +================+===========================================================+====================================+ | PUT | /environments//services | X-Configuration-Session | +----------------+-----------------------------------------------------------+------------------------------------+ :: [{ "instance": { "availabilityZone": "nova", "name": "apache-instance", "assignFloatingIp": true, "keyname": "", "flavor": "m1.small", "image": "146d5523-7b2d-4abc-b0d0-2041f920ce26", "?": { "type": "io.murano.resources.LinuxMuranoInstance", "id": "25185cb6f29b415fa2e438309827a797" } }, "name": "ApacheHttpServer", "enablePHP": true, "?": { "type": "com.example.apache.ApacheHttpServer", "id": "6e66106d7dcb4748a5c570150a3df80f" } }] *Response* Updated applications list returned **Content-Type** application/json :: [{ "instance": { "availabilityZone": "nova", "name": "apache-instance", "assignFloatingIp": true, "keyname": "", "flavor": "m1.small", "image": "146d5523-7b2d-4abc-b0d0-2041f920ce26", "?": { "type": "io.murano.resources.LinuxMuranoInstance", "id": "25185cb6f29b415fa2e438309827a797" } }, "name": "ApacheHttpServer", "enablePHP": true, "?": { "type": "com.example.apache.ApacheHttpServer", "id": "6e66106d7dcb4748a5c570150a3df80f" } }] +----------------+-----------------------------------------------------------+ | Code | Description | +================+===========================================================+ | 200 | Services are updated successfully | +----------------+-----------------------------------------------------------+ | 400 | Required header is not provided | +----------------+-----------------------------------------------------------+ | 401 | User is not authorized | +----------------+-----------------------------------------------------------+ | 403 | Session is in deploying state and could not be updated | | | or user is not allowed to update services | +----------------+-----------------------------------------------------------+ | 404 | Not found. Specified environment and/or session do not | | | exist | +----------------+-----------------------------------------------------------+ Delete application from environment ----------------------------------- Delete one or all applications from the environment *Request* +----------------+-----------------------------------------------------------+-----------------------------------+ | Method | URI | Header | +================+===========================================================+===================================+ | DELETE | /environments//services/ | X-Configuration-Session(optional) | +----------------+-----------------------------------------------------------+-----------------------------------+ **Parameters:** * `env_id` - environment ID, required * `app_id` - application ID, optional Statistic API ============= Statistic API intends to provide billing feature Instance environment statistics ------------------------------- *Request* Get information about all deployed instances in the specified environment +----------------+--------------------------------------------------------------+ | Method | URI | +================+==============================================================+ | GET | /environments//instance-statistics/raw/ | +----------------+--------------------------------------------------------------+ **Parameters:** * `env_id` - environment ID, required * `instance_id` - ID of the instance for which need to provide statistic information, optional *Response* +----------------------+------------+-----------------------------------------------------------------+ | Attribute | Type | Description | +======================+============+=================================================================+ | type | int | Code of the statistic object; 200 - instance, 100 - application | +----------------------+------------+-----------------------------------------------------------------+ | type_name | string | Class name of the statistic object | +----------------------+------------+-----------------------------------------------------------------+ | instance_id | string | Id of deployed instance | +----------------------+------------+-----------------------------------------------------------------+ | active | bool | Instance status | +----------------------+------------+-----------------------------------------------------------------+ | type_title | string | User-friendly name for browsing statistic in UI | +----------------------+------------+-----------------------------------------------------------------+ | duration | int | Seconds of instance uptime | +----------------------+------------+-----------------------------------------------------------------+ **Content-Type** application/json :: [ { "type": 200, "type_name": "io.murano.resources.Instance", "instance_id": "ef729199-c71e-4a4c-a314-0340e279add8", "active": true, "type_title": null, "duration": 1053, } ] *Request* +----------------+--------------------------------------------------------------+ | Method | URI | +================+==============================================================+ | GET | /environments//instance-statistics/aggregated | +----------------+--------------------------------------------------------------+ *Response* +----------------------+------------+-----------------------------------------------------------------+ | Attribute | Type | Description | +======================+============+=================================================================+ | type | int | Code of the statistic object; 200 - instance, 100 - application | +----------------------+------------+-----------------------------------------------------------------+ | duration | int | Amount uptime of specified type objects | +----------------------+------------+-----------------------------------------------------------------+ | count | int | Quantity of specified type objects | +----------------------+------------+-----------------------------------------------------------------+ **Content-Type** application/json :: [ { "duration": 720, "count": 2, "type": 200 } ] General Request Statistics -------------------------- *Request* +----------------+---------------+ | Method | URI | +================+===============+ | GET | /stats | +----------------+---------------+ *Response* +----------------------+------------+-----------------------------------------------------------------+ | Attribute | Type | Description | +======================+============+=================================================================+ | requests_per_tenant | int | Number of incoming requests for user project | +----------------------+------------+-----------------------------------------------------------------+ | errors_per_second | int | Class name of the statistic object | +----------------------+------------+-----------------------------------------------------------------+ | errors_count | int | Class name of the statistic object | +----------------------+------------+-----------------------------------------------------------------+ | requests_per_second | float | Average number of incoming request received in one second | +----------------------+------------+-----------------------------------------------------------------+ | requests_count | int | Number of all requests sent to the server | +----------------------+------------+-----------------------------------------------------------------+ | cpu_percent | bool | Current cpu usage | +----------------------+------------+-----------------------------------------------------------------+ | cpu_count | int | Available cpu power is ``cpu_count * 100%`` | +----------------------+------------+-----------------------------------------------------------------+ | host | string | Server host-name | +----------------------+------------+-----------------------------------------------------------------+ | average_response_time| float | Average time response waiting, seconds | +----------------------+------------+-----------------------------------------------------------------+ **Content-Type** application/json :: [ { "updated": "2014-05-15T08:26:17", "requests_per_tenant": "{\"726ed856965f43cc8e565bc991fa76c3\": 313}", "created": "2014-04-29T13:23:59", "cpu_count": 2, "errors_per_second": 0, "requests_per_second": 0.0266528, "cpu_percent": 21.7, "host": "fervent-VirtualBox", "error_count": 0, "request_count": 320, "id": 1, "average_response_time": 0.55942 } ] Actions API =========== Murano actions are simple MuranoPL methods, that can be called on deployed applications. Application contains a list with available actions. Actions may return a result. Execute an action ----------------- Generate task with executing specified action. Input parameters may be provided. *Request* **Content-Type** application/json +----------------+-----------------------------------------------------------+------------------------------------+ | Method | URI | Header | +================+===========================================================+====================================+ | POST | /environments//actions/ | | +----------------+-----------------------------------------------------------+------------------------------------+ **Parameters:** * `env_id` - environment ID, required * `actions_id` - action ID to execute, required :: "{: value}" or "{}" in case action has no properties *Response* Task ID that executes specified action is returned **Content-Type** application/json :: { "task_id": "620e883070ad40a3af566d465aa156ef" } GET action result ----------------- Request result value after action execution finish. Not all actions have return values. *Request* +----------------+-----------------------------------------------------------+------------------------------------+ | Method | URI | Header | +================+===========================================================+====================================+ | GET | /environments//actions/ | | +----------------+-----------------------------------------------------------+------------------------------------+ **Parameters:** * `env_id` - environment ID, required * `task_id` - task ID, generated on desired action execution *Response* Json, describing action result is returned. Result type and value are provided. **Content-Type** application/json :: { "isException": false, "result": ["item1", "item2"] } Static Actions API ================== Static actions are MuranoPL methods that can be called on a MuranoPL class without deploying actual applications and usually return a result. Execute a static action ----------------------- Invoke public static method of the specified MuranoPL class. Input parameters may be provided if method requires them. *Request* **Content-Type** application/json +----------------+-----------------------------------------------------------+------------------------------------+ | Method | URI | Header | +================+===========================================================+====================================+ | POST | /actions | | +----------------+-----------------------------------------------------------+------------------------------------+ :: { "className": "my.class.fqn", "methodName": "myMethod", "packageName": "optional.package.fqn", "classVersion": "1.2.3", "parameters": { "arg1": "value1", "arg2": "value2" } } +-----------------+------------+-----------------------------------------------------------------------------+ | Attribute | Type | Description | +=================+============+=============================================================================+ | className | string | Fully qualified name of MuranoPL class with static method | +-----------------+------------+-----------------------------------------------------------------------------+ | methodName | string | Name of the method to invoke | +-----------------+------------+-----------------------------------------------------------------------------+ | packageName | string | Fully qualified name of a package with the MuranoPL class (optional) | +-----------------+------------+-----------------------------------------------------------------------------+ | classVersion | string | Class version specification, "=0" by default | +-----------------+------------+-----------------------------------------------------------------------------+ | parameters | object | Key-value pairs of method parameter names and their values, "{}" by default | +-----------------+------------+-----------------------------------------------------------------------------+ *Response* JSON-serialized result of the static method execution. HTTP codes: +----------------+-----------------------------------------------------------+ | Code | Description | +================+===========================================================+ | 200 | OK. Action was executed successfully | +----------------+-----------------------------------------------------------+ | 400 | Bad request. The format of the body is invalid, method | | | doesn't match provided arguments, mandatory arguments are | | | not provided | +----------------+-----------------------------------------------------------+ | 403 | User is not allowed to execute the action | +----------------+-----------------------------------------------------------+ | 404 | Not found. Specified class, package or method doesn't | | | exist or method is not exposed | +----------------+-----------------------------------------------------------+ | 503 | Unhandled exception in the action | +----------------+-----------------------------------------------------------+ ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/doc/source/reference/appendix/articles/specification/murano-env-temp.rst0000664000175000017500000005461700000000000031164 0ustar00zuulzuul00000000000000Environment template API ======================== Manage environment template definitions in murano. It is possible to create, update, delete, and deploy into OpenStack by translating it into an environment. In addition, applications can be added to or deleted from the environment template. **Environment Template Properties** +----------------------+------------+-------------------------------------------+ | Attribute | Type | Description | +======================+============+===========================================+ | id | string | Unique ID | +----------------------+------------+-------------------------------------------+ | name | string | User-friendly name | +----------------------+------------+-------------------------------------------+ | created | datetime | Creation date and time in ISO format | +----------------------+------------+-------------------------------------------+ | updated | datetime | Modification date and time in ISO format | +----------------------+------------+-------------------------------------------+ | tenant_id | string | OpenStack project | +----------------------+------------+-------------------------------------------+ | version | int | Current version | +----------------------+------------+-------------------------------------------+ | networking | string | Network settings | +----------------------+------------+-------------------------------------------+ | description | string | The environment template specification | +----------------------+------------+-------------------------------------------+ **Common response codes** +----------------+-----------------------------------------------------------+ | Code | Description | +================+===========================================================+ | 200 | Operation completed successfully | +----------------+-----------------------------------------------------------+ | 401 | User is not authorized to perform the operation | +----------------+-----------------------------------------------------------+ Methods for Environment Template API List Environments Templates --------------------------- *Request* +----------+----------------------------------+----------------------------------+ | Method | URI | Description | +==========+==================================+==================================+ | GET | /templates | Get a list of existing | | | | environment templates | +----------+----------------------------------+----------------------------------+ *Parameters:* * `is_public` - boolean, indicates whether public environment templates are listed or not. *True* public environments templates from all projects are listed. *False* private environments templates from current project are listed *empty* all project templates plus public templates from all projects are listed *Response* This call returns a list of environment templates. Only the basic properties are returned. :: { "templates": [ { "updated": "2014-05-14T13:02:54", "networking": {}, "name": "test1", "created": "2014-05-14T13:02:46", "tenant_id": "726ed856965f43cc8e565bc991fa76c3", "version": 0, "is_public": false, "id": "2fa5ab704749444bbeafe7991b412c33" }, { "updated": "2014-05-14T13:02:55", "networking": {}, "name": "test2", "created": "2014-05-14T13:02:51", "tenant_id": "123452452345346345634563456345346", "version": 0, "is_public": true, "id": "744e44812da84e858946f5d817de4f72" } ] } Create environment template --------------------------- +----------------------+------------+---------------------------------------------------------+ | Attribute | Type | Description | +======================+============+=========================================================+ | name | string | Environment template name; only alphanumeric characters | | | and '-' | | +----------------------+------------+---------------------------------------------------------+ *Request* +----------+--------------------------------+--------------------------------------+ | Method | URI | Description | +==========+================================+======================================+ | POST | /templates | Create a new environment template | +----------+--------------------------------+--------------------------------------+ *Content-Type* application/json *Example* {"name": "env_temp_name"} *Response* :: { "id": "ce373a477f211e187a55404a662f968", "name": "env_temp_name", "created": "2013-11-30T03:23:42Z", "updated": "2013-11-30T03:23:44Z", "tenant_id": "0849006f7ce94961b3aab4e46d6f229a", } +----------------+-----------------------------------------------------------+ | Code | Description | +================+===========================================================+ | 200 | Operation completed successfully | +----------------+-----------------------------------------------------------+ | 401 | User is not authorized to perform the operation | +----------------+-----------------------------------------------------------+ | 409 | The environment template already exists | +----------------+-----------------------------------------------------------+ Get environment templates details --------------------------------- *Request* Return information about environment template itself and about applications, including to this environment template. +----------+--------------------------------+-------------------------------------------------+ | Method | URI | Description | +==========+================================+=================================================+ | GET | /templates/{env-temp-id} | Obtains the environment template information | +----------+--------------------------------+-------------------------------------------------+ * `env-temp-id` - environment template ID, required *Response* *Content-Type* application/json :: { "updated": "2015-01-26T09:12:51", "networking": { }, "name": "template_name", "created": "2015-01-26T09:12:51", "tenant_id": "00000000000000000000000000000001", "version": 0, "id": "aa9033ca7ce245fca10e38e1c8c4bbf7", } +----------------+-----------------------------------------------------------+ | Code | Description | +================+===========================================================+ | 200 | OK. Get environment template details successfully | +----------------+-----------------------------------------------------------+ | 401 | User is not authorized to access this session | +----------------+-----------------------------------------------------------+ | 404 | The environment template does not exist | +----------------+-----------------------------------------------------------+ Delete environment template --------------------------- *Request* +----------+-----------------------------------+-----------------------------------+ | Method | URI | Description | +==========+===================================+===================================+ | DELETE | /templates/ | Delete the template id | +----------+-----------------------------------+-----------------------------------+ *Parameters:* * `env-temp_id` - environment template ID, required *Response* +----------------+-----------------------------------------------------------+ | Code | Description | +================+===========================================================+ | 200 | OK. Environment Template deleted successfully | +----------------+-----------------------------------------------------------+ | 401 | User is not authorized to access this session | +----------------+-----------------------------------------------------------+ | 404 | The environment template does not exist | +----------------+-----------------------------------------------------------+ Adding application to environment template ------------------------------------------ *Request* +----------+------------------------------------+----------------------------------+ | Method | URI | Description | +==========+====================================+==================================+ | POST | /templates/{env-temp-id}/services | Create a new application | +----------+------------------------------------+----------------------------------+ *Parameters:* * `env-temp-id` - The environment-template id, required * payload - the service description *Content-Type* application/json *Example* :: { "instance": { "assignFloatingIp": "true", "keyname": "mykeyname", "image": "cloud-fedora-v3", "flavor": "m1.medium", "?": { "type": "io.murano.resources.LinuxMuranoInstance", "id": "ef984a74-29a4-45c0-b1dc-2ab9f075732e" } }, "name": "orion", "port": "8080", "?": { "type": "io.murano.apps.apache.Tomcat", "id": "54cea43d-5970-4c73-b9ac-fea656f3c722" } } *Response* :: { "instance": { "assignFloatingIp": "true", "keyname": "mykeyname", "image": "cloud-fedora-v3", "flavor": "m1.medium", "?": { "type": "io.murano.resources.LinuxMuranoInstance", "id": "ef984a74-29a4-45c0-b1dc-2ab9f075732e" } }, "name": "orion", "?": { "type": "io.murano.apps.apache.Tomcat", "id": "54cea43d-5970-4c73-b9ac-fea656f3c722" }, "port": "8080" } +----------------+-----------------------------------------------------------+ | Code | Description | +================+===========================================================+ | 200 | OK. Application added successfully | +----------------+-----------------------------------------------------------+ | 401 | User is not authorized to access this session | +----------------+-----------------------------------------------------------+ | 404 | The environment template does not exist | +----------------+-----------------------------------------------------------+ Delete application from an environment template ----------------------------------------------- *Request* +----------+---------------------------------------------+--------------------------------------+ | Method | URI | Description | +==========+=============================================+======================================+ | DELETE | /templates/{env-temp-id}/services/{app-id} | Delete application with Specified id | +----------+---------------------------------------------+--------------------------------------+ *Parameters:* * `env-temp-id` - The environment template ID, required * `app-id` - The application ID, required *Content-Type* application/json *Response* :: { "updated": "2015-01-26T09:12:51", "services": [], "name": "template_name", "created": "2015-01-26T09:12:51", "tenant_id": "00000000000000000000000000000001", "version": 0, "id": "aa9033ca7ce245fca10e38e1c8c4bbf7", } +----------------+-----------------------------------------------------------+ | Code | Description | +================+===========================================================+ | 200 | OK. Application deleted successfully | +----------------+-----------------------------------------------------------+ | 401 | User is not authorized to access this session | +----------------+-----------------------------------------------------------+ | 404 | The application does not exist | +----------------+-----------------------------------------------------------+ Get applications information from an environment template --------------------------------------------------------- *Request* +----------+-------------------------------------+-----------------------------------+ | Method | URI | Description | +==========+=====================================+===================================+ | GET | /templates/{env-temp-id}/services | It obtains the service description| +----------+-------------------------------------+-----------------------------------+ *Parameters:* * `env-temp-id` - The environment template ID, required *Content-Type* application/json *Response* :: [ { "instance": { "assignFloatingIp": "true", "keyname": "mykeyname", "image": "cloud-fedora-v3", "flavor": "m1.medium", "?": { "type": "io.murano.resources.LinuxMuranoInstance", "id": "ef984a74-29a4-45c0-b1dc-2ab9f075732e" } }, "name": "tomcat", "?": { "type": "io.murano.apps.apache.Tomcat", "id": "54cea43d-5970-4c73-b9ac-fea656f3c722" }, "port": "8080" }, { "instance": "ef984a74-29a4-45c0-b1dc-2ab9f075732e", "password": "XXX", "name": "mysql", "?": { "type": "io.murano.apps.database.MySQL", "id": "54cea43d-5970-4c73-b9ac-fea656f3c722" } } ] +----------------+-----------------------------------------------------------+ | Code | Description | +================+===========================================================+ | 200 | OK. Application information received successfully | +----------------+-----------------------------------------------------------+ | 401 | User is not authorized to access this session | +----------------+-----------------------------------------------------------+ | 404 | The environment template does not exist | +----------------+-----------------------------------------------------------+ Update applications information from an environment template ------------------------------------------------------------ *Request* +----------+-----------------------------------------------+-----------------------------------+ | Method | URI | Description | +==========+===============================================+===================================+ | PUT | /templates/{env-temp-id}/services/{service-id}| It updates the service description| +----------+-----------------------------------------------+-----------------------------------+ *Parameters:* * `env-temp-id` - The environment template ID, required * `service-id` - The service ID to be updated * payload - the service description *Content-Type* application/json *Example* :: { "instance": { "assignFloatingIp": "true", "keyname": "mykeyname", "image": "cloud-fedora-v3", "flavor": "m1.medium", "?": { "type": "io.murano.resources.LinuxMuranoInstance", "id": "ef984a74-29a4-45c0-b1dc-2ab9f075732e" } }, "name": "orion", "port": "8080", "?": { "type": "io.murano.apps.apache.Tomcat", "id": "54cea43d-5970-4c73-b9ac-fea656f3c722" } } *Response* :: { "instance": { "assignFloatingIp": "true", "keyname": "mykeyname", "image": "cloud-fedora-v3", "flavor": "m1.medium", "?": { "type": "io.murano.resources.LinuxMuranoInstance", "id": "ef984a74-29a4-45c0-b1dc-2ab9f075732e" } }, "name": "orion", "?": { "type": "io.murano.apps.apache.Tomcat", "id": "54cea43d-5970-4c73-b9ac-fea656f3c722" }, "port": "8080" } +----------------+-----------------------------------------------------------+ | Code | Description | +================+===========================================================+ | 200 | OK. Environment Template updated successfully | +----------------+-----------------------------------------------------------+ | 401 | User is not authorized to access this session | +----------------+-----------------------------------------------------------+ | 404 | The environment template does not exist | +----------------+-----------------------------------------------------------+ Create an environment from an environment template -------------------------------------------------- *Request* +----------+--------------------------------------------+--------------------------------------+ | Method | URI | Description | +==========+============================================+======================================+ | POST | /templates/{env-temp-id}/create-environment| Create an environment | +----------+--------------------------------------------+--------------------------------------+ *Parameters:* * `env-temp-id` - The environment template ID, required *Payload:* * 'environment name': The environment name to be created. *Content-Type* application/json *Example* :: { "name": "environment_name" } *Response* :: { "environment_id": "aa90fadfafca10e38e1c8c4bbf7", "name": "environment_name", "created": "2015-01-26T09:12:51", "tenant_id": "00000000000000000000000000000001", "version": 0, "session_id": "adf4dadfaa9033ca7ce245fca10e38e1c8c4bbf7", } +----------------+-----------------------------------------------------------+ | Code | Description | +================+===========================================================+ | 200 | OK. Environment created from template successfully | +----------------+-----------------------------------------------------------+ | 401 | User is not authorized to access this session | +----------------+-----------------------------------------------------------+ | 404 | The environment template does not exist | +----------------+-----------------------------------------------------------+ | 409 | The environment already exists | +----------------+-----------------------------------------------------------+ **POST /templates/{env-temp-id}/clone** *Request* +----------+--------------------------------+-------------------------------------------------+ | Method | URI | Description | +==========+================================+=================================================+ | POST | /templates/{env-temp-id}/clone | It clones a public template from one project | | | | to another | +----------+--------------------------------+-------------------------------------------------+ *Parameters:* * `env-temp-id` - environment template ID, required *Example Payload* :: { 'name': 'cloned_env_template_name' } *Content-Type* application/json *Response* :: { "updated": "2015-01-26T09:12:51", "name": "cloned_env_template_name", "created": "2015-01-26T09:12:51", "tenant_id": "00000000000000000000000000000001", "version": 0, "is_public": False, "id": "aa9033ca7ce245fca10e38e1c8c4bbf7", } +----------------+-----------------------------------------------------------+ | Code | Description | +================+===========================================================+ | 200 | OK. Environment Template cloned successfully | +----------------+-----------------------------------------------------------+ | 401 | User is not authorized to access this session | +----------------+-----------------------------------------------------------+ | 403 | User has no access to these resources | +----------------+-----------------------------------------------------------+ | 404 | The environment template does not exist | +----------------+-----------------------------------------------------------+ | 409 | Conflict. The environment template name already exists | +----------------+-----------------------------------------------------------+ ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/doc/source/reference/appendix/articles/specification/murano-repository.rst0000664000175000017500000005314500000000000031643 0ustar00zuulzuul00000000000000Application catalog API ======================= Manage application definitions in the Application Catalog. You can browse, edit and upload new application packages (.zip.package archive with all data that required for a service deployment). Packages ======== Methods for application package management **Package Properties** - ``id``: guid of a package (``fully_qualified_name`` can also be used for some API functions) - ``fully_qualified_name``: fully qualified domain name - domain name that specifies exact application location - ``name``: user-friendly name - ``type``: package type, "library" or "application" - ``description``: text information about application - ``author``: name of application author - ``tags``: list of short names, connected with the package, which allows to search applications easily - ``categories``: list of application categories - ``class_definition``: list of class names used by a package - ``is_public``: determines whether the package is shared for other projects - ``enabled``: determines whether the package is browsed in the Application Catalog - ``owner_id``: id of a project that owns the package .. note:: It is possible to use ``in`` operator for properties ``id``, ``category`` and ``tag``. For example to get packages with ``id1, id2, id3`` use ``id=in:id1,id2,id3``. List packages ------------- `/v1/catalog/packages?{marker}{limit}{order_by}{type}{category}{fqn}{owned}{id}{catalog}{class_name}{name} [GET]` This is the compound request to list and search through application catalog. If there are no search parameters all packages that is_public, enabled and belong to the user's project will be listed. Default order is by 'created' field. For an admin role all packages are available. **Parameters** +---------------------+--------+---------------------------------------------+ | Attribute | Type | Description | +=====================+========+=============================================+ | ``catalog`` | bool | If false (default) - search packages, that | | | | current user can edit (own for non-admin, | | | | all for admin) | | | | If true - search packages, that current user| | | | can deploy (i.e. his own + public) | +---------------------+--------+---------------------------------------------+ | ``marker`` | string | A package identifier marker may be | | | | specified. When present only packages which | | | | occur after the identifier ID will be listed| +---------------------+--------+---------------------------------------------+ | ``limit`` | string | When present the maximum number of results | | | | returned will not exceed the specified | | | | value. The typical pattern of limit and | | | | marker is to make an initial limited request| | | | and then to use the ID of the last package | | | | from the response as the marker parameter in| | | | a subsequent limited request. | +---------------------+--------+---------------------------------------------+ | ``order_by`` | string | Allows to sort packages by: `fqn`, `name`, | | | | `created`. Created is default value. | +---------------------+--------+---------------------------------------------+ | ``type`` | string | Allows to point a type of package: | | | | `application`, `library` | +---------------------+--------+---------------------------------------------+ | ``category`` | string | Allows to point a categories for a search | +---------------------+--------+---------------------------------------------+ | ``fqn`` | string | Allows to point a fully qualified package | | | | name for a search | +---------------------+--------+---------------------------------------------+ | ``owned`` | bool | Search only from packages owned by current | | | | project | +---------------------+--------+---------------------------------------------+ | ``id`` | string | Allows to point an id for a search | +---------------------+--------+---------------------------------------------+ | ``include_disabled``| bool | Include disabled packages in a the result | +---------------------+--------+---------------------------------------------+ | ``search`` | string | Gives opportunity to search specified data | | | | by all the package parameters and order | | | | packages | +---------------------+--------+---------------------------------------------+ | ``class_name`` | string | Search only for packages, that use specified| | | | class | +---------------------+--------+---------------------------------------------+ | ``name`` | string | Allows to point a package name for a search | +---------------------+--------+---------------------------------------------+ **Response 200 (application/json)** :: {"packages": [ { "id": "fed57567c9fa42c192dcbe0566f8ea33", "fully_qualified_name" : "com.example.murano.services.linux.telnet", "is_public": false, "name": "Telnet", "type": "linux", "description": "Installs Telnet service", "author": "OpenStack, Inc.", "created": "2014-04-02T14:31:55", "enabled": true, "tags": ["linux", "telnet"], "categories": ["Utility"], "owner_id": "fed57567c9fa42c192dcbe0566f8ea40" }, { "id": "fed57567c9fa42c192dcbe0566f8ea31", "fully_qualified_name": "com.example.murano.services.windows.WebServer", "is_public": true, "name": "Internet Information Services", "type": "windows", "description": "The Internet Information Service sets up an IIS server and joins it into an existing domain", "author": "OpenStack, Inc.", "created": "2014-04-02T14:31:55", "enabled": true, "tags": ["windows", "web"], "categories": ["Web"], "owner_id": "fed57567c9fa42c192dcbe0566f8ea40" }] } Upload a new package[POST] -------------------------- `/v1/catalog/packages` See the example of multipart/form-data request, It should contain two parts - text (JSON string) and file object **Request (multipart/form-data)** .. code-block:: none Content-type: multipart/form-data, boundary=AaB03x Content-Length: $requestlen --AaB03x content-disposition: form-data; name="submit-name" --AaB03x Content-Disposition: form-data; name="JsonString" Content-Type: application/json {"categories":["web"] , "tags": ["windows"], "is_public": false, "enabled": false} `categories` - array, required `tags` - array, optional `name` - string, optional `description` - string, optional `is_public` - bool, optional `enabled` - bool, optional --AaB03x content-disposition: file; name="file"; filename="test.tar" Content-Type: targz Content-Transfer-Encoding: binary $binarydata --AaB03x-- **Response 200 (application/json)** .. code-block:: json { "updated": "2014-04-03T13:00:13", "description": "A domain service hosted in Windows environment by using Active Directory Role", "tags": ["windows"], "is_public": true, "id": "8f4f09bd6bcb47fb968afd29aacc0dc9", "categories": ["test1"], "name": "Active Directory", "author": "Mirantis, Inc", "created": "2014-04-03T13:00:13", "enabled": true, "class_definition": [ "com.mirantis.murano.windows.activeDirectory.ActiveDirectory", "com.mirantis.murano.windows.activeDirectory.SecondaryController", "com.mirantis.murano.windows.activeDirectory.Controller", "com.mirantis.murano.windows.activeDirectory.PrimaryController" ], "fully_qualified_name": "com.mirantis.murano.windows.activeDirectory.ActiveDirectory", "type": "Application", "owner_id": "fed57567c9fa42c192dcbe0566f8ea40" } Get package details ------------------- `/v1/catalog/packages/{id} [GET]` Display details for a package. **Parameters** ``id`` (required) Hexadecimal `id` (or fully qualified name) of the package **Response 200 (application/json)** :: { "updated": "2014-04-03T13:00:13", "description": "A domain service hosted in Windows environment by using Active Directory Role", "tags": ["windows"], "is_public": true, "id": "8f4f09bd6bcb47fb968afd29aacc0dc9", "categories": ["test1"], "name": "Active Directory", "author": "Mirantis, Inc", "created": "2014-04-03T13:00:13", "enabled": true, "class_definition": [ "com.mirantis.murano.windows.activeDirectory.ActiveDirectory", "com.mirantis.murano.windows.activeDirectory.SecondaryController", "com.mirantis.murano.windows.activeDirectory.Controller", "com.mirantis.murano.windows.activeDirectory.PrimaryController" ], "fully_qualified_name": "com.mirantis.murano.windows.activeDirectory.ActiveDirectory", "type": "Application", "owner_id": "fed57567c9fa42c192dcbe0566f8ea40" } **Response 403** * In attempt to get a non-public package by a user whose project is not an owner of this package. **Response 404** * In case the specified package id doesn't exist. Update a package ================ `/v1/catalog/packages/{id} [PATCH]` Allows to edit mutable fields (categories, tags, name, description, is_public, enabled). See the full specification `here `_. **Parameters** ``id`` (required) Hexadecimal `id` (or fully qualified name) of the package **Content type** application/murano-packages-json-patch Allowed operations: :: [ { "op": "add", "path": "/tags", "value": [ "foo", "bar" ] }, { "op": "add", "path": "/categories", "value": [ "foo", "bar" ] }, { "op": "remove", "path": "/tags", ["foo"] }, { "op": "remove", "path": "/categories", ["foo"] }, { "op": "replace", "path": "/tags", "value": [] }, { "op": "replace", "path": "/categories", "value": ["bar"] }, { "op": "replace", "path": "/is_public", "value": true }, { "op": "replace", "path": "/enabled", "value": true }, { "op": "replace", "path": "/description", "value":"New description" }, { "op": "replace", "path": "/name", "value": "New name" } ] **Request 200 (application/murano-packages-json-patch)** :: [ { "op": "add", "path": "/tags", "value": [ "windows", "directory"] }, { "op": "add", "path": "/categories", "value": [ "Directory" ] } ] **Response 200 (application/json)** :: { "updated": "2014-04-03T13:00:13", "description": "A domain service hosted in Windows environment by using Active Directory Role", "tags": ["windows", "directory"], "is_public": true, "id": "8f4f09bd6bcb47fb968afd29aacc0dc9", "categories": ["test1"], "name": "Active Directory", "author": "Mirantis, Inc", "created": "2014-04-03T13:00:13", "enabled": true, "class_definition": [ "com.mirantis.murano.windows.activeDirectory.ActiveDirectory", "com.mirantis.murano.windows.activeDirectory.SecondaryController", "com.mirantis.murano.windows.activeDirectory.Controller", "com.mirantis.murano.windows.activeDirectory.PrimaryController" ], "fully_qualified_name": "com.mirantis.murano.windows.activeDirectory.ActiveDirectory", "type": "Application", "owner_id": "fed57567c9fa42c192dcbe0566f8ea40" } **Response 403** * An attempt to update immutable fields * An attempt to perform operation that is not allowed on the specified path * An attempt to update non-public package by user whose project is not an owner of this package **Response 404** * An attempt to update package that doesn't exist Delete application definition from the catalog ---------------------------------------------- `/v1/catalog/packages/{id} [DELETE]` **Parameters** * ``id`` (required) Hexadecimal `id` (or fully qualified name) of the package to delete **Response 404** * An attempt to delete package that doesn't exist Get application package ----------------------- `/v1/catalog/packages/{id}/download [GET]` Get application definition package **Parameters** * ``id`` (required) Hexadecimal `id` (or fully qualified name) of the package **Response 200 (application/octet-stream)** The sequence of bytes representing package content **Response 404** Specified package id doesn't exist Get UI definition ----------------- `/v1/catalog/packages/{id}/ui [GET]` Retrieve UI definition for an application which described in a package with provided id **Parameters** * ``id`` (required) Hexadecimal `id` (or fully qualified name) of the package **Response 200 (application/octet-stream)** The sequence of bytes representing UI definition **Response 404** Specified package id doesn't exist **Response 403** Specified package is not public and not owned by user project, performing the request **Response 404** * Specified package id doesn't exist Get logo -------- Retrieve application logo which described in a package with provided id `/v1/catalog/packages/{id}/logo [GET]` **Parameters** ``id`` (required) Hexadecimal `id` (or fully qualified name) of the package **Response 200 (application/octet-stream)** The sequence of bytes representing application logo **Response 403** Specified package is not public and not owned by user project, performing the request **Response 404** Specified package is not public and not owned by user project, performing the request Categories ========== Provides category management. Categories are used in the Application Catalog to group application for easy browsing and search. List categories --------------- * `/v1/catalog/packages/categories [GET]` !DEPRECATED (Plan to remove in L release) Retrieve list of all available application categories **Response 200 (application/json)** A list, containing category names *Content-Type* application/json :: { "categories": ["Web service", "Directory", "Database", "Storage"] } * `/v1/catalog/categories [GET]` +----------+------------------------------+---------------------------------+ | Method | URI | Description | +==========+==============================+=================================+ | GET | /catalog/categories | Get list of existing categories | +----------+------------------------------+---------------------------------+ Retrieve list of all available application categories **Response 200 (application/json)** A list, containing detailed information about each category *Content-Type* application/json :: {"categories": [ { "id": "0420045dce7445fabae7e5e61fff9e2f", "updated": "2014-12-26T13:57:04", "name": "Web", "created": "2014-12-26T13:57:04", "package_count": 1 }, { "id": "3dd486b1e26f40ac8f35416b63f52042", "updated": "2014-12-26T13:57:04", "name": "Databases", "created": "2014-12-26T13:57:04", "package_count": 0 }] } Get category details -------------------- `/catalog/categories/ [GET]` Return detailed information for a provided category *Request* +----------+-----------------------------------+-----------------------------+ | Method | URI | Description | +==========+===================================+=============================+ | GET | /catalog/categories/ | Get category detail | +----------+-----------------------------------+-----------------------------+ *Parameters* * ``category_id`` - required, category ID, required *Response* *Content-Type* application/json :: { "id": "b308f7fa8a2f4a5eb419970c827f4466", "updated": "2015-01-28T17:00:19", "packages": [ { "fully_qualified_name": "io.murano.apps.ZabbixServer", "id": "4dfb566e69e6445fbd4aea5099fe95e9", "name": "Zabbix Server" } ], "name": "Web", "created": "2015-01-28T17:00:19", "package_count": 1 } +----------------+-----------------------------------------------------------+ | Code | Description | +================+===========================================================+ | 200 | OK. Category deleted successfully | +----------------+-----------------------------------------------------------+ | 401 | User is not authorized to access this session | +----------------+-----------------------------------------------------------+ | 404 | Not found. Specified category doesn`t exist | +----------------+-----------------------------------------------------------+ Add new category ---------------- `/catalog/categories [POST]` Add new category to the Application Catalog *Parameters* +----------------------+------------+----------------------------------------+ | Attribute | Type | Description | +======================+============+========================================+ | name | string | Environment name; only alphanumeric | | | | characters and '-' | +----------------------+------------+----------------------------------------+ *Request* +----------+----------------------------------+------------------------------+ | Method | URI | Description | +==========+==================================+==============================+ | POST | /catalog/categories | Create new category | +----------+----------------------------------+------------------------------+ *Content-Type* application/json *Example* {"name": "category_name"} *Response* :: { "id": "ce373a477f211e187a55404a662f968", "name": "category_name", "created": "2013-11-30T03:23:42Z", "updated": "2013-11-30T03:23:44Z", "package_count": 0 } +----------------+-----------------------------------------------------------+ | Code | Description | +================+===========================================================+ | 200 | OK. Category created successfully | +----------------+-----------------------------------------------------------+ | 401 | User is not authorized to access this session | +----------------+-----------------------------------------------------------+ | 409 | Conflict. Category with specified name already exist | +----------------+-----------------------------------------------------------+ Delete category --------------- `/catalog/categories [DELETE]` *Request* +----------+-----------------------------------+-----------------------------+ | Method | URI | Description | +==========+===================================+=============================+ | DELETE | /catalog/categories/ | Delete category with | | | | specified ID | +----------+-----------------------------------+-----------------------------+ *Parameters:* * ``category_id`` - required, category ID, required *Response* +----------------+-----------------------------------------------------------+ | Code | Description | +================+===========================================================+ | 200 | OK. Category deleted successfully | +----------------+-----------------------------------------------------------+ | 401 | User is not authorized to access this session | +----------------+-----------------------------------------------------------+ | 404 | Not found. Specified category doesn`t exist | +----------------+-----------------------------------------------------------+ | 403 | Forbidden. Category with specified name is assigned to | | | the package, presented in the catalog | +----------------+-----------------------------------------------------------+ ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/doc/source/reference/appendix/articles/specification/overview.rst0000664000175000017500000000341500000000000027766 0ustar00zuulzuul00000000000000General information =================== * **Introduction** The murano service API is a programmatic interface used for interaction with murano. Other interaction mechanisms like the murano dashboard or the murano CLI should use the API as an underlying protocol for interaction. * **Allowed HTTPs requests** * *POST* : To create a resource * *GET* : Get a resource or list of resources * *DELETE* : To delete resource * *PATCH* : To update a resource * **Description Of Usual Server Responses** * 200 ``OK`` - the request was successful. * 201 ``Created`` - the request was successful and a resource was created. * 204 ``No Content`` - the request was successful but there is no representation to return (i.e. the response is empty). * 400 ``Bad Request`` - the request could not be understood or required parameters were missing. * 401 ``Unauthorized`` - authentication failed or user didn't have permissions for requested operation. * 403 ``Forbidden`` - access denied. * 404 ``Not Found`` - resource was not found * 405 ``Method Not Allowed`` - requested method is not supported for resource. * 406 ``Not Acceptable`` - the requested resource is only capable of generating content not acceptable according to the Accept headers sent in the request. * 409 ``Conflict`` - requested method resulted in a conflict with the current state of the resource. * **Response of POSTs and PUTs** All POST and PUT requests by convention should return the created object (in the case of POST, with a generated ID) as if it was requested by GET. * **Authentication** All requests include a keystone authentication token header (X-Auth-Token). Clients must authenticate with keystone before interacting with the murano service.././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/doc/source/reference/appendix/articles/telnet_example.rst0000664000175000017500000000274700000000000026315 0ustar00zuulzuul00000000000000:orphan: .. _telnet_example: Telnet Example -------------- .. code-block:: yaml Namespaces: =: io.murano.apps.linux std: io.murano res: io.murano.resources Name: Telnet # Inheritance from io.murano.Application class # (located at Murano Core library) indicates, # that this is a complete application # and that 'deploy' method has to be defined. Extends: std:Application Properties: name: Contract: $.string().notNull() instance: Contract: $.class(res:Instance).notNull() Methods: deploy: Body: # Determine the environment to which the application belongs. # This message will be stored in deployment logs and available in UI - $this.find(std:Environment).reporter.report($this, 'Creating VM for Telnet Instance.') # Deploy VM - $.instance.deploy() - $this.find(std:Environment).reporter.report($this, 'Instance is created. Setup Telnet service.') # Create instance of murano resource class. Agent will use it to find # corresponding execution plan by the file name - $resources: new('io.murano.system.Resources') # Deploy Telnet - $template: $resources.yaml('DeployTelnet.template') # Send prepared execution plan to Murano agent - $.instance.agent.call($template, $resources) - $this.find(std:Environment).reporter.report($this, 'Telnet service setup is done.') ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/doc/source/reference/appendix/articles/test_docs.rst0000664000175000017500000001750200000000000025271 0ustar00zuulzuul00000000000000.. _test_docs: ================================== Murano automated tests description ================================== This page describes automated tests for a Murano project: * where tests are located * how they are run * how to execute tests on a local machine * how to find the root of problems with FAILed tests Murano continuous integration service ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Murano project has separate CI server, which runs tests for all commits and verifies that new code does not break anything. Murano CI uses OpenStack QA cloud for testing infrastructure. Murano CI url: https://murano-ci.mirantis.com/jenkins/ Anyone can login to that server, using launchpad credentials. There you can find each job for each repository: one for **murano** and another one for **murano-dashboard**. * ``gate-murano-dashboard-ubuntu\*`` verifies each commit to the murano-dashboard repository * ``gate-murano-ubuntu\*`` verifies each commit to the murano repository Other jobs allow one to build and test Murano documentation and to perform other useful work to support the Murano CI infrastructure. All jobs are run following a fresh installation of the operating system and all components are installed on each run. UI tests ~~~~~~~~ The Murano project has a web user interface and all possible user scenarios should be tested. All UI tests are located at ``https://opendev.org/openstack/murano-dashboard/src/branch/master/muranodashboard/tests/functional``. Automated tests for the Murano web UI are written in Python using the special Selenium library. This library is used to automate web browser interactions with Python. See official `Selenium documentation `_ for details. Prerequisites: -------------- * Install the Python module called nose using either the :command:`easy_install nose` or :command:`pip install nose` command. This will install the nose libraries, as well as the ``nosetests`` script, which you can use to automatically discover and run tests. * Install external Python libraries, which are required for the Murano web UI tests: ``testtools`` and ``selenium``. * Verify that you have one of the following web browsers installed: * Mozilla Firefox 46.0 .. note:: If you do not have Firefox package out of the box, install and remove it. Otherwise, you will need to install dependent libraries manually. To downgrade Firefox: .. code-block:: console apt-get remove firefox wget https://ftp.mozilla.org/pub/firefox/releases/46.0/linux-x86_64/en-US/firefox-46.0.tar.bz2 tar -xjf firefox-46.0.tar.bz2 rm -rf /opt/firefox mv firefox /opt/firefox46 ln -s /opt/firefox46/firefox /usr/bin/firefox * Google Chrome * To run the tests on a remote server, configure the remote X server. Use VNC Software to see the test results in real-time. #. Specify the display environment variable: .. code-block:: console $DISPLAY=: #. Configure remote X server and VNC Software by typing: .. code-block:: console apt-get install xvfb xfonts-100dpi xfonts-75dpi xfonts-cyrillic xorg dbus-x11 "Xvfb -fp "/usr/share/fonts/X11/misc/" :$DISPLAY -screen 0 "1280x1024x16" &" apt-get install --yes x11vnc x11vnc -bg -forever -nopw -display :$DISPLAY -ncache 10 sudo iptables -I INPUT 1 -p tcp --dport 5900 -j ACCEPT Download and run tests ---------------------- To download and run the tests: #. Verify that all additional components has been installed. #. Clone the ``murano-dashboard`` git repository: .. code-block:: console git clone https://opendev.org/openstack/murano-dashboard #. Change the default settings: #. Specify the Murano Repository URL variable for Horizon local settings in ``murano_dashboard/muranodashboard/local/local_settings.d/_50_murano.py``: .. code-block:: console MURANO_REPO_URL = 'http://localhost:8099' #. Copy ``muranodashboard/tests/functional/config/config.conf.sample`` to ``config.conf``. #. Set appropriate URLs and credentials for your OpenStack lab. Only Administrator user credentials are appropriate. .. code-block:: console [murano] horizon_url = http://localhost/dashboard murano_url = http://localhost:8082 user = *** password = *** tenant = *** keystone_url = http://localhost:5000/v3 All tests are kept in ``sanity_check.py`` and divided into 10 test suites: * TestSuiteSmoke - verification of Murano panels; checks that they can be open without errors. * TestSuiteEnvironment - verification of all operations with environment are finished successfully. * TestSuiteImage - verification of operations with images. * TestSuiteFields - verification of custom fields validators. * TestSuitePackages - verification of operations with Murano packages. * TestSuiteApplications - verification of Application Catalog page and of application creation process. * TestSuiteAppsPagination - verification of apps pagination in case of many applications installed. * TestSuiteRepository - verification of importing packages and bundles. * TestSuitePackageCategory - verification of main operations with categories. * TestSuiteCategoriesPagination - verification of categories pagination in case of many categories created. * TestSuiteMultipleEnvironments - verification of ability to apply action to multiple environments. To run the tests follow these instructions: * To run all tests: .. code-block:: console nosetests sanity_check.py * To run a single suite: .. code-block:: console nosetests sanity_check.py: * To run a single test: .. code-block:: console nosetests sanity_check.py:. In case of successful execution, you should see something like this: .. code-block:: console ......................... Ran 34 tests in 1.440s OK In case of failure, the folder with screenshots of the last operation of tests that finished with errors would be created. It is located in ``muranodashboard/tests/functional`` folder. There are also a number of command line options that can be used to control the test execution and generated outputs. For more details about ``nosetests``, type: .. code-block:: console nosetests -h Tempest tests ~~~~~~~~~~~~~ All Murano services have tempest-based automated tests, which verify API interfaces and deployment scenarios. Tempest tests for Murano are located at ``https://opendev.org/openstack/murano/src/branch/master/murano/tests/functional``. The following Python files contain basic test suites for different Murano components. API tests --------- Murano API tests are run on the devstack gate located at ``https://opendev.org/openstack/murano-tempest-plugin/src/branch/master/murano_tempest_tests/tests/api``. * ``test_murano_envs.py`` contains test suite with actions on murano environments (create, delete, get, and others). * ``test_murano_sessions.py`` contains test suite with actions on murano sessions (create, delete, get, and others). * ``test_murano_services.py`` contains test suite with actions on murano services (create, delete, get, and others). * ``test_murano_repository.py`` contains test suite with actions on murano package repository. Engine tests ------------ Murano Engine Tests are run on murano-ci at ``https://opendev.org/openstack/murano-tempest-plugin/src/branch/master/murano_tempest_tests/tests/functional``: * ``base.py`` contains base test class and tests with actions on deploy Murano services such as Telnet and Apache. Command-line interface tests ---------------------------- Murano CLI tests are currently in the middle of creation. The current scope is read-only operations on a cloud that are hard to test through unit tests. All tests have description and execution steps in their docstrings. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/doc/source/reference/appendix/articles/workflow.rst0000664000175000017500000001103700000000000025151 0ustar00zuulzuul00000000000000.. _murano-workflow: =============== Murano workflow =============== What happens when a component is being created in an environment? This document will use the Telnet package referenced elsewhere as an example. It assumes the package has been previously uploaded to Murano. Step 1. Begin deployment ========================= The API sends a message that instructs murano-engine, the workflow component of Murano, to deploy an environment. The message consists of a JSON document containing the class types required to create the environment, as well as any parameters the user selected prior to deployment. Examples are: * An :ref:`Environment` object (io.murano.Environment) with a *name* * An object (or objects) referring to networks that need to be created or that already exist * A list of Applications (e.g. io.murano.apps.linux.Telnet). Each Application will contain, or will reference, anything it requires. The Telnet example, has a property called *instance* whose contract states it must be of type io.murano.resources.Instance. In turn the Instance has properties it requires (like a name, a flavor, a keypair name). Each object in this *model* has an ID so that the state of each can be tracked. The classes that are required are determined by the application's manifest. In the :ref:`Telnet example ` only one class is explicitly required; the telnet application definition. The :ref:`Telnet class definition ` refers to several other classes. It extends :ref:`Application` and it requires an :ref:`Instance`. It also refers to the :ref:`Environment` in which it will be contained, sends reports through the environment's :ref:`status-reporter` and adds security group rules to the :ref:`security-group-manager`. Step 2. Load definitions ========================= The engine makes a series of requests to the API to download packages it needs. These requests pass the class names the environment will require, and during this stage the engine will validate that all the required classes exist and are accessible, and will begin creating them. All Classes whose *workflow* sections contain an *initialize* fragment are then initialized. A typical initialization order would be (defined by the ordering in the *model* sent to the murano-engine): * :ref:`Network` * :ref:`Instance` * :ref:`Object` * :ref:`Environment` Step 3. Deploy resources ========================== The workflow defined in Environment.deploy is now executed. The first step typically is to initialize the messaging component that will pay attention to murano-agent (see later). The next stage is to deploy each application the environment knows about in turn, by running deploy() for each application. This happens concurrently for all the applications belonging to an instance. In the :ref:`Telnet example ` (under *Workflow*), the workflow dictates sending a status message (via the environment's *reporter*, and configuring some security group rules. It is at this stage that the engine first contacts Heat to request information about any pre-existing resources (and there will be none for a fresh deploy) before updating the new Heat template with the security group information. Next it instructs the engine to deploy the *instance* it relies on. A large part of the interaction with Heat is carried out at this stage; the first thing an Instance does is add itself to the environment's network. Since the network doesn't yet exist, murano-engine runs the neutron network workflow which pushes template fragments to Heat. These fragments can define: * Networks * Subnets * Router interfaces Once this is done the Instance itself constructs a Heat template fragment and again pushes it to Heat. The Instance will include a *userdata* script that is run when the instance has started up, and which will configure and run murano-agent. Step 4. Software configuration via murano-agent ================================================ If the workflow includes murano-agent components (and the telnet example does), typically the application workflow will execute them as the next step. In the telnet example, the workflow instructs the engine to load *DeployTelnet.yaml* as YAML, and pass it to the murano-agent running on the configured instance. This causes the agent to execute the *EntryPoint* defined in the agent script (which in this case deploys some packages and sets some iptables rules). Step 5. Done ============= After execution is finished, the engine sends a last message indicating that fact; the API receives it and marks the environment as deployed. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/doc/source/reference/appendix/cli_ref.rst0000664000175000017500000003245400000000000023102 0ustar00zuulzuul00000000000000.. _cli-ref: ========================== Murano command-line client ========================== The ``murano`` client is the command-line interface (CLI) for the Application catalog API and its extensions. For help on a specific ``murano`` command, enter: .. code-block:: console murano help COMMAND murano usage usage: murano \[--version] \[-d] \[-v] \[-k] \[--os-cacert ] \[--cert-file CERT_FILE] \[--key-file KEY_FILE] \[--ca-file CA_FILE] \[--api-timeout API_TIMEOUT] \[--os-username OS_USERNAME] \[--os-password OS_PASSWORD] \[--os-tenant-id OS_TENANT_ID] \[--os-tenant-name OS_TENANT_NAME] \[--os-auth-url OS_AUTH_URL] \[--os-region-name OS_REGION_NAME] \[--os-auth-token OS_AUTH_TOKEN] \[--os-no-client-auth] \[--murano-url MURANO_URL] \[--glance-url GLANCE_URL] \[--murano-api-version MURANO_API_VERSION] \[--os-service-type OS_SERVICE_TYPE] \[--os-endpoint-type OS_ENDPOINT_TYPE] \[--include-password] \[--murano-repo-url MURANO_REPO_URL] ... Subcommands =========== * *bundle-import* Import a bundle. * *category-create* Create a category. * *category-delete* Delete a category. * *category-list* List all available categories. * *category-show* * *deployment-list* List deployments for an environment. * *env-template-add-app* Add application to the environment template. * *env-template-create* Create an environment template. * *env-template-del-app* Delete application to the environment template. * *env-template-delete* Delete an environment template. * *env-template-list* List the environments templates. * *env-template-show* Display environment template details. * *env-template-update* Update an environment template. * *environment-create* Create an environment. * *environment-delete* Delete an environment. * *environment-list* List the environments. * *environment-rename* Rename an environment. * *environment-show* Display environment details. * *package-create* Create an application package. * *package-delete* Delete a package. * *package-download* Download a package to a filename or stdout. * *package-import* Import a package. * *package-list* List available packages. * *package-show* Display details for a package. * *service-show* * *bash-completion* Prints all of the commands and options to stdout. * *help* Display help about this program or one of its subcommands. Murano optional arguments ========================= **--version** show program's version number and exit **-d, --debug** Defaults to env[MURANOCLIENT_DEBUG] **-v, --verbose** Print more verbose output **-k, --insecure** Explicitly allow muranoclient to perform "insecure" SSL (https) requests. The server's certificate will not be verified against any certificate authorities. This option should be used with caution. **--os-cacert ** Specify a CA bundle file to use in verifying a TLS (https) server certificate. Defaults to env[OS_CACERT] **--cert-file CERT_FILE** Path of certificate file to use in SSL connection. This file can optionally be prepended with the private key. **--key-file KEY_FILE** Path of client key to use in SSL connection. This option is not necessary if your key is prepended to your cert file. **--ca-file CA_FILE** Path of CA SSL certificate(s) used to verify the remote server certificate. Without this option glance looks for the default system CA certificates. **--api-timeout API_TIMEOUT** Number of seconds to wait for an API response, defaults to system socket timeout **--os-username OS_USERNAME** Defaults to env[OS_USERNAME] **--os-password OS_PASSWORD** Defaults to env[OS_PASSWORD] **--os-project-id OS_PROJECT_ID** Defaults to env[OS_PROJECT_ID] **--os-project-name OS_PROJECT_NAME** Defaults to env[OS_PROJECT_NAME] **--os-auth-url OS_AUTH_URL** Defaults to env[OS_AUTH_URL] **--os-region-name OS_REGION_NAME** Defaults to env[OS_REGION_NAME] **--os-auth-token OS_AUTH_TOKEN** Defaults to env[OS_AUTH_TOKEN] **--os-no-client-auth** Do not contact keystone for a token. Defaults to env[OS_NO_CLIENT_AUTH]. **--murano-url MURANO_URL** Defaults to env[MURANO_URL]** **--glance-url GLANCE_URL** Defaults to env[GLANCE_URL] **--murano-api-version MURANO_API_VERSION** Defaults to env[MURANO_API_VERSION] or 1 **--os-service-type OS_SERVICE_TYPE** Defaults to env[OS_SERVICE_TYPE] **--os-endpoint-type OS_ENDPOINT_TYPE** Defaults to env[OS_ENDPOINT_TYPE] **--include-password** Send os-username and os-password to murano. **--murano-repo-url MURANO_REPO_URL** Defaults to env[MURANO_REPO_URL] or `http://storage.apps.openstack.org_` Application catalog API v1 commands =================================== murano bundle-import ~~~~~~~~~~~~~~~~~~~~ .. code-block::console usage: murano bundle-import \[--is-public] \[--exists-action {a,s,u}] \[ ...] Import a bundle. ``FILE`` can be either a path to a zip file, URL or name from repo. if ``FILE`` is a local file does not attempt to parse requirements and treat Names of packages in a bundle as file names, relative to location of bundle file. Positional arguments -------------------- **** Bundle URL, bundle name, or path to the bundle file Optional arguments ------------------ **--is-public** Make packages available to users from other project **--exists-action {a,s,u}** Default action when a package already exists murano category-create ~~~~~~~~~~~~~~~~~~~~~~ .. code-block::console usage: murano category-create Create a category. Positional arguments -------------------- **** Category name murano category-delete ~~~~~~~~~~~~~~~~~~~~~~ .. code-block::console usage: murano category-delete \[ ...] Delete a category. Positional arguments -------------------- **** ID of a category(s) to delete murano category-list ~~~~~~~~~~~~~~~~~~~~ .. code-block::console usage: murano category-list List all available categories. murano category-show ~~~~~~~~~~~~~~~~~~~~ .. code-block::console usage: murano category-show Positional arguments -------------------- **** ID of a category(s) to show murano deployment-list ~~~~~~~~~~~~~~~~~~~~~~ .. code-block::console usage: murano deployment-list List deployments for an environment. Positional arguments -------------------- **** Environment ID for which to list deployments murano env-template-add-app ~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. code-block::console usage: murano env-template-add-app Add application to the environment template. Positional arguments -------------------- **** Environment template name **** Path to the template. murano env-template-create ~~~~~~~~~~~~~~~~~~~~~~~~~~ .. code-block::console usage: murano env-template-create Create an environment template. Positional arguments -------------------- **** Environment template name murano env-template-del-app ~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. code-block::console usage: murano env-template-del-app Delete application to the environment template. Positional arguments -------------------- **** Environment template ID **** Application ID murano env-template-delete ~~~~~~~~~~~~~~~~~~~~~~~~~~ .. code-block::console usage: murano env-template-delete \[ ...] Delete an environment template. Positional arguments -------------------- **** ID of environment(s) template to delete murano env-template-list ~~~~~~~~~~~~~~~~~~~~~~~~ .. code-block::console usage: murano env-template-list List the environments templates. murano env-template-show ~~~~~~~~~~~~~~~~~~~~~~~~ .. code-block::console usage: murano env-template-show Display environment template details. Positional arguments -------------------- **** Environment template ID murano env-template-update ~~~~~~~~~~~~~~~~~~~~~~~~~~ .. code-block::console usage: murano env-template-update Update an environment template. Positional arguments -------------------- **** Environment template ID **** Environment template name murano environment-create ~~~~~~~~~~~~~~~~~~~~~~~~~ .. code-block::console usage: murano environment-create Create an environment. Positional arguments -------------------- **** Environment name murano environment-delete ~~~~~~~~~~~~~~~~~~~~~~~~~ .. code-block::console usage: murano environment-delete \[ ...] Delete an environment. Positional arguments -------------------- **** ID or name of environment(s) to delete Optional arguments ------------------ **--abandon** If set will abandon environment without deleting any of its resources murano environment-list ~~~~~~~~~~~~~~~~~~~~~~~ .. code-block::console usage: murano environment-list List the environments. murano environment-rename ~~~~~~~~~~~~~~~~~~~~~~~~~ .. code-block::console usage: murano environment-rename Rename an environment. Positional arguments -------------------- **** Environment ID or name **** A name to which the environment will be renamed murano environment-show ~~~~~~~~~~~~~~~~~~~~~~~ .. code-block::console usage: murano environment-show Display environment details. Positional arguments -------------------- **** Environment ID or name murano package-create ~~~~~~~~~~~~~~~~~~~~~ .. code-block::console usage: murano package-create \[-t ] \[-c ] \[-r ] \[-n ] \[-f ] \[-a ] \[--tags \[ \[ ...]]] \[-d ] \[-o ] \[-u ] \[--type TYPE] \[-l ] Create an application package. Optional arguments ------------------ **-t , --template ** Path to the Heat template to import as an Application Definition **-c , --classes-dir ** Path to the directory containing application classes **-r , --resources-dir ** Path to the directory containing application resources **-n , --name ** Display name of the Application in Catalog **-f , --full-name ** Fully-qualified name of the Application in Catalog **-a , --author ** Name of the publisher **--tags \[ \[ ...]]** A list of keywords connected to the application **-d , --description ** Detailed description for the Application in Catalog **-o , --output ** The name of the output file archive to save locally **-u , --ui ** Dynamic UI form definition **--type TYPE** Package type. Possible values: Application or Library **-l , --logo ** Path to the package logo murano package-delete ~~~~~~~~~~~~~~~~~~~~~ .. code-block::console usage: murano package-delete \[ ...] Delete a package. Positional arguments -------------------- **** Package ID to delete murano package-download ~~~~~~~~~~~~~~~~~~~~~~~ .. code-block::console usage: murano package-download \[file] Download a package to a filename or stdout. Positional arguments -------------------- **** Package ID to download **file** Filename for download (defaults to stdout) murano package-import ~~~~~~~~~~~~~~~~~~~~~ .. code-block::console usage: murano package-import \[-c \[ \[ ...]]] \[--is-public] \[--package-version VERSION] \[--exists-action {a,s,u}] \[ ...] Import a package. ``FILE`` can be either a path to a zip file, URL or a FQPN. ``categories`` can be separated by a comma. Positional arguments -------------------- **** URL of the murano zip package, FQPN, or path to zip package Optional arguments ------------------ **-c \[ \[ ...]], --categories \[ \[ ...]]** Category list to attach **--is-public** Make the package available for user from other project **--package-version VERSION** Version of the package to use from repository (ignored when importing with multiple packages) **--exists-action {a,s,u}** Default action when package already exists murano package-list ~~~~~~~~~~~~~~~~~~~ .. code-block::console usage: murano package-list \[--include-disabled] List available packages. Optional arguments ------------------ **--include-disabled** murano package-show ~~~~~~~~~~~~~~~~~~~ .. code-block::console usage: murano package-show Display details for a package. Positional arguments -------------------- **** Package ID to show murano service-show ~~~~~~~~~~~~~~~~~~~ .. code-block::console usage: murano service-show \[-p ] Positional arguments -------------------- **** Environment ID to show applications from Optional arguments ------------------ **-p , --path ** Level of detalization to show. Leave empty to browse all services in the environment ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/doc/source/reference/appendix/glossary.rst0000664000175000017500000000005200000000000023327 0ustar00zuulzuul00000000000000.. _glossary: ======== Glossary ======== ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/doc/source/reference/appendix/murano_concepts.rst0000664000175000017500000000022400000000000024664 0ustar00zuulzuul00000000000000.. _murano-concepts: ========================================= High-level definitions of Murano concepts ========================================= ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/doc/source/reference/appendix/rest_api_spec.rst0000664000175000017500000000013100000000000024302 0ustar00zuulzuul00000000000000.. _rest-api-spec: ====================== REST API specification ====================== ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/doc/source/reference/appendix/tutorials.rst0000664000175000017500000000033600000000000023517 0ustar00zuulzuul00000000000000.. _tutorials: .. meta:: :robots: noindex ========= Tutorials ========= Integration with Docker ~~~~~~~~~~~~~~~~~~~~~~~ Integration with Kubernetes ~~~~~~~~~~~~~~~~~~~~~~~~~~~ HA and autoscaling ~~~~~~~~~~~~~~~~~~ ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/doc/source/reference/architecture.png0000664000175000017500000016614700000000000022334 0ustar00zuulzuul00000000000000PNG  IHDRIbKGD pHYs  tIME  J IDATxy\TwfX=ETdq24jVf׼?YirJKSK̛撦^i **]9?&CEY~x)gfm{{"bJDL C&J?%I |ڻps(K:ܺ]Ԭ"$^W#ԒU2ZDbdd &`L(dTі= `T!%VIXࣔ"xݠU2@t.\Nd?ӑUT9A~ [Z*5DYİ`LApB.M/eB $#u;k-=>^BZ@~X?]FA!.cX` U(˰}5[tNk+ܜ!'lXTP(ޜ')&Xf\&$HbϠy]"bJDu(KԄ2Շ$:&ztlc/hnlr Oz9ՈB~>#BD,Ve?{0Q]`%ķ~y4i 0%]>G =Ps"bJDu(Qzc/ ^rYf5Yjm= <[;wwでzšF X%(wh&T̚`E&6UEy3K.=Iz "D1 wNݫ<<L ʂ?oo*瓧ȑ+8w6M)١S'/HMGVY{y2 ped߂wDH?bݪ+ܔRc߾KH&6,wzvwU>Q]r9f@RbvooGDGbVhxرbfmXpϳ1hܙ'չfV\WϢm.g`޼HM_¿>xxߤLL~u#t:=8VA.a|!1u)6 c„ dg ca봠@~8ooGё Eٿ2$ @0p, @P`JDTGll*oc"7wCXXlqU,>؁Ep7Fꌯ>[hro%SOvǑW;͛;ڵ\Ʀ!~:ص;20bD'x{73VnX`1Yj|p@2ƍ:6mCAA1Ǝ늈a`}.a޼=X}IysgL}/z 7M۰n <5wFT˰6~/_cSLh`plX$l9W^ hQ>YO]}ѳ8yqSCo,V+~D|V(P !@_aq*XA;r |{Zbpۖ|wYYjLyqV?;Z=> ["8?v%`sx&ڻ7 xV˗ 6+zn EaL[HRSo!BlF5._>X\Ǭ©xuroW/c[?v͸?F:{{k_In]}KAjj~ݠ)=f,TavRol? {{kxּճohk^}x{5y,+(X`؂JT޲ik\vZIDԠ⍩QƷߎ4KnH<-+:Ǝ3nd%ٸv-}4NbccM5R{7bcS[`oqr 'ڕPuR?1m{px 1``kByv57oן7eS@G-apR +U"j:Gaa "waAfWNIlqcvży{e9cw_lll:o@AA1bcS1YibI8u:l=M0odeMf+ Nz(q< 1QCV>ojZͺ L2>ܴӁ 0h0*}I0n`_Q՗kל–-gMn- UM +KcSpz.uٳ":(j?g4u-Gw5g 'g{4oB9rFjuWP-RL=na"?10LNafb4O,Z4ܐul- vxW=:cǯuN;< V_b}Gf41\6|뾽IGq]>a6IRaa L6^jjQFz^fl]2M&jըКZ `ATkȇav Dd Kl4)ʓ؅ !i/V}.UOooGIɓѷY˗Mq2`c0i)z\Yۼ3BBPXX3$׮bQdH׏*w{#+KnV^sngK=е/._`ܸ՘>m+&}u]86ysgFO-ȑ+0r ~? -ȑ+0}V _b`hYv"!~3zx]wy-~"3/ҖG/ʯ1D(x{sny{E R!,E_@ θ]XcR[_\L-JGCGѪ+n+(,,Lf Ɔggyo0r %go"$8p &z/f#W`Ӧ8ݗ?4وME߾Xj,O(3Z"$M% ֬y-9M~ǑWƫ_f2W_=E`/MEV0vJLh4ZkŒшMs7썦Mmxeg(ꆭ:"۹E]ȊRA2!;օ>2!Ҹn^ӻLW紳N5`JS`li:U"(o2|d$fΌF||3?]}zI:yN^5;{anUayTxSh8{&bcSƕN}#7]5"=M®] u0. ә2.܏}{뼶o(FRCS[Ö l_wMg5(1~|7^ގ5~}%Eiu{{;VC"" Pᲂ$I+!❊U^7J$MZ!ݽ|"uAEt{QY{@SbKT~k2+DDh4Zyl1aB"""jTʻf3uO(W~HBZUz'vi0#6vbb !Ob͚i}R@B_ " !Tꂘ/b%4Iv^6UxbJD`)10|ZY*LʲbӤ+mmLl- KOb-0 U"""""3 bHC"c%H#*~'z~2{t~{, K-::7QfJDDDDDtWM By[w{ty-W< Xܔ , B' HSd*J1,VM5W!a|mjj*IbEb4QoaR:J$5DžH$)0{D,U"""""{Y, PhH]Y[NXUN/6|H?ۢ"}W!LL~t~Z#ADDDT;zJ/W0EPbͽԍMU5'HR"i$k#Lt^αϮbDώu @ɴ2*,VkM>IJzIHuo-Զlmmjnkn#=73/ȥbNWݹ twQA $-X1YLׇέBYjv C[]ܺ[ShHFޜLw"yj Փ7>K L,X{eJ˵95M A1""""zP|wfMk,#Jz $DDDDDu]%@+6&YN-կGq XBm}:0dT>9ADDDDTj9{&&YvM""""zV+,1T"bJDL,&Q(bdsʒHXeƞ(KB̼dײ 稤 "bJDLy,\N̓LZr9P "DD'b-4/+r9RvbXK^.e22[1:$vzio[ "z e˿ [[;5ל>9_zU^ӽ:酼W&~?:?-~@G~b}GwbsERu2nONIĴOGݽǫ}h;6 i&ەJ\_X[Y& X6o_s]J^iKD(- Z+۹1َC 'x7#V ٵEE{-5g4*$iR'@2[ë3YVq]_'Vދ8r&^ۮE-+2r"䳕PvH`Jx\=͜1&Ex{Bӣ@[B?Q~Dk Q&)1ރ> `IYx4%zآg &"Nj3_@iT֧XH>ԧc? Ѷ6>#)~Ћ3ϥJtq-l}O 7=ԏ&L_Tl۳ {D`:AuYn'L>'BƜ(+J5V6S^..:xhP37Xb Bf|,I&1D ~Z&k^l{Z]m|:Oz M\?w u ,Jݲ(%%XJ{%X>DY&ͳ 0]2B*QS7vӨ唘b htQyPjU`:rQ鞷x+DHL#`paB-'73|xyi} ľ;qexr K:{%%ŔgΟ@Ii \0q׸ '0qn `?0n[x? ]S|k1io83Ӱv]<4 tǞƳCƘ{@'6j0$^xunŚ"^v#V.dr9ڴlqzܤ^Æ?bǁh:!|V IDATHD<_>Sҷ5&?Wn$cq>1%%C=1q86s6a*c!YkxI2x|ى\.A=YP1o8xHנ9eB뗛p*ՋDYqпd 9m{{͝" }7sI@:k?! Vi{;o͐ ɇv}恴g!mڤ$IW,V]ƙ/cסxO}6/MZWVfN>|3A }M>3o09ܤX ˩L߸ѻ7x]SxGqMdPH""u/ѢힹTI\fcu(KyP+i :O]Y `_hR+Q2!L-yyOgӝdD/LE *``dMloŪNkoAae?D"1|b(X!fθvnW6-0ʱ3z}ŚZ[ܶu{Lu~,[f;tb?^ϻq(5Fp왲%?$e&ʵL![d͆wH% @(U\K[,hRHG¦$ Ix@-&s6.<3#QZȐŵ"Mq\VLQ!QrdfIWRZ"u-%$B]ƨMUEx$^7-w_uq~ zt]kŪ5׏mgx{!Y<{ :s %uG}^H!6f5D2(ib{8X$AZ UFr+71wIdYJa)2I;UFҖ8IW.Xv1Q{ v7k=-Z1 _@SP~d\AN4goꛔ8z:%Xq'PPt zxV ןBȰ% iHҶÌE$ʜV?U؍VHmF@&{R7qV` ;h w)$?W5[xL1D8|+7q ʍdv nO5Sq Gֿɲ{ЫK_mM4/vu;c ݟOס頴WnXi?q3x"JK4h!akcg~o"f\ҩdr]ŭz հA#T-\<J{%ZVyuԫhǺ7y?rq.)uo|On^ztwt'/&>s2s zu[eʏ++q[6aΣ%!6UجB- ̥gҶF2BPS FH7Vc)=nš ad%N}֜$ē6*ѣubxa8gԅj:3'׫D-5EP|2cw10S'4+V723LՍk+󑜒xetlėCәǨd"~t \ w@4؋O/2+n0Ś"޽-}[ L ;/<; cm(|ku:ءXSdm獿_:_ u͜}_vm__cju_ Էjӿpv^~݀f/ò~sM;~2?HՉ(.-(Y4MI{!&lݬ w ̎B`@K?L$775`!ֵej_\HcaY}i NG?I/HRY{$WHb^.oȿZ_ ?ԅj̜k+k|2}T98xb~O8v9Ϝ?n{b'kagc00E ^9];4GO%gޜ~v;`±3ԅj|8/1ŢA ޞtXm6KL5z.,:>Fzf-lyIz,];`i>_]XSOvJ|Bca}]Q?`okS0prO ?h,V[⇍bny˜UY1d,ZۆuRxf!4,"QڦǼ}E,TIzqdg 공6U\0$ő^' x2tTS[&c@oCt-5徟f ͜-ޞ8¤Wpur3v^c-:Ś"Lxn`1~*>}Bu_sCe ^ݵ/5Eng3}d|Wv>ˆ 7.|1{aG[TЩmWg[p]< wxug幁 )-D,1d.1[sTj]6Z~_#Dj.i[Nb*gn<u еF ';kXB}O `ۻZo\<5WZvSv)k+sִ-dUV~*(^k>əKJ5Gev2o  NɆRsM~Vl\_: Upk|bɏ^b^8[Lqk-5/ N-.:)z:OT\>?nH>]QK~C `\łFnې ԌpIw(+pgs)nJBo21!:gذI 7l[$IBBBh-U `bWA11@&[?}8{׍YpwY'1y-EErl jIi)%9uuv7ʍd>Wo\Fvnfa~A.S^w^/F_(T%@q.%8]gA;>律՘Ȕӽj7xojM!j4EXRVBς\lJiP )ILO\b M^{J X wvL](6L~~5Xm*8I zmy! XMZ/Yp!ϗA̱S[^t:F-́^W:aټ~1=8(X~Îh$^˽*Rǫammm~š'H}(VT1Z7.9={/ 0i,Xb Յgȱ l/@y P LwpY3+efUdJ?nnWbAH  ua<=m9[l ZئxҖu=3sl7um~ Wt^CfNY1y^}ڼ k|2t%™U:6shKLƷRNb$} nK89zeFgUiSd{/(SbR6btӈt(CDjY9{)Bl2m$*_bj|F&4xdgT~J;p-&%ohS~d{rJ".]K~r&P0-Բ_ ;{fܾMG#;Mu:{VPl[}lޞO>[bd&}h;V}PkKkWmKNO+PRyNQCWX|yd#.f5k&u84m+)z}W l;; nb( V;P(k5>{~*E;E*jsgGŅNkΐr)-^ySd  lec"Skk~h[?Bw{*9](_#gMdiդmyœ]l= r^c݄^>]q iYɭ[SbjtCgkckSD^O`QRZbYoN|/cdc_NS5*xbޒYDže'3&wDjX6[Ǟd"9{Rӯჟp>9{3B.Ar9f!>_>vM;~2䆼[9\bF̱QO؈ x?~E$z[~4PI 28_ si#0!x5akRC1`fnMNa9.c/:mÜCBDUl,,4t0rn>ӑӚlg R2-)UQXI1Y)BLZ:`o4[2&=3  hh,2s2f\3V.J%NDTG(2nB5]A`eeA!uǽktO>̜ e l{O^C• /C >'y]EDe m"LƄ1/?J<2 ݊<|Ѻd]ԒǴb3}Zuaѵ:xgk2{]]gQ<7TҴ쇈RҶnCdz!Th$6a5I!%ʆ{]5mG_Z IDAT\Uшtp+ ${}<{84 U.9) :ZZUX\^mUeW!uV,T?(e=erP}Z׫g Ӊ84zU@ZVVվꎩc3g h DTo]ڒ'I!*XF~$ʸ2#Y$H<`kDzSF)H/>8Y%IZObtKݯDT9V.֕Ym?! taU8lp* [œXL4QfN=)X=l6ə@V#h7mȅYjF~h{84\e&Պ;*J%pu4gIk+X%2r s- fma.=$0JLvTҬR;=6/YT 1ۅ*+E*j7M/",0ޔ9Hrioꆖ> U"N¶ͪp穀XTaҏkJ͒dHZ=R!S*Q1)XvOcZZժmY w0^B–U 62׭#\m;c U"[ѹU\P@QLWl$ ٔKŹ#i5@S6Z+8675fU|@bJ@`X5gNUc?$qXnR+l@;҆Q"zZGujBO`d=DIcX5Ă5: k*'6z6XJTgAv?*] +&aG} $ fJD5Yy,iQOTbb,SE!]2JDX(%=IYQgMA+wO.jR9i`IJ+ s(5NzYN-aj{oPcJԀĤ+l ޝEQq,rC hx QQfYZy 435 0S1EIEQDC6cXvcacEQQ9+yfyv Y̬k=[8>B9IB^&}@qT4[nzGA%TT]ޑ0R#fZȤ@^=`8pa5>l]s(q[C0M+!iPaxuHK beD9g>pPzy04lN#8Z[ؙN?~KZ)z6r/|0:ϨX% n=v2-(b{>=K.I}[G\!i*y"##`}qܰF^X"b@Pt_*L#]ٛ1B7!9M!4|PJHi`}?Ry8W`L+Xf{[GEBH]EFF~G(*TB)#4Us"!_  ckYf[`%a\?eee#(*TBej28} 1JH}Hܛ `%8n&M}QT ֋V56WRT y8W]i?_efci xyG"EcU@jСmJX%'7 @W#DƱX%\>#4PZYuj;9EÒzZ/Uz0D2CiԨQٳ0+A,7Ādw̺"VTRB4"""")RM DHՕ-6JH=| GndwTS!0n3cS(DTBԨ+x; 9Fpl|1D.Xw(\`-í=)JѣR!Xlх3S :i080@i*V ?qUA h!`C!&Mf9bIΏhLb"0de5\ Sk·A\Q41n4B@9*PYmR 1 pxd`yfFs VSQ>"l=ya y"##w3ugǭ x"F=X fk+Cv6k{{s05FHY*TǭZsQ2~'Sڛjs;uKQcaprE KXl`,e#3&JtÝ-36XpH$~ݻF,gnO-ɕJtRjX; /=s=Î"L$J@?ضHZ%@!cl͂}xcOSH#n}8y1om>mADԸc#q 3AzcL^x"GŪ6AۙL0g@ m"͛Ã8rX6dFݯ|!6~L82Ԩ^L @f":6]맫Ϲ5RӴN͂] _R3,eSx;ɧdkZs +[GwYc*V/-00:\g`L2 -cc1E4[۶LJK"QZ"9;yaWԐcl̑ bZ[0IzjTTA$22V<hnq.Io<ފMo bB!Bb1Xq,{XbO=v_×ΣeK >DbN; Qz ;{)p5Poiz߽$)"Zn'$ƺ7{Z@#нv7wo3omlm#7;b\cm]o}o !B!:z1/z:u^x : :Fjj{ڈLtǘKWB!B*!BiClEt#hBH?*,XB!B:clX*vyGbC<9>҆&B4YٙHL'7:Q@!T> JU%Y@HnwO|;8磪Vz C%qo~ !R_Öu<h9j2p!`tIÒ| +.ÄTBXo)i8Ju%E4Y-mD 6G B=FXs['Wґ"Cbdy+G^9pS UҐY[OS0!T֧u'vb[gvhc-0H)ѻ]qYH˾\| Y012iVpdG>cky'`P!0jTPgA]_H#ѵS/tԋAb>Wϵvz/ٮk ZPIW^l= '7&RF{[g05$AG܏Tn,O I;_EpwF5,O{ (++⁧;tYOvٹr8Af*缌4/Ƶ$xݻh_W(D|dӠVΫd*¢|x 9he]Ajl5 coxC78ص~87WK[<ݱIq03Rx-jAm0Z{\ZгKY#>)2m¢|d`gc}qV*{13]zLfQyq1173o9:sFi^SBu·Kgc|c!.'bixԼ0 a6֔oY|6ޜ2.Ʈ[ώƋ^Ş=׹]|6\Y|;lFڋÔd# ]/|X8صƪO6APqQ}0n%# Rcttҹ(>r2 19dg Rc Dz۳q/HNM޲w'#\9TNnzvѹ|b܂?ާgz B7t☒5[ØݞNcle a>`:'c٘2j:c>z $L[Vk=k 漺XHl _O`' IDAT{+r3GG)se7|;,̭}Fl߷Jκ$FõlIqX0c V]2<3'pSmٻ;"6<7`^}9|κx?7/zU3g0 *~YF{7ER">) W"$FF8s߇܂L'6Iqx_@˿í|&qNX`Ǿ/ٕbs^϶}Цu[|2w%-mqAl۷ 2~&uyN&Z >z7~#|ŊBX[X_m/ps@7bEN~_5[Ezwp\1Ġsnp8:_ ?|}ݿmBτ2ҰvǏ>fNۿvmt[7aFFmXd10{xyx#S G`TMU*r<Ad7Vmw3C!Kpd$vFlWka;cvrYcy9#α1DU,P>psܕt}nG4%޵#~:c=S}q :K;GB &X|#[ S [\{أKL_8 ѱkUAƪM+a&?hD>s.$s1sm芏Y zM kGbΧ\?H˼` >Gp(WAsf=B!"*9>iGVsvHNMzbG>x'tδ(;//6BEBEĜ?d;13zi.]CFV0YXQm7Lf]mo}wNH~VCAE~۳2Sf]%جFs[eT~;k{|q|.$NGvAG/|;wӵ?fGX$&!CV6smZE-H v6\YBE*+\2p6H3Vcb**V>Vr=GӤg Tu"OĜ?5Mkgѝ5H5H@`q ejԲPJгQ w.8Wh&jҬx7mK}bfw<Ϸq+4=ǟVc@/Rc<M{9J%.>]CYąs:,̭ a֮UυΝU_5 y_q@/~degK{oW>xў ^*5A4=ܯƕ8Yqu~i7H!t~ MJIUb:Msǰ泭i{ډkI_RQv¹al/?spw[./1`L j5QڢLfс2d,eQ ;!E~ FY+=8O=fYYYK*K2ֺ̼X7Lf~JuNoY7͏5}C͆e@^a Q\R\60i7mqIQAEAh[QrOh0@3<{aZ?epHg 95 yv2iw-XȖ[Uc`[gDk}kz}2w%2<;~bO-SZΩ20negVZ;Y?WR:|Jj bJ[4W!-f~0 .b]}+XY)sѷUeyrw<EӽV*s^<`&3C (Le9ev=EbE]m/z8~a9GG}Я/kw⡛]6%Ƶ+2do^P8_}%-V3W J%~"15Ό:Qߨc #'#njXq8Iṛl4V`#pe8o-KD~F"zl iI) -rƱ- g~`RҮڏ]]5,jI-Se甙̼m=LEP(o(W^tιaKWΉے3 bܓN4Z?@V#l/!Vm೷pC *(sTõ#,۪;N¥z),+gѵŲ?ً1b595I[Oѱyzk5H䱽XŻuK!T6b5?iН>T2o#8r2GNF8Ƕ;ZݟPA%j^,\>[?BbgĨZJz28P[^XEYΏMYi8x|@UB8{L癘ZU76V5yJY?kܹخp"W'7 z@':{n9OEj+oHkd⁄kvyAT!Xڢρu^WJz2ߨ3/>)oՔ+ Leظ"614Yw_cot VոZBHCÀ4X!Rғ!1hߵCo- Tj[e@/$\D(V#9d<Eu8߮_w>z ݼzvbѷ/bLa^]»`\,\>zLf HIOs.^ضo~şF c3dnEzVڴvA< 1O@b$ydTGT)Hv-nJÿyr8.`D%w(␝+G78negkw^^+fchӺ-͕#>)2S: ^oߧEK< 9ewy0¬iዟohS(7`"5ķy߇JxJ%^?[C!Tѷ( f,w]Ӆg?>uJ_e`:T /oxyx]o;hL0 aano"w8jSSec/o(^ ma{?`t%pkeoYcقo}oHNKs+ زp2G{gdobY96YbCkaafAѭ޼^񔍽ng5{V/@'$F, DžX̸6mN"*H:?Vߝ/-3N=3NA*5w~vm;kŮ-KW `qܸjT3Nҕ8D"N4><µfW7zu1>j٣yVkwB$14:jFJx7}|f) Ijl!q,.G{g䠬 ҕ {ݼzApI5HGj{~O д\ <7`5~ !N?xz!@pWHbAu{V]U!C} %>qv@k7̅sHvQ3/`_gٱ-\ ( jkuC mcp8;aߑ=HIOϋз>CKMjk3wt?Ro@ sT۶{yx` 46XV !]km¢|ʨe!PJ!pX4 !q!B!PJ!B!PJ!B!UB!B!UB!B!ML!Bi"""> PJ!B! A0RPڌP3`B!B!T6vEΓ? j5(ViyEyr(+.eΓN,B!B"tG_wTjJBc?'cщE!BbB!BB!BMQ@aQ~{7UV*'S>9uzbE!{.[ uY@c(U{B!B'zt]?#iJ "rIqwdE#è :n _]"IMЯ/f@o{n(Ul,m1{e3mZ$\]!0!xجV 1Wؿd9 IqbeCǴ3p)|xC76qg-dB!*Vdž]3b3\0bL0!%:{޲g.<; #*++nߵ έۢg˧%|J ~&Hgxώ|%3^4&'7BXQXtSB!Bם۷+EP3N^\)+HNyέ"U=Έ͈= àC <02P(Jj}c4bW K[̼hc`V{5AFJz2nN-́}⒖YfhK+vWѧ$i*f_)#!zB WKl]:t|?~# , 5B4wN+d2|*#~*؄C6Od2ȴ, t_iiLF ϗ ŹtPI:kWɔwCPX''cV-@Γ'^Zm'GzKM06 zE_5KvHMu 4LsPVݢTjF'SXQ~\ kK[,~NP?<&AEB(4M%"ݻ[%_/xHvEE)IU碈ۄwP>BH2NKqM~p1j)Юf&4TɩIzE:'&_pG1U9aȀaP*v^APpf2 vqt\H[ͫ++xn0B~hyj*U瀫nԛv*qjd|V^}J!+?_$(#!x>)#!aS39;!'*j?.:%b# 'CcфyQ kG}3Ξ((*@|yC;֌q,I) 05!-3n.pur)vn AgOȏt7 3RҒ'wH7/¼O^G.}!K8bE!^]`O3NafK0<;f2stꍲwS2U,= IDAT -LdKEo(J(++}"UTa̋H0`_cٟ|(AP?{Z tOKĜz_ܾڼci BȣV2n8 y5'x EyHM+[̘6|{?`, J%1v :5z~}=%y޶fL}u8,ܹbpkcd~25OZ),[vL ƅ,/;OgdldϤ gh/JF꼓lsorw#cnHX%O=_jyGrUWV3K i*cժh0͂Ma2b[3P *N1N 8pf'c`{ik'R śT !#;c03RI#װm9?Q= qMy 7tn:i^0Y\H9c"9J՗13(Byp#+yNt BXGB*i*%E-gU*A"1.[];`o-ZSIU\\b(p=9.ݪ9[΀旬oLR}PJY-d`x>GQ>*VI(VEE88^|BLr0DeԨfw<ŢXFsg7U%P1%C "8p{r)*P>*VI-V Woكc&4z L4K.PPT.!m|GSwe]?u2[|sSp`r7MLr((3cKsc}L2ZP*^Js&6-|$T\Bݼ+C8ϗ]_zƸV @9qx"444B O}}c,}RbPJ!ӧcm15ƂyhI*ckg ~޽B*~`*p7, `ddU* }"Ri?5%U@B|q'Ю13c15&pwfGWZDg!'222H*^.TDx'9*//9pވwqQ_{ (!ZiiaiZAp0u[k796hE[Gdu@ePijzH u4DLd@ Fsh|<(gy~zK1Ʈ-1:b[$1bZ!ϝ;^FFcCT"JnҮXqskw`w@s+cC!*(zp\dNVWݵZZجX~yƚa .9B,{MunI5cqcݖ@k'X>~ X[ش%krm$i\d2YYY0!2d\2 ,ΝQ9=""NXYOGDmr佝7\Ouʔ)GU*$OvJQ0'1̛7/մ'222%%%Xt>Vư@-&p-%kZ0.wV'c$;v?d1ƺh0oFF9---rhʕ\dB-i3OqKT*Ulsj4BZ*!/kc2XYm/wzjRszX)Q]h5.<ђ5͉mqlcbFɟcqc=-apzj'y>~I\\kynuf1*cIX8aeUnnp"ZK{dcgY%c%BLwp[;Ҏ> eh(ߔ 6d(//Oi2!!Zرc,J;)kkk Bp@B}III<11 ٳgB45rJ gNX6כ "iܹEq+W+ʅqgvѲmmfzy:V$IҘ)Sbj KAV璝P(H&"YudA 揎Ubiiik&$VgtMKK+lj7G@#" !D$@D-ˣϿ⺐=J-X'2{lef!qqq+;ZRərrr1 `iF1L&l$L+aS^-JJXeX'+T*V*Iej'׻D^ЖYvmb9h;siii/sMg pѴ+X`{X `F1t2u "_Ήj}s2eY_1+t f= k:P19Qm_iiihkwp!D-m3fY[.'XhO4 Q1bQWNT >>XR`Bz~b{1ˉ*sgʔ)%\"U'K*IdXǭvouʕBYLf)ݬ)ozdV t<DZl&F3\X'[P9999O1m9 IeY$J٬P.*cVNT\JJ^ VN7jk-u-a]ǜ9sLk;v);;{Tkkk@ 8ƥneh]N1$ezO} LVŬmG";i>Ax/$-t NJXCy(OQ@ `PPu:yz__G{rkdz-0kǎ+ut̶2m `d[q]<&x˓!2 55 kY!DVГӷضzI0dmKC\{ cеm8dA,vH-d*KP >Sg7,kW@qH]^}[Ge]]Y;/wmm&Eфْ*ܜc j%hyØaO}M, cViǘ=* ԰L9 ?F]Im-4fm?999O{̽Fj !rYe1X# gC0CæF-u!pNѻ=P!zWe6;Zp!]KM83Now$zB(@b9͍s?I-߰/-pCs|sK:@m(jvH>j$= `-SMfZB/A$?a͕:OARTMS*s ۮ4.Xǵ>%BaDԡ1[o1p6`j1+lR, fvo[9ǬՂ( -1`b:hZ=fP,RhjAqB1 @lŞj8Y]q=Bqqq/pLܲcIOKKND X$Iyꐩ-X}Mit1i7.lp{kh0&*vk/`]ǐ"A[+64'"k6=&1g%`BX׷YyQgkOTH"I8NT;F\\\Q||Fs~mi. IDAT@tS1{GWqV=M@4fp 0B4M^1 @hC )zTܚ5>mDDCM&ӏm5YP{c۞1NVc*PAn''Qt>"HH75`'i\u4,,%'A׸KADrǬ6YPNǬrK{, ",0AV>gUmK̯8P! ONrhT<~_ٶORCUV٠EX /@K[KBmmGRݦ,CeD"nַڄĬds@UgYup"!iQB{/hZ`?lYmz2;;;Z1[YWi]xUc/<& 8&C&ХAXg+ ֵE[$I˳kJRϒw"r! Xjk>WPUƕ]M s֮nĘjT1+*CQf5nU "OcVRʎmO \GD3|qc2\* Yd1.-'7_B:zWʳn0b Yl [iJCx?֖!ѮI$G 0CEtÇMj~c}fԽ\, dk,dV&^B X91KD <-+ck5dak?d1X6VCB .~Zi { ,#1i( P(:F%@(Z5oɲ gG*Ⱥ$,2l<Ⱥ ]uj Xbj:fmw+ֲiZP d$_h4FF t,UcE Thf NhB^l OƊ=qcfP]賆Kt4 ޑ=Ǔej[q_P8X!:v1+{:ϒ-YNݍS='`ErKtº>h1kb6Ywk>w֘h4<'eb'Xb1)ʳCVx"5XU@0TH$ DSKI,@" "Z!G2I*(8Hh&@3 } D-PpYpq:$i9A8 VYL"iHQ-ʘg1;(81Sa boPDdy.49f,۱hzS7 -2:uEA4Oһ l%AQ@4j![l1fR)UZ!55u$IJ" $ "*U(g333ɲ *J&t܅XLU'ٻ N. d ̰n-J,!d !" (AbR8X,^␐s ; =]ѻH֮sm`s{.ޕ|:,Hp1k[ bτ%~]b>lۚ}CH@4 ,'M$xAYh|!!Ig- (z|r&@"U+K}NMaȦD"J@RM]}Z"Y.ƬBhʷ͝:f%meY8YeH+Ξ=1==]-#Z.JZ66qE.ᴦMlJi>(C#ߌr셇3o&oƬ]v9(w0j잞Sm1*QB B!Fs,zB̖m4f!Q5 BY*c=EN{+99Y32 !ftoyFDHAicYaj 8f'uS1OȲ|x RSSC '"3/t:.HII=gOMM JrUz(RygDX,֭[W&U&($iXږ T!\f!mDD#@˗/e.|T'bb 81YcV e2effMII%0*999HT8EDb$%%5k] VX_,@De_B8%%%q͚5 LeY~.55uN˞;wndS璚^P|唨Zz ~UVN Ha?.$''[~}ƺ[K{WͶICHmKT%,c2։R9|ڵ{ Jb&(!ĂaDD{@$l q.B"JuErZqbs9kux˹Xܹs #dYVZ499Y#Xe7Xf; ýw =dq2*cמUQT@Վs 55u/T**<33󁴴7(@5k7AZZhBt:+hz(jݺuE:ntsBD t:YlI5$Io{HX>O*c18Yeq${))@D !zcC:Q1T,Gۻ6$I([ ی͝bm1=553!뮻nsFFY1¾,c;֨Ή1c1NVƄѶ\___,;=ep~&$8ɹdffRRRf !wM:{l~FF䒒3~o10s?48f.K"`=ܹsX֯_o2KaZ .N'$IuJ(Zx`vK:N8%e-\ bTW"璙i5klV*$8jH9w\>ۯ)""K V5l(@;EqnmeǬ0*cXrr*%%$mqJB9R0,cnժU ["C322%@L ?Mbfdd(mc`GIk׮\~UVUtr`į Ξ={~|!<-?KDm .kc;^O8fY{EpRSS}2 :n0HN{ٶfڞS:vteiiiKm!@!MBamY5:kpG N#r6NJJJGB e8BTȲ/8DDLm{WDDޒomh66FT^jժR%}V joos᥎[k׮X,?8w\XFF\YYYJj4`̭("QQ1: nYe̺4{ .=X[Yh&~S BH!DyG"ζ]쭬j:j~فeY~͹̲K}}}dK'ْIc.tla^eK~Q" Հ5'-- V/_~K5Ek'))LJeM""Gr8YeZUvIBLuLgbt: Zv֭+]׬Y8I{yy@ffQIn$I#u:~lUWWwV)IFgdd(׬Ys.L1Ɩ|iN{sQ*wE~T*l'~njOGFDD\ODqmdY1ՙ!^`B,ta ighjټKڲX,/z#s;8fY#;vx cuk.݀c=Fh%\2\T.0TbZZZ6̵3V.3L/s`.I#fqqqUxd}cKXͽt !fS8j!Drjjf.N6Y!DYs١l\\,xUcR&LB$HYl&Jsqt.qqq+w+Y?u^r\\,d1XztVJDQ7G^$I*ϝ?>ub L U(D !8f{hr2NVcu_Y?`kHJJeu<0c1c1NVc1c1NVc1cqc1cqc1cUc1cUc1c=ARRR!¹$:n'c1'=TZZhDfIMM'cw\ݞ1c,Guc1*c1c1*c1c1NVc1c1NVc1cuKt c1XrҥPY!"5ݎaLDT$IRa]]}rc1|8YQTTq~~~ȑ#닺:>}cn SLA`` f38K={6Hy??hXG%/H`uBCBBd2dѺYfaƍ8uF3f(**¹spw$wy'N8 R`H]$ 6 Xf L&ꤤ$ 2Z.\w܁3gh4`08 o/">>III(((l7^|x饗0p@$''c޼yذaN>'OƮ]PWWÇ#!!7nDQQٳgԩS8x 1faժU0L9s&QXX7:`ԨQpt_߿?ʸ=;{,_~%Ξ= BSL 1cVRDDDh46z߈$&&vLd4c?={6!I ă>JZ 23f #""8wكXx10h cO`uVBAAAr\oܸpaaa Cxx㵾}:[wǭ`muneø;pB|7ؽ{7O^zaΜ9D׮]/]8p @Gj$Ijv2/???̜9%%%uR*1tPs CMMMK0Opp0JKK7JΝ7|}<1YK1xtNVY믑۷oGVVVIqyJeWQ]F#^}U5 ~;&L+VTVV6N$o߾݄oDDDW^'N@MM |}}]n08`qGPĉ.Rqq1.]1c`̘1?~<^u7Sc\xq,\.dh|=³X,8q" ;Ϊ,<<Ɩɓ.?͵y{oR?+U8p _7og}'N8^ ׯ_uG}}=fΜK}m˗/Ƕmgh4:qC``#uLsuxP__ѣGkW y.Xl{> gn&u?#bbb@~p 74jlkѣx.""‘xePdF$dRU>}Xa;'{())ɓ+J6 {񼗗zH###9s,v$T*6n܈#F`ԨQ2kbHIIAee%$IB^^l޼QJKK|njveeeشifΜ"8pxQSSɄ<vOŔ)S //F;8pjiii5k̙3HLLСCuҥF]c-)Bcu9DX, jꥦrt:Q̕7=___tMmmcR!bqtcF`;J%3J@cg$!22xK2~r,]2Ǿ(..vf/#牔N۷/***e޽{d2ZRڻ]M}Znq gDDJ%JJJ`6󉌌~gg!8Tz555'2T*RmߧOա-ŋ]Ghh(BCCa2PRR7AE IDATI/(..-nSK7h4~i7;y\]]KꎻġYmݵDf yYAW&grmj?> ϵ9w 'y4|1,kYmmm 4,ӆul6]̻6fQt3zhȲ}ܮkOcEs0;UX7(TVV7po0ص"o8>cu(NVc˗Y_ɓ\0NT\ ?۷UkhIc]3-3FM"e* Ee-w/Jl?򲦯CB1`p/J 96gw [C~0kDZ?!zFVWpKlˆё>F~gW g4z^JFzT^l;wV}H.UYƴ௏8wg*k/g-]*<3onKC͞gyY5{(ڂ<|wy?`ٳ(/O#mI~gNs6a7]x._>\8Ye1Fv}Ze$*_o#*/{x/o|Iӳyw5,ccɸkp׾;\#'2'cJ^KU&,>76́ +/ 07dTWҰDGbuwۚP~>Kvɏѹz4u#ſ(*g OlnK* ;־F7Ξ1pIV? C_Oj.j1};dž1lux-4J(?{◻0 ?z^{ .x>OdM&ާD3K;$Ҟ7_gte LuxO㱸e *z㱫WcE1ncΉph Chkp? wsp"өTYjϳUӽн@ 'c'A~O><7r1_]eϏշC{ <~~UZ;N]t$XSn'C'Q]e r;ɏS}8itd7"Ξ1b-t(= SDzcS; a7]{n;b GFtrqdOh._wg+گ0@gN2֟sKs~#~ __oT苜cݨf zYoVu=~dp?agqm" Cʄ#`Cwrc1ƺgcXhM?iv"$Xфy1g*hN]DL{p#O?Q|Eux@tp^+piRB)aPs!mIis: ~8vѸ: Μ,o"1odbBB]^;yw5,t{U_goM-fe} R²;%^t۷HyV${>񜏏>zdo"x!_ɭN?pp=?'2#FG?@-\c8'1ckVa}7wQW4!=i|HkkmhWd›;lG{]g6Щ=' ɛ~AGW4Hs: wXn vmyh)GfXWkú0aWsi>|&dmxOwgу_3\RwNۏa32?`Rc;uð :NUcnl[+bbع(-XmIAu *e<1q$Զq%W*Ռ$?rOo//<:>8wV: ,f_}Z}bxF;<3zJ^&r3t] jg{jUfp>+c[JC l\?>@4n7t;TR-@My3 '72\ݵsQLj!P&7RjXWt,;6>[ ~foE Uҗ)ѷucwιOXO?׶iî8Sݯ.FgVW+R%O'P?fB 觤]y(Jı.[Z%+*cdSJgֲ/rn]}(?n貌WKEcGї}$x:fw,Y.ubz/ow}W| Upߧ}xqo5~ѹ j|1^R{4PW!^f@W<6jq0],af㑯MvT pӡ &XV VUYPnw^z:wvnݺo>-+..օ nߩS'կ_2[O;I&U|i}7}=6kVVNUU<''e`sSa'Nٳg>gBBB_i`Ȑ$jV}U֭[uȑ*mݦ 9te[iX=sN:U2 ˂s~~M9&"'kaO_װuV:t誆}]v`JX25jTiXӧ\`\ݔTMe*,,]VTT0WdUSd4+ FQ^^^v*+<7j>`իnݺXRi YϿ9UN-H֬xOM|n I#u׌OVaAu|A;Iyc)-[='afA lBZsJ m ?}zomꥻQz-77k:xCip>ɹđg.d*ےW2-yH϶Tes9v-WXi{Nj]^E%vwە[ίֹ23rs cVjZ[qgkdo)iۏwC4jvMt۶?>@cND(~*LPǮ]%+WfwiupcN맸Dqgv3C`|bkn7x6уn%dS<7%Z7ָg*[^`=o [>f  /ߥ>ڬO` &7Fm9bm<|y39 ~mOKҚe{5mbE#~ݡޤ^>[>y;4 6J4OҌj޸d@WX5^X]9\#ɋn[̬g{/sy?nwn8=_}YEX5c+_^ЬڱDmaS3 Q^?gޜȴ67^@`}I]ՄYu.'dOAFz\quXh6],۫‚b%/JSSҕ0k>ջc5 <'4q'4mzOJL%Yo{P%J/LNo{ж^m-୯5ab$Ѡ_iwSgߧnʶ÷h)8Cj۔Z@4;WU{_m=Lʶ)y.%\c4ջyZ~wۖ<^diҴj$Is6畜R*ɮu.'_Ei/-_\?І=X BvL -GծcjT%:I_p{gVTP%}^ZݤëŻ2O?WOg&>T-jeo 9RKVNS4 6 jfn-G+=O/hޭ>i+I[|^Zh}v|觀@?d'/ow+azŎG*wLnF ӄ1Zd$䤁2 ںUKw1mun27E,uCQԪ%cmW-٭C;m]sE@_vz5UyT۔W-դׇCA% {q۾WVaver&aĹ|mXK4}efh\'01Fۿ= 4}nS׷t.3E%kĭEp}Vk뽸D]oo&-Zu}4쁮:;~tR:iU8&ؕV%O;6$RǮePޝS 谮vw!j2GІ*I{q"[3jn;6Q~^bGwo_lK]_#uWBmZ_cVG[lphCm/w7=1aPrM jͲ3{+{ ) OQCfԚe{]wmDf8 L;ͨP1A݂kU׮mǴzIϛ]x>GVǮ2,@6Wٖ<=L?LF5>І~i,,(֚wp;^i{N^D=e]C:s0yhC8jq/v /)G_&2\m\JNcΪ*,"}%%0E*YɋR+)-j1m+z("nX m;wUDP_I8z~^W'aZdFh̄v?}Wzhc& !t",YogP+EP3F=CO[yzojF=a i@liTI5XmXW蟺uTTTb!jڢZ7Vr*T שセR>8OzK>^n݂չ{ y1χ&BKڧ:qERiWCK*lCdV ^U!/Fc:XZ(mIe[<2,@Q[i4(u IDATM[Ta8q̢;~}//&6B).'A]MMzlߦY}sQMqH5zh++t*=Q?evl:lKB4RFzB4҅ ڰ۷lw׬vc:"7<iy_/ۣ[D9 jfK,FkY5VyRGgK9e漹Zuܔ]ڲwUo 4 s9 UUw;2)܌gl7Wn°x||=ݯMݯ[xzk#/owO7(;xmJ3 ?f5ۺG?͓d_9@Yn}[G6w:e {$>>4EZwzY,Z*7'#Oowz*#=[{; < <UX|9ik9r->[Y^Q;eUZݸV_s/VU2=܌?:vm^6\S,?QU렲w~"+f]UԄVU*pn0IRIIɔٳgG}4jNt9slCZaB|||*afYd0Jj,IVu$I2H:vHr3 aT!nF \[E.ۻo &Hc/.[SRR$͜9j+{Dҟ>ܬ \CHdRIK/]>{%}LfG* @e|}G->P 'MZjzB BWZZM[nVZq!UT&>>-`6j' 5zP;(77~մ9Xp=F^5 a Wxx8F#7p3;{5gGDf- @ vM^@X @FXA rj@ %av~ZPS,3$..0b-=QFt*a(XnaT뚢U`0#(jK5C?=5l6'^E=XKJJl @`Z{r-\סe[a0ޣ&/_*Ѳ P-XVg&KCmԯ_`.C iY Cd`0RWP%T#˴Z WT'_mԨѹtf.v &j lˍxa*@5h4*ƱVJzl6߰GkV9+ɋՍpM¡/i_uÏSP3>}-h4nZ oVkI>t`0Z.N춲` jQQJIENDB`././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/doc/source/reference/architecture.rst0000664000175000017500000000222600000000000022343 0ustar00zuulzuul00000000000000.. _architecture: ============ Architecture ============ Murano is composed of the following major components: * murano command-line client * murano-dashboard * murano-api * murano-engine * murano-agent They interact with each other as illustrated in the following diagram: .. image:: architecture.png :width: 600 px :alt: Murano architecture All remote operations on users' servers, such as software installation and configuration, are carried out through an AMQP queue to the murano-agent. Such communication can easily be configured on a separate instance of AMQP to ensure that the infrastructure and servers are isolated. Besides, Murano uses other OpenStack services to prevent the reimplementation of the existing functionality. Murano interacts with these services using their REST API through their python clients. The external services used by Murano are: * the **Orchestration service** (Heat) to orchestrate infrastructural resources such as servers, volumes, and networks. Murano dynamically creates heat templates based on application definitions. * the **Identity service** (Keystone) to make murano API available to all OpenStack users.././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/doc/source/reference/key_features.rst0000664000175000017500000000472500000000000022355 0ustar00zuulzuul00000000000000.. _key-features: ============ Key features ============ Murano has a number of features designed to interact with the application catalog, for instance managing what's in the catalog, and determining how apps in the catalog are deployed. Application catalog ~~~~~~~~~~~~~~~~~~~ #. Easy browsing: * Icons display applications for point-and-click and drag-and-drop selection and deployment. * Each application provides configuration information required for deploying it to a cloud. * An application topology of your environment is available in a separate tab, and shows the number of instances spawned by each application. * The presence of the :guilabel:`Quick Deploy` button on the applications page saves the time. #. Quick filtering by: * Tags and words included in application name and description. * Recent activity. * Predefined category. #. Dependency tracking: * Automatic detection of dependent applications that minimizes the possibility of an application deployment with incorrect configuration. * No underlying IaaS configuration knowledge is required. Application catalog management ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ #. Easy application uploading using UI or CLI from: * Local zip file. * URL. * Package name, using an application repository. #. Managing applications include: * Application organization in categories or transfer between them. * Application name, description and tags update. * Predefined application categories list setting. #. Deployment tracking includes the availability of: * Logs for deployments via UI. * Deployment modification history to track the recent changes. Application lifecycle management ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ #. Simplified configuration and integration: * It is up to an application developer to decide what their application will be able to do. * Dependencies between applications are easily configured. * New applications can be connected with already existing ones. * Well specified application actions are available. #. HA-mode and auto-scaling: * Application authors can set up any available monitoring system to track application events and call corresponding actions, such as failover, starting additional instances, and others. #. Isolation: * Applications in the same environments can easily interact with each other, though applications between different projects (tenants) are isolated. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/doc/source/reference/overview_index.rst0000664000175000017500000000023000000000000022707 0ustar00zuulzuul00000000000000.. _overview: Overview ~~~~~~~~ .. toctree:: :maxdepth: 1 key_features target_users architecture use_cases appendix/appendix_index ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/doc/source/reference/target_users.rst0000664000175000017500000000327400000000000022374 0ustar00zuulzuul00000000000000.. _target_users: ============ Target users ============ Cloud end users want to simply use applications as opposed to installing and managing them. Cloud administrators, in turn, would like to offer a well tested set of on demand self-service applications to dramatically reduce their support burden. Murano solves the problems of both constituents. It enables cloud administrators to publish cloud-ready applications in an online catalog. Cloud end users can use the catalog to deploy these on demand applications, reliably and consistently, with a button click. Cloud administrators ~~~~~~~~~~~~~~~~~~~~ For cloud administrators Murano provides UI and API to easily compose, deploy, run applications, and manage their lifecycle. Designed to be operating system independent, it can handle apps on all manner of the environments in the cloud, either Windows or Linux/Unix-based operating systems. It can be used to pre-configure and deploy anything that can run in the cloud, from low-level networking services to end-user applications. By automating these ongoing cloud application management activities, Murano speeds up the deployment, even for complex distributed applications, without sacrificing simplicity of use. Cloud end users ~~~~~~~~~~~~~~~ Murano catalog lets cloud end users choose from the available applications and services, and compose reliable distributed environments with an intuitive UI. Even users unfamiliar with cloud environments can easily deploy cloud-aware applications. Murano masks cloud-infrastructure specifics from end users, letting them reliably compose and deploy applications in the cloud for the widest range of workloads and use cases without touching IaaS internals. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/doc/source/reference/use_cases.rst0000664000175000017500000000224100000000000021630 0ustar00zuulzuul00000000000000.. _intro-use-cases: ========= Use cases ========= **IT-as-a-Service** An *IT organization* manages applications and controls the applications availability to different OpenStack cloud users in a simple and timesaving manner. A *cloud end user* can easily find and deploy any available application from the catalog. **Self-service portal** An *application developer* and *quality assurance engineer* reduces efforts on testing an application for compatibility with other applications, databases, platforms, and other components it depends on, by configuring compound combinations of applications dynamically and deploying environments that satisfy all requirements within minutes. **Glue layer use case** A *cloud end user* is able to link an ever growing number of technologies to any application in an OpenStack cloud with a minimum cost due to the powerful Murano architecture. Currently, Murano applications have been integrated with the following technologies: Docker, Legacy apps VMs or bare metal, apps outside of OpenStack, and others. The following technologies are to become available in the future: Cloudify and TOSCA, Apache Brooklyn, and APS. ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1696417899.7411807 murano-16.0.0/doc/source/user/0000775000175000017500000000000000000000000016145 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1696417899.7491806 murano-16.0.0/doc/source/user/figures/0000775000175000017500000000000000000000000017611 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/doc/source/user/figures/add_key_pair.png0000664000175000017500000020533100000000000022736 0ustar00zuulzuul00000000000000PNG  IHDRKG IDATx콏+]ߙݶȄ&P&! 0PTMʥ֋+)uuہEM W[ҕ VQ`b7DW>g9C3$[??k^ʒf9sF>ΙwB!r-bB!Bq&B!L!BřB!3!B!gB!B(΄B!P !B!gB!B(΄B!P !B8B![o>u<{wZ_ 4|Ϫ> B!/8?on, z77>sB!w~{?qNWv?':<=ZG8N?(!By<z||/| _E;#9}PW.ʶ\%bjL?ǽJ!B8gۛoO|o[u[6? |x~1U+O۴?|7*I9hfMNꄬ빓\t 5iOʢtū)î3_3.$#Mmn  ,Ur|v4 fh!c›#Α߀]ݦ/V>#q0!PF, 9q>x_S{X!u࣏I>_?g~oۅ@GOBoVWw~jwopi MxѮYɳ,B:TM4 f0ĠEǫ:eBȔ;ϸyž jYfh~Wg0|qufdǞ]k;ݖV-\}AߓH?\_ QQqDpA%:FO_&BytTen|-gHww87`[5x#oE_[}XލGf_JegwUyxe@7F&> :rQ~LL-Ceu,=m?oMt G 57r,Ye8pC$?,uS\$QDFoQ0(2I~jBy|sS,gѐSHd}~j-{ޣ}|/۟ǣ}q"6N>zd陋װx'az +F^' ZF F;b?B.eyO>\֭6^aЌΝ{n媈mz cݮrv.qr8nM$AHJbU#W"Qr,S5de)'VVE=d2BYs0ٷSՔ{uQZ&JUwFnpAg^{UVrƨ}5owk^ 5n8#j Ҍd{s4h~k ؘ^8u|F^5IAQ+B!OWS~V8x?{Bߖ<zuO#5s+kP뷲μЀ㘙Y5ӌ9p㶗N(hQK۩6ret۞fe}]߶QJ)78+Rnd&ByXJb \~k8[E>~O `^AGEe~~GUچ%R{>s8h%㶗ׇ=ԌTڅ(젚 LE(:6 {䄮yegq"ٮnA< s::QGŃ4Ũ nW+oM!!ҌԄb{54v!9_;KsF)͗/l:BЪBRKiԵ;590lXy[4G(yno2= ĹmbOH8;U#;4Toy~ί؝\jt`М@_n4_~|-F_Fߺ>_}T@a|}mD?D;`no:Q4wiOC"tJ3ʴ82Y'#&Dy+SU9Ak'7ʶ :/nwy79`d40@ZAT-GiTV 3E'~Rzd*˕4 ^aJlk LM6c+ՁM9leyҷ 8_wY>,O S !/a~78 oxx8kwi>|܏S8㵥HT?Ϝ f>e0gei)2Y?N3#i$L֜aDS56:gfj~RF'2IGY+rֶ< :q(B*![BhѦs+/YҬ/Iscj4ў c8pԹ8Gi7>89,dYDRVL!8  U9ΣYxQ}ḱ 'ׅ4#>>KƖO  | `pT@̓!S!?ZmyRy7 ')32~sXv RnjhcJwv6 os:XӗJӟyu'~6ۉs^E pmf3q~颣azn5Wv~SxwcSByQ9ގJXïFȲ{_oďcE?4]Mtn8ONK \ZWn'ΚY :$ ,wEo[zs7ɛORNםwFq)OH~y97cn ѩhыFKWYGcq&W]}3x\E_PIZ?o3J a?|ow[TY>9ۿտ7=9_;ݨ=9NYFR8 MdΩШO՘Y\ y4&ǐ,-drrQ Z%c,(A~bySY#oڹzS]&j̓_IT;+)<>8q&'MZZNrn8B+(d,婴a&L˞x]ZGΑ˻ܬUX zgySJ2McYt,r?':lCyz{s`皛I~L i]3ǰ/}|%P@'G(397+=3.[iڨ:řB^=q~[HUe5V,Sz[M&-~sGg.~GJJczg_BΓ^ #]4jb.+qTqS~)f^:ߵZnι忳tt>ze*POGݘ'kOzzW*9I}(i═kJ _M_"s;q^v:lW1mkhvC |B̙O1p:N~TOa5c9@9?@Nz|-K(L!`9uk LSqͿZ~wcGT^P֋O݈%D9)s咙/EGqqF~,-yr_:]0s/X}ƂmٴZժ8FDP2*5kq4{ɔu`"z">hgPx(YOTF}4ɃHCXjUd_e'"Ѫ(kE@TsrSrpFz9q^Ɂywnv)b-Γic,Λvrt&f[B5َ߃TL!8'RO8~PE īyTf/nfRqJ9yYG"[Gn~'9NJ%T=~QNk O* ta&=KsVm)#;^湚+ ׻Y~3FYj,:LEWsܖ3e4rDjnO˰f5Q2r[Lȷ[˱rO9ş]w ܖ2Yy 9k0t:SrV7X8+Bmɓ-gZ G :sW=鳌ۙۉsN=rtt3!g)_C\~=xlAS)7GkxYNAkz0S_YAhIQ_0 }B!sQO[J5Nǧoլ?>/]~E1𑩫Ƨ 8S !"s">ÛhiieRWYdf{Q)΄Byę64`߿33!B1Lǽ=7Үn;~ý-9Rv6lgwATӏ߻=Qwԅ8O|m?.L,qN#2W:VoNӈ g'8>5BqNk=:Wq{L}|RFB{QyEB!WH|U.ݿ/ _`(x?i '~D`s..9L.>qT{|+b0KeQu'9λ\sz#!!ByuY>fHqְw4 KqUq~Æ ﺻtʬrGwsi p13TSjfY?)POqʬI )΄B!/8B!Bq&B!L!B8B!Bq&B!L!BřB!3!B!gB!B(΄B^\FC[Jrnhh?0]/D 6:P065BF3EY}nGlBڢvȮByz.\u791f:,KyuVۊY88shQfm37g"!,a@(g,r||~\pR.77믿\sخ@OTGEuc&iNE]t5PNtAavJUC9}Z L4eBs$2"O$o:<| .\^E]^^\C3_)˥L&<,1[ #ysD K13%>*FQE@?,ʺ­˜Lhfr]/'Bo(W*(FRu{^zOGVWuhL <',/aBȫ{x0oq2F90D+TTo+RKIL7TKARR=v哷A8BYm2Q-BP+MgXFG4<[Fs<+R ~&7ɕ.wfFÖ}}3!L!Tg7^Y4ۈ3Mo0NgGGJ N$x\7tizQ !gB8Dz.14`<8GhO*UcWyGM3@q&P ! -wś#jn(;pK՝QMq&P !✘!zd f=8uud:Q/^+u(΄3!P3r@1 9Bn=fs,RQZ^̦ғ8ʭUf8`lL8kϟ8Yn 7Ʊ`)85!Mq&[>af ec #>?9hNn<#).9yvܾ*PeWfOG7 pTzRV3!<2zwlv /UGB+,slm{$Yvݛ1G-ʢFw5< mQ)a Z'Ru*"řrIx^BzcӹFWq\~-^o^o:?-!r<űrޞ`=c]ޞZD !τ|y^gB!8;XN%\'KH_o$B.מx}<ν^'D4%%Ǘ.Gnq^J>3!k߬dhXYY@'|%dGQ%rۚx.lCԟw>uD^{D02m݊1MT.C/uۢm3!D 4FGF]4+FwLthR+pZc>uANqmwJګ#7oˉsشQ)΄B\uZ/ h85t薁J˴bTk#Hƣz VuCo89hhnWі_L#fp\=r'*ψDG>e;WQAliJ fe Qՠt=8xi0J.鶢.J DE4s@!QFV)gr`q5W6ho'ihaAQnXF\Ak0ZKg>WI=Ѣ-YeT]ڢvpEîmDŽSw`tMjV5{m'e~x=F uAG쬝Z_Ms \jdq{*f^݂Nh\_w}"# vu.ﺍZ}1P5$qT ROk z9ٞ.T9꺢?3q+z$b[i[|zGgB!/:H .Z~tbb]SC-_aNZqb{a %!u),Q f$umU8r4h !Al'B+]g>W`$_bfۗ⼲b y ͯR-B#-DӎE:l9X,x2Sv$vyCDc#nU-%*o,K 8T"Q7WHsK/fx5l9}} hwCw2juDu+v|-$,CI?2D?=Qߕj@KWv/)JO82L!䥔簋VFʨwXfS*9wGUP_UdPE D!(s9uG1#3vQGeaWH^CD22Ek%JlrƢXqA iW7%bQ+Qp4T3RVZ*:(fYK6m}(1Lhi8ߐ'7H)S (Dz-TdŢ 'WI"Ryq],ľ\lm:?Z_8Ѧ򊅴Z,Jm ZW,5BNg%yuÖku%G~$*!GO~MMyζGgB!/6s G89ғDR)E>J%"dBZ[ GhYFX,v\QNÁmk*%a$%y*?[Ip:skQQI.O-zW=~J ➓)&je3Da8CULofeE=]Q4͗Ϝ}>ʧLIJˋf&QMSm"e.58_ӌFq.JN\˨2YNj4=iT8z=S5Z R=KcI+>BWrGk[#oK"3'Ls::64MqřB4D(DVIG;C1"FgD?}4ݖT$ނ/177 Q0RYքDupޗJ1#7g[=}0.,a~?ȢAG*ݠ *g˲{sQE<\>y^ey"F5NI-R,'ΣOX91E>]8w7%m ,CG(GqOEW&益sgD3`C⬶[FzD^3qAq&!oZYPmq+WDVlh4ZIʯyV{׈6_>5n3uDex,d,ovS3$E]S8W\V,죠⁅Q %J AD{ʫ { I|]sq8*yŚ8X>1u$Ѿ8Ǒp. qTV\95^Q !|$8ZvUtXr-'fHg՘@[" !gGפjQvI|gI4ʓY' ljVRri__U>?s1✤`X Z{K0"e*_t1ukKV)yn/]y\ΒQ.kaqZ卍n/4]BNg6ٯ$u#?H~Y2mO o~Wfո8[\zC|}SqF2%u)qFDYOR.>`#3!W'p[|1TsB(΄B(Qm. t=tOq&L!58ZGٻř3!B!gB!B(΄B!P !B!gB!B^Mq_bH!/:r:>cyg0L<%qx;vNƏ&]8Zu'qDhKm 'y8+]lu ^]wgm {Կq-ܽuUUlnz ~1NlZŦwgEVoo&Vewc?|7ٟ$~__A@KKԅ[`E,Y׏4j qj,X c Xķqw;Ml~_9wx 2/RٌW\m!⼲bލpث+D8Bl7,z8Gm5!CЯJhGQ8S5QeyXܺ)UQ\^Mv}%=:Ѝ:׉z{vPNu*D]~*eZ:9l_U+ᕔp畴$Di[j(7-nf1! ڽ[uuCnfEP]@4 d0xEjqC" ׳DBq^V9Xn {8 0ȝ1./qz-]DWw8R|ylc8J4X1N/8_O5l_ ~ky-K%rog ^/wo"|OQgm&Fth0oX1]iĹ*u3rG׍8r;G$SN= '[Ty<ԅ]!zMHg ~&BdF&ΓԒD !p9ܯJ8nn6m[Q3FM53:%1W$BgX`"9,nĹ׽KrlLL 1U9!|q #I(.Zb2c\m8U# $i*3kː(D^uQjN2,ʉTOyzڀ|8STs N/qz|;qwՓ)q`GEqT.]!9ʴ$_̇a ipvv_4Uc >S$*UesF 8B"'^"և8e<;ey?.窋n޺wD NC9ͪ]lf8Y uqKs5*Ny]Tdl;Vy]Mްe4Toiq.̱'2٬GiIc~sI{%>h"!rB8\[M6eD]H7hG88!0q.A[F|dYQ۝m`[^lM ztuC|9 ڨId9np:,Qqǟ5VLbG2B047Y >Zeڀg(}BJU4V{QX!;G_}N,nrsί+jn`'?\:3V<8Bv˼eUCQdV yzqVM/`cW=䌠]+gaUx4BbT+HlL!\?e3gijD,.lnTl{Wg(%i raV%9WfHz53<BUBj=9DSY9lrf UìZ2a yMΝov_x@0 0[ǰ7ikMkGGqچ ֳDBq&8wǧt BY7= <؄B(#[LGak5#Bq&3ɸ<9YB!gB8B!Bq&B!L!BřB!3!B!L!BřB!G… rq7OB8/t/} =bB^zo͓}B(K!1[ #Byiq0 !UAQ !Lq&3!P)΄BqfFܯ/> 1|n "rꦉQC(CQu=TKFqfϲE2RG74FܗK% P)΄s-9K z6Qv?@z-TEyuǿVt8u;/Σ.Qn<[q>v1e ~" U-9l)kD)gByi9j'uQ1(nIO!:4Yx=q_%x}>4&kVlׅciV0t(Zyxdu]T=zgJ\%)qhlgv-5'.Ywnv}EH {$@;D,m ѴT)mTm"n*l=٦]E+iq=Ѷ6 1H|"v(5mE (V1U}(wJn= Ir,!q?+0̺kľs//O%ZP6^mS]qV;M&$?kW gUm"UljiD0H2+.aCQi֒Hw?\IBg8[a:!OyQE]ARڟ4nݨ ~U;/76ޗV ~o2Tcؗun'mY5L8Pe|K !gvxjN,VNFISb yzDt8nK#tʚz]i4t+ߊ$g}tRQ'fyW2TwB!)NL4#Bl;n%7 #9lj٬ł-OQR_MN~E͆0 $kMd+kM J&˧D6jv4xT,Vtq[BL7'B<Jf}n ι%[D$AED]eЮsr@ ΋TF2,Vl=&l/9~U)WP+Bgȡv]HԔE498e_Z\QQQAoFbaJ : ۚ'jbP[6qם1O MwbUt+G6m)ga""vZEaWBš as`{wv\OBOFq&Fu]*,o/8 Ԅ^$qzUq.\R~9w2SQ,eoN)ثXVV )R|8ӛD;t*J4zjt>94IIP헊VR㣩Gtͅwt su[aNmc紲sQvC-׊VF%RδYAJNgőAk3S5d4uJIʋi9q^\6շhD K,qVD2L"Ά\;}ЋŌLGL[uTT2~tUU*Hneч5J4ZOh8~K$꿤898 JxҖ.%]EHlc5SXG;>zq@DqRLi1<.S>]wH;kpb8t3šjl <ǽI{r8_v6V8k.67Opźs'^v!=lﭹb/!q{a9FeXN5e HzU35잺l"ǗJ F -Ԝ\?o Aݞ8a.g!8oWhhDzgD|9͇ j×>ʚ~L܍J&[r9|2cNEÎ OR⬶7%rhr|+M"C>4o~Ke1⼨K-8F4j9Z1'ǹ'nΕ)Q!?=$BHGaakǃQQՕ $Q[m[MԔꜨpV煟KxcGb6/q|:N.նUn0ylϞ""^=YwǦbPd% AyQyMIKaEF3g[(@ӿ9yi赜&Xs9ΚqN#Ff&R;\RBN:}1HI <3ɉhr.ǹVIrm!D)q^㜬S" |43n7ys]WX 9,-MSlj|%=)h:%6 NɩUs`1bUdTqEݚ溦7~ϩj\zoJ}ٲ}E/m>AqFnV @P <{['7',,rl\a|U۰TJ=ɄXNY Z4~4i~"*o\UGO W 8Ƅ7c%OӜrQ]^Rt}2ـ#D.YY,K=v2g`kcUE`u{; Juq煟yB>)eKX_T'_ UZټG/q g{XKwv.>w? ?ݵXח4/8׾[1,ﲯOXɝD⤙PR!=:qVj+S-jn,߄cNfŨ׬bd9ʥB jY5yu* ti4'3Kg˘U#UX6**!^2(kkod=٬ *b2냒,ժOͪr)=()!HgtْɌ'yR`MUC~L꟔Vf}$0UC`f}ڣϲ bܤhrF M)%9m 3'78ߒZb[mbcT}9#J+7ƴ86nv8fu|'qމɓ8eazYp2Ή]uְ7Kggy^h_|q[8kr7uzX97TNGE8N1DKS0R)ŗ8Ŝ4P⼷v_/pod$ykr8=bq^9q^u}8݃\#,:p{H'GgDӈBIdyQL^^"KZ7)#aM(3#gBX2'"u0P,d>5q^9!kBrb{r=!3\8SNn<=9Sȝ 16T%N}7㼨g řNQ,`Bq8?,Ɓa+Y6γT֒xF uԬ?u=oSy<Ʀw/US5Uma*OT6wwo%9 A8ByU"$Yݼ&1Bq&Bq&cOeYNUU!s18B8<8cZ3!gB!gB8B8S !g"FzCvgB8?Sz^|";9uؕOҧ>މ ns<?0SDZ^p=S wKSt_buB4L 1x3řB^VqP7WCDQۄc@NGa~/0'(ΝD9.Σ]XBqtM$oAˁ ps+(#&5D },J_R3xS0 qzz3)!{F!.4k6a R7&_W5tuB[ ٰ8Y0jɉY'[9q:eq7@"e!rlK3ѐ+,P+ imx5jYpU=FøU6B.hPH#Q+i#?PP.zhvCH#n\v,6֝DC'-*, .#`2c[ul29qn}WRy|5J=?)q*(8D;f]ٚqe1zhhw&ן~EKUOJ^=Ǽ?5q~^u|MQ !/-7N+=x4_-=pԯm"?gCt#%ڔoyc/ ؼ"08/}}Sk /PRU8A"8 G1cW{GS#;gP3qzzV>%ukfZ ִ*^>kqTGJ]i '`pfXG71.9`rVHyN4qt mΑ|DGD}) s4+h^<#Ljv|%p+{#ȖD*r> /d(F,Yc/Uw"m _K6F8},CEiYwCԅ'5ZyKY @t}LGE3eu'X~ʛ`Jks$=%gq~Yp^ZKϽq־VY{it~ve 5V%v=z<"8K+Ө3TCgbݘ/[W8 jXeY5[5Hx~hKb͉| F,ֳPi4&J1c:RpLO¯2Fڇ_V S-a(3<33T t,UWV,xV dFeof6]@N1<|א웃9=_q6zl襞B[L_>rpV0֮2=Lͪ༴/;w=Ӈ93ul^κms@6 j-4EZzզe0E:hZ0ĺ%1aq,ӀYn ٠u>9 '=_FكM6h.ax]Z*tn\ʿɰX,gbp~yp@졫wMwI êku*0zX#c$gmfʭ봏]GԾ\V}gߦm;C%:8YNܖɈu8s c#ǨY,3b18jĹaQ69 D-ɒN VɅ[oZi"$Ԛ-QQy4j '>Km'\JUxy|LbpfX,Wgn<AhY*+."8\@0aT8}giT2kg|X,3b18D*FE;N2A/,FY.NW)Y8Iu t]guVVVup>6V黕 Y=Ʀo1yXœӋK7??6'BX,-g1֐`3LSqN)`zlѦeCP= bEI^TgwvĠt,+]lKם!۝Y,]'>hu;mli^e8|qgBX,-gf&mb& 0P iQolU#AXwdXph?߁i5Y(Ѭأ9/@nbםInTgXg{ r׶TĦhS(|~_\\v~Oc_q#ޔV7 D4ܐ[Cp[$Tg?vabY,s牴"+~d_gO%xSp9;8ZL0~r3DO1hgbX,3b18Q༉c<c}o G\]: T>ǁ!%~ vvt8}ݭՕ5fX,źX3Nʰs'G8<({/A7m,ygh~p %炿bY,tt9(*ɾ? C֣Sh;Ϫ?Ya?#;FE|98)Blcnlc7Y,3b$p97S=bX,i=RNMbX ,źsg~cw7O_y8qۉbXG72qʿĉWM3ו[ ,u/ ~J8qI^O-mhgź'< '3>9'N8\ⅺ0= ,zyB ? ~>~( N8qVW\ODz4%q" ,u91YGs`7O^>x)NEzW}N8qfqq]qכl o[ԙb8clϸ  :Ӱ,nv?SgqJ9q骉k|/u,{!lOnxgbn\gHy}DМ&}?>;3~.pHrĉUR~ב|pAו y}ESbXh3gih ?߼ʷ~'N7~,d|b/s1|9J?<,>}!#W:?ISqĉӍ>H~/5_ _3=7 VᢓF^ί;nѩW`%iR`xjWlm19 -Ouj-ESe0 I~^Jsd:N XiN`V XB.*_ˑ6A<?:zݦN%lekxpm a©ukFu0s:iXUlj6*~GDE٘lQiSEY54θm,ls/ǿQ@j~CFzʽVGű2/.8m[1GhoۖjO *#e˜nݑg1ECv8_\\b pIugwvp$lyӮPFF,u2$jr<κsyD7 j–y!hyq"֭rp&k <MVQ$BR?֎e1o<R=p ],Il V$ t"Eo u/8=ULj˩]^T0y"A("sc UQJWgScGAnA0L']BrKuڗuc[I^>%9j.B7f 9AդK˜8?^{ǗkbC2q<;q+,Ű%چL+ h͔ڠKdTmm<ٲ}r7Dw&|fY|p^ҶcI=V+K4DWgIed;j]on=:p)Y̯GMbDpA| 2*k *X5&G 40 P'SWΣk<:6嫦GMQ #| نd&mR8q 5I8% U'H`$;hGQF&yJ \uy@ RzG,E;)2B3Zd$%V +sw:y fCM{Ϲ ,Jd3c ןUfAUycVE$ym-m#P1&ռ2qچn~jW},b],ӥ`~8ѿD2- M*9eCWT\p^ֶc!8'{ʨfĵ̘l?Uoi2b18fţך;^Gùb'U̓<GUec@jE[H!gn$Hs1%H(~Ԭo贸QgC WJ&QE:æ5wFʠYߛ<5Ϩ̍p: VO#*/ϢmQ-Y@T=tn9(r,SWQ&sBqۙ֒|,$ưAP Qin'Rօ5 rȯfHfot q!>Z5jYp5;~_Z%Rq8e9.q|4D0J.$mK Ʉ*.!̙06YM &lb$YQAQ~/{@_NK<-Uw؋^.8z @oeF?r<#ɩEga7%BGK1s2q6 e7 X 0|ٜ$,Y8r ڨ0-8/j[G$ܙq {ifo/7apfX ί/8K\vЪ}9pkM.V4==Kq6`:v,<2]EBSd+wg}v#pE)Fcٮ6(Dy: ooP =+r3[@wBOZ_*)e# (ja1ЪkJbTtF>T3U=NЉ_G%N>#BISSy=́jKm,֗ uC{1o䫥6i_s:5SHx-x g9 ,ުMh 8/m5&3k<ĺJ)Nlݺn ab :bpM7' Mt.#iOh:/8aeeO&>=_sǛ6B~pC1xkW:l˗jf2+h1mcƨgj1‹? 8`eco?8S^sdd&~~m"n _?H?bpfX,[WW?N>uOA&G#Ep8}DP{v=؇gX_md[7 섅|F!v6VU(mPq8B` Ь{0 gY|wSsN-gbpUuX[Y^t1 +&X>'<;æ|;k65^ VwC/hy ?<mȿ%6Ӻ܏89!8;`G]'0?SL􌱙*FE_wUp8bpfX,}ޅ`"8<\ƣуUQ,8=M<:}Ht~$~(Ax"88/{H\tqCozth8{ضb*FE_;7oG{Jyl`X,ΐ\]{p8+P' JgY!WwF{q,i5х[޺x8uro뉌~? &M[*#4XEHhK3dX,֝g"2`}y9Xq6TX36Q]y[xRB&/d DFTYpg{s^Q#g(i^mfpfX\)$8 0=Py`zg|,aY@'ș#<$PO|nP>=ձ7<'AEbP̤^Fzb^W-J`l7݈R f-L:M2W/_Iѫ4cVlaa׉,Z%iY/7V&g8ػ22+" [kڲa}~l{+2<ڷulıgx`SΔB Gަ38X8lbpfpfpfp`ax -Ɨ YG؏\7wT4EpX,UAVm CvP#Ј:u-CVLd/0i>-[5@?3ö4 FU؏ctQu2 Y5B}I-PoV%_bs*M)pYTuuv,b lS[L*v4:~6vWGTp"_m51e4Ab™wf>*+@7. >(p 2juTmæK9t QM #V'-PUi? Isҙ*AzTz m4*yC1G08X ί18(Ly-"J@V l&t%x@#Lfr} WsACӺiQ`MI"7dQDN XCjG/4N/>Nh&^ϡׄ#aXIP!hii8Ouܶ͂J 8F-- u BaTBM`APu%$B9Z8Kړ0яێ1:-(Gk~JHYSGLѱ )JVwUHpm萑 ,jN<^RԉcαjoȮRAS{h,ڢYrnޖeq!qVa%JGujǹ]yĹp.IwYĹX6g9] S%p8:`pfX η%m>JNG7dd58EDm';AI23{HOtK_`eHy,N4q9XFˬz8ko<ߍPis<D;KPQ:D[82 i'jN=ΡTb>{hUo ( 08'L^ǹ*] ҫ:'khKE:gz ׅgXqn~(OY\rpiB'fi6n*]qV> AycwUG- (h;ۥmZҧgDϿz<1Y,g1@/G)F*oRjxV-%_z>#r'Uc聊Y|$j x_,M0;9AւG º$h2p^^ٰg9e9^ts*"/T.]=^MΆ!fN)wϜ0=+t٬ԤAYEOu)UCaѲP׳ YS[TVAT&uߥ뷦5]+Z5./@Me/]1F޾<[ybFh_4+hgx~.k gJ/>/?y?Esgu"YQa~Tz`y/X.s/+?L&ʭ1eqa%p]8?%p>Dͬ!|ߟrX{w{YǷ+xgFX,]gx8CA=׏[C ܝ%$ wsjVmLw>xBY̮kxҺ'N78ID__wpfX,)Fix㏞Kϳ0NLNU҅| 'N8]%qt]1⃧/pwщqb89대fiן ΢S-"ggźoP yYD~Y/5DyO_7 ω'NL!#z"+"3O547NSbXQSg-"A" -npyJ8qt$gSМGY,?o.~4sF}Sa:ĉHtF֌OC 4,b8k_]ٙXGw7<i_ĉM9$--S\Ok~̛X,9~7lf"@-/ qĉӫ-f!q* ,uYh׭}'N8}qJ\Go̦ JUXvl ^aW +MK5Рe ԻIG1P2^6ʰ> }~K ź? /Kb^BY+Ef($cCm"0n jΥޤ[axr!*j[90C䫂;IAF =;M#t< GY,=g>3bcFDWR*^l>8 +0وsĈ%gH!¦^Qv V܆kh'bYFqs4"9fs:L,뾃~q bMJE;tpƮ d\F:I)Z6'pۈ'9 l.fi}L&0gX BE3'N8q~'viU lY G+\6qN |tGxZ)8c3bp.DO͏'NL7;F^TT1zh8s< T: %kl:8ae E5ѓiꦃYd,F?aȃY,}G??rĉ$ ۸tjgը)FSc:[Cn6 =mf{zASg(sGɜX,ֽgb}JzW0b.bpfX,)=$-G1`73fX,gbE]rBAbX ,bX,m)NιX,b_U!6WVab>/#u8dӳǛ 6_ЗO.=<!3bX,.N%ʴ73?ܑ'9C5ilq~p#~6CRp>_++kΆ_٪qz-OAsG=\L Gt=J65 =8%mO}X_Q}A98A5u>u}~tmЛ ~bXxtmcduοxG_h|tvyęuzrScKFDcp쉎n x3A> 6UXD_WW g=mUXU)pQX_sy/KɎ:N|Q꓇*,;m'Y ,]bX,5 I8ޕ o]iVC }y5p8>m<>>y588?zوsa<:K^8 D5Ȧ`Lw8DespQ ONprr#D'g`nfX,z9i4F Qqw6 wp=ZgCp)8jKG8㼊`T:pm{2]]ΛFD)ll#xD}3<EI^^z;?X,b^3p#+[Ý 7CN 9%6<5p*U;α7J\qD:D@q8':<|q 8~43.YOA^vt;15X,b~|ߕFxʅbX,̚#֊-<ɓY,bYs,*'mfX7O.'/X,bX3zC|wx.IY#]0Fr]FNNY,铏7?~)Ct?~?S.'_\6ۓ }~ݿQzQBDɴQw0̮m5B}7o FCf&Cjnbg* CUYm#n~~ fI(pKyzt*0VͱImR۾nzbXu{=Fo7No~_>h?y 9[pаgK{ݷ/K)5% BĈmlH/,@2s,#hj]7X D D$*AG.-d] 5uzugb/ޅ[|+-y գKo`4?[(ko_<|S|/S+tƐ,W|E_?o|?{Oͩ9q~~??|"#_gxѼvSӎŇWk" *bj ^yzP!Xfب=$i8tW0WɂJp6 xް=G v ޟ sQ5 8 W mQ%@Ln? z,P즌꧃jS2`U%(U:? 3h.&=UeYH1hM[GW|wѨ XdyLՅc$S8.ft^1=81i e8(aW Kcnۻd.,CKPylu4x ϫbX/I?fcxߙ|oa_秓~\?W@GOIJ*}TPݬb?}@Dw}wTg?|?ߛ?82n "l"'T  KGS@G8FV;&az͈2~,!x+i`M{ _,yh-sHZ!02k_ʯK:ߌN"٠eΈEλ,8gIKW f-喆ڴ]GsY@0F<7;݌[p ^{—ks #yTnLo`:>P奮N}n h.|DAkn:u:+ʿBЛhϝd8jGfFcA>8Ưo`9[ߟ@=oj"i?%o=_?DdЁ_T*]l$"t/DS`4 Jo0kcH{\UZV/@GTO0- uK.I@PeH8WQ_qnfy>," bD= &igK; x%K˃ywp6ቨa&˔r{9VN:((#v`Vss[FZU-mxۭb4l.4䆂lz;m'IЙO՘:YWŸ3yWDO%2p[2eSX.k3)#QP<Wj^ p^Z ίgϞ؏~#}N8qI\ĵN\nL_-W gߜq<ylmC}ӧ{iFo@mqsoa~`V 8eWR܂SO{UDiO EeEAd/2=pPGsq[Z6_mm2:(Kp9|y'#6d(˂(nqN&""-A_DHp2rL;'_q sHFO9Em/ZfxANiSh0yӚkO`Yy2824?{vvƉ'Nw2kܻヒ~9خ·`b,ttO?o)oşLF~⾮ bUh1%Q嫀r3A Ctag+HHT/{DuTq^2P;igҺ"< 0!,LQ LB̑o@>Ú)D"(7ҏ)#+=t`ie,a"}"J/"#qr+xp9dy 3q'{9 JzˁVw@ש끳9jPYfgPGt¬yUc 8Oi2')AaV C٬N GP'rk jywy,l{L+f(fhaXBVi1J._O?e"ź:q׾A oo?@}^WW]u8+źY...Y,ֽ<Ӻu 6K&!gCNBwq7H10LmʧWV,*||;b{c = VVVѹ`ڃ=cOl{v mb* p߽~\*M<ǢPTmR %ӁjTw^(n1fY[kGp 2lar7!ΰQe\Oӭ%w.,C,3`㫝wdtH@%hT,k8Si cgXa*{E իۏq"]@s'bS1()ִMnur"O7JGEҁpՍy*t އ AJk^lPt6Fm Ϊu v:RUiôW#xs (g :SӰJфd 2 ֿZ&p=hǯpS"SX@aРrt.g#`p`AvIY-KaT  #>-Z7V(13Qi˺ :w'..==m{K9iS[eOqK%e9{2oӬЋLE2pq,fڸs~nDW {8&P~a}xU&,__#B#kRF(g8 `o+Y,h8⚆O-&ڽ!l18!M$ SUr8Ixq %n*/!ҝ4 JR+VKLZE%Fe2DY0´#X1dB^a!q΋- 4P-+`+sAPHҎ0Kus15D/?}m{!-I[ 8ۗvCe>ѩN{ep2 LUYRk8>?58ࡀ89l`}_V#G6Tu;XY!(_߂Zu^m~8("W֏@a%%rnExQ /E6zs|ꦭO&f#[45/mx h$[ )sb6*LMn4EeTM&O;EbD]d(z_@ ?mo.8[Uo^Q6ߗQYFa`Ca͙Wy,d,]3<\Yq]xpޡ"ѴcG ,JQe)@{3$jf 3O jm#}`P.]A[OycB1 z+_}rL#l "ɈsX8 аWHp+L~K[ *ZiC):E#l8*%\_k#j5W9Pe%yq'.%sϾܒ#6}J؁ુ.sᎰd5kYA0AvUp^V šqp^aV1Nn` ,kÜrasпUcqn=8X,35Ga2Q 'ϣnaøPrV3b18XאWQQ1]!T'ZһT~7A3b18/Pւ|{Dh+4 3b18X,bY,bY,bX ,|я~?KbkX,Z\,嵏EI2&M'<[?|;Ϟ=o-o"O>gXwV'u'}_֍zP7Q*wQ]x/}OCTՊrkh/̛^#*pT`NBs3w&̯P:x'K^~R .y[MUMU^>Cx7R8q.&q׺/saLT^ $8IPG}t<%;>~W-8 T|$!p KSâ4b&ĹSl38X,$8'aB;I~Ն!y6*͞W͟L&D}aVE;î"3 ]ncdVЙaԑmAMF9ΰ>t~MUF'feo vc0EQ/54r2] ]xy6PԾYEuA{U)/:* ǶՋ09LP֩d>z>+W J84=Nhxpm a©ukFFozʵnYvw٠1S?3Q%7w T5UW*?-\kTgM/d;tꨕ8vK""T.y>:Q43d9e^Z,gb}Nl0,COy^WnSӆdIv1zVL+ԹJ4Vgdi ʿ?L%\(4l}4阶yziT)_i#-2&2u|:e:UC ݶdys^Z,gb48=e)Ad TmZDDN-!լ+n <%? z.4"$(>yି4mDtj2-j 83Ue>@&YDzsq_mq9p?װ%yi]is8'l'#pztSՆ7q *1Zj] hyA^ בOQ\Jm@ެX Ή,2G:jn'YrНpQ&dĔ忼f-8tL9Ls*7QacyX y'~٩bpfX,M3ݠ["Ha ?SɬA2 ʈPssw("%S| j\F&gJðP6YDWHj&B3%g .9L}( UDM B HUBcyT_e*lPeՕ]q~|viNlrp&3u ap K u@Ji *V΋MԫY򓠣c8S4ۄ^Wv'bX7 =P^95?օt  yyky-Q,ςmaXtґK#og2>Kp|q$֝.j߱u"V1r*FO>qLEK:<*6B0+ _KtNٿJY~2n3ɈrSD}4-yr91|gY>Gw;83838X,gXn%C8g L x XXi:04`s*z z]w )]P mC=v)p| `S 4r3AU ,Ǫ83,ֵPD<>㼴.WW>=:TB=Α8r΃Y;f} p\DLtGyǹ.g1*"="^kp^\n#ﺮyLӓiYt>GLq~gi>νq]qLbX8T*!{4OLj8A|5ڒ|l-wjqiQo;B*jT=[AqKY V?]Xry%0j5Ϫ.TԬ20=u.j̈́QX)g,/ץҩ¬V^lHoG./7 V^ٜͪ:Wr|L̬QO&So4 ,źG=D^Ͽ43%YdfbpfX,40hWa[ ‹M=633bX7!Eo\ ,^"<`xO| ,gbX,bX,bpfX,bY,bX,gbX,bX,bpfX,bY,bX,:;9bX,n5`u磯#?/gpfX,bpGpa8xMӛTnwhSEHZ9'0~uc&H$;jcP] bX,66W%콿H1bƈ3 0"01րc:waEBlmF gAe~w8{NUQt^}mu%Ūaq[Om%,̏QvS*dtCދD%qw~Fw8j-{Ib-w?BeUV'|=sOsq)q%\ #H]-`Z4(3-tQeFqQWŹ0(UDݷ}z&ʪb>>ׁǣAE^sS,~^ęki9Ѳުv1 >qw~gՈoRIxC/֟SzbfhU|̮{yP ; =Y+GYBu|\Hda|%}GY{gKТih9ɳt*k8W9 C,#Q67MNP^EXĹE#yJ:HnElKyi`ɚ$먯ū)MrLjДSJGr)J!r]GT}٪zN@ɴf8ڔۈJCɀ2ufs:,l^FuEZIIsnBeowXlU~B,FKONF}-G/ |IUmyC2տUN:d[aFzYx#~/hkyD֪osS;F*2hFqvDmm; ;.]7YPsA|YN`N$8X䩚I5NzS%}F |}犦*hFkgxQN`KC-pn?ɜlˊwףUTr1m)CA#@"\HX@XxT=\CKPo4tokQ<קdOTdrMKs!E YB% U!y,zQ<9އ̳O7ń"V<#,mh^fj{?+Vɟ H.,HX΀1=|;j)jo,`B݈3VX9mzIX;вp|ryXp>Y?6H2p7k6vǕjrہ_;H})ӿ/ꦦHg}8rhQTV/ǏlGk+&""̮DjC6SW5`9MN'HęUuIy0e/|;mRg]hԍPd4 m 9[#Sin>rRݷ=6Y"Pk[ׇ}u]~3quT >vX}s9:ߏW`uktN-&Mt0h/6/",5-\2u3C=M!aoMd΋o+ *G\U&L|U1/tև%MNo/VSӃ{X8K&vCkUE#{-]Ռx?wXu@⬣2`aQ*)%޳M4"NQUNyZqAg v_#]6ISrxPd=iHI-9Y2݉K fuR2O%jtip1qn|M eC:zK%}D~ qQ& *ܨ4ݬ+"v7Ϯ8śF.yK,*:j;Ą%&!dy7qp?$r3rXNSxpHUp}'*JɓNYm`3V4YhTC|;\M[Qw8I/RϦxͦ%M"[]tu!h3w"YVT<'ԟFxR3;K/[636=@dtVD3/h{/3ėr1NuH ,)K8^EuM7٦`03~\FET,P:]][!0`M`Adr}#⼺!Sr_yY18mMTx˜FRGRjc ,Jm~i(qvDbi|%>I/zY5Boa^oH2f=+NX8Uc9H,c mWSif-43[q+>R_fj0RlAΫf;Vo:8;[|ٲA|Q^c5\okّ,( ͥ]ǧ~t<I<܉&k2^>9Wm/.ΒkV7b'1 j)j1[jg%$zV xEmf3GCk[E#lgؚUcT?Y)RZlۜRK(6nW0)ȤP cn8'#\!Ǒ#uN1qqf{o`;꿳Ϙnͤn̪O|JߧTԤ|,OexA6c6[pA$gc^)LuYbi og8yG)"r˧tq_~ʲ9,X|X'<w/^wNOO`\~3@/D_D[u" ,"Ǹ7oިc@/\駟_E>yX'<98_ ;;;C>$,>py/2C[-g ry^'{GP'xEю7~C6&!ryJȷxΚ:x]an$I`S/Xb~FqG+Ga(sωi$tzurZNEjS?zl'esyEbybD}Ի|6.qVm욶r)2_/i{=ޗ@!puę%Ҏ&F6+,bD HVR2rXY숦1?5\M)vXDs5в)b[egIG^.FĹre)sdkqSnn>` &",&Icy?KO^=r\[Y/٢P8#YJryYdq8ii7ں(-T]V!y,]ςgզfV(9zK$:6FYEȴ}=Ĺg'Tm[YwfVѢփ , q#;}k\|(T NvSX~YtnTcyy_{)ͷnLՐ͚ e9SRz8)vVnC3TNJzσ838/[xE!v8V4aAFNpLTvg=z+"NQYެkqg&\b?Z,AC0ƫLgg[%-!+uVRC⼳ ZubRL`YEI'&ןO>_C Aq)vW"seՎ"gqN_8=JN$S8x:}zyFPg'/"ɓc,i|_kS<)t>ز*19nSٴ9%j&9΁קs^@.RFˊ|Wsr/.:Z:-SI!qH[r%Yhrw8:Uys=JM* o_V7dbB >;JpD f `UϩhʇޓmzGW_h4J)4+烜QH)=oQ| xwlۦXoWNYsI.;~pDY$8ԏ}=ΠdϪZ3,!QԷMmI[6fո8|l&o9,xYgV c 1M`ewΪ!u:pT O%uȑW3uuum8ǫt8yv穎"_XEoP|Fj@"C.>qq~e:9=S~e&=:U:{zv]h9Cp1$|( q*>z|csz}vF?[:]^Q[׮ӽf ?L.wt}SY5g8 g g8C gg8￧5lc@/EY+8T>K/?N"o߾E>Y':9ɱYN&rR`S\'ǺOE뢤ˋ[SMUr) UeYթ{Ɉ3'.ġ^Gv>7 =9CZu&KR;,.K;nz{=?tV]#˟Pie`jN4]cS~|$$;^^$\+>ܡYyǽ꾙SQ0pj9yǘc . νCy}yqg?q.Gl eEU< QAjs9|q \s$Ǩv7qx?_\}[i3+*" -,ȵJQP}3Q},T*Hz_jMQ]O҈HrOIgJC{,#S\37s޶ݧMh:i8k %MvZ7k˷u~&\">ocjFimr#9u(Nb]lf農 '6e!jc C \? S"Dr%B'&/Q1K ij#5͇\gNj{Ҷ~RU>fs_,wlDqK9A@.ϲ R]OF4;%U].YᴳrQJ~\J3M) '<ɔJ%|Esof߷f9=/Ş3ԋ_[q7R֪_WހGq &>\/i{z_,Vk~K W,O 8[YYEj ?R{ZhˊO-@,(hJ"_",ukhѷWr4W2џ eG$a<]EXoe}=;a1suMrZ˯%rN( ,Y8 l+`h͇|DE"Eubd\jW۴04KJv0rlQϊHeʹ拼uVSlnlQq IFd{s_y_FYeSN.yݟp6Y33rPr -:*@_ouy͆f@mS4׉PU[׷OY0'˒۩6ZPVW Y^?J-ˈFӧL3FO cکȎtjӃ}} Hˀt!~<;bi-gřbkYyI+E* (FϥjIڊ(s|rosCO{FBt^V-VԦjVK0QBq.OZ%r^7)V", Rƕ8\t#MIeP8]?)i$Cww\IfȀAϡ}Lh[R le"YUn+ |O݆*ItHq8$e ;тdE+q>Vmq?fK(RL}i뤍_\eBVu=A=E_ӃlwĹӷʭm>DdDb&`qh7RNJmz(TD5z8+#NLdyK2>uqfCg}-nDX%_9rlYPtFC%J4ڴ 'YQK=[^[.V4Tk(v$es-R.J~ ˨7U>$K,G^@K^q#B>lي,YN8\s*rDmިVH '?<gkjq_mXq̝m~")[+6 qmSݺܴe,>̃6kHT N1N2X@|0qVqGۈs%eIrNBG{v( VID--*wqkskqM=uRߌO$SloVSYV俎8܎`Q:}: F8W;#p5Swg!kQ݈8sybOxĹ0j#:i8.g&uˍYʙ𫨽JM,}K!6#;DMZo8%-,*^L%?]huh=zMaqj8W|YVm}N#޷]k3YwrR5F4 \rG7NF6_KƨA3G3t'nikV\D8,1͖Y|2Tu-,r)ҬSesx7\PCsZE[8X9 Ime\N5ٮMIǑid]V&-kL>k;Zg6ZOG.Yh\H>Cȇ#9vqq3"2ސ<3nݯrd'Y}o:s|d N(`JquyqU5mB6=؇kӇSM>{tnth~ qnE5F>;^]mg$l|-l+E;b=FЙI,ԆH.UC,S8[hǧhޙúɠBZ069m$45wzUX'̷N > ԯM/ιA8R6n,^fQ|y֙i47$v87w3U:冴3s̰rY5lT*ʞ*ۦBz}R 6^͂8T`{oڿ mQjUc5ͤXy@YM@^}bo4 ֩ 5QJ+6sW喬?73 UЙo@![zg3g @Q@8paꊪiύsoUc pvzBghk:#یy>T&Z >)v0-GY>M /{jۅ~sێ~Zic>mɉ='0']w㉖hq\zq7kb7㔮Ƿw)vx@YgS۵[~Vnn?_ '覔{soEܻɟD'gPszka\|]=gǫR<8ʺ~wi`}@qf_4+>Ƒf]cED@?8ғ"rWTy𕉴%=݇/&1ݾQݣCO˛gW۬)444uՌV U'JH5EOU"6l}bP1Pി#tσj3Ź^d{^P2-y Y7sE^{> mDž*_#m4q.2 Y֔4M.L8~CBIdILK9OzJRZRwnU"^1wGGtVl޿GUNާ;7:z\d#Jo]q׵T},~>xLY:| Jv>x=_y}$,=i䱖,̏QvK޻ʱO߾lK^R&e9=wSK?يçr_߻Yq޽im86*6InɬQ,b7Koh)qH0 hZw_JBֱNYBcc8sR{Z,,5{*1eJ(Rl |{\l8-[q=ߧgM--}OO ye =/N:;.Sz5zHKKBT Ym^ދe}`YANߴyvj&ڮk8|;+zq>2~CoN_ыH~#71m˶.>q޹g=P|\8Xd@`OON)T<ң#u>motlMqge>>A >}qhs}!t"Iiu(<fq{wc"}-t~whKYGA(ٹ8KvErS':b$׈?|wٿqI;ęEyyzaԑSV>R*U+Z64NiL{Eo[8wX&9q)7$\]wFUE4tDw*7r+:؊&TʕJ,_6⬥u#܉V4dmNgi#΋soq|Ik* Mς4\!qu7Ԉ%YAoTtiS#zu^n}۷޾IeA|"lɴeŋ5ztҐ锆M {IޥX }8?%mD:由(>zvBڈs^ zI/^OjyNG ;u<ւqn#Zqy9q޵oڈUtMxs6=Zhy͟yU^u7U#H:i+T8)eE%wICJR䆬ZOLUm4dW9[ 6/UޞPd8KC8b$j=VSIٛFq6WT:AfVW3=\DMa8q_݌읛U=:>(JfX^3g/ֳjl!bzsL*|Z7끙B){@u=Si3H{,ܤ]zuKO-:6봕%YIhNowdx 8?<{BO3"8++uCClDIOmOtIpf@Nqnot<ǏQvv?8o^ң:,ʼS 3g @8q\ q_,Ki>c'1Nurq~'i~ ,X>Eq?~<HEN$o߾U'?X`I.r{:ɱRȥ˟~~W:Gur̓c󥐼38A"rq8%L\c gĹ U.JjfWCUYE(} q) cz=y׵EX>M+%^znBf]Yy4zeVe{5Rr#JgZIϧf,wX,.Plˊ`JETłץdm]'s/)-9Z!WP!q>vڣ/qEř7mwzN8x8>o"?SSx3 +f6YN˲\Rf[-ri@='yYS|+Λ~ {zޱ(\ 8CcjV0rp> ]4Mr,<2Ćmח0tV*lnd%4س^LY&8Q-V!Q59%rɾ+=%cNqz} +iȾ4FCpBa'n:l˶q>MAOv[ِN4krYHDh;Z?_vbg4ieȕ} YwHӿ3\ݧq ^\@FOݖO㎼~~X呮{-Ɏgke!.sb2XO0r6L b4Dէ.wJ˿9! O6 8$&uȶaF͒ƱG$"Mr¶gG žm~q>Kʥ\7I ?4sanG`S"=@%}}}Ƨ퓖KpFj|Yo&8{N)$JzVĦj cN4 ,%)Xb[ DiVz G4J#-s^PP%i_SurPgH!K;)d){. wXB+ζ?b[i9N}WI*Պd+c#λo#T@\S;,w8(j5 @6_6O#UO sEl8w?dȢx({zݑzB (YK5me4(<JRY%ҧt^ZdqoT2ﳼJ]lFV) >֬n1ٻcQs[f9ܡCEw#DPYs5{4lK-)uԒ},ҕl"88A&Y@j ̠E!YV@Ya[jS),S5w~1(%ۻ+>R I^ QAW_Nrs^o7_*5} Sf{? LUg73MtD E0y Eѹļ#TuI<3'́:aBiZ뜂ն\}-6Y>%؋Lm87Nug/`"ojp T_-i6u+u٥"8 {e5Q=s?Y~%"Qqޱ#-TrYQPUs/4:e?Ѿ^K5UDFZq^:g. Y]E47\ǁ%edFK˫D[Vnb-q&ٵMښ%wwoY6)-m~6U 1sܭmq6Fr"ܮ"]i~g8[z+ 9񼟥+t1H[C#[H{HlT 㙉nwٿYSK "l+\>j!+-Ϋ۶J ~'AZmyqHlsSW'fFJֲz|agm7R5hs3r-֩Jk(XpqԛPڐe{u5~-rT;3nHszYΥ}_W 3)}:kkos[͈oEoOo;鯿W__Sc߿}-}ERuBWYW)A*e- e&AG%ۤ?1MԵR]gFqMjr1 yNm(rN˵?UN9:'R-rB-de 6"m#y~v"mL]ֿTMCg:K0KE& *˂ŌfBO3֊-qg?PVJtc'unp+2`Z }a!B,)%}[8vrY$ugq,r3]#~- U^\S$ Ze xuY)}S/ҿv-7?ooo/?-#g',_~GUZɷ? :w~m~}9?3gyIܩgXL<3ÀY_Nfv ?{LG2FwvάUKQ,'\ss*U+13[X";:3s-7'Ŀ8/Vs"䃫|hRmdf3:m;tł/8f_qx4j3yLҗrr &Eʄc?uW83M"{nی8Im׀O_|E^qH~oYz˸ꯌ87'ßfq߾o pȔ{sjm#zFAK` UETTv*HE;yaD.5W:t_3Ku1qt}"[ƈnq>{_&ZdZ-qZh}Xə)lGRGFa3D9 NIt|gis;,)mzӿ?}ISmeXn*Kg%3*NqjJ_o8/s?Yڿ˿t5ۛq@,Vᐔ(SCĹ}(1fSLxefPF} 0B͚OcsV̪̪ƬR5~/ԶjF/ q@qjgg88 qJSIO&lgGocJ?qGFdy"`mvA vVΗY4 SvQyZW@=FLe-WO3_Gyaq+C<=|bw9ߓ ~{8q8 Ο8{94\rH)\=sruDYLS8,mG6TTU\(X#2јO ;BetO%yLvϧᬤmGkC!qsγx7$iV!PjF*R8E^({N@˂q$PD2ZGD9!%\^%X8&϶܈RzR>p$qIJ4c"'ҬE6baٶb C"w}lO۔SJBS6ٿdzr=(rStIhsEU#?$ȑz}LcI0$^??.omhu^d\Of o:gV4KEscNihuq_F1nmE䲘 (pUy_雝McSmlsf;zݕ]3кHB.\Oo~udۇ8o?3)Ayrzb7'D91eVܔ9 XfQO}@DręOV86'%nO1qIrLe"՘|˧L(XE6"{~DmeEYdOylbU/ydl>d1RCuKYXj>$gA"eeY_'"*בMDiHt.,빔h5Xzc%Մ \9OC%>0#][ՄBJy?f2~Ź#՞jD%ڟr8s?&?FLU('GË >eFRc7 1m,ęI{bv|,[9p<1ǃղ}ۇ8ouƲW^L lձ*DzNLdNEl{>;X%_=Y,)Pygz% ЎKd2© Q،|njȍdFr9Sҫe9sGWXG6.S-CuS5;,r8 -Zci 4-ݶTmMUb'4ZKn9mQqn~6֍ĹH=ܼ_T19Xz!hS4)/;+ **cl5?@s;Ƞ$H=gOj5*ȟTN?n[kāyϒq_~~AD޾}3*Eqrc8%c@!Xn,0{.uEJp."yNq"_K/[.E\Ҷ8/}O?c: qv;9%$J*`{;B9?f2އ@Y$ITeoŲ)NW!Ζ?^T8#(]qmYmh>[a~KJgMqB*5$gqiV@wܷ#g/gl0dփ;jo$*m9/@b43ӝeoxt㡺"IoV_ݕ8SuX^O9,'t)yX`).rc'FCtىDy&2PӲjsu3䂾V2u3L}V:Gڼѧ^0Ź.h0Tr_%'⬢,-j^sQ:sБ ![y}Z,eqnD]nW"aqܮQ'D/'FVF:y(:~_S NtR LĹ\Тz!qV5yٽo, *̢owxzRUtkӹ˶B=Ym(J}ՠʩσ?]^\BZ ZvX7 KG,RGUrJR6Uy88k9bg{Jr:縤g|[RY*V_]P|T`?%6'b7gq.RckX{:̧OEN{$۴T!q6G*Uf't:8sg'1"ٙv*3rM %FŹI BhrO%czƷK/)kq"{3;n5-\:c[ИY鲲T*5BWr~{Zunv+&IHka[M֎[ؙ,}_ϠV3l1wSTvrՠփڇ.J)XVwˬYLNVgpt=VٽC^R{MA#lIj i01h^[򸈃)9fѬj>\{=ô,$0i2;GwV̪Ѧ8R?*j4,Yz,ln@x=eYV+|xֺNF0OV3XtPfq٫Y52F;oԚk!#5F-81OEwܴBY/ꆺr)5gR'cIԎn3fXT4'Zә%6|xQff!s`;(mDȍ}=74O8!6WLO =D=S<~PT859=s37*ʇoz03 3g @|FGCG!ylx\E7$׊Hܓ>Vj tv |Ź)h.3G54NjF-t(d %d> gyB#߳ȋٻŹ{ZTyOꉾEFR2ȕmXM1o~,+AkJ&9=]7hn(,!ovGcxz='kc1p}X=y\RQENo|(BRъk,4f+3{=#x&@)e) \eBF.rMD6J^8P;$qkj{SjmE|eի:Te[7oc9̷x@`hT4&H?Ⱦj8r<Pgxb!+uDIq?4+[K^ ZS`K6e!q޷*a60sY*"gl0$Zm;rɗ&)tzYI:_dWS$#?S^O6S1 C킟g$(IH~,)qn%J\|.٬#͜byw{먯7GG SU^6{˾AB=WᑈB-ڌ8uzNLYTR^GFz6`W>IE:z$7 f?s!lsB rqnf1YV@%pq,,2z,IruyJ~'y.QoH3%͆,,C>Bnd1.Tד(o%Gʾ9aIyj:U>՟us$M},/tF`,(%@Ie\;:.rRY$(k'Gmǣ8T.QqV_Rˋ)άMzvIux/,/E)r)9SږӖύi$"+wݝeߤ6cxQJ3i(Gf0Q6jj6 -:Gy3ȾhzuZg3g @8q@8q 3g 3g @8q 8q 3g>q.˒` ,X`rxADQ@8q ki?*, ]K.($!F=Kq! E6!#BaVԮ5Xt4!|viZ ~%ILNo3@8    _jժ}4^[Sjzh}NXݹfrӄ3@Q oE t8e2rvM E3rqVs^nWV$v8'~4xǹU _g=<2;o/u6=ȍEFnъ֔5k^,/Uufg gPPTB^t\ᐭxvsW:r^z8z"f)|.߉l3PpTR$EshqS߫kՋ;7sW^е]ckR-5J]gT yb'CYr~XPȋHb}v']e^g p1:BKǛ;=_oOVtgo$p:es9trL?6G5⹲ ffpJMiEewoJkUѽ f7O)#P & %s8ʏ_P$di`BxB19\qƽ )Rr8(?,*qlEmY}cr:}ΌsBUzFypiQW4֛u%G]xKͶf̩?M8:?>9p53 E,uV81e[jOհqqtnF&ǜ@S52uZ^ZQ3?[m=~dWƴ57^>sN4 p?/g™pL83L8΄3 @8΄3L83L8]j%L9]>z<&ZVp6uXg:ql< pFWsuscaah0]7F=|=s=" N{yأ]saڹ ܳtE)YYukلavV{JU8|ϐf 8M;eN%zsk[ ôx/Z| X.0W+.*X̭9|W .[uuw^s#8(ލ# ;[_>\.x4 L@hН+9<| 0ns{VrH`nDOs""np![ẅ́22E&{z۪zjV^{B}/#"6+dx"Nt%^sw, U؁cܹ"Me߲5eH%Ɉ~|ä́3Ȭ[B"R>yAqm*~ <ݚ&!.[ާRl&m_ܫ8 q+& ;&*>@qw͘Tu ?/ULq% Qm?}~`4VnxZ4* "bU^N~T]ŢHptk;T׉,"jf^1UUs ѩYǘ{Fкv# 7X?WCW_`$31/߾v%dgJ"6Ei|U/THf6*ֲT'KDkavu[K oI8"JaˢݩL"efH׆+qaa:ϛtD<)b"1)F:47[ZhǩC\v_H[_ݶԍ E4B&""m32tm~kexڲRGVUPT&)R^ Uwb l7ŎtlZ]'/e$h2zpRoz&L84bZߎ:fTۋnWע&ӢI C2Vܻv;,կҴSUN{#Uz Y ~)8cѨCFqyNȋxĺFFjŴegE?{nfml4y3g["qe"@_2 /3+7ʮsjBv(N uYniuMOG~fhPFJ;UrHvoDvH!Sp[zs~6kZiDgȉ5-\cM? ϬÒc{/H_c~C+iJ ׅeH!H!H!H!H!H!H!H!H!H!H!H!OOHH@/_ò,zz8#H!H!H!H!H!H!H!H!H!H!H!H!H!H!H!,|tx{3M-GhRcV`eޮnL<}dp%_B௣ :ije_2:<V[ rS=ur3 WD5"נ~uuJ80~L \YMG9M>pq@ҩ%TGFsoXu}fSQCOXtR_1i9QHtU~?oxڵea;pPXP>:b|nNƮUgK>L%;oXMo:aű%0pLyZI. 3|1e1 "R3 3F4ρ[0 ckcO3EuXFD)GR@9pkADyx}$fwϬgG9O{㺏Z}*MbWK'Jzxv>?YQl,Q̪idQN{ֹIJyO6'(AFTn3vx&%"UNkͽ "5FȬq6Zrʊ#L5~9 IeCq>xRe4|YuT9 /m8lϽ1DhU͐"|~< Yw& 4NHЕ]ӜHs}h!{m},#]rD$ zcyu֎ 2v}/8(^Ѕ f]`F]y>.ݲ6l÷| !綍oMd$%faKV/_LXbF/=k#)X?4(8.2o.{ ض,eٙ\q4F()o6|C,԰6R@=D%VqQ5Ƶ(q&_yBM [&*HwzДyDX8ж!?19CRI}*fŽjq#pbb "ɽsa£)Z[6ctxVZmqxZzoC$ {}n7fB'2qDNJZ\Up 8q'K@ (hHUvԸȽ*.s"WQ Ri9_sZqyv l-\yP.] TPYh_[U{ ݺ,ü濽HBTOt ݙ5mKP$ӡbpfhmHD*%ީL0kX2dGMMwbu5БP.*qv> \""I8"Jaˢw*"YfFիvZWv>޲gի/(k'>VY5).fN;kN: jֶ((tk;T׉~fEFiͼff2 *%#VSG/ZZYNJfqu$Y9J"߹^Qktސ"2}䰎5DHL 7M}2 ?Y13e8gH&q4 ՉRil _H!?aIWB v_x-jY4l?xkca sqiù{En*>9#4tpS><>7s&uGp {tj3'" ~}hA`8n! ܼt/842#o y`zc%",/8\n69 ⋲Tr/K^(8PB$"y]Qs*#R?aW jԱG)lVp,JZ)E;44Ʈ#dN.]s^+y.yO>UJ1n5̄DwdYY9E -8uɶc'׍jC{JoOoHxɋ67mXdID_A u- 8D b[8EFdH`RGEo[><"spk. zenQeX/&ބK1ȏ~xBc&Gȸ!u7"JYqy "掕YUc>&Uƽm & ܚ(.Jz ]z!Vqt"As7{rnU}lذimvzq\t?aͻ3gD n$'T GkNjN|V.*E_ߐ3OI{SUa{=OצYOEd1M4un5qON|+'aK6<""aQf1M;~MF(bg(z|Ƌl"A >'P#;'N5LRնib[ڊpf˳#wkL8Nڰʬ'!-9!KhqުyDDR"E\8#zp5-i .NF!W.07p㍪fqr࿋aXQnVnwyn{;ڏ;ox=!`Ȥ|Q  JyrKw<߳ Jg,r~_4u߱ѳp]=RmQ4vEOhv}<לY|`/v\"n|l0ibJ~m[y!=׬gF\=y#f&WsÔgx vLˋ9:m٪(4Ϯ_ j9\E)&xsD߸Rڿ2My#bRD| gW~9yNȋxĺFFjPeπ^o-<|NM*~|;$"Y"0رE]S1Kc4TG‚KZdZ4aq @  Xxx; RφB@ @ qJT+ :࿿uuuu+T ౡSN]Dall, y<OPeeeթSCoK!o޼8DzBHKKcY?\Yorss-,,C ERd6 IDATӋFoP֫SYEp8)w$D_-WH!E)b2 tz&f, ô@*G4ޏ@py ;$ v FQ|;MfNax'of/?ߡ׶72đB_ g7ҿ2I n|_<.FL8%R۳dQoo0SI[*Ily{i1jѝ#xt~Gf]ہˮ&|(@ebMe̚+ҢGʌW)S6ޒaΣn(\]3ʥUk{ڞ9**;{tv`q_p!VFzuX,jg Guws3;'g4[Kiwy)u) 2U#\Z1M[x1VV|X+g;Z"z}z ӼӰRfrX][Y_䩾6Fti9;4H'JpW󭺈{:7cfLxfʄ#~3\gY85ői6lUiS)BvNdw0HXX#MR6<)`MX|#|o,R͹3uBVhS_7k WWW!l>Wzqac<\]A /6K qLu ߺr6کػ8>h5g>7jZRCMWgVly"&F0}nj*4mxn4{_q{OQXŘn]ݞu)c+z3'o{kӞRrl^ym阽\Ý}2JVj]!uf;LϋIR%nd0x*7+&loGf}b'5eJ^?{GgVs4ʱϐQC:ZUm%_o֥IXT2в(!GA%peX|f܄ JcŧRd +U~heW+"SĽ"JY1Z,>Nn\H-#Z6FĦϯI}$O~Jz2/ OYIc6>۪.[siCS*R0mR>[_C$K{!/sr~թ|-xOzr=ֺ]C4|r'FJĠ, 9DjVmk܇'NxwJHҡy!"* 5 Q᳁OoZk[DG֍-)J,!(tYJ=m 4݋ISSWWMavޫ>J|ăъUѳQ:KY =m722bZ~BfǻMJSuڸՏ۽TXj^۫7<6RPeiJ/2##E)_s/[\?;{5 .qZ\Ij>p'SUbj{X>ݼ/*;71YJ"Q3zq_syVza~DG7:Tfu参D Awrj!b2k:jM{lR9…c|-#m6'>Q+IOj]DDQg s=ʶWK{e6u{6bs]y%Ugi4km0zѵԋZ[#`J=_;Aqt'J!tgN6GUnPשׂhNh\S;LnXKa;m֩U/&uR+n0Myp|&le%ըu̻.[N'&wi~wbۥW?mPޅ=:k>n\7qIS0s_9{sˎ/\3Qί0[+?4yŠ lq}*kܒgVwq7S-do~M8wrEGyerU/6 J²*asҰ5T,˲G+=F|%\sT˿^dYeOE=?m܁RVdM/Y9d2كwwJ>{~zE;߉T,˲l{(Fg=/gi9i]+꾫yEsc [=LLHz|`@6g4ڭ s֝ ~s%IQ=~HĬ"_X4w=3ɗy*8kmiWJip ;Wܵ5<-IĐkGlS0i Rjl҅ 'Nձn>dxk)8l. 6h@MMGTbMi޴ǿJկUI Ϟ=EWc鑷O5s Xp4+FW2>0{czVUG u4nSB))~pT* W#~Ѓ7dggglFF.RBd\^dLKKK oHؼ</xzzz*T S{d)BB_oѣmSIn-Rus[3?-}} NtwRq 0}7eQz;͸'9}ٲ.^/erfStw$W-OK\RڠL8ҿ2|$;^HXA8ҖHcZ><{tIR Ex#ڋuV<U˚/뿴d1?*wW*Η%oاmSqtJ%"UγC6mcϛ߳NY"*|U#=:`ƾ}Y{B2ŇvbҪ ص=m)bwYx"w>_KDO/فawBT>Mm79.L3gYf Va[0vm]g~PUo*Lp$1oLEEHpdP9/ҊiҦ‹&/B)wo0}Q^S%D}hܤ3) >#.|Pl4Fgs3ƮMqoˊ6-ѢW|eA}eo0:eQ;{}_\̖Y ۶u5:&GCl3iڲ5(M{{_vҫwg~YT˲ns6[_u³sX|kjkhu49LTKc;5m ]mrV_~+-C EƵbge?ܳVOh6*žtڔ6`#Rezc*٩aNi"EIXQո줤la} O̬Q(o}Va Lu kt8QQRFuzIcku}-0/e8Pvg~o|YU3~KPcLBA͖--byVu!jҥ,جG-k[i$ZjBZQYIjB]ҧ޼}N$R@DZ՛VωHEīԠST MDīFSmg-q뻗^Meu<")0Jx:GZ;-(#0fF$NHȇ;TķS"c>2arV%׫n%,Tp4Fťf`m/h#O&9<^P3ќ}_N\\iq`*G<Ժ+7O!"c2J6IZX0 QdF&IXH:Ly_LO;mDԷY[^QlQjn \(w`|33JnIDIiBZ<"V'çR5r}ocpWc2lE62׋b̲tw(eŐF~\ǾC8G-b 10} ^o}iSGK}'w4U//ڹ~""%._Oʕ n.fwlӛ, 1־o=.Eo7y2dl֢͝&/߼~dQ+[Ba_%yeqC'C ҵ2(ud#K ='8[".r װEDUG%d?chEJ "REfdݽȾ>Umzv5 ]u)"5+ᵇc24K`ݸ^~Q8D<>k-QK6ڸՏ۽TXj^۫7<6=MzKY{ ε۝id?#?:}7GE)h[K(-s|3s[JX1\LXBoJ g}Ӛk}܇gφ{-ֽw]9sfv [˾kΫ)6=g]\ݪתM,E* X˸]{qmFo6W[򭪺[3<sm:N4.m=sl)~A8髟|!}-ӱf2ZTHݲ{脮̻m2rֆ\F5mXWJۍf )kEEg^׶@FR/%s;[K#z̹_yek.Nlt_r^ V}hytgN6GUn ,efuKMr: wQyJ/|+F)|xR_ى>7s&uGppX;>H 떽D?UsF:M8/Ei4}jC!}w/{YO4aErHѽ_~7g^ӵ>'עD?8 IDAT2=`? w*鹵/e_u(8VQ]<>aK4T'Ұ;_BUel[ u]kDž_1P*_\N1oPߒ\y%)R8*荊 ]jiqz۷ol6Xş\SE{3jW6/\̺eX=cXTd5Jvkq)?nHVEf-,A ~4Cmod !yWɸm"nu_ϕՌ'#4/!3Ju!$%s9U5WdWʕ01\y?vp(-S[LG64TO\BZ135[-uo_tVUF,_ޘM9Ϙظ\UϯRIDFξpFFD8m츖Hڭƌq)/pYgJɬ׼|Q4j`:B]8iy+)YG_t,~r剔HmE<Ԁ[T#"uFD̔VdE>eRm`)b~dtY,DO/فawBT>KL 4{Ka;򺝢bWU/Dw2"ʸ{#`,Hd"69*eM$ORy᧶o~Yian]ۛIUc3f_qƗ6?f6ŷ\|#>Y1aqN4{_q{OXHeݳ)S]{ÒEuVsou&"&11!yצGmNt)g!Zt}|C+73o]M"E\8#zp5er_4f]y+?PJ=hR|Q95UIa/daawaQ$MټKYP3"""bFË`t&Dŀ"f1bF1 TrN|?0 z❟>>wlwuMuoOuMvPg;$9twkqx#a lTJpה+Ձ}[# 7i.M1?ĸ(c,9jlLX.rU5}t(0.lkM gs+F좷eRv{)Ͼ9mБA="1yE6ĄS1Ll@Vt=Ys=zٜ8FQ=cvݦ:t2TۯY >vE] Lo4|<߀Ljn6w3qî&H^?z::;:CIj "V9nvQ[.eVSo\Pt͏? ]ҞYc/[q)R,(@TldYZO2}Cd%H4tv*I₈F}(L5r]~`|B9Ei~NjF'?/k *fټT"dP_BðGAvV@>d/og=w,>ĿJiIESN6CB -`*ͻ6%˖4W$ KːٵPHѻ1yюk9 73JA HļWhhjI%d(ky!5|T(Y4UέH/{gmEŬ8P v|1+װFN*]΀ꩨzȒP]xۚv:ʓ OxhyD۷x?9" Q4@4wЦʅ{yXr*EewʓsZee[ 6eeE&lBĴ"i˨FmCI+(A #GW޾}l6@ ~U/f]['hPSo`s ٨,rtX_K aκ(.ft @V 4TFDcbll5K3xԳڲ5ke0/ = _ZރA^tPqnDj#h^K: p@ @,x$ֵ{w=} uxҥK#,]tIvPo7{.k1m˭bGzt::Ph.wPȘݸ\wʾXCJ.ޣ3k448a#CϟXJ,xwo?.k_3EdC/0!5ޚVmu ;vmVmL _f]~jvإ2PoeF/9<klij#ˢ҄De<ݲ_:lc|_a=v\qr뚫̬m%z3#V GZln!m+]~/fC{7{uQXz"4i}Ť4v؏`wR OꒅyEfyt;ui{vKb~7E`uK@ :*Ułm:+j4Žj@(稱1a:"Բ󫯩7kD?softf}] Hj}РYu-IU9ؿvnԊEJ1) ;+bA0| -./b] ?wU5}t(0.lkM^=daT;uڙA3}d3âujA lu˄8ԓ@j k)4:EVRPPIhWaQnL ô؏y~b\jeT=FVl.vUTgLyy3Yib=bȓ#t&|S_n1وƕC*S64-& _?4vB?ݻ-0˰(B//+W6+JĚerf#+{W `]j]殜qۡ;_ b5`bZ]6E})2RL\0cYdy~U.x$]\!Iۨ\:ð>%Dd)WbU JW%dn\(cjgں+y&& SKL*¯ZCi} )*B"hnP>g*Eː;co7QU gֵ !MٙUJ5¾b A --me_1 #PQ˳ {@oYT55E2t܏cٌXkϕjgOo.DE>4nuY{#.eV8`TEy/ee޹~x y=+Pm[eѬ I]qeqEfuMfgvqzToaU_JHJ]E`JȤT[3H=!/@4T Lӫ6ɓcuc: R\l2j0mk}f_MUMs5 9-n2diYkD5S8D9ԹOU}9\ ̆R44(r] ahۉuvw*S݆gwj]>VDsu3ĵ3:pj8`:86|6gG}R ۾Z;'y`VL54ԟ<4s59'_g*,oCCVnŸNhZwvEAGiڐB}&{O(i{ m`*6iHoXdf޿ښNoS6mچ=\pԚƯzi=C۪=?u~Gt2SmaFCɢY:uj$O^M8<2lwhޒiwܒ i wYQBfkBZ?bZfk@BB)2;w3bl %ͅk 9|x;v{#`u9րHc=Z$k/˙f)_Na3@]נNW(T ]㧬s z؍%sGYzGo}>-Wv }[`@%9ײr>elSZ˲z`^w\g= Etn㧏ꬍF je:̺i n^˂=м:G J6\45 YOظkClZs:GFVDGyn>c{J ۿVo}`"&6|W7"e7c TɷxYG,;J%F6dIt̚)\ $/-fŁ"nPص㛿Y ʒ_W4+ةdo栐mBPV@|ݴ޻wօ!DNK/$'5O~Õcu '$+7+*8u-mFWb"Ubѻ׫Ǐq4W@Rȫ2e.TR@*@5ݔMf] +WRRy|\&3111h pdxx6 YfAjs:Km Y(}m>U(&-Ӡ봹0iz~w6xmnfoܚ]7 m=xV(DVM1.Ѵ\L :+hquCC Ƥm{$V19kLk6`.svFL[Ij4_[6~tǾ5gFѴ`("Ʌb!8A$ A$b]\ğlժe Fv-C y펱̲9IAN$.r'p =Dldjx!}V-[$Id2o?JhE?%&R#w R)͌<;E>ӓ(JxޡN7?ն!=V@| .@|> @V;"*@ȮPĬάW'y=uD/1 A ~o>>=00 Hc!(ʋ0,/M rJnQ3_5z5G2DWsr[y:SD<{'zF*@JΆgl)(VA^cޝk=w_z%@J.ޣ3k448a;;fx:r{\xIU͞I=/J(+.+qcg;1aGMvPA*T/)p#˵7)Z/y!`a$`FIIb" |MCG<gΎ#="^1ok5T!xD\ףaEɫ6:rܶi6 I0!5?mY;#:uҞ W7>)VyetĿz혹f^|Yєg$6v}B睜7甆_Ŏ[7}<.xDgVL! EeYGf/lbžݳQ+IRzU!Xc%\&D C>R's0!hG&gWnw'3kQyF=ʯ{# 0cE}m,u 1Sz[pZ-eB2iGôZy$ťV;;0O+}UƄBs^ JGa\؊ikGL.da1Eoefnl;(WF* * >G:h^ 0  > ?B x # s-TyUroW敊v>T=x%=z:,:MhXJ~™aJL*K!矕bU$ qp1L@Vt==dٜ>f4"Ϯ=cvݦ:,%ll[t L{N& @ ~i?O À y,n*W‚|w;T5}MNQGzPd)`s$!FV&v&yyFb`WŠ+xa8ahjaz6 <떑tCYX{9>]f)Y]Lzfdkw讉œ} OmB4b//w֏f-1~5r$j+"=btw4HP zzٹb~ Q "jQn#l?ȫ*g֘˱3*"ވ|),UGûh8]MCIR&WRE?7_̬>::;:CIj "V9nvQ[.eVSo\Pt'[q)R,(@ThM\;C͠ }ef4z \L%i(S7ljpGWЛ[19gC6\)ƌ{F;pHhMhń9sN<1irK&)z'xR\i=LD[$<*ѵ[>6pEIi_{^$ɁU1yɽC[\zVl8{ѱȥTu ECǩl]Q}}#,jE6Mke;!jF;f])هOc۸r9 $'W_ Nkh=|C'pTÁh-y/1qnď=f a@ԱAb||Vu$/-fŁ"nPص㛿YHu9(d[춐/ /))z^vǬRB~};bVh8G*)|ʧ7jz}rAc[k)|p2BU? ̇HdžOs/],B~4n*鉯EDEl^h|pBN ?,y%%|Rê["Y1(.Ԉ7f3l&&Kr&1GuS [%ǘDY=ȓy羃hݣFm~ihjkʳД!CTH$?GŪŐda2шFZ +z߹ F4usPY(lDÈ[>q$$B&pW8z8\@ ~_ى^}1SH✛[u1C0Ik ~ #@DuL2MWЛ 9xG?͝I DCn a+؇S B֓ݦqCՊ@|,feHL.QTd"kx!y9YȦc& ? Q0'1:R^Qb  (FAF~#SP)%ŪPy!`0$Zz]kU   `2iS0ʇHuLJar8n`**PWB;Fÿ>^#μ(?AE|q^K}/c\U0eVYƟNg0L}rbY @4FrZnߧ6uFM-h,Cǎw<\.g,6@^e3qw;ڮ=1S[s=5/\fM-6ךEvR^rkS OK()Ȓ=y/dnTBqqۦk/Jpֽ͘ڄE(L@vcu@A ϛBѴqNڰvZet =oINE#|*Y+J)uwV{b"b͉=~xG`˧x*+J@ )U5{+QZ c=a=\KdGrtqsr}TJ3c:N$kyF87 ъ~%ߖ'SCɟAHo%_Zŧ}' '$tޗ%uY5wkŸkMM%5 UYq0 nS|`gęiRiG4k5xng D彣U= 6txuwD ? νSDSL80?QG>TnYp8EwOF;`Ee97:;^jQV ݘ\##畈 ݈Y0/`aLI\ LD*Ha6ECF +=laCYr/0D ?Nq .]fIRა\"gr~? ͓wlwL߯_ p˄g##H[<#kcj|?_ ]USKKK[ϴݐQa mYN!xwo?.k_3E3_5z5G2{p;z&[ P7; 8ȑqDzy^/Q iF`.wđ<@a?HIٰ1{,9NMZ3jH.G?3UX^jǵ9$`\JLQGB]%?AJ.ޣ3k448a}"&wڹzݗ^I(%/c-׾ȅԒD QB\YvYS;۹ ;zh˄ Ra4VQO]+SyQn"I>h{]oe \P^[?7gԲZ|vZ {!D3[kd b: $/VRZ ғMIgoI7}-ꩱ[L9vc'>i]@;P=:>w%7اٙSVT) gG0bT{zOIѶR_~'L^)1ؕˇM[Í ܂^ӆ gǑW/hxyQRPX)H1U[ 4qD)^I8m/Ǡ[KmO+Gю+nVry'9}>!nc M^ <3+&͐PY֑ .쾘'@lJTb^ D됶.^x3]ӡbNu$lΥg/M,Ph7_ !q+>m;hLU58 LVġ{UtrBIRʜ;qOuPSR}7RH0hvbS{G-:#W@3 /8r&ef._U *{c-M؟*˩X12ȣ{J\m[\RUyF=ʯ{lBRxLLLV"W(.)ʂJBOd9k{hE7"V _\6Ր3XV*U4D"0cE}m,ug'G&gWnw'3+4L`aJ_⨱1a(0-y*Iqusߟ;e>V:VLsHq96< YϘPcYYjNA *(ԵBI.T\^ xÜ WLGUqiaf?awVȿ%Xu^ExHIQS[)U 42d.;v.fc5sM8Qt6Biw  n]7v8MEwţZ0c -ٴyf gߥrZ)H@V{x@n]i@ajY;kMc4movS//ɭ›+[*s*&M?#E,]6?TۯA.8h*D2׮'1 ^(;vEG$>^,]:+޼dh5/K_q&2.FiE.)-Z5\1,upB:>q-TyUrʼnBayȐJ1MSs*R̐ᵻa+=2O %qpoa8'ct1).T^Sib1nSzrEVx.Cچ︚3r&XG= 4&_1MҀ\>^n{Z¯0,'??IA9=Cc=?}chvW}z YJDe/pZXD ~#JKJy!AyU; tp-0 @* ,VIޒ.z kNU0T((۫P;sjʕ%uoRWi=^ 7Lsd(S=~fdkw讉œ} Om[sTi٫/e*~E&*v&$HHXPQ}`semKMX@Uz}%nB K5g~S( Sl+g6qɌCNXQ &U1hJES3 E$1I]Gz27lڋΑAQT5CMl%d F9ZJe;Kw׋;NEiMLVQ !-e cNjQw_ (S肔hhjز- ז]4ml_g Y1r8>f{ 2f,Xtm ,:"r!WUUU^qKnt66?:wI{f-E[AvKb)eߊwk}h^nl=2Nenx)JE[tSvkTsv;_BTCAߴ2+(I8]MCIR&W/_/ee޹~x y\4~O7oY!,}tduwt0 BsADrBa Aӣ\ʬ|4tZɝR4;{+[˦e7&_IFmz%,Vc➣m?`~Q6! IDATrK@^^:`Xo3^B $&~pdТ=Loޭh;jIjAEpڸ/UUЌ2WsCοs֫IЮ!ν e{v`m_֙?: 9 g(ʮn7$.>7'bgbФ*Pt:t5jmݢb%Xg' ^xW 9Vc;wYalo{(XI7fvp24{X5ihbOttzjnkA8W>hM\;V#suB-Eٟ^!s(>wyZ4JMG)EAGip(M&Uin^KL"}X[?#mXr}kkk:WDy9Y[G 67ͩ{$rKKEBIAWUU70(tVNI i wY4TioL[p0pYlب ~𼐆t| fM,̚X S ~'=6$dQ((^bBV?B/h* tFh>Eͼ?k]@  WJI3,3 @ ߡb!&m\ſz0^~wh] S{}#܂Zm^taVa+k /8=o[w)G{'ML{W.pt-;>;M*Q9cӮ'ܻ?>>^,WdXc6k Y@4*?%X*hŴrAץOs~L'%diwwhNoZcꚝ{l[>3T rdp;u2yNsKK)'#@^%qvҪKs-= :ck@)<=Y@T뎋;B7UL^rЖ=29^t,5ri7UWUB ccѣF팍0~_ω@ J,]ݴ I> @bV(E];˘_M{Xã[% /LQfAnuË$Ew;;Ȋ=mcu{ORi=Gp)ҡֹǘZ.6(\JU<*tかؖظkClZs:GDYfUWW7yS}VYuS̙= 8<"_ !D/;R@(%O~v.gmTLlwodQ9neI@tZҺOg㋏y$ R6qקLX={Sm6Vab*1Qftu$tx ;zU_T~'1^zQHH;٧{3-Nut^Mo1bD73 U?F W ҚWZ ,t#%l#S.~ IEǍ7nܸ5wr`[:=H%ݪj٫:D%c|W^Ŵ iAn\*WΙ>cʝ Wt;.n;Ismڵo\wág:NXz&*"> -SMU|#Z&4^05{ꅲoq/).pTE I3e YG+G<whSM(#De->N.rr\,.Eyzd1CgAHo0Np{ZO9_oGԨG 7t^a k'=LK} {Ҩ5VWҡ!**aܩx׭{~mU1L}=kJd8i-r+lԨt0Qڠ=ulqzyEA@=`ĖLajH{g(*:٣<ށM %yB (BQUt¬ ق**TB ~8FƦ%E>Io:.}faVJ#[Uw d#֪GocO^y, _oGLH؅3'62 mjjj ]///}H$77[=z/_=ؒf9r\~"UG |9p]<`2 qm=wKܖ|/{k/Hxijl׶琀Wr(ʋ0,d ρ۹ϒRȳp=bgԫkd ={=- %YNrљ˵uPRX[b) 'm:,]!x>?vd5vߩݹvsW/~imkd(1T=J~i:D~S"Kl˵=r'U{&:_P W]VpcwvncŽ2!YwVƵu{)P֘ )ή8STT/H7B8^I~~~aaL&b4m'Ls:q4̼1o-:eiԚ*dut(whz7bֽ|<m~oQ~\cϯBrltt!'>^V,wV|olISf{i`gݖy#d'"Ӵ9}ɋ}'.>Zl7m&5"+-CoO^{o&Fԧ(#*բY3m7n^My-5Vfa^rx)sgGb'E UArÎ]|hn۴UAӾc#M<ܹt쥉 kr.+l=^VW]8;􈈿zaDˋںicjZ=wnmNɒ+Jl%ʞI٥-*{E!^=[}9? {[<~c>9~3w{f̉ Sg=34lʵc nz4)"Ae>Fo4AQםIJ:⮘s2yڋHHI>*,L -S4ke<3]_ņߎy=er r%}gw?uW|eoS#ry#֕c*}哖$yOT|aʿiNo*S8'봿w@{-Ht ?fź:^G&ROm5KCwxM~jM,\zc`HL j)ފs:X偶J_s*jll\PP`0D"Q{D}=O VСUcвtYl얯ޫ&B-QuZ@&I"6XKt YuwwhCt.8|!|9|T,/$ tfF+Zz%s}MTB ASjrh47(D&* kDXU5n*<>Ml߁Em1邈p,v⡪͂љ إ>&ϝK(s6ΡlDe19V^%[t.NcɓMCXbfg. l!UQcWE +!e[ý\A'gsOڙD[gAir /ۮJ_|t:semRAƔ/Zd M7/LVOc`1/x;eĮe8JCaKqĸ| U 0ms nYr_w23V̼04liȃk0쬫-H8//Qs F& L ޽;DGFTyubN\氚gm,u}!X$fO9$5CƓs(ZԊz9t֤\OW/央QG6Pn6N1P` a#uQ!` DҔ( Z9nQ UUXZDWB-R6E뤏:Ͼh\.f-$nZ Oe %]l2,^()@KMqais k ]t P@Pv+9¡I/ɯQ\9 CGmNHrCT C `:VP q|]=iruj DcyAe~g~b?nG M ejY1:i@*f7[T0x!t*_ݭ53mATASd.Oٌz" \Mn~QQ2aE-ɂ<KAqUh͠';S +&~FzbI[է=/Ps.38NUsοZ{ s\܈Jʁ!OpQX{;ڭ oIQ~VA^8rWe Y%$_V;D8h$ n"+>T Vhi BvvrThiit I37E%jgx"ՊGa^r\0"3J"C3k?Dk>ܹ?%*-qܑhwBL3JoHá^Wg_DkĆwv)lD\Y@e駢Ҙ\6o+:q˔[YumP:+B+mw uuuuuu6}=ovSUXt3I[$^AD%%[J5 [zd0I޽Wskؕ1/| lW,qZ,Iuw-YW~bA 8^-=,Uv?[<F~ }C@LteX6AO8 ;L\cӇ:}ժOe N,Ke}Ϸy<a}g qmzV);#pwV@GJ]nr<_ Mt=U(!ÂXɴc&; Q){r> O<(j٧cr?Y'+g:qaJĄ1s {Y)⚧s>-,1^vB7 {OvpGg83#枸g?gw*P $׆!o ~(%W7Ըk'YnE2!DEgf)Q[{:-'.q9'!]\u?.Zn*ӣ tڕR9|V\9NS,r2W1nIVt:|e_,;sA8/?!x8 ],W8 :}X-s95/Y8/ʎG ͬE=it]܃S=dKȶMWK.9SR}mŴ\ o_4ђ>egL4)aŃ6ft3ןyEN81$Pq{V =vAR ޺WS [(AͰf6~ӫDGivl>bȂ#GNT8C6qfVzYFp2Ms/;wwj+>eP@uqՠ6{n? _4ag,EuNs_`RȆaT^>1NTKqM=XlKrF[c^hOܻh'sIRE WƝo\9u=d0N7_pM=.μ[gg#oٹ 3 ynYݎ:3ѩi|6ڱJ7vGi[. K~fU[zn]ZH"V\d$$ETչY-mWs;]G)Bs!thz#.|F$IJT|aʿiNo*f8'봿w@{-B1+]:z5阗zžLo_S/qcmNpi~X`qh$> SdUR>p_\{C6*O惒Æ ,}$Qх|)AȍL+PT1vπZ( IDATVCa͚v͑ôe`UM\#Vȶţ VV:J&Mgӵ FzJn=@jfwrc YYƮfV]+mcF{t P-|lvO=Tj)KVʀ͉]`롁PyotPeiDqu[NDy?ϑʴLUz֋HEIs&N1jo[΂ ƾx~R?qFJZfGax)./])= $w>1q;N|a5jk4 )=/mlҳ_^E ~tUi1i\Q։Z"@q,Pt@ۺǒQ0qRJJu;,?Fϋ aU§dUB}>Pm;0٢AH--)hvjZ6 $7l]%VOtw^0&p˔:3q.\e<巪jѫo0AtbǮ\Qw$&9->Fds" (<?*CihS!eb <`,E s*ڌ9/w8`6HdkSgXu`iM)TsOƹ7fM{2~xF۩@\jnW{l/4"sy"fS ITQuW& zvVs8)Zۍ*b?nG>cؗ3QQCkV̟N 2MA1"Fx)5L%ﯘ*1hBB\r2mpQf;tV0fMIQ~VA^8rWe Y%$_V;D8hBZl7pr;9 nHo+05=֋ J.IL}xk&3~~yv9-:8˙,?& q|(:6:*om1GPL;m1vPi0 u2:,Tlll쩃!>=[DQO'n@CL +hSFPP@>ǡشG=zᵐdN>6z ]qIr 6匪/X&٨BQrZ v$ߌPuԅ;-osNr^XٶFwuKx=9ŤpWD<њ4ѐ)w--6\_N{2Ga^r\0"IR*-T0+ (zy¦"\Y@e駢Ҙ\vvD,ٵ,^eƩ{b-ͥBT{TU;5e>}MRd0I޽Wskؕ1/| lW,qZ,P:컖ˬ+I?z֖]Y7wOɭʿ{hKw,Z/"ͮsfc%9WK Ox[^ST⚗)~ebA\qǵθ||t`!kKěii7Fmt0QCA еkb ՝}yFօ$c8iJ?%?.mL[rRE ZlԺ[{3ТpXY!|&_x)$pgJup +{pץlJAgMBD3@ ROir䴻w~|߻kmi@CY$Sz4]rdt-f9=gQQw2&|HjMsYuS,'DUlW8 :}X-s98nIVt:|e_~Vw|YSDQ~F:zJ)jSɚ+;N1N7V Š}mg?7Kqc6I 4q{f키\_t N[ue}CTBJQ/,8>xK$Y\s#')\=kF! ] A_-_=^Rۮbv ){d{.v}'Sw' Qvp;5b)9̴KvYǥ\?ӭKv=eצ-;7219!r&v=O-Yݎ:3ѩ҄+NN7кw=Y)-5U&o=Y{ )IG%EVy9~;?sN{Ǩ "X ?fź:^G&RO l:r hSkzgdR܂K4DJJQםIJ:⮘sv j)ފs:X偶J_sWB]>UM!'>}LgI٨B§@B3Y>t:S揿c7fMVR6Nx+1[@"L)V5q[i"K_љWLcR%s!4Q;v,UQcWygzz1RS5Jp9+|=4p8 Fk)yotPeiDq$yM󗪇3RU4cL=4$=)qb'eT{tT*8Os F& 3#o 2N~{_>> p!/*=߯ Ti6*-Nx*N(風|Y\?>LÃ"˚;s2,=Kgnc!2( /aTVq@KMqa鵈˯*1e=}ZQRTY6VZ_sC ];0x~|]R l$cphjR44(7K0@Jd}Ն/d4mf@Q@HHr.38NUsοZ{ s\܈Jʁ!OpQXeDE ejY1:i@*ft q ]x _ڏ~Uo4"sy"fS u-4Qtug*}ńP&˽p䮶,KH⩛w\pЀDE-j}6bU)7yHU`fgJeǵ0DKK[RJaF"HA%:g7G"@ 5 lF8Jl]C]]]]]]sߺs{TU;5e>LEE[SgDDgVH=?gt&".ggZ y)9v=}$? {vh{gqkGF4-CaKu,5!`CN>xb\9Xj<~HRJֳ=)2DOH< ==d}IѨ=NRS2s8vT]G̻y<޿]eh?@)_se3)|ꖞL1ć~>uQ=-m93ę/oW6T"&g4qjOvF?gײ;5F.1ޤwo= =]EΘ&ʻ~[UWE O^IP̪hz* !é`:\5¼ӏ~Svgشn2 ^@Q7>0t8D!=v!'L%b"Fډ=wr oII@RNi?Yi5UH)g0pKzs4M'Ol՗Jx$4~B% v wnMoZL-tyt̾J.A4i OGn>VٸjFjO"=Cg5F*rB%v4@q\=B"vG"STZQ~ȒY۶Lk{څ~+Wdpf浗{v$ǭٹ6߬3$T4~RP hG-S"^1ޛ"O}CcrDl7G=z4*2tk{{初T}sEÔ4ݑ-ݺG7S{7d\s$a# -\+1 TJ]~)Qja{<&=Cԯ~QuC %zw6㲔v|(Ȋ:]ԏTL ueTOBX=ʪ=vOd"B]!X,ceu5'"+P z׳:GB._ bNLfIكI|&–)Ŭ~gZaW 0@×_=}b&~pя>Nc_l[FuW^7xG ?)bdu6HABٱ?o+p45)jH#j,r,b}عeULNŭOX3.>2I[ ؕXJXÌ"j+rVЌ^+v!@|a>6ϱ7.vnLR4n?Oo?28|j>2{rwHJ Fƈ.n :c{E*ecms9r.u i])Qx:O&N6e{9࿻'zyAvHaHO^ćo5)Ziٓ ǷŖ0df.fkO[:p*`~!{5-EFk&<qGQ+oomAYOsf_ iQݭ~ 9h]r.k{څ@ _ 5~?(`Ec,4}>~)'[Nl yj|1)zSlE&ߨ/@ 455E@ ߜ_XCQ4ސ!qA]eͳvd=5Zr ߮@ 1䩕D!N_!:nNj ѿ h'hE@|?Ї@(@ B@ ~(0X@j|! @Q"lVVsxUURo-lĸiK"sy!;L\c5q5nӾCSlVgK3Ni޽jjj_*~&S~ڙܸ5DŞ*wIiĒ )/)? s%/ R C$!!?=D:JP IDAT"FS(_56s 8 4/1w}U:uL}Z12x˘!Ǔ_(=,\|׸ Q%kEeuGf;>]E_o$$]H0{V/@!nOjhFyv4`J=\p?LJvӂZP%s͵(ᎳOW\&Sl[ne_)xpdӎ\#֮vDQe͡gRsk:'/]9RXrJJ|ZB}IJ6}PĦh r^iQTu~;a-9_V XRK׷ĊLI-&cD@DT&)71+BؖI/DM X>N"ǃ- *usy{Wb㻑 OΙ=sLzǦ0}AR%6vj2WWZEtvIߙ"*%*'.k8/f@Pz3lALr晩%4]&S*?w>(9aOgҫrrq~;OLd~m?xl(SSSS9/9N\skxz`5صa0Klzneq G;~#lν,a<;& LOcgN{q) 'I2t¸8JbnGXY>%t갱[3J/yXL㸰(zŌӅB)Y.fJ|]Zt|t`Rfeέfƕ]Fܫ:MXI^mKIJ̶.*Ohii=aSBW7j9ORs_ߘ)/Qkӿ0 \v55{IK% !{ 4mӎ惒Æ ,. ]KLƭ%S"^=>s>A˶lI'*zz4Y_Cs'b\J{m>Rb^~ܗa#;DPl7_x&#5Uy}AXz#:Sw4U{]3||.'vqM^FMgAKL &Fǭ4iE#H&|)U#)7qM걛 3WϦk+)\e'0dry|1njM2 t\<2281 TE9]3M%zINIJ₶YH(o9@|Jc&wpTJO++A%[ j%L*r*=-~xFnD+vU!bu:])H\]ڽ4*+k k k]R`O |`sq"@, 苍š:j]jCLCѳèh(7LcTVq* =- !o@@̩V,Qv$JDA#$1+GGiPXcq!}B('wRI߹V RQ:# E^~U)+RWگHWzRR^l["RL)ZҒb)M*E^Ij]TH JV=!~t6 @sPhx%)Uso^ 2yyX/-AEnmǀ?, s*ڌ9{ DiC'Y !Q@TQ"ByceקG(,[Q9KX:'*jRJ98a GjvT?c8WO5TlT*1t :.|j?UFӖx߶"*ID1s[L5+O'MY }*lgۡk).BE)y9CqUh͠';S +&Jk%D«_VP{$V C w]7`ҷZg=cIMx2#`ZA ^MaL'߄j5_ڟ>c&T];u;mK\y~^m-P13c r ظl\Yն\i"ZPupB$@mUZvP|lɲ-{vx怾tqN/[î̌~Y9ŤpWD<њ4Pw/yy_er"$B 9^k7K#8uhteIξwNV #vL䭂x+){_o`E d 'M珧 }A {{SG.ڽA/vF6p bLjgg5f%@99XϏ,ݴsnwC?AslY֎VvAwC˴uef*`V"vj"8M`5SlFXI2i4¹NráJ}G[:_e!O5Q|rRRR[nb)Nx1UoIʡJ$IDzc,Gmf4c!ۿM{@ ?2@;h.@ B@J@ h.@ B@(@ E!@ P@ @ ( A o ;#pwo*qްw H!ɲBO+u_4ђ>egL1˿ct3)~QOj xpƌn>z3/>Hpc6I 4q{f키ĬYƺo1. =ɊN[~LFy1/ 2=oPHG])E|@$Q F@Q@|u r ]gG\N2yڋHHI>*,L,*z4I7X'BqNi5CZ$KǬXw]դc^ {3qu"ดgVyuɮlr!i!΍LLN]}Sv;4K"Tnn@i)%(MX4A~'$) 0Cxwn&2ݒfVFFF ?o9>#/9\, ƛO&~(b\\kM*Ϯ|?o軙|CΗ=N3XǙ)k[HUln^ʙb^fwʀ͉]}!bq:R>1.dn{CU u[6g>STy|6] 0s پiD&* kDXU=`:.c ȬTTZut41vϥIs'mF²!PҔ׮(*)9Mnʵ+ϟ>1Vܭ-U0\ĩʾqdמ6O:-Qe](gQ%E˸ʨDž"/Ĕjq @XSPGKmY6VZ_sC ];0x~|] bWr󏺏>pYcV>9urjB1J65bfQ:HQ 0VVX+ #?K}n5Rb{V4F*))))T4ܟe w~2x\X~,s4W!3"Ě# Ό۶u̅w%c9LAX6"Y@SMqq2̑]30&]Xڸ {Ub koXe21*8QQZ_hW|5V\OIIIII}JS*Uct$SFxkSs y".n >IQ5]8WORJP4 [ړ)LOySW#KM/|s8Ԍy>tjq4E"m_lZ~fB1vk9UUoeOBrEdE㗥Jcr|8'{^ͭaWfl ,K"/C t]gFxj1Iuw-YW~bAm Ȑ,='lyy_er"$Bۚɫ2]UfW+nÌm\L {Q^_}eOI%(@PeJ⭬:1jL@s`ˁ)rJHn3]y NY0Ά+/j?I`0R_AK}5qC@Lr_W)#,ZM3yrç}U[jx@O$݉kv #v$񸕡UsFUT7gw/HM`xٶmkW᪽=#Vͱ~O7n^DV5Yd,Ӧdvsz ͦ.^o ha-7]+ALF¼Bc)g nN= K]ο-a0Ҳz㝼3>^pE/[9dOL>!y'`n[2ظ۟.8#{#{A ޳N涱6) \u=rQɯFLfw0NjԐeJC "NjO}1p֞?# a_Ҿۻ}4x .adWij#k ΅ʮ]P֭B"|B))BB))BB))B))BB))B޿Rrc٠>?)c_Pf JHR^jKO*q0Bv3:)ϺU\>zuv5H<ѧ?\f}B`KSȃEc[tH 3 ,iJFݺIF-4hCi.d-6{֬?2v>xp'gs#sKh,fzuۺ{ά%~o\ţȈ1IZS׶_ A`opE&%Ϧ̓{ W W4KӒ?zǦ bC】,ˋ=r¿]ROdj2HV䢅:,tOhL*ţEφiڸ] ODͽr*͡C + 9EM )Ab|`)D-eV)NaFdƍ*uiYK.;p<-m\+?YQj"Ҥ;W}[9HMFx[x5isɨܒl9޷wrTr)*@MWd,C42Z5K,h5߰@[E5?,Iml R6LruXyFZ\v oƚH7xK_fԌCڢUS*@V+tتZ czjyXhރY=zPܭdɔӧs 'e-pʞ{שk`.R$(B45Xˎ]D:aM<@/ck`n$r")2sv"lU Ȕ (̬{kZjܸpFf_SF6Ҭx #Fy}eEs#. %@6W@ :R~ICKC""t2l3|xowڳ4qҁ7{tSK|3üslY1nɱ19ž[NfQQ)KDDf,Ѐ<5;qdgkr1o ڈK13>o9b Yv>|KQ c4€YwҘ )r7).""DSBDgg 84Moznv5wk;0o`%"ICQ郋DĵoT@BZ\"ZgH+ϐq[1ptشRրkb)Z)-uȼSa/j3vB';y KmN.:Ru]m]ކDmc1M'Sݥ#$v&ʐCyrl>lI"RzwU~$lܻSO/9iEI:  IDAT=oZ鑸^6:RK$saw6[Ttlހ^*3Bgg45N>wt2dK$uǪbףٶŗΙ[Զ"X*4lh.DvϳDĈL= Ȃ˘:¶ٵY!0onȠm m<&- 04vjɺ cPK}zіUQG/vi3h  C'/Nu]-9~fز~O̩}7yN}usR)3a#yZ=MmaD)K+2ù@ @ @  RRRR5 v񌍍BuT*-,, \. h4EEE 6x(k)$))P*x1DM~;-}"^I>5g ]1mX5{H3 jtoEN= *$"֭$"c,."Ҥ1aծgega'"&]Lbmy:U"o?TYM-]ݶoSA^축3|37q4-k ;.Fp3d^:0.=}[L׃EI՚j6tUzz) >X[pcl?;4ך}I OJ88~hUEoZLbбYGb]u4!b\"Vw)b &û{J9 [^V(* ˗43n:t%Mgע`Of[2OFW>&+"NU ja0*^vU#7^ő׻sͶbzOL ^miyr*BHкj}%+q}{oSȹG?V&BT^fƨ~s˗~*>9%EzJR쟿EYkC&ά!×v*|Μ[ .^}2pa G7I{]^̦V؜YMG/Z nb؜M 5]lL*Lܨ &~^HU6¶:C Ɲw^]V^ >?w,!Ui3LUKe|z^Sg|ʵb# v91ۏ+(լ&Ȃ d喜dTU^3=P%_N!ijmZr=2IA+;ԡ.m=q*Ҥǜ+p2af-Qo|=lc 2-Q's=ol"h0uկpyFSJ];M^hFcL,%BFY|0ϒUaA:pol9޷wrԋM!=KN-ky&UMD\=[9R~6|}<[5k'i0U.^Iy޾FF6MћrC=--*DhJޣScIfRWy%UUi(5jԽ_ #.Qn$ϥHdԻWi 5ƻ9w˷tƉ↵Ft4>H\zmLjJta --% vU\<5|Z֢s|KU\9t#Ha߽WV Fq~sewNJG)*P1uȡF:X}t9%| {ň,%"-vҧ?0ogIʨBq\<G Ӿ,9q V[2zѐ"b^tîV}4Eb[g0IW{H.Ua$oWi B\柘8qש= uS\-j8ݐ!"E;_6Kpz%]LL%[cUdju5hefܾ%BUvK뾸?]ZY}mDz0D507t^ss9;qiʛtED:\X%b9OJDnQCih%~8y"]UWڿ %u3D܇y$^Roy뫒 v̺:J,%9*!RW6k{~np08,ޘzw [b!K1~[7&] {q68oGL7\2uJCjG HXnBR-OQo_,#myC̜?akfK5;(I[qdԥEb|mWzy >F9}(籬 kHh~nolWI#+L۵_HT9x $~f\T(v39bˈd;VeŃ&0)XY%8.ʡT{)WV+i+_@M,[v^^^|>mTqkeow9o~unRuֵ V|E *)PŘ_G7g{Z[:>>SX<]~]D(?{_W{P²s{T6w|=Fg? !o V^1o؎ĮQ}ޯ`_"5wR RRty!oK>Ktm+-O=7t m] ŀ!ŵ!s=i̵W#[=}DK~a. bX}!qGdܣ|Oǯ0%"}m>UuxīA+&uwZ1-EϜ5,MX4Ew J Q۳=.BzBujδv\"VSym5;ƃu2iy7#CΜ_%"a0¦E+rCLN䞵=-}"^IT ҇,v'plmJy}l,)ۿ[r7L*ţEUƞ )M"qs|~Q_Ɉ.BRW\kh&ReKӿ>:g5y-ZKF&)z N8(kw6kIcw^ݷ=7rcT|é,44 X"dJ 6zrsɥͮ ţ?zodĐ4`PI^ w\)b\;M^J-鈈صwK{16ВR"~A~ 5nSStRң46ǩ'+*澍,Yq B KnOvpzNƌCDڒ'7/Y: ))W M 8"$+gͤy?Q k]Lwiyzed.г] jكϜYګch)*-O! 8i2N\>Rm;<{aL zy%S"ILAe_Q*÷r:}rV}luVPll_w:‹Li7M$yV3o浀9# ҤZ=-"n f/an ZR[ awkq=U$ugG>עy>f!~E>("ީ H!H!H!򖰥Y6&o```ߐq?#+{*q͐~sh~>SN糬>#6=|x5#FoxPFCfnkj4KWNqmfjjIM늿_3[=-00pG_]?0p ξca_ 7 ^P[p 2vJ薘oT\_0 xm[(]USV/]i_&_kIw수5=knJpM iPKW>[br&]wx%Q/҇,\o6¶:C Ɲwܨ &~^H}[<~[3 zOT_]Sڙ[xH_?O M_jzgN- 7fի/X\>l&i{"bru歼j4',|~H*.Ghs/*r53];sN)*n[ԏvQջv O9t#Ha߽WV7a7-?w_[ۉYHW2e-*;j.=m-00D0'/du~k TLrl9ZkB:xoQ k]Lwỉ}:mkcRW 0fXmi)Yx lHZ3g447d 4dZ~P27[eXϘGߠzK/JL-Op6+XmnJ-a(>RH1K@Dm,ʜ'%"GI봪YbNö\M-*NxG`T,ıS&~֨[3sqގtoh]Ϸ}˾%}:6ڱdoZJ`ԬS73nr%pB8)c =0{B_ [F4b%zU<9dw1,[/bcc||RZ1vۥD"-!ۤA- .K_ju||Jo?߯wÈASpO>]g? !H!Ze7+2H!H!7(LnU%1zÃb UKfY]iO8; }qM iPKN~ BEu[u`Nkoo'ngOڊǖeͶ3wd`eبn?H mxbgTBǩC4d49W~_qvХv^hdV CȈ1IZS׶_ AX!*-zg:눈*칑%4ESj7g#qi*cgw'aH[pcl?;4ךSq.\źDa,W>8k7M7ỘϖRŃVdɦ?j>=eb`-싹ϫzwCZT10jyWdOO[vvwQ̚{As׮9'f5V`8s퉈6¶:C Ɲwz[jҏ,Xz*hڰ9AQ[nY"V6gsVыև/6gSqTw[y&hNX:UDlL*ţE \\3V=t%is.8v53{Y\Xz Ugl]ozgZ$Ef:yҍ|b:Y\M8DD<#"R$puRrq{ _K!$۵ɻȽGBzI_j0Hlk%*[oj/奓(@!~S+y u%*Zϖ2mλmsJO"(&&몸V߬ zfRD׀ KN.M1ne˴u) B ָґ~^l yVvi#g1 FG83UJ,%9*!R ]3vGH/)L޲ŧ8Vzs73kޙk cDz)"~\[`L a͂{;]7_3ςGB%e #S7L 6cgB.Ñ61q[qɼ`wqN_La'?֚G#Yz YzSYPy&8gԐ杕ML\c'tb1pȴ~c'vUIHϬl1i؂.5` w XNX///>_:څdf>"HMb̯ !?::>>~.oS$u%,L=OksG<< FaN>+ Q‘56q@ 0"'1p5ީ H!H!H!2U7<(%"m gǖ`u_8n~}ZleB 2͓FMY8ܒ-M?jʾZ|7 U<5ummʪҢ7z&1_ج_-Ms˚mgd)λ˰Q]˭kTCΫ zqXMޥGO' )Vliˋݶ2x|o2_33.o o쭞 u:UG 6xos\ܼDžٜt B&ٔ(>=]V^ >?w,,gZpt=12b+6Qi3LUKe|×v*|Μ[Dz%B*;v^%Mv+4q{/tݒG;eQ_V\գg Bv2bH0sKDEFBVKP#r+ۀ#n޿&_ܴ7zZrCOd/8l>r|_`t˫a+k)H)e :aI-W*diZG9ZoɢPAzjf>2;{ߑa5R]eĤIgߎ.*L:O咊Щ][ Qٷ?eXJ˗IwОrB͖_We tK]LүZ{ԷDSfO7O+Z!p9)ӒH'w,v(jΜ9:ӭܪEܰ?խDlqɓk7jc}K\ J5:u^ci[zYf3Tnx8Lt]m< *4社mԚ zڹB̦8߼n' G̒ψ+ (B?=&hC/Feff١1,[?ccc|ߢV}||P x2@ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ Kt:˲ ? au甝d ?B$PBLÆ j5΅ ܹs"CY]Sccc|>Jj\VAAùxƍDP(߿r0:nݺEDׯ/s1PxСTccc ޅ )))IJJ677= ed|> ++JR) DcCCC333@ZBHRiZrR=")-wd)BB))BB))BB)BB))BB))BB)BB))Bޓ'OP1,ˢ @ @ @ @ @ @  %MRk4t񌍍AOIxn\ֈp\[ E?rzXKPeffT5rRvuZGQ7YβlmhVe-$E`XE*))¢?--M899aqr +4qӹ(0`CT `riL&{y+4G eY.[s/juş7vGMN>Zܝ޹(&MR@?P54vF/ܘɺa ~XOz!qDzZvGO؊:dz3:ciG 12 ʐsCugfd}z@7|zL\]/?+]_ڻGQ+12?("36f#r109XB_JC4nds蜔R9#cQJǚ~/ O7}6vVz [(Eq;X4Q40:}xYkg=Ꜵjw>BBnghxp"PJ?;+Z(bN9ח7dTԀm O2~1*k<_TpLl Uzʵ~y{oʹW78'$!.ݹKC$($IV'bk%Ζ 6VS T(#%c9BgrZ~B(w,hݰt'J9kcG'p_{edʬ? >Svzfc홂/0\8BLH%5}BmBb_[SkŴjl(w-[AOؓ ֬-5FMpjp<$D6wҚ'>yPwR=fs dhDDnf|i!궳PQ*hE=F { z!wN[η.@7,6}ˉbJ>N>!ӒD~UP䲠bԷ8rC;sqRBIcm,<{N>~;#`Y9w_H"9nLu676g~}Šy^ٚ ̖Rfc.:/j¶M#\*..͈=Qme3;XsmQ 8ox*>>1Q rg!v}˕LOƊ #?9OR9OR9OR9OR9OR9OREQ,wv]$ ?"b4H!cJ`0wz^R;VF-˹u^vR/ttt0 ï\V^iYM:c7vDxʱꜴ16gLKGSSlv<6MbwNcc49:R RR RR 㸿Eg}'IENDB`././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1696417899.7531807 murano-16.0.0/doc/source/user/figures/add_to_env/0000775000175000017500000000000000000000000021713 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/doc/source/user/figures/add_to_env/add_component.png0000664000175000017500000022164300000000000025243 0ustar00zuulzuul00000000000000PNG  IHDRP[LsBITOtEXtSoftwareShutterc IDATxyXW/jjL7[Ҩ Q1"EF0 uF0( p3J QaܐDTDT H7҅K?]qLyޱ]s)a8ft:`S6 bX b%n5pԶ\zm2B!Boj2umCp8b\rybsl`l6b,aJ>}q4jWVXlB!x@U@1bVX`tŰt-X5hZ[bBkB!B_gn*\♦&F5Y|hO6ګlcc;%s5|Q9—G\R`-ޤÑn",.B7#ˊ] 9![w#ѯxŲr 6118p >}yС>˳9r$m^;.,6.+CrMM5a7cfN *8v0dvaX|+״4N6& 'L)FFE!x /^K]V(]0JFx]x/y.ݹ<cIC;it~XX,`ҡ2g@_g !3Z(.mtN0$^w<5֦Z#Tֆw-Y ^-)r[E^;5n's \[>x]Qsl`: abnQz0LquuZjyw0)m'N@o&??-xb#G~JJӧ'''GR޹s ,r?y?cZZZffȑ#{FgFè`'Rs寧dbsX:81}ߣҿfY1j5| ]kAougMoTEHz{֊es%8{ٵU t~xr$*fYt ]x0fEܶޙ%"ы":TѨr &o8mz{d$ot$]EJ|]+(4#>zO ֻhYF֤<9!zIwݻN j%Cn_Uؗ. 0O1]z0>\'WOXo|H ݷ:.8՞|}d'4I蚛نF%C9dcgluz033?؀$I>_gmm -`Ȑ!O>U(`ll?2 PӇ|F#Fr ^gijkѷo-5R!sOj O"fU]D %I)i-#A(nBf '{Mj/ MM>#!yQ6#݀|2,.QL/yob ]k<=<*[7UQclea6{R eH ! ʏZ'Uv4"ۑ{\EtѠv{wEFEd7v<-BH(+]Ht^=\ץIe g:؀L%DL]/s5NHNHH.؝jV'(ңAQoɏ ? g<@Ǯߛ6+ WoE|RWLLz?̣yv+Hwn/;֑ N􆬨B!,+*(SMz^]KP %AiyR I"7_mswmd%)iY~wco4 쩃,ƚ 8̏etLKdF]]][322N'(8555'Lvokr%:6[7h8uuVV.K94=cȧ1Si]~KϓSřǺJͪntE^1m*mҲK_;*?KJ>̣,><<"28̻0ZU(atw~CزћBI ӋI/?71b )-+xHϑi7b}>>Ke@; Dv6TJNDη]HKc*f3IBi(j9eb-Ny]B7Op5 1vB F b5k-Zf">`7A^Wa娯N!IX=ϠPPC <Q,eM @: 2)OH03:<<+bmoXm)  }@K3.{PH>B9]]F8 ؒ9 IU+n`lٹ'D"-6C$mk~ckBi^{xZB Pц\.t:v0Z&biuSO}i 33fжa=Z}ogVam'ۓY]QEBRlϝ\ KٺtY EoQsū?!ې""0±k$ EMQPu I<Z.W7 ]8G_yƭqaDhOJѨ&m3`DcEؘPhR`,$_V4E~2nűA[rўᡋ/TX?cR4f ᪠i3n=TB$&5M"ȶ3!@(ڮ>Y2Q@U7Ȧ^ǍhJFA9[ݨ)hOÒ!*`ۥFx{MRb{πBoңP}C\Mn[tc#5AI^Y5m7VzjH`l r5ؚ\RL7R$=W8'mu[fVY%-)QA.ZR hkmː6d=d7zʔI^-yh\Mڑ)ӝ =:xH h }I|?/` Ram]Ю?Zi3R{4H\끖ZTޡ$o8)m˱\`mm5_drrhioz菈.lwpsR rpDjy%W^1C:O`ИG4*hcECݨZ.4ODm iZ.򬤃LJ6y&t$Đ]Ƴ @8 v]1)[Иg(SBFnQt] IL(E#e@=9-mwK^!7N nRLM~v ŴBo6K=Rq=OtB {Nzvih >Tc#bxҔ $Х >T}0)kRdm߸{=8wz{-I޸1 T> SA_% Ѹq=饾r'K-3v^kC@Yw}mDA'l=CX,k ,};aͪb,1 l6&} J7uuu XZ>I+//־G~+?_Ȉ atZ]֟~eуIʥnJ O?mp\Q68=_tii'aL͓ҬԋtOWe-g/;"=*?=v!ld42ryVl|GHưݥSdq7OyQP 'Q䡠29-r v|CdQ w>z#!ψt$|BVdw} =5RTYzG`90kk{DbG_7 >z} GH@þ#ɃIVǷ~mɾ}dqD,*j H;0 ~BwgL4Xғ Y8|Ӈ餥M~q5}KvK7¨{*drp\žp Ad.) Lֆp~&xLDD΋CVgs`~ĦsHͤ>vY/nT[XԤ{Z7:yko_,\21 h/޳`hrt\hhh:Z04SRhii.xZgH6c=OfˌÇH$666+Vx۷oJĤbZXXgSWWMLLjkkg͚5vت]v5j޼yh"""dIYɢeo ڭm``qVUk7xC oZn4sꛯc2iI{O\3o~ V_ݻ$I4<\OOO!)ښj-Ay Ƹ1?,"Fe}~w2h4YS-ߥ9 grؙ,Ya8`Yd쾺 z#/rVuK^Hi`L;!~I"n~Ktapo1].]|(G=WHnondf :V*Z *jAMsI6|V( ݬ l2`30;;;??^!%4=*zw|#׻PB9yvF51Me+klvKG6۠€tZXe>!B!Կ-,ƮEaZt:aga7 zOJj-B!P߼Eb6}'24䘼|>886\Ggo#7B!BɸF-6)`q,#cCC6AB!BojA!BEB!B!BxB!BB!BaA!BW}B!Bx,--ߟ~a ô}t:Nժw}!B10hZV<B!k~0 jB!B-p8W9t{B!B&N0 B!ź[跢:N6!Bô~3m GÃB!0 B!B!,B!B!ai[łB!0 B!B!B!B!BxB!BB!BB?zsiZZ(B!Pl6Al pX* &tZV4R***&Nا`B?23iCo0ZB!GFt+`Ao0ɆB! B <!B?( <!B! <!B!!B!0 B!B!B!BxB!BxB!BM"@0IƟx- \MԾ IDAT&gdwo\t m/ j/N ?fJw%|CJ)?mݦuV=㧿Ù}h{/d:wD,Y{Ei .whܲ_|+47Wg鸐a\-!uN/JYrXgaif~)T]F~h{o zW}uzUA/؉l+=CX~Qlx?y&Bk{s?]ʭÒ-p$=(|{c})%\Eф$C&!z G}w=-00wh˾c6 _ڷT6U `@;uR'dkRrܧԝ_(e|kR\FG/&(OY&g|эkR\N:ݶO]MK#@4?4|;PwMF6Emv'r&!O.fWr̲NmB9ߕdrb؈@SrdL uᛦvMLdy/'UNʧ&MG +wD 1?4*2 mJ/|^Bȸ;r!tȰ](R$l̍t;RRUQpq+,"y7?3O(PpYVhԜ[GFJ%BiyO3m&Bͳ*W} J Spdh{~ h@Q|,G`b2Eh-id[JM3 |S5 Ѓ%slF=g4pǺlD+='„Ͽs"YP(4 ~W[>/Gi|2!-s>-` O*߲Ī‘W+|%>B^Mwƾ',YP(=? => ΃Z `tmv^Im0Kʒ 'N_h _<5O-=).pVP|Vy}0?Ч}9EblGJA!ԓWc9YE7< W.[A_GKȯe]l rS#MlU- ~O͎ڻٲȴr+{17b.B ^zo8O I/2Q:=o?\II%pLJX ~uܙ3w%_K{MĔ*(O;I:wܹ\&ÛM^ߕg8ܧ$Py|$͖jvTB.x{d7sbRϹsg$΍ ddboΝ9:,'MJw3f jw9l6}f8ߕ '3xFdЖ"xPSء~CEGi9Zǣ ͉?ot?>y]z>u ej?y Mboc7x]j?e*w@æQ%,?.E&~CgU~tхf ^WS[X>o[G((׊WnC oԗԎÖ 9t 2C?*x\T܄)Am늌 \.4W<>o9raO4 5ʂCm ߺMTUrG>蓿}2΅o0tx;oKO*AӔ0pp6ǴBzXO`5)zpE4F:8^vnjXfpfk.?1ASNeݫmpN3-r5h*رfdܬENʔO4p!WV~a!mI3)5>tONܟ4LO":{ErJda|xo qRTsMjz !7JVʼn(u-W*^0I|۩  &[ I"HϟN2{ñZN9Bh:;O-0FO*aݝW+Uk0>V܂1 ('IS;v]d*)H漸٪|vvQXc@goHLGX ݍ0B'm {<*6Ui 5i%gQ2p>p m#Q蠡z,!p*,±FB3) Z0J(8So.OCߟ' &|Jj<#H1y6P~ywyPeiWR5EmYiǞS>eue_Imq㌕+*W.hZ;%Px#P\>|:7R.He..!OY>O|Ҋk @֘B-vOPr&ZY=w%!KiqDS|H%[o:=* ;)#GZː&iA~OIȥrvyEw}}GN-Øyr9Mh=>iE(i"[#fմimz1߉vR # 9 h%ϴj>usGDG+$պS ;u4M7}ȝ#-K[u&[Y}uExU pw٨s<~79<\.ipU"dګ ܩ}Yv lr%\.mZYC]K_4sy[uWw~qBe4U\8/?+NB14+r9jn `$1h3seES^frܻ#]|DC)sgH˝rh@0y<'h&CC@ah-_^Is@roN;-+Xw̲։8U'VI|hRTT$IД MȮ3+HIP-ۂ"m锐S9 S}I[n킖yb6=(zzr#tz.Z衽nOǥֽU 6MBe{,僪(znp(y"kaH"hL[v7M)-Pټ?o {ŏ[O !=>"XԍilW%Ab߰/zBUSHW8#3v>8YWT),y`hPw[Mp5TkIԖ@`}kʆu|Ҝcд0P>((88gh; Xe!Pmo}ZE@U@/ M\!!!!Ohju<؍3N+몀܎31#M\M`5yFJ~b<;JPϕK^_W7h{wIV`l{kuP]̚;u"DUUNy-F@SVKd'.R‰Rr{nʟWW~h͂1aZdp*[cb1iB,TߧܠZ*JΫ%ORrnȥFL! LjesT]9&Zsvɀw{p¡2R d%k{4y_>c#u۪fVרZVXg9W{s_!LR^b`„fx}S,xj«[C'XQK{PTAӬl@ϐ;yLAWk BGcӚ\„m[FLZ%-(kBA(Nl\ Y &<"qc*יw w~vO#ɶ2%R/B<:dyT&'lo P@O}dh򦨩]dN 0cGEM0%Ƈ,(^iiiHbpdϦƓa!sڶucUasgx N|]tqdȇhBVynhrzg>Kddr; pG9Y_:-?lYۋ:-=r&!2 \\!cU9Ed3pG,q$#|Qw?k7MG8oyOOj 7' KRRs>[nױR?>JSgx IIrz.(*7̶yXjĘTwPs>RvSYߏ?STv>%Rǡ,?M {0NjZiJUQQ1qėgϞ#3Sբwn:0!BQ/e2; $Js N^0w& -JݳcOq"0q]9\ =`OJR2na,[uw@cQ9g̉ȩO_%I0 Bӧ!mv>~U[r&v3niaw M,tr|z7m Y~$))X<B_TLIɭ5p80 iC!BodH[wւP//kL;!B_1X!B_ !B!!B!0 B!B!P_6~B!ANxsB?4 0-!Bf)ŒBozp8X!B\EB!B!BxB!BB!B?B!Bx?a)0:NjZU*UEE B!zpHB!BB!BaA!B! <!B!!B!0 B!B!B!B!BxB!BB!B׸X!BB 95uuXG(0l 9B!^=픭gT*6¶50M}ƅCw}y!mCBBBB?o4ԧlhHgC!BʓclbWA?r9ƺڃz_UO͍Z ~u+W\.;B!|>C&ʼz_9U]s^1ϖjNvx}typܬsE w|J=r+s@Crc+/%=w+t XI_R7y B dX\I BQ 0wy7TȯO-rEuE[q4f(uGʽ3kԴ]D-+!\]s05 wKCs;&?58,O.7nܸq㼣並8B!~ }QW^W8 4iXCʮMw97\P~}{vwg+ rcԔ{% $w"7o ɽRYS oxg`r~X椹~ 9E}ۆ++{QʂS/+j(/y>ޣ̰BoQKsn74gjYTSda -< ,@$ xC.Fu/k@3KuUOLzG`o:JIa=G6y]mCNͳ7(:.%w >ABwxrA8w@ss5=@`P(`S++X;w;yFH;Hx5.V~U0)YToӯU|l'zMrle uU%/ePO$F!~w,c<'/Vd˹0r9T^I˱>?@3cF9+#Z{,)x/{֖c:%|Wb&彯6qK;jҪX!/-h*˹pgqB9}٬B̓Rc<1 ~G孯ҫqw[Ǵ 4ẑJ,jBRqQ0NjZiJUQQ1q>7SZ`IکW?o֎r8t.*H.^K{xGgQĔ6$ۏOZ5]:-L}SҶ緶gKP#2TA 02*kwW$嶙@Z9rY2=/MAiO]52Hv* 05q_>^bki(oMr}aV#'K&LaQ:呋%u4}Vn *e:]PPRE-G0rҼ+gty2cgmpL3xrԗ_j.SeüQg7ّn?~/SVu{g?PKR6 lj<!GP2f9 6zJ[s1h홟=nalϲ泇ty m4aPNWOhz̯Q^OjzY/![4,رjݑn_<v1rW-6{-E9}-֩$w5P sz*3"6wZ6KmOtJUç ҿXyvh?/ ee֥ae#(r]Lڼ*u%]\ur_׸$;:hsjIDžt]/rJxb>';hZyofu%Sc.g^:z&*:nUZEӭQSi!Pi=@3mo+ߞ}Ylys~y~\N+Q}<zC~tvGw`SSoSP7/-b=9|EIoyqX[߲ZH\z#X5㕗"ZJ_Q֦uSo^Gjin'nL^KMe%eխ̯\,L3)[GHd-P)]˂뀾]ZNK:EIJҶ#1sĭ32)-V_3,Tܬ1Gzl!^DWAL# b&< F|j[ y5Wb %/=p,gNAV<-*5 vU3O&`lp1π+/ݼi*ROCHHޚ<0>)cba}BECۧCiV_̸:UoKC-},+ j?V-D0[8a{so?=GExwj#yoBS%%ͬ,)N:yqY2EZ[u%0%sxrt1ۏWuig F}m:{vs>.Ԓ%+/w?jeyulF|E浼dUA|?+KMEQ𨥝\ಲX2P;uӆuKf(ߨ=GBQ15p``}5ۊǜ֯k/4[3m Y5džz=c24yƆ^Ù6?ZtVn306psJ5hod4684wqM /%+Al(`+B%`gܭyܩBwF~ ڟ0H]`f5VvLCZ Z BGTVy9܇g߉^F} @=8>x{^4 X6 ޯ?$x>}9Int[x&ҼeyҖXo12]FiRZfn{b4XUm;"MFWeٚw[,5Hҧj\DzUD,( x>rߟX \իcmjN/K=p'iBVbV&6{!Vqp<?8;Xa=E:Fzhh;>whH*`upq?=\O} ꒆx#Jm<.$*! ""8-57I?g5/P\Va3LZLFgkc(2SVg'iNL<͆њH)X iSjN0 TOX}<$iBzhjQ_a˖*>|VH<)2pLPIHz;\}bP; 9qްmNz M_o514lr*̹7_@SBiuy*eTW>3NODDt#tq֕mK.,Ug]T"VSe54^s50qOZ2Z\h4mvcC/VqκumJv4  IݘMR8͆bax%_L儝$y lƊMƊak_VpUM*1=Dze]mQfɺZ_gD[O%""qҥKY t]\pySNݘԥ+*"MF58 F[XGPc47\ D8UB.3#]maLSuyhUN"Sk4+2Ӯl DˌVH$.x5-6gsS%xurF(k""¦MLcԄO%樝zYR4qFaϒɢzwN}$MvEDDDDDc&E=;/,B ssrDe]|MI3}g], d6m#i%~5٥_.g6""""إxxxxxxF< ?l? {Gm>.k /ۆ-'"""""v`׮}: bYEaˉռw?5 +4a]F+7V?o4M[2k_᳗e?(|q~‡Y/=];ON_WHzM:bi^(|":w‹- """"ήXvӹV8+sswzݧPƶYLJwszpif>=7? ^wMC]v9W~l1^O-HDDDD7C9t L ͂./k)j9p,xqq>t;a_%X0 C =X s!V1mQ35LG?L,f޹3G{k6\u_geM$Wإu T)X)]oⅷu "MFD\vS@o4pt EѼnBϡ|k٫F@tX&DDDDtSa4vݷ|zЭX2>f#ߨ^5o5}h ُ3L#'vh.mKj-MX<p8]-]}rքBCmG%,<ϊ<`!""",Z6G>M4 GzpNέej?EJĜiכ A<]I^8L?]y1{AM… ======>:uJѰdjL#:}!nL[\"""""y}fsGgrc"""""|7H6K]kYDDDDDCDDDDDCDDDDDCDDDDDCDDDDDCDDDDDCDDDDD _oo/K2GGG(hBgdɓ\ynW_}u3MuDJRqJ2EADDD X%vZwU՟ sm}OC]gg/0}S?B08>ߪNtJޓ˗*:zW f~Gꊥwȅɽ{T-vؽd7 """"x[x-NP >?̜ݿ褥3bƒ&qٽ8v Fzz0E]9'p?j 2Z&ASlw;U 2vvTAk;0; p OۖhaRҒ"8q=A Μt-?"M1^)wNeQэx^o"d9}cgS G#G|w['Hd;ꖃ{Z=zcO Yxddv5ڄO`vŝƃE3>mvlc@pԅ`!"""9t^D%=?Goӓec/:HYv.KU_/ SVd.A}՟X2DDDD4an}rw, Swr³p6 nw]5#>1${?p{{ 'ߝ)w־~cHh3ўSGw(6ރ1@=o>5IQzww70%vᣏkcx.ޑ-O55aŏ7@u|nM \x nR?oQp^C+6C>Uqf~SY G~AtWZy(zgάlrfGf~,+G헽5"""&qu4֣?HP̔_Ǐ3g8_zy3i=S&~6?*+)RA_9E$'\82qbey&GOu/#}[pG}>3Pz} bfSYF;7m'-m˼̻#--n:k/ݝuw[0Ol֒B|VEmci#+3)#KX[[;(TcnҸGzdy_ǣ|r͍Ǟyv ~FwHѦ#yO[w?sDS?L`] 9q{G-Nfc]C|m~,Kպ;.%cΦ̺S5/=-9/G=';zg7ޯ~}y8|f 6LFddƣs3f :k.Y:_ _џh<5\7Utgvhj}m(MW~h֦|!shO'D߃I3^Ȣ}f-'o728 $I:, Qw7:/t|:/ $+DƜ$$jFCYm-C'}W~`{3fcBg"ʅpL7ԈnVig}nt.wWDTee@WG0.ApJ,vu3"B4*"~/ץev|-pˏ***s=y̩)S-zM(=g>Z.zzڼ@x0 łd : GD@6]?ww[catQCzt&EOnbjw{red4-r1Ɂݓ$#}jDDDt8n)ypޒhҲ0¨ضzˆwOKϙpy8'.?v^sEӕ g?)3fF\B#ڏ{Κ'dV̝. ?7:lTN5ڏԈ -b,\ܙ55"=`ʒgTϺHy.s.cw_do.\=QF=Jw?YD/X 1(G*w ^x)7Q -ّ䓊WVB(੬oy㫕L1}j `>2߾j\0iY;@X~Ξ;f@ [ wʶ/,L]Wh>U/o䎹/}U"L|o;3,j޻({ߺWtxXK;|_EO{>=:9/_|xJE4S#""+sCt5\\ug˿~)7XQ:Iƒ:ckOh"Z"ೆg܄&>5""]ڈhBl]L|yz<5"""RFˍץ#""[{Sѵ%8z0K&qMZ@DDDDttR].\|^ԩS,""""إxxxxx%HXWlǏg9эnΜ9 Mغ?sW>:Hݻh_ ;҈^.Zs(gg Ep.B1;.6ܿ.Fόj7_qz}Mח~6ZoreW/=?JԊm炁;HcEH#]hkaڙ-ߨ^\&V(7G?DDDDDZx,bNSWMIgh:~nw}mQP!kl'sn]-'[}7< m`P<;+^ókޗ> 8p fӦOsˆƾ{_H 9&<ߣ=wM'.bwz M#O,YǏq_6v45*xbڗ}|]ɑOJC-DͿKuC ZuN[VjgnCh>rX㡦Nؙ@= w0 .BYf`jzowttEDE o~+Ʊ-v…`|^ԩS%CDDDDW 7_:+]b2iӮ,Je!""""!D?ͷl= +-_^;"""""K݄إ5NwFDDDDWkmm=w\ww7K.)<<|ڴi111"XΕ'_L2%66V,@蒼^⋹s^-} ǣ̙qGvuu;wsaӎ@ 4i .)""B"={UP0фFEE]jWUΝ;7sLXc)JymOTRVn47{d ڬLE[ƒ:UQT65Zj=K F;}N>q1#wJ%Fd*mNA>QTLruz6e/[gj;T%[ʍHN;VKuYy_F Ce[U0l^툛L sĩA \rݓeNي|u%e&@"Wk,OĭU迪&-Tzd]Fnw-'Tz )ױe&㨯Pej4jʫHq"qrG;j27=$N/{MfKZo4(L,lWmӳE.[}]Ÿeu:[9vQmD)cBD;4a6ZWDd[bs/E"T"Y*sY Fx[T$߾(K_jY ]'Im)[]\NLg)e,uū;*lU[>@Ҥ(ebj,[g]/b㱛 WY䩺T9cuٰ>!-1ڰq՚йiRbm;eLJv|8i.,3JSk"X]g+^Um) KH7֙=%$beak\r,chZ[#ڼ$N `册]7yk]&`We߇XbZGu&"uAkU+lMRեPkYrPg?\^ޘ1h)MvP&jTXZګd[Ņ՚P-I,|5)/%C1oZ'I\U:5FW]5D->g]tmUU_N䚺fCquFei-V$)/WU\_gZ,!W]=cGm~33Rq՗Z*lM@Bss7e+TnA\fN߼kc4S,kl7uJ:3_h3[!O-쫵;jK\@B6K${ZVbѕ&*.(ߞФϏ)k3o+G|Dy&,"[ 4}H:S+\ᓵZEhb2q?UI.'ExLն˚m^d+ rAvmZ& "ev6lޘ>{z:3UVo({u@\3 %\nz~q9CASc T*D o7YƆAR_#`4ڇ^켁fh&ٛ\C%I/|8j#X4T; Tc%tR*M-`8neZM1]8 +/\/d91f{]6Yb+vT$2r'NSk2S%ZO\]PXk(JK1钓Vk~6=?RY]+mh=EҲ4GkCy~fu7VfZt%|]RRr^o3OnON6ݯ`I+9=#o~=ӧ}a&][}O󳼈h?֊Z d ='d<yZ;t,"Yؾzh*Y got^΁M>@U 9FmiOuCHԦt#Vp8`G &#VElղ'+؞1zNq[l$}j @yHĒ$&>aICJ}}:^i{8MUzX#iB j:,L r .uD'Ea8dbisw+-%*7 >8"QT0c٧LUBy喁;J$`-)jؿksnb7C=n}UmPk֯.9^M*\;vY]j^KqVM]vi}uV7z!ϭܑ%TU+MTzI^s;U;kV#l*]UEzT+1ƢUkʬҪwٵ@+^ї]Zik;vk6K#'KPKݯ`(=;1&1S/\D#+s}$?Q\ǎgg*Kb.mtqM@=813]^S4t=TWɆUE2a zʥ಻nas= ٥Nhi|jđ;)dhu:nB:p8aP^8ыbY pL2|O,r#N4N>|5\ x|. Vzg v}X8lbpL:܂~UoX[jZfyHhZ6Yɬd7Vڠ~9E@ψ3U4&=> 8oBW[c`NVͭ0n0=qEiJPn,rnP$VVtah&ƋG$J,lޜP 6+ emiR։ 2cp/sy\YS(S-ٰl{]'`GTyVXn/Ihm0}DlgPPg( #~6m0==}ƍoxqʼ%3&KfΈAd vLsz0|1"1 j*7xWevh#%>oPEzϢ.'.:qt3mDxL"1o)s,|`ȳI@ۣT_x+olڝO;w`A0ZPM<8lh)eD vwMMqB^n0&bj6X*HU^_.yyRq-?G [UQl0:t"sUj&uZ Uyztôsk & "B<_` {;ɓ']]x f'Cu[u9.ˋ!/:<d!\6҆z܎V`YՅe*.mķzb e `uZ\- L%\6|\0#FWjI;TXƒjD)EzXTpY{0Rvn W8zeJ> cw;>qX`OoT'jy{\ovV[rKo㶔<7ufY.6T7eߡ^P3_pxs=E"6q$n IDATo/]BuN(6F$-;pK݃ L:WdiJd*Ѿs~W\ HЫ]9~w0X,[ZؔRXOUDO|[L<==TeqMk3:10yrFd1K^9ؿ~fa*yH}ijp)m$>r*bV%1k-nݦe̐g͗gvj5n7@OLFi2O+zټjRdZ&Y)XouPe_/zyv m1l-k[D"yFlK4K#nܼ"{Mb2Z/{ؼ!uV4JneU %bs R bzmiθ+1i̫*V-Si<2p8=udPmۘSnZSg)^dJKDl7߲;GpŭeD8AMZ5r -e*}CQ~ѽ ƢfD2u Q+*7ˊKK׬ @r Ϧ5+ K.,͐T;,e[Vf*49q6WVUfT[d*Mkybbu_aUF@^(ǰrK-ÎdeW?Y @xys6bTB}+ "b! FLG=⯾*C'3rH6]biY1أJ%lܩRj0>2UJznAxKvJ4WVR9甙vd359Zؽ1Ôo6bUe5VX\V$A=iEUJl}j֋R-ƊKc Uj.3#]=|gʪˍ z#Hԩu!tV^X sR_Z޾b6Zw(yh7VUi dm"Y&53;G%3)EkNSiʝ f1gRR3ro{1VDh:yGUSoviH i * ^+ OീzAEl"X ^6= !bPQ"y=erxsww:b:x{3)vL9}Uhyr-x;evhɲ7- -},;ny[%l = \!Φ~}:>݊ѿ-Y67J[%C)D7"\}#+Əyo7fꎯҧ}7fjn9D W͟$ 7Uoo|?*xrss{q󲽰7 Cz $I, @hg]f~ۋ4cO76?fIʹ}Ѽdx@ M!@ X'wx5>l$uE#g9$K@ @ @ @-T*'v /~_R]I@ $Z}Y]+**JOO'lS΋Jz'@  d/&xbn5g\.b哤@ l(|.qg۝NgRR_qIIIEEE${PT&)))㸋Q> i#gn_ `b\@\/ed@ Ÿ8z1 B{S:%ߘƪ*B{Ќѩpmv2sI=&{o4Oql/r.c"@ BÕp+KO Uns^*CFx.\> z-t FJ pʝ3Wqfu˹kf~K#y๬g mX--9K";)y>S9n!=wJst>V}tMcU6.ݸP9sByiӄ١\RF=誷n۬ߖ'/7 ՂGS~:=2K7@Em;ɽT];Lw ~om{=3(vWbe9<@J ~Ǣ n[4ii2񥗎oo I^IZ iAOop:^W6+&k-g3;.ꆘ/vX;v~.X+^r{_%~6<tӣK_/yN{O?9ŮwX71k| YeG/.=oq̵IgC޼Q_no¬$a68@  P}=8UKsaD\*'|> JʺE @$!w0nV}꛳cENT<걷՛U`j`*2z \~@4"3i.; PdT rΞnɾSq pI83 Ο 6]4X`z?,q5@ Be祀]9<@sh"}jgL>n?8*4%yJwLi73:y5ߨ 5 财k sru %~*㸡!e=@ ;sWn*v~\cN%a⯻zrKe[{XGޘz7xJ_8$ ./£.2 >XHhz=xy?$¿:Ÿv<0 BݩEQBn_ Π1sbnּ\!@Ȳ0 CHP^xxxxx@оp晱xեZ0B=#^ ^ ^ ^ ^ ^ ^ܼpYӈy "x@ A@ @ @ @ !@ @h7Y2*(b4exU痞xx@h(r-_bPɻB WTT|>ZѨE)((2D@ 훂ܱcGdzCc5U]v5LŊ]HHЎ%!m@  \Ю]n;_&2pfXݐtvBZ@ @h8eu:No\2!DQ%i1ICf)x^l&$@ .SǶ\ʅւDo}4 `N!.7dYɨ5I*o'_ٰY.ԑe9**+Z[WT-q b_K*^jn!֐ 1&<p9SNwEfA{1<% lyf[1g5m@,)M. 4Grojӫp TݤgG1験'OL&JE41<Lz7s7FAXVb_y^/ǫ"sxYIl;0K~V^KV/CtLb\WkB0z)ux'\: TIa d;}!i:::bεMARGVc`6*Q/~㎑ƦzȿIYEc$pA ]!EQ4M 0ƒ$ P^^NϯA{+'Cپd9>x#tuDwi9~Bf aCuuq?nA&!wAXSYng[}GҰJ 4>UR^0b6|}vɫ0_kZ2Oav Gh"|w> vԹH&_< RCH 5'ݯC.@rͳRx什y;sCIq%CFyZ1<X %كUA˓שeDIZ/"'` #:NpA%Ye_"sIʂMFFR 8MOvxE 3ti j'ASVq34 /wzGP?=t z]Ǥ&MWQifgqcr[BAIO:!e"0>4EFc =M h<0C"0 je}CQY翰gU<ɳr`4car!пj)Vdy MOzbs6K|~/Ժ{c,b@a2vF u):/pVQ';!zMx$6&"TJw0WV++ {ոoLy|~'4X uBv) `"f!-+^j޼9ٽgr=4dž·Mv1+j)T

{OفZЦ\lpVqM$xu R~*o}:Dž>.@h#eY,ݼvV.eSS oJ)D(C҉]/o`w{}%;O(OOf(Y&5EEw'"0eao] JQ0*%,K9/FM۵7xk)ӨӁ0%__4A,MQkp͖D*MQpV2׫#tWV TOMZ7b٬NlXvtMCxzwSfN'<<4S-@KaȝFLL@ Ǖa?hO>vwW.cOZqGk<^eڊ[ ( B/Kh.>iY<RSS4ws$y)Iq@a'{v'D*rx!1WV7>|W><誏ǔ{NSnM2y+ҏُ޸^_ Y`OŠ*oU;|^ƣox W&7o,ߘ2δ#uj j€0ƀ${iOd4$D1D|f]R(+ ÝUf9)(qaџLoVc^~8X$ި`1pu&0f|*l`mPlkr!{ X !G`Y7׾4-;L,NU&K 2C daS]54%)y][Tڗ 8@NE[$#jF5ZNr]wAIsj hP(/! LQ`@ВA{Pl"kd IDAT7u@n.}וl쨨:QnFSh2GtWT>v:Xw5:qEbg_pxvsqȞW"i Oͽ-c47Λݍ}u&r\C2C .jUtLt=!yEb~̡˄^];@SVڔ_o1BBMQMSEQI`%C4#1Ŗj,p)C_ A/K?Vs]'v!Z3*" Ma7ji-`y^x!];o'wLQ `)X@NwT*3Ve )Su\l-o90@g{=kՒۆ(ZŠLׅ (a#h0 EEWhB䆪FNǜ4,(o0.Y:aѡ G$I:X /R9s̻8u`'ޠSH"L 5}S]&4D;i6xQŝ E)By!1tf6Ij'Y-Ol;ݿ/"Jb@DjY.8v;,"@ϡ-Fg4p:q,ô ?Ig_T%xX!ЗrpL4K``h'{]ڽ7mi%!P~/wݭUsrdp2.q\N`B~ʏSBw PxkJ6.>1~k<^k dY:ljSE|M`,"{}s2P68GR'<cvszGD\&MXdkSc=896. 2^?\aےLb' n^tګ {NZ45Ej9eA J>n sXqkc F;FJ!w%>]ڈ@2Rj3!cxU>>/ 5Ef5|^y~+I)t/q-'nrSjh̎3%[޺YU HcN9g!KR:5\ճ V,"f ̀,xB+bʧKrPVVyL:Sw˜یع-11( D wKL aNؤsIDSRR;!+XC4fNAzGۭR% TJuݩ؛pERtXN)u+X2!>A(˺5%pdbT!5LqSD3,sZyaqodϹz}RO#=ё%TV~d 1Ƹd9pWUŞ cӒ;;޹QqVP" ^DOo&dn`iOc=eJ8V]-ML#*=灠q5kYxpUc5*BvMn:0*iT>q1&LJVu+7v0t?vK[n6\l[UV&>:mApW Q\LƘ xÁY[Uc*Q"ZSL{B/ĥc>HRUzD$Ɯ{[)/,?vphZs04K=b+ca޼[\Gx  01Z\dbe˜i#uw*zZ-y!1!CB!z`EQa&kCqN^~ƒ@B')) J.iϏWp%wȩQQjj%,RRN\ȤTLzɬhX==iiNI됸k W 4j:{6]~}+?l;nGVhnv?@ypu !;1ݼG^zx-*SS6Hav8k*j98{b~Guc͖AzA!@a1gߜPU(x$Ͻ-S^\#y*>`V,$ YRz1&U WF@8| _g] w:vvcqFFE^T $&&.m7"p-;0qѴuòTטӉƆ2YPp7ŌYE~v]"I&๢ =VpSR1f kN)J)O3pڎ7D9}zBT u#W)۷`tLmmPt4竍m/F#]$Ьa9o&qG?7Էg7:?lyL-1.$&*Bt8݀(>i' FEXJ*Qi,'1F1\N Vvi;-x oa$9Zd#(*lBSHCFIX# ^פUӠb 5'Q~d9Vatu8FhCb§udKfGwV]gٹѠߛ|4{S D[b" sϰĐ A"k_Ab"M\P-9FZkACrH}3􊵇CR{7sH`E%Ϯ}Ve1'?#FYoN$,أU祅4K}AGsk>L6oîj3w|lccƄÉZ:euMbZcW{}МE='Q;g!2EQD <FA)GSm a?w~cBH`h`h40C6/G!tLL&Vsdmg{_%.{OzZ9&3ouãY~ނuIOfgy">>xdF62]V54Gq,0NĚ}9^sRj12;mTm?vt $S؇(%ظZNxTv%Y8xN"zZM3 Y7\AYRk6W544vT#DQ(tU  X559b?e:G1/̺D'ZNw韌&w\n&k&;:MMSZ=ܹ=NaTM>^Z f5%9N5ttۖ:hLf>(ǒN,K[qpxUD4dn>sn#oۉ8D{*`kZD9KJZ31Ƥ&JC:O{&>p+SgK6҇$3쳩t37|9lAזZ1M]TnըT};zF' 3 f6mԎ( ؂B@QjQc- ^BMnᕀO'6UY[pn;B"rXRLF)ɜv֧Ϋ5uKMmWr4ĩp=u[6.]ϔḡQ>^m%%}O =AK @QQY(*?LaFūt:DI`qY( }(:Xam!  QJEf!chEQo^9ΔcirSLh|N(.9#2)I<͒<zYV,[{}Dl5Ƿ `uWkNs,D =&ݰkHkAh]~rF gw>pİm"2ưfդ$`Ajj壖6 c.>np)Gcӓj'mގ :kI"xhXC|!kgŷj7˧ҫ0'''T6u(d=Q3 %<W/=)\XbI`Eq: .[фY,u>PtV+ xdIZZniE `L8{hB;&H3EXj~@ G~DڊCM7 %E Ev2VՅX;0n*謫I+6覸́7 PRб؍o> 'I_xvxߏ1mۛFrpP~04%h݀YCCiVž'LՃ\p#BAE2ѽ#mPaYyEtT.s"򒳯>iyREWݕ}(̄W7uopDQe;J ZǭB10ח6֧32Bt{cԍ aLSA|C&DG嗊gwx2Vp&U[CS@SQG;8J; .%'O3zAmK%&4h=GeJyևsnT|'}2mBXk0eYMUf҇-_ :nR&ؗ?I$Pן J!pᲊEEE' b0BJe4kjjAС`RQc##t: d4EA)v={cccu:olh(*.ƊlX IH}~ q}7…PS(BPtoH8Ҙ0(T_5GCiNyOSma^p*st7}^9"^g(:u3,Z8mk~;ŒeYөw}2Mh7 闓eSnqqV▌0׶)*cZ9ѫأl=#c Z mL&ӱ yOF)TOo{d]K)!Aa Iƽ:p ͇ݢ ŰPm1+WFej֭AuRZs&O=34d7c/w39-GȀH&݋ךkU|֮efaW櫯{du%߲eK~~'Izgg?=g`|[`7o^VVxqoBVnӧOG{.<<ٹĉ}0@2339#|AdNVF{är5ɂT+XhIcp:/B bs; ` E%᩽eޡ奀BBQ?r] [&a75}O3{X0*PZN ݯGNqZ#AM fZi b@E;z񟦩/7:dz ƀkUTӉp!%<1`GBh8Y)<;\]q&sx!\8;lO$v}:{;vXf?-))t&Nxʵkv7~q#""<;+Wk +**9/͡˗ٽ=\׌ɬD.>YY5VW~b>4REۑiCnܹx*Dwdx7vVe_KӇݸ}NJpM( T[tbck_=cQm{Նмo7j2bgB{㸱W{VU \|ahjj uO;; ewG wuq) ؿY[.X崻M/iB_aaD8V.Ӳ2O@LJ3 8IKh IDAT0mBᏫ%oyؽڱ!/@n4vvN 3Æ 1yѣG7n;TUW<(O<)M@.]Z.dY-[EaYرc@cLpA|~51*;zM2st1ey5 !:Aw/<㪲[{E 1zlMb4d Kx5n{Z'EAu:Ps.1jw²|*sՍldr:V/+ɏkz/{} `ۑ@C LN*?{!x̓4isּACxdYnj?=),z7)TC/뫓NmW%Б#v#6.؉4E}~}@h# SFHA_zaC8DA c,cH+@!ͲzQeeeeQh5*Մ[o޽JF4<."\.Æ%E30Y QOutc&3Gf Ўx魥5uu=8wGڟ'<ϻn;|ۂs-?d{M7w妏֏Y$v_@)\lf(ˠgc, 2~={M<:?P,mb򅟞lkع,&@ lAmȒXe>B0j?|Pyi)$֭Y7Ԫ]32KJdIk~Gcr2O>} a(( P9}TU<)5UE{>? gYɊA8{KAᬏ$ɭ?#"`%@ o9/|~?~t" -WˇQӳw8u*TO%F&=)T;v|ߛ[E ~Fcj4]XX.sR99~7\²# U<!\jg䣏]nڰ=.1|xRR944**{nfa I~HQԤIF_w]Ee]vޝǫT:3Q[dIFFƿ<:::di DDGO:voܰut:]rr2 fkaҕ;sa zMI{yyFn' =ڒQa=H//>UTzV7~;K>:koLTsgJ+#,:&/&5a)tG[4MgD؆1owp}cSR\lf4^r$;j2t?rTqIb\LPSH#+OtLL2]U[[;Kԃ^ZdcQ;^m|z=tSS UlE|3sIJ7fEEEOiR'<<55υ!peqI9a6EQ/-xIVj ^vY]nL8[VhLHHjV?d R%'""2byWizy>22j+ee>bİ,7^׋**~jWҊ'Gx^8,ȲB @AؽKU##.:? uhj6@|td5NKM`:&l2 zzɲJ Zysx_y!$rLT{/?R_z݆97cpy? \nв,YwOzHs?p0 Y\ ihh}p9pΗG6jdP}; lR+*~XݒY7썍&="j"xDN4bL&ES4,!EeÁhZݺw7 P1 Bi!5 Ƙ(CÃcc|BiÂJó{r[{bp{ee|Eh2ZIH~~q/$5@rmo>Tpy2[hTe ˟~pz.1ƛ+pwܾmϾ+?ҷϴ7kTzQap72=K+*|Ȉ9M>˺t?)Y!+ ͹-pXcΕSG뤛׋6je .#(N?e.B6KijlhDy!~h5a6Ai ?NеKF~Puҥ1 @3geb31k"=0ƣXong^gy{m(ߺc0[PU[z-?5 BO^z{`N6ٴ7pwM0wOI0 #54~a]wu0[M]߮w͈n@Q7!@7EQXP}}O&v̈q,*FYHg\>6EӮ֦0o|FMV00BB_}~㞫ZYaţ75a&(e/"~*gϞ$q<ϑ7 }oJ 1޾'F옔p)=.:[΀!1! 1@\yMLWVDKlE``+FJ&, e9~p M|9s.Ϲ9$Mi9d"+ݜ]F1}eCVk-{矏[φw~9/K^SVygJʸuVAWEvġ_vMTvһv>ŞlMgEc=Hz;Lont5d}౫AvY.]:vhD"J"!7%}S[W&v{3n7|Pf؀\U܇*Q\刁Ww*_2}*;U#*(͞eg&-jEuuuuɯ]`7E5~vĩiD$<ψz<|_:R_OյsLGq۞Z iz8򽣺5kٷ95Ø,=YK[g%:SQ_3ooԤg*SW~[ѥ #ғyDj0 =h`x񢥥s<"vi}eGvOi'Js۪_vSRR uPNW^{(<-BMDD& Ӗ5sux<ћgP׼C~C' L}&֍~Vx>{xH.xB.?sK3?2}jЌ\csAAϐύK,Ϯ1c^Ce_Y`Ϙ1>>>>>>~rn_O\AJt OOOŮO Zޢ/7cijv2ƌya ?U- 3'ַk zrrԸmrimKx;y_g^\/0N= 3_iƼr"‹GU^־<U,\UIH2ovF çKĝ^4s% 2v->mDuRM7!NP/I,^ i%6-O-E35ZmI2"gD/q}t]Į87/?cEmA6G7?F"aq#Y"T"ޛqRFCmqފ[n=B܅N((MsqQl0-۽D$j[͎/LG슶%BWq W%Huu#>MO%ȞY$&L} ti־<ީ3e #A3 KR"#i!#FP, $5]J$Rci2p]ݧI50d!-SOS %(]LfH0Jԁ$" vR.l# AQs3vFu"5)ZbH8G,rIՑ HâTͻSFT[7~aJٍ%*9FjiaceDG2 Qߧ"ӄZݶc\ !gg&NY*m1h4^e+z3֯F 0oqC+ũQ|9q,#CFA>=,FX4{Θ]t#uvʊ?<-zElJ;iaM 3'슳;:oitZdzTV~V#MZ%-6;>S!)h4A8G-uj -wbqp'cj{F@Db>#NGfj qQژ[v+:jf]etZ]ƥLIJDD.W*MKI1 uHRg n -KY" Xm>]_/"2_/O3H-rfy%ݾdg :yUdj|˜؜B WDD{3ҒTMdba'C! k#-hvZESXdi⏖{OW10"0CA%q~r0"pcJBs1 [>Vpg7E˙HQx MWhM}_+1_iۢGr̗dn|yEbdb"}{=$)+~ºU~=+5tgI6uBLxmXҧ- Ф9mR3d(Dnc7oqJ!oo/.Lթ !Vً|߼gVIAY=4I옔BCI& 3˗$,=7Lڶކ W$fHq+)pLֵ}‘OWf%f&J2@iJ!g0 wS>5hv|W)kTlclH,wS Z#II0]1;OBAg{ UӴ^1&&oϦ[>[i2C܆z}򰍾6U1ylwvUf)  {;MF@@Hx9qWpvBVe!V"+* MNY &Eonz,qZV E&J}6-O 9T#"2htE!hx>Jw7u rZjt\ƢYvL-y+35[o{ύ9TJxvGhl(oynNVFFfv2x[*#O[Tg )1RZڍ] :t;r3HeRLD6Wqo IDAT$hSVj붹Ɉymlw([mһڥI<%) 5$5A0BD)HZm@Ln"u%<-rqhܺA]84Qϕvnr]V[T%01{,Q<]aa}DDFAx"bjf4{oTA%\L$pܺJTj9J"XԀm]Wax^pQ{Z'VD^Oğ%2,ΓFPq405]z 7z~^t;Kuiy[)/9Ǩ)vlUqbUػyۻyN{TFr-y #iCZXbbp S)$vZ4Q 0ݦJxmFr;1FQDG7tj] %gT8s4 K=]ʔr޲eX43y'k96vP)% +\1y +YWb nͧL[&\sݗڳD=b4sOsp1 FAJ |]KyNQ>W4A'uf%fPq r5)N,{^0H4mi@ h-t£=Ut%9Q3xc-KE'DTҾSl99!5SzͰ֞ڙsD$"5fWIga9{ϔU==5_O5DdJDnO{- p(:06җ(!n󇇫k,g.s@CDLϜ9rr-KEïM9`r[~U4ل5%JGԟrLXVD$zaNФFev:C19xJꉴ=r)q5d9ʶune QMeEYԒ˕wJ#s>cMC0e+SEJ$c'1:EQ{"@w Y^6#2.WBJ?eE{h+jUzwCFʋG?[VE$voR]w3d;^]}TGfLs0'S)"S|gߤOVng]=?fo*>*+0N{rȀA֦tq~zr!O7SuƥҢCf!]}IwqaO'W3W_{xLL&\&ORs!-\8Ǒr|r=K=s'/֐J%њ, ,;sg 7L`97`kTt*1j"Ȅ*?% 4CYUv*1|%q;3Hp [OZMd""Sk"G<I̸RBZ$rpg$/"poxPv䋌hc꧉N)*/ߟIAm%6<^ Qmm-jѣG_t=HǕ#""ӧm{6 kR9AI{3tt6S_!tobb4fo걝qHda!f"H ޛ|X6"뜙ϘP-"kG_,T03"oΩ޻Tb)YX;zyښ]6!G=5`uGyhJCLtyW*t-=Q]!@եA.mO&tixF%?ЉuJK#[Y@t=7 )3).aB#c#0C{7:x<ЙOy{!ð) _%":w֨hۿf8oA={b.}(/Ew0csك=h4 .kI~r"/̟b|ޚ+_l+0FJsbHͿn,`l27,TVNxz~٭=4~̵ M2f 36o?kׅV2GWM)'Ryut{Fʭm?ZP!o{3{q>_} dODMb–#9jb}W(-iDo ,C;D>b)CN _z6Il.\2훷 2ڏ0B_3Yy`# hde;hD9FXJ-?Y%:oԢ%W#\;*uFǭnp! 3z/}`ٕ|oLG7ouLJOw֝ ^Bw?Y{j#I>'on" iّ+JvsnwEH?rCQڱTw_ug+>>|,dxoE[lqf-~N{oqM>n>9IH1qÞ`/Bޚ?MWȚ9wx_ᶘL5sU%߯Y G4oF|:oÞNxI_3C 0t#$ՈBdw: F0%LEratLqny1.%Se$Tm7s*C'|wC>#lɟˤrKV{J' 2T#3o/Z_ $$׀Y )D01$~jxϑPS.c= )C%D$}^r2-2Oo\kz1CAv~߯Wf-{NFD;WUt#/#aHY CFmn)zyNt>`NwO Z>{ ՘WCg<~ vVCgQ (h5oLG$w: }L{Ah#׵ŀ=( Q5OO%AW򜺩%F(9%UǸ1VUDӿ4}e#8BCHմw]{Kjl S4H0$EKՎd>2ž#F9y ],iZ}$D^mv֤ߜ) stt#Knb\ɞs!ٟK]9ƺH:%F1Dj3Tol_;C FF* {m._-Z{F{9[$cX10L\bH6 F"YD&ۂawٟWuә ǷzMҎy_K$DKVINBdccf.3Ȩ[6m]$x*BB$\voObldxQ c̡?}?w"SrgE8W/ݴuB6w-9lYZA3dXC[>&"F>oMNKu񻗿Y b,T3.gwy˒kK6lߺLn "uN -D$?b;7:x=xGcf ZlB @d,^gx"D Mr@ >d\?>;+,.&<_\,>9<9F1Bv^o|e,"7(#KFΉe+.=knW2ahpl̴'GFn?%,~ዾ(_)>atdS4|L#wbυo}c|oќm4ذ&WnMM_`ǽ2Et`mNm)5_+~`UCدWl'$'[O\%3BIz#~w%*xOHL+ߦcqҭD$&a?y~km,@$SGD(b.mg+9".@DV."fԡ-bI{/pߒȑ^# J""A/02iM2̑eYj2$$fp%16p$l +aH0B4ϓ'cd6-XLDkrv[L籱cQ___WWW[[[[[+󥥥...(s~GٝW=rw,βDef@7IOZx[155D9tJSSS`aaQQQr0<_~ׯ_/++C;ϣVYYYVVv~}L`իEEEUUU(Gښa<(N]A6理2<] ܷP]ZxQq4GGG!\Ӑةcy]ѓ05/USG7ɳB7.@Wٻ`kc'\Rmg~P]X8mw_qrh:Lut#LG>@8Ѝ kbt .FQX6yK@܅䝇ꕅS12ٓw5'I \dTuc'c5'6ߝ6lG2CjMlb7&[>mYq-9:Юpk:IMN7LF{Np&=-F!#=?&L8?:!8?Dqz65=6f:P &7r, &"7_a}71G';: I :qtnZP|YhdpSXo˖6~1<_m/㦅#>;d%D{ƙj͌tEVkkHYxY|J*=Ӌ .vPp6v܍hM37^TZ'/⼈# 4fFJs/lIHmM ϛ3y1mM-&G3Hn_`{ `5ާj뼝U_=_?9jߞin(o>X4>=\z֧{_MR0c6L}ЫPZUȎV0 Ά:b>3^y虙@sC]mmm/0wY/O^*c3=:o`4g&jkkk#)za+=,wiK8j&Wv:f2eM\U6UAs4c Ŏ\dlq,Uݓ3;\\D(b t;#j%ّx@~=8#F']W| M{fӖ{xrzz'gf#8CM @!:O'GCd{O6ǖkBTww֘[ԍ')e'z8/Ƌ9wF]xzD^>-1wB*,<~b³wM`5l,iKTf>nեhf&hDzaoJ̻S%U.DB>}VSѴ%.GA6.w#t۩-E:{Rg53?56ݣGFz}&gVSxhq{+dV}ʶO?hs @>+8ڼn_KPue5dTkj-v(PXHb&Br7;gث:U%,mPr̰`ou, v8ݢӿN5u&;V-!5gK0䊨R]UiZ>s;ZS[prUC ;6/i+&@eg \D4g~[ 4{M;3; yzB>w]Cgdg9BM s}2E^QjlZ< ndfa5vV #9{x:|NGUCbn1cNo]7|aoJtJ*ۺ}uu G|y'@rz].Ri?Kv7L^;g53?Pfw9Q b- )O5< ە]u7ʆv;$&7OYES\>ZDvzxxP 57ewi`[P!0fs㑥hϠsqµy#KGFb邁:w5;40 gꖱNN\eЈG{GG.wo@)jM,+b U7U P*RWl ytlSX!\cF EE(2A MPYY^kpVcKY,=@/R@I}*z DKYq)|s|6(Z+ڣ-Wչ <1d& õOWSxhg}s<:J3Ɂ~lJo>}P{z]8R=lK?wCr3SX!#Sə ؛j*d\8}'nJl.±d*)]5?|"Vl5nk&bwMQD< QDdsTr&<*>sƎv1v03tpshym"䔬`C։Qӊv;a56N~G:58_P:y[6 Mthd|<2P+NjDZ@!;ր.fNeFFڠF{"Y`{2VU[Z`8UdL˂X:qPHbѦЭt4D\l۪ ߆xo>u4F@%A[|Dob`HoW0\-Vu݈k~h=a:Dw6_i`َ;:'iy|؅|op"WvP,kؚ>o*DOkx;N&N2vzyx&""""FL`ڒ*DDD%mpIK xx^A?gOw6_9 m[ᡟ_l""""z11111VW9obs/m/R뉈hG,]߼rz' gwuaR6zZ|29Κ{wDv~>4t}GN}^YKx_[ղ|^_u*׿[,s !_؈[TM5CO++*9a{{gwTU5GΝ{Z93t'{{ߨy_v-qFNUׄl6_ˑ>:siEH{}w@D&}oJc;ʍ˹Ǧ=l"-9 x7Lbg|p Zᅬ }Ϟ4kv)7 ,Âq/i CK)Fkkvx<ï K[+=sm~O}r0{o!W!""aF_IDATʓ}Gnt]?U KXO+ Aو2[l5.fr9A+w?YckGHV(cMdE.Yxgߵo}{8w8e???U""yO.]cXv겱(mc7TW>ji9bϿȚCI{m^s4M~_o#{׮_)]c8sM@?Jž&+3 9rǯq?3okoٳ؟z3z= ?U-EZڻ綣5w T?5<e ztl C`?t3ch?T~oӍONZ9wC i;"ze4}r<|o},2Moq\/ehG3}Y'#5[6"""""ڶxh5%@HD$Db"X$ĖKI$ub[$ 66F7͌Db!ܹsw6LO>iX%+W|ᇡJD/KZ6''666o;}wz7/\P^^.^xᅱcZ1s唕ߗsQ /" /Y- fӠ)ݷVoO78xk7ܰ/?elg;MIc%-e!yFX 0h.L]VtH9rŚH'|}H$9ױ2l4hhBIJ")*DB՟.J7F<6($62>SӧwcϲerѪ*VO_:rJ'Wfggd z'h߾}CKJJ}}}>k\Hl|T*=sLjU*OSw{|ukΖ5P_vsc >˶66MC,p`z5 @$8 W\!"{{{h1,d2D ^HO=Tkk?jh?A0  RKe2h$"3rʺ5/^F:ڢۻkǟ2_.r_~1pLU;v2,Cj5u{L0ދp3˷e q j[+ڍє.Jpݖ22+e3Ҥ+rWlVq% 3W8&?)-eSf4IR3j5w%yY'ɫώܖEI뼷/N [?q7KWK7}C\um~.(KُV ܶ*>\-+ SWWxf S7ĐD)3ۉ4q30]Ls{Tb#"bd l4Hdc}uyD$\/h;{-@|CY#:XED̰g,SkK(7TSG2gy""GH'"b{Rr]YGĝ=\ {_eقZ5:Ɵ%"Upt0S{o.;m-,ƃ+-Tz/I ]\4xybX&ZC vpVR4뗽alStbeAZRҺK z}WQzfɽ5@V%"͂, &vQ,tuuKJwÆ 2u> W"e}[ r0[++v*׮ɔ vrG gn"?8w(_da8f'Ǵ宬Y;**#j9FZ?<,Jxp\”<9X:ctv ShVD$~814h~P ^{񎙦9bMm2#`W?>qvi9ùW[Fn88b=:u#`8zsPu҉KA^1߬{d*Zb_»Xvd5Qmg+ _dl4v. Jkx &~9A-뻜aij'$*'qJr ʮqD,9nE>Ga")uѪc׈2ݟf󟻿D-`~œtAm4 eJNNNԭGBt<"E,5CѨRYo?㏆0Ϯ{ug{D$&HHLcASgD揯ȱKx-E %=>YNH5*%ٻ͜utN5`"FHw<=(VF״@˱pMY׮R˲n,5hzL{͵$7pdXW%ձn-?(N13>ycӒa Jⷥ>+e^lpgUPrc~CiJrk/perMZ_f (?x5pxhJɻ{Bf C|3ߙ_4k"zߔݳ_qgfnrBSW$]#։eU*nW2ks `/DĕaSPksnYxuAn-;2܍!Maڰw(6\!2oDz4m~_Z'-|KgMGIju vӖHSҝcO6aI;JggXx6nZ_:^[ÚmwKEMɎRK(s pS=q;Rw>!!k%"E ʠgUGD0Z5q,6n|7)21+=ģws׽@ tURf} |gO3O~gc2IжO]\&s ^].===E"Q]]VUT&#Gvϙ_}+RZZj dggGD6l={6L&a;z_ us7sIbD"1 |Ot|W"}ҕ+8}/yrHzHt#I<[ @}5/~ްNb%\ӨgYLRovΎZzVjwDtDDlp|[&VxD Ȕ3'rDZz|0KjD.yYëF-O"RŤ7&Mhy%iK a) Qt[%oߴAA,ud/Q$mS>H?--EX\7e"r9-ڣF%f]aÉI˞|}<CL:!uLZ۴g^g{GLMoNM9#"#zy8!!HYCSOiXhj$F%(gGT GK8~}DNh4 ftXL3Ƥz,!f3'3/\6x69#eĂˤYKEQp¶mۈHRi4WW׷zK*^r?T(:nРAW^D/vtt$_~eƍmmm2lРA&Lx盚֯_KD홙F199;L 1]n5rm/XL"Q+Wߗ897-ED!O~>jWW%\WDd31lrZ?wrjc?6k51?ފ~nHVJ /n_4/mّV4%&.8 T89>wѽ;斖W]]̂Ob%"OxcuO "::9L&!C7|zeC6f}Q?l;.s RO/cJS| "}J X vؙ{gTNA1"34# EjճT.i PX;MD8 HDu:HL&26YKe2Q;'h5ae6uv 6S@/ǬYZ-MGg9K7S}܋;PP88_Xl-Ed[ml"2l"YD鬃Uo=%? ӓw~Rg-;x=$3DS$=u$"'Gߚll9KIM uN5͂ X.A&(00`NmjY[K/PKp+m=N2Hd#ؐTj*,} %@"jjn4_) D4`Hʐ\.f!I   ~g/8x>~&l6&`0\r^@V P&!M&dwH3 `ccc2'CM!ul6QW',M8ߋ%Yʀbgn"eH\HHHM4     <$~-Z\&`0f4ba⾫}"^6߅ fd2L&z}CCCHH]?z3(< 70tSe$@G     ܎/>jbT3EGuwpߵu_ɯϠ>3Zlfԋ//F\hӭE_om_/ze{k6~T~M5@E9S̾dIݿIvf**oYO$Y΄dAl"=v-\~_t9g4 ch*o?;6-xh?H܆Mu :C~+i^_n깣ڡcC%KɜN7e#"j;4ojNaN5f~ԤO+s?:fIFa&sZQ.ܗS2h8R49NNO|'b"9rq j~y&i_s2}jb؀ }kfLf~}YÌH~ٙtgለq0)n{1Mr=1}^O^:tFQ׮DR=_SNǭK|~d0D̰1KV:NI\cUng:5w_ˊB-N_H@Wə"j)Ygi|OF6qH$Qzn~ IDAT2mu-ßAAu? ,O)8ҥܳdɳ^*'CK_hD΁oF(GQC[JRǐ)grANSWxsgqv=#y7]WýiӶ_oJɫ'"b3Ŋ7mijoJI-aW_}\~sb:)Yqꫯ>K_g}W&%fUf|f91L@΃odĬz"4'ٴmקq⬜z~?Zo];q9t> "Y){hƯuk>K|^zjpi]i/}WuMO̡8└MD{R.Y?MxhסjnS쩽l=wI%RcC<Y6ZiPo>+V|:gv"^lufO M?4]gLJJ˟ktdP&YNJJd;>NO<$K()m<}=@'x5Ѹ?~3tcD94(*vBr|VK pkٹ(QD͘.'jfӭD Sc|t9e6ѮğR_9^~nʥ͍H ',{RISy}dn#F6c>i)|Huj՘D<.chR(d/t2G:e}}ғjɯ(H>f򰬬8"fDD5kT>kGgXuxV}2k36{=~u_8tFdX[0y hkӘq>2"ru'zj6o+p7Ĺǟ}MND@G)IFr(9yQ4ӶLG:ʧ"GgԒo:HIaՔ|W#y>DƯt8G&'"Q`/2 < \Bd$#DBDq1^^r هԶCg6*t[:ϔwe9[6uY9ӡzK4t܌PE]'r6ؾnэѳgx ڈEsTDD[O,z'ʱf@/͚c>ps2"bXgd3K'9aUe]483I ">kfxwq] F"^C ӹJvRͻSf4',LXګh!햟F2FO}@/?ቈoP|5޹32֙Or vN|eJښڝ_u{?0%Q{/.^H"WX(G6[-_1n*rQ"'] /%;{ -c,ۚ]hӯu ~>b=JD 9t*rqx/*ָySPޅ3EøorNQgi4zIAo:sxkIxGr1έyz";e7}ڼe9D$srY]Ig7ft)n雾߽rqIIk4<䦒9un-M7/k6n)n""j;>n|H_\Ԯ!pݥ-dK+um:Rb vuwZ=$w}֡Ep7]|#%M@79QKkWhJ["!pmÑF}BO<<>%EJ[ )YdXҚT 'еQע#U*?HhAN"QHjOi[; 4HjTʇU҃'$y#j&Ӹ8ӜGs[M1N#36`i$wz"fhڼxf^5:.uQ>vRΧdN}%'IcOr¸Ⳗ'm{yƔ Vv.>)#)%(q4cs';Ŀgb&3%/~(Ϣugm1g89шs'1Q>yi܄ W9m1n3 %_B;14!U9?*aRM~!ek9Ӱ18:+IҪuyks'zʩ -Rs{F"IsG9|:ϡ,CkCDg5{H:?;iTJsٜxꥉ8 Am-w/zGʜ᧽hY)if%Cç*ƿrnYj̔PFe9>dC9ōIu<^kԱ>^( >,A0&d2<BBB| ˯%m_/OoUY3~+9f qm 0`@U Q6lܚw({Bf/ ݸvcEGxTYPں1X4ew)ɇ+:oZ;"jb6"}XzJY#Oľ}c̜?yD-XuDy_|/`&^-"4.hbWRX1q7t_z:USj{3їz<|g/о,n%ۡ.=hqߋMHHW70]*`R)+aaccc#P b[[ۛ$K@@C4  [oH$_| Afd2L<4 o@     IO;r9vNVgݒޟ x=Wl9u$J iˎM2vSBUR]/ah);%exLhƼQRwnɫh5J|B=]y񛴌C:(IZ'fvPN^>S Vui &JeyM(>X!Q,h/ RVxB_FS'U@͉-qgG8֖i2TBd. RtڜQS^p ܼEḊN)§NyͭmO2\_lHG zcw[44!ȍڍ..WuT!eDʠqOHe-|!*4Q+,kUL plo/>Nn7>2(6*O449^F nBf˶fƼ{ǖZv3O?{^;@>ZAQ6:vgv";*D$U8*'@JAD[)lDFh, ֖R? d[2v\S}US}ՑdH^{t)b&(,?_[HkhԠkԡm(=xB2:_?7:֯GumM5U5m<WvRfϖCm`q!).W}DQpE?NY {B`@TT爷㦻򱏗-ZCDT6̑#nFi=Zm$ٿW:cx֜B]@  2KW%xZW&(8߇ fd2L&z}CCCHHݮH0g H_9a6"b^Z_x}H-1!k Fʷm9R>]]сkx""bâ|sي('"7sd6dҌMDDv19y+o|권S GZc{-(h%b=}G}3zM\y`ނ]lޘs"PxIC0yUg-\0u,sŊU|ϩ|[}y6~HI5DmǪnȤmUǚ:,/o\Ydy3zQz9UG꫎l Iز*vH;4fw è6""bCz%FfD:{qaΎS m$C7zNl;[o"j:2gbOf=FSM=Q}A.֥GMWYp=aM""rt>2kc}b/#( 3=V]߭ˇWI91oƾ8ŎjlzĹؓ "6ZaKdgkʑa l#H Tم[[FT]PH_SPIDo_Mn9_yF}{u<]X36""&$uOV#O},yƲ6j][G/N?5Wᄑ'-?%36/H@xپ1ITS^T^diG7)g WmkIv1iq&(2=6qKX ِ@v{G\eQ WD4$"2-ˋ.l*/J]MgD}MN;9za.<FZ%2!!xq >>2  +߾&q nYb^z;W`8f."ֳ凄ź][ODȑ2yF1yj:^yy鐮ooմpJ˕{_tSu泺zx{E 8'nd)E,K7_<ja<*hxDaU~ʌظ-.|!aam5xlMg?/KWRR.TFX>QWPn0K/CKUgt疸9[ 卷WWs!~v7GibJ|Me! z][]M۷WqG.;^s*C\z@}[uNŐan$s#j""]NO$SUO|MQ~Ge0O)<"њq# Gݮt5mz" #5@xd 9[IDW]JeʷYktI 2Rk+lɺr/ЙʮC4Y.ԺY~K7I8xs'9~agzum]A!£;ikQޏ}#۔kksX 5]Fy tnm⚎7.BtF!>>d-֕.(8r3 Pi7%!Z5vd  è3_UԨuGهSd^g-1 ᩦ^i/xvޏP6$‹ ʩFUY}wT ?'p\]~ @~AUI_>dOr# (/ z#aHqw:J[]fҷuV<2/*w5܌%w"ʢ:=*z z4mUіk:ƍb)t $@T֙;]\4t2 ^TYm+jYGyq> #"Iya@KXܟ-֕- 02RXz[ءK\l+^WWͷ FLDTWTSg xlEu exe]eyQa@t{V.;=f#n%,˷hsݱOK즙\+WYcvd~s޶ۏO*1*׮n)-:yzCB숈9QIDTo0PkuJoWuXb惥Y+W45^HH0i*8d׵5Uw]fR eHl;35._vL;+r[촷o\F6$җ=·o#"qCO{0gj˷'= wBST}a䱗LJzdz嶚򂽹Cɽ{KJziR~sb#(X v~3mFx~V_ֳ8~\kq[8s?(ϽawvfV_R9ޏw>}H.I,[iÀڏO۬[r#kz3&mY>23JYXDU寭̛uC_|E.A0&d2<~gbX!^~/EḌ\?^Yu30$"vz!7))|=œ{E!a^o0 ߞ_P^Y]ux70baCz%2@o-*M<2Q AF0       # 6'l50'l8gz?ګzTst ק)+4_Ay;ֶbc:HpDzLO?,nޜgx fd2x !!!h߈oaO/VH>M]EnN^}gH/Px(}&F GliPPPt-ヂ’+ѝ Y|s ='KPoҗ[ <$X;Xo//ؚ8cR~ՙA#0Vw5Vm?xO;0((Ff}vrȜeeeE~h76(haޕjpMس%JW|pHbJlY~gLD[nAgusC+#b]+j"RM 1{ib숨ޫdx]_9#YK[y7 'e{J]̸#YXc+-0mo ]>K^fYK3TwGwxu]g+X52r:U5>h?pTPPPdfm:?+|cC\=MwAwlaPЫ5DT޽5q$IH &V(jҪ (ns]} ,@gҳʞeKVP)JL.#7~>x0L3y3O```ڳ;Faj6//=s$C1+j>o% wAE!B^n/g7M?P(GDO}aESJDZq+ՠ uu̸ώ>T4IX5j(yLs[nhVW6׈~5uͧ>KgIe|ޢe˖'sz[. 0cr%ԁ{2wmhբyJjzĢeV-LϾ_+5ߵC0}|׻U$ZlٲU _0B`<{3cQB!sf?~PYS:Oۿu}bڸ'zPeˌ\cץLۡ 15|)0MI= r$HG3WLz-Gj3rюc[Ŭ HXZ|P[Lk7{HO7SkS6ڨ5j3G*O>V_cx R\ug+F_napɩ㺲SmIgw@U~R:;&õxM x,K?P j>@Bg IFGǩ?U))#?ڲH{he=5ԼB{,ͪrTwQ0{i䫶2Vq] `<>jm9 b:h;c̎S{y^` bjn_ۃ7Ht:Sc=:;.aoXB!l` z1Owbd:䤌4Sj(8:^i6  Ϫ#WzGIYgl@2/6;yPnlc޽5Aw!-7G5> 5oխ:ofz}垳= F<}&Џoƙ8vIf=kĢ;ƞeec,FP$e]5eڱ_`4TÂdQ "B=k8 z13nY$PM?vr+.gt2aDfPsP9x^gv̧71 ϗWvP] >$iC=C_Aܗ!B BO4))3ݚr\:=b:4{/?yڲL`<=:]-3V͟.֔*lf.}n?9sWIikj굔{.\[ELse٩w7?srĴU]s}yt|Ww,_`+؛% [R+)`mEr,Xb#}-^H%"B!ГJoȳv}fVdQy\kY*fh?B!zr ТǹW~[׵ •+W ~[sʓ͌X9ۉ巯C _7<2[ˍcg/z̷7[|st@m{l#mƫe'JZ{r^u Z<s?^'Oն3<ߞ"!B!z-~O.ݰ gI>g.Y|/Z&xYZ[_trgNA+V/#t]4S ɇdپUWMbɛmULg@_EߔeoNWU:hJ}f+OjLx\ B!^Dh骹82_W`.3]Ljt57g97ζw\Қ\^>s\=nY^S5 ->"Ώ/% p2Wʼn}F]}*y^B - M~֔&c<2㕲|o'!BP Q0Y=FcRMyvX,].c&]|KT^UYi}Gue<Nb bt4 |UKN5;p'Yv}|'K:2 !B@ ФGwü+>Fc:}8t ,%~$ H>Z58v&N]'׋L }}pr1}@CjSN3Y˿To1"B!$@S{+8 bb_3W_5Ne__2`K R,׎Ʀ6Nu.9gNeh\xʬ[i^q.4M|,aEKXГz޼yl6 B'jL&"%K.B?mea6y<I1ceY,aŒЏbD/Qz]Sf3\bERr1fþ(=q<N%!,'[r1ތDX"%!&@B!zY`D!B!L!B!0"B!B!B B!B!L!B!0"B!zmܔB!zZ 7Ľ~l6jZYew B!BO1.@^/3Y,#Bee>@!0"]񺥙Hi}?[&5{<8pu>T۷/nj_/ph(O,yd_Õ9l¾Oc V/=7vfV_ƉWy~dA BcX`ՍF(Xѓ/LGwVi` 0 [~b#[K5,ub[09&@DUH)TYXزba)YJ՟,aCRZO6@'[SGq^0hUtZV=ⱚ̜jɒPxK<=YÿnݝۨIKX-0HR҆ SS^?uukZp9:uuf,$&1+SFzMu6K8COa[)UȒX UEK3~4ƴjdK22= BwWG 5$IǸ># ZM.*`Z:Y R>>HafdUTK|Bo2PG)Pp=Ww9!84n,] S[_*6z٭o'2ފ (=;PnԳ@ysA !l/¢% E ×SLզ5K20NZnjQd.4KIzmܢj@ MLX}uzJ3]ٹ%zx%%sXU~vnau(BbL7.J9Ez$KRd+4FT!iQ[Odu2ϰ('14fV4v Q-LHvҍFH~M*$Vl||Xrm#PhZ:'(S4x_?Ń =Q #tWK?q(0n-m5ȼ$m sah)]ZP % ) Jiٱ2J7Sj-}ԃP[_Ѩg$%# u麟Y9u[,MKSCO3ӹ9K5FJEӲce|_FYL;+;>!$+>4GE /) (yܹyɩGLm"}P}Bؒ̍ }U~I;eũs7R3ë,}9[{5>y,:k_AqG.OmTf;r /)XZo%9ݓ}~D^(uud֑sd+3K[+NO/yvf,TdO>]yƜRKWPog`j.5ퟲ/k/đ4 A\d%5;XiR=D!`*ЙqӎZ zjmV7B+}n5aͩIؾoδƢҭ=Yb JYp /%x0=>W¾ '2Bz3-o̩ٷ3[?yEh{&nߙ^4ԨJH,d5F2deqib]h]iN^pgF 蔔ɻw7xmܢNIh2:W>\oNE"U9P% @EIJ+4LZwW)iB.-Ř}Zi/e>V⮔(q%uuJZ"(J"J1}~%Xt'O|]K=Z32̬>@`kvbg V8QD'pI8pp Nz6C'ߴ5|CQNf,KOqĒc(_RFꎫs}qzzɓAG J؞}L붅ZJWھaYͶphӵ%|!'3AOL sh6Eޛ+CG /%I% : SV)`y!Z;rFSʂgZhU`uNoSe IDAT-뼼tuuHAZ %nȽ=!Q^O "  ql6 HwqC_pV11-M73o_^RipJv{IIH[$%-[!PluN`Y^"7z֔0f4ꮏAQ~,Prc_PHXL2\JwV <(iQ>EI@Z pn>%\ 0ݴY,&SC8ډ?.?&I2Mk' <,i 3sW;FQ{\u\KLVʭ4(:J(nI2V6*1}}w*j#3j&@^/g"hɒ(ܙ㸇rUv_mz&i,,KBRR1>98(BALbݻ77|+]*+ز%m߾}R|Z]E~5+ ワ Յ 9:<7@+=)h-,tͩUA06j ($GZZ3ӋZ;w! eԭŵjNRU%~ʍՏUҠ cizZVm8-=W# IS>qN49ۊk[ZjOJ,H*غNR+5] mU) Z^Ңay> f{Òá}C$*.mhh:]mC|f#x`jaF/wg#$0-XfKsjn?_Wg5y\;~Wuۘ  t ˄!&N(_%<y>ipRV'[zmŧOߺnƃϘPָo"=5EӪNظr*>ȥA +TªN-޶~9 їunGt_"iNqAi[SW\Zm= psxOxd XSS?ر"!4i.W/_1c~4d+Tc Pqp~:$!Iq;YA1S",c1s Q&euį[!M*΋Gܳss7%IܞHS>vfɳswo\ UЊGwZdXH<w&E) 2%65;gx%s7''RY_R#~\@rmҗ%٥YJ JH퓧״jZU٪in67b}2,qqxT?03vGSu]]/$IB*8˦Ouxڿ.`77^@rZ D/G sw-,(<гʨyTN iLxК>iu;wʳsSsXJsӞaz*$-iy;{A#H~a);2|#0kjZVeioo7oSzk׮7>ʣrppx{fhhff Μaͮu)m$Ixm۶ t^If,puc|G{聘o ydvxoXܼysҤIXrpXrыQrE[zY i_;{%pdI2C&& ;9*&LS)'RWKv5 $zp^m=wL& &' 9]Oi:;oٳWH[vwwwuu3$L&uurQv]pZam7L2{Ԏ񕱮4bN^7$IBܣo0^U_3[,wAs]{ɝ$ry|^-g!=T|KdV:;`Dc6-  +=}$g8\')J76Q? F&nګ b}+&yE"YV;swF ^#z y}V3p^9{c4-4`pFwjvP{3mZOUO q!h>Ιw[߷h?/MsevaD=f8>d3Lf " xK5ZL ga?;ZtA|04d3R8[m%[??z }ʤfZǻkL̂Xrя̃`v! R9>kZ˅4&s6*,69::Czg?;!cX4/.уׁqW>g84w5V|b{BBBg?lfYHtI TB|>ס6I:Mbr:K?2^(G7kY# Lq$>eX^h l6fv60p򆣋@9m  HGwI_;|/䀻, 9U3Ș@(^tE}]{x$2.ܓNsf$*sSn|fgaَ={Ώ^3:8+~׻L ;db¼@?DgЋ]u'No6_:#$rYLAީsϐlC9ݫ$ ^wrkKWWa>M񛚚?A+FᮮHǧ͖ 8h e;> ?^J1A$wn|) @/3Vd2DB>ϷYm7+&; $<-D"g GEK7ޫ֛e_tis.]v+x_dۺkh\Q]{M zMQ9JW4:{~mSǽa6yÉW/fuյы UIVq yg37"rfV(t hfhUkKͧϜYt[Y## Bf8%WTnsr^ԙMv(wpjg0 2fpzpٱ$y12#H7mn=gJ Tmr7fn$59) rx$7Z e0B,7[jn9Ynv5x|89e-lj/-xSH㘹LCMEp]P{o9z fp~y?罕+C];vW>}Wk~84Mjw7_:iQeS @8aƼ%?yo#4?Mk`TuQEgbDeg[[o &V_W?{8G缿{~O6|@ `!D`8 `B/L-sfW08LOAXKS|/Dfl_1qh"B/!R$8l619aܡ_bӐ'>+H8etVj75LDbD$4-Gz>_ ЏҖeWfMmb`p81 ǁČ(b/AKb}s^ A_|]գ8Bc βnV}5gVToL|jzow^hjP[Toq^}w>yg5!yCΛ)nL].BXӯODbOJ}"̕uU*> &pFʹ#NV燽xab988fm\@@}R2 8X_<ޥK~9In٬ I8ٗoqO(I1٬6ڸjk&]%&8lgP7^h I'Μ/ܠ^|kB1 |#mg್"N$uЍuU8" )@ <9[o |xupdy)7fŒń_@ (Q|knnW8u:u7/|*1! ApP7( Bh`gifڸH,;884GA=.Y'7I qsa[Ȱ7"޸Z߷E~F/|IE^L {t7|ZaL,C]L~7)k t/sB&6@~]]{0۹ao-+v:Ξٯ>8uF؁4P8G]Uv4ųv=k$m)_qho)S=@/3>g4[?Nk{f苳.9Wyo;M8$A8 V DoP8M&2I>^珖tI1FH< АoDs L_#&nIc?0E2G%}?fMN3cƉ}Lg_ $)h yN!͛r 0QNBpl6l6˲0sYI>9[@{~& fQ# @O?H6Qci|xO8 $x?FH$|>IЏM]Cә/* ;̠@p!LO4s{S_)YM>BSw^Ι*7dT~j ]܍PoGb`K5tH:7pWgJ*NvL!=Uҹ  pyg]@4dM5 ^G4m6k'*x]޾ad}ڑ8:9qajX~ݠm"3Q'|.>I׹ c?nD /XCl6ϛh0QfWnZHZH Coq)ߩ}>f$Z"uppD"i|>dzڛ9ƻRAY'4c X%80-;V7&>Z*?Y}:vIcm6uAqVBbRb(3dbu ZWkM@|'_ ǚ:}tlj&zp$? fj7 Y'nU.L֥;SN}R[~T,`u?/O8>QTWuN#Y&@ټ'KQM+'>%> gv,YFw~]:G%!= ~Q,]EWz*~iWE{|". uּյLS^*9 C=P$xq= Wݏes<fÿ} vQ|0U`i엜!^uR5\Qߜ0kx 74x\8St#[|VG.n8[8r >|jw''' ʱ=b/7x&HJٸ6/ߴv[m6J˟<&NN{-+wc g1q |*֧F=f1J[aс^ϧ:3p 18mrAR[0\M{k}ckk76yVo>YїfЬхQ?{"wb"rtY];PzNYV>eocYǎàXD" .|];Ip2s f i>A}CD9Hr|dWVyqTg|~.z+|HhOoK)i5Ph/FR%EymK;u>R04ffP2蔔XW8;3B/6%%GTm^- 1VյvJBkwh7KH4vg5YuƣvaOvn &drV'ӎl,Lϭd@$%mݝ.Lϭ(UHbZJ;Tm^- џtveaiYjэD(Tdbl6' ^ ],H˞Ѓܭ IDAT%`#9 H)!@k-֋}2[qS י,śOH7Q3 81&Q+2:^:É!D)^pSĎNeH'>3kT_g=+ IJr;2V KHK9?Uɫ;nQYQc2m#TU3.--}Zz'l_y=I<h3 [иgKvnPu[cxj /CfQgY[Ҧ:}]p>^p:esGw]qO ct#_*mri^|?̻bl`')?xK߿]jl'Z-$GΟ<?=v`ĿےgT[ S*!/7TiO(Ք_ʴǧ5VVY*K+RF}tc|jwfO jmWgJvD[+xGJ0}G*;`CTFJҜНcwScrr꼳Ň]ՎjmaN[Y_|ͫӳ+BvS y6A[ʜXH0@(@lM쿩3Kxb?(9 @x^^ Oik O= 6䁃3!zvzrhٝw"֝]:3'nd(o U[(e@X%I۱Kiڼ290e߮ Vf7,ܤVC~ӑM$e^I)Lz5ޖylڟt?@dʙ+NtsSۮ >'t4 _(?mbI)-p:s[edqY'Hy}KLq o67}<7q0yxnЄs_w_ƍ'G8'f! zs[ IP$PM$qݭ{'bGPHQ>O%Sh$TEEK@k$&7H?BAеP[Unl- YlCنlC[BJmImIJ ɽI@Pպzͣɽǽ>s}ր9Y% ǧkkl9q0/}m _GxdGw}ӕy{1JsfƆO("гzQ -R~lxovf`?鳶gRE(a9<a vִ= #kJ#&!;/}` ;͊_WׅpPv `1)LS S$3]'L"(tG-O?gO [Ayg;A26$Ln$22"(>رcͻ޿͖KאG&O]|: "??{7PSCB:1 Wp!Aqpbކ$"a@p( 74 ~sgb9XH#BE d-}'Db>pȳݑx%#ֳ8I. BPuƏYƈ9(VZR;R=qTlueIXׯ7^E{-&%"!!ayBgb*!˝Dsd_'?v\Tc 7(Y)C֏fs,/=L̟Á7BIqhGM1U*tNmhgqZe=SXωz*|ÚGƇ^g(@"PM{wy[x{@rxϵZ L?Kx_Y*>EP)R?2Oń"g>e #Cq`98j }8G_ﰜSQrqx8:JBJnܲ깼0Pu=.e7yuV]UJLQ4>puc55J\6dN`SePDd#IdZd͜q:eX%\_iϭɊÁٜy9+l\i,^_XVXء.ٱPh}67%Z.p88IhhIq&#=h "@CEp>"8D! |!r*B9) ,;G"p >v8Y'QPfF2DDBdBy?PI>PL$D  ȗT,FBA}¤ᄒyuS 1F]͛#܀D+W|`˺z9f $"?Iɪ|GpJdhH ehp0LHSgs_uPgXo8X|{6D$|ICGMS6}1jKl$&e0.C;^dljW<[|cF2 Qo֟c>?1>8 0Kȶ#bA $@fD"IQ=FݨǗ J3SŐ!86G}=[ 0wI^R&+8 &VS1N'C(T$SobPUmiԯ\_o'mZPoЖvOEh hK[HJUᗗjB(u-FبI+ unjwUjŅQ@īnl`hބ&8`Y`YPL(E+6SN3r860!6ŀ | pBp":C .|0H&^ 43VvEQ(w(k,''= N-=Zho{_l}:7#:jaOL'g/~jUBǧ9Y @nvJSN #2Yjfq FUXE>S(E}e3 ?5!wkܹ N*.$4mY#c?y~i<,&B`_A:ѝwOLlݱB7rAc0RRa;M !IL#OZNBdEdފ5$O*,.\__mEY0^'qORlaAYni%L `fXq+ lŅKi֖MTTIU 3%&km2tEMN)]ԔWh0qiW>X :hDB\V^|zT%]՗ +rx[z$ w ".R2,lhC'N b'H|E|U8ceSű\zA\|EC8a5FǍ-QhTJE@panؔL=ΝMfeb{"nzziUI]řm̳\vfooƱ,~?04M?~|Ѣk݉'BCCQ I+_n~hw@L8x4 g>{:9Ⱦ~vRi=UEX?ZGyYf]Z}uvmjݎ,>ebcco@˕\.!q< M|49%`(n!;E#_v";l&})rEa 8 8a#"8 8( 82=?#|a.3 8H,K%D"J@ f r1}pg}-OrL9M3a(DF83 3u@O1}7.=쳈? 8[͸Q&y{-Jyx vmZw>yD$:gz 8d}Zr{p8?˾m_g,|4s̙q9ϕ>jJ  &H`?z^f&x<=fh͍O s"9`?>DCsg]sg~nOpOGYZ>`tCQVbOHPJ1HϘJa GP*@AvgIFs>k*nɀq@۫,Nb,9>_^ḱm`"P|g/cvi9b XH &q\bqXX,)xr r }.@ ("?=l@FHdSHkf' ,\y<7|w[sE{7Usws9rR213GhK,7}AC`"ߺ:&&s#O>3kl@xF=K۩8e9[l D A(0,1%2>y>zp19(,ppow<)or DZ`}!|.H\}#p*+s :10P݋,uy{ ϴh/KDO29!;Xߗ(p"4 r9.D1~p8E8B)Eᢿ1jR\(<<L)Yi544zQJ`D,ȥc&.xY-˲݃ADc88?󵀆8Nv!B1e-$bB Fb10W. TY =H  υ+|aWDn64nF%SF`DLAY?)EnQ*eb;LG8G熇{޵kW`&...;;;* ._1~fo '*Dy\O> ICDÉ[ ^4.H3|W:1C(;wH2IA|Y$"o,ͨo}<< q~CQ>|s\H$T͈ٝ}~~cgoGFfDxB/pѦP.* h/|b-ǿV"H_f&"6&DzA3/_FFD0tLzq6C cٱ դH-(~5uXqC܃a@H@ƥ J(\ɂ9i܉ۇ\a8gfH"bAP8U<<<ǹh7xWW_ZڶrpppLɩfj;qΝ;{oվ냚׎{i9JF&,:q@83+rhr7^ kQCt8G\}gN3`yffsss__LR$,42i8, ' p\a˱ghb I$b@ C>$ܠ qАP(0 Q{<|^GD(H$  Я!s#C9q >8D.CrY AQ # gJΝxe[Ű4'Xߜ&8p4rj`` tu-K.uO<յEw{f+}PA; 0 e8I];49{N<*OT,tA3}BWMNڼf9A7z/D?`YYY__w GDD~1Kcccww7q^ <\N;p|Ǐ=|0a{O<x 1f577ݻ7>>?90@084t:Yo<K,qtTT_Z_s @gy ɻO^\4'RKφin%KXzD"9sw[kTVjcl׷DN[Sy &0#. D#onUZ}tt,L5սu~LˣڌT~So0'd~АH(Km-pf޼N:g>811p ڹyPO>aY1a IDAT_}֭[mU dWTK7?m|xxnz?[fU-,+(kHU_^fUoE͆į՘J}9nkuV/ԗ6&nW<<7N8i\98`gpY3g F4MgxG/|l֙ioM"Ϟ=7ްX'NH$|pbw:=<|nZ=}4g}BFdB`3bYӱo3g= mm<,LN j'?CXL{V\K}aA_Tl*L֌\@zȭiiҐ07۔ee+~/7 1]Iß7.O`LF;N4fҚ^w߾}uVZM`0fUoǺxP˫iiiirV+{h.š?pnl^|U4c29CCC~$pNdTAʘhT;>_I wE6m}w8a@׏+d$AG͍ݽdJׁr}5UR5K;]]utx3ޔt~;d$X2'H͛sg$'GX|=pK/6\]>E.P̟ޮ:??+{4PLdi,-ZG:#E צiuY[ NuNN d9 H'#2 pک%$  ش9'DuFhst9:9ȓ J䶮V"+gkH7v[\2uu2$%LB[Mz \vv@[͊,LnWkdxU9q8(u :z6]+D&)WhCh^&@0jxXa9d3T3gf d~/3~?qG\ d!!Ϟu^+*.^r`-`& $aVSS&%1 06qӧj#KW .m[ծHifq"`LmZySF(Y2Ms:U4Yj*Aٝ\N"%=!`hYIvd6K'Pd@Y[(MNd eitLGLP{E1INbC@9KLfzʺa6#P7R"(soSomUS+3?͍ݎi_sU~soͼDŽccTO^ E"Z^~6CQT0]n,ŋ vg"w O,ڳ|?{ǝ:ǯźPI1F)H$A8I1%偯 T#c#9` O/vB,`m:6eCDD4E2FzՒJ1Iu EUͥJ:% ?Ct2s~J風{ٚtt-ZiuAUЖFiecÖ#WG9)ƹkMf[ Ca:yn?YghKQ5ǃl塡.khhgq%<7766^3P~W__ȐZ@?<~v|/\r8ip e8nKzb9w~\b$((qzpPa@\,!}uBjpXU;jrN23=t <6bIltjͪ8`ӧư~,2T˸"`( `8+KTBJ]`]BqOem 6Vah1om< Sw鎏|߫o E_ Hl=('C(.rz r2BA`- 'xunj|VMQ(@wE2$19z AA( LW=}x\MGUj'CY($q`TemBifBK)UU[dWOv{O\Wq0^۱ $I)jfGDXVc?sFPȦӧOI0 c؋EE۴ZB<<zaY8# }m۶˿+**D  D"ǎnSRŢ"===U۶/b/~F(;?rW"=FЦ]ּLa . 0q`<.%CUle%M^F q,̕7/S[JNhY("i36Z4aG, jjR8l, a9E}y}~rϲΜ8ނ[#0|>8fdDOr)΁s޲<-ء#c톮3K$Y }tַs+4xIjҜ3a!BA #0`l̘q2#㐓82MEWN@c1뢀#pY)fcoB;s\.&Th0s('1Eݺ/}`&t2\m+v};VU<˗=XxtBG*VGGϝ?\=IL%\T[/l(Wokm]qu1<޴}?eR\_[oU֘]^y:t@[+i Bl@Pxt2BE=&"k0|%`>v 墆N烏;7WԴsomg?=!ӡ,;ap1[>:d9r}s./>:d>Ng>@{r|H)=48 ]+ڑ1FhJjc!*=0<ѮJ*!"4V7S,#4C~`8wGUQե++ L+Z rKK,)gFbljdp+Tf]IE]Vy\:V[ 3u2*),/]:P ƒ'ft_Z``/@xMfI*9k Wuƞچ=}~ATY*9qLfgA8MlBА͉4I?wp)4$ , bQljr'!.(T~BeLo'qd4FTOU[N?Y<pE9e9󳬟e9E(@^EGre9x8CQE P ;~M;-,ݺ~T1d}Nm˜=i;aU&-]">qԲ =a! [gYRdqUOfn6"H>umKwa|g+; 3~ܨgA!}`RsCvš+4yxn~c lKyюvC9pw)ע}t .tP ct9fz  @uIWopmW<<7"*u+8w`&~_OAJD"+J;*Tݛw`ie~y0_Q ^iԐwԍwg?wTij}'/L|޻&MrY& >$CC[ݹ%ҸzUU,U?}" [ U(J8N| DclӒiprа'`1.҈G>+٪p'/2FV7e. }eIⲶؗ?UJ @(B⏇:#Ȱ8rFEWa QCz Px8ߨ[qLp˂[||w;Z8r+ IDATHG \ABx+@ 7edDnvF;nWKovPt P(Yd]nO?w7\;qO.O^xQ[xxAQ$nFOyzO2^>HGV(ڄw׌hU(˲'l'WG U{7u;Ѿ:6wÝdZEB]3+kvqHn <w8UOVU/b5[3[gUpbyԥ'^u3Ɋ Wõ1oES+sbmP4nFS}SgNN E+"QrdBлe0@F=s.P (*;/QB6aO T&XF\A[ګ+kۺ mJڼxW/T׌3DEӦݚjsbFXR͉Ƨ+)M{ݛ3㷻p.W5u_ZlRmjؚ.7kJ~v a쿠YɺIe :k w|ȴ\M,ЛE%Uݔt3b+j]ֳi3-2kXLDψQ̈QM'e*v*vCR߹ur}E46:*6:zqJ*)%Aގ5zf[ִQR5E{@|{5Yiz-ȖPo"jp0aond;l'}W<<fѹE_fH&%bYZٜ1:Gwo,d6k;@Mt^\t-h]ylH}uf6Ocn7vJ,`tocqauE*Tt5mWeYS԰qdKL}-UgkiquBhu hFh_]]A/\ε2qRjƏ:}58|%ǏBYY=͖QghJqp`$؛;2jinvjt*[j G\y{9X^>A.m^1 rnQW1U6۶+Pk{h cTF;*2{m:PZZZjF}i2t`y5---MUYj}eH 9e۶+Ҙ$OvlL]d~þ}J=6| c][Ҳ!_a(.ndO𼓥RfP`Ɩ^X(cYXe oTsY M3ɣJ2lgݫ54"jtܤ-ihٽ"Y]03 ]Ԋ}K..J"ȴ&%,6=J{zES2\_;qnׁMY+42S6 ʤ,` KM]XRԗu% --- *Caac\u/"нŕ˅n:`[zѶ;*R*]ŕ攊-;w$[ v^\3-fn7&K7a<1==Q~iceQSRv90 ʪ۷ 5q&,:j.,5[[qmDL8G)h䤌]XBF M&)#=H]l#Ԣz}эq-^[SnGL"T:9` A٩ dJFJ#fyl]nZr]Nfl el6iIJ)9ZjSe$ S%`wR)K.ij(JLNzrW$%LB[M.&SM SLP9sY.>W#Ca7t&ib:9)YK4 $؅%ejpsrd KP(} uu{rpPuȤt pEv N"%=!PϬ$=kC9)$V$=>8R}S{薱!Eke#O57P eln;`q6oLSMLu4U`755+%)o?\c:M.Mm.gTDW@; O@o,_}_\/NL1ՑdsˎUw΋ϨN5}3Gd9qv6Կusd/ W%s,FkItw=w,;NO9x:L:\|,ЩRM"( 9Dc$ HӗcLP6[ '#N,uYz-+l$R*8c*^R>I}Xw7L3Td!h`tT(h ƇUڊ[V<[rwW +JTz[),~|*~]^^L{f>3ߙFO}-%M\P%(DDL:DX &'&e^H'׻`]C3p/=xoU|2vYM=' ޢdΡ'K,jm(lNy8LmVL-goRqbk \i-+qKeCܴ?TfnؼkedD jM[*3(E$֪bӐeM?ۑr|׺/>VƢ )VK]DbjVcJ˓,(p~Z䇯@rډ(cƱXpeQ֓T*Sڐ{V} JeRzю={=?cqykR;fH;w}rWyK-XwîZ}\HTP'b;ʈ+T&BgɧדԮ# 5]w@1oBbK dӷ/2Q_ul+ϧE"[ltf*S}j; OD9cfﱞ2!Me3"BD02G(/WWC_ !}M„%}?`T 5Kvg'!8i σ$dq&;!iSy6y'j XO's""vLtglK3-cyYM>JLԢ=_ҤqgdCQ9G$"b'+MHl5ւ/I)ܝeDd*bI$#"Hk3%0JLDN\ĕg* R#]mDdjdzw4eM3?^kl1wQ/]z?ivQ}^н8 R w G7]xkéguDD̵ѩ#ɐ  RkLBH(Sܞ+:9W'7] iذa<ĪeᯬٛY_yxo{Rrߵ"3PI^L=\WwoզQ]~E@d \eǬOx;@ODrzxRB}wOHLZp6jٱ+<&YGx]~ꖿxw ){W*>݀":dcӌoxUC87;Kz)q!]#C_a;>T9=j\;B5}M‚df{HS,:ϵ&2"PI7D$O2א%r#Ks_sO _(vXTZ_fB!1a׼?#xE`YRbz":xmرU7)vAswe5YKfMxxb:̊yY"BuA=>`#|C7ߐY& *_z;.x8$Lff#ăY#< yNյ\Nx{>*)RMF-~I1];qˏ)[,]м3G-T͈0bjYq)QO{F] qݡF>p]Mj}Zq)\ܰY;gRqҏ߲5yl8?-W{.y[sL~k:::Ayʕ+Oկ^:b##\~jҷ3Ó~ƍQF!- "xSk9%"goE[;jm!ʼ3 # ];R-DRmzJzgkLDy[>jpY 숎ȠdJM b9ٺ.Hl`ѣ}KK+:ibܕ7d~dSIQD"V`x15@A/P H.J. 0T@ .{)SLK|ao>eʔ)S^{ǘϣ_kouVy47@s~!:`L}yg+ξJ r'Os?:|?O>\@$u~] aKq"/mz{EYX&o}v|lҩ*=Է S-!% +"O ~ʂ}^z!bGM++8WU__ϸ7lg6'.IbgHo&Ȗx-MٰU9Vx 8Rrlb~7 #xTvODWeһW/|MzS/I&{ӱ5=zᒎ'"F.!"lBd~ޖ:thG72>#b鲋:̋Y>F ^5۲gGL2T.O/슐JLw6!,"W{,XŐptɦ_슙p1q^YϿ,HN-承o%~wlrޝ )d슙p1)-_d"v}2=3&8tC֧bc3)v_>bϮ tDkbO)#v:t}.G4|@ͩ#~W;WcMT#j?IZ1ϵ2]繲d埻V;D$w;`>kӷoΜ֦&",y,e'!"G7S}p,Iw̋v/n$3a Tkꉯʻ(8{ȉH2n Xٮfxk,er"bR}Hc I2}՝eXj{u|Rk,,ybɺΛ.-˾h8 >NBDL2\vW iP}P-J] &RǭA/Px|層Ľ5 Uu>!*LI#ƕ%%'&HW c;/'"]ÉvZR_//F`'z1Lj?z_fn빽e(;"]4~G{&bkLdLT*e*]Jo{'&e^H'׻Gg?1P_/0]EF*,H6 $2BW*݀QP "aT0Ue^MwfH謅*# oٲљ%LQ,=2y !>f1 ;xUNÛwL]WW@3 O۽2=c-fmHvuP[vӃ*6==e}f;=B^ ^ k!0HO'D&,ٱc7߯Ggd2F*+P z=KDT*2a>uɲ`H8 zc&&ܥ{ˤ^z=#gy5$""Ʉ`Tj)f<  zSʉHwaof :W+brl䚽 rX{LK]Sʣqћ`*yj}^_SU8sD_ڻ  d R&!+nleHG>]A:s+{J?QUf%G)WVv~ w!5zmfef+!o\pskޜwSWY8y/xcj_{* $DDvfQo+69ZQu7)3q|k1Vc5.aiJ>3ZvU}KOHKp EP IDATnww#l,}ѓ],+FX#k 6a jSKg򯫝\җ-U7Jz1ܙQ=/os)GK zfķll8s "j㷼6\JDffSFtֵ19cqQ˜[|B lU|v#),A0X5h1bAi:_h13ѷR?߾_HlL7npe_ImLg{kWl8ɬף->zc5yD,xeVu8M51pcf^7v{y |^$ PPjo9&NqY6XD˔pS-3f(Q : 074kߍl˾nꉈZ +?)≈֛D$7b"j~ Bݷ 5[DddaibaLԳ2=h: oXjv㻃?~dߎ4b$sgե۩?NѨY~'[foi?3RZqrU:cm9g.6hbu+Ffmn?X`a RJ ? oxH. 0d P*@@i>sW޹}R[S֫|vtt 6  <lӷ^v^[^˧F]R$*jZYpiIBm&ZR]?ּVjGRal%"ov75aer9,QGD^MM2WgTnP\쫶D4LBeߴ?4.5ڴKCՇ2O9|Ѱ̏ ߷\0^T牨\_L&3le/f}Ƨ+or]G7gf⨫{ $_pkդR>~Z#Fu#0w5'#[RDU9j+g?pUw\{ЍF"j>T_@&H̘Ԟw=yH9C\Kmկۉؙ@d#U7-h#"j{׾}Cole1zƭDDM\~;oݯ[$\M7WMזР$9I$*j+ir./1!j+r~9/="v=x6׌oKVm@I}K oZo\|\3_)_mXm^5'"cq3"%'EnUlMK_.u[ikY$Vڱ@1F$dgZ+'lV?>&xϾ_W;}/[Jn|,sgFIpK}ϥ,K.FlYҗ'\mOdImLg{kWl8LssxXMy_=Q;Ki;7GJX$agŴΗ;ZL pˬpj*V fT1=n/aܘcbboK$caoJ U?d_i;=]7h2}Nse %"92Fn,~{f<p ~uc\rrmh "c"& M2zqAq@BvuYXX[e^{ZWu)$=毳-'"j)̯'$ym:;cGxM-Fhk?Jki vܩ /V[N2vb~g&\/=urv]l "cDgbg?x0,xJζKS $!QH.saX'ǼG:: $|#F7@jooqƨQ\$6H&P  xrM  T "_WV性NPT~綶7P~XkذaÆ 32226660lذVA\  Vbpt ~ 𿑑q~ P$]'T, W E 7_5?RhX ~ O ?} gM06 ^x\@rɅ?Vklldjjh@B@r\$\$O. 2Ǎ="( z4 H. 0 `Pn>0LMM $\"E0T@ <}/N[RQgv4GTXXXQg=gh&@rDSTZc\|X H.<o^.22222X-ZYQw:1<}ZPȒ^ ˪grw;RGBBp6% q%fj`_~EڧJ67!oZВUkbS ^oV SU#_߭5MJ[{[/V"47lj6-!Fa3yH. H.d*@ UJ9bGKtuEDDFFF~Wu70Q}A~,o{TddܡZ"k_|.*222jݖC?6b4x* ?=4)Vq+/[K*/Eۊ:/qBU^*?uh|VXq:?znxtbXJR&>; MI ] K)+RԪK\nJR1%; [θ͹zW}w^93.1Bd)!*bc[af O|Z~g.7/GD\K}+LPesQbYYvыTuDF' [(oQJ[6 x:BXp <6cՒVJHȪo""HecX*ϯj"-*h UWcFJv Z|sKՙDb9yLS{73pcAzymsB%9%DdBDME_;2`w},ʏW];![E wF'vjn, ImUeqJR{"Cfؓ1lrJUǏd%kJ"KcبG265^#z}3n_ZG=1wml(Y)eڔ*P^UYr7NEF:OZWuՂc2_[(LTW-/0KՁH;8[w$z!AI_YYReIH. H.+@rwf*rc}R;v#R.C}Ǝul'"ju&.AAΜ9VDT_-D&A. |GKMg[5`X3g:ZQ>Eb3֔[y7JyB-1S7?|Ko  {H*r28BG IRVV9%rlG wCD:W{YMǗh$SDĺUlaNi?Z[B ;_H"(t:^(/(剸Œ"ZVOTVtSqs[qez$NO\ԞD#ɝmtWofV&Fݧ %N\sl{ dZ-dDvD_{Pw{1n*K"8m*rJ9J0ҬBi HL.SGpqƕ%H. H.YVN&g _'|'P[͕dk>*푬Ȕc&#"SaZD׉Z8o>5hC%j鑽Rn4U`[.hgJBB5[4Y9% eznc+2J+C^K]dY 5lY9KD$F5 u} JXF([zF:5H[rI*PQʻO`n%o\U1X{)$?CJx"rJQAG8?OtuIa #e¼ xRԊm&G0 Ou8))I28+Gr\$t ]LΟk"|';_o,Nd?/p_޾nGIϱո>&DTY5v]H4irL"ˢ{@3"ӅI;!Tlr uzwr vR>J"eI8g8sqj^9.U L{WYEA#2rn.#ѫWww|7-c_dhĬ4 ރpP{ }=϶JX9Wtm8m/!b7&yfF'[{3u342 k/NKH>*:qcz}n׮q%DSo8]x La$;H˺Q8yOǮٝ>Iw $Er#= T,VLv2!z765h eW5V63>&-*8~zvD\rK߾=4xEkʿg ̱|NxϞE'Hգ˗O+T܃A2*"OVd67Csy=$]i:"2r8m (I&7ͱj.=WxB:E剈jRVG;trq,M-6*O&-GKW[}BGD$uZp;4#qb >Z6QB6^lnóN'tί3*X7C_OV%"$#C8ۅ%"KOM쁸ܧ\_DthWxR !"HYN&tgh4i.޳yghҠ1{7nu ?iH{fOZi렩˂Al)hRx @_Mg*k 'ФQI=;sݹצ@BoJܰi*m"%L\K Οa^2,'v,]3MWxMRVΎRq{DHS$;wB{Rkss"^Ү^kis|h\/B3#fߞqKUZ֘-nF?Í+7.l[RTs.YYa75j?UP7fK˘E*o-*.paЦ$i$uu<BrErG5矹/nijl"ssSo?{9?mmmmmm <OO'qW^1b`o~~CZYjOUUs:i4h)[rmT=Z6{EO{Η$]o'xyMtP[|Fy饗aoܸ1j($}ɭH Z8*fJ!Ar\$ErY[~LQܬ^<ǥZƢxH:MqG_7Z㷥hΜ/IK8.烍:bK!ArEr̝1 tS;7oS@c&,Bz`N~T)fp96KP?/ $>* lD"H.{Ob/P" kjj(  aÆ h$DT P*@U<ԷA=zGI&J|\d⁘ďwV `=5yORP;7lͫmAC<E NJ( U2'p53\onY8,Qfo>Í YՔr[E`Vb焨]tXf"u2}4͵rLv1'ƢD1)(d+VWgφ w5[| IDATyCZUpo.Yr;؞qU1ٙ%:Z/*lM=_eA 홝;{M0zŴwq3[WDb撂Vw#+gR5ںT9\9߉~CZ""j8ss 3M)`-M-y HIJ&D\Sk3UD^sJ<+[E{XWoip$r6ndw=K* \ǻx8+ʫ*/Vc}f:@R{:bLW;WGRg{*B>t".JܐRеZjoeyeT\DD "ެm=-N|-(i& Vb\LD%;>.'*ZܻsDĎd hjaJ54Igiiʊ&{o䞮ihZɔտ'Altf֭)<ĹέM B mDDKR˵S9UD I)QlE'[.?_`Z‘4O[1Luj;q&2*fG5s9/#jj9" s^[jhZf"Tl:ҔY2ĩV?)U H Rn4U`[.hg#m}С/lu4ޔNV5|rN%MDDVGwFL""b+KUg8S,\}D*(/@94E%󊊾=pH4Jl:H^5Hٞz *\\P rǏHisf)\aOzJwwYg=92u"]wn=N" '9ƚRMtyKz;Dc/3w;92QzoJd:sgR!'sXJV phB7`y~l˵49/_:=Gmmmmmm <OOO S C q]]3r:ֶnL,ix'<#3қ>M Cv:+I>'@ӓ}=--#_S;MU.SYb1hMQVAD3PzB:fSRrP{ؐ C0DT"֯_6/ԅ5|:rT+Kvʗdd47 *R"JS>UKxR9tV5/ՓZ޿;B1OߍN9޷i2'>lUX|݇$xru|Ϥh pZ%αg=|:}'?2yEs;kddd׊P_]3R`vvRҗ#Z4N*wVmD/ A[#r]I\EmqTL]^N\9+X #qtrQEJBfqpnnN4wi%~N|Όj(1nlƒTv|ɶ|NpCӉnN\ܝ g(!1HȺn&?2e|Up$w 7ԥu2ItXZ? =5~N2w']"!UL zOktN O#f Q0. **zMP% Q9KS"t՚,DH,p4MGN{'ݝOk_U3WL=+WR?iO\`ED͚0|Ş✫~￝9lr¥Sm7Ղ|v0Cۼzs@vs6&F"+bOX11E=|`Q~i J#vg,RpYyYxҘhʔ)XOhoookkkkk+W<tB Q+ߟQ(@R7?]D7*訳ƻ>@D,gX$/D:}_s: U rJ3-H$&M=s^ ݘ|/|o;zw/TD_ϸHnh4%:rI<”/J˗Bݙ wxu9H1UYv}=C !C &va ԉlR"Y;_|x^W}=xV\4A=|c~Us.?S6S߿a\9p=n=Y+%q;\^ \q8dLY?eY`EϿBWKfʑD%zЁϜ4=S2#=W}ˬYJ]enV"y^_}HL$tЦ* phE2\-}`ܦҹ!iE)G}qY$ů xċe _-CkHvjPbxl_aTyKVF#%8~eH{Hƕd%nN;QXwy]HdZڝop*mv]qYKWjWĊT(h!H%2@D ˆugΙ3N>x7l^thl[WcΫӓ2__{oaZ>;e ]pH߈+g/_3I[<щNGJdQx~6թݪ :p{u>@˟ y㎨cc^%%}Yaji̅WJinZ?&@4 گCxsqko9g^J#s5un3S=:ⳋދwIhЕJ5d'|/%s?E}@2it'^wPEM?GBI^/l>64{l1 ?7t:o$_7b^V7nkE{'oXfچFpz ::jaxqy[G0^km0L+x5i˦}xilk+_҄dqt)6ӄN 7,h_Ys,X}C ƿ' yXZ_ԍo+06|ԀQ|4w\ q^!a/fY,#=yɱy ە6 .F6DfgŦK 44Y`4&@4|J׀zt'R =ԩ4 h1=ĭxsi G,l6o#. k.}T YO pO9Qo :0WںjFN66IV`z6N$s~r d .H0ש .թê>#}֏6u=l ppE_ifF9 1縙\^ud%8-稝lzڮ[>VuP75U7|͓-w*>;%>O*MZ)kcSJUo+$*+z$s6{Ee9~?]yt>=;)K;ݨ'Jbݜ(D/4dL#ͼ~M0~BEgu*5Hϥ_Z3oRg^E,!,ԝSSD5vEG~Z}0*E:isGW{O7~o$ 9~Ƽ[x0ֶdϖ޵+߶4g7Fx kd4t*Ttm!@)>hȥ?Yjj}"W/}!'hϐNYtK^dO b쎳G`D4n?/4SϜ'G^gԑ+%KNsfk}Z K{'ý7^lѡ3Ntsj@639-v< 6mHFj[9lG.,<7i_;gKKfKlwqYٳg[r ~4g)nn*%gξ0uHLŊ/˃WjрV[-{.70nOet߯UW'$2@{a'0nӲeh7]=?;?/z?}\;;͋G Xr z |~B\RO,J:rӨ#R^?.VeF鴸Pn8_)Z&Ծ1qsp}$ R'dl7@1omn{Z=P ~XW4L_yWdˑwjq_~8ܮ!!{`64aKi%-to[N}w?y3[T8ЫG9ޥN/%/>kvz7ݛs `g6]&eN":/V^-yv.}_~}E bŤ #Wv.;:ҷ?y)LD:MU34O8GQީzԩSoP̚7S]":'"[7|Ӵ/24Iޮ[RP w~ܿCĉ'^' 0N<Ͽc.3O< 2ׯy,v}ٱ(1fڏ8^`T o\Ȋ 검]]]gΜgy.w5wXs׷eQVy\Yȇ:n5w1MB! dž ol6c|y@˗/7444tvv677tc. ,&L!?8N7773 c2EۿTmm˗TOrppD9E;i0vC(ۏ;ˇh&L!аu:իWG!HH'"Z[[띝,8'#:;;0"B@KlnnV(d(ѣG[<3 IDAT<ȇB qlnoo7$I$NܻL! F'v"B!4Cl6XCebGY` B!4_o 7aDa B!4R_A37݀B pp7aCaD!0(!1!0"B1 0!! !ցS0aEO,0Wl6L&؈3T566J$H۷%B X݀B'؀G5B& SPz=7i9mˇ5sa({MYWpWlKd貓'jOVpttT(QD B!]2|pDvskB׺N?=fד]E=p(B!FKvv S"?s,4n&m7-SvB݄ }SZ)߰ P3f\.wf 9}|5В?W>T~ΏY3!fEה` [o+5L'3l{a od)v-5-`7%fzg~ TBaD!Яe1]O皝,[-/ݔ)5|@BݙwlKjvn(l17;CMe *w~soOVe{fSnm-٩r\n'7'TnYiތ٩G3f[Sb,W{E!L!zSly%M(w{pB@&>2aʔNn;7ȷO }?} -{_sٹaP7nBdۛruP'e8)'bbڌ"Gđ!0 z,83͖+,0L]]ԩSdB &d2]paڴiw:8~y<xV"r ?x @mmm,Fٌ]"IR PeccfNÚ h˟h6|&L&cEhѰLQ4н̱,[__/J777[,_ BS,!L ˲ " 6 ^z}{{H$"I$I p B,|N3LN谶&kL4MKB XORi u:Ѽ/\g  #E|KSeI\<ab}x $I,ba$Ix<ˡOm.BüŹ@pa6-?æFeJ ZmP0j.MӖ,"k#ks"48\LhhkkirK_x<A}d (\ 6wpa41!`Y,5!5'ŚQk! `YF!=)m.&@4\f"m.B0#z\!&@Bw)nhp !Bh2L8# B B!#B$555aQ =MMM4Mc B B! 666d2a j-X!@Xq\EEEgggWWlqѠ݈L?H$BBAX[[D"d2f35-Hx˲]B `$Iܛz;z'hkn~ٯ_?ag!0"BhhY_%YnaFxsĂB B!ofg2x<%b|reZFMXJ!L!0e !0"WM1tuFGCհZߎy!2 !t'=S#B! !<́R*@_jZ*0ůzUv+TmZd4/XW{nN)aoqo򷛂[a9%u V bTŻҷ)P@,  aeD~a;F~?_uN3m_vƑ?oS)jxP77yRPZZZFcuxBiNٖ[TԲf7$<*b)_ڲ߮Ձ5R ,46OpRYA]@[5ǐʹKx`Kc)i%9Rݝ.m .cĖ'K$zqH|b`jՉ9g SJAbK)V8v㎕~,XjԻ~WxyA<W w[vm{w=%囫O`N <߰}ַ><+n\7@O]˲)9r1hˋJZ4y_ƘtNrr &@0;4{ruqqq0v2b J}WYmCHojr@nwxmE|ՌVnmo7w` ,hdՉG?8d{HyI=;]$4}A֜mdſrP}s`X W*ZuMAa/Y7)ukE7^;#:&@<.`EfV'ݕY BLWV)$GCn2- <`~\|Lwbjn+(S{Dizm{KzKPDl\DoXM)ʴKPt||w[jUk@{נ@@J@fA5~è糾J_ {:T?//HbI}L|X_~y_?e?ZSs?gkknQƆ?e- d$IP]'6߰ry?iWO-rW]x܋o~]JDtJ)?^zCkYO|O~WzO2)@<_IݽLm~{ޟm$ ;f@߿^V{Xw=TEz*(9_a!`(WY^Pz͉M\9@Zڶ25 @}DGPЖ Pˉ {mtsS3*,%`NlOc;Jnє2|j ѕ%TmzJOۖ_PR踸Y4tG+2iKAWY{–diz(\*b?XU {Te){qwؚ槬H-g,Z$!Ky뎭\*WRtrOJ.%رu]l(-a[ci.YU! _#'g(yYڲO17`Y!Z\z>|F+Vn:pV=Ԑ@OeoLna]SYǴ4A\vn:}_sN|$I3?~GxH$I.&1y?*O"]=>:chW}kFW|$I'9e?Hř/bv=͢ng\*;/O".5h&~E3j/WX?A7;L$I6LIxϿ^*IǓ($^諌:1:=a'({_GSD5IW}(0ߛ o>ei7ر9qboR!KڳjIJ> ns9[ޑD{m-VQ[sޱ9Kj{Ҏnܼ.8P{}rKXiH CIKv)ݣ7ٚN,[u@@E,= K޼5>}>@b?i] =h2KX0WEHWZJfjVx!Ґ@ aQsTa +z(J(] e*H/лD' Jez2dQg5zNnQVY(K-G^ xr3Kdt@0Yc%u,8N3ipv0#*$Α7P"(J,yluuhwp~gC+tj ڔ\k~3xA֎daV Ж J-]z7Z=hpڳ+X̐A K^9K0cydAZZ~t`B"SPX*EDb|kIU!=h @'bD2ܘÆ |C,("JS~}xxr eL)}E涔 ZRFy!^I{+,0G@p}7 w(8dF&!)sR`w< zcb-&2ǬTgg/Z1OGp&]{oĸXP.S\d!g oEŽvc5v$iRt !dlW[j.A@˺P,U#{]J*[HPU%z$ îպpŝ6OYׇn.]%GLw{\BRMYnȣ{ǚKAۯ5f4& E̚UT3ĥf#q _@Kd4AXs,pho6ycH䦳2 AF nnLW 5~=h$"Mk'kn7r"L=9 ::SID %.#bt4qJZj 3vS5"AIc>(,eeEYNcjl+݅riB @-ڸn_;uޡyrKҸ͉7vRr 'J|UE==*"\O+ a3n"d!!7diA~n.p2xJ&F]\ 'PVY%ԝ^"U9#y]m.PRgS@*{|eE[PzRbT_ sWnMmmO?-ct;={\]]kkkQsb(8Z Wtk* uO5G+!gkVz2V٣5L*r7WWh2tq*i[+f3\'R:^80m 0:r$)A0&H"^Uk]XK6N'y VܮL%%+uuc4*=u)wnh*Н[]KR[. (:TZRӺH܃吮K]]e?R0>88(Bwa=(wBlInlAJڡrFU{4=e@)-Vi4ŹA_]1kw%)*?pP*U'=pVQUY`]Ow +-Y始_$hН^bT1߽3Su0uu `M:?~[8;qؚ5;v0t4KȺGidͥܡ,5%JQUݒZ^Qu@)8]UuЖ8WA[ IJ_wZ4%TUj>r劝Mfee56MǽxGկ( jo1cOKK˨Qvvv~,bN1qcm9g<9I<cݺuׯI]x[5 ۚ{WϚ>k،+:U_kAjomm%! ЪwtfH(q;sQ֝T:uO&OKxBXa ACb BwѨH0d0H0؊ owLsZ}u !ͻX˵k1bkT*~Q8 {Au {3TO_BS9Oݥ^a$]FNMږ6+Ʈ~Wdb7٤d;;S %Mɩ+-!d2F=ƍ~]3Q}@WrEgM?5qD̢Bdq/BL;p$˨.rN3v/5m=?Ws~/n#L r[?d2B^[Oq׫?Y[]`0Y#@(yoc͕ rat熪MoΞB3Gptvj AȭjLfs`IXsѓSW/ %ڟN /^QY.RqI΢իK>ֶNc2]P~ӳ}Gz0>]w|.+8Tog%@&@\qfl6[v fU'bR$Izm(`4,e2x<$]P)F& FD$@#Ǎ)2qQd/ѿ}A# qk$>:e*&p&`ہoEpAO4u0F i&^\[ <d7Ý^n#f#&c[[ *5NҦn0,pio|F?ݿ7;uf3&@&`0477DB>7̌N-G;$<-D"geO"4_kkKuuk4ſp¾4ԤI/o~3yIWicW85W)uG~Tw0| }E(Pq} 8 #<n'!ܞ19تiLp^hO+L)'[ѭ¥WTV"ee}{GZs)⅗\F=;}+GNFtp; L B830i6$/"bT*"hdݥo5+mdi' A}ڊ(@y<A!dEh|ҷ+?whdB+̦ӵ+PYyï+vĉBl8]FLqF1m3Bfe+XRvUg # Faco[{BP@YX5?_f4^Iu֗wFAK8F±_!tϻ|S=yJ]]&X/ Yv'F>O#'LO} cǝ7iA}l64*1=/Rg24-u W=Ƹ`#L ®. $i+M<ٳ9YYkg_jMdn'M`ju5 -I`vz}$E"-R@ y<{(!tuuo|;2 8<^ee8;[qKl6$yv?_:Za=zHl2[^;R|mxms '"zg+;: SGi f#&|֎}r#mApg$i2{[1A4RYK% zƋ@"ַS !ws=@ (Q|<3ydQ/K4ы7c,1!0Ap]PW)4APnjmOTN0sHJde%i>; BQ:qxEyy\ܪg  }L&{W&M򮯯:8=<|%C!ՑSֳ G̩;cRmxImc*#GHm(ZS6^k;*kL9W.g3=a5-VY! y֯\aDhh",YnGd);v}my3f$҄ @!N99D ;J6h~F('q g&Q z>pNKBhHڼ{y{6||;㸮.cccիjk+~K ;1Ujw|D E_괬;/?+|ʡJ[Gia yV0wE͖'!?`C Bo4ǙfeuzNijH>9|7"UlgWA8[ɛH6$}x ,@J=Zt%#\|ǙA$ǣ,cE{P4B$y&!BO]S9S||@ͷ-Ocq~w?j6{+f2 +FX1k--H؞S>.O.lB76@ ݵ jnm)4s@of kȞkrCBw9q]&0&yhNk9ErfoU{l?)+$sgvVqe" ]D87Z' #)3I׵4Ϩ'IOBF;^ݤ~FQXbee%DB!EE|>g%tk9|REToK,}*S~lƯ=tGc^K {,v IϢ7z8ᕁV/+@WV.Aq~`l0bBo<]sr~)wwffMϑ'M[_ҌcCS|+$jڟ{z8gYH'+ĊMwE?6O{szkIu9{z W[.ߵP6` u)_`6~8㓡\ip,mZM" qѨ1ZLF2 JD0sz+`ݷ׿Lז|O͉WX쎕׫ݒ_o7)s o>&@:e93L6]Uuxi62]IPwthGDE?-4BtrNsgO +tE_u\;u` 8# 0CrvAK*,2I1M;ٷc78J$h$iѤpflaS$Ư"eCC(D<(#+?9 @mnmVJrY,:8]kҌ;^68e~8'ROW+r5S|0Y-Y;_>QGOՑ=5.׶u8H>vv7>5$u[~žI+go綠cwO [UwNA,$pfCOLf3T8L$Dܨi丗8uWu8e(@h_zֶ3Ə\Sg`X)u1 "Smˬ w&+@D VQL=|=Z"y``f' T]ʶwX;uq=={B7B %ԑYJ/]2=m:ØցK8O 'N^l,XXƪ %k< jbEf+Ň9kS>B:})yeF |@\|5-XNfRJb%Rb~5%%QzIf&9΂L%JVͺG%'{HT'Y^D%&{лT K@tbb+Y|UAZ/]-]ynJʶ2-% ~D-ؕp@˰)L&χ[t,[QQovuXhBD|Uy՜\I#1O~9GH hn<}GfH$$bjL,s<(5MԽZ=H#(bʤej=>'q@W]cj)FLJ{Kկ#ejqP\m{ʔJ84yr?՟,k~|r~ϪzXFy3z0iWi^>^f@5'>1S@JRzR@&rկJʔjVҽ!rwwQ.hnV];HXyÌ?=ERNsWTL8J1g9՟3sm\a85_ԎxV$BY0Z 9ǩN،"W;[oV^eG)la__ʊjmI.6kϥ9^`G+k5./k|4cAu]$%'QX8!+'%Ŋ; U[/IˏG]ݖZ1gݳbIBWjw Xu*xǦU$7oݲ|cIzQĖY>-i,eY൴ gʼ}o4śRhf s2_[9zqRjQЦo2PnAZ~D`W{F8|d>뒀ǹNϗM&+V<awG;;9pr1ig[9X$K$WP.Aݭ&UfIȘlmmm5;zF +@6/tHM|<]ig$4A.IG,@wMC@LvUJP N̎WB"z kj6@|Yj4 DbcMJ8'mWbF{O}u/_##cO&}:5nL.V{Fb䟯h^?F4(ߡE#+*)d/u3oHL̏ 5>aתy58ʚAJ헕&AK,k$O UfzLmee>MZ"hU !sIĒk-JE0m]T>:aքs6ӫ' ()9TOWZ Ř,ծ*rtP:6`z}=C1* ރۍ>6m;bEyrC# 5x"D 9|i]af *@=/?t^hQ8 Ikxќa0=,戰1;I $UeXQ@hSPR#-^ JC1 |~13ɸ +ʇAnrs=1LH29={|hlO0'DY׈F<2 D: 0hl ~h xE9Xć?3u [GdR)&c.U$mn:{ TnީqY'w;c @N:ŅDAMMgצR  IDAT h zm g53TlMStE'-$(Z2jyL0_8xk!v~ﯵ;ZoY{B[P|W6hT@[A0Ȩk^0 <Ś&I H#et:uk"ï_ Q4E0='Fg9E4I!s: T3rWrjj2I`flt\J ⍅Z ;e5'3j J^-Pp $/X_7qX3+_ ؇^'r @!A\>2ʜ:9b[|t5̜yBo>q0{;6Ď&ÂLy5dSo-sDX䪪m٬/14kx^q"JbARCp . .XR_wE?ϐ[ۇ #x<  DrH A @% @@< /@ dH7,I{յߜWzG؏n yDH~St)BD0a{Zv3nvZG79=4oiT;8 F'VCUpG44 Wo8bklsmD +knߺy{Sx޿<ý-C1_6G)>ༀ0XBwRRaEaa7jEp5O8//!ᏬX ]|[$D8Cn]luK\"ZSӵLG}kUgWgҚ_},F!/- 3Gϝ5w2 8Z8qÓ[JVun͒2@h`ilgXAB#0 S BKCd]k-;cl}[8ͥl}w~7?"L" \Dp #%I``\N-{tĩ0o 1!^ $' / hj A 03CCcK'\;021aH9N4LU! $ ܿ%:Z\J2h$ ֎z*-oƼ 4Mu}5BO5?^j9(U@ҹZetдJ>/pc}ՎUeyŒ;I-ˍ!6( rJK׭(A_$,>vɒCFQ?͉?17OH0$P>8g1 D8Fa# W{{X&cC^{`,59k804 q 0ā$HH#q 1D#Hl<$;~ByC&6 ̥UiT&d2L&9`㾀}mVmJݮLab/<GEE}+/Pm'PK/F?f ۃ¿ ;kp#='럈)y?Zix ~~7>>.hM++ƱWu4xTSb'd''mgF,!B@$9wCNf?$Ra[Lݞ-It ZP  \U9H,T&}98`F pQ2.0Q-pDDZr_ D"\pǰ!Yka7x^Ë/0t#oa0@"\I.v(@B]zq<[omѤD[=_T9?,n}؄+1CץÉ.=upx6~b>qfpc.Ib߽ObHM \B6rrzs%ICo^|BSS҇Dja7}rX7daO> b^*䔄$)%R4\*%IR*D"Cf$Xh7##h|<0dQa;=pwtkUq+ŊOv~$Ka.HR@ i#ao0!:! YK@`;3޾ JL[ar@,x[|p( %nCya"zM, 0A,F_bidYy=x<@H$$IDwxfp&rLq Ex"Ǐ5niӃG'%Q᠐rSx}KP@Gl0 UwKT//tw<HI{$+{E"f | ]$cQx L,|9sD@* "T (K m͝MOb7 X(=( Ug,:α1ǃ\.W*dRH,.\7%Pa#G<w:GǏ#"##$IR\,%w{>:!z+PF'!1#O`pa8($WJvf#1<<\EJX,HU@.Aâ{PW?#:ٍ0t7\ALoGJB$d,s*`or`AOnLN wX@p_XBs,;2>wȞ={謬p2@"9%={sUC/>8u;=￟˗/I y_yr}3+=(<2Ld:xg9G]hKajګaa>&1j_ V>JT*@+F* NAIs$ru–?IC+ 8@ sc"w:Μ/yARSRHqXtz!= s`+B zaM꟩ş4rz\.E FCq1sֿi.( !y? GK]a,J$bHPmor .fl"l~G* @`=5ds۩*m 4s"eTt-<#4>8ճ7G2A|xy P@fg 2N}!PFFssr,$8pbY0@y>!XDҀ xĻ]1<>0L,!I"8r{=pΑQ31!(BÎ2ؘX,&($qna0L,d A0U@cBNflg}5iZڱrtttiGj'BhdddpGjk^:@zޥ'9u1@C |dC7zLy6$$B@*B*iÜ'Qp39"0+ ! AQ*A ̕- 8@#T:kőCh>~X"_gWשSn+onnfycؘʫ~-++۟z0z?>fnllAtgyrJ%EQ^wnzǏ~ G}".lnn޿LLOSHhtlpbNzڶ^ 6>; 3VqM~g gGR켂Kɝۖ(jG^r~f.=A9;sk:sv$)|٤˲oqomM@>QQ׮]+*3gN#NZCZV>*z[_~y۞!b(1`"Dȇ,EI,y3#" \=7ЩuL[&=EXZK9M;R.#|EYs{uemGP*}bvZpJC ]=eo`rˉ'yo: ~`$Ap,8z?9z}v^X`Ϗ o/v&Ina!$KKK?ۻwyfllL&glO@O%!b_at\Ox/O~ =gMM aU*L*~ߗws\?=}I5 `ّTմ""bűbMö$Eӑ8?/5A9=Q } c+oCEy^^8݉EFia8itԹzGlQ)ɧIJ,X`O>9z@{k/}r|wbwT/(y B=mQj~G6ϛ'.X[+iڞponNMQ LmY5!1F Nsw}y55hd*Meurca~as N \#f"x>pHIIe7 og\>~7ʤE-'Nd2XHHȡ>f69Cv;?m)->}XO~{R("Ix|?Xg50?mm2$DJN1Z'yTʺrg]~Ƥ[[Q g_ciimٱ 0?+-o3n=0a{پ!R[հAeP*ȭّ>G%ɍSjaMJmaKӹykl_>@gώ}FPۊ+L 4EEɶu&䯶Tl?|j޳un+,p@iחl:džNo"ߖ$:M6+"t?\3k礩sSQECY4=K𬬬P8t5k7:,Zh IwRDg:de/;Ykro/ Ռ/"C}"8y@'i>*D5+ߑ 66%)XskyqPԼ JpͨLFhVpVV@sҢ3ݒ%M[HokV>WҴEۻin~e^ц$%s{_Q&9h9o,.up@gO}qyA9?q^Y߻c{ cI(f7Txd%a?R)I YUt[rUqRE@{nViiک!0ȁ@9Jo696Fdto./%JuͪU5l?iؑ6U( ĢMm褵e4|_ldMg*p8hrsۜQN$:{-6N.*4cx c39h}NZ_ՐMe)F \P eY} _5kr,3펱1χ3/<(udL.7#;p0݇e?xIiK/ɱcHO+V WCH)O1AHeHLR)'&A0pE[l/^{u0rxYD(ܸBV*ϯf2ZZZjrN'Aۮ\wMͽv\n6ҩ1ERipAAےRq`8\Xܡʫkٻ.W\Vn-Gִ4Ue:+Xs}~1l7W* mEEZV}3l_uy]Ph7glwvvy h*+KYptwҹw]G*YAqW>ضd!滌yTTkbŷJK>h>w^zm>\$ WÍh "勰lhnnv\hx L1:o]Tť]ΉpkZю*-{*30+Ե4Us VLK*ͯܺhȣۊ`/,6hZZZJ{f*Q^mIhiiiiɡZNpvCNMKޖ<٪N/+["Kz*/ib9c vFu`5v34_[\iIh+a8ɬ;kC 3k" 1)4%Woٽ$l-r,IE;w[)}-MeZ21[t4{ .3q/cMFЧ#鉴h39;w+v:78/.oUYg\RwcRVs]WL$ȇć}(5 h,7|>|׋08qFAA~a3>8&7G$>Qs=><801AHHHBwHyB&aa˟w<\UuH ^HP^]2Onkj6@|Nj4 D+X>eFUzZS]SOӒ6=Q 4L@ci*:{TjNHur3ZXkwMC1UM5i'h36uYEl-ܧ$%(tI:&P!(`^Q}<:lDT6{!a o[LtE)rkSPS_=̦+<S)e 4'^g՟{n߾c~2|Ц'4-:-NKΎU".$ IDAT;]eh!4HU 6fzM$ӷl͎!/YjofMͽeRACSIjRef'(IZg &lͭ=f'NټmC5vC|_o }f2ej5Zw$iZGݤM,8M6MNAl_M:>IY&/֧s*>7vcEόWTYZ]?ؒ1*۪gsF̆VF=1 Q4=K&=9.EOMFPtZ Ř3NPe:kWg{#k6/Яp1E* pqxk?y{l053rIJ288t}{s\oڙe2[[qao/ |AlfsXXokkI 8Ƶk_ڳ>_!{^/Bhi?1%s:_o5G>x~ Je$I E9jشiknj,hq88Ƙc@0F9'NKWiMhLOuawNtyy `L8PST͔`L ٭=2ڦ$cf8&&BEgWQJr}]}&p\9ALfCjF|>Sl͸$ļχFH@Dj7l8p]dꘁA{7:tF)Ʒlvu^G>wүrn%$ɫ1.tnQPsOp ጥY+'ǀfJUTAmmcb[[>gRœij$YTYQ;yRupv"tM&䥫QSV5u3셚.Mc49u=F*)[OwZnQpIO^ݬ;0@'@j0r(`; (&6ʰ }v"SZ<݌vW ڟ1=Cqv6́2.}C\:~C}y|hؙx- |YBq۵CB114M*fj-m;sFRhbYw''KIr|| EEzJ8=;9!)wðBa"D$^~r[R*e,*2?WcG&%Iϻ\O| \ԺWx9f 9etBͫM ):׶g6E'k{f&3Y җ`.ʰAqpj*ES걚kgY-V3CEph).}8ͭKc'_q3&EMkSF !O:lEg妞a7њ1.EuN@ga)52@ES$djcsol;wRn{fpc2n2)jsuOQʘm,l-F@.A'RF"ݳbŃ=OFDP2$<<|z[=4EFD"O junڿQGGxeO.\DV^|g4njye7h#,X+~{4Mcklsm,p[Onn6Pi5܍s88JIg߾z#qkB|;x< LM/]&]%yݟ7~|ɼ4vw<;z oIuDaj0`54(]JII{k,smfkZCϝ)IESw76-;,|͚nF79o<ڵ҃Tjbh_{ ..0qioi4$tLs'XtJZ;[mpg/U>+,ǽ^wvQɌ [+j ]#γ@G=l:~6}ӈw/9aM}p?18O5xsˁm45Z-^?ՐIj)c_ξF&MPԞ۶LoggXsG~>jU^ĘKss? ^N!/&9`: Ko_m7MҐײ # \Be9)R*,ZT>$ɀ9A))`Yg?<B"2(\ߗa 䯄r9z?QgVT@yuJS˪F@BAYNilx:LMy):(.)̴ A &Nl-*1^I^_!W 3tp&1,7$)Ta~#++6;#21'tAI+<(sWi>{}^baq'\ӛ+=b98UjAQ v}Y^yƌZ9J(t9j9B-JUB=KH]^fVSUUI%r@RҔ% PڕyE3Șl]qi= neAn )(YQOSR>Ԥ$g|n>Ei+$Д%@_Vis3WnܢHq{g+U> n}aߠ}T@BvF$N:14:2 Dؐź%AoZ5k@lb)ynSpP A E EEB\Dg 7޻Riӓ5Fȁ xMOtƐ0|CƮ/[_Z5I.([5!tUu%X,oљ5tuem~F)F$-f)&BǍ6G'$+s!c6TՅY.1(] i׬ (%jG<|>q˲ǏۮRv'N?Sx? Bq&Ǐ/ʿr9s旿f -h4 E"X,q0 y/8a pa8xGy# E88O$Dg|?c8.D3?7|_Vϰ"ᨨ/rr,?eGGG P.m~qA_çw@nnNLk!:JpLz}e߿_wC5+ϊwջ>D.ݛ汌M=y|J7ߨp)r+|n?@`~PvmzT3åܔUPsًƛ$,W( p -%s_U#'7!Xd@\0LJQЀ *8?Neh#L3nhKaZ d_1WbxA$9_x\n>Rg>xo|ܲ;D}? tDgcs/qkxԉl$%I^9B,q30D|;'ήJ.@X.vNP(~ $ v+dL޾iuqjUV:/Spw)8|sR<^ۙ r\S>)!;o5əAG͋M2rFLK $ܸvME,E)<}YE8N$I{ H$Ss M[)ZR 02v6|qwLJ.w2˞}b>q4s[ez+='l;Dg83u [xh( U1cb$B̟cHD14F_HJ}.7ȅ&3E-Zt m#J"IGH!A *@( #\ ),[֡<[NZ;};əo ڇDbQue?̅$~ 3LGϙ Dž( et⯙R8a:/$ }u˯׾cxW^gU2c9w}̢+X,n5( %w pQ J֡tJgpQظ8ŢV,=bG}^ۈ[~c /q,z~ď7:l?i<3X4OIcs׭#TW*SOXOHUa" 8@Aɾ^וwnYW9g}nZkL zlצ-п=k9Ϫ@%W4m*.lȷ5l܅}CVicF酈ɬrdWj{ \3$Vun%V4l$f5&(kWj/ I/XS}usRIJx /c: ]IG&%GG`HH5&L]QQeM"£"¯"̍T2eMɱJwԖgjvd^tT]9q=_]H]f*`M9&m5: @͙ٚk66ۨ}%8@np(.gdU|NAAƲc]a@#G#Ssލ-+%s > 3Vvm(Wj{cV/19 -bd,6occ`#}yU[奵6UbnQ^Zs[~iUDt5:ZYP4>9}ْQ5l;Zˋ 6Ҧm??w{YUZSqڜ̘֟y1K6%)پ6^3m6YYR6Vlcqau7C440#ügݚܦGdaT+ r&de\ ?_\ ?3-GiXmNQ&Xn8]ntlF i|ugzd@SV]oppݫ)hV\fHUT8CYB>@gO}qyA03g秂=6W?ZRO˴" 6wm ̯.Px@66Oa)c6bbl--q@VSk>?\fWm{l ~TU5ۋ?];[UPvz1WlN4yF&$'Ig:s&:R]{ j.&}>ww쭅ܚLGu~eĬ*IԲulܪreަLGuae @`붤T[DqQ"EmN/)bk W_iKhjTo,ϯЯkhٻ*GM~_dV5NublH}>-Ξ;Ap&d] }3Ή-U?p@]V@g갦ع"ٲ`S\iJ߲{WI`,Շb2KvjH_ 9e;w5[j{ً=᳉Pc}Ǯ"۱sW]`Z!luE:cuu/Ĭ-ڼ]Hg_h*+KY;L-,-- y*Cqqf;:leeTD|IîMSC@͔u%n9"B> |n/fj7.3~FW2.--Ny^]ISKKKK]mvpI Lf݁]\jjp&dx8p`US'gmdmZ 5q [qc%&iAƢ)x J=گԶh琛n9i>cM(h-p--JBs(w BT{$RwjG?,| _}kIY!)"1w;*@; wW#Z%i*"bU)RCy3KcY,#"/2Y#4tNh#dhiRD$&* !ԷHD؀iڔEt)F&"QC&%%XFDgRUM;X<21HFDZ]Fb(ŏ%"vR2[g7#ȈU+nRoVF;39BTFDDrBIMJ[x"[IQ'&rM<^~LԌ@> td]s %I`v"0'eB@|`8=#ؾۯḁc1'H"5C.0 I%]Z n# )CY%y|GR]Z+ ]-mN ✛r*!C&DD, ~X`7X@?,va({ӿ򌼇.L`aNA40lMKk"",IKF8I%&hr*Njتn~7ey?*s*Q%KK) QTPQZn\g0Kꈴ̔ aaw^~Lğg,jKD$YzДHfCYLhSy#yHE&+mAFybiD0! W#\UC?o,Q] ډ ∄[dO 4vDJg)_yo΍=69XpZyIY)9Fw!=6kPQqmgi)hKl#wCE ? ##rO s2&$Jl2'Ɍ4x[@:F~L#%ށ~VI} IDAT &6c{TfF.W ѹ}ŪM!*ߐkڛ6lw ;Yk;W-#`͉Q: 'rF9PD緼hˊڅ{" hYoPf|_1]]j&msRt]V]j\ʉu͞Ǻnpkv.DY1Ȍ TYFngB3 cX"K6q_@ĩ#2Tx,gAa?dZȼ*\/mJDfIftpJhVkO߻-B7"FzET<;%:D<49;;%XFe;D*׻vg "_f߰ UY*[q*0zΛ6NoYeOIf[vFySV%KȕAQDr 0 'c\ڶ?ܕb-gMeDO+(_%w^ŐRC)]yA-MzAd0ه_3F2.d+0q v@pϧ YFkZIIKe? Bȝ'`dDD]bǩ8F!O'YlnޮO$g!g!묯jGˈHꯅ:KMEE[eDe+VP+kH -b,rsi6|8WAdͲn;UDQOkRMXZ]`rNdޜ!$.j:-Uɑna?aXD`KF%TY"PY OpR/y;*#â=8P9ϐ]N)=B/e-:fWW.WNOm[k[O$']wT{z4u|w_!hӞ.c Ǻc6; v'ܾo,XUE=Q/?ofƌ>%+}fNjjJec[axǭ'~S=OvVf(]IxPNK/X4/",CųC&dW7wO{ "qpɽ㟾_%r}ⱬE^^\q\m%O}~Y}c~37^_Pc4cFndߒ>3'K55f҃0 ֿ?zVxsVUŸ?>/ %W骢מ(ϟ  NK~:csOD?|uB&qœErDpz׆Yܯ} Wi^o'>ތWN=ޱ#o$U܋x,}nMzYBr wL%{?qDCS-$]wfs3xړs`ۋz~}]_='qxzH%vi뾫׈ArP 2_e \4k^ً̤3]^nNj gW{]=mdRcR|^͙3vpѾՅ3y[^ը(5N"9rFάYؓ\@`^^ӧOWah#Q3[O^# u DN$^DDtܞAD$懁$kƽ]͗/w"g/nGJᏟv{Ev;pɝ6mjK棝<) 0^jܿ+# o}']:Ko</ 0.LEԣp8IDQ@jߛEsJAA3;6ݭm3;:&P$Ʌ{QcFœ,_2?&C:9oΜþ?u^~~ՐP:di|su][ 1snaUgHyai|~G4ŎI9s̙K0o2uInbVeĻ v'2M"vx$wB%۴#3a y"H.*@]fc{LY-.qƧUm2}73Wߔ1p1s5lSo$w,$WlXam^V<$\$E0t?ŋ+wst4ѿtZvmmOPL͋IXd1!U7O풆~3[,Ϩ">bLObtTCݝpT]9驫t1;ZŎI+m|].iGǨE;v: ܼQ;]cGx1Sܒl|$ؼ)&~2b2DķL͛7O3 0lHI#i, o5keΪ.!p„UbbVZLbbt16Uw4d;Ɠ+vUOo%KPmY\@r\$ D4],{[{*o$u+,5{pytƿ.}KajHl5W˲?_Eg1نtG>I c_\]]zӬMuw4^%dvێ;vkRͼH[ݬHLJ͛nomiVڰHkB\/0zK n}Ul6nfT9}{Wu# $ICYXR5ub ]Iş_8H'bHDoٽ"p͢zKk+߿-Yyt9Fl")@rfrrwz# tބ"&0ED̢mowȈ +o*֨lґtVg";k[Y,dywxR7jM좗dD$5׶ =~K.$V;1~:U($M%(NVD|sIEǭtW;-60z*1a[R]Ղ)dЉ onKQ~vR7Zݍ2o3n3h)X&_!II SuH"e!|sxT Kym}tI>Hlnt8Drfr:mҬ""Hg/%+ww|t:T]j0وd1Ӓ2O[(dΣL!#"l@׈6?/+S1R/nQ~CЫr<7,_lgKT$egtDo0 `a-vs;np2HTIb`e,H$u$ů|n̂d,6^81FwRHD12Eyp4xUXxxd 6|tI>+M|$4ÝZS\Ȏ*\$ErQha}g3{Kwf+uJ<=u9@eުv@յ{U61ƋD,o$m6^RxAM&t+>iL rl]?g_,Nq۪"g#V1t=\̄}HW[Y;]BE(Wb*6dJ,|+E9C;m롒d0fzZƓLy/z{呓 8Drhreaѿ8~`¶gܔ&$\$ɽ3 0e5ѺW#OV&@y='%? )BBduDUWiCKDlS7'+k0nstQJl`Rf檘]N_K`2R}L9wD"L;6 x+%V76IDATxSi.5";v9l#"fdo]c82|a]’*|ng}w1 'vvv[SUt̴J۬2"ORm7ֲ% #LfNjBX༌5-) !"H.*@pSmwV87q^@³]u.Z5 sdw3] 0#IdCʎ% ѩiȸ Ym/$636m:.aSg|a p7>0gNƈhNrR\ݬNH wY:Μ9e_ڴ;>2 fH. H.{?0 &Y`DD>2gNa^u  ^H΋$[sG'H.ܲO2Wv께<$H=@r\$ɽLP*@@ OW\qwwG;  !Lv̙+W)`NnΝ;q$\ŷA1>>>]]]d2ͣ>T*H.Nc|}}H. c P*@@ T PTi_wsE;^777$ErJrQDp8pxxx H. 0V & a<ވa$ErJrQD1mڴ~M0Z~iӦ!H. 0V &I&rAE\>i$$ErJrQ9uTM:@r\1hPP8sss힞nnn'O~0^vp\rE.N"oH)Sݑ\$Ep<=====/^(˗qwÃa3f¤I}Q U[ P*@@ T PT P\U_IENDB`././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/doc/source/user/figures/add_to_env/add_more_apps.png0000664000175000017500000011365100000000000025225 0ustar00zuulzuul00000000000000PNG  IHDRrDsBITOtEXtSoftwareShutterc IDATxw\I8 H+4 ȉ9{{Ă+`Fޥ+H/R+E&M3/^ʚh4!BgddD(111=z4>aÆB8ObѲMMM2fffPT̘L*>UUN OFUTT@E244χ<@ >1P >1h#' "'6H۟?b$қKJJ8N?6Ҏd|ٳg J "^x!BB JLW|>˗=?!DP^/Jkk+%ZZZ#~xm$\nݻQd "Za/_aÆBP"G9"ijj:s޿Q|w=KK39o<   Ν;`0o8 fcc`0yʔ) C7//`,^vA TའP(A8mm/ka{:]| b&q|۬nܸT]]bjkkڏ )ܹS(2>h===B;vo󝜜Z[[n߾uwV]?plՐ6lسglx~D"D"QWWWwwwIs}}]]]@`8x`Z:eO[`ZndO+W:ݍRVViiiyӧO/^k.KKQiiiBpҤI'N)+V#Tj\\ܶm 1מ h4{lllvD" Zdd9YKunzEMǘQ4%H$qŃ"͛|r###9rx֭N233{U҂5jBP>iCxJJJ3gμuV\\,J@9ijj紃ܹZdŋÿ$,,L<Cg͚%nQϣ SSSygD`PRR2r~%\.ݜngq{7ĉi1\aM}gЈN~rvHiii`` Bh֭q8#KKSNM4޽CԲP(gݫGn/ }c2:::֭/8‘r999󣶑tviq?qqftB«9s:;jGq넾DɱcN:%++KNEEţGW˒q9KK7n4m…999G&N\;>}:p@{{{@pٲ2rrrleCCCZZZJJʦMV^lTchhhDDĺu?ӷnrtt6mZmm[wAGi4fΜ . 6L'__߄|۸e˖+WeΜ9sQ*:qD--L???,ىDXMMM'''P{jkk׮][__o```oo>}zĉY\pE"7%%EQQ`(((=~XR+ݿ[022|aqqkȃ6$$T*u:::wލ=|0>CJOOWUU577WRR*..uVllcLLL#ԩS\.7,,ʕ+BdffΝ;޸qʕ+T*]--F26Ç 4_E& 0>>~d/Bd)eٳ:*;; CNf̘1x`|gbe˖!r7gG=|CCâ"@ ?v)~*++Jkjjeee}}}k\yyy///PDD'k-4w.YDqɜ9sJ{]~_wp={-E777P^^X~][[!TUUS_. ,Y2vXSsε'I{n6lPRR*--%ˉwy_"_x1ٖ3bĈq`C ?~D/B>|@.3>v@UI'#Dٙq:,d|Tsrrtsss%,8zh^ یsĉO. z&ۘwnO>}ꕾqDzjy3%⁋KUUU>/ѡثyyyۣ_[MO]]]Օ&|И1c+ـX,VEEx_SSSC#p8xȞʊBϕ tuugΜϠUUU Bֆ4KF<7@ũJ,>RSScd?~Çį}ҷQ E[[)q ~ُ>|bH&2qܸquJ"ԔA7AX,QQQ_1KY8X"ij yf$R砡/MFx1.r1>~xKh,I_߾SfS^&]\;d-壋y15ommmii77K)P5>cATH|_ڌ.zllljjDfngX\UV:CWWo zmKyHISpMee5kqx.,!Y?ݰa377QWWh\.[U4[สz΃z#9;c]."DŅy%;Nwla# #"UTWY+|[_Wu/"_v԰yY44%$$̝;|!E =/"qQުiXi'Ѿ2/:o%667ek<<<\""y]Չ߆Jq^+\R[[;~Ǐ]N_YYٳ1F3Hɗ J\l~駌222$ѳTb/zB{{Dl6>7s?a|+)ճȧS~qϜ937cǎeU$"T\jjj8v"J JSR#w CLf;yt>D )X/\\\WZW(رnm23m@b̩[W|ɒ%rrr/^ؿw9?F7< ZRRĒ)))!ϫ333i4cw[$~79[.&TZZI{ICC@(>yD<= _⍕WrDsBNN%yH$UyqĴ<Ob(-1Dw[k@>d l땦P(88,џ>"E k71h1b E^^~k[v_J̇ v횗Waa!3f͚tmm~ /\B\~]ސ eep |RtJ\WWwAeeeDhhsl6{ǎ~~~}y85JD$˗/3F|7n ((!4uT)GV?#={ O*YYYAAAClFdffԨ4MuF9romm -8WȥK^x~BXRZp!J7`۶m[nčWo |a2FDb0o?| Ag[S(eeee?h"*z$EEEUUV~ٹX|s߽{?bݼyo\]#Gܻwظ3##rѢE}_K 7eUUժ_u떾Wx[J tqqYdJNN΋/6n(9)%%%%%e„ '77W(Z O$~՘#Fgffn߾灁yyy>>>t:d.YD]]}֭=󨨨qq8즦&bŊAAAi5kWGDD]'I5,X`iiIPGMZx;ݛpĉݻw?}~ٲeoX|}}۷qFؘ7l qrww 0 yy—/_jkkopGWI|m$… VZebbBRdee-,,*Sjժ #"".^(S`xo?\d˖-~~~Ն rq%'';v,!!_j477.$$ť#:::99y>>>x]Dzzz]]>6z>xѣB0::://o̘1]d_eeeddd}}ݹs$+~NNNqqݻ7oތ000033ˋ ={E&LԔ$~7|oUUUAA.\w^bbÇlё^gg' {7{쬬( %tvvs>ٳ#F200(.....bJ㶶%%%&MڷoJ?~~~ 111388X> Po._V~ߞ={Ȟ^p8XinoTXX|aÆ]zjE۩Ïh{{{EEF{&# "y;׿TUU> PxA>?mڴ=0ɓ';::޿ݝ` 0ٳgժ>>>P?D$B }vAAZ`իaad+>@-@DH "D$"H D$"@D FNN"|zӣ@-G$zzzP @DH "D$"H D$"@DY[*D$asMy,6W@*8(:ƉT_:h#l7,kd#^4@8" |T)! I՝Hի<ߙڵk۶mvooSNmIIIoopйs缽8cT5A2gC჏#'N| ֬Y\ IDATw^[[B]#@'==착BѦC&UHpD$)((`ٞW\IOO'#T77V33e˖ܹS >|;99gfO>=vصk ˜Ԏ}}}WW!C bbbY,Ѯ]B]_!oܸQRRBP⢧VXXXZZl2mmmR&LgZZZv1ePjժwYFUUU DGGXÇ/]gEp|^=`uvvrPnn Nhmmm,ݻϟ?]>255]reSSӱcǸ\nnnnDD͛O>Zr%J4h֭[\Ç-Z^^^~ >gnnnoo_YYyΝ^[Νֆ O 8lCC)SgΜAM:!TTTSSSi4w}񌌌]\\jhh1x` &6ɓ'[XX1'M$//_[[kF=jԨemm=sLB!%++`dddnn^XXH~}^l@J6ڒ 8!DeddX,N= JJJ!2Va!jkkª|>d*!_D:RWWGQ(:vTWW׽{޹sGF^ggg2w*p8NOI?+**.]lll)---BFUTT;6,,ŋAAA}dyp%fYZZ&''*855v???_kKSSS߾}!4f̘IF&,,ٳgxpޓ@ uVQQŋ޽K9B-==!TQQ!Lf^^^ww7JbbbvvvzzzZZ֨Qvc̙#//Wz]ޘp8YYY7o޼z* "y)2l0:AqլI&͘1';wd:;;>uwpp?s挒ƍ 0w\uu .[NQQ144Prrr[l144 zQ6m$>DJAו̜9!))رcl6{ٲpQp\ǜvEޗ `y|zd6 "$8::2 )GC*|Z?Fݹs?+fnnuշ ~D$455%&&RT@~{{ğޮ!B"6wbb<೎H\.liiy|t&~q]@nݺ/d? vtt_͑1|ʈxBAAaРA>Ï~?ZPr… srrjޗ"D$L&#4 | d2?x2ccc}}YfDDDHD$6l077߻wo``GUUU6mڤ*pll7={b `ll1}t&$$\|fIېILL AXYYm۶akk[VVFV̙3&|,Y s &b͚58ʊ`&)+S$]pѣG?lth_ۇX,sssMMM2{lPDDD˯_JJ8E ;w$?bkkbpʵkׄBNQQQYv> Y7oB ׏?^d"ƍR3aaaovĈd)S߿/M6tP"//?fFFFd#N,//)W\Ay{{+((ѣGϟ? 2bzmdB󔔔222jkk%deeǎ+bjj*--/Mjnn~ 8bᷲB1BNNNNNNœjmm/333O2eJ```VVֻLNNJ"<999??_T^^ۑk׮_^SSl B,$L&>$:K(ᛏ|>~Y[[JHH_!T]]ikk#$&QTT왈+g?i*H_H$##e&DDs%ZvС#&Hf:ԾO]]]^D=7GE;Z>VrXT&x"aO>@UUUvvĉ%Σ=[#h4nrJmm?.~=yyZ^^cРA x/`Lh4>7&?'"Me}-++JSS3555|Fq BHMM s?|^#!l[M-=x`z]SS#W7⏷fb82ecZ$`sAƊ<J%m&xpTQQQ<OzϷ&&౱qqq*l<))I"=%%܄~000 O<Oí RVxHG5"'Ox{{{{{ə|>W^UVVcٱ!gg^:tIgg'yg/BF9romm A͜9]ϲYYYAAAF"TBnnn-jkk;q ;;;]]]^q%$${GEa񉉉\.ĉO>eيjjj"(66B3(KO3m#qppڻwH$Yt4rttܾ}qlllx<^VVhoo`0.^y &ܾ}|p9355ȀzGp*233Ǐ7>->S *MPP"O"@D "|Ns8b,G)HZ[[A=DSSbd"tuuttthjjBm>8ŻܰaØLfYYtjjjÆ |= JsuEE/77x&4uUϞ=[`xn];v숈055=YDx722*//;f 8"ٶm}/XXX )ݿO 11[KK+**L߹s'|˔)S~G<m޼yg>{̙3mmmO8!a{{ٳgB|>ԩSϷ]vmaa!^ٳ+VC=~`0̿_ WVVz{{l޼!`0\[[[/[E׳ uuu ?عs˗/gL}}=̙ceee˖op@?EFFjhhXYY͞=;99/OVV!hѢ?744:uĉ<44رcVVVgMMWWWÇ-,,jjj~Gݻi4ٳgUUU֯_/O>>>\.w\pyEEEYyF\xReyƎ3յtݺu&Lpww/((pBѣOvss;~/vڅ33f̨Qp_|l NNNL&sϟڵk;q,^8###11qɽ.''+++S]]=vؐ+))3ɓ'MMMFP(t:}ر9$$$466^z+WwO>k,Hŋ^`eeenn~E22 /_hѢ`ڬ\RFFfΜ9Νkjj8wN"觘S#<DIIFD pcF+++斑#G9!ŋٳg\.W  |~9 tttA!Bx>BQTTc܉%K>LRqJ[[[2w*p<Ύ,vmm-^ HTUUoߎ_޻w/::֚477|uuufە(>qԬrrrrSNݷo_vvsƌe˖>JcZ'vttH#F7oޭ[LLLp[AlܸQAA!21'''u޽+WCqݻw)L?~Rtt x111fĉƿKUUՖ-[&NUXbժUO<UUU:4p-[O<ٿp<<=HA >1g"@D "@C?׆嶷KΣUdff޼y?S\{{;7o>>xDrmeeet@{n}hhh*))RQQ155urr8p`?SQQQUUeeeڵkW{{_yϻN6EeXO>͛+V4i/B(###&&djii>\׮]+,,FoBBBJKK+**W^t ɢ"jjjnܸQQQA̙CP#""LY,,,\_bBl#p8YYYNNNǏGT*!<<̙3Gbw$&&X,]]]777###ӧOº ^lӧqIhhhjjܹlbb)//pBBB 444f̘qiӦ}ڔ"lllȔӧ_^[[>||ѢE'NT*!b.]$ W\IR uVEEEP\\}ee;w(Raaŋ ? 8ԩS s˻y&BF!RSS]\\-[VSSGz=Z__9a„3gPTooouuK.Ϝ9!e'$$L>}͚5"ٳZZZ={d%INS IDAT%WUU0777""xͧOFutt?!zjuu/__.Tuqq111QTTTTnnG%@Dɰl99999oL<¢]uuuGGI&9f <!$//odd 7~I&9993dee!lv~~vvvSL4iy`M8qeee\.PGG͸qϟf͚aÆׯ700033D @ 2D<:t7|#<mSLAIԃСC ;::?.,--G@vý.ӧe5ȸmczuRss3á8E(R( ڊRSSV}}}N'f>)BD9rBC b2"H -rTBb.o2KRǍNbcc9^L|гK::;;ª|P(+joo/*ץ;::8!r?CP(o\899ѣGf)>|'>677s(4O\ŪU S!| wء?I惃.^$%$$(++fee͟?鱱}l.</^ֆDnҥdeehfziNfRRR:ydTTӧܹ#''{(TTTҴFGnrrrL&3//A%%%=<<<\FEB(55gϞ]}۷oST6؈~A}}ȑ#ިץK055UPPHLLϏ~z]]lviiiff&B1uƎFEEQTKKKi&^Q[[[zzzee%BXFF&999///22GK!^ӥ1zhPVPP_ѯҴip+ͭ߿/E\\\#;v8zh?ֵrʨOXN1cF@@~77'O|b0 >O#gU9Ύd3FQQ >ӳѣvvv͛󃂂t |իW=000Xx1ץ6mڤy…d{{{S>xΝ;˖->z4qrOX~w?VRJKK##/(ߋ;wcR>갰0OO۷ou455IG)Sus}˾,mmm4-77P?_eɟ'=rȂ%Kxxx?x`7o>}z```||<9r/MM99` /\pM.O?3Fݖ۷ijjچߺu !~y>m۶c '$$P(%%%qرcРA6mrvv^p+Wn;w:uJNN2//oѢEg{BGaX222k֬gszxx| ˗/WTTL6m֭+WΟ?_"JHHPWW={ڵk<*##m۶Z͍d:t5&&p߾}<_ueeY]]OGP~Wj&յs1cܺuzzzeee k֬quu7o^JJJCCFx<33_U]]Z233mۆKu)gg~"33T*_Ν;wٲe L&N_32?`0B@ ضm[nn}X,@ _ȣQ"[xqDDƍ׮]+~iO˖-۳g^czv5t .| $^ffÇLݻ^^^׮]+//񩩩jL:511׽oaa%-,,x<A"+V`X"h޽۶mßavvmccH$:~E$ؽ{@ ZfE"QRRԩS_z%f͚񒓓ϟ-Mqq:Դw^{{{<%'ihh斗SԐn&RRR.\5k֯)TQQYzu{g@|F}|T#6mi޶QE-Z)' /'6r9CoDǏR(S---m\.Xtvvr8###&yC5jƍ8zT@ffu2l[233SvFt2,Y,s\6->GR\>IQ(Nw `eee \.BTVVJneknn=z4,yHDo{vf{y ss΅999<.???k!u //ׯ HPFFF~~߻Nfy=&#: _:&'' CCCcڵ.111|||z^S%9\;wn[BFFFFFFҥK[n6 *~- d_}9qٳg*,,,KSSsȑ/^ObuunPPWW_xt#???&G}֯_/$F"c2ocW3FmU._liiC.dIֆDѼ7m4rnJNN ,S}͛===pիWaaa{xH4%%ѣGgΜwPT|d2LO|2=??d1BQQ#))I$uD6mZzzX,nooOMM첳 jjjnq} B99qqCzzz+i;\.N$_*x<{s OիFFF= _ y<^}tp^|&nݺ5f"|Ç{ۋ\Oq8Q999x.Lڞ7lgee9r+==B;!ߛ܌- ˗/&BKqqt5ƍ'//_PPN;::nMK+//vcso {iLL ɔ~źyK'H0F6nXo#^^^~m[[EMM߿]0;vm޼˗ Czg׮]C qttr BHOOo`VRRR{Ѽyh4޽{{۩JJJd2rԩq֬YYYY ,+Wtttܵk{B:d2q[bV|ѣCBB(ygƳ=}t.]`0TUU-,,6nd2 ;---CCC_~MPۇEDDp۷KfU$׮]fX** fm/n p80k| =}akkꚑ3AggΝkeek.`>lCCÎ111{K*((5(=999K>|VRSS===?PHR-;;{߾}ﯬ&>>ٳg.\HKK~{/ 搐Z狔 i &XXXdeeyyyIyի^׬Y?lٲSRR `jj*$WUU۷F(Z[[HhkkD~~~8q^^ɓ'뭬BBBSٳ׮]۶mխuss۾}{FFFMMɓwM&ݻwxѣG|7o=ztROOO;;;gggf† +uuuU9sF(h)S{nѢE^^^gϞp$%عs+**B 酅=+#]BggKJJqゃzΨ"2f>|… 8WppӍ7CNNÇ JJJB=zTNN.%%ȑ#>)>|d2###_zꀀP8`N@ r+W㥥 K[IpTUU'dH$BH?pb嶳= /!$)D}GJHfeKRl6[z`LÑ4WL>=99ťҒdGݻ1 j̘1VTT7N[[ҥK'۶m۶m =fyyy)++Kf%ϏdΘ1!$y=F޴iȑ#mLL~qEEŲ2R4!ֆl6[QQQ:^"z 7 !9w܀Hd“;ϟH(#`nnJPN8mEB]]njj[ gϞyڴiT*uȑEEEmmmƍҳ2{ ࣚ6mX,r [XX8{l*?BgΜٱcP(dGussC ׯ ==J ##B!),,| ˗!--=\.wĈIII"H vNKKCyRuuu kPiiiCCC߽.SSSF;;ZPMM֭[?eeee##!>}W[zgv+何rcǎP(555ٽmmm}U<+hddD8jԨ .8q℩)D2{ @DRL&֭[666?v4...K.9s… B|>?44^:a>>k׮E_w6mVRR{7o̟?~377Vfw̆ >xׯ_]]]CBBӧ500@뗕IȬ,$8l{[獵(#3 _*H $?,H "D$"H D$"@D "@DG"ŋK.1cիsrr>G о} ?yfnٲ!ihhG .,,VѣG111ݶ=zа!fee'6,,,,))IKKŅJfggoݺu׮]Eݺu CNNlGDD; wrr255UVVPWW># IDAT!C\pܹsl6!$/\/NNNb!4|774[[?}]v+WzV޽{K,1cƂ RRRy͙3ԩSvvvǎCX,CC sss{u+oonn={?؈9s-[l6655mnn>w?u.pٲe!!!\.!{nCC/Z" [M޼ymiiiood2E"BO<$~e̼q=%!;$m trrCgii9k֬d;;˗㼆-  c+))A988 4oQQQ9~իB/^d2zzzL&SWWȑ#!*ZWWWTTk.'N<{lʕ#GHEfުp|||;Sl6;00Jkiiٳ͛7!99ɓ';vJ"rss-,,= jkkm^^^~~~7oz{{ݽ{***O?Iٳ 6H,Y$-- '59s挟Innk׺}۶m3g΅ nܸc<77m۶eeeIUTT.\033g[Ԃ# ޽{ɱcٳENNg7n]H[kk+Bh2MNNR;v1c? Htvv-X!>F!&Mҭ{R~~~GG'NvZ*@QQӑ'N477'ŋo}||FtRЃ &888L<ޞ㕕uSSӃ͛Zh]\\핔?NRkzK ;v۷y<vŊfff8 AΝ300ppp@U8T*А` /^oϟokkkkk[:tճg622jiiau֙nڴ IӧO@СAzP(?UTTBt:]^^h!|7UXX_\\, 1%%%W^U gΜAYZZ,o޼9s̝;w222h4ڴizHKKK222rrrb1޽R---o߾Gvނ H$Ç ;+''1vuttzɓT*qƌB<8}׿KRR͛7޽nff:rȃ"##c};wTSSvڵk]]]B/{ ={?8;;QԂ tÇ{[͛ǎW~Beeh:y'OΞ=[:ג%Ktuuϟ??7.11!4po߾YO^QQk׮OJJJU)44ȑ#6lPRRڲe ߿_mjjs&)]„ B˗-[m۶ϟTVVvUׯ:thfs3b&pB$5{7;;%KxbϞ=ӧO)//^H+7Jl3gvܖrkk5&** HJJJlll6mT^^gi{M6ߟxyyx{{I: >1g"@D "@DH "D$"H mŊIII!>{ݿTTTGsyxxܺuUHHH\\tz%55{yyg6g|JlCCÎFLLǏ? w BCr8?sBD!!ߺ XWׯ_gDG!ɓLJA ._ڵkMM͌ *,?$"IHH6mڿl<#""\$11111Q LݴiSddnjj3gB!F 2e B(%%%>>~AVڵ+==_{SRRN>㨨v tR} B"(00Pz&44TOOfƍ˖-C-Z/++KCCc\2`Г'OLfKK ?~۷o0b;wg3gΤ߿...=y.]igg~ҥM6ձX栠 ?ϝVp8nnnу***"Æ >|$t% B(//ɓVVV!!!d2~rssǎ9:caÆzEFI9uuu+,,R=ê}BkkkǏGDD5k3;w< F~~>VPP 2d|Dǵ|+W|n>|p,K$޽;00 WWuֵ //8 bbbV\FDxx8N)3ܹs  (((9sfee%Awܙ={@ 311y)AFFF7oޔHx}3gY,MQQAf*..)8~sI ݻyf 8 4ADJJIVVA&&&uuuMMMo&j̙ҹg̘ zKzk׮kI%K...7n$"44tΜ9A9s[f #"::+MRɆCD3gk?;wNұA|ؘbXSSc`` $d>>>nkkCDlllPP#>H...~~~ ___W?fm;wXYYijj䀀;v4JN2b! ޲755nٲ絲ꫯB666 D[[{ҤI!+++<"L;wnZZB喕{kk{%%%b77ϟǏ+((ӆ 2k,/RPP@BږoߖB"TTTBMdcc#//?O>]UU!#9F… qW,Y/&***FbXiڛ7ojjj,,,Д͈#Bcǎ1bDiiiosGFFz{{#TTT&M[MD"HxXN_ Ey|^G?K_ "p(}ȑ?"-&'-..vrrx8;;/]ݻӧOGEEE@EEݻwm޽{E"Qqq1xǏqO|---BfKmllԖ7qoL$~p884hPoXܟ%''766vttpZ[[zni\VVr)JeeeoKF8zc@%H766J #GRCCcڵ.0e_ni4ܹs޺uKfW_}5qĻw޹sPWWojj$hjj¿ddD[[b+ȑ#|!CKJJ=zdɒwZBPFyd;1LFbMzd?KZ1?ڴ7o^|Y[[opBrr28t*++sh4???&9c Pkz;lbD bccW]]->|R]|!d2>rꆆٵ[vttUVV !T^^.Lggg|`722p8ڜkkkׯYi||<^:mڴ;wtuu5b :}}}##\ )%:B(--mhhgP?Kx<|իWBFFFqIbM>|$uõɓZ|dn|i[4~xPaa/xB',,666=V#ŋ#&O믿JfC$EDD;vIC1p|&3]TTԓ'O¾-[hiiM6ݶp\wwaÆO?y$`,g >1#J{(D$ "D$UV[ZZzxxdgg $$$..KZPP0{UV}piIIIOy}w8644۲eU__>c3rss<]x?ϖ-[E"1o<mݳ޶mۇe?y򤻻{?M8QK:|ʕ6l~}|_ ;"碣#..n˖-arrrqz*""B(Y&>>j߾}555B_ݳaaao߾%~~~AAA֜9sΝ;"];v6~GHg^v竪n۶ oLMMURR/qrrz #o?:u*##L&yȑ#cuuu, ˗/_&BIIwʔ)Vp8nnnÇ{w[n;{{nIR>zHGVPP >%m~zVVְa^zU__zÇd'>;w|mmm.,bccBNPPPourr kooٸqeB-CUVV⿎#G8p@]]]fᩩo0`:w/>b'UV @~:t &[[ۜ ccc vvvzyyyfHܼtwippCcc#A/^tuu%¢ G.^ ellS \bjjzͦ& ۷oQUU5s̊ޚ޽Yf{=x`s9s挧'AEEE666AO.8uD!3fI:p{~zLL BoYs(X,nmmEyyy"H!stBbXEEΝ;Ǐyμbq%g2}I}ޞpܺu+33S,H$<ւK<9ђ9;EEE#566޻wH$*..f0/_|1-#\gKW/?o!"Lʭ[ƌrGё$ԩS_xQQQJNN{722³Ϟ=C=yK:Ν;uuuwr\999!ݺW]]]]][@&d2FI'txoWG##|ܹ2ZZZ~B۷!`0vy/_2/h4;;;D:d2@GG!Y__oll{gÆ "hРA4mgώoLZbEҤ?uvo͘0a?3͙3gp"Kƨnefff``|z;j2I:$55U"]]ݼ.}vuuYfeee-XN\q׮]qqq!!!OLL4i#G}mmmxCMM!CCC'O-ŋ@#ϫ,{ ?l"_H222.\poCBB(?B6ׯ_?qD?K[ʽ{['`;}iiiF9k,<]\\0qDa^=~mCCWdwC-`[hϟOP׬YE)((ڵd۷OIIi޽jjjྲྀl:w\ pBX&T >1g"@D "zٳgʒ{O .BBB2>B0<&N%$$t{|+I D$ ̙siӦEEEaÆ><)))++رcVkklɻB]]]yyyrrrvvv~~~d2nkk#ɞC9;;3|66dZ믻wxG<.!<p8~عsɻw3f {|AA~gggϣSlٲ>[ ^b _~={P([f #"::ӓ +Wxyy-MMMEĬ_mM'ѣGƍA  |}}=!! j[[ݻwrʰ0HѱrJg;88b/^JDJJի (**%2,ٵ@ 000x-AfffYYYAx& ۷oQUU5s̊ ʗZXX444qŋQSSc``pI ڜ ΝqF[n ōqX3f|R^+/)DLOO711)))!';t>*{{W^~!t pBВ%KZ[[[TNNˀ,//M;*ZSS"HT*!bihhx9sBÇ>}:B`dEEEL\UU!dggWYYf%Eiii!JKKe6F")((vvv!mmmKK۷oKg:ujZZ133|8pǏFl'O-ZD"F6jԨ>ҍ̣I"KtttB!Á?6OFY[[߼y9x BkF: ?~xii)B=Kh88VKwt^y۷vРAcǎۦ&BH.Zmmm=QSSp8"ĉgϞ:tH$O}𥷭 _zlB=JnLK;T9rŋ{K?~\QQGUccСCqU%԰ErZwBuu5N?rHo{9h>|7|lFFFB0::zܹxիWBFFF4MSSZ("$3&lmm^:::6oޜrt&AgΜ!H<NKKCyÇ8022JHHO> >|X__cƌ#x;w!޽l/L&d! B</44ٳg \#;::D"@ _ݸqFxczz:FFF2#==DX\]]b %s/2+/hֺW^1'@"N:xxx._\,=:$$!dbbl2 Pw^yܹ .Z`N_r] ws̙31w 7L0~---CCC_~MP']φ D"ѠA"##Ÿg#{{{ooo//nwa4СCL&3<<!dgg#`ر]MMm/_d0xpbٲeuuuiӦ]v2d޽?#ё/J*/+hرc ,G,!!!ϼ޽7o^aa!^*|V`ӧOBlAD9l2wwiӦA0kO fmY@DH "D$"H D$"@D "@DH "D$"P߽{O 6 z"H D$"@D "@DH/*t/O{{{KK şUޛu^eo J`3Ic!4/ 8q|aH]]\aÆ4pCT*BH$ ή3CAITTҴ|5lll;{B$// GB E^^OedXtvv~5}'JRwc,_}D$-M4 k_ " "@DVeh}F ?ʘSV1# d ^[[#7nܹsG tOOOГ'OΜ9ܬj*kmmݵkGyvlN$] ĵߤջLOۼ獳'/6*G!uVێ2M_c=p@DwFZ~`#yU2s "X,_lBٳgɇRUUrѣG#""\nDDD@@n^^^DDDll,?f85Pq9GzcVA OѦ8@DR^o5]:!uLBݝR떩ڃ"/R1ljf9֓D "IPX,^pBhܸq4 !w ",--bqTTԘ1c>r)P[;| нM9^˦.P˦lM+{ۺHY[!VJo %% H͛Bl6ԩSǎIIIB54\.w8L_{wq9x548MKie:ݠl07du֛ c2lF`S*Zenpaa*d1Ĝ$*9_sΏ c>ufPT Ǔ;o=%|e>U T xh=P$)#D"Kijccwj!p:mmm~w||<844$Z2pB>00zkjjwcDү~k]rv$k%9qS11Ndcci`P.mlldzᷟzBd_:>xųپ̝KufbSj\*Z\m !l*ψZ!TPL&ïp,--xz$BT__mBd2kttTh4p8zzz]dddh4gwMxkmo\(R̯7>_=>yp`qO;;.9۫EkB-{miX߿Pi~*{d]c UW{I$H1&ChYggk+H$ .wH@HDH$$rk+6R#zuTqD8TU=-4}B#_:$N]y-ݾ=EH$HI*J$ҕf^kYEp bH$NUTUhOnfn?X+qTVl|[z]j۬M?cHH$D@"$I$$@"'$IdRQymm<;;K_|X,r4 t8xsssY,Cee|(jiiq:*LLMMivE^rυ(IENDB`././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/doc/source/user/figures/add_to_env/add_to_env.png0000664000175000017500000034020000000000000024522 0ustar00zuulzuul00000000000000PNG  IHDRUWsBITOtEXtSoftwareShutterc IDATx{@?.Z\\( 33ݝZ&xH H}H}f2Mnfm&Qm644j^6{_E v}a O6:&f7] 5S/d2uO%0Fښkұ~*7|d8Y$1d"ze<"" D@`&f"QME.]llԧfy whnx%D666666B@ "H$BBP(@ I+AxU z% ZGFC#tDf$0 LddkN ,t"( tfY0Civ.zR&C$\3FHd6 7@ 3u>3l>b6@`h'P`kwCW7[( 6tFE[oL$$ f"ri \, &#Yge;}FO>9sLeի}Qpp\.?|0//Fʺr劍[oFD}ѣG|͋/ ^xa„ ELw%iMeDĆE(ދW2r[@Av1?fqY[=g[V8x {Ooalm.S틷N]0'6s$LxHfQ(eɫS .Ǭ^7YD&#YzsL  XV#\!H$FhWWdžf2m$&}=c?rJTjyR<?P(ڵk<{zz^pO?]jՐ!Cx ":x`GGǢ"'''"j?>/5D$o7\fܟw` ".{mpck{?HlȚ">{H8iI&4h2>47PR_ysktǗ'LOxU^hތ7;${OO6tg_lq_FwasW-we~Pgoܜ]tH,w 쭱y,Hsrmb_fI1{{3Bcm/~#wslnWѠ 6BH(ČX ®/|Z]Q:(|Ccx۷-[fkk_[ԩS}||.\uAK+,,t ,pss;{޽{;6m4HDDO_7 y?0 fRܱ#7 z%rMLF`c;WuyǾ>B5{H8hCج %7uܗO<8D <>r%P!_̎Y|{/.+!%36+ruSְ^;ӺF!(yibײ@Wo>%9qU[o-h46L% |$~(w_g6Sm^z)\fE}Bіq=f+@+++["K/TRRRVV֕1O?CkkJruu-))d^#GgϞZBx'fȑ#>|u"2iDDF# D"fKX Bk(rf]%XfHzXnfA6{p0\UC \^DU?֤̏53 WHǓ"NK\M(bcgzԼ{¢\K54poGٟ4\nMTyc1K?sI|W\ɹ>/~k}e OuNS抭 ccӮdJ* اcMfH[73RY[k jH?sÆ:ht^QcDD ]C>[!3pw5h|f]K)5ŕ4qʨEf̘h f6aG%;>W04$]ͥyʨSyop/.CgCߤpt" qe!.Ωڼ i{#=v#RF.$$l1;66sgO="#u#f2Y" L&-FP 0n 疰וFa|.l{/ ~%\2ҧyOWQǏ2}aǙ2tP?9m^2x 0ǹ7qj agra̲D UPA?O1 aYVLDīOLYG3Kg3g+rz-g`FsrƁxu""+jb :ɽς]ڛsW,5zLYg]:^xuK"7zAM2r%KD=|ZgY߬{|ZZ`吗XwQmg+ _mMub:G2\3G Ns&][BFw90MOH;O&j=VKsD,)C۸@*_ex|&%/]{:=hcgGu{;_ː']~nPLfA2%?RI*{KW#"NgYR4 ]ՉE?ČI˿d/rJDfu"fC?!3HOβ{- EiE?]$ϑef: i(P Ums]+~ᬟyeYW~ȫd=7~ʇR{EN?hnu|z2,6*{Qڧol]>CYq@Cp%,-loͽ}0uQv`. WI~ׯe?"N MiQ3yu. fo;3KZoMabE[3{oMNHxJD*YVߘs*N,D_t]kc.1LR?Z_Zrת`G{x ΁t3f^D|ZO$Fhck# QT*f6Q>*_]][ b\2n:ϟ#" uK fmLNBX6%ڋU+^˓{gnc*ljqNrK}Id#EDD*?-rEs-6.'0w!7"OD\In (\sDīk1 ՆƾXbʸ<wMי)*1O,9E'|&%USr#"uўw<وVW'.QqD7;zm߲E7̻iyoqj:"#5ꏹ#ps/=)=<<A]]FQ(ǎ#1ctϖ_+RRRh bggGD%%%7o7oFaz_(vu3qB!ͺ^,m?U]I.H<+ W<]+AgffW_h5(>}m-gګ9;vUlh⺹JȀԷVNY1V{D ~uspDofjl K*DsY+ƮJ RD6''Lby)˽u !I^ Qd[&nݺ>עekKeƪtd ~}jg5l2~O'D/8yY"1*7D_h$h4:3oz}PmRFS* ŋ;w$"BV]\\z-X|Տ>H&i!C\vM ,[ё~-[I$!CNفĝYz/n]0ᨨVE%;X,16w}=?wZ3L---׮]3XHB"H!]ti2Lf";*_%Fl&b.#FW_{N}J2::PPDFF[4sYYˋX ~'*ZR2Bē鶣-HG'\?h,6igW7gXku/=/p_{x r[eRMMH}X8coH='yFtxn{×IP/L W rUgQm )ƾ1]A?ZV?>n?_ED8  H@fkBxmB1cc+K$vάQH)aKW Dɜf2X>w_]{.89-gb&;rd2'gk~ %=,Bۦ[&2hցL̝տYBo~Eݍioo;=i6tvud0dA 0 :zu K}Dtt|- M&Kx}PTd6-ԙf,qQ+wq]_w?P3ZzxZ}XTj3qHƆbgP'^hq QSsG@ lDAbR!Ð@>pO&@>@>D.]B+@>Rπ?)hH}H}H}pKB<ЌF^7Lh " "C ]a6M&h4@>@>>@x(.i骵S"|9,-Mk뾒O?3i/]rNċ//FYVۢ7Ӷo2g_ӯݢE5'>&yxC J[ikTw`=cNWӡԏN{.`3yk OhOzBW ~YӶ,`eh hGDvtጬQSG\ꛠ IDATtZ oY^q'>~Ik =^9udƿ齕^/e?CLUXbw]1l]4måo)ۿ?Ho>=@Z_hhhh8?Wj:ڝ.)v >o wԥ99t"K5rx'qG5["oדnݹgI9DD l;~=GiMU:"ԧ Koݹʼn :I-ȑ#G{/J&{6ĸKkݓ_A$RN{[FDFdd|'1#s""b]]YYM4B }߸>Ĕ~#ݥ2K.z x5GHEK%%xtR_VV̑3DޮRtt=#XA6KE6#bMTUP5ök%jOn$p=UUX.d ٸ{*ke,[%mS=ǺYgoܸ[FT5}7oucLSj5݈M}Ȟ2eJ5fJsS VC8O:7k:*Ѽ`˖inU.n_BXBkge 8";zxjUe.w:z.Ѽam MZCAǥ\ۿUAץvX1!oziU'v/Y\CD$sp#m5H:پr->`ZsPg1leήCDd<]dIhbC۪gDmV.|R"|A֓6H&=Eio^]YZnU:۬ʉ1nݧ9)ۦ bCsRUjvԊnDD̈qmٜͼb\Lx;>&&'\HJJ@>;{ թ2Θ& `666?.Bֶ$Y ؠh>@>݈ m hCfd4F#:p'R R R  R R Rԧ.[lllllʑ[3w:}Kǧ3$HSZ|?y¼y+xI#ѽ,o)=!yhTpSnb{男DmJQgT(H j8S!Q%}ݸhqaJQɲGJ;Dr3$}^,j֧o).ՐC ~\I{Tڧ>=U9q޼0RMW QrTeoȪ1Te(Zg>zF:c Myvvm;^~:bsj?[K` ZD\<ʙr* UQnbV[J$SV_ m/DrTkKsZe&rlo?q*\{?< :b# 78Ҩgx/=3s;-m(ݑoNQӝ\1vegX'a!!ёavwЧ* ߐoTUXCNDRG2GQ'SȈHd+&"  }E,#"u g3gNkl*42Y"(7?jl=S*KU^@1P)YFܣȻ*Whgz&ϽX_sQO_ϗL;a] mDyunit5yDDdktݷGWWم9i>m#"bgG?&dҬy'gm oo|ʱ$ksZm},fVqrңs}q C>D]ymM5ee<Nvkj|VrKbJs?ɰY욺dܦpI8* wXPw'aaa|vY _VPvi_SYoHjk-J=lwޑǰ̺Oy""7ƌ鳐sx\\nq|1Omyd`vm(cOdL\$[t5OZ. wa!n6QE^.|$aL^1OMWV Zi[YY{QM[KTXc|#;3Օk'1czLi,8l)LEڏgyBQxh $ܤ١1O^LB<ֽj*,XNn|M,( ).FKB]=1>!2,u=̫#""-qqA%ow mM%1|n2}&9bå;7tڶûVr7,.xgۦa}C:xlX.I숚H[I#T_SPki#CF2:^Iy]6]]>n7vʼ;j&7uIn*O 6Zs-|؀۵MGPxX?D>?qw< 7TW1l?S6k-41vVXtmemYW[=;Wu'E}wZ7wŶ Q>7UH]`1$~~gOEpZܱaZ>Eg#Q7c'#j#"yZ`cdQ-̟=SM^.^b_dX'QMA6Ydr)?ikHwx^ܼ6"j;$uDu+;Zm  ";O(mum7IY}Y20_""N5RGXא12܈* t+ 1|7sLrFZ-85aޡ|7F|S}[gct!;Kk nr\I(,zv7SkȳMk)@-d?Ǝ&]E}7!1YW֦ 7˔ -^t>GGcvnq>$,-oWe1JW} 2 ٣sQ]AM%ԱY=,ȇ!⊺&wi+Y2+zVzF]9$vnw5&͊I9,kVZc$߶{M(첔^ ;"k(h""7G,xxZ.9|'1T}Iotڶ겲 LY{W Nx'w%ގ fG|[cEA2Ksvb$}|6"aao8Qe_߷ M*~ג/EN Ht+m5ey-~[iQ"G1|[]]f|OpB<<]饿$%7mҮQWaYvXyeI$_:^AݻoZ&k i?)ev8go89+z7)iI|^W22o]s Z hCfd4F#:7y&uRX{92ۏM*8⊚&sAaѳ ILAe5P' \k/M߰~yt,dܼ&&vn>afM w$)XL9;Tck58"vy/];,<4vM#^?]/o5`֧)oj/jqrm-G2wDftOmG~EDZ7ӶC'_1o3]ڕEO?ONv"jo,m$rx!|?H~FߖͩlA\$iD"y&prwgٰL+ #9TgGDO%#4YJDRgW7GuOD d-mԓ@Ax'^_C&K/f IDATKeRHJnvNwu8O~gz'KI…8է*mk:o?Sq|Я)38;`3ʁQR}sGED./Wo>}OH[sBCÃ=.X>[ļ2mUΎ=9AOD8aq YvE9!1M:xh޻wo;ik*.%~D֖Wy^]a5+uMD2)f;+em{5k*+w9^Q5+x >$> 5Vu;>3+|L@@@@@@Ȥkl۹JŁBmQx%! "puy[:і}fu]cgs]u.K{\w?24AL@jXPJP/+ԣ,EYtwEzJ*{U.ˊ[QRp*%$ \GoxZ磏Gqd>|>4>zY#ϲy3-Rh}4n넀M42 `Y]՛<`ckb=K{P7K>$lޢK'M_-e|sx@w@}a@.Zhhҕ=Ȼϲ7ƊcMswš:l!{EbV-Ztʅo~.0o Շ.x"B=7\z_bc(HuSo]j_h&noݧI<aٱR#(ؿM zh}LF-.;_t6~Vǀʜ=IVz|˧Ƕ (`*>XؖE1+nn:p5&5<$uH5)GmTwŁj_9#|g6ytڄI)Zb/ E/طrq]֤3{C>*)t<&mz_Gݽg>T!B9¶>r뾴S)#8~O JpO,[~|O59/)q$P~rL&]tCZʭe\$OzPo~yGqںԞ:r7B나LŁ;ћHe Slek|jƲL]c U3+B!zN4>Y;;(2FVg髳S)5L_|OYO/U7`Kg#+$"3x qwۜpwS7Vߞ}> vivt'#bC>z7?;{J4c8'k˦iSŁjÛ $w;+B!zN z9GT_few.]PFٻ_klb@wIG/vtZ2كU6%=۩Or;*ă]ҍk bhay+asu݁\v#7cDՇ5>Uڰ}=U0@_< B=7s7RI?s|}Df/9XWZ:z2u G]Q[]Fsn:p̞6,tMO/[x/\v5k\ ՇKdۆRi Mзl6>Bp뢔{bjlӢ"( g|HaIJyL {NٻfS:Ժ&cdK ϗ>>v}MP_toGVW~P5E1҇B!h.EXe @7Peػ?>ezhit*C @1H?&^CU}kZļ[lX/0~}dvƳg[ OMcgT0{174я =Bc|@ӱ֡4=3%>+7CB!>II>PHqz Pq o^Pt]Ҏ n?G ^3|V/VovۏmIظ1!!$>$-e32=Նڄ5iGľp@C.1$3ʦ =euYFP,Z6CB! -/߫7fMw~?ukE?CLk]EO>qЋ}: #B!>Knܿ9Op>BGsA=oZ]fWoD!ԇzкKu៤̇B!"h.h\B!z>B!z=m9AH/{05ovi^>Vˬ7f9VIK_,B!B}ԧ\k_]M[|y.o-h6H,7+Nuqych{KD4ȇB!B/Ytu878/|Sxl B!^O\EWS3P@ڛR_.:Zg|Q~wۋF̡uKoZ,7K~qY|բ9'~W0E3E0|ʻ3C;Xy4yG#voo: z/}{ Ek)ԁESrV􈟮Z؏jpDcC!B!Rx>Kgmug/[ܔ#$ x{L|lk]ee3G{kڬ?k"qDRDS.Z*]x rQ8wdo(?)p~]ZXDW/\bԴgMqһ83^-6r]B!Џ>Yګi;5UkLʩQN hSEI"xb=Ut~QRgzjy*~Id8Hą;#x2<ҷTmRȘ`v6pծs5gWw:Y5s*mZLMuWiiT(.t]&22v'oJM<_l2R_^-%%xXt0͍Y+,8w ˲}}} Øf͆_&B|$ xn \"%y>DSfnqt朦q\#VJ#'[>)TwsG>)|>sC;S>?st~wE-*1_8kr#ˉo@~ 2뽈.uB!BB!BaC!B!!B!^]^@c$7B!Y y)XlVj2 Ct[[B!e]pppxw``t^jdBSBW͆!ԇztEkd4o[PWmH̨bk;BȾ@JU BX`U F˽dXѳ/L5Gwj` [+~bE[J4 yb[9>/E5r%t4/8<4%Se鬌b&8)%JNگnԻ~%}-zB,%,RFfc)ȮG$H[RS3~ݲ{sN2&5Olc MI:'0MYAͫZRFΛWeTIc3CTRkNegoUo߷Ozv 6B*#bbWO/.K}\SFG ԇBt883I$!)0BwpVŌ7$_1]Km#C Sܧ'q.̴*w`HWòi( U, 9'`Ɇڰ{+%yU0i}LqƆz}v͌S6$oإVs4L ԗgoL뜭љmfې7gw)\m]rN<6sߑ#IEZQ}=iߗN& 7[F}_~~nz4Y.^S@`cwb{\ևz6tyU*1@땝W44).HAgTb\ުrmTJG2 @{psi IiKrJʄԍa)ڌFcAF>L k[IJ;c)PDԯH޽aw]3TvCFQ [Q"gQuZ}s-P%((*0:P\R^#dМJI]"ϵ)Ƭ(d)1bw@-(ΪծuWPb)IbDWlxc.ƒ%%,?}J wguڝ鐑`FAR Zr<6 L0nӰmDMt 4,% ʉM`%q B).96CHUiLI=.-t w4WR@t}Պ℠hD*&I Gyrj!PD$7W)1"@Dz{4++yZO]UA-HG(P)g1[L)Kr C{R$QbCpڒM T ow A $t " y34zEa֔8k"N~Z@5rO0 @i S|+L% e15e45%TIL,{%#&12`lyj=YƭaC+EKrj[:#42x,=KK䒾IqʻEiiruo]20a{j k-_U?[}-tòmEku0=#KxM d3AGL yԚ$wxFG<^!#J FP*pxԁЃT0Fģ IDATyJ+>XURwGBZIQZOO]mA-<)еv42 W ji_ke >^,˲,kp8@gc*\Rgm($rG^% ]Miq>5\ f0ݲY`-&UCZKYʉ?>?&RMK)pYLh #jܲ 9U*@7۫a/-5aPw 9Rv+B庐{jEQC6&%VȘ{v<ȕb@3W|- J|"Mhx u;X9`C=A4}yAZ]֮goG =ɿD羺846elf쪡RC_BwT(m1Cx;mIA AJ(fۆ,MXzfCPwIw#i =~uA)TPPB ?P4':Tr_U;)EWeC*727CUuѣy$ٌ;*?~uҘܵwNFc\3[^>6CK㤺ʸ\'\1HKxƲS&^}t ٶ# ׁ6@)=d 䒬^QwL`ٜqPx=DJ4}BuZ#G" [A~{ Z-#}г`ӵU :44D"Q.zR!=E6Dwn~#nYj˖}zK8mmm/`muyU44ZE{ *e FdJ2OkuZuYNA=xE*Hh)(tʂMO+`lihP&7gln/H+lG',jr6o-Qkuʂ-+Vl8~-K2֨:mmi9qpBv{tB ޼8WA)Tku]4*}M@EIfI}6+坐,_eWQYM}}}Mm3+.ΏC֟}vW3=rc?n2lt_=[{;>/[i=Ԫomv&&xP-$~bX>IPRfZbeem]bA*&3%D5.)OۜW֬iwlXbUV{J}洂fN)ڶnŊuvfn!Gt_,nڼtZ6m^b"lCh>í)n N/<G*>c'ތi\]yY%R,W&'kZ4-hѴ{>W޼442 ?3vGSUm=/pX,>}]k]@s\8sn@ڿ *}$'@, LL|!cQsi9CFLx>)ϵ;wʲ6g3TSPiA=}T\ڝrД X~ d2h̰HZVa͛>ƍ^B"VMxMד{Pܦk]]]]4cfY \X$J|^m?]-VPt]ۅ;ʞ#BG|5ko`-wó7&p8J7Ź[`nXy ņF.+s{|M~}'wyPgF{Zmm7<$WhԇXd6- 55+p8O) o76Q?j F:n F٧rڬkc^ VhBN}]zXWo^n߀ D7^j4 =Q?0xb!ZN=\zO[.&^<MYu{i?j#L}\^{Sۇ]Xx\|&̘A>7YZTM;.60|gtq AM 8JFf+9< 7TP&6j\=[bÒ~d@{kحsEonS__.H7ô <Wdщb$m7:w-n hZۨ-y}/%էWUK>>0l63jru2wssS*=x<˵_{\δInbMA8"g!C  qaIv@%L}\Xfl6(h +n:988p  Ù*+ZZ^ Pj \e\kc{ əM.rhnh;$.q[}:aov0_yzGey4̠y=-;kOeuFpЙe=SߛW3!2 W^{JKsggf|E?AQk>>G|a8Ȍ|\e 5RҌYc(?@H· B# ,{g4X`q9|.*L`ZM&SOOP( Ix b.1r|. wX,99;WOF˲~~V ˽|ADz8'[[=6io7OV&O$b ڬ6`Uګ&M'MǼpob8~sLt)z;8#X9p-s\POPrrYN4m$U bxq<HD~^ O"3;WSG gڻڻ,|_WW!@U"4 Eh6ob0\tE|Vt —Ja6Y~;_ k`R IIo(d.ƍ6aeYa'ǷF ]OHQD0O1AhAlt_tx\fB8gԑ\MTj[iJҩox>A|>%y7\ yص ,/nTbZNIR5U@=8K;N܄"A (72T Bh,jr5A8~?'FE }+"|L}BУiYz.W4&ϋ )e JxC&zA.77f"a;ڵߐ;/o{"gGٯ͞ڝCw[YX̝7ovx] QshkeN^Zmg*Zi "?֦g̬|հ yk,,9e *#c XpV+) ǏSt忋o7unNApb#gP'Y Cb&u%ziR N|oG%8 )Q! Yxžk|D?lƨ!L}.s g. bQgg 6~n}ޙFkįֆJfܗ?.?VwJ \5{,1Xƻd0 76;O6v<>kž]>:Q=,vuu\mJJG /F…//Tۍzn2 7Lt;nrPiqA~rPܩJ@?$ycѫfcYf1 c0; \L. [=Lrtzqsg#34; 5#7ǻu+<p'x\.i:$r8|23_J 1; ~CN83򃩍oWBcpZ.l2t^.;Sn۝ŋu.F eB0ة4w03W xs%'\C\& ΐ2\^z 48&][D<k}WKvzĉ .빑~]Ws`w7z1ő"y<>hn\kO}n."X覿(מtyۊB&t-~f3C%!Ǻ?ZlpC9sb#"doէK~;o[O=GuASXb4mZMnVM('ۯ=K}!ſ16B/!6ŲgS0fW;5\c1V#$,1Nq6L6+'QbP( $E$x\5Dx\,o5ܻ <_U*1c<@7XߐxbWHTd#ś?mۭt2X{WyG?A}f.кn#ejsreYgsjZDU1Gtͦ!ʅߺd]Gʉ!OtPjˎjdξ?ue Gv=`CuGkQa |goMuu5m<px;8p4n6\p!UB~!oG-tuhNLuwEy'g$~0,ڬVC-&.ixwb<*,@׋0 :VOW>^Dž3qG7 pX u}P6@v L`ik_kv)D$f߁ޜ E"P R$io $8{ }N;'l׹0riGxZ͏<0n8[8r >tMjv''')ȰNPǖ\X(!8VO[߶tYm6F˛<g'əY,VF?bb5 wf5c6ʏGvTA9*p18m椅A2QR>S0LO}|k|>ek61y2W o}?^d̑>Q?ͅ*X:w<>4FYVeocǎÀHHB .~|ϝ` P< zzk.]x{o8XLNeijg"ZWW%΍#;u!V]V.WTiF)Оޖ]b qў, LHMKU'4tž1k%`/] ))ޏ(huQVFNHolJJܴ$Klmi'%>Z ӷJ*we6~ƣvaOvn W%Sl(H˩`@Vek2815%+7-ɒOj::̍Axc@i IDATH"]2h8^\,p \aH̏ lTcg-ՙ7pqq"dSAhA0#I =ׯ+ϰ"rP>>~k}T|>>Q #T_gy=# NLre;2V MHMk͝%_\BE:Dctfs2_Rj8N"<#FڋKU V-LJBmZݢUqwDiQJ6UeqʲƨKf vtt0^ ۷F)TGNCɇm~k|ZV,2sze5;g~?dvIƾ#i,5;n@c"Y&w۟#Bmd^7;ʍ.9n(͛yN2BRwEsp=?ТOZGD&E̟^!ޙk#I|Ny`N;P7hwX%!;GFfgze7YEU:;8-k瑿xKteVe#I`:rwűAأIqH샸|^Y,O(7׸nP'򉉫:`i.0PU- ۀZ.^51_T6`]AőN'$  8gPZ.M*'Tnd &VgPb$)>Ӏ#gh.LK6q7It7ȩ]5#osZWA29oݺ䃪{ď3?΁/d <.b޸>[fg U}/6 `M/pTiy,Ϲvu121H+oZ0kֶvt@pLH=PVNͻӲ5QNĺSڣkWedn IM ^&ܷ ŝՙr( H`*ũ;v) V$o.Hٷ/2CYy 7G-#b#?thSI˫iRQ5Jb~O:wO.U p\b&yḹ)ԭWt or_6 2 2km8MJӬ Mܮށ%Se< $B_+*۪Ɓ;M",L$8m]׬q㤲 /9Z` L o\qsAszOW%J()kņ$^!^dy*PF/3vHPL486PU閣НY`l0 zy^[UGGyKrVeg*R%RNJa*3FЎJl9r6W[IcJq{AZD(%7L 9z9) HUJQ{<)M"`ّ{0'IӿXm0,0fX =qe9,2,z̙J@?[M֫m-_ncn=җ`.uuqE,c`  l$9*QZ!HPM(v%;?$9' '\N$K66zڂRԵԶh7u tC!ېmHWd qCl JmImIK@rNs~jm=՗|y>9+) b1!:F(EJˈ\P-~q0)2MRtZbu?`,Zz 6j1 |n]uULW@*ϙJڽ^_W_p,CA4 ?7Km;Ύڛp|gRCCfsx 0'B>i;O @F(C;tIeO' @x ѯ_z?c`9͈]W׉pPm6 `yIL S#U'L"(˰|H??f[n(̵gu etPIxxX@1jycǎ5{1[n+YCva=6ebtf\`ROn}DcA3bNvn]m|UɄBjzm~b Q)#]j9,n5t9q%qBI@' {*jZLea2S F0`yKd(PTwp Ay:?' OxexfP\흘6_;"*bUcr9%mU (.O"e4'61 k"yaz*~ xxE(6Vq>tlomi}ه0o,=gi@pJ}< ,=7 ਹ*cp}|FEš! ,IsӪgrBCBADDĵx`NOemxzU]3>J:"QTPWW'+qpؐ^8CFm+"b&H SJ/k?sH.!*)eyRraƅYe~= kxQ2ն@q8IH23@ PP@,^"A1'B@A'D@a ;PIb!xĨGxqC(]}:XX 80DA"2P$ʃJXb"! AX@ [Ffb1"" 'Ωܱ`)gԕ=ͅ:$<\0L\ك1?iiRF;>T"&bf(@B*=7@:9ls򬫟:ò,~Xӗ},;Ŵr ")LF7]GHR>z*DNJ I[dþl/z E~~nhz*~23B_`G qa8~Tn14E˽>cq @`{P%bA (Cf# QFG J3EA8 Ƴn6ᇗy] ku c(R:&hSޡJT!*pv32 (u*[CC67\_g'iZF_vMӟyS7 D$$KK5J`:PILaԄsú7*BX) bUrw54Y34/B_7XX8 e MԬE 8wg?'`cc9`8/q缅軋d1_pAdH8 e)6 UsQI]IʚK !jgKoϔcR! 0"|=-T~ v׏R5t;]u75ewA!$P D-˱zvpuy?ֱG[k^{ãv0~'ƞ3~/vjUB9Y,@rJS #c2QbfXq gx +}TMC/P (ʚ_u̻9nyz}J֖쬖<qn!Q'}p߸9SG#s펊-3Z(",n&v~JR_{Q;Ϋ2!UUUnՑ)tDS%Ю-^8hMz;I*Yks Un[Qk.(H`ҜXl'[_]RfIw~Ů|kQAkp/!Ucky R6w)dEuV)YԘS".1)V_:%p܂ALFNl:T%]++-e1K\ApH !!3'5w)ƱǤ|>!w!sӮ>^yɝ~L -ܢ~G#ڙG"C \>ە ` DܣE)׹ݷ( 8]|‡W̜ÿ|\oA B cYdMvl?-cvN8ׇohpTs/C Ɲ}(K "p(ʊ0V L)f:baRT>(AP0*@@"hw`Fa48qq=Z->˲lC/'?uz?%ZE~ 8*C)mxx8in׿v)Fhc' vi D.q\yJ[FC Joڒ3nrnIs, Y"LqX,K%xM2 jr ~NCc (">=l@FH#d;cHK&DG|x׮]ɚ̈<@$ 3od^tv}Izz:,XV$0bO#_&.;#Ma r^7P6lOGgd'ADRM?@izԊ׷ >.v1ܹQ5# U3@v{{!ŘY[ca\WEOL_JKpఓX508s_ܿ𰰔d\ QT =y[qx 8u!)OɎ˯7x5 (ØK<<<Zm dC0pC+x$CqRFQ wscY}<<r:{Ϝ>gßRIHpaE4 qcY8N(8 0c. 0"0 Hb@vms>gؑ';NKCB0 pGv)z<^A@ HD&@VS\yfދ 6y; S0x'xi 9" Bf, !@D([}<<<8wmC-hg`]洴9<OS9'ЌKԱ7-]:yTg7??bB@!(@LZ(b$-lPXX9 ?X< qngz"ѯێN:yKYSSr( zI8O?9W^z:t`[g_.Y':zի%I|is࿭-?|llVRiZVՏeY8[nDv{lt#h;jk /!!Fן8yr36۽w|~5;t}}}}AggSccCCDzRMӿ/+x@o~׿Q4<"ð+tVUVjP FuYqk]ן߁Kr/ts:r%^FUMMBs]amxbe[vhWH6UUԴ,Phr֦]i)m)4IYi8V7i,k>_~lwSUe˟/))00p,*Z3CXQ~i`j[\%HG 6Ϝ9kYii_|qt߾_9P_P@yaA~iI{9kE7m[4ō[eζuU-Y)J^A}yz^[,.p8q{}w͚90QE^Gz_^}\_߹nupxIIɧݻwJ$}׿]]] ȯ+B!_mdUK'7e|xxnx=SfU ,/OYWVfUOyU2SJix9mX_WǫUq8oISǚggdi3IrKxDDfffh(ߗlmuz~Ggۣ-5u_HD9VgiWlIƑ|~q˸v?χ̚9S(i4bi={?~yАE`8fX<{7xl8qb_"CBB~1QiM(k޻~! #V_9>x͚}N9stYAhmE!!b?uH(UO<JbJ>e4._]^Ɛ2gwCIIڌ8Xs 5ke8S߻~kٶ.B]YNa*ToKdqgvXh`SKyך5X鰶m)h59Sr lEk*L[i.z~c4cKAYBxp~åftϖI6 6fsI-S6&kĵYO::3"+8|uOIL0zc Y%%z$I޾뽽jsz@Pff>?'7 Fn9~r}հ6+rkwnt8`ݶ}\E{Qԝ>_/(hݚZm$ˮN{vQٵ͍iLBOV`:-vog@N}璭EEmv0U͍ʼnƒ]Fm{Y%:8btw7*EEMVeZi ++HLW\׍NSCf:N;)u)Vݷo_m6TtIOe۱.j)ۃT7777Vf8*i^.Yˡc?p.=/Uffi6{A'<" eTD*mq᠆:z⒒^ce/eA"а \#(Sab$2: Xx}>71lo/pBM~`bLxYh/XNL? 0a,>'s]^1tw* lL):ў6̯onnn/LtΎv2~߾}ZcE+2g;ksI}E]/< SR;zTwfvU twE^-ywscXWc'di&.d$hSiqz:-1F,|ZCYI}auSjrMֱ+/oKC[NrRO>*glocQ2 ygX,fQ'H͛B^GHL dYh>ZB)/so콣:D}؝B? UuWOzWi:n߯NcL Z0 + uZ@MUⴴꬬ8#F&e+QcK H,- mV'isV,>E%Ц&tr'7'ȭ-&EFB9)Yj[K8݃$fkkj2;qe-etY J6#0'::U0ځj[ͦJu{+bpPeImVNCHNPL2 ?D`А>;rfĨfT͜x|_f|>`r9),"ȂN=u먡9MVXT?'ğp萗eΝAcّ9b D ~,I$,<,/SeeeUUeUնW^. B!jab Hl#c,\R@5l`rl8ŕPSPFhM&-tbpPf\&ÁY)TZR3R)T;h8C2dU S') OI05Zu.#%Z:,Nh(r^U`18,btzr(8vIoWa(L./'sy(n`[y\.v:O=z?.H>mЀz< @%ƒlfsXXojjpaի_۵K۽r!㸾ӧ}CC"Ehh/S{Vٳ~~#rŒMsr3:qn>ZQ^]p01/}hycذ$ALjbWl0c1Mw̭hrsʀ#u/c:c(*/%jj* (vu=5-& 0LH_a#`\#L9pLI`&X& (;C 1jRL5 } Ռ!l`\ M) ac CM^4 ui@{ue%~{µV(O>p`ƣ3cc6 PMMm$yk|x;!Ӊ;DCt^-}˥P$7@FY)zXPP `05t["N˴CuSsVNR(@WEC1$11r m7@A( LS=ur'xLMɫG\NhjC)$1ZdimBi&B0*+ul*޹:$'ca<]c @ccI SԠj 3[,.vR`@ζӧMJ c؋[ZB88`\nwdTP(|gm/(/G$::|xICH$^/;:Rɋǻ+m8cY_䏞D(Ƃ~1&cf]ִDa SJ/t0~<&)MUda,$%M^B qԛw/KBFO(ZC´c iin)YST7rd۹d%Kxf&qXIQ_?C5u,'#3gDϟ02D$:?}t}gnw[%8?.ttMy7EMuɉ:=^G>kyS5% sfΠƋ "A+&2 =N0qZ$ ŌJD H"7zژ >(i/vJNP*Jv6YCjUէ*Qa+0%qPgJ2T uZN㦼T^oal 0UB'eR CYBLNp5xVgEqT"!d2 üRX$_q àEEJT$CPc9E0,48x˿|b\,JEFt/~x p'@* JQޯvPVUƀ?^_JiX_]RfI3S#x%[*+1s@vI8EPT 5ppm] rdv^PE)۵qjLK>&ZVGc3rɪW$ Njg=_Hʼn W=>z]OXϞ{[4Swa|g+; 3~숰g }`hKٱ!jaere:\~Nyd$TtEQگnSk@ C[9o "0 K\\næsS 7͞t%J3"cG@hHGZ W|W͂Dž˲֎=n|ɺ^{@(: ?~*bo.S[}bdi\ƢYxsA@Q>Ro\=Ox9$pLf IDATwӂwxw3RHÇ MABX󒇇xxxxxx:Ϳo;̴=|AQ$fFOz{N2ϙ^XV(ڸsnjHU(˲''Ba[}<<<7Wuuv豤Վ`o[Y\{j)k]2vc~QΎ;bFu %끇<{Ǧ5E㕅1UK:mnִ%dѤ-6;t)t"~C악g:R׆EcfD>z;uT^, R K%{E)#|^ 8B2L&6UUԴvZ( ڤ92p^Z#: KMqг5s. OdVX] 7]l[dž,S6u_ZdTkX,3i~V~[ҦDV :kVD:MqqzcZ|uć ming:.mZ5u=) ̚˶xx&D"gD)fD)`| T"nXEGDG^9q-ܰ~U_ژ'{O{MIɚ#Yq2N'81RB[Xّh3l6Pg/cjL'7;ڶUSf,Pj0u J q#SAS^.E+H)_{Z-ڍ6>^\:N6>_\ԴTmk ) =֯-ߒĴT-ڬ8}`CzLme]9䀼##GkS]6 <ڝ[Yh*+(r sS ;䕴0RIJ ''V6,] ZGKYQu=>]4RxHl[^јڌ6R-)PTPI 6U{ṊrYQeo^Ǧn(UuA_MKY_:^bάӮ [w.%TIcg VT`H͊cg، &sڈagno2cqp`Г=`jnjrht*pxOϻ}s9_=Ne1 nQ[1V4۶Wji c,TZ;lE{u(˫2*s%io)ۃT7777Vf8*i*ݶ}gmXUe9*L[cc<(+i'sX5Vcou () }QQ&(2t0B[g,'{@Kn彝 Į,N ȔҝVo`ʝ[R暂2y GU_0uZ.~0 SJkY,P؜LtWURw77ey5f;eyly[P&dlMz,jlXwE^IW\\njX'.} )Vݷo_m6Tt~7[S mQ_dUi0*0%m޹8ѬfULmFdmFǧ˯ބ m0hksUN; cSv?z:&ո_M]E z߾}{+(6IYGkð2{l KK}$a[} ¹6GDZ:U_7mtem\3Q_Q@D](\4`}s1,}}oȢ'Y#9w߶8ǧ:aexȩ3Y̓E2w׿q J#1Ny'7nH#Oe׽y/HT2Z2G7G`cؘJ?J:Qm8 eK_⿙a(PԴyRqZ: w TԴ(0)W4#qBI0#<)RM09<4s ><'gdHHAo/ #F% ?{wĝ$ Yn`*\T(hl+nbb=[Ωw.x[q+RiU,5^ *@ˤ O? >B}NO$o=3LxYLMe=g+{{')xcRilƜ;LP*2KwY繗u+JY9QB鵯 {e;XGh? <`׊jsO"uRHv3!WhL*2h0Թ!#  ,.uù٬$Ko\Φ/god֭&D5Y8r]ByzxP(gOb=a$?9%1L&K2ň#{Y:)gy~Uvw|yjlZ;:K/O~Cu.39;8D=zBZ0鸗=:'"/HϺ8`Gt`J+:/S=^_:ӵT'B&q7]3}w23{{j9((版|%ilhkEfȓD =y OD\޽/>Gt:"/{ޛI2Js6J2L'}MG&>"FMp`zU>ס'=2l`tԤKo=Wwb?7dCܚk~hʔ\ %"枡]=xTogM0ƴkf49ϋPf_RFswkλqk}O\b7ت{LܺSw}"KJ3,bYZ ͭD$h%DVދµOehkk!LD"e5Ao:-F@OCwk0~ q}= ް q*t=QpW! jRR5P] e^!xCԂ^;*9xEDKZNֿty^5nCmJꛡD$u ߐ"T>ӂ_nx:/WPS87FOR:W.EX_!ߜ))6!I6.p#41q֥.~9U0i<_i"bzǞP$71I+"nhR2"HQ)Cdb^M{'QPDBu2+up7Kyߋ'撴ƿ**p٠"~4/D{XQ_!Z)Z}PƳ3ޛjޯvk)V`ȁ:.W\VzѣqyB C͛cǎEr[$$EG @/ *D>Sln;vz+5jc(sTLO_Z9.!m\ ]/OJ DD6cdV˗V3>+cS T)ݛ ǯ5gV"D VdzVMuQEA9i# &]<1 Ozx<O EryrE>T}P>iO>}W7]u|ݑk_}yӧsGV޺Vs$7HK~.>tЛɪzUEg+ϿJU _m_} s_l ꫯ*'>!;hkg79#t)+%㣎CC+ߛq^ Ήٰ6Y5g 7֥F"F,} Y4Ux coϣ@ȼELf&N>xmͳDDܑ5n:E=@;֢ɦLKQsBMc  # oَDM)32Ifa]a.7B+O5.OX(8R[_/LkOI;}M2ȯ剈FϓRJ&m_~p&5DxIw9wQ(w:56ݸp?:3R1 KȣsmݒYJ3̛3 q{z|D/$C3Ed1iYyNl _ OKo Χ+O>SddğKKGw~:ϰ;)oMq?|0|zz MX!Jɸ|I Уy{g,(e]-ygrbN$%#"b''{>UrTOT.2CsC}5ԟ(!f?>lr!7:)? k>g 9:Fb}zu}|큣F^,9NfH5[IwtdsF{鬤^{eO:]GD'wg }#?,d¼O_rüۇ~0HTJk |D$0k$ 0>D:L2_91JH$y!HDX{ggF9/YBDr }:v|#LdΒ^?o:7kIKG.ug/9,xє9gL˔a*n=<`H5G D$j0/cj~m /;qV "X+0wOjr*"ל<-)Ώ |kw`IF#ȤC9;Puf%8g"}4G 1LC{FF&C*2X`x}۫p,Y7D$љ{C5= $.RLfAڽY Hoftjdp.=c$b\v# gNMсY!Y"ꟚؔY[nd-zu}BJ@n$!|ݺw~H2fO6=AbDt7\222lƜ}nyw,bL =~0"'<.tFJRi=]|%$`ddX"(+@D$4W8qUBDBr"ҟ{Ч;p>#)vsuW;NK\y 'N5G7 .eedպ.s6 Bޞ2Е8SGDŽ{/alA`2 _s4(p*C`yǛ5ėtZ_qR$).Dĕ(\GemVp 8aVnJb>x&Mzfk,&}lܺʙ}1nrx+444w(*/tG7ko~߅,h00&n!uˡN!R0R]KֽƄ;OykS}J/WȤškN>9wb6D0oZ?gCBץ<52zג.͒9Gl*IZ82$]7ebff']\7Dzѣq`0uttܼysرH. S5Y~.>ڸY{/9N%ٸ{ܶu)$d@۵yٸ| S\Qo,&{&{??dnnS`IJ D;{Z6.RL3^kUn7{d!YxMj!f'SLCm[H` 'ۯ[?{| 5/;>vW'f$b,Dfc?Βk .~dɹI IDATW}son$HNbݫb0$ɗ O/ɨ92-#^fi?%|89,+\'K5i }k?G_/?]줵7+z?i"[^eܱb>ZRk ~jj8|-gZrR>2M()Y[OyY-ھa ޷Jط:k7?jW0FƛbkF,"aW쮗;[-mԻsoW[͛4J$6T;>ўLT?LHjwgH?yDT`Hvϰ1( f^vGU?7|;.I)&j22{I2!XCB]N޶CɰxYcqg+Q  0u47kߌʐlo~2|XYm? D-&7I:-O׭ѕ""s[;K[ @o5}t?k py[iobCa0t~sB 뉎َd\bGӨ_<)"+~%.u\j{wEc֒ rSg8\Dd!XBͻoؘ2F ? t/ 3 "O3T}UT}0{} NUHÚ@p]N333|o1֛o;ښ_8@ S=<5 aBkGDm_.@.;J?&#ߋfX59jw]lɒUH7~k/5^~?UUlrm<1|O6w֞nKlx/{I6$!Z3ڵyٸ| \@\ k?m_2b̆x@=ohcFN TY9+]>q|}ۙU^""1^MjqU7<:ɜ(rsݓD?\q߉wl\86xaW -&l3Z-iϙN;^xfIu2eDs0t#Z\9[\9;nϪ:u8ې@(UV5-i'"j铷}5p/gj{9ODe7ҿLEµ+|96|7?Q[#DUԬow|7[Da;/M?MSvG#?!`빯obebæewHm4RԪU"E"p e vLxKe+u 1.Ϟz1$@wwL"'ON:}K!8=qXɞEQs~e[Fڿ7X|nr?99,+\'K5i }k?G_/?]줵7+z?i"[^eܱb%w7x_N.fY؋=rw,s&Sbro/Nl{F[y"7Aڂ~hHU"{n޲{{@fޢBJDGwjh wu[}7}7̶Jξ=5Yyc6h?b~81GLnSӗ7smlHd=ג_7]CH.>7(jo:+^Ѯ"uW?Ut&R{jݧ?Sw݂ )ؾ;WE* (&b H ƟHؕ}>VkKc1H.>.3f˘ypOg펪65n7w\RLeNedlm#Cl#+&m׫paƎ 'ZgV+cQ̲7&lMH.S\hog׾-ە!qLOzhF" ~""A5ZLD7ro t}sP[+EDvDVGn\`&=66/HDZZ\aOH.Sj0(:9x!ODlNJ|K>HD"ɊeNZYۼiZn/౓+Y0/A4a"#YE{nE"A:;̆bW^=z9.q 7o;v EfP>A1D7T}P>xT"3_XL"e>KӡES(hAlG˴v[XXaff&•+WP f0LGGU UDсU Q׻3]3~X_i3A0TUY7<0U\gR>T}PG&q:::Fc{{;Zs=`nSH. S\T}#NpwwBkjhG Bmmm}}B@k H. H.%'F~_ba'''ш@r$ 4 "+++t^Br$x@U|{}O5NWQ]gd..v4ǀTZZZ]gdh&@rGB[VYg>Q}pa&"5,,ԸҼBl+R}T{R Sg/_.)%%Tf(GN=nZ#Tr1a/͉X6)%eoFBr/ H.;<Le_n /kv1RH_?i Uv|G/*3khHgl/Պ'J\iZԂZ$uQE&&jiR[t>ӣ]Jp_ɪ?I˞we m5On HHH._^x͛8p<$\$RaIŦr16_>#9.6666v}@DTؿHSlllEZP?%8Z#.66u"kşe}\lll.4a;6x"'MH-<>]}ݖ\.՚0pe]*U:2@'i+_5L SU۫q#UQKTQ|uZ]uN+V3u|UJRh[ҪbTvعb6%xmė& sDUQUgzTupf3;d-Z޴_REl "49,,>-1jҰ'ְc*Up*xib^5O}d*,@rGxryfVKMͫ#Ϲn\@r\$w0>8wk4r}w|A_)WZ4Ԯ&"HDDi#f*E\EM TuV2fPEEAEYQsYΖ^jULCÓJۘYbzm_\Gw@wio  Izadk8;yGzy,4mTCaJj*Ἄrm@D|EZRXIU)ݤ~ 0%c\^¼DĤ !cY!rs}su[rU43m]J ojM:,8XRĖʞW5u[J=_]X*x{HW{ >ʗ%w=t]^ړɛsV{Ria5WlYS.#bH;Yi} .Ǔ]XÛJS\R _yLsA:ܑ܆KM OHƚ*;BrEr!g2KE gInHwߋ ?k2]XzOz""CmckVYNQyx.c.5ל*o#Ki'dQ[UQʾ'H4o+th[fzm,j/FN"e=I8=g:Zks|gQ5. {qQ2]WopuK1/[z\tPe]3|uVUrG"F>wS&2>5x_=<6hYE.wGWsSos%Dv3lל,s;Ɇe+܂'I҄"xgxŊiĵb{6"F]ɡ6כJGImne%ryZ*NU;rzn)'[yNXnl{/'UN 8LpV vuD$d5 D+h:ǐqe<[@D\{]P)޾X>ww]4Sycws9%#ZODN6=z$it]qTI:"zwz""Kp\Ga|*'I*~ٲݦr7LϨ]`UU=A~6@GD|Fwڱr{Zd` HH%8&>2?:=cYw Ir\@r\$w>"ݒ,l-{*"|jۼ"M#ҝړu̞clM-4+Cp wMsMU`u٦eRޝ9BFLsrw$(nKaLUpv K>>/-/t2wJ@42ϱM+i"5.1so/*.ѣ0)3y'lQ`U*jA#ȓ5v)\VV޾D)q޴\RhLv~KkʘAo@@rDrY .!$\$}`f---fffmZƊ=:?燋IOΎvAxr功իG677wy{{o(^=;O[Y'0r uZ>ūV}fjF"9lCv/}q"uZ~Ӿx8kS3Z? qͱc"H':-l:*Er\$}Pa5_LYޢX6ߣ:1SƣxH:[q\mXiwan)3a#nWdiݖHQM寐 "HC'`&.>pbזm{JhܴH "=J>""Q3թ&\l/kU8"H.0 |40MD?1$Er\$}*kFx@U @P"***B+3ܼ';P @r1+ 433C .W\ىvHDb@r\a\g0hA1  H.M.>xشtvv)KgggKK "6bff&ɌF#`FL6w H.mfhL<K\1q x|xo̐STmۊ-GvYDttC)oyP9c7ZDC#*kREˊrESV}𺲱dΡ6bG=|hNr] vO:^Z~YrzZ8 IDATTGiT{QSفܳU-dxŽvV!oو^V^GV[r ;s^̇swh.)Mfj^n&mdmYgk\*\;kWτM2(fFld:mM+H$&j(i{3vUEw*kOY3Cv"իsqZ+XDDNpg*EYks}^^X֒knk|vfTggWBR^TCӦvlM+V ݩD!3':Òwg()%8kzBd?>wՔZ-Ax/g穡cS=N@Anxm,ZH: +JRLDc߼ȉ'؞8vlm3[RB>tBD;?)!j[}znpDĎaMKiekE56eWiiŊZft{_iȊU vm2iU䶳:5T^;QPK$[mVc.beD3X~ lg9JfIUmV;dtP1;ƊDQw#ez3Sgh!dVcZ#ܻnYZQ`ն?Um_ ZWa6^gCi>ٺc7H4n~D+?MImeCrA֎܊f""{ic;{1HR[osfǎm @%эC9J.irl}f*/#_TVunnhk k^#Pm3ޟ @0<=U;FDDcgM߲U{rUȺj817MW-j?k1ٺΟ9ފziZs4ǎe$َ X8IL{D|lR1++vD|CmnY ?Pdbff6b&S͓WFΎvAxr功Z` P4zx=<~? aa  X!̸0 qYYY=ehnnvX `u,._lmmmoo/H y֭[/_?~SV"p0:acsO$^$93.JoܸРP(z^& >Z--- }FDZA!6&?F'''DbnGڵk=U8 $^ZZZF~~F}rivrrjhhxυta ~U J[ʍwڇ񺓚ٚbm^ "F=CRmQ"UMxGtuf؂- 4Ѫ55$ ,&{ʾk7w* O)>:#= N49߷y %jMTJs3-ۻg1~6Vܳ&?pyŒ̼Q횻5N/p޷g"3j]5#wvssbagKCaf Zǒ y]Xf$Ʃ=%DW8'vmt?{wHUX'A "bS걘PN[).=q;܏u RW%vv]"!UBJz]Oj1 ?j*yOSLEǒ >S#|E}C4L 1 "RD޵V[ᒐdA+ö֖q.wh zZ=J][huu:KO.\{ZAD|ibZfs">4p[adXw9hk"jur\ =`;ˋBme^DVV]7]$p2u]JAejE VE* uzEAekb ԷK|:.[)Ts]Rv8Fhh?u51ӧ]}'Lcm>5ߖ[r)-F_txgz^rӥKg*cwk*<NۅK=G2җzˌaK×'lޗÔ&ﮞ//\_/ݟm8 f&Z.!cY!Bi "hi5 WK]l5h5.s=F85ps}^RH~X[U+zU߳QmyĥX͕ "25G}p83 v؟e_]2U?{LZ3ιA CwW W Ksb ^0Kے}Zo=[+j#>{;ߪx^.Uɩ{\=ܭ3շ?BH;R%-.>٠ :!NevDZⶽH^^^RR/Ν;7113C:#.QxgPnV} Q׹KA^dJveu87 DF!*h A}O'mTW99> L,A47!CSc_ײE:eH<{* \_O_9wMI$z\1W|wzϧƆV|i>?;nu_vG ff-IÝ]pмrw٪G~¬=gm?gƿr鷿;xUJ_.ӥԤ/h]FzdZIasr剷e._ۥԁ4\x*ssVgsEFjnFHZ2:'-Zc?L?Tld8t9}V}Yj5h'Α~_qjh;Xpu)5k,* p̍Dfl (jx=cplxWcv-͋S F>vz´ʊEQ^zTZL VY2{wsw΍&%Ia烽 8݊5J[6:t);,Jˋ\6>d/s2$YOd9>v\h`rY/v;:Ei=xE7ܗO}\}@rk} ;0.37'lj%^X;/:X ```tdGa|Ÿ ^H}hϾSaa#>B/ (֓?:ѽ;~hl)zo<~ t=H=Ds~xBI;qJ+wyjA$V3/+ZwDdFih3oR2U >\s+O/)~lg 'pUw68٠ c}֌X;}}̟iiR/ȸ{G[fH@زeU/ps*xټsx+s5kޞ7 Vp`>10:ZwX0n&iy FNUhGF̞6ME{|13kٽ%ԑO5t=F۹ԑ3s!w 3ݸClٳqdGĞnlZl}G>֢E0CDɣbAA9Oo߽]v\e߷(ߝչolwvc0`dtE _؝yw\+;a~XH^L!|URw v>54'  Ψ808sBqV=ϟ?Շ<0:ă ,S {Gr.uDerӬ 7\5+}Lί޴y'(09 x3` s]B2Ӭ[y>n~lnX܊y))@i,(N^^;\iOA=m2oN:{b[FW/{'}ok\A[fg =H̚͏[NU09hDwKJWk?:eDN~4lECȢ@97LBFoY7E;}r4}UU3э>\U6|u^t0}>vrB.-[Q 㧖6\6Z:9t¹.C5X'~-*>ч{`Ιr|~I]E#|# s)|O|pjnFzcnf 2 sqòe?WqpM}:p 'Qg({iYm]ž;憆p2&Ϟ 49aT'_JM9qNy #XojCݹ x`lid rosevS}MwtL;gw,5<8peӧGBlߴr57wlͪjY\NI3|˖Ua뭹~aBkgH474379óX#BEɛ[vs>"Oy}ZOݼ8 g=kUQ1l|H~w8ۻO Yp|n,w߲qo;P1gR 9+nM!Baևг7ͪ|u2P(vgopvLBׇB!B!B!¬!n%ˇ~@hG$f}!t3*944"4٣u|x1A=p6 .]l?>rמ &sNrx1yf1 =y6mlkB 0C=$VhEy{{bև~}}}ޣtnYB!\|||.]=IX`hhjΘ1c3a`ևB!&f̘ݍCNg̘AQ矁q!Bɘ@!B!YB!B!B!BaևB!B>B!B!B!¬!B!0C!B!YB!B!B!BMR<ى'!B !B!г zB!C!B!B!BaևB!B>B!B!B!¬!B!f}!B!YB!B!B!B=]x I~v xBG$Id2<<<x.jE#!\ink```pp(T*~ `kD"ya"0 2L"`#B"HR-(;riJa"YBwOӴD"!I#H$@( IDATDBt\D!\0r1CNv;|=ZcyE##I-װݎF.F.B6r1CAp BD"y pa"zŬ=_10rE=LXaևB!B2B!B>B!B!B!¬!B!f}!B!0C!B!YB!BaևB!B>B!BO)\|?:z=W;88tB!B!¬gA =G.kMjɟ+OKcVJvrWT2gںbm>ߐڑWc_(4{R&%j'aqƺ &$*15-1BK<=Uk>vivm^dA=aXY\Z^db"<.19)>H 5oY6j- <xk4f}=WD=h*m4:0.LTh]bV~+ y⌴X*zjF~mřE-UYiê%몊fd-Pkx\zOp A<_=k}i-YJCD__Zk}L ʩ4]S٦J#X.%*}G޽%b ͜kuy)eJv'Z6m oPWKrJJvEYj 3[9WE_woɶdMS Zqx?)KѨnj4`*ڰqv+zȝ@{uTbήIj 7EQ`*<^wjZd[W؞/Xj ]Go>]qY;vYTisMjUwoɎCE榝tDFIB -{Vny̵>mI6.)0ژ8f*.=9ZK@ВE%Z:$uWI2ѪimrrxEf1DY]j,c^9o`4ȫǫ uqymYS A,ܑJ61#yŦfXNϫ9̭ye&>_7Akw@^AYE^}҇GE/Yz$zԞȝX\YY꤬ym7Β%AJKtɩZJbk \ZKjZ3UgLV &'W7hiFEQR*1V'#QC~w&zɁ(= )rn>PܓP0, JN𨌌 ++2L\Z $=gc ~}~EaaUjtVb( w-E**'gm 9)Eۇ{xHXqT%Ui]6\_K4U[LIYuK[KMO 1CtN熱3↫꘤(*= J}p^GASK4ŶշLv8T<p6`B4#SFύpUt!|yk1cNeH[j \ĄU0q9K4R4CyvA#Jݖxi ձk&[Ꚛ FyPk*Gs(Єjgy`ۚLяޢߘ?S d=loirsڅgiE0*[grp/& jܰIQ0ڒxwĎ/R1HSS *&$|$1]}umP 4Kh05@8V3&Pb ^bXbևc`^9{ Z^j0-}=oUGnTcLc⌬ P|}<+Y;o[|^LOiB""Ư<]¬Ē(ܵhZMEe{a/QEA I{tA':D"!5i[7SZ Q7Eݺ[<PjmOYWa _0 ՛R ?I8 [+8)A6kv|B= ČA&Ѩ m&ZLi0~hP\vœ-K_Ͼ=f4^QQTfԠ tiq[Xˁuo mg 60C"R^eab27.ütSvUiSj Suyu]Օ-Iژ0vFRQfVtQQ@gH ?%J DžЬf# XWf}ŋm}3BxG{\bWǫ;;;6'^ҔqMA8f<0 ҙ*MZ;2D;OPFz~mDHq.%r;!Sy tjSAʕ:?#n vvÐY2_ UA?rC2$AMM؄njڏ2Ki9L2Ԫm ~w]E.œQ1Y#(nPEm=bHUU@V'&O)YB nǓpReB#8UuUy5:޵L]Kv3յ"44ǚK h]jnj6nP`OCdn f4J4\suUUvw:YFY'1 ZOvڵ!awM\݃gs߾p=5-Dpud>\eM5[6IYw J`2>[g$4奔OMhU]^*[:`6 +''W@rc1q̦ʐ%cW6eUXbBj aFz86MEm_VWtexb XS@} g O͢j7vBi՘={NW%޼42[G' 5 $FiD)h+/3u3`Z Vqq"34/@DݥOPe(=n4[ʷXiѡi1l]ʖGj뎕o]!4Xs;[Ѣx_zd4cG!•+N|6g÷~{k(N}eKVIIm[I^r 3thr JZfckIN.(PHckk㱝y&:\K7hvm=34[9)homm7c< _ظF75k!4zKuS~0 NEWB"NSFMWZzXMͱܵ+6T3bw 6;h67۾aŊuև.QuuFu+V+lLtHbtJ3`aj RXsll)\bu }}B&{Vj}-Y(+j &&=^mDږu^9ټM+ʀ$/g S֙Kvؕ*(*ڔZkU~ 7(#6((*ڰ%Ҷ'<'_[,.-*.(FJIc1t7oj3Q,?2Ř~kt)ږ ??&HMRݡr鬙Sxn֬Y־g4r 3S\ -ؕpUuu|.`ǥH_̂u+ ǥfeűE-dlJ/ٹdIrLUށ̔pJ+;-^;3<3 )x1i{g}SMo$IBɸOQڕg`K{.߫k &I2tˋ+04ٳvɶ]TaaqvJ𲓋v}J̻Ot;)<GO_{i}bB$YB Uh2vbH=\Uf4hpkr:Ny\2wuW^rMpܞ?wW\w64ؿ9g Af#)oݟiɅ_x(%0JɤMM )-Y~؅' [ۏp:A$ht[zWM[0tOH$jO&P=gW/zZ0dB܍._H%>-뻥lm"BvDfūn{H?]3}r`e?\Sf]#8N1g1mJ_i7Ⓨg:lwFG }s~NuKu 4 PD\8$ҷ{oewfL67¬gk x)8N' HnZh ?#r?Ozꑢx#E5n->fLg\~Ƈ"%@`9R_/z(? _3i;04Y>\z>d/\d_{9bF4r9E:OSR+޾!gw87, ;@Ks_}LG!! M/$g:%N@FH"9;tK_j0^ IOK_Svh+d5nrCԥ9ֽ￉krfE{WD;8~}鹯k}}0C'lt:m6[OOB!(Z&JR)pV+o:[ wh^d$IJ%Z.W(ĕ"!6"4󽾾޶mׯw8^&Ҕi9sNoM#IrtE>777; rj40I H됅4Sj Dq@DR&@JYŞWN|}y|uW^2Ǫow Ú_ojMy0 COop ag>pKYfKm}c⢷0C虨 ( nFU*+sJHhg;pk!Պݍ(L&J% Aa"EhtϞyM*EypΝp/~t]ٳe#+r]DQp@y%? ?9m2fvddLgR_-`wW@;P_K5eۯu$|#>%*}pW[ٶ ׺-( nJ+of@G Ӗ0S8(;Ps.q[YBO9RE]"Mߜܼmgo[wwk\.Eqw@vBTA&:Zy+]TB!iZ&I]7\@=DQ|_{{{/+N_˿_W'bww}l3g?T"qpQi,kvnSi g])ihW-Q)J/~ivY[w8NPyp\>%ʽ ubbf|}A~un([Gyq%Bw Nto|+N{Zt|YBOy]@F 3Ϟ9Bsf.t'AJH 8{$N)ߢ IR@8~!:8^BrJ&dDBŪB?Bh{k##_u-G(WT"9w/'k' HR:-1/h_u2R@k{8Di?f@BO?ST Μk }az@JO!ܧ%S}4S}kOLJl'ف SzQ`1)("zj#AdJ_{U?^;zZI e A4ɨkH!r3ƾ1X&W3nnn 779MKѩbBOҩ'[7Z\.jwߙ3':("øM Je T+R=!Fp gwLWݖAK?j׺Ghmto` ε]v:p Dido _qwS$4!;QŻ?0CAs= D II)o/_/_^; _,xE?Є)@96C2rNB ^~'DϫrL3}[&͑@I$H $л-g3iǎ?l~HhM)뱇#,쥰N E_~54*X9Y~p:/N]$MI\ኅ)̅i_>pjC{408404Ig7F IDATL+IY|aև}?ADQy+˚X%n*')%goHS*|CE`l e܁pG A_+ľq5ſ[Q IBJDBƁ(m$I@*(vuN6##{kqcWDz7UU}M}{4A7s߽KHؽy+:BKg*;Tj% & !6B;!rh0?N$AJT@/S|r(WO A@)NN/d3o 52 Bmi5"vRE8|pF*fpQ͙7mڔۻ5V1Ǹ(vu}5~0ᝢ(<'曆,$@ !auiI\M;O2+H$ ~ngz)O'I=*JuM:4d/Ĕtws6$1 %A(}?)tlB-NG|K;NN?a ` r:>}2kWln)e}48cmqikh5}Q"W#I !xMJ&= (A^ݧ:đ0=75+ IJd2L&xsZs]Zk𪘴%4f{^aU R&xK5)SJi9o>msc[]ۣ4QYYA4>kJ$Uo]2rrֆ*\WdTIOɩQG \ItJR`iW--->xX4 `z{H@$s?|`~7xR/9ϟ |Ռz ғ<ꇏ5s-TQYIR1@7Fbk?RWTk OH Uru0I1l]S[IOh,l2,œm#?=<*dyFrӆ  9}GnH \z%knH^vQ&dqFuRH^vQt1iY 4Wyi*9r`2YTqǎ#p7[TSNY0T)#v;I1C6ƳW_}y4u(/}bEE H>;ßns:2T.nW^|A<+F%L\n|&/LKl^WfMM۶'5AK&!YWWb0DdCR.1zɹ[$RwPp͹)M,u[!pH$.2L.wUDQDQPxZuCRF"#|I H `>$(H&AV4u659N.|a?.^@e &A-r Hםp2cJvXMp^iJZK inf0hLv݀j61mǟ7+6+jZm*̮ )ZK׭T]kK !qZ-$U\%Ej6%Z} ?T5yժǪ[Wo;qtS^KŽ;tݺ̢dTO kwƴl*bSw_@kl}b<1zv|ԪKݛu::in5Co{.\vwf4I 9 ӴxKƿ繖6K9N_VyqqZt/xUgWM];d,'3c:Y߻=Bi<~ufA iwW\RX=@glۻ3TɵWnH, ٻYOo7Ɩl_4jEvc΍t{_&L`o T0J>Oк8~%9Gx7TXPu ԛ#$pƪ켦?Uk6.OQj#v\cVV%E'XwZ3G*8ΎkݝazEς(XOEz.9NSwG[6}Uq>g'86;'yJDKȳ-;|_=b\09NHBP2O77Ͽ5͚6'!@ R6D S<ɜr8ȉھڿ: C D  'YsDOA.RD|_|$f $@@@B֥yK(=r9Eɤ)92T"d3P$1B h `?ҦỈV0I_\\oL W+j,g2f.^亐▄(EEjI9AeHlUjꏘt hJڱ1(iIR,,VY Q hPG%Pp7-9Gj<bEsciQ2j2쐤StȐNZ_]ǯjنx/g)=}_~%8qs 悤op>j)AJtֳ_}sR!ExaO|yrJ~ROyRUć5EmBP* Or.ZT[8tOvSNZ_k$. U/,@!`HMf\U@a#"OEE } I!0^:-| =upXa f0ewiR•v2IMHD !!HԮƩ& = 2!RT*)gT(瀡(Eg,R~^<\TJ?V5VLuoUV[R7G@1ZN2PGX 8)W*f<E/r[,<۲iχɳ²77nE[*$.}[z4H hyz[L:ݝM˗+jS:Uyji[Hҩ.t6~=~u\4V}L[Z. b5ףWMlN)Fc6C{LgN  AC#^cE; j$( xZ9fEQczksUaQed:4mGNEqyINOO;V3xYO! ',t{{yLGV| @{uo~5t3k]!3lʺZy\Wо  9O\Axz0s^ j>T*&GC濫J2G~i|KaJ%T]TXdx=VqG1?#qaևЃ#;E$H]spBBH@a'HG)T4 csErXgaFkZbXg{}qym6A+Ab^y?n뷙0Op!aH!,B8`|y0 f`؂jQW'\ń'4;F,=0,6?G5))7rqWf/ɴz5ezi\>]E hqPi*RM2HYgpL873eΖ OoUjřI˱ȄK`/;& .S󗂈|w_|e7"ќᑃG<}o?{;7k~\78| %,M]XD%/VMH}? }!kE=eC 6{}a& QxV1n63YdJ0]?>}K(8ǂ0ǟ  DuL0:ntT)bb^b8"La%nLyS~A ,Eώ%IqI?3Y3$A70 X,>vZCY9A}}q~+|b1ADI|ll  6=B|6D$=$SĿneOXGO<+ 0֊ p|\lFЗ.XOHq|x"\$1l|;#np0 ;//>yCS_J @ 1O־cfɰ83r—M"%"AڂT}T"'GX, H2 NBP(w0a|xH)c_=_~,aăxGIA,棤X4%!$$IFQQQQ QQ$IFEEDHu|w>ɏ@L I)]I_'"?Hш"XlŽ߃[k=#o~Ӥ[]sm ÑI"_%wbr8!&O^K@9v[Gg8q ܗAf7>~ҍM_FTq@ܪ/ "{7y@,X &LĆBp8 BP0B"!HˋlŎ]ـ/u2ˈpgͻZ4* ÒEq; IDATKP10/̬0 a|zPkX*8է_hH$$59 3O^AA:D5KiE"Q,3oL"s|n{&"@ i,QTq=2}ye~P0q<::Z+FIEb1vΫU x^Ƞ} 111#H$Iԛ@\e Iཛྷy`$VJ>* c,r9)Oqa 3c%/9?uxfOHPд\,#SE .;[ ! Sa+&N +oY*,:>gܑ/咡#1K$@D6K}۠zF Շ@\$k)Xp߁w4{kzspP({{/''n&i(t6344/ӚKϬ "AP^F'x%RG)fcR  6ma&XHqLx7Ϛ-WWFEE!@\v0RI{Ο` 0` 8 ޯ}@Kޤd@>pwK?~b7 a7}> X811bAD2,Z.P!ncĠ5H!7 xTԔENGK$կ{z9u[lfyǰ`_O%#<288XYYyĉp|||8B}liiA<3T ) BǽP(t?O bٲeO>$ _l6ݻ799G?qH484x+̚5'\|eDԙ#QOčĩSU{yY?E [89~S~`#P'߼bŊÇXώg.]be.}Rb<ܜ$mjg]|S2@\YD.b}KCF@qᑡ!XFͭ~srȑ|;臘bݻv쑈_{=g;o~]wI⭷z'Z{zv65}-/G?744dۿqÇ9<!(ge|K'O7VskkKKRieSU?) Fٿ_Qs&j찹պUm+6ZNTi%;۷-+kݚJw~nK_*Ӵzmwak] ʹe{9;6"{9 ֭['Jk~ǎў''^*::FV_vc/n2n$k)q*b0 aie)JG?Sd7/fs"{o4vYMLMD[L#>[V,h˼s.P mZ~a |b0dI#Շ@\ٶٍP(0A^C}_{U}.$ cY1P(g[,?<-?i]$Iu;0L$F}}/wyfhhH*~g|7U##/zƂshV4]$uFuUADZވrc\ռ-]>c:2] q[MYK%m^)Cs(xw~^O+֗22bcc'o};9񿫩>cp_W_ E͟?_*l$pᢅ 9|}tKRo $ ϊ sA'ǞyTG/|l s鶸4e YJԴD bkaeIesZNʸ~muæپC#i{uwl,5T7 DLS/|8,ZP,h4Q$ɲ׿3#PэCwH/^ҿt:480 JGzϜΓ>I>bem{=zXO󛔌b"Ɂc/pwwر8~ðut`(oGGE@Ltc=Bm%SU79Zk1m>斔iZ';3u9k>QԼ=SܐSmޠp+6 QԂBvsT+նvMeT{{yM##4+K-}5kFu=vP JJ"Dw,SOAaR>{[iUJQT 9z\rI[Ktw:<+܂>uѽ㢣2cy4=E`nBB^^ޜ9o~S[oY,^ŋ6/ϸ\J))Kn=ʫҾ޿xΉMs"qfZvɂ(|YY`y|(ܔ.`U@6,s]I[qt=ZYQnRYH+,+N"tK*Zl<WѺEm4фՎaҮ,.ې.pwo7F,RerX{MU& E%l0pP&_w3g1`|Gys u9LQ\%@][5HqFHŦt9x;噴ݞچ T q麫 6[=>e s`{6Wi=VaT+*Mf~s(յ.^Sۼ!:Xk+hFOZYѸ,)}]%䭯ړ3LOXE۬.Gh*Jiʶ9.Hu\(4i qhm&L/m'hrںT#h1"&ɨ( 8NLON|l˖?0<2`='AjiUqkKgoR)'Segrk[ @:Em} ny#jYdnTJp@@౺2%tK˻Mm/TH)7v'4~K {†\_p F{z}Ԕ{teeiUٴ}1l_}U]\8'wW}Vh5=n|npiKEw!q|`D=_z$11="=;n[կ4'8c}/xĉZd.%(yyO@?-A/ /J:VwlƸ` ٓvT*7G涶 p.wvYݎ5V {v3Mmmmmzbܖ8& PشoktGyyr"n4lqޮzim,5AAC[ m Ky٭WVRf֟o]EX gy\nQgkWv}5@ &i ̔ q8w82Yx"oZsX\e;v|]>5dj 5ֶZ+u*CSe̎YNKMXGzݤ^=RO]v/"+ޱsWSVeNW68/.R=5I)赾ö^ J"R}č&MH~d T .3w. >˅l($`p32rO>0H6kǿa3<|Sr(+/G f!?y aabQ" cXD1b"Q {h"RL,?},}KsmzLs^[L=n`f H[=JTi)22Y?.6]N99;/zM6B&>M ෷;\@{cmf;UR,.mmY$_P8C(n[IgLoٚLh00@<"M;K@&g=IF O'mHv+#&fPvERu۽:\Qekdd򆦽I$(u 5v6[+VAVdn"#;%"s";Oo6W'PH(#"\VFqd^Xn6WuYpZm$0 G_TddL-tӖHhb\]f<붶zgt藹@#<O>2Lur|~hhrAT*#rj֖<tv~`ĉ sg8ll$0׭ݡPٟL֬Y3 pp$1%aKvrq h&|H¤lbZ[*}IpSgv&x X0>#9%}o, c GĘS܍Jh)eڕE%'u zm{j ⸬2AeC kJaGGߞsIpX8y7_uvGO" 71E3JƯ;.[oeyr`{39d!v!ЇX9ijx^{k߻8tO[1ݭP8tѮ|гU',K.(Xc|RPyI?kpPr" 1N)nrgIH 7v uz@A))B]ش#{Ȥu[2׍-ԨiJt4MM|5k3nbBJWp6m)25S=޳kĥ_y0XyO$t]`(w?hr cgݴp3x9 *aZ[ %w?[oMHX`…+|?tBgϝP>3oBD+g2w&sͻeɒH qx7J[zݑãiZ,&uvnݼT4aj4ԑQ(ijpwl:zT0x~ͦ~l:[ k6<,pSOo6;Y)* &|P$oq\q5>an_Z}d(:^*^ Rd"->wn—z{ dY;~7>6Fd*3Mgԕ3 GK4 *35v8Yo:3f4URNϔ(xfKu`5uYOڵUVFVL-}Hf[̓&uEjRJ "rH⵵XMZ `Z-a5x*lptNVXwgYԵ ba9w~]ϷuZ<5#~][:-=_]}x~R(3 }p]{o߇x&:g>?9LТqsG`<<]kt OdZ,^`vՎ/IUve7EQekǏ.W}fM /ͫq@(4iez%d_jz hCX/.<0q, | E8Z8&0 # D"Qgpƈ_.8Aˍ>G̖°,;88ȲC2%ŊoadrSa'@5_ܯܭM7c]De;Q?#Y_ŧqw~Itk?d}iCn*&Y}s,ǽv { `Ag j|߽)~io4c6,! 9F0]+ ;$ ǰ,-aEQsbbgq#1(yd)OƾO&$?GFp>qP1ZxF5$I=k+yq3`ߴE$_2!RLb"bbHC!H$uQ/O| 6C 5HBr70^e}0 W!|G'MbDy@ Շ@|(kv峃ozٳd8GFqXEϻ G5O8kJF=X6]H!#h@ 8`xs eŢrZrCܳ`re!ѩU"(H7 _^-˻|j˒7a~fFtѬ˧N_9tuK۝۹.17D-tenWKO*nkӛ WT7v)YԤ7OB/CI ~+'G{ʍ{ U] O|<<m\*ޖW-. (m~IInٔSETvۓazq w{XD_qV\elzieI[]mcL6AksKJSk^wKN)WּUk*x8JU\aRܽO癴pꂊI^5u\cjvèVVTKR^ZoehJF vir#(V.ʯO ?۸d6^ӝ]鶻\,kityriѾ>d:dS~T`6v{8(_:aSퟐiu:6>nuq5MYѾ*yrJE(J*vcyYYR.Ulw\gҮ;v{>&trRe6L_!{{ʫ}YURnb̘LU11ajqr+I4_0a4}8h%YDsaO^jC6 5}"*w5=߰EC( kwmV:Kڊ^h՗+z (/KʦBJ}EI[3C'텶J8y]O.nn{aWmƍ}kgrk[ ۯLUywģV"-_K涶;M;Ap.F_cj|Ϥ25ڽk*cM/ p.wvYݎ5V/swy#zoۮN>ĕer+v6Wyl;+uz;>qE.j l*kJ[;JTFS;eFRC~eݎ]Me{} Uuqͩ;bkk{ƗwKMPB[sR^nvTןN*R_YJA*wnyfڷdW۾:ןk,nd贃&W7K[*raVScT5\V/SKaar,y|T]!5廡a߾}{k?7O6>eF2jlţku) ..MB|\PfSYiJ ZuZ\\} Ȥ\5c팜Ji3u[M~V d^XI't9^'29#W3%DuiN}~[HӫnSd K+f^{K)gnlqUt"-- PeF/NOm\LSLBlb<9 BOK"@J3(t94cn[AUd n ן>Ռi:%K\Hň@\c^]8+ ։C{extx0U7ꖉ(=N/+_aL9;Z\KuɆ dleҌٌ 븮XOфJ-6?h"2@yps:""1!8M7ZUv)ͰA 詜1vC2a@UW,,FeTIn[^5^AafJIE@RJ ̤>:U_bp\"s-81r$zWCG:b̙ Ƹ".AA8g0v4xm{j ⸬i>GhjRRuT_fz~g7@\4<LӵGeU6VGVnlY_Tif:wIcŅ8ө.Ր}5fYgȴF͟XIHULVnZaEaD_0tP4E(jصnuSI*V@h(r\ 7*T$GAP;8PD$UټL/Ԙ,^QLK)(BWҼm$u̐;7=YEMFіQ[-E4=szwNqC#<ӿ ܩRQА$߳)8}Ø,dztW1" -]#oGfq =YtSʥxZ7Vi tIF,?O'w]FCf 5$ؾ9}SȚ.\j8qLW׷tY("Hy}E#<nυ 5S7olfRX`-*-SufmJ0g *;:,ZZTgRC+N/ Sg8†EEFƜFReTJ:dV=TE+4Yze--,0/Fu2mc˔ῢ|2U^B̆FGi*&T Ƽ5B/,~aٸEU9/+n|xP*`ySc Lm\ESٚ=%MtѴB_TVZ^T[6vvxEPR2V]^zeEY|ȩS3.+F2[@40@VV6,UqXV>E^мa̞5 Rg$L.g?/$g0d@m(yVКܢq6Eæ3:irled|Q]?@ ]`\\|8DZ,{{ ewСٳg8F\fK{_+PglJWU|$;pwo7t8JQXV$ٞ9UtAx|tFY妥r u}{js*$HXU5o ~A[ZqW,pgf|NJŭE Za[  :r5h@V}1$9'y|DRR:H8jg<:['Dܻf㭈{ryI i8"mnd) ʰo꘩3;e{#ɣ׸D8Xz*a㕠ӝo4o<_FDd8i}ޢ\mYum|FTj񕏽IMW0ުkbvˉwoO:]88M] mFǐPzEn_?=74e ~GoE+uYSFBzS yz_?HҴ4ݝn{ԑOcG?{^OXp1iQZw/>xӥZ""F($Y'~vdIDGw$\r:_sҤYtwQNŖ3=!*(_G餔oxܥ*ʻX-=t}q|i,uuxs+'"41-#"MqD]Ar"bݼg8pU}a1nN`H6́zMj)VU^=WND$bV^m,I`n^ͻblv˻j3yˈ\e.᎝ȿRWO`H=IHbFwWf!!&!" 7Kɼ㩑3фo~?z[9{ ,ɼWe畚?wa٧S,hY6\qIf Snnb)+bSQ3<`p>qPǸϥx} bʈ&3z[K,YV #䈝WwYY5[Bﺤ*0mW =euf,ф)}͒Ci3;NW|:Ik~?~BX+ D1Q%K,ycQavψJoۭKuS}-.qNn?5sM7Mo-Z4 chx!:;;Fh;wUWW[ZbU?~.h@r!a+DcIs&e[mAdb) >Q\\0X Lp"21Rݶzy7'|M2`rV(`$uQ7d{_aޏ}Ywu ;۾J'] = -]DǿڨɿNqpo_Z[ @3_uyIZ݀SLᄵ]<ѲVG|֩|,>qϸf-rv=$k!/NX<~VԞy؛ 񭇳u1Bg7 %"޽WDdm?mueoNIc+&"] zUDžh;{!^N*}iӲPQŞ?["F$p$,쾻%'g֗U $uoujyFqjylzŦI[nqn.SPU!fȩ\0Ɓlk&/1Ûw~i<)MD^p,sU2Rix4:vlས]/td]jwC7a'H\w63dgHҞu#"jU~PYD$d9^DDU?l!Az<&x,/I灶ۓWɉF\f7+| sکCL7f^5/d;~WX"r(ڷ al]myUNbi3NN{64Nf'l[cgzIavx;-J;ȇz@r\a\D`4CP>@U>@`F0`c_1$9~bv/.4a&WNqpN#U\]0D>x,f{&Yungv{R<}ˎi"g׃o9ONn5>*ogm?Hy읗(jf򭇳u1Bg7 <_rSwSyXo}ksArPXmWDd5h̟,{sJ[1̷jhguV:.EI q2lvVK*nGɽwEU;)Ydnݫ&$` O-F cKfKwst}׸jDVBn{_-(nYm3UM؈͟`'}oAn7L+VgfvnA [!s1uH <&Y=y{;4L &"j/8^9*4{^ٝ!'Yi 59LDVD|G+ B+YNQs'[HG>^m#"KKR+"dm Ulr>Q:W 'n=Z+GDԮ-'$` k1dS{}Dd9e9n"mR(ڷ al]myUNbi3NN{64NfV6vﭱ3=0;lhw>$#َ\\hx1]]Fh4 ;w|}}媫--q`0uvv"H. 0<(fP>@U@UT}PX w \mC# .@#fx -LY| 0tp`4ù>8шրA1vX'''KK ҆ &w}6Ar׮s̘1h4RD;<AꜝCo6m 2>RTTԝܳ H.\Hc0~ߢ0 qh!?L8% s哋`20ƌKe$O.> U>@ 5ZMP(݁V$*g4 \VF*woiJj@O "Sp`j2뵥ED]<'0<17kVę~<~kK IZA[X{{]r>²k^T+5-]uGBR!3jլYfR# ~RZ4Q*uF͓[/0,*y *Jc55ևzgmVZ6eyHH̅~lN]묉 cx 6g_k "يZt?u¯rӫOj̼>۪\3{ƿ]vdhľU{0O seAx&!{z-ffV<TxݰAQ!*%G*?U:<)QbM֨E5DS"BT@*$y礛P+Y5.SםRNWy7zɭϋyڮqyD͚C1W\r?]%]^AL+d]WܽVU=޺{V[K^\@;\$U EtQD>&76_nO]9(QH[X}5괓g $NTh bH\;##7+Nq>a7†3g &ŞQel TL@:uWU(""b&nN^}ŧB3[.Vw⣿;rpZۇtؑi(i鵒Aﯝh;s{jBx]goؑc7xϞbi5CbQXk} t+s0Sp&=ڽq|#!>w8H.>B|{_ L,믲ʉHyTgT-,2LW:gһ'1[ROHd6!X^jL[$VEW }E2\HDgZ^_fpVM]_p8 "͎=_LrsS㈨Ek,3i<*(&"BCL""f5h3JD>KD$ ](J첾@<W"]}"Nyy¤~K[vob\@;\$wwx:%ℲSgU٠+4z"" c_.]='oYnC3 Lb#Kd""A/0E}wf ˲9#";#^v&Y{@Dl*T!2~pDzrRS)}o2P A |o JX"H}VA$5=V*&"jong{yMOhl'u?؎ ["Nz#"J]}hmjnh'o [Xkm戈DRP]57 ҁjNmk}BkC;3p$>>yguvD#Wv9aSN|,MH.6}.;`'#GmQaaa}lyL2eT }ٕɼ$مDTSyl!`XEt| %ysSt@,xw멎 Tzb.4\[E;kbijx"k4c<#(Yb% V "&Wk\j2kz""B?06W\tg&hŒORa^4EBT>< J4keg=KW-/;E0=_+....~ZG嚠;]L>>./Ҝ >go DyywۉH9Ŷ:JZVql/>m&"NwuK|D225RI"0*69XRB+cT}^c0kV䀻eE4%A)z]Gw#qfҶcΙ5k֜ Q=@T5B|y+r:7kNx"*1wTO/LH}EU&f4](e:J M%!"bdJEuji> nãSg ̡0JUI 1h8W0ހ0$\Cv#"ulgnJ_Gpڜbg8%}|k9:Mk]cW\I,Qܢ˗܊õ2ژ *gEr玈>u}T*h~G6f*e㓟T{$8 Ueh< FfWf 4DꨝJ3\$Y0~$OU9GM?:YǏGں\EL\#>p-֪:pDr\$7xRP'y#gTnxWlX|z"H.s}>T}P z^+safiivYJ%a@Eh @ 0[2`ù>]dFPȄC/~@UT}gsD; NKKK$ErmrQH$2he4E" "6Wa@C1  H.M.>{AWW`tuu=x@r\a\T}+baaq`p`aa H.M.>u5 h A~lmmmll\$ɵF ¯X,ljj/1e:;;;::F~>3f  ~ͽ{xokkWK<=KKKH0ːN7cƌw H. x!رcH.MP>@U>@UT}ЯM@IENDB`././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/doc/source/user/figures/add_to_env/configure_app.png0000664000175000017500000007572600000000000025263 0ustar00zuulzuul00000000000000PNG  IHDR[~^ sBITO IDATxg\Ig7$@) *vر#yw<۫b׳^.SQQPAP@(*B"(hݙMٙ]"<naG,|,ܫ]Mk]M%=>zXᄍh̤e9lGjzk̼Ш.*ƯF;{Kv='.~qgӾnߞn;BHhi~JB!IH xF,jMDiI*:e"B8O^&%d67_[0sEŒ=&7fP u 5zԅ$WlTZ\ y`+QS4iF|(ÇT! EtV&*r:6)ya0O< {bhh4mtQ(8䡡'KsB!{k55\GtE׈͛~$\dAgl\Y~''~skru]9wD^,zj) 8b 4IiS CouUiܙ VoTIW>Ja=V{cƓ*-(?`:ũ a٠8B a-J)$s4H:!I 3f&IҪuw"͛%g't(KB_&'l)nNBD;\w}5[v1niӧӃw ﳬ=Vwߤ" Ϥype;7ۆȩIoŔyZwxvhacۏt\!Ը-Cdr-= qG A $I[ZZ:: ߷.G=NrQQ ;Yi !G/]eG~A;MoVXm<ҋq:0=)TzXN<3j5(˽{]-nE:룾tG TH{:*_>{Dwrqhժ)*-=:v]1oϭqc@ %{yaT6!R ,%5@;@aBi_/⥟+pko n+N?.zu@gH~@ w%\|N`ҲQiW ws$9 727~MtY  炓DM&Czjwܚ( Y1%~y%IM ؏ŗ9ڮE7ZIC{vvGxyvWWw,1nx{*|i nHquy)QJsl@ v{]ڨMВ̰nM N|PNNLa׎9#;k.ER ~rzC7@`k?|o |4*@߄e|LH҄׶\&%Iv;pU)k Kd-_Ÿ8NLmkt}.IR~QL`@n5H͞ˎF}Ҟg%f(  Yybqh*7dWkHLM~sg.φO25;/8(쨭Qˊr4B<X%oxnro _읾=6uޥҝY1=v>yg}35AT;4``p+xQ[؋:DTm~[Xwj`fWN5)D^zъ3ϲ c *DANrduv^\C^9QEgJ˰* _4uJȳco6ҥY1l`L w}v’7Gm_KT uĉ=LIʙ'T 3_09o>C !%$3&2>Ol-3C2-6CD$FF-rU:(--mJ 骩WI!v nDfb< |1w`T_yV, ;=]Xdsoӛk16x$qw.=Â< >H"/RiiS8!29?r*'Λ/<~tٶ*>yjZM~#~{В [eG0w9ZٷgPn˪1@[7.hETD)dW׉|uzeV!oŖ=6bӍgGW\ yޕxǗKVf!bKi 3m-fl`0 '4"x"0*;ҔeHP!،"Ƽ}sW\\f[S[ G]J{&$,nq ?}"94Q"Ti9rXcȌɖȕ,a< -9r j+-n[ktNl4:ur^S3lS~v9  QOBI &[6dKݴY-~9$rEo|כwTһLkgo,.K{^TyעZ"Ô}LFZi#R{>@Fpprȁ&&P2.'ovT(b#ER+rK}8njMDG%lpR {57'tQvlDtYW Ȏ*+1 M4dH[% Lcc>חw֮qOO *\Qjj@:uuvnL MPBf͊}h !T`j} 86_[vՒ,(`0\ݝ9 C͞˔Æ1acWk=eD`0I,`5j@W,,7*@UGϮO]׫Ib4oZUSIҋ0iRe;By'CMZ2D,v-{[udY18'FirW :1ؚmڻqܠ[;v~ZƤpoս Oy\2XIվz$U ڲj3g/᫗D9)1> ?ȧDzv\!gŖlnZu;QCQZ4~'5iG %KJI|peC6m۴i++s c0|x> Ŋahn]:B<SgjS3$TkE8'U9L-U/"8J%y%~7\;XAe? :<Ω%͗0DETM]Wd[zswh1T~6CUOJ[ea<7W6)Q@ڐbX\W⇺ur!p5iGEiu~+Kټuwdk1';[&aeifegU$__6wQF[v1 !r&W{W.n9r&Z@0 K>b0Uԩ!lbYJ2\w QV#/D0XLŶ_h 5H( ;Uq$USw @2+5rV=d@I:!Kj2<yWnV-,Bf9FjSLW\!_V=Y'Z߈B"!Ii؆즩'Ad2{{{(AQMS,LZ-XwOotZ+2CWㇸ6D0t[kPd!ݦؓ4;*E _)1P3TH.̨ 9{-&'PҞQ"QPcn;a=?o׶1XD #NHu#OG@ժT7Tόʔa傋ӘfP1\[)De}#jS,zv\!WY'>4y!ꑼA3Ih$IL&d1LhJ֧g)ί@otyno:\F鮑>}*T񇿷_N'RsW6}ŖT<,/Btjp3t )f{G}>޶fEHEeXu&ݹz%jYc*M}3@kS6C3?f| h+[7*tJTS}vSzU4DYT_C[4C՟ZQZ^TRUT7@di!xV-{AL8ѦgW+#U(H~:{'KzL:%whvz'wo'Rl?waoz}!*=.`M}c /0%xdl*ߥSO8 ?ëęv2ylH> vp"d(Mع_"l¢!5”6,ұ+YwT+]Ȁ.{,zrMzovjՀvڒ>}8 |.ytW+p[8rFNwj-M{2yLiF50%{A3>I0,|M;6WOAF Rֆ+<>".b[iޛY{BtՄ];%G2uyXbɞ .|PTnۊmxWmyS?Сy C#!}\J= Fm*@WggGO\0.@#~IL.Sv7d V=f6ce5AC!IEwCAa-}"[#Q9/ɲw=˳2q۶x&慟|6$AàCl5ҟ{:?8fmwzbviʳuJo>xrWVWdc ,C#qw8E;z{s\+J1~GϿ+\^Ki~"ue(H79?;ǵBECЅAo[3"iRų"+Ѽ>7^PVf&/K|- :E6F|ӡ@*3Ju;q!E9*T7TBfȕGB|٘,zhKOnG@)M5_2T|Bb՚E;헏K/:yE(Cv mh3gҮs/a>e[?E'ehQP5csk8rgqmmU;B% |Mi!(^x(8r12 IDAT~h J|(kRj5,>t Pn1|Z R^>UzTj}S!jXWm n.}{vӮ,%XR @w|ѓ)4 (xV|ak6nR0i%j˴n2Iue6{G7ty}2~A!~8 ѧ/MyV,!T[j'w:b{(kk6e-mM˿еmlW3s/dN,Y8Ȭ),Ԋܻq0'R_FL h)~7B?͈R53ܻ~qͨe+F+}'ZnԼ2|;!-ƸlQznAomA!oEr]כқrךC[)ɱ!BJ8!B !B!bB!{AB! BH5u!5G*ƯF;{Kׯ]?;vtSa/BH1$IbK/Tha!׃~ {AB !I/@2aB* .SCbƓ*-(?`:ũ,Ψ}c> FC %%% EEERk#d|###6j"I SiZ^ص˿9A}ABH`CIwYYY19*I._а`7E]lqB [d2 B3BB4$33P|+aܚ{Y|MME "Kr;lǎ|~r12KHi~3flQFswEFE%H8fmuvZU~hqó{"?OsGiIfޤHUÖ]9ol*/Ҧ q߹½7% q~JTyj[?Ϣg~~B?Z4VWkG={Ғ0 A4-ɊڵkINy ϻ>LMw8tsWNYb5sđ'*k_wj`RbһWUYVP(.]m΋zh5tQYS I @3E7ӳ93% 2i6BA0 .+H!;iW m u l< ?_7'Λ/<~tٶ*>yJ(?n6V GιȾu?*].z7ROHpPh -Pu{P6U),߳Aj#6{'vd_>x?l-*]^_{?x}{=D_3,#-XVvz   zU}8c@kQ?;LJR=k,?N]rT,jҺ-4]7nݸT uA ҈Yih:wi{6 |E -k"F6w5@vZ@E6f1 ~s'Y@f\vikMe`94Xl6ε^/Ղ] mF/c9i/ ְg2[{;EVsu) @ss{(@Ԛ;^g*wU{WԌG۸4]d%%3iCu '1G"omWށj\>W,} /f1 ]+R)lHMx%Rҡf:K9U47uӐ!m$AڍWZftY`!I Mh< 5 ]RYGg"c:V I^J:;v\7&(wvhi!Kf>WrU,Bu]fF%MS>F3 ئ{wO cչlLn$7)K9@rRcc>}p;6Wq2ck˪4zWVB*U֪9$Jx:CCs)P,7*@UGpͻ4Wɯ`^Q-unJUKBڐb ABI+ /]~qN? og꩓œ²qz؅G=OU4"-,=U9~X\WJ/aj勨:u1˄BPGSS JSZjrƷ+!Ԉ*"Vm)PDAvׇ?ڷTl ~OI$d Kj2<yWnV-,Bf9/߂ Y$P<_C0XLŖTbT\ͮK2`u#_mU Sh ?zíx 0lGvvږ vъ8-"ԍY}zףT7Tόʔa^qRWpӬ ~ @T@JE d$Gϭ#|C btʱ4;*E ɮC"WmPrU,BuQBFp+4Zp?k[?V#QV幢Pu'D @:hfd{ҷn [R9-ѻotqN,΂xq?ʝ/tn!u+CĪEZژPᷞU-49v Ѭ WmWՅB*K"Ϝs*Z5BHp3t "d{G}jm_uC:Y`(XtnjOhZʖPqj-_7ۇ<0V7l6^-)π ;0;L(yeorZha?}`wuӪSԆ[k 7CjZ񗍌EXfoe \|BFnGj/⹖}^5ZBuثf;~os~S#{?0|7rS;miϝcH3y) ym-ባ_8 m[1;OwMg_l4Ѷ ;+7^MX4Ġ/btǤs ^\|j'}rFp"s֬c#Gm[$` Wڧ75dk!aR\`)HU"YkGNx=O,跶;FZ*Gޭ'IaqSlR.ȠzNY:.d9>"f)- Z oӋgh=tl'5r~[ץ;!T e@5ӝc9֕Y~g8-S_TmGޯJj^%cمW刵]lzumUH>1ntģ _>Od:<,Ydσ>([k7m6"=^ybdَNy:ע˦&߮ +Pieߖ|:7ǟ yivX/zPgl- AdY^2^3->ݴi朢V$>f~Ǻ A7gsw"۞}Nå{g*\=)|{m~8^΢EADիN:) P W ߇?~r/Sz :f@gaڎ=EH..ܑ&_6hή>i™1N=ָ7"+,+?F"Ӭ5E%>O3Uu?77r̴76=Kх0A5"*#bí,h}n7ψ}^Goʕe_g0{w/,ڒE&뾢gʁ?vE/N=S{ /B6ٓs|/nb/O6Ҕezdݻ'} #M| &ӨHˤkb/I^y6P3dr{Xڿ.RWr aPu}Yji6>qy=Ձ`70c`[EϞ,$7d%|&}f.'DޒJˈ)Ma:b`se%M&"dHI^jje׍Լy sov\ $~IgĄӡCg_^A䅿Uմ [N>quٴRB J)kU\u%Ia `*IM?J--=?NJ -uBZkwDnlO܄ct4Y.,-S &AD'& <_iW(sHSS, 3ҟS#ee.;ې%E=F:A|E9@u-}otڮlfZr"O-`4QggD(9_Z^|.9fvv[*JE/sW`j,(moI/V@*-$}iXpAoMx0tmLU=eZ1 \$@DV.GU1UɌ'oYe4YiJJTQblq3ǧ&^b3z.b~O:{eMʮD_ز|z*Gqy2;e-L+5S- ߈ ؋L>rq}\{/C RŴ?36^*m&Xj*lBvmmQ]95Sѣ :e`^_”\1M徹u}tșgY%Q^4V  j-hVԲ޶xgd8l}a,yϩzkEf-}EEgu߱o^o3w펉܄Sށtn5+wowdӚܵHJU`?YM\̈UfJ5 ^Nj[G].ci7{J@(UyI+uWs,ӨߨV鿼ZyQ%~# } 2ې-\#Ow6gC$ED^vRCZʾyeOZ9gXQr&]|@;BH&Ɏ{Iaݗ;E!O!J+cs!o?u @ B!bB! BH0A!`B!!B ޔ+|g%m;M+o-4•1/j%q/HLc]#V, {`iG-<^΢캖 Z1ӽņ=㛲$ /Xwjg\}FOM?\'B@ ȄgC&7JBA{}9]q9ԒyivWھfy䖦h ) Ȭ@ m IDATlLe?=o3n }J4lt${'aaiݡ.xMAi($ٴmG ={Q5Ũ,+pV/}'E;tD zPΨO>cՓyCݖv֜*)|Q*81޻u3-A!Ph&H *{A# @3XO>]"[mewM3e@ 7d0 iNff!mf9{H8JwWHXYDuYWJK~= 0qoɖ|7y_:VҼYa^F`c98z:JtDEPAƢ\Q;,Mm!jX @2 (Ih &[ja{gn^~V8*նҔ1”c& %ܶVNqbN2 څIr t9, #5fYT3TcAV?0nMY &OSNe YF8:F2B35ޠbILϴeA5KF97kI%~Qڋvl)1=(˅P 3Kh %@'-φ `l5]=悐Z<ɫY.e& 9&[fl!_nsVk%!?QC-D%i駯yI[uZu?}+5-S{}WnX|@3M M.,B} ֟ I,Id@2Hva}_sC 2^p)Ui-1lY^9!Gܙ}KYI[W_KhZ*9**B!jȹ yY_NMO?}6 |$~"ˉ+Iv%݉.9&1՚vǨlYE3Z2Hb?0!bh(ȉM3*7o0S%$G;7MfjY;8Xrr_ڵVڷrhٚ_KڢbB+oԾ9 ].]6u3܇I7^|}uCd]$yhnNF_o)oRCAEE!ꮡb$42IɫJN4''""*.zA[q}D iZtGiTTDp@ |#cՓĝ)USߛ gF w' ^˞따8 >7j,ziے.Vy}\%~s[IџֺjQ=vMlsZJ,޳ ?Q1H++aDDS<`s>fiYm+c?&c闄aatʳTT D! uKb!fƬ=Rםq<;R1i:T5gIf0Ib_|m7Ϸ<*lS c^%|E˞5PUs hTT@q;݋4b)V%AuyZSݖd+ayew݊}V@P//l~~DHve30_ݸbo?VL2;vۻ(.w ˲tDBDc̊bWĂb,"!XD,QbX@DEc+*hb*Xvw?P&y왹{ihV-?~e^xeON8{t:_v}+9Yfh.#|9˽- ^nQ?"qOak6"g"tv~檬ǁC&ɲ\xz9KECGGWF!M <јZ?nOcnQǟ<," O#֯(SڼӠY> RI?q SX{c9o,[K-fiLeD)}q.˙F;UadPe^o XrGuy쪛V5UsqQ_:'u8t̼vIMO/ٸ+s ii=|^9 XR8d2-rgm&DLYRĽ1T \6%ή*7oWgy G5 L>8v)9ϞԉE ''W~( VǑgQ=6iFRsO5mƎI ='-t!Az^xOgDD[gRe_nwGu8FmޖԖV)[)0fp;FI*-í7ov֊2\<}ml>~r͖xQ;<\/2׭#˧*`clgbf] aixL.WKž4uP; k<6?{1|gk<6Ƌ"YFLHuc,=}Y}iK391O}/fw\<"]\=şu)2t+b+2:q/Q=(R4JDшmgwOV;r.}ºz=b%{w8 Q|yYpM.F|;x7ɨv Yo}.D~׬Y5قUH&zhi X|ę~QNԾvڮډzw%ÂW\p̆Msje1nEoEʈd,>N]Ze;,mcׯ/TcrEU#Pn[iu=Δr T 4:͙9DYę}-~AD׿ X!Q30)3BI^JǬS 5Zw2_g2DD\=cM6JDT,6cO<=iDDV+"pDlM}u68*l""4+"gFV."GB _ŵs]<"UYi~jvin׾IˊER"yZob%B|=f阚k|0Db2ҀwYA||<0obӞ#&OO5 o(x~^Yj5pU5UDbY\uۅXu"yIDD% Z'M!hqLWp+WH('YZ]pwwȏLyb?djQaIUDpuL2m-yg3HZ/"FHZn8u)h$W?dlqqqqqq/tWK',")w^Q7`r7mR0]>ۢ.\J<-v:&+=<aIDD Cl*sws9کSy7os]&v|=KҾZoHP?AΆs`Ϸqӷ^-Teڿ2Ғ¢w J,))zJ 8|7 ŵ=^)QtdK*<8KE:٭d9t0YʒEyQY/$旊$Źr:_}7<Ec{tǔA'e&Dog4ع[|*6)A <)So,/~« "9kʽ.޻Mk2xsi#5mdTG;1D7ZINM}wE,R폈5=cVjal]y m*wCIK+]>Ͽ}SXtХ 4SQ$F5͒ Y}ŏt춈g<vpo\.,+BWquFm,auU/ukթ'VsBi[Gghff>1*ȅ`gjh (@luO=*ne Z HA@ p"UP HA@  %@ M$ykT} TOʈ+<)JT u>G yIt{>koS"j(wjɔ&5JQ;<ſr0EPcf\H~S{ &Ӽuo[0w*?wV̅JчXY#97xP{Q'sw\aqB>ġ?|Q Xν Vv=w+QLw*xsOawыW04m<Ͱ#쭄}- T ۋYsOfr'4NQ݅VJ̐45|€eQgܜ{ w6YW'lNŏWqb>KoHyuBк+R*Ufd97xh'.y+֭82^;M i+ܭ[U2ZY+/+}z*&+wV&],wFjMy_^[Nrk#ie?mj넓?e(l)}ֵèE+) ]6®]=SskJIr4%+~6BN$0$I?\Gl~&/卵vrJiះ,gsؿc2eDT#7tܿWf؂U?LAx)ms1ۦm/KN[7 50㘟;o˼1ÂO_fts1I.4<&f a?@)z)ШKX+6NjTFvee]=xmH<SW_uyreQofE뿒ƫqqqqq7V;iی3(l(iA/Zix-RgTyW'ge$+=*wQLE\b6{wŃK:Q5IDDLq_M򦝏tNkok4D__镭؋>_1^2bB"u' c!E{WNChN\-4 HX6_; >_劤D̝#ԯ/wbͽc˨ΞϲUsmuQK-Ҍˇ=^[9s;G7`qiOUGXAn"⬄,vDb)h {Hj}G@G~ D$+NNHW5=9UQq4do:ƀ1HR%μupR!"j rr~ݱh+]yujye:. 3>*m>7R!_T\}`2"0nKRbqow5""*ycTg#yt*˾fW=-ؼ¢RiEWָT(~צzWV(hoW'.lgN}5[G܋~R?K~kC6>8򏬼;&KY*^uoDR+ܦz=ZZs4$㭮voA gн}mzQ9Frk]|) g+ͧc5XvLhoYi; "56dwזr rcrä6 ~SnͬvM(KZW?7Beck/ˆK2zF'i[GghJēQc ;ݬې%W-r )dL_h_%┳1կ`UfJ4x碄]SlyƈokFl8=5i>ͫm"5cnNgۭٹJ5ն|mR:j SO&Q뎤yW>M}'.neBz oHuS?T$a.-CU;շl6 ?֠ICER->1cn 8| ]F\A18J)(RP HA@ 2IϻV@s{LqP* HA0eIсӝ{u {%[;=G9vZ:kc+qyQ㏥ɈBcJc_;>X1/ֺ!roE^g J,gsؿc2dҌeއa/GMhjIDAT3y YkIW0GE,/_qoXEů/*}|_fxpI9 HX6_; >_劤f\>T 4:͙9#n^I()tΆϯ>+)Jan\xXKǢ"GOQvB߽dٓB+)QiՂw^⴫?,S(Z vT̒.OVw~R_a[Sy7DuyYNֶc( WLY3)DDǯey92൹ DcحP͎P~~ ڷ$ɭ;l\ 3Xmd=dުNΆ,_=6Licqּ|yϗ:]m%MVj$J*4*2I/p߉N=Bu1v̑ʟ(-#&"ǟR"<8hCnwN ѹLE۴={8Z !VW7[4U}u3w {pt1c- İq3 N.H'ppɓO,Ls/ac&t-M;9#<-?TK^̵Wie$R U> Ј.R3'ٓ:(r"*yqOt4~mOtfU;Ȳ/};;$S[߶l{R+--S#+˭Qޱ7V/~ξ)jYUkS;LDi!bjp]:f]̈́e29{YiH}ǦWb//,~xQ~*IXk蹘CmӶzn/7uP; :V6]3,iF}<<+d1!XQ_w5,gsؿc2e|O'F] NJX9^D$wb#K/}>f,wޜf] aixL.WKVYc<4daA*7ܘęRʗש'b0w\-"!C>k|<,%YD ^f5ۜ/ɔ3Jc .!S׭jR!|n]#C' 鯥:MDlZ FlS+"8VX/-f*Dvڮif浚Dsg/^aǙLqYwmq&Uz/r<<*cbb2y>q6 u,fPCdҌˇzld'8͙{vӉSt@2d"N!#7_vמ]mYI$hD")./k%SOT"oۚcIlZM'{="\ٰiNԹI~fˉt7Q凧tHAr^J$}wQjԩ?~mi_ܱ"uˁs[kʛXW~yl"EgfFQ}3dDHyK#T?%y)yDN-xDhɈ~~%O ^Bf`SҐ ڷت-;qꤧHJɰVCg#yeyNf&v^k]*͗i~P`M3hJDTZo҂BN%?,FqjHQ 5oݬ*LalE .k[|!lF""u|<0obӞ#&OO'wf$yIfoW'TUtowU)Y6B5;U!"hn+=dƜ|ނ~~⮜r.kMX^GP(;If'*/_}p:DIRŶ᝴(3yi9E5]+.w߷9[gna%od%qlPnAyXvLhoYi; xFV_#݆,]r*9FWбyL MsXaRNl˲a풌I(5[n{۫8 :6r4;_Ocl]~60*Щ*}M*?B}ǻQ׎//uR+އb艳~gک-M98vȆg].9h+;hC _/&i!`> Ulmx@S%ˈ{ް;[׿ǠI5IL&J}Cky( d1%ϣ_zޫ"_Ӟ<<'~ݼ\eߤkb_5Ϳp.O5< C9HA@  %@ J)4&B~81l^3ןOKE[o,j$]]+~Tcp@YT,=mqg1RQS-_Ҽfq4屸s_*( ߢݤiy ןV)KܫPhe7ss!N g'f=zm_H}k(BCgm}%fH>Crne?7nlBۑQrڬ4m<Ͱ#쭄}- T Sj-_i;t DOŗ5^ce_Z"ccxs}qO(fD.> >}9:lED$ͼQ4bݓί?k?.O+ef+{F,^xJ5\̡i[=7ċ݀t+"*O~>1,NVok~ڧ`i1H"TcrEf\>c#;piܳ7NtE l_ءYW>?#*Kv/Ÿ~:{]?fl⿪ş:_I%A]%ߝVŧew]W!e Xl ,$B,Ss*5H y:&&ũ"bjb>X $/P`BP:LmIzBDw/t(HA@Yتz-Z4W| .:lv-=('MQ+GPC|eZCĈ_ ZkַSWMMC~F Cj,Ĉ]NW|ЗuWY|D7|R%μupR_T\}`.I8GwLtQVQnB{F[Տ¾4[go[Oe?868 xX"h|aRh:P3j#gH{{k/ˆK2zF'+~ېeE\ؼv}90M+^YU,\V;Nq7 /K ."Cp a1<!o,XPh<8?Wx#8 aĩ;˝MiWv,J1 %@ J)(RP HA@  %@ J)(RP HA@  %@ J)(RP HA@ pSSSQv 1HA@  %@ J)(RP HA@ p 1,a WOG,nЃ”E5N 1xG (  J*( 4!2{5%FRʇ /Ppf?*H!hrT._k> h2L/Ԁ!hZjd!1MKPh>rFHA@ p")(RP HA@  %@ J)(̴ 2IENDB`././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/doc/source/user/figures/add_to_env/drag_and_drop.png0000664000175000017500000022200000000000000025200 0ustar00zuulzuul00000000000000PNG  IHDRNc sBITOtEXtSoftwareShutterc IDATx{\W7on.l-"*VD */Vq)]ytE\w"Uaۂ!" $A2\#\k[lW_09 sg9\.B!}ac B!0 B!FB!¨B!BuB!B0o00:iiѵ44RhNG5jJgM\S+(\Όt߿s  !B!Kcaǵ-͌N:-CӺ&JC5ꚟN!1bFp8,tbA`l6fpKok-ہFi2lFB!^Ӡe)ڎ$8,p86bq<.8bl6bY,6J}4!Mm-h\ہ/>8 !B0VSOb\Ŗm a7[G_[j,J0 bvEB!uNe5[ V X1:Y~aX@{aB0b1-fcDer5X@!A-RX"&,ЯuX,lbjgp8tۺBiΝ;Ǐj Zp!IаuVOOOPxӟ4evP1Bcl2{{{rʹs-ZTVVVXXb~{OGonHMw=,`0?Tن%2͌Nk6*Oh~,,,{-B1hР… "SRRh>|{8jff6p@8uTKKСCcM'S<6pYz_66- !H0bkJZV$g-tW49w>R"8dAض$ A,].;s= "HH4= ;Jbr:o|!+' ^~~ܹPҴrmW;,5P "&)yIXKVƛnKIZOH, ڕjV8n۳^eGBcvyZoWyBSN{*_'/buvP.M~RmQeYG9@؜Xph3Im t8 1EIYvLHPKP $Aai2ĵk ;?ΝG"-96B`SI.&G.rZo۳pf|/a{ D"GS#9 th oO2z&&&tV$Ϝ974qM.Pc!|ƓGkVG){:lezZբLSo { X +_LO,CtM~F*:]Zvdzѕ%G/7K3}IDQV s$ڙ跳# *,yQyBIV9 `Jϝ(XNˮ](Il) ȨÿhiAfv~7n s%*(!}<@3B%!Hgg^dP @8k.^a/,9ؙR (9j!Ml$ jȮeY5YK8]7WmÜB1hGЌ_Z`U8_{+tJk/g}]<{f5aLΚ'@].F.*buYYB‚Y5>Q5jKMm4X/ZyQ@kB.grϕwu7ǖ ٿHHFtS?!h9 =:,]elK^seW50 mKcX`[j?4k4lK>nm9 =8::Θ1C۶/ahw}#FMEuXmMau E- bZY,˞{,`x:!=7/[ 0ԎQ)YGA)$Ih\t'|,rngJ 晶@umc;)Et1M gSSB-)JJ|v]Z@8/$|Bćli mCl]{~N"hR+Nj?]A2gvHL&EgB MQC}v&2 :?i]kaє]AI-rPйF@LЖM䄅x!^ceGO~7Mke X\.02['{l}աKc3'%tF"BD+S!IW hRbiS92_w9qrCېbjOsF|Lsh?zn\k2pt1Mru5]{uCKC{H{O($.;Xٓ jK柶*=k 0&yŹwl3P$ЊvO%~qㄻ32;>NVnڎrCc}eEv"y86}؂g4EXX,6E9FͦUPRTcS__0~ hy!XXX%MLFe:(hS7Z'uoʔXXZZZ._4PBO0k4}Ҁ߈=l ms`7J2 d@a~t|-͎QD= HW?*~UY"pq *32h 2 hg/;B@9˳貌#e4ݝMY22uIdɑ,) TIZg^[-B0ԇ>&xOW 5[DQYN`ki}W өZa'Gۏ^hu`ͪ[TV7q8nŜX,Ç H$I&u T~[PPP(ݽ`׮]˗/VKaot2^GQ!cbaTnOĔ2r~˕| ;*lxcǑS?xC$ӲMں2`<ٕ,ڸ/^c4~b+ޏw aQ3bgqGHe!֧9M;"ĥ(B0.̝)м7ȃ!KKdrZ!džb"`mZgBXL-\{.w>{'?-8157"2>q, ; kp3v~]*ym-H;X }FƬLl^[R{J¨0YLY~ #ϔh걙ޭ@0bUI3Hkw?/kyvk碘+erp^̑O At.% NVyt}*tҘ,$?統D ÖocpAԆHݤ~YoTGDʖ]A۾W?6e{ ;,\"1 h.ޱ`hr7t\)hhh:Z0'4Sdmm)xRoL6c;7@jۜCH$vvv˖-x [nJ̬b[YYӧOw]__fΜ9v;w5jΜ9ܜhs8dqiق%oꥺmddq6ի7x ;ϕo[o.sꛯc6i{;%c",_5 `~ 6_;uV3?;eBF&fQt:xGyg~]_B::)դl|:Q lc.ˉ46Ǵ>fMFg0|r@-;9::~GϟjoV79sD"??? r>Ν; $Izzz4<\ooo\ )MA9Ƹ1?"*iFeuaW h40ESXMFߦ2N9 gr8-Zf8`Etz-/oVqNHYL 2 hP;9!~I"I~KtQH8_1]!vxͿ$E{uJ4(|޵41bNGN+lhh9$Gpy|>brnO7 >'e1 ꀾW! ?Ev+ˈUb1",BwN^oxݧþ\@`ckna@3fUWYՁN Z PgyKP Ћ a9 B$" ~ΧO`pprm1 WZ;IT- 4`t+X >hG0#B!Ke:>;IX@RN0~ 0 &v&A/IV㻅B!zA/:Wk7nlY~cDŽds>pYx[;+چ7!B!TdUK8\)G1 !j!B/ B!P"@!BaA!B!:!B!Q!B!0 B!ac) B!~oQںY}80LaNZZnhhxB!+:|> VjC!BlA< p8V1!B! o~gN!B_$ kt ð` BWu}~+aأB!^!97Ӟm':!BB!BaA!BaA!B!:!B!Q6LKXB!FB!¨B!¨B!BuB!BB!BaA!B>X jjNâ@!CaA=vްT*M0 ZViZRUVVN8O9?!BdFFF=^cj !B菌~QpB!61 B!@0 B!0 B!FB!¨B!BuB!BB!BB!BaA!BE`T,XNOM<[oY&<+:tU IDATS)߸:k'Ksǿ8!dU/+)ۙu)d5xx,ラVgZ]Ђ<4;=_|֭teus2P)_YbK~Q/j>L\BrMm_ԥ{>;. (ec]]Ysąu-%mFO & !F􇧼GNyWo%ta?7LAۛzoô |u?;uݍ0$@kXN΍L?X%<sHޙjcyU^]Xmݾ*N_INH-@4>0wFB'N\od=YɄϏ 7Ͷ|'`|$p"_l# y9_$FBw&ۂ{n0tڼu9uuJӬ?/r(K?u+pzw <[:Bs \4Zq|Ǒ8 ~ȱ~nc`=,U'`B^fdW KW[&_d{}!}/_KX2f1W.&Z=a̞=&~(KN y=s=!zC\t~@zoDOJսI||\O*gddR4-ygioD11GϞ>}*q|gDR \њm{;AzO 'gϞ={*C:7O2~pYfߗ2rc:E T?%E~7ʿ{OeaLB&X;ڹqgnEG] Pq":lj7gO . Q CFܝy߁Eaw3E |Rj =vhZЧ1QZOq<|K[ݏถO/hEpZO>C ]߷LsH | iT𸠩SC>ۦE\ho7km>>}Ӣi k^M y2ci~+om'dJa謀$wHU>)[ij9!N|o5.Y|N"ߛ2L7"%Njv/qɇS@01TSɮ׷ES7%&@(>،'}'HjTp MU蠱L?3!pM牖,Ӂ±&B ) Z0J(\%\ͽw3O @L3P!5ef~bda+#xf!+< jKxߧJ,}#{+WR w{+ΐ\N-έ׀`L1y.<+1VboI p{RRA͵)F:>`K&T_*-OjJPQ$ @Sr @44!"Ϡ E$AUn SOMN"H#$\o-óн?lzHPU&FG3uuC{/1ݑNHͫCmF'%wgUqΝ:P"D"Bb!EE.44M)-TXzؽ?o GՏ !=>TMijԚ%AbywM:.|BSHk"U8# v>8yd|W\%,olP&EݬhnOA}f$01e:?EN]V±FhZ(jܜ3uyjmHB[khm}h% Qd)P+ BcEsLJ5\|XXXSZE~;[xf#njt᭺J"=8L6xH~}yWE'?{L.#TeOGWTuSҽ,#}z.nUpzJNab:7 N"NM}%_@Uqx9Vؕ!Tq&꾆Pj9QbBODo^Y5iUm>#Ugд([g,3hO>]Xfi:#ۡ #2ezul{k$XV`EY7EeH(3#Kcprꉭu+_P4׵ߺV0:n Vb:^Ns@7w\f3ydgE@Ǔ_-u1s*ɰ7xmM3+1Axܘ x3DblX\%ٽdD3ln~n|xut4!.~ j'{/M@ǬkluRӔ7.Aɻw& [G$`@ǰٳDbшy6, "޽dx:.k# ϶c %̘ˢO%ŎSM_-Ǣ\&v!v2=r1E*tpğgSWҁLu pXݥ%sU[V;vron:oĭa8tr֢^eQ@ۢ3- My8`ҕ[@@S_Drߢ-,G?s;qHasqB!X*W?lRΕɸ]>[ynR\;{Dy-=H!>CgV^JK \{ 粫ޫW'}KERo>Ǫ8T sn< t:VjiVT'N|Wx+9_Y/ Em~pنS&`"B1111N:h*;ub:yyЌ.߉#<{88mi3Ɖ}EpݩyJɸA3y`ˡb|e=T]{ww]1ǞKGi.`}l9p \j]]prc)ϟ);{+tZ_R7yƼ B [X\q]AI5Xz:Y뗫ZuqO_Ǫ=L3 AQ=߶EqZzyn֕]x/FQVތob :&.Z؋g7nܸqwUX!Bꨫ)r5cl!Vށ{.5Oa;3Z⢹?%VE:K}ۆ3; Q¬'3.-f(79~,BcQKsodon]T[q< @$-xc.F}/[@ K}EK,vKGao>FIa;G7\OαօI7/:/K2o-!,4pLg+o~s@UVV)l(iw2w~s'$k l~`R3ũ.ݢ_vۛI+h#I+cY#BZ_K\{Wgs6璙Åf+xbAT9_r˛_eT냎9@0|q'YUX!P?RQP0NjZiJUYY9q>7PK`Iɗթ7g -?i_UuO^YQO6#|eK}0w{5Se&4u|iѥ`y8};30of ' i1|;ni.ϕiߢ9Yl@||̊嗇s oUSsb7^KCyLҗ. 9i_>^4u晤/_(,0w|_|0:Py7ᬜjn= ,^[)WX'd۾0wr2/j;s݄KWدTAP^IxEmYa$&}0w5/JM0' ANjz+/uiOWf|&'_m[ܩU4UG\:EK'v1xr㊸/Jח^qR݃ؐi.Tܺp)I]Qڞݠi}wCח^Ju1Dy&`Ԋ؄jMLBnmmŬÇJix#1<{[r~&G`(?,R=Pr&%x 3}pߚzY3i %Ҍwa9 G9)>zYS> &5HͿ2U[Vn NY1Ų{k~wJ8Y1 IDATq-ymU{aL&&on]ك/zG8'gˏ*stF+!lk-9`pرoj ZYi*cZ#!<7) <_[x<ձ~pWSdKu᜜-RVz-J_LP_9%Dﳖ~nkfF9'KUeRRFjRoytF.bcғ' OnYy40askLp8ugæF +WTE}9DAUTV\8-߻s]P/Jxn.|m~Ǎ/D/ڻoCzx ^X$$K5f_Tnn[~$>`B(B6m_S*TUިG/k!GWIN# b߂Ll{፴vɃbd#ps7 /=p,g&.!#6= ı*5]|VuS_&`jtq-O۸I(@CHHޘ<01%#bqöʰlOuÀ!FY oun< z05vXbƭ{ )D/!ʷKkD}*-m\f;eH}@xVnϒۢ]ֶ/)]s_fG[ [0(?}C νsdKQTaxƞ.;i0Ӌi}g/8'IOl!և/|ep?112fq3-o;#TwO&Fmްnb_! i?H/QBϧnRec ,xF "g{틬-MttCV atj0L@p3-O;L>V+izuH|bYjf:K){գҐ:K7F,^)B>`ty#'V$W`*xt𴚫R9IpCG/ D&0,6rgW?/r \9ռ:Du͘nGSh2h+ IG}wuXBmIb8Cdeu4cHcO(R?SJUM<:BXQ nvzbFLH/˷2b٨((If!"5uN:P ~E˿~un둑1ˆ•hOƤnzec>/m.@dUeMt~/ Mh„o0 UKXmɐ,IP2Fq[ 4 $Ȕ1oI%;^$KhEcK$ilRd>@$. Bpaݸ"iה$J)EhwIW6YfUz[]v\giu[rwh""e?@-mvs߿ӽ^-V}Kp g| ~LFNcpE?^dƟᠫd?뤊;ܣDtپ=yǻf-Ԟ=raZŒU?_XFSs3dX4 bPk'iT'ԆEc_CydICvї.uno"u&s58:7/"T,Q(uz}IZC3|ֲ ;5Uy ;DD4m^,@_Kg9h?aϮSVZ_@Cد}}Oɼ{X+?#_rnM '^q,0xw _%*[5[9gmM=_]`!lRuFd387`ґfnU&d4x<ܭ_&<(˴ 8 ͉:DD‘WN<ʸ:9;gW6=q"FCWc0g27Kg;2!쏟ΗdfMIi'Z`pk'3O'Bl?}`mY_k;ܯT}Q3$W",QGwCUDR$}zjD:]Q4( 46hdDWuԙZ `#O_Mb2495{#SM'[dИm7Um.O6 ""b!a2BYl{5_&F\,WMMVBJM|kR_LPSg(7u{(UJeDDD:D7pQ2pPQe^MD:u Mc5-S-Ӧ'] IիaD|_*ymH҄Є;-cڟ*U|(эu2Rd4̡rqH5`4gŖYd1tr2&`1ڜ 6ZGj*^ghvufUUlfZ6T6H;^(84)f^ϛhrtq֕mK.,Ug]<"VSe5w4^s52qՏ[2Z\h4m~s%/Vqκ +mJv4  Iݜ&S)fBYV [^BHE Fl["!ETʤnfi07?(.(Cd:.Kn>v\i9iWx}P/XVghr&pV+4͆{W2v'++F!OqGU)7ibFlpW뵗tcE[% kl>.k}~*unV>?z:˗/g)5q…^zO>}}R*M41}}Ն N%ũujdzh +e˫F[q*^YvS&o*$\f|D2&Avţhmel9*ūSCYU6cƅ@`&e}2Ɲve:%CDDD?KVXd$b=54 ci Mzw]XDž@|Ԉ?oK3s~]> d1cCi3%9:F.}c؁uuuuuQQQhDW{|^xW'c5@{m#}Qk;g1we,DDDDDD%wv7{$!/tZ{=Wܱrw ^}+V'8or#|o?/ 녧b+rcI_= @_>5cs]cn0BxDDDDD+ljcSX_hٻX:7x7^{νgl_h|xϩoX3̸?+kcl8fޗĜsΙ*?cjaC""""N{704ݽRusᇗ3~`cxxhw!V1mQ5D?,eҹ釱w{6\}eMف}TL(X)o7u3f#".إ(ެi::cKy&軳A&DL::ivw@[G2wu|F>#F5o4}` ܍2L#ǘshRl3Ksj-MX:p:-={jyԄBÇmG%ygz1 uqe'uSA&i@Og?/pp?H~"$bwHӴw_dy I.{%O=(Wq…^zO>hX2DDDDtk:q3V<<9nwٽ3W>lNeLDDDDD:F7"v`#"""oT11111.Knh`؁uuuuu!`u4.Qxxxtttxx8u&i9wL&:Mv嗷z+X;XGGGllT*eΙT*XDDDĨC4L6p"##}>ˁuOMDDDĨCDDDDDĨCDDDDDt] lymǟۀ{6Evy;ץsD?"Oռ;~:kB߽x tHyp;"""""ˉ:ieCW:a v{> Κ{t R#9DDDDDtYQ'@}I'9zE˼N} ^䣉,Ŗտ z7~p Dh2g?yt g.zHXb_^uۂu= xNC'ۼ8ZRE"p_:9]k熣i{sg$1op}bDtҝs-GױCg$p6v.Z4؛z_>&ϧ7\;N랪wk~~Gc9i"\ NÓ=sR.oYtܖjmb7?t!p콪GO;}T """"D[u-NP(?;̞;剳cֲ&qٳ4vzɃ{1M]Yyp>f 2Z&Ampt;] 2vnt:!k071p OږiasRҒ"O=.=j ?XŴCDDDDg9ϼJH{padw:s߯=ǿOMcb(鮎E]!r#?\Hr]#{*ظs9g_}?JS"""",&4VD//΅ U &}G4zO<90?zc^@ #Rmw:|vh}T^ہ^l' h6<;d`҄w~03)Q6^toJ^zr3 E 1\UzR@0=aU2Wl%CDDDDDj=,'{yߖ0},'=baG8zI%HE3jC%v_@60;xF)9 Sd85w~ױ~b?6YٲD>PXVxA@p^gohln X8(w=/?<=_Oa&`ZǻtK_smeqD Շw̹Iۡƿo#gn1kGz[#""kh/WgK=ēl|ĉyM0ݬ'Z0kц3)^"lʬo?^xOx0oX3?;Ғ#rso=~'{l[bKK#nTDFf<>뿦%{^>t Vl˺<}ڧ}heY9ygꟼkO]S @ @o>_Jm gt߂=eK9C}pj2= sޡ[}v=ywU>*>gcxn :5~:&oԙ~S?s{n bVĘ}:?l+eAD%>@x3~^( @ĬY. "6V?˖}G`_;ۢSRgݣ}4E E͊tbݲYBQJ4hm;UJ"fE5N+v58%Jz{98G D>1M \uSSGI9v Cwxtu /@Xxm$QӢM @4=< pҁfN@"] &Sf IDATE/D[ׅ&/Wo=ˡȡhR- }y !/>=SHZ& ^Pъ% 8FDDDW;D1K1H(ta "*b]݈X!CnJ_?YG7fwv#Y=#}uG/XLȮi?[l*h¦DE>‚?~|U^S%]O4'S$ l0 Dڴw]:ʗ{/t]@46E_";MIHԬ'巆kZ}C4E/ Nf͉G?8Y˅ "EX ~pW`ZnV/iw}nt ĠnwwDT%@wg70 nApZ,vw"4*"g~/ןBZVUT(7M#wӦ[TIP5cga=\y`Dt|Տ0l~zKIo?3EF/E 1ә=]81Z\K󖪁:uaeʌ)zOLDj?фqTzG^N?ݳw͂Y7㷏w}qe/c]Qmv? o&5N3e p͏]~~*w7/ρ~f͎чi@ɜۻ\~xjdp|=>=`Q`,[mada\wj8U1m՜K^f;"awsGPE~]y/APU?;t <|~t !1<{;M7gфO,d-^55/"=24`ڲVϺHz&s4.c_bo.^]QF=jwū?YD/Z*q(*w ^hic7Q-羿㊗VB(艬oI[L5szƓ `>2߾j\0e9;@X2yˮ澰H L wʶ/LUVe޺UK/m//}e!L|w3"d޽${ϺWdDU˖;rEx6=:y/_zxZEѲ4S#""z!\ug˿~iYQ:I:ckOhrX"iӆf܀a>5""ɂ؈hRj]T|yr֭<5"""8v`vbэ}'QQQ;z@DŽ% """"7,_@ą z{{{{{}>=}4 v`#"""""F""""""F""""""F""""""F""""""F"""""yXtH$,6s'Nz7oO+pyͱqvpϞ8YDDDDDPNwӞ?9rc+{u!@ۡ{rK{ x}B|;_SS{νoj}Ȇc{cn:ό׬|EQ@w;w=HJ׼s*"!i":#`;bC0?r&c{jc9d2VwHc(]_ QB3;9K=B17.6̿Fώj4/l;>8[ o8[ U.FkGx&Oαu@xd/p+""""N4pucV@7_77oyZ@xTڛT=3"@+7ܤ95=+Mڽ}{9M&cn8?杗 ~r3{woj~__կ{tX#q__T!+"V Pw;HcgDH#hkaΙ-{_YL&V(;? DDDDDZu|l^S͉7WMIK=ljGA?{wpܶ$N(v56@w7@w˩F⾤?I[$쎗ޚ =9>L0b/B"""" ?DΜw)}q%T,OQaߑ~y~e#?+3VczᏎv(3?>z)_~p#2eqw(~vKKjq-)Go<ԥXt?k]1a~}͂s_oooooogg9sXDDDDtx8QQB_ }ql?]p/r|>=}Faѕ.z=KV^!ms%I9DDDDDt}GpGpŒ\kGDDDDD^: ؈é̈j|===, Z3f̈D:DDDĪ56% ibccb1 =>e!""+ڞ8q"**j޼y~4mwww{{'͛Ǵkmm @0e}DrܹVBCDDDjuu+(WXm &^mK>{lXWnJ  NT2X-zN?|&wG~fh^_$-.0Y]>8.'?7M{JV6$jseuf@JI* lAJ@s`YQkjlk߷`XL!o:3^|TY_R\n:}HnKeq\*WnOnS V6CU_hq|#mTW-e*J1TqUöݎYdY^:7IZLҨe/=^攭ڽ'WWRVn8=$r6{}>^:yʤm@=ՙjB'KLef7tBr[un UV@FڮS]M$TYciRJf5 otVV"v,%5#U!u5VWl06U.aXҵk*ldx)ZVc:i}PmQbĩ5JYfqGeZ eU>SD'+~\6/sH)rdF󋻋BR˶sj:={^Y6@:~!h Kói uN@$OHIV.l)6י J#X$|nKE^q-2`׺EE􁥖kS y"ܖut}RRWѾ2?q V]$*MR&Ʋ U; H*<puEIi1Vכ ڒ ׬ &Y!u;,fK6јkGv|h24E^.rٌu5NQČtsmQBjJ"V&J6?j|.b1YU7XipJkѡ˜4l\iSyڵl TE+\EJd})o_ޡn{ÁWKg}^xzmOEeK~Ub!i؍6 ) IJYuM9ê"h1i쬔ֲbS`Fk5ڢ_l}e۬撲¯}n];[@ZM7򫓫3 v묡05%ٕ @.TI-Fk3 B;5Q]H]P~|2q˖Tu6vZV;23ُl67f i$qJvT9usqay:3UVoh{u@\&3 %\n~~q9AS S*D4YƦ!R_#`4ڇ_ fh&ٛ\%I/z8j#X4T; ҆Tc%tR*M-`8neYM9]8//\/d91n{]6Yb9)];vU$2r'NSk2S%ZO\]P'\k(JK3钓o;TچV_:@mZ]d]vhަݲu>ض<20wRY]+mh}EҲ5gGkCy~fuk7WtZt%|]RRr^o3GNnON6_K3V6rEj=𡭣ȻO胆M;ЗG>0λ7|r"Faf.@c"NHSy?vVbECPn0RU輔}(Asn>ӾPXM那Dܭp]=zCk @M{GJUC7'SeO& V=].j3H4#{Zfp%ILz}DÒī/{<^;b'rulp,tD҄t<|QW@\2OHUOc{%.%q؞`WDWZ,KTpUlzpbE:Q` O8!C'-wHZRbU8x`wݖo(z;}ڤl\[:s [27TvW~%Gi-[-5]U{T.궬-nB[+K _&4{68hv'+֭) oSڍ5vU+U{PbEk֕YYUogw]WzC?LҜWvmF4,-O~sĘL~h]{K_腿O UDltqM@=/813]^S4v4RWFTE2덬tʥ಻N`s= םBcuiY칲 ;)dhu9nB:p6N^:bY pL2rO,qN4N>r5\ x|. V~guX8bbh3L:܂~UoZ_fZneHhZ6Yɔd7Vڠ~1E@ψ3U4&<> 83BW[g`BVϭ0nď0=qEiJPn.rnP$VVTah&ƋG$J,lݚP ̫ eiR։M2cp/se\YS(S-ٰlg]'`SyVXn/Ihm0}D7+"ܥLPFjkkl2bazz͛y;_z>>˒g&L̞HA׋s~aR\,Db!j*7x̛d[vXcM%>oHz4΢.%z.:3jDUxL"1o+sM,|`> *"|ݍK;Z^j u:K[(~|] fsGlr3$V6Z_SESXg6UE))S3Ue!30ig *rM.2%nI 24:}9Ǟ F:Dr7V`;]0w*=a0+jٛgy W)4q (S4ԉYT,XQajMŠl@U&IN0408C`HD:DrkfۘɠPm>P;."ѰّW MT;{/d/\$C?g'&us;_*[a.խ1Bh %Ȯ7>z]CLp7XRh*x'lMO0/hMFc\_Ux3OV]կ[4lyѮJpXSH*e'uH"if n@,pTfUTT(JD>Ƿ8n񑤍#w)t)IEa=xE!p٪bѡP7^WnOyeIo/peQ]uvsnnBT0}=>PbQ,Y:uScoeq54uԸot&)Nhݷg]ecNʠbH||X/ͅa+X`Je 1^/Xn8tY;{DKC,S _ߖ* հZ`)hҎ7͕dQJAi^)*@3\֑>K][<=>kbRziwC=|B"+/N&U$jZ}^7ܒ7-&l֭!]}mk Mwb;Դ%34n`tNH&j+  IDATEPj D&dF ɥpKŀ{.)|n`})Y=YjJ$|c+ .w$ծƜw0X,望ZشYRXOUD;N|[L麍:}}Tcq k3:18ZyrFd1K^&9؏~Na*yXMijp)l$>RbV%1k-n퐯ݦ̐g͗dwj54M|NLFi2=kkTՕWcRsWq9 $*JU^G ebRU fRk&͜^18I@ek2!5IdP&Zc5ꇍjHzJML6fgɞ=iD DM?㳵dntHS3*tV}=_rOMV,Mɺ knQ(6Zt7*eiFd# V&+Qo5ٽohmQsjɯ4. l4r 6@2F+U[䃓L!PJ` r&V&PXotу>81ldTdn&$p[*^uѰ; do~emlAĄ/ɱD"X&t1H7 6HRuVxL s,yKҏ4,u hgDqiAIk[JKx@P8 !Uu%^y"8XGIv;6=$=Tyĉd,l>To\ee6'Ejwd(68&*vv^mn[ @^|vvkI֡m,ezF46朒6nXgOBBu/y]ƒ-۶n6/a%*E7/{]qzrg/S+6h޼YjY-V{CfeG7W*RsS%.Æ- MMͅ56$hQ Jhm7To0*5xl`410^&7#U[jW\S zF޸;pa Xbw:5KR8!K'w;#E&SɈ:E&SI,pD-2YD(Pc|paaTa/P+HL~W+.Cb8Vn LK<'b[.[V#'dm[V4Jd4IJA'mͲ74%$*dmH-P'ffjl2fm .[Sߵ޷aui(s'RU 6"[oQJWF?O+zѼfRxZ&Y)XouPe_/wy;v 7l3lmD"ydK4C3#n޺*{]b Z/{ں)u֦4JneW %bsG R bz}i΄1ik6*֬Si<2Op8=ukdP؜SnZWg)^lJKDl7ߴM:Gjŭc̽8AMZ5r -ek*}SQ~! ƢfD2uM1*ʊKK׭W @r ϖu++ K.,͐T;,eVg*49q궬WVUfTlXd*M+ybbu_a5F@^(ǰzK®Wde;>^ @xqk6bTBMH@DĨCq+,5 N&O ;te`)IF;+Xyv[dtT)y.M۵'PV^mZ-N@$OHf tSf؅ ll}hb Sl6U^@W\Xaq5[;TU)5;wLf@$SrɊY#&fI+,.H"W̌tȝI +w,76X S2r2&aN*Β:\>O7$*UΝъ`s>'GxY+FJkim'b49˙M/ZoPfv:VWRH7W֔WL 698ӛD].wp1!bflQEfcã*?~lڦ'BG"b}A "6l(6i@(MfmcCT|}xvo\ι|gΜps3 ߳x6OYc ^G˓3os/?CK`m?h;ea)muۺ-fCnV Ddp6\w+FZdk_+nYܗ];f gurpI?E͇>pbޘI;Ӣ>C1SWs#fϹ%'o8jܕ%9/_x_zy-7uQۣGПdҟN8?$I:x}f]H B;2^tǤ|?Ŷ1OBT-'C:_:@ :qվsD񩏬E#v#g.9wǯ7x@ k!@ :@ ¹QT~؁p~Jua$i @ )󓒒jEuU>(==8of)CLA8/*++BV녜8HFu@ )L&nخnL&❿vm6# |>fs\–L@Mۢv|>nw:III;?%%%H6ARL$.l$@ ŸEz~14m&rMwpջޢ'"#:@ ,Y dO ڧ)YVV cgNGVY':bdɱ>7W}mҜ1YpqBR.`B!@hrNce)pQ zv QWbN߿_Q N?.Tnam=q_:I #~%@t+K˸e 3zX\W̺5 ^p-vԹg mX--9K";)~>SV}tM#cU6.ݸP9sByiӄ١\RZ誷nڬߖ'/7 ՂWGS~:=2K7@E7m#ɽT];Lw~om{=3(vWbey1<@J ~Ǣ n[4iQ2goo 7dҶY$.75| !tӽHmVM֚[wgF}7v]t[C51_nw^\V%:Jlx$G/^0'ӿv%>{msl]!omnocl,ˤ^T]X;_ؙkɆyu2aބYIºm"q*@ %@jg t7{p%;%O!x<9 SQPT P:% Aw}@ PT_}`{P(rv7]WPCP ԋn)IsIa"(wptC<Lm:nhKH@ęAnpL)ǺmGd @ . 8F-;^H?v+5W'tfLOwE,!aG$7ONP2-tfpS݈[+"lC$mK:g|a!8} ]Quu6l[PJ=(9ߊa|J4@ .&kEjICzܷk]] \2O!x<-O<yFkkK,gW\Q/Sg٫NEkWmk\ٰxhv?Y:t6xd%2v].,ξ4u:xb0q]Vڊ n 2h=N,ztצ-76z C:*$Ih4zxxxxx@ЎKr^zFO Yn7 ڝNQ!veJ@@@@@@py 3-f˵Έ"u?, Ð~/////ڗR;3@PZhԊb"u@ JAAlرC2=\]LbEQ$$@hGE6@ A\.Whhh׮]/U`d\B83cnHfu!VKFoG @h8eu:No\2!D1%ҽi1ICWf)x^l&$R@ .:Ƕ\ʅւDo}4 `N!.6dYɨ5I*_'_ٰkY #rTTW@ZgFUc4uB!! bL"u[Xyb_c[_̂w'bxb'y2n }(b¿ ^_(3 6(]]N1c~cPu2#9+ƤNsۓP %xw!BE4K$Byy9:ӾUN} %N|exAeE)]Nؾ$߆Rlۋlj7Sk4M.=efRȠ( (;s:gZaPmlJۖ)zm+z{UX|ʄbsJei6C! R'/EQ-Keu/;;ÑTIC!13p8-|l5Fx:ɒ+ 5 q7_NM,ChtŧZ4=FahD|@_/BaĪmEIMM5o~vɫ0_kZ2Oav Gh"|w>j;u\__$K[Ra_"uiv9AC7¿ yP *\&|~io܃cRuFЄQ:8cVClt^}}J*xGgu*b¿QV˯H XˆS#1\jEIcbW'Ȳ̻+}Sİ9!N:^Q F⁶IPU1 "b˝=7z5R}['A%ϲ?T:?C)aO:]n^1)j}dUTZi{ء!>WPRaH O6M{ 0ј+ :𯂦yO)s<%)!+ Zl߹PTV )-}iO2&17/?Q."V4vb* <;OAp0YOF)عN A51EZwL`EQ W1 ZˈJNK@1Ŀ~4j1@^#^38R?Uu *^6.k|[ua׍&V{xmPh j=7Yʊj!@7olyNv+\ *ͱwGӧJ Z/Zy ZJ#AV㏖J;29 F8/s) M]MMu Y#0,r,KQhwU+()߶t }2a W](Fʲ7%Y^y{CG]ʦ{ԧ@pؕSQ**-7uxU:L<=a`gBChWXS"ue'(Eƪps ,}PX #wlךw^9LN|`71+GCm6[JJMt5EY\ ^YP=5r݈*g:Ųb}4y;uN9@VL$-i( r cǏ#wϪeb< ðlA0V~ɷk۸趇tτ{RX;L* VU._DEQ4}?_Bs{ot^OV<60;$5ٝ(ʑĜ_Yp_]ǣ>S@;J}"5d絓H?f?z z}KR$da׃[> +\ U>yGb7oH^ߘ|cʄ8ӂʒK?N'u&?%" J%ǼEJ!t!HQ ( wV{tl0ƅF3u[INZ5?zIiZV`zaW韙| 򉰉AQ :+`17~3'd__´n5q?8U),*KȨ~eNvДvm]Z.R^j_`;rmps;SQvhM:V]ͦ:썋|S,N;rp_(J }GFb&E.0Wv] WBgsʈHք[GA7^џxQ6߰qK?W.Tav<Hdٿ(brG: ו⨲ pmSm.ޚ}p}w;[PQ&QM%|AQ 0B4P0EAKB 蟺CܤV@Qz$ IDATo헲FuMm]ES!q`WB u򊭞5{|'#{'_K5<5k8kv7# ٫YPo0.Y:aѡ G$I:X /ң9s̻8u`'SH"L 5}S&4D;i6xQŝJ E)Bv!%t5Ij'Y -Ol;ݿ/"Jb@DjY.8v;,"@ϡ-Fg4p:q,ô( >I_kT%TX!}$m9Z>LQ=nԋhAOj&:_C {xݒKB3T_DkHCE21.d\N!pz %`הl}]{,1bxO<Dɲl t#'2(YDd 10lp掤6Nxr!a8MMɠצvorl\%^e^ö%44}\`pF5 0:"u!%dY58M nA6w9 J}BJ|hdfCxPZAakVF)hD T>LÄ8r"CtT`TT,5EQDTдbO@ycG74xTȢn(&lT :6as pKhm}McSD]Cˉm(T;B酩0DK[Ż3.8X?]%;_xo{wUx} }^Akx~ ͼ=mkF#\{V[R&M8%^_*[tO1j,33H+uf397s[cPkcQ͙œIGX]!(4MwC ?,#E1hi8[JrS0%7谜RV~}'dJC| P!AQhu7ikJ~ŨCDkNMf5fXZ-4 si9FCc{#Eե.KLI/`7_g 1ƸdpWUnbODZi JMܨ8+GcO '|72Z74xDC2}Q殖 nE@P8]߸b .j9OCrP@E#vnug^F%J'Y?fB҄I).>xc_zxi׍цÃsG/j!j S/W|8! |U`L$T(9dP}()+OxhCC:#A)%QT\PA@9`)JUMtrEC*ɁT6I15 ˲'P4M=i5iw?_ RMg&Kӻݯ%i vǞ9=xdzFϟjʈg@QÁ_ovnxr3fZV= ⟎Fg'v$$RA,\[RMI}LA$lM 8:(t.>)4i;Xh< կ?Q(j׵z\YTl߂X:ӑ2uFCZќ6aLwBjsŚqqKk\?fDCSSߞalƠoR*jXILTpQ^WFO?ATT YOc`I#c<wyN4E;n 䋓sJ~8}AWW6F bF.-+ |egu^:UWcGxYz`A5iOMgrs(XgrYzbi&ˡ_?Vvi;-u Ea$9Zd#(*lBSHCFIX# ^פUӠb 5'Q~d9Vatu8FhCb'udKfGw\lUuHh?Qew"-1Q{YsXbH sRU, 1&Jk.pP#5u!9mۙ{rZz!S9_$YRyg;kڧ>-2ߌo}s@,ק]C@WQѪyBE k}*Ra^x;g>1qc - ۲:&b1L[r1z+>hN"Kޓ蜳eE"Dqb ؔ)϶yn0v;?1!v$0c404l:Z& &Թda }2öν/ߒGp=sc'Vu^֜qFޙ:Q,~?o:gt3XSfw}ZnnahG?ӿ[lӲ')ʯV0V0`-zSxY[ހ! MG#5 : 44h(2 @)b*K6mV2GIJ4}K" txjz><9߫w[KޥSm=vUo`nxb?o..*Tl|%/JݣGNMY)* Q{3hf*Tlzwt9f?f؃09֜a %$%\7o6Z3=}:jQƃtSzƨ2.ht[nT}oM^wvM"+75 4NLeYANwl!vr<抑gQSSV^'1XBOٚ\hK`fǯFSfNd y1)5ʎ$KBu:]^Qi!k!(?YJ>%U_q ޼Fc#Ž*(JnXCP?ClP?Rk>ɝn"GQ?BY{mSX)pvbir+nno(cFV3]Agy~w{B};hsOl r@²i[6;_'β}eqCd}6n|/-;ڒ_+|íuJھoSQӒ}u FmQsnB(J0*` E!S~C\y<ܤ*+c N56mGHZkU(GHartJHdF)fiF$>2ˠl-yMΉ|UN-|SEOܙ~nh;`J$6~/MR˪jZsi~V]QQ!,)|C?2*Hz r̚)9ьW{졍߳tDM4{V ƾ(#Pzp٧`t<,sz%Y,S5:[ibgHLahFf}`pMt{JD\CΥsu@t`Z7Q,Y}ѳ'SSyn-\N85.snƥ0*gC $/FPEEE) F;aQ*QRdc(cݢVTv 2Gyڗ:喜pVn W Ӵvۖnc+\NӨ 9dP/j҉P_?jԨ+V蘚(JSs_4M4XVdYVc) #˲($546^:f3˲4P@eYqsiB= tgJ8˱ nągr! 4AFj"rC 4NtO͉$YhN?M1Y#飦]+Mʃgh>.s"/?iyREWݕ}(̄՗7upDQe;J ZGB10ח6ԧ30vBt{cԍk aLSA|M&DG嗊gwx2Vp'&U[CS@SQG;8J; .%'O3zAmKb%&4Թh=WGeJy˴ևsnT|}2mBXk0eYMUz҇-_ :nR&ؗ?q$Pן JapᲊEEE' b0BJe4kjjAС`RQc##t: d4EA)v={cccu:olh(*.ƊlX I3}~ q}7…PS(BPtoH8Ҍ0(T_5?GCiNyOSmaXp*st}^9"^g(:u3,J8mk~;ŒeYөw}W2rMh7 闓eSnqqV▌0׶)*cZ9ѫأl=#c Z mL&ӱ yF)TOo{d]K)!Aa Iƽ:p ͇ݢ ŰPm1+WFej֭AuRZs &O=34d+c/k3z9-GH~֭ܿ]0aV^W_.Ks>e˖O$~rƉ7:Nc0o޼,?mnݦOkkg?pm]]xxsѳ'^za>99effrG)Ȝ4_VdXY5:or֔S' ~Sa`I% !$A3 3f՗䇧%XBzc(>"u Fݷ >{oh=y jcBi9)rdMm_|_C˗/߳{cQm{Նмo5s3=qˇ|x#0455YtV:#*k\oV V9n=(WgΥs0@@R3NXu9iL]e|͞T_g]We>{EO5vl;s(!t4>1IgO0y& Κc0hZ|,Q'%Ef<4"uxuu JXQ :rp;qfY֯(`Da)} /4l~((Pde c#YVo4L JͲƐ^pݻwWՈGE\aذthF&k2XCQ<>csRNm=}ؤ?uȌ7/.]1ym07-?BAf״sXnhjEXjB g z{9,޳s 5騚_&-_氻 vKP@&,U6[#s{GgVCKK$!U}n͚7_]Vw(/)%I=G[4BO>?\Φ&1(@SSU|ZJJb||T#GE|,O%+? -3[A>$> IopnիdF銊&x_y?#.bOGNXʆP=ɚ˧@Pݯ7n9#+ktaaa4FDݰssˎ8|dTpi꜑=|yl _ \>wIIIШݻ].͆$I|!EQ&M}U;wڵ{wBBB|\RtgD1//oɒ%>HtttɤjEA :u޸awtd¢+w+,!8zە{%ˣ"^ys_\|2=#`wᗗ|tޘ*.WFY:uL"^ M'jjB3R;RL_(Iť iHMnY c\[߰ƦiDIw%-d4ĸ顦GVTU*.阘}i **v`6PmG"xՆ;G@N3؊jE 39oL7fEEEOiR'<|?<5Ο!RpiqI9 6EQ/,xAVj ^vY]nL8[VhLHHjV?d R%'""2byiz7yZ/RyYX,111,K=J5Fڕ{`Q0N *Oŀ vflռ<}a.iZMk @ #Gg?BN 5Lx|&"V:$Ŀ1oq5~9 dY|g[}m30 -˲NuǴˇ4'b}م{=κuDHHHCC-,Ky>s5<*P$34HY`:XWҿͺ`ol4L~!R@tNt?M!dBQ4E!JòIRdYv8e^߭{w0 EQc! BZl`)i:4<0˧(Fj9,.չ:.)[Oμ3׍//WV W_9,9ĸnM2V(f3O 멗_/UFEQf?wci7>Xև+m{-]ِ}M^R׋| և+\WYZQ DGF̹z^xW.ҩctdo.9iS2CC;sS.+!ǏЕIͯ%lP&ʢ\>\cGP{~0]5m104,@Qi)^C :jb @_j M{o0k.z * (EHcfμg1fbZzK{`G 9l^: /Q0^uރafU>~[~j2 |>|kisTq'o4a@$akFkhz믺6Z#{еϿ]74/oB^(EQXP}}O&r̈q,*FY`踧\>06E.צ0}FMV00BB_q~ɽ㞩Z)aC75a&jH¿e~*gϞ$q<ϑ0FoDq޼[c}O^K]1),S^Y쓯Z~vJI Rٻod822c2`8x1X4Sh n^ZmE[V _J*^/a( WR H0 90Kh:s\>s9g;Zzjr 6=o[qK[FT34"~cҸǎUtWŋ|4}BUq̌x%o'޽>{:`醋B#33~}s?¢3Bk8hogDdaOzmgjlUUU|@^d |L8n&EMM{ {Y {*YPm,~vZv{1K:zȭ\)؄۱ߡfw=:Z[pDVj=YJ3yOBfoފ41q3u~Cac-`_/h ~]ʏ31Zh3 ow8ӏ2 r|Lsٹ~H;Ey{u*,S֙웷/8wtS 9N}J@K_=1~K "~SgxO0=#cYuHLk?.Teϗ\EEEϞm[jcs,7tRSSSr8cYxb|Ӿ?4s޽{ð"N隚_~YF;_îףjI`jBDчu}؈Ϫn7j.Nʼg3l rvw/e|g)[\+.ߟӔ~lQ ]**~%4k~%M5ѣ~ѸޭY"d9S>CD߾#hMm!i"~sų|Fz;wH$v7X[Kk߱0qP(ׅ9{uY1{۹qK޷xN~i|Z"SoR>{үiHWYù <}Z #|^,uX*Җ+~Zz2 SZ^Z;33k-sZ9DK(Zm9DTsw/ ^&&yuBN9'3lU/'sS#( @'allܯ_ʚ{RBmm-˲DDd"5m^cDoFԁg.DoF]  vP Oǫ&0ǚHF-\3n&Vك;:c 7uBDDDDDN=s4ݟ&9xZtƩui,͉ /bϭ=sߜt=yFm Ouz?ObפyO O.mR)kN 6/pI9ҥ{z۫G}ҥg~腏^٤K L,}K/!&6~ &Z7':Rusj'L[>6N NִV9сKRt.sl]IMiXˉ8oZpIi[U ~s=h9[Z==~?8֮PlކisK陇]fbl{M1[?22QE I>;j$)i26Dfj#D&,U8YК(_.'QeHω&FQ_X9iW_~_z]R*s9"=#>FZ΢ L ew;4f!IW*D^X!07Rk/R<Հ[5;}h:1R_E qbG*q"`yh tBޝ_npLsO!w(2#wr 9TDBʥEa-̦`3&4̥9kI tikes Ñ?,׎$-(!jM({2.SϐEqi g&5+=5"̸ p?])cI"ًOՆOk92 FU(/r5m4i㇯oӵJZ23.]e QPY8Nĥ۳ ]m9E #m7|IˈMWiU˷DDb9l/VV뗸2E#ǫLoXHeaH(B2]|n8 ڼUHDlڙJ!"}Fܒ\Z+ [Uɸh:^%AWS]cb)2\4" 'IRPW_)3SD7'|l^w_JpZ-T%pi\{D Z*-Ks11mL̞D^Nyf.߯D;Qllޖ;\-|*_(=Y+uNL7Ɋ|OiKdƍYrz$/pQzr.&%&Nm4)K*S\6( vm&Y?up'A m+T8g~9"Sl&C"{{f(_U]{nO .2Ym/O/ , 3yׂ} G/ˌx0DtZ]p)igeDKCib}#yO!"Cũ$"N{|\qZV&Ħ+WMv11coC.efe%as+crl>ILs+*lȐ>_gkoc; egsq.wp\5IfҪ_.-K&. M__%K\lŝS͏ٹm_&.R(k=ĥKr _u?cP/ rr  ui g3;9k?6;B6'zpO+yKc%Q{ܩȩoY"eό=kowі-j- l1KLT6>hb"}m4)K"(BrvYe rW!  tj u"` #QK$%*=%;j#u-""kiI`?F'"HR#Ե48kύ=KqLgKHO}44׆boLCC9ٙii3cd!ۢil9;ӓ@%"=d|"(q> Lֺs;UuaDj-)nT2{!% {&9Wr֮$ҥڿ|JfH}GO1 IDATL}r`Rh] I$xep<16_/8jef+Swv +M\t˚]8mQ̖ =$٩TE\~jmۇ 2rK$9NeoY0;D>Ww*}%|"$wIG@ l\G$t I;?R aH(P|B"N^KĞ#Ҧ,E$%姵!r&/;3;#mǢ85kW{!1"@׫Tr H cr&TZNuF7V$ĝ۶r6E=xf$ {1Hphcͯzwf҅yc&Mm:NW(@HlD;-뵹yCZKGƪ7o$%9s3Nlf1-ZѮ#l;U9ӕ{uBHpqa2r-Ξku:P(uu >3*Md\-@>I_d_&S& oiUDED t*aUi$WqՑ0z^B3@5s+F뢍9)1EDDKSFfnoץ-x5C+fmOQ e{^K}e1q`qWbMLk&N9^KD+b&]sMBא1%mLZ='`EZ 4;S/-cNy2KOӱBu$!a z ܿ ٵUHE[vf8# (4G&o^;IvV/Jk]h=yɑ3}7oi~W({4 l\Hv޽{~ͶLz|.GĩKOS 2W1-:dT.gCpxk&+:"bKnig|JJj4lsc;%G.Rui˧Ws"H!ӦmY\tOBD?]CDI۰!M͎MFpCNQלd|-_0Dа(_u2Y;?].ui jL'*MߟoӖxiJ2S>;7|F&t%.㲳DD4U&kyjb$A!Q.ّ:ʹ|ؼĵ8xL?H"|-r xA5ʸ.8GO r [e &/ ސ1 L,bz(2'jsa8kہH$|=rSDzDHٟ.zu:,]!GG|F,isĩSruDP\eGܽw8puD"y$v Âd W(jDOSq`Xv|/F$ʉ]V%d_,-Z>7X"L)cbׇ/_:-!SzH8q(anrI{O)jB;.3ro6x`mDmb˧H 9CO~bzDD3 C'O#SXhFdI>;BqHˎODREPŗ]>W9/8YoCD>a)ωc8NSPr@"NeCu63/߯ҢLr|BjNHTΝ#V(Z~܎_>=!8F>{}$q}xnb]#YPDvϩ CPʔm!aʹs8o#o>&pD|ElӒ%2Bzٛ=bP؜mKBsvGDGP9708"(Q1$67rb6"lfG*M#8(fG#V8-Nh!ϝ(rqė. LI}&+qWߨss+EE.Z"$S2"IraZeBB-w. ֬_(vxFp$ Svjx,>S.]"HDFuklۿ먺mWH6_! "5IrphB q˲nnn]nC_޻w=H)aR^g]MMޯ_@-:7Dyiz1ŤIY27wiXmx:3w y6}OPOS11!|16T.PXP9:>Vfŵ{])&F0QDw?w|OmvR}V'&ftӉgi;"`G_tCd6Q50~uuuuuh>y$ ' zuQ+xl]zuQQQK2666 (`0P Z@-jk@'*++kjjPOCMM ˲Z@-P ZZx*?I(t:|{4 ‡LQ jZ@-tZpF,ˢC q˲nnn(0 uɓ'Q ݢΐ(f0 uuuuuQQ32B!t6o>YW!D0 uO3< Ы1>=տ$KR[ňNvHPeߔu?qzuD#<=G8 K'YM5 :1*˧vđ!3 Ru:_|$g+?CMU_'7ַ͐/䥒;)Uh׬#Rq臌,qb^+wb.{U ̉HŷY?7ddab^/Eiw**@Նږw]c[q+VmM{wi~ufWzcէ[~+|>ܥ;48pçNcD3623ۭ]ቇܟem5>3)c D%UDT]s$%ị%|io{3o~}όog'&̿gH%D<A?w*4':L0ɶNܨ&26&Y{Mpꇼ\tƓ Z*Cm^^^III-ԨzVw71YVK_fze¦!&}ܔO[c~2nGVES~ӗ_4vT??ϛV<sp]ZјyQeM~ 7x܎˭_{oCT]gC/lvNWߝ5̜ngwIVv74):7Ss7{Ukʶz3pυ~4> l:ի=zKy& I6Jɫ1 4H(>m{㕷oD^|^e KDwN~dÛ<{ee.T_*`B#~ٷau㆗ޮvlOu]<^|+ODdln^\L<"-g>a7oOm. _}Mo :t?䛏szu @RyݖV8ceB""=;FwdȦU-??rXOٟi5]:mk*<_5zoWz&OV@g{m{K#/=sŻ9Zܦ]U bߍj V8r.;#d]I~l_T:wl߷1(iw0}?Һ/f :ګLк};Pr2oQW%LY߬u>{w^'~/xX>Ռz=pƞ]f/vKesNwG4,gܸ~{ޝ?'Ff^9\@xP]\߼uVvw>?p|'*rΏx| >݆ȧ/[|r;~%U\ާW?O~~PyCY*=\z CD]m`'}*'{Yt\"}ƎZ_GΩ[|5,KrAl\,_O$zaƪeSoW}MF,gzъ2l?WƑ#pه[=e-G$]gQú slu1V ;JgJl>m%w5q;8D2|ܬE1Gq[] F-X:8ձTF[R77gSa6V \qp3 g3xЉpES=-wU/v8iY?] ;\?uםc6U7<'mlҧG>`W[틾]we']v_SZ?\ψV—hPI:Z1Dy{7sZqpꑏ} .?[P|8jCߺbgGOz\ڎ]㈈l;zjߦ{#uCDO>n]CתOHܱ b+&[LʹvճE'V^MDDcw"U n_팘ʤҺa*޴{=bP&NêU[Dxi2,bu;#M?p޳5tַu^*2Tzbia_*$Oq 1C+.V_l~A" DT&.V(5iowkhu{1 _Syn "F 2gH(  U9e$9T&9^ɦML.Af/|}9UDToiN2U*l$g UpHo3gtgA: Ӟ=uآHC2I*""8zJwOW0TR#r"usԘ`+6S xDE1؜^Z?-#0gת cbAs8Tg 88ѡ-F5|Nd+MsZlۖTq| GeCbQ9ieI4M8xi]n,um01ypVpD>-l]y!ZJE\}RZ!#inbCz]c35i3E~KSҒBe5}]kՆËq=Q٪WZwd}!&aϱc[۬Bftj+ioŨb}Cg#d41@'!"ڻ/fN}֭G^뼊sNӹm=ݤܨ"@Ek،QVb*:O$h;w=vM]> j١$p#ڷYuX5k@×vCF2– x|=y^o7EJDK,>RGMt֢rMa2F*blDOOOtښ`08cYק"C:axb'ݣm\8ni/>!=M{<q^s-۽sEc}4>?p)5o*7څNaEY;WҊW4yjslpD;F}rs$}v\¼_:DT絒q'yȲZPU6U9Se%,S> A3 IDATtpE}s޴{CiOv%K_:N/`gn;6;Ou& ^*a?l_|^(. >CGAq@ecWalN:Ui7~}o\A%/̈3k\.7}$ڰu?DcLod;WFd)]36;8/CUDH7wd&RyݖeIJ,J:D:nnntWFu~YYY^NIlmߤ_zϘRb{ys=Gm?kpuh6{=7tr9CV.3HRD&" ##X+Dw g[4j5xl߶ُtis!b8|pyvC]]HRPެBa;F^ƽE}[!"#eQ!jkkkjj `8eb77vJ_M4fW!Dp<@Co)t'DtoDDDDD@@@xx(P8KKKDxVvk `DDDDD@@@@@D 3"xR8uVYYYee%J155ӧ0:O=XXXښ@[[D'֭[(̬.L޺u~{u2KKKC,++߻:OFee%ƭ'33 t(':tN(#|rw'b/DOpvPY^<9 |aY;gLYc&b 3nAVң:y M":X4d'!1B}}bElgkeHw艫ٸ&fA5 $%'vg^ zh Y:''Na ""N1bL,dYP(,p @|86LTY$PgӸLJ2a|"buĒ ^ܰ[ Q r2ѭ'W#DUx4IڎVέ#AtNR1Öwp:Z.게FW?h WCfE3&)I,ԗg VW1ⶤƏ4:b|/#[ _8@,~$ƩɦWbQc@Sx0I3I/OפvY IY:"x0aaR-"*IM7USB֮us^N>Ij*!UOb1iN>=A*Ozubb|L*lqҹ3thgggg SK=:yJYGϊ9U=߱ _J +vï8M1k֬Y^@DSޚ5kk¤W{v+1GjDOpv3&hHggѳSouӂbKrDN!aL<%$fڈY% S^Yx#w_seKDϪxЄ :bBG|RDt5r#M2[ے^SBw^xfkYDDi]i52#]ePZ)xHԘWʚWi\FE jH/v"Mp7*[Fk]FJ!CieJ<0J3/3R`CHBlnS~^8g3s8k>U[LAO Hwɑj|OsCyѥ0>- >]7jr4 ʅY\؟ 叱Qgf;u Ņ^ZBv< &riv6퍾gf uu'gyP4'7z4kkk뼁䍞lomht9'D7(wpZM[PL4X+ /0џ,k֪ }gWSuhǓ]-v"KkלpgdYꞚم @!o)hW,.΍Ƌ:#dE^wKO@M'5.SXj+N; Wdd&/^!VxF\ BSZZ,ػ\j(,O:0b`otJ:& 76335ඖw邷mh¼33#S33#>937iRTwOŭOyj"Q4 {j c=\{ܦn<-q->إG~yT0^|;0풴c1|hټ;(Vau4 xgk`{dYsff|Xh`x-ux:0H.OD03\F4Ф%s[Ot?}wbޛN,'ti/$a-M[{dc9y/޻ƺr'xVY3]si=viigxfU1QvIHf7\l㿎5;k<>Z_g[ks *[;n_[f"6wM5Cv] "P$THf|zu[Xż#J p6'׀"RY]WTܣיd;%VT:dU&s\5[*إݶkC2-@guGLoKNYrdGul| 6Թ}Kckfbvuegf$OOk ـLtF |nãZ+'^n>wSGPs{ۚl,oԸڪa$s/COjhہB"S-e 魫:=l=m߉N@e[!Hܝ?FIG3EeKA6\jiw̓SFe/gh]nuTX|cdc&^C; ;;]9{`hwH"h*$+@@ظ-&,z^]9®#-/3B<ɿ9$({Kٲ,Ԇ ̵|6,G{KYj<2K <չݡh8 UOv:wmPtFF80ѻ@<>zǽwШ4M)P hfXM4ZR:bSUkd" 0TȀ(*E`h:Bj^Ş[rb5DzUJBMVٶDՋ&X*pYחh󉢯=rU ?cCVlr1PtU1Qv ')4ϻZJpΆ0*gFZA-eC{ŅXrYAiWZK-p4$Eޑ ^Pglh#ܛ5l:ɞVTs6{=Rئ)*XkDwp;hˤc7m_g^w`0L|zhna0]ܶ[JEU*Kz֚ܺtp-4(e/y#Wejg;r_xd'~+Mi&*DydM Uz1oayj2L-Ϝ#$|cxxb9˷vdRN<~rx|ɊMr߀^mxL)6q e;ryX➪Ojn!XRUs|2LwbCb~9ko NY@q9m q'2VLOTZN@1HYVt,<:zY R! I]p}$Ѭm0ku3)@Vd %WWR* -ؼJ{m놾eO%Ñw?qwbs 7%Shg'b_v : j"v+HONS`phb"eMt@&<8L%g9s2T Fkm۵PSz7dzXp7$hCCs[/MNu Sel*cqޏ;qwu+B3 L:;1xǦ:\BbTW8-8[;=O8PX NKem[G#i~84s@ %uqkY޾_}+o r;찯[kl۪g@}ȕUlo{]Կ?ͻcϿ]-_k*E3w׭|C\Suл:W 6prl"/isoeY""Li{K9|%T2UK$_w_d2;BC{d%~Y`=XU>[;mxO>:_`ܾk?' ѳ^af/v_T?'WǕj㹏>E]\ o%G'&_yùO*+P{Wޱ[D"z^/N"^;)^e]y/qdi"G>ptFv*7gq 7?_+UD՛ l/c {'nC[7JO(^j?hʃq @A{`@ux@9yZ)/QQ.c[ ƭ7V z}UQh=a^鿮Z;,mH忉MtK[7=,zl ,ܸv_?bpZir&륻WK 짟>у+<ҵ6fk >@D/%}3}+7ΟsGE;0bas*?{= 8S-E~Yuw T832 Gztls `?r3h?Rʵo͉NYw@ k;)"z 4o>|в,4+`Yv53{׭1"][8.`#""""Qv ^CDD%V]LY"""""b!"""""b!"""""b!"""""b!"""""b!"""""F""""""F""""""F"""""%Y"""""b!"""""b!"""""b!"""""b!"""""b!"""""F""""""F""""""F""""""F""""""F"""""ba1t?JA@oIENDB`././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/doc/source/user/figures/add_to_env/quick_deploy.png0000664000175000017500000034062400000000000025122 0ustar00zuulzuul00000000000000PNG  IHDR<= BsBITOtEXtSoftwareShutterc IDATx{@7.Z\ 01OXB !'sNĎגc"ir*0AM$P  Ȯcyy=e?wwg罟|G{HѠzb4  &El46z37k&odjxF Hp WZWjof27_5s/d6wɆB `~~x4ʼnn3tTѪkjj2-à eeje""娕k" u#9`俎DfH,AD"H1HT$Hsa4AFA AϜ9gϞ{޾/_.-VVVj'|_ѣGU*?]v-++y// .|'+V4hOd9UOD՗kx9|sb?-^KJH͊ s`UnwnkGw}+NYT$.+-ȍoJIn>JobTNޡOJˋ|.'=r 򏎋Rr+)>W_y|RX^:9ʉ4ϯq TDD9%j-").!^}dMzbXqqT>?QdsZsΏ8(G̀)nӵu4pzX^}dM&_4HE7I_XèFo{DejM^1*h(<OD$DD`oVnYn+]戸sGjTos:_FED bjM띢,F{p%jX 5k HYkܲ.1YŒYr\1?=ʩ&\0AQ,9y}zGv""b]]YM< *)ለ'@^߼4}7\NQZ.őM/u^`ڴi L!~kXҳCsCý-ߕoT_=zJ>ͣ)muTlcZnrBBDRn 3,4U9 __BAӭ_o5GqDNQqK/'-u̎Z9[ikG{d$^ Z=/HHd6 "|&L?ڌFHd2A]-3 6,22\ _ mW,Q~L6C2oI7-\F<SC՟0W*fIwǪk,+%"5S'Yu,G_OCC4/^70N+Jz A`@sJxu"";zj}P Zׂ}ߛ-z̹ gMZc]:]xK"7}AV2J'J/kyz0{v)Yw%մ|z˱/+!Lw?<.L_c ]5VH9>:1='Ɖ\GOc&C[Fw90ҮMOHU*;N$ݑ։5^Xr {B }8E$dQǯ3 gL?Cj"-_ۙm[>sӋ#Vx|r}ϟLH5:9ɻCMu8N5`"F O<=(:WF׵@ˁp]YׯR˲n,9hz@}͵878:2~@7韼y0fqӺÕkwH=g}*0P)/btׁ4952W!&-G[/]IJxw4<Ț~8~K;Mݵ'!3!/5I=lw/3379!3qׯIJ*DSsR%"'k=^uL$R"9'eO%"F;tk ?H$V">NKJ"RՖ^kk ߨQGݯ-KY{0nO f%[RsD$~zu>Z*&Q[񷦟?Xo^-"!ks/kWX?Q>W(:WDDŜ$,n""s?ʃ+o`g9{0DDyyy"JJxPW)0ܵ)'OD:?Ɛ }MMh\ܻ y/ȰGD[m3q"ߟq *$CSQb]ܴ%Gj8"*|Ǔ {kE(e5GD|ӹ#+W/Ypͼge hn^|@8r$BSĒȜԻrqDD\Ĕ<OHzyNxua^2YD%hMw7Kōy$7fMzvqHL}n%u$5@YN D"2_YS٘FmW4W(оWOKOOOHT[[jU*Ց#GhԨQ]CW_}+hAQPPPIIƍ̙CD&a>`6.JǫkERX&#"HLA>E2Ktu]K߽_?ҡ'l8H~kkn9]+’̎\t  K{+=}5<Jo P\f$QOO bIM(5ixUDԸĩ,O4u79>4-ٛ!{av^$`uiߗN[qNRC3Xr'<ǽ"KJbNbUGy~=脬97Y7ѩYӃn߿xI2D7Uer\bky`PגsIqM))&qDzDHp~CXW'$)77i{Z[qkOZcy|咬jaG"oH| &l:ݟN^vu&x ؘT%S/d2ld2y vP*gXp<[`׸)/^ܾ};T*F[oIҫW~ B 4ڵk"hɒ%D/lڴU& 4eĉ?|cc bbb-##h4&%%q4iR!+-_6KH$j;ueJ"'ƅH:)C'] wS}ql=&MN7]II|l΂f7sܿ&Ep Oܹzap auMy罱=+56ߊ5絽~ޕ*=–$Ņn~sG?|ڵ_k?YTLb"D)]i6 `D@G' W D ޵xiذaKsu)&&رc****2}Ο?OD>>>DIJlHHȓO>iO$,,rmဗL͗'TL`G>2>)I<`tP獟^ݘEFjEY߮F?ێ|xKRǠ7߲}.,kf`hlDݡ;w/oG`T_9|yD͂'baZo!w3Gv==L?wZt?|UGm+l"`6CxNl2' @V,ellRL Z[U ]R$M21{;W DSQJz9`UO?wNS(.׮,[|bYbz[Lf2d@:'BG=F躳4d?*hihσ.F䝟3Yivjk#^OF# f "@$ tvp;5? 6-)RLBSf A =-Xr`pڿJpߦ5t"f$D6 I6.oҧR#Ʀ&I uq1HD b!;1~\t ǿwߘ o6Afl6Lի/72 ~g2L&x;(d 7666]" ܇oLD!Sf h0~/g)bT~S~{1`p   .:Y"o 2Ll6)H,3 #Qz4.A0&d2<냃* ЛmΟ3 hx=` pʈ??@?@?@?@蠿zҋ/F&}Ϭ{=eze1]-xw]W3lL W//⋑V=xm}}i׋_)ݢEEwvw n}Ng:}J}]!JE[n|}+9oZ~DO=e]dAaǷcW(aA׭7]@}`CYoM>紴3ʡOFs:?;qզ.l|b ~:aQׂ>#gvXv'xz*ӔME!툈Zϟ=b򰋧M?l^J┧uS;YuZ0#ďw\Q% -ڟﲒ3rj8R89IKr'b29jIE nnE&qs2}ጴ Ejb30VO̜bS3JN=4fQbxQ[]mtpI{5c/%h9Fu!]T'ǧ,~;boGK]:~ug'gd0D̰W8/INpcUncf8yz_Z>e坾r-D\2?OӈOUn IDAT򟖏m(ڻ%H|eZ?/ =:ٞRj;qeˆwWZ#Sf:TN沽;lތQ{9w3\%S̜9.S47dC^/75ekj-/ů/#_-8GA>3y/W_}ug[._O8-Z;eci 4oӡCT|norűk8OU}4Cmʖݵ5]R"DjG/( -Gt#cUbbC՟oZR{FFDKlÙFӓ1/USCCY 꼿9ωpfiq !%"Mg.TP䦭+%^ 4>c5-8N$Q 8Qjh>{7HJm5WPn(}"gN q}kWv"[i>h_Ny/[`h3H[MFƏTшyc=&.RCS}}dn#G6:s>i)}R(d/t2G8e~}SEj)(H>vʰ̯x"f1DD5iT>kGgTuxVߩ칤2k3PmQw){꠿xLlbĔgucNcȈy#7j멹j$۾}kӓyvy79˪۝D$u=ڡ%tO-gihK9UK %-W]])7:CG,C ſ.r D G{y5'6fV Vs2;˨eY,>S foݸe,Y/3C5vhs4l**̙_O2i""24ؖue;7dn,evE:L$cY:DD m,M|xƙH  {N7DkԼM6S0*bUZlٓ<;>qQ^?e-n0*Ѩ5zr+|Y OD|F[,xgXgc,WV;G*ښ|}}vɲWDº\D8*lgKZg/:%۫Rty_ӣ'Myյk7MM\BDd4RF`ki5 5>tMN~34d§EPKΜTUyL=Ͷs%jNaAt;}X%#)L MuM.:*\DZ#_!}fem3}۲ lugv\׸gSB9=H5rIJ,i8I?Z+bκ,8֝ώK;=v> 쩳{d;K;hF~E㗓Xۧ8wKlQn^q^-W̦y6MqL+zRVŨT%vD[Fb9#NN]uzWoOHj^ب3Iڵ$\>߭/)[:+ ^n9vTg(N]~2k+yjҪ}p=\gNOKKKpKFۡg:AADdеHaIJ9Q5Cpz\\\_Dfme9<|Gz0K xZeF3wP֢Scn Bc;.\Ч):pVGD4n!,o^D;}4?,ĭwL1a7٧ZK2϶rQ= 7>DH<-8ִ=sgv>u z"9{Z{CcZlө4Y$$5qrSɈud:FNO5?Ne5S ?\t肞H_S\Ԧ!`E-TK+umHxaM"QHjNiZ 4HjTʇ@~NJDGMY]9!Ƒ*bFf4e)݉ꈘach˒Ygx՘ؔvD.ڍOI1FLNIhd\ڴLӰθ"mS#ل7N\ca oDDe|dtW 7)9-<4bQPKZN,^}C斸ii>fMSS\6oX\TNSNdӦGiK^;h}b_:cCSi "361%qݼ: ;e鳽ܝ-\'a6o"u4/j޿~m$8 /V6TADxVY$g{J5/j-_H^<Dz{rgyt~ڗ6XfZZ24lZ47\n7n;VGO Qj.hP-S//I9]Ԡx3em&NrBl2L&z>88/*[i[^GgʺYv_ɅS\ݿhS܀zLn Yx {Ӷ34t̜ő=٧ 8Kr&gӺM9$a*4wqB}xۦ"[ݥ$⯠]jpIIZH"n~)}fazzߏ<^zL)rs<pUd&1j}$J iKOM2nΜ9SCTR]/ah.=%eXtHզ 1R'vm-o1J|BS Rm HyM(:T.QG/\/ RV꟡DB^F'?ɭ9-s;֔j;TBd*TLκ쪡S_p 활ܚnĊO+¦M}-ɩiO2ӷx_lLG3:Ww[34!Ѝڌ.#\(:CJjJ[C|z)J?,uYNBdG4E1bbǶ9 mI1#idvCsoOp֬xt[Vm;GO|'n8:; Ŀ;JcD ixFޮk#"BJDRkSD$VJdKϢpQmm*Sikֺ.ޙ;n'5U6U[N xwm5δ20zCjڞUȉfI QIC'%c"Ec|be;^X]YVqe-cn1 :UI9",2E[P[uwjh9{#LQӖ+8"2ls۱3\x@˗o&"\y|B(TQs7C2_y+PItX5r 9Kitx[vnD 2QV{ZW&8߇ fd2L&z}}}}ppݮ/gH_1qV"b^Ppc ڭ11k 2Gʶn=Z>]m坨mx""bB|s ,#"7scd6dDDvٹz>N['L^HD)"eJ}}[UVY:1v&ļ5&#\8c_ lo^/+tR (u 6~;(N%ڼODDok! 'xjDlf( lǺn]UW#FHDŕeC:WZVJDvvT+geu pnnS ZJ1ob'xVCPx$Dʼa1[_LCB={WZ+, ~^|uP h6ZG]]1. P_,v>̯%""Ϩ-q uAޢEIn7V%1Cn2ҳF9C?wlͫꎮ[~-UcN*됛!C$s#j$"]NO$RYG|ua~#eXPO)pFޮtխz?xd I[dIDW[eʷZs1H2Rk-h&2C/A4Z.ӺY~˶M8D޶&5aQgǻu]Ý?7y[#Q㾏}σkssh-+3\;u pnkO5,Btn֮7>dªՖ?z# }P 7%Z=vdC#_YؠuGSh^g-1 ō:Na/xvwP6$܋˨ZUZ}T 07`URUv4?`^~e+M[6doR" .?04;\Dfe@f3[;qvvKGDW;u _F;QEaDF{YH(ֵ}lF1qoCoku.dkIX|uaM.k"3J^HU歫̛uC^|E.A0&d2<gbX!^~/GMXȄ};QQu3 8crvWёx~NJD"sS jG9UFtGD 'Jtm͕[7fR|NTBD:yItEZ⅘Hd?8I# UnU* T DD3Xn?^"oCC^PM-=xI$^HD4t â6}FW!!l6L&y^___-HtRy~a<gЕdhI5q'p[{t:Ob4lTfiiigw'=XZZZ'̥b] п[#zT|򃣭Gl1]uE<[s?˯;BgmY-+Jma#oJ?xH]mGDe^?'jjmDoΝnGT`ս_}_Zjy3g/CD ]UfΈ:azR/uYuИE*zh7,iQLeE"f,8XuݑցQtek]ר>XWm9"сU+{\w?24AL@jXPJP/+ԣ,E)@Yd|"=_g=˪Ee V\E%A  Bd&KZ}?}۲˙Mc5 %_q4iy\J5(|}A]s(3n3MXZ3/<<|\?=v[ d$5vy2]OGli hҥ+ɨݦ1>9XL;}@.Zhhҕ=;ϲʣwsOzli{EbV-Ztʅ.0g 5/x"B=z]dc8Q|mYj_h&nO'I_?a2#(n߷u vp}Lf͟,;_tӟ7~ZπI^vl'G (`*?\E1+4?MͤϚMԃ6r Og_푻mWݙJQ1#\ >yLWv-i rݏSJf$y6Iޕ.;_wj0>+cC!zFz.۔t h$xts%2@2͋=]v]@KJM,Ǣ߮.Itu4]@rBD=_wnQǷegx1;N |}8}.T DM_@ ;yvў{[6ӡf,[Tsp` s+"Bg[Ћe"ciuF:'e$.TEw2]MfP`xF >;L?Mgjyw9 wzuSeϾ)9@? y+oN+}2+wY027a߱c3'I3DO͡_5|'6,xWbO W!B0͜{d}޿k"3\Fe/+;]@:cx cɨ)ӎ)L~G7?j[bq:}';q}Mx.;vH{Lǚ5C5mf= f{[6B!г ЋuQݱ{7yѾ[Pr9O0aIJ?>pq[L'=ZujX3l[1߷ ϗ:>zcMp_vίGTyw` E1B!L/DXy @'`me{Fޟevhiu*CJ@{>D?&V5w=w$kV~mY2 TB!pS͟d5dzth>Zy\=_uS=y>YskvǼomX/0~}Ϥx3gj zdܗ-sctojn')eeGkچtڔ翲tL!Bz2$%@Mfuy$@}v&30_>vThgyq4g@ f:@^0۔EwݜqcBBGj$IFQܗ{|V'wjF>V׬e=2w5;RW.ggAh L!B>^d6޷|2_ݴ-qUf.k<0m:?es΋IL_֟v%;.kY5㭿I ˦ ][Mp_%q#t43cc*H59Nn1',\vhnx7L\1]&Yu)EkWirwJvϚe5ٲ-ً+LXHxLB!TazQm^*0Ly}+OhkNU6KSrr>] 55Ziʮ[%J͈-"wdYeV3uƁ}8jFbJ1Yǐn:_ >+;ϻ,[`*$ S+H`mE2/XGi%t!B!39B!0!^TtӾJ+?B!~8 B2|yŶz5zG8'B!?Ћ],O(%%f6?B /8 B!BO >B!B/w./1W e9! ] V'>|h,'*)"Wy [E!B'tߝבkzm eӣFɳu..rMgB!B ?\9P]]ׂpW27_17P]>+W:3|El'}]_O3uTUO>a^2Xn=sIOPf٢slD>G[Wˎ]OjxzW7m@)~@)+gNm=EB!BEYt <5 \a<h/ό\<#/Z&xiJ[_trNW-#t]4SهadUWMswz a&<]jhJ<*hy/O$A4B]_Geʼn"FWF]}2yw:]<OEtպ> , M~֔&Sa<2㕲|/'!B8QBQ=FcRNyrxH@,].Mx+=~˸VWPN" D".t4 y<ᑾ'o8 zGƼ牽?>5.~ B!B/J3iεnhLS翁; zbRΤ*^"zS?֑}<S߀Q&<~bu\={|É2{6}n/}x&@B!^gjbA~=L: k(s r(D(`rXIlkxk\:jrjT ˮw˨Q\調xU9etOD_ Ux"}͙n eĿs)2lJJ$( C|nWY8%[6߁,3 c6m6~=$Ittt$f3 Xr’gLVMu}i鳚YJpSWY)wxʻgëgs @y?"M aoMS.KQ/}~ae,KN\ O_xXljNˬw"`{cG;gT{{{IDQ~T*uppxf;00k/Xr’C_x2kwqBv(&)Ɓ>p8A>(B?],˲,kXeA"<5 $I%!,a}l΅^"z!'=^d588rvK\"%I\Sl>|>b=c#xڐ"ݑY׾Ƿ ?\s%ƅ(>hmՁ]G; {F'$Fɰ0'_j/)6/YWfG> j7'gj20 r!t8UHʕYXز|)Y*{ Oeg0I)Qr~TxuIx#bY/`}2:-+ĞMafN82%1Dj/b=]ʘD?^#84%m;5兹RWl SWeVKcBTRkOlSoۻOzr ;7J*#bb7TK/)CR[FfdD BwG 5$qǸ/A#MZM.0Jߐ tu)Xro0*ށ!A]F~#$/0(W+o%JvhbnԖW3- AJf]vk{9艅}дͣ!B¢b#o(J;%|w7 }=9f B_C6襡Ѿ~k)jɚȌ()3@'9ٻJ:@ʽBR"FZ[+;Zc+cbGHF_=3@T'D{jgvnIEĪи_=|-n3boplbbȬؕSԨg@ U1 NHZ}*';B㒒kbh()hd@ \>Dt%JΛdp-ٿ>{F%hZ:i(/$h>à =Q #tW/qێ(PyJhG6䖶^iÆo[Zsrvu272!%!LA-W@wJ%zj s+ X8r0Z~:QSo/偁b(߸$1x~>Zc ^ II-Wėf$%Ua'P_a/=*ZTWFIEمdgKTKN=kev-(؛̔dnnZO#oo^V17yN5=#wo^Vb"'uW m|'kFg-(ػ-N^nC=5quɹج%KS!${/K7[:|`}Evf}uũ%#ouG+OߐS iy{ HHMީRf-?!뿗2F#}@JU!aQ X,kS a5l_;1p5+ ,rIr"DX$b88P 'K>_*4D{Rw@jkU'&]1B>C_"'$!(Z!Iȱ+20#cmI-r+1),-nY@W]XQ* Իr+yL3SlZJՒ– `T&$EÆB?mi~#_3ʂc#[:HeЭ*% utHccFM8|*e5 ghPx+ Э T& SAic{Q-hM@r=@%F~qI*Uґ-rI` [:A}OqKJNf+ KOqGĒS(oRFKs}qzzɓAwG Lؖ}aKe!kZJUQdm[bf[EqnZp ̃pDALgP]޹ IDATwGBziqZOO]]a<)еu42֝W i_ke ?^$,˲,kp8@\h;EwGm?^ /~qsP%9,0AΞΔvt\de88%+Nu|hK w`]1h?}kK4 }TL?kt쐎8G|wB#+I"TPWWX 7Z]B<դ 1-+BzѮR,HvW#aAot \QIiwY|~BOA }VئV|kg?0 /Ǒ"w_}"詯:( F*--)6'jAНUJ?9EuN[Z f(e*neݺ![=p(e *:ĴZޠA*(j,oF:.HMd*_9쫮+߼!J{_:ȑ'lƝRݰeźiL^;vQ1X.ʼn\e/b%\qR]a\.$j[hfc)]WxM> {m:El@k@X a=} d #^YQ`M< Q|DJ5}BuZ#"Z[@~k Z-#`SuՍ6U^~H+6ШbRB$:XNn8}Ê ? &,WR Z:x+4o^+:XY }aB1;uVm)O]bUj1VO !<ƿ~)n N/<G*~O`MM͟G ӧ\|iƌOF.R1DXB}%P/'ʼnJdlKPث}+X\\YN7omPJ^4;779>{XLVZO {mrݰJ+8q[R#Nyߑ%ݵaU62BuiYXF+{C V&HR@DJZmjvκ9 V&s7''\Y_R=ǵ~\rmҗ,b^]%٥R,W&d$vkZ5[lմ:>W޸8< 703v{Su]]/pX,>}k`7{\8sn@<'&=THinfXP*99s'Lۑ4|Skwe0TSPi=}L<ޕrД X>ʑi GahZV+04Mϛ7)}ܵkƍgIQ988<=3443395mumʦ?gKrYYY[nr^Šg/pUc=A@t7ԅ8{nw+n޼9i$,?:,(]q㢭Bw=,@j^ؾsAE"8GA  !f ]) (t;GTJWExO8`A?=wL&LNb[5]Oi:;o޽G@q[vwwwuuӌesbT*yuurv]pZam7L2{e34|ށNϒ^7( ~b4׵\I!7 ߱ACv[wy j_\q磟=wfb>fE|Y5E& gH-Uyz "U_5qm+bjؕ˻~7KЋTCϽpC>n!A~C/4VKץګ\isx=%ǚk$n}#54}ҿ—ma!jead>-6 d23AF7kB۵ rg&?;Z|<.04d3G)wP_lCK|y IjZ.{$䢟X-{ f̓`v!9ޗkZ&u6*,29::qCzg?;!mX4/.QׁW${5V|b{a?BBl6br\t绹)|</BI.g$T7.xl|sR!?ux A0(@ ?/,kl6p& K7]iAp]D%k ρ X`/OV f(Wf5]Q_.2.L{Mr]nkfL}U&94׵8.07zv/of=u V?whfDu¼_Dg ]u'No6O:#8biLAީslC9ۣH@/XzD3|>"yMMMG(w#{0WIgtf?.2[-X`i֢1y$kJ̑ ,>_zL&Z&W($xٙs8K Bڳ"a5znqxČHMf{W=]}{-zm׵]JcP|y[a M+ki)JGPgws6wܻf#_1zs 6K^~ŌO; 0u4o(t'8S8,Օ+?P_o63O6kGG{MMZs .]j>u%Kg͚^ 6r(,tϝ㖳B4mDo|V+>Y MR6xG~eYFtm766F*R!`6"KY}. qpSDI|r ;[ޡ=gRer7fnp8d$ʡ%( +W`lѣgYv`pBcg).i|CwHF`mֈikyf80T[iq GE/`j*52>{o%z fP~y7+B\;vng>C784^ D9wi2/ 0c^ޛ0."p .eL lkk͛As\]j9w˲==U?aڴiq|>3Y|`l3cax XzbWk[/E=ee< -8B!˲`M v WD}7gPpU4/myWfMe+ki[wX,V:>  `Ï"&,7su]=ze/46̜,l[}@ Jӛ[V]S ] j 9.#7!/KsW9whω)ȫ:v4tpwqejpAϾٓ??}op'>;+X=WUqtNH;{ + Na{_:Qa/vޅ>%7w5q`Yժq.5{Y$ySwf:~q` 䉢ELsXXf?6Z{ZBwWB6z7.P0e&a@}f烹D![kWsf-#~Ròlww ߦ?~{ Oa4ϟ*)|uhzW&1~D~ǫv4LS \'NU:yH'ND^7\l6˲6an:HggwIZl_fˋ7;$8yၰGlMt_~-ޭG)qFCH>rI{$itpϧkh:չ޾~xO1 c  .7! gF7Е*0u7oBy3MK+ `{yph vԝ/V[4]CssJQA9oDkH˄t8T.k8` |w֦YOښd3%,f]1qB<}hı ۯ6\90V}]͘Hho4k*7^HfOį*-c<@l_ߘx|ghTlթĩhݖ1wH~YE MJϐqF7h] 299pʲ٬9ff  R`vɐuVB7lY3G:(G4^QAVg?:;ﳂ򡺪#uʨ0,H2YEW\w}?{ޚC v{G^-;M Ȇ_h**0,12XO5ॿ,YtgoMw{ϊ) fi =o͑_oB|XM-~ Ü=[?5\F)>~i2vW'k\  ͩw=-7W.j_ȏv]Dm2͗(ĽߏZeYj7o%863mNLsG!(zq_曡OǪip&^\k  v`"X3 0,aM-._b"⡟;8ӛ3aH$rp B$mcoNOi<@Wm|?]fUɟg _mӺ]ʂ!2,c'aˍ!I,`ml_o[6|MqEdz^L{,+ww3 k1 |*ƻF=@3f1J[fсZ#;AAvbW~K GC-]?5>5^MY+w,NCK|2CBHVBG_DBwb,ttY]{@zNYVeocGàHHB .||ǝ` gP< zj/^xsۯ8XLNeijg,ZW_-΋#;+t!v]/V.WVkNF)ОښSj qiiў. LHKWP['6vž1k%`h(U ))1huqvfnHolJJڴ$[lkm'%:R 3Jvg5򔌵~#tavl  rV%o,Lϭd@*VgVh281-%6-ɖOi:;дAxFH"]2hU IDAT8^ \,H\aH̏bXkk.ř7pqq"dSAh\0;;$I .˫N"rH>>e`~+}|>>3YT_=# NJr3V MHK9?<[L2:DWg {2 H'NucW݇9f3W aqO{CL?ki̚ȵ?$li {X~[_d.V+_gfxud՞pDxF<nrj/.՝ X0)ccYi]NWum=ܖ:#(eWG)5*.-)uJx%l_y=I<%h [иY;+;ٱ@>(oRiZ!2CU2P'g6F8Z׭KtV7mlPeՆɹƄQ|s|r`˃ZԻR#Ss 6H`:!{h ?\)ޑs#Y|nE㽫`I?{P7hWX%9!;Fye7:ے$=kzKtVgWo#I`:+v9%AQqH#|^Y,O(7ϸnH/򉉫`i.0U-w ۠Y.\56_P]6`]AőN'$  8猆p*Zn Uj'TWe &VgPb$)>g/Lk6q?6It6̭[%3?5+c ݒn]>?~/d <.b^1[fgJs'7 `M/l`hdYy,Ϲvu B121X+oZ0kt@pLH=[vhuG֮ $]w۫3C1J4VӶT6HN-Lٻ2C 7,bб?thSI˫iR Q9Fd^'glzG.]1[yḹ)mWoFz{ oO_2 1 2km8MJӬ%Se< $Bo_).[=M",L$8׃׬q㤲 Ϫ9Rh L o\1%r, 斷bB2D/EOTw*zRp$H(@*HNt ZN,06PH} _[]GGyKrV*R%RNJa*3FЎJl>|7WKIcݑ*q{-AwZx(%7L z9) HUJQ;<*M#`ћ1'Iӿ\c4,0fX =qe9,2,z̙Jٛ+l_ٮeo=җ`.uuqE,c`  $9*V!HPM(q-y'R"k$&7H?BAеP[Unl- YlCنlC[BJmImIJ ɽI@Pպzͣɽǽ>s}Xa"@ BL2e ,T@k̊rrԵ]8 >X6#ڈJԼ=H%93cPY'Y`f?6Uq^{3Yfcа OkPA݁awG_Njf >[ 0fůe8(uɊv;0VԩIJ)̙PuD7_l'^ڇ^ܳ'm 3{sà B&7|Di?{ر|cfk#׮W >U=lC_ (C{N}"d\̱Ӟ?ݫ[NR*Plo[{TPO!!q*%u{۬vmcє1P'tQ øLPVI) `3 !HS88 eg.@CQjiBA:$qFO ð%MZ%pUV7 0v'h;xڪ]kʭ*)7??'q2NU[Yee |BA]cx;EQ0Jp 3tt]CKF8 88X1oCc0DAapg?f?߹3s$ Ģۂ 2ьо"Q8LȁGvYb[͉$ BdxxDH(:`,clXW-mR 8$qQgD__ev0wμYaT1 nI㐃Y@y{2q }b;}Vss.1pEjw!LJ}39ї}C &ϋ[Bɤ8WFx u_ ˜*W:SQ]h38Ⲟ)lF=>aMfLC_³|K(=;MM= 8x<}mw?H&ϟ%/Qt," `be{ݑ8 Q>towXNة(8Y˛hK-A8s$44$8tE !"C8xp"DDpB  aWzEGi8ЇIO;((uc # ATH""E!2$SmRy+Y{|碨SRDΖ-)¥B`DwZz#, FRvT׏R7\75[o/D"I "49Y Ps_}/9а)=wsci/J־އG0q'ޓ3?[*s@,N 7FwzR;)' mo3`ԅMP`#*"{sx)g~kwB0քs *U\HhȿZF~#B# yXL8ERW' @9u,;(cnxvԛB@(U6 BG4*ڝvmɊ8ɼk2;HRYTX\jۊ|KaaNS%`8‚5Kʙ@kV؊ 3Ҭ-[}.[f>J*HMef}-tSf)fE_rS|0 uZAф)4C%$J[V/W?r-E\$IdX̡NHO$?Gp>c=1,痉7W q"jƍYR:"=ѨxEaj%&pC^[rJ lK( ?vfB厶?O/-u̘1;8w'`YPTe>v @ X0+bMOFw#'SKT uF^f}MTQ||YE݆6z;.cy#E2>ҦR3m/qB0 7уwɝ/;l6Ǿ"0zGC0GGCpPC螟0pb}RH$D"HX K`@p3 ob w8Cų|'~a9y~0L"#`v{:]p ԧҾYDDܭGf(S{<<`XAn6;O<@"p]=Aea2>-JϽ\}GO}EqHqe/3>b8ʻ)%B K$lpe^/3a`< CƎ q9vz0"᡹r3鮹3Bz 7'w,-p0:¡(+X1'$Lp(KgWDDIREÅ# c?3$C95q]pd8 g j'~gYe2}a9qCnw0ʨ,"ESP!$/N#&8'-xj\'6"hɺq2YHgpsUq;cY;[/op؎/R(DvݴuQpc~BV,A8.b8J,q\, .\fd9CuȌE>ciEKPg6|~AS #Q2)5l3DG#C4FI~? !~o]b?'Ι5k@ ѺmRy#満ϥs˲ _ŽF6" rbqeBw^zDq^`owEX8n|e7oy79zXD>h$j`SDPtLΕPYX@EnǼ,򏇇gZ4M%"'{|p,K8@f8i"!p_N5FE)HR. iNT,KӴz(JRYL"B҃1wF^eYeAנqもbb1qZ@rwa܁ p|yq'1s!DH#PmQ+ zO *,$D|yPݏHI򏇇gg7_XUǢ1q (Ͱ>?,ǹ=l}-BIbE'"+1,!SĂAE0OJQ{TydX,;<<q8ÇwΎƒDUӛ4É:|O233`uuu1/465m2; peΝtĸBADR-C<sɿs!eqw.v3ܹ1# U3b@v{{j)ŘY^\9'  i/"җ,&.aɱ{pyK/i@@( ]q_AF6Xv,J H`i 4K %;J 7v zpй`"(*(#3PqiR<<<7h`Nw}!wYᾱ9)8gFEGyI%q.jhf?9#? ޕ՗Vƪmq1qܹsΞ9yo浣Am}Z|I<,N̊)Znk‚Z<<$=<Q.WߙG> Xw3T* vi8, arAaҠ X,z=n:7zGr܀D:<4$ 1 #qT ( $((Ha+B)eȜ`ieG\':Cgq8`#b lbBD<ᙜs^V1;BM 7gdd /?sxEҥK'Ouum/C]8?J&T$̤`Y%NR͆DDDΞϿ>'a8n9~LP$o~yS6o)onnYE!ͷ/E"?<88XVVww?~}~qFyG.:>_>aؽO0^"H̬Y{N1LDD.  9Ne?!!!O<Ē%K\4%.R\3w^G<<8 Bd=W/GG´\\[wYɴ<ͨHwF s?w ¿i;{K|ԩß}v஝;Ȥa IDAT ?e۷oݺ;ax≱5]쨫~vs:,lhhd2d'Osn8?`cRx>jnjjllXV*xhw_^o _Z!FFEav5NګkیVH.' +Av]~{s URN~ANnjUg^Ӷ+٪X(|xxn(lOlmږzUN[ګ+k;V'@(I9yk3AAWLvxysCh?e-ǾS-nOgHI 9U=8|<8V-!A Hc 3gΚ9sVFF_ݷ>89'q'6g-G^xTF_o}QjpޚuQ @; VMId}]ukNZT߬q^l>Vヒy'Nx}ฎ}f1iEѣyȈW_=70vqoꫀ 8^ZZiOݻ CCC/`?}~_aR4(H*M~67n6<y2PҴ){6g뙒Mjrrڸˤ)kؤ67v) %l[K+J+Z{h{NHmnQQV<z)k 6$C^@-H){,ܰ1GbjmÎ,%@[6kܐ rpή׮ vSj)L)t3qx׿X5sP Hh8N{~ aχȦ?$bٳ| zĉD ;,~@çvYeӧO#}g!dL,ʿ9s~!њ5>shٳֆ8~IPV:y?Ŵ$~h{ӤjGvyꊺ zKKkNf$RԚiL}yי [Suꪆu RA^Ͷ)pybnnWa֙ l"u[v;8[60.(^J{nSc0[ ; %.Р#RTӽ[Μ,l;1]~&~: .T)I}`rrev:jYQ\^Ze8Wcuva]X=ʘ۰-}"Z{RQÆa"Y\2^S'kG?3ei:NL =n3$gj;ɔ )IjS5peڪħu\>Uqa>Զ &d=pll{ [{y{$Iپ]}}}k|@Pfg?7ߟ F;~y``vیak.,P׵R).tmu<]ICKKKCPX s-h )]]XrZZZZ*2NI0wYS+ޔ(B^ݾ}6mTu---M%ɦR.DuQ^mͨhiiii%\,-ܚ- CqqMQVBJ7r ӕ4ydTN l.Jzk+U{W m4c6PYuvZ {,g]:i{.]lMs=&S9449@DFEH߽9p9O=URZq"y-Y!HBqEg 1L,D Ya!N^i߷yvY$Ik"pMFt@Tqp(WSYUM---5yXa@kZ^)SGWLˈgI5 v◶tǤaח{woEڮ+*J"ȴ-w{;Ȃdgab ,/ T5U`$n߱.4Ts=*4Tmٹ$bߔn&m߾cwM.x{*􊦖-Me:S"ORYCJ'hs  |:#1&,bu%Zcyica =uSfprMֹKmq\S:I<<<}MzLw+gCN%Pw޼?6Jr}!Jt!CK?|aCe)zS Y+gO/-pZdJV O҂us3Rrm̝VT$@0 12)]-\_A`mmlﱹMsK:Pg$)67A<$Inj5+rOQ[&8!$1{{skŅ+S7nY(ͭ]IRȴY)4jgݬR4vUFǯ[R`Njӵ2pMDZnpeJ1Z^e? 9ّ#,@L6#.N5sjH2> , |}Ey/`@r[G ωo~u?I ( <C|, E =^?ˎf!DX X HGD ,&?UUUU^yZJD/ʐ%ojZk2賛fb]nZr]Nfl5:UjR cI7}K讯6bt5>`Җ>uغZxϩjɛf'e 97e]pJ $k0g3PL&R[ua*R؂~jGitr.] FLP ]66Kt4hZ)mGQC$63T)84ZnFh3u#*8'6eֆ\5Y[2s񃏯혶Y;Rş<đ^9͢=7nwܩqZ,) |Lab H~#c*^R@5"w096vQ *m,^Yl'4 ֦cS:9IJt:)L3Z.@mXW-C4_![XTUP[\ԎS 3N'i1ϧ$.IW;.MHV[Z9m騮l4`VF16`9rubd*0{ vO{Uy<8 \ryo9~\"SPyscc#?3qULwN [,aW~m.K/qʕ+}>OҊXaiiƾd\.,sg~?(%Kpe4!U W'@[e~.d(>sCa#q]FGѬ(<} ` ;:ȢIɯ_(zNռk|.P FrI\NEQ[_,ek %O$tTֶ)0ims`( 0cJ3l5A@9zaseڜ$Lht%* ._3a H;*4e /c 5u[3%f5ԗCŜ>}ΔzqٽL)s`6oڶ:$|PÃ}cߡOܕ s}Ξho'^.:P$0F#΀r2"O(程`(' +hزp;qYƧۜ.wqmby\'Hcyuӧ8ǥޔz4>E.yB>HWxM%k;]f+`&tJKԘRU%A`|edWdq}%;=;X2,LOdxl6[xDju{<3g ˍ m?}18XT$ M%d~Agfpps{<11B۶mA؊IBA"@$|^ԸX,J^,*?Sm[_"q?˺_OiT#Wx%Jimȼ wkzݟzhFF'8-K?:b>n:DMuɺ>G>k}?zB&͙9f $Jq0ƌy'329#ؤ\_yI0V. 8\Bo6&3?թ(mr,'lq=6*0s.G-'+>FrNaha \Ԏ16@[ VR UI>]=vUV e/'積뷴8heܦ!V䧍̙[{ip:YOd.ԗ68FZϞ.F¯g ?sUDFFe>d/H:W`P(ΘRܩuxqq~]/]l%%K(.h-k7۝"h amY~y@͜\94]D&7KQ_Z`` m^QdI Yڴ57Bȿʒ Jť+2@jҊ9%%eEMqi`fYA^<./(ZRHSR. W&)vg>^VYv~TS8st%BY_~"u-*\BJY[^;oDgq<((Ha(AXpéweXMߡծyyq'H 0T* 8aPbJRT*!(±b_~1.J" i`^L$I TJME8.0$K(q>W;ʬ (.]Yig _ZY8XXP[ZfI93QS#xn[*/5J/R}͋,RXyTIyey8JǗUm0l<)7tJS%uk2;HRYTX\"놇?06sRR ȉSgz>7; R ЯmVC$jΖ\|hN\IesN!dhd8N; q9Ey .#Ee |;#0R.~jwɢxFG]ekKKPq~q)JɂIޛ*jZ}f)J$=~dS'A횧Lv;C&嗤Sʰr'B[V4? 8eYi-Fŝ8q"44JA*^*c=P/0mɂbxxj{dEM^-eفr=(ǡ(*R@s8vϜ9/nnF?ܩSR.B! q,QE8P (ʱ,˱~e,!(*@(:,˱sX q"(*_Kog߮nd}Q {wjIۙC\2iy󷦖=~g X(<>mzJ%z2sPWu^Aҟ;n\ڿ ;_OPϜFE8ƶq|\\mX.?su#(7L`ȋ}{?ܘ)eWTC !80 L BA!]`( `]S9ƍ~)"9K~4eh+7n'bQY,%B2ȁ3}Nsx98ntYpx~o_q4&H5NK^\+M~#o-) %Syx=\0L_ P"(H$!ZV]9WޔK+ n9lPJ\0Jsܿ{nd>KLS[.>ya5i3}Κ7I &rߢU/ߖի*Eer'lo:-@Q"(qH$ʅHfM߇ㆆ=qɕ8G^q<VL3?9|1)sYe+Km_W.0.gǾ7"2AB!'Ǎ?W~<<@@T90*0 K^gs;& n=3,—fDOD*ψV|BdD;򏇇皃\6Ko\$?/Xu߾s 1i g/ri'ǖNe<ʨ?o2"et BedCQ}w?0xʨg8ߍ s|N22^]YePhSrվzif6,-&*6%@U3’*nNt6>]ipHhڔm^ޜ7l}]visbjmVlvYS۳=/xxxU dm"mnNM-Ο O*ka_G`Ք4m@Wj"ެ.*bDs[QӰM7?mYsu\}gR$b|FbF&>)UUJέ;+ҥQQH-WUR)eM) rpvԖS5۲);Ÿ J#Wl燵lzV{s%kuf;- :,VvtVJ[4ֶ[vFW9Ch}6^Bm:vF[R? zKKkNf$6MV\D` ]nAAF;H&* *U RQ[V5>Ү݊}@nW8nWUmƭJUTz[P)$X& 3< (TPH23|g3Τ{iנl~S͟UD::g4 Rv!tO9^ "X+OˬX\Qʚ,&~ǎN~-e,#VM5&Fv݁2=D>VtJUJ&"눌 BKeR$t{&}Td4 4foΖfl'!^$es6p>j2RJflٲq"KTw=% =7' rf͞#\BS.̌Td򞎠tp.\I_Lx1P'5r:W~Qjq̗ƥ/ݱc1WКs^?-Ms`{#ǥ%Ncqiֿؑ\5sc=툈Q2QV;Ϡ'^=Y:vdD3͊ _G1]F5UqD""v wK"DYzʈHd?m暩dyMqt/q(_18*Y⸾}>mXWZ4{[\?nT$s|Y GIgY DD Yɐ I)qY|ҿg[sGPiB{>ƺC ,,Z~=y"k>sA8S)sE6XA3$QJe}^WgO2ǹ!m;{\fA7B9s\Gā P)aaCnx9D|]QV~= *TBYM 6(&z…ԷxCy~;qd{n͞UO4gJtws??ogM0Ƽkd4ΉTf_PFqwkιvSW[{nnk%"MBںՈ孞W"kJ3,bYѭJZͭD$'h5D6޳µnOyhkk!LD"u 1Aܕo<5Z( ͂OCxs0~M q{=(Y?ϕY *t=p{kפf""F9#fRBNHK_v*zL&i;Y[=9=)o=bPI?: ܟÃfuކ5!f~5q-Ƿmĺ )o\lIIy Ham&u˩2 I,3ߧ>2vLd>|"ʉ+%tC[^_)2/.eaX}Nt qi$*:\}n;JP> eYf""{`ĆK_t&IsIg4oGΪ{^Pst7^N!!/H$_;3OTڙc,u[ګ2'ل1Rcm#ӒJ&f/ҏY#<Y!"20}irǾHD%i}/!LT3cCtH_#>+7l]L.S]̢ gǿ7ٮm_TSfQ=]/]?D~#GZZc~VGGGHH. 'c<ʿ(M T O虫D\StqG|bXJDW̵բ-HDd8Jfc Ot|\*v$yeTD{1eg36w7#rvo]WJ$vZ,ȑ~fesr.O]0fiʃT .*س"/9mD"Vpx15?AO H.0J.b L@P?@OCSN:Սi|M_{yԩSs?c>x=uZ لAoǫ&W +v :jaU$Dğ[s6Sҏךe`PWĪ>/s{3JDg~||Dj^T,!צ\i4SND^Z-oޣǽDo-(ݒl<5w$߸09):|nHLd(y=#s0cI xBeFQR婢s&k {2 <ǙpjkRj9@$0/~X֛Edz9ʢ}B[qǍ7^?~GF'~z=f%f32DAT͢d%kF#޼+ڔT9#zmLg *T@zl _ce g$Նnח_~R<%,/3t<'"k|>OJ)_~eN}/I& #kIsyOD\BDܩԸt-^;\ښ=حxeIFV9OٴtǗ_~-s2vdH8Ock'KO/q+G+؏?Ixx>o+JYEKwזHW 99 &bt|#&>,9WOdH3SIdsЩ^{e꫿O:UGDo,W "sA,dܜPӜP[ǖ~f0?ЙHTJMFk sD$7c d3u(e~r"bRSHB?!ԙ$J?nьrNXD6!4Ec̟!|FI9g8 f0_N_p9"OKE9$sWJM5Fܾ OtIOSR/`|LJD|錴ӵ Z sbCWiIqv|hL^ 3_M0F&5:iL{.(Ļ) 8zߦda&2ݓ512iROv' wΒ+<-}9@DB|*>Te,h R,NwmݛJ^0LTfXFר&AnqMD{Db)N6#t"KDBW!TR2c˖YoOHɿ$'$DuYs}ÎO^ZmS7̉GE]_ &I&ڐ0ύ?}iΘ_r^_?ùq}=gT7 D"29a az<5.~]%ė'z o112s'&"dT*u2#ϝ}~&˾ >@~TFJR)ϝ|$hbdX"(+@D$0O8q1]BDBr"2ݳЧp.#)n͞uW;MKdz56 ΁s.dedպs6 ԙvLޚ2ȝ?]GD={.`lQ`2 _s$$Lq3+C`zP?:ݕkCɞ j?WDpRJBY]#ÝJH_3Y+(6.z}57/$qOYꂷRkg.vO{']03挓ΏEo11>޸5.9_ԩSN}-n0kÖDR[aaaa%}w[7_yx۪FgCܷ~^dACA3 _{t bY^WV0?6\[Q|B'ԧ/ [uaFNlPH&Ͱ7Rg_uqq^5쵤 ~3D}J-L9 DNx&XY?] ^F1NmEpӴ} X(ZvvD$k M Y0~K#Ddk;udՆ~&g%rj62v+{6(aߞ4ꏮXa(*`x WŶX0 ag쮗;[mMmԻ?oL[4B$v8:VLd?L k;w]ݑ(廁?y,x(`hvzN1+f^vGU?=&I)&j22{I2QQXK;]ƽЫ`qñ֩3F kΜD7'XϮ|3R;Cvv/{a""j--'""n _Qc$w޺'WKoQ.7>M'Ӈ5’^P >OxMSGH2o_\^i/$ u;/q7=J?LiIwz[Dߩ3N.F"yOT,qu7R{,Ȣ"/<C? "(%3?@(xA?ybuEd|oۉpY/2y"2LD,W_vj:ʐ D?l[yz(7WM _4U;)4vk;2?;;"j+'/ 3Hrb_peDX1zB__ P8PZ͟>Y?] ^ڊoyik~^r|3y˲?~ Ot|w~;VA9n)~1A'ؗykt{F[~:7NA֊~lr}Lx YyHKoڏ5 3gd%%"[۩#;w6Ѹ;zYھzۿnw%gߞlQ1zwK!uql;{"dMG NŶjo:+푮o*ѫ@b+~F(=aG󮓍jMv$[iߝvwGꫢ汫.yx vfzF v7lfXxMLc펪653{LRLeNedBǵv{ۡW&cSgo9S#HNFX1K-!Oܙo]fmwD3XU5?J߇ȊoADp,5RLDr tyP_/IDVDPfGd@VX\;ֽ0|X Ty6:9} Λ؏yDIy~aO#~:_2KMnhSZ]^wꌓ<}|DKz{ ; g'W2o_\^i/$ Fr?@Oz!>  &x&@((P?@(&XNhx `H??wtttvvvvv2]VVVXXXtvv p%CUb+++9?xDсxH$CCT 7GWoVJ֨}}}}}՚C_=]ZKYVwiԱ:+.ⱴW=ou~`)I~I^TҤūiCi!q5iB0o1;WhcEo>VhӦE(z'$ErVXSmk#vԭ.lO[-35%../E%n{ѕց&2OqqqۏmK>xJ'YnyZ[< RM{-<>ܜ]}=,_2dtojml: $|q̶Kb܉(u:DRb'̋NE!juxTj޸CQ̄ :S):ƨ5Ճ#ʌ^*w&o.4Iz:iijO I<ձתJ6xKCS;s1!18"*vjjMBs\i&$Z;FgHlZEfoS&, QãSO4_xqxH }]:$D81 x"VVp <^jiW+奦Ցl[H. H.;$l'KUŵDWJI9ѱk^_)#VX0ά&"HDD) #+E\E3M TuF2j0EEAEYQsY}^bAUGC+LېYbj_\EDvn) Izjdko;?իT^K9mycPZN;z(/c\W)_nT }E 4+LI8Wx0/ѣ01HTV,ߔkol}f\./+̛ټR&7N6Vx)rseϫ:#jgWW USPZxӭ|iꝒ'NMNI8гs%>Qs YƛJ ybT&=Pd )CB݉v]v,is)s :thSpiJ[F^5w+5nH;2VDdmlwV*"I/ӥ暓md6eW^(j*B],{n,1^-޹7hnj+3m3gd$Q%9KPZg/R]ԋ<$$\LyyDTճ=$Dٮ t:M@Dt,I-RDziliAeߍ`:Rg`H" i+y"T[&ר~wc5uTbTzoML߁r3 uŜ߁Hq(tʴX=0\"+(vI5.'Te?n\ib,UQj"xk *9Z0ʼRiǐHL.WRPOpcƕ$\$} OG_O%'KH:VKz{+&"aEwFf^`6,5"V#3tXõءx|dVP)hmS,\ "Py@Ug.P3L lu.쮿*'(xY󦙕DDA`ݣJoJA5ufJN 굙gHrDlm%%(kbDJS{!r W}ی cݤDAȻDB55$} 1,qtlu{ Ia] #e xR5mZ\c£p i);X)ɕ#H.D)^g|b'KU]7Vo=iɟz/n_Xj\Yk"j6rDDWaYVD$dEp&eQ= IQ`j'Dd 6jy67&{Hzmw0+Jw-ή%RF0p<3pp'<ƚ]\ ۺ7Z+ :?#_R{K%W׽ N۴u\gjK(7 UwϳJX9Wwo8}5/w&b7kRC&\d _,ai }| =gB 0*vg9G\B0m62lx*!a\Von#"jknԗ̯i#jZ{y;0)|D-~]Vr0赮m\xd)پ=3xOj' ܣž|nLec?Ag j7Cၮ> lZg8N e+pmۊ'I<5CTꄌMsY.o%y" Ş1+UnRN "NӫY/e]ыnn!^ '+8*Tro 9ȅn83IϪfU`VY'"B5xb_\-@2{0n$q$W IXBR!$\$}dXiMWv>Jݛw^Y1FVom> &O:xsz hpED&w死KAa.ūNfj҄&i+Tw!oqD콻?9_:o5qou 'N?Čutt\~}H.;ɭN NZvZ!Ar\$ErI[LQޢ\2׫zQƢx@JҠ!m%B=J㑲-Sv{r";M}\A@rΚ*uB";+c3~atQ;7o]BcD. rDt`!U)f'zIWKç/s_O:侎 \$ؓp#3A:@ .dH."H.' Y0? 3577[YY\@rɅ>(e/]܌:rT*ES  O}rg߿+,6x SNE# .B+**t:4y眜H.>a /CKM OZ&xyC~Am|ъk-m"a W)Orc"׽cCz1I+>x]X/`Imc)s#5^]kx$'hHd?7L(nY yNi=s#kŔoj숨lVVL\8QvT IDATou1>6XY_CV[r۷sc\ėsk/)Ldj\l&mdܭYgjm\"B?sgτ2*GdD:u_M+H$&j(i3nEE2kwY3Ճ[w"ȕ+xr~J+XDD'ss+EX؃/RX֚knkx~zdWWW”R^TCS&vLM+T Dǻ˒wg,)%0gfB86OՔ[)FxWaۓ=RQ A?nzWm,^nWlX?Xscc 7wy?~f}8颱*v|SK4]㈈Śgކkln#뮒܋ӆp\3w7 h~CA2g+[fҊȉmgvn=j=we#lkrH4W!65XvMZʈw3%6OwS̚,Xqw j͊bv Q w26*Gdmvk[f+6BdڈmF("VFzv'Apa@Q+(s)|mzΟm/g\ղYklU0Ʈ}{(Y6S;eKV4)ڷ["V+*G^#RM4"vX-'έGId4wXUɂh=Hd?&p01+/۷#ke`6D6,[Қ{2kIُ#H*@0ςa3W)lb]Xc6uvvvtt K2CMê'<]Q¯"ZZZ X[[;88xQ+$<ʌ;880 !&93f㚇+W>1yŋmmm%  o޼yűc> ?B hť GlOxZpؘ<ʵ\D" 42#=>*O49"ڰhj| p Z2T0n?+)Roܹ @hׁ:R$ŪXO]@ؤbcuQٛW2Tg 7,4^ŚO**A5̛ ^^:8# eEjƟ<Jiι!Qɇy*S-i5}شKV wQ61+Srݾ{:o5Hm(*kFAD|ibjfS"!4PGaTx*XsNMdt& \29>܍Z~LڠZȠUVb7j$p"uMJA4`yZ;RV MV3Ĥin7M|J)ԳRv9Fhh?y56S݃ XFy2\nLU+$DW%D';&A띩{u=QtiBHCؐd.bWDSNJEgggGGG{{{{{ <_ti?T_YFً$DDM|_vzqۭʙʪu*M 2#6 6= r79Qܟ(#"x\..x &rW{f.`).6a(H:;ּs|R]y= ٟg5KD PF;p޴a=!-QN]6efDΓ&>g5/uXMUP=_6mW|ZBO%q OT9/2,S[}782CiXz۴`s9X#U%G~v8N0SMg;by`归(X"n4KX c\E^cS~ߢ7T KgVB5Gp5wCN|i^D( D:ā H0EnWۢHc[O9^={vbb"VgtZ|=z9ύ*y$˫?բ~W(/D}hW]HX#9ϷZ|nFnIU,1n5nt !lu^1MBŷsSBTQP̫D$ 8o+f;951D$2u,=D@|ԋT)'1DkF{˅b]t6EJ*#!xWZuÒDgâDU] |ttiP F#(*F"pwΎ],?FāQ hnn>GgԠ'ϓ?~SLmi ݔM'E`&сo/PTv2gq޶qܥi } 9X+YHCg5lQݴWt.w>ۼJiCpER;]c$Q"Jlےr&ߦobRp7]Qsfso.e7Q #V%/g<^ߴtRbQF,Zţς!Ѷdknײ11(;G }ͫ"rc ^Or`sEڣaю@ƯըT36V372ZY8N5@{gZy^J -K9nP/.8A,R['ԔMʲQ^x\Jtڋ6߈2{wSg.!%Ia1'*pKρkrl\O\_rxe 5M͍8h0]7?;j/'-Ng?9~ȡd`XeXK_ƴ5O}psқywLI9M۵}^#glk:قJ^8lm5\p0603cYT}tӝmMԂGu1bg?˴U^?4>=@f>d<#SACD.䕁%,ʙrg<7j(bؓύ9%t?=X,^xf[a5Z8rS/YycЕ\K.=Xj&L_?y4K-Xyo-DΑ~O~JK}p_:-(9oˌrs;<vpm>.J⾬mmZ¢ "վox.#7* RJIqS]SKCjQ ݷwRs)LRbcOmysѢE-Z~;v.LpB"}ri"y F'99JWSmQfv^;h׼;6]^P[c>ޟd-^mHK.O}|[?uɳ~ CJGl I[|p*E?zQf3nɁwr~eFSw~ڑS(,/B-~gj:qi2mնss$-K:;>T. 4o.]D˹JʹFC_F@M<^[6,D"ċjgFّ6vwWom%ࣃ'\錄}E;asPoΰc1XьTۮ# ׾TS 4pQNٮ91@i/N/LYW^/ߖqA=ʴlKk}y\S4' O-o3:9t.C4X!~-/s?}hEm۟ E#ݾ}m(u!&[w_~_r֎t-Z꿴`΃ߗd; "󑌓G!~'kn}=(:P̈X4c?q"c5x>~ܵmQ!f# oM p9WNx=5ZeSSX~2B]<0wd@l}œl xgb!BOGxӘg ==V gmR_[:v:S9P_cG\!B?j>S2tmξ>K}sj"B=]p?B!!B!!tWr|hhGߍ!\~Nqx1AaB):::||pRzN/&'Nz|}}?>rמ rNqx1yzxxa<P(¾=!ykKa"t]{{{1C?>ooFFB!B|||._=Ez]г`hhf͚5kɩ32a @!BM!E͚5G#ǜ_y{{Ϛ5?e##0C!BS.SÅ3"B!!B!0C!B!B!B!LB!BaB!B?B!B!B!!B!0C!B!)@ON< !BMg!B! `g B!n{B!B?B!B!B!!B!!B!0C!B!B!B!LB!BaB!B8) Ey.I2(  `0rE#?`OOEQ@4&|GGJrss{l;00Y\0rcYv``@P$I$A'(( Jm6T*{m6࠻;Aa"Bw700088HQT*uCxNz5kppP"<\0r}XSg 12L"`9#B"HRV(:riJa"BiDBG!H$9Bab=!p'r IDATy#!\\ŰDP pFv\0r1rz""? I$GF.Bg6C"$F.BV!B!3?B!!B!!B!0C!B!B!B!LB!BaB!B?B!B!B!3NKHv8nnn!`!Baӏg뀇s +rARĀG_rڎW ڢ?m\ˮeCƚwxz7vy9,خW S*}䒤Ԥ?,T[VPX^lfG&&1#'*r-'6j͋LCi?l KkVUL&$%)+6D<`ؾ5SyvDX%-ʀNUKA5pU>r @uK!hǺUbbbsea).NSUqI [\x+ѳꐱPFi"BdXZr\ C~AePcҖTo(i6"mװ}**w,ѣa*a촛F(Sw^ %@\PPfnipT Uf҄-KIOҮP* -l 5VE.KdNlx+O͋54뫼?-p녭zˊ蝇\CaeT)iiA4ܛQFV ? tkX͒'؋`>_J%>Kfi(ڛWno\[M([7/I\uvndRX]T7/5Zc(l\ON֒fUB^сE;4>v+q<R&+QZAj\yˮvzȝD{5TBNj 7l(0WN})7}hkM~n-Y{o%LMƭ&Tgm6TAlޢv2iӋ0a=m= T[FhmHиEhw*U߷|krn%{ӣٲZA)E);kٳqZݩƒw\( Xn֧ܝۄ_\U~KxUlB0 YYeF}΢f'PUVh(` ٻGb {KMqO]M 6WƍlTŦ%EiiZTƴMT *{jjhPU-D@@RRyYFkSCSaA3eomʶ֭6*SJCQ@M3kd+(3RbOnK.+I5GC|&W*!8?eÆ K]i#V-^XPkm2%f׮*(mDS.%-! o6GGxЙ0fK3#:(]ԍ5z f.AK\\[l4[ّS[ ֌~L)'Ui1ZyTd܍ qzj6rjĦg/LՌY5K(&2egfm&?Tldiml,,1܀#;Zs+;a@skBTяP  (M6AO%'nd(kUA~Vc:v: µ3DW`iUMcH j=M8 ԔaI  ^Jb\ft1a7 UXʪ[m ڻ1w,8cm+౭к0ȍLph eCP4)˃h6A,F3.l ^bXbУ`!\9 RZl0x5wMI[FT OF "SdC~YzfZa(.c]ٕ띷-n~w}fi'I4~`n{jIHIQhZOKCqւ¸_aƍ( A$ =_:đNA"G[Xj)-Ϩvn-(5EOrv%o,MόkتO%,-0+8)S}vJ= YļĬ7A&ѨfZLi0{`PvœY+^ͺaf,_ TFMEKJP[W ,7T$A!(ruUl.2yIcX51%gdxώ<˺.ߦfФdnɿ,pF(hg:Ũ(wSx҇j6gFFԍPJ-Xldӿҥ>BXlJ#$rU~c٧/riJ͸&.&aWlo^UfЧgnm'd =CNXs8srM:ש J`gCo;Apa"YίB?EXKLM7q5U1wZVf vg^]WKcT *mwfĈ4J[oT%DR[k*)fʒD)1CY#xn_WLlblȄD,EŹk5rQpV]X ?rw1U0<A͸_qywѺ=ԘT@L&PrkYRv N'k4hT8FDsӹ?o6nz atlA?  3PUlIЎԋ-͍fP- Q4PR86R}say(xm)/7h? &xc \yFBܴK6eee٫(|կnxX%p*p6pFRS`4::%ZU| 2ktȲ흚$T TyU@h_g1ܽx(=u47f2ݹ Kf6לh k̑y\ YU@Z)gD?Axc)U 7LǪұi\É&^][fH-К(=&t[ ]$lkƁ66%e׶4UfnȻS9 &t{՛ڟK6&oSQP{tǦfNJ ֐_mɻ6ףsp-? A\$4Go[?|7w"x۷g x++W<}*#IJCc~nqmbjޓ IiI ;*ZZNʭ22QaZZpkY0=M1@ku]KKG}^Ć7r_]~{_/wIt prX/оDQv*2*-oo_zӎ'ʋs6\fԯKQ5LRtbի~pLY*m1YL ;6^t{hL3`IJ4ӜDSbj.Xz]FÂ!=[t{Ajn)–/єWĔg2@i"L׺Uln%V1͍䍖f &V[!@m{ 6kԝi OWi K+Jx#SSXVݍs[ˏTc1f;}OOH$STwx1\:wv_:?wG}S ;wSCaFrkA%{Sή 7}lZOl7l\mF4o>5hIѕ3+Tx4.t] u7$ %>G5gVDŽ׮|!^? ]`s ^&_\Ox1EzM!><UZPX^U{G,)w ¬䂑+eNDw/ w R#m>!VUYfƭd X`4aS0^8< Gt:N']re^嚏~=S﮸momn}h[xs3f` l!3wpOw!}rg7$ID288cǎ>ߞ#~uI虍W\;+:]'+jI6(MApA!{{ziiE_pW62BE["- sG.!t%}D=Id)|m߾b]]]ׯwq]I]Rdo+~+.4uD辫ŖnfA9vpHk}6v-;Lg/H$'`FKޗlCN~q}^]Ip}}.ěewfYaSux)8N' HnZ As>Oz*x#E5 nMa;oee? 7ji!o;[.w_J!50A:[{GSJXY'@`+^աa+/\j~zUnI)sİJkh;IOу O/;!|.VY8[~=UkQAWoR(6<˴$ALx@v;;NDBDwW e2N( JIJBΞ8:|:4[ZW*> InlR$AH%0t?s/!@t?R7B*AD?=@.#^wr{HHW^C#\!=vP?OzIqw;L.t||}_{M^q(J;:q}㇆/:0==ap:=== hL*Jl\9ڡyG&$)Hh\PH$WH@ДOz[[[/\52.\8rM/̟f_=oIc+I12T@ANJ@_/yUP{P "  2 RJ]F*|r}FW,/| :L] zm|ɞqnhp麛B4S_6v z>JоF# cQ/u&A[5u KJ((pgص$%kU*-Wv;>o\u&$2T7w7d2T*H$IJFG8:Kǎi:wnr7ggvU8>w͓. #ErvAE  kuO^7|tl͓R>l|.K}U Ixx{_BP@J Z.WZne$I_x4iR]oc[7^붊808tKگpcco= dOkl`Ν_U93"LzZ ( iڶ{ ~ߺ]*NKBr750EioM(re2u ҩ5@!OГAŶ?D$*22_.W8OnOOgveRD&à и/vNjsۯM @_CSҐ Z. J5zB]J_7ys._ܱp({[H(Wˉi 7/W׻(i>i>Ns ug[Z:B:9 39t:-=W:G!T  d$0/8+0ٳJJ.6lwHNatG Ϟ$sП ®CSE+ Z.d2LFJ$ Q!#$~p8 v>)"eײAnH%盭=(zy2־Ap$%^/a8:9s?SFSpm 3]?WU]~[]<70v h%]J%%}s>>5eL&{)tۓ@)}p@Q藃IJwvM=MUuHd2DBIMG?s^$n"r DAe5n R۸Y(H*H 4ӭ6'H)hv)Ca4GJ%$X=E_E$NxM+}f+B?6si BApdw7E|Ocz _s<?Bk6?cB ;Me_+E$CMx"$A##9$s;'w?$gB,V13yeґ7$H$A⑏۲H=vcߔ{(=BC_ }aAQׯ]3I#CވzyȆ9gyN6AҔXy\l-zEO9K АoK܈aBpH===d2_IHPϝ4C?Ѝڟ (M] IDAT65_ vKTNRJ&_ ,EtQKeIs$( Gօ$e|g3RdnJJJDA $ )%# ":\$INͻze>stooɮĭ]˲_][YIu}NۣWAϙzNB. a߁:pB.ti,gdx0)%gM>⫞~yvQKs?1}uOT{=ָm+BЄ~h]A[OTY#' ״cVk桜p04]y L~2^r)tOSy"xfvȳ<5l{k+rVl4s_ʼn=Z"ϋ(8~[kq KRsp+/Z D $@$xIQl>m3K*y9FpD}uHqeHqaB;!r8 0 `o^)HR!)rMQA$]}0:=p[bv>Z3@X\,Y5u))o{I#e6gi(ܺuk5؏mpw矟/d% dCNdLja:ڴ_]!G#q`.s7H) v\GF-xG>EbP}jXXCRJJw7kCaF$H-HoZ1Ig#⌅7DJoâ_}@]ˇԷpξveÍ^.̵@'"g),nI[?J$UP$N!Bhdbs J$AAqOOȓWiHRIpқī-γ E!f~ϓ~z{%`ڀ!Vt [KDwjHd75 IJd2L&֙x3Zr~]\cԼ4zW`t)\-uuI9jTzuq*}kj5̄ %Ď,(MdRffB cZEdf.rwu;6mj6L !J56Z(UXBzzbc*huxRJdYFQc968NT GyG۠&A"6Gj걝δ /^*A 5s/'HHa(_5Ƅ8ǚvOVf+ Ȥp o.ܒuRey[@ƵW`B\Lb4[jf F+${p$O& ^!-ߺƿjJ۝75mf 6Wfԙy`gnX9+r j,NLm+UIF٪9vG:x6&tpKC2n3./34}p/ΛnCl ;(\Q쓳>)Jr/vxds+DPH&+a]Wg-1TV+l]gNڹ+a/41(IbTuIf'e&'f33gvWK"ÅkIjdyޤ3z}(p"rW-PEA o8d,f$2ЊnI$MBdE9gcYg3uD?P92 Q[a$ ]Mt.<^Wki:EK{0 榠(Z&zX}wÚo06ĖչKrY5E\Kƍ[qxLSM18)Vİ*Sh(XŦ}ZGG:?JyaǺ͹?Lжom}lOoܘQ\뚢vi֐n&aOtk6ez{\݁O裧Gv)[]WO_?MOo3ڤ4juV.{-t{_%$go ֐UEk]Tw`ݓ C`ݕ_Qg ə*rw0Di޶.+&zWEoi ߻gLZmLJaj4fT"q8׺;CK2Q ^v:7o8#:~vO輠)ͥ"Ϻ}¤hs:0o̶+pnRn>*h JXMKaXbm*߰.=&33zEW4~v|PjO@\dڣn[54hχj[MKh&zU]"q/P!i٫6숢}$ׇlMCฑ:"XŐE/dnnk~5ۇM"vHAHER$D^NI %Y '^t^JhFhB8"7) * K=(JE DBAayEp;|I "&"/Kr:PK{rI%RrtD"AnyBQ`h$dGZ/,3%LJKf3J[m r]U˃@"#UխĸCaT28&iruf]’ wZJ7#GiF+˵@dޒԑf.&9saLhi(.h"XXM&׶]?'luuo:1Y?@v|D'vNc__t X6G-%B*;aGny+RQf$r_^^oTiCcf.oVŦ+@w(Jh`]a3V3Sݔ5 C@& FN@1aJP4J]4l?tG(LUh0m,e6DvcR 8 [Pu n=Rn >*/sp6x1idvҼ&8bjs:_i%AʎkY?ӵ.ӵ޾:?,%w蟯8qNmp %㽐$\QNh_14_)_,ܦMTtF+iFh ',ݚoS>(}\G87z-3;G=w>aaCs#H d i׺("ɑEF(""ED9H_$ oBL$ 80 _=߀m8x6At 0 fPEwiRܕv2IMHD !!Hg& = 2!RT*)g4T0'(xigM, ^8RfJ?z5W֍oTJS@1ZN2PƊV5mgyJ5ծ@Qjj\V+6o],,M>(/zś772WGvA3- ǪLAh3.w> LzHNGQ(ҢrW!0نl+07WB#AjK#GD$&GAA]wy0zC8U{r/~@C!>xx \(O1rAA! 0!p'u4O0@ &,c" hInt*z95эw)vBjw+M_]kc2Fݵ@XA̋Sa"! 4y :/?*}mn z͛X\f:i)*5Ki7[}Z`V:M#~Ri з8mi "}h :jzXW{߸vFz;Ύ c.ċpHb+n-GZ~Bzra(?G/ko9sc^[ lӴnCcz ([R3Śb.Ȁqf\-P53*I_o c }F`?& .S |_~m7#:C#x|uvvo6|sb{?^{#p8-_rc헴T.CH‚|!<e A')nø!LQ`cFq clfIa.~@C O p #%ILLtT;xhCGBv)-=cq&`\C ɨC>z & Y &3aؔ181a r"iHy+I#^Y*J<+4&2;YS6bPf]= ROYkLnVN.X^!"smbWiir?GS * HN|iÊ*.ażQpmɷrڭV'Ι; ţ047( Ga8DpʚY .h0ٷ0^} b0Na"L33^.[(/}nuE29岵Օ%8$ Wci#rM)iJGAÎIZ (MfIٔaĜLkfVUҤaX=x^3:4w\.:مx^p8  !A81*F18 cC~Iw\NO$0A q 0āIL q q pā\ 0ĆcS؀'iT&d2,*l9'F8,0M;s6z< $$$\ufi뷾͓'Ov/ >xnX8xF,*m R/ay_jB) m;k,Yroxx:Ƨ/W1Ô.c2cc'OX $s=7$IGFOVRbbVq'_,`9}Fõ2<5!;H;N !0An1͍(;3B|:D$$SnQeќOYGN<+ 0ޜ p S2.f.\OLaݮp|x "\$1l;#np0 ;u//>yS_I @ ѤO򶾑;gʱu83rWM$%"AI-i欽6-$FN ;Mآ{1zxg'p% ;a3KKIs[`<@lblYvW{F @\dt/ "=7v @,X cLR8ˆBp8 BP0B"!$IJF"g80ka| EFݾ TWAyWV=}pXr)7qK( - 0&($J:?ۺN IDAT̯?xnޒ%KE"!>̻UX} @:S, fD1Tyru H^>yLo69LBh pw455bH "2xLH /{/>*IƉٿXqCa xAG8ĩ*N;I8^l 0KD?y2 DŽzk}T*E ` _>c7$F7x DA<@ }7~dãW{z166Kr즯B '?:\B RE)j!.BRh36?Jm5r08:zb?==GٺbcP0K/}'sDm==;r8?,*:tDc^ሯr}iWrIxf<%o?=?_5q|~|}r֭)y鶸e߹ku֥ɑ|L[X}wϟ78#i/VIHX~zLV=v @ 0ώo<1VQQFggZsgͻv$y]wx90AH4L=顡!L= Wx@BQQQQb(Zy*?;<<<hW>ð_JR&ÿuկ=:2"1W5vMV]Ee*[g4["RF$m4miYɅM͜B|h/\)qum*9~COPU 7'~tգJzzLLd0A|O1/w5>p뒓P dQʍ$/Yxr?8|єFD ,P`O?"O,^,Z)I,?p8`쀰Xw2tҥ/_.СC2ldh(667:p^-FhݒB}[s\E}d8[LFJlcӽ(J^PVxhc[v?.bq5W~ФlJѸA<͜΀#_۲͛Iu.g w/r7oպ#{!\k֭;v{k6**ĥ zC+պ,íٴ^ڜ~Q*X?Y-UF bTzmmHiu=Ξ\5Q)5ԲMcĴ4jO<0?;}5m~?+(J$g*:?iw{~:rӔ5uw*9+S\]1|1`eY}#ueY8 aHE.EE}`(tgmofM^O1( G3W0,^3w)%1T&[TQ'~'?)۲e˯" XxAZC*tmm W$zLNmk[[[Cb4u{o>N;,v_ĩ,:ӐkF6wF緤Kiyۚ =Vp{qQZ7.HܻսYޚz,3+O7iKښVה*S]ܼo߾ ƴ@Ml߱sWS1m1< 9{Ob+'$$}_sƮX5i׿5?'{loN8qfEx*7?( C|Ob_`pRăov7[,a+ f0 #n]MʮrSO`^_\*۱:ݽ曜kOn&u[,2ل {(/\ZnU7V9LS-UU6T6S=Mfohk{Xi-/xTJB_liB_Mv/~v1,-X^Ϊݻo߾|04۹1VA .gz޶];+\VoDh^;lǎ7s'CXͪnm{R26y:⌄rYmN$Q:3觌C*tVy~*t*Soy})uLyN֠6t|,PuFC nX0hD  Z._^Xxy`8lz.fC!x S&1D9s?ᛒ,GYy0p\=w Sfhd4 && )I"B=O?x⤖+9BcHW(tYpvv_,yJA!,He^fr M  [%inc&:-vH+inHUxlNeN2IyomJiiiuHUmS䬳u9锳1mÑdiN2'GtpvzYZ9iom^" *}s?:gU!.ۭLJȜ3+T,?ҧQrv͋8[2`Pz6\Z,.ochz?bjCz rmpv:FP:C$oFM1.T Qemo0ݍkz[[z}:\\ɟ##SɇG +u bܹ@`hh/|AL&#j֖<vvo pĉHsx]_56q{w(z?FG׮]; pp$1%aK.v:u{oo~aR,::$IvpPN]B3S>9k=\=5"_CS1c 8-Dh*11ˠm2tPsXgYZ>ӦW/ xp"'d8O_ސ1$snr9W]I55RdcڔAB}OMc`9g ddmWp8?۳# 1 'O ;CJ$FhB8.d11szֿWỒ·/9$gx~Ŏ;dIAg6 /Apa]Q5fq  șʮ"[ז{),✖x6& d$^u+3? 'nɗՖ46ZLL^B^PRlP&ڬ*16Kv*-OGt+3t U_brݜr܂ǟnZ՘1",39L@Q0>TEL26ðr]^*alRvF_JQ4xPq1@G"&'Hp^9o} (R S *Sw^j?ejK GFGvXMRM)f̋s#cTJ%//=zt)I$ALsNGco&Aadtt…b|mu5a c$D" mI4*J2}}}uu$֛oJ׾w^{yKɒ{nBБGCTVtX,o`f`;sJIm˦4Y6nٵtvD t;KK4Mp %MAӎwy"3oX?'T)m}C45)֬ˠ6'芕*B-6;8)} h4kk%\vNzϮU oG%#M; _ܘ7-^ s\0]ԎkE&ʰʘEM@f kdJH 59Tޟɝdhb-;܀uZ~MN&f[﹟Z>-I@*2 ;zlխFKOԒ[nQTk׭{~Xc1)E~jmn뮸 zkjzzE /^c{쁇4=wV|GW=ؼ DZ?7!''Q`-߾&?_TR.ǥ3"Җ^OK1U RcqV:n{4+UA47ݚuu bxvЍw{|lefs? *}`]-ƵEf/ @0:sS-.J8j1;8n ϏWVcb23jdW7)??g>o|(oVَ#/ t01Ti%D>~,=] Sn]QnsK_ y˔E;lڃTh'R-VFuF{NG-^Me.M 2~rmnY<}XX{_smVO 2fxoNkϩ9vށxO C8]y#ɗN?;s|yD)w;dェ]?r⸋t ^೚m'8{u^LRJ9xY:D&* ќٱH-.ˋX63P3f-~vӤLuRSk}{m&MM^jz!R 脔J$IKV=L8&I2::: p'EؘG32"vEFܭmw%!BLFAW `q.Y^.(YTD"JSRnV ^qq ͝_a))˲?) ITttT (J%[_0 !A. 3v IDATr޴Aܿ2dڰN+?]29% ?X0**˫rW0˫,:veZB^X&WWJK9BZPYD =Uj~|UuӦ37VME+k8BZ\2y<( Voi.8\]3ar2Wel,-_%ŋoiwz[oX}((Uq)3Kʖ +ʋ#h+f閑ksƕAe@&QgfvVmxBJ]FvVXVR.7]ife)*J<+4˦I9yrS vUIA)O*p֭ĩ(RLM3R˳lڪ2;4:M-uhH荦5Rc((~i)ڢΘzU(˾aklswTtfZ$9r #lX{R9g_)1a(1r[ng:͝CϝsJP &1QZJ6Gí h|MeI$ݐɅ&SĮ)uzI1JL7(k'KYN&T75lZ^P4֏BhRqѩ y;'dJ4ԦWTuժeQw}xp8e~IA,fY2::J.0LJQscqG)3d<̴/3DIϑ cAVnL6DfA IrYǾ_ٳkQ {_D%s>'sw;N ls^A S{HI8{|o].,e :ab2K^=5mA :?ZgvR \_JJ09sR;cY7Mk4ʁv5%Sq SB ٦ɦCc'kZ_겾qiR\i;oKSshN޷ezuYe>$8M$ D"Ll+:?[2k@ @&oz ĵ/Cã* ݜ ?v:3wB;n|Vl۾/,JbNݾN01o;c G!C >wO|Y C 5 HB|i?0_az} q0 띗&!|㑇&~`bhD C >% qQ͏5˧='‹sq b(Kf^wA 6ղk?)wYwA @\נmZ,q,qт~'_8 ;kȊE.Ң˕(ȇ\^]X-k2[@Pd9۳)^v$Zo~qGv{x_qVRejyeYS_mFS ; Lv?ArJJ򒙖5k-qwKv)WּUo*z9JY\q@d}Ou5Nr)5m\mh˨WUTOR^ZochZA[ ul#EfJ NQUV%Z6.9xkvg{n7/Z;^/-jPMWo$݆uw&b׸hskz+6;t8buT._;65W~ФlJS4ϙZ ,Fn/kdV4lNmFcP'#<R VSԖ5oR$emT&ۀbl7ۼPU%eSvfjcw٣B9/';)Vzm3d_O]yK*O<&rr4{a\ṺZ꜊$:Ut ,V{BE fnυ_{qQZ7AڊTά\-,ݵ-Kj,r*۞:__Zxm}W|,3+OT*J"ؚAkYխmϷVUƩzL]tqsjvn[c=SP@MUjkz"ay:XW45Ip sw1;v6W5{o,qgݷo_S>tjzY svyvlNwﮱ\k{vHsYH!<pn+SQ:gc W\w7Jt^L)mvl*Q}yu;v5ivHZ_Q&4M;7}۷B1L6!b?<],rW441D9oYɮ}{?X}3= qBm~ "%++EqX{M][Tv|N. 8iڷ$ubM(tq֔}5\?TyZG˘I6/}A$jvcTC2q}"$1\PG2SU@t=}3c9d99 Jh 3um^f" dZm)ƠWs+#T`'鿀F4n5yyrNwH^O4#I%ҧ)9:5K#RۭLJ3'F*PDt2M3c)3+b*+)BWTS9GA $4xCV\6MK-v"\N93ǚ:5Of  c A3u+`6Qr$6+ɝ|eMd `얎U,۸us]Һ,.ɎǺdC2eKkke*Rch}ƊGP\W#&iBm0P{utuO|@; g8 ڑBPO"c\ PzO;{jEWMaXBxj,(sØb"ePs4Uf''mS~srWTYP{YSRQ,3eԎuu״ؽp~775 2NI(‰Jj?X# Ɗ%|DAD 5qz`< {j۝ Z~?GhkRQm]s5fkN&RJЗ4o[6=MP0@QլhnK̏ٯSpnB.O5%|\Tq;VTj&J2T$z6efn!Ā˸iXKTghҖ;g{38O Aӊ,uyHղ|}-EUie`+ܗѐپ*c7!'okv:AkQ g6Ty.+eWUh'q`؍:a-3LqRK IfϬV ,CpaTgRs@_KWSMqipXl`[Z"tY:nwM^Kٮ4#~57F훷u&|VKdWi&963ns8f&UޮY6`>q]zT$@oDӹ҃m,W>{figb? q!N7@ڰ`}}[ M_}|]- FIqNw9Y49"RO.QhSkXE+=e/+76v r]k"2L.,*h]^I`9ӥ zb\cr{bJHM^h]QC+5'ƚ-iP^Y=R7mT.q%-㻿Lo\EώuYښ=%M7dwѴRWXZZ^ZdN0QTMUVTYUQ}LrX,9 $T755 PU Tv8W./RW74ou@-13?j& 4%e)$ ӹY!dd\_1f?B+imNa*ѴaaC|/_|l;] s5+\|>斾K0X_״xfN{*w:?qC2M>ci8@ EVݫ^vMVeaW_}W"aݖzÏ~Ot4<5k=p-nTo5|=uU{~֮.X" Z݆uw&b׸hskz+6='R+k.2;I/,ٔui ]aEIV" np2:0CE=TM u}-&SPMiй1FSۼ1 ΂LwAM?r9B "ꚖT\Vq)΢:QjBT.^*6ȭ9`σ. z߯I9I9<'9Y#qγ{K=}7M{kݷI"nڲST.d~yFӁ;DD#_NwȘzK4]\(ܺ)Au\H%hb^$]֖˸G44p>D;|9yC]<#Y]?x_DOO̧U{>ό%-YgUq.}33ӤIΜ[cLyA{,e<V/:t21cOJWǟKv>5威z\I}_}?\Lٚ_ݻ?b*ex9+1&0㟇޷NI^,N׈DDbMIa2y׉)=q1NgSSO>{.MG6%v '"ԭ²|W{#϶0q ȣ|k+],X%?q(/f+H4U9ȉsr c{xgivB͝S8%'D$ujG -|>ﶔx, "N[6_rbѭx"zM"֟-l r"'-\P|TL[#'"ss-I3c>sXM@Re?0m4))dsߖR"Os;r)p #jٟ}'"JIYpODr{7{d3/$pI6]׆{pK%BC 5'.2޼/GF1LGPI982^:=b #"B~sRqCbB BjD±&d^շz+_mxf˫[ D[WNh꣙韝o`DZjS4!&ҙ OO4)X6+=l #"&0nίW&vjZ"2T:=q&}ZZpqˋz@}5!pS~yߴ]q^3`͡Er&W+pv{1D(xBU{.x!M-9;; j=2eB@$%"Zg'H-r߇bVN+cz+aGa)W氓wmޏ_;np!0Za'tvz""N DZ5E"2/<8 @}io~lBa+{Iy|0I'LVJbhgW$/8"pvDb z{.a'g/s㉈ $,'"3AS0Ŭĭq}_{rq N>rm4v/\bسL IDATC`b+׸-0s gEV9ntl=? >H}V{W.-^t1eݎ z6dhIY!N91I+Ջ-Zڰ0)6E/'^񉎙/'2)fW-ZՔ3΁1FIIQng^-~AJHWʺPM gynI<фe )+lK5Ԩi,k\)<Ųw]ew$%/sΝ;孟3N }e[hѢ7g/Q-sroťn>u?M_|%C'>:f~K :}$|5Fudtwwwuuuuu1DQ~c\mm.su,NnI"eﬞ=j]ŵ#ŘAD]7~?ٰӟYsLeAD|H?.v<&JSZ1!T&!B挟N,v>ĩ6mV"2 w29]de9̦o^w? "#(%S?@(PM5gg9vϲsޥf >p?]4z=*$P˜AD]7~?ٰӟ<Fќso R%GhY$Px/YvsAYn:|.4'kqo+[oV;ծ>`Um=DcTߚoҕ\Nq;k`]Wn;Fbz[I›K͟vpL5a< G^8ڕ/}[*.b'o MTt` z'"=?S:\k}svoqק|򊽤B4yTl)?u|z9;p8gx}'X[ `H+so.'DdlO䖼29c45bu淏M6+M/>냜Ux%EeUiW;UWWgnn1vXNV1VSSРT*HHVWWWDQvڅ }Y>}X/c4?G @rayDS >w;v,5!0XYYݾ}\džuU'bϣ9m%^_U/H}4J4 #ky̙*A{;2z!E{<805J ~+WEF Y8+ "J:="5ࡈfn"5;=VJS#^\jc\bJJ zPrVs=Fzhעhs3BKj5:x$Jn>6%$gO}pYX{|wƿ "[[L򵅍CW 4l;٧o. 8ͺY Wnܒnp9᷋9eDD#"2*Bʫ^+k~:2ڍry~g"W|"dԧGl/!䗝 "C+ݣ2oΌr'nӔj<0@k|p.6d eWirX؁,U&7nLQAQGōìL2#ᣄpꐳ~!`obcuo\$Ċ]+v\ NbgEPbJY߰WʜWTͫ>r_Fm+ UQjOFhfj)%uG 6F/ *#j>h4׽Eh"c##B5bUVU7f,hiG*oIU*Hf Luҫ|ӣ5<j}.H>! 8Sm, :d$"cEnV3sL6vدQ ,fNOHߥ{{Ѐ&82T3S̓hF M(ipW!HO.k,߲\;Qӑ?D~&3?>'v[ opq\gٻ"'Gψ]J [UFD %HZK.]vA K )'sF[ӯYoumcXXc׵{mi17 ۸[J^–mL;\˨6}Aٵ;“+l\JEEK>*ӷQN+3*%@^Wڻ]](UkI_Z%i3.NjAbƞG2V#"n}wRɥ}m~ AHUIQ'#ԟH.ky"?hGD袝iODZ?oZJDR/F/f±Y&!Dq5.</3At *E"\W>Z\ ][B^r K/LwMι%܆Ruyb /(DFwc:ַo6L>PT[V43nn^\4IAQA'BnOzGGۈEy:L@a9*Mf:KX?XW%Dy80ڸDQjz"-1፺RNE,WD v8[QU?1.w˚թ֦&4]2UeJYa_~yu=5)=t܄'wKCbi|)ι5ӷ+{Y1 !{Ojt'/=cv̚1iqGwdބeL=5v=}Yr/'pԘiyhjΞ6"N(B"f \\!B~ƋW56j-(/)7͎1szv6^Ν;888|yyyO it&M۷k233wmˁǎ3 ӧOtҎ;+D"z7޽ٳgs}KK W $Gw&hBrMxE2jo[j@h'3\,ݰ []?J$v|.Fݤ|O3Zؽw~\iZ%[6ŋkeeZVTyyy2v/h>}bKZVPtjg޽ ,`6D["ZmU(cjk5*/RSt]`A`.r;j2ɦ&ҭۏF+P3\} uK;\r@(~Çs< +%es`qqQ~,4f=M[PZL6--~'+hs$(_xlܬ@KfgurxҸ1*zjʆR0 ژɡ +g,.ʸھ@`_pR 8QN[\UP]H O|+N]9OKbǨ09U>mȅrV1<~ق@g1-eU3S7.n1#7|*HQy>Ӗ%F@&%@g xߓ 3"!Dz"Ѣ~8YIKq gh֓e'V>/ar*5˻PTaccoξPZeG'.vV!a0Н^pт_d|%n+23<&nAR=o62_8{gr?;@R*WO]!W9 OYe7FY[;+"ܣE-,>TnN=ܐaK/<~x^tzzĀjS<{@/r kΎkR[zviY>ZV+ Jom,emmux'*++kkku:TzYjX1z޶P[D{{{wDxhM.1q^ʊO?\hbikW}n\UB:b4;z#(pg w861EԔح 0kas2vmn)[x9_$d-ژrGn+OtTqE# 3R&{ƢlmLƎxV4#)H`nԂ!+:{tq1g'ĻoK9mۋ~1J@?aq6Y_<{ |'eQEmm|A5*/\?{~[1OdQ@5cjт[!+_?ĭJS1\^Rt†@ͶI'z'n\ ͟$gcTs\AVң wK﷿F!6c",~gB[YҋlM}έJ|y&x9&ke)Ui snQ!x]^揄 T(6?&i}y) Y\^ʌ$ߍi^ytFDijz9s6LaYZ4|Y~,ricp$)5/qG\ 94 m:^ rWd(=0:}[aig͎GKjSQl5v%9tCQra~ ;!#z({ tUJw{elXd'[cD"`Xlڵ>hٳw/sD]P>ޡco{:lQ7'a :~O=I<\-Wp`Ͳw2(_[*0#zۦe"ph@9?P'tNJ=Gۖsdr )OSG@,E`x Stל>Q]qS y~s&$,=?*yu#·El^RoѪ\~v;zj OW:Up~SV?,?qZ O)Wi9BR.W`+4!*@"eDTs8q{d BC\^\|Z1sfdd̙>Gy--u>VK0vVF0Ət08ܢB@5\pO݅-_t QJ`& gp2<.F~`YBBz襳n$"慖фYZ,B ^'D+|"&0< :ڽt:/H<=Y\ACޱf(b^ˡԭk7/v|U 6g9;DUNԬfE$#=Z3q-Zwzt>}+s[!)1ϟѭZ!rW}Fy5,`s.'[6/%OT'`bݜXvuhDzxpy<?vK[~n ۽L-Ӝ([{ILC8HQsT845u|A%G[+FٺBsnZw K[6Q(YiA1l랰r)8 !wX~X b9 onhO4=3}E/]~!*ܖv/R֏ھ74i9g2 ^eFRoU!+TRpk_KcX%k<,Uwz~h'y36"W3^NRRPs<8>-<֓mu<`k2r%Z]Z#XUͳ}] j9/]j wޟp_өgmd Z, *JiC˝Zc`[~h2{pBԭ{ں6bƷgN[=@-rs`57o@$j,:iuHTimݶ̮#?"`}JX͋;^D"8q_'&5\1Vg;(RqhzV.so/1 :h,˪X\hkh>jo懿yBƬIF"m7+60:)ec`R{dFg zvYuO8]XU0jD>]*i%lVɊ}fns̑t/61B !Ǔ‰[t~v|˽A|Cxs+-*$+GEdLocӏcRsJ d/n]e @,N0ymscSRK''1@xܴ38Õ!/VȊIM X:c<bč^2c/<"X9lNE79uF,ˈ uk5VVdŻrXy^dgVtDFͰX,f7XZzUV[Ȉۤ7 RXXuV BzzzN6M,Ԭ\R&;99 7776lwrr|饗XVV>S'NԴb ɴtR.'["Y9ˬMNV.Y@#Rz͚ @'?(iTu0q_|j1+7 _&"6bΝCг\g➽r>NNs4!˺$&(nތm~+2ڼ*d jH!B+|z)><ϿLL]K\v|(WSɯ6p Ic%n]{DQ`lo T1@CI}3) )tmD.QO56}j#ڼmұΝnkUrRR.W$]-EcMk]l1|#J=U"Isʪ.uE%Ps᝿9ƭBE]-gT 3wJ T^ڲj= ry*䍹A*@ͩMm?V^6[OWH\4؝rPGKyϹ¶a~(. x}lǾ.J,sLe}UaD7oGّ䔫uƢU4wW2yLQCl5=ؕR@B!1}}km{\AdA^ƒ#ל*񑽹;=ɡhSj9ZJQ23,/至TSqg7 3eqa_Jd9|]2b)2߽oO 9wsS]w݌bX5}Nn;áhkʑ1pߵ錾"/=z V7s_zF><*/Ed,;g8K<RԨ]<\w8snݙR;x`*K',Q 4&U%%>}zN~s_gH? iw\gA@D˿8Gg_Lfgϑokn.t':s~#pXǘ@=,!vƚ _9 IʦtmXqC}}{ b*s79H%Fj]mT &D3W4$t]0AqzGG{GʁB!ĸEGEXSU\̓o n1FJ]e`,{AjLJ*ֻ_}#oO=ڲtV4l>Qs齜c^>'pn;bsKv5y+lh4$6|۠1}#e:l.ۊ$."\'g]-m4h,3CPQ> `d5%h,SاN;TK- I* ;R$n}-nT֥$RӍol-RQSGAʁpmy=KGiv/S슞y!_>/?ӵMc.u݁Z·iϫ9*h J ŞVi䖭xwtut-?{uַoS|OrE3:*I@?ܻ|)ykBNʮi.۔C-_\r|᭿ ?J%Aё%]\z U uhl0y =`5"4{S]X* F@ 7JدP(n@CH WT|(++*>pvVt_ ?:}ܙ(::-xԞ9yE|iȗo}]Sc Vpeʳ7f=KjpFIg+b;^u``lxFH<ϕU\7ݼX.Eb6 nkAwv>8;vT}&oOߔyHcmپ@XyO6ɮ_}WS^aό?T}ԟyIk0Ʋ3eP2s݇@r?m*~W[nzWDz»~)4]d'WY.tl"@,Ҝ)ޚANξS9wusWPs}˚gH'r/\z2I|Ht,gɢs{?[M.aK' k{WnOZ܈ƛ^5J.$1MR gHDxoEՀ^35WZk[Nd+Gxh'ds{KF|3 AncCQԵ+  &Hתy _u0՝p'j>{;yxKNnXw;[*1ݙלּTbj2۟7-KUS{%ڎ:1r^56yMf3=U@.;~~pw1$T)+3ǫCº@uΌ| OZ ;=)'e%g1IqZ2%-j}]1-e$sIA4}q”2_ܙ锆X߱ 0ǖĤj7U3=͒@CIΊl)\[ sPFe,s{.q7y$uWT\o;)۷bvDJgl奩K @ϤIpp|qa M.dL\yIyxk;?<ű )ϧ;cv3B|@&$us-r0Q.7]['rNve.<&ϩ-\} |kԸzKn`6Igd=b`cÕC;gl8Ǐ=PW+1x-n]d>˳uכ*!#4{? K:w!FEk~|˖?XWgTe5_'˶`WŀQɲMU?y=x]]C/OEDsh篬==c"g!R7q؈re v9b.zFPe4uRq70#4I$`{D?})Ii_nԸA#nN9U)W ̟4[e_i#$$ ȦlY⏋[nJ>gB1ŖU=P}h~dZ浡=+vqӷϓ?<d Bjm~& ٠y=B<p.-qm9a+M#9iq΄S Vݼ_nL,Hlj|[2^w%Hb?J>wXy*zm]vBcy#c]Wo?'rȡLUˡꝥzW=|Ubvۓ*0>};|zm]Z6=g0]ʫ> '?uUoˮ_κH$75SBC`{Fd?!Akr/Ouktʹҩy<èɽ&Ʌ|n5WzK)íE*qOV]0%:{SnԼq3^nk|}1MG[2M\q[>q}kXSMtu{[gP_[wb7PO&Bk_{M\O -^\܋fx%)/7̰uX$ dοq/Yw:_Wi%{XvmgL C9x\8Lj S#m[Y[bp n{oL|k;~@?˪~I:sԪZ>~кԤ[JtwYqi!@97g7V"< ~~ U7Ii[Ǣx]OR7䎏mNb4+zH?nJd#Trx#+LKog\4o_{g^_G:L>MFv=%>yaJ1>2 Ň P#3F/)F, {(d2{EGP2퐆Nbj+/{x7-퐚> Vb9U(yHrKu\y3>oItn}s1WlR0Rl-6&RPZ9"AtjUj>UTIҙ^-7m:xO-vQFN1æȊmz ƽ6:צ _LX7z`8qƷf8GgO-;cz)ya=ؖOLg9PwBmۺ\Ou賈Pr00"Q=8ݓ왋{pZf"hh4- ;cig$k}.ɥOXɁXcvzk}[Ž >'@]|z4_;: E-9\1fo+59&;K?20&gI_D[kɤGH(jj |F[\}2yЫnhMuؼyc(B!_$y?9P_jEh8y=rDu=\GGD/n1uD;늾Uq^nS}yXX /.W)z79:dc7G˳(##̞o:qG:6Ϧt+9cf!x3+,d3 ^[eeQ_V±^|f\e@I䩇:"{֋UZ-n#Гa͇XZlbF4iY0_~E#o+YeX~E\f xpqلݑK6nؖ(,18M_]VɿJ9'^d !^udc?* JJfJrrr 9&Y)\e` چ\}e]NJ\\\ܿ2ՀBWm[L @ӷ_UUg?/iG"'#3O)†CÔ3R=Caq*U5}CT7 Y}ÙJvrI 0рon^ϵAAa*/J,d͘yN ΨS#Yy*1vY2`(;sIL)^ZQpyek/ 0jSoXNe/;s}XJMkPo)mjRW:_qr~YfؕW\쭿ܛsu K\(qߟ,yíli0\_\+An&ˇٳKųeFK__ @'v;+sP5tbA=ʾhR_5_gK{n^SٖFQbgq-WnYr`yׇr+]@^tp'Bi:9"[7@ss$#Go`䲶+sputg n-DEJNbuO(GAߊ{C Pv82lWiCTf. RQE'5ZI^@ ŦK 0iDW঻%%\K\9*g27#ǥ)/υqiKU b&L~i&,.K0eмyLE&a(0gԢ<Њ ^mLFg AOP*2oKq1˕1/\z3=ZWY$uV`}_JJ tυ1/dJPtS̖<ܾ ~B-Vs33k}]8)׿uaojO (Y-0C<'tqrK7\I IDATr%;S^S  {uz˺=b/-8|]=%uӳKq]OmۺwMGA9?m==ol~}='MkתeD&=o<#쳲't7]\R/[a۔B!*`mύ~'ڧi˧+m>bci{"PU={u ^v܈iYjY@XΑYkG;lJ('uٳxŴ3^|޿5>.Vb1fyPZZ:x%? $ޟOnڪ1 wzצ; !bX,d0z}SS߯=mr*Byݮ]:88ۋD"P(޸+=G^2kv %?E>V)١d꜏7dW==yK^b8zȠgEM{RlѸKU?qjB !BCvO{JI_O|;3G}Fֈ۾ϸYKuDoOY…#G%j¤7B!ߞ7\W/O_DtytqMl=H7>#l~qI=oA>B !45`%Oe~M &ޏO]+vi-)O-}j:Lڷ:f饻~3uKO>s$gJ8xMO&B!q+}^u/?'7g s}7ҧ5/|pTuB17,xINZg,[Es|>ɠwƂ#k7-l}\Pֆs#B<\*B&ֶB͝۴kFѓr(^VHܴ ޝn[Mѧ}]Tg yKS7{(2!Gc;[ k,9v+;䍹}WM S\dܴ ߤYٻ&o`&I@(*hVhԯ]W+vv]j=m=Z]u Z [b(<51@`|=?If> ;/>YU5}}>DDͪ//Y?C6|͎:3R"hڧ; XiO qe}r$Y; ۯ=4D~SNjDZwTf:ކ+w>g;-J쎴ӎ/4կ׈X^c+>,Ͼo""jK9>~#Ck6/]_~vZX?O3^w֚ v|r~ƭ 7J5"%c4~+6̝=J25t.\3׻NZ߮li5\r_+:ibs7n]f<}ǯY]Ԏ#W۸uM/HJN3ތSzyY.v5|cw?Yk(qpAHvlΆywtb۷&k%}cr3~w^ڸ]U5XvOeڞm*K[qVX%QO8)}iuc oz@J4lk;߰v MkW""ADDG*-S>D}K|E^n]:VP~9wNnfoMbho{"r>Yc ^a~p:kkaړ_m9 ãTPKDMeYŢٛ"ٸMP0D w W nk곕#Y"`1KtX@"bB=k/ᯪ8aF?ySK{R +y"j4sabGaka{"%Kň|{_3}DmIFd/Zn <޾ {Ȏe;?V^IM;f&,F+}fޞsf5Pk07nDz}Xx %.p5sܚ+VxIq˦S{}#"ps$"rƾ#"tzjua2"זv^I=xkR ;Hg;_@葚 v|rBd/#GglO{榶Jd"=>8uSgMZű܊#bh40һG~ߓhϾ] Keuҷ>bP[ЛkjbW8~[嶵ď2rFN'Zq>{KnmVP+*soz_E%_b"b% fDD%l 4D$yNَ٘/0\MN+0/zk9FDEgpQ~괚'Zՙ ~\"";81a'NDwɅ:(^ѵU0_ pDD"o=#ktM2N/n ;扈|!5kk᪒Wlꢎ> {>X% pPJX Ʊ\Bdv6PCf=GǓ%Y D|tV%O*4>lyGၟ!a}*ӳK/3 Gŧk/?]a%{)8UЃ,k4Q^ϓSl[iG|p~}iq/#]o8ѳ_G:zͻ{jI/( Av7f~U4^_?ңom-//|bg9%,a} ;d7:::d/T|LIܧD,{#>>MH?i>dZ\]cm۶fb5͝[k}>04y @sRdio9>}1K>dkNp<˄%|Ì7|<#fo-XL< 5ƒQ6l[:m%k;:?Uë68G---fl6L&XQQڥfZZZPOGyh0z_ 鹵r}ruozw<~'C]m[IW_;)jу%^1hH$ v!x lvvvvvv@7Ɂvvvp+6Z4!C2`O.H "*#a( yȁ=K큷Je&?h^@eǾ-a@@TٷY"婰? ;>ZXeM L@Oi>[ ;ك"S)8; W-80b=O&E@,ٓq?}pdrpө ZIowl*4EIuEN JǟRU_5wʣi[Rٻ‹OIS^'hj=ϝpvф%o>= d_l➿vӝW8"b1aZ/R`"ЧCݸuOӵO^_Qy꫟t.ON>-n B tZxūWrw%|եX{'sGvc⦅9+A^}꜓9%s'ޭOޓzg\SUOw2%.?3L{5~3>t^Eܓ)j qqQn9+eX/8R'?ygi\|JjMo>+>m+_.%XS>Ṋq\l_Pu[࿸ծQptHHHHc/Z >B!D[bJT;@}r 2y{(X?(@$k9xOddX@"^՞$&9P_CONAnER 4yu%uc#eŇr-9Ox `ĉܧ E)&""I䉋P+ꋏ$t#8+h$I;h"%Bo]+bd ѕ@qk|KsK?~~a, k%9쭏ZF񈶥%|Ԛ&W|t:-8mw*RGD$ bT-e"Oّ4rU_{#o-8m̶: ށb]:wbaY&"򞗚]ˈHົ4@jmh$tW괅Vfi:* vJN^c5 dykjSBBRcIS>;}[k* α@i;>[ss0;0;}[kޒBȤ1jKӖƯݴes2#$"Y2Te"*KTZd˄NiDl]@i>@ƂM m!P0e@)iJ2SRT__%h}Av"qΜB""O!':?3=9D:Y(~-ȝX1kuKWEwe\X+aBLWHLߝ]Iݯؕ! ZcoJXEPzH[+)L+2"cD$ feʒ3ձV`8#9Wfo4}ړ蘘uvg%׉䕛  vmmt Tם IcA5DD~q[7*}4|N^vlM1 8:Z*%p{eNѓ"Mwjc.D$ PFH3K3ZcY筈%D,X@DDwx:mKqMcvwׇHkn)]>ϏʒVa3[]1a2BںLTزi/UDͰA]vZ gmڪiFvOeeҶImtU6U_eS ';E9z6MaiM`|y<>h -Lo蘘.B!nWBBJ% $VC[ i 2`yk KzZ@vx#go8G(-T _ned}\=QSzoLГP#ه>xa2se|ɧlZk@U34 QF+>hRfI\!42yY n](ݻ+ J觌tWo2)V\07mmmvN r!qGʦsysƞ![ȁ{*Zݞcx<Ф3Z:l9 [SFuBm 2ҨUeedin\7/D: kjLFӥ믌؝t[,M;kjNdden_Ji<@m8c]glǕS y{sDv֦huT9΁VK,u- z0j˕_Itpw沛`iZ%dz?3N[[[acmDžK#x7۠P?3}%hY[Ʀ=@fy [{Jwƻ uRyIh5K'Ҥ,\=:mU-w_z0g?D)7Zҥc 1 ם OFaY0L{Dz -ct(hcCdgcckcڲw,GS1kL;>뭵́BHCDR9;nGbBh+MX4 '[[elmZ0/ $[6{WiBG@ZUiy2z6'&T#; ! IDATd$'5iUҳLDqXj5D%}&wht2kZNMKMSާ:E+kK-1PFA[[["b򳝝e(.99AERklЛ)B: 7NtpY֧wʃ}I&"]fbvX.EHtuUV{bjK7_4g%Վ^vjJ - $(:ȤZ.7Kj ή4Qn=Cy |2egxz<c_nӒG{!Q5%|H(=>F%L6'ECd^956{2rpeL.MOVVL>7GN:+k[w`ȟ]GBص˳THteھ'޹u᪌N@4ZqglʂyQJL hDb{%la0|:lf]\l:6F%CCCzԻח}>(<}PɺkHPn86Z`Qgo^2`#<}\yssX; øek5J?֠ ݄h?ı.ȁpb"|!t[ ȁ;@9(@ r r ]ھx%_s_{/~/۟}#"oZxGniG#'z""丙q![>y)BB];И9%)N9}]ھș#ZyEƿ2,r'/>+ube!㖽 &jrK'a=bc;@JFҁz)/|?m{zJ(hyn#b"9Q?%nP~dϑzq83qGӎ/?3ԉ2=9j"Q!>^ٿܺVgo_P5X_p^K:nbA#&++V)ƍ 1W?w-0'*ɈuPHvX6۱ݡ_(Wsq2\C=o_G$q\uvҖ#)/Otp @wB9}d2K|輖8VOD/^u=Ob""CDX])Ln8cu #_-7dpbPޛYLLdjDbjm[Ƞ!Б餽'c1u'D'D=)g:{ -RH:hl@mZMD֮Un]uF"O؉/߆}GU>YDBQ/oJ2g-m49xZ]>לo$aFxPӥ*5Q2 U5RSwu[gcL*OݗUUT8r' \79 "I$D촗W0Seeg S'?h*qWYΎTBc?j/?v7O&T?\nqWWjU^39(hvFG~Ql`?Gr304_}f}7]Ω0}ݙ rPD驳\J'6Q^n Iܼjh`Un~WЁ""7Sslݘ"" (%%9sxbx*W ~bK 28(JW2?RȫU?֦BU˷<JDDFCi uMD$q0DH>iչͭo3wXD$tXL$QMSCс!"F(b'b&5e?G윇L8C>}j h(?(聜˪r_kln'E[HҶo&rȓ~/lz vNM t_wYBM <)"CCA.!j_8iԗ9?+h(0,x!aKJAt}BUb&AO8o@#:rʺpbKF"EWodO$j9bV7XΓh&"T]x,>x+o:=ۿ>['"q&vJw1Ju ^]j-3hZBpԕ w(T2b^Wޤ<˘〱3c#|칋w3EuuuZaNʁ4soD$;-n@{Ï~zz3W=WЁ U+/t>e޳<1næ2G=YsGꊌDd7`_sr>]n$s_Hd%{ywg"88i(١OT?w!OƿiS=(~18SJvf\ F";vO%X*wpQmW:͖)I:eoрWY1i[#bX.to܉OU5_ܵ%q̛Yfi IWzg~eiiE/Ǹ1-]s%mYi֬YfY܀SgG[چ& OC?~戈 q/ۡz\8?\ֿ6<52%c,":;3*=ǕHcƸD0xl Svu#Y##mcxS'~8W"GZZ]EmI9yѓr7h@/`O惗O>d[Ɔ孾A""FQ߁}4զU7'KH&s]EH wy|\'lHODAHl ut&"Sh3 @&%"ؓi)7ӵ ܗNx??؜3~CvTUP m֦/jjqf}x1D٦Ày8a Eݞ:=3"+c+zD,ö2/ #vw׬I(3Vb-ٴחmyrzר'7">6U&o}w0w1Oe59PQ[ĎMĒ*o6HRIƖ%Y}l.ueZ:vYɹ?E]*}ėƢ&:3\3oS?&.U-RO1vE)| /:i+h?&l%SKY_;KDRohЙۣ@WH!.wvv<8KZɦr }0 Y{"4fpd,&}]{Fkb]E+^ߨ''"zFڋb*koy]LMebWߏi]_ߖ3F5II> WJXQ[~ # ":,a8위Tdn`tMDd3&"5t`A9GvHԖ{\u1uw19x??]Q3q3!K9"jx :eerDr+Ld@pDUs{;N~#"{~n6R ۠y'"sS_}vE?Z }F.*5@t?i(tC>? bQ?/ا J=]lYX7խV.U-#jkŽ麼-Kj눈X/7x@]4_03ߚ/%-eûI<=<ُ͗y%*i{oH=-۰Q/<:Lxu6ioD)NaGݵw&"yЄև?흖틱7L҆S8X}дG|PG&5p(oؠz!IO:Wߚ$%zrD$pe?bV_OoT"ҿT9ff $ ?|""8HD^$/ڪa5󯖻bZF^'NDrǫ_:[^)WdFlxD^.Pj"#blMfxEW7 @??nG?ż`}+èmEM1Ay׳Q]͟}-qs7WaOvOUztk,⩛.lh!\[nyP n̳νwu+> c~P^o @$p `Z.[po[Mw'^'4iP BXc B9a.]<*@ѥg}UG---fl6L&XQQw*iȸ?̌y‡/XTwI1G[TŻ bW~6fl6766zzzh b=mΡ䍑---h2FcEEEhh(*@ u>ZZ#2ef 5@̐7@^GOS9:e(U%C0pE%Ƈ#jU윔J"-=#"ZmݺʌFu.82|$(%@!+0'*1CC䭌#2eά}>Pf*‡^u S*/nǾM m!`] tY+WgbhU MtIY:"oeB0#`|,u粨w)'/ЍGV VoJטH3{h/_a2̜|<$LmH H%ymKTgl9D u\-:}]N{4MJLc3TԵIXWSnhd*R7dA- ^yi5RhIm-Tu$,bVKl Q[a'e%ZIBVui ߖS$)3 j I0jj5i -Y2<#mb65KsT0)Smb,>1=^NW.LUG5+zf^]esjdג$ v X+KJ]+#mFmU BVnN$:99AW]sQh1Vczm^[!!FTk$]EUxפAS9;bMT2@ 2m; /@|?#uGmm~ {wL^8761b<9t qmUqRSޘ9(  s2 j6O'ߨΏSSWl2 5oP]fPڲoZ2[kؔU[I}}թ)r&5W%k ͠\˯bݠs)#fibvÆxuW !%Y ҆ËC.TkPGvCne!6UI) rbJ{ E0[^[ ZnC(~ce~$!qVSjHRS#^[Y'RSVi(J.OX(%rhYKv)HTgtJr|ߺcCv jԾk秬j(.mXq[^e@J2iEqDkJLU)Y*yCչ)>q_XcVzeۮ/Lͨ0b@`/cT99i*]r^z otcU#nJ_5k9" \2+n1,nktcUE3\)M;lh,9kT:As UF8Q[eZv]I @<'hj֮VKD7ZHejkoK0#VXgvR(VOp0*29'AJuO3D8KUɛ?/s]$`I920kMyX;t͕mK3ű 0V՚hcE99K\XC8ދ|'Ce :iLz jJt jm~ıYy "˕ʵ}R]0l$Egi訁EE[6o)ωF"p0ו!*9.ZC+LҤ9sR˳4lRM@gI,Q[s%61ziӎރV55{Cynjs4IM\:05f>Zwהmޑ;%;kbҬޅa4yU}kwΙR^_dΜ%|9sRոV>LK&d-Obn67}_7no/js72@bآj|e^/pԖ]Ԛ)y}!zpX`360dF5hpMyG펪.JY蛙fZfl6|n(ڠL;kRūI0NVK_uiқn5(&~hL#f )wZH"_r됚*!_"SG3`;@'JPc ^⪁OfﵹW<*^M8E\oľ _$rU<9JVʑ -LUF_XYjʯ,I`(,4h+>q zRUPAڲСg)ekR:;e] oyrqmAZf~m[MٙE&JmwR`\Sf$Qm[t~SZF8cVj*,PG ik UE`ev4) TUXZyzT Rֺ ("+/Ӯ+RRuzSUވ֩AR؍lOS~߼gZh㧻CqE fq\,2bd@yv]i@:  Zfѕ^c֎D%a؇74"l&;ݛ[yb.\ov|?AlkHB\`aVCQ)JZk(IV ~+CIkU M,86 HI,ఛl[7yt-¬!_02`1i8@)6o`Pyi 5HYTLFhbHfTCyEɋX/j+ C9LrMfNLm!Gm9r "KWך$ҍe%:&jk)Df^5[ WV2~<=@7g*XBRX"SVmaMecj45%%(cE֯O(eGkV(~s~钬e%Uy $W_S!7a׬2Bp߬-5R,N+3d߸TWWoذaƄ<Ԝ7:+npŸ>@l;m=hd׊g#D 0V fm٨iҖlsoR]#-11JA$7ǵqNeyBo##y(Yt q1kK Wf5eSsE)epPf{wcʗŽ‚ࡩ4o@4 Q}.ߑmp7(Fu0證7X%ZAڻ ٛnzWFN%1֦iH\k8펾1vuOmZ@J&?)@o]@d)9v}3vlNnmTdrBgVIƨa/2qEF*%5xw~'z=a6#"2l5< @FAL՞o9?hhcEqdM!:B=`͡Ӛbn3bKm9S9ֆfEFߌVU4TE:%_%鋸[%>YP#R[k miW}LzyqVb͆-K2=mjQ2 bU tFHGEiW%^u4Th մ2VFL8:kҠyV}Dњ[ZOQR]kJꢵ~Az6Yv OQc#D*x]EEcJmњboY@t&::#R-FiZez[V>fѩEr LnȮRhTPgQJJ$R hRZsMͺPXlm3KL,(TbkNvǭ)\WZ\l(RBLGm0GZ6o>5@ 8bѠkI?Ʉ1~N@C #~4ZǠ""VjD VUPqXU;PҐ:%W 4%zioNN `J STERR$XE[o)4jد$$ʤ(iWv*:D ;!7V]"%4*/eن:"(W|K5q$mkAɕ@7VTXȘUqwn/6䊷iSͦ*D%}'Ca6@1 1$@sqnQ}`Zf]vZf/$Ž~5mnش)w]ՠYh%#P^c:Y Vn*.(7Y憪Mz'_i^vCl-Pa䘫oeySv~Qh]YSH`eU f(;פV&; %&FIsqV^dsS-˨0PQI fSÎr5cғem*l57-ne_njR5f:|b@ds!+nV-TS@E@ ~衫ࡹ|}h+=CEb'G1 tqJֈ5eC+Wc"%T3:HuVΐ^QjeK*bbb֨737;ܸʐVfԮ[V[-kQ@ERmd4nJIi%`3댌z햵Lb"7($`YRJ1y#HK.mV_bIULR`ժ,u"37ofoٌMkHd}*D1'm\f,mq:&RFі^{ʖo\U/*2FD+)ڤ3@*'AYS tu~"FͤmzmQ-bHץeרb|doɸ5+L* (37$Ԯ_Rd"Qo0( kcn2[[P3~bHT f49, U'ʯټ63%[*~햜$9$M])m&3E [ץdT&%lؚF~Duo( 3VDinIQٜmHHW\glyCRP\D6a**)VNՠEt 1LezjUr+Jɳ՛KD"&1=kUTLMEZ U\[pљ[EWV z )OJ_ܟLټ j:m3U̪Y9eKͪ04봔*"3W5m(:`K+1%%:DNHZ{G{#)Eڲz}Cq8 E2ZeؒRmN Rӓo-`Juł IDATIaIL63qVIE:NkRY}[TWY(ʆEL|Jjz'e,PؼcJ*jz.H$-1ˆck_=MQ! T%']g_ILfQuMt $nN̽ƭW$oܪC%H$PS cS}I}TZ (}ω5FwHVǠV@ ָ71D bݑuPAرcw4@ UPR|d(tcI [9"(t @ b$ w974};OPX]g>>8~!Sq}]܏mRl>m@a1hەy3]Eɛe܉-%.̛Ao3;);>8yAev}9@ #L<'V'ISB:p`z4.=ңO$ĔqC]]'?wQO\@%|)/z,pgv9Up,$ȈC -,ЮPPECfN/~,7h @  U[uZ =QD:^@<7A"fs6S7@(5e,j^@ G.Z&ݰrXM+mmpԖT5@48Z0=kfm(덜ӟcχh>/-iA΅6aiU$Ώx@m pXq}~ʹm~Ejp$s $n{^ԲG bH$B^C.C !אw(emU%Z}!%踔ducѺ ز95rdf<^\GnxWnb52y qzmzח'6n+Qb(avω,bp8nE^C.C !!!!FkWJ7zxD /^~y @^C.Cܝ^"37W©"o!wG%do(ry y y 1Z^ @/ @.C @ @ ,H"@ ҁ@ @:@ @ ? PYG߅gtk]xVV@א{4@u@ @ q'Ay>+od@ t .<d@ ҁ@>ř`CipA< cB4/^e{\%[WpH?2q|I#x;ES+YD U8Iݐiz8O~q>s0lvw.?g6>lQNRd6贶$,+ b=l2:(7c>}lF@:r6᧪CL6~Axlo22zs{9mqNx\V?C,ܓ_y*Nn*T }+-5~:CCbDN2 `ށ2W}8q p wC_RԘPdׁ<jt bt@) IA[t3B?/_^iw8K$ݻ##BDK. q hvN+_˭8GNRR$t{_< _F=mp30M 7^am(I_^) ex0ep#.cn1x=q^ ^pz* }u 1DBsCl=J{4?pxpk_~\)#m=4''F؁c-k}bkpÞ@s#wb*Bqtr};MvaS=)N3HP!J?"e ز>D q}p#)qݝqqb,xpen7t?"PۊQ9Ĩ Bd378M34E x<%wqq,R= >I9](̍0=u1MPdWW0;o.%?$ŸCǎ@ODǍx";ndGR-aZh?䅽n#E"]C;LK̂@j>au 3g ;pzosBM0᎘8'Z _)gQ<'|Ǒ`Yl~~ӧҥK4b pq{I˦ I 8 b@8ˠp^.|m{Թ<`0-B>H] -mKwNAaLt?U£q!A]ǹo+q|`p,X$FA GqavbpB̊8J<7%XP%G9<rw7p Wr,-ߺD&@ yzwz;q)='D?+ߟ$I |eZ;{'KKrNX'[Z.sKYϾjApp0aAAAݽv69r>kkV0`ܬ# $40E/^aoq}~t~#0}dr7y<뭬z?۵woNػ]cOH8pcܞ!A3]{:M)Ϲ\pНÞ BA Ǚ>38q) ?8>vgY@n yhT 'ռ]L ((8[׋x;Q. D.d➿y< .ñ>wVߢ㯆|Yz4|ixĖӟ]8bnw1eY pC.]]][F*n)}O>ŋ+Yv-Ms@Ќ8i a[as~) er*GDL=GδE~pe ߞAuWE[CCߨ}c-~tnޙN78&ɁDxF'n||e44!e!JO4Uh?@#p=T1xw*+ue9o6V}#a`qn'P%x=n(!v#x(cYe1eyCFzP}?i>a8qgHOLڞ+Ʈx'ۿ9~f@Ľ׋qH(|䡇DL}紣 :L'>M|'n芼M*?`?IQD !@oi4Z8*pwWkY?9O:7'D<(x<;Wya8]ގ.:]^}苿=V*v}k@I#Gྉ!r&sW;al6 EK4@7p"Ch얆)gL÷v|}i ­sI6Aa p [ gkO8NC <`2n/vvu:s8N`ac8`пD?q Dn{Y(7.0/pb##@A7]]~}a+H #Z)F!p (|y6k}(H<ͭ l! X0wUG ӑAp]E"^kðnw?^Gfo2f#s |i&.6FtA>Ӛk`ߥ}~g?}f38|׷boݓ:>uj{ҧv=ud~SxWr;^I`%l r<fsYymO>38x?JJLpke\ck%˅K$ \_PckȈCcDrs s?`ox|0Ý_v͝Lb DT`@D$Iy~Yhp8XZzzzd2`GϿW-{M_l\e ߚ.wv ea=7+a$8 \  ,K|ǚy&+{ |bda8_)Rt;֢|Gg??6cjg<pAd˧M %Y]ԯR1 S+L`󣛭;55یb/~ ӳH1ý^ɰNxn80Kz8 p+M@)1ncM<bsJx#9W)O|LEqn|܌???>7'(mecMgWE&n~]^ nYږvcK贱LK;<Е9}Uೳ?d~0ZjuX֜VC7u}9~NK(O/BѶU_6yufg(E zOZ3=?c-N@Ȱ#实]rwt3]s^šSx @:]6GÚNvJ2RX/w>5z йz_˼Cy=ݿ:s]~ofػ=ݟ4@m D;N7v|DϏ;{ IDATs}2#QWv}$ޗ 4&ͽ0@XD=|8^0&x ȩG>_ ##]gvOï.? tHHq%HH(/6K}~`%t 4cƄ?O.b ùva{(NbtBB>F0!wVW|DmjЗ#=E1oރgMYfq_M(~lNH衻1 RfYf.SX}HQ?2ylvδ{f{1O_=gf6yUGo<{Ԏ˭&'sq>#Z0|ދ-M>B CNw :"呏-FV܌ 5x󃑾H+XXߟ/Lu^R^߂żzɋ/vxx8p@{iJB?7jPo a ?; /YT* }hCmeY6aY0 yyxтOϵu|xP,qM ,z[ 7ytH"n~y3Cwp`w<}3WqqcUٶ ,_Ta~;DvM?lpfaӓGQk!#'$~(qǻʄjugOPm$)yA!q:[}su|C }0Z}d`1G)AS„acNJtb>%ͯs#Ss^/tˌ엞x=@:]>'R |>vܵłЮ+zxتgN o^?<1'̹si8ȃ;ݳD<j^vT<73J3إȅ~95_)/m"QM%|'qtv]p3#I]N߹}~E4Sce/91f&ns8H8?Rt  EH4F, 7=Z?fLػ%>A2{v)?Ժ /eцB|  7Fa厕,5U-R]o7vLSM 'ncO?4q_b|!yp;_&+¿s?e?l5¼Kуo#ޞ6_g'&>{mOyQ{}90gÆ9'"@leP:]P0Өܗp][,9zڝ.Ѭߍy\>o՜`~M s g.0c$ۇeBڏ1s _rO659C:Cȱ>7 ڿ<`q؞KKBBpcฐAnf&r.ٱbii=ҧOⷚ G91бޱS'8NxAhu;_j/6ْ ÀfݏUյ1 &u`mʤ}{Felzd8Cx?3_$pqNcA3M0<s_$ ҁr@pw5˒~,>O(ek#qW=C%3woB?!`<` o6+XK/iZa pvV\Do@ ZXarY4?,@(X> vӉ;fQtd6R1/r<&[Z[; .֘]{ M ÝWNr0Ż>w "hlnnV46*U*ssAZZZviJ7ma) zjP6 z}}}}/,,,f͚ŵhUǩ? |i@ Äo?>C/t^e<^~ɿUdoSPt6T)5v Qv'λ0˲Q:ʩUs}_x["_WO[;u}S>{Z :X5ǁ,P#)* x[_/:&r4Kv}dll EǤjuuГmt7}||MZ[[u:NjZ֠EQ7$jz=ԙ(lT4߿7nhZNgZVz#eDW+mZ1c^RͺĥJXKu\IM&F&JsңCǿg!.էfA˥6ȍ&U}Xůݮ\ɧ]; `>J}! l%ךlm Gsϸdɼ[~!jYlE}7wB2|2wyyե?0Q߶sT]Rd[Ԝg2Yl\IA/{?ݐ"JL&EQQѪ-pbQTHm{t&hP9ѿhX/Oxk'7;}3!ks0/|ăv'۲Xb`+J]ge:-X›v{_z0zN>OxsFCgMvpWך?VwRvI"RN\u/ճP nP{&#>8vi±  prFg!m78.h4ܼ!=qnmEG%G[n6T>$8.~nŷst:C۪b籯S/Z3Y W/z+mchRxP‚!<kI:x !CS7Vt֭|//^xU&l]}1UK@ d:ڙK;||ԧw]ˢwPpݳpZ:0&(#'zXYjHHmg׬504z[Io\.u65ƆYlV9fIGsxVVmݥ֮ݱcǖ-[K^>}g1ʕ+548p ??o*+-}o2F>!rvfY[cvVٵ}m? Y,kz 7f:;S]]}.]\9mHg~.4Ng` غ%iyΌ=eFV{g#tuO0^sVJYOc gUQ58 t~'5,Ŕ5obSk]F}%p9#AB'utGLa9G ?L; piZ|>IH(,-KJJT*UsssqqLM1}ĉ***ƎhWTZ^.,JΞ5V1rEMu <쳯JssW^U6+?b;f4Y,j)|{@V+G7xȐ\Ȩyb0 !߫ᄆ=+ٕUud?vVjt~Fppws4 /h.>,8DmèFFN>ٙ<ǽXw?bAN϶4]kix/Dӑ:)hf.F]-s~Ϝ1_n>O${j=lA{5;Q~^^nܹmnC,uyowww<0A` kgppoK3:>1K._4 /B&3]ym{.UkG>d'dG @C wz ՕvQR+J~;wV*z\Mk_}eafGUeAYXhZZH| ?s?eʔC:(@tl6{`ai1??:aT4tIkǿw?zjJ3g])h~|?FǶwtzEQt:`n0u:=l6͕`0|J}BF%jFV2kgg,4 TRVķ3fJhz={tzDDīǕWO6Q(ds8\.w%N'HorŊZXHrȐ![!GFFTC?x:;;cGV\tWۣ^[̻?i4F4hܨ/\'=b;2;W`ii Ksj,&˒g9#D3?ٶNJݖnsg {'|Fea2m]>boIKuګAGw* z]x t-:BU5puq2E5h=-f 7djfC aA! b#5rlE΢̝_&p1PfQV&nݖKf@?s3Ns+g/r4Z&YQ%NINK<;ªSsG8X>A%X?;B  :Pos|E 7o)knn÷ַ{_v(80֭1u*￙_cy !gؗz&a0KMvS~9{y&,]ԇ_1" G ƀ-i4A,`0ܺu`y<1VVVL&NSeaiF h@c04Ғ?p52Nh<}`7 ||kk'F4jz+X:ہ%1C ՠSF{<܁m+=:%.~YVw>|!%mp}<[JMVVF1n}ʥwy֍'?=_-zTW{c9s Ng1=_}^}Sz6حY`jiM]ώ>X`q3̛5z̀/N1[) I1 s6{E?#EӉ_Ζ^[#h\n/8r6ا!eZ[?\twЁ|mf*/]oE{{z `Mިw IDAT{𐃝ejNtzo/[tӀ9a%F-jUh7M~. y%>/ASl67U-`eɻ`NWIH(*.tdNúګ]y/G z\x'K%c/V: ,,:: FgU&*d4^?-8(Q+Wģz sںE\X:k>I3!`o__=GAUh]m 'wA?4`uByЋU#YYrwoN_72Nཷ/s{oxIx/!4 ynhJ;NcУsk \xXLfsK E0LhƦ[F6RyV^|KL٬d2?}|?,`0h4?skt4[r %&-g㟬Ͼ'X&hL&2{?d2wguzr_$wBt:awTIX8-^:J6Sb'ts 0|]&pq$#xKO'~`\+k.yQHVobfĚOL0 ɔ]jMɮg݆?}` ʭ>J=ٹ]>b0X 8/x'aG:fқ1° "_Kc'd2[j8[~`4O^9)vV~"ޠTbSz`k=զRDvl}E=lםƻvk 3Ђ9]8}F+G8oE%^:Ï&1BUwO `ihޜwkW'N(R_o sIApx֢e>8F`=jit o,y>)2ݸ?)6Lo=-T;`0ϲ}SeҎ\:)i11jnnp8Oq3ꕷ?{Ph,--{mz6tOdhg=GqAVoL G8i:N'6Qnw` =\M#^pxV܎aCF<֠71=n{2Jjz|tNE8LI1q#=_Si/tx9iY RNk++|=~J"Ď#դ֎jCӂF\s'D8cհd}lb1_w%;QzxVkzhP0hE'~9 Ѽ7_h\ _xr_N4Px>1ʪƘo?򓋓wXI^=˯WF{c'JO? s@iZL#i$FєIjY >5+W 0)v2땡BBhy!Czm{9V,cUPO[KE1tH\ x} ="Vyൣ}Z z?!z/5L.EpXL.eJ?T\ڎ: rs_Bλ} ++)e[b:-]2[Rosg*L;s"b]x͹q{>"yܹK,Vnd93@zq3,B8IT[T1zMN4͝]}c8ݶ'4"B0#yixd,qNK4%I2zG6|ˢJ'@<n 4eevP3r`}EY%٠IZ(N@?#2Q|ۍTYpFN 4[`0ltKfYZ鑔V.Eܿ`D5V6.*n m+b7P(r(2yEV+T L\l[R@ ,>1@* SVd0:91ԑ,MJ)T"K#8P*Thڷ$"ɎƐL4=i[)[n5)\?BRIqlBo{MȺ*$"YXQ%x56SSmenBR  ^ ɼJĄB^UEzEgdUU_<+!P$K *B$1Vp0igUw:"qE`9+˒y-I g2Ĥ / 8igBl$.6ՉTHU^SJJ r+_O]T*ʫ^umUc[Ə;&$oJPer~֌q=2{[qcz0}I$+6qWGB/߲(Iy˒V0 ">8,O.VFUeeiKq'naIuފ#vVHaXb|1Q;%:FP_( Vʎ)YB-MYȤ":cw l%gf/*]Ua6 I$B]]ZGV,*tOLKɢsn "2yM[ZiV%Vpg7Qpvȏ/+d2/~s;tʔ2/*&v SVfU$"mD/Ϊz9dyGM;*RnÖDmʵ7ܛU3\,MIL޻woaiD @dDhb=dYy2rgR{|ZzzfF0ig/:'-.T6UҼ$yX K}IPbjej Č̽F4'VHrHޛ7-BsZws#dU UER̙3L~eQatDj>RvD+#򥝶GSr{3c]Tnfu6Nj$YGَ3CJ+Eq}l-[/$+d*eiJJE`̴HrgR~} UV #\vT3ܾ0ˮG @JFǭ۰esy0B 2a K 5Ѥm{33n&iaD詒͕Eys Sa1B vvT SW$U"2- 7h,[ |>YJJ"1mI}T]6R nf^!wt#am6L")=32m_ij\IrZg:!w[88"ZSTvEˑ |l$UiS*&)r||._$W%ϐ B6DlXJQmY @ dSg}r^Q_.)+*K"PUT%̗J LV~P,p$ ! х$u |ߏ$AZV*燊x`n Hʢ ~`{eKI!4$@ Ӗ/ҔEy@*Mt/O hJPTdO?nq~/ K:yR^M)2ig|{PQx.v6W%IcD\he}^!#2Y}VG ȪeGyv0ΥS vW{z @$ ?T7Q5L;V@?w9uwݏbDy253TݛReaiИ77>Uu*\%i{޾;3/i>L?xF򪼸 >mi>gcMvL޽^5GH|p&JII4}2<7_~Bq~({\1<hD٩GBEo|ˋBmmP?H(j,&$&$=AvLL%>%˲Gt[ ,L1 XW/ L,OI'Y֖ O1;(JJ(NK sd+gU(T Vb2HXʇ/e$$,oQJY4t5owaBzܣvۜod *HR.H d=I|hLBL-1+ {h%h 7Q>?7Rg%mSMuzS_4fK9 @"W$Fö\~K `\@*$]h,˓(z^;Hl*U3̍ےa]o+Uw>CRwH` ܹRz\ܿ&U'; #)Ie6Hʊ"[( ~ѱ^R%{?hݾ"TzIךX^Rzxm-NoYx>ۓdq6wa瞝* ԥGr9x&maE+bQh aRJd%n+,$q6PulY۸@r$>,lȔSR$~!Ş$p~RxKp u`^4aB A?ɞ^V}t|eIG2uœ<6ѶIj+^r,@( Dmx\li¢Y86ޟheY˖W}sVIWwaNJ ~ 6(Ŗ=zpEF`0Hj?ʕ+ u|͟e4o޼9dȐ'Xўv%{z%CX5ZY*gT-Q >KTzG;(+i~Al k8Uj#B%]mA$?`I#.OPǵs_Ы$R=A֠Xz<dk+4Kjj({ǎl+x2@B!|`9rk  9򜭻AB9z∙1Jr \'8nΓglMϑ'Ӂ!B!9ONݬKK3_6~S0w}S߃ϑG|ʾ IDAT. aV Kzszfo!]25~oNP4C B!Bh6"|΄}'wo-OD- !B!l0s=}rDCþ%Jf]˙bV6IgHdMz }ELr P]}4;Xyi9yzؖfz \'uJjq a O4i0*$"[Žy~³b37zp:Jo]qt}apapap9Sl}2u(NH/,W:O[MO(߱ZVNvZ!HӳNĄ8ԕ]cĦq)&DD9jJKuL& @sQZ,ԕdW4.qĂ3]gsv\TRt 6AsZw7ZI-NSD!BO2 -4<io_ر""sTYΦKuҽk\::e+lYAV{^AҦb{9ukb߿v)ky#tW%m*ĽY~}W /Xq9*fu8'?iSR~LbV|19X}G?N*Z^o?s qw1s|]%1 &>բEF`0HjOoS* [V6XfB!(FɟgMMS#S@SaǪOxX+9%1 Y3sF4stGV۠ D 2(+a@lPpymojP3 J3c 28,pm;XD]tW#Z !B ¥Hp}䡂}{Ӓ7Li!#GtdB1kǮj[ P/Q [`9ymP*N۱#躾s dH./ڗ_`9=#_t/5LgO[x@SuJu]; djA=(їB  ,-B!zkktdǎgӂLӔ]?,O<ٓߖ|_pw7r .<;0]Mp6Nɍ?/Fر ߛtkELa`4M]v~IVz 0}z/B!Ba?.^z(Zdg}mۇZ;1ľLUE!́!У$}Y L7VnZɔJds ;ϹESgͩ9}ȿI>SߞՇfnVVB_.BaD!1T۵h~SeW۵27}4/1]/~Uک_zdE—/>QTG5c-7)~o$V2X37ٞyL.d|Ųk乫u?*ݙKn@䇇9~$=5eZXEFoYu*7.A~N/\ϑg{m3AeəG+H` g-_mUQY'`!8kn''%4gQ̤=pB[{m\Pp]/XdR#e2f*/,4%>K9(s^ݦB\#GP5ب:'_x#BYIM d\,IùJgjTMɫ?)|xn]y{o p&OAKE5˷ dM 2nN҂O>쓙]9́u,,!ԃ>ޮS> = $Hܑv XN7%NJWڿ2kxG Y[4sqim" ;7a $zs;hhIebsRxlR(}odZUiXGU{ijHwǟ꟒T-ߏ}M7EʦsVOْshk{6gftb;Q϶3s n!HD$4ufOq?|sa"D՞ٻR|υcNLHH@]%I(gΜA=j܂#)ŴQ7cߎ)?ëGvC$I~ӿy s?[ZYw]>k݀ob{g#6xjo7ʷUo]&" SZ0`prƪOw7]2$s \vgc<~6_7WϞ=g&)2 || sG BP}}3"~1pw\qW;Up_x 0#F6 ȅywÏMF'"A-7n|註_ 1vH/*v;.$A@\g>/_ 𺻻g$M qJ|JD?6yЁ SŁ`,e 2X`,sKˌc3cBx r @yK0;~`cƌ;zǎfCܕE.IDQA!4dqqqqqzcsP[mwT.{"K;q"K˭evd0b]-EZm5ZġO H~sipKӔ7wnhhAZ,r xg z-Ҋ?: {{vQ[m*K?kȩy$fT?~? fYi%^ollǏ^hֿEle9}ECE27&ٌ`6F 4E֚a/}bX)pŝ(S[MND$,˯n%ED^#]([ s*%Nk믢[[b׿X5Uqb.*vY">i#2֧:WTվcC?N'mvE!bMU"bԆ<}{q\n%Ti9jk2)%dF5+4dvGdlۖJˊ>)yls}͵Ybn/.">T%L sZRSXT$M-*ɈĦdr?",'m"|@kf<_gg6Yba*$eJIU$ץ6$"*">ϴޠfԍ/W(%yk9ӞfҚ-F|IvXŅM^"VM/)IbŎjnv{&dT:+Hu[n쌒xVKqqE&P)/>\l5=/y󒮠H,\/poʉb x bKQ_/ȑtc+%Fħxj!`J#O""j\?7Xwf+날4F19#"ZgohhؖRZX%boi)1й+m* aw8cט-mo1쥅n-؋7՘;3Rao56eL"MD,v&s[y_M!E唤*)n{w/^1j2ZoPm.Ah*ͩa2w74)M-"HfJ2[sRdq_M\7T1`-.1(+h]o7_*k5vúf(U[ի65{Svijw]ɛR`oLWWK|CSPe3i5-"+zm5y\J1Ʋ;ꫛHl7yU5Jb K{٥+1[Eث-)]}C r %} DF5D6C[zHp)SjI+"=V>EQI)*)x%F١,j3DF%nK "&N*be:)#bb+XC%W[HrD\-#Ij%#~UߪH͌(\M^""FWD"V!]os<伛7'S$`2DiSg0[$"FKѰD@b*"QG[m 'LAD&Vw (8i66Pdʸ.1IYp{`5y|VcQ-9QזÉԩ!Ljyj|UjdbocV Q,!SDE(g 5K*1h91 IDAT"bjn10/.&={-I|'"6)/$:l>Z^c )e,dK$~Qp+'\Lr D Y_ HD FvDb_}H&c/I"/4{g9%#8bۻG%"5 $I%>1ƹQ rRnz'-`'zYwd IX1ɲDdu) E)|]TpPF6Aj:ˎh 'r h^ W`$DDpͥoW,I/'ި*ŶI$ 9x""'J-(q 0;߯1Z|ŝ!cH"H~""1/D,Rz~ه2V,C R Sy`N{t5!\0 &lN=qRURvanv,&Zl&3FmzxY߳Tڐ$˲8R ¿1p c!9LYyE6]_˹Eƨ][=Oޗ%$G_3xrjlFTlgB^&8 `ª :D镾z[$uGyX^]"bx?ֺJs-m"m-֢f1#esؼc5)1Cߒ'ۼD$8N"TĈGmD$8-6AzCNѤDy-D2=l(ҝh6uKP!P:,1/)U=&clq D/a" klH}(d"4kcƨյ@ a6I鮳E"ks+ Exi{ښL–a@%G'RqXJ%" bA'׹RӞI*-'d򳞶rC.D]TVf=[,432*vfrDQ9eEizN*(1Ӑ?'i-5Z|B_PR D\d :Ĺ0Y/MR >^,9#\'@9# /5>f_>"y{g2}zUMZ,Kj ʌ@Fg/rcGQeF5QIFYeydYOW (%In1pGt\2h6$SMy¬j؈7P[qq9_z!t-X;tVK$,6R j"FS]h/*mTI:յG+s U$ JG>XJ+)(Uy\~הVhR#Y2²AཀྵmYi~ Qf7#'&A B'pox XRARn&ϨK({Hlʋ3VXM8}m/I ]7f O+µ5ɢ)ecb)9..>η$jLY8}Fw5ᒈ9FcY~}IF5^vT"_'ZEP]o3$:+˰|ͧ^{IVxޘWӂmɆp%"jk{r*Hgz~yٯZ̅iɽKg4xEn̅7׵L$??ZEo%F_PcKG-{SF!;59qqqƢ&ヨ(8jҒyZGhS`JgsB3 yJKΪl\0\/j}6ȍu3# EF:{5SZo̫qBN`@7TFZ0$N"W^4/*j$ѹ?%"X,d,2~gݲJ-5YRRΝĺp}!KDkR2b*c$&aDFbdD^f#J"FWP-][w2vnK-ϯnt%vieۋ;8cΝUj{+R^X,7Uܹ9j+-ؔ ܪy1M,.y5ٛ$cŦNψ׵Lf@u.^S` ʶmJW/ηzYd`vqDj$_QJ*X<"Q5?(-عHG˳J9AaY1y57puny^5o)YuYV:K*Vl=QmMu͒Lץ&)ii[+w0)rʜ&`ZVm+keոiNoOȁâN-LQHͥ}ɬڽ{_Y66ɨeNYFi3u i2r zc^F;yv(&3SkNWV㴩:<^"#>M7+#&3G֦fnqD#fS ߼4?3L6pZ1 MLT8З޽og>6ޘ"[|QsDĪcbՃ.K7Y0IDĻ^AxC㢢 q꘨BvHS7mu.; y%yi}Dx>ږ996dIF}|rNAQw5zcvwxO4F}Jz"2rzCfw59TX}ff f")ۨUG%g<6isr lM/iBВ|D\c||r^^l:ɴI A:9斸-6_>me%j 6{Eix}ZAc}p\mM}QN-u6yB1DL,H[ "gq}˻|zN$;Zouxg0VRR?c9! 9U'癒Hls;vsyѺbmCnBZYbi>TCO)2ו:HnF{Kj JRu'"F3d[>@HQRAܼ&" X6!p7pEGIJ  CD$y'"N;EޣuN":Gxk̈ _O.*^EDk&Nd%>r p'e>_^W$ 1h;eIe׳eorͩD~M}ooE$E~("EJ{_|^,I<')r#P[SMuK-ȉ ُq< ȁbkEam3]iDDR"Gj}Vm(1mfKe"F;=*}v:ŵ* EA_B {}O}.^$5K\_,$|BASWU}-kD"uɻx"X"2g>^"bIø`YICDO$H#r^nv^fHZKˏZ,%T~D, -VkN2J[$>HTNoO? Nm, #sI""E"<.TD.ǻjd6*-EI H2RZQLhH9䑇eGmڬ-SQ\d%RR5€  PQZ#F D>ǻrL״DZj5>J+MGJP'i"dmK+olВ(u:bo7aӚ6m1YdcZ|6Kj I kR_\^ni^USTD"^SjriR|'@_3#y12̊Lr3*ReMep}fy6H28p'MU7.+QĤs#GN[,}\WY^^YkEH/ۙ60[Sbb<%e+t2$% RTo+/md(DI"6*({c/ZpjoMe)\WyUd;̂g fꔂL5Q>M'#vmTƊk61|>!xk寶LW1ol˖ukR v$*[؊/+mf9Ij'52"Iowʔ$0YR(0|L$E'Nc悕)*&ͨ싔#(Z-ؔ&EːfqllveͲUez!6*j[N(__PȮږ6 ޞ~PD]%I(gΜtۭ!5U{@W+[X'̨IiX>F(t Waon1]9F:USX,4 ]yw'99995U?^}* ɓ'#1ߵ#>8p!C}QTn?@$I#X ;ȁȁ6|_(܂+|[yy]1fBcȁpCK_,T]spw n#JcBCGŐ]3j1=/ݶhE= F3#@.H=bرM@ r r r  _ #OQ6]h#}Ƈ۶Wyl̟jCtm1f+0?#4dx+v"-kJ?l;vG鳧r#P<:aʕs5X^2H{wr6ma:s קWDq/D$4%q5?;pB5wiC+^4eeۉwjx=wbx/J:Eei ګ?l|qBCw>|no;{>ȃIO\sXmBL3q4uѻOC"tf_3[Nm?/23{. '(5w},LMkWM+Lچ@YDρ*k쭑ֵ1"T̯aokܱYhߝt_ g%=q&wS39yXh_]}懗aBL+{$ijᶆ-)r R>8?씈\c3kMs^txc̙ʉI_?MW.3<!0J72_sc%7,pDz4mɆ 4,?NӖn0W>:GN˦y1 =78Nȧ,XvQKzcF1D)֮]É^3l%N>VrrO;7ksS Eb?u[jn~y@n޳~zis;T*$N:vQ9ƺ>H9kՆW+||ͫDЦ[ۙIHDD=/74#:7sקΦmfǩ/\6Ԯu[vjgW׮Jv@'6Ϯy鍉'?|;3ukfnTT^3c7[6r> ݸ'Ewz~:ز+4W+L$bׁ768#"R&,ݰrzV$oQ̟496<#NxB}^HmTx%on^b|om|i?LtJrm1G\>_;3}O1}NyW#[жKfϒv̕]hy6$N|®Mq9iݎFO;"nX9[u %g@^zrS}K~{Aе-2:1/k1[ yẍ́e'FuRWCf7>TD Mk/HOM^ĩN_ L!:zBd3yw;iBhFħMD9sHH]2U8t';q(_H"̡6uO& WoT#=]Y}?; {7G;*y^?9E+hO^ǏϟU ׳~ygeaaOW~iNWәCߎUFMNy|D/JT*M=}a/l}R9׷uAиe*nnzj릃>"ZTByr٭D";5}~r[yݿf}ݓQxᜲb}N!1_ڷk.xzw"F>h[%vz.u;\r̤!4%ZN$:xfYj\OkǺ4%tx"d'b6XmOKDLX:{'pE}1`Jg5/o^8ݵSƍwђZfYl\ڏ!lЦH%<7o*̔E ̱=O<{T U#슧NNTL={%ѹ5woV[Z>Ɗ_DDDg= jk߭]"])2$:Lߩ_!ߞ@1㯖.4=wꉉ+ NQJX V,wlG?e4I'Nm+v/|_B.SD+ /C3'DD۴aS^aInl|10M]4w'xVj];T,)'L<>?fΙ`DDLx1C0?$/ rS'Nv D KD\tT͛ EOd>sR "AO<~(A?a=̝;l1P/@暲`H9}֤vɳP9 *8U~;tz҂EDjfyP`D^؀L}g m/ s 6!Cp?rA%b^VQ^%j_A=ȹ&Ώ;-p ĄăOJ,/х3'-rgF=#N_4&d~OCW:}>"^HDmvN*Iu?!;::tcN"}&F'=uzv:bܯ5t*x;듆G8M(QїDAc'ޤݮ?!N׹ЉcBPOtI6"6DF]@~Ǝ \k_ku?~Cq=}Z蜑)=)et/'nq%YDK8"Ff?.OJ߳OSz($^2WdoD|`M@5Fi֢W~/dLz/D꬟Do=HjBR$%FW `[,`7Gr%]begB)DA?HSQ1oc2#$iN;K0AO#tܗ+d~'Q{?^dr>A鋨wYϋAN( s݋/'ke<CM7sc3c8/H7 9p_2N ݥ8:.tSPf\f2{MBB^jABFDylȌ|tQ ]2P J\8E!Ci{}wvwҘL`}kQGW'3zB(">i2SgOGGx'Or 'ƄŮ1pOc&VOZR寻 -~>$c￸GX8Q~)ߞ] SHl' VbT$[aHD, _ߵe*Yn]!g[n[J6.DˈX"M[R[@Œh臨%Tc7LdJ3mvަyvK=BD 'c$[hD:$9p0 9ӗYQ(x$y_j޽\a&GVͬ{7st{kĕe Ka??5ɔl:e1Kj_udkĎ76!]S!"9+MR YrxLϷK$L$]xCXG<:=KWs˳/|3Ȏ:Zϙicϟ=6*"vtӗ*GM8:;;U!a}׎cBR)zz.te;B0D՘OBD] uCqsO?wmpIxs,D]rz:D=u1߸cW@ʄYʓzojoX4Z*!N8#"ѵgiT_YϿcܜgƍ'_/{g8=!ac/7NCD!g5v~m]{0>"tTHh=/xxܕ|.`gƅфs 6)=9FG?vie \DQ_d AW-66O3x 'Fiug^9?>PCO浪s]X8yrN+_jəS/lj}H1\~wdשIW]sLھoIes_9.||4˕S-_w7WzV6ķKO--_al+k˗lRdMЗo[mH |y K0ֺH fՆ$ÊeYӤMɧ.Rju8{9/kbuvHD$h,Z:m&cV2rU/:U횵+Xg F3\??ثX;[{{E'AD97\cYA1 #SHGGztnF wEOOOwwwWWWWW$I(9sF d]ۻ8$i|={Z^2zw#s\^f]/7l}WRtn4cݷTkV$oTU[^Ewq:oG=v'̽K7OZH]R͹U w Bs^= B`X92fBXϕ+{Aϕ+cg`dO/%Qk@i G1L E=Wt]5n|< ;?(;w> *3!{&QpXE8r ܉(8qيV*~0/99ކ QE%|`<_1yd@!zŁJ>0/999999_#/>_|rZ->הlj5w1>.oy7nk7e Ͷ/Xk?p?*Qg%-SiX/ipmޅ&+ˮNo}YSi Q,7 a"b )RHl*L~U$՜gLzޘgnڬiFSEaVZ1Yo1^7 m(H]k6ާŖd}^߿-4S|VֿKS[Pc43z/Q[`o霵NJ>!19e*FSRe۬9&lvL-";Zb 6]Iٺ/U׻,F2Ss$@ogޚ<1aUbgBCj*LR@ā34*ga+H Q`@EQT:êٝNJ{sp0jd4U:HVm-""fh1ܨu{gL;{5'.":dɱlkn2pfHSl71Vl}rE$YO<Xg'"UhED'-Sem=V^B_@M}ukZierRjmM%:At&zk8,:"-|FY=C틂@t`9:"{,*狘T(øD!rQ0gB յBUN}}=;% A(bA :v"_]hZYHV àˣ G*6DeMkA/Ub٪Zooc.^x[]:*D*7;񑔩uޗ@D“#LC$8:aLɑv]f,u~³~$"vt'~ ^`HXkV ֝aH#]*gW(3TFi6JHzI<_) mLűFQRn\tygI6w0[piTu/\ﶯpM[$tpo.f6?uj;lV'@D +?f1 - 4˸( \.o|&\x'b\rr+6 lC8LRp3 mg)͇P)lm9îԊ˫"*lTҌp'b./8 :@@t Ё:@@JK *a IENDB`././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/doc/source/user/figures/app_category.png0000664000175000017500000040032700000000000023002 0ustar00zuulzuul00000000000000PNG  IHDRUksBITOtEXtSoftwareShutterc IDATx{@w?OB2!:Mh+@ E[RKz٪= )}Hbk+V-x[)b!lAkB&7q23&;|GG@VB4  L6l6SGIϛu:ӷFolШy1 w?w_kaߨl2hyM0D&SxhTlyX[S}-wxo`s?5MNKl66@h6H ^yB!@(HTSQ+|o"0C$^78ޅell&P$D"H(666BF($P( @(6Bfp0^hhi!^},k !%3)F8=) !d6 -9T@?h4 :l l٤=޽P%@ɤ 7 g{h$2$0MX'L]Ld60$;I(Pjs0 #xFN+:t7^7 ߐ)[w.NxJw'W<$_\ss3()WnW~H*&,=>M&PHÆ ,+Pؑl@D$ǂxqc f36~S`UUՁ{1{{YzT*|cȑ}qqwtlm"7Z2O;0#G_6B81]e $-XbM="J8Τi}_4[[W^E, Vj8 3w3<.^^d_=${O5tg_bi_z)xa׬ we~Pgoޚ]|H,w Zey"Hsj}b_f1{3B!_7-eدjG4Uj4EBP($ &1c:V`@@($ЧOd6_zVa _X +V~%%%ַF3}tK.eee>|ؒ u:ݢEΟ?'N̘1C$QGG<׿rrr||| CB^Y'"twC^\!S .T]]- T@,6Bɭ|}O>b5ZODc5kx9cVo#^+JKɌ wnJ*ڔuQwG|,N^X"635Е/mKN\|a;<*g1 =GIK#~sykWF`p{J.{wrШwee 64{?+xJ"H$D/u%@a|||fO?ڪR\]]KJJd2ѣbVP(x .W1 ^,5]8Mג Q| Wy"D.0ys^-xE] "7&/.QixeȚpWxՉ iۊ!bfFyQy /|8avf^+%$N CWu|T8reyՉ )ۊy"=lM@|"mb5PF߾}6~uQLqAf5&%>T_xCd[R("7Wۼ$$B 7ikڬVħ{~OKpQ8y 3KI&(ɉ[K8/̽k6rDKȒJ/I=+fͳ~РfÒ֯c;a!M1dԏ%&o{A1!+bl]|+G1|"f'.V_(g=7 jij\allUWLBdcl6;׉2Z ˲2ٙ"SO=?j4":t/l2,}A- J$@DgF 捏XM:ڢ=k&_Jr_|>tbC`;2,Cb5u{L0p% kvg*1+b[?ҕQ'/Ksٝ4_,a3\I¼u kgd*y^{K\==PAi^S]Yuqp ƥbyUn^cV$,OP?|oZ{^oꀻ/ݖ \弽7tg !黗z\wgS~rls~<=}1Ҕ˷g*I3oaJK5.!=G1>}ޏ! ~+Ԝxف~[mvk_KAY1+?n'^ׄ P0Rel R/F][yls)iC7ffè[x"Dd彫W$o&P%q%iGmψ_<̨ J.܅Ă=@o<; I8IepFl͐hkk xC !"dM[ZZ,5朜G~+X!f3OC}B̫9""fI2k|f_K)uAĕP0<$-8ͥyʨ֓Yyp/.gCߤp4f2D̨Hw|m^ ζ~֞)#cWR6_ K\狘v\3K6?wÙɬR&Y@FvA(MtwuKJwF0v>l.EWQg2}bǹ2tp?9o^| 0ǹ㹲gG‘k0agqåDiUPA?P1 bYVLDīfNLY⡝7Kh4g+rf-`PFsr֡xM""+jb氍 Xwς]sW-5zM`]&^xuK"[AY2r%KWD=ZgY=w[q=^{řU{ꉨ`_mӶy߼ILgJQZ"kaiwΤWkxKgF[TX4`P9N3^Qa&GĒ2Mk k9QgRn3K6mFy|n]|eO {dt-_ T*ԭGBt<"tE,5CPhZ2G+({c_׻"Dd6ZM llj6S6kD?,wiQ_ؚV㵛{|BI^Xo欣pj1 PyX؞4YP1׵2Zz4y+Xue黮ZM#:|ȫ.iaa]vG(b}:N1| W" QSg[PZ[q=1x?x05wj;4սK!3!/ j5ݗlw/33ԕ7UB!5TX.grW]xs~C%%xI}4|@GM`c6Iwn4@P(`66B[P93ˉHRY{mmmfy[ze)p)h/o">n`L_wźPqT?-ĈeӢ?^DZkp2ikx|>6~z|qt!ه-M=ҏ/[LDī>,_;Wp2ksr sgrs/Dĕ^!.2 ̥9;OD*/ʐ:?mCmHl;K)[/{xm3a~ q&?q (8C]ajb]]5%'j9"R+zϓzsMxmGD|ik7X2fK˳~KSkjշyXݠy& ~r.[b eʀ0Wվ W7!9Wu٨S"Y]&"^U xVRY]~ykJ%Ǝݐ?Ю0ԨBfsW%,=ɤ l&ƏT5篝h2YӮ;wM}CMAtuuuFP8qƍ=g~/rIIF6llݺuDd4-&qN  %"d6*NxA8d۷Lq7']cWw%W_Hwѓd2 tyI8e_ynV]`,7 Ijj5Gl+"%"bcSLK[=mO(\1*“j7̛1 ٩,?~zޅf5&eHۜ0s Vb'64&y1DoX5t 1^KV/\Yk[&`^rZ^HFʔ5R'"7DvEIbieH;#?Г\y:9;p+oyx(C4LZ%b^eF\LMmNN7#"=rM+ |k]OD{'/Ь&Yob&5mX>m +2D'K |?>g>Z}fh6}EB\̼,D"cOd4dud4xl7\9|6)#ffKV rݻwBP...oX,~| ɴZanܸ!VXHD?Ӷm$ɰaZ[[N?i˖-Dޞn0TVO7͚$=ݺn- mm=ҩi|"xJ_I\,q!"aMF ג[MiD.\ %ָ}}1+cCqCqs5@rƍ?l$$buKP뀟&d6 #|l1ubf"^B2jԨ?O_|h|hT*  Edde!C?]xe'|Һ"Qhh:!/[+DO<)i;2tl}bͣFᐡ#~r}k& 5&]۲[?b;y.3bOcrSx"}R ~ӟf߻/TyC3(QiK2SÕ\YwT~d{z|j뾯F/VCldGzZh ^GF#yPJ3kf2ʊmkl bϟ?sjྨK>LN˽Y:#zu2ƍBkO( fжV( LF2d&usgUV+4?G2}Jsocۻ{xNOg-:x d6YgDN殞0 )梖g&%YJj>sd2͖ flfsܷ{+Yl-@-e!@*8I$DH`cCbfi/8YLwS {\6"1CRaH D?O$E&@$@@$@\Vx$R4' p?ul6L&h4ׯ?h"_%J$$@}h4x P,~(h6mllFcdJD&!om2+mll ;2LfY +&@@^,R x+p eH\&@4  7wdk Jh$@@$@@$@M @3zd2)'  , :::Z ~fd2FNkhh В]lmm  6^FB:I2      i/Z?-bݷ:ϚB=-Tx䛶+U_~&Wϋx^xᅈyk76n[t_v=mj4MoٞamZTS~\7|Ө 6\V+Yg?g=n7K S,ڑUH}qI~zYm3K/Z?/m[$Pْu^k0&#Т%55555t쓯^O.n][Yݥw~P佥ψo\,,Ҍ,mg*gl[4/*j lGDv|1G]>RQf<-ϚptŹ*Z͌Y7٭kEW,)\v83XFmd&Ws(.as2VHMq'bG6)\"㲚b7/J[V&Vp89xz"1D옹qq3KgŪ` ܬfƤ%Do3RssD(',K7Bjjj n9i"zbd-'7(W0DԵ+aߊ^u ew#;CLUXj~Y1l]8c+#o);?Ho>=_hhhh?Wj:.)v `Iԥ99gT=wӄc|uaxk۷޷挤z""?_$[a$9uuI_d#>[Ҏ ͟嗟,/R3ɂ&W>8Cy+GO??&y߱>;azK|F!BŲw$FQuIG9E;v E~:nj6/.c_#8O~[F6# -_~5I;.^K(mޟ~u_waj|-vg(4Lʸ$CܡxS{w4zIL$ 5ȿu^vKHHX7_i]kTD`6VM5W\KHH#jW&IHH ||[9hRwU]9j#ITJ~S=sJir_&Jkޗ_A$ :Qo98LL_;_aɽ# vo_~Uk5`(omֻU"3 [#+7(bv#qceD4f.T_-#"ر-""&>-!"0_|uY79󜌈&@2VV"`eWO =[Rx[FD3Fed|'1gLp""b]]YYM4J _6Ĕ~ݕ2Ë.{ x5GhEP%%>XabgOW'gileecfLdW?RN-!"Wrjj ~oq'OO[y8x?JHTV(&"gf{o{˿?O#{7_ȡJ4 1lojD~O9{؛H]D$9lFQG2v# d H[09S>5vdPҜ3Yy,nZgv4kWC9T<~FN,=LVwމX0q[F>a wĿ~8K|3ߎpPWYbS痭{NBD Zbub,13,7} X'3#dDDd8V6S0 jbUZv84?=uYܪ>@Ue0 Q:r/jr会'"YMczoWyftHX'?q<39i/jjkwzBDD-U_ޜY)H*w!:n2[jّtFPu,}J뭽bp}I,O[|jpxztp)I kۉLZZ-su9xJA!l`,Cج1 "7QUնBU,ۮܨ="F9gN=ByÿZwJE6z(>=n]t=6yѶm3$Jy}M_wnt(,1ڸ\CD$spmN5H:Ɂr->sPg1leήDd]dIkbC۪gDmV.|R"|A8H&=Cio0^]YZ W:M1nݧ9M%릱 bc#RUjv̪nDD̨ cżͼbBLD;>}&''\JJrjƢR-9Ry3TtH09 "mGqTkKsZecqlo/:ND1ƑFd54j)^FkK'IzJweF{uV_}zioHHtd mTVvuoJͷN*mwFڡm'"LLDb{)dD$VLd "sImm:B Rig榿 γޞ3lpn'5W6Wݔv2xxgU$-g|leR"EabQ}TBфAhNӶ5TմDD\٦e;kHw E qLqTV@gr Ceu6"b^ܒkH71A3ivm]O垮kx""b|C"xc -#"7rb<%}6d=MDDvQY9z?Nu;LDD[­J]C;U[z1vn>D9ynUs(tY[7jpOTA5"4 yNXWV{d Oe9|oֵ&䈈7_lM5~d=Q}n"LHk)1,0Uo3ٙswq`˖U=oh59EmtݭG=}C"?]6J"deGSeDvA6շUB%3U[]Wхw]]^ODdDD-( J>~)sJ:ڲ7k')juoot9'9jMJI-@m'zLҶ56ՔDDմ$d}@]Chns3}Sѩ-2gqU4nD%OD#NR2KZ4Zzzdqo'-;MN#n6r?:9>f\rOǟ-1!( QWngsCKVGd88Y wٲCew%2#|GabJ|ME t:m[]M{vVr'7>]- +#@][uNوaz95F#XG'N7ڣ2ʋGd!tpEn.1⭔!eHHHPo?Th͸efښ6@Q <$ĝ' "+7T_%M²L۬5:N2`Rb:!9*ڬa.'"_{"w""M;n|:7o<+uMY驞eAmiux<"PG* z.LYph&CFw(1m5S$vW.Gy8wbd5.k\5™Ziˍ⃬}4o}Ĕu;++(8wtM[g,},w׋q[R:oxkK~@$@Qg&#Q7c'#j#"Zxcd[-̟=SM^.^bdD'QMA6Ye)?eHwd^ܼ6"j;,uDu+GQ^z£aHqw:J[]@3:rvv]KKDWFynFQEAtuD=x=EJy"g}er?q#aA7 :s{ yXB_SP7EsM,#}Me@ж6u[8;" t.8g]YΒ,Sj*x My; ͍xL Ŷ=E*]\`.daNDDu5uv^hY NWU5H[q(eݲ9s+Ehk,ueM2'&3- u@]ujNf@xt`$x4};mmMee]HvM5DM˦G;ŽƊ]e ރHF0'mDD#zu q&liÀJ|z;(74O{z1rJD]k);m>ܺOǟw-R0:w*N`7RzbG'W,N/9%q,\evU̮J˲{,H/WJ<ɲΧ2ߟ\kxDl\#ه|JI󚈸M}ycƘ{nx`pp@ @Cl6L&h4yt 31O_"xoybJO垮i:='z܈i2H*>qD'Z{r3 sΖ!rUT5q7s2gJȈ{%qO9Ɖ={ *;o:yEF <H0`PPZ׀`     5666nw7cWfPk: .?ND$u`hoY~_geZcur#Y9W4d7u@GwUC4Wk5tI*k-]Y*ѵ~:"v.8,<4v]#^ߙ]/o7QxFdQecИo=S[Db=LP׸F, '"orvgO𓓡K@2vSw.C5UZ"IA<85ڻRzyҤ1nnHS~3#}|'i,%"ֺݧ{"y2Ԗ6IXZo 1?-D2wS-weF-pD `^v",M(*-۵DԸ;mHJD$ED$JEDt"1dzKEts/ݠ\s] IDATvfn{&cKeRHJnvNwu8O}{'KIg*mk:p@3 *3tvN:gQFm{KeέYT)H]^^Y<}OH[SH,@x-bAH*g׾RQPȠ'"rtҬM"q&'}jo3]>SD$D/@l#{/yw3Rp l2Fhy^544e~ a#X}e#Ǝ2ntFϙ C̙3gI݈H0t͖x8nΜ9s"q bΜ{UP9s^/9sF"  'A39Lܲ"Rv=?(hlDZ+s. W)mVDT^9bEtӞQ ͛J7WGH:(FS%_kkm>˗yy-[je:[x-7`ƒoX*xy˖Z2.WnrGz|hxk/[lU뫃7eOWsLXo)  C"O=#:{j8&r\I9[XΥ[a܁DLvX./Ӄ9[^ЗFOp᭳@7}z\We-\#jPT~\xCfG'4?p)"fm*?^LzK2]:*U&L Zʓ95ٯ[~w",KpќT&9_#$G4e/͚A^^Z)7եHM L]m!SkFgOWsZ5  Cs9[Am?df'$ ?Ubr~˶#{jyq1B9 @=CeIBo}PÀ`q" i@Ӭ^t5+gn$HN}ES |@y t5jFAC-V^   $@󇩇N͇+GoCo:xMs.x7RC5ulۛ65/ ]4{)kg-ii,prjn$ʱl=.:RZ$S" \VӬo#G8njB3FQ5HM_D PZgDAy:@)eqڜ{~zNZHe7sfϛ*R}e4=%[G+++d,\%*sQ .>k*h.(e[cfݕNC95239u]z*=0="h-ԩ˃k]"8Ϟ GcVM $l{M#5uqxyoR.]RڻnyTxִ% S3/v޿WwAAy CMU21|o?%]LkjUe . .J͈,ٮtsekڔ-wNKio0M'MkHebgruZE0ߙFU_I5}\$U[7uyoz3UDAA$+mxNJ8MxjFx%ݳwGAy 9W P! D)nIW2W  ȣ# }ܥz%v0(  *@ARh͹z_%$DB  H0cFAAADAAyVW/ЮY%Z/4 -~Mqփ*l^Y1h]eF˯ϒsh|   Kj~8]_B@xw U?Uˎzמ(AAA4iᵠ9Kk_Tffu;cC.3W43he,G8LJ́/s6ʿdz{wzr~mi \8YtR7 ux;hn   q/Mn~as_Nx:=3t귧 }], yR]3;uYf+6ř.^<&:Ѕ7\O˯hxa.=>G曮HѪ4`j-=%Yěj;STjWAAA{4ל fNpLit0%YSa'/շWdNc9V=\6/O湻8Lk)QsWDx"_;[ezM{'#3x @h ~yZ ۘ/Xe`ڦ}V--2l# AAy?˨?\jNXG9R<6=n?Fm;'{XF{ݱnsi\i,Z8{{.\ֳqt ' k8e)34b;޹k>ٗU|bMa~ 6`j.Kw|$6" h-mREIgR϶ >˱˿~vtY"=Q"S DǗUe<A4GEQEx\ )1Ez>AyN\1oFkYҡߩWliLgΏ0o"7,22]8pLWy", 2&:ĕ—y:'En>+~k+\HoˢЌ͞2Hl_'[g[ze Юѵa4;::@ I( 88M7n|>1ko[\_(J8ixX ÷ǭ*'ޚpem_+D C`^Np%W<˫\) ZExVZU#//ޑ9TÓ(7xX}Uk.ޞm;ĉNN'_oot&L`YZ%*oLرcT*~UY]yn"c˲z;S{,Zk=mx$!2RWV+j%~|Ջ޼DI|%c[؝UXhچDc߳&p[v n,(琄$_4T5j$HI⋉aYFWsSQ& %c].XSf3! He*7Zۙ"X-##ƃH}Ez2$ЩS_K֡M|<-zǾܬp4uc| 9xǾ87IDơ/3V8 קeJקV݅Mܬ1|-wҫ[HDu gƴ 2<%+PnVJ8Yic]dڗyiZT<4\˙oZA, g5w=3p_hA{˫лݥzɊOg3$.)T]1ؗDƅ{@F5hQhBL_dR(]K:-.%6Y,vOH%GwWn[J$ht;֡Ʒyw}Q28)% GgR"26.]ufZu ;7%"v/PUg淒>I)ܝŮ!q)+D̡ @oOwqM΍괴L\YiT?wgWؤh-Ǯm(Ϙ@F'aE`XT('N9sÍ^nCN/wh zz,blNAp9c9\_|yp'f̘1iҤ殮.hXv97moI@/U'mvwuK IDAT0y=y0S s(gp_(ZiPeV0maޮs#dL]f^ P$ @(  3#.es=06%F/M?6( %#+#%L2-e.y$n];[ȹbgIqК{~ $?KQ"c{ڠꏵ0jrǏ?yUV3aӭ@IpJ rs|I04V =BdRhTH!?)0[bH>q;2ّBR-_~{_OYc+u+[#rL޺%dpyF) 6@nR0!V(@!>}EZzHյj %! q7kܣS"$1)fNٛ5k;{k}_wr׫lgLg]2;?J_1d2ĉu:^6LG!8:XΏhVd!+hmPw`o 0z(  _ tm]MkF=HdCG H}%P'_y{^IR ttwosh w(VdXCFzyeH&Bwg"ZFAG: Xe  =A-78,\VˢPa:[u^5:z.Y$) A /wz0':f'WI"7AW/Oo?+v ,mme@ 6.FIRV/R;Q|Թ琂;[y]&3uwؾ~THDxq>P9 p7qoC-ׯxJ ^!&ZTxƕ+o z3:!@8:H@WTZw[zIXZ-M cŨL&S{{{__^|~vc#xܻش1oy綮`AwAK< sΉL{#dϥk_xŅeپDr OI (r6YWRo7J[t6;-o;uvFRVDZ-~kp1-qgIRQg'Y&ߌH@[^]ۢT(ϨhB]ޢҨ %;7\&J.No^\TTʆ qqj(7KKͮjQiT-iN'\ByzIRR[ZL^(iPؒ,Xw# ۿ?suTu &d27@-.>&aR59 F%HI"qHJoaHoLF\puL4H K< 7ي@\< pk/x"]VEyfleNܨ %) ޣ9)蔘5鉩ssog'Og-n=Tȫl{Զmjf5 r N!7%ݍ+HMfo IOv9H1SL!I*ڌKTZ}E OhrEg> Fy:\`Y|w>|\ςY6Kھe \zu\._Ž9]Ҍ ~ IiiIR" IToʴ:G3 E%)34jHvY7ϻ,L۝1_, !#:D EpOߴ^og؎,2=}|}&OhRF\!w\F;}S@ 'd̽}]wdg2Dlv(SS +)2sS&<20 b.r?`Ybf0 Mӗ/_7oܕ+W{{[mgg o$Cp 8 Cpq΍>f-Le>>v 0XSԕo~7QQZ-ƍ?c->B(5ᅴ~퉱11׮]swwgLР4kx~l;V9_ћ.'-,KކVηHڐ2yd>8-z"#CtVU8xAFyv 9zbC?QW:L4,S-IW+}W/>]RsD A ,>S_,1 :pąy? ␔ }ZZm{ pܱM;~}: zNGGT*lyb}BA@eY`Y}V~#w붮ׯ_8q">P <\ѵ~{  V7zN&T+G3q˄ɟ㫃 ȭݰuO2eGn-, f vfqྫྷr^=\ջdž"ɎT*@yvv,˲0YxڿN RxKA &@mijo˾%Pn, %6utY kATg't,`-+:5o8_uRhDHgT717vGaAGwZ0m Ӈ^4imB&X 'IJرaLf^{՝v#]x \G;" ΀tsza՝K̵?25-U Yela _"NQd;i$^/"O4~ `Œ9s&4g_=vh;Dgu,uRк)u ;,T*ODAJ:;;B!ia ٌ!<"5li-EF3+ub3сcg3F 6agC1{mm)3%7n;ǝZQ -PDεr'N5~?'>7]:#؂`vVo碕+\aW >cwk?y4;NPڲ^}^Xt`3iƼtSfgiq4MYXq}gDHJ/c 1|2p*ɿ1Suhf\o,ܖĽ{ˤ%yB?^x7>~;jUbd@e'N:,ߞ^u.c^ytg~-s_FͺuSSxv-^[޹|6'YD[JX,lu=z,XY5_3o6S&2z `Y‚x][Ʉ f P<  ²8x!B;/K-p eY⭟-sL51%?24AY @+NoǬw>{moֿtpoU.Z;&ʗ=M:i y[ۛ]=W>:dx)dW]Ӊ]EFբM!+_ʊ<狊x k)t̪!~~bM `!pxcN֫p5LcK?W?#-3TOu]>uƎvYxvo?j~[G "E7 7,6@KNH5wҝ_,|i3=Z];=Oh__,`?UZ\yrttp8 CgxQ@>/SKxuma}ͅ7z6%uk~l>v eYnh#[L`K<'฻e6A?- Ls+՛-˔-qpU߱Shp&Sj󻺺b1>\ RB[]^yy80 7k؈ yN}m'DNBh{lAÛ1>Wk`Pu,ѯXe vs":zڝrfjRR Hy5x1]Lf*y䢓C-&i;4Mߪ}ur9\~]\W&l1_b~Ŝy1xߵM~ۿ/""%,`0 ) m+mwCgیkug!?mmU!uLB B[Lv77^]-({ແ6ء@/:| lՊA[O'5+l5=~{xv6\?_VZc~bƻ7o`r-vEmM'|]~3L(:88%CroK$(>ӛ;1XlXS4: ?`$}K9– Z% vkMڌ#4rnttmIC&o-mkf[[[Ho(@hﶌ܂ܿo6Lbk3Ѡk?_^TݏJz VWT4 LP[^69Nfz#+]eҝ-tS=˶\?¥7o/tЕlƢ[kGU50iY5o6SWxLu;Ͽs!BӪV3^=}u[Z~bZ4AFa_ ,5a]~~9_D^sb͐ _OWalfI4a\Pf^7!Ot؜*o %8$%I+GA@KLݽ#mdo b-l8fFo@p cQ 6Yr@mW)"I'4|CݰK59k6»Fhc/PW ozZTTTi2x_wY9wD֮g];gt~]ʛ MEצJm /BL~~s7k}g fe+!9u7 M{sW h˿&.Zz\7l6p`pʏeGyQ$Oiğ%?7`"#w߽y>2)m#p8g/2%nRWO~nČ ߟʋd v$H[g^CӧS=łύ6yva<={p8 ݇z%oSQIBFcamO}u(W;^^y0WRVh h@^v狼Z4Wmyuy[c6xW߶m~6:q[׷"Fe<[||5},;0 f`Є`;kZ퀗t[ TynF뷺lyZv b69*{hiyBV[ԶpW t@.*:ҵVH|BFu")߆mX>mwX_:|D 5~FWMfJ[>. IDAT:-"YTRRHYkKM]fnq)G -R褤0w}ɆٲꀌXQNyQCCz%l+jʷ'2 |!>N@+)Zv u ċ(`6 rm t|^.`))!c޿~v= &̛<_Avf2b2-FI}v85>BK$>!~>d7/>KVlYQV륡))<g{N8vn鶬͞RfV;UIZZ/ MJ-j+㵑dyB֒>qےƪ4,i3h_i3vs Sf_ EHO!ݒ'O̬֋$RY;<3s~b4o*dž`}4kC<.aoC{zu:qݾҞ^ fh?.AQZX!-lX$!/? C3l<!/{i 3?U) 7eb2: Cʡdo nغ2I9@:=6Q"R6x a[zcUH0}3Z6ek8MY1&ME}[i?13V>XHݒI +5DmѦq'$?ov {iN6!O$ھJsp5 ܑ:z$0\TWgSUeS!*苢8I\^bܶ%y4ff@7dȬ/Ȉ tuȎCE_Ә{==$LikZUWdc^hJ2[vʊT7F6)!_~hjzȹDev|j߶/>uzbf M(*,][(/+⋢}d<\7E̴RQBnQQQQn0Z2`]k#zL=Sg gzeBU8;FIps11QNYRyZGҡ/>Oې\(JU!I>,wG@k~zÒ%G[hMIZ:8)s7mGhKϾȊb 4jrQO?|_"=a 7U;c*->Sq(+̋O-(銀_}YBb2(rppP0_f #B؊`a٦+wKmGPfkKr\bwpY "!ǖ$lH,_bBmdyNًFsNl[cZ.^0+E=!bLH q*}E_ϑg60M+ kki ܢiEbbbrܖ}l[Bq7FF7fb0AEvh? PںHஏhw ݰr&"O4tgUzCQ`Pf 8N<\Bo1BʂOG R z@ՃZ+ pQ$Z\ vr +ǞW M[-V&VH"}$E"R]RP\Q΁[z G}_fDR'aZ߫Ұ`w r8X*Ȅk,Vebo hyzFnMsE{yu_7l#&S~x7 }  O cfvN@A_iffw 9L;#x,[8 ǼH 4VmM>@q`\"5s4(Q>!`F&J)Za 1tupJwmΕ%e>bgN #ZR^Ʈv0aPTO5QO/YWHk[%!փAg0|~.Ifvd+6| *zW9;do_r9l7?w<_ j<>#S>6>Kw}):RYicIbY/Ko Opm̌O3Zhe&5j1_o݌ekZHO!#$:]\:' ƴ\ZQ kaһ-:Ix+K5hG{R2wzue̫+!O>a1m~d0#H] 2)nz`Rːr([-rRAQ1@@t2%Rz~H7mуUdeBr7S@j)Y[?{guhaaَEĶۈā8'Zvb&mb +-&Xm1֦MLnmz/ƭۀeAY$'H,-! otc!3<9ysNt?"d CT.?y8̱V'Q  I5M!P{~=!WL/46{w9]S[SXY̪)|U=<@'ٕ$F\5#P: NNX\?3MڟwX}_>ck?z4==_!Sl?p_w>|{'( ڀ܆U,hhcimڠRI }UAɴ嵜*4[|G`< J hz?faų5Oyey&H5Ms/')[A >V>"2ORF'?X`RA(4}ش+sfI3dWuXE{g=pS.->~J?;Yrmف<֭JUv]`͌nY ռ1&3o1Ϭ(qH:&mUԂI t\ayR3Vcc ;XVϞ`m:j;\+*F?ai(K_h PDK#/N'C~xu?_Rʆ6PG13$?6UɱVdƕK4ɳ,?f:< ѱ!X´Oš+:aU%Z@ŦmّFW?>̩{Wu'ci,ұ$Ki<Ѕ*  6]E +?FOz )$moտSÏ?[|ۡƼ|!sn{֘[|⛱XXƣoL|@Xiۍ q~~ײΝr|;CQ9 KkcFxhebq5fdGMes򅆥XXy&6d@_[$iݍoHN䄿9)9;WMK]pR/c쀏aR%h"{s}-TLӊ<;=YLVdnWҔkɻߙ0Yq!%ir\^Y4!S$8PBjm׹c}EհE D|""a GN$L<;=\FR't6#mvpX]nUI:a>Zkd$ӖcVޡOs[[W]{HM*w۲eO1$9Ip%8쭵MIubQ_8k}mnS1hٖI{K -Bj.T`(TZ P@2  B_SVRxFoySׇeHGG2 :NpѴsfI#%/>?@ ON{###ysM"(Sk73gGyuc`c y_QIKf5YuOѮf'4E]-R-`vpw7|Ɣ"5w Πw| D~aT-M¨i8A Kc)mV*bJd)Re6S vUYIrF*!ϸд1Q YL+3Zj,_ p`)}xEiprRW]1: HFS^+AeeJMY[etBM,4ϬdTZCn=vhw͂Ʈ-/YIMrny33ʺK+gˍI U9VӶ+yI.0&[^q;^?_k_?MH#ʈğ=͞> ]dѷ(js3w{Fii{Ÿ^MϾk]rnG\]LO_Q2j7Ҳc\ղp`ب^ek/޶&f4mKcL^uP72!%۸ہd5ltOQ\HS4t}nxѦ!DXcyɴ5HUbQ%mUDC"J I$ONs?.$2,8q: >t&9:`Bx`PWYO8̾!?+" <>ؽo,iSK 9w >?RȤ!R Wn6Q_bQӒU*9~Q@R} *>4 k`%ɗfܕƂIFeБMcQ͵-d23$ŵܝI  !r]|>{z}bhĥ3Of$PDH4S#+8g -cuQTp38bG8E,Vɖőĥ%`!C>#Bp +9b*E4 @QڋchԈ#AAXC3p~oeg)aZxh15}ٹؕ[+Cu4T$6Wg9 ϙ3zE*&GĜhR< K5sl<pމL-T6Yz><&N#'r`Sx7 Ifn؂sT,\y'N^puWLsT~N+h\kbg-zT5᠁AA[œKx+ K פ]UR&Fo\k\R T%^ o:$}[i\#>bNSV۝/@vq!?Uf$Esn%4/,>5KbŊCYF莠@L<B \!q/vWTaANDD+ʤ=_髪3v]ǟ5S.>y~?|~ `l*&hMEEHy10a#1~CO zAALMC8;ynx4i{?<(Ϝ^J"_ PDDs2_gCDp1ز)fy9flEDDDDx^Z=?>eC LΊ$̖1 "\CKdoԌ41 W!NH#ÂHw2f7a߃m7NBrq1Q-5 J<پF9ԾT&ED PDMpYvc % _$2U|ي|퍍? rTbȶG܀Ltl b"&G^8}=;1VТÄ Y⼊y<M٢Oгv7 rbԌܩhq o}""eY>Ӊ8Q7 V0 |Y/<4MKR70;ZzB2J ``qo?1r߿&c_X8GmRD1B*!\nYN< #v%cWpM] >>‰S#EcޫKgUX""" !yoXT AEن~n3{l ~xcbҒ91~ÉR$"P$@2_ǟ >o?H1|a&d/>g_ЇO~IJ{(^!::zddDED PDϽ V"է n3jQ7yMSPTBA$>?^ T0 }S7$Jeq S\]pWclJkcVaw/ 0b?^J%OR~ `? /û™|Z- u\*kkD|""AI䩋&7'@ L ׂ؄nv,fݹgSu;W΢ջY:ݢ@\ܭ;]\_V WdhܵzŖXjdټ%}C]AD={VBb @JN 鄍TRO?w y|CK+5>?{#$vܧ缿=ƞռ:~ }> ?pc[s0?|8 bC9Y > ||__'z\7} t7$D$a u;1C ,&A(!`# xo`D%9pEN=3Eq*"r}cK   @ @Cf6e6EʌO'7*<*1$Kw IDATc۷wl5_v(ݸaeYu.yTUrnIAF,l޲V9vؑwoFBWJwdtԚm6\(mjHgB1ɨti,-HԦ8VX09ƒ篩=G5C[-kjfKU2*jzlV]^G6UV9x֮,(ORݼeMm `5ʶ0-;Xl]UwgS»5:MѦJwWTcv$2njմ68hs˲b)Mr\i9/VUrdζk}+;lmq8\dbA45BD [6vȏ`o\)a -ɚŪ ζ5if+Zl<ZW=erwq`tke[fBu!{< 1z:Z[5LNls8\LjI$Q/YVŋ#yu .<W_nYHXBܝL/HB&#rC9/@' 8KLOF;Z06aA_D_ j_UPgm0U@>h̊i[hgm0ִ\%znmhhˏ S3.ܼ;U|uF4-=>MtP)۞(VB9Thݰ=c%_{ RJMG-,r._`)}""׍>̈́)FP' k+M{C Ssh00uǏ?RTک-eR[p`QckUdbYڞJG#5)OTXf8~0? +`CL9dCȖcI=Pps([|:ZT76>XWj+-mΦ53mm|Vپu/DNu剶*3'Ψi+.nrr3J?X;vé6u*w!P{ %J+ جGsBS;p-M.߷Ёz'u0ƵDЁ{FP:VZ9+*+Kʴ8J%IQ:"몪hauu%)IYYz9Ǐ~Қnn؉.Bx6M#e)SakG[WBV.T*={'HK2vk|]k}.#wd}O;7;}Ͻ:t',Q˖IBA:S;"prAAvvSa r?r]s kMqE_VsGvg+C.DmU.珿|Lo(= ;T.eh0])]SZirRl:IYW׸Ϡ8hlll<ٸ/uC&nBp9p8v'L6 ҪwoyѫƐGPdOA"H]IB#]ݮMWR8AhFS6C@4 |A4$keWkZC |ԁd5(YtO67XԬuF:c)*99(&!5ipXfg+( H&9C@Up9XQD S+(el\\OCsLN&MH!93pX٦J͈*veN"BDtY 5u0\Ly̱JʲP!&E;BKcNNs!v ;)P4zŦ.)dwdzw4l){> jhw͓;w.*B15rp%Ke6ӿsT|,`rܫq <^$,!#e,^3 d /B: [>z_M+f]R֞3)ƺ#eJ{GMeWbSlGs$dlMm6+5GvQ;yGOlkarէT\fRԉ)*6EN 4i*057wTa/1 TDzNã1 a&"2n!с MpV46_9UXK49z!kg9$:[r< ;j%єt0r4ieYI(?*G$A ogVBT>0)Y ;XVώEC»\ۗoJ Fa㞏sn{zdR2$,! 9ߚjй⻘0sC?|RN^86.4%uae1 I>ȅlvb&R; 4t::T-8$8>gm7;xƫu$SPi1"T:L0 ͬ,=\<˲2(Ae&7{¬]쨩lNhea{a;&RS|ݞp]X \5\XBt!{TD!z]߆ek3;p%G Լ6bw{:1Copc=tr ~C6рڭ9m[tΗ+/`9ŒLJp& EtΝk9Z|%K\gF79a 3i蠪7?ZsMF g R@04LtQ KݝZnQV2bsfZ$C b}z1~mYYBNnbiѧz*>4p `Lf$[O>N2L[3+FeQq9'zJ>\KY*)ສ1yJ@kLXljʘj#9E(s mW$ R\`L[-,^Q1U=TeDBulO]fdi JbqPV[n)FǢ #/u&~04"i~ϴJ^8_aI?3 ,@EVj[YR֖k2қWOh{(LdyZZP69W;.B 2!>[XhZYɨ܂'z +J|B*-|'I*}n/ȍE&χ ~|>9;y˯Rq~ittDr9uDDU}}#%/[  )'IswͿ [s컂G|Q}}} ,'K+Cu[{ Iu\_̗ؑp{/Ew#KnHl6… #""Cyb"\oYvWPV7̒2_7OpQH#(EH{x3''uqj:qw18[G"˳YW:\pt CŸ<044k+PD :,'o?? *86Ixfs'&""r}pn__Aw(ݝ m.MXBcmqFr~yS_W9V^\ LB<~m$)#G_ 8|^R$׏)A>''[*Aٸi-l{/k  RD7u@\("rEQE,%ᚉknY+HVLDD:H)(H֧) ʮE0g/J j%::vބ%w3e~kp-F!97o|c8"e\F |~ =x<[Dn4;}N+ڟVtMw(KH$v}޼yK/ Ă 'iw?;4@ L7oDI ;R*y Z1ssH!U{if Y錌\k-M#Jd?oWbdT*{ҰTIOoDODDDDD@d8;ß"|y -\>Iѿt)'sz¬bG zL|`Mp' < 1_"Vk*'16^*>붬ߙ3{dDDO~KWȍCDDD"epO8A?T$FxpCio|fnWu`Rq*ރ*wNGSkJ߸mT"dQQq틈U'*LX:ަ&eR%:LD^x /&e'.⾛?W)>vK%,,L*nDxE:'OGθ]' 2X9Wp?vsĴsѹIW}e>c:ˎ͍]6arW'{$Xeg$"2"##{{{bbb #Nj^ Qsl>rx+8Y 1ɘmx΍r!,|El>u DHɀ4{\cס|7e(""Z"""YxC;ED67&az~ l2J&y5|.x{ -~]y J| N?iќ_M⽾pɃ?u-sg QoG-/esML$&&ԩSߘJ{KFE@k$V }z8~ |VO xa Rcc6@_ H0+O% D\/r}'DDn\f$E} Nڷ]b.ksSEz>[7gVgQ]3BeYk7oi0_Ffa;*ifG*fȰ0yͳ4?:BD&,`@ʤ>78~o&ig='pJb"}^V/ KwOMSw{ģ_z߿YvM}v.}_6l۴y=x:8^<wq>MF myWxnߐ9EuZYZ[^^qj9~yK>@\Nɤ_9J v!gkUj3 ײ,Gc?AS#G@0,|7ȯO޲ퟜ?9WV?+|/Usp‡gW>: EREƤ%LZxX}fO}+~[DDf",,~ULC8A./v A@lɼ]; trj@-Sw $/¿ ^p/(w*>@ Ǯ[JSl_@D l6ʾQ"=23W\}_E;7df:Klp֦VgffffMwm߲iܰ7n;Um]W idž8͇֮M陫m* eƔB$ێش:3}EMVͻNOOOOPTp1rEuU_!3333s.љx1NFɣar"LGaYXd+uaC<0yC%K%nCBHR`G<9Q^z0̋gّy" \i vՙwo]z|Igmرiuz՛vݹo@ߴaZ!3vϖ- vD233mln/1<7l B\vred^Be֥oٷh˦ Vg|=vlZz;n-,jyc1u{DQQQn{```lw3I~}:A1cCZz&J /)1C 9 Brϰ mFw{Q{æ-}jS͸rzSD"=sCѸ;vL_"|'Ц->E{Ag5?sCzx‹ a-57N_"sӮ֠ED ˏgSyy. ֺ쇞䆆Ga,DBXp7 4>\\h $\#{RAgWeaEObY]cccQV\@ignwpeSm(3HUC;萹-f]ɑg9z S SPܡ9Vs{0r?^5IJ asǏh;*L-N[%?~HRi:j-U6#9t{ w{EaHcccu.Y_hJo{?x℮=A({;Oۚ;b\|&g+7N 3iќikz秮DV=~ k]00;~C''{ AԽqͳȤUTU P[Mŵȩnl|@VZ0cdD'*U ?2Φ£dnucc㑽Y.nXs2qAm-.mhllye IDAT(ma3Ro6iqgkativh2td\#FMGe4Qh?wg8*+xml`)i*GsB[fg.ʬY[cvL#@t>ZX~ X_lj;%u%7:@I+@'v4ɱ>ͥΎWP$dT= f'LrVRūr4z^@eLgkZC >ՄGgPD=\S 4e!i)eGJRvq t474uZݔ:hW~ihCbX 21;47YDB'gyW?:?oZyJ6iwN~w/}{ =ⱻճ"m ;S ~?U/-EVRჷGRaЛ?_͟CmW_ly3pt}D$!jSg=MU (fR">UjBIVԩz6kҒM=준::.+ 6+|mhb ;vfQYCj@Kӓ=͓;1BL *G[P6Sqɜh,HR,UO}`a 8HҘA=7&]vBu^V=RQ$4_EԝKY"m7~7sM`1;B| ƤՓ.Ik*w494q@e=RhLrvB"]s@O{mV>1+4]l.a^C2NV{Ƒȵ3BGpyx(mr+7?9Qij1` z>k0ҶpՉ_>3okNՙgx3'E[Zj_/+0K3XͶ^7f̌ ?Z`̉Nuy N:BlZ>Wakk]_?tP>XvpLr#o}r{O<7j_8p\%aKaa1~ẕ~{.'6ay IgVYh$W C0-byR3xK2c߻UV5X\<~E;CV M|i9ZYkI%{55 &M5T.Z d,t, _-sg1\*}K*oj>ao?ϼv-O?=x]lW'џ޼;OH ,#Vfi솕 Vk7mJ,zi~  r^~x}@ y} <, BA *Q `,H~Ͽ"~m8S I]U =nMLV c,O2R  ϧ|5)zdv85.M>8Y3Rp\difTHϲ3b-b2o%y(Zn:FJѲvfcyaͣK81#r2W64 d޳3}?p}GMλ[&}fg~ud}?xj=~?N+xgk=yL-<ˇv4}xϓ| wg~1qb`>J] MBOfA&u&]Pv.?m@GTuxґS]Kڹf۔ x `YdhT\Kט56mˎ-55) ͬ,+mޜ8{)!*ɏ8ʒCQxQYVbT=Q}h?ImSZC*&*wr7QegoVZ|Tb*3Q]6=uci XE r%`gPP&gjZU]@- ׵d}}}>/<<<22$Oȧ&5q @\— 4 A S@R <~??WlFq4ͼKQjR[lu KY, cVP4*IӲ5fʦJ*5)*G̺YKPfeAjF"1 :=Q]4q[z%˗Fb#>;Z9ZgSj̲;ϩ7N KY6W@2ݏ܀ Pߣ  $e3™ 0˖2d1sax{D-}v5k L?_ |F{rg"}?DؓEˏBoв-f7[jg̠(MZ2c9؛ٺhWyB?H (Mh0;Jau"Ra(ԚY XfNK .MI]Gk-y"ޅ/Iz̪U׺;z;gic26eFYYwiL }v1I.dW^PQmM gs{D޴aKnPX^Xp:h :iTR`L-X^t+ t-Eb^xjF VRUg7d5O ,$'+|ڦҭkZF+H..-vxoYԴ> 0U8Jcyɴ5HUbQj4eSbE/U}P|3Aehq2bsfZ$C 9oUC鶍m*FKK֐Prg*p%ԂbK?{wԕ. kol-N"7?@=4qb\m{}kݼ" ]=s݅'ovޫ5X/5tL6ӾB=&r~Xg9!J m١J˕珕 \>&w[n=߭iww9zi]XETVmc˪Qp!W|TlIƻ@ R =H G%C##G+l12B!Bq B!BaB!B3@B!B"B! ! Ǵܚ ,X`M9Uh更ůl=ۓ_KU+ ,T~k9ܲ,Xzںꕬ||7Uox ,^zom=7kj{H ^kz=o.^xW㶢U ,e no;j몢4~5f*jn]IN\^z)`F򃖡CƄDGw:mr˾Es`=u@ݐn۱}x¨PtaQ\TN[p`LGsrˮH79U;Iؕ777.XFEҡ{a9t^ ]T&Υ .a>rz"{_<R{cw}sc :cд<=/ޑ&8Q"EreZxyɩaUL!gjg#X$Ng]@R33fhΦC':lЄs)iz5Ci֬Ngeu#,"s\RM"|N:^g }pEA gg>V5+iºn<i֬S77o@!͛EGڻXH8?RUk9ErLq(Ba#fϹbc9, A>Jߣ:mԖrqlviUVVVRګo 橶eN2;*++m[ٕofT?O+|v㆞iy+vCO-C*+++H|Sub0s\NA/JU*fSsXej꼸&Ùl@JzC,o:F ٮ.pΡ{礶vu.&{ڑqDz f0y󹣮.zqأ_nŮ-Cbۋ6on뙫#_^0Ԅ-R|xZz~l3lMa6K O’,ApNy{Bzn0l 谱6 nVo11uy Yۿ|#(f{E ,{l՟V$6`,#=.0tv&M&`,OT=`mޫޜ ڗyWxs߄~kUvof;y$ߤ,~볢] h3NEsC&nc;-[ 8~>U{=ϾI͋'Zt BʫNůw8z4'4Y@.qѵ}W7{ɖݪ9#gIE fWuqhg1ۘ;rXflm6ǛNlrݖWA6/. *vn:lswI#S3/S1m|!X;:oy(&zr@BmP=nksfsg6okg {ہ }\榢S <m{7u r ~Ty 8TH& Iy ھ~4\lo9c1xekcn޻C\CP{K!0ZM ۶i BR@= B=9`oB!tp6B!BaB!B3@B!B"B! !B!f!B!0D!B! B!B!B!BaB!B"B! !B!/;/sș)a}...!B! ;og#;/l]Ǽ#B!cw>nHylWJ]TDJ\eVӾqcm6sgcO{"Wӱϝ}V~ӱϝneB! =L5z~:0pUT]*=*ˏK;Ztz*;ǽ [3L~-/歝YA^T']{XU%^{ $B!B3@3alz5jNk3g7XdxM 肋CpO k'].uok'ngۜ'' wMe>=}Jb;OOI|!B> Mj]I@Lw'mkz{tWNxg/ _>'o9Jű]o>gL<9 $~hho'O"dw޳n\\u=V3V?IŁj8]r-֡g &% nkOvw䆌'_ICyqv^Xҥ'N?O}k'?cMS/\0D<'^Lޓv{Bhe\y\`:5:ui8S }ۏ=0u+ꉯ>sI3^GŒ~ܸ<,P-p¿Ktlsm*1ۅΝ>ѳID1y,B;uT%[fM# VuSBH/1e @7,BфuO- z3g}شm1Ha13)S?xÇC\ g.9:qԺHRF'[*`BW g5m#ʉh]jJ㿕,Wj:7 8]j}\#%,\y=9wxjk3w3$^yY &<~H_{ZƟA[߯3"}ƚSOxzv+Xpx!{1!]})Tl̄?L}';?]_8#~zoՁ~(;}vL-Pbe&N~w_ݧMkCkkdts6sypw0}677;Ln~(5XJnrR ~fzS3L>ergXfY/s=wKH8AnwG"x< 8u.UߜZ|ҞӱϝY߼'Rً z}0/LK|ܩH񰩪0zW}tlz" ӿ9qL׳ؠWHzޞBgWY>=cƎYlUUGR>y?lqWbYo=*N2?,kQ~ǰWl9yo;\O^_/9\J͐@7_=̷?Ь $,wT7X?{Y>sHp ?xe +n՜hpׂ;;{a nzK 2m锑2,35eoo;Jz}=?_E>QGLU?p>Y% Ȑg;]&{/'{?7x2XAZA>6n  ttBw DL)?|uI>N'Yn^#W213k_=kTdVL'_S/VqnW'l?f!}Ë}@ˡSOqw8_i;ς\N'2&A+=nDs׈s&My{8q&IvD0}p a n}e'>+#fsaKoeTϩ0՘=zp!4;9;'Drk9uϘ`w70?3'{@w/xBaNmnS|&Lqp.) `y_;zq\gί/~>˔' {.=}}Ndw)28϶?wp2y߶z"t'Ο\"LqLxri{@Ҏ~c+@Ζ;pc'~j -,,ALZ4qoOwd.U.-?ԞA>pqq|ɓ'7B㦿ҥK3f̸OϏaF.Bh|#!B!"B!f!B!0D辺O'"B!0D!B!B!BaB!B3@B!B"B!z]|t z}nkPlAB@!BdSl````````oxQ~4^eϝ; B!B+$>:  _WWWWWWA!B\]]oL0D)tL$B!B=ރe~fgB!B?G2=T !B/Ă@ ?#B!"B!f!B!0D!B! B!B;BׇOlqG MӿկCB ӧl6, [0l"5Xh|yzzN' [0lc"B!3@B! !B!f!B!0D!B!" E[L }H,b܅,@16+px@>q&cSJc$!XRA& \?kKdQ|6p?Y>QN٪I){S $rJ&iZ.E,}lⰥ^//3HN'RbIja c*C meqm Sw6;5'aWc՛5z> +r2E\#LcDj[Oy5 tKB&T#FtN&I-3;'Pk[F2UD"N{I*}VjBT"Iug2X"K*L ,_Z=+3W.65t` ɲªZ|Us{ڿYp5/N|INsܰV$.Դڴ;.N]~㺨)g?=B~KaCЏ?nEdxxKrxac1#62I @޼P 4EVUfF`"/5W[I(51\:CUSVWRh"Ae^qYNԔ_HD`?d0XE{?هi)LI%bqKXEm\밄G76qb; pM5X0joۯ䔩tgF<6D7BgiI1Vk*ށ2FQNP T&?BL5 $LJ5l-^r2K}0&C+!9V"R($ z~fuNiDJQuT-I52`>ε jMQ}YJ,*ZDmԷk J&p\azbLxkt&I*Kjw6+'F!uXҰBYcEeD֛gQκ4O>2~֤-҅TKML)$lֈFcFo*Q Ն[~Z~O@sYmC,x? v=lMÕniC J#$ &P t{23+ CZDSf˲pM^ƧDm&p0Knhb^@2L*#^Lmm6 )z#Ƙ lȈV |"d"p)dQ}'5iUZ-\JnɓB`b4YP OZ,19E4ٔRCuu֘FY#6wM\TW:m\ kr=ΏYۿOzR ˗?Otw8}p Ȗ.]3p^\3g0|<ǽ2Tn/lXsV[:K $-f nرc HpWky`m mf YtRքP0fTH%TBˆj9ZkR"(Y u1#^p+ T,ZSgs]m<@S*fl#DN&[8lm@q9%44r>bMEҾS5d96p$Vfb$H [ 7 IDATח*<,c4Xx2` HFC54ϲZH_V|ر/rLM'i+C9c<HƤ}YH+L _VEABS|!)>hvb$V}Cn4k,5~ dJ-u>Ĭ&>cǎ} d^4pxo!=Izf\k34GƭpC;?¾E獸!\jeb)]45qe{+YnX)s#Rv'C=,   7Zsrzc⒬KOY&$kheH NQ!1Bq43${HxV/) Z9kzN8놯~@LП}L:ɷRM|yN~iv,WulmtO$8CܿWo|=xEP$m=PVleb>miQ0dᰭ4Ni5YiI/%J0u%VYĚjkˍ,r43EN@ *Dbͯ7 l99=waf!š6m3@qo6j'#Z [kN1DPX3u%Y+KbԲV&h21|ho=\R f?>\5&D_o^^WÆ+1|x[.`),I 4VT4.y( ,n'ZՕ5Z X9y,GG¼\B; \X))keM\@N3:+4;[GᒴfGK[iJ`ovV0*nvfwm\JZi"JIP܆tbx 8kz' K1ť2eF:Wc$I@xwp<ر*3ܼ{w Ϭ`?|wGQr5= ppPAQϬ]3NQ= IGK1{I~x0QrW1LFO$K%bs6 f)*F\SJ(MQE0F$k[r9E(,ZhlkWIkb"b41%oV"TH RRaJ_ 0!};Ï!(PGMVJD;JJ* e'k@J0EXfDV#SmVBi 'hVL Ë 6$ʸS)H4|*) UY=1LWK5[KMY2C($;G n.V[u PPb-_,r orPeݸ2ܲf0cZ+wH0pZbiz0s& 8مIFB[],~hc II1kCl.bCb;,A7Y [aEpj=NDHW22\iȀ:U2}ҍ:Hą᭣zOEOo,.IE5sC-X)h2FQ8V soMfI~x0QQbthՀn,k$B$:l[.rlb<,'/,IftHHJ,uO5H$K,\#/k G!kzx_t/_t_mnjz1郁?_pmjŏ{!|}*Vwlӿd$!O Ro0 wqDljY 0Yzf伙hоn,TeUh+ 0M6f 53 Pee}huׂ+[D >2D *L"d/s|")VwZ-Y|v"@H[ #:d*xYS'2@!)W}7l,NTF1>c#k/kl8`ቯ_ ^b-+iPl$c$P*Ї_tkP}t<%ta\g0 0fc*U4jiJEHc֨ J7GЍe  ͎wJyM5r\"JZ 9kV]n2UΚwP\.am53`h>!1M^hcio2$O%̆z MY~)TLsP e ED"Vl]-%'񍪴Bi@F $%& kkא$ʇ8N 呂 ךu{|X&zf=E_/Kߑ}OU~F!!5s)eI/͟)9IӬ 9 ۔)A(K`pB[2Xj). K {LS!`a']meQKd8/[iM~­ w)Ié4AgQVl*cCf$47Fu1"T &θ$#'XЌ+<6rkT_?^Q9+^2yĈB@~|ʄ 2đ2+ؠ֎K|HS#̷1'Ox98>""!C&/Zc2S:syN Q %"f*>kxmMĸr5m\|&M_3L<}}}}}},2 sܹrϟఅƊf+&I) [>T%InR&ݢEHX^t\#E䔪a:Qsa[b]=Y|vEկaS!tHHɰ B6fgMp259O>'mܙoWEP?2P$aApc&oc\Bg&P+B'rUfIBn#}Dm~Q=rR7}(l6aib4OW88WAH</#8Qh9Z XCT!@hQW_3{?{6B!BaD!B! !B!&@B!Bo Cd{ ^yXx1ޏJ

Xt|hzs{.B!Bhb{;»>[6>?]Dwe>,=N);0:rN?oRlj.ͱ#iں:^0ۯ#g.s>F g0ormҀxQ]f`?dkoӷZډN&G!BMY?:@י!k` NpN7 !{@͟7Lw{\ggB$/| cgbF!Bh&޻k\]n]X Ϝ}YLҐٮ}s"vx) w0H# w@q\W#; nf~Ԗy0ݠmr.z6Od*,>~̖uB!j@tޝyg˜Qn0cn@[=jUU4: `! 'Io-:1Yü|w;MS㹲AKK;.sf #{7V-7 QB!^w3g>GĮ?9wlUWЊσ<\'*qJȉ3 πK纁Cf}b3gY>ٮ`eN{㱃I`x-_+*9AQϛ ?8UA/uBf6i4;S޻e9yu= Y67G6j(bܹs'$$ B!:8a B!sI ;vaw1B!0Nx)B!B?"A---?ǏVOG@9(2Xopww$UD1"|||M}so߾=cƌ9s渹ay===]]]oߞ3gTEݺukڴi3gΤ@obuܹs'}z~Ƿ`ڴi~͛71"43f|͞;::?wiz7@& ޽{o;`xzzΚ5  BB[5gS9vuu͞=N;9rvv1cݻw{H##zzz޽ i!Bf3ܦofyЛ4}t1{쉰otss={vGG|R!Bhh|&ԩq<= !B! MUbiԮV.Hz.+)o5P@pDKMR'n^A˗&2՗ݐJuFšF! ID[V L&jJ;UrHZuձڪ-1OKCvT!"FhsM3sJ爥 ݞRpVOB UU J3s2kt_Wm[U;!J9G:dryBPRU!H |Q34@ !GuqF狓REB#!DvtX,OCDSrP_Rc B։:{JȠ"!24((((4B}Rk˳#7$;W:%积߲%bl(ː$&uQ[Wi/h<."t ajE}Im,((2_;X$IET IDAT̞6 rT8%+ko"d`Rד0Ug ' ar8wjn?IY]Qx,-i} krR^V—Q* g WDۢ]=PG9L&?gue[2(}u޶!+]J cy%Y]eJXwA,qzqE[@zͭYfVxy3RkC+ܹsX hjPj)fx95T5ȸKf@K2Rt˲RԒ̪}I?;|E"xDS G֮))nIX߼ytЪ5&=ID`͚%I,8>xIQiҼV&ه摑kkXH:U}Rn.cjP'hj %PI8nC9=Yz?͢P~QIwE=Y͐0S#$i "IWJ֨ISh|@BIFL^/h- ;E"!"J!|%ju:/)_GTImT;;*3S .Ly&'e%b)AkoT4sʤ@: Ԍ2eDb6rPSkAH2CԘX*^ݣLGlic3~DDoJnXNq>pǚ`[A#ޫsb>,Ԅ̳gRUqDz`jA~)!5Ҹԓِ-*. H>%,, ofZ)X,wy+ըr]' +))ُ옢 @x>zѤP2b0=Y'29KjuI"ECsaxj \ ME21B85vC_C!s&,ÂR"#Giă_|E^^~ŧty8gD3so O), lj' a2,4F]@c:& ۠% z׈tHVZHEp"N M3C UPUR5Q #Q%xhԪ[Hlޒ-)râY!GepVgeGZ~i,xN̬*툈4Uִ!)ױ|Xշ1tљ!&&C ƿ"~ԻLc՗Su4~:;C~OtItrjűZ3׈sD $BIξ0uOpLk*(ĉ5nJ χ+PhWL[Y*!# `'<94ۇ C톈RXbӓ3hBpMONr(l{.՟B}d~2O%Xk,EY7/J*[R ͊NV\!@C~\RR8<^UeꬔaVd"D]\Ol7X`ptԚXO%cgu#~x=!9~CyL|]$X3 AH0g@q - A)>uZ]I |).OͿQor FpwZ]8⬔嘀 t>i ck8|/sB D}%guOѝ.29,B$6W yAZ. K<"ca}]\7ɰDR?j I,g䳨zG9@B_%8޺=r`jiVc{c~'&݃>=Wp|@\⎃D'( e5ZZbLmyTc=G_5j2$eR)'|2@ %dD$aw0՟l:2_0۳4-"3)Yq!;)(  %HKC(㉫H"1LF:T h=YQIY0('ehk*_>O%%s۝oojh'c n~{W˿ ? b7Tk)Β@ BĄJaRUOwxl!Lh)swy!?5ySIR@ʣn"jIʨl谀2#!{4*T)'ub <,*U"- Y,[9OWV`,c1^%Eq@4tU/yW~pΓҁ곩Xօ)meVdv z$AXSSoNxV~<: 48vX *K%g[j @h()$Pq:yxٶBI, 暃U-uX 3kreæATKƒ CDž]%"$k&.URRI^Im .UTx? ycbb읁h]()<G1o |q߮} 84$E[jr EI%gev8OF$d$l>A6T+i zXZhTBxPNjL2`z8<#sI&["? dYkXҲ-!b5O+IzPF|'%}$5mkjhQFxlh*nLKtAxJMG@2`-XkS"Y+ ^~x(%W)&-~t>k2" v55 'w0Lao1r7d=k]APCѡ~P&O1-pKRO ybDh|kT=~ׅgktR1ODS;y#Z\!.wL]s)6 7zN-PR*-Sr6 o}qH@]/D@Sn9ҩTűmǀr˞tCj|a[Dv1 ',sA`zM$~~?\+>4L8j~6aC-=t`n5ULg:yC#ư~rZ'}n~TYJ(rҶx˞e7ΏSL(Ufn+huzvR`LL$*'M$744Ģ3LtA\!P,'2S]0X8MqDbV2"#vxɄWq `dþF!+r7;7b]"-Z 5Kqe~TV qIˤ^=lś Ʉcj$l3lXʂV" |N, -ꃊc{.!zC, B _8X :;-J*f6'`]nn<!B B1Cŕ]{g'V`\j<!BM.x?@B!B!B Bӻz1Oljzxzzz B B]Gݹs;w|G9{WW[z}p&7o|1l7_ӧGN[)һFa%LJo߶GA7777ѣGs̙`0f3įzfc'}p%LfbUL pR"bΜ9o6X!o%ϙ3 \F z+>| yٳgs]O=3&@Л g{۝oZ1}#b4!| ٻu:vG7Ht섟5kLb ^L~Zu7(O,͸^6 N6m(bD Fo+b=~֬Y㕻&LnX C=z4WwwwO6 3!4!z}p.PB6k!z'` llXom6iiiY`̤cZ= BaDMC[SbD!&2z]ifYViso_!;Ba p( @D!&2D[:rumgvFG!0"J}#=3:?!r( pĿJO? !&@T~'S{#Bsp&GAB`ƲGltL0xOBhb>@ /4VmBM|m:+aB>cLVgX...NBE|>c B—jO 553hXumqBm@o> VP__ÇDzFB0Uvi4@('uqqqdtrrD!&$DMfMa9;1##]]X}!BBh+TscB+onhBaD!NJuB!^!4`^y#WэX6V B!x>@ۦk\ϙhma!B Bhі5˂B)k!B Bh&aa';81X[x!BhuK1&-)7645@DŽ"B D!Be=zDQTooj VfYVgZ߿ƅj%I-B/Օ 3fh4lMf ME]]]AN kǏwuuٿNBbEQw6m\rc777''''''ONoOhR~3+"I{4 -B]\\HgXr Z?~MdݖfJfsww>fx0_wO \&lř`bZ?]]]h4lt 6[~B;;;"4e-&@4 [.BSbDSj({|7f\6[LhjaEf- ۦ0"B!T !B!0"B!B!B B!B!L!B!0"4˾X.fˍfMޮ-?\gm6RBYf0h $\jO[(񏵯]!f7= B!&@Є`֨T]]y, f1cZDuJxBmޮUfͨ-UUv02`oo g ='H,L&'WKB!L ى:C2I_ NփN=9:8_k](ܮ1:# XcfmWikB!L  XAtP=]ŀf6pIS(8aaa]]SS.Y][zUlb\o47Uufcubr\o4MzFpjjMa0}}jYaA^^oR ceFZ#Q4BnyqM`YpS$vau F'I7jYSL5rcӿIU7eòÂB^ng_ójnV{8ira^oԗ˫ӜS IDATIUb` d˄zU9F@B VBcpZ&WgT In/T%lYD1ƌ:=DЃe]BZk0a 7V%dpUM`3P!\k`?1'fKkہ1+X&fC5jf' K p w2ֵ¼ČFap{b0c`D\L]6bMՌf:Ar jv}^tlۨRVsê(.x0V-o2' ؘB۴iӦƍ0Dhl֩eT7;F8Zg ;l6f6;c\Uf/a ЫdBP(+7Z1fx=l{05=+=1:.CFYflAbF>W"oR 3zëۅwpfAry̬J3jr[/D!"4&4:}j}W{6;?$'%cVgTccb6a2z3EIhc & XA()3'ofZÕH cwd24z6ޒZuc?\?C?\{i4d7-d:dqlD;X]`E~TYt6LEք乭Mk[VߗZZ}IGR|;yRYt^I>ekVxq u#7fcE&GxU|qXο~:#,lܘSm{wgɗy,Uz!nTݻwޙ._ļYw}ErMSJ]$ !L4ZnTa:99ܽoG1&jD-<  c Y/+̰YD?W6_8 yӗƽ,׋Ov gUs)s %H?ĖI76@ƆFZRYkkG:&0ogzw:ak 9j~^E+1ס@򔽇NY+ˍ_'$ޤmlνfI/~$eU|V-"K_e=wY]_XБ#voj7Uj)A,R Kٻ?k.f¥'n8kF/b:+O07ܻ7i>?nZ4`unCˑŦjƴvÉհ,}C{$ߌ~mQW$kօ,é̍m> 9?k-q:g71cy\sj*'a#WAQZg""@T\\r?쎢+ܰVMc+E)F}-NAE; Nܽ+U%ժ;OK~ =lm64y0ޣ9cX:o <^}(bUzH;!hu=,&/)KۈEYYa,୐gݸTy~ÞIb=&EN~u~Zu>kP|e}՝LެtMÍd,&bmJ3vGuQ ˍz&,yn2 (a|Lh2D@_VK =s Y5K)FyxD)aMLO1)NIl~:+e6zTyX OOL˶lHI8\ud6gyZSR~mˢ{b^ ,,\[4/x+SNdf*W ;Y RQLKyV "2OׯD0驨ce?3V7={@V$  p*u&?Ao7xkząg8]Hs|~7Bл<7)Y1h+L,tͲP]oW.]xFs"|E~┽+xڀXI$kr8}\e/p9C][Q:-mOw8`0/h(ް-m=P}M}Ol^`Psr4>: 75z {r3(4;2`Tz/sڟi"xVxA({'^C-d~n,OI?e7+LԲ)a0"wKNcq OkffOq9 f\=`6Zǔl}fx [A{F>Da(4\o2ZoƖX {/\rNZge:gQiOޓ}W*jȰ0B% n49XW:th4dkLd2Qjr2YZM{ϡc{|^{Cʚӛs*V[Ct(ғGL1L/8_&^¯&@~6'F~xd.Z?fV$OX#/zwS֮n[ymΌ^˜I.3nN l;llM`lQl =[;2NoG ŗjȅF9~ZCgz0<{Ӆ;6 ,Nc +i,fz@S&vG0t^K{t'sZ{(!BRs |V9z}6'b(dn|rX{m{W(J5Ԑ$Y;㽞n*%}IW9mN LWˍ9&/W[iN骽xwQLL^x.61/[KlVEoqzd^ vz<73z65[$Adj}!}'_ l6[wT\#XH`N0{ai̜S;̗`84D=?ҹ7owqNyKE#.ӱuxzDwO6E;x:xO~K<Λ<${n -FH& >nZ¤\7W 8>B._t/og|wf/$ YI/r>=,0.(Lޜa'q8>#\I)ϧ+ $ńH𶟛it OV)U"G[Z/U9E(eOݰaN_z'u4YlW?3rg]WUuNkZo}D>>s4xtޢYhxHh&sbOY5όb۞'N;wxa?Ĥ6 v&h֢5^iG/h7^99&fs #ȓxQ37n=zk׮]9w(cWe<G2-ĕvCG36|U:2O]R*OUG}\B&85+W\rܩ{R!&<5?aޟZa t:3vCS6e<^hO=qƍ+ 3sN1?FJ_3ӎ$a1"mϩkҾUo> ' ASqi0Ya? $<| +%c-Lݿ[T~e''햯~GYV+iXJG]f !JJptcfK &䧕v Sv6%ėia-A:5ʹ60l#)X,w yM駟lӫ{M6mJm쳿XVmƍe˖]#h4P(˗/ϝ;d2y{{;;;;;;;99h4{+l_}tz~}4 '4uV >^:ޣKLE̮AhMidZF|?d! 3ZRM=1f}%F"՟X }q }`ᆔ}뮩 NNN^G֟zF,nΜN@ ~~=[o7t¥=֎BC`U{箳>V>Bh*ƿk1+eE\q=8iƿ BcKO9<7t 4w&MK:?A/f-w> W~t^0cj塭ZlݴyPux5ݦڜ!@_7PP~QW(Iٛ Bo}|ck)t9K 6XߵOpx]Bh f@˷bE`Dh%@L06jZ` lV+I>n7c+o.{ڴi4mj(ڀw/TL@=i4 lkj;-`EEֻiLrOXw;Nغz4Wӧ1fr؝G Eu.h'߹X `p$F?ݽoP=f$B Bol~ j!IAq0.COWWW'''gg:64=ޗ\hhغ~79}mQޡA7mom yu2:pKW~j?͍ mwwt3^vO۳fZ:_63!0"4%n''8:Ík>L3=47߷ܤ;8:G>iӧ ٙ<8S "4Ј@Lfz}!7nϻ{`''r9Ll Kt#tǶ{6qw- lޭ;ᱶw6Ss\il 0F! g㢸ξݙedPA.A@J /`RT")TLoIAh*J!BBh1[0&԰@H@*,,33c@b[~r朳s͜߹9G1ApHӷd.*Z>euE&] 9ȕѡw;T*\\hBNQL&:_&"EET~`޹ 9;:<h\]~TU9j͏C^ Ew'Wm. \2 Ig#,t< gϚŗy4t9pX""בݖG?l9Px^||/C/Z/k[1^ ?>-%F[}l߃{p>v U"#&xg3QJe/,|f-g—O$E/EDnc"Ats?O;BaxB#J\r#H <5pX]`^\\\ %2L&#$b4ZT B|9b95t; Jtk( 3S;ͳ 2E wiܽl3=k<{xzmp@Pɾܷ6>ؼ%yΌ `D("*@GH DdJZd _}o/DA !@AF^&DU"$7sCA'P.JRT)J*3_*}_01me)着0D&N*WZW}ߒxLLA76?.4silV۞.GnL"d"6ب{N?  b=| ZauX %0l ^jHf` `lOCjƧ1?q(@ &i,^/.4D=&  sϽZ*'~+’qzM`x*)mH. AyjUɭ{bCwH~r 0!bTBVS˧<ehp\>WCrpIGmsl$Q@hG偣4MZr>U_,b IDATqWz }-(q*M.m*b+Kk]E1scwFFVĨEX-V @%JdC@t#ɼ%y̟T+d#޵;H >yw/'FwV|s@V !ω 5  /J+bbZ~0]gv?R *1 c^16-(BH"֛ȿ a[˶ٓn1F.?84T)U%r燖/3ͣsY74ՙGD8{+$dm-XL^k͎733_'"*@M;x~jO]?+<鶙X$Y(H6dOUp/ksgυ60Eh6dP2gEs.P^?czj]WgϿxRih22rU|MmKIdd($w: }JLoЛTYe0 iiU]O-έ-}e}2L tvL ]mjZ l.U "`h̴e&j~{f`0MU՜ "^?4W^3Vjc/\q\O~s \]>gnP0ܶ%6ץcB۴幭M+ 4U,;oi:⤽Ǫvc鴥wDB``yd#~r'>/a9`O{##Zu7;G5׌ ̢guuk_sUEv쉿wNSVͧ.O}|sMv|A!0a7I!YΟ _VKI0qr; 3_<;{%K"?"[#lG`G% "3Y/;}$AӖY`Pݖv^"$n*rlh <sǝ} @ctp3!>e./^ it,ݛS@X\ԚP)ihDB@,~բ,/ f &)&9ŷ8ioCIyGzRqj󪻋nl; t`SIK7 hC/Ӌql۞O:Gfq&CqՖ1 #kyΫy/I1:k6mnfS;w7HfjKKl(;vdWEDP.*:,.si<ٓs  THExJ؇/ts bbZ0.@3s sk41$M)̃j.UͶ4>MT:G!L&dFͧ^QSI 2+&\wnoMoM^ntzH mؕ= \fQif4t:Su&biM6*MեtN-*su\NvԘڴj5 Na*rqwwsu.\1[4^%Vx_j i=+K/-q-<|wF w{yT*S?pG,S{u& 5Z_7]BVVR zvv26,%''h}Fk^d}[BLu<=m^cFm[Q^ѣ)c cB 45w xCNlGI@]cQ4H6l͊,+naH 9Yo:۲k )$%mX?Qޢ\a[LH _Ng,PspΩ.q@ \"WHs"w-4EcMW*V>w9xjSʻ>Ϝn't*]OMyZ7tݛtvTe737ڄlin NWdɵrCErrղƪej0UEowC̊L&F&gF6ƫS962> l]Ui1]UuMЬi1 w{J$xҢ23x7mslcއo>hA;Mw P[x{IM `p<U@HxK迦bfݾCq~ܵEEx[vٔ8|Bu kj^uP![W5:s)5P10H[60 o )ۓ1P2/QVQ5Pڤ$kkP۷?2\4jWRom^>x!c[45U99+e[B(u[F!iߡ?(Z>$T?$nIh{^$E{? xy*8, Avs{7ӝՔ4˴kfW{dQ^?*նW{귣5ʓ* H9iOo9$<7u֛XIY*JֺvF}'5殛Zc1Mٞ>0PquayG [fhj7]Wibӝ@\Q&l(_6p؛I$ c#!*ԚRH 6z@o哫 k&!}ߑh07m[XV7Ey'˹ogbcw=k7gOr,;%{'ټ0-IT""7sXPФMt{dx1m8;<)sX, ' xŦ/s'Ώŷ pHL1? j Fl9@VϮS @@! O_ԧ]/u\\r9Iʤ$!H2ٿI8#"z޷Q]EiiZ5L -bWef9 6Lnti>5SU 0@Wvl&t'6VhAAj0Z̰1y24u-(+544_wM̩ZlrSmjr}LUe1-R!?S6 8|ˠ:z} ؉'~ѭ4<ϟ<ݶOww^7h;}6p)W,FU]k71#6P.><58cJyC. %@;Km<ګ Y(`; lGaI䦚66biC:P@L6tY0OT0g bu@k}5&Vr.L7 qQudm]'5{(J]/}퓟4d9 Nw 7O yyuy/H <4d].O=`{_.冐 ]\B)W 76H#^su~\:c'Hҋ|~+L{4Hִ ,ud`J֊55{ƝZmu=ڨ bRŠ _9,8!T!rdggm 9XjZ`PaacIҚRuqa_6v8s%DkqSPGQT}̤ܐ7'F߄(? |tzxD("rP=p2A $s Eij\˕jd#F'jĀ#xac  bbF(rG`GX01G,` (͕.lnu\BPB,Aܴi@!p_G/ʧcI(T"(RI7s,P {/(:9<27U/K۾}ubr|r(>bC$[? }A*^oʍ/Mfdn8>-&uY}f|Erj]rQ2vYiicbf.yWE\ퟖ\LjP滛8?ZSwwur4xqt3Uk !vWhJNz¡g]1d4 <'23!33>n >K`vt.췌͕^xL @Tp?לC7!Zcv[0&9}ޚh䘶Qa@00Å%5 q17p pƃW֎E4@4=zfHx֌5mi 7\RXpdčt ^ ^Ao`- QGvo ?xߎ9~s pK^9a p 8 59IG[gr.fpW;^2zMTHJV삺ѫc85Kjh@$$iVb{\_F#G/F3}hhf83I 睐2<;tGVS-'|7r+Xm-d,ܛT% Yak 63cXn,.joC~I7.ÍwL/4M߀5M.Kj4$g|,͐Q9m\vi5~w' mc1#E{g10aLRegbBm;1a@  `tb͋{5 0v a (T 4 $P\d}\r hJxnE)Z^iw?=Gyt驔Ҥ v\V| IpUN5;)2%eڪZۻ*ruAbM6 <ܧyjHR@Ȉg`R@XzG=ezt|tƏߩ;%{J>~qx)fM^𓶎+N8wҁO'ƪIn2zm[0.oQp-5P₍Ι؎-78{Ouu f} 2z}=`v'%c kY0VV|cub譇e}_TSa@v]:ʵ\KMmmΏ5:w'@ "X`6:՜SjȐs_ZF| ?Ӳ&j?3y7RTK7=NIj:XƦ/ڜNs8ZC!5`36Ti0Fsܸ5Qa45ѺI?w~דNH Jsx㯅՝_ R;xjH:_*0%'y)Q QZ}7뿚[C(5eǝ6—+ dt,m]d~H6l]p4Td4iХ*"kkO~@ؚVTmZӠh}ɩ-B$D("rcrk ,rrdT J2\ei9"$`#njZY`fatxHH^T+2\.{Ers&':ED5` @gЬf9&fp"# d+O_\wPJo@(M[_5h&4!EDDnc}ҷo.'aSvAM@8ED6+@1#1 5*ktr@)B TI91(%+1p8v;2%(J>sNNH㉙ νa9r71{.S'H\i*_Wa<ߥo0IH4^,%^X "" -DhE("_ZNefbA(JJ!WHRi"o ?2b6?1vqq1c:IQE^>9ߓsMa  H*Bw 拔^xn]fYK.#*x%00h呛/YDJX"""""Ua,;42~AoޔL'z&MZSC8'+WE)\$tB~Mߠ%/~ J~3ϫ`9 v؀1c}Z/ dJPz¼GѢ X""""" uxxoFi^^1єDBD*u:.09X,htF,F`39RHa gj<[vi-C&;I HĨua±MS EkD 07_0ԃG䙈@r7pbq7x PD䶐0p0fs˟xlڔJڍsjeY`^X*PŅ$I 6ba8< $HHT(rDnZ-##y3 v耠E*$I4EQDfca;x!$H $%wGR$\f2_ăg| ,W3# )=0=gj@{EnWDDDDDT"" \>Ęxf;JT&ҥKvTWW&_~)~;vs=>idˋyEWweeeKK 888瞣m6G8oN}Opv#4cwy'00g8ˋH-(+R__O,_̲ R~}*"2\g!Yw}՞EDncd>@L*}0 :1/0ҥ>;sthh(0|ÇeRi_'M%ٳ%Kd$y'xb@Z ƀAlaP`T" S˲##Á;knb\>gΜw . L jvf NH5 K\ѳee&%X1z[+KFRKJ֗v]okicõI/ MIѬ)Mz+n6p@I u; k;G 0u'flf6?RY34wwEPe-rͱu: ؚO?Dܠ?@ʲ,#7yzW,hmk0Z,1 >Ӽi1]T޹d ;F#32|dғOno;Յ$! }t,_xzy(9¹$ 3r6d_?73>'7w˯]`jseVPqp4{STZԫTzy}TfVH9ftm=pȑY 5@\Q&nog(;=VwT^vH:p^1.0!FV<Ԛ6MTԀGpMJκN3 /hok7pG{csy`wTѓߟb5M5Mf||<(..hm=pcqvaTHI({͍LM^Q߬aY W]b"o""7ve۝@ dg&;rȡ %LfݞoUi(,`TpHU޸ʆtH E߬ڗ.07d3 {9rd:2#ص<*0%'J{dJ*?_X#"5ٽS0םꃷ:rȑtߞk| &{wh0p/LQLiچ2+le*# \[ $(VYNp[qD("rC d$32"!>\pQf{N&y>9g pc;w;' Dv6ndFFVvN^ƴ4!@!g<  3j08FHJ2J.BN$ U?wEXIӔBR v"rݠ??bV]Ip_4B##"ra덾Q~D%HW&i;&*.XoXXvSOA[M'ls[u&..pc ~괖N- qQ>@D%鸆%L'о4c0S'yjiJBW{9h]3TK3Fn? <’I}MuBIJ3!%'iR?G@cxpbm͚5#xe~dyL˒f݂C}}:ӟarŅ(vxXE!kHal} 4٭*^B"",!ǚa4:זzys5ǀSPϨ^hnHq5GkH'*!7.SwȂ5-F Lf,s$IpS Hr4r_DҖ$0w-[oԔ%u3@k_IkcetwLJN71T_ cd8+kW#1d@$J]IiIqF#Ǵe\><3@ͦ5m *AH-k 7UI zä*S!n=XRY OIOO h6Å%5 q1cJ'ܘ)MZI8y} PD俑kĀ8jqw h4 cl+W|Zm|92*JNQ###EQ.hyS328c6} T3ݻB3gܽ{#$$2frR񛜜[[[xAZO)BB%6bpX EP%1Oy]TKSpSU~KsK6}ZPS'wz0_xgUЅAziehjr*d)FPU=/+(2taCwo$",4:"̹lw8>>Y=ԮˣbFF8΁Ѵ.<CBۘÑNeFeعWq:cz`^h4491r&T %G;mڸk_0TC&cnCpzh)r#)f;53ѣJ_h܆m GF[@F vV+I{nN7)&?V5ҋ혴Ȍc4rI8%~vGYL-=tm--(I[& Vm.0$X~v9o_+o(ũMa >4=y @EDnTDQ,_{⩧ܼ5 @ۮo%bc"JctU~WniFJeMKJ+Jssmiz+65rs]AɻE˸`9֗ܺOG: 3fr_-}k}u MC櫮+}O?>v]wv|󗮹g>>5 T?mtfCfg/pB=GYh|5n`؞-;F mf`;kWz*]\sRo=Z]l3*Ag,,nMJSX`:lk]>k3sPIGhUSO=EMcc%Nkneã})L_Puyz׮/lcDD*]\i)sL9.]mdsKyy(8P|۶eORy8QQ¼ବY yMz;zԺ+7>4-}Pm or v7ht:e/@n e9s ^|y\\\$I%?т `%;_Ӹ.?$1JVHt(rיY08˓RT(2E.w ]L"$=/ny^NɕJ$Yկ8ɔ..JhZT<EG#c@!T*㓰{7.(s>Jδ?-G&.S׶l`k9}hŧ7noU#eɥK]'7T%Meڊ_쪭M_uzbZEvWTK#sSaYnmjEZ4˞з8?0*%pol_]tN01n^IIxݾiIqU ϕS׷nT4=c,~8.gEvޚGJ4(m'P!vmZY?iv" pJQ $&*5[h@[6fF7n&-ΐ02ܣiMavΔ3:RdE ?"Z1 $8!Wz??~|$$zZt^ @Ah=_&vjE3_Q\^=gVrEJ{’s sBK*yn}ru2,yL}`32Adq5 i;Clj2veB55@hӹ5 WxjVsZȞ gY7tn: 8Nɯ(k.3sEhnvEd5 n]])esgH[T>+SrKw.3ZKs"U*+ΪW_Wr)~>@zzPTR DH;{YRT(QBT*RR(H$ÑH8*JR8h4=3FH$&GbEQRTT*Ul+×)c<~vww Bri[Yp%Co/Pq.gaIG IDATKߗe1Ӟ΢KTY01&w(c&?o%:e=R;9Ry=f4W`t>*7'N矌8裏&N222zv&RPv+M\>. [C_5loHK͝z09+oԿΎSQ#7gyBٯ#_w?oraO:FM[n=خI?yf6Пw~ ~6o(A\k[/i[i ]=g٥]H$"IRZZu;|[j[:m?1kjvZ_ [flCa국஥Uۼx[lDb/$&ꉙQh4 ''+ RRnK1 gJX'NU :Yw`CY\b̟ſSΣ#zjJ[`8#]0IYԻA&Iuze8$G, ,u%!4ǂ'pZ~ſ1O<3 ƥB㫣*3ڣҙ 5GSOK'Ĺ|=h0tFHHLqZW&pxsBhlv]`ݺIк}K56۵gF1Ԑ К8; _% IJJLLT x#M gfWO,xJq$EZ4K &L)紖8|L,쓍&;XYbX#<$4C~IBe?(ʴc3RK<_&t[璃7o{O:e惺AghWwZK|h3|s9KU&aCBWvRQټm ܼokWMeSnٺ)x B7e(ATjjzN"QsjX/{uI'ɒ_c\g:Z&TML칢ev׈E [h@"g0p"SrF }hԹ~G= |и+Z^ {3.|sJ}];-[YwK}I.ZZѷђ.m~ύG&@.-~ UBB4!ўcu8Eq:f3#<r^YJV䱰^Q&&*q=QdRblO) `fRos n3-8m\&H$Ga1T* '"MG'ݑq HHPGMy k܉!"AuM Ho:AN<<!S?l;Z\Or'k~'X B7,c%\+ȖnJ)2o6ڡ UUnK@&D;5:\z5l&Xyb DTdk}Cס3g:s=EeOn+E"GOh(}C9.8vwG"h?:A>бa?_~!ﮖymF'OUJŻ|}5\P;r|+ GNm=51x;Ñ蝷*B(i<UhSߎaD=w6W|#ɛz%&4h8y7 R1~O?~TWG7"9i,}{2|X`;RDŽ2@H$=#Ot%$$~[1wI'γ2? \d돋b+3"YTHWs|[49 !ؼvEr{Mk3s͞-iO\;{mk(a}u?s62gPޚͶ]B1{+6 9&o|ʩPt:FaDw8P@6ING W)㩤H4J$ҵ'v穙 ^ˣ]=5SaR+xuwyRsi^п뀼珿rdyG%O]enu 7 < V<㓓†2t}FǏǏKuGzi>;w :*#mlFKicǤ{.\fvٚI)о׳|Mg\(k׉s,5(i&M[<87: 4ݵ|ה=6BGRM?R(up(?O{޲pP:0wK~''3G#y'6#5:!cSuׯ>|LϤJ>n{pԉղ?c\W'>.;>fF B7xDΣkͦ@ųƶnIg ߰ !t{vZxj,fhL[khyeeеwKeFrEZ]P~HR QFOj-^.}tμ҂I긹lp]pε5!"RԔ`9: ZD)^eu[*7Eeq7%( y]FȭIdi4vLODfM]!ta=;l@8ms#Q2yK5!aτ YRv =HSp|&s{aΗG0vpWcw\z{[4y*NO`kCu6 4\Qiiavr׮OUg5wZTuֆK>omZ6%Q^fSg9Jˊ$C|}dOMwh9mks:N ϩ\l^;wa˂kgv4y HUwůLv{eb4YOY.beVYi0ϋb@Wj2j˫k: "3oQuYk6-77DEo}Gݏl墌GK#tQ*-% 5X&`ogwOɺ&8i*׿*M˶MNׯ.uz؀ױz}[76jhꂮ] ׽Q__aeY.-ezǜ]{c{㖶ޯ7v_V7?W|Mnwě;; 6kkjBry%U{キ]]YE _v&z+P;0eKc{--ӯj8BYAczﳟzތƧX;nK.qtX:fM0z*33ht}S7cB&"_G8p_Plhs$u۵7M кqEuW~cMa5{z)Amƚܶ)ꎆK76[%bs}}+:ʮx~tbSr  mA`[Cc+Ii]Qޘ^M˶Xkj775 2xM]] \Q 6׻7ӍuZPolvl eVKs6eԇ&@.XQZ_ҙ$_פ1Mu7 t= u@Cks?ZSc3Ϛ>M%?ָXvƇ&B%mu {Zԩ3^<%9R9 j|COK \'7։vPIg\QM -ڼB. % U IO0Bj?mZ )܌tNTPHl˦Mtaa-~ڜ"Nܲ:ehqǻrv _|/.C~;_how|^<3RG u/zV?6x'NN]IC}߽hָ_;'c4PSggsG09h w7х%#03{[пDfAl뚴yS 5Km EExoݐS[D2WiihH-(]XQ#*dAYGw@̞/E@ :.| { `, B/B!::1T,޴*P[%|̃0yjٺҍ*WDfނ3hZԬG(@ =KWgꚂSuYrR~B"\TMjD% @r4woٸ-@[~$̍Z -}K[f_+ T5.Dn%w%&*O7dܶ}?ſ|H8ڿ ̵?A|**0h*ڍjVhEsV y2d櫓wW''PSY=t1 zK|d T7-HF)㒃mh[? C`Qg/ZWu㖚myEY2BP YU֤jBdH͛,NŮ٨'k,IɏL]\qʔS BWkCͥ;=, @'GBCEW{k@Ckh Syԋ >\aNezr*Ԡ֤jN-Z>c%+6ϻ tvb  @.-q6B@yc5df[)QPuPu@ER(n/L+6t`'PȼEѻ˗lٲ|aM_7=]NI%\6v\u $IyZvc]+ک-x_X/h^yOjfRz tRйH)ʅ55CE9xafk(0m5tfB9►Azhrg !ƺEhV9aMS(}Fi t\|!/ 5: 'Xȋ9m[vt]nkPX?fOIϤ5@\ʴlAE5ח:3?v|t[YYl^_&liiinKueOymk6er7㌱ " w&_V!TV}<itx5"t]Sg[mX_qٜ-ZpJr{Η<`!68(rڬ'KLR'Cy gUzꙕ2~LF~>aMϹYJ~YEkeY s見9+ZU:5V7pzZ6=wEܲe+&[!uVaf2^FAAֺWXJJK ť)|SbТU,FZM4[B/`W5Wϙ"47bj*._zJ祜+dY9b3*J &ⲌCaތ]sgVA&PZ6E}1+Zk>"vhCi&O/h?:޵+hĉ@ ##CRT*RP(bGq!H?Fܻzββ7'r \9Xi$M⑉B!uZHM%kc/PB!Эcҹ6tUA1"B!n(+STʌ3n^!B! !B!&@$BϿfLn]-/H1V^d-]~EI*8%-|5p*^Ypu4 iP$56!B B liU/qG:‘Wpv78,G~~y7 ~;d_gpX9Rp~Gʼv^l>B!LW̼& .ahft :)Nna/.sQzAvc,&Fp9(b~b@qaߢ24M3i`E@:'tVy^ hڼH>i%2oeYstvdndi(34E3nS:20iP**Jt#A8̑By>(vG !BB]&ut-vh\~Qu4[6˃CΜeռ๞׶B4ZV7q`DݨMV(q\Wbё$k+NE@@IO, n_g5Д#X1IXKFNty{JaѥS0z_z3Y+6ɚ4c>+KPQdOM, @jz%HL9YdY&ފ I%9Y}^!፵W}yĤߴ7bx>(m xOЍ66c&ki]d??ʖ,|yn8rQ%~"X}/D3CQEQ(eCD̔ IDATp#ʂ-) I$Zy,kJ jDݬX8#OKbiGi@3]>Aܮ[gȒ(4՛)J1&xl&˲8I&4C KtJq6,[Iؔ@ReYXQ$P_F@Yt\m1Κ5k֬YXV \aڃܵt3+˟i,ƵI)ؼu\h.۞hܳra9.:bf=Ա{WkW>?k]JE$g+LN)0" BM4֬ߗ~QK֬{q_.ZєQUƞM|%H=dge(U'8=%, A|U@d}V=jr68rI@ND-V%GF@^l")ElN#hS2ݬ '%_1jx{ԀڳDFGKgWX!n>{Woזn\6s[c+@kMoTLo\m3ּ|橬@cC[  sR.4mw)וeu·Mnwě;:]oԯUG Be<&KC-cҒN~uJ!YyL&@M &KtPUb f%.6`LVg+qHQK~wDД 2'(άi6;= FLƾ{Ag`z]#@znd8_E h$%dqekw 0Z=%&_oSd!G,_emsɆʢ ,EB7M&TL.Zi+Ȁ -taPO2e {~ b9\O mp\\WSC6KH-䠩%Zd7?6cvDMtn۸3KŽ2$` H^# ˸4G./g0p*7,3;\7DP:Nlg6Y[bmwf!Mmjl4wz"'Wb &cVog2mr:}&C2HVw$xd LY*lQVHti>ØlFKPbzh5Xt>mh;,#=@rU.VYF_,$c`D_`,N;o2 +L6z#~, !t(ZJv㲚{rQ@{ϟ16E(fv4ZMlX2WC]#q 5@g DjzFKZ!A 5ưjÂ_>?[͜kzMybUaBף ]^OC$crLӓCOf,9tȦ>/cbdnwdUIO{K\|ɹYW.[2W 0?<$ŦyXSg#IHn`9VY^d)U6"#eJ)KrayeT `S' W2Wlhj,551נqN! :CZs˽զfƠL_Bl(Bj뾲 mTFdg ?m!_B/{`@il[uAH35HOoݲ kOr[]9 rBL5\\H 2۷]ky3װF:v54=a Bj~Wt6𘙛H]`4Ӥ$cr]TMR_v8,yKig!n vV.y&D@榀|άh2,.z'Z59+ZU:5⵪`˖jbUޔ5@!˲B@D4D"p8B`0xiӦ]裉' JRJBeW۫&f+窄 #݈Tuر ) quXh1X׫H$"IRZZ\cIj/>JCg5$ DeaE|(cC:vw['6?mGWw|ԘL!ta/PB'N6+7B áDB:o,ѓA|TChB'? 웿*H&'vOZ#jV< }f?:4.BW~"B(i@}Ф;hm Ha5M.l{1F!o]uyzI~Ӱ(nO@e}wlM4 BaD!բzpNCep7` 1x̞6}v R`,{ŪF E!B!L!B!0"B!B!B B!B!L[֓]ȼeܿdEљ~y;Ga`Mn@UXRAR1!BBhH{k4FObv Nsiik59hEI0UZ=nPBaD݌E4MӬ>۪g((P$oh[\ZHFoQ_@F(9k@(QtfNt1"B Bf#\I2Id;$гN_ș}?6OԝC|6@qnavBf#ۛ/43s@8Ȓ( KidhNG=~BE)Cʢ4PaR\A2zPXt:gelv)y:KHeiHd!(Ib]6 *r (} !nio4LZy',|D`ȉyS 9~5sL}BP]D?^x 2yer|&)&t$+)a]:auVپv.0KWg 䵸sx81ٌdlU %g-[ʦÃ2mv:^#EE09:69zh68)tGfEQkw@QWE@]"Y69 ́!;&DKG#hk/MDP?/- AH~/~ONK6 {6}sӋMMmg-;)dYV(X蚈FH$P( 80mڴRT⅏+_l;?,MѻM|Ϳ3%WlJgaͤsCDv>bs FQdž?+'T,7;@3&US%'={O|y*2ꎱq!w6T)K|6v Ia!u&q1mu.9ϳ;]5M!>$Q ǖnA 3}~X!z( 3+SW~oG@<;vvzbenkmlG@ nL[ݩ?N6, ɉ3o|7c! tq IGMG~{g22!Tc1gX{ñ/jӧ4͡$7"c:̏Ծ0!TUa BWZߖ챆QJ8j[cn>кU(PϨӆ+Gйt"?ݿcnKL8å;V5[im/\ rz^Z| Aű9n<7*|(zmJD"۴;777a(Y Yʇ-!B!tB!BB!BaD!B! !B!&@B!BB!BaD!B! !B!&@B!BB!B B][[PFtv5Q -]'z&C* v,mb @&R(H39x$U29!!GR>/>"z-:Nhi{գZȼd6'6;4 B BFD3F쉃ˢg%9M4E14ؐAEQktĚdRp&]O\%NvY @1zcGK:AJWqk;2r M4k&Jħ21-KL $c*1n_6 m BaDxd`  ^L2=% MQ~8 IDR49؇8i&[c}TiCYbX#t$4C~ B! !t#"IE/8\~(KzȒӾK|9C$ myqLl) r_,/rFVpv B`arOfz;,pYiY!qoA!&@ uXe,.ic}$%dq 8:GaY dh=g,ű4w}GdQIV_vǾaU&f']M2WA2PQ % ~ d˽O3%{bbw IS VAl&=6 wgY*fXMV͑F4;lidSGQ~<𹌢M?JP(FDS@l%Kq:}d,6Pcu:2Kz"ZXh]6LF4V)(rYcy?p:wB], +]h4p8 MvwAG$E1mSV/Fp8ܷe[[[]u):u&z ٌ&,Qr Cu׼U.=5x|U~K>qOGM81dddT*JT* E("?.x"4#G4}yGG$$)-- pEa[ÇU*ɓ v&U}fi n;/ə`kZlgs ^` Ux>2B6Qǎ-x^ЍktH]7$It\.l .*etTvS8w-fśE. ƍ*Ex^*lDJ|3:.7>7 7%7l0죈-2` B!B* !~;^šA!n\Bhp=!&@BDBaD!tK@!D&ux R265V!]}Ie&M8p<=XSZ B{d0m7>3=c@ʳwWqGkH?=ysŏV;}ճWo]>X~'"2=b٢'&_yGW\tR={#h|dQi^8?_~"nOb>v1A6nm^O 7 (-[Ϫ; W)H;{#UH_oQ;6,"=[Ѿʸ˸#>ZIGng0Ph@C(K?nǓI 5F@OK ˜h,FKFPm L,˲:h $쭽ɤ 0s''ݿ@TVV>}͊D8$x=1_Tw"5e6g/=icѩ((%cr 632fq Y >׭LLDlGG1\_{]F粘M YtJH RFmϾp w %dTꏩlFM5 $ҙ.}a"hҙ M9Xڍ@zft:7J^3kjTݘF&0S^`q;,fg0p`zvL56`'0[ޮm‰Ϛ6_$r3_]&"hs%1{Ui`q8s 0uw6&"eY5<㙘HCJz̬뵵MFR^Lh弩%=y[ !G$'ו\$)me:sI ʗc>b1XB<c>+t:geb> t! VC; l&VWt"_{Fmo5g}Ve `Sn5S ř]$l@I/&ꝳ.S !K7F|&U:ٝ`áPh@ri>t&w(96O;ˡkZjk?5<ޯ;}n.> ~% #fd^HE1%IcU$ ј`(bĨ%Z^"VJpP$r.Wf] zk˖ĒZ+#TS ƈNuc|a6 ,{3\@̌gzdH#fUFsZbxr\h4:"z:gz|i (}.%j,<.@&A@6>9D[]ƁP$4NRlci)PuFNIM2HVLJŠP(r zRB~\y0D|y#Rr-j1|J_ș;ȍF>oۘiH'' ӫnR+g4Xw'gt<#-31!IYG5q?)$ܝCe8=6`L9m#G-!uFun<)@"NW8I7L4UǻgVH;=bp$ NHZD ?+$GFiΨw` Hfi B.]h8"'B4&2":2 (5HJҧ@oVy 3ǴJ©5Ij>-Տ>W߯s;LM{ϟ?Gx1h_܃/ק[UdһܡT~tc/^8tKQ.S457B4gs%Hk<j&)@6O Zk\(JP4)fED"+5GIA+'A^8Fi*ޝ[H%]pXN y%r@L$EHl#X6GDIE.&'N`鹱8&Y,UY@`r,~{ԫon}"kMO.C}簾Άz~V\([7ݷ.Z M}J\k@kh~dӣO=JV|nٲjJ2L&Iҟ^Hd0^PQCB#l:fZ&U'RCұ~'ܸHs0 VKODM+@A>b$~lk⑔xvQMS"ASs\ j ~74˯>%R ȥ5#Q$iTL3"iBLPk8=t6]o[Ȅf:H >%g>o)\~ޒ?k!d&[[VɀHd LF>yTga;e W@RG$Q+^6u ͙(I>ŋZG<]V$iоqw.Vb6j ,;i`L bx !'"/ӿ ,D@"'e1":Jm=?.fa,=pN_R/L( 3tsL58RvO9^A[ ٫@;99955599yҥBrH B+)1yr'Y>@iνo0>l^t9=1qt>vӌnd$nM_}7l2Rҏ$T*4ybD"p5$-_q7_?4WI% ))~̖Qd5[%GArsRs@Ii=='R p"JGxw 鉿qϖy$3iV+ip(Fsq蝌^CŇ (*}=1^WI!p-ͬ$H+%!-5!ahNJ_j-Ɠ(TbwV*o-^_92G3 86Մd0uIH2+0<_H|: B=5-Vgheo#BCESe1 f@CĉHiYo, aft4gLJzCM֠Þ^n^SiH^Vfz{ +X4H;|-˜÷JRs4s\Q鋥bYy 8tcۍԳ2d<2}\3]YR'xQ D Ah*mVM3;|:% n6a>(D4- R9@䳹fE!f-7W p&2G&FMLVck?=|l XXsFRS|2ĢyxLDŽb,-6+q5J;T1 C7!Yh#IQE @ūT#Q1X4QQr];I:]CsDR@4MHǂ:"tfReŮO:CWMQXtUQL GA-Mf?t؟R%PLs}K0ݼSuۊ{d6Dbjy$T1biFS<;-̜+)uc]:H;v[ %@.+ M#XE @y|/>Y"FVY&am};źc1+No<-{;m:KCI|*#'+-\%p@HZMYZ2t#6mΏ ECBF}t>}|TLQ Sk7a,Ȫ/EK1sc(*s_OjOH ;92q{Q|G;[.|{ЬҞ)ӕl5xQEY!k;P1&KGm_M]ˮ'2^x B:59}L&tZ2*3S>Pd2/?Ş l7O& padjEU}]}f뙯T}w]znM@*ݣ-yiXT.%HdRY>;dRɥO[Sԏ<,ܭu~B5Sdm3 `\Ĺm=qѡ}?3)Rext9]olsu䬋)ٌ,@*um5TX֣P1f:[6۽8긢g3@{xXH;涥/mPӃ=>pٝw@AͬlooJGJʪf5i5óBcHsjכ!]:(>B4Z>9`B@srD..?[iz)[#Y]#oow՟z;csI7pk5w?g8g3<w9T$h1[ ie=ֵ3nm}۹vEϩ9}W04$p,ETkX5q$#$ʗy`p:m;@MT-l)zVjTKO֤ը≣.{vL.i y&tNfZi$hF92qXdn3v2V|͊}7"p~Z[ '''EQ>j7iq~u7 \YB:>ɌH*3sff-"4rޠzk۶m |r.Z]מm3&ս˽xѧm+_@NU98$ooҷ4Jso0wv2Vpӷj4եt?p~ਃY6a*5<ݨ V`XsvkuHMϺlN GSw ,3NB71 L^|p+ x[ݠ՟_J;^P^$W(Mϣۊ~5LiOW}Q3nP*"Ο06-d֮]-裏r96-²JT*2} 7c`^<=dqw_x?'ݼ e{|*abbbDhYJ6mv@!e"plB!B!B B!B!L!B!0"B!B!B B!B!L!B!0"B!B!BB!BaDhRBKhjjJ*b"r)[LzIlիlEh-&@t!?Z$ lEh-&@tYv ._MВ| ֮]eV.V.B˥l1;D"Y~}.æ@hIrK$,[rrZ.e Y*++׬Y<6B7if͚J,[rrZFe irT*O+++RUnZI.]499y9e[QQ!ɰlE Bס>ܹsxjH*^ ^BVl+**>3QlE BNG-9DRUUV.B%6B!BaD!B! !B!&@B!BB!BaD!B! !B!&@B!BB!BaD!B!L!B!0"B!ZA?Eh{IENDB`././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/doc/source/user/figures/app_details.png0000664000175000017500000013223500000000000022612 0ustar00zuulzuul00000000000000PNG  IHDR$mHH#CsBITOtEXtSoftwareShutterc IDATx{\SW7/HI E*UQ(xCa[2B!e8s9oG9ONa,R/h+z(PAT(PI4은 z_!+ٷVv,-,,yu֭[ZZZF``YŋÆ C+ 'wkeJbbbbbSVky'DdhP9 ߿&XjjSE7*_GQUmYVhx*:(~8O^9BHӫJ6d?T&۸ Aֺy=Cj\$qrϺų7-|dntT qgːm!yAŦ?7vnY3Ͽ _"?1yÃHk![wMk%c#H([ngPw,`!YV_N%53y/6m'1_X˞o4Ї 2O gNZ_ծkdQ}]_=g}2#퐦3%c{a7“Q:0e},syVV:0TfӴ,n5G5˜[)⺬GMh4o;u*cmG֦H:$619z[SS7/]'OPŒ˱5Ac@["otb/i )L˗5̐KFI{bmJj)1sC4VW&=GAhu'1;?o07{u.ʍ,B=?ՍLArwUy/l]Y3F}_7]0F{h;3lxJ"f}W[ԯ_|7 kAoNe>u}~;եÃ=ufHrDV.?.k.D/ G!M:)"y;쑔Sl^.`vl(אmS[FG8 MNTv~B;:K]7cLwzF>Q~,W#2ѷCؑ'SQ{~ffSʆ|5[u]]8b%));UDuRkFmLt/έjSM~VnԢG=RѫTzK,X~nF{Yݾ-}M=ܾ۩v̙_8o "Z_9n2nȂj7jtjZy>kg~mץo̷]Q%'T> sPG!"v ׉?c"=۩=DU8;g &z~؍[CLQ?Yf:\Q/wxbI{uLZp~;ߚw,z}`yo:Ã=%2QTUŐs}ec#W.&_|>ۖ^vu~O;H[_y޽;JOU -ĸo&i 3NSڲ1uXRO[W?w޽c}HN:?'+JEo x3#H:L#:㽷xo?@ hĨ:_O&]ai7_]' g /HZOe<ذ>|"2x?^_ypW TďF ߜ{RLēJdZl?s΁F+,zZH_WN#Mf}HD[>M8I{`B ^锿g1]"lm'R%W5sND^{AF""xoZ[!#"57RSl%Dij=H_,lzIYIz0|V-?bo۹WtzXĩ0F{gy_}gox61ݒOZ"KE-sc{my˅:rzktnD$#6J˰7]Ѭ2)kyYGC&|C(%"w|HCֲ4U_s, x-\CˈI _×b"ϔհ$$uzFLO8M pKJ1R)sy|G'/*szPWQmIkz㲍F""ݸ :@B|p !&"Y?z>I޳U28qI|a}^d$H{ap$OOV#G:n?(Iؾ F ee~]U(>{ᬯFI@L$%j]ڝ2oPa/nb+_Dܤ"bDr)DrqDDHD",8Fv/+0bKZv#"NS9gtu:F*%N#"}WNDDХkhÆ)3R.GMQ_j^"F*b4;[=vpD]֐+4߶1\8cDrQ[,l5V?عߘ-n6mg|"2cxE7zI~4:_nl1m[X[rdID6UҢmGVn}*\r`m3nw;~)t-K&^`-G޹³)xxآFFжEy-{Zw#"2bc>=FOtt ?3oB7[.E^D^ߕ꒬SGv,QhiJVlqӛڟ"ҩ9󛑊t~r7L|7~w6浸 M^aYQs'ߕ *Eql %V͍Zi{O##zc^]MB;>T $zyF<>fefWg e˚켜d (%Ϫ' +%rUV(4L' {ٞ$aN*4^$rz |{Ql8{lڵ4/x,sI/t2趁WuT;08J XJG"itDbӇ.CTӒVU9sY e֬Y%&˚:c_k$9"8|6ߛ3C. ]H8)s}npUۮB,ƥe_?KKH~g殆 .F*eLY-H2R1N# ;F+FtOnoI7_QcmĖ|phDuƺgƒ>C[kC'HfJfRkcje-h0m+{e|[ԡ&,%̭ 7[LJl&?b_hּ'yĘ|k l$NPKM] ~!nn1Ю}^tۓUeĞX2-mgyDh8)4&"n:nou~s:󴬺2 I]$_?Z. EΦ3~/p=oyĪOlHHH WO<ܕʗۃ4*!"P'"+/9xqlPl;(?ů V7xQ}=Tƪ婩_?or7Ƞ%"/ %BF}b' fզ|"#EJyci?O?`5i޶/KJC}AIPUb)yJKDlM~)ty%"m2-cD7j"RK5?Z1Rw1*ʿv'6CB.oY[gk(`SyeZu%"]I[JQ/|aZduY~%bU'&O=988c"nVmmSXb]担[:U2o-/:\/9u{6Z_䪝S>QnYD:ĕ暾t}1gk;FlvP^ZY`A-7Z\YY~IzXI,s%=k-D6^7{Ak5#M+:q[v<\sHݹ1YZkX'Yn"kĚ/拨[o/kvFyrQmV]ݺUk `>OjUuKD5""lkغzァ)4@tjÞ,r_g-h8:t:IlVc`ԴYaiD$rX*#&FդCmԔQe]ik l{;};m=ü/j+zPJm,q=q'ȯ[lzvQ}Zb~]z$/6j$"KcCٹ=6/y&ߒ/:7Nӭo[1?^JZFK^n=4[`7gfU9dЯ_ln%;oz VlAqswyC[Me'gF֥ϟZsY9OL*&q:<]@{Oq)C_:Y͝2Rot\|Z릅I\5gDgO:m"iĹw}Qw.KCѾ]W~3$$%3Pw5\7?c?wٵfٷF"o~݃b3T;V4Ϯr>Z̢¢7=7`' rR8>|"\GN={Q𷄉 w-MO^/0ij8R2rLsk>r=Rz.?D) 1Ҿ2z+ψich{<%gWM_cPnx0&[bI+K,8X{vOΞ%QZ[I1zF?fHO^o?32llߙ9o>?s}Viy|SsUxQ{KD=D4 I ^#ڍ;{_%>f~4ꁳ6;)Bag|vG_8uژ"Ө%qx-gR } ^X/wevs \2`5_%GJF@;(c8n$IUsOVE:/n=yȳ c/[;(3Ҙ[Hf#5(̬[b$x|qcfdyA {!lJ5]>H[vXin~*34&&ܱۉ#2V8'l` 5Vn_U6u5Vn=סi2iSOn~N="FKӗgi_e 5x[V>n fkH lŁ =F˖AYrLKvHotu܊I}tU"@^|Oqݱ/J h˽*KoW64HGړpAB c3֮͠y^ckXMZf"4l‚ҊZ32"9(| IDAT/8$ϱU*Ty'Z2J$mo*qlc)laA {k-O97u s&r*N[YPGDTcۺp.ͿZ~`[hL!x3cs3"Wqg "[犫$ s,!:_\D̬ZߑZE-Eڢ%"wӒHNI8sV,P{wD!"m:UEQ⢂U ߳ qqw)tEۿ[b /k敚=sSsκ\m܅D$Y)g ] tm=Dž? Z]M2I'4'cWTX'h5g4Mߤ =] -/0MSceD1|]n0GEѩD-obM-v~KubGG1qb&Q>REzڞ%o[~o\x) HWSN[lWI77<<22}flfQ%9b"̼) *.72yz~+QOzDҖC򞮪"̣F3K3gt9Udh@xd*ſVm~t\3m)5V:ӃIΟ*4M*<C7"i?}9uy'_,K#RֲQ/@xԈӖq8seųZQ,=dIźW>,Uj"G2[D&HTD܈]':f!VyBY1,=ς%iR-xd~rڮ&"Z.-%~wF.,}!ɩtjyN GK?쫘" LuD*ȫs}/Qr QQ]L[,K'nc"s#C! FWcpi <ƒO+E;28*i! 3|̋VWX [φ˄9gPu1pdi'j9v-I٪E+9]uʤ*(}|(E@xFz_azܹ[]iȹIyZ)[%j #3\W~,K{_LKO\L|WŘW:6|fZ'gkeOj^|RǴ?tO$-ɻWRH_YSd`<2z[KR""])"goO!өUv}"e+w%<H+)I63؅ZYpTpۘtgܺuY";%6;E(V+=|cU~O}.sVĔFgVR"&N!sZe '8JC*..F-+BlOԕ ӳ>uuuq>'n8~!P3=DTZ#"UQfj1!힫%duTtr9ßڣq։ůVTE٩Ew'ŠU]E-,,P\ܺu8e/^e\qsj*ʫkm.eDr!A#,Y]P戈y{K=fMW*)(PWWnH<+U𚄭Os,7aH%%{u~j4Z9kBSάcq@Dw3_J22[{JKVN#gK?^[i$-&d6ffE$Dvyiv{|<[HTiVddBR=sfB ^wOвeT_(0iVt⤶텆k3KΓoS='|#I1V6ht $ f>}HxXcJ"aԺ;}yFφүS؀_*Dvc}'"ҫXU"DZc} Ɠ.>aށc}DJ ;: Hl/3o~r^x@:#"k=^gk34CCiVf٣a F4H,6e$*DdkDi–*$$"J$<" <"roccJ=|39yI^wfΣ?/0-u_(5kz"!AVn'$jvXinYITDt[WXӒS( ݈/O1s+*JV޾vDcJ"7N!#"ɓumGzrK5,o!i,!2Ύ p5Z:ذ#k15Wn}NraO5Y ^/.LD5}q[npDzŋQ3 +tɂ-IF탤[OA{r{ցFxF Ӎҍxƍׯsڊ*T*BU<g1Sz؜6DOgkJYd[sV幅ώѓO+Gd敛ZDGXN>Mk4v`Լioݬ-ܔFKDb1ScƸ[ӕK?ehKϚ=?\k<,7pqw]:to?/oEtC4$n9gkw"#1omk@wjj9ҫ}N4'י=g'}ϖYu˧x;$ۦ#+)7/IM?K;ze&c]9%|ћg<}-5>qOr6\)Ll^FMy[҇lI]G#>im^> oe3?6Gƺ4??zן|W3b~ĬeG$}4yzofԟv4Vdum6[Ϳ˦g Cq-O羭 q,k5ck$)MHpP-7~K""^}mHسk5 +zُ&]rT^X^ǯ/tafS_ uOS=Ddo`yCfk"&mMp?[ ]7<ސ$f")S6iz~lk/ɣ D-FOB yR_xy OeWI(](Zwf7&nOz5ɞǿ"9֠#󟌀O-ݺa&2ʼnоûw|_;tǝ ll|CE~-*G IZVإqQF}ȚȠ|kLH5ۅzSHY_l+LREt Kde^d) jj5]71mQ]M=8Kx{:wtm2ӹkWPg57Ƞ>6W7>Z~=gP5WpHO|`Ӿ6GHEGDƛ?iKC"j}&GDVV}8 ൂ-xXhAy?{'uFXYs%s''"yQɠ™dC{?O4&dK|y̯_F"y\dE)kwO74ͺF0"~C騤ssN3E[D.oH[h QKb"xǁ]g"2DThׇ_xK7?9)釃^t=ҝ=5?99^cFfkDF7yw$-xS1;/,HBDRGOvgP+z{n?_OCd;sdNߡ?kM7X Oӯ;ϼJc_f/_7y)o?dž9ߙZx݇k4,{thHWNѰDc|l.;}jXpwn](~ K$ r!ㅭo}HGõ7H ޔ`'+qpw&!czplS8M$<ƙ+FsrHU7"q.<͙wJw좚= :@Vthg4݇L1a3w[&!"w~0ڑ>mvB[AMxF-DDQ;dһp5mic71Z $v$ž=8{LiuN=GV(Ϩ};/o\8vFswugV ˳Ddduu0'<"wΣWJYu"0w+"_~ ͈`""|+h`w+cʚg4+5z ~ 5t;A*4l,{5GEՊf'׶R""ƊmaoM'o "{DNMW(Zol1Y "AwYX'{+"zEDI:x3j$-' [?e{(w?ov0 7Mʦm+J* "VI3^?[Q}]WPwRIoOS|7|7@jCo\n$GQ-D2}:‰ U.?8Ia_7Lo#otc/*ηPC{{3@J-]=ďE..%r[=8{tQwm!"cEYWtF*/\i7׻;Q?=H~U">??|ȳ.1<,}_{R*ڶ5p;]yi"BG葽oV{Iwq[G~xI[{efޱc  -[JCR"{z|sC0cujfte"qnzxUh]}/-Nafg,RR۔'~ūʫ8xue ^R#ؖsPmWZEeu*}=|ťmۖ/"RmO=-NWZW#MZY˕xxm6Gz0"gW u:""F2}Μp0 8U9iE:"F<'91S|PzjZNQy,XbnN+;rVXJ9DȽCf&GzvhV}:@Q戈9E͉Q{6(V}WZ#": Ecr1o# &i9rqh†⹜ 5gڦ(1զOTQdӳr=cNl"|a`uނQ][IyJ4aIԖSdmɽ̜3'UX""Q#Ki_̉Zɼ;YuBQRTXip[ "R+faU(H2) Sui.Ůk3 CDt05v{8(d .ݵK+5\)-\+L^T&yCSK %oJ o'"":{ajoRH 4{qGޒN:*cn H?Q=TZRZX- 8"5ju8$|N\[[Z`el-nr$_$Uᮂ d䌣nV IDATxHyAzW2*ұK+)ENb 9M]LSpr#yEVQQ=zö*LKM$H-(\Tp[F N$@5TED2?yW'Q$r$ YmY[ a2bWWJJ [f 0 1D\ufu#E) 'Gg +}̃"n{q̜d]u+ AD”51~VE(&oR `"m[}'"ջ2~b"bZ#22/>*hג!y R@D\ Bet+sj\z "(fg&&=IJދqlir)>ݙ EV]&"ϝMI߰næx?$-@?ZfkU*"r"0gReǣ3D$P(H:OpǶ6jJ.&"~2"-!b8%"P#53UֶsO4`Ja),=JvXAGq^S*J4iEGNDM:ag ݈cd f>V9~E"RZ됙c# 1^ew?S@iUj"ҪTtfKl4Dzj8鑈L=p*E7]*UD$rtP[78:<Œ"wA[)aLGjk JZ~YI \FdlcN[vvưXx쵼-MőF/< {#u12w}ss`lNGޯtsĜw9h\N'd]JN0Dݗְj MB}P_wYM "Qtm,ćm\7Yh%%w2'ùמhϛ "El"#w ?OtutgOO$ٴr߾rE걉":սfۚxěљ"hǞ[;wdO(ze(gZW\yDowvYQh+Lw)Rlbإjb:y'l%bK+i|rr02]X3|$D"i^y_ 7Z#"4 +Ji8 v&4w>JF=M{|4I+5C56GƮ>b#[ nJ|H}+h0I*#rIMÑt#=lOML.^s ʿ=lz%Ɏ: Lty*vQ:뾭勪|nsn>go E>SgLϿ?:#Q? B$Az\{̙ѫU!%ǏutK@kC}I=L^{kA/MTh+OWݤq]bO1:s9r}](پv%!NI+OlI(o|&!lhllhhܔy(L~GAр^{#<e#YVFDJ̅ UD4J8[]q'VB#ޛnyFP!$%on 7 %$,d)D3ݝqsA|~F|\xh˞},%R̟hh,;mͲDGߘ{xO'):h){0W+}G !bRJ/LIMKˍ)ɍ?<ZS{hKg-v"gD8:C9Պ܅<S 剈0yu[q??^P^ "ZI'f.MVsbD:{&A(lUsC箜9=LX Ol]MVbyd>`սgŗvoS,\ ={6]Pl{慗/VTVo˗\zXu]6fu$Rhω3Y37q1"y-Og\7WZv%ў7:'m|+vo='¤ݎN&nfl:_6M'^iqaa{`Ցq\ٿΑdI vLԻO/DVg_yq.ߣx l<6[[j_U{o|EO,l l fQU[Ob34b1ׁCºSEOgsoMKim29"kBJEb<8!dNJ+\ZR.Wk1Eq>HNɭ,8"Ao]GD]dR)%J-twuO 1k1K [#H]^"/qT-UUD؍~dd,MŻT_T hqy_YZZw#b>uزM?}8,O=Y_؄ 0M;d?Jݥʥο}97{?KD'Ke}OeN秋مo_m ?og=l[܁-s#%9p$_EbN+lMq׋D޴- [of/}޺K%N/ԯ_""<Ǒ/'l=tHX9xsg׎(+o_xHߺ^qu 6~gI9oٟN5z爲XZg9R|sG+ͧJsFg9>7{}N_9m|ʃ66]㗷۟mٖW([Ym?Uu]l"<m??)v×B*#"z [ I"u-'̷rX"_]cg"$mVhgKhz>MDD̓\ DY%k(3[6߿Nyּb$_q[6Hk7E _g\J|YbgjZ=,i5iFOTW F|֝%Q,D'rk,ϔ<00d%B ]٧Ttf'DY aҿxvaibkl[6Gc=TO7:7}c%[tR+xg}5kB7?wcH>"q@XFn3/ȿ&6kϏl~w112JvV-@]՚[{=V5X'?;uCmҾO3.gY%ʂ:hCWlwG^w]ٞje䷚6KEchOYks{#LUY) N {p8> DDhMzF6 *d`P_j<%OJ$ [jQ#gzo_ݺ&e?F/[KDD_5{bgĦ_(_M Z֟Qު[>PqW~oy+cާ=w olEycdny_^?n4֗t9;tBjO>=v"b8Y%H$D4yeXR$-@؂O2n{HP+8ƥ-8l`#6ڸISʈҪ [ 9[Q(52)DD Dd,LD1`4N?T#"E8A l l l!VD@--LxlFVjZg ytg[|f;w#]iu/y|ˬ흾&zmFOUVi6DD\ 䥵]a{ީYKB;L+2*.c<(MZA-ce|dҽZQpt4=bJR$ ^ bC6wΠNswP Iul\9^7le@-|NDLwJ5aSUǭ]]Mu{WD"PSal1Hii9p8L8HZpח9$-xP_( /_t <6PS--@@@--Ǒ%x_zlbaoQْ)] #l•ﺟ̣ΉmOѵqCf+ MN"f41_GSh˾MQnVKtR⺗mOͿTUe[x[xy+vi[ڥ{m۽Uz8[]DbV}=?"Uj,zK*#wGYa ~cz   lwPJ$y-NLDRA-a >q"J1(BIbtvCYa >L^oP&eeee@,--]~}qqqqqQEA._Pxl` l l lÈV76xDDZi"!h=Q歅1qF@롄xb4F+k3j.٠~mÚϠ5n:`kZ LKŵt˜ˤծ~Ā+7l<}浅ۂCQ6s~j?wa.!D}Vk>ؠ;58#۠8UBbe1Z. AM53 x+Z~| [ٺ O5Fڻ♒ I~)Uw|ou U6oT c}sE܍N8RswN/]{'ƖKdS=Mʀџ fG{u9(2ᡫ9Th?N8h5B1S*(5Cm2-@ icmۆS}HI@0qODBJ#G zHDB"' zHZfE~mw|C`T=9Df-fTMz=ڵYFƤ k۰i^%=f/Y-K2.䶘 ђn/&mf^7}(ywjVH 8Xp[D˲q_m7&IuGZmgpzdS0s )L-+nPg1k2)1Ck݋i8a̹/)SXvwF Ra!1YT*8b9Nq,4to4TsT[jI1[jq1,9) 51~טۓiBjHB "15)*nMcfd5š1'4"lJ>5|IqO#*6[ Y~_$e-]wFe\ibrC˨X*{V>jwjŶSRtn l>(9J "b,k-DD4k+L, 6}9}J"OQ$eE0ce;L6c/H&㵩ᨲ¨dUVY 1(I|L>BL*j>.!8yqPuӯM>ݞ)XLTUD GlF2CXbl_ihj*jkTrDqi:_fW9۠$(9&^۰گ7VNa"Kƣ{ 7uTJ&䅛e~4XR*+_="bh:<[,ӑL9PWn$JYrm>3rU!G$$$ ͳ߄hkޛ#힐Ƭ&vƯ׆S@)ڮՖ U k\izWNl/.3V5LHg.e)sͲvhWv7ݙ >\R' -uQ"*zͿwOˎoRuڔ=.Gd<kv[$FVRq% Wx2Euz5˒0h~Z"b u5--fQD̵0'Rjk8bnDDj1MG(+sŲ^/mm kOkӔ F9uA{qgJd~S-vi^r7۟HS&%մ6ڔYN[ b%$RXLiPxkKKٽ믈j-6%ɍ-ùW4fWYq7UYl:[sL2[u1ۑ˺_b546W:Z "N^;AL%yϞMGiZMVӶۛ&ﲶ: %#w~ Nj[Z͞گ7Db]ǩǤ-Q\VVֽ-*}[Y?w,O[p_( /_t <6>ܜEܥkeey4s~'n owx3:@س\ E-SWPW9{ݕs|SfOfO9$ܖW>S gfO7gk5GCfGgh[Jj}E>mҹkD %GvۿHG{Nak':ÉM;TD DCD'OmEgFO: y[k|G?h9FM^BEϩgV~579t"/ܾ&"$,QD| <\:3ϗl y<8g}%lD$'~M"sj../6qD+ɾk+nE+gs9D4{'* Z+$Du+;O$CzVsHZVP@"ٙ٩KSw˺+B[D4w3'V&PKh.zY~r믟X.6oU"i߰EЩs֬WV(gn⤷cDԋ[$ O Ϭ n?}$qd)Շ;Xy{謢R[Pl}LgR^^.ѵw ŞO݈\oL)v)=fhHZ(z8{f/W$[6bq-@@6[[j_U{o|EO,l l0"<X}k8|2F&',1+oZ,-Xl70 vt$ =bvL '!s4DjOKSʚãAY:& l lAxlFVjZg ybg[|f[x#]iu/8MDD.wZC?Swtٚuw,/٘}?Z3;jǥMMU6iԶSUSKOEcv+_㈄듩M: VAqh)^"LMg}ۀ>bZI[;X^rǏy=c}iQR R 4YLcMm1 SbɄ$N ⲕ]xJJ(4{z<Fٸ∈bao֊ [on&ʊ?t?;ey4{蕹IS~ek)%{؄dW/=}襙vo.3k'z&sD-JsuYA׼aR.SbǾ%yDtm=3s^W {7g&s_?f:79S~gcZ G(Vi̼A'cIoMت[#U}j˅[oq\]4}9[-\2OWCZ4‚b6R,_ ]X"9[`"qcM;bl:'H梣 ^>L\\Y3:^Vsjϼ?O-!sgyuEx76?;{|*dr ڍ kTb'N!.5䥺BJxJ{c5XuU*4-rمk]"]⒧*hnbd vlhBxrqe;87ʓD'xFQ"46m]Db_eٓ%:K+Wg6l-J-Tx ⩧ʊRs͜?7v\OFzL|+m~*؛Sobd2JzggOOOϩåbJB(O w#17-d&>fgfrs%sfhG/rj~R?;t!|iQvGA6]:GD7^}y=93]'"nS%’\"~fvr^f<0,%N+H,P.W>?Ͼ1ܿm!ql(gc*4 LLr""N.爄[Zr0=^ԬZwziCpRFe=eyW>5KDZEv\_-_$n#iw_ Fy߭ۜMs–UslZ1۔K4_ɍD O-7wafjfu(ݔK4W/O˒ҥ)a}b9Hc/(Y/؛ҽZQpt4=bJR$ ^ bC6wΠNswP Iul\9^y&JW'/ΐd˞'s(ӗ·.gF_rlǦp9M; ([ tFBt¶}/>{cRƲKgNvឋD݊l""ěG8mDE"VmAvAn٦D(Ph%o9+_x^3$;+@d!SvOc;%12ze*ք-NUvuY6ՑV_BMŬS"p0x""iʚʺoylR*<2څ3'ODg"3|%3tuo=W{u8?K<$_]'nzVƉ+wT,٘=s[vً<_yDWϜ9wqf$[K6߫&rw|ɼFKKKׯ__\\\\\EQ˗/t:T>aA[[fيzM0"<^0=l l l l l l|T˳Px _( /_FYqaD-r] IDAT-@@@---?wZLzVjZ/g~#{}]x: " lw8c6>Tذ&">&? J%]G1u-HL$ؚ,flo9|eQs.C"| Bbe1jZ`w$D#D槵@rj] ""!0hFWT Dհsf0}cfBW"%4Zl.?sɠQ aCڎ)SWN3{ZSLilRZ;5;47pvg;IPxqz֎i""YIMS W*e T%j9+\)rVUUqdr,)j#JʒR%Gp(J 며Ea >IX1IG]Gݎf@nJb&7gIsUNO_;MDҒ:OMápw0pD\1Z^*!ڵh#4xs(>e-^?)HNo9ΣN-xl|xp0&M!b8L)$(56"RDݟ$XP0D *+J U2O5U>v#;kTL1DǠp묬,׮]?{[ ݷT ᳡_/*w>۾/p:2k f1ooB#S_4S)?:o>QBnJG~yTlew)knna `Һ~z&f(˗u:* @@---@@x9" PdVwEc{)-bv y4l&|%jYʢ4a3P[QRDDBYk4 &""~n6zhqCZlf]f+cai4؃ vW^XH7AJB?W&9=sN|9”XfɴwrDfmv|4vSjV%-W]۪Y`^y6?VD=&;'QJDbh]QHUpfA&Y,%"ib~=)_N[̋Y\DŽ{ tϖHWedk"""i+26F%">)s:9YlO*ghjfrEE@nbbW k+j ;+RSDI9RڟFw̴|]M\پX3.}T؟NXd-xlOeơLy>HTbf&;&YɄ(DF#Z6D45mK-!SK5UWz-')ɠDE5ԭyZVƚK XM0o/ɎUtI3%RyF>-QLvQ\ŏn_"`vv6 ܜ|>8e-KJJ?U'ƜvP0x|xpj3ڴZ$-yհUY5y-K2"~[[[[[[[[[( l l lk%-PncQ @xlWQFi+Хj4yTFhEu}N6_պ{kzZs?$"v<]I/kk(ѧj4mVy8dWM>UhRu5 cڰ}K{򐖺oWw$՟xPx;`_>>su`"I_V̸QfYa__b%l)3\':ROW*L[(gk q?(6,%Xޮ]Z_2CI">3-Om_ό]՟Z^l%VΙdbc\rf5͈1]lT 490EΙ jW %iʳ)L+䜂g0;;Bbnn|>q˲%%%'Fa a a Dۈ[kzN5l08ݒua?Jx!pً-@]\y߫r6WtjeWӽ3m`Hl_Fd|3 8#>99ȋGEG76{YzctߟuM^GojnxI$8M.[qMVEWnO?uSڧˈL`ҙlMGvYO q/weU1{8qc;X?Tzn7>z+~_V*:xy까 ":V0t:r6?&C=k>h1twlx[.cN_Y#1lZ/}ngsтZ9_hx.;>;""~`QDmjtmK-G%\k{36vO:y8Z:vpV}7dƮ7':C,7Fyy|fovn~K)DnkאW\#c'>N!Z:DuG^ Dwd[<"іMbqkKu.x$gnT (08յ7"FtZ)Vr;wj3җe|O&Iu{t2R_ur#d7n,]ͺ4Z<plۥ:ë?MX@;WEQ N_hxQycg φ:KQlÁ` ~3гh_o!e[C= 4E߮q7M!tTq߷+2-TIukbt/mڑX)ɃONF.4>Q<+T$с N M6OJ^Ip"cK*Rvm/yhmu0bCXz )"9oua<Ǎ9ê/C>9v+ցB%sUwq93$?MB5n|u+6yz1wyn*-E{7_}nO²{=>0&s)0jqMIA9 st N^VD|糾́ LO|h3bIzZ Z,_s"Zǹ]3uTS&JQ;#=9椽Qkē'@'.k$C3>ˡ$jYc(gΐ?t>j 網[$Jށ02v"=taDv֬M͛s!!_E\+Bh)?.[-OmvY=QHC*[$G$xhzgF|/?! "A|rX&tS|N֞RDH{?8LM_ˊ@"lz>4d;<)$D-!w'WگfH3'H&_D)R_H=BH E yNL O ]H-ҙ餌GS\vn~uc|ox)ۦ,k ,RUŷBdD$ T$ R T;Xw^@؂'CdTv/[p唟h=xn>I'vϹiZhG[&o :V[HZV B%W"{Smр6K~"8Ə^#*LJ>'3}} /I4>Oυ:7t׾jzj_}y̵`޳CfQjKI#2Kk-p!IM|bD˿K /TLx?0Z]Iq'#lxޞב*oi:@%Fݡ ] XqYxX_aVo퍎}#XBv֊8o'i0pZbArb/4]S.њO}VLONĿ}ߒpuyZk' "ST#|sn=.ˬgA"y*P FD|Dv>dVSƝHax3|om_, ^ {#(-n CpU؛jkW3\:C yUMX7fD>ܺ3TF-fGkE4- q \#"*B~t gk`I8M O9cb ~:B_+ Hڶz=߄.`M|KHbߦȰMbb׼LDw*"3#ú5# =ֶx:Zڂ#^\ì=-Cܸwpg?7G>cZx<p/cBrƽD1iY#q6@ qS8VybK$%ꔟ#kt*! 2P&sU6k6"&np0sW"JMw^CkrwڪS5FSRe}فt&5ik>XSQ_Rnh4+6a@4Tjm}YnÍW̒̌Dݰgq?4%uC1Ғ8$"QLneeQR lVj4MzNyHIDAT,h%6_պ{@Ab^2֔t1{=qU]N"k^I-hIդk(. -ei56k#4YS0mVY|Z|M+$gg]Mv[SLgZs5F595V=J4xMN}q7ξ-ڠhRZZ/?M斲{_c/["bmyԅ֙k:&5ϸ|lWQFi+Хj4_}Ù9 -YZFW7ROh4 +tj l<slAEyWw$՟xPx[lGM&ˏ0DLʡgw>"&%[\|I@ę,CD\wm2PNl+ɫbM̯ϼ^yLrJuq%Uf+h}vd/cvkLg*m'SU}}bhg}}=7^Quo(kT9̡{{U W*Ϝ|]:zh%b yLLdCAIݫX\d[}^|v""QcY[T^"ˌfZq-~]}(;&CVE]]\qBooo$fn'&%?'5Von2vZtg/fKJ)KWv6]3D$I ]vs:9"R,kW3DI$'""hyN,d[U7G̢1~TJ4YԹGKJJ:wlf'I;D Pg׵WjYn6 X绊;fHNUzL_L&Z*X__=gD1j)MNHTKYOSp(']Itno,ʟo3 ]MNc~렝Rq>ӠZ\{)k_^7Q晋"ZJySdcNc(@RYFTLһg W$Sb{}wdes1+&)?3=LJ~nz"'j/6YRccEya Qaa`ٖvx>x9."ޟY Tkc-zL!r j Ylv"GBZ""vQDDN9Р-Pyp11)цеřgk1G]bvwF틵XxJ޴J}+D$O)Z*"" 0Dı+V#-eDxIr"&O5*ˎOL D'2u޽Ňh$D4 ƶjMKr")<i{H)T[tW?tdl*3=r;PSR=,__ȉj/4nzQO.VnWػ+y5U4G/]˗Bs44^~bajW%|j lD1ęZA㱂F qjk8"I #"rwQP~QάGIҡEBS ٌUu]mU%յjPku6 i5jE &;+ UJ"\*"ahq,vh#"{1 Yۻ&6+hX][W#.3w2Fj3;ITHTn㈈s; WDoй^]N]Si,+>V]ݾtYSVe+hHwjjc-2m%=c/޽8d'e'H\W.nbrk%K8ӱZhr=?%Rg2TҥP]eMK>CÍ=Tꜚdx8o~`67W8T]_׮]yUCj"6+EBc-cnhU啯+Xbg3+]]Rkr-R_s%.&%l#]K&LZ޵kwMɮ#]!fggPA~8cYb2 l l lo{RϩmG}vKօx+e/oXuqA~vȭt\ҩzKg  I_+sOδ]惁;n"Ӳ}N7'~zf#/K,-F.\,GD{z];_7^>;N'9VoMIO/O%^.k׆N[X}ad ~얜 SA_pM#uӳ8xisS,>*p! Da͓̋?)xOimҫt@iJ?~Ky|ODߗ1!  .8wH"⹦|k&ohQyC߾KD.;O'=|swӴЂsWw2~W "l[/en1@8}ozgG{ck#[jml6@"ɢc6-8ZH98HB[,Q ]S.њcY1׾>:FlOHBYk)X,`OQi͹-#[l ,I;B[( ^[ѩQB"O[}Owښo#x6/E_/ʒw80$W~5%lt0O8F,$kKD>gd 7uq#[$:IJ)?9Fn]eTBdbL,\*#" *5^vq~8X1=>9YP+[c;DD|YˠEX4{hM9~mmJ3DDMQ9ON:$XmzC&USzO wG{%"_D }8\jם99};"AؑDDHةQ,TRٳY̷v&"Nn>u4/ڜH*dȩ_D|sq'aX.`vv6 ܜ|>8e-KJJ *O \F@@---@@@---@@@---a a a a a a a a a a a a a a a a a a a a a a a a a a a a a p(˂ӉIENDB`././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/doc/source/user/figures/app_filter.png0000664000175000017500000025562200000000000022460 0ustar00zuulzuul00000000000000PNG  IHDR/KsBITOtEXtSoftwareShutterc IDATx{@7.Z\ 01OXB =?sNĎגc"ir*0EL$P  ȮcyAZP߯ٙٙ{?WU/i4#K&@,B@,BxTH}QA A0 BGl mfIe14^B/Aw} 喟:&8#w 2yD _?kS~G?7<(ZXCb`c#ĂL"H,"@$"IMe.[llXz Xx$q+وD$I$RX"Hmll"XL"X,D"X$mĤL%qBF{X֒Eb\ u,$';9 q~RDb",EL%Y{Hd37L&XAB~nvzb! !.N&D",EYO$@]P  H$tX,k5-`TN* 9Z1NnN,S,DD@$&EHlD"Q03E"`ccyH$n ϝ;{nON6eY"rʇ~T*8@D/K/$쮩jsrr._lcc[ow}wС7|… "^;ve3]NY}9Qկ\$y-"р( ^B{`6 2}ku\tӉv ӛoZ[Ζ}'L>n\ґRFgTNȺiE#WIt>܇XH"s/&,FfXL*D,+RّbHI$$\?&77Ffg4<}ݻ{1{{Yl\.D<1}U]d͞o/0Ҭm}_YZ[W_C&/p y,+*Wm{woxUxQ||siuoϾ6ka}=Wsݣg#f-_ܗTې[|HQBéLHD㫒 n%s+++7. 3\l[2O2 B?LFD,Db,elŋH$$w +/ KN7_;LK<tDxb[[/,44Fj'M{}YbѣGzܹsϜ9k׮ÇOxcW",E:mD66/-_tXȟ pٵ_$ Xv)__}_ē/^Ut3r Z"A%ܸe~bҲk뺢..\"m%m]<[̚m]S%Wx/NvcS])h75$ |4~^wI]^[(y3.&htJyzA\X(6mcbM?gEO]UUED>>>2^zX0 O?CkkZvss+--U(ÇJgΜZJz'A>|]FDfd""DbXYX$֮g)[hWg]BeGv&û`ܦ/W/jnxXMNV)peFA3r xRZ^䳹ThL|4ow̛W[iن>KͼU)ND)(u]A-xG/_""24.-Ukyn ë367CO񦊌yIśUğp^BôُoȄ]ٰ )5h[zmȲnb.;˫NX+kSpLzCM:c \^j0Ý .boLk'5ҥ3׭+ K?4e.5kTainߐ^//3?x> v$rS7r*'W?XWPa;1|iҔ촑\˟$ j?Y KD i>K.:{Ĕ,W#u[(vxڴdC͇}ꚞnsҳF`=k}Nz$R:֨Y{ƆiW_4wtnSfAʕ+DdoooeYLfhB֋驧jmm矵Z- 8 faaYҕrYL&3D${fx_]YW<[[t{wU?tM1_5b\{~V/`#poNF5eU vnF{tuFrp!7n\-S6p wE1E RFCslbfW4seRMY*8ag#'l V0i^jtW"CmfqDVS:/508+$yّ۲Y^(ix)aV'ޑQ4x>7|q馻o(ۘ]܅<`?յax\ŇZԄ2N:T6vއ7HsapX 􍑖Ǜ jbYs"z45[#Y"R* `,VMRH?-̍!bEj8k J)x 0Y):~ -)iݥQ+F#=3MȄIޚL A fAD}G(Lf[T;% vEaÆEEE:Kdb+ᾭ=}eEkŸ? ʵk2%qp|naď=W1Y:tY 1m+b'{ȅZQ˲R""^1\;0ee:_#or1X:V395Թ2dr<Id <ǩ9d^#"bcfnX8y&tb쒆Ah kƮ7k/Y`9e'˅j DT[)L"<]#&8Z"kaâtdPyhyK gF Y Je5sRc`;:kKNo[Q^s-7 UI?hou|SD|LOشdCҠm=ƩJY:[3>Aܘ!eځ \yYŤ~E'; :?u#ڲf#wӤ7eW򙙛k׈ubYJz}"ϩ9Rɺ‘)}&K2H-^s7}OHM#BCƟH$EFl+y}J%jK%Mc멳NXWWGD,I9oY"?nPwѺ@qx-Hc[Wp?x/IY5˭-u/m&"B>˧w7Œ*o\#<" <qw+ڜ'"^][ˎ wcHS6,> nȶGd[m3vq<ߗq -DSQ҇b]ݴk9",tǓ {kyoEҎR5GD|+V5/ͼgg"CjnA$A4RBSԒԜ";srDD\Ŏ|RZEnxuQ~2YL/nMžw7K i'?vuFvqLJ}nyu'k--$]5CYA D"2_Y٘FS5;0ȾWMOOOHTWWjU*ÇhȑW_}+jAQpppii fϞMD&a>^g6/HǫDRX&#"HLA>E2Kt v_K޽_m?ҡ'l<Hako9zLJ'Z4pVFƲy"),m*2%6ủ̒x3fmQFT1iͩIS&Z`IoFs8cuCXz7Cy7-=qP᪲/u)Y˃ffeE{/I[:mŔOx"ROKED+&MȪ\GN(,>7{Q٫rmXhc-zbҲҁ[Q#WUmZIq峃=\KZ&7Μ<эn}}]}ODGyWЬ!爫o cFŧ4d^4q R#Uُ #UD%eO?>j'E"Fs' `4w*9&&zՙH͂LbcR=nHLDfדd`j\">9ځI<`tP ^ِMFjyYߡzlGNA%~L)ycЛo>DDbOI7Ԟ745!}e{A֮;sW44#(:fZDot"cff~vz(_z:M5ݬQ~OIѝV u:]ݏu?_ѪQD[D$P/^d"o3K&sV#\l#^gInHA(czxx̚5pjhJ?J?Y:,L7s* gWEA, bmSXD&3Md2H u~x=Y*77dB_G$s9)H=<=y'Av⛾ۉדH:H$: ]GE"ܶD[s[ ͖tg)zΩfYE} o,y`0鵅ݕCњMm>kkDj)`Er͠I&#l$"Jm\\%6O{XHDM͒k"bV! I C"1<̱b4b! b! BrE "oAz `6fd2 W\yD5d2Bcd2LxXp? 666&{\ mllEif!α~2͂ ;A~XsqX`(Q] +ZƞXX隂f@,B@,B@,BI[!:`0}NG,gBGb!b! b! b! b! <,WM|ŨUV~,:J~{-IwЪe3^|_|1j5Gn-}=mz+3w7MoٔQ-ZT{샄nRlр74b!]ҝ9ƾdIݯYu;3Bw mYO$Y΄dTEzY-3/ [z?;3&'~v_CSy١i#KOOOOO?j>kC_OrWm6XmOx1;]O?<5 3_=wvؐ'xWi;siƹLcm숈͛0i؅j675iӊ)󏎙:uRh ܻVTg e*̼3T#&$xNAӻ7laƥe\uACf,_IڗL_(3}151Dl>US53&3GܬaFe$LSpD8^7νw&9se͘u` `>'V:bkWx"I /)֥hqzdsR2sh"fؘ +Bw$?ɱ*3Ne/eETXYy/qGy IDATxLk3w4emqH$Qz2mu-{ zwں}Ovs?UHFuY|ώ/kF"[7}C^ݨ-WtIcȔ3fut)+<؎ ~c938;мΞ ë^Oݴiկ7g1c7mijoJI-aW_}\~sb:)Yqꫯ>K_g}W&%fUf|fp91L@΃odĬz"4'ٴmקqcY9T[:~hw tKw zdq/(g졹/ֽ,ey v_}4=1nNO<$K()o<}=@'x5ѸkٙWxH;!Qjh)ٵR-$ov-n0J>Q3ˉٹtk-h]٧D/'fy/[`h7H[MF$PQ\=mƼ>\AD2#T^h1^󴌈_`~ {c:jLs "ro4)2Z:BFD#Bn>Rcjɯ(H>f򰬬8"fDD5kT>kGgXuxV}2k36{v=~u_8tFdX[0y hkӘq>2"ru'zj6o+p7Ĺǟ}MND@G)IFr(>yQ4ӶLG:ʧ"GgԒo:HIaW#y>DƯt8G&'"Q`/! A.H$%h\\S!vhp9FeTnZgN,gkކ.+g:To>t=Dwߍ=q1r!Ȅ%t0k6bќdk ED9`?Yx엝Sg|NFD Zuf$13̱,7K]g38)DDog>QN7 OTk8"ba:Win\:ڼywʬfv K{u"mW:3¨XFɽd;eg4< qo_F3;wF:IyaΉOL_P[Cη< Srۺ,⥯ʉ$ru;BydUR=::ўW_u>FWR%s,FGMkalؓ򹄈h  t[NZ9zHB.RGH+wqswD*/O`-hlkvmL΄f~ vϛsR\(\'&X%"9 g gT,kez벤kXw>'>ԘuV(s-,z ^빪㖥[FYSMv٪ݘ-9ڞ{UM6^3㳚n8]Fio,aͷ/Fb,32*p qub_Hur(׻x}F(Oh_񂰶&HJDԡ n_xQ٪|Y`pbMaJϰ7=Ètc/&|Gx$õ꜌7h:aF3cDD]Duhۍݳek;ie%GMdlѶYsQHH:7CQf+UEcڿ-6杙971E#ƹ5ZOD/w_`GiO,缞dÞVT?y,t2eyڡMz"}W.N?;9xgT2"}79έexӓfe ƍ9ǚN> 7\ pj>t켞H_\Ԯ!pE0dK,um:Rb vuwZ=$w}֡Ep7]|#M@79Q˱l)DFCpۆ#"2hOtwxxxK%|J$ G7AR"/D8ANԡk3*"EGdTt2[9V9zтYDԞ ҶvhԠ*; @ϭ~ OHFGLHMqqݧ9>盦*bFg5lIDD̰Ѵy3ͼjt\;^] ƥ&OɜJ"ONRS`8->5*K4q/g-Ov61)V\}RFBSJQ<4biRcNvJm308,LzչgJj-WK<_-2E6?5#bTGXqs KO|cI5|Ҹ r8c>}kgKkVvnchB:=s~T&&�BrXac&q:uW2Uk %NS`nFDr$"PuCY"b]Ŋ׆*1Ϛk#u~v,OFٳ9k#IK=qH{j[nP!_=kxxgџxHHs4+=O->5MWWU~7uǖ7Еd1SBڣ  ԮˮfjkTNBsԶ^rGa!l6L&y^rk_~-iz/zC5S_۷hSǀzO7^`8ecaƭyiًn*dp/܍k7^xGhPE^ s܂,.%PuTFDӸLпU'R3 22~3bO{ޘ[nGԂQ7L5qPfx(B&O}%uzCWU'_S99Zmy;>O'RGDCZvhD 2!@ O%}fAzZ_   y8T[)a accc#P35Xlkk{3g dhccvЈ&@,B@,GoE+<_D+Bl2L&zp)b! b! b! b! b! b!  5GvsTP%!>{2xba˩$QJH[V[D9ٳgO UIzg^RvBKy'bݥJvnɫh5J|B=]!򛴌C:(IZ'fvPN^>S Vui &JeyM8vCY45y_2r&஫2-9?ڍtN4Z녚[r+Z%^fώp-vOX]X$ܵ9C@ӹy=ʍ֙ROQ[[ێ?Ÿfel9ؐtxmxLGAnnt pw)]-k%R }Bꥨ/rB5²VEc'r'QF8P9-:zFW>[Т>Ʋم1D9B񥣖quvg/kkbA}pmzu飵;tD$wTHHpTu $R";Jr Y. ""-Q^~J^TSU^qk.el>cFR:Zh U@'z+jq q# ˶8G7ݥl"YgDni=Zm$z$ R]ۛ_RYDDعEx?- _QNDvoyzmȤۛbrV8m?im5n[P^USoKع{fHs%]lޘs"P/ (\/ꬅ gX9o//Z^㗒)|=xU[ AxSmt+,{FX/W]0^]u-b)KS nѩYɑ.xWb!<2JJڈ 뙠Ƃ9ą!;;3cUf.\Y9~CXjn^Dt$eJŞ0{"pz]Kz?R~55DDb}eLF9^Fz]c]yQ#5Ӑ0ޕJK*dw.xԡZN@6{來MDM{?.;nS4c[kIa| */OwN.0:k*)o\:ąre.!.>d-֕.(8RR֙?(R^ߒ?u}DԈ*jԏ@)ڈozdTSPYa<;(ETSTuQ,]LȾ{u*8~J_.?R ڎ/L'9PA xb8t"G XSpuͤoyu1__UTN3k\KDDEuzUe52viږy-[זu0kT X$3w%y5Eu7eb GV^զ'v}4 iGD\yQ냐?Z+o[baeJMA/: ti,b-5ыs|%Cl+g[Fa{t p&"+>B˺2DTRYWYDsR"]޴ NϬ0p \Fw|>iBʅUEx9ߜ- o ʵ+[o޸^;"k-j""/Gnyy[.,9)1T}f{rݭ.ەt]"X/`iVʕMM5GrW"HSǙM7=bB7ӖH,Cbɟ)wcXY5 g}(6!LH~ѐz9SU_ 2 O)~/E m5{s-{DjҤ#QֹˣF Q0|[]f/ IXߢ!Wo,/)-y.,~ni*soWؔy#OҽeR` K 6V,#H#I[GLR&4qUk{-5qw0_| Afd2L<31/"LJykR.rx|T4qk ;rMn hO*0^qH[;E1kaWV5q°s ?}|ؐ{%sL;s+;q:{D 䞗l&'ZKPCV+ [ 3;J 6Z>÷1ZGnsg6Ѯğ5G0!>o]K,k_9SYQ+WF9 RVpBO[Uk1gTU:h9E ;St6@$QWq^Z٣D=}J2֖շQ{cY# AȄ(`VƖSlHG <*P[}ZG06*g'tV[N5ځ':#*67tlHۨ!G9]->}!/8@Ʋz#9 O`,rJc0l.b!;[ۉH9NЮk']9oHr""R)!"\.!;.+9ʭ.klA.Q>*n*x"ڒowfB<=܏O IDAT2P*Wȉ:tv"9E9AN2^V)'J>rwUՑ8~ OHFG!<KAUU%yFtGƒF]{KUޖ 97OT*^cA^]Y1- Bg0 RCy[wIBDFG5dب[8PuTߟCBl2L&z!$$-p?ItbE1)z  Ϯ/.UVVVٝ!s%ɺؠ9uxW<P-ož-7/W~#mG4]Me+3h]yJ]QoJ*Ǝ~vJjmDoΙaGT@Ѫ|_x  g/CDmݻgdM6~-{߼\7miu؅i{+ooT??cƌ'n2Z_x^2oBݞ|2 ֯g̘: - g̘1cak-gCl&sq3fX3w1cFhL}aƌOCEb=?cF)  B p4S*wUgEGϚ57zw3ҥm_8՞=O}UҔý ;nز^ =s"}f2viz_0zE7,7<%.!KG/^b&s<]* x S,j$no3X0/#W/^bsAɳ>W÷AA䞂k 78}Si]Tr 䬴~ck>hܒ+':S*(.V>{JU-KeN}M62"wGtn[ڟ%z_6SnmҝV?7gV&NڔC'ൊ霛ebͮzb+ݢ?\XoےeaA|H 翖ҩ~pl~sա3IӦp.N[4z8)%.{v&Z^1 U!  =Ϙ SQdo]5 t¥ ;I7 f&%*os4n#EkĎEb0u胯SDo榉&L ƶخЎFb眪oLͮzn™d6 `>ݏzޡ &>)KV05fZg-  rAo!q%ρd(L5 r;O L'WOZa `bt0I,4>q gFOݛ03'j/?kZr=;S3WQY+Uƚ/vmz, Wo>|Ș4eKi[tL~wɚ]fsTEAA,hf>cݔ<0F#Pg蓅܁19$,s[v'v7W(a!qWUۂ%~i Itq~O=L_| /zEecͮ_:Ta Ĭ AA >g|~na U~yh!Ĩƪ*Ϫ 12?q̈4W=w>}9rIǙNj[oxY.cwmZ?kz [ɛSK/MI*\2 U!  HX5;b?0J~abn)b/`rcݷsmbq13֬{:qP{{sz, W.߳Gpڕ~NU8}-&@uD?`3, F4Mӎ |04?]j⣃b,=, W.ik w$@_Q^AAe!=_S"NnQ;/;9z2W3 _}Չ+L̎? )4p&ΞNP_ :z3K ;j% &zJǶNB7^șd3NUx;?+B*AAe!]&NJ z^i7kLQv肵>{Æ 9բШVseWNp,PS_ /m_zMnɋHx9VDO,߂WDC;ʂF&0prǘxȊ3nJ jUK*DA!B5K?ר|x /em_ZBi9i +ң]gOXvhF'O#ڎ5~tUę7miܳan |^/LvK;9''U+ kg^g8 WFu;gh]fIȂpDHJ\6ϝy$gh]jțMMK'rK2  LY2_#5AA'YÙ|R{+fM뫪U՜/V.q?oAvq󃅄d}}X_9wkSΔo̟O:TUSoZQ\rk8SV操' 1(}b.#5,/`jv97„t,!8!q~4+n$pf(}c 8<AA{6,dDŽX?[ cX# [ O71뷳Q" ,DA>Qa@x\!  AbJؙF=&)  BA$h󱪚FrroP" 4pdd[   O -DAAyIW˭ M,={x f5yuk\igv&I0qu(UAAA,4{66???]}~ɢikuXs+ci5!   ]FlSŕgG5yUWφo1Ƴc?qx G;j˾ncQ1с|6tPq yd=mpx Q?~[H:P;m89fM ݧ5`r{g:#|v|ht:\vU{! #㱫SYDxyI#5uP͟jhĂ1OsU?vwXYqХ?=2O D,[vU;kOw>>k҅~gj? /<8 m ݆}_pڎU:@AAVǮ/ߩEѶDfDL{l<<|3mǍ_קzѳ=0fzed9 ['aIĘouWڮt?+"lқuWDfo'qPzJ!Bs+=^P,窿A=oBN[+o`0嘨Hz቏8]Q ^}2rcB,AAIeaDDrO6aϞ=ł   ?,"@2f̘*׮]AO0QIK~`4a" Xb9 /A!ZJب.2goJΥרY<y;7ZJ![dpn#Z2m(^\֊/+o#x"Idlb|?_:~' M:_62ro=ݲyR fνwisq`hƹF-ߙޜ )/olqSuPu AY DH0d*+KfqGfM%MNSn|$.)$\qČ81 e1TkrU D(>n#ή7le5Q>tG)Fg s7WUSi)sP47F5M֩$#i2ZL:֢.`aZRX %ŪbҬFrU#n}A &1)&DY ȈKBz{`Qqud(}$6K" PiN rs4: #7 JpGJB\5,Ov`١01XTLuuְڬR ϤDz£dqlurxqR2Kjm # A׈aTLl\ufBjqjj@Ѷ8(j9*FAQ AGHTenq6Vo۴j^DDDDDļUUhKIEs"""",۰>""Ў[37,g[yݐ2'bٶ꒔es6z x"JY*Ĉ9=gޔ zQ*"b}ypf,z5]dIvDvuLv?vm,r祑ߩ dtUWđ<",z5e{eۢy ?^Ch抖9˶Oj59֧knl-$ sC#<Ó]04y6dXb9]jzڰyYDĪmܜE3]1jVEDlݾ~^DĪg ON6{N5ݲyew><ә9R 7X[XkEPnwe! D.! jպbS`bN޽E9beQc5שxy;rRTXXmTVKsb8ƒuThRNޢ<*VSZ]]Bhsɸ@ALԹBc If U%4)ggQμ4)sPQD)tKVq֝EEEyrRQUm~q9`Vjn)Vׁxil :?F$.Y\J1Hׅr 4kFaF"we,O^*IyE{̊4&D ٹG|VjT+Kݨ,5vLAAjvtm}g-ː _* hhu ŸFlƦun+! *,: ɹ͂ج{%IuNd.O]<i9y;s2ZuBrD$o,efl0W() F./CNT@kAnk"'/cPS%xQIЪrk=71rյ!9wBFBei uPD![YɄ*bllܛV'XL2,MK  h a$I1aACL$L]=,i%8y;&F qq* Mɷܙ €qR޸P_v_|uܴ8D.tB_ݡ Y,`ߚ7zed ^^<bJ@%ƈ 05R 4Z aonZ阠L՚+n]k  wۦ' $` dwH)3 69LJ'NK x nV%3.L[YP/[2A1[$IʁHXPJ1Yp( gα{_SOmcl_H 0$}AP4cfn T, Pf :"XD*%R5zRI0WFMfBEZR$ .ugۈGc(}GHK- z3 EZ\[I%uꃲy0M A ٯ]yըTJg&X?(OOX8u8ʄBUi̿ĠG1n|l$O[j-p*ByO=߄<ؒ_[K=G@Z@#:5ŽP$ O€/!U]>!$rkV |;#-JH3$ceI360ð04u 97M!wj:+0R\Cж@Y8R{^,px vLc>#43|.AM_cE*9.  `234ǽF(3^1#V ddS8L /DD: 1>GCQL:m$*S [fbWk L:*P.8Ռ#Ъf}&%BJlްqT֛Ɔ"`hh5[W\Nyijvjh(ٶacbqԈnrֳfiv4[fhvao3=͕L.&L6%/ %Zs۫fVRjҸʤ$&QTMrĠS j[fcKvbk'"29e-7Zx?ȃդ}Z2YIиR*iƦ\)<>0dn.ohiiخTU0pgVx)٬NVѤ7-N-7VڹIЕQè1/ rS-$E#% Sn'"+*()]i8oO~h tt2E${ԍOTQ)2xR xbi|VRs_5Pg  xp Er2U#.m#r E ro}/NlP %e,T Ic&]nΜoR槮.B Y$֦ԅ ,9EI).GoXHX4->> #K[S YN}`WKK@swG<'5"% V1HO?&#ʄBJsq '$ikU%T$ 47O]\E^6 JVE FU{w!HX4 @ L&E$Y'=8ArՎsiZFs~TFWo,eHqdbNlЂ ~TZFjj Xc,D䶐zwcmj'ofCX8pjlV3{8ܡZtQ6GmA1F,8A҄x~׉v;eGN&LAݏGm ]~vpۉtv6J_DAF-jו:zB'~S1#ǫ&81_oG*A^\vh5X؏[<ܯm_7uo%'* >;q+zvq«>.9q`#Sf.\6o8ʂN\7#zT{aҺ̄(4'-E8qԅϰx]`^t2^[_|c!O@OƋ,wkhU"tӄ?7]=7 Djs5&7iL ~e/!!篟g-폌~%߲ xʾ ̧?{8ɮ9| H\5,_oQ5{BSeA4oaxXO;OXE ׯ_e]E]D1k[O||'..jبGW,X1> G˖7S?#{M1h3]{iBxh4E{`,= mg>^g5΅}h8y~r`V<s)\ e{kJKg^뙽 MyϬMN; _3"nXwmcFCfoL YAI=ݐ2'"bѶ5Ms""ViËvӜcԬcު s}lӶZj.&) MË\~pK 7j2oRM\=3.O֊lXjDDj]7\Yjʶ =,z5d 0-akC)ex`ĥ/^>ԉ' f8LzU-jiU kԽK>)l0o'V;[;=|oqG_ӄwF-ED,DZ2TCH7m^4/?hh}eGB_'y!ȯpT=.yU8y f_"o{S֬0\4:O8yT4w?f녓ݧ||xlkkS'J?+(sb֯ yk^ѻtor)~\:u3FJJQb"evfyͪm]&@^HN/*Y(ė{gST)uhЗm_oys 6m[6Ep/տݶkvovJ>'L.<v ~o?_)MTs<Hrۭݒ!1h|?Cn8{^<\fm3LZ/Uˉoz³z/ :X6hץǍ.}ћQCcj~|\c&D>h6xC zj0 ĕN=ʯ.j:o cm/T u:kfѩRQsS$nWK- /p"c}jc2!>o@JS!*e@"ib"ƿ3s _DƈrKL_3Q* tx&eBvWS-PC[\% _$2;Ncɫ51;? ەf )彪nٞ0uW5d䥄P۔["pab}`I+3חh[M /4>#Czx(rojr87q-&sHc^{;NT%nҗXCE:=֗UZ$VqڔWԼ8Bg0,ȴMa|0WoNU4 ˡ hSPT4[B>Cl07Ke/ZM^C\9U]i`E&fl gW~\}J5XGƧ%k2Dʯ3iI1Tݢn3,ewT:ÜNqx*ۘ6(QG@7i?}=Q3`x};X3y͎;\F3y:^ŠNj.8 ]nDsQΝ^AJ綪U19[LELͭHH+enW'[f6/OʉVB&eG S-;7Gnڼ:M&t7RD./,,#Hp75\{B}ٺĽ¹<\ɖ9PkTeq3ͨb3KRz5??+uB'L?6֪/n(֣YJаo Ji]_u:s`30/>~4 elw=8[ '}^Dy:3jKLyȑY -4A'mwkq*]`?H!S~BLL* GisCIEڿ[iTd[ő&%ysvn ,ڠei&%P+irEr\\J畕͑[rh%>#^Dv~1*+I;>lg?jV<>Wu(6-=b8SGqZ=6 Y$h /?U,(*䓽[&Ƥm I?ɋcJZVUEe|#3j/ZZqyee% JeG ª4RسƢrՕ_utw'ʧr)Vc}\hӇGo 'נp}5^D'ȨU9B 6ِz,Ζ(VW7-}25r끲Ov*K @K]5/~ۿ>xgO]BsC3" j3ݒ$f#wO0JJ}XUfVavE1T=ڲnU3y>CEPc$Gijkݳ `_˧O;|ԗ|䳺l@AwΦ>ԵpٺL3GA{hld9YG}Jv1gks^_;Ǐ s xJcft0;cUN 0!‡ꞛ, )9!z 3*yubg<2nXZ"?DNe"đ$Y@q]  uB1W:excP !$9–Z)]3PGQ='|B`}S+Pyxu\PPPgixQIJOA}<0i7# $H xc0D +$SGy\mK[Z(#(%}NjW&345/ON U%s\ډP}GehO\7-Oq|޿bт~ehoo,#3*fgv?xëU{ilٽ7N;`tÖg ~t,ljYCC}To7Fz귟~l=221e SHW+B7. pb-&y,o47G:pc0dXo;)f10FvE1T=زS0F [lT/Y //Mnhێl݂gS"zXi׵gI>(+K,%-ZB׺&ur)./O)CWƃ⬼^WjZyA'qAsc%%t8߭ZhsC~rrr~`QM#EKt-P>GyعOWGB0ԭv)#E$IE= \;,[\YQtw \M 7C2 N6FcquP %$*!k:M9?\.ԙx~lSlX*h[7{䑌fڬ71e1xUAν~2GC6p>N4=7/&xniB~>~hf觢>Ϥߡ"$*BW+DA EQ| s";\(Ҫ[< fIps8+4(lַ* 2Q A*v}}uOZ 6nh½{;w2]exy͕ĆRZR?`P qh}eb!&H ASb \Hg5jR7R OZ%}El_~[8|Q 4AJ K(\Dh9X8Ղn)IO)hܭ0Ts@A,%Vk-@LJYKۼ'zkrPw # 4%M4VR%Q$abh.љPP` huRyN9X>ZMy)i "Y(FJ6lMץVw8`mhLCUӾ4: .]=aI\."%x7G|R㟅8z_{#fu;,Nד;zF҇?~GM/t)h+$ѩsbTmhmmP[#MiRenf:,\k0!Fb=V? &jn=ٲT Mv "݅b߫nOO~Aoƹ'}7WXɫlG8qr+x^<1iak*a Өgڸ#~{Jq"%6MDJn$ELc4N ?W$B u zZv1}I[[1bCH%[6(qP~Hq"ʼnd;ᇾF8$qFsܹ{9ފBAbeEu`3gaD#B(SZڵ3 Ξ[H9KYZl--`eƊ,+zCfB %j5mLa&d XmeE j$$ʃeҢAc>2b1-5sZP#ą"a5RjK\eTdNMYK#[8k)~+ҖtyB 깹^!dG1d/):&Jeh\Sh Zm{l@I-& 1wr)^?3wB*-׸{9Bg-fO^̮nn3%[vH dW7U[{K:8Jgl!R8"]rΖzPENȏ'7i0 Xs"I-طЋ_fJџ4v/?^G/@m{w;;F9P+tGZ4YmJ( v V[0ɒMhβΚ))To'*I%Y'&| iP=en!-+·Lˤ$%+Lm4-Uj~=)ƲY8q,˾[l.w5k֤|7+W~cD>gΜm}i<Rx)R±9f)׮.sՔeF<$1qtܰLB?w<Nf.̹h߶\dNĪnVU=x@i3 HV|x07/hzI\Ͽ6'2Kg*uܢ0JY2*Uo--0PoO13Bxۋgk^?U߷6H8H͓Ծ0)/ΛYE"8M@K~ ,Dupdn!缞BA>7_|#4r[6+f ?{%[$9_c/RS/άy}9IJ#3Cx7cU0vM E_TM~N۞o⛷ޟGmA>7S(xK,F:o->=y-X r0oS/'yTNxt<¼"V z{oؗ{+yы珷0?!WezojNO_")+(+ WW~7%쭥c8~OoX/ᯃ Ϭe?3OM%&_;/R҄nA-C N]%[sM'[mz {!xDZN~㟟H|;xs?ԉ7çS҉=#2V^<~{X'5%ZJx}gΟ9uc"XN0ҩǧ6[S~j1- *r%9Nt$xk]~xC<>!c+ƹN{?NPG/;(%^JJj:KI %O$Z:[R ϭ}yuL^7o+MvY& g|qx<>=$>s$>sڕ[ABv8$wo,E©`N311K%WPs6]'4z\;:~͉#]8x7fR/1U­* p%Ǽ6g KoΗAP)@,@Z,ȵJi e4AFـ>[6Hʲ1{ NnDq-32%>tWΆp0}<|=ʜ(=Uk'ON_ϫ W8ȕ3z}g?O8IWY>{Ozbmk-3PsXߢQk> 3T+ w[(A˵F.{?6cvxBeIV%hB:U{\GoEF.?yAOzKj!so6Q Ċ I0 32q3!++s˖ 8m6TL53e`GW0 ԗuM-:3/sv>Unصr"5ɽ < -H]8yDKGz ;Xj*O4W/D(F!ZrYftJf)^3 T&gN3gGmj)7u4YA (hf]ڣ<@Ĺ[u@9vA/d&- 9YEKFfQ@F&dGK Qp(75({^|ZkT0၎ڊ6W-"z[_(ZՂn骥݆Ҏ΃]{p[5gGhh%{[PUKI s2{_lsڞ s@Ɋʹ¤'od.| `kqRO~!Ǯ2Ba7YkQ"ȕ+g|t?LUY2=oUj͚<={,_cPIni?[XNC3(j=>]ژ# q51X]vnuGٿ!;TI m^H].?PQsύ8⦮}9&2Rf{PyL'!C=eV{!'Yէn=? FUUmr.m9T+ 5\Oy@lܗ+H]ٚ,\6͟U͏ 51guWO ɬVKL5ue+#uq.?i6 0$ Oe.e+ ktAUWf4gyIԤ2%f.dDߒ!'Tj]f+ھ 8visf$f?Xe6q5zOҵ?z˺[^9 eڷ_+5w;\6pam67"641إ]R qѝ _Sjw(k9,!yv%-8 mB:@z*ŗH%i_#PYs2TkQ(VQ޺C'%5UNRHwHMV^Yӑ$DٜHNLs٬Jj.k= sż=>:OH1OftkIH)TB.f5qŚ,ҵiHZS/(1OiR+^ DJuF.02J 'i)i[I#+7JXv« H5y"\-kVdմؖLPrXH|)9G|0O7JzRP)JO0;|l b 3 -SJCIQ81 cƂ25)`B C3@Iɳ) E/I4$0\4 e Gss! 2Uh3e2QYKl8$e] s )lQ˖]Ws./x- ! @}?e0APʹūi-yVA1Gi;iY" LfM=p,"`$1KU%}ӽ3&+kYYf1عO KŇi\JHLZsOJu6Oa),PN:D22r^e rbe^˙-g ^"d Mo0*(`"%Y}[ aD¦zEdI2@I(2l(`KL`X(PazZ[β#) 0%#Urpfl BJH cWs |ȲL#O}촷^إp+8;wZ& htRw)0=б85]LPp!nzX5,JOi` IDAT![K 8nSCsS :\"Kg N\&@9;džmAn|y(WW wdGssY=_DBlĺ^~DD%1Aq\YhA>Rsn; %!j%sX wwj/G=N ".@xn"\#e{e:K`C.3PP16m('U1Ѐ rL8qv$"}u ~̦̑n߼7wDh BJ5HWXy$V-wX{L%nBj"c.ՕmBf vxYMe}~J/$2AN:2 ./5h#;Pܶ0 $_EрIa| !ÿ*5cvGf4R>GGIkv7$̥@,^򠖋F9J(IQ[# y`ngEXQm! I ZWZ\cSZUd&F%|HfW7U[{K:8Jgl!bc`ޒ"+usQm1m;,\!PEvZ(T{ͪWhKF9Bh"!tXF̶"&c. OBufN r|I3gز?<1K"A* 06I;[qx9%U;(',4rNuԮWP-PKLR&OMj2ח%zl`lEtPHKeD?,f(P,BWm/|d{Sgc..,>D(zZZdrFN9a?ȚmV\ߙP=eV`gå}Qa8h$+PB)g*v8U%4-zs^G{d @ yW[0, =2K ,n, x|bbb|||||8e|-[|B;y5kRR>+W^W?iB=z]prcbb̙37xu3O߻/</ǃLXrޛ}f / er2/;\jfi!(a4י jJKO?!"ʲZV3;X*n&R RA5PZgؑ -:,rb͊?j-kC EFO}wx˷Xc,iٿ`c`>B+4j+ E,G-DdYCT m{ K,72ՏoY٩酰VnϞWk᏶و4l,iҙ鞪m۪DEi VTa "E(?{i|(0 'ƾ7ξݖK~gbJ%-5S( GPh#XAA)i?jnHYvgf`R ABY<[ZU <V #7nHMMCpw恻SxhmM0[ +ϹnvM iȴTzlGNi1rA)x{YjJjZZZf:z-. hB.YI=jmV<=7g&]zjmY 7  ]uFz=6d~&+ҧ]{9elZӼgf--LKK_23%%+A F?=\a)vwΆ߿ccEY[3SWNW¿Oa~voRSxOfn\xlet|"?ۑˍgfdt|"~ՎEx:sM_ ߽ş^6x]+FI~M$  h#|[ WuԊlњ/$K.sEunՊ7Sn5+xHK{.N'WoVР?͈03aZJㆵԶ,~scG7҇<ڻ}7{kA-Y_mW?~ A{WAel#=e -cCvl-8q ,YL.= kbcWA+!53tW߸f;M0,#m!\1l߼8GkWc:g/pǾ73/I|7G~ }Jp r^}7ϯT(0sdY&2-a|Ͻ^?Oed+ͩ+]e{a;(٧X!@eST2VɴkJvv?\gBl3zv,`AAIΏ "1KO>臇=OW͜Z֙3GOQ"-{UDcG3U^[r WOg6׷yZ(S38؀jnsB6B_lB(7GQkTĆOzKJq5Pk5)%Ϋ0U';v^O0jQ-Vtic> 96/JҼjc z ae -Ԁ*&7yaؑnr@M&m6vwԊb#=6k' TVj ,q+d~_8fKe6 ԟ+x9@+t25*ؔ 8m6wJ3|vFK}r_Wő*~B(A˵F.!Ǯ2Ba7Y*;yv Y7ʎA&8YM&`MMYEkJlCJoamG}QhEѤp7y32k@KOR&vd_Yk_>7_Ss#=`5yPbri,5D G oU f ]jE=YKgM !g͞{QOKWC ҳ;NrHW0.߾5]=ī.{QaogsWN?xk)Mu_:o9aQ-so^Q֮j/&Aɝ[D<"ox߹'zu -5\;pl=tТ É[l,G?ezl6w[vЄu&s٬G,rzt2+ڟ}v6@z{{{Lh;pԛb}goog5\RX-Ƚ6@ ̶ݧ\z>ۥ析HԴCH(,]k~WXz{{bw}30[[P{tbl`[>{I:@Mj {"**1 UBdJX訷䖮'<ҢۇY'rɆi(6h31#D% ud6/dUZ*ĄTy.K*yqv,2I[lRQ'e#Nsu3\Gm('1yd0m@$' vY6t3rB\S ^+ԋNO>0@I<ԩrIL<4 acBS[tJ=iP#gP͓\"BpHۼi6޿q%4J7-У?8o_uw+y#79pGyTO?k_ 'M_^d[zw7V}Yi/>ۆ=?nZ6pC}~벍g_yGi>xk}RB $Z%8,Ҵ@/p8:'_e9eR /SɈHtltY$ZsY2XNBHr0$%8_Ok$u  a:o15cyP|_=m]_]볧\'MQWFJlHSS6ӧck%xhjeaMF퐄"la9 G.y;~ r\ᔍijVQp 0\4 $&a3ByqF)*GK, "쁢.q@LQMAR"0ǸL|"5@f ^aT\SǺ&yF&kV&EwLbK@S=?$WAΟ?$]bD|ɮHX&(nҫ9:hc|D8d b"icA/ NburrT&pӢ`<9c@̗kC&9 &խ*[+zlrpsq!Q^ܾw@7%,M!Q==AVq} n"=mMjM&D]?}^UwJϞ*c:z onÉ_0 b$Տڐ>j 飁?PDCػS?\}hG_TVߝc ]􅗟x?|oUծ{c=jC/>cCCwXޘ~'|K=Xb;sǏ&|aI:o=MG|}%(7=ƌriS\@ĹcnZ "b%كHrj\( azZ[βęMmbl2(BZy@-e 45w4K41c@Y&4MLTJH cWs%׼H++ u Yn sAf62@Ic̅+jʮͻJ .D II(NFnsAхM]usZ8K*Lo\ &xɘb: 0#3KLaB %\%w08YUpV_ŹAa@RI5D\@dۼ&*_DaYJrtnov+~ec$pgse׋WAK؉S 2p*شes&=v UF=5 0zM\ o? 6@DZbi/3 }æ{7 s weAo}f֮_]'`sї6@{H<H\|c:dl!;ψp=[fqCםOx")ၞa"n'r!,"C=A"\u0 QB1MĆ:|3=Bn!S 2%! _'_(Sjq= 9zO x9J<ׅ"jx@fdj, D󕴷/D\# Í쯬7R ΢ ZILbaZ@|^*D 8ܝ0S|q T#ܓ`u;J߸:Ƞ(MYRhEMPB &&=Ƃ/H'~*KjzȴɤL܎7(cݐH.K*̌{L%$aّ6QahTm\EyZiiK-\߼籇ndr(gL(i26)%=U\?mC lb>ccc?H ccb2L|{\8c7YZp & WޣP=>h+ ݶ"MrPjW K7VpJhZ(Ujh*`G9qQ'Y]`--BB` n-׸{9B ucl.-bMR%tT[s&UFKjSdKda132&ͼ#X.r^aDNf0@вbcEg2ZPc&q _}1GCRIz=Z*I}~ҋrRIh˚[x֟C[GсXb޶?vtuEX驪\?izGghpwd[=M?=x쉴%SĨ'Mc|f˱B+񉉉qX}7l ɓ'׬Yoȯc]c]\a&&&Μ9s7~%Fv5i!Tfv?oRE}^oRm~i\~4}]3FBЏr N<퍎纞yrK]]S֟F273/@AA!6|tS .c' s/6>|B^XϟPo̦o):F7QjG8"҇cG!M |ȉo߷DO]ciޖ5  r-3{ʆL}Y۷n-pDr\AE ñ[W$?$4;Cfguq} YM] BG}~Zuc 9Ru[ px_UQjŤ@h\'hlοҞ%R.6&p[Cuc[ڏH~aUxQŇTMy9"~,0f[6dlT !1Kh調RYO>S p,9ʃ y]"Xik5Pizt YD\>?GGȊ\gLE6vwR%y Ìb;mo0-Vŧ IDAT^|g߳s(ج0PYaҫ%d!A絶ϲdn(iR;w֖bO 96/JҼjc*r =('[j 7r!`K HC67l=c*+Lzװp|2"2>h^SkMnBNlMC I\or@-*;m6;Qhyɤb܆Ҏ}Zz)j:ҘCBȵj38T/""f/LBY^>}+pZm @&ZBBRq%sfQ_e;y!.ֶ|0!˗ r*[:5 =R}WS*$Wkj:FY+jwj-O7>[[']:vd_Yk_>wTDcg^- V4+<;\Z*7mڧz%rVf]|40A+57O>YA5[]gv=&{ Ԧv=aݟrNchsiBLHUİ`[>{I:h;t0ۃcǏ,>.'rɆew@y{o]zl=M˓Ǐu @3坽GZ45VBYb~?D*ulM& ;f]]&UȀ-i=^N :}d"iE8nEb6}Pk滲fWf9r.cc5G|ۏ?~UsGTXAX/.TLC#"`^FU<8Il!D D|Aa: % jrA %$Y0$7Z_bȬc: "J̼, F)BN̒3HBKF t{cj AJ(41e^Byos8* $_'_(S7rP H8&)cr[ؐ'X%_'EwT\*K,PwO. 4(G4 ^c.'r2c.#|a6w%6؈/,TϭmFH ; R_aȕe j^i&Rj)9я-DJ /ˈVZ\d$;83Fo4{dEUn|o栄-vIPjeZԵksʤ\fRoT$rl--`eƊ, ^2K& Ƽz[i 5 qI"mEVViZnZ5Krd;-ej}`j_9FeaXXmi[¦J HNmU۬-Uj:u39{N_M\yPoG@9O ɊMV'UHj]X?яeq34񉉉qX}7l ]ɓk֬II7#vVG^{;Ϸ\}.MLL9soD"lBݻJ՝ׇFЌ^\)|̐-OΔ ,%))) >-E -Q 6㎊ukq#h0 ADJ_; ers[]/cu 0j'8g/%Ϋ"A 8;L4"0 T8AQ~݁:A*HA>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA6H*a='uHmP7kT˵x   "Wbn!ر_Up&U&   AdZ6 `xH^9k¨1ۯQow6ֻ^86kPDxBWWjDx:߯ۯ4XsAAAYFC9x$'vf92y7W!!P>>8D<Ң, 1ð"Ŷ|Nދ^"u?<'iɟ6Nx  [,%;_Qk~j7"S cGXh|=`.]<`lLK 3fqoF̕~qgSBZ* nwưAAdys D{;fkGlL!&1zZ #EUeXSg0P1M%vKNCg rI%N_W;5n;Й!n`/]{M;SXL}׎%3SCwt5iŀzj"'U syyNy*&OA;CB%_6_^,{22r# V@x|?FJN%%[,X~#Lwf y֌׽Q-,>Fd$ѲtūotgO-(ྒྷ^8/;^4ikrіy1f&M_|0}ÊW~^)Qꁿv7WgSc~.HAcYbO'[3%޺>:~)N~f`P\%K_XO3/I$VJK的' UYIC_sˇo([9M=n:#$1I*#LJ&K,Yz&hߗ1~}7cpo8CINo-KÒE됲OBȿ>KM4?}q!jڴ0c?Mi!f/ϑ&=2jt,҇C\<)+$y|/Q۲(~&z73m >W{d/,O'Zq^]Kb83$-^G/$'$0ئ@|K8R.uإKEC^ק{o֌u|g_ 'xZ\J9AL& j&eӽ~ :ٽ$@ZZQҢ%GZ{eUYo]0o?Zdu:}-hohwʟQGb?&M'p%৻N xO< _\>;sdž߼/Lh9rO_/S$%3U$u>qt^qSn{+~ʣy9kpȆs^~ɞٓx k9-5+wzo^yW/,;vr-y- E./xP_j{ Ḵp_Y%5F댴XރI' WMgVpoېS~YKVbmK+#_F|bC5,2a*@?0@Mf>[(c9FB𵂉H!i哲g>;dF%O:jVbRn+0oHߺ40B/)}70rn2<__?:#럒eMxgIwdd}]K! EA3Evh{}-/\^ 0i>00p_iZȏ2#/_|},]Ev}83Y"tYO,yh(edJ3oB6sHȷLO/Yg?#ɴg -Ͽ_|"~Jb?-^m?~1'Dv. )oKagwH|ŇNO.۹i>2qů-_ 0p3f0d+پso>=s^{ s 'yB3hsϗaD),OAf}}~ %$. }_A$smw32S2彿=GYIx$/&] K ]d}.ȗ< 2b2rzT"-aRq㞟}.Y@cU\Am:2^"=(^2_qrK#GzZ>Ha% <DH^<}`՗B_Xt ^Н.+A/C'~}]}E2REVMZ N+zώHX >Ozůx4։7`zͲ41-x}=Ͼ@evd&T+S}^//x\[\%R>q$ܤ/q̧{C?h ;K eon䭭x,fSo9@>IHH|'& 9B3oxxҥKK.K0B/'B!Bi"B!!B!0-D~p.,D!BaZB!BBB!B"B!´!B!!B!4DOkDJU,bq{N&a! B!k?'ٕͬ%!!adddppP3g`ZB!ם&'''%%ź!-LLLLLLa,B!eZ( 1-Dw)- c7B!}Z)`NB!нL c6XX!BK+8SB!=[B!B"B!´!B!!B!BF4Ly|D.E͹Ŵهe٤$BT*, Boo.ɰ40~11/\_RBh86 ,YF(0~0Es0~1-Dh Xhf(_!0~܌_L B!BhNô!B!0-D!B!i!B!B!L B!B-}q?l_0ԅ ;+#-8^ts8;Y}Kꨫnn*4շ|Z]lXQv۵2vFTxi]i01U4|irM~|X@DBKo1n׫IyjZ̊늏mf^sU5Ernú?Rm(31_gEEwZ]cbEd+ ן|a\]!@S`(0lsfф3v4YT|JGʸgݍ:G{O4&F1Mp&U˾R==cʌ:W `IkrhR?zZؾl IDAT8GcfĔ*|NDG<֍ Tnf ȳj7C5>fnty{F'%2|k3hm6-UBlBiuanԾ6XXxяu愄t*j^ئ)kxΏޭ3H[:n3YwX~dӁ:ܫ0aUTv(%d|^rhE7nx)Rajn6NnFdo?yk,O 47>ashN( חn6Ó4ҺGG[ mfVhp_W9!f.Z!4Hr8f֛Cݻu~tBZz]%8NmuGcl3Xo 4';*JrBB,\[9ek/h.:RWZ*`kto# Z{rҢr7aF b9X(U9i>괷ѓ/nyEo6Y zbya''8^s?>KŎ;vi"?߱c?8ۺرcO}pn`\<cǎ ˾>Wc9pëe/>{5wIwް8yϾ7w"+lo^[I@q=lLF3Z@Bw7嬣ʧ5hf y}6h&P_ F]Trm;.iEK[@ WgjǛShm>Bɨ8;Q1w~﷕2:-M+=V*7ڃ-CN# n2vk::Wfwg5CYYuBNAWHBQo\Uմ-&ave3cIpkP^\NYav^Y)ۯAwP1g)2 !jkj(9n1IVlkU}meҕ*"ͨL$ -fGKT3r -(`w{UV^s?ƼJ^Ww޴80pєM/oX%st*C>ڣ6-e"xs@H2?a3 m +)R)>(hII|% F@_R\O@&];&u+ȡk; $9b>dW l3:Yan"īAw!h(xҫp\WORQR[_'J7Ƣj!OSD]C@R# ep])(ITU2h^y[UQ-l3+H1͝Gkk#\'ei>jh ]^RSBg*. /cݯ H|?kʐ={6^G|iY]D&01'MGIB"HƃSn˨3Ha_}y=QVgRݤAPRr9uI #:< ѡ8Js> <m]NF3X^>a܉4uQ"c'_Y/*wƞs,78EJHD7xq?@1K͠. p\7wXiȽIë`{箁AKeer ( *ZAJF2t#eAu_B L.U'tUգ͉ p|h(BOh"hO}m#A8~Fɦ1w#QF=,vFs8+ ;{x\Rk% F9$7 kHjQOU5 mS@,GPb(?a/BD (u20+m (u:%lk툰@,vRgwlLD"Zy(멯n@@G00;6EǟDoԉL5#vqtgA{塩aiaI$%%X\P $7ىr$ ZA\P,sAJhpb ƻb1 :]{Ȕ+@f~;\YKIT|N f}k-0EQKȩ| 8_BxġB~> E 7EiBk1qVuT[SjI7%$ #p_D %L = t wt"ȡpr-M2,bBdlOxqdJBAF3FSK]^Bg߳ lunJg~ {QAAO}6-?~)7ԾפT _)J z|˫ )0'2wڀ9}ԩs {rqADCKz=cZr5q JE5YmP&)BerN) a2)g~&^r.H$3qg=JDl";+$e5V^>ֿn[VB)5Ґ,1^\CA0 m~Bwn`}El4;7w"9%y/@GpkvON#.ux3R ]=Q M*6U]m%V/%\0;<\xpA)-1*'iqmKݐm[\k3h&uǮەfNo@W-2{T4T^?'QzG;o|sH`a<'/XB 7NrlHz5Z絻\(F 6[Ive{JG*-i<pq Uz"=퐽]s=хRr!s49\=`TXgzU&WN՞~Ky#WܢRM4"t].9M.yNHuŪ\ZI/kjws WsANB >K>8>plHIaMa0[-7/ w8DJRr` Fy?㳕1a̜JBd]9zgw'djE p [?{O :G/85{BWǺ/s=o㗵{ڀ9֫7nxU%d=ʒ2UXy>gei8*mE(flc ΑgV[WTxX*P:>>MLJ{TJ7ZY.ؓIj>e:Fh2V;c 59[Q(4idB|MhœU[K<R\Yba:7Yos6488`ݭ!)}NTn28Z#':S_ A3iNM.r=aKdl,VwgRkפlRS nmm>^p ɑRq( @0M~^v]knh+U2J:U@*JTkuRa.oo)$"`y`=Ξ HvW`f1v.%KUJMm6qr[^b̧-F!puvAz|<*S .{PQRl6nࢯcdVlԌxn'06).0ƥC:vԕmI+l%${B'` u4ilH g`) dkUJڼvtSWӼ;?X5S'k Jr .d_bl`: eyX6-5le7rPƚh,[cpMBYܰREez?{4֕6Tmq+MtzFD%wdfW"y7֔6=Mm^Ue^biO1xW$}w p?Y٦yFFFAy̙3j.mٳOLLTgf7\a -'뽅t+SM?Un4 {vv:VuS-|}Goc/]tR_h:t-fq&SM>suVyÌ3׼Zd[RT %~!s6Z#(BiA0~ZUv+*-(0ܤj ;-3raʝ~kQ9_b*5 5owCoTIWeOmٔw,W`NWGTY`ALRMRKM0jm`dMڜO!e'Bkh XӐb7Ny3JѽƗޟXWK߯%S3YuXӶ-UZ-dɩnyufլkxΛe:w6+_2S,aS՛_Y!tJ-ޒJ.ṇm vɛ_iRaVi|}*)"?X|MBp k[ du?Yt:fx"9+-"ES @!B!2L B!BBB!B"B!´!4;fVRR/Fh/>E9s`Gܹsba#_4}ᅬG}%O?4Œd,!,I^/y|A,_ a a;WY'11}B!EB!BsW-!B! !B!i!B!B!L B!BaZB!BBB!B"B!´!B!!B!0-D!B!$z\8<E`Tg @zJ׭^z}D|^x 5/mX2WlRw,ڰ~ՒE+>`qv| [䆧B'fm40k3t#{ 6p޳zbB!f;C *}Gqd}\Ju^ƇGOuSW!#od8u*Ɏo oB*\_ ՠ*70{88l P2pnH^W ^`gӛGS6mX7B!Bs*-8@"Or1/W_QS-@ꌣG;9X^  R{peLvKSdi20*w @J5}aE2 rPɩ S6mV^>(JH|ѴX6 ;osdydcQ}ÇD+%/]t\ö}dz 7bNB!u;Si>9zR*m\. [^;[!oh^xb$go|mz(2k7[ q܂:o2? K!vݚhy^FסnRڼie*@slxoQ~- B!x5;;pQXc԰aFFFAy̙3jK!}ӱE !tqB!Bs!B!4ͪt&/a~ !B!hZp8|{)))/cЭ!ðRRR222233 aHbZ-_<55^Cs3Cnݝ~ov.^|9fNSN-Z$I,yʕ+NZbxHN~[O"_pw.SSS,Yݍi!B2 VY:3じtrrBH.\_uCe KNNwf"W[׷|r,{.##I'%KHLL;&URRRzzsb!ޥKz۫z衴{oW\9s xwb\!Bh}2p0kʓs޼y_bbyCҥKCNiii=ХKf#B!v' CN8 i!B!B;9mf6ɦ͊{73p4oODB*` %*BwY^ӺiFGZRUMKaI#´ob1D\.Saw9-*BPYp )l,:j{q#tq|u!ΕɧDHNF0=KBaZTVskTL*WפʿK5D*2dZ mVaf^獀BS뙙yF3=+; B] yʤp=^[^Mൻ"5!7u Z,mAV)6uQ`a& R*Sݮ-eeڱ 4>0/funN}== o[wv4a3c/ٸVchttu]b۬FP{o>Ig~&/6{5o1 f3q3wX6&/Ok!o,~f6yy~ZFcuWĺW)4ZW^Sժk3hu[E {{׭u8,L ќ QB]( ) X^i*n-,[xvbhDA1*uVF%kjkwWhHOSyd5+r b5t&6mRSMCCnFmnbrVha4ك0agyNOm߰ gUm.uRP&w<5B:3a4r$D2sFf :S#nKiWr P#^PO}$H!`v wsH`qs!6ǰvC.F拋7:UV C;۶5r5-nخͭ/fUPKCym 7D5lH6?`+!HD,frdulWu5QG$UbݍvoV^IH63N}uE׵ F`s=PMmEMdd!p٪3X8 zv@nFT=u&\j*\Ӂn"u I#!NYb,|^` &;h7M,+TKnh1Ƣ먫:kJJіַ6*b T.C~&hK =AՁZ1%vWcdkzu,*9qkUnL\&YRfaFS:lfJ]eI[ZuFm VmvKVUfk P^,LRv%]jYի)'ȲBΪaMjiCd42y]\bE 4Fεtvy<`DU"q4i0*I|6s=A,ZT hTuꉴn6k ۼNE2.7ԙh |i.joiv(m _o_}e߹Gp_'gz󻋣'ߕCZOyᡡ!Ax?s(u8 sc=J3\g*UR z#0k ET!?\Dw$Ŵ x~tRMBӒ&5*/ Rjm/VO쏈qھT4yCJK^{z^. @vݸ*eHf/w׮D8=.+h8j\jTךz%iMMNw`õB%pHG?g]Hu:թv/lS7hlZ9H r'b .O Ux&he-l y\NلUx? v[ lʧnh1*`sPS]S55+_Wڥ*,OeN ]RKKC>n3Uy:$D<ޜ L[lgnwDeRWaXY֦T_ިr?RNB-ؚeAi[fNgyKJoFTi3ʢrp-6Ź֠PY6^}YaW#[QgMR6ۻl s+UW񫚧Xy1?-#^>r)[j ^~_ET7K(67{v/Ipw}HtnSV|$طO~d򸽣d97 ɉw"k,TZq;´6.wTѕ>yLs38ΪIVVV)>H w4IBJF?&}r#mwv,ijSO,v_H(+0 QHt,!v'Og0|4{$D3io,  $s g@)UR > G@ WR`4.~I| 2bU5ȡk]۔h]~6Iip@)e5el32))nY+$ߢ l[XOGW r:$9 |%5t&PqNE!S NI}/Ooo:..qM'<g|v17HF8%C!0la#v/~k/ =׿V/ocMwbZЌuc|ͭ!h|g\M]&˴Ko˚YB@Wt7fiAYI%P$p^ԟ ٴۢM9 ;. {ӹŴj"Qar^'i>oQw`K:3C ,Jo{BN37S^WksHdo9xCڹB}N]LtIBH_+~~ ȶ9aOWo.-Pi6Z Αr6W 5l*Gd{ua ^ "Q߷d;*ݕDsל}<8>ayǛ=KLoH-y74Ki {; LȄp\#7xsJV~;_ *eY rWqyJ'H&'Y?@o;Ҽ8͇h<)(pLǧ͙HRfO/TrF݃|xwΪsTb UܓϹ$+hUl9>cgֳDd?$]X{"R8)w?|⬱y:Y}ܨ=9jj*(iX1eLAH{? ;f,={^n3mݧ2A"z>4aoY진Κ`+ bł9,xk7 PXuzcb=U/7+z3tF&F˱8]IRx"Y8Ujv#"Y;V 'gZ35=. W5_(xbJ9J]kXrB9NX3yy;KL}kGκG\+XPr5RRR n8D8ӹUrBe7G\WyO'Y.J \@8fHJ/8{ Y$߷c=W>K-iHj)JٝEQϚ,̹L$<#ʂ$Cr$悂ƂA븓\hۙk>߯ƼFHj.ktx¼*CI;kn50$ 2>{ݘcz_8UZVz:o,Lw<ǣ7UDMcrkzWpo>f/yz>i_ooï=I/]r"__\|U&"7a܃uBxR<.[noq#- "z>ϯZ>In,2D嫮-rmPb݇y!ݝrLIIi,˜̇Gvs]Vxmyy\ޑs%5NsA,kblu/w!kމ""itwzQ݃}-E 2qh{2}/˅/S3 G.QRbGs]aݩjۮ9{73a„ۚHp_00:_?O cdd/e-i "RE ; IDbT7 tg+yNRm K>*!"N5=U7%]wW6Sw' sV߬r+% }&w<}2i3EN_])Œ}+܂\vP]8g߷6$ Sj(h܃ko9(~|>P@DTTDDDQQx#=2㪷,wo;Α*"/L^'1JYqu ۔lo+ZJz\meyqyu6̙+5[DSe&sj] 5wur5;E}6djo5EBɡ-} )x-QE}®3fjHnXk D~[?l:f&NxŇ-ի/^8˩ڄ Z[[]kk `}Y"VF=qĞM8믿ɇcƌA\XXXXX?;t-;:޶6O<0ZwI:urjP7Xf`U _Zc?vF 56-΃-VcEa:qHz{̙|ٳ|ToS(~pn!'YK-'r^<ܹs]FRiJ[_;|Ӏ,ū]^B j4ĩ 0!!Z}cu M˺GRC~aӵTx4ԑnz@,h3b@E,*0-;{{Zl7J<3<&БJaSF0R4-{:qH\73}}rlʽR!EqTǦTV)0"mx0DDd.L3! L8H>i~ !cΚ{n^"X'2;H,,4r~ X#v6'MS\  2Hݳd[ɺEFWefEHN+pn!s@Θ]6TEn [EC_SyЌf Q 5~ׂ!&hNYb-ݔ`}D &כPEqƲJ # 5/:/M*u+,Y/G:Z&V 7+b3(I¥eYާ.\`cs'?;ziFL[ґҥK'OFW< LPlЖ_Z 2!b!֕6(4WIS+Ed%aN-?6+-:::ڮ^mooOD 9sf֭;v츝dYnoo8~~aD4 tDDBm~ ϟ?*,C HZ0to4]{^:]ffD$… .\߆K(]=W⅋m~S/L}{… T$74FuX\&eV{²5Lk;M"2iLV/? Hʯ! 988/++O?[,Җ :;;%Izj[[[[[[{{{gggɲܳSØe*44;zy(,ÇgAaWS_ד,Fud%7c8p`pq`aŐPf Mٺ{ TkH0C7=с>u&X58EH?;,>|AZB^W>|4:n 0ྒྷe'=_ 8w$IJ'Nꫯ{;;3f7n- ߻woYYYwwŋ,X`kk_Ojmm/~_4mcKDn?X%",{̚ja?o,4FS(X_~])CE  u剈uHG"QLPX\)zhBY"bs +MovRCa5d(H韸{$׋xxhU">Xd$#"aƏ'%%ڎ;޾/cbbBBBnYÇO>=iҤ_|q_}g}P(^xr ItrhDæLC~Y&"jܝu Q̯{U]#}>Ooj͜ǢĸPg [dz7cn(S 1#$J,["ѷ ,ѵaKK>CVqHD$e9@,~᫯:~(N:rqӦM4ig@@ɓj5_}UQQW_}~=<<Ξ={ܹI&Y,/iXy؜7>y]u-DM9Y:~=rcTHD[u=hy?xu )8(DloH$Ι1BV/ZA ]r}{%D0BɩD4we˖ۏ5jܸqO) "jooX8f̘Yf455]r?8^bA*DTQˬ$*2 7GH2UI9Qj2d="7N.^""VvQ:ԏ9$PoVznkU攙$"~J}e_F$j9G%jɷ O744L4W^y牨իǏ/f($}Ddccccs[?̘1ݽgyƱnњuYWF$-4(]c5Q _'Um8\*$#&QJ08zŦRo EketdD:01c󭎐OL3juoIޣ]Z]Lp%klGFQeYާ.\pA:cǎJ??/\f/\XXzjҥQQQ Ȳfg0a[[[~_}7۶m Slppj]z%KJewwG}}9s;??eJzmmm !զ/2nߣҥK'OFW<0Z O2%88l6g%%%?O;::rrr.^l…*yyySQf̘QUU'sO=Tccb {F?|/^/jƋ/^zu}7|P(^zGk, _Y_֤Fx0Z3ǧ_|cǎ;wCN:lccrUYnj~DR??<_ZZzI__)Sxzzzzz3 2hÁ =O>d͚5K,yh4~s=jX&M /_xή{ʕnnn X֮&]]]=< <)替/ǎ5jܹs.\3, XD8nҤIf͊4i  3f̘1cCmTi*6Ze"/:6.|Jتg͛jǘݟ|'o0gYpoBaqrA&zǟ~駟.IN.4PYlĪYw Kggj}9rԐeK3:;PEM9\4ZV+.-1ԙ%2Ul6D:06%.HJ'1~jlUdVJZ3}{V"3DJM_nۋ2n8ĤDDd*ݠ5lMI wnSqҚLݒCxa~}?Sj(#pA3FYp?BD\wٸӼ2 QSNZ}iu=MDĐl>Z;SumXP?Ev}jNz@DJdI[`|vX&Wue ÇM3)'AWO?j6$g, '67S 0Zp!9c pn!-&ֵZ~CqE"  ՅղU9p:')iec.!a SyBSEV~ˑ!QdU HݗS'ˢL Uk")?9fMTLά:-:Gnܮ4d%,щDĩ㶧Nd\(kufӤ*i&vB8Eg2(el0W,P{lh,.XfIBK&ٲui)9y\̨b3bXhʕz줙M OQ_SeIΟ?{pႃ͝;ziYYM9oݻuҥɓ'+ED XO*[s #F    nKSWWL~OH7 vIoMx8p xw<B;&_iʎPgTIg8 qؔ +HZWYKfU?[I$tYf8Eu1qO!hՁA*D='nh0s~YF I۸n&ORSNy(1jPgV:DXZ#cMIk2)vK:kte Hl2> bC8Gd$d)6w"t#LźQ+ HSaC%|GMq,DM9ڣjM4ChL=Fg8h"N(6OwkR b])l㘣zgJljui[`K/tPH{݉zQ̕Ơ?JX8aAޘp Y++vAFSpB+v-?LCLH{3"-G ș<)}}TY=[ID[HDDp^>J""PXM>>J"R'ޛ$"b\#DDzwHvd^AlmZE"UD Rw/b\TRlN.0#-GdφZ͉f+I:#GIDH}J:+܋'"~Zdo09r79dQ4d\07*1 3xgjYg$"8Af`UwLYJLDh)V139p ht%~a+HEQf_;>BxɢU$≈DQfǩ856{GrP:=TT(77S4a:\暱J"KKL%b}Brǖ o$[E%"jN* 7sTZ1FL"g,,H,=Ԭ'Tmɯ9IM˺PeNI"NWWwNM"qnj%YNIMTLάu)XٮTWXm9;,XYX-p:Ќ7 -6tgyl~ ?%2:-Ѥ.[(HL4t\ӌZݛdRx*NrTB畠OHv=dDsI\~B~ݲzWds番B5'_(>L1 `$%IzNY%I:}… 66!;'T[bjg`]]].]f?N,DM9ڣjM4ÐGkcqd Ѝ{|t`oO>!aHn8*Ff~Ð\wٴ:CrBuى꬜Z~᏷[sY1$StdxefVKDM9 :P!9^N2-{8=O *gi ->|8; {o(3l=J Pו)4>7ͫΠgUn!JKOԬ.S;6d8p`whӵ j8Q{w@,E46HJ"bB]ŕ=q n֨d,=CY"bG{Vxz 1A~*"q:GID";3uތPj/hsfWOS*جqcX-{ǺQCQi'ڏ.x^.-,|eY)^8͑fOۻ;_IDc(9<ұ!:Ex9sr]zONO w=ȈՅO4Y$uI=Ì&]c8חb8F6ˢhɘraVϣ(+\{ѵM"qL_ D(è8v@5}00 =$4dYe"V߫*62{y"jź% {ejcN~@D$IJ-lvoVlH7C~*3jK$5T(l Uk")?9fr 5IMINUwkM9oݻMƸُKuuu]tixmE `IM} #&ϴ\i![^رskalힱ/.մv0'6si,Wǝj|_?M/$RZ/uڏeQg}e_!冷6o|J -]DCǎ]*}`Ζ_ `λeW#߅'=?op pfySuSlbA@8C դ/XZ-E+ ZB?Uum&m+0`h_t.S@Ă<KͯfF4kk:t(3QTL$j |nLLV:,H%':TJ0h5VYrO-أ>mg -X^nQ+& `ο~hLڷ 5Ga.[85,XXrٴ(9LCǎK U=. 5ʹ63fGj,9'P2rqZ[P47I[P= I9bs7,''j'Oܹ';""K?3JB('2CĸG̙HD-UW-X.E5ߣdX9QpnYUsx"R~ypٲoN1iPʝ,cD|H1/isU"n$.8=-l`hu[$Xn 3kOe&I9Pn.IM!=M,indD43T~VP9UD'KD}r*"7 U]"| FXr,(_;$T4318\@4pgD . :QvDұ<ݶs1$,Ab6/ ɲtA0ײ D :93Ӳxso~qڻN-EMC<083OBH4aQ ^yfH@A!d{R;g~vz!͙#q6?2zjm1ד➍ߎ:3\.No3qvTL;sCoxNjoA7BX]/N>^}|+{rP-xtv?BbfiU젣VYϢj5Fh4zV[]]DOɓÇGїpÇqjke*.yzwlf45KԖ.gf|ZT>gKcBvE_[G d!Y,znցhlvuuukkzL&c A\.h4Jb?СCGB DQkKBd!Y,@ Bd!Ef)4( d!oDh>FH$d2) ea2B׺[)`oZ.S,}{ss677;::Lt:YVM{؄tоJ&t:|"|fFݭ @BIt˗ZիWZ+D"L&?QЮ\p,@ Bd!Y,@ Bd!Y@[/zc1aIENDB`././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/doc/source/user/figures/app_filter_example.png0000664000175000017500000026355700000000000024201 0ustar00zuulzuul00000000000000PNG  IHDR/CsBITOtEXtSoftwareShutterc IDATx{@7.Z\ 01OXB =?sNĎגc"ir*0EL$P  ȮcyAZP߯ٙٙ{?WU/i4#K&@,B@,BxTH}QA A0 BGl mfIe14^B/Aw} 喟:&8#w 2yD _?kS~G?7<(ZXCb`c#ĂL"H,"@$"IMe.[llXz Xx$q+وD$I$RX"Hmll"XL"X,D"X$mĤL%qBF{X֒Eb\ u,$';9 q~RDb",EL%Y{Hd37L&XAB~nvzb! !.N&D",EYO$@]P  H$tX,k5-`TN* 9Z1NnN,S,DD@$&EHlD"Q03E"`ccyH$n ϝ;{nON6eY"rʇ~T*8@D/K/$쮩jsrr._lcc[ow}wС7|… "^;ve3]NY}9Qկ\$y-"р( ^B{`6 2}ku\tӉv ӛoZ[Ζ}'L>n\ґRFgTNȺiE#WIt>܇XH"s/&,FfXL*D,+RّbHI$$\?&77Ffg4<}ݻ{1{{Yl\.D<1}U]d͞o/0Ҭm}_YZ[W_C&/p y,+*Wm{woxUxQ||siuoϾ6ka}=Wsݣg#f-_ܗTې[|HQBéLHD㫒 n%s+++7. 3\l[2O2 B?LFD,Db,elŋH$$w +/ KN7_;LK<tDxb[[/,44Fj'M{}YbѣGzܹsϜ9k׮ÇOxcW",E:mD66/-_tXȟ pٵ_$ Xv)__}_ē/^Ut3r Z"A%ܸe~bҲk뺢..\"m%m]<[̚m]S%Wx/NvcS])h75$ |4~^wI]^[(y3.&htJyzA\X(6mcbM?gEO]UUED>>>2^zX0 O?CkkZvss+--U(ÇJgΜZJz'A>|]FDfd""DbXYX$֮g)[hWg]BeGv&û`ܦ/W/jnxXMNV)peFA3r xRZ^䳹ThL|4ow̛W[iن>KͼU)ND)(u]A-xG/_""24.-Ukyn ë367CO񦊌yIśUğp^BôُoȄ]ٰ )5h[zmȲnb.;˫NX+kSpLzCM:c \^j0Ý .boLk'5ҥ3׭+ K?4e.5kTainߐ^//3?x> v$rS7r*'W?XWPa;1|iҔ촑\˟$ j?Y KD i>K.:{Ĕ,W#u[(vxڴdC͇}ꚞnsҳF`=k}Nz$R:֨Y{ƆiW_4wtnSfAʕ+DdoooeYLfhB֋驧jmm矵Z- 8 faaYҕrYL&3D${fx_]YW<[[t{wU?tM1_5b\{~V/`#poNF5eU vnF{tuFrp!7n\-S6p wE1E RFCslbfW4seRMY*8ag#'l V0i^jtW"CmfqDVS:/508+$yّ۲Y^(ix)aV'ޑQ4x>7|q馻o(ۘ]܅<`?յax\ŇZԄ2N:T6vއ7HsapX 􍑖Ǜ jbYs"z45[#Y"R* `,VMRH?-̍!bEj8k J)x 0Y):~ -)iݥQ+F#=3MȄIޚL A fAD}G(Lf[T;% vEaÆEEE:Kdb+ᾭ=}eEkŸ? ʵk2%qp|naď=W1Y:tY 1m+b'{ȅZQ˲R""^1\;0ee:_#or1X:V395Թ2dr<Id <ǩ9d^#"bcfnX8y&tb쒆Ah kƮ7k/Y`9e'˅j DT[)L"<]#&8Z"kaâtdPyhyK gF Y Je5sRc`;:kKNo[Q^s-7 UI?hou|SD|LOشdCҠm=ƩJY:[3>Aܘ!eځ \yYŤ~E'; :?u#ڲf#wӤ7eW򙙛k׈ubYJz}"ϩ9Rɺ‘)}&K2H-^s7}OHM#BCƟH$EFl+y}J%jK%Mc멳NXWWGD,I9oY"?nPwѺ@qx-Hc[Wp?x/IY5˭-u/m&"B>˧w7Œ*o\#<" <qw+ڜ'"^][ˎ wcHS6,> nȶGd[m3vq<ߗq -DSQ҇b]ݴk9",tǓ {kyoEҎR5GD|+V5/ͼgg"CjnA$A4RBSԒԜ";srDD\Ŏ|RZEnxuQ~2YL/nMžw7K i'?vuFvqLJ}nyu'k--$]5CYA D"2_Y٘FS5;0ȾWMOOOHTWWjU*ÇhȑW_}+jAQpppii fϞMD&a>^g6/HǫDRX&#"HLA>E2Kt v_K޽_m?ҡ'l<Hako9zLJ'Z4pVFƲy"),m*2%6ủ̒x3fmQFT1iͩIS&Z`IoFs8cuCXz7Cy7-=qP᪲/u)Y˃ffeE{/I[:mŔOx"ROKED+&MȪ\GN(,>7{Q٫rmXhc-zbҲҁ[Q#WUmZIq峃=\KZ&7Μ<эn}}]}ODGyWЬ!爫o cFŧ4d^4q R#Uُ #UD%eO?>j'E"Fs' `4w*9&&zՙH͂LbcR=nHLDfדd`j\">9ځI<`tP ^ِMFjyYߡzlGNA%~L)ycЛo>DDbOI7Ԟ745!}e{A֮;sW44#(:fZDot"cff~vz(_z:M5ݬQ~OIѝV u:]ݏu?_ѪQD[D$P/^d"o3K&sV#\l#^gInHA(czxx̚5pjhJ?J?Y:,L7s* gWEA, bmSXD&3Md2H u~x=Y*77dB_G$s9)H=<=y'Av⛾ۉדH:H$: ]GE"ܶD[s[ ͖tg)zΩfYE} o,y`0鵅ݕCњMm>kkDj)`Er͠I&#l$"Jm\\%6O{XHDM͒k"bV! I C"1<̱b4b! b! BrE "oAz `6fd2 W\yD5d2Bcd2LxXp? 666&{\ mllEif!α~2͂ ;A~XsqX`(Q] +ZƞXX隂f@,B@,B@,BI[!:`0}NG,gBGb!b! b! b! b! <,WM|ŨUV~,:J~{-IwЪe3^|_|1j5Gn-}=mz+3w7MoٔQ-ZT{샄nRlр74b!]ҝ9ƾdIݯYu;3Bw mYO$Y΄dTEzY-3/ [z?;3&'~v_CSy١i#KOOOOO?j>kC_OrWm6XmOx1;]O?<5 3_=wvؐ'xWi;siƹLcm숈͛0i؅j675iӊ)󏎙:uRh ܻVTg e*̼3T#&$xNAӻ7laƥe\uACf,_IڗL_(3}151Dl>US53&3GܬaFe$LSpD8^7νw&9se͘u` `>'V:bkWx"I /)֥hqzdsR2sh"fؘ +Bw$?ɱ*3Ne/eETXYy/qGy IDATxLk3w4emqH$Qz2mu-{ zwں}Ovs?UHFuY|ώ/kF"[7}C^ݨ-WtIcȔ3fut)+<؎ ~c938;мΞ ë^Oݴiկ7g1c7mijoJI-aW_}\~sb:)Yqꫯ>K_g}W&%fUf|fp91L@΃odĬz"4'ٴmקqcY9T[:~hw tKw zdq/(g졹/ֽ,ey v_}4=1nNO<$K()o<}=@'x5ѸkٙWxH;!Qjh)ٵR-$ov-n0J>Q3ˉٹtk-h]٧D/'fy/[`h7H[MF$PQ\=mƼ>\AD2#T^h1^󴌈_`~ {c:jLs "ro4)2Z:BFD#Bn>Rcjɯ(H>f򰬬8"fDD5kT>kGgXuxV}2k36{v=~u_8tFdX[0y hkӘq>2"ru'zj6o+p7Ĺǟ}MND@G)IFr(>yQ4ӶLG:ʧ"GgԒo:HIaW#y>DƯt8G&'"Q`/! A.H$%h\\S!vhp9FeTnZgN,gkކ.+g:To>t=Dwߍ=q1r!Ȅ%t0k6bќdk ED9`?Yx엝Sg|NFD Zuf$13̱,7K]g38)DDog>QN7 OTk8"ba:Win\:ڼywʬfv K{u"mW:3¨XFɽd;eg4< qo_F3;wF:IyaΉOL_P[Cη< Srۺ,⥯ʉ$ru;BydUR=::ўW_u>FWR%s,FGMkalؓ򹄈h  t[NZ9zHB.RGH+wqswD*/O`-hlkvmL΄f~ vϛsR\(\'&X%"9 g gT,kez벤kXw>'>ԘuV(s-,z ^빪㖥[FYSMv٪ݘ-9ڞ{UM6^3㳚n8]Fio,aͷ/Fb,32*p qub_Hur(׻x}F(Oh_񂰶&HJDԡ n_xQ٪|Y`pbMaJϰ7=Ètc/&|Gx$õ꜌7h:aF3cDD]Duhۍݳek;ie%GMdlѶYsQHH:7CQf+UEcڿ-6杙971E#ƹ5ZOD/w_`GiO,缞dÞVT?y,t2eyڡMz"}W.N?;9xgT2"}79έexӓfe ƍ9ǚN> 7\ pj>t켞H_\Ԯ!pE0dK,um:Rb vuwZ=$w}֡Ep7]|#M@79Q˱l)DFCpۆ#"2hOtwxxxK%|J$ G7AR"/D8ANԡk3*"EGdTt2[9V9zтYDԞ ҶvhԠ*; @ϭ~ OHFGLHMqqݧ9>盦*bFg5lIDD̰Ѵy3ͼjt\;^] ƥ&OɜJ"ONRS`8->5*K4q/g-Ov61)V\}RFBSJQ<4biRcNvJm308,LzչgJj-WK<_-2E6?5#bTGXqs KO|cI5|Ҹ r8c>}kgKkVvnchB:=s~T&&�BrXac&q:uW2Uk %NS`nFDr$"PuCY"b]Ŋ׆*1Ϛk#u~v,OFٳ9k#IK=qH{j[nP!_=kxxgџxHHs4+=O->5MWWU~7uǖ7Еd1SBڣ  ԮˮfjkTNBsԶ^rGa!l6L&y^rk_~-iz/zC5S_۷hSǀzO7^`8ecaƭyiًn*dp/܍k7^xGhPE^ s܂,.%PuTFDӸLпU'R3 22~3bO{ޘ[nGԂQ7L5qPfx(B&O}%uzCWU'_S99Zmy;>O'RGDCZvhD 2!@ O%}fAzZ_   y8T[)a accc#P35Xlkk{3g dhccvЈ&@,B@,GoE+<_D+Bl2L&zp)b! b! b! b! b! b!  5GvsTP%!>{2xba˩$QJH[V[D9ٳgO UIzg^RvBKy'bݥJvnɫh5J|B=]!򛴌C:(IZ'fvPN^>S Vui &JeyM8vCY45y_2r&஫2-9?ڍtN4Z녚[r+Z%^fώp-vOX]X$ܵ9C@ӹy=ʍ֙ROQ[[ێ?Ÿfel9ؐtxmxLGAnnt pw)]-k%R }Bꥨ/rB5²VEc'r'QF8P9-:zFW>[Т>Ʋم1D9B񥣖quvg/kkbA}pmzu飵;tD$wTHHpTu $R";Jr Y. ""-Q^~J^TSU^qk.el>cFR:Zh U@'z+jq q# ˶8G7ݥl"YgDni=Zm$z$ R]ۛ_RYDDعEx?- _QNDvoyzmȤۛbrV8m?im5n[P^USoKع{fHs%]lޘs"P/ (\/ꬅ gX9o//Z^㗒)|=xU[ AxSmt+,{FX/W]0^]u-b)KS nѩYɑ.xWb!<2JJڈ 뙠Ƃ9ą!;;3cUf.\Y9~CXjn^Dt$eJŞ0{"pz]Kz?R~55DDb}eLF9^Fz]c]yQ#5Ӑ0ޕJK*dw.xԡZN@6{來MDM{?.;nS4c[kIa| */OwN.0:k*)o\:ąre.!.>d-֕.(8RR֙?(R^ߒ?u}DԈ*jԏ@)ڈozdTSPYa<;(ETSTuQ,]LȾ{u*8~J_.?R ڎ/L'9PA xb8t"G XSpuͤoyu1__UTN3k\KDDEuzUe52viږy-[זu0kT X$3w%y5Eu7eb GV^զ'v}4 iGD\yQ냐?Z+o[baeJMA/: ti,b-5ыs|%Cl+g[Fa{t p&"+>B˺2DTRYWYDsR"]޴ NϬ0p \Fw|>iBʅUEx9ߜ- o ʵ+[o޸^;"k-j""/Gnyy[.,9)1T}f{rݭ.ەt]"X/`iVʕMM5GrW"HSǙM7=bB7ӖH,Cbɟ)wcXY5 g}(6!LH~ѐz9SU_ 2 O)~/E m5{s-{DjҤ#QֹˣF Q0|[]f/ IXߢ!Wo,/)-y.,~ni*soWؔy#OҽeR` K 6V,#H#I[GLR&4qUk{-5qw0_| Afd2L<31/"LJykR.rx|T4qk ;rMn hO*0^qH[;E1kaWV5q°s ?}|ؐ{%sL;s+;q:{D 䞗l&'ZKPCV+ [ 3;J 6Z>÷1ZGnsg6Ѯğ5G0!>o]K,k_9SYQ+WF9 RVpBO[Uk1gTU:h9E ;St6@$QWq^Z٣D=}J2֖շQ{cY# AȄ(`VƖSlHG <*P[}ZG06*g'tV[N5ځ':#*67tlHۨ!G9]->}!/8@Ʋz#9 O`,rJc0l.b!;[ۉH9NЮk']9oHr""R)!"\.!;.+9ʭ.klA.Q>*n*x"ڒowfB<=܏O IDAT2P*Wȉ:tv"9E9AN2^V)'J>rwUՑ8~ OHFG!<KAUU%yFtGƒF]{KUޖ 97OT*^cA^]Y1- Bg0 RCy[wIBDFG5dب[8PuTߟCBl2L&z!$$-p?ItbE1)z  Ϯ/.UVVVٝ!s%ɺؠ9uxW<P-ož-7/W~#mG4]Me+3h]yJ]QoJ*Ǝ~vJjmDoΙaGT@Ѫ|_x  g/CDmݻgdM6~-{߼\7miu؅i{+ooT??cƌ'n2Z_x^2oBݞ|2 ֯g̘: - g̘1cak-gCl&sq3fX3w1cFhL}aƌOCEb=?cF)  B p4S*wUgEGϚ57zw3ҥm_8՞=O}UҔý ;nز^ =s"}f2viz_0zE7,7<%.!KG/^b&s<]* x S,j$no3X0/#W/^bsAɳ>W÷AA䞂k 78}Si]Tr 䬴~ck>hܒ+':S*(.V>{JU-KeN}M62"wGtn[ڟ%z_6SnmҝV?7gV&NڔC'ൊ霛ebͮzb+ݢ?\XoےeaA|H 翖ҩ~pl~sա3IӦp.N[4z8)%.{v&Z^1 U!  =Ϙ SQdo]5 t¥ ;I7 f&%*os4n#EkĎEb0u胯SDo榉&L ƶخЎFb眪oLͮzn™d6 `>ݏzޡ &>)KV05fZg-  rAo!q%ρd(L5 r;O L'WOZa `bt0I,4>q gFOݛ03'j/?kZr=;S3WQY+Uƚ/vmz, Wo>|Ș4eKi[tL~wɚ]fsTEAA,hf>cݔ<0F#Pg蓅܁19$,s[v'v7W(a!qWUۂ%~i Itq~O=L_| /zEecͮ_:Ta Ĭ AA >g|~na U~yh!Ĩƪ*Ϫ 12?q̈4W=w>}9rIǙNj[oxY.cwmZ?kz [ɛSK/MI*\2 U!  HX5;b?0J~abn)b/`rcݷsmbq13֬{:qP{{sz, W.߳Gpڕ~NU8}-&@uD?`3, F4Mӎ |04?]j⣃b,=, W.ik w$@_Q^AAe!=_S"NnQ;/;9z2W3 _}Չ+L̎? )4p&ΞNP_ :z3K ;j% &zJǶNB7^șd3NUx;?+B*AAe!]&NJ z^i7kLQv肵>{Æ 9բШVseWNp,PS_ /m_zMnɋHx9VDO,߂WDC;ʂF&0prǘxȊ3nJ jUK*DA!B5K?ר|x /em_ZBi9i +ң]gOXvhF'O#ڎ5~tUę7miܳan |^/LvK;9''U+ kg^g8 WFu;gh]fIȂpDHJ\6ϝy$gh]jțMMK'rK2  LY2_#5AA'YÙ|R{+fM뫪U՜/V.q?oAvq󃅄d}}X_9wkSΔo̟O:TUSoZQ\rk8SV操' 1(}b.#5,/`jv97„t,!8!q~4+n$pf(}c 8<AA{6,dDŽX?[ cX# [ O71뷳Q" ,DA>Qa@x\!  AbJؙF=&)  BA$h󱪚FrroP" 4pdd[   O -DAAyIW˭ M,={x f5yuk\igv&I0qu(UAAA,4{66???]}~ɢikuXs+ci5!   ]FlSŕgG5yUWφo1Ƴc?qx G;j˾ncQ1с|6tPq yd=mpx Q?~[H:P;m89fM ݧ5`r{g:#|v|ht:\vU{! #㱫SYDxyI#5uP͟jhĂ1OsU?vwXYqХ?=2O D,[vU;kOw>>k҅~gj? /<8 m ݆}_pڎU:@AAVǮ/ߩEѶDfDL{l<<|3mǍ_קzѳ=0fzed9 ['aIĘouWڮt?+"lқuWDfo'qPzJ!Bs+=^P,窿A=oBN[+o`0嘨Hz቏8]Q ^}2rcB,AAIeaDDrO6aϞ=ł   ?,"@2f̘*׮]AO0QIK~`4a" Xb9 /A!ZJب.2goJΥרY<y;7ZJ![dpn#Z2m(^\֊/+o#x"Idlb|?_:~' M:_62ro=ݲyR fνwisq`hƹF-ߙޜ )/olqSuPu AY DH0d*+KfqGfM%MNSn|$.)$\qČ81 e1TkrU D(>n#ή7le5Q>tG)Fg s7WUSi)sP47F5M֩$#i2ZL:֢.`aZRX %ŪbҬFrU#n}A &1)&DY ȈKBz{`Qqud(}$6K" PiN rs4: #7 JpGJB\5,Ov`١01XTLuuְڬR ϤDz£dqlurxqR2Kjm # A׈aTLl\ufBjqjj@Ѷ8(j9*FAQ AGHTenq6Vo۴j^DDDDDļUUhKIEs"""",۰>""Ў[37,g[yݐ2'bٶ꒔es6z x"JY*Ĉ9=gޔ zQ*"b}ypf,z5]dIvDvuLv?vm,r祑ߩ dtUWđ<",z5e{eۢy ?^Ch抖9˶Oj59֧knl-$ sC#<Ó]04y6dXb9]jzڰyYDĪmܜE3]1jVEDlݾ~^DĪg ON6{N5ݲyew><ә9R 7X[XkEPnwe! D.! jպbS`bN޽E9beQc5שxy;rRTXXmTVKsb8ƒuThRNޢ<*VSZ]]Bhsɸ@ALԹBc If U%4)ggQμ4)sPQD)tKVq֝EEEyrRQUm~q9`Vjn)Vׁxil :?F$.Y\J1Hׅr 4kFaF"we,O^*IyE{̊4&D ٹG|VjT+Kݨ,5vLAAjvtm}g-ː _* hhu ŸFlƦun+! *,: ɹ͂ج{%IuNd.O]<i9y;s2ZuBrD$o,efl0W() F./CNT@kAnk"'/cPS%xQIЪrk=71rյ!9wBFBei uPD![YɄ*bllܛV'XL2,MK  h a$I1aACL$L]=,i%8y;&F qq* Mɷܙ €qR޸P_v_|uܴ8D.tB_ݡ Y,`ߚ7zed ^^<bJ@%ƈ 05R 4Z aonZ阠L՚+n]k  wۦ' $` dwH)3 69LJ'NK x nV%3.L[YP/[2A1[$IʁHXPJ1Yp( gα{_SOmcl_H 0$}AP4cfn T, Pf :"XD*%R5zRI0WFMfBEZR$ .ugۈGc(}GHK- z3 EZ\[I%uꃲy0M A ٯ]yըTJg&X?(OOX8u8ʄBUi̿ĠG1n|l$O[j-p*ByO=߄<ؒ_[K=G@Z@#:5ŽP$ O€/!U]>!$rkV |;#-JH3$ceI360ð04u 97M!wj:+0R\Cж@Y8R{^,px vLc>#43|.AM_cE*9.  `234ǽF(3^1#V ddS8L /DD: 1>GCQL:m$*S [fbWk L:*P.8Ռ#Ъf}&%BJlްqT֛Ɔ"`hh5[W\Nyijvjh(ٶacbqԈnrֳfiv4[fhvao3=͕L.&L6%/ %Zs۫fVRjҸʤ$&QTMrĠS j[fcKvbk'"29e-7Zx?ȃդ}Z2YIиR*iƦ\)<>0dn.ohiiخTU0pgVx)٬NVѤ7-N-7VڹIЕQè1/ rS-$E#% Sn'"+*()]i8oO~h tt2E${ԍOTQ)2xR xbi|VRs_5Pg  xp Er2U#.m#r E ro}/NlP %e,T Ic&]nΜoR槮.B Y$֦ԅ ,9EI).GoXHX4->> #K[S YN}`WKK@swG<'5"% V1HO?&#ʄBJsq '$ikU%T$ 47O]\E^6 JVE FU{w!HX4 @ L&E$Y'=8ArՎsiZFs~TFWo,eHqdbNlЂ ~TZFjj Xc,D䶐zwcmj'ofCX8pjlV3{8ܡZtQ6GmA1F,8A҄x~׉v;eGN&LAݏGm ]~vpۉtv6J_DAF-jו:zB'~S1#ǫ&81_oG*A^\vh5X؏[<ܯm_7uo%'* >;q+zvq«>.9q`#Sf.\6o8ʂN\7#zT{aҺ̄(4'-E8qԅϰx]`^t2^[_|c!O@OƋ,wkhU"tӄ?7]=7 Djs5&7iL ~e/!!篟g-폌~%߲ xʾ ̧?{8ɮ9| H\5,_oQ5{BSeA4oaxXO;OXE ׯ_e]E]D1k[O||'..jبGW,X1> G˖7S?#{M1h3]{iBxh4E{`,= mg>^g5΅}h8y~r`V<s)\ e{kJKg^뙽 MyϬMN; _3"nXwmcFCfoL YAI=ݐ2'"bѶ5Ms""ViËvӜcԬcު s}lӶZj.&) MË\~pK 7j2oRM\=3.O֊lXjDDj]7\Yjʶ =,z5d 0-akC)ex`ĥ/^>ԉ' f8LzU-jiU kԽK>)l0o'V;[;=|oqG_ӄwF-ED,DZ2TCH7m^4/?hh}eGB_'y!ȯpT=.yU8y f_"o{S֬0\4:O8yT4w?f녓ݧ||xlkkS'J?+(sb֯ yk^ѻtor)~\:u3FJJQb"evfyͪm]&@^HN/*Y(ė{gST)uhЗm_oys 6m[6Ep/տݶkvovJ>'L.<v ~o?_)MTs<Hrۭݒ!1h|?Cn8{^<\fm3LZ/Uˉoz³z/ :X6hץǍ.}ћQCcj~|\c&D>h6xC zj0 ĕN=ʯ.j:o cm/T u:kfѩRQsS$nWK- /p"c}jc2!>o@JS!*e@"ib"ƿ3s _DƈrKL_3Q* tx&eBvWS-PC[\% _$2;Ncɫ51;? ەf )彪nٞ0uW5d䥄P۔["pab}`I+3חh[M /4>#Czx(rojr87q-&sHc^{;NT%nҗXCE:=֗UZ$VqڔWԼ8Bg0,ȴMa|0WoNU4 ˡ hSPT4[B>Cl07Ke/ZM^C\9U]i`E&fl gW~\}J5XGƧ%k2Dʯ3iI1Tݢn3,ewT:ÜNqx*ۘ6(QG@7i?}=Q3`x};X3y͎;\F3y:^ŠNj.8 ]nDsQΝ^AJ綪U19[LELͭHH+enW'[f6/OʉVB&eG S-;7Gnڼ:M&t7RD./,,#Hp75\{B}ٺĽ¹<\ɖ9PkTeq3ͨb3KRz5??+uB'L?6֪/n(֣YJаo Ji]_u:s`30/>~4 elw=8[ '}^Dy:3jKLyȑY -4A'mwkq*]`?H!S~BLL* GisCIEڿ[iTd[ő&%ysvn ,ڠei&%P+irEr\\J畕͑[rh%>#^Dv~1*+I;>lg?jV<>Wu(6-=b8SGqZ=6 Y$h /?U,(*䓽[&Ƥm I?ɋcJZVUEe|#3j/ZZqyee% JeG ª4RسƢrՕ_utw'ʧr)Vc}\hӇGo 'נp}5^D'ȨU9B 6ِz,Ζ(VW7-}25r끲Ov*K @K]5/~ۿ>xgO]BsC3" j3ݒ$f#wO0JJ}XUfVavE1T=ڲnU3y>CEPc$Gijkݳ `_˧O;|ԗ|䳺l@AwΦ>ԵpٺL3GA{hld9YG}Jv1gks^_;Ǐ s xJcft0;cUN 0!‡ꞛ, )9!z 3*yubg<2nXZ"?DNe"đ$Y@q]  uB1W:excP !$9–Z)]3PGQ='|B`}S+Pyxu\PPPgixQIJOA}<0i7# $H xc0D +$SGy\mK[Z(#(%}NjW&345/ON U%s\ډP}GehO\7-Oq|޿bт~ehoo,#3*fgv?xëU{ilٽ7N;`tÖg ~t,ljYCC}To7Fz귟~l=221e SHW+B7. pb-&y,o47G:pc0dXo;)f10FvE1T=زS0F [lT/Y //Mnhێl݂gS"zXi׵gI>(+K,%-ZB׺&ur)./O)CWƃ⬼^WjZyA'qAsc%%t8߭ZhsC~rrr~`QM#EKt-P>GyعOWGB0ԭv)#E$IE= \;,[\YQtw \M 7C2 N6FcquP %$*!k:M9?\.ԙx~lSlX*h[7{䑌fڬ71e1xUAν~2GC6p>N4=7/&xniB~>~hf觢>Ϥߡ"$*BW+DA EQ| s";\(Ҫ[< fIps8+4(lַ* 2Q A*v}}uOZ 6nh½{;w2]exy͕ĆRZR?`P qh}eb!&H ASb \Hg5jR7R OZ%}El_~[8|Q 4AJ K(\Dh9X8Ղn)IO)hܭ0Ts@A,%Vk-@LJYKۼ'zkrPw # 4%M4VR%Q$abh.љPP` huRyN9X>ZMy)i "Y(FJ6lMץVw8`mhLCUӾ4: .]=aI\."%x7G|R㟅8z_{#fu;,Nד;zF҇?~GM/t)h+$ѩsbTmhmmP[#MiRenf:,\k0!Fb=V? &jn=ٲT Mv "݅b߫nOO~Aoƹ'}7WXɫlG8qr+x^<1iak*a Өgڸ#~{Jq"%6MDJn$ELc4N ?W$B u zZv1}I[[1bCH%[6(qP~Hq"ʼnd;ᇾF8$qFsܹ{9ފBAbeEu`3gaD#B(SZڵ3 Ξ[H9KYZl--`eƊ,+zCfB %j5mLa&d XmeE j$$ʃeҢAc>2b1-5sZP#ą"a5RjK\eTdNMYK#[8k)~+ҖtyB 깹^!dG1d/):&Jeh\Sh Zm{l@I-& 1wr)^?3wB*-׸{9Bg-fO^̮nn3%[vH dW7U[{K:8Jgl!R8"]rΖzPENȏ'7i0 Xs"I-طЋ_fJџ4v/?^G/@m{w;;F9P+tGZ4YmJ( v V[0ɒMhβΚ))To'*I%Y'&| iP=en!-+·Lˤ$%+Lm4-Uj~=)ƲY8q,˾[l.w5k֤|7+W~cD>gΜm}i<Rx)R±9f)׮.sՔeF<$1qtܰLB?w<Nf.̹h߶\dNĪnVU=x@i3 HV|x07/hzI\Ͽ6'2Kg*uܢ0JY2*Uo--0PoO13Bxۋgk^?U߷6H8H͓Ծ0)/ΛYE"8M@K~ ,Dupdn!缞BA>7_|#4r[6+f ?{%[$9_c/RS/άy}9IJ#3Cx7cU0vM E_TM~N۞o⛷ޟGmA>7S(xK,F:o->=y-X r0oS/'yTNxt<¼"V z{oؗ{+yы珷0?!WezojNO_")+(+ WW~7%쭥c8~OoX/ᯃ Ϭe?3OM%&_;/R҄nA-C N]%[sM'[mz {!xDZN~㟟H|;xs?ԉ7çS҉=#2V^<~{X'5%ZJx}gΟ9uc"XN0ҩǧ6[S~j1- *r%9Nt$xk]~xC<>!c+ƹN{?NPG/;(%^JJj:KI %O$Z:[R ϭ}yuL^7o+MvY& g|qx<>=$>s$>sڕ[ABv8$wo,E©`N311K%WPs6]'4z\;:~͉#]8x7fR/1U­* p%Ǽ6g KoΗAP)@,@Z,ȵJi e4AFـ>[6Hʲ1{ NnDq-32%>tWΆp0}<|=ʜ(=Uk'ON_ϫ W8ȕ3z}g?O8IWY>{Ozbmk-3PsXߢQk> 3T+ w[(A˵F.{?6cvxBeIV%hB:U{\GoEF.?yAOzKj!so6Q Ċ I0 32q3!++s˖ 8m6TL53e`GW0 ԗuM-:3/sv>Unصr"5ɽ < -H]8yDKGz ;Xj*O4W/D(F!ZrYftJf)^3 T&gN3gGmj)7u4YA (hf]ڣ<@Ĺ[u@9vA/d&- 9YEKFfQ@F&dGK Qp(75({^|ZkT0၎ڊ6W-"z[_(ZՂn骥݆Ҏ΃]{p[5gGhh%{[PUKI s2{_lsڞ s@Ɋʹ¤'od.| `kqRO~!Ǯ2Ba7YkQ"ȕ+g|t?LUY2=oUj͚<={,_cPIni?[XNC3(j=>]ژ# q51X]vnuGٿ!;TI m^H].?PQsύ8⦮}9&2Rf{PyL'!C=eV{!'Yէn=? FUUmr.m9T+ 5\Oy@lܗ+H]ٚ,\6͟U͏ 51guWO ɬVKL5ue+#uq.?i6 0$ Oe.e+ ktAUWf4gyIԤ2%f.dDߒ!'Tj]f+ھ 8visf$f?Xe6q5zOҵ?z˺[^9 eڷ_+5w;\6pam67"641إ]R qѝ _Sjw(k9,!yv%-8 mB:@z*ŗH%i_#PYs2TkQ(VQ޺C'%5UNRHwHMV^Yӑ$DٜHNLs٬Jj.k= sż=>:OH1OftkIH)TB.f5qŚ,ҵiHZS/(1OiR+^ DJuF.02J 'i)i[I#+7JXv« H5y"\-kVdմؖLPrXH|)9G|0O7JzRP)JO0;|l b 3 -SJCIQ81 cƂ25)`B C3@Iɳ) E/I4$0\4 e Gss! 2Uh3e2QYKl8$e] s )lQ˖]Ws./x- ! @}?e0APʹūi-yVA1Gi;iY" LfM=p,"`$1KU%}ӽ3&+kYYf1عO KŇi\JHLZsOJu6Oa),PN:D22r^e rbe^˙-g ^"d Mo0*(`"%Y}[ aD¦zEdI2@I(2l(`KL`X(PazZ[β#) 0%#Urpfl BJH cWs |ȲL#O}촷^إp+8;wZ& htRw)0=б85]LPp!nzX5,JOi` IDAT![K 8nSCsS :\"Kg N\&@9;džmAn|y(WW wdGssY=_DBlĺ^~DD%1Aq\YhA>Rsn; %!j%sX wwj/G=N ".@xn"\#e{e:K`C.3PP16m('U1Ѐ rL8qv$"}u ~̦̑n߼7wDh BJ5HWXy$V-wX{L%nBj"c.ՕmBf vxYMe}~J/$2AN:2 ./5h#;Pܶ0 $_EрIa| !ÿ*5cvGf4R>GGIkv7$̥@,^򠖋F9J(IQ[# y`ngEXQm! I ZWZ\cSZUd&F%|HfW7U[{K:8Jgl!bc`ޒ"+usQm1m;,\!PEvZ(T{ͪWhKF9Bh"!tXF̶"&c. OBufN r|I3gز?<1K"A* 06I;[qx9%U;(',4rNuԮWP-PKLR&OMj2ח%zl`lEtPHKeD?,f(P,BWm/|d{Sgc..,>D(zZZdrFN9a?ȚmV\ߙP=eV`gå}Qa8h$+PB)g*v8U%4-zs^G{d @ yW[0, =2K ,n, x|bbb|||||8e|-[|B;y5kRR>+W^W?iB=z]prcbb̙37xu3O߻/</ǃLXrޛ}f / er2/;\jfi!(a4י jJKO?!"ʲZV3;X*n&R RA5PZgؑ -:,rb͊?j-kC EFO}wx˷Xc,iٿ`c`>B+4j+ E,G-DdYCT m{ K,72ՏoY٩酰VnϞWk᏶و4l,iҙ鞪m۪DEi VTa "E(?{i|(0 'ƾ7ξݖK~gbJ%-5S( GPh#XAA)i?jnHYvgf`R ABY<[ZU <V #7nHMMCpw恻SxhmM0[ +ϹnvM iȴTzlGNi1rA)x{YjJjZZZf:z-. hB.YI=jmV<=7g&]zjmY 7  ]uFz=6d~&+ҧ]{9elZӼgf--LKK_23%%+A F?=\a)vwΆ߿ccEY[3SWNW¿Oa~voRSxOfn\xlet|"?ۑˍgfdt|"~ՎEx:sM_ ߽ş^6x]+FI~M$  h#|[ WuԊlњ/$K.sEunՊ7Sn5+xHK{.N'WoVР?͈03aZJㆵԶ,~scG7҇<ڻ}7{kA-Y_mW?~ A{WAel#=e -cCvl-8q ,YL.= kbcWA+!53tW߸f;M0,#m!\1l߼8GkWc:g/pǾ73/I|7G~ }Jp r^}7ϯT(0sdY&2-a|Ͻ^?Oed+ͩ+]e{a;(٧X!@eST2VɴkJvv?\gBl3zv,`AAIΏ "1KO>臇=OW͜Z֙3GOQ"-{UDcG3U^[r WOg6׷yZ(S38؀jnsB6B_lB(7GQkTĆOzKJq5Pk5)%Ϋ0U';v^O0jQ-Vtic> 96/JҼjc z ae -Ԁ*&7yaؑnr@M&m6vwԊb#=6k' TVj ,q+d~_8fKe6 ԟ+x9@+t25*ؔ 8m6wJ3|vFK}r_Wő*~B(A˵F.!Ǯ2Ba7Y*;yv Y7ʎA&8YM&`MMYEkJlCJoamG}QhEѤp7y32k@KOR&vd_Yk_>7_Ss#=`5yPbri,5D G oU f ]jE=YKgM !g͞{QOKWC ҳ;NrHW0.߾5]=ī.{QaogsWN?xk)Mu_:o9aQ-so^Q֮j/&Aɝ[D<"ox߹'zu -5\;pl=tТ É[l,G?ezl6w[vЄu&s٬G,rzt2+ڟ}v6@z{{{Lh;pԛb}goog5\RX-Ƚ6@ ̶ݧ\z>ۥ析HԴCH(,]k~WXz{{bw}30[[P{tbl`[>{I:@Mj {"**1 UBdJX訷䖮'<ҢۇY'rɆi(6h31#D% ud6/dUZ*ĄTy.K*yqv,2I[lRQ'e#Nsu3\Gm('1yd0m@$' vY6t3rB\S ^+ԋNO>0@I<ԩrIL<4 acBS[tJ=iP#gP͓\"BpHۼi6޿q%4J7-У?8o_uw+y#79pGyTO?k_ 'M_^d[zw7V}Yi/>ۆ=?nZ6pC}~벍g_yGi>xk}RB $Z%8,Ҵ@/p8:'_e9eR /SɈHtltY$ZsY2XNBHr0$%8_Ok$u  a:o15cyP|_=m]_]볧\'MQWFJlHSS6ӧck%xhjeaMF퐄"la9 G.y;~ r\ᔍijVQp 0\4 $&a3ByqF)*GK, "쁢.q@LQMAR"0ǸL|"5@f ^aT\SǺ&yF&kV&EwLbK@S=?$WAΟ?$]bD|ɮHX&(nҫ9:hc|D8d b"icA/ NburrT&pӢ`<9c@̗kC&9 &խ*[+zlrpsq!Q^ܾw@7%,M!Q==AVq} n"=mMjM&D]?}^UwJϞ*c:z onÉ_0 b$Տڐ>j 飁?PDCػS?\}hG_TVߝc ]􅗟x?|oUծ{c=jC/>cCCwXޘ~'|K=Xb;sǏ&|aI:o=MG|}%(7=ƌriS\@ĹcnZ "b%كHrj\( azZ[βęMmbl2(BZy@-e 45w4K41c@Y&4MLTJH cWs%׼H++ u Yn sAf62@Ic̅+jʮͻJ .D II(NFnsAхM]usZ8K*Lo\ &xɘb: 0#3KLaB %\%w08YUpV_ŹAa@RI5D\@dۼ&*_DaYJrtnov+~ec$pgse׋WAK؉S 2p*شes&=v UF=5 0zM\ o? 6@DZbi/3 }æ{7 s weAo}f֮_]'`sї6@{H<H\|c:dl!;ψp=[fqCםOx")ၞa"n'r!,"C=A"\u0 QB1MĆ:|3=Bn!S 2%! _'_(Sjq= 9zO x9J<ׅ"jx@fdj, D󕴷/D\# Í쯬7R ΢ ZILbaZ@|^*D 8ܝ0S|q T#ܓ`u;J߸:Ƞ(MYRhEMPB &&=Ƃ/H'~*KjzȴɤL܎7(cݐH.K*̌{L%$aّ6QahTm\EyZiiK-\߼籇ndr(gL(i26)%=U\?mC lb>ccc?H ccb2L|{\8c7YZp & WޣP=>h+ ݶ"MrPjW K7VpJhZ(Ujh*`G9qQ'Y]`--BB` n-׸{9B ucl.-bMR%tT[s&UFKjSdKda132&ͼ#X.r^aDNf0@вbcEg2ZPc&q _}1GCRIz=Z*I}~ҋrRIh˚[x֟C[GсXb޶?vtuEX驪\?izGghpwd[=M?=x쉴%SĨ'Mc|f˱B+񉉉qX}7l ɓ'׬Yoȯc]c]\a&&&Μ9s7~%Fv5i!Tfv?oRE}^oRm~i\~4}]3FBЏr N<퍎纞yrK]]S֟F273/@AA!6|tS .c' s/6>|B^XϟPo̦o):F7QjG8"҇cG!M |ȉo߷DO]ciޖ5  r-3{ʆL}Y۷n-pDr\AE ñ[W$?$4;Cfguq} YM] BG}~Zuc 9Ru[ px_UQjŤ@h\'hlοҞ%R.6&p[Cuc[ڏH~aUxQŇTMy9"~,0f[6dlT !1Kh調RYO>S p,9ʃ y]"Xik5Pizt YD\>?GGȊ\gLE6vwR%y Ìb;mo0-Vŧ IDAT^|g߳s(ج0PYaҫ%d!A絶ϲdn(iR;w֖bO 96/JҼjc*r =('[j 7r!`K HC67l=c*+Lzװp|2"2>h^SkMnBNlMC I\or@-*;m6;Qhyɤb܆Ҏ}Zz)j:ҘCBȵj38T/""f/LBY^>}+pZm @&ZBBRq%sfQ_e;y!.ֶ|0!˗ r*[:5 =R}WS*$Wkj:FY+jwj-O7>[[']:vd_Yk_>wTDcg^- V4+<;\Z*7mڧz%rVf]|40A+57O>YA5[]gv=&{ Ԧv=aݟrNchsiBLHUİ`[>{I:h;t0ۃcǏ,>.'rɆew@y{o]zl=M˓Ǐu @3坽GZ45VBYb~?D*ulM& ;f]]&UȀ-i=^N :}d"iE8nEb6}Pk滲fWf9r.cc5G|ۏ?~UsGTXAX/.TLC#"`^FU<8Il!D D|Aa: % jrA %$Y0$7Z_bȬc: "J̼, F)BN̒3HBKF t{cj AJ(41e^Byos8* $_'_(S7rP H8&)cr[ؐ'X%_'EwT\*K,PwO. 4(G4 ^c.'r2c.#|a6w%6؈/,TϭmFH ; R_aȕe j^i&Rj)9я-DJ /ˈVZ\d$;83Fo4{dEUn|o栄-vIPjeZԵksʤ\fRoT$rl--`eƊ, ^2K& Ƽz[i 5 qI"mEVViZnZ5Krd;-ej}`j_9FeaXXmi[¦J HNmU۬-Uj:u39{N_M\yPoG@9O ɊMV'UHj]X?яeq34񉉉qX}7l ]ɓk֬II7#vVG^{;Ϸ\}.MLL9soD"lBݻJ՝ׇFЌ^\)|̐-OΔ ,%))) >-E -Q 6㎊ukq#h0 ADJ_; ers[]/cu 0j'8g/%Ϋ"A 8;L4"0 T8AQ~݁:A*HA>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA6H*a='uHmP7kT˵x   "Wbn!ر_Up&U&   AdZ6 `xH^9k¨1ۯQow6ֻ^86kPDxBWWjDx:߯ۯ4XsAAAYFC9x$'vf92y7W!!P>>8D<Ң, 1ð"Ŷ|Nދ^"u?<'iɟ6Nx  [,%;_Qk~j7"S cGXh|=`.]<`lLK 3fqoF̕~qgSBZ* nwưAAdys D{;fkGlL!&1zZ #EUeXSW0/̥bPh!jKL:mKhqv83!3gJafV2SKfwvb@ Mz`@'V)<ܛs=ܛ?Fّ+9 H<_k쿼C'µ ߠvryrcӝCnwO͵#MOX-Kp {d݃'X[xM nܩe"4w&Je13BpBBR*y/U{خKW>7?dZKaI51sdiqCkӡo,Y9M>l8c$1q c'K,Ye|6pߗwk//{1ep8ENNGo.IÒE諐ϐ|יпxWi2á3}~jڴ0e}i!zc.K& 2lt$o{ߺxWI<ķ$ Vl|ROj%_{|n=ˏ߾i:z8Gn 3̣u>!sxN=SvkS+~}5m`Ȇ?nn©W"u:j3W.:z)~͊߾_Zw/?媥c}M5Epw.~pzҎͯ֒g> G//˭ L֮Vy`A?u[[S1M~#}IS)waza#aR|>?v -:u% qo_tJҨŗW> qBHJzfHCpn N(n?_}MxS?y_=I^q.xnerhaH{XHY}χ\R0vӏڸH2T+c'Sw/Xp ڍři;XIvjŅK{Н啡}^⡪e)%9y8j(P^dS.2 _IH|e1咊哲:ڳo/u<\2N `ZBwi(>C}O_Bn8*-0|8t M9#Fxoϡ qĺɤ WM$}K} iKVb̋+#_~"C5,2a*Ї70@Mf>#_9z8lBʧ$~zfϩ5KϧUŤݑ3W`| yi4~a D_R8vj!xv~X\?n,?_{Z6aU&ޞ5^ogkx6YB+zFdىBL!b2eˣۡ_/=b͏/\$@ Ħ&D!{~EJ?`@N:<2d!.{" z/wq#t)F`٦3044ŵ`WgC@P IƹpGO={Up)Bwd\W䅔X+z=0@ 784)`ɠwh$̏\߬Vg_@y5eG\-}Hg@VX"ȇͧ_ bJq]6{ʬg/Ny˨ElN\h4% -tIs';pj@@!>99B //XH--_}n!/aW?TF@ =|O`QC /dŤ'/Ei DTaT+~£ƛ'yT⚵3G~b.[=5@Xpaqo)@,y㨿Iy_ҕ~ u^=UH<,ٻ977㱘bO"ew bd$&&^sϟ3ͼK.-]?/B{!B!4aZB!B"B!´=!B!i!B!B!L B!BaZB!BBB!B"B!М'<<*oV,tI$,B!ҴzCCC######O9bbbbbbbcc"Č <YL B!HB0<<B!L 9!љad0B!O *Er B!iad  !Bb)gB!{ B!BBB!B"B!´!B!""@h)=?ÅCa9"48Lv+ ,;|OOg}&H40~11/\_RBhaYG6 ,Y(0~0Es0~1-Dh Xhf%&&(_!0~܌_L B!BhNô!B!0-D!B!i!B!B!L B!B-}7LҥTF%/fbqC Ke ~w'"jK =0-QTK{Ô*`gev@?6< 0p^8 aۋ@z`zIm5 c2JʍZu-oT:ݽaaJk(+Z9rڛ8q}q+?1Eձ:4a) RuVtt[w[l._BIӤל_ UM&uhdtnSE1E.?b/12ӳ弖RnK:,αo"ҳLeO>cX^@/)oqqfф3vU׵c=rd\{kw+mFF6MpqmFEhIn+LHW%vmԘ.n4׎ ̧Xԑ IDATPʦ'51E2N6X-7;8%w-4᱌#9P.utX*ID#a[[AUCNM( B)2^FlsgH{w]$:N=hCќ^n+[u%uo5ztsEuU?V!;AG̺ÝbgN"n7utk2kf4Cʌ;͈mG:::::5eźiFco BM&WpRZ\\ӫ(kgjvm\lrM:>8ָ70Y[:av݄~儬8elۥP*!'ź _!?3H=4E5-5;IG %nSڻ0A=푼,P 9~gl۷y_l߾]͏o߾;?0݋.8o߾̼mn~@O˶o^_u2;@M@Ijldyu.{՚l-穯wV5TT([׬Ֆ[ ZZ.Rj4Z/o v!&Mt56z9<Ż}bARt uXchf5c{ahc Kۼyn]ow"Z%ή-N($%Q0Bh8h]hQy-:@ek謬,Mq~@ah(IRWD4[HbR1rm1[LzhksciYSQ㲛dS{wydr56/av2*sS #a[jÝ.d Xk"Z+8"Y%qxDZzFCgTݽ|Zm]^>z,F{\7i<5 X_ijTږebnFR0r-#-sf[dHW]h)mV/jrT=T͈<00k1Z}"E'(yގ>HSǞH[^VX&]w2ss\+{|@ I&Wj+ꅑ'!a5+W>0/◼^뉋K{ux,S ]}êT]4 96Tb7"2TEr ~;d4KNfeQJ(BIJ>txZ,C7a8CȅXXMHt":(EA6v9 ~䴘 E ܽ;Z*5Ѿ0SKRUp~G;T ]P%WKYgpBm>VB3H^aѝ܉4qRp'.~i/˨ƞ}ѓvp{!H n~cB|0p1Ҿnv`U2!ς[΁A+%L2y0 }F?%sh v8!? /Bdj*p;+愗XVY@@7DLhBpwm>m%xIXnFQ#Sܿ߻JCFjguVtTvO({=JHD?@n5֘k@dn" ,FP2ڏanv^("O5Qlq'fvbV##1jYA,kr }ع6wHH.0ռccl(j߬qGak+E LM'.i8 'u UH-͝,-nnꮳBTXS$CjV=js2 gA ,"zƐDPkdq8XLB dz<9>L(`CHhFRow*MhzVxg_P!L$/(}Ƀ>$,"-dmŬpV)D 6nm*uﶍ. g`km>c|ŭ0E0tKْ&BNIiZhwty([tB-PdrT:~P]SDI8l XCR|yܤXDp 9TթzET~`M$% '`?>[!PYD$@jv֙Fi-|Vxgw"%;5l`/p⭣݃z.O.S$49o<p{b?}_ֆofj#Z2P1GorWU'M|JF5`tDjޖrc=cTK(1rF3M2s2BUl.q@eRyakLh K ƨNPWL_>#mF7RKIV+lm A5NMtگ-4ɨP$i5,.?p7-fIVln@dp|Q`=>cy ciBBZ'^CuFL&KU7nQ⩪RVTYJ-"=vSi`M%!_@{Bb~K CWY2}X[M%-2dQ#; vtBj|<*Um?6X(ZXYYajw\Ha1'*5HUٰg@[M;i A8đbl,G`; 4^_.ʙ@QQiAuyvuUKpiq}uUN 5WEքSUz_X\25M&$LUXCӴ.VgI20d}>aA-o5XZ7i -e4ih;jrwfҵJ%= (o )̙I4%"ɰwC+JzV;T2|FT(&\pj܊Pxm~rb8~|*#NYXA oʙ>vWSi: Bf>43ncw6(lUWMi&FǢ[V6䤊M! (bBZ5*: j22Ȩ.6{ C ݢC>c{*}f U"r SUeJZR|NY 2T%[uZBt#-%vz2j]bwXT*t:t@(V2ZZc(ʌQCJ ,5 ܔ yfE 26n]62MdL&ƺ""R>3Z=)2H/m-h:K,Ojj9>'k JôTKS8#Y:%q}]]i]i]ƱҜ":P&T%&&kvќ#V*+tUTNJc}q}X\ XRUDoiη2Ց!RH]; [R,E5"ªiZխ(3ޫ UU $f{u<.?;U83222<<<44444NPFo !.)Dnx`glyƗ>ZH_sK\khmUe -dɨ&Ŭk8ƝQmvtaĕ^|v|ͦ-A݃n\sˣܸa-J2m3jYFfҷʲ/(5&l>xU_OHY ,RN )7fb,YǬ]!-9[ƣEhEw9MY CCCwo,B!0-D!B!L B!BaZB!BB<YqqqXCa9"4Pu~, 4 B, _!0~:w}-< ŒH$@ aiLvC=c9`bc#_9< :ۥB!_4No,B!no"B! "B!!B!0-D!B!i!B!B!L B!BaZB!BBB!B"B!´!B!גwR@sAllpl, 4upa"!+xg4}1222<<<44444;>+[&=nB!B(*`|x@Og>1 |y0^j?@ak6() /x`Vf<zrV-Yb2 v \8W?izfjDroռa=1v.{\OB!Ьssh}}WRW{ۏ=y_RtGO?u{RkWO}?/XR]LLI`!a4MWف~~{!ZB_{޺wlzbaﱄ0F!BhNwV(% R}kkR8e!ejI< $.H/0/MaD,\0^`R}+ax JO\pU[I6z !%zf$EZ~ X90(y仙X&!B͉p0qWLX& |~Z.」)_ӫaѪ5 8r`aE/Y8s d^['x%ҍUҎy[4H@r$.@ߣ 'tV(/J\@{G@t$~5a[OoĜ!B3"Ɓeô1HX([1]D :7?Ҕ6*&yG| YD-/\JCB$'dYv剉kϟ?"t+sY___WW13tr>}:))iѢE$IbqW\9}+CG~чN~W}s5W0%KtuuaZЭlrrD"UV x &P `,$B_|1'>?c# ݋OII]f"W>[׷|r,.%% I'%KH;&T\\\rr#!Ky[ky䑤oW\9{,wb\!Bh2p0kʓs޼y^llyCҥKBNIII<ȥKfcF!B>\;s!' yǴ!B!]k ќhdrww(͌ mvwwBfE"BwYniFGKz靿\YUCaI#´) B!NcAW^ 2T9 z}Õ͎u[H998% ōuمEYC)w%E4G}n%!0-DhL; gۨȺG{iis/(_͌gzmjZGL%{!;RJ%j\:tn]T Y:M؋-@.!N;J?K6TlvxoeJ0jJ6?5kJ`[JRa[Rc!kj^ mtsxOfRipjFBї8kDF$],%y5,XQ*mu7hԳ^g%_M3&{TV3Z+mk]TnwFc󷌗um W)8ZW^SժmghuGE WR֯tX,L ќ ]aBU(RU79T]$QlpĊZb.R5赊ҷݰ9$/UU]LM7[@*],28PE.SZ76VUb$>[&l.25 FFw (aHuv饁 ^i]3!jtQd0j)P"P$YVkwZ-t|_B3OU~PټRAᏮnPdM}$Hg sv22HaS@~WꝌotYwME#vPwngs>RM458($꺺j]uSXXN!2*O ʫZAP(xUWљQR]J7Wa+ષCJBw(uE]Rs_/6vB?6w(=##]asf%w}K!Au 2 "a3Ս^}CD.&./c$H\/T4׻ 5Yԍ!> d7酱~?(,9zamϯu%&%S"qLFjqŅ +2oy5`m5fbCѬ׶󢼺*Cd\!c 5r9 $ 0#Z a"7;CYeiqdEH4:9t[=uP0x]2HRHH٤mŭNȘFM 7k2DmK? $)nmkmTBE婂T+ aTŭNH(Soi30H+lfxC2D ,RI.#yĨI :5XTs 50Y*2뤾ݾ^W5ݖN?ρ0ȲBhmvs:`VJ_FIkڶ)T:y1REь;D+b rXTn9 z9䗲t@UVW4m=jޤWdX}+^?UF{ ci.O}k(~׌%=fYEmn6 A6ս*ik1ER)Hn]퉾vk7P{]]w2> V+5B YJU.^/ksr )k!pu 2S []^JpiX Q" Ȥ3ȋY;Ð;HՙDs$k$naCTfL[Ќ(9ҤBQWQ5䄿Xo˭ ^YQԵ4[ҩj:^l)69榺,*b7+tH3R9wy12 2](CkNJźz@]nR.([ ;YS5Z]d+ VEʶZGvjXA[V-ދzN yGo7o7l2 $޿] Z-u{2jLXIJj_&[gn&g˭& ϬY o9-HsbeHg/[d;- J,%$@ r7{v/I uZ=PxnSV\6M~rg$Ҩ$"h(#zIJ*}4ITE)ik ҚT;]0f݋ nH ]Q?NpTTTLz0''OFӨMR55E&`,jqC-J"Y)>k%2zXB  i7:zt G㿇,@hwݓ&`D{b9`%+剃cs56Sv@`Mk\>T^~o$~b ? L/W?>p@ˏVox@}ta@HWjQ_~C//ŋo궼/V\|`'?<6.[ͣr~rCᦵK*?-EVW_<~W[9owrx(9*p˂+RHid0ѝ—Yhxˣ{4zeGS]]kzؗ[URL n<gC<yEwXmꍪxSO]RfTHE ۴?{v}1B$%wDdto:?+;)& ى_- jlZEGѭق3ww;_B1(A:CUt-[V0u`kaJ%ۑ$vL !?B `UA}=L'?=$WιygKXy9wԾ֓}إ79ҥK;">dVc]kN<% 寀{\B_ؾu$r ,?4_2;vaexqI$|ơϜo3ke;__!"EQ2k7F|gO?C&\id׊yg)UB=,g.ш+>""r C]{j뉯G7W^W7%[QdM'\v' ٯŮAj ]ա'ݱ/ʓz;,{Ϫ_y~ُ%.7"^2=pD}00{|:0!%뗳dL  'ٯ*fx)6_f<H}R㫺K  ݊0~MѾBݦ?PlC.w_^Fٙ?#o/ttJX穝I|~؇,ISξc]V@>],O$j+Z.{Ǻ/]`ZAny?#I N1q.̌5L.#rvDOήF|]1'ܭ[oƚw<yұ})Eoٛ ۚpE믽sk)Q@4!]NJO"#)$r7.PΙ#{cECt6U\V.c .{,$-W]{Y1þgt>3#RFc6D&r|'^&rt^iUcca6;SH>;QD{GM $  9|TH9'΃GήuDzxywӂ5Ɠ'?Wm/N&Lc vg?LvSETH36lX4E/4MTzohbв3^l<4%ݜ#}i[3/q~׏t|[y۱rLJvTSF͗[wnIvm~JM^ZyzbHXlCLԻW?ے H }ad+vVRN69˷.G)y?^$N_Yϖq%3GkLiH2_gZ3蛏U//\4I$ r 6'錅eNԘ0Hם=Q;dAD .((hBuG`غ $z҇)AS>5U{MKn%т&p .2 IDATRv;o߆B69h,XѡCDD౵xo]\E+o8o 2~F+n}%9<#W6܁tCn˼nza>"bC:Pv]!hVD=b3XW䵽l +[.qyZ2гTаf^0T'ҥc2.4]B[IM8eAEG[.FM >{GzR(R٧ϕ|W4\P W)LNW?9n'n'r=;Q)Oqn{(00xh4@_%$c`SS:I\ʓ^H:Z=W{}2WuݯB8.fC3m9w}(@]=?]BOw?Eݢ "dG ]DD"22|o|7~ v3J ?ڒ $[Qފo00g{{ xH+1t̲E;?\QFK|[y]o쒥پuӓްVW7]:}hh |}0>ӟawo9,|*REN_|ޢv™`  ڛeYh /̝@<9'8-9eAz}שGHD=TColag|!&O.ڟ=40~N??Q@U]/KuEKUWƤ񾴌5}¯1鉄~[&tY_mo{&\ϏD_ղHC|[8o޼1-T^_>ph`ov9O4<=Sv)keٟ-{ok<2鹇K,[N3gxS .!G*G?Q@4>ݛ,cQzQ]~-fu[^?TOlK7Z Ru-W̑ zNo KO.[/p>tAeL0v3lEg.|%_v9)1g),x# <0f ~@6(;?İ[XC=3c]q㤐OkfF,")ͬ=7XX*[.Ak!b!<ЉR݆BB@,Bx`uuun]HHȍ Z@Ck!|~Z     Kx\Wߏ F0aD"A c/_0 IRT/ 8 F 3xvZPPD"A[7r\.W@@dB{k׺ pgB w >h3waxP#|G? @*:˅:@,׮^*JmRիWQZoo/2!OETb!xNp&EU k;S T7 b! b!b! b! b! /U𵂃Im̄nh-NYOv8z; 㑍pJ0.К&4UY׭.*bOr 6T褔0g>Cn ħdiCx1b!u_Zw,(w[e=A;,vVW[Aiy,;lʽiR"dCIPfHʒƍpd@,;IiK=_EΝÊ^nN*ڤFA(mc\8V5˓V6KLꊭs=EfV@ܘ(L-/\.km-y_t\..^+_]gTZ clZ$+;1vquKbccccc۲S ""gczlխccccWeno{svd]vyu΁vfr4w TF|CIu(1, p|~[U_"oua| Of-uW%k^hܘsQ\QUUQn9`dHw TV|ĉwSٶV^o\DŻ+Es*-=ZXGJ+v(fm,#!"TEMT2”ryd>q.kqެRN9lg߰w,h<,K C*B_ۮu@#_4/q}2ubP.Sa=`iQ -*FMdk 4qj5Yǰ,hl"dZ'm64W9bT$ZZm7k/܍Cm+-ORbXى' zLD.k s^" KZ*ۜdolax"/y߮0,CIE6h0495ꀱİCΉCZJys[+"4ו״9(]A2UdDak.;`:p|=K@4tIH#'4RᅓU Uɣ6s|G՗qM5<q]ʬ0^Ti(:~/T^a|q~(R̰iHpźdg.ypꨕTmmEYPkV榥%u ? `<.OEAΝ;7o޼;tO8tE*80067D=OC  Q睖' I<; =zx;=ꁨ2YD7Љ` ]=COF"9O@,|@O&\O@,\k\r\Cpkh8(?DDt % gDCؓ}\z5Ȇ.߽}"j  PQCXf+~ubpbp8wOQװCϰ.wRʉEC9Gb!=R_]q?l@$k"%"u^ҵ/PXp\噍[ G Sُ>Lo_/-[@,jYohBbÐgrv.IhN}n{ٸ%16v]Νcs[sX/Ke:GyKR wiqLEA^>Y:j墳?˫bo4%,{Ke;{ĉ'ޯ(HS[J6oLܲVj%9Eg'@ܪiaR(*.]r$' K"!KB 5'a1,.*ZۜseDD֦J:Z^l-7 oQ|Ƥ4Ucf: /9TJ9"_5RgGXb#"RD괡ӜT^Meɻ3ÆTjM݄fr"Se4"qYyh,kDz.oK|[ EbTQiIa|}rJ[|qi&ײPt,2$2tr:n<0`Ugx";w;FKq.u3"!kls7YmY@j4Sjim۵:EPm%":J)ݹ%%m gJ Ĕwk.NU7Db3EYE6ڷk MYe ! &)l-mkKeI|`*$m*Meڇu7: EmGݝJE[;˳1=6(C`M|{29nwoNcj F gJL "7Qԉ ܏ˮGk aSxG:(\<%SE'˛DY]G%B\BQ)J""&95/F*%"*#'[[C*%E,K gK] 8TJDҰ$w(˱[ywlsl|:66669MQcluN2!0slxnqJQ1 b[[laR"iXJqU@TNȈ^g5r""yt̂JPVl>ad)[B~BS;yZ# Zﵱ2.:%R`:dB,VɊ<V̈*2,xwCKGGO\(:xo+6fm̿aw[8][ %׭[nݺqZS:L&6%+(yLD,^uДm\sYWenou4gK3;U}} (\.5tg$k(x2LqU7t41IaRyLj+E1`JqeD$/2 (Zx eߐ 6UFN4Nd %2 osZpWM`wU{##% I\Á!OP@88:9khOd1<ѴqX\́YY%"<)9+HQa4n^^&x}P"LX&>4O65q?d8UTj.݅LXrJ:' _6iTe\StkYEJ(te3'gyCdTiiC-DD$uLD I)ϕIƓ*ԤFgD!FGc R;$V`l\>QA8wܼyΟ??qD?oB|՝_U"H?? OB$I$?HDAwR}ÞM Xv{@֭st\2m4THA:|<ҁLǞG{frz鉫qE B&Љ ޣښݏ%.r&3Cw[k2}Q=źfCDU/K=u5@ؓx3%\n-tn!~d;iXR$N&t"~3^50@R=PCDD'=B{A5< ry~ SQXpOޣCYop4CGOM15ذH@|OB{-'{6E#'|I *Vf3Qx3`ԾkgZB{F_`\Cqot﨧?r6!tԿw))|nIv0z発ZgcW\qZM^WVgxUJAAJ:&ź̹rr֧//wexղt!kOV'PKh-O{|MRYz㎃θ\ՏZI=wiL:g*=Zi/2Xayi*F۽';qEAJWg\Udn} ݔUI0$Gd1ĐLnKu\1~n7`wz>qSm@q9ZZ[vmNd0T[uSŲӨ};-Mbrގ=ۢm{(=Q63EY&sT^Emmm^ՔSgwenjswb[@Q]gi 4#xy28n IDATϠGX(HըäD$ O0zM|9cGc;K] 8TJDҰ$JDDF &3V#'"Vp VbN^>ZcN4,>9ܽ\BQ)J""&95׵;m\|,)UY3"::LJ$"8F:^59))2"JIRDD UˈHP)` _W5Y]\.DrDBJk." i\*NW°;e&Cm5Eeuy(.(gXc1چ!z8hg8ϖ>!2ឲJY%+Ԩ1HW<9|Vgm**HpeD7P%%ϒ<*)17YL^UEnd1ጣ!q)U&dfΒ &J]*%"yTJ4V.:|19CMwoEFz1%a9mۍOET 3G`|ᆽD/2* ˎOt Yw9\TRH9HsCeUxe,do)*3DDElnnڸhciz]KEEyHw X@"`QULp;뜖9fyq09:Fv"eXVX!H8c*&JI8|7yO$#"yXјSKKRr6oYnto( MHߚN,++,|w r *#`|B'RNd$5P$"6Fhp@Ddoܞ]XoȌ(Zya5R$"gkesŨJDBGeٝQ8)PƋ;Zk ,9[[9"y]y[D$8Q'iĦ&+YX؉ޤ0Zߍ\a0^1&@?WFg.(Й Lѧʉ|"SkDgJ-뚳rId8UTrR8S7PV'8E(dQ꬜q .<9C̜eT(M" b! b! b! b![-]*ID$4oI|n{>tkݖbcdVX.1y+[+%;|%8cc3C%ؾ깭gnr/>dݖC{[+X܋;%Ůu0^2a捆.'ޯږj7nʪMǗͭڝ&%rژUۓ~eqEAlö.'0'N8Q-I.^tp{\NQrYZ,Wh6=ZWLMN>uwmmmmն$"mEEmyUu*K]`3+87Q/~߳^UäV';JyPdn=y1MrdltGӴkDS%i[mmmmUi*\>Ki3[))*iUվ][Un*$\aJ\JDJ4,2=Q?e=G1јS.&ٳgO>DЄ}*/RU=h2I-`1EB"X:IV:+)#Uͷ;H,uÇp7UYMJ "H 76eWZDL]< 5uV0쭕l1C "RuVf=\V|m˘L8\Aޚ]!ã3ٛ[nv):"PqR"癚vQE:voouT fXy ,t~%059D"l{Ng*+m*FIژy` ggcYh$ K͈f""gG}a=<-c /9Љm\IQY"XuԹJ"apMK9N2em%+ Yϔ CBXJ ͦFbbՋ|s˵yF%<1\)i9aꨤxUKV6-o8&%Cc|8/%`\Cą/ӧyD J5l:I#2 2D"V/XJD bQi6Nݖ'2!)1d-7DD*j4-alձE'LTsьߒD0 ' Ν7o:'Nr3W۪#srʴi #2x&$iĆwTB/&sO'[0ރaLGQp        *K-}/<=v.W.HaQV:o&L/}~6_` &?Q _hmU'F_0){a ]C Tg|thC׮taHsF7VL4"L{tʫܴSG:~3azS Ha? @|ŏS_h O'3y3~mԜiC lJ k}#_{}??Z|*x:ς~G_D `$\vOH?&>rn UO?0KD]_\p_??WN_e&L!޷%Je?{VGgsᡟ&}N:t"`a,>mqk?mhۏ"ӕ+^]˿oŸ?Q0I?6>ߠ/7bjXcYmq= Zëw>cU QъrHfҤ Ibnݹ]S~6٫UGi9M *dB[Np_]Ϸ/|4Q35C%Q˭׾TCǭ?~yHD\5WE=z/>ѿT_տm%#o3_϶ϒ_=ٕSN=tjGZmO/?xSlf x7%jftǶJ{щ'\.j=y򤳳Q+2@BY d!,@BY d!,@oZǏ7f4[?~)  *9462MS4o?~5|* =e}4!v(On4%!`w9rݒ d!,qB`_*+jC݌u*͕r };緽=$a+qhщy dž辷LJ!W}g}ؽI]}'˹t2}w!"} fNLJuGo~)v-mJ&__%cDD>m1X0pvhyCč^I_\.N)Ur WכP(""#.O/=2{52ue_-}oaȂU85cwH{6xWMX[HE.]v5EXV콥{}'z9en3ww9Bzh }k9躮xf,FhKm!]#-;L$G#FXdzezx =dW>q̩xގuWSSETfn]|E션ɬbk{%WlR!/\x\,޶T__$=9̥sj}iBL7R@B'S!]z86nZv1 %H_@ 1CSq[y~LvêiZ]7KӍP*bB&dFZj6OˣlEZ-jBd41:_sSAl2`?Ow /TyGk5<Φ04PXrPhʭ+Q#ѻ՟fQ*ڕK6YeoXo\p[/Mkϓ—Þl"mOܱ؄\Lh{~=Q/YRbny('3$̖7.71bjcB0W_Y̖+/s$ }Ϯ+"ߺ4Rŭ KK_^zm(ve'RvsU^sEKT}l<qjVin{qOvV/,T6?S'[7-떨#P_He!؍*,$&zFS7jDz^z_,snHQT o,*znq6cT"krKpH{oDB׻2~*3nSfʪO?,37R/*/2Эq;0xE+ƒbV%#oS}k+ӹ-"Vq:WDcٶ(Tl[Ԯsݕlh]&w|ҙ:3j_~RU$ӷo?ATy^ ǃΝhnmGMk%O9AnM=깙%R[*"tt)Od&a ]*F<ؾbvJaڡwgg Egqbbw=HZC[cCsUݞK';ieщߍ8"^XGpi(Encx'3/w:Z$v?D`\,T1;~#y"["np8>Ϲ-""׽Ma_=61|vϙ*"h5]755\ }\v15ySF$e" xpӏoڵ'#k^c]MƌD*݃c_dro$CCajc[D4<7oP`lf x'VWW[Vl6ض_iq=:q˵ךܣ97əx1`~uPɬ ѳCr&OܩlbӘʧw|xیRZ&Fώ >zjZO<_6"ۧ `Q+7n(}uG*oN˼3UM`[7ۅЎPD_7 }ɸFmv}B][Kol-@BY8Czd!=@UՇ>}Q`8z<FoWTVVV&xi ?~ԩSr:;;vKFd!,@BY d!,YwZs~ V?Bvf_C<|0s,k0BvرgϞ2 `w>{رcx8pɓFQh9sn?ܰsqYJKK)--p`%::Z $""" &`ذaZBWZZٳj k =]tQKNύݮ&rp:j;(􈈈(􈈈BBBBBBB(ȝҥK?~kIEfIF ĂLx=p3g|w?{ ƍKg1!g̛6r̞W[@K¿Y_7|溼=sk۬߅;u_5 t؋h+V-mc(#GJbSag WI nvm9,G45)5ǜĐ6c;`X0F1}R_V\AR=1ͥ eڬdXl׎Q0~3XwaB 4{ _q#p<ʨƐX|ČMسW3+arD`Ҥ7gЉXF;sh&u1vi>%X]pO]DZy?/d~oYqۈ3ڼ5<ރ<}kYYm53sX09y$5iܢ/v ^6Emgጃkz3ox9WdZ~9s}?)ó]"9V&?L+;GXto2/Jʩ˃gNL :hpwa6<^ ? y>/UvL$o{0%>4iOH"w3ރ'px xa 8U>Nj Ù֥KlqJ0t_4KUgncyL;`¶~y_=&mbN~de1FswG&f 2Ԭ|I] m'q;o(bי瞱цM?L3L=]!'j97M<ӔO6YįKppF9 r\t݄7l0NߔO6aǏs Y[?#||] .UϦ1<6[T{m'߿PLXY]#xM^fqŸ~)'gqõtr+3$ -Ozg.e+3eA{] xwtes_/#8w ̡CluJF< D/EznFѽQ sڒCq7Nk呼Nz^YA2vQ|ж_ #p$HOm(>c\_`hxku>^ i;±&.u"[b_&.K{\ʾqzx],vu~f‡vLjDmG~ |򄴋oA˟Ij;Vovɽј#8V%9z7]ݼmL\!p-y⒰ԧUCz6r`Ù}\08܃"Ǻ50zӳ -G<[~Fvmgʾ!5h^l]M~S&\e݅w 3~S.6ס]p:?z0m(=ۘX)~(+Ǘ6ܥ5aw`sqHM5]lG3ח?iǞ5;xYB2v$;_q!lO^Ζ|'14,i;w59 I$&˲x¹9D*lI]?~tʛϦ"_:Ŏ \>jE;2 %xG}ٝ9G-Y}pww>AM>6[ Lj㨃lcPس2o·8( 98]a lظF?h0Fp,XӞdQZä*~<0ccS|9=w9b!mq`80 &|lp8YaYsc5 z8Τ_隞5-L<n ;E}H]o&&>ee>SQ̻U牳8 w5]Fӥp88{X|s8kyb+OSiҭ͋1A,RߗSPHlZk<\Vxs4x-`VbNp2ȕlo9|[|c6juQ}UA *B#6}ټMܚoaRL^e@@8(ZbvA =b(Zr p64f}dlaPϋ>up 9o~<0|%o ̝9>! @3^@! T ]¹]cGڞu o`0潏Q"Se74p`oÆslF?*Mh˾PVm4t41=⭙Q!\m`V& c1k"sbhhOJ fU??#b2$f͈f:md۪f!( ~/9>+2ѷoXmiW9N*ŻVFFFblC,|}ח(SVX{D=)v,x뢯vWŐOVWy$i-[6ju7ǍA[,]N3Qzx>_O͞x7a!+d-u;|Sr(=xėmӬ#"7熹ؗ $}>l2*|:PA/1x/^@IQE%hkL8M"pQx=\QD4\K/V3g'?AD;oK 7%ͯ/^xPۑĦKs,d=e?M:7+'וXw:PU7lkI.<7=6,6#~빯uHకayP;O`ө'!v2ϑ}!)z ;#n8MO3V;C%QzQz!u_[4:`R4%Ҭ֒-4lOg fz+sr< &?ɡuxlgׅǃWǗ&+ye7[>uyw^릅j!ߜ.RdWZ6f\ҳ.<^ 캥ķ%I=nwEўT׳ye;ݚC% *ԓԭ9ּ3w'z0Z6fe }uq{+ɼi}Oꗛ9БxxUXE&)#=݇=   9?opqpY *i,/3n(>^x8-dlMWFw媂Ǫ-:#6iN᥋:7dIkizd"O$pCdt7c3:qOQ~&0?T5aʿ/ӧgڬ<4"<}:ӎ C7Ǐzā8?M a a 6dkȟ6sF굋㵸pLz-X=M- F9)UM/?iw0rlw9NO&+!m0v`C_S(Ȏ6qܳ5_z&uzȝ-Ug6?q'dK^?b'i0y'];U11ek av gPlw\HW ;}Hn/$b<6v͛X=/e~(Im1Nˎg/9""""wq:?H֝ʎ-yla776ށ^T1vy`\qi?,3}M1lkWܝC$U9X+UY5R᱂wI99yN`x۶mۖ}f~sOCY®?8۴+D@+`(HNY|sD "{ As%E>HΑ<Ы3H 2%_$Ys*d4X܅;8%2w'GzD.Ϭ5L) m2`aEDDn;76͙ƌuo&l%vj&m:Hq<^@+NjjFnj Nx3P;iN|Ŝ$""k=ܴDv&"`m"yF akd66(=vBnٯA|ޙs/,'a)/l5ܶls}/Dq)̈a:1uH+zLqc(f&hBj`ϩ)Dnܹs8{,p8}cݷo/&%=H>ADuk'}BwƤ:-H&|2zc:è[=O#I*/'kb.K!}W6DE'fd ]B{I`ݜTVuFJ=Ġ7FzvWxY _"lB[;1hT*_Msz_ʊ5od4? [ExGխA_1swQC'/cdm`kfqM`gW@3cfʪ0onF jKފ J%3I]ëO=yQ>ͧpZ]WNJFhpGDDzDa7 'b CF>PŮI\m:/WaCbfQ ֛ p䑱~%R3q`#Q࿔y|WL@&-dF7P,ō`vC yXr1`\VJƷf0'.4Èڊ 3Xw(l鳧m!75>)؀e>[&G }^7h""AIDq IDAT#"xl~,Շ ]0ZuÀ9j1)zXMͲcxESq[É]R =a_uiY},+`h<ވiči-xBcY0/V'ዞctd8m,`KI`9.GEuOn<,N?`5E'T9ؓߛP;\DDj%8PP1\`n5w!zq)WL(ÑJWWsOp~2O <""" ="%{zʻ4O*:h5?At}V\a U̺t0)S3y<u)+30-]ƠᆧٳVZ>o0beLaFڊ,=QV3e?8[Gӛr=Ǟ Sۂ2Bm03|(jr:jek81[a>tEXv<]n,чQߌ  b2FߦLj]M G'Sh WROz_"""r)]#r[Sunb?o?UA7e!4Fp?A̙YrykffJa0&{gB`w}]ot\~-4Bt!ꯪ21JFA,^d,-K22wshL-g-#aWU2S&uНDDDGDn 녉OFq#={eɘ2/tʳf2xYSII-LX:l믜Jv7bz8| sYy11FXq~'K~EJj* ϊbB(\gHRyoe,:,'&+2S|a<b&f6d rĉP^_c4âvf2WX3$]\w11v>m2U^uB^HH#??+Ս!nV~E0O7EHEDD>qx{r G4OcXLWZ摞iԛVfWZmL#RyaC? J[t,f/ξ76#H Sxa>!#̯V,5,X0>rigՈD/m)SsWp & &!tfHXu # _ȬK?a#oΏĊdgÆ j ۀ9jo<ͨ8Gf )?'Is )nLҕɩ{a:gsq#?g*ݲ0k)pű8lR&'5~3cl {Yϧ1xv&P Stݮ`bXϑZ|6PiYG6Vâ1Vso_ۉ+nF#c%3.}4>kʡK.j ܹs={RJKKq8vrssoL Gr$Lof%k ʜǢ+ubE ޮUyZ1G1aYHʮtmhLfZlO[9oF̬dl_g}vF|B@tyb2V^Ąijȕ`ϩ)D~3$E1 АK1:F"""""#"""""#"""""#"""""#"""""#"""""r5 j;Q0×mgBDDDD#="""""#"""""#"""""#"""""#"""""rK'{ߝĉxG>y'^^6JHYQs8q"nT\DDDDDC˲=u[+v2Ą+]."""""7wzג9\x%:'d*O[dpݗϗ#"K^>+xtg^K`_'%eFLbR'skˏSK#N_r>~vb^x\/8}E^#:iVMd!1C°\»< ٘b!lP,}<)YK'__:ŎghSrۥDk_pNv1@1yl#Nk w`XAڻ$MQT>v[^nA^a `;;ِSRii:GDйW$f({xҐ@Wq~0\ٞg۳]M2AGFӋU]IAZ"sy2~jGDDDrӦ9S `6SÍfIV%u ̣EkuW]<ٙٷ=b@pp?9Sp: "jeDDDDz;%Vw, ˃2~D`_/ȹ}p:,gɾ-dxJ}yj NxDDDDP7ozo8af$o #m9\F䥱=JqA %iUWG5?v ⼊嵉MEФ3aJ%'BhH41[ٝ D\e35:NM\lO˛S`W0I֘)a_:X|U˽v"%|5;]vu#|w6K#i.?sqYJKK)--p`%::Z $""" 0ܙȖ83MVދ <""""" =w ˓XWaݫ> ȯӝ=Mgm""""jQQQQQQ`Æ j sCO{"""""rG6QQQQQ5ڝ>}'Op88{D~1PyM#ѣGWMwuPz)DGDDDDDbz($cI bQЍeE1o~*>IiM?ZJVu0\a%+`2>.4PU[8`Eou[VX^0!ka@ҙn6z*8)kwr?E>DM_g:J-dSЃmTI\pY<1! :nMCٵ%u"""" =g<;mdSBH Wǘ`*[SruUr,x7CH=Y vamFJ/f*6̎崻N-wM8~dOv?`tB=0);ؽSvoj>p8k~ۭx2Y -)(T 9_#УwlX94 pS%8},GTXW;.|0N?#Z6bg4-xu >uU[frO @INN_Dq ;-R{U*-J=&/?|DDDD47pMO (-OSpk8} }2>N9.(S *tA&SaJO` @A.!=DFrXk9mBɯy+rawLvpt %w u4cWEDDDvp}wc-ϫC:ېCa=x?A|$&?{?Bby Yk6|~Kߞ'/a/@{i9+bk|[אw +\uxIu}_G.1nW׹=XK~V'\Oa4\tF zf.ÝNOd\(X)P@uݫ>*GM;EUZg ogw8 1my:676Ŷ;xN xK&"ד-h3q|+,a {\ IA'[ϝ ̞aFx\k nu7R2o._{QAGk؂}нolӝY$MKq_KUэ?Cg(ߕ.p,< nt*{6=_wȠL<VI8V|8?_aaz񁊉=Ҽgvi=Ƙ/#0FF>#V>= [;5.⯉ڿ7`+,bźLJtcŁm?/}o&w25&;.;㟟cdn3p-7wvѧ3L>?zdX#Gm;b47U޷L_$& N~;}x(i1&%bWs;jwm{Ɖ!~yw3Sďl^hGpCox?Z6#iqHVsCCkdzcX-V 0:#\R>ݏ}b~!li92)Ϸ):wsKK%X݌] ~l;bRGq P]=`Ѿs a/JVZ7v1&GjѬk;,+R)ȫ)eG7?ۊExk wAi1 ۊEO7޼x53$.MY׆q-p/}~ 50r쀍}#xv;X(9a{C)1Cې׆jl* (=x%nL#3,ͺ4ihڽ9Uxŷ~sl+>ۊ?{ϋq=ɺ׆xח@\,y=3 K SQ/m89e%95f5=αҽt/}|cpڰthEz+UɗǃKYnQܜEXoWR,|1䝯@ƼڀG,s Gn6z6$ǵeawO=.v6̎n>Kd=_ŬYeY>bc[ՎSvsXr_zW^yၻH|w38ZokWbY{>ŀ@3:mRsrE]q+ Q #<} x%|aqU-t\3ZHUoxu+44Pk=<9>n|{ Gœ)6w\htG#Zëυz羺yސƍ 4K=xhpzz14I93W='}nn _w>{XԸcdz'[ϥ!Gq41*kF]w+yɫC=ʏu[cl\gO[^MUn{z[nZtS+O<֍Z#ۦx^?Zy#8tvw롸Cy܄7Рꁚn<2- ljn\7zv%l[<# eOӓQ{.v';{>zׄ 7(gt,h.\ e}í̛~Fi%;ڶ^+}g@eN Z߮UF}3+'n#_e~ޙJ- =:x#_qh*֯#e_}3 b ݼf/hB=ڭk=nuz@ּMO܁A=Õ^TbĬ[Mkhnq5h<8mm{P@ᆇ؆^Tτn^\Ggml?v+j/ruJ𐞷qAg~!vG#vrJ.8_k.׺\CzBe-lpURz3_uM8썀RWWM=p穢䜫?DO'ރ?Fš)+9Cq~vۍsCoT(\!sNO2~:M냢ռ͌2]W}{M_6ܿgSMkXٲt]OB{?\G!*֙nl߹P]sϹ;z/Uki[ets @S>3|qs? tvS>p)VcZs VmϥR;[n~s2j䓄 |i:1/OOQTQH/~X׼]ԍ_= ߻ӳ'# j׸/Kmʹ zc_ysc @׋)KO+۸y'<|i뻢a̗{yCݺj;=]|s~kYʫ=Y8=|GKPzcMb*N"fgodovHh5Oz4iW7;nOd 6yN2~9{W1 F;2˔ױ7Š#s9@]N}U$E!T;Bq繦RvJ0{*1g؏ң?;1^;Z n>~ZN;K]O s?l`(b /O3=;Xѣ]xA8/f8x1ȥmB!nW+Oo71 |)nW(^2F^+bnMESJD;M#0Ly~OgVlI{퀻vS^⚞\6-p;^ʺcrEswWm͝3`UיB!>gرR?^Iy>LΉnRGl;FE|翭{Pyסj4p*g] <۹4&_s4{ kޑ#Bm^ wj4o7F߮X'nP =-Qߤ/1K\p9՟w>ixC ;V3Ј|u۹4fsýpwy xy{GY!B;F+ԟ<3Toݗ{t p3;'V9 |Ͼ ?v;tG,Hݣm[w%Bf垾?˕gq**18YFg7z>q*?/ӯ3'ssc99M{g'+s`+~~cX?F2 _sⳃ>A3?'&/nF>g֗~V*ncxW79߽[ xi455|2/_… W ]!>`ۛk…z<<=qͿe'0dѿSw^SSW\i ;ΩSB!^ γ0Fs-a^xB!yٜ'p /2CNB!x6!-&B*E B!#B!zB!BBB!BHB!B =Bx cؔ *8BN9p5j݁DO}^SɔɄ=Z;LH!!-=B<:||t:5Fmy֛:y9'gh V) !nr"a3mWsxײ*c~HzI=TF!xC}#pQk@$s"D|IID=&3%)!mB9I98w3nڼT;9?g"]4,29zm4D9 dl}ӹ9 6GI:]*T~A¼)2,lHa.!B3Gwg:rUſĚST~ᣙ0!*JvOnM2o8bM%ˋ=ڀ|?ǶdfOA ә>}6|VJ0#y= ̈́ do,⍕폿&3TTMݟҔ)uk`=fWPU5k>ѣch;,?(G!#czx0k)pR7zt Ӧ2ɛbY r3ǯMUlk;3h-7oS nWgv#5)3 !y+گ\KT0}N^ Mɔod_M4s|ۯ27Z@9*EܖDk l¼?dk_MM "ĵS59KI-n2g/'qpy wKx1~h^ۙZNs OZr63y9$Rtfb Ϣ, PK)y#ȍrr0D22j]>f*\#k8dmK Mv 9ٕsOB@Zzx$9ej-r!$& < /ZӮK\sgvi?@.Jnp#ܾw(r$ <SHꎷ9\UW^J:&m#o 5*HWG%q6-LHIhxz1|0BHBIpر#gLHM2SQs EtvuqS6Nk'$Y!, Ts"!,'CN+[ L&zꚰ&|m5iv`~4~ΕOtqP-b"W} +B"3zt$CYBHBcU1(80:h(/qʵ5o( zݵRUfOѱkJ% @E r'b( W61s֊:G_zEJ$VߊBQ${ma]$MY0oB!!ͧ П .]*m{%->~Ǣ혝5d*aCզ]tX[+-;b@l|6v ISCΡNΤv>0^sYE 3yWX4XiOx5)*"?7ݞ\!ce_F ws-[^rsGcvl8B᫾{583a}6;M`0'J;GߵYT "s+= ΃ሹ1Wz94>Xjpi/A$oE?'N!@]N6] wB׮ {5;9I$:&'Ln O D}Mj8х0ps${Ѐv78ODT&MpX}% o? ԣ=oI^pTSb%2RsmGp=UTDVؾr!AW5Ptg :9X !~M Cvj舃)taS $gk@zBܷsK )!RʃM* _'!M w4sI.:J)NGl$c1חv3嚖9љiyS r0Jxtk\&Kȯ])'sz1mL;o-nbfݬ80{n6QNӘئ03th^ =-~5Sn.=GCh|ovYķWA: frԒ'sY/bP; !ΡxA!2gBzfLaZ鴚ZjBz|US(]ʮ"3l$$*QnvV~O똁fޘ1]ة9Y $M]f01'5͕c I(11;\(ny9< CBӁx %UV )xT)ӠN#}o)yzk`R63\H[Brǎa_H8t*%]Zg bsy'f=U4e;t`>3//Bm'$RddScr Va.h-B. !#,.F jO zN4ï7MVHo&|ZQʕ L>T XIUڴZgѳ=cQ ,;ZZPF3!U\dnau]): !;Bqt81 {*$Y~Gwo$>8;IGMbgR<~:ÜI&ʕ d~F !M_{00{{k䃜\;㛵 >!!!ۄrKT J|&I܃I$w٠y h7֚ ёH5!zvݕ92)?Z`ANJDPh9-%|1cr-r=O\Ң%!#z%Q fQʢ6W$i۲YJ8Z|3f?"~ҵM!CD&2B!Bn"g!Cܒ74D,N{uڊ&#CSa9TTTгgOJX!BBq tQ-x.\N@-#ۗUs+6]mEW4ؚ׋n-/;D*mWKk4Z4QczttP]Чܖ4J&8_CxgҳgOz1GB!zݥ 0;uyv쁰UnJڎfo 4q ;bMkԟ|\쵷.rqK/Ary#ןB!m[FB09s/"U?5]n^9sL>BոD]ԃ$߷V!Bt81 R"B%%%% 4H Q!6H6!B!!B!#B!zB!BBB!BHB!B =B!weÂQϒv`OU6F;vZR$Ə_!yAP`rZY۝yȨ_]>"'cCfF6{+8@066ih>):6x* )k^12QZFŁ Vo5UH PQ>Pw8VS*YPcOJBc_`OU]{ܳbۚABʪm8PJ|\j.x̽Aej|" Tߤ'$jo'ő?uv_LT=-'yd.(LOdN SM((Zƒ? ̡5&ҷ;Q뉈I 1fh^eO_$ZuYi=լka{Icj'k{sh3$? cX~}l9~'!!_le{(#8 u{2IO_E9+de16q51F ;L$/ѱ fEOT8{m Y c_#>@ jV2-!4($=EmB1Z=z((;*zԶ2glu#0-7jB 2-A]b Rƞj5 5zC2SYhӲy*fa1}y.#,+(N&59ff's=ؔ8j+ ӔB7聾jw$lI) Vc)$ٴTeBvT1j-U]%@bR$ #CK.d;qiTyPQRXA|T  .q*{_3w5CY4e[Kʪ#8e3[*X)4Uh# 1iCuĸ<^̦4.0MK-"Y[m #x.@72m;س7 +1Nnf3ߚf#F YXG}hkK `9:Mx;U٩lƓdp&$kg&i6ZFվf.o vahTvW>q>cc,f<꼧ZL){[Oj=tQp%`pnpmQ$ćBi6mSO@,1a:@( RK`Z(څƦ23  a1$R;&gC];u:Bg(Js6oT ef𝻁`UaA@|t: aL9;3'q|cZe1?3 C Q3nsævw&ꩤ$D1HB|nz^_x0 ģH ~eDcZE獷! >aC]-~h),+vIؕ|ՕcoEi_e[qbLT4Wc;R[ꄫ"HٰҭdW 0-1ctqjPAHew<-ח`* ~E,Ze[1-IE!oM3]oUcWa KEY,гn{RYi!"q%3X0]GRؽZ %x !"›̼bjc 2,J#w/aAPmhC&cuv\Ѷ)j8`SjZR>-;j%,H{(~, jWœb-k640mGM4.$2+cv/ >gxc31iie. ma*e)^|+QZ\.FgaXL:Rֹ((ػ1* em'00u"XuvI\@^6הԕf@TԁA( `DHMo8p.2vB@j]G sI*yfP: )3SY+((;*@dsN uÛZ՝Z#ފ65k]ֻ ΂23}n:a ^IުkN\nXTzVnQ=Ҧ0t93|!j;#u;7Cez+Po*:@ov^EQaks٨[ab6z=jG6zT@ݱ: !}+t FJDl~"% 7bBFnhiJ'ck3ĢcfKŎB j*NQס %&R١%TfknlskBP,#8ŤgVc=[FV. ;c/6c<:]2gBLz_B]wuΪVo@%/- {BَL6ꛔ0/\^Rfzc9qUQi+/H{&M-EchGE^ .`!3uN\:Lw/ W”#,,VVR0P-w'jlYd>.{5al#fj-Q ;1GE!6`7& z+* c`AZ=ZvW\K@Ey7e1 S&#Sfbt^ Dܩ&M3#vr"4N3S.1O @8f:ew:Y#]ǎtg>pUe=L[}XoE1cU1V>. ]~أe\T0zyTMCGD$gqQGi^:3|glYɤ7b= &Es jc0Z^ʬUڝEk]5bTױwkQ5:s׆q&_";0cEK^Y$(cʕ+455n{5ͮI.]ҥ ]v[n-ҥ MMM466p88u= =<ݩ,c"9 zUL/ڀZLk^A…<./]q)+Ld.6`*)+c] a+7g&2եLQX L暾Vql<fv"e`O2ka,-W6 fjknu`# 7yc0+wf(+2\<'JtrڀRݙ\'5Fij2akNi!;]' =٩+=`])`LyaڊՐNֲ; SHO~͔vN"/l^v,0s ̩餾  8V$F!,a%qT>&8"mE [KYBMzF: ٜʤ8m0#b3\YHd2H Ɲ'Y 9|wD"8;bj\FbQu;>nƦ $OYPf'>e!RTu$w:1h!"k) .27p99 k"2~/,!&vc"p{FGn5tdAqZI^'i|ɠ7x}j9(Td?x3Rdr$LZe G'zLv&`DklH(Vs-8|ݛҌJ:g@Hb7K!j7+7)+n\f{LIV|}␋Ueqc &OWAR8y8+S IDAT^;ba ;M8}N Sb'%oe=DA2P֕hBвdq a5IggT3ez+x*fX߃@שHfF(<nM0^<^j$%Td/4E$4CO 1%ٸ$j^8E1rQ+L1 x,pd=ۇjBTPH]f"O %vbip-ٹ'5Ow%67y0Y[+`3}Rwg`!0`$#=@( j=Yd1 qi&~yں=7_a WXZ|!VLTbH S5]Bn3"jbA⊗9ԝzhyY\cCh:ƱNxI1;}-$)׺0lȽvY0cwK~V^$BZde~.]r0ͥ) ߼-:(N+c3fm+ (~>ES)PTX559 =Υ`y=Lo8v0J'5Ol|8Ƙq4# Me;3hw¼9Y_#QpV5M;>GuL%7:̤Ỹ-Ir3Gs@µts3qB5RE $>#5rG0?p@t-EQ>ʍF#;eL4jljK3RL7V}wQqVwäBVM2Xb` B;k4@YW퀹KS荋CC߽AwA- RVۋZ!ĕu۠V`3"wzҵv0ǐ?լz()K+w(a$nf^fg? ,3G StPLJ˓,zԘLXQfnY&uz^&I`V9IHۑ<jӅ C==ɻ1\u]NZ̝MqXfX8>Jzf)v*[c&nbB9iLӄMRW.MYv2`>.*[S 1~3ld%]Ĉ$Aq3[FP+Q\nIop]n2@:+ӄ.i*=Xg# _`.w X.MsroBlt9g磄JGy 4E$m۹Fܢ|;q4,uQtӛdKi6V] 龱=8ϏmSV/+κ(㏟@#F䠋=`cmiܩNPĎ0.wgu X;ۏسB (Hu&[HYV76Xw,f.~n=y\90t066ҝ"UX;nfWY`}v\b.1߫٦c~̝X&5E#wK9'zX #=/O?'?A\ _aK# bb0 Rk/tJ >f(Ȍ_b]SѐHWY}YIQjۃۊ,Y G<<Ѳjh WkV\\b) MIfiyl!]akK'ՈcxWfjAd0q%ȁQ\ /kkdZgSAXo4 RƎ7m.fWѴ2xh0muT}/Ihj "jH. H-6קexGq*xTe}?ksǮa.PI"a3:1b7eEdC.=L)Az1JH50@5f8ZiÑm% F() R*? ȷIW@-I>u2;1OQ=.X׏NI9mD"8ADgiɃiśL+2_8vda"H9Y60(/ }>qq.&DϮ6(X|>;S;zCKXlX,fFo03ƪT]M'R+⹺%c-X}8))ei<>nbE^O8bve bo]aOf5Xp~ Nm8jsy ؝F(~j@2ڀbSV.R `hk"xf?r,uW"J:{|⏪ ί`4, 1δ?#򻏒 }]u> "L$~E]/?'~'o̅ /6o>u""|s/M⧤gW'|W!f~yg^~? ŷ-VMQp[=s&R: P15/R1a>2a0ucftklMҩ<佊TdiaHLO.2yq#[^еsͯ0d>tC'*%T]Biok, .XoY p 5~ DBIՑY='[m"8<=-gbywbJ3}Tvm!3Q@ aHjz4ObTyU&kEim`]ߔ|O1t(u@^dk`tN m>̬_-_"Z#Zc 0(tN2Ke-|97X!Xc140n1mithٍQF>i$ #Fڈ o~iǍct4Ʒ޷+|~WWw^ou>ox@Q3T_qd>οb  >+O_[o2_|߮'_p6+>f-cj&ZQF1#ˑ+6z Q%Y A53|ըrrE Z|=ڜx)P%L?,*p2F( E(Fz2{d2%n0AzpV؞ of;\n"WRk9nlnw!s$>fY9`=CcU=2aaJ'*##620[X0K ɘ H̓BH]}8XJ8|OӃ}‘R\2JhKܖYˤC~'r$(>XIW ~t^ݎ& J:6BBsܹǗ(mNͺ 0۫Ymҕz]Xڎ>5Y"IDfe\Î#\f'PP֞ڈs OW_>1?[?tp F^hhO_7^|gʫjL y*C~U~g˯a~^|xoO?o~7x{;>,όf>v{Q-[A&m@< UtP엙o'21ѹ0WFj(Dt a@24z^w#]% щ+I,yS$IM j,tUzY  lTU"M_nS+(:76dy]gXPmReйͦ?txws(΂gij(Bt&t(قTي5',-.lGF5@zE8:nfiU&Y#88UCqϯ@$"iq/0A0`fy`5;%<3%hڸ~hiǦu>h (#%"#貕~O?hq>U\_>fʄB&F[ ;.39]CtMDs۳޹?d։:z;,aQ?W5@6yEp$- PNǔs.\pz.#]YL1bZ'탳23z &G?^XoMd/$."o`h+wɳOj|)|?яEOX-?7/KGwrS?Go"/o ߽vb'ٸ?Vʍ_;^{????]שj׿^{b7;/Zf2(_JwtAT gGLo<|Ïoyb_OIMG_[{ \ߧd>3Mc}3ɇoRߑÏc>xe^yK*}c>OxZ@ x"f2CZVp'tק %6s@cMo~!>P^ oׅ˅WoJ ^\x7x%Xu.*t"> J |e#!f(>X[ ΀&#bz@ &@ @ !z@ @@ @ G S#wMzMP8ĻyLO*{=~mk~;=+r(/mCϛjwyo.稉PĖW-)mϣ )vzG}x6MA,Hhm.8N}n-C<e3H%.t8պAT;ž5P.Fgpɫ(-xdĀ@-MVDy;J8<*opQ ౪lfjc}O4l]" sJЙM0^<^jtP^hHځoi"<bJ2qIX,6bnRI&˘ Zx8Bgu}z{֎c5!K*x xZm/5vQ5j&p MacɅU3]`섃ӵ&Erh(̄fBFj"SvL=۬(7xf÷ʪQܱX*ɘ5 V N9`-Jf?3z|i0yV0RL'9ȫ Xڟ]98ŎvZze4N P;Bn-p郎n&0Pb'"ށX`q-c'ӝ@M i8f7;l;fhe=X%H-e<±l<8r3ǥIlh0|Yg{'H2_ciWX3)&R #5@ZA%zjbA⊗9ԝfI%cٌ@;(t*$h( 6'me\h("3e߹J,Ub`ٍ-I@1[m4biF֬lISegY캟*{_ 3aZ#W6Xrlɖ5@ x|UI_"(qYJp'UDk;pWŊ`vGU%AoV]#|*{'N}VRƘ,Z+l GF^?XfRӬe3ʝU>}他\Hel޾v @u{K==Lo ~QɄs4@yZ!A(a'!{uO%ˊ͈։ä14%s1̏%?AeޏogIc@%3}6GU!f}Qba+:1jT91Cfa(67ZU" +GuVA);#_W/Rb2p;wO5Mp-E񴺩c3fm+ (~;-2qq *61ĚNMGA1)4OnQk0ucE%ZeqnK[cy ޚ\&Y:Y$EL8[nGMf &N8~1]f`*dlnj4khpL 144)k5f&|.Dcm*tlM144LάRS* t3@W}5Em՟rwѣU3,]:c k:o<. SwZQgu}g IDATr~ݽv: [0{զ@S<ֱ%76X\) )4U66Y ݽr%[̄1q6+gW&8dKr3 nzrF* Kܾw۫^wi*=8JWc vwwz6 Z:-$,ϻl ѩ)y VFeO=)U[08pUvqeg=VXp{/F*V&ag~o Gz( zWN(.7Vaev+ e Sf[PwXu[]gk'n* ]Ygc}1sThMY}vww=kF>'!x)Dzx9tEZ+keTT+.ph5 T >:o\KJd4Z 0t{γbi?YQ|h Ws#VI0C!&*K[^ w$\2D[>Ni;NEgm\Hz7ҕ )@6vȨ;1RZ=HYc *%'LݍKi:jYGvᰴnoᎳ6'1׉NQd)6Q<]mw$in1{ގseYqncZdg-״* ڰغpiF mdu`n F6[76CKjy6>@W&j%@*;pv,]|}J@-gK51Ҏo pNւ<CĔ$j%Y!xi1tTǝT>~dqƎ }phta>S?JQ&۹jw@qv<x}n:-:GM\hڌw~ ^ZxVlSE0ՃE\O^6&™6[7p 3C0~$j{׸oLm:O6b$H/J!'^qiJ͠-e߉u4m.fWѴ2xh0muT}a*IBSk <0mp)B$deaS= 6F(΄z) mzG9z]{j]bݭ擬E#fVubHo eM9\/P2H~$XN;p:tvߘrBrpt>72&Hz]أ NCF`,yMDOD1i*My[l;%5E0bl,,a2=ٻV3l#i%wAQGKOqtCwgUJ`Z\Zǥ߷߲X4om4lo͌zU(:N[Kj}-ϱ3է>`窬:vw}q˱i]ƺ̏$EN/_Η9TX%tZ)b7ه. (n)0u!$[əU avddR fRn?f3:k3eby}z;IAvu}앩u7>(ctIB%bk 4ҹZc.7`EkM̞7P`Hu)j.5 f[ 5_ܼ3ّJP>َ, s5=WZ.M^Vb.G.#W:\9N0\TpB\HA]+oGa63J$>S9ud#{™E~O}De68dK`&fn!=7Ah̘wE@D sf C I|.Ͳs,=W~vMi+XQ%adFv ?Ʈe¥z'HTpKgdd jeH$d2% :$X 8Sˌ&R\.C^A(l}3 e69J \ʹl5R0"SQkJ0Ibҳ$ Dbg\nt0zL1z"CT&vc>`3~!q@T"w$~}s<)qFBJV"cӞ;1r%J{7lǾ$QNSp/!uZpQȒ/Q՚YAtm2۱buCaMybvX9m*khjBdQ^]^<6CKvI0k{J=wPn, 8[J~92l EIJ2 "}p j7 V7J6QT*e،=0-qkl {`d@!O"rA:لYHogԎaWo15#ɑ쑈-\^ aFA{JMHSbG 'V/lg$ʖ2MgD&&8ja&6ߣ%P€bdvi =[FKxCAWX6I2"՘`nNDW[5Wn3!(:7 -1ۤʠsM`Jd4J˳,u< [q"[QV%𱑏vhE6cn?C:|p}BmkY <aUݴEIqz}f"L >ܲ`e[&뒌lr2< 9S0D\Qٌktˍ-GOտJNn3~)ش$%PO0H2Zz]$E2 DQfF2 -j/0A0`faуw0KxfKpljc5Ve‘04@B:Sub58:⠷9 8ttIkgeF \޽"#DhvE*0Б0X `6q+*=~4RBWۻ;YwafY ^eeKEXY\N4?`-v2g&2-8,D"MvX]\qϯ@$"Sq}JZ#uZƯk^{'/K 6hwQz  CLxS_uWMrm(xEpNm@ NB`Ȗz:#H+UUKEٗ [bz@ *L K=]#&%[{-4bz@pm@ bz@ @ G @ @ !z@ @@ ·6=o}'9+{,AOO==3}pֻ]w<̓kӼ5je`o^`nV5+Wػ gزZ Q+mϣ )vzG}xݶĂ)c$݇2CQ6YZB'A4Bdt:OY]ne7޺OwYߧLM?ÜN)F8zwMG+K&{Q]ݛҌ݅NȘ]q;΁\<̭r/ceg5؀颿|sώEo=OS #]˻RcEfN2P֕`.m/HghnpQEul.\I9h_=ވs5e4JNjo=ej(oG 'P 7X$fGVXU6C3L:hXS\l(zPRUBSD|K8SkMe)@>a3-2dӀaYGO謮Ob3cwXMȒJ6!¸>VKͲ]q贚 I$dBSؘ;Kc);,nj j&'x!EO5Pr[b) j\|ҬY+M&ɫt4C鎧gvvHj~"O-`n|O9m 7Ӣ.SAu0cm"7ڋV9qv6JB"fOR=0u{1ɠc"ShI@,i[F_ +LaN, E ;Wsf3N&4kkeVwZJl,Dc:l$Vɂw Zm+ѤdSA4vch]5(UG[y_d sŮ6霠:+oCR>b>'=݁%ZBE7e0^k9{BMJ: 諫axCbe}EUQ10d8~`,+(<&[A<%GvH"/?MdUԻUQ@RPょ%>22 uh+ʼn%81 ؔhX&5߽XG ,K3YȰ}:Ǧ#f}:}$Ҏ\׾OM]dFuBxu4wp*GH#xJO+U5XW\ 'CO]@d()rI1FX4%YKWWnpc1H#LHDQ} ,L Xǯ(jM43 +Vb&5-ӭ=Fgb0; 7әx~4lVBaV76aͲ@ Aί8'լ.[um4+[1f7HU9zjӎz.=ٟL"EL2B ]yu9SZ1`||*U4KM]?jY=_lvQs(G:<Է5mOD{Σ/6%D D xJIC4\l]t׷0a& ݁?LwRH# 2NU Zu+6r )HL||Mln@&k "GGŒǔBZ5&%+ga74QF}DBSdj,3 C4X4ʑJ8+L88stCAK]kPN dfAf -u>1“aP蠦ikY筙<2(. b< T2+•>jNR0V#%dtnP8X,QhE{SLM{͹Jfmȯ$g*jVP]lA_:&DK()DӜ`ty;kTr]w+۱Y~?@pOp޵PRR{D1724>$Cnj.X ]8L=sQƧLM#>%WfJ2c!&AÌAhkfEc"&0SZ uԴ1359DVF9::5lq0ӧy׍3Lg 298@ 'Zi?3=UjS1b MlXiFe;Z=̲t/S3ήذa{G 2Y\A+!O & IZ_VT-ObA7ݢ`&?ƽƌM.~?p``*릢c۰4wmZ{;MMT ִxmp )N0:>{8%;oxjG>12b0ዄijS h il<Π M nxb)շ768F39 o)4fj[lt8rZZ*)*>}aq \ [n%mB 049 fȹNIa}yF~!`ps :iغ' ӝ/nbqrąý?r[q/X:ff0)*QZQdDuR7?u3-\j*yүHk /͝Tg.41gJ8tW,^Y8Ms] `ӡxi6Su!C}[=f/ r-2s6oCdC{9ĩǾsY8ɯopݱ oI/Puq~/R(> xԶɬ&CKSVWquEEdR\Y%̯rOE ^*TRXDZIJa"f<*逢aufSҝZLCCws 4G66dǼz&ݍjb -&VXg8|w ƚMHc|ۚ)j*z9}9̷.mbw6<:{VO%xa>/p2wMs?3oA,g߾}?O~K !B+׹]r~_Av0_/Y 9G]|9es'o%'ykB!sdZz4^ek_n|&Xw$DTs볏ZXNtCO@rR>\YI\~Zcٟ|=t_έ{yuWՅx00m~ 7lWtIo8K>'gilCB!cO:fB~+ʞ][~?_)e^v%-  ݅a7x9 Cܜв\r «oo%dZ^b-8W8p k^+.}~_BW[xAvHCB!seSfoۺc\wy #X|K .fcn^nsz#^[pe6@cI$,y3aRßv>ߙ_I{o[z= !x%/$‚dƦ֨b$/砇ݽ+WK`]v9 ׷;S8AWq&vA&c~0B2fsīGI$HTN$eG!I8,,.A3lsfoKNf]l!;ٻkeq. \(sk$ea+poo}\'l%m."G{8{._A xB6WT@qQU%#aVwcזoFˮ _0 Mq7s(< aW79}חž%~:>l'%{QyBO?)M\<%/ "Sy:hNN;=c'9Tcp YaFlS_=Ǽi<;.z'W^?@Z#o#:~bxa+񷿞7ڇ<Oҳr^O7tAni5Ev n7 YHu r^SKondptƊ _Ftvf%ׁ,MN1Wm6BxKssÙx50ml:}uU+^q5 f+y^"ﮠ T:4Ce#U0T44hkJ~BPc։TլxŒTR찢FmPSau GQ5.Uљ੩)/}Jvi-6 4j`U3&\Ve%3`UC!ˑ& kBIuK%e`y eVF [ñzN=G-dQ:XW/"N|lFB\=[_)Ouc|$m㥽.JrvI)yiKCz-cK >n 3PV[^zGxns'3e%>⣳3SyKUmcQeiԖ PWvbfPlMv[[3c"|{H셝n5v3J[Zʯ"δ%Ć:5FX=OcYb^%SЬ6v}v(C\o5l@QS0fnPfx݌ݞpn6'7lvFO?((2Kkueă8'/Df:l:ԍq0$fR[[LNm_pr G?yh-=-SuLڎPffӱ۪XVrbqCgަFYN_8ROӉ,@dCM91@ b Rj\j\8y =g8|0u339mob Ss`?y,AxMf0:k뻷]#]Tju\Zg譩\8z*BJo ,t3Qb"%E&u"dLNdL8` TEYlPPx -4A}{lIjT RT%K%M_d+":(CNY瑾TҵxX#>Qzʆ U,wKk"=Bg0ifkurjJ<ȉjS3 jEPѰYV4M1 YKWWnpP9&hR~S iX+ !ހkyD3ÀOKk(fR=ctvO~5; 7ә:iج gWxtPm84H}p~E9q~bᣇ66i*!>_ '~|Njz'EVɭ:^AǺ'>ΔΉO/R49(Cn.tu `1x<{!A xکD~f9V`&>&Fc}Aɚ1Ba"Q$R1zMd0YX>=MTgBSdj,3 C4X0sߛ|vn&t̹ ctCAKMY# ,hݖ,3OAq> " hy}a-Wct0t<5Po f9X 7Pckb, 稫8gU ӝ/nbqrąý?EU.ίcfyi_Z~n~ !8g?[ԶU=) r&^Njx&fO3@זI~ٽ-R#N( (>c /w|WB6(ɍr\P }*W֛>/0[Mvְ3=|5u[@ҶsM ;H\a9rso ?}K_w?W[j!?#y WdX*[!B+czwKBߏ"/{?g-so:f t;C|lMt_ 58e>dkl;S:;_sq7H<8$ ܺoak)n;R']=;79{$\ ^&I[%r~N'B!$YSf`?}w!-bSAرWͰ0 WoٙNs] i/`ʨG1N^ٽ7E %Tn,p⎗9$3 8L#7̍:OZ_w<4}KYۿ !B_d1͛IDAT-lۚĭ7HٍD_aߞm}WBs}W6A2ߡJ_joγ'/c^ڱ Dnα?1/Jj& g~.oi~֜OH:7 ۤ̋E t|BW[x㽃쐘V!Baٳ3-V&۲>'eW\cl{}/&V-pm%&i Ŝ % D-wwr&7dXuqh-l "E*og~a'%$gB!7нm_ɥK6+~$(䕭{79Nُob!r؞;4B Εr{V_X/sǿk63a|(~_xywƹ5WG-R9)#}S{)Q>0cS=rӹo;?;I_{2i]aOh "xy++?3_}M۲),Iz=۾O⳷Y]V`+=\n2Il{iw𣔓:q<( B@,\|\K?ɝ;w矹}6o0 b= B!x M@!B!AB!BJ6 H6!Bg!B!$B!B zB!B!B!G!B!$B!B zB!H!B!$B!B zB!B!B!G!B!6KȈB! z/9!&s?3oA,ᅲB!xJ{B!B!B!G!B!AlIENDB`././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/doc/source/user/figures/browse_zip_file.png0000664000175000017500000010740600000000000023511 0ustar00zuulzuul00000000000000PNG  IHDR׸TbKGD pHYs  tIME :Q IDATxuXYMaaQ[QZsW۵{MlQQDP5\w]gge3̹wΜa1 C?M      &I̕1%?$dKX_E!NJ3$fj4b##S -58 =UtbS.2B3D, a1 #%GF$ާ5c[[ ͯXzu# C~O\} Sͯ_MQ&*UR,#:XS&".GND|}kCU"R%DDT>`8*|%6!Դ6y.pVi^XEO_9InXE_G~j  _sS.yqdyjc Pxa7tjkcP;[wst4w^n"V |4+.<,Q!DJSd. ׆}=w~޽kx/ Re!D*|6GD)N,@ ,F͈('<80osKPhs˼W}܅B[;fR(9[viVC]Aq} BcSeݩBp,iz=abnu^FDG !5jhdKKg>;n; { Gs2ϥ~F%fx54M- 2^:2}zs/N|EoMYA0 Enӫzx⮦rs0uѥACEr'%"bkܼk[ ɸI:f4>nɲr*l""yyIDR`e%E^xc Z閔vㅌƑd=yW+)cTLݱeG*Y";tDŽqDDqW#}k<'-nLUvAƛe&Z^}[9xśg$<ڵ*q7\qjnK2 ߶󁤺C:^()%΂ݳ6>*"^}\?Ӓ(q"v/8n=3xՂՉigt6ڸ>>z6mr l+DUW޾w:e^}3MQ@oㅌ :9x5$` Ckq)/pۙb,Kh1mP+r)9GdzHpG:^S]>Y]YX47^pPh]ӜIw{ BZ]z&`ߢ>&DD=YUERvÐffUy,݈u{&I=VBDLvDz2:X<^;;"WS)JUwґ]7eGk(̌}zsYDdc@}Ҟ<$vAU$["J-΢HT*m.uy,bM/T0]wVIVn@R)Wv\!v;X\oUW+9=PLVR&b \ibM,-1~op!FЬ{#Z7sa"OF()3=NJ#vNy7ճEQԵGD6,l0QNZiIzU?\֨6h߿snTs~`qԈnTpԵ>MɈQr}TNƃ\rve"U|~''v*sj_.$; TkqpwI%ٸ}F*=5:QtpL (ꨕ=8DD]Aَ6FjYҪhC% 2͎'RUmnCϒ'JCѬYǀ [g]IQӆ;VQ-&J{&壐[/ƔM,VNdĶ|;MT)W@Ϟ2QV'DdeW]\ 475{q??J [,ߜ7 "IVf40"4r GXM$ʗ\)&"M#͏RS!*)*sTQPힻoIRݺljNL."" cJ4mA.cmڼBG%|8e4ٹr" WZB e@*Re%"Dɧd9q29囒^Y0`"A[ ֱ^ö d°౉H!Wb|Vzf\{E-n\q>d9QK |eqx\"Rure=^JvɌJ)뺐 O!"sqtY63:IS){teF'ˈ4LX_ɛ[}[V?#|GKɬl/$;u.v%89Ȱ>)Vc*WGL(5-2UFe{Pie"H A$/W$gjjq?̿QբKW-UikgI(ܧdIV-LxLj8@@D24Irm7:x;gU+xV}g2#ʽj baWK]`@\'Ibo)VT8` H)7&ɾG;_"Dv̻[L&85a:ATtgՆkI72∣˶G[9W yGE#bң>qU{SZNND>+N~Eq ݜ,y)fJ?ȊI= DDln-D/7/?tfEνkDu{9bHP-o7'zr"~SYDD\˾ 8?=Z6 eo\9 nI3}yDtT*UGOUl7/ d^?tHhK$K >s񞊓κbLjOzvq73'zn'N}/bǚ+]6v3lq 9ZtAzuZ^ JS_b뵛1'!]ono~pܓl"3* *ux뒳O.k?~~wLڱqXčɆWz)M(A4j;g?~t~&Cec{ڜ/ol_PR[-e+Ɲ^2)&G ~3^ =WhfN-3hw1uk[C[-hQ8&ݗ̛7}]`|`""n!ZW.y7T2kzb12ӞOtГ| v<UP ޺P(lչ ?J)ҴGǗOéPhu̒ӤHѺP(t:qǙ;5X(%M6P8\dq dD$Oq so=KKPh2nٱYE-0R m)v3BR~9%$Bz\( ;7,[^v  #3d9O.N(w9lIf" m="@e4jAD5C}y᫲ 2]?mr1Thdr.[]#Jyxf͘~}83ݞ.S^|,fxjIVS=:P~ ,dӘ{$D)z{lی]_"7#ݼ.&ˈͻuU%"2nҺ]Ƕ4+Yqā.Tw[yzЀ['ke^]?ENVc嫗lVa]AYCBiIucɲ]%Db3K}~4*QRםma#;5"W{tԈ [(8=PLVR&b \ibM,-D$IZBd=djz:"bM='7&+()7J%ۥ.E,j=&*ۿ#4oXBju^}z<Gnĺ=l$y[;=zeR+Ku6x՝v,vP%E؁c } tTa[V,(Mnq&Md _Rk D6nC_&l"bqUD|ARh+gWf؍;[?,ksDnHvTQJ\̧T'Jy7U^u-ȲƚenBfET.ٲLYBRr.X̎AJ=֬|)zm vLÓg=;G1i"_ĺ~{A qSQs H#`?J [,*x de(HCj6GoҶ]=~4YȢmh5v0Zh֬c彠Zm;K~Uц_'?K*d!^);:&HvcDjUې*6"6#H z- cxsJ٦M5ieKŒMMU2 :$<}zj#i0ℨL"2n'"EavA7ÕP[ bi=-Y74#&g]qѥ:dtikVbZ(Mp4uD|Y%Nęb"4P% QQ^N4JUbw 3zB}5"YKz8ETCk߃)[ݴaac]ry;?:HݴAA>~_쟻?ŲBow˄ ggDoqAY,v߱w{;js=,1kXsY &?A,aOw6-?6%*CVۂ;}UJ>WϦa{56o6Y}BmyٙS'Ԝ./d1ιƳщx IDpqHi˕UTA7nr/:37n<0&汉H!e ]WY²RͭLX(|$(YL"'"BrXEaFL,"RpaV Woԥw7{Ǿ~+hҭGkܧg׬{$mgxf+]}w⃡,[~ϱ9ߜ\SYރ y7-\T揚nUˀ(u{|o>xhtzpHe}cQaRb7*$yYٹ\f ; mcWN\(躋r"զ$'K}mu3DUK.w\vD"oV5J;@+VJ,zjQX|-S[{'G[m6ɵTRCٳ]oU|k\ae Ө '_eUS8f5yDXĴ, 7d׈ɹ=KqB*JƉO>"[? ~y5.6*,""EνkDu{9oϜy>`Fұw#"*z\"IdmYx,vL"_]s5+[ֳxVEݭ & ݚ==+<νkygՆku*G]=A+{YЬohܗ Fy? gD 9ồӧH&hhgOjɭ6Y)bし>Bg 3ήN)kl-LzZ/tUTLՌIp$?5oے= u #:3)he&GJqt"5}>fIE "始> ŗJ֨U#3_u]}L![݌I> Y~_w{~SdY͔CDl={7G'SqyݭC-Uq“oDIIzh;κbLjOzvq73Cަu/rM&Nq.^#VԾG{Z˾ 8?=Z6 eo\9 nI3}5qyőF} IDATAWQrb#*$1~'D.%1εzyhņ j䵀ȢרY7lԱy˿yױ)ݡSE^D=6nNqYjݜC/n`w=R`\Nb<;{>Θ\ZbLZ4W3sivt3K緵)xraeͶѧmq]5IgT4jm*;fo :-d׼Β׍^`gp \(JSXznn/BZz~[2険o]r%tǯY;X?O:ny!OOPhLڱqXčɆWz~a*7z -|/3B}%2OʮC3~13ڄ ?w÷YRu&CG4D.H WLz/-go,}tl_R-$9y$Ϯ^k9ڡJmy׎+!quan 8Dx'Z78hDF&WoPIuT/L!{VX:SžS-;S?}V2)l٬z0g eEmkϚt =k+lEW7T޺b~;&'ms/DI@Ix_o+Ttp< y |_,V#o?' ml4 "" }_qSnQQQ||H$dh8ށ277`P*{SnaaaDD@ p0 a:up\4|J~.>>H[[Xx/pbedd$&&Z[[A߉ yyy|> 7(r"̥cǎ룭pTa?~ARZ!"?: "@Y$ήYC{95 \<n\ߟZN˟4{5gK{{Ym*?4%X 3YV̇6^' \joHٴvL֑-.?YZc/3 犷희*g7c1l W:k ^= 9!Un [N>$bTFNbȖ-6UwA߳D{dG-N89cqNS5ҬwfO_m6n֭ͤbɻGѿ^&7J=dbcKsRRrz7;1*)4{TKǦݸYdQe/ԳOK-# UUK%+wA^ln[a8N߾ԃ}0_.B2s׵5'Lbg8{:(kc}Z)kw2|ye˞+kIe?ʗ)U׾Mu2i2p ϽكXxvW4]G+qJnX#/n9j8w"i5>/“=[e9-K'bd܁#kYOΖ}3Wd?Rs]bt6)y|S]1K`f!J̢sUl:vДnE =+')0/GcHu/{`eW7̿ұe=H~smWUwV5Q-oRː Nɓ. EyDy+sAq[(>,S6o~fZqK򟮈mK&S [g7-H-MFF4e)=!~6͝}C$46G!b)jJ&j&-pBqsfO+al#=~c6)DQ)*ւWdޘ7Z 3zQ23[*eE2|46TC GX ~!5QQ̙AnT5f]>uWzx.4_lXMnQ<qYOlڧd99Q HҢ>b!*"6vS [7- R ,"I_?pT&ɮRg|U >lǎ$[Ք1-=@@Xlr«K&xUã7r۹OؽTXZ~VԥMikdbɃ'S(e[@+>JzoIcee@P$ )ͨȸ|Vsu7,-_XsA!\jtffܝˏ*_͗cώXw-O_N3z|Ȱsm"G"' (jӫwDEoS:+tUyJV_F֮?슔R)>]i-Wcr|-[^K<"7%V:dJ.Z2Ot٣Gs8  F x^e;IWףpCMfX?sIkĔnm;ܞ~a&w?[wp_xz1T4puѸ 3?mg8c>wɇQh|dR};8׮eؽSy =zcEijq_CZwr^sFJu.I6h%5U0b [Ln4ץ]Q{encʖt_Zxգk+_Ma+V?AnʾO|e~{WZm~!k8Ƭܾ~6j.%I8r_lbJu7<|ALSS3''3o;!@5PEssHBS"_! jժoffoOG%|\p8|>fVpG ""#,Mϱk/KjG;lk#O:2svYAw)&Ln~8A +Zlsu/>{i̮N3ς&g;,F.N ;j kj;=6g7ۛু^X\ ]]]=Zmy6G)$b.ݳM3Юmi{*Hsj?';CAs}3#~өו٧f9gf; >~dLQWR^(/!nЫP(tpVj |LQΎfKHqo4ͅvF!IV^$1;vsLSsK]{Y_W|޴vPR<ȐnsVFج݀űOMM҆gٶXD"*x@ [/ BsBF|Rd\_n\9͞WR3g~ϼ[Rzf ^LpLLg1._7¦9 |'n\;2|" Y9/l:xӓ.J}gMO޸o͍{ .̝ø;wNOk^EWzYMʵÿyt'"(mO$O=Μ#Wм~K[712B@iC+ٟVh(߃$ܻwOvzT!*eM58K\{Vug:S}|379Xg/x"鱩)O#EV[ ڍ<(<4=%%GQs]ՏɸJ>бm\ٺٮPͣGCF]'M>kK }gϞl"3tA wWb8V.]sN{']r CYCɉSGF"[Mܹ>Xl&RlY /gt,5r,;>O`e.(>[nADb"ij@H=7Ι][rXJgS2-ݠ>/9Iqyt>Rzxr##?OJKDbYD$MY3 QRn+L -vG yxz;pVn e Ugg@_l=Sss7Eyӏ/y-;"@]Cj,3{L'♸nN9&*H6SJ4|l1K&!½ w̙=wV]%ZD99Z: ir)}4#.l&hkD%[>HхvpTAbJ۹BJ+G_M ar%IwxOHNXµqv~ylqƳc+֝ySef1uJgW-Wcr|-[No9.yʣr>tXi'S(h '^s*,-?+Ҧ Mz&Wyzn(w Uѧ.De'="'g=#V`;DpW#pjg}(;:} c5:lѡu)u=WOm$(3׼F7Wl{$U6{5 cnsos?5sn:ut_xh܆B-ٽ^=} o[OX9V9K(AL/]N'tkiKt>*hK.4(Rc|DŽ]; ^nrP(i)ĮQ/TO >Hά9(~I\2oF"*|ehY&jh|O{{|Pc\ؠ}5U  >boٚ-}p]] (r{dWO(:!Pz.u/~oX;uɨ /yVl4'$ AS䆮4Tr| Iz`zh.OkGҸc\]]]7˒|suuǔW(|qǖEw ]Zѳ{U+YjHWa(_ZQя*Kl0뷯]tzqgI'nc"yqA^š}͌ym k٦M+7w8%R s&92?oߩSB ?p.jfKժ%S{n/ |諷3HwS;0Paq o+n%}UFU~c=:X}bz#MZMܻWUe)".w~tFiFr#v{#e64lYD`͛gVf LAlϾw^slv0Wc$f/d'֬iԴkZ Ոdfv,IS upmQR[ S,{-vLPqvFg$ub9y,>u=9?+vV]_ҍlS0(6ER ;d0s<CQplQxh?0+wsI^5YwNQ^׽>+:`עDW$[An=Z[>=f_BNؾBu^7/:MPHyyT";t낭_D#E>X?դA}3ʉg.( 3b^g#X0ږIHnX៬v{mVw7/>ϲuG)%<=~qӂ=7^>FI垣!g޳W+\~t ?@eW+c.yž_j-v\k.aJcvuqWLȽadWn|E_᳥=׽,ai"dݝK L~b2h߾n纜 ڔjЬݍ9u 'ϻ|ʧױV@RKrIݍ";R)O)8n&BDY Zƚ"JƇ% asV1=jI&}xR5Tlq']ٓ')VH`!I޵)d)zWE Q+ewb,,laZg%>,"cy^\Q-Qadh3v[]5i}IlsakvTbwb>[]_ԨuksqLXK?L0PpTSvFe&E9aZ.||:*% P1Wvq LݨHpz\59h}ejar\|RD$x{nVX!J wZiqHŰ>QvVjBӕGY~J.BCD5{IDǩ.W7)C]S\9=2[Q(.,R 6{Xڎx'N(KǢbɞy6b\jDȺs{{l=8IҰvpDc#=:I2Ft7ɼmՋi-,fO#Vd/kgf.Ϭi,1׎̪d?sCV?:l ,D̳uc#O|D)+ eo "O{9!k{wuO^߁Ox/Ij'vhb?1=Gy0n\VlF^ ,1O =LD09Dy_.͎f6O.?,">l(4#VHImԬ6ؼ<؋Щѿ"y/OitMl9^ > 3z* 4]'O:Y&. Ǖ{8w1j=•P{U箚S|DM2OG7G/hGi ;Uj,qXNp\U}.wh 3[t{\yZЙ.vXlY 4qLk]֧byNRFa_O#W>Дnm=x7ow(]_(< ^l޿ᅕ H2#=~2I`F7Cr}Eqqt3rA@) v"]DbC{TĖhT,رDE@z?~ ebyOr;xΧ)(%>}zϊe J.Nٟ.S>ګϿ'W_$y3)Iޛ;U98FI$=>#\e`5IbċNr{><.|R H;$.̺kG IV{ TCid!䙇| (˗p^(rٗA޹"q47Üc/Os٪΄V-^Ia7%~\ևQR.Z:,LV0f=lehGn~j/;nYfnrn)w7CÀguSn52~nwp6c7<7=Gp >ǥ_uQ\mc 䍣\Bo[ I ! ;ne5c5dq`:|o"@ HA>^3 "=){mLM[-(!J W)/"Zq7lMfӴ s- 岴g'7I\ձeIw$xyѺ:@=M x- 5hߍ \AկT]n -%Ko|<ӵK.yƽ; K. z#k\glDèN=>. x]ɳ5^^85NGEa?HABDI_:#0 $I ÔWҞL2#zWꚜsqw&N1q Ͼ}-{.: >Z7SKopf^'LA.v+S@?)(?:I*|ÒG9xUOzLJ/͵u0楀P~-&6Ӂ˵_{.M;8;6ʂ7%wwm8*Cxn츁. ՇyT,• 3 8]{tʈG"jp}A#zqmܒ##˩ Y$U ͮYG{^ѧVރ]~yil|~kw?´s0#{Or?,T-[p<{&Wy/ktX*Jh^H8 GY1gn R}e&I`2!DA:d>iF]&9%ɧϗQ_ۤc,eohY4kkãu+QRyz0/cG S3}ˈS/VJRU ?P צr΋x;xމ,9s)t&ssTu-8,F,p~.Oyˠ2X> 2y8fVrE3Q]Bk `|h2啹jD% ACD+EGf]!}[3XI*;$I0L9%D(*OޭUT5Om6Yg? [K YH^]NsXL!:lّ!$+J)!D2|?o4!JWTUtiAkkdq{ :B(?Cb4Jhf`(HY9TPg\)Hkl?m dH@?oooKIyB5cաr֎/I"RoHYF ߅6m` &B4}ȉ}\^7ֆ7e貿D5ZN}rt7oܲiLN_B&dRT"J$q?Ⲥmc <{A x8X$[5oX$ʞm;$L@,ł?/=tw墼K &L2Q#&zYVh״ <1C" YCNWOuɤ2 9ޏSzO) .:;ϻX+r=ϼQv~1 nJg8ZZs_ˈk$IpOH3: Sz۝*Sn*kE5[8yǵRޫ20%WѬR)NZ1x+ I]kܝ*d-}k#YrGLW7?`%k8A"z;zj2A[Gw cnXݏD$BDTVEiidA05f 2?;{IEn q'CIaw1e&.?z{ۻ<ْ,/Sw췻*> 7i,Mb CIbFT>WObF,aWzִMb_qa_kb]ˏ;pqihc EY&'1(_bY8 @!IÑˤb]?֌~Q|H@*_ H18:Z32ō3D !LDCW!DK5;hfeEсda85 Hv[Wg{Q*G+tkP0 C$Aʹ4D".ܽ2r_ﺵqԳƇlULP%av7tPQCK-O/0V5yMe)(@L"u !iڙwL}i8cсSyQITc BX~ (1] @qW(pE߾=.~va }iBsҎ\}:qƩ5CK,oCٚU7cHu)UU-`U rCՃ"$BVcB I 5)8Y' Fb$"FS^1 !4x |)>GÔ*FQ:/4q)*$ɻGZ`M1HF-"~ A7ǾKC_:A7x9x lܤE/Wy!ue_hdymrBӬعûjYNxpn.nm?>2Ƃb(uڭ>Ĩ)oGP"O|lrM'W"Bq.:`B&VwdnTM6Ċf&;ݝRSvwTsi$y֩7:ΙoP;ur˚I/Fm?ݮ[P8;6-)_wb]^9/8Ũsi2)kˠ^F.)#ʷ[fX!=<&"61\5we3y崮W*ݮ<>%J5F1:nێw>4KEuُw5pyo#ze+wm/L5.!XX6E7}r-kPMQs*ƋF?46>~ֵͻWYk=۫Xa[e9Dkt7FOE$BDA|Tބ ᣬ`9M٨$GZc @ild%%:i-uLT*i &M]Ξ FE, 1qMug,tSܼ"ڽf||OAMz^0ƂT%U6Io=գ~gWbaë׏=3`D7#-s>&mF9%>Ff|GV,>l0KW&4Q•;UmDSd] ZXj^/ET׼W_Z*ֺԕcY3*kЄ"U+l7%4FcZbwpNs |ջ,2#)IX1q#}[sh G DRmUE[m6:WVu֌Vnb9 ݜISFF 3~9ΝٚiK|&Y|߭z)L-M&+|_"TE7}ijZŊЍb?$+l5 5FA_VۏۼgmE"y_geYY~ז?4{Ըor:\hj"ڜ.[֤ցbEhUeQQUe+l6',!5ʆn g HA) R@ ) '\Ruٻ^ܺQH;aH|-E,/+M~ҘQ=f%fTx7/&[}|=Oe'ګ0;-OE5ŷ<k- O qX6P(4 ucj}Ţ!Rgr!i.c3!BGDO.41R?8Ŗk̲B?Vrv/M|}dfܣSF>%wwm8*CἇF:%!GF"<7ve΅O<*|V`M8gt/Gn/7߅5GnHi楍ӼrmPQ&^xk!N\.sZBaӁcJ,| A%{ں\R YG{^&ENCMKky!`k[= ɋk$}X=Yq;ydɀNFl6GKpDU&|B_ٓ] *$>_SPgG}mh)Bf|GV,Z ;mp֭b Vʑܢ0!p~.Oy.2ҷ5&`pDNu$bkSCE-]$<憊kSEʈdE)E2([̒#ME}@>/W?q,-vArƈήBD"~ѶyǞ䖗'?~ Z[Oo˗۷_N+_y&ɦ9l~ҕB)ILqXZsB ٷkވ_ JS.mdZ&D"_I-)ɼw (R.Z:,LV0f=D$7ThYW0 ՟ԡV; dpSdۢ11Y8Fb =BHj˺/0{oʃ}#8,Ϗ.!/,!w`b~k"'-is1;Obvk?OBebk^9ehיP ɠ5Ek#Pnz,xjfCfZcsiy_}ZNw93- iءq3"պ>c+6r]3; IuCjVU<7WmSkw\3ѨZ eC-F$4L8oJTF ܈ @ HA  RHA) @ HA!/6 rS!\׎d]drmg^M> Yrn`B(5iVB$xzE"}g|֦= Mx7/&[|:8e|Գ]ėHI#Ƽ(IuEsc \v\>ܞΣBgT/u'.k9-Fas#Kcv/K"p_H^\[a3$TERK']uxWӫ܆o1Z;@cNY~3N+BH[[yQVONlQ66<S{xx{mcc{ v(ZQ$icc8nMG?%'yLFZ} waR/ͯ$IJ^F \$Lj^IHT{őn+;\~vVRuwDŽ7JEqSĽ.Ronj7d9v$~ a;ޖ=Es$I8czZu1ϹcF:䧁+Ͽ`c3Hj ʊPm)9:egS޳CS\?RvP_'vK澬Ui<R6,.IUFK*"Yj;Cv\xR$%Zr78uxM=r%E5Y*ڪY`܆P>޾QؿSMrjML8Ёuk/AC ,! 'jLEFwKPPhV|}+Mh"_8wz8,<, 1qMug,tSܼR70ӵE5>k;}@ώ&=F/cAGPUzy~zK{G; 5ή.BՁ|B_ٓ]Mɽc'/HKǜyϦQERsd/NcO:.ú k@XS&VDo.eՒUU1oe dм_Ȑ#ԵØ DՖ"b ~}4a*80<^@DǣU%ji?QpӠ!0 C\$Xw2\*d k3ڙ,NdbT\ ZXjB Ns !y{]|gek@)YƴjVm Wu[TNau4b]WWd(gF`4BKw-?sdEB M@H*4''ej<JS[V[ON:26`R\,[X`7T#ӊDo.gwنLE l+HA_4~;ZhG9uc m<!IG7yP{cT|Y B]2 NٺF*Ӷjekr{7ocBfYrj(M͗YB"Jvscơm͡/C˘#J*HCHvfS߱]b7;b.QPIBZ&AJț]\-RF'+J)+dRm@j)to. [^Zģ22K*LS8k"EVek6]XS^9ԪZlvӊD.՚@ #Umtp#) Զ5!L禃BHe!9+6mX4y>̿2dku旅Ӈrfza5?R\Ǡ~ѶyǞ䖗'?~ EVBt+rԒ̻{םڏIG!Z:,LV0f=D$cXyz[ܾrZ3L6 !4h4C{u@,!C͒Ư{xx2!A#; dpSdۢ11Y8R*ڡ>xi+i| GBtF c'])D߭Y/N.]FHPri[&C9ؙb/֧8-5;U?v<}-?)5Mg=BqhB4ͼֆ5_^:~-v!iO)LzsyUOM; m+3U-IF$IOLh0q]b޽bъ!$y=_ϰ_]EHSQh9׺}!a^ BveI7Wk6 toLG~4-KA]p#_܈ R@ ) OCe}Q(5iV_bVds(OR;ɵQy.2_A-ݰE?͊Xjx2G_kMV蘿_ OLuxWӫ܆oKy=&&B܄^N=m]F:|ȸlQN+&v/Kݵx eⅷr=E(D*y&3r#CI|!DPŬ(~;p-׾e/ *Wze努)w8֖zrv"v$6Nӓ˵rṱ.p.|WnOQT5ڣ*N<7vg Gu 9$aN\ӈ rS=2 8]{tʈGF \ ! ֋kO*noQ.2hĺ^H4LvcHh8|ⴉkxPC 2NdbNNU̲ Ns% QeY Bi03k֗Ⱥrӵ,'+cY3*7Qy {VRy FCa chpTFBnAGg8֚P %P7UZ:]l'WM(yRҚ!$+x[X~) {m^t'珟s8:7Eƽ-*}|tLB1`JyOb֟PrM3gE^IKd83|U؀݊~%$u'zY;/DgGIgdY5fIv\IW=!ygBZII=kOJj %q >ǥ_uQ\ .?G.}m܉ ?͝eWNiN!AG:޶>S4U H=}S7-Oi-?zӀn WFwa!mXS=[_i}gtus#f:(TQGCH^RߌS#>_webkÎ2t_Р ZN}rtګShmC3ȠO BoB|ó\h1;*|Q;sɒc t\s'ʟ%k9m#$Om?ۻ6O!DJ3{:p{:<%l*;8;6ʂ7YG{^kmU5IS#Lo¡?#{9r{.<'T[lgkD9it_ O?iBB o#EWt^_ECdOp-.z vmOEL˱;32d(CϿ% ~ߦxn Wct-OE7{u jO?ֺP2L 3o.'h(I>S`5!P`BolN%\98E}/(WW՗waQG{G裍!DGM>}eقwvO5<{B/qF0|K&_^~C/]>&׼CUu5 2Zbu?5$B—;Fl9{TX.SKCT"ٶj#zx!$Ͼz{VpՃ@xOhեXHL'E=ܵbCط颋cIDAT#Kt2d9Z꘨TTr, 1qMug,tSܼ?zv61z zXm~xw#E,|w;X/Rł77LTݗfY'I='/HKǜyϦI"?5`vmgα뮧nХm+YI#7kz1X:R6u&/~~2b2VԤHqfӱhF{ ! +{*/Jr36&1ng W-jrS#b3aWNdbNNEzHuPUuFA\ٺ[J7d ;\ av7tPQCK-O/% +3}okMtbZFۘ"Y)jƢlّ!$+J)w7]Nї҉KkӮlo|챤}a!Aנ#D3kOLYCM]5-ڪQKNIEijDynEHLSNґCe nawmlt`Bp!೪xi+i| GBtFǐB,B\3t4Ya̾{|HV׬4ޣmÏ=-/MO<8<,qLDL^iwuouDnޖZ1M;zsu ٷkވ_ JS.md'mj:ǀOr?S(=[oZoӚZe*~DyjBV+%%w;!eɥ~me*{HvnJTf+;ֻ{O n&WTHAc 䍣\Bo[ I ! ;neIp+GtP᩸%$r"b~~׍fDs5:oq>6mWh=\jQ #z?hLU>Ӝg4s/ɠ5~܁}'G繮<'3dm,v0ewz7NݦhZS77ZSR32<6{7 nZWIQ:ujDiϫ]{4+_eٜP?0w  uQ+g>ʡ+`FAπ _s=y^ '}φhF  R@ ) @ HA) R@ ) RHA) I 0$@ HA  RHA) @ HA4oT* kB3,ޗ5O !%DKa>j.I;|$۷oMMMuuu)-9sxŝ;wf0j K-w)b04ZA\.YV8{^Vsrr elL)li=1-z700u^,HꌋL&3Xč!-#INl\^u Z`4 HArA HA) R|]`!)罼|ܽYBRݠS;:^f[usfo2\iY4֐]lsyS o'>bWiQ!(}Վ{4Wetߣo_{4;Zl{)|Ǚ&B!YQR>ߏy]wu7ѣ:^O)Y9}翾xX|fҖ!RuxLܣn;!Gؙ~\z);pV?3gS] t'} Q9fm&٧v#:ԢQ]x)B`@ޢ{}.Z9>D7%?ģR!=ؐONOw~bx8NH˘-oRM+ڦl=EW% _ogP4uMSOk.:Ndl/ roeGrɓqS8@Rz+-d4.S]60i@!ӠLL-= qXVZv>?Yhkt?ŧ&IT6wMlIr䟳r,!>㥅e`'VUӵ2uoyT^ QdYEʮ_|:jsen7b8BnAG%{.||jrLИ-.ӵ+s!pAHPM94fg"W.+|M2!лH[=\P^WKLeZ:L5=m3t{:!4mm?}#UQA  5t_9T@>9uOÀS&_&ykhV볥)%2Z CHw3g73] 5Kh!BRS2Q|oҷ|%< !D  qA#EE$B+$bF3SF2}]&ij_0'S7ɞ]]݇v=qz籮fT9ˌ)C'rLl?h外eO~߱Q!!,zz!BT437g& y-lSQXܧB~f7gUȞdFuS?|M2F$;ߪǏ|cU]2xʥ)[|R@ ) -aA|K$&YX/ 账iii|xz_r0LڧM}r< F ۭASR (  ( (bjm"pxIENDB`././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/doc/source/user/figures/bundle_name.png0000664000175000017500000011077300000000000022601 0ustar00zuulzuul00000000000000PNG  IHDR!bKGD pHYs  tIME %v IDATxu@6, &+"66*6`a7ƵWZ^[T DinX~ |>t3gZ 9LR%Tt,""ɻmN`ؙbꑄp ś%MqNlN#:()~ZY\3\s(zk!v4iѠ|T@wz>[~z\jܳ+2nc48ŻȎytvݣ[!.afJQ<-eSAڻW޽yt!['.Ll#T>zW@F3QХ~q屈H7\ ^;|Y! -YqWWM}&q|faM$1>F)ͤ> CU7dta0"l|t&6D}!o훾.<-ו]{rZ$NN.r]%\֜~ )-Չү/_#[SmOà`˜,2V,,9f4lbX.Vl{dUG]<:Xnn ]J(Cڎ.j+qMԾY^Xz^EGf Tn 1'Cn.8j` 9_퀺bh7à.`Dm|RiX_M>]3&"O "q|o"ʈ(6BzYjhq,53Q{XI;%!"e惽'tn>Z w.W!EooۿrLwu5XU[-vOZص%ȉe3o+~'NF=(oxB|8' dR*er`qDږ||د=dNWb,IzHDfbk0?E%b,CHX3Մ#JȮr<}3M"Y۔7DSF[CEψOT )^̌| NJx T"vV0|r'Xl)]4oػ']Cq ig[ J寸<{h&ng ,_Y y)LM:-jVn QBS[vw=njȏuToZuVs(ɯy*XhoSl(MtqXB[߲Mu H Rbj a # Rƅo}!H!H!H!H!H!H!H!H!H!H!H!H! hJ?R RR RR R RR RR R RR RR M?.ĥ}5u\/䃗˞Sa/~*(}-u=~kC93ӆ1HrR"Cn޽vg7ԬQ"nKB U LQlpe,?z*:tSz&kk\"#!J4RږOtm~?(DQ^ FjB 6hʣA%4Svl;:OXqI,}FnB wqv_}Yˌ ceHSd/ YA`]8,quo-lOZuycQ97 l- %cn^$HIM!3fʋ.~7@0/MqNlN#:() ='"|) u&ܷ/Ez2Q0"l6p?OG'̃l+/9ɹK?WƎ/DJ*Kq̺q枏+>$Vu´AB Q2]f*&"EṿƸ, #"b`oBDdԬ].j޸*EaO|Qtl*H{wʑW2A)2Lu3c EmSGB=kQT~k+柌&~ӑ|n? F$}oƇ 4>pAP``P%Q%2ȳsw~ݹɭ.rnp'Z w.W!Eo }?YDT;pPs"JLki2i)#LB)!߾GTexb׶r;h$OD 6MD  ;d5];/ˉtKAlF9ِ'-RshɤvOjc%y?Y^ei!M5\4lI$,flv(.8VDc$/TJG{)mЮޢ=m4*eoqSzy/7NX市jX!mLbi7LQ}sfޥ4*Ձ<#'=HɓqvrH/S򉈯g>#[)됧E*$ R٫ڱ}Qd~}whe۠6/Q"ZNd̺x`9Lp(Q :+Z߀04mTw /"rlְF~//%T,4W+B>iк 5{(Lh/jaUӣC_۲c9/KcϭZq"45iݜśGWN]q*+Sާ~-.) 2D$z}2'(.P1UݺV -⶷mo"ҭu՘D$K#"u#JlTU*qaYd KHI$ȕU<\."" RUb9Yb [ EnSͳDIsui=L5]GLtnR]>pib5Fw}u~},sTzre޻^;=U-3mϲ6Gvy ;Ɣ[q;u"r_t#}bA~%wR)6Myzźse  ༾uޗyemjvo߾}犅kodwX?kodF M)+lXqI<^[[ū2> 5˺D)zMm!Ew\}%Чg3'W{ ˯աIMfY-WwiU_'g'*甼U!/67muwxzx; 㞗DlmSm*zksN_|"ju啨$u"iiWi J 9?$Ȉԍuq #?RoE21t72(Vp\^jz^RK)Y͝ˈgkvF2&^bq-tXJfE)B'b2h;i+nQARo?qK}UiCI<B5lYꤐmpO/9ī]2ּcbrm^[O5JFí1dݳ<*CNgy "-R]W2eÓ[W>Nz8xèz|ER-z I.ȼT͸O`bg/?wt(;⡁Ʋȣ\uu0 Z#ps.#Sq|+Jܨ.+Τj֠5~ ߦÕ DZ--x Ѱg1ftTz`H"FVV*^RH%4hHM uK6ZLkܻK3,cVǐCD|ˆt;H)IOQk-Rg[mf[jNg֮q:_[0HsH)WʲYDk갈o^GĖH\q}&'4M$O| ׷oQ+1eZLjCD >=Oɐ(ZO "YnVfYTҼԨS;}Od2h)ڥyЛmٮ_ "Rd=ܹ!PDTu/Mz$벉_o`WgW}L2/t"Vm->R]h V9NHc"~E#~yWp,sKn,ZxqRv|W8^Oh0Э3.օ&|R&\h^It0H:=k֩":QVrl*37y?¶z}A92'S#LH55c&ܣkTOÔFF$K?Kݴ^3ޥLT$V,754$JȐ3SK[Q}g<<|\V,m_˶N;? QzBXy5R.HUOd|l2Zm=wY0kH/SM\6vv3Wj̷g.dYi""nK6!wONgo*gKέ1p.^s-kid~-&(2a7{o.,"n3ZK`q9Dzԙk4oYbC}F[FdQ]Stz~\#1޽`B8KD۾b_I "R [;ڜߛ*7ŵG *{/&mt(0Ѷ <˨q:ts8UQMU#o?,cĆJE۶4>iA 뚫DzMdܺn7>:runj!Wɴ_kUvfJԓeTV 2k0Tt7͹ dƝE܍{5pј|.N/rPȥ1c^OLzYȞr̃ | >7lL׎Xqو;i9yꁟoPqh|RxÏZbM驜;4,DXǷ L[Œo |+^cH IDAT* Ƕcs?wtP?_YAn㻛? y{hu}mp˝9ysӷ20جUӾDDu,F:}$WӸQn:2.SӨQvr8ŸlӫW[q"Av 凜ܢVXӂ'/@ #}8ZMY:AC:VCG#2T=Ye:{\Չ6]㰄('Fq똨2IN=k3sR; 5YCx7q>3ƾBzgǪizmnjl'vXeH!?֐Si(x:[NB~8ϬmGa%'{DžMH!H!Mծűyyy2 m@ٚfff\.n _-(( ,n_*JXzl4|C466PKKf3xH+op`[YYA.bB 9}'uuuVUatRD KRBD ~A @ Dai+1gӉ i`Jm@{Y z{) ~NMoU=Z;^u)yut=W{azP^7Ua 61=M (9e"B_DI={:v>>pڮ?T_4sfWOׯ_zơח:78*_ )dmZH!P{K.]fǁMuOHsY tZ;p`kg[]QdDD{gr<.$mSvj)urȽ+NGKwu r!/X0cO?5iY7~#mdY-DTLmC]S#uv JI}gԴ[mW"׃z ]o]/EowXvrk7ί˨ ^%ݲS7ͭ}sSc-k<Ϋ5qn֐beoӶFέomktmi+Ly)e#n "ysxeHի+&S|՝^#$e/r%,? |5,5odqg{D>13l猭*s0V˯;ཱྀ8}=*:w(DN6Ԃa^%E/!͞M%z<ݶy^p~nn>~9o^ILWZ,۽ڻ-J{{ݘ}O)DQ̴}~Be\a;Eeq=j^'݁ywW/f7ZACf=7ftC&v(aJSnCWkTm 6Nj뫐HPe2DrR!J}=FVf{TG6Ql/. ~LH!ߊe>xN,",/ ;ﭾ}FS~bȘ\"k46%v)\"4ݥo&FHSxEnF{1UuUk,Zi6Ab3jLb8L""%Qg ayDJYn\%'2cQ|aeXnL""yNbV-s~ᦓkhm-BY´+ zrq>0.gባGgv#kVM.Y[b0e7y\~6RV~3y<&1L[i"FEKH!߆Rdij̽ES$3Air'hoxfɺCꄜ*k!3~|:Lys=S10tG^$stm>.K#]?MDaJ}NəI.ݟq%O/?j)Ud>s|qiG=ˁ'LP;w9lh45mQM;&U A$+P4+160M+/ebm OG_HVeA7BQٛ7zxb[~Q Mw82UidX'o,Hd|>gbԳlj\$}X1 ;qyyV|""E^\T~M%-?`|+y_eէM}*:ndב|+s,YROʳ3a^y$Y1v<6_I{7Mm~jͮ'9ۛ)۶A=߻5㟽xb{}[eg{៝ {.8ɉHt8PϬӋb"%ݿ^k]D9bΝ;|#]g. V=5 Z>wSN|n^yNdsoq('#J~ܝGwnYs ^9ݭ ft4A)*o.ٵqѼ1DDzxy3Kɥz䒼77]NmTKd=~?&O׽zTjp祰Lᓣʋ,"S5ǟ 3rz8ZV&OU ӧޝr@eSQ/@R2$ *I*lrB*/]?TE2عqK.鎜\rט 4l]Fݎ1U s30jLW~jef谺)d,6CԲ1(rbMaJ<9w`&Q7Iy3UJ3ӅIeȲLwtpppppkԦs='W!qY#-տcd.{ϫ=zG{tb;pϑ*pMsj11ݺuv]rjʚ u|}*L}{F6U4!NVRH.= 8,2F?Nո{|dWg\9؊CD60`W5Q%"7}UH |FͻԷmu5g#ʈ<^$ry!(L 6m&N:5o742(<.1ާхm 4Tjڰ1\S՘TҊ E!_Z#o"R E*|IRز Bo\+6;\PїS 0Nn PT.޴G b:Lʫxig ,xǵ޷԰Z>ҍy}"1=M ux.|^;nt).uYBKN,nJ਩e[A{yMjre;1M_\eWo-7,\ mm . u.KZtt]+!"E޻F:Z7"4ֆ=[ l0]N;w[`]k@Dr=]3֩EK.JQ~vl-<`{=AB$OeJvq7,&<ѩ] nNiL%N`۱2ˮp|I|c-y,ʯ*eװtSRsex-f}]˪חtNWu"B­6[5>WNv443- L4u4,},5:Vֲ33ˉ!Jeq=jtur\h9sq!+3K6)JLR6ttԜ*.p*T5u5MFN"'k+n ~aH!_!#U.uMIJ|F4):/ 2w4Nm}H˻}>˄Fj&n]yc;9s/Q Ԯ;VSL[|ǭ1|6u>n_j,6mkzW:5gc:8V׿YN=1ضʥ}z$bUҭW饦*4ŗZv3t7Wu6ֆWj}}}-TލfV3 ^?+oeTU9 m=\BjAN5U~e9z Nwg^+ *ajÚ0a9H!eELߝ,Sg~K-P7ռs;E[JaƂK=]kЪM39h#N`)g>jKR??itЎ#sl5qvvLˍnEp_zna@󝝝]KUȄ>8;;{T"}g9;;;O9V0HeνE#6*m&ΜR-zϒOD~~;7\ {vy77xz _U5`%۲T}6r%/\D뛯'K9p/K~tB5qn""e)k|?{ oj8n:v/> ?k;~ÃǚfyVme[m@]|3&Tt3U.໑C$L}si,ZOR]}΋{z{c/d Pա4oJ685SΦV{?k>l m%\njh@V-R޷#:]w_e?h\ϔ>+v;|,9aRQ*au^eΖễ^ָtN$uQvi1dműf̜ /" ˷ Q\^_H66sb<7jSe/J)ߊB\+r q4u8~ƈVРR/j~qzM\5k7o\8~pɾmLX__Tdw2w -Q2|O) 7\)V%gD?:]>I"U<۔=]2;6RR3d oZf(ʓ)_>KݔsЮ|K(m%@%/+ [SˌHd.KhμCQYReE'!zf.flӋ,ƶT-5jXUި-iʬQVo$B,R$L5gV񲰐SwFLabƝ9gn?UYz$(uSuK "x[Rxzm_}ﮜ25\P*WPy*ID`{\ܾ ۦO+/ *|wrB1~v}Bc"t߬B3?B'M6C#2b#*@;=פ7r^aӊWhHJ*´U)~k)Eoؔ @WZK )V%^P {jЀŠ}c3&!ql.".e쩈iNX@WDO}Q|O@!:{!N6ΦڌEdhE=%S[9 mQ3"*dZ89>6ὕ+.>pBkb4o\,"culz3-k6vRw% F-O#ЁeշZy<;+zsK#s"톂KVF04C8-|FLlU\ gGq/u8~k߁uAtK_?ƥ55Ge Otjr L=6 DEBu R_+!)=ŰK:kFY5f} WhuidͶD_i[>(> O2]o;E\HEAӕ?iSD  OKsȗ{ʛ>7s"YfBw~^\xz%,OMSttO ҌҴ Hզ>%Scjl =#l]qh`k';jԴViO 6j)H h]%h35g;ҹEIId+iJ"!BDEѪ?KR<5Зe"i,|H| ]YHg`* JO 4})Ol,X4 i$h TK@!ۛ9yzDAB)u>I2_Q"O>#"5xiqI(^Vt:T.D=A@AU Jh %wqZ`XRCLSw8,Uv$ >`Rv]z DI ((Z>.ioF:[l6g 4T*O$*DJ>EpH6Iԟ)!ރOeP4@E\6:\.baW$l=*XMMMY]'B2 TMDh]SAX,K}vRQt~B!x]M }mBf!t*n^ Z7PxTmB,{eA}YHm 3Dwwy2d/]~BLDB1 <­BSg!m^MBBB_wB$ B%{ GsO Nt'5,"O@<傰!Zt@'Ϲ*n%g@gЫu~rKOvz}/ej72 zTh3٧ͣokt%ՍD - r Qҽtoxg"BUOAƗE]+P'1?,Y4a7paqxvHRG_e6:m^=GG[iWcG*/K([`N*zGt8{j6>kP⢧#&"pr}1C {W  ;*[vF#K7gv]01 [Ct8{:.]N&H.׌QLa|};DM"0wC!W"~cWggnP#+ku~.娪0n1Y/~CyRRuIUaҧb7gﶹC|:9{fBw}p f!ϋhNi>P 7S:`=X=&wwkJ6w¬fl]yƏZi0{PlYv XnSɟI:>sPytrү'rʳo]^:ZG_5أ1X6mM{xl}s[?X6mqc3bÚfV~ k*9G 7:w{憐U۫=VΞwa 2pdW`EJ筦l?svǔ'{,=}6w.Ɔ3-blcثUFd.G/ W UR>ВƘ݂ cn6=X#߈=Z@\u$0ք;!gkn?{~z$R1eaPާCxRΥ%6?=H̒IxSv9k qn=YBPyW@ZA}徊Oߪ=͙ۗHP$SXҺfG O!!aJHnGϞ1ynVcGMn@/pmN>Q [KV\QzFݭyba=[x|=mB\ M:kUwTY6eɢ w\>oRs\哉ZsOѳۘ3 Aɸ f50NG˸W#ѴӺ -};4apIE36cw#kױ-iiҁ9V=ϊ ԭPЭ A9B%qU]&:^1d_3c}ff}t*>y Z.06ҐHۤsy>CꆍJ8Sp5iNQw6-ppҺ5AM@+$R 1Gr;~%=ru[V6ڭFc}JTEY\K[CbVWQZO]ZEpL$+SEzGZ|1mmFfISاJ&bhtkyY,L-7nWŵvm GS{9F6=SYRزML8)5<\ބ] (P2΂ע)UB+V@sFW-'Ҝ1>ԭ <˜=5OdePЄUlVs0UCX(|c]z׬ۇwi5ZzZ$IueM7ה`do&(ٚKt}Җ`T@r8,)7n|6.M8¿ɸ5?v5"hDd4iUr \\o+Zra%|{=6TTR|4r+xE2ne]fGPWmi7wΟ"94uf1T\]W&VF[Y>m}HxF!(s>hٞ@} ԇ~mkBfE+;`7jFliʝB~^/ܹkfO2cWφѾg}S gCo<-)H<6/|ṛ)"Y=QBr y=[ob9 @: h7rgÜ37E쿟QT*~ǬQwVع*%2W:F ynݙBqޣ_Kiյ)Z}Y=\\8u U@ZU%\R~~ߖh| IzϞ~T[L!Mԭ]9ejԹdT&M f{^uynaƽ={RUu  E:/}1H^fvV?Grnzr:s6K'%N:=Oi֪+ٹb^{׏\{֫ˢ@G2 esVPk xM0-lB=Lnzef7i?k 쮃ꖽ+4*w^.M#ЩRّwb\wČ52cO֦1>=;n4~|uڍ\얼jw$~lYl][jmĜQuJO}ТKW.0i//jڻ749aPow6S#&Ъ<X]zAiP GD `|:P@8=N}K'z54'~F۞Q|T6;_; MвiYa3{n*!l\"b+ (eBg_~o6(|UkUqmgd H:or̍A~n!# !*M@@F= Q j YHt B,!T6BJ/);! E$2%^W'WLHI?[qLDB< OU "Y_YA}h*oeUo$" >pR/@lf?Lr!I\$Uacn\me[R}qj\"ET(}*M * ThE $ɋ7r)A+!Zq=$$,M X S"GK,^RKok[sEqT-EY=i轶FߝqlgJc2u׈ϹY_k$4,I y~tT{. TH*.$,`&6 ,POec*n*X]ko#YHA]H& h&TP:&hZ?B^'"7$+d\eaDi J(1׻8S޿ ٧oBV,c}'!]L+\yVka%yj}i帘rl݆]8Y -qȓ7?5<4In9 lkqNE; ##a)Q?vqʴm) m]Q*1,E߯Nkw}{9Z/΀Yd2Dȅac55iw^_aJ jy)?ZvGG>նa< uRq|CDYrAM@4 |.!TC~~D )q[،ذuE~ ^Λfk_&-z7cIo~Z'jgi)1G;-_b%鞅sf;5ro0zL?LY֙/E՟a߭YyWA7M[ٽsL[m}p޷1VǴ4ѓZ.]}t9ٞs_jK~`%nf%va^;PgO\wua7T%^s]koѫ,:BN[vdv`z4]:Q, h3{a{V>S;I=llhnXeY搊7YYBC`Q_X:^!=mpccnc^j<`Zy{5rY$N8s㼙~ 06{+%!;.9R`<̢3m[^1/5Hی)M p;oЩ{o ׺:Z4X&bp"N ؎`XK *1eo'gg;vR'cLB0R3׉9u >#"DVSc؈gv D]y".&ԯ~\KעFT ]/zF,7֥\ffJYJ'Mcz߭خ%ZGhZmdg+)UB+EzGZm0 RPUo|ɼ+ Mw2$} YHy2RA]44 tٌ,^TN8ӹJeïzJU$eIkRd'AEmшOoY^M*?N^CYRyҦϷ姯׊C!5g [R1"JoQDȊ/~~ï*ńrT=ʀ$B(;J)~o+L:,U@$CriՆk[~zt61 "߳zx^Mn:<0ޞ=J5';s~FQFMZIR=KQ+Q%\mЎJg%9l!Uouj!ۦO+W*|wrB18TBNAn}S$/vJJuk;ّx\? v d`ъ T>S#&q ݶ3TWkZNl9+Ң94V4xUuZv#g<\y^Q9|ڸes:VbZhYE{~}ʅ&9/ofiCGNqZ7kyU6`xCʣԉi]i:J+3pOYQmG=*c0WK|={Pt݌dQU[z;),!P]>:/|+B -|ճLN`ΞKkZi՞)3d78&p \|,U@eaa3hm߭ g[42XU}]PsgEMwwrq¾#*1II!Qy't[LFӴId%Rr}e SsΥe=BO<%]^exlJ/7y/n-있[ԽwO9xiZŴ{x*1O~c(G3k uNe\`eE«!]Bx]p.^ YtMߎ'̼}ݩ2[NEޡN}M*=9k]j"{%:nC*:2}Ne$i[dZ<\gS)M)?={bƨSv8t\UzЃ⇑MiBC*}fe[ =ϛJ1 mf^&I68&cn3x712v;oNB}Li} ͧmL@-M:0gܪgBYqK^Mq>Uzt)7l=sam@|x5ˀY3/]q+\g/I̒<@Ugfo9"$P,Ft }@ضaⴛ; ۺ2BAC]abX]eynb\CQ6dz5AV9e\CQ_Q(5JW~PYKzƫY[5RT݉U  YRYRT~Y~R?=ah#[" bU:c͋VM[Iʴ Q"+JԺsI)[v@:Rr y=[ob9o͓u$uG"ISӚߏV@8׾o?kןKg޵| 4<9sSEw5}kEݕSFKJ* X쪛H^Mn:<0ޞ=*HcC:ٻ1UMx‡ndyl^vf pH-ǹ%Fe׿h=(%2MC /dAYkN-edv~= nn])ʷ q ZV}{>h%+z43镙=ܼ:=xrO#4ǽwǍƯپa.؈i##%s6 [m$Bx-09}sC}îLآZiP GDQ֓MAn3)/׎>" yqSDYmo-Hq|bC.|@wĀY%G;hW3 vܵ{΅ |J(J|ri[o{Fթ/eӲfTBٸEo} B!xF!B BB!BB!YB!f!"y@7T豾Sߋ2uWwOXϊR?g2Pe1jɗ5TuXj%+`gװZwբ3<)ߵnBB˴yRGU..{Tax򃕉s!@seԾoBԥG&wt Jgx@d 76,p1|E*1IC3,87{ȜEJrEe]Y=ݩSQMwvI, ]g![*@od'E*pvΞKk\E-`l2u0S~ 6AZK A`ah3[GB0Ez/׫kPoU8jְfՊyT Z/~CyRRuIeC\GfRMa1*E>qyL.#hЊt Ivax-+3].;_z}Ξs.( -u;‚q]|)cx&McܺW{ͺ!i>~KJhE%"_޹7֏rqt >S O9z =1޵Gg¼+ T)urhR4n^ WbjUy;Q0x7ǿvvt_7wg?20VqBk$yķʧR{?R${|ʬ#x0(/ď]lM1vn`l"egKOKξZK `ahAr(Maa*{KtqtqeAa\8xP\+q.SOky{'9:ٝZ.S.Pc\]\(TهFޙX;XȩBB aV2v>;A6cw#kױKXzN룼|Y} ͧmL<+^ײӺ [xla qX4v:Zƭ)ވdL+ k"Q |zp ϰig|F&m<*ƚH*O0T!uµ5-kJ'g_كۚ7>}档L="LV 'vh¾[K`Ac +AGB9ZWdO[CFc'2א"kDM6)>tcLfn]u^\J^\j6$I?R=],u9:.!8߸0fzYw4!>Fv<#<-LQ늆ٛl BTʳVڈN{Q3ꘆU ,}s]B g.J:wy-cs*Uݱ"/)KFs y7;j5di]Z !5o&տqwkQy0߶ycΞgǃ:w7%B+rԨsB\P+Q%\mNnÜ37E쿟QT*~ǬQw& ynݙBqޣ_޾ֲ}~V׮?,,μk*Br y=[ob9];lL52U4aX1Tᓸ/aTJdYiE8 ,735E{oI-#jXT>%jC\r~~ʵ-?J=GvsWAӯOygm2ݬo+N<-̸'rOz}MRȰlײeC5Ӯ,!_"}CCV v j35bb --zw =Y^QA? vlr60gMc|{~whkXunɫFxM7V.?%=Lnzef7i?kI]WD2BX gZ>si#BfU|%ّwb\w\42b}L%F͸"P{k!cjXaaա|ݶ3T>xis:קV:Wjh5 ^ꖶ!طg3N? Z/a^z6.0Q9ו܂,!B?BaB!BB!0 A!f!!B BAh32 zC*=}j7[z;)e}2L]P=]>ϮF}LNޞ `΅ >x~zr2vW @RNs8y.>*Jgx@d }#6wO'gL.=QC3,87{ȜE]N1gq!C|: =OY.77 1^Ͽ#VV+&wu=lUƾ<1οshT9jֶ*&HkM};DM"0wC!XUΕ5S W QrTʿqyLZ1,^&~cyt_\ūm͡p#BB͈ kY -6LZGqJxcٴ5qm`ٴ4*A}fɤm);Ϝ58`!euCl:UF0z`xל/Z`*FDM'/48b ]Ve5Y.p (Οn9еMf!~Ԥ|Qsww+EXQy#(/Lм֣ LxM;4;76i@/Vm apo[U|̣cgnkgw&kW:؛v -ty<6!.f50NG˸W#"3ۄ}wƠ|SX7H'~8blfe )c裱s4ߊB\+r q4{!(%2Mg/HÜ37E쿟QT*~ǬQwV~]3!?$^E_{0߄'|xfHwϦE/̤RG@KsnzC(זAW֝KO۲"ס.-jD~w唩Q璅R dԊmӧ]ڗ~H*|wrB1^0 AΎ-#ְG aWft>L`{id ~ˣMKVaDz9+<{ۦ ؖ_qs&72wYǴOqSVSдO}ТKW.00m✰bO`U[h1wY]jE*ܿnKr3 P}%*m{m9x⻋-bG[>v¹B-<={@KLAf!!>QnUӒ@A!BB!YB!0 A!,!B BaB!BB!0 A!f!!B BB!BB!YB!0 A!,!B BaB!BB!0 A!f!!B BB!BB!YB!0 A!B}zMB}zxF!B BB!BB!YB!f!!,!BBaB!YB!0 A!,!B BBO!@!TdTHT*>oذ!˭ӻ/ ,DJQthI%[֫"M8|B_R疖9y{MV*7 l'.(R(*rTg|BB}^xaddԠAzZtRiccSŞ+Jp892V A!Tj/H͎'卨o,!PF4Ūx A$RQ BNY0!,!B BaB!BB!>;B{rS߈hm;|#rU٥˞X6 c4[3-=BRڱL%~qtC$o̦>A`B+̹nIY*s_;t z,aZo7B4lt+O,S|??RXYB-z&#Ǻ L5wq6λ#7^CAM._u!GUgv{%mmü b2s;jٿ>;Ϗ*Y$¨֙$Ճ܂]to>&SYyZrY%%X"Һu[,IoyqWsp^z,F)̴R^B OZw`T#1g۵qYZxLP*w͈ȹc'˨7~tM7$"rѨvyGǾ-zgoڍ [rQ`=H~I;$Sd=T 3Zh!A,_Y 6ݚMf:Fzg_'p݃']{e 'e=HmK}bB諘 Qȵ1{q[0ccCs Y`hv`Jٷ.57CqT>ú57Z||x+]IZdB p ;t&BDMnQT"}ss+mTlg72JPJiҠ m-32!KG#+QTk[wuꠟs[=Ss|g^$zBc@W$>1eJ!eRUJBePeJ02_Z)-=Z,KI 7_+#Zҫ>ԇB_-koml\צ utFk8ѳ~kEHnSYe6崽ȼv򑉧܍H9\ -y'h$7onhs_!9];4`d<_7H6+3co>OdG]3jqnLt<ϖﵪ.\.]lJB*P!TBh&YmwjUQ˶mѺBbSUX,;]K-q9iYBi\P(6M3-aj5ވ8cFݲ.uoip;gF6T*z]i`0i{vT?UQԲ,GBknkmty*4_d*P!TB@P! B@sM=|aIENDB`././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/doc/source/user/figures/component-details.png0000664000175000017500000016302100000000000023747 0ustar00zuulzuul00000000000000PNG  IHDRzcZcsBIT|dtEXtSoftwareShutterc IDATx{\T׽ 3 xali0Em$7&[%_ĞJW9"A~爴|UihI&QLD3F6쁙3 * *Iky>af{68NB!B! B!BB!B zB!B!$ !B!'B!LD|>|uuxjzt|n7^ݍB^ SIy.'Ӊ/1)!B!+x<zۀOhk^<Ÿ4|4=Dѹ~ mO!B!A=zz#(Ȁ`> 0 CMk2_k ZгzV% !B!:6]lqAӉVB SPAAAM L&3& LPPF  FF#F?  FAFp؊9i2S ϟz:dy`B!Bz4sAw#ڍ#bk&M6 m/ ۍ;_St?ﰠ'B!_Kyx0 \gW+ @C apu\3 gh|(];{XW F#։otał"7CBO_+6*CzPÇY &c`q^`]z? |AA: ^/t {Nb۶m444лwoLO/j*Jdd$ӟ~O>#r/$((3fЧO}Ë/Hii)ǎ`0cO7-ëi|@5R?5i◿w 4z]ol| DL!w??n;wΥhЪ15-/^|De< 8 Ә]< 3O,D` @`;!,bt  &L\+fA6=;GPxDCPpH6O8mܹ3]v7̟?Ц}G\.zΟg, \t AxR%A]@۾sab.m:4JC}կ vb S } ;unrc'|n"x(+vB;^ƤHVlZH7Q73kn!0xV/}i;p]zXLINaJNђȊ^ͦynne"\L}ILRy![/JpeKLZdYDҁK+ϳ5/x`tvvLGl]STzs$1 Pd`ň^B ]~FUu˗磤Ju4vb{]7m@rr2!!!qQzw0a?O&??;v4Cv9s&}ɓlݺ}1qDL&n?1z>`׮]߿OYiH/2WmnŸHFaDyApr\jC^Nٌܺ|Pghoܫ7w%ϗ:@4U*>WiŸ)FrKw6НoXʒ%Doͼ F!!u+6%Ȯ̂u jv%k,^[!Zs6~Q9zU;@3lZx%jd/^R5Msl`[7%v+$c2I$;Jؿ6eJXn%7uMN zC2pO}=ݿݑzyf+>{BLxgBF{G0`x')**رcW=EQ|׏nݺQUUnfQTTDxxxPc69yUbЫW/|>O555xjjL+m4k F#<5՟rʕ@%iM5_~mW_Oԗ08uqupMIaA䇔 #Z%k|fߡ`1lNdUYdm( (xRRc3f2j5bfogr*:qsV(52]]!.q)s^ݥce)+}Yk),T5~ ))IQL֬4nUP>%UR+ⓞǶe#% Z2 (JŒŤ$+6x i)UqHdgLrW+ {c mIdG >,0,u9 ,hE)aTF7nj#!),fgo$a!qNIc@$UUG!f岿LT&uG9K(*qtP5xi !:" ˮ~u2Ft{YDǏ"yjy[u OhE|XVĈPqH_1xHɸe,F}ɩ̉Wdo.J ÒSX8؊ncźy*ط[X]$%1 ng_r:5f R`t[DXIH}M׭czZmO襬ה$ƒU:p* ,XpNlYDcTq$4rsOIp"Zn)D'{ 릯n^/ B ~o]N *wwVK4ً/еkצJppp{zFnꫯp|x^EPWW״!cC߫ιU-}x 'CC ?o m:jTBBD*Zn gf248Myñl_hE,Y1'o+6gasd)(9E+ZQӖ}:r,hKQ sq[:V&XyYdm"3QL&ok*YdH gƆeٔ/yplLIHudZD۰Dnq'96ii-N$=e{_:V+t{ \U+NƲԴ{,`Dr2 !naɓ9d &<ςQ7(aY[ưo0'NYoQYmV2Ou}tg:/沦p @UTDu,A$gaDep+hEYJ v]90FTNvn%23i)3܉eVͅ5̈AEեd c9ĩū$-M9@qoM!Aup c>=*ï]D((*d<89hKPQщðÊ샔mSPj]we-(Yq7;lFmyذ<G|8_pUQ5Z5?>|S`׮]O7D~dj#D_18KW6WιG{u|1(Wt}= uwm`V.ZYYX@SdlizsN@Ϛ˰_VP 3\oIJ1KK40 բFš` \F*dea<82x(_XhQwGyJRb=ưuP:4bAhZۋwGw*=NV,^B Zq%(gJλbl$%2=ҊH#nʨDr9,>]MVKͩaXhE5&'M*8JҔ+|hiS[]o63% !: q0yQI=y2gfṙ, OZEQ˃VpR5QqrC{j4St_Sp^)JzS(/^VzSjPBS@1n"Db) V-(ˈk-8zXti Ja*{NSKyL|Gu+͆wk U[< L&Z9.K~~IY|蔬ɢpb6,&gβ+UqhWnӢ9](X,aDdSk?e0̚fN5psO .J[Sc zOw:A \tr wg.byiSˆ23 Js$0-#}Od%Q4bO* #R)o345T`oO؛;8M5o (|GVTs 1cc#q,HO'ys-Aue2rEiGySuf*jP*t f&2 dl=ñ| b&kBQGR6pm=QaSͨyn=* <ڂJ]ߜn/#,~ l=%cZb* oi?t`g£ >^ҹ/`h4 2b2bݭ_  .ٳW4:m2:l2_ٙ+ S o]u!P̄?᫋Rk/O7/9q6Fnݺ^7]T4O@IA?lQxBn?@Vګk7j|"1Av4!~TL`)((WZE:qâQ0*{PM,ˆ9,_CiyƔ7ٱ 2۟nP'3'Bv9(R 2\50;7XJ \B-J!?%۷$? wqŁx{j KqJlvl/ Q(ޜFF]a/'SB/Yں6լ-^ wU=*R,LQw-mE5)tdㅷ, 1J=,ZTKY٩,LIቲlho!ebcʂQ;ݮbc7o,|Up`U2- `qf%Z̊xgǣ`.R㙓Ct͚zSON jRW_:΍k+ }. ¾}x' oO=EEE\."""ҥ f^z@k@Q6^E?S?`6c `0χ }j+B̽nw[ȗi }c0cf]>(Tt\!*_] T1}0W-(--eӦMcit:fƌf.^ȪU.]` 99ݻpe֮]˅ &""*}Yz!***x /zjkYr%,[ mOΕmߣ*k<$@푿Rt1Ɛ/0Y{P1gx>shi`'p1M pC=Ne˩=rǚ\\Xu۱/HZ{0s/Α!!ГG'(LhV Vr dxԹ[keK|>_ V{bbb>}׶M!hWEUZ@59t@%&q##B8)z5|JŚ$ɤpzK_a4^3hg4RQ~ ^6@>rWz LKBf)K!B Vunvڕ{AGf]޼zy|`J|͇j>#B! zyhL?0F)5r5q59|>>[K{4F#GZ!Bqsu9(դY^'Ch(AwAp0`d8ј ꁹ3ܳ IDAT_!B!gzO w` t0 Ũ(`0ʐL!B!B!B<!B!'B!BB!B zB!B!$ !B!3gdO!B!ķ)EEE]As'D|χЀŋFc?!B!Ѡ'P!:Nck3C.B!Q`C]YdB!'B!BB!B zB!B!AO!B!=!sorMX,B!BB!B zB!B!$ !B!=!B!B!BHB!BqL_dOqhhhzeg!B|FEhlv_Btχ륡t]vS^^?~[!vB!MBBBZ {2tS;㑝 B!zK!5B!D{ۈB![FB!BHB!B!AO!B!=!B!B!BHB!B zB!B!$ !B!&d]ޥ3"{ E`i;Xnyi<+8Nҥ;b]̓}zL~:{~^v4Ơ1/q9;8 oel4_Ö~|+FO}V3:N1r؍;y:?ȯW_~C E^Z)'dz?_è?\ֹ:{3drNm)f˞ߏo.ɟb@h[b=[~DŽ8tx87Tjh!$ !nap*c#T?8L w˳B+>4|]Y3=CYdX{?إ]Q Cn zl{_GOb}vmywDṛi;ب Rڏ($=efٶ< ?gހd婢BB% O/ el \3[C'vfi~6I1bG8Nd,}ܬGyIA\á23YācX!&PꄔffWaekdD(=S'zR2EYGz.Nj !/h,}Zk>zШ`q"wMǀ,˶%Ѐ}?/NK&,`;SCIXAui姳2$NPb9-׎lC2xg od+1SA=#;(b* f;]zJ.腧Yew־ti~=VDUSN1PvfOu8+^I*OA} 2xCwAe>s1(9y*DZ?}«,(rqʥsSgR7 }wwR$XNW|Ws#Oz]?<_²gxh9~e.:&J;ݻ1T3>KQuPyGԀ,{(eπX'B}5߯ &3#Gr_x KN3ʶTC=i3_@j?Y`c84u9o'șެGmV簷 A/6>_g\?wELsy> // ۳d@,{2y-pΣ%imO-T/y{9L} `3T~3rA1L|PUUW1o+4M?r SL553o-G.AϹ1+>\*j!:P?Ç3|H<68']:izf]<]\l]W|;G!L`;WVt;h3Xn[IgI${&n`dsTw-[o3u֧onNO'>7⭷62d wP҆ZƐq5WH֧ ~7vd39'p NulںEYv|mFvٌqn}G{ʟR.U4~ C;ro93׾[oIx'H_iwg,bܭ,Ӈ2I͇kwlYdpz:;+;=o,w]v_Շ|k*~{GD~!W sx.je|&X6ωcn(>gR/|̏_!oz,5~_T㼽}yKdSǘ1E~pz|pWrıG, wH|)śo7I8_-S$'rRq%{nؽA'YD/}iĖ_p!x2vfl\dþ Cy F H[cUc}<0QGh7i"''y?~:ɽ;p׿NyڷsUuԛ"0n*!=[DUg4w?(}/Sx_26@4Vpx̡{ك9[{v'۴A,LΧ_cϖe b5^`j,#sP3r޷KqP[?wr2E+=fcYT  P[ͩG8l2q?8܇cy>P?qH`ޚͦ9@, lmx eJn9ۓѴ[;Poi{VرM e_ytŐd&d5C[G060Ϙ z6QmqSI_2/ *f@{dl?s_'e/JHv=_xA?3.;FڌU?E(k ^}\n  904]q5NN2. [;+s`}][<ggȇC%>[`9<ɿȮ fVWD 2=|x{,__׼X{s(HS"CBs=']~6퐖-l{d 2;c4!r(v@Ѕcca!){6& ,Trw< sD1?vrb.c22:+qhjEюsC,ꫢTo0i'9y1mX<<oSy0{enH7yngCTOU\'9Y_0> fL`R0G;Xz `.fGt/:kBpK 9îH4c$䉦j/v{(h k{GoUss"/v6{]eɗY)@m+*=T8zKF*4h;4w}^qJvt @;X,(( c3razO'ݡٗYJ+C7kövUqbwi},(P}9ӿtNZno8:Jf;$XsDuczpS_ 2֬1!ʱuЋހEMe\nlnLa|Ef8 Cu%ځ-^t]$l7{0BQkX3ޮ7F^79H>8^,iG9b\9ZFhB_!YSSPf T-hThn\ Ju^!7uo!y61.-_U'?/r#|=.j !2%4SF]h27;M&BM/Gm~}mQ{5z >Z!BIAڅi}:4ƨjob8#'~G$MjٗDGB9{8tC!2t`/lX=Cmh( \<Ѭk]ySP![h `jIuf1;pS]@s[BU t 5bQ[kZTQMY N~J&G䒻;w6n{>jtvx?_8D΂tRw d[)QD20+{X^g1+Sqdډ )ܰp:]s+bO\chꊳhg׫J=,]h>ꍾxt*K \Ķt\sPONc{&/ʴTFʧ uԛr˩u.$IO 6%!Cq~dZ˞3}C]7Y(H=U1ݚ洘B# \\{*Xt"%%_cϻP}3_# X{YH\f7"@I2#}[zp>G,c{ x>'8rƳΝĪNP[w 8X^v&s8yꟻVq=fW5$=1`Nr ०t9o9h$kA@}G}n_ w_.s Mx rv/6+6!aW*gY( 2e mL_@Ckz2vuĶm8lcxg6Y-~{pGLTӿBs\-OnU?-jٝH39zj_#M!֣s$4)=u-ϕ?XfûC]u܇>O5~Jt \D˜ǯ\41CfڗG^Ѿg!㾕!*x'$ֱs]CgcmN,XLaZSe+LXi'q8t,C1 6X2Nr2O`Ȅ `}y)c;Ҕ,vHLZBEz*ϏWzen}3sgX+((XLژZ^*M5%wm^YOL4@glN|p`+3MI,X-"-M!3yֵS;H'3NKZgf6t LVJ5g3wR2%?,%=U+#&2z4?"[_Ks66/+<6@տqjBAԧ՟K-'Xh j%@݊`&$>g/4@XTg޶(3)l'I1zUOd5vw9z0gZ?FpSUG}AFyc[uҿ3cg55?Z.176pľ̠kG*Sr.,Q9Yr'Qp[ǰ$Qi-aB"Φ芍!/gr.(7q{3,^dw̵`I@7ϡJ? j,cңc0nșF#Idf 3nbA9m՛7|I7z:cl ƍ,OA189:sw0W빿|+$.@_o|`ZL ;s`/~#Ndn Ust.] Y~B_܎=^uMyy9?-/_z  tuՓ)]ՏAw! #VnABq麹xca蜹ɹ.NЗ!/e\KYr !B(lg!B)"AO;YPPҳ'B!064. {AAA#B!DAwQQ]IP+XFI@+\M0M0oR>*d[j5nr@N~8&aZS |t00à#j3眽9·j=Q="{O-!"""""r#|P-!r9sӧOCOO>ˡC8""""M=Q=Q==Q=Q==Qaڷ+Yr%k6]lJV|ݝ_D/c(?ܭ+("""""r^hsp2Xև~YA""""""1\tQ8MF^]dDnep IDATST1GLŝ)]f tyq'=*<4»qUL8>|6׹2fJR/EA}IwPHwg7A!sDDDDd&=zmØYӹsLa.z(8&5r?u qKp|%4Nc~nay6?61|\QN]`]Mn|x oѦQ""""2]n\;0ioe*s0=;:=ʬqři-;<jtΚJdmu5OeDvvcW6:?x1FO#cT".J\m<01DYĸ=EPKN_W̆m/K:s58Vlޟ(͈m """ww ]@ ֿawrs+Ƿq;'=18wƼ-v=](aoܹC9,ϟsͮzM-sODqziC """CMҹ~01eqFx"gp~R\6Tf[Dd4hU3u =6Μ9ӧ顧χСClv^WQ {{_fۺyc/.zwD/(blfd3g~gSifn\Oq&whFK,l/CL_gwhsX<8rV+q6;4DiY9K^杔c@ӀzcHʼlr.יϜ%v.ڗ:׶"A]"""53 DnF^YRP{V48*(f$^LgÔׇNV-zz|&j˞Jk9yׂb5GmʽHX:F͚XQtᦎVj+h`c򳼼&l</ZBaii,/Ro=\U+*pdE%gBlT(tiS^TMCY%ˉ;'8 ׈}:[CC-Ƀkg9+XRʟ6IFrj+Zƪ sb.i5XQї$efoŌW2v59lZE$լbIF3HI$jjSN|Me,_Ki0?Mi21F~נ >]kY;1)TKs#ōi%V!p,ժDDDdHUM rsq7K;yEh^֐{8G1YDĘ,I.nM$?8:i>\Ls y}gflne g#1>T]TgK7 ;YSb %mNf%n,f,1%-+q/c p譧ٟNIE\1f Qgd#gzr.F&-`nP=Z-8~qcHV^rEBFcHL&cA}П[%KHdNZkϾs6=<.\)Br;X-n0hߖ14.Lň ^fj O.ewom!""" z"2䯫!P\/'ZԴfkDDDDAODli?5rՔ]\=z6PHK4ԲƋzpYLݎnq>gg3^c-YQtd/ߴ$slJe<'OU>VTy;F$I* )#+ɲ6mFb/`!3qI&d"""|P-!r9sӧOCOO>ˡCP)MX笡*12kԷhD¼LRU)rv:hde搑dd3IJ)mdcq HJF0ep&?ywAMt'FY!ODDD~x3j l6HDDDD;M""""""'"""""" z""""""'"""""" z""""""'"""""'"""""" z""""""'"""""" z""""""'"""""'""""""7|f++Y2t?<ʕyK{r7;8{Ά\_oot^WI]:?Ol2&."""""r32\vKI[4lmy_8J*;p.N#2}WG3{a}zzz|x^:fSugq}0w*䉈0qyqh|C7EbsS8q>ӧOb!͠&N:ѣGX,9[oUx<ž izR&N8Add$fY!kb6X,1DDDDAOD^WW#FPC|FSC K[~(艈(艈(艈\{ァI""""""7 "7vE׷ fM3)ƮPo4 ?/S,""" z"B_)]Ǝ^vV,a҈ݴ]Em¤DDDDDJ~O'7CO`σʛ qI=a͔ɔ nkȺo쥭̓ac,Ϲ#ܓƧi`+'g06|U1 R9o=B?i{;0Z|NsM)%9桻Gχio@#< "{]Os7%mX}O (~HNyqS [!tH2@Aj}͋~`?7ADDDAODn'to;{g 'h,oR\'yzR3JbG N'R K:'׫Ƚ<PJ~o'@ NlxOFtf8_!f0pzR6T䑟>O NּZL/~? o+ϽBէ3'H_ÇUd]~'P甗_5Ҵ|34;SySNai FPp4{O;"""78M"r!cC.ٓuZB%0Cw采 !Dush̘FpLGzԷ/h,s9 D]3 ݴ&1cla"-gy=}$ pIiaF0`:ssooO#G}p|6>oBMA=z"7.;!$_2 CЀ B.BXN7>p[H]=Gn:Ol~vNw"{tyƚ~f4AO7VF]訾vLWC&^i|d<3Go|""" z"r㮄p<;kiN['];5%-dRxt9 vz: ;9 Ӊ!$!B:;\-sX!, qd>-Pna4qn55qS}PJFzEDD䆥?4co 翟n>? b`lE?@tyg4fKx0q%NmmØelХ0n]o vjvW*Jtz0Αys~EH'h:AٟzwmS=?y`` M DDDnŸ.c\E/b?`'4~ bF#fdxu~h)YF@w IofOvbΓAP.]~YCݗBK*7?MhwR]@j ?ۛEZSsM^[|;ߋz? ֟n1W03ώz dL1FzGy p3j [㢮x0a„S#o%[o(_OƂml?剈ܴ4tSDd:ͧ~#Ň;FM<DD[+ s꟎EDDDnNn扏xwh$[Y!>?Ew~FrE^4)?"ud|o'ǤQu6?nt-DDDdf=zusap/xH)r ᣮ#&2>&Fk'"""""7yi}¦:Rw;xZNFM8?;AmLL}GFaNFG/@ͻ $*n#{HM{;Bu㉈sM&cBIvO(NoqH8!ξ- lZ>=1ytֆJ6?»}/7M ?>yG0w^?m/>ۓ7 ]aODDDDnu#>BXsȾyoK95!&8ŷ ϩ]i@.}hm'44q4T} j#wPLCNpVuS=@aݏû_x^Ǟ͵LPJ';RBaODDDD|GC0wpS¡:>ɿVlo V⣂q;pn+OჃ~T[ 8B[6y/25~pw7'[j8 rAGnݯx?I?@ϠɤCMtM_Z0F jΑ/2GA;Au0i|ǑNc grq~KBTm|̣G(eowx#9MNNȗ-0ιwr݂rȵP$`ABDDD["""""" z""""""2}5C7E uoBd CŘ1c8pBDDD'2LDFFٌlVCȐwDDDDDDDDDDDDDAODDDDDDDDDDDDDAODDDDDDDDDDDDDDDDDDDAODDDDDDDDDDDDDAODDDDDD QK|5.J """"_M{"ș3g8}4===z:tH#""""_  IDATQ=Q=QQ=51m4RVU,g"""r"ÕƭUTo |j "6FZF󒬘PÛ5RlKt, dd-"""zD糿k+2fB!J|4VP|//Q%C,cδ$rWpaBBчbV/zˋ\:OƜ5WSE]]5DEDD"nm%?j;x2ssLMibn ,Zar}j!T4nύJcԔQX\KEY.^.'|mѧ%""2\GOdx<^yTn*"7m@0Ǒ|=^9 (/kzDSDscӦNv8hzm _V*, XbEBG+ ؋W`w>ͦ,b@DDDAO$ڈc(ԵlZr=o#ERTM~AUk{`|M;yy2S_Xv5D]m2Ze4%jPQc㗕WV-`Iu /|BO 1WK1g V-`3zeKeٴ)lOP<,R殦șKbYQ؀h2_."wKn\VRR֜ @s:R7dGqTVNsrb"ɏ`H|N׺{*P;hiƛuʜԶbI\L^Gus/+ם~x_Ee]e =Fͺu~v/ Գ>g ߙƺ$~\64 % {5ētʹ82ұ5x9|bR;ȶX#ky#}H2s3pV=@cb<ka Jl!#/~RCQ) 5,;o&kZ;;=ԅħYv^HL%W Kޕb19'tg-5f-5FG D''\{ez|K\^kP eM eNiI0rrh{15܁-$DRc71!B4tSd0bBhхI38n\AtªL@bދi ;\9_2[D)a؁0Avv4pĩ^=*W)pF5sho nET;[a"H[FB/ڰRHIM&%N (ܐX#pշ͈7x{YƋi2ISӥC藮b G>7\Fc(+˿/Gsv\^\҇Z2^yא"04q1b4 /}K0PT@"JZT79..d瑓'""lSx.kLMþNcLxƸKa=(>ȘytVTA/69-t8h&p =00+" B }˝du.|/1ba}FcYazx;a>.9kW7U.k1WH$#?|8ּG{y5Z 49oFVU.)aGQB1sL7ڳW;u^uTnۅSDR).ڏ]Fnyݫމ:p5lf|v,lvN\bMAu qBR\TwF\úyq4DpA:q]D$Ubmr(ByY^*뤱-\uJ͊$!7_d7tCﮠ0sYV\E`?o<]ϱN[I_Ax'ٷ *gcs'tKI[4lF*wek 2̒^˓ YQYؗɺHƱ,/lB1Eo3%M--vDzCA>7٩ $\ϊtPooƛpB|$κN5Zs.XCX;.+ͦRC)w oykpvĐsmm^/bjg2sG742փ&p-rFv8itIG/3XDDfuu=zmi=/9GdEA.^)a:˞`vQT0}Eli0~R>6(. ͅ;%<q۫lFwa6o񸁸Gtp8 Pēs^͛w> j&%o f_KXTuuXSYhXWv[uUj V,YIh} hmFk.x)%kQ!MOn5n&Lh:'՗;ɏ/)vn XMUm|UD٬h˻=01\t2 < s$aAXa(~ZC&2ԥxv" &b9z.v&w|v;hDJwNݼG^~W:.C`iyPš_b\ --_x 1P: kY889|g}G攑JIX,:Ds%c⪯ ee6'fOU,XrIff&:aJ^2>]Ϣxjcmq:[kXp5-c&6Mn U1&R@xQ4ٟ}7ZlO,_QJfH+> ⱘc})x^h $b^kihekªP" $} ?%V/Db-Fu\拉JgUF~g\xw&-h<2ZƖ=<6&!{B'y4! 4 yt^O] E Y簪<̪"kpַu-$ %f1uUVoIDed8#+XDܲy-²Tv^R1'ƍMDŧ)1 zʸr 7StP̖de.g=ϒٔOYQ/baG+-{a &!)ԌL%Z/R/_7sR.keuuM xb^z^fkiri"h[:Ϯ%-Ƅ'?2j8狇RJ6g-Q$exqqBnzglN=]y;zDvsB O0}o.G~8>|}f% Blj+ϽJct~AxY7{(/(dg SC^|٬́W^1F8+zW@ѻupKɶ =n磢 lO79sӧOCOO>ˡCl7w{w;)j} ͔/"""r]\usX-7Әl C!]mgێ@g 퓣 2?x~:]?s{YL6]Pv ʹf4"tgƮnfa"yxiA!\8$;HqȰu+nɕ̞l;vP:QXCÂΝn{ fh8 L53]4V#̟9=QXv<9*ިOꌛq+)~--f&?'fEBP4sg3Ece1)#\!Jy""""2]᠛'; #nZ^:0ZOri%h覈W_Py67R '"""""7G#*^B{ɤ?ά]}.ԣ'"""" z" z"""""ԭj=Q=Q=U/{DDDDdS eW=tSCD: #{ScԞT/ނhmI2=EDDDDd6QhoY(䉈(0Ƌ96DDDDnxzGOd8s1mKS*9IӘ6mI{/0c|ؗ`ڴ%x(9IL6iRH[FUDDDDAOD6F&YX#Y6O/᫥pF&mrm2F$d۶ԯYS DegӟDeI)j ʥQ7,#/-3@D"QvLf,fFf3xV5YDD`CeDdcyUDDDDAODNQ@j6^|mI39oYIֆf4SDDDDAODN_{x0b6`l<+"""" z"2-f47 #$bڦs{Zw@tr9CAEDDDdx1&OIS5jk[o%::zlcqfYŶhn'E&6ժ&"""2܃v+ڭnߧVH},n\‚Pmr2R"B""""-3}'"""""2==Q=Q==Q=Q== jC>7ȶ7 1]qw|Ӱk=o1w_9s[nE(`dK]@fʘ߽f0l/][ljY3ct{htͤͲ|~0=]rLaloO}Di:}{/xlEwi_GaYoD8܏e^<{2nTnkȁywЃq |`8n$Ǒ> ~F0frK&^8̙1n8v̇s9&:7OӸ?8)@=a=DDDDd;z2[1!+^玒_KobԷOұ{Cw; xp}[nVb3ngϿGMFp8AX*]f֬lYs <;<Iă+El=ؔaxJ~vD֚;d;V"z$c$dm?){owSݓOhQ&3=[V=&xdQ\MsҲ&V=vX0Mv?㷏;rL׬.} c Fn "|fk*2GO0CCDDDD__|u9gCٹ*#S2WOw, ?L޳_mp_g=;忽[.;yCrμ,lC8919>;(ƺGxXmKb7V\_f\ǒlocΗ|Ϲl9_X*{-[n#<4s -ro6~,%yx a4tsl{qav=~^SExυkn3f!&Svʟdr|n}<|mrf!,nn 32,`?ǻxJANy}NcLcҊe=_Y}]x]x≿c,e~–~9~-󇘶?b]wί% ӿm` Y OȼJ3έ /|20c{_b_h~I:6S#o3flvFc *颢q#kܖ]qC f7 %N =Oppzۨ=߆N4i 2qb#@i 52 pPaWau%hبG_7i~e8~~;,mk.{/W*RTêfՏ-מ mVRxc9s ]7ⷮO]rz6 w/y{xp^i400pdoT66=;o,39oxv[~ z"""" z"ro.%acqvf0?ڎߞ>"}7g{ވzt""""3ݣ'rhȼQx0 dsG,O34J:m탘j}=8)o(*%8;n㠭7;nI;CZp?1 IDATj|"'J/*"-ZzDDDDDDDDDDDDDAODDDDDDDDDDDDDAODDDDDDD>Yz⦈(艈]@A`]Wo?}c> t,kW(1D `[Fc7ݑ2>he1m#*艈(=ckA{o*.@e&laㅄ8$qկFu$Ǔ1(.#eTw]6[!ޚJ֭O{P&7{""""AZ99]C*R|LNP@ˋejdk};e]8tm&K邁MԶ%hu3RKk-4tiу#r^f"3;;I]W3X,"""r=O^ezzW233 3";O-ZĢEx衇xᇯEajjL&?g%3)y;%1zS[JG[wbiL+Iql92ݘ6Nvvwa`)g8mSǂ}rssyᇯ]*|7߇z>իЖ6m? 5wkSm6׾n*qC9h7F"!yG2uܿA/''!OAOdws؛ɛ {0bf`83I>.|9XL!lIi""""J_\ț|"$>iP"e7ؿѢn'0ib[IIfZNvi2 :X3JgtOAoBDmd) FxjX*g{k.jQiv.37R SGEW#[=84kQ˲ԝ'rpU&`YϟDDDDc@DDDDDDAODDDDDDDDDDDDDAODDDDDDDDDDDDDAODDDDDDAODDDDDDDDDDDD&OyS̥Z-,eZHDDDDDAOCGsI.y7^ǾK<""""" z`e<&<.}b'/=Nq&pz7eC=:yy rO"㨱WװI{LuԅZWasU%Ab岢t[6aY(&|O a9L}p+G>ZfE?.b:xkwxֳcgذ#KHMa <[Nh4֗g{(Y'ce~{v6=3^g ”HN=*wIkׯȞ~Rk(]W.& y䰔VÛXʣt.u2qdy̳rIKX9V&7_g߾"`q)"""" GOO SKW(KVzX9L0yz\rqYɕS'8=UFA+K?8sy|22=}[s=YzFȖ>#Q~07}N=zr,]u52/U[.`W' ?sfZs /up rY٥Kx)/CSC6oy%縴ȫ0y+=l~bFmaeY3j {cffW2==4L˲8<^W $""""; Q=Q=QQ=$ D(g)nPFy/3o| K}j9DDDD>ԣ'+XG]]`XJ #"""'"|JTWWnj(&CQ dt [j=yp͌CDDDDAOD|Rq æQ5}wѥn|^ "qډ@:9Lp X57fffz*LOOd,z@""""=z"""""" z""""""'"""""" z""""""'"""""" z"""""" z""""""'"""""" z"r[cj~@pT>O(ɀy}Y+ڇHš_EJG sA?D8:oZKЗ]6XaʏVLeIFC}|LXI* ̖l+iE+>XCDx<‡ʛx[Ib[#2uZմn"ճ>m[@gsH<@wTyتl1t X0@ԦCp룶Mon_$Z*y|<} B:m=v7~2<׬!bMhm!""'9Ky]CQSn{o9aI6p~Ԃoxg/QjF1ȿ"w$廏ݕ㴿€:ٶXkѶzZW64Njl=n iܟ6Jg/;~z[}=1RKȏM>v4H}HAz4eӿs^Aq(HG^*ƶmqePx6b)$;+Ρ~9P-1am-MYgpqcm {#GJ֞+E>.::"-u*%?5HZn7 ׶02He6-uHVj>| ׹[)C}~jcóK{EfH4>C(2u~4p7P mM/k̾f%IՄ6m p93'"" z"r;vg1+e+GO( d/.QzMˋd%ܮ &24{!nl,pN3IvG.3`ځc`?@eC47 fb[~|>?ݩ;] 8ܸDN{{=Dao3qs6.}]\wm} 쮣VLÁ.Z%!ڛj(_oto% þ ;$g|a@j 5;m^4ȤHz?A@>"i3P1mnx7]{kp6mGt3f ڸc]ĶHU>HsHwX+8'^Іw!bex_ 7M;&b4wP\k77́m{9DWaFgE=U2Jm {KvRS϶y7VҲvK1l`pj1q}hM;[R=NɪloQFM&M;zp547n]gYuz?.; P<gԤ? $`k2D("dW)=\V {za]Y Ɉ7rs73jZwg 핬9p.*Z3d3|>6mS*Ŗ_a_h=T L8Aykp69Zky~zH[{1ɐ6m8f0fיa,=1/ Îedž>M{f2m#<-M$Mԕ-n^{ Up\=씅_#>_w9E6DD䓕&y0= ;6sMk0fc,N&V:Fa@ WvkDFq3<ŽgF c(vXo rs'ӌ6^g2$ IHqeta`Gs$}|]ߠ}^ o-SCtoÅlzrk]'ɶsvX֮ k~ShZwf?om3'iSuxs$ɞ#nm>'uv>"V3dUs`?-=6ʫJ>i=@ i!] wꎕps#;:y@%.0%` /z7JOLzP܇YƝdO2;7CG"smd/f/F,&dt;?^ltgW޾͍psb`+{l%hoYD'Wћ{yc`%ϾZϗNy?F>2B-+hk6@vRTqF[jǼ 8U [_jPLáNBZ4n Жΐ_\N5Lp7fvA8`sPZ>NF׷1N>EOZ 5qڹf+dQ!*?.=ᰛV*0㣻x櫻-Tu\n M4|aH-9J2"5_oµ@~Q9m'-UoWwe9)瑩$cwX$0:>+ʵpkAԲ~8 a;ۚkQXƐc7RH3ّGF" v 8J!6DCo=9\*BںSbhC5gHE7)%շVOIum;`sExҩDD_=-Rf 0?l}̘|pU&`YϟDDDDcw[s5,6XN&}o%cZ6o Q .>/sVsW(aӷXHENA?ŕ<>Đ7 ۧISX6Gi| n.v\h\wVn9<^׺jr1 <=TGN<LwŹŘLdmWd⦘ԑLeCٹ RB˂ӳ{)y돀ɣhm 9\aY.Nx={E {k6(䉈Kb[rޒ5lzb%歶Nw>#=z9X\rʳ6aFg'L=z""""''""""rO=&Q=Q=Q_ӟT(_""""rS~塛&"""""rQ(艈(艈ਜ਼" lqZJDDDDDvp3`a"6q6ZIDDDDfiwjk6f{KUTS]# -_=?Lσ:gl8!ʦ.Z]DDDDAOD>bcT>iy+p`8]V;;wSWa`#ͽ&$Z ;wԷ '"""`Oy >#UDS^ 9;W)5~J pxi8I,gPZCTE{sd] %v%D^4d2,gң<ㇻH5 (l ls؀q2;gǼB͠'"""=y)(q qXǍ2mL:80.xKqخ%CZVDDDA{D>+Q0J"<θaښ;vp=CqZk>=UAf 7y̥MwppAiN|ࢺu' Wjb~jjQEDDDH)h$"tSVBumt V늈(}b,AXKlORtMDDDDDAf GiG&;!*"^V IDAT"""k,˚QS ^DDDDc(艈(艈(<,"<? c_69AkSmdw SG|vћ{ychoY7tO s,ZL{S>ٸ1xmMKol)Ѧc^g%Eqq"""qz{yq'{TScVRSSM@ΰJ|Zk b*:wqO0M:N͛FH-A'@ml&aB~?~@(LtwY.p'b,z~MQZµT+"R_M(_r7u5>_ք XڪtgBν*y_PھwsGb~Y&7c S9Kyl[B&|?^Lr&^?/|oĩor9.Mr-(]~!NqY φ֬ wr7F?%_3Iɳ,ƹ9*e A~g/q8ĖĊI~;1d+9}=-ߥ~M 8ޑmV:m62#=tXCQ*4Ũviу#r^f"3;;t&`0cu6JPdFщhQԷSMeg,Ȗ&\]-}g]p3tE0$Jq;FR&l m&QIJZUDwC.C@/AP- DWj0lX]U9N}0 QN\WGNCεa00y3Su+a?G޺oS&tt-kHߞ9z"[vf,'I \|xzR'p#w𳳰fKuϖ`{$p?3)ٴ+st<&'n<9qC'ct^j`ys~ߪ('TMYvc~vvw`)N[kEhc v^RNJefäQЖ&=n1|*'zR*A((!iwVɁi.>`wG~a&I,ϨXX \œV <'F`Z7N3L:0>OS(o5O{1t!9`y9mX2(4b96?xvƕlxz ,OCJ1bSΧxjMK0x{}$6l 'ﳐxH+y:{$:x6"~faFFly&w6l 1fv|c$bh9Hb#`I36ڍlX6isa,a|v8>Vfq zmҌϭ16o 6m޷yܡni#60ʙ%[(}4\tXҼy](^R~v &0N qx֮ 3\0yK7Iūڃ)r&b6s9- bN29af{Sog/5 lo>Sy= /ly^?ex>uobꬤr,QLk6gY&`FEml$ഃK}`l rO s'Jbbcc+2jnyΌ^F>\ {w[έu($\OKJ\vԪ?ODDD>F9 g5g./X <~٘cd{Rxw9t,j rWf9?e2qaC=;r 'y':@aЏ떀gkhO [#VO(g}mK*\PP^GP=1G O9%tQڀ;W+c8^dchOݬ 8{?rQѺ=n0?j/PJl`s ;# ؊8u=ѤN LX`Y/ޣݰG# .M+=O/]"w wx? ,-ag[ߣ7q=usP59''V~-,͕Olꛏ8ȑ7q%a*D2f?i H^4d2,xޏuclj/@vgׂcjV+|F=&lѣED>C QzjϻMOȧ9tSDDDD4tSDDDDDDAODDDDDDDDDDDDDAODDDDDDDDDDDDDAODDDDDDAODDDDDDDDDDDDDAODDDDDDDDDDDDDAODDDDDDAODDDDDD9?O""""""}M-!rpU&`YϟW㈈'BC7EDDDDDDDDDDDDAODDDDDDDDDDDDDAODDDDDDDDDDDDDDDDDDDAODDDDDDD́MA|/@e8Jœ"w KbRۊ(ȧ46sxІ6063dTO]m6ռ"""" z"R 3`ܾHu F9[j!":0"i0̎ox] љ]gC 2 8=PRRN/@b8?vawؼ ~ W:ƶW2hνQmAix'UETK;C$c{ٻ{;qڛ5SDDD>&y@>"L[k]w0\lbJK [~"p*S]# f2Q;2\ЇlB6ϥN SwSw&va,ƒ$j;[޼@foMl5ioƦo& ҪZ="""" y@F46:`v \n T d$7/a1HGΗquE l:$͉c)l.gXRGDDD~=ݞxOPw!w!`ٰ ;vY4NcH˰R*/n!Z+Б N6jBޙfzz˗/[[|szz43?~{i\O0^w)]MxS _DDDD#,˚QSܻwk/`YϟDDDDc{DDDDDDDDDDDDDAODDDDDDDDDDDDDAODDDDDDDDDDDDDDDDDDDAODDDDDDDVVσs Tn&iŏuW u7z!,-OU@Abhq@hfog=eOs"صDDDD>Uy`8˂4ql_ߏ'XBZi25aަlDߏ?%O):C>B~½iz<6e  wFi$5=XHu?@ & DRWF)!o>=X 7陒sI =25{ ם|#Y2y5+Ե8|,Z꺼֞Vx|MI4a%fG.Z]$N#FAnv={D L!XqbwhݑM]8 V2fx~ǘ\sgwaNJEiN3XY4949N0LX_F`ҎQ/ \$'bND:Ҭ7&a4IaǁyJB 1aL^p4bS%LRKk&0, wMV\q})"""rh7Aߙ#‰l^ V>n+=z.[5Gg:ZdC&k&.;*9}xJc$=њt(u kN1wR㡥&vN{H=o֭ eyfoz}&1fOHK*.c-ˬB^@(Ўl/PՋ$J 73ٕtvX)}fԄyF;J+ΓfB"YJ{WDDD^`ƘBcttۍraݤ~[fTWW]Om> 9ܗM_ٖ:2w%R]WxwAnȗHyG(% K<3e٤Vz7袡SRG4uSD.eme&[DfK(hڌ DHr6'UFM{wꦈM=Q=Q==Q=Q==Q=Q==Q=Q==Q=JrRRHɭn̠>6vSЋQMiPv4RRR\W](%IKI!%%b*[SMNY8c)!?ӳlJz-83d#[IK!%%ܒL(++2&FGJ֑i;3`=ƙZ{1%ut7G.=jh .Ⱥfz9-3( yj+H/.(ՂV.8dS:F ΄d[*&}{ײCt^"sl&*eoܖHafV8i?zN8I\Jt8Qņ `вk/aˁ}:Ħ N?z3oIC{$y{SLZ^DIkUEZVPձi]9l޿8{;mz"""r|H*.&'͊z0CS2{hݓ0bB0đH@2j&.kU나uP9P멦ayrv tBI%.f%LF; v0>?x8Tў\r#-֓2 !:D#7db< N3Zκc. N7^z;vld'@L5/q#9EDDDDka%" pyyYwQ_[ ļ]&{Bd^K=qOAW' ގ $f"Ӊ0;N!#eJíDNn0hͮZQwxxd5I6{:S;_ )~ ;D=:ݪdf{e6ۍ>NP[CU۩ͮc5Sܙ-TWt}+sxCqf !"BG*o&:)]̰dK Rq4\{UKHKD>r"EU!ODDD GO~d,'ԵBXLgKGmS3vz)B=3&8ݵM4qd?. (dݖ)U;2\ʞˁ(5ǰ١9$ u:Qu:{DDDDAOD^߾LGٱn kn'_-%+19N4^*bV3qjkwVR[_[dGwn`Îz\ɫ)ݛO f-+M]ھHr H])J eͺ z.B-U繫Mرn-vOJDDD001 Ƚ166(nۍ0 IMMWN==Q=Q==Q=ȋo{e;MV~;tB#|tcЉk&[[Y^.~ѺjqRwg>ɲO=l/V|JO=N'l1i)=I͸Wkfl;̩_'+xsVYUzo~۸q0r쯉?ͼ}y}8yysdG9uy獳U\?=%"!o~N !v/FpBձ>,oFP|8~#K,z.e^9܇)c1/wϱg?F}m棺/^n 3a&{MB K_o;?ck 6>,[@F5j;99?9XZ}W’x-K"7N y'"" z"Uwi39Q?9Qb3xmLOɃT> )DsIdtԵBs䘯R[IU0.̏zoV i:A?2V%q?9h橜lL0Oփ<:C\ bV>j.y9SΕoBU. B2vRP?Ȱ{!~? kc} )Wx9/4 ya?mm~Bsq?&pADy.0+ ̡`_YSd(ApCԍA<"^Ӷs d X <9HA}A9Gn<>rL=q[V>rl08`l.~y0>Ϟ#zO3OW=Kh~97͹Be5yygU7S|^^bp/S]ݗ9I<`r1`aSݳxv' Se\iKx+'}#7Ng+}<ɳ98;|"j(;\5``bw^?\?F1<B勜FC_%# .LWsGprm='YUb:h`9ny^a 3oppQdy.%0qwP89Q`EOa3]KT/ΫWJg}f3 ~iKFKCyJ~L~҇3_cVbsϹ^0[Ɍf]V@?ۋ%#.F .AޑP }>xTS,<ʥpK4|oOȩ80!gA(8鵉{n/-q~XODOڮzV2# VNvlC02 n۟9m%{|9,adrq Y<IG9Mg:qϣXpTFQwv˃&UF8N`?\=;ď߉00kd'xfܴN~j⅜hbXI?\h"X㗇1'X =fiyow1L>p! Q;6)qo596ggca nam93p ,>Qp:I}8b?o_fa9yan~ܲk4v8K#Oz&OwE,Jd*} c ?-dys| `{0J?Q^᭞ :,`Y:f_90(`!x2T[mV*c'l_ O`瀛@?O?^4?eDc0<|q=mBi^?ų7c E0`Z^{f&9Ós!<~b#Cӌ/~Vf'+F vD%u]d,VM,K~e5|+Wcb+}yuxX^H[Ia@++{i{ʏ1EPq." \? kroUYDoI܇L0y(G-1șXj:\vOxɟ.Bw/ǽK7f~~{|՟(x&~_ 0};=q m}tϲN!vi t]62}>X?o"L24<178f.}4Hڏ^:MMg)ho Y"ˆ5P8/}X4ox/W:HqЗaQ{/uϩ;߬5gϟx͇Vaj{8Ɗ]Bm?>_ A\XNΞ?;FYԂiŏA:{yV,L<&8#)[xw:k@ I.(!<}glԟx|^Kyțy4N2CY8z9`t>.""S܇,lh&<П /7? yjy  ˹Dֳa^s=Yd>sT(vvDeçXOLq߹{Ŷ?r[xgSIlN(m<8JacV0~>0AʓM=@|_:FOjŚC؁VVlØv z[pxwO>cU GWlbӦԎo-dz4-{%GΞj۞*]m'3ͻ-l=qce0.=VzN16t [I6))iq HIɡH%ٞJn2&1js~u%\WI˙:x*~f?Pה7-!֜طClPMl; )?K|>o[zM;mSf#< {} `"5/IWl4t ׉ ND(Hͱ$֗e[V^b]馮7*XMIT@6Quߖ~g0vo+Wg9XmU(Af^  h1OmZ6tVӔK^yDH,.%/`yٞlagP[ =ĭYC/eW:VNCH%۞f< ;wzBoaG};1{ٿ/4I-Pa' OVwP\)ml?FyL颫vz97m08OmE%Z۪^ :2U Dm&/rm'`y&'vYg]|øZio5;RYF` NhO.8g%2>;8Պ$&&BHeD !2tJޟ;#rVO͍ #CLyT>OK s|1VLLb1VdvV DI4l8ACM;׷LbHQ{Z{hCg.zZlǤ{173 BLL(r^ @2٤[!y_ ^cvf^e_aj"4+FJNnȏ!6xl yl=3qQCKRS|M6z>xN}=YfޫΙx8OKρ p )WIagyvה׾)AO'FLqH6:{5^ %)=ݻ3UD}K.OlVNb6ƹC_)K%=ݙmz+/yݰvO ȷngb'^7azt"sJܺ*vjj{KsyfW^=`X"4fe+F& SV" ͑&z``occ1cų..DDDnWeJeہx?E`zZtJ?8Ǎ{I5ifL„S2PXRɌݸ%s]Q9l񄤸"ܧWIBhz1|!g4޲ X1+=4S_8F[{K mgx$6Cdž(7r?x}/&NH:w._z+Fh<˛GxY^><^}|1oz_={:G棺Oỵu\u~o{7 6mN.\%z|߸;C\%6#זͤT N m0E&?< b MFb 8`bE _dJ(?k79>{PCsީ~DGZ=o(22{=Su2l</d"c2$~~3YkC`T˽)8̄92fDE乔X<đ[^rBYOT Dy~&XIDAT< viwj/Qa8߲GW>C7YHfK\2B|<\J5Sܝc×>L;-l||l%3=w{d$Ӆc#uP;ADDDnIS7EQf`<v0)`\Kbx=_&)W Oi1L^%:`QO&2 G̣zM-I &󍗡YwY98u,A+^f,o0o&0?%o[;E[0р stSZg]6Ld]ߖ+c ,OabYbt:?IA`č}FQu<ͱT<; \u k;8t k=e=f` !86c׃%p+뫉8b?o_faS$Ɖf;-Fc( |0QJgwE,Jd*} c ?-dys| `lM;"!S? z t}˥Gh 8r&5p~-u~.Bw/ǽK7f~~{|ŗ__$c|V/K9> m}tϲ4.qL,=VxڿE_3fO!;o‚C7酇l? sJ}4o~vqjHǹw,lh&<П /7? yjy  ˹Dֳa^s=Yd>sT(vvDOL^LF|~slRזwNQ5h}Cv?yW!0>A`nGvvq\Aww7 iꦈ(艈(艈Gߣ'wl'Ask0\giv>CDD>߱mG!O17emQˆ _ 3VY&BDD>701 Ƚ166(nۍ0 IMMWN==Q=Q==Q=Q==Q=Q==F;IENDB`././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/doc/source/user/figures/delete_application.png0000664000175000017500000022657400000000000024164 0ustar00zuulzuul00000000000000PNG  IHDRcsBITOtEXtSoftwareShutterc IDATx{\?fUw岠PC,,R4/%=/'J|3"@PE@`Zj{u f7[ _Vz BA~?)àtp˵ҫCUNvJݰW$^G$懲eYrۓ4))bĭ)ki)Y-R#,x KJdf[gώ" v.Dvy띈DF &"͂ Z_YWoYHx(3.*!"R X2f@KJ$yw`6migbjFPXVnO6"Z$Tzo6[?֫m HNSNmݺm۶|y󜝝{CƌHXP$Rjb.H 5]͘9e˪J|WV|s3&<5wEtCnZ3%"'0tJԠ?<0) Q"%bH,ed^ՍD"XLbq]Ñ/n$A+)9/1m.vwkl컭[QddӮ]rrr=:hР_wW^իSSSoHJ7ӧOԩӧlٲo߾cJR"_ҡCǏر###W^y.Ϲmj,"r[}Ǻ<x=)E$$sީ9v9,kW"LX[}˗]_:tl9'ց'"crF"Ozk!wE,mə ٲa[X>E3JbhXN6qܸuhj_xa-=CS\we&-\4KۂІƶ8&e'̉NYey}^ڄyQ=>_tI oYVTF_)+6l$KIɓD/ˉh999ǎKJ Knݺt:VT*=z)N>]jC sΝDd7l6"I5[" '5A.85)ID~QcA~*XI tm=mDtB`UnzV!wjƸ8]#+'f/8I8yir D: <::%3Cjs 91|چ=7kY| &$g _XT@50=aNZk⢇iufCDDGQnXC亥!jVXTr' F>Z<=&!1=#b4SF2D\؄sg}FG6qMХ-I.^,?Ɛ8'ADka/[>է͘$Q2e|Fy5M\}xcNu󵔿y^dšMsc7STʗ!rg&[3+sΒ )I%KVo%<.?csvT'/4ˏrWYi0LH3eXؐCƜlպ)2d.9?h>s;,aӕ:(gYRV!X}#mn֠$ܶ[/\7߯y/xHGMOԾ '.πI v&k5.9nw(aRG^߱cΝ;w|9ƢX@,wεs6۴#hh/"F^V$?״#Drnl2K/}D?ZMSC `r3x""O ~;Ա7˕pD\޾uh9(X"R2 xу0=)mp\3ξk.g_f!,= S X2:%.7= !"FܧGڰ`-CMݶ}9_߰0Mz"b"+48Z}CA\aмh"bTbH4@QS}G}APt n<6sR$Gᜑ/_tT>sg/ "f2ez98n6yaX"R{{TO0Ql"Dk"78s/ȡ 0:-ؗi¢#UKbcKBfQk<jQĒ%u|;l؀0MAz/*`X"҄L 5Y 7˕2Qfh#-]*xfbjҎ`4R)vAD66UVX$٩ㄋ{# ե __ߑ#Gj%FV SYD$u6P(8G {@ POI҇l>/qJ>ĵ)y+&/(p<+-e hy=/Sn*ְLLcغ=aU Sogo/hp=/ܕfئaګ hXYAW"buw ^׬.ĪT8=ǩ Ư60X҄L H^iR.yu)ߜA^O*L䗓jK%O> ֛mxZVL%a7,xFSu%6`e4oBGLJ #-%n({_ѻMG:]-XZh4Tjթ BDd68ڠVZGå^oMb( o@8FM,빚Q8BDZP...gU WeTi3՜ *+yeY-K?M 927/xg'n2$PV̻ف+89!q߀Ī2)E+].c7XBu{+6> Pڞ^ޗ.+Pշ2\2.֎+GbB_T|8bK&?4֓!_Yh4}s5q.1qjXVݚs:NRDoHya@ɆV%O63qus}Rxћ֯op9,:S!gZP#}֦:АVPAcxݬXܕ˘ \X{YD"EI B=j󍢫$"F,qb1on$TDE xxj|2ն&]t\]]K9~bFEۄz1j͟#"qZ,.,`%n)!F|y Ue`^sWPson,?uwXUհ{艈Od|z""^a.w`|Ys) ԇ!"*LO版IMPo}ZzOD.#eȐT=?f*}ɚ6"e_.[`"g%'esDl@!9'MB'".?}snó6ۜ㉈KK{>ijgF'/LHw i rSf|B)`~ c_$cfIxcQ%i:muG5 Ʈ45+ȐyVɽւYԨ`~9:x}޾ą "g6z؀YKϿx[e En&A"o\7_U;}ȏJ|ZzGWݻDK.FZo>"0`@<7<999FM6۷'5kL6l60ٻn/˴.d2\ND"|gϊ]rǫgW-eοw-#%MRۖ*X_&Yp1_& ~єIR"*:$.i$/4b3tjb⼗X%/sD: bit K:eMxuPԒ 5:|I>!vˎ]2׏1KL* ^EO7#nMȺ;am NMhTxZdRrn@tĤ/sDxGhb"C'V Ripa.l <$\Kdlb]S f$վO)bYJxsrFE&04}W0DMhF̋]B:읓Շۺ\'" ܄k 3՗! /LLrc1|އqȔaj"z`fF*o%j"(ladN܂q/ja>w5UlPLʲ5Is־O)|IJD`f5^7D^j v۷: &zۓH\*٥R[MnfYy\Ez3#cĂ+S31B~~MHV ooSd+V(JԦM7nDHwww"yڵkry6mFSO^3f UUU-_jq{ ˗ʴm7+یnV$DUG-^HDb'|[,5dX.]u\)~]s{w]_M[n$]Km;mf)3waDz<܄q-wmx0 +b2x0B ?,}7 {_xOmJvƍ7JӍDd$&ry5$$hp׸T#~T___}޽6'{F3fjuXXc믿~ر3g,;hР;Txep?CGqN8q5nI/M좐ufcŚZQvsc_r %Y*i6:=ٗ,RpR\c 99_7bV$>7qrafa>?+]~轶)LK/]0aՙSDXD"vkd۬ěf# /Qb#qr*N01ZSK윅"]B mJ>>>SL;\.?=aɚ;¢Q($C·^e8O>T*=X b/\o)鉳/Gմ)/Yt1DlIg/m4m^o`-H{ΓL?/q#"F3pvZsWܚ;)޻ ÐyO;2KqI=  Oʢ9Or_EG}(rܩKo߯Sj梅<Ǭ>±jIO:1}{s;Rιsb%sT>_]jk|׎Q,?|y<#IƜ)yyiIlJWNf"tyL6*6^6>Վo^WH:>F8܇.nV0Jعsg|KۘݸF;c*s>s/&kM!_pJVQ ^~b;oV$}*[. m oh4ZUOO>\:JgK6Z'5{ӹL'oZՅ#)~Sr  {Wo8j%enό:aЖ;UxT| UsOρz~W\3axwK m<%ulL}b^{c.o~88fO&WF$o6NKWdVȪ'n\9Vs+o}tw1N2ٯrݦ-J=o;M߸{ۓ^ЙK_Jdz/Slz7H*F<$2* oٕϛ=_JXԓN޲9Ƥw_]"~U]ܸ:(e?.O_sDLo5~|S:t))__f&Hw2 Y*8iS,O1w,ܘ3WV\C2w[S͓ :볡]WN:n$~W=<'eJ|.Z__=3yW))ͩ"RgIOly۷BY&nJח9z~Լnz*HOz~$_"Ҿ4=6<=zzȂ'D9"q@R*4o.\)'"~4sDl#fKJ"w2wժou"؁DDVrzZM=[վϿKqKA{7w xGlOuOmnCo:Cϻ5_{PDv+{F9m /'Nç[}Dتsڗ^b6k27S""9_g\臛vK5➫gɌ:Ye]ToQbyɈ+(:ߴiJسռVB$փHURit<$RthCNDҎ.vʶ]D}Ը\Vf?O{>HZAszsQxkL8Bs"#ÁNȜA;ױ3:o&"ӉN𾃴wV_K=RND.t9gu7\?u&*I~WDdwk>;9mtOKfK=o&"o7sX霟~'}ysH|Ԓ=D⟶.\zh3ZNd] vk/dnjjr~t$@1Q#z*YD{?>5vEc[߾Mۗ/rʯϴir.CՕ7(ݸE sklJˡ47=lJr*W'z>`Oӑo/T>7JOg`O)ɜn^Zwjq"s|uw&6j*刜+svs""UԯRa".Rf1:vӧǩ5%ò#[Q*.fĂN ~ЮNM?Kuz̈́W:]&b|ǑOz!;~DB㟏I3J}iѫk<׏=nԲŵ wz%qQq|<fr:X>MٱRvL'>ui䵝V8zRQ8)i'Dޜ3eFs FG/\Y;iD?~i g'6_nE g\ND宅O_41?>TV;d͉;QkI{m)ɺh7557?ս͕KuwϏ0WvVrGVGW?OYS CF? Avf<<_TTԯ_͛7-iݩS[nYh"l_4Z>>kT9{xOUQ֧6!ӢC:șyD&*{O'h<|,tDvȂۺ N-}+U9I~ZO;x4czꉘ[̄wPQHJHJHJHJ KeMxdo0HJ$Hr9ZEbSH) $HP)$=p`?JARzgQ @`m6fyl6X5@;$%$%$%$%$%$%$%$%$%$%E;\+iÍ[QDj;8uTI fX.oR$jطﻮ^qwDGGGG/]_OEG 4m B$ED+"J% lffTzbIUR2=[iӦ3r9ʡu3rStk)=zHAg=\4rL'Y>dGnU?Z[#|4s)U>-ĺ'uY#y5Pm)zS_4FISqv-Dj,U 4шNwH*\+k.J<(:j$A ԒᢚV%Orˤ=FL^pXdR"}Ud<~ٮ#=FSi; 4J̼2dgܬܴ*֘^ íߋ5KS.EJShiQRի8_GF7 ԫLS=3rPow)2eQzWU8VTZZEW;fdowj=p8D9H6/IJ'4gL7F?&%7j2ՙpӱO/~P9Q٫&%njfL:1jS9+eD$S+'%ZIDR'99ɈR""ε(DDNNDD8J:9IeX^ߘܫ ䷋EY/^IE$I7|-(zҙh*5Yb*2l$}X:pdIL^,ss*x 3. !IJUZI;/w/cgѫw7**;cOV{ֲ-aoMz|iK>o, ?0p-ѡ`Y+){nՒ{U]4zaLS/%3v;WGɠLlF)!nl6y\XXؿ~ϭ:mǶhS*d̨/S 梃>Z/y""ֳW :fKD ^oea牨_l{=m˵mG}y1=Dd> s*8^F ?le/5BG?8)4nO]z]t5ݚ5~W8weս6zecxsNdV柕ԧx[0)kMug O~>C+?sΡ:e"ж_LJ_t4&=oJ/zx!󒧸R'.2ˮ 0Փen2󖷯~yQ L UWWgmuw1v\q|ʳ<#\J?X[Aׄ>"sRwGKQ4mRA.3&uا‰LKQ)U,$${.K ?ɂ\ϸ"79I㩛!sY *$ =CɰՂi.}tEv^2≘.,mŨ DW_Em-zGWF;y a[&]'Ƚfuo\_U<:G}C& )Æ-v[q sk&z n;in&( -cXH: TvGIcUIA^f ""ݿ.vIS7NUY+)Y쳺= #R8~Y}wo);i*ľ J+3/ډ򪹒 J8rQ _jW"+vJJ$.ѐuU .}Wq^Bޜb!"#Rj'MzwG}krt/NugO:e>e"2]{Qk1I"ඓ>}%JRv K+JDejS.4i334hoi9%bnDD2K?V\.cyEvygT8~=2I;~aJS'ű/Bu'{mIbՐE;WK=I ^;7!x35QiB9XT2Y]PD@ _TJ(%2 Ҩ7*PF?9~R.k@K=*_5NwD#H!eb-~NؘǕ0[ǖSo|a9u>td:1okWҝތSPkKAvTj~ڹL׮bc񗥎y~zEXYW gW=9B–!*N/*_/"iYsF۩Y]*R.ݵ\3I L5'[We cxX~*jE{?9=׈d<dsɹ4o力_uHYx`Z͛g8<cA;Ci߫!1'}xf{,>dzsC3}:fԻ%37ox,<  < {Q>>?+-JcW(\Utnn=t~u j %~s8/S>a^ɘ ck>)۰ޔZ Tm6uխ\G;hI I I I I I I I NJ3km*6WoGG/Zs8"om:@kNJ=;2U9kGM4m0}/YuԯN^}ɯRw`&'Q,gkYUȤ^E;2ϚIꬡRNYhE,ڱ>U6*zL{u[hUi˙ƉUSD/鎃Y,2gYsVۦd~Ys@I'sMNYzr}B$Q Ci#z8U2?4jGN""ioq oG"kUDTUt홾I>']ݽ*KYu):L#gO܆c׶M=QF5thNGt#cڳgAz [Id,239{i;;nYm?{ԝ $HDU"ŠP-J[8gܶhgnj8w*vf;#vLgUغ@^ m%5B& |!A8I.XZP_χ!s?|.gvC3Q\-nAig']6]ɛ|=&]JpGwK=D$y(TD'Dyo~7 AA<" <"·%HV655d͢`۴`f*~u-y6(_ =D6^ QD< ~Vo>4g &FG-]zAWYdYwli3T@+<"j?~x{@(hէHZ,,-YHJY&|MʪDU;Gy1ADnQ/<>’DQOFDl' Xx_ 5?18奄:3t/|pa ofX )Nm'.Wm++BWSpv侨||,4D'~ϱs`*f8I%4tX2A#"+eg;|$<<7{l;\OɗkS˳XMOݦoLBҡ3GsvR>;f5̧-KB߾`w=z)͗Dr/7.?Ns<"ȣΡxI47aM|_оSۥS_PϜ1}Gjt&r_l5/Џ3ᦘ,GswI( K۬?q6s#,%zngD#"p=-c]}/sO|\{72_9M#"#"7on%ݚc'>ۉ|M^3?&[wFϭCX2)~qՏG[{#dQ%'Dĵsnܝg NvfOΑ sݝnz;D'ǚ'-?Iеt1DNWO%!&5O,`|S٣gu}~ɳ$a߲}Lh3ͧ*|xM}\{ی9΋m7!6Cً#=f.$jgO9D4#F.(t~vko<?eVW^P򏖈yu+Ën & z[g^yс>n՟u,KDD=/Jaf]}U򧟳UwESc?tw7tQºDHKsjwOy}^R#&@c?,/tO)Cx%PaF{1ͻ ՇZDZ .,=zЗZ佝I/UsK^nS^1ߜ=MW<&a戈IƤ/g>3\OWT_9;u/שvV_~yMv7Rzc oƘ3) 7t_lF&8)9NL>VlsI)/`{M*o>x+R>_EE1ꝷIY0eڷ.2?ηS*yd. XM4ho"U/'#K :[ vܝ^SD }Tw1P{GD>A(jꈾv_v9}z?[v$b_LzdgԸ?ul&/qۊ ]:o*>xl䂹S_M%8'9"BLT׸p_kz,ԛ5y_WʿJO3v(BIs$G].F:w*$h;^/2v,]vgVuMKB^ O)zj6rR?BZG-U!KK^폏).Qp7j:.ZlkKW^?"Jf>kտxDOYݵyײUR2pXw˗旷x%evKuuݻyU(o3;R+kDEmWj:f>!XZ̉EDwS?ܿ?pPCgU?xuoZǎvׯT V-\8nm%""$Z7g*aBCV8Qw+Y2?;3'(k^8$"o=.Ÿ;vKs yO$LZ[DGg&ߟn.Nse3" ~c!AF vxW|Ozr_ Cz(~Uǜxf^/=+~x* N[js;O>^g+I} aBr_4O{gf }<9<~UgWN7M`uRzs>NqWt  [Tyν$V|̛'|yسcH^' :y߱1#|ɗl= HY4"`@@<^_gRm?~:j`{ @p]CGD|?X@MC{zܼқ(@ x`oRek0_It#%%[NL%as=*>CD}Wڹ+mb}}ڜAYP?rpn'CDݽP(N.AOpbe_T6wkҠk]7^['?}'\sD]{t$K }U 5ο͟C:y֚/ ?{a?W]tyJpW|e,|o\|T՛f ( |yxϗ4]&"+f__7M&]>uf/CL."r}Sy|WD_\Dxd[._3{|w:|ED~3gN°H✋y/Yi7魭n" zOFI}ۅ{Z؀7Oys#֓D}z64L8g S3nm[ϴn-1Uboᜐmeg]R^cylli}D4$ivÇOU[ۉsCY]~d/ >%-r]&/UR?"/\,zH~jQ>x}Gp K ~+7/x|rMpFɛ/_KDO%GDpyxuuN%G+,yW.>y3<"C_MrD_<||Ǜ:] bjnIosV)-ӳg~/̃_xm$.U _["?;X^s#gqJch '"q*{g<ۚ}Y6w~Ь/[yl:s:kJ}$bXUO޷1&{iߣ<%?O馻CKn>JEMݪ%ι }I&W]8fH;Z!4xwޒ.Xr_5^_ptB?ֻV sqR4Fm3g~_N^|H^Ywi=e%'jgǥ|0om p }]N:RcR+:ܪrJL+W9NΝ;bn5n_Bpf̘܌rYDD܁BCCQ7L$D"0I I I I I I I I I I vÛ/ڎmKD<$= `_:ɧOC+%3fH0)>*þO/ b~ᘹ<ϷOL+G}fAQmcR}J=U$kH(hͪOw:.P}UG[D<#s+opW33;D۠ۍu*]^"QkY }opJb"Z>|K ?&ӧj;&jq`F>/2~ʧwPJB/*9pIy8NI>:]Q|>w,r~VX?PC=퍇66<>DCHFN8pw)D1v;fXmN]fp&%H@dhqw?wm9 =QkxGADDPML]ޱ}oYwɌ.Kd>tk`Gg];_~Tx?nv-Ju^}>j&wxTOM$ DdWہTڜ? : )-X&1fķ҉8J"puvCډuKp>ڏ7;DD[D=mm=]|C1AjzCO$ĄҁODN8ᾁ>9/^ ~0.>׿dҜ%K%inl2m|ɢǞy+ptJ7|t>C_5776Zk}chn:NibOwQ_,xxgEOdrs){>߲\5g,L9/ntuujy£8y| IDATvʕ~OFr:ǝ;w.66%S.۰JXuxbBNJ>c~7OJ]ne܁0-,"StZ֎^LoPPPHH0HJFkQ !__hh(˲(,.__w}7!L 㞞戈ijzbBd Px%*HhwiQyۭhQ={6˲( 1>>>ϟkܔpAAA:::P;Doo?b|<+[X9`Z;wF}Ǩ +Wඃ>%{+cbbbbbV7~/3bbbT&Ͽ1111点eLL2aJƙٛԫb(Uɛr jI `0U=U'0 T11"śVݔWѷ؜DP9,F}M Tf<Y+cbԥf5wIw B.ITYf {>Un#z xV=}ϫs7UZH5-I)g-S:wdc( pO Nv}FNNRˈ2u*΃eԙ?^"-/X=(9;b"G<-MEotnK7mmp eR1˙ mu{s"#b6{C>,AFNW=V l2pmqݬkv5{R0 -B(^555|JBbyRRF &:!Q&drsoyjU!&b5zvkЍ|-92MS̏M2RYqwv홊 -j 0\}ƪҽUR_pߝzL.nP9X0g$X_ w1HDI"xF_g)mL0CDRwRS3n {?_$*ߣlL̯N-MI2^ch!ՃMbLENEV. ;ʁDDd(YI'wZ&3iMDD$ZV T%9Oˮ-J,Cl'U6iۄ s.@ ؐ`*im-/  9ԴU$&Kk,ڶ$aJW=OTf毮QbVShJ[9pI k7VꊵVZs[(:}oAsT ƖYB]⮪ˠdTE_1R"ErLP3PXBzvd0؈{IMe|`ڴ(b,E’zk4;gm[w|qu2}ƬTKp7XKl^ܛE[5V1O"Y)$i.79W(D(&"}~&YZ6T9Zͮ+e NJtX(&}nǎ]9#zD0"2'3TQl ;cDN]ne*JFDi-R 3j]I$N1 ˆP1ԌH94VyB9M&fxQ\jR8+f""XVa!"EV.Rdd2DM~Y0NZZ!$"L5E:'lknꈗ2sDUK5]9FDyBlF#) [%GLƲ&6)N,#}Iycٵڂ U\LLLLM?VĨKsW{VWg6LTGƊTjqMզk}qf2JufJD>[v9u[Vesd+F=`\cEGqMԘ䊁73ggȯuc۬a""" ` "DmPQPXѷ8[`#"˯>X"&8VJDc%"kS8$g;(1&p Ɋq 2QDd3و :_B$B28&;cL&6-̆1vDd19%6O_@%;Vom0lY[„E*J2NmXS'X1ia\f']KWoM/쬩1Ey1I,!"VXLoW]윾Ŀ'""iT^78 $*WPԳDP\W1H̰DH,DVM~MwW%"IrjKQ!)Wl(8,BD$Uo(8YSq%REDmm,Z$mo)mH3k*#QzJ߬)6Rs%D$XbȽr1T]]s1srrp7ߙzt4 /rED3DD<Kx  P01X$".gc򨶳FDd(֘Uɒݫ'8#ZW zU suM%(b!CD\}u, ONꋞ$!a4z(O]]"4O0uy9@|"i*,N$%q%lXjL3ztAUW1WΫn0Yl 1ǍZQaԚM|HӠ5q hr4N6+4A).)Ik"&vD?*+UʨA |A)SM585F&v>&Mfٶ*fVܩT*"pSruoDn""r9=oTp fM7[q`RVaJ9n4G-k]24oEMoPJ pDDfGM6)yVk#gĉ_•23FoU]cnLST ؑ 9zш7Ybsv)eb!C-%7NrmY32;#G(L8Ez[VĐcx,'3bF! Ӕ[V;**Z[ur u{gޜM답V[82,!&Uw$I +9m7֙dqm9C:bֆ"Fɯ5J2Rlە#:ڌUt:S<Fj" #Ţ՚2F=34E%Ⱦ{($!}]Jl轹*z\Qi#abD}l r&g}MQc+GNΤ58HQR!,quVkV}r夞N"WR5y ʿa Z]YS]}uh໑eaD4FrεDQS ,ԏ"#B'pf7ThԚmT*NfDbQ©<}E>c3jMb3iD0h$)ۋ m[DyXe]ڸNy;PT0?3:}##Èf+浢L`r0bv}1juDTȡ-ӏj4*ʆa+=;RnRW=ؘ]_T'"2H(&r՚:" WD,zӈFF|g}xH!ðĝMRuZ+L܈r*8iHXy)2"iO.^3+rs'd eZ/,BĦ߼84Eõ͙jvW؈(Z+|jj8jG,cfޭ'0u$e&2D-#אoڲ}ƌ2$/`6-Φ߹{wQkqfMr)?F-9jss*&T_3eZpFXq0WXyj;646s,a 2HNmj57g‰lzα!2ju&+(3-9zjn/N^vSDIC_h6yٚ,;dwfW2&LU [4:Gt[ˁBV﩯0qQ,@S"~\#ZQE||T'"D*W>>\`-%u1_UBMcxVU+bĬT3:Ef֘&I εb"֨ӷ8Y;-gJ1l,1jF%dm48(:=7IJD<9EVhܝ)buFbޭm - {\BZJ"EhRz#㪆D;}qR1q:d)sFe+޶[g3jvo&bB\0B^0B"eήu[*%WiQa,g(ϰuvg%c6Fʥ,glD8ƤIY<]'W*":Ql-Ht-d\m5ƒjdў"f'Upҵ8j67$ؓR۰H rڌzI$Nܑ„qnyR-۝WSژ&"Rl͌8+v5LoYo9IuOθѤ{yE;78G'(R$g3 vnxHnkn´a[͖dۮuZ|mj;9ސnZo#"Fܺ'+Y69E7Tqp(nG.q^AJeod^F!)-uu鏛_]=aU5RUF*:b8W[r[_;rxYH\j~\%斧Nrl(ef2sBz81=7܍x)aM}rd;^VT^_B7CsgڢsW_0-ӘvC,aM]ݖ^^:w>SNnGNBL}qǺUٓ]n6%]7ڥ 4i{bw!^xbwv?~׸/=;6V:HE6Y(BakN4Nx{SpbUwIfLT;L(z{{}}}on'Nzzz-Z fsދ^^^}JpKuttL BL!!!l64frp͞4zzzz:::"""P;0VEDD0 swwp8N1UHHȭhQ٭vة=)0 IDATD"H¼ IDDbź(&5?1'sg 39s.<;{`xάgM8+WxyyZ fADaoMIXBTasd̟zIye-DPJmڭٖWNAVBRV8Gmtہ˭C9y3yΜQܳOgOԅgk,9ߞ~<yz\sO8ٳ8`p%>(1gٸ;Yaw_%O.fX@xϦLJ_LܕϚ?^|M m9w>zkhd(3HkAESW1+(^>x`;}:w#ƻN#w96q?x7X/Ysa/h]s;ayJy .]EYgI՚t#0bz ) &gf ^%bR/""r؁sk[>Dc칹%䟽2{xZXi˴ժez',6|*@,h9w)CZ^(`۞y9}$ oby"dT<-lyѥ3g_,܍MW ;7eӮ,Un-ܮs0!M2'wG϶OX^8apoQk nO/Ol͒mWڝD9s`J/ت`ȢٹaUL2.f-DDmQETSBTq^޲8j˼M}~̋zf琼o.$"kBw0D}7t~>$.n~𒓮066Ŷ=5 MX.e%Jnξu|||D"P(DuFՁX4yOToq8q<>9IXPqݓ6,Y^n'˗/xs~~~"ՁTP#wvu'2ax+,bmp\ՁAu:PcHYٖJHJU__WP7]|ՁTP#w|u K{*TQ[7=u~Ç@:P@1 veV ܴ[[%fSN"]; 󔾛aaa+^9I?eu~zEyKDx^yOx@,IrNwqQ ˵\ r&x(hXʷ ԾyR+귔-ʰ cewgwf~_SK<ݙϰ|x3`"0x\e]Ml@ neK6-e9sVv@}[rt8N a'NL"#܄8gVpH&ݹ-Qr!$I$H/M8h4h:XT^v>3f̘G:"O%y>T T<3€ {)@H$HԘ'Ք D\t@E}/њc w{Q"56<{{KSge}뵞/w0ӾƗ$i/ң#.7ɓnظi$I"kz2@ ɸyb۶]4ui;rlRfRѕn/]W[ijh qa<AFttt̚9usrrnĈ`?Z񫯾jZ$I+<Νvo74/?*N c2GG 4"A&T ait oİ<9^Mg*QqwedۊhhϞ_̛w㗣|jM +K260"࿗5|32}3aw>ܬzo7 eRm~u/Kt%j槮}RI-xz%5cGXJGQڏjMy .Wbpe)ykߵP?SB 6XyH42!18 Џ/7V! ٟ %')p>6BqX,H$MW@#N#FTMq`,58&)&8TyXt[+ n~D&t;vX1gi;go3}SsAϝӞzkkNiOHTW`h8t\,WTx>>|>16oXmXŗGcyY&r^K: S:}[#`8V, EQz}B!q"IB)8koo B!-iR޻4M<ޮqLP #@[`A"]q9uA8fÈ&nƨ"'-@ky(5I6O 1 'p'qq xlߞe]0cE] kkkdzh);3֟;yL1cҗ mi3o2M`۵:="!v# M%k7WNRaȠ_ah LLW8@<5 & $ѝI)BA !!0xe'YY{ ǚ5jW炞q0 qIVo:N[ /gSͦs.z⑽ۃ]OyV]Sme gDGX| G=ah:Syuf^͈?h]|}<Ͽ6oǭ`h P(Y_ݼiWZMS/N7v,RB vIȔ"yD`P_P p fp36Amm8 #d$Z!rJ*(ᦛZ9M&SqqqONDڏ {8G%xcBU^ov nhMȶ7AmCkodOzw wBP'߅\4>8XP;8>$xKnZ,᤿x}) ĉ ~oЩBB]>˳`,\1qXϟɷEJL~ "sK8#6tBM[ܘ@%z"B(C28$z߰ϿOOR1:IE 4.<7F7",4j۫LBUH'ګlU4j HQj)l{3NB#{11౫팭qY_W~'[>x9 ,ING@>oܮ 7~mz'2T9 4l=祂jmi0ʯ`r#ߠj*ױ]h\̚$:uah{1qͶp|OO՗_n7۷rE嘤W^~Y 8{{y}ŗÆţH)!l ^J k873p[ƒM(DTQGI'8K%'-zCQ\c/e=cøSSK}AO?{ HE'/I g݀GL6Kؗ3jL CnBm&_Ryxyy@gV yS:; tQB2v8}۾]+g4SBJ '0 ''xn^A3{+׹;yqrU셇S+? yG(nh{`$&>#d;(0U5>NJXLM8Ah~ QyzTWUv9Ҫ*g͚YpЪqnK7.ջf[l*e9hPoϖ͈ܶ[i|g[[[R qhPcΔ.N)̴_)ذsu[m¢}_{yV0jHS ~F㘾 PGwtVkVW KrIsJ mW{q4 ؕ E(\c6;z,{x[oXL- 2.Ցv.\\23z}Uk9d}9_xaXdۛ7O{H>d`$ߐt\]cGxzp<ޡ0c_43E}yalUu&QyZȰrQ'8.L~*tߥاhm[l'3fi#X8~UG[a܇pHNMX߱δs]zZ{ZD~N9X';'Yqo϶Mܝ8\2;2bn׻b'g t6Ix-[6Ϟ5só^|b1:a } xVKBB,zYӕAVITMnغ9GQ88w;BgOѷdhBgc,bCGY q&2Iurl~SGijE={C:99}}y%$pDnX棟qR(-V+.JWZy1 LJ H 偃q MT|1`l`c smb!`b!ݭGDrr Sx<Ɲ./[@x=e/!YyC!Ӭ~ϛ*^Oy6uݑf\G@ J$JslsEkH,״ѺNW2sv9*:qPEbq}1vW5 GI]RL&/[,;qJO+$=-'Ƒof\2M}β˭^S⑥oe9aQӛZ}U%bVnxqIVGD[iYũsm6;c$3[x+"w oaBwyoSV7fku ?W? '?yjzސfqvרI/w?E`ĒK \!xp_)9/!{I1ral-[p. e ]:W/ (5~#hTJ̱diW\`I-TTQa% 's8fJ hq*;n{޾scT͊>cH Drc,l4XHL4g5:AXSCM l%BYawyN*X"!p nK X _Gs]ܝ0  1-n[tV큍6K_t O7. ь[ whk\{%@Ma7y }.V\ʣ*VRVQ}b_Oi:7`6#&s/d? c4Xkvk')Rʏ}0p$AIl:xWh0wj^F= |~}ce_e_ܽJ.0Nz,z#RBz/_]c_}yS"IH&":32`+CBdy͢fȐ'* s:X Ӧ{[g9`cBmhٙ֏nf%q;8l8JK@ iT;Yyyw\|T?fcl4[\ )RH@ WƎPb6g=&¹V' 2;t$HRPfb6 gYӄ.g^fg"#LWcF jx8lV(d0s.>IL‘<5yH#Tۊ$:er|e9 W: -/B^ٚ_(< ~zn`pyQ55,-ZFF3wFAJ q' ~GD-үsEץjP&sx l+(lE;;{w_(cOǷ.x(F Dѡj FX,vsuKѾqxnt4 ؘ3D2aDr%#GÎM-ZWJeO5Zƈ k(c,~<^'D>B/ ̥ZmU*`@aC_GQnUhk>\\8^qrUxMsglaRh1WR>cmSo?l\cX>eYI"V$^>q>Gv@J qg1ן8xQmǤǾx7<.x#, huaH/0zY8UWzWDI#3#KuJB瀁Ow0wD|js֎eo@7bP^nmkǸWeeS [vHS@Oߖy[6H=8$nx0ϞRQQqrZcXC}`P(MMM rj6C*26dnkoW(61е+,twwJfEVTV*Q|_b,Yí z&fʧjO۶.ڵ]R%:طÉ'par㥹@%?z2X9ԊpRc18 dJc[FN-W8rM͗O&LPBwpǷ;J?Rㄵ( !@U}$ogxx􉩿xWt-F3RJ߀ |!=gD3#~(H|E32~Sev%k=歷|bF$ {ry`ȑ;v.g瀨޻ӡx^!/[,**J'#|+DEF+@ks7hniqww{{ɿ xŕ|p `pl( IRXp̙;,GI$RoUM~jWz'gettA:;;kj n6Vv6On2Slq9^袪_gGߤjOhqe,Jog; v 0veu 9 ֪nq2'iՌŎ }qֻorDc,Mwx7xuZbVǫg9V5W=<{AFq,pxw!hqaomhL qɤOYs]Uw@atmm رcF#??a&O>1rH'_/--2dbnNhZ5ӧ9{7CJ$@߾]iN~P'7.|?Jˆ{x(<@0K3t~RWEaAb>|󇿮):4pBפmho8;x=%t kjk25 H(CH&~Kv3^!Bq2 \u%}B׎ sܹ6W>"A{apl,py 0+WLL:lry9;sqcA2Ʋ8I<v$NNP8g/0nfCVM󟤶ǔn#KOß~<|djR*bd^>IY+terh{pRJP&ϰ!w^Pr^zD*AHqN׫q|>xꩧIHK6x&Ko9[ɹXW6<8EqEg1 hM_906{1H@ *6聦oXBJ qʤ_c]>x`6w>4jT@@ӳFvg[8:G;vOBT9V\\'DDD,\Y*G흖f4={Ri`` wwCr?P"hl6_B0 &M:ą 8y&w'^!,^4r7\ѓg+s$^~!4{L.-Ǹ߱gV}__i2!(8yOzv յ*7>6#ƫA,!?ewrCBBg,D..."ǰ?^g4Iܽg<쳑QQ _"xyymߞبQyxY;c؇~XUYi4yx\]? T*s54MT*//+WԘ-WWWDAo?_6i㽫L14ag~痌!V+kt-󗭮 1qb3uK.:+CLV,Y m)cY[ /kު׳+74묋U^#@ vɤpru;;c8b  fYVce @ qy\+00  $r1C wwG<‘>c&<<=.sWuJWqvV:;+omI$cMzd :[U\4gudPTaO<22A/&\G j@ ɤn0X @aAiY?}eb1Epxk d+i5y^cXwK_2&1qI2ٺ,fmp<á#O8>)5ͭ;v8 !w w w\An{GO*Hxupכlsgqv)D9<5tw7T߇_ <@ z%[=x@q@i6钋"x:,ǽypIYꡈ HI f9ǥYo31vE۹dw U).hU-KMzWU"Վ !0lwi&ziEu:djV&Bb隑u0 K~oN^8kJqwQ& 5;U0tPLT߮D| VuN+׻)'Y&pQCabJ,jyʄ$NTEV}"үrhEG@qe1%!ő%{R{R{TP/ѽC[-u1H<$?$X.gyYLge{Ouq٬a/>QO G M(>Ytٌ~񹘠Ż&+25rr}!)!t,u-@[c@ 8?W6(&y!)ҤTHuߝ,r7?tuoiJԸM偮^.R]B=:S?xM.$9rr}$RBbӯ*RyoBl>>wYwho;|N9('1%$af=VrN/3; w7rK%OkZb2 :%}Eך%[_E2 @ 7Ai73pAG@@ w HownBtXZߧn^WcFSNܱnO#;;{43c <4q&~@ kRJ@ @ @ @ @ H)!@ RJ@ @ @n~J_!wcKjl[Nt"k',OCW7fO^2c( 3oLڲJ`4J)&~11ոU1*`PEO]4?ɯ_omLXa:e΢(]ƜIWn*O`mc%/ܫa~:(㖮_EǮ$TreY:%L3#JM̌Z67lY{!a隹W,'}Ev(hF͟Lª`O(ʰS?͉.6zՋYxŕ-,~t!o-;i,,W* P3Reܝo̞0>SwKہA IDATSe웓fU2݆i,4>~4&-EnPnZKMfy ۝kwaa_fNIݶY+)%qgk٥VM$.*ѡoh|b$hs&M̛ZTЌ5=Z3kOW~SzfM\@lƽ Q)wd 'əwD+6y[gYsSb\z:kEwǸʲΚBmИ-_qy% JdƖ4lOYK`m6%UjW[S.,~WvRhx`yӵO ~'L.mSʄ1ku1&{^'1{WM >_r5]w,$3fAJ @Adkf̏Y7+/kт+qjƨ032f %YKh( T䜥sc\1wْFOFxfiy"Zڂ UEOWn,Y=ހ1_]\ZrrEiT iǐ\AP$34eE~Lꅙ[+/.+Nq$n,蔣N7.O*eTPڢո-_()'K_!MՔkT)꒼rMF5u?]%K :eњ5k-v/XK3^#dlIJ1d-)訸zѰ==\äϣg̼҆0{&-< Z'==[S͌D=*>k\%Ƨ| .]<>SI.OW0Ypy3V'XU 0T㑾QLXuyEI%A\И3iVAZS8*7&%{h s4rn)dį'm۲rhB3fҴSu;n06R~/~sMU2znGaLk V/,EKS]Ӟ/HUMaR*-cq/sjnܸ݊@cŠRÔK7<[0~] j8Fa.\DJ [P^JVefj]c,Y_39&P$ˬ%&פֿ]KYy6oYzNyB205sh5+JlHrC;7ܞQkY~NV6{&g1Yё1 qQwkkZuJX IYYņe12,sM_WHLZ>ɏ6͝6AnɞCY¬9J`4Uks&M*M[Θ~C s'Fzϵ`ŊU!ok(0g,-K޶lU`Bmbta#  =AguMM *.so$03n?vq+ӄZ;Ő;s|V0u"u~jF Sf[Udtt||bB\ ;d)_NCcEы ذPlc6)ZSLNSeλFk(Fm=78% Wr5ѥ4)?Oris#1>qILcn¼Iܹ=sA$r-z(Jj1ݯs7Z]TXp]fs Fbz#PJ@)## Lԭ+&%d+z{ވ J=(xƢkEkY.2g-4Xo ARKTK(1&WS Q}rUw&55TXdQ3G4S5&y(@ jJ(z9q7%;jj (YEaIqNQ@ad1sf<}jpOs'rf缉0FHkIϯk4e:eT dajj/UyXRغYs%z޷RB fZ2)ZehL~몳ldflY#f.JXWRʄk#:-ӮuJ7-y''&.y:9f5FgekהLɊ%ʥ[֒Wv{{#!%㏭ҥzT%}탖0f͚?ڐ7s|ֽ <ՠ5(cY7zL;mbVa[*HæZ}(KeV JU3\xݍ%9%Œ4OW_l?֨*[w\-53Q~ IԼB(E*׋)j+d@Mw@Tu? :υ!ef *)l̚&:f ւ xS)9Tj 9 և( RX1δp?h`_:s>y?3#giNY25@|ӯ3a\}ֳ̩K p v;mEJJc?sFju鲓{8juߧg?.c~o9Ϗ/xz{@涌wxJ8QI>ܹsΝ;?Ξ<+KxGýgiVhɠk(3"[k$im}D?m})?իo h&""Tg_[ €['Vb,揎:G6o> ANw}T\|"[z ƓӓXY= G\=OStQI)-8.rlÒOާg~Ghs"H~ B46>Zpۿ3 ?@G_:ZÜQ|"(!J.oeDL@:㥿If~ (hEzgkg2KK]<$ V>߮7"7o9gzxȝTc=}% _ H$ sW׉y陳F!"~vifx}R"יp^d%"GWH@ yO1y[N|,WJDlxҊ5O/45ܟcadŖY2tҦ8j~QCD.N_Y-HY'm fA߬]L0N6oc{oelk %DD06|^1o0t.\8|WWWWW( 9sFVoowqfh/2ݭ>rwuTǀ_c#V#@u\7G>wMRr9mGͽ52攚:9Σruv! Jʜ0\GR{u4W!<|<}y'N?xsD\E P_)c&՚6wr_z0Qk"s'd,:&cjGyv7i֒SƣnNOo+׌?í(L۷we>(czwRGۉmizVf> vժ%:@AD#f,M7Y PqE  ȋw;J^9!pr?jï [~׉iR i59CkCMB{!\s͚<9|FQ)g'{6up4}Xuh¥w?ڛ?G^ gL24sFT<SiVSmkgwE[y-Zpp=)yOPx=v{ X󧎦C;$""k2{l:S@RkWRرcPàkر:PI v H:::Ο?Ο?/D"AuFՁm_#j '8n̘195խGDAm:"W͟7ٓF~}=>O}|s>&,%l?OJXt7Q)Ӻ=[nY@[j0cQ64ROw9tA3IP>i Ys4O^KWjC9A^bzgo|8/)ިo*/N󧦆>a,e 2X`"c~xx3ŸtIDk귶?O^\Ee>uN>cG\s:O#YԴgkqMɫbcEEGZ/`g&\h>VuwS''QGC95+YNݾEGOSxBr˝oG';ʇ.Apcuw^n766<j`$@uwcpw CVƶV_6}kRg{X~yiyxMƼ Cmۧ\3l:>mzd53K={}~͹랚a1Uվb}K_~[}-ctH3oպȆW_=&_xjϥvz\ό^+j\ %"h1|څ Ο?% gΜQշjxoll㎱R޿@?l 8nxgZ:|=:ۉmk6sZsЩv|T3&xҩ"jضnawS[y"t7~<8|9o'"Y3mț٤>5ߴuV%L;WSעkI#n'ΰ#ر[HH(HJ8ZL1Y%j&j)uw|H55 N03ao"O-ܱXc3I0d9IDُ랞?C^7f#)I F)T`l2?8hZgZI0MxwVO{fϟG 5qc^n ]ĒZ!VvV>'FI z044_*]86T3>eL['|M/lJY IDAT|D~3q"=,~NjϼIA \f~)gԵK1ɽ९~%uw_6-9."_ůaW>=II d0շj9|w]_}oD \yOѸy|*G=ۖi)Ӗg;XʺT$-l+V>HD=jAxp׭D{$/.=~#4M[Sw0 6͓0@pĦZפų}NUӪKs';/_biߧ>b9x|l >F$"!j8^yb2CyqS}Oe%*\53{a:3׿YQO/]ϽoO~7I3nwG+ov\WvRJ,UI{toY´36lV]yw _ _fEՁG/];wm{o'w'Yfq; I6SiJvoZ,Oy_{\N~xA~6%/+*_^Ywb;hx*'7ɔ{C~}cx&t@REdN/6_ #_:.W)lYP?-VgOi=po!G>r?t~j֋oZ ؗ!z~CW$w_W_5,7%vb¥D3ӵ*[uUx< XBD<O_v2_dẗP&< /Α|xi۞|$S>D?8-#'$ Q&j7%A!'"4蛚fR-/ye#UW;NDL]| 0Ip5^Q 1DDDt-Wefɽ=s?bSU3yz8Dt$97lн*#8Uֽ,#ǐ躴j3qz7pD(T5gC/Q2mNu8|]|3NB$:~{;u$j.xƒkN:Ez`1`֔?1 яHg%\Cϯ["Ϡ%xo%S$gke& Mg%̥ Hޱa$ )5g$L g[)\vvuA[5Ub϶uMYǁ7f,샒K6,$bԺSk\D!>)쌫g*!-$%A550jϙsm<$(;i}eM3IWӃ:>stiYaq ZK$ w"#_ xh u'gb=,9'a]C7V]*Ck7%^D DB6&`f/%rkuɖͅ/Jo\C%>"߅^y_b?ma""Ç`a'ǯuG^y)2/Xj_мM~ Xjh?Y:!]>Ͽ2d=y@gbSIY/獭YcߒdN,ɃO[wm1JbMw=odk*sL`DUme/&=YJzW{4eD$`~SGuceCEnL*l7H:/ "={6Jn .?KEAΜ9b<=6ٵO} tIR+}8coO|y;^Z:$>; ϘHiݾV߅z=ŮyK^ {A=6 %tryAIڅ,xpo/~?.׵[5T-/.?}O%xw2 Q_*r U>dzD|xMOK273{׉O~yWAk6= \}ؗ_qqlʁ-:}DOl]ʈ_꺆nUH_+y[V"8j~[2>-|3hۅHD?-"0ew~[my$vS 3)ó1 pNIVߪZF!Ƀ;M>8y%?,μPƵ[pw+Y`tOzlJP*p`6 `c}q%(nv] 0r www0 L{e~1n8WPMI I `cǎE!XwAR~D$t E0t3ߜ@Rnr\ E0bh" ~~öR<%q'Vųͧf&=* 2X`,e0,#8CR]%;u`;$\.ΘDDcA@-q…wuuuuu(™3gj5Jn}BC ) ) ) ) ) ) ) ) ) ) ) ) ) )x`jkkC!uARW)p\EpKсҸ|||A`I `DĤӧO?~ҤI(ӓ&M88F=nHJ#BKKr9bx{{PKK2G=n>%[0|||P0`{@R:::p0}?!) ) ) ) ) 6liQ\UU6値#rXeظѐzRS4h5zc~\+JR4>BKUYUx5Y溫TibuזjcLLN5PQɪU'\mETߏ7ۇ5Uz~k[yy iHJ {eS-/'yu|v}pr@NjI[]5bZEƽ bR=55555JI9֛^nlX@f0yƒ,+ϋdN_jz)yو2`+),m!>`(YU-(?""δ:PM34gU"X3 2gb 8s+*X"&9'=F,_QX6\4Z\a&Y5sZow8I#'j[k07D"F V}QOh0.N4 a'uzA>c&msAJ*sJ#P}%p5me& V/ %eduDD-Qwl{'*9V>xQDA\ HK+wY7G2ډXrՅJvmꬂ]yFSM !>,Tb޳9h[ {eerKYV%++4V9dγPKڌ YrKq"/Kz Tm7[Ő84:oMI=\j%֫]l<_eH11]ziiN b Jeũd2.e~E$k0F~qsNk]Xf@:Weƽ}b)/1ː_1sZު2BZQmn)WJF{tAܲ=UV@D,9%]Fe}N b*>n\ Lb=-j!!?h)VeQc*1ʒ[ 4YJ&8Sc&:Afw*jDRt+D7V:dKąFkd  M RMt0KĆDF2g3.2R?f*NGDlHVfh䉈 #baF` t;&x M5VhsqZ WWi""Y"*d t{qiNK (X"V&[vTSƤQ+߿#$:y[LXl1Rut0KD~arǥGu)[ZPIr"ä.)'ԗUxVe2?zݦ+s¾Ǩ&XB.*&XGDvkC瞶G.R8-ge141!,'Kdqr;ErTXxub%bôZiOD(9"bd >`B'/D"Y]Q$"2ҝx^'I/p4bU_ص'Nla9Cv"Qps{x%bc O "+l#)0ݹO"gLD’K( ^;#kdY"NWSb!"EQ*wYu#U/OO9.ONg]qqw{jAXDR$`QRPr\yFlhtrVW19F-`Z̪p$vB){fI.1ҳ*{Dx'L0aN kIrsˈ "2+\D!qY/4%VES{-4,0ux'O EDİFtK?^FTrKsJI2#FNʒ*; :yѝ\N3 '\ C4| .JSKJ{G1"* LHW^% gpbHeqw{V;yb$=1$J mO.L͠1IUi9׈ ֪]Y`MHד/'s-Npr dNNh5F;Zj\hd@[K+"ӲQ >)aFCJVY]@BK]YVRv H欦 ;ѡW5Hl]a'"ZVOjxEtPYjm!"\+G:9+<4Dܜs$ح%i͌V*bbG{`cbZic>+:-~n! dl_dzrEZ2N*Q[dD90($%71+#^c$b91~tΑ61u^Iϋ"Y5<٠ڐS6#JwMҀCc&B:JZbW INEȯic4T\mFF'b9lȔ9EasPHbVA=C PDDDH$߆ iiIsyaĜ?}RWԛ {C6\!;*z[FRT@>>= )~~~?nǴ0hoo?lą Ο?% gΜQ(EqSyyy1 * )\@R@R@R@R@R@R@R@R@R@R@Rl&JRE-|+y1*UAThȉR dC3:s.&]SQz 2SIU˕b-1vxv/fOd*ʾRchU*G.qNRiK~** lQ"Q @H3ק9,9iiiFwG'ˍ='*{7zzrSJLJJ-Hu1!ToLKK˱8OTV)%JREhmVSR{o5"T*UV_Чn6"%}єQT1oKRT*^—o~o-JMPG[P*9ZZ.05Vo}{gWGѦ?0-pU:yy 5[. )Ԃ홑B"l&mVq՗N2Q\VZ/)͔VT-Fnߞܛ?b 9ʪDDjuRB,*˄ܒP~wU6ruȚK1-V(<2'S1Nsf[ IDAT̢훗˛*76P>wnRJ^dr 25beaV!s8W@BƌjQ` ZZo%eh)lׇ?"e {p͹dݽvN #7+50aOccz=aR+yrr㊴2,i9jZ$>&&==R*6]iLόM{ E QKYZR~S}{*7&͢7o3Jm$X "3pQhgEx/>MhK´.2}sNSQURUZ-J4u\!?in+tVdT)[^j25wsnU]:dAL| ) ".#Z*V-.8xWn2,Rdl26QQ.%FMHNlsQhb&,,&yy 9*عNfionpDMLф(qjD^0vm'ӆ9sUQ:O2D484ďjrwmk"сD:ʃ0dDDFk’wTTo8 TeD 9m;X{*gFH#ᔺHI2}SH踋21D.e6(1JOBj)٘_Y,S^Ht瘫\ԯ^5Yƽk)IBRXezNP{D/ٜ8:D$ITGt:E]ӑIezxC$KT&%r6 "dRXVe(sD,+eH*刈2DD"QSbU~}DuD]' 1r_ow'cXa1Z ^YZODi+_qTص:99KąDhBle '1;zN?0-0T~QK7VI-֚ [N!%rntVHGnEJ6[*FF=JΓ%tIh۵ѭTKO (] Y*lNꕔjgpjqA|Ӥ]4@Qt4`VeZEY2eS Ze~}F/eEcʳ7 ۝BHJ΋'zEH充u"hs s41Df1$֏{$ِhYEiVѫ'm7Xem6(I7JJLyYiz"Z)#bUWa6SHp:U%yf90Ddk 9̆ºK#vJUV$N*9CR%fSlf]J/:\qR%arM|(QS^b6E?{56[aRrUUUyR1j?OU9*N[uaZRv$U(FSayd,sUa/fQ`t\dFcYĐ_?%2ZJXh0[ 9Fť s1(FhrT68&RJTWVe1geoXAzV$J,UHU\ >4 KIs藇J\+v3yJh6|gF2Mצ5F$ΑV#tfWsҍZqqj.R IMVJl3MDaah%C M`Ò 2C]W%s rcؐƴiV+(ŷ 4FpO&.\8|WWWWW( 9sFVdnk{.Uh0&s ` #oMp;}JeHZF+1sRbDB)#Z/M W-`F^OF!u4i`œ@_SQ쮻B!̀9%$%w? [ɠFa80p5e]pK\ES sJ79gKkI/GZ([bHfO!).,w' 䖸pI/GZK/qx`Z1vܸ ^' )H#w,a$a]UARن٤bu*pp,9rKW=إ_M[R3αhWŇ?pc~?Mv?x [/*ik|Dz'<ٯiWYxo>w1*R0#M^Y6չ?p[tp˦m>kvV># ||T?ngkZ5pi3C;bIl`Ń|+}\|;jZwk7}w?_G;;bLO~_}*k45)@3'9DD]5/bR榯aeIÒ ̾e#6mr_ItXa-JGIR""bfDq߿=e/1ͻ[>= bۑu m6$\8[=p4}_0KLYᯛnrr7k_{wŕ KWJ3 N#}xɤCrf!<+0!AEeQyPQècÉ, a!3B ;#roԋQg?8UTƘS>7ꫂOx,$sD;E";4 ɟg-((H_x;OT^TV GBGMaaEQ%NP$SCq>"ePJ^^R 'n3 j8f2Yآ=+9MboccHWVxtefmHoґtF}T5$2/vM05S}Oѩ$sÖ2DDM : +aS|jkEy~U;:- "o9}S+ƨDU)[w}mZ}>[TW`T}MT1;66ܚRM@+)%"V:ZLDeii.fP'OuY.,(z+#n#sk Zcsl&]EIZl|nH(Z|Jkl2-LXή_]N}7B6Ufk\\HPSܞ\R@Ս5a&Q2ݒ_a4YHQZ嬣1׾Ymi{&9,w,l43*M@T({0جϛp)[DB๑U=l4|+*^V>y[2S8_f&I*{%yCDD6G @߫=hdòV|_>;pəsf)ö^yխ;q{|㺾6]!Jzv ے^/q&"_<n ?˩p#^#'nk'YU,/3p^S?=w>Ǔ|aȕlWqӻR7c f ޔgM/ۗnί.|-I_g>z/+yB7οxNӺ_VދDd6~` Hht)Af #oUVۓ`.?f";4Եwm9f&"SUN]iSɦ=q}e$"Z;Z>(3$'&7 o5+ ”3U-*3?^7ɲ/C"bHk5VL7v?|dplw$,*[ZZ+$MBQ^㝱4ID$NT5yТ:P56$uU䗙7J²6o/n ,7?\e"蒴}GM c4cY2f3~9*J4hG7goOiT>r(s3A):S&mRvtII7/SY:VcKC;+LSu">IWPYyxe\_!ฏ;)tC.r5 C}sKv22V>r(Gm,DZTWb܏&!YUg؟q0)8o,-PQ>~ަEAm7W<+(x^-=7q)uGa*pHy.}v+3߅ycv'V֏{چfzI߻W."4Nw6W9KnhH_NfSts `m2{=;ť6'K r&"lܓ~𯊄j?KjM4Y-"uqOE> 9Jy.knͲ5od~<8x=?<7~SD|kDD [L{Y3SAlT)W"V4W ֣wޏ=b:|tFK 券4PS%mRE- ՆxtEu:5q%26iDJy}e2J{=TCD_-}HiZԡNij<hoGTlQpDjUpkLEt$64Nc?`hL(bĨE,IFu,}gWjڟeHMfn&xaGD\`Rlaw4t$W (,>%F);ߡg'DDD3ҝ{S5L~:ʹ<'"U H!?!jjSIPRHmh uZ8mx̭ I)aj"bc_kj1I ZR4[&OgtYcpXij4~IQZ \QYgj|}9,%_Qx߃iK4sΣuhZC^>*9 7kS6kW_rPF1a?3]:hub꿽FDo ڼ+hjc%zJd#m=Wm4T\msFz>^zgsY/ڈh&uFd%_ d%W/;yc2l]n^.3CW%R0U(i_'gf~p/d'""Ń_^ C#(^Qxϒb3 RٯYNVQ&ۏU4t DDyY%(3걋?H&b5;zm[dse|,R m}I$N_F"yq0R5Ʋ}:xhOI0ʱ):M#"/ʝ[cISfH5D$ haIA;jҲ6&oy(OF)"'} é*S*,S>JiuL~ag:Q"ome"/2QTq$C}g#IXO,}cGVr14/d$Évg%pJ"/2c8j\"Uߝ5MܗS20M1.^d&7W7fFzIL 70pD^l= 60Dn wfSIՍϏ ٍlom6GŶ ۮ}~l#.pX)gw7'"ix#y6mj' $%xHx嵡yT&|FKobÒӭ.*.k +㈔%W+l_?-|@Tʢf"b8%#c}:5ZŌU{aIzD$ yQݸHImͷ.eڨ&ټ ;NYwTNRM<فhH]-R)꣉,%q/ltS7Wd>t xS쭝d5vpJ) Xq6wpp}$J-h_yXQQV3I]5x߃iHDTl:cK3fBY+^q[G. \p#!ȅ3,s}JKPVqsR)+>3f2_:l\ ?Kkv sKn׺{m頀xGApx9kaѩ|.?1!̋K|ae"FتSSMkK k9??jHx!=}&"wcnfrL|Fi?K}J ߌ.MXچr6seJY-MR3Yi~+s_Vy,^ʔPyKzLgFeevL?eMlcfaʷv_!*(%/M2)[e*_Bƚ׻WܐӚɨaI L<f%w[7ڛWR#5~ n+MQEk>Y&2߱a~[6v^AI{*Uީ՗D{~aieZJlRaW.>U=}> ^qR6,\S%r8޴Q:ڍ/7>>y鰻o8pt&604kK~fL(+r'j:EYω S殌eCk/l-GbBUx邙 go>Iۮd.މ4uy6VuOn_m%{o5D4]o|K/C3sZ=5H4X^g~VoF#mwOFwC(C/n&{cC'$Y/ {K%qTvwfahx‘}nI$P'FFFdY$… w_,ˍ?;^(<GLm^8j'fz-VYRG>)=8ԚS-[OF/d"~Qv\Mh3eUo?drۻi42LaN0t?BGmQtm)x H]臘Z2JmEUQv%`[X} VgX}AR@R@R@R\=Fq#.HJ?&/ òxI S%/vbkV,C#C׬N3f|˳?&Ww[V'6x2N=ZDR_’l8Vwq ~' NsJHJS;DzAIx`N I `*X}ŋq{{駑?V ) ) ) ) ) ) ) ) ) ) ){UώI^U#n;,"tG,i.ndIijOKw_8S_y὿DB%_Mzz&2~Y[fz.-[Wnl\uU[KV-IDF&o=yH_OT]$B(<.Z3L7}wl{UD$޽,>22222&y}Y[cbVؽjir|Ld3-%˖$GF75]@R>.Õ]37ö==ZZZZӹώwD # ~guuG{KLDhٶ٠^ɓ'JB:J7D& WI]uQDrrKu뚪ӥ#%{o;=okSKS[Z9w甇-ۙe}X)RzYY"~3ğ>3o{ש?ټ@uXC $%xر/Gx#)j5c6՟XMWpDDrDGM-g'Lp;Fz]BܓH=W)eb!Ĥ1O8]wO!"':5D~g37>ē8_5rg̈=npSw {"X^~dSf3l:\"bXN<+JKD,LDBm>J?D!eHbRȑxASoLE&'R(vòcqg U &fߘe[ctte?J%E D`S(!l^}X]ÒprYf{ Y!눡ϘsY)j8d~t'$p|c4|ogVOrif"v/YRrZ "Sf|÷uD$uoᵑso$%eW g*Ȓ=p"ɰNPV%ѕ86d ƈuWS瞵?Tj7VgNXr]dކM1DyMR1pn~SBV\K3Z3:sQ7b j&dɪu/ްj/fR7lҘHNhwŇF D>;[X_#?{[vlj@DoʭX`9I>x{cdddxxxhhhhhHeI.\~tiɿGX -I6?|/ޓsW}TKO~ަ=Ȼ ;M1{H:kQs8!%oerq aۈMx̹X(p;gwvX}u :J IENDB`././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/doc/source/user/figures/deploy_env.png0000664000175000017500000022670500000000000022477 0ustar00zuulzuul00000000000000PNG  IHDR5i2sBITOtEXtSoftwareShutterc IDATxyXW7_o@W݀4(. Ej *qM"&g܋ "44q w121(FEPiTnB?PIFI<>P]Uu::T b @ _THlPScyd&sykoX* Rh a0)àI'v͏F5ՂF6*ݵU#AxG$^|۟8|ZO}h\"H$"H,l$ib1H, D"H ^D>#H~DWX{ع S'VXƁH"I%D"KE"T*RL"EbXL"X,D"X$%b2{§-},؊eM$ƐOj)M .D$&lM$l$k鵝D$WWZ""$핅*w|.S'fDw-OWj%D$ 6~D$P}@Ah8C"$ 5$] zZqFaQH$""1T,DbDc?D"A"fcZ(/^ܱcjԩSDD˲DTQQ6LR}WD?i⺞TF1==d֬Y;w&Ǐ߿ڴiND/K/d_qtؐ1wcuin_"9qY\ՂW\IOO߳g=>|d2͙3s.\ؾ}&O,JsǎO>w>} MF"D䖴|8iT}L4X"HLyKTg'K\Ɨb''_d2l su>r[mOD2^^-rEF"wzݧײ=u'ސiYLL'9x13>#fnJI`T  r2%/MUlɢu"> -(Z!JY|e BjK$;qA|odtZRyE_lfJJxltxοJ%j`|";80vb$_"iU;wr9 ><''ԩSa>}н{w772NjsrrJOW޽e2م VVwQ޽{WUUUDd$"ZH$nX,cX,Ƨao^0$KL}QvSX_+ `QEA܌"վa&\?\gߠ 'C۝)@Ĩã#|Yo;7;d6';ޔUT}H!{MbjfEEP2yMPT|t!^w %y]v1DDtt/&ύ3DO VܘV3Dyz !.wmbrFGhf-aܵqxVZEmOMR{iHY)GOD ,*TK9qSRT-*AԵyKdžڷ'KٔװuC%%zˌLvVAD- RyIŮ3#55'(?S|ա.7:z;t #TjjĎN-9%۵kHP<E6n_r4?mB5wϝnʫ hڠ0us0dIѫVk.'qFܺܰģ֯ P[-Z{}DKG\ƻ%{\#SEN=ĨfúS V}1ߗ1d'/4:kKrW.d\Xz1:cfΒD1'gZߺ(2qm^9q[)*` q9s}֯ hM!iE)%!IIԫYܚ]=˛%>oͲ[\ b⼷C) ۣXEf6*Đ1'[~e Hݚ9yԸF"%o6YjT -/m)ab7Ab~D~ V&&&;'''"=^ҠKR{{^߻wW_}o>)6bբ%b]JU6ٕ{wߚ6bP}OkB"ĵCOܻDr\mla4qs/(; P> #fh"RLfDD2!"b}YW}9w/+ሸꠉqp,Y"R0G xɣE0 mP7sH06 %..>=*6Q.\}ӷ)|KL.x~h܌<6dfMp?4j 6Wjp`|4%GOꉈQ ^ gdjWIXwqEUB eUS#MxDmLg?iK4kު$X{]`ة1'5M:u޼gc}i9^2'C^v{P9q:a|F]z>؛LF#G͜%He0/<_C""b[p #҄EG2ť 2}cN55xOOa\_UD lC^[$^^,Wb$Jϓ}Z8j|uɈ+&z5 ZpByIm&Q*%"&jPmE"wh%g̘1ֺu {+F{)wM?ÇiL͙SU+_#u ,bq ɲn>8x%wDf"}jj"9F]7eYo0.L`Uُ2E_C#S3VM *x"? (Ys#t XEDGgCV51vK^,ܧkj>K #8cٸgєa㝹ᵡș/&ϘRWR-xz^lˉT<(eʨ4,,15(`fYOUWPXVТ{$-6ѣO7~;y)Yl~2F%T^} "d_bX'h+^3_f~QnK'"|OkRkY`iy Jlt>又[iiԁ MXsc9LDZP$mnX=Rȸ7*#4UFPU,jY:[?7j8땝ib[CY2o@ URܧ=ht@hU  g"VyFط'K࢓.+H5`2y[?k#917*!ҟ=ZƓz6:G15UEs|>3gIs WUEej y$2ų ,߭Qnyɐ&CbƜeq\bYH0V6ܮ1V7>ԙG"FQV8p 0}F1...Dv_V+0M f˴m뮋d2\ND"t?r窏eϖu2n._Y7oi'-٬bC }y,;;kSKhcfMiq&$̓5cRTߐDDl@tҬER{^D(ЄĔ8"FxY0vyzZ&R/'M`_ }Á䔢_(,zցk/|xԄDLIM(JN;#"+(2V,yujoXxjm pKlXZ"c3 MHc7nNRi-=zEOL1#";,*V0@"S0DU֭Ndl䕓ԺM%'"@Ĺ;ˇ! N(JNY0!žP5&>S&RϏ|^ԀəA|.JYn??"z–F/2U{ >GsƦ؝6e矄/Oh~Mա_`0<| X,Vf=GD#<&ȥM*V#l6o"ƛv.WT);22F,x:S=ClBDj`0xyy͚5K&ٟT*+++޽+"##;t@D[n]yy\.wvv.++7n /P\\'cٲD$vpp3H֥5Y'/"ٵ7nߗD$qn'جżۉq^n\ݘJDn(_#M$md.?Kɼh|L[<]s3&nI ?]Ol6[iiݻwqUwM~Y"Q*![Vػ|ޤf#A LԳƭ@5 _[|jׯ_O4ͤI>V\]]ӝzSN]x|}}eaÆuԩv?`Ҏ8oq`U_}!Xb'K7+֦ŢjL?H/7zSIz;OoY5!\p\\|Ƀ;sL[r2rx(/h@xubó&ψ3KK Y:޿g=nIeekTuXS}<rsrHDwWVMd(X88rQ5' _c{2]T$}D4>~=s_o -3ͱ?*,*a!r4xLjT*=<=޽#6EdY"f%MDU ZTNjp8a(6Lpzwz{t!"M/̚S|fCnf>CAEM=y9`c~#qǟ4t٬Us78"GGs{ˉD"T$L&K=d}Q+"*Eה*U$RB$cQ0$<|OO7@zU|w[f Al6j把_|E'ra>ZV_<h"<͠"D"Z s qD"i;.cS% lDTRٰl @BxVބ"v'@y?@>@>hꧠXOOOO~@)oj56 EЖbaG4L& Al6jZ<ϛL?Q87刂]f6Q!<Ϸ< [|m @kIQcb꼽=OS q{wΘw=iÔp/urVl#u;>ةח;0SN#"V;79̣m10 IDATptfFn2mz|haJLRe łyS_sso_Eع}Uևk\4|݉VJOܲlu;]OI?¾2I?QI:m/>!ʒټW3J]gZLX"#ƥ B$!v.n.M/tfɯŧ(]touPtYUj#_Żw>˩YY%>\"i9+ ne]{9mŵ]j0/xގ7LHuyD'л1v{ipG\_D us#F;5>'t70}ʼ#^U93 LwcFwu'V`H=pNL~J<#)i =O$q0faTmpʤO!bN\Y150}2s`^o`.IA?%$fܒѝ8˗-WZ;)fNÈEIo||z5CD{etN=GS/I]>P~54?'L``,Ad#!3sT>OruG*;Kl ;vB ]+KowX7I""2ŕp/<s`iN++yt}皉$]{iꬲxs=Bipk/ܫTa* +N/ v<ݽ6"qܥS_]^⽻?WZJV{f5o:msn "/t|߉V|ͧU;#lh͜;=ݿϗoL{wfs_F>5Okr>>}KOMprCnU_HCv2oٖ2^:ao!_8~͉}ϬLij?!;m̶oa~Cʟ*MW6$֍NnC$ɤ7Uyd!v兾۾=) Į>o"bpUl߼D}du]믿zOx쵳-}Ug(x =OIdiuNoݗ:τ WLcz37S/6_ pRl:Y_|mHB—D;VHcޫ㛾vQv2u}uM1YkfvZR[B'?{DDt_"P_I\/t=ӳ۽U:w7!k?ONPm̩IHRO!V6&Wrw./f&2̸yG|߭^,)˩&Rת굞)/7نv 9~fsL&:wWVWqDp[%\?ޝۘqR?x^ײ/S\kRi=q_-#"sO{_=ܛIޓ4:m{"ۀ1 3}YFmuRYE3}ΎDՅmv|Y R;`p[hD{w|62GYKڞ/wpc*\+uBG$"v@'˧vr"Rٗ`i`̗#S=CIӢ*J9y Y}PMĥGtD$~y)p>[t2y={Lbޠ34~{SUǔ Lol C:͈~r"򜁟7jev£*o_sA2auODČqdH.D?ܳ1aN4̨3^R{UL %F1SFDܭr.Z _;w""I'WI>CؾDID6{ ٸ0gIF g݈DTs?D" pTůZYlx/п5IXA^D$s릢e/@yСkǯJ{B56IR" YR",0:gOGÏkOVܻXzE3ճMy;dƽkwy.v鳴WÔ_o?a1Q5都NJ:Ɛ̥?nL;1nt>7uS\OND +rփODİT 80/H1~g<ifBQ7pDԽe.SiÆ 3lqoƼPjݩ2F5tun*Tf'_^0D nixgz팜` -V3xUe88ȥvbS*un^iϷo]B)UPkU8Vay}fQN '~~*Ѫd3Jm-WOL##[W#=eDڰE^'lbRzrRRR҇_8t7}.ݔDDʲJ"R#ʑX5eDdܟ?:"KܾRFD2g>}xqlD_>>mzsˏPz;y 9~>p DdĠc򊉈*|y};އ.D9LB9guwT|8}Fs_7Б&ƕ?-(>a.>s|ǣFpx5͉L?Xth3ZNd*. W s&25_ig@:~̎#\ѽO_~+&"S7NpMlSx[nv㕦97Trn|[f"wvsbZFQJU v"ڪo-v{D$V:$D}(=*OH9hҚ)2:(;xj;8ˈdJ7GʲJ&TV9 WHdT ]0p%AT)-8Fd.1l8wѱۀJ|Nm9Ⱦ>&:/hF/pOҿ+&F32zFNo`jgB"g(mqAC$p!zwĸ+ +IA.́ISǬVk|ƿQuKFdc'NbY_M)N8'b4I~ONWH3Y=v0hrǍN=?+['7DOMuSM$؉u,Bn|R o5_+m㰘D]ycV3.1n ˜"ǦGhFܺ'Dnckfwerm´&|8[CDDVMu{o!d2= K?ûq殤U-x W'IO Vܻ7ר8z|qun?|0CTgn |Iί~/]sԗzvF 6jZVM&SQQݻlIO,L'0BˮdϪA(S?.''^{V6h?խC6=O݆`.Q~}ApO>Z;x59^Sۿq]J) ucaJw0oJc&.'ʟ1 ' U<~ftF/?ӿ gҿ)ێCOxJk~R2J#F +wUC~d2ܩහaݒH$r(mX,vppxdMzDH$(IA @?M~^@)" 6fYVP&d''||''||''"Ev-R^h#$*U3f\ZSt6>6xfAfl6q\^=Q,KI9$ER\o3>f2}Ҫ]>~?Rn~O;W8og>lL&BpҦRBa.۴9[On;VNvs䯑)SL!E|H,&1#r;j8vP?3ϝҏIZ/8_n|K}n}$eA{H鑃X3û;Ԯ#ߺC7KYs;[b;{|_6Rc:H[YGpgE$o="_.1%b3wTJd-8RyXvtvt5 ݹF 03{:~\2lDpU^lI٘u}xҫ3UҷX4~y؍vwL9Q^쥑7|rr|k{bךܾ]C\IcGq.vsO}s43ol6z}R/˙,OLG \- mg˖}c9U٫OǓ/LSk']<~C WCD. K0Y"qpuL&.ȄOjwlٺO,ybQ&^躕ѲeQ|Ҭ.О{^ˁu ÕsbCQ"IQm"0=BBc̩v"L׿JXY <"ϷR>zڧh*gzDžJO~sP0뜽Qme[<ގ7]뻖d{y?0d||VR_7oGTx<#'x~}g6'Uc9 놯>_ɠLϏgWy~wW+On}6iGT7tO7r?dC{*k+~TLDUVQ65. ]=OD޹kvږۻ^Y%+&`{`|<c98-0+RUֲ/~6Bk|CA5vqmk6?^>bcvx}'2.na<J<7?o4+>3\x{2G?rH]:O ؛28ngXOOZܩ r89q(U%щ{7FNUk.B('G@>`_׿Z?|RX..rSw}q#⌄?''"ϠI=>rK~ӆ#q}Ro',Pȕ]yN!W3􇌶˫GUSH0 7'$ Ŷq'dPFFD$ us/B eUHz!fvӯzC2≘,U.~ DNJ7(|v7Eqӛ8`{@._9|Z{@9뜩x`W+ol&B&8Tٱ3ѽsƌZ~qf ""?{_]k3DdyB FA,neD ǐO_k^#5H؃UyLD3:*{(n9tFDU7MU$ ""͛VYy@dy&SI~/˫n D*([ț2DdoVTE];R<|͎5wyeZj) ze}j<;GuԿ 3xY{ƉlUB%ueUW[QBOlf.0Ɉ9}`{^Q}dzU *W'پO-0 9'[mI:(Ī7TQ {"?W2 Tla&2kʴ{R [ {E*s(7Jޮ(kBQ[oUcMKr\Æ:\Pu=DD VTj̺RZe+FԺJ)|'aS>l]?ҥLS!ө&JOLxrv]+r;xvd#f;`ndG*^QMWOVY{Wl.>pWofb"K3{[Yr̈POPX5?Ծ&[$ V~uk4'JK>åV]nz}!4ƒ_x= Q`ԏ@>@>@>C1b{Uoe$. Oe&">fYLGL䓧x*cNjka3"fLcV/Y+5x|UԐaw/+?E. G^gI;_{BDǜlزu_'\[U?xhjψHT&ikQy1sY(kiimT8"r=*pH*#=&PS^3'CfDͱ>25>ۓj".>UL6gNnH\cݯe'`3'8"Qz6w];LeDFk_tuuVi ~~CF=UܪDպ9oYJArtŧ,6yēgO6g$}͹Fd9NOhe.sMY )X"55DT3)fgݼr "r\S?7Ï A4JA5T %q>6q (ܭ5kkh+ִZ"ŀ5QLBӊՂz>=I|$|ㄆr!"ϵkUon>$mZ8__~T'\?QC@~1}?Q p sߨ8~ە'4'i1ғ镢p=](w|Wb {58y`oH t|eU8qDK{ 894oi,} Lm~y<ǩw^sV.!"xxÏ?^}K<Ï^%Zy@SUD=C=Y6g*ǒD v[[[{/"pLvƳ߯s] Crdʸ_Z z-m#Q|@~ ?'O @~n+]]"?Yh w' ?vni]]].ѸFaaa ?ɥKAPP/:np¬Yt&:::"""|>I '7LOOO`` p\.O`|}}1c ?io# ^?2}gbZ^~is3'U3,oQgH5?^vhv>o]H7]JB&#Ec- ! |ANv.;)? :(ئNk'8DD6ݮv _J/laSIC8lCo{O9=f}491?X!WkO9 Q<. :DdY8?uPl;;$by1u+&>N'_{}n褰{,OH]] d:Y;IQvzo嵍3R}l)k1쭨\鶐~w>?')yax=, vj_=sX!QyMc^ڇN'A'^cO)ŽZzp]LxZ),FDވtQgܡ݄>$B3W-К Ӗeo_8޻=KefrZ?qؕ6, x4/s!" ¡^=.շDI.ZOoF4!q``.d?>n9ICZ s]qz:zAD]| h?Za3Cj"t\krrpN-gRF&5眾BwɂQ٣gt~ړgOi?8$fTa}s:Пi:U[-uxIq~Dj?{qte1!D-LfJ^+N'H!bY&c8s 8Dd=-4oѕ;!bB~آ?X4"j:ˆ-IA f>tm>71̲>pjOR?ycHĆH+ ro}ޯx(0bG AkG8C Y|^7y8ًV(\(ֈezws .sSj}f g6!@S[jmJǕ8W/=,xC=G*)dax̔Tba]EF[I)Jss1_&W''d=[xonjjdrO?='?3}T]UirTxܧ^Q-}7 2fndq66^˿.IQ8?q>z\XT(sIH7q0));~iKDĝ葟*E@ko}MDL[(mڳN [h"uZ[휅Of_ܻO&7hos+~j&YZ]T8}꯿{3Qigmmm{S7=`&mٞwDď|ɴG^+ ;6;E>V_{N8LCD{Sߙ.3?lkt%s0O4Eg.tA!x!D=wϖsg% xUOfXҕ?l] ""O+;/̛5;ܗHSʐ_D:ζ_֓N"?}+LTߚQr~%"jmIәS=vY85v6h {qfs aDr}e8lrqP޳lzMW}Zzk|}`LnS|4N𽅝z+&EZ:h] L|t6j/_1{(嫾sw k3y饗^Qp^e"ψ,bQږms#(2x伛3{`" 933{@" /v{UQx{q qg>M Wy t_vEU3D)PnWmOIUH_uyg) 㜵 U:vⲋbs+gg,bVgǧCy}̙̟DD4k]>FC'͟xsYrtk=Ɇd>KD 9 Wlx񤇈z].!aDa}yD0$DŽG4lߜ?GS?}v_8u]Woo [ ~kf- !w:; 7`&>M45n릀܅xw)np~i8?m릀{WvLWuwn pw1g>S޻V;l*# gy[=.⒟Oʰ~uQoxA7GX.<}{ް~ w==/?u)q6v/u_QdE7xѠV7GʯgY퀇ˁ"_f x?5gG' :4\m3ްpltJu׃OX@9' ?ϗjM fû P_@mCK; 8n@"" PcngfQx9?8 MPGuY _@Nteť[O ^xTAAzۜm\//zonqfC}pz\D u8Y& qo=6V]փsK\nphIܹ#AmW/qx>['kNb #v.=csÆ:{|~s'RӴ+zgL00ܙMO}f \{mFUK!.:HKm&7{<_|X٫DxxkU._5wu~ڋ=tMD3gGLr5u]#q;&]߾jkwn\Mjp'@ a>cvUu˿+q"+~*q Qટ],}t?Mv _)S[g7ڟ-&ncK_o#0`| ?@~O' ?@~d?n=ohå"E< '@>n^lw~_2cM|&n{>nO~{ݻ'nƤOL$Z#DD=Ǩj;Vg;gƒǟzAYWOx?215{s_^3ˤ7.ψ[ ?س_ QlbTޏ/E=B.d2'F f܈U"k{pOxfuĥ{K=JDetC;'OO/qL#;ֆ876"_Fq\̗͗fgN !;@D ;ܴINzVg*slϫ72]܈swlr߽\y=  -{7K~'3 {v "֊dzp{UMiq\n}Mߣ}е0i?Y񨆪%"K'6hQ稛=ND/ۖDr9Dv!"!"t!nmir+?6M{s?}f"у3?#NxcYd۔ J O7SwL2grQ_M-fW'~Aw/h0g375/X}, V㏇~ehj<~T89eSM'ZWE,u_H):jm}E+3TKUVm.""F %d+=@ܤ 5""2l[(Ń$ƻ@tPuf Ix@@[PDbi٩;55"d̔Nb{^}/WH?ii:PRT,J&(ӕ+\YG1/[$-Wk #ӳR]__Tnʒ3g% Vz$%-(Tk1BY*#K9V}eLoQR"Eq e’Ja\,uӖ+ƋɕiúLjźb`ힽbVoqO(d)C!"5D[VΪVo/RHHjݾժGwHQQ 4g-$U."mՕ&e或 g*- lFmFWm]bJ2 LչĔDV_]Y[[Ug^QG6n(5@'k)ެn)ە:5Wn^w"eqb]~WYO 䪵.m",91a?d\C EҠזj5 b lƒ %NYjkkj ʫ4ͺJ#1 !\S ٺnZc!b1 bi3u|]6h0deחfWS2#Q`3Ԕkv&OXU#HڪڶD{s5vi2M, F_޴,K:~mPhp$q b44[5kws39&mb0Q($^SY+ߦ2Cu6-.^ķ:}M^ؽK=pXc]~jTm26F_TRH MIIU$&KxX'"{]56Y ezM^R1t!L5j#$KyYCJNt](+o^\OS=o+S1u5%,n #'A}YpColjC?x׿mMbRDDDi RS֐>%5Wi%5t|m.o ~7T_΋heu]Aq|hU9닶$iw 5X9j Yb"{]AE]K9QUP*K _14xmVN@7W9eްبٱ#QV$P 㐚"S=ЬVק U%DD$INbvU#224UnNs* ^'FCc+/qgE@2RljԆ;tr]&JnsMsy~eJY %[ .%XV4ئ* @F!"[M)ewu@SSlժVz` QL"`uy [*.A%"Xso@5(U!V+n֗hL /K͒kt&f 656+=v}jc@(pgCey3OJoFM!+O6=pnXk]A6^S^>OVMI}. ]HVPYLĩ}|YGdӌ^kA5$\, "}~IzX9kZʮ+ f_i̇˷d<DDX;sRDd5NfRm Ȍa;cSyD.Z7~# SHa4M|P;$N,b\H}X +ӅDd(ӘFU敉|Im*]~e41\c񒳇WJ$"cLD4WZH4|YfVCД*z $d($I#"L5%:dKjY<"˕%ے!6K/$"Vw ͛og֔$JNǔ;UoLpdRV-^""ȇ|Nol!`PZm#bR̛`cRDZ2CMX|H(^LMG1 |I\z˵̤w11rɈ;]*XTPxg>حVl6M6o{n*zQC-XI!"y2`d=.hp5F"%$ Y7"#I-/Z6r+;Hm\he<"LN"h"$F,ƏIˠ0"%">sM3E&yL!r׍V"##iV 2nxXT&nĮSkT)KbeJKQs${ϡݥdk 7hD0Dd((0*=wgf|8vlXX/IߵbϮ,|놂z'drct殊{ٙnډDɻ*%D´T̕ww}wr{u+\ys^sފ4&'WcZMl={ݻgg -ܨRD,1 9E.{rVkNc$ߴL嶸pqJl(uG`Z53}:/x hllwSo>+;mHP`4;]:xӺYijԢ2gX\"Nffz薢ODd3ٝce69/|]+4f7Kl6""t"eڇ'⏮KDtգ8Xbn6vb"YM6+٥7D.VS1~_'rl6;L6"bc[p 6 NNs ~ԁFYf' GT}KBtRF'x1[jKE)&"%b w5fň Ti8 5z?'r֗(&{gW2)knMaAuF|Ȥ)7RdF^\DDɴf{8%j#^|QM2U/fkݡVZ٩޾\aR)]sʵ_{^]$D<-xtǨޱcǨɹ{uX>=8 3Ua f¡ =}#/D@~w2VsC!uDn#"29eٱ7[\.װvؕ{]v+=K$ME M06X\޾cYorVzR"!CFhӵDυF=̑Ꮝ64Ia54t .8 FJKZP[e*D&dd3fdcWR-~!r/Ɖd0;"㮬\Ʉ35hdרӤm J$hU^`j(/Lcόe;(R>lYkTYo!^d L78FvKeB26z/?T2JR?fחiMbש5f׺-ƑKPɭה;ʿ}}9/QG@ ԗp5j_f1NNf@uĂαos oVM?u^ŤέO'"2;Jhw)rt&QV*R.ZkVUZ)|(q\$Q+}tN۰ûBk`>L>1;o@åEzVk7m+n)ΆfƎ%_SV3To?+eG\m#>;2n\Č{n lϊ xːڪNV.x5bn'byτ 2]pD #bCu`t 7(DrK?zwՃ}Άuҫ~lxNa|!Qǃ0:?_s,]7bS722f]ʛHL)emzȖ ٌ6'xljv@8bdhNJ4H 6Q:N"bYޠ@6r|ӢM6@" Slry5nd8 z6ȳ&X^_~C]|la% n%akmʐ.V Pe1iEDIHD6ﰱog_$+refƁutJ> vMPj/a{QgLĵe6iw sDCy}3O.}lWU_sؙ zOYGĤ*RFLPo`g& xD3\f#gg\%mTSdzmOiKjF*}V,g6;7EHN!.v Ae1uQ(#OO"|tۚX^m+ 3u6M>S:s¡ӘaXhvIE[g۵xkiòbҹvvm: `FJjJ.bjM#4E'Jy;6<1cm*|yIcB4WfVوxP_(!ѡ6:EI㝭(am.9MZBraY#%Y5F"IzHTIfK֤ k_'Pkuf~b|D(>YB:fȦ+NqU98-Q)$6}K٬3ED,ىu V:{B19(+0UF{q6mEV9\D|P\@*&9f;+9!ơR5#,hij뭩S .?n~BlLB..)63PYndąUK7Y ›/Tx'u8w`.{=jpǰLjuGTrC[>rUL[e~ys_&6W_Y0ɮ/)Orm|,=Y@?`j-*QRMIDdԙk 6W1 KԙX-vET\~C&ѲҬW$Ddl[SVwe-Y_fG(2({ ) '0DzOy~K7+gi)Q2N-JID Zz"*b&'1D9EW+ظy IV`A6MS0g[esD2@*fnׯUֹcoX(pʯ%e+Qm0[ s5&aj~|uj5WI2ҕB&'L[P_Wck!IY*˴&լ\ LzNޱd6mdWmVl5l\f]~'' X_7 ڢӕ!P5mѶ|Ť&| %8fA͛Oltց:E]IbUeZΧ$ux[κ_ȒK(`nw wDF,_#ߕ?bXg0ֺZרuǴ:bRLzFȶOItK7xw7:<"Iٸ$8sWnzvvs>L"1H==ve֨ $HQbRuI$"* 6"ʽ]g7>j $ƝJv"/.ٺAe);gDљ;3,9%[U#)^ɖnnXo۽7W",/ܸ|r$g}xqg(EmŅ@lܪ(Swf J/v1BY=#4(N)|{r*OQۥTOcꐤ(562^`S$cBZTx"YcF*q{ՅEZEDIBrFvdS𥙻JˋՕZAo!b1􌔡d])?TkiHvfE=7EUnhiXIKk)ۚ ^Ϭ%UJJʵ:HD RLKWƋndC/M-ڛהVm.E O()ɲUė5uZ /RHIOxOPRwUJ J556ҷd*YBRYRRYi D$qkJu%J܊ yyIyu޻#KLU+gm-2onkb1lb['N]Z3uux˜ĔtURȰ(h3fbEZ3[2L1^URWUج,*-*8)rv_V((R.kukL6TV^#Fmt 0%|i'#Yf wWk8u${x>vKpeެs *3fVoeTTƞvsrM!u_oI\Wlے/fFman^W(li֕k=e<2li[ۀ^_tx<Ǐ_C\yӋX{~ADb[vg+'=oî۶f1kwTEcl">4kW ,Օ:zgn5X Ӄ8ߌ=c~<LĉQQQӪTMMMK, fs__wߍPU?G$4B L(44}=44g& w8fрqof~S5 i~Udw|o[8/oiy2} X";wv׷toz֓݋O]x2oyβmmϬGwE:g`) vgoZg)i Q=sؙ Ҷ\\!zb/u^~3#D"$'ƒ`46F9?p~켧w?ecnb;|xKs # yvuE1 yEL/MݹӠج-ј)։rz>:y{>[.!;-IP'X^*|9Y~x۟h֭=D>,7s?V{WwOFDx7/cܥ=Eh˝{yݻKz{8׼~׫x۱5>);={^/?Y`=FK0fYA.}T*'`qZt&"*d#I2!2YSS0'SuyR kzݛ>-q8yO-~g+Fu{q{؟%r6udC؀<ͽe =1Q+gϬ(_>r^H8+1G:O:y|ńQsD~ś8ұ2sq#%"wM1' .C',-ْ?YFb_w`}y3o`3KD`}0?؁9wQߨ|wIܼRy_][sg܋I)+0U:}֜6ZWwI"(CA~r+]鼰>,`It.ӛK|.,}oQ9FtYܕ•D.u>'7 bq [ 3.C)ԕvl̡يh>t(kEty n6G}ۢ `zDy;vmUDDmIe?ATq]޼8b˼7ߞwS̳W{쯞(&"GLcz|~+E/nفDqRۮGeoo~8a}x9wHo???>yGvDAagũJ+K4fDJRSd E۪ 6]o-3r+>n_|A#y<@>#<"#} xn#so= gr8,r8[.|||Fy+,.fk^4G~Mr8㹁|}}ˈ<Ž#;"ZG(*8a7k75-CvDvDn`اE g 3 ;""#?@~O-iXn{``4,V#pA L/X .[:-.@~x@~pkdS[#]n%e%6}f,+(lfM޾=V[ghHPXH7FǏCD OCXLFsY|Ѐ/5бn KD>Haos-f٧<8cse>\x! #&7&?1Ce-}.!-χ|:}ק#o\y] 8 彺}}}x<.ܹsOƇ'2))?bfWd:;(?]r齓H(@i*( *W@Aી O A(EPJ K{B~A^z7>;;gOO{NDr)g` 4W2n !!) :Q p)2ž_KCҍ(Ch!$IHA@ ̟,KӴH o'REQHD!EQ]矟֯_jך'J?9(싉LM[OW8qӻz\Y:܏:'\Zx_o9X75-Z(Chچ*"ÂegO2N9wхUJJR91*MFλK3Nϻ/|eF-' ñtfiDwhR:+y:ApX7luH+E)B 8IÄ߿Ss{שdc6UrS}~L+=uq}KG "J?_FS“=ʪ u?MFV[Xhl}dC10B]?pٷ񄔳![Xr,cg*v1޺qqz :qPbNm$LјQ27 1 `y+$aw)'S/C.s[e$QozH?nTѺc,dra&"S{$IŖa6 2%njfAOJbNcvMMMDϛ\ z/˳XR*‚O[P9|=PQ]s&8sQAU{}OjVЏNT]bJꁦڌdZW" YiAt}ƖIZ)>+Mݸ}sS&Aϟ`ÎpBIv;8 IM5Z`0,r,KQq)3$Lƺ:R͹G 6l}Ý  ƀy1)gk^{tDGG1!:sēX7@"C=xdNqI p:.tUV U)JQQUsπS66Ja<72-͡cw!n=eu?>?TWXVi18#hv8]jI+&XBRzc`nas((WY(PyׇpJRB YFęMv/8J.1D[b)kk N.DO^~ScLQ @ŬRS|$ 0Tx}DEFo9cSV9uj56- KyyPTqCٺC8K2dM:>X~R,)zꇧ>Q/^pSxD=($ ( 0 @#@SH nߤ]P/ 5` /O2NxHN *J _nCF!9@ ɪwXGQCT;E+H((((E!%/g__E9׼[2ԕkT>^,ʪ-+6]:sB"TԘ۞̣Q5֌ڎ1pf&L%U+( {W.ڤ_M/=&lB̷t4#gT9x"2,\0\ӝUF2h3͙13 :uȾ! =8EQXbUf,<EL xÁ[9ȹQ;x5_Y"DK"J#ֺ$aA63`€@$FYDv*J s-c|57>(dXۀF/\?!Z.QWXy^j L;gI>P*# T7PX \T2Jr:-Vs2L]2GTTY,_Zȟe3?r6k8}OOoՊ{&T<\.lhlxC%H$#&_{煷 )ERt'-[9x[Wwbk3ʢx XC\U=CBSէa]df / SM*ݽXĀ]8Q!:tĭzkmMD MC!j~SSǞ*7S@= ڹfW_4 DQtѩ<#%s` *T_A)%uFq(tcM+>#[":UDP@g.'BQ(uu:ҝAw,i; ~}B2|!qCG)HtA<*%h\O[=ema!8(>~ N. 4*EJ7IUXx;a,x ة\E}4(.54~jj=! c8r<(1reh*̕aw5]q?3P9 >\ݭg*.TĽDۿr#T5^>cp_-΄0=6 ؈X d[2 >Ro14A 6 \?>bABg]dLHf?A >Qw[L>9hBa9@`Fu(. 4ԲFuLv[IE.q&'B U("Iҹhj 2kyk\S%r%Vw2IoURE1Tdl5 "rA$ D,AUUJ:"zȘx V&T*L&Ff|F$ Ƹa';oZUwESujaUԾ ԳjK&Q!)~Zűj;SdNɰqP-<_Y֐?4mW-` J@>i—F_Ή tHj*%c뱱B X%ZbΌ^ xOc={ͶcWx<ԑ(|Bt6e}70xxbW{?|OXK ~z3ҞKntiD86lO֞\(@ a *xB+@sQ^&DN(@h;i}rrثcD(w c\qjw*~!U 5μ#^ըlXh`q$ EGb<\9%M >d*\HxHER\xK͎ B IDAT;+k+kCDwi5223MooOwS,LRDzɊ7+6?<@*!$ 3WĔ]pt~gs#1'7CtHX@r~=ZVD\ii{/> vȋG=  v\)EN˗%,ri߼ /3Vym}ri/lꗴƬtX@ ɒ1' bȻ!݉9!p6WS,?Ks>ssGj|moF *߸ syP}dي giAdŔyC{}Q-7ѱZåϓYGI-AnP>^.}9M4~\^(XX€`P5.: 0pr)hAM%*f-JM^ 5'({X??Cll߽h[S ^#9;2r@J9@Xb=B\{t"_|_Գ H_9^ǟsU?->"k-(-(=6} ӵSzgd]=XB>q(z4O!6[_}Tdu4Gѭ[bcry t9UyJA܇8'xNqs~t&ѳ{z{*7q`pTxS?'ab\cU:nmÃ;wM\f.ѕ߷zY!}$֨#Nz:e¢(Ym 8u!q7q{EN :㧍#5&6QZ0\QZhظbۧ>z4:̓m*_r!N ՗!tGmQg*?Ⱦp|c'Έ e|_It]-3hXaJ&Z'6˛DɯrJ2zbZQz鼬AQ߇)3ɰRYa=w njKQ*4ÐR 22je^^}(O:30S]VEM.Xqd{}<Rvɫԗ( rVGjjbg.՜Ȭ34呕R 8( =6H~;KUPwN4<<ysDƂN tE_%k#Qu QT=^|odn! O687LlhFKt_BWTwKU6P8#0B3୍WOvdؙHٱ@4U?Ԑ1{KK:'fW6͟{z+|a´T|XeT@3 x\1"No,&獾I}G3(E~5hR!w>yɵ{_ذ)a`8rƵ(-~I<T=l 1(Q) #W(:wm6jmPc:#j0qfL[{u?&/.fssتrTUV%W'Ex*U*hi&R9q℧|9n.!XPTnT4^9!Bsz}ocߥI_Da␁P|v}<sWߞ_t>i],M`B !DÛYVF4`v +WV,7iN3$ k=};&Pr#ts <Ӓپ]Md oS eͼ8etr]v` $vFk]#|0M Y6O76z8Qh,`^q3Qnu##5W[ծqGO4 6=pЦ۬FiA s^VChHgH$$E93k<)ajss,EX0y E. :,; H&y7j'liE/9j.-(KѴ<й= {ʵn< ~щ + j)KL`~p*Q"gf9;+#;k~:66ƸaFA sŗ^0qlbnD?] ֲGM[v?gq4 h8 ZshK)c[OV T#V?ZdߺyVA:th^F=`IIpH;AAv]t8cCtyn;p83}ﯩ H76]p8$,<%EޝP?GF`[ozziԥ +)7\YAe dr._Zp@_yRcv6 h$ӀS}'f[^Tu@ Aˉ9W>$Ft'4V%]8Pkȹ~c179V뾫'}KӜh~ %ܛOwy!]1Lx!ww{o"a0Ƣ(,w{z[l JU{KWP^'{'o;;! @'rOL `m}>_,[hMЀ)cyo+q/lvm̐Č䵯u0h;el}e p"$OuzmϟxCdd~~ռ\@PyYhtUUU<χkZ+**KV`6t:! P_V6.KRhXJv_l%<߾/*{~wy~AcvjN}{ݗk"LB/LSDPR{Bs*o5cl"!7`CU_̵b%!$6~$+EDk,37T9

#|ݼ«K~TUBQN<A< !^h^m `,kfzF6#(V[?!܁yG~ܽecNj8~AydwPO}%R*nğƌu_[=5w[P__ Jյk'N|7sEUWWj}eQG*ߴykƍ׬YΝ =z=:8$YtiiSU]_ݻWONNNOK{q\w  c]]ynogl#&;w"CCCjZ1c豏o?ޮ /A0!V,kޚn7(;{DtЬS(z*qD҉F ~X6S=+WҢM1J9SU矧X2lU{g-p)-cߺa w,Ǎw{~np{pX'_7't@VۤEy>#㜡^զL91~h]t_z)++wv{M&n,,*8^ DuRTyйS駔Ҳ2cرc,9zT$emv;ƘjG۶Sy`D]R+'M4yS4M?3K#G4te^:+5W+/j5M w4+ lM3߇wIT v +SB#D*~E`+o(/PM{v2W>>c@!DPH/dQQ)-O{ҥ}dhBsΏߕ~d~с|%[V=:HvK7?/ԗ(G cLQDz4 EIB _lɚ *CC C^ !cIm6fZۅLwb#U>bB }{^zހ6݅%l6VC|||vj21p8mNQiYԴ@\V_=?066\\]*ޞ~~&M2Lj:,,j͵Rzd*M&B!<<<<]^򙫗ft:eEܳ_Ց{M31*<N:zÊ3V__3|Șw@ڙ~㺽>^o/SPR^TRޱC(9 ¿Ʀ TUzzFvÑ[Pt97Ȉ 0յu/\54tj~ug;EEd<{)+0$пklK#K+*s ;ٔc_~xXOӜ m)\bBpssS(B|d4,7n\\|N VT}!:P@@{WXP`=}]}|֯['ɼ}}}y睒bϲ:e͘ciξ[>:Ño~mnw2,Vۼek rٰAIJ<̅ҊWDfe2ڪ yABNQV@?kNj6;)*e| /ܥyolh4V*Z--_\X\V Ã_9Zo6&E{o/|?3K/.]a4EQR͙:P_ycթoB%sf)nE!@I[Z M.ZWWES,!bcc#i@u( cjCBi!j΀<=c|B)5/Ü,'zWW-!Rv;[H;{]h_/3|#4u=ch@SR^>oJ_o^Em4{zNyo*()Ys];EcO9G;ox'{MR.Ya ~_}#KJ^TZpj?o/<ј̖u;>YikZ̬LLoE!@I[|vN˖0hVd/̚ر;u(M#$@Qu+<4* LchV+nKvNJ9AINswaV}d_#a'Nt~л?_*kcnn$@k~Yl]z o,yx O۷gwÁgpMawWzycȮ5KQԗ~^ EQfXPOcG7AmYbQ?!K\'f+ݻww8LƵ(,¿&t?XS ,48%=u ΟUnݵcLjvO!oWrB4*U˨C"BLk<9ǨC:RYS[TZS 1C&=7y<$[PpJ]t,y9QNlBآ/,W$omהb[hюiZP|$_@Xp‹YW%%a:z) Գ+m^{߉%g&|8oV1s9D!~Zk-bYi2e,0 `dX\ږk]}1EQrhbs FF&#?^yEQB,h+cBGZu וTZ\i]~Zm;X?au3߂p"?<]C 9_^z*#EQrze #ⴂM)Й18LћWYS[X :`h2'k@B\ɜ9nkX \fVS , 0e^zՙ#!sqYKVztל_F㥭o=:fk 3V: ~ fkTy xF*[MpsYGOI90jWz|9$vskqWO>Q&%{ 1TVX|iHmͼ{i:֘m==u+ kB5#V5*y0a14BJ8(ʷr@& 쓘998w|l˹_C\YS&ᖧ^~u㯾;h4-JyYs' }EWfLWݜ_^rʜk^[8t@S_tjj ^798I/֢.\H?((655YΉ!MӋZVK2̰t~ 7rmk;2vΕl6V*g&Sngڴ;A Bw8cȰqH?` tbEQ t_T*\tVYr< IDAT .:g2Ǖߔ. nؾ^&^34Pbg*drqwG CNT9fѰY=LcT\(zkE!Q>?4ZoV{rOǞ,swSO=VXR?ǫ\$<8##+'N馟5剻{"*e KJSNI=Q]k96>6&;'nڄq5NΖUVFƣ'RGGT9̝`NvJ&ScI΄y^RRx'Y.%EU7O ...5 DqG6,IRCC?Ry'Y,;A1f(J$ r!l槩 E檔E(Oh[Wם})$ϵZz)<Xu; ]y#wr|(8%'g{ZXsQݞ㻈eyR7vfZo9Юh%ty[dN_OopA'X_uoQ?!;6wJ nHp 'Ԣ\[.ɨPdb7@ ޤ-m*O@ '@ @ @ @ @ B @ ğ@ Y@'1v͏B? ,1-覎;И6s얡ۗr˧2L&jwnM&޹{Ċ=z50uѼA=g̈́ɟ}~_/fl3h?x+tݴ[_]A16x>*oXH 13捏cZw=ƴeӗdǯX1RK[|mJ%pW*ڴk/xN}j~n\?}c|npoG?c /\Kx|}j13f[x|ڄv-) hiSs%SXy|߶`L^@1x}¤Ej=l<>sžVA{.ewe@},cL?ڠtAN34oP ǧ~\c>c>`/3kqOm9;`}bP/t$TFrr6Oi˧'޵&HPr+s՛Vx̙<Wi貧Zz 1sͬU9 3#C\쏰^ʬ~z>eOTg-bٓbR<"m^Ez?b1򹳖eԿ؞xªw9VG/ھ^啹Vov[fN\upտM*lٰdbds_ْ4'VaO@őݕ30+9WsǮR'*MJ>nƊ=5%TrKt˖l2<=iѼW^<3+f^< /ߐn{hmeAq}[2 >mgHNWY=>s֪=EcsVNx Ce)|ҊܜZ1K A]ښ%k EKxÁUsfSW,E/] qS^q`+YEOZ4 ɁIކ\SsiŹ٫؋,YyuŒsOZ8.cڌ\X2gѲAmtԥlI}d[95n`J0gKVqAbd_;UHe1d>hј篕yW/3'6e.TvWϙiA>{Xn~|eAf/:7anrs/YRq\a[gYb>f-Ve.]٪ؓexO~`\ϵhjn7W__[4C;>aAn2 OiK'Lޒ1~YO8wh-r_6kzŻ:`;=uaêI+X3wU ,Nmmm3'OWVE'aަ1Wq6͌9*w/Y8 3Z5ŕVX52YИ5;̵zxQ\ƍb^7f&dAmnoo'?Fў݆1A #v97򕦞V\nv{L\yvl޼iQ`ڃEPw`ZØ;v|nwxO7MZyǮG*xnA+6_y .yފwrӻ vK3vgG&EL2ٓcwv5Ce+l^aCh0E_clȰ1mA+vرiv` #o*F.]c=K3֭_y֞N cVؼykLnekVLn{\SW7ol!d-BCF Kt*lO= z<Ī00^T`ĪXX=rJ)xy$%0Ow:7?"$;]k^k$=\Qݑ\qXtLoo&F8o߁ϭ̍Tn pA2ٳ0+eOiIڵR8[SVyFf<#.;ϐRvxKV'垾TQC;闅)/㈈+;~Ux}jȴ/ޱo};7?a8z׾awdӔBU}ӕLYeɎRH_JpX%b3z #7L۲/ۥyŲexld1u|5thc=IhN[dɒy/-54͉::qjߣ8+)=8W_hoWvoȤk&2WOOHڵ{ }#[Д~|J2} Lץ^/MHM۹#B8[E&]6쯬ܟ^5+)zhF>u`߾[&_֧ab~|¾]cD}.}Sw?#"m{ McSvͽ̑8v;&}ςOUaO&-P^~QNaH(iQ2N_LtZb4l$wIdb&PӥW׽v.&pMg׽zIەslׅo,e嬤K; U rILuF'N "F#'"MI2[~u.1LUXR=RBD5ilI{OIKK 7e$ MD}L/KK>hX9Y)""X#؉ Y|nj$oXa)  D$ p׳iB.}ʰS{9/cxf\]kc>;v~ Sw{+z뙔Żvϕk. &ʚW";W/. >>Ť{x+d~G,+0M>aE v._w$qD^|euY'Fx뾈.V0bfX_iGޫb-45S}s+gO`{*|!_쓞{Ⴘsq9wux\4:Wijן"2~ KۛIt6iR"Oz77_I}WOф '}4'}BBS  MM0&Gx4]zB"\1 =dzt']UUe6 c+ғ˒:tСcβ?^GDBUqW7&+`D\ @r|ɫwL.p+`oWWg^n`G?U9& eeV"⯞{*R""^8~NlR@D\Չm=fd/؈lw4Zvޮ,*'WqDd=zg*02;{o.3IԐ[isT6V54,_|UA.^=>؟uͧꈈ/vS,xeϕ%>>H\^)b3ѱ[&_U,̼3c[3spuRGle6= (K+'=csӫ<_f:"G,h0OH/:]o= ~c'FKΤ_hR=O_p>wސٴr׎G0Ri-'p|%o,plW+bn;<:/M4}q#"[էW"oǵGn^ȭHp3J#\үmhhb$̑ŒukW|DϹgݚ}iKr$M416aj~k rAR |i[nxH**}m]zfJ]`PU>K;MYs/4鈑H*?mhhI[)~k2W3}Ru-9$҅뗇&EW찘?zKߤOva?6iK/JHIY,t҄Xvê2炈EJp>unjaa*vr®L}\;SW:W.F/ƈ&n]iпF95vK8/陱> WUM#rRʺed'ϕI ÃtvҲgN]c\)4:aA~k #lqB;+PF'zcӽ=ws$沰n-t_нכ=?֣{i-QÑK:sN,Ow_'"-|qث_1@I[w,v'݋CF'ڠ_HLmq2Gx#V^zcq+W- 2Zu+/54ƯYak2_P?WE w̕I:aЄ_x-Ԅ-D"z7&_4FHrKxqu$;Ӕd>Jq1=ς5~.>p%=ĽD\ﱿIDw~EuY턁s۷Ayƍjz_WCH\63vo'_ٹ\/qόg; `_^P:Y.H^9q ?d+O/^w#Z-PÃؿ631`4GO\.7K?}։͖kgs~\|UNYeGE1x&ޡVLs 51Zn:zϞFfgK7Z^bq>`nSFSTU"""j8qo:f49GT첎阭ߖVD5?Q ,՟}Sui3)U#:qR~?FN7Q5;_ٳ957p:ˋPA:K-u+o)ܟ={ӭ#AytjwvYSk%9?sYj( Oc?1JrbeYw\^?ͺpz~}u&":p ,wDDGD7K3K;mZG?H~wg_zK4f/&'䤘RZD%,]oʃߗ:_۳youOu$^vf]#Em,DA蝜XPHQO]t: t?mΊ)."Җ[DDZ-eN"jۖ>¤P)n~yW:Y}#ܧx;כZ9Xք)<$'"gۅ'#C*]nV94бN=T*g,{cKWN`N}E1m>x<3֖[hDqȢXTw!<"# }pj;uvʛ+\B㐜< \~2⹥3s%r2.)KB9 ?y4ʸyx`V ?@~O{ <ӧ#0@?'z]?'O!W[[O?#<"#0haG~7o޼}6Bܾ}yX#<"#0ha"P`?˲O=q<,޷EvDvDb؇xG#qΝ۷o 7j5".@~p/?Q!L>QGO'w' ?@~O' <Μd֯$U=ei燱+-r MX?MxgU3Ν9Ey:g߶"9(Tgs&yAg3?e| rAgݼ:a^io؈o jiwӋ7-S6=x) #݆LZ=xJM"'ױyPQέu7gLҋ9rQLYJ;8{zMrQw[޳io5;ulX(oqV#%UW]woLC5]_=f/g+K:Wn2777w}E42帡CCm>85p3;&GInqrD.lj9-& ;yij#n*Bc"ۻN.#`'"WSZo),9U_ 91eж#?e)ԩSn;r04r%Q%ךZ>QBD4ʳĕѳӦ""gn_[=ݭ^iJN\7@D&Bhhzoym{eq'7 tMlz);68WDeٵ~}zIpWގ{I`5a:qJi_x}ÌhRO3d>wf{~Z`u LBO0m[̒e49;j}9_ uw'>O[ޮ{MyU ^[I?g*~ޜsw7F{{w"^}¦?lqle@H//]MQt,WM1EuQS$=6sޯH2on$y3׶K0=7=|i;랫ڪ亝emڼԇ^}8_m ^u;1a1sH\ڄ٣Hx_ޏ/@,Z;Njψڬ uMao}9_$l$)Q/0sw2v5K?,sSbܿʋ|c֯/4x^`xIڳň~CZ~Q\7Û_pɌn}qfj8~^^p__;d8k#"jXzұz׶5;icz7j+}Wr3EMm yo3D/l29Y>}士l\a%Tmfmg@D_ϙ9ׂy6:dqL J Yߜtˆ5gx27.[QʝWzJ7 Mv"kU1rg[F/5|j{5m7Տ D$4ۉ1X↝`Uo3x踵eez[ى7| wf@ 'DDu儧x ϼq(!Ih&bow&>za)R]zgZHD=Sk+3+`wIo:I_$q/Lb%NZtm,R$6|=<#{ЧK}+)-l.:m׻o%G70\A,e%IFK]?G`H^zmήv]uEŽQW^48V0~gN'}`kt.5|f3IIUY/!QGΝ;onkkkkk7n ,O 1g1K@ީ>_^K\ش>.|M_W~uY.a{S;qy.*#FGg2O2sPƓW}|-96 \$Gkz;\]3d/ԓ1 ^pz7f꼤;ǼW}]_ynre1Ct¥kwWI݇2]K⣼J=W#'0T ?'O pךyyFS5X rky t%d%mo34DT]qXޞ6WYFhT*JL?o,.(nVo3gY43KXW`2 "i@ W 28UK,n-o ,3 Mj"앐gYv^1^@7"a!EB}K̷Wf&[D*b:ЃXlYL:jHJ;#oHKD܃z wtY([IQA'=x"b=u $.M{o]%#oQ$""fe,d3DD HeFH+b{#V*l6[CG&Vt!ѳ5Dmmὄ6i@Ir NKg"ΏINw~#YhpOU EEZnD5]jo{ n5`ܤ`tlJtǮ@E꼥 X9>JEd6Np:v(vKy*J^qyy]:$ qI OWl^Qb$<:J:8b" qlmO=OMF!Z\9HUۓM5^ZnY V0wT&RXxFi_6w9, ayEj[#4D9M "f:YqFc]OD~Q15F^B".(QW/Oͫ'LU Y95$; H^_PZyPx\V,DDņDC_cV!0~GKI,h.WOPt6]rXI|,#0,ȝ܂bf IDATgr!bu4B }FH5#2}K1<RC?t!kCʭT&'&/pD6u[Ԋ }k"C*JGe{  TwӵP}#լҴ!HIudd<5jk j `{ GVmOMOHGDݻF']_Q,%~26061:8'"Pg zHyǤE$y"i@Tm)O<~;wܾ}MoܸV怀-ypsbC%>4O}EXD {ͺԀ"? "rss- .ߣk}pssI <);vlcco޼\\\\]]ǎKPp>0 nXP( ECwP OA 0~O]3:b' ?@~O6[RT:JzJlqթ^dע)ܘ(p]yō}ќ]0̫0i);(ɒUT wh7ΕHJͳ<}4'jT~W]RCrJki5dO1&ӧD1q @\1JcB<Z~ى)1Z_"$Tk9(?ƙm[_R-C4?[P.h>㨗WXU$$)8/ڍ7 -z Fz0iy&;Id+/4@kVCOȥd;7hʳuZJR#SM"u*6QTHCq)5wzǥFSzペGp- $<2222jaYc1Rƚlթ*Fm TTTSP9ǫj1D ڐmaZRRj7k\sN ɕlyKaj"F+7CդQE]sbJnhu帺J{5w"FLqFT__1ÃۗN-qA}'`{dK q\LuNVSj||:BCG o?f0;6$PRGwe08sdx7USM*\RɅ;jgօT@.[_w pspJ 9I8sztJRGgsB?߃۫ӵWh}MI~̈K/&sӽDhufymzZui4tǸRߧZi'cVSMކKq?)8RH$""3I+DgSXrZlI-Qmvt|NRdJl㦉T3d-(YKL6bab\+TMsC@+B+&2GƦfMZV )R NܹfpY EݹmYK[!'"i}}ۚR⏚YmI\Vr,m9%fK[9'%L [K7.sWu+YbLK" + <уMP>K{iU'Ke֯ܐofvnYQ{nK:j58fIy "[Qv&!!H*ԝZ$NsyƂsӴtn[~9 ̾-Ymě}aΤnmOmlP06$X_"5uX*%}:#zQ33?)--.v.#6أM^SbKۙ`/Y]K}Vh GX|P07m&X#i~A.F" h4b"[Z7*JE'fdb.X1<R3$OC@m*%lh>>zLUpH!eHI=_ w'[˃%"GS x""KB~1GJ:L{8z+"lj bqiarz{{0Dzzwe1Uy&Dj5CG~T]xF[3uouR;*l$JLjcD>i5mLf\Լ"c+DjBˉl56KDaQ!~~(_j^dҠmwpT;Q]RShƆ$=xOzkI+*NdzRo&q@XZJCҵ"#Mxrδ0i5_w( !9\ O"3tVh.1 Sm8t:|}FQ݁ƟJ"bX[Dē@l9_ADkŖWdF*fݍ&"}5,LTP gB+brmV,v 6SNV$iD5%]{t@Y)+n"F:TWXQ霨üD_7QN$C@쬓5lXBNs*jkm%?sJl^q޶l^ HpFNr?\Jdkq6@$KE Th&ǝ6alG5YYWWSkz4!D5ƢjdϪ:˩"#,H]Κ } =  DsEE9ё:Cv^^vzrH1^^d2C,yF+*-aRTT\xG%%2Yw{Z*$FT2W.w6B5F4<) +eE0 wiY %KO.,6!a^tLq]Smƒd/J Vb+n(axEM [m%Yѱ{jÃ!*,6e]3 M}#(!2 Ŧ<}F+=R,T\Ou2  X h4&X(&kyIJ Mmۊly-w'2LŦl+LϵO0p?;X3( n^klDr}+-JA(\r`EQK򸹩E"OV- gp_h"6t&D BݪP߯ęwM}w&rڹn &m Y띻j'{k?ܻzPu)3_KL|m2uW+IH"zZv[~8GO韖f9x2K.-ZG\?oLJg(̺  o@xk_v`'?hu㹳&f{rxr?n9 \wk2\O {x@>@>@>@>@>z97_|vbx":\LZ? bh,'bP}uf:.,ߟ7OOd-1C<̟bsgB>FN& 7OϿ ,OOO`*ϏO{{9zȶʙ^:#߱'zm- i~jXy@;hٺ|Gjˎ*{a[FGf۞tsN)xx@i]EX`mBjRDtJTբZi*T>YԚ&CQ&;)2[^\%OGjz'$oSs}c- <:'AE.wK}BPGZ> G) wϒ""ǟ+YoqkSrc'O lx8`Cv7u\s#MY>ƪ9}|Cw{1*-ϥX?*6W1flaBRwoP>[9h.M@IzgePFl)T֎&BZ,ߔ|<&i68ʜ !R"k`ҿ44tL&R%ϏUL:9"VMI]=Xd0as4Gǯ,ϝޞ+km3 =}|ܺ_}vq_%g#2}=hzSbெ?xK/ۇtȓ7܂^Z>0R*_8*ίoJ7M_ljpU:o_}8!2>5 &.T`0neNw8T]m$5ȶT666䇴UȨOkj<{8('"=,D}0E3Y K#5 \uKW֟iqD4h'w]SWZGEV׆䫭sf`w vٕx!KQT\%xmbY.~M˒^~"y,U^HLD\YyϹ]ֵ!r=3cr=z('t`tyNj 䳭?ҥD}=Eg_f9Z({ΓH̾//Fb2y3{ʭa R"ipZ^ϒiWD$V.:$ThD$UkԌ̓[2M{5kWD/ و.ٮ6<s׈nx_}.xJ8nN>!᝹yxz:;0"ϑX襈|[~bh𲃴Dpb%)lE{'*ΫX`̭EV"" V#'0ᾚX*%"a⡾leΗ Gj+7 afFHdm+*n3;w,<'V\!И!oG>Glw0Ɯ/ lRɑMfkdI! HYj3u 1Y"sH,UbZMFU>2V^Xcvd&T|wpF!"햣Wp\yx\OgJlOD~c{Npܧ':r:{2s99m ;Ѡ;~ fYfTgA WꇮۭDw,s5RlXR&&U*)"vYxcyz˫Ī`7xܭZƽĹ6xc]E'*["q8-Y""FLV~66ExF!'+YkiwV߁{Nyc*7sTj%"kKyUHQh "U4TY v^tbUU<Y[[fTY mXZTjTIfFug-&+onݘ^gɥbem;V^=7"dbd`&"h( [ wyQ_ڢߚ]e|@!ݓh0z.[s׊5l{i]Odm+{-1 VNc)R&N^>;g\k!M[ƴD$;=wP/|=H5}mywbP<)rM$U~;3=7/Xmi""zfbk;x/"rW!zԑR?"YkGE/=y{c܈]g|Ϩ@w\&ww]h'?Ϝ7dk{o<`y>v>K}냫ͳwIv]w*.: olzxG#'bppp``p8AxtҢE&Aoa>>G>p SzQfaC͑<-7:),{ 7-U܄GL^͜Q-Td{|5UcDA0 `%&Mf?VQ`PšO!uRa~tR|:mVv(wt^5i.9@>@>x$<q&~*> Ly%Ya@W^E>5n a wܦM{0yg嘞;Uk8 O lM|Cg'|FS LS˗q#ظ`]||''||''|IENDB`././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/doc/source/user/figures/deploy_env_2.png0000664000175000017500000014673100000000000022720 0ustar00zuulzuul00000000000000PNG  IHDRsBITOtEXtSoftwareShutterc IDATxy@?OB2&, "hh- J*v*xUz.poix uW+VN["bZQAN!$ L {ZQ?ìOgygfGB!r TB!zL,B!I!BaE!B(B!$B!SKkjTJmgC V(Z5}\N7[uOjdRZ*Nr!B=]w[~tvh5Ш4iT}MھbhZۢ-󢖦zlml!B qD+}X11 VN`2 &`0ZVk6a602zA}9L!BO%Sk(ښ$`0X,6bFFFLӈd2 `0FLP('s8RUJɲ8oB!NV Z&V0 huyL5VLB2_qȶo2/"BD`WF j5Fa0z%-tGQ-V=&al-H! >^QBCVb1E=$`0Z&0`0Rҽf0Z##M50dqѣGjkL$ܻw믿xNw}wڴi̮~pLy]##e˖O?tِr;E4u7>r }ƈ x11lRgOCQ\Տ{o'?[Məw,UN8M3'-]wQq=X\!d asb1~$ _,t= Ʉ#\I2f,X0sZ,Z2RՂVk16F=+5MLLtdڿ|HȠi͛9r1cĉƍ_---5o2Ft}-NV66Zo2'}m39Ee#28Ư,_O6_Ad&S<46EC%V+H+wf߸`l|-|?,=j*%4b2L&S&#́`0dv7|*jܩܑ+:&#ٛvѣfll|Ҳ2///^&7o͛7333O8KEEE bʕ666ׯ_?rH^^Y,tvvo3f?llPT1>תTZ̓R_T#ՍqH`0FF;yuIYCdau$*LNO>p:}=W±h`~Sd=ԗj/7Ot hi訨#IRn1Gv#$7jUψ"IKw%o^UzƲƾ)V0enN&>*[CB*"3܄$-ߕ:bwʃY<$ݒ;ZT,&^oY2J c–1iT/D+++ɉiJKK˻(AZ7077ommBzǏf_޽Z>?fV;~SNF.Pb4`rw5mZ?e`jU*LSF3|F[o^Ena#|ǀI8o/phR/ v$ArhժbeFvYL v\%>@oih1 "r@٘]*H?!8/5yWq#(822*WJv'zUTcpcV_8Wx@<퀪ؙ[Me#bglBv5M };#񢊄v.7uR sutJcy 6ޑH1Tijͻct{J ЫVv(=jUqaqA;]($$$Su>Ow~I[7zJhGL8Fe',km.]J{PW%gDĊWou"PeHo6Vi''._#"C aS zquvH6NHʄzW7R $iq^jb @FĬsВb)wzLgw8u_/Ņw$R-"1G@O-$We^BaIzzwPMnMW8EnvmꯎHdw#-Mg'dPK jlSӁ3ܻwF$Icee믷2 LMMgVhP*t}!GRKjX8 9zU"?~=EN7k&&gw[sMXq z& w6#vB^ >])L ֺQ;5G8!4*`'QJƤ %ѱٛwe%}7:Jsc S -]x0@Y+ 8CJV%$QR=R98wShlHQXwhlj(fa+-)=t1L ͋%#3ВolnߙMg|(mtv]'H3 nghx~ǃ\e${`@Yaum[=zXʮ#AF>DnMTi’]nCq ߌߝƧGgN@Ug'=,g;laANs#|뽷~Ƒ'+G }wJ%S֑UKMfm$ć,2ev[woBݡ𰄝7Z&|@&M۝ 4qoF`cjߤ~ jT&T]ve$D{\HR_FL󃂄 #n !<_I^^bJyS2 -ش$PFLfݷh%ɃIg>%)apCEǿ%_[Zmη356R>` jFϲuM#FMF:u[ZZtm$''ԩSOi,d7194|ݖd7̥;ApY\hsvaS[,]ptcq5m;Mbp[+$Ѓȯض> G;lk_rI@UG=EP_OXHiIFA`;Ж*-@:O+7oTH "F>\\?qH TEn5S'XD_Ӓ20[HK6WJ9 ܹX&T+,ZR2()$&Gخ+4!#H) V_&Ȍ}~ԾzkPNN&ÅQeի.\zB۫Q ?|ץ@Z]8QWC{MI[f& Q0HelQ eWt}~){ H\o>Dccƭae";aW<7<@.JmIQp>BJn">K}G{=EbmMRwd@Kh0Mhi)kzZlk{ ;z1s絠)h jEJd0x81 (^@Sk[ZAwb2}Ϗ3޹?U\h ƀy\55Z`7,ђ0 JΝgװj &DXF@Qk`$IlZ*U߈Z0G7UlӮ/n 5p+ A(KS^OLM (1<Z@8J|cIW'G腻 4'JWdSOn`<%kWN/>MFmڵ_@}N62 %h^S& ^Div߅hJBA9oWme4*#~1b;:=%"TJ>Q}j;t{;]W@v%Uj*C y( EJ±+RTS$|7 "b VϬ2'*ݰj0PeF9]%; ]gҚ>0ŴT]O@B)@0JKJҖ],ѿ&Q( .mM$Zlջ3֝? #1"H^3x^Y}"设Q{MW %2;P@ HgL)1HV~qgzGp+od`sw#t4ɞy0ɭwMN@ڻF/'Mͭ }iqvh5YyNn"a?I}ym Ȩwj{N߀`2؈ib2i_<b]{g[[V=50j(R>uFs5}*o/eJ2tvv^hM͝`ä5Tůqd`k ַsK%@e׷CZ\M ET ݬC %sshJsKiGokB0Z[M-Ϯ'=|H S##7Ƭ&O.A [c[ZLGiR)]{8TT]u! i%־I Q";v.;r٠ս`P|8*&䊇iICz?e!,mF)U`C[ "X D>aYZq"H]31w꘍X_DbBد`AZMR ݄Ҽz Ň QwQ}CVQtV0@swow/d֨@+P\+۩F g0oߖd|>?//<<qKSyG߀IHȤesRi$(/>>!u "H~3<Z"|~`b$!v%s$yɩI@@䲼Uq;}v xy|#R+D"Us( bHUǀ@ONou[6/OޑB HXu?-CT)IHX26 "Fa)>x%1:vӇh[oA-4P =+VjHqa2;32195tNn1~||^X&sUlr聿֟R:bҒ|u e|Vio`[ =hkDLƖ'6Xg'!JrVVR+am 5ZHUb;4 ZC+Zﴶ^L6Z[_1rϗJ˖-cٺg,q\\>r30 ?vpF:{~y&L JG͕$cw[5\5a`t\.i Lcc%l^c_Wߍ?:}Fըwb_Y49~`cE{HE‚X"eB/&iױ7dy>aq>x_K*w &hZZZ߿.;h LÞTݿ_ рV ZHWL-(tg&nvvvϝ;Vz;U">0j(#F///q8999::I^^^PY,8|-wXc^-0vqc2}T\bj5s){8˝Qt*~)O~h%ށ ȐeoM&IORT67cEe=a!ǔu4vˊ1s#ómH^+]' s/-MT.dMǍ*x|1 j48Z.gU@+@%mdJ2ل atPZTKw2 `= \]ϝ]t?"rwBϐ^!y1?8Q.kieyLk-ihdZ55 Ђ\'=\|Jyc(B Bm]ʑu/Q=ڵF-_z(Q @+@F?`tv3xo#B#I`a-].P@TF{.jZb;Ӊ= !B=GahoeWnjX;kA2LLF abmderd%B!Tf+W$ #c)M wB!T(B!B!BaE!BDB!$B!0"B!İnݺB!A=|,;z?rgj4FVJ{y,"BH(zITVj!B/]e٘3ZHVN!z)(7 ) h(B!^$jdd!Fj]@B(2 lEϊ.EL&~B ѳ]DB+b B!L!B(B!BDB!ЋD i{鞂łB,I!BaE!BDB!$B!0"B!I!B=XEEEX 蹦VJF@! &IFOFgg'BτVh4jZV4P()* ,IBh266:brL:=9=|.Y:}?{O@BߢNFFPjSzdI^dƧd^ Q,7lL|.WҮ<1;0VMxRegj6drt"9u".baac_Oʷ~q~'(nW[;rܐJ/tNsrm2ĕݝx;^~U?gθrĐ͵7Uj e>> d-5I~=LΝ8oqߜn`[)=pF9 3ZaUzx@˕ e۳1? XIIE'>M<3`%KJEJ?H>u'IpwߝH/V @zvGl_H˼cgΜ9s"#@׫BW!_I^3g8qIsSaQBxB6H q^wQRL&uwgo\? p,>nz3z(GLz^xl´Ƽs\|K-UH$tj?n>O>`thUTu^O+XlսVQȺOm2Oæt|pϡZFxl-[zׅ>Cu9)8d+?xY VOb]z9<^<}[6(TuIIVM9e03&?)j`MbV٧kɆ/iTxNz٘t4xpAUk'W8i@'ygU*8K?lܰPMCM~N&GMʩTће,4 Eum@78?Å8Cb:?]9y?}[\|b*p?pZrr  @Ɨ/?rf|LbPHR)G~4棸D1V|P-7<0V܉ \{ҾquQԝ uu? ;{]>A,>Ӊ6ЦZp?>UUo`bnͅNEOŷ"W^[̛2ϊ ͽ*?qS^\\h,:;{kJ{c.,;[;uKs6ҊdlZsdnysy[>`b -^euv*{ש\Kvs8 Q*.!LĄI3L%;3Ί,vd_PZZV?ӂ>&erv^b^7-7s̑K;7n_>ioCKL>o >d_%7*2ω%ҰuM!^_Ғ3iIe H~W~$I()EDWVӍU4= TL !M|>R  U5{_/!^a@|U8wT,U0*:u) D .wx㢥Ru0Ғ/SMiIvM|$ %QY,l(e[-ޞ+.%79A,c1@%WCHC`f +Y=[٪ǘZOnSEZBgk^y?P|4=sLbM$:NL*eAG˕3W:̝&L2]Ћ 1S `߅`êUSY$\hܽ<7 /eV5Hn;upĄm2X m,z.c>ZތIU/8pXPJk|kgU3/jLI4%I|VOT~Y77P tezz'.@C悥?#iC•MR)Do O梴-ĠƟ[y }=wL;:%B[Oj>:%2Mr׮6PT%΍$ {:.'tA+e|>IPMQ~y\fiɼ?jS4h?g :ctR\ǫ&&,>zBDRVs>#EB/'LNJJJz[*q{jt9@)oW?`L:eqdg####)PtYqilx⻭}lꫴKGmy?sj5G`9uJO酓;{A*}x'OT+p`C"#;mC갴m"o̳M@S}BѓU P4;ZGN!$ _WgT 9bii6}"ijU E/GÒ.JiB s ?f^fJKfN˙ڮ@9Kڹ$g/T(LاOL'ŭ\ug_;@)Zx5ncV;Gn\ï?y 2`/ VhjZiZP466?''m?.3@%]iBOͭ 6:!^ #FOK|53xMTC <ǁUkۮ[,[I|"ǁ+>oWp҂6l0%BgM3xw+s@ R !PB;~sMrῌ ,9+>>F7đ.K>sYoBצME1I ߋ 7%VBRoW33/_zçw!BOó7DpV0B!^rBoIJBG!BW"B!I!BaE!BDB!$C}F!4!BADыȈ`(B!40LccYXRFFFX!XB!&QB!I!B!L!B***R@!B zyy1 ,LhZFVj5M !zI` r=Sh*BKjTsqwM\ȩ)b#2K9E>]%Q5Q/a#N^[ކBρGbF\'8L*^g;T,XaE=X !<`tvv2 ,LhZFVj5M GK66ѦA54\khHKgWߐW9p ;k޶f mK^~"ج?v|X{j`8{evYhm)˛i焂~W.ۛ]RRSF馐\}BzvX6,;b,kv*,l3 A_:jҐ_;cr]aAکYkh oo C5ydz k)ZA>1`{Y;vfӂ>45)g5خPfkx|ZܵB !_EpCNm+f5+\y~_RXm_6%ǯ= GQ%c;v]I+7Znʋo*\[[[mG8>)!hM!}*zE| t[CykE}cOițŜE’cۃn߼"JbΗ4۳b@[9D|Cbg].MknXO$;$b±Q}nG4(BaEBt]D>#,]\ǚqmKͭ97>%('nv(DyNOogi#SR,%Ps \.*mIgNq"崐_W+Kn}1?++69i3eRc[%M:r`ɅJ:T8|='wmxvX.ߞdIaP\K P9`,w87./=~$im}N̢!IX/p,ů' p];;h+<^EjZ;Qi7`=~pdϖE})g>s M7kVʊY q;mr/T~R6/߳}Cw?6xx[ YwgY lUݷ|]IH|L8MȞW=vѼm P~Ps䴄o{WY!qsۀ.Δ Y_6dn]Խ'޳fG ϧ\ZYq#,ZY. μ޳-ۍmoVVBEPd@J_8{.P]=VBuʁL ! 5]ѫ%w/u'۬O\1 hr' 0ES* >-b(p&^64~~u,0n}rW\ֳʻi }.(M$}~G~!c;r}]0oIJ;pT*X>$d'!;B lE//gO!sKofBsǺZQ( g.Tm{÷pyyen`M9Ar au,Ͳfc h69 0o߄BDS,}?!enk;Q^BuTµzky (9W*i@GSYîSN=ғ7rEO7n {MKɮʒʒ}o!I5ĝ(9OsU}O5 F B[6=ɚyWsΊ;k¼%]נ_=o(BDz w|(+>@C\}4Mv13R{a(7u Y+Aq|~6h;6i챸a!b+u7 ( e@ÓȄjĆnk֏47fEDՏ7mqF9xmHܛE/G!0"4qCQ$'cr5*_k3H }݋) q0 Q?zT=w ³jKrPOKn$|~ymCmVܶrCy);^@Bb Xs&.p]Wl{"q36,; v4hJsb IDATqb`fm畇$j}-?ko~>~`5,͹gz5#hySMInVVI3q]|4h*7:lXͼyOx{n| Yk.N jo6Q(ՖMxB5IK8ڇh4}q,5!OND\=[ <:撬͇z]/>_9,G=rN%K)h>/<AVf%o^e9tm~*npQk@τVh4jZV4P(C;r>]FyGw<+;Zmn񄙍ǬE!þ?ywy =_~Z=0dߓԊ7ۖ_Faf{<a<Yjn7wt0wvhXlE (VIK!zK!B(B!$B!&QB!I!B!L!B(B!BDB!&QB!0"B!L!BaE!BWUu^oo'?yCȻpFљߔi_IMG!BD,JGyik g_|vdPWK';S5&3SSv(&& B O*wFf" \x*B3./pWy7%ER3; dBSy|B[|{zTTe T`$>v&-yi[Jv&eT B'QYM}7tռRv]llRW:ay9;{5IL6&=f>r;株/kRA.D=ּg2^*-%2 W[`(B!+;Z;Hw@ӑF隷Il,yYr3¨c㚺p/H.?Hi m%jCW&>ZZ fh7b?5tmVbWasƵtZ㏑0h &Z jPp/AR磏Gͽޜ{}sB3'˙H6YG9$sŚZ]$:F&θ.u[H@1"BBBBB-?|9rw=sLOOOOO;v,-- k!ybf?t;PB!Lߓnv!&yOB!1\?gэ#\<|.)"""&&&.. )B!uC##q(ܜx0I!B -H*FwBE{{{LL u!&&`E!u O_/"##u,/&QB!tm`E!BDB!&QB!6B!nT>J"k(B!DKKJ]bFn)5UWUK⒩ӿ-rvzi4u *ڜ} oʹlJ&DaDB!t9鲋*봭)).Mtk qߞт՛t#rd"'kYFRItΝ>B!tq%Kkj Ү54dnaLTW9K zg3Z1xʗؽ|a&6\qYs L3.\J7:{]nde r|LKVu-ۨ6ks,%)T 5bvA%wn7kk,NeԥS9%%Uvdu8i,bhjx=V\W4lha6?^YYӨλ&cKD! cΝ_4x5??8|;NVǐS]H-\YnK S:4BC51夯Hgj+)_]aJazlH`Mq]?_grC3 ,.N.'hH5i" =nј겥ǜ]TR(ki` &=k YmIdϯjK"'zF RŘFWZIKBuf9W_PUe5[v!B7 xg8$Ý&d87eyYr *n%M))"ߵ)K R0Ϲĕ'H/lY') 9%e4hV$g + o(H[g@LIJ"ҥ@Iش ) @5)jXJA'B4#/.Ҕr@ԲB2R"Uo*ɤKKXhv0@)YcKE&m"xIs7N"$H&[wE-=,4 1xXhs}<L!j^ 3K] _Xalܫf K(2viX\Tת%$4d.9BxR$ʖVW_I&ͯdvt"`.I,̺$% gꌊA9³!9I5ZMlpΘ_dD8Ei9`&~#R 4zY6E eyJ1pn^gv6)Ӥ5`RB)vXָJ28-jy| FzF cyL!Bq" RC^MVeSSgV %>X9_EӴZ(VKȔ\i}J6z D%fN5.QVTқt*UBvMMn(`/DYFmp U1K:J.(#4Mg=)J95)ӘUZ3r*U&c% nW(X7>QB!tm`E!BDB!&QB!0"B!L!BaE!B׻>ԧ*?jYuoybөQG7p=9)oM:uhwwXA&>ywXBFU1uvvFDD\$| ¢z:N]_/yTc/F}5 JcQ9 c(B!tioo$z]hoo I44;Ӕ͵VJLcF|n?r',N墹a._>Aq]8(qʃsw8wv 9G=Ouoy][%ཁ>Q-;)sJQ{dղ֏wO)ғ"tmu8UsB!4577#)Q#11'b4u"[;#x::.ș?n{mOMfCƚc0i:5mMYqm?Tڴy񓞞ʸs惧\8.N͵>5YNf۱{ᙜYcS3 &۲ȮmOl>>%oݳco!uHLL|]]]X!SDDDLLLbb"AW<Zo 6E\OO`bSgǏ['Mޮ)CO2>XQ<A$:(GF?sATHQٿT`ڤ?V6ľSҒ"{>ӖmN@YԤ@T#:ZO@Nu۴h].skʬbU aT"H$凮~[횔߇H%dTu:0!j; QQ'%L:>nt~ب On-!`r}vl9 ,vyIxzcV /0BD~zՏޱmѮDY_=z@0EDb犈%k Njr"'I~bmwm#N:hQaJO~3#{wXZ?KkRch]״rR7U}QB]˽h1{sII$Y{bȞ#Y:~P#>>HR2Px}:5^ohhp=:W}3sD? qPeJXoPwo* ,O}XG@%$K`k뺦e?6E$MY=k?:A!%u~nlRnq~$*f}|$E}#8/>0cOw J ٷ~u\An|޲;)M]w`%0gP|4}m< @1SL NU>jCsx|DN1PYcSW֚}s[L -W,w]G|=c3׳x5u,V!&Q|}i̡6]~&.(^8' \:Ҋ*"mc5qQ}^mH|cM÷?i*DL1"t "`6|:LZ 5<5hSS5c.J6CvuyOV^\[ܘqѵy.@QXQ5e˗r)OfzOW3RSSSSigצ_*qj[wX N<6C~_+њ[w5<1kiq56ߠ]hdgY['60Kkc3ƾTZSX3SSSUؒ1"tMPi9 0&,-&/\\5m(*-f3hKfI Kӄ5ܦ5l <R"LfMj5ay@W̑2eeWHZWU9!E[#zp`+)`4eWW.*}xjy!Me)Ednr6؉eeKL}EAZjRV֦~i+,[7Yorx ٍ-(Bא4K!K+쾳;,rڲl\Q&]B5yir մ&0M n %/9 nnQt~iLf͒1*J3gr6@T*",%9цkkWkiy&#qxyR"R@JSOvryA6zG%6APXDTSw^Ea+ YRQrJ,9'VonPt:@b&Z5JcpؒG7V`iuf4zkqԔVX.%@PuTԒ]^ ]+T5Mv[];B/gǎNp=(t{viH\G)]:%S.єXH8:gЦw+yZ\ !:Vttr *F~A~|#[&H*mo/ ߶5]-o;a%V "Nb"`錚kvR& !1t&DCCC~W,3)}ϗ}{|˭k;c_~nP=ݓsO'#)lgT|L$ , hk::tn]k9i>lm!B]hhhXXX#"<<<,,,,,lk>;Ktnhם%]'qC=ɒnb[ -mжS|?l = IDAT۳%zlZdZj̮͛LZ?HיLmur놝4#z8f5K-[.rZYgLoÜ]ʷJ˶hh4Y8< LLMCGq>;!uBηD{o_G>n;کQDt] Na#_|`FZMTLcns[|<DoNx9n} RlyvxyaҼ^4۴WVok/p-mĭ ><{<.W4KͿ^k&.{,0dPj$o}Us#6Lx#ymg D r ̞f2aۋ,$ջDŽ$μs91ԻcN/{&&E qߤؖխS'pMoCfZv7[\^zl6[Ӱ7?_z-_qE _aKh.hጔ&I{xzպj~A㥗YCח_jn+;X?zb7Լvg!3U~ikf͖?.j{7o6SIZ=f֖Es0^(F`uI5iT[ϳ«1luhl`.+6MU&9fȫlR*'\gnK99 \mI?y2'R˚L @:*ec@<'zoKKpЧ$C[vô'ݠԅs60DܡvOK @tOf{`csd$PK7Pw׎.z,2a$[ ib,06$-)fqGѬwM׸d$WgKIJNy/lLX&dmmJui l&=eSGg)[g;#%Cw[9E.dR#v[],HIT$+e$!RHZJ6mv59 qi nss@$(i) @"a8}[%H2K8ء> B 6F=⑭{MOYI; kqaaD_DGy㡳4c\TLTg[O;1KBi O8~ Nwt-y%=?,jHmCۿuia\,>Uцo<{ M\Ɖ;~yL1 gW5?vy" !M[4߯s= ~gĜגi<$%~!6`Zy?Lf  „Bc0ǶKboycbsbf޻kcKi"lyoU'Mdv˲_^響fd߀iXZgZd~i"a-A^$ ƾ@,"93o/ɁfH(`ĠJ\!!3; %s'\),oz'p~'Gxn!*:<yTŗ+єx@ ۍB![a>Ҫmϱ~'b>JgL<آwɋ0Y`ʭV-˒S`Nt6yeu.Wf=fmK6;O&7< 3^"P:{>Re ̍Npn"SYJ0,ӷ3< Ha:GHxBE(sH ksu[:b~Y.+ vU#I;vtYfΜy]lglB\'K}ި rNBدKe}̉hFF;9%^[yBpܐ\@8b[=*x B )dœL]@5_(ILpl 03ɿj^6e~s gҷ$޾p%<I{Cb`!'ʿ:0_ p\LF{7~@BS'y?K! g"8`¤U\O~a* @d)Ut_1<ÂH&Xy!DZ^I%$fs O"}4Mk~PdO١)\ZYyL]ѠKlUxVSbeozj 4Yn ̷}$ѧ| f7P(Q18Z9Uɴ\ٶ~V"Cj4YiRuJjc-ٳqX*4P6*nE7^Ms~˽ao×zli?/<;V-#dp}|0Љ3nUA]-4q;N?o3/<|˞|aւWV=g_/\ةO$|_Q+Nz~rh?)aͲߨh"6aڢ&[d9DzCoQZV_{(_aң輄]%*r miA^|Y"J$҅{aI~tuR:_?&r&]SjhZS,)UʵԘ=R2V5$@aY^MSmI)eRd'W"5yerr(єUU). 'JJi USme3@TY\&Zc(\#4y{&Gktʻsv \+5/MGQyΡUZ "V<*w||oo/s篗3![Uޙ/#/٦?c(ǜmVVgKy-ģC!tm[ڽ7MhXeq({v%З8#tyI<<!B]DB!&QB!I!Bݜڎ?I!B7z(qoDŽI!BW]ttc:;;*F MII?~'Nr-#[#O!BW݄ ?t:{zzB V5ۻke͛q ]Ə2 q=:3l)w+tI$K uzF-a;'OlooOHHL9H/~?<{]_0"ẠRy LJJI& 7Op+yB^_sӃsI6mp3~vI`3fH$ ՄI!BW=˲dرx$11111qȷ=X,3fL0w%QL!y4M'FGGcHbbbEFF`(B!}A~HĐ`%]'2رc;v̙ HI!BW=Xhh/1( B]T{̙3g`mܨw?9O]߾"9A/^3+l4^7x|HHHhhh ]I.޾B] vqs?~|BBB  yB];Y#1"B #)&;^@/%bE!͠Do$zY-aE!Պnjb{~L!蕸(4R6]ֱjO&l9'9dNyɃ޽pz{߱~ަϽ_ppѡƶ,y_x\tcs_fϏ̽?:Gm~/k8ϋ<8fÿ{~\ںw*B݄)@\uP 679+M:[!AWjc ^eYuv1p~sUyJGnGԈf; 674j$bhLՖy`.Z8`ܵ8Deٹq4)mfTY\l,zM_bc* nd5HK.uV/mz<P[WWWt̞ZjsQTerI YbQFyU>Tp#ݶIcok>q>:3婨سM;|]ot gc6~zǶO$k-!+Vom !NamR=]?oOZ6m8٭T?'wlT,y[g&TnL|wE}EpDa{?cۧҢD~nܩ!t;z蕾vD$ش ) @5H*4 (R:Lrnk#ȥLVjnI0r.C |ӲكjLtqRFH*&XRZ-j28anic8B1KvrVVpR44尺J)4)!Dr RwϾ-˒ST%uE`BIR,r 'NlA<; ,5xgod<:q ,)1$E u{\]|꣎H<F\sG_oڱ`z=@wR3Ƈ0}851|/ $~x\$6jFb(PD,|Ü"m;Cg0~݄qG#ͮԩSW00,!eX}&SǃJ$심D<ȳ<P" X Tlj'ԹSD9}'/K774Z KHQs /#)1X!JB8h+YaLsKR:s&`q7;aSr"Z~f:%ܒPQp 2qveƅ7oo)dfM_dh{d߆d;[* _u k| ǍGz'="8x!_| f;'&)H8KWgT :rN{PHJDpeXD$@ܬ,iI@ґofK2 $yQDr$zyBD&"ZK9'=hlA<;~s?yO }=%rg*\8D ۷}ӤmIIs.%S牍عs#:i|0B+u8K$ҩj'O:cC^ FV7P+(i63 4bWUPJ˶I4XU(ʠfp5vMf+Lq dqs:L:}ݰ1X|d|IF]621zF4{]t|LzL(#!|]C IDATi-6h~SG[ƑI㱅!Bx}U^LYPGV-i4zRr )e=Q.KzdraYaʦiZĮ(6.-KD  'm]$I8l 4i%P tq*= w*Q-n~޽h 8K<{vc ߎ+->hL_Cxxwi@{y˃Ž"H!#2Y[G;{Y3x HYl,t RT$m4g_$")6j:s(ξieOmY, l#(,C[d˾Zu^:=rA;JkvĖ9=B]3g~6^"VL^"%8܍׿>#aaa ,,,,,,444$$$44.rX>QB8!334Tb aE!ЍfK]40"tNTy#dLj*q|Sg͔nB<߾86qD85F bVdyl|N/Ys*tDYf;v6SC`~ףm ?|o?ti?tmg/ TeẄ *D)bpn>[E4M ͰjNOOOOWjy,: N4:Tk5&zm,_\X4fjvXPCӴۅ74_kj*\)sU4R*pNJ65j<:;,Lpj:=Vs rMzn CG:;wvuBZ/תU:; 4MӴ*[gv͎O݌&WwRL'_?)}yy]X}hbu^2E9.ePNvCvalITV5sǜ_3(X./ӧSØ -s929ZڴZWT`EZf}A_X:geꖥlLі:JINqns~vQ iVVKW?I&[eM\)pKtVm.K/ 6 V7%>˒BpyM%<%KM:9v^{hX}:_^1cUb[hי[7_ o?K1n|}SSSggV0ʲʂd8)l\) 4Z̻=,p.PhP,MHfȽf謬4Bzn"KI,vHM8z4Z9T%cnyV_I2C簶j9 @J3҅RGL,:)Q(iBײwgDaD;9ܩܵXEB"*( BDYV3rh gXDMV?H~rcϴY'&_ǥg)JKyZb'e=1<)$% ~4}7EqBĉc@A 9|f9 Q=&ayf\4BD c}">@)QC(GBif銆tC: PDrKyr\NMLk_Hg"7әTH(ˇ2 n?' U0{ "yؿ;(^e)Es-Rl/ո=:K[8[{MFFF qOQ%;8R%W{ٌZCc)c3Y?l%D$ U9 SsyelRXKN JG>] *Zʬ  wV{R.˩8W m0&洆H.-b45Kn'JFq+|!2J;Wqb퓲vpl;?`4ڂӌ B\fJfUɺreaqQcFUJSVUƲXFM1h2q Cم%˟ 6IW^YєfNԆ7.~D 3MVkx,z9Mƙ9ZW@XlE.Wh:E(^:epқU9t<V9[E+rs ! aIK0nL?lKz1|c,G5Rd4B9Eq1f$pٺ`f:Ui*wkKz'g=7Y{:dÃ]{'ކe}}3A5|c?Ւɹ?^~r77y$(!K yϖ8#jG^tYYeXTD%UՓ>yMG [S暋[ sx墳n;UPMuJIB%Xu*sΟ2Toz1ɳYŸ_\c#>]gݐN3y`HogguwLS}HK{7絛"tv~~s%"Ut~KeS^xso>k1|vzlRhnR_{E`&̠ɌW͇㤟wju'˗ _/ߓ& N[1&9oxxB΋~|yNgEu|wcW+"o)1#.%^?C}Mn>FIB~f\8S}pͽ?[7%󍛛V?ʁ{t r. 4KeorZ# ؿ}@vw8ySX,o;ӉqzY}3ğg>w/\w#}Ǿu\Xjy9H=oO06^:L;}g]Ďe΃Op˜?yϏ [L\oPko,,籑( |r^ŞWmdD~x́w: ^ߝs'Ԣa>OJ$JnAsI]__B!s{8t`ˢ7۫r]>CEAcbbhBȝÎឱ$}?VѨG?u;0z_Hz5(%'8%zB Yu׻paq/. UF(:"(3;gx̡+vH .t :eFdsJ!G/ JINL$K>vww_饄C6' 111W_( !dc`ov8~ZaO<:3(.;U+((HTJ˻H,=R(!BnpdCۻKV,ɦ_;OnÇϘ1Z~nӳZ[['M44^_)B!ѡ<!H<-y.`ݿvH$Z{rD"8q?O\MD !r#'zB^4[D}||b+Cy*:n)JIB!7>}ihvLtCDOWOB!0z]Bsu$Q\O^ՔFIB!77bH(W3::rq-OW$J!A==_R}=]u˼$^k$J!u<}X;uPJBqJQm r著u?׿n_8Hώ"?Dwءt/w{z[U|OWŬ;Q/QGxIޚY5 6.::ߎ,h]jm$\YJIď g1Nܵ{jbU J̬ʪ~%N_[ڄQU9ˋe#~_X@l,ZtZeM7wm5B{/oސf@d=V{Ͽd%&No;w r6@bDֽ@?y}?L5?ǜks3=a(a  /:?qaA{ %ƮscBԦ0 >?6N5?64{5}ڿ $׌U^0gǟmF5+"^}3_m͟Wg7_zg;3!= @˶S>z W^[8s$4wS>ViG>XG6_8^^>f`;=`g4>hA?or:ۙ~6O֬~og/_vw[7"ö9E>FN=R*^,u ]CXhsBR8Y>vC 3, Q2JU%΍(}ûug~kUQj*3}ksKb(\M'_}Ҡ)]Y]MU Fw"dugCS'E <t򟜖̉]3IPݑ7` DB;/9[ 'NW˭Vksؽ/5_j(GE NiS5˷^ eR+UayUŊ.||5k^~ϵ_^^F<{%o=՟:ԴwkxRkWٻ ;fj J:kv7-C1Ni ;<|Qetjߪ:meU6mnZXy6`᪷2ťDaYN]Z^^Qo1Z-5"SѼrZf8>n6D_*)NI }3f>Bb&ߝ;y2ҏuO_<޳[L4?#{ĵtWkN)7& KNOy9w_ g!4>h8܃<5_:ߖL @1sG|i< PE>iG΀p1O7(9Gw}`%b6s# B"<S Zj)  3&L?|Ai/%w^MKVnrMOiR%U\N!8=m 7`,-_ 8R䛜"66*)]߸A֥h*;*gՂ6M U {l֠F#,(Th~[ IDAT"*jT ,T`L^bV)NͶJ zRdV!pڌ<.zkB3t0ڬt q'^Oz]z -AΜza{7'w~w>]x؋}|n, H#%9[}$ͤIO^jUF̬?>V Vr#ok[ a]\_ȻD&lX\5>q̜Wn"1b9Xr]IÖ/:EבۍB`EvO\2 .>h[s57M?SNo\AR#SX .I>7/ά)v #:D5)6mF3T0OgfY)oCMrby YTybeK=U/h*)0ǤӮVe5cYZN `Y/:(!G:==b zz ws!ǐcݿ\NV&7jE˘ui!"xI8 ~< ;Sޓ:ng~bOR#io_ KYN7frŸyxe̡s _ဦ| 5\Cp<3S\9a 1bkHZdQ+%,\Tn 9 [˅KԂk- (ƬM:+*8{xJ$նr8?__t̜FÖT5 Ga>y`‹ _..NU-|n`얼2Ez-w?D?(:EF˰uQ;t/mfF-RK֥ҌyڝΏʐkfVZSMy2Z}k?ZJ|au:7R >u̙?8wRPOliA~ ?{JZ>sr@?ok=EI騐 t{͙tp&:ȧ)8@h'!Daj>>816>}SO]~%QhvF[yp~WDBНӞz@?дweɏ>j?>(Nvaߧ>M=sӚ6)bɬjR-!y<\|eG(N[DFR@[Uɮ&Qyd$Pfk{EQ DbQ2j{mv#(:'2Yâz&&{oߐidĎ?H]}ݟݢ!^eϾ@)ԯ6`֖RXuuuZφPgJ67en76[&#ylR/6-22r |_ B6}osf3/k+yb(%MZlfµt>AB.!4iQ׾v})e9{DHJ.K+DSn,1rY}\TT:g:m{:z.t܅MZe),9 NܢY~h?R;W !:UmپpSf_0=vrw2@!rh58\}+J@̐Ob3%yy{M֩gDI% NVٰs{팗}mu`̹ S;X*{V$/Y4aʢʬ(޾zjw~Q~IZT[6(,MYo BF)S$pcSv/m4hbq$j %67pG8;Ð3U %_ AWS͑F'̜& }<#<ݲN7l0cˆڢ som M!:,$Z[sȡ3xr#d{>dA9 y@wmvM;*oҖ`]G`/4t\@!D!Q (9ܑm;|g,z}FzlNuIf!@WPR$:¨twB&{TL=]C4]'[Pʰ2:>svoJ6S-AwC.Mja4*b(!BnY/. Qɏv^Pt@S$=wlnD-?cQ}Vur2͂%\RJH]B!w*n//6[s]X_N}}}( CkBHFQB!BIB!MytFZɝcӧwo;qpK^lmS=&NXqlq^`]zVA]w|yZ|Ԓ֕Y}IY̺ gvy:xkZܲGחrB[iq~~,yy4miVZl]{kSqN,2y=dzku7rɏg%ґq*0>䒗]<6}ZAb@cc̣+"-^8/yCh.b9-aKl|lm+,4/6s)Z<㯟un)͌a-,)rp[ *[ӝ}fL}k/1ke;N?Vq} ujNھ zUu[|-L'˽+3ٜ?ʾu\_菧<-ݧV {۾CO֟q+lx8_;)K_&O=>Q<՜4vϲOfO6Lold31tԥ;TB IYof|]TX"\xbVQ UyV'Pi"8 ,KΔ,lyŦz1\dF\i]9L`)-ݜjէmvh3V&d/1X`")UUddʲYRŖ45;5tFKu:.w7b.37;I Gq6}"gP/3Yxj є1ɶU彖SaǦ4dLС-z65{|v], -JR!o2&äk0[6kKv{ 6~s' yP72\;ʣwf7&Y93lDޮʵf|uAE3̉CU-_+>ncfGxtG90?}PcN|S_Y|Q $k}.G'O1nͻZZ2rcrˬVk)̺lYYBZ{4LkmVa̱Ef[-)Nk@KUX*]ƄgZLZj4#j-e)E5\r2 \rպ5-0Yy`z+RV*چd/̩Y7: 14[U\{̮JS qZCx㎪6እQeT@r6d¹oL,0&IYX At֯ؠȃDoF:%-擓^H&?G+ps7.Ue"96͠hXufi9MPChr)0E-2@}4E3XV4O7(MsepÔ/4ZIOTc=@Afpx-X( 䨲6)R VO +Oѳ%6@[򨒚ES464\STnIUS-NO` XE3K')&ƫ JJ27jru)Zj1XpZЦjAah@5h8H}8 0 'y`? U|]ը0"=[@VY&LS+NfeOO<^e߼.]!j9Ng]f='ؕ0/{26M? ݻ3ZT瓽X?zN }× ^oo0Go{ߝY9q|"RR?tREX ceߖX'$HdUIꂪUWbsDg Ȫz)86ESܐ)}nȁr ]IÖ/9EW}F.D,'ߟYow»LVkHT:%&ZަHp#Fnh9dX .I>?0{i{E^NIC`$Noڌ\!gN`xڠ(WzBc 9̱ȋSx e._IƟCZb6tĞ-y^,Dv/tݪn4^scMigU\3TFU(rciǼ[uo\D8? ]L`slTX:O /v||U~F>A/LџOO[בPY[JYYbU6s~$I*!8mZ`ǣ_$&XNא˹vÔ;/s Ǻ#[f9?lmNx"ʐ*)nwDRYuWTpH6ϫmF5rcq~$<- S;,9-jbü}9#"+\\<}X^z)@[IrFxɃp:D"eXm]9Urfm؛)uiٺ42S1O߸Ur*Ukʾ)14:Oȅζ 1>nc'=zFIw:KN=Gߕ7I4O^j/]&P ;)RU Y23 VsY ^҅-6o+ My8Au 6<)Ufl%D wўlpVO%My&A@EẰ >elRXKN JG3 )µvM0:4)sac䐳!:yYv4o."-"#W)YdW(<2k(\xahOb.}ƌ< 8lySc8@hX7+pيUYV+4NKIZlcc`mْ:T~@tei6BJ6ݬ %/,W|Bd>;vJ;'esv~﵈4wf/dݫ% BkO7M_'P"uYy``0RW:ui2O'8&E굆čDGK[7İբAyS[ɺBJY|3HI.^/Wtl競,}YؔczCof׿fge_XUƭl^N^j\5[L5 d䦛ɒ!8`Ve}͐"(WY MSݲeI:ks3"YarydNmU'ʪ17 X'Ur#GּٓϟEQ昘Z3KZjtc- ֌kPgJ62Ktfi>B)(Xeؐ7o;R獪.Gr<!\C [tٺ%_WcUS %wPeћDZ_)u2bHf͚E} -IDATvOV !Аl3ri~n+ϘM_o7+RҭR .<єKq4:OF\yB}F۶NN0$GHM(:.][sr ֝;᫜pC!RכVlny"Ȟ72dTB!wt@=EO:ݴaI߈Kd[vn( fFp55'kuEidz5+M;7lq-TٻigQQHKM0.v玚z(%)7Y+mlݮ^߽/gg.\>P}&NC5 Nh.T&nǏS=B!#֔)Snb$ t]=kNq  0!fMX8sWcMnTGmC7(qDD0'+jϊD8 /H $Gln?pr_@hQz憦CwHá3ǣ=RyxքdOai 9 pVmQ.Y")B!#ٿD::@d=k^3fk/c'C "űn W2k:܃I͟p9\e 2Ntnwl(p!z3H'6daDOmцJ߹6B!NS8 HF+2#%pK 3+=$k3Q ,{_jԗb|)7^tgxLzV g Sۻ_N& @NިMP7p(,& !B];޻{nOg 27y Sgwغɼ Bf_Y3CH389Z˦M[pfgvo}ǁ8@wc_yLhZZӼi]T*&k4C !rJxu\=׽,Zұ}ہ\h A2fg+Ұ%{,P Izܸ2 ɎA_fܺNydтRxn%X8GΝ.d?Ƚmg 5NNNPgoIJH@!ku̯%O5]KHJ#Ο?+ 111f!=B!P%B!w;mt.4:O!ͨOB!P%B!D !B$JulуbS3B{Uy ՗f7bӬ5|5ʱS Ŏ+וC!L21nӲ勉VKmRɲ į1_!K%NW._7Xt1IjBD݀SF&]E9 T43 :N2,5֐o\j%e7\(NQli)ԙuU%eeu6eF.Vj*.N5PjHfZeWPWl>g}<&-?#ItFK@hȎ4g.NM%eY2Rt: `/3|OͅB%QBnxb+.DajsջyaĦ ސ[\lٹXk؇[sVUȳvVUUi׮8TU)RT^Lb˾zmR^.*9rkyYnL}QҦPb2t+/3-#wiUj1iٶefij-ߒu^Vlk|ߪe1Lp-/Ϧ-Z,̓ !JH,.$QV.gZ*,e6;*MZ 4V+o6 f.,[ 4)(]b)GS4 3ZCL0*)LAPƧ'*py-\eQef鸫^ƆE:v``n߯nwϮX,;`4JV?zD &hx 7&Q#xvșjAj\fYebHt2/_KkH_[Y넥tQ*xl0bq/]sNܣGɓ', ͛ar+Ç*t7nD3fٳ'SN8p`ʔ)eee_~mX8FBLӷъcկ pus9_Wj1;M|?캵y ]?Ys϶0ms]kj:Ԟ:m~ܴƇK2$~KBBI\[!VƆ)χ[bm\" $p XV @ocnxyJdjI/^ܹsg.]uV^^kѢEW/\z=zTPuVFFzrʦM/^Խ{w{ijjvZ~~[߾}%%v/"} cez^͙e|Vp֎R.36g?䲿:/ mEΒыʍH3:_71Q|Uo3?_ۅ/[5-rG!#&?R)+,x57대ĵwPļj B~?CpCs}֜9GH /OZQw"ܑo']j3A/taڤ'jd_1W`Az3ժ9{l2BP(BEˆE 9C  [;ˍ߸}VkIMEMdw[s]v 22/,((8s̰aÚxn}rJff={l=//`0̞=gϞ;v8tЄ b1?Oݻw?{}rrrk0\.q~j2YF+ܡdoOpx-6%5C  D"CŚ}i\B_$(!W\Bҽ3x:S"Ӿ1{Z.hSLttǎǩ1G='&hq`%˩ r֦,S ^.2g*·|8?9bH겤w53 ӷo_;;;թjJUPP l[#H[?VPtjg ,z=bqV[B(nmn#RStA j2ɦ:҈΃QMZ,+X^[7|.>^qLJQ(}9rN.JITDTd_msPTVr^3b#M5wp%mr/cevW/ S1ՇRSWXQQ(J\_OXg0 U7h hMbJv 0ˢ½+ZU³JEnĉ3\YvMP͍YBAT!#Uhؤ% }Mײua5eU 9sS7.n3'?|*HyM^3"T _SG!^%%_zE_a\}-3AA]ėe%Ư)JPZJ/N|W^a/W)J-|?ޕ@wfܒߤ؄y,mKLVe+82jIRp6x3Z:Gr_մ*("qQI%˫&8vA5RbB^nd>kQK?z;_i1L~nyF.N QeRsٹk"lFRpZzzApI{#;]]Tp_`\ٟ/6?ŢݰQ}$wxPcF>ׅXV@xn޼ [nJYJ6h{tϞ=kZ- 0F6mċm(T*5LoL_L"͒{:!"8rґl*^AUJJ/^w/Octu{_S Zbɚ3=)qeFFm5;Rx{%*m[&hmX7D eN\yG'm ͈LM MN'[v+ٖ5Ë_*&uql#Exu#E_s)כ1=߲Q,6tz ?5Egt`}%,P;Vwx VXPxy8r:(*l>D`=|rМ)WFLn.I^3{> (*cِLNqE~cReܹ&M;wwѓx}vB[_k|=_a8y/7t:\)H'}Ws 89H lPGEskBݗbM2Kۋ09))B ^}k,.L`? @2=W\$,Wߠ׋Z^:m:&/_G)x7lۋUv;fFI(-xܧ/ I [4oޣG6,j=I q4tPrUUbWͻ3򲇮a]dޞ ʫ3a&/`Ld)U Ele%kƆKڦ||9[M,ql90_L00N`dLC9ܩ85G9{vk7bbz&te;cE1 pA{---l5u{Mi&P^زB2w]itön +wx6{rew"4@\XxNár`Txx* @b75 !m){ʍsu2eMؖ2c%+)8v0 #iتʎ`떣#8 ɵFƷ%l35iʐrcۭAZM:]p&UDs0nqK)ƦT5W`Vx:Z\L+Zbmw{(}f\ETzFlCzg/>`/Jb^NW*hk/Z#8`n2ǾL5]3^xng]\X&ajv=p4 b?-k贏#K֤-ڴ,6.A TO,kH9WQ([nkxV(Z? :Vit\sK,˪X|:j\joOvƼ{jJKro;oqĔCCV)%2 "ߵ(G9Wn kjVRF\MXDunv|@%moVJ{imx$.NNy 4Eݙ| |Uw /;JJ/zp GkVJU($w&򜚓,lSjCj6nl|524*"knʦ-fJFmIl{ X𺆖Ku%g|o;fgE},6?ʧHN_r9 ]Lx.{KmZ_&í F( B!oh$Pն>zz _~@nls9lm@g._mˑus455:U  ))ƦBetEXBByb괱(''GPg7/VIA @I8ֶI |_qhNu i3)e∬"(؄ʔ9c9Gpd2gZ7<"H iAe7t,+,!ݔ9qNj'/ψxУU4rXű*: !đ:̗ת&-[F6QPa?l٪v%Y[wR oJLI?6ն0_*>Gĥh'S]~bB}7\mٲBj3f̐H$dz֭[ 22۷׮][__/Jƌ3`իW ƍظb ֮H5ߨnp4yOY@hg&VUϛ@s$=qߏ FN~DE"Bpǥ\7U" []sޒGGX,nݺ5obB^,Mnچ4?b Ex 0|SnV_c׮xSlU㥋^sv b={zlo #/rdFdg/JUM1Bb/BZ[Bm^^^ӧOUyeىIk l?8a((!I[oRz2֭rB]uP37.imY-"BI:!OXN@!S.Xcj.ݺuzX0gs 0`4A -=B!!P 1_bEm[>hjXVm jZ~#po|OB!;:w+9ISbgbJw`"g H, =ľ"W7TB!"@F#&34dY@$88 $ FB!ftB!B/AHE@!BetB!BetB!B(B!B(B!'z*!B!(+Jۯxs/ߠX,fh4޼y_""B:KRftl6#BiyajD"6B!3X,gEv+D[,IB!䏞E"y,j!B 8'O-ۺ҅B!B!(M+/6W^yWFO[2Ab8]L IDAT|ێצɫX.qUJl雫.߲+G_Ryu5PO^wIL+qGWMζ~Smc}?[޶]e;,?{N⩋/=U{N\U[O|DS?cȑ8yZ0Nkc3,RWyu]s9+##R=NۼծϠbufWQ/~c.C"f.CNr(qCAӒNr϶s^ *`]4w4LtKWk5$aKs:/ʯ]x[{gWbvDJlITŁbuSx@g|t/pxqiz ͽuW"Xˆ+;bWXjޓxSg] %'4Mp0+e r0g.w<3s|YQz)ruՇ?^3=[P'6Whq)^Hg#úN ui?{=Auz^9r\$0֞ڵ&@,>m|0Vڵ`q]n' pm]y3χ͞>E.ٺ: v>eU_qX.x&lpKNj7߿nMu՜g?$r MJl;l2&]bTpsSg]~+=kLG׿8;?%q3uЩ5;i0ulO}Klv-;6'?"rǁoTۿ?gcW|7\Y_y6&}k *Fl^9Ay'k]Q?qW{I]v0ОSrݖ*e^1bߊ]_}W{R f<(*WFv6U?`6-af믿LKy+0ww$ʶ]m?;/96~ޯ/UKHؕV6"˯n~>o﨏{.n0ӻ?WSYL~Cw66wF]k`<{ޮOuy&:3k?ʘ@m,h!`ZPϕ=V7M#ý2?;5#`ؕgnrMu\A#ls]k|UJ]eoI^k[ROquז|o̎{xkJ'5...fg8 o>vvfcbMQuy\S{]L.n|J2rQL\\̂ }j# A\d+>zꚱ9_(G>KڋWO%rczU;Q/f#sqcH'vl;IXG.5rS' i4V|}uML5p̤?}Mv=̿-K'j`l4J%T|=H\+c=oSx T5hbu^YToF7*F>hǰh!$}^22'kN]:yl{pkGHt^;KB᫴+X{bcIĕbAFݘ֍)4*Z,N`N[K6FP괝4<^W"aZ>]G%c 4l1F/FuaCz3hZzX5Zw]V3}Z6FʺiyaؖϳL{]W]G#hQ]+bv_wn{t7fo͕4Oo`%;.`Du<ZW*fa-|k.T ^>9gߣÍM??82*pw ̮̊MF\=Kv>8Ilo߼$-"bq4%bF]#(mz4!LP䟇/, $2ϗ=_`ԫfl=_o2qӓk^I,i6"q{ҟ):xG"2#hZ1k}u pS6 9ta @Mp4)xq yIwδ['Հ3*܈~2"s\"xwBQF-:/mqB[&CwN>_lsh#QikN)bۋtv1 rl Q(Xcx〮W꜃<~[[rVwN:t$cV#Zn-aG3s9l9Y`ѩkTn_ { p{&9#;W讌{ 67rЅgH-.}}v 닏VYм#U{>Z_6AԤo5$b[f/#+5XLz5jLrvh{g{kzژV&osϭf]-sL6p}q1@f'~f72V>v80!(O6t93%999uw'Xm L>+R^J! dZ֖jCG՜yG cPn1Ϩ>2ֻRs`8t{ Q}9go;uyKfUP\r'*l*A_? ̸l'*>g `4km:PN.LOsj43]@lM,UwX>^U wg)IRg ͜}6帇}v v~N68⯯:NR4 774a,/=_S(q>z4}=) ?nƫ7o=U#MOҜ]7 nYZo]էrKM{ϗܸ£z[>us8gSC=<\{ƣTۂ:~ُY*'EM-l Z +.V筱 G. .זPWV}X@$~}Rd;;fV,! 7UnVwwgܦp? 7D$ܹǽؤ!6acU~mGu~< vP&O~2$hd%ee>aaUljќömoLvN rW]fma &yo:%|춬?Dul>g y}Lx>{KrӧWoQB'!q/?O6G?ASS@ x۞(i^w<v'|ofyIhҒŞ!BT3CDR{ًiYjar]ށ#UysEcUߋr1L]1^Փ:/Vb1fyPYY9xG۷o<]OZ{Ȝ|73rbd=+_2%tQ1 iqpp谏[bؼUG2nwCg}ϳg%YW^!*8k~H/Ƶ)0.U9=}޲cV:! ;E{^JJtAʸSFz?kLooSiF;ٰKb'K&@!=Xs9`ZOyƺB!ʞX+l?V;Pw B!}ꨤ*tB!B@HE@!BetB!BetB!B(B!B(?,cB! CI$OB!0 CnD"TJB vvvN/b*&;"ʁB!OE@!BetB!BetB!B(B!B8//JB!Nч & SaZ-l6< J*B!XB!B(B!B(B!BB!BB!B!B!PF'B!PF'B!2:!B!2:!B!2:!B! !B!tF}Usq QQv !B$jA,Cwϑ%Ϝ9s0v !B?L3'uD v_ɪzJ_8}þ:o5r@+bWu&_Ӂ}uM?+Paƍ_^=Nr@kٱT5'RxFA!Bc{~tc3:8{ s'{ҵ'7dՉ{93ԥ! GʽFG Cw1˞' Ŭ}wu79YȤ/;tEYY却:cB᫴ w5J8p}^EA!=A?=hr}=$U~5owI/Yř'y.Z3d xdVUmm#T.D>p.4;YU[Go{Y[^ߐl8bhNҍn84fvVWu]ȅw|aТvw. y(]g= ֿ0'g׫LWg}Bz@!э M@ӑO4OW۾IE& Ȁ]I;IL"s4юcC'1'7SgY3}n̩ {X$XK}K{IEǗ-:޷{Ϸk '$gWWd}'zAFB$7]l|`?_8vXycv2{@$1T.xЁ7as@Y,_Y}ia}Gan\߽ezY_\Xxz?Yr B~eCuMq9r[}wFU}y^!etG\kTutc! `RvEQٟs ~Uq|ݞЙ3/_8, Fy򢄜zcvjB&z^ռ˷2l^r3SD'OJm=bx go(v~]u*r= FmdLZW?*>p@BH'$hjjT䩰Zl6f CeeZ2<?fvv=#ؘws8Qxz5g ) q> ް{ ;=_9+KTNܭn}B5`{ ys{S;Z[sr KWӧq|3Op IDATV~gLri;Wԭu^6Dp>&lv(+?b7 }} i*gLo a''o)N4꽥Gy=۷{VGT1U`y/=|"{{WϺ7T_]q'\YNdm:g >_w+ s6l8%mY{kwerœO srf_<7r-˺'0V\8qi t0^{Z9}]h}o`Q؟[5vCUnӦ/L[p=cyѬ G> x/m=7>`M!PF'j[ b R)ovZnȹ>gʚ' OpLu*5T?֜ /|n6ik@>]Iq0{@ф 'u=o䷘ 7nOm0ɣ ҵ*kDF{tgƭi(?ܱ9w IX^BT)88{* ,U (kKР*ϝԁ%256ò>:_We'!FUS[[- ]j@]D:U6**ߘ:~H-2^iI[q*dAd$g꭪'C9D4&l]f;55,6)=gcejһY`U6^*+s}Fn}]ߥH|VdȭH%T<:t.'$svM M&uyXT*BCClM?N6p*+k j){&]]"sruj Wa=E[Dc$gm&Ggު^: N( yx^z<׋g6rZ5@^u 2oo[ee5Wk-+[uvVV!IB]mi޺yJWqߘYfD'T Y%Yh~SzL:b 늕r`IZvK)ytxf\ʷL?w̻f%F]-k#m9J$s3MBd""Nd]\kқd(5L C$6cO)r/A6~ks` TV3=%7=SE.$edh4F^Hh.H.]=t=*/*5u}VѽJޠ<K 7 i t|K`WLWZcY.ql ^F 2S =+b$ iw.ѣS|}Gk&OOpd׵"ȠZ(wۓd13 ҵe{2:/o,TG:e{l(+_ѱ=o>riާkjsvIoJX1~ ]ꈡDv[@tr@tɲNBNuנqv-?kT3fc֧O\?WD/(3<|;GN8`ML+/2D=l3K+\8Ϛ{KR2^ϓ NwHMgu:,G޽Ud.1`0to9*ka7WRs׌--ު11?9,nT!"1!󩛐¹ 1'1Dܼ?5u|̺]=+F֗kGz#9{CzȤJOt^z(y''ZH-EE6vЫm-fSYDO Nu'n }p  cwkwlݫRT:.Q.>٨ʊ|cvtXlRzFq||sAK Y9|Vݚ9ӕeef7Օ_VY"88c5 Q;¢r8 7 O% >ˊl$1CQցۯȺ)w՗DmnGJ^ε{oN۷-f5gd_ۚRZ*C4&x<<(%eWe.j14զc!E8JrCaДH\m{cӊYk,KOXmW6oEo]c2uU5x|9&UZz]H4ɛ -U?$ԺS/PY"88cCe"*_<,B>_.XU0~a1X2<&9'&cy!9MBIS^U$gώܻ(ECDJDL-_HPgD-R-3dhRu=lL\Iܫ'җD5Ą5ˋk84}Q/qx ,g`b"X"*T&Kv#sAD.7ťHZʒy+=n2 q^T.xoL;sw%Ր:{ea)V+6ƫ LQb"=di5&sHSS"g4ԠuVݢx Ngl--DBuaPhnBj*>2/CU&@ޒReqh:+y%P)I4&"zu7cH\ N߳'+=KebФV5m6M*l ?sw \n=3)/OEc"WeU C63usf$V}xf33 sNWWWWW <_ti(UhwD?pu b/P9*> e[422И+"=ӳny;l-k1 D\}#c-lغMk4zH,WYrF?8\L; 4&>!ѭ7&8mت⾏B<<<ҷ;iH}w(* XIߢ7>?C:ݽYʀy167D#:jS S3\Qdt@F@Fdtdtxİ"qvu8{DjsP:z֚'Ǭ^`b󲪠OsݿZ[pƭZē;볊˝Dv""V2z.h= ED|h"j+=# x(Gg}N7hg{sae?x==.,Bcq7o pΝ'R'"tЯ]&7㸶6gggtx~ GtC9o5'v8zqSl#5֬v/ [xsVH0\~$"x!do>Qm[mEޘg4t3prtJx.% `=CۭNE5wu^J~(ve f"=$.6[3wlj+ u{؁+Źvj&"Ӕ%-Z^f@DtlzɎn^_i?D u^ȑSHo_]iG#7V~4QWƿt?>[ [w1{Cqᙏq)fDw4&[Gj;^w-zxŋ;tn<{.(Q%Gy?.c+in>[}wI]>I-t{JUΟrCܸ NHt٭w~N"Ԗ:5rG ù҂j j]9cj t_.]ofGG#"wO ?DD'k?-xѝVR/0Dd/ sooʑb"6tj&N/fMzi\p"gfa&Zk#o?I%.ҙ3^ "ǵ_W~ZBnȈh!!NDDdCıۭ&D:=aK~ž_PtӞ[,Gt^5 4h82b2ä ;ia/nNz,mqE}lQ"?hİCD'#; uh 2s~N`M#RK_ IZ)*;unٶvoF );LNvgnrf׾{6qFu&/fj<=߱cEbG1ݾһFD&/>zGȑݏ2j6`fы`n$#v#g&K q~[ }H j]๻~)FQ˝ŗ:nߥᾓLI e}=8u)_%; ۃI" Wmm=f'`F}ZID;v^|A[DU}ot /srUpoG/Х,.W'wdOdy?7uQ<K0a:]^]җȒ/hۯ8k$CwlZ+;~Lwh;-ID}y9gM}55ܔz~F _|3\fGdda}D4#;ы`]O_?e-6\wS .˿{ߛuE=-d;Ժ#:N+\s}UC.ؙv|J"6%ֺ붼F8S޻_r餳wcޏw6O?! niY$Jz{=Nuyחb6_9gY(EoϛlW_i?>lDG"dQ_;\(*Wm0ADFͷ7_ ]뿫:ҥ7oEDqvH#A^8e0 N6TTML?h@?{Ζ{P;.i.tѨӧL1aiuŞ[>sCgy"o}/MOt~͹ W:xL]|;n_mOuUYĎ u)o=P_]8sة>2Ϳ~ͻzY(s,1x--_H4{aT3z! 1vj.vKΔz{#WDD4Enwf+ -&k]mߗ)YkCY_E(~3|81͙ҮpN|̣ל4EN GM /^pzunxB]>ȾFy/Yx?F6 !<]0YFOdt#uxPB wS8 C"?k;1~<:5փ5S-bIXrԩSq5ɱ7rDtXNrtƄ2-B*qԩSq׏'džY!2!qeIaݭN,׊̰SƖ]_ֺc;z?S@XlҀO:ujpziv%X6 ,6mGjs"\e"_:ujXv#С_?'9!)G'8v/2-;G'TxKW$ɱ8oπ!ϣsf"D`zN4eúkkf;_. W'NЕ4N:ɡuoC\oLz7^^tK?ժ5rDcCv[ݓزS'J+|[ U*= V<-IAW(K-ɼ\$Q.ʨlޑ`jgY""&'z̼C^3 ۼ#I]\)H$t!"^|odfhcPkʳt;Wť DbnSfUȎt%"⛊Vg WEK bhm,7,])˷ Dro =VeUcH0hbTIB#W9ZWڴ\K;b]H^*W $7Sθ&d&$m!b\3=e,Ө54wluc{d2k Rկለa{vI+h!*bF04i꫋25mߑyxn3āalW68]SxcwU٠̃rDRnwNDDD"0qy077V'"_/enS7/TYiɽ 1DBSzaޞ4˦PIT\:cEqEzUQ}KHh[忾4' [ 6b"W2Lݱ9;ʒe[^ɲ CD-EE鎴1dQqE|M2XBD|֔-'wv]YrlFuyвL%"AE=osYX$6FZMSQ66mL@B^Ux'==&;yo Ǫ }'RTMi;b]6mi"\#;̅ז$d,[Z<>ܳ7kWfiT煍$<#Xb4YaO;|ruSCgo](1gLB*bEw%-؛!"у!--R'5J+]HS,95_-H DBN$pEݏu UHk/(b2W)%}MdR6 m2ԃGiU&"Ԙ> 0ٛTZKhk5!)u"P[y  abT"V>YRR ^3XOs{'uvpWT刈;b촳17t-fW,bDJwԟ=BG2cX/O ѵ53i{nNNKX1U,.yЈ~JODFmu:oGL CD2~2 ql ?/V&" +eDUm㉈ViLDbP0?ԕھs2 &0`W"ҫܤ*Q1 t CdReo=Tx^`x }}t{k$ߔ~s D$q պzxm'+(jdIҝKGvmiU.|/&]']q;w~WZ@d?+55) |gXkq$$$3]IbR5>/]$c$DIJ;̓E;>Ub# `}!%SDO) 2YwY#|LVs~Uqyu e!oO.SZ’ j؛U'K'b%8ڕ [T=H'1+2X=dDD:@H&{$&ÀaIELD79cs'd "˘!dn2{ uw5RCc$Ddb"2iTszc37eXlZv2"Cy2/iҤ6lذaÆ?s W$t I3"AvJBırK4'ԕZToR DbG,Sߝ^ " KĺJ= 4&"#-X)&2+kƪ*'ׇK7C$lNL 2  O\q]/Qս#b"2p<! N  bvͤ\ED٩]BW()ۚYn"G{?[YŽ)]Rl b-%cCD}w;Dnr"SyZSRsꆔ_-^3h9mEq[^/PzD&}@$ dt'KXzq@LY~Ay LD?2&!:x$똰2✭;5ժz"*Eǯxt{oRTқw>+KwoZRUMD${F'X+,޲Jn""F=s^LҊLZӔVToh. [=Y>E[iUDiA/_W'wrFc;_|l6;Ok׮Dq ={~9.K}}=f˗S#Ӆe{GdAF@Fdtdt@F@Fdtg8y%)>[DD,Yo) 2z,DE,K+iB5, XNfuWfePn-%""zۼ!6!)|s%i"B"IGN, Y0cs4乭>J=# WlhKGCn. yR*="(z*W-ۭ>y #V4<.XP,"$$((22:8$Mw7ۖ 9 <fepOyuY J?:a˶3.#|n,;aGY[> o( FA7G,*6ΐ zkrZ?ȑfg2X@b{<7;C~:omi尓tv^Y#Gv&;ʲD>k]Ml͍M{M!i_}pcΚS(Z6?Iͻ|+oуm~R""j 'JZ~"Դx7ϔ2Z Sm(4-G\^{ I'4CFĺ;Z"L縸)"8%wAVuQ}A!8ە%vB\A3Ds -Ime2 b1@!89u d_gu}g⒓ߘI&O>%. ImSY"oBhk3]Sh<{2(̲>*iV,O*ae^q%q&"2,+ȑ#_x'wL|B|Cw}2-/a>ԗj~_Ł-zZ@Bۉs}Yնm{rNhScӺwM/a "É3V,c&0)Kz"BgV6sᲡvJző3~P%369;_w*}S?>Xm#[FA~<Ź 5Lƣu{g""2ߞwD [Ojn0;K &vG,e~9%?y~joWK}绛L/Fco H, =]L aR;Kb/1z0u>4F,~#V",9N W|xk׮vߗl4 oGtwbHFf>^<]F sve3.e' ׳s,>c$/6n7ťXGnJQ?O7gg|ͤn5CFwCHO`Z{fѭ#_K熜ݗ(zF'7_Ur1LĈŃX&%"-&`WS3u\y "!blo5YLJݙ;y%'V@0IÛR]Xp%uŸmǯ9WKlZlͭ"" s^"RbKNm9"[?iӚ~{uʸ+5q't|{R|vą]I_p]=u1&w*{nEހNDI} OqDܩmCIꉸ݅9,GI@ Dg, B5Yzk(ͫ9rҗj]ɺڼ5kNqDM݇6,czo9T(#C%}*yۏk8T&uO 9w<-Q.7H$\+Zw}'~P:hScк;;#Fwf$w)&㉈k؝Yww/vo]mR!4l<g]5)z共^N<_yoڻG7w/Ye=˷.~wK?6WtsMy\ŕYG0txӯxR~{#B}lW{6n`kuyGG);aiqK|Ʒ?(k׾LJ龵*2plZjds֭kZa"&ORJ|kJΘ‰Q y/9j%m }x}rKƚ M9%nHb?5dׯg=w@$V&Ob86-RHvW:7Is^I{,,lȮ AN ^9 .]M0KwX5@"&zRqVJb)v5n `H llv珜Hy晑}yf@dhmM'\%-n&5lW`G?\ <?yk8=}'I#{k揌?b;߮"Ojy*y߿kL85vIRmMuA=kAtBmkFE!׌LB!,B!ŒObިVƁwKZۛذLعalYתwdˇ&rV7Vuzҋ&5~@!qjZV3/|'_&u[vjZ_r rZoj6W$nս-Zu@d[u-G?F\5 O|,}m<Ķ>+׷,hjq-$_ӤQժ*O7 v_[ЮWg@c+#Ͽn[ Vr6GČ6LOv[G vb#CMNzLv9-~@cexzzWu&bQ3'Ov#2zO͝z쳉T I~f,!\;[)U,)vt:FT }v{*Ɉwi"NG us6iػ&;ӥ Sm*B?1Rl(?P0!:=wf@55 y.9u7 u{{q;r)HBaFGh#JciS HB'dq rfs!W`2S?WXV"\pݳPl)@F;UY>.ԺW_ (~҉||OStfU@rSE~yl'x2,S,@l\]?zHY \< &iʈvjꚙ :N k\*890P_j4FcULj:1S_M'Z*AWc^k5}1vvX7XSU7 [ ZXӝ.6^/mtgVlEu,k)OU}S7Wۨ=-ZZmIRtהz&Hzv]eمکxVՖf}l]lxT7V"ՓCsp,늍UZjjJo_\_lT߲v҄ vK, -FXYucqccU@K}UQ_Y۵FXZ=]~Bl +µhZkJ7ʱO{^k7+)ڍuZv9ft B(~O\((Mq7J,$840ZwR>rC~[Q~fhCyB)E,\CZV5V&$E.5J.8q[Ss8otרoR$%FB@285 :qf|+HXu Y>::bmQ 6lxYhM ̀kHrq` 66:uT 1o*%]'kRMM#b'3}]A3×BO NNOv':2ę'''먱VV($ӽ ;,sSNP h6!SNLAtg 1w= k,ܩ:3uÓPl ,lRhK ٶ+1O!pCT#lmHpL 89!8ɘ'''ǻ!2`q bS> da:Ó?/;ptYL_*'OFZ6\#W(u#ܢW5prw#_OOH;>99&8S?n/j:'OVrVG?ᢶS; !ŒЃ bYr I@eUch S#Ş׳N8 [hѪ"#8 |RajPDM[N[kj%+Vj,ӧ-Vͺegk{fK}T: ~z .+`î&c0;Ǘ (ojlHP=J( HAJHQIH1LHJ,AFTSO haQZ$ҵ(;3dF^KNy+_$I `{y,0DUVO bfɦ$Ŧ"5 bR'@"BNqեtM[GZ^ڋLMY@😵}DJk[I &ɉl"’W`h34[[m_ҧڬcxB5 h6붞EC)de-vE01.#w (%҆\:kp8[Ϛpa YGP'fTf"!gՙϺCI,9\!( 9c"GUHxܙ]14$cd&ދC!@¹.sBOӘF@ \ I+.3ɳPRfPpSΙ@28把5t#y%)5( 5=1 p(B[QSoۍL\H)F';fb,uG 8#Ixfg|YO/|~$鹹n/gig@BF~i$^! 9.lHN1v?X'|]X%} i.qojd6$dYQk%rRSnZ MmwyR9?' 6;`-#:8.%p=0 F=~F14GHlxMqN lcnpe,aC.җo^9/06䚢ih=1TmV{MK { rjMwlUm%.vd|# ,fBk(qkABkf8f瀒vUf̺BLf*hd|ˉ&_z iTA[O1PQa-Ԁch^w4ÒOhH_8S8aW fiػWi3P~4\k_ni3\ cN N@rR.w rol fBo8JZ֡@A]M;!TXڲwǁL$EHgXG6pA%37dy6'2Yö.XD 5UʚlBXSi)mNHYe|$!7.f*LRbqݱp;p\Y5^lmHea{q1񋭽$@lڹj mA)F -ZǛo|iHYTOɺr6]0OixVK;FVw 0UDeZϟ'7+&w!sPu]J(*lեPf4IMֈ鄋@teYK 3_YUY{_A/?[]Uww[LBvpwE+1\ &Io͉!I&B({ꩧ RHbzUSMˆ 8!s]B}󹧾¾(tuՕfLkB\l'"6\s]B!y\_{dɜN^;p6B!r^~뫫׮]~vo;vرcΝvJώ;_q܇~!B_NĵkvڕO!6=' IDATsΝ;wkװqB!nd]vܹ3:e1=5x!BܹsgNFF9S=RC驰9}FZ֖6,$׽[j5Ćgudc*V֗]2j,kzcU'h[a/w{^Vk>kn|^8n rVV=[nBrXWZ lx\'G&-Zu@$ӄ35FV_T딏av[@fN*isjʆZZNKnBm#//fs**i9T#-dbnK;F !8Pdil,8; u|]jr:a+;UY@.8Z(𴊊5&'M"hRw#S"CM9쳊8u#D aߐgźlu2ӆ|n{}Oa<1jH޽vDCiy#D=>6e뉪zj[Op{όk@*= zhRZ wsl_'fJv& OUg}kNNDh !;84שD28 CB+0ŋ@ 6` +JB`= Ŗb dy`4k@`~xej]+eq/ Qq[7Yd9Eiv[$gZkmn0Y' V|'2CT٪NkyiYLQoeN'mSd*2sI> (2(=le'ZU}-5U'>b3UF^p2$1?XnkZc}LY_תUݙwوhݬbjU:og]Sj4[+jZZ_rTƱYTƪΙT$f˚tVV O$R^k`>k)7z}i+TvT7V9AE{ѱ Pg('.Kyt&B*KB"Lu(c( 1U'lF~Az'Rt0:5jZcU{*Hcҥ.8q[SsJw. ҭ&&b"=6q-9u*rDukƃ4%μKޛY61R\FB:Uv 5ĝ=G&'Ow)=蛘|+fLC p%L9fcjzG&'Gžn ;OMOmΰ!G@1rԩ mצOn10w R.9,ĦSN7|  [k|zzzdMꐊM Ѧ 1lCrrrku,q F{cHG!m/a]Mx,`ƀiDgf/a:59ҡXi}=k8 [hѪ"#8 |RajPDM[M[kj %+Vj,ӧ-V'q'ۿЮW?[C7[ f`t}F[ 6HzN4S;NQYWىS?n12%"bcxj̞ 8d96R'kQ?.P'@(SQo}|@Jg uT#k@(0>F^y\B faީc,oji~:&;B+'Hq Ao!EttIYvee s1Yd]FJIu!R6Kf< 4G5%% &8/G,3 s LfP,ƪ*! 4K8ZJqAQ 1 +0[=8H3@KzׂrR`L]ռtT=I`9kd~?id_ꛛKg&`t:spA㘑Z{%$Gt%%|>pLap_uQ7[E, tJcEH'z Di௥JQe[`;%]pyksMS:9cVis +qJ^R\e9{\(G]90ᄏHuBpQEꐔ8kB-lw ֯]ȗ (ojlHPJHJ( HE6QIH1JNET<Ȉj^WI`c-,*XKdDexLJxk`I2o%󋄼0:lo21&xJ@,t5Ŧ"5 b/ʴveDjեtﰥ@xD 2-2:o1oh='=wNGCv :ZND&ٵl,BS}Qy/%ENU $#3p^\7+(2;Zml'fmvwvW%Í뛀%(ΊLnB¹.,4&n_# \ I+.3ɳ%RfPpSΙ@28把5t"y+)5( 5=1 p(B[QSoۍL\H)Fn/(iwX62bqGK;]3J{=C Hg=qTHssRJ5r#'[n') L=/hK)ċC$@;4)L- -K#s)gK.\ lg*! B)6 1\T'$$ZG($sRfc,$ڷ}eY=D'ͤ6W>R$-R7i$ ~ @H.}q7߈OnX%} etl .GG_`l5Ezb6W穚ۂ2- |YI[ @z({G :6[QbkGI)dlWh%=nm3Wh0P֮l32 1P1g:g):FU6kBxM}|ˉ&_z iTA[O1PQa] $ nL[dhejJ\\aɿEnD6ktbIhE)Zk{q~۟PR@圳4)E]R$Ui6TBlQE-涰^9mE<,%+ͼa*: ĕ'm+sPi>qUES!N{[ěqRk;-m!Vq PߩHW*==.Pnmֱ.v"]٦Z%^cR>;KRlWPai :l6UsQRXJ,r<6Beիxstt\~ڵkDZ,>3_»cK"HzkN FN!pSO=8!B uX~'CGK̆'b BaFGqG,_~uOvA_,< !fty'Υ%=C~';ƫ K3ϙt@]~.\/cɉW Ͻ'п~Õz3w%\lvB!({IxrssV.@pT^xQK޿K#A8w)p~Y.: =RXzՅ#*:+yOG9=~lwB!&+g[2;M݇ D<{ u8w)grJPo/,?ܹ%ɱٷ{[{=<Ջ܃o}1tB!˗#o귛^X`7Of^J/w W.ɏ9;璗y?#{k^yfRޡ%e#Kʼn~:VK8!BtrJ~_ޘ{*ys)' C$x6p~y~}\"iqjſ~؞w\Ҟs<ioKg@OjZ[8\n^՗t{i;7eQ+OzՕ-o}PJ9_F-my &?k__>0ρyC7]ʹWxWots= opE|yoFw?yp9| g{Gλ}3O^K-pn[ Vr6GČ6LOvL"%Y_BdIOϝ.?{`cexzzWu&bQ3gOv>`"CMy>z= 3_џl"<`e6d+g12nzZLj3ss#Vn_ߡLL1hlUuOO[S{٨R!)<,Q{rzKN&ל5e z)~pg=o_J#7_^PwS/ίõso*˷W~V*XGf-p>j'nZ}ɹ];nGS9z?sŒH+g[R?/y8O&K/o~Ų@K\=/G`ogr77 ~ՋevK͝# atJciS HB'dq rfs!W`2݁$@lV\,[%$NUWn?u2㕲(b K䛬x24367,fx> dbbcj^lՈH\'8<:M-Qkf6fn6tҦ:E ;*3"-ޣqX6qJiܝ IooxGlʨN$ˍzVkuaRVμF[Qg֕ ᱖r^-mKb~Ԩ-l.vv*9j_];sѹpbpRsk`= }gV~#T7ޯ>[č+/+f6yk/ܵ߾ؓ@~h ;}pzޯţ^v.xwG~s$_][هwW#9{)߭ B;8K _ٻ6˞'Ox C.Tu#뙵{7ߍ:/vƦ uI=q\DiB,L'Rb!Ao5>'rC@JKU'28D _OW(`5C(C͆lHceL I$0QwkcVStj [_Dc2Rpj(@(:W tkXeRco 7܆ΆQ]E8JҔX^/LBѽ*Wž1`H-y̓ddǧP mdhx|^^mt II|1k.2PSO48bp խ‘'DS+^*yHM>ɆzܡDQꙀI 2V^3[|  F$v@7a}t]YOuBǧl^ۄn  b!hMvEw'-e(ΝZ|y+?_n?k_ş^xKkǡÇrzNj{폥v}{髿\辚}#O]9E?`w~ZGy2YW{KyB +H_+??%eSzT{^os/ Bїt59V"82| % ]&G:=Goq@ТUEGp @ 6<==ޡ:̛N -T dJ|?j$dgk{fKZNl&Io؉aj~>.賡WyԆpTaD<5fݓ}hN5¨{m!)ITg,lo."1AH.zB|UHGf;(鸄|&LqWBYw*$({|KZ2~tFBb b04O@  l]۪֦BXl(:E 7u\$Qq#^vF?.@y4w C9;?r1ge))CGߐŗGX<럮|Rr9;aZ)?]ݑK'ß|kg.fz\-vW_KGG_x@CӬ$! !BjHY+0K "@H-' 8R)&R!2MT;R̥]*mQb12FU|X0@ @1<) jZ'Xn̨I yau#/mb=)L*$ECYTwGT*J^;{%Ǡݹ_4V~j|9MNG?t-R+o p.ݡz̲ ._+S=dsۙ6{G,Yog6R?uzH.# hZO̦5#h8f**;p D^9u[8aVPkmM' lhMnBI!sPu]JTYP :Easvj6{/7wDLIen=fsNnN#4<%qeIDn(rA!TZ; JY\(k*-~Vߩ&5Jw)Ifee~&}")Y5E8 mA)FeK@HɡP!u* eMt28`ۂ6{ ֶlWTo]DT{lgl`_=IMǟ<ռr`G:zlr]^ƾ_.^ aq.~W~,|Qo g>jJGwoLji]@?p!B!!B!!BaFG!BaFG!B3:B!B3:B!BzP$ɲ,B!>GX%I3:˻|2B!>G._}a۷O?x"#BDzŋ\o߾-BkAHҋ/~B!#I2//A`FG_~bS B B!!B!!B!!B!!BaFG!BaFG!B3:B!B3:B!BB!BB!BB!ŒB!]9\~7 qKu z; kj)ޏmB!ݸq.RwIlHB!;F:wY. ~Lo_NbKϺFΜ}'p9p?Pb>x #%/n0yw_;rLg&ԢKJԬl/iyi9wOVo Bmqt#<8Vt ¯@\?~Op2 B!}D_Ͼs OٓK΁?R>,?*~2v?QR'ǎ7]x;v?gl_\X\rÒǎ+Y9=!>=hxL$@//_xՁ7w?Wߏ!BwOv`#s?]9 O|S}\=-^J&`ZS"r2 WQ;E/E 7~2by@ߖ?B!WFG;g(=ƒs/GFu~/TD`Oݔ. Kt`?҅<^@Γ/Yz6'À ɷsz !Bĝ%7#!Y͖VV.Lz"+Kpi1_MLׁɑ73ע=WϾ}^JF#26ߘ.c@G! 3Gv}D)}58W9^,9rg\ڴ ͥO9k‘o,牲^dbG*3q!B}z;j-/]^{\a^gp׮][]]]]]8e?gA!ЗYXs0xXzy}OƀB!$q']ouUCOJ|E!B0#B}y&@!B3:B!B3:B!BB!BB!ŒB! 9l5-j!B8:B!BB!Bm{ [@!B B!ŒB!ŒB!ftB!ftB!0#۹{<,@R!BRd  " " ^0E")[XSX;pS((¦0"6E`dR$`"s[s;ty;?Fht4:p7rUFORgqyyyqq 8f]htF@4:htF2~7X9|GL*:htX\k]jxzj*=1Rcogm)_n9%pU(>SRWϗvŭb_ml=erPk={]'91>mS(WV81yjP:h$?dcA9xH:T+G죙耤^uzƳAyrhMLv|l9p;nÒۨ.w酅 [u7Fޕ4v뮼TXDžzK=Zφ.|uP:hnzLG#)knѓpYYJb~b{os֑LCRڎdaeNo7dXWrSҐd&AXĸ%^=v4=Y-#2IL-5wޏZ!Fə!{ܸ pf*v/8rkMUdlh~f"^M8p&y=XZ]~G_$g]I';7:9_Fת'=I+Oqlj{amIVK'ٝ+sЊ28]+'7hv\$ 9A_0j>ܘ[]˗ Bѝ_4$c$2>㟘nX iZL9A 3CW1Om[kl뷮o1ڧ=kL_'Bم\z}j֓絚]6:|uq/֟\Ԗ1<i`$:{ij+IX" Z AIf(6*{-nVmRQ/o$<9m.P8x Fc[m~auy貽*ӳ(p[>W*IH:3=y,ɹFy{eOL|𺸓B[ Ʈ1܍Z$IMG|zS睍Ri}E2s{f{Ȗ7<IsFdzȀf?I2VxzjyJwl枿~~$IVeb ͺ .stF@4:Fht4:rUFORgqyyyqq 8f]htF@4:% ExԯIENDB`././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/doc/source/user/figures/env_default_network.png0000664000175000017500000011363600000000000024376 0ustar00zuulzuul00000000000000PNG  IHDR ЬsBITOtEXtSoftwareShutterc IDATx{@T? 3k@X3a(X$XA7v[ѓ>9C<}+[bi+ b"%Pa#Ȍ9`ܾ x͒G\f^z@B2:  -XFb6 MfIm16Z XyE rbb6dy ]7Bfso)b1yi;0"E|裠 L_+2j(a{VsU;;Yfyxxw}w/VTT_~_.b渫?T.7AkV'WxdZZZ,fSӞ|V78ѱ OLM}3%iJg$1e.q`ҎruQ+DDkh3@7GJl"kY RGer'D?\>o2 W*ݞmXbgΜٻwO<ѧOuppZ駟}!\KDׯ_y~.\ضm۲ezSO[[[?QߖWٿ'5D7PWklx>kǙ?=b~OOW^O2Gp Yfa^E}M+k wPSgt2qrlbͫ5oQ=Y#~f3bk;Mq 7ʗf,^]mH b"}q/LAJ[@'"Iyf y{hs&>:s0}_^56䖞b Zĉ}||.\~kF/))s8{={ &M$/SO=jz޷24\I^ wvv3W-Q5s {r@, B2|eO=dL,ꭞ'"q|M*D<џ^S-~ժ=iSy1 {61}:ռ+#%l UpcyI' \du͇u|Zu b(sejV\#^(;&.;_)Imʸ=ף,ց^›Le32ˉ,kgtbxJ e@eO&K$"5jTyyyEEEGFgbõk׈O>JYH$։Vnnn!y晦YoXf30պNj+D"1D$yn/%֭y2&KԨ۷?}c_:8H||o}P64VODy_!V׽-jwN W^,[#;D_]c wU{"4eѪt#!ùr6!#K'ΈO])+[Ε*t&?1X)_Ny);܉ 5= 4e^JzV-)*0cG7˫W,J\#7&9xQbfoB]}c6=M=.ߵ;FA)H\ecg͆\~jgJjWx2_.c=JaQl6Z,'u GjnxwSepJ5y3 Rܚ,%ÕHXޕrMr\MS4pU)I7߿1SV>gU9aYm"Un꺚5-f4 }T*=CrMYuk3]3m(z+'-/fr%]aĎ鉻hYg! % ؔhQȺuWBnɖ=.*9˓%̚ YE=wdQQJfw8V][h`C:/u'奲M n˙ǵ]#"=Wg.sSYKIKXsa}sͦ޷y𑼅$"EgG{;]b-m)߆nիW/"2ԮSύv}Z_|šVe̛헬 4555RAsɯ55/KD}o9A#ۊݝhɱ/ۭO.5ټ>tjzSdkGowH`6[d!b2Yh1L;+Ѽ#0 <<Ծ-b_ iw*VQ[o=Q]!p"ꅳwax;/0I*̝31IS,+b!]1,ˊxpܪyys֚XcbڻqFћQܨx"߬8G2f@9U>3SF_n*=6V|G3VԄL>}ioc'v y5/vlUF`.WZa;9xN{ju}Drmň.sj6,7 rO-O]()e#721.J_c 5].#u_o57n^1ܾ#"ƱaqZUJAL~rt$Nq2n ` opD,)Bf[YX`}d5mGtuy۔m3K1>Hy#ֳ˺]nsuy(SF` Qk4֓>vevE]f5Ů_g !wy"^`>iTFEбP(S9~H[F>=76̟fGK{}w9{"Dd1@$"tb׃SfHOqZ!G|ՆҰeV#mTO,˫H9@}Q#-.gebcetC;Z+ -V!ܸ,*Y:1hr^mB[jJSA5b;>Ke3R 3}dUwT@eS28N{XGIEa^TuaD[˰ V9Sv]֋GzJHtx'\/KRO3ķQ1ܾ+\JSGV'n VrDSqb%"J7l[Wl36>V;?}ۄM0ؗۑ2Ǖko_8ý]|m&ۘ18G *ۑF]f5HX~C&_%]iXl_!["@(0B;{;H(6nd2"RT6fҷͿWR{ ˗O>֥}^3Uɶˆ˗ږrd8[[[QBX:>ZJ5T_=C ʰMݬYVIUj"/~=_tWW'~\}'XOxuVuѮJ d/DĕbX:7'"^U[(g-OX Op /,nٖj]f)GFxk wk).vTk(٪]*\{au+S}p8uenʍ\pܬaJ*ܕCLJC@Gך:m׊Z=bk)U~~+U+ݨ-omy^xSyeKSqCj~ k2Ӌ4=eSSsU<1ʐ@y6Շ4'~T!_-/HSʽ^|mlleUꪂ|cn6كuHKGk:Y-f,H@?RTz;;HmUV=o2,֧[///@peV+ (00_kk޽{;99yy fϞMD&a=dn2KJ3X(@ $EW%-mT;Wx \j6{V$'$ Y7y:xTa=bofq&i[첸̙2wh0]DD\ڬ|X(,]c<,9&%sxNMgIEv{*X:BN$LS$Noތ =.8-ٛ!U0/iCȦŷܸ36Δ"tYdnLJ8߰y9"b܃c 11q7#d\fũW'b݃R#Dl+,9ݶn{ʼѧΎʩIqꔔ9"b=#%(Hݸm^0*qśl_O`Vzݶq LN!) 0D#3ϴn 9!LW}T(č^{FGl،ũ2SWN%'1I+&gS#< ~oۈ:w݆EGL͞>#&h48b1M雸?Jd$,Z~!f3'-ϼڍ}?7;0bFhq8S>/^ܱcrF>k,Xl}ϨT*t{~@ qqq!_~eƍwMMMƍ{֯_?dȐ("jii0III|MFXtˍS? ceM+Hho_ f3?v7Nui۽?>z?a16窮$>1{39ÈvʔɉLڞ!ౣ)(1[W3$&).Oؑǽׯ]tE,$!H%_vil&,DptQ8?aFB𭝛; 0W_}e2^xᅎѭ ETTTII\.ӧuz^׿VTT;wMD,=m*XbjŸ=%ziid~yIJgIQܯ6d(_gַyWxk ѳ_FDB퓜f`hh@F?d%t.ެ<^掀Ѝ|Q_>#Q3?;-L_,Wɟ㿟>u:/|M;CD9 H@?&#z29B1cg H-E1Cbr`XZ===gΜ돹cp*b~Jrk""%/KAgҔ]| ?T*uus~g\( о^( &2d!]㒖"o3 "ySz<=Ttˋw}ŜmVji!^OF#Ym3"A;KG#DTR(aO#dc "@Fdtdt@F@FJTRRR?4d0f3z>P0PxrAkk@ @a#aXfd2L< z% ,wl8<dž..0= 2:2:  2:2:  2:oFaW^ OA@KY=><=xmQn(+y ï<ձh0 IDAT3_yW^y%|5% w}76{~ToJ%=n>lM*=壨=ЍE݉ocv 7\XKNK楒 7D̔.cMLL;113qNp UWd{syACCE'[A!}/I{GQ{놉/_ 6oUs3̊uշW\!'9#笆!b^je+8V9}艬sg*+U^}Xa_Ulju̙/Q"]+%"U~AsLzwF*|pKlwZߘw"lj%"{ח&D%5~uniub~2CQ^uMMZW]ZפI/";?;d$9+[zukZNĹeG?о~S#2թSGx=`_gߐJj:s0{K{]R0yGs+XoU"x7zqbG647wc^j:ͻ'?ݧi$Rg랙8W+Odowpk/'&&Q<-pO1xQUPi^;n`}q5+:SJ$5nXgZjݽ|SQ$>}JQKwij%*9X^#{/q -NzrW й#>km~k$K.6O@DʷF?+!"W2_}sQ?5|D:&}HJ%KD*!"Aokab[D$=i@V7*~ 3hHW""Vd9h\N{Q5|[I6 (;^;Hn䅤b*߬tc4<ݺҡF1Fߨ s?iڨ|׏ϵVt#[NшāD΃dӬs)Ar"ͷ9*")CD3g6?]b߱5GUDp+) .d$qACV^jqpHnJ\z So-x}KvZ4}"۞]ΛsLB\'X%"9 G$fqLrjۖ%]C-z9qi'F[z6gol`D&xnnώMKe yCwLro=ODJgn~ S7Vgen8CB3umFw:.F.g\2sG6V_Br =zwyOr9Fq"簵guDxF4U}=N|~,ۜcDM͜c*DjۚdTc꽟ّ*7?΋͹'"g04T[nK)%/K=xAOoao|L1^ (r ۜ\25uD$?{x)p &&M[LDmW!&:;tȨkˤg@\ )IE5G-M-6hN?u?l't񁣢=<5|sV1fGi#(|0\Nb8y܌4Z]2у068V+RF;u4&%BrƔxR X8<.mJx\1୅+]ZܸlB܄VǷ/11}UCr„pQ _t. NR$ǽgbC&57UD^Хdu=gm1A#gnhҹ&+&-]1b\Zjci1o].qjǠU)D8ƉiicaFO8qŽ-щD^m#d}̌AwZ쥙Sv Nr Ã2bD$H~FvJ;ecŘrߵ戈g^~7Akk@ c/|?ó%Ӿ ^O1iJT!SByyZqo4{QWFNiKB\{."9s^UQ.gQ7=аgaXfd2L<{_/PES6#93GWš1W_wzݯ~ʃ,3XV_qk?r[6^2Pt&wڍD!΢Rs&/!&ARj=@ $Cѕ TLD>?QD@"~nAz<^zT); >&:㽡3pxA+V$My-U^rK'uZ~:Qsw{3xB2eFmennM TOL?JrBss!-_tW6lذaÆף8hG7>#aJj1 u%HO1]9D$6&)@i:-'{9Ъk!"R)Q.KHd/&"g{1ADDd4vߊMJD`oODԊC,-kQh0MY2ç7$N<\u<1n$= 鎬={?Fo)9JaK?5-KFz zqGE#{D@gBC}X)Y |h~s<RsdCC|\SܴEΗ3יꛎxHkuš;1C{ʾF,Iǝ`hKMy#In況dDJ_'ʌn) qCDG@_[|dV@Gbfd2xuuu:x'oo֔fJG}u_٩fu ~{N@iQk 2W+<|oN?Ŵ7)2'/~`[gqss"7{5MWm r-;sW7s)Ј訛;wXV\|Ŝ~;_';+N7pDDǐLsН"-*F$:ͺS_d}PEu3ODW@oMG"}; +*8m/|{n $&"į9]=Oo'/QQo/~l]롴Vdߖ}Npnh?0GH" "i[.b휅;k;k8}hk¡²5L~k?%Sʬ#*δuU]oF}A҄[r eek?XD"ax"YE2oڊzku7KCW_?-ץ, t}}+u^)\}hk¡O7ӯƮ̭:o=}JJuk<;eܵ]zeieGR>ss\eFV-!2:ƴ?e_"Io\oKi3H-8oFtiwjۗHڶ.o&"ç_"׎;47-;EΎ D\8wknE3QmʅItҴ2 5UJSz uQvo=9EΎtu'+22 \֦qOĜ~R].n/ivͺ#Io>\GEG tseG w,PL^V}}4DD *[w5w(980#XJ$h򖅱D:{}V+CDH*#->sx#!rEqCѣ _o_3Ӻv\Udķ[%9 ڐ!acf̙!7铨OW u_´\yҎ^!1q":X-[޲P?/?Kyˊ6qm-U쫸:fܾy쨔v*݁CFG$Mo&#mEް5:$0x؀h_.8XJDR77)Ҷ7~nn^0)v ;mn4-~{=<|B@lDFR-Ok㪫&#Nwi͈pJiTO3<C$E8Y[O|;;t"" X_Vkoٞ+*ԩ?NIKT/4m 7Wy8zlhj%"5%"}RXokGĔiov\oFh'pI%$F )}Ί@|C;:@gO&T1J^O!=ڏBzX}jgGD潂o^__%;d%tbs'N!zL?cgcU\* e?nI\}g}ZE/j=c &#=ϖ#Hatf:^Xzwih?ޒYa1x}Dtrdc;mNCnT8D.sD9K]mza$օl#Vz7m82,.e54wkg۔"cݗ{esVQu~4/888 `]dt߄k+~{F%-`*x|Qk DW=GOc\gy][ ;FyF ~2Nzu:N)!K7/#)#4GD7.?]q vHdt?6_tSv'/7#Sh{6Zz]z#H2vCeO Voj}*Hdt?rH;EF֡tZ񺶾ՌT򥤣w?}~osI|š/ O7Q󡴅i>M! '<3 +ߴ`!:Et׈#-K=|v9 m}zX1N7OiJ Z ou6 qI|BzYe͈ncemn8l{֟\`I֟T43@=tC2:9{E;56\=ѽ= HڝʎftPWBRȶ[s iK=-wٖԾӢe]tңG95EtQnmҀ%Qmݚ=^=\h>oGO/Bݹ2[?i;kq99 IDATA!;73tPaEumΤ X_`px(4M[ʓYP_TSf$ѣ$oF/m ^;}bE`/W˧ wn)nK$E"[Y0!~N ?_sgYIK~}8vr#%".?>iNp?霂y{I@~rO;16˩D_m(ܗ۶oQx|ua~=4pK%"\wnm Rͫ)6>ie;oͥIs ||u­~C)7P#C[:0*حͺ ?=PnOWdꚥъ6 <l6L&^cJ[ZaߠOp? -,;Ul}/8vZt_'8ӊmWXq}k`@@d40qK"t3ODWҁߙ_Xq+1N^>~c &txУ2z@@Jgxfϔ 6ŭQceOj9}s%~2ΊDstdߏx^^""gG 2!5 ~Z۰N~㗴d;n܆6L+:#{eȜh~mQ9Gao8>DOd=G% w{ -NvtO[:FΟ>RtR[_ӗ,ҙGs3<*'R"m\Mps{?3kPHC=So-xdo91˶fGwA3:}] -M-D$ZEJgtDdhѵQi^k$""LDDbsc㢪p0 1h@B).RjE7ln Mn7-`oX `1$Ȑd~c%apÙ99U`(RcUb׍ )=bNH}ep2X9NG$'vrӝDzulj@8+sSۏss+dMG!..`F#?@AY]]y~A#?0]L)pκ9m#"Dk՚Fr{+YphtѐI^ {o%HěT W$+sgfT,F{(HXIwr>,AD;[jjG[u'Oc!AX,fl6Fp̙P ر9F,F@dO슅nlc ܉J"2:  2:2:  2:2: %ز2܁ZJ[o WluqiNj)~.\m֭YuF"8M]zcr -uZl>WMҘeJ[LqIBf=LSUlL^&쎏}ﵒE [ѧLE)x(c[mh?R9ܞ^LF#2u]}(sekE㹪mUAǏt)ݰ\>rr=GkmCcVFfKE9!*w2ug\٦:><8888xQrz GW߶okrxpppx֣-ˬ?o3J.lŠ:i$ Z.9>&&~lؐ쎏x>yr΢fzջk92TF+MN.؟[CDdJ ^h "Mi31ͪ~DDSn_n=U)=7m񫳪84yNb~7Qsf2tYfty28<~嶣hyvʘ<޺GFIt2ss~e٘>]'#xIBDH2@F2p-mEOv %p RE.ZMs-uU<'mSmg2vL+o+NM" |݆~'j%mݶ2&k#Ck~}h`JbM|;]$DD >DD-W_%3UB'0T-!2àxNL{8:ԧ|O [yPɥUx:UX0P|JOKVI{-J^ʏ=|.C㶴VS=~""2Zۗ.^l*<Ϝ7Mv!^$IR_۲e˖-[^[5ߞZZ}w=7z\:#I:ym?2:.Ή'F""qr֓-}ױyō-[W>: *.~&y֬ݻm\QGJ$HjKZ0ݭ9{[^uj R4Gk :>y7Ѽwk/IVH ɍohKq^+I/_6۞: 1ND۶;Zqͦ7(&H$Dth#7=w!=yۖAƼ?<'LJ?tx.L-I=dFfvqz~ҖWKIOB{ՑZ7;}K+uIˬ-t7ޭ2} ZyʂhZ>e?|GC}}yiH \=ˎfLI]C~pw]h.-,@dpߴȘf_2x}Sf҅@gY""OVP􏺾_U߿>syy1qQҎA;n毟X j-bGD<߇N_S_h9C< i?2nRCGEѱn>Wb 1Ӥ846vt~j2USTTmwsZ2x8r3NСT]}3f z8h ;Ifʈɞ>9χƱDdrWc;O:IF2ON|NDD6_dنnz,1g5ߢ&YtUk@HyoM2?1KɔR"2h <ٳoNOW~~?6Jvt=SJVIUmS f"^dݡf?n'!κvυu Ws¯@;mUis;BF?;?Ӻ?3]jdIׅ>~}ȲGX"eϜv "//[mUdwԋ-WE[龩DD}cRo#~4SW/Wu+ꋏr\~==qn]XɩCy-q߱gߔj?OIG;>nZÚ#}j¨pBu]9yYV6TX➚ȳ(<9断lBp#&^F%cD"%J`TAF]ni_[}]n.c2:2: ›x`C0Yx7fOYތ37}5n/l=țQq]lgnH.3l,fцY %(;hމWX;-+7?0h7-f"{<3v'WgBUM>6b@7 âtٿT}Ҙk@Ec:7Gpuz Դ魿+"cüPxpC_unC_7\6Qqmۜc׽mk-/iSEy˯Yu+oC^XS%c7U_^oopDD2Gg%tqDDI ~8qw{}*}FzȯuW}oMϺǭ]zoԞV7douMWs5Dq[=eF؜=^ CoU;sm'*.?ߞY)2^գ'"=*5!7\]}]Ў02|Fx)~o !wMJz K21@OsFt:W<8YvهlWN GĺGxCND&m̜lu7r姨/2^b}Ogcu}E~c} SF沚.1bHT_6jh+, L_ǚ%%sCk?aB2{0w{QO[%mmE=%x# `'ۂz|5GM;ܦ'O`.]`aoWs羐]cmRR4\Q17ʽF@SCanAycH$zv)(iH"[>0}nO[6L=71ѻ>goF/SMNy[Nk|o^Icg}<ּ ^;PX dUQuW6x boYK.;~@cOpKSHR+be95]+jDu)lѓL0t蕻TXƠ`5WHYWYȷh Ni6֗o_Y=z^\EDiWKH\}~W_&kzr_][ҷºGܨa],8u}}%R '9F'<-QQ&B7Wp^q/x?}CuA9p۔f~{BJ|tBD:j |sw13 0zꈈ,KD2>$!1 r+M!>,+5&"ҵ9^2%f0.f*i{G%-OKvm/4 UVKJt.UYGyz]X^s8RK-߹KMM yMn@{lb\B[qHR" 1%1VKw\g:U%F=6%e";w@J s5eTF&D\_KY=p3&.jrw_`κfkT}}Ϋ77{ȤDD/  ilZ=M<5%V.}G8cHbjb0_3 IJ]/Ⱦa>q)#eUWt^|{t\G"rX`fLz O275qmzRxɚT4S`\UO}ڋlpl\=sS#ivJƮ Y>T۩RC=p GzJg\l ya]g_l ,ըm0_Y}MceCZF"|r2Uy%<''Rj^Tw.VC0?IxG5uGT)zya ?VYWXõmD3r{eAAOİsd bJn2y74q@k'zaOesd^ y$kL."…[RSTS}}ynXfe)/&x_}6pZt&"4wm}~iP)8Vׯ! .i(׆yD=$O|KPM'տ+gZ`HvwS^w ({0: apZoU/%rC TSK)c [{Cu]$ sl+H-&?zsI[Y64p >o1kL>)\\&N?긵+C ;s*8D=g"ݛAAm!b1fh4 3gܶQdejڞpV䊁ab0?YMecMgWEO0_%m> u]'G$S,.W(X"g %wtdH,Dt1,r.,Gߥ#kgHAD::ʉXڝQ^95%>C%E*(Jr'28ir_];8bRNtĭ.)n-hP+G d*XQk!\ IDAT^l1 O|OM-r]5yMc{5MM]2x`maN}[H9'R(L-'wP;f=izL[CŎ v!"]SYa'Wwk(8*(2ߑxYGUVVWw6>Wy&W)!&p!)UoT=buޏ*W%$svo묨zqEw5GՕyy5NbuHz=|# v$j.[VPynHcvq')oM9; uvD=Mu#U%(z4ՅE<:/ЙHS^tuY;s ۮ7w"b}eޥSԗ\_o{ܜ]'ǐP*j:9NSsgNYץ_A:\gCYaV-9; /jz"&7U7FȎr&`,ikjZ&4m^ }cM9x;#"PFu _coY^#8-d\G*ɭ^N_| ut%TkM "+ߍ= vNM6d)jUF,qqSܕzGD,v&j+ɯyq>s~\w>73]"֋s++jኤ^|e >?6_bJ5k_]6շ'v#V_ope}=8.x9=bS{Jo)Q.tF::ב*ٵ'))ؑz*r  ^ (;B67ԙ7]yROEފŪ`ſyr 6H T)|DbKD2o_G"{ǭL q]{T)Ɂi 2zSXPJRn}Mf43D!˗epcz=r衡׿[ #etu%e͚)aa~8c܁LMJ*5Z$V(վ!p3`tet3 7ti2; *aBFI%I&3x 0&ӹ C$$1hY C@0 4:  ӹ cD$ ڲNH09Yb:CD bќJ逌c#"X,B`d!F}DL 0D"'YaFq@""B`! Dc"0 0$bHD$M"t7tNϿiѦn27qcT]]]mllf"ol! 1 Y:u@gH$" @lDĊkLY.:pIxePPHx̙ & 4$,o!E-dox``'F@$16 و, cˆ+Uv"\l:DV766DD&~0 ( R=(ߢ4Fx`& ,ߜϙ[P0F9ۊ|Ƌ]X܆H`1Ĉю跜\.77lmm]\\R) (cݢ vӓ)NdNb1>s9˧',SE6 AD"0"wm@===*eY0fr{{.VW80:B[Bߜ8'P_`L_.ȋ7gMsݥ6:ҋq&L JE"4 fccwݒND b!1[L+Q_Ψ/\-ׅN 6Y ͢0   "ݭBx\E]q38ɹ~Y24(dtQ_櫭ڎnmP_/-Eމcnb.,(lP̍{h 6Z6-Lmۜc׽.e)f!/l,c]`vtS癛s7v}ΝjvAnE%J` .8[pwg'_I{  ndNSg|fg/x7.vt'on[Ue-[4;888x䴼ڟ5WWDc6h;H65L%&"(ۙSM=䤭)lXg脄0&kzr_][Jo9{4D x˛{ s D2g%StheHւ]^ӟ"G Oa1MpGTDlX.%v^?V֓q_I`S]M.5~}wT*gM^aLTe=aecoYGӱX+UI[3 IJ]/.l'褹D87)59ؔ%K@{a殒FwT(dN0JL]D,(PXXE91Dμ ˽ FwO;x韒GFFFF=VE7uTxҧDD/"""qIb i)]Y'߫3MOB!bjNN{?ީR""lj۞ *z!(%FX3~|sf\x2|$ + -};}YZAR-;- -ҖY@dZhW&/Yh*h`EgϞrw#GD1- _qfetq<Pȯ7֦yL p&4]XVˉHvPYud /4q]SQCߔĪ;j9"ȸBcc}1d udlBL '?4ϋ_yy/}^l@d`}ȴxߞ=%a\Θh 퉏'Sy~{wN䱮m'KZ8ǾU\G<4m|oZ~妬י_/#lr'O*{{('&fZJ\yFFmж%;"Off#"-Y4m+=GԲ'mk=wd&oͦڠ߿#%#-N7v 9ףՑ9|OM[Kr5?5@'G$S*؁`>\GD G9T)C5=_`:g,JIĝc"zkI}OADO""r9YJDNUx&HHR+Nk "\Z.et*]VY)l|:L N\RSȩ^Hyȍ<}sQy~onFri+P)!&p8XZ|恠Gy̛.wkv{<1}q;u6vmԧ牕(\=DJY N`'`dX{Kr%R{ `2=egC2B@te;'}'9P_Gd)I%NҁY8B!utsƺMSF8p;h梷67e|Lz0GD\SCNMueQ+Ol]lvC+J !Å"uXY(*/S<:*U S։C'H~&};%忞zoQ<5iCatCS8yT/rEL3:Ӂz[ٽ9SR-?Gr}s&no4?iKݻwf=:]%fbl܂I}wmu]0ckǧǏuOJ.t&UgS2ۿ8EoiKP_fy;;b YYԧ?PD? 24_F͛&:=W.X\L{_!ber|hw~}vU ׆6GUt;<܉tPMY>і/X%T"6l׻W]Ħ7y*Y,K|7U/9]];/l;sQĵNw#ӷmHtcYN:zɉz)Ԧ Y?v-b-bj?{.Hs4g':c^ܺ‰Oܾ/"fglgsj6zСۜ#x_}KS. |fi/{*3K;xJ"mܔt&.sC@C&MmmJ4i&G|JD$k7.fӞr'"RF;}ZiJ"r&o{,W_\/>7mUI3s߰5ǧ$,>~Oy/s~.sG^|D2tvT;\3nuf75hՋD٫lix>mҭONx\CS$s˺kYHnzԵߦk3 qƂj=*|NϏoؾ~WŤa D 9D$VMRRYwWZ)?SL-9ELdj?vB7ELDbUꍝyuKĺw""[ʁ3:X;#=u]>mf./X)%bOs""+N~ɕuDD#'hj19L]n?jzpW-zNwck9]+#-uOI=9\5eQ:]e) vl~#rl->ʉ ޾K),uZ2=#v_ʈݦhwǸn坹60za X,l6F`8sLhhh4ͨ `Y\q555?z1d"͂f` ^D};1djDܨ?R<6u犚'MP%ptKH:?0 ПTwS}$tufMRN|m 7{!)cٿ1ap5+t\y˻Mꍚ_;zHU-kIs@RXQ};D\YU?6!NeQ v6[uJ?}Qi/?YqI$3p=aSkBD$M{މfw78/i)` 24WdeWW⨇eR"Cs^ʤD$?DD|a `霩Ϛp(DD̽6/YD%kAAafؙOKHr"KDD /ݽ\cػ<_T6&v D 3D{'m}{VKo(K{H$"'i?^DϺ6D$ !]l2֟%Rk¿V4GI6\eKY.MsO>ӂ< < 7~F2Rk|gq;g&+J(=Km g=:iim?zX!?FeuI~3u&rs0|@DQ{כ_ͤ""K/pט(hDd?59K'QhGds?Y@1NZcKmW7>cw$"=Z%IBd~rٔ]F$nOfD}WwmT@OHO?uDOUmE#: a 2%aw9D@Fdtdt@F@Fdtdt@F@FQ@Fdt@F@Fdt1eƻ<h4(j5 fLWW fb 0R֣aaD"aAyh49s&44ew3V"X,k~BF![D", bFDWtdt2nmAto}~f bFg,6u  G@Fdtdt@F@FdtdtOH[s $,N!YD0`)$`pYd ]dp ShhSx]DP,^*40),"h i,N軰;sv~v&ON$Ϣ0>tg꼼瞡X*udht\ݸ;3؇Ņ\.x绰\ht\8 %~a(s_뵵J͈3V޺qs>WރM=sf-v׻9m scyҶEDGnZANeht}X/M#EDUK}-_rUaVWw&~P~p*[iw24km.WaMz>WiRj7G-I?sxkG}q]X-"rSM""-T۩1YaѰ'k>󴼽뉆3A<)x3nQ[D:oytJg"*"t,qys):-Bzqmi' yv&/gg!ۓf9=GX]htrˎ դyӖitMHPV5WK 'ak  S\nnjߥ?[cߏ~X˅z@4`ꖈ؝Jc* *ŪEG<B5V+ݶED~MDzž%"bK->%ytg(:=X.Ƨ3ۖ3SÚF]n$jD\ؤGD#J;"3#nQ3S۶81?\@`'''~q&.3κ4:Fht4:@Fht4:@Fht4:@htF@|w\o?ZH`ͫnK/o,5Ϫ] HzpۍH-ݝ)O1^u JR9dHoϟV7-W>]t_,LSD4~/yt4EW~t|g7MxqWGێ15MG\1&C4^<~X]YiFoOzbWVKD)HUu]QFgzU(N KX@({]TՈ\b:bB刈8VUo9"Vٶr7.#ll[ɯ? BjF2+ˣиL$ t otՈ\z#W]}\T&fRCX_-wx ͋HfrԧJ|;4n/S}ocj2W3>e[ݚZm?٨9===99~h?3p){-[t*mD|AħnAOD ESS-E|.#ht4:@GoN DZϺpnbFht4:@I)L=H 0\ӓ~ 8htF@4:F@4:?Jy]@(6Lx7/wԏHx7svm|lԐbfǠ߫ėAUD_=y`_~qUV3L%G%s(s&bח,T;|uDҙ׶έBQ<5xP/7-R'RDxgeuSDt~.5&bv-G\/.8;Hϴm˴D5Z]G4a?A’fFBKSb~qSD F DٙhSl+Zx*uI>XU9Ui"=acMGvnے*鉸tM|HHeB/+.&͏{YYZxSs߄CY~?DJ5qcvcus٥%"|b7J1F,,MU^fs$"|nqm!≎[ %p'.LT8L|,|S(O'ξwD@Z2}QqƁ9{DLN:+兂(P"=䕪&ff핵B!(xƦWT9; E= S\LzX&f핍Rn$g(J"?%#$SXzg~k.Y#w rwzzzrr~<g22#htou_ g]Fht4:@ht4:)S"S>FO$ 㣣}.9κ4:\/Z)^~IENDB`././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/doc/source/user/figures/environments.png0000664000175000017500000014215700000000000023060 0ustar00zuulzuul00000000000000PNG  IHDR&sBITOtEXtSoftwareShutterc IDATxy@?OB2&Z EEl`+TVܷ[S@}EuWq,"V+"HbPdq$9ΙsfRB!zX!B(B!0B!ПjjTJmgC V(Z5}\N7GuOjdRZ*Nv !B0F:IymBgVZKӚvJEtMVU;.jQ*xy^B!per#`j5`0_& `2Z`^_ᓶl#`7ȳ0>a!Bq{jmEې1C222222b f1Y,Ȉ`1`2L&`0 &ӈ vB`P)Y/<IJ<`b7\BFAj@iEV/2 h5 -CZ]$_gCOWZS Bj,d۷ɏ4."BF`W j5Fa0z&-tQ-V=&al$Z*CA|`BCVb1E=0`0Z&0n`0-^3  bg2޸qѣj_ !Iݻ_x{{xSN[o5m4fqLu]##e˖?xкroΘ1C&t\_X;^ίw?0mo*̽i;:V~ԔijjmGc5t2-< ?gO8nIEb{Hcq!Iwx ;(0VnSs4j]hL&02H7X`źoɟj*Z#@ZUUu{nܸq׿"""LLLtd/|H̤i͛틌;v /'N0a/R\\leeب6~͍4kmAr{h91#ḻ)*+1~nj}S*$Ӕ4yS#kcXd韩[[w$:ZhEs)+0x+$h_Uaū{=V%'dyGFU4zNFXx_zmWК ~z4/QD@vqK|(3PjU#龏{غh 6Mѻ1U#:>H+3vd߸`/zr-FFZOejd1L&aFQq`0L`2?U;hwTIj1 ?z(>}[͛rͬ'NhQQBXr׏97|^xᅟ~)'''??EؠDRiJ0ON~SygsyO"-4b)DօQf {9f 6[T29=^|><kс+"Tc %D逖V싉N9<*p= GX>Uiw"cw$%.ߙyUE.lF詪GF:Fl J\9)t:F8U$GD+£2݅$-ߙ:rW\0O;whZRYL1 ^ueƄ9c&ӨZW>gggӦM+---//Ahڗ_~ܼU, R.4?qD6}^xAN8ԩSj`X=a2 d21u OBYZ Ef`pdkZx9Ԍ0$*w·W[H3)p,>^lKELJ ||(x7:;%e_BHZo[Dg+l#lIČzp ZP6&ee'2>_H-KKY) rUq]ɾ|X5_-/{*v$VS}@{]M' ~*H"q5M۲T Z/Ҹiew$ROlFw>UnXݞB-i7:IZUFX\P%% I)i:vo3z@`]FS%j$ 'Ant]vbR/uQ |Ҹid'Uz^BzEFZ/jcKزND I @V; cֈHPbڡj)@GmHr7rڲk=Mam37:2!^AՍIZXBv~$/%&XJA~^v]-/tKbd!ADqapLu ;t8neoXQ 6T>}N~Qhģ쮱ad!d*sɗ#5Zmj:p{qtHp8:֠ /Rkko&~j R٩[Jw^]T*29{[!{Z[ӕWh)f-Ą"8n+ut?/V|KoiϔA "dMJMA<($ʼna)6R265]@-޼+=OG/I;TIX(LؕiAܪ6\ixXRZ*1=ݟVz ݉Ź2D|ҊER -w=oXZRzpS@KFe~+%ya1_hW#1&HHWڼ޻Z3bd tݎ}N@߹X-L_I!Avp{* ٙ]!'|ښ.$%q;+%/+՝OɶҧN/l{XW{> y7ȷMQ5N8%,_i 6@)xJ]GJW-MJ1ݚZ8doݵQu"wxhSw"3U*.~Wz``욂}~AiwQRPuّ@WVz`IU|gw  *DĈ|%{y; +-J`ӒCu^2MOXWݖFZd&! bF~e]L`NQae9HxЪ>֝5uƌ:{ҢkH$999N:}4Mӽd m&1Z<')t.1m *0F, ޺_ ΌBb)Ux;Ӎմ];{WtSs}BO"`z t"6~,mH@U}JDP_OHiIFAP'Ȏ*-@:O/xTH 4Fz-Mu( TEn5K7DD_Ӓ20GHNK6WJ9  ܹX&T+,٘R2()0&-MjlMƉwHA!b[h'/[WTf>k_Y5(B'*U|2A .\zXEi0V7Z9jȹ~ S |K6U_;Bd2  - Oj//)CUSp^~R\\5Ld몕FdCHRRhqEB]#) VHz=PDwe7h )H_$66$uGtSKr@ MK)0X~e\Sx>1[DᡟMLAXh 0شUu/:T*&Ho4ty]miZݞDHq5C/2bkrT/Y0 =OΝϞ`Uبj !DxF@Q$IlZ*U߈^0G7lӮ Ճ+ AwKS^OLM (1<Z@8IoIWDzuX7DO+e'"w .i 6U/ >vEGBLP],Ie㕨;MI(h6gJl&$mH ߀=uRN+p}t}D $~5?w `wU`Hj65JBQߔ| />6OLTʠ]FeW 4OiU cMqʾk՗ 6CiMĔ2tcPv 5_^ FeHɶ[+_k)$%)2!(B;7KIا*"BC7J銡T4tRHI잉4%< tݾ <(L0pe" ^ouZQ{T) lH)w~1?y2ognn]PHMEkl7T>zsu[؍#:  #K@w*7  FFLcI+ >@,Z=ڴZeO;tCCCw۷`ܸqt}1cKZZ+BkF50w.T޾_ʔ4gtP(;':Pv+Q&Oa8 c3w 5RfiչO[XEtb -.HzN@;nVI u4P !g#έgדB)i>QQcrvO'N|GzH+HQ4pM;TGT_*.PEj{Շ4PWɒ͉";nN'j٠5`P|8*%抇ȣɘDze!,mN-*0i~*Ch.q*sR},8qrs"ۑk:vcTg}ZJt4V&0$ҏ>-z+*XEHw4i;=ؘHCb hIu^-Ճo$g=R&9uo2--v;Z)~w^{G#FZY]Nx FcjԂ]껉q-_Uj/EO{HE8"H?"LC6ogC\ܰUsdUn?00pܸqcƌ^^^~ pvvvrr$_|EqX>c[ׯ^xM7qIѪ1v쿷#T*~dFѩG}d$ #c)M !BqQB!8&B!0B!0"B!a!BaE!Bqaݺu K!B=0jiivu?VXFhjRwޛoEB=Ca`EO0jZ<B(Bv*jFFFjwHE!3FzПA{h4IB!LQLF2` B\] 52LlG!ADӝ>u#(B!a!B! !B(B!BFB!Qm/SXB! !BaE!BFB!0B!0"B!a!B=X?zzjRh(BQddѓP(j5ZV4M+F%Q#Bp(^GO1RBN4Mg6 )WB4a!B=1FB!QB!a!B! !B(B!BFB!QB!0"B! =m7y뭀-sb~V@C֒wŠ#ZmW)GgZI~얈%o[od֢Eٟ?ҶYrzG/ˇ(QمϢco.FGQVcg =/X IDAT[W. ._>zlevgf \cȢWoKKyC9伸)/Asչ qmfxYf3_>7F\lly{mp_GYQQU}[csgL_O&qq'k^Q{Fل/`Yw$$.y4h;ja|U,Py">LJsV`Uj[E"1 ~:t~FOZ w5a\\\ v74]ćo.疷8N46~}GL5M޽c>xU/X!%qe1# JG;n]wrshcSŇhIs :X gZ-%GZFDz jZ;U,s޶& >\P N0Yqf 3O66*(;lPFG)Sr*{숗uOG8e {Q]b{?ep'JN1Ο5.XL91\D?-p9`5[ dĂ;sc;}}zwbz&1qT+R($)٣ ߟl~|y\f>fR@N!y- G_ bi߿l踺(^))ΟN.J'LwWN~YbhTmJ8{yF^^*El01Uy')"[r+-n1Ϛ ͽj> oX4%.w,+/]<6Ywv4fc5"yZkÅ]ҁ lki^oy e< .i4ABN6zÀVs7ORT\ץ4)ᡎJibbpH+LQ4M5e0FeW;޵zgg>, oƶx ^q-hBͼ)G lL"Y15dlgch=YwɪNi˝bvMB}]cXV=gT!t! CˤRt\9sYhJ~zglYUU;k*A Wrnkᥬs-pv},;01a[pMd&B[ G ﯙ'3s-vV,0IT?tiowul>juek@S͔) IW:iOJIj/ LhZ;{!mIҽ Z*47I3#@\z`U$S+ CNiOf]h{pIޥS%+woEUȾ а{"|B6EE'L/̽lB4w[M?{Cǽ%T1~MMXV|!띹-D6o&|4{"gTUγTf/wU|s.R*2t:TmkΦDEEE3K j閳=ŎUhw_6lK\Ǝmu?sj$:Sp%낮#rPԝ z>'o*@~[89=}6h.yux6JJ\EsQY V>ajL*(#-$ &韓VEdTe9bWlE Ig柏Ƅ'_-҄@(ϺBu-A͔>ZϜ.4]9z8s"wH^P4}2O[&<zG'SWi?;1nEu e \Ӥׯ(k>4N|7/-8yo, i=xӄNL[9w7R d۸`86OfsgK/.6"% bwډ% d]ߺR/.u8}Ž Ң1kB_,'Bƛ1}[;Kғ\QolAw Ś0s/lօ&룃-?;^8J*ZFQjiB15dOھ [.{7d-yzDnn]^7LB0f̘^6әTsoNL<,=B|YU;̾Ų ̇+|{wf] '/m ".t>S;3݂]-pԢɓ'O+y}kjH I cN՟IOS4TX ! jTspM\ȩ)j+2K9Ev_хQA Q׀Ѳ/VaSسN^[ކBO `/QQ "yjَ<խ{ 䮾֘EBO6B== EOVh4jZV4P(=<0~s!ZF[=9ǿ{ZC @Z~ߑ_Y51mk^'j=v+SMG}?ŵ+̂v8nKH=^L$ryMޞ%%5mn i0/04gzeoyjS+ƃ&﫯W6S$/r7diȯJjr]aV.ک/m0s\?_X^^Lpp ^|Vs_w]mZ'u&u;ٚ.sݍt=[c E! !OS1_l+@fE{r+)hMH Ήk`>*dQE>Cr8Ssu?;b/ƯՇT3A>ښf.ohݼuwa)*7$P_hIV\7_z?3@i\;Z.OMͳޖ\B.+}sA~j%n; //JVBCkih>h3;V-_mIYv<6'$zp8l oYV˷[@qR#It \1a8(M s*I^3 (BaEW3 %KfN( *&Z$V9=Y%tzеǏn޲5wyyj>9ڽ±{k&yϚbmr/T~r68,߽}CmI`3T,;z`b޽Fc#lCwԽR?mkwgʞ %~ݫt' _tIwgQ}m>Y3aE{3kG̬| >\5h~9jurJGޱg֢uіnkE!G1Aˎ|{ U[u zZ.B^<^9uOu xPJ_m0-{\ueI+dMfvjo>nD3)4Vpqq3YP>+ ֯gw u@% (]qHWMvK\ȹn˻vyp\ymo`U>n}hB2=Be4|6{gØxf f@~<]xRַm;oc3[1`rݫrfPynYg@Pmr``y MV)oyx^u !s)Sh%}V ,EdR:Rq܁qt&O!П [FFxl# PP7ЧѾ,|n^TCNdfxAxO}jl [Iw躞mx#]f`y*i{ۺWVn U;3~᷅ n>Vw,wMx' ed`$0f:,SyAyQyrM| x3ij5d6XafEP MkEZ~(Apt Ƶ& /@$wG]u5MMmTN}@PmZi .wR$t2\?HZ^ro-&M&\|||<<&=B(B8H溅z%4PO*ܺF`ˏweP$Š;pvw\}rWC"N95Gs办#Iv]I͕%%{+7FB(BO [Qrg}O5 uxfXk#FY!-pM>qbp0/?|Iא4<ܽӢ'FB(BOIAF{eGiMSm]h#匲 {@X|ƻZ6kE (֔=_m&??J!8 ?jK+Z;>X1FV6؈Y/la6^rs ސ'/gYm_]m0SO%=YmiZY ktG[gUNZlPw{]ކB(BOIu1h+o{MEY? p'rӥцk $yB]èը+![>5 sOXڛ00ݽˢ.:5烮GDܜ\8䈃 m%{JG0Sm8 9_^P{0~[۔Gt#П0mۊ $1relRtY4Q 8~T6vy+}Ho_?'VE? mSRPfͳkBC1 IDAT<Ǜ$f ׻h` TnL ,z܄8-p8GV[<[%3?"aKh8V6Kn>D\[ "&撃%0&-w$}tmބ LHʍ;)ԏ,ɧ@9X[yg 糷pJںEB oz"ZFQjiBtY~V{Pа6'Kնn$O:xZ6/,:oQ=˯v'=Y]ҔSP#c53ǙU*8ϨK?Y9{泣?^0ޙq1%YboTLyOC6a5!BeTk޳52QSY?{.^~Ȏ3*,h-ʗ:-h).8\ ,ڙtCܦx/k&7'c}Q3){HR#9-س!BFTgx`֖+3fںL5Yť² vq5IL&=f>7AU_֤eSY =\1>y_3d"U*[JfQBC^Wvvː\ .T@!#:5oL&&< &&,v욲֕[`mT໯=>W_2!BJepM:ik @m,c Pq*>RsҸ^™EGYeĚI!Ba/),ΩQ[45;Z*s:#|L<iEEeT9.,yYLv3k&QBamwW^pc2@f m{u0Dz4-BgM3CL7ػʻ/h96LHE`2!LLlk8[ZIpuuބ9tk`#w$ŕ0AiGOC ?âCuu]$%N,J!r%woci]!Ξ={̙nIDQ>aDooo?5)B!dd"66c`meїH6#!B AqQi%f6TEpWdx'j"(v6 Tݿ4]*6(-:O֘UVlmv4~oeڮ ;sU5gb[XŠBcynQiK6M). 8mfU{8 'BȕCC> E"8PCތڐ s49Q`VזUV+q79D]4-[77;_FQxk LV; lEJNz`[CJbi R@NaN)3T8x !GHR1>gl-QO/%BȕC#BYQgGerB@ͣcz" 5^+퉑I RH?"GZ+ H$W NYj(1L_, ڐ.YܡBs e6hJA`+ tӁB(B8SOMX `J*Q%BȕM6Y h/mL~Ol!)kZ29hM΁~*6[Ɋ~Gν4e_ڐxվj6cR$ ])K\ƅGSes.v@5i0=!BT>E@l\vNjMfvg@ 4W^d*9 =5ސ QF Y)AQ #pу۬ڐ.97dA%j@3 b]P\S' "0(!BE9o.ú,K6-6XH[TSJTu:.U3ljQU_k KM8 ]H:5ɜ2 >jhZ(F(ʱz0sCi.R ٪:NLkHίYi}yGG9Jk&)! iZ+lQ(!Bl(B! B!(!B!F !BQB!B(B!+ETfwsTJ0?6g7sc='w״$ŢoJ!\bbbN:KU1:u*&BńQ=mh{| f a܅׽c߄ 7OUqXBIBRǷS㿚07;Yڷw\WF|C1떘|NtG%\_@3.zK<ǫ==1ѝAkv&c/ Fׇ{_sz; 5c @mٚo>r39T}DMP͘pnXt복ݒ>֮IYwnJ Nx:D *nL}keԄ!a'11'R$N:ё.~oS;sr[[I;,Λ}`˯ȿ_?)sZV\U:pr-K2'RW'Ly`Ff?gih|Iv=|7sۺЮvk f|˹+Ypオm϶,VB!da&999tvvR O111 |a븯c$*!vO~49I0qTO)x 9c '9ҍkMK>,1fo]|F c}''fM0. S&Ɖ p_ `~a`B{{{fQfM۲о't}x86Y7͏Ft"-soGUA!ϣJRTRU\1.0zu}E@_gf)}=8ٱ1ykc)$FMzCO9VSw7Ql; c}ߞbEfctSftۿc@TE 8'f矷>ŔG !U:(".5Sk{w&{'qC z_ؾZ~&vAu`S vw|͸[ƮF6a,89N~+޳[ߩMY?K>/)B!\}F;Z1[gN:U=Us >Po{2U? y2б;^Uw[yx?[Dć#]lM> Hg_[5G4|py_=:=V>6`Y 祋uuʽg<:z\٨}.B! ]}1i}6u}"Ӧ&T>&Lߕ*Cl,|ֱLssۿv Hsd}޵zs_'L/b7׽_ܝ'v9rv퟼ sk[|}M>M 7k WMvJv}8:ƥ.~]TyB9 ~9% uy=LRP%B!sB!(!B 20=!B=B!(!B0J!BQB.AnHKKK0y%ii6HCBbHKKKKK5}] 8 {rWmw@vϬKKKK(xZcz.l VҌ.(!#T_ZH[reFQZU"+ @VTCn뚊f:=吼5 _D. \g.++3i9C B~s#OFɠKKKKK4Z=Ab)#-Puiii:!]%S%r \as0,-\J/6m.^ ,:0XVhHS#vXV"M xWҗ[*[]TAa2Wl,_eg*VJg&rmw,.[6OZʏTSY@R^:V6Tl. eUIU*j҄jLK~8}L֪Zܠ*y*K uE:S*;XވvEڲڈw#?ƫ:uE8Ԓ)ryrYr[Z é+7l-j-Gm~~ΐPu| xHi4مyIs\/[VS9sռwsEiqyiF' $Tr`ԔDVnPΤd1$JR5G=E6ipRXwr)Y[L KC>pmQQNk\[fQSRT N<'V׵K.C[RZ@Ìd2tB}AZHMU@\ -\^YgY$kJ+Mt98p "$B@д{ioI5rL-1hyTטWk]WB g]]ɪ,W@g+/{k/]W|d 9\J.s BOye QK!g\3Mjj4kueǞkәssTrjy*>en[F`ULcɊT@ krzorņ{& ޶@v&'a7b7]Xi$b𩲲V̕$H 4BY]nrz>$B1N-(!PXgH@BͮF Z(G4;؊ h<\X6Pv/0lj{ kð@W<l.wMAeA1|V*GCkHK˶8xpr%Pœp;Zh :Ner3d~Ug倷qXWYa4 ~a`4d%RKc/5jץLapIԙReˋ73%Z[]5|iX^+ ?Rќ%R˄Ӗ+˛xk*֬X)敭3iX*CN*֚V緌&}Ӻ嶦 kbB5&FW ]cI)rReAmR$M1Z 2ݷfSeYJxrYh4Y$Ver;")\ټRlnaeYv"tի7]¦UK?R\-\;tӤ<zש1|B!#zΜ9T 8{3g%IEȑ#T-B7 B! B!(!B!F !BQB!B(B! B!P%B!F !B0J!B(B!rޢ B@! [_B!ˆzFv5P%B!#B! B!(!B!F !BQB!B2|5mq S|g^7:|=IcTS_ThB!|\XoU }7lsXO_lmʠ?׏{[6o)gHtghǁmxu1S-]x}p ^w(ɭ[(׏G! ݡPj P(0z.`ʼ'1n{gvqÞgN?[oK.Y0;nN}+yCMoWE3$lxi{;7}Aެo;^8mk~!&eYm?)#9k fA!dPG=ztJJJll,Z Iґ#GBХ ͅ ]@4 ڐ4y 7aB? ~5fR *$M=$L{t$]{ځ˾ (OM}3`~wg$ %ڿ~Oms`}(!a+ 0J_J!FcX@ho;>3kΡٺ}kBPa$pѽEB{T)\|,D'L_?lyE?K޷S%2djJ"^ ī{>8ݻo=[g:>ضa=aOxG\on_Yy⁔Abplk څ.Svp6w{(/}˜1 7A!.gE [^3ʘΏ#elמNw @Ok1}iBR Ny 8uStQJߵeƝѳoo &?mѶ;Yﺈ;R/ߘ wٳgΜ鉡$xȑ/A$FB/mUh4>t>sGxwߖny˾NDOO@IB!da4aqipk{6a?h>eB!Fc&ܼeyxSJ0N||Wݱf$P̼h/{c]1)W,8 &0 <.SxeVlں6~kX?MԲt8yZ- >sf[UݮitlvwSHdcFⰭk5tY:R5ɤ-*m1x-ЫI>F;77qʋ'aׂ՗Ў~:Ksx^3(!_Т'dFOI,h^P`rZxf~d9֤>詪F "d˯>ɳ5Im)/Ό?^p5q-&;#~Gz0qq˿7у)֘='v&mR1s{R o1ZḏLr{K6#GNJѲ&'7籖3[MzqA܀Oɿ.~b4-5,I0*,!Uu3#$ OgiOX§>GOmI79S{s^SlzҰBxY=9+vIsǦ߾9H۔m0scݟʡy;/Mػۘ~w& ,u3O yc-K ӎ'n%ϰqܟDaQNhtѨ% xXm6WxHPLel%չ},x`PR *b/@bUY%ef]" lEfG9IZkѰRw@!ȷ2L[Qԕ9z:i\k.гUK/r]$[mI`H=怔Zd펦怠-YgK6yj9#5FcX2fJ' JڵV Jސ$B[adKS`<]\.%̔1"hj%Ov`k܇>He?l:!@!I}db/V=u~u򿅎t Z&=ݝ^vosoZ{!)gso ?QN}HSX(̞`iʋ˯/buɳǏJss(&톄q.ShC 3'bs?{O;n㱤VbK?z.S=;"3>teR~A bβc ?=f5y&iǓ|w܎+v<+s6k!zf e/:nW`i0kյ' (^ݨֺ\J})I¯ty<%5/vW솴O.P%䂜iw=j\7(LU}D<3:a_Ԅ@ש7ZOK j DAe@;?){g@_2IHX:tJQi'@5Q/~]卫kWEY8/Wf XBlݶmOp7_}g?qx @1kRk(R3^ڗE,>ؤxpn̺3t&)_<$ans)Wobq[v8|{R9lS#hԥ ]N]:GAbќ@7 fA6=au*4:{6d$`j `SLى5:K}>!Ő|-Bs_HhyOCpw;^d}֨K Y֐\%kj`*N "Bl9^%46BS]`аs0IzXF.9`UU( )4EN[d3.z*V> mIy+&"4Ѩ(]7Mqs W18;=_ۺEȮ{;F㽹3e£Foݧ?V֓%e&ء'%54n40I NIޕ0o9Ž_?G@H2ͬ$@6Hm%IHLRwF %l3Om0:D6wb9LJ/mRdl}oۗ".D.0BIXZ#}2Hm7ݳ0!o.~M^mtZoӜƐujz z+J"DZ%oXn[i7(2ķJ)$^oڌ\!g<iX3=mP+{rco&b$Hl3^`6o) #c$^90Yzsi]i,zg') yJ^o;LϪ evKіT=g&QB@'wo=;~40o;EųhwHA<A"ƍÍ{~s~r?+aZ= DG%8ntghܝ4'<ܮYOBYOC_;^|6Z_^1R/EzOf?Kz󱋙*1Fj \~Ng`3Y5O]i HB[_ ?3\ }J|zRp=˒6m,m M_FYJh\-_vsugie6{4˘~gfújÒz ` `rzEuurO S-g@_x$O$^b qxrQ-c&g9~ATVBi1m-UjGk~I;@!dΜ9#b;p?H;?){ڎ$O>8żlRu)i Nn']w4pV1J^zuϦ ] K?OeI(|i[eI,?-ȧ*8@ xy1 "M35ҫ{YWpӗc{^,L&;oœ/tM;">9h{fJ#;[_>ӛ+4#5+}u4'I,*f9免4-Q*+ntP6'=S/KϽ$MLMцB6-/2/@Vp, ' !,nkk4\鸀{}c Sg" +U0+lVK~}"Sӹ;V~WS9_"joXK 78V+Y^ӘmpaOM7>iHflvg =W RВ9CHAQD]dY29JA佡ʶڳN=JuT򈋻#>f ]iT??&a鮉m_fOu``?T'ktڴu % ;Nl:v XutX#p[/ݽGԟ<38% @{@2?|Ϟ2KPL~x=g1 I3{g.7dfMwN~ʚ{'lO?qS!yYy'?#b+s@L:&%y7ٕyOcU>kԧV\XAYiˆt+RYQH0/U)TZcIk*-jS>Ԕ83SPLyu+tE:V2AH|ڴҜV/Ǫr+7R[SY ),PTo1lONEakURHTfYW@SlȵeEvܨ44dVb()S3ʪxS|ۼ$ð*}Ifd~zFɅQmzB+qww~Mc߭/ߛG\h4<ά9t&M/6Z"~ےb֜2,m2J!Ң/Ĝa? Sh)\޾.W%^Q74G!d wyڦ?集7rVM{NdHJ}I8w^,e4cazBD4LO!d|0B!ˆ(!B0J!B(B!BaB!WJuԩ( B9r)+;F0B>Bj]s5GTÊK|$zn=j%S*jB>IKUY(B!#=ҜQB!rP%B!F !BQB!B(B! B!|ed;z(U!2l%''S%&N!ጆ !BQB!BaB! B!(!B!_ryt>]ɛ̱y,o_3o{+[S9{QB!FWj.>J3Mpx/bm'nη_xG>CjBёA|縼p,t%,/^^Z͉SYgZ=t +N=`knx®7oNAmF]=_n|+zc١ ȧ'0殊I͈ U|Mnyr}aY&^ZftL)I@-̥!T7Uycv$g- >υ-O2% + 1μf8گ }ȑTW\>y䨨( WQv'!mZXJ†{P~ei!yfF}; 7^ji +~l}=8ռV<W4.~%8A롇J-wO\S9d۔uwsjv%X)$k@q 8wNL-`;o۵ wG{oWm=3CBXiK)A#G9rDRQqy(ئ$=^&dKo9`Z,ZNk%-[:vwİػ/L$81o \6i4ppܭ:yt W8{Xѷ<(c~o1n \ݖ<$c0-4:}-1ka8>"BFa 0RR˶3*wz.䉚t6IvO5%BW-+S8HOQYg{`GaG&[+t|YQs)M֣CgbigB$zFG[c'ĭlOjdh(EĞĉH]F'plqݿB_1+n>xW.$B>#{m"?ӆglrmq_t|?uOGW|Q֭Is֗ζIM?8dzƳS'Q"B磞sFk7:T]%+0N{yWXZr ,Puukryɇ̤p@duZp#_yKݫZo/9˰d8E !9Fzr]M/0}F !ү&y\b94J!B. B!(!_?.̏]wo/3T}22f_21P<Nu͠(3-MW4Z3u&H{%xiia`+7ҌlOb9t&{Cx8^lFΧ ȕj͆O_%g#M Y2l0s+۴7zl6ˢ/]zvW~}\cr6l1kGWD^-aj|B>Oe9عkuV紙Yί5ig zj$k0JijZϸoN랞3MSX:s:{b`f347 +(Urf%Ā\ ])#-*oH8uVIYA"g*mV`aPUhYT$r2޳tz^.1lul7,M:dFi %OtRK'i,\ .za`g2JʭhI`ITXP[]6- UsZ͗"\V-ۻR˪e j_M|U+GʌU )mYU͎A ]mxfsTfV"Xܧς%uQ͠+/8$V@27'[#Yk-6`[YH"dUՀ x+2gQOWjsߠ} %*mKC:5"ow45m:k&h]b赼9Uk@e5q0Z1FgTz=`W׮5bUj$ %sw%X 4Cl]~⊆C̔1") ӓohnk?wc!?Nξ1ػ=-i ΄^RhSh+*mN/sz<%S\ CWjQgY\X]<4K\UDf3RTVĨKj䱘ȯx\glv;/4XPTаgKi^ #Pe*mԖ9=.2(5 [//v8kK=k~h-vk*(;˳o1iWnj-)Gy AW6Z6,.r0ǵ./H 57 f! ֭.eUl=Ƨ.vڴMUFQWYȯx<[Vp*_nvr+x\%*w]Hb Ziv+Jj].φ|Tbh 9Uet*Vl,/k=.k\C@2{uEP PCH_V]]_]("4i\864yZ{V^tnZk].W>Psx}]CSb˓+A[__x mIy+&"4(wq M&yazgݑQɘT/k SE){~ ٺ*qj|AķJJ HoRB%fhKK&ce!0/)dRCD+DX ӿB "}0r|3ˇ>b˩9/%$fy5$)¤Ԇ]4PU}0]sTH[QZ * }5wtm+EV)Ki3r |bIO'g C4.{ DNp K-'Kzd)"zgu?6RSa7/Y-VbY̎bc70ڒd0JȗY{ }?c+08oƃ=c)ޝ3iȰfpL1髜 g5g/qOQO}DpZchv4eˇZ$ʙ$ 0rnJY0OV}€89FW,RQ{ލYn.p, f\Ap3TlXW"XmXR3R , !AR\.SU0;Rbas*k/q@c@x2,A?"(LK&YxP}.Xh!٦r W4*CYmh-֯0ps?JPMN&8~ 0vL12+dSfݫEU}UaduEDĠ A$*:uO9:mBy `h{ ԁAJ- ʌPQ 5\8Vrz" ꪼ% x{`Ǡz#fOòo 5GW5kT  4:67K|:($FR@U@p~/ HO;J :Zbv3g+E6O@!}['tgw{WU{ ʹ, i=>6}C Q{dnL (",2Z8J%\ĪvWDA=u_$Gj2Z=fbšUw%{|f*>dS欺K(Ec*/)5dVZ%?eEEȕjC9hMYURPTTHh{ rǔeWī5FB88$F'2hү $;uFg9*(y.ms Z9 C㌄B9W5+J/q"g)+M84CI[J}Q0gN9%gJ#}Сazr;7_' '7h\g9,dߜh[^B!"~BWWWwwwOOOOOυ/nvPi]Æ 6lã"6lXOOO8E(!rgQT FM>|twwSB!wnH$_LF hO4GB!wz%f$J2䉢0 u]a4AEUIna*J mȑbbb699rQJr|]D!dRT^jcPqY\.;vuQ B&L@@D%B! ܈BQ(!B0J!B(B!BaB!P%B!䆡釺c<Z}uXǏ&BS%$)O$)yX?n:aOClcƌK!1N"0Quځs6ԍ}~kmyt;e¥|oP?;?Q]{Ģ{qk̓E[v> @8e2:hmm|(MTg-]8%^JN! :wWNKH#Y,VW#˞͟sxeqOKl;>?Qzqm}Sbq/{oan1_ctL{ckcҲg'vc7jOz'B;Yqپ-lZwGO1!Okn}\.'&痙 *BȪ2Jw6/)\UbqIͫJjYZyA^\U^ӕ8Pjs+仼L u+2Uh4/* A+MFk6sEƐ^t5Anƴ(6`4SqU\y܍sSX(1ne)}M7o<˅>R2J n %CQŴ {zvoNU6zořok?qYE ]Ok*}WsH_#q]Τ-p;Lspd&hz? V|s[O:9^}散l=׿~7iWtV=^P~C]^8@1kIҩoDnj%S/䟽qgCu~ OlUc{Nw/|_YZh-?c\F<{|˟wTЇ{OO_0e[ͮ]?,i}ׯ60Sv$ғܿ9i\J3NkHn=&!YZ[7XN8m6X] ڬmxdC^ܹ)l 4ʝNGuf_d XeQU;έy|YAh ǩXUAbOYaWt8HT[uUbj,XM@Vzjrݸ֭-u8f4EmX->KV֣-v8zoh)*˓*lF}tMj}x-i,R%KgSNuQQSL;wyT3Q ONZ>*wt˧1Duo5@ْSWsc,;iw3qwNOb换\'qߊ9}g~Bh޽uSs[0;_V9^,1kA8QGr8Y?|wœ@8X3x (_8^<$b{$29nd7yC^{bj4w1q\mx4O\|Y,h[ )٠zҰns8N!$dT;6f&H|sPDS i 27jgqtڠ.(J@%hsu Aᭉ\-UrFBʼn^^Xz-ѫx'7jR @BjVp;&IS+`Pwy5*ouu?2%tڜiF]µn U(0ڢ< 4LOu"[ v weبޢN =Wg.'=?^$vanQd@ 4wɜ}zXt|/N|1W,w!ʰN, `Ht̡ /6]+ݑEU$y~a/`SVT-E1b/Z{>6C\Αݐ|C#gPt T>#_o{^}ÌȷEPPd/Y,$Ço`+")8i7P Y pl wӀhtj*e#7} J sRXE{0rFlEĠ+˒WZ9ʁs^ ŧ!k3yrFx>ȃ_=|wQ a9|F9L1{JTruvSѬi\sdUR Ԛo` ӓ!)~,|.iWP(3a8;z.'665Nx`(gμ#.\n`l4~4~D0|fOu[wNlhl-Rc#{z( uRvq^k6iCv7 _6(!F>Q[&͚!ᶷO]H[Wa2V`'~ak M?qsى {{k4x&CYIwj yYs#]@}5c mlua8YO/ոsSݖʢN8z?vmi?;%:Oqob=y$m B!d?~H6cm0nNJ)Or4ĘΤGێ񝑰huV$iM': 4dq ACV)|C3=m Iq&td_s{<hK'nc]›Dd'B`v-a4&qZJn=qSh\(N 1Lb/qyˍ&mk;Ҩ?=iXX3\Gj:5 BM(gS8ܑm?E?%j7^b#?R.@{+$r 匔= 65'N_'r8㣝;4޽ƨ)t4?7yt|JBa*SSqyqGo'dxI,x𔥋%߾u9ʾcKΚ5Tە3CB!a el?׎܁ S.|TzzzzzDQ%55jBsHb)tCe k}'P%Bah[m3$nKӀPD=B(!ٳ]]]TC\.;vlTT5 J$B zPm n[^O (Tk.d8m B!PgϞ;v,%ѡaRy)(!BnFjJbbb B!䖡0J!B(B!;W~޽Te+5kU!BzF !BQB!rj4JnW5M%ߗm>'YHBħ'^ӿ}=DԄe^dqs/~պ"C weJ}-Gy =v'LaoPVS,v,m!w)laN.rcUiEfKM_dɳsW% 9AmM7>P5la^s |+AD:8OTq鉗}Cc&ZmÖOW~.w fvE]_J|NdIssd)Rr%*-a|Zo6[WXh,_n᲋eZnǃ 4Ub1QBnE_u~Nxyj6Ÿcp7I9*WǞpUSػʸ7]>|2ce7?OO9IĹqCԛujoΤ}vb]m%ʘphCeS ܉b }O:?Ld/sԷgktw<w8#+}Gbb)^b,wK,z -=V.9ԨK}md*mVհf<|~Jh,{<OPt-2VSrXlYI1.|-;rmɔd8r myMI^{ ȴL ȚZ,jUQs]èJhty$Fc;J]Aiq>kn}\.'&痙 *BȪZ96m^Sfg'j ԛWxE<_]>]=?ڜ ./wX@[Q\ByQ!oH :M^Qo5Zl M^^[8/2ڠ rl4%>Gq}1ͬ*]NTnܘ >G(t+MuoZ,7xK7y_.,K n|J׌;mA!_|S=[N ^8O.Cp]/?~}?}hg/ m<L@"AW_YeZ2W/{Вs0pw̬;^2m n -U[4ϰ^1nքM^DI̷q!?)eDTaWhr@KRKNnJr+뽙<2w @X;[Yv$PrT~[7l.h/ؔILrQͤ Ni/R1mKkח䗇r,ӹ579y`'Wa٫Zw&Я$oeAG[jw:`yhsulG~erDog֐ܴ>9c5 \䏚orp:n"r'UXmfmC#&NΕ\MIehVt:T5"*cpn *8A/DZtN J1p86=%]wyӋ-bԔYjVT5>KV{՞q[[p:hv PhV[2|r7pG[\p8*ޒX/Oʫi7}NwZ7bv2/QBC~Bz &^6]':G7j灿~ |#o7/3`8 㳮o>?p~2 *:ػy/ ;:^C@` ?u0G@ĨйΫR-'MzN- ',vab΢򠾴bE kL^p\)u\ @S^Ad4~ۗEgg.F:C9mkFþ (_h9b$.ͺdЧ8uii.w6+r g'/lJjyV{\wW5%\oS6mQ)D06dCZV,<ؔj o|!-`UyZ]4@.[t? j\6WMޚ&ZJ.YQ8 م}@= ߰Ia0h"{@Vp;&IS+`Pwy5\ѫkݏL6'p|QwWEݨs='CՕD18CɪO ;v DƠQ#"Sv ;c2O  dQåQ>יeifl'  EIN@H.MpRPs{g8m;ŅkFygAIXS bL]Iof AAՔ>lDYU_:B%ekKJyK$@Pd5}-2>:,_ PC~!h1*Op{C-  29Ued&P41{|r79[ˉU}#zfQn蛾lsI'0lSA6mF3`0\Hga"mP+#7YKEV.0>(@02_\YFƈAQ~Y&gŠػ]2sdO^Ի'!Q\)+*LUUAyj8W{PjAan+0"Kd0J׎g?=w'%a8w)!HF8 [*7п+^lQQ>_--.e~KZOIם+W-*Y_V2%Ǥ]r,s[R%sQMo<x¡N&m p1+ި$!9wQxLQ;l((0GfS"UlUUQdQY pl wӀhtj*e#7} J sRXE{0rFlEĠ+˒WZ?فU0\nlWe N~&};(EF˰Kۺr+dr+.y*G3.X k7ڝqG#2Z&T|(hސ}o֟pԸ+ݕ~oԧN>}*~ecy>XYg$iu_y?0H$##%>[UahJw&eFQfmGX~]i p LBbCE*]resx濐:Ԕp(BPT[> 5wldVp%xwfk{u.O<kjf#賓+l-G a( ~Keߕ< =MSK_d*% v55f'\rFמl&+\{f Vrs'Tmm` Bn+ PorZSgh* ഺTQ/AjDdc1WL D g5p k!XUf|c(g ]=l%D)M&{5^oΛ]|/L_mG$>hǺZO`ظ>xC1k&Y$_,!GWþ16~ߒ;kصTih ʊJ+rm)/erSf˕ɆeMU59i\mP3cca.2Oƾ$f]՘QAYtdC* j苜J}fU*TmAbMYw\tB/X_LyJrSUh4_*nXeL_XUNV\bΙ:#oɐy&c;[fUVkUUF(^/<:kJ5,N2 EM=K1*eUTbi%KƸ(-aURa P'&-twwwuuuuu(BKKKjj* !ϖScȼIc^!Ǚ7<,#H=nV!xLYFy=_u~ѼP(.Yd޽B4:+bݠ T8VJߝW9*ms kT'dE@nHhOOO8mmmT-jB9fEY 8rk H'rEJ}Q0gN9%gJ肎f?Z 'jjBb0=A:ˡ !B-CaB!P%B!F !B0J!CE!=**(!B#GSU s 7r)Dnz!!^/]]]TC\.;vtһ !rcEEEM0 !BQB!BaB! B!dJ70u[^c&N5oqүcQ?(Lxd/hB! g4ayfЌIOu3TB!䫻G;IFO3^)ysdO4Oh8> t%#'[ mt`T?0ś?1g=gÐ8:C?۷cONu)y`e*h‡xC=}cYswo}{ѣ5`CGlƼzd?0=CۛB!vIGM:5CodymzMb8y1'xÈ֤A{ˡ?~j♺7߫&<xO'U30slǡS8n}c;T{`?YFNA԰ߒ)'H$tHıDկ[g{4F>?NہBƏc( q@G#v$0r~Q9ó|Re{?EUms_@t_B!d00q-hpvھ/m!SG=t]9ځ3'H=ӷ_4rB BddEBmaDG,J9 ~具ZG}7T1siVyvvW ?hRڷkvzn/0r}nv YKڶ7+>rD !rm0?Ϛ>[?9M 3u޼;j S{}' ;jY7d}3f~'? عo_$#U3rSHyD5%cBřc9? }/:HGڼ7%jzB!\ag?YNycƥҵp84j5 !rׁB! B!s[ ӓ !B=B!(!B0J!BQB.HvAZzNzOK77 _4Ő埸B9^KδiBu]dbfWԙ,GqnzZZZZZ,>!bץ1 ~@Yl5u>ѠKӥg昬47`V0zcrOC{ % &LNӥm8=RV<'3]Y`89:vcNod旹yj.B(r#ڐ*k\L[rt:<εFl Vmgh+ww |J]oL%4\H^d!4h,VÚ'0c[Sp:쥩 [D|!p UFH'*1KW$1E6 m tl= }k|šb^]TBpnut:T^{=DBQBnSqC(+3Ye@zR~ӕ:źZfMAA9{o{l%cLkHMyJI5h9 E+TE:Ky</ {쑫dLmNIDAT3 -78˻m!!lJOB!(!7lxǥ[hL9sryFؓ2!_!d`0 9k$ek]FAh!7Y~O(8QFUU(y1Xx{z+$0`l?Y`5K34o ,n%r L@?౪|w/Q޹ C~^X@!psεf՚d积Zmn`5RX*s` Kw3u Dѽ(]ZmNwᔓsb՞4^^XeZ)-xhf- E!&Q2 FGrKH/<6!)Y025]`.bAS,{l[<ɑ:[2`P]}y8U*7NKxR`w:N^eVV;DYBPi .K R6cKoB2&ZE"xYmE=Uv@{sTt+AE¶?AHW*RAkB&>a&3&̷#-VB4@{cyG+h4@ @D@D@D@DD?km,`in677 E7zCcmoijVk4!#am=Τo? 57Yf2,/ !l1 ,3YAefH$ ECnn."2#l647F[/d"2$0[+BmBdX/нX,$XH(kK0rlȑ2DnԨQ! Z633͛vvvg 3g=ztڴiW^---om]q7bQJDlcUo $2irGn_b6<~ou޾鄎lt,?L`j!8_'NS73KH.Gs<'W& " [:6DB_Y~>3‶+æFDM}%SWX ݞ_R5{4n؍p'"'$]n- Q>!=IMSvCv"?xVO#Ţh vBH(Ō(A/gB! mb 7,7.o&!Kmho>"/JJJΝ;7rȖV;qD.33ֈ|)^?o<K.ݻ7//oҤI"?s|MNNN~~ZGh D䜜# zlG풖! o,[|' kD濻@, B|uC6b):'"q|'Քix_i ⥘g^S-&::}obM ΍vcM SoLY9,~ s5YI :㓽 ĩNLaw,T>[(Ve9%./HW"cbeR|M"u(uK7n&htL&L:Yh23gB[,uzyy9 8P"ѨQJJJJKK"20>>>_tvvnhhPTJD*ZW 0@,_tmrs=gX p;wY#"2H Ph&{P lImZ 6ַ\jR):κ@!svZF9BG>Ԩeok<ee5\_˽ChU6n?IW3rw` Ǔ!Wdl+1r߰,wϟ_<[YRTQSyΎ 㳷P W%*HSy*!]_NDdM\]eQ!JxU^ZƢbXߩQQaT2?N)9HN|gkf{ik)0Z/K RW 6&Y8]-vmI`oq]UVlqfYaVko1vvMkfC]THaHu[nQ^,+H/ZQ`hz~GVKD|b1  fZah4?*vK# =|mpwBoM7c_8AzSFޛevoIC#h•ȖWWDjnaP%)J\#a81d\Ʀ+1q+7gȹi+{kr R97?1{G;*W7%OL9mO#ݛU+ǭ8.7WS EQ:/zq;{VmHf SW1f S2 l1! ߌ%rRgϟ6g=Uo.S7Ðr>ENW2\I̸e _Jؔ/^4&se ";1N+'r"ze@: ˂V>Л,7&"Rj 7J7.ag%8H_Ψv\h=gO"^[RoJ{iD;eĕ̏K)=44vIۂ3jNNI]]ts'K|1iU;z\g&"X$twj[`ʔ) L6zʻos4E5a"=7ě%"{o O3iݣ&"@%C u~[Kp;!ʫQ7vkO~nbl ^wZ9Ze}*r4|UA"hVpN=Oybﰖ&qwgZyrv)]ż#'{UcZ5+VKyfE@6˰C(Lfzi߄aMm_~cƌ1nb?h6ǷB$| e |!ϗjWz+\ȥzwX0gJ'L^[U=.sgLDwʩFFru-˲b""^1ܹ=yuCޜK;1X1c8h(cTq< As#G{ ^{*^mc/_4бֱ|g3WTMނiNo;_j^غ\2N9wö +s$Nnԛ5T9xww[w\lX<~[˿Z:/FFAWCD/-X:L~+N9Pe =U:/R6蘃;wiBFV`Fګ[;{dg GG'֬˰ pGĒ"hڕe6'ֿEVFߦ kɥwri-EO'=֝XK)ٱMs74DR2XOzP˽NOիh5|<֤om3={>chֱ E1hӖH[WVF\ܞ o~vޓIDNȥ7YL-&b15ܰm{NI"яޝu۩U )E!<\ZmS,˫[8@Q#㥨geb莖w;ZgsYUmۋFC-m|E)&d,e2 *Db.󟙘7,5DNQ{JFL*c w8'r|Mۑp;*m(|8P kWQȆ Qil*XM!ER %!=}ޥsEjVv[FcwW×ݿ߹GS[T&!Vr<2KDmiema 6SQa RMشCYO.\ KN 3<)!WO)ʮr]P}"q]z ](?@ 5]gXl k @(0B;{;H(6.\d2"RT qccbnپZׯ_'^zYײ1{`/X&KM?+HlˠUk-k9v_BX:>[Ǻ U% !XϬc}'U䖨}wxEDDDJe܃w XU]F QMnU;НQ vWgVDīaAJ4'RҪ.&m9+f|qEKvxBϊiES!"&ʓQ_&"j= VWDUdg݃_VD Q]k©˲Vnf߳W VdWpDD\\}Rrz21o࡭ךmQ=k)]rQeiV^3TrД8y{_ݐ{AaUi)'4ݱcSU<1ʠam>4E}vBCR[WhO=X+v8"y)+WF,Ԥr"f}&.9gŝzXjd- [f}3Y, ̷jt;HmMf=o2,ҫ˨///@puV+hذa_~[ohڞ={:99IIɆ ̙CD&a32kWJ3X(@ $E_È {8JjLyw.H7xvq=$IأXHȲ k+cЗu/ѳMBbw䲨Ycd !X))Kǧ-@xDL򐄄Ĵ9"F;59ʟ%BKgV5|r"yXR:1nx$-f4y)i5 Qh6-tr!X,Fb6}B!ћDlLg"'2l6z2̼b1[*oYc#f "]uz;H.k4wwٳgbRNٳ۷ADD O7nlllH$={lhh7nܫZWWnݺAQSSSjjhjR> 2م$4-nYIDB{{?,R-ED>//~E-ר !"lR_̜̚o!"[1"zMY8&yo7j=h>K˿2Q, ½^s'wUd\__ퟸ;?b? IH x۷c/Z0b!'Qg]n#1|s"t~8vdzWfDnD)N塡zޣG?/_&z{{˲#G|[U$ u1*TtA8 tA$(u[2h/2({u:{xثϞfۿ2,+!P.rnԌ%ެ$^| ݆|O|23N #9D_-U_= :U-B b6}:d$^O&x;GV(fh5z+}{bލg"{zzΚ5sIJć.KXG@{=x*&t_#EJno(l j2l"Y@ҵܩfi+ PEAa0B6KR.uۣhѫW/O//yKѵeDF[,DVB_CD&"˫տ'4IZtAf룡yb0CKw/lmx~G޹o-X) {@ k!+pp,I$DH`gGb׮Eo8:#2թբ;BV* z8 98B VDU ~.ѵk w#B\h""`>ckYKn6fd2 nzD$#D"AD'M&dKDň&b3L3"KHoH6DpDC+d6-|D(Ób BP(Wxھ~0EDF " " "hIx1nY n_xr@D@D@D@D@D@D@D@D@D@Dj~7&Zcb̙o->{ѝp@ IDATN:M~yվX}w}uvwz8M?/ώh ?n?WZh"/  `M9;PaY+#>'} 7g~ל$#ᥒABn0KXCՓ{!5=>C]iЮφO2"ۿP71D"򆘍Wڽ`<0(|?JHۗOjN4'X:L 8rpok3/Qبf#GX;I.*74|[I/,{z_{fkb>?Nt<Ƞݜ}-Ocf_ۿH۶lNڇ?mPծ[O>÷t?xuZL\f 9YcHW};Kcb n>2ۃy^h離3ߣxj=@ [cgω DRqS2?ʸh$D}=H|r݂ v>nhP) 7ݸ-L D9t|ݩ37^tc_]տy@K7V"%")FT*ۿI"r:R#1lJ1齁R"x1_zW*~43`W""Vd9\NR7t{IOwT<|=6by+1WgH\j{kxC"7N!R1:)YZ q_]eFYww֎p%"3'UE\H*+Ώ%j/;syFD|GD$U,3kK_v.(r -Qs偿ԍn~d^6m&C:QH6`Vڙ:Hj ޑDoUw|_܎HF"teCF)ސqTe7hF\NיFYv,2pA2lr|eKkwҪ#{`|9#j7je#D>.#[ꋷf|1.7$/fS5ok}wiQ+"bXW֚$+Kg9ae^]ޔpЅ ʯ|Ud 157q\;^krwZ_0~{,RDD|dDdlj6ڻ+uuUQlήM Y$n&h_["ח?~o+"79L#f[x!{^1)nخ;{_95?a=ߢzdY5U[5X9(oܻ~T`}G뼥DI_ZwX3Ƞe.DDϾ1Jj}Qk%HdliަR{ѳ"Kp}g;dVm޾X?NhdE6^LINNNlkFfFnȠk/9knh""єO2UDzmu=7==}xqsMG'>ӳ[ncaYShBcMK:"_=ZQgNODF*a&^?xF"*yADFN1R"}ݩ̣j[s\BJCD>*;M%c3>#iD$qR^=iuMhqľt";"=;o0הGꈨTquk+ &;yI1zE*uʉQ eELV5ĉDDL9b%XK]BꔷbyR X44*yʘt{VL,^qśCب ǭi]cbJt]B1<9PB}y QnEqvL *רGM:tP&cҢw Sgɔ˕#EFJ}1mF~3lftteưr;~JqɩpD(߈|hEnyONRgА_mrBvyF{Bq^gv|wPy_rYoQu|ÉSC*oNJg_s o֬]>Mh&"ibx(>/}i 477 /\w3=;?izHs*=9G%4%H;SXKIt*o2'jeA ZkgɫfJ_ :bG*㦼=ʿl6L&^ xw駟~=ijon4p:sfn&:4iϦNOh ?\O=z>"rSթ$6n#{ V5c/u u)6juhJJZ4bLnjOhx5]PZ^57Wyg >>"rSU#X_qӞ(̹rmTu) i[#2FR1c˾b._F">xddok:uHѵSWnˌ׎n<䆄 ݊tuxXQ7-f@'bfd2x555>wo0'^Ud""7r""ۧouW /Tq%k_p>ekvA镺F'"/`~n պAD9'HKsdnPy!oWVnq{ư^ vj[_n䉈q ܐRq3v51֝HtY4wWVҖ5t dEb] {$"Bo7QO}$jꢕ*N+uq iN+.?~Tܺ}f<ʖE-5l6/>26 ] e5D\c3uloBƆOZ:1'.?yM+MZ{#"/zKRH[˚a!c&n$۵uw;z@u[ K%>w,"fŴc9EޝdРa!w.i)O[׏͊[NDDl~woXsGNL(zWz]ttCB4niIf]xyxxܹ+vdYߊGH£߷ؘ\]i^s֭ TgN~>^>su,4wNW}+Ul-Ή>raVk%U]&ϓUgmi{<5n3}Sv7gڜ] [LJi37Ok2}nꈤp5I'4&ѩN\=U;Y0r\CWJW`Lj̩y"VIZBKB 1Wr t}7<ӄJ"u-[ௗ^:\-$rE ԝܒ%g땥w1qIwL-um4$Bg VsDĎJ\?!nH0D<Q"r'l~LN+oi!'/{ω*++u_BD>\nE|9za< "t|i/yXDK޼^p~iyeLgM[G5qzD_Z:+7Beյ$qJ';Еg ~F7""H -][GėJ+u  돴 ݕu^i!mUHu鑑]g-_յ˰SeN0W]PZ7{,z iGxs53R=vMԵ8Qkc,wno=IxCl͊\>>Qsy9c'nO(m*ە?]W1A^nm;2/eϸz_B:( i"p׈(e[*5'}Uy[L v=])߬u,NmNlc' GDz $^(.?ud%GDuN}l^t; J%O6tE[v>:qX٨m|ss۠S?NV2vL1mӺ6Jl탺{\4v= nJHݙ?nKDDջ֗mDd^WMڬڈ~^b-w$ztzLLLLLLjNrjٔU-7uXolZJm,us=W.^~-[8ϋ}Ʈh ~_7@Dx*n(d%YY2hl8w_n/e7-<1.!"N&nWei3UWӶ;x>֟skm$l(kF wI-;vԬ7sgҊE~`t "<]$N~-}>\)YՇmITHFʺ]1kJ;KNW6xo +w^׷e=+-Yuvyxv_5®zVBB́s߷Ǘ&uWL.+?pP 4dieu;m\(ʮG9ٮt7vK+d2n߾ob9? 6e#N.A=u?MD?ޥ":Wa|(?'kzx{=k힕JH{&s ?ݪ,+| IDATZ_ ǎ[5[QF{A7ss+ϘFN]DTw+55\*>6?>èNo؛_GD\ A}zóRI[v?Xz xgQakٮ{"0ԩ4DC7;V]UE-?4y}G>k]Iߡ`oq#Q}qqu+/-ؑ?Gs-< hVzG]E:rbǞn歘{0 gκALiLȢv$b{+sm[4*z]R;uysbˉ)lsN̠R'UGDngw.lVMˋ{(̌Zw"OBD [-Zckg"ؠ6D%+fi{ f>H_5cՕFFϬҬߜU~t9UdxR,l֏y555OdOzvJ_u:3,8|ܱ'E_'b|&v :~x떬ӥlj0N^~B p%>HtJɁIIg;׬*`}scPisHH26yWiOĺqGAs ڹ~KR'Qs%=IԜQ[N=ha]Ɔ qLR[ <_Z~&Cb< (  [E䀀̿I$"9GA~u";az|%kӕׯ7ypu0ߠ^h.PE5Hj OVVWJ^y*6ֻ݅9'^4w۴磌F"Is>^H9[3U«z!k?磅>DϧEŌq b^x*5uw 'Pr HD''q7QۤHZ5&%n%Ukvt{+tzW P0h3Rg҉Am9zBpPOT/ ':\+wzBHYQI:ެf,zR/$Dtr[>_l=S #GӳM 9G>Xa4x^ Z+Pq{oA'{\xtӧ?7< |$r޼QQ'=:z>oMIjy3Xo7E*!DrvA#0I04{h>Hu3oI?yvսYq{`ͽ)K"1 h1wX֟lHD4woW^3H!DD!aa" e7FΘ]jI2).n\8c9UY}`P()†|QQw'?~催%qaIpX$蔚#S؜ySz"yͶ=ZIJ1C6 BDLې cGŮ~TnరaP O iD#Gs=R |7G:uixayvĉ^ꄆN<9&&&$""Am„ AzzzbbbFrX"W[_HHL&;ydWWR sVsv 7D׿N0=܃B]o㸿<\Èw &7 Jzzz&M$J€0a—_~9jo sVsv 7DgKq}%s=s#GK.',,<𰰰" |'44Y a^jd<ĉrr/b!̷GCBB.:pS\xφyac;P^̯+seFD@D@D@D7cQ%hP( s&WoP̬kQd8 ]]DC#\syG>d)5iWp v\9 ikasrL.Uf)ak6n, ; ~YuEVGNo.u^ϖHM~wD?u)K+N\mչk{i)KU8{vb[KM|-u.dz `m)]I.jifXYTJ":9w:9R)s6pj'\FuWƨ"VJ޳᰼Z,4l"0{*Y"]ּey{J uDqoѝջJ2?, /9qu<3'8;6c˱VeݒJ=@2}f:i1EWز< g[FE+MAkO} doP5Phb%J#kʝ5Ev/P 8 !Jrxg? -6;efOqUyn{2LhW*H18- nGC\K|}Hgn3 ى% tqO\\ϒwOaFxs~a={h?qU%gʳn9VO4:98A()yO-,֌mϔ+ Ԝ K"45Rȱdoݼ!NN)wE1n.߾gw%8DOجSvl:z)̘޻/gNHWˉ ׬I Cy տ;~-YZigos `"+?z?\8AF3Ɯv⫞Y /# OEN$|Ws授m_g`_f,g`5/+A=kܝ{8erovpڹxfF/<රd%k伣(VksqcA؜(m(*j.YŷFp[ZnΤFyTfk~<A%")Z^XhOG[3g%ڪThoS7UXY5eB 6 Ֆ7tIR7WYmV5.[^SSDZDZXJˆC{?_X9cDoTv8\n/%]Fp:<•-wcTܳAD%Ή|km<٨E m;s/'D=?ϩ/.4)5l޻/%FY#cD4!";x]frHo6˝6{b)\%GDߝ4y&&W$" Xd"N4Ol@0!#O;#" EoOa=uU%LM"QH^ii_}Cա׾އR1nGr~2.|!T;ӲK4, k/[h`DQ+yA6kB'3UۡNg(+9<rUH5:;:Dk-(e1)`D7/KRȉRΛj,)cYu;)R6mGZ\v)O.ޚ;(lyF[6`%"b븋:=+EDLc4("tϒ2lPڑ)z VƐyXb2TGKغfvQJs,౗U7zvQE$<,3\  Qo!_ǤOdC*Wȉ}9D%](K]KD} I͜Le#gQCg* Z*,HlbP3Bd3@DaZ|v.2voXot/bS#x+ GXb3W.Y}7$$#"F$6Xa>#OOD$54}Dk{Ls<12e> 2ُZ;TRVS1?^Frk.gU|CQ^5[ͦ iI'"F*c:q?aGe܈Lܟ??gwۧ8JI1}=D2"g ɾ QhXЕQ؂כ:ŅD4*H}7(,HlJ==jF6?P{h .djmHX_'5X#MJwe'4Q]Q#jED|k#R.ZNHتMV] PP"+`oE"hmPT ur_Ӓc2 )8-B%JиsM5Kz`Y)g,8s\rTYh8}YZАh^VN+gS͊<.39l{?!F*Δ?c0 Kj:0nk}ФO/? #&&}Mj_-^gZjy3æ?Ӷ!u3sM朝?^zV1 #?Z܊ Pgrf?KYtg"#LL4*gŋ{+L՘N# |/?/')Ptj4_*VˮqRsNH0 n?4jn~ NKbz q˖6˶Y^uޡjkNwc/gxk)))wL(2\ӝQV-$(g~bb^C[[QjymҊrE{w5[ Uow:8-I'NkJ=wDYi+wό"ֿ45yN_"ӃRosqm'""ԩS^zN8fx_yg5Z؁Ĝ:u| tվǏ:ujԩU"v:[pӑ#iISά)\;+~ҽt#VhG{4i~pdrB1AL>3C.Q|~k׃ S"Qh{%`DSIiDO@#""{ӂCۚ< I: OYˢ?cߋRc tuu/bbb9fz0*7,<3h@OCBBy#GENhhI/ۿ48u~! OoߡcSb9+%"HCQ4B#C%D$MypǭS(tI \pht| I"$L1.k?xh -iށ|N0GS{JV*JuI&:($$DVj↹o_4Ai|y?&v(#vҿӠ?obD¥D$t $ ~EkOl{{oSQJ{z{OM+tڏA=z`P]_>`= ?KWKs R2ˆW:4;N;mZG[|{ DJ"V{D8h{=Gi"tM :WAn"lm9Ո͒f%:q߾hOw;NMG_O%l?5( JR2ӟ Dr:%W ʮ0 s=M8ݻS(AW_, , IDAT9[(T~|cd؜w=3$KkjQo4f錇cg*r?|"igZ9(y?y ȗuY$LUniw?Q')PӉc-(2"2"2"2"2"280kGқ WU-NRj-#y\5Fp| hn 3ϠjlɅ=VZm~6ik%_tS.K]s.Df'po83Lz] BVX[+i<׺]%ߠj5/LUb#yicۼo~JূƚLm׬Y&iQIr5i֌]O<1kw[+e&b:uۦ^L.uJ*rxC"{pEQYMlljRJrC^[HuRJoR"tZ㬷y,*) ZGNqY\lh %+j9)rZF7"Z^WƢ2omƋ~f``) y%WFuk=QWC2ޔ]z̷TY/3.2ڲZɓmac*]\VbOj - =LVM(tBEɩu]]VZ}՚gkzrY-&V5YlU5h"̩i&9+,izV7[\< %3Z!mxGPhZs鞊l^&.+uӢ_]cmƅC"LHu^,#gu =C{k0[*UW_;Hj ~RUך0/Z)wyNNN+OV?TBEn ZVoγ]*kQ-GM;+ , ۦdZymIU?]WI8:/ѩ5 Vkʯ8\Ƽ2t5r∈y~Wv~ 6mW4_? ?eg.@y Kuk fKE؏D#"\?%"YQ n^5VZQמeYś%[k,+;q pYA#3{< *[Qdw+7m-NaݵwY*˜[;[^IHh)Yɚ7mۜYh&ܐ,%Y*NuvnB/x&AcYEEeN27:l99M֭f߸8{S]k5e-  HR"vk`)\q|kR V[WCu(3tF9Zoe8u+5\}Yv^Ctd9WnjQi 25()o_Vdwfm޶)SXz5xl9+mFdԺ7OWyE c~O0d5x:1TD䫷 UbkM^QOXS(n]j ˮ-TCADq;ѥً6J:+7[#m٘W(o޺uuV[.G3nyݦM孵E4.O/\%ۋ]-"1lc%3Ͷ7\m=RtzX"'ZbT,TRmZDN!ܼsSje,s6wMΝ ve+l^k^ѭ,.Vֶe_| )]ѧYYxXbYg^C; rjx b}y~BDA/z=eeD2A)e5۶mZgטRb|.G]FfKAo۴b!욎ͅDRemvn:9QC<ƒk6rWjwms2*MrQNn#PFʝE.qM62gLQD$7f33bD܍]X$J423sT{4Exb`0gDKhhV[WcdYNN^j dg ͛ S*U4CDlZ5Zڬo9E2Fɓ3>5{6#6VI |.54!-7+kttޖ*dD}bvMaJ?6zN{ igb:3֤۠f㰶33 eium"A.U^楙 *(D[YQ/"5bMd+ϜtE%;QoT^7wMe9dE}Вf2@Q-'"&Z{>MY6GG3˸(6sYOX+cG6^1`rD %͠1d4Ǹ/~FTնir nyF饅^ ֱЈcgQJJ=K͍upDFxۿF7uyRIwc ωD2!"bN|DԸbL!Q0Xb7V٢goNvu`ag[2g]FNJ㉨llGj""Fd̞  8|TrR%3fHݼխDT~xĵ5g(F\9*AEDeKjkLQ4''q5}F\ND<%0kmNl&Yrꙑ2F?,x^=<X[0|wHr:[vIa㢍%ڟT_Qu55DĦn3WCKcD>rXft&`Fo (s$= X]i0g;k= )odYQ$=6V)6X"n`8DW6j.PۍqcslcϜ0rCnFYC"g5%ٚӌ 0ɹ+ r)_'{my[Z[Ə Zdr9QEyV'ܚu;ӝ+j]{Q^tv^!up"x7u[stܜ/< hS~)_r9e5Vml?E-9C40L#r.pn/ r"qڬ"ZάaZ֒T|Mu7+Duf :YU(2KK@60 yh&>1yvbtD?9l}ynueZbMFyMuciы֢zNhf"r r=7#ϖTiRuDƧd[1CJ˜ԒMi:4GG3(#wXϝE)֗Do+suFE16> kז2;ɸdWT,+%QjQwYQkfJ1Z͚VKed]%MXlHj^ncZ3yt+ 359F&c6S_]P#GYdT Kgsf )[}9nAQ'eá1 Tm24eΚepms23KKd e ]^dGIHĐ@"7p$Gꢊt6Vyi99Y_EfZ&M5G6-JӖTScYeޝQ3gu]epS|׿N>}iQA8~xѩeJ{^]ť\~aEYN#\>?K%%U`cm%+ueǦ嬩/˫-NkrT[6*s$Y.#/"_q-֋[KޘhcMśeDrurVQI#w1jP7JI4F{<,MފuجD+&k{ʋڼ/)XQCL|S!MW^X[P *we;^c6k9btߜ[TPqE=,֘i1E_CReS`Y4Z2g; EQ 3ci\U?V9 J+׬ smNN y FX#:տu6+(YVMBcd`X;d,k'E/(s-΂+#W'fP" [sKʫ VTФϵY.xKնҍUv{M+J)E(wiQ5ey%LUEG u+ ck]3{JSV^_&"Y.5Ӓxdˍ&62RvqV{NY}QN:%Ӭ(Gt;SG4€m\./q&gfĶEQ bI?#UeoD%֍9DY%iQ?+Uegy+ /u1 GDqu:*s.`h&ڳkQ+wYӔKٛh($8-sճvϿa.2-ܷJ,) MR{YHrAܼtt46XK6)QDdM\Ai͚gj[1p^+ EYlJqE=nah7&Z8@D@D@D@D@D@D@D@D)pkkkkC\r"pkq Qi}Q-pߨOn޵ko["v}%R.X532(i~B`Ͷ]x"ĸEf*k<4HD;~)ޏm3HNIHYbV?p|pݎ]DUO&M@mzZcLON,-|nzN47tn=W8&IHZ:']u_ҔGÈ(ES鰵b`'5Un;x |YWeKG[ެrGdQ5~rH vu*v8.zTnmW<IX6$ٸ%L==94C ! QN 6.~NPȐC#ަE?R)>A.h1G3I%qr gUf-_7\Hv;,"!D|OwM0 |{{ݱ-wDdhkBD$ d7?*,{>0\zj+h`"/Nr5S_m K6mw . 7T DD=a!l|`w'J jcm\(I1ݯopi'5z(2dy~;.*HBDݟ$"l>HyD e6ioۮ% l'u^"Lm>d]~6Ar ~:{\II6CuϮJ<'4qzJ=!H .Lrr7wQkèi[1IXw^UGQV_21=g4))+Qĵr˶lKgFRPTRߵr{3i`qn_ϝyDpUs ɯ":eﻅ+}ٯɯ>a+ٿ W'eYɵi;!sO7?8/wG(YDTpj^-JOh_[9ZɅׯ}C$"YO_|̕OSxi.KDܾWW\+$'"nJ^W?lH5WVϒl^%%с\zY#ҴYzi~m^+}wV(\Jwn OxR;ռYV32nE1 p~4߼CK+ZubڵWd8W*v'7VU "Wr||[5տ~Wlv^:0VUŇKkkʗSW? j֗fqk'"|E~5իõ/7(CGŽbs8iëxg1MۇkkZ4y E>({ 6l2fF! Ly7FNAxSJx?J>\u"ɒRT1}u3D$SD3qI3v^ }^]S;'Gq]= '"i'}ޭVPӹ ""O/=ի,EӞ|OD\{ӞFY%wI||xC2v_âƥ-+Ms )|},\>CD߈g-//^x;4ۛi: <\vuYߞ9-U$_\Rh_ɐ4\b_sߜ־k?Qknһ$5AD{/] n@DMp;>lc{_fb'ٴ(9qmDNs־w[=}p.}z񕧙-FјjG+/=xT5wֿZgJuK$qFielܨKS[TҹTh4_IDATszՍ"ǨXjk'pֿ0$QsY^= A879~A+97+hyƌpLe:OXjL@-Ry-:/b'$,`J$/7FW$,4Yt]d˩ͷ2Ly;/")T͟2A5zoh{yw Q_ξQ.Lq)v]Tܿwc!Q *됫M;O7}$?I87OPA!gpApK RTOgM)vU{vAdfGsOU&JM;u(Q/Z mߵ3t!9$j뮻n BN*S_:}ӧEQ:H2pECGS}Mx&>0n9wnuHMZ`F8޾a`J 0Cۇ5k" "'ZMsaatڪ¿|9J "\ Jp8K5XggDm6Ǹ쟴K@gm#-XpdѩV͝6Ul>(*}#<QsK~4u)EISCPw@D+H2:9cU9WmZ"ylV6ejذm!UۚٔU/<~갵lK?%Ho7vfdЩ֊ "U6eTuGPwu`ŝ(8F;%X%+aPSX;O>3U ?Oa3sg&hg6I$ D{-$&ʦo$"rRGviWc[8Ȕ4AHM[p$iKM_BkM쮷e%Qlw9FE{j }j fl'X鵴\>oez&cII鹋$5(zqzՒd%.0V"/NJR-q9O0fΞxv4+pH]Cǟuաj$k;[OdTvqh|(>;lKN[2TLp039x(۷X, |>s\6]ZZjnnf> k±ޓ-[;{.A |ƶ:CO]dCv<p6- ui]Y,FCϙl8$LhK_1#@"$2@"$2@"$2@"$2@"$2@"$2i8M+;onvWWه_|᪡[XXt8,0("o{{#s_}WuWV~p? Ie۲kbVʏu3//I*wl`u7,FKTXxi~j=Տ6~ػמ#Z~HoTvߙƿHRJ^qojk.'W {_wמ΍E,Vl*E[d8_v~rI-fWR$NwfYaAR6ٰ{]eURI_a;o!Cʷk͜cLs[<#rik6Ob1w{nRQ&%?|6{B_N[{[}'+ɱv=;l[.,WJ,Uٶ?WےRI\PےTX۽#ܙ<+K*; xƃO⥷iu=AV}xܿվ+xkߓeG}Mv[<3{ͶxxUK?{\Ѷ= ?2gkƒߺڶc[a l.hK_1#o#/3>:l,vYd3^}fRv>yfQ2pz,\]O/OIe{Qʬ-pXHdDHdDHdD]+  䜎Y7uo83 +++b1 bX,?mr8t/X,ڶ喖+[l) B"sH?KJJJJJ$ &A\ZZZRR}L"㏔7Wz%~wJ"cn7:A츁2Df"Q p]dDHdDHdDHdD>/N`3I^]\% 4uk gی*.<>x臡 V>y'֮5^z'u إmOzg9+Hdl(ky|ZzU$~b*aJVFiip̚'rK2!_&:$k[+NE#sI~Ww'} 6v7 uJTz5mK2bkiɽ^i̭<VRE{j }j Vl, okG15vz9iJ'FG^T6*}!&㶝^v: _SW-IV siԲ%Y 6+"K<17[MD~vUI*VV%#8&bsH"#㳋)[rz*;?4j=0;8_?ɱEgC߫_{mxx:2>q]o ORnU;ZB|.fKKK,5 Ѭ O;W^Dw Z|%=LRRzqJ[|\=:8|wHrz:q      ;30 lj  Q, B>\..--1-`cq      4[s_~tZá vs}? U6.reck[[kKOWOGs {q5C:18zv{rc dl.%?5:[bg צ N/2M]?q6<2>HNOucxW{kX GOM)&Sr:/0425չkXs}/D͖6nvҖ6;6?,mY-ً+V'fS5=={5;2p6fLiq*d-NlC~-M-C{ⓃO >֪kގ:O[$Ӑ2iymL|rLZJ^Wgw u%NIkRV<܉7JL =%i$;E^UU3Wf6 oS);6`ɊOE32->'SrZkjM'fonȢTPg8X[߼#Tj&x6oMhg){a:n%W$ΝJ]voyi|Ѷwje%&]^lrjzm-#6?5펦imfrW4oWUe\4tZY2j+$;HI.ykj ٩Ռ2݆-e|=]Ǎ.[lN:[1;~-ѳ/OWhu/]Wc/ %lTjjd2PWk '$RفȊ}swC~ietxtzn&rvd^4n ?~\dfȼi`҉Ci`>wsu$Uh w\EEkg#^"ƶpcjY|{q9=PGk[7EmE5|v||nfhx3snp>%-wv﵆G&'T7 + !G&q8Zgd2]w>B|.fKKK @jD~vwZ; ɜHY>NP(p8lbX(|>rlviii ]?c\m2JIENDB`././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/doc/source/user/figures/import_package.png0000664000175000017500000011644000000000000023312 0ustar00zuulzuul00000000000000PNG  IHDRiwsBITOtEXtSoftwareShutterc IDATx{@g>oB2&(ZE"x TVUܢ*{p@{Y[Sx * U+EX [ 5c!rGH27gO ZmJh 0&zE-XFh6 fIka1֩Z XOE F˓nOh1l.g&M~_b^Y֦4Pq1U|^]"xPbgBLuDD$ BB$,D#|dg+Ȫ*]>ag~[n,cOd'0"@ EBH$ vB! BP(B@(^o8O:qs,A nXD&gQ򼀄D,dQXiɐ&I@$-vZNEd-Fdlhj wF^ Dd" H`{W Z3b}Ab@`ij$P`oůQk+东U*Q(,@DD H$XBpdY;;#ϛfF$j~S+Wo2iӦ,KDoG%Ɏ9BDoѣ-CZmFFƭ[̙NDgϞ=vӯ]VRR"^wyǺnG7ؠ?ĨaT}GXFsʌg?}ۭ*1&l51 ;I p3d6uP, gOTI{ drHD"ѝ~O7)4Z,dImK.߿gիWee-[lYYV}:uJ.ѝ;wy0`}˗#655~(,,tqq&\Zn@hWV0Cu&]LDe|F_Vzg. #r Y׆ =63S}{%"V*Ce) ;W;_՞K K,|׾~PG#ti|qԄd5;vxuu;+"qF. 2|nBs>&"qy>! Nޗ vIv#ɈW< rK]ڥ>iJm*r@$y3 t1"[,oϛPh' BY-M@($Tlt?R~%M#xgo[3Z߿Ϗ5KN48x5">}Zϟ?͝/=菕.DK[t{tTT۾__̾ QD-H9by˩6%ZP{%:Yq5:㓽ĩNJfv.Rv-kv]Dž KJ#ӓ,.۔|攇Kզ&STIe13 1"`"˾Ws{ ũl~f 'C.JN^!b>ӼXRY0hgmϯQuy"#C4yA&|sRDUOKnNTQ+ݘS1 9"C3D\؄rUx7r&Li.prRWo/V)F,^)9õ'bWɹu=Vm n) _/Xofς!'+oTǜP%C."y]^%Gĺ,Yp)/.4qֻRP~^}ײ6srOGkbcӆsUV˗\=j zkf!/V߼˾<5&~KĕIHSa#꜈%{Vo^ڟ!u%;ܒWxݮc#BWsdyUnjB5Oz-Y''չ 5\ژ3bi|ֲ*YiLa~M[mOgnqFF1kݺxr[ۇ1a3ZX0;FusSСǃN' ϰfB$vt۷oQ^,+HZR`hz'VKD~b1  &Rah4䥁?οnͳ#u̽|wrBOכnsǿt|3 +֜tۆFv^+NMΗ-ߙ(e 7Ѩܢ}!JFS$*mgbpIISpűcVmNKsQR&+7Is2vAB$7"Ce&<}_ )LX|/-Z>`IljOL=}7?i5.޳+\Aɍa#*xunԒ/iKZ0{]4Pm~]p[ q۔U&/!Õ<\dYJC{μ9O_۳lYti \yVB\%~I O=u8ATY*Zh lu8 Sפ`T{f-X8>-.f %]۳,Zb-I(B=*X1q}wsOmOq%q;ޛ\5>,q9l "b|3*^]z^ P2DWl/qs^Ӭ  Qbˁgj9O/MDbiJ\Wߴ ""gb[[)B"eyn-מ`/2eY4-11TA*cJL QTjJ/ć%"E>>^Mr7U؇Ty G61?L[z[GdkZKyfE@6˰C(LfoB[w&ۿcǚZeX6qbi}GΟ : %5)> ?!r^< 0gc9҉VUdiX99Drʷ#o|˲h wDM`}gŎ-t 1GL$N{(ZG:ʘOD{}#cPQ %"bBg˗'/οK}n5he,2_Ӭ_pڻeSZw;l*#WL]-ǰ{SsGa j_ݼKrb95GL]CS\d1锰dDu)ڞ䃕"[ٮ 2yc9{a1a-}_/MLrpt$Nq2jɺ ` rD,)[Xczb\XduNĂ\zW$ϚRrc#[۔)X:SC.%`W uDd15o@$"TbQ;9uD?mpgOSmMV7&/ؾj]dzLC,/4Z!brGYP'ꇬ97Zoj>]^,gYVw 5&oCg8z((RZoVBr`92Gq21>ۭQխ{i}q`:8MCS-:TA ` 7?zHNZ| BKٮdoo6np/}ps^֨LݽKeryNʼne2kSK=~sm')"C&oyi⡑;bbxݖd5gx CerrDSUȍJ[cpltj&a;⑯@ 5]gXl wj~$@(0B;{;H(6NYd2"RT qCCb^Z7n ^zY=z0,fM+D$|yЪY^MMMߜBX:! f6zki"/epLN,ok_tA9j"e2!>|B5:E)`}B<֗O)% :'OD\qN1(ry"UyY@%Cɩ+br7^ҿQgx"ʳnA/+ Ç׭JȱԥY6qs+)U{9""tOlB)9- 7YVjZ۾{ s3ڳj jӾKҬB Zr#ȏ҃g8 /ks,Y925;rr `b'F8\^OjuSjs+9"Ϳ>+?gypUbGD<7yj+cgQRʊ̤wbi%4l1MdX,D2IsgLf#Y 4xhN<==7Z\.%Ç _}o]\\j{DD~~~7n;w.L&alϸl_+ǫnbDBD,}w#p MaBn\ѶobO#!z^IfGXHȲ䫢ыu/QK}n%):呁qǦ^A!C~IsMHgb &pDgZRK*/;*Wkx#DHuB ,M\hrS9 6n^ԙyIvACSוDW'.d N^0v\:BݑZ4y)y"- 21TIϺv$,HmyMɬNH5#"#dyai;2Dw:U+bǮ|/V\~غ6zi≨҄3?C4"2:9uɄT c|qy`9|Qɹ>o<Cg%$x-M\hI3-rBVǭ2 q2>Y6.2/rhb4}84ͣb&u DoYxE"3ɟ5DƟd"d2zo}Oҟ1#N-_!t۵kvIDr\Ѹ͙3G,['Ju:]Ϟ=ܹ#Ý޴iSCCD"ٳg}}_}<8,,SRRFc\\w,G$V5ݪ7|ԙ. '\Q}*";'"KD$ V~U|v?q_&"X&ź[ ]fDŽ`"rٰ #iԔ&LeEzV ݉Լ+N2@\Y g_ŬTE6uuuw-xS,$!H)ܶhl&,DqtV~p[M&+:#rs%J ;}\. ի=z/))r  4ˋX5j?߼"Q``usс勢瞗Mk}0^qwL\61FHI{Eas97=^9}+CHȲ jk"JI_jsynGHd(ɳb5 ӓe_=!npZ_hr^( Yf^d"o ŌX"4r7Y/z]J zH:X,֫Udٳg;^ZNBb !VxЙ6\cR˝;? b?[Bj{Lf2dtWYZ%QE>xL!hx Ҕ}K;օ-zɻ}.|/z+rmfl6Lۯ:pDH$#d2L@άlXL&S #]Q̃C2nl&֠ "2lX,hk8B! cxbZ3=EDdDdDdDdDd~eD 2Ll6)B!0Bk^'bfd2x]@?4%c< b_ o~7wc-}iSUƬuiӇ6}_A?;^w^G> IDATo_n/x7;(yh >j _/ڍ_6oK&݅w Q~Iviڂ-߯|5'ɠ{y_뿻N/̖=eK[7찚ig2"ƚ9W1D"MW<`{P`7= ~@ec.b&d\?| ']p𫯾8R#"xuWVoyh|>;x-M)2풍7WA/,ڇ|=uџ~&8tಎȠْ}-Og]?Pۺt_Nه|0}PۯO?t8teztlX}(&1dǎwEF/rHe wroDg@4{"Wwz0K׿Q5f'=O>d1Qxӡ쓗jD~Cdž,6f"Sc剽'7EAcgN@Xubޓꛈʡ㧆e|~HD>f'غCA ~OܰaR"2$˚7(%"r0f'"o(!"0ǿ݅×o|EJD.c$TBv|H%D2l"k5Gdc(CbD$qsrU"f.DDRrj QT| mzht\y3?s9}D*%"!o)iZC b!s""W\F/dc7׾ "%"濻DAs+BD7gIK:ԱUTzwKXW{vў2"/98TxJ.7MxYL$cȁDDMrH7:w2"|q+{a jEB^p ">~WADgk_0P&&"٫/_RkxW*i+N9b`?g |C"r""#D""ҕ :`hc1}YdeQS],AS> t8-{ckWN%<+V;fSV6rL}CRqZGG"!"uayMºt㉈VlY -ry F*["rNш MD)fN%'ҜP:v]tҦeUS[N7*"YX?wgU~qtfF?jsAJbgUcNw!~ElMOJKf2ߖ~>[ 84* fӽ2xI\-G,#Z0Yӻr᪚%]mǺI\n )QUƔ_" ҅U 1{ݜ1,#}1CvNviwRm~xș?Q^PLpkĜ蟼ikND Lr߷bk7YxMK{pUs V2-q*.kݱeR"]ɚįcoDdi2g"g_ vj݁% 5;T _ҁA$jj467ocG{s pywϻdVnٱx?LhdcE6^HNJJJ|ӱF~gmyo)AW#"79-kήo$"O3TD:mu9W==x{q~CGi'C?ݳ[onCA9mSŅcM:"_;VQo^OD /G);_a&~Dƹ":iaxN1R"}cj⛋s\B˰1J×D:;-%c3>=qD$q+ %*=yӰU.l>ь}QDD XE|o{v:nJ)5]QV9qHh뾴ը:[۷[JDSdl4Ľ}=kO15rTKD$v4}`uj5dn2"dl4CF"^69{ͅ+9U#"2ޑ2jrXsBGDb۳+)8k@W1ήJb"jTdbio&]^uu:"{{;ie#,ozDRQ7D&4e/i "ԝS7cy>&=qY1fe*1'jɉQ cGJR5쐈IDUDL%|e\S}|ԷcxR X<,2i4Wθy)fʑ#"DDƼ:L6w3**lcYr8fm؄c9"boFvK>Ѿ77|2CLDiI*QJRlM+JKRI}BiO;Y# \4oPO]33P^y,FQaΗ>P?AQVX,fd2L&z}uu?lIKnykG UZu=19`{7qzE[gߎò}x/]O>jђ1d5'36m˾DF]2ÜrK.emZ)#pP9$pTulۦr%of=Z5']uߟ8>b;!~YPsfO7KU_#Tήis/J}]qlmHz<ȍ?^(Y8 "w m }b:ֿqj"K  _>? aSL,5 "ïD"A-P( BcӞKi4"2"2"2coC+܋oZb6M&dy^WWWY@ DdDdDdDdDdDdDdDdDdDdDdDd_wD֜SdddddѺ ["#?рʈ\wt=d"Ҟ/%bsN%'N% uђ,0tM٥Ԍ s7?QwkviQ{(ck>|L7 tKZlP@㱌Z dPm.9"*L|:1n҂kD2%S%CA ~O.W uE{>#MMsY֬zр1s9W׶pxdС2^Zq)&ve :FmiVVe#>nJ7o mM_X9sV}vEo:1!lzI:Gv/ *#~r5sm'uC][Cw/+p܇@GR'bfd2x]}{y*2ʝnLˇYo}u6/,/nݖ_r㉈u7c^kB.w^`L9nɎ8Cw&nҲ""Ϲ[o]ݝSTrF˖1`†ޯ9{ö#Ex"b\0/xT5e D'S|%,ٝwyGiݍ̭ym6XCIx"De DhS_Ix -mh6fO\*BeNem>q\Gϡuqs>v ]m>ɍɩՖeemJn_SvڲSbNeDmM`=5f$L+zgE aիWxu"o+x%%y6%O v7m6ɌXۚsYϠу]zIIwbޮm=8] UW8U5O*%]MYED K$v=w޸}"?sy}B12kCx WJ 5svg_{bՎ|:=dTB[57JvgD UT4G<$r XH&:c*".omއ\ |̎ݕ2qa3MJ*K0?AgJǜ∈ښܫsv[1@ۀ>< x\PY D}>ޓ3)k;DX?%  ۵lg9ۚs"rMfts>e}ĽA6cײT-nø̈z'"bCÃ͘w$bR|OU><{ 7C#gksrt ~/3䃠_I߰|?VUPW6Ӛ}WFk?@elҮk~uX:wtw7׷ `ys/?C]"kenTܖ[Y+4y=*sk qy븨ع~o <5PEL>>xm^ya}dM߈_D% DtFG$m GJ9;H! N5$u~yDf}TJSeN2Tq5:HҲr@ > JunS==aנ鞟WqEGn7n^~xڮ+Dd'ִN9ި()*kM {صczom>'y<k]LY*#"vtxÿ6Fz!≈kn4߾6gxerNu-]Ys 9yzo[NT@PQ" gwpD/V  )K<%!)E4䕔UtpeTs_'BFa>l9y%+jGxoe\l p:Ua6/֐5sDVpwebHɲZ"lw񬋷@@e@D讬L `B+Isz:i&m}@(mj+ڔ>[WZ1ܰ.#"2BqCEQ65Vsn3UUm[+zSY'1mY"i[+63N"8$ AXTw$ȩ jsb"< 0XKl0[77mud7qhז_|dF6kU]kbJçujlٖu 9+h(C-;TU*q2eW,UP"2SAm]lV\rJlDF_Okxfu&-::::::%Vϊxg7ꋥJ{V*!Q!krr*~*̿| s?nAzހ5񕇶#^sZYIaoi IDAT755Z/ID WWWjl\tl]|џQ/ʛNyg"5o_w#Az]]EѾ3fgs.:ot0̂Zjܲc۵%&DDu!}E DuEYhg^5ިx.gj;f w.g׿xLĈeED=]9)攍aim&WN85j"V؞it6WrƔS/L%th_)oZ63[CY֯1@1>^Qd.^LkG1JlnjfS6~z^+IYY߹)#|!z7 DX,l<뫫Ȗ N9⚙!TIu:4{xPywJPTO8yN"y<ȶgJ*a<}GL6чJaɤDY+$vݐ挓`q$8vuELZum$mkS%ֻ2N}Ko{a)٣Kef)i úx bc+u$FUE86=+  zL("enn|=p$/=\𛣯m٩/wE૙kqCeG!\|Bst*2DRwbw҆S.n˲^6/Z 3T~}΋ \Ul9,tnRTgn)BÐlzr%U7yKPqh&T~$}Ҳdn)*mqr;<$l8_ B/TA'\ Ȇ+###vV:Z,-22O59}F""3 vxoXu"#WF{ס'h;SqȆu ]8sTDmi?2@Wo Db?+ga%c ŕ7m{ W %}Я-V]@Bo ݪ]ڂ%!hD>kN(̀ V+*əsμsr29ڡNH9wqɏQ#is'O탷>rml&nWhLv-91߶7~Ԇ1pFNah̬8I1GF5:M5krhCq/blByF3edA!*UƇ\8PFW/[h~27gf%:z{K7OXIdȅݧeɖ a@DB7vi}]}@D$#"]tc)=FWPK"G㼍JYL%_V3 uwpD `z(M>&G5'm^X@ǯ)E>2b@g#!췻AH16~BH5Kt!c\.m}_ ??u|EskGGieBcg>E(6R8c;Z%Oo+{h.(d3ɽ+ml YPnNziG r? {^)?7/`#jpL p u0"|gB/-Pޯ>_YwU)pnw~sΝ;'I('NDd@Dd#_|aL& {/* gf-nRwwwgggKKKxx1ݛe2O~  ӧU*Ո혃{ 3x?QFqBo?p7򊀈 pkoo5jkJKBIggرcrOS4 aQF}W[H똃{ хRy#sO8qQFs'7 wΠ;uvv| 7OK>(ɓ$}WoXFDܙ|WM72;{MWWWw}%]nO:5%:\긡ڛjFm4iQ?fR-gښXC)^cǒȤ_^h2WWs D[F",Y{ɔgUf6dD"rT5 #RK_V;&dX<&Iȵ;}6SY[*%l }R~:@s6lܲYΏ׮oҬ~_vDP/U/I1~$;%ny6pe-u~f=:[^?<9Vv)%x/ySe_'Qo'+]R[yնo4xwYRGGDD.wkQ<5MeUe\榒xRbIN$9lMeM|qĦeԝ5MY\D$'w+~kMs#1 MV qO9䢣7UmM%KQM;*6R`px颀\eTY]S"2 ֬^y*8vٹ Zjb$oFnUD-+3jV(:k CTX4QEgu\k #&e՘ l!qQ& K$1kY"R.ı19E( Xw?=f!ru:,4oXCTg vx:%""gg(H=5ٳ'1DަLgOzcgk " RGt^"RD泓"fVy 'g3DAgkz0q84{Ϛ?>v4 N~ȿC$s&4MED\DŽDEp/x>4&'"V9kZE^ԦH6"&|[S֧?]Li˼e1BND\x6=|ެheZ#SD(U9m}2h>MuFYATMzRzaTY%"=OJ""I8 5fWLMrkX" 3F5N7gJ9.-Ĉ0K{JFr$Wpލ/ɹAa\XmF&JYTJDʄ [bmc%_5-bS`rKDrsBd%:m|MD)2Db?{90!O) Qx&z=W9kD}oQyMz=]Fb:翐w*(kf$uI~!YH7~n$uD\OMl kC"P1cS/֠=Yok#9$\XxD,^5]XLݒd~NΜPN_5+.z"QOXCzviV_#"/lhdž%d?YLrSD,-+K&Qɩjp4؈0V'#$K`a9! x@ `DW…flb6Kù \p0+{ O%"9p`}?})T7Vvuz [H."QWg/sE _׻ԞWrlv3t5)S';}<߹&g f뱽 [A$5D0Log͆~rsݫ^6e䘈-|2&-;>3x%^bGóĈ<KxFH6? ]X,g8Ŵn -2c %%v؈xbiHtZr7GI֢(QZ 3DD$5˝*cT0<,^#֖ډHhX JA@$:>?dE| \V$}}`CD>i W=C#"l)K+t% ^gBN['_vB=#+x:me 7)iţҵ$Z o\t.KmHgCCD}^"NuZs+U}.]$>*W""nޣ!I.Sji48wD"rW.*uZjډUc{¬:Eܐ(n^_'Ǯ΋iΊ5 {#8yY/5/$d"VnQn7QyS?i0L9y,%fc22ecbwINT1?6|wCs ܓxv&8 (g͛\%Lky̸)Z>Iͱտ[pQϚkYU9uQMg2-\9c3fz|ɳ-XUFO>+{fƋIA,`aq>"yeSl޼y{zeNq ^IgBMӟeIN/HeJ C63tLEpT|cts^wd"N)cIMKQZdRҢbQ <*->c0eG̠k\V*`0mԵ_ Fߴ2e;v]'ΎG6Gl},Ugwÿ;D@flr2;upOU͑H>fLS\T౿""΂uHZ2GEC:D"3㦅R߱c)gɕѿ_RzyV$k0fއm*((d;;;.(p\ryfܸq<#"<Ϗ; k>]Z>`Y{RRבCöLFD$66Lqd̽͞SM +w֞;#ד ?}<6~R ogdw;;??iyrTU}DwEDϘ0>n9O}?jp=o\rWOm*88ٳ.Ι-p\gϞ z_Iw̙K.#ckϞ={ĉo:oN8q &l>],r_Qx.dR7vÌM{g눻gr|Xstv&F$ɧ'Gs /qrCƇ#3 xP8Dsˇuai}-m;G>S]/3qiL*F9LO$t ={:(0$PFD2`m?mv=׿].E$']gZ=dD$ˈH68gr}Gt9IqcG>lɎ4/)H*JRpsyU:f@@Fh48o{W{KFib=1}{َGg䮶.ORc3_6;OV~i/k\Dי3}:G4 $ oG;>:vp/vguOt9&!*v~%ϸpILmQ}0Ox}{;n;x"" y>@x IDAT@׾sё5 _%?TDf_A<6{X͘~ߎ>C" ]<|~y#U}QK+mUΌ(DH8-GΈԂn@&_'NcLoZ+rΒk~u]isK0FYda2$gnZJrDuS!1"[e,#;6eΔgH[{ I9UNH*Z\Thɉ 9M"E&NŦ+Z\'H?ety% _K"ڥ]wUcy8:6os=V{5%M< p0KD$W\Rn [7-K3m"Q{u֢u \sE,-ʭgޚeKU cWnE$3JmҘY9͵yO$g"p kްmSfb_+8CZ<' ]4.*I%6MzfRr>NJbxO:ګ-*嵙[vݢ|@Dr"6Mz^8*rD}V ɉZ-zo+ͱ5J" M.ؚeozk+$>Q}Zhe.(Xkӳ2WsDYikBdP!`Dj6 u9򭎈M6rYױ]_cq3 Nx[iVQ8%<7%wCzdӚ(v=> 6rWչ5<1DD䩭 6i抬:YhYid,غ5;ߕ^|mх&kn""gM>1l)uh2ӵuYa[WY[inu^t.ҜaCv<׼+Q [P՚_wmduq )))L >y씨|#’ FzkQE!;_}9nk i,7/[Wf??ڋ䳇,YVF0]ټu8GkmDgѢ%҂ K%JCN"&wY5lݺ) n<`x\r9ZJj3mVmҚ"/Ԝd0$gLO2ĦDINpO:|q}(41eHMՐ106f& P")a6iFH*pUq *"-o&eOLY]n~PQDD!4uW՝KdPh%"+:\N=ذWqD$|)nDDlT 6ŨUesMcOrc ɩVeh_)H4r*,5rq"%VN{+g8"Oҭyn)s<JJ\Y#.ld۝Ͷ͹9Ŧ Ny/RD]YG#a-O-4xϒPdV&0UI?+}W]{hs…)xgS4DɉHt8(.8ZheHpD,mPk25r"=I xWzς<̔SbzKqnEiT5ߥa9ƍU9JkKDD;mU'Io26KaITҶyey3-J[WʍDH﨩)ZfWBj]iWQaeu*eiUKj[UKU<\>f-5۬UM1fi).iR},*}P/aeօQ>_ȯ7J> "[jS3"LF,6hc"rXu!8#O**J [`"bښ:cZ** 7$Ӄ cQq[/^B)VIAJSclTAQoUY.)'O:)^YQZ\DhT/21Zc<5;,VK Qr xR".!.㲤ؘM[Tr "˜sh91t]+)k&mbX}z8MlJQ[hY֜j09%]Faݼn|"b8M|ffE%ؐ50syrbڄՙ1,]LlTFٶueVkE3)ڄb(X{Kh.*dJ|V~pКĦON16[Eڿ2l K"RS3b/lhbd9.+_֬IH5+Ґ~g.糇 3m)I26PneݲZ"E贴$Ր_ٕSrҜ9%vycvj8OD `ȈQ3WNho`HK]R%JCtՋ]i ]ZiIRyg /m[z`0і1}Q-rENj/3W&3ɭqzpӱRh#fZI@DINݐU|NQ[/)" 2 ,Vp`LDdDdDdDdDdDdDdDd?@ T\Yd`naƍC%,2"2"2ȽmvWZ{HƪԏNQC?uҏU\1e4tW7imOuݶ#{g;>:aYs#wTOQ<7iQw۶}dW᭕_$?/rnIW3vhCr_Ȕ^J>ď:x\pzÕqɏQ#is'K0~OEo(vO-{=f'/?;Tݲqw[oWK;eFSgdb7= hmɡfwuBe|b2OuӮ}~UА^ZqyT|aI0ך"U:T6.1&zhR쇶Ɖ>̬KhȾҍ{B 4I==C?u=]BPM  n"bdCX:,̍JYL%_V3 uYhrx"Ư>N3uˑ_޶9z]|v=}[Bpttz#twGĆD$DD}m#+%hvruȓą+Z_kE{NnwjU@_c/mGL "hkGd:9$GD:Ըx[~h]Pޖelg]D)1jvoh݇#*%OC-*:d#U}?F{*rՌS͉ ޻ecQur.:>. r?K=OEydťe.ky~cҒQ앥[ie#g/^8%B㒧=퍤iy1!~XYNJ /;D"}Kj%1~C$g _M4]thM\&R:_}ڹɿ؜ɟǔkoy< t 5ʏz[B$i?Q본{j;;"f_^1U9862|:lNͺ/*eի1+W>!kKqn+}坃nf7^ʼn^f[""Eo^~ϗDt%uy_+}y߷f髻he̙8"-5oY\%o|*"t拯h~ռݼ\nAi[}"&t_Y0fy[Fd.ES~z+rttStbcq޶9N,2M8.?C K]V UX,̯^Kg+ZUo=ſ"1qYҊ8J+/xVתV|Eh >Hkύg&./(аՏG/XwUl^;(]TR5~bZWk%"<x݊WU{"_ kLeߎo:w&1" yhnk׮]O/L E@ɸY+~7/As^exj?p"q *HiV)ȸXO Bx塖|P}ODsD$T {9x,WO3SID=Dr_ZP>|%"$L"o%3 GH~ݫtx8/ Ix.O uH5Fh "b#O""o[ia9q,{n.xcBaRtC @+Fsb$y=Ϸь1@EpPfmi-<?re􂗣WqMd+jx;ܱ9ԝ:"Dv~ǻXt\Z# T8$vH?\$yԉt7;0GH{"11TA_U$"TDNpc-bb(GD﷒ċ"rS[yKkX"3n/Fџ٩/ْl4kqRy̹$IDATq8W^$Fin̙?|и3-h4򊋧G?X{l|CN]rfZ&M]/Fiw" ?{^^}!t}.$ڧv,n"⢟z>QW@ZQ[X9oܹsΝ$I'N+?$IF5+v\W1WDQ707мc>޾}PepUNJ;KG\mtr͊]֫{7u,- -.run{E@D@D@D@D@D@Dzp c@${NDF~e_ ^T(~,񆖳xX@-mDvWo9;y(@pG?ݶ#{g;>:aY3zO\9Y>;Cz)c|Lqh{# RϚBU˳Bzˎ>>qFȣ,&jk>I#CvK0~OEo(v,LFD.󧏗 g]=cǩV{Qw/-N[8%ewo ٽPL!H?zIuꀁT/Rʔ &Gʈ: ̙fYdD;{}jl!秊V7~Ch(<>"Nס=}DmۺV3ES/*+ճ&޿wkjE7K%4.N~cqoY3,!GtJ'oB`99tl3se}-;;}sO?Fjرmwzj`s"-wVMcp (#LgiԨ|O;\~I~oܹsΝ$I'Nz \V#%y[INt7|"scaF,r,=Dp]9Gfn?E " " " " " }L:" " ZCsaagEM "\5N{W*jl[?k=!/6O ޶>:M$ 2?͖-( hNW}&sn_wۭG"bCzGsS&F.?Y_怎OoԶ{{gMza[Eū^pdwlkdG,[z(ZF}dۻKm.Y[-$ukeeū^ ͖&B- -7EDĪX;aʏoc;7m$=:P}dAJX3Uh4/q%x}_,rZ*u9ZT;XZv|z^rIe5${~|VpהTh3m-wr62>vv`Kyco)o{ef)GRr$a]*SnHN:酾gw|ɔYֵ/2<:;TB;wd@"cZם5eR҉ InmG2%VthWlɎO 0s1hOen:5x$ڻ;X7 Z m(On) ˁuXΉ<X{+`      0 `cJ| ?ҡÞ{p^nʁ=wNcB S䍼yxOd+;êȐTѿ)I.d$/o]ykWZZ:9IR퟿xӕks>xMothKm׮ ?7* >?|]*iYH:ٌ%%_WbIK|* ~ӴvDYIB8$)˖o*]{DT$Ig%[e\3dC>?Y?ŧWE;Hd|Fz~bVƉt=Pɹ/]ddݒ7UudIRܦbIzdg?UUK] iGC+ks5#JʿnAR*nyPO[w-iO-:K+*%{i]Re\V;ddNw26u6=?E(r|>_( G[־KJJ(*** dF /4͒|!$%y^lƯ1S"Xkk,^vB`[E]" X/܆/2@"$2@"$2@"$2@"%0WǧVɰ|pMo|xjҔܺټ҉c*gGu~?[qv3cC`e5~=?:r6p,(cGue/]VSWgҔ$[O-Id,%V={B3{ރ}KѤ$ygn^GSnU>,LؒiU7*LߑV/Ywv_;ENe[j7HdrɹUkKalJRjrpp>VWn('#K7~),\gΏۉUOҔLok-ŧV=P[ҳq[ _CǦo~Y-wr62>vv`y#0^Cc)[Y%'g/LŒ 9ˎ?Y't }=$9NZdI2<׶ߴƪdVr{OF.a'ӒdOKfM[׾PвS&[Ϫnh^ ^tbcfہ[Mf~td[lY.זΞݕ7ub̔\Tj%H2)ɽV);snzYI5y@JGzO 6A0h!zsR– -^d1+|)FcVN*L_Cf/LؒSgRrթ%[f*تXDmIh̪ zo7l</LHR-h2 1$酤#-fLnn LzZ쫛zȂQV_&%7JE lǷ;= |'W& MjGOpwvW/rj/ EknY- B>r\.f2FXoc'cm=߮LoF"Wɾg0Z1>jZguH( A #-A DHdDHdDHdDHdDHdDngk*%*`] |>r\6d2, Xw Z$2@"$2@"$2@"$2@"$2@"$2@"y=w_ P]y+JjٞiO GF6 ;[|~զWOY>32]N;6;98[zz-{vW'O.: m;>qp:84:KK>55#''hsI| v֕IJL[MѾk3}=ZM-VǑdlmoؐ~Yd۸Jr&mL'ۺkzL5ZD4%^:rՅXpuo#+ڵV|jn|-)g'}ӱS))>qN]}KdV/&m~;4S7Ύ/=%ò\L9io+=a%':1oˎM̦e|O&eZk*Z-ƦolȂT=k0,\/%[0O۾=XY5Tj5ޝ[4/~>Pv \X飼m_2w<[M%mRVGjҬi\N,ٴ< ^Sѕ{^=jrRUa=E-%WS\ײ|mbmR:i۩dJR<}UGnIΏOʷݯS'Od;ue 'tɨih]aKoMLg47n6e]svu4=e~ ӪpKrR5咓'%Ǻ\xJզJZrYn]Ҿ֮qcqkOdsxh [ Z8 g^.˨jkq/^V/ MI7PxR$,_'3}}e TМo]~ԥ9ɨ޺`%SS} )`>wu$Uhm_Ey#g,Yp}jZ|ڵw9#2r\6d2 X_jD~ 5h; )2@"$2@"$2@"$2@"$2@"$2@"3^cg. B>r\.f2E;-    X_BLw >QIENDB`././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/doc/source/user/figures/logs.png0000664000175000017500000023711000000000000021267 0ustar00zuulzuul00000000000000PNG  IHDR?3sBITOtEXtSoftwareShutterc IDATxyX?d [@ *Eh[E*RobAη@NV-غ+[*uᴊUԊREA!T Ad D"u&ޓ;JB!B"@!Bs3B!BB!܌B!fB!'ee :-jhm1hV10mCnҫLͯFNS)$:B!z{:흺۟|-=0fZG7Z!TC,zgݧբ*XX,Bnoz|r <``x/1t'sRcs3B!fhԳ 4B\r\.#8<r9p8 Ap8\h4{a3V@9}i#BanH;#X`I_[GaY}ezWfB!- VA4tvR^ a ,qޅe p X%1JyD*VB/T4)I), r3A, q8A_r: $gڵk9s&EQظ~X|axǎiRܹrϟ ?ѣGg͚UVVVXXHK/k'1?}ٕ駫V4{Jկ__ԩS233qƎ;"## hmm8p[󝜜 Zsb/2`;R[U%qɁ@gX\)j%!`)#,_mP5p:n*#}}5u ͽ%7k{K[^s3S7=Qnj߱^<t?5)lnU!B3#g9oozX#?iً'WNIay+BdIO|SeglοVEJAYVENp$wU4agAp87*k/߹wXXQִ-BVx؛ƾ} ""o-((xb@@CRM:uȐ!7n:p17>}Z,Zի{=~ix<۷O?tС!Ch*5K_Nj`MfjoCZ&\6ryoqcegZ~~~6;>/+]U`Mϭ=v,`E;$M~ݻ|91j#cH)Z^9e-شjU3[A-ONzF{-?&i-k1f! ۰+zМ<+":3OF1͉K"|s'rWkk^9  fh²tpye'x}0v؂L!CX4h}}}\.d"7c|>իH$}eYvnnnZ քaq!8Xߞƺ:ө*-lc#N/YN4gGzR63:RK[('~mWU|7vڔ\)z ;$;%eGEG@{*m~l#Vx-]*eg OI\] W1 xZ* (a^P8N%9HL1U33 %>aSd{v(M)9%4)&:ԝ6%f0+8LܛS8jB夭Q/Y"iU'U(%I$tAƦ5[bCkJ1([ܽxq~RYZOJR2ri%pI슠wʒ.)7?<."HG/) rr`ʲ7O[{bb ⦧Q3G+(vRU֭UYT|^Im3؄>];1mw #WK9+Ze; Qw$gz]GJhQxZ|@GƮ0)k3D,ߔeL&{U[{!S~Z02?V$"bLَ]=k }p?r 3k`-C@ͫbjstIO֚S_¸It {8<'HM0|R(Kq:G)~0(-g\B|(gųUҰPc;,0̍.8)gZXҵ#/8~Ha!גfl.)Ka3*YhByXkHPiً >R1@+iD <(Xh.e$P.^UҠ[. i*n܋fftSlHЌ_sQdɌ3,Ǩ_[&FPW9{)*B'ċ#_|i| * T(#HP/%J<fK()T@!M˨ _%=ŪOmmǐ)afRRJ\gfH;>=LEBѵ*` .M]>qct֛Mq8DvCIڛ[Uƒ`` 0`-: x;q=F3ĉmbY֢{ _  R\.LKnTڐs:|QlS^ $cCMWjovBIȈxɒ@ଢII[^Rm3}q84m¶/ օV5kIi{`l.߯BR`hZNPLݯ Q5^a3% fJRVk9k̔U4xAj֪۾1a _ضDJ6*ڷ VЌ[_9Rr /kAb*,y"VPi7$!|fxnIFLw<]EAZ(Sީ^6eSȮinv!I)/Rގ@$7]U@mhMZҫ-^l(X'5[="g0SXmVk)s'jihڎ չLIJNљQ*e$NwGMgņuj́*chΌ zKn>t7{E\ܭ ,>,J;.C[ύv4qcNH#s_/j޺I{o=Y`՛֍eL+y,ۘ޲o\)%?$mǚO=Ae(FA*-8P+󙩢Sb!n46hS%E(} T~UcJ*KR&rC/kiĔSC$@|mym:f:-J(-U@_.HJ|SuhTG(MCI)ے-!=|Ǐ<Ȅ0 !/gko4/YW:Jb^%uSu3Оf%dh9)`v>vG fSaKRvLٲ#+ٯq.Q5;h ͐!ޜSv$rR͕cϊ thGN1iߋ1,kӪi $kX8Fc+{^ 'ԍ;`UqrwU99e 9W ))afSd$(OFG]*IT`=M?b[IFZ> @z)s(ĸe He9:y,d4U;W]p ~^V/Xc?B+lw[KG{Khhw\b!9=C3B-xʕy[Y\sJF8[eǭ:]nh.Wcl5x^BA8,:p )۔$vut򴔓X́l9,hhp֟T'S/@'Qf>?29&nwFQr9͸ SY|8"3D Y8.O-!O$rĔPIiIH|fFϗ@蚈)虡n's{>2ݶfؔ|sÒ2guߧ1wB(GeYNG} '1W/1x{׃``4 2licn%'9y%+.Օڵ $Rtqq?>7>/P$jkk릦& """޽{7onhh&Mzjjj6n8tphiiIMMtej2__pz@-կ] }:,\Vɾ7~!7Ҍ-̵;q},3oNo^Yo)JG&V]P_{Mե+%_9Lٕe#7 {t.X۵|p,ykj4v04`` G mLkvoczljJO>-HBCCí~k׮PЯ_?vxAAAVcu˼D-DZ9.>AVX :mٸ)t:IdAӪһY|9# p5bP]Bs3z&빕w{f9W fг@:5̍S.L2ey9rnZ@PH$rrvjjᘚ9_EM z`Am$mkV-AC3BtMBIV]ܰǸԏ=SZZрN4ADoAB!_67 kl%lcfY10G"B!y]嗼 PXi(ҒkmApy |>مutF!B 5 HLl8+!'ҒC@p'B!܌B! !B!!BanF!Bs3B!BB!zfn޼B!Pѱwڟn|`0 z^666KXD!ꍹY `nFO17z^<B=c?3@,rz}<B!s376?!.w|m0==#B=?``Y !B77cXAO119ցB C3zjڃAߌB!!BanF!Bs3B!Bs3B!BQ/v>!BB!܌B!fB!07#B!07#B!!B?~R@.^j B!!I5h4X^`Y`0z^0F5jcf#B跳1:c? jX!q3B! !B s3B!BB!܌B!fB!07#B!!B!!BanF!Bu"@3f,9ČK;?\S;$?we2D>qo|Ѷ}&O=}_2эY'.id#/Zh*?9ax7[[_ٹko[-}jow%ZS󕤳 IDAT>Y5uյ%?Yg̶wEV!kjoʝr?>יz܌P_:Cg.?N}Ԍ=D5}͙Y7ƿh+^s#[& '~hq7r-ۘDs+'1ֽ{zMaSzXn<'Nb^Qlusbsx؃Т_;x0=&be 3BY{m;  1QgpgZ<|ǪǛ>ڨXߦB͋HŒ3 l.5l{R`%M$̚KJ21fk*G77z誒E1qs@]/9y@J\@^^yE)*#M͚ʣȁ͉MX7#Z9gy2BIJ ?g&$J),.jk*-ɦ?{Rs,ur\Jƪ 4ѫ7 |8Mt~ Ӌ2F*ܮNY[sVBjU% @[& @sc_B 4%3RE2_uEr:{-'9Y\ 4#[~f_bWжh }*G)aбnLZ5j+`d#GJ6.k7 h+cߗiQK\. )ƀH$U;U Dp M^APfb3r7E p7=={93l(Y}(iKw ?p@#00VmWM 02ŸޢaQG0˥&x u/(-.-&ܖ;3|ݒ9+ខtX:z[ J.:L g>Ս7I ټԾ5q[ώ۬2v&}m<gE؈ VFPk1^yAeAӯa_ Vb;ah9pu?hX \sm" RW[B,rTzMyxޞbJpI5VmMaiS2 7bB%@:Eu#&yzZ*m<*4'HrPL8fsiMxO'bCv^;̣7a9#{gmXz1՛Ϩc冡Gҷ/&֝۞yy;{񯸚cr2cmC}W.raR@9QpfHJҖv)dLRZI3S[CYSybcK vBt% AJ$(i $fivBblݺ/a^fH յ)%H4-k*iʕɊN4.#c2kͯrR"ϮqЄUIVtzdBaB[SN4x;uaxk&meEgR6o*MqҔPaEJ$1 E$C4i~jyzs~R]S & *Lhumn 8-$I!\0oЫV.8Xpx!TڸUzێ;L\Be,x&}˻S!?|zըuƓhU%fo]JINNN^MIs$m!\b/Ъ 2݆ &J-\OZ3wu^݆3Y$ܿL^:c촬iڶ]$Nґ+SVz  ^4MV$[7풵r؞C{yf׍[g$d9i \=:vdT2xʨIe뤛L$G~uYFZж1%Sr7vQ첄׳(i㤗vIxі@fn&_&?ukZZ뇧fj7OZ\`jS]z:Y#0_9q_7MVyU\PoRΓ\ݍǴU| 1N]֤uр=D=]k͗M^Ix.wj!7Fh4Goi<u u-?XCrA2CT1t:l'lzXujq҅&o9AYgE/P~sҌ׼=]eYz=0jԨQ>{.kbYdp76L(:E,SBG- X|d퇊a'>peɨ˃X AC{zO89tR C3Bgc?4%;}O 8m=tvn@Ks}\[={DQQcg@E%wBA!Lz~QKYYg-|y+=4B!V? sGB!3-,qB!,` B!!B!!BanF!Bs3B0B!P=|!B!~$IbnFe\.W `3B!ñx8BztrX!6B!BB!܌B!fB!z ?B!P_R@O˲Aza4MUU B!z'직B!fB!07#B!!B!!BanF!Bs3B!BB!BB!܌B!fB!B!odշS>h4L~Llmm]\\\.fB?<{\Z 5Ɵ|}}߿|~FGGGG'{_jyi˷DGm-{B=/^QgI΍=$zyxȺ|,X0=@=Bge=gEںU  P|l;TT{SjϰGU:w؂ ѬCU |D[ۋ-_Z?rT9}̑խzI4cK:UQvvy ޘoIOvIys)9u!BQެ>sQ #dТs b=xĄ!}ʋϨ;L'~ib0Ca99]]W">q ԝ9U]W,qO6o.}m[f6X>[wq{Iw&:iT|9Vu X:9:fD< >X[t<RD"K VKorؖǹZw3 5 5r'cߊ5Y{!Bhnn)?] aEƿ=sLysC,D*uZ |Zew#yc&L *FP]qJ%mЕ>Yuč!θB!3u 4jqCO_o2:$*%cnޮ<= 59e*FgB=ǹ.u<}31כ!`$OY'Â16C<:<~ !> ,B^{}A/["r {/Vuw*8a6Dӿq}약f4ʣk޴ > =%, ^h4UUUFzܿ.VdًcC\(x᯾=WxfH[סa xzҤ9 TCw =C(WޙkZ: ;TgV60`Z!}Bw)?<xzņ-p[ύnMsPbC6H`Nm؞rs$#8pO^᭪úyF=jVC߉]T,)VNX[y"0w{aCӻa*\5B{teaG۟(mٲOt|J' `W_޶f't5rʩG3=iPQzn8>>DfB55Y?d[Qv#q_+>f?CŌG:47}f貽zf s6TJWYCͭfK fT .X_0 @z ٖdjJrJʜE ?~^b30 ?q;ka@p9wS5@& O0NfviԗOm*`% LzƊ/W. ɬ(k*}l`p죍B(m:ݔ˵uqZloF?NIar?9cfݥ۳sHLLιs 4~~.ꐨFM5vp}'~vw_50t禦1];`;+̹C!Q\^+TleL¶Lr$^oC`U'15K7^-y{]W ?Es>ׁ{>$LA+zpnf1? j '|m˾ [׎MO@܌DN"P75;lZ5|KF1.viȬFX[0f<ɸtuz58{sbQC(=|N3>!A}?_ׁPR]VmtIxh F9nz_na'/> zzCq3t`̳Z}l8 m8fm,Ջ17jFr̬Ĩѿ4)\Kc3mwu0sjW6hri[֭´&dz` l=lM 5 L[C5B\'jP7@sF2]ڭ ڢj^jGS^3w>&q]nF߮&87-}ǣ?W ݬ?[&ӿƟ|U)m:*p4vz܌t7Ӕu c̀Vk}hz%qMiDTDd&" MƟg7UQit}~|K4j}޿JG=.@fۚVmrŷfa=83m,o/^.8ޫv"f8U>yQ\г\h}!f\YSgp[%mB*@`K`hhmP5 !uoN'9@Jۖ:EM`j*7]@WQwJM+ijK>E!07# Ʌۼ24ia5)fMrsO!܌nTp[1;jf'W]̫#?0ts_}`uޗWaCB!ߍN'1uVzΉ9xP.mΚkԔ{5>I\G]rG{MSo3OB? ^Bjr?O55]SqJ̑7F Nk7vj)r7 C/"gs6up6~#7br&2@N8֬`_[i(=so勝4B?/7X /Օ4=1.$KHX[@_J#m0wolʬY|ҜO\[ah죁BA.óy^~e,,T,k0z^gFTUU!)[C|C}{Ntw+\0co3<Шœ^fx>WGc23iIYP9NPw\wr0im~/zs{˪0]E"w1i=.m[:Qz؍Pp=Θ+W,O7Ӓ5!"""rs^Myz7snpdYWsx1q~1ߋch&""'&.p~~涝Ʀ֞݀eں$.bb閞D;T7G,:{ch&"""Ӹ=S;6vj+W$}[)w=6A׼~rܴL75Mza4#uޙǜ{ qk׮544444/^#Hoa]ق'ӏhu^ DDDDU6=8 zq=FLX"""""f"""""f"""""f"""""f"""""f"""""f"""""f"""""bn&""""bn&""""bn&""""zDb`n4$"wՂ \vx) 333'4_|YTZXX{'Vat_V]]mmm-xr\Tj4Y}}}ΝY? AXbn&"""mp11(vbn&""""bn&""""z4Xjm0#(ٶ|ʬ_ļ=?ml.M'w^aKDDDDDnV9iCMADփB_˽-x)3P}Gh:24ѣjX>{~3Av"PgZQcٙ >Aӂ:zGSno6՗e^#ܕ"/HK=xV eqe~K؂߬ = U8{7,z_HBj SP{E5zҡPȨ2DDDD=o6O@ߡϳ={ؔE"y5}&D[]Vm|Ѻu[6Rn~54. n87Ϝ : jf8BnMe"0^*)Q{;S=̽ KADDDͩ,gyvӤœzǞ㊥] ZZc-Y|Hޘ*s/ihziMkjiƖYXrmusYKL&xצ:ͩCcB8WԸ\7"Y5^LDDD͍szXzjrJ{n εp71Y4/m)2\~aԱH28<)^]*Bi!RfbTgl[\؋J礰Aغ!""'ѽD Fb]*ɹo؞U UxMi-ۿNK?ھ|>x #|¾M[=&nۥH9YM5v- *\*\A#vݷ+lwtp5];A/\Ǡx)/.5^=^mgx<HWVyTPQ|u͓%/zMyee+1þQ |׼-q1{k]l-4_{oPWb9nH1X+2W8i;ݦvٶ2hiu^jZi3 ߥfx)7"ţ3٩WƝ o17=Y۪PȔ] ɯS4y@0up n*+jTvSE qv8J. fx?ɋ q{wrң5(lknfYU1TԊ0cZ L`a1}t"/B2PzՊlm]8vmUB^^:X?=?^jw)\5,^û=6^4IJk{iQƺi եC^{=<?WՎךHq3(JWk݅~{0?|dq]oxZ@eIXrMğO'[n*+)4"_C{ 0{Wm-~KZr7SnoMS ѽ"D%seINbpcPE%%s{N{FӚ+XT3622Pq)+9)1KMk64Y$k3qK̊~gFͳ}mp_-:!yJ @4jj(θs,Ug/i5ZKȫZHOl?QY~Ԉ .MKK'-~s%\)#p|駢J/"D])΍hw߽O3?j4Z8 ݹi IߩF@>KYkiE a~ԌgZ<;<%?Ù"c>mpoP]ήvF3!=7i;=k+vU^5ꎊ>Ze|jRJ^"'okHD.t=Wj?:PQ.\xPC;#h럏"jvY׆z~v^):!D:VryC䀝oV۴=Iba9Cg"". n(ږϫ,[0뼤5G0n>a9e˖-{'EY!",ש~dzfԚAolٲwk~ 3y֢r.ږ]蚤Sc-[,6DtlMOA~uUur {sg{Ed܎ȷ-۷2#Ue_Hc5E߉/NMUn`(ھ2fP{˖-{U+~1&;y>e\<뙢"27wyv[oG. %)L}QwJ5A ]z ɥ_\AtD8YHf1vP 5KQ1f# k? PXKzAg4r~ pSi6ObZ@S{"2Ncu{!(zpQ۳3ʫJQ@cmSSCnjTKyl윲qqZy ʯKwX(AsTDiC'; {եFG6xxJvcR;Sp۶s˛tץUm*U5`+6E:j}OOcbJg,46磆KYyu;8Ȳ4KU#6nn(>/5ήF0[:C32eQ^ō;zР:;9/Tڲj sKQ.bUv)4HQzGucl.6?i1D). gK(.r{95u7ڹpRu>Яy0kMPS%Х3ר a u:e/u 33a"05T&lӍ9M;)=RC5ޠC7&`5Sˇqj^>7ayӏB\jCvjHpӥ&}.X׺;Xe.'i'Es%RS4T!kS*Xּv_mP_u55 3Y㹏S/9ɫ zddCVkԫj!S4S\86vjEzQ. }cJ`AS:;K~*Aa1wiꤰh0~1u{ƴ\͙N[g,Nۛ`0ACQsEYN2ŰgwʚK5FXW-[6"c :+ŭ_3@?{Gæve;E MS ou։-gYͻ7-Llio.:?MLhsTtzF?_JZl.znUkh3R\K7&`*=`ޤkS 6몾s[}zzW9] VFQkM=`v?64}ߥ7 v_50Sz]SD>]}pҬ2Cx&Wj ]-&U״W hnpd̩>'woj_v=RhԛY0quWp:ܛwQ4ڴSn8FtsgΥqgoz\2g:n\%kÊ$o:*fTʽ»,}oFQ\u5u@g0hD75N[gni.:{ߖvȺzԻbykWʂRm5|XTWglxRzp|3xG? SۛT\ө6Aj"vW\4@MY]zR'N>f.ެz±T~(*?Z} ߭E^F@`k۹Wff:y掫ʹRb?zºY1=LUYs*+O=ryWE. WNR9tjG7LJ`,9WVSD`^xXS.Xԡ~s5Do8Ve\{:ov=ZI٩W4 v.>MܴNpVt P4(PSg̋iӚkP??zsw{S:mG5\Un͊}SbA.84^8U=ZdpQطD[K'S]E.,rܖZuW=߷Y{Gִ}K˾dp(ڿ i IDATLVU꼴] ƬDvɹlLE}%Nq7̬M{tywbj6W_mPp|"DLtyruˆ[J~v]k0>;oѳ>?ipN8pMH]xtP=SSwg_TU]eh׿ Q~\vYr41 \yrٽ yKܪ|v0h *_wg#<4ԜƺO:iߚqooĊ!BAzp=ZqD[{qi171717=*DgϜbCe衸~kA/^dYqDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDOK@ϳD$X?nݺ?=={dn?MDDO\.Yqs3s3s3s3s3s3s3171717= {P{gDr;FxT`/nwJMykMlܿ#|(*;:[,;w&"""=7 = ?&9UZ̡ݖxu7mޮ_~ڵ,^xb@@+CDDDb;/=&̭ךP<7cj٦V<&Lǜ~?8 zq=FLX"""""f"""""f"""""f"""""f"""""f?}A__ߐ"ւIwoo>tKFdСveB'|\|D' 0$t{wX?Fr)m)2+/ܕ?g Ǯ:SB}}gղDDD3fkN+ 7.Wh"3mr}DDDwD [`X\``P̅إv&arW"U ""jq$ \t[@\TڂԄ1}}}}}}CfĮ?V 9rl}Tؘ7 $ -8&42.%y0___߰[ B|}}FB,jo1한QaMK m\cQg,I-ߴ+y)q76!0tΒ+|mAjܜe Jlr%%wrR9E}}}FMҴasl8v&D6/042!Hω m3bD_vp}-DDDDH5}FoZ啃×n( #W积X0yΆT"45~#GXalZvf9`XPPP8s[̐26@aJFMs,da>r@*uNȍ:7G ,Ip'}$]Kڠ3e9`XPP8gWɑ7VټsffÜԅI f6N{>24i _y6'.l򢤌|ϰ-x m,z n\:yN*aڃKcՐl4Yz#.̈8*`n8Xfwc֢r@8S %BaRU[c}0=H>!|:c"6'o,Oߘ#mL fQ1W- _+sԀn^~w&4.,4 6dAzyK^aQvrr<)*(C=?y 7ӌ9Ǧn=#on]X+c(l'yN_5ʫ%J [+!uu%""ͰxJg%tP:S|bbBnܡN5{xPҕlG- gWN{ݚW1-]Hl[@ܘya8%լEeޭ;u=giR@O n;ԍ`B@8ՅVb$@N۴+ js֧夘n7hf'd rRԍ Z,^PV$ 1KVZ!ODDDG"Am^z1}vK{HuNa( m½drn/`۷ <>Ī%z){är[9Ӗo]\쭼)In2@+"RH=wJ)&Pv, qn3̶%R@'v _%u9\)Ӷodʋ^7Jgy˖IEZrgmm}^%A Ӌ-M8pذ>vc17ج)+g% 32m_\r; "DrKtyr@{zrFuzJX7)NӃ=;jP߲ݿnh4VT-BcnD|Vq[[zC9)fIgv8s3(OT[@|z q+-O-Ȥצל[UmD-u nqEyFJaT`J1;,_&HA9iXG+>6F5kA(Hۜ2yLdܘ8}Y^ffzjJjFaEaj>ㆈo'`bJ9ޡvt guq̺2-7˝m%4|Bfg;Bmcoa>6K^Jb`n.q%VQ0Ж4Z_SaC6M3\u""z0D-6چ堇ygb.R_7Ry),L, mT[qjϩK/{ݸXO_t0_\S2r3ˣܜ[P~0GmXkfx~4?#]N13=$UzTX )e:?!R@Oɶ !?PӺ+,'@a^v@Px>7*xU֗|BCZɐڮH*זv8уƌƌz>rC[t,uuӜE$,OOfgڜ xq<%>%N(K_$>OA)-omXȂ"FW'u{Dž{{Ǎinɕ wK}7!shdur١wů]ٜ&$ ;{ANNyJr,aBSA_z8FtDDr3K@O!+FAZy<'"we8.s]o pVB(,T%AmK8M TXNFz >}v E_OH:QӰfq661tV R2r2@t1;t HDDJn8 Ǣ/z^I1vixG͕񅒠U|X)""zHx?  VN[%c{SR}X[O[V z8Xj8(hf4D:>CB90(%>10?P /{"$S<}z+Qx&zGą26s36/3#G Oofuϊgz 9&" $""""bn&""""bn&""""bn&""""bn&""""z|>t>q\NT9.ﴥϭqɑ?j8]+o1ׯ_ԩ"""bnFΟpW}?"P=dSݸ_~\o}Ac%[o쒩ҕJ\;waYެqɑTLo2Đ(vw&!FU (%O9IM8=UjЊ'mZ3{Jw4 ٜ/^7A1e7Pua_Ԝ}g]ǽt}ݴ#Wg;/(+md,c?oJDDDIlꮄ9䗺\I;͵ }׼c,kC W=uT55uk_GIu+_pٖ(U ;-v.xMOAI8Y'U׳z~_bv;p^*]RJϑNR7H7NI-V4.+/==_{֐ƬÆϋdA|rUU^CDDDD*vڵ[n%\]Яe{@j%L]{v]0-eS*?ݮ՘p#ͤ|MVtfRR/{< HKK&6Dg@G>fTrҿN6=MtA_wX:I^I^9t$=Cz(pU]&MtiM>(*^*SW^wK:5N@q%5%QRClێ|HLSFDּL ]2L&K\o>2qxH;5_"zF~Ҧ Ai.SZx:%]K…#Z[8ݺq\5Tڭ[˗/_|ޞ'꯯H;#1`>L/+ e +)&VwQ?6p%!NDDDt{w q.d>q[}Xj1oLrz5W|>h[[׊)u@ʅ%A^z3&-n.brf;ʀ&: /i%YΑ:9(B4͌4K;ό7vk9zHUVS|Y1%N Z"""z"t*އϲsOrrrˢѯtۿ4NoqqǺ}9>74kFf=՟;6;tT pʩM}N>:燇ГL_oUblFD>bfTDWyKeW (9%~Վ%A2ozrĖuGT:@8"%wzܖ뎔 s8*I @jɋxlۗx1S[W8rD'@7!2f1,x~Q7ڄ}yA璗VU^+O[t] ˈȨ)(#7WU!}7+G"۴[.@_ji_xsȔoyyZbbM ]=ڬ/}Mo ;Bx#!^;M?b`Dq\'OS17=ftwxIžU 4MTP-^U1"p͎E^W~UAPKg,NN6lK*|?]MIDrLzbw'{ Gſ.qܴ9D~-kW ;d+}6Se[ k\$QDKuDa!9s W'l]D`3$#M IDATȎBs$$^Gx?7Wibl,@DD?,n(GƭGL?mHSG*(ߟF{ȁ91ڲ䅮Rύ؁6Rn]MKF6KERg;s_ 8I*9wNsظmuY/:]cYr/)6J],pmYIԩ=˛תuDXSmiJ) u ]hV3[a*ekd#ޟ$lČ?+Hy>8gULx?tѣ:o| %V'lͮ s<3&fLFO,yY>V=KWWeUfi|91MR(j %Ưړ[!@8b)M*\injšC}ϱ'A ѣ:ꅹq;ijil9H[:,z_߯nY[vH\8MWy_\s @Ҫw_"AIV?DԹi%#+)ng%d+M_~8P СC| =tСC-#dC>=t7oI[7T-^i.k)&LdoĞ;Px%/7}8Eز>G2qM|.zюjn绿;ebEwێwrZ|hlɆ'}i%7USuQB͜7m1=$wWϢ=7+z//7m<&XVӷTx]O)KZ_DD_=:RF8Q/,d t͂BպdȘ~ٺ(z3 so;j4I]NHC#?!^rԺUc Vf䜨?|F {-JSӋS0tc狇ExqC=-z,wD i')To/ݡ (XםVLp'[[' {H-*ؘ!Q~M>Ҳg+`G]S-Gts&z8zGFDtU{t#/jk5YqzKvKp 9R v 6iJ)‘-'>-)9\+@Vtr8eϢ>j;#=lm]'.{efW'SlOb:"8*9Q#%W{ yhG;EڲGc33jJ'[בs"KQq=QtBk',Q跽mȘ)r52&kis;Y GGgd#>پń662Dr#َ ?WwEŋ[v4ZYU'l湮mB{s>j2N:1ܵyY.1 R|9QQ^va%xWi‡_4%[W-JXdQNϡrkO^t襀edo᷏*w @=~v4M 轍@|}uS&w?8_9/%3A< 1nA=f.bZ'H=^GT#* |h- S`B6UxDMhY85FM [kWXub-Fz-+&M2f{Gk''4B16 X7slo6A*6lIn8Ȣ |M RI[ct[hN1@WS LD7 2epGFKR!E\KݑhS|nH-4;aZf\qe'wD0O"piB y('?#Zo &[pIUl[s:Ϛㆈ`*zSwf IoMX@ !eH 9R45X.@qfO?>?= zꩧwgg?z?O>QU>\,<裑:Wx"1_P4X](<I"hu)7Sq&gbLF8)LG&0*\|_f=X@jz @(*` u83l^8ARJ*¨Tlx~wx^rFq@r @ד%%J͘dDs::WrT:|3FYnD ̴RѹpxҾ57ghlΝL m%#·h&CD-j%Q $bS!miOݜ0VOx#jϱ)͹ .a.EdN'Q_y8FȍttW0<KΌKfby;I&ؔW 7v~kb1.98q7r^/qe''|@d&޼st4ᙕ|Ɉ^U1lSD@ 8͇fc̓|;A_xK/젺|=,?r ;m@ ){?O?fcǥ<Ψ!0I|-fp/hD3MW &xZTI*W(Q! _o.),'*RfT ց@ g@ @@ @@ @@ ,իKU囓^ucᕩuYWvQC y۬BE͔jFPe]ЯgtMT1<-i)l5Zbk׳ 6]e+qD<]d @,4WfG+_ofY.Wi m D&h.;/J;Ojƒu:J<]k2'OtrklE[wmtlh2Ё*D{dHD40ut29VF$Aİ)dY F-1X HzȪH9n#Mȭl ֘90bðƫ3|IX)L**P6OKSHL [N-( 2g/UUIg.|tso~=Aypky15;`|ߡhQ5j \%o@\P%Jͨ<>AoxFW&'_ /i1Yhm @^7cm1)ҩCCi5p4Li>mX2࠙XXrP:9zӢ92֙G߫+1rMh_~4|#QpQ66s@zR <0P0_*Qd#zqD9+N-_1l H|BғrzիSW_ymq4owV]j:͝xwآ޷}ݽu$.J,;,OXF|Ay;{{C0" {󷝏o5?9/}J[b 11J2AB{5ɸ& 2,J3/"\" c;oIʒ dDhۧ'" N<c /S\0;o]&o9n`r [9.ކo>Vw$ ,͘N捛X'$@E?a 6PYA:] PMd[i ^dnVdHoZV W[2 LZ}ȗ/ťqP+[I`)B!a0"33Q6ZՐӤ"6q)1q4cF: paf 0yxџi@k[\:(x;88t X1`G6\K 0eXJ+̡ 6 \!on6k{\ L#2fpM+x+d=u]ވgRӠT.; z^-@UacBB0ێ)at3Z~qW>2PNZJGIg"Hny#jYUpE @{GM +A` J˪>?VH|~\z랷Y5r&vZE ^;fDvd`T,Hׯ.z({\.?Z4Qfę\y???x{?[إ?NÏ^~O/¯<>,%g3?\?5>_rO3gAg6|'_< j"_??uslcܟ!ˆˁ|>zי~ ʿ^p/?_{|X47ڎ062wj5X恴s-uTQԖHؼFC^ R%c@ j6I$ RxBdf5Y ]5&p3NH(žGE @PU N@hYy^&bY,ȋ*8AC;.zi16JOBE [=P.Fd3NOPP IDAT":Ā> |p8/bv+Iw? ~+܇?Gz 3OO?W~_m_~xv/+Ҝ+Lп+C^Fv [lwtDC:v[Ƞ@-5;vKd87k%Zl*t'_XfߊK2qk?Q}'!r)+ZʗfN40>v?=JscV&_7j~#hg~?I;gYO߹G}G3sNgߠY}h>\5ZB_}DnhN ?_?gg܇w:|ʝb׼ÕTˌNQ@  x}{?Om|t'?|}_t/+/L< ?^{5]{a~瓧+ϿcYb3\ ɭFU0n]BnqV-Q:]Uʥ6NM qx_`lWGݵAN*xn~a6[uoq@sHl-p~uy6\Q\NkgS=3i.Z`I}F&7g9%@*[=CabKwˡ^>D{dHD40ut2a9VF$Aq sh}լVRDF G̃q13&+b,R΀Hr+b~u{gq8cz #`Q$bK2u{m<e4E$`@YqoC11.6paN^Vjah %iag7 RYT&͈J'=fW;&Jqe^qjqY^藋 Υ%v %2WYAl. f6Ӥ0A9j,5Qټ96.$8 TZF}C AkSGJ}e68X$:4E+퍹gleK-I%;UI3.sA%\,NZMe҄@U2kA:rb=c󋫼p=:)8Z 7D"ȀQ([s-Ϫ Vaͭk-@/J's^4NbqsBZ ,{ [)"4AbK6-%n9V~&r-']xA͈'EU )ϗm`)R%Pl^xW^[(wE(0Kץ׭ԫo.g/UH&צk/J"I ?6uKӳۭfs.d Fk'#;[0õzŸU7A Ȅ'vdhguZ-kamml()%lN̩.hF nF % $$7^JU%HyYE tFrD6Ci?ꜯneIBu W_YKYydĎո`0ZϪ(/L,j!{^~3^C1,SSSwדc} i 5r8OLPYAV&Norm)¡VRդ4*f.Ŧ+-iBGЏ椙՝u^+͵Y?ޚfē)[p mǎh \;E,@Zf"*pPE'}P6JVZZ-c&R(4% ,Iz ?%L'7MgdГwހS(J+ӞڿBa=g(Ja K6j,(? yv{]m})'0L}d&PA/^( |& 5t+taI 5Uk`(L{8E^P #M S4dʭ1XUck:Y{xA͈o3J;vnEcJ-;r:qGCgGPzv-tdPe?|DkXJ嚝n̥ 2ew-6kv:liYЯolzYoE%ƈ^qşѾ9omfY 'cqzʭL\o6F4,񹹹}J;2^%: a) wOv'Ψ˕QF3fݬ3k$np{~8vNXY+4PXuU9r"iZZ(P5Fϰuu"5.Ӭfv0u֥DLƙغwaR3Y$ 3YpLp}d͐{M%|"*|z @(*` V =&d'Xq$ߕT h뫡QWJpNgGJTtmeq?7z~\ћ @fI㉹k,A=Ciscg|;:E3Z$N%i4ŵ x'hNF0ky< ku1`s2`j1h&#D zo)c ls*`q:d@74٩8ARi`t :@O?SUUE^~勇K$6Cw7T0{? @ oVsf(Z\f$q4⛉*DJT0MFȃ@ nF Ư<Ki @ H7#@ H7#@ H7#@ H7#+@izu| j/ԫ_-ԀKKJN7k7~兩ut:O"fJ5^R#vo(23D&i mCf6-AR1ݵc2Al"2t rqP+ӹYUZA[fI`Y=vKɣ/`y1vWZKd@=1|[cs/`ݺJ{k~~W8*KuzvFG͊kmk-pb:oH/WGݵIˍs61V7|栙88t-zih봕&knDv~ >"H7#_/VfOPXR)Drh90R.ÑAU{::T!$r`$H8R~5dY F-1XFc$"" 4!)6WwWZcz âƖ˙yrxc>$@,s_nM'xUl ZPT('d&P%Jz_4eYF%&c0s,-`B*6i.|Y\X|!_HGC@FXrct*7w-e`M& /2Sf7 8h&'hF:-c{+q4>^-]kB s\Pb `qaN^Vjah"i`5<0P0_*ʤf*Fh5GQGm%288n,/EWO dc(Í \08^sigrwJoĒDHD3fdnn0'nLjL5Z @0VO @p=Nn`;AB׳)6S%z"1 uG e Dm1s"tKĹ/`$c_LSRܝFMp=EdӐTI͙ o%2 E>&6pb9WJwj eF"Q'%^hע>b H` fJru0[==v|=,NRKT t(dHmto̥~uy6*G6ZޜKoWGsZQ6$ 7DBNbEf1G6uݮAFcHG{![6 5+5jA͈'EU )ϗm@Ւϵ[*=)^:u7qe(q'FBV|vjjj73ˊ$yBצk/J"I ?6uK;*j6'`lf׬Hַvq?IEwEklgΪo(7,;S@6J[z÷1\8Z<,NW]>B*U?o}goo'{<Ї`0-["{{;IQ:_:8=CMg?Qͽ۷w6#.8zŸ?^\̀ ~r%{no$vNY'3<Ʊuppp7i̚wpppppsFRh;(-.M/. hN#t^ds KXn6-óA=Fm)Ηm\o^^1-UrR)hBq۟GYZYэT0Qӆ NW ͈U.$ >Ndc@dkW_]I=o0QeQ>ERTYcW'@e'm@lu}{3c5."{4"}VlVۛɀE-G4o:55ufB6c X;7ͅ'ۯ s$0ڵ1i4FZ<J0fNO8nؔkmKnDŽAl^ZEPmI-n;iy47H.;':~¹`JgshufJmRŤjI1;tft{}rɃ,l6~Nqgi9it oj R =sOt:'tlek]Kl IDAT$rOu/`W;^95zf2tzeFYTko=iЛ^75,)a+i [`mNLKQy! ` DܓC R-KTk@oط$EIe0Iq6͵o F^Ղ %npy LMdJ]+xw\ėl*K2vA3, ,ESU:*' KlgB_OP@*Vf9l1'lIX[{ 5pfVU;\B TUUpx^%-,RU!~}*~Qֺk.sfi10CE @{g 9.~3bӕ4zUvk ` c43YCORк4<6aQevyQۅ"6%'F:zLST\Z\kX m'mj-¬ͧ"ElJ@9$ ͕@Yz,oQTGG0j60 Q,a-7:0%{6i1D2pI Q9#81$@쨺JsM"k j¡ZWwva249ݛ`$:_p^Q{F׀`0 Wb"$?ڽ9Uq]]~9&2w7霷n[JZγ; JN6C{3:Gq%g !%pw]ZZ.fΚI1`D8[cVN浛 D@~`~8ڱٛUI8g>bla1_h7vGZ}h m6(C뷧MZz7gGI%@ ݌@<^ dh;v@h|ߩ(bj@M(B]j0U>\`L]lXr{0ntۉ`6yj:$2P }N cF'uȍ5p>%r%vI;,zil>3% ^WBt! fķ we{BnFcJ-;r:qG#LiBGUGjT\ SvGi-^+bSfS&85FX7V4^k̠+5p '򭵭|l6[DxB85JscV&_7j~#h)\ؾ@]E_/fLYHxQE0ssscqN? :y M^R/G+^]5d9~.IS +͞ ʭ>(5{|vVwZvr3N y jJO{P3vmWs[cL0i0\*Ыrl徉I׺Z3d#(6;N^̬d θ)>n)]W1Ȓiٵ`Y~Ny@!vj:uG]N7 lz֥ngS50N3.;NZy^ Lݜ/}A|CAfķ.:Wx"1_P4X](<I"hu)7Sq&gLF8)LG&0*\|_b͐{M%|"*CePHEwU(=UYy$e # _( r\|WR-F"fC7J.8]J/K͍ޓZ_@|6OI*Xq۫Y9t AP6׵)ߔσ7%V+ uTbԊp#F:+e%gN&).:ϩpVqG$V'殱I2v Nis:vH)<˴7)bnH͂Oo=&WG;o9Llϒn:FY{t4ΝL m%#np}sُQ׃wsKޭs:bv4/itŹȵ^ʤ<фZ#I2̸6|F B:?&< /۟G8g}駟~駪*w}饗PI"JuzX^1yYJ}m6.1_^M`y{?{ƕ{?;i2q;m8: 4 ^`+ž]aeUcJ^A-V!#_47F5HV yV&D}e[Io' QU{a9!YIw}WqwgQ&4zNH70 ?rf=4pi[0۷~vRzqJ[sx*Z@p{+Qvݟͳɠ*yY5X@w`o&vµ$R쬨wJjjli9/K|P]Ul|*^;wUn$]~sǾgw]93_|;f7Kw#:ۋW'KK7fQ o*oLn7t6CCX1?ޘr^&7@|#t3|R!:'+@=gk嗪]o5='~XR[66>WycU ==ͳׯ\ޫ<pa&{99[=&yEܵ5$Bѿp.`fgoЛ[=up'27#r'il- C^Xfs jª5n)`+Qn.`XW^Ru:[U>tW-Ћٹ]?>9 @yCrʥKµOso_syo{/O-Son`FeaPXPs-/4B$\}r @ ~ջ9صob43073}=q1AߘY%o]OR7woVnS7^K/ z}tVD@ ͫ)]OeF5f܋FиjkLpƣO쨺~=ܾuosKB~q\bfӏ s3@.\d j!tR{NOQD6mMJN.p\{@|ֹ'_3l_]맑AǗp z|x#u&ea%yqtcߍӎEwzHWCaFO e8H/^KB`7O#2kZ:kr ,єH^. ؃#QVnkD9%RنQZs"!G4o %6nZ-M659~*sJ\..D%iX0n+|p ": ^) r!7胭_nX" 0|qyǛG:&8HWo[%Qؐ}>pRӢk>5VZmz*&b< KFX#b7BcQŨz{B+N_8Re(na#{fm8q|Vc: pܳd p ;zKWGYh9&,D;`x zk&8s))d[tbA)gmhLbC.Xf#U&XS1ʆ\NO8bTIe J&d5Jmrx}iuir\-_߈é<]uqdB\x|BgyXu69X@'\d;@G85pM0@(q/c0@()}8[}Y)Tw-#Dy7JTҿb.D\#ŒU/قo{I{{{{{]?Pȍ)p<ALJ#-}F L*Ů4=Zv~G""Gh rD8_Ϛr6iZᓝѾ({7%J祖:6P ە1_)a`YaL%-@Rȅ=d gXc:Vzb7^.?Ee*0i-Ag$b*^d_/rMXfDdY0f1JZYHՒ{9Te2f^/|2#jH%^$*pZ 0uFd ' %!U\a0TV\6q'Ky@ nF Z4BGO偬_D0g'J 7D(@-X 7@͐]w<@ ƧA,;Oa6b:SBT~[˼snmAWoF|Ҿֳl$et:NsV:nD:P t:MgYY:3HeTA&tg:ګD*Oyd6;pyiިd2r8ìԨ_S,LtҀK98(DN"d2 \ Le[#%|T2HTEl[.K+OM0KuZ g\.pX2E`8IIDn{rt:a/~FҹB!,.D4YX%#$eV #\. Pv:4S@_}}ClPNh ku XRoRCҹ\. \j #]T(+.Hej$.D2˦&|}i@ 5ԓo#av]^ׁelz*8cjQ&X#8`=]` IDAT沱C$G\P(.IAʎt. yqPԕF'`nʨ*}rd3vfo38@1y3,{ +ٛ_gx(ʥ[ެ:5!?Sq-2ʙ$ywGD~pƮ~z arz[OK0c^yd Z T23۩yZN=v#uAGuG\ϸKYk;J pg^YR뱶 %Xpe SsO=0ܭnjKuUXOk*h d.Ki;qRe0)K d-"z)@R~J6=Ӱx_7p5i]/Elbq6`qH5qF_j ]SNWN:C#/ɿҘr;>tš^q/휧7̽vhc]UDSoOܓUce}9Ǹi햘YXMl-8Sy(׾ dz=yދ:WZ/z/uH՛v8ƁP1q/sAX9\F0T45[_9w^8zjWK8/@ WDMnjϘ71hra@ 6v^ @ Bnj3Ϋ\:sEx E@ p{+Qvݟͳɠ*yY @ 6!{W@ q4@ f@ f@ f@ fzMu׶DdMY]h<cm:Z/{'AB'h;@X_R&:Oޯj ┯~Vp@d].n;~xV9-N:OjZݡ$A:yVm{LNN^U䨙D2qooF|aS)Pf)'nw+t"(6o>h2A.=c0ej:KENCu-\힄ߣn~/ZGpH$O6X r gSƔ~+X5:S$pG.w+K^8g<D80 n/ -:'$%W cµq;3'iisH /AL4D[d?!}gf0v*w@ nF|N*(艳  %@kiP@ }C&aMKg @.%+36 / (+[5"jHLH!vdގ[-M659~*sJ\..D%iḩ̇ *Pݙ.Sձ\8@1qFV+%t @fL2u"lM0x(O6||ǛT<gX .A!,%< KFݸpD1}TR5ZZGZc)#Ֆ.Tũ3ͰFMN0 WP\6rz#jsOw:a#m ~BBǡvd& &wv` .%\.ҘT3o;/Lܙ:Z0_oojjr$8qv_|MW(P@2_F!qNjb&RoL_ܱR  B"J u;%w̍2!(&yg QAi<,6c Ӊ}#)pA&Xxzlh$2Q:9R#>ZiТXo5|NeЫbH$mPN,nw,D` j\"H$լR\b)0du]#xN'2q4j$ebܖB?yYբR@|%xWq]՘$IH<b7 d{,e[J™u]X `8A8a bPL!'M@0} MLI`>ײTp٠6NYLoU’bt5؇A 5Y&mjc޽@X;8[;b:xnpFӓ VZb'ҿ<_".WB5Z#\ IB}Qf0E*դ7%r$(\ )(rw|(l^:GhQCX$U0:p>CPS$g{Cu@ V D|Ҿֳl$et:NsW\gTBHgӑC etβtfʨLfs=Βu|LjI=%h$g;\l.KGCk-Pv:4S@_}}ClPNh kWP( d8 BA=9[$iD<gL)HƓBD%R)"BP*MHl.9POf|" Z6@f&u~_jr@ӛu\y Gzz*򸌧ݭz @M~m=.[pzْ( Z , 2Qxc;UT Àw;:eE`t3ee~ڎyTz-_cOv::۵ٛ7sP' :)BBz,`kvkKre;mc8N &eb@{vFL@2/r{'j:fOEh43J&Fg,F*z.|jcS06uϳ<!xq؄}k~:Az`#Je0IS햘P>0obo'cZ$š^q/m7̽vhc8"Mėˊ~鍇_OOPI"PrAuI!ʿۿ}{[䧁@ @@  fX ̤@ @ @@ @@ @@ @@ qpS-ٹGۈ?8=.JhDDayӶP\X[9{+sMv?C5r[i̾R?l{f/QIy;!ЋAlMx=p<:բUN!+NxeM#*7H _?]TfXv7ws1p9G4 emJ u xnŚ7M,8j[jEH!Gbʁjk*y>+yVQGnH_{`u"yu;Z^l2|ǟW;D@Z*x'w@z6ۡWƿnFe`S)Pf)'nw+t"(6o>h2A.=c0ej:KENC1l1W'(j(GEK-veIV`ΗG"ybS8 =V~xܕcJkEZ_BGHL8#^%/]K@jVE$|.O 0xb㊓0줽0Fa/\#=z96?Gmds4HRhC8 ${0]ږZ _ߚ%?\N$Y??9L-C4(GZQN󛮩}u\Ϫ?rAHƪ?p ??sp=v=yj8!ϒO*yÛsU͚]7~zrqZ[f{oUOqj:b!Y.31ϼ~j'Qz6gW._+–]O`;Aؕk7aˮ'O}*T7PBY[!ҝUPسgCN'@.KҠS@HLš\0K4e112Wvu%؀*>evF C^_\ѡ)6j5Zs"!G4oͱ%Z2Cl>krT8jb1\$\JҰ`VP7D;Seʴ: (&=Ψj坶ĂhqЬ @fNm\Q&cᛘ/ݜxsj e7(\`"gphtSu<kT[zR,f`boӬAn4mWP\6rz#jsϺ3#PM 7-:dlH/tG{HG0"pba.Ǫ/W\d;@G7p<*EUr3c]jtO1l/^٠+M,)=@ԁ1O W a` \evǦᥛ$ѪI}sڨϦ? .Xɦ o=ˮ}z_fv*vr|ѽۙkb~ۻ`Z< k$715?z~:v5&ю$39N|__lӫMnAZGgQ ,ʿzvщgy륡Pس?sOBr쥟7kB@_yofޛ-{5ddž|W譚S?niuF]O=] y51)[Kue߹wfn\z5z ^Y*-i| fvfv @@1HKBF@jRZO)eGZ?xm [/|R)/w\r6iZᓝwy m,|v@?r:b62,:8+rX HcRͼ0rg&j|aɑX%#6b`>\TC7!W딪*f)uy+E3Bgʼmq3jBJpcpO=uv󵷏*?kPvTMK3w4/\e@ncwzT9 fJ &@iP e[~3p*Xyrro64WDe6^BVOFnݝ\gr5(~#Om?5 |ByE|tt?+$w_:rfeCX׊4wa}ok~"o~gKymQG뚐g''d{nӿ6W((J(0W4g>%=pHv{Bs]+WoP/'P%kxRJ.L"B~A{{N2`򑝏i쩂l+v8H:o_1w?؈o/zz_IT썷^r ]q+\3MFx H;lnNg dF|>8oU16<˰KleIp`, 4FKWO`{}ٍ礘zZT2[֞^w:B)'.wX:R.,YqV{6 IDATX6 !2F3s;Je2c'p>}x4_lfpU:*Z,1Q&*&% Ha$bLc2)!5As% o$E2JbBgl[d%-D19.5⣕֞L,&)lf jT H$ u-V!%5zs#K[/l+6F!%I&";\jgT+KjXIz|RoD`h f„5-F]L"Kj]v>.~~Go``.8FU|QZǯfx/?w+?7K'y.tn<7{ѻώп{wiٷ o?M_޾Ierô[oItxcXOnzFz?og5ka'(~XRYuVl)kPy fqvv8\%s@s;~T?+CC@Q1wա7<{q] $Il-vC@f­P:_^0 Iǰ)  r"d Gʅ&W$ k-휲Am0ZR es ;aHMzDFVfhf_}ζw-ooxs6}U۷U=|;(.ھ}F{{4ۧ#7o ;vBW.hܷwS \/}3ws P+c?̮j/>%9v 'wVܪz[Wz*{SHꋗ]ќj+R H pH|-,jI[ǤIBDl.{PN J]t\E,U+ u6j,a/\!!>_JbSnX^*^5,<|%\>VXfnw}BTAg+\9 ‡Sp6g X.1۽Y.q>z#uo4[te7scx-dU=['p~>|kC5cae4}J9tۂhaOs@|i܉oo^~̎=qPy=%2Q4^^~8EL"NsPLoT7 Z SBP֏qH,|o=]⑙~wmh>#Ehn/ U ɽW$wkGo-Pu5 }`D=™,|kw{3 z?qڛwx W$kW,O5[j^rm^B[{oVg랃\YMշ]z {Ջ7&u~򿲑7tN@ȏT2ʙ$ywGD~pƮ~z&-arz[OK0c^yYN*LD|^?xSa@gyHy}вK0:|i2KumGiNL<Sc=֖/`ct;NT͛L(ʅ`r\DAD}=5XTp/Kj-v4.&"gJHꌰ;R#DVqH̙A\ĝ+1s03sw6TDp: LSHRNNY;fmgז}dٖ8>񾻰}5m 80֝ b4.uzW=^ߺ#hvZ!3aZn`0iP$(.*pnuke6GhZޢog37gRp&ƹ dű24-@*};5,-VPu.uγ PVa$6Ӵko 7$z=^mK6JhFjhe!Mv0,s%۾Pikg:]u.fy`#u[>4)zEhqޓO>Ks!Зp!9cv̳6/mЌgͫ7?AC!nA눧mA/7#Nb88ej@_͝f1+ B!܌B!fB!07#B!!go7} ğ\ ;oGm.,.lͽ-(MVVuoJҳxZ"t4})>o 2a heKŤSTg<+N mWA/HXhzc~ޤmx2%m1j\J]N,%o59,m2 :P-#GUKWJP,vA P'! OL H:wgRhyd׋* rYMަ%OOx}X" @0nI+V z|DAV 洄hN{֟KM/|-NSﻭ)醖>|Q:o~:R~>9a}֕޷~oS~~ OlyaSm GS-_sn2ۓ^5ܦL_W+.}4rj}*0|4Cr랇Z|ϜrDέ֖o'z?#">Cfl؞C^+&! $mjSm푽`z> z|(7dR/rj-U3Ytb{HtWs(5s#E\I0)** ˴TH*Xὑho5@PbMڮ7nD/$_ЪZ/zN%cDW bԮQwO*?UDʱ d-g'B ٻ(v67Ss;r9} @OF]XjfJrcW>rrWp #Mbt$?˱*U0c1="[>?V(ye27ݮC^?bhekgz̆\@g9wtP@ɿ Q{խ+m%8cO>ںK`'D-.OU럺;w5Gnl={qV#jWݿxKJ|k j.\}Yص_y,l]s/} }uZ;Ym2lw8h(e96 dRRK&Sùu -pD>9 KzJTzIں(ka%Ϫ_\Sܺmpt3{-m|뒰`s|(ްPyV'W?8S db _YRif ͅLu1_vz ɝp?zoK0a+8MQˈ/|&(#ʁQ=MA.,zU 鶍X\[K$8JFd={e[FA$d05ɕRL=>^]N\9$:(.7[k-z!T r];GY4G2Zijr@(w+-jߐh8 @M#e=V e&[-)9=F}dI8T -6}@fu>9RZD29'oO)zרC @"UMػG9^+},:J"eڋ þIt= 5:% exfZ k.! }_/, gAFl=7Jw.lO} iW+L`W/c}n '3%Op]qwJ]_iwKWզDž{2жɵ1ncn[[36Ϟa݆s=>{)KvW.[sϊ7H}+p~y5rG62cΚ]l9?oZz~})[?2"}\cTwjB0 βd7&t3?ou#7\ f"fh *%]C]4PxÕf=ei #wBgPT%'r@$vﲑ1f~ꕎWyVvcs3AQ7JfWJirwih.NzlP[bݫSEvu.M;/h}Z(lW0Qjb3+z7F.%L9E@A \&ʦz_x&7rIUv>7٨VZU;R2c)zԼ |v\b[`+rN _mW] W/U~pʵz7;. OT.lwiW\9}|?Zb{rUUn')PUukW["l*Xff__hOvW-WOtp:yLf?^=-KcU?V7^(#rԂAjtJRn^l^+ iUr?;.;q885θ Ƕ id"Ov3e6kܡ'Z60uG\N4X gWo~xx\vRj\xm˶?`K}ΝpO|܏LJ~{㦅zË.]M֣# =}|oS-KS ?[l?_Ǻ6I~)ٓ_Ftf7qڵW '~hN0[ôe1(I h&hFR60Uۺx6 aE&)D"R!o\JCߘB3zr^b \.%^ u|1 -Y[P@p)k8=ht(}c-SHpO kC!LqLNR ]j%-Ģ9R!#4]ʺ KTg keZDoZ꠱ 'wi ekrJ4p36sKt$7.t%eo/ZYЍ 1ewC{=!UB֚_"f3Tq3\o V.g˯M~Y o~/T4U7G\̙ZeƢY+cKx`޲WW.Zim55<)C~=APIXԆ&<|n/ g%u~k|׈DˊOoV("[fXt^:O_0_M^]N D3>0q-.9Kt:_~wIbPCLgQ(++ZMӹ,-"40|:9EobMW#zBl6trtA-(ŇCdrz]|q蚑vڨB$"d29ou%fex2 o\_Y){/OO4j VkzBl>z"i^_Ű9R|vY -[aׁh:M< +E_I$RG1g.n˗>ZХ]x6ϧy h9#ِ7p@#d!VJ=` '3 K.ϼS1`Zr0+C՟o 7Tt9׹4dkag6dq%øt#{<fHETpZwn'(ԉnS[~gIho$Aq Pq;‰lUw\)N(u]&CÝN 'j~z=w) QǗQ:lSnhiڃ;X!07#yR)Plr(L<#K׊8a - vxAP$m]l)fLJ\9Wie&Ԗ]fB:!<-AKkd6^)TѱD߄2dRuN$Ffh Qc# K%g&KP&.J9&(EV_Jcq7\"`wWMM.[Lq9z^ŨLw[MR+89aR($&>랤clP+O\Fn>HVv f>wbmTllϡ 6Dݶ`0kQ}O_=@>2e)ԎJR|q6^ uOtj%Fc+wc3^Ea7.VFRUmĒ D |hѷToJvq[&:} &&B<'-VxK_DX7u*3&r|lv{eT> HW3\H7oM\ޣ39FeOZ۩H PVc_Y1}9=ܹw[ FR@ *Qee .gC./Z=0QN!w4LUA,|Pd8Bҥk!tafw8h(e96˱dRRK&Su8m`Iq]#C?>06:>>6PXT=7Z ]4"큩7j oNMMMMCnj͑v#SSSSSDfj͡ƅqmwSGʌϭlE{dȑNuïS h%bO|-zPQ]Cc#]~!07#toSv$,@ѐ~n3ESW`!ZERY倒`` \H`r m-ČlGڤߗ[ GFG&t{Lss=Θ2hZFx6BssNQjPH sGssν>{ѴN|/F4vJ, l7IwGF ~əh 4C) s-A! ]t*83CV mw; M,} `jb@"+XW `>8L`Ik4HjAIqi>Ԗ&D"YH(]Iބ]%)u4p_짍nnFM>Z\/%*eX9kM6mV5ȫdBr!3 n` !Fh6[#RK9ϕ4 3 4P3ArHZACVBq?")(dI6>9P7*\7 ݼ)z~iCC{3Sr% ,˃G 2d Jb]j$2 %0]MYiwtD X? љ8Fz:!3  ,pn.M&2nywr4~=ZHX3E9LQ-9`>:OA~?tt2<tbR,h\5loFanFh9yl ӖQ1D&)RXKfR@,W@AϦY!l޴$H"UT#-{K@iS}FO=ҫZeXdKR0/<+DPnn}BֺӃF17?5r94 ZW(@-`f:mjH^ G^`+L& `Z8A *&&TWԷP`0eVG4L+Hg p-'keZDoZ,# %RB&A= |!YP Bn~Ƨf!%rt:N=-%-5z"i^_Ű9R|vY -[aׁh:M< +?N˦9C4 @j.d2Ϧ㓁@zn& 6Ogi(+յjI! 'bRNY^΅]h:ONx=37VkZԡùVhvTvx6Oǣ}˙ზҐɗxg>Lijb1O䀩gZ )׷2@ ^ïA]~Ld 4?ێ& NkoXZk`PI*|kIR-":Xg펡Zmېp@[mC& E10@[L#}J21k[:rOE { emT[ŢedD. 8<iȲt )6sb`q2Ð{at/6B{'직B@/q7,h Bބ4Bo1w2fBs3BWȆMݥu^j*E{@!Bs3B!BB!܌B!fB!07#B!!B!!B!A$X!,x'Is3՝?!B07ƍ/\piluF!y/^ܸqc?^O裏0:#Bv$YWW Λ7o޼y3VB!>WO!B!!BanF!Bs3B!BB!܌B!fB!07#B!07#B!!B!!B-<--RIENDB`././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/doc/source/user/figures/murano_actions.png0000664000175000017500000027347500000000000023362 0ustar00zuulzuul00000000000000PNG  IHDR sBITOtEXtSoftwareShutterc IDATx{\?f̪.(x1DCS4E0LrR,sW8ߐ(W4#MNhxAPYV]bag/cQYavfv35gF!93 }Ej6 F+o4U[*jԙ˴f>aP1QKd^vFZjxZ͙*k ZP `y/ǵ(M v~OgxH\"H$"H,X$mb1H, D"H >މDLM!cM[L@lJP,cG$YD""H*IRT&H"D,&X,E"H,1[eaIlu|emQ[$ƐW@l7'vH V `K[ufE$W,Hl4 j}e_aNރLhqVSmT$2ګ- D$"UD7ãH$"@$BZA H!Xdgciu]`TjS/N[U@ʙ"HDRH mC4I$5H$$3[VF*Q۶mX,:u0a˲DTYY|@RW_s=7tPq}U^~uD2uΝ;O?wމ'8qB$=/m+]!#UFi^ 4D$r>ipY{*?Ijj͸7ڽV5^q3;|}:qCRܷ7>g9]vGLK6x馘 ynՒE[ΛH9&>qB?N D%uT VXLmۊ:{H $4]:uVM^ڪ* AX$k6km޼eYOO_O>ix̙3wر <}mzUUUZZZEEE=AذaCUUuؑvܙӥK՚}R|v}}Rg"b< t"".csϊYj翭_}3,@Gb?pc'C}ѧ|h)/c_z!Bӽ26.'s?|\#'o{F>7zs_P0/bEiZ)-/;H[*[ m!L;SXhh69?kI>+?|#<~Rͪ% kV-)ڟ[&X,Db*c$ "DI$$74ZN_qܵkykca-ֶF___\NDC9qDCdW^ tٹ\xxx( [_ٞ={dg6VRٚ@{W_UWW` "XH$mX,X,hk4] I D$6H~Kl}.͊IĎz0"*O{)s3~H_ts sNUӹsCk/M*Ȩ0/ظ͹:T~S#¼l_(kcbfDUItդ?5"jc{ Kb3sU3/GŽ"r7&m>cϠw(c5kDD|nh>jSOqFB†-Oz,y;a sw.#!aCQEDLaإr8V>xߩSD5ƏeM*ЅU|ɑ aLkZ0.*XlH:Zf;2jШdɩ9A1~%)?U|MS՗IstA49L?/| rwwe^~zH۟m5O.gDfy`5W!GKDp8hrgy^ +f.(iY/!nu#b<##{ܖEtr55[jVf_R [<ݗcTjͪ̀5k& fښң[jf;Zv #h.Ykkw k[*ѱ DԾ}{۽X嶉6nnnT7@vZ^^oz"rttAZ ØLZR޳rl6bT& _@w5j./3Ÿ90Z^~M\/qX2g5U!Vb}|.8bS?ßp@)%@.81spZ?iUqhڮ0΅xy?矸50s찔/6{e~vߢI>"HN^˃ V O͚?NL vI&epǪ#ڰ05.3 c VŦ5fΟ`P ksvTOIZ5ԃe])&@Zki2|mro63:G̚e*폞)ޟd+,KSnɩ a׻zqO晘tcv_Z<i3fNItiN{$uvkeYxyN M<`8^8˧pKGT~ l5{Tؙ>݉>ư̹3S7QB;!mk rϦ > 45YG8p6+21i>.;zflRv[;@{'{nkTϭL* Zi.$sSp5Ÿ]ebLLaAILf%"Xm!F888FjUҨ粲2[jwdJAX`vl1w~ml }2T*IHDL" 5װ;Drh[!Zvfvg֖-_rq؀~IJDȎyrW}~Drx5;rqTA X"R@,KD~B<"t*P~cqxrDr3r [cX+h9EfM?_SD|af6۶J?~08;P!(ć!"ko9+T-* /):w$튻;]jyr6 *bBmktw<M oOu ,n LF'v_#zegRs.w1&DŽzo""U@Ħ<ÒHꩧluڗ,gBC[2kg%fif{y&3aY"Rk:"MH[M5+9׬}GtBoBzff.=Pnɤz\jV3F^*%"*AbPc6E"Zd޲ZAh!Fi/Al{*ws˗eyI"=yBLA Ki|a=7X01˺y֚0*&|OѬztl=ObBDTrĆ s1A/ ""Q^61 6l'H7:qlQ͚Ws㙴6,83 JdxU;O]㉈A5/rO%ohr^hE͜?sGX)L痌mɇj^ƪa'+%gMaWLu!Ǔ#14t(,oWL'k(4S&M7LKN2(2-*'tDOD_>I  SujGveoPcnNdi9 㒲4=(GD6gC<\ hFuQY&x)Ps G'jGcvc(׬Z\Nm:V1[Q2eMZMD TߓAC$"h[i6U#n;|V: ˦+/_%=K",u&JI,s3mO&VmNwZG>s/ܰT1K}dHK V3,P[tU%{1D }X"ɳfֽ fC"2f%lfCJ tk0cL\.LMN[wTw;̂yNBG 6_&lRs7/9F])ʽsq?s[FђVEG%7T#8}5,C_?U!Htϸ5*"ݾ76ᶨ{gaLן޷֚cV?bSrX*Vvͪ5q(H'A KJH$N"żܯTFUVTTСC%ʕ+DԾ}{R_*X-BK?]@>ӕKuK94SBL1z,d`֡:ϼ>ã4 4]~z]q273O|WrDĨ= {ؒ#" R5j3ğr6gkmWw|B|X"Rh37nUz_dJ,ޒu&1z];w~vDŽ!θ Oxuk'D|õ#<ڐS刈7_\ԅAћs4s'Ϻߤ;e65O]f'rMʹCc`, ODܹ {j"b|B#7ax2Dk9b=UD;G͔ d^b<r7t+W.{ˁkԬZoU\ZZA 7UMM~Xf5Ɠuk:hNM:z{{D+WzJo>"&T~"(tqxEF*B=gRtO"X-Esa1DoRE*ZjnBV7b卂`+;tpVVaլ&7)((شiT*N>uTLVYY|rBa0ڴiSUU%]\\ƍWmڴ)//5j3l XVjXL&See>"xs\.G΄0gZ,łc3)D"X,'<9S"4J'?[V"jHL?jA\8LTbK&MXtB&e@ ș ș")(LL@L@@@?eXL&jEQb10b[+EF AVbX,<A(d ggDYxL&{΃ 0thpLx393993993=7r/Z:/q_T> -ZЗ?U4^glχߔ}&Ž &|{'*۶˝⻹/LV7&Mw)k?翲"/- >/Z>goΒˮwEn͟[5ycK& Rn֡aNt:%#:7h{Csv*⧴e8R $zR_ǯ=X.dE(bvFœq}ƢWCdI3+jùE Kw%/i1rDĨ̉^4sӠ7{|I7˶%.hKt.MR=baWx"Uז}pqyRho߯[egu 5lŁDƋbVXǐI}N3yEӘ([G~X[f0S1#싋T~u3e?I}Ù9_8gED7.H)C(7>X_)q6‰W8Q}X&a u9G^W:[NT'[ĝD UvZN|¡7Ne7āú(ZCK7^8ڽ~QIē90Trx}kXn=J4ODNi[KS"'_mwǸL%?mZ>:at?F+,;Yjy}o9b5۷w@D 'u}yeR@O22?H- }wȐqݞ2w~:oScA:irwIˠq&MzѭqQw|?xTz8]s0W!h}_ٓaxܫklڒ6%fwß=f֍}N.7&{%[3Q~1/IьHo8OŨª^JؘfZ]l-{vڙneTJ!ݱC9k6mݸHu8%v/ظgϞ=;^4UTG?׻| ^sKjp 񼯂KJvo:U1k/]_]ғQI<~5^tzϞ]{,Rh{LJڸT~*>`mu1Yj1㻯ұJN uDDt{M]CxITt˘FE zmy3sLDĐL5OέϩIH2nSMk_ L%&"̫?sMyJsjȶ{&#&[!#fn s/yqYM1eooEGsӥfWM̵;>r훲o5n]蛷L߾va@xdtt?n"H&%s):c?D}\&~li%[ghåbo`l۵K8A<>mЋ#=eDda~ouޥ55}?>N~z>N#;̾-I;Tvtz[{f~e fwj߲e^y-£aD?N{~/TFZSIf/ۄ֧m܀%Dg.jQ ZU^ "ǀ *&yǫúˉH>7WO~yV5lQ_H9 9-A@uwZm&;Q䝇A|}+rZJE۾xuoW5"/;u1u=Uwٕ]sQ^W }Tk5[_~3=Flۭ06|Y;z_9u~iƀǴͽ5o|Ϗ%Lv"˟rnX7eWMu5 oSEq|MFD\I%F1vM:F_=$Dnm;I:TDD$rخDmIyʁTUmlTh%IFyNկD8)32#ϙfD"1pӊJ;(O\eD;U~oqvɺ.x,'=Qmvc*)g tОҵ?|?7;+蔡VuS%]Z]&՟YΙKd&JȐUDXjO 9;4rxF;`w9ºۈI=[L_e;# )Y}X2m O֒2IY;Tvt}ڏnsz1/V7SmgDú%g]Y:DD O,ZicxƵLni,~eʶZxWa"nR㈈!_eN#bSim1SblQsooFsܹ]#et:75 yߤ'"^>oxgYW?q<3+[?;43j\_rgN3%bDR ݧ8}wD^m'ߎtG&R"\VAbnIBDL"Sb䍻Xy&~g"83\|ݱ_LwpU8f]9I3ju;/Pd.\l면M0i}3GD 6k׾ck-5kPKDRi\ξdSrD_ڋDRmp '>g:jN9YKaO.!tߧkCƏ",[}!MxT!RBʑ?*d&ŐQ}:|=T=.\nDz{7.㫳_\N5_.U̺;ٸkR m3g3 $"+Dr"#W˲DD<<3*9V2\Qݲd(-|zDa qSk~lgN6͠4bu n2JYϦ]o?+v(z◥?{/z_zd̋L*Z{bT*34R 9Ml߬AsBpN^J #K h6NBuA<[2 (aMC\P>B9z͵;vWDJp77dQmg}3̶xn2J"vgL 7)e "_{R3"'zuÝs](tr?%uH3N6+O;HDZ3K~><<Cq0>|!=!>>>~꽗vFM TWsT ""@D o$Wk̍jy &DDDD;]Cd.WؖsSȈHƳW^*7Z+/Ҟ?(e]ۓ{i՛[q8jĐw!!#']y@DƂu^//p˓WGӰ} "*.~Vx N1jP^-uͣs,#<۾3m=8_f.Hh$"WwU9L'4O{kӰo,ۛWj$2-2<Vyg*9\2<_xK^#бåDD'z`{_4Y{h~*w_EjUq{NůJճwpxn3>~^"<;yAas5GDd:}3wyb,j ڒ[JN "R(ڹ{DDڮ)jK8""C~[9+=SFﳔMz[twv{mb]uI:{Jo""Sىm32x^,=]vs@O~]2{7GGGGGGήit &#åǓǖٞPnTDfCNhfvDz吹]J5&j#3wGԚlϏ!#[y$*>zĢ΍2*b/#&k::1^ChmZ-(vQ^N#b/,BO>tbsDďR{:g) z^6*b̸Q w~-aIiLԘ<0?a.63^WDgb}Dzk)?ztsSWw^6b|ѨDI#/~%ocXMY#5*6zӌ%oĄΪ>L}l|? yD47/^/w+6k?8X0HD^xH$$-Jrv)>tuKaKo<'"4eġu׳߬K@3g'RѮ[Ovn57o}f;GOp!}!=em~mۓ2H~ᵮDw =*w*:'NOx=D7ɞ^p6]߹OPJn[`ݻs!DRv&)_ظ@%} SۈI½KϾdK^{ƿۣ38 XVbXx7Ń 5ܸqlIwsǯ}~mP'? L2ó/E`x>y_U#|%WߝG݆L;g_MC#=W6x7wTgw SP[&á#G/=TAD_q=EEŏ!(iQߩɬv~o73z>ZOYU昊n9\NvBB2S?dO͎O@<(@L@@΄'+UQMGL'A΄ǙD"hb;;~KQXGMDrhEEș ș-"P rs=R VbX,Fcqq1Q~ ș ș ș șE+gÏGg]W.H3,}ƩG JO͙D=kyK.w}s|t~Vg*\$"WM8Ǥ,+0[ee=|YvP9IR?0iӦ Tpb eoooX9[˯"ʎ'epXջs,9[-7K}= a#J߰i/I;nt?ΒCq { =ƍRsV ;U2՘dM'JBczr|oNXRעq]FnLjiB\ LJdsdX%2>NmgIѺTYQXS=) Nn~.Vŧ|7߿q~uE߿G0i*9|@ν<ǍjҘ )TÞUّܺ}_ t&"EQ}_ t#25"ǎ7"sYu|u'mITvt}~Y#]М Hd-߬IsL-QM;TX}]2")\DB "dDf{)]n ""{;;"ZVJ?}.K,mۓ{= 7 ;ujq"3eY͙5jI?l/.yžHo(3d@#5e{~ +bp܎p̔ 8̤̚${)\ط/N)刈XW~AO{w{EsDNakwGlHh>3g/SX4^9źY'N_)x"b ;}lP4N>c}kXl]KNd~#ȉE<F~!c dM#\.dO8P_SރƾoܑE䉨ۻwLwV|>vԇED M޷e{pkt=x%טHַE߯*ojdL@X;NV -WV6W]KՏjhJZ4#`E)\i/bqNuqQ([N̟ x_4ã{{eJvDαLvhږLÅ#|x֩\ihp[^"0|9=(wpN|F9)z<}ud͒;l7ފ)̆v2T֒_*?ڏeЮ]ETmJ-Mf(\~@'1% {A1un㭯޻R&QG8$tUALU.~0D$vNfңqm,.-r6/^! YKBDt,Mצ&vy,}JKvηR]ZvLbʺ,U%ouT3L=\J"W׶>ș3^H_2^aoN޻ KWNXq!Lݜ?r#͙>M D|ə;ĝxb+_qW:tFQ~K oߒmI+cyw颠28ZJ/a~>wԕL2qޮ 2|""O|4+h+nDF{0#*qff9׃eLGePݱ>pwʺ4g:heɋij=9$øݲOգ{TEGYc xz^x4ȥl? QY]&_H)5 W[mD(m. ڭo~q\ MWO$Vq%qbK 6&+kj=hZ-<p}]}]ѣoDܾvb犣Yuߕ?n#8WFN;|xWX݉!,"~= ""i+viQkɎȺ=mWNe=z<Ȕя!"*Z+-egzO[c 4|ej{'~-h6[Yu1)dwې 'R"3*5,xz F5%]zջ7*ҟ$Yw*LW }27~WDa_^+f-)Y~ϕxUXَ*v1dL@_q[:WN DR߾T^4veͬO2Hl˸L"v;I#*L2{);7;ZiО z'*ڱK[}3K{Ohy+zِ2#e];(vTQm IDATldFsϙđT5jRD͏ϔI5{VɈFeq}[yMgxsj OD2 C7*V6k#"Vly6@Dw_qӣaߢu+i/5,ymg}șh3+LNy7B]3֗p嫺1uizEtN];919"bƮ\|UuĐ;纅L0&;c&0~Ӛԣ CE<WaDDr!NDEMNc{0(N7f2$~>~F)1.nlÙmpS:Sxs+f:v|c#8o{ +?m]gQZ_´|:eͶLL sv_r|F"@-|oCƫ9|Y9ڽmK-/rtLՕvƼ$72vd򠗘_j[~o4NLB{&<W:uqjLʼnp~\g遘),%fe8pn!o/FdP 2ίT~P6FN=BC\(ђcݚL!]n&)͌Nwd~}7R&/ܿ=' J"4.'7k/WShw`Y"ԭ" W_WL|uZ;M5oA*ϙ;=GD4ֶ%_[fWu~Vq3:75INp{& w^69R\ʼn#%~t55N:(S5y IDCRE5lK\+=z-3bΛ[nOWyNW]-7_`aZҮޘVGQD!7śr%\7J^s}.yjr}dfpanL;icʧenS?W~Zy]DL3GYdžµq%YW,a2QQ$ Motߨq=&?”IDDDts`ΤH*򤻁͋bkh:ڭ8[ ,ռ4k֪#`-[6liqdV偬K֡ P*#aZ ٢V 1S̭>pYUleAgFWȢTA<3:T!+H6:-K?d8l1ge2 j:Ǜ3R.Ȍnѭتw®ʊ/[>5h,N|?9rUJĎ:NoQ' LQlnwZn5o\<U;6Wb_ߟħ]8,d E_T1N:5#Ml#0Mc]iDT0u:3)ڦ1_ o|ؠ-%oPj_ƒE}U_ C Lv}%"""b$QI#T ̦/ҍmV, KBf\֢ߥso[")\x6C VR%zҢ1gCi٣?.cƍ_ut""""""Ysz|g."uuztt""""""\3>ڽ6<&v:]~d[7| W~`[/vQ}6\2қR_~养ب>t#1@WdKOG~qҍYsM0WK+"""""]i{봶pom•OW`H03nyWou Tꉇ`)l]f5 S}}>޲.ݐ? xgڳ(x35-<'Q ]iJ}n Zmӧ#Z(>S|̂{g[ 3f̼΢pg{a$"""""+7;eA8A'F7=8jT?`u5 ڽqQcBC ѧG-چYE2\gQs# DDDDDs0ԥL.^t꽳v_A4r#[(Ewm;{Nt]b{# 16p:kS/^I|======Np455Ʋ2DDDDD74u]՞<Ԇ)Kf$"""""bN\CeaJ䊧S3s7HY,Mɋ% """"""L""""""b$""""""L"""""""L""""""b$""""""L"""""""L""""""b$""""""L""""""b$""""""N1@DDDDDDW-gΝU """""f93993 ` t:{{{Y1 a)9hxQ =88X*2dT*d,1g =yd 9Nց3*}xG+1gs&bh/Ϭ]ق9=T_ck,L}.aڸ֧gG [>h`9z@+{:_7/y0>EDh47#Yw "CQIրV{BsށvsL<  NU秺C]~ywߞg{?3SOOIw|よJ=l9(9=s{́=#ӖљcE#O;;Ohdysbwzr/P&l7 <38@<{Lx$vs􍿟;u;4~yv4!pFs<(~н(~~KADDDDDΙnp|?|7=sV}1~>=tf /O@ 9g'4h{w{Utv?ϖy\g{O@#3V,< x.Xv{Ȭ1+XɨIDDDDD̙ݍ_w?jy~xSY}A}$;6}(@1G ~X.9Whq_jŁ~hlowC.;{9/y'srۭS_=pxOբ_͚c 45Jp5h|gv7N `w̙1gNĜ;ԁS[v|AՑSGusLn38Pi8~hU{|FnI~4kNt`ڢ=_?ysԘhh˿n~X̌9&nkUIe=)H&DLYu2DDDDDDFé>BUQ>n4ϛ #Z{ā ~%AߝMyIOX5{~!Q3$x`;woⲊ*糞ZtgM*5?paQ?:q`0J #( LaU0明ْ' + j3n Pi4tƼ[1ƨy5w@84L) ,zgd^WIs}/ڴȈZ:pIژv{3I?{}:{_<᱔M CO?yO P@}x;5!iUO""b'^6MvsП'^w/3_̕7LgUCyZ̿e 4/s^';z;-Tk^[J-1+9z|p=cH}i]gVJYi&_%w5|79S$Ua֐q'g6) i tC!5fJ\[ߪ>BԔq|T1eyEOߝ:pv?hlz-%q1]a$Ṿ_#ϮP@8mރO2Ҿ[ RWwvo}Cu[쇺62-JZmSLuw 556W]GwGʳ8o~@cΙpѯNSO/ުnz]eou Ǟ~$z} fGS-6<Kn{\N@L7ro׻Db»~ܼrATݾ?w&'kM ;s{ l*;^SyF%6ܧ%cy" SvwLqdw]2N5ϟ;\"IAHK ht P^q*'R VrLAW?\NB,IZ6;U8xnF<NtF7ʝ,7-=j߄ \N@[_Myˑ\u@wޓw:>hVѸ[! DC^qIi]0bM|qƃs,NRmkNژ玥 .Q3/ hPFsh@p+ԔjNB;]yq[d0қlK?vL !!款0Y?ODD7/n%G[19P8m`2`VaʹDzo߾=+IPt5W<-Y*yRmޒ$|qdjP TVE\gn}/m߾7K:4o ]5^ۍYѢ75K~۷g% ?Sab _g_ؼ M=~⩤`Dz)Nko߾/:Ҷymڲ?!:|.94肟G?{d?xnqy_VMㅎ#j4;Fܴ [_}⍎1rA}Nٯāe8s/?cNsu;w^ 7.Å_v`ƒ|M̸;|2`l'/Y!Ɋ³<ꨩi WL vjFQO/ ?-PujL`aCl3`wE?pM⹧UӄZZ{SKx:T={nD WʦpQdXWԙ_Uw}m(t"qis9;[4ͽ@oO pΞ֦#dAO|yhj/EwIgt?ֳhm0 'T<)'/ ޡK_m^ٳ^$mCms 0}aL6m" (o)3| +Dž X8hzsˡNHNs渽-ܞ*J{zv>cy8 IDAT޶ IZ8YG\K9x:qPR@/kzPp eǥ|g>86m`u O9G^d<ӹ ,+|ow.|o`R_zi}gl5/zOۣ{s'gMS]zNGkl1:scjRD=g:Cu$`ĥǹ5$(^exsBcN(:u'=7Graߟ6m//|Px%mS=E_VHVpo㡳M~(x$}:1+_(hE̙>eyfQҲ4zMq߱,͆g-E8|f;_}٣oA0awu}ay.W ×[a&GcB֪* OKұU8cI8:ʎdu_??0yڌ$$8ViZsZohitp?bh>x۝?_ wj9 ց>iKo|.Y7axhaXK4Z| xj} ë! _d~{rL XHYvǽc隖V K?@ݴk8&}%މOnL^>z[^ Ǿw[㒗ne~0YpZt_N!@w s}Dݶ2U|$ xɘZV/O.iWY&" 33y2fi^u-NSKKN=[/v=lʼpc_jj[b~v_sW>R!k5Jyݿ_>>yOaͶl%_bK`R'xـEq/.xχ]VMEũu8Qm1B׈b??u?|%ݩ ]vD=wҚ?:!a_d$~zf38_eȂμ=]m#esLhY]hQ d)Sk&' O^E/砹|< 3M}O/<7?>{H8=8eN|NGwOn?˥{r,Nw/~U[W210s?10<<gVz0iԀǣ<7XJnk腷θ}Ng Ng:'/K}@pR;8g~tȗF۴|\e""Lj*3B'OΚ5k[_go-|5z|sNg+~p}N[y}Opěv5XRcG t8@DDDDDt=￟U 8NIJo39339nn\EbSN=y$@DDDD5k,LiWL*JRց:%""""""L""""""b$""""""L"""""""L""""""b$""""""L"""""""L""""""b$""""""L""""""b$""""""nW={^BDZO>m,vp׻y/6N]?ntkt5ݟ֞󍌹{cf ̇rk_i=P^3sKMDDDDDtKW{fWmf@bc}Bxd'o>6 h=X@ L]'7a7wԗ3g?mn@:󞕫QÛ0;y\_{G] AmG0s\!f<홮 n`҅ O|sIl@Ԍ;\^4f>zSMx|6쭵\쳏-p5~V`ޓ}ݳN}ja}zsOuzO6AëOFHDZ={DDDDD`67 J}GzQ``|}ssn"h u$mH |}N4. :d f>x`ojnl%3}'Dp=f@~_LԮH)Dz+x ]}en4mޙ1%䭳DDDDD8_I{}sh xZQWN;9KuismmG|c]%-ٵhRZ-% 49Ż 5Dũ34T_`uHLHHO:j?Ya7/ Dn_rg$聕hvkV(J>0ſ^M/D(Uj%ہ ;MfFNyҖm59:*)bC]yZbxuz:B3#w)rs F"T66CQN^35Deaӥ-`DĖwFT(,NeBZz5m1 5S'UyXFIQxǮRb/:XMWo$aJ:5#%nPs iUErwy"7@gHBuՑҁv+/MUz踤яF4im5"$D`99Fa*4HQ&+PWPjRg9I`夔D ZhśWXJv)bӤ^]f'$'ȥMiUU}ݖ>V:H)ZjFmZn]Δb.]*$aXDz]΢,Ti+ 21!L$TF&3eH”!bVo*6Vi۽X*zrWCN-UUT;:KJ/q.l֗g$^ UVj艶. @;&^_m* PA:#Af5Vk9klm!6heS e<˓E") kr*lщT &}E"E.#yr H"b2TSeojV,O"MX *}4X;-:ɵ}'bAx{gZdbE"Eƫօu CEΓQ$9dɉzMQTBbDUgZ[aU N`u%]LFQH,EY27yUOZ[%MݭɌVؕPR")]K]燞OU7矟Q.}9|]uD\o%b$qڢ:QrK,ENDPR3Wlst;=I̓ÌeƄ5D(m>7Z505y 뀈ԷwVow/+MQ\TSn,դ*-+BjX>ky/i\UoHuق'WinMP&5됒L{h&yPM2L=_[t[*,C tmJAU[З4"PIqHZn$ZoHZV*;i+)^ii ٫V4&8jwmatB7]^{*$`7%Y߷) VB^S7"$!#׬ɭQ . ik8)_,5Bm#h:uvf/KuQ2VQ\>5{oĨ%w{3<Di'ReJXÇk I^?E2H(`(3 3"=c")=^uucKզUad+7_eAuJDCm i~e˫ىZʔuUuTZ`E=hfM/ݾMCL`3PX!}UN@eP! V70iMCOZ VtՅ ZeI7 xi\ZrRuťMt2s}k }/(~X0,cTK"mK6iD `0q2TӆƅͣaEG J{{E2'c*KHV*SCΌIU<(.)3Cj*PĤi֫U5dkL}R=](3W?ܳìAL򔬢TʒeVXWĨ `dI<(.iMn1m5R/0cb~0iIR/b&8jsǨt긘J2bTYKqۖĬv7ӛtD_`t]q^~cצㇾSC.t}_׷X/b$8@|BqTr zRw!(UCG#rhw*iDTcˊt5N@r=(igN,V-OJhki1fdmOi[G$(E g'S*eCO&p:@1Hbw Q4$[It#$R9twؘ9a QFJX&`64*1Ө@n%21WsmjJX|%ЎX-`-U=xheBΘy] tĔh10`eb`))]yµJ 0UJ_jblj&5;j"w.ٽsTݵfCq]dΒݯ&Ku;dl7i=l^Vh Uko&'Gmqaа!MdE5Y;M4[, |*0m_$]~;!mܐY/KXe}B&99i#jbf6'Df$6 #ۅhp y|jlZzXXV\ [/]&=;䒿68j2d vzâo^(XkB,M //k}W bSyZ>I֚6,CNZgu`c-f;YȐ:+ 6s 0bБ",BZgqbأ Xl5XȒRvsX VC,VNOwQU/(VC~aj(h~L)oZ!*@ev*WȮ!amv󅮲bB*baxC d\"EVOwTRGԙ6-81f5VIAu 37W2s+9m. 96`ځnxL vqyX$nےgC, ԝyIabr"8AZ%Iu Heґ UJF[Z,6@%=RTQPHp1LHTl.i02ixO&\Wh]2g*U">9r}WWL.:KVm!3zPc]59[] " ڜ4떚dB!*;k4jPcMt!ɻ5e'kC{{hwfPtl,Δ9xLy$(!ceeVKڭQoo[~QfMvfCdb҈M#zpwQy"ZmM5(;L:".D DV4v[ u$Ѫq=2(ZcE3 { aKuQ-5ok҄Րcy\bF;5`h풄!> 2mEq-nÃQory02-i ~-$\t!D,Tٖ{|Gm,}Xiv' U $bsehMb_G&KYfq}YcIP2oS XhߑԤ&?`@l Gmi%"#6hWrJdau~RR'mMaS>:H:2A#kB93hCg2Tv]9ŵA'$GѰ+G3hhî|*ylqMLOΪJVGRy E[mOr=c.tĀu4 uzgK=fIbUoCVU)YEu )+VفM}>H/e9jNs-(6*iصv]SŎR+(uܳiia9Т۱ D5ȦHL Yy5!3w,6sD"i~2Uuǎͥ!q^[V@:Ԣ+;e }YS_<0C5b6[{`)6kJw KHƦCڬ"]MmMu {rX%Hgji14k E2dksUWm5m(gUֆ]l]bŪj UGRWm2MEY H鱢<]l.Ϊr%_F;)镄$ PADKUp` ++(ET@AQ$C -d2>s1IuaA3{z[y˙Rb~::]ǧN߰kДZ36 @ztL`]o5H[o˲@n-+uT޷ m}R,IѐLƍ۵z&8`clt_v6~עq7deZxWn.kVvc ^{ @%es|̷/g5b'=%o}CdN@2ak/̚``˂',t:Np7eO]r=IFM|bmIsS#@exFkӈ2_5ehnzv>PC 'ϟ7qc=؊vNNȠuKsvO`٤SӃ;+ppMfMȎ%S+N@´ų'Ƽ,IJq + [>˘K|jU>X<3̜_3*"',~Ţ)g[4%eKs.[wmEKfLrpYc45K:mYKfܷȬoLaFeٖ5yLʴU [5j:,^_dָEs^0pT󊟛.YfMY:bJ?wɬIΒ>੥3Is]ǒb@ :@hnܲ {O{eӉ 7\vܜYWRxĬ^Y}>)ו;^>᧶kmEqz`pcA+(H}&Кo&\>?'?̹oQ dmqovMn!rpsyNٗ U̹{K_ƹa/K0gK8w ~M|+# UT^2{nꊕ.fp]0$ChCHtȑt;":@ kuļsSc3'^y"kswKy&to@ :@ 5ׁouY?~ ᦅ%@ љ@ ̓Jdm ߯RI @ M?VQ|N:P%&& P^^.kx&@ &&n&Xju>***\.z>@@ ~}QQQPpN311XquuuEEE$pQT&)11$o@ oAv{[샎j~@pչުoAx&@ ,EEE]#@hEsɫ5jSFoAx 9 .~<>Yr!pG;c :7,$ !D@ q͟8eU p z ^9 $y4ozg. m#&/7$9ؕ@ @TJKp޴,+Sm1{70Itf"ٷ2Uk--뭺EW\ݭ<(g{uSGfxꁩ1fϟYP`!s+zX:t/\EяfҕKڪqX(uqe[*A5壨{)pz?Yt YsC;rIC[C$qsKGW~'~5)r ìa] Į@ µn-nY8-IQ;7/[~Xb@΍* 뚴-G3V[3nR:\*-xu_{>b77yf"?$޹ ~vo?#>śfg>n7ώ[ݬvKήX ywg3f:][_$._:10}DZ7%ʈbf$ wĨ@ u P:Ⱥ8lwUnرyOdhyPITu:9Iʎu@ȱթ~G04{ (ruݫW|[ A"2uPD%zH _|{L5xDsp)'rfm΋t.rslbfS@ DM ɛa3z#`?:s=Y:Ot<oԅ<U|;srڊPZ[t7gYݧsqBfJ6 @ k;[;oUn-dvRo$:f@3PN =Y;ஓAY+yCȞjN/5[;Z@ @2!{jNU/OfHY8L`qFfo x~=^{OڌO43#9^]U=W%RU9|_?7+ O&@ pdBԅOeqPqл[[]$}"wY?"TAHϭQkg;Eↅ͛Wu|tϹQfV?zKL/] Ag΍3UA31#]~I0v\.Yə֠iZt:bDbA1"XX7hD>a,YrK^[,uzskMKOL9:75*e)b? cry^!6mI4^'F$$#Flaμ*o@-9<0N$Q!vLH,H F$#  x}s3EoH~!љ, Ðɿ EQz ĈbDbAb߼[Iiܰ&6#ՕOh3ĈĂbD1" om=&#L $# ĈDg@ L@ @ I @ љ@ @ao5˶"1 p(lhn]ޢڸ0ŨܦW\X "t3 79@=L&jENWQQQll,dpnj>~|tB<\l s9 )!f 1RNh8 @t&@ 7 gϞҥl>[)C!.]Ilylyp1!!zbnH%PCultoVY |\p(uޞfN$I I 6y9YrY,.]/:[!2o a̴|^FiZ2h7fCEcTL?YR%HзguHO"97 <,)Za^_Wa(!2 7Q:E .@Fh"|wn? Ҧuғ BQhXN%ն:Fmf >zdv_ƛÓC4o~yT0"+-eei3E?m[H?Am f|dz~c|vB߽dRIt&:̖R3 JMrW(I+/KXˆ-S!1\8$KL1U{,U>IyQd`Dq52~.wDŽZǏ9( u_R?cfj4fWZU^}{V}J: H;[KYuw {tɌ"@ qˠҘ@͊wjX :6fN~yWűd Dgb2_ BD浺[)V#~ϼn4iP0Ec?q 1 `Rag-s]]UZ16_wb2)Vdr;'}>Tt֑o.|=1H}t螓Ń]# IDATQ|IB4\މ,',b2c/:)VQ$Rƨ,||yx{o!wJd 7PpA6HR9#(!*aPs oDP7I/$*ASxԇD^YQ+OQKix1 hqT = Q4*N[SLKhٛKn:Zp(nKŬHd*YVCPӔ;:_w`-Rcߵ$@QXLc!6!

[X0AxBB"EK]VD09Í' tMDǃXX2EmȤ^-IF(wUlK0)!JgQ.p&-)KgŭG;"lO /WtDN˲LQD9ܾećU^t+fDyަVnDhI̯F KbFoIF'Pkv뛑*+ `|w.x}ՎOW${{ՙd߈ey֭,ңFOSz<) j4l " Y'<0eeFa6-=M) PW#`j,}P}s(8[wr׼ON|UEi0XF.򞻆Cd䯎佧鲊&qlq7ϳ XwtSY FbٴXvD/MCoܵNi!$+xPZBygiwO:G ֠(KetHKik↣wt йT"`(eefSҼIWVl_n 7((-n}hG %@t&@}aۿnUWGGJ5RZ#S⎔) XV˫"XA<0efKg^Cj+${cL* - 1DIѩ]R( j==yg@X)eDX%&'AӴ" iwE0ԀqVѠi=\V"M4BP됵<Ҩ(3@;2[g/(利5_ϔiX bШB,S2Иۺ8U5)$\dWM58_[Kf@1vISTSil.N$:LJT/5J?>3n06ze[V->dY?_@ :@ '0֩ՃoeOgET*uڗ 8Y޿b9J8F ĹUŦh&VxZGm0E]HhP(! LQ`@мI3MєvEnP+ (wdȯre2ٕ3馢?qr!`\V+l/_ WW9.1D68˽zY#QN:Đnmq!ӥ# Zuˏ^@^j\NLPXQd60`˄h䨐]zb+ rL>దٲ;z=mzӓғ"9Dg[4Vct̒._Dlzx@!<_C4ВJm!^# D!DE4EQ( amd h +2*^6X@Q0 G1Ma@ F,(2օȶʀFeb\コZprU 7/ /Ht\pŽ(:k381w󣭞`ǠƑ7Q7};zd7P O{^ŵ^BwFI)B~A\io39֩ʞi dw9R§F$IZ +bXXR;'uO*)* !A UP0^m^˲G%=51>T"OPљf7$V:-p]TT?&Ѝۋ  e1|@5l.8*v;,"@ϡ-Fg4p:q,4N _;lA,V1x(FWF@"AP]`Јa^6~i>O8MuoHdkc3o|l\,B y˩< 6f$hLC8R;%ёjQR EN#$݃&zDV.1P `X 4Lp w_u1O_K/Lړ^+JKK322K>b/tO;9&($nD&ų9BpBim VUPVUq'J\^fNU6Gz5 ʐi'H6W;:O4d9I!ՠ9=IųC4ZO%(Z66k6z0+,"ސ{kv=ӓmkwz-ڕ}}'ωIt&;2Rcl)(px#NU yQ}1}%7j|kĭc4ddBn/nVCAŨBĨP!6ƝɞiyN/sD۵PmGR;ZFߡU5im`:űIx )I?bp 3GSC͌[q\جYŲ2d2O;x@# +V`ݐ!s=ڛK')V޽th$; }s# I=.5ܔq# dd~B S:Fg8U_X!ʇ)pЀQ(p{uH)+Ug}v{ ` hLGȴ7hkjuŌ49_M`tGtrJjs5*8b+ERui&=ľ.r,}B!ǥgDHt0c] ph'9*" BQ؞)q`c`{~cм51FHg6kffC??6Na5Rf֡\*C( rGHD,nWwO;[^em$eEA۽#xRw)h3i]<_'Xʅ6f@+;m]l8V=s\ -EQ0^rL.[B Q棧EfZbv[-ro4 c ~SOվ v:7m϶mO7ndijg@,-93ض=vjqV?Z=cF]>B2Ct&VN+w`DgbEҋNWq9/\1RY8N]g2`& 50FE6ܡBoUh i(\/)v+;y␛A>׫ԴyT,DQaT,*֘hZȠ[~UZфP, ܃c 4_"3fޝk,E*/hƸ]8 /[y!O;L nMW}s)NJIm۔]W*-OWn?Vu* y63CKEˆ~R7/)a͕[;'E[ZnQ[Ԥ0!MQľG, lw-!P%,jM/VTTϚ젎 (,GtZ&n*؈,\ "= /˫g uýNp,_x ī!:Ӱ^}jݖKd=c#,[QW?gmԴP77ǽ0-bWVykkD`2GMI7Y| K{ܨL3K{^m @REƄ}\3N^9CP_``[&k|Y{Xp4RӠ@C-Jѱ Ԃ*֯i[,M!{ϗu]fU ͚7mseYAU3"}P2זe}!9)$LEcѦpfz?jDxeѢ`.߸Ҟ=~Ep\CdQuuuqqq(mRc.=xxqGZ5 "WCai7,k4ZI^' C0o VW&[MeQ5ξ;Z/\y<JZRgϰێ֪Q B䐈8VKS04MduSMʐxhF=vK}9|oc},1#d+yP8W_dƸ\u*K}{[jDu"( &: 7/5@K޸_0'W?x7=}_Xyyd"  ƃi+"t'fn5zy ;}RP.bP  .;^|ϣ/+&ielvc(ZxX h3ƫ^"ϯx#aqh.棏FCB_;wn8xлm7mjlo-@x",/YEb_{M8}6o^ݾ]*/o-љG+7g,o.yLQ,TY4Y'SS;ԇkL85n{avo?ؼgJua>j׿–i%%}O PEEE) vaQ*.hJ`|:6.P!(z}bѨhXeaƸ%'i=?@ȨHND=%~rAd"P.z4NN<^dY6]zIH2 $Ha@V=cN4TF\[[/Lbij|04)ԪBCFI+s#D]#0.K{^i z |e pUu bб4^վ} CTs%fF0hZWx{oa3tm}"K٤p<#6&&m :׬i9Ng|_3Zսz[ Kc1 FjNw:w#?Ik`ҦT-͚jL`Ӫ u0tH]k{&W+Vbnl95(Nfs&j5 -&orP<ȒTYYbGZij]̐~0>ckl:{ g]!7U|,ӬVҵGgߒE:qOPK^mj6jPյ[V'嗈'JőXBU^a*דtNwa]_ ɕZzb+ޱd MQó[c~1}aGjP$dM;FjKӝYOU ϤS@xBq,=5>C"FR%G~$*2o۟~}J|TXkн{MF=jљG 1ؑX"6u*V$lqo%W0L WNc5MQG IDAT?z};i߁GQggCi cSRu]dMSfz䑆իe](ߥ"PSO%ءq~"љW~`[dت=!{mCo+dEFeW8q3M*cywNƪâ0v7mڱ$sd|V6 F™7EQ 6II+WLIMErb$M4MceEe0ii wdYVE$[mڵfewfei0,˲,ƽQQqz=ۍz;1]b(뽾zF(.e(+/ ium>[))pȨf>`Y%^KS}lM2'5jJL _*>ٖm iy戴^gI=(ήW6C4_рάQ*"FNi+Hk3m>MQ1֐TΉK7wHek^}o{UQ:DZOOg.gc=XѺ-ONi;/MǙn),8ԤgnWuL,r NT]bNW曊ÁT]]aa>Q/_ΘLVN*aI=c_ (:׬KJ0gO'4|qpj@,.z uf/ez-L\\2%vp0E7 jVA lTf}Yiaa,] n>b%6,uԸN`=<ھbٜD@}T_"2EZ$1 ‘cJRRS a*].h}l6[II(1a:NESZRboh0 `z}Ͼ}aaa:lEaEILJZZ0\'5?޿ǎqqV2?bKST ؗEN=HgE=~Z i\y1L'$ qEO812<|.!ۧ 8! u- oI`L[ݢ R:İPe1KWzijwAk1:GLSUbhJQ²f v(XSènJiTFMJzD ѫ۬mcic(K&G{e&aZ[=];&4}l(ړWJO> 6I/7{y P=cFwW-Zεn&'$M8~N6EB!}G^?17wo_5sfδ27}͛61 sk׮dIzo۶m[>}~GIzـc`3gN׮]|ǝ;53sԩfֆ+Yݻ>}ou +;%##8r;Nh5>6Y%FƠsfO@e F%`0K>YٽFi52n(Ԕ`Ԥ='ڝWcQi[0ٵc<O> 9aZe1н( 7㏶z ru(P#M3P:t[:g NU rxyaOQ,Ҟ8=- eA9@s&x`IV[!)WAAa8|oC{g~G #ke%։X1r/k,T&ހke׎ɡS+9>gX²jCSuzH1CS !ع`D%@Hnx2Dg7+{#{GVc$v}]:[nvڸqO?)..tƎdժM6efd3*'<<\Qϫ۫Vq-999 w]VVvС55֭[~b^bž{>^뒞>)5gt}7Gq{ + Q:S?Bߺr!+pݿ68#ݭ ?jX !t_͖~!Y=5p;O󊚗zaLzq98#wyilY] I*:^S}@z1>"Z?]ZnYHxB4*GAenC @'cw%(A݇`;wHo=RX18=W<jn=xAъ;Xi1bE?>zMV܊agnwƼY1}.i{vMOal׿ 緾a˽N7^E/k~p ۍÞ={nܹs gܗ3l؝>?qDUeU߰~={@eUUqI#GEyuo߾}t/'',4tm߾emY=y?s- *nS"cj,r:)Lz~<?ߥ2Lo6wvW_4ڑu(Ы.I!QG?3dP+jy\,\tzX 7 $+UĄ,M#ţE˶=WUlyo+<nwM5aˤ0_FIehsgE,bYwuodr~dtb +gÚ?˄KP! FׂvV]h"k iEhW^*mm݊[ *]x! L2D@+yx|Lf&d.9s 6.3fCOV(a;~sjkVkc̴d˟&S(/5Ul\su̙=PK[[c{0Mm69=!\;giC U?f=#c'$'%Yx?:U_WǘL3l5dڶQ  Gxx "壏w,֖aAX `y>gnߦI33?mvt$ԅ+cb՛u0u;ilgo?Y CMyW0rٮf9N`c"XhOz˻j.]eс Д&3s3n[- `E,f%ţ'k~:[;Xh@ic21isaɑ(A#>x;]6uo޴\f˓qU9wǓKh`'L?=!,vdLVxu$wwwHD?s#VNWrau(o})/6k~0b3nC:+Ws~o[9#0`Z{SbBfeul/6ѯ]4onLO}Ѻ^M4EX sr03^}@EI."yaTuzyEkEr>i57Uѧ61 Jf97) g>,/1tM/zicuePnn2GGC(dg׷_l &00j'JnD'HQT^W_~k T6^O]5!Ws&z:CT'+,k"ښE6:.|M1 zglmm]\\v.jlT 1&bذa,.Kۍ!CĎ]fbsNv6...k֬Wvuurq6tuҊ-׵÷ ƍ}@ 0VVg6._#I۾m9OiY銮nܑuwޏ}EtzQ*a4ͷ]=7xb+2<K]5r훚f2 RC FBs l0I]8v#pw#w'a׬888`-DY,6;qجZZ+]ᑢA| L&putՖ΢k-:'{ MLnt3 yX< fD&🇢^Cr+ךIbKraߝ1UMKl1f`*)X݆x1|XE<OVۀ}9jbD2Zl6~`; Xl`pRO_N4@ ;vРAb1 ckg7CJ@f 0 b"''AXAC&c!)cxg,]j~jhd6VdR_ϿeeOlh9kFprv~_Ox놊,[0^ еպ۟{vPٛ+ߛ;gh :r8W^]%rv϶3^_5g\3A6!6Z=svF`+a 91 pؐ~1V9g wY604A>#9pEdGψ]ws$\t\9p|1:u3-W 4mE/uuvZAna+1wמAt: Bv`` 06%yyxI=,b[V/[$qux!cm믽:XixB26pY,A 4a6sdmx_ff3a,!lX,!f6ñr-SrY,K70w0o Gn$1Q(r9+RP˅Μ{;2w$y<x%}ߕ6^A!N}[8Nle^mԐWߎp{u"{{.ۦ]%A\4ca$vM$Ie1/ٰi63smGrA0f>So*2:i8QC]FL Ex6a ve>i"8l6 yI8l։sݍߏ f6s],ū-vbICX 9.l>O Fs斅7c?T͚>Lw?jҴ{#;M`6lGQiٱoNem0f4CLn~U&QwƏkii&|f=NQ3jbDO]nXY[7.$8x;LUgk%,D=5M3Dd91LSC&굺q 0~(KU0g5ͮ:|?5֤,qT__|bv_\.xE> 6 X:5hjq=ʍYR=]6xi3AeR&fXl#XIfpXZ]יfWǞq X^&`Z 0Øo^eh5$jYLZI|Ԏefdi]ۍEԠ10TfK"fc61fgnq9var|-9; !l md+z-`?='8b̴bC|rNc`if3ci)eQKBq̑#XJ#^u|~C2ۘo=ePXvl6kus e7_.H Aqlx$˱f5aI\{hP['Ѿc,:0 v\X!+!t.ƨ7? P *M̍k̍k md.311f˃>e{G5.1ݺKC$ӔaC3f?n@M Ќlfhlf30f`h0N|1܍aҥ?UYupg#=Xsg~#q/ ƜP_eFE~xkԼ|艎&@A(\]~[?῕,hHzȤB3Z`Y׵mkc39xcfoJxgfe,^xUpIKkkĀ?m ?^q<ܜggЫނS۲g ~:0xBJ]v bfNZSF Y7hRkZFK-A5YDO5~c;|c%d$\ L?40~W | F 3|F4טNo.0/#ݶ^( ۻ[v0 7g.]rw|U;C._m2fĘ;e&9ls~|_j0, S8nЮMFאtk\}00g" /U'2>Tr}6/_Zdَ|+\7@8xcFHY㕧\;>ok3ns+^4 $W9T8ْO6l6g2~|CKgLqЊm0 _fLA<7bek5.4mfX/?Yӣs^Z IDATĈK s*!pN85CpLq,atLzH k1Xr0Ak=08ZmMZHmfǞrl՟~N6&xjtE<8@B&3fT䇱=j^> Ϩ9 y[kp oA[#%d@~\.eS;w }uRs C̜d8T*]\\lR";Dͧ wlf3c4ѥ;fr&L|{(k񒵐_X`6M9rF\+v<ӄ7 FxLnd {9Օfo7p:v "a}~ !$ba,-ܢASҼŐ*қ s&z INS"Xp8 ?7nUOȑCE#2SXjO"< XZ1]wq^4> n?)ܦ !Bxs&B!B!̙!B!0g"B!œB!BaD!B!9!B!LP?1ԬS 3VąUhvK.n.'j*mSBCCC_ x%44444j/c:mI(* tʔ)SN[S3/5mʔˊk Odib^Ôi1ˋw)K*TUKfGEEEEM2-fyI}ZMYZ4͞ . u X KĔj{eQښ1ӦN:%fyqֲOxߨҊ&@R7EQQEݾC^YRi@[Ul)SBCŭ.37Fdvh\疩-{%tyknQ:G:ъ%B;εnèle?z#ӃEУ?U>C!ΏrMHm{qyw,+jYD]nlR^l ˧gdvи;#_% w—+5{x$rS);{ uEy %"?&![Sf$c~2m,+˂X/NXWC]iF|| E|_H?_kλGP]r.ܲ 4T+H˙x'e~_$#~^2su˶߈AR!h  ej>hJUoO4U{ߜxCA໼pg\>'w]B 3贊uUu1nEUHFQ)xS)t$<=%܍O(QE c]WRkby"5ˣSRȃTZyiu@Y~qY+Y22K4$(aHBJT~NF&TeIb:M}cgSD.ʜr5ICR"}ԗ@U<>.NpeY|9Y I_=`Q s _ Kd)9%EHM-R :J_"\jTjJ'KXWON-XCY*&?5DM# }o~SV'*(@4YR{X:#БY)Kmr}b.Dg/*D twвU~`Ҫ8_ASENFq :J2ٓ*^$YK,3d֕d+;5)Ĺs[+W$Ys J*DػL/.X>e4䜔֏`P%%u1nZER'րhv|U%z4)5 yuq-$!lzց7(؏C cmrJ($r*ioLhjqe㙝Gݰ0g"40cfB89M&ӥ̊P۬Kx1reg%T3=W"M-o] 5 Bu9 ųWZ.ޗ/Lغ̛EeU<˰MK|yt~>>A~"j2Jӳ#x9y>['Zjے'V(JAXqHs7Pzzj֐$B1dMJ_7&:ZIG{ 9$:8_)'5UnrcIRkƬ['ЖE,\/Ж-8SIٓ3Fv4h+23 wmْ|ef(? ߺ_M dz|*wn1rKg,eVeo rٜ}>ۂI)u Ue= M&9>EYKt)SڪCjtLZ#v+mH}cYotTQIU.Jٲ) Qg̊* uu)%EǵcX[{#RII'wogH l} 9q\ 5*…R%?(AXcm;%E(Ct4]Vy%I;x 5%./_uW*k ֯>5fi"#-WujDZ^7 @۶DT7=P*~YR"~[w`}tgKv.5sPrʺ8jYDm]G^=̙ wՒ$g 'Ab2z7)<J%PԀXܔ$Q$xF^R$_̧tU)Uy JG yRfNN54W[vBytz-͑"۾<~(97KuZRRZHI4 4Ք4V_S]tY_e@UU[EM}H)sȲqEEY^|Vnc^勅Y1y$ jNJ@ |2+51RdA*7DGP|_ udbuaYZj c{&EuѳE[ %ArQgX(Uj2qvezwyA~drb\PH7i] uV1JD>ń~JCX-%%sD~᲼M%:)T[M.1(yX*MjO=whQRX.HGUtmڽ~x8?9~I`PPP`ˌ_X!O<2zk}e0~iH&UΛfE!IʓLID]Ky")ny"DT9P~N7-(F].5,#ovvO[ۓernӴDJnܧD%jmIa`6@6&mLPxdiM|7SW $I>1}] jNZI|Pe"]8* I~:j 77$<(jN~usYgh*=;bOͣbw .'*7 IImOD4)̗7KN-^WZ6"L^j▒Yh7Tݡ<D//)I?9;O@+TVkC]kդ\v'1ۯoSL4h)ٱDʳ+J$Om6t*0C}%`#,D.V(@'%(TPR }ERw8QkC{k*dNY5.ƪļއΚbaBvHW},?,uF >q7#UW\[UpXH42 )KXy|JVY޳,6n|&Kss> wx./3&v#fR즲 tV0qX_Ix?_QTIy}кCDj务B}W 4U!1I>By(L0%RPvՓ2+,!{>e=<ЩJhRVhcd%IqF~WN(P 5%* n=.>=Gy Ȼ#y-APKS:i*_)R^1gX, %8.))09cz%KHw^wKJUGvQ''eFQ$IQ MNIԌ9Q|~ ^7TggZFm<_[| P5/fΘ>%(DZIS3&I InL"M. 1+MHHM*!I $)Π5CQPRBEjbT t "h<<,վ|dtT9'9{rp~rb\D(;:ӑBqEF\Z!$Ԍ9I(0ޝ>czMJ5 U{'O($@$t/l*+#9"W|iplv8@Yd W*bRC➳¬yS-JJK8d)qvW|V 2kΔ<90pUv'hzD ~F?oY./$2^;4ޘhIbRX,!,Qeb@$Ecr\>(JbB[OgLuy/}kd$c[= %q*F<+Oݩ%log$V<'u_v']DžѲbL.!OW.3ty&7` +3[l= iž``'c1-CH@',Pt14M4MQ`0acA.^hoobEv.V0---m%"ATbcQLbM%w5КfD*a_V" *3O[lNu9Tq_#ձVKrOBj}̴RyBݎN VeDL Ozx$U?=**jN.BܡN99 uLB!z3ެ8/ccU UL|paIU]GZzFOlXrs:Qn03lX1?jȌB!p\Ə:qAi^{kUA3>=%m}NX^p_!83b^h2p !jrbD!BYx\x/+КeWx7 %u&m2N~r߾O+_mue[(4X}"jC1d"B!0gp]&xYAϘi'XWnQƙ47̝cDs=I9m#Xa+^]:'-۷uCNB:a(6g"B!ЀC NF}Z\qaԏޟ fOaf3M4MSe0'L}|,ODX+a >%8pp l_oQcG{`D!Bŀ˙##fNs|džr8wzB!B!̙4=B. ŪA!B~B!Bs&B!B!̙!B!0g"B!B3B!BaD!B!d9r ݓ,B!*؞B!Bs&B!B{ B!Bg"B!œB!Bs&B!B!̙_firhfXX+a%b b >9'ooo7Xl6 |>+ka%"DA'BH jfV"J|*q Vz$14M4MQ`0a B!B5o!B!LB!Bȑ#X !B!-g`)GX,!B=YB!B3B!BaD!B!9!B!œB!Bs&B!B!̙!B!L>N IDATB!B3B!BaD!B!9!B!œB!Bs&B!B'^&6ٲdٮ_$^'rcUP9%-b B!c~3M 7T'Kv)bĂD!B!؞q= JTT'C{n8xFF\ٓ=2mx~Ҝ*蘥Qff$FyT&LCW5pDž9L5m8^j`2>bΟG@}'jetXd N\-KHJN "bXhFЩKug7pipipNCp؄ ᲉG{"@3N&TFݒSR:qPo˞&Չz07( ?3?&̥؎e[flebSX~uF ~Va0'1&Dp, ?d\4+zM(_FWxkhh8wV5MX!Bhc4s]olm?5! 3[l=aiەddF }Dž !KUq~Hz9i3= 6/)uچ&9K玴6^ړUXҢq 3+3Jvԟْ,gt}ki%^ճVnv/؁ʌ%/ͩ2u~Nh~5f\`rtG1Nw[aL4ME & jϝ;b\Y`}#BEZC#Zl77X9XsUzh.YXf-wqϟP0nk֥]8U--±v6M`,Zhz ?N,GC Nؒ0pN;i{Nv6ؑsj꽄H5l5y4!B!D!.\J}"b7w3K!LBœ;*u$)]؜3_B衹\_ 9!z4ݴ>Y8jPAަwݘmCoV{\`Ĵ7Fl?ZJH<&X W B&LBoz7o=/}^|7,|g]֢l{!j~ް WAn)an˙4)tA͓OukڨE!̙! -(fn I楞8#Ϙ{~}b"҈ 0|@[oQ+CXw jѳryMwf9oşTvW A:{iM}%' ŏXyYag*Ͳto!vZu:o-~ڛ|ĔœZ epIWm;TLWÈzvő3 3%f;6 !)K'lvٮoUۿ?+3if|L>i?Ig4|ϗ%w:mփUm\o'ZfB̃*X(!x BAs#APWH]e#k*w9Wߔ?$p}5+{+O˲5_㋯6Q̆g6þVL|o)׊+=l|W8YqG;- H [0 o'޼ػ@X;ԦU'ɛ^>zMo-p Nljׇ㓒5>~ۛvoc(? '2V Z־LUp~*0m{v|):b~6Wn,wɗ%ϊߪ@s0cn^Ոk7x!IhSl,^Oٸ=GtBs&B!4:0 ٬<& U7ܩ}Wݹ:bZws%iꌇ8Y3 }xZ҂מQ uGt]:'6xR0{/W7=Nh0e ys`dXP|^\@*_Y__W2/6xc_ Vض[I[vX4LZ'{ʛ-z]&,u)UW] B[|ONU 8zy7~:Mz#bOQ6LPsBeG)i݆G{3-ANTpmqw!ǐaCcD!,]q<폶g` H%uw;Ru?+6 -@]q->##_Pw& d.}KGvD|6h:Ppqtg=ASy%oBYZ>!P"=:[)jNW}+g[Q#]rS1$fm/ZIONەHaDɎ8t늋m^/|o^XDNդhNSpڭu}mWKso(8EMdEoMUI54{uo [lF&]MM !8œP8,B>_ӤݖVڴNH!G) mq&<Im//Knd6G%An: KٔF EʃWFwa9ᥰIwZ"">ÂMIrAˊ:~~{6c]|aҕo})i+=!#G`) B朑exm'gKy/G&>/*!0um`ݶ°w5G]ƌ *GHPyE Wy1iJ-Ӽfi)]KRݝiʷG\5\ !eݽ׊[)NUj/){Lk#t/8_!̀,H0 c6?{ԕ Ƚ*JJ63|@(̪j GЂ,U@XW\FXPFA݂&sO랓CEA.]juQr|rW$O}r^RKgdKvsK!E+_J?o$/}nscC43#ᵀdjOͱW,;O cw{//xuK2jʾlq6u}Y.0^΀$s~󫋓2Oy=$ˏvY~ڟ 5/搫\랬bߴ δOQ@WWo}/eE?ueS_dKўG`tLTc77Y;I)Y/|7(3bFN˷?YsO .yze3.{/? "򭝛79d~B꺁()xv"b\_7ScJ`_=[`qDӗFv 0p>300Y(^'|>K ޛ\_o|^zweФ#,Xa??*3O6}oIhz8*d }șL&FVgggS:fș0Zݺu e & sș8!gr& g~~$/3C#\PO3G3dnݾ4F…0MqӀww%4DDd4wL_yYXPXfTQq+@.Q4S Zy\Q[izV}goUsV,6=!K1c,@o@,y0mL5Wl[Y$UTnXYkbjH~Ҭw"g,==(&\1_oKA$b#.>7nF7L܌ElnqRL74茌e٦eoCé$hIhnߞ#!Ehy-Ts&K&gؼ;F齃q"?"gX֤y5HDD7^ݢ(_SbTEfGj:QݖYQ+n6\ Kf-zNVF:ܽObΎrcy]r]|Jm'"V0j?Hؒ',~@'kXF4Rbmݦ5JrV*tY%VJ>X(ϐ_xAs4\ٜMYZjڏO\ےU'd"gǛAX}٬;f^(%r kHh5OJDnHoywM j$Uy(f6XgFIHQK/TڲB1%IAD$uS0-2pKD.ޡn]ߤGFD؝HB%8,:>06ܝ%b5ܶ7Wj_X7 c{xl$版7V1D 3D*B' 6)[+_Xʻ$#0DD"'2rL.eQ*-5E%JhEH{6'uOJܺ"0$ZgzɸH=EbY"xI SnwH""g<vvD¶"fLHfk½KDMZ>gۼm4W|D)=)6L12^mAx3YU+1&<0G'Z[DF>㉑7.6s4hgar9~}^4fLZλ_BI "V HD$f"jiֻ%4AÜ[[Xc}q\%)7oV~?)rZODMz/Ta*)7 DBSeI6ARC&gsN _)0  Hr{?;)42SR+&$%$UxEX5ɡkP5jQX⃊̜qD/<0!̍m[Nmy"lMx$j4Mq'ȜanA 9D';{Mvo3"_6o  &L&? ho0":;;;:::::DQҥK&^7_O+.sMuu8!ggd鑞IWXX4'՝ o*ThTz]i˨߬˸O2kΉJc@?(N7tԧ/EW63a dٛEU33k9cfI̍ODܯԚB=w_tjM>۔]Hw g-5J2T42Xf<5T0K^LZlc+ )r[|j((o""SĢZ14A܉qyX41@,5MDDZGL`X")" 6g8L#u c2 :IPn["&0{6q18{n3 䕐 "etm~'RM31n r7Պޡꋶ/yGNb>E[M5;N%-o\IwwY%:~k0@vW3}>-%z&493o%7>gI(ENq&urr\4a M*'2ĕyg8Ç5t&we0/rZm+R˙%& EzNx{[ùZ>~ g,2"J4הԊ2u`d qАr1=5;;ޏ;}T0[^vhB ڕ{r&9\.ft%RU>({wƏ!2{&6!"UL\ZqN Nk&U{{GVʞ@'j,Sv dHԟ0 8L#2 wDUq2WXL]1rlV'4J"EdTR"bݽ_%5&^5ڄvbHę M{+Ig3D$uwl@K Rk*KC5iej7ާ ŕIB7'kAaqɱ "}I8NHШ՚5r"N߄U:<:TNDn1uDl3r"5ז艔X/ꊍo{5?aJ"sXEE$ߙXKmH KHp>Z̬-ѓ/TRu %gl WLJ˶|/;#]>H_*O-S%Q=F τ1rb[Qg.dU{}/ 1R%H$7DdI=*9s\4)7$<&#wH q{XBNX &^Wwdv}}7-yZ9jrNdKH/)}Vgb~̒c[Zf!3rD$WȻBJqHjR""R խ݋oDrvVr" ow""B˺1DX Y&%bY9CDr9CD$Y9ylo1q6疸KY>X"⛬'21n n7YH`XYCКΔLkgo+41nY;N:;H=4ۘrCF3spoW6șcKXšrKrk$ͺmO$oU}:KDVU uwFS-PcZHH36-M<$rH"0Fidr/,FI3[ &G}rаosQ-\<H fs # ZmcE۴1O ZluAul2ۖQmvUr"k#r#-}Ф }vkup ق'v'ˉ,4f.u;")Mw}3,grcjH.r ‰D,M&0+gDR8B#L]5[#Y"3sh8T!F,/A.7r"(YD"Bv_+/]XN\( XA$FuϣP Τˉ[oGȟRМ,\A}zQqa,ՔWK8L#~Xәʢ1 9Ņ%f~ "a*uzuQ "ji-D.4 ,QSeeͅ.JNd9f_4 InR{ J 9qb1Ab='H\$ T.ea"2VVN72/"5:mfNHHHMznS>aZؗYb!&0:FKƏRY~™jWkrwwX_Q+)뺔23FW)ýC"}NNy8303$@91?S)̰}kRk&/ZVjufY,UEʉj+i%nͭhuׄ+ۊu5#*R~O33Kە$3JxCBפʹ;m̵kTɇ3p͈k+ZYąKdǶ$3ΕboVr%jE;I#$ԺDŽmp@%agesg2keEOdZ3wwVǐRkX5{Ym_Y+`v`});;{с}n<|-iw@;A]]]( pҥ@ ܫb[?km"BCo(4` @xp.DÝxr쟻o ڀa |>hs̍H`@g`,ܼVg'>ˆYQnU{qBL@LL@LLxơ `x"*C/x`>'g(6sLT%^0h 3qBL@LL@ O\vOn.A@ma!Ig Q9_sl{+'[(pQ`>gAAAAAAWi@4 >{vߢ/+ܳV#'%(t۷N66ERVr5I8""lL[}^hCo9XKfNrW ^NՈ`<_rhSmh֔-,)Z "dthh.K>Zgn]C6e%TIsJ$QJ[7OA$niHKSR$OH=PgH0%a?@vm<9U 6*{#=#RVW5Z,X;kܝP׮&i)3IdKґyvL`;r>-եf`edKRu.KM3@O0/8NoX>ӱխ﵉sfX~ˌqS?{)|9_u5i7vPgLq+-;i Nv&T|psjGyjڸ)tvK>V3}1 c8d<~beCn!=wْknϝg߹^r&nye|J*G_ ~VOJmQ[sډ޻xƴB/|nVKE$~?ǮF }W&?_4-toT^7aC}l֙ϥzs,\6EvFhC)w &|ǹy'*ui]UHRjrSIeeeyVp f"uWKbce~ڡy!ҽuw2)YUTYYY~h w ~TogzXaX+uʢ٣< =U^YYYYIi>UXUOK򡎡lguyIeeeebuY(+œq]uػ@!!*:m327>rꪫH0ݰqNRNJ:ɑ`,Hk^pBӕ,S{ D\Us^i'>]U7D^z]euG8HԤ*eV)Y.$j`8JԦsUW,Ð-pB6'zCE+Z^ׯ|UMZNW[wGղU>$_5ޑ:hl&k,ζ)N'K{uѩ^;N$v|/Yk>:qɫL!S%׷șÿ tZono:.$ 9M^82t+pds c5^DDDӽdd?ljB\MNNo"bd%3:}F3>o=fM;zj1ԅj딽83}Lv@燊|>[%Fxvg|AU-}~R_), `KFץQr"bUzޱ# !%"V se˃D*B|S}GBj|%"7'(빲SufNgX9+#=1LNSGO,<\nӃ`^(e+= $@7pV;ڮu)^yCwz k{9LO\9gmK}Qʏg,D'joh4UܺA~Z@sXGkm5tu^kf^i'~a D%} IDATt`nwq>W>N.|V!N0Ց:|bmj'ۯ;O/LVme]?M|;"jku"JG_˼b{3eƍ˜5]/n߳nvPӝz͝VAh#/?'N&";g'dOjm?t6l̸Mt'.jk67: wbo?D03ߵsa<[cp/m.;P,^zhPk"kϡH"Zy9ZD'H}Z#~?k /Fi!vɓeݳb7\iSTkҌL.r8dSAAij֫υR-P T""IF2̫xח%Ѥ,;4z )ZIVSz" "3)=ɱgΣS瘐,MqxM‹͐R"bԬڟ+)8R7p}>![ uB { Dkl/[ZoDq~LOg/JZA^"/2E(d j(ϽUUbps7N=Pf"penO|jr㡴y'%z~;aeGηjЋx-Ks.n1~RλS]C6+ZEb%9YhuTֿW{;YͲ! n {j}qpb₞;_8Oe_X^;:ҵNAj{ F3;dDںl׻';Gۈۉڻ~T{G[";MvЍ!u;9bcۈmsl3]NvD$vDѶcxZF6mN,jh'2gcvJ'r3a>su(61ǻ>z:ߞ/{Ent82ur`ߊyiSgV=Ԏ&9:J'ksn${'}gD>1B^vyO'yy~lN/g1JOTʮ߿12)^YT9{Y[#pUddT!a Q˳vJׯRF[ %($̜~B,""F*a kKϔ'*@L-:&dҘjy,U IhMěOHMzާ "'&sloANO:l)*`~DXʵt0ر~L LJ"7iEҷWDF&{rƿ2ʱ[E +w9;z'}~;CϊNOtE@hX6RvMb'^zmD׿ I.Dj3iIb޶.f^֏Hf8tzNXj$'"YNdVC'-9e:/ji!"r|n*#!"Fȸ)gk}=LΙ;+و AKQ,ʝ]j"駾3eugU,Pt >;LJR߅->/&cC"|#̧s'ѩ9""C! gy6ZsH 'Wǟe>FXWm_uS](.=zãgˆ+̳H1YU@*Ce ׀r{y+#PHKWI*=uey"=;5"\LUVK-R))J}zXx#"{@Nq@DRW_! qHR S-w*,tOw/r-DDH(-tfTb!C=֏%)xPuiRkj颐/Z8\uU}etCH^^T5Rϥt)`2,8@A$XO6o~aoRLm{~|E E`_uU- =PfcفjISiy~G% {;y7No$PI;]ׯB#O$" ̎ELp""_DX L)kIgm6N}GG IBgw՟̙Nj_"s$S4Np"rdկ2ngaysSz: ǰO)O0d3@˯V|tͩ>qj,E"Nr?p7:go?6['us/ c%)\o EStᚆ357.Q3?6|9vea'̝8q"_UmCdu9{I>읂HxE lӿƿQ*7h]{9:)IjgRuBTF"HZ"ʂ7ɣXRS5V"x,ܔ$2?,sP+1ʐuiJHsMhݖ)WSJ!u;#;aH1?% ))H쵕 s(Zd!=uB sEj|X0(|VWwĆկ**0,~"UEmjMz¢U^voܕ:@$"OTr9Da&tLhզ7NUmIX!)LȔKy2'ǒ&T1XMQ +1uF$ϒ)ݧ cu^aQoL ݿ C@ ^,uMdB ~<}{b7ZZD~UKNFTd ߨu5i͑<$Ҩw\=/#9r4~L)!]Kܿ0$DVU$)nG|RDUtݦ;VTmϹl+=o|+͒co+6.OO].+I\N{*veN3Dۯh)Pbj-TLD7V_ ܋Wë_ьnn8ab.۷pQ42=}<!F{6X}( 檣W+"zF= Os_(-m\+Eu(ǪҖ7Ɔ12Y< Z{nF 2 g#%#(< ݼy޻Ne2y3ٗFApLx"!j^~]dgY liӦYVNvo8q\.0aBo|Ш 0k׮M<_2:̚5k֬YԤP(&Nh : 6m=ܤIP'c3577O0v˃g 7^̮NՊ9&M<͛3${{{?pk[@;MggN|fgg'jc߇L._JxjC+fmX,XeXl_9kgggooor&e}A<6_zƴ54V͘1CTq'd~9cngbw~"92 șc;g/53]SC|oBL'1:ș&_SAp |_\ Te7pȼ+j"g!^x}[+&O?' 1u̓l. 󵦧׎O??0Neykea01r&SL/ւ̒Rt£`؟k}qRx(-5|!|Ϗz.%&$C-{onx]od謬m7v;"lnX2cEFk qX uӘoDYL75AV)H}wIQɼxPڃiS6{o8pQHM"GDrIew"RQw67'͖SwW^ZSȫ/Ԃ~\\W(M#xQiFVMPDU߭FEVͪcV:j 8D2Jld^f0⽢LUb7힣]MYL322#ybu`)X9&%YP)5k!-m$Yo;|RyϨpHڈe7-fC\m*=O,U9dѐGkfatyG'r\p(Lrb,|İ?-Z7 Uw1^ *Ό^~ҋkXxsaG89+}bsĉjR&NL.}AtBJ0">r;65qgP'6HpбkVHRԤL4<'"([2Z-4Gdۿ[iՇ/<ŤN8O_*kN5o-/]L'so|f䭊Zx\'laN0"mַWo-/z8d_u&׊d4WGZvjUnYf%5U/+rR6ňXZ&͔LRM3_pϕ]7 "5g&lTRʷt0V۱F.NDukyyyyVq|`kNU=(a~&ΚJDjIYRYc6?w5!oӧONOOOOO(B|l -[j٠>'u ʠrkK}~8 )Z^/yY[[KOYDvf~*x^` 'R^`Zn!ID$F*|ӵC fC)aOPzDݪ8^(+ڊn1yg1iͬDN=XŠrkKuF$b7rNEæ]sGHNM+-q M~ 53w!_<'Oc]QKWz_|4{~! N/..^~}qqqqqqaaڵk W?>l&ѳY[yNs"I鸏vW ^cl< =G;IN|+1qH/hϺ!eF:%[ 0SyvqDsgOrWe )4y]ҝyka3・p8lvPzݯցƇ[A3#!3?y3eE};4kͪ60Pr#"Yco|9DuFؒ$P$oLݮ3o-}QiV6ND؉ 1O1"Jě@sG,_2D$'s5qx`)DĒ"-hlZ:92ь9Cvf#! ʳxۛok4F;MDR,1?d3fC^;DD'#BDq}Mk4:R5vQ\oe9A(2M|< 0pG'R6$RHbct1D2oӝ7Lx T$fnVH̆GӖ=ۃ{[mi12AܚhE]IGRB?!cGoCMaRv[QhyøYٸW/G#XXt"I0cNDbr7)0bIU}8T-o:̲WjV ND3ݟ&S$ˆfeƒ`66׮xKП eF6ٹcWbZ^$)VI')enŪӐi_nD.ܝWVn\ZnE5lW9z4,Z~`7 c^~٫efUA$%w13>Hq=0_k9]u>^UQa¸Y@L@x^Q/o0킩/////ry2r>~ a+A5u({6v șN2 0|=D$]0u]Θ_H7 v7x.i3}n7떖dDro7kB!o` zw=Q߳p`0/EOjWO楥3Ly{(ʼn܆Bh:9"VN*Q_VKǏl&Mɛ)RQ=,fLs+ջn@DjΊM&8ɩoac\(->]<[9oSR42anwsˮ 'ӳ(~m6ECw?xoO<9==Ǻʿ<|iQuo#r0ׯ/......,,\vmaaa8J߼t_g!|d:9*kήLL7NvHU`, gr& g gr& g gr& gr&zǿgW\^6k|P6w+İjXAsnM/hL Iub7w7|Go']7@#g$ƝeaJ{ĉH]`꺮+1nn\]0M0eg0JSl, 4|[zy N.J zb4M,z̫M0 ,{+fYώ;CAD|ԩZaYu}!F5jDD+//˴G~`Vj`|3~,k}jbԟpl'\{0 KQkSp3=SC+)sg G=W_ b]be⭋,yq}z=[Ky-4Gjuh4w}k{non vã[25[Qizs}p<#ov?">:șvHq7a;U͆}_HQ2_R)i=Ã*6{<[ZQe"9cYʰ;X)v촞61k#"dlԟgp4%KcD42lxJkF$5"`. z>a#8霕JǂI`')a-1"R'"9-{onx+dmlY\esÒ9W3(8i NGu!?9\7IE:_˴/TWk{HQ,5/d (d%vcIȣUqQd~vZխiUp5Qr)($0⡈kLy{(G..Y0Z9ZTͪcV:j 8D2Jld^f0⽢LUb7힣]MgϺfWcN.d"IeF0"")LҪ碤Vj"_ԶmK !˖2S$_CNL)-pRńJY{8 ii#A~qV].6G&e^DA.T\? 9)##H貀DVgo>Q޻IDDvZ㈈dueI gDď>rLfSt9{(vןӶk{v TS Qq~wk}YȔeٰH]7L,1UC :IPeY&'{{aTiџ oeM$)[sjֽJdlIQɹgF+jJU6wK[FC)ZaW3HΔ6>kΊ][58K[ FWZetH7&e%XN2vkn5"jT|N˟.QF6_9)]!F='")VQZ&G۩5f5!0Qx;>}zvvvzzzzzEofw(0Oɓ'Xui0ׯ/......,,\vmaaa8J߼t_7 9393393393993~+^qy٬ /WAޭêauW<1 6W,@PHcD>L؈qjaFq"~.Jg̯nynx+o]dɋ5FyZʫmy8R=nfG[#vs~v1~{ZYnKrk w!x&T56}s#ID~I L2F"φNϖVTHX2N.V颭;gKI(\%`? -Y#f?UZ0"Yͭ~w np9]ɿpaJv%%Q.CNRLf~\OT5)x& vQII%D?6Lduv 꽛DDZ`5HVWpFDs-d6E 7ڊ:nw}AD|Զk{v TS QD-utm8T#QLF}lMLx+due-n$IX ؚSTP"{gNJε?3Z U+VZQ˿]ڲ0HJ ڟIDrat^sVڪXڪm0"Z*tܴ4ED1Y/-AvI]pTeDxNb\ii߷)%4V,۟Rm]^OӧgggQ !l6x~>$ߞ3/GM7g^}.g>1r&@r2y¼É^4(p_ߥ N.Cs)7g~?4jN3H%i.&r{32U3M&7g^d>7h2;%/N#fBW_U-@wz߼dw[|0^șJ#34 sb N཈ș!h%r&)wo393^pHI6 IENDB`././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/doc/source/user/figures/qs_app_category.png0000664000175000017500000007214000000000000023503 0ustar00zuulzuul00000000000000PNG  IHDR(5fsBITOtEXtSoftwaregnome-screenshot> IDATxw@MϹ=4(Jd${~"D+{W6YEPov9?*ȕ:|HQ[MU^~kV4aliwD*!B]^׸<.9mcc=A(5T"#k؞BZT+̼pxgap;YHvf9PM7 Ȓ$8sx[q.2X3'Gxp`+g8)>k@Zo!CYG+[q}ɴC{up8]z 3͈lq%)a+'ڑX+nՙ*-<>LR,>ǾW'ӹg¸Epzzɫ8C˝vp|`2 -cF K#kxm~̙=ވt5IonMNΙ~cσ,P&e%qR"!:-3ϗ>VڵOx~}'O[Z-}v=/KoƧV8X-8Taީ<(`h[.P3Q}dh+,r DANJ/w^ZmZ1ɇ߉rtaN\m>!+nϪfEWL;V,aC Jg3.&)xgڋUr$7p%;SVܟߴJB(A֧< ԍh3dARt,aDK&':8ĜH^~mOǞk{(_rmOok+:/gTHF o(y3,֯J+=qG[-w ztq` W*7D[hk}mѠn6/W۬E忺x3䛵d@ۣ|GmI`y^ܻgnUjĠa샠ۻMPmGl(Nv rew.ludntxš>KK2gn?a !Gh(ĒBhL&ji4k:AuW `u7#Si@NR҂}Zг270wr?>0g!P!/' ݽ< r[̥Jdwr㤾mMkz$ nѭ ( Xà$%Е5dŗ^^!luLU,..go-*m\}`h{6 _~!5[$w,9jP3e&A0:u_0!ٶå@4;l;9@R B"͛5ljPELFMXupq`;1T ;oe"S@&U;RYRIVG&=pn/%`C{ [Ch"lՌ&cE[oULin[0=L*FbH#^K~l WZN~92"K#͜w-mn'JHnwʱw^/&"Q$U,WlP݅B_G0Օ+RҨAUtU h󐜩AJ]BԈظBy:UZkl"R$ KPlBSȴC'Xi&" $?'5Q/(5"^ߡY,жʦ* ).xzE*Z мB=L(45ӃЄ/ ɮQw$ }@BIEmkaRի$tEm/P Z_CȪ Nr*pT|Ϊvr?Mzk^%DNEqQV(h.|Jko݌˛/8;qDՑӨ Z3H~nxOGV[~>۱BII]Ԓ/#)4!B Y{յ)IwؙӶE͚\yL"I :t*X}un.cnAfh J*O"ӕ3 k8BO1lSB* TPS%376GxjscS :5R'3No*_އ#\ =,Ul&x35,]Fl`GȰ{.Sѕ22c2Ġ^D'BV] KI. ]IhJ53!U=vHvQ0Z|{eN)>M:J7DÿdVӡ"7JO?~-GT38 p1zNfu2&2~ W?yV162&K X6̚B??ϷoS~$VM`wB0 5tپ jfςɼ;x-XibBR5nԊ>.a+ea0|٘'z-pP6a>H`ΘWhkB?<bSMWGH7N0Lƹ?<.2%*\LCE+yS]WNjŖfOB<>$cf7}zgi4`ݹ'B8mPO4%՘[|,JE_:x ȇw v}tP/%Ӌ!XF-ڵ35^%ʍE"=4tnԱ+N Ѫ"ngaѩE æL4ڋ!??J%زq^aKuЅCriH-37淐C!':PR Ț.טMqVם6B#T.Yiv>b"ZkcBz_g%]'.?y9*B,Fvem(];BGB!m8B! B! B! B! B! B! B! B! B! B! B! B! B! B! B! B! B! B!B AQԟB!-x!B҆!B҆!B҆!B҆!B҆!B҆!B҆!B҆!B҆!B҆!B҆!B҆!B҆!B҆!B҆!B҆!B҆!B҆!B҆!B҆!B҆!B҆!B҆!B҆!B҆!B҆!B҆!B҆!B҆!B҆!B҆!B҆!B҆!B@Kx {|":lκHG.ğcKj4pܬraB#y7nWO!aFIMeJ~ _=FdlMW+Cqs4Z+C}gb5oFϗ?Rnn=jEKǮOԐ˳[zUDSpcQA诇!TO.]dH~.AN?BMk6@a_ yZ4kGcgU"(3Fp̻LQUP”'VNԵ#ñe7sW\_p&䈲lጿI8H3t$8M^^sKҿɩǜ=ʣrtaN\mL* ;u!b/ͱ_x, ϯod*`h[nL@C(~}ĥxgf|1 #J q|@GOyw߬۟ =0 D bJϾdφ~ŽOCηQ%;C),<0$/0𶻝!@ 1ق*zwbа]~AP`@p]& (߶#[$}lGQ?7 o(y3,֯JȵmCSвy}qWyl`py/40(u r|7^Hx㙟׍;ϑȸaFy*'4FB>ɩ@q.*zȟCpf,ls{T ; n=1RӋK2JC2>n*e,c~ͷEj!ȐI}-ۚ+ҿY[>-XI; @zsn"rC_NP{,kyL]Kܵ 4 @'nyK MYfL7z(a@Cq*ݱj4x|@l`\1 >K]/lssk7e߈ Zˈ*64SehRa.C'YgHX_ F8Wp1M@f``Jc:AXT3RR}u_+J4t O?dqʍEs/$;\k>6ݞikҺ=88J%Lq YM[t?2rɼUCV(5"^ߡY\жʦJ-Q܀Ui dڥ!~HHg`yInl\!i{*r-WDʛd!4._RU2Q$CI:ݵ1k&Ȫg\=mϾ~RӵU."AVyƐMx3ٰIU(^G.h4d͗/jI;aÆ!qf(}i .*mTdkIQGQB] EDȪ $(U! ?A@6șZD>ΰK~k6CE+41m+H} |laƲY}ԝy6c؛k o=v;,>ODm{a}aJ %P~?{VPkv!5¸s5uW{z;(h{|G O$4v<d= 2]߫ڃS4"x6f.z,*paϰ0:cKqg[E WɖNNzn}4H<'iymT޳uSwoFxm i+ t&dmY|9k{BʛAHӫ|}#YA hL\ h|We" @c$)^5 )F,ET}Az! Hx,ǁQG lM]{,Iw;aonwsA`ƮQJOk+ k6}Jܐ3>d@˛W.r@ᕡӦx C&M3}tAnZjfIr?H4WJ;s/nhۄoxIж߇8P<9UfS cJ rsr+;oK>~WwLTH*`@k<\YʸTK* Tjd(^c*PQca@~W #@úgS}Z! ,voyT0VDPߚ^$#YR[(PSR RRWWW+};SA۴i] Ԁ<[o/\b~PI^ٹYt(x$'nW+#L-D?tL[i 7* *7`^.w3IF >ȇ4&̳O\1SCKEnГϕf[<ϩ(C+-%  =fۻ/+\ѤQ:.|u٠":@wd4Hr2 N0-D 2cj2!Wwԓ$~k3`4()J)O,$T DQQf'sc߄J(~([;n̒Y ͵۰Ni?d J{aB΋ЄзJ(3٥MG/dRvwڷs-d^~i4 gnT Y 0r\4@45 .r0`,/)uߏ"`wuC`׃v>ES.{E qy5nԊ>.a+- ,&d=rmFf*0l̍}8[m0[I$0Z[#}$S@oѶ rdߏ@w# @o:AH[HnCG)! { m@cz^kkwozhԹ-91b0$0{E(e a/|qsa_@! HS<~ؽ>_\23]4tu4zwGZVܐۏb6wm|d̥u:? .jZ+I(j'@w!d>ѮlLCE+yS]WNj.sիf=~t&ZK,NS+cF['CJgҳaЯ&1];7gށ 7NRpپyD Mkګ7)wWw Emfvz[:YE8OS>dQv$L5~ՉlQD56aSKy%e e3ϕw "f#)Np睯omƞ^woٽwkяn[7U)/PtY;@ߛoFvNCɀ*jl ; zE7Icێ?:mPSD!T,P`P噤z$EUߘ`6twzK)űiupIF6- 7*ėDRv&0 ~:RQH ($T"P2 BO*e*A+NPR / /可Ç E%=I)(*6`@ϫrͅF h"*!\?-%$di TIsHi?JA : 96 دk+M@R YUB&ꁯ?i@(F0H hA@#0x R%`$-<7(>GT(oEoZG濖J ( fsBES(m ) &!XrEM#Ʋh?⢒oyɼB:!#+ Δ)}  6P4QETY  3BB(Qz ER%M ԃEp  П/.(sǼ<ÆLZI ?ihj)!)A) )Q@}?Bu?]@J@>d Z*X,:z")!^$w,+[VMI޽EZ: !N+(m F,5PA( I P1T>&M |HPrE.X'9+u?/`J/@p>726jWle{ʆ1BOCeR/ ~1ШwfPVQ B&uw,<-_+~?$ +(z&˧X,P6 #:ϟ+ IJBD)=;3̺-B'ݼvO!%ߋDВ+LFi?7_h2,a,NFg!ʯP_}KRq ?!Њ %  Bǡ=-8 rpY}*(Cy/\s86k^iʈOp86 S/vgp̻l7g'e3'6+Gl?=JU?{v(p]5U/_<*#HIIGP{8I971ǜ'm_~imWպ\t潇2gt E7x_ 5Izĝ]+-dxq0߿]%횰~vyQRvlR|^_}ndgdFIN_8܉^ ?xḛ;Oggg7jˊ~B'bX$*S1|VdIh8蝫';q>>8C2VfO3㎌z 5)~fiX;^@qn}O q.p&ˡȢݦgcXwXt40M(KW7Mx<y_' ]Xد3ñ=zn]EE$k4FcqVMM+,p}݄]8N&nW\V⬐#sGؘsYqm>)?2FFkª|dogk1nd! un=F(aM/JxakGَK!IʅqyXlgkinc)r8]GJը4~Jpo۔:r8݇?*_XeT4whq S{Nɥ~T]A]֮d1+ñ?IFsg^Hyn_UqV/~ IQ$I%PLd>_]$I⊢23n Zm+&q}%uĩLGCk0i3N (H1gd;oRxEė76NNgCNx^P}Oh7S2|qv$/ IK(_Ccs33˙r ii9!+{u`KGY/z9ܛQ鹑333ˆu4368O3d׵ ̜$nfffS6ޱl[xŏ̼U~<1lO.IWS~Qz[3Ö>{Yw+'ZX_38&_%IM9<%/m~Y$L8cc_%_hOZ󢈢_o7hG$ݜjm3wdw75n_vVO[ڧB1sDCӢ@qYM_{2 2e`sFD5eI^^צ'`{E.sO&MGV?[vm )??*4I '51]{^Yۍg/?wwώGtwOcV3qA%wLZ}-#nu1->t(Xkʚfj(Om,e^E)eg%;zɾڅ/=o2D$Smt}ݙ+-fϏPlna{y^ۛ4/ n<rCk7v $7_q5H938šIC$;7YWgU{K-8 _Zdx@odLAEf79>x 78kU$>hwoSr|/s25=%prb %gaUXR/ɫռla۰dK@}~;SVmRr$z RT5R+9oPRfdeh@4!.[QD?87 '>ص;2P7Rȧ~fHP~ɚ/?z9[0eB_ouݾ%h7Cd^4`&a( 6wj78J G/6R!j+R(vZQSU*|> _NA{{ftP2f쓙$@5Z_-,u>/|?d&O^\~#eҼ#lMi7j0hl@Q m3SsÞ5mI~Tk&!*[Z$+;~[UobBBsS _ݿwW[[G^`bےpj4ɄMu߂f6py;&;PqAQՐa(#&O{"h4 ;1*CdՈɏO. "ڌEƾI`kkȖ :q92M(&=ulו;[qQ(xi Z/k#@Z4=͵Tئc/M(c[ tj^Օ\e^S G8<՛՚ي]ٸdܒul'vS3lڻտF9L6_/o|9[.>NJZìE7>^EmQuv0O=˔ρ8:/.;`T*QaFˎ3tebA>tm60㽽^ ܤk͢~V(_TTV`Œǃn~{?ՏQYNn , w,0־)Hg悢ډ9eTs@ ;"nRwY\G(~moԎ&EH(~V O@'G~Hm2SIa?^:7l;6RYғ&aBmcW2øvm:KQþ36OM{bӟs mj! yIH(  I HY99}411u%-f|0 UCFN>Lh㖫/<=z m.*18*oِmqwȅ7>HF#Cu{WC5nvϰ)Y9٩1+Ǎ]&@܉(Kzum}ox[ח&78:Lptҏd,sib>/?եc]JA'DE|~1+*,(fggddfeso 2ކ< z_$(Jxa7O}*8bK^+_pPp?S[/}U ߏ,lM7kˮ<}vTET71j)8xuňrths7CͨC:p;}d2V3]E9GNn~PFC Z ;L0깻g\N1헏MP{yrK'*5<KԻN~EvKʍ:9̷k4kn"5ZɒGP"m=D{})C>; 66 W>xց=cMhNY{ HeP1S/[]No6BqAS"IEX,H<_ Q0/2tI`_Oo9tUr!)n|Nx\@e(އk[\HS44fұHZ!j>*A>/Q> IDAT  $( @(:/r-ή[Z-}>J~d&jufOO9~ܭע +|%\3eOiѢ+swWWJ "Un$Vo?h+,(͏3@IF9^6$]D$X8iOEMvvLGZ;iճGUB s_%+ kS[s,myGP{ۦ ֑1>|W$Bacα3fŅ/ꔳW46kGގ._sO.dч&t7qb| ԣA SYmykT n<~IMn Y@J}CIT{q%()yKcwҳդ%#=B󔺬rnY_-YY.|h>cvfj 88JɨCˋ&\ PzO$5w5%= s(ȜSO,URicwqQ{׏zbE"6bkXb/nxVTl$D{슉1Ɔp~}wGPD8@ߗ73wy6B0Pū2Xnh os;pmܯȽ]}1wg|cҌN$YkGv@E_`-!6MF\J< 7׿+J\mr^|: iaaK4iљb'{ l0/gb rJ]|L'-jNMVDiv3ʢuFxU&5Qfm \sA'.Gln$tnEGs 4*o'^vu;%ꍨoHy/xDo; x\KuX;CæټW{ɿ$JM4CPR Oj63q;9嗣 ꇱW"H  8EJc`!E%)ZNMP !meٜ`YѤ*Xw>PǿH ՟f^}o;G1%5ZX ^΂(#On;pfꀙ_B ݬ %#qVTz *Mcde*;0RX/FWe_p,N$jSoU@LwH<빧YT,M<|G:%|p}:D~6^&9XyNyt؃L\͏|'W{Y,C>QM&OKNKF<rMnHXBp̽:Kve,#; GWl<VTL2PHDюh9,}(D܊Ȯ_z+eDyF&|JotLuIp4YÃN ) |EYΚ\^І z{KqwB/y!^q[Wo3O yw Bט>>I sz 6j̬aT}qvu92ac?pZtrU$<45FM CiP\μ;p,G 8m܅@J!1j2~4oΞU&|7~v,\[ 9\p#"^Vv/}@ o8E(p(X|k"A }!ԉzcI </EA-+;FZ?EWKܾhD"P$wB_s7JJlִo50ͿPhӤ]WzL۲uBvu3Kʈp&zkċOF!*͝o޴?aTE sGzpNu1Ʃ6w9pZLA9IՖqc~5!PV%V#o ݱtiF``'&eqNKtQ}Gi g4!@(ԀB*J'l5ilmᙸyoeW&6pf lƭu]~5M[obim-!FQM&OKNKF<rMnHJL"H$3o]z!B!TVUb5lڑg:Z85Glo pP ;R_Gʌ!BeϿEp!Po>.&jw>!Ub!P]6gH6^\d$u/_wuKd0K+Ia:Z嗬t˻ zѱWևKjCuv#*XT}@%U^ey"^uqJ̿ L}ƴ3aۇ8}`)Dz_1]j+Z/fV>ėME>W2 IzE9I -\[}3$N*g!G94g~yXȮޞ-Zq:R&:xy4t dqǧOziրJ_~~S LNIбczl~ߣU^8O;bZqVu~w=6jf0Ej#<{8xBlKJ`;;f kվčaT,hC\z~;]Z Vu&`xWof-9*s:ڦI|<==[u.菄Y"v>`ehrC@ Bd]:%s,[`w-R Z{`ʴaZɇ-$%.fziG*ԕ#m4:ɛN+'o/Ğ;ɨ..yyC\>Yzbm[Z/|6׭, }щޮ7ŋ;G]YLupvw]D3ËN0Wmg.hzǺt8M}zxUS`3 f}N]=}3VqɈ?NnZ}æG.6:(X⽓qs[^9u(9 M Vsp v\‡*)X=%4*^Ew/3߼E*{IWޯ  :◣]6{@6]L R\{3͍ t.af?XS߱]\ f|;8O2خiL8EIL׹P(6YS[]}h/&F>y#:ꁇgohedV˔_?`4qsj E̗S'Ժ(ڢX(uTΘy pla_bOg[E( ??)W']ƢAP7ׄ 􄥕 ngWqRn\ZlWcx.:YkT(JOVeM| uT9Ax|j)$@Ol $&-*65yQI(M LL 3"5i:{:j^<:Cɷu1EDlb |K7tպ3wpHIdlb#e߲྽Ӻvhow#;İ?~B@_ūѵ.p;{)WWQޯ  $]YP@f:pԘ[/hR_I;_$ZSdGGHAPFyv&Ae篑Mg45)~V Tm#qVTz *MJĆI [4{9)٭Y=.1.ސZ˪xMm{16T~N22,%5! Y|WXXmҝcbn?wQrP[ OU:V\ 9E{Y4Pdǔy^R̪ q}р&)fg>`mϕ92}Ԛr=(dѸ×dϷM[:us}3`A#'.|q'B%cٖqm-}'wn<ݰsCcZPW))QPv-ܼxm]}^lF>%-Uk_'"d :',s6(КC^}}=L!^lRdzfܝجZAL Dpgߎ Y|]ᕼF(kʩ#u1=81+5 Y5zNj{wIA"*,MҔT˒Zxu$6=n7vЙTdy?t&-GU" @wgPڻq$͆⤇ ڲQz}dKL)m˲2O^s>NSߩڡ/,}wrL(SE;ubn>kwyM?.W7a9vvfF -j5alOˠ+[>gQĈٍDtcvNi\ë1]j0գ/92luߘlMCecRŇrY]ӆ53iN*I][>glIMY슐2[nR?lʕ]it_w<À7W~荗ɴĽ#:ps{;V|Wl/,~+pMOr=#5BD'066urqySeqHe4={ RηM]{wEK3r}6.bxe'7R`>TaS9tV3cPe?Ӛ+{&EWN WօYM~`Rnik΂לe; ޵mi[OǨ&xˎ-SW{-k®V~K6nc<_sn &@j7wN;yQ*8m&a@ͼ vX3_U*h;wgCw,]؇;pmZJNC^x@L=|kz៉zzQtNN1:jk.9)1)!e;[XVn`և*cxGCBs(eS`1aIDATRީD:ձZJp,7;}ۆa 57$E.Jl.?OR@sxeW^QbL+U$ʄbU;SIMV?!(PiqndT [o[[@x&n}=[ ̍׭hN#F%D__|̻Rw@Bku֮cڲohG-B_&.Rف tUh8v*#<6"BCΔ=?˞+\~*p̽:Kve,#;eod*4!]ұQ^(DsyCWqi)҈Bݽ )<zibVƻ'8[5ĸ_3;OytW:FF!PQ?(I?_~j݀@5\.enٽp&C~3SAESZ5ag9\ #m9{FkRj(Ƀ7Y6/^H}.$|(.3iD 0n(g)G<!TIfϚ%/iCr!F2?shM LL 3آP)@gH3.9r۹f&'Bm/aV joOra(jof"#A)Sص?47ZJy \S9'㊄BD_"R`:1Ye䉝mhsS[z-ckCt++I@!BJzR'[#?^9,[OӰWgg+4xA >^g b| MBx8BF}91g  1P}kE(RrONB.]:_9$ȪFPZAL Dpgߎ GCC#W<r_8zyQy]ڼ_^׫Kz %)-EC/ѵ>1vPy+׵XXe۱LΟ4aasv(CfB&' s(¡K.,$g.eD%5 kQ  mhLd-8eփs7(C%\V-'Տ*^`zN=&k\!3m|V}i'fyYl`#ϧ ,ò446)aXasmyP'rWnNP9 E@BjUߗ#۵qţ}gf/;D=BRN׿. @.|BUD!m BY%v,ޑau7gw]P4\&B|aXeY`rFDrE2@yP%#: f!$7 ɻ@5?Xًn{E9:aM$ 't ,@?s>Oʓwa>EU!TS ;X6GЖ1@GȝvL>B,yUYBHyM!T-!.3&"U~&E#P9*#OD!TmHBܕEzb`*PՆBH*$(?B!/G!aB!}!BB! B!7?B!o BH0@!aB!}!BB! B!7?B!o BH0@!aB!}!BƍB!e!,Vv ! B! B!7?B!o BH0@!aB!}!BB! B!7?B!o BH0@!q+;B(lFSٱ|.klllooiK=MR0 ʀx;˥1S0Z5\,,Bj˗¢)wi4gg;!ǫLTK'6`U!B:effJ$ʎӋlLR.x<^TRviJbBcYTve' ju(iT%+:B"0@!aB!}!BB! B!ҷjy{B裱'<:)6AE|.._) pB(kW,[ -M9G8MnQp(W[x!З@gI}O`I|~ [aveZ}E?'Kp4&~~2?\B?6ɱ5 JBXX d7}kѽ'oLR]jZڷoEP-ҊK?=r Tiظ!j :1umkx9wm ^xⱊPHۻ .2_t6 ncx}`>]{[>'+7[@9umЄJŭެ߽70>xu#,W!>C}&e35`5'#Q~pcd>f2m#^+ :sP~6(N-ڋ8<(LU!gdDF:gZ ݛ yF.MXrU+1n`ldyԘL~C:649w:Rx"PL+a 1W崣RnFBu=_rד OeIJF(XʤvM]luU~ BY4@Yzunc& Gv~A, uNB(tVL`e),%gϭ}EQ+8 z4 J 0)4@LˮQf O X _B!o BH0@!aB!}!BB! B!1Bq+BPx۫ݹPJ-0@!TTvelll\dPh*%OVť)Bjݻw Vccc}VwRR(˲JhSxuB՛Rʢ|@IEp8&&&|>}ӤLEu9MB$e_׫P;cB!}B!7?B!o BH0@!aB!}!BB! B!ҷ^BL&IENDB`././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/doc/source/user/figures/qs_apps.png0000664000175000017500000026077500000000000022006 0ustar00zuulzuul00000000000000PNG  IHDR6M;Zd7zmxLt $6f@ 4L$PLDDBH DL$ek֓# ڂ!O O OYn,cKd#0llllll"@ EBH$ 6B! BP(B@(N?W;b鶡 zSB DI:m *^G9i &I`lIb ,tN( tfY൅fz]i@ɤ 7 WgHd$ H`2[!Y l6w_b6@`h'P`k#Wۮȝ_qdP*9ݙE[L$$ f"RBMDlccyĈDn/]o>OϜ9eY"cdÇK/4n8ageFϿ~͛oAD_ѣGg͚uB!^| &X61qoDFF|eG@AQ ך&鳟|ݏV{p9p7:galk6OoN]:7kv?"\KK ȝqЏՙ׈daץD9_'O@4_d$Km$ i`er{D?XghLfĶL}}=uuurJ;;;ˣ駟׿uq\HD7ny}ѪU SO;:: wߕQhB@h](}c#3l8q{!OػS }bbk>{\!`a&J8hPsglke_} aAw^K_wNMi\ ٚ \EK -6|t>ڼeK \Ɋ72/u|Uˣܙ_, 6l*(tMO$yD[h|cX5LԚ;fEJ͍dz)nv,}(vQ񜆈'z aK=.}ef%{6t]#pʒ3YKica,XvwYb^V;˷\,ޜ-9wPYecec,Wj=%X?m6Sw]FL&ZfKШweO 60=/xj"H$D4nܸ Bѕ7>c[[RtwwJ#F .tV.?SfyĈy&Z"""HB2@(q+%[ +Yl"OJ <4 9Q`g&}.;WTzXIKS~rvq #w xY^S1ę,^,Mכ)|GZx%kcH]AƆ:!Y$DNDo,XZPfo.k$ KmɊEI3zzP]#+,ؐ+/6rck37DW䪔!r"Xv25#wTn*eL`eB?_ynff,SVZwË́Lp|]9=2ξ1)+nPx1uZٛao)os}^^fh`p9@^יJn-p 8N;Fzu݇RA:u _<™+RegqյWzGE665vT~VzM)KY"/"+|Y;[V,f KX[V;=zAŊ}bBK0[g0h066ʫݠ]rHg`MfHonIHW 6N~+|r?keEejb J3Xv 3<<9Һ2 y3ҝ!"yȌ0\EHLDP_Vin]㈸oʭt'"r dHԝ[*O5:X-c%O𺵩iCWDjNݶU^P5.{m+ hfff3c^3,l红sj d9b^^}(LDĺ5 7[x$wH4iX9,ͷovfTOp,+&"jKIӧXM,}xpC,47shYD$ [xSr4X$"b|cgm\mr<75R)k$kO^gɺYZV'9YȜY&jb-콫j._V>hR"x@+鉨tO]MQ;z&Mb:%ƙ\ GNs%K[[W0{ط+uq2 +orD,9Gau~_dR(cS7rf:9>%>_^פxwi!Oب4̂mm4:+@te"tM,A.KSCs޼xW]'iƞ|"wENCl@$"vlgT~Ȍ$ޥvqSvY*Vo}%JitL,˷p{9FOC|,2kGb Ֆ;<=+,vF75`Kw]͛Xβ;Kv-jv ᯹eFޒ^}$ú[CqL-X[ghPV#M\Kfg4r;~ȃev]]VP78pC uukwF#L@Si*Zȷ{3!Lj5ߊo зԕ7ufY\|k!)9L壵^ ц~#%RggJ' ye2"R*gJe6? 92]__OD,A3f-SD$|Zhjj0kh8}b)qD^q=ӗGmc*U.-̋.U vʒqw>60Ƌ+=hYd9 bDUUnsp[KAExeqAΐ${m]xb;)KdE(`B =yU%O{DGŖT}~XwMű:e{J9d\\BrX5tliL4uFad^{8W bDBDf]vc^, h?]ui7H<'%zڝLFc&`}5OܴA:׃y|Ҽv%kYjѫ#΋^+s  Iz3;{唵<j_莑Z^vJ aIIs/yUDĖS,O1<+ݗ!I|آM[wf|/ .YS n3Vݹ \*xnFvqxnUk3WO'"y7DzYEZ)-lfWIqdG)ذi͝f Я{ рqLqP)/&V whYؒ1w GDW̪w"NON< {.c5Hys8C4:11{)k-O%/~V,[%'/I(5X`Vԃ7n~jݬg6 d"O ^vs!2&Ddc?i?H&בhufG\͡C?1#4N'_Bf+WرrZvss{7bRV2dȍ7e˜觟~ڼyJH$C ikkCbWIJLz$>Jj|d2޸q'ƏDd2XHB";[,4Rbd"̦!D>O vrv|B1b3wt2|?}h|fpvv;q\.qpp,4hBq%"%"eǎO[S$gal_=4n :vB2ol4  {s`J4:t(n];lG}B^#|B&zkȬ7m_&"!:g.뛛W]7dS[xF;32{?=7U8/+ʙRZ?bxG;o(iV[]?hy"zqP@2L0^ h$=o3"hg:t@t(o6-ZƧ͛OkpC;dd],K;ScQrRƍB0P@BmsP@Fd4 LZx[))"L<9{%V07 Z{uN 2+f"A'sW@YD7>$4,2+:L&7[·lfC}@,A\`mtw~[o3Z.Z=,CԃX͐'I"!lDm\D6C]yETwS }RJ6"1CvvB!< #D O O O O^V<=t 0o4u}[M&d2z~xD0iD< a6Fo<xD4fh잳PܦE5'K&cݮ< +Ӟ?ɾLُ {J)NܚEH=I/H q۫^[8Wؓ==8 Հ(m̿w0fEТOF)+++++q/7'K7t[`7cCYlY%ωo\:qR3lBSbdqy!Ӳ8jD{""E3GN~Rn/H!ϞQjf䊌];jؿlyc:/=#Is^gZx"v 2'^QS⼖ թ3_dm<$9')isy͌Dỉ/Q i/ 1cS&z ZT4_=>{[ikG/":H>bjRCpl Zz}os/"fIǺ.KOxcc<{v}9݈ewB9r]h {!w? /qۦw5dޯ̜:q_~^/s÷;[L?1&&Wvd IDATVqn;N?g{’Mxgj>1g˘9#[}Y0K-[vYzKnza_8)]vˎg^oMF5=W9qç 鳬otұcX|+oMɹ02cϑO>9vzcJyC3'qdne5~|ȑ#׾Ο8DCݷA.^ ;=¡؉/It暈?O7ޙs/W}JɧȞ釚^#|B%v۷wX墳ۯm"&ĆZ?e^Yv$&dZev"\muaη6wEEW%1MZ-E{ޟRSS4 M\C'D{Hcxw#Y+)pOSa%MLx/o ɆGM$ַn"/(i0d~sf#jorϞmDR3b7޿$vxvN|CQID4r.e+ǿ>BJDQO\QNDVBD/d>n<;=tA>>)L>8R SD*!"Qcsh~JiIH1~/D"fĴ1.DD;˵< GyoX8sk9bG/5vNbmʞUg;N+GШlqKGN.;zA;B<~<^]8j㙖5OqcM:f}vBnGDfEmKLG;*m[vM' \v# JѰYbre_׊q{c .1n߳a~<ݍ@"['ة7U^YXp:? Km[*~ߎ^k\ǚmyGҰsJ=wi|R#3N􃒦?]tH9|{iץoG;=Zw>3ezل:.' !"ua-!Sºt㉈VޙY[3qJDD/7;Vѭ`r10k?1cmݺ/}^z ;br|Ҋ>JEcmF9˨jy?x肚'"EM#{΋WyfDHX?q<3 ۹Y/Okjmwy'_2"ؐYU;"z\H*lyϔZ?U.bxe6㳣L{խ04LDDDd0tLN< Tjj=rݎ,sfW9FΈ)'?2vmNnԝV"C'ꅗoU>7OKxImYy4 gM [ow^\X.:e88޺zRqvOvN~dgj5xAX[C@$&"tݓv?MQvh71e᳼Éʓ'މ/g$> 5YYYYo>z`;lp.wmZ"Z~#̎CnډHs4;111/J"CFeU*&"/^r܂,X:rKlcߞ?>\3cnQH.c&:?i)V 3R/x%'S32~o8ǏJ̚+wz|+7'~x$XV?-b]FpUP>ea@$r1uh'"(0XaIaR"r zŻvߺ@$b]:[,1o@>3n7!@/̗' \^=Gzt?/DnVR4,bFF6םZo͗-*PJciN-iE,YW/{M!;IIqNsWL1֏ l2Fhy^566~_HT_,{O4~!M_v%ߣM3hР~E>v҉~ Ӱ1F;Z:i| "4$d'ӗNݰR)ssұ 'RfNĺOU𰻧z'l~w6{N7slEF"L#A=%YhqxW70Ʃqbb1f^_0H$Phkk{!Bmlln4444cBW_3OKhMfd4F#:{ O O O O4444 O O O O Oߑ_3vm%%&zֳ'H$b ϟ>V.=oo~lOv5w,qՔ,9QsG#5.<!YKF5cGFG?(Vl~]?ʕk瞚1jEu'j $ R>2'jn9}dGhGF"NÃCbM^6_@C߆l6LFh4<CCCu?Gc~ǧt59R3ncINЭM]@Ą+ *een;?m}ʫ9{Y&8Z4>bg&H9c {?N&M]LDA%麦S(VT6XjW{oIqb\z]]l9 |0B y(ue{,g˿Bޏ_*<i-XUQ>P}|WnhҶ5q=3p恎7- i\^"""64gd$_Mmo "ӕ!{{3Vįe٬̂O{v75O_,ݟ@DAT@P\s1*Wigz\"\ϽYB=$6+J+ϊ.LuZ׏%/H?1ޑy3|s;o j(JYHi($@륙 -CA+ Uљs==|l .w&z' (<&ngi)gIC7jur.j -PUkuQݏGW_\هiЌQJ'͞*m" ֗ҭa%v0w,avqTE9qiyTwm{,Ҫk z;wM܀E\?5EuX}lfB٫%5u}3WᩤQRx9J)[\>VgdT$_n% e(::mԘxG99dSߖAy""YklTTRΓp Q Oq]<$$%M`,3>SW{t>2λ=<֯o *]T$ޑLq9OUWxvTP?ۿޞjUJWY*ΧL$iD !ǒÖtYCX\S^8\ 5 1Es">J}jUU8x4Kh eK#-vi?Uϐĺqmuq=ytkP (4#֑z[Ijƛ4gAS75PV?9~aZ޻VZn/⨡lW{ 탂\+ۛrE OWU喯y VcڻmV+JK/UuFkE{6fvN@}HBfϝw3_]ڤ $KTD7[K9k;P*#<7${ %>TZmT[ma/zMZ4iA2(.*VxV| )u*''(+ c:M$>ֻTVҩ:G4 2.ukuӣ{QUitUD=|Vdn;V&rtwZJ="| onPudn7DV~bS>42ɇJQZ5t u!(muOߚ.'1cBP˒*-^ti_JS6ԷPKV/jYn)dbzjE՗[2Y歠,x24W蝨UwY]fޯKh=6#pKýꋊ,= j+jjwWhoww]"FP׾"prrqss]U?GGHs9ͽsNR5( ]2WmzqoY_K\?!O{UMU 41?޷8Jz$oɫ4roOўۻz"N}bOP)ѸG##"٘oݗ (#C]eC;7U690ۧfo;|SV?ZQwESs^K8!:B&usVm#r0aGHGҜ;ݙD^qcN)%4Վ\=n[}B_t$C]eM r 8? t$oY^Z=6vq1sv"J-1X$Z"ҷkۉi_vۤ1L&""z iXף5MW;5O Y[:C&<}wvR;ȎHݪ\hG:9>g̎?+q/[sڶNKH->rZ4&aO[!DuuyAa#QSyepۦ#ʁgED>wAx𤆆An{"hi@WbäTBeD=ӄ% 3TVτ`acQSH]myQ|A"ta?v.x{ IDAT#l6L&h4ythx#lRs'$0 @=W_!%ll@yH=QzxKНZ<ݢ~ۤ*W\TŻ$۝3+&ԅ񎌉5ɓy-qk{zj"%fVLk9Y͍'JVqg] 3OãM[]Tg5^?\"v\Iڪ Wp lOjӏsngrHW`,Eq{q3pjp]_O5=!O;'pm {CV-; ;O3}ѵ4sQx~nӱ.η0@C MpWIx\‚ o̱ gi! Bn>rzج_0sѲeK 36]ߗ?#Mxikouml7.Ds"\]O!xHUFM_4xiAo@x Υ{'ܰ|I8(aQsm17Nh*NBӓQ@Cصl^~@P‚M|7<`[2ypzx=Y8=o߰xuog8mǂeAûX,Ѱlr7 ? qL8B!8a47kԷqkұfEO&izw[73 Պ]_e!Ѱu~w:,}_߽CwYXLñ.̮:Ba#;qzsw]?.Do3ĭyws7& ]`;cr4l?&Vf2y f?] 7nvY@ӮcM>A TBoi58-0μwED.O_v .<̳`͚irfC9U6^>q78u ܾ`{ |qihy᏷y!0O#t_5+##2)f.42hթkbveTnB{E[Y'lҗ ?X֧]Vt  Z7@q7Ͻ aX;=w:)(Mnw E/ ``"iBFn[vo}Q/l8|>mqz5;c`fOX\dKr/=v,w098d 鍓W_oڽz 1\CCwpv&?u{fܲn̬o='jv‚6tøq~?}avV7gMoްw> 6hj0klA8~—N\0װm+;B! 4z~e=?ppQo3-9Akm8|xcg/gW/:tKEſn`gLԗJ+60s gXU>si^Ŷuw_NLY4̗vNn.J8, c۽%;¨Y3 ֦s`[=pݩy܂NMMVWܦĎB=:p㑱pzüݶ}[_u\B=,>\>6`ftiBBe84B >~\CStOqbBBhǚ ?Z<4B >/<"B!,?B!W=/'i3_퍝*ӡ[bƲ;ڔSJsWsO>O.0/}IUB!t?ig@Q]<`,d݉? 4B!xJyEg]h۱=5tR'ӷ>]>Ǫ~f4:eѓ{id?s>dOgPx.q͙9^8҄OkxɳJ'ߣ /=?!BhXm;iNz/}ɜzgҼ0lown:fK/'NaMU/k*rp)K>xU/̝3~J|=4.;|tΛ{n^},:-5}r3!KR^d;ﭱ8!B ֧] '@'`Rl=kh>@- g{.>e!Boi[~/ąj_>~?{z7*5)u,v t*k־i<⏒'w}tY^ yMWBgy[Go{8_^ 2p_OpTն?HB!ӷ>P,yc?xY~ ='><qnPۭ7=(|p_zK=eɤr;{4\xYMczc"?70zd=}<}oyn#kh,=,n኿8ZZg(`?i+\.EQ$?=\7F$IC<=3rp(B$INJED!}8j#iB!0O#B!y!B!!B!!BaF!B4B!BB!BEG?kA@!Ѓg̘,Kx<( ?!BX!@莔w7v7!Kk7|h׿ڗꢢҚN o`@85mR; kQY+YGnٸg}$+(.*Wo).-##1_˾ 8%$gϖxl5A[\n}|zs-8;9dXo 0UL=G|Pnlܖ]iNʯGel.۱$'FܗY.E6{D撲Ɩ5[i-吔WVVRe-.m'6֢K.(۱lƢUk̃};눢xv.Pjs`|\ tYDfZ+3W26س$+]_k{mq5`謩n}j,uq&lrWVXB6Qʬ07Ť%YQBv>0wl<4i5׋!ITSQƚ;7{s^j 䬬ҖhAXbM1.ZkKu7^cCWn΃m.$"**6>iw@h.-nSFD@<{}JQѾd0 .T_Sݺre(t BXh/-}Rsƭeޘ !yUΰ֘[ "+1QLvye{>mL!FOt esi~}N@m.8p6rHo~0ƚ E@h;j&*>dݴi9j"o$x]~ԹmۆnJۘ|I+_]nkklܷ~߶WFj;0F\;VGc!CIܚ֕FLK e y[o;zm 0Bz|]>)-3nW޼|s1 E-!r2LLjK;V{ʗ^G f>گo@SdqGuDus|a 61T0ggo-ߑ6^dFMOkWJbk[BC0hUJya7 !iRݺι-\LVkK3s6D~HE5kMe MZ 606]60&"ks~{75 U|/>ȅqUZ,&mRDfm+_"%eMEƆpmVR}Dss寯^#Y`ÖlV|`Ӛ%u[Ӕ**mcõfo:nY[,ɮ/qCY`e|_a6Cz4$m,aJsWlNDlHrn_I>> K WUFemNKĹBs 8 [!˲$I(O~… #F ɯo2j;黻;N{qMc=pq?t/HtʕGy!4BÈ>"B!0O#}疓 {!BBGȷx@("iLFnB!}wyDY$I$ I%=u_ZMAI5BݻU}d dy޷O;SR޻Q>sjo9qw~@A9eqNܱmۙvg!&dH<ݔ[T$(OX?GG9h4*U*BPEBfB=ynZik6,ڀ?EД;.u\r+"Tݪ cpʥLXg*+xw;\CB_N W>Ύc] /E`"}qOnz V.{h#$A0 EQ M?9mؠg߻+ 'LTxb J  d7Av* {` BOXN zJijJV,MӃ;"B!* mǿ\vwZn筏wYT]NKMK8~U?hGh]on鿘F7lFj}@2A.Q2#G~pYA*{sc!%H H_  &CȽ$qE|){R{l~bB$A@-"o!B_=O+Ǜ;""x닚78RWxqc5 wC9e鿧+KN)hs n`dHG? . WޒgW.xrJغѿHz=H$yIE})CL5!V飭bft'PO0 @|A X1^~Vl$%KA$IЌ)v `o5IفBaդysƟy-8=-,H|t7(L Rtt:zÂ4?) w;W,]ʙU;OijvN ?Q{K{w>E7mvE+{rYfKpȲ$nG?y ݣ%GV HŧIBB[Fp33?Ld%bXDN œn$OP2-FPr2>rZZRTJ%ò 4MS-k,3sX9#ƍLTKSw gZ_OYQ%mܳ>ZqtؒwwRV%^;%Rl_ٍz-چHZqpgIJՅE-R32BYOB=y@7Ǚ><~x K?3/̲`ɖyK%?1xm^U;\?HcZۼ 1;;?-RΪOnPK\rlYlOWnm/Jq  YeY<GOn'EKp & #z.VHB (9{* @HHBA+п{͆k-vQ^Xn/pƸx×\\\az+ܶ#[N1dy^Nrz\99ɡ, ncJI fWgִt\⼼aZp46]DrVVj7Y}򴨊j6|g]oFlyG*c'W4++)_Oɶ'GY̵ R ?ZRXN2wۣtY;6ݟ C ˯ {9YF'apG۪]X_8{Ct9;֔W)Z/Wʬy kbZg0@jDqq510({+R+Wȭz'Uw0FXKWżlh7g$n`K(W.6-;뛣7\kpذdMQm򖘶b>dOR0k;~Efqdžo,^ ]MoJgfIQ`=7dTggVqY%{ :sEfQȎ! ƺ{É+[HZ[n1&lիoB軑uO-Aǎ㻷4(潜2 $ /n,xIYRq_ٕιN*"C$0L,2 2Z} !K}|Im#U= }ঁ`"r =#$_4L@Ȅ ;ۤ)$<\0$0R^|)p%8o S* )x(ZlcRr@`n L΋:;5ޚlzP{e91|vyվa]\I 1*JW|͖'ZКbMLm]4&e 4uyeN]Lj!сL*k!`v0 fMu}Cl}|f0"| cc;a.ndB7CE;hKNKK;+[ٛc R#-Ę`L1L7P[k+;CRc ;B;AdJ)BZ?6 IPqeu#F>uފMD-2 2ed$%SȱH2r =KJr_v+J UJQi)# Е~Os qg/~{AqzfJ*)d# ) QFܻ&yjĘ!hB[uv 4XGsEy.9õ笖edc+Gu2/%FPL{12d8=*ĠjY!V[yYܔ9<p!2rVN5Č늩Dc&0|%i"K[Xj~NƼy‚3"cI+H e&W*XjNu\gE`M1 r מ_d1.=x:wpb"1{#aBA*зQeI<EQO>݅ FA_dT sw]Fx\nt:\t -n\v*}4S"OB!+3BSz hOkOq* ]KK A̒KK,I0K?s_h4Ձ FRU*JR_ہ)e$]O+W<#8o֧OlJP4M3R~G+B}G:K$u\$IjVTQ4MY# I$y{=eYh4c1,˲,VB!GHS"Ȳ}}ͧNUTTx+)))cưˏ?:m>$v:'N?>L#^?˧>kQU#G3.QeHL*5 0,9(<MQ è5RIQ.ry<o v\\I>dJK40DZ,KRq:<ϻ].CMQ*J("B B!0O#4lRy<-ˢ,8/v]uttlxRd z]<{Bxzzz ._O'?7zhrϜk///?q,&g?tzn%v?OөS1cƢEDbcUVV~կxQ=z4KQ=v]:t萿E̙1cƨ})BaFk$߬>-y< ~d4i[['Z;:N}~idd$عJA޾ɓ$@'֦M6M0Ǐ_h`[H}];ee?LIկ~%5k.|>]f̔=omܹgWS^^.KZr  ӟ\.wO>֭4I0WoN{qM9S29{6D>J))Z?t[GlW7]*n\yc|}&&lÝ@h?PcJa|յL\b(.^BajYn7-#GO> 5Y9ѕng($InwퟶnL\rek<;{{7Kz뭒;w}翸pAp:_.\ӟy۶]6mh4Ey5K^y7S/&~_N.oRRRq7쑱c;?/Ku7xט9rȑ#ɋ,=jqlXzَȻ Ru ^yGA7T`9Pl୕|ˁ6]Ǝw7 Ke~6sqej! 4Bp}}ZAIt:{\}wn!70Iɲ yndo7㗿2ET?- .\R{{Gyo;mwl$@_Nv\xXh2Ni~4Eu_r'}$_V裋]]oKؿo9Rɲ_|А/hhhP+өQjV<-""BsBݾθ%ur i^Fh\Wvؒ_0Q˳BYx=h_cTZNN\g6;d.\qӵ+VyL{u~nq}i^V%''9Z_Oɴ&3Guvڙ7in=){gKIKWe--mv??Ye q\+SM;gvȑ#O>oo}}}~~~AAAEy ?>vL fϙ#Ę@Z$O IUGEU*%qVt pnկ^d_@xW!A/((Z ʕ5ƼMzX$7?lQت3Z"(dڰNsf~KByժbSYNN\l]͊BcQQ`[a,6V"u8rv|wx}ɚul[XRdnY;ݜ[k+ZͫVeVrse~.Wc]kʒԲ[^)lٸcKVh\"ȴc]@g%mG Ew_$te^Z͐Q7 KsC˶$aF!=wADbNZN uuuIOo_ڻj-|?]OKEi/^ovvvZ,LHR&Q341^uKA.$ \&a#DE rKRIGްa\{\R9U* k< sq# -8Ztqz}LrDavunce 7{sXC\jDQ~%]XiL@h=@‰۾;긘U'ZКbMLm]0g4r|h=SCJK뭩nZK`6l IDATrR䐢!aeb ;k#U-$ B4B0/%%߲lٸQ׏1»Pe:uJ=o,ܰtj0K{<˗;w=?KKYEO_ߚ+߫pݿesɒ%}n,/z*$EF7GOXۯ^p ބ膛BaF~uӒMTssȲl4G5f q 'tFVև~HD5zXEyo~sȲ3$''?ct?XT>s\.ߨQF#EQq/P]]}+WEEEyQR`Fݘ1Fr=8ҥKA a\[[{SNy<#FG=jRE*5{܃I!|qyuZ֘!"'&{.y]R>5'54*X\^<{u^yG:0 7h/_ۜT>^zcH w}v0XGsE#a\!*Xl.oY s~%>)V{W6^ ۸t# -7DfQFE$FAo[cy-oJ3aq!0O#]뾖$B:~|oxXh4j!%~~ߟ;x===,{u~~""V' eYRqZ-0nZP@EQ$)?rs)T*ZPhJMddIIȒL$A Ì1QJZ`A_EQ(ZӪU/BB|.mau5:]`DjzFLvnƌ;]zpj^577e\ȼ[4/,Ȳfϟ#0Qa {jCU/uD]D8eN]V٦}=z .1JX(,$IQ$Aޔ^{J7x2I2,EQ4M$ ,IR$If΀!2S*QX3DBH`͊يiell#V|`QDIِ0`=~}@PPj/} >s;rpww; ަ{{;{{[o;E s;_O)`B>;ۖ89Ъh:KOƲvvvR t|=6lyY6CیB8ӈR}gyZZF%Hˬ{Zkt/e Y0$j|vmr2zi}1ڲ3KBuMyv~ ퟚBƚ>-SI7埊.Y֨o+ݘҤ;-[>'?ڶ>QQLc <=Zgӛ{*\}{$%";#s_%}MsF>P~d➻_ "zxHu3I#S@ϗ{<†..Z:-~֣)7Ru̼QMy{bވ0d }ѽ:RSRxC$<>M|U>sZ:;-.Dx:]=o xޥs5Ke1˺k>l2^g+nbj))L|ث‘ghl0@#۴3)nzlIM%'qs&'0`QHDdM6d^^z0P?jq`9 ؎ VfHjj B:2z6$C* 0RgMi@DM`B)V}IJRfsdn-fܫ, jǎs}ؙ2Q׵g9=g\=3g]nb٦.eHĞ!!3;.rD""v'"q!$"e9XȈH2⺨LD,q2_qV[?KLDNn,q\ר8N\S͙/qվ~2i"";oC>{<O~j%""ŢpCY.h+d岺GY"bWϷ|Ph+~j/Cuxa{z:!#3&+Izȉ١m$~8E~j""C5`VwQBX{H4tQăvsը3=>1i#k[uXVt+RD"xCI$1nomu&%EnD"p8sM5'Jw^}1bdHݘ/.=g[lJK[a%"WTb`gx8So]#Dx,OH_e12ɋYH:߿5Ϸ[)Gw.~# e2w}+fڳ!wdi}Ջ.k]wxX$ݱzv3iN#gmݍ!)v(NFwpAeNb|::.|南;P'XyI͎}}}}}} <ҥT˗g̘a&ׯ?h p//^t?N;гƌ^mOsrsn^wc,$L<>9q\ < 0(ֽ:""s/_ؿ蝃[BkoozDoY0PZZaE "Ɣus%}V]x1^BԺM aYԖ~eݢS䒜EiOYH<>%1SBg2KYEW­D1~~iGm#ʼUb܃ǯ$8|jOi}^-C[Nz׽d%pLGwwO|+މgJ2k8"bZw3q=NO8sg_^RQQQqjǎJ31$?n OwKZv8Smxv~****۷QI9q_E%Ҏƫ"ÐU0[V|^]vZ̕;2***{{%{nw+A bF-i->lNEEE{ojNT֩Kыӣl57:.Xq`q*˖ɃӹLzHEkڸS,Ƭv?LDȂýX"/p'K<,1(V貶=+?E1 ""rs])!*OY- {(Wwt`IcSmwjk?鰟Q4oٿpR*}h$S3bJRS_ jө^ {CZJ c=oHF${]FJU *\=P193x]̕[^-+r!?p.w˃6t󥃇u_V%iCi/KG''3cWR6$^GϺ?jq `BIeRjD[\jXRj/)Iݥ̢䒬ţpmcV|;""F*'"[RWl!۷N,CZ`@ 0f}6`xsk.I F|pL.{Șh[G;7|[F6v=f%F~'Z\ѺVZ/q<$?T֑-)?tupywcEn<&GyJX +ڽeKgIse #>-%,<q+OY`|q_۴x;zO̶y4UW?m}m:H^se|JJ^.+Ο߽<<=V"F3y1K<Iv29e1K agbvk/MQNݫu12J l~Tr\ ;79-3{SD6ISDu4O`q ;2^YUz-OL_Iϔ#Dx,OHБӶl0ĸǧTJz3E֕oWujϣ&@_____ <_t)001w3f=ߘ~% L _ן~i4L7?OX=y1"4=ɂ+ 0`,)4444 O O O O444H&O/GMa[,Tm!k}py?i9{Y ف+~8]{L)ѳ?c=GDY쫉_dtSѶχnXb?ѕk1KD|!~_χ?w> )7DD'8Yx9 O|?c""; Ks uN1V*zuj1>#"]<#'Fr\1;?*Q߼*?Rtp5Y7ʿy71![@'psy! 'Z#[LD3?u/DswϽ^3ܿx2}~:)/g{vXjᝧ_~>TÝ"""3.1U`R`x4/ə̟1%DDIn䒁n (MG~WQޑ%KW)ͳ_!N'^w9`dzyb$Z/}m ~'vbfc,}.7oT^g(kjߏN5g^K:m_3Y$k3opY٧vtLFLO_͜?O#_g,e̓ 9Zz?|<ç_;ؽEq~Xtht]1WŒA:m1Cߠe Whx!Zq*,u>Ϟ$̊GJddBܶgӞEt?~"bgIOO5?%fy"ޮT]x9_bFM\zîo7/iϒ/[Ŷ*^"3T}_oMHw[{X}>r[_|w?=7-l-z=+!eDc+;WcH$Hefg7w">_/ IDAT)/|!'=~:SSI}AD^pK;H2}췟i](׶?v8g͙'?y>ҼO;kǂI}?wfH8Z^?KO޹_g3?>k]"ttz'͹ i"IPjYAB-~ٳgϞ(Hn;K0tnxߙ:?o䈝) pæ^"cɚn? ʨuSmDm %sc"K]Yc]]F]Wj?l4r"vvȋ1q&ROWI|0EM{tŃ}5Kur`'Jz,[3oWK%ϖm :}D ߢ% [FXk[RMeiF"TGru^EkUHڸőEEZ$&TwGn*3:/`"GMjUɥHW""*(.#em®jmzC~ KQf:>5?QY _tFVˆ6Qdcv@*MBN.zU~kU-OҠ)ZX\?b6 ج >#2tZӦf$x'Ff TDD-8CBYY`zil.ֶu]m$nCK}Aeeˋ z(A$XUӻd}2Tzq6QWRx=sɚeSUS/9QWCIޑϯΌزuSaܝuo e[2D$ӣkPjidnZ\Z}L1uTgf7krO+ۿUnh%~rT?IKqbqUc7X$iϊj3 S{[\*cvNeR4w:#.2,L ZT5ܝ.ZfiJ8rx dN޽lGj&ӥR.0g'qPKzG.eZc;tUǎUnrIf"!H_mˬQ.͠:V}p"e?*kmCOF!n Xjpt<|ֈ[~ą̙SDy XQQ?g̍{g/9.FGi;.ui%G%s<=QŚHRjΞ={"+82rzURW"WMws#"ADUKojkVyK$><}PeREG$D ]2W7Go! %,V!o%"Fb-U:Z&4_vd6[;I&;9hD9@r)aU`愻5~Cy|UUQv8/Ru{JFr&y \5jsi#JBTmVh~"r ܠ -zC` 2:q&-Lݙ:5xчX1F#{^LVLDĊqkɅ%FDqD}6OwvhVmȞB~_ӫ\%_ "" OBǴ+?m6D22NךjO 1Zݡ#g#T2Do9kە#}W9#o oODUe7pEy-i:NkbUۺ%!# -#gS+<;tvڴLb|<" lm[)`A+9~dKX9#4s>T.I|j!estS7SK|ϞC">,]\~FDž]+}f-rcn,fu]!fYha.PeZ\qF0 ~j5HFdjZjֶCjӉk 3Alal91)#9HBęOR%+}1z*ZU^Yu4?CҬUj4]cZO#o}Ɛo.|^qỸ-fVEDͺ>Y ռo%Q\`ӳDk``OYq\5 mzCN(* [I-<QG^T.~]+3f#ODڲQM6sD6i扈Q6gh\]QYtnZ9!eWz}NoZ^-UfSr\?#"$0T%!xk U{Z<}GV8q?*ñ`ށD^3->XGļǘ$1$&99%me@=X4k36 N&UiN+K0H˘ZaþE##q`*XRiiiiqFG0,{ҥ.4<;+WHR4<|y1 'hIFTƦ> z)wwwL< `oo;>3h )M]4޻NC#LM@<"7Sy}hl q(}}}}}} <ҥWX,0:d'0QpCS0}}}hIA;4=00L< xd2ՊLVL&CS4c7mڴӧs4azӦMCSg`jJeڴi"KEď*] oho!2\x"GLDvVUL]O4],Y;͎yU' on-KuGJ*:{X&F{[tׅ%'D"(7qOkɛygg39rT.YILD7MʅQ1s|}MlpGcl̛ Nh=i[zvFnΚ[ctBr."iK!ZlSȦ[3XFv,rY̦5 GZ{D"1QwS]o[=g 6t]}EٺE{+EDyftc=X֑qL˲<=}ƚVr[ g[{xX`%q$ _6sΒ#hfy5>"jچšGD,9!sl5"{z><:??3<#*Nxí|"wgAuZ7sSCvuvv98ۺrʡW;9S'.mb뚺%`.XiۇYkgO75ؙN.ND\gW/9fx 'VDtq]Dd<KNDD zկ<_{wmY{kF_ϕSUmD9Jʼn%x+ExΩ#e$s_ܪnȊih1;Ӊ<Q/w#rtay戩t9NbNDʨ1>CE.NHn`\PW*o}mS5~XQQ8qDDs"9K<:?`\*[PEDd&qgKp43dKYm;U3[XswdN VQRQeC"K2Pק#5 +-H$&vId=k亮6-hµ"u/4LYy{'qϲ5ngly3n>AyҥKhxy {$&9A::::;;ptttqqquuey0}…iӦI$4|7xy ̙3#54LI"mԮV),<޿.~Oj{j6kծ OC+=ǩaU' X4#*ޜr";JG4vs+ ?Q%rv #W=ѽ64Ec/9k,DboI;|ʕ<_s7{l''[WWו+WDjizrSiUEDuc4ꓒM:yv8m%&eKu~ !zThKAPPjt'Xl%^"oӧOGGvEwtt|4999͚5ŋ$O P\ef5m # 7qq*w$$Q e Po݌Hh.KWgl%Ĥ1zǥI*G\ ^kM+Wn0}pdG¶m{UY?(T+XaGqRŤ] RDzI/<** ^0 OQ]P+H5,7:TN U[&k*RK=kGf"U#::6+%.LelUJ-~ IDATJo#Wx(Y*"6?'QQc֓yMD7Л܌ deu87ɫkr"~갸X5 H$Do]V6,,H VPn*3F* >V},;hj3w܁u"dC.O ڜz$>U 2UUhm a+qssknv\X:(2!+{z2蔸 'ls'Qk:91GTk72\lߓ]+x'M R"srWC?㟝ӛ%Ƚ}TJ )#U= ye*h &WBD$ֆʩX2j#:{8$V"b{ԱA Ur"lM$h"F#'SmT_o%Ez3rA͵3I#o՝rLÉN+s #*W7:pJNKҚa#G5(Bշ>.UD#"UdLeCzwo[&T1xxcu@Hjx7R͐^xj+g_ݝmc'[jJ-_UP}Lu<"L)=V*lfHyQ 5F$,CDf3w;pMey'[yLἹJd.޸UM -R5v[NC#.aUR"s˭SSJG}uFZ[U|CJ-U&R'MYRCo 7!\YUZŁ8Tl2*F "UDfuh ϸ^k%D̨[wZ-yﺭ6FH~ut֕śP0e& *߶| Λw0ZF'f.n"Z cK&'UrVBA1ecפgD*Gg;FPC7Ì|VvEZ˪LL`zf5|+h= X HC}yIh"3Dao Kd׶}ޡ7c40 fRIOEnxxҢ*zݱ ҅e\}AyO~T`B K_qcjOviaNAJ,wW+)7zkGŨȬmGWVC}dnn2I#!R2GN.RIe{͚U ?`ꋋ8*HO*Og7kT Tu Y*-Lʠa qKvIHbqxW LۃTўyIF;$Ӊku Erud} Pj6xܓM0U'#IGHjkNY0|Q$էm;@DRج͚ VJkUfp@e֛2IsLv枂۬D[5p)b%S&%#!~B꭛yUvTS0[MfWO.}h?m+ݽSVAsuۓh>&!x-.$__隩|xOS}@y[oD*P^|dhHlӶO 8}Z W)cmQETzv*ySe#28K0Mx&>GxǷ-_Vo/ZuEO*{)=6iӇӁ@૯JJJ;~vGbߛWOmrkՊz=<U?ײϲ"g ֎mMi7U7Y8/?[!U M/̳sAl;UYTܬ mz{w;|͸jû1 4䇋/ͦS/YyͯKի2wqW=4Zeg$%wt[o_.3YsTbWm*=[GE9.2՟(FySJ+cXC;rOD0Aӧϝ;7<<yKLKLWhOmhhhaFN /~3]?~Iy9%?[DDą =qňFFF:u~ԡ{FFFޖW t8}tzytq܂$]׹z/RHlirN<C<}%HCY۟yJd)]zYl[^8=.7l,?mh<'CƢ@4Bw7NoGO>8xu%K`|2^VPJ5u@+i]I/ ^˪<0I ُ68Wr޷?[{_,Pr_L?[P[acjZG疼z'"B&JMq};p/kj;~b@qNԯn.ܵβ$_l:2V3`IbFLZ>׻Q睟@ tRt$ ٖ_|x5h<B{=XGۏ]~ ww̖,YRZZ)dܑ}ۏy{7* ̍}lb˧~ZAppÁ>sZB2i\m U/Y6t01BaF} }^?U`ɂ]zoK,2)m+~}DQEQTR  !l:0OIj```pp(TLzptADrG@d!BhL&H$"@#B"HR}>(wiZ*b6OӴD"!I<zHH$DBt]h=$ 9Bt`Fi>k!x!tl$8Oۑl=B<& AZJ.Bt`F(BX@zy!B!!BaF!B4B!BB!BB!<B!iB!0O#B!0O#B!}I!trՔ@?tnI#B!0O#4=vxh=۴n wsbzXfM;~^㶿_U+޲rNUw?eLbhĩ)`⍩{9P%՛2CrXeu-^WU9!'uzKOKPCoכLMwfoofxHaF辄owښ}D&CgßxO}+y`@i+LEFS)TmkqxX4"Sfhlܭu RrM9ca}[̖K+*ʎ}KoV>-d}֧9woe?Mu-@iki:_)YZ㩶Ek},֍J)w%\~ku]sT<)3. &jnvKwoMǩ&q`(JOQ{cok{m)Joks*}F^XmYdnԵ8}G]!tbr;lm&#;lak<{,v*f_s44NzTScG}mi l.tqWm7-i%jjkruNka^ne]f;dUV}m%urmaS]UcG\z}miƳ~ wsm}Β]]WP'TqwVd3mV?6csIYU*F&$[/n=Mzl>?+@c6ۮVfMk[m!>h4677agc-VW1= m.BW*tCbb=,ޖfQSO6kea]s5xvenkrwmuFJtܺ 0[v;vP*}ј*;;=5xbL>]LJ3dMtRi@ɨXq1O#tz愄.Gs׶@uciVQpZXty|hW<p` KOS&n R cyiueL׏ҐgK"I&"KsEu-m< k_hƘZ]:uݦ\owGGG{{P_Uy]pKM\UQ;[{eԱVd'@yKS8M^V TKS/ڐ郸kR';5T9!Kggh9.%_>D tz|%Sm)q<>Eq:Y%ƞU\PuRP׭\7Y=NV.+:hsuzT&ql(FR hˍ&SV9#ثmNeGwby<0O#tnS \x%m\eϙ?lx9JŌP=^ک_ZTj2h;JV=T˳=5|7|Y7A\/ink2JG#;EQqLR3R3si+)6ץ/ ೮~:F +F;/.;AYžwA( A$7|_ DB܍vak@/qEʜ<\] Ƽe[_u ^<ͳ,7r)j—ၢ&wUZ~QQb(iY_ҁ>i\>vp9>&*W]f#rtώ<ϲNM^@˽ t-TBQMqͽ[Ror+3_FW7;],m2ԪUB;:v%eB*lkrz`Q1SLvzc b,:RśIơ ~왏*ƶlЭ*Mw4bPϫC紷4g)++<$O8qCַ֏Q'd'e>\Vuw5ZFKfkz]VF\=8[wuww6m*hopEW dk.n7 t5ҘTö]]O*AF{kl,QF_kGgwۏninbG&\; ߶؜ZVƥ24;lͽi`RLLcrƸjsQɦpeChتZvv]rQmb ?ʴ.5{ڪZʐٔ/LYYiٛ-- <hƼ.mZ|5qiiMui>*%{\N/β~' &!&/{t£3çO:zJB.7G;wΌoy$OژmJZl|})Z`E5/VTKFא.Mp%ͅk};6fo,u r"XO^[{[TwDMO#tz 0 J$$Ν=e2̙3uX\>>pT*%%<%!RΞ7|74#BF| IaѫIJ`V.JFaFƄaϟW(EdRT*Gn ϝ<)HJ$\P($I(|>g n"I_xo=gΜxLJS#G}}?̗Y$I0&#/D9^v?30I ;Cs:tz 8EAIdA yݿ( UoLU*+.|!p=49$Z90S )J&IRDB"NAMرޮÇGFxthGCѮ^'AJ K}Rީ 8 3`ʧ )L& 9:/;0&i@ ` &7)AzK%v8m*sIJNS}%#/< $*HQ # (uffE)DnD8?BܥId2DBISgWT3F\>)PկNEQdqqR, D??!3S銠 q#gToP*!9}3'TS(Z&qz8{\lE:m#ߤp!pU D6E6_oAώs$}sǏh;3pd@!@=ګ M:~C>o,[6%ܹ)b @ Kr=SNyNQ IBJDBf\BQ$I l-EYW_}YTT1Q;D|PfY/ڛ?8EE_p^7SM?/ *b?9{+R蚣Q%SdrZ2[7՘;LM y۝EQ@0q\0:z *"wT,|+$nѡyY*(cy:0<&NKX"Ȓ0H!4У/&Əfь2,,LP(r)JR$4־j%;gʝW?H7֮nN;*׽i=|emNeթvlY֙t奇zm?]?gf䕉v7r_)hct)9DodkszXrL8kCOS'gcۻ}7]h.I;Sh_O]߼BS)aRItx9ьaꈰpe "xq?|rꢻަ i;Y\]7%Bq_[YQUF͎;W]Vݿne]zﳵX+0O#P(B0 .C<2N|NFO7*HBA4H dS \ ~O?(AF#JN>!$1@R$R"`GqBHq`@va{F˟A ^|4dc0\)*4zMdh.Ct}+e{_Ȁ6[?1Uy?k8|1\'D8og]aaanǦqg\Wu'~KsVZwGGI;#gy4c]/EQy?կ/΍  1% sel@NdL:qgF}kSZ??A^j XaωR O1&/;nV]ެu1nmzcYݻ-~ڔ_)U;i6iI{]?< (AOgKX4;N ޿tƼҬPݱiCa2 **+e7>J]T;N3,irq%zz|tC/5;r( ZnӢ8ڴėmtڦ.ͮK7V3\Y[I) JRа4NW{{o`` PPA@\C"A\/R _svXVnOUЗa4ST53LSpQ Pkgq񺴢tM5>Nr(}Z~T-mnյ:].V[<ސ2K;&m2[\,K/-T-w\Ҫ cBy Cӗ+/PRSfmZn*ÊuжeZx<VUd_ӵi+T+2\s,~J6&cYC9|,ƪƜɥ^ݵ9_K<coꠌ+Meָ4켼Dd2jR@krM|]fJ 0m}N(ѳ}sN3gj{|\;:u? Zٮ/7#B4q3/J^\H B>z6u7™JTY?0Ng#ּ_MRO+;9451:飃4-:!SV}?@kLT`2@Zc7O)@鳗)@e00n׍o0Vh i&?@S3/ӨJcX \DgռF+NHׁ\p \M,c|LVYUv1AU~AB[WAoGze4'P{ Kl)$d*a{}h'}cy 1I_G[ IDAT@! VM60gsF Q .""ED9H$g&@$ 80 ~%3 b`aP1GF.˞ +evL.!i2@QB@ ?l _M(ogChQ $T*Je4%>]&Ϋ)X7 .ڗF)˫W\yS;i-K2^Ǟ꺦u|<֡.(55F>: TX j heC/:)[Ϻx*ά:{rcXW|Ws rKk=<{`F]{H^/( EZӦTSLϻG $   AdDeH@ t21À ɏIN> #T@Y=zͼ2.sCeЕ%{ƭuY =14p] |kxwd}]^|<ѩhY>. W<܄^\-{{֒Bscw'SSu2!k{CT[]#z' kdEq`=plv4R@AB'7"#  KRЅ} !!%)/ϙ.UHɟpj*AdѢg s=6uZ]Yݗ5z|1coR5XJQpV[xZi[[֮tߩ񙸉4BhuXCAyϺbx7 LN N{ x/xolUXZS]kP,ϲ<|=r56vsw4:x}.%6y4B7śC$Z&M_?po+"]SA$DA;2=pY3鰓^e֬Ee-/OE2DRA$BE(V31_ת"J%MSdh;+&]^eRSSwͩ4]1MYm-n (i,|1@V4_֢RirM)%%+L%%˞.%*/D?EUf-{XKVZ|<PCJnUi2K+z͖/@KusdfR V6csBhG7,t?͖~ykkkbNAsӿ[|t~JFxeS‚at.O )gƋkfEq!!Ā ~8]|%Z+m2%6GncH^d&+i.'Xh^rJ3).(ՖUͫ_12dfk U̖/[Bϩ(ToYWn֛fYMҥ'Z Kj*S4;˗xU}slMy+u}H9+ yBP(aaTJz08@"… f͚uaaa0vtt/sn;03G86ٳuz~NᯧMҎEoQA8|:1]r}oCs]VJwW&]k{mmkmt4B7P$HR+”)AAcyi~xx sÜpt_?(]NA"880OZo">Eϓ'I_NA$)(AN?e$rL /)H2R:J"!%HFxrA< Olp؋?L$#b8-BGP"D(HvZ'~0IDQ g}~$V`#y;]7O͑?3REe@ y$EqlV*%Xz7,Y@SARA %iZNry\NӴ\.H$5.!=4oxx7F}>i.z (;~vxVUSWO+k:Nx^ ?:n71R Abi axy: "AIJBwKN45 @*Tyt3d@0 d2ZNӴ|twMg$p'(Ҡ6.ԷX("Ujh7dϐ> < {y:VH7o)vNJ[ZxjZD%ђy jmϛs_u9{>rع!Q׷qHяqgΜ.Y;yy;A'CP~>N7}>M>@qXLz/Np<>!~t?;Vğ;W"=<<0\ 4B9>IN 2֓'wn%}==^)Y088p48K,)--E#Ӊ_/!(B]^{^X= ϟ-0aUkh3kXARѠtdfloOqI7HOgu>|~*1Sjj///L|k]p.W_O…?Gq8gHR |)#,qG@_ 8>%cn8sħ-xy/-dym3^}nxghIOOA/Xutw}6h ޒE;fxy pWL1023_jKŬ\ɍ|PO0  O}9੸8y6o3cxy. 8C@^A8W_K48N< ||}}}z{N;!v\q =]}z/] ($izp``x'A1!t#iH|wB'<?mn߮{zzFb_)|㸋/v~s4}\K_rJGRVr>xsSCw){hy(!|k&帞3/txxz/i8z#o`00.]xkOOϨ[vvvX׿uwoٳNM)^xE ~c 9sɓ' xᇟ~i`x  4Ξ=z.].{}}3~+W:fܹބq)8ŋݯno3|{ 6g@z8`؁M3Ƀz'zϝhj:>Xv#/o0p@Ҫ׌ٳ,3z}W]NgߥKo)/ KG?wҥ<GOϮ; yzxO8r#%^PPxıcǞ~a!MG(-Eb/_twiigϝY#nLOG}?=;f?Ǭ|ϽL@7nֿ s8=Mr\b9сwٶԿ/zX6qsr)8P{BjƼy0+nCL{YpWZd8|koli>c6:t={C|WW[p X)cOc{`{{$` }՗55u?>wKd`kuů6m;g]ƳqGmYQURt*|k4}qr:y , /dp߼B_ospCMhѢ}d6={ۻ%c|dۻl6>@_oyU' <3tНwF=ͦMu55/\e7xwyb/8w#H.8w>^^LύCMBveU0hܘX\#e(,,j*諍,,X޸L̴꓇5}m4! IQcHn諍v d!fhdkJ~M6 SVSdeRVgomoh/ A$Ӿ-1Þ@4V;5g|ߓ@^ESCMYu4 YgW^7~ڣlYN[hR& B8heWҰimkza^xkF;۔Ly[FWf'yDzKS5w6:|0Dh9s&&&Κ%; {zEn {D;8,/P{5:ܾhVgy 'ϧ.n9_Ǧߝ˵I*;=&["'.r,%ּ 6wOrZ#7Vh%MV(a+k,ihZ,qPaLӻGI_ЖK;<3[8 Z#A о}m9aXG8zֶN}q搢9stuuNԩSz{{͛ܫ8@ʕ7W"q}<>߽{V3g,Kzy8eB7@ݞ⋣s>`Ho-Y&aN[ZL d6|[:}2RX26d[eڶۃlӦ"EivvhZpgٝɠơM82 2R yYQq7Vtm7:zUW>=RkL+3gnAAC_~uЏO  :NW@: "^ptj_zO{Dw6;8Hzl+@ Θqo~y3kVl6gddxYs渷sҮ].7t7xIz99pxIyEx{fϞ78882E^^<Qγ5!rw R(h2"ҕbqhRUedBDFEJF$)*s*K" (pj<) ARe}WG B!a ΀EB(j9Te$.$/)i$`F$!6Xd`D\53r 䱡W3ƪ&^XL6U9bB?JH{B鐟x >5rSā8硇VH$ <-ZDQɞy~8>G|T__NuB~{݅&Y(.X,.)Vک)Pb"q|K-&+"!ZC<6ti`dHy rϤpGL IDAT挷 @cm--$l|^`ΌViSRBx,M۸q]/O\rrr q];{{z\<!8nEP'7|v~+vxyy$ $lcn˄R$mfAӬ}צjwɲ4bgiᖔ 8]%M:3+ MIOOZ:ss[>}I,=t@ûfh +KΕfErD&(uN/u%ibxaRlM`>#t:_|r*nIxNk|_Fco'[fgQ<rرCu'8} = x5q篞w[ޫRx0= /B()$kbJCL(&YNQ:qͽg>3tךZ_>A[h`-a) E~Wx1}wf Zɤb@4GBJ%e.KVhҲtQq4huqq-qګ97R\Uz̘LKRb>`nBAR"13tBt8Ŝ?#}@,l=ͱp:;O|x"S*=~ءj>=C.,tv|s[_:֗Awyxx"l΋ Aڧ,$PKW86$ܱu60rcNQ(Jî&#(%RB M-`BR mKDa`!lmrD!ʠnY|,'^^HRt>r媧zgf̞=kz/O4xEΜGE۴_<9gF76|+hC>i8g73@JAIRl5Z 5s涗tz^>>>0L';0@xzxE }z<2=;9s788&7]K\I"eV2\VB-#lM7ee=lg Yj*$!TVFW~w'0:%.1-PhuNsqjMfS\H$Q&fiӧ;\EI\I%_85[MVJnM &-)DŽMO_(2F/J\yT]Z5kuV;͎cq3gn\|B'F/fwGwΕ<|XcC)edNoh Ք& (URJ' NmZk$PI;VX5kOhY)cQ9xPBBM|@XV * e]n-SF*]9 05 ORdhWEy*}1C#ˏ' zVcB7os\Nt,0̙3g/_ٳ3g_S,qOi'~…  ?"9].tq|@ pw:zsA?3E )ag\b RBߴzqvUt[F*;=Lֶ8eG(x^5S"Bh88BWQOoߢ,|lcۖ8Ӻ+3٦̕?v#&enySl`pO4|}#cﳚL]aG-.KՖ?˄;m/Xؖ׵S5??|" 17Q!e)%>XXR֞LL^(24v>'XS\9h*Vg=^ /Ḩi arL{&IdW- Z}dH%3QCqO.n1<\S#jޮkIHZ;ɰ5hFPNWo\&fZ3b[i*"}떰M cj4! MV8(%^bZ[ְ-C[m%DED$͂h͌QIJ/m.1v*:7R$iee-WTіIoCF5%`5;*'fؓZj'BҷDJLSMetUh#%tF+ @ɣӳ7/OTwVB-i^+wKpy*SZ={OR1UEf (eZ,n^ܘI²-im1T5W,BU;nӸ˒dg'7S "qzyafQvn_)֨s7.3@hz<mcڥ#TܑfjjSYQ^ZYY'7M$T&@Z0B}rOa(C:SK!KS'I/ܻTEh@;K%uѱTZ5ErIktZ}v:m(rqC˕O6!6Y,Na5;v&mkv* D[K Ӯnoj ^߱4]Ԩ/՟)B?\ղ*ɴ3tƐr1+6}eK4zsLၚꛙ H.UIM[^w{QnN,,VUd픥j!poT(&>i8fOMg!e6C{e[Mz.H)9PkKSrP|(5%1$CBݻ*$!iȠ$B*BhBC*Rd#aVeE$(%Uc#be 1\[xtKHXvwM)cC"Mv_#"2F!LBr3؝^ա^^]*d2U)F?zZ6e0LM\h'% @4)VbtS2hs¤@!al'mj4Kb@F$Z33']k'Hh}0,wOyv0ao3dbh4gϞ1R e!JmW0@*=&4Rb!a`h +KbEr;CcC CTRg+RB@i־kS\{ ,K!vʽmYBJ DmtɠD},=_4K,%†wvQC/RELԏLln_u5>N&S2ʳGiIiMR7Ȭ4;H_beY$&(5{ء;)?Z\^T' ӗTi̲1][(UZ_dhZ/`̧tz` aV#R\Uz̸- -AUv*D Qd]΍cZ&"h $FY,mg 95g:+Nx21ٸ)bЦhɵ5뇷:B g1Fy RQo[&L%ږmBSډ#@ Z4}nBALOuGgU26L |G6PGJIpu/++3,!B 2֔@ּۼ{{ t5x+B$:C+Ʋ&ws! *Z heRBe^]~n=BXu-K]Y (cjcEihRIF@`k0dJfh^Qak57iW{G3i,LEZJM s[m vI:S>>*k;22 #)׶;`l-v2X$2 hW J;g Hn?yySLVel%FȚܯ-MJ("::_4d皴4ѭ8&7]K\I"eV2!0@)&FD&u+D"Y*5U Oa,]c:DB]Orydvu~u~]gn^,*\ADi+ԭ:MZ\ "e:E `'B2+"~&scZ%= U:0: B!4B!B7Fj-{+*O#B!4B!BO#B!4B!BO#B!@0͙Od`Z6YM+TE9 'x<՚k{rylrELjʵDrwa!ʹ$!da`Xi'`i(꫍4%PeFJI8heW۝O|MFUj4t7e4٦_6{uVh%MA)wKؤAid5"L#|1Ui5EffcD-ʪ 43&$!CQ"eB<.R ˓99b@p4B۱4uʽ{+KU>-m_߱4]Ru WVVeYYU6 5YbО\eN}rOa(C|%ȲĘWLt=lC /t4Gni0Bӭm'$1r.-sR, LeYFYzieeܰ6m.&[̑5{ktc`U2!D41!hcJFKw(i/1jjjJSZof& J]1"@aF$ԖjE\-iBeUѠ։kjj*}+b]*jM6fȉ[(HPġ Jh2C<6B V`v`!&J! Kn"a5T542sBX!IR2!2V98NYLh 9"Ia=C*Rd1YocxbS5}#4%1H檊V K "8ȡS# Pii4X B! ˷&U'~lmU4U% I@E'ɭUMeB*aHr(f̆:ZFBI$HC$y(\lm fIDL0 ␄P՜4@`!2ZVU)P v1)rDn̫hM}n Vh;R H D^WY4ۦM\Pi ]bbh pdm @)ե1مvJ'YBA b3\D"j;X(X4kߵ)*DMd+m Y-4 ?7Y2:;_5v|8&JJ*4+XRhԩP",슔1ӯMM,KZWn2 +KC+wn%2d`hdeɴIMT=ʮPWB7hU<RBJrU11n:LaU0Br$3*(MP'f*жDn &N(X ;s娡 Bcݩ*A:+Nx2З' ڬ2B]\)%qtK֝770sRJmN;jf1 xз"T麊1rwh3 }ha,2TRRmbl jmv[ nBi߾qcA{x,HD,h*kZ*iEBT V+cUńRm% $mCVUUثˎM!'eh{vHÞ/{2F/J\yT]Z0MʓT!ĕzDJ_gЧ#':lU[VQyHlilx \n%L|:7F NPE7WDPUj2CT-F:#Tk4D"2)5=4KVV}sVBW@H)[c0xkn\.t:,2 s̙˗G;{̙3|L{1 @.; B覄!Ba>B!̀B2xJ,BF!BiB!0F!BiB!0F!Ba>B!!Ba>B!!B!̧B!|!B!̧B!|!B!̧B!BO#B!4B!BO#B!cEV.6uM,Y$~槢ESDޞsdL9O췠?Jw|GG'_e  %aͯΚtI{P>S?/a{ G9^¹/F[EG>Msr͖tb7w j1U;\# 5Ap?Nu9ՑwyB7i\{ֆ.8so~6!Q{e@@y8g@ ЍFF/mu>Y#vi`yʼnyUU?U'ϛ_Lދ/<ɿ΢ݧ-7wR/үl>_[ lߡNs=_?u9gL̼rM'{β{zo[<ܞk_ |=:vη7}8pҴ᭎  Gگ3sNg kԗgr:'hA1G0x]V3zI'_>Bl_ [+pv9>un!5[G+~c~|~Bߙ,3ʟZ,^_Ȳ/ ̒]}盳7@{ nn .;U/pyA, _D#^u坖 "/C^wtzB/nBlb orzE$\:RG2}U:?8N6oFpaǦ79f:BDE~C.=qƶ8a>-`~ěAw#wk FjǞ\y Ԕ$$W|vՙ7f֨FǾ#9lٍ2LζyS/vyx Ҁ19ő%O~`~kVӳ[3[wjL=B79v}Of#'+rT smWKp't ,]9vl)8 |Y^MgoˆoHrL_T֓"c˝34OV]pB;'W$K ~]ס~sɁw#sP8S˯J9.ٜ9l/X=vdٗKۺ@$x3Q}>~Kvcc t+qt>NSkb/,1\>yjmVkPidwxhCa)|b?E;K]?YMJ8r9Nɲ,0gΜY igϞ9s& cr0z nJB!!Ba>B!!Ba>B!|!B!̧B!|!B!̧B!BO#B!4B!BO#B!4B!BO#B!0F!BiB!0F!BiB!!Ba>B!E&.j4t:4Ѝ2c Dc/FnЁ4BtttKcǏXׁeYlZ͛CB ;9D7AϧiBb|!t=! X8Bh|!B!̧B!|!B!̧B!|!B!4|4BtdJyB,t>ى%XL8:NЄ8pY$ǤtĉZyXL7O#0jFIQRV?">㝓,|ҝb&JOV:,Q*~ggnZ1؝"zjFcקLJ?<}3x+~TAħ?带`O_C}]R%i'x cbxrbU\)]nMz*)4Z'%g.]_1yxc1ef)ga絝 i $;EkURf}DT_\h㶫Zcsu:fIѾk"-]ģ`zz}cKܦOQqj""W#z&#}رcǎF]&@{Sl~# AY2u_,yoy{dxݧ'GoߜI'5[>nES@-G*-\?-\7ayO?z !~cǎޕO߲&r1F~Sfmr,~`w\њ'~?nև_}œޝ*|EԪu~6/9_9sz溨.9dNVyK~]|Tx|Nܿ>|9w#̔wmt+yo~O?kG E".X>Oտ;sqgÜUww@g}+VD2Uy'.O-?i{N****<*>ӝmt\bN~]U㊶`Up|Jzoć;vAW~#Cyd}?點ymo;܍:Q \7`(sMUx*Cx#S~ݪ(D^TT扡ѹ{]72¾Jy}n_J=MX[6~ $ѧH)9j;^*88Vz|+3~"8R힬dUNzG7O1̩ݙ뢢Wtvs"oժL}jݺUQQve;N.giflI0F6o﷌r$Գؤ/X NNSz~z_-m,tku_?_i%ŁYB~;]d%Wz*xkyszcT/D<[c;>[***<|EwGYF=jTWgw_N`NY9}jjwnTqB (FhwzDx]y0+]ze;鯞 /KSڽi+w;G7SsmJ'vgKw v׶$~N?xM@==,w~P2yE(z~^T`{F<;1lr8q+j5pp`4"@`l$h)~H/46 fnZ'wKdKShh5&W'.>~K%=~#@ [ɭ̕C;F[Nca*kjYe #ު"\q>PszV+kkksLdMAT< hb-ȫ?l6DDdqz&'Dn5ku,]e-h#$5$6xGz%':5ȰlU7zV@F3 GۆCl-"(nЧi6?޲vypOt'I:?Qew5~Tb@V3d*.7O:-^_RCJE"r ŪTM""6Se<"M~M D(^12@-%8 ' IDlhW" JUk8rV>HEBqD=9f* CT hX+=coڙ FTY -|m hK/ħf˲$gG{&-d`%AaH.-wX6>?y+J"q;4"!iRg`!K錨`Dk5ͿqQFU7 1M۫jNvŚ+~qS&Κ y`<dZjzI~y}]RLbDFĝ6gR+;7m&m[b< _-  "{kEΖ)m:qȧ5A.6D#ݲސmp&q[mwbw7gɁֱ .z]jo.TW\No x$wI8 Mᙢ1hZxt Q7K!z՘ba{#D{BsuzkG#ѵñV=#dŵ졥Ci!.ƽX8mWgQ _;xe{C+@'6I~3-D/kv{}*8Rw$Ø-%l!y u=qyN R}tHPԟ<^t)gΥo*UiL\Zߊ$u,1Ÿ#lD\0?䅢ޔf>}KVo{UU6&_%[ݭ(Ba݌+fq dvGDĹs0dTiXLDw9i&R ,z2Hw?;0찍nӋΗDv`o-yHb4 i8q, X~ "Y>.-apj2_}|IQ(4/mc8;>{P;]itC8ML J@ k{\$ FB Q0p@ɅC%{6 G1)6*grDhKa n[Z)~&QC yPb<{4{i'#{ v\lTW\&x2 _ϪZ1BٔA(TȬ6 lUJMȮ o-z`M!r+/c'|PS_Y*s5qDjB DI qV:m +q_I$f5M̍⁦1UlKM/2=gi:?caQfPs1 7KC%›BfV% , >m 8 1OhmT*~Ғ'zƝNNj2o86u~D8͉9pBV">ť'ӈ$ɑTSsÞ! GqRZ7c&oA]DdE|$rݑԺEDr4TRV`E&5G :yoe3#ק8Iŕd2>w#jUqz$kSS_,DfV~Bh#wSpnIx6̣&'22j r$ؠb-݉ )7~kSSq!ىΰIi!~C"wJ-"4=U %{zEOVrdB!߮'gZ0y/Rl,D_;6tAs3I+F2lD$'fҩ5ۭ5qw&<8tO9wŋP3?qMSsvciaZ\m;/2DdFn4]DiW&YyqufF󉵕dc37r>zƷ?G~B*c꥕\Qo8`DTYD4N-Sn{ѓJ1-FϸF3"Ye}%O!((ޡ񇒷LRтvzr;=w㑭BމA qdȷJ4>@GR94}yymq}אj26%:Ok7f,-3G8-?~aH͌Wn]WGqėkwr9C43ǣ+b%Y ?Y,~Sjꜵ49G#zՅ t OEŋ^BS)f?r)P=cx^46Z]ի?c@>P=߱&@@@@iiii@@@z볳3Ti"/noo>/^D*`GZ;EU@oŕ+W^~~ʕ+pVy}}}׮~K?IIDAT]kh IݾvZ__U@o˗mFS|5=˗P= KwhS\k``Snoozڵk0}z\t… (x8p_x1?~eGVK.|sҁ< ߺz|^vaM< < < < @DD&~&#\IENDB`././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/doc/source/user/figures/qs_package_details.png0000664000175000017500000021532300000000000024130 0ustar00zuulzuul00000000000000PNG  IHDR{يsBITOtEXtSoftwaregnome-screenshot> IDATxu@igfXCBŎQQQDE1N<=  1se٘Xb{~ǩ,33ϼ5MӀB!B!`B!!BB!#B)F!R<B!8y BHq0@!`B!!BB!#B)F!R<B!8y BHq0@!0!ͻ>`>ܙ X?3OSr ^gi[6im^9DmwoZ%/^lϩ#B?-MS33`⼤i=-V5!(+.*,R{"_B)>'R>}H O)!!_Ryu˵Um5?|V*<#cZq{z'P@vB!@ClǏ)|h0dk_{\;Wg !;ƖMMT d4ZZU'ȣ*zL '**:G! >;zuG~wA_vr3>\g)_9Ѿ+d7mvr]'U}= Og{#R36&551k[nlKHxyqÔx➡F0E_[}Y *7ŭOG{@<'z!f  ]rkӗ}iy`O'!`ؾ{s]f5I9l5%@~k?x!/YM8 ފX* aV̓ [M׽{f/BDe~ˑ[„L.>D|WO'-?|J{k{_uûW߽+@lpL43ac-g> {piNj[_2 /X]? s zqo?^ow=Gث^ u^6nZߤ!$PmB 0!awOz];|/lt˽K^'n<|reK3&oW9'X;mK(l:eXKHKٶtj;!!OZcotD҃q+pM(,v :l{x.6| 6bKAS =]&`2Yu5ވC۴FC`h42ǵ d|-L2^Hh2w6ml;kKK=:#N;LoVۨ^rsl!M4YԴwiWay`>ݺmV3 @ۍ]۽)@֕7q$ Bj"XfMoܬ XZM--5~M?8FiBLm3Վ^dH_܌t֒>*56в{O}B`6rl;ur=\4BCB-]47 r.OvYZRi|PՐ]h}Jd2*L>nd+`ʍu_p!k~rVrwl~m:Lc>6 o'wPG[":$DՅBRՔ'o8\?y(k!mlIVTgа!_'3ΎPjgTaTC7B(<ԛ6ѢbXX0u$Ie -eYq_b?{zDɯJL7)1 f+DynK&,i[n$W֍0ָ Ǖ|/iw,W:%0WH@!Zn7[ԍ4*0 H~V$| *M!utrd.*Z?pt8|uU[om&L ?Ѕ;/PQ(BdauV͆=tz ׆bo'G'W1h`˴`P\ ݋A!6 jB,WHDd@0 J,-;׮R$iA,iepWnM[6i e&,,(C65z>18tqa{&T/9PBW$BeA^A1!~_y>'P5TSR@ &|E'JU1/:Y fTq,E{~`}lD+ұbjhfDgDMG00$ic~qێ]{ Qv鱮tKchhJl{(#:;BYאOJ̣SeHHeujxB_u%,Ӣ;) "VD.ʊ̮ M2f∲8$yi2[ڷPRi1׭߲,9ˬG,g{[~MS2n Bi¯( l, R 9O>WsBC{ &{odsG-e%@cJf7~`wqѭV֩{TC3>]i ^G]~~/n^vf.w诊e56"Arjm4o{ǧK6{jɤ-Rs6=Lb>@!{s{{)zv{y6lğ]O.}{Hֆ/ς.$Pl3gqOo\+ ]h֠aN?NJSN*. ;~|~䩰 @p=\ɎHjڲz{`羿u6Ol^E; }dPԹ_jq/嚓σ%6׊f)3=RNs:o4곳?p9"e cjA! *0vr[Ř/iwiYɴOB>Rw|[-djVvȗoj4y՞j, ӗ~wwuq^8z8z󫸋$mFZ}؃xnb`a*p~N>?پ/.Ma4x|C0MYaz`BT6[]m̝\Qj`rJK"G FдbWΖ$܆> zw@%*L y^Hj7vhÒsXy(~ܓ#3h5gP1O5Ҵhmӭ)IvkqMɍ}(mb[²y۶z1 Ѣo?@q޶MTb}FNh2Pbx>~!gت]f BȣɏB Qr9.[Sf$d-Qc/l BUyZ  |Rnf|`C '=G Bu F3B}^}cSNO_y"ډئB0q翏˻B`k5~UpB:G<B! WB!`B!!BB!#B)F!R<B!8y BHq0@!`B!!BB!#B)F!R<B!8y BHq0@!`B!a&%%< BAB!_`o B!#B)F!R<B!8y BHq0@!`B!!BB!#B)F!R<B!8y BHq0@!`B!!BB!#B)F!R<B!8y BHq0@!`B!!BB!#B)F!R<B!8y BHq0@!`B!!BB!#B)Wg!dq_E_&9 vdkL}0pB9OM4fmy ~19ÜV& unB?!3{JqQIWMQn|09NJFjTUk" _\ F_d66zE'V Nгs+|"u;Tw #oP]6ǒNlDӿ:;#P@w׍r}(;'ޑv?j憓! (M ZwtQ$haң+]trm:o:'.K(x.;%(K ;r:⸿GqQlj@7)J`]\̍g^eIwo׺HH;ҕq SNxaיrm-4~مN:͡5ac)BjvrܞK@H0@ZMY@B'\v8M f+O9m\d#aYCy]K2 bxO,0NDvsd>Rab\6b饄Z ITR<Ň(*7ŭS='i `ؾ{=W>gkLd~|tzäQ^N5Q١^x_zVHMz{sWeo#P]ApL@FtFuJZٯi7ipxȣ(4{mAH'sཧ>Gt˽K^'n<|reK3&o'{px_Eߙgc/*ΒQ>6vrNӰn]`@:cM[7-oRiX]a᥻^v!wN{:d]q^a[pA;YUHvZiZ흧L8y1 ly REK ҫ0o0tn6 Ʈ &<(0DK-lc[7ttɩ$4&s{:Zi ƶ6ͱ<Y 4U=m2sf*swQ,ZzϑMGTkj0vl"ΰ{t3S%`i68Fg m/tg3%Hf#3X^iy9'ٙ`0<BuQ/Qbڗn0= SS O%v[.]C:u+ [46aРVJ"FF*$(0Z2[)56ņ pIJ~B`6rl;uYĤA; rf2KB( EڟU+Itxۥ tUSv~PujBn9 vwo%0WHRA:}v>l@.-ke._%훔(vWUȅzz>5XY˽BhXևɯƙr$;:&@ժQ C7BhXF BH{ooB@>@>(AvQEA+HfSwo돆eNs!\X .(5CjTnQ#nErw)G@ݠRe%" JTab~Jbٮl\ ʡW}mMԞ}vas^os5Ot68 :n)LQsD&1[؍vgkO!B-?UK=& yu&qY{OJz݇!OBãyQw^unګ"Uo j]qcQ"@2#r#Jf;OŮ}ppz'yl?m I;ų΅Q#YM[A滫>kD4o@)/ΡS o x%n9l.+G`&B)TÓ|re]C )1QJ[Xn<1iou=8mZgu>ד%L--4SB0h(9BQ)UO<&/:_d&E'Ԍt 'ȃa{(sQǞf:)2c;4k ct_pZ P!L3f8;8Ϛ7Xr/G*n FMrKr!A}u-ƞ3r?$UcybQ_=*W[6|UU1e۫(ƭdfNfJޝPobm@E^{Q>qqRXQ ~O|/MKIC)-Y Pn[SM0$B`4E4>@- H̭qPF ?=>q|Q\Iݮ]l R:m ?P}]572m#S@8GHtM~VWx]|Dq%{odsG-ٶf(zvRIS pz'vg~{vuxA?qJpy ,h3s璞-:\l m%s 4aE1i"V:UOGai~u 2SLI^uiÑ;ggprD6˒A u:9۪D{PǀܣNЛwۻϳє`Y܅6i vԠf#z2>pCz6yt9$f9{VY$/'t@u}V-|O<%" @|\\tuL r/ʝOT뙝a(B N=;8ٚ*W\̝\Qj`rJKtcR*iW Iv~~ &HA4I  @| ~Z!!hIɟҖ;"%*0BB @0"tIB"+ -@Bb $Tq[ty.!* 'Te #>Ih{604I4 48!T>9ZJW&-4KCJՔYv `k0K?ڀB#( >JU3aB,% Mt4Pȣ %kQC)VKfcBPG'OAZtYW >!ShA'V<أ~0iP+Y}4Bբh(B7F|k8y͗dMr=?vZU&X:v`7.ksUǟfJZzϻI 1eMwsjTC#?XG}2A\U;$@5T;H0@wW2ϒħZLчCK.P=Ozp?/X|&NE ĵNF=ܗ97bf^ʎ\!%_]WφZ9(]q]noõw~u.:s{p#Iza҃=sFvs"rc$ɶhP9N\[UM̈́i'@|9ޖ2u㕘BfOK:jMF}=K_oS~AȂ_Ѝ۹rd!I~*9b%ridYԧ3׺Y;& iǟ}FXv/=u&Z@ nvʵ|irN$ԄA+lЃ۱ט5WS|LlRouqD_>rUc @sCx=5ԠrQ+]wc4\#y[M^⌰GYsm[q*ܑhcnvv_t,Td=yngVm ϥ`i1eIvgWz9U>5rB7JySK[DlrVܾ] L>˩N =!=.00cCGThGnt3X!PA;iO2>|d>'hЪ~FL*]E0;~at^ԍt&7&R#$4 jߢ0{۬SM0Oܴ6AK yݹ{N Ʋ-=azyy4Ҕd~xp|ϖ kP 3/to]lߢ% z/סy^OWn䥤RvQkĸVwt}_Sˉq1h\кǟͳt;V)ivÉ.4~v%;Ҕr{ ywi1i?dlG+5BⁿG$i46u@KUZ`B'.ӈE~sЅO&=̘TZݭ$[U] "IPUP)["  @CБmmH(*y1SҘL|$:6frrL纸!ĥr$V;&6bHokc=ƿJqOj) xJoޭ r]-*Z[+kk) & @멒@0X$ HOk3_+! O 洤HO>h!S{M;9)˵)%QRTɗZ5C^ҙ Q3}:$%OrNܔ8& r[nUPI^JfcSNq M @#?,m iDuoT 7TfNGmE)ΎS57H`5eUͅo/pgVZ /^B7d}v.֐]53fuQ9⅜x0pzIGWKni߮FL:hazؘ=g\E;4ۆ'rv:SSP{$ҳ6m^{.Z`40efO>O6X?! TA% |2e*8DqYKY) ~LM5ܝj°9UOi$<%14|NKSN0\OՖCnr;, gic2ԍ8S V0 B2Żri5i!Ojz>SݐZH PpV,#- $ڰݎA /QYjZm~e$IPaй_; %[RѝiuO%"MZ9>vkVRQU"a/njfY9sեYfζTX \qv87")g!-UY;v? 8* _?lxƸeW2j0g2\tKYEeB m- ^@?>o>< .7Bpڸ]OlwЮ%ö×/?؈E4^VX*Gjٮ<.l Ӗf3Ҕ c'm?Fh~3w,mdAya: 3,t e3Q3 J&Cѧ/TnmGOnF7xo7+Ww!Utݾ*gׁJ {N;$9dyˊƠ UoО 6iv ޫyV;j""N%39e1fYG[*՛@^K7f{LWohu~[J¬q8D-UWyu˪g4Q? ]sMS-Eb/rqb(:SZ4?U(~ FO;>kL2.37/7YE4M cN*u.=I.hGx?r}G ox-^[g!T2 FYK8 >BhP1EK ES4P44M+_lJ!L C & L` eZ>paP hKb$ A4I $4&he#@R$B_˅i1C:N(YZB0 H$ I 4HITIӈt xЯU2]1,F@<db-t j@III`AK7 )΁PP5ɒyҰ!Tk~(JLQ'] B( )+[T[m)~h Yf A&I0G~W*H~TItHiYQk1sp:'T*ЯG2ϡt;Xxn'e 1eMwsjt%94n~5w|;rUE?7*PX+qvB ʕ~Q_פ{q@@@-}j-]ZZfS?*B%MDDfP?tPia7fF~݌YD6n~%Q;qmnѨQ1{/kۈ]y);r3RN2Y~(WuP_·RI.%]DI+H/~~)K(H6cڵk׮ܖ㖯]vǴ:cp\k;ǹ{K}Rޝ:y.z#*U.r\~c I(j@.;LRu/=;˵s?MU${[.@׍Wb iZ%p}Wuo'o.$w?ukӒ4H2Ï,tәkkw4OuXs>#w,#:-* 9$0hE-Szp;jE&{>2W4quA 0x=cL }{hPV,}.߻qb@=k˘ʙKBnfhIm߰OuU;v 70{.L\Y 7>(v`rg|ŝYɬnwm]6T8pniϮGQr%tUU`_BtUՉB@۽'v^s޳GfKTl%MWA~+'[=.hEV3bʯ(9.W?G{[*#}+Uu`(Z=ܗu d6ߑgN3oвn#'M4@IA@RT.*Q IDATllʑM y䱍k1W+HlFrTNB"/3`i[HDuU 7TfNGmE)ΎS57H`5eU2v esფQ޴1Uܜ-ui :wVP?\v$f;% _Ϛfòyҍg{-5yӏ?$b SeR鐫}?=7Vd\Zz ko3۩?6kϭPIuu/r[2Wfu÷\xI}a/V /Dg}{Kߜil)' W_j->lcjQj4tɫ{o%s;i@RTc´GBs|~u)ijZ'5.P`=yLJ7-IS\o|}fO2 [U${/vogǣ,JnEԕS:7*ny;9! )7S+t1V$@w&[]:_c>~|#L۶Nf Xk W hQvr/)5ӱM۳s~Nx^ \xe/]U ?nӻ{kZ2:v|&)_c]kܱd}m>ׁoyg=wiU&0{47g)ɽomr;(_:l*Gm3Uoն1v~d}Sde/2j"ꖮdJW:B( DvonM[6琒cmnޚ:g+V2\XَAz=7[^ggf B ꨟ y *5p\37ylmפ -Ir>UuWGVyOQn ZnO;;9Ep)BQnw딲^Ӿ-iR=dd^؍'mrOY3o>neKԝB՝*ۂB!B)"2r #BEmw-Zt.vnj̨$YOiLBFmd=?IęGfUWGѬa?&o R!!G0NڿV{DkB!F>}]Œc;J'r;i93TSMv$-"$Ȓl%+K]Iv KBHM65v~r=}N>i9y98ÕCWsBis=qv\L. f9Gz.<lɸv8JQ_Δ >ɇsۍl-AiĶs lۙhy GsZB7IՓ=x[23"O?,yS׉ct*~Aſ8Oh9L). 57^%x$?  nǁGomv泜e҂ &ˀw{5"VpQ=jΠ+I:uB˺3~.AI$E~JLn z  ϥF,]鱈bYu]:DUdzj6Ԏ<,v?y:76yo?jfG6⿎ \ˬ #ܹ  !,t75v*Z  MճvgΜ9O74@_F]CGN^uEUoz g0J.9`0fd}v6|ϦWGR6fm +|ݲ'.qAY ԍήiKT=qRF(hb;㹢Å^ۨ |Ag4F$qҒ߹Dbpqʏz:s)3З>N*]f96/(/a܁ۮn5W4 H:zAWhg^-Dgz(OI e0֦p@{܃`\2!xgP3KΧsLmk>_=/@R*rTgm;B`0 kCQTQUUUbCޯ~>x1gӕ,bʛ<'V*)qA.㮄r0jqr@\ro6 yvݒuS(d¬p7i$sc:[1l^!.EsoᅒY$ޮ]X;<)"an;o;K EW{}laf /@-19 _1%Rʞ\ngΰ9yuԛz9᜴v.E"iɅEvu1p|&j*["ADKU}g e۹JK9ds6W=H9yq}2=Vz <iwNqڳ2PTj+/XQ5njUű^KWܵ'ɨhj)R!Y3fs&8}_=t\6^7qO?죔kq!AK=0u)Cl:M%UoJI}= T>(w`hpcgo<:~gW.A)o;M޷ku (K )U%dcW/B/߹~WV`O3a(rיR JЙOC?El/i~` g\~(嗔}\\cS/`Ƿpȥ՗9jS F/[uKȍ~3q\Jm>Q8V ZˍyM8Ȳ e*@#< ?2HSI9nEh H;a0m\|kGEjg]B.,w4RZL_4R l&BB5Q vl U;[c"YO7b0xڃMo5h Il`M dU;_MԤv5ѡ|Azps;"f~U*L_5;&)iȳ yG|FFՎT|6Œ%D2j @$J.gj`\VݧϞ>^o@|ӣLa3G}%)Cy:b>a2]T)0An-Nz!cRt "❉C!%9Iw3,ڻrLE@'vvN".*nJTWCGm$Uf>p%yZ/yu*ʭQ5FWu\PKiTX5j췖j5]ڱY*wƉiI#5y Ҏhg^o^=`͓YTSﷂ7EK#d dе"KRuʑݗԲŧg{2zO>E ;/HրRaz4'y;컎\ +IG9ẂU=t:M0~#K1UkY*~סIZ9LhFJC!%!QZI5KOk;xrxgd٘mwUfpc* ,N/e AbT%-U1y u)&2ڞ!K &nz.E %+iXE\DeWNl \Z0)1BXYǍCtHϭF2-hq(239f]@m2yyyl6[$Ȼi BԎ[CF6Ħ]bRG{p0*Am܌X9a6ރٔ{7b_s̖.)\pbNiֳ;ChzVyrp讂76r~]6qyv]o02.F7q욷 :@^IcNVU-,:q΄SjLֲ4ٷ{9?wĈ - V2JʊTLPt*1Yšs/tM a[Vkbfgύ msn6UADdgh VDRƎӝ{MC?DU%$u€k~?ǏV=4VoRm:-# uuu2=A~v"bO>J̃R.z`2K;֌{>01(c7emy/ꤐnh3;O. K*JUXF Do7@RآK&a@ -ټ Zoya 3@ZtZkN_-Q0?dN/F+"тt}1 \BTrkgT !k޸tӶE.[7֝ y-$Vrٷv\<2ZS~ ݳt˰PVw ~rn6yVlT:!w4}˅ϹɛИV1~ctwÏo($!cըPF.fC++Ö[If*@t5.u68wNoDpaYɂώ =>N݆pL0񣢢2Bi8Շ qHTVV㸁AGWA-paKPVpkC{ⶔR S"-7555:::T*L&p'H999] _EE5); ^F-@X-zM:7ƉKX/дQ  >urkSMoVR1Ə΢^vA>nٓ܄Xn>b?\hu*?0[G|ۂݩV~w UK*((ȐHDoFt:@W6> v|ᬬb녎td@1Fdq s{\j<|i)W+wvS'c)l@\zc[鑛J5[&<S>+~,9? ഛ{ ^f:OHT%EIGy_Ig8Ү_@EHed, xZ}|O uz ~mߚIeÜ4}L//z[A;{JNM+MwИ9z+b3|ܬYN=pAa?/=ʬtlpmʢ!#ۈeajgpfJCI/1VGɝh1DZ#8pէ2v 3M dǮst1>g73xˋhgIAIB^M9^} :t~@E;MYUF/YZūtUpzw+#NJr&YZ$S=0k›oj(?M~):Cm^%|H^31@Rq.VdaI½' eGW6QekgYwU+bj~\*z/X,VuږI&d &-ց}VEQJq5GRގ?=Ffwv ĝ+9d* J֏A}jx7(z₋+_R9p->rMᾀ ƹKG/ rԦaV;r#FLҡ?2HSI9ƥ'lH*q&HR͞|9đ:j &wަV /ߖy;{_>DDUPUQ49эgab4.2bݺ-[ƓK}c y\1PUU(w?T4^.5+CbVsdp=9{\`HRim"WnLq )ST:kPkhtU'r?K~e S^{=OnM&""*Sxh#bP\q7mB('vdbt-]y6k:Z_v2v.F -,i`0P ˙L$ m&Z#7gkZ5x973fbM2h:-XxuYSϗj4uHCz?8 y+n L3p„ fNM}:'%UppD4%ySu QdH²"We'!YJ)TY'ݙB{w}c?yhV 1ods-j}+){GPݍ˹sQ$Q 2,?}1jg]*QbrMr4R#E#I%QkxV^X5e/N#-,4}e 'FK "Jω##I؄zi̸.t:ַ{8 MfEUSQ6rz'UgB?u%noN (+/ jsǔMyF&G:@5J:΃X}IX_u'6soxg @X;Ү_rjWn[_EF¼;wzزB?4 bf^7D*L޽5No}}(.P{b ;8P Pԍ ڽ4IX6ݻ^9`Y+kᗛ0"ZBɨwQ`YP=32671Md0j'&JO2jDy7Â+T GD=R0_?0E.+gOE/(ϭQ\0㭷bqɕ}dM[[`;ᏊjDD +AP7A㚰pQq艪jh]hUی$q?OK|3rk|MU]&Z"rw}ڗikRHOը!?%'9G|MJe^jW? V%xRkPߜ|wA-a{4g37T}sXɉÇ d:ҝs5f9K&K(a5JJMh݆v]gLNfmVwELJ>nO5{S2c ̗t2DQd 33Gyj+.ENN[O0P2Upq{F=KɊ oď((d@X:g=B762a)JW`E?q櫵RD+E|܈?at8[T6LRE ;/Hր:4FJBQɭo$,=i$Γec}"$lþ8OZ5l3qd^\oY5H*VLnn@TYÍ85(ɥ-hogYI(&r,B(C_YOϩ4T 5UFLhkgPc{0GX裆 0As7,$߼lK7E~\Q y+&.lo>}Պ)s%*0{~9O%&bOiŒ5Z0ȜfpآϑݫqE߯q1թ?'05d>f%ԈuP~o\ wkcj>qWL1q78όpFL˽2cSK9;bvNM`6~ٱf8YN\~캱SN R6L "NQ\|yS?v_yJĜK. oW<C:S:$HfozXۺG|4C[M&~bƊiĚV F\~cᄠ'5MTm~1Tפuux:0K] KwFIuȴyUDP4DɊ;r-nD\rb)vs+L+ J }:{K|Y~D>ouG` Lu뼩g:uע~~-$ϟ_F滞 H ]AW[A  <Ai??0L"A+#,AAnN}zV='%h۾6+@R6fV=^F"ԆGO<iGW$-jJ=ǹ ӗZ"83%gWgRo"wS߄rnN] ܵPd_%U"Lua0mS:8yu@U_u%/ƞOJV m3?Gy3 = 3֮s^Չ`xG4H*~4~rO;M I s~;osyXN)20+r{揵a0γCYsoᅒY%s2o|sc:[1l^-љy(cũ~>x1gӕ,.([g3c0̇OxQ]~dĴBZ3,X8Z&%zoHR5"ɱ^ fu.^E qqe[;j 86ĦSݴj\R&޳pIeb}^[.M;{`2v/]1oxM]C14?5J_K5 >{Y;"p>|q.:Gl@|61oEo/9u$ JЙOC?Elotًv ĝ+9d*1Q*qŕ/)Ʀp_oŹKG/ rԦ*̍^ꖮߑG4bw?gx'3IWm8>,Vuږܽr76=,wƍf v!|% A+:-!ǟHт,^3+S(ʝ 4ݬl4ک8 t5kNndm;\S{E' lVI:S(zƟ/6Ư%.s:Uo沉5z8-X8|m fpOqt/z7Ov I,*bJL.,7̳Vj4uHCUm~jjtA&$UUw]v Qb\FUp?GT)cJN)j}WUe}˙+E&1Sr{_8X]jFƅ6*<|֎4κTir?]5YhF׵h2@R\`E7&;u2C]N!z4Sk* 0 B+ttBR81@~bz2 Pw*^WU2( j9q6&mqsS`:\GsmbV]!e,~`no\Kd #hOquQ UP* )FM2>3U4Y}V>urJGѥ \P7&EHn5E\ڍTMcM3 yG|FF|6ŒU#n]{W(V%(ݴݫUкjKی$q?OK|3BEU5 ]t&.ۂ7Q;HJ$0FQQD5""l݈fd$8t4@]HJ$4ƷBI9k*3E #a?/Kidㆫ]=Ů^Pd5]6l2yE Z>W.[A".Fc2]KQ4)T HBѫ[d*Þ]UFes<]r|}iȼ^&;T @PQ* +izO>E ;/HրV*nYzZk;H'lk2W85SQhW\1~t<h{/J,q 0M *Mceqq9 @P]!lA2,hY0K7[72Q@{899999 L8^y d^j{EK_ϒS'Qd)թ׮T?kWAZdvՂ.vB)b>ΎKb-dঝ+n]SCCCCCC{s n$kzvM)2c]ıkޙ3݆+ycyI7?f'@}Ȋ|[QG'mPZUԶSyb HjC N)~tt'rDuAq!O,H%I"3yFZ%{{nfVT><ʢƩ(no칑U)Oz1[FUADd̿gJh؃J aTt_{3YStbϭ*1IKAL!_SjPDzdOoHގKLoB#Aځ0t=;/W@0F Cͽo]7]8;ak9lfmB5;?r/]NC'VZd6~̄]_uxhm2ª’bW+ṷCԿ}3;RxY{m^evLN 7bm$Cgd_E`u׺CjG675n3[Jώ }>NSBї N^h [P6|D?+[^r;??}NkzmCс6]Uݺx` >w6O%VY;؏=uv7'SH_1Mưq@\,^=B.j7a!A;nt3]$AiCGK09cHJ nmHxCܖ7\ $߶'Cs HG` >ƒH8YqGnIZa tj[<d0d?SI[W]\m&|ϫf6l7ee  6y\zIN?TNo7t`K¼Kc,vҜC7r_?k㖱:u".7iR~vYЧ{e.~! \jcK83 ˏ>HOM15СCڽZ-%ytJ iԓ\n4:N+ju5>@&e>¼žaox*͛$kf;̔Ӈ @\y+w}dyACȃBnfj;¹IU ԥ%(vs ya۾'".I~[i[暖Fm9kI 69vfv/ IDATOt] z I~TOs= ~0k5T5ETx?.pRk _X+G ~ 6#4܁{!`$B[|ؠt֠!U+i@\SĖђ05AAFW[hrUf'ÎoሪE_WOIaH4u9nqv/A~BT`{0m׫@;cߍ[ ¬ v˞?x]CӚx{tQEB[Sm,)g=߶`w*-մ <ۡ12B3)/ǚt<&0xR9=jUO(P[qF/sUEPҳ3sra+=[5)<<-Tf; 6#<݁ I't\4}N(/OO%7` m?|(68CsZȲ,:p9ݗN퍞 MRC}!j;')_S;-Nї؏vRWk㯘߈+) ;|tg+cϦ+Y\@\ro6 yvݒo"[x ruqyRDv۟ƕ&8?ٮ "^TK@Tbw;sɫT\᜴v.E"iɅEvu1p|&j*u>C+NeՖIJJH D:J,a 3I D~$kَUq% M{Z6pͲiݶ(Ayw[EgX{ sON2Hqg=\<|7*6vj㞶 pAvKqqՃ#͎xVt{!N{[\Ulkdvq_mpqQѥ9v^WǮ=#"_w_Jc}-GŦ3329P,c3F{8ɹV^q%䜘iTg EO_I4~]U-gtWx$4WR]+gU mp%e)2$!)Q3 DwN\6Z ^`*&tꥩ('GS8^N\Tz GZ޸FpygڑfY )7Nt9&d]bdI%QO_Z98`]{dia4iC=qr7_n j}: ؜*Qr@?TMiO_IB+wlU sk*~sx ޙj@Vw ~|W#II.(t?&؍ZwGs^r])(<}${u Dmn*;kgۼ fpǙ 7ֽ~k _>ǽ#/"?Jtz閷Z^69`3jUU߭֙>w6O%VY;؏=uv7'SHD TW>;89N I3_8F_8, D Q#7ԘaS~T.J0Gw#!$ϼvtPW-pL3;DDPVpkC{ⶔRV{"~G &xBsiC=~|6%p7~S[Ⓕ ;~-h H{7Or{fQO_QVVL߶Z}6_kd_ ڂ  HAs 4@A  <Ai?h  HA#AAy  ~AAF 4@A  <Ai?h  HA#AAO<Ĝ= }5wLgScդܸ$%]3<Y>YCG9qL^ⷂ(5Lf?I#FRvqej%>^g l;$6h޷<13k;ASe jӔcE87~xn>H'zmu>Y9Q= 3֮s^QAS2 KI+"V@R*rTgm;B৅:3snqCB8)A =g;[Y:M]{> NsP}e]rɿHWo7v^HGr0gX8zXՉ`xG6UR3+{j)j˓"<,v,S(d5aVEvk`0,g-i3Ͻu03|y(cũ, Fq[3 .euwaQem; !(!"b gرb,*v"v &bJw3}~H +ss,9{vO; Zt.A ԛqi>lc\}.R?OO>|E%!)+8t(ԉSr.8F>uy}?mk{c]99v;..'˔_V2)J!/GA)4jo>L:t:is3HV_jۂgJn{\W-ݞKN|FKWQ2 ^M2NA-=01pw*oؠ_0@4jF=J|s=we#'83Y0z|Sׄ  ?_H5.hswi%+N]g#2lQb+oz"bG - ^õ}?2M>t8ر d?;!Lz**2ny~,zTnq+2-lތCGkvֿ2ځͶ.:_S3hBJv՛U~9÷GmB{cLyEV nz)dIk.+į.pKWih" @eb˸^_kjτ#3b0~K{km.cZg ݲxU8Q/mdjJZW]Z1<` S&~Tိ.?vg{*lj;̃QnDRB.'226{vjjQQUZ̈ذG{}:]4=f )?l.0?iPUJi@ۺGE%ˀ|x=Խʏ9˻_sgC@Ew 6v!av^FEQ,];y,[4Ȝ|i})MZ ׯӆUDm/ ݸEFMYw-ȿPH`Mt9_ MMtRg;w9{wZndsqy ]άY~-Eq,|P7cueҩiBzVWUӢ-SNIwi{Tˡ}>|/*|}=AS&MдZw&t lsYp8(Sӝ[ ¯-u'w =I1c=Jk!Wes =iŒ(|, 歅gϤ6q(MYÃO۬Ut5=Oyo|:%LT3Set*v ]aisw^$6f%y.5z01j_LNβ(.hǁ<0jFtJ޻D!e"2XΗ/Ѫ ߥ( -+{mfc$NaDzj:AEU+>=@jz3nиcKYY%_$qWn嚻;hjhhhhhh;[E]~#d:w1xwНQn}  &/naNtmJުMѝ_e'Ŝ L2?}H_p C<緱^-уOzpĆћO?rʊ;IB9Yv 85OsKqMn%7sDAcOϟu6#Ll0^UKϰ2DNgYp55`IGًUcgdqy)o[~臶.IJׯ|*#U?Wə**{oT[$|sn[ m;[H^|YH7>soG{`aML[kMI럗JppoO!h3nV?)|fl9g56RKaa#'9^vpe/}^"6츐n]{Y5Y?ʾm*6mLu+6Ob.sO:.V lX̭^~75o֪n']M.(VS{14fUd)iRkBo46lD7>ʯGtN{[,)@CgKilf]U^wioV ruqZtd1Ve>hg9k{r^2=Ѩ~>t+ɽl %/V[;Ե]LtF/?u='̧߼_7QV2V)#ƭW%j;kac" Y? a 1dsO<4.B&|7#l~!w3l#VzL/?G}Z硢 %P_OnairH S<`qeiYF!jWѓ`-Z>(]h[з寍F~LxP -\fpB|򉩋vVHS5i;bgnCߦVV[B!^B<B!Tw0@!P!Bu3B<B!Tw0@՛?;{oyӥ=R$|cjpR3B<Bu_XۧCkǎO ~_tWY5>=98v0mlȬ998v?~$)w[<Ώ.~4nE:0~C>,z(Q[_EeӁc8;8y$OQY)yStvty3sR\?Kk'aKs)EћKFhжegDxCG+!TYG4=ꥐ%>q3a;gힱ8"S6o!#̵ ;F_?e L[653t&mVb~D[!}ǟKoJɧf<-C \RL[%ɡDoMpDkz>Cy#3b0~K{kmΣ'nHrYzY1'b~*_ꨮ%9Bu2U%79{;[]:v4\ gmE㵟<޵yp~y+zM ,h~䒥oՁJ̐Soo<ÀUY66iiĔŃ"'ru!Ѐ i, Mme^C<Bu.;2P72Qɧ>?fe.oKLKI.`50U%݄` IDATJY[14sJա"S )yZR܄^CC^q2reBY= KlePި<7߰ڍ@%gwzeRRQE3PPd_]0=Doюy4aԌޟX-z*X4-6262PF#(aB*(K*9SŔ%FR U=I. ;vϡ6L}ϕKHc骋eUyRE f~̖UuyMGEQ»T bG>P]db*Pa扅R En;hYZvIܹœw6{߷{r9*H;_7zEUBx3pwv.?Z۠6yR &/6m+zzdE੏L MΘQ9 3xq帼7]%hZ;l[(9?c&$'/xSPtBy Mq7vmYCĈAv'B/#&tN{[,~#ޞѩ.[Kўoam6ۓ }:逸 =Rts6US[`;Jڊʊl ǭm.Q\:F8snx-;nL>Hϭ[{5onпAQ!k)Z]F2k!i+ ;:ӋO/np!jMѓ`%^*¸Ŷfx S-,'.m[!MդsJ.A藂-!;ڂBB!N->XӆB! }4AN'/S_uꉼy;0_߸0Ý{nqѷNڽ#+7Ζe\dﭏ<]^p6Q}7Ҙj.DOϾ8 FO%ƶ`p-ԘHE- \|!4طJ~!Px=kqB!􋨋y0TtTD)ONNczZse6goy=[05vc&)րy ͚KVhxJ:~~VkF6[j8d^ztq3#<1*o*9̼{tkp#&J:aM ϲݵ4ٓ'$(9!"e/o.3IlB~8״ÐMURFjt7qn”|AC5:ukV% nD R2rçP, (oyB yP/.jik9&n$eh>ux~׎xc.v*y dK.WܽZVQI|l֘!Lx@UA)2{S&%Lx RgSBVZ!}sj4INҳufg{#E}l[56/kN_'&z huYWnX Sm^?A-ֽBtgB ͛tJk;yYME1%`п%jk<B!9 86jM'ϟ A!׏\GQKΜQH3GdTo~Ϩf w ?+1̷[j{uE\ma丑PƝܛ?q&\Hӄ>ys!#6fֆ!BynqnA&4ew0! ^BB!d!B 8BB!f!;y B`BB!N=]"$$$ڨ!1 555CCCU߱ ~ƽkhH!ÑHI9YY:ZZ KMLL;X܏~SPPРAE1@'I&;mp_a9,ISdr~^^^EaځO%%9)++mIQNg7J2L&Kx_J\B<2MU[.'P,,,ːB,>HФQ5IŃaL&-J]bdRir!aЯE&ݻ_*޿$`$Io`_fB+| P! QSSԱ#P(8ہCy\3pG\W-'mTl#-yQن ߖL:\]Cϙ_l m헋eOz s*v|'yȇ [N ZhziԒ֝NYXYÓF|̑xfgs,!fAV=#,>u#gfe-O<63[t6Qt%?QXI/*}g We;aš8-uR//<Yb/Dg.%4\QqEـ@*tyE]۵~ZK4(}-]sp蔀f2+ƾM+WlPWA*CSkch5ZjXEGX:sFBy0ghWUS+JPҔ[瞎|h2|oG}&%'x,Rò㠱;is7e@rQMb_y2v۫*!Msp{W"3 A<=}$Z#~>>-R4_,5AZIP 㞦;Ovoހ FzH ~)0%2;qِA5p3w4*yQ멦mtTjJ7tԢV!CO_ ?/wr]U"7)3zqFY+I99tX'B&f, Ou0ea H̾1܌tv#Mq"x}g- @@=z@|_<zܲJfO-^#B7xf[P3*|RT ]ڸJw/PEʢ%aB|g72,8h*cR"L!SXrt /SSJ[,̕+9)P66~ϝ]>.Pg~*e> L-@P\VyWUV:5DǕ$9|7I*HR.JLF+;s ׯFE^+B}2(:OKӸךe]hg ̳]˴0I@f+=͚)L7w3f7hFT2!"4O4lɥPd!ǘeu[k,%(QZa &bN euʻ_ P'UvF坭lkZwG+ $ EABA((usv)+++EQުMѝ_e'Ŝ L2Y$ʭ\sw'M Mc'wo =&<>tQز\B2*<&ȲxR ] :t'@{>h_D2t|qWm:>%+7;o,[j-";l\T >ҵYsm0?;w2]iJO %KO3}A=޻bI4zI*ݨMc/_zľP$wpd0$-EA) nl0''+FoSyT@ڌ38p)ʞY]w6JzQue|;LnJ.+*_ (L֢ݝ[7fVEk2mmmbVkʣK1P}{ocH1rr\N$UTNmf޾E+V[%Ro}@߫;EOx@BHY:O7]tm#aB@A|N4P hB߲ගe}*zا5P/A@z y >'|^z>g!T0@WARPe<Bu3~DۤQ_z ! _׬"(J}i=BU3~ %{K\BQ׵ P Z xV Bp?ɜ3ΥY2쌑~LL5cyⱩV=,~IqUzj_\ =✃ @!5ZjXbuTǕQ2AB_AInvzl۸u7'vR{G"5,;;ܙxruXdyS/80?QΐN8oyIG79~E6ءo&'}(iʝCJ Mr9LˈJ/|z/W&) qL9 gX]8yBvn\B,,[%? 箙Ќm};3?wuǗKk1@խz[m|8b#}A+^y#1AK;L <6h7fe@f=dԥ>xkQP2UTٛvo_vԆ2 9+sksICW.(7ʍڶ-JgࢭLltbOd쨐f=xQB߈_}8`{-߱caZK JZ\qudk7#.ۚhBz<>^o;|ݬ^ztE~̩^޽un&l󣢒e@f>jFN);22~=Z|I!C2"'Jh^Njz-=ݹ1ʤvW87\>t.,A6NHYjV5k˱nVh\C'VTVm-B!j0KPS4bEaz$a/+dbFKnj3~j}4i@*=OEQmVC( ٟnI=,Rڗ Ի7zGp/k s]4ʂ`Xśh)]Am(hٺkxB2]nA|2el9ikͲ.Z4PR3Yif3sm:Oّ\iZ<%@3c>KȔP&L@1T5y_'}Wk_QC˯]uHS7U\X{JBV[L\;?ݳ懜vK% Z8SsI& ԚYd:kӵ*#ѵYsm0?;w2f(XEa6j>h)dUxLe? ;@"){Vq[R#BXe,ۥ^0gVpЎz ͸Y7Q\ TY3MxʼnJ,F8yfwq#F/84yV߆L`v?ύ4ZP*c Gy<\1z]^KjbZoMZ\=a!Џ&?Ptt-])Y9~*C :W.SBPP\.2\.(hȡ,B?')B!~ y BB ܖw B_y B`BB!f!;y B`B ]U[/6N6meOz s*o>ē'i_w|o,n1IF||[ PF4_ 3i4૪}R :OMMMP;E؊DICy^Ͻ;p/3]n 3.Λz1`!A\ IDATR޶Z0Ĕ @fD(Shͫ04y _:w!=LRrdܱ. e 6nͩHtc:7?|C|IG\.]1/ߟs׌xpjJ̇72w(udBՖzAf\*2{Ўp6Qw!gem^7uvdALw6 Z쨐f=xQB߈K_$xUv_[z"|-Qّo>fn kq8`+1Pʢ'SW_KM.(req~%sYjO \HbslB/V@eG%Iv)bvSw 7ՖL!SP*1 IHS&8--%WrrW21@<Eֵn}%൜isg ٻ FK0RgH R ҳ OJWUi=nf_O-^F7xf[P3*|sm*v(T'w@۸ ڟFEMՖ  rJOK% 8O>(jp7L"7dɐdo# ET$壒r~順RUỹœ=M. [r83Y1ȼL e$YAUG[GӸךe]hg d('iN?S&(N9.0@b~Z%E}ЖLKhxZQlj&'u2RUitGCWO!bPS4@GOO(-BGj)+M;B`f3sm:RYi`kSSs Ĕ)gh\ SkwpW:sѓUTrQ\a|`'CQh9vL=a~_iwdP9|7!//񱠋–=lzdyUFa~R̩$UO=޻bI4: Jesw/}TXnUiݡCw DE$3XtVJ=JL NBEvخӹ6]5*k8fnQ鞽7?d8\*IMOOG Y-J&!|dN<)d%K+_veU7ỉE[P!Mm΃q̜WUֳvE2h?쟻7G!T_y>F=h EQۂTvjh;Y$%|> zL!B<-{.=M15{OZSrˤԍ93>vq'G_%? 箙`}t&IȱOw ~IYY| t 9ЙK|6N&nvzl۸u7"(iE=_fnq',.=t"w߳V,icʈ;߲ujcgn&(iʝCJ MbfH*v!*VosEO7^eկ-bb4` Oo<-f{KM#udkU@c-2U-;q"/:Zc#dJwuP/j_1jۂg plŖG^KWze'W~%Vӓ |xZQlj&'u.—9ynj-OSL@-:_+ϔPl@WcEy x )i6;n)Ȅ*xom8S<nftwpW:sѓUT625&ggO(+>!To={X @eG /t@Ur$%Kz6GwQs*`3^Yy54tbС; A"8VORDܗ>*&՟{CNAƋsϥmewvՍ=xnbAQƳ3TӠK{76%+'SL-6ݚm}F[;r:AREνxL݌nkν[nfkIH&_YO $"ե&kWO}ۜ˸7齞dsJֽ7FmL֔ /-t#=ظ~mWρL_1[F}v̈Q6]$گǷ Zrv?<LѰy)K~wTAakm|-7l}뵲oRQCZүA!h=34Mz{(`_cn:&r\Qz@PfѶLf{&!zycr~d\JQ 9mѤMUwl+)JP(ΔZmePU 9޾~CE!Bu3B<B!Tw0@!PݩƑg}~:ICV<(,"\oMꉨ&/9\ZkfQ x/dYJ̿+>s^2쌑dlg2^AIb7N d.k~FQ*Ҿ!Jj@!TCvm%nt\Q=5$"_@{'Nږ (޸`o~N߳Fi,nCR[o k7 n'aA㹅+Cb5cVo]u>Z OCqϹ!Pz÷>}toseQn뾣)wn*]<c&x4$ @?<VOO]3J|yD,f.h=*>}-U (f夣V"e׷W^ٹ NzjaErd#/Z>+7'kj^Of.G=&c!To-8i dib$:t?(2=f?쨐f=xQB߈%HkiPJogވ(2UTٛvo_ۻ({ (((jbnX}b0b+1Db4آXPlQJ퀻nojgog;m쪰6l2.+a kIW .:pc!"ׅcog9xl8[o|J玖UG,1-K&D,vK]yC~xX 7F,a}r K): vXa_ljԽQgr{iV^c9+ϟ]i.FMطJdD>oגq*!3NΚ[tƸ]e9Ɋ _vL6x?m7HVm5GwU 8S&B]ޕ(vnRzbFY˙r6Ucx=J::(AwԶU߱x0o; Rpu;8KJؚ ⹚ħN)\mM& l=m9%](g_K$NGU:wu -ӄXMEj*ļ:x֞)N&Ug<7)&*UzF[KTIC@O³TZ讦6!&'T&,,ɭhϙ -?r3j:t&3~0qM4mhY6QTkY(e}SIB ꌤsVRe(BtizBxR[_4Z9%bJ}eԨK&T`ՙuv2Se!yDf# iOqXa(BRb~ߗ{B6f]âOU!vДJDݻY]z~*-32D_Ztzo&,:d9,6ng|(&bz7[Ϧ4:ym?՝s.<,y+6'ʪm__6]\/l޷1DVw?MQd_mYCϖ|H--Dw+*ZZ WK^SVSuAs1꧳ɅwG)mIkkhwv;Y.H8CL_H{xTówqenCy<6~H"ZYם 6Yv \C϶s7nYZI[w p}ve\}Yws_(EIm U44|8*ϣ\ϙTfgA%  Os5_ķ7۸3`a:jZB`->d&dTZB+ou5DN^Q(r :W|Ϝu[H]; AK !'1Oom|S*\?6>mw$..W Ku96ɾ"D9#L=cu S/Y!.ϒ 3Ys?4b|!ܸnlP^gY`` )W^u,k?+g|ȽRդƫ[Z -.e4OpUF>f_Jik?/k@aș_?9p!Ϣ2wA-mkƿpl 20d`:<ty >eqKmV_^4vzˈ- >=b+}q#5[ &MzeMUC:G@y5YԢQZF]f\lzfRew+>zgCd/-F6<[[dMR!mXWuh)%bGFh'8ǻ|bL!!Lgtui;cZ[qU+ 4~|fR}١Y>K5Q? IDATSEXqEFgf?3py3Šo9ώ|Ab:y+vq.|(&|R^m 9Ü-=\fHP/I|bľτ÷|K)Uw5EvT Mbctɹx<ãW;^E}(AwԶU߱x]N*s >ɤ*)/i#$R)TjshDhܡ>_]DM}X59Ĝy&6E֮V|Bt%9[y۹HY+M7:1X|DvlhB_ ZY#*m!X4)\k{-)j %'ɣ:BtUk{rG-a%L ;jiF>:B_xtj1[}Xs 9309G˺ (BFBB!*-tWTL VV*v)Ov.r25"D'*e}SIB ꌤs; ϯ^ՒG1tjT0YufQ)Cj2:pTNѰE!e)1ߒ>ad+)BϢ+iZ{Pr֪:%4B(^BWKV@bbOg /8RJO+cOEIoi֛nB[hwh[U}9Ƚ{7Q[OWe8`CZѶO/..J6Ҙ*7괌@ji! k^Qiպ*\&tFP!4&GK`O;;vR$!&/iBX|tuY?T\m*2;uA 4;jXL]}X@b1[] ѡW\~:z-϶s7nYr0O!>smJ#u0t-%wpЅ+)r}{0mUæ8#,K<Rm<:w6c_k\ |E߇Lӄ޺np#h8([ O\\@ }7u[.*,)BSm7n„)r㺱Aze %z^u,k?+UXĝv`^%pxYl; b 2I϶1<0L20d`:<t yh6X->_֤O=;5sLba2>w}ym%5Wa^ɊkW_DR/(}Jש//g~ϳƪae3}\o:j8F۽E,8:k֮M{7ߟ0c+>:%@Q˒ǯ{s GYԢы6\&Xں?kg!SMn%&⋅[=9K,I!|1>XFw+W/ipӋ/eV,Ks"ژ1`b"Ie?qLW璿&f{󯮼6.-%![6[dTʕľ-uVY]鼸k^T lZ > )s1~ayB~VХmqXoCrRI?BF#1M0G>{@!.JHlE(!>銉ve'n>Q19޾aYjb”B+w%~i1y?o "Uڸ%rJQ# N&_UkQZqdvXcV["5MXc=/n韂C~xoJ/'s+~|$!?캭)h`Ⱥu!oiP~8w nmlԕlsV^S)Cޙm'|= i:B{XkN:J͚N+UBԷn5uM ߕq-[~}ݰ/mX6mި3KUD>oגq*CCY4[6|m쪰im71nEE8baD䒠&v[bjc'a)Z+NiɊ _vL6x?mtblGQ {Ie.u_8TFۼчb-XxPޤՆdXRcϡq|)*'Ux&|j]\d2Ƕ[zEl]G(QyXbgˆV=ƦsxGv:KY j1*p0D l^_foZ6H"Q"b#:emG_+f+0^^J½-cZ>?OBj۪~:fXQҜZ0|ذaÆ 3s :RJ} tR 6ct;/:~I`Ll-Y_tAQmmp0 ,B gE{!D|E7(в/\WZRU9UclBkW+>!tӮWOd7ΆEKՆo$jb2n[}n!%wpu\Ux W[QE'ذsJ CJrJ. s  ;P5r?C?O mu!xOvbC5}_}TGV_y]AJ|Z!*D y!]ѧ>wT}QAB( 2 Lܸ߲o{*iBA#tK=nAsIDFEUٓUgj:t&3~0q$由n]<9E\Gc%BRhi낋y-ʩ[TT-5W/VNjIR_Y:5 -.>#2 дTibHxRъ Da=!<=!NlCԴ-ٖR |iBu>9RNѰE!e)1/,),mŪm__6]\/l޷1.%}d9sFXv @Yzz7[Ϧ4:y՚jPaK7- #{!wluT^g)B%oݣYޡ |n%X-+iB@tyҪul- Q?M.,L;Hiw|*]7yʀ^{H/-J:}M۷tpȽ{7Q[OWe8`CZF}i]'Vtc*uAbBWZBTq0!yP>>}k!hiq}3X`?[%#|E߇LӄڸdWO+Lsk9A+3y I$kto@"{ǑsǽfIe)!WLR%kݣ~ٽNӚ[vS:/0#>JXe} [] ѡW\~:z-϶s7nYsmJ#u0t-%CXѽ,k`xizPѳvR\\@h_2ykХC\֪. Qucz=KXc0atz^gz ٹWGT* ZJB((Jx|@ y(h-у%yi~AX\} g[ژ;0 >fgoO Bʟfh܃9ٖuUP0 Ξ~2Q>Ql-Ey;w-<y"H$GS4)(aYX>uR"H-d>hodg BH$HRRyT$ucy4mڶ<lJ%X"qqqutp0whЀ h@)# eݳC]35I;˓ 3V焏i,d~zi׫|L,;]hlX59lپMm緆v}n=zlvi <Κ+b'e<#Z42uxsC[w"g5dʲɭĄP|}'v% 1f˨y6p @c?Lᥝk-WE p?vCwpVp*?k^T lZ >Ӗ3VOOuμKՅ*& [ǧjƏLdpI?gW,zێIy=kcSvRG7ks:lQLn) x޲ve'n>Q1= Xv ΝY=gsknOUTe#f}١Y>K51C;}?|}[F_,BNU;?ۨ5hx?ZRP*B@*i"GEug!!rEC_;8>Ey)mabw^m]DDvTJc!"ׅco!bϮZB>3d^^mlhΪedS֛ٶigq^ kP%^˧HjܽRPp)uo+B뮴Gf[M]yw~sG§ؙUG,1-K&D,vK]9u\!̶f"G~sϦz/N9]s94U#;tt}A]}4OsrYw>s"BZ3kO'i{f9U5׊"OE?xVEOPƲ)pu>IӺ,.ǽSK/+^q,p0G<۵}Upƽ(~~{ >߫N&_G5̍%}&t3? Om2>Ĩutuu1ênkV[Z0|'Ps< gE۪(.{OpHM W;ޠ@K4x&eZ)UFARkE:`#ⓒʒzz%:o$1vf PyQNoS9YW' ]U?9pO(o'z<OwIumWs^WPRk+)S4H/GAjyUgj:t&3~0qMZj6H]*n5ے*B)V'h"_zUTf{Y3hY}J޲ !N~B[hcKxhjQ9zNL[y]]wuL):LtDqg9>ww8cupSN=̫Bqqq [PX*̕[ {vzFQE oOMIbs ̃S=cH;+^;L[okƢ5{OٰA3ζ20d`:<ty y2m†IS+U//;`fﭯRl^^i[Iɋ۹6Le3.-%![6 ;R g i)Ɏ|Ab:y+vஎ|mmwOݱ@!lGI?ڿ#o^8*ٺc1+\ҏYJ_/-f bׯ-˦Mu&@abw^m]DDvh^㈅Km5K↼3NΚ٣lq)A"y<>)fݽ]WoZZĪTDi4"$shGwZEAtqQDwlBq0"ЅIUΨ_0[!  >|Ij^h)E1g>u;8KJX||)B4W+k .^`䟯ՊOE]QaCm{JЩ+ô+5}=[x~h䯖8)SEQThK{*)Sz(BtIyzB 5iRQ _xo4z7[Ϧ4:yBa2!iB@tyҪu,WM"ݬFm=y?_} UwqRQTwΝ̖y(L9Acfs9sdȴ5ㆻ_\q=9*&eńF́O:sJq"ryz+bz%+=;}^(hD ^qXI L20d`:<ty ALGUc ߅*=и(M0i{e?M}y3wKڟԽsz 1f]tYbO߳3  3g/^ O SZRR˷*U"o֣W_8_9ӵ=_2#FЭ~eTY7<~3KM,O2%_ݻaT3`}I !-K?n3wQDE9X{u{☮"Z8UKRIYv?W yq;Fش4}t{^yB~VХmq(xmώ|Ab:y+vஎoѧ}؉vӟLV I;t1il֥y Spm-їSJĎovIIDAT  0n"lOa[oj7-|Wz=w*>bYo\4!b[;J=P?Ҿ˧go1g8baD䒠&vۨ~Ur%r1uB1T~8w nml>FFBj""BXv#5ɽpk7^шE7.x>darO\-|6vU؁GÍܝFgAU-瞵qB!̶f8kٶigq^ kP%^˧HjܽRPp)Ve|L0䗑f`0(_1odMiNgGϾc/)aF"9K}oZ6H"Q"MY tR 6ct;Ywnwv },\먿p2K6ok(AwԶU߱~Bf.SzUNN13wMX/q p۾;m߯\6nÜs94U"T4Q|)j:_k+5{>-"f[[ 5&|m!RJGns4q9T\LѪ\HW qw^G<띻wUKKJ2YȽ=<cfmѧ:z4}l˔阼IC4Bx WGc[OrN)ݶBJ,)a)BUFAhGFEI^~hghѡ} : ŶzEMkeďx(獋",v5Al( 6r5!Ίms'%R)D\hRZv.RuV)^Y䨖U5jsBZZUFԨc 6">),YΩ7_M_P*wRy.-ZNOKdY" ,-eN.nJ[{`J̗y+ذY!-]6ow7iIa s"D ȬuW/VNjIR_Y:5B)V'h"_z&PTՋ)B)7۞JbPg$W/Um$L !'9U?[x޸]:3UۨСS5WiWk*RR_Sƺ (BtθNW9.ѧ=[k?kCKi*GI+Yq6ZςjJy{HR²(|8ˣ95%?>cx89wG\tYG>_í ?@6ۢ穂:X[[[[[۸uլ$ۥb\lraaGJۼ#kB@tyҪu|wv;Y.H8CL_"ʪm__6]\/l޷1Oue2gfu5jxYq %ף z6E1E3nA} /x,`q vWixu?)z{e#1(m7mzUWSk,`\_D!,uJ\*2u^j,:d9,6ng|(&;XNh6qYU|jڿ:yE[Yj ?|G6~ uG+͙Zu~*ԵYpäO6EOZ*w>w(vޢCiBVMos01P!6Yv \S(E{vE[qIE=>t`#߁Jgu>m\@ǵ'AVü%q^6u5eGsZآ-yhFS qx@AU7ח8___کL,t>[?†=;~g`\rM4X /1@t1.~h=D-?3qc<i ohg皦^qWٖMヂ*ׯ^"̶1yɓu1A~^n-BHvVH\[5}0LNvԔ!@jo\b޻#X%϶" A(5Fsܜ Pvf dOGaԭ!LVCJմCCԧgV{ƸW xͤ)2 'Ͷ\\}L40d]亐~t4?f%riPӄnYB ygZ.Ħ!Lc=;9(5k;^\4[6|m쪰):Bj""BXI|uWzۭ.߼is?)T?=ʇxQujEx[dń/;&`}DD¶x࿅iB`̃)*'Ux&|E.X m' QdR!,3aD`+wOMbctɹx<ãW;^ܬRѷ|K)Uw5Ev!shDhܡ>_] ! "=۵5PWwQ 3GX~*ݭ.~N)8ydaacl-,v5ATqV6* : !]ʧ{Mg _=l@;0uPS*u!Pb;:!ΊG[:w採䥶=]PS ܏JrWeH$ :O}ֆKV,׽fCm{JЩ+ô+*2q~˾ !uFRYRLL)|z4ݴuż|!<:K=AKi*GI+Iy,厡?)[Tbuy!mr4lsEHYJ̯qv=sR~[j@=2}ף z6E1Eͣ eնO/..J6ҘhJ޺GCJFT*2D_ZtzoToc~?S ujG,WNΎ' #~I yp09^Oy!QQQyyy c1 wk׮Bjݻwy<^ZZZII@²l0!YYYׯϿh?r%ooooxOBZH$RՉ ^q?! "K`0BNS˲E  ,{{{VVRÇ---mllB^<!Z699azS(...H;^H ϶ AL20d`:<ty AL20d`:<ty AL20d`:<ty AL?--1@CA,kl 20d`:<ty AL20d`:<ty ;H&''zs |\.wqq ^_`b>6c4M9YYUV˾A;Fdeey iiܜVZU d[@(4R] N?M 𰵨}gdJLL5w )55U׻W{R=抪&ZZRJ/J6wOTVۘW6 I9%u^Y,x{V?͘;cॺc:}<7iBnfx%sw {~61GC$>>$1};ykaFng o޶؍ [ޣFiAV|.7X5~}dKN{]R/2aao7NB^ql4~a-yL]{^}e<~kKgJ̠,BgHv=xEq~dKmq藋BSҮ֟/>x-w]Z–ؾrOkc\wsGc7Y7|.ZYiuZ1_(uO tgZ :nl=uJzK;2hX*Lb+|gܗJ,-V5BϡKNbώ)TeIqnWѧZ-z\iavs/byq eǎ>>%pXy(SOJL]@uj%et߫Ɋg(xQ f[xR@[.ug|A%sV\y2G ES8Tdo'*ZnG>=,91 : FS3FITc.~BZNg;Ww|kv-pjw{Ą`r6*beL\LM@b(_G~քCi_3z4u:qu}*czF.]}٥dgJ~0~1XքP\\WL&3W^eZmYY3Ve[Թ3VOWVMNN...fyixl=tTto*lTԃJBBI \?#Jǘdf {4 D"@DBh47ޢכy6t|?4zQ5Q>'à &,,f3M7msOd6`X,ϫ~I-Jܱ\^ 嶬Kk>5)8Xb6@@2 B"P   j 98<)KU+ RRV2Y9ރefP$D"H(BA($P( @(B ç; 3ד rֵMCdlY=/ !l1 ,3YY]P@?-1d2 zbtЮ[۷>~e1 "Ka2L$  @p7: Pk̲Y, t/ & ZO:eY"u֧~:j(Lv"z7F-l雧jn޼0{lOOO":{cǦMvڵR@믿;Uw31DĆ>No zuY_\[nijM='O߶[݁o2q:s5:&gJ L8cxR ;GQ\O>ɤ7dW UZ+?O:%ۙ<0۶m۲ezsсQ._-w|_ t++XzѡWo_"r= {>Tv{N9 lO:N 7k=z=m'zK*4}Zϛ7ʕ+{˛4iH$"7=_}|___CMjyz-F` )iOwT:aRCc\3Be}nMYwWC=A(ϐ>?$~g#7{H%?r5eZ" R쁖)7+?dL@M QO1bYܦ ?˩J7_ ͆5kKj0f=:{k[/Ds+cY ]x eal#VQ2̔%˫7&%/_),k@WnM&:L`vuPdpud8  zY jK.ѠA$ =5e1 kX^|޽{744T*RYRR"J(\r瞳X,KZ_33>aˢȉ 5IsJTZ^,!:TKOXTC 75::܇Rk"7ˉ/t~LG`~{iD6$sD"` W!>)g>! ܲ~eIm۸k髶#. URItړPkAq\ ^+7ŅZU'/HVG{ʢ7nkvrb!R}6#b=- xӔwgz s*㣠ʻO(| _IJP~!^;*$~r:;u8WMelerѰ!,1=يwؗ%.cI5 |BxϪMK3[xGJr۝Y5)Y^H^!ȉxu^j" #W6شi*-NXpZ,K V׶ 6&[ [-vmIP_ʉ\]UVҩ е.1h"hVko::80MfSES)3b;/p-"իu e%I+www ^xjD_[,0`hnne7h&HF#I^([kW?;ZP_B$뼼fws7Cؐn?+a Kmsh;•Ȗ rVDlna0%)JZ#q1dKPp%3csVnȔsE13w,'(7Is2~~Rώ"Ce&2so)Jz2 #ۖ^>(uqީJD=bSV3VřjEkKJ޴\Iv/L0brݴ,`I7e-<8y[HfxM!))A79?rw_YlzePڎ>,Wxt2)'qot>4]_m@ iKd29bF,gΟzo2Z^P٫LeVڕڄ{7-yظ7wnwvt0<`̋,&biv466(=zlסM'CzkۚZ>|#G>s.)ًBl vۺ9&p[~0[4I pjʺ9D$zz7(-p{:9y|MQ9 '"PKg+s(`v!"yLY~9OD$  "+ǃnpD\y^ܱGp%CDnh8RϯTO{/uyu7rx 6/-X>[<)1 L2e)^Lytl MyQMIpu{gP}IJcj9~S ?(CM/#DD߄ 2#REGIXةϏ#khLXCSīhXۊXa2 _YPDҧt*'V$,wCK5O}:V4\k8[9wE.U&x}F#=lIɢN" f6YL&&Q(չ܅nk W cǎ5lb8>h6vC%}{Ų߹~)ǚ/jW(\ȵzwX0gJ'L̃3 >s+;1K1H,ˊxpۘᬿb疿>k/Z3-c`e̝J'"ggƠ9NőKA>U>+]Θ&Y,`fRjP92gq)b1#o)g״ C"6T5CD{ V`Sf'ԤeXPߜqlwveo&ߩ}޺ִEݹCer'yNʼne2k˼~cړVQ uۄMK3܇ޑжr•kTj8U*ۘ{-Jg U+9;U7%W1I۸]=mÃb o߮P"ۉ2T*k;Uccbnauu5bUUUQ^kY=z0,fMD$|6Puݶ3Ы3vJKGQSM?OY`$q24.sS'ڷx 5rXT0?hwWL{N~a^\ꓻ/ċ!"ͽW[y0!rxU~N%;7 dM;]=U?;ѝ"  /*U64ep>!>,6$Y<:zxezIMw|x,_%%%ZgϞ...DPRRaÆ9sdbn?ûf5qJ  %"d/}4 agɠM_5-?m.Y~3?~^d6 {4ZH̲ k+cs(Fz/ =KQt#ˢgM)|B†:&"bSf.a䡉I3sDojJtK*/ۗ,Wkxe#DhuR ,I^hRkR}y6oZ2 p*BD- KIM?#"#(2N:F',|lCS$yIꪕDGPtrH{m&fnOr2w.ԄhuR^aCݹgBf<~{l {m[iN<S4cZC>SfgFD'֤/n@b\/,!_*'/1?>5Wo3R2|$/KOH75zVF$<{ u2U{.;".sU 7/+<9sjǢٯaX,Fb6}B![nDlLg"'2l6z2̼b1[*nӧF'F-gD :Mqڵ;v\.h4gŷnORNٳ۷Add+O7nlllH$={lhhx_}պu <8""ҌFcBBw,W"V5l0t.$\qCJ"::E HC= Q~Qd?s""X&M7⟙YD~+Fg_Yx&eoZOSi|zN#?2Ȅ`L]Ֆe6o߾wGwG"2[,XHB"'[,4޾eDhHl&,DqvU~p6߿oǏLW^yNYV """r<,,W^{o%A˲Fzm)[}l_(zyid!eJHsdpa d4ʗE{as7^9m+CHȲS ꐲW5OZVS[,@'|Q_:3^ 3%T_+U_z2mY:oiUDvB b6:d$^O&xgV(fh5:cbNnam5kֿ~o z eKFQ(Д|[Y=ycv\T*usw}Gd%E(tq d&LfYHgҺu7<\"`%n C9/@$m'0`^y-KkR5 雩x=d1,D^7,""뫳տ/4a4$Z5: bX,fh`/ >-v\V[`_=Y=XCgI"!D=D>~}/)˾:ZTTG }VJ"Ag!''!Ð@>*!) ) ) Q ?[ӧO p+Y,/b1fd2 [n("u,D1eL&Ʉ/<) ,dj~)Ѥpl&֬,'lX-P $,Tmq+ksP(D;*<iUi00. ) ) ) ) F3(@,,,,@DQ e2 lFQ/A(2 #vJQpXX,l2L&z&00K _`7ptt|+f0Po/b(ݰ扔sB@@@,,,@@[57&CHk_ IDAT^5~l?T5ŧu]ZmgPO{ݗx㍙ءoFWT)숶6پzCQ_  K颻UȾ.fӽ2J9q]Zpsw+}E2(j^*BV7Y,ƪسuJ^r}6$#"j-~.Ʒ~)؍W<{PpĔ7 ~8iaӀ\RjD=]NsG\9ŤQc\͟5dbkT?/)~ҋ N9Q~J0CbxQ%Ƴih8?).KI|N]rs:62㚆g.^?D_},-e]"vȌIVM̘ĜZaHMxˍtLL9|#"F1rQ1c¨'r0ľ|QBDT}f0e}u*Q)џ&7RZ5;B;Qaaƨ}nj(͋EUr?O'w_ ewqH$6sP{vWIrmW_׵EK  v?HYI w^щXYjFg+*;pH$~{dPneHR﷦NzG)-%vMk93fڇ'EWfxm!}'Z1 :}iӎ=3W1BM;nOr!-Dv,12f_}<~s\?t6'fƬ/>dC/?}$ݬ9.ʐ=G<>I..㲞!͹E7ػ}0#;=UN~ѣ?Ot8~MA_̠ޤ¬+z""wN#?ĴĔ |{b uD.ʊM;n8^GDbڅܛ+<1Dx,.0hѣGYq^uCm]/Ҟ}Ǵ?Oػ+:"*wsȏ;Euez1i/wz7O D6-6>?g 9UcHW}YKccn>2y^x[ )=Ea㍗j"=g DRe}yHD{՜?F:c M~Ot?ՇqbIhȼ\ܾ_jkR@)IÆם8?)?xE I595{."s+R"rz`$IZsD*!"a_9";}>$%"盓gd|1't#"bJSk:Ky~ltKG{<3`|F{5ھ̛+>X\uj a|0̅H_rd_3,jޚRb];ҍ$norL5csWQaT݅/QS}i[G{ˈhdDDR2&+1eW1_ҏ"'5WAˈH_ od\'{ "]?[21^ z:ûRHM7M_G$"2H$""]Y~18i7dS9 l:8竬L,f{um Q7d}uKǒQʣ{|9#k7je#B.뇣[!ꋷf~7g/gW5oyweQ+"bX7Z嗰n,x""d#ex5wzSN[KC5ĺ}<q#ለ!iyKۿ.cg%ف/cIU_0#盓YYVɜkݩs36SDs*ղN..j0y#,dB[)iGN8n} aN{]n9&&ȕ ڊ/|Yd 1577q\C'vj `]ct(kyPܢ mQ-ڲITADF];6 FcHLNS#'~eѾDn/5~H%~YhDTfރNd;dƔQr"͉,Q)cȉzUDurWz]`;[23ft 6ŢgNosWFqz" eY""pDRk9tzWV2\m]Us'r͵kW UgM=,]h3PH_4yRםXwxȎIno =n#g_D!{A1)n^;w5?a=ߢzdY5-U[*+g{{Rj{x?_h_oQ6o|o)tuq4Db"2F+ѳE8vReڼ+E.PDTRAD$57m`}(u=;:?u~N{ ڊ۷ zqYƫY))))nG#Q݉+1+ڵ\un0PZJITMI#g^QyIo;1I%My;'ŐII0EâS͐+hkW50U-+{NLK0'bâRI;;/8o"1<(]!T%݅#f c!<'-z0m,\9rQTԐgt^?aD[6a,+WAǬT6k,GDͨkMqG򕃟$bCBkʥ ݽk6:~ym=~kuǗ 91]v~7qjhɉloܮ|MD$5nׁOH{`47XZ%raP%&:?yӘ/n LD?G23gΜɣb|k'_iI>e_Վ^(޳pYQ{(/ck&;zL7 |ZlP`㱬W$4yo[^vz}yLtr1݀K mO @ M~O<ܖe/>ޣ^$݅jmY53'ĵ5ljEDƫ'+ƆrΚN~7i/lf[iɯ6jrr*1unjь-4wR2V^˛;tСC&_ԣ8X]m2QJj2q×N=t|lQωHZkjt'nyצ¯rjHq#cR?cWY:z Tw#~lBg 2ߚy1)K&j.°n}DL7><N)ˠ:}䚔.lzɩYDDNR1R)K*QLDNDF'y+Rw)#Q3>nJ0w1m\|}G3}{v]幺܊ܭk?Z\1k;V~rƮ59 ~ ]U#Iְ ]?}6Y'uC[#OwU` ezT6X,fd2L&z}MMM```W;},""7ݸKDDn?2oWuW /Vq){H܈ʥekNAպF'"?patys.Kñ,$L\HD='ksve^j3u2mnp{kZHF7i  _M{ƮF"fiN+?Sj[aAWU%;.=`p`4L9O"Ҿ.ttz+iqUwXe֌-[z>7 ̩ҥS9N.y\߇阨e-b^6b%DVwX.\*z,H2*uNm;%o #a:EƝK?)n_}ZbxФh%;WZWQu}HҺPw|) ƴQKwY1z[/)n]߹T5WɢľGkѕ&[cM2nӦKIW{)׮:SsԒShM;:islfF,ia.$YUۺ5}h4Vsek=g}] {ˮ┨9H>dvF䒵 |Fz+ڋr눿k"!>-lTBUrꋑpUTi\]$w̖d&2}j".MG#$ֈŎߙ6qӲNL)OY?-0~Jŝ∈ْګkwY#zی7<(t\Hy9Du.3ikDۿnzKsOrXX[y]1}韔T5a쨾nl9a' =51]>7$DSC_>܂ߍK<'0coćևmP+^ml+bƵ'q:FcKΨ(kTnUD=;wQsfv2]bkSۜo);=)Ɵ/lynUT%}MDD[.lOxњؘ!l3mG11nmYD#؆7΍&GL_o*m$:"iK&9Rj2v Mr%K/7{,H%VlWPlZнe@_y"VIZ-d+䣐y\:eMB쏡rIbW|J?6x̶J?5ШRSݩ- (-2zon6U`-.-Z;:i]pCŻ;Fz!≈6eVT@J3:\/Roh$j Ir91+ Ksϰl {K-ӗ74VPWe\drу]ޕ_z{Zn@ wSmƜ#rƪF^DDaKsKJd| pRziJ3:OE~k+v9H4-i5nXfA9]xs%$2R=vMҹ8+5΂ ;7NII˷SLkXs? ) Ww%6;jV۴̍vw%m(U&nOi*4J]dE(;7_hXwȇs"{bl|)f/DGͻ;#b/((W]R%,i]nHYO =o: )kkaUAgm&޺2-hgl/tkl[gRmY"iӷ7uxg;̸H8""1`xDbqq#*8"ˍrk7I<0;< J /l؊{ͽ;mqb}XѨ6}ss~g.ge]V8mݡdvؖ8tȻ倪P%"V?%"]/m,^WgK7\-:MFŋv3i7;m۔UW5&[olZJs.yZf3+vNgRoV}_K[}ǭ8ڈ e< Zg4 f'|qK2xջv]ۉ@[2'q㎴icbi3;GW yϠGչŵvYF{(l5fq;C۪Gƍ"-ܙbE;Ut,͖ItH #KT`Ӷs40k*}wڼkbCn^!,_rg5٥Gwqʨw˞UI#|*2tKb=%~h]/M)9nYR\WwԮ#} %Sե$촓5Iu ""r"qRd2L۷o{xxtML56'ѳӦ tQ < IDATY{OD//]<+⹚ʳ_}&ϖs+y"&YD} ͭh"RnvANӭڊ6-<;:~=[+n=QOD=|O3-o"j,>k=^"" v6Qݩ/2NXs$Nvz:"*o ۃ睞Ha12`MFڃ⽟g܎rua.مu&2*8M*KnI~?YJN{sj'^5UU\ajqwo{PB#[d)ީq6mnp$QGTH :<\Js삋7g\<67"$*RvD$UZh[t#{It1 "gJ+mS3จavIbK^DGK/UUA`!ºR~+C7JY(_#M "9DE0SG_r,,&BpfxjCGGr: `Xck[lld,%-Z B!ZnZ9oG 8?C/6u 6(-VH79E䤎95[NJJ}0m*ɄGF`$|4 zS[c;TcTkq'B!,&?78~j~G7L^;SOm~tcaɞǟH  KH4Ś쬴u\q~j}p4<c}1X߈I֭r7 tl`l켿émKY!Bhٻc#FE F^|ox0iiHIKu_ةQPlݰ ےQwź{bgK2A!Bf4* `GW*I\fԙؚWGzS?a3d-D+c[1B!B(fG fnȡ.v^@,| t5A<7}'a,ő,Ǔ( kUkM~Dw/DQ8ϯ$4>M{_lp@gRv m<گyޞ̍_ "t'#5UF:W5X !WTtv.XlFdY7[-TǞ:} $!R n;jN_ca!0OT!\GB6wp7nV4vww eMTms+9d.bn:TC jTW;[j۫hjqoqUxA,nuvv7T;:@Bo|xM%W͑b@?S\ݜX Z_?JU~a",'^c-Z'0KOl`4 &.Q))*C:t߄ӡ+ R?+V/oJYzKyW}^ q?o?ǟHJOOxܯM"/i>G&elykwb,}+pMTO>իiԗ&&fԵ&o/M|rQ!O]1BL" 8*YmMf'+\j[Q|f[\W^wZV.no(oY;+KL:9UM-6U0w|>SS+{::{o, [cyNs8w`G(1-SBOYh)RiԒސ |)Qjr`gHB֨%s-Z (z6hIMaf- 2!֒=s1hy[ kvgk,P-  s&gL9$PHz("h}ƨ|F@z7@GO$cooL(>N~R0 P{ڊU, ÍJ0r1 B/D3*@f(4 $1i\77kHtWA@)4j(VIy.~BUT2v5(hO*5/"\{"{zWm)C9Z)'x. ezVzZ❙nK14&$lٱ~R@tG7/KS?ku7~i)~7; CCpʕ5b8<>4 B/|!^5L|T =r%{{A4\[5q񢄦X 7jhPTs|'AtƟBMTJB Ngh xN95}YB?Zf-6r@WcAOn/Z,3V}$qx>R,6$01+GkpuCsQw ҥpi"IJXzIڀ,c㣧?"'ȵ¥KI1~ԩhwϺUb8M}KӚ @Rb+\w4ѹ79&vUҊ ,('t dox\=)[ ,g3L_A1pJO xoZRz:fMwoK233}m"g…^+L=b=~F_<J$WD|WЄ &d$c:dizꃟk7~ڤ KPdw-,1Rk8wk/{T24iE_+r4ͼ/Kuw(( F+*nW*|Z'gfuvC*ЭG[ wV9Vq1<4WC2Qh4LXOɕFF"F+Tj/C]v>2Z[%䚵Gq7 tQvE^`Jд 3Q$Zm3;B)1+ⲇ%y  eQdAX%eyy\EVX- חXs 1ui,WvU،n6AZmvJsػkVZUȜҚJNZx>I>WZQͦO3[7ӄ IDATMQȸ$a`2Mz3qb^wlnO3;\K06sc-WC65-<ܸ%W1fFqLX?aמJJM鷓RSSd3MNo |ZgMIɄ w,<ҽy2k_hoy$9s%ݳzmߘddʧ˭ڻO VH]|U}N|ɡ&va߽*yXǫj|xnc^s .'=#(M&o6f\ݗ:{LLf4@J[^cv8L @* U3R!mL|W0 =tHU Ei';1WwrWo)fTk4}Avq:Rw~eZ7R~j)ٸⷳ,#H$x*-X,v+j4h5Ć]zm-B=;J=7pxivt-(p]TƧ~*)=}i[Rje-=$kįMQ+RRߜ)mιCBAgW*רI, r&m@ yaQ?L\ɵkMA c}aVrW`ZHmiy.6\o[G/#tN!N*׮K70wYBha(L>Bh2}lqwL/t "4u,;>c!B!0B!B!̲B!Bh9~ d|/KrU_s3?~Gg熶g#=C+%MoéWB!B\=:0U_?5b[+55uddd𑑑Tlgj56`ś,8̭11~deXsײb\!dԝj{cB5,wCɫ}FR2k<[/ײR%a? O>c|@w^N_3mդڟ V󅻞赟O]ˊN*Ưm]+lRH"MSfM*\<;}'4,$gEkH"988~`ffA|ttT*¥0&dI,,-޴_Id2-HbtXb_ _ˊq*_.>z/oGR//ޟ>V?<ٺӣ~s'Co.. o0<)D63{U|Ϟ5} =QGfdp8|aLvE㏱J744tŁp5̹Z2 Xˤx_"$ xEkqK"%z QFeγ=*YpؽkVJޕpuPoddtc϶OjGN@%h)?pcCئ5Y+%&k6cCk[R_^x $ߟ[K?uC1b΄z`w_nXT;1XA]ԩS"aOMM\v-AWSNLz !XP#Y*MSaO$Qe{?N, gF׬A$$dz*0z16YV?:pҳ% Y=k & \ WYjzz2\84YRWfH@BJ  Ycܶ؉7~y Un]c? z~I]V"x) BA-01`ߩ(*saױ(/7e{d.==͞]JY_S!kR'?+HH%zC[a񧚕?7UpP ѡt:9>{:§y{ġ u)V?/RKNlY;&Z8B7ikX!tY$,{jV׭S[ZyU0~y~RP婟G>ɾw./yL ƅq^<m)`|јL=?q䞍g^?NW}V4\Y~1I:. ḬKjT'idp3t B!Zn$=ÞZoaOGZY_s]?{h\ wL]|CR7y.W'“[x߼Yd{]><{[EY/_?_@f'Ç~ܕS0Yf]Wݡ7{ԩZ.$Xx!Bh[qiyq`t#WB!nkIX!B!YB!B-Q7b#B!e!B!fY!B!YB!BaB!B,B!u$X;ڶjcU.З-aŔC@!fY; IVp"Aw7&%Aΰj_YYB!p,|YhݶͻRccg&D";YC/|A"'JUF ܕ`@ʷWm߲K^kGDZ[TUS -%]b;5 !Z,)+卍5fWR@cնr# ;zuAEƚ*Qiwo>wCWK9鎨5Er.itj1B!fYed[YlKIaċB1eDC!Hem YOD nfs0(TJ HZ$ !Bx_Bhr<mM.8Pr@P$BcFjzM&$D!Be!)MDEfy"e"(~cV;ZYW՘m;.B!)YwO`)~LoB/t]#/ug("!\ _7ǺIۭ4j52Bee=X 薸t( ={vll.iTF])#MeU %:fTf}/-ݭ+ kJt9|׿t[mfY!rB,t,^? G}2ҮȨF4E_Ș 5MOȵ<ōkێV>K@}ujvƃ B#o}]zyXG^7 Ӟ*5y5On1oק{)oaq-E`[J(ˁ#\sK涾=֧+{d0Tj1n*ξ'-uވlZj3We6kn{d;p<Ǚ#@pNhӅ۪݅0e;X*6ӳrGmghI>mjSB*Kx˵;[ wdqY_h8k7\8+U*Kq(xZHm)\Qjڭ-ZvyBvM B-.DUqM E|sǮh@CW|[Sgq¥eY3ܘ0]>YʚrU*%GjZmWl#$wJLN3llI |Ot:C3|ufLU,B툿S:=!!kKt:.vtG!YUbbt:.Z럵MC_C^՛n1t0t: vAә#Kb["<_ l ;{1ۛXY7h6\lne%:Tn5pE5ǖn1t:}%:2|m/ʲ`xּDsq75`C5ZVkq9fjͧ;LMDƕl>.P1Da[@ffhK32Xp]x}6 ;lo t@a;G_d765SlsesWӽ%#vy颪l[]}㻫Y)e3Ȧ&oVͥY,E5U@mtM! *Ws^ځLtq}~Kv }]XBxTuA^YN5.#@ՖjGkFͻJB0"Bj^ʱNwU ,0áκ^Q-&T~f1kjS#Pk=]T_'gB(^nf R!G_j!͖fiJs) hͲ Bddm2yV hVZ#+?ںfbBg*Q@)93jA Q7$aE6|O? t~Ycr YZʵ1a KUc=-(H[i!VSƙ?H_? f2S{uQj-zD#$|>uq1јJ'm/4B }z鼫f&G* "ba^Y}ө9`mAn) <_ Ѯ#myX o URרC{}B)S)i 9lֽ9de0KܢVlP* Ex##f!n7J'l5jfDg 87 7 -SLc$; >m1$"gY0R# a!q+daДvsYN ޗмk޹y*/)mw7T6Ӝ2~ԑi&~*"Ar9f9@ (|sQڢ2^\Q*;+ldLNĽ&Su)ZP^*ޕXµxf7!Fy#Z_U!D}q0cKfbN)[Os EDZEs&b5J:] 09I.>jꨮ]Ygȥh Dp(@i^_X> z\aiѺOf{^;4Z@hK+,Jz_䔜DmxzD $or&l TYtGEηb$̃R")% UnbəgI8C]`V=3v?z*۝uWr 9d$\zB17m}֕ӕk`y ?%;7qDJ) LA_h|'YF#J4kO>!_p fa$+rKTPrz2r^]QIdr ﮫlovM|Dg#,Kca SENQB)\o0DZ"nW(;y>*r=!v9A@习#=A^nZNL4l7qAw,S\+HxMaa7h> Ex d̅)D@LF_[ey9 v+Ӭ\]]QZ24>Ow49Jl>NזRI:}M\.R]9/4F WCg^/g)̗Co]-+Frƪu-nOm1e@ \ޥ};y66U$$BՏcoa9A*SHD<ipun# ҙ7Q}f SEѮVG尚ʼHi Vs> ">',mo8TFXp%;U.^X/6N׺>{d#4@TWo[Ӿ_yPfD$5ȴ&_wg]H>w$'l5U*yΤIԚu:E+("q]aМR2(ݏfyi8䞴Xi\{YӫtTDpu4+۵5.[_zH"Y^y ԒzZd5{si99myE-tTJ~j^_ 5YWmEx1eԟ9/E rh4OD@jJ+]e~ini}I P/9xyD*F@Bo1UY_nTv,5RrړZ{1kSV4JDQL\+ l_GKkls FUyϮ0uQidyyVZ_V[j Z[cъ=%;weLq1#[Hi2g'3ԗ oe]~Bk:u/vرի/)3'cjr׮^[b^w (/!o{ks7Di *4Z9B*&Efݛ!+wJN 3,SMcyuvtqreoKMt&,N6R2VSJ7Qhbbj!zFD"f 8 3̢"~PԔ38&!p em ZvBMuт|9{6/:5bP0xERmF{UٷtRnw8"RƇŇ! jL|kYh1! ^BݩG>-IBhGbc.Lp !,Н=f=3PzZ뼽ӳNJM@P17  !, !,^ڼ}_ni|~WjUC]Ft0B[G "hJsy \_4|.;zD/^znfB!YB-Rc+ϝ^؋hWβv\+__Gu0B[1n| B!#}Yc:=0 J3_]1OڧHoϬVB!؈[ i9{LF>O'Gу `Q#B!58Z@dzRny`48~3"Opïx` Z`Y}ᓇ^9\H]3F^?p`,U3;օU}o} [ild,%-Z B!ZnZ}15[ɚJd=}GG^:pdSQs[.8|ZX&d>VTpj_ֆ3G =l>p'/:?6vPAI3;.H/ssjtl`r_~cX2!BhkY| @BQiHmkS#//R6*ii5YicUfe ~m5>rt`m_U(Ǝ1)`WA$p''pr)~^| cChS JfaB!0˺7QiiO&&&( ={Vc Bm1n>xPc 8xl$t0;B!fY7k&/z ]!B!.#賏>qKdffd̲Zj&}ݽO<\~_1 6#/䒓Ai{_ gkfYlƊ?IٞݍZyoJkT\ysٳ[m;"H<Ϫ)o[[x5ޗ(6lKE'd/dC>1BgÚ+0#5ZV5j+b/J\%>Z\uB][."`P +\VRD]>Ho%hKhDLl( GQ߯dfəsǡkZobu $y$5jTPP/~l?g ǝ:up<Әe!]9X/}:]O]L&}'Gx] wS^]_ztƟG! pe2Onh%F[٪òL]~ul۲(fi^hȜ~˚8Әglam\5Mc6ql]G抹v>胫QaˮU[vR6}W}ػ`gEsDmmmC6|. 䴝'^0ن }_k_Qė_o;c9_żlyXgAe >5>b NT~wc^}z@CˆƲWG}-0L_W_R*JeԊ1WT8-Y+Ʈ۴2LH Os. 4ܕ2 -G F_.&(ӜYX"pu l{e+|"@Icw|4)>V<|}}&fY !I?%OT~uɳ x!xҎJ4gLJg?/xm> [;xN>q:;'!'[lo_4xjbOg@'FҸޮ7zy;| \$I!aĭe4cBMϓ3 B 3]6'0)Mk;B2sx!aa3uu +>ܪZ{umN$pyhkwet맴lYumaG@ш$JW9طŨ<-tC6o//_4}D]/p "H/|ϗ18ྐp0BumIѓC}̪tmG/Iczblu>ma.lS qJf[hN Y,5+41xWG@IM[<޶p{=`OlYݱt%KD+bCHoqd9>kYmA~ixt-X&ͯ I5;gb)8`g H`lNM. ?ZAF#Dhh8=4?;ds.؞noG~|orsTv77wRmّ/BGM qUnh4G|.;\ @MY?^O-XpCK`zVKxa?8$,rvO Zk-]i(-Xخ[ ެ8 j_[Vf㇅yؖ ݝNR5cy(?_!Or5SZwKf! ;yd>1F8䪢?<_\ܧ_1u{ΐ&<mp x#!#. 勚riHxl7緯~gfa22pQ8_)XvC1GcqҿdXi-ks ߴbpdO\Ԋ %Ĵ|Îٚ+"896Ŋ/N Y4+}٬-.YuMAyAF#c,7|r___ooooo/q,˞:u*44q$XgJ?<` =]hْ'u~oO>G}8*^>԰s65tdWEdN9ܘC"6^_5&6F_`'!͚ؖt!XfM~& *_LsMY" b߲u; ~iy-v} ݿmawo1.cj^mJK6"N{Dy^1kkYmD\#0j_KcfgaNxJ-F&e%W"攈ly>Ftϣg%Ƕ,4u9^vv[*lМq>'f4өBC/>9s/9sSfzgE;;z~WY9!RR=`#,3gOĆݧ,A Egf1$["LP`*cVD> "X)/158p7=,4tuA+7> c4򰖊JU+˲۸m+T*J3 **l[+TC-7ZJJf-T"#UUSWD E{M`zERETQ+6;o*j{h`?۸J[Qc}qʸ})Bqʸ}{_Ld\aqhHRT^y`m"UƳ0jŶFzwn,k.^463xu^~W"UQ*, ʢeqDE^Iv[2zƽNԭ eԪ2aZ*z=k4SֲkVGldc?Yȷ*WĽR~ȁ%^QjЋ7!s߯l0f%Q -zZ4뛅'XATF(#4)m,\PFcRt5ev QC#2=V!^Y&R$$Hݳ5RT([cS4JBh ց ӜHY`jRTDg;NuuuV{>}o~0Mh4l޺Ǧl2ji`[>cN^\vγM  b9*;Ĕ;y5SiGmZO--,hnc[ӃF;}azh-vra-ҷ5PKefX<=Hzխo/~n)GFSݎ kOehB)wnե#j16>+5ŀ1H^L8iޭE4ecJg44 j#E*KaN]drtHmVKqAjRdjDV*x`5USV5Pk$u :jiiU= LAJCaz+y%fY!t[HX,h<>~qlHWOltsP @3N8O_@F)_jXm?)x:PkZ+5knwٴ|8 ~ sl+o0!i\}Wӹf)Ru*lAUDM B ױ56FM]0}Ml*3(j0b@?|I1#R)Ӊ/NpnAn:EJ+&@69nгbb<ÉB#=͊Yu Ygj.vsC~bJ;c9F*%$%T FJ4JOŘMM,n*.J"RJ &H".p&4 ][~n.L-$ RRU:"Fs@LF@J rT (dۈssU4OJ RB4BBB2 NE?]':F\y\@ '`&|0Oǣ`\|'GL)I y\@0|Ik2g fv9Yp͹>sYw07g` @r]ۖm9M|yoNq.jKia[jNv&̪맓@J I yir2ܕHZ*9X'7=Ocwiˢiu٣ qJkdP*S]B@_gX`Pf*dFE$-3X 'YwK{? dQrc$a, rD4 ͑E pu6D8eǮmޝ=W$"ƿZa=jL}2b]il! L*uuB>$nHOZy~GXro.zr]ul\qqe22)8\@x?U\4G4ǧTas\٪w*leQчl|];Wa邺:*)aDgf8w&u H P,cul7"ѤВuu)GS"2HJ@AdnQ>^Q!fwUL r£@ų`8!)ˇlte\:();>>bF 1}-,ek0dV9VV>ۭ% ;t{].&\+i`mL㓁sg@cY>^Y2g o_־%=!|p93t)Κ2e_[\G;9{NSeY+1Zl+ݱ< a<mome H=aspoemwN 'a?oJM:Oipʊ[f.B(XG}HwLR7);J-1B\% VB\D)j`̺4w4Kk(,lE>04ǓJXkuIcXV %ͺ:;{y(5 IDATk-OxR$"-Cfqp@"":Lj2```u.JdY`"@-*/)`[) ζ õwl;-aGqlֵI,i{utofnYbm]H_Vq0deJ ,Oi'Ly5s[)6ksj{2 ؖmkk+K ׇE:8}pǢ-_>eZ+B 0<| q8M*;wY)z |a>_8=v嚗nXTc0Y޸9ZϜNr-+Ի8O]t8\:~״ ?(;1>+YN޴lD`Jxq㇭y7\px27kk9?mѦM!B|ƪw4|aoھztq[g=iTJ6p p%sDX,"EJ^4 1)"Z%&nHT^a~;dQdMiBeP"I^=S bOFnVP#ĪH\Ip2 7横剩DgNޘX ٝZ1OI UNv:m %lug%F~J -]MK=7&`AF;Dt>t ^ޓ0B# }hfW p]>ߟ!e1=n36}9kXa;6.=k~l8~X1ѫ21 #{ͥPbxR,>>F*#Va< YBCsԳxOUcS4~uܖ<<c,%^8t_|u9^ϒE|>@QWEK~3۟z, ZqG!BhN;@dlkw2 a lkwAj)pzx悁 ,*>|6vWn̡e]Ӗ\ jvXl86??VJb1MeJYYW$v,*-3Ff*F@'fY ՗9bx'fy?A'$N"'F9-J=$(h&'J a&2`Nc?2bp ). @6WYJ?hM_>H.|:/-`mgL9S%R99zuᐝ$;Ye\͚WKВI!$yMü A1Uc⣈|6?W~WT;:zOg@'FҸޮӽ<_0wF?˷|BIC®3_er: @M_>eZTΊ2-f&ƇIIR0E@L {~' cѣ ۲D=y9\O/_./˟MR斮 N]!nkۗleIŵQ@7n^z iV3CA #YBC ;?1vQu}IQcQk(˨ RMt'9gh>[ !m-*kJwqp;&w2 6'Hig y6fFduGwĻ5k֬YS^5k֬Yg1Ѿ\̦3=_5p=K<lg:sv'PwӎS'BޅB&Kc} lxkvf08_,>\֚iot 3M-6:nkƃd 2z$][}("'ߍSL=={[# -.7!!~q:\"4(n]!T?o(/А9Vֶvç] jņvZaGȚY[`xK&8vUS탽>d'BL>H½0iqOut(mT̳_ߥ9S BOwmmmK~B󦏿o:g4z&RY>vwTGاۻ]mΓ_S_ "%9o5LL=]蜊iX-){|v^D !B.yyybp7YF7~L\D%ۑʿڙE=AR?._ן80pP{eδ6{aN^E|a &Z!F i}β["thQ'ܭG2om໮ˇA_C.'0l> Bqy{SXélͮf:WCɇ~B!YnB?tt7[@ٓgnҸ1aʋ=Gy^+ nkmkb!BGܨ{(4h4G_j,,{M9q%^xXl,Q}e_7Uag]bɰY>XB!УFrO:[߮*yp$wB!#[]_dj$qQCv'^)B!Baeu}[Gv,7F"lB!Boz{ "̲0B!Bo5 B!BC,B!J0!B4ifYOtB!D!B!̲B!BAug=1d莄aB!#۲B!B,B!zPYAB!B? ۲B!B,B!, !B!0B!B!tg1!B SNa!&LYB!m9eVQ*zρ !cLHm>jƼ9z3O<3!##&kfaKj g32\@diZhR'(.wK֥%GJ+dyU4W?y$j@PP/~l?g ǝ:uԩSD!C xL^Ya5ϩ$kiU45=OaO-ncei*t?0BZ]p$Z1s%4 *KNJÐZE* c89Ǣ̭mj:Rˋ~a„ b 9ND~aHֆYB!vh(13_Gu $ BP'{F)׵3")&f y )J  T p S)RHnqk)7*I%!`Mӷ4|FrMߢvҢ)*(HAAg%e!BaGUΓ؍z4Wh4jڽ0@-gXb0*t)br\ HVJ(R u?KU0Ŭ*(IS@P9|@wwSKh ? AFe!B=(HJ-: )mIγ.lU?<^շ #u3^6 ʳ#D?f8yruyIW$Ad$7Y@*!\ ?rAFe!B=8(y|Kg4*եMsH" Xk]1 @ɢPWwh$ׯnXѬ**Ryr^ϩXXl+n463VRyi#qNʢPWXmesqADG+@ - H 2p,h;zz9})?}06!4Rh9Z5p&*tuB 8&%-lQ6F%:Aٞ :f^Ria\!YkݘuB& Yxc\ жӢs\u%"Ep{ ZmȢ6Kcq,u֋¸=UA2/帀'L+Hɶ0#/h$x"t]|8eO:: ֙O\~eT]qK}y/+? Csu &{z~\[` #NŲ{Xl9|fԉf/,K-n)yFs76~ֺ)PGo>M~mg}Z~\ԛ㦎ƮQџjJN,:p (/2g,i`ʻ%%4n,.E**Gau/egOD3S~n `Ѣ33 OĘfh-Ra&(f0A\1Yrm vݔ\lueeuuܠU>| MPni=٘'N 1Zb4O\nxד֬u~xדcSg{{X/wgj<گ/aGg2x& `̘ٓo錣n |߅ύG< WKǮMt+sg.L=.v[@>m_^(ovsޮl[6%rZmۘghc(*);%BD*^4-mvJ&7V+MRVCNF $OLtC^bj]Esc.f4$J٠Uࢪ ĘbqA60%G4R%`7C0~,?&tZvwM ?VG _b|ˏe.'Xt}}&'Woq ׿jO?S=7Wx_-ǹ:za`OW7k@P͊Yu Ygj.vsC~bJ;c9F*%$%T FJ4JOŘMM,n*.J"RJ &H".p&4 ]h\ZH$)2(=u+E6 "Ku :AA@2ZNMX:7Wuͣ#Ҹ{|E /.&oqxOM6o/7pl@g7=@@ߩ^Pp\'F{Ex /kg.\EŖvcO^Y9.7\  %2|/:!\HHR#,,p0FYyHxG\7Gz~v2VP۰DX4J Qr0PǙ\ "Ul9;WWˋϱq ڴxfjeN0dj~%<]KR'TjHJ F#9; +'''Xk]AlgmMR⛞ĕU C-.|!tP*X#s VitrRFcRpA32H"V,B͆޻䥽L 10r p9h"XH"`8[:D⋵,Ap g P@K.[Be 8qu)ޓ KRh.JMe!4D.7^mj67'|k/uzzzO{?gί_@3,hsxE>>X33Ujڕv຃wwi7;NŠCgLiS&IUYO$<0 u@Xm0.xB WE(`ۊAQ3y3"J\WLI (BW>13ƍ%Ć )9buBPt4KL > ŽP2#/6?.򘘒F=)I)`*.Z׹6%\c2+IhV.)%uwv6UAU)B%Ǣ]D\(k9G;N@ ;%%?:]3kM c>VY?sYcƾgNz.~zȟx;-Rb1v4bBoww>DH@pQofXྦjDg1xi"Z(wtj\npߓ CTbtuGHl[V/JzG1Ӓc#C @8X`m9C`ßmkS9=)Ӭ/mXf@K<1F]$H27S`.n0dep; r@)RF%F6hRm ڦ׷KԌ4R1y4C3DTHvcN OCqrPn8!BR-w[c, W :`u:P%QAJxYwnfy ۼO* `%-.aY24<1F{I% QgbX0m6֕6(id]_3ߛ=@-*_ b[|(8ՙoi!?.koZ~5|[w}lˁ(bQ+< o[\ov{/)FA b_? }EJ1[Rx$o\xlMa'/Oͻ'.o.?կ+{|8$>;%5%=%jTf vVd Ҥ#e-.`צF(YRHy/%`:MACp\vB ȋȎQ7:mK53 S6'%ӈRJF RkݘPɸs1S*)ڠ}-N SfKI,)N'*6AxGNRM(f@ HJ45)2RRXRg! ɚ s%VB4-v+P7|Nune]mاqEEݷ PY%6g7s&b2eFS K%zS<g` @ s]R JɓV>IR؛iV#=A$c^QZ2J8./ɀyK[]\  A~HPڦaޅC̈*e=Z|{η$yv,hSa7ӒC lܳ%999iǵhjD[v!]]oo+\Ä&kx)8pbb\of8{S['!J5O1729 4RJ VSqq317v RD`!V)$ 2=0zxD_D75yHA׹K2̛ ~{%>j!2>=;kmkl9!|/hh_ʗi;d/!G!4X>eaYUP%Rf]1zAG n~캙f&'`% T c ޣ;mu{o:AxS=K''o=@t<#>3]-3R3tIК BZ1nZVM*VXP+"\kzc1 Ljwvh%MPV28GbA]{mq<33y q"AJa~p\eI2l|yl44 #YɂgЄN:ut8@\(jM]$eB.[6H숑E!4ؘsyY+x)26mkU''M&ʬ|4~AAZgNՌwFkը+=b~F9@ۙ.^"04@#ǭ9&CÑq>fqcNVB>}W8CkӧO),"Ŋ:غ WipOfObL!~54, V4HS҂\X2s]^z1L c;=(pc,hOK";BǙS_^~prE诿LY8RXqaˣ {??D'͜Δ"ұGp{ ұloqu @-cA!pi kjjHC4l?0qϟH$eAO ʐq,B`1uuuza0CB L0a@^ga,tW)_|oK_5:L qV!B~e{T_|j́je!B!̲He!B D!B!̲B!B,B!, !B!fY!B!YB!BaB!B,B!z0h:{,!B I&aFz B!6b!B!ÅSynx ʌN,cB!p7%O`8q!B  b!B!fY!B!YB!BaB!Be!B!, !B!0B!BCB!А2Lmmm]]]!" 1B!У=apq^Xww!S<4e/ÅXS76s8s5^SW~{.K_9GHa}G7[{J_ 쳔KW[קOIMqZ|glKVVox^NbGr.\`ggKO ǝ?d2=S?b!BA,%tMq=AG=S+j4 ~ےwX`j6%,Qi4G &TY0\pؖJM>xm%@V-Hݥc[]§Q} ͝e]Q@ XTz_(ftx0784mJ37p s}E;i yK6G h, !B%謁p['}U-s@g-30`gˁÆ@gUyp8b=X CwpӗPw1ԧ !|H8 B@rXe H/(ZYiAAFeC+?igex[CE!< %&Vw s+ۖoftrٺP>RVcƲSD|~tDZKvvnXpctE'?J9k8aD-;?T[S f dYq?~ƽ *p<rǶ*je ɹ^yK>Jhԕxnn|TuHssn[.ʝsqNZXf`S>/+eݳ- H 2pQTd٧mخk0/|g&Xa@юin^ԍw;%-PZbv?[=%ӁhFǒoSd[ʭ`}[d4jx3퇸.;`/er _(HfڀrO*yvR@6r ?'^륿 $1 ?m]m/3K%;0:J܄EwF|R ~vo"^ƹXe 3jԨ{,{VqೃnyΌzw^v\c^^1cÃ@:vnw Ν~$k3>rj$1?n7x;SU;PW&[WpdCA ޶0ȃڻ̫˨cfun,}PAFMV܊eIXX?$]T=V} :e\GTC vJ3IZs6g'1m{ esYKU!s&%Lv#Gw8{3%ᕧÕ]M:M{s; @׷ [NX(3&l8){0Y.'.0>\Sk[Q &8aunT[?^qY'\ldk> HE05MTʕN*=e0 9PoZoZԚ5^`> oڢ S{R77[kVMD)w/5C7 滝*ڲsڶEFhy|e0>M]_Zo ~W^04LG6n+kg,NOG;jLGL so IDAT pd^ff%(zx̊WqҼVM =OV.Vp􋑦n=II.@8x PpaFeDww.vEN f1@>Z[w2&naSJUϥK=H^v9͟|k1SsW=W.0O̞=gʪZCUg[yx{Mr!e԰'ljߢ CաNO^8 G?1,?h˽;oƨC `]?j'ߛe*\e8z;qwYkXXm9`jY=ɩUUE -/1\K)`ٲC6r(tG `ᦺiw$o)̙kjVU[HQS>y,.8T\%JM=`²]:>q)$} XևRcܮ>Nprb)B3ޑn(b6~T_Tժ >>*5x) UbCY lݶglPVTT(׈U}a  J*Mc5WZo((R-@pU✂/m9i}%oS+kjh1+y)Jf\V-NT@zL?dK7~F'K|hj^|Tlo$?Wۯi.JDOY,ruzw%oE^QDr#Ak^[$$cO{&k=Dr̬ͅ.Wr2+Z^Q@&h3WqJB#,AC^bꃉTEf^ lCBE%TKT2qZZQ!LW^d'qZU1o%K}K̲FǗ0OˆINǟ9}α}sőmvۮzx /teXԜr՗gd=ch WyL4Uof?[#jZX .XLPaP#DTi=/dqHa`4db ؠRZ[QH *-1Ь?#` eUfE`|c驁M 3(jFi=)ft O\NC4V6FCprEPT3JL/`;ɗ = 2c9ZC.u %; R*/kA7vpN~wdl18L9?{y3ޏ-^p'm_anaqōOͷ^ۺ]ߦ Q$8?alY_}$ A::wVdC {k+lϙ#߾qĶ*at|'y͗ʪ?tsUv[l &_{{9_3fwJ[>kw]kXg I yD#˘,м#:hW8Oޫ1X杄Z xXLoޏX,lLxa9qcK,Y֊ ܣw&x?WeW5}rDbGٶGy\j}sڬi/47]mU‹ 9{7wcם}<,r_z7b7]Irw <k c#m23g nK n?~(ۧ@xba'x>ћYkĄ@Vk)3M>>9CY%Xz2j}SE/ңTOC.(#$YC!Z@'oT.k[̍9PTo\L{N0>YL BҖS֯&ǘ{O ~)b `ቹ=?:s`س0睍BϚ+0BGF=› #8-Y3()A`h`N%JBxqJtymon~*TQ{URW`Ii$% X}LcQ `\e"+x_/eT&eEz`,q M ' {baဍN*ػZ)ψVb9_χ1$ /LzO>)tnaUgjS$3mn~|7ՒM?N?śBMKm[J/VJ.󝯕h= ְ%1@ t՞=E7WlSlg9%;Zk(+iI*XCei3ot;jT%UO XݎRUk4nZ>M,ʲf뜡>=e{jhVw !fgӔ+SqL7AS݄< āӻl骏?8 z BTYa*gm,2O pޯб.=͹oJwT~qӑK> )Tp Wc$pH X` JW)?O{#_p Tz5VA"C&4RYF[tfsrE]mK\X: ǰPOj*0.RqK$)i^jEaG$"USt ۛjPi[X}EAraT*kY`4R7'W"7{PR,wYޞLЁRåOኃh&&0mc CnB̝h2 w/q]= -ߩlZUh:;aғ\8@,tÖlwx͵ko"5W(&xְ%J_Z?7oA֥VRSA_.Yu)594 ~˶,"^Sah1$@ Q7Fmn`C\[qr`Y\`隀}U{,jҥ7B[`'Q pnAk,]<lAҰJ>_l{/Ł5#dA-dMI²+75.=箺0 DcOɏx?^i -f޴e-vžYoj+(0wݐޘQ.K` pɃn ZԘTq@?^rb[gQ=iܟ^$IW@IYETGIAb⎩$Hѽ&Y4A^@(LHXB7msBtG扠zzc]fо߸DMX-(K{MF R5Պ`@JR28+1zq9*<ʋ0NN]?M}X`J pz1 2F[gWH&2*c"A#%VYͺD1B!pF-J $H* 5;?wY\/  6}h94ݧsdIiK+ 5KetEeK;A  KZADVĪWDmfe!B?`NۖTGap  G0@F{\wgӇ <ZL g*I|sSc _&疊WQ$sjcDJ}{U5Wq;׀n<&q۲1ԭ8Տ c JU~rw#2[9c "BhcޓJ#֞Zfurpw1>M\oI_Cn~!KjՆ@~FLwx ~b`-d 2zaBa خUEۖ$*||:poSsVZ# (eD}Y#sJ4mm%wV6CA #Yz!GB8}VWd`獁h]]}c֪uwLp癦1d0B!ЈCWn[ DIe`t5Uu4uےwVL#6|XRSW f,𼭓O?3MuGטmj9Ec1葄_ Bh#}V,QrOabPlXSsFcIeŲ6Tֶ1mj-KׯO aMt˚';giT#ÚM4ٴI%G@b1{ v{{) g%FJA@!@L`gg'0?JqGSO38+1B!BSO=u…./Kp`f, !Bgoo/1#è&ETgڍ@CN$ B!4bpԥK{zzzzznFzv5jԨQvvvFim YB!YY,vvvvvvݍA!B#)fYeY}-[,kB!YBɚbYaB!FTe}@CnaB!H˲0 h{TB!4aB!B,B!, !B!4|]pB!ǤI0Bx#B!4 B!BaB!Be!B!Уe mMwJ'%)wxuSq[3N_j&B!Be  ~k9u^:S}pﮏ͋W. _c,穿Krp#B!fYYţ/ſ"vp7 (0]vjrPtqr ΆM 1+Nj+ijov@eMN8 e A)˧:w\_ՙ;ɳg:|5onMwYۋT]H:ixLd'<~!B,a6\.^qTq_Qodz97olo窙!!uN^y31ASgslߔw]/HK鯋R';ǁ#t\:&):.lE:4\ όC?mo?S|{N]r#)KN6M/B!#G:͝0zSx&>'r`LGߕ 5 7=f{9j m+.SŎyfsuuW\f;3E 'Vł49sKn8X!B葃ﲆGq/c{@V[GL'8@N~o;1ך?O@g51L8#wr~86s\ppniS/>^vg/w) yB!, =t^.~Se[Pp~fG[1Eܻ}3jGi+ N.J;5jt8ZgNoM-@{Lv#B!0BqN?x>yr+S㝠(QN'[\pMG6G?we_K9@"H XoNJ6͗Qv:Y]䞞'O,_/<}1Ⱦ?Q5j%§&!)TFTkW$NJ2 ag&;.T&Jиl-`,KGεi$ ӧ@{I2 5O&`t&@ym@d S AhRFF7@Jdrw0, 5$!(oy/]l`Hµ YR")#&WAA <&fY*gυy\.LIIIo-?>!0E""[`T+MFavb 5E^+EJL idUm]5{;D,ZeA-6,TfkFתU)_='VD3q Pՙ12,8&Mg1ȏ@ȁY#ˑ|?fffffq@!/J"[@T2^y7P_DMJ?+«ߪzZ̄<0B!zxd1 #۠d7L#r\m]*e6N%[5Y䑉7o@z$N ȬڟzeIڠ|k@P79|֔@,6FHh ? AFe!B=,H7—V*+ԄHqlmfB~VFRTfz+ؾ1RNUM3\:EmPva!Z*պ1,:BbRkMw7O&ZŁ@A&[h/jbb5r IDATX #dYB!Ãe)yR9B$"_k"dPu%eTN ͽ^y%eZ;0z5Uڬy}8'"dPsH0ڼZU(fgҜk10*#_Y?bta} ҔOb1/fZ>/%;V g%FCff%?og[26'脷~<`nk`/L\2uu?!aal7>>p|IBjf;9Pccfu)lUMmݴ=`IMڌ,_r(3޴#(GolBY!ߗzK }L\4(m <ر:f ݬOʳ'V**nk'ZEy-TFZՊwVT0>Q(nm>S=õ48÷O\=r]:W^⌽<F7{f>^|`9sq_MȹS>mLJf<*8dpor3gĬܣC8?;iH)72r<,CvS$Ѭw̬A0 ꊴԖS1Z 54O 6 wTxQ"![uוu/ ,}]ȗPLmڕ5מ}eʇS^Cv\(Kk>Z7 n;Usj7J,皓N}Okpw؎̙Xqzm?_:\W~%O]gw˙|eWz@g۟>=ZT-[Oom+Rsֺ#ਲ਼ IR,TVJ:+.T&d1FXM,4-?+).2T&L+Ue$Ce,0^,˂I- BeӃoOd`,4.Cm2Feبf n[V MN +Fe4Ri"U)TZ;;Hr16gKRT&O*y7XJ,ƶ04.K:z#4Mk5NTDˤӥrJIir,N7I+>_.ϊtM4wu48īD\Z& +4wfma,K"ꔽ;3ZL|˵zDz^Q`,842HY i1JQTتeh"ilm[d2,wkLCa\&Je+uLm<8Nidf[ u,efZU, +n23̲ew]-|b MƝ?nYs07,[;koKD}cT5n5vuW]}}suN _b%Ow^9e}oSv[ uetgE_3HE{TRn鏙i>B-N٧V7%Tm cCtFn%enVs#!9ZS푲O@]NKʇB\JS(F3D*E^Ze H6f$j}P<÷g J"PVQFBrPєgmYכڬ,rZޗ"ѫ4f%d7j*M^B:y疫5dU$jruoVBRߝ'-`(kAr oGRsR} <,Prw~%V)vK2-WwGZV5iyLtay*7>pEO2folZICzIĬeLuffovZ}47HY lm2aFs47T8{!g* 4c\ yiJݧV&Ry , xP]/ERq@kjE0e_yzo,!kUL,0Bhj\:~vX~c':óƷϫƽX_4 ]׏68;s#5*x#hY<]ensS_M'\> cZ6BIpem,jԨtˆ+M*zQ o;ihF {S@J"ܙ 47:HBAr1Y1ޔoDS2"$C+C7E;>].sFBce" EAѾ1Ճ~b{ϭNem.WY[Q^2/}VyHoe]IYi^uvV(Pj%GR/:~yRZW yS GuEnj֊R@.Ǯ{J"EխH.$-:#;fΓIHkL6R",F{H/1b|d+PܣIR"hLjK @z.mF!ⳓ|)#i_'-1Ze yvO;{2]?^n^8!&tuevF?ݏN |3V?9ՙ.>0 ! ):1Y)˳} ܭx"A~ea WzlxXL #9һ7K%)!X DAYqDͲh V=B?Y9ժUnS50[Nڽ((+&g*Rb,4խ eQ,ʇqHq@7QֶhKMi" t)TzN$O U,<ߵ=JBA_~^cFRKqE렑!qEpF] "Nc9N(_{5~k7GJUS1@D24GH36`vudYEF 8yGs϶@( i! Y>a9q,cfxkE ѻs+V̲KXgδ3օQ_.w!j1a-K^ĉ?g]ϖ:,`wK3M޳kWwJ̈́ gG &e4gz}7YOERhiZi y,\϶ +lE'ި YH lmZp\?켊QmP2}][:Pb!У|#} JMVQᬙJs y^XFoS ^RDrU}.ۚLEW%uV@R>fzV  6+cY|)2)S'߽/҃cc@ζmb8!) ltBWYͼJQ|Ơb .pj>z&UC˵ndkJsUgX; h/7͛U\@EUPt:ηn>z1깛Tq:: c:雮e=?oT_hPZ{Ϊ4T/=hJICŦBkY^UfYۃYV`J@+ CA6j1(`rJQGYuCR Pq`3WMʷgii* j9dV_U!5%Z729uZἃ7?`$7_hKv=x'jEaE+ה3oP_dXQ(tHmx9 UTՈ9ʯp e "hDVdi1q@""Z5E WZ(yFe X6_كQС T49:Somõ5;<4PM}6oL󧚦vsn$<;Cw8ON0]{̸5?R &>=a͸='~C;ͬ7C'>}R{ԕ6 U`ol:$xIFI>8-#9B=ZGQV 2D eXPeeDpyFgh0ptXJ”{C@MmUCz-Z?csXm/5l^N.X()B.ll3=ޤ  bnEa&m1@W\[v*:pXLRsR3e{Loww-Ӗ6d-m\lU® ܩEyƺnIXmr1Q, hޮ]{VhS1K0c*Q ;mٙS0v_/oX%sPn{byDqeNA% * uvl&V=f;\Ի`BǤYN oUI%+'tZ$0(RS"I\H<6U}4ɢ$qU"#<.~Y$'ROJZ%ByBN ΎԊUOFylEȖ]12*WH3;e4)x]є]aGNoF@-zH_ѬP SѫKkdUt%ʑ*lO:cW޹- ᲨUm'SSwd$M\O RnAV~?Mus!rlĴHqWb&^8<=ܘ.S,&5ѥܣb=U]N&m1xTNX=nHE=JtUl OqI~Ϟ 0R4E7|zQJypۢld>yY̆J[~o:h 4VKoVrr Op؛ L.d;h-UUO/(!4ݝN16~ONWZt]A܄pEC;QEԸ(?*dRL_S8 O !'E0U|y%>e;C>e!2 0 4Hf ó`/m+vU*uNa kpY}Y!z1>v&vB%2-ve$x"#Lon rg(Q@ ɜI#rhgh 20B! mnQLNԖWg Z久1Q{rtaavղ Ls8$KMmJ@A #YB!^hLW}Ei:)p A˃!D' ƣէQ2L^5|U@Ѧ{xi 2!̲B!4-jrv&Xh{HrC}[?0m9[9;g[Zmhn79,˭.=X-I&$۪+zb -d 2z)!z;ٕf儉-] ;Jj7=wC'YÌpA; uvmrϮTD;
  • Pa'˕+W@ XY&?&̲lOOH$z`_B!BSO$uuu]|h4b4~Z%x >`B!333spp8L8bM1B!%\c!B!5bpO .'`k,2M)ꞳbǶ\<0!BO˚[>oJ_}^}a $B!BOZ\ K^U_mXl CWR<sK7{hi[ _)L9nR޶tRPv^!k6Z kJgpsU,^ 5 tlՁ`)Պk1ͧ)7޲k+jmZb\w B!2zyYsF/}G>oBbc#=+#/  w7^KR;М=rjit{+MEEl*K/YWN907}҈w޲tL=pH_⵱>vJ;qmUnvᥱ7|=Gj[!B\œ,F衑+M6> ~vpz+͚am|o o,:xIAoc4KB7yhEboE9 c xrR?gs\|&oP7WM}QhbAՑKEG[D0#BS 2=go|7hEޞ6={t6xz͵+p eS5Þo@1nnec@ hfggEܦ!-ֲ2zF kX QAᑼ5#طB!.5 KMmNqW/Y]0lŵ#cBѧZR0z5 6}E\ `iCud8X `IYYXͱ-Xytꕹt?DPeQdFX!B#UUUg8CV,k/;WjN|Zx^sn>DÜ^sB൘ntoOO\92xͪҪKߨ/(^ F=}Ag^0wk1Wx@ocs/=tC}UCxl'+Z)B!B_?s}BLr#(+p+Oh+{goxoH狎s!>V0!/.}R' m߈ Z{ڨO]!ލ.=tH{pwz*!BM{{'| a^5rρ!Ңpݻwܹc4F#˲ x{{?y ,bB!  9B!BfY!B!,P##B! G "B!, !B!0B!B!̲B!B,B!Be!B!fY!B!4=cB!rFQFƔfffe!12vcgEYHm~^Y2󮳄ոտGث=q=lC"WON}|QdӺ?guC'M?ZZՁz8vKChׄ d_~/ѯե @p]m'Qb!) IDAT~c+d5S)ޜRVr"ӫ`bA c9yF?tSza} 6pRkutkvbq|C2ӷ3{{@Wy]Vwbٝ̊<݃<88pBL;NgRe!<[` k=f ̭3=0 @X[B?L՜ {{~{%i_-Cǯ̞c٠娖g}W9136#w\ɭ~lCCZYߵw`r̅D0PM߮68_f +5 %2>U*F3 o i ży_\psb_[kf1z&ܪjt}sn?o<6s 5_~W;@+\0A~z~3,ly܂ ]s|ޜ` `f5XH|qg棊ZD *t9Os BVQ%IÕLgyH3]9Tl,o:`8^R_7"B*$?HK>yO5Iа,xW#eGձlJ$oh:1qaz/ 42>YT*()q;wFȲ,0===3 -f8g֌GI o0# 9A0>ÄY<8UOuXGcɒ<0e[/2<038{sสy3t>JL}"Z|.84+"BX^|QN| 2.Yw嚻|fs`؆G20K[+ޡsߟ)l`fCpqr/1?mmFoćܷwp8pN߾Y6fߏ _a{g{t![N߉QgF I`A(/6_T5"֣SwBp,A=>LGi|l)ok1V;WDh/j? ]u>0hi$ 2&p^BV{fNVB+0~}IwOӃWFg;:X3E\e~vx̭|;i;+j 0o_fZ Ꞷۏ:3f>`+: F0ٻr@s;;7}gq"гCEx` =^|!`4uŭ@j)>)V}@ߔ~]v`vRѪ*kxhbz* @|. ^>%T Uc-.p^GyM ؗ^Ãz}˗[Rճ]n˟o~$HpW}D1 ྒy a&s7ng.p,CWsם_[jS۽.XsVZEd#v?k#̞ocN 'U*y6^S߶-0e&mq/\"$ WrWNE-ȝnڶ3~,~61ڗ*m>4DLTv}1糱G]rLMzVG,Vf_ւ݇bjGi/3> |4vuWqKu_tJFl=uY33B3N%.,yku>8l O `j*hjRؗZoケJ. 瀹.zQ~2*"M3K,ͫM%J"A_c.JӤ-HϺDH'E>AyA B]BKFs*EdVhSqzO^I$ې%.ߵ_yYhAcacGи4tn!k֥iiU6r"d2YؗDSF:{d[JӨ"EB:uj&y}qToo{DIMC)-YEt:MHMVQo9F7i5hV),Sin? PBOfJ薒'2-5ZQiƔ@"ի{/@ā2#o"l4͢[uޏMކ.Q-PO%HJ o o[9xqi6-oKA d 2ѱki[Rރ;]慭P`+ 7Ԉ+{)27rq^Bha 4csNp`I,Ԍ'8b|Vq~7-qGP`%\ƞJR|d0Pv?[ߚ65hxfe냪Le s#e., ,0 t,nnim.m߭gQ&mA+CZGn } 7pJ9&{v8n;z>*/zy.0-1{iG]ly2@ţƮL<2S!r"F8|G&Yߜ6|7\ %{+:h a:B`5E;5 i2]59{J{`uXKpFkN<Π[%+$L''xe k)8ŊYBh]~8$c"ZgGqCIGERB 2…`uc 4p%R0S~Fx`: ZW.~7WaǓQS K.:pJJO,&vI6?p(^^:uiJlb2!$+{MX$֏}1 rCMZÓv9\PWWnʹ<}%^S,~(p]nM JAŶЬ)au,%!HeK t;ɺńvÞ*DTLc^ޣʨST2aOpgzO*V}`գ5guZ<)g:,^N 6#u8b!4K՝Q3S7)|BMtk3ONk]_,FSUɑƃ.thʻ^n\qh*VitT ) iے鵺) ゾݴGG]UiP.b LXYiOSZ]^βҍ)|B_)bFInJ!MSI_VP7}OڅSܐ7BE@<[9`zkJr,kw1tK:iV1/Qmduu,Id t+TXa2o:$H'4 e H@7Ӱ su.W0]5Em)йWlcR>/leIGs'%Bӟ9E7~rݗV#HОSΙke̝ڔǕ҄?x޿S$gצ}$00_*氦O.oE;̈Hy 2Ӗ*ަ<ކdiXbzEmX}}]:`25Yar)wٔ6K0|7JYT#6m]QblM! ڷ)+L8i$ oNT-"9YǕ#݆"{#\!Z=˕n/LItRKBXK !~L&YϜDa9 ]I1 IBNa[})$L.$ N>OdQ8EQ*Y_,iI{'%b *A^\}LJ"1}c@]D'"郥jC"Utz};ONK׮ *p Swj xUUAo_t;ia5$scn;o SJ'̲,F FpH+~irp9׺3D߾,jL0~B:npn~ײ-~fˤVb!"?>׀axxbc?kKy \ipLpr5MZWyM]{u[!pۮ H5+,>Y$*6X2td)n*/:]{^$$9S\܊iU\cIc|5fr\2*'.4 Og=`B{ s{`^9Ly koHnF1:U-v }Sñs5B<>T:@ue'KN$sv^џA3`1M4&ټ٣?Ξe6af3d˕uy*5`Be:if!F!Vo ҅=02 -z@ :̓ wӆ\"6`% GL#0dp BxW?g;_X&ep,RBkM?TZIlniEOwJ7cL{Zd 2B}YM Ruw9P헆[ƻ1.pk5!hUzbcZI~VBPaí:FSWb];> =}"(:ume*kma1襄}Y=Cw[~kYwo>>t;6t' d}&]9es?p=2䰧|F!b+5e)cR5%(4bO[f!)?t_&1E!ѳ%љi҇V5#P~MJ~`ٻ5R/W8;O#1'ŲȄ!33gRJ.#]F*w=qe%z~4k"UWnfIF3sL@`ee~Wbe{zzf̘`=w%FhT51B! \vF,Ɔ?` \ vKp'bBh133 G )#B! G !B!,aB!Be!B!fY!B!YB!B!̲B!B,B!, !B!fY!B! IDATxe\k ,]6 (*6**X݁XWEDE;Q10LTlE%wٞ{_:L&~J1Y/NlߓdZڍpW!.'pf2xWMԓdZ9zmI;??QvݙL&z^A15_d2".tb2'_O?i1pC sd#*j)T8k]^5{=Lu Kw_{W,S<9~0[s&im^nT'L&n̯6:1;"=LfOO`?dqũ'کCm͙L1sB?J-LM*r''sM+AS6i 1#djް"Sceeez&p// <4lB\l:iNA8-ODWk2 Qf߬P5-," l۫괓JXS@" OX ˑ*Jrn jO5#sgd(gw> Zz*]Z5`Pb-?mĥQW/?NR'"\WLirUqσL;6y!j"WQ&9?+ȊO&k! ʿ$ȶY{!\G0o\u yPC/ދ|ځyrMB>]/b.EGDF;PrۥڷG<2 {>m1h-޶s:?\_6ԩPsxg( Πя<֭*`޿=Y vT߹1~&(̽qH[|zԴĥ֞в޹ GąW]AO"n^j cvGcB&#h5TSoR*Ҕ5'ݍ n#[2H@?c05-dH$)C$DP@/e}[(HY]GG4h`6ʈ=˒DSleJ1.|5W {|A;ӓoҐF7v_v4a7,!ѯN( m-?].h𠧣2$m Zp]aשtlP;TGѴDyʀl}XEDUl=oe*?_8T5> @wy%7+!DSoƨU;#: DS2hڰe:YC(|'kn4#BM{KwG#[^YkK=^W\8F/;ˆ~_,ZN=eg\o?LgQt䮃"Ps/B!&][N7ѝB:5Q@5r6=kEߤĬV.jPugz $`Tэ&w;UrDJX.Bu$btF4_JzqH2f\?1Я!I"r&h&TȚtժ LF.}6m۫㨃bhmЌS4>OVP1$7#-5)U;RjKukS;js]E#EUս׮NZѥzM& ,Z:$Vf:57U8櫒Z[ soZ0dܬΓG}M@TQPYc+Vȇixjz^Z*4Q&qGҳh'(jhOk&(VIK>^3_~so6qB_3i/WK1ӹ k's%`;||xKpoKpRmRtw4EA ^ N\ N|)Pac@_+s)MuvS<- C||ڂ@VԃjsaqYGíq\B!#,@ Qyއ2zITQk&M;9^´#@`yo\(I: \ܹeL[{KGg9 ^>{.AF\-el4sUa{0Ȳ|ǰ8>gMnH YE"s3g?o3l^voY7-2T\nu#T*Esc$=} ƣGSh vb2̈cvO-?Z4>|xɧw5\OȐ(Bm$آ ý]./lI|ϥfӳT럸*/&2mjaYA-ykZmØ"Br]_qygӋŊV6]d޽M2p`7/3QS_>ͦnߥKkAi/ߥZukF&oCN רyV/sT5V&]کKt`.B!Ҏ62yPjI3D9:l*hi{?B!Tןہ~RItD87Q-d=;;@Vs jf0klWncCՓd%6NnI!L;b́.hw?y,w|+؏lc 6B5;ہB0\) !BiB!$BI!L;B!$!v BHB0@!`ځB! !BiB!$BI!L;B!$!v BHB0@!`ځB! !BiB!$c@! ?B!!BiB!$BI!L;B!$!v BHB0@!`ځB! !BiB!$BI!L;B!$!v BHB0@!`ځB! !BiB!$BI!L;B!$!v BHB0@!`ځB! !BiB!$BI!L;B!$!v BHB0@!`ځB! !BiB!$@7p-?!dz.6c\ZID`Ot$xs /B7l@IbNaRĕKFuhB!I:hT 10̓KrPXmмrkmm=g@})ڍ^>ARY_Eݜ,X0oô!Lj#`4[)tc)U@JQ Nk׶R$Q.]'ҿuÐ!5;ZNSaGtWTӭY/EUW\ט0LbPS+}1=PF6AS1Tߢͯ@[cwt%caS 1ig1?_M >*-+jv4TSĜ*`(1WYPgm\?LQq_ªT!CK{Ϩ~#G âƵ\.]׭Mti7ݮNm۫}wQB^D$Mc|49@֤Vݍ ]l>RQTy-nPu;;4U%sRH}zzXgb3fg"ȼut7L.ͱEwO3iFa"BpXJbni8O6X[ط6o>U*9NZ Bv4'$݄{$y-/yM%2R`ZD5DVQU 9 ߓvH9s^'aK.Ľ o>rEz,yc)SN~\̓67a—N?|~ vB9`h;<ą xZ+ _<p/&uN2E:ۖ41m򒊅&z4*Q+O?URU:7exW57vߜ Fy&Rk+jEѩEov:U6TJ{DT7AAD7FFi;,x@׶֔O,2:{N-&O"id %A") K,Ԧ$v#B^sl[P '3]b\:X GTyqېazoye?7矐\ݜMH >t:*Ɗp!sKw^}+8b(&->5I o>E\vGqiw>p {M.+:p*6чSGӔ ; '.x|6sh[IZUɮ;IT̯(-)-5<`M.>kfwy޷rET%%9E3~/G6}6VRNX<PUjB_-UAGJ%srZ*xg/nɑ_^YM!sLFfXrPM2BV1`tP80'%4׻9R=/^wrANdn_K?kUzwHJmMbUy9#֭oL#dYT{ڞ!ν\N<1\{VIao?=>VkQxòGvO]"0Guȶ}3T )HVa*K:RZ:C+aϴԗ=q?ڴ2Kg 8FT {? aVR( I>Έ_t9SH-{6%U~* m,(A UuQ?B0QE^9MCOmGl  &ERn[ 0 :iAxgpɏDT].ݨ$za Uݰ[UPtjo/ KوAV?"Ń!`Jl_jOqۮZx9Ieq: R'P啫ҳӮ^}?nW/@X ޺T;UPac@o o>qCuQ1ӹ k's5 o>wMJpoKpRhqg;vshQv|O 4yVMU('W"Te.'LEDUrO|RKFqטuͱ5]eDe2xB?}˦2jNhKA/3r TU]t D/D}f [ҥڏeLpya NBè|.]e74jLxvaLdl9Ȯ]kb"#ަV2{V W%(IΦii-C\p*dэtdK5ӎ6$4zXf~A@n{"BVGǷ=K)p^8eD!;T:B}=!B|=B!$BI!L;B!$!~& dB?L!$aT*UAAAWWNůB/r  h ~ xII۷Rfdffjhh(**RT :PsG" t8s8֦ Xp!PEEEUUUA!~R 0@b!d!Hs B>@B! !BiB!$#~{v^}-ڶ+'tUjOϡ3 d>w? f/UgJi?Zŀ9[֏iMD-?r6rMmR޶[;-??~7 -;B^r{([:Ly /wŃ;r6IF~n~W# rZm[4\Nw[gdvy6ΣݖfS̕ϖZ #];!Pjp Ɛ&;6d,=;XD81v[Z䄋 xIw )%Q8׌m9;(]%˩~,P[=~`TdE^yv׷uS*z|/WV+LNTKf 1L]q1|qSkb2v ?)mz{[&Ӣ}rkNd;{]!jx3+ZZy$&9v y`t_K&a[]l2iƍ=x9iFH{ktk^L&sQNWRVVVVVn}:DSRRVVVQm{kwzRd* I+kEe >@YVNդ(UI&QeU4 L'⤦5.ۯ;Ӣȹ{gA- [>vţQ]T0c̆qޚiF&+S쭙mR(KO>u{ {?ޖ^3z3G-=\ ¢CGY0NXw.\\c?'ɦl5?܉?2ىHԅ3lh.w]s"Nn\1AW=F"є5Tt4娒sR\{UkAJMhg;oskHV2k]"(ZݽYv!+~Tw[E7- ?}U3#}|h:]U# {Gj?rqwf`ј*qY{m=/޿{ne7yéUa?f{<{ZqMg߼z޳g]bQU+)N6o>_e..;>@ ↊ 2.,_{Wg;' E)G4N޸-Q!;]^G/nB(ݹ/f;Pk*353K؇7R* +NG]&+C*&VwrMo5R~ʜ㏫@oځs[*zRUOfg3| ޻CNXdQ{QKz=^hFv=~8ЩQƏlŜ]mvzQtbo _'@ψ3FuQs v:Xa~a4pQDοӝ~q4/X<|sV'F *9Fx(7$6 ϜIZTF!2zܾ $j-׀}֞-Ԥ#dː8%ns mF S=&*79p nHW\`*d>es2@\҄'n>? Q( C/?}U!ƴtYf|ʔ A|Rv#A"s{MB C*eu) yP:@~" R(B*@q+nE, =-̩mo~(/%źC{V槱gϒr7Pn~W&nr!d jj,NV:~M,FϊwYggC5)2ޞ5;7Zg #~i,+cn1{FdxlFAqA懻M]@7do{_XLFAaNBDwvN]ȃH4re~Y >JntfNYG.V$Zk÷goxUz~UNyIZĉe. N '{%XOl d[TrE4yE9:_}hdç;~R)~wi4i2Rdz۾yR"BP8:oK)ȍ>{T@VmBxkĥck g8Oϝn;gX~ֵm*\b0qbb$F7;w,wxmF=cVq$yG,b^,e˖;:hJ[UϮ-!(-`W %8Q*.ld%6?Yl?iv֫{3U=j{^|d}ՆG4*w,{ ƀ-^pqѐݼ{7vZf7gX1#d;w)Gw@c f.t!:`ꮻz^1u}G.!3f4vТ`eםlX:8 ՞a{bAǪGP䦄 gow}gU"$^KSSSGӋƍ9d9[Kgr7/Z"rizk} >GzՁ%3w|kf3Û ,:;~n#C/Kt,`oN9O&1b;.coM;5vɽ"sK ?Ad"@&MB_qm/CC6,?cζK}<Ȧvmʌ$ۣ*bNJ;\Ӿ-0TxjNoZK)tsߞQ/k' "v9_aᷰq ~o"XZmov z)mX<ȯ,ocj5NdA5[͠!B v BHB0@!`ځB! !BiB!$$I,ƕ(++wt>!DDDDDD~cVZ+(BCWW711Q,BsYYFv&::: !W)YYY)3Rj(.͇#ЯBLBI!L;B!$!v BHB0@I/f &izzQA$t?H~H&b}'Xh郭lݯ-q<{ʟ@bc.|6KSOzM4/v $CTU_ p"0a62y`'jYet S j0@INL*@b> 2=[ȈcX2yx EDžA~|LSܣ+cn3,9|އLr?[|G_[;g. * 1f ˸bo4q#bv)LfӶHŽXk޳l9áӼϸ F*7ܖdZ~XP'pS"ШksDT}t~ݙ}Fs?X$?? 3Owp+hsj5/N{sA T6jiG3[pDw\v4339~ۘu/d=:lio533k1ffLBTlX~}H\28A^췼̓!p6368cŜq#*}G甕cWlqT>]bԶVi{YDXxj͸GFp1W$̾8f|Qqc7d_fvAyAOO9t㭤.=̦)nxU{[/.}y|3JU4x m)gn1lJnSvn/ VzKޞic]ӻdk R~޵Y6vϽ//NqhGy†"aC~Cgr1`I~ʾwڛ l@Ib^}ιׅiIb`7^r6,8u|ۊ[}ݺA>_(-(гn/D2h?i Հ Oj!'fWk[hĠ+s'MX5dg~G!Iڅ s++YM_5Z6" L5iIK3eH7=\By QU1e@pW}XxUREAE)M .\ؾV'QčTJ{up Y)}ElNCJ# *}_\IiCH";}S yփ,t=Hj 4PnH?t1Imn-oF=ɂB !*"4u՜ͽRX\*l:ͦ8Tq**4^Ft&ݴ*_n{#(byrʲ_jL $s3OxLcS*YS[hIW_ZԉJB~+g(WN ]FzC (xɊr,sg}*,̬_^9 j&=zØV3nL"UUBqG_]GfS5&/t$'eȨ]HX%IBPT?ZCgz3ȜgˇyP5UȐx)W=v s棜1[vޱSIyUD'i(%hBxE],Nut¤B~.;--톶ȵTYiU-6szvjUA+;H>}S!((6rJu/DV^h))Yht0EASUg>t^缤f'ڛdA~·oX=[2HL~ Р1<ߵcGRQTn RU,.A/]ڻ|܅?&,Hؘ;J~Ͽ)/I8etaKbe<=-m5RQ$~A٣e\jdhNJ)݅Ѥ@J}o̿DL IDATIAGݷ?,"7RY;I9T5zҜgfHTxEPbya)e\Hj+ *]6YWσ,EңS#^c +Kny5ܡ> ;D !,-dF1C4ZZqغBW4kOo*K9SN$}Z,kl-/:π[Eo|(d ea&}sƈu9 ?Y{tQv7<И㳂)[g)C.ުjWixuߑnȌk-f:2e4rJUhZCx9ECzwɭq@iԯk{(|7Za%vw:0ͨN_nno# ~2k~Y/)o!ԌuW)sRBm<'( CJBm~ކŃ*=xWKɂB! NBISRPTi!_W|1;k)ӆ7ǃs{z/FT)cN2 ʽ|l;}Fny^k]Y΋f0t173F#{^㦶t:*Ɗp!sKw^}+8b(&->?"*LJ($LP/PvH]Z 6OM7zڱ7>j=z͢Pj]"B~O4yXg9nG_WP`>{Kl"YF{hq9( -.8åpm #I4[%M |+ewۙIuvw@,{ ;eؐ6-*C;2i`a6%;D =.2av\_K?-Ə׵i ?2 w/V ۷o&8|œn"&Vz8LUI؍Sة눭-7jEђ41|E*hAHVEoJl[i>y؎^~O]ãnwRpbVD7Р)u+ycRPFrҭ̒Ji0ۚMks\5@ie`߮]۔&nVPfĦ<8`^* }w KD=yܵ3DŽesEjT*Ͻr,9^.oVDQJyٙ%՛wҲ//'<~[^gS=ʑ)I]V>Ȏ/,I=H#LxoB\PؒzDZyu+h'^wߚX1:,n3%^%'cI&M3G /k>vUF <p,,J&_[7mtGCq>Hs@Ix <+[S\rc ѧG+B@9U C@`%PU˘I&M˯3/_[F?%m]9}ºk1%mK}4Қ][4ikiPF gW»AKE +GvlӺC35CWBj)P&=5oF>zL%c@q``%O-ߦ.?0c3o#;&f@8o>3\mȡHQ=/xuPWD;[W LeMz{{786F#OVޝv36nԦhbV$>>w~9*P'95}򑘔7]u]؛wmШЀGƵ}%Uײ7XB>4q* 8}4Mcz{Yt!ZK$϶|&>8T|}<+|Ўͼl Xb.ȧX7dPYZ0Ϟ`FݚQiYͳ'o;85XWn{]ibԳNݶú8ɞc7iW@xEPj_^!'\;eOd: *ulq`W,;}1O8\ @)UOCwr{ؾYv)qQ=k[g-jwOyPn褠e|M?Zw%eµm?rA뷿Ip=W_^ _9>(203뗶q`O2l8o;lZɛ|N_;2Ƴ7>BO_2vǥkKQ2'2ZtF0lۅӿn~c<;#? 񟠓-X~+UvO|EysXvᅾ1\k [9vzm e<&|eD󛟤iXwDEnd3{o=wipLm'd쑰ϰ4ߦǦNnwXmپN_;]Ĭ]qj8]k%fɮm̻'JGwl-|X>;\0 goF3ov׬3!Ĥ|a!-xE NmgVI{]QohZe3Ikj1B5Wd vYѓ}4ĄPTu,;E8sN)p_Įzk2Dés) gNegRoAݥ/uZ{Aw Qv?ejݘShoztoZ,/}ŠyC]lʵ0 ^w|3:U]>,3DQ4ڮV S+CT.+PfͦNl]ڣɨy~^~pCIS;xەk6"`T_A%Md9^kv Z'ezW(Br=f5TnݬumMkq%قSvo_qU|}I"kWdE5[ ӴJ`m\,ٵ%y0? kS G,o8Bh^6}u7BNy|ꥉg^W\S/=eܹ'(! 1qk6bqڥ|_kvTK#`L>x\k{z5rnźTuSvtZX:.2YA 9F(E .I"6f<[O;zeFغN^N zw9n߲|YzlߵÐF+߱=35iI=#QJ$-Ii;y)uCu'h{tN׶@KUGnTT[̲ fE|mpMtV0/Y=/\x |;b\[L,M( "\s3M$HXWi@xem (J֮ȶ v Vy4+!7K6̳'FbG'$#Ǵq:w:(FLK7eӄg};r8!׾LF ]@Ji軥H֏@'/uy-˘Q1WEZHYBH Do.EP+g ك/i&F%Ӭ% s,l̪ >30"$9OAjC5_ 6I)z3Uf&b%,E?L穹5/W(H B}HkW]'D iݷ] Ws,Y6i=O,3$VYd4ǶYj3ys kEk<ۿXqg;Sp^=MJ͢uT9[0`gTVa'W>ٻr!ϛΉ*ޝ~ʦFq˶=w'ݿ)Ϗּy\͛DɳYCID2*lp] O=$ |u)ֻܪ_ȝkzri\u-U]Ӂ>1V}YC­ҽ}9 BiC:0k&N &)p9>g3rY r5499DžM4L||Ǟl*JzqleqjIQ->[}N}|fV˰v2]ȯ-fɧ'F;ԟYڟ_#?so0|7Hw/,B}  @ @@ bؑʮ A?OoX!n٦T~Q-#L*2K̗}F{Op\:/_iV{X~X%>D$E+WW`2cW݄ sMa|G*4ΛXKӂ2>}ƏU!ʴ2h泅kEɳfҪqUu~YVuy~u Zq(yRGY 7ff&]xK ]+n֯h>Qf,G7[$-NM#xÿ=lKowC1T '|u%gg mh~2 >lB ?Q', Z+MR/,bq=2xEknn`,Pz`N`hưVBE <E"0@0,_iɱKwؖIYz6Ϳi6xߘx%GΡuCӠdt2М|A$uA(h:(KMBjTI}&PҌ2:5{J(B8CF<#"E !dEM;4wh ZQP`68p0 C(B(}.xBȠv0 UbL*,ϔc*,M(/^ |Ԑ0 L<BRk;T44P(9ȕ,Vz޶=.T# ,EO1 ,xr4W{&1;BRCs1jCA3r%iFdNʬDq\nΙdB0 |\ m*9Gp\|BM3J+i42QNW|N?)i.B!#iZ.Nν9EH*$J*hVҌ{#)Ⱥwp{nj=!jy{J~# ~vZ(h1fFҡL{~d'gq_ϨxIaFu ppo!ZqS%fh hd|3vp[LW Og)U{Wm%^wߚX}W:uPފ(8@uI};޳Ywg ><ϒ"e]3iӪčQ,X7SSo3w>LV-wͼlXw+e򝭫@}dH_.]5k ,9!P4wco+sFH/۵Eoof/H>_X9cSo}z"|_&OmݽVvl2[[W "F~>`hf Z_|6Bv[9GbR;tyā]\<ޛ?+_}dH9wۦ_Eo۷Ϣ 9@ʲ?z_D,l{v1OׯjV˯A4»3&p\ލyK=$zf\+#lX]ϻ'oV#Y Fݱ}˲&~;xmK͔q}KǸ\[0'Y-$!?2=%*)7D#sk<[1y3QnQ)m{P}+Wv Z(ުEgö]:m9g"ݨ褠e|M?Zwf Cne'r?=Bץc aod{bм!ޮV6ZMFi.`8e{`nbW_y܈j}s[;tpZyB潇A!RD\ݰ$biΰ2-Z?Tغ:^'/'u[e~QjbTOV? {h7KxemȾXў ˭24QMB\!#M WuV+PW+gkL%׶ /92wXC4r*zgoي+a7/ԙmx!2{cl!{%MĜbdUI$O 2@>{ژU|l_Og haDHiagq~KvY"bSskhpQ%,cF5xK'0E{,B"EB4Ui[5cۥkrUVHy劔s$vDN ن<);k_8xnaiSE\YZ[,]$"+])apzt.'P XjW> M=M-p ;|*z[/S**o^ّcf/H{~~Pж;zs6{XFAh-WC\q\d@~ŖC]_s<ͧg]Dt ~K 9S9×=.s}3wگ Fzxگs*|HG*PfiG_jnÕ/<9}l}7O}y$t^?֩eQbZ/[?RίS)Lܮһ7idE- .8^~m/4aor_4Ú}_rsҪqUMص3훵1iGg~ܬaT~Q?F.N(zme}޾vzUE>BoUJKVFVW;ǘU.ln;u ZX5e!Y فe8M{ԱP/E[h}p<ފ[hiFdTsKBET<4I:N:TL&˷>}Hm?io4~s74[w^~[$E32*{Ru#:ʳN6vl* ?tݸ+@nx%~<'G{EgWݨiXŝ]]ԗ_PRo>Tٟ ғ9yx12nRjD{I2۫ym6,mj!w_w?&)mZ? =;ryJи/XZ2zbjy(ʷ^܄ ,ة7%RgmBW) ʔ(UyGWYeѿNj'~X[ēznIjXS2!մ5e ,~Ӽ- >8vk:xO̱W$0Ą,~즩5sFop5gF|QUkz"B.oG/?qwg>T~}?w~R ٯk@rwm ĪO2ຶ=eP} @Nd<6t[uEmIn%+nn\si;o˞?qiH9wkmL۶wd&޻I"⚍/߾cyۛ^g0LƳ{OܳkͨJw,?Ng6#&~QF-?XR G_Tvǎe=?Ao2>(p]ev_?}o52/,h3|9GgE;rMڵjXk< [ew\I۷/H=v%"3Myw<67UK_Kuj܄c+Wkf8ڳ٠UnDp׿y2vݼ)܄H%ҨWb*شSZrguեOn\m`YO(2¢^Q/s ҶwsSu\)b: d=(Y̭ ڽ^){[2 03qSS~6}԰v+ԜIkژXz֫$ӿM5miܯ(˳F7%peDfd}rMx_zޕݳ|Y6%wkj9߻93'V)_+7kF%-aҲޓ%ql\-u8RvEt)$Fe9ۯ\tc`* O2M<5:M@[ *t^[[{ hqlx"Mk|(C3B"۱?YB5UD5IzP@>QT1!ȠSO;t:Ϝb{c(=oZѾKE6>3_V".NYCiu(5?ݽ1bYsǟf(cѓ0 BCɍv9֮aeɢMӪg wkA_X6qCGs5& ڹC׍^n'zCk}' {yQ\[@kf˩sRY9aws6Sv+˃جu_9"γ^Ǯ;zw#LH}߼QBwKQ 'M޹+pھ4ƺB~YdݵYuu=Y9Ш m-l"*r,d9[,S%_SYg92[0jܣaYCB38έF:o7GW'~G. jnV|ؖ]~6kYǒo.ql6]3qݾ__uX/RT~_P!d6kQ=[9|_FZ\dqOo>1|9~גzheFSp 8K.! N#C sJàO^G! @(R}(B)Լ:!T`N;H_D7@wQPiFs@睎B_DsՇ}izktHLE_(!P]*q "J@q(4 y kF;n  {w B1 s/BVݑzdu Hiy_@!!%mEoAtI^Giw&\{R]Q5ځB@ =odM0 ̀j%#4 }-T1Q&-zɞ=k[8':B8|Uڡu\ !cI 4=$a=α!Thg!dpI;`7 Q| _ TZa2> !B*E-B!P`ځB!#!BFiB!B !2L;B!d$v BH0@!`ځB!#!BFiB!B !2L;B!d$v BH0@!`ځB!#!BF.B!C Ôt !!IB !2L;B!d$v BH0@!`ځB!#!BFiB!B !2L;B!d$v BH0@!`ځB!#t!PqJBPPt,r|>If>"6fzUaʄaJE!aI$߻ذ~ZR4)))11z\Д|.KQMr\PS,1@!}񣭭CIRDQQQ S{a5wE x%UnRɱ ^T$J!eddږtEgoocar$G)iB0(:@ BL(R*W3B(!BiB!B !2L;B!d$v BH0@!)GB!bI/<{}D1P]̋Wse+\1J"0R!#Φ$f#jߋI/g1sdW[򟐣p!Nߣp{DaeI,M╪oz@!Cc>V~H&S?G&aZ%={%`\ݐMVԺsC)F} nUxhXE: Wތ#}ǟRh x߻r٧Vm<}#Cou8̜'ukdf^y+EJ;Z(XBUl\m]'dɁ5~*0*B!M=hW6r03'ɁOZ~5cB2ə];saR:n\v:Ig~?Q %dˇWĈ_e~c5tꔩe\C>ߘ aͶ^,ʗӫ5Ħk3iqEǨI|Fvj'}:Z\dA!##s+8)S 0y:32Y?9tb,e}s,y (3E&N&AcƎtYUsTiSSbqQf&ڳUT*`Kk ΪH8\ L9?I!~h& *ѻ/=IN=$@<=y`adB$o?X@,c*y /[q17lВd,ʘG$JQdf*q&$0@lL1iB4,)*h$ Bիkٰkw\~).)!{v`)f\PvP&eS3%N܎9V5TK:{8c0#?Vl,3~y߁`RZOg"-vw%BLY~&"Ԡ8ځBsm=m݅?*f_W>-sslݧF7y65 SFƎn`OC )?zdCgF+`FܘouW(4lYu'f.Ͽh~{.z ϟj^eӱ)?Ps9tce;QW<6ysl(\ʼr Tl.]0&E7yy{Vu8BRIЂh&˧R];: ^$/wbq/d@V{ϳOJ R`l6IEEMb(#hu{Uf14wpŽ#t:/FKbE R~EG۪\]SrʼNlF,1[GrnvT V7m;p-C4 «R ʼn7 M1o@ M6 kb?j# gQYy} eKkCey|,- 6&PX:9&tdEiAJ^yOozɘxrP_ =w!edAI͖M$AП";@ajWsv sUC#eKqj}\1J=_ȻV?U4]Ցyf\h(#?4z.VQGؕr*[ _w}Әjj|--)R.9SӦMWZm% EF07"p5*J6S u_ ].Є0潳n̔e6IojG1H9oEz5kWo*?lۤgfcד/]tO-SWQdiN+޸=i!I)v7`$o@ xU+S90Uć7?:'FD5eK K!ʋ 9N؊EҬ+Fq'PRU!RO dU4H`uS164lh6W%Z\_Dm6mϷFvW<=}fRzjԝsf=)_w}$lA(6׆Y}n(lU\PWiܙs''7aa 5NP4W~@) Ɍ,UK4oTFEДT*PX~.W<:/X0ҵl۲gKgPrQ ,Zu% N RMi^|0noZLJ-D,qۯ]bwo򋉕{pʃ+7|N.)d^I]oZ7 ÛnnU.;?piߐP3d9\g|CN'n$;wpOz%0<~X̓ @udϜm$)~e}⠞LRS? (YVDn< g @ݵkӥuOV$b/%ݐ`Oq͙PykPnY]yp\sWƒZG _J6nZ5t[y~B;$0m&?q''fEkԶa Rzpו۞ yqo@vy>o` 7:U/jkN DD[Bï)⻎wdljmebL &Q׹u9ni "`1 [~i&ǭT|~D\wONٙs4ie~'o.֬m#Q\~&!8ؚڵS4{n7e:(y93Q Ti>bhcȎϭoIYdWRcSy%/T뿿'Զ*@wq_ l:  :G9 sO?xh:gYG x].ߖ}zmg[\M}A2l}2]3muk%Os7Z; ɺ K̹sgOp4T,R^֨U~`c!j  ;1,< ru?;,=>[ C rExnz ٚSCJf9ߵrkNW:WT1[[U*\}S?d[T o[|Uv}R3jbѼ& X֖fͬ['uJQ`\:~⸱y@0\-]sNLۛ6kfܥɘQR3.v쪩@rX$JeO|0ބҷ>O&#FR8G.@Vppjň~&&Pڝ)G7_x5435j6zlWqJO2@aM]ےq5< Dגeح݈+ ש( C';B3uN&]cUib8M2hPk1LcC>w}ٮbgAK)SkCznZF6N[5C5WuߐS}>Q636lTZI;n\޽b6L0r²k[Um\:*$ (o}IjwZaURRyl]% HtaÆ[XX\r%==}qRkv ںy3I wcb򒓦Ϙ[oп{RTFN*ir@PPZ۹ظb\*wjS_n&Uuj/a$0PbW6&$@DzW7KOrK)P#IӁ>6C 7U8+3|'ߤ=O6V]5@֪r3gt_R^6O%&Ŀ}$͸O]U#ssjl2uGXƉ5w6,Y SN: DR~T!SAZwޫ<ȼsl :U/8mtT nO4w,f_lʿI_[Bߟ:4M37o޼ycݼ=_ߔgϺ `oð{>?wޡ#G74sn1$88DII) ! 5C D*Mj ' A7ImG+@UD@XT TIv hW=#3Tt}VJRGP@4%IHk> ~Sڮ^QU[5,d꩓b B}>(3잃|#R>^))F>$ޫfMkԺaXJO_w}S*+0T:l4rNv >:-=$uk*_p m]x2XxźuՋ@SXI\|Y"888\zUMMMSSS,(((yղ(Ϻޛ]T֨U5GA$08QEmv=\3S.D4MG[j]lYϼ|`4p[?@?:$ >fLwGGQi{9eʔ7jn/^f9F\jZѣ{;_{8::taӦC $!P+6õꏶPONP3"Un$fjxSɗP0kXiY]p0!Ym^x}%t~8]FT8t0sOM£$Qa9޺7Qs2X-UrC ^_n(A4^e%|Hg]^֨Uҗu?^*~}d>.J VK{&"<}G:oR'THE"Hd2%BsD(ERH"I"J*e(ÿRH*?,D6L|TaUK0m.XZl|͏>Ͼvg7Y?aTں9r?O/ R?U;A+UsaڔBjuqxkᇷh9A"7@i{l.-o|=NpuTjL6͈ZiQ~JRo͔ PY<siӑMMO!A=>cU^֨Mu?NC ď7,YyGKrBw-=0Y?mEg@iǞH!}M+/<| J Fx@[uEufeo&}o8Zulc EB^dp{}L2T2X=IݹL=?*o?ɐ> Y4پ ;uO>Ƞ4e}ngUruRNy2?4Kބ}Y3A⾪epZO[/Oԥj_qxEPhx`rrKmIƛǷN 0$YWo+8qIf@ f65`ߴrY~HH#.=*tiڶF-a(y;E7gEM TafИBPxiomꩭHuK;cjho'}wW= FD9\laNZH-OcnzL(d{/:o:\Cy&_9 l>v۟F峕r†y/g ;%l8uθ"`m/^t/_0uɭrW}~55QKŜL[q3uTuׂ.ҰeJn-Ոro&]/|u-{ݳ}uoߪݨ`5lnw?%^^77MwgWBck2Em{*{H4L׺5>߰%GXy`ͧ%E'EPvԢ|꣭HCڑP`K?!j:hI-_~YR;QRG#2Y5 1U,>XtԬ]'Ehq?O(ZtJ)~dG||*My <*!K1l٥gGOAqnJڦ-whS.)G`KoBâ3lfݺ8鳖j[haa/SJ:f6YjW 0ahDJͬCvQ>^!)A~l@CoGfM;6=$jNYEACu4-*<>odll֬>GQmӨK,"ݻkS*Jxe_Ѱ zV]+M0ecWtbhU`A-ZD!YL@#&W=/<8Pts ˷G8;4HBOW} Zz_3kce~r̻\1i=xB}Eꀡo9NǎM;v8r{7S%B}:A!wB"!'EB!$O BH0A!7 "AEzW & ?cϒ `LX°Bt黀G/?x^li!Ycb}ϐDe) k1dndʥwu6VW[N9}Jm\z,7Mg=podJS] c&EfoGEKuSb5NC!UC%wB A!$W3۴6NyEcS2̡,释!_(f$˺ 伺mH%ۦ Eϼ._\3IcXQ7w:KYʳ1M-[^!зE[ݻFH!l:z,g=F0o',VLf`b0dYbgt'DQm4Z/}?$$;6Se* 9s.ՔEn.Xh:埵} s%&%Q-n ~}cGZt*[DZr՜謔p귥ײ̧I- Τdd hA4P1c[K%qkQRcZ۫!q 6#!4\,B L P1,jX.;t P:ya'U2~z(ahcb SD4Llh)-{m:h߲Nq@*pj[& PP"`0Ihan&aB)VK  q4X,Boeqœ6=u8,BFеdOeBnP!HB"!'9ܣA!EB!$O BHBS GFؓol•tsoٟO_g3c\@շ8kɸ,kCds٢9lvo^\z>fiZZ6|^M8Ǟ.D$$$fYffMm#@uBHn)^,*|Zu$Y K K 79Tk GOǔJ'&E118}umlj~=Ba,q_Y(CG;tVs1?_aAq禚*'vu\XN(?U g^[~Ņ62ף2o\|:۱Km&iQarčC;bťU>΅ ¤D!b2\.Ȉn[SrQ636l%rZI;n\޽b6L0r²k[5])!ƥ"@֗xZ>@|BIRt`/3 ئ={9} R ;]@ e3)A^ZbBWOBoߌ8;)Un8{cjZTJUyH$2=HPB)EL!(PŸsw}q[mF?tgWw=nmXn[;+9 ֡3xq.v]wG"a,j\BBTS|1 ۶D 6L@d ͼj䲻]xmm,,qBf \~ d@sb;hn?xɢ>zUDK9(}~?QӤnmEdզ'Gz?$|p#@`H]w"a,jTC/rE^!ߌY$r&Pm*]ˊ2@H n0Z>g| 騜Jy3  c`Xyg0zᩑE-=?5NpHIEU<)R$08QEmv=#f\ȧi"ڻزCy=o h@~#*Pc{1{j@ͷI%J@ze?9qU̾?,6-Tz^H& ꥭ5[LEx\0uߤ |wXbDWQk׌-i=p`u;& Y4پ ;uO>#b/&|ߜMlZX71RɯCc @⥽I"y.ȇ*TzIHN?'qc.Jn{(맗|^MxCMsV%DiN?*8B=OSܦvCL` @q-86i}i=YtiуcrV=GXzIR=mſ/2?N[n*q3ԻʀOv86[H0 23BߎPD@yVzi]{+nn߳Z帠TaRv==kWW@jؚ7h+Rv'ҳO;~txH$URiϟ^MH<Zp%EB4M#6g%Pr]q9IvTq{s̔/KiI_x;yW*|/Gf&pԌQ|4߉6?(̑)}3f4MW[f(Qҕ#rٻ\!Uae+N}'_&]9fxMӒ %cxK>Vbb!H#y=I):8-yсWwEǾ 6cw)MӴ$孓z-mA\י#r7,:K5#QΛOQ_wIO}y;{iem_]#O;[fI6@N}<݌@*ڎqdB@a~Qv֪: tTIY Dia MF.fN/s{ZveHMy<݋K%O>8R `6ӃcPƻV']nEOo^Av#}V݆Zu95kn_iCݦh)ܦvKX-%dЪs$mwwK$-N{JVJ$!Z;Yzµ B!r~dEoѷI%w(gB ".o7D(e)pX }uYT4Jo~M6,֗?qZtc6h~uCkϟwAAD׊!BB!۽Gj|x!p\!BB! cBSC"APtoH$i^FPjAAAME5sss\+4T,bdd!P=Dyyy BPP(LJJ*..J TB0 555###6- ~.  B}>GB!yX!×;xgέgFT\;|¯LwpP~M4iʙ6E *,J=?ƾ48㜻xFq" ' ʳst]ߓ||"F<9n|PY1tzYLfZ\;H8Z sMr}+>.뙾_>w j5a ߺeP Vw0`,jdTnه,M% ?~mtnNNnK?+ha\ztz_x8Iv؁F8LdaywG ֌ؕױ*$P%o.0+ya|fv?f}n4ymtU5-̝.8[Q6>TۛN>8~p -*byPRW q\}S@Tĉ7/F'nfhP$|;y05 `7סӋiI]7ruc3B'qL8e۸1H`ij0$@Z#HoR3eiCqw$ q|My44ΨDGv.?wAKK0mB.Q*qZ:|Hn>7қԦlfZJ0ODZ^6h(/ǰJIjDVTA&FOWu+M=!ت (P J ,36^X!ԈD[->I*wWfشI^lb7Y- X'{pH~[zRa# J1Tu9-Ɯ;6L$mue*hT3yJd⩒fnL]O=lR+\%Eˡ:/>zzm&},}MꩈH4?]1jyGP%[8(kkcڪU1HSknv;>mF(-2 €!xJ_]i9VE3)~]wcssZaD e Qf!d0vݎB9u-EB!*p!vtf/hVVtS vľ{D,}&AZ?S} >p_mwt3[W=,KܵPgZƶ|kWznF 2_t?d˗B!Su: uu }ɛ-㽶Td/_kACzСEn' yzzm%R SQU"\h*]GyYi8K/Ұ79 @ $qk!B7Ew^ 6@ Mc͊,!˹spRBXGVkah5cUkKK,+^JDgx-U&U14Y6&x U)WfD. MkB!hy4hR F)Yy]e!ֆʄɺi*ސɦL@R4 *zf+~4 r XʵK6U 4em}3zj!Boxyz "^ |LѢfY'e" {饥ُN[{XW&V'w_L\pUT BhwiʮY~/i($4y۰fyYg7h`[#;bJWŬ_Ոܪ:]'+kwf^ 7xB}}I#<}!M4tHC O p8Qyu4{p8qJ%ccP4' {'Ȓرע'miYtGK7wk 'Wpgwɹ c]mGچeEOQm"!}s4:zM(H{g`hzGDQg3ON^q4Kꭝ2BMG{h;K[6oCUg{Ii;Zt7Dd܈KznM{uz|Ï篝<¹HyyvzG/NΎ^˔:p8vSfudec#O_˨lɑ8kA7_ɖȹO,qp8֎fy3_s s݃htvā]9n>sweu}{[s9Y+NT/ OW9s8=&lZ;͆c6~Cܣ壜qFUр8ږCr8==&;^D:Ws,uSMKK#-u @1}qِq8]K?4eƙ#=yIGr8]VM M?t2C6'?vB.ѫ cm?cnR.f[WMoߎظM'Yѭmkk NQG݁eEt{d<ݬ#oώñS1SY"/xν%AV߳w7)3Ŋ֧ֆ Eil9]8.n7E444{99;_gNW6AeOO䩾H>s1!#~FX/WMZ}K_^,7zI>#ܚ?`jуٛ.%xq˼wѺZCBvW` :m yeXc+;f-YMAѳc˷$RDşkbU׍!r^hRǮ- VWͶr3$D6gz2[:>$K216P{/onQDl? ;_6u@}cNq;mʴ >9b^6&Iynlxڐa!-/[]p=&.zp"r+*/u :j`],܅U.GyʁwxLS;-Ì m !ύ^pu񆗗z+4!%[Ki\ٴr?۴B'G>,ؿgݪ3ZR]0hbKW +g-P7[,һڸ*d]իW6egҥFM Z9bW1#zSSPq_m$9^fI yӔtMt+TZ#d@9/=9GɌ-Jp8N^tQ웲iBkiV'-Y8?9_dWОAgY3j(B޺58U`a 52U:y_ζPg|Nj`9#͌;fy_ ƄS+/H9yZpA}tWMPP`g0q̀ͳ_6sTemD z}h熥w$J }I7g Y\yÒ)K {g Γ|xR9)a>Ptp7NocUBu%Bf?%erGez@K"Mun3jREMR"15X8/;Bp[ 쫛>,u~IáeuL Z* KPUD7p+ Aw\!H.J0hZ9EMhv+ʮLsJ_ oU]g@nPT^ø?gޒ+M&0nN~X[@ݗ,;h@ڙY~Bs 89Lܴu\/33Mv\ۊ(ch8!ؿNs/q6g9pў} @JJr˥uڐX=رSXto,ϧ8V>`AW'*ɭru8)ңs&V;oCmV?W!/g3ePuaon]ofSC&Q[h);jHz] 'f;ݞboKũH |"Ч? tC^YB7p^!B sB5&ѩբRđ>b、6τ"bVr=}TR;HRv1l{kN=w?,ޭއ3,+~O"M sχޤ״^?UnGn[wP{rFa\Ӓ\8F} _l| {g^ӂ6'b95?\! &xX+ٿ;lGON^<)UPXEVtkc)b9k5e\9e/8udРje ={wX; 1>SL,+b☳!ӌ2++Fk2'6%/zڦL#O\=mc"ƆGk fNt+j!Wu7iU.݉e3YP`D'' /eb hlSk`3v$R.՞}LDRkXvLJ7Z@RU7YOMƞAg)*HzA][2c]x|b4&))r]^w2M pb@[) Rz%WgѮwo`ש#L:0wޡ+>H2(k*W֦h)IEs*EA>B z+ߛ+}.Ϝ+::>#_Q0$e1j9wטrmOHɧ%]U~`$Lj 0y^Kc9ad9S~R3SUыί"/A..\=WqsK難4i/ -S35.IT3[*W2lӄS&*5ۓ+,S2^qsXFZ_ /+2$-׳/%+Ϫ֋-9vL<ӞCFOG) I/ IDAT 칎R*&ő4$@f]& YU3nЅT_5FgN3F}'XJ t JEL3F~ԄZ#v/(Ŏ|SGU<,GҔ]-YW|`ILQ1NZmWg[SG1dY´~.kW&1le.Ō?t$i"x3]uWW;EW"O~٦ofn[CSѾDiɀc=e\>b3y5ʴ]WX ;}z;fmVۂo"Ko,|M#3%'9S~{a0TQFɋ9j}S67_h!i"$Iʠ(Tln?BR#4i:j{Ke6tm^y44+r3#Mh$%:ҒC0X @2 ,g=h̼Z%:4}Ťv=v^Ez3QO N/^rC!JFSkZ{>Bq 0r525qZÒ!sPE\2HAD4Q+MWlpq~rXAk ⍦Wg[SFRGMmG?Ȁr^zr[BBUSqv>eȕ5OH`Z{Oߜ}_P_R% EIln~}!E3]Mh>v^S߉Uu su];6L^޼|6^~`D(ckq^wJ|1 r-Rn8f6UV=5m?[\L->=&,S@T[*Sˋ/llÂ,/'>ZRY]eKvŨܼ-1dٺʥO.M ~+ 25RjP,CZtKYܩg5~*/z̞m"V*+*TTms6 YA֔-=\kuܢ:mzO F @nX.UH˟\f㑝u #]NILlHUT}gBֹ[XV?D{\QCOގSQ=2Cˠj @ԭKzm}7pnR#+ 4lt8pfwOV'@HݚR+ ˶+9_v$uRɆRvEE"N}Q6Eq [f"RUJECj!`-|GVu4+Ebkk+[ [6?x0o`ޚ%q4+lڭ ./)f }&SJ$I?\Rr٠~ J2O['%YQFLz#,71bСKnrS6^IJGTTFTEJ5fG[󺠼S|OEOr-$>.H퓦Kբ 2K/6@$Iq> *ӯ TB?[~+n}:qᠣ%o :ԌK%)IcHMڜ={vo&qq<ʺfh wo3& BZ\PPNY.DY @HUaCy%6ϢM5 ugWC7}HOS:}AݬZfx-/MƵ`aBL0gK]:quڡ}ufD[TT!W(u+v"*$I6kۇWV^4!i΍B&_g#/|C{˶t^_X #S `0eͬ._+++fĦf߈}kƨ2DVt5mKjBή&0y=՘UMqU+/gjh2* Mcʫ\.D7<2 l폇y93Ѩl@ \F9FJ)a`Sx>eCUg4*QN!~4 C_?rk7EVr&'PWWГ$ g\/(,rrrzUʚ+W ZY;&$/xY!s3EL]1d07"cL WEWSVJ$\r 4f}MEhlӬ2YG:),+7e6ѝ,PF]8DnBox -BB.ѫ cm?c8&ݍc0pL$m͈rqϱ,$u ӹR^nNwWUgS$)|ϭs?yVҴzy*#~^¸Xa_o΂YL8ʔ Q7/W?֮/C i@^Ui(zz3״o{m=ym4=9Sy t-^Ņ~4Rx2nN\ّlcs5^aԑ\u;(E>o/MH}]6HpP%ەLcؔ-M=-YW(Bsw#/_:ACM4݀~T5 ޗlT$?}9B6M rE歛(+6VDv^BUc@KOQ2U+M,@/ΣE}"i*CJY(ݰPo81yl|[h:Tn@o^c`hs ;tEr\R+ [\GϔCF5w!BB!"!jL B1a.BƄB!"!jL B1a.BƄB!"!jL B1a.BƄB!"!jL B1a.BƄB!"w{fwNm.!υ׌ 1؉f!gܥRZƍG>.Hzr8oʲp8τk5!m,+~h|§+<^>F.g?FcӈYQ1wEz]\9/8[nji,}t얼u'o_ pp#;ZۑU b07}yts.>W r }m8̗U6#+e{.6N'Lm>ĩ+cҺE%c/[|펈N7<Z\9ϱ,Y G?+9'bX;y;O\aa5۳=wDnxRZ)Jft%oߎظM'qA.KVL:Swv^tOٔe ={wX; 1>SL4G3˼{u{ҙ#Uؒ?4eFrF/L1v9/th7[ߪv)JnG.`ݏr9k5eGa./-q9U' dDd5jEώ-ߒȇ7]JV㤗y{u sc88jv뿦wGb=fƸGǖI*/?-u}uzea֞a6'bzKZSH_@A^2Fk2quVۅr¸&툎=yrYVkm?{d]V k \!qFuYc[qFKR)h6iϞ[9O6}능!={~DO K'r!x@/Op⁆$fUgl]۩|B-V{bb6מfŝLm9OSiefd[1Μ+?RGðOpShlSkkڌ?DRe1j9XRȗ)\&))XyGkxQ4EQx')SC;r>cT10Z2ٵvV, u# jih;L[z-&֥( j8O}5ee / D$:3촀xzwG!Ϟj5*v #u\YM HI(gŇ Y.U7TjneM +k=RUѓ좫*+&jU4%vСAcKb Pna\a&2`6f4Qϒa߯jhےct39d}4X5"2Eu 333XFZ66WeqnCKA:cf DTlǰ)yLvQl9)ͽxE!ej% iIz ]_S|Y<] ~dW{_-~FsvNrQŠoLWj{v(t87*luҧ .?7gy#xy:zzzzzz-RwYr}uևE/zx8pa[.kq0kmCV%25RjP,ΠٺʥO.M ~RvoeW6?6aQg>C*QN T M#@c=,n挚~JR](US*15X8/;Ba6-[. [Ly`m82EB_E#/%"-){@kKHV]w.'y,Qc'ؿNs/q6g9pў} )HIIv^QVn%N\.]jK:Jp#.߆@ی~#B^ZgP*Eɸ;ytZ_!K6~sD.I3p NZ7ɮw !(ݰϜn.gz[42Ntw=iߖ5o,WTNFƦ6Z܂{]џ>Z I1.]=[]b4pYSlG;tUczyN{Yu4Ax/BLxg`爟yCW_|_Wl:΋ B1a.BƄhB!Ԙp^!B sB5&EB!Ԙ0A!Pc\!B sB5&EB a#Hf#D̷̺ƭ<.vSݞu MZ\}}<מھ ɧl0A}7*xQ|\z/+'RF SOFisOtj̞E%gK|B"Pk׻T?;Mtd>$aڝ '+MBK:<͖lbD{u8k!%@^^!]i鵰(Y;;G@E#>A,vs7dYG V@C6'm! sChK)q:Jߊ@aozD6e1}i$0Xgtܩ{VVǗ.膮as5+hD{㬎W/,FԻ]16d>* osw0ii8ΕbK/y-]}u+Skwcz}WՆ12wm&4>C>oΨK 9ꆭEEi7w:!timˠ3 EZ^a^?luACa#д %HBkΚB΋ /f֤ðS `Y45k +"u:OQ;aԾU-A.ݳb#vY8L<})hqu{Aj!:$Ǝ!B]x!B sB5&EB!Ԙ0A!Pc\!B sB5&EB!Ԙ0A!Pc\!B sB5&EB!Ԙ0A!Pc\!B sB5&EB!Ԙ0A!Pc\! %2Yc`.ȒM'==qJ/SN_/#?3iQNE3js=SZKVm3W Rzi_n@X=9}с*Ng}N§+<^ڊ?/u[$u`ǀ//&u_*w+FB+8,@YVβ}O{}ЗqZe-Ib)5F4ZozGZ;s~~cOwdY \>2aoyFID8`֎e'Tqn>߽eo1:9u|S Dg=/e깎ҥ^ s$yIGr8]VMTJ 5њc2|qs\Дg򲎌Ze ={wX; 1>S\ Kx?̻A.KV4,N9y}~V@.ѫ cm?c@w}vmB<uֶR ɹq]nΣGLt["͊8l8^.ÂΥu->3kZDvϹ׸⢊n#DF)Ex='a.* 7s* J*Fv-z ?E%mk: ?aV$ͤU/|Z^њ ]k&{skqT2\x.{ېoCο ~^s^_+^{ nA&sb१-VVYk@W;UQ½-]LCM,> @s^L%=R%<McKl^rHtC9Kﬞ).DyWOۘȯ:8Q+͊?oMc,!Qb6b릁>?v̬Q.ˍ^pu񆗗z+V0scã}׆xS|ͯ?@^byѤG{ݽ>6O^v'$oe1qۇ7'( HҎ,lYRc2[E{\ouݦ&Bޭr4U֠ rY~c\o yMEU-M\ EǺYwשv]cyJ-8ƌQ(#9FLz_\\MQ#fQt>AN{-j'}|V_5i|ZPOR /h &MRS*oe*a+ ?dQ'ҬorСA3]NMNlx ht5^(yT8A: ޡqa.䲢 2 O-o]XL|=(_zKI9vkli^,:ٕZ6AIf&J,`tւs[I@FyT ܪҏzD @+77fA0\KI|jх2y+F XۭVj2 h=p"⢗[NXrzV0mKDEr'6RwK ef&"]_Sa˸9e,#sr/+P&u/h! c+Wp=n]4i/ -S35.ImK>4}>ϴF`Q~hFZVkˬ<3t]5,׍Sڝ*Qa.h cxX5$xAS:"i\=y~ĕ@97ѹ&FL!/\ֳmԭu T5G$bisDb3Db 6U!ۈ4*J)k?mKg87i(.p[*K]M*gn;A4ғsآSk$;?BR#4ѕ]ot 52U:y_ζP8DhUR^I ӊ b3SE]ˋ9j}S67_(x' 3>q3u Y~Hݐ]# q3޴bVRYi=5&^!50UZ|رuPWo+/~!--6=K\@.<4 <'-#ޟNge2u-8n!KN0jߖhRWA#ڨ r%-zet2ȓ^y\U33#" wfj 7"⊢-n .=▩zrfR^>Zfe*wP?pA y5\ss~3^\_ι#ADBe"/ٻwʟ+ͺE>}Ptdm|߳+n]=s Vjt|?e͢-ǒ\!6\6jK}W>gVګЧܺpKvGcn~5u;Gtm]өw'\gXߒ|҉Fjyb+9i69eBWzh d`2:iOyzKƾt.3`*!4Vj!Jxm~O~]ݔVxZs֥ԩPU%*i۷0 7qk7(QB~.xݿ۵ZdO##}L7SS&%6.rrݯNcg/xr⏧UgZ?xϟJ|A&^ y`nާ_=쫷/+TB\ϴQAj/ͿOWʨfnNZ>!gV2fP tݮw1fZu!7ּ:~ˈ mBx=_/"5Fm{u&˼eo=oSE7kxł·"@&a<"@&,d""@&'HIN^0! wW y=.=_mTy1ƫ"b*_n*Ťuz7bHÒO NMkSan? ťWF9żGwQ$Č??eûp~ [;m74W'Ͽs$^7(”S6M}5pĬW8hV|rAc.֔~[/?00߯C7:e.>m . ݭ <@xzEX^x[Ggilw-s;^\d_Gnk7WQuiM7wGOެ/.)a3&mhBn#{৻=~qvcܻH5 ޠhF3%6e;k<30ee,U۬W;q4\!D7ƟbN0w*nM7mP+1]a+E*CsS;vl6.ewzSc9rW6}wq j^9 %֛{bpMIk5]mdY5~omlWWPmK<N6^ݿ|֞i*go,wm5rv!'9(P:ڪ*ZeUKkuo UE*d̸zWK[!l{{ !bk> wZ{E"$R֩^؜83jK9k75ЪG&bA Ү1>JS˙4B({WXij'n*^%DnGܡz7k~qAա:<^<,RgvtrQnݴa=6qH4sfժ }ɭ dm.y1*}Zg|uxD<",NД1MNХD->5ΡZiӿk<{Ϗ 6lMJyz+qKavn ƚCCr3湥rlWm߱kJ(v@:LEg-g)Ǽ1!"@&,d""jgj1o;"<4uMXLLbх% fmN21u"-dĦ~v<–k@O׋t _muGiguė/Y| ~Q2l8gm'N#!S7N3aaO'0KuKc/˜wuè> %L9VMB{!^O"Ex6CLNh_Iamuaj!9څ B.}2_+8꯬G@ IDATxgXW3ۀ"MTTD\QQT#blQ{+T5"R¶E;s{Ν(@!j^ f!! Bf!!BB!$ BH:0 A!t`B!,!BҁYB!BIf!!BB!$ BH:0 A!t0$,G U%d6֪B!rIA4e3B!+2!BB!$ B((BMy!& PH%qO_ zY,qvUiOm{-8Xc[;Nii ӏ@xvأsWo#Lj׏ٟ?E>>ݔdӁ,-=5iw C;"Kz(J=? {s!%Aw-UyAŒeiio^xYtlts|H@܌؏\ iVJDijllR]3@xSTWRbܾ@Sɂn-ϖU&!P@Q@REѠa~NFf?ѐ @v=eBF&{ շ7"Klz)4[ &9sȖH W4m] 'ݰ\V僒g yׯ11i/q/N{n/H?7psȬVŒ`5e݊)کh(%!#tǶdTR<36F+R[MUAGo1zاS8,;w]hӷ9-Xr)âX4 I IH 6)YW/ofAKIQosʣ7(t$۫(amжMeMRzz%=YA%X}sGFR;R1ޑju#L|AA<G]ƢR21qctNex~|ty]vzq)65k8YTJl\gЮX>=@+ճ32N*Jynd,,--g,dh4q*AG}-&25[m k@ޓ[MTh;Sr,Jݪ< P?J rEb%%pӚGNƶb'7y[jiZ_1uTpWw; 1O*}e^>SP+"PE}gkkMxZCfaC ?3c{r8N!u,Z̾7mS_kӴe+}}9 Ώp8' 4k2A8Noi/+}gcUErp؉LỶ{ԉLV62d3Q 'R'gڽ/'[$7}s8!q90\ےiOp[=:%CQx3ǧwb䡽9^{/jhZ 7ƙ#vp86'(O53iw~.U;c=dK$d]ڛq+/%*Np8}/E!96Lsp휦7~h ee<ݖH攈;Cn֒sgiW97Vb q8!;NplWؠfEg'|07..НPPR~@]Ys ք6֪T}.3(&_X-&h2E*> ckۆfna⒌(>`SWnw>1<,~x~6~dA)[n[@%iGZPd_1, T,vPQ;0dA&zw,C>4o'r ;gO_JV~ #[7BZ8ehaMT h5](/ڲ'_Vɢ{^o;m.ž%K#O/})G(dŝ#+hQ㍓~}@)*i1uf[U0{/WA^v IX!q`+ZpK6a$gE=:u汫}{ݏMXVR=y6'掓},Ѫ>e8tܪ4 V {= I\ٹ=Kd{T^ZU |(^NBLRXFzr8%&hFKNc!@ )R$lu~"<^2)?R Pe2ұ#9!՛mՕe:!-z]i.yc~WÚcVuzW<`O=,40(V'7LA;/y{^ZɑȺ}I 2qް{\ &3]LjH[5 @aOP``ϕ-YTJnFl?|ҟvj42]> j6ή;ZTqF[i{ԾuK~[]}B=@mۼ3D /V41pAS3Ͼ,ԞGy&rϾ 4uC{y͞_ڽ@s)( {|e&Aoe=cEfгwXd[YfKmAIpS໑ׯo@Wj7bL3$8KT\,Yh޷ dѐ(_r];PEMӄ5 ')b]߹@!q Z C}سxս|13 N.(~xBA:/|z@|qՌa':9Y}Ʉ|Pӥ" *)vՏ`*33nXELvFmUSr J$Aߺ~})y@ jJZ&:=;jئ~^'L˼׽ H$~'%!ːe*0;kէдMW LGv;ĠJOT3RR$1{y?e3up3pޠyZVIS RL>j4͊_4 JVPS?5g՛KdLFͲag\L,кNv028ge$0]N?yoU JEJT[_5r%dy`VYOʭ{lۯuc~=@b^l9G6\Pun g;Uij'-$>̬0w~~~Gi۶w)SDB;w9݆ oA$t6o~"HLNSc@yas,Dq%8K3i oI*JkR)^d0nPm9Bm7=MjȺPlg^mJys]]krH~~^ uri~ ``,κex}(jY@,ڠu'BCc67I_Fbu_ 'Y3mOJ| |]O}|RSR^4r˗-_O>{/WW=GpKȈAWSH"3>d )q(w' D<.(jպ-USVrAFc1]A;L%"[u}dyAQ}.P^TN5%LH:6ױZPfkT3!+j%BɊ}2nݽ z-ԯyr ׵w;DC0H!oTjw+w8 sBϬ[q /#6,V5~@(uq^D{2B| 9ȩ]e9jWP"qBR nݾ- mll޹& eddN b@E4 !hEQ&[:tILLLrrrO^ ZZNNNΝ{exMMBuuu&yãs,$vV}lO\|{~ibFCA/2@˪* PS% 1/.KuvJ.Pi o+)def Ax '.[充_U !থ֥RMVQg~ UG`ٛ?Fr¢]ǔRҢډ?3*TZ|P# xf2 $ny\AsMdn=+YWn%D={(`ˢALS22TƈɊMA|hV_/t$̣h@R'Oko/+=o^~gϞcN],G˖[,^J_M6o6~Ҥ7ݻG5ʘ9O3;&׾m,zuJ<h j"*Z^#L@eTǓG }2ϒMp{!U"%䑯mL ϪK:|ࠁ z*0+ ~%g_ r}rHGHn Bδ<3E5 whK+vS]=cWQQ_n5qՆ +jW$ҿ~́~2DJK dw$Q/" Qnu,,ˈ |]FSmTM_ XԶ>fr ǝǟN1}n{{Ȳ/Blw7ņx^0lۡk7R6iSn-5~ ,giQN IZiQBw,M#SsKKS_, K*YZi *zJZbN/2Ric}{Okj]BR d Д{]@GG'gsi:ڴo5C~́~g!ɞt.IKh(ANWV׭_岬( ]QZ/48U;4{@bHBOE;u[bЈJ*屲 3a@|rÁ(GǺB!T|d6|ۓeN_)O@k7ixnB5f!kvMI~Jf5˵!PI:/!BqoB!$ BH:0 A!t`B!,!BҁYB!BIf!!BB!$ BH:0 A!t`B!,!BҁYB!BIf!!BB!$ BH:iiinB!~FEQnB!~FxE!BҁYB!BIf!!BB!$ BH:0 A!t`B! yO IDAT,!BҁYB!BIf!!BB!$ BH:0 A!t`B!,!BҁYB!BIGd! W{edӝg5EjZ1y¶+7SR5B!Ba>ᩥl-# B?FSW?^&=J*rju:U3wCLe#Sf;>Zs.<ۼ~?2i 4y.wTڀ,gX\.O^bह.֚ Av؍n> ͎L">L|cժ[s&FS%156wpFMGvezoɹOz.VK}v% 5.'LUY;Θ^|u4yBBDnY:0qpi繀xnNF `0k]dd-8A'ܮKACG z6[D[$_) 7Fi1fCSz Cgvv=Yj KY('0T$}^Z%!G7}kp+;6])]8,$!B54znAe]|RrJ1i\W0: g#5+H)dp&2WhS/(4'G)]@-L]QVj,ȉ0͟QNЙ^=+r U=ZC#Bxr)0롑t ~[?5ўh_߃ >hlAB!1) h(-f.{wЭAMoY&ddBΟ9s\$T͵ K2ZJto7jn; 6rӞ}Y2'H|:f &S0քb^;m&l3 OSlz.Ƙ Bul89L/?P>+*慇N7 DvYך7W^ Y"K9ZIOm-Udd-]3LAr*V Bkt:%f̤rU>O>0A!",*ybiAV:da@^[M`?GuxI/PuDKK;]s9OLhz?GF#/'Xspy%Z}e'oڌNFݯǎe@frhB!u.]wؚգidݻv3Ԩϴ3;Yv)@Ͻ*{xWGS26e]Tuw]~*&..%_ gyՎq?ťcgݟΥX&VcfU恐䄺.׾/ Nêz4#N^|[w;j1ZɶCCYG "Y%,)ԼбDhWYX˟[E޺b`/}T>\?˩d./N8`#]MX,:YB?7^v]MMM]]9Z#LHBL2h4ΰ&g4ךSxm/+E%zmt+_'(ST b/oͶP>No ~ΑUD 3np?:䌴GGPɱѓĕܳ$Hy0@ @$?0AP0շ" _@9-=,uK~[]}B=@mۼ3D%"Oy`2~{@h_`yͮ*8;!h \qP`@p (߳/ ReKʣw |%]YY7=˩,Ef?럀u>w!OAuBZrFE\f5şʴ8ӂ@\X0dCubsz[ѠbglL?$BTIl1]+謁E M !$0+Z`hlE=JC@i(3 |+,2f~4 RKf@Χ2,Ƕ$`Yc)@yjUr];PEMӄ5 ')b]>rϾ 4uC{y.'[Xs2Lj) Y)cU@{ĄuM`=pr@QÛ:5y⋫ft /K~5gtB@ڗX'E1]*甆b=LBsb4Ndg;xLCtGMRH%c2j 78K }O?eN@@~#ZFU0tΙ4|xgAծy^a2P I ˔cNӬhqe",RPxo Ⱥ1s~U c1NDSy#k(:{o*XT h] q`feiϼU i 0; Z}Q2O[VAׇU?&:1ox@URH'%&|~~|eo?̪w@ mEkPBRՇu˓_@S"D $??outyqNNƢMfjk&oc#瀨$%28|z~h"&JFNP2@샐:4e<PyeU )íjN4_xcz|e]!:J5*4n~I\[~VգnD~=땓zEXS ڊrodyAQ}Ė(/*'[@s}~ 8'̺~1ro?ab%"[#lR'+EwX/#4^U6vH0 A5/I!˖rcWvrc\XkhHA( Mz9t17kkjֆaK%@$87t&d<'| .ק:#֮]3AdhXٱֈϓ +FzBHE`y/v:@ێ |i{594.XчwI}, =H8Fw=UX, >d[MZĊٕeG#LF6: g-^ "C'xq#ܡikv8}J! |NB9}Y[پ㖶WE~[OuICCw,l kCh,4\fQbwn㵏ߝ1m?Vyxyx45K,]<>`ccVg.0f|_=KqMߋnwhkbDe -> РA#Ŋqsfo웣QQolO^z'϶NvqT6ժ_b ]tR b)$XUsԭ\Y ӗ^be YJYُӬ:)d_]=ke7lM}h*BuyUĉӧOk+!meė5n~fnσ.{*KXwXbZ_j㛟V1xwI}Iϡ> 4uͧ70Vq?)!ҳ_Qz 2 ]l\wyǞW~e3gYj`z{.O M Gu6,%UhXT"6O9Z8p &Ϳeyoyiq9vnp9$o#[W,bǼS55k0.8|5 WH ^ۋMȧ>>CZkB$k*B?|IA\]tMzͬmvSn-5~ ,giQKFBŇG|%,ll-4D= J%t- R|B,*J.idjniiݔ /6˘Ra]jȲ|.MYQmu⮀4C~2H2m;V]nɌ^ILwR-,prѷ@v1&ef? Y̚H3}օ_Mc4!H;͛l ߋbf;̶4%MzTb>dy͒tYL5S6&۩c'I6C~2HYb 8u!ro **ॻoUBRPΰ,) +T3Vg|# `|Se ) iZt?,Ha'eSL@W_.Y 2|+:B- !kw0C>J0P:sKR_F=w?!8ߞ4YeV7e(JZ BL  d<;s}FSrkNٿ|!l3;ʱu ^8(Sirl*5LveOdt,8]m/j՗ PQA5Ȉ# +2!j0 A!t`B!,!BҁYB!BIGd!A$OFUUoG5&yjbaa&AUQ}@۶JJJj,UV$IB?o ;5gB?&yv*x䒒HG5":ܪU+% ~"M B}#B!,!|'Nfp;Nt!,1 O_ B]g i5+h^ScYQڕTT"Tj٩!$/5w[/0s5Q[O׍+V3NN4It~YaR|,ziCݡiP3! CR5PU^AfՃU3(( \1hڅ9rztY{M! d[;pz G|œ[slLJd {ql8nWv^,#7W ώ;^V.ٗ8U1,# =y`O/ zʧ@vekwtӃsGp&ZAve߼=vvn7ag2_\K.-\ꞕ6{ܖ\xIuםñ?f噈"UOq /Ntr)ߪƊ_ˊkmzRmjń3ayg7I~Qki"A6ng@񒼶Nun7~G26#P{jl_jB5^ߓ{ȝrQXޫ _n_o=Vt^v*ޘi+&'7!>RijWRT#xSFQTiACRŏ?5vWZsV_y]fAƛKsm7,(ay˯,c/3q[tq[XWğ!;8v>+Ac{U?L0#T<ލ5eA{mYYh)[2^<8uG]VŴwVn3[ʩwoȂMXn8z0y5RR\K!lqסdeيr7+*'1yZ$Rsٰ`UMM[XYKZl7UF{a~Jc2&;i'ǕKֶ`:1ڮo_=A.@\)o,"N‚T- A5J+̳ePW;\]l\N{M*0559wP%b BiZh224 gW;  *LԒ:lIMu\BEH"X v| 4V3J(aJ fuZob!uKKTFd ˋS7khƒݡ1=". IDATg `2*Us$6 *VT-f!fR?Zoj!@̇Z%stqd~e]vΏ֦Y1CFWt}YFsd+wѕ?n54%˖spS/_}?SF@q3>(w+jnD>IlKvR UW߀MUkgb W+guTZM~U"V1}W|dVTF$ j_|:yq;Cӏ(_Md7d|cqjnNRP3awm+puyܬwO]j1B㸼w\/гMw]}Vxn%?Z;6zwnk;Idu#:Rp}Bі)~o " е\:{+2$C:#Ȉ[{4޵*ג 2(A"YsbCć!IjſJ%npUwPJdF.v\+kwξ 4MvߘyZ \pl?c=-PׁPdE/]j=lL𛈩~]?.HeAZ}_MMLS%ؖcMvQcw_C15M{Oëv9ئbCc߼ZWW*SiF_ĭ?V/GwqR WhjV Lh D3sX_V?#t>6Ӹ_}yFƮ[Qz/̊80 0 A733:_!$ xE!Bҁc!!BB!$ BH:0 A!t`B!h,~j̆x3D7{i+7K>!^qR!z,&f@AݵRx4?5NzlI[)(B3aKAj`nC.hZ}.ՕXj7rG+].$ AjNbBǟ_?]Ԟ{%qM^9:*x왻!q|)U9m^F{|On;[cm@?w3,.'k1p\kM ;I7bfGs&cUI8҄@g;'oxRfdGxHÐi./=+bjV-P%15JJ?x?qeӹk햞GIilLz;%j}ZlêdS,jD4"vұ/އK;us7ffȵ]YW" m :v] J:jxլw׳ܺ&wz> m5|D_7wH)M}ϝ7N&0|o_{Gݏd0h[NDʙ[)N9u}7RLȂ.$f6n?ElEJ+!"`(^҃=supYy&]-@v_0'!~YD¥Yinmyɭ(7J=9v,i*Qڕ)5۩݄-U ;LN㎏p&^KOv;_{GG^qǟ3x}Xq<{Lk/+w,d=7~ӡy" K?; o7z‰4""b{Ć;X{CEQQ񔳠"`{C;Eq6޼};x=V+xkS`iBۀe,w1l{j2b)S?G+g.X8!?!fv>̚PE@T54Rํ=wf8lR$= 49oў 2_]Lq9.8Z7kYs|rٳg 6(J"Ud9HCN?NMùJsX0٠`Y+WDMs!&A"i"l){gKWqTr|4YnZ{Ƨ"^n3tWIϜ0&n#G^0gjFj،_0%?;T%3m $*4  @  *VYyTůBPcB(7ΐR*J$(xD48ƹxK)Un07Gf85;|t?cGf;n3c(Ie6?v*_o͓%Vsc0v%s,{ľQ4SY I Wa*8(!C̀+xV@Ņeb)`}ͣlTHk! Vd5*if/E1֋5mh?2R-4W2CD;ya1Q +8cI֧T`w&@>*qUdQ Ijsh t 2LUwΞ8yv"V\vnBJ''iY;>UCaNf'n$k>߻jS7M :v+Q+@0Ba T*9gVE/I0߿Aqj:A!RPeG bF+o_so3ZKA͊W۷_d=?6 CV%@Z$_ˆaͥu(xi[Á,j,޺Wㄓ';)5jwLFʊ3Tp]e=>v|4dp?.M~ttф6Ύoun)%6x!_r'*\vg9d:g1-kQ JOexd-M=;g~^1j3oB io#3{wtZq+z}+ުs)Tx[AIan3n"#D)׷Lh׾=~*<)Lܩ}ErK2==nщ"I\LVBH|bD) *DxԵ}QPz, )QwNq~дs\x_ZD}zM{n}Z6pԑ۳z 9jlN;0U>]8g;Šu2v7K MXnME`@UvsgSsDcb;C-m^hDσ;y4bpQB*8H?Xn @7i%F]'-2 9σ@W# ?Hy!@~G_UL۰z0a0?BǂD9! SALi@,#, ~4c0|Dp\`0zb!%~/,sJ0oBAO@ž,<} So" 4;#& DAII$)`B* t B#D@#P_R~ WSC"HA$H;"  O4""7^D  ~ EAAA,R/A%N =?慔́)  !) ]#Q8_TD)BH`" ! $R6`0{!ˁ" F OE_%qc0 5%Reo&@H6jAq8! D@!RHB# 9ET欖:Fͻ2`0zGUUoB|j|*l,`2$ lw7<45?#S:EnF B(򣠟%ژd2 ܧ!I .! d1S=?>Sf4Hix!Jf3 `04MT**'?Q͹ (@x 'ȚTM銨qPɆrX`]dg iF961 ~!/Ż] - BxZG ))d&,%qD? )tt?':u=0L3u˦"$AJ# DIPonbZ[ 8:50p YaJLBA$$`:[Ĭ;c!rs"z\%Qx=IQ]$x<{X7gx<ބ+y?7,$Ngcgm&Mqu竜}C ~,\&;5(Pi֣#K'9;xz 3gk䗢*_!;?NAhc7-&O ෡μ7ɓu(oRQY$)o~)0=W( zo+ׯEXd{hxɯХ60zw`+~8귖w]s+ߵз$ft|ۍ;h#u|+88H018xkk^^6ĤI.x7Ʊ3dz5b^н좸#'_*??'gxCf츕AUйώ-dYw2} o>{ˀ).~ɻ\⸽.. 9|ݎXG]I4[PIn<ohg nx.Nn>bSť8ĩID78x<:ͽ$]ìjی^K+%ɖ\\xJ35g{ЗgғMa4!mgPQn[ k'DJoZJu@;g4)Z:e)аfH%bSguwyG_V}G[7a@WU''PgW|ofO'f M-vs[g}zWE='`.=\~G+bA5/m4gm?tNГ|ZQ)o':LP)gfy\v_*/M?;sעm3g u?hs8uM=hªr.,p񽚭(9w +xknX2q}8tutݵn+~@5Ć |$&WOZ~Qacl?_3c–g%{Àٺ,`ik2_gI6MN.Ͻ"~s ښd:5^[(~+Mǖإ윳lέ-q+T xAku.+&?(`O *v$Wl{*`U -zoFF]qs~5mʈ}Ţjћw謇tN(;Hp[:FWՎ-gOwM;{\ + Y+WDMs!&A"ir;}~ e >`]ՉU?SyעJyK㡽 !g^;0wΑb'ѵS+Mw+gg7Pč :U2؁3W&f 7*HEk:w[;]ч;69yMP֮87{8|2.Y2[ 8puI\`k7h UMMղJuAh[՘1_RA+ c }6lzt[ay7ϟx5m*}ҧ*0@Jfawn:]ɜmи CjTG3bt:%k7 B7 W|Ьiwc#Fŵr{z^$ttrL)LMͣv <(RG1 }@0&n#G^Zh8d Ztwh:_> -L#Tx*&\5\`3~?UY" [0kMC]\5eB#~Q\AH9(aPu{N4ϸ~6p#zsa3@qsZZ?V^7QI2jkho͚#Q_/EiYڏ2) i`KWM1ePk|ۮGrޤ׸]ӆ4WΈXC_+a7ffvs'u535*1eeTX\UDgsDm)JZYUKtF$6B! YȬ _qZ/; *Ġ0 '5}wԫlvI5妟["cZvܬ`%9ը(*#iE,evn,.\iZ R\T"@t+dkrXȯ)MXuD̅IqQSN:MX8N^>i댖IB҄muwC&"|'|aK\ Xu4 UB` IDAT0tGYJvCjVǬ,ԣɃrlk['ܟU.Hpɓ'ٳg:U0HBHü ڿ2j?eKl 91׭*iU$/1mXj/Ppa݅3SdKAoU[S~us1X7\F6&l U-/B hR( O<8ML&;:%?;TD|MaISm^Є+;6g R:ֳaH料WШ$7:,y/Dz.G;oe$>OR5 iaZ|M-x!2Ӗ݄J~:-=y袴J u&Sܰ՗mL&%%TaJj*>4JYj>" *H I~ #|߅D+yZ+H[{5q) O-`5ҔEq#U~r"~B^-ϏO: 8a y[)}}wN;qݜ ʽ{]=eW,ǟ[Lڸo!ܻޗI[ l;a%準DjL׹v]ˈ sT3mX0mԹkN#Ԙ@= jȖ*~Dv]}<~NkюVa>*ؐqT \Fm1JvGF>V4ﻰ~_Zr2WEU;n(Fe=ZsA=N+ٻw{"2veWyXڤnlz~pϲw?XN’JS.nrWu}\n}U(q ԚwP}u5[|Bٳ_(h>l!󆷐?võV@<ӷoq1 ĉt _o:}W 9oâ9}&w1_h5!лli@?O]E@F0 zBl}O}!CM_DTLLLLLLT>KT~ke`$_]U? Iܬ Ź3d_b4?K˲e`͜]姆TBLT>&<3>fT?d /sjkr&@7G=״i *I CCl^APŧc=vp$,?fS bdu k3tVaW<:K-n*NG k!P ^HMp Um(4-TL]<:J܈ƸX5tBo^>MX;Kcme! Þú*5;t4WirDMtŃ |]򽏤Dw7-]u^yԞLZT ]Z}9_[Qwwl' *v\;2ӈ;Nv”lH2 3iX:.0βbPU*WPG]o3vn0*ǀ~4i9ıfVZ<rJvզ$~{!,uT4lGj})M:7PsШj ZQaŊ[AmRQV7;cJp9f=~]R@ na0/^|.Do.ٻyxz>Ӥ[;&mhSw40rd_9~[yCTQŢ:hCR\h1= bi qAw1QDUHRWrj(VCZx8ڹ n.oeFaKP~*&9+{djϦ sDJJ\Q߉dNK̶ESnq K4GPb-Rl)P70V棒0(@UribL?!Әm&HmB|1FD0X B~8#!1RHv3+AIRU S]Ҳh'|&@ZKz#t˹R[Ot5S 4|Pr*U:Q/54(~ci7aޯ|* `*sYI3,]c=Kj*ADQ[RU'IQ"s>~3 ]7m[+Wb珕DRn:zHmt%nn{m #InJzvfTޓ"5mXwY{luVݦl>̔ v킡Hy><H,9Qѷ,u\q=|j7j sN$ʪ{Tenlqiͣ/ wKf@8ydÈ)vMnTAVuW%T <6-dn?zF eK=-B۷LWymI@x`_tܫߢ,M R) VcWL^8 ^#F{ly+,4kw=nWmOڊmV2mo6I0jc3W6 ^ylHLx~ߝ':YC]4RH$b:_HeIyOYigb(;]*"κ1#oeg]]gO֎q,(ىg{*scWnJ<]1+,A"M=z}>B!i%14&vH<twx.fE ~+B;_ z}۫G <|]EB27riQ=, 9ff9cV<#lE2$ PM|]QS>=eZ'BU+\Տj?y&?9uιTʫ$I9nSB>OΘ7(ֵvBL??3յ;k;o댓/(Oz~@?|^B^^S{#ZzH"X*\H_\8@T$q?CϺGӇ>d%;|c˼gIbpuu穏B1ّ >(9??ѩCY{ymߵk۷ow1a5jÜ.o[L C0dT-|z'!//ɩ}mZMB b;B E$֑T(km.j0ͺk>>tsNAƫϧ~sh yR$}}Jӣk] >_#ȏu4䍮Cb5ph~ƋP"WmbKLϋ2z!E0 Y74-8xYئ/Y +nU),gRȒ7j՘^80P02<>ԹfxJ +{N6k4ݡc[Ԡz}F  *`5:e*r$NcaOz}ю6,]:@:vn&Ttd'Q*dkjp6iנ02[ߡ+30Oɴy4" RnطB15wrܘ @ioo ZPqܟB50xgΣK&dZe]kd0l=ضefa.Ζ!*um2tg=vB%=fXnƬ⬬6󙚷|6I m{UfəQ?oOX/;%RJ]6BRͬ]Kwb~1uB"inv[bk]}s۸$Yyjr_9J翋$x}&@!h۽Z|2.(/v'}Wyi3uky~76%1 6'u#C77] +5O# Swo}9N0B {廬х64:y`0Oy!Dg]/ &C]^&1nN5jey۫};~ ~?u~r3+u -!{}7(=?I_swŤՏn2_tck<ѣr\;ssQn쬟2SXHI(D`0zƅP4P "[9`~2VS2(&I0<.  !J!DkO^-$Qv9Δt-?#SLI`0/2@N?䅔 %M0A$")R `!:tD8yc:tLXyEoT!7a\&V UNވJuvΪC !3cU*}??pݴTj9iU#A,uOR%X؆6ީ&~,m6Auԙ"I85mCr[jJoCnC"^Wm-w_Gx3:mwp~ B!xA(@X͆ Q UZpl dQ돩wg|mߦqCKsi}#IT9=: $F@%8ܺ 򬺍Xv>A @:mdzqp-*'>KZ<*YhN<^'GHB,xfkaޗQ-zsƙc/{(@ clyV\K Q t z/tVY+%;LЅױ˼8XJYAyoa<#̧J)鵶UVUuvpsz>R$W s<\lߎdz,’UhD-_Sm9b RԪW\0jV: H@ tT VQο 淠)X6;e0WVeL$AeT*R#YݠlK%mPig{S|is| *#j=Yq'xna^:83vGtI=b[WK 풃9lvqwښ1lGA{m1=5W ֯jTlέ_*o \Jzཧh|?uk~ \mxJ[§sl{X,"Yѕ;_ij7G}^evz KmH%to4|i[)*Ivv^ѾFu[UuVtѡQ>/jQXcXeUNk;v_zr)q TݧF$_A͗E(0DAWo9^s* *UWT\-FjV~/@%I @ᩚ*RZFB%S|1̯lj! & `/INP)TMƇ o}3z6ᐉ*Lhݡ|dHIfjjݤkYG P&||N,5 v>*йTG3bt:%MNulTl1Sz72{а4@US&9 481ɵٽ:68.kD!v*j7ع™t^ whYb*>3!:w2ٯVKZ[PUXSYs")njS7jk*'x ZuT\JG~,BH AD {!`~&r4"I` 'Y aC0%WPM>QE-WK*ˁ6ֳЩaY#P)/H҄+~_gf:TenX%۰M-*:XqjinB WV]?S*Qs*閔P{V)\bRIM DZKV\$h~S˩5*PP*NʍG` (q*H@"4P]ˤa0:d A*F@l\W2Y.GsjKGZ 4,b6eeO Uy  Rr-Ԋ4_,\Lxs̳,a1Q,8cm>ġ!$Jf\ѧb*ΎY2GpI]>ҜOBTpPQagW,2 ?pŭɣZP7P߫+RI7CC[jj-YIs&P1֐~"A[u˲νﯸrT9aF[QIhA0,bl&fl&(16xNϯa3%fA:e2 䆦)u5I2sr&-:|הķa+}/n.oeFaK`Xp紀+qyB1)w4O 2rx4"f{9_b玙UZ3HT<$R,5 U6!N4NP FUUUbZp#`O9ɷafE-ٍھr\ _X*1 jm TҭmX7kW]ژYSP(V]k$TטKIzg"@ſBq"HEAd1`0roǥuAܖ!`r51~6f8|e9~s.0bݬǴu)Ar JM<6-dn?zFpR/ }Cv^S}x*X9K'  7Q腳z`5v%_~[8!?.V߄YFRWB#"! @@͋a('JBDA&{a0z%G/"s>Jj0o|DDA ?G2? _v>0ϥb! }  /2N`0 ?mJ0`0L9`0 ׀ `0`0 竜j=0L1 `0 k^Icg{9P IDAT:pwY D6LՊdz:'I> ҄.YcضwPɡ|ܣ_㘭xv"%4iw'[sqEG6a Pyſ`? `0?![FwTVFS\ӦEJP& A&l}ui͕8BۊE͕c!»K=ޔllݣos5]J3>BZI ۊ6Hs> % ]%s,{ľ8)A1U$&@>*q] DJb0A AEN^}!ysAjչk.Z0s?YI jH6ݻ2` ;A nl);;5'6#:hHKMCM ']HRCEڝݬSv;89?Kl1CJ i6Ab"/ FB0!b'hΡ~A8Tg/FZw^K`Cx`_tܫ_aKMkk3ɍYm{NyOJ kHR/s~PˮO к=„A'Gx~t|`ﶸGFeµ&R&ŠОƩ1o'w p㍊ͩ}9tcKòYY Ӓ=[+ >^߻*IɈzgZqC`lZ?;wԻ\#AU8ϟӐQ%`05ÏsG\{#A.j)S\Z*KZ?w^Z2޼xv'QXrlܦwVSjg{#Hyn[hlc;lҫtNϥƵ `~u Ovmi,8 NcL*Pѫ œG;K^BŴZunndRJ])ޡݔ*U&6.`0酨vֲ#{slڽ OQRthDqL5*=y{2 i6xҴğs{Ss熥(Vc016Z ;QsdZ'o y7JZuЁя^>:}*lPm޳2߼ k!JBn ;-t @=t`0 S nSyZGxm;ñISGne7>q\{]Ҵg;:~&iY;>U1`0oD̑>VLt춵} , `=Vp >މ|E4e] ` ^4c7ƼQm`0ߨG`00:`0 F `0 k^`0_B0 `0 ׀ `0Z/D$%$$|TZO"0L]d2MLLlBBwtuu\.W!`DUU4 Y۷?5D"QVVVfff-~9\PfTKB544L&A!pݻ׫%%%cccPRRfr3NU$ h1nTgb0+ )((022b {!L54t\8q)=N|i|PYӲɹ١F[55;(-~lHZn#c ip`фnƭY߫ifL<obqfzKqʍwyQDs6sN&iBȨgbSsUC|7iNcP}ؑ+=~:iZ)LY^zs4%g"*9tۭwtQ?ETvA0G"|߹_ v\5$ƭ"H׾n? ѻ}^>^~-"COgm3 ^],j1Ĩ)I kt*A`3HJMZ$xG֌Kv3slc^eqlSqYaʪ{XLd3׭s,.c3۳z-Y{ pR׭r> lYS5%︚**qxeS?&x-ei96؜#k/2M4[_܎e50_O*y 0A|XQ% S"SngEKTy6U(~¡ͮvJKu\9̪!mF/_:5d4}#_Szm{g.6)Vf;r/BwissƙB0<e~v-=|}nGC=񗷮yI2P5l_ *tc\ ~y&ޥG!]KSjOx8?v mg|ݛO=NN ,q2f!Qe$e~i41iOпR((rȔ3BSY䆮*Wm Wn4}n&dbȘiF#xyl#r&Zb D|iΆZ;.x.a$EK0?dJhʞY9ˀJL]婠 &GccKRvTb6jn\ u; nw-g4ڻ!Ί^"hʞ 1;^}o5v02C1/~-pخ+_^9{\+OʼnADI EQƞ/G{^(j  G :va14$K{'F9Y tZd׍hTѱ*zT̕ W,.0 ^ah*[$'?H#nl`!uDE_T(^̇18$ip&h?@s?4H:FsSߦ>Bc&57rB$`d^_C}D/(曇Z,u24~E@JT껔0t}G4\l7J';5?fUKv3|쾿ȽǹkGO>ͦk#k66u^%7#B76v#qfiBUCt"7&g w0bavdafWMJ ߟ9a,'Kmuvn#M48-njp4tdmVKY8[@)XlX|^,Ս;s3HǟF_MmSΜЄTvCl1Sz7ⲵKj`0!H2<?Ġ`q L3J, ْN g\{D4B  ,IDED4#P błCb9 *vO{C"HH, ;:tyݘݝ04|jŧ3Hw\$J gHZ2ȏ8.S㾕,,++H?V0(2 #`2T@¾ BKX{]d(J:.@f.(~}3ziKB.ev CP+lWErF{]F&0=qI*Fu5E6RNɖ$ 2m Sӗ/ w?iΑtdJrΫiܢg'ׇDW D$=o:\dާz.䉩LEy&O:q ;9Ϥ- (K4dGOB)( !XvmhÙfQB (5QQO0"}=pg1!MӖF!4!K<$Cn3~& L;~UUUO7=HjɃ>9g >oǬt糁KlY?sSF㺦֎5bڴa] ='nίNgُOOܧU]aA믬<4}[5 UHJ=}? jVBaR*dHrD0+yHBvꪻZCmilh QAbڴxZ gӖF!@V6%}} F**.mc,}좝oؾAnzv8/=fJ^/Oٻhs`g·9>~ lUFxе 22ŎKwPbo6ӓDi!*6l9?xM$G8MCMYU 8 D<=#kbxUUXKLЙb#7nnܾa.nB[;ǵ9Yu4ﮯ/cQR;MW/.r'ܸUkϺ9pZ,5{6uxwu9wûV=)2 ʉ@_WulJ >s "Kxe 4sPvʄe鼼&ٳ~lZ?h2>-m<2wjzKUFWו̯T"*~٩7y= /]S+(+%] #}*0yXՙ+̴XӍ֍4ԌUM .%@}AM HKjcwdvO=y>@V5s{%ܲ{ǎV=+i$rqy\cJ YpjeSmXBʨʁ+=}FHx2'ܓ)⋩r *_S>AAڮ69  ):˂d5+@'z?/ߠSh/7)dMkW}'_qA‘2wz,ؕػ,`Vd..եi ĮN﫤C?yo efc2w,EAљg IV+..J p25s.2zlȥ=V=9vl<=}pKrLplPJwp~R>q8H8r`8yVYkZ8Y#,Y,_3bB'9[Z{)ݮU= E9~iO뽝l{Y!Z;(8gWzڹx#aqFWۯ1S/ Fw{Ё,3}IbGj0l$Q_0'ڼkB>k񽫽L[[\c$E7V=cO͸?Fm˗#1.+tp~S*qE#.%KJq t0q^̲q Dj]_p#t[ߛycKsG%ο}KWx^]=pJ IDATc?}Wxb"Jw_qr:3\;8S-f#62 -[HZ|>9"FLxܪ)U!✔CLCy­Q/*7{k?@c=>Sk$m?~C^; ;{_<.* Z}-W{~s`:t<:DF~0"*uYd9?k%vSQxx d|q*pO&+$ʼ5"LIO_bZ?ӴrFq%zD#Mr:e4m/+"2t 9e"*Pܤd2$0Fc|}QiF\{]PJ1;q!yE㼜?4 AeHo?yԪM\0|S < +E 9 FyrA LMi^%?{:?"nA:[l.G Cc&!aL-FEjn%ޓ ? L %񄓇\5I d3UTi7xx颀J j.CXQR}XBVԕgfqr/(H-uD~#7̓Fz ]^i)ѓEr! -TtGN\PouJ HIE}Uirpk}2Y?f#aD͑AǣRѸ'2i ?XT yCqq}Nqǚ)mT} 8ܹjZ)>Q$i*Oɚ=m:󢠢$ʎOFO\{]^# f8bs^* gvqvYɧ'9.3g_Kcb)_}eO%fpEEܯ>G SM4ѱM2k_ZH*D!*.#QL^6bV J8쾙V\qo<;Z4=hE6FPTVEe5˵&fk#>>>>>(**RPPhI*V~7s8IUQvɨ$F1f/>;)G'LȬaJ_D?{8 uK*HaU2#nX Z.{MMMUUf=Bݽ{4fbkkkjj|a‚B###EWo{PKd߿k5Ȁ𞇹>Gj;+FQh͍c3šZ022 g# +˰m;=z|adEEŖ yU{#ҧWy&+ ђ_vAꕽi!4TN:5S Z'usC9FT6-D4T<|S,wdAAV-CAuQ  BAih  H@AAZ  :(A6Mw͂Dy8Ky8=eU[4Dyfn<翈A~9虺id6l5][=k!Qq.!Ϯsyw5{ŴJwp~R>qn8A ο}Ke<-vGg>3&$ [gÍO뽝l{YQp']{1e0ؐKNu!q~ƕ\e̬FD=)HST raU%(A#ɋ?hzG# y:Ÿ%\1Qw]g;vQU1RIz䧆 r0*@.ev:9sv!\6jM*5ZY9.?yܡ8zܼy9*T!&BPJNzG4hnJ&VNTQ_IymkWE|uf3~Gr!O BiA0p& $\-QFEjn%ޓ ? p>Uz f%^(R@X>F5٩Y\ ԞQRyKfj͠'4C"Zc oR'>ͫAঢ়Oc!aL8m -2pYND؉e%Ο4H:ӈ:Ow캖.I:ziS3g_KcbȔ_JXJ"F)]F SM4ѱM2R 17~\S4Lt,* u ]_y5[st q$/oyW^{J-$(AV@R _Z8/Vg&1?g \gnwz#*Q!+s̿0  לdܺK.tPo1:HrmhRtz='# o4(?E kOkn"xYq^9uLw>ֱFxۋ\yIiʱ^u4wm/+Rԋ WӋNbӷTqX/+?D-CLsVPT zٍUvhRq*.&dp}&S%7^|.woCC!A7l,q۸2P9vDW 2&mHo:pg/{g=@ rG/>EB/K-t]A|EѲKnbF$|q?*Aunl6q=eT )}{/V#J:.ۯ?>eS/GǿVo'5> qSg.`jA+,K9*@/+hrT_x^шC;UQKIkjaߝ O7|ж46BW#\ _ѤEi„3>r' k*|: vՂLh*羿x5Ϙ1s05`߽1" ȯ)|Ƨ I\S:1ؙ09G;![Sed˨(Qfnu*ui­%yMSioB}QR@rv6׮"q?֞IUm,pnGujRc>pYI]4~q>|[D# 9I@ߎod,֌ϭbW,x6 N/mVy͟:GzYPػUYYSɗz0f!d$_53sߜX3C5l $2S P6_hj@Ź dU 2N:_1-_SIswA,q~8hټyL; ekg?M~qOM?[zA$Jiw/$_a7]RʺPLFQhdwwֱ1V0[6?ת91t_,.6w{bskUXT\}Q#gd Q}p9 soGSALs ܻ9c84ܕ 7d (tJY縙sG rq&UP;?!kh=#xrwFv]!ƅ|`r]hoO W({r1رcǎZ7ĘKtܗulq:W$ub[UХAwN%ygs;DAi&h>dMԄW~):ۛ Ҍ(&Q?G&L@AiN  =AAցF! 4 A%WY,MȓJ ;)| 2z5 eU7a#OJ%R.L?b|Ҝ X,3a^Yyfn<)=[_vqv8YT4Z4$4C w5w\t!CĊE.36_͔Z0mhR &͡)m~lI-ff" p΋}smq#BC? ReGoOn)~B}=j;;|k9uS=5 (#ǵ6\-XgeCM.w:aeAՍN"LY}E i5W gD><;;cx9/?ݗ$$WgX.=flUDx)IKٛX,3W] .w.;YBÝY3NeRTjw2 jNSWWh>q{J$wz9[X^LqUQyn6f,sˢ_IGg>3&$Eq~FL$g V_k3 P ):[xGUA?c9B~JE^IIYYYEMjoZjGϸq,耨'eserJO;va>̬Gvvwd8;ze|\Z ɭVx QDEhn5’b;O /Qgih H~7L ;a-CO߼{ʺRY"|U}nQyEqǃ,svlIOؽ}θ}CX Sko9)uA[ۣ^T78?AGQY:?$]y_ ¬yƣ;FHiΧ:k@Rtc3܌;:c渚#I^|lR⼘e n]պ<CNqqAZ)qݖFfZV>NV߻@G⨈67b"cVHmh0om\sݑG$5SW(.W+\L(.Aȩ P4M8{ќ,yF)͛nݺQ(=]w>V~o7 + fe̷w~*er3%߻AĆㆴ#FU`#QfK\~ѹCqyMrTmѼuuHP0 =+A: >߼)Lh2sq\伵UVz>y-T3Ӥ*ߍ;u`Q{ʲfo:ߛkA F& I\ZcKLd0J7ǩ~)|Ui(1'Q,]p=d&_;v}wyL S=ڜٗdCR!DpPs4rKТ ։l] .Yq̗o8(T3ˉ;8SI}=JrΫiܢg'ׇDW} *?]Z# IDAT"G^+,>lY|_jYL^6bVh)$.䉩LEy&O:q;IdMkY6yQPQze'Z#\f@=a)O4ۭD)D9V$o9+Z'@ʉ6hںf8Nd2 ?bPRaD/v]={oy%˨7 "` Fbtri b*ΝK­2_K]HgU?!`Xw)<oOyCsWߟE r=k^ؙbl+8x jMQO<3iծçEwmŇ,y`O.f5,-qQzCjQĈDu?</ofmyyM޳gϷٳkr4-L;fA"rRqv o|kt_׫}mfOK÷QO&ih^rC~wU8;zլ*Bmhd!ndiT bizwQSԷ? NeB&5N`gPY +Q"-9ً<~mUv^r{]oǓԖIvϽܽG-ۡ((V-XDf KUU+W_!Qi۩,!)_|#Ka܄#k[v_; p.2zlȥL8Y#,Y,b6?Ϙd.+ bYzR&R Ds1c;_L ;قbZ%Nh=dytme}>&2gTH .J p25s]?:E_ZtDq6i 0.}O =='TT^ {T#$ψ lkb{Md(j=$fW%fsjrL]'v|‹;Ǝ& Nڱ1 lUKf @Gi@ Aj\xmit\~o-Ϲ a#boDέڑ¥Oؽ}θ}CX Iэիnhs3DQ%U- <q9xe-)\2 -[VzÖK!vծJ$J⼘e n]պ?iΑtaaҗ7c+oȚ=m:󢠢$ʎOF2T38"ժ0NRHtǮki#;TǪ@Ɵ\=k{ 42h~5j4mkkiF24-3Szey*V~s5옙7TE5ܴ.7bKʾfI&8܋~ih!!4d] r3_DP71ְ5<ܡVS#rlWo܉Fr2WkG"&FIj|7 M+,ҶLu_f K4G{KX+jK{F))֟\-?_,GPfA~D"Hum`kkkjjra!-B}z {:|h֎WеA_iTp_+$nZ쁫<[_' TA:y21i E;2[@wdi a z d -Y0L6.zЩSgAYF!߿H$ Aڂ rr ##cUUU A\ >YQQ!OD_LVTTե~Uik  R?^  Bi)~[[\xw Ҟ OD9U`fJAw͂ˮKRu嬛llKk~VP=j?M}kcY_W^yfn<4/4)o߅-?1~F@!Dy7%Yypav 8I 7, Zn|pR{jg/݄Ufs#Kk0^"?z›Gőzk!8AS u";قbZ%}~ţOc,^ȯZ+))++YMOKMW6Nsܗ2Ln8A#\B.]i]k'uK sΛSrm{gGf>3&$KGk1-X}=W\fpާYf Hp7d]y,<]c&qQRT]n~HI8;zs x{|s,l.2zlȥLnZ>%l1>fc2w,EAI7Gi-Bߜ3n+{ˆ 0]1=T< : mAb 8g䀉 9..1u b|c^籋>J-K7MT 4yJʧo~œ@J >45GQAv+q kV|}D+.-b<:0h&TyLFV\yցK=W̋~WV.fQ>kAaeɋCL[kL3 ||l_.|=;kah=p.v4II2*ں5%7 T3ˉ;8SIaޫSl3 vT< 'J`󸂆ٸRJYy0դ1o Jt,SI/% yCqq}Nqǚ)}i3kߦ3/ *JRDk!m\ v_Z0Ĥ흺U/ ]_y5[st@! ]RfwL(AARZzhɞ8Mp6CrRjI^z/mb7~:EXt/>SԻx!~3{,V x{v_bݿLaG#|(ct2Eg>:>J缻\r6[~dZW}u@75mVOY3 w2\vl׿'Ëoisٻϙi*E+IDKBZt5KKג]vn$dToڛi#2r~c̜Ӝgsvf9y&Wm]%1/xo.[*_Moϯ#7ra%#BMNQB].Pz7\ड़tnBOQmSLஇtgߙ]=gHY[٥!/9J8zǐavٺJ6u[.?2|H<-a$Ac;JZ"QRk翾RU[(NK+G}#=ܳ,܆zLzs'L)EM&UBl*.]f߿taN:vNJͰM8 ;X;AzףQlbN@ s723 ?M`%sMIcRpvT>صT(I&mp4!P+|,F@ҙt E$h5e.ɀq3IyGG kp,MX_ mBŎçwjY3Ns*Dܨt!3o;x&j *@(+P(gl4< @Π0,gIg\"5DJ %wP 0tym?J^Z@#iF!Z{4$S{f+~|wk/_~:kR6/>'4@yaV㑅:0_O#yߧ ԺhP% o6b5w*[_{kQ5K h7җEkb#*;!Ji Z<*_n9hHYBHYe!dիS[P`7+PѐTʔyb~zsyRyCv"u8귡Fih<Nl5/h5UH[cwg6P̸UKd}a'ط [ ae5s4o.jmBU\BCVCT=ץKv'lcۉ+¤upyQaT~A*:ifJ?Ov*UwոgWbs5zZdRGU];T=<ܽikkINtmryoي[x~EŝYzÆmRb׏*۷avk-?JPjw jBԕAi yG=BTpuټcY},Ț"xy^(TXTsO:}uKB5 ʀݽxKY@1c”uX͠EQ IDATuр{0ZFG/5bXS [};AaOsZ'kN W *{KؿygXC'{\,v¬^ ح=k߃Y~`㒇fBb-rs,S̺|h:QnU/!y!iW̓gxW,ޮI.?N۬undRîd2ZU>U! K+v ֟t$]ݱzکF?jsw3u#X,'g;pGW!Ak༾p{8# ڲXs@bFzϯx~oe1{E"U|lAkJ>_/ S8$H-fܨ:u^,ր?VLk/}qgշ'reYnKbv /gkV[ ;{y:fr=GW}&H%[bYۏ=BXoh뚁Ql Z!d]Q{ n/S9"܇e0b> ߆ >a,~s=,<:f<1?-xm'H?a3bH^浀 n^^^V0븷Kk=wpܦ?1BBYt.4]g͙hE@꥛ T{02V*ĥ";'n]VINZ9qWl:] uanq&naE i2ݝ)눗b0td9M͛7۫: ŷL_sIU ޶*o=gtѳ=m{Cɋ3k$rG'/i$rjz:J45+gWϣԏ<1Pu][ .qsё,7ݙȩmO/GuJSnO߻ksz'QFZt~mweyLdDw>9HN’I[y&ZԅuŇ(\h5; 7U ;2Hz;%۝ 78kzPATpmSGPF=G+B#"B(GmtM]̘ R\2bTҤ{,- N<3|#>*wzY-@qAĎk7QKhƟf!,r& t][͢A| Ty*AܪIhGS0w[߭_$XVKFRA04TTɢA\λD.9-@Q>~Ϟ8@h[@Ҟ+߷?y9 ѐ-v2-񳙠2df %*Hzi!aۇԿn:b^:z@W&eqGb-f0kNx?MN M8g`sM ۩I #N1TV 8Rp >euG+*:r0QSmk3Q*ėl^¸i\dݺ^lGc)ǖ)`> DO'L]2J[Y.H4WSEy 'Ƕla= t#ފbR曏wyV]&&Kǡ. n6a &]#Vjkv'G'BBߍ\Qk *@n?|Z*ua~'6:3tRFntBmcR4.LoE z@Xǜ QMQݻn?m8BwRg-cMxeeT#+:P̬t".@SJ@EUpmݵB?AUDRN3G_geNNs3OZs4S>-/W2 5o H33Afwu1fhTA@=2p 2Ѯ $-i#_I̊CY7J.! m[|TnzȂ^*UWLjNL}%vnԍ5h"ڇUu+S7rC\;^ڀ8ǐ 9M#uZMy z3M9A50 A}/;gPH[q)yewEjaN+,{=*M#+]6L/3/6frPZy_}qgA!q2Uݮݽ_1f P˷3^0uRZ Cm2#osZ:Rw[V ]bYa>S|՘ǟ9C1;+5_ޘKOD癜$(!ƌ-=qIŧuoˤp.n=rӁ[wNd H3:/xWf5j)2;gX^EOߞϼr]+^DUQ?((RHN~FLSP}h\v IwYaȴ`D)hiS҂ZxF!sο h oҨ /uԀe`1myN߾zL&na™g[njEa1lrBT$]DDtiÄ7rtX$xh::˒wѣGOxǡ:OW#slؽ}U`5c]Jn|݋7,OZ)LKKy4ufaqa֫ȐlV*~yV~;x¼M)={nql A,ռ?IW$D''=(r$E/+ݣ,hIYL̎پ7:$ށ Y7j%~ֵ얳TEV%[sJ/7q8}p녗EU7vx<4tԯť]ٳ;K?ubZP` jia>7.8;~>݇vBe :;2V }t'/Ǔc;S&Kt_{U?LeZRjٷٯ#T ұ\*]GWh_gTkݻw^ARѓwJfYqf׹<7 GqmY;(S9'; ^pU}ږݘt]람k>W>{ E~ :X|Eal[ś&ȥ Rjz,%cD u&p}uՏPރZoDWFV9{='5d]kd\8@~[0ejpӺKH'J@W,z؂Vީ</++f x7B?:b``@87T$qoCIsjZ ro߾ԤRP(z捅|`2A~p$(ƁΘMVV*Fk8 !!I`@NNrw^ml7OqA>\53Cֱf|32O>577g0T*~|$ID"6aee%pBdv*It:S~APT@ XB! 0A'E!l`B!,!BYB!dAuqAF:.yi~/I܃yj-w:]EcecIT%"›$"~26.fo5?S/ W17o޺}᳔|v8%*{u'MUi96#}k$/ƖzX#}=;n ޟy̲c˥YKTr3i|~|5fи/+kg=Ra)Q :vòv1{gtQnoCV_ q0jNGGی?'g"Y}:IM]̘ R\2bTҤ{݅7$ 9ywǓ؍g○E]XX|xɚʱ 82qܝiߚS kkw'rv-pJ -B,Ȑ9UjX Jꚪrq^ |~7U.UIH.'I6W z/JM.FCF_,PtꌇrKAYs7K}&EKy.ZHL,I)B68B)´?۩ 6S{^v*|7W6(OqkQk$FYiSAu K $c" ,î{V IŻo>޽FlZ5*&Kq6%ܔKWK{/_fFqnUw26S>̣1Jғpԅݭ$>4qs(t9c~~Caf%\eqGґhEՒ!VaԽ_#MZVjL>XFT|HJ|JѶ+'^{N*TQ$77YC;0YS}Q[1We%&I5Xeemg1E"(¬[;.& *O4uSO_tNmȸJ7E n*,˪d0ktvC DkiDT_I3QO%&J !ȩk>h5S7rC\;^ڀ8G)/C jO 6צ7>f4tkBWU<D8'eL3[#5 }sؔ*c#L_F3PHrml5TV IύA?A\zi'2)Km@X% TiT/!)rE49 AWb|Aeȓj+0>Q19HN'{$9 ۅD "9eU%:/?yA9'@̉ s+$g6c%03yڳfz8̦iyu :cdzeQUi;n&l3..Ξ5gy^KE$d?0+~z2~y#jo ,ɭ'}otzIIq~cjmxinE釸#';!lC*͞t+@}>|"}XE_0h[$.{~'dk+s !29m]E ɯ~s+y ǠI;#41-TRr5 8/HzA1fϦwKيCѰ_:CAN#_Q5VxzD.:<=w6Ӷנ73ں9?xWէmY؍)uO5+\B~[0ejpӺ۷VF'J@W,Ru/dto`V,;Lgwd ξP?Ft|Pk XG4qokڱ ke]93:k+w0$}5o{t!^ H_~䉕\㿦Dju4VU+߽cцR"Fۮk/g1v⼾][S;uf'KW<{YnЧ)-idu惸vV§j=kxVoN|ܮSEm ))gϞ߳Rvܢaۻb"̛d{*e='+Z1W ( wJm̺[j kjP_NJz)0{Nahգ2vϟdP䕵uhݳ6=Z!B_S5ul'3̲{ ]V5M9~ZP^ӬYm?`ddbҽI2&5mM!Bߗ̳hRE!lc!4!B BH60 A!l`B!&YAb1ި oB&Yryy++B!IDEEEց CIb``WPP  R}}}Yǂ&ϑUUU%_!$ST*UUUNoBVYB!PBf!! BB!$ BH60 A}' 7u*WqgUfbXÂ8$Y1b Xe 2{X,[QXu4ngeRoY}7b^ڟbYwq׸JR~`㒇qE6vs׎=('5Z(7~ݪXjcfr\ǁYBIwjwo U! 刯RVAC:4+~sѦ{ܥCN޺¾%g2FQ |@}7*֫{UNG~?E̘ df\#GVx9[L] qP X,eT7F@iCq*ņbY;z^Hzi .{~j7;cy3ө7}AT,vn]gٰlܦmzvK7r |q;h+N,Իo~cXSnJ m5kn;,cgqY| : c{^.1;b$7[ WҫI~|5fи/+6I]Z874֒K?Y瓚 QvS b$/ƖzX#}=h cwՎbٸM) sOO{_sǃ6I(7!+ï`r5؝3zqp6tODݪ~d IDAT0͖+Y|IUCytsyb~ZE~f510 AɌֽٞܶř{9PbmovOpˉ޷hUG f۾غh׭Teѓr_%4Q(zwkWRo]|z|rOϬ_!အ=N6 k?JZ;xnM=ViۑQ=GG+.sΎ4Ϋ=>1 qGPVJ홚gm߸zǗn4  Aeӵ895=m }%Z cⳫWQG~AUG :0pwD6ݕcp.:2teƹ;9˗:%zĩvy{}>ވ{x_:@>]5# :ѪwDG[7k?-~R kkw'r5B[9;O8#DE,VFDLQھGyfɊHهoFum'UA& 8}눏]^VKm>n_61e|>ľ"ťI Y?!&Ba~?Kf7<(5-\ܱ @$]ǘN>R5 UiնZsۨu̻++jKkG٢mkR*Lv_%; *CVbyI㏇KX`hf8bŌWmtyK jjW 8Y1eq:~/̐{?sĠ|w߭){ArП焗K7Nl;VTgQ("n@܍JK脙s@{d٥=^rmb;L (*,_{ׅ,}FvӠi;S;l.>ԣ1JғpdwДs66פ@yh/᚛iܮso]/G1P 8g@pJ9)49c~~Ca&Q-bB̪Y&r7I4WSEX89=p}\fy\4(g҂AfeJt&d¤AG;;8"ɑI^ȮbXLju/|e7bӪYU1Y:}uiw[`2@PRDʽms_hu͇[mSg #7:hsZZu ʳԩuҊ@\Zf]]3CT>No?v6VRov<@Q(Z rrmmbI7}:(m/VAi@Wok8J"5Drr%wP1W5FRKk 3$G<}F٩hꦺb麝ڐq99YS"fS^̯d0kޢ 53K-#g1bovK"̺bBXl8DzžUgNX0d9۰4CAۘi6zдS%²JE#akӔkyBij -.S7rC\;^ڀ8x@6f!,-!_9'!&sW7ma֫ޛGWm^΁17mOSU(N+(R*q1еu[Hn\Bɫfǁ*oDR]Vt42-;#{aʀVD!+?I#Yg 66#P1fPi4ןrk'o_P&_4A=h{m;,X/,"Ѷ8w(uw/sCX/c֊A* Z^{k  2^~ςK%Z]Jlڮ{ZB־~k0FB'0Z}ZyLKKy4ufaqa֫ȐlV*9A{nd?,fl3[oOs+J?Yij7O*BE .tR"I[P4 K|)EUJ">=|{ZNdqmrjWIsjZݥ*?~d}&}豘 6ۮc! |ܳ{NQWН<:6`??p>J,椇ɵr2c0n$S&` a9Ħ[_3:׳-~IKQ"h&wn|Fo?i|(}'m*8mCBA!l`B!,!BYB!dBf!! BB ;nc og]C?dvF:.y؂u|/I܃yzϹ[o@6[^.fPʑ;f ;VC._p[Dr?o2Ėe8ze $7ff8ГRDnߴu_j BBi6.cӒD?=s_Jٻ%r֍S(w{#ˌk,! b Dzv=_"1;b$7[ WҫIcЪ'5##]V<怨0vל_X,šJ*~4ŭiD%m>QnoCV_ q0j5K6IqbK|F'pe2}@/~gbi ?>ڿњe3hЗ5emH^浀 n^^^8h>,kwFCݖ Y6ڹOwMfbٍ 0&th K~;f#{p)jL5SiSR/)X^IĀ%)Iܗ BB7L׵ /YQX`]9v#CY&n3qA?QׂN%U€{FsGD$v"Y} g_/)!$9{w>xΟG[7k?-P,(QKQ|=j9[נ-$;hRπѷue%H7OӍ\{=P%F%5˜U8ߢ4qQ왗z#[ta~!]24ph&BuH-%۝ 78kzH}l*3MWӚ' z _ithGOv+-qo8)r^b@Ҿ$'' ,5ն6Ṣ([KOEa2gfgPhrAy~~X׎sK0@\׸/.]-\CEqM=yLs5Uװprl+ȖzBP 8w )$M8g`sM ۩In]M9ȄIWhg/( nN2dFy] K4Ja9aK֡__f)^4pD- QۏܖF3p4Ϲ|8YU1& w-Qo)C[󦀡 D+yPe|&/+6x}ma;y/qS$ړ %8|PBi@M+*f2(Wcl5ˊ+:sĂ!ن='M4X(m\TyZ!SM'nQH4ђBCnP34V )E ˲*F̚rvC %aGrF괚(*+z&j57QbV0 UbOWAP"JHF}lE]]&LMM [2[iD-B_Tx%u\aLπ%-aTDoom /HqazK->*7=dA/v),  r1(l:;b6ã(0*#|"i(JɩEv{[PZO6ڗBBߏ8-G ~ۏ3G=t( fg7O'g Hvj1!9X[m|ivt8S995}~hn%ŒBZfYgՋO?aߖI<\2l:Lj^{zc$QGD*:JG"E*Ϩb)q UrrK®!̽<%3MHA@Lc[4ng+a,9Y⒨e]z=ӢXZ]v!z?UwNx7]EɨǗT~ls34~'G*Y:*h'%S4%,';A}[32G̎پ7:$ށ 1 <4Cܑ25o=d󊟞 _! +͞t+@QѯqQ4C5]{[T֓Yo*)jHW$D''=(r&nƯ칙^)N:)9MR͵0K^Uؽͅ*QKFWwR*񢪷o̺WQx[ʵbڄŖN;zRuE+Q1r6O*"Ii0!-UeV1 )1%?Wvm߅{k=B˩iF뢏nn6!4FIE{;,!(Y#:>g5qsw,Bp ĸHǁ^gXR UwIFVz8 aZldxNN (Sf2>0z0Co2!]׺'𕏹Kgu]8i+ f O/ޜ(iΟ />mnLɭk6,[܂!.Sm^P%ɥq/_nG_y!nP34V ?ԍ5hUW7d|N6צn`@s3OZ;LclDQiK+&HKOT_I3QI;fԍ6`76ǧ-Naѫ~Ը2^!.^m%ö~;RU S99%G?%+9ZRUY[y:R卙4 (Gy)K.%MTPB=T%r"gT1͔iL*oDR]Vt42-;#{aq:-v*<#NHW$D''=(r'0:-ۢ''5 LMF[3ټ'K)(F?oOs+J?YØ.^xYTUzcg ..Ξ5gy^KE$K3WwR*q*0 A} :.d4bQmoN|)lNqul=a$T'[0'D8fSB)ҋh8Mb\}@/ڳv,e)&5]NMK!Cos ػL sZ}b{:E Qs%hvTAJz%w;?jBqI! A!l`B!,!BYB!dBf!! BB!$ BH60 A!l`B!,!BYB!dBf!! BB!$ BH60 A!l`B!,!BYB!dBf!! BB!$ BH60 A!l`B!,!BYB!d+B!_D$)B!_gdB!$ BH60 A!l`B!,!BYB!dBf!! BB!$ BH60 A!l`B!,!BYB!d&B$IH(ciP*FD&/brqW,Z!B1׍4TnZIC!E@uM!Fk:yVxN(㬅X,"0tkmC!ZB$:f$)^fdYw)8r8Z0:!g{  O[7fγ>(}LMژ蛪 B`'kh$-QuT[8uǦuWRmwO(]bwȥ%! D5yo+s?D>*3EYu~/l- 8;fuI1>i;~- Lis;6檠*D{uִWn}(#S Fi{ڎ7[3RU$买'9=ą|.GQ .mp0$i?-G5I̪wD͗F'kJ9mw:>8OHAIQ1<檷]BP_)%N?%/&$uD^b\"'RTs*cC${4̚oTfO$vK n>Uj;55$z/JDޠTYo Q^Ib+~G9n]B`Pb8]t٬!"؅Ztc ne/?&Bu7Un 2 )]Nt^9\^c3g%ǯoW'ޫkWolݍmPDĨYvhڲLkOoI8<㞂#FG&Ü_}yx 0?:?a8DjqhX!"bUAc!0(Ώs pΟ2XpʡIJ_$;s9lZ2ϯ{qn00?b^{a /Ἵ@[ 9+u)JM@V}q&qrٴeiJ{k5ݪۭIkL>m,OXo@^5m 040~)#7',*ેz}\==='W7cF@ y 0ysBITOtEXtSoftwareShutterc IDATx{\W?on 4P "*VEHmZj+ nF!2EHb e0X,V|,p4 .unˍ7<H$444l޼S$?~xɓ'ۺf)p8%K888?xԩ痕]tboa=E=h>E0 ye;bP'473z~ǭU9ԸgRϷ=/$]GSD->a.1b,.B藒n˭Mܐ+ۃw.bَwlu`׳l07g ,d-@hY\.pBtR~ZMƗҲ?OHHk׮)ʁ޻w̙3b SSSi:t[vjnn>`8rHKKː!CܹSPP`kk;|p:ưLnjTVhkkW8V'257tekUxTli:~^Y63/tu`n+ѥ\\"t]袨{9'_YƊ=~RtٖEG'jtakzY`wʞrg`7!Gm-<1:-Oc\"gd_>Y;+F <>CJ{K]Pel,Q}/xAIхa?]i5\6flGpBXVbl`ۛV4W֭[G1Dq:։,6KkBN0DM*k݁Y<Ѱw_ ͫn}Y"% +jđk(k 7/=veȳW$xFnr0;-aÊB oJ]蝔{,'qC\wo!OG:EƻKIZ^$t ..a'_u:Vk=mH2[ ciMGa?6_u$i׮]WWW>'O.,,tR{t!b ü+VVVuuu2L* BC'aÆxׯX,0`0Æ ;~xSSU*X\n떰ٌ!q,vkj$mmebXGf`;%1WV+\-}! ]teVRO,vnHȖbEpK2v)XX + |H J*+)%t9jc[bSih/4l@SY(S0_)tbBZA%[`X %Ro]yExe`s6,=%rph[lBV @Hܗls"mQ%4)qnBĸmqEe%n](xBaDђʜ+5r+2n*LVa{aKIFz%\+VqI\lIH.H{k)J KD:u!UE'H\l8?e )DZnB@FM$'RWGhȢYW*+O5n' (/nY[RDʍ2b3JC"Zq'O]B)wTR %iĸ9 @:Fq ) B,qmؾ= iku1R!6/P5r-v'+b !E&.JI)q+1v7^M|eEO7wν~ 1\!8fm}K Ԭ'$# ghhhKKKP~$|a=|PT@ 駟Ah4Rnca6|>_al3܆!m]~ڕ)&U65w{z~݃:)>trʲ3 8@lH,ڴw Im rܛ 9E.1IyNlHXPoJ"g2uŋJ{?΍;bǍK iԩ$gSʟ-ws!>珐=edh+agZÏ zៜ} Zrcǎ?~ ;fl(d=oxx)UW:Y5uE[ÛV ,kew~ @AdZfNǏѕ%'/W S˳ }DQv s(ƅ' *9].e!wrߍ;Q~_<[)3*z;R92H5^QQѹ6Owesv#n[YP>rCR TQV C;Ѝ>{ZFˋ.*~^Rt[{b5 )@CN~~@ bG{URP<{(&CM@ڻFAbv?}9ʳ/ʟx ;yueR<z-Aߙ~QryVx%|;o\vPTx.&n-vy! n.$mxގoP*) <@/)@8y+(Bpn%/,XRj (Ov qqe*Z5H@?IyV.-'Hx/yuY!=I(n !+ ^&uq,SD^I=xuiRMrm`t:Z-顷GfGCbi%NNNӧO׵ap,xӗ?lx}ZTTS!%e&[6K7E~SYYsxC՗vyrO!HvQRmpI<Z4;02y/%֗R6iIHh*h=>'FNS||Z@'}KB"rf^ίmc]͋֗{Q&H!ܕ4-yM%tiRRپ'H;S )SyT>Ub"x)9ۂfnwF쨤scc]YkW&%hfb[IұJBړ@vQ!<)!pF) t$ #wUM\q!$m G* %5FO@~?i }cE"@6hB֮=Z6,bhjbqr$YrۄsN߸?,c 0mcq0Fk|"r1#"c?egDlK( uܽ!bh#g(T}JԀ ! D>{:zR$QoMJZ`8ohT9Ip}"P@UzǾ aUO, ry>UbNO+R$K}"ȊME(Ӫ"lJ}B&8q$Wᖔ,7R4v@alRBWnOkm~$溄cnA>A~lM/*/EE]nThE_za& %$)O)Hv'93fwJ|gL=k'6$olx~ZV}4V!1B40ԇ>)zO6\AQ%jsH{wt95Z!12%^ Q2 hy +݂Wv &n7G{a M)^na˚K?#GlV_~xТu Z>;ٙbݽ{WTӧOĉ;o7,,,T*^XXm۶KN#h7zvOꠧ(Zv|`0kWL'6]G4/h7О> Y ?'o#w:Y[#lI͉R",H~c{XtS\HӋ7&<6qL Ƈ 'dQ\A=B<b0ylܙ[B(N'$VzǸ~aKN}4g{'?381-722!qL {H)[pODŠibh[+ &nM swWX@ź|cR){bW$KT~RF&]4/4Raz9@SK#WD'qt/,B@n'`R"RyKʄĠD-٢T_1xUNJn=|Hc&d{% M3\AD ~ ϝI'9d\!tIԍ)~?.5н=ƫIBxju^]غSmZ5?z=NOFfJllM ff/\zj{YY޽{@,+ {{%Kx͛7 BJennbѣGiii|>ܼnƌG޺u#yӦMZ6:::ϓ=?j2``/l+^xk ܫ~[zߟR'y?Flodaћ 'X4ccM;''otZ/H$AXR0QVGGŋn[Pwgظm_ Y!BA!^-~n:"3*YǺB[;Ƈlvk 6ۤ҄:Yu:80<[r!z l:cx˜3m;hnZ Z-0X,Նi$b#B!BUt%kGabh`=ڦza La;ewb<B!BZ:֥W&{abFLM9`8\<ΞƱōB! T;BuK؟M Y.L`j&`KB!BGB!=B!B]B!B B!B B!BatA!BgŽ}6B!BGfﱠ06az^t_"B!BF ^`tt:?x!Bw30 pt:]HB!p:o~!t[@{A!B虢 B'^0 B!2`}(bhxaB!0Y0^Ǻ B!.!B!.!B!!B!0 B!0 5)X,!B B!B B!BatA!BatA!B!.!B!!B!Ki4^EB!2bA=j5z!:N4V+++'LЫ܂`B? vC/1FB!@t`tA/1'B!atA!B0 B!0 B!FB!FB!B!B]B!B]B!B B!BatAn[gϵtƙ#~VCE7TZ IDAT:;ɯO#;Ͼ~&խSCM7x6$Uw-N梃տxӛKnMT'pKP(|}Ʒ*G]moKQ[{bom BuE\Tɷ߆ <>Sjֿ=ނv~Z Kݕ0x|5 .fkLJYԞS-/ zkӴׄT_69--yu fG–u}f<}ײxAB!F͍mi7;L0r7Y󛇮dիf6Wyk2_H LϯfPjżQ. 9<6j+Š+_ S(ĨߨPʼGR=Pcj̦c-ZTw\c7xye [,_"4Lu$5Ԧ2 Q 6 S,C}W&+qڂԘc) $VGtQjTbJBJxCHu}/:Q|꺘]fF}zHNA)qOW9=fSu@8MY@}`L )NZ8rGR N.E2aS^ :4͝wVKJ 61,[XDR4hMuO) +2f:no'U}Z8_|4\}=ѽGtGo̥[ģ5lPr5?*o(~3F<=pud Wwx35^Uik= z {nϟi?Nէ?=j;}GnfzJROS>Z&P}iUGxOGn3GB;a{O,l|%y.]-^&crW5BPu~l)Aвbo߻?=yṟ /\}=.oX:4|g9#r*1ɲi)'=BϨI=Is$ЎMG?y9jī=g8?9*m:D-sɓGߣeFx)\i*ިNQ[iS]ꟓcoD{bvR۾>N974_;/>2G,}ho.l/ϘͥvV ЗwfϾfW`|ndumC/@JmSKU|cT+S؎$'H#IBL2=W!qJNNFO ͙d @J$%W8 ;u;{\ދ3Kn_RZM ab}E9(ۯ6S`\aL!O]WMILsm 7jT+iG`ՖB a 1ɔ>U3ᑞy9u_+AF4v `u /odrE[ߴM!^_Җ4- ( m$ ZN?d@)(m7~QW|5e9 BFK ǫ b@moi)c1rr،k0,Tg5FwBP*&y4rzr/Zam;'mIE4MdWHXtQ^i}۶NWc]8lAɌZ.MW+Rq@:$N-q44p@`xVJwl4}Na4f&G/dd4(k95nfOg8ZL=Xp{.@Q-\S֝癶{VfeTjM 4D@*mKOb m:Zm تZ m{K zxQAmېE[CsOwjZM^t/<!~ҢR*\h|r  }|(OX̛>J -.Ny-O?ϤMDA4]z>8Bw[ I3wwƷO:p}BO_7-yɡ1Fc)MVT'9qaӜMu_}*n=gd9|P ;/B,& ɫ-b1IPMQEDcTKJ+_$Iy?mpE9>>~sکZ!3zۮb+!FU_5(O]3(O%#]Ug8|pG1^*wrͭO 6@{4Mr9=0EuN+No@u˴p{N)E_y@6Muێ&X oJ֚U?ǭI+T_tiŔ $bBNW?@K$|Z TPW|~jZ)5گ 4m\Hϯ|06mt%ʿPW|ケqͲ oL{O>TXO$~'i#BPkk4S9SzkSF϶m oZ9}N3bYX,_PN:)q2gB׳4Ec]x]MJle[dwb.|3ejwr ψ?϶VmӃ6ָ7No4q/:mlI=וp[*a?YO?YˏVc7t}Pҿ. sӮ3fd|șY]>YE^r1w~8\r|L?S;S%^̵<nsL[-.?_%|e&Cg{u6Q0 u:NiZVWVVN0ѣGʖ4o=OtH_c+47lX5e>v7eBaff fbUA\EU9iÐIK?, Kr3>O˼u>V ˂ ev竤cpO7! *oy|ts B!G:^]`'tb8eGS};s"?1DQ>B/yތ S> !B/|rvzz~/rn_z}B!z^T1c4ׁ;-!B~⽺*>K!B!X!B!.!B!!B!!B!0 ?l#BYuXC/1B!G@FGp|> BlɳXXeO/!BrB!B]B!B B!B B!B~K!B!ףo^azNt4MJ,B!dvC!BatA!B!.!B!.!B!!B!0 B!0 B!FB!B!B!B]B!BJ\,B!gyP`Ƽ3z,>#mh B!ByPWj9Vg%61̐i=04,,,,,dmsr3}w}rB!-<ػQ9uw8DyuqAyܯ7xvo-]t!BaX} ~qއNMJy{L;Vtjzsw+r\=ASӹ9'ZRoc7 r]]Ŧ %R ;CbU W43MiLyM@!+tJ%X\A] E%Xy6I Z]wfqN[Ǻ=ڛ9NJ@Yy!nc坚mZg>uY=oVZeQffy3;\LNyYs->N/;vرc}nmԗO;vرמU !B6h/jvZLX~@4vWGrק{r fxr@[[k$L5-;mm Sުd ܮӭTg# yql*,BO4k--9N*+ov|մE BBC(ׄV&<)@}z-B;! @ >J,v~ln vo1VEa7NeSD6B^F*4ͯ7(U* 4*l*I{, .!uti.Ͽ( í S9nl> s!eX .6d !XZu=+w=*>+=G5 Bzp^1X!b|#b0 u:NiZVWVVN097Ob_ki9E}7wٗޭh ,L X5Ӭ'&$$O|mTW-U`U놺R־g/]0F X5wR]re@u_m+GaĄ~`eƝ]r^fVT}0 &'Ի=h .ѳWgO1CJB;^pdk)t73r fM'ws1V};L6?`[SpjCn!']nQ*"sRեJe}1|ƍ'Zvz'h^{_w{j1P^bL8_lݯ.ZkA- νnR]JȬ[F#B`T䨁R{Nݨ|-oUv yu>`c&sv7"_lidgi`40#خ!"G>z}$خp˯m:߼*piu=p;mCҏhb0 )R}IC,]O߿@ 6mH xʝ^${Ss_ۿMaл­~ /Qh4>Q IDAT;qs}?%O.&r')#>ͷA}u<8,>\?3v /i^Ww68qS}D} w^5ތadIZ\Eb^d{Ȼǟ? \B&Xk$ဆ?j5)sƍ'~We |h f^QX7 pZk5^8 0|Qs[4|iYpa=4W7Z w:|߿牸\׷M?*r3n < [H4PXP` zKs vp2u?bmto5-]ZX:]VVǜ5w>x4;ΟŔPUx0flCPAP*wʬzn[o1[HC!^^ M@km1tݞ=:r^ikpq ;?o;1 'wښwt_;0}mqlє&g- M=& z՗m?hۉĮj7U5Cj wGt;Z61}GhnsW~ (/-USa@ >JU-\[ܻz&ox!Pv}W(}x@jC?h@К3m D-}oݡ&Mw5]thCVakhzmmд79]KM]iZ,[ 3տ6\m[Tk4X'o\`tKϳŷ`f6g.1ݭx B[MB!C|l(=|*CviO.>FzmkbR_\֎B7>'FqWUufwqMߕ܀$8bVV27ĭC8=o[ mb{nz6!M9Wwap:bH5hM*&0H$ w V ?Jr|+y]Awfgl?ϝ^83_],n,"oFo>mSQ|}}F3T-|c#νjAIW# s #sqZr2GjuAthO '>䟵ob 'lcb]L9"IfN rz@߯QAb:{Iebr jq6&h*rхm-).y.p]1?#"6_j""I &i56AW01^'{[Tu{i7QK:{=g=g;Mqpmɖy{K}_FteOJcW&zD۶i$ !<웏5UUo'iMZ^PgGp‘'U?;޷jWiΓ#+ogOƑv\۞73gDp9_˓cBHIpW Op]\xIu"wp郻F9omh|W Fb4+ $Ts@Qv4.bSwe9 uyʼn"՘y [ .Y@BL%MĖCN.tepr+J[.\ tv"d1Ǽl56%|"ūѸfDR;Yhnu^:Lbtd""'9NtKl\h6cv܆i=2x:v ITZYiȢ794S'Ng=vK`0Y}n7J6rhL 0.psIo[ۄ Z'ZX\a&TA(L,f1&\YkeaAAAAA~SPxE`Fh$ǴuKmz )f|k(5޸A}`L]0]jDBqrmcSaRF<~-SY`0  ]j(@[*I |ijnc1{\:+qղKR2j'J;"Tn;7%"Qj3[ʋ6j &%RHy~itUn-0Mc=_PP<&88ˊS2A>1G8p4/lAx[~Vdt@{Z&>\+XKruN"iz)]$Y1Lv' R\䙚i֖ؒ$D<ҬmE^9;23:-Ū)7`H4EŌHtٷƲ, nT*vl՚"ͩ͒f"p h|ߜS$\m6>o|q 6G}3Ǎ{kX:qrper"ҋd\$a .p˓hk[(Z]]]Mt{ӗ1w=>/Q]c\Vefdj ,w߻ݶn_$A97^_ַPɊ'zmk2m\pM3/:#7G/=CU{OM{^#Mk?.gÈzz5ɻμ^nR +mu1Tbṗ]/tU ZD;%ruŅ:Ɓk3$2s3l$M/JH}OD[p9wizx׻7Vtn;"Zҥit:G$y䑕}`.uh:˹sY)$uQDFDa1a ^1_y\}b᣹ʕH.w??Uy4U:1htk ;yPO!qOD7u((,2CDܰ0M|%o׵9N d(B=YRW ^ɜGL.0aрOF^>7h@hGFѩoTC^º7>2~颅!`酻#k[cDޭl6շcW%ZlNs}k{|0.CD];/_qMCK/+" ]k3,WQ&DDD1<"_]Uv Y{BhcߗXvq N t˲7nz??_YKpn.\x<,˞>}ZTd`< g8lEkFu{"Ҟyd>ncp+B1Y(@t@tDDD@t@t@tDD8>Jntra ]]n%zzz<JcBCCBCCQ-sΉDYЄ8U.믿ۑ^&zzzbbbB!r˴BHՅDoٳQW <<noY..0\ c=o]Dxc66iQ8N5'ܫuG|P;agD,;Y\7iu9?7RT #c!!"{kҴH9]= gCu5Mg}ku͹w=wn ]4> WɁIO{HgirBB9sGISYe=OZ?53V_'z%,[l GV|zjx҉w V@lg$b)J}7G~Çzl=Qx˜9=D * _kB$$, h9yGwk0a.rhbV&5E7Jt٘,ɟI`95p>J&]>(<&CD%wƝm &E?'d!tg!%tN8P8 VyG' Z]=Ʀ[,?9椧!]N}z/ [N2٥ %?,壅Fb}$z-=Kh?>t_O$AQ8Q$_b˔M5keÙNjvևk`:SA cssSIDDs=/(q' /?Sz(\O`~7={JR9r'#yS.s}CI:N6 0xz)ϦIDDy~Qryg?O|"3mY׺w^սܿjvv8KZq~oNh?v,V?zg?~JoYpV%=_.Wg^yz;.">DKf^{,gg\Ϭ_};Z^{Nqb~'w[s%Qh\3h]Ii1ǩw7LjE}sdsˆ|t/^(9?>y7:`Bwǿ+Jȩ8,cOcfu7{-݊hjm/cש^/OTE4ݯVXc<]O5w=G_j_ɐ.J9d[]8o! ԏܙ9^_GbzH<c-;>%`RP0w~p+UD΃U{W^y@i~mq8q\ͪ/ܼu[۪,&_G+=m/>-U}K޳oz}3/+]ݫq/j.痙IxgCmG$#2[s~7w]pgurKWOܪ܄5_qoa|N~)wUJфD9oM8R㎰UI#"/rKDęzi͛ƙD煘ի%{ܳ?mmJJ&"{U7ljyQ^oLz"=,t޻ y4^%i=ǽE>hGjp|ty-Vn'"⮌MSOu܅"ܥӑ}ko-2kE%/s;ͽx]׹!Oӝ_~OD|߄kvhxM:5pA0A*Xҟf:}$zz9 ^J.yC!u}+#>Jx#DȬpS5':L~tu& Wμ00",g%-e*cWWWUIm^uGpXKe3.Bz ӗ}V_& _ T+8FDVd/\8:靨4R>8͒63=_,gA]Nt.&ܺ{Gs 5\>0k\,udon8칋Ozgܡ\ƺr>Ug_z{]oupQD}~k.^=j ZDtvj>7B.jjzU{gU_KD޳}K$W?QggD4{?@=%"ӳVWU)D:;厐 }_0_G}瞑 _,\;b?vt^,=V׋Skj,^fMg 4+- Υf-^u{fŔ.  x>ޡS̱l0/K\{h0áf2^ș_{WU>"NT*ϟ^-M4{3g|D$}Y(FNコ/\Učx Wktݳ\NGg_EDiϞ(b}z}/j՟U:/yKȔ_ ļz7ﶦD];8# k?nz^X) %?m8v/;bì _w_3A8Eo4Kog_OCDD,{e@@D1||`4n[x/hMP#m(8sLfd[%~]oqNc5'YZ=D<5޹31{h0]\ .W_o^5&+o|Qާ^҂Z1:dDD_2g̈KKI‡WntMse{|}.\q}wCQQO}0p07ނ/,=٫D9?Ŝ/m|x{pNNկ:Z?5upt07M 7tDD@tQp=RǴ .=J . z<eO>b  J8(y ;w'PpXh ܲbbbPWL( B,a ]]]]]f$t?.5uF]ַrq._Jl/:#7G/iۿwۺ}D…ďmh=zS۹йqʴ'. ؿwmݾPIB-z蹥\?b:.#>kV ñ<.u}ޖS3s^sH}վ3pmD@fΓwұʷ\ Ӟ}SU=;zg~m2x,v]:3n=d{l߾C](q°^."0&&+Y1휏R" #, &?5ܨ1a^olݑ/]ޕ]Z{aD_i;d\msM^Qi;'T=Ifp%K0Owou~ S[8Gl.dѨE%&aD$ #"D g-ukr9w׮JVaba(@o`Wj3hzg/w?9Q5oygw/=7Ah  tuMG$ ODDn1x;^7Le uOֳо2rm\MkuYvQ[݉wm %玴uS8IKDW|""ח]c":|mZwx+?!'s}6"Ƀ 1s>:#`Ng1< i'x/ÐZկ~5`*PN[Oٜ\5!؉DAKB|[i7ىHܜG1 %% aՔDʜliQ0f&SipjǤ3:~Y`M։7uI^d{jrSs % Mau6%ݴ؈cgg, x H&7Q&Y*o73&Z3z r[&;eUFCDX:;/'Em%i: u>UY\`8=D$%fe$^v ɶSin`mͺ*!59Y{:k˫jMNO$S$dFlkaʆ7IJ uOc|d\J!$+T쑕'/igcXR^i0u'WeklJD$ߑ+bP?P6V >$MvF~sWo2QIJ+ f'5r..KŊԜmy)'eZLm5VxbErVNf̞:j1RFtteSSjӕV7;b>69plXJM jtVחEzJ+MDB9O'yu%nJFΪJO+OTO~5޲´ NrGcٷ޲ .ߔ!"V%!"e2Z&W6ODd3kGD0%%'KV}mSSe[MzOermPe!",>NH6Ph\[j7/f$Z_dv^$rs]{s׃w_0X "OOE'f&&{Cu[vAܨ?<":2(de&m5ˋ{\gNީ/J TQ4YKͦfKb[˶TYd{vP8[ ҼZ\Rxkvk렁$u%jH*  R7V[&"zb#R册radE*^rЦ+nlMjsHVH,nq{A}tAkݤ-fL@=#t^'N$F۴} yGMQ Ro28*y#5% \S3\J.\CW\^V!A%) .B_B<"r6[;MO.JPl5;oi)>6WsvS|K&/p D1Rwjf}ySaB6O&im0IS ђ'Q|~}vDSQg./ae3\7lljub΀~~6ΪHWZ[r)s}J#7.0m"2k>(ThU"aDc qz^pI*'UDDL3z|,;'R(Ug'Z˔fr*DuѪ9 @DL'*zy۶A΅ m,MV(6g&heTTi}|Ab|.T›F}!o *K5b"2WOVBYroUʓcmsޖ 5?YDd55L])A.T)yDnC58xgxIyvZkCC$\6ab^a~qRlQݘ+Dd7&-tiqH=IR,Zk_IH7WiTIҵzud.DunYdXkcFub`%Z]x!A-1ڈm+Q!GGt1YyUQcmERیygX'GsevꍅsSR|j4UAʜ$St=M_8OV??$":>y~͌7DSsIS_OO 7%R * eR"X[S1CċWF5aDw7k |\VGYf٬NUFjX8F4_:B!}|"t%r6E.BDe؞MbRDDQaI qK^ocnuǩlDdendD$KU#Ol=%OD+P"MW y$eLEr 9k+w/+RɌJ.J&Pj<KJ̪-w$o,f$j-24؛'e6mhj~vk%[gGX!FDcR%ٓ'YS'SL$˯y@YضF~jwjɭU6wm+߸*YS_S-%m-ڰ,,y*WCq7|"kmIl=;ݠ9Q\%+W%k5¦JppsP$]yw{iř!}IKDvI0.SM$H ͗kSuUvSE2ј*4O$vטDDDN t&"DtCh-+60oVp:DIDDv oD8DD.ĺ9eM\ʼn:.O`V<|o}woDn""ID< 0Z" eu2ƄPn(kaCDvf p *IݶYlڔV+*JJδ1lku]*T HHO :s<BW6l2TՑ(Q&/r}yѦm0yⷽ&ܗ 6$ъm55a -J$Ybc˕J' \r]OcGJHDu)CWnޝ"v߶MM$hӊÿEZSv؛+j@, Y*FC%W($"GhkkHH.U5;/7^߾}us?=t7ղB%!D áh*BDab#-G! 9&MfDDM%'J<~aRǻ#Hɒ%+D"ܦ<6"i?iJ Ǘ.4IMhkr 5P2 +VU:Cnn37Օo&eg)f̳s\LeS?d8P$"O"2)jG0vdxVwʋ *ŎO!Yj4.U)6%jqXqMLROV"iRP3H!P"tGJHTGäcH,jfSZBHQ5R4jV(8 ܬc]{.". > hIW/=YrֲbnGqu4k֬k M ::[ +wNQZ=DQ]'l >MTDdv:-vX%">jcD$RgtK/6yWa`DR 3CLyU*/nrpdl%i|TBANbo%%Wn>b|TDMv1SlV+|LLCvo %rF4jV29%3 ؚYtܦ˃[VY(z$+x ޸ nX"p,ITlBYڙ}~V뜊mVk΂ڟ^AnABp::'S3D($}MD$niEIղӕ<4\yPdP,qTnlJVhm "A|*[T%ՙ)h2RMvklP9DRvfʈiغ^[P|qz0jӶ7by1Z#rWͨʶuv"2*vsq*.mPZ$xRX"27M02D$ *ijGQ^P -%7mݲ!Wg a[k'%fH._vNدz"]Avn²W6[[kma)Cg5l/il9lVcvӕS-v65ڸk`EFcɘEFcIH^h,Q?jjC](dT+C(laG&M?f{KjRqqUC{Dc]k e6,n""/hte{ZN(6!$Luh_#Ӽbؚf,KfpQ|NQrmb)jL*fţؼggKFݮ+u%&g%BY,N{U  'QlڰOU&JEZ[v"e]{D[JMNt'<eNYsN6Y61*qb>kxݎm)W8y_1Ki2;(63?U2R3-gMM[M)R!9$)6eOȴaKjÚ2޿B& iTKwV S2L,iav(u[vܭ_Yt1:㯔t%o#"e'28DmEAlamvԒ jOIqE:@yOFBDm/3[K7@[T.kXU_Sa&YNMuV\nQqIՖRO$Sfϕ󉈯ߝ)(۾@$R\سST\^q}1 b/QE_˘,U}Dofbhܤբ?o8QV+ JX:;/g\'pJ_Z3"ɒRssӟ\(S/וWfN'ksGIPqqb2t8;U0ݘ3w |Y:Kď-(bf ^1%)E5ReEh2,D*R5ٚDլ ڲdPU̴v8=nxLRkSc7&L,I443M""A"Y>Z.5 MD<"5{s:zϗjjdM`&r&;[# *Q*tf?OLfekd/hiKn7R0zJgl0&Lfq|RrzvVJL!YXdl)5UYs JDēi5W-N!> v &vV/)m~p7R;7WjOH22.:1$1k2n)` %p3_uјV3ɶSTZY_Av' Ql79w&GY3nF&?eld4^֢ uLl-ޮ*w4Lw:f ʙݚ+. (|ް;v`fŊ#\;|5JAy{^Wm(&=yӟZfؔVl!2H&Z7lz%$YPDED)*^/`W, " RDA)J5{ߒ3c!"…L9g!datA!t A{ /!wO{ĿP\&0B!B!8Bp8Z8BqfB!tMJ-v{nnnbb".T尰0,BHH W.!&uuuڧťsCFZZZj۱Rb4o[0}B]knn7 m/vl6GGGҹAŞc( __hAn[`1B]+U]]]WWw+zF;NBT[֨oO.!V ܠ^[)>\0oͽ6ܔU@q'l_?ix"|5Zgf#= X"RJu!B覱Ԩ GFi]gˬ|:.7s1mpk8_6uIB!̉+ @Hzr;c,'O'Mw޴NNh+,ji b>:S-0N܌ѧ_N陶'Gxtq/M\R;{rmF ^~ӦD%dx-|CG'e٦؝*u-ݖ*w)F}`(f۪H`HO韨OslmvS^BcDŽx.X蓛QDe愚 ֺ)p"B5{hیc^ )Vydq?鏥wY}FȷNcI+-1i˯ٺ)Wp:7mPB$Ph.mȚ¾=iOHI:4gw#mj6{LK u̮:SԴ֎3LXٔfE: eL+o'vѢid_Y`do|W=:?B挪=`Ԥhצn\!BMl_>׍+ڶ {˜7atY\qvIhND&1MyNhkfV>zYcoO5g!)D˷S&5n+ . j Ѻ@k NǷ[i]\HpiD3 ՘4inG0%l}<.jBD~P\NW7 viĕƺ8\#WgTZ 'Ut Ju,A`XDbˢp┈KW(G#zL柂`B! 1Ssa+va{Wv4OyKSM,tJ>TW8euz X%򚿶rAܻ zJʮ|g;<Pe[5d4շE\!BM3mL}+NN5ޙ7^ `aW@s3woGcM#*u jv-~wѺ.:8`BxHQ`*^`) 6M`]UOMBqB!jrGcNh+@ُwoN?O>sƺ`ⲍk}W43'U,Ȁ?w8srΓ>afȰW a3Z5m]3gNc4S zNH4-Sf&M.VbHk²Vh4X|,>V+c_O?\Klmm}eE󟊿xxKJtkiX,6DuxT*VcXy,>bpJ;g߄cV(B\eɲLZ3cX7,>Bv¤CnΝ1 wIq͆cX|,>2v72CzmƓjX|,>VaFV9)cB!B]B!B]B!B B!B]+BMoOewoT[S Y"k>GVtkk B!.!B ;!$[rS=:!B ByDn yOvgF1 X?|nP Pl{k<$9 i5KO$zan)6wK.F_2ĩ~w[ Bхye] x0 0 2I$>Oڵeتz}=[>u"SW _swH@>pDСe|B~GOQ'w r¤E }tN ]8mnnPt4x[7f Ijé^lrbZۂFٳg?6ke,---OŒ9P)4Va(QG}|!]TrʟURr4UX%>œk@ώ_{eV \tAɛ[.xK,^V͛p5khhP+脑(>xAG UG ȁc%,'+bH ǖ@oě-VV<:2'dtz%x!m?b+@0=/ڵ8t͍E)]rdKUS0Ñ &.൯czc3i"@euXП.XtX(R ެRUQ{rkUT.0Jr3 榐ejԪTʜsg>}waR tϑ*"F- &6B`dTAɤlw0Ԕv!S[C.V+*i#&-i-G5g%q|e|:W7JaIg !P1wbePS"*+G6d۳M>]sCvm x ]%` j{LIm&IRTJpTg;SCJh`} jZen1/ŋn߶#1Tvg:URR%ό3}}} !>>> RpTRnIn~ܓ@)%r˖^;e 3]_/u868 -#Fʋ I|#IIOM9|OUE-j}gȈ=7L;lxC#%];hqcyj_eǏ%@ l|zn!+Vܰqٻ_dp5cb}\ًJr6eWV,e0nݺo}׽{/x< NtJQ`w;]H8 w43-{:ՈР5,yjw DÎjYkj0pqt6 e>Lu ՁHS4M7kuU;vf[ Z9TuU?^w|~J"+G)'N֜Ԕ̩5Ă^? W.޷♮\Z@ip50@RQ)X9Ӱ>f,a8<)9N{{\8_6<0@gxxªRuTSoZ\m}h9_Q>ngmEluϒ\.gWvؿSFǏHAxVs4yʫIII}M-[QJ_|aRn]dY~n.bll^/++%?߱c{q_]jūy< S֠ ԄD94SZCZ؆Mu Q0v`|e*31Hnh5jQ:A$ smJl6[ppȯ3/khh\բ#S[!8,H=s/L&;?xdI(bms>Ҭ ϰ6v7Gʿ#[MX < @c!H Ϫz]cza(Mʃ Xuxq+~&Pu訄Q V히{B#<>-(QԮ!|k>10-|?r҆R[=jʫy[ kצz_ =rHK9UV.pa/<"5\.&= RzSի:qdYy5N5srL~lӦqqq6[J+zo; |k IDATGu݂ 0$I5[}Ued$ wmD=Ŀk =dzG( DA c8L,#ղZBAawg+&ӌFFf1lpRW(j[>_eu ˰܂D3q`mQa:^\f`PrTB}{;Ϧs8V&pZGV"#.s}:RNw$@{}S[Q K˚78nS͟D[%:h۠%['6L3޷Xa^yay_ OOk; +(-SKB(~"ri|̬rDD)Q>՟{aeA_}u;Bbcclz>7omڤx8a Y⫮]}.5aT,},F&μjS˞P4s1E մaһpr )k\`7q 2Yf6ZȇGdwYS $xQj(,y{˗,,sYtׯj҅2 >(=G\ e-:o2*yٰ3/;dT?R]avT\is䀨 բEƮBQ ʹ၁ uSlwdቅ՜:@m Q,.8goB %.g~9RYĩT Vk]"((0$$l6{aQ_ݧi=n7x$I_|aŊ+W ߙ%&°OKe*˲7$xS-V`aH M'rms`|}O^X@ّhxJWa -Tq*Bn=iO})Svz~odƵ{.G(tzǓ밻(W!VkJèPXGBHddrÆ=0GR:nègFX~yNx;>|vQYQm}~eyotKâ82N;\z|[d!X`iI\|кN B l:ue"S1n5+hX^^sl JF/pHcH4v0n6>> ICDuqxW;j/pP8}|n| aM&[xAe`u1WT=skz'l>pWM}}vlL:xYm+-[Ⲋ )7V iR!]u.;8И_\r5*13D=kAi2 |Wk;}w}nIԆJ>ovxZ<, UT`ou}/j)Y~n˕)d*Xxfa*۞ߞ>scI&EsųY 眎" +l, R}SΟo/?@2-#Muh"UuL9q Sv+V 6YSai|_f3~7:|f] !=ztǟ?ׇ:1zV}%(tlP@i>׵B?홯Ϭ2$}"K?6d }x>c&&},S۲~J7??IP{e'?lܦP*a>ΖsfGܭ2C*S*SmS#'%uQ  FB,K,hPb@́5?HUdw(h1Uγ,!{ڽkgRRm?=uG1 n5heJDDZmB3 zҵ[KF@<E7誇kʖ]QVc++uw\ r\V-*k]o^12 N&-c pta9!"O( Vm2$%N!Ÿ:J&/OKuTݩ:gUZHZ^ )T5N8!:5%'}+z~n}#z_z:钆OOmm;pΝ>>\NNsvTb}d6J;t;pS۷o^Kgxyg7s7]|~!KHx- B|2K:Oj*8L3f|S0>0N'k>TN:53YN'_ sCFwACRcKRj_A*ib̬Ze#B9y0 ңE?R d|k{ldUy{ңXVW)V:~/>T>|ſmiVDw0e]?{SvzM͋ة6ؒdzNfjU=&Cᴨp%肮ϙS^C:>Η1ct`.z6^q^Tsݮ+~55Y:`7ʼnf n }_@Ε[5@,v9އR>. ]ϥ|q W}6kIF$Rawv oko눕n,^ٱ{#/] Jp6 Ů*9MYgʤ>:_3*CoLL1]%W[,r{ˮX{*fbT*Qӱ/C`;RAP"HOYYٲ%_!Ch jl`,yFiԅ&F+Ss)Z,#Q2$- |bfi3u?Mէi(9[Ih{<#yo4Hzp) .%V⼂I ֖*Z{xP Pq;#Zw=nbR9uYtr-+߬MO!.KǺ(|=9KI iO\vkvFv10A |Hݒ><Z/{e.8> :໌XlɌeD1\'6Pp8QX\<>>77JHiIb.+66V@MMMAA h4.^׻].J P[WpFcw8jkjr,GFJEr8ӧX7 ayy>:ZH\r친 ֿ!CVY[5sP2U;~"^ :l+ sPM|"HhM-Ve =zHVRm=F!4Gv11 Y9^t4P8z\T4~a-:9,^Q!.!>橙2i̤_==;k/Yi3ԋ].7Crxqpɯnߴq#q{ZvwַMi#yp`YYOmM{ᄏb ??l0﬋:vءñcׯ]lف &5|٪C[-e,SIJr#-~"ERP*:<9_[rܛ}r٭;R$:^b% 6ӞY T+:9vm[sc5am>{IӍ9+,xCf\̣uuznVl6ٳ[ޣի^K/eggwp|~ju:eǎɲꫯ&HPչsV>=%%e׮]nw8)yi/eϧ;\neзw@/v aNWTZPTol<B>U'[ްݞyfaߙRS* !~:j7=/v\N'NVfwL1'DLW)/(.;u]dJN(#Ir]ҭv;]6_FE]^r8]~b_3@dhPxH v \²Ѩ%0,Fѱq$L&²JMnFs0 T5%eYBZ3ގ7,xO3 !D7N}§]Ǻ|} wZ}Oッإ2/* 0N>>%υB+prf?37WXF[!`tA[KƖw\L$²J'wjU% anj %=.̓_N)wYǧK0KTK9pRڧg7'_625uL;<}owK'(^q{nim|B* \?صP(ڵx<( ح ix2۽`Ŕ}M)s 16,/^a6$Xw ~cM:^eG:E5z/:1{UD?}gzLi|ص;,la^?0֮u{ Y?S>pG{yd߈+hK$oD{&w5}k[pU\qӾ/^HpԹg_~`f}#a қKEw:b@fެ(J˜z%\q&ķxX|,?L` :ޘboxMDؼk6ػ}Alv;:>9#[s?4٩ ) ̓S2B}U/nd6Ӷ jptOҟ.7eWEVQ8\T'1%dcv-L)7َbX[j~[1OB򫶗M!Gzۯ1q=S_?1uztn%5dУs)g֪E\dxHxhPGwazC[j10K|HYBT7_FT\ [i<"Ȓz[|;dOߔ%lj;Wi~[B`i%NI:9ڿֆWc1\!S^5mzŗb_vQ(-_~b<>&*>& |Ust IDI)P:ت#wo9CAվ #n]wX..Xʧ|n3OthMKcn-luAIzim/Xn\ W8SiD^N5N|ehH}qsvɈ# Qr*bf~v~ چqxINxS#,>s FUM掊pB6h߹%8UV2m*~v/w(?͢=^Z.y~>4}XZ\1{[Ԙqs 3}J̭Hx&,>rY -@NlD7pRY%I$r9΢{)[?sc>>>7JD[~Ӌ,aaa7*խصfſ :{G)2W_ ,wc?[˳7&uAq/Otr Bnz NX|,>ٗᓷgzܐq]m^f|(4qvu nMɭӱ˸Y9†~tnS8!Bjݨc-.!B& B!B B!BatA!BatA!B!.!B!t.0yX2 \6xk|;3k&|es \VKhC -->@X:ŻDj~6{.y{s/(;\.`9/!q#H]ޛ $qy=/y5m̸ya7*zrZCgp.k$L[j {7tի\&]62~YV Ym3 +gD\\ζة+RӚ=3޻pս~P{E{cƮǎ~Pz p MrF#dζxOYWn]ςwgٸ rl\ Ys07njgҴt^-,c+/<[7T4f/;wwF>7e&==}M/׿E`tAoX2 `p ([=ٌdZ[^n~bٶe"9u@hZ6}rp =&9:'-.,5Eg3WQSuA"䞱VJmrK-/5od̙>@- h7\fO]X5~'lWe.尩Szjv,"=0;hFaaNmˉLu>}uҤ}sR1`aoNKΜ=73O :_dS[$N|sd \嫧),LgZ=sZٻ&toW0m$6QKV#mulhA? }** l8XU mmP9҂<_>IlII 3>yQlA2}}τd&9JR_odˤqHnǺI{23";29kO{),~SNsβuvt yYvJJZ,-҉?[[a͙+>AjbHOO^3EڧM`Xa_=iO8&bnJ0K|3/@Dĝޱ!댝!T64c]DqE׻w&uE$Jzn}+l괕kҧٜT]a$s5Qd|f[Dl,?So@v-e@;cB*Z*&wytNܥYY^߲ٞcUiw(\cw Ws%v:mi/ 3D%{E~L!89mqXɂ ُ<;Yə9[sk8l Q[޽gg-?J޵giS.0`'޷sp'"IΜݻg^{R`&qu/lٳg.5`oay-ݔxFe=wپ%+3+1d1 ;dA=V|zwٳocͼ"<}{L_tN27vB1A[z^W>ER){ylٽgoWMq3>?99wif䟗ծLlI9R6K.d-lMJfgg<3]}9wo_l#!qpPQLD|B@wC0evrA/3؝{vޕ6LFYos~惩[h'.|pJD1s_p$&k=bcYV6kz NDK,yW];$eZ.]{vޥq?OD9r)v˚~́'Z,i{-L֬d>||B}wΖҚ;NjDcϝϪ;!<OsuEB4WvYgL؄2?ڽ{/=a`dX+2ILu\MHHED$Rl4Zv.[}WhBTf w|Y" pN^h^q\sf$Qo.{}y=L{W{.؎oX^HW$ipmR WFD=NS d9牬GI$$i~R+t$_ 矵$VLQH"ĵHxD$IDTWs.d!n1iseĝ-6Θ{es݋6M / Ra>^ToY^[ODf/`2?w2#̔;{kݦwk_7v}oM.{7g);(g1J[5}XjV9sed=vS$깹=O3X'kvr27@bq"M ^01xA I˳hQ,޲;W.ig# Qj fnQM 5%aF`9cw_`nYOY$$g*ϕ Ti0^ &tϜ?#YX4;N=$iLRn6^k_E쫸uW$@˭xv|zZδ eiͮLNj,N stRvpCn}mN-7OnkMMx Dsׅ?c=yes'- XYOny|_( c#i[~o߾}}~1Vs^fc; $e.|v'⛋֬yW{ +'kmn?rff),55f"/l{)?X;cu!E)܃5VIb;sbd=GD棉w]!!⛏m6yxY[8xs-GSf pDěfq'7,}mG[EK܃s¸9G6#s鳢3⹳<w(#>'v2z(r|[IiA3'фEY &KE7""뙃5+k`3]Q}o\g6z:7xEb__ |wn&7_Ѧ-2Zヨ&1^b?_q.׶r(Nwh яNНfܽĶ-{g9c&LZ?Y`9v00}pf%_T-b2Z?X+3r3ojgmXHLӤyP}“cϥ1 lp^w$3۰lE|}kYSv^0R 6 1ɹ, M2AF}KoFsrú%G"ڕR) }A:u`n3vHjY bhbUݽD-Rdt' ֭l)}!mai^sw<@+yqjƪeb4i.lprZKFm C TJZuˎN^+y̚+f57N1.dr> $sD,\lk2 M(bncKZU;W޷Dy,+>tn?KݱcWO}݃N;QSi^< 0D(ܞX;Mr16lXLڲ}M.HX*X!bck#*y6}>6kijhXߵW06qɲ,>W&#t'whwDcN2Hne /;s3w(gSW$1Cxfr#DaĿ'هipbN01&ҥvojH1ɞk}Nz- j#rvr""SɫSFT8q U7-6u<1ua.jN}K%>#=ou49w'Xg"1mn8"#qNp꒭k>P|"{񟇼FgB,q.etqNW/W6*؁'Lgg1~KWOdW. \OR}>|=>IJ=B#>Nmڿ.HuC>mBSxn<Qyzi"}UEav94IRQP4p R. RxÑ 0H]0?0"0H]RG} q纺}QGyGG6H]bڵk=t5b1#<## e,#pAУ>ʲO>(6ׯ_vZWWWWW <_{d IDAT|9000a N_|=u>}:#b{]&R. R@ u@H]HNwp%j񯋫gy~{+uY [׮`3rܹ3='/n9M' ӏ\9{xg"r<{і#sϽ8_h>h>h>h>aG}U&Guq~2p8-5v꽜 V(ܡʧNdZ/QgKūC9E-cWŭNݲbǺlQV,HNh$rrr&hj깊ө}uD)DQqqK|/wDNDv _8SDpwx흝- gmAM?_՛wټgsW>uIj4q(Hƶ6G7ݜ]< mtCKvsĚw2~sKmMK]el;)89FU[)a˖Kl81 mxGѦsW{|'.8na[h Q7U9=9;_~ڵ.Ax|r``nk)`~5y8^~XD{[[v"j;eў7i㜟TV|~i W'颭vmZw֩-GDcǸ!گ :21Qεi׳SUCD>7 0 'VGMZ]»[FvbgF3|ԭGEy x6u?ݫvb%DK5c8rg#qKW-S{8w/:Sϳ˜'upr* Gvնoi3]lD.1O!ꐇEt8{$^M9w^/[|0D`v DqUS'GٹeߒӸOOU}YunX^^v"'>A\S=9pD4^~OM\7u +:Ҏ4\K߃QWW`\d I|>3-x:$p}{O%PΝt'"O=-qΨ-Quh& r<; `ضAI Fj7M[(4.w4qH]J^nib bS OISK/C??v;t 1 t=赯L"<~x.K6>,۹;^οo 3aƢK}cKϊkP6I urkQϛĬ/6?=gB㗷9?Yg'Nّ*onҿ3~>~{GW_~7Y\eXpRy|m3o(STtϱ풩U2׿/SD+n|){ɀ4(a31G޿8ԘџEwemd8{"F&,}VLD|Ѝ-/o?&h&ff.ImZgtE2mݶ,{̫gWۖнcZ N_1ŋZtO/}ʲMwޫ竍/,y+>hI_M=dosq65M}KxGɿۉflք{Pͦ7>Ϝu*nQ6"b$&{5uda~vwt3IKr_߲m_^f[\Y}g^ۿgAav%"r{a"~FK/{ ' NҜKmoIC׿9zl=LP+C\)3YF5'd)Q7b**(<[K_@D0D3KRzP862ۆ  )_oIw2>Wu-^cƙ)gD7tW;g!GYm 5aCɺV!|>ZwWx@7');ܷ-/%e/E;Dm-WNB(^|ًξS w/r'˩v^ p^WDOY,CϘ"~zz:5sJdgO,7>ظO1DLhct'% C,"_˳=ƺ{T^oNu$5s~7^זx-Γ7o>]Ko-q|_aAq=%c0<̐yLF@DD)SyH+u{RzdxQeRqc,}!\51=;o}bϳF̐do{0DolbLx=Bb1C ѕI/ԱRuȔz"Ȉ%b~x̘&8a{]`ĸ*dζSWhR!?yI}¾;$0aly]TStO؈do%|lv"kU12g=]_FtFg.tr{5mhh Տ 7͖o}b`oclIrlZb2-XDmlui"yx$5RE;o}FYmVA,;A/5떱sJg?>c'yp불[ N=mA{e>7HNŧ>ឝX.blmeH WC8b"fɶwg%F"#j=>Fr$X/9gη_Kl~viX*&rMI}0vaxS/Q!y9;oy?׽F pGU Ku=pqqk׮uuuuuu ˗ѳqg %idS|Og׾c5Ys_I"nkU0Od Ziַ_Sys#uKDD[ۅoo:ai阁|$/ֹ_EKUVmq~ n[wk`qxH7V >j6+o0!TqUo~L3F}\|$/?}W*OyuߊPl\6mXhj;C¼>sp&N7/bFb~]5_٨壷ӭU/(=^ٖdo߳1:{&s69ɉxz|o[b"5l]2y_6üۮOD I`{7!{i[}|?_^SqEʲiGtckg_n}_N""o'/yמNDcdKn-1^SՁSZ6<(p%00pDvh420bs]|e[7k%Aa_m ]_6>3{M<|&(Ȕ6*m$JbXwu@Xa.= `h\DDn<F-A.DxQ@p/`ܵOC0!( /{dx cDDx:Ok'߁P ӣÚ u "rib\;~~0v/aÅ cDDbX&fKE"_?[ޯܿe:~7]1A_jԣmKEޯ}j/{З*5_>6qduQ.r9˲&У>GX/DΏy#׮]˗/"2u.H] R. R@ u@H]FB6~0H]R.H] R. u~#p;#Qx u&H?/~n3. сhW...nnnD na9ot?>nܸ1c U{{{[[ۥKF0nܸ;f/H]箵03fLEz {]箭qN}"@}hkkbH]箣YPrRx u.H] o2苋WV6RZ1/%c֌ĭ.JJS9"bJcΕEEqzea*RT:i4w[+K+[5fFkA4oЅ7e/UJw ДZo/B.RoTjTbȶ{Xc{%O1Dy:-5$L=¼r}έ+g>c<UWWWW^F{DR ǒcՆIeUYeYVɈgWu/l((@=?74ø;p5s)1y+K*[UnDD|mļTBONU%ުoǯ6/vR!Ia)VJD""*.]i-\YMVytrU_bllzf=XjLf+eɉZ+6d"3LQFMN»leCs5,V6d7 .? Q9="SqJc|j2,ZfԄ#&5j(PtI4#=7)%Ag<M E\Zn4MM KZsz8*iGn;F]M鞼/+DDdLx%(p})DNEJL2=oa;5S)6i*LTqLLQYaFZD\EҢҧn& B$xg0UFޑn6}k'%*i7`*kJ CYq4c0 ^AR^V<Ձ^L őM%i~n̐iKk "Yb~]@]^A-O$+" {xC6[/Vu&u2CY_eRFw7W5 yT)[[Tpo.C#XA3Y[AV{V q'̲*O7_%co.o{D>!=[%$0R7f 6W*7"eM1>aAnD"$12%IŜՑi1 TMJwFьX?-wt}eA!""zj7H#bX"rSEX &""#D)"I'Iui>5vN"\SQr#"6 bc,m≈ۤBDjUw7y8 2DND$-FuMH'b׭.ѥ,oEaTU7DܺW:ȍLrgc`Gj3~_$ݕY rXID\pV.0fH4ILb'LoEBY@$XʫD$q ^+)77Õ[_5ԑ#V2LDo)yV%k^TܳgR8+OD t':^=b bX#4r$b-DD[EH+(i mao∕8Z2Pʑ։= fHt (@yoQNR2hB3YykD~RNc3rtnՁBJ9NpgaQc]Y 7)#""Q]z6%bn@*'%'Ny o2'3jM3,@08ƛғ O,U:Lf}AB&ή __OZ[(6:2YSIiOěKd!?d `""j%{0 QX";eW[ ZyeXXIJzEx0M<[6<1vf)+4?Olx^I[Xf1;'U {ψʏgƤ$EtD"mz<ē2afNfDA)I)TNOS^w MKې⺷)2U4vMhtBqA},ٌ8&"H~{SjEnA$(@NJ SqD"yP\#Ycd>^d&L'Q;@FS:ҘTvo:#=]!AkY҆8Mؤ{3ި M(GCkYl,pͰw.@p M  ; LIA[R!-Ek]n2.p#Ha A? ]Y5621@ u\\\'(.wֆ8 >G>"u77d20hoo7L}z77;.u;aƍzҥrqqquu7nna ){r@ c GR{`\D ` u.H]Rѭ@T*AJVRka7*VЄTJehm YTi}e2(M#BM.Ndd&M!#G$ K[fIHb38m y'dB8cVF1$@6Xk3o%A{~oAFoPw2ԏE܋3/ 3z?g!y vJZURɩB١6".bdR7;yh*RTFLDWjJuV.ZT*UѺ&Czﬞ+˭xU)M~<$na1=H죎fytJePڐT4ذ RN7"7l26 DhuX:^Ú3ל~x忄xT`Rkk9⍚DssYƷ\5cGuTjMA-.qZ}KDZ2AaY%ꀕjͿ'Ev/^fP]RW ڧJ 鉉:lj7z 940zeaAJ24igg0%&&RZT*)eMƂ{ʐ3h"CJeZS' 149G+$EJeP1bCJ246n0p ŚjQesRR%03d,1ҶkJ)0r}wT,Ǡu5.Cƭl]Y箌f@bjY+Y'uJBc+Y[@) b{]az b lL~ʪ@̥&""sJL`DDDd?QGd u2*94DĪT"yHzr Cjsw7cfX!o>514+dD"3?%@fnM*TiijXmm|)%kDWrk;.mG66Dwϖ%*!9z?O`z_ƌJL6sG Ϧ[W&n3IIO[):AVID&V"oNo &H LQuV f"դIaq+<\~|bP*Xn&*2g,/&^Ȑ`<#̂ "fx[dg$. Z<= #qIUEE5IADZ {{D$9 hl4ѤՕz f4P`^ 6L겔d=ad(#Fm CFGItMdJ6*ѫ˭$IҪUALh*:.DJd5ć;:N"DJD*,fhH6i*&ƏơV j.7ˌv0m\mXo܀{b4y$B5\J$r R] o8(*2%wGfztF7n& ЦDT8of}iO=@iý.c؀ S U7N<ڒ7NT2DL""&R(S{o4 sЏ]C'M3FϏqa9.Ӳ,Cd8yjt[ꌆ'JRʲ{/$KUvT'T)%"e`r+ ЦG3Jlp;TngF+Iev&cV@jw,Cg2 DRoJeR"3d=X!"H (~I,H$eH*e2DDQs"ev&+ȇn'+rlCDDΙ9"wb69XH䨒)N%uDt"qщW&uf̓b}T>DDM斅j=@a֐PM\᢭%[+ŎVCJ:qHdw66SJdNOWu!FΏ| fG"V;X"AkeAF6B?u  É uMMVꕺ YIUVHkzQveOD$8:9BDa)a)|kJz0EV.n*MJ÷6KR d 8DRo)dɉs^]g۞Qg^'O#b{2µ{N,e+rԲ2d[#ę< hI."{GD2DvU gjv{b"<KM3 Ko*zBo2iwO"jKKk ٝT&%vG8Y ʤK41LZPJ*̍eyydzTaDɠM̫v?)w/, Q{:7fg*KO?Kt06Z+(..JĮ#ȈFDd-7)D[,u&^0ۻ|o?n{&QIz x^.6>s@@Ӂ_Z*j2(TKq)QNyZN+giS%ނh5;ڽ#6y<ŝ^kdZkFSR]F?TWg9W͘@|:_WVt ,uR^[5tz$k:gՁ@m3]#em-buUz2+[rl+^4'%pw/[Ɔȉ;Q>o}ؠZU4[N ӓ6d6Ώ;vy^;RÎJ<[kziKK9-WoJ &á.lpQ؝6DE˲Ꙁmo̻Ǯ;۟MB}^ފ6+h-Z[>{gCq-?%bFqEYGߤ!]|Q0kh 7SL$5&b([,:GJb=CqM Y/{i[9a/ܖ+3|lSTuD^$Yپ 5Jݽ~'w`eYG3Swowpe7f4<#gΜ @r#. @]B. ~HkUSJ\ۉF#z׷v_!T OfynyŁ?Wc_ =wWƿ~' W>v6~6+Ζ>;Mu_-{:G{"2w]n?_{>A`F.Iל]垲q>; D}cۿzws?ϤWeS0iCQSEɬ=_1붿/DD̛_\,LKn mmXWVW___uGnM[ӶEud.[ֲ55q ^{N>tS7r+2?YѺi^~ݳq7=6ajnf*wF}k Kj˯:Q󖌋f|;^Mُu49XQҲ olODBw[W9rMpczɎ&u;_۷Y$X0_8gQ:\AƅVϡgzю`F썓#Ǯ:cSY.l3WE* rc/?׾\zlg8ɮ_,/Tc.wKD)^_Gl6sU %k44rmzA FmЪ$;PӴ5v耙pP4IDATlСʝ1]Z%ӰW1Z|{m+y $ ķvVji*sZ.)1$t6v(Ngew`Ob`V(.w8Y(Okd$Z)!wO ]ht%ҵe2[ٺiI2JSv;P\+i#R/[]ЙT6+ZbfOFk!u{2\i63o;9Mn0]&͐A[7=xsSd+?uv$sdxwMOk{l;+-QV=S<}$D}cWuzq;vICS7OmOD~fcy2HLBI?%w[6ʼn"e1-O=Sq"rzv'G)/pY 3&>z|B.^6:~o @D,=:gۯVDDIc9XN{×H/CČoW珷=!"$T9[N6H1qx{"N0ӑ~5Gow?T{[FN_U]WnǮRړ=Mx%"ߐNhRqd'"be3G]:g!"hYn[?oOI;ipX@ev:C)#"҃1x7Tu*"Y"=83*O~PK~a d~+[CA7|8CKM%0<|:?ٍFM"25UÒr"b ނv`"L`Xh%;ݢ(CoS 3ŝ%^W5oߒ$DMjh2IPD$WNtzv'[BDϘE|ߣ`n rQF{W%@;:X^b5;Y"x07ڊA{zRLuGq :rfo6B?" #26^Wy$Xσuԩm# T!g>no2gX11~2""$cKsKy)ڎ$ )u87TOI[gu܇& V(ĜxpF[?XiZ骵[L%x~T\)X6uV-cpf۠WĈF< pP} ]VUG3DvdG-DZ.:?ӫFu sV/e_U5"22G<މz/7:DfKYKe+_nz:5_ vJy |_c6ODgT/fr C{۾.+xaH?|o<{7WNu$6|Gg>"ߎ{򺱛;CD7@aH rrp`;r "||ݝdl=J;߸<0y/=}/c[y#"~5_QV{{}Lwx",/NJMFwIcS[g"g[JOwU=E\ޚbyʈDD.N 24WVӂFětsDbud`0 \kZDH yW-/UjdAj[#xyr[G=d>QDohӖlݔFb$ET3Ia2kXH%Vk=e}Gy3cȤ]r3pB&Uց7):9]7y"9 &N`Ɉ%?e1[oK^,T71n Q`<ă@.8hmV!'b$ҁbF ޤH2XLJ#'*LiйE?ICT& zeD48DqJ2C6 D"5oCN,k2\XJk鵐ELo^d0cEvD$Po{.y&KaIDtּDDcҪ2$yajKCe?]6j# Ds{QVD+N);4 @?1GID7$g ؤM_U¬-.%g԰$"XBLM>I [tF&Kr<82`w{`ovQߤm+χbeR[ڠäf>6o/-2XZVe_Fd=Eּ  X.f֩txgSSp힒#37m J`j`ML# ět#L6BG$3uamrW=}ETMk63ٍ@o/V*$,^RK޾\jUDQ`^z3%7ŝ%[+T$r33lduhO.e*V@D$ernF3u[bC8"b3l-UQ3Ņe8aթ{.޾|kB e,d*$O*wJY~{ZނtEu b (J:rrG>yT! kvNܒnoX#"&XUObkg)<ێNziGZCv}4筿P4lCIpx>0F0C:D퇕{l|K*-{F~oX(POT{[N?og8kkTnqcbtMmcG.c70OCrxW^|,RU+OH@ C2:Kb7KnP! c< c 1G0„1B.0ֹ4v>3%~sf/txq ckfR ΢`è x5;v抳5?QTZZz'Tٱf~\*4(ba鉛ZlN |*PUzeРC/ \4)Nr*(0(4"nCiiRÈV-Z"YOxˎ9 1dw, [>bshҢKEE,<^0)."(H txOUMsF(rIߡ]WWWWS:OrPaƤVZr|uǐDLuY5_nTw sի5wm#+܃%AMRuM<@ΡD4k6Tn h)n)OwSbnKU0Poe/U.[fRKj>5Xs'" ǽV.ܻZeE_,e5:, SJ]Xkԕ&%tÎ""<%Dĺtu' NHTnGD Of]f&zՕXa*`l0| PJ%tl8=S$b^ .!7=qD$qq1}%ֿÄE2,ZxN63N&n.Z_Pp`CF=pjt"bE*D.zNȥFunAuD4.07+ļBe#u',sӘ`2}`%U<` esB+ d00İX]F#ROР5tЅ_j,XpQ痉D7D,os$TqY4ԅ%SЍ7)_n #LȎ,]F3$ !$<$urx~=s;ku^{EQB!kѱ B!!BAB!0 B#B!FBaA!B! !BAB!!BAB!0 B#B!FBaA!B! !BAB!PYeboqy`b/"B軐ܤ9| T6zʬ\>T !2ʜ~%U(T"hJhEaW"BH*s+?9-cc}w m\B}?_L5}]ZBFV|x/(99ObA!C ?$JfLCBDgbj6FlPbPo &Ak:p:*B!%*eT5eԔe0 B跆sAB!!BAB!0 B{nʥxح~ 3.Nmot@un5zbڸC4~tY˵?хBB Qqz|TSĕMC!F&#nM"~W*9$:Uo19ӫ%T'!{l~ᾝT 7RBv B!נq͜@i||!M"YpÅ ɁXae?y_| b^̙{ai;mڟd8\*zlAX:ѡ^ucnAyuIݥȼ+ T,*8< &H FX X/ !{߽L%m%*c˫f޿;A-}eAIJjס6AX={s+UQxI]ML Կ;A='>ë|cAY˯?5ȊCl 9`M#q B_L]u >D :̂W7O*vm߽B2 3_4%~0mrTykN ҟ^a܆0?w[3̈́w.nzt7rMnʡ*O_@S۠޲O"UVvj&[R @RpoA<WV^v~Cݾk{@U>>k1K!,NyreǓggKHzyٌï2]{mfPH#K<]/%s{ߌgo5s$/b\ΔW}xzm[w]EB[FAj%7McG_rvȕ=8§~au~7a+m~OBrT۰!0Y|Xڹ|8d0?sswݰ}Vi;ˎ-~oܢʞ_ 5P؎ @ £< 9/cy~p1[q^dD{\LPcW8s-Oo3ronZudIĮGFD< ^'?Ȼ&| _p:Ou-,:QTc m@b2 A!o 8hݪZrh?/mZh2Fӆj@qf^;,BEcYLv7FOU:|9= @c)_Jg|ֻOE/x)u22_[DVҞ1{эb:#qq<cPmq5´,d#Wpl¢c=k˜Ol84xnjUA<0tSoHӪy.25CX4S5Gv S. ڰf2hlc7;98!Я.ṗ)~zxS@ WܾYT G9.ݔ(eZ bulYD'n-Vۙ[`t^.->_h9n?PzcjAtN; yźO1PdOSe/_HyQK.L~nCudDGJU굾IՑ l+ѩq-9׽ H ;=jN ;(xo|PӪc JO"3`1@`vKQBvϬ$M ]s@YOLE]TGW1/9G ʦ ?\>xOV| PV\(ݺ<¬Vd[[oN0U|pfm@!Dӝn}:r'֞X;+X<޾~Ǜy)~GݝԚzRcy?@}n@ϖ3*OΑ<= = N^6H gTwTV >q8@Wԝ p`ca4kW(0+ BW^=p`H")`w=J4|5+e%N IӜŽUqФ1,(|tJ-Z=T\hXLy9!Dߎ%Z*r:N[{o&shي[_`' ]yщ^;긄#^8.vX,ڣ,v%,Yf*js̜vEn>re3v+7J\ 0!3zWSk_RsHwMxyRKWN M }Yz6' "B[ێ篲oy=yG{骽or.Xq1!ڶ;ztov"99j}_\vzԍQuml\9+heh \W࿋|WVo٥g2dUȈ :mUYM?u!r,iEUO3}$%"h:ftQtGKهgdU5Z33kf6:JTٳ6pdvO Ӭ"^19LuVm;w5`c@!Ec6/k[<(s\wKk=sma;&B&v TT.N51\1'GW02BAЏGSY˕רzM`?sϣ9쵅mej󇰼C܃^Ͼf:Gosk,q˜bu~Fr3A#Bxt6WMO畖{eéa~sVmTtg>!kĒ }\\FE; q=9V|d!6A0jY R o¢/>+5f?:u9AVG'\QޕAL *F`Eo N?= Q q&-Tk=uEvUֽ .=bDaE9¶Aw^{]"7\X$.}qqAX:L\y*2WXD;A^'!`ҫ5T}9<F.;*OeUE^=wñexYæmSFgT<9}sq7nS,98ɕn#^Ϭ??}wD1TyN-Y)NnZ2bӧon=+O<3}ieb`3ݣ&Z~#a|"K"O=m.Y9;~0 ? P\(8o4tZXtģj(IJeTMJ <\\ŧGEFDmPvBcy~p1[q^dD{\LPcW8qv[9m |zކ v 4moٱeF/+V;B#"#/z8l\uP 9ޟk[H_=p?S]L8y ?M#B}ǾGNYJd#a|Pc׬^:ymUX4ugmוu74e`N&\[8`E}[(h y=ɞM*PtZٯ`% 3o-_z dh@W0x>~}/tq9g$[] #B}8Heh4KX&v$GT)1vz)*{B؟W6a2 -5P*.ݔ,%b+6@'&lk#vֹ V ""^i;rAdR5RRSwKoʭx3ΎAIrboZ@c׼?>[{ Ztk/_e ŧ*7khAtҡ0՛n#u }Ӏ̬GW o/\۵~mP<PoLG.&&SODxj;ۡn[@\YX Z&epVlxR+M81l$ T@Ǧ0嘸BA5k{G9q֕ | *o;~3&TDd7q.* ʙmY9*Ǥh849 =޿bj"q[}Us㷹Lf4::~uk2Kl3Ey9IٯG~:_j:@$dgؿ{P}%Qo{. ~[֜K˛3ޡy,E-F{BR!=ԌiqxJ KKʪ͹st;ٍ_oP]Ӻq2.zΑ0u6 /]] gV6h0/>pB%gļ1*  #?`9)TڛjW&?M{YYȫKJ.K ѢWZ蝿5Ǫl~&0\Ad_{&-ի=VI7B3ˉ:  z|GOׂ-Ʈ_z)7nvi(>h _bls5D;ӿ5̹ fں=J?|\'d[{ZhѺ/{P%MufEKo?͸f]YI g|AWFK{." LMd>ޞٰ Oww8fT@{ A{9V2M[ֹWvew#Y\ ߢҡhlEcK[kc%:HeN{{Ȩ*Z&)-`i+2@im@@QZ)d={s0bH)..*`dǰ6PB^>BO Ԣ¬2 |Jci n6@RUUT<`nMrn-8,XH}mzqh[DV{Bℨ05.ׁU:VMj=_D$oӱ ,h?JTeǹgy\ v#n_r)kfXPӐ-?ݹV5EIޓCkp̕o|᭽%=޾~v5Rċ$;Y}4Y|aK?/\:'4]i%5./Zt)PQ"1W_={?_Ω-_`Lϩ(%Dj%)u.) RNl$"@vRze̝þ߱s[IC'93dֿл?;mŭZ_<%|ڣ'5uUq+XJu384^ma}s#:˕yݣO7mݬ*35XYL BӞظT!b -i[4aZWڃ&ܬ'CND{W5VҁC,sڵG#.$jм5RBW*Mӣnzll\9\tFowx=jJxK*vn4_e\yK{r 6\t'tĐڗ5np2'vմٓ5kAQE<T.ohѯSN%{wo8,X$ecgNҧ炴왻=R{[l\(ɻzd^fqk0ϱj&֎]5-Yx/ 21L,V+n*><jDb^%SȬX@U%:iDuf y$on ] qLbN%SݰU]M4},1=;gز#QxAR٣7Yl ZOIϸau\|m0*1޲K5eꯌ?u!r睑U##^|(+!zV w V2n#GZ:LFۅq-C*u#1U &Co2F֭k@7֌B!gi5v8zrK8)ҼIv8!sdzN^s2C!o:!BmtBaA!FB! !B!}M ##R,c!;0zzzl6>!5Duuu`3/PPʿR%޿On׮B&.d2i46(++;! :::l6``A迁$%K~jjjWQ)D",FB_AB!0 B#B}RFcg߳8kE 5Sqv4APϗ| * BJ&r #^:΋?ۧD)F.~ ˢLdeR*U>||?vCIBD|&*7!Ybf'g!Ojg,=yv4,W7 FZDf;omR?? c6y{]89yDȲg"qG K^v!>s?ȩ{ Yˑ ̝?/H/^R&x\@b @z`ȾaƎi/=inꗛǴ}ۧ[VncolpFXNv V*1 ْuX *(+19XfArt݊|).%J=1fʍ#M6>j؄|+wvLseNG,~^VgM}9ϾS|o%e@V1ޚ r|3S ,^zME!ʈ'9ݡ'ѭ3/JIB3ąQG珴 [yuݦO8f;xUPKEa{g8ؘws0ԠJjd_0xemSz}Ƭ!~I K|N,ݯؐ}ӆͨ55oԅ!~e!p5 {ϵ=il_+vDWHVk=v}KMW/9tYiϤ'׺ Ih9p8 E"ENɊ]-L_0Z9O*-cf_6mSmdK^r Ji,eMU]-揻c2DIQ#҅g ўv}/d}YqC =>x ot݌מctiC-ex\ ai-d=qW9xV8j.^" I%ו\YmshyzWmѱ5vL[!J]wNRA %D[q!(+7xW/$÷$wwܽLwrPz`Lu.^p1hנӆwP Gyy.DO_v8D,_3  5{YWnK)-(|6Z-0N^ާٴj AIU-O/ +N;^j*v{`)T:WApga 0Z1!]=HyDZ1LNۿtTM?1fԬ\>OBNI&5vk>EwQMA1\ݩME3۵ ^7_|tpe]<܃/dqBY[]bW/KդYq¸3@K]ږvr|{ce~j:zi`5en>ѥ7:-»6z&$eN[r/mEՇKkJn 23b)m~?l۠LhXh%)dm2Ps{t;.l"z[%Ra]A~M3(qe˻vz7?8C|C6BuL˨dJlvmrFU5%l* vTK -rRxEffloei aE<1(5Ϭno;n=xT˼*j[)ƛ2$Ohh">YP|{{V7u]y%Vod{{Yqֵ%cvקПԩӧOפI_+M^ނS w[suPcN0ڊ"<,䞫Y#gt{j!;{I)jq2EVYiqY5;XTuۄl}\ɫg\Uv6T𥪣WoBY;777777+%x0 QNTeϷNZ[ Yj܂ˏO̚9v$wK*R]ڷr. M )Juu8>EU=2T)g/Ϲe}tE͋qSfz_vq-Qe$C"K$bm۶m۶[!8yZ(qƹ]vu_򇼇O[%+t.C{>#?#Q@ʚ]{˭~oaq54j<3q >ƕm1djt r ٺrw%ʈE򀫮gKTua6_ր#t8rvu^={]IKjZn[}Q - 8]ɼus}-]d,JXZα쾇܂/|ƻо&wM׶ěQ 1 jcEr N((ˊ߾|l 9u!24cwff-^9u/_u>ק6l<Y^-htF͠BfѸqo;'Tto" np 9m7Zg6xs+O)"1ح&mg@k3/&\S w_5e*+ *l9s)︷ӥRNִEnTFȥ͕s=u .f(Ԛ9K0wpts6|lja^9kи8 rd62&vg' Z5hݜD6}qm/o;|<8#=|FzwRM Ϟ=355eX?)Uqfnqޯ>}zsp>½3TYHRbٳ۵kW.Џ߀Jʋ^fuB!鿔+xwfŦ8 OB!'F9e'~uݽ|cBj$PBaA!FBqh$Ib!QQQw#XMrKKK544h4|B_{UVBD!'yyI[555]]]+ԧ@ ȨH$q1 %%%===6_FBA!FBaA!( v#1`܊KBUq} ׊j!A8׼@L MqNQɑALWfI.uu[62O~lϒe{ q/7;x&@}alo_aǮx(IS+!K#6NCqv^D%Ul/yAB?vm*J ڻ`룒߉̗%I?܄Wන>ر>=AשCQP5j̩叽In[?QUo:8oMl\=瓅aA+ׯ߰əF#2@8|As>*Yg AXqAu.G0w\"xsKZZq{Aw)av9#ZDO;n 9]DP/Ġ3С,H-+H'n3|/ovupNeP ,I@Rp54SDYZDOKE jcv ~D7;B]tukU>g?;tH vMW=&[DAoTQ 8;ak} AV<K@{|xo$E GFXkސ,!  mC/¼Ϙu2\?BNţQu͚{-?gu5Y =ޚ0fP&U3=;$sZzE) @ ˜ ,zXqa#m-V^x]V7RSnW势T\wy7; N *<8c}t)zOj S{.H~^ӗ2*bv.w=j}ճ"N]! X1;͋#O/Yx6c_]Bgu=R, ^'$njgڒYL;'+w۶zdAa-Hx\5Gxnd+_흿+-eK? Vɯ{ YEk֊x͝DZ  ,,wÁ8>X:Z z tKKeTMn4|! ,2{_ǥ2{o LܾmHG߰E&I^>wi_!H jBP Wn]$R*]=j9KsYFKSwKPq_vrT_W}iꥐ7 G|٣WK$E7  |Rlu%#|VZRU|-:N t+:y7,J)ͣ*^xӯl'JcwR_ܹb̍}=$BߟeC|Kv!f/Ұ{ON)% ۉ@]ISԛrtu?YFc%*;3aՁgIuZ5sP3V6]T[J G@K)Fj2@sМ<(۠LhXh%Y/H+5~f>.340(H&lZ+빳6WՔe#oDd. |1+GB.k}'{lKR d%Yi)5.^!Rz9ZҡۤyL>Nvi!OB ~ F]oKU7J IDATwdIXj2.b`j;|*$!۷mi)לK3KzU4iE'1Z3Rt _P0&SrSybPfh~ז'1*OxZ?}y7cp-nv{72gU qIf T t[PM)0UutRVA(ןi?Ҏ;/\ ).N?:B̀DPɴ-X o* ^Vsi*~:&Ir:͔59L2L ,#՚PJ)43Ac0kVL}mTZht-[Dȭy_DS7R(h⒌rfEl}!Fvpb 4w6` u>WoJoJEfJLїq%R"Hi#W[,q&\oQof w[suPcN0ڊ"<,䞫Y#gt{j!;{I)jq2EVYiqY5;i;XTuۄlV}Cmɔ@Y}t@ZVyh  ,IN:nV\uc罫@~l1H~RG5e[1Rpy`a= 2xӎZt +3qdefjUKu'3Ylaѵ7^QKAV*gPܴ NKnm@b+Ti6~!qqrn5e̦0tZ뎞m,_UQ Gx4#Ki"Qvܵ3Xx!3ڜUTGY0'jb3ۯRZp.B藡$"@p˼(ހ]]{[pFP @U7 ?l2L:!y6-:6u޽Oï[PX5WᑕcD^E2&+eg,&zqb/Pהa[O͞yUVUKǯn#L:jo Cf nXejM5tc-rl]Vqj˜i,5^JiP4^7qy|\{CKoK}\[\%/}{y?>ůjZ*{gE%Gw X((6a'v+c "bc 6q?X1׽w3~vvfVI %LS&1[^I(:&8Vึi-/ L6mo^Hu4Vfɾ[a5A?U}HTX da͈;O# @ ~zkG?R[mFǎ\JҶޫ nsz(C't  ie x|ZB.ym[Ӗc.g7hߺ>z5/>q΃,x2lX?^C;wWܼל~JUԍsnA>6iuٷË\GtT7C@pL^ Y;bt{Lٴv1jJMGftyV/;^XwF mO:w(%xyf˙?э@ J {-,SsE\-. vi*@ &h#ͼa֢Sq5&$3z B1B  ,zv忳մ$˰ <VAsrQS_e^>an㼼Fm~svs !U`w@\+Yg# _(Js߸w;o!"I/޶k:1d&>8vs%IۉKU1TѓCr~1eufeg; E!+gCr[0\@vzgJ,si_+w5ɫ:,p OĊ\MV|n.9xyDq@ԋ5S ;w?MAVIN$'NE$oҊm||iMu>P)Ӛ~zi뾎؃1E*z6~q8N|Әt/ff6,WfkS;IQ1߲F}-xb"\FA%-EMC#Q)zY=}oDݐ>:7*zy6hIƵ5$' nf\艄򔉧V)k KK[}-Aдqjs7+ef^zj]FuƜ/Oq:EoMܒ_C}Xl HFA(QQ)urgrm $ݎJ Ͽkã׳DL] Il,8ܛ杕 0'xu'MHL0N[§i2~Ռ3~e(Qz]b#U075>0M=+#?"I/޶k;M&`l杝욫@pܻw'4PO$3X:F* PlB ƺ MrYaZ!˰ xn1%TxVDrr֢s{pVUac )+=OJ\L14m9}ҽ3H(4iccAM@ H| $F~&?Sse0)Dgz>l>.~y5 3e ̀Isj Z_9f&[~Nq~5VKj5ֽӹ_\I*SkyRPSD  _EHIIaɂ3(NWV'w;&$ϒf<9I=x\YDL+G䶮͎x"puf'Ťv|t) [Go|$MZSl3õJj> Q_i5x'?L׌SOU9@o`v<sl^T17oݛ`/N 1=87]s^8Qݗ0:=fV}-.֬#aM}㠅v*aG)b5ldZ W۽j@c@M5 ^?S^?@ ?JUݑĽOtW Ni5{.ʿh\1GЬ/WsM9;$*!a~~۱Rf1.iSoޥKüDkEݑA  |a*TT??8vԝYBn{d?su0_abU!@ |T tz.jL`ȗCGzWv_p/lJ$|?`S}in.u46EV\!lN "g{lxF\}t>|dBE7^vc:"sR{6(((?T`a`U1Q_k~QF>['uRXEp1O[K0ލAV٢M~) B$$IQ$UYP@">5F00 2BzGg+8le5uu2Q@/ڷbO"ZݶғΜpo64VvtzvJ˾mS҄lN篘S1}VL(R0?}dGCܦFѾoLRwwċ Y1fQ+sJtdʌ3%0޳hKN^ ՟%ͺas/r8vC/cR6 45cC΍e@cHh'jz95~`ȅDE$E8OBmRRuZqfִcE0K&gG-o{uǪeȨhw ) ^H JBP%!( %&( ~W2TK JJdao/w?zח9XDZ/:v꾱76ZHk3,71',W}%8 vNy-+wȾ^2xDžG+\Y1iwʡ+Wv`vJWȬ!Gm<~1bGEh[Hݳ7 [?;sB13< VayZ/5ڬ,݈AR|z#@6_N0cGBFJJFI=MyLhaooPZyҰC#,Y\Y7cʏyv387Vv܂|CYiqA()+`(SjݘbIxxݖlLfĈd]4p2zfӓ,{eN2Y473O63 9&U=Fx}"W<0@m 6KP7f4D wjک zg| 4g2zF;(Q^Rm3r&WjcGaOuh`u._HV_TW> Lv3eUyq.t^0˽C v2~|)M^uC$HM%!#ߩd_'ar A(p p sRmF,FL 1 yc{bӶZToǕkMEfPk!ajF&JE +L)52 @}'-#|T+c&Rj&O.Vl_\&+~Q^cɉGf^ 06xѽT*eMwl0 HVeTCDCQEEkV Rʞ15t9#2R[jp+wf7K~T̪Ns%HeI.AC P&2R Q͵XM(J>S!X@ R29&ՖY 3ETs6 ͊U-ɝss `*/J)P {7`y {'s+N!u f e=^I\f)e$tO~-HTj7@ТsJMoéTR:*JGI|V@ 3JxfY4Hk6cybʌH>bF,uQ}Jh:ʄi-DW_*[urFA[|GV?3*z+P g'EV*JlQ8`8(@h5޴nqy}KޜY޺t2nk,Y>) *:MUؘ$ѽQ|i>$t+Mgث&Zn2 qmyNyܷ]aGIE57e56 B^/$Y?0˶AP 8t۷6ݐoKs~ڡ9 N-|*L丮Kz>Ԩ>;jz-}4Gnh8N9LB^/O A*;sdu5Nќmmd2.$p?IҔSNNQ8G?(h!k8gYpPp>urbAi}qnX⠩"|Y=Ou2b]ֹ뺟00xr':@3xvr]0e6w^a9z7L>ˡ?C,.SNlYn2}^A9f^W`h9`QMِکR l=k ;WZٯR}(6wEYzQ4X8W)_u[ ÈsAtPek2[)II JB_騨0 $A?H+l*E `101>1"g{l4:;?g͔^8Wl`'>įݢJM!)%. @0,IU~g ^<$ 9qFOGA5YO_ğB:L^=-3`V%蠅t_D ~\Sq#FbHJ$2jY,4@YHi کsLc` _\@ ?TJ@ XHlB$UCHr뉗)BUC*@|/ RJ @ ~[z$P@ PW@ ~)B A /AP_@ QB&lP\[iIW;83{qzˈ?) {SeT) 9{(Z7'A'L lBS3⏪C"E$NykܦrgS 8YN'GD*7yUT6cowh! U졁TLJ'n{Q!g1  lyA#,?9c㍬yWMٽf0sjXWg8vg+p]LtVVƨԵ7_31p)L<%4;g۵[BI Uw$%SߥV3fјt_P5ʢTmfYdt0pϚvdPW7|hҦQ!Vgf1*/)#F|BMG}H#Dpس&4t:Z ;Vˑ95~] V&n1e>MȨwu33oy :P`lɎl2N=Ju: bc]̜.:Wf8]=6LVxHAGlCr Ӿ#8G +Itc;fr$ټ2g L?n0#$ڷ~wȏe6x' IDAT`FLWSD9I&*K>%~x?3'tl([y&lmw.~ ˥(ҷN+][ajrTbJV!_߮)*NU9ax\ݕJTE j-IeqͲTgmeiU`QN3PM9N@B9*]ZvdWr&=hҫ:/S3ժ˺rQ}ԺjOWOۜfvm^酐l;,tƩ^yPuqPj`[ջdΑDIqZQ;#n\9 { aE,71',G%ɖӺK@z=" [-tE@x vظ*U 3:NiXF{Qt05} 3wXP38!.<sV:,A|̊Q3AIjxR =4fV+簘\Kɤ7٩[*q90S}L+].Q3-oK%s0E_"/,5L]pnM6MʗPuPT۬8#V< K&Kʷ=~G".8%kZW n3V`.;qbg7cscuU#q +FFar.ϼYhK>{b֨,J).E`݈A  `Ekv&OA$!Q" A n*T31 r{,a`3Vߥك*.Uevp= HF`L"F}HYVP1 4P.2ai| },1BF0'_ҙ/Z I%e-eg*4V*zP$sxrKLMHK Z9b;qZ 6UZckiŐA|tS1 ?9SR9Rd!JJL]õ1YLB"Й/"N%ӕYGY49:t"XAaDYiBNzX*܌uc&lUrnu}K+Uw0ux88PR1Ņ 3ETs6 ͊U}}%ItUdP̤(Sw>aC)5939ݰQϞb[ub,XgďJe4Ii{BijBz ҪPKc3.g$9y^X??'ClZ:y :& z6<0ejHB`8)JCױfjf0 O?ɷAVRa͈;hx}GArC Ά_{UbϟgpT] X 66M6ca@JZwȀwDG | vS|u55l*Hkqƛ>7d\U޽ykP#&cbb)-6Iul彤ko A=piQStk67kk@k#MO4_*"WgДEVLcѹʧ:>>&}doxPTϸD%)ʳr]]&T_ Nk,OirgQ1כ:>Ϣ`\ >SPF_ B|@ ?D@?)8?@` @  ^gGT|%_zz/*v)W ~lR~RpuemZHw r_+*X!%@=fk?fUW+~l{%?>9pGɏM`T7_WZoN2c~EQ  Y.Ϗ]Msnѧ8x, ٵ A|tQƼo@zFr lUh #۲5 R zzv|J_|A zФkͿfhmi8p{2& k⢷k'aciewbȭt 8fГ5UEd5XFy^~w>_#ILq&;=tgS!%N8g GYZڸ]u>5 rvQOͲC/$%N~BV}^D,?;Y[؛"rT:^r5SV[riM[uqxDW J)bX^ƔIE_#c#z/8v|VC.HjɈȾyj?{KKK[ ׍$nK_艰O{xtv0e4ImT$X鲔C\:eJu?c+Z@{5Kc6m()}vᐁ=VyW|,,n<¢Ѕ-¢ˤ YRaUnCGJ{¢ۈ%6,yI$ q[L@V2P` Slv,碯mOyw6Avs!7/1v= zm ?k41EQA\txX>/Ӣ{NJn׹ 'YXxIӥ*-ݑҴ!Vŗ^sZT@Q9ݝ朎*|ydB7%OU/mo뱀6̳9:2(/>6^{2:O*ۗѥJ$q)":.GL _W z[R +V\KHhpWww{)2^vS+&A[uI' d\EIr.]kAg]ϙ?X wwJtjāI-slz- yBiaJWQ3uWK%. c-\SnLj~7݀ L^x.,|4W3 .gTeu'=sY5T: 7̈@Vy@_&N\.#,\>!̹IT lmg%yRydȺEJNbK&u2bc?sv:ԻZ/'w6absA%QIm |)0<>辍@IKE$F# 8q< VJW CGw -h.,FL 1 yc{_bӶڸ6SWϲZjS%,ZyR Sy yrCl#>F f߉jm}1ǘ Ეɓ'jJ=8TQNsS(HZZJ-Y&sך(Z/ڱ#3{/u F͈|b%|Mt!Ϋ|6Yx1JVZR*c6nPa՞qPQ~@]HAJ\&DŽW5 >SD5gcҬPՒY:n?Gy< .O2 @ᄒpsw=W[oLPeR\ @)_ZJT;IN\Tqa5 Ow,I ##R;`o0E 攚߆S]x)i^r(I"f&̔˄C ]%ɣRrRSv~8ZArz!Ƭ/DjUA7bDC#y"qW3j -Loa-7~O"4h1S$+,UC;U3O_vj Sfa%﷏rs;j.->ںAC3v=,S䁹#NUZIY}R`;LDUؘ$ѽQ|P4uhu땄Baޫk?8`lEk;ϛYp(il~ ^nv-_Y{fh>|CNazѠ)V{j]ů=#(~ak | v^a4& yώ|P>A'6-S~SRwyKs~}ZuInJ٧]J[73-XuDIѳh={6Q2jHkC"X^N> Oj=dgbX`MDދ19R=#z܋/~"t<{a >t&Y*V;cЯ7u|ZKoknX⠩7Y==9Nv|]U.;ǸyAwS}sk͎gBuoG-K8ܹR &^{fc7ۥn:|;F U zp]79ֱ8ۻ3vb˪kFt5~{b;^-!`Ya3{;ۙb㨦uRWq :/JW%/g[)" #z @|AlBǚoF 0 V j?h@ ~s˽ޕ0!bQ;g3. j+h:*@,^=-3`V%蠅TFFLecU Zc+c{۲;ߒu]kf_B:r6ʨ@ Q]i܄1% ̲}Oθbc[,?/% Sdi>yBI=&񥸢~{Q:3$>Oeok ;TI#2^^ع+i:J:p"Ō=v*'axVn;lTuSG3h@dE,~ {cP_ ̅ یc\B=(@R5k7ӧ Ji{!ubI\xPo磒\Y@ _D(/Ml(j7JQЫτ{#$שQ˳AΧ#ɸ&"CaV[dí~5Ԍ =P2Ԋ/emdz{i˲$aJ{tudQF6)"bRW!c9p/Ag?G'n_yeۯMj#8B#ڙ㥝Ei!р GEUA2ˡZi't;*U/J>Z #O ^φ1P>tՃ$UV JR30'Lt.]ct6cb}<%OFAj.sow.)t{q072FUNҤ=mӚ W,V&vVW^LF~$ m̢NGiE(WB1ޓIm3b`h-%0 ۛ0Pl7v##N: `}ؤfř ,x͵ :-@Bjڊ8"n+@4 I♝o'os:5wnXCIK RHtPK{zw+0?CBm|{8%:E1[]g_ێ+23>dK-LY0%6@AS&" 7 (BJH;% սR 9px'nErT9I=Fy(z}r[f\mW7eyo'MG^&<-QfNU!b0sd^zo,P|>ed\C-^+6@|? 9{(#sz/|q)Sc+zҽo;1dž:LWZrԉ rd!A8MLUskڽ 2TiΒa6&kXx.Kb4e/tuz̙߯N;Q)C-.֬#aM}U} FK'MVT }7y-2Ν8y!Z`b7jضջL]>G+nͳX{-Z:  IDAT}y"wuOzFr [T?ϿJlQK#~J$_=.]HR%Y\[FwntlLM}YXtby6:OtXD K b?#ܺZX:xKS%}gsg +Cnk͢Ж$<mCT,nka8hGyzD' }<=='mmuc==GdPߎ@uܦ踬{{tI: fgqnO3OM9ݝ朎*|ydB7%e*(;, A%<6Yiac_xoтJeiS;M*úY~).mJȹk,~^ me!Vŗ^sZT@AW  2sӣ҂7GY[X <_tgM:OiUSҗ+<6Q%K B@o7eBYىݜf{U{qybʭ3媳uՠ*ѮQvBgFbAam#m͸#+ ?QNӡKaF_l͉8-ݾ_3^2٭ScfJWQ3uLCŠ=US ;W:J\.OY 5Qhvv2 t5KWTf0KMZ6<>4:XkI'tΓtY5_m6rROS[4O(ɺuyt5M!̹,@mrKQѮ^/mNs&;ێ[ģ_~?c0a1˺A]9+4YR~z(y؞;XKTӽ:uطjɯYµ^cɉGf^ 06xѽTxWz=n+z-ȴ:%żFF R d)4I\Hƚe)B/ԌL,u f!f$MQ8GSWnZo/5$f#u (o&?Gc<S%Az]/4К5ŖPMfrLfVGfe|M{%5PQ~@]HAJ\&Gx0.UH5gcܸ\i(SҼ4R?y >t*35j5P]>H3Ŕd}4 =4$hO{C3v=,S䁹#NbKr ӣCN忼0Y {vt"klJKn25Z_$!`0nŵ@ o`!]j4ٺJB0Չ5Xu6# Ǭ_[]Kg<>zaRJ*"XʪJlL(H(.JoLj6f,q;eڮ{N))%^%4YuFii=R:ZBհ>)q̈́WGtib`Ih' 9 39RQ࿬qsCэ/k>v?:vg{{&SNlY:Ai}\/agO*̝c\{ͼ~V.!Ln2"h]ν|6U`0+.q i<`y)vs׽(oF:>_Q5 kfVbY[p/  >+| lfoq;3WlT6"Pw!m 2i<[B0a_ o>^#uQiР?t#)$M7%ݿx'lYB5n=F j$ 8$nC/~1#x?Ի&D"jlSe޻.rr4d[@  즣WO kg lb3:ha7UOq;o0t)))@&/PLCCC6m+@ ; ">| G#JJJ [ndSI?*))2EQ2,77(ѰʦX__f3 $A?D8@ ( 0 JQ4&AğB=%@iAN &Jj}r,小H@`T׺]"PJ.UȢ!lSq@D>mA3/붬ASISHta PA !Xw"^DG_;owK+!R$DS[ZZںOM4ȔgC{<%N~BV}^HA쿁,-mƮ:PJPOkƺwv4jJYA/a$$~gKˡ' iNc݉xAUg'_ ngi|5S9̬i2 S$M9 5}Q*R4QͪD1}Yvq%lRkiW}Pee~i?\6CA ɹQEǮ^7V^ qaX[^Y&÷oc0tOX%̻v]yx`Ľҝ6H~in^?6͋6=+~^?ïUNL={(>}_BgXjUJ+S7rk7O]@!k㢪?l ̰( .I&fiկ4ru+uS3i%۴DQTنu=BE{=<99{g NYa^$`;͝svdzV)ؓC!;qM,ܻ|Mm{?ڶQҎekO4Y uu{Ym7^kd!6r־ا'_of!s}UZ&(I=^#RfDF="Y<~mof/r)Tj[fU, }DzGx;<^+(y&˙:Uei\1m̀gԹߜ4`=}U+S9$M`Z[E~&4|FaQ^Uy!BHBRym<'V]kkj5nvkN!T-B&$.PY7r;(<RK;<ŗrGg[wgT8=C#{'H_~zݼUOvZ,oScZy L/9=b5rO{=_G<5&r?n3~[mWT=m=WQzjS>*vm߼6sfQY^Ɗy+rɘ0(:g=#u6}BO\_X].'5S~w%3EҬ 64q4)v|}j:bWu.mXS[m=<$z%;NWVW=F=BjsCt([)/ܒ*;{˗*0zq~]St2*"?8e3¥vu6Q#3wsBܦOJJw};Sn׌{2xtCbJWe_|YXo%9eP;?EWoy N)Bowx^u3KB;edh>&'8yg:zN_G<!+kvփW/RvW[=fbK @ٳ1Dk3h==3_X2>E}LrnC2q@AucPh;vBon%߲b^ oS/wq>yEs+tᝇྖר$dq{g''nP=tZe ! ^̀6&qO(uzKfA6mN!+…*p,qy׍ 5eO$i|tBuWIU%OBQmuA$ϘN{chnv-i.(ɡ*IgY^4jL(t>ZGVQ+C!l++hu%C6^W#_#Y/ Mg{\/'zooA!?YKʊr_/]G P !dϛT`6gp۳$DӮGsӮM_TXߺ%ۧKg7KkQXjD M]c*i􈴭v[X0Ghf aB%QN{wJ&#zI2Fz\C4^ T/j~-OJ>vC^KƐ^}mLZƽ#X}8{ST;b\&<A}G}Y@DA$Irx1c"`(//oCD\dY.))14Aϝ;WPP`i5f啖 ՚c6N' \JwxxF5\X#A D@ "A D@ "A D@ "Aߌ*//VW$20N" A D@ " AQjɩr8RpFS_ӳfd\rSVB}/h.H,stW%33344Xޔd*..n۶JuɔO.^ѨT*Ip\vp|hר=wwq__߀kg:/jPM06zإ!+s-zVYY{_^^^kaRk^Pg2p=eYT^kZ^w$IMZBt "" A-#!tdڍ2sdA#Rz?֝_~%3ⵗs '>uҒBWhݛpw?>GW{MߢfƭY/EI3^3'G|pپ"絾c^QŪx,B:n1/>W!AwEPk>b' 3.δkC=m:DqÝ=H,eOwɷz54G%![rV؜]i}ؑCknqy?qGЬ|ЮbP^R3F_@N-k\i]I#"BQYjܾL3aӭV5BepۺjՆ@gyfa4$ ^pw_i$گιl>DQ_ e_^Q`q9~vqԳN#8pdH́e7)q"IiTטN!Կ Kݐh,XS!0X[}q\V/dI! !j@ p3U Ιbݢ9% v3 3 6x=|O>_Rtd+BXͥ+2-nWQ2WE@xDtiz/5$߅W页?NZt}S!Y~^̪R\ZOk*1si[~70#rଧ{*.KM?EI!fGMABI vXjPMh8Tnx/gM@HC\>m?dٻ|3$p؜*N%eGwn9ReUҭw3[*N^&-_Q1m\2H^y}{S{(ۿqQ !ғ'Bѡ{$t#CY37G 5bI2LY0 B:~[nZ`}UiԔC5a^2}'"aVϧ YkxcIDATe4fwN>2VHNc'䭙k*dCdF֊wv+pU^Eu՞ݢGKoqX_:䰽s3C9F׭3y$2ztBY CӬ7dMN @D" THr-Zi>r) D`0\L&Xkhfz""<<ٳMsy^^d 0iɱ6f5YVxnX\?9՚c6N5TRh4u5=lֺ_+-Md$)W҈"x" 8 @D" A p?={Ϫ:IENDB`././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/doc/source/user/figures/select_packages.png0000664000175000017500000041661100000000000023445 0ustar00zuulzuul00000000000000PNG  IHDR?MksBITOtEXtSoftwareShutterc IDATxy@SW>oBrrch V,EElqŪ:UUf^3/{q8w]ܨ SG*7*0Xs-f@`7l?Brדp==#h4l~#DOBh44ޢכy3t|w5Q5ˠ^à"HȆ;5߮+5[f2,k!ASkM!WıL:^OGѽˋY"0{ H`)L&PX,W5t{v8'OkZlhh Fn Dd" H`YU sb\sXH 4ԓP(Wר5#` 94٫i*( e?GD H$XB$,,vvF7͌HԸ#_g/,KDwO.>LDo#M}Zmff;wf̘NDgϞ=zɓoܸQ\\,xw}׺$4ؠQ?Īj}HΟG@.7{,)Gۿ &N'ttj]sv8SmM I4qxHrD}2qYRm""ِe+che:֒@;?g l"kcY R׮Ger'vD{}gdT>hb'm\gϞ{[n-Z`}Z_s)\CDyO>ׯ_߲eŋv /z7x{{Q试ڿ+eD;PYaxl׭;ӫ7qY=]_.{@b̹1ӿrC:a.-ET[þ>2mg%/e(/c-_71Q|ے?Vs΂Ӈw'Mدmǐp/r.زzknQ@$y>}LKD3s}2BYs朜;=G?=m=NFK /^)=uTSz]V׷ Db_C&A$ B"Ph3vݺ@  I(ln5\tb)}N`rгCDyQQ-jǏ}[cӧzٳݯ^{ &D""jhh~ /|W6TV试v xb4Z "ꞜY#qd}.9-C K^~X_ev@, BɃ|-^|>bUOD=ꯓ,D[o-1щn;rbbwo`:;zNc7(XNU>eٜ h*WTE|kM0dڲk6k݌\}7 Fsd2\`dpg< vy ˗_~FQTTT\\kX,/rkkkU*R,**J֎}W^mެ\.,K߾}>|}"2tDD& D# -4. 9Z]5i)Dg]@ iҹ=-FtL#<exbJYIv^%mW m|Ɵ1)jF xY?Ҭ-%"FŒz9A3EnjP>kK^m55oyB4%-QDDYE*-XdW夥/$ 9q ɁrK?] F:Nkk&d]bJv)G(f, q%JyV*[-ޝS8.&w#;m"5bܘJ*&1D{bmFO^WlClHY_hr~ܴ-~K4;),8Y~~k8ֱYcogիn]KϱfB$vtlݻw[naXH$\]]^z饗jkk;VKD_}b1  Z֮z%h$"+}]?ww##5/_l~|㯗 $>7ݩ}V4 Wvd=XXyt21*\QZJl@9#kiܬFeQ2)nCtW7-&.kن 9W=-mgi/Mv\b2aC4'$xmKv#2gk"2vDz9)'҃-iCR{*{邸^ۢ|"ĥؙ#bSE;w;zE(49qlTƁ@Ή^Ƕ%Jt粥yatugݡJn-^f0>SꓑPN9WjJ{D>Re@5y1 ΥJ+J$$O3>+ Rdylou+Jϻ~DL:kb=rUVy1DD*[uC" Yu֜I)+3җ0+HE˃^an\n%nE)q;iqƁ@qE)s{oH wrAҖʴշıNRQn,1Ã%mXq҃dbZW(?V}2qQ lVՏ6f(Hsr;o ]œ?u h(,<-UK gm֨[mN+7,j{)'Y.ίuv3S*C<[HKDr?L9>Rbo NxbXܾ /*9Y=ImVFĕdAӇ*QgrTVdX^6)­ Q>s^MDHUjy4X.o{CH\}k Q2Dĺy鶆#rEhxcMPWШeCZ!{eddZgWقQO>;wҤIsN=w|ZԶ4=qC-30>^,ze=Ϥr} ; M{T×_P3.-)!Qܤշ絋X`EEyM5q(<))TA*c5 Qghr/ć%"Em>]^ErWأmż#ćרP/_>έ+3-=ͼԢ" fl6wZL&FP 0S4wjKڻwQFeX7I;޵RImΟf ڕ }&r?/0繣qĽzՙ0X9>rʳk#ozeY1=quO^+vl+cvgsFd_D$~pptd jT9Wp4uJϥ)bSIm=Al-_鴥8}v'iy^͋+X.6 +s$N-Nk(Wsz(}5#nϩ9\`ZZa`Dթq7 qcgr*oU 9$f+*Ь)[mX؛!xPԶdD%^{[j| -d;scVخQۨC,\I….y,.k,D|^շ$Fhgo' }T*%b%ioݺEDݺue)ta/Y&KLM?_Ƈ0 n6unnhhbұaDT_/wwq#y-L ōQ[݆xPivYG_@MDīN}V= X.ouQɝ%Ufg+.Š 7uVv)OD*7dHs2%< *jISJ8m,GDGX%*bGC#YVAexҸg=/j蜸6+XMKL HZ8-Zy#Ox 9,(_:q,+wq2uׇ.;46cyui 7[mר@^^fMˆ>55h4sG5beOӝZN,#"Yp7=_2F?/ o!ȮsIzuĸf}n3gl0݌'lYZ81I."OJSi\ZvOeQ^0'{ܶNϟ֚暚{}˿-A$ Z%7wl6BsW>9:+?gKB -{cL&ӫ򤭵:7U;ݻ $ tBx=LdYwK$z΢XpLAc$jbZ1}~͛G]nd'&+N8aP9 WESYbJv)gXJRW{ el +2l"Y@5dij}&,UDI0C6~~ Sw/5IFB֭'bi'^OF#Y̍ "AKsckHڌ5|f5Z?PӫfbXjX,~GaO xgk[:bʿMR[;![lϓDB$؉vv$۹|z_Bq/kZU~#HRHQ fA0$1tX QX X X ͛(xVcm=Z.|X,_6b6fd2 w}7PDЁX+Hk&d2?N9b3L-.@b]h?Cml&p b-l6[,at8"K/h B=) /9Zǎ³X XZh~X X X X _J]&`0fP(dF(4+( EX,l2L&z߿C_`{{f0P<Ϸa{ X 0Z@@@ IDATZZZZ@x/曣,;*zȜ΂Ӻ\}ٺT{oű/;7ߜՁovt~e?+~āhWD:g%[Ew!3}=<{u>~Z:c$u/Mzi1+&,po>js|҉IqNѨ޳&,Ȝ8[Ω4 3021z{*F==p)՝HH=tUÑ|踩JIweOrsjeÙ1745]=;e~(MW%kUFGO\_>)J3us Z ^ |*GDb#bmYQ9N܉=g/{YBDTq`m}5*Q.џ*=LsJߊKYt"WMJ0r #UkߺmjggGGyYb+Ֆ-{7x#Wޝ_\m L,&2Tݻ  b5X] pr?*oShD.>PSw[U6oIWvl D";_wAuJoC|T{hƦ{O̩ShuOsС_V PSrq6uzω ëNpc7lض+m:=P_͗N۰māRO GbO(w}ſ7Ʀ[1!]5r_|t,9za+'(㍱W&:r 5WD iΝ_aKTq(u/7{#GOßWìbq_L1oQ~U=1":|o/bW\jjW^]aہCǪi՝HH7n_y'Fb?r4kү8~C2zicjYoi~{ঁH7]&qzSF1t[(&..fP]*.ȹZDL\\'+StB;CLD>O_~PACidr(?+U D"YA>,6lm*V`:iQ}ʼn]N^m *}0).+F"/g9[`7ďz8=XJDgb\`Qc1}D$Q,_sFD"}{ s [m;.*kR"rIZsD*!"nU cIHքUH"a.DDRrj QoTGW}L|,vπQF칠{uƞ[)﹌Y[5j aDD$d ^io (%"gD~c)BDLO>׶++3*v8#ԗmm CjQHwˈHwl}eb"ұ=Ն2bEо}&3DDF2DD+ɭ42OMẌ*^Se}aEOrmn"&~p>su\cu)M5rpi_f_daUa3{ vPڠʉ 53t](lW2Ԛw5:?U 1 kXօsODİ0ʲ v4ϸ45uZ +NIsAQWу]0r9bM642q5mܸ'azvUŵ}O7¸5Ay|9J\AUۘ'"SuyQs˒Xwo,%ވqǬyiwTڲ.<t79wȠ-;xYmPKDq " ء龋2{gѾCcez"$#"c}޹i]#wnuEFcH@/ؓecSjycmNш‘ D)߮{ID*AF \YiU$+RE$ LQ/3;}Ha RfY@upU;Y_3Mh~3NڇvL8X6gsWUsz" eY""pDRk|9˴*+gq]UWp;|իUdN~IXw.4hб/\4rO/N=4pVw* X {͞ńHXl)뷽DTwtθDF[ɲkʃ`,~G}@bmnOG`ȨJ\f/)%Lz8jHLD](s&"znVQPuoYE*jʈDzccZڋ{}#,g֭zaY<ųk)ɟ?zh냩MY]JDdHjD2mSКkH{4%***/*"cκTLD=vVrY ޥ#Mک.?|0nBfYM:"8Qm'"݅ÕG9ӛ;y'ύHZi8FBJ>yTM|c/ײeHz+z"Ws4gQu=I\z,oX0Ku'=5@ M@dЖzFrWPV\YODګKC]Se:""CUy**tD$=1~FOSr *&UULDFT,@Ԡ+r"FGd/ܙ}w"l؂yK"TT%@]W sV|KѰQ<UȼUk˰5'ˉQ fFMKV5DDLa1bU[]<w=!u;<)OL|݉Q>%:!v(Q L'6_wEBy!F1p~{.S]mJ9-IOS-O^~QrrȁQQ/Mm>{ӌ<x(V6uLGX:}GDȸ:ܞu{Xpv  ?*;;x9h}iCI鯯ǶK5!Qv>\ucR=3u_I}OޱEB "Gpۺş8""'Z@? ~D> <ͣuOϳR3uA|.&^}pЮ#4S;ieGHA|`oY= kj Dl/7{|NZkx%hsCkTٕ ɇrDb츸 /Ks]zluHvx0t-o9cmr33k}>d;*&k? Gwג} 4xgBʼ8(@@@ZN [H 1Uo0bgg'Hf ' mVE(x֓ෛxQX X X _(xVcoR_b1&d2<+++Q,! X X X X X X c_D'\eBgf%Y f UܼKħRL@|UH`nd7ďz'ZCMy-u0%.|Y^rSVIș3kXDdvcT idʼk7J֡Vͽ  4FmIVVy=>j=b]r?kБt9 4hРKz:>HIFׁtiU% 9qiOu-֓1j@gCҁ \_eVUMM=)D6(l@ge2JG8l/M`j?'C?K4D]TE֥g_ɣwÇ D 'W%l|zWtD,X,%jkr)D^Ldt"u95S~ЦU5]'}4ͻk=T.+.+޼IK&cm}+ $A?#׿m/u jt*6t(JR4lTȴwHXĪUߺT|'"+\5{n||X[_HW\'];}@wTU^ZbKrDĎH\?D^/ax"U#kO$KXNTVGTWV>" gwdsD՛g磗 }iY{H\j.ÍV޾s/5jtYw)ݞNOq}[uqۑ[|{msMGd1I\Щ6'tqC[uD݈$KBeWw$ޑxAZuՐa}lD##P\ka; Sg5M&TW\=Oχ,z'/il\oJTC:/ֺ:^X:$~ɩsjh4eևakCt|zԪ}77%>b欰 _OWnҦLx+}Տ>ͮW,MicR\Lx}83,k/~knu s}LVg\<[.!=,ڙܼƃ* 6$ĵX S_Znk6V}+93}yٛ+*6/K v(]},4M-ml' GDzy>C $W]*,=2c#zڌ?&ߺbk"4-mz:Y+wtoԏSTg-K=k[M#O^jq`ݝ*MlV$'tB-Tksƍ`bz|ǦU7I.6[6/gۈy.FQ%ΙԼ;mrlܕ即eZ㚎.xYb=2-G/;\>K=}zo{^8/ZnX kI/Uo%G[(UqMo#snޜW%$=?UdVHykl%А\mW=9zi>IKO WX ӑ84ymE*\$ٲk u⪜dkd>z@nIIA,_l-}sl:مmbeY:wϦ#"w Bʄ IDATTM@ڗPhϬ EuݵM˒ /qD_}xEvvl/#""'_m?ݟ'R,ML&dw[G7b<<%.;\}?&w*_{JMџ-fD|eu/MR z []VO}ytV[Og bG-էuO|'j{<Msp;٥DugM#ƾMDDR'Qގ:">u@uW NqSjwVW~۩.rYyeFDIj1 .gV`e"r x(뾹Kg&"r ]=WJD[MYa^5˧D~d6͟L+DˊmھEɧ,QY852r#fe}g.E7D)/bfk;-z9n]w: =$(l}DDמ'o_I^] ;yӾ3eOD8y<+l?V*QGWUg-M84DJS}}yg<+,$qbwWDoӨO$?kӡnwqMa> &5fGC#s7wq& u6+9I}n ;pn[M]{HP䐎@ zL(d~rwOy/\f=ٻ{q/%tD{t3K5i;cFz3] {G&ܻp-~5^4gxkVd"GsGA*߯~Bf(n;|twki!Br"L\t4vaN㬷/^̇W("!-fjB!쇯B 2FJT?#5Ֆ!BhZ$Rnީt7L(:;s& >TB!E "B!2 !B!!B!!B!!B!0E!B!VX;/\槱rcW} _~埽!B!д; /__h;B!B!45s{i2nc#}Ð&oJ~Go3nPEv r|v(o?~v68tO__=kߐG/TdDZ̵/;eabXjz6 B! ww}=aQ 7AϮ?87?1Tz{ZW2m؞-_Wp&d>\Ph/ȾĮ78==+_{׍#B!fGX<,}l{Xo^56۳)c~`KޱPGt:#*#"!K&Kt N}2݃K!H b}qX[Qo^x vpt,o|mOZp!B!ts]"Щ!D4JeQp7cC:@zfREpzY82 u`K~^y˞.~V!B!4Kt*`Npcۗ oPο2uEO^vpo S?Q lSw=cZB!-q3W))S|? GgV/EΆw:6SSEp@*EܾG>Ͱv:iB!B̵ 95{]ڞ=v6~A6s޾@c/ڹwk{AR~r!{I_}Ԏo^'V"%F!B]yL `J;4c' zG{1JM>ܹs###### <qN)B!a-ʉf8b,H4o<,4xԩS"H&؊9U L_Zii?}ayOD8ux_Dd;{civ# 4Bh]p{{{'^x__߲eH?> tӤ,\ĉKLS՚۰ğ{7uS220ny"QH4_56LK 8AA}}}gffM2<<1-ϟqѲY1/5m?-g;ǧQnUHk-qiZs6 sLZZE[Fp-kEKg>]C kB!Bn"B!°!B!°!B!*8B!u^%^oz<]44LXp?p?A3"n_Ncք;|\WGTjHzK;(rn}KfRa*J0l44MDUỜEBQjrJVNrVS(;.}}̢aЕ`8LuN[KjR rlj Q Ś|0~iwk\Huҋ:74S]t9Hyni К⪊| 7Yz&fe2 nfBAVn2ˣ "ư@$ʂ$cHP\QjC g<|ꭎV[m%;DeKݑDQHWcKV{C(K+j NR@ 0)ji{>9}葻r`++V oN% =OnV@>鎧J?n?;qVqK_zg̃Aѷ,|eSFfFhkyդOwu(F>_.PHć*5%7~ɨHIZu6*+r܉@-Ѳ]t;,PIq"y¹/C7r6)ȣ,>$t *:Z+ۼa fgMCMn94XmqVX/o + 6S~LxRAjY3Bs.0مn Ŵ6ZIז00-zª餧wwr vmK _Xa^`mr{vl7]v_>.8^WI: [|>͔..RR(ԕ4/4A6̹ݪ$#m5QsC^`\NzNWH_ٞn A)qE̵;}NGk"nhxeF\|5 lWZ)ꜵM-պPCS ;\>. [|ۍ]Gr}M/n)ĺ)bZe>Amʞ*kH>ɚ<.P;}*CaIAMK^`lўz Ο9w 3éV̗?#ݨEȁ;%/=za,_aU=}5nAW:2g:b`SoS'b-xYA)2npqIJw>Sf]^T}-vp1P謴5;}-vuwDcHT: N7lR~&o r *-N- |U4ֶ|>S1+G:ZWVcX̓Zc9z.-z $uF(M^HĵX4)7jȀ7a_PlPkP{RcCːk, (@uyj# dxGYmɞEWUfe .;IVap#Wr,;7gRIIP2XI .+*H/EoTn1I/Kmu>^cҾ$ YzoXg̴R(O$A !Do?90xRGN>qlV kx'|ːB(e x.zH  2Op[0Uv4y ?)9bIC!Sa`}n_W46 p{ҾX5qCZ1坢/oBFJ\&ek44 2'UfP ZmQUQWQ4R֨FW b'$%%'NlXG.%&.4Z5w4o{C/DMsc$)6ihuU6#p eU!3 5%>Ԕ|.~3Sܑ  SQ_\*kdd?);3x@95x>#YěI[8U듯7Efz7#3-PXc"Ia~X+,ߘm (;?a;PX,t9#c哢+ft*B*k5 'Ya\hz(X5[]9kO" kB7*[AS2v:G$M'7*n (B%d ȕsL'* $[ ܢhj %csQa|D >2ڽI&~(p B7#%)VX.zϐ؜90bMa<!&2J`EeJO\Qꍿ2ꂈ^2׷+I4HNqs0GKnk+[\9vçmVS[sRLrQqߌI4C!$c<,"><6X(*wOJ4y]/OXtI'F= vKXz{dpkG_=TX4w'M+٧2j¾U<>W=KS 6rJU[XpF!p̓Xw{DU3^] 8 1i"N~Vٻ[KO 7s p{,XBt 8Y N%Zn_/D|nfҜLOԔ{rxy_G}2-'RV?9<}Ld<H]?Z*b1S|B,sRS R!~hȲߓ3Yy0#?WjHhrKe >QqR]\j|\* f44O+ 4% [pN’`skwb_G̒RRA8չbӃuUK/ʛ^?X`swG]ZK'/ }8G>L_~BZeˤ/ɀ!O'|Ooꗊ=/o|f'p_= op[6e7>3r{'=3|M+2b#=u7x6An2I~^2aĴURb2$JFM=4UfdHbB1M>P+xhhWiEZĊ\vErcͿi1rPh:Գ@XZQK,w.b"0FF7;+f2aO$›f4ސyj-8OThٕϽ'ۊ=3!#M|fƁgM J~d9^I|xr3OVo4.n*.4-< Е|5|nĤ@xƹ!Sa;"3-sH{okO1TTOd\z@"x+eJjB7RS\fc^B]/r 4Ì kߞ7L kBhj}T!tCE=kVbp4sw"=3 !B!!B!!B!UgkߟX+ZC_ہ?Uo"K}ymƱW$w\xĿFOIW0WB!Bq-GNCOCh;f~{Ocg(Y1-B҆n JKÆażZs6 sE[R232x\DӗVL\lm 8 4j@(7:~-#ّ%~BH4{4pή2o>a -x|O{eZ8D R7}仿8%ZfeďW[.Iѳ{oxH[4<Rub,0n}}}ۅeff^4F]C`H}cfVKkmۀ`ϽNܲh3"_B o\GXگH8?KuR<|=HѮ{<#>ޞ㓳w|Ô}d4d<~rwKӋs:;vWNO=Mhb-kw\ `÷RR4+yXAѽ|Bng{t?ٷka,/r?>A"HD"hx TΞ={cU̩jm؀`={ŝߛG#t>|p~z\ii7Fѫj{aJHֽY}Yz"{|@ptxuV&}G`曚/!Gp1L'7e,Őbg#g.Z$YD0^m',7ݓ5ѿGO?dhIgw@8qc_^o6p^+0)ЌAʕ+{{{9r*9)---33sʕA\4F< tɞd fܳb;V]8N,ժia?O/d! I iHhi~0i*/ZZ~O|L@BcaX{!=_0hjў1GdoTb}W‰y{y C\&o]d2咭mtx>\vO|+Zw}\W04S7~3oy`A֗{ag^'"UU_B!,2={:7jb #̝ˤ!B0Iڙ `B!B5!4$@!B!4{M"d.BF!B]E!B!a-B!B!a-B!B!a-B!B=x|RO.\iB!ЬhG'Sū5MVn0k'қ*#rZoXz]9ZVSŏmqZs2H7LS'Aj654'>djv&UWX%Yrab2>2jݽ^&g NI.VᙆcsAwzSq6V[M.aW\E>mbwMk=ohGa;yN[;|'雳,V \ѺؕLLٜʱ\),cBFosMr7oŶ,ה&4NΐC]vpPYӪPXrd$GUM;魰\VeV,txv>cT5kNGEaU 䲛s XU<5 lZq- Si{;UD f5hSF8[|X;4K&(usCz8bZ6vs6@z;]Ezyu5wsNE<6Vk`8nG/|'̏E"ۙ]<"Ajsl*=.=HYnYMvO0趛Zd$nL^Ag8[cʷZ\9fj曔4sZVo79^q79zVkKdwZfkڜj~nPZ륷gvqL_ l.CZYvg}%%%.&ikD^jMa.vX=*MoɞHvW42u6v,.D%%%uD=89p_YҲK7%`EVk7ÅŬڐ[lGyF(b7ukkMVd>TWRRR=D1sI7hZ5awe - QٞRlwʻ\NwI]惮Ƿ0њ'%2iNj:9" Zao+`}fVYaB8tqpqۘÞ^|M m<&%?ؾ g|1E#h m߲ eլ^펢muq\:кBijLEQXޡ!5*!Y<*VnwG8ȰRciUH] @*P;jK֒Mɖ҆\Z`\:'5OIhGƒN,/5 {z=%E5 ۊ*H`5VQjBΚ.$@.lWl։Y鎪UR.wjY}~Al7,FnȭhK+CUUmְ56Gtk. ͵vXo@'ǴudMrYT )kwl/6G{Wb}^y 2.~P-$lݾ@V384:@cY4b `[jUBQJL϶jfXt>o FYek\cHU\jӐ]~~bk&'}ϱ-$5m^K*5IeyB2 u;\6d7seV/I]yAfй[]FY<Bv^\ǰ GoЅ--٣m++,ˎv fR($m"nGmr٤nL㯿;EU۫6kSaW^P2Q{"t+6D]55 #)M;Lj)O0 4V^_Ue7ps5]P[# e;vl/ԫuf e<thf!tfBn/ تWjr4ܾdqt 1l INuT8`Z6 9VsWz9lKI6`}䅥 ` XLHLd 9\S*z 5F(@sG$ѳQf2۫*,rZ@Uʉ HY/7a6\WBF:cdѯ^o0+X/:~| 6؊s9Ui]4㍂ؐ?C m\c) 0ma>sw j {lLk bz=aBgP_P*fR,fN^P߹}^CnM\1 t4$nћl*!bLaR+&ݝ%ˤD!+rI<3ZכJ5'[:[^.mEPn69vQ<%me!ٞo 7iL_owƇں4|~v{SH E@]ܲ=CeZUwD^f=\b'8@Dm0$&@4@Im.7>XJL:Cr7Վ( ^X.ƒ8SJAMRD[W|9fImئh!Q0`BC|K\E#}t<1mFVQvBU Jpݐ*Amy_EA< lƇZ E G9=%hMPja>Qa"kOĹ$0ǵcECMĹ qİԔVX*[miBWa.i{GV;R:Qq)zJ)S":[J\6ˉ?J1]Pˈ XBxA_oJ&piJJ& ` ]ɚ0Hde:MSw6鏁(OSҲ\iUm3"tѹ 4 @H DZ[jcW1MD sX8:Ք0Mm>K]hf@WRe*olM#0eHx`tA %8]Y}x+HGV2syC:nM~W2ľ%LVG@P)"Ic?̾)"'x*uaMJIi-]O 7eSE5 nkyaFe iȭj(TC鮰!p<\d2g.2z, MIG$M\He< (&dJV kQcKh i4bs}+ %^!3+q`{SW1^R ʆX$$lzZC6* 6 ]Ds>Ǔ.ڕ;'}' r<G}4œpg7IC*Xsĭb MEy x6^I0=#|$"F 2Ψv3ZJr잧;.l09 wה6:&T@3ǝˤENkZ.';PN .G0p3JOPjAth&ƚjhY>!B5MS욚T`9owfvFbVWZ"n듕aB ->H45DIGsyX- gҼIѨ,@\\`>s#  4F!Yja5@7`Ԕ3E\Ltz;X tF% R5J7m&|.@$T1yӧ͆m͕ )SvzMWwF͹٭L ̲PX&`uF!iQP"Fޠ+ ko`*[ΝǏOaRRp򣓱{W|ݙz/Tn%E~]U5z5;YgECMVmu+I^rJoksԯwmօyU[%[:j* hn,atSOj{mi,lQp߂v2pS Wu9ךjYGE]yQ-e0!'%8Y>jSbn9n!gE=nMo\ uqmi- D I`UIg%7fE/^gmieyMbnOhFjepuK ca;AJ̄YZW\eSSdҗ֖ByuMbT?UBnd7nb׎=%O?Od&z>*犟ye:=>ݗ{8N O`L;"*ƴ!Bhn뮻0UdWw#A*YC}vR0s=p}}zUvN<ć"93tarAm=GvA]#ڬCJjSZߧu(~ Kn\dz:LB!4"+L[(==jW?_;ّ'c6UO7_Yq'CG̟?=!7G>g_u" ׭,p䓃<ײGGVf(y([dC'?>عd0sc`LYBOeɹON%_]UI flw~Hؕx@!BӫG&a:\h4:-F)KlHXP…~5='1s <?~\]AdD!BӨ:]QS}ztX{UFM>/4C9[{r]Wqsߟֹ{^v7~W?3ofׯC?3[+|k8K~7FM>Hψt KT̽>߻~ӟviyWV{TSW0oΑCZZLط &(qT.ҟj:D(x)X PC1h_5F: c-J: )'DzZxn{}w_N(bڝp?"fI)rǻGg<@#^-)㋣e/&>^x}ºmz㍊YrՕІ8o+W/n*IzΟZl\%>XDߛSlʖŘϬfnM7zUr$H } YIkrX*_^nL25]?ޭΪ|7a)9jK?֪Sˍ ?'z>c?q@W'N/|Y7*;]y/1,՗)8 ]6brhIh?ͪ]o/Ksǎu=s 'NN/pN{_-fmoѴ2nµqJR[ᐋmeڲ-o/NS*j:a¹qJe 8vaRvEKZhd8%P.~ϻ)0˖U*Je u.p]bc\+66,Uƭ}8:6ko"kHބMk{?=:aa,M4vōW:d&*0nOrU 6} ʼnscqK+l 8Ĺqʹ}rfY={4㖖y T\%M4pC{{S._\hY{ oWe.NTyJopEVt{pSS}oolX[]m6m{&kkh]a5) s{ L2XPZ[[]k\!Eƒ;mІKC}copl3WgO#} }>bQimmile$ Wfi[Xo5;6mںt㩗kkku~U#b~P=7e3=3֗X8ӵ5scX?Ƶyfa*l1,vo<ΰ[vԘf8vq~ǟɰmΈCeOuank}|Ĉ9U5ax JXPj6kg?icsv‹} 9g|[9mNg GOʚySTXh oBh;](a(y 7#}qaR]怣/)a8T-t)~Q1ڹ4M+.} ׾DZݡlfWcae[aҿ7E:vn?bc2J ZDEyrv.[Tc1J8YLbyM(>Ews&)l~s2QoV.r',S(/1)m6Uˑ"=W_y9M4"'O#,#$U˨ؖZ\S!vqZR_+̨1Nb td&ěѽ<=4.]]м~S\c׽O>5uA촞2xϧ ވFur ]&fȆͯM~zw٣ l >sW}1}[^y1􍳋)>m<ұ\'xq$&g?mmHf+}{1^̈iR/͎Nv585bHaS_ =@&L/ 1n󀫡̈93١1~~ bfj;ƞ: D6Tcx61™fouM]QXDΌxs|_3oh!˜㹺}2Yqq&cv}OHx;p?y8XwU5aֱ\@_TD̈"glsF38uocL@8mɸrEPcadeW9 "x'<@g}c뎽{=MxrBɴiЛoG0Ո{8zB٬@ މmKb:e;7#{Ʉ;q1[15{8l"p,7SulCmaUDƥoo-V(TK"'/DGl4M}˖U\2&fILw(Ĉ0DQ1bߟ,,6^j7Է0V-L#O;.IwիH'qKKKHJK %%2jD5`T:-%vI,(HNFr=23RMTUK]Qn,L+$R tr Uռ-bnV+Qu`l-$!tm8l7d|XbN68͘kaJttc]}Y۹-{?FG<=@>|b+  8??w>~J}6"\p&?&I_:/_XG[{GyStȧ+W| Uww~^ZTx `e2fS'x/_1\> y0@؛͋VM L#)!<A{қ#8EHj[qx.3=azK;C=+ ۦ Շ?z~֝]1ׇ{[Mnf;soPa[;xYߚCwÖeu;\2{,';3%r?:f.6goO !7@7F͜-$NAUT6u5ܲla:6IAّ=W?;wn4l1@_پaKBS@*b'zP2U|%0g𦳓!Tuɱ%zk pH60djVg{|,[ V!dcM˸9Rp'XQ8k,Apn-!w@( u=Yt }Bsޔ+w9`^Zwkjy2kF 'N.> ,g/t|csd7%4oW a_eq^!s' e= ^PaWc]eT~qc8x @ 6ILm={Û`xZ0o:|9V. \ϔ!qq8]d(HJ̃n}Dhz=xB'=17 xŵsˡ,zM,)bī{?c֦+KKI`ޔ{"*2S7DyK#̉:"CqU$&}LbX^qsǧ÷/0.'ʖ;Scp2 rwAm-eQ`U"2kus y0n` lZ&d2V{Q}\mRkթHJ@ی 6(ʨ竪Ө˵ivU%,:4|Wι] ' ^زA{r;(Jىm__<6,8zfLp7=/.]>]@?'F>9Ƌ'}AAQcwҟNW]-kkw3:t+[I~g~JG7.=P}u˗ kv،~җF?w]Yy$&v^/{;sFNu{,q/:ƻVs 3|2tKPVpDLm'\bժSr?Bw !C[ZpԄm˼X̷ԪLb> 4+k8@bqb7P!qL4Nߜiwݖ7 r~>OFOyqT[ݾfSFn1'BYGM9'npZpKiP!;Kx;Al{Q25~~rk53Ormn2q츇x1;oZG͜yKu smSLa ?\i: nz۴5}'A@ g<`;kϜ6PAẺ=- IDATۖ9ё[Wg<Ieg! kof {|+3)Omw!;N  *}߱]%6YzȕBdgXg.TET8-tU,c5usǵj^}aa(6*]xa^]QP` n^DȒe\!HV @&ZLV^[bÝFc# ݘ5/ Eݎ>NJυ?EŖceLeQգs7 -=55BWWOBE ($3Cag}_C_wuD$yM8{-O,~gcSys|%Nr7<1< Ie,<7FjCH>x%C5L93f'OׯtefjX-cm/y+3]aMK$0"Zz"sv6x5{A j'X=M6mI?(XR:M X*:>} E"5sBAhLLq8o$[` f샙I|p%_I_Rm/.iO^UHl^1sz'W-Ԭp.k1Ɓ aTîH>#"y]8dl1Is]9~?ni >ACs0b#pKKi[ЙVzjř1et2lέ-1ͳc^Z tY:?B dŌS- ,]!%CgJ[W*OxΜN9gz-qQ/-X*p߹nh~ygZi&ӶHM 236q+kof)f*WK oE.ߴ`m= |1xn=.ٸ.]GĬl1LwhJOZ {rm.O<]?eu۸α*:?˜*۸Q)z,,""9 U%:>YU)R%ɊOK\LIJ11`l(QjxBШRjJuRDl~[z$~b GX4FYYkrU)ZyrJ%JT]5uMrdW2:9k:M eJBQiZW )QfH'Xz k׮uwwwwwsDzٳg~8!7wVs^5 8}٢bg\G 1ι(XiG4)q]xx=(foHwgQ1,nll5 T6ƬzA.jԳZ2l[~&֢ fTBxmq01W#G(5s^ BM^::7Vjv2]Wfϛ:6 B!Bh?dA`R<ADGtM]Q{㎓ƒ[QxcvD癆ݟ9&gZWk`*vs^}:2nTQKmK =_|z) ^ >}Ļ5ze51wt_|\}e̘qb_3*>E)SƝc%7uiqCM#;eJNc\0ĴMnj~ F!BǬ >1|hg ͟]Qé{HNֽ@6w0 ))K Wlzt <|1힫SNlq|LB!Cւ(R:رCD)84WT~BHK&D2n@ 6`8@ŞQP? o({OQ>HsuQ^Ծ%oO/ó/!B; E=UE؇.g.v 0|1q>S\Hϫő-- 'N1jL|b>8ç_.v`LB!DQC^4n5jseaGS^>ڿc=]~igC'z6yczؿ:DLAB!=vMT:EqZ?_v8eϞ=Ӕvw{G )AГ!B{UL![Bt,|r<iѽD>޾*=gs7*S>ٳ tkg}wy]?3g!ΰ򎧟᪸o[U|>}?m>gW1sgFa%UoadajoH<_7_j9ƍ9*Zg}߃mQMHe~{F5]uև%u̘S'jʝ?1''NHktLduZ1qDJUMx74SJ2{o$'PXXgMV8q"o[]9kr=[a-7ܕ;f1fC3k'NT,}SϪ9AoӌuVhoi5'7NZ^J#aswQZ9o+!>d~_:g~umA^Gnˤ7WB9y^ xsLTO3s**[zU$d[F"LEbY1[JVMj14`KRic-9X_'zv$Κ5 +drdJ-tkheݔ_#_"i\Jj X+S%jJ}Fwߞ\D-%ib|Um5a9U"$|yMV`ag7012@4W#3V*"$#(a@b9Urs vׇӒZ,3qŐL}};7nܸ1WH35=_L[y?eFzy5qA|yMsj -{;Zд;]i 'T?pŝY>;Z~߯R#=QÞZ8GZ`@חmǧwlv;5MXtnU#fpwث--:BA5ڬZ;wşio7:kꬒ|]rjM9Z k7eiU4RѪ]@>?YM+&NT%X}֒T M*V'{;F-8Mɴ:yΒJ tjJouE= R JO;XP(FW~㕰v>AE{3T'[ZjZ IIs)ZM M~>9!Aթ- K mANQh|A;=;j)HV D:]|B B[M6Wur{c,u -9*DQӴF_|k &} Σ{8-!zӛ Bٝ1KuӸ /L:OGHi,-`l.@[N":VF<.'G+H da)E5-tf(3e-2XJF+ŜYo(ţnݮl05Z<+UQ*mZONШhUrW*?,cX+6cO&2F!-i"6$J381K5:ƷG=.CrܛKٞ*^K"ER]ouF4=gcZ|-4kTF' ۄkU ZN)QWjOVT*Z딝!ݞ;\f/{y 7Gܥu?a W$]>#/lh߬_wm {޳.^s(^/?i"ÆN̙?uBba WI*_*{`ŋ_+ߚƱb`77ٔEG&cm?|O4YQe6k5;tVQ<^eSM%ZX`nry&sMMMQ0- ܖ*LƜ+rlNirRE&}q ZTdCx$zR##Քj$Rfsͮxw[ɘs 2[,|yӦ5}'&|4l62$v\[Tl64O:eZHD']ڞk"Qhˍ4MzTRQT/fն+jY&XUW˵2lK~]]rbT%Au&+K99x؂2L h1o*iy+Bej=N%S=iI.Mk  /HCjNg]&LF%U j56RQjT8}cdV+nsB % 6@țnldT}VMӴJ{ُ!rN0;,V@JsR\T P(T cd}&sIҷ!r({qϞ,gQrMm3i4$(tآsyW8i*fkjLE `uY&Ҽ3'Nٵg󛓉zZI!w*0HV[ lcAZ)Ze)Rsս*N>͢ӈ8IUfy26@o7TeHj]A֬ 3jj̻]wњn]Dg ZXjܰ Mw p^a U!CFrhy~WsW C܄[}g? Y ڰOwot-Y!pSᓇ}g: mS|kw \7BZF@HT$vdk!@&L'$"B W?&hde|S_m<D ÕfmzPhmJlF@Y ֧mrRdP"LC<댱)u-RLGM;;rU:+ WcʡCRm;2B Ij[.?KMGaZ\ڌAkr&*)Iz(Pjޢ8-s5p(JNjmli.Pz["5r U)JHEBeJDL1.\iP:^[LT'!955E| @4)Q2=!Ϥxiw6k7 "]NDDm7Zcc68d]ybC6'%3R֨ܞB dm,vܯ\ Д%' }(㑜M" w-k-7X,pn7@OĀewm=Vv֚&OHVZ+/u*Z #]X_hllesA斷$8PfIƮ!uc)XUݦڍw|A'|A n--"HEVb,uwWQtѱc:?UU7">Gs%6FrC_پႉZ Ř?Si(/87 !* LoIRB pH60djVg{|O:/ACnqsz OtqY\C%½%@( u=Yt }BsJ>ٝj(u&aXϼRxC忺zbsmT  ni~ ';&{?l;e/w/|)6xJ Ɍڿf<0AĀ2j^5SPy/tӣ6ݜ/ Ro|$<_+ku)0xB ?dPX&9k&fØC0!xc^VB./WH`,57HRhZW&reDT&33/Fۓ:򄄒zFNq,Zq[W7v4{OӄqJ•*w({zVݕ,Q!pARx;Xk3p\=Τ lc*ۉCɵ VolRA=,ê՜Z]SPmZMIIxؖ ހI '\V !7REY&!zzbR%!btArUhmit dͼ1-]w,ۘŘV wRx?ODqVsnR$2v@Gd 6&+<$,z֪ۤSe`=׌ 6(ʨ竪Ө˵ivU%,:4|Wι] ' ^زzW;(J"*AUdƊu, IDATԹ ~3:̑o_]M_/[ ֞P/?t g~b[\be~yoP~SBPЯ!za]{ 8?W៧˅3/??l/ԷE ]F%HK 61oij{,n2بIJZ)@5: SGVQF+m)E)YYlkjZ$p7X`[j]K ^Ven7G$i46r,g^&w$\蛂;I`-͙n5r9?Zͥ5ݧ+cE֞G&X[--v-jra- cjK[Yot3n R\brۓ2}6exG˔\1)Orn7GxKk-z8_Rxj R-aӴ2QYTtC+k1zXmlMrlc~Z1A@ 4VYZ\(Ѩ\d4z-- hs6 PE$b17t145-o4 uTXc"[-A]%Y;VWx&R:D &; :k vR.iM]Q77p\֋b~`xa^]QP` n^DȒe\!HV @&ZLV^[bÝƞߘ5/ EA~1֢}0D.t xN}gB{@PPۿj8X2J|3Yw%?C`b B!t.S ~~~"B!];wܐ!C15jrΞ=rFa-Wɿd&3g.^lj4[jP潡+vj1];MƝ.B`Q]G3:UT~%1=uB:B!hoodA`R<AD KK㖧j!wwДKŃ74e] F1M%wW4 h}[E~IcΫq/W7l,=\zcӣ\*&c屖10B!#Ya"<@w;_:z@D?5f( EAǥWROAUQR>YhptB ȴ|p 9"B!~ qؓ?RPŎ.KL?ճt]=?{uDLGظ<E3{ lB!B֢Xi|}lCvv@e )j^|U|c;>~ɞL|3B!B'u;`hx<,v8y#'"@ˡ:?+Jv.~} ЉɊB!)h-.wqM\ffhMRkblh jh+WWօ ]D~ VAmZp]a 5hKݠPė*Dq j+s?wiLfNsf<眙LzV)рI'Y|?|ajH7b\愢 {0qo mH*jģfF?S!BɍeYtZjRXx;W;;;;::9cYԩSnM嶶_@Q>ޘv#B+Foٝnmv{Uѭ33LK3g>h0AJ[V~.ƴE}SN/n|o^곌}cu?ګw79lV4mU 鈍y?Wv#zaؚ%Ee͵k#Kj+*}޻d>[Qyg|!-*OL&g-)u-Z2KNd2:bR[eη-]*-^y{rAd2]7=]?k+Lw]%etrb \1!!2uյg!!18![ TjZU`FFB_۝A- B3v}j"dtK^C3e'j֓3PdѮ+ـ>zv+:Oݎx|55?_6Ї_?2o:OaDj▊]LeRĸƐdeA Eٸy˻{U;LR ׇ5/:ZȺEsru3 +O ZhsAeU&5nm9/W=>~֠ nF6]+6m]-A`ھ4yky{=gmU,᱕o?M.uUKVm߸d~42ٽ`ٺd]5ڲݦM_pMUU~t=ys- LaN~YYI]Qyr.Kjb׾o + I_:lޥvt[f7/rUɽǔ]}V֌9W-cl|&p]_˾*;/\WmYYvoҥ{S>%LKZ HU{Ma[~NKs8,Жm{cNWBu>ŗģZ#EV6fh!QL߆],bw9[0Չ#mG6q~7vRo^ sMُƌMN4Z%57pnCMGkNsW]p\M0fb?Xo7if^ Kaڍ{"Ԝ|RfL_d97c͖hl_>Yb q*'$QRe.TgZ#rJmQ:6%.(-DvXAxoO65:3tPbD TYz);XcVb>P\Qq0GiNהt>f:Mb,-C%-=:غ,ZG_Z$)+qXLS&J*.-eԹ_\e݉<i-vyom-,OyS1L%/ gA=Anm}^Cwp ͧސh+U~,? q'ǫyӮC_ "@Œ0(jo _*3c&R1~o79VU~r̩A3Ubp_2h[ ;7FeDS/K^}ۤ5&=}7iNHQ\>p'\}5:mj@^OD8jv﫲Yw[>QZ7>;jCwvԙc(0e,Q_^,0gӭU cKתaAN9~g NǬ;a3|n/e.fLj^lصҕ1eo"тi4efnJ1bj>\2)u8/˥s\}-,py`o>=Ɣϗ+j,< #0Όaj _f=ܟ*0| Q> 'HA>74b q絑AFPhWI)gH2$R)!@,&kMAKH*(דkCY 5eyfID(:`: %`nvM_Bd@̶m*3cN&R P~03;v PɄD+%SI 2VF]2#C~M REz@W MZokb'9n[Zqqs}wvVu5mpهU<8J3߼O[|[D36ۦNM}׷G΍\}pӫѻpakYDcg^4g_ámڸ(wԉWHw>Xؚ(.|.ֲ+y榙8Gb5Ig&0|0 @=w#Mp۸hR(p}$/}ڍM37̸Jگy?!:@NC|ξ [!k@kR\ !)cJ@u] 15$Tr"Nkb9IZ!a^ hڊ( 4VL ىH`;GvU peLFXsYfza%n-JNDXaɞN+4'3uQ-#^K,Zpi-B}tj{'=N4]Nix$A$|ݵ>mu`}71t٩R{׵nmaQOqOtxhK4nO Mou-bKJ3 y^ٚn|yPcwPKT<&{`'x>ѕ:bB kںo8֍nrX],)bߥcW-N,ϟ("Z\u]Dcg/;^[tmvG\al-瘦ppwm]}ۜ$=& fnz$=* __8sxARgouG={,h8Tn2u}5{`c@Ragzᝠ$T(H8Kw'lGlD h?y4/1,ny`-L}I EAfm?PϾ* oV/Un`r-[f7v 1QR{VYe < IDATJg`\ 9h(A` 2^%J9/Nk0IJa#pUk :XZ@R5wݺXͬ@@"2rB1LEqj&ն釜\p"R /*(Q eZq|ZLp^;q2B}uu{8N6Gd;eަ>;Duө6hkӼNK+@Ž%-{k[O7V6@)yھ.I<;@Ƴ;/r7F:c!"0^p`W- Uh쫾G>15ۗ$ﺍҊƾ:ȹ-ֲog=/,9v8RTnr`I|jGv;'R6'K}wu$|h:|5fJ`ӾΛ;YӮ%E|$S{QcZg)noxVךi{%k}dbw~Ldwb-88sDؙ~O%IJD=#mc;/FDap`ԩ7-ɫjóg_}+=Wsҵ[ }-k+߻'ɱ/ʻvHYXfG}83 uwv ?l}+}1d`LU^ CmW˜_^w]sMk?Kk%{}zW["\SpJ܄R\3,KT-,c(>a#DKըS; [w^:O.̹0Є'~XSNgM9 qNwtiݼ!~c5l*x~SMwƆeJ9@.X I'–jm|skR_,U <,r骽c6<(+5.]% ^M]cY6Rpf REy3%*1,t"7iNxHz5&-OC`u[9|]w8~+}߈iGmM?l0՝:s-UFf.xwJs׊޻3I0s,_f8:#\tD'bv x'j#1ʐ ?yk5U'x_fX ^u~aAWأ;48+]CL6 Z~Nr|ź&8eٚپ`ZW|R `yo]~Ϊ97i9q_@Y`ʿ;~`fK-KPsanKYGm ?l{ֶ{j򰱷^eo_N>lc BJNDtLYeԙPDV&FMȥ$H/kn(x녫o7d |t:_$F%,YF#%#V.[rufj3ʤi\B!Iu!ae\d[":.e2M* TN]Q )'IG7gq˲N =5jU[/ݘL~޿PVho2%B]gͷSX v͍>0(fk.0^,'l]nK!]FQ*K^+Vj`)5Oj`0:r/l- R- Ra=a˘"Zv GcN۟[maw vncC~5T#럓Umpm%Zi-Bew'&؝M֯sYWYr/!S i? 1IĨ DJEȨܳE!Bݦ]EB!B0ө=G?!Xb= B!~^ZvP)>ՁB!BB!BaZB!B \i-B!B>>>NjiiYV)ZB!ryO>]WWގ  ; ADɘ"B!<!#B!´!B!´!B!´!B!!B!!B!!B!PwkX !B<:4_{cW}$&eoMMO_7hB!o .B~]#?|g9vEk!B!i-zC}XD:3'Yh=g_zWN:l{~_ғW^9b3?%]]g}KԿ)8gl?W~v+ZXhs<+}sGJryH3!B!i-s!}Gg~٬ 6ޞ2n֬fGƟsOg~JIgۙŊ͎$$?#>u@)摏3ӯ$!/$&53uڹ#ϊv5ӯ$Opn B!&W=Ӽ-2ip𡻟<'&0ĠWgQd{$HˇI=>/hcB!´SOc>P{p?:3mKt8HgCyP>yr}CEc;quΑ<w mk:+Yp4߸3t_z$1яbB!B֢qO]pm9o>5% yOm }; wO4֝m;xQs?1ə3`H7vPғO ͨ BXsvhRڽi B!BݵWJIKrl; G ?}a?Xp=Iݞ3Q;9Qh;NBg ЇF6`lqS[@e-)"?Qo(6/B!݆6^/t)Cz-ӹ+4ߏk$~PܖGN#~ç4)/rϝ ƩMxL3#jV:~x%}o$qI }xb奤Nt=Ei2q!B=@O:O?cm @0l0[>5n}<;;;;ܰB!P8qbooo`0H;qNH$)\%nKo^fw~8 k!BOÆ Ü!駟)\nZ#SHQ"X+!BכYa%#ooZtQf~?ԃB!"dB!B"B!B"B!B"B!GF߆f{sK[5ߛy!B֢_F盗/Mېѵצ.]} z)/h\wm?s#SQjsqllƑ-g;z7T楌ЗUkkOuA ,m 2EfkLiLȍd b jEZ{y1MA lUv޻dSb u]w<3YHO<8,$$$V Π31)֛R[CoM[}UF+Rt7萐kk-MQBBBhF{ӕ4-NU<#s{ig)iZW"C|_^1!!2uյg\_7FjVigik!!5ۻՅxFubZYY)fͪ[F~ڇN5>K|1ɸ5kϪBM Yͥ6Yw=}9axFzQ?%xmWWWWWWdg] T`u.EJb_c MWz;Ls^LTyu2eu,0 UNiuŋe%Zl2`uTfei#cHOb>eM\3@.4Tpuٺ܄tQZ]}0Gi/7gO\Y)R^]}0KiN4.Ln7E/-oqd'X 1~nDm;e Z? ]J Zt]ϔQا`=+ x"LkoAkK O^| ƴܕ,mȰ g/M evqzV䬚'Jmt: /m՝=yMqcsO"@ۥR] 5j_^h}oMEG^52'eqMԧMŵ;29˸.ۓw>uii1.7829fr֑Oں?u29zz;N[Vܬ-f7TO?\{9E]LVw]/[ik7SIo 'ۜsGOlYt ʪ~yCC+T̆c[N`{]3m)ӘujLF+U1)eY#4M+cҜlVe㢔4Rjԥ%Ĩ4f]Jj앙qJZr]*C^iM+Vm tXuq2f+heJZ)Sh Vm{VQ%6:gV"Q#˜2LFז54Q +tˬh7j̜Lq9Ve!2Fgfs u^ @cEVd2iTt\^f M Y\iW}L@/d4Q*4;>"L kLQ\7oY$0(0& 'e H1L؍"'Pb)RBjrk*y\BPxL[9+TkRSaCLP19L׮w@tT0-Lf5Ю c7"lf,uLJ]T4F*HIQ)hELV@RЊo_}oJhIDƨ5h˼l1/N!(us^S1ZUV7)]#u)%bc%Jյ7iSWV2zd*Ecrvad4 cȌQh2. Mt"3NP(hJ5߯LkkPʑ'_t@DZ[.'a ':|fI6\ɰ/2x̟8vn9yc$<.|&Z.p+V2{ ώs%N,W $kbV B_%y c{f^ZqZ&S<}ت׆^gS<8a3}BȿF{6=xpԟX%D)_Ͼ3iUIcٟAvp[c5wO^{pAקJMIOy7cZ<=Wq g/ }h 5Mz66q+pFTK3ڕ f UZ锜]A,[,ŤbmN7J3tҜ({vkWT$+>k,0uz]FX irL2MLׇI,ϩ(MF]b/"Yo_70e=+^23 Az^_$1*5f&B3tz^OL52s&]SWZ@EzJY#APYϡ)ְ^S5u,AP&P֛caeWFkUH)0tXZ4RJu5#"$6mٕ^h+A%ʔ ב*+?VlAoBE k=BAg2̜H*p}SvuLQ4GTAP4Ok HIdZFBKaHBJJ+PY؜8Sjp5Y\dFvOx[ۙAvOV97ϧ1Ŀ%U>qf2FI WAi)՗E9rsڔ\&TUbiy>R\J̒[5Dt4T?#7e5kJ'+*(I~֪$V2TbMBl^߳*KϭcE{IG4eRX&K}3čVS芏i-BwoK??£=i!p3OO7h{t?v}L5F8l7΄n6}&Xq酟JHl:ئՙRA=54qbuZy.Sq٬*M-cz;ŬLSKI8+ ! e{98;ǙV:X/^lLOȾ 0}!X,, Mc. PҤ^^ IO]@2{}I$)r7 Cymd1URYVCM˧IZ+c:dE E=kCY 5eyfID(:`: %`nvM_Bd@̶md7f'fYj)@K?P @LdBRuD3:Ad!)+~-BeF\p+Y>ڽA\K,.[UJ$V%I4bu+s c3sˬr3I,"BY4=.V <ÿC"m^-MqeOG IDAT Pse&!)I5k5qYȬyqZ 4V(!W~M(A9#0u VrYCZ leEt_ ʱt_ IJD@!qZL" c'4mEF+ su&pD$#o&),, &ZU`.L/-\ZvÒ=#WhN f3[:mGXfm9 } Z?<"vC/~F TQ#vm٧g ى]s:y x_K-ߌ ȝÃ7~Aݸ]aO^pf&`by4r, AACߞ%Egu-bs|qepM;epv y,tʋ,cvBl]n!`xknft" 8Jz Qt.Aߥc+ 1 LEbubRd1Ƃeq=b]˒BpnN{/n ؂( 1EÐp|dpVnYX+R%󣳄P@RH=S[fl7%m* y֍V7R̪^`AW JHJBGGJ s 6up[Wv\ ;aH*$0Z HKSKzY_V'+$$:+*M c5/'*CɸRvC!c(=2GSg`\ 9h(An,cvER΋ `n8ihm;LEJۡ 5)$% ADFN6F(NRV:ݤV@@5sЀuN@ :kY[{ 9,%(_?47˺o0q<=!kR'@ۥҊJ5iNnE6{EϐmN=;[gs/5\I/&|~c#w 7}^Mn/Ͽ@qE?Jbk^R"Pܷ|5cUQ PՒkK"VX5  2`Xeli$E;wZ)d:@)&+uҬe&%NTOVEB<(@\a^>fH E@ ƨ'.S媻:ʴ鲕@@(SVKIYI8E. iLJ]w(Ő/.^7"HE御YE ᚴ ]vB! ILoIg^׵Bdl>8C&g=^H):"`?RNV(HNtkea1b  .%DedVfFf(FCl?|%?bk lDzCu] Ȃ>h&9Aд1j&oK Q K 6EbxIY`nҤE+/;A|Ef5j)BM=&)+>%u,a4価X)6mJs }9cOOdR!!YP*$j@ejTDV&FMȥ$H/$$r/{pum)Y*_=IQ kHɈhVr\!2)E7}e8>FPD:AmHX%KL`J&ji4@JIi3Ο?iLk]Mkͯՙֆ[QgZny7%ax~Ξ*jygn. ,UY]).UU;YCZ9&=ܗ9OO(ͣ) 12 RXZɴ,EVkL~v<===oi1Ew):u:3ۻ|331-mt!VŬMBDsxJsmIwnjG}a1Ŀ!?F6&J+% J+i-B3u1Էꢴ &G`u t˔֠w?oAˌzC be!r_b =PO !F<)V5!Hx]|Μ8e"dB!nS.Beĉ2Կ-VB!BNc B!´!B!´!B!´!B!~QKK!B!K<#NjiiYGyg}+!B!$lkoo nA  6 ZB!%<<=<IB^6I^bssC_mH?q|B!BZtd7=:ᵅovx[76Y=~ryZ3/IA>B!趵9}|?~Y .ڱkk '5B=n}Guiu1_[`z#Zwś0ne_47`ٳI^͵ڝu#F.B!BGF=Z[``{z3YZxVX֫?lIc{O /KRw~ШP_/!q|kp8庯϶x^s7mϛoʛ;\uLsO͙"B!.}xyo-=e'~ʴ'kk2vhk biA{Q|o-/UÞ=?{ę.{H55Y][UÞBLZ$n%~UkB]F"GUG*XBK5K]"kn-q *-3$ (Tm?^8?癙~~t2}Y) >=g_Uώ-B!B5!@_[ܚ='ض'v_}㋝Ynb93 |jgSΎ. v#׎.q#Tr,'Ǜ]3'y:*8cl اZ4ctHnr>kx›0j3|_|2vX,!B=`YSaxh?V oܤ3ž_hw;( 3KtApӞG[^(_**j16{Ns>qOku^ \mH[asU4c֝4#A< 7o޸q8e/\~Iq B!+A`X՟r lҏ`XB!a- q~cS;t6~1:XE!B ?8q5*(vFKIEΪ}ޣb1QB!BBF BF!B/B!B葂a-B!Ba "B!Gwwnooox(B!B!t.^8bĈ@__P}32l .cZ8!B!<۟yi D"Q{{}a-B!B0"__a-B!Ba ZB!B"B!B"B!B"B!z\wk㡳\WGRQvB!{X[WWrCɮV|ooš$;k <ף}~~ڳ lyϓߊp_mGN{By{ہrw5*71x?^eiuq?(jIirǽǭ. Ae!+#e-.v8Uriƚ[۞E-Z=+.CnrA텆ِY5s=YYy#S߯ɓQLKrk`mex&(dexQAV,wƴ.4&%{JroIӔٲ2m k#""7o޸q8e/\c̴οíwɫ&q>!mz@_7W?dGi-]q<8|Kuj”{&k/3v3]w~ _[~;9cgY'qw@xQ_X3:x$JkXKӊع rj* "vcvV,NU(k:i¹ E¶S k=~aB<ű Z.Y{Q`N.P(bś`?P؏,Vn4*b"8e?@'{G脃y~ԝ1 'p؏mZC4M+殨$HFBK?coktb\M4pS[pKWXǩןb.ìySL̊Ҍ scA4fzb7?iOtbW̥V$۴>F z! ^Q7HaDthv v/ʚL∙`?lE#D-T&;4D%gٳ]QgStn6kFeiBzEoH)R(7/ [Hb҅sc1 ?hm!c~ _ed`0t1cF_:x /5B؜}/lAi+I+ *J1-L:N*mEM#<|G)acy ' ׺:['? ^$xaN2ǭeH^r ['tuں/緒iF@W^+^t}'E=7`_lryk8s}i?/%?!boxos [#ֿ >xw%!]y7~g;_=x3uL`/ώp23g~Hi4ݩ9%:XHDtu̜1y~ Qosjvg|aj(&@8sAuʕAD̈g15DF͟U[|8%R~  TF6+BTPD8۷g'$\U7Z X3MZo2昳N y\k=X-x&/`*t"ruf͌@EM!?9S(NߢӮBߗ/&@1]Ga9|<8?w(qloz 6;'!dt[ 6:o]:xDfG2WdDeb呪YKܵ9%SH87 p,7ำur|MP;opmvifl1? "Hޭ.`kwM H@ {ÅYY`fޙ7B]zu޺7ΞYm'/;n^솨o'r[{uř' Vu^8DC$<,]к3Ag!~sc (~|Ϣ<.;;9"gQF aԂ)S g WLzOx .'9'ݧq. "pn{W+E޸OŶ[q.xA.kޠVl)  qMխIսɹyњ)$<ERB487Gpv0{4ٶmOY -anMGmhv&.1]0\oAGM>x@\ ;<`97o0/XU"6 pi;-˨/ߝ5V_X?ٓ,+(-]`9[v[b^ s n9?BHavtB8isكu+n]<ւv A 荫bBp"Q\m !z_@p6(s&`А T+dr:P^9[67^m9E:3"X[wx% yZX>/rBz|WOLT-j/8?cDoYoN|~$3>thқ#o]u7<g-n烗DHdЦ ^m߿o(8v»%;/?wuNC ":xafFO:j߮uęNFu9'DHz y"`QyIz7{{7ERbr(# D 5\ O'zb^w0A<`{6 xUmJ[RV8ǧ1oaS~e)dqjm' &l>"#e !xyvkΜ.-*ߓ:O) BBx&gOs98ϻSYsK7*ڽ52;q2z"۷pڝ.{&UJoI.kC?`]<`r?k"i¯uBs9>3tC>>'Ɛ<;sh&>9s/o^n޾L\y*9;+Cܻ/t` FH6]l-9U"Mw½-Z,/"v:]QzP+?l2 24׹(`;3 efM9QJ>82n)o9Ko[b3V 4*m8OGbqB;!qTϘiGݶ8?uG #{^;{9. փn1":O`?>aft!J\m!c\=IJ6ߺLMڿ%{ktp㥧\]̨p|:j;MQN߰r{ $T} #n{۴}^N@ {̩=4gNq =˖ZaEE0n%oὺE{1759H(';_5vhg/jv`l;yrj:HZW|x x#|{@;=8tgF̉~Wxk}>Y"{GGn0PTui~_=:ˍU?{W?o?wk3[𹾱Ӎ׉ԣZE+S"͌I׍j xo%^<{j{J aK, &13^rBЛY $@ĚoJPlv`ct۴me`It`њL:\FjR- ZR=W(DEǁNh])NrGK'D-]: _ՕkWXɹQ @ ^5$a3p<͂! 1WFDw;lz%'o򒍳V" $m_%F(1l&51go& ^^A\ޛgm_!.Y[L2lΝ5ހQͳ^^ `JU  xΒ8lM’ɀQD~;gvNc/c,?97oKƽ`0 qjφ"o _r֪k{&o ^eAFbf'zcbLPo>^lR0IÖoY>cafo] ם kݟҺfw%^ΌNd{M1@f6ob7 DLŘCUpS$./͢IV@iyqPg5GQyBL Jj^ RTuI,D(ŧ.YF/37I%_bEZ^z#UYr)tR Щi u)4) QBrCvjfHbU˲Aŀ߭ -pw/7iT]3 0?ľaqlzOTGss~Aa ? g6:SOGlc,6L2a&y^Ule&kʜwqF㐤h /co-z V/ Vc: yG4Z=chNkEVIy<#EasG@{ӟ>yu)ziƼ9Ň{Z߼>ԼIu!}? B!4pwKOVQsk}FOzJ'8>u ͇?ԞQ'u@gkù.8޵d Rޙ!5|ay b|]>[t/Xo\aҼف̧?6 9?e4AWd\svÎ{B!45… Mf57Ett;["- ~5lya/Q=wpyZd,Yz7d0@=/s FzBiwKy\g:;;{ah?}@N|wlwv椼< !B o^^^baxQ!_WշxOt\;oSM{S| =z"ßN_H85~vZYt$=X\a`"=!tINژM?W݊cvR4ƴ!Bte^F>;c~x;WW||`ϧԸףC'^ r#쮊%j/ ';}{&aNs>~P%97b'N{;p]7>tFt(+!B!{eas_is5v8_M4ݼyƍDZ,{…{q!B=VLB!B!B!a5=Zp2B!B~9~/b" B!X?~6!B k[\2{k 6eܰCo3?s={cuq\?(gcֱv뎔&ݷ+O:Ⱔ[ZX 6T2$IZs<)֘&P㵶A6e+4=%[Jk~aX-O4_m?tܥ1CCe~1%+塡:DT塡rebf{rǽh{lrZf,NCCVj{oWdU;ckOib y*V*iS,,Y> gܞSӌ,[FKLA mny1sZGQuWSE>׿P>ktY_g^^\ӗ_ Y<^ڭ=bo:(eI'DP5!y2rx.c,9MHZf(*6!i:"C"xܜs՗~g0-ũ}Hvhc{+9Hv8MdI-p%$l5WEGbambaѥk*IcNzR73''hS%ЦK}eoJQjY_S-"< uu ʿ"TVj&s¸}i}KSnF/(gdlPG> MFF#3F*2?5#Ԭs5]tY[OSI˭vo~ٌs]iS3OWX~ ZvT~r[0A>Z+v]h]OWXGOϨA߫>Muq]0fkJ,:5|{N-kbڷ~prٙǶm)KSJpjy*iZnFghUfq^Zr3kLD ke&*h&ѐ塡|XiZIӪdwMH<6]2yx*3?M%Wj6mWM/׶3?՝<` \.ie4Jڝ*9U[}I0>))1Tlh[$ǫ*KJV)ik?YErUj\6TJU|Zs?3UZM^%z:t9PH,'%I Fgo4MrZճ7s;#SZU;>0ƼDV˲U*Ѵ%J%TkkMZ^羸՞ESkbk׍ώhfKj/٭[ˌ^7OeQYN}=ՌC7t:=I_ꮯ?djߡzé^QE@@5w󂋓Bbտ]Z׿꾬[3qk6s'p ^Ui0jru 72]Y[:Ӭ-m Rc 555<3s74m_S72ӊ!`1V Oh4Z٩"mɳBu,l mT-Rf0j93=&cY:\___'kڲd֘g έ1 U XS^j9%_6TAլ腫*kj "o9['>=@tu}We /?3_?7QkR׽UeǺf(]՜\H4OoAO{b]u_R~G'>ID=qjO%G{;5oW5+LP !?<;::{ֳ|UG?qԳ5 /]>o+}o)mxW)/Oq7}4KO=Y1 "C$E< 2vX "B.N uA$ՐJzvX\@ z*&Xǹ "pnmQHӷ7xO4mҖ~WݯI_rۗ'AzpZn8op^r_ Y%~Lx ;8OAHS*?Yn$<0:O-ks`7`\ / n2/m)6NLO?Ǥ*'% e?^0uڲ2>U3mi9!DV/`cRYۭa+/n`d4p.z2vPwzzs#$) #^e;eu{=8.K ފHX _u݀ic{; DOM.xx?x>#zҷvDG_$|ڼ_pJ)Nѷ _nR) BVŚ]-*ӄ89\Ι趆rjJI/„J]j|n}]^+88<)/SNȤtȭ>\owV !{P$&92 c&Jcv/ LW(_~R&1$ܹ/ e K{@k$\nɬ-S2u:ax\nZ Z㹖H ^6k欳dw%1撜A>-'11NLpyiΌkH,$846(Լ-5꒴YtP"O]6U]|_fnF3K#Ċ4F?{wę/[_2$lkR5` 鮄:q|LJ\5-tW8P[-FU Z +qnӵZRyZ3\$kF}%Ma =|hRn7%o?T9Ly޳v]yziB聉x*tI ) 3%A,,`U8Ĵ!ɬܙ$NFpڟO[g6_y:0QBu$bboߚt[} QimY^KʕiVYjNU@Um2B!BwNBԻB!B=0E!B!a-B!B!a-B!B!a-B!B#1/`XB!BG?QWWŋ1) =Ϗ~!B!d2n-Dɓ'cXB!B"$$dڴiB!BZB!BZB!BZB!B"B!B"B!B"B!BC֢ɓ'1B!A:u*1i$LB!~p2B!B! kB!B! kB!B! kB!BaXB!BaXB!BaXB!B [B!BA~?cjb8$$ZB!kO1bDDDĘ1c05nW*f CKf˼ߟI IO|lnʻ%zY#;gʵI'km\fxR\;􍇚{/oHмvσsnHP4v~^|wۼyPa(]7\lJ.W<v@T*wv2mKZR 5z!w_U{&!e~4*u6nX@T*K{6nxmaV7^ºvi~﵅MB&)bc?w~f⫝gG^?iN K]KY-7|8R=f9\}VS]ۼ9"2jS$;(\״siu[]fnSTiZk{oYzΎ7G7fn-,)kr",A2$'G9)f&6o?8WʷW#OJjJ=Cxgpʎ77vdwg.3rַ6|{uzjՃ'.4!\MMx1ܒ^ŏ|A MvVEjTnpWETY-6HfnO$籕egT(,Qw">>dnTb!OL a[gwOcǿwqYxlTq׶pȉ7DbxjIq?xqR8`7w'lN4R{/| mژ@*?.yo,vх^pjY,<]  ddL4'f[ZyR^HA/Ǐ޳>xvD13`qH ge.k'99 2oD/%<1y$y\1y8g;LՁ '=l+SNK|6:8ܜm}‰{;H篘-&wtd5zKzk{jNQpmkv z( 9嵜cg|/z5`۪s7T!7V{,K,3!'MUoGQEȵ ޶XԦ3$o 4˘)U D6wc>Btժ6nr@L|~i~AG¹@9us#V4GŴVT-%)͌zW=capc IDAT|Ң%r]09~a7+]5Wz{YJj n{痮OmߵnݎgRh~gٖL.ݼZ&oy7  L&y z{ط6`=X5Veߍ5nwZz??i#.z5ڷ= LZ"3ɫj1 @FQ vA,(|zN$-yET |{>P3Z!,^JQϨްǹ 9|kߑZu>ߴ8y?}S;EROAӥWoWzH8“#)qz"1ϐe{e3Gu6w}8t{}MR,~a;6R:1-e㡜˲XfeJAAR Ih9t%ʚRriI[tD}y -E&]m>lȐXJMՙ eYk,y*. )&X 1UFw6f%lgT{iv%,6a$hv(bf)n[ɪρ@ZF/{ "p2BR0gڈ|pfJQzRo韽]}W>=x|=N;']di׿RdzY Ϟc^cayhp#497j'Iϕ4 ̾מIu?@#'ɴ_A;_|ȪD}r x>}qIQ^l=6i"{Ӊ0r/4뒦W2eڙ/˿\#*CQ+Yܙӫy7-RI}_?tǦW{L ޝ3?g8'YR>n ?UZaRusm {;g,[lmj\cvu9հ97w prL-9+6У gZ[uCg?'s/}1-3$?y&fj)>eA mi,4444ޖ2oyscE憆jisN~;L3)˟j/lwK^7kUCSQtuM̐i$]mZmm,ܑ=w{j5-붴q:?_9L[Lv= Y 9,\O5ov+㗈:3p7z A m KVk}揹;XDzfO;^m%3_آعW:wp/<>JshnRr\uaDNN7/!  z:\.&oPϱvHpx㲅&a٬7EV)X'|N./ZIiZRMӚm+8>Zy3nSIO5󋋶\1s ,T'XmG9baF)r燶@hvqT&2ex֝I9M';h-D~L4}燛_EU'ksg˶:Vr8GivQk,׹ u9ŘmS%QgZպ/j((5Z}ڕ:/O ╵RҬ ='Ys#:1=R,yfIw*㫛{nX Kճ9F`℩!O^Ǧ)B;}? c$մGgzy='TL񁷵% ( }M1#9Х.?X׹0&*Ԍ,x99[sG=3[ܱm{ ,U|w@;}Uq@,|Hu$t2D &|m{Nƻg8_&GQ@E'/عmZ]l)jR5k׵xQ??2Pjv=eA*2>lLsah s7Y̫)R~{qz"*<K,J>oc acG0dtŲi8oB@e=!zqW6ޟ.󽅪Ymѷb~='E4 <At6"xO.lM Ցlk*6qM[ԊpLOɹ$i~$%@kaj%Oo/C$2<)VÓZBIks5Z<ABL!cY]#_-Y;?n4P#L>ٗkAwJ>sέ[Èq!%w5^e+gߚ{I^. 2'FqcC$9.|0}om:nTFBِRufLynw񳢏V.; . |!u8_n̵}HJ*6@nQ=@D zfD&Hsdž^-`"X/ /R_V}~ۼ.XUU5[Ln 'M^ Mi^*ϯ6o&GMu]))Շh5>/ˁ P0B@H>wz>~s}[|Qzabݕ"d C| Kf0il&lpbcҪ%Ru;5#9{,Z7wX1떭s/T€giHJDp*'8XSRsVS6)O~ѡ@/?q)7z~1:z"Ћ]\Ĥ#::_Ec8/w= WCG]l<_D4/.?ǞS4~np=YW C{v\$WtWy[G3M{9`[#q\:=oK;wʙ]mq;Z)]5GM{O cgCku5 ;9pm{^Zݹq]#]H=ǼpΦx*z~dρV&0ۓZ3=@,lc<tz-%7"iX4^-<#8gi~ ÜLsa\VX=G|40y1=SԞ9أ{!Q}M<q]ʲ1l)\ p_xwj YIRԌX-ԂA0<(eŌ\m[ n [s׵Ǘ%^Jw^!m?-p7;kߒCp =pslێ.{%*r-TNJ0M[hc-&Xgks;y䦾%( -J3Ḿkln+;Df?n9oUŏ|߶JTz++$S Sj.pKQJZ쭳8n:kuIB\؝, sUxti75x8WMZ dڕyQVcX/Odfs R(s5T9|;iJ8y$xf,w~@'d0f^z#&Kr:ޢKNȘ+\xxYb!z>(|N>]ۭrR͞4ϧ [c+ƍxQx~wBBǏU/_x/q0ʔ/mø?C/EW "tBƋ?<3zc }VXN >h77W |RśWysstǮش"&澴}"TNW%ېAQ\^ъ"Ĝu7?,35˗G_EDEu5@"_zQrSuMB8:y[&'z`Yߺ!^9IVl~kck<g$^:Ym>6q׵C9mMX!c{3_Z>#h @aGKǼgxwE7Es;s7&X\T+"˟s^E(__vfny-WZV| 'Sp_cv oxaӱqRϯXKNE-Y6ގ"ݢn^m}wa?KӇ.)*{/ez@0Ec)mm3Qc|kt5]L36YFiinkڍ=0rd)x7vULeUy[]6ksڼT d[e+6@#WX wWoW˗o^]S_\T@ ~ku!U~uCeݸ޽Z{n76E$f1fp>lQ$@*K,rӴUVI y-I*(*Q#%n4zA٦wR }U"J,S&ed4zݨ 8h\ZRMVI"ʕyEk 5eZV+茬{ڴRL:-oMJӮ/h_e,)Ӭ,x0 qWW^r3fǰ!B!C ZB!B"B!B"B!B"B!֢+cǎ´A!B衆яh9~w 9?O߹Gr>/\qxw3]Iw{>W_} \9C--ۼO6oE>6uKFUzvX2Tڒ!Ns3UA<)ΞP̞[|Q<mL m8W^e]XͶ%uZ3T11ϽW<֒4*&&&g8:j*&&F5{!w_ |SMʳ~mLcb+,Uz+O}A`+qsp֒4Mk)bc?w?ߦt>p=Q_ύE/Ӯ_>3t~k͉MV1{~|g{, >~1OR3jOOsQeʻO fۗ?i*J[yD g@01`|8RW$$-y;}z~VT:W&HEVH,wP`r_6[UalPrkO IDATWҲ+$Œ2_S6iUBQ YlyeAA,IƵ}Z^]WU'iiY s -TRR`Td:u 6VXghnf^"/+-id(<É?͊\- RK%mQFaD}{b?Y|eh7YUFff;wUHUEAPbZkv-ܹAp[IZvFrP`=aŋ ||P%Q}a%}w}WSe? Ĵ=;^Ù3~a9?W?No^,>b{T`TJM3}}m|/J7 >?S6moxY':,xa(=2m:L]}}-EmJ̫8g߳]ztp$H٨Hui#Iyߞ| 棩N]u?8r_!2ld{cs`I:jt*\Ii -Ud%h:)a)0u4Tbg8%Ϡ$MGbbi`G2COӴuiw@낣K+x{9[+ҩFǜ$݁,$66c߱`JR도sYIZ:ײ3.U&NJM5D8*p-%ƴ$etԩ Y$NKLyn[iVbTDžT*Jk(v֬W69uj\E<;7t: ڬJcZ^Gzc|h%9drE2b ?iRtSW]IymrÚ,ZBE%"$k"(RB'( z9x>56Фkd$Ч+s@ke[TӬLTPT|R Z#.sс,q@NGQL1r7}TzJ_Zakif1%鵴6r+G>ohܟ*2pl2M,tc~x5-|umn" %TbF4u{c;M:J4^hK Zեt鶕Z-ͮu oL{Y/\$rtph272#zkl~F#a$\9{&I>_{zM#eD.xZ=mi5 4KeDWFKw7w_/#r ::=.|YǵO닼hiWEˈޗBLRh(P#Go䔚7uVv;p*]YkZ&{[[z _cW(ŏ|˝:3Y)QD͐UI([w&*+:< 6Rvi7:h1;q%KvHRXP[5β†nUdFv\*4 tx,lX/<3Z}TCaE\GBe׮5yy_el~ʖfUrj%:o"4~p.zƅ. ԫB0R3ǙS0'aS ;A+ P0.]u} )&4_<< |Z<+I.0\V8r/9Jp/+Fc_\L2"2kVXS830c8ʼnU )r J]")5r2o`xlV8IilKC@(S42襌pNK LPNd7yt銇`!Ӓ[ǒvҥ %%w=t^ 8E[ 0(q$k7\\*$Oƹ,yAA4N}ZK(eLd/4@QS֎zM TVI8ܐi.Z5JȪ=Xi ż3<[aB,2)zщغlfCRYP_N7u1) yf&F yes.1$*ub^x1d)oK_s $<qxV]Pk֮Q8 3ʾ߂Zʮ ,a(NC)lra~\fcՋ1* J@gݼ۔mBL$)RF% ,,61ej`&\ O{ ^YbBY(cy+Ne$@%ez:ײ:Lw˨**]EPt, @T qkmaqadag`abeeDzi #jׅ@nng#0RK DκQb0:QXm<'LW\nY<4k14'~vesf6o)$d?B.u]8R'_7 =6 !nC9 L/Ԓ<\%&_ޫ윷9ӽWdetxYS66K5|za{l݅g"|{Syȃ!@HA]y˳,M$% ^˒|HHq#x;u7L0pL`&9%J (yYV𤢿X|,M$A Xt턇l1,Ϙ^ZEqCD4nySy.apVd,6UJ n^:3ոcX}./ †89Ge˼B]'S{zB?͏F\Ko=DvJ^7Ze+whee9κZ*I|y749٘V$Vzb*Paƫ@7TdJ;q*.?r8J2 P[:('Te+ٻ4cf ݶW2e7Pe&kQ28rF`cGwJVĉ.yMt"H&I 24$#")֯ ×_0X)N@TXaq@ O$VAP?PRhjp3n>"PBA"8ѕD͋$,wϕ|]A1R0]k]]@\Mb)_֮YY\E wWK6.?D=1-2QɔVB_A0ēZS$#si8Ҡv-қ;y5V`q:eNI*,QzIr `"iH`](Jv,ˈvCH %]ta?d]!WH "{SP80.Ls0.ۖmbVdJ(k߻d޳.xvXb8S.Va23|&Wy`?._]ZǗOt8yctt1ϵ;s qsw!?K;n'&O>;0gi"Nm0`l ܏$*Ui .+%l̺_29)LDsA$c dܙ"{Ept$`Z"уZ:Abpc59묌4 cXi)9D[L/gZSd(q0,ar1y/ϞP+X x؟L) q>Ske 0<(eKE%PZro [81GM Ud<2ve^Xbc9"趙< b\ U%Ok|sp<4*6 8ZuS_^t;iλ>9įbdEu^~6I)wV}QӦhZ<U/+.*|zJQ3u'JB$7c|߸WϿ1Ǧ<=y#FK>%\2g'67!E5#H]. zqOFLQ>[1om'^>q_(RI#٤݆ pyZ0 D%nUԓ2FMӟ=[`g i/)e(efq @ixEr"ze& {ө 穲y+KU"BvI$Ӎ)ΌMiFYJ~][Xeo87xQ#Q3xLx=[E 5Hj R!_6V޼b__ZKFRqwmI ky4.$OEt+֖)ژnj|^YP__R|?hLU𢨅k 2:_˞ N6#BVIoK|~]ZV^DUG _ ۨ{A4лw vKhDv :vF }=}< If9sLV5ӘҐ:SkSt*溹ZglxY-2V &5k ˚W-ws -ҙқQε]_4i2zBgӜY3ջ3W"Vg*DD-k,z^ bպCu׽o[vK.uwwwwwK$b{{{||oG{ޮ^!NEZaT Ct5+T{Vm9ϡՔZg;Eg~TlTރ]z bU<,tj4k3~~ X? 6>oAUz=߸Qpg<%e>>$cM3fPŨYQ捷h&ZMV]T6Qç -(jlfq{Je!r[bT:s_uSJ5C4SԭNg,!⢒r hm!Ðgn.BFI0!Hk!IPC… aaaHkģ>~Ő$>裷)n-@Zvݭ(;t:rI܉'|0`2 @Z  @Z 0n- cNB!|L<i-<8&NB"d@Z i- @Z `ZnwܹnƐ?~B CZ pΜ93r1cƠ4nh4w $|' ܹs&MBN;TQ*ΝO!YИ1cnw8ZƐZHk[ u tu 8nܘ(a g_4fCΖs6i97X:aGk=="*s.pwv:o뾋#so׼x[ohT= ŇANq9%b/^_x?شzWIMC{ksV˹=O~q~_~) ]WBr^]w OIVDe:^InP;jo\*KJ;rN,IV-wwx䞝lǾ^R^HD߾4YKJ%]9DT1MqqZo9-|\~ G2f(:̼KDv!.}ƝӬBksr,:{l-?y[/gl@^񉪝{l(ӷ_Ȭ7eޓsn;?" ]{ҩO~N?8<3DD]'o7}?wύ&W^z_"}i͵`Si;?-Dמm?xo.dRMe y݂{9 V7Tg^+-ZyOWo7s-H%/S/~͕$'!"Ku<哗W%4/RaP.?yoe1}]x Cg=;6Lx pՊ̒:;oɽ'ox$unmvr}r3MS6_Wy5Ilٱ|K3uuuֵU~ ) S}{zKV:}$¦YK^!"bܢ߽WiZӖ#s]}uKg 'Qc5箿= s%N#"&Ǩ]v'3DĄAB TH" vȾjŸ7+Fs{?QOIs&~ƸD$AD<+9{3#FD᪈ԟ8'%"3%k<6ar"bfy`M=#p MOOb GD*q^dHL]bبl:)/ vTemZ"ŋkZ7{Ӓa|1Aq\iIE `gTH9]+D6٬DMIΌLiF_OΒC/-ለ m=#L_ GM] 3”`(=3=t+_Lb8i;&XqDf]3c~Y]BKD $o 1SYRS%S= MIailszt)^rvAOOʓ0oʀg9tbنޅk{v!- ˦tKhzsEgeX$w3$u$6TUҲe[7juM1־lĢ'3 Yv[Bf{|Dܴ&3hy~fzaBյzf&$$DD|y n1YuIGḵ,r~w|!AFv6*&جhsM"啂/"VmH0DD@<%]Qw|nPo,#*;8[ 0}r[U^?Y|]aa sa;.D<+?z'O@?vd2swDO}k`+)e]DWn;swnms>}+-^J4OumƗ\h&OVz#2x̄/6iԨЎ {,lĩf@LEe2H|:VnWϛ`Ȯ"# qrYo'#X&U&(3,IGEvdQ!jXܖ$lt RtáP|J{ xo"iIݷM{wپxi-zStrQ'Hu6@$[*>ۙ>Poq=d4Y2?SȨ1 1\xOyd䓤˼7 m+z}oO$"t0CNwq┍'DkwΑf#ܕ"M*GDϾouNp$Kr$?BX%$Oɓ]WTPF{`0О3r!"xJL -ɕxUFb[H?ϮQTҬZp`xlC26,˨xo:Mnݗ;{څ_wfd,;6}}|6FxuatuGƎF]]R(vZXz S Le_r*5D\IQ';$Sp2y8ӓ F.ڵ)vm?p۟Ǣ$bI0,c&T+;>oU%b%q!SL_vB"˞UX)d3kۦk %z5|{*?% ")( 'IBgO.' 瘀'Y|L%U.ؔXζSrbxVO20n6Xn3/nbl8Ŗ;|**X[ 9vnR$nv_8֊69rT{w8˘O$ѵgEk&L(w'ΒD$xNvOQ]'#ULod$SĥJbU^V6qD\&!5i )xp򩳧3ڄ9~s *FC&U/Q)9C B(!:eXC_ D}ݩeA'˰:RW_3W{Q}.K5MAӬ%b99#{nnQ bIVH_Vs˸&5%oqࠁs$+ˢVTU*"w ek|S ^;[0{9Iv.(:]Dt3חg\5WQcq'uuN4s+`_}oO]8wh$eI&99v,yh8wΎg>/t;;FOs˽Ұɱ2:NOG8fFO}C&y遪Ry3"1x=RLT$4Uw)ywC{d {'9:tlg23 {SfOHt_dk&m;F%édZ#NKDDܴt >mItY9TweJNIPDBʓ$dϞi9▫"2"bT 'vL뷯<ᯯ0|NB^"aDσ:Hl;Txf7:/-Rdq^ ~"K[_ 0HG$+%18˒$ =YN4ܟM]8=wG=bb :2s) aM@GQϽ5i`N^KbI¾Km{7Rh9# '}_$9eǪ]x# M旮d %% ʤ[/ɢD@yK@DQdz:/Hɢj!Obj%K>DH˵ڜD9@DE%E{fD5w0[J5%ƒo_\|Ϲӭ_RmlVϖ穽"/}Ͻ=9a_IDD(Y>ʼDczu ~oolĪ$qdHD#'=XD~nG\ʌ_yV,4zIpÒ2gMb i/'Ǩ ,7nګm[YeIDbbXhBB̉oyvҔ6,bnZ9M%MŖɗȖɗMHYwpڶRdɔU  ydbID96y6Lq3%+-\V[YsYf/Jlܜo$!Y,-ږ]O]vѠVH s^U$VUy7= eW,M]iCD&Ю'}g~('G(iC[f#J Sm4](<|Q#\_j[ciU+ /lư k{Ïw'̲E+V'c/{oVTFKdL{]{'qyeK2&_%",ڴv:Goڵ񭩑 1Wē{ znE99K[:)/ٴPED}i-mk&T^J׶.Y%qͯ穈DޜU$D-ٶ&gv1iӋ "k[m K*!ƮmѷFڄ銛*loM&gQŨ (c< cXTURdv[2e"J(EMDܔԙeZS1 :dk[j9m˜ZQޝ}g:V%"ooYc֭Wߟ##DC˗.]$I[I s-]{{6㍞axqWby*տb{'C ?}ӎpwMWyn{Qqvn끈mTfF3T[b؜E^zt:Ngll\3ј߸#[| 'qߕNiŽ{-T1;@pVy-ڡiVSUW;U-9 bԺaCKtmOK9eM7>e!r[bT:s_uSJ5C4SԭNg,!⢒r hm!Ðgn.Bj2 Z@Z i-sIօ ܉qƵ_pE1$$IzƍOwkB&Nx̙np:w ǏW(nT*Ei-Z@Z i-09s2yd 4|`2 @Z  @Z 0n-@Hwww{{nƐ&M CZ pn7EGG3qNFo$]V IaJZ~ a3vW#a i- @Z +p܅~?7(a gUJdU`m뫜m7ݟMwo/N-?=tʟTow[eԚjANёøXZSܵYDY{Zyss7;8|}\o=wQk^zVGRA0:{6.X^FsUs*iZ j'ָ[jy}jgy/XDomI4WimqqqZ\ڻj6^T1MqqZo9-W )3]--7щ`ZsMy뮖>?8x1V;?c䘵__VjQ @ IDAT}ٓ)?}?W?~?afFipC 8&KJ;j@ER~1^W'[EBp12untgYk>8~?e" jV(k\*8rJǏP e[U`.suز^wEiL_X{RqӎA֒GKSe5D$8,F*F`ZWv)m͵D栴{&3#O~7xu1΂`}ZN.P jK0x'FUOJ)~؞XIjqMϿOD""i?[QќZܔZ;EDh?T|"u|)7M7ͭ`zMcZ_\WG O:NBZ{G*A FSn[/0<S~p]tyCny9#󩹵Ͷ,E<+CĐ(kšK,2XYW3!U¡HMO7E74;lZcQ%#5ը Y wAk*.6R=owW N&t83 V5d%4/jzSH_ȚQn.kuǰؚכ-F-r!LCyo{ͫ V5OvoQ{e&El_5o#2+4J("& .Dԭ.\J>)7w |S,GĪycy]]Ue,k묭"YZΨtSb9".Tj-G8hE:9]9*>dHt[Lc4GĪ2yWD2(M1nDBIk,2TpVY2Rz^Q> ⛦j^TlmV𪫮&2obsy^ j<LVXWʲYf7K@)R78=SM jJr8Gw;*,eOUuPzam m5CgkpQ(85*73E>issnI>)8'qJ/77 rdy^o{Aۘorγ¬$K*C*4rXuƋn,IRXn}t"J~qdWϣ*)Ncr.1>EޤZ{}wj,k-ҪjkmcPȟ!Se~o+3kp%'a5ozH9#-g{R)1٢fɨV{̋Eg)D457ů f K${%a uN=/I.ϯk6d\ܢ^P3槨yDD:aHV7mF,+3*V7Ut'Y`9~1D%і҆ɟ>HQ.B,1."QSʗQ$1WRYJ;@'DI6'NQ'r̯B&9GJ](N:?zX T\9;a~AW_L=ȈGSiZf >$ĤX"SGRH6Բ}AO=Ȫ[222Er 9se2Vpސ&jW$"3qf.f%@$Ӭ)5D fCΛb:N X_o7(*6wD$!"Цh Z͒ (Gѫz|aͶ㙂̒*ܠ(bJqyp$BIדM>TFdXmIm-wIGmA?8zr,ǐ(FŅDv0=(U U-]EXc7^nuGh8:X^LHBor L hɰ(RE\3ϟ}/1 SD.(b(3)$ ~_iY"HTByvvf ڂ3L~짇>Q᛾?BicGH{2ߌ^JK.IaD6wY4j;I|_!DQOS%Vj%+vR(`2kE 2rP/C+#7%!g( !Hl-wF'θOYQNHؓ2*[zN.gUU, ,jO5jMDUk2+9&~X-vYfMtU '") u/D N*7~J@ptM[Ryznwyq,G.7̭,1 ӤhE:1G(*WUYsANcҊk]Vܗs*So/ikx&XNͧ˜>s'ZjF|uUyo$WG3>OGD$[uBYfz!^4ˈH $қ{CQ^3bU*cTsGr˔B 8-96Qp_wZm2N'״nQ bIVH_Vs˸&5%oqࠁs$+ˢVTUS#w ek|`D-`{OuY~%I}rʧ~Q.Q_>@|0:#?d] ;e"QÎ]D#W}t5~'*Ӱ~Noc:k H56 rsSj *_UDG*{:G(C<9ʃ?>&&p$X̍è$Y->ֺ)/0oP[Pݕ)TY"Z,o rWeZVΐ;Bk;EA{E*$Q~E "uW|:(^3F u DM55DD)8"k/zf[Xpd!"7MpS|u}*P%&eBŤTW\t3I[5ͺ~@z ~B[%'v[UOd'Iᩮ[$ejzK\l OVV98qaJSfזDB7X9]J-\kE[%*~ULW^{tFpW"譫v+t%+|ҭ>旮d %% ʤ[,JA$$A|<묮T1?,J֯΍[($FVDI䱱\A$*DD\TR[sQ4l-tV'&(ƧFhfE.=o$"&ѕ)?%;Y+7T W y-/m\QMt^Rk"Ԛ0Dz$u1}~MOf"Sn~*_@FC\m0W""r6MpYi23c(.#HiX^_0|3e}\И̫gd1g{:麆0Er7jXū ,2SnVYaXݯ%ux91xΚeufEaMDT"]C5<͖HI)*eV|re1ӒښaQWiWp+ډk{Q):y]s,"=l^ǦW'^]oWVYb٨jkD6T;ϟyEVcJ-,/J}vD$Sŧj9gM{eNGlk)nMFFm6vK$I^WhRך|+qDwkHϵ-,*iuqkuQ#w%۝{El}_EhHgv<W@2]im~mg-X7J\b^QŃxA2ư>Y,2V5ȘҐ:SkSt*溹ZglxY-2V &5k ˚W-ws -ҙқQε]_4i2zBgӜY3ջ3W"Vg*DD-k,z^ bպCu׽o[vK.uwwwwwK$b{{{||oG{ޮ^EZ.qsž1c4GG2]0w1՞U[s(k5bљ6ߓ_bz bU<,tj4k3~~ X? 6>oAUz=߸.jeT٬q_~YcX {-T1;@pVy-ڡiVSUW;U-9 b:)9!V?tKDaO<(Zr?6PsYpϲ{-T1AOi湯:)E!UV۳SgqQIa6aܳEhh!{@TƐZHk$I(!t…0wGmoopbHHz}~ DVnp:w r|ҤIHkDXXؓO>r^ @Z Hk ~SNS&O'ND!|a2 @Z  @Z 0n-@Hww;w\ww7JcH?^P!mgΜ9rdtt1cPtj4I}>ĉoSX rܹI&!* (s֧ˬPCh̘1i- cHki-Z]߭:oC7nh Zx3/~!gKۿ9t۰Og}g˵;\_SnK{so|soO3񙵕ϿmE'+h;D|{ br>iK"^~<(i%]-t+8D$Vsg{ ^_FRA0:V`KxV-X%XB˞ tVݠvoߺU^wySRV/XD͋xV[͕=;["\}Ͻ_}iNKrbT}yųfGsZ8[eNQtyc:C\;Yw>5Ytl끅ZxӇ ^U|X_D`َ+~<14`Y'4磧h'VT[ Z[.26Qvy{Mq̶0?ptzϑ"_Pڜ_y>sN՜9Թ-iFף[owE';w>Ԝ<])~Q=|I s+} f|s7xhRG]Vzt[WZ_l`o*YzZn|ͨ5U//i ,eWTo{;/1W./fgӑeY|r/RtꝝxYpҳtՎïLY}06QrU[3!C|uF$J_D+od8"9SI{Bm vaDSVYl_ro 4Qp&ÝNݶƍ[Hzo>"5l/ϒ{_d?Qݕu[jNOɇMo?~`jŏTYO:~_'ā~{Y /x0k?bbX,ےF?cYqϬ(Y`l|g'=b)yDD$o\V4vX`/t3 IDATbY"|ueAf e[>eY UIZ" KWqd J.1F5KV X~f{"WYsrg_^w,^tIݝ"+}zEIEOJD߽@X ,+yxAa3D߻̾*=:#~a ONKf\HhٖNH|fَ*^'޳ؾٓ{΍=,~bټ@dR4Om^9x'k rD`~6%H>0./ԱD\ϷB=*|jQ.G<%~l=?{ ߝ4!R!ռ7Lo } V8^SJS7 781:,X|+K6_iߋ"Q|3[{o;:/q,짽-S~G&sݴ5tVW$ 5wFe-;s6c:u8^|cÌ;WcAKWe:l:/ 78cӾa|Gmi]aOn=~f_0u^kHlO[R,{09Tʉy[[kQ/ND(:zŻyƑyꃺP(]\$GwV}) KGd`ÆUe}~sèu:?WO;vG){2IOs_r险6{iik( 4Y3*k Nz0I/Ӈ|2g|él8]ytpdc9 }qA>jF?W4"&[B I񪷶.[`Xm0@#"~?{ -ڰy4HRJ `yDj gH>?fU,)':E_ U;h_wxTF1ƙ+LVM.uyEMP ˑjl%Jj@HR9Y*]W?6/R#/Vꪫ65B7,ƺ` lE{8F-V},'ʰޮBVq^[Z(^v$! q5U@REwμ0#LeG~ψ(g|XJ}p|='eoQAzN{S/㉈5yDDjƜie4L֌l63)J<1e\X]3}I%b ?r_>-[np葈4׮T^#9g`=@s\Ⱦc9K 4vϒJ@Q|𧀟9"WP7`#>20#2/>?_7c)6)o}7}ig rVit_^։hz/4DM+?x\̳ H3w߷f^ZcHW0E'Sg٣aiqVtT=,e㯾*-6°36jqi ~>ԫ)Oir|=&gg_TmX9}\ґVmٰrKu61b%"e^ $衤HгD\nۅL^+E:bz{EZ9nn鋌jΩ7]F՛KQ2%DlsW`]nt7V62 .GD6 Z).*$F"Rݬ&bRCD$E;zR3GDj$_]r^_ef SPu JngI;iU]]KYw})|yΝ8n|惁3ʤ4W F'dNXb9!%+;1sfi?OǫYo*>+QTRJe+̴FN8. $E%o@%w2Q۱sT""JP9+_ڼwgm=K~zɴRm[VҝJcr'N>0T{-_;%"F58͏4*:R.kU*FI*ʨe>TM?m+#IEz".Z1/+t,E\Fld,cdW*SC@ρ/e"RNt"">o[DTa 7grj]Uݩ{  kxr)U%JtWWؿxKFu^@@yK>OwWqχr&aې7sqqćr9oYjnDG.^pb3b!Fg&%y_Plos%$skFM )Eej#^yW2`]COD}ao]?*C, ˟³D2o]'~65"ǂ:oWBfE+9¨2{!"YSƅB C+V/$BHeP$"Cɮ XM8CZ 0ww~/O'o-4-ww}]Dg>?<%"b&NeKԁľg3 !+/(}R Nx#o/\TtDLq[T?1TDDP_s*%Jé,f0M' ϫHr${&w>zJܸlc|%CG3xb9]E;z"wIYU)Kg`G8~-%b2!b|טUjw䰔h/<4Pz2{II<2jkshǨB5>my8NLBM*wnS5?kٙ?!"zϿ,wݧ"hbVOlN}68wB_|}SN|G&"sH7Wߝytd3կ/.M{NIK{\UP~V*utO晣NdMbGwA<35m447O 7}/y-6%[FܡMMDr"i,&-#hL$EDyMإ-W_>H2Dr'Hو7 cme!HeU5!',RRax% {;,F#ɱ`kw3$|n䘯f,^M cFk7?W#6a7yԟܡ~2p눞s&,˿~2~¿$GK"֝j;nZ;y|?NDn5OW |Ǟn[ad/cn^[RDʞ%E$W, [me.K2M)=ZS4XlRbٚ sysy"~ڂ~/_>mj ]X89H f[-tr3|EKm43|oW[]YZxxKTsnrS[Wv*D5}ڥcǦ}v`yYLS[~DLv,}XU/ y-B~ ǫ؅ Mh<,Mvˎ,MO+uOV-a:1ś0Eqr\j(k xڴl\6{Q!?5IGS':gr7S~}ś7'7>[2{B羥qD⑝^]{_@>Tjͼni6yOm[Z]º_ɚ6o%:"JZbzeu|Wq[7EFowwwLk@# ׶|ӕ/n$ҋD"FbeF|M4Fk7)dzؓT<o *Mj_qg &_@suvjn !FHks"m69mt,-z% n :>3tuGc-n: psϖퟶ|_5ɛ{ղ-!SڰkdF#ZKUi13ԛo U p$ݵ7! i- @Z K{IӧO7i-1iҤ'O>}EqM(H$&MtEK{k2w}w``%D"ɓ'k4WHk2ƍP7LBHkZHkF﭅ػヒBFn 4 Hk Hk[ 100p?p``qM<w7i-bDsm4T$1L_e X3U۸Inan7_\v{<_ 7ݖۇNogo o?z'9\XƧ]~擧eOσ+yjp%fg@u +"3G2Wǃ#.%.ZnWn}hb-v3$ᬼj0ϜioK| OSf5Ϝ9s`wu.uy̙f&ZGkCklhHK0syKNjV̙3D6-Sjg閉B2 X!"uy 9g4K/⾰@!"v rf[ߥn\2*Uj pg1?I?soO{j_Ç;&c?hO2蓏^j}!"9Oئ;6է'ȸ/{m-YD1\/8kL>5ju̬7<&,lj+K1Tr1*)荖 }aoWXK;hmSJϣeͦ\H]EctW$k*6vFxQt6421ýZU6+-oJwKYSN0VqosE]h1RoK94cU_^PU[5ï(k_nw ?kA]Q_.'P{ݞM˨L7"./U\72=T6^sDr")h6/7FkOkO;' Yztz6tFG~x/7%Y4es[~6YEӉOlݷm393_rkh{/3H{G?~t[)ē=m}Dxypn?R":oGtۏ>̪?Urof6 6&SngAlt7v aGM_[HDDr_㴎fHֲ̙H>Ra*t{lQL^=rB4lf;9C}~KgCE K|҃f,]mV!sV ]B,dp f%VWK]BQ_&f6Xb-va\q)TaY`Z"f7#[##hύ5 mTzeԹE/E RUΑZȈ"\N#GѶ0Y-z3Mb+uvv,UFq6.SRY(XtJ4&5Bz(KR Ogd;F!#"ҚfiXL ﳙ .n >< 8 *X Ha ֲMKzA Qukwm>orwK,Xm7o56/=>JzS 7eIDAT"GKRId&`l[z\vl6(Gc2Itluֆ2jڶZ-7}aOj V^oHkxU(_o z۔=wN{;ΎAn 'i_=kCj.Mg-3joDR |ЯrDD|1{[FbJ\CP^Y_KJ+!4J55Q[}| >XT&XI1ܕS rnI ոZ- UCn/AZ{mnuecJv,M@i>e)o B]bkt۔Bu|kp81l[?|bx"P(j>=uz(BJ'ڛ?j}D Kb Nls{-nR cv 6i|ݼŮgqRG'%M&n"Vp蓾`,^0 ?f5sɮd8լ%""n.vq9M E1d֐S4&>5pb4q99Zu9m X_8Qm 9 DCIMk+r9"Y1n5͂!GDj>"c]AQo0bW{Ltm~!%R[|5J>|5ǚ)%q&!J3SE|ɴ\DQS r+{f-79(TέGkvJWf|t^g/UM P&K.GrwCe+[dչ;:T~we\[oRosJCk`]sLroϭz-h; ָ@ rj}"|ZI;>Z2gl=I%??R}tlꓳ&-} ,4wξ]y39͞R2{&N?w-D~7 };?my;XlFųsaTS4)*1$2r9"Z7Y lg%¡a7rD-+n<0%bs,v娿tC3[["׹m [}H@]^<9[%"Vk)1ɇW;X9Tv]8>D >2iX)d婱h(q9"LN&\VgOv3 [D/n@9X}bu^z%MnnAVzX#'0FD;Ęc)ϬQ 2U;4Uf`Ylu.%Ej]&E!\r EN=ETh(!ԶBu_<<&fΜpMI,ci o@=x[3Uսf =dqG\򅹦 D,oŬL(!Ź1oWF vSf|: &e^ $衤HгD\nۅL^+E:bz{EZ9nn鋌jΩ7]F՛KQ2%DlsW`W2ݍLy9s65V c7X4-E;zR3GDj$_]rc5cl-Xf?ʽGDto偦{To}9EuHIѸ,h+oumYSy)үDDeݚ:{>G'dbS?RMIuq䙟Lg'ƕa^fnB,U1Q"YJ(3J)!*\G*9,R F͓,7H|SqJJ/*q4*%8ZJM?m#QRDV%W!ھo[W""JhxUmzuqE7]Mҳ& V'SDX ,]UBD s̥OO&"%)ipAT.ahl %춘/Wm/߹p}OHeZdSߨ ^p= *BMEDm.6յF9ž!}ծZn"6\u5+JS4] 7{b.pi³Hgw Z58ʹxw'4T8+aX$Z* ;"}cFpVjy; ^E#_hM6J󾘡)t9'$瘡<$[!i55ds ?S{x_Կz" {QJ%EdIT"cd"%޺&NଂXSJ q%']C`!"YSƅB C+V/$BHeP$"Cɮ1p+7N_;IEDtK֝gK[wcѝsϞxw_٧r x3>%ga|~uS7@oZGZ?P[[Α.Dfʐ,D%f2or WrzED)PDb*"[")%Jqj9KY5\Y0+ ǟǢ$*P$y0e4%m{/f][U˒ zZ$n[S3VuHC)ϵMw]'Qг$wXF2P*664{={FAmihOqrb!ސR#™fRciay&fohs".ʷrzXm$]O$2/_20&IV5KDRGsʼAϤ՚c`bճjEN,mXw1D$6ٲdSsڎ#W0 39UZNH\̰*MbQo|EWsv ׹\~ZCr<#=,%b2!b|טUjw䰔h/<4Pz2{II<2jksխA-Ծ\ZӖ:!*LB#~Vѩ[>Mߛ4,w2ͧC љ7X_ј,3DJ[g+J k?}gPgL9&h5ɠ/PKW~5oMR..٦K%(7˒,e9 ij[>[_Eɽn|fVn}*IIID$;J]`7-^rX$i"]ޮƤepHN܏fޯ& >`3y^  u|-wEc˜)%/WŦdKs8IiɢyoK1ŤrM#.]!Qx^kSu56vi/b,ʠWɱ"ɲFg#`H9܇D""AVܞ$KIZ/+D\?G$ǂ)""P Z|5c(njpA0Z 7 8zw}}׈8w6al6k-G翞ƍz7_{m?Rk-DD&YӖ}ohӧ76z&Oi٤rˏO=R?ݴkO5w~2{'hزw~wYG |OD_Vpն;Nw]f=e8!nuu[.v{&=%u wUXȒLDYco%E2ԖDBYSdSݞJfS+j~SÍ2S3+]ٵbeimc R:S\U{XݏZ<19]U*׸Z]RKWBzbrl5L,K:wY&")B*tmscr\󣶞(BU&>*yj5Z}|V빠i;rYXoV|-KGmd /_5ID7Gh_ᨯO8_t3Gi^h2sc:r7L<8sM7@X\]6NTz"JE=HSr{E EU z"FnXSUӪ[SD kY)12VFSްNrpHhZ"mUCyM]\bBz1$2/Ty,j"9[̗Juu|`R1Byd~~o??O.ܽ?o%ۯQz뷏v{w*'7Q^s{I+?Q,Gj+c՘N~k;BCW$1Lj}G:7Xm15+>hJJ׃ZM:ٙ)9_w7{}uuto_VI|TdBN{-3@csRH+esUzDjn !FHks"}:>gi+_lX8%k ?^?}}#>~Zo`Jq=TvUP/fA81ZКmW- B= >=׬Qk* U:f7qi&!5s&!\k; փw i- @Z KRp >}zܸqHko'O<}4P%H|߾z> 4H$wߍ7nԩSQ7LBHkZHkF﭅G7}p3Wh$d@Z i- @Z i- @Z TwBjH6tdT@Z pSF(ZKauacLronAW&;vO[[E[c@j6"6{fH 8Kնy\N*pY5mmN傱UU~-UymZ#b@h꼿*rb55TWCOLyl YHD齪i6ApNMD4Z m=_ vKSu1{S( JaxP׷^k'}a+Bu|kp81l[Kh> ?%G}]dv $Z|ROG/讋k>G!悽UGA`}cɬ@cU}1׿ac-= ꅞXfBMSTXSշ-Nd\S,HJKXgA?Xe∈s9"bf>6ӗZAMD\ˈQQ*1&}`V;8njdW9zU*&r4e&"ix&fu.99G5N؜CNdʖ2b`?(.Z2HkYJ?qږu&1{p2c3FERJnvCLD6^Y#n d bI"lTz\ ]KC䚲LQёN,,\ߣ(1Q&"bUC[e63%p}"o-]sAVoVi~JTRLRȩ-!W0ڬZpu}: 49Ͳc.Ґ]YT#ElR1Iܜ2/_b^,Q16yS%Q2[_Fk'j]*zXtߐĪua2ɨHD$G;BN9ʢpZ=IݾnENg\n@!n_z8}^G%" yݞfUծi$f>M?Q,EZ\?+|"%p]h-ܬCeLֵ[FٜbqM,C59#F*5șV3A~y骚UU-#^JQT}ݪ2YN!0,|ěv^ۥԚg]M#"zy|_pmDD\jɣ \%k??{(,>>шdl6#I022nns-eaM" x9Hff&Lrr2QQK1p8R |(p n7 }>))|,X@zz:Ag;iii᫯ƍ. "--mti'|Ν?RamlKqO~aEcc#_5ޗ|-򫥥fYm'8V^ͲetɁnS__OMM ާ !g;w|VQkgʕ+),,h4:::ʙ3g8}4ccߓ&}$m)++Y… x I,_łlYdxx>{kf )sNohgyYczp '$wG?NQQ_\nsWeY~q*ct#E9 $ z)֬Y,˳d0d2ގ(qss=GVVs $&&ܬv-X,V !*_œO>Iaa!s<6+W]fXY֫!؃{+77k2n: TF]$^~=$ieYfÆ Zyk^H)//q\#@QQQX6Slh4ҢvX,{V뭠gܥ7iii111j3N>ѢyCQTV ?~[E˟$I}=ưnܹsՅvǞJH(oEYgPEQrYبƞ6ÁOpF~뭷C OY{4ឫbrzr/&0Ƹ~:fUVJ&=p]f,ܾ}1 8{BTbẸ].lԴ`Z}:`?ѫ}Kl6l؀$Ir̙8uTȋ$/椄dJNN҂-^ݺ3\Ν;MlܸıcǴׄ۷!nq67oOKK#66{}zƠ(ʴ 044Ȉڼ!6"V-[ˇhkklcuuuܗ訧/ڪm8֦}OٳFsjoo.''#ՃW^yE-BZEb2׭ݸqWz,@>1rև4#krvCC%8qF] w=qSS.+¤p:)?x_#O2}tF~pEKK 6'qP2!~/2¦6ʅ8Fq7`Sh&f4u݄o۶:ڞ N?p7~ 8[#@GGֺt _Ntdi~ Fm=z4xr'hvn3p ÜGMM }}}CDMVV EfSSS_v !zDF7}B2^p~m_}^^7_Sˉ'46zLWx_;#jX,cIKK3hnnO?v[YYA (j=mX~x! .^HVVϟ3dr?4zdz'ȱpTLJ~mlst֭[ :fQUU`X=88HUU9mݺ5B-p\_⏉aӦMڌ a.> JXAgW>ydb.\ ###dz[[ӆـB f)Z-0X|YOٳg9|V`%\8{x*_=l>|˗/k[ ú;vLi7FO\ERRO?JԻHT׻FءՀ-ZDqq1 !K޳nk"\2ѬZ*$QSS=(IߖVM[7x#qttT?vDaa!+VQ~Mjkk9w{NL[ts+D;w* NCC.\~&6=If%0Kד%##CגaNS IRE^^<~!OvQ$i(4wt8+Igg^=?;VqL0{KKK^Raǁc3Y: !2$IZU:O7ZpQۯedYt\6`s:WBz7 &dyIENDB`././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/doc/source/user/figures/topology_element_2.png0000664000175000017500000000723300000000000024132 0ustar00zuulzuul00000000000000PNG  IHDR\\XbKGD pHYs B(xtIME   \(IDATxkTTWUj0_@cP0udMÌL:t2f6Z,v9lM;2j GBIUQZcATTw׹>ch4R1,r(a#Yo+rI;Fw&@*0 LdNWeIK7m:F=Kw%r} Kٳg[z"\!KHUյ_>$`@vמ( łjoN!Dը&|۶m끗 eڴi$&&b41L0N7=ZiooV~:v[>#I %…F% 8RRR>}:'OFefHmm-UUUtuu=Y%yΜ9H 9`+0E+ 554򅩪J]]L&l6k6 !qUeVn4Yp!>doaZ;wv w#IޡaNYxx8̛7#{(ܹsrΞ=Ko}.|,?? !&qt:YYYdff6 رc\xU lAExQQQ(G||<< #&ꫯ())qu1?7 7m.WUeffdɒaC13Z+trmQUu0`0rJ222$^OJJ Nq,Y[E WUusss&jY?P…{_I\\DFF2pu>Cخ] B8q"k׮%""ъ7nk.zzzZ^ux@hrssG5 <󄄄8E6pqqq/..|ݺuDEE0 115kh':;;wͤ! h֭[ǣ>CP]S4$N2(ߊ >|DIZSPPǧ;ܿs/_NFF?9W˱{PU՗Ntꉏ.{ُ촴4ɶX,|| XVaҤIH+OSDEEb n'~r,焄j*m$%!^TT4'+ Z؁+>cPs&MĂ m7oኢlq7̜9ky]:;;l߱ͻ… ^ڤMxQQ|`+֔X,N>M4"ܾVvܩ n``ٲeZя pEQ '`„ ?(w߾kW|%Ο;w.'OvF/zMπNNVmM ouW}NVVEwGsnn?0c m|b@/u>}z5d_-穹Y _BBBHLLvK"|p>1^pe F cin BDs~ԩSNg?j[rw6p í[(lEQ$a)Mk+oNF$IHO!i(}>MlljO$&2>F㘏77]9JxmmSIvGx::YY`_2,*9Jm?UU'#I7}R"""<6d +H_UZj$8SN'GpHss39 F#כrcYN wH_`X0Xj݃ rԿ cRDs.-9Fb <5J󴙼uϕKNwGH#{ 1 U9]Gw,!դr?rE}~}-$}}ɟ6?Dԩch dbC#RFUipo/ԅ+$\Z _^WWǸqv28z>YC˟HG}AYY٠ee'~DmZV;rӝvl6۠r$a<4=_7ϪV} !:v1cƠo<;*ztq;"V>l@JepINp!))iЙ8_ԤOKtuu6%--LƵk|>HRSS]w_…Bc2UUpk)OpСQP]pA{wN` |4٬]CvyDxxx']@d[[--->_!b4K.i_5+t !ygL3g\t:ݾXhOK'FIF7"}1FO<mBxE[p8WVV3-M&d2iUk/Br!NII k׮!B ѣG_!n p~ uuu?~Oeu舄'+;vL['Sf=B?q ?MMM{,{}^߀z;to߾^ 0ݻWC@d{L,IҋzWCIIvKģ~^ΎQ___ORRW3G N. n/ep7W{nZZZ*M&Gъ{ޕׄ !zu:ݏ6KGZ(Bcc#sQQQ?yyyuo> `%Å7ngmXK/yl Ŀ);; @ldG޽[[<:7/^*VK.4V4L߿_,(((2u(SZZZx6JbbbDʉ'8rv0,K/>)/**sEQJNYZZO?tPoCnb߾}47g1B -999#j=q7[V !\iaѬse˖uzM>LCCVl$NV~YcTU݊cZh-9{VF7|3fjJzz:YYYʹq)L&0[B}頻 ,g')))l EQܹs\rAl'. IzUU_s?66T]MM fv|!(ϻtߗ~[ҲVU%8w,]q$Pm``m݄z68cP%m0э IENDB`././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/doc/source/user/figures/topology_kubernetes.png0000664000175000017500000041137200000000000024432 0ustar00zuulzuul00000000000000PNG  IHDR*])JsBIT|dtEXtSoftwareShutterc IDATx{|TLfNrf HB%/%PͪPxV?]]~Z[(BB\1` -AI, 3H&If$HElDǃdΙ99|;zh6`WH[SP)"""""""""mNA9Rj0 :?5' -ZG sy;\I׃u 0 C"""""""""|t_|jN  POвT'p='l6`I+COK/ :u Q)7BDDDDDDDDtzT{Ȉ 6l6{(B=,[`v ` |/!"BT eZ{+TeYěQ@-#""l6݁$""N6;v-plvvſn5Nav%)!5ݨ\.uj$݉u ;$ ` !ZkmknkRXzl`0x˕_Rwt^RDDDDDDDD@M uut覾PXi;=nl2Џ`}/k`h͚`c'O,O%' i?QSgΪureK?2#钿ק\f3T*ODDDDDR|(c\,[F; zK?>@xw ;}{l1LĶp/da/!nW.`ȨV}ڵ5kpWбcGJKKկ~Ŕ)Sn+^ݻ?EJJ ǏgXu]޽{Y|9SN}\uUhu먩gϞ|ҵkWnd+![#J}K?;|#":voj=_OLh<=$GkwV}x#1_!^@H ৴TZٌ̍ci$#ne? چ_6Zuxx<&?32UN΢hC/`$.Zb-xCDaK BJs1 (dzijG4.A{cØ;p1L#x 6n 7܀+]]t#6Fl""-šVcPi[(l׎n_tG>:^yUw̘k4DBI?+o"5캅4_PYf%~pC1蛵eyoUB)Tfc^|3jl/?ᲢrtbbکWyLfĄ/8a8 Vc~吲I(N s&X%l\ˬ%L]6 .!6[hC)\0Z+q~J{QWe{}yݛHرciAap A:uđ#G(++#!!"bccz ݻO;⪫" ҫW/|MTUv*9n'צcC/ȺçRafv㾶ȳ[ߚ cFHS 34 \kvOWr:zg1 gXir'"SH< W 33lB*Ə/d K(/86Zo.PEĹ䄯]R 9}*觼 Ik(ZӦ2+3UBV3e4Ix21g 揟I̻TWfEa$%%P 7} 3=t#/!;w-%;y0]YsGҷ,/ ǝʄg2$G7M,?DEZlmUdz 3&5ꌗX6 c3(5ݪO u7i?rݰ]RZ[ˬ%E\nSt=B[R`6ɃpV`TLz# Y^LɤPv>%6odTVSLG\W1#L,xIl2ITnΤ♳xZܦmJ<4ԂHߔo^k&2o SJi!6K +3q0S&j?m{E SemeJ-}YcnP%/Sgg1moy> ,NCljIZYJZmWoYV\DqA$f`0@ ""0""8Y@M vUNBW11-{1:vظiDFF6nkЭ[Pꫯȑ#|x111|GAai,ax<ՅB{aoÉ-9hAWyεYw0U _wxڇwh"oHi_[9s}KՅp)YtY.,_YzҘd2܀g3 03 7VfNzW0-%kF/ X%1V_ }ɢgyr4W'طdS]NXz ʵh#Wf$2("n%1+?a/1F+ǞYzY; 7Ϭe9hS.2Fi+|aL|6JZf/*%mkLL61i.l^(Z5e ϮMdhwPTIyL6+Y;~ bXx!ӌ2V=>%ѶJ,EE$-Xƴطj KH]1ESRHy4'7ѿJv6>TmD*7nÐp0"h4 ^Ѷp&-҉cQ& LFYʆ&=3^ġBpefxFvf޷2SmaS8jtMRF.-cQo: 386Y9gIp}Ǜy҂.wa4\.+ϣFϝ˰3# )l ⭵z@V JnQ8E[tEﳎ'IIIw}79`0HTTy3!D#jȒFP ?s*|tVa`J| 6gx©S7]Hn)2]2,>yb&6O'3ѸU ɾ;4 a4oirb K} 3ژW/XpGO-gV ySes5tEQi08,]ǔ$XV5F_/Ui \nTM+Ì#>3|Ƀ?>dKLRG |\j`j$ g>eQ[[ }"""""Uy(ҳ`od+%n&Þ |(gɤX~ ]iDp6)s;ufL#Hn/&nPt܃Ljm1,ˤ{Y<4i/O`xC|8jSW!>0caEsiT)L^0`]9Bd :+$l! ?_cӾ{<_QWXH+iʋ >6#wqt. CńG)c$]۴yt΃^`#ֵ3U+0_=o%yJ,*i>t0+iZT> +sXy͔?S3.YX"^\_wZ[]mtf&;Ofy<`$ir/cLlgĐͥNs"d&Nyl?!2 .+W^c ugZ7 jL̯pwv0)F)n{1: . aen~ 33RDDDD仪r':nT|V[ˆ1<5Ϊ ӍLd² q&,>S4ѶdPjLܦ<}$.zZ_|& je.2L )[>e0{fG֖"-78o3CRbR&o@i+j;F}(4rKb 4Cm6 [DYX0l}6`rfna#cj[|^\\osYYYcɣG 8u>g}l(KޮF>) ?UNIDNjjIC?d៨.xai;yre˖5g&8x'BIAQ(wv]iމ)X( V&򲟧wagl̺'VnbU1 N S9B7f_Ei[_ڂ6kK1"ϦnޒN%ZnjUoU!osSȘy2.o0qμ )>|8ԫ2xz*i@0l/{'Q ޓ|/jj[°kg}raھ`w}ロ"^/۷Cۗ",YO<zaXLA* Ӊ="f`ڿ$z!9A!5 qvǷr4w>8{&ztBM_.YkƳHz#1ܟOps(ԩ Ϙs'38VbVd<BiCf_r^OS/cJTbL*sP&w̞@^~9isrH6LnϬ%X6o LkS6#E)d !+ׁA|ZY )ddټܷԅ;9q,>ɳ7#VO#svh6EIoFoprh#isԴ~lpss ɚPB)07"Bݠ=+\5B0әXp -tIM8w>h][U)rٰ(I1ҥ^DDDDD.3 g-KdP,2U]L_ź@ÇsqN:e`?N{h\yƵN;uǏ5+z^]Cgw'I0zi6SO=;C}}=7tw$'&3Mҽ{Sp0hР921ÇvU݉HҸtql|`}G{=%-Y uuf;N;׿>CTa"Չd,Q7 43^ *Ew! Ϫy, aRDDDD[E괅NSM|Yy^Aa?9iFf7' 6.У2111c|m""ucW@%UZIbTr&RՈ|y(z>|/cbxwW266ݺrKm4ۉ(C}Aƪ IDAT:KS=-O ."kP!\(#)e}'cdDS_y;xX]Vn52'C) 8FP'twx_Bܝ;sq@c˰iOx#6@ ^6  ŅtGCn|C}9*ϴ}N6/oFUzӤWe;[t4H- !N"HN!KW$A%@Ee%O{%v36H4 :aͮ!"""""""""| Aȅ DDDDDDDDD)6RDDDDDDDDDڜJis *EDDDDDDDD9~ՆƠK.gml! O ~;߮9CcPyF"篹z.CA\`0H0$""DDDDDDDDyAeDDY LD6 nj,EDDDDDDDl"r`GDDDDDDDDT*Dtʆ^v]=EDDDDDDDZtRk4lXXGsTLA9"""""""""THSP)"""""""""mNAנa3т:""""""""SP)"""""""""mNA9"""""""""THSP)"""""""""mNA9G??6D.~*CDDDDDDDv;a`'N D.`0H @ eYXׯE[__Omm*XDDDDDDD.+QQQ +5[2U """""""rٱ,(h\'PP)"""""""""mNA9"""""""""THSP)"""""""""mNA9"""""""""THSP)"""""""""mΡ*eQLxu̺)2dcU'-f`XmthZ8l/x5vvozFp% @{dy{o(7ގ7Q/K+Fkx',0e--<&~ovu\kuOJQ{=ډ.uygawT?~fݚV]z >gm@ {HG)ѺtNAwMǬjrーuT4KY~Y~B+ʽL6:D^֓;c/jݵ70&?t]BNʲyi]th2^a%T~/D8?Y2% 㒾ҫm_jv}{_g<fh*W2nĕ/ ẁ՝W/{8>Ȏ7|;7oj9a2>*X+zߏɯ~_odGG}~8 ;O]u$zi?<?=s`WLJg~Lz^ { ?dk\/ 8*G["uǜ?mpGדf»Y5?Wղ e[/1w$;{^^ZwbX%p^j9^7%~m^σնxOwr2j3`{!vлz y!9¸oA/ڮ<;^'f]fs桷!k%[6o6Lw5'+pwۯkFKKխ.'y䌺cCG {{CuZ1=Y;=O%Ut#UY5'3hA>_~1ILۢ,l ,ӓG8Nj7=cǽ1~Ղѕ]Y41U*.: |zߕȂG#fxW`=*r kG̲cޅ}npSX@jϨII튷pmZR>+g'w0nʿg[:ݖYQcW$N )EDDDD8.eQnf:f-p2%7+v?]w7w?dPt)VrpOg#|zi=*OnbM9RC#^CeD'6z&vLoyzSr6]|\[R?}Vcҏ3[>:k|/7_>0kz^ z-X@y ;NU}:]w2=<< HoUEݕ~,|`t͈C=sD G505b@xF_H傲kx/AfRQUX|r><=Y[WKdI׋exޓkZSjٷCǀd}FpU ޣ}'C{Ϋǽ‡Tr={KxNqc&& J\t"->e'2/:0b+1Ln}{`%D6z"<=#օ sՕ\ouog+<~w9y =<ϤwEqsPq :8mV#c\wc,ߏc?]^8.Npw)Eqx6N^= Jׂw]wm{❄>W6 g| ~:k)ѝU58GURxXCDDDDD}}sTFG[wu\ქ/Ny=%-$;\Ƕ-5g_z=QT_fKYu&v_X~zr#ݿ5^fY4 }ԊzH(TH) \K{O~S[ήew(q;M7YI /Tc]1H&| 3L\MRD4*-ZMzE6]Y{p Wmyʰx8 +FxE⋬CNId- >YƎ#%< P橥@.Y̪C_yՏcajR!&]M}>, M]c^2[ktʣ>+3X47;ʑyixr/0tlNڊ-,C}X1uq$Fzj2d1oVtƢ9ňbda8.AH5aS=xϫ~ \.Ӥe>X>>SV~WۉԅN8:/_yЫϽhRLlZ?u w igoy d1O۝5_.8yhLo!ח۫ &׳kZ;La\7o4=n6S`t :] ]~qְH'RS$\L+c_s+)#?ש]qt 7Tvn" O wNti~,9>$w_Wcң[Wk(zxCq SMzTl$00Bxy>-&RA|`凡+ޛÄ>3p]X+{g`8+Sڏ y=4'%xg>wZBUgfʽ#&qT(#k/`?a쪨j͘Çwd;Wdc|M^aYTj\ Õl O5z5lbhX̤>+׳uo|"Z~'˶q-C;cky(ǹbvUCsRrK^=h.YPIՏ_k6P]VAfr0<QEY75}5w:ɶ=DWA/vRP7_/dzW+HTz} """""=*$vFBppox@ٙh0U\ :rŞubѝB ĥ>OX?I{صcިDn֒K(t!%TqN~q04V~.00_9eTzL<=^$TVZL'`CKμQܝer[Ȝ3p'=ȓe_BJ`gf9l$}I=x8o&9Y  &Dۋq#dϋϓd{voJGQ;.,*=EK{5`z1|XHbC.ܮ^xz:dN\Kch?9EdR?s6yig[6yc١;cɸ;Gpcv7 t{rQ 8N`U깞lp{oT еwuO qkC&0-!bDpzWs&. @=t)@Ww73kiwscn%5qZJj?(9*G\<^k\x/t8Q׉'W-b)?fgVӛz.YgH{H::፟?NZG°neĥMy?Wѓ{D\l~Wŵ4{z]GK,  滳,ү_ߋ.ĉ_y}o^VJ'-fR^`u,MowwH&ߢ@QDDDDDڵkv{۝^q2˖.z:I6R<4ubݲ_l])z boeӸ6w/wU2'NII {\5TwCfWuz+Aww^zoK0uZݥs ?D=ɠly.z ?w&?'IDDDDDDD!P6 à[n;v^Ҡ[88x/__={>=fpAG(xDLbS֠m4ivҘ٦I4iؤ=mmjv6UմD*V(oxC}?̚f]~2IfTZ:""""""""m_e m&((s.wc[0el>Ŷj ]o]ʂ86mX[||=?}k )L4dzOLLf+ l S0pO#~t㧾NeewQocǙ6̊Lue :q9\t刈(mVUUmC4vrqRe6,qCffj;Ȏ+01lȔaPg"SGtN8y5|v MA7w/C'Y.zRZKeʷ?\ 0mTE٦hp#IyR<}Pspǁ26'8 wMsV6cUޱD&Mpirq}JDli)C|N\_} 3<>c  ȔY*3ct7>zjcmfk`+Cwnwogݸ/Hg "0VαʰÂ̛;iABG̹%9vF%_IRy st MMJOSaC0yMV/D8USf 74ކaĎRd HN~lî'55+%զeAP|l.X IDAT܄2[TeobE$k0(|7m *;+'v=LZ[Ʋ4/vѺ% 8oGLb֏ھytl> ఖ3 i~ 2craCYl{")B9ՋLJ&,^Wtyj'kq EDDDDD {?ӿ GD^zNGK,)^$v:gX:_͑}ktӹ?(7ȵר J޺)E޺yGyql%roϽbf0ykX$[g&3ୋ}#O[z^ "U,YYg$kfAZOw~w'FO:kֵ =.BsOr*ڕSY֝SR~;еd$ұ5Cjj8KTudŋL~"o:9eIᬻ@oS |(+Y?juvvgmiU ׿ԼmNμǒ-W{$ u[=&5^0ܚnGN&kk"=5FH e;99U\NM.؟nBC?%tc}'OHr:~S˧׿j۩*2ֻtٳOgorKw`͗8.R~oZW)7[?%x^aN߲Qct ttJ8[GCksX:)p)MqgMԲm%1u?x|$zW'0kRMYū|ɤoZVv>—}wgtf^DDDDD|Rjו7. ./BY<:ߣ[{b9's}t"t|~VkKN.\+:0QY)MCq7CGs,|j4餓?p?gD̰QLf˿ /?UxeE?{t)}/g?If3!Sgy-gQB~ 9 xv[v<;CoSlhATttN0ťӔ)nXV%-qZ%<&u:M9^䴶!^LFuyoo%`OXw83 ɣH-W΄ߜTܜTuV_cTDDDDDjyZx$OOr xф ]3F@~WȦC W[kH"B-oxQ˿K8ˌ9.jnF[0~g+9Y~N ]}TM]>/#IN+6. ˜t;?5@N67ew2clX9f9ډKFywXxv`y-)/Qm%"[~~mɲ, N DA%]999 6p"9 “Hh/o3yWxtM\| .ZbHu$ n!+ $gqkt ބǍwUտ|c{N: {s҉3Tz΀9fnm7cT쩃s3?]xʝЯ ~ +gU{Sw92wqfg~*&3sŬ+; @8i1]g:MmXo/<5}$ixZ ]{]˖ igh""""""@nwvɉs~0hM/m`$ML࿽@CMÝt2/c6r2.t@={>@~f)Nq :c[+$zwmRjTB,$e$`FfIVӟi1#2H4є@fj8WY-XlAFz\h@]Y@cou&.> !Po{Kr|IlSy7Uszw'Yw8ZX ?KoK7qbE[r._ٿןxGtRzON# ? pr''zW[nZۧԛ{)}5NukIA-Ē4#؋4 Y:}+#^铈X#%(>-} R+Hg@4k*82F [XAI='d^DDDDDDh5 ~N+4Q??67O_l+گY|4QilxZ~S`~~oP7szN쩜K&ojg(pc;ۛ'O^˗k;"S2½+ֱ0sf^aLNFG8et$l_mTjِro;9Y9^fy2 *En)&3 s4dCw |HR3|+Xw&kcmNSߔTs9'1)EDDDDDD"Tfj?:.a9ĉ]g *6y-}w-ILo5vYeI).Tr"TљC͵yXUld9K|;oM$d$=Z+f߽HO%""""""r(5~tL :M 6aKSw-M?.>7ȉ;O'5RזRVb<6vMgL*V۾fiD1ΗT;2ơRDDDDDD1DnEfq`k`1^,|08F`RzGV1GE\Q% 3.4J%TpwqWz JDDDDDD::T~'* [iXM镑NL72u& oJ^2YjUm;bMzV4ť3)b 7P:I$+IU&sJM>}F<Ӊ餤);gz[u5Waw[It i,\x7)ݢZDDDDDnws_w6ɠƸrmN'eeetvjzQ__ZE'6Z *Gyc?@?چ""""""Z;?zSөMWmDEE]Vbq" Y,(""""""` 1?DxEGXDHPP)"wW}}S{q& 5j(MN{~f-`Uo쀑BjSHQP)"UG|۟4M)"""""""mHA\T@@W8 H5RD.kؒrO鈈HSP)"""""""""NA;"""""""""THkžf3{ Kg{\׾}|z+U9aټi狈ͯըܱ(sx'9# zqx=}L~ȡv5ye v@"Ofƴa،.2燑2#!0:/3e?Ͷx,\TLɳI?8['82ywbRfD۝ˁ]*kTJYp UA 3eo@9/d!H1YSZ{KqFq>]WIܔL 8UDOdL@c2g1oPΛp;(1bBJqQZKeʷ?\0mTVtđL7w{^61ݠj׶XJEDdt/lAgox QW;HS Fz~QƳ rt(ߺفLyd"6UaڼqBShYrؽ˖Z|*75cPw'v-q,k !C9q V{ʰÂ̛;iABJFDDDDDDDFj (@2,;?r2fĝ 31mCց^βmN4TL)g]$ 80RDDDDDDD}ﰁY<.gEy-n@Vvqb{,Z]̥6d׮<W.c~ )q;X %NAJD""""""""Jc/&fbgϊ,a`0kmcfǢ[18OQ}B=+C17r#"""""""""7Ro٣t7~&@yy&A:r}>}F<Ӊ餤#FDDDDDDDDaA|Z&;BJέTZ*̣{b9MEn052~*io *EDDDDDDDD)vRDDDDDDDDDڝJiw *EDDDDDDDD)vRDDDDDDDDDڝJiw *EDDDDDDDD)vghz'4DDDDDDDDD] * TiHPoiw *EDDDDDDDD)vRDDDDDDDDDڝJiw *EDDDDDDDDT""W]]8NU W!00J`` CDDDDDRD:ㄇc6SEa9z(=zPX)"""""r/f먺͆bQHy , TVV0DDDDDDnQ,"rӹsgD08N-JAt/,??)"""""r+SH{SP)"""""""""mG*?ƅ٪|;.z<2L,-dpvè/pƀPC1DLoމƖ4ػJ Pc3\ݶNA` me'l 3kfL )EDDDDDDDDnVCR{-O&5ҷDV:bs6F,>jQhl+kVo0pIvfU6&OA 4>9oN{?QYKƿ>ǭ$q}0aa(w|61=@;vRTui;XH6]aDPǾc0O&1To_ȫl;M&Ípõ I9| L7힩|s[7R5>{"(ھ?m TީS#XŬ_"O|LK SV/^מmTMynNa%!<6m}`eD܊u( I(oEye;ROlp%tIcIHbL`01Dkx0{yW.eոGDc57|[1rhR'_ `7ޝTN-OԸ |ԔWƀ]OqY%zޏAͩsIw'* m23w9>ǒݧEoPLḾ@uK7_|umfwon-kYWF3eXu}27T?nQ+2Sw܀!ɉԭe'e'3~Tj``_<*)9PE~ c<;(ldN>{7~_IBDϿsaD5;֒s{7fѺ <zy!O1kL9a ~8l8m /4V1_Ը1ý53M&5Y+ZGLk $Lh6?]f-""""""""br =`̀;!O)e@m]DTl۫ L* 4 y><`o*LVbI'uh8\* X|G[m`26%7臟h8Mb݆[7P&k} Yy}{w=wKڲr6L 7h)݂s+L޶۬&n^a2#Y~TP=BʐHµusy34 U, $->"et'g% +j234w ?O$0&skpp' `Wzؔ{;ð^g;t%}6]w KK䵨{et-ODDDDµR=Y#`DSocd]l}[~y:zlf̿?|vg?Жo x-?wهGb:~M}.sd%~_Օv3 [5 </}+ VT2:y};V=n >0&:c8W_!uﶝ ^j 73$ia= ז"tuLxO},뼝[Yu%m9s aX2q!=XX=g)rJx3k?09 ۠;DDn+l/b]z'@px8S@gWROh^7 *+PP鄨H^vk\DvZWLFv8qkzmgE9sW4֨{ ;|9uqq@<" onSdKF*JQ&6;DD08L/˂{zU1ٕ .<=c:P5Z{I 9y!":}T:؃1 N}Ɇ {;53g0 ǟw1Xw7+7 IDAT~/d_*bǝ2|+ff~P a{`|Ȫ@aG\ԚCy򡾤{גBp!|gRo&`ֻ 1'mhL_TCcbeo~]a_ uN* LKJM!L_ؕ93b<.[E)5ը48Xp`%P|v{wRBqN5֔HG.\F%0AnVw [/lr^ dSCpZ7>9ܲF%y:s ;U*/ꮼV<^en֗x8CvN-N߲ho<|Vwg$A5?-LS8+\c"-3R[RIx2Gټ-cZjݓ UR:> M d*NwVβrVm{x-Wsx;0.6F 5RY)Myc|L;چnO}w@i$f8l)iWk˹(W)dc8rFnq/]{aP21ƎRFMP[硲/LVmޕ<|~ؤpz>T{k =9w&QSuF,8nz68oNӟ(?5,s[ñ3AYLrWuX0x W^*'W콃08F ھ倇Q^tG v]gH*=_/LskFzh=Y| SW|32#ˆ1$aiu.jilvS_O@T7L ;""""r"Ot dwtњ$u\& ?8ή%^'-= 0kE-~<#x\g~:jߪ?9=[wmؿW~Dg?m{i{TW@4cB;FesWR.`"L1\8;XZ|0( T:\gS_AMP6$Ȱ;2c̲Hf^#I) AՈڃqfsz4Q.wwjqsVp `U HpT X^&1n OG\sնޒ.y[g^tV2k<D0﫥,|?5̔>dL'k:gy%>վڸ N\!ZCUi5ah=\u;V@ǟK<_C.L9 ,hRŻN#{w:z7G@KmDkkiẂ+-T"#cu/j~43Zs{bꏱZwSjfxlΆF& Y\LGX?fk,?WVI}HLOox}Upni G% APHwbmbĂ%E+qer_.1';{f@A]<޽Ĕ8}*(#ǍCx]T64r6p: Tyן0p1CZ 0!~~N5_D[ K!F-~yѣ_?bwV+}\: )~t]q;-`)d7B fp2j7R%CyjkşWpn#dufoyi㯴6h[~\ámp{֣:DDDDn軣IEd{bI>~sď(6b*ծO%~$6_HNV+W_$hkY95yS^Op?Yŏ!vW+YQ>,"ryq6?&v/!O^UO^`$vts qMvo.%(+:wg J4E7Si΀>[[3AV{Stʷ5;[s9~?y79| "-܏ਮYWBeG,i(9k]ma8VXg&`{rn)稵 B}XKMN#Ò^l%z\~㱕'|ׂh9l+\{u guK i}n/̄~|qH"^^sV]-qgIEry/un]"+/BmﲶcMzs` w """"٥"Į/ry;_᭡hIxLn byʛ2Rf{uy-j wNssRB`%w_}7?e.obب}ohF 1t%{>&g7XxfCԦ |4fcx`3}@gp bb+Y櫔kp[0=s3ebyG bԴ(֖ct&i{g?OP-'xkc_{f>uߔ9I i㥇[⥦ѧ3|jNFY7wǓ oxk]Y͵ûzwiU?&_=yMo|;#H),b"q:@_ ~8?gn={ypOwx;k5HGw_{aC0o♔8\s{!B;b`=v|-f;yVs??DndLK8ӟd׺V-|{ғN gn[}\.|=<7-<'%uoABg:*.)=qlE ?O$06%СCoUĢW1AU+6~F+1^7yk8^pB:ry^[@]Z"E]kpbݼ3Ã-L//fROZXQf?64.˥""rU\ﰹ;7SKyM i$d&(SܧX}<YrՍ]9j0-"""r%[DD~<uiqEҟ={uVa{xĤI,gzV D1瞎DE\d.j;A"j-rVMk~HSP)"""""""""NA;"""""""""ΌEo tDDDDDDDDw&{T"m466xx<8N())Qለ\~HSP)"""""""""NA;"""""""""THSP)"""""""""Π"8:B`Um[nDDDDDD:>}:_A-tC, E!"""""r Roiw *EDDDDDDDD)vRDDDDDDDDDڝJiw *EDDDDDDDD)vRDDDDDDDDDڝJiw *EDDDDDDDD)vghsYb;S`7K|efn4#^^XL/X΢H&5*]"6'(n(cƌah=kxdܱ(s褈nڠFSDNͬ6"'{,ʅ* 2w9V1p4l`ُg-5[EK8 7`$\e V/c|݈9 ׳hW Jjìt>'⍺ DDDDDDDDDٵרt&epSH `6 yr-Aa!v=+'f2& 1e-`MɏԁpQ.5~v>}$ko.Κ=,]2@2#]4R f+j4UZaAo@i{KT+˴/>q2G0 8Y{iy=&5*'OOr%oe`d # {7q))ٛaa*hd׮񏿀1eU`+f&eT?""""""""7Ӌ^zGW(DXu/SPTLYȁ1ѯrg?;,ݓO3&a>C{KaD]JY~>'b&21K볽TL"#?bOľGٶsXF &`"8k rvSPbFr&.qsxow_;t n[xӧx*kOM]uϪ7ojji7``:lM1$ @1 C|<0s}57DDzJu *EDDDDDDDD)^RD 'ɤVVgȗdTGeLeU†Y[9™QX,ElefZew/%,I#mt0DKW1n"""""""צAߟyDnM@`H  &&+=ggomؿte!">`sYɪL2b]1fxLaN5j^7kҷw7zF?ى/8]Eڢz2c"jkq'1/{9sF5N:],۾M&gR;Ye3XW<,ܺ9.*6搓zE y*3 MeTfIeY噌8ӾEKcرv/Uؒf4\^X,[AVjgdZWL^N.y{ MbҥeW',#L7屷zO3svDQ;YjLSH;{sl(\ %^=8Ğʍ#5 uv֤xWf06l[3jȵG:3Sڽ ,ݸŅdUjYN:h..P9XPLsLR ,Xf*`Ox& 72#vg1 n珥81U%n,X@U>^M<󾗝X)~Y*򙿨̍.*Ilɧ PM15USY5?:3L RKmYƞ: [Y$8$8p ^xG :W$uc16Pwjv%MDDDDDDڡRk~aJN&sn4/sIIP[A⚋)J\XLŒ721{X6(T),Jhw鈢vOU]!Zpw-3Ux/c؎%HsL&Z P_Ec㦪8-uo,$$\¦2cgclF{R*1RU-lI9$vt_辄FT汧kw67k6EnbWو cga7}<3rQ;iyX8=ax,z*j!<{=CQ=tnf9g/[,ϼ`c੭*rM&sDU6Duo2“Yp%M'/ܱ컫 Ŧ=̜K=9k7kJ~lev ő'*w,exx.5X^5+rH3 |>c) *æ*g³g:~&""""""[D.*"e<%QP!eFőpFEA}U5n/W8!<FN5e#9]_I_YF9?MlbSU3'u줙QP=$f\ H/QP)ҟ:8yq]XrQv%%v}y2#JV7, wS-1Wq;SϦRVPG%L<ea6C]NtW1#5㙝T϶mfdMDDDDDDwhH?ּGL{wXu3.cg$YRդ IDATe$\A͚rfEʌY{>q,s֬t. ǤX.YsOU̚62`l$9"|;7:0vv e9wcoHo0t=y8}gDD"gL楰q"b"""""""Z-"rSWGV )EDDDDDD:i鷈WIT0;eچ[DDDDDD~Hou *EDDDDDDDD_W?ss¾ύ[ZHcڣoZ\-.s:r1 EDDDDDDDQPffw{6P$27f\>w72HWHA\ws"|!wr8m /{7i9XV 3?c,Qy{cB|ûuYopcqrq e˸>Ƃ|Gq|paz`C$X#a+Szixxk<8e)'ຬ z?7o^Ȑq:η)#$&3WcԿs?@hB'!4DDDDDDDDzʯ5j܈G^ -|{weCv~ MXq)!Q&,}g,XΩarsnjŀ1R=l&<;;h#8iXHHLlOlMswsq:.~0aVtN ChGkY &]8""""""""=NK.vʎP`6~LyM5X 1Ym6 _n Ex'~`^CfKg<6#Y|f[O tg Etv>u[{G%MݿRDDDDDDD)/vwǮw%}у04 !i |ǂ1F}/0,+)õWVyD98ɩ-Zƀ]""""""""=IK `}`{}>x/&0Bƅ~鷴8i.Q=׮3l?jfrot<}>Z{Rr}aDDDDDDDDͨZG9DĖqIMg{̏yL10` D, Ct:gc{@9^ n! x8 |##phÇw=K-oŖٔ""""""""WA-yӷzF* uH7Z*O* ȚFر$''1"g׻T(yZ.ܕse269 dQ\untYvۥg;*w:2HNN%(pWfM}NweEDDDDDD5 *E#wWXbJKƅbpWKV[ 2OaK)--[qwdj&EusXEkTV~>ʎbܱ odLuTG'.;鈽,ӞbL;)X. H%55is.>WE35gw%3vԑ(  +5eN:-Qd,[r+qf1#s",V.̙$efB:)XZ6wU삺"ugWcF̙ΈtUAZZiU\!ul*i9qѷCDDDDDDQ *E#(2FU;?Tֹ ,q<^e0o+EEElƞT?W";gmQT̨ T짘{S#HqQ_\FUKrq:ְ}w(ʡS[LlܸOŲU+Xd-y[)ڽ+&Q+X&9©(( TrpG1%TeWrU<[[=NY+HݻY3%/)#n6l%7 k*`R'3o Fu۬ʎcE6cXbc͔=QTDٸ6eSP}ާ8gyylؽoZBnwJac"v,'z%""""""rRP)/qrG0efֽw16c>Ͻ%̭Y@ĨT쮣Wh#¡zUR 5+$`#~Xm_-z<{(r0X,eGx;:BWp$uTl5XWWUWĝĜԈΏ9 GT5{\S+ (s*vTq\x`&<6Q(bm]n^:n3F f[Oucg!){7۟16w%x\4wkKLRb >S.[ψemA1㊱e{pLvIv'W%kq- ;w%S_ޒ6ãb+eǹь̃:z\p}ADDDDDD䚤"UYu-vSWV*Iɣ ܮZ*(ȚϪId:b!*l,ʥ26ԝnK-\$Rle8]Y@A}'tD)ֲr'^QF$Ole;:CwU䑵lCJ3xKԻ-DņcE(ĒhS=7NvggW&|(l;)*(܊\[TCEK9ܕLr]L]VB5d@(GzIbIZQ*JۙrUq̙@YT3s0p `!*[BرM u?Ysb]M~l>4ƬINN{Qv|2K< òݯLĨ)$b~#-9LzjgM(ijhTR&98 Q Y:{ǒEɨT-""""""$Cדw=zFDY\.q|k/\ eYMz:UDDDDDDiF|-+7,R\ |͸ȞWL^CZzDDDDDDDZ"""""""""[DDDDDDDDDz~QYYCru0d,@!""""""W.]7L$D"gDg6o mNZաrU 4ZqxC!""""""W|6  t}O O?7 ~|>cǎXPx4ͨfR *E.Ct;zO)))89=h4^0"=I a?""""""""wޠRD롐RDDDDDDDN]D;%"""""""ry *Eh PHoSP)"""""""""NA:"""""""""THSP)"""""""""NA:"""""""""&xj_ 3Y$T5u9? sG/M_c l$`Uc *e"dF*)7f`, {9'ZS0+=lxz$&NfsEKϰ:]lF oY#2ABp ]L̬)4:""""""""=MA\4GNr Ɉ!Z''9#B9qr=ϖ'IKaZ#15vBqz k`A8wSnB֢px8A8طn1y|BU{#5<""""""""=LA\C|Tn}lCMg֠qʦRj3!#0ȺR'ɉ5ْ.݌sr脗#ia}AA.`(C> oRz2qsGt4'D﵃H6*EDDDDDDDzJ.3oQoYs 0ZmX;sK_C h3s '޶'Z 41xg!G\4BB]]-ķ4I?{%V|hZ:VHRP),SH c& gRDb>YB͊PF͇* rZIH!4NJXOݹxMg88n^͆ 1,Iĥ? E[\uf0a&$:nPNa#1ažMk>f('> k=< h<溺+9=swxCO(pxg/ur =*㈄f:m<`#f\MD5S32H`tmc ȉ)oXɓ}1bgʬ`b4n' /̔#[DDDDDDD*0t=imm;Dz什6xK0\UA؃Z)9BSb"!4r l0N)r77lF|-z Sf9H Ng1kZGf3nB)(uj %~8xm; z>}-/_łu۰ ՙ""""ңNJկ~vhkkv9v))) d.;h9qKHHGn,11 @@ku5:~@Á>-#Թ""""#4RDDD?Waz H}}X k=y[_}!姕h0Pu+Q&.ԹW)§Oz~ IDAT4ַ!Q)""SG>^}۸q-ilE\ =xgbFG3;9@` ֯_`hDDDD伴HUb<ȹs Z[̱_ =mmϛGӇ{/a<@ |Mk릾?O!""rm#"""5\^N3yFӑ# 7|zx6YQQ<,qv>ٱk|<I(5QAXUFɿlO|@r7mCnf-Dq""_S *EDDD?F} >{0\E4Bo>TXG"d8N`P H?Vq̫3)>k=Ek0#c ~DMьljrӠL5u@u׈J> ׾}Xn&N<!zZ{ޔQi024c:v~>i9twkE_Bҏ*CLm w  >ğ`OdmY/4%ߙK5X,"5RDDDiض {;3f\BJ 8|7sK CPZ`ں㱏2IIEDP *Eoj??F젡=7\QHy<ҳdm^8 ~FAHRYI[s3ץ_\Km1衇.~Oɮбpk+wyZ/&m`6 *E:W#S_@gbS=bbiK3OOOf04<>r_GDPP)"""җ\]@[6or AD=(hƚNݣ | L& 1$i|D)I''2ڞ͋W2k TDDzv !Ch_'Dk|e< 0B@` F`Gc|C:\{+x`1=? 2Y_qud;IDPP)"""җ;ƹ̙]4hkގ߆qc rGa[4ȈoGW5"}77z<=?v LXuE[,>~{y]+DhtLk E>ein=ů1õ5e­cxxoF݅ND?e%O&ph;`Wsw^KDDDgXo`>/]sv1 nwq-~ 7nr5rF/0Xe͟A{{1>v ǃ[4"}ľRvMr8CâYLff vPVοp{*{D=cd7Y.jvph-dsA!'[C>(a~f=ÿ%Z/d!#hvLn⺴vFĿua_j 5F_liA{ v H٧lbL_`6g*&ݔo/_#jY'"G]]M)L#5+e]d XK7},m~jJӒ[Ca%2.n_͆R 0+$N~~$K0cJ֭fWM&bBt ۷j|Kn7b4J}xITn^ͮ$s\\'C3k!}o:dhdۡ$OB|6M-xMsnOmb?""rM2ʧoKt a1t .4(kFd/^bυѴ:1n` Hpc^?GN>B¯˸)R^ȏ~˶~飌Wj-h⣩a?5@tntoо0k myp)G wR 1wcɶrobù/%jGrS64鉄1CZ_}5LY0ym}%G˝x#+)? '211l]x]s"}DOC3)/w$q t\/DI>Y0He""r 8{E[&l?黵`n VMy܇sNm &/xȩѣ\hEv?(Fwb6^1_7d[xF"C5""}U *[hlliiqs<[L#HIÔִ`+PDb0 nz9=+N XWc >:J [WhaZ]^-.Z[[GMI?\&fu,5_i> RDDa3f,NwH@`陏KG@JH!0K%%8 s84"}K_'EF6?xg:U7;X8D>%no0'n" 4z4hɖ<])-Z 1JX cB1d3AM{-Nbauj;a}h&hlVLY8+̲rcV&_0nVg{L_ !vo[j*gZ++  xuad0qG_VO?#[ fcxd5-Mwss ?q  _AHՃgk쬄T8B#FM+0| l@ ڸ]NB'eN"3hJ4y &;cmaܳrH}{&%2.zkvy .JBI`ĉ΂N^۶[r Oi%q. %:$yx]-VGXiڹ )EDOC|8ak\1m͜|eMy)nVnxQuJ>2,F3o~E۟/_{;##o9:K (y1jH_ӣsO(yРx&Oaba)^Ȗض`#:>ғ#1e ̮|V14qS&0JR7wn`=~={:CLuw0C-cg1e2qljc$tX2 NzHLOW7H1gǞxכor}n ^QTXH/c0{.Nqr: ~4x"}? ȡ\VygS7SYB=g%E~<1^f_;p Q)"ǜekkZq r~,xf 6J~?ֆvs1RRRA""# zA?9 o|fwn֎%eL;KMWc:g;~Anկ s_Xo ahEx v;/:‘Kūytoi EDmQDDD2s 4_鄌M :˅W`:}F…YmpN:Ĩ#4hOwc X|]E˵_w|\gsΙ>겚-Hܻ8q: a!Mh²]waYrpiK% 8!!'N$r/겺4Ҍ9$ Xdޯ M9gF~{.0vhKM fjM*(B!9|0ѫOiW_M+cHW\)SڻN$ 1\ NDP+? dVIIcJ)T Pa>;C,' Kb;6m}rbJDn9B!6l@CC)FUVyX?N>ǔ$UQcwgtu9˖Q񓟰FD"ٳV." 1o:̏ `6~fϠ);EY~14U/6ac0|s&T*x2M\WBqH**B!Ɖn6l@__l<.N:?qSPo;HկhFYU܃O3w~;nZa(vh8~qHSoR}MN 5QSxrl77~c|w۾('Q!! *T !K<Ü8q0F*\/|ʢ;eʟwW(]5z\ץ曙`Kt^ "RYglܸdK2|ٻRsyP1e׎RYL r{ !8%ANJ!oB,cǎ֎TQa.\HMMJyA7n$q ɦ&NX Nkqa׋UPwDӦXMQ8}p)d߾}߿NL$NS^^… )// $(ki=UœϿ:9g/_-;?ge0~ܻxwx_fNNUUUx^Z[[QJiiis q()$28>E?ycZzݙe߼ J-!F_^Xv B\`ּٳga̙3˗ʲV__?O2}=v?__=eeer<QCj=s_gJmЏΣ?O;]q,!礙 4B͛7ЀeevK/*8ׯ57x#eeevݻI&$ɑ U/gGwrۏ?iOn\|?7y>m"˾< K_NeB\lСC_ 0en~~aCGGJ)9s9=*++```XZ p#Q(/l:f-'mSS 'J!T 1Z$B V[[e#?&MĊ+Xpy'eӏ|$ !yBJ!FBqaڵk/2mmm̙3K/@4u]\ץRB***cY477MEEa-Z:m\~oU%s);g~WLOSj!yDJ!Fr IDATBqaq{Ca6Zk,|'f:o s!\E)EUUֽQRR¬Yhjj".===8p\rsse (XR|>JKK蠥qqz<NԩSy'4MyǙ3g(w}7'Of͚5vRh4F@äIeZe0ib)>"!BibEQ{9N80Rlf˖-TVVc///u֑H$^}K/eYTWW5y~/K8/QdhP`P^(^Eo忟QV r? ( ‚|-yillitj.%B2B!`sݻye@UU%%%būJ*z+477{VEtw'bA/A/{%`R x-j2N9!`r_ݏ"<C 0OWW'G7/Dzם;e1o<(!BJ!B?axႂMY:cǎqsGOv,dӦMtvv\v};:BQq)nvIJ 3[d&U8FL1l6q}N ;T;K`)ެP(>1|>M0$ @ ![! 4M3gd2(oҢE#Ӄmۯ[:L4y{:u\O)EOO۶mc#M20El:⧏u` !qݡQ5t㲺Ͽ4o>7 x{z<&~f$&Y^9a(mmxCxӢ(x5]j;!B!={:?7C,#Lc/_>;d,Y޽{9qb1㌄uuuqe0rظT4Y(pM84Zc2$7(Pf*)OWkBY#6vV5Y\\3X IX)t:~;LD)E^^@VDG!ĨR!\r @@-;w.8C[[㼦:k<oWQQӧOkaL\6ͮ)K3 .3Տ.֘&p8D`IWʋף4i('~LX23u3ǑӐd&Deb3f۶mi4VA(!J**BqVnCax|`LP)B27Qr|tܘ;w./eqQ*++?oe񒥄C^z;LEWkLCKjIM8^8;hze&W/9>*K,r۫qE#6/K~{69!P:R4wyaf^OTg;rqm5R;w$!B!8kN );^L` PGbf 3ifV643[O=5!IεYfq!?JHP ['Lcr4˪>n4O`s'HٚlzOWy^:C[YRcRcϧobܵ>Fo2͹r(ɓ SkZA>}%21iٿ?>_/"m!AB!F⦒8&o/6J>CD{V\oF FI?ɏ(rСCArrrػUVa)UcѢE߿4ejm}PxeL?W. P=? \>7ܒͤtfywWCqIqIwCKCaIaAo7Weva;J0sY~Kv3{ZpvȤb|>Rp ~(m3zo gϩ98S^^.#8﹃1>ƧH>HNOOw`^?Bۦ8y;mk@%] 3~_×Gz]u袤O?F:OFSUaN4%PSS;/[9(Aio2Ռ~bc˪}\:O{esZ⽷ 1~|&Ƃi^X|G=lޟ=MvР8 'drɴg*n5e>t[PX7rqMRh+SL&yG̖ͣJυBmR!gFi} Nԉ>︖ކحM~$ S3`y8I9BҗFۋj$i2aB!mD"aKJz㾍1:L3;桀N&6oKܶ&D^s7eMh\>ޙNx=maHG,4IRd?O41G!x;HP)BGKS,/Hz㓤3trq=)դsB>(CsF6YLJill0 ~?+WB!vR: BF)Lʯ#+sp?84DZAW:ɁXMYT&.^.Zk::ٻwf͢{3fwHUe%O{n=X{njjfĴ Nsmf]vb۶TSN뺸˾}smT"4xNj5YA3} 0\,>a@2 <T GdJc(ŷ&V1xL:}E^Hj`($=Rf^C>/nNrNFG SWWMuu5%%dI!O**Bq +N?4!љJu4='(pt< DAG!QOώ.p4 %K̳=G__x,/~]7urss9s $I,c X0wbF ̟u]ex^4vf^ڟ0 WSrfXCB8;w00a+VB1&HB!*CqRRSL-D?*S)65*'`4'K{qa7&0K*eJ(PDVV.jB<X,ls5v:iZu.'455,b===(~4I"b>%T .0;Og7Q,r yuK)3MN~n8ȯ?_8Rj"1\ YP C ?nf>+`hV;y,EK8ڻw/GdB]AB1fHEB!*e(Jg"c'iw\Ie,hl8IWi†IyVSq9sj9.jjfׇPS3ynݯY|mmm=zs8.C<gÆg)..brCQQhٺu+W_}}>  vO uW B~yS=r0ɞ-R>jLCuf o=~:^u/&t^ ^>Vj4P4L8qm۶a&f!cTT !ҮK8A&yL-is<'8Xh4 C,Ǡz`Yskf3k rssO~Ag``  r{Yt CAZ*+QTTȊ(,(`v,#0SNȜ9sz@fd29.XviY W+25PmPQ 7㺚bۑ$6C!%阹՚C6^6H6XN,0)RDbi7NZP 0p]ǂ#iueR^@4?KVfhCKs%<"LٳgK.wö ҕ pj3a;3w3vM^ +8r!W($zO\!~oAn4_ ẚxJxfgI&1ިî@A m.M ;l(F>NEyN '. O>$H5TWWˠ!s$B!3MP0gM>GOqƒz,~4}#-@͢d2&_y-%ė_ %x<<;uI˲x}χګ/NBskŊ<8CWW׸|i 9ސ?1 TTjPnKjMπ`.9a{02#gPJ/K-^Q%R7pLslӓ4=yvGgv54Q .umiLB{nZZZPJ ;w.~_F!Ę#AB!FE_H__gi ]M@͜K/c+)7+B-I=SɪSz}ۿ޿%<7zR):::hhh`Ih?ࡾ=m'*EN* AA~K-.cL 23sQ <'szQhq) 16Cm ( McM*ּ:>} ~L_]\]`vA^X,Ǝ;Fkjj(//B1&IP)BN+5U8χ%Y8RҀ2P:qO܏C& ^wh'>< |utDs+񼥵fΝtww Xz B1MJ!Bug l~?FqKC iTVVrQ:;;WAe=isJE+>iC IDATlVg"W%xzG2˼{}.qCEťs0 37vյePgRLHF&D7/S8 _:Mczd2ZZZEkM2o& !۟dB1^ZJ,>pSZZz&Hcq>tL1ഭ {ņ]q9S._[r2銸lޟdB ZJåsu]fjqcvmB(+xKJ)8vMY?,p(Aq賸Q+c7|dxJ& R)l+$??_F!Ę' B! رci U]&# R(4{Xϣ ^ڗss NCVকAt A|6p{m/BNa} "BPLnq^ ]Bf}Uz zƿc(ױ0fTTl޼>L2*!B! ,X`dZz{{я~C=D"ǭI˼7F -E9^Sv4 ECã/]*,tKф&pY1Ge[nCq8!8>Ljv|v|5+T8Ώ+{Qрr{0D<=zZL$jժqQ,BB!m, ˲غu+s~ucePC8`"z:g*-5~br(}/ 񙛲,uK`G_d<]#T0킶  &4(Mfz7H+MOO6la&+V%B! *B!9U[[˖-[سgPw˲|ضM*Ǯl˪YxS$Lޕ x,Hٚ/R Os,8hE&DqO<% G,HBH9gKchyetfx}FT*xʔxIlذc9ɓ>} BqE!Bs}xFBaJ)]=|^>n^KkW?4@i΄^Wk)\.~\7(n:LCS:Otv4)o΄WB=j̣f^97U$ yx,\z饘B! *B9>oiH'ѶzjۉD"9x`htϼ+i8qNN*._#WX=.?&;8m CiJi߆o¤=y0L5%3G+ɬlc&d2'9¡CLC W !$B!['4^%+Fo{isd29vv*Χ#'҄}?~Ey콤6ĸbAv:GeqEIP\S$/e憠443 |yId<D"كmhYlEEE20B!% *B=M{';#zh<?oc_4wΟL4uP׿&,wvvd`PUIFuz"vYYgjź 1c 5=L) Pf/wV5<'``+?pËa8.{ĉIqq1/B1nIP)BԾ%ӎxM0] S(p2ɩq>OF/n<Ūb *ϑr̙޽{O|<Etu0$ߜ˿7t'(:!ǯˢgODiuhcȭk| Z4);!0-CI*>s~"b)9Aٱcعs',j!U!oYvHW:5yey,T-KO&o0E"5rM4ic;. &5 "dMvm6ZiE(L2JMĒib4 ۡ/n3Jg Σ6Q`ѢEv#Nj<]kS`Y w8yΊք?+Db.!Avk);lb9hs2`Lp 4l]}/@ \S]5C9yVH&8ƍz(X|9S!TT !0VTh hJ\>fN$vxMcO h:iD$L+bIEJ)Ṇ/bBc "&iq̉D6=$TS 鎥Ѕְ=Xgk=k{IR#q70>@.#-6$?"e;;*`JБq]UjX1SS)Ȟ۽pSmBz :T`rxrqgq\eΜ9TVVʠ!W!ⲂM39PrJLH{?+14iQ+& zY1eA/yP+7+'JW48ܴ`2 xyMSi={G&7̺u0M0hjjbڴiq<>b4h?)&vE)p\ (yqz2J)\W1 ZS<U3?nۈgv =T) }5[3&;ऀrAR*'``` &x4B!#Y-B̦R\W3-JE^,?EY~Tѕ8wewK7ieޤ| ~~1~l\~u[o:a?DfP5@*`]S#mjsjJ B\s5i à3^7,\8cfOKPx"E1Hܡc5&i, h<ˇ4Tf߾M,TBH&ٓ2g1'f@ L@^BT@V13d0nxeCĉ@&_t)B!RQ)B0^&tP:|]{p F`HHPmْ-N؉xdיɸ$S$"-ٖeE)Q" gu )SIH6~xz ,CQ c _tC|l9<@,daGI 4%ctfJ\=lPf{SS\?B󨭊P0PpI}Vp/6*lvS qM8&rJ۴ָPiZ(4lH&ed^I߯K҇iakەא2X#㯦[}-f8L5+ä?_aͿB!k75`Bh4:|BzL&֭[qɓ'3~xh!3R$8'ya6r6.]*$|˻hx[gs}?Àl m<G*! ĴY _\?eRl=OkG??D8T eǧ;_inn::i YoT~|aٌ㡇B)̙3 q2xövhN 1sDdÄu| H4'JUԔ]>F(>H_fMp"5CaO[z@R +  `3`!9`̐ptuuGWWLappQJH$!LD}}=֞y=cܹcǎ宻BqIR@J!|{Z _*l6 ,S1)itgȕCK`5X5EAU.+EӚh$l]_h4PVid ELx^nR|pXE$޼"O=GR,|㟠*""[6y ^ӺߡhձIfEkIfMAQcud2Si<*ew+U帄B٧9m/1+ :x+9T.{kа=y{`q~J%|M8|0AMM F"JP9Y(^$=ӧ3uRZknK/@UUstBqɑR@J!([vh/`ꓜxh J Nrc_S+׫\1cGC|@oα DڪL{sȦV֮}\E qOVyBmӬ,qz)ܲ ˸O}}{* P-_Q?Om(`|' } iH|35>e#DuTPZ=٣uPSFB`HߕsT*~zx ,y1}t,H$eY'4<qp]Bٶm{%ҥK;w.|'ms==Z_!%GJ![T !{Ƕˬ^ka20>|צ׈"UJ*Fa(}\O GHqÜ+q4x "L&ޮzt6ow5MX0"!I8'R@} }If!$9yf~i x˗3c "J3=tX(o~͛Z3e\Ŷmlfٲe̝;W| !$IP)y AByꩧ*s2J1r_5(̙3D"q^o۶m\. nva.$--BqIRJ1}t+(&4fÞnO qqn!Cl:V(tK0Oa;16Ɇ (~RU)$AB!.Y?;v_g̘1{v,թ0_\E} 5 5^}l Ϙ:x5G}(ӝou9Rqkpzp+buuM.BdMv^;~T*+B6峟LBKl$BK_ee˖-2(f2&o }ba4!$aapP E<_SrjJ渢3ؒ%,M{ǿ<; "5nK\_ydŊ4D[k9t?OebB\rR!%Kf޽qQ]]-s EjVN$ acoOlAٗzd \Qc8pbE xLC2.4 cFTŋb3F@.cʕ m!9yMeX|GWW;w';IBqɐR!3gqF2 IPyiiY|~@S0)^e Ӹ.`(|c 'vר's+Ɏv7 al&9<^uUrG<_f޽}oRr9Y8,]ʇロX,[7`„ ̚5K&K!%AJ!B\nv~ӟ7ߤLkzhߡ7J[G<*˵ Рv4 !S Mn׃XD1΢dє0Ljޖu~Aӄ,*CV2MfAm9qa\~z,XE~zQ6mD$9ӻOS78}>OK?x<.&'AB!. 555̙3gr۶m,]Tl((Dl-xD A)|9yմ0:]:=1״}_Ks<5J)h ܈b޽{ټy3r@] {a۶m,\m|oݺG~ qs>Lϟѣټy4[BqIJ!B\6Yp!k֬,yHkaX:XQM } 'G?NcD -"47x|}F8z6KsU\15LM\|~|~r<4SFxqK?bP"_hw JeȈr?eYDQٲYz5̙3礷y8𶷟+ڵx _e4k'? !IP)Bʸq3f ]]]twwcf̘!sh<.FeHFkE4n~$_;9˜h6#qdU^v9 Dž=Sh5AC8(4!3=$1L&y2 a\ѺuL>mٿ?b?1o 3[ 5w:t:ɓYv-bQJ!5٣R!j_|E@)$Uvȗ5J NZ].G|@ E賳!%hk|44ѐUQER>RКG^Q ',H`Q|٧&y#NSSٱcUUU̜9?L?ª`"nj|'̚7ͨuD٫ IDATQ3C}S}74ٹsLBB!(Xp!,O>x 20PW.=8Aɘֺ(P`(,wCMVQN|OIDa@cBɷ>WW>]C,b`TBI_g0T*AieJTgd\vM._1W_w9s|LJCwaGǸbqc/mǃN|jy|!UhvLh_[q9G2 |,V,OKAMaX 1NI&Xt:t5ӦM;4WfjB0iҿg|[E(da&:xloYtjv%(&g:B!lz"lv0.4dUA|!M4MԥLJ%jNp=ew}hwOVE1–i^RBaF ECG\ "Yq }3/Uh=z)H$:u*rω ST ˲`„ 2B!.JT !6|lB>g̝;fsZZ_-S5*&}(t}FSF{=:=<k X0M}2xσ.! ZLLžh6LDŠ&ZCF)cw$9 3TW,\n&ƍ'y288Xit1Ԅ8DQL>mv\ץ:"8ΙfYSzOM!bR!50x'0MիWя~T< :6ɡ˾ˎ RO(%[SWpvaٜ( xhM?ļ !L 34<9\ >&΄m~fLCm%?8A?6{6xr gA4ش$1iL&d2yi4(B4mv(E446M<"Db)OY3a zy 9Y*drB\$B!e/3}t>mlذ3gFepR&Ai\.@N<_SeضWO?I3qTt 0!7Q4an*Mq"fGfv=_aFt U3+5Y?|-ckw:ʚ萅2xI,ö=Ƞtgq\p}t`)cVS3b,?/B!?9uO B1RIP)BL0qƱ{n<;9K0{J> )_SY(}R& jM>8Ƙzf3ZN * #:W,Op[oxL_B9Z 9A nӆzt_3i9_#l=USo=>i,͛DŽ hii ΣP(tqgcJV&PJai=/ؐ~~|1MS&Q!EKpB!*̙Ñ#G( tvvr,i yyK F +D>SMKe rSVe:;r}̟|&5a}_F&ԎZ;jp+ eP.>:Bm}̼A~g䶿:̚"WMq=qcG~N5j9rkAww7ZÜFfiwccw ,{GQM,Q[[+(%AB!DESScƌaݸKkkg)K$Zk,ӠdLbtšnխyn+3gB/ߝ5D<=i*aEo˜v%䦡AP nAHp-Ai|;(;xF5R6Vx=V{1_VQJ}ʠ23q$lۦԓ;fA)Ef0 IoK&M]mP(Sgpp2B!.Zr%Bqe˖NX-[0g3Tc{ЗY 0ӤL}%6{](C7K8/ʂIaϏWA1,fZh i~6)nYa +8Mw f}b*T tv}g ۯ!W$r|9;v,J)v}NV8Dm}uP"}(R+%Aèw*㻺r:uLBu !B4M.]3\?;JmO^29Ēq&d2'K`a`_Aۿ¨7T|ס}o7~+}׍:.V(m۴OSS---rMMMDQNy~֯}BBa&L ]W~*CqW}+(W_q>~,X (&AB![L2VСC̚5 l\"͟ޝb> [Mo&Gkj/n-n/ܙ6&@Q,D \ yB TaYf ۠0F6C C2[m?h ">|EIPyM:m۶rZe //UWvb븦>p^{ &e`NS}]nNn/v t<_ۏf6&wkeW31~|].!Sq8IfLc`J0 0|0BwV  *'Q>`p?~|c4R,kx poodђ%KذaSk&SNe̘1ddƍˑL&þ}:uT~ !IP)BqXEi& o&^{TUzВzwx}{0 ̟a`-wF%T#$=0*S>ù:oT߆F@P])Οfjjjؿ?455c\e˖-ر]'Qy*ڵ|#D"ۧj:ڤ*6y1:z]MY8'&Bg`_gf:ځUmA0"O8+L7[M## Bijkkq]Wn?);w<Ҳ,NJcc㻮<|0;vx_e^u8q"E!EOJ!BQSSYni3p&5x$^zPPYT'RǖZŅ̓\UӂJ|uEӔ8P;V < AAUá+˼8n'9rtܹ̞={R?3f`̘1lڴYfQ__̟?q<,ˏb\y×CI;ٳ;vp- &'AB!)L2={KOO۶mc֬Y20a+bp0Ì>>lWʡ% Mu_ D,Ŭq!jMQJ>h"'t RX;Ҡ}L패1 r`],oǬ_n477_=\'|&-Z$ՔB!.2B!o/H0e<72uN)HpZ_n?GVi*<޿(53#<6ϟ}DԠ>HLSaRxMTLV/y?cu,;CmM`X8*!l<^&MW\+ĉ9s <:C=DP[nF&K!%ANB! H$y֭['r'AMM ^KHѓјADGPX55}5-f}vrXZ?JUATQnZ? =俆X-~g@.P(ĵ^˨QxGL> D !teB!ҥK+K}:DWW 2Me2??EQ2ELG)`(ȗ|޷0ƄQ:lk(\/( [ χcP3MeEBMy?]Uc0b`Ղ74j?G s\fʕ?3=T@}}=˖-# ~=_n[n_&H!%EJ!B0n8Z388ݻePB"^9_)!lwhɷB)Uakm}~hB^" +w^aCzTh 0:! €>TCeࡰ3!˪R3~P~LӤ\.u̝;[o0L[[kRSS>1! *B!N-܂eYΝ;9| ZS_WJ1?VC,z8 yXL /}E4X•Ke*,\p T&AsF8MWk#0*(a߆aDPJ100 `.]J??0;v`]*Xz5?8 |$Jɤ!HP)Bq(K,mJ+s$ 2L|)r%emM؂dA<cJAנ˯Ga#,mooGaժUL6￟t:-"42Bۓ١.uI 9 j_PЛ(_|=v~E9h!>7Oޕ>GM0`HIB)szj0M0XbHd~}}}7aϞ=J%Y`#Ja[?q!Z[[imm0 R]w/BqِRtb+AB\ϪUp]+V Ouu,=]=֤82`@7Ng5Pt x\3+*M|#AFyt Ր+}u.s$Q= wfi˕W^agg'o&c``hjj"NNO.===w^0TMŋ1MS&C!eEJ! T !ĥc߾}7oݻwqF^b d2,XU9\~]`+JC1󩯶EA ݐ? ^!hd΂[h3\>ݻ\.a̝;WsM6~pn8fŊXuǗJ%:ѣG&O.Cc?rYEmm-UUUDSS---2 B!.[ B!Yhe:::mO& R)QC3_7ot:͜9s(˼޽RtnuYh;hiӦ1m4\ץ\.)J'TA<'eB$B!8ktT*Eww7I8444@ !B!gaԨQ,_D"qp0 m&uضm/7nx<}bq>nj#UB!"AB!Yjnn;uOM)ѣGe.cǎ%HYnoͥZ6QFqW~TU) oxg/R|>O_o G1!P,k!. rLT:<(0MEI()BT !BC|] 6l E2 ]$7(MdWV~]bDSy ?^$<,֭cľN IDATz2K]B\q̜9SF!dJ!Bs,q7_4ٷo'Ol.OX*fQSWEy=KG5hxhU|87̭w VT>Olg``~-I4GD"TWWĝqxW8|0m`,X #BT !BL4dSk+s%Y!MoNx9hX4jSU%M {OrZ\;O_?P/}aСC۷3fybر2ag`۶m޽6mW_} B1o!B[o|5L0fY[:K$b hA5DÊ]6>wݬ&MmuEٛ|bx͛d22(#G /뮻nDV !5CRI6:Z>ymS.ikkcҥ2@Bq t Euu-a~&y!KO'0ZTeI`߆ *->vvZS,"FC}s,Sy>"h|e\; ){V8C, 9sy#: 1cHD&$cOf1M믿ӧ!#Q)Bq f(<1 [Exuo'xy[le:TK^[)U !!>[.Oo( (lcq_3Ɔ/blܣP,UZK4CJ5`ʕiƏ/A[y^x,Zk͛'!B1IP)B.K%, \/3~&6eGcTCCi(E"|)^^-%.PY>Ty'.5Zeg{1^_ńFM6P0NIXy yf~R (+tqX~=G}LUW]%#B`T !B%qe466o=vˀpHPaAX9);)9){X'Ta],Y&^,%1L5@'˄ [laÆ Kcc㈩ʋbhq۶eڱc;v`ԨQ,_\z!bB!R6_/ѿd{C"bP8 Gk] .QkE$d>y(eT+9`'xs}PJwpZ Lֻ4zhxdž r#}aZZZ;v,T+ɋ/RT*m&͆B|B!8  ?Px;T@QcpЕ񸐱0ڧh ,+8As=_JEB)}; ȃ0hߧ%6IT)J2iB:fڴihrÕzd2G>>3~xz{{y'0 h4-B,X!o!B`;.b[OנG"(0+J,Q T*(APm2yEsU&LGw%᰿ӣ%_ J*5A%%hJ6 bi*g꼏νg4r- !8 H6.ea<.eXvZ @ N!KbU\$KV4{?$ؑvd~dIS{G_γjd*瓵*_+VՅa̘12 P<gڵdY<W^y%20B!%BJ!BW,)X47_L8h`iߖ[ )-P"qN٘D)E[~eag^wȯfo{8ޞ:Ɨx!K6?&^eלgܘJR)Y|koo'Hpa~|>444իWK!$B!@kM&!KusT[+P~vYNT9ټʍekYůQ_,h9МcGccg[ 1Cew?Pʖ-j?)|^etwְZm\9GgWƏu*ϓ_|M6Q\\[˅adY(--}]퇦&ZxXz5i*B\cF!Bs4 |^/ -yY5J;m ~D+Фʍm%4^7(ȠJGQR`P6(*0ynOlL`ٚ ~zlj?[u vNKhp !f6H5]]]lݺ5׋u"6l r馛r !( *B!΁ %=> -S5JVZ6 [-b)m(9ˆP`Y!w^MvNz0`:OZ&fSg's q(+&-{ 4gy,Kvxm,^bLdΝtuu&v Ad2ɺuj*-B\꯱dB!FFkmdIi͡V Z߃ӁW) ;(*$SRN o$6Rj~w1ۥ0_͜MK(RvizlEN֩<Ƕm̙3XxaV\իy]m<쳴Ho!2"kT !B@xh1i2k>j1"\fgOMћn+$w!IQv2yi &|Eܶ"@߹-3nS]Dȧ ڕ5 p(r/ݦel؝`ahʔ)QPPڼzTWW}ڵ8qĉYd4B!.T !B[[$1;=,[4 }&ٰ7C4w̙?ZFWb\ o/oIJl@1?pk,1.u)?Y/6^)fX`@vyb2і-[8z(EEE\}կYH,BJ@!h4G,RYgj7k8RY6ݙ)ˑcL~8~tƹ~Aph'@qAEmkL`kCN˄oqp &Q߯h儔TܚyI(}x!I$ض}WkeYh/mlldX//r ! *B!AOO}Gh (j=@P7XTzn͓hyMy6%"D6Z۠5ɴf [sXƲl\&lܗޯwY\.6OG1EdrY&P^]l7xBvرc?9vEndž bF"IR\.:JJJB!.#2GB!d29zƓy`SJ', זl!XZS4ht(^|:1xO!j<@S\iH[LR79@V f~^_Dm97iL 1P(, ;gX8E2#/d2Il޼*3>ϓd0apD"ƍ4M/_B!.3T !B0=yZݷto26V$&{?InP]Tdj4ے"kʲ9ςmkB,aq /Qx_d+uS}"n̤E9^#l#7q) #/B,XƍF]nFKJ0aYHɓ'_Vc܌֚yQWW'BqR!b"x<`r&D&\Y+Wx5A*\| K %h6].OsֶD)^©^O͡߅Qh7AOMh Sƽk?p?k^ÿ$ep"<V[[Kss3袄p˅mtvv^VcrQ `,X@4!2%NB!FHc}$G6 x*J'pT$36AEAW(+2h&ϸ;N,uSa2щBwt68?Xn-!'/hPK#튺q*P٧a\q,_P(tP^^NEE&Lls:t5%%%,Z#BqR!bl"?ԅ4PJ QۼeY8dtNcY` o-9a363hDlDm3 @gv۠S,5]< '`Q7.eK*^>^/]we5'N`h \"9B˘B!# Ie5OqY4 l[S]fbي<3Şc9VyЛh_T} ` Td6(eРL $-Ose HwBlu~̈́8K\/?svӐrʋŲ,,~_c(6l rXlcƌB!.sT !BP  cOkLSQ²5?%xj{H03s 4SOټiiڂ |},+FB.Sə#lr/oe0P+ֶe(Yl^u(#Hb 0R6l۶Yd ӦMģsYNF;06+c&aHHqQIP)B1BX?Ĵ1.vRxљ<.Si*R'wr|ymo!`l>s1~DG\9/Hy㧰1 ',bD~Gs(vnL6p֮4,;Ww 8Oi>yN2}t &Mb…2z,[,, i,"L^rcu!1 ,Y땃HW֚n_ͷNbucoo lZz籝ky|sL'~nw T !# *B!Aii)=E1Xb G4) j,eF=Ŕw9Mt-3b]WvLz;[F#Ho:m:goY<TRR L0ݏS% mD.1bݺuI `#+dkMC;( +oooxƎ6;[\߮~=4uok~ozU;`B EJ!Bsv!Q_u9ҖQض:R~eI))x\N6!DGOd\:QVNH+uro3T mUd1ƌsZHi6dga0s̋r?@%Ux'ɂ 7n8BB5{[TU8ʄ|OwO>a'I\za!ī"AB!9PJQ OPU{ߺH5~*`&RS6֐}.ed zX0Ճek›s0rp8 -gzwˏ7?{8feobڔBwtmmm駟jƌCIIT\|_r% /T{d2I>g!9hV>˯ks -3oNM7G޶f濸?`k.>aB!BWLJ!Bs ]fO(kwcNuJض"Ttl<.E4tSbDU%.n#Q||㝊JLd">Gm{V$ 9±cǘ8q"Vu,ϣX,ƦM(,,dېRD f;w.-DW _-wwwW8^YUY2 >or/_+;EHP)BJ*EYi Ug;Ji<_?O\.8m7+?y Ea`x_|kۇ: T ~0 WB|Nhoe1}d. tO?~<;v )s%S7=q0'O`uF35/"{ŶmƎ˥r0AFkk+'Nra&eee1fjjj䃎`>Qqoh]YXGoP~UR,#8gT !BRJQ^VF'Wg%( ضV_E"eS;'ZH{4ϟvey H4Nd&GGIHV1nσa@΂A`gVp=:/={6gϦc> 'Of޼yTWWKtVZc=F4_䪫:!htIn @aa!_ߣX,cǎرH$2cŋsWH 9rl?5ZqѷpB||cB *B!^0pLR(zKO}4=9 )͋ g8ڞQ-@ۥHmG+T0ݽt@cuW =4 `*#8m(hnn'7yTb̈́ >}: D%%%2?};_Q(>uۇ87xeSsdG !FZF@!ZSZRB<gD[ #. QtY|}MEk8M*mRawVBU7}`(WiYN&i@8e&A|]yuޱ0=:kMM 555̌3N<0S=ρ,0`T«3*-?~:Dii)7pcyazzzRQ1ڲ,lBKK Zk&Nx;W'OsN*++7ny];0 Mĉ'wx㍲їt~c+GնUw~;~NvbD́>O}VCGk=eYeep2eaHY1;Dw͆:z;Ʋ M x"49H(J >Z> v ?"OÑ/B & = |m+JjmX,F" QYYIMMcҤI:RJQTTDMM ZkΝ;mP(.(Nyٷo˗/o4/̛PӤ3fDعs'̟?_ֻ|/r">sLJG6NĮҸS獺0U1:Ss2B7je6%e2L&1MûI}Ce?gH+q7u?Q.g|I4>իWKH9yz!9>g/4íފa_RY9X2g|)j*&pyP=G;\.˄CR!(//p~qJ*\0'{,Z M?9t?H([cқ5*]{^q BTUUE*"H T**}Q,MMM֞v/-**)NOH$z,]T:|2k֬СC\uU\wug&ɐfe<~7fٵk< ^{/d6}o|V0mO *ÒR!)++%1(x2VѨ90mʠu ,ݸnf͚Ō3(m6mF]]vO&<34448ޫ#m¢"sx<3ο[Hz Jg:7 ]$!PnL eʔ)gTS޽ZZZL6 .ٳg|g㏳|!"MƉ'Ψm֚sqLӤ ʺHWW6mne?|>^ʸ[8}`yzu1vQѵ~49BKO;˦sXw-0~Te~2< k‘K(wʎB, *B!. Y,X$ ~C$ R O~B__Yϯc̙x]5x<\s5B!9G˲8tu]/[)DQƏ穉'o?c;B3gΤ={pn_DQ^Y $}>>L~q5C6]c[ \v֯~+7n`GIIN|s`[-kY#/[z#6yB?m-s*p۬ke[ضM*'g#AB!$~/^LMM 15fMv5rNVJ68q"$ _瞣M]]tZZZfժUCb[6o&˝<5ڲA)  S_;oBYo'0AeҤIlڴcǎ1yj_R)8v.k0,**RVVv֩aPVT1lH'tperfp.L l.w<,msT_INiABy !%AB!$(AƎK*:cmhnnf1k,.SNgy3 twwfx~9y 4wq[?9a9Hg3eCRA)) *B!%43?raY}}}P^^NUU%JKKYv----~aN22TJ1~x0TWW_]vЀR!5Ge}wylOOUHqO/{Ly/}>oJ:U碦4ihh$xiH$yPoo/ZkRd23JӞLL&se܆t:MOw5'-ۦc'me(v[0Ruj~̟<Z"-ta(T63lf$Ko2&kS !%AB!,nصkTӪ)cD*Fn~i?>H(D"R) Oeeຖݻx<sn ".d2I{{;ӦM2e[i'!J"ۓD޾>T&ģ>qȁzN2:;;_QS(۶CZl;-\.RX,F:<|>ZKƗ>R8r;K^OdÖmћa&tXCSg+e%&YRr?^bÁF?hlmc*C+CR!B\6RR[[K__>o<˲طo[laҤI2vQٙ0 :6o!GSS4M` ~Dشix͊+κΠ=ZZZ|Æ}J)l~٩ߩd}F+ m7 `QI}˘9^'r)**²,lvv`8p?J)$mmmv)W&!LvϯS])u n{9z~۶x<~@ra8mee%+[RCUUcǎ'/66-mS.!Mu{&mEa F m~eW\ce}=~<CR!B. Tɓ'>}iD"y***FvϞ=p8?OOO}}}4440s̳^>LN6+ͥ/H9 gxY0ߕ7A:"94iƍ=z:Ν+%``pӾR, i`YiAp-bְJ5zp۶9z(g N=mr%^/^5^ɲ2jvv_rJ),"S\\L]] zqR^P̱rin5N[o j*IijXg3.՟[!&~d&(B!u0 *++x<~F }v̚5)Si&L@kڵkd2lذI&IA": ڣ ^@kX-ɏqqZU XQRR챠m+oa@ڗ|XpuNr4 ۶1 c0dTJQXXHAAZk&M:z>0a|{^ƍܹs IC^"\ʗ18+sM?M^zDێyWKEbhT !B@@ʞ.H&R):::8y$V=qDΝ֭[z!e&'K碲hRϿ z6~G$Js5''ʏ x۪" Ѿ^rmYoײ /e}v;Xl5k7|Yj{u f2vw<Û_{üiw4Zs0G{|dB I>B!(..fٲeTWWx|L0y• {gwWw7D2xN51r695Ұɉ.O_;l1%J"|mmmlڴ ǂ <.οC#~zvMSSLL&C.; *++rbsO[7-@wҶ=.r.[o| 7.|So4R1PyשkQ^*Zi5~~a}LRyt̝0UI,TT !BӀfL<^Z[[*}Y:::3gUUUx,U+$#L86EEemrGk4 SQikk|xMu3kwTdذa#l͂ xrFikklz& R^^磲`08X1NI&455 vs0x)nYr2]Noe+026/T&îmîM9^}u7'w8S*'mmvMޫd΄_'C B! 莜hll$XzaKR):;;Pq\Xl;l OG{V,P ŰmEֲ1e@V|&͑Cbiۄ ,ۙ;`[;兤&NI6H$B&9뚌w۶rg<{kb6y+a<yf̘SG*nr:O95@{{;aP^^.)OWݷkꖍm|nV~_ 7̽f,& *B!ә4i , Nc>gǎ;vZ0M}aY7njDdD*@4])&n~IS2ۋ%2 zm륬 jjjɹ|>dӦM] pfU\M+o>_|`]8'|o9Χ%f!AB!͂J7eqqؾ}{񉦣tci"q Kee^I9g`*eLȢ{IQߓS |n_!lbx(?c6lfP)V)9қNPFpP"jBd2I8&0;;K<gzz4I$TN.Tvmۍ#777;M{*'/J޴\.ZZZ8y$lܸf&i/R*N?6-ˢ9>/ p˪1?ON;lOy_)-[߬.[!"(T !Bq-LYM&/{m(0-X)8?O(**vsN@&5Yܹj.'n$~F(_;}x\}ҙޖvFq8T˽<| /w)LΙs䃦ͯpf̬{i+FS H,1D"D4lA—%'77v;kY QSSC}}=< MMMiuuu}a\by<jkkYp ^H_Y{k*>ʗ5|o`9˼tey%<;ߠBCq$B!t𞞞F),X 46_ qX|3yZhu#s\{9f7|iGSç n,a&d1~7ʝ{}E, s!o.6X7?[w|>͍N zu~/ļn6M#HKDx~-vÌŘz[B|  Y?Zxhjj{]v}_;W=71njv|2b>վ`ϩCT~9BKs$@!B+x $XheLMa) G^3;W>%6,eO=O) 4 |B~B۩5׿>]>vyo|m#Stt Ҳ5M0ԙ)S'"~I2$LH$ PP(D0$F頽p0 l6N30 <nmyMw'~W~i~O~MC˥gy+VaÆ2Zm[s|>vů|%l}-U,Sv^P,B<`"8Dp}]iƣȄB 3[1HS1^Nf̈́NX`" Nwn2v̎FDQbhxO-Cdwv_@ J*ٲF>& B\rT !Bq-LbX.'n( +uM1W9 Ҧfh:\noqsLTpQ5vxRg?`.5#Q7Kfu++S95or|֛۲ӳ:k'Itb2Je¿ ݳ^/\.lvl6#[M).ފ+wc=}Gyye?^~077ǃ>HE4cn\ w{}]<ÞV&CLό𞵷}X(&?' 'Th/B!WBa;9ufs|\4iে?=M yPbpؠ$}~>t@џOt~u!ِ2s, 9x?H$R ˲wnCiK^^>^/ٵ%vvʷ?ȣ>~>QVVv3.FIW?}OIe)4 oJi:2CkO:ٵKQ@'ϴqzRZRR81r4E2̮+мχ林K~~ iW?1ٵkk׮]i:s=RO|Wtu7hgHKB!B,i4X8 PmbZʲغ:. NHRQd8iXV`C% g6rˡa ޗߚa|$٫qvmiu Jeu&Qu-͌MPZZBnn.8RuXz5N{sN 1.CI&LNNEmm-wB! *B!X&&(..'J*psׅT H[qTg£i htZ{|o -L{ql\ah?࡯MLs9@hBqjTp9՘NlpMc*hq+]k(]^INt־Zձl2yN:7M6oLCC˖-r]cgΜ+n6nVn|!B!B, iKtt2lEِPS Cʄ!p,+3Ur AjAa|xoC3&PL["TL-6r4vHдlUZhFkWֺT^<s=zCq멨<[n+066CCCtwwp8ضmMMMTWW Bq *B!X~i$vC/*4ji0|'PaӸF'7T;.Qӳ~XX"5)޿كRw覩Ah$NiM)@ai τgsTN9}kDuu5_'NJgg'~ϗm~p88SSS$lP(D0;+WRXXn,u$B! Ŧ {Nr(ei!uM T ӂT:Ses;׹(ʵᰁkB69#&Ź&S kTT3?nRV`Ѹb*daiZ .-4Om.bO iSh)FLF ,` 'i>״, ]ky0 ejJm-3(ͳNi i>~iE_p=<:hծ4y1 u"//<!x$B! H[pb(ӞRhJuL,v%}4Ѿ$ 7{Bh:(t]#׫6Ϯ5ih .GQG1:c%{<~vʮÿWȲ|? D +X4FU(2M~L vC),=(B%AB!W&a*qah44-֤ץv' GS}[׸OO) ]#䢹ڑ:ˊ2{y(ɴљ Yܱ:J)'|ww i~z0JWg6dL_`QLB!őB!BNCc  TB.teix]ǩoφI$?s`klovuew'DKQZ{$]7(I<#stx rt|nИ [躆ºv#I*B!ER!B+,J r(IedBLolnR:v'xtoߕc`dYf14S! |:_HeЏ+R_eMx5chX:fJe:aZiK֨B!őR!B+ D"Nm `蠔F&L4Wfʵi*tM ֛D)u.r3<}03A M04=$(h(sV#&zL Aa|9}PO(F$0M5+LS׿Ǯ-SgROO)v Z4H_|!r~L!B\4i#B!L&bxx'OHQ;7#qn EXu+AuiL;<ٙu|i#&<NC]luF"xX>dhL/׵: ,k60 ? *BwDJ!B!SN(D˲p:]$IF/?o|4>F*i =#i>s~5wLe*!w>֟mhBvjX 2qt%,[=LLNPZR,*BwLJ!B!.66uЀ&P.= *G ;{!FT*\'4ēo,ץsS"[GC#7hI7ciSf8w7 EKZljph?ʉ'in^CKK B! *B!XdJ)N8J"@L'm0Xf 7npmV@ɩijK=ݦ%1|aۆ1>krV7;q5qeMX̆MRi|ѽ. E>tL"/GO?h?:L&9x y|>p!B\cƗ%CKd4ML$ RQQ!'G!NNɓi\.VZw+lqavnJ/9.vNN͎%_ !Q@N,XT&t;tN4eEq uN&dBKXRc] 8\96&''Iӄap8 ÐB!B!RJ ȑ#bhiiv4 \NS3|Z!O>4^}6`rr4ɡիW#/!B% *XT !חq:;;! aYeQUUիY|[VPX4é1;S# k֒4METîe+!߸QޥiBtmB4f#;׻ˇI'=8n1sI>i,[f"B7%A@J!u IDAT044ıcǘ$ezV^MQQ.\[x\v7gx4RًzM3u Wr<ۄLMzBxJQ[j?]@mۃ~c, a`٨a֭8Ny!$bHP)B\ۦطoLRbnf/ڌ Byy>d?tr-P[[+/"!BcB\:T !מT*4tww:c)((`-1$ Ҧw=bu֛NN*~w!z.*|fϞ=LLLdؾ};NS!"KJ!B!ĵ#311AGGO0 t]P\\Lss57?ק<á/~/)5-nO>g~n78y$`͛7SSSB!$b1HP)B\B 200eeˣJ***ȱcH{/FMF8_]q*xGq8\\f]ɉ :;;4M àիW_ְW!KB, *BW,~fggIRI `͚5TUU$sjjҒb,e`Yۿ4]7@)p:uf JIsQlP__ϦM0 C^B!uJJ!B!'Aoo/h4InF*++q躾d7hllyPSZEWw]]8v;aE (GaٶmTW !)!Bq=K:tǏcf~֭[ill\U~gΜ! bn gA 7LӤq5UUU~orK(b~~zF6m$kW ! *B!u4M"OСCR) NCeÆ K#ukk+())yGɡ1z{{/KP`aݻwO2anVe:BqR!B\7,bjj:;;I&躎ᠨz133a|r ^/EEEߟmtshΙ3ghmmebbi}Q6lUB!qT !BkR>#pP[[KEEUUU=}rkmm0 \.x{aPXXn'2::Jyye)..رc?~P(Ç+W^>B!X:$B!״N84X ˲uUVHAAUb0dxxMͥlχi;vYo#GpIfffؿ?CCClذW !biR!B\z{{ioognnT*R Fmm----lrt:i_ghhvmTWWg( vZ֯_//r!#AB!qu]ש榛n_?[*b``4YnQ,b``690CۥR!HP)B!jJ):t1l6at:g# ocǎH$AeCƝwIuu5f~~I~qFq\fB!rT !BV0dhh4 0ˣ櫺,bddD"466^}vI$ JT+VPZZÇ'~iii$BqHP)B!: J)(//暨|),+W( 4`^ >۷S]]Mgg'O̙3LOOrJ͕7BqR!B\5fgg9raL4M-[FKK ]IJ)B(m_VäR)FGG\Pb***xWb;vejB!B!bɛ~ReaYlܸ25qQJQYY>K$azzX,^vLyy9/qAضm53_!HP)B!P(0 4MC4~?[ln$bddMhjjZu#ikk# 199Iee>?yyyݻ4Ge۶me; !b钠R!B,)d`0Hgg'9Nǚ5khhhu }MMM>|p8Ԓ*^Jv8d~իWn:4MdB!%AB!X###twwG:F4<˗/gŊeW`0HOOSSS[}|> bbbx<*WNNw}7?~idÆ x 'B,AT !B+*188H__q,"//Z***X|9a\СC躎륬Maabb UT.hjjǏA"`߾}BUUB%FJ!Bș3gb||9BPv*n@ @aa!Kz]>˲hooP(D",\.---ۀ24/_~]WW BRXXx՝@ M_dbbfffet&!j#AB!M{{;R)Lv, MPJi5u]֬Yd?˲8y$&²,VbݺunYK8z(ip8㡴1z{{^&>f]vAkk+DSN1007L]]B!m9B!KO2{2??f#''UVQ]]jxxH$@69}4O>$7x#z+粇J)4ꫤit]G4v;k֬aҙ?~K0$??֭[GEE$'NرcLMMpw܌$ǜL&peYB!Μ9Ckk+p]ױl|>*++q:2={Nill[n0<<O,477S\\,/B!rS Bq+߿yv/Y#_f; />;wsss8pӧO322G>R0@fYbeeeȠQfff,+v~?EEELLLG* KKKٱcON<$p lG!$B!,b۷>SYYhk5:nFjjjx'طoPsۙc݌cxi&&&bF?լ]|@CCCD"46mbǡ::vJKKioo7te.B\ϲ//9B\:JZ`2%3M~}?MaaeitvYv-=z1jkk/hȹ9^x&&&Ǫbvv`0L^UUwڵkl//ݍeY>|>N;BH$B!N>?NII ˲,S4t]]ȴi&&''y饗a۶m}?04MUVq_T*ũSPJKuu8RPJÊ+q4nZ: sss?uֽBqa$B!x {x>n}$xO\4iooGu\UVV4TU^JJJYb/2h>ijjMBk\A !B\FO?4ak׮~߲,ڎFyg?ӳlÇ`tt͖444((( 775/#;sILplٲ%WzjI& _wA%餮R^~eb]w%MBw@J!BDQlق|RtvvK/-g޽|`׮Nݹs'N$k(x^aOyy;>Onn.sssLOOɹ.sN:;;9|0X}Q6l@CCyB!xsT !B\&GA7mYD_]PӈbLOOSZZ@˗ٱcEEE2xh4,_| ߿p8uT.hjjpbXrƍ/uFBB!A__irAnhe)4 Xn (Ken6pḼs% ްjsQeY144(D/ Ι3gscnnb=VGqq1ЀAڕ]]]8rY+Wd2(UUU~lJ)Bt 4Mvgי|2B! *B!Y:f|| N<_HUMiA1տ2Mrp|&t;yϽc~bu|/+WdrrY d ɓ'I&(.8ȾgddH$"Akx^֮]Kyy9{eddt[x,6N8Ν;)--222ɓ' iL$Njk͖ -KJJcղB+FJ!BE6??$555qQЀT*˰ 7JbYӑ9Rf&"mhNJs1-yt]gy9 *155R Dze˖1sbsss@ik.9¡CHӜ>}amFmm-nw^v܉}Fٳcǎ 4 ˲RSSeYH~?1 l޼ZP!B!, 155/(TJוT Jvw@Ml;Kؿ|4g֮)8ҕYQUU2̙3b&6l*pPVV TWWẫ׻kR]]/Dg}ÑZ_]zmC)E0ٳ0a͚5bŊ 0===tvv288ȉ'(--~BF!B,T*E<Ϯ4T6Q*sClΨԹMt^c'LJ7n~H$'344D*^ptvJJJ-[H&s:uyL̾l6===\.np8̱cؽ{7T+VuQVVe |fmFEE BE%WB!,HFx3<Ù3gx{߻(B!t9B!kQ[?"HDēqv;M$AL[L_v===2oB)Eee%k׮%{ x^>OSQQqޟ6￟{n{1B BK{,@ػsfF33IH,Bfn:iĤ{joM%iYI؉7 F V$FhH,j@z3gOlܸTvDG|[ hnn7 66~U[۷s<8y|B!l9*B!n0łje|||m 0t|^/ѱ1}|#c™oG @t 6}P0XWϘf@00p]]'""BϘEcXHII!""c8QQQx< [!**bǃa\tz-bbbx'򽣳 0k3evZ9pZJ:U!ĬR!I~~eC?EQHHH=`(ÆkpӇHMKEU4 c:2F IDATEU#ULtt4YYYǒFWW0 u~~tUZ呗G0ECC###TUUq^/?0IIISl}=/ VSO]Ri6ٲe ݼ;Ǔ-)IP)BqEDDp8D+8 1AL,l%:]!.>nFU5M#vс$**J:q Ӄ8N?֏'''J T^ U)))Q[[ɓ'{<~gX ɓ$'%أ^r}TTs?x7ygB\7 *B!nC+555nTU0 """fm힞~ .[)i---O bPRR2/ۑ 200(ѳ ..b8@dd$w}e_}YœlVdL8z(dӦMqB!B!7$&&rq:::͝r;0ATUEu̼0 Ο??d+S4YbC:q LT٦ϋǕGEEtvvRPP =K4M6aۧ|]W?d2 022®]HOK#11m>9r֭yJB\ *B!n,N'GlP9UH0::JWW׬TT3X!JL&yߏa͛U\\LEE`^ *g8DDDf͚)4t:1/=\r%lٲE:O!5R!&HKK#663g0666 f3+AUG%..n^ O ~?Al6e8lK__^w?quxVjv}ƍgnu}^VvLiΝCQ+`ʪ)[+A, iu fy0 &t{dzzO*f/_Nmm-.D!DJ!B477x|9s2. .}!@ $''OVTL&%Uٳ͛gA0χlf|R^^瓠rŢEl6OYhљmG>(g=5.՛qv7vG+6:.u}ڡ˗/tttHP)IP)B1iDe˺ux饗طo;v$pxgzr&Z|c߾} r.9 FGGq\Naa!6m=FFzz:.n|><_ i!֖`58a ?=;6bދ$s׊qwCHs:DGGv'8B%AB!,u~31o܇GbXn-&:uiӦ}(rKO&&&;C:uAoo/,^xWf E!''KP9 FFFx<\l6r!NԢ( q=41DG9ywm4P>- YYY022锎Bq_B!z9p. Պ(JLL \l۶ Eyy9144{gg%`*E!;;{ކ7dbll^Ǽ eo4M %%˯҇Ʃxu&GRaLfuoA'PM*&s%@__PH:R!5R!~2Y FZZeeeSbO7|O~$%%o#<29O~N3 >"""HMMfP\* 222BDDdx90VZlBu@AUtcߊ`:ʇuEab*f0 ~1L8NFGG ґB!B!WVułixb2330%%GyW_}_xX^/;w[RZZ*<?(ő2+ V<ϟ' IPy 0CQUBx<:BAHHH0,ԔTaU~_:R!5R!bihh`hh`0(B!ؼy3ﯸ1~m~c{?q\^M6M[%fnhh˅9TU%>>p|>sbZ øgV0WKJj 'OrmtYx& ʧA0MTűni~_~ !fT !BLr|>߇+'BFDDC|m_V^MLL //7nd71544sNزe wut,CQIIIY966Á李Aʛ@UUl6j~LV VEC5ԣƦ71BT !BLA4ٳgAiժU}c^|EGGG{/Fb>ÇS^^>˥g臨UU]0]LL NZZZB⋙S@(R Tf3 odͮx>K0s ![B!G|>8tϟ',, UXpŪUf~333K/D}}=MMMvm,[Ʌza "##|GUꀉjؓhii!@qqUAUT -)EQHO T XBcj\>Ԯ0l\4n7B!>JJ!B,X5559ss*~Ԁv;dggyf:;;ٳ`Z1UEjF   Fqq1k֬!++h n0hkk# Xzltn7"UW)&&N{{;BnGgg'$ 66O΋j5߷(tuuxfTihh ##, Btx$ 'e,E,,fe(Ν;❙ɒ%Kd5tuu:˖-PpT^Bbcc)//g͚5G{90<<|ݫn+B!c!MJ!BPVΉs54v(9%+b2Gjl.Fpf>)L/`Qr d'ߒR]]Mkk+݄B!L&P4/_NrrJ+f={0p:$''/A###IJJ><d!Nqq1>od0G4EAuf1~?:@`*Q***p:[N:N!uR!X}V{?ҭEna3O,b1'× wx;_a:;+gO.3Zjkk\[4N'[n%66VV7(MMMȪ*((!%J[lرc?~+Vsz ?LL)?w(=Ь̍)baR!XBƷ<Ǘ^::o/XYӞZLf,&3وGHbiF!3]-~K~bk“]~8ԧ>uQE]tpxZPgΜ0 """HNNۇ ػw/>~222QBXX۷o_fϞ=<]@BB-;&ILLdʕaB!B!s?PTIvB:ΝKn*lUw?wYj&6+~?444`^({vOf2,]KJg UUIII!11QC墳"jJ˗/֒5gx<ݻQJKK B=7B19yɳ>%i!faM4 #B[nOΟq~lx|ސZWMǀM=m,N'1:nz=zjV+fz8p@UU1L$$$PPP}GRRt)N:bxG)B01e~kPTTĩShmm%55[z<`w}Zt B\ *A$BE?=*W0'?sX̖z1(,JTx$?z%ՕStg2 bǡChll\&.xXhҡl[;_|)g``l&''G[]QUe˖ILf L`POx穬i=::=(**&< x x 4M̷m:;;)--SEՒ6UV}vLA{>7| 4!3&CAdbM{YI\.֯_/ $)G9 Iw̹c|^Ͽܛq8dddl&22F||E q455o>ǹ{)((FF]]G!i&JJJQfI__444FFFYYYp8HHHjNYz^:;;'Zb/fѢEz={ӧOۋXV.]իӄBLI~B!C']}o|dH p]])"VXbj !n.jkk!//liȠ`0Hcc#%%%TTT066ڵk FFy睬\nN:9z(DEEaXMi>χOFFIIIPQQ8qgϢ*'NvnݺUh !XX$B!a^,”]ig'm |#Ӆ0?rE e EEEt:g|>Yj4uRUӉ$??ߏ8}4:aDGGt:#))\ z9N.N'JGGWJB\DU !PP/oVڟ:wo|*囿%<1' bPP(L=(aӮRuvvr<d{y^i$,,0Ͽǣ( +Wpp>3::ʕ+y U@!_4]gWAl,\8{|ͽ~^g6>BR*Y;Tvyt]$1[Č*:Ӝɤ&p sT`0H~~>[n 顧vTQ n =JOO>~e˖QZZJDD4B,@T !̙&BZ@I`< ΢ YnFY]s,H1gy)?* ƍgm*22e˖q%( SFF>|zf3555nlB\\4B,0J!gM þ_d1 IDAT/Ǥr G~r6-H}gtS%ͦMkʺ}I*pnʽK0DQxWinnBFJ!b9j#:сdl۶ !B1IP)B3]g,W^mMv64}b]UU'B ,&3A-IQ1M1L ^\]d8S^A\3880 Yr45ʢ#GLV 1yj6sS]]ɓ'|x^^{5֮]KQQ&IJ!4B1-6`yW1 u&fΩg&Z7Oolz|?w/GfA-nXt ͑Ȧ3 *kB(BLL 6msSii)tttHuҥKѣϓÇƍHB17,!b)Hekg- -HF\*ɱ ,J&&"J` WdeE%/11/^9/e;˜߇/G&0}Xʢ%BJ~۷o*Y`2Xb*VKp=j*~?kFWW4BCRQ)B̷w`(Ȑwd[+n}Xì{1&E!khF a0._:ΈdEv 1Ρ`vXLf򓳤Ŭ4Ο?Ogg'}}}ùv;$$$I^^200@WWSVVFdd4,q:dffrhnnXE\nzjٻw/1::oŋۤb>H!Kibyzf~#Ѹ8TP&TuڸQAa:::8<===z''--8nOٟb~QUx {=4jY! *By %Q:W CG5 *P+nsX-V-Z!(Y(juI^^٤FDD%vIkk+tuui錎bZ1+l366WUv;XVN' 뺎㡷JHIIaٲefbccyGؿ?---hF{{;/6l 33SI!>[B1,J!j۩J3]רl־vN?CLx$CpCDXqw6meaL rI1MWq ҉twws)MXXPʈB!B!wo}qQEΉcw|vҁz-*++q8|'%%eV+f3999rJ^}UvIww7{ffl](ɓrwf"##k~IF^^999QQQ}hooK汄6nHtt4ǎl6L__eeeJ# !\?}/Is1{ Ø\xcDJ!n *R.^;.9{??:/ot ?O}}=v?MjtR~?UUUtwwSRR"a|>=mmmY'|V`(--QYYI[[/rɅ&--B("[!c$Rq+ð[١װYx- sdlo^7#4+? yg͚5l۶ PPP(NvKX+FFFN{{;wy'sM&*l6hmmeѢE<( K,xBx<IMMn( `7|EQda,!c$Rq%&p|{m&96a&ٜ S;pI6nȶmۦ&iPhsz.O2::ɓ'1 < &B_|6n6o|K#330*++q\RY@dddG?^0AUUN'Fzz:hB1GȪBb8|'&dO9sl-=+;(LǟgabZ'N7 ;;;vLM `ףB8N~'"B?rSRRHu^z%*++yGYf͔y}>utcNC>aOy|oʕ+y'䅴@R^^NSS& ]IJJBu&""bwxxv09tp8$''.%UB!<&o9u';?}oΙc^?Rp{nWw ܉2TK*+f3=w8r,~:v\!8 UUU[O?My6lӧO&?/qdffw^f3n)GFFطo۷oz^`0 66h:::BfV+6 J~~>J !4 *ByG~gVd;[:{?sܻlt>}&~iܮ|{> /H~^ޔûپ};?9s ֭[v 66|UVUkY3}Ѓ^p >墲|䅵Nxf III瞻}AQ8r5nٽ{7躎l&==[MnnE?VNkk+gϞ4r?} WY+d2Q]SC~~>)))S^Ğ={";;[?Z`O<9eg;ݻwsN<iiiq<䓔DQTUTUUUZ$$$h"֯_Oaa!ɓ'ill$11HTUB,T q IP)+0**Ļ$D9YQܼoǗw~/ n+\s|0tgjMM N{!55m4MzN'1}+bAQIJJ"%%eAa,_իWO9| ^}i6.RYUEQQєC#""z԰xb엙R̿V*B `tt+VB!N>+"??x[|EQQeeetttP^^N0$..pH!Ă CB"3>xG?}˴t>E͎7t}s"{#;;{뇇f7`N233/*;;\}V\`B YtAO0rF \<ZZZHKM$ *ٲeT-ގl6_&N;Ɲw9~<ѣdeexPc?~CqܹB1IP)B, Ey?o|ƓC_ංvTkoڎ>R҂a͚5 222rbBi(OTk:e,JZjAeTT:to0dgg_6Lx>΢E.;l6 rx )5ill$""bʹ)g0D<#і8HzP#8g\eddAccSNInnĀկ2{FF²lr'Kܸ]˩S(Bxx8N[O۵ꫯ2<<̮]8w;v`7}d>ϑ믿NkktbޒJ!bKw&yN |xd=l((#1:r܊P3[TꉲE85g{׬O||B``^=} fEA͈g# F<# t^JBBuuu9)W۞ɟ|wiwwݶc?/0ΗOыScO [$99E)BNN999~CWWͿ3>>΃>x*ݛ!**{^xgy@!ļ#AB!رq>Qv> o>߿tcbYF1q 53)&ݭ4u72@Nb&~g'g&^QV\>TUQ:M1?W0Y-)&t tl6 կ %h`Y5We\Tuu9)))={=JM("!!w*6 ޺7Dj|c>(&o0g=܋dLLLa |01em8}4uuutvvذaׯǗ]wū/Kv؁,BEՄBaNrܷ|3nZhnFP/nɀK䮒HcybJጌkFA*dx`ᜇ|oc0Z~}l1m*5Aoo/hhCSo~bfB x'ϡt¬h h~XaBKveRMNMMv *E-[f\6l0gV/..ݻw~.0!ļ"AB!.H"͑101_U#GIʼWUD=0M'CJ ɄޭƇa䵄frm0444v(ʴA*,]#s^H/\oZOrL&b+n7Q%~}u1v;[l{_͛7SWWDZc((( 33S:L1oHP)B+ (DG)!n rӭpBԟ=/cdeetƱ{.>z{zq\]ǏrW ]GtT0cFVƕ 1B600(qW]70T4MGзʇ2d뵬nw8UB|ԹshnnxVD?|_|ӟNBT !ByA4BᴴA zz![WW;vMӮOC׉JbQIU{  B3_}z>ӆɚFSSÔtR>Lfo㣾{߿HBH7qq=iq\Ӵ{6͍ۦm4M-^b0Ć&@ ! Fi0-z>zK>>4wBm%=CЙgt̙SY'_Me~mnmڎ177W+VPCCN>=뷘,R(LhFa:r5%&qi?k7N8ֹs生?#ahʕjiiaU{T`ɑ /--LTlxTyY^pB)`xTZy/= $u]eMpl;֣Nm[ܹs+-VSyt"5jPTKs؁J9sN2.ð,k™===r]W|A쇆f͚o㺮F~"NUUUD3A%s*ٳ^X,6PS=!01\w鿖eMT?^x\ϟ=*((jooR*FFe2Y)ťe˖Ɋ +,EHTsUT\tor<{MWWQvv6ht1TEEE·iin_$'qU/^z*)B,XFZJPF*0my^)k``ߢѨ8edd(--N6 Cc˄%+77W%%%\X" IDATya%E̙#uuܹqy} CL>7ѻeY|^s99sIАFGG###zVHM|Mu]n+D3A%ŋ׹st+^ge333LTww>4N[?ahΜ9RwwCh4:a(++klk5+//Kѻh" IbӜ9sFHmmm>Qڽ[ZpZP(~JA%kjΝjll7FFF! UٳgǓj+V_Wcc㸳,˒mjjrrr)L6 CeʧNR__l7$]\r 'c8iϞ=D" WY= {ZO[N: [kLӔߖe]vW?uuKiŊ:y򤚛'*++M'wvvj׮]Zz˲ёp0קG(%%EMMM Bjii E{N,t*0#TTThժUڿJKK֙o?YF ,HAmܸQO֎;_dgg£cr]wFo* jʕ GHzSsj+iz۶ݭBf:`F,K7oVaa^}U577Oq={ViӦ>0c67o֭['NhS>_ ߯Z*=4<<=:`P>Oxdd>l3|Oʽ{)q * %1%L{Ww}vm۶M%%%S6&Kbz1 C6mҹs*''GS>;w6orZ|4y^_뺊bUWWuQ*TϟW(:ء\Ws0 y<TXXPfJ(zG}O۷o׃>9sq477kO;vL|FGGz$H$kzDqFs=ͥ PUU{n˲,eff㙼FGGuQ;ڻwVXm۶& aiiijjjR]]***>m /u]}JKKyf^q2jttTMMMZ~=:9{;ʲ,B%%%***>bZ[[u={VMMMr]WVҒ%KE1E"'?z{{UYYjUUU)%%cАN8:*''Gwq*++ )ObRb1_RRR|fZsϩFw֮]K*Jh``@  B_uIRgg4000+77#O%|a( jݺuZnZ[[U__ӧOSgΜѩSe2\}}}jooW,S,4^W=A^)u -\DP YSr]Wc@SHEEEڼy!s__Teee]vIOO4M׫pRAr+++wܡ;w*%%Ez|>ߔ^;vPZZ}Q`V8fAE"IҒ%K( i`PQ}}6mD0i,'?I j׮]eY KCC~4M}'`D"arGe mۊjhh(T`PwV\={襗^ Á+k۶m***1f-fT`RKKؒaaȐ8:|/^La0~z!9S̙s{:v|iu\b8*++9g+,,TYY$W###β,=#z֦~zeuyf}ԡC$Q Yq0 ٶKRY*33Sd٣;Xv +_Wmm.^X,!:r<6lؠHUbN8!˲ (H3gHAp]-X@O=vޭZ}Pii/_ (%%%-'ޮSNСC ֦M؏@!bdϟ/OQf"eff*u]kѢE׍ӝwީ+V:s~mٶy橠@ȐeYx$<A;w0T`hoo$B!S$|rٶa RfJ CCCFTQ$TUU%ue5<JJJ$IMMM :::亮,Raa!I78իW˶mF9Ca )fh4IՒ%K(JKOOWjj,RMMlۦ(N>=TA 8CQ*0uuu)u]͟?$9ǣs4My^ݻ0T`F4ku]9+WR(++Kv` ֦N@ KQ u]?0T`FT88*++ SPP`0(4UWWGA*0aqٶi߸… )i3N:%˲xCQpL!qtQ 4FP +W모HR\fժU,K못[x0MT`Fr]WZzL\ZZBM7$0dZZZEQ*0#EQEQ9+P\QYYQOO)A%fY%0'˲( h͚5r]Wbi3R]]LTZZ Ҕ-4U__p8LQf*0qH GQ0.0zjqEQRJ8ǎi^|) X ]"hϞ=m0T`ƩaΦ HH(RII\UOO8E`!344$0 P$$Bh߾}i3ʩS8S (HXZZ233A%fsq\ RPPxOahhhHϟ(L1J===cA%˾1.\(q4<<6 #FD8}cR[NH.\0vPbZ[[eQUUE2MeeeI:;;f)L 0 Qi*##CaPLnMx\hTr]0E*0b?jppPmkѢE2M^brɲ,*}XJ&ɓm2MSj4ijҥ:xz{{DРH$^{ N(ҩS&ǣ@ ǣx,˲{nuttq͛7pT`Z,KŗHDa꥗^iJOOOpM^x ɲ,I1M}PQFFF$&m__~i?_ (mۖ뺄 l0 m۶Bc,Kԧ}auvvR,nJ(XLׯ@\ZJx#n)xu#\7tXj*++Fkq BP u]|>Zb`R[N%%%mGQ R nJx\k׮U^^JII \?\UqqϟO1p]=)B^N1pݬ_^co۶FGG) 7A%_VXXJ;eY.n7D( 7A%H$264Mmذd>Dp0<Mss$+33)++S P<ׅ  ʗHlTǛOjn`dX&2TEEe(y%DP KMMMRSSKRRRTZZ .ͨp|bG#uDiEQv%2-y-~6TLAHA% ue۶ta?~\2MSa4/BdX㺮8 ue<~97B܎+6S-r$yMK 뵰h.;3ԧNZ?u=v))m|\oH$R`ޘ9#۶e۶ѨFGGԤS =888zݻW---TjjJJJTZZP(ٶqb1555M.\Ȉkɒ% uom3;~oiNvʴ{uU镃;^շՒ9 m[UvJ? 4J@2D"޽{ޮ ͟?_eeeZ`U_gg?&544(==]'l2Ĥwv?xCz܆aѪkA}wwwmmΛ6Rx~A%pTQSSvܩ:iڵ*--UJJ5w4UCC8ZjÆ 馛(<ىO>_mx@uⵓ8~'Wب`[w=KKHVu@P H6;Cx<[JMMljD":s挶o.ul2mݺc{f.}cPz0>f݅39_U7vO?5Lu>.;w.*ovܩP(}s.xUVٳ:~z{{UZZ*Yi:o=2'Do&w77-Kw.ݨn}u>{*넠 ~mٳGW^W+VP[[>~$RH׵:,O?+nWOO?65@R#Jlj*))ѓO>yfQ^aZt\/45|YEs03xZqZ1zJaZIg;}kC4XvQ*++#Y5=__sSe;6$J$jkkSUU8l٢^=za߽/LKK!_pڎ\mx@ϼjiqBP ޽[)))[8h"׫A\=v֕O뱦SumjnՇgW% T`B---x<^Z---,$iW>wΥxxϯ֪қovHJʶm]vF777W:*++zib{Ȼ zk٦o3Ȗ+udLlQ?E1\yZ\\~"0h iT`BgΜUYYY·Tܶ庮I[PJLs'--MWCCn$α=zPqs·oRk~|uT??kyqeBz~}ݺx-M$JOpX .L6x\=>k޼y^TTT($64:^\Pm^??rrf4o?zY}?T/4U*#%]TA%ޮ$|;w]L͓uuzW'PZZڄקɲ, (;;F&Sgwl-*,M/ǑeZ6T_H?n.ooxtJIS]k#M$ JkppPXL ]_SS_ziҗ[}*3G[_Kӕf-XF&Sm۶M#wѾcLK1ǞԱ|,=S^ϯƎHW,8^ԤGL Ð8 ]\7oV\9`P@@}}}O&fK3x\/+/Zqvsq]PWO)W.5"˴,Hm۲m[EEE^Fק 6hޯ8rGiJx\UwO>uqMs{h<ܥHlT>OX<ϯH~~OLӔ^6m:y=+^)k`dA%ӣh4:8pXMdn'˲4<<|ճQ%aDiy+^ձzsl*/Q#Wiÿ-_h_CeU2M-[55q'h<4W$J@@@x|}'mVggZZZi&UWW_ViiJKKsNm޼YaW_c=>}zܥٗ 5::*۶8bOwL4 / -W3zy|*-{xNu] W>wܠu]gen>JlɎ/Wwi" iqGa^{)lԑ#GTPPX,N䨽])))x6羪7nJvle% A%dYU`tGiiKsܗҲ,mڴI*((Hv###D" $VY\.s'u/Q,1r4 Sr{}ƢZ[A% ӧO{… UPP 0Р J:;;U^^NIŋ.ϧK^Q ׇڪH^Wܡ{Š2߿!RZtB6V㥁AP ٳ:~,Yפ{RKK.]*O}+neYzf~S#}L'ZTҠ49V< -YD^Wcu]WQyy9~CQ;k6tee$~`B999*..VKKʶm߿_ . ҭUP^?gcGKz|6e* T !wuFFF{Mq:uJ---Rzz:$zN>^=i9Fu~$۱5(qBP ̙3GUUU:sZZZmo\-^ae{~Mã֎`${bn|wV.]T-olUO_vqi5_{U?Wi,N$J$,''G6mui5fܹSyyyi~ӓE>ֱi3]--ֺ+h )T  6HoŸzzz+h۶m4 W4?wWd~;C'Zt<1;?'l2M$kMJ-[(_Tkk딎'^SWWnݪqݾdcMҷgfvp̎/_k_}4*p.\m۶W?,FzU[[-[hҥ4 ytoO9wBXOc^G _֛o_N$7J|,K,<>=7<w?~\[lц dA¶|^oȹzm7q"a}}O[?V^:?`]O?5Lu>lۖmܹs)`(,,TQQ:G*55U2 .[n:y睲,કӭڡ=gO+Vcy&=v]Wнk:TϬrҲh~F"r"q2jttTMMMZ~=:.\s=N^ZׯW^^ޤ=CCCڿ~mkƍZf 5ki魣?UV(CխT#Ӹ}(2Sg??zNs*Ϳ|_@P \dc8p@`P֭ӢETTTt٩߿_===ԦMXI9GWWܣu W`d$|_1;s]-pD?=_?ُ{ooRpJ: $+qti}Z---*--ƍUYY)Gq]u'?Կ}E{_iTU,TUqͫRaFR)S@Ht㼆G#֩oms:xLeys|Pw/E(_FJ: $H$N}:z222 5|eee)%%E~_ȈbZZZק^ )릛nҚ5kT\\ gOM=my|~r}:_ˣ?(U8 ˶"9ڼx_yn[\y-E`u@P E(^2 c0t0T\\e˖iҥ `ڲGdkuiϩ*Ӽ9y*/O.Z )49TA%Wӣ.uuu4M-z*++Svv6H2= l-Zb` gL9JS#0*L9JS#0*L9JS#0*L9JS#0*L9JS#0*L9JS#0*L9JS#0*L9JS h`zDIENDB`././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/doc/source/user/figures/topology_wordpress.png0000664000175000017500000025325500000000000024317 0ustar00zuulzuul00000000000000PNG  IHDR}p=MsBITOtEXtSoftwareShutterc IDATxy\?dLЄPUmQ)*V[S~`+ JݱEm+^ iEEA%T Ad ֖Uz&Ϝ3'D]] B!zi8X!B! !B!0w!B!z oZeeZ ܬonkV3jM]nЩTLկzFU*$;B!BA{5wX:a􍴖n7=M(co`YV1+ˮZeLs_3!B!BH{/6j ,K  @ +r!. .Y*Ǫ.B!o]:f( Ky\.rxAx|\.p9 8C!Zs?xp:eښjx/!z|j!B!ruhKbbߓgK5c'CZN#8j5˲D+/TZ/B!Bw]^inDV`#t: B a"Xh ^,˲mg\Xmn03wF( sb}A!^TΈRG4w`/ u, A\atz=gxb~u:]Ϟ=gΜIQoܸW$=zx1cpZz)ʴ{q;;;?>k֬҂ ^~۰EG4RT@H[>}OqaCg2mjb:鳻A۽U9԰{Vq,-{|c;kGbsth2=>b,.B G-_ur\ / l$A ݺ=9}=#Gs,_4?7n~]@cY^3٫h߾}E_~eSSSW^pttdYW\1LohhHMM۷/˲vjhhGGG8rH~~^˻zailPÙ0:gkk#n@; CQ szO_~t#F{r,,23]m 5 ]%wi>PaSrxIQs\V'{LaԤ̟P0Uo87lINIY57аˣ|"zy-Q#Q)n-\G?3Mswߩ%i+|jxp8=䆆6@Nkʽe*ub w[SӃ`Jcoɧ)Ө^7;Si:eh/R)&%!!=V/3F(gyer:6_”J̦l6\IGPdܔ|h鉢.WTb21KN0%{xy![w2yW] \; bF\CTGV('H ]+_ Yz675i 7MC˖Cݚ'HOHD8#UrEq~q')Wo?4O`0[Z-+d|DJKd[2}mɤ.[r هzE3d#{f\.6n雛9O BW7Jϲ,Ҳ `ccc{(@`h̠W^W*`ii?,Ih4͆ V q,,dק$u '>uQ\:EH37K8^ݫO|o96 K[g^E%G_OM2-:/1>['GLn\6!sTj0y*qKEPR`\4Ѕn N'cl'G%z e~$i۷R(ݲ8$%kL*/>jN_t~mɣ[ȃ%Lqf>EnIL))\8>;.ti־Fn 4@T,_'GRReF~qj<3*&[m{ymR%867e_ES_^b`ׄ ƋLWj 13]jwIYNҶ5x!„l÷K)S2$S']|q@h o Pz 4eb^HBKRޮYJ8 5ُ =YL,ݴ<8XB瓣\I\MR$HH6eij WoiǷwxo` Sq{E*f<=mI:3!AHE_D.BQ ga,Y"i(E%,N>7B'|@w r@kH8O{6|ݑnAAKSdK@M{{Q@%#) ^~NZ@Ե3kKe
    ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/meta/io.murano/Resources/Agent-v2.template0000664000175000017500000000173200000000000023054 0ustar00zuulzuul00000000000000[DEFAULT] debug=True verbose=True log_file = /var/log/murano-agent.log storage=/var/murano/plans engine_key = %SIGNING_KEY% [rabbitmq] # Input queue name input_queue = %RABBITMQ_INPUT_QUEUE% # Output routing key (usually queue name) result_routing_key = %RESULT_QUEUE% # Connection parameters to RabbitMQ service # Hostname or IP address where RabbitMQ is located. host = %RABBITMQ_HOST% # RabbitMQ port (5672 is a default) port = %RABBITMQ_PORT% # Use SSL for RabbitMQ connections (True or False) ssl = %RABBITMQ_SSL% # Do not verify SSL certificates insecure = %RABBITMQ_INSECURE% # Path to SSL CA certificate or empty to allow self signed server certificate ca_certs = '/etc/murano/certs/ca_certs' # RabbitMQ credentials. Fresh RabbitMQ installation has "guest" account with "guest" password. login = %RABBITMQ_USER% password = %RABBITMQ_PASSWORD% # RabbitMQ virtual host (vhost). Fresh RabbitMQ installation has "/" vhost preconfigured. virtual_host = %RABBITMQ_VHOST% ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/meta/io.murano/Resources/PutFile.template0000664000175000017500000000200100000000000023027 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. FormatVersion: 2.0.0 Version: 1.0.0 Name: $planName Parameters: path: $path Body: | return putFile("'{0}'".format(args.path)).exitCode == 0 Files: destinationFile: BodyType: Base64 Name: destinationFile Body: $fileContent Scripts: putFile: Type: Application Version: 1.0.0 EntryPoint: putFile.sh Files: - destinationFile Options: captureStdout: false captureStderr: true verifyExitcode: $verifyExitcode././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/meta/io.murano/Resources/RunCommand.template0000664000175000017500000000165000000000000023533 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. FormatVersion: 2.0.0 Version: 1.0.0 Name: $planName Body: return runCommand() Files: scriptFile: BodyType: Text Name: scriptFile.sh Body: $command Scripts: runCommand: Type: Application Version: 1.0.0 EntryPoint: scriptFile.sh Options: captureStdout: $captureStdout captureStderr: $captureStderr verifyExitcode: $verifyExitcode ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/meta/io.murano/Resources/conflang.conf0000664000175000017500000000137100000000000022371 0ustar00zuulzuul00000000000000yum_repos: chef: baseurl: http://repositories.testbed.fi-ware.eu/repo/rpm/x86_64/ enabled: true failovermethod: priority gpgcheck: false name: Chef repo puppetlabs-products: name: Puppet Labs Products El 6 - $basearch baseurl: http://yum.puppetlabs.com/el/$releasever/products/$basearch gpgkey: file:///etc/pki/rpm-gpg/RPM-GPG-KEY-puppetlabs enabled: true gpgcheck: false puppetlabs-deps: name: Puppet Labs Dependencies El 6 - $basearch baseurl: http://yum.puppetlabs.com/el/$releasever/dependencies/$basearch gpgkey: file:///etc/pki/rpm-gpg/RPM-GPG-KEY-puppetlabs enabled: true gpgcheck: false packages: - chef - puppet ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/meta/io.murano/Resources/linux-init.sh0000664000175000017500000000177400000000000022376 0ustar00zuulzuul00000000000000#!/bin/sh # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. service murano-agent stop AgentConfigBase64='%AGENT_CONFIG_BASE64%' RMQCaCertBase64='%CA_ROOT_CERT_BASE64%' if [ ! -d /etc/murano ]; then mkdir /etc/murano fi echo $AgentConfigBase64 | base64 -d > /etc/murano/agent.conf chmod 664 /etc/murano/agent.conf if [ ! -d /etc/murano/certs ]; then mkdir /etc/murano/certs fi echo $RMQCaCertBase64 | base64 -d > /etc/murano/certs/ca_certs chmod 664 /etc/murano/certs/ca_certs service murano-agent start ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/meta/io.murano/Resources/murano-agent0000664000175000017500000000252500000000000022255 0ustar00zuulzuul00000000000000#!/bin/sh ### BEGIN INIT INFO # Provides: murano-agent # Required-Start: $local_fs $network $named $time $syslog # Required-Stop: $local_fs $network $named $time $syslog # Default-Start: 2 3 4 5 # Default-Stop: 0 1 6 # Description: murano-agent service ### END INIT INFO # NOTE(kzaitsev): not using fullpath to allow different locations # of muranoagent binary on different OS images. muranoagent just needs # to be in PATH. Up to image to decide where exactly. SCRIPT="muranoagent --config-dir /etc/murano" RUNAS=root PIDFILE=/var/run/murano.pid LOGFILE=/var/log/murano.log start() { if [ -f "$PIDFILE" ] && kill -0 "$(cat "$PIDFILE")"; then echo 'Service already running' >&2 return 1 fi echo 'Starting service' >&2 PATH="/usr/local/bin:/usr/local/sbin:/usr/bin:/usr/sbin:/bin:/sbin:$PATH" LOCAL_CMD="$SCRIPT &> \"$LOGFILE\" & echo \$!" su -c "$LOCAL_CMD" $RUNAS > "$PIDFILE" echo 'Service started' >&2 } stop() { if [ ! -f "$PIDFILE" ] || ! kill -0 "$(cat "$PIDFILE")"; then echo 'Service not running' >&2 return 1 fi echo 'Stopping service' >&2 kill -15 "$(cat "$PIDFILE")" && rm -f "$PIDFILE" echo 'Service stopped' >&2 } case "$1" in start) start ;; stop) stop ;; restart) stop start ;; *) echo "Usage: $0 {start|stop|restart|uninstall}" esac ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/meta/io.murano/Resources/murano-agent.conf0000664000175000017500000000063600000000000023202 0ustar00zuulzuul00000000000000start on runlevel [2345] stop on runlevel [016] respawn # the default post-start of 1 second sleep delays respawning enough to # not hit the default of 10 times in 5 seconds. Make it 2 times in 5s. respawn limit 2 5 # We're logging to syslog console none exec start-stop-daemon --start -c root --exec /usr/local/bin/muranoagent -- --config-dir /etc/murano 2>&1 | logger -t murano-agent post-start exec sleep 1 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/meta/io.murano/Resources/murano-agent.service0000664000175000017500000000027000000000000023707 0ustar00zuulzuul00000000000000[Unit] Description=OpenStack Murano Agent [Service] Type=simple ExecStart=/usr/local/bin/muranoagent --config-dir /etc/murano Restart=on-failure [Install] WantedBy=multi-user.target ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/meta/io.murano/Resources/murano-init.conf0000664000175000017500000000075000000000000023044 0ustar00zuulzuul00000000000000 yum_repos: epel-testing: baseurl: http://download.fedoraproject.org/pub/epel/$releasever/$basearch enabled: true failovermethod: priority gpgcheck: false name: Extra Packages for Enterprise Linux - Testing packages: - subversion - git-core - wget - make - gcc - python-pip - python-setuptools - python-virtualenv hostname: $instanceHostname ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/meta/io.murano/Resources/murano-init.sh0000664000175000017500000000233000000000000022525 0ustar00zuulzuul00000000000000#!/bin/sh # NOTE(kzaitsev): old dib elements installed murano-agent into a venv # so if the image is an old one: symlink agent into /usr/local/bin if [ -d /opt/stack/venvs/murano-agent ] && [ ! -f /usr/local/bin/muranoagent ]; then ln -s /opt/stack/venvs/murano-agent/bin/muranoagent /usr/local/bin/muranoagent fi # NOTE(kzaitsev): for example on debian by default PATH would be /sbin:/usr/sbin:/bin:/usr/bin # when this script is run. Our default DIB elements install it in /usr/local/bin. # Expand path to some of those locations. PATH="/usr/local/bin:/usr/local/sbin:/usr/bin:/usr/sbin:/bin:/sbin:$PATH" which muranoagent > /dev/null if [ $? -eq 0 ]; then echo "muranoagent binary is already installed" else # TODO(kzaitsev): use deb/rpm packages as soon as we can echo "installing murano agent from pip" echo "binary not found in PATH: $PATH" pip install '%PIP_SOURCE%' fi muranoAgentConf='%MURANO_AGENT_CONF%' echo $muranoAgentConf | base64 -d > /etc/init/murano-agent.conf muranoAgentService='%MURANO_AGENT_SERVICE%' echo $muranoAgentService | base64 -d > /etc/systemd/system/murano-agent.service muranoAgent='%MURANO_AGENT%' echo $muranoAgent | base64 -d > /etc/init.d/murano-agent chmod +x /etc/init.d/murano-agent ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1696417899.7691808 murano-16.0.0/meta/io.murano/Resources/scripts/0000775000175000017500000000000000000000000021420 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/meta/io.murano/Resources/scripts/putFile.sh0000664000175000017500000000116700000000000023371 0ustar00zuulzuul00000000000000#!/bin/bash # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. TARGET_PATH=$1 mv "$(readlink destinationFile)" "$TARGET_PATH" ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/meta/io.murano/Resources/windows-init.ps10000664000175000017500000000562600000000000023022 0ustar00zuulzuul00000000000000#ps1 # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. $WindowsAgentConfigBase64 = '%AGENT_CONFIG_BASE64%' $WindowsAgentConfigFile = "C:\Murano\Agent\WindowsAgent.exe.config" $WindowsAgentLogFile = "C:\Murano\Agent\log.txt" $CurrentComputerName = HOSTNAME $NewComputerName = '%INTERNAL_HOSTNAME%' $MuranoFileShare = '\\%MURANO_SERVER_ADDRESS%\share' $CaRootCertBase64 = "%CA_ROOT_CERT_BASE64%" $CaRootCertFile = "C:\Murano\ca.cert" $RestartRequired = $false Import-Module CoreFunctions Initialize-Logger 'CloudBase-Init' 'C:\Murano\PowerShell.log' $ErrorActionPreference = 'Stop' trap { Write-LogError '' Write-LogError $_ -EntireObject Write-LogError '' exit 1 } Write-Log "Importing CA certificate ..." if ($CaRootCertBase64 -eq '') { Write-Log "Importing CA certificate ... skipped" } else { ConvertFrom-Base64String -Base64String $CaRootCertBase64 -Path $CaRootCertFile $cert = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2 $CaRootCertFile $store = New-Object System.Security.Cryptography.X509Certificates.X509Store("AuthRoot","LocalMachine") $store.Open("MaxAllowed") $store.Add($cert) $store.Close() Write-Log "Importing CA certificate ... done" } Write-Log "Updating Murano Windows Agent." Stop-Service "Murano Agent" Backup-File $WindowsAgentConfigFile Remove-Item $WindowsAgentConfigFile -Force -ErrorAction 'SilentlyContinue' Remove-Item $WindowsAgentLogFile -Force -ErrorAction 'SilentlyContinue' ConvertFrom-Base64String -Base64String $WindowsAgentConfigBase64 -Path $WindowsAgentConfigFile Exec sc.exe 'config','"Murano Agent"','start=','delayed-auto' Write-Log "Service has been updated." Write-Log "Adding environment variable 'MuranoFileShare' = '$MuranoFileShare' ..." [Environment]::SetEnvironmentVariable('MuranoFileShare', $MuranoFileShare, [EnvironmentVariableTarget]::Machine) Write-Log "Environment variable added." # (jose-phillips) If the Master Image or CloudBase-Init is already set the hostname this procedure fail. if ($CurrentComputerName -ne $NewComputerName){ Write-Log "Renaming computer to '$NewComputerName' ..." $null = Rename-Computer -NewName $NewComputerName -Force Write-Log "New name assigned, restart required." $RestartRequired = $true } Write-Log 'All done!' if ( $RestartRequired ) { Write-Log "Restarting computer ..." Restart-Computer -Force } else { Start-Service 'Murano Agent' } ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/meta/io.murano/manifest.yaml0000664000175000017500000001027300000000000020454 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. Format: 1.4 Type: Library FullName: io.murano Name: Core library Description: | Core MuranoPL library Author: 'murano.io' Tags: [MuranoPL] Classes: io.murano.Object: Object.yaml io.murano.Environment: Environment.yaml io.murano.CloudRegion: CloudRegion.yaml io.murano.CloudResource: CloudResource.yaml io.murano.Application: Application.yaml io.murano.Exception: Exception.yaml io.murano.StackTrace: StackTrace.yaml io.murano.SharedIp: SharedIp.yaml io.murano.SharedIpRange: SharedIpRange.yaml io.murano.File: File.yaml io.murano.User: User.yaml io.murano.Project: Project.yaml io.murano.configuration.Linux: configuration/Linux.yaml io.murano.resources.Network: resources/Network.yaml io.murano.resources.Instance: resources/Instance.yaml io.murano.resources.LinuxInstance: resources/LinuxInstance.yaml io.murano.resources.LinuxMuranoInstance: resources/LinuxMuranoInstance.yaml io.murano.resources.ConfLangInstance: resources/ConfLangInstance.yaml io.murano.resources.HeatSWConfigInstance: resources/HeatSWConfigInstance.yaml io.murano.resources.HeatSWConfigLinuxInstance: resources/HeatSWConfigLinuxInstance.yaml io.murano.resources.LinuxUDInstance: resources/LinuxUDInstance.yaml io.murano.resources.WindowsInstance: resources/WindowsInstance.yaml io.murano.resources.NeutronNetworkBase: resources/NeutronNetworkBase.yaml io.murano.resources.NeutronNetwork: resources/NeutronNetwork.yaml io.murano.resources.ExistingNeutronNetwork: resources/ExistingNeutronNetwork.yaml io.murano.resources.NovaNetwork: resources/NovaNetwork.yaml io.murano.resources.Volume: resources/Volume.yaml io.murano.resources.CinderVolume: resources/CinderVolume.yaml io.murano.resources.ExistingCinderVolume: resources/ExistingCinderVolume.yaml io.murano.resources.CinderVolumeBackup: resources/CinderVolumeBackup.yaml io.murano.resources.CinderVolumeSnapshot: resources/CinderVolumeSnapshot.yaml io.murano.resources.MetadataAware: resources/MetadataAware.yaml io.murano.resources.InstanceAffinityGroup: resources/InstanceAffinityGroup.yaml io.murano.system.Agent: system/Agent.yaml io.murano.system.AgentListener: system/AgentListener.yaml io.murano.system.HeatStack: system/HeatStack.yaml io.murano.system.Resources: system/Resources.yaml io.murano.system.InstanceNotifier: system/InstanceNotifier.yaml io.murano.system.Logger: system/Logger.yaml io.murano.system.StatusReporter: system/StatusReporter.yaml io.murano.system.NetworkExplorer: system/NetworkExplorer.yaml io.murano.system.SecurityGroupManager: system/SecurityGroupManager.yaml io.murano.system.NeutronSecurityGroupManager: system/NeutronSecurityGroupManager.yaml io.murano.system.AwsSecurityGroupManager: system/AwsSecurityGroupManager.yaml io.murano.system.DummySecurityGroupManager: system/DummySecurityGroupManager.yaml io.murano.system.MistralClient: system/MistralClient.yaml io.murano.system.MetadefBrowser: system/MetadefBrowser.yaml io.murano.metadata.Description: metadata/Description.yaml io.murano.metadata.HelpText: metadata/HelpText.yaml io.murano.metadata.ModelBuilder: metadata/ModelBuilder.yaml io.murano.metadata.Title: metadata/Title.yaml io.murano.metadata.forms.Hidden: metadata/forms/Hidden.yaml io.murano.metadata.forms.Position: metadata/forms/Position.yaml io.murano.metadata.forms.Section: metadata/forms/Section.yaml io.murano.metadata.engine.Serialize: metadata/engine/Serialize.yaml io.murano.metadata.engine.Synchronize: metadata/engine/Synchronize.yaml io.murano.test.TestFixture: test/TestFixture.yaml io.murano.test.TestFixtureWithEnvironment: test/TestFixture.yaml io.murano.test.DummyNetwork: test/TestFixture.yaml ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1696417899.7571807 murano-16.0.0/meta/io.murano.applications/0000775000175000017500000000000000000000000020444 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1696417899.7571807 murano-16.0.0/meta/io.murano.applications/Classes/0000775000175000017500000000000000000000000022041 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/meta/io.murano.applications/Classes/baseapps.yaml0000664000175000017500000000410300000000000024521 0ustar00zuulzuul00000000000000Namespaces: =: io.murano.applications res: io.murano.resources std: io.murano --- # ------------------------------------------------------------------ # --- # A base class for applications running a single software component on a single # server only Name: SingleServerApplication Extends: - std:Application - SoftwareComponent Properties: server: Contract: $.class(res:Instance).notNull() # this is "private output" property. It is not part of the class interface # and should not be used from outside or by inheritors. It may be removed in # future in favor of attributes _serverGroup: Usage: Out Contract: $.class(SingleServerGroup) Methods: .init: Body: - If: not $this._serverGroup Then: - $this._serverGroup: new(SingleServerGroup, $this, server => $this.server) Else: - If: $this.server != $this._serverGroup.server Then: - $this._serverGroup.setServer($this.server) deploy: Body: - $this.deployAt($this._serverGroup) --- # ------------------------------------------------------------------ # --- # A base class for applications running a single software component on multiple # servers Name: MultiServerApplication Extends: - std:Application - SoftwareComponent Properties: servers: Contract: $.class(ServerGroup).notNull() Methods: deploy: Body: - $this.deployAt($this.servers) --- # ------------------------------------------------------------------ # --- # A base class for applications running a single software component on multiple # servers which should support scale-out and scale-in scenarios Name: MultiServerApplicationWithScaling Extends: MultiServerApplication Properties: servers: Contract: $.class(ServerReplicationGroup).notNull() scaleFactor: Contract: $.int().check($ > 0) Default: 1 Methods: scaleOut: Scope: Public Body: - $this.servers.scale($this.scaleFactor) - $this.deploy() scaleIn: Scope: Public Body: - $this.servers.scale(-1 * $this.scaleFactor) - $this.deploy() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/meta/io.murano.applications/Classes/component.yaml0000664000175000017500000003143500000000000024735 0ustar00zuulzuul00000000000000Namespaces: =: io.murano.applications std: io.murano res: io.murano.resources m: io.murano.metadata.engine --- # ------------------------------------------------------------------ # --- Name: Installable Properties: allowedInstallFailures: Contract: $.string().notNull().check($ in ['none', 'one', 'two', 'three', 'any', 'quorum']) Default: 'none' beforeInstallEvent: Contract: $.class(Event).notNull() Usage: Runtime Default: name: beforeInstall installServerEvent: Contract: $.class(Event).notNull() Usage: Runtime Default: name: installServer completeInstallationEvent: Contract: $.class(Event).notNull() Usage: Runtime Default: name: completeInstallation Methods: install: Arguments: - serverGroup: Contract: $.class(ServerGroup).notNull() Body: - $serversToInstall: $serverGroup.getServers().pselect( switch($this.checkServerIsInstalled($) => null, true => $)).where($ != null) - If: any($serversToInstall) Then: - $.beforeInstall($serversToInstall, $serverGroup) - $failures: $serversToInstall.pselect($this.installServer($, $serverGroup)).where($ != null) - $.completeInstallation($serversToInstall, $serverGroup, $failures) checkServerIsInstalled: Meta: - m:Synchronize: onArgs: server Arguments: - server: Contract: $.class(res:Instance) Body: - Return: $server.getAttr(installed, false) beforeInstall: Arguments: - servers: Contract: - $.class(res:Instance).notNull() - serverGroup: Contract: $.class(ServerGroup).notNull() Body: - $this.report(format('Installing {0}', name($this))) - $this.beforeInstallEvent.notify($this, $servers, $serverGroup) - $this.onBeforeInstall($servers, $serverGroup) onBeforeInstall: Arguments: - servers: Contract: - $.class(res:Instance).notNull() - serverGroup: Contract: $.class(ServerGroup).notNull() installServer: Meta: - m:Synchronize: onArgs: server Arguments: - server: Contract: $.class(res:Instance).notNull() - serverGroup: Contract: $.class(ServerGroup).notNull() Body: Try: - $this.report(format('Began installing {0} on {1}', name($this), $server.name)) - $this.installServerEvent.notify($this, $server, $serverGroup) - $this.onInstallServer($server, $serverGroup) Catch: - As: e Do: - $this.report(format('Unable to install {0} on {1} due to {2}', name($this), $server.name, $e.message)) - Return: $server Else: - $this.report(format('{0} is installed on {1}', name($this), $server.name)) - $server.setAttr(installed, true) - Return: null onInstallServer: Meta: - m:Synchronize: onArgs: server Arguments: - server: Contract: $.class(res:Instance).notNull() - serverGroup: Contract: $.class(ServerGroup).notNull() completeInstallation: Arguments: - servers: Contract: - $.class(res:Instance).notNull() - serverGroup: Contract: $.class(ServerGroup).notNull() - failedServers: Contract: - $.class(res:Instance).notNull() Body: - $success: :SoftwareComponent.detectSuccess($this.allowedInstallFailures, $serverGroup, $failedServers) - If: $success Then: - $this.completeInstallationEvent.notify($this, $servers, $serverGroup, $failedServers) - $this.onCompleteInstallation($servers, $serverGroup, $failedServers) - $this.report(format('Finished installing {0} ({1} errors encountered)', name($this), len($failedServers) or 'no')) Else: - Throw: TooManyInstallationErrors Message: format('Too many errors ({0}) encountered while installing {1}', len($failedServers), name($this)) onCompleteInstallation: Arguments: - servers: Contract: - $.class(res:Instance).notNull() - serverGroup: Contract: $.class(ServerGroup).notNull() - failedServers: Contract: - $.class(res:Instance).notNull() report: Arguments: - message: Contract: $.string().notNull() Body: - $env: $this.find(std:Environment) - If: $env Then: - $env.reporter.report($this, $message) --- # ------------------------------------------------------------------ # --- Name: Configurable Properties: allowedConfigurationFailures: Contract: $.string().notNull().check($ in ['none', 'one', 'two', 'three', 'any', 'quorum']) Default: 'none' preConfigureEvent: Contract: $.class(Event).notNull() Usage: Runtime Default: name: preConfigure configureServerEvent: Contract: $.class(Event).notNull() Usage: Runtime Default: name: configureServer completeConfigurationEvent: Contract: $.class(Event).notNull() Usage: Runtime Default: name: completeConfiguration Methods: .init: Body: - $this._randomName: randomName() configure: Arguments: - serverGroup: Contract: $.class(ServerGroup).notNull() Body: - If: $this.checkClusterIsConfigured($serverGroup) Then: - Return: - $serversToConfigure: $serverGroup.getServers().pselect( switch($this.checkServerIsConfigured($, $serverGroup) => null, true => $)).where($ != null) - $this.preConfigure($serversToConfigure, $serverGroup) - $failures: $serversToConfigure.pselect($this.configureServer($, $serverGroup)).where($ != null) - $.completeConfiguration($serversToConfigure, $serverGroup, $failures) checkClusterIsConfigured: Arguments: - serverGroup: Contract: $.class(ServerGroup).notNull() Body: - $key: list($this.getConfigurationKey()) + $serverGroup.getKey() - $state: $serverGroup.getAttr(configuration) - Return: $key = $state checkServerIsConfigured: Meta: - m:Synchronize: onArgs: server Arguments: - server: Contract: $.class(res:Instance).notNull() - serverGroup: Contract: $.class(ServerGroup).notNull() Body: - $key: $this.getConfigurationKey() - $state: $server.getAttr(configuration, null) - Return: $key = $state getConfigurationKey: Body: # should be redefined in subclasses to contain semantical signature # of the object's configuration - Return: $this._randomName preConfigure: Arguments: - servers: Contract: - $.class(res:Instance).notNull() - serverGroup: Contract: $.class(ServerGroup).notNull() Body: - $this.report(format('Applying configuration of {0}', name($this))) - $this.configureSecurity($servers, $serverGroup) - $this.preConfigureEvent.notify($this, $servers, $serverGroup) - $this.onPreConfigure($servers, $serverGroup) configureSecurity: Arguments: - servers: Contract: - $.class(res:Instance).notNull() - serverGroup: Contract: $.class(ServerGroup).notNull() onPreConfigure: Arguments: - servers: Contract: - $.class(res:Instance).notNull() - serverGroup: Contract: $.class(ServerGroup).notNull() configureServer: Meta: - m:Synchronize: onArgs: server Arguments: - server: Contract: $.class(res:Instance).notNull() - serverGroup: Contract: $.class(ServerGroup).notNull() Body: - Try: - $this.report(format('Began configuring {0} on {1}', name($this), $server.name)) - $this.configureServerEvent.notify($this, $server, $serverGroup) - $this.onConfigureServer($server, $serverGroup) Catch: - As: e Do: - $this.report(format('Unable to configure {0} on {1} due to {2}', name($this), $server.name, $e.message)) - Return: $server Else: - $key: $this.getConfigurationKey() - $server.setAttr(configuration, $key) - $this.report(format('{0} is configured at {1}', name($this), $server.name)) - Return: null onConfigureServer: Meta: - m:Synchronize: onArgs: server Arguments: - server: Contract: $.class(res:Instance).notNull() - serverGroup: Contract: $.class(ServerGroup).notNull() completeConfiguration: Arguments: - servers: Contract: - $.class(res:Instance).notNull() - serverGroup: Contract: $.class(ServerGroup).notNull() - failedServers: Contract: - $.class(res:Instance).notNull() Body: - $success: :SoftwareComponent.detectSuccess($this.allowedConfigurationFailures, $serverGroup, $failedServers) - If: $success Then: - $this.completeConfigurationEvent.notify($this, $servers, $serverGroup, $failedServers) - $this.onCompleteConfiguration($servers, $serverGroup, $failedServers) - $key: list($this.getConfigurationKey()) + $serverGroup.getKey() - $serverGroup.setAttr(configuration, $key) - $this.report(format('Finished configuring {0} ({1} errors encountered)', name($this), $numFailures or 'no')) Else: - Throw: TooManyConfigurationErrors Message: format('Too many errors ({0}) encountered while configuring {1}', $numFailures, name($this)) onCompleteConfiguration: Arguments: - servers: Contract: - $.class(res:Instance).notNull() - serverGroup: Contract: $.class(ServerGroup).notNull() - failedServers: Contract: - $.class(res:Instance).notNull() report: Arguments: - message: Contract: $.string().notNull() Body: - $env: $this.find(std:Environment) - If: $env Then: - $env.reporter.report($this, $message) --- # ------------------------------------------------------------------ # --- Name: OpenStackSecurityConfigurable Extends: Configurable Methods: configureSecurity: Arguments: - servers: Contract: - $.class(res:Instance).notNull() - serverGroup: Contract: $.class(ServerGroup).notNull() Body: - $sr: $this.getSecurityRules() - If: $sr Then: - $regions: $servers.select($.getRegion()).distinct() - $regions.pselect($this._configureSecurityGroup($, $sr)) _configureSecurityGroup: Usage: Static Arguments: - region: Contract: $.class(std:CloudRegion).notNull() - rules: Contract: - FromPort: $.int().notNull() ToPort: $.int().notNull() IpProtocol: $.string().notNull() External: $.bool().notNull() Ethertype: $.string().check($ in list(null, 'IPv4', 'IPv6')) Body: - $region.securityGroupManager.addGroupIngress($rules) - $region.stack.push() getSecurityRules: Body: - Return: {} --- # ------------------------------------------------------------------ # --- Name: SoftwareComponent Extends: - Installable - Configurable Methods: deployAt: Arguments: - serverGroup: Contract: $.class(ServerGroup).notNull() Body: - $serverGroup.deploy() - $this.install($serverGroup) - $this.configure($serverGroup) report: Arguments: - message: Contract: $.string().notNull() Body: - cast($this, Installable).report($message) detectSuccess: Usage: Static Arguments: - allowedFailures: Contract: $.string().notNull().check($ in ['none', 'one', 'two', 'three', 'any', 'quorum']) - serverGroup: Contract: $.class(ServerGroup).notNull() - failedServers: Contract: - $.class(res:Instance).notNull() Body: - $numFailures: len($failedServers) - Match: none: - Return: $numFailures = 0 one: - Return: $numFailures <= 1 two: - Return: $numFailures <= 2 three: - Return: $numFailures <= 3 any: - Return: true quorum: - $numServers: $serverGroup.getServers().count() - $maxFailures: $numServers - ($numServers/2 + 1) - Return: $numFailures <= $maxFailures Value: $allowedFailures ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/meta/io.murano.applications/Classes/events.yaml0000664000175000017500000000616200000000000024236 0ustar00zuulzuul00000000000000 Namespaces: =: io.murano.applications std: io.murano py: # empty, for python-originating exceptions --- # ------------------------------------------------------------------ # --- Name: Event Properties: name: Contract: $.string().notNull() Methods: .init: Body: - $this._handlers: {} subscribe: Arguments: - subscriber: Contract: $.class(std:Object).notNull() - methodName: Contract: $.string() Body: - If: not $methodName Then: - $methodName: format('handle{0}', $this.name.substring(0,1).toUpper()+ $this.name.substring(1)) - Try: - $method: typeinfo($subscriber).methods.where($.name = $methodName).single() Catch: With: py:StopIteration Do: - Throw: NoHandlerMethodException Message: format('Unknown method {0} for receiver {1} to handle event {2}', $methodName, $subscriber, $this.name) # This check ensures that the method passed as a handler has at least one # standard (i.e. non vararg or kwarg) argument which is supposed to be # "sender" object of the event. # Although having the sender in the handler is not always nessesary it's # still better to enforce its presence since it helps to prevent many # hard-to-debug errors - If: not $method.arguments.where($.usage=Standard).any() Then: - Throw: WrongHandlerMethodException Message: format("Method {0} of handler {1} should accept at least a 'sender' argument to handle event {2}", $methodName, $subscriber, $this.name) - $key: list($subscriber, $methodName) - $this._handlers[$key]: $this._handlers.get($key, 0) + 1 unsubscribe: Arguments: - subscriber: Contract: $.class(std:Object).notNull() - methodName: Contract: $.string() Body: - If: not $methodName Then: - $methodName: format('handle{0}', $this.name.substring(0,1).toUpper()+ $this.name.substring(1)) - $key: list($subscriber, $methodName) - If: $key in $this._handlers.keys() Then: - $this._handlers[$key]: $this._handlers[$key] - 1 - If: $this._handlers[$key] = 0 Then: - $this._handlers: $this._handlers.delete($key) notify: Arguments: - sender: Contract: $.notNull() - args: Contract: $ Usage: VarArgs - kwargs: Contract: $ Usage: KwArgs Body: - $combinedArgs: list($sender) + $args - $this._handlers.keys().select(call($[1], $combinedArgs, $kwargs, $[0])) notifyInParallel: Arguments: - sender: Contract: $.notNull() - args: Contract: $ Usage: VarArgs - kwargs: Contract: $ Usage: KwArgs Body: - $combinedArgs: list($sender) + $args - $this._handlers.keys().pselect(call($[1], $combinedArgs, $kwargs, $[0])) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/meta/io.murano.applications/Classes/replication.yaml0000664000175000017500000001251100000000000025236 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. Namespaces: =: io.murano.applications std: io.murano --- # ------------------------------------------------------------------ # --- Name: ReplicationGroup Properties: provider: Contract: $.class(ReplicaProvider).notNull() minItems: Contract: $.int().notNull() Default: 0 maxItems: Contract: $.int() Default: null numItems: Usage: InOut Contract: $.int().notNull() Default: 1 items: Usage: Out Contract: - $.class(std:Object) Methods: deploy: Body: # release excessive replicas - $oldItems: $this.items.take($this.numItems) - $itemsToRelease: $this.items.skip($this.numItems) - $this.provider.releaseReplicas($itemsToRelease) - $delta: $this.numItems - len($this.items) - $startIndex: len($this.items) + 1 - $endIndex: $startIndex + $delta - $createdItems: range($startIndex, $endIndex).select( $this.provider.createReplica($, $this)).takeWhile($ != null) - $this.items: $oldItems + $createdItems scale: Arguments: - delta: Contract: $.int().notNull() Default: 1 Body: - $this.numItems: max($this.numItems + $delta, $this.minItems) - If: $this.maxItems != null Then: - $this.numItems: min($this.numItems, $this.maxItems) - $this.deploy() .destroy: Body: - $this.numItems: 0 - $this.deploy() --- # ------------------------------------------------------------------ # --- Name: ReplicaProvider Methods: createReplica: Arguments: - index: Contract: $.int().notNull() - owner: Contract: $.class(std:Object) releaseReplicas: Arguments: replicas: Contract: - $.class(std:Object) Body: Return: $replicas --- # ------------------------------------------------------------------ # --- Name: CloneReplicaProvider Extends: ReplicaProvider Properties: template: Contract: $.template(std:Object).notNull() Methods: createReplica: Arguments: - index: Contract: $.int().notNull() - owner: Contract: $.class(std:Object) Body: - Return: new($this.template, $owner) --- # ------------------------------------------------------------------ # --- # Replica provider that is a composition of other replica providers Name: CompositeReplicaProvider Extends: ReplicaProvider Properties: providers: Contract: - $.class(ReplicaProvider).notNull() Methods: createReplica: Arguments: - index: Contract: $.int().notNull() - owner: Contract: $.class(std:Object) Body: - Return: $this.providers.select($.createReplica($index, $owner)).where($ != null).first(null) releaseReplicas: Arguments: replicas: Contract: - $.class(std:Object) Body: - For: provider In: $this.providers Do: - $replicas: $provider.releaseReplicas($replicas) - If: $replicas = null or not $replicas.any() Then: Break: - Return: $replicas --- # ------------------------------------------------------------------ # --- # A replica provider from the prepopulated pool Name: PoolReplicaProvider Extends: ReplicaProvider Properties: pool: Contract: - $.class(std:Object).notNull() consumedItems: Usage: Out Contract: - $.class(std:Object).notNull() Methods: createReplica: Arguments: - index: Contract: $.int().notNull() - owner: Contract: $.class(std:Object) Body: - $item: $this.pool.where(not $this.consumedItems.contains($)).first(null) - If: $item != null Then: - $this.consumedItems: $this.consumedItems.append($item) - Return: $item releaseReplicas: Arguments: replicas: Contract: - $.class(std:Object) Body: - $poolReplicas: $replicas.where($this.consumedItems.contains($)) - $this.consumedItems: $this.consumedItems.where(not $poolReplicas.contains($)) - Return: $replicas.where(not $poolReplicas.contains($)) --- # ------------------------------------------------------------------ # --- # Replica provider with a load balancing that returns instance from the # prepopulated list. Once the provider runs out of free items it goes to # beginning of the list and returns the same instances again. Name: RoundrobinReplicaProvider Extends: ReplicaProvider Properties: items: Contract: - $.class(std:Object).notNull() - 1 Methods: createReplica: Arguments: - index: Contract: $.int().notNull() - owner: Contract: $.class(std:Object) Body: - $index: $.getAttr(lastIndex, 0) - $.setAttr(lastIndex, ($index + 1) mod len($this.items)) - Return: $this.items[$index] ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/meta/io.murano.applications/Classes/servers.yaml0000664000175000017500000001373500000000000024427 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. Namespaces: =: io.murano.applications res: io.murano.resources std: io.murano --- # ------------------------------------------------------------------ # --- # A group of Servers Name: ServerGroup Methods: getServers: getKey: Body: - Return: $this.getServers().select(id($)).orderBy($) deployServers: Usage: Static Arguments: - serverGroup: Contract: $.class(ServerGroup) - servers: Contract: - $.class(res:Instance).notNull() Body: - $environment: $serverGroup.find(std:Environment) - $servers.select($this._deployServer($, $environment, $serverGroup)) - $servers.select($.endDeploy()) releaseServers: Usage: Static Arguments: - servers: Contract: - $.class(res:Instance).notNull() Body: - $servers.select($.beginReleaseResources()) - $servers.select($.endReleaseResources()) _deployServer: Usage: Static Arguments: - server: Contract: $.class(res:Instance).notNull() - environment: Contract: $.class(std:Environment) - serverGroup: Contract: $.class(ServerGroup) Body: - If: $environment and not $server.openstackId Then: - $environment.reporter.report($serverGroup, 'Provisioning VM for ' + (name($server) or $server.name)) - $server.beginDeploy() --- # ------------------------------------------------------------------ # --- # A group of prepopulated servers Name: ServerList Extends: ServerGroup Properties: servers: Contract: - $.class(res:Instance).notNull() Methods: deploy: Body: - $this.deployServers($this, $this.servers) .destroy: Body: - $this.releaseServers($this.servers) getServers: Body: Return: $.servers --- # ------------------------------------------------------------------ # --- # Degenrate case of a server group which consists of a single server Name: SingleServerGroup Extends: ServerGroup Properties: server: Contract: $.class(res:Instance).notNull() Methods: setServer: Arguments: - server: Contract: $.class(res:Instance).notNull() Body: - $this.items: $server deploy: Body: - $this.deployServers($this, [$this.server]) .destroy: Body: - $this.releaseServers([$this.server]) getServers: Body: Return: [$.server] --- # ------------------------------------------------------------------ # --- # A replication group aggregating Servers # Adds a logic to concurrently provision and unprovision servers Name: ServerReplicationGroup Extends: - ReplicationGroup - ServerGroup Properties: provider: Contract: $.class(ReplicaProvider).notNull() items: Usage: Out Contract: - $.class(res:Instance) Methods: .init: Body: - $this._env: $.find(std:Environment).require() deploy: Body: - $delta: $this.numItems - len($this.items) - If: abs($delta) > 1 Then: - $verb: switch($delta > 0 => Creating, $delta < 0 => Removing) - $target: switch($delta > 0 => for, $delta < 0 => from) - If: name($this) Then: - $target: format(' {0} {1}', $target, name($this)) Else: - $target: '' - $this._env.reporter.report($this, format('{0} {1} servers{2}', $verb, abs($delta), $target)) - cast($this, ReplicationGroup).deploy() - $this.deployServers($this, $this.items) getServers: Body: Return: $.items --- # ------------------------------------------------------------------ # --- # A server group that composed of other server groups Name: CompositeServerGroup Extends: ServerGroup Properties: serverGroups: Contract: - $.class(ServerGroup).notNull() Methods: deploy: Body: - $this.serverGroups.pselect($.deploy()) getServers: Body: Return: $this.serverGroups.selectMany($.getServers()) --- # ------------------------------------------------------------------ # --- # A replication provider acting as a default factory class for Servers Name: TemplateServerProvider Extends: ReplicaProvider Properties: template: Contract: $.template(res:Instance, excludeProperties => [name]).notNull() serverNamePattern: Contract: $.string().notNull() allocated: Usage: Out Contract: $.int().notNull() Default: 0 capacity: Contract: $.int() Methods: createReplica: Arguments: - index: Contract: $.int().notNull() - owner: Contract: $.class(std:Object) Body: - If: $this.capacity = null or $this.allocated < $this.capacity Then: - $template: $this.template - $template.name: $this.serverNamePattern.format($index) - $ownerGroup: $this.find(ServerGroup) - If: $ownerGroup and name($ownerGroup) Then: - $groupName: format(' ({0})', name($ownerGroup)) Else: - $groupName: '' - $template['?'].name: format('Server {0}{1}', $index, $groupName) - $this.allocated: $this.allocated + 1 - Return: new($template, $owner) Else: - Return: null releaseReplicas: Arguments: replicas: Contract: - $.class(res:Instance) Body: - $replicas.select($.beginReleaseResources()) - $replicas.select($.endReleaseResources()) - $this.allocated: max(0, $this.allocated - len($replicas)) - Return: [] ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1696417899.7571807 murano-16.0.0/meta/io.murano.applications/Classes/tests/0000775000175000017500000000000000000000000023203 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/meta/io.murano.applications/Classes/tests/TestEvents.yaml0000664000175000017500000001623700000000000026204 0ustar00zuulzuul00000000000000Namespaces: =: io.murano.applications.tests tst: io.murano.test apps: io.murano.applications --- # ------------------------------------------------------------------ # --- Name: TestSubscriber Properties: called: Usage: Runtime Default: 0 Contract: $.int() lastSender: Usage: Runtime Contract: $ lastFoo: Usage: Runtime Contract: $ lastBar: Usage: Runtime Contract: $ Methods: handleFoo: Arguments: - sender: Contract: $.notNull() - foo: Contract: $ - bar: Contract: $ Body: - $this.called: $this.called + 1 - $this.lastFoo: $foo - $this.lastBar: $bar - $this.lastSender: $sender handleWithNoExtraArgs: Arguments: - sender: Contract: $.notNull() Body: - $this.called: $this.called + 1 - $this.lastSender: $sender noArgsMethod: Body: varArgsKwArgsOnlyMethod: Arguments: - args: Usage: VarArgs Contract: $ - kwargs: Usage: KwArgs Contract: $ Body: reset: Body: - $this.called: 0 - $this.lastSender: null - $this.lastFoo: null - $this.lastBar: null --- # ------------------------------------------------------------------ # --- Name: TestEmitter Properties: foo: Usage: Runtime Contract: $.class(apps:Event).notNull() Default: name: foo Methods: onFoo: Body: - $this.foo.notify($this) --- # ------------------------------------------------------------------ # --- Name: TestEvents Extends: tst:TestFixture Methods: testSubscribeAndNotify: Body: - $subscriber: new(TestSubscriber) - $event: new(apps:Event, name=>testEvent) - $event.subscribe($subscriber, handleFoo) - $event.notify($this, 'Hello Events', 42) - $this.assertEqual(1, $subscriber.called) - $this.assertEqual('Hello Events', $subscriber.lastFoo) - $this.assertEqual(42, $subscriber.lastBar) - $event.notify($this) - $this.assertEqual(2, $subscriber.called) testNotifyWithNoSubscribers: Body: - $event: new(apps:Event, name=>testEvent) - $event.notify($this) testUnableToNotifyWithUnexpectedArgs: Body: - $subscriber: new(TestSubscriber) - $event: new(apps:Event, name=>testEvent) - $event.subscribe($subscriber, handleFoo) - $event.notify($this) - $cought: false - Try: - $event.notify($this, qux=>1, baz=>2) Catch: With: 'yaql.language.exceptions.NoMatchingMethodException' Do: - $cought: true - $this.assertTrue($cought) testUnsubscribe: Body: - $subscriber: new(TestSubscriber) - $event: new(apps:Event, name=>testEvent) - $event.subscribe($subscriber, handleFoo) - $event.notify($this) - $this.assertEqual(1, $subscriber.called) - $event.unsubscribe($subscriber, handleFoo) - $event.notify($this) - $this.assertEqual(1, $subscriber.called) testSubscribeManyNotifyOnce: Body: - $subscriber: new(TestSubscriber) - $event: new(apps:Event, name=>testEvent) - $event.subscribe($subscriber, handleFoo) - $event.subscribe($subscriber, handleFoo) - $event.subscribe($subscriber, handleFoo) - $event.subscribe($subscriber, handleFoo) - $event.subscribe($subscriber, handleFoo) - $event.notify($this) - $this.assertEqual(1, $subscriber.called) testUnsubscribeAsManyAsSubscribe: Body: - $subscriber: new(TestSubscriber) - $event: new(apps:Event, name=>testEvent) - $event.subscribe($subscriber, handleFoo) - $event.subscribe($subscriber, handleFoo) - $event.subscribe($subscriber, handleFoo) - $event.notify($this) - $this.assertEqual(1, $subscriber.called) - $subscriber.reset() - $event.unsubscribe($subscriber, handleFoo) - $event.notify($this) - $this.assertEqual(1, $subscriber.called) - $subscriber.reset() - $event.unsubscribe($subscriber, handleFoo) - $event.notify($this) - $this.assertEqual(1, $subscriber.called) - $subscriber.reset() - $event.unsubscribe($subscriber, handleFoo) - $event.notify($this) - $this.assertEqual(0, $subscriber.called) testSubscribeSimple: Body: - $event: new(apps:Event, name=>foo) - $subscriber: new(TestSubscriber) - $event.subscribe($subscriber) - $event.notify($this) - $this.assertEqual(1, $subscriber.called) testHandleWithNoExtraArgs: Body: - $event: new(apps:Event, name=>foo) - $subscriber: new(TestSubscriber) - $event.subscribe($subscriber, handleWithNoExtraArgs) - $event.notify($this) - $this.assertEqual(1, $subscriber.called) - $this.assertEqual($this, $subscriber.lastSender) testUnableToSubscribeWithWrongMethod: Body: - $event: new(apps:Event, name=>testEvent) - $subscriber: new(TestSubscriber) - $cought: false - Try: - $event.subscribe($subscriber, handleBar) Catch: With: apps:NoHandlerMethodException Do: - $cought: true - $this.assertTrue($cought) testUnableToSubscribeWithWrongSimpleMethod: Body: - $event: new(apps:Event, name=>testEvent) - $subscriber: new(TestSubscriber) - $cought: false - Try: - $event.subscribe($subscriber) Catch: With: apps:NoHandlerMethodException Do: - $cought: true - $this.assertTrue($cought) testUnableToSubscribeWithoutSender: Body: - $event: new(apps:Event, name=>testEvent) - $subscriber: new(TestSubscriber) - $cought: false - Try: - $event.subscribe($subscriber, noArgsMethod) Catch: With: apps:WrongHandlerMethodException Do: - $cought: true - $this.assertTrue($cought) testUnableToSubscribeWithoutStandardArgs: Body: - $event: new(apps:Event, name=>testEvent) - $subscriber: new(TestSubscriber) - $cought: false - Try: - $event.subscribe($subscriber, varArgsKwArgsOnlyMethod) Catch: With: apps:WrongHandlerMethodException Do: - $cought: true - $this.assertTrue($cought) testMultipleSubscribers: Body: - $subscriber1: new(TestSubscriber) - $subscriber2: new(TestSubscriber) - $event: new(apps:Event, name=>testEvent) - $event.subscribe($subscriber1, handleFoo) - $event.subscribe($subscriber2, handleFoo) - $event.notify($this, 'Hello Events', 42) - $this.assertEqual(1, $subscriber1.called) - $this.assertEqual('Hello Events', $subscriber1.lastFoo) - $this.assertEqual(42, $subscriber1.lastBar) - $this.assertEqual(1, $subscriber2.called) - $this.assertEqual('Hello Events', $subscriber2.lastFoo) - $this.assertEqual(42, $subscriber2.lastBar) testEmitterWithEvent: Body: - $emitter: new(TestEmitter) - $subscriber: new(TestSubscriber) - $emitter.foo.subscribe($subscriber) - $emitter.onFoo() - $this.assertEqual(1, $subscriber.called) - $this.assertEqual($emitter, $subscriber.lastSender) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/meta/io.murano.applications/Classes/tests/TestReplication.yaml0000664000175000017500000001344500000000000027207 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. Namespaces: =: io.murano.applications.tests tst: io.murano.test std: io.murano apps: io.murano.applications --- # ------------------------------------------------------------------ # --- Name: Replica Properties: name: Contract: $.string() --- # ------------------------------------------------------------------ # --- Name: DummyReplicaProvider Extends: apps:ReplicaProvider Properties: allocated: Usage: InOut Contract: $.int() Default: 0 Methods: createReplica: Arguments: - index: Contract: $.int() - owner: Contract: $.class(std:Object) Body: - $replica: new(Replica, name => format('replica-{0}', $index)) - $this.allocated: $this.allocated + 1 - Return: $replica releaseReplicas: Arguments: replicas: Contract: - $ Body: - $this.allocated: $this.allocated - len($replicas) --- # ------------------------------------------------------------------ # --- Name: TestReplication Extends: tst:TestFixture Methods: setUp: Body: - $this.provider: new(DummyReplicaProvider) testCreateDefault: Body: - $group: new(apps:ReplicationGroup, provider => $this.provider) - $group.deploy() - $.assertEqual(1, len($group.items)) - $.assertEqual(1, $this.provider.allocated) - $.assertEqual('replica-1', $group.items[0].name) testCreateMultiple: Body: - $group: new(apps:ReplicationGroup, provider => $this.provider, numItems => 5) - $group.deploy() - $.assertEqual(5, len($group.items)) testScale: Body: - $group: new(apps:ReplicationGroup, provider => $this.provider) - $group.deploy() - $.assertEqual(1, len($group.items)) - $group.scale(1) - $.assertEqual(2, len($group.items)) - $.assertEqual(2, $this.provider.allocated) - $group.scale(-1) - $.assertEqual(1, len($group.items)) - $.assertEqual(1, $this.provider.allocated) --- # ------------------------------------------------------------------ # --- Name: TestPoolReplicaProvider Extends: tst:TestFixture Methods: setUp: Body: - $this.object1: new(std:Object) - $this.object2: new(std:Object) - $this.provider: new(apps:PoolReplicaProvider, pool => [$this.object1, $this.object2]) testReplicas: Body: - $.assertEqual(2, len($this.provider.pool)) - $.assertEqual(0, len($this.provider.consumedItems)) - $obj: $this.provider.createReplica(1, $this) - $.assertEqual($this.object1, $obj) - $.assertEqual(2, len($this.provider.pool)) - $.assertEqual(1, len($this.provider.consumedItems)) - $obj: $this.provider.createReplica(2, $this) - $.assertEqual($this.object2, $obj) - $.assertEqual(2, len($this.provider.pool)) - $.assertEqual(2, len($this.provider.consumedItems)) - $obj: $this.provider.createReplica(3, $this) - $.assertEqual(null, $obj) - $.assertEqual(2, len($this.provider.pool)) - $.assertEqual(2, len($this.provider.consumedItems)) testReleaseReplicas: Body: - $obj: $this.provider.createReplica(1, $this) - $.assertEqual(1, len($this.provider.consumedItems)) - $foreignObj: new(std:Object) - $res: $this.provider.releaseReplicas([$obj, $this.object1, $this.object2, $foreignObj]) - $.assertEqual(0, len($this.provider.consumedItems)) - $.assertEqual([$this.object2, $foreignObj], $res) - $this.testReplicas() --- # ------------------------------------------------------------------ # --- Name: TestRoundrobinReplicaProvider Extends: tst:TestFixture Methods: setUp: Body: - $this.object1: new(std:Object) - $this.object2: new(std:Object) - $this.provider: new(apps:RoundrobinReplicaProvider, items => [$this.object1, $this.object2]) testReplicas: Body: - $obj: $this.provider.createReplica(1, $this) - $.assertEqual($this.object1, $obj) - $obj: $this.provider.createReplica(2, $this) - $.assertEqual($this.object2, $obj) - $obj: $this.provider.createReplica(3, $this) - $.assertEqual($this.object1, $obj) - $obj: $this.provider.createReplica(4, $this) - $.assertEqual($this.object2, $obj) --- # ------------------------------------------------------------------ # --- Name: TestCompositeReplicaProvider Extends: tst:TestFixture Methods: setUp: Body: - $this.objects: range(4).select(new(std:Object)) - $this.object2: new(std:Object) - $this.provider1: new(apps:PoolReplicaProvider, pool => [$this.objects[0], $this.objects[1]]) - $this.provider2: new(apps:RoundrobinReplicaProvider, items => [$this.objects[2], $this.objects[3]]) - $this.provider: new(apps:CompositeReplicaProvider, providers => [$this.provider1, $this.provider2]) testReplicas: Body: - $obj: $this.provider.createReplica(1, $this) - $.assertEqual($this.objects[0], $obj) - $obj: $this.provider.createReplica(2, $this) - $.assertEqual($this.objects[1], $obj) - $obj: $this.provider.createReplica(3, $this) - $.assertEqual($this.objects[2], $obj) - $obj: $this.provider.createReplica(4, $this) - $.assertEqual($this.objects[3], $obj) - $obj: $this.provider.createReplica(5, $this) - $.assertEqual($this.objects[2], $obj) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/meta/io.murano.applications/Classes/tests/TestServerProviders.yaml0000664000175000017500000001250500000000000030076 0ustar00zuulzuul00000000000000Namespaces: =: io.murano.applications.tests tst: io.murano.test apps: io.murano.applications sys: io.murano.system res: io.murano.resources --- # ------------------------------------------------------------------ # --- Name: TestMockedServerFactory Extends: tst:TestFixtureWithEnvironment Properties: reports: Contract: - $.string() Usage: Out Default: [] Methods: report: Arguments: - server: Contract: $ - message: Contract: $.string() Body: - $this.reports: $this.reports.append($message) heatPushInjection: Body: - $this.currentTemplate: $this.environment.stack.current() - $this.pushCalled: $this.pushCalled + 1 heatOutputInjection: Body: # generate simulated output - $outputKeys: $this.currentTemplate.outputs.keys() - $idIndex: 1000 - $ipIndex: 100 - $ipPrefix: '10.0.0.' - $defaultNetworkName: testNetwork - $output: {} - For: key In: $outputKeys Do: # if output name ends with -id then it is some openstack-id of a resource - If: $key.endsWith('-id') Then: - $output[$key]: $idIndex - $idIndex: $idIndex + 1 # if output name ends with -assigned-ips then it is some ip of a vm - If: $key.endsWith('-assigned-ips') Then: - $output[$key]: $defaultNetworkName: list(format('{0}{1}', $ipPrefix, $ipIndex)) - $ipIndex: $ipIndex + 1 - Return: $output neutronListExtensionsInjection: Body: - Return: - alias: 'security-group' setUp: Body: - $this.currentTemplate: {} - inject(sys:NetworkExplorer, listNeutronExtensions, $this, neutronListExtensionsInjection) - inject(sys:NetworkExplorer, getDefaultRouter, '42') - inject(sys:NetworkExplorer, getAvailableCidr, '10.0.0.0/24') - super($this, $.setUp()) - inject($this.environment.stack, push, $this, heatPushInjection) - inject($this.environment.stack, output, $this, heatOutputInjection) - inject($this.environment.stack, delete, '') - inject(sys:Agent, prepare, '') - inject($this.environment.instanceNotifier, trackCloudInstance, '') - inject($this.environment.instanceNotifier, untrackCloudInstance, '') - $this.reports: [] - inject($this.environment.reporter, report, $this, report) - $this.pushCalled: 0 - $serverTemplate: image: 'murano-latest' flavor: 't1.medium' - $this.provider: new(apps:TemplateServerProvider, template => $serverTemplate, serverNamePattern => 'testNode-{0}') testCreateSingleServer: Body: - $ssg: new(apps:ServerReplicationGroup, $this.environment, provider => $this.provider) - $ssg.deploy() - $this.assertServerCount(1) testServersHaveProperName: Body: - $model: apps:ServerReplicationGroup: numItems: 2 provider: apps:TemplateServerProvider: template: $this.provider.template serverNamePattern: 'testNode-{0}' name: testGroup - $namedSsg: new($model, $this.environment) - $namedSsg.deploy() - $this.assertEqual('Server 1 (testGroup)', name($namedSsg.items[0])) - $this.assertEqual('Server 2 (testGroup)', name($namedSsg.items[1])) testCreateMultipleServers: Body: - $ssg: new(apps:ServerReplicationGroup, $this.environment, provider => $this.provider, numItems => 5) - $ssg.deploy() - $this.assertServerCount(5) testCreateScaleUp: Body: - $ssg: new(apps:ServerReplicationGroup, $this.environment, provider => $this.provider, numItems => 3) - $ssg.deploy() - $this.assertServerCount(3) - $ssg.scale(4) - $ssg.deploy() - $this.assertServerCount(7) testCreateScaleDown: Body: - $ssg: new(apps:ServerReplicationGroup, $this.environment, provider => $this.provider, numItems => 3) - $ssg.deploy() - $this.assertServerCount(3) - $ssg.scale(-2) - $ssg.deploy() - $this.assertServerCount(1) testMultipleServersReporting: Body: - $ssg: new(apps:ServerReplicationGroup, $this.environment, TestGroup, provider => $this.provider, numItems => 3) - $ssg.deploy() - $this.assertEqual('Creating 3 servers for TestGroup', $this.reports[0]) - $ssg.scale(-2) - $this.assertEqual('Removing 2 servers from TestGroup', $this.reports[4]) testMultipleServersReportingNoGroupName: Body: - $ssg: new(apps:ServerReplicationGroup, $this.environment, null, provider => $this.provider, numItems => 3) - $ssg.deploy() - $this.assertEqual('Creating 3 servers', $this.reports[0]) - $ssg.scale(-2) - $this.assertEqual('Removing 2 servers', $this.reports[4]) testNoReportingIfSingleServer: Body: - $ssg: new(apps:ServerReplicationGroup, $this.environment, TestGroup, provider => $this.provider, numItems => 1) - $ssg.deploy() - $this.assertEqual(1, len($this.reports)) assertServerCount: Arguments: - count: Contract: $.int() Body: - $this.assertEqual($count, $this.currentTemplate.resources.values().where( $.type = 'OS::Nova::Server').len()) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/meta/io.murano.applications/Classes/tests/TestSoftwareComponent.yaml0000664000175000017500000001006700000000000030410 0ustar00zuulzuul00000000000000Namespaces: =: io.murano.applications.tests tst: io.murano.test apps: io.murano.applications res: io.murano.resources --- # ------------------------------------------------------------------ # --- Name: InstallableToTest Extends: - apps:Installable --- # ------------------------------------------------------------------ # --- Name: ConfigurableToTest Extends: - apps:Configurable --- # ------------------------------------------------------------------ # --- Name: SoftwareComponentToTest Extends: - apps:SoftwareComponent --- # ------------------------------------------------------------------ # --- Name: TestSoftwareComponent Extends: tst:TestFixtureWithEnvironment Properties: reports: Contract: - $.string() Usage: Out Default: [] Methods: report: Arguments: - server: Contract: $ - message: Contract: $.string() Body: - $this.reports: $this.reports.append($message) setUp: Body: - super($this, $.setUp()) - inject(res:LinuxMuranoInstance, beginDeploy, '') - inject(res:LinuxMuranoInstance, endDeploy, '') - $server: new(res:LinuxMuranoInstance, $this.environment, name => 'noop', image => 'noop', flavor => 'noop') - $provider: new(apps:TemplateServerProvider, $this.environment, template => $server, serverNamePattern => 'testNode-{0}') - $this.group: new(apps:ServerReplicationGroup, $this.environment, provider => $provider, numItems => 5) - $this.group.deploy() - $this.reports: [] - inject($this.environment.reporter, report, $this, report) testInstallReportingSequence: Body: - $cmp: new(InstallableToTest, $this.environment, testComp) - $cmp.install($this.group) - $this.assertInstallingSequence(0) testConfigureReportingSequence: Body: - $cmp: new(ConfigurableToTest, $this.environment, testComp) - $cmp.configure($this.group) - $this.assertConfiguringSequence(0) testCombinedSequence: Body: - $cmp: new(SoftwareComponentToTest, $this.environment, testComp) - $cmp.deployAt($this.group) - $this.assertProvisioningSequence(0) - $this.assertInstallingSequence($this.group.numItems) - $this.assertConfiguringSequence(3 * $this.group.numItems + 2) assertProvisioningSequence: Arguments: - offset: Contract: $.int().notNull() Body: - $this.assertEqual( range(1, $this.group.numItems + 1).select('Provisioning VM for Server {0}'.format($)), $this.reports.skip($offset).take($this.group.numItems)) assertInstallingSequence: Arguments: - offset: Contract: $.int().notNull() Body: - $this.assertEqual('Installing testComp', $this.reports[$offset]) - $nodeReports: range(0, $this.group.numItems * 2).select($this.reports[$offset + 1 + $]) - range(1, $this.group.numItems + 1).select( $this.assertTrue(format('Began installing testComp on testNode-{0}', $) in $nodeReports)) - range(1, $this.group.numItems + 1).select( $this.assertTrue(format('testComp is installed on testNode-{0}', $) in $nodeReports)) - $this.assertEqual('Finished installing testComp (no errors encountered)', $this.reports[$offset + 2 * $this.group.numItems + 1]) assertConfiguringSequence: Arguments: - offset: Contract: $.int().notNull() Body: - $this.assertEqual('Applying configuration of testComp', $this.reports[$offset]) - $nodeReports: range(0, $this.group.numItems * 2).select($this.reports[$offset + 1 + $]) - range(1, $this.group.numItems + 1).select( $this.assertTrue(format('Began configuring testComp on testNode-{0}', $) in $nodeReports)) - range(1, $this.group.numItems + 1).select( $this.assertTrue(format('testComp is configured at testNode-{0}', $) in $nodeReports)) - $this.assertEqual('Finished configuring testComp (no errors encountered)', $this.reports[$offset + 2 * $this.group.numItems + 1]) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/meta/io.murano.applications/LICENSE0000664000175000017500000002363600000000000021463 0ustar00zuulzuul00000000000000 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. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/meta/io.murano.applications/manifest.yaml0000664000175000017500000000501400000000000023136 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. Format: 1.4 Type: Library FullName: io.murano.applications Name: Application Development Library Description: | Library of base class to develop scalable Applications with MuranoPL Author: Mirantis, Inc. Classes: io.murano.applications.ReplicaProvider: replication.yaml io.murano.applications.ReplicationGroup: replication.yaml io.murano.applications.CloneReplicaProvider: replication.yaml io.murano.applications.CompositeReplicaProvider: replication.yaml io.murano.applications.PoolReplicaProvider: replication.yaml io.murano.applications.RoundrobinReplicaProvider: replication.yaml io.murano.applications.Event: events.yaml io.murano.applications.ServerGroup: servers.yaml io.murano.applications.ServerList: servers.yaml io.murano.applications.CompositeServerGroup: servers.yaml io.murano.applications.SingleServerGroup: servers.yaml io.murano.applications.ServerReplicationGroup: servers.yaml io.murano.applications.TemplateServerProvider: servers.yaml io.murano.applications.Installable: component.yaml io.murano.applications.Configurable: component.yaml io.murano.applications.OpenStackSecurityConfigurable: component.yaml io.murano.applications.SoftwareComponent: component.yaml io.murano.applications.SingleServerApplication: baseapps.yaml io.murano.applications.MultiServerApplication: baseapps.yaml io.murano.applications.MultiServerApplicationWithScaling: baseapps.yaml # Tests io.murano.applications.tests.TestReplication: tests/TestReplication.yaml io.murano.applications.tests.TestPoolReplicaProvider: tests/TestReplication.yaml io.murano.applications.tests.TestRoundrobinReplicaProvider: tests/TestReplication.yaml io.murano.applications.tests.TestCompositeReplicaProvider: tests/TestReplication.yaml io.murano.applications.tests.TestEvents: tests/TestEvents.yaml io.murano.applications.tests.TestMockedServerFactory: tests/TestServerProviders.yaml io.murano.applications.tests.TestSoftwareComponent: tests/TestSoftwareComponent.yaml ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1696417899.7731807 murano-16.0.0/murano/0000775000175000017500000000000000000000000014423 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/__init__.py0000664000175000017500000000000000000000000016522 0ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1696417899.7731807 murano-16.0.0/murano/api/0000775000175000017500000000000000000000000015174 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/api/__init__.py0000664000175000017500000000114200000000000017303 0ustar00zuulzuul00000000000000# # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # 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 murano.monkey_patch # noqa ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1696417899.7731807 murano-16.0.0/murano/api/middleware/0000775000175000017500000000000000000000000017311 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/api/middleware/__init__.py0000664000175000017500000000000000000000000021410 0ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/api/middleware/context.py0000664000175000017500000000442400000000000021353 0ustar00zuulzuul00000000000000# 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 oslo_config import cfg from oslo_middleware import request_id as oslo_request_id from oslo_serialization import jsonutils from murano.common.i18n import _ from murano.common import wsgi import murano.context context_opts = [ cfg.StrOpt('admin_role', default='admin', help=_('Role used to identify an authenticated user as ' 'administrator.'))] CONF = cfg.CONF CONF.register_opts(context_opts) CONF = cfg.CONF class ContextMiddleware(wsgi.Middleware): def process_request(self, req): """Convert authentication information into a request context Generate a murano.context.RequestContext object from the available authentication headers and store on the 'context' attribute of the req object. :param req: wsgi request object that will be given the context object """ roles = [r.strip() for r in req.headers.get('X-Roles').split(',')] kwargs = { 'user': req.headers.get('X-User-Id'), 'project_id': req.headers.get('X-Tenant-Id'), 'auth_token': req.headers.get('X-Auth-Token'), 'session': req.headers.get('X-Configuration-Session'), 'is_admin': CONF.admin_role in roles, 'request_id': req.environ.get(oslo_request_id.ENV_REQUEST_ID), 'roles': roles } sc_header = req.headers.get('X-Service-Catalog') if sc_header: kwargs['service_catalog'] = jsonutils.loads(sc_header) req.context = murano.context.RequestContext(**kwargs) @classmethod def factory(cls, global_conf, **local_conf): def filter(app): return cls(app) return filter ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/api/middleware/ext_context.py0000664000175000017500000001010600000000000022225 0ustar00zuulzuul00000000000000# 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 base64 from keystoneauth1 import exceptions from keystoneauth1.identity import v3 from keystoneauth1 import session as ks_session from oslo_config import cfg from oslo_log import log from webob import exc from murano.common.i18n import _ from murano.common import wsgi CONF = cfg.CONF LOG = log.getLogger(__name__) class ExternalContextMiddleware(wsgi.Middleware): def get_keystone_token(self, user, password): # TODO(starodubcevna): picking up project_name and auth_url from # section related to Cloud Foundry service broker is probably a duct # tape and should be rewritten as soon as we get more non-OpenStack # services as murano recipients. The same is right for project and user # domain names. auth_url = CONF.cfapi.auth_url if not (auth_url.endswith('v2.0') or auth_url.endswith('v3')): auth_url += '/v3' elif auth_url.endswith('v2.0'): auth_url = auth_url.replace('v2.0', 'v3') elif auth_url.endswith('v3'): pass else: LOG.warning('Wrong format for service broker auth url') kwargs = {'auth_url': auth_url, 'username': user, 'password': password, 'project_name': CONF.cfapi.tenant, 'user_domain_name': CONF.cfapi.user_domain_name, 'project_domain_name': CONF.cfapi.project_domain_name} password_auth = v3.Password(**kwargs) session = ks_session.Session(auth=password_auth) self._query_endpoints(password_auth, session) return session.get_token() def _query_endpoints(self, auth, session): if not hasattr(self, '_murano_endpoint'): try: self._murano_endpoint = auth.get_endpoint( session, 'application-catalog') except exceptions.EndpointNotFound: pass if not hasattr(self, '_glare_endpoint'): try: self._glare_endpoint = auth.get_endpoint( session, 'artifact') except exceptions.EndpointNotFound: pass def get_endpoints(self): return { 'murano': getattr(self, '_murano_endpoint', None), 'glare': getattr(self, '_glare_endpoint', None), } def process_request(self, req): """Get keystone token for external request This middleware is used for external requests. It get credentials from request and try to get keystone token for further authorization. :param req: wsgi request object that will be given the context object """ try: credentials = base64.b64decode( req.headers['Authorization'].split(' ')[1]) user, password = credentials.decode('utf-8').split(':', 2) req.headers['X-Auth-Token'] = self.get_keystone_token(user, password) req.endpoints = self.get_endpoints() except KeyError: msg = _("Authorization required") LOG.warning(msg) raise exc.HTTPUnauthorized(explanation=msg) except exceptions.Unauthorized: msg = _("Your credentials are wrong. Please try again") LOG.warning(msg) raise exc.HTTPUnauthorized(explanation=msg) @classmethod def factory(cls, global_conf, **local_conf): def filter(app): return cls(app) return filter ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/api/middleware/fault.py0000664000175000017500000000772200000000000021006 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. """A middleware that turns exceptions into parsable string. Inspired by Cinder's faultwrapper """ import sys import traceback from oslo_config import cfg import webob from murano.common import wsgi from murano.packages import exceptions as pkg_exc class HTTPExceptionDisguise(Exception): """Disguises HTTP exceptions Disguises HTTP exceptions so they can be handled by the webob fault application in the wsgi pipeline. """ def __init__(self, exception): self.exc = exception self.tb = sys.exc_info()[2] class Fault(object): def __init__(self, error): self.error = error @webob.dec.wsgify(RequestClass=wsgi.Request) def __call__(self, req): if req.content_type == 'application/xml': serializer = wsgi.XMLDictSerializer() else: serializer = wsgi.JSONDictSerializer() resp = webob.Response(request=req) default_webob_exc = webob.exc.HTTPInternalServerError() resp.status_code = self.error.get('code', default_webob_exc.code) serializer.default(resp, self.error) return resp class FaultWrapper(wsgi.Middleware): """Replace error body with something the client can parse.""" @classmethod def factory(cls, global_conf, **local_conf): def filter(app): return cls(app) return filter error_map = { 'ValueError': webob.exc.HTTPBadRequest, 'LookupError': webob.exc.HTTPNotFound, 'PackageClassLoadError': webob.exc.HTTPBadRequest, 'PackageUILoadError': webob.exc.HTTPBadRequest, 'PackageLoadError': webob.exc.HTTPBadRequest, 'PackageFormatError': webob.exc.HTTPBadRequest, } def _map_exception_to_error(self, class_exception): if class_exception == Exception: return webob.exc.HTTPInternalServerError if class_exception.__name__ not in self.error_map: return self._map_exception_to_error(class_exception.__base__) return self.error_map[class_exception.__name__] def _error(self, ex): trace = None webob_exc = None if isinstance(ex, HTTPExceptionDisguise): # An HTTP exception was disguised so it could make it here # let's remove the disguise and set the original HTTP exception if cfg.CONF.debug: trace = ''.join(traceback.format_tb(ex.tb)) ex = ex.exc webob_exc = ex ex_type = ex.__class__.__name__ full_message = str(ex) if full_message.find('\n') > -1: message, msg_trace = full_message.split('\n', 1) else: msg_trace = traceback.format_exc() message = full_message if isinstance(ex, pkg_exc.PackageException): message = ex.message if cfg.CONF.debug and not trace: trace = msg_trace if not webob_exc: webob_exc = self._map_exception_to_error(ex.__class__) error = { 'code': webob_exc.code, 'title': webob_exc.title, 'explanation': webob_exc.explanation, 'error': { 'message': message, 'type': ex_type, 'traceback': trace, } } return error def process_request(self, req): try: return req.get_response(self.application) except Exception as exc: return req.get_response(Fault(self._error(exc))) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/api/middleware/version_negotiation.py0000664000175000017500000000630200000000000023751 0ustar00zuulzuul00000000000000# 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. """ A filter middleware that inspects the requested URI for a version string and/or Accept headers and attempts to negotiate an API controller to return """ from oslo_log import log as logging from murano.api import versions from murano.common import wsgi LOG = logging.getLogger(__name__) class VersionNegotiationFilter(wsgi.Middleware): @classmethod def factory(cls, global_conf, **local_conf): def filter(app): return cls(app) return filter def __init__(self, app): self.versions_app = versions.Controller() super(VersionNegotiationFilter, self).__init__(app) def process_request(self, req): """Try to find a version first in the accept header, then the URL.""" LOG.debug(("Determining version of request:{method} {path} " "Accept: {accept}").format(method=req.method, path=req.path, accept=req.accept)) LOG.debug("Using url versioning") # Remove version in url so it doesn't conflict later req_version = self._pop_path_info(req) try: version = self._match_version_string(req_version) except ValueError: LOG.warning("Unknown version. Returning version choices.") return self.versions_app req.environ['api.version'] = version req.path_info = ''.join(('/v', str(version), req.path_info)) LOG.debug("Matched version: v{version}".format(version=version)) LOG.debug('new path {path}'.format(path=req.path_info)) return None def _match_version_string(self, subject): """Tries to match major and/or minor version Given a string, tries to match a major and/or minor version number. :param subject: The string to check :returns version found in the subject :raises ValueError if no acceptable version could be found """ if subject in ('v1',): major_version = 1 else: raise ValueError() return major_version def _pop_path_info(self, req): """Returns the popped off next segment 'Pops' off the next segment of PATH_INFO, returns the popped segment. Do NOT push it onto SCRIPT_NAME. """ path = req.path_info if not path: return None while path.startswith('/'): path = path[1:] idx = path.find('/') if idx == -1: idx = len(path) r = path[:idx] req.path_info = path[idx:] return r ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1696417899.777181 murano-16.0.0/murano/api/v1/0000775000175000017500000000000000000000000015522 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/api/v1/__init__.py0000664000175000017500000000267500000000000017645 0ustar00zuulzuul00000000000000# 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. stats = None SUPPORTED_PARAMS = {'id', 'order_by', 'category', 'marker', 'tag', 'class_name', 'limit', 'type', 'fqn', 'category', 'owned', 'search', 'include_disabled', 'sort_dir', 'name'} LIST_PARAMS = {'id', 'category', 'tag', 'class', 'order_by'} ORDER_VALUES = {'fqn', 'name', 'created'} OPERATOR_VALUES = {'id', 'category', 'tag'} PKG_PARAMS_MAP = {'display_name': 'name', 'full_name': 'fully_qualified_name', 'ui': 'ui_definition', 'logo': 'logo', 'package_type': 'type', 'description': 'description', 'author': 'author', 'classes': 'class_definitions', 'tags': 'tags', 'supplier': 'supplier', 'supplier_logo': 'supplier_logo'} ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/api/v1/actions.py0000664000175000017500000000600000000000000017530 0ustar00zuulzuul00000000000000# 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 oslo_log import log as logging from webob import exc from murano.common import policy from murano.common import wsgi from murano.db.services import environments as envs from murano.db.services import sessions from murano.db import session as db_session from murano.services import actions from murano.services import states from murano.utils import verify_env LOG = logging.getLogger(__name__) class Controller(object): @verify_env def execute(self, request, environment_id, action_id, body): policy.check("execute_action", request.context, {}) LOG.debug('Action:Execute '.format(action_id)) unit = db_session.get_session() # no new session can be opened if environment has deploying status env_status = envs.EnvironmentServices.get_status(environment_id) if env_status in (states.EnvironmentStatus.DEPLOYING, states.EnvironmentStatus.DELETING): LOG.warning('Could not open session for environment ' ', environment has deploying ' 'or deleting status.'.format(id=environment_id)) raise exc.HTTPForbidden() user_id = request.context.user session = sessions.SessionServices.create(environment_id, user_id) if not sessions.SessionServices.validate(session): LOG.error('Session ' 'is invalid'.format(id=session.id)) raise exc.HTTPForbidden() task_id = actions.ActionServices.execute( action_id, session, unit, request.context, body or {}) return {'task_id': task_id} @verify_env def get_result(self, request, environment_id, task_id): policy.check("execute_action", request.context, {}) LOG.debug('Action:GetResult '.format(id=task_id)) unit = db_session.get_session() result = actions.ActionServices.get_result(environment_id, task_id, unit) if result is not None: return result msg = ('Result for task with environment_id: {env_id} and task_id: ' '{task_id} was not found.'.format(env_id=environment_id, task_id=task_id)) LOG.error(msg) raise exc.HTTPNotFound(msg) def create_resource(): return wsgi.Resource(Controller()) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/api/v1/catalog.py0000664000175000017500000004470300000000000017516 0ustar00zuulzuul00000000000000# 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 cgi import os import tempfile import jsonschema from keystoneclient import exceptions as keystone_ex from keystoneclient import service_catalog from oslo_config import cfg from oslo_db import exception as db_exc from oslo_log import log as logging from webob import exc import murano.api.v1 from murano.api.v1 import validation_schemas from murano.common import exceptions from murano.common.i18n import _ from murano.common import policy import murano.common.utils as murano_utils from murano.common import wsgi from murano.db.catalog import api as db_api from murano.packages import exceptions as pkg_exc from murano.packages import load_utils from muranoclient.glance import client as glare_client LOG = logging.getLogger(__name__) CONF = cfg.CONF SUPPORTED_PARAMS = murano.api.v1.SUPPORTED_PARAMS LIST_PARAMS = murano.api.v1.LIST_PARAMS ORDER_VALUES = murano.api.v1.ORDER_VALUES PKG_PARAMS_MAP = murano.api.v1.PKG_PARAMS_MAP OPERATOR_VALUES = murano.api.v1.OPERATOR_VALUES def _check_content_type(req, content_type): try: req.get_content_type((content_type,)) except exceptions.UnsupportedContentType: msg = _("Content-Type must be '{type}'").format(type=content_type) LOG.error(msg) raise exc.HTTPBadRequest(explanation=msg) def _get_filters(query_params): filters = {} for param_pair in query_params: k, v = param_pair if k not in SUPPORTED_PARAMS: LOG.warning("Search by parameter '{name}' " "is not supported. Skipping it.".format(name=k)) continue if k in LIST_PARAMS: if v.startswith('in:') and k in OPERATOR_VALUES: in_value = v[len('in:'):] try: filters[k] = murano_utils.split_for_quotes(in_value) except ValueError as err: LOG.warning("Search by parameter '{name}' " "caused an {message} error." "Skipping it.".format(name=k, message=err)) else: filters.setdefault(k, []).append(v) else: filters[k] = v order_by = filters.get('order_by', []) for i in order_by[:]: if ORDER_VALUES and i not in ORDER_VALUES: filters['order_by'].remove(i) LOG.warning("Value of 'order_by' parameter is not valid. " "Allowed values are: {values}. Skipping it." .format(values=", ".join(ORDER_VALUES))) return filters def _validate_body(body): """Check multipart/form-data has two parts Check multipart/form-data has two parts: text (which is json string and should parsed into dictionary in serializer) and file, which stores as cgi.FieldStorage instance. Also validate file size doesn't exceed the limit: seek to the end of the file, get the position of EOF and reset the file position to the beginning """ def check_file_size(f): mb_limit = CONF.murano.package_size_limit pkg_size_limit = mb_limit * 1024 * 1024 f.seek(0, 2) size = f.tell() f.seek(0) if size > pkg_size_limit: raise exc.HTTPBadRequest(explanation=_( 'Uploading file is too large. ' 'The limit is {0} Mb').format(mb_limit)) if len(body) > 2: msg = _("'multipart/form-data' request body should contain 1 or 2 " "parts: json string and zip archive. Current body consists " "of {amount} part(s)").format(amount=len(body.keys())) LOG.error(msg) raise exc.HTTPBadRequest(explanation=msg) file_obj = None package_meta = None for part in body.values(): if isinstance(part, cgi.FieldStorage): file_obj = part check_file_size(file_obj.file) if isinstance(part, dict): package_meta = part if file_obj is None: msg = _('There is no file package with application description') LOG.error(msg) raise exc.HTTPBadRequest(explanation=msg) return file_obj, package_meta class Controller(object): """WSGI controller for application catalog resource in Murano v1 API.""" def _validate_limit(self, value): if value is None: return try: value = int(value) except ValueError: msg = _("Limit param must be an integer") LOG.error(msg) raise exc.HTTPBadRequest(explanation=msg) if value <= 0: msg = _("Limit param must be positive") LOG.error(msg) raise exc.HTTPBadRequest(explanation=msg) return value def update(self, req, body, package_id): """List of allowed changes List of allowed changes: { "op": "add", "path": "/tags", "value": [ "foo", "bar" ] } { "op": "add", "path": "/categories", "value": [ "foo", "bar" ] } { "op": "remove", "path": "/tags" } { "op": "remove", "path": "/categories" } { "op": "replace", "path": "/tags", "value": ["foo", "bar"] } { "op": "replace", "path": "/is_public", "value": true } { "op": "replace", "path": "/description", "value":"New description" } { "op": "replace", "path": "/name", "value": "New name" } """ policy.check("modify_package", req.context, {'package_id': package_id}) pkg_to_update = db_api.package_get(package_id, req.context) if pkg_to_update.is_public: policy.check("manage_public_package", req.context) _check_content_type(req, 'application/murano-packages-json-patch') if not isinstance(body, list): msg = _('Request body must be a JSON array of operation objects.') LOG.error(msg) raise exc.HTTPBadRequest(explanation=msg) for change in body: if 'is_public' in change['path']: if change['value'] is True and not pkg_to_update.is_public: policy.check('publicize_package', req.context) if 'name' in change['path']: if len(change['value']) > 80: msg = _('Package name should be 80 characters maximum') LOG.error(msg) raise exc.HTTPBadRequest(explanation=msg) package = db_api.package_update(package_id, body, req.context) return package.to_dict() def get(self, req, package_id): policy.check("get_package", req.context, {'package_id': package_id}) package = db_api.package_get(package_id, req.context) return package.to_dict() def search(self, req): policy.check("get_package", req.context) manage_public = True try: policy.check("manage_public_package", req.context) except exc.HTTPForbidden: manage_public = False filters = _get_filters(req.GET.items()) limit = self._validate_limit(filters.get('limit')) if limit is None: limit = CONF.murano.limit_param_default limit = min(CONF.murano.api_limit_max, limit) result = {} catalog = req.GET.pop('catalog', '').lower() == 'true' packages = db_api.package_search( filters, req.context, manage_public, limit, catalog=catalog) if len(packages) == limit: result['next_marker'] = packages[-1].id result['packages'] = [package.to_dict() for package in packages] return result def upload(self, req, body=None): """Upload new file archive Upload new file archive for the new package together with package metadata. """ policy.check("upload_package", req.context) _check_content_type(req, 'multipart/form-data') file_obj, package_meta = _validate_body(body) if package_meta: try: jsonschema.validate(package_meta, validation_schemas.PKG_UPLOAD_SCHEMA) except jsonschema.ValidationError as e: msg = _("Package schema is not valid: {reason}").format( reason=e) LOG.exception(msg) raise exc.HTTPBadRequest(explanation=msg) else: package_meta = {} if package_meta.get('is_public'): policy.check('publicize_package', req.context) with tempfile.NamedTemporaryFile(delete=False) as tempf: LOG.debug("Storing package archive in a temporary file") content = file_obj.file.read() if not content: msg = _("Uploading file can't be empty") LOG.error(msg) raise exc.HTTPBadRequest(explanation=msg) tempf.write(content) package_meta['archive'] = content try: with load_utils.load_from_file( tempf.name, target_dir=None, drop_dir=True) as pkg_to_upload: # extend dictionary for update db for k, v in PKG_PARAMS_MAP.items(): if hasattr(pkg_to_upload, k): if k == "tags" and package_meta.get(k): package_meta[v] = list(set( package_meta[v] + getattr(pkg_to_upload, k))) else: package_meta[v] = getattr(pkg_to_upload, k) if len(package_meta['name']) > 80: msg = _('Package name should be 80 characters maximum') LOG.error(msg) raise exc.HTTPBadRequest(explanation=msg) try: package = db_api.package_upload( package_meta, req.context.project_id) except db_exc.DBDuplicateEntry: msg = _('Package with specified full ' 'name is already registered') LOG.exception(msg) raise exc.HTTPConflict(msg) return package.to_dict() except pkg_exc.PackageLoadError as e: msg = _("Couldn't load package from file: {reason}").format( reason=e) LOG.exception(msg) raise exc.HTTPBadRequest(explanation=msg) finally: LOG.debug("Deleting package archive temporary file") os.remove(tempf.name) def get_ui(self, req, package_id): if CONF.engine.packages_service == 'murano': target = {'package_id': package_id} policy.check("get_package", req.context, target) package = db_api.package_get(package_id, req.context) return package.ui_definition else: g_client = self._get_glare_client(req) blob_data = g_client.artifacts.download_blob(package_id, 'archive') with tempfile.NamedTemporaryFile() as tempf: for chunk in blob_data: tempf.write(chunk) tempf.file.flush() os.fsync(tempf.file.fileno()) with load_utils.load_from_file(tempf.name, target_dir=None, drop_dir=True) as pkg: return pkg.ui def get_logo(self, req, package_id): target = {'package_id': package_id} policy.check("get_package", req.context, target) package = db_api.package_get(package_id, req.context) return package.logo def get_supplier_logo(self, req, package_id): package = db_api.package_get(package_id, req.context) return package.supplier_logo def download(self, req, package_id): target = {'package_id': package_id} policy.check("download_package", req.context, target) package = db_api.package_get(package_id, req.context) return package.archive def delete(self, req, package_id): target = {'package_id': package_id} policy.check("delete_package", req.context, target) package = db_api.package_get(package_id, req.context) if package.is_public: policy.check("manage_public_package", req.context, target) db_api.package_delete(package_id, req.context) def get_category(self, req, category_id): policy.check("get_category", req.context) category = db_api.category_get(category_id, packages=True) return category.to_dict() def list_categories(self, req): """List all categories List all categories with pagination and sorting Acceptable filter params: :param sort_keys: an array of fields used to sort the list :param sort_dir: the direction of the sort ('asc' or 'desc') :param limit: the number of categories to list :param marker: the ID of the last item in the previous page """ def _get_category_filters(req): query_params = {} valid_query_params = ['sort_keys', 'sort_dir', 'limit', 'marker'] for key, value in req.GET.items(): if key not in valid_query_params: raise exc.HTTPBadRequest( _('Bad value passed to filter. ' 'Got {key}, expected:{valid}').format( key=key, valid=', '.join(valid_query_params))) if key == 'sort_keys': available_sort_keys = ['name', 'created', 'updated', 'package_count', 'id'] value = [v.strip() for v in value.split(',')] for sort_key in value: if sort_key not in available_sort_keys: raise exc.HTTPBadRequest( explanation=_('Invalid sort key: {sort_key}. ' 'Must be one of the following: ' '{available}').format( sort_key=sort_key, available=', '.join(available_sort_keys))) if key == 'sort_dir': if value not in ['asc', 'desc']: msg = _('Invalid sort direction: {0}').format(value) raise exc.HTTPBadRequest(explanation=msg) query_params[key] = value return query_params policy.check("get_category", req.context) filters = _get_category_filters(req) marker = filters.get('marker') limit = self._validate_limit(filters.get('limit')) result = {} categories = db_api.categories_list(filters, limit=limit, marker=marker) if len(categories) == limit: result['next_marker'] = categories[-1].id result['categories'] = [category.to_dict() for category in categories] return result def add_category(self, req, body=None): policy.check("add_category", req.context) category_name = body.get('name') if not category_name: raise exc.HTTPBadRequest( explanation='Please, specify a name of the category to create') if len(category_name) > 80: msg = _('Category name should be 80 characters maximum') LOG.error(msg) raise exc.HTTPBadRequest(explanation=msg) try: category = db_api.category_add(category_name) except db_exc.DBDuplicateEntry: msg = _('Category with specified name is already exist') LOG.error(msg) raise exc.HTTPConflict(explanation=msg) return category.to_dict() def delete_category(self, req, category_id): target = {'category_id': category_id} policy.check("delete_category", req.context, target) category = db_api.category_get(category_id, packages=True) if category.packages: msg = _("It's impossible to delete categories assigned " "to the package, uploaded to the catalog") raise exc.HTTPForbidden(explanation=msg) db_api.category_delete(category_id) def _get_glare_client(self, request): glare_settings = CONF.glare token = request.context.auth_token url = glare_settings.url if not url: url = self._get_glare_url(request) # TODO(gyurco): use auth_utils.get_session_client_parameters client = glare_client.Client( endpoint=url, token=token, insecure=glare_settings.insecure, key_file=glare_settings.keyfile or None, ca_file=glare_settings.cafile or None, cert_file=glare_settings.certfile or None, type_name='murano', type_version=1) return client def _get_glare_url(self, request): sc = request.context.service_catalog token = request.context.auth_token try: return service_catalog.ServiceCatalogV2( {'serviceCatalog': sc}).url_for( service_type='artifact', endpoint_type=CONF.glare.endpoint_type, region_name=CONF.home_region) except keystone_ex.EndpointNotFound: return service_catalog.ServiceCatalogV3( token, {'catalog': sc}).url_for( service_type='artifact', endpoint_type=CONF.glare.endpoint_type, region_name=CONF.home_region) def create_resource(): specific_content_types = { 'get_ui': ['text/plain'], 'download': ['application/octet-stream'], 'get_logo': ['application/octet-stream'], 'get_supplier_logo': ['application/octet-stream']} deserializer = wsgi.RequestDeserializer( specific_content_types=specific_content_types) return wsgi.Resource(Controller(), deserializer=deserializer) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/api/v1/deployments.py0000664000175000017500000001334100000000000020441 0ustar00zuulzuul00000000000000# 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 oslo_log import log as logging from sqlalchemy import desc from sqlalchemy.orm import load_only from webob import exc from murano.api.v1 import request_statistics from murano.common.helpers import token_sanitizer from murano.common import policy from murano.common import utils from murano.common import wsgi from murano.db import models from murano.db import session as db_session from murano.utils import check_env LOG = logging.getLogger(__name__) API_NAME = 'Deployments' class Controller(object): @request_statistics.stats_count(API_NAME, 'Index') def index(self, request, environment_id=None): all_environments = environment_id is None LOG.debug('Deployments:List ' .format(all_environments)) if all_environments: policy.check("list_deployments_all_environments", request.context) else: check_env(request, environment_id) target = {"environment_id": environment_id} policy.check("list_deployments", request.context, target) unit = db_session.get_session() if all_environments: query = unit.query(models.Environment) \ .options(load_only('tenant_id')) \ .filter_by(tenant_id=request.context.project_id) \ .join(models.Task) \ .order_by(desc(models.Task.created)) result = query.all() # The join statement above fetches the deployments into # Environment.tasks. Iterate over that to get the deployments. deployments = [] for row in result: for deployment in row.tasks: deployment = set_dep_state(deployment, unit).to_dict() deployments.append(deployment) else: query = unit.query(models.Task) \ .filter_by(environment_id=environment_id) \ .order_by(desc(models.Task.created)) result = query.all() deployments = [set_dep_state(deployment, unit).to_dict() for deployment in result] return {'deployments': deployments} @request_statistics.stats_count(API_NAME, 'Statuses') def statuses(self, request, environment_id, deployment_id): target = {"environment_id": environment_id, "deployment_id": deployment_id} policy.check("statuses_deployments", request.context, target) unit = db_session.get_session() query = unit.query(models.Status) \ .filter_by(task_id=deployment_id) \ .order_by(models.Status.created) deployment = verify_and_get_deployment(unit, environment_id, deployment_id) if 'service_id' in request.GET: service_id_set = set(request.GET.getall('service_id')) environment = deployment.description entity_ids = [] for service in environment.get('services', []): if service['?']['id'] in service_id_set: id_map = utils.build_entity_map(service) entity_ids = entity_ids + id_map.keys() if entity_ids: query = query.filter(models.Status.entity_id.in_(entity_ids)) else: return {'reports': []} result = query.all() return {'reports': [status.to_dict() for status in result]} def _patch_description(description): if not description: description = {} description['services'] = description.pop('applications', []) return token_sanitizer.TokenSanitizer().sanitize(description) def verify_and_get_deployment(db_session, environment_id, deployment_id): deployment = db_session.query(models.Task).get(deployment_id) if not deployment: LOG.error('Deployment with id {id} not found' .format(id=deployment_id)) raise exc.HTTPNotFound if deployment.environment_id != environment_id: LOG.error('Deployment with id {d_id} not found in environment ' '{env_id}'.format(d_id=deployment_id, env_id=environment_id)) raise exc.HTTPBadRequest deployment.description = _patch_description(deployment.description) return deployment def create_resource(): return wsgi.Resource(Controller()) def set_dep_state(deployment, unit): num_errors = unit.query(models.Status).filter_by( level='error', task_id=deployment.id).count() num_warnings = unit.query(models.Status).filter_by( level='warning', task_id=deployment.id).count() if deployment.finished: if num_errors: deployment.state = 'completed_w_errors' elif num_warnings: deployment.state = 'completed_w_warnings' else: deployment.state = 'success' else: if num_errors: deployment.state = 'running_w_errors' elif num_warnings: deployment.state = 'running_w_warnings' else: deployment.state = 'running' deployment.description = _patch_description(deployment.description) return deployment ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/api/v1/environments.py0000664000175000017500000002541700000000000020634 0ustar00zuulzuul00000000000000# 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 datetime import jsonpatch from oslo_db import exception as db_exc from oslo_log import log as logging from sqlalchemy import desc from webob import exc from murano.api.v1 import request_statistics from murano.api.v1 import sessions from murano.common.i18n import _ from murano.common import policy from murano.common import utils from murano.common import wsgi from murano.db import models from murano.db.services import core_services from murano.db.services import environments as envs from murano.db.services import sessions as session_services from murano.db import session as db_session from murano.engine.system import status_reporter from murano.services import states from murano.utils import check_env from murano.utils import check_session from murano.utils import verify_env LOG = logging.getLogger(__name__) API_NAME = 'Environments' class Controller(object): def __init__(self, *args, **kwargs): super(Controller, self).__init__(*args, **kwargs) self._notifier = status_reporter.Notification() @request_statistics.stats_count(API_NAME, 'Index') def index(self, request): all_tenants = request.GET.get('all_tenants', 'false').lower() == 'true' tenant = request.GET.get('tenant', None) LOG.debug('Environments:List '.format(tenants=all_tenants, tenant=tenant)) if all_tenants: policy.check('list_environments_all_tenants', request.context) filters = {} elif tenant: policy.check('list_environments_all_tenants', request.context) filters = {'tenant_id': tenant} else: policy.check('list_environments', request.context) # Only environments from same tenant as user should be returned filters = {'tenant_id': request.context.project_id} environments = envs.EnvironmentServices.get_environments_by(filters) environments = [env.to_dict() for env in environments] return {"environments": environments} @request_statistics.stats_count(API_NAME, 'Create') def create(self, request, body): LOG.debug('Environments:Create '.format(body=body)) policy.check('create_environment', request.context) if not('name' in body and body['name'].strip()): msg = _('Please, specify a name of the environment to create') LOG.error(msg) raise exc.HTTPBadRequest(explanation=msg) name = str(body['name']) if len(name) > 255: msg = _('Environment name should be 255 characters maximum') LOG.error(msg) raise exc.HTTPBadRequest(explanation=msg) try: environment = envs.EnvironmentServices.create( body.copy(), request.context) except db_exc.DBDuplicateEntry: msg = _('Environment with specified name already exists') LOG.error(msg) raise exc.HTTPConflict(explanation=msg) return environment.to_dict() @request_statistics.stats_count(API_NAME, 'Show') @verify_env def show(self, request, environment_id): LOG.debug('Environments:Show '.format(id=environment_id)) target = {"environment_id": environment_id} policy.check('show_environment', request.context, target) session = db_session.get_session() environment = session.query(models.Environment).get(environment_id) env = environment.to_dict() env['status'] = envs.EnvironmentServices.get_status(env['id']) # if env is currently being deployed we can provide information about # the session right away env['acquired_by'] = None if env['status'] == states.EnvironmentStatus.DEPLOYING: session_list = session_services.SessionServices.get_sessions( environment_id, state=states.SessionState.DEPLOYING) if session_list: env['acquired_by'] = session_list[0].id session_id = None if hasattr(request, 'context') and request.context.session: session_id = request.context.session if session_id: env_session = session.query(models.Session).get(session_id) check_session(request, environment_id, env_session, session_id) # add services to env get_data = core_services.CoreServices.get_data env['services'] = get_data(environment_id, '/services', session_id) return env @request_statistics.stats_count(API_NAME, 'Update') @verify_env def update(self, request, environment_id, body): """"Rename an environment.""" LOG.debug('Environments:Update '.format(id=environment_id, body=body)) target = {"environment_id": environment_id} policy.check('update_environment', request.context, target) session = db_session.get_session() environment = session.query(models.Environment).get(environment_id) new_name = str(body['name']) if new_name.strip(): if len(new_name) > 255: msg = _('Environment name should be 255 characters maximum') LOG.error(msg) raise exc.HTTPBadRequest(explanation=msg) try: environment.update({'name': new_name}) environment.save(session) except db_exc.DBDuplicateEntry: msg = _('Environment with specified name already exists') LOG.error(msg) raise exc.HTTPConflict(explanation=msg) else: msg = _('Environment name must contain at least one ' 'non-white space symbol') LOG.error(msg) raise exc.HTTPClientError(explanation=msg) return environment.to_dict() @request_statistics.stats_count(API_NAME, 'Delete') def delete(self, request, environment_id): target = {"environment_id": environment_id} policy.check('delete_environment', request.context, target) environment = check_env(request, environment_id) if request.GET.get('abandon', '').lower() == 'true': LOG.debug( 'Environments:Abandon '.format(id=environment_id)) envs.EnvironmentServices.remove(environment_id) else: LOG.debug( 'Environments:Delete '.format(id=environment_id)) sessions_controller = sessions.Controller() session = sessions_controller.configure( request, environment_id) session_id = session['id'] envs.EnvironmentServices.delete(environment_id, session_id) sessions_controller.deploy(request, environment_id, session_id) env = environment.to_dict() env['deleted'] = datetime.datetime.utcnow() self._notifier.report('environment.delete.end', env) @request_statistics.stats_count(API_NAME, 'LastStatus') @verify_env def last(self, request, environment_id): session_id = None if hasattr(request, 'context') and request.context.session: session_id = request.context.session services = core_services.CoreServices.get_data(environment_id, '/services', session_id) session = db_session.get_session() result = {} for service in services or []: service_id = service['?']['id'] entity_ids = utils.build_entity_map(service).keys() last_status = session.query(models.Status). \ filter(models.Status.entity_id.in_(entity_ids)). \ order_by(desc(models.Status.created)). \ first() if last_status: result[service_id] = last_status.to_dict() else: result[service_id] = None return {'lastStatuses': result} @request_statistics.stats_count(API_NAME, 'GetModel') @verify_env def get_model(self, request, environment_id, path): LOG.debug('Environments:GetModel , Path: %(path)s', {'env_id': environment_id, 'path': path}) target = {"environment_id": environment_id} policy.check('show_environment', request.context, target) session_id = None if hasattr(request, 'context') and request.context.session: session_id = request.context.session get_description = envs.EnvironmentServices.get_environment_description env_model = get_description(environment_id, session_id) try: result = utils.TraverseHelper.get(path, env_model) except (KeyError, ValueError): raise exc.HTTPNotFound return result @request_statistics.stats_count(API_NAME, 'UpdateModel') @verify_env def update_model(self, request, environment_id, body=None): if not body: msg = _('Request body is empty: please, provide ' 'environment object model patch') LOG.error(msg) raise exc.HTTPBadRequest(msg) LOG.debug('Environments:UpdateModel ', {'env_id': environment_id, 'body': body}) target = {"environment_id": environment_id} policy.check('update_environment', request.context, target) session_id = None if hasattr(request, 'context') and request.context.session: session_id = request.context.session get_description = envs.EnvironmentServices.get_environment_description env_model = get_description(environment_id, session_id) for change in body: change['path'] = '/' + '/'.join(change['path']) patch = jsonpatch.JsonPatch(body) try: patch.apply(env_model, in_place=True) except jsonpatch.JsonPatchException as e: raise exc.HTTPNotFound(str(e)) save_description = envs.EnvironmentServices. \ save_environment_description save_description(session_id, env_model) return env_model def create_resource(): return wsgi.Resource(Controller()) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/api/v1/instance_statistics.py0000664000175000017500000000475100000000000022161 0ustar00zuulzuul00000000000000# 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 oslo_log import log as logging from murano.api.v1 import request_statistics from murano.common import policy from murano.common import wsgi from murano.db.services import instances LOG = logging.getLogger(__name__) API_NAME = 'EnvironmentStatistics' class Controller(object): @request_statistics.stats_count(API_NAME, 'GetAggregated') def get_aggregated(self, request, environment_id): LOG.debug('EnvironmentStatistics:GetAggregated') target = {"environment_id": environment_id} policy.check("get_aggregated_statistics", request.context, target) # TODO(stanlagun): Check that caller is authorized to access # tenant's statistics return instances.InstanceStatsServices.get_aggregated_stats( environment_id) @request_statistics.stats_count(API_NAME, 'GetForInstance') def get_for_instance(self, request, environment_id, instance_id): LOG.debug('EnvironmentStatistics:GetForInstance') target = {"environment_id": environment_id, "instance_id": instance_id} policy.check("get_instance_statistics", request.context, target) # TODO(stanlagun): Check that caller is authorized to access # tenant's statistics return instances.InstanceStatsServices.get_raw_environment_stats( environment_id, instance_id) @request_statistics.stats_count(API_NAME, 'GetForEnvironment') def get_for_environment(self, request, environment_id): LOG.debug('EnvironmentStatistics:GetForEnvironment') target = {"environment_id": environment_id} policy.check("get_statistics", request.context, target) # TODO(stanlagun): Check that caller is authorized to access # tenant's statistics return instances.InstanceStatsServices.get_raw_environment_stats( environment_id) def create_resource(): return wsgi.Resource(Controller()) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/api/v1/request_statistics.py0000664000175000017500000000656000000000000022045 0ustar00zuulzuul00000000000000# 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 time from oslo_log import log as logging from murano.api import v1 from murano.common import wsgi from murano.db.services import stats LOG = logging.getLogger(__name__) class RequestStatisticsCollection(object): request_count = 0 error_count = 0 average_time = 0.0 requests_per_tenant = {} errors_per_tenant = {} def add_api_request(self, tenant, ex_time): self.average_time = (self.average_time * self.request_count + ex_time) / (self.request_count + 1) if tenant: tenant_count = self.requests_per_tenant.get(tenant, 0) # nosec tenant_count += 1 self.requests_per_tenant[tenant] = tenant_count def add_api_error(self, tenant, ex_time): self.average_time = (self.average_time * self.request_count + ex_time) / (self.request_count + 1) if tenant: tenant_count = self.errors_per_tenant.get(tenant, 0) tenant_count += 1 self.errors_per_tenant[tenant] = tenant_count def stats_count(api, method): def wrapper(func): def wrap(*args, **kwargs): try: ts = time.time() result = func(*args, **kwargs) te = time.time() tenant = args[1].context.project_id update_count(api, method, te - ts, tenant) return result except Exception: te = time.time() tenant = args[1].context.project_id LOG.exception('API {api} method {method} raised an ' 'exception'.format(api=api, method=method)) update_error_count(api, method, te - te, tenant) raise return wrap return wrapper def update_count(api, method, ex_time, tenant=None): LOG.debug("Updating count stats for {api}, {method} on object {object}" .format(api=api, method=method, object=v1.stats)) v1.stats.add_api_request(tenant, ex_time) v1.stats.request_count += 1 def update_error_count(api, method, ex_time, tenant=None): LOG.debug("Updating count stats for {api}, {method} on object " "{object}".format(api=api, method=method, object=v1.stats)) v1.stats.add_api_error(tenant, ex_time) v1.stats.error_count += 1 v1.stats.request_count += 1 def init_stats(): if not v1.stats: v1.stats = RequestStatisticsCollection() class Controller(object): def get(self, request): model = stats.Statistics() entries = model.get_all() ent_list = [] for entry in entries: ent_list.append(entry.to_dict()) return ent_list def create_resource(): return wsgi.Resource(Controller()) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/api/v1/router.py0000664000175000017500000003406300000000000017422 0ustar00zuulzuul00000000000000# 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 routes from murano.api.v1 import actions from murano.api.v1 import catalog from murano.api.v1 import deployments from murano.api.v1 import environments from murano.api.v1 import instance_statistics from murano.api.v1 import request_statistics from murano.api.v1 import schemas from murano.api.v1 import services from murano.api.v1 import sessions from murano.api.v1 import static_actions from murano.api.v1 import template_applications from murano.api.v1 import templates from murano.common import wsgi class API(wsgi.Router): @classmethod def factory(cls, global_conf, **local_conf): return cls(routes.Mapper()) def __init__(self, mapper): services_resource = services.create_resource() mapper.connect('/environments/{environment_id}/services', controller=services_resource, action='get', conditions={'method': ['GET']}, path='') mapper.connect('/environments/{environment_id}/services/{path:.*?}', controller=services_resource, action='get', conditions={'method': ['GET']}, path='') mapper.connect('/environments/{environment_id}/services', controller=services_resource, action='post', conditions={'method': ['POST']}, path='') mapper.connect('/environments/{environment_id}/services/{path:.*?}', controller=services_resource, action='post', conditions={'method': ['POST']}, path='') mapper.connect('/environments/{environment_id}/services', controller=services_resource, action='put', conditions={'method': ['PUT']}, path='') mapper.connect('/environments/{environment_id}/services/{path:.*?}', controller=services_resource, action='put', conditions={'method': ['PUT']}, path='') mapper.connect('/environments/{environment_id}/services', controller=services_resource, action='delete', conditions={'method': ['DELETE']}, path='') mapper.connect('/environments/{environment_id}/services/{path:.*?}', controller=services_resource, action='delete', conditions={'method': ['DELETE']}, path='') environments_resource = environments.create_resource() mapper.connect('/environments', controller=environments_resource, action='index', conditions={'method': ['GET']}) mapper.connect('/environments', controller=environments_resource, action='create', conditions={'method': ['POST']}) mapper.connect('/environments/{environment_id}', controller=environments_resource, action='update', conditions={'method': ['PUT']}) mapper.connect('/environments/{environment_id}', controller=environments_resource, action='show', conditions={'method': ['GET']}) mapper.connect('/environments/{environment_id}', controller=environments_resource, action='delete', conditions={'method': ['DELETE']}) mapper.connect('/environments/{environment_id}/lastStatus', controller=environments_resource, action='last', conditions={'method': ['GET']}) mapper.connect('/environments/{environment_id}/model/{path:.*?}', controller=environments_resource, action='get_model', conditions={'method': ['GET']}) mapper.connect('/environments/{environment_id}/model/', controller=environments_resource, action='update_model', conditions={'method': ['PATCH']}) templates_resource = templates.create_resource() mapper.connect('/templates', controller=templates_resource, action='index', conditions={'method': ['GET']}) mapper.connect('/templates', controller=templates_resource, action='create', conditions={'method': ['POST']}) mapper.connect('/templates/{env_template_id}', controller=templates_resource, action='update', conditions={'method': ['PUT']}) mapper.connect('/templates/{env_template_id}', controller=templates_resource, action='show', conditions={'method': ['GET']}) mapper.connect('/templates/{env_template_id}', controller=templates_resource, action='delete', conditions={'method': ['DELETE']}) mapper.connect('/templates/{env_template_id}/create-environment', controller=templates_resource, action='create_environment', conditions={'method': ['POST']}) mapper.connect('/templates/{env_template_id}/clone', controller=templates_resource, action='clone', conditions={'method': ['POST']}) applications_resource = template_applications.create_resource() mapper.connect('/templates/{env_template_id}/services', controller=applications_resource, action='index', conditions={'method': ['GET']}, path='') mapper.connect('/templates/{env_template_id}/services/{path:.*?}', controller=applications_resource, action='show', conditions={'method': ['GET']}, path='') mapper.connect('/templates/{env_template_id}/services', controller=applications_resource, action='post', conditions={'method': ['POST']}, path='') mapper.connect('/templates/{env_template_id}/services/{path:.*?}', controller=applications_resource, action='put', conditions={'method': ['PUT']}, path='') mapper.connect('/templates/{env_template_id}/services/{path:.*?}', controller=applications_resource, action='delete', conditions={'method': ['DELETE']}, path='') deployments_resource = deployments.create_resource() mapper.connect('/environments/{environment_id}/deployments', controller=deployments_resource, action='index', conditions={'method': ['GET']}) mapper.connect('/environments/{environment_id}/deployments/' '{deployment_id}', controller=deployments_resource, action='statuses', conditions={'method': ['GET']}) mapper.connect('/deployments', controller=deployments_resource, action='index', conditions={'method': ['GET']}) sessions_resource = sessions.create_resource() mapper.connect('/environments/{environment_id}/configure', controller=sessions_resource, action='configure', conditions={'method': ['POST']}) mapper.connect('/environments/{environment_id}/sessions/{session_id}', controller=sessions_resource, action='show', conditions={'method': ['GET']}) mapper.connect('/environments/{environment_id}/sessions/{session_id}', controller=sessions_resource, action='delete', conditions={'method': ['DELETE']}) mapper.connect('/environments/{environment_id}/sessions/' '{session_id}/deploy', controller=sessions_resource, action='deploy', conditions={'method': ['POST']}) statistics_resource = instance_statistics.create_resource() mapper.connect( '/environments/{environment_id}/instance-statistics/raw/' '{instance_id}', controller=statistics_resource, action='get_for_instance', conditions={'method': ['GET']}) mapper.connect( '/environments/{environment_id}/instance-statistics/raw', controller=statistics_resource, action='get_for_environment', conditions={'method': ['GET']}) mapper.connect( '/environments/{environment_id}/instance-statistics/aggregated', controller=statistics_resource, action='get_aggregated', conditions={'method': ['GET']}) actions_resource = actions.create_resource() mapper.connect('/environments/{environment_id}/actions/{action_id}', controller=actions_resource, action='execute', conditions={'method': ['POST']}) mapper.connect('/environments/{environment_id}/actions/{task_id}', controller=actions_resource, action='get_result', conditions={'method': ['GET']}) static_actions_resource = static_actions.create_resource() mapper.connect('/actions', controller=static_actions_resource, action='execute', conditions={'method': ['POST']}) catalog_resource = catalog.create_resource() mapper.connect('/catalog/packages/{package_id}', controller=catalog_resource, action='get', conditions={'method': ['GET']}) mapper.connect('/catalog/packages/{package_id}', controller=catalog_resource, action='delete', conditions={'method': ['DELETE']}) mapper.connect('/catalog/packages/{package_id}', controller=catalog_resource, action='update', conditions={'method': ['PATCH']}) mapper.connect('/catalog/packages', controller=catalog_resource, action='search', conditions={'method': ['GET']}) mapper.connect('/catalog/packages', controller=catalog_resource, action='upload', conditions={'method': ['POST']}) mapper.connect('/catalog/packages/{package_id}/ui', controller=catalog_resource, action='get_ui', conditions={'method': ['GET']}) mapper.connect('/catalog/packages/{package_id}/logo', controller=catalog_resource, action='get_logo', conditions={'method': ['GET']}) mapper.connect('/catalog/packages/{package_id}/supplier_logo', controller=catalog_resource, action='get_supplier_logo', conditions={'method': ['GET']}) mapper.connect('/catalog/packages/{package_id}/download', controller=catalog_resource, action='download', conditions={'method': ['GET']}) mapper.connect('/catalog/categories', controller=catalog_resource, action='list_categories', conditions={'method': ['GET']}) mapper.connect('/catalog/categories/{category_id}', controller=catalog_resource, action='get_category', conditions={'method': ['GET']}) mapper.connect('/catalog/categories', controller=catalog_resource, action='add_category', conditions={'method': ['POST']}) mapper.connect('/catalog/categories/{category_id}', controller=catalog_resource, action='delete_category', conditions={'method': ['DELETE']}) req_stats_resource = request_statistics.create_resource() mapper.connect('/stats', controller=req_stats_resource, action='get', conditions={'method': ['GET']}) schemas_resource = schemas.create_resource() mapper.connect('/schemas/{class_name}/{method_names}', controller=schemas_resource, action='get_schema', conditions={'method': ['GET']}) mapper.connect('/schemas/{class_name}', controller=schemas_resource, action='get_schema', conditions={'method': ['GET']}) super(API, self).__init__(mapper) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/api/v1/schemas.py0000664000175000017500000000401100000000000017513 0ustar00zuulzuul00000000000000# 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 oslo_log import log as logging from oslo_messaging.rpc import client from webob import exc from murano.api.v1 import request_statistics from murano.common import policy from murano.common import rpc from murano.common import wsgi LOG = logging.getLogger(__name__) API_NAME = 'Schemas' class Controller(object): @request_statistics.stats_count(API_NAME, 'GetSchema') def get_schema(self, request, class_name, method_names=None): LOG.debug('GetSchema:GetSchema') target = {"class_name": class_name} policy.check("get_schema", request.context, target) class_version = request.GET.get('classVersion') package_name = request.GET.get('packageName') credentials = { 'token': request.context.auth_token, 'project_id': request.context.project_id } try: methods = (list( map(str.strip, method_names.split(','))) if method_names else []) return rpc.engine().generate_schema( credentials, class_name, methods, class_version, package_name) except client.RemoteError as e: if e.exc_type in ('NoClassFound', 'NoPackageForClassFound', 'NoPackageFound'): raise exc.HTTPNotFound(e.value) raise def create_resource(): return wsgi.Resource(Controller()) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/api/v1/services.py0000664000175000017500000001027400000000000017723 0ustar00zuulzuul00000000000000# 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 functools as func from oslo_log import log as logging from webob import exc from murano.api.v1 import request_statistics from murano.common.helpers import token_sanitizer from murano.common.i18n import _ from murano.common import wsgi from murano.db.services import core_services from murano import utils LOG = logging.getLogger(__name__) API_NAME = 'Services' def normalize_path(f): @func.wraps(f) def f_normalize_path(*args, **kwargs): if 'path' in kwargs: if kwargs['path']: kwargs['path'] = '/services/' + kwargs['path'] else: kwargs['path'] = '/services' return f(*args, **kwargs) return f_normalize_path class Controller(object): @request_statistics.stats_count(API_NAME, 'Index') @utils.verify_env @normalize_path def get(self, request, environment_id, path): LOG.debug('Services:Get '.format(env_id=environment_id, path=path)) session_id = None if hasattr(request, 'context') and request.context.session: session_id = request.context.session try: result = core_services.CoreServices.get_data(environment_id, path, session_id) except (KeyError, ValueError, AttributeError): raise exc.HTTPNotFound return result @request_statistics.stats_count(API_NAME, 'Create') @utils.verify_session @utils.verify_env @normalize_path def post(self, request, environment_id, path, body=None): if not body: msg = _('Request body is empty: please, provide ' 'application object model') LOG.error(msg) raise exc.HTTPBadRequest(msg) secure_data = token_sanitizer.TokenSanitizer().sanitize(body) LOG.debug('Services:Post '.format(env_id=environment_id, body=secure_data, path=path)) post_data = core_services.CoreServices.post_data session_id = request.context.session try: result = post_data(environment_id, session_id, body, path) except (KeyError, ValueError): raise exc.HTTPNotFound return result @request_statistics.stats_count(API_NAME, 'Update') @utils.verify_session @utils.verify_env @normalize_path def put(self, request, environment_id, path, body=None): if not body: body = [] LOG.debug('Services:Put '.format(environment_id, body, path)) put_data = core_services.CoreServices.put_data session_id = request.context.session try: result = put_data(environment_id, session_id, body, path) except (KeyError, ValueError): raise exc.HTTPNotFound return result @request_statistics.stats_count(API_NAME, 'Delete') @utils.verify_session @utils.verify_env @normalize_path def delete(self, request, environment_id, path): LOG.debug('Services:Delete '.format(environment_id, path)) delete_data = core_services.CoreServices.delete_data session_id = request.context.session try: delete_data(environment_id, session_id, path) except (KeyError, ValueError): raise exc.HTTPNotFound def create_resource(): return wsgi.Resource(Controller()) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/api/v1/sessions.py0000664000175000017500000001266200000000000017751 0ustar00zuulzuul00000000000000# 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 oslo_log import log as logging from webob import exc from murano.api.v1 import request_statistics from murano.common.i18n import _ from murano.common import wsgi from murano.db import models from murano.db.services import environments as envs from murano.db.services import sessions from murano.db import session as db_session from murano.services import states from murano.utils import check_env from murano.utils import check_session LOG = logging.getLogger(__name__) API_NAME = 'Sessions' class Controller(object): @request_statistics.stats_count(API_NAME, 'Create') def configure(self, request, environment_id): LOG.debug('Session:Configure ' .format(env_id=environment_id)) check_env(request, environment_id) # No new session can be opened if environment has deploying or # deleting status. env_status = envs.EnvironmentServices.get_status(environment_id) if env_status in (states.EnvironmentStatus.DEPLOYING, states.EnvironmentStatus.DELETING): msg = _('Could not open session for environment , environment has deploying or ' 'deleting status.').format(env_id=environment_id) LOG.error(msg) raise exc.HTTPForbidden(explanation=msg) user_id = request.context.user session = sessions.SessionServices.create(environment_id, user_id) return session.to_dict() @request_statistics.stats_count(API_NAME, 'Index') def show(self, request, environment_id, session_id): LOG.debug('Session:Show '.format(id=session_id)) unit = db_session.get_session() session = unit.query(models.Session).get(session_id) check_session(request, environment_id, session, session_id) user_id = request.context.user if session.user_id != user_id: msg = _('User is not authorized to access ' 'session .').format(usr_id=user_id, s_id=session_id) LOG.error(msg) raise exc.HTTPUnauthorized(explanation=msg) if not sessions.SessionServices.validate(session): msg = _('Session is invalid: environment has been ' 'updated or updating right now with other session' ).format(session_id) LOG.error(msg) raise exc.HTTPForbidden(explanation=msg) return session.to_dict() @request_statistics.stats_count(API_NAME, 'Delete') def delete(self, request, environment_id, session_id): LOG.debug('Session:Delete '.format(s_id=session_id)) unit = db_session.get_session() session = unit.query(models.Session).get(session_id) check_session(request, environment_id, session, session_id) user_id = request.context.user if session.user_id != user_id: msg = _('User is not authorized to access ' 'session .').format(usr_id=user_id, s_id=session_id) LOG.error(msg) raise exc.HTTPUnauthorized(explanation=msg) if session.state == states.SessionState.DEPLOYING: msg = _('Session is in deploying state ' 'and could not be deleted').format(s_id=session_id) LOG.error(msg) raise exc.HTTPForbidden(explanation=msg) with unit.begin(): unit.delete(session) return None @request_statistics.stats_count(API_NAME, 'Deploy') def deploy(self, request, environment_id, session_id): LOG.debug('Session:Deploy '.format(s_id=session_id)) unit = db_session.get_session() session = unit.query(models.Session).get(session_id) check_session(request, environment_id, session, session_id) if not sessions.SessionServices.validate(session): msg = _('Session is invalid: environment has been ' 'updated or updating right now with other session' ).format(session_id) LOG.error(msg) raise exc.HTTPForbidden(explanation=msg) if session.state != states.SessionState.OPENED: msg = _('Session is already deployed or ' 'deployment is in progress').format(s_id=session_id) LOG.error(msg) raise exc.HTTPForbidden(explanation=msg) envs.EnvironmentServices.deploy(session, unit, request.context) def create_resource(): return wsgi.Resource(Controller()) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/api/v1/static_actions.py0000664000175000017500000000553200000000000021110 0ustar00zuulzuul00000000000000# 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 oslo_log import log as logging from oslo_messaging.rpc import client from webob import exc from murano.common.i18n import _ from murano.common import policy from murano.common import wsgi from murano.services import static_actions LOG = logging.getLogger(__name__) class Controller(object): def execute(self, request, body): policy.check("execute_action", request.context, {}) class_name = body.get('className') method_name = body.get('methodName') if not class_name or not method_name: msg = _('Class name and method name must be specified for ' 'static action') LOG.error(msg) raise exc.HTTPBadRequest(msg) args = body.get('parameters') pkg_name = body.get('packageName') class_version = body.get('classVersion', '=0') LOG.debug('StaticAction:Execute '.format(method_name, class_name)) credentials = { 'token': request.context.auth_token, 'project_id': request.context.project_id, 'user_id': request.context.user } try: return static_actions.StaticActionServices.execute( method_name, class_name, pkg_name, class_version, args, credentials) except client.RemoteError as e: LOG.error('Exception during call of the method {method_name}: ' '{exc}'.format(method_name=method_name, exc=str(e))) if e.exc_type in ( 'NoClassFound', 'NoMethodFound', 'NoPackageFound', 'NoPackageForClassFound', 'MethodNotExposed', 'NoMatchingMethodException'): raise exc.HTTPNotFound(e.value) elif e.exc_type == 'ContractViolationException': raise exc.HTTPBadRequest(e.value) raise exc.HTTPServiceUnavailable(e.value) except ValueError as e: LOG.error('Exception during call of the method {method_name}: ' '{exc}'.format(method_name=method_name, exc=str(e))) raise exc.HTTPBadRequest(str(e)) def create_resource(): return wsgi.Resource(Controller()) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/api/v1/template_applications.py0000664000175000017500000001560300000000000022462 0ustar00zuulzuul00000000000000 # Copyright (c) 2015 Telefonica I+D. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # 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 as func from oslo_log import log as logging from webob import exc from murano.api.v1 import request_statistics from murano.common.helpers import token_sanitizer from murano.common.i18n import _ from murano.common import policy from murano.common import wsgi from murano.db.services import core_services from murano import utils LOG = logging.getLogger(__name__) API_NAME = 'Services' def normalize_path(f): """Normalizes request path It normalizes the path obtaining in the requests. It is used in all the operations in the controller """ @func.wraps(f) def f_normalize_path(*args, **kwargs): if 'path' in kwargs: if kwargs['path']: kwargs['path'] = '/services/' + kwargs['path'] else: kwargs['path'] = '/services' return f(*args, **kwargs) return f_normalize_path class Controller(object): @request_statistics.stats_count(API_NAME, 'Index') @utils.verify_env_template @normalize_path def index(self, request, env_template_id, path): """Obtains services/applications for a template It obtains all the services/applications associated to a template :param request: The operation request. :param env_template_id: The environment template id with contains the services :param path: The operation path """ LOG.debug('Applications:Get '.format(templ_id=env_template_id, path=path)) try: get_data = core_services.CoreServices.get_template_data result = get_data(env_template_id, path) except (KeyError, ValueError, AttributeError): msg = _('The environment template {templ_id} does not ' 'exist').format(templ_id=env_template_id) LOG.exception(msg) raise exc.HTTPNotFound(msg) return result @request_statistics.stats_count(API_NAME, 'Show') @utils.verify_env_template @normalize_path def show(self, request, env_template_id, path): """It shows the service description :param request: The operation request. :param env_template_id: the env template ID where the service belongs to. :param path: The path include the service id :return: the service description. """ LOG.debug('Applications:Get '.format(templ_id=env_template_id, path=path)) try: get_data = core_services.CoreServices.get_template_data result = get_data(env_template_id, path) except (KeyError, ValueError, AttributeError): msg = _('The template does not exist {templ_id}').format( templ_id=env_template_id) LOG.exception(msg) raise exc.HTTPNotFound(msg) return result @request_statistics.stats_count(API_NAME, 'Create') @utils.verify_env_template @normalize_path def post(self, request, env_template_id, path, body): """It adds a service into a template :param request: The operation request. :param env_template_id: the env template ID where the service belongs to. :param path: The path :param body: the information about the service :return: the service description. """ secure_data = token_sanitizer.TokenSanitizer().sanitize(body) LOG.debug('Applications:Post '.format(env_id=env_template_id, body=secure_data, path=path)) post_data = core_services.CoreServices.post_application_data try: result = post_data(env_template_id, body, path) except (KeyError, ValueError): msg = _('The template does not exist {templ_id}').format( templ_id=env_template_id) LOG.exception(msg) raise exc.HTTPNotFound(msg) return result @request_statistics.stats_count(API_NAME, 'Update') @utils.verify_env_template @normalize_path def put(self, request, env_template_id, path, body): """It updates a service into a template. :param request: The operation request. :param env_template_id: the env template ID where the service belongs to. :param path: The path :param body: the information about the service :return: the service description updated. """ policy.check('update_service_env_template', request.context) LOG.debug('Applications:Put '.format(templ_id=env_template_id, body=body, path=path)) put_data = core_services.CoreServices.put_application_data try: result = put_data(env_template_id, body, path) except (KeyError, ValueError): msg = _('The template does not exist {templ_id}').format( templ_id=env_template_id) LOG.exception(msg) raise exc.HTTPNotFound(msg) return result @request_statistics.stats_count(API_NAME, 'Delete') @utils.verify_env_template @normalize_path def delete(self, request, env_template_id, path): """It deletes a service into a template. :param request: The operation request. :param env_template_id: the env template ID where the service belongs to. :param path: The path contains the service id """ LOG.debug('Applications:Put '.format(templ_id=env_template_id, path=path)) delete_data = core_services.CoreServices.delete_env_template_data try: result = delete_data(env_template_id, path) except (KeyError, ValueError): msg = _('The template does not exist {templ_id}').format( templ_id=env_template_id) LOG.exception(msg) raise exc.HTTPNotFound(msg) return result def create_resource(): return wsgi.Resource(Controller()) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/api/v1/templates.py0000664000175000017500000002643300000000000020102 0ustar00zuulzuul00000000000000# Copyright (c) 2015, Telefonica I+D. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # 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 oslo_db import exception as db_exc from oslo_log import log as logging from webob import exc from murano.api.v1 import request_statistics from murano.common.i18n import _ from murano.common import policy from murano.common import utils from murano.common import wsgi from murano.db import models from murano.db.services import core_services from murano.db.services import environment_templates as env_temps from murano.db.services import environments as envs from murano.db.services import sessions LOG = logging.getLogger(__name__) API_NAME = 'Templates' class Controller(object): @request_statistics.stats_count(API_NAME, 'Index') def index(self, request): """Lists the env templates associated to an tenant-id It lists the env templates associated to an tenant-id. :param request: The operation request. :return: the env template description list. """ LOG.debug('EnvTemplates:List') policy.check('list_env_templates', request.context) tenant_id = request.context.project_id filters = {} if request.GET.get('is_public'): is_public = request.GET.get('is_public', 'false').lower() == 'true' if not is_public: filters['is_public'] = False filters['tenant_id'] = tenant_id elif is_public: filters['is_public'] = True list_templates = env_temps.EnvTemplateServices.\ get_env_templates_by(filters) else: filters = (models.EnvironmentTemplate.is_public, models.EnvironmentTemplate.tenant_id == tenant_id) list_templates = env_temps.EnvTemplateServices.\ get_env_templates_or_by(filters) list_templates = [temp.to_dict() for temp in list_templates] return {"templates": list_templates} @request_statistics.stats_count(API_NAME, 'Create') def create(self, request, body): """Creates the env template from the payload This payload can contain just the template name, or include also service information. :param request: the operation request. :param body: the env template description :return: the description of the created template. """ LOG.debug('EnvTemplates:Create '.format(body=body)) policy.check('create_env_template', request.context) self._validate_body_name(body) try: LOG.debug('ENV TEMP NAME: {templ_name}>'. format(templ_name=body['name'])) template = env_temps.EnvTemplateServices.create( body.copy(), request.context.project_id) return template.to_dict() except db_exc.DBDuplicateEntry: msg = _('Env Template with specified name already exists') LOG.exception(msg) raise exc.HTTPConflict(msg) @request_statistics.stats_count(API_NAME, 'Show') def show(self, request, env_template_id): """It shows the description of a template :param request: the operation request. :param env_template_id: the env template ID. :return: the description of the env template. """ LOG.debug('Templates:Show '.format( templ_id=env_template_id)) target = {"env_template_id": env_template_id} policy.check('show_env_template', request.context, target) self._validate_request(request, env_template_id) template = env_temps.EnvTemplateServices.\ get_env_template(env_template_id) temp = template.to_dict() get_data = core_services.CoreServices.get_template_data temp['services'] = get_data(env_template_id, '/services') return temp @request_statistics.stats_count(API_NAME, 'Update') def update(self, request, env_template_id, body): """It updates the description template :param request: the operation request. :param env_template_id: the env template ID. :param body: the description to be updated :return: the updated template description. """ LOG.debug('Templates:Update '.format(templ_id=env_template_id, body=body)) target = {"env_template_id": env_template_id} policy.check('update_env_template', request.context, target) self._validate_request(request, env_template_id) try: LOG.debug('ENV TEMP NAME: {temp_name}>'.format( temp_name=body['name'])) if not str(body['name']).strip(): msg = _('Environment Template must contain at least one ' 'non-white space symbol') LOG.exception(msg) raise exc.HTTPBadRequest(msg) except Exception: msg = _('EnvTemplate body is incorrect') LOG.exception(msg) raise exc.HTTPBadRequest(msg) template = env_temps.EnvTemplateServices.update(env_template_id, body) return template.to_dict() @request_statistics.stats_count(API_NAME, 'Delete') def delete(self, request, env_template_id): """It deletes the env template :param request: the operation request. :param env_template_id: the template ID. """ LOG.debug('EnvTemplates:Delete '.format( templ_id=env_template_id)) target = {"env_template_id": env_template_id} policy.check('delete_env_template', request.context, target) self._validate_request(request, env_template_id) env_temps.EnvTemplateServices.delete(env_template_id) env_temps.EnvTemplateServices.remove(env_template_id) return def has_services(self, template): """"It checks if the template has services :param template: the template to check. :return: True or False """ if not template.description: return False if (template.description.get('services')): return True return False @request_statistics.stats_count(API_NAME, 'Create_environment') def create_environment(self, request, env_template_id, body): """Creates environment and session from template :param request: operation request :param env_template_id: environment template ID :param body: the environment name :return: session_id and environment_id """ target = {"env_template_id": env_template_id} policy.check('create_environment', request.context, target) self._validate_request(request, env_template_id) LOG.debug('Templates:Create environment '. format(templ_id=env_template_id)) template = env_temps.EnvTemplateServices.\ get_env_template(env_template_id) self._validate_body_name(body) try: environment = envs.EnvironmentServices.create( body.copy(), request.context) except db_exc.DBDuplicateEntry: msg = _('Environment with specified name already exists') LOG.exception(msg) raise exc.HTTPConflict(explanation=msg) user_id = request.context.user session = sessions.SessionServices.create(environment.id, user_id) if self.has_services(template): services_node = utils.TraverseHelper.get("services", template.description) utils.TraverseHelper.update("/Objects/services", services_node, environment.description) envs.EnvironmentServices.save_environment_description( session.id, environment.description, inner=False ) return {"session_id": session.id, "environment_id": environment.id} @request_statistics.stats_count(API_NAME, 'Clone') def clone(self, request, env_template_id, body): """Clones env template from another tenant It clones the env template from another env template from other tenant. :param request: the operation request. :param env_template_id: the env template ID. :param body: the request body. :return: the description of the created template. """ LOG.debug('EnvTemplates:Clone '. format(body, env_template_id)) policy.check('clone_env_template', request.context) old_env_template = self._validate_exists(env_template_id) if not old_env_template.get('is_public'): msg = _('User has no access to these resources.') LOG.error(msg) raise exc.HTTPForbidden(explanation=msg) self._validate_body_name(body) LOG.debug('ENV TEMP NAME: {0}'.format(body['name'])) try: is_public = body.get('is_public', False) template = env_temps.EnvTemplateServices.clone( env_template_id, request.context.project_id, body['name'], is_public) except db_exc.DBDuplicateEntry: msg = _('Env template with specified name already exists') LOG.error(msg) raise exc.HTTPConflict(explanation=msg) return template.to_dict() def _validate_request(self, request, env_template_id): env_template = self._validate_exists(env_template_id) if env_template.is_public or request.context.is_admin: return if env_template.tenant_id != request.context.project_id: msg = _('User has no access to these resources.') LOG.error(msg) raise exc.HTTPForbidden(explanation=msg) def _validate_exists(self, env_template_id): env_template_exists = env_temps.EnvTemplateServices.env_template_exist if not env_template_exists(env_template_id): msg = _('EnvTemplate is not found').format( temp_id=env_template_id) LOG.error(msg) raise exc.HTTPNotFound(explanation=msg) get_env_template = env_temps.EnvTemplateServices.get_env_template return get_env_template(env_template_id) def _validate_body_name(self, body): if not('name' in body and body['name'].strip()): msg = _('Please, specify a name of the environment template.') LOG.error(msg) raise exc.HTTPBadRequest(explanation=msg) name = str(body['name']) if len(name) > 255: msg = _('Environment template name should be 255 characters ' 'maximum') LOG.error(msg) raise exc.HTTPBadRequest(explanation=msg) def create_resource(): return wsgi.Resource(Controller()) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/api/v1/validation_schemas.py0000664000175000017500000000744400000000000021742 0ustar00zuulzuul00000000000000# 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. ENV_SCHEMA = { "$schema": "http://json-schema.org/draft-04/schema#", "type": "object", "properties": { "?": { "type": "object", "properties": { "id": {"type": "string"}, "name": {"type": "string"}, "type": {"type": "string"}, "_actions": {"type": "object"} }, "required": ["id", "type"] }, "name": {"type": "string"}, "region": {"type": ["string", "null"]}, "regions": {"type": "object"}, "defaultNetworks": { "type": "object", "properties": { "environment": { "type": "object", "properties": { "name": {"type": "string"}, "?": { "type": "object", "properties": { "type": {"type": "string"}, "id": {"type": "string"}, "name": {"type": "string"} }, }, "autoUplink": {"type": "boolean"}, "externalRouterId": {"type": "string"}, "dnsNameServers": {"type": "array"}, "autogenerateSubnet": {"type": "boolean"}, "subnetCidr": {"type": "string"}, "openstackId": {"type": "string"}, "regionName": {"type": "string"} }, "required": ["name", "?"] }, "flat": {"type": ["boolean", "null"]} }, "required": ["environment", "flat"] }, "services": { "type": "array", "minItems": 0, "items": {"type": "object"} } }, "required": ["?", "name", "region", "defaultNetworks"] } PKG_UPLOAD_SCHEMA = { "$schema": "http://json-schema.org/draft-04/schema#", "type": "object", "properties": { "tags": { "type": "array", "minItems": 0, "items": {"type": "string"}, "uniqueItems": True }, "categories": { "type": "array", "minItems": 0, "items": {"type": "string"}, "uniqueItems": True }, "description": {"type": "string"}, "name": {"type": "string"}, "is_public": {"type": "boolean"}, "enabled": {"type": "boolean"} }, "additionalProperties": False } PKG_UPDATE_SCHEMA = { "$schema": "http://json-schema.org/draft-04/schema#", "type": "object", "properties": { "tags": { "type": "array", "items": {"type": "string"}, "uniqueItems": True }, "categories": { "type": "array", "items": {"type": "string"}, "uniqueItems": True }, "description": {"type": "string"}, "name": {"type": "string"}, "is_public": {"type": "boolean"}, "enabled": {"type": "boolean"} }, "additionalProperties": False, "minProperties": 1, } ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/api/versions.py0000664000175000017500000000353200000000000017421 0ustar00zuulzuul00000000000000# 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 http import client as http_client from oslo_serialization import jsonutils import webob.dec from murano.common import wsgi class Controller(object): """A wsgi controller that reports which API versions are supported.""" def index(self, req): """Respond to a request for all OpenStack API versions.""" def build_version_object(version, path, status): return { 'id': 'v%s' % version, 'status': status, 'links': [ { 'rel': 'self', 'href': '%s/%s/' % (req.host_url, path), }, ], } version_objs = [] version_objs.extend([ build_version_object(1.0, 'v1', 'CURRENT'), ]) response = webob.Response(request=req, status=http_client.MULTIPLE_CHOICES, content_type='application/json') response.text = jsonutils.dumps(dict(versions=version_objs)) return response @webob.dec.wsgify(RequestClass=wsgi.Request) def __call__(self, req): return self.index(req) def create_resource(conf): return wsgi.Resource(Controller()) ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1696417899.777181 murano-16.0.0/murano/cfapi/0000775000175000017500000000000000000000000015505 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/cfapi/__init__.py0000664000175000017500000000000000000000000017604 0ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/cfapi/cfapi.py0000664000175000017500000003236300000000000017150 0ustar00zuulzuul00000000000000# 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 json import uuid from oslo_config import cfg from oslo_log import log as logging import tenacity from webob import response from murano.common import auth_utils # noqa from murano.common import wsgi from murano.db.services import cf_connections as db_cf import muranoclient.client as muranoclient from muranoclient.common import exceptions from muranoclient.glance import client as glare_client LOG = logging.getLogger(__name__) CONF = cfg.CONF class Controller(object): """WSGI controller for application catalog resource in Murano v1 API""" def _package_to_service(self, package): srv = {} srv['id'] = package.id srv['name'] = package.name if len(package.description) > 256: srv['description'] = u"{0} ...".format(package.description[:253]) else: srv['description'] = package.description srv['bindable'] = True srv['tags'] = [] for tag in package.tags: srv['tags'].append(tag) plan = {'id': package.id + '-1', 'name': 'default', 'description': 'Default plan for the service {name}'.format( name=package.name)} srv['plans'] = [plan] return srv def _make_service(self, name, package, plan_id): id = uuid.uuid4().hex return {"name": name, "?": {plan_id: {"name": package.name}, "type": package.fully_qualified_name, "id": id}} def _get_service(self, env, service_id): for service in env.services: if service['?']['id'] == service_id: return service return None def list(self, req): token = req.headers['X-Auth-Token'] m_cli = _get_muranoclient(token, req) kwargs = {'type': 'Application'} packages = m_cli.packages.filter(**kwargs) services = [] for package in packages: services.append(self._package_to_service(package)) resp = {'services': services} return resp def provision(self, req, body, instance_id): """Here is the example of request body given us from Cloud Foundry: { "service_id": "service-guid-here", "plan_id": "plan-guid-here", "organization_guid": "org-guid-here", "space_guid": "space-guid-here", "parameters": {"param1": "value1", "param2": "value2"} } """ data = json.loads(req.body) space_guid = data['space_guid'] org_guid = data['organization_guid'] plan_id = data['plan_id'] service_id = data['service_id'] parameters = data['parameters'] self.current_session = None # Here we'll take an entry for CF org and space from db. If we # don't have any entries we will create it from scratch. try: tenant = db_cf.get_tenant_for_org(org_guid) except AttributeError: tenant = req.headers['X-Project-Id'] db_cf.set_tenant_for_org(org_guid, tenant) LOG.info("Cloud Foundry {org_id} mapped to tenant " "{tenant_name}".format(org_id=org_guid, tenant_name=tenant)) token = req.headers['X-Auth-Token'] m_cli = _get_muranoclient(token, req) def _set_new_environment_for_space(space_guid, log_msg): body = {'name': 'my_{uuid}'.format(uuid=uuid.uuid4().hex)} env = m_cli.environments.create(body) db_cf.set_environment_for_space(space_guid, env.id) LOG.info(log_msg.format(space_id=space_guid, environment_id=env.id)) return env.id try: environment_id = db_cf.get_environment_for_space(space_guid) # NOTE: Check that environment which was previously linked with # CF space still exist, reset a new environment for space. try: env = m_cli.environments.get(environment_id) except exceptions.HTTPNotFound: msg = ("Can not find environment_id {environment_id}, " "will create a new one." .format(environment_id=environment_id)) LOG.info(msg) env = {} if not env: log_msg = ("Cloud Foundry {space_id} remapped to " "{environment_id}") environment_id = _set_new_environment_for_space( space_guid, log_msg) except AttributeError: log_msg = ("Cloud Foundry {space_id} mapped to " "{environment_id}") environment_id = _set_new_environment_for_space( space_guid, log_msg) package = m_cli.packages.get(service_id) LOG.debug('Adding service {name}'.format(name=package.name)) service = self._make_service(space_guid, package, plan_id) db_cf.set_instance_for_service(instance_id, service['?']['id'], environment_id, tenant) # NOTE(Kezar): Here we are going through JSON and add ids where # it's necessary. Before that we need to drop '?' key from parameters # dictionary as far it contains murano package related info which is # necessary in our scenario if '?' in parameters.keys(): parameters.pop('?', None) LOG.warning("Incorrect input parameters. Package related " "parameters shouldn't be passed through Cloud " "Foundry") params = [parameters] while params: a = params.pop() for k, v in a.items(): if isinstance(v, dict): params.append(v) if k == '?': v['id'] = uuid.uuid4().hex service.update(parameters) # Now we need to obtain session to modify the env session_id = create_session(m_cli, environment_id) m_cli.services.post(environment_id, path='/', data=service, session_id=session_id) m_cli.sessions.deploy(environment_id, session_id) self.current_session = session_id return response.Response(status=202, json_body={}) def deprovision(self, req, instance_id): service = db_cf.get_service_for_instance(instance_id) if not service: return {} service_id = service.service_id environment_id = service.environment_id token = req.headers['X-Auth-Token'] m_cli = _get_muranoclient(token, req) session_id = create_session(m_cli, environment_id) m_cli.services.delete(environment_id, '/' + service_id, session_id) m_cli.sessions.deploy(environment_id, session_id) return response.Response(status=202, json_body={}) def bind(self, req, body, instance_id, app_id): db_service = db_cf.get_service_for_instance(instance_id) if not db_service: return {} service_id = db_service.service_id environment_id = db_service.environment_id token = req.headers['X-Auth-Token'] m_cli = _get_muranoclient(token, req) session_id = create_session(m_cli, environment_id) env = m_cli.environments.get(environment_id, session_id) LOG.debug('Got environment {0}'.format(env)) service = self._get_service(env, service_id) LOG.debug('Got service {0}'.format(service)) # NOTE(starodubcevna): Here we need to find an action which will return # us needed credentials. By default we will look for getCredentials # action. result = {} try: actions = service['?']['_actions'] for action_id in list(actions): if 'getCredentials' in action_id: @tenacity.retry( retry=tenacity.retry_if_exception_type(TypeError), wait=tenacity.wait_random(min=1, max=10), stop=tenacity.stop_after_delay(30), reraise=True) def _get_creds(client, task_id, environment_id): result = m_cli.actions.get_result(environment_id, task_id)['result'] return result task_id = m_cli.actions.call(environment_id, action_id) result = _get_creds(m_cli, task_id, environment_id) if not result: LOG.warning("This application doesn't have action " "getCredentials") return response.Response(status=500) except KeyError: # NOTE(starodubcevna): In CF service broker API spec return # code for failed bind is not present, so we will return 500. LOG.warning("This application doesn't have actions at all") return response.Response(status=500) if 'credentials' in list(result): return result else: return {'credentials': result} def unbind(self, req, instance_id, app_id): """Unsupported functionality murano doesn't support this kind of functionality, so we just need to create a stub where the call will come. We can't raise something like NotImplementedError because we will have problems on Cloud Foundry side. The best way now it to return empty dict which will be correct answer for Cloud Foundry. """ return {} def get_last_operation(self, req, instance_id): service = db_cf.get_service_for_instance(instance_id) # NOTE(freerunner): Prevent code 500 if requested environment # already doesn't exist. if not service: LOG.warning('Requested service for instance {0} is not ' 'found'.format(instance_id)) body = {} resp = response.Response(status=410, json_body=body) return resp env_id = service.environment_id token = req.headers["X-Auth-Token"] m_cli = _get_muranoclient(token, req) # NOTE(starodubcevna): we can track only environment status. it's # murano API limitation. m_environment = m_cli.environments.get(env_id) body = {'state': 'unknown', 'description': 'operation unknown'} resp = response.Response(status=500, json_body=body) if m_environment.status == 'ready': body = {'state': 'succeeded', 'description': 'operation succeed'} resp = response.Response(status=200, json_body=body) elif m_environment.status in ['pending', 'deleting', 'deploying']: body = {'state': 'in progress', 'description': 'operation in progress'} resp = response.Response(status=202, json_body=body) elif m_environment.status in ['deploy failure', 'delete failure']: body = {'state': 'failed', 'description': '{0}. Please correct it manually'.format( m_environment.status)} resp = response.Response(status=200, json_body=body) return resp def _get_muranoclient(token_id, req): artifacts_client = None if CONF.engine.packages_service in ['glance', 'glare']: artifacts_client = _get_glareclient(token_id, req) murano_url = CONF.murano.url or req.endpoints.get('murano') if not murano_url: LOG.error('No murano url is specified and no ' '"application-catalog" ' 'service is registered in keystone.') return muranoclient.Client(1, murano_url, token=token_id, artifacts_client=artifacts_client) def _get_glareclient(token_id, req): glare_settings = CONF.glare url = glare_settings.url or req.endpoints.get('glare') if not url: LOG.error('No glare url is specified and no "artifact" ' 'service is registered in keystone.') # TODO(gyurco): use auth_utils.get_session_client_parameters return glare_client.Client( endpoint=url, token=token_id, insecure=glare_settings.insecure, key_file=glare_settings.keyfile or None, ca_file=glare_settings.cafile or None, cert_file=glare_settings.certfile or None, type_name='murano', type_version=1) def create_session(client, environment_id): id = client.sessions.configure(environment_id).id return id def create_resource(): return wsgi.Resource(Controller(), serializer=wsgi.ServiceBrokerResponseSerializer()) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/cfapi/router.py0000664000175000017500000000444200000000000017403 0ustar00zuulzuul00000000000000# 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 routes from murano.cfapi import cfapi from murano.common import wsgi class API(wsgi.Router): @classmethod def factory(cls, global_conf, **local_conf): return cls(routes.Mapper()) def __init__(self, mapper): services_resource = cfapi.create_resource() mapper.connect('/v2/catalog', controller=services_resource, action='list', conditions={'method': ['GET']}) mapper.connect(('/v2/service_instances/{instance_id}'), controller=services_resource, action='provision', conditions={'method': ['PUT']}) mapper.connect(('/v2/service_instances/{instance_id}'), controller=services_resource, action='deprovision', conditions={'method': ['DELETE']}) mapper.connect(('/v2/service_instances/{instance_id}/service_bindings/' '{app_id}'), controller=services_resource, action='bind', conditions={'method': ['PUT']}) mapper.connect(('/v2/service_instances/{instance_id}/service_bindings/' '{app_id}'), controller=services_resource, action='unbind', conditions={'method': ['DELETE']}) mapper.connect(('/v2/service_instances/{instance_id}/last_operation'), controller=services_resource, action='get_last_operation', conditions={'method': ['GET']}) super(API, self).__init__(mapper) ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1696417899.777181 murano-16.0.0/murano/cmd/0000775000175000017500000000000000000000000015166 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/cmd/__init__.py0000664000175000017500000000114200000000000017275 0ustar00zuulzuul00000000000000# # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # 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 murano.monkey_patch # noqa ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/cmd/api.py0000664000175000017500000000437200000000000016317 0ustar00zuulzuul00000000000000#!/usr/bin/env python # # 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 sys from oslo_concurrency import processutils from oslo_config import cfg from oslo_log import log as logging from oslo_service import service from murano.api.v1 import request_statistics from murano.common import app_loader from murano.common import config from murano.common import policy from murano.common import server from murano.common import statservice as stats from murano.common import wsgi CONF = cfg.CONF # If ../murano/__init__.py exists, add ../ to Python search path, so that # it will override what happens to be installed in /usr/(local/)lib/python... root = os.path.join(os.path.abspath(__file__), os.pardir, os.pardir, os.pardir) if os.path.exists(os.path.join(root, 'murano', '__init__.py')): sys.path.insert(0, root) def main(): try: config.parse_args() config.set_middleware_defaults() request_statistics.init_stats() policy.init() logging.setup(CONF, 'murano') workers = CONF.murano.api_workers if not workers: workers = processutils.get_worker_count() launcher = service.launch( CONF, server.ApiService(), workers=workers, restart_method='mutate') app = app_loader.load_paste_app('murano') port, host = (CONF.bind_port, CONF.bind_host) launcher.launch_service(wsgi.Service(app, port, host)) launcher.launch_service(server.NotificationService()) launcher.launch_service(stats.StatsCollectingService()) launcher.wait() except RuntimeError as e: sys.stderr.write("ERROR: %s\n" % e) sys.exit(1) if __name__ == '__main__': main() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/cmd/cfapi.py0000664000175000017500000000355300000000000016630 0ustar00zuulzuul00000000000000#!/usr/bin/env 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. import os import sys from oslo_config import cfg from oslo_log import log as logging from oslo_service import service from murano.api.v1 import request_statistics from murano.common import app_loader from murano.common import cf_config as config from murano.common import policy from murano.common import wsgi CONF = cfg.CONF # If ../murano/__init__.py exists, add ../ to Python search path, so that # it will override what happens to be installed in /usr/(local/)lib/python... root = os.path.join(os.path.abspath(__file__), os.pardir, os.pardir, os.pardir) if os.path.exists(os.path.join(root, 'murano', '__init__.py')): sys.path.insert(0, root) def main(): try: config.parse_args() logging.setup(CONF, 'murano-cfapi') request_statistics.init_stats() policy.init() launcher = service.ServiceLauncher(CONF) cfapp = app_loader.load_paste_app('cloudfoundry') cfport, cfhost = (config.CONF.cfapi.bind_port, config.CONF.cfapi.bind_host) launcher.launch_service(wsgi.Service(cfapp, cfport, cfhost)) launcher.wait() except RuntimeError as e: sys.stderr.write("ERROR: %s\n" % e) sys.exit(1) if __name__ == '__main__': main() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/cmd/cfapi_db_manage.py0000664000175000017500000000474700000000000020613 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # 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 oslo_config import cfg from oslo_db import options from murano.db.cfapi_migration import migration CONF = cfg.CONF options.set_defaults(CONF) class ApiDBCommand(object): def upgrade(self, config): migration.upgrade(CONF.command.revision, config=config) def downgrade(self, config): migration.downgrade(CONF.command.revision, config=config) def revision(self, config): migration.revision(CONF.command.message, CONF.command.autogenerate, config=config) def stamp(self, config): migration.stamp(CONF.command.revision, config=config) def version(self, config): print(migration.version()) def add_command_parsers(subparsers): command_object = ApiDBCommand() parser = subparsers.add_parser('upgrade') parser.set_defaults(func=command_object.upgrade) parser.add_argument('--revision', nargs='?') parser = subparsers.add_parser('downgrade') parser.set_defaults(func=command_object.downgrade) parser.add_argument('--revision', nargs='?') parser = subparsers.add_parser('stamp') parser.add_argument('--revision', nargs='?') parser.set_defaults(func=command_object.stamp) parser = subparsers.add_parser('revision') parser.add_argument('-m', '--message') parser.add_argument('--autogenerate', action='store_true') parser.set_defaults(func=command_object.revision) parser = subparsers.add_parser('version') parser.set_defaults(func=command_object.version) command_opt = cfg.SubCommandOpt('command', title='Command', help='Available commands', handler=add_command_parsers) CONF.register_cli_opt(command_opt) def main(): config = migration.get_alembic_config() # attach the Murano conf to the Alembic conf config.murano_config = CONF CONF(project='murano') CONF.command.func(config) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/cmd/db_manage.py0000664000175000017500000000473300000000000017444 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # 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 oslo_config import cfg from oslo_db import options from murano.db.migration import migration CONF = cfg.CONF options.set_defaults(CONF) class DBCommand(object): def upgrade(self, config): migration.upgrade(CONF.command.revision, config=config) def downgrade(self, config): migration.downgrade(CONF.command.revision, config=config) def revision(self, config): migration.revision(CONF.command.message, CONF.command.autogenerate, config=config) def stamp(self, config): migration.stamp(CONF.command.revision, config=config) def version(self, config): print(migration.version()) def add_command_parsers(subparsers): command_object = DBCommand() parser = subparsers.add_parser('upgrade') parser.set_defaults(func=command_object.upgrade) parser.add_argument('--revision', nargs='?') parser = subparsers.add_parser('downgrade') parser.set_defaults(func=command_object.downgrade) parser.add_argument('--revision', nargs='?') parser = subparsers.add_parser('stamp') parser.add_argument('--revision', nargs='?') parser.set_defaults(func=command_object.stamp) parser = subparsers.add_parser('revision') parser.add_argument('-m', '--message') parser.add_argument('--autogenerate', action='store_true') parser.set_defaults(func=command_object.revision) parser = subparsers.add_parser('version') parser.set_defaults(func=command_object.version) command_opt = cfg.SubCommandOpt('command', title='Command', help='Available commands', handler=add_command_parsers) CONF.register_cli_opt(command_opt) def main(): config = migration.get_alembic_config() # attach the Murano conf to the Alembic conf config.murano_config = CONF CONF(project='murano') CONF.command.func(config) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/cmd/engine.py0000664000175000017500000000322700000000000017011 0ustar00zuulzuul00000000000000#!/usr/bin/env python # # 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 sys from oslo_concurrency import processutils from oslo_log import log as logging from oslo_service import service from murano.common import config from murano.common import engine CONF = config.CONF # If ../murano/__init__.py exists, add ../ to Python search path, so that # it will override what happens to be installed in /usr/(local/)lib/python... root = os.path.join(os.path.abspath(__file__), os.pardir, os.pardir, os.pardir) if os.path.exists(os.path.join(root, 'murano', '__init__.py')): sys.path.insert(0, root) def main(): try: config.parse_args() logging.setup(CONF, 'murano') workers = CONF.engine.engine_workers if not workers: workers = processutils.get_worker_count() launcher = service.launch( CONF, engine.EngineService(), workers=workers, restart_method='mutate') launcher.wait() except RuntimeError as e: sys.stderr.write("ERROR: %s\n" % e) sys.exit(1) if __name__ == '__main__': main() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/cmd/manage.py0000664000175000017500000001312600000000000016773 0ustar00zuulzuul00000000000000# 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. """ *** Deprecation warning *** This file is about to be deprecated, please use python-muranoclient. *** Deprecation warning *** """ import sys import traceback from oslo_config import cfg from oslo_db import exception as db_exception from oslo_log import log as logging from murano.common import consts from murano.db.catalog import api as db_catalog_api from murano.packages import load_utils from murano import version CONF = cfg.CONF LOG = logging.getLogger(__name__) class AdminContext(object): def __init__(self): self.is_admin = True def _do_import_package(_dir, categories, update=False): LOG.debug("Going to import Murano package from {source}".format( source=_dir)) pkg = load_utils.load_from_dir(_dir) LOG.debug("Checking for existing packages") existing = db_catalog_api.package_search( {'fqn': pkg.full_name}, AdminContext()) if existing: existing_pkg = existing[0] if update: LOG.debug('Deleting existing package {exst_pkg_id}').format( exst_pkg_id=existing_pkg.id) db_catalog_api.package_delete(existing_pkg.id, AdminContext()) else: LOG.error("Package '{name}' exists ({pkg_id}). Use --update." .format(name=pkg.full_name, pkg_id=existing_pkg.id)) return package = { 'fully_qualified_name': pkg.full_name, 'type': pkg.package_type, 'author': pkg.author, 'supplier': pkg.supplier, 'name': pkg.display_name, 'description': pkg.description, # note: we explicitly mark all the imported packages as public, # until a parameter added to control visibility scope of a package 'is_public': True, 'tags': pkg.tags, 'logo': pkg.logo, 'supplier_logo': pkg.supplier_logo, 'ui_definition': pkg.ui, 'class_definitions': pkg.classes, 'archive': pkg.blob, 'categories': categories or [] } # note(ruhe): the second parameter is tenant_id # it is a required field in the DB, that's why we pass an empty string result = db_catalog_api.package_upload(package, '') LOG.info("Finished import of package {res_id}".format(res_id=result.id)) # TODO(ruhe): proper error handling def do_import_package(): """Import Murano package from local directory.""" _do_import_package( CONF.command.directory, CONF.command.categories, CONF.command.update) def do_list_categories(): categories = db_catalog_api.category_get_names() if categories: print(">> Murano package categories:") for c in categories: print("* {0}".format(c)) else: print("No categories were found") def do_add_category(): category_name = CONF.command.category_name try: db_catalog_api.category_add(category_name) print(">> Successfully added category {0}".format(category_name)) except db_exception.DBDuplicateEntry: print(">> ERROR: Category '{0}' already exists".format(category_name)) def add_command_parsers(subparsers): parser = subparsers.add_parser('import-package') parser.set_defaults(func=do_import_package) parser.add_argument('directory', help='A directory with Murano package.') parser.add_argument('-u', '--update', action="store_true", default=False, help='If a package already exists, delete and update') parser.add_argument('-c', '--categories', choices=consts.CATEGORIES, nargs='*', help='An optional list of categories this package ' 'to be assigned to.') parser = subparsers.add_parser('category-list') parser.set_defaults(func=do_list_categories) parser = subparsers.add_parser('category-add') parser.set_defaults(func=do_add_category) parser.add_argument('category_name', help='Name of the new category.') command_opt = cfg.SubCommandOpt('command', title='Commands', help='Show available commands.', handler=add_command_parsers) def main(): CONF.register_cli_opt(command_opt) try: default_config_files = cfg.find_config_files('murano', 'murano') CONF(sys.argv[1:], project='murano', prog='murano-manage', version=version.version_string, default_config_files=default_config_files) except RuntimeError as e: LOG.error("failed to initialize murano-manage: {error}".format( error=e)) sys.exit("ERROR: %s" % e) try: CONF.command.func() except Exception as e: tb = traceback.format_exc() err_msg = ("murano-manage command failed: {error}\n" "{traceback}").format(error=e, traceback=tb) LOG.error(err_msg) sys.exit(err_msg) if __name__ == '__main__': main() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/cmd/status.py0000664000175000017500000000240300000000000017062 0ustar00zuulzuul00000000000000# Copyright (c) 2018 NEC, 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 sys from oslo_config import cfg from oslo_upgradecheck import common_checks from oslo_upgradecheck import upgradecheck from murano.common.i18n import _ CONF = cfg.CONF class Checks(upgradecheck.UpgradeCommands): """Contains upgrade checks Various upgrade checks should be added as separate methods in this class and added to _upgrade_checks tuple. """ _upgrade_checks = ( (_("Policy File JSON to YAML Migration"), (common_checks.check_policy_json, {'conf': CONF})), ) def main(): return upgradecheck.main( CONF, project='murano', upgrade_command=Checks()) if __name__ == '__main__': sys.exit(main()) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/cmd/test_runner.py0000664000175000017500000003672300000000000020123 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # 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 argparse import os import re import sys import traceback import eventlet from keystoneclient.v3 import client as ks_client from muranoclient.common import exceptions as exc from muranoclient.common import utils from oslo_config import cfg from oslo_db import options from oslo_log import log as logging from oslo_utils import timeutils from murano.common import config from murano.common import engine from murano.common.i18n import _ from murano.dsl import dsl_exception from murano.dsl import dsl_types from murano.dsl import exceptions from murano.dsl import executor from murano.dsl import helpers from murano.engine import execution_session from murano.engine import mock_context_manager from murano.engine import package_loader from murano import version CONF = cfg.CONF LOG = logging.getLogger(__name__) options.set_defaults(CONF) BASE_CLASS = 'io.murano.test.TestFixture' TEST_CASE_NAME = re.compile('^test(?![a-z])') OK_COLOR = '\033[92m' FAIL_COLOR = '\033[91m' END_COLOR = '\033[0m' if os.name == 'nt': # eventlet monkey patching causes subprocess.Popen to fail on Windows # when using pipes due to missing non blocking I/O support eventlet.monkey_patch(os=False) else: eventlet.monkey_patch() class MuranoTestRunner(object): def __init__(self): self.parser = self.get_parser() self.args = self.parser.parse_args() if self.args.verbose: LOG.logger.setLevel(logging.DEBUG) def _load_package(self, pkg_loader, name): try: parts = name.rsplit('/') if len(parts) == 2: name, pkg_version = parts version_spec = helpers.parse_version_spec(pkg_version) else: version_spec = helpers.parse_version_spec('*') package = pkg_loader.load_package(name, version_spec) except exceptions.NoPackageFound: if not CONF.engine.load_packages_from: msg = _('Local package is not found since "load-packages-from"' ' engine parameter is not provided and specified ' 'packages is not loaded to murano-api') else: msg = _('Specified package is not found: {0} were scanned ' 'together with murano database' ).format(','.join( CONF.engine.load_packages_from)) LOG.error(msg) self.error(msg, show_help=False) except exc.CommunicationError: msg = ('Murano API is not running. ' 'Check configuration parameters.') LOG.error(msg) self.error(msg, show_help=False) return package def _get_methods_to_run(self, package, tests_to_run, class_to_methods): if not tests_to_run: return class_to_methods methods_to_run = {} for item in tests_to_run: # Check for method name occurrence in all methods. # if there is no dot in provided item - it is a method name if '.' not in item: for class_name, methods in class_to_methods.items(): methods_to_run[class_name] = [] if item in methods: methods_to_run[class_name].append(item) continue # Check match for the whole class name if item in package.classes: # Add all test cases from specified package if item in class_to_methods: methods_to_run[item] = class_to_methods[item] continue # Check match for the class together with method specified class_to_test, test_method = item.rsplit('.', 1) if class_to_test in package.classes: methods_to_run[class_to_test] = [ m for m in class_to_methods[class_to_test] if m == test_method] continue methods_count = sum(len(v) for v in methods_to_run.values()) methods = [k + '.' + method for k, v in methods_to_run.items() for method in v] LOG.debug('{0} method(s) is(are) going to be executed: ' '\n{1}'.format(methods_count, '\n'.join(methods))) return methods_to_run def _get_test_cases_by_classes(self, package): """Build valid test cases list for each class in the provided package. Check, if test class and test case name are valid. Return class mappings to test cases. """ class_to_methods = {} for pkg_class_name in package.classes: class_obj = package.find_class(pkg_class_name, False) base_class = package.find_class(BASE_CLASS) if not base_class.is_compatible(class_obj): LOG.debug('Class {0} is not inherited from {1}. ' 'Skipping it.'.format(pkg_class_name, BASE_CLASS)) continue # Exclude methods, that are not test cases. tests = [] valid_test_name = TEST_CASE_NAME for m in class_obj.methods: if valid_test_name.match(m): tests.append(m) class_to_methods[pkg_class_name] = tests return class_to_methods def _call_service_method(self, name, exc, obj): if name in obj.type.all_method_names: method = obj.type.find_single_method(name) method.scope = dsl_types.MethodScopes.Public LOG.debug('Executing: {0}.{1}'.format(obj.type.name, name)) exc.run(obj.type, name, obj, (), {}) def _validate_keystone_opts(self, args): ks_opts_to_config = { 'auth_url': 'www_authenticate_uri', 'username': 'admin_user', 'password': 'admin_password', 'project_name': 'admin_project_name'} ks_opts = {'auth_url': getattr(args, 'os_auth_url', None), 'username': getattr(args, 'os_username', None), 'password': getattr(args, 'os_password', None), 'project_name': getattr(args, 'os_project_name', None)} if None in ks_opts.values() and not CONF.default_config_files: msg = ('Please provide murano config file or credentials for ' 'authorization: {0}').format( ', '.join(['--os-auth-url', '--os-username', '--os-password', '--os-project-name', '--os-tenant-id'])) LOG.error(msg) self.error(msg) for param, value in ks_opts.items(): if not value: ks_opts[param] = getattr(CONF.murano_auth, ks_opts_to_config[param]) if param == 'auth_url': ks_opts[param] = ks_opts[param].replace('v2.0', 'v3') return ks_opts def error(self, msg, show_help=True): sys.stderr.write("ERROR: {msg}\n\n".format(msg=msg)) if show_help: self.parser.print_help() sys.exit(1) def message(self, msg): sys.stdout.write('{0}\n'.format(msg)) def run_tests(self): exit_code = 0 provided_pkg_name = self.args.package load_packages_from = self.args.load_packages_from tests_to_run = self.args.tests ks_opts = self._validate_keystone_opts(self.args) client = ks_client.Client(**ks_opts) test_session = execution_session.ExecutionSession() test_session.token = client.auth_token test_session.project_id = client.project_id # Replace location of loading packages with provided from command line. if load_packages_from: cfg.CONF.engine.load_packages_from = load_packages_from with package_loader.CombinedPackageLoader(test_session) as pkg_loader: engine.get_plugin_loader().register_in_loader(pkg_loader) package = self._load_package(pkg_loader, provided_pkg_name) class_to_methods = self._get_test_cases_by_classes(package) run_set = self._get_methods_to_run(package, tests_to_run, class_to_methods) max_length = 0 num_tests = 0 for pkg_class, test_cases in run_set.items(): for m in test_cases: max_length = max(max_length, len(pkg_class) + len(m) + 1) num_tests += len(test_cases) max_length += 3 if run_set: LOG.debug('Starting test execution.') self.message('About to execute {0} tests(s)'.format(num_tests)) else: msg = _('No tests found for execution.') LOG.error(msg) self.error(msg) run_count = 0 error_count = 0 started = timeutils.utcnow() for pkg_class, test_cases in run_set.items(): for m in test_cases: # Create new executor for each test case to provide # pure test environment dsl_executor = executor.MuranoDslExecutor( pkg_loader, mock_context_manager.MockContextManager(), test_session) obj = dsl_executor.object_store.load( {}, None, default_type=package.find_class(pkg_class, False)) test_name = "{0}.{1}".format(obj.type.name, m) dots_number = max_length - len(test_name) msg = "{0} {1} ".format(test_name, '.' * dots_number) sys.stdout.write(msg) sys.stdout.flush() self._call_service_method('setUp', dsl_executor, obj) obj.type.methods[m].usage = 'Action' test_session.start() try: run_count += 1 dsl_executor.run(obj.type, m, obj, (), {}) self._call_service_method( 'tearDown', dsl_executor, obj) msg = '{0}{1}{2}\n'.format(OK_COLOR, 'OK', END_COLOR) LOG.debug('Test {0} successful'.format(test_name)) sys.stdout.write(msg) sys.stdout.flush() except Exception as e: error_count += 1 msg = ''.join(( FAIL_COLOR, 'FAIL!', END_COLOR, '\n')) sys.stdout.write(msg) if isinstance(e, dsl_exception.MuranoPlException): tb = e.format() else: tb = traceback.format_exc() sys.stdout.write(''.join(( FAIL_COLOR, tb, END_COLOR, '\n' ))) sys.stdout.flush() LOG.exception('Test {0} failed'.format(test_name)) exit_code = 1 finally: test_session.finish() completed = timeutils.utcnow() self.message('Executed {0} tests in {1} seconds: ' '{2} passed, ' '{3} failed'.format(run_count, timeutils.delta_seconds( started, completed), run_count - error_count, error_count)) return exit_code def get_parser(self): parser = argparse.ArgumentParser(prog='murano-test-runner') parser.set_defaults(func=self.run_tests) parser.add_argument('--config-file', help='Path to the murano config') parser.add_argument('--os-auth-url', default=utils.env('OS_AUTH_URL'), help='Defaults to env[OS_AUTH_URL]') parser.add_argument('--os-username', default=utils.env('OS_USERNAME'), help='Defaults to env[OS_USERNAME]') parser.add_argument('--os-password', default=utils.env('OS_PASSWORD'), help='Defaults to env[OS_PASSWORD]') parser.add_argument('--os-project-name', default=utils.env('OS_PROJECT_NAME'), help='Defaults to env[OS_PROJECT_NAME]') parser.add_argument('-l', '--load_packages_from', nargs='*', metavar='path', help='Directory to search packages from. ' 'We be added to the list of current directory' ' list, provided in a config file.') parser.add_argument("-v", "--verbose", action="store_true", help="increase output verbosity") parser.add_argument('--version', action='version', version=version.version_string) parser.add_argument('package', metavar='', help='Full name of application package that is ' 'going to be tested') parser.add_argument('tests', nargs='*', metavar='className.testMethod', help='List of method names to be tested') return parser def main(): test_runner = MuranoTestRunner() try: if test_runner.args.config_file: default_config_files = [test_runner.args.config_file] else: default_config_files = cfg.find_config_files('murano') if not default_config_files: murano_conf = os.path.join(os.path.dirname(__file__), os.pardir, os.pardir, 'etc', 'murano', 'murano.conf') if os.path.exists(murano_conf): default_config_files = [murano_conf] sys.argv = [sys.argv[0]] config.parse_args(default_config_files=default_config_files) CONF.set_default('use_stderr', False) logging.setup(CONF, 'murano') except RuntimeError as e: LOG.exception("Failed to initialize murano-test-runner: %s", e) sys.exit("ERROR: %s" % e) try: exit_code = test_runner.run_tests() sys.exit(exit_code) except Exception as e: if isinstance(e, dsl_exception.MuranoPlException): tb = e.format() else: tb = traceback.format_exc() err_msg = "Command failed: {0}\n{1}".format(e, tb) LOG.error(err_msg) sys.exit(err_msg) if __name__ == '__main__': main() ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1696417899.7811809 murano-16.0.0/murano/common/0000775000175000017500000000000000000000000015713 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/common/__init__.py0000664000175000017500000000000000000000000020012 0ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/common/app_loader.py0000664000175000017500000000620700000000000020400 0ustar00zuulzuul00000000000000# 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 os from oslo_config import cfg from oslo_log import log as logging from paste import deploy from murano.common.i18n import _ CONF = cfg.CONF def _get_deployment_flavor(): """Retrieve the paste_deploy.flavor config item Retrieve the paste_deploy.flavor config item, formatted appropriately for appending to the application name. """ flavor = CONF.paste_deploy.flavor return '' if not flavor else ('-' + flavor) def _get_paste_config_path(): paste_suffix = '-paste.ini' conf_suffix = '.conf' if CONF.config_file: # Assume paste config is in a paste.ini file corresponding # to the last config file path = CONF.config_file[-1].replace(conf_suffix, paste_suffix) else: path = CONF.prog + '-paste.ini' return CONF.find_file(os.path.basename(path)) def _get_deployment_config_file(): """Retrieve the deployment_config_file config item Retrieve the deployment_config_file config item, formatted as an absolute pathname. """ path = CONF.paste_deploy.config_file if not path: path = _get_paste_config_path() if not path: msg = _("Unable to locate paste config file for %s.") % CONF.prog raise RuntimeError(msg) return os.path.abspath(path) def load_paste_app(app_name=None): """Builds and returns a WSGI app from a paste config file. We assume the last config file specified in the supplied ConfigOpts object is the paste config file. :param app_name: name of the application to load :raises RuntimeError when config file cannot be located or application cannot be loaded from config file """ if app_name is None: app_name = CONF.prog # append the deployment flavor to the application name, # in order to identify the appropriate paste pipeline app_name += _get_deployment_flavor() conf_file = _get_deployment_config_file() try: logger = logging.getLogger(__name__) logger.debug("Loading {app_name} from {conf_file}".format( conf_file=conf_file, app_name=app_name)) app = deploy.loadapp("config:%s" % conf_file, name=app_name) return app except (LookupError, ImportError) as e: msg = _("Unable to load %(app_name)s from configuration file" " %(conf_file)s. \nGot: %(e)r") % {'conf_file': conf_file, 'app_name': app_name, 'e': e} logger.error(msg) raise RuntimeError(msg) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/common/auth_utils.py0000664000175000017500000001373400000000000020456 0ustar00zuulzuul00000000000000# 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 os from keystoneauth1 import identity from keystoneauth1 import loading as ka_loading from keystoneclient.v3 import client as ks_client from oslo_config import cfg from oslo_log import log as logging from oslo_log import versionutils from murano.dsl import helpers CFG_KEYSTONE_GROUP = 'keystone_authtoken' CFG_MURANO_AUTH_GROUP = 'murano_auth' LOG = logging.getLogger(__name__) cfg.CONF.import_group(CFG_KEYSTONE_GROUP, 'keystonemiddleware.auth_token') def _get_keystone_auth(trust_id=None): kwargs = {} if trust_id: # Remove project_name and project_id, since we need a trust scoped # auth object kwargs['project_name'] = None kwargs['project_domain_name'] = None kwargs['project_id'] = None kwargs['trust_id'] = trust_id auth = ka_loading.load_auth_from_conf_options( cfg.CONF, CFG_MURANO_AUTH_GROUP, **kwargs) return auth def _create_keystone_admin_client(): auth = _get_keystone_auth() session = _get_session(auth=auth) return ks_client.Client(session=session) def get_client_session(execution_session=None, conf=None): if not execution_session: execution_session = helpers.get_execution_session() trust_id = execution_session.trust_id if trust_id is None: return get_token_client_session( token=execution_session.token, project_id=execution_session.project_id) auth = _get_keystone_auth(trust_id) session = _get_session(auth=auth, conf_section=conf) return session def get_token_client_session(token=None, project_id=None, conf=None): www_authenticate_uri = \ cfg.CONF[CFG_MURANO_AUTH_GROUP].www_authenticate_uri if not www_authenticate_uri: versionutils.report_deprecated_feature( LOG, 'Please configure www_authenticate_uri in ' + CFG_MURANO_AUTH_GROUP + 'group') www_authenticate_uri = \ cfg.CONF[CFG_KEYSTONE_GROUP].www_authenticate_uri if not (www_authenticate_uri.endswith('v2.0') or www_authenticate_uri.endswith('v3')): auth_url = os.path.join(www_authenticate_uri, 'v3') elif www_authenticate_uri.endswith('v2.0'): auth_url = www_authenticate_uri.replace('v2.0', 'v3') else: auth_url = www_authenticate_uri if token is None or project_id is None: execution_session = helpers.get_execution_session() token = execution_session.token project_id = execution_session.project_id token_auth = identity.Token( auth_url, token=token, project_id=project_id) session = _get_session(auth=token_auth, conf_section=conf) return session def create_keystone_client(token=None, project_id=None, conf=None): return ks_client.Client(session=get_token_client_session( token=token, project_id=project_id, conf=conf)) def create_trust(trustee_token=None, trustee_project_id=None): admin_client = _create_keystone_admin_client() user_client = create_keystone_client( token=trustee_token, project_id=trustee_project_id) trustee_user = admin_client.session.auth.get_user_id(admin_client.session) auth_ref = user_client.session.auth.get_access(user_client.session) trustor_user = auth_ref.user_id project = auth_ref.project_id roles = auth_ref.role_names trust = user_client.trusts.create( trustor_user=trustor_user, trustee_user=trustee_user, impersonation=True, role_names=roles, project=project) return trust.id def delete_trust(session): user_client = create_keystone_client( token=session.token, project_id=session.project_id) user_client.trusts.delete(session.trust_id) def _get_config_option(conf_section, option_name, default=None): if conf_section and hasattr(cfg.CONF[conf_section], option_name): return getattr(cfg.CONF[conf_section], option_name) return default def _get_session(auth, conf_section=None): # Fallback to murano_auth section for TLS parameters # if no other conf_section supplied if not conf_section: conf_section = CFG_MURANO_AUTH_GROUP session = ka_loading.load_session_from_conf_options( auth=auth, conf=cfg.CONF, group=conf_section) return session def get_session_client_parameters(service_type=None, region='', interface=None, service_name=None, conf=None, session=None, execution_session=None): if region == '': region = cfg.CONF.home_region result = { 'session': session or get_client_session( execution_session=execution_session, conf=conf) } url = _get_config_option(conf, 'url') if url: result['endpoint_override'] = url else: if not interface: interface = _get_config_option(conf, 'endpoint_type') result.update({ 'service_type': service_type, 'service_name': service_name, 'interface': interface, 'region_name': region }) return result def get_user(uid): client = _create_keystone_admin_client() return client.users.get(uid).to_dict() def get_project(pid): client = _create_keystone_admin_client() return client.projects.get(pid).to_dict() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/common/cf_config.py0000664000175000017500000000522300000000000020204 0ustar00zuulzuul00000000000000# Copyright 2011 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. from oslo_config import cfg from oslo_log import log as logging from oslo_middleware import cors from murano.common.i18n import _ from murano import version cfapi_opts = [ cfg.StrOpt('tenant', default='admin', help=_('Project for service broker')), cfg.HostAddressOpt('bind_host', default='localhost', help=_('Host for service broker')), cfg.StrOpt('bind_port', default='8083', help=_('Port for service broker')), cfg.StrOpt('auth_url', default='localhost:5000', help=_('Authentication URL')), cfg.StrOpt('user_domain_name', default='default', help=_('Domain name of the user')), cfg.StrOpt('project_domain_name', default='default', help=_('Domain name of the project')), cfg.StrOpt('packages_service', default='murano', help=_('Package service which should be used by service' ' broker'))] CONF = cfg.CONF CONF.register_opts(cfapi_opts, group='cfapi') def parse_args(args=None, usage=None, default_config_files=None): logging.register_options(CONF) CONF(args=args, project='murano', version=version.version_string, usage=usage, default_config_files=default_config_files) def set_middleware_defaults(): """Update default configuration options for oslo.middleware.""" cors.set_defaults( allow_headers=['X-Auth-Token', 'X-Openstack-Request-Id', 'X-Configuration-Session', 'X-Roles', 'X-User-Id', 'X-Tenant-Id'], expose_headers=['X-Auth-Token', 'X-Openstack-Request-Id', 'X-Configuration-Session', 'X-Roles', 'X-User-Id', 'X-Tenant-Id'], allow_methods=['GET', 'PUT', 'POST', 'DELETE', 'PATCH'] ) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/common/config.py0000664000175000017500000003162700000000000017543 0ustar00zuulzuul00000000000000# Copyright 2011 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. from keystoneauth1 import loading as ks_loading from oslo_config import cfg from oslo_log import log as logging from oslo_middleware import cors from oslo_policy import opts from murano.common.i18n import _ from murano import version paste_deploy_opts = [ cfg.StrOpt('flavor', help='Paste flavor'), cfg.StrOpt('config_file', help='Path to Paste config file'), ] bind_opts = [ cfg.HostAddressOpt('bind-host', default='0.0.0.0', help='Address to bind the Murano API server to.'), cfg.PortOpt('bind-port', default=8082, help='Port the bind the Murano API server to.'), ] rabbit_opts = [ cfg.HostAddressOpt('host', default='localhost', help='The RabbitMQ broker address which used for ' 'communication with Murano guest agents.'), cfg.PortOpt('port', default=5672, help='The RabbitMQ broker port.'), cfg.StrOpt('login', default='guest', help='The RabbitMQ login.'), cfg.StrOpt('password', default='guest', secret=True, help='The RabbitMQ password.'), cfg.StrOpt('virtual_host', default='/', help='The RabbitMQ virtual host.'), cfg.BoolOpt('ssl', default=False, help='Boolean flag to enable SSL communication through the ' 'RabbitMQ broker between murano-engine and guest agents.'), cfg.StrOpt('ssl_version', default='', help='SSL version to use (valid only if SSL enabled). ' 'Valid values are TLSv1 and SSLv23. SSLv2, SSLv3, ' 'TLSv1_1, and TLSv1_2 may be available on some ' 'distributions.'), cfg.StrOpt('ca_certs', default='', help='SSL cert file (valid only if SSL enabled).'), cfg.BoolOpt('insecure', default=False, help='This option explicitly allows Murano to perform ' '"insecure" SSL connections to RabbitMQ'), ] heat_opts = [ cfg.StrOpt('url', help='Optional heat endpoint override'), cfg.StrOpt('endpoint_type', default='publicURL', help='Heat endpoint type.'), cfg.ListOpt('stack_tags', default=['murano'], help='List of tags to be assigned to heat stacks created ' 'during environment deployment.') ] mistral_opts = [ cfg.StrOpt('url', help='Optional mistral endpoint override'), cfg.StrOpt('endpoint_type', default='publicURL', help='Mistral endpoint type.'), cfg.StrOpt('service_type', default='workflowv2', help='Mistral service type.') ] neutron_opts = [ cfg.StrOpt('url', help='Optional neutron endpoint override'), cfg.StrOpt('endpoint_type', default='publicURL', help='Neutron endpoint type.') ] murano_opts = [ cfg.StrOpt('url', help='Optional murano url in format ' 'like http://0.0.0.0:8082 used by Murano engine'), cfg.StrOpt('endpoint_type', default='publicURL', help='Murano endpoint type used by Murano engine.'), cfg.ListOpt('enabled_plugins', help="List of enabled Extension Plugins. " "Remove or leave commented to enable all installed " "plugins."), cfg.IntOpt('package_size_limit', default=5, help='Maximum application package size, Mb', deprecated_group='packages_opts'), cfg.IntOpt('limit_param_default', default=20, help='Default value for package pagination in API.', deprecated_group='packages_opts'), cfg.IntOpt('api_limit_max', default=100, help='Maximum number of packages to be returned in a single ' 'pagination request', deprecated_group='packages_opts'), cfg.IntOpt('api_workers', help=_('Number of API workers')), cfg.IntOpt('dsl_iterators_limit', default=2000, help=_('Maximum number of elements that can be iterated per ' 'object type.')), ] networking_opts = [ cfg.IntOpt('max_environments', default=250, help='Maximum number of environments that use a single router ' 'per tenant'), cfg.IntOpt('max_hosts', default=250, help='Maximum number of VMs per environment'), cfg.HostAddressOpt('env_ip_template', default='10.0.0.0', help='Template IP address for generating environment ' 'subnet cidrs'), cfg.ListOpt('default_dns', default=[], help='List of default DNS nameservers to be assigned to ' 'created Networks'), cfg.StrOpt('external_network', default='ext-net', help='ID or name of the external network for routers ' 'to connect to'), cfg.StrOpt('router_name', default='murano-default-router', help='Name of the router that going to be used in order to ' 'join all networks created by Murano'), cfg.BoolOpt('create_router', default=True, help='This option will create a router when one with ' '"router_name" does not exist'), cfg.StrOpt('network_config_file', default='netconfig.yaml', help='If provided networking configuration will be taken ' 'from this file'), cfg.StrOpt('driver', choices=['neutron', 'nova'], help='Network driver to use. Options are neutron or nova.' 'If not provided, the driver will be detected.'), ] stats_opts = [ cfg.IntOpt('period', default=5, help=_('Statistics collection interval in minutes.' 'Default value is 5 minutes.')), cfg.IntOpt('env_audit_period', default=60, help=_('Environment audit interval in minutes. ' 'Default value is 60 minutes.')), cfg.BoolOpt('env_audit_enabled', default=True, help=_('Whether environment audit events enabled')) ] engine_opts = [ cfg.BoolOpt('disable_murano_agent', default=False, help=_('Disallow the use of murano-agent')), cfg.StrOpt('class_configs', default='/etc/murano/class-configs', help=_('Path to class configuration files')), cfg.BoolOpt('use_trusts', default=True, help=_("Create resources using trust token rather " "than user's token")), cfg.BoolOpt('enable_model_policy_enforcer', default=False, help=_('Enable model policy enforcer using Congress')), cfg.IntOpt('agent_timeout', default=3600, help=_('Time for waiting for a response from murano agent ' 'during the deployment')), cfg.IntOpt('engine_workers', deprecated_opts=[cfg.DeprecatedOpt('workers', group='engine')], help=_('Number of engine workers')), cfg.ListOpt('load_packages_from', default=[], help=_('List of directories to load local packages from. ' 'If not provided, packages will be loaded only API'), deprecated_group='packages_opts'), cfg.StrOpt('packages_cache', help='Location (directory) for Murano package cache.', deprecated_group='packages_opts'), cfg.BoolOpt('enable_packages_cache', default=True, help=_('Enables murano-engine to persist on disk ' 'packages downloaded during deployments. ' 'The packages would be re-used for consequent ' 'deployments.'), deprecated_group='packages_opts'), cfg.StrOpt('packages_service', default='murano', help=_('The service to store murano packages: murano (stands ' 'for legacy behavior using murano-api) or glance ' '(stands for glance-glare artifact service)'), deprecated_group='packages_opts'), cfg.StrOpt('signing_key', default='~/.ssh/id_rsa', help=_('Path to RSA key for agent message signing')), cfg.StrOpt('agent_source', default='murano-agent', help=_('pip URL/package spec for murano-agent')), ] # TODO(sjmc7): move into engine opts? metadata_dir = [ cfg.StrOpt('metadata-dir', default='./meta', help='Metadata dir') ] glare_opts = [ cfg.StrOpt('url', help='Optional glare url in format ' 'like http://0.0.0.0:9494 used by Glare API', deprecated_group='glance'), cfg.StrOpt('endpoint_type', default='publicURL', help='Glare endpoint type.', deprecated_group='glance') ] glance_opts = [ cfg.StrOpt('url', help='Optional glance endpoint override'), cfg.StrOpt('endpoint_type', default='publicURL', help='Glance endpoint type.') ] file_server = [ cfg.StrOpt('file_server', default='', help='Set a file server.') ] home_region = cfg.StrOpt( 'home_region', help="Default region name used to get services endpoints.") # Unfortuntely we cannot use murano_auth.auth_url, since it # is private to the actual authentication plugin used. murano_auth_opts = [ cfg.StrOpt('www_authenticate_uri', help='Identity API endpoint for authenticating with tokens.')] CONF = cfg.CONF CONF.register_opts(paste_deploy_opts, group='paste_deploy') CONF.register_cli_opts(bind_opts) CONF.register_opts(rabbit_opts, group='rabbitmq') CONF.register_opts(heat_opts, group='heat') CONF.register_opts(mistral_opts, group='mistral') CONF.register_opts(neutron_opts, group='neutron') CONF.register_opts(murano_opts, group='murano') CONF.register_opts(engine_opts, group='engine') CONF.register_opts(file_server) CONF.register_opt(home_region) CONF.register_cli_opts(metadata_dir) CONF.register_opts(stats_opts, group='stats') CONF.register_opts(networking_opts, group='networking') CONF.register_opts(glare_opts, group='glare') CONF.register_opts(glance_opts, group='glance') CONF.register_opts(murano_auth_opts, group='murano_auth') ks_loading.register_auth_conf_options(CONF, group='murano_auth') for group in ('heat', 'mistral', 'neutron', 'glance', 'glare', 'murano', 'murano_auth'): ks_loading.register_session_conf_options( CONF, group=group, deprecated_opts={ 'cafile': [cfg.DeprecatedOpt('cacert', group), cfg.DeprecatedOpt('ca_file', group)], 'certfile': [cfg.DeprecatedOpt('cert_file', group)], 'keyfile': [cfg.DeprecatedOpt('key_file', group)] }) def parse_args(args=None, usage=None, default_config_files=None): logging.register_options(CONF) CONF(args=args, project='murano', version=version.version_string, usage=usage, default_config_files=default_config_files) def set_lib_defaults(): """Update default value for configuration options from other namespace. Example, oslo lib config options. This is needed for config generator tool to pick these default value changes. https://docs.openstack.org/oslo.config/latest/cli/ generator.html#modifying-defaults-from-other-namespaces """ set_middleware_defaults() # TODO(gmann): Remove setting the default value of config policy_file # once oslo_policy change the default value to 'policy.yaml'. # https://github.com/openstack/oslo.policy/blob/a626ad12fe5a3abd49d70e3e5b95589d279ab578/oslo_policy/opts.py#L49 opts.set_defaults(CONF, 'policy.yaml') def set_middleware_defaults(): """Update default configuration options for oslo.middleware.""" cors.set_defaults( allow_headers=['X-Auth-Token', 'X-Openstack-Request-Id', 'X-Configuration-Session', 'X-Roles', 'X-User-Id', 'X-Tenant-Id'], expose_headers=['X-Auth-Token', 'X-Openstack-Request-Id', 'X-Configuration-Session', 'X-Roles', 'X-User-Id', 'X-Tenant-Id'], allow_methods=['GET', 'PUT', 'POST', 'DELETE', 'PATCH'] ) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/common/consts.py0000664000175000017500000000143400000000000017600 0ustar00zuulzuul00000000000000# 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. CATEGORIES = ['Web', 'Databases', 'Message Queue', 'Application Servers', 'Microsoft Services', 'Load Balancers', 'Big Data', 'Key-Value Storage', 'SAP'] ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/common/engine.py0000664000175000017500000003210500000000000017533 0ustar00zuulzuul00000000000000# 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 copy import traceback import uuid import eventlet.debug from oslo_config import cfg from oslo_log import log as logging from oslo_messaging import target from oslo_serialization import jsonutils from oslo_service import service from murano.common import auth_utils from murano.common.helpers import token_sanitizer from murano.common.plugins import extensions_loader from murano.common import rpc from murano.dsl import context_manager from murano.dsl import dsl_exception from murano.dsl import executor as dsl_executor from murano.dsl import helpers from murano.dsl import schema_generator from murano.dsl import serializer from murano.engine import execution_session from murano.engine import package_loader from murano.engine.system import status_reporter from murano.engine.system import yaql_functions from murano.policy import model_policy_enforcer as enforcer CONF = cfg.CONF PLUGIN_LOADER = None LOG = logging.getLogger(__name__) eventlet.debug.hub_exceptions(False) # noinspection PyAbstractClass class EngineService(service.Service): def __init__(self): super(EngineService, self).__init__() self.server = None def start(self): if not rpc.initialized(): rpc.init() endpoints = [ TaskProcessingEndpoint(), StaticActionEndpoint(), SchemaEndpoint() ] s_target = target.Target('murano', 'tasks', server=str(uuid.uuid4())) self.server = rpc.get_server(s_target, endpoints, executor='eventlet') self.server.start() super(EngineService, self).start() def stop(self, graceful=False): if self.server: self.server.stop() if graceful: self.server.wait() super(EngineService, self).stop() def reset(self): if self.server: self.server.reset() super(EngineService, self).reset() def get_plugin_loader(): global PLUGIN_LOADER if PLUGIN_LOADER is None: PLUGIN_LOADER = extensions_loader.PluginLoader() return PLUGIN_LOADER class ContextManager(context_manager.ContextManager): def create_root_context(self, runtime_version): root_context = super(ContextManager, self).create_root_context( runtime_version) return helpers.link_contexts( root_context, yaql_functions.get_context(runtime_version)) def create_package_context(self, package): context = super(ContextManager, self).create_package_context( package) if package.name == 'io.murano': context = helpers.link_contexts( context, yaql_functions.get_restricted_context()) return context class SchemaEndpoint(object): @classmethod def generate_schema(cls, context, *args, **kwargs): session = execution_session.ExecutionSession() session.token = context['token'] session.project_id = context['project_id'] with package_loader.CombinedPackageLoader(session) as pkg_loader: return schema_generator.generate_schema( pkg_loader, ContextManager(), *args, **kwargs) class TaskProcessingEndpoint(object): @classmethod def handle_task(cls, context, task): result = cls.execute(task) rpc.api().process_result(result, task['id']) @staticmethod def execute(task): s_task = token_sanitizer.TokenSanitizer().sanitize(task) LOG.info('Starting processing task: {task_desc}'.format( task_desc=jsonutils.dumps(s_task))) result = None reporter = status_reporter.StatusReporter(task['id']) try: task_executor = TaskExecutor(task, reporter) result = task_executor.execute() return result finally: s_result = token_sanitizer.TokenSanitizer().sanitize(result) LOG.info('Finished processing task: {task_desc}'.format( task_desc=jsonutils.dumps(s_result))) class StaticActionEndpoint(object): @classmethod def call_static_action(cls, context, task): s_task = token_sanitizer.TokenSanitizer().sanitize(task) LOG.info('Starting execution of static action: ' '{task_desc}'.format(task_desc=jsonutils.dumps(s_task))) result = None reporter = status_reporter.StatusReporter(task['id']) try: task_executor = StaticActionExecutor(task, reporter) result = task_executor.execute() return result finally: LOG.info('Finished execution of static action: ' '{task_desc}'.format(task_desc=jsonutils.dumps(result))) class TaskExecutor(object): @property def action(self): return self._action @property def session(self): return self._session @property def model(self): return self._model def __init__(self, task, reporter=None): if reporter is None: reporter = status_reporter.StatusReporter(task['id']) self._action = task.get('action') self._model = task['model'] self._session = execution_session.ExecutionSession() self._session.token = task['token'] self._session.project_id = task['project_id'] self._session.user_id = task['user_id'] self._session.environment_owner_project_id = self._model['project_id'] self._session.environment_owner_user_id = self._model['user_id'] self._session.system_attributes = self._model.get('SystemData', {}) self._reporter = reporter self._model_policy_enforcer = enforcer.ModelPolicyEnforcer( self._session) def execute(self): try: self._create_trust() except Exception as e: return self.exception_result(e, None, '') with package_loader.CombinedPackageLoader(self._session) as pkg_loader: pkg_loader.import_fixation_table( self._session.system_attributes.get('Packages', {})) result = self._execute(pkg_loader) self._session.system_attributes[ 'Packages'] = pkg_loader.export_fixation_table() self._model['SystemData'] = self._session.system_attributes self._model['project_id'] = self._session.environment_owner_project_id self._model['user_id'] = self._session.environment_owner_user_id result['model'] = self._model if (not self._model.get('Objects') and not self._model.get('ObjectsCopy')): try: self._delete_trust() except Exception: LOG.warning('Cannot delete trust', exc_info=True) return result def _execute(self, pkg_loader): get_plugin_loader().register_in_loader(pkg_loader) with dsl_executor.MuranoDslExecutor( pkg_loader, ContextManager(), self.session) as executor: try: obj = executor.load(self.model) except Exception as e: return self.exception_result(e, None, '') if obj is not None: try: self._validate_model(obj.object, pkg_loader, executor) except Exception as e: return self.exception_result(e, obj, '') try: LOG.debug('Invoking pre-cleanup hooks') self.session.start() executor.object_store.cleanup() except Exception as e: return self.exception_result(e, obj, '') finally: LOG.debug('Invoking post-cleanup hooks') self.session.finish() self._model['ObjectsCopy'] = \ copy.deepcopy(self._model.get('Objects')) action_result = None if self.action: try: LOG.debug('Invoking pre-execution hooks') self.session.start() action_result = self._invoke(executor) except Exception as e: return self.exception_result(e, obj, self.action['method']) finally: LOG.debug('Invoking post-execution hooks') self.session.finish() self._model = executor.finalize(obj) try: action_result = serializer.serialize(action_result, executor) except Exception as e: return self.exception_result(e, None, '') pkg_loader.compact_fixation_table() return { 'action': { 'result': action_result, 'isException': False } } def exception_result(self, exception, root, method_name): if isinstance(exception, dsl_exception.MuranoPlException): LOG.error('\n' + exception.format(prefix=' ')) exception_traceback = exception.format() else: exception_traceback = traceback.format_exc() LOG.exception( ("Exception %(exc)s occurred" " during invocation of %(method)s"), {'exc': exception, 'method': method_name}) self._reporter.report_error(root, str(exception)) return { 'action': { 'isException': True, 'result': { 'message': str(exception), 'details': exception_traceback } } } def _validate_model(self, obj, pkg_loader, executor): if CONF.engine.enable_model_policy_enforcer: if obj is not None: with helpers.with_object_store(executor.object_store): self._model_policy_enforcer.modify(obj, pkg_loader) self._model_policy_enforcer.validate(obj.to_dictionary(), pkg_loader) def _invoke(self, mpl_executor): obj = mpl_executor.object_store.get(self.action['object_id']) method_name, kwargs = self.action['method'], self.action['args'] if obj is not None: return mpl_executor.run(obj.type, method_name, obj, (), kwargs) def _create_trust(self): if not CONF.engine.use_trusts: return trust_id = self._session.system_attributes.get('TrustId') if not trust_id: trust_id = auth_utils.create_trust( self._session.token, self._session.project_id) self._session.system_attributes['TrustId'] = trust_id self._session.trust_id = trust_id def _delete_trust(self): trust_id = self._session.trust_id if trust_id: auth_utils.delete_trust(self._session) self._session.system_attributes['TrustId'] = None self._session.trust_id = None class StaticActionExecutor(object): @property def action(self): return self._action @property def session(self): return self._session def __init__(self, task, reporter=None): if reporter is None: reporter = status_reporter.StatusReporter(task['id']) self._action = task['action'] self._session = execution_session.ExecutionSession() self._session.token = task['token'] self._session.project_id = task['project_id'] self._session.user_id = task['user_id'] self._reporter = reporter self._model_policy_enforcer = enforcer.ModelPolicyEnforcer( self._session) def execute(self): with package_loader.CombinedPackageLoader(self._session) as pkg_loader: get_plugin_loader().register_in_loader(pkg_loader) executor = dsl_executor.MuranoDslExecutor(pkg_loader, ContextManager()) action_result = self._invoke(executor) action_result = serializer.serialize(action_result, executor) return action_result def _invoke(self, mpl_executor): class_name = self.action['class_name'] pkg_name = self.action['pkg_name'] class_version = self.action['class_version'] version_spec = helpers.parse_version_spec(class_version) if pkg_name: package = mpl_executor.package_loader.load_package( pkg_name, version_spec) else: package = mpl_executor.package_loader.load_class_package( class_name, version_spec) cls = package.find_class(class_name, search_requirements=False) method_name, kwargs = self.action['method'], self.action['args'] return mpl_executor.run(cls, method_name, None, (), kwargs) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/common/exceptions.py0000664000175000017500000000400200000000000020442 0ustar00zuulzuul00000000000000# 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. _FATAL_EXCEPTION_FORMAT_ERRORS = False # Exceptions from openstack-common class Error(Exception): def __init__(self, message=None): super(Error, self).__init__(message) class OpenstackException(Exception): """Base Exception class. To correctly use this class, inherit from it and define a 'msg_fmt' property. That message will get printf'd with the keyword arguments provided to the constructor. """ msg_fmt = "An unknown exception occurred" def __init__(self, **kwargs): try: self._error_string = self.msg_fmt % kwargs except Exception: if _FATAL_EXCEPTION_FORMAT_ERRORS: raise else: # at least get the io.murano message out if something happened self._error_string = self.msg_fmt def __str__(self): return self._error_string class UnsupportedContentType(OpenstackException): msg_fmt = "Unsupported content type %(content_type)s" class NotAcceptableContentType(OpenstackException): msg_fmt = ("Response with content type %(content_type)s " "expected but can not be provided") class MalformedRequestBody(OpenstackException): msg_fmt = "Malformed message body: %(reason)s" # Murano exceptions class TimeoutException(Exception): pass class PolicyViolationException(Exception): pass class RouterInfoException(Exception): pass ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1696417899.7811809 murano-16.0.0/murano/common/helpers/0000775000175000017500000000000000000000000017355 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/common/helpers/__init__.py0000664000175000017500000000000000000000000021454 0ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/common/helpers/path.py0000664000175000017500000000217700000000000020672 0ustar00zuulzuul00000000000000# Copyright (c) 2017 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.path def secure_join(*parts): """Secure version of os.path.join(*parts) Joins pathname components and ensures that with each join the result is a subdirectory of the previous join """ new = prev = "" for part in parts: new = os.path.normpath(os.path.join(prev, part)) if len(new) <= len(prev) or prev != "" and not new.startswith( prev + os.path.sep): raise ValueError('path {0} is not allowed {1}'.format( os.path.join(*parts), parts)) prev = new return new ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/common/helpers/token_sanitizer.py0000664000175000017500000000421400000000000023140 0ustar00zuulzuul00000000000000# 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. class TokenSanitizer(object): """Helper class for cleaning some object from different passwords/tokens Simply searches attribute with `look a like` name as one of the token and replace it value with message. """ def __init__(self, tokens=('token', 'pass', 'trustid'), message='*** SANITIZED ***'): """Init method of TokenSanitizer :param tokens: iterable with tokens :param message: string by which each token going to be replaced """ self._tokens = tokens self._message = message @property def tokens(self): """Iterable with tokens that should be sanitized.""" return self._tokens @property def message(self): """String by which each token going to be replaced.""" return self._message def _contains_token(self, value): for token in self.tokens: if token in value.lower(): return True return False def sanitize(self, obj): """Replaces each token found in object by message. :param obj: dict, list, tuple, object :return: Sanitized object """ if isinstance(obj, dict): return dict([self.sanitize(item) for item in obj.items()]) elif isinstance(obj, list): return [self.sanitize(item) for item in obj] elif isinstance(obj, tuple): k, v = obj if self._contains_token(k) and isinstance(v, str): return k, self.message return k, self.sanitize(v) else: return obj ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/common/i18n.py0000664000175000017500000000151600000000000017047 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. """oslo.i18n integration module. See https://docs.openstack.org/oslo.i18n/latest/user/usage.html """ import oslo_i18n _translators = oslo_i18n.TranslatorFactory(domain='murano') # The primary translation function using the well-known name "_" _ = _translators.primary ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1696417899.7851808 murano-16.0.0/murano/common/messaging/0000775000175000017500000000000000000000000017670 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/common/messaging/__init__.py0000664000175000017500000000000000000000000021767 0ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/common/messaging/message.py0000664000175000017500000000302400000000000021665 0ustar00zuulzuul00000000000000# 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 oslo_log import log as logging from oslo_serialization import jsonutils LOG = logging.getLogger("murano-common.messaging") class Message(object): def __init__(self, connection=None, message_handle=None): self._body = None self._connection = connection self._message_handle = message_handle self.id = None if message_handle is None else \ message_handle.properties.get('message_id') try: self.body = None if message_handle is None else \ jsonutils.loads(message_handle.body) except ValueError as e: self.body = None LOG.exception(e) @property def body(self): return self._body @body.setter def body(self, value): self._body = value @property def id(self): return self._id @id.setter def id(self, value): self._id = value or '' def ack(self): self._message_handle.ack() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/common/messaging/mqclient.py0000664000175000017500000001006000000000000022053 0ustar00zuulzuul00000000000000# 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 ssl as ssl_module from eventlet import patcher from oslo_serialization import jsonutils from oslo_service import sslutils from murano.common.i18n import _ from murano.common.messaging import subscription kombu = patcher.import_patched('kombu') class MqClient(object): def __init__(self, login, password, host, port, virtual_host, ssl=False, ssl_version=None, ca_certs=None, insecure=False): ssl_params = None if ssl: cert_reqs = ssl_module.CERT_REQUIRED if insecure: if ca_certs: cert_reqs = ssl_module.CERT_OPTIONAL else: cert_reqs = ssl_module.CERT_NONE ssl_params = { 'ca_certs': ca_certs, 'cert_reqs': cert_reqs } if ssl_version: key = ssl_version.lower() try: ssl_params['ssl_version'] = sslutils._SSL_PROTOCOLS[key] except KeyError: raise RuntimeError( _("Invalid SSL version: %s") % ssl_version) self._connection = kombu.Connection( 'amqp://{0}:{1}@{2}:{3}/{4}'.format( login, password, host, port, virtual_host ), ssl=ssl_params ) self._channel = None self._connected = False def __enter__(self): self.connect() return self def __exit__(self, exc_type, exc_val, exc_tb): self.close() return False def connect(self): self._connection.connect() self._channel = self._connection.channel() self._connected = True def close(self): self._connection.close() self._connected = False def declare(self, queue, exchange='', enable_ha=False, ttl=0): if not self._connected: raise RuntimeError('Not connected to RabbitMQ') queue_arguments = {} if enable_ha is True: # To use mirrored queues feature in RabbitMQ 2.x # we need to declare this policy on the queue itself. # # Warning: this option has no effect on RabbitMQ 3.X, # to enable mirrored queues feature in RabbitMQ 3.X, please # configure RabbitMQ. queue_arguments['x-ha-policy'] = 'all' if ttl > 0: queue_arguments['x-expires'] = ttl exchange = kombu.Exchange(exchange, type='direct', durable=True) queue = kombu.Queue(queue, exchange, queue, durable=True, queue_arguments=queue_arguments) bound_queue = queue(self._connection) bound_queue.declare() def send(self, message, key, exchange='', signing_func=None): if not self._connected: raise RuntimeError('Not connected to RabbitMQ') producer = kombu.Producer(self._connection) data = jsonutils.dumps(message.body) headers = None if signing_func: headers = {'signature': signing_func(data)} producer.publish( exchange=str(exchange), routing_key=str(key), body=data, message_id=str(message.id), headers=headers ) def open(self, queue, prefetch_count=1): if not self._connected: raise RuntimeError('Not connected to RabbitMQ') return subscription.Subscription( self._connection, queue, prefetch_count) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/common/messaging/subscription.py0000664000175000017500000000424500000000000022773 0ustar00zuulzuul00000000000000# 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 socket import time from eventlet import patcher from murano.common.messaging import message kombu = patcher.import_patched('kombu') class Subscription(object): def __init__(self, connection, queue, prefetch_count=1): self._buffer = collections.deque() self._connection = connection self._queue = kombu.Queue(name=queue, exchange=None) self._consumer = kombu.Consumer(self._connection, auto_declare=False) self._consumer.register_callback(self._receive) self._consumer.qos(prefetch_count=prefetch_count) def __enter__(self): self._consumer.add_queue(self._queue) self._consumer.consume() return self def __exit__(self, exc_type, exc_val, exc_tb): if self._consumer is not None: self._consumer.cancel() return False def get_message(self, timeout=None): msg_handle = self._get(timeout=timeout) if msg_handle is None: return None return message.Message(self._connection, msg_handle) def _get(self, timeout=None): elapsed = 0.0 remaining = timeout while True: time_start = time.time() if self._buffer: return self._buffer.pop() try: self._connection.drain_events(timeout=timeout and remaining) except socket.timeout: return None elapsed += time.time() - time_start remaining = timeout and timeout - elapsed or None def _receive(self, message_data, message): self._buffer.append(message) ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1696417899.7851808 murano-16.0.0/murano/common/plugins/0000775000175000017500000000000000000000000017374 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/common/plugins/__init__.py0000664000175000017500000000000000000000000021473 0ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/common/plugins/extensions_loader.py0000664000175000017500000001254200000000000023477 0ustar00zuulzuul00000000000000# 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 inspect import re from oslo_config import cfg from oslo_log import log as logging from stevedore import dispatch from murano.dsl import murano_package CONF = cfg.CONF LOG = logging.getLogger(__name__) # regexp validator to ensure that the entry-point name is a valid MuranoPL # class name with an optional namespace name NAME_RE = re.compile(r'^[a-zA-Z]\w*(\.[a-zA-Z]\w*)*$') class PluginLoader(object): def __init__(self, namespace="io.murano.extensions"): LOG.info('Loading extension plugins') self.namespace = namespace extension_manager = dispatch.EnabledExtensionManager( self.namespace, PluginLoader.is_plugin_enabled, on_load_failure_callback=PluginLoader._on_load_failure) self.packages = {} name_map = {} for ext in extension_manager.extensions: self.load_extension(ext, name_map) self.cleanup_duplicates(name_map) def load_extension(self, extension, name_map): dist_name = str(extension.entry_point.dist) name = extension.entry_point.name if not NAME_RE.match(name): LOG.warning("Entry-point 'name' {name} is invalid".format( name=name)) return name_map.setdefault(name, []).append(dist_name) if dist_name in self.packages: package = self.packages[dist_name] else: package = PackageDefinition(extension.entry_point.dist) self.packages[dist_name] = package plugin = extension.plugin try: package.classes[name] = initialize_plugin(plugin) except Exception: LOG.exception("Unable to initialize plugin for {name}".format( name=name)) return LOG.info("Loaded class {class_name} from {dist}".format( class_name=name, dist=dist_name)) def cleanup_duplicates(self, name_map): for class_name, package_names in name_map.items(): if len(package_names) >= 2: LOG.warning("Class is defined in multiple packages!") for package_name in package_names: LOG.warning( "Disabling class {class_name} in {dist} due to " "conflict".format( class_name=class_name, dist=package_name)) self.packages[package_name].classes.pop(class_name) @staticmethod def is_plugin_enabled(extension): if CONF.murano.enabled_plugins is None: # assume all plugins are enabled until manually specified otherwise return True else: return (extension.entry_point.dist.project_name in CONF.murano.enabled_plugins) @staticmethod def _on_load_failure(manager, ep, exc): LOG.warning("Error loading entry-point {ep} from package {dist}: " "{err}".format(ep=ep.name, dist=ep.dist, err=exc)) def register_in_loader(self, package_loader): for package in self.packages.values(): package_loader.register_package( MuranoPackage(package_loader, package)) def initialize_plugin(plugin): if hasattr(plugin, "init_plugin"): initializer = getattr(plugin, "init_plugin") if inspect.ismethod(initializer) and initializer.__self__ is plugin: LOG.debug("Initializing plugin class {name}".format(name=plugin)) initializer() return plugin class PackageDefinition(object): def __init__(self, distribution): self.name = distribution.project_name self.version = distribution.version if distribution.has_metadata(distribution.PKG_INFO): # This has all the package metadata, including Author, # description, License etc self.info = distribution.get_metadata(distribution.PKG_INFO) else: self.info = None self.classes = {} class MuranoPackage(murano_package.MuranoPackage): def __init__(self, pkg_loader, package_definition): super(MuranoPackage, self).__init__( pkg_loader, package_definition.name, runtime_version='1.0') for class_name, clazz in package_definition.classes.items(): if hasattr(clazz, "_murano_class_name"): LOG.warning("Class '%(class_name)s' has a MuranoPL " "name '%(name)s' defined which will be " "ignored" % dict(class_name=class_name, name=getattr(clazz, "_murano_class_name"))) LOG.debug("Registering '%s' from '%s' in class loader", class_name, package_definition.name) self.register_class(clazz, class_name) def get_resource(self, name): raise NotImplementedError() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/common/plugins/package_types_loader.py0000664000175000017500000000642100000000000024116 0ustar00zuulzuul00000000000000# 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 semantic_version from oslo_config import cfg from oslo_log import log as logging from stevedore import dispatch CONF = cfg.CONF LOG = logging.getLogger(__name__) NAMESPACE = 'io.murano.plugins.packages' class PluginLoader(object): def __init__(self): LOG.info('Loading package type plugins') extension_manager = dispatch.EnabledExtensionManager( NAMESPACE, self._is_plugin_enabled, on_load_failure_callback=self._on_load_failure) self.formats = {} for ext in extension_manager.extensions: self._load_plugin(ext) def _load_plugin(self, extension): format_name = extension.entry_point.name self.register_format(format_name, extension.plugin) @staticmethod def _is_plugin_enabled(extension): if CONF.murano.enabled_plugins is None: # assume all plugins are enabled until manually specified otherwise return True else: return (extension.entry_point.dist.project_name in CONF.murano.enabled_plugins) @staticmethod def _on_load_failure(manager, ep, exc): LOG.warning("Error loading entry-point {ep} from package {dist}: " "{err}".format(ep=ep.name, dist=ep.dist, err=exc)) @staticmethod def _parse_format_string(format_string): parts = format_string.rsplit('/', 1) if len(parts) != 2: LOG.error("Incorrect format name {name}".format( name=format_string)) raise ValueError(format_string) return ( parts[0].strip(), semantic_version.Version.coerce(parts[1]) ) def register_format(self, format_name, package_class): try: name, version = self._parse_format_string(format_name) except ValueError: return else: self._initialize_plugin(package_class) self.formats.setdefault(name, {})[version] = package_class LOG.info('Plugin for "{0}" package type was loaded'.format( format_name)) def get_package_handler(self, format_name): format_name, runtime_version = self._parse_format_string(format_name) package_class = self.formats.get(format_name, {}).get( runtime_version) if package_class is None: return None return lambda *args, **kwargs: package_class( format_name, runtime_version, *args, **kwargs) @staticmethod def _initialize_plugin(plugin): if hasattr(plugin, "init_plugin"): initializer = getattr(plugin, "init_plugin") LOG.debug("Initializing plugin class {name}".format(name=plugin)) initializer() ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1696417899.7851808 murano-16.0.0/murano/common/policies/0000775000175000017500000000000000000000000017522 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/common/policies/__init__.py0000664000175000017500000000226600000000000021641 0ustar00zuulzuul00000000000000# Copyright 2017 AT&T Corporation. # 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. import itertools from murano.common.policies import action from murano.common.policies import base from murano.common.policies import category from murano.common.policies import deployment from murano.common.policies import env_template from murano.common.policies import environment from murano.common.policies import package def list_rules(): return itertools.chain( base.list_rules(), action.list_rules(), category.list_rules(), deployment.list_rules(), environment.list_rules(), env_template.list_rules(), package.list_rules() ) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/common/policies/action.py0000664000175000017500000000247000000000000021354 0ustar00zuulzuul00000000000000# Copyright 2017 AT&T Corporation. # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from oslo_policy import policy from murano.common.policies import base action_policies = [ policy.DocumentedRuleDefault( name='execute_action', check_str=base.RULE_DEFAULT, description="""Excute an available action on a deployed environment, retrieve the task status of an executed action, or retrieve the result of an executed static action.""", operations=[ {'path': 'v1/environments/{environment_id}/actions/{action_id}', 'method': 'POST'}, {'path': 'v1/environments/{environment_id}/actions/{task_id}', 'method': 'GET'}, {'path': 'v1/actions', 'method': 'POST'}]) ] def list_rules(): return action_policies ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/common/policies/base.py0000664000175000017500000000174700000000000021017 0ustar00zuulzuul00000000000000# Copyright 2017 AT&T Corporation. # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from oslo_policy import policy RULE_ADMIN_API = 'rule:admin_api' RULE_DEFAULT = 'rule:default' rules = [ policy.RuleDefault( name='context_is_admin', check_str='role:admin'), policy.RuleDefault( name='admin_api', check_str='is_admin:True'), policy.RuleDefault( name='default', check_str='') ] def list_rules(): return rules ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/common/policies/category.py0000664000175000017500000000311600000000000021712 0ustar00zuulzuul00000000000000# Copyright 2017 AT&T Corporation. # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from oslo_policy import policy from murano.common.policies import base category_policies = [ policy.DocumentedRuleDefault( name='get_category', check_str=base.RULE_DEFAULT, description="""Show category details or list all categories in the application catalog.""", operations=[{'path': '/v1/catalog/categories/{category_id}', 'method': 'GET'}, {'path': '/v1/catalog/categories', 'method': 'GET'}]), policy.DocumentedRuleDefault( name='delete_category', check_str=base.RULE_ADMIN_API, description='Delete a category.', operations=[{'path': '/v1/catalog/categories/{category_id}', 'method': 'DELETE'}]), policy.DocumentedRuleDefault( name='add_category', check_str=base.RULE_ADMIN_API, description='Create a category.', operations=[{'path': '/v1/catalog/categories', 'method': 'POST'}]) ] def list_rules(): return category_policies ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/common/policies/deployment.py0000664000175000017500000000313200000000000022253 0ustar00zuulzuul00000000000000# Copyright 2017 AT&T Corporation. # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from oslo_policy import policy from murano.common.policies import base deployment_policies = [ policy.DocumentedRuleDefault( name='list_deployments', check_str=base.RULE_DEFAULT, description='List deployments for an environment.', operations=[{'path': '/v1/environments/{env_id}/deployments', 'method': 'GET'}]), policy.DocumentedRuleDefault( name='list_deployments_all_environments', check_str=base.RULE_DEFAULT, description='List deployments for all environments in a project.', operations=[{'path': '/v1/deployments', 'method': 'GET'}]), policy.DocumentedRuleDefault( name='statuses_deployments', check_str=base.RULE_DEFAULT, description='Show deployment status details for a deployment.', operations=[{ 'path': '/v1/environments/{env_id}/deployments/{deployment_id}', 'method': 'GET'}]) ] def list_rules(): return deployment_policies ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/common/policies/env_template.py0000664000175000017500000000443700000000000022567 0ustar00zuulzuul00000000000000# Copyright 2017 AT&T Corporation. # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from oslo_policy import policy from murano.common.policies import base template_policies = [ policy.DocumentedRuleDefault( name='list_env_templates', check_str=base.RULE_DEFAULT, description='List environment templates in a project.', operations=[{'path': '/v1/templates', 'method': 'GET'}]), policy.DocumentedRuleDefault( name='create_env_template', check_str=base.RULE_DEFAULT, description='Create an environment template.', operations=[{'path': '/v1/templates', 'method': 'POST'}]), policy.DocumentedRuleDefault( name='show_env_template', check_str=base.RULE_DEFAULT, description='Show environment template details.', operations=[{'path': '/v1/templates/{env_template_id}', 'method': 'GET'}]), policy.DocumentedRuleDefault( name='update_env_template', check_str=base.RULE_DEFAULT, description='Update an environment template.', operations=[{'path': '/v1/templates/{env_template_id}', 'method': 'PUT'}]), policy.DocumentedRuleDefault( name='delete_env_template', check_str=base.RULE_DEFAULT, description='Delete an environment template.', operations=[{'path': '/v1/templates/{env_template_id}', 'method': 'DELETE'}]), policy.DocumentedRuleDefault( name='clone_env_template', check_str=base.RULE_DEFAULT, description='Clone an environment template.', operations=[{'path': '/v1/templates/{env_template_id}/clone', 'method': 'POST'}]) ] def list_rules(): return template_policies ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/common/policies/environment.py0000664000175000017500000000542000000000000022441 0ustar00zuulzuul00000000000000# Copyright 2017 AT&T Corporation. # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from oslo_policy import policy from murano.common.policies import base environment_policies = [ policy.DocumentedRuleDefault( name='list_environments', check_str=base.RULE_DEFAULT, description='List environments in a project.', operations=[{'path': '/v1/environments', 'method': 'GET'}]), policy.DocumentedRuleDefault( name='list_environments_all_tenants', check_str=base.RULE_ADMIN_API, description='List environments across all projects.', operations=[{'path': '/v1/environments?all_tenants=true', 'method': 'GET'}]), policy.DocumentedRuleDefault( name='show_environment', check_str=base.RULE_DEFAULT, description='Show details for an environment or shows the environment ' 'model.', operations=[{'path': '/v1/environments/{environment_id}', 'method': 'GET'}, {'path': '/v1/environments/{environment_id}/model', 'method': 'GET'}]), policy.DocumentedRuleDefault( name='update_environment', check_str=base.RULE_DEFAULT, description='Update or rename an environment.', operations=[{'path': '/v1/environments/{environment_id}', 'method': 'PUT'}, {'path': '/v1/environments/{environment_id}/model', 'method': 'PATCH'}]), policy.DocumentedRuleDefault( name='create_environment', check_str=base.RULE_DEFAULT, description='Create an environment or create an environment and ' 'session from an environment template.', operations=[ {'path': '/v1/environments/{environment_id}', 'method': 'POST'}, {'path': '/v1/templates/{env_template_id}/create-environment', 'method': 'POST'}]), policy.DocumentedRuleDefault( name='delete_environment', check_str=base.RULE_DEFAULT, description='Delete an environment.', operations=[{'path': '/v1/environments/{environment_id}', 'method': 'DELETE'}]) ] def list_rules(): return environment_policies ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/common/policies/package.py0000664000175000017500000000711400000000000021472 0ustar00zuulzuul00000000000000# Copyright 2017 AT&T Corporation. # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from oslo_policy import policy from murano.common.policies import base package_policies = [ policy.DocumentedRuleDefault( name='get_package', check_str=base.RULE_DEFAULT, description="""Returns either detailed package information or information specific to the package's UI or logo. In addition, checks for the existence of a given package.""", operations=[{'path': '/v1/catalog/packages/{package_id}', 'method': 'GET'}, {'path': '/v1/catalog/packages', 'method': 'GET'}, {'path': '/v1/catalog/packages/{package_id}/ui', 'method': 'GET'}, {'path': '/v1/catalog/packages/{package_id}/logo', 'method': 'GET'}]), policy.DocumentedRuleDefault( name='upload_package', check_str=base.RULE_DEFAULT, description='Upload a package to the application catalog.', operations=[{'path': '/v1/catalog/packages', 'method': 'POST'}]), policy.DocumentedRuleDefault( name='modify_package', check_str=base.RULE_DEFAULT, description='Update package information for a given package.', operations=[{'path': '/v1/catalog/packages/{package_id}', 'method': 'PATCH'}]), policy.DocumentedRuleDefault( name='publicize_package', check_str=base.RULE_ADMIN_API, description="""Publicize a package across all projects. Grants users in any project the ability to use the package. Enforced only when `is_public` parameter is set to True in the request body of the `update` or `upload` package request.""", operations=[{'path': '/v1/catalog/packages/{package_id}', 'method': 'PATCH'}, {'path': '/v1/catalog/packages', 'method': 'POST'}]), policy.DocumentedRuleDefault( name='manage_public_package', check_str=base.RULE_DEFAULT, description="""Either update, delete or check for the existence of a public package. Only enforced when the package is public.""", operations=[{'path': '/v1/catalog/packages/{package_id}', 'method': 'PATCH'}, {'path': '/v1/catalog/packages/{package_id}', 'method': 'DELETE'}, {'path': '/v1/catalog/packages', 'method': 'GET'}]), policy.DocumentedRuleDefault( name='delete_package', check_str=base.RULE_DEFAULT, description='Delete a given package.', operations=[{'path': '/v1/catalog/packages/{package_id}', 'method': 'DELETE'}]), policy.DocumentedRuleDefault( name='download_package', check_str=base.RULE_DEFAULT, description='Download a package from the application catalog.', operations=[{'path': '/v1/catalog/packages/{package_id}/download', 'method': 'GET'}]) ] def list_rules(): return package_policies ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/common/policy.py0000664000175000017500000001031200000000000017561 0ustar00zuulzuul00000000000000# Copyright (c) 2014 Hewlett-Packard Development Company, L.P. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. # Based on designate/policy.py from oslo_config import cfg from oslo_log import log as logging from oslo_policy import opts from oslo_policy import policy from webob import exc as exceptions from murano.common import policies LOG = logging.getLogger(__name__) CONF = cfg.CONF _ENFORCER = None # TODO(gmann): Remove setting the default value of config policy_file # once oslo_policy change the default value to 'policy.yaml'. # https://github.com/openstack/oslo.policy/blob/a626ad12fe5a3abd49d70e3e5b95589d279ab578/oslo_policy/opts.py#L49 DEFAULT_POLICY_FILE = 'policy.yaml' opts.set_defaults(CONF, DEFAULT_POLICY_FILE) def reset(): global _ENFORCER if _ENFORCER: _ENFORCER.clear() _ENFORCER = None def init(use_conf=True): global _ENFORCER if not _ENFORCER: LOG.debug("Enforcer is not present, recreating.") _ENFORCER = policy.Enforcer(CONF, use_conf=use_conf) register_rules(_ENFORCER) def set_rules(data, default_rule=None, overwrite=True): default_rule = default_rule or cfg.CONF.policy_default_rule if not _ENFORCER: LOG.debug("Enforcer not present, recreating at rules stage.") init() if default_rule: _ENFORCER.default_rule = default_rule LOG.debug("Loading rules {rules}, default: {def_rule}, overwrite: " "{overwrite}".format(rules=data, def_rule=default_rule, overwrite=overwrite)) if isinstance(data, dict): rules = policy.Rules.from_dict(data, default_rule) else: rules = policy.Rules.load_json(data, default_rule) _ENFORCER.set_rules(rules, overwrite=overwrite) def check(rule, ctxt, target=None, do_raise=True, exc=None): """Verify that the rule is valid on the target in this context. :param rule: String representing the action to be checked, which should be colon-separated for clarity. :param ctxt: Request context from which user credentials are extracted. :param target: Dictionary representing the object of the action for object creation; this should be a dictionary representing the location of the object, e.g. {'environment_id': object.environment_id} :param do_raise: Whether to raise an exception or not if the check fails. :param exc: Class of the exception to raise if the check fails. :raises exceptions.HTTPForbidden: If verification fails. Or if 'exc' is specified it will raise an exception of that type. """ init() if target is None: target = {} creds = ctxt.to_dict() if not exc: exc = exceptions.HTTPForbidden try: result = _ENFORCER.enforce(rule, target, creds, do_raise, exc) except Exception: result = False raise else: return result finally: if result: LOG.debug("Policy check succeeded for rule {rule} on target " "{target}".format(rule=rule, target=repr(target))) else: LOG.debug("Policy check failed for rule {rule} on target: " "{target}".format(rule=rule, target=repr(target))) def check_is_admin(context): """Check if the given context is associated with an admin role. :param context: Murano request context :returns: A non-False value if context role is admin. """ return check('context_is_admin', context, context.to_dict(), do_raise=False) def register_rules(enforcer): enforcer.register_defaults(policies.list_rules()) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/common/rpc.py0000664000175000017500000000643200000000000017056 0ustar00zuulzuul00000000000000# 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 oslo_config import cfg import oslo_messaging as messaging from oslo_messaging.rpc import dispatcher from oslo_messaging import target CONF = cfg.CONF NOTIFICATION_TRANSPORT = None TRANSPORT = None def init(): global TRANSPORT, NOTIFICATION_TRANSPORT TRANSPORT = messaging.get_rpc_transport(CONF) NOTIFICATION_TRANSPORT = messaging.get_notification_transport(CONF) def initialized(): return None not in [TRANSPORT, NOTIFICATION_TRANSPORT] def cleanup(): global TRANSPORT, NOTIFICATION_TRANSPORT if TRANSPORT is not None: TRANSPORT.cleanup() if NOTIFICATION_TRANSPORT is not None: NOTIFICATION_TRANSPORT.cleanup() TRANSPORT = NOTIFICATION_TRANSPORT = None def get_client(target, timeout=None): if TRANSPORT is None: init() return messaging.RPCClient( TRANSPORT, target, timeout=timeout ) def get_server(target, endpoints, executor): if TRANSPORT is None: init() access_policy = dispatcher.DefaultRPCAccessPolicy return messaging.get_rpc_server( TRANSPORT, target, endpoints, executor=executor, access_policy=access_policy ) def get_notification_listener(targets, endpoints, executor): if NOTIFICATION_TRANSPORT is None: init() return messaging.get_notification_listener( NOTIFICATION_TRANSPORT, targets, endpoints, executor=executor ) class ApiClient(object): def __init__(self): client_target = target.Target('murano', 'results') self._client = get_client(client_target, timeout=15) def process_result(self, result, environment_id): return self._client.call({}, 'process_result', result=result, environment_id=environment_id) class EngineClient(object): def __init__(self): client_target = target.Target('murano', 'tasks') self._client = get_client(client_target, timeout=15) def handle_task(self, task): return self._client.cast({}, 'handle_task', task=task) def call_static_action(self, task): return self._client.call({}, 'call_static_action', task=task) def generate_schema(self, credentials, class_name, method_names=None, class_version=None, package_name=None): return self._client.call( credentials, 'generate_schema', class_name=class_name, method_names=method_names, class_version=class_version, package_name=package_name ) def api(): if not initialized(): init() return ApiClient() def engine(): if not initialized(): init() return EngineClient() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/common/server.py0000664000175000017500000002222600000000000017577 0ustar00zuulzuul00000000000000# 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 uuid from oslo_config import cfg from oslo_log import log as logging from oslo_messaging import target from oslo_service import service from oslo_utils import timeutils import pytz from sqlalchemy import desc from murano.common.helpers import token_sanitizer from murano.common import rpc from murano.db import models from murano.db.services import environments from murano.db.services import instances from murano.db import session from murano.engine.system import status_reporter from murano.services import states CONF = cfg.CONF LOG = logging.getLogger(__name__) class ResultEndpoint(object): @staticmethod def process_result(context, result, environment_id): secure_result = token_sanitizer.TokenSanitizer().sanitize(result) LOG.debug('Got result from orchestration ' 'engine:\n{result}'.format(result=secure_result)) model = result['model'] action_result = result.get('action', {}) unit = session.get_session() environment = unit.query(models.Environment).get(environment_id) if not environment: LOG.warning('Environment result could not be handled, ' 'specified environment not found in database') return if model['Objects'] is None and model.get('ObjectsCopy', {}) is None: environments.EnvironmentServices.remove(environment_id) return environment.description = model if environment.description['Objects'] is not None: environment.description['Objects']['services'] = \ environment.description['Objects'].pop('applications', []) action_name = 'Deployment' deleted = False else: action_name = 'Deletion' deleted = True environment.version += 1 environment.save(unit) # close deployment deployment = get_last_deployment(unit, environment.id) deployment.finished = timeutils.utcnow() deployment.result = action_result num_errors = unit.query(models.Status)\ .filter_by(level='error', task_id=deployment.id).count() num_warnings = unit.query(models.Status)\ .filter_by(level='warning', task_id=deployment.id).count() final_status_text = action_name + ' finished' if num_errors: final_status_text += " with errors" elif num_warnings: final_status_text += " with warnings" status = models.Status() status.task_id = deployment.id status.text = final_status_text status.level = 'info' deployment.statuses.append(status) deployment.save(unit) # close session conf_session = unit.query(models.Session).filter_by( **{'environment_id': environment.id, 'state': states.SessionState.DEPLOYING if not deleted else states.SessionState.DELETING}).first() if num_errors > 0 or result['action'].get('isException'): conf_session.state = \ states.SessionState.DELETE_FAILURE if deleted else \ states.SessionState.DEPLOY_FAILURE else: conf_session.state = states.SessionState.DEPLOYED conf_session.save(unit) # output application tracking information services = [] objects = model['Objects'] if objects: services = objects.get('services') if num_errors + num_warnings > 0: LOG.warning('EnvId: {env_id} TenantId: {tenant_id} Status: ' 'Failed Apps: {services}' .format(env_id=environment.id, tenant_id=environment.tenant_id, services=services)) else: LOG.info('EnvId: {env_id} TenantId: {tenant_id} Status: ' 'Successful Apps: {services}' .format(env_id=environment.id, tenant_id=environment.tenant_id, services=services)) if action_name == 'Deployment': env = environment.to_dict() env["deployment_started"] = deployment.started env["deployment_finished"] = deployment.finished status_reporter.get_notifier().report( 'environment.deploy.end', env) def notification_endpoint_wrapper(priority='info'): def wrapper(func): class NotificationEndpoint(object): def __init__(self): setattr(self, priority, self._handler) def _handler(self, ctxt, publisher_id, event_type, payload, metadata): if event_type == ('murano.%s' % func.__name__): func(payload) def __call__(self, payload): return func(payload) return NotificationEndpoint() return wrapper @notification_endpoint_wrapper() def track_instance(payload): LOG.debug('Got track instance request from orchestration ' 'engine:\n{payload}'.format(payload=payload)) instance_id = payload['instance'] instance_type = payload.get('instance_type', 0) environment_id = payload['environment'] unit_count = payload.get('unit_count') type_name = payload['type_name'] type_title = payload.get('type_title') instances.InstanceStatsServices.track_instance( instance_id, environment_id, instance_type, type_name, type_title, unit_count) @notification_endpoint_wrapper() def untrack_instance(payload): LOG.debug('Got untrack instance request from orchestration ' 'engine:\n{payload}'.format(payload=payload)) instance_id = payload['instance'] environment_id = payload['environment'] instances.InstanceStatsServices.destroy_instance( instance_id, environment_id) @notification_endpoint_wrapper() def report_notification(report): LOG.debug('Got report from orchestration ' 'engine:\n{report}'.format(report=report)) report['entity_id'] = report.pop('id') status = models.Status() if 'timestamp' in report: dt = timeutils.parse_isotime(report.pop('timestamp')) report['created'] = dt.astimezone(pytz.utc).replace(tzinfo=None) status.update(report) unit = session.get_session() # connect with deployment with unit.begin(): running_deployment = get_last_deployment(unit, status.environment_id) status.task_id = running_deployment.id unit.add(status) def get_last_deployment(unit, env_id): query = unit.query(models.Task) \ .filter_by(environment_id=env_id) \ .order_by(desc(models.Task.started)) return query.first() class Service(service.Service): """Service class, that contains common methods for custom services""" def __init__(self): super(Service, self).__init__() self.server = None def stop(self, graceful=False): if self.server: self.server.stop() if graceful: self.server.wait() super(Service, self).stop() def reset(self): if self.server: self.server.reset() super(Service, self).reset() def get_notification_listener(): endpoints = [report_notification, track_instance, untrack_instance] s_target = target.Target(topic='murano', server=str(uuid.uuid4())) listener = rpc.get_notification_listener( [s_target], endpoints, executor='threading' ) return listener def get_rpc_server(): endpoints = [ResultEndpoint()] s_target = target.Target('murano', 'results', server=str(uuid.uuid4())) server = rpc.get_server( s_target, endpoints, executor='threading' ) return server class NotificationService(Service): def __init__(self): super(NotificationService, self).__init__() self.server = None def start(self): endpoints = [report_notification, track_instance, untrack_instance] s_target = target.Target(topic='murano', server=str(uuid.uuid4())) self.server = rpc.get_notification_listener( [s_target], endpoints, executor='eventlet' ) self.server.start() super(NotificationService, self).start() class ApiService(Service): def start(self): endpoints = [ResultEndpoint()] s_target = target.Target('murano', 'results', server=str(uuid.uuid4())) self.server = rpc.get_server( s_target, endpoints, executor='eventlet' ) self.server.start() super(ApiService, self).start() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/common/statservice.py0000664000175000017500000001226500000000000020627 0ustar00zuulzuul00000000000000# 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 import multiprocessing import socket import time import eventlet from oslo_config import cfg from oslo_log import log as logging from oslo_service import service import psutil from sqlalchemy import desc from murano.api import v1 from murano.api.v1.deployments import set_dep_state from murano.api.v1 import request_statistics from murano.db import models from murano.db.services import environments as envs from murano.db.services import stats as db_stats from murano.db import session as db_session from murano.engine.system import status_reporter CONF = cfg.CONF CONF_STATS = CONF.stats LOG = logging.getLogger(__name__) class StatsCollectingService(service.Service): def __init__(self): super(StatsCollectingService, self).__init__() request_statistics.init_stats() self._hostname = socket.gethostname() self._stats_db = db_stats.Statistics() self._prev_time = time.time() self._notifier = status_reporter.Notification() def start(self): super(StatsCollectingService, self).start() self.tg.add_thread(self._collect_stats_loop) self.tg.add_thread(self._report_env_stats_loop) def stop(self): super(StatsCollectingService, self).stop() def _collect_stats_loop(self): period = CONF_STATS.period * 60 while True: self.update_stats() eventlet.sleep(period) def _report_env_stats_loop(self): env_audit_period = CONF_STATS.env_audit_period * 60 while True: self.report_env_stats() eventlet.sleep(env_audit_period) def update_stats(self): LOG.debug("Updating statistic information.") LOG.debug("Stats object: {stats}".format(stats=v1.stats)) LOG.debug("Stats: (Requests: {amount} Errors: {error} " "Ave.Res.Time {time:2.4f}\n Per tenant: {req_count})".format( amount=v1.stats.request_count, error=v1.stats.error_count, time=v1.stats.average_time, req_count=v1.stats.requests_per_tenant)) try: stats = self._stats_db.get_stats_by_host(self._hostname) if stats is None: self._stats_db.create(self._hostname, v1.stats.request_count, v1.stats.error_count, v1.stats.average_time, v1.stats.requests_per_tenant, multiprocessing.cpu_count(), psutil.cpu_percent()) return now = time.time() t_delta = now - self._prev_time requests_per_second = (v1.stats.request_count - stats.request_count) / t_delta errors_per_second = (v1.stats.error_count - stats.error_count) / t_delta self._prev_time = now stats.request_count = v1.stats.request_count stats.error_count = v1.stats.error_count stats.average_response_time = v1.stats.average_time stats.requests_per_tenant = json.dumps(v1.stats. requests_per_tenant) stats.requests_per_second = requests_per_second stats.errors_per_second = errors_per_second stats.cpu_percent = psutil.cpu_percent() self._stats_db.update(self._hostname, stats) except Exception as e: LOG.exception("Failed to get statistics object from a " "database. {error_code}".format(error_code=e)) def report_env_stats(self): LOG.debug("Reporting env stats") try: environments = envs.EnvironmentServices.get_environments_by({}) for env in environments: deployments = get_env_deployments(env.id) success_deployments = [d for d in deployments if d['state'] == "success"] if success_deployments: self._notifier.report('environment.exists', env.to_dict()) except Exception: LOG.exception("Failed to report existing envs") def get_env_deployments(environment_id): unit = db_session.get_session() query = unit.query(models.Task).filter_by( environment_id=environment_id).order_by(desc(models.Task.created)) result = query.all() deployments = [ set_dep_state(deployment, unit).to_dict() for deployment in result] return deployments ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/common/utils.py0000664000175000017500000002301400000000000017425 0ustar00zuulzuul00000000000000# 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 functools as func import re import jsonschema from oslo_log import log as logging from murano.common.i18n import _ LOG = logging.getLogger(__name__) class TraverseHelper(object): value_type = (str, int, float, bool) @staticmethod def get(path, source): """Provides the ability to traverse a data source Provides the ability to traverse a data source made up of any combination of lists and dicts. Has simple rules for selecting item of the list: * each item should have id property * to select item from the list, specify id value Examples: source = {'obj': {'attr': True}} value = TraverseHelper.get('/obj/attr', source) source = {'obj': [ {'id': '1', 'value': 1}, {'id': '2s', 'value': 2}, ]} value = TraverseHelper.get('/obj/2s/value', source) :param path: string with path to desired value :param source: python object (list or dict) :return: object :raise: ValueError if object is malformed """ queue = collections.deque(filter(lambda x: x, path.split('/'))) while len(queue): path = queue.popleft() if isinstance(source, list): idx_source = source iterator = ( i for i in source if i.get('?', {}).get('id') == path ) source = next(iterator, None) if source is None and path.isdigit(): source = idx_source[int(path)] elif isinstance(source, dict): source = source[path] elif isinstance(source, TraverseHelper.value_type): break else: raise ValueError(_('Source object or path is malformed')) return source @staticmethod def update(path, value, source): """Updates value selected with specified path. Warning: Root object could not be updated :param path: string with path to desired value :param value: value :param source: python object (list or dict) """ parent_path = '/'.join(path.split('/')[:-1]) node = TraverseHelper.get(parent_path, source) key = path[1:].split('/')[-1] if is_number(key): node[int(key)] = value else: node[key] = value @staticmethod def insert(path, value, source): """Inserts new item to selected list. :param path: string with path to desired value :param value: value :param source: List """ node = TraverseHelper.get(path, source) node.append(value) @staticmethod def extend(path, value, source): """Extend list by appending elements from the iterable. :param path: string with path to desired value :param value: value :param source: List """ node = TraverseHelper.get(path, source) node.extend(value) @staticmethod def remove(path, source): """Removes selected item from source. :param path: string with path to desired value :param source: python object (list or dict) """ parent_path = '/'.join(path.split('/')[:-1]) node = TraverseHelper.get(parent_path, source) key = path[1:].split('/')[-1] if isinstance(node, list): iterator = (i for i in node if i.get('?', {}).get('id') == key) item = next(iterator, None) if item is None and key.isdigit(): del node[int(key)] else: node.remove(item) elif isinstance(node, dict): del node[key] else: raise ValueError(_('Source object or path is malformed')) def is_number(var): try: int(var) return True except Exception: return False def is_different(obj1, obj2): """Stripped-down version of deep.diff comparator Compares arbitrary nested objects, handles circular links, but doesn't point to the first difference as deep.diff does. """ class Difference(Exception): pass def is_in(o, st): for _o in st: if o is _o: return True return False def rec(o1, o2, stack1=(), stack2=()): if is_in(o1, stack1) and is_in(o2, stack2): # circular reference detected - break the loop return elif is_in(o1, stack1): raise Difference() else: stack1 += (o1,) stack2 += (o2,) if o1 is o2: return elif (isinstance(o1, str) and isinstance(o2, str)) and o1 == o2: return elif type(o1) != type(o2): raise Difference() elif isinstance(o1, dict): # check for keys inequality rec(o1.keys(), o2.keys(), stack1, stack2) for key in o1.keys(): rec(o1[key], o2[key], stack1, stack2) elif isinstance(o1, (list, tuple, set)): if len(o1) != len(o2): raise Difference() else: for _o1, _o2 in zip(o1, o2): rec(_o1, _o2, stack1, stack2) elif hasattr(o1, '__dict__'): return rec(o1.__dict__, o2.__dict__, stack1, stack2) elif o1 != o2: raise Difference() try: rec(obj1, obj2) except Difference: return True else: return False def build_entity_map(value): def build_entity_map_recursive(value, id_map): if isinstance(value, dict): if '?' in value and 'id' in value['?']: id_map[value['?']['id']] = value for v in value.values(): build_entity_map_recursive(v, id_map) if isinstance(value, list): for item in value: build_entity_map_recursive(item, id_map) id_map = {} build_entity_map_recursive(value, id_map) return id_map def handle(f): """Handles exception in wrapped function and writes to LOG.""" @func.wraps(f) def f_handle(*args, **kwargs): try: return f(*args, **kwargs) except Exception as e: LOG.exception(e) return f_handle def validate_body(schema): def deco_validate_body(f): @func.wraps(f) def f_validate_body(*args, **kwargs): if 'body' in kwargs: jsonschema.validate(kwargs['body'], schema) return f(*args, **kwargs) return f_validate_body return deco_validate_body def validate_quotes(value): """Validate filter values Validation opening/closing quotes in the expression. """ open_quotes = True count_backslash_in_row = 0 for i in range(len(value)): if value[i] == '"': if count_backslash_in_row % 2: continue if open_quotes: if i and value[i - 1] != ',': msg = _("Invalid filter value %s. There is no comma " "before opening quotation mark.") % value raise ValueError(msg) else: if i + 1 != len(value) and value[i + 1] != ",": msg = _("Invalid filter value %s. There is no comma " "after opening quotation mark.") % value raise ValueError(msg) open_quotes = not open_quotes elif value[i] == '\\': count_backslash_in_row += 1 else: count_backslash_in_row = 0 if not open_quotes: msg = _("Invalid filter value %s. The quote is not closed.") % value raise ValueError(msg) return True def split_for_quotes(value): """Split filter values Split values by commas and quotes for 'in' operator, according api-wg. """ validate_quotes(value) tmp = re.compile(r''' "( # if found a double-quote [^\"\\]* # take characters either non-quotes or backslashes (?:\\. # take backslashes and character after it [^\"\\]*)* # take characters either non-quotes or backslashes ) # before double-quote ",? # a double-quote with comma maybe | ([^,]+),? # if not found double-quote take any non-comma # characters with comma maybe | , # if we have only comma take empty string ''', re.VERBOSE) val_split = [val[0] or val[1] for val in re.findall(tmp, value)] replaced_inner_quotes = [s.replace(r'\"', '"') for s in val_split] return replaced_inner_quotes def reraise(tp, value, tb=None): try: if value is None: value = tp() if value.__traceback__ is not tb: raise value.with_traceback(tb) raise value finally: value = None tb = None ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/common/uuidutils.py0000664000175000017500000000131700000000000020316 0ustar00zuulzuul00000000000000# 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 oslo_utils import uuidutils def generate_uuid(): return uuidutils.generate_uuid(dashed=False) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/common/wsgi.py0000664000175000017500000011763600000000000017254 0ustar00zuulzuul00000000000000# Copyright 2011 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. """Utility methods for working with WSGI servers.""" import datetime import errno import re import socket import sys import time from xml.dom import minidom from xml.parsers import expat import eventlet import eventlet.wsgi import jsonschema from oslo_config import cfg from oslo_log import log as logging from oslo_serialization import jsonutils from oslo_service import service from oslo_service import sslutils import routes import routes.middleware import webob.dec import webob.exc from murano.api.v1 import validation_schemas from murano.common import config from murano.common import exceptions from murano.common.i18n import _ from murano.common import xmlutils eventlet.patcher.monkey_patch(all=False, socket=True) wsgi_opts = [ cfg.IntOpt('backlog', default=4096, help="Number of backlog requests to configure the socket with"), cfg.IntOpt('tcp_keepidle', default=600, help="Sets the value of TCP_KEEPIDLE in seconds for each " "server socket. Not supported on OS X."), cfg.IntOpt('max_header_line', default=16384, help="Maximum line size of message headers to be accepted. " "max_header_line may need to be increased when using " "large tokens (typically those generated by the " "Keystone v3 API with big service catalogs)."), ] CONF = cfg.CONF CONF.register_opts(wsgi_opts) LOG = logging.getLogger(__name__) class Service(service.Service): """Provides a Service API for wsgi servers. This gives us the ability to launch wsgi servers with the Launcher classes in oslo_service.service.py. """ def __init__(self, application, port, host='0.0.0.0', backlog=4096, threads=1000): self.application = application self._port = port self._host = host self._backlog = backlog if backlog else CONF.backlog self._logger = logging.getLogger('eventlet.wsgi') self.greenthread = None config.set_middleware_defaults() super(Service, self).__init__(threads) def _get_socket(self, host, port, backlog): # TODO(dims): eventlet's green dns/socket module does not actually # support IPv6 in getaddrinfo(). We need to get around this in the # future or monitor upstream for a fix info = socket.getaddrinfo(host, port, socket.AF_UNSPEC, socket.SOCK_STREAM)[0] family = info[0] bind_addr = info[-1] sock = None retry_until = time.time() + 30 while not sock and time.time() < retry_until: try: sock = eventlet.listen(bind_addr, backlog=backlog, family=family) if sslutils.is_enabled(CONF): sock = sslutils.wrap(CONF, sock) except socket.error as err: if err.args[0] != errno.EADDRINUSE: raise eventlet.sleep(0.1) if not sock: raise RuntimeError(_("Could not bind to %(host)s:%(port)s " "after trying for 30 seconds: Address" " already in use.") % {'host': host, 'port': port}) sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) # sockets can hang around forever without keepalive sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1) # This option isn't available in the OS X version of eventlet if hasattr(socket, 'TCP_KEEPIDLE'): sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPIDLE, CONF.tcp_keepidle) return sock def start(self): """Start serving this service using the provided server instance. :returns: None """ super(Service, self).start() self._socket = self._get_socket(self._host, self._port, self._backlog) self.greenthread = eventlet.spawn( self._run, self.application, self._socket) @property def backlog(self): return self._backlog @property def host(self): return self._socket.getsockname()[0] if self._socket else self._host @property def port(self): return self._socket.getsockname()[1] if self._socket else self._port def stop(self): """Stop serving this API. :returns: None """ super(Service, self).stop() if self.greenthread is not None: self.greenthread.kill() def reset(self): super(Service, self).reset() logging.setup(cfg.CONF, 'murano') def _run(self, application, socket): """Start a WSGI server in a new green thread.""" eventlet.wsgi.MAX_HEADER_LINE = CONF.max_header_line eventlet.wsgi.server(socket, application, custom_pool=self.tg.pool, log=self._logger) class Middleware(object): """Base WSGI middleware wrapper. These classes require an application to be initialized that will be called next. By default the middleware will simply call its wrapped app, or you can override __call__ to customize its behavior. """ def __init__(self, application): self.application = application def process_request(self, req): """Called on each request. If this returns None, the next application down the stack will be executed. If it returns a response then that response will be returned and execution will stop here. """ return None def process_response(self, response): """Do whatever you'd like to the response.""" return response @webob.dec.wsgify def __call__(self, req): response = self.process_request(req) if response: return response response = req.get_response(self.application) return self.process_response(response) class Debug(Middleware): """Helper class to get info about request and response Helper class that can be inserted into any WSGI application chain to get information about the request and response. """ @webob.dec.wsgify def __call__(self, req): print(("*" * 40) + " REQUEST ENVIRON") for key, value in req.environ.items(): print(key, "=", value) print("") resp = req.get_response(self.application) print(("*" * 40) + " RESPONSE HEADERS") for (key, value) in resp.headers.items(): print(key, "=", value) print("") resp.app_iter = self.print_generator(resp.app_iter) return resp @staticmethod def print_generator(app_iter): """Prints the contents of a wrapper string iterator Iterator that prints the contents of a wrapper string iterator when iterated. """ print(("*" * 40) + " BODY") for part in app_iter: sys.stdout.write(part) sys.stdout.flush() yield part print("") class Router(object): """WSGI middleware that maps incoming requests to WSGI apps.""" def __init__(self, mapper): """Create a router for the given routes.Mapper. Each route in `mapper` must specify a 'controller', which is a WSGI app to call. You'll probably want to specify an 'action' as well and have your controller be a wsgi.Controller, who will route the request to the action method. Examples: mapper = routes.Mapper() sc = ServerController() # Explicit mapping of one route to a controller+action mapper.connect(None, "/svrlist", controller=sc, action="list") # Actions are all implicitly defined mapper.resource("server", "servers", controller=sc) # Pointing to an arbitrary WSGI app. You can specify the # {path_info:.*} parameter so the target app can be handed just that # section of the URL. mapper.connect(None, "/v1.0/{path_info:.*}", controller=BlogApp()) """ self.map = mapper self._router = routes.middleware.RoutesMiddleware(self._dispatch, self.map) @webob.dec.wsgify def __call__(self, req): """Route the incoming request to a controller Route the incoming request to a controller based on self.map. If no match, return a 404. """ return self._router @staticmethod @webob.dec.wsgify def _dispatch(req): """Looks for routed WSGI app's response Called by self._router after matching the incoming request to a route and putting the information into req.environ. Either returns 404 or the routed WSGI app's response. """ match = req.environ['wsgiorg.routing_args'][1] if not match: return webob.exc.HTTPNotFound() app = match['controller'] return app class Request(webob.Request): """Add some OpenStack API-specific logic to the base webob.Request.""" default_request_content_types = ('application/json', 'application/xml', 'application/murano-packages-json-patch', 'application/env-model-json-patch', 'multipart/form-data') default_accept_types = ('application/json', 'application/xml', 'application/octet-stream') def best_match_content_type(self, action, supported_content_types=None, specific_content_types=None): """Determine the requested response content-type. Based on the query extension then the Accept header. Raise NotAcceptableContentType exception if we don't find a preference """ supported_content_types = (supported_content_types or self.default_accept_types) parts = self.path.rsplit('.', 1) if len(parts) > 1: ctype = 'application/{0}'.format(parts[1]) if ctype in supported_content_types: return ctype if specific_content_types and action in specific_content_types: bm = self.accept.best_match(specific_content_types[action]) else: bm = self.accept.best_match(supported_content_types) if not bm: raise exceptions.NotAcceptableContentType(content_type=self.accept) return bm def get_content_type(self, allowed_content_types=None): """Determine content type of the request body. Does not do any body introspection, only checks header """ if "Content-Type" not in self.headers: return None content_type = self.content_type allowed_content_types = (allowed_content_types or self.default_request_content_types) if content_type not in allowed_content_types: raise exceptions.UnsupportedContentType(content_type=content_type) return content_type class ResourceExceptionHandler(object): """Context manager to handle Resource exceptions. Used when processing exceptions generated by API implementation methods. Converts most exceptions to webob exceptions, with the appropriate logging. """ def __enter__(self): return None def __exit__(self, ex_type, ex_value, ex_traceback): if not ex_value: return True # TODO(lin.a.yang): current only handle TypeError here, we should # process other kind of internal exceptions generated by API and # convert to webob exceptions. if isinstance(ex_value, TypeError): exc_info = (ex_type, ex_value, ex_traceback) LOG.error("Exception handling resource: {0}".format(ex_value), exc_info=exc_info) raise webob.exc.HTTPBadRequest() # We didn't handle this kind of exception return False class Resource(object): """WSGI app that handles (de)serialization and controller dispatch. Reads routing information supplied by RoutesMiddleware and calls the requested action method upon its deserializer, controller, and serializer. Those three objects may implement any of the basic controller action methods (create, update, show, index, delete) along with any that may be specified in the api router. A 'default' method may also be implemented to be used in place of any non-implemented actions. Deserializer methods must accept a request argument and return a dictionary. Controller methods must accept a request argument. Additionally, they must also accept keyword arguments that represent the keys returned by the Deserializer. They may raise a webob.exc exception or return a dict, which will be serialized by requested content type. """ def __init__(self, controller, deserializer=None, serializer=None): """Resource init. :param controller: object that implement methods created by routes lib :param deserializer: object that supports webob request deserialization through controller-like actions :param serializer: object that supports webob response serialization through controller-like actions """ self.controller = controller self.serializer = serializer or ResponseSerializer() self.deserializer = deserializer or RequestDeserializer() @webob.dec.wsgify(RequestClass=Request) def __call__(self, request): """WSGI method that controls (de)serialization and method dispatch.""" LOG.debug("{method} {url}\nHEADERS: {headers}".format( method=request.method, url=request.url, headers=self._format_request_headers(request.headers))) try: action, action_args, accept = self.deserialize_request(request) except exceptions.UnsupportedContentType: msg = _("Unsupported Content-Type") return webob.exc.HTTPUnsupportedMediaType(detail=msg) except exceptions.NotAcceptableContentType: msg = _("Acceptable response can not be provided") return webob.exc.HTTPNotAcceptable(detail=msg) except exceptions.MalformedRequestBody: msg = _("Malformed request body") return webob.exc.HTTPBadRequest(explanation=msg) with ResourceExceptionHandler(): action_result = self.execute_action(action, request, **action_args) try: return self.serialize_response(action, action_result, accept) # return unserializable result (typically a webob exc) except Exception: return action_result def deserialize_request(self, request): return self.deserializer.deserialize(request) def serialize_response(self, action, action_result, accept): return self.serializer.serialize(action_result, accept, action) def execute_action(self, action, request, **action_args): return self.dispatch(self.controller, action, request, **action_args) def dispatch(self, obj, action, *args, **kwargs): """Find action-specific method on self and call it.""" try: method = getattr(obj, action) except AttributeError: method = getattr(obj, 'default') return method(*args, **kwargs) def get_action_args(self, request_environment): """Parse dictionary created by routes library.""" try: args = request_environment['wsgiorg.routing_args'][1].copy() except Exception: return {} try: del args['controller'] except KeyError: pass try: del args['format'] except KeyError: pass return args def _format_request_headers(self, headers): """Format the request headers to be logged. To keep log more clear, only show the X-* header include murano own header and several useful headers added by keystone auth middleware, and skip other X-* headers. """ string_parts = [] # Only show following X-* header useful_headers = ("X-Configuration-Session", "X-Roles", "X-User-Id", "X-Tenant-Id") for header, value in headers.items(): if header.startswith("X-") and header not in useful_headers: continue string_parts.append("{0}: {1}".format(header, value)) return ', '.join(string_parts) class ActionDispatcher(object): """Maps method name to local methods through action name.""" def dispatch(self, *args, **kwargs): """Find and call local method.""" action = kwargs.pop('action', 'default') action_method = getattr(self, str(action), self.default) return action_method(*args, **kwargs) def default(self, data): raise NotImplementedError() class DictSerializer(ActionDispatcher): """Default request body serialization.""" def serialize(self, data, action='default'): return self.dispatch(data, action=action) def default(self, data): return "" class JSONDictSerializer(DictSerializer): """Default JSON request body serialization.""" def default(self, data, result=None): def sanitizer(obj): if isinstance(obj, datetime.datetime): _dtime = obj - datetime.timedelta(microseconds=obj.microsecond) return _dtime.isoformat() return str(obj) if result: data.body = jsonutils.dump_as_bytes(result) return jsonutils.dump_as_bytes(data, default=sanitizer) class XMLDictSerializer(DictSerializer): def __init__(self, metadata=None, xmlns=None): """Default XML request body serialization. :param metadata: information needed to deserialize xml into a dictionary. :param xmlns: XML namespace to include with serialized xml """ super(XMLDictSerializer, self).__init__() self.metadata = metadata or {} self.xmlns = xmlns def default(self, data, result=None): # We expect data to contain a single key which is the XML root. root_key = list(data.keys())[0] doc = minidom.Document() node = self._to_xml_node(doc, self.metadata, root_key, data[root_key]) return self.to_xml_string(node) def to_xml_string(self, node, has_atom=False): self._add_xmlns(node, has_atom) return node.toprettyxml(indent=' ', encoding='UTF-8') # NOTE (ameade): the has_atom should be removed after all the # xml serializers and view builders have been updated to the current # spec that required all responses include the xmlns:atom, the has_atom # flag is to prevent current tests from breaking def _add_xmlns(self, node, has_atom=False): if self.xmlns is not None: node.setAttribute('xmlns', self.xmlns) if has_atom: node.setAttribute('xmlns:atom', "http://www.w3.org/2005/Atom") def _to_xml_node(self, doc, metadata, nodename, data): """Recursive method to convert data members to XML nodes.""" result = doc.createElement(nodename) # Set the xml namespace if one is specified # TODO(justinsb): We could also use prefixes on the keys xmlns = metadata.get('xmlns', None) if xmlns: result.setAttribute('xmlns', xmlns) # TODO(bcwaldon): accomplish this without a type-check if type(data) is list: collections = metadata.get('list_collections', {}) if nodename in collections: metadata = collections[nodename] for item in data: node = doc.createElement(metadata['item_name']) node.setAttribute(metadata['item_key'], str(item)) result.appendChild(node) return result singular = metadata.get('plurals', {}).get(nodename, None) if singular is None: if nodename.endswith('s'): singular = nodename[:-1] else: singular = 'item' for item in data: node = self._to_xml_node(doc, metadata, singular, item) result.appendChild(node) # TODO(bcwaldon): accomplish this without a type-check elif type(data) is dict: collections = metadata.get('dict_collections', {}) if nodename in collections: metadata = collections[nodename] for k, v in data.items(): node = doc.createElement(metadata['item_name']) node.setAttribute(metadata['item_key'], str(k)) text = doc.createTextNode(str(v)) node.appendChild(text) result.appendChild(node) return result attrs = metadata.get('attributes', {}).get(nodename, {}) for k, v in data.items(): if k in attrs: result.setAttribute(k, str(v)) else: node = self._to_xml_node(doc, metadata, k, v) result.appendChild(node) else: # Type is atom node = doc.createTextNode(str(data)) result.appendChild(node) return result def _create_link_nodes(self, xml_doc, links): link_nodes = [] for link in links: link_node = xml_doc.createElement('atom:link') link_node.setAttribute('rel', link['rel']) link_node.setAttribute('href', link['href']) if 'type' in link: link_node.setAttribute('type', link['type']) link_nodes.append(link_node) return link_nodes class BlankSerializer(DictSerializer): """Return raw data.""" def default(self, data): return data class ResponseHeadersSerializer(ActionDispatcher): """Default response headers serialization.""" def serialize(self, response, data, action): self.dispatch(response, data, action=action) def default(self, response, data): response.status_int = 200 class ResponseSerializer(object): """Encode the necessary pieces into a response object.""" def __init__(self, body_serializers=None, headers_serializer=None): self.body_serializers = { 'application/xml': XMLDictSerializer(), 'application/json': JSONDictSerializer(), 'text/plain': BlankSerializer(), 'application/octet-stream': BlankSerializer() } self.body_serializers.update(body_serializers or {}) self.headers_serializer = (headers_serializer or ResponseHeadersSerializer()) def serialize(self, response_data, content_type, action='default'): """Serialize a dict into a string and wrap in a wsgi.Request object. :param response_data: dict produced by the Controller :param content_type: expected mimetype of serialized response body """ response = webob.Response() self.serialize_headers(response, response_data, action) self.serialize_body(response, response_data, content_type, action) return response def serialize_headers(self, response, data, action): self.headers_serializer.serialize(response, data, action) def serialize_body(self, response, data, content_type, action): response.headers['Content-Type'] = content_type if data is not None: serializer = self.get_body_serializer(content_type) response.body = serializer.serialize(data, action) def get_body_serializer(self, content_type): try: return self.body_serializers[content_type] except (KeyError, TypeError): raise exceptions.UnsupportedContentType(content_type=content_type) class ServiceBrokerResponseSerializer(ResponseSerializer): def __init__(self): super(ServiceBrokerResponseSerializer, self).__init__() def serialize(self, response_data, content_type, action='default'): if isinstance(response_data, webob.Response): response = response_data self.serialize_body(response, response.data, content_type, action) else: response = super(ServiceBrokerResponseSerializer, self).serialize( response_data, content_type, action='default') return response class RequestHeadersDeserializer(ActionDispatcher): """Default request headers deserializer.""" def deserialize(self, request, action): return self.dispatch(request, action=action) def default(self, request): return {} class RequestDeserializer(object): """Break up a Request object into more useful pieces.""" def __init__(self, body_deserializers=None, headers_deserializer=None, supported_content_types=None, specific_content_types=None): self.supported_content_types = supported_content_types self.specific_content_types = specific_content_types self.body_deserializers = { 'application/xml': XMLDeserializer(), 'application/json': JSONDeserializer(), 'application/murano-packages-json-patch': MuranoPackageJSONPatchDeserializer(), 'application/env-model-json-patch': EnvModelJSONPatchDeserializer(), 'multipart/form-data': FormDataDeserializer() } self.body_deserializers.update(body_deserializers or {}) self.headers_deserializer = (headers_deserializer or RequestHeadersDeserializer()) def deserialize(self, request): """Extract necessary pieces of the request. :param request: Request object :returns: tuple of (expected controller action name, dictionary of keyword arguments to pass to the controller, the expected content type of the response) """ action_args = self.get_action_args(request.environ) action = action_args.pop('action', None) action_args.update(self.deserialize_headers(request, action)) action_args.update(self.deserialize_body(request, action)) accept = self.get_expected_content_type(request, action) return (action, action_args, accept) def deserialize_headers(self, request, action): return self.headers_deserializer.deserialize(request, action) def deserialize_body(self, request, action): if not len(request.body) > 0: LOG.debug("Empty body provided in request") return {} try: content_type = request.get_content_type() except exceptions.UnsupportedContentType as e: LOG.error("Unrecognized Content-Type provided in request: " "{error}".format(error=str(e))) raise if content_type is None: LOG.debug("No Content-Type provided in request") return {} try: deserializer = self.get_body_deserializer(content_type) except exceptions.UnsupportedContentType: LOG.debug("Unable to deserialize body as provided Content-Type") raise return deserializer.deserialize(request, action) def get_body_deserializer(self, content_type): try: return self.body_deserializers[content_type] except (KeyError, TypeError): raise exceptions.UnsupportedContentType(content_type=content_type) def get_expected_content_type(self, request, action): return request.best_match_content_type(action, self.supported_content_types, self.specific_content_types) def get_action_args(self, request_environment): """Parse dictionary created by routes library.""" try: args = request_environment['wsgiorg.routing_args'][1].copy() except Exception: return {} try: del args['controller'] except KeyError: pass try: del args['format'] except KeyError: pass return args class TextDeserializer(ActionDispatcher): """Default request body deserialization.""" def deserialize(self, request, action='default'): return self.dispatch(request, action=action) def default(self, request): return {} class JSONDeserializer(TextDeserializer): def _from_json(self, datastring): try: return jsonutils.loads(datastring) except ValueError: msg = _("cannot understand JSON") raise exceptions.MalformedRequestBody(reason=msg) def default(self, request): datastring = request.body return {'body': self._from_json(datastring)} class JSONPatchDeserializer(TextDeserializer): allowed_operations = {} schema = None allow_unknown_path = False def _from_json_patch(self, datastring): try: operations = jsonutils.loads(datastring) except ValueError: msg = _("cannot understand JSON") raise exceptions.MalformedRequestBody(reason=msg) if not isinstance(operations, list): msg = _('JSON-patch must be a list.') raise webob.exc.HTTPBadRequest(explanation=msg) changes = [] for raw_change in operations: if not isinstance(raw_change, dict): msg = _('Operations must be JSON objects.') raise webob.exc.HTTPBadRequest(explanation=msg) (op, path) = self._parse_json_schema_change(raw_change) self._validate_path(path) change = {'op': op, 'path': path} change['value'] = self._get_change_value(raw_change, op) self._validate_change(change) changes.append(change) return changes def _get_change_value(self, raw_change, op): if 'value' not in raw_change: msg = _('Operation "%s" requires a member named "value".') raise webob.exc.HTTPBadRequest(explanation=msg % op) return raw_change['value'] def _get_change_operation(self, raw_change): try: return raw_change['op'] except KeyError: msg = _("Unable to find '%s' in JSON Schema change") % 'op' raise webob.exc.HTTPBadRequest(explanation=msg) def _get_change_path(self, raw_change): try: return raw_change['path'] except KeyError: msg = _("Unable to find '%s' in JSON Schema change") % 'path' raise webob.exc.HTTPBadRequest(explanation=msg) def _validate_change(self, change): self._validate_allowed_methods(change, self.allow_unknown_path) if self.schema: self._validate_schema(change) def _validate_allowed_methods(self, change, allow_unknown_path=False): full_path = '/'.join(change['path']) change_op = change['op'] allowed_methods = self.allowed_operations.get(full_path) if allowed_methods is None: if allow_unknown_path: allowed_methods = ['add', 'replace', 'remove'] else: msg = _("Attribute '{0}' is invalid").format(full_path) raise webob.exc.HTTPForbidden(explanation=str(msg)) if change_op not in allowed_methods: ops = ', '.join(allowed_methods) if allowed_methods\ else 'no operations' msg = _("Method '{method}' is not allowed for a path with name " "'{name}'. Allowed operations are: " "{ops}").format(method=change_op, name=full_path, ops=ops) raise webob.exc.HTTPForbidden(explanation=str(msg)) def _validate_schema(self, change): property_to_update = change['value'] can_validate = True schema = self.schema for p in change['path']: if schema['type'] == 'array': try: schema = schema['items'] except KeyError: can_validate = False elif schema['type'] == 'object': try: schema = schema['properties'][p] except KeyError: can_validate = False if can_validate: try: jsonschema.validate(property_to_update, schema) except jsonschema.ValidationError as e: LOG.error("Schema validation error occurred: %s", e) raise webob.exc.HTTPBadRequest(explanation=e.message) def _decode_json_pointer(self, pointer): """Parse a json pointer. Json Pointers are defined in http://tools.ietf.org/html/draft-pbryan-zyp-json-pointer . The pointers use '/' for separation between object attributes, such that '/A/B' would evaluate to C in {"A": {"B": "C"}}. A '/' character in an attribute name is encoded as "~1" and a '~' character is encoded as "~0". """ self._validate_json_pointer(pointer) ret = [] for part in pointer.lstrip('/').split('/'): ret.append(part.replace('~1', '/').replace('~0', '~').strip()) return ret def _validate_json_pointer(self, pointer): """Validate a json pointer. Only limited form of json pointers is accepted. """ if not pointer.startswith('/'): msg = _('Pointer `%s` does not start with "/".') % pointer raise webob.exc.HTTPBadRequest(explanation=msg) if re.search('/\s*?/', pointer[1:]): msg = _('Pointer `%s` contains adjacent "/".') % pointer raise webob.exc.HTTPBadRequest(explanation=msg) if len(pointer) > 1 and pointer.endswith('/'): msg = _('Pointer `%s` ends with "/".') % pointer raise webob.exc.HTTPBadRequest(explanation=msg) if pointer[1:].strip() == '/': msg = _('Pointer `%s` does not contain a valid token.') % pointer raise webob.exc.HTTPBadRequest(explanation=msg) if re.search('~[^01]', pointer) or pointer.endswith('~'): msg = _('Pointer `%s` contains "~", which is not part of' ' a recognized escape sequence.') % pointer raise webob.exc.HTTPBadRequest(explanation=msg) def _parse_json_schema_change(self, raw_change): op = self._get_change_operation(raw_change) path = self._get_change_path(raw_change) path_list = self._decode_json_pointer(path) return op, path_list def _validate_path(self, path): pass def default(self, request): return {'body': self._from_json_patch(request.body)} class MuranoPackageJSONPatchDeserializer(JSONPatchDeserializer): allowed_operations = {"categories": ["add", "replace", "remove"], "tags": ["add", "replace", "remove"], "is_public": ["replace"], "enabled": ["replace"], "description": ["replace"], "name": ["replace"]} allow_unknown_path = False schema = validation_schemas.PKG_UPDATE_SCHEMA def _validate_path(self, path): if len(path) > 1: msg = _('Nested paths are not allowed') raise webob.exc.HTTPBadRequest(explanation=msg) class EnvModelJSONPatchDeserializer(JSONPatchDeserializer): allowed_operations = {"": [], "defaultNetworks": ["replace"], "defaultNetworks/environment": ["replace"], "defaultNetworks/environment/?/id": [], "defaultNetworks/flat": ["replace"], "name": ["replace"], "region": ["replace"], "?/type": ["replace"], "?/id": [] } allow_unknown_path = True schema = validation_schemas.ENV_SCHEMA class XMLDeserializer(TextDeserializer): def __init__(self, metadata=None): """XMLDeserializer. :param metadata: information needed to deserialize xml into a dictionary. """ super(XMLDeserializer, self).__init__() self.metadata = metadata or {} def _from_xml(self, request): datastring = request.body plurals = set(self.metadata.get('plurals', {})) try: node = xmlutils.safe_minidom_parse_string(datastring).childNodes[0] return {node.nodeName: self._from_xml_node(node, plurals)} except expat.ExpatError: msg = _("cannot understand XML") raise exceptions.MalformedRequestBody(reason=msg) def _from_xml_node(self, node, listnames): """Convert a minidom node to a simple Python type. :param listnames: list of XML node names whose subnodes should be considered list items. """ if len(node.childNodes) == 1 and node.childNodes[0].nodeType == 3: return node.childNodes[0].nodeValue elif node.nodeName in listnames: return [self._from_xml_node(n, listnames) for n in node.childNodes] else: result = dict() for attr in node.attributes.keys(): result[attr] = node.attributes[attr].nodeValue for child in node.childNodes: if child.nodeType != node.TEXT_NODE: result[child.nodeName] = self._from_xml_node(child, listnames) return result def find_first_child_named(self, parent, name): """Find first child which has the given name of a node.""" for node in parent.childNodes: if node.nodeName == name: return node return None def find_children_named(self, parent, name): """Return all children of a node with the given name.""" for node in parent.childNodes: if node.nodeName == name: yield node def extract_text(self, node): """Get the text field contained by the given node.""" if len(node.childNodes) == 1: child = node.childNodes[0] if child.nodeType == child.TEXT_NODE: return child.nodeValue return "" def default(self, datastring): return {'body': self._from_xml(datastring)} class FormDataDeserializer(TextDeserializer): def _from_json(self, datastring): value = datastring try: LOG.debug("Trying to deserialize '{data}' to json".format( data=datastring)) value = jsonutils.loads(datastring) except ValueError: LOG.warning("Unable to deserialize to json, using raw text") return value def default(self, request): form_data_parts = request.POST for key, value in form_data_parts.items(): if isinstance(value, str): form_data_parts[key] = self._from_json(value) return {'body': form_data_parts} ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/common/xmlutils.py0000664000175000017500000000547700000000000020163 0ustar00zuulzuul00000000000000# Copyright 2013 IBM Corp. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from xml.dom import minidom from xml.parsers import expat from xml import sax from xml.sax import expatreader class ProtectedExpatParser(expatreader.ExpatParser): """An expat parser which disables DTD's and entities by default.""" def __init__(self, forbid_dtd=True, forbid_entities=True, *args, **kwargs): # Python 2.x old style class expatreader.ExpatParser.__init__(self, *args, **kwargs) self.forbid_dtd = forbid_dtd self.forbid_entities = forbid_entities def start_doctype_decl(self, name, sysid, pubid, has_internal_subset): raise ValueError("Inline DTD forbidden") def entity_decl(self, entityName, is_parameter_entity, value, base, systemId, publicId, notationName): raise ValueError(" entity declaration forbidden") def unparsed_entity_decl(self, name, base, sysid, pubid, notation_name): # expat 1.2 raise ValueError(" unparsed entity forbidden") def external_entity_ref(self, context, base, systemId, publicId): raise ValueError(" external entity forbidden") def notation_decl(self, name, base, sysid, pubid): raise ValueError(" notation forbidden") def reset(self): expatreader.ExpatParser.reset(self) if self.forbid_dtd: self._parser.StartDoctypeDeclHandler = self.start_doctype_decl self._parser.EndDoctypeDeclHandler = None if self.forbid_entities: self._parser.EntityDeclHandler = self.entity_decl self._parser.UnparsedEntityDeclHandler = self.unparsed_entity_decl self._parser.ExternalEntityRefHandler = self.external_entity_ref self._parser.NotationDeclHandler = self.notation_decl try: self._parser.SkippedEntityHandler = None except AttributeError: # some pyexpat versions do not support SkippedEntity pass def safe_minidom_parse_string(xml_string): """Parse an XML string using minidom safely. """ try: return minidom.parseString( # nosec xml_string, parser=ProtectedExpatParser()) # nosec except sax.SAXParseException: raise expat.ExpatError() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/context.py0000664000175000017500000000230000000000000016454 0ustar00zuulzuul00000000000000# 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 oslo_context import context from murano.common import policy class RequestContext(context.RequestContext): """Class that stores context info about an API request. Stores creditentials of the user, that is accessing API as well as additional request information. """ def __init__(self, session=None, service_catalog=None, **kwargs): super(RequestContext, self).__init__(**kwargs) self.session = session self.service_catalog = service_catalog if self.is_admin is None: self.is_admin = policy.check_is_admin(self) ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1696417899.7851808 murano-16.0.0/murano/db/0000775000175000017500000000000000000000000015010 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/db/__init__.py0000664000175000017500000000000000000000000017107 0ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/db/api.py0000664000175000017500000000141400000000000016133 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT 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 murano.db import models from murano.db import session def setup_db(): engine = session.get_engine() models.register_models(engine) def drop_db(): engine = session.get_engine() models.unregister_models(engine) ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1696417899.7851808 murano-16.0.0/murano/db/catalog/0000775000175000017500000000000000000000000016422 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/db/catalog/__init__.py0000664000175000017500000000000000000000000020521 0ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/db/catalog/api.py0000664000175000017500000005070100000000000017550 0ustar00zuulzuul00000000000000# 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 oslo_db import api as oslo_db_api from oslo_db import exception as db_exceptions from oslo_db.sqlalchemy import utils from oslo_log import log as logging import re import sqlalchemy as sa from sqlalchemy import or_ from sqlalchemy.orm import attributes # TODO(ruhe) use exception declared in openstack/common/db from webob import exc from murano.common.i18n import _ from murano.db import models from murano.db import session as db_session SEARCH_MAPPING = {'fqn': 'fully_qualified_name', 'name': 'name', 'created': 'created' } LOG = logging.getLogger(__name__) def _package_get(package_id, session): # TODO(sjmc7): update openstack/common and pull in # uuidutils, check that package_id_or_name resembles a # UUID before trying to treat it as one package = session.query(models.Package).get(package_id) if not package: msg = _("Package id '{pkg_id}' not found").format(pkg_id=package_id) LOG.error(msg) raise exc.HTTPNotFound(explanation=msg) return package def _authorize_package(package, context, allow_public=False): if package.owner_id != context.project_id: if not allow_public: msg = _("Package '{pkg_id}' is not owned by tenant " "'{tenant}'").format(pkg_id=package.id, tenant=context.project_id) LOG.error(msg) raise exc.HTTPForbidden(explanation=msg) if not package.is_public: msg = _("Package '{pkg_id}' is not public and not owned by " "tenant '{tenant}' ").format(pkg_id=package.id, tenant=context.project_id) LOG.error(msg) raise exc.HTTPForbidden(explanation=msg) def package_get(package_id, context): """Return package details :param package_id: ID or name of a package, string :returns: detailed information about package, dict """ session = db_session.get_session() package = _package_get(package_id, session) if not context.is_admin: _authorize_package(package, context, allow_public=True) return package def _get_categories(category_names, session=None): """Return existing category objects or raise an exception. :param category_names: name of categories to associate with package, list :returns: list of Category objects to associate with package, list """ if session is None: session = db_session.get_session() categories = [] for ctg_name in category_names: ctg_obj = session.query(models.Category).filter_by( name=ctg_name).first() if not ctg_obj: msg = _("Category '{name}' doesn't exist").format(name=ctg_name) LOG.error(msg) # it's not allowed to specify non-existent categories raise exc.HTTPBadRequest(explanation=msg) categories.append(ctg_obj) return categories def _existing_tag(tag_name, session=None): if session is None: session = db_session.get_session() return session.query(models.Tag).filter_by(name=tag_name).first() def _get_tags(tag_names, session=None): """Return existing tags object or create new ones :param tag_names: name of tags to associate with package, list :returns: list of Tag objects to associate with package, list """ if session is None: session = db_session.get_session() tags = [] # This function can be called inside a transaction and outside it. # In the former case this line is no-op, in the latter # starts a transaction we need to be inside a transaction, to correctly # handle DBDuplicateEntry errors without failing the whole transaction. # For more take a look at SQLAlchemy docs. with session.begin(subtransactions=True): for tag_name in tag_names: tag_obj = _existing_tag(tag_name, session) if not tag_obj: try: # Start a new SAVEPOINT transaction. If it fails # only the savepoint will be roll backed, not the # whole transaction. with session.begin(nested=True): tag_obj = models.Tag(name=tag_name) session.add(tag_obj) session.flush(objects=[tag_obj]) except db_exceptions.DBDuplicateEntry: # new session is needed here to get access to the tag tag_obj = _existing_tag(tag_name) tags.append(tag_obj) return tags def _get_class_definitions(class_names, session=None): classes = [] for name in class_names: class_record = models.Class(name=name) classes.append(class_record) return classes def _do_replace(package, change): path = change['path'][0] value = change['value'] calculate = {'categories': _get_categories, 'tags': _get_tags} if path in ('categories', 'tags'): existing_items = getattr(package, path) duplicates = list(set(i.name for i in existing_items) & set(value)) unique_values = [x for x in value if x not in duplicates] items_to_replace = calculate[path](unique_values) # NOTE(efedorova): Replacing duplicate entities is not allowed, # so need to remove anything, but duplicates # and append everything but duplicates for item in list(existing_items): if item.name not in duplicates: existing_items.remove(item) existing_items.extend(items_to_replace) else: setattr(package, path, value) return package def _do_add(package, change): # Only categories and tags support addition path = change['path'][0] value = change['value'] calculate = {'categories': _get_categories, 'tags': _get_tags} items_to_add = calculate[path](value) for item in items_to_add: try: getattr(package, path).append(item) except AssertionError: LOG.warning('One of the specified {path} is already associated' ' with a package. Doing nothing.'.format(path=path)) return package def _do_remove(package, change): # Only categories and tags support removal def find(seq, predicate): for elt in seq: if predicate(elt): return elt path = change['path'][0] values = change['value'] current_values = getattr(package, path) for value in values: if value not in [i.name for i in current_values]: msg = _("Value '{value}' of property '{path}' " "does not exist.").format(value=value, path=path) LOG.error(msg) raise exc.HTTPNotFound(explanation=msg) item_to_remove = find(current_values, lambda i: i.name == value) current_values.remove(item_to_remove) return package def _get_packages_for_category(session, category_id): """Return detailed list of packages, belonging to the provided category :param session: :param category_id: :return: list of dictionaries, containing id, name and package fqn """ pkg = models.Package packages = (session.query(pkg.id, pkg.name, pkg.fully_qualified_name) .filter(pkg.categories .any(models.Category.id == category_id)) .all()) result_packages = [] for package in packages: id, name, fqn = package result_packages.append({'id': id, 'name': name, 'fully_qualified_name': fqn}) return result_packages @oslo_db_api.wrap_db_retry(max_retries=5, retry_on_deadlock=True) def package_update(pkg_id_or_name, changes, context): """Update package information :param changes: parameters to update :returns: detailed information about new package, dict """ operation_methods = {'add': _do_add, 'replace': _do_replace, 'remove': _do_remove} session = db_session.get_session() with session.begin(): pkg = _package_get(pkg_id_or_name, session) was_private = not pkg.is_public if not context.is_admin: _authorize_package(pkg, context) for change in changes: pkg = operation_methods[change['op']](pkg, change) became_public = pkg.is_public class_names = [clazz.name for clazz in pkg.class_definitions] if was_private and became_public: with db_session.get_lock("public_packages", session): _check_for_public_packages_with_fqn(session, pkg.fully_qualified_name, pkg.id) _check_for_existing_classes(session, class_names, None, check_public=True, ignore_package_with_id=pkg.id) session.add(pkg) return pkg def package_search(filters, context, manage_public=False, limit=None, catalog=False): """Search packages with different filters Catalog param controls the base query creation. Catalog queries only search packages a user can deploy. Non-catalog queries searches packages a user can edit. * Admin is allowed to browse all the packages * Regular user is allowed to browse all packages belongs to user tenant and all other packages marked is_public in catalog mode. In edit-mode non-admins are able to get own packages and public packages if corresponding policy is passed. * Use marker (inside filters param) and limit for pagination: The typical pattern of limit and marker is to make an initial limited request and then to use the ID of the last package from the response as the marker parameter in a subsequent limited request. """ # pylint: disable=too-many-branches session = db_session.get_session() pkg = models.Package query = session.query(pkg) if catalog: # Only show packages one can deploy, i.e. own + public query = query.filter(or_(pkg.owner_id == context.project_id, pkg.is_public)) else: # Show packages one can edit. if not context.is_admin: if manage_public: query = query.filter(or_(pkg.owner_id == context.project_id, pkg.is_public)) else: query = query.filter(pkg.owner_id == context.project_id) # No else here admin can edit everything. if not filters.get('include_disabled', '').lower() == 'true': query = query.filter(pkg.enabled) if filters.get('owned', '').lower() == 'true': query = query.filter(pkg.owner_id == context.project_id) if 'type' in filters.keys(): query = query.filter(pkg.type == filters['type'].title()) if 'id' in filters: query = query.filter(models.Package.id.in_(filters['id'])) if 'category' in filters.keys(): query = query.filter(pkg.categories.any( models.Category.name.in_(filters['category']))) if 'tag' in filters.keys(): query = query.filter(pkg.tags.any( models.Tag.name.in_(filters['tag']))) if 'class_name' in filters.keys(): query = query.filter(pkg.class_definitions.any( models.Class.name == filters['class_name'])) if 'fqn' in filters.keys(): query = query.filter(pkg.fully_qualified_name == filters['fqn']) if 'name' in filters.keys(): query = query.filter(pkg.name == filters['name']) if 'search' in filters.keys(): fk_fields = {'categories': 'Category', 'tags': 'Tag', 'class_definitions': 'Class'} # the default search order fields = ['name', 'fully_qualified_name', 'description', 'categories', 'tags', 'class_definitions', 'author'] # split to searching words key_words = re.split(';|,', filters['search']) conditions = [] order_cases = [] sorted_fields = fields + list(set(dir(pkg)).difference(set(fields))) for index in range(0, len(sorted_fields)): attr = sorted_fields[index] if attr.startswith('_'): continue if not isinstance(getattr(pkg, attr), attributes.InstrumentedAttribute): continue priority = min(index, len(fields)) for key_word in key_words: _word = u'%{value}%'.format(value=key_word) if attr in fk_fields.keys(): condition = getattr(pkg, attr).any( getattr(models, fk_fields[attr]).name.like(_word)) conditions.append(condition) order_cases.append((condition, priority)) elif isinstance(getattr(pkg, attr) .property.columns[0].type, sa.String): condition = getattr(pkg, attr).like(_word) conditions.append(condition) order_cases.append((condition, priority)) order_expression = sa.case(order_cases).label("tmp_weight_uuid") query = query.filter(or_(*conditions)).order_by(order_expression.asc()) sort_keys = [SEARCH_MAPPING[sort_key] for sort_key in filters.get('order_by', ['name'])] sort_keys.append('id') marker = filters.get('marker') sort_dir = filters.get('sort_dir') if marker is not None: # set marker to real object instead of its id marker = _package_get(marker, session) query = utils.paginate_query( query, pkg, limit, sort_keys, marker, sort_dir) return query.all() @oslo_db_api.wrap_db_retry(max_retries=5, retry_on_deadlock=True) def package_upload(values, tenant_id): """Upload a package with new application :param values: parameters describing the new package :returns: detailed information about new package, dict """ session = db_session.get_session() package = models.Package() composite_attr_to_func = {'categories': _get_categories, 'tags': _get_tags, 'class_definitions': _get_class_definitions} is_public = values.get('is_public', False) if is_public: public_lock = db_session.get_lock("public_packages", session) else: public_lock = None tenant_lock = db_session.get_lock("classes_of_" + tenant_id, session) try: _check_for_existing_classes(session, values.get('class_definitions'), tenant_id, check_public=is_public) if is_public: _check_for_public_packages_with_fqn( session, values.get('fully_qualified_name')) for attr, func in composite_attr_to_func.items(): if values.get(attr): result = func(values[attr], session) setattr(package, attr, result) del values[attr] package.update(values) package.owner_id = tenant_id package.save(session) tenant_lock.commit() if public_lock is not None: public_lock.commit() except Exception: tenant_lock.rollback() if public_lock is not None: public_lock.rollback() raise return package @oslo_db_api.wrap_db_retry(max_retries=5, retry_on_deadlock=True) def package_delete(package_id, context): """Delete a package by ID.""" session = db_session.get_session() with session.begin(): package = _package_get(package_id, session) if not context.is_admin and package.owner_id != context.project_id: raise exc.HTTPForbidden( explanation="Package is not owned by the" " tenant '{0}'".format(context.project_id)) session.delete(package) def category_get(category_id, session=None, packages=False): """Return category details :param category_id: ID of a category, string :returns: detailed information about category, dict """ if not session: session = db_session.get_session() category = session.query(models.Category).get(category_id) if not category: msg = _("Category id '{id}' not found").format(id=category_id) LOG.error(msg) raise exc.HTTPNotFound(msg) if packages: category.packages = _get_packages_for_category(session, category_id) return category def categories_list(filters=None, limit=None, marker=None): if filters is None: filters = {} sort_keys = filters.get('sort_keys', ['name']) sort_dir = filters.get('sort_dir', 'asc') session = db_session.get_session() query = session.query(models.Category) if marker is not None: marker = category_get(marker, session) query = utils.paginate_query( query, models.Category, limit, sort_keys, marker, sort_dir) return query.all() def category_get_names(): session = db_session.get_session() categories = [] for row in session.query(models.Category.name).all(): for name in row: categories.append(name) return categories def category_add(category_name): session = db_session.get_session() category = models.Category() with session.begin(): category.update({'name': category_name}) # NOTE(kzaitsev) update package_count, so we can safely access from # outside the session category.package_count = 0 category.save(session) return category def category_delete(category_id): """Delete a category by ID.""" session = db_session.get_session() with session.begin(): category = session.query(models.Category).get(category_id) if not category: msg = _("Category id '{id}' not found").format(id=category_id) LOG.error(msg) raise exc.HTTPNotFound(msg) session.delete(category) def _check_for_existing_classes(session, class_names, tenant_id, check_public=False, ignore_package_with_id=None): if not class_names: return q = session.query(models.Class.name).filter( models.Class.name.in_(class_names)) private_filter = None public_filter = None predicate = None if tenant_id is not None: private_filter = models.Class.package.has( models.Package.owner_id == tenant_id) if check_public: public_filter = models.Class.package.has( models.Package.is_public) if private_filter is not None and public_filter is not None: predicate = sa.or_(private_filter, public_filter) elif private_filter is not None: predicate = private_filter elif public_filter is not None: predicate = public_filter if predicate is not None: q = q.filter(predicate) if ignore_package_with_id is not None: q = q.filter(models.Class.package_id != ignore_package_with_id) if q.first() is not None: msg = _('Class with the same full name is already ' 'registered in the visibility scope') LOG.error(msg) raise exc.HTTPConflict(msg) def _check_for_public_packages_with_fqn(session, fqn, ignore_package_with_id=None): q = session.query(models.Package.id).\ filter(models.Package.is_public).\ filter(models.Package.fully_qualified_name == fqn) if ignore_package_with_id is not None: q = q.filter(models.Package.id != ignore_package_with_id) if q.first() is not None: msg = _('Package with the same Name is already made public') LOG.error(msg) raise exc.HTTPConflict(msg) ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1696417899.7891808 murano-16.0.0/murano/db/cfapi_migration/0000775000175000017500000000000000000000000020143 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/db/cfapi_migration/__init__.py0000664000175000017500000000000000000000000022242 0ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/db/cfapi_migration/alembic.ini0000664000175000017500000000170600000000000022244 0ustar00zuulzuul00000000000000# A generic, single database configuration. [alembic] # path to migration scripts script_location = murano/db/cfapi_migration/alembic_migrations # template used to generate migration files # file_template = %%(rev)s_%%(slug)s # max length of characters to apply to the # "slug" field #truncate_slug_length = 40 # set to 'true' to run the environment during # the 'revision' command, regardless of autogenerate # revision_environment = false sqlalchemy.url = # Logging configuration [loggers] keys = root,sqlalchemy,alembic [handlers] keys = console [formatters] keys = generic [logger_root] level = WARN handlers = console qualname = [logger_sqlalchemy] level = WARN handlers = qualname = sqlalchemy.engine [logger_alembic] level = INFO handlers = qualname = alembic [handler_console] class = StreamHandler args = (sys.stderr,) level = NOTSET formatter = generic [formatter_generic] format = %(levelname)-5.5s [%(name)s] %(message)s datefmt = %H:%M:%S ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1696417899.7891808 murano-16.0.0/murano/db/cfapi_migration/alembic_migrations/0000775000175000017500000000000000000000000023773 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/db/cfapi_migration/alembic_migrations/README0000664000175000017500000000102000000000000024644 0ustar00zuulzuul00000000000000Please see https://alembic.readthedocs.org/en/latest/index.html for general documentation To create alembic migrations use: $ murano-cfapi-db-manage revision --message --autogenerate Stamp db with most recent migration version, without actually running migrations $ murano-cfapi-db-manage stamp --revision head Upgrade can be performed by: $ murano-cfapi-db-manage upgrade $ murano-cfapi-db-manage upgrade --revision head Downgrading db: $ murano-cfapi-db-manage downgrade $ murano-cfapi-db-manage downgrade --revision base ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/db/cfapi_migration/alembic_migrations/env.py0000664000175000017500000000274100000000000025141 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # 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 alembic import context from sqlalchemy import create_engine, pool from murano.db import cfapi_models as models # this is the Alembic Config object, which provides # access to the values within the .ini file in use. config = context.config murano_config = config.murano_config # add your model's MetaData object here # for 'autogenerate' support # from myapp import mymodel target_metadata = models.Base.metadata def run_migrations_online(): """Run migrations in 'online' mode. In this scenario we need to create an Engine and associate a connection with the context. """ engine = create_engine( murano_config.database.connection, poolclass=pool.NullPool) with engine.connect() as connection: context.configure(connection=connection, target_metadata=target_metadata) with context.begin_transaction(): context.run_migrations() run_migrations_online() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/db/cfapi_migration/alembic_migrations/script.py.mako0000664000175000017500000000176500000000000026610 0ustar00zuulzuul00000000000000# Copyright ${create_date.year} 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. """${message} Revision ID: ${up_revision} Revises: ${down_revision} Create Date: ${create_date} """ # revision identifiers, used by Alembic. revision = ${repr(up_revision)} down_revision = ${repr(down_revision)} from alembic import op import sqlalchemy as sa ${imports if imports else ""} def upgrade(): ${upgrades if upgrades else "pass"} def downgrade(): ${downgrades if downgrades else "pass"}././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1696417899.7891808 murano-16.0.0/murano/db/cfapi_migration/alembic_migrations/versions/0000775000175000017500000000000000000000000025643 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/db/cfapi_migration/alembic_migrations/versions/001_initial_version.py0000664000175000017500000000376000000000000032001 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or # implied. # See the License for the specific language governing permissions and # limitations under the License. """ Revision ID: 001 Revises: None Create Date: 2016-03-30 16:34:33.698760 """ from alembic import op import sqlalchemy as sa # revision identifiers, used by Alembic. revision = '001' down_revision = None MYSQL_ENGINE = 'InnoDB' MYSQL_CHARSET = 'utf8' def upgrade(): op.create_table( 'cf_orgs', sa.Column('id', sa.String(length=255), nullable=False), sa.Column('tenant', sa.String(length=255), nullable=False), sa.UniqueConstraint('tenant'), sa.PrimaryKeyConstraint('id'), mysql_engine=MYSQL_ENGINE, mysql_charset=MYSQL_CHARSET) op.create_table( 'cf_spaces', sa.Column('id', sa.String(length=255), nullable=False), sa.Column('environment_id', sa.String(length=255), nullable=False), sa.UniqueConstraint('environment_id'), sa.PrimaryKeyConstraint('id'), mysql_engine=MYSQL_ENGINE, mysql_charset=MYSQL_CHARSET) op.create_table( 'cf_serv_inst', sa.Column('id', sa.String(length=255), primary_key=True), sa.Column('service_id', sa.String(255), nullable=False), sa.Column('environment_id', sa.String(255), nullable=False), sa.Column('tenant', sa.String(255), nullable=False), mysql_engine=MYSQL_ENGINE, mysql_charset=MYSQL_CHARSET) # end Alembic commands # def downgrade(): op.drop_table('cf_orgs') op.drop_table('cf_spaces') op.drop_table('cf_serv_inst') # end Alembic commands # ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/db/cfapi_migration/migration.py0000664000175000017500000000523300000000000022511 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # 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 alembic from alembic import config as alembic_config from alembic import migration as alembic_migration from murano.db import session as db_session def get_alembic_config(): path = os.path.join(os.path.dirname(__file__), 'alembic.ini') config = alembic_config.Config(path) config.set_main_option('script_location', 'murano.db.cfapi_migration:alembic_migrations') return config def version(engine=None): """Returns current database version.""" engine = engine or db_session.get_engine() with engine.connect() as conn: context = alembic_migration.MigrationContext.configure(conn) return context.get_current_revision() def upgrade(revision, config=None): """Used for upgrading database. :param version: Desired database version :type version: string """ revision = revision or 'head' config = config or get_alembic_config() alembic.command.upgrade(config, revision) def downgrade(revision, config=None): """Used for downgrading database. :param version: Desired database version7 :type version: string """ revision = revision or 'base' config = config or get_alembic_config() return alembic.command.downgrade(config, revision) def stamp(revision, config=None): """Stamps database with provided revision. Don't run any migrations. :param revision: Should match one from repository or head - to stamp database with most recent revision :type revision: string """ config = config or get_alembic_config() return alembic.command.stamp(config, revision=revision) def revision(message=None, autogenerate=False, config=None): """Creates template for migration. :param message: Text that will be used for migration title :type message: string :param autogenerate: If True - generates diff based on current database state :type autogenerate: bool """ config = config or get_alembic_config() return alembic.command.revision(config, message=message, autogenerate=autogenerate) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/db/cfapi_models.py0000664000175000017500000000361600000000000020015 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. """ SQLAlchemy models for service broker data """ from oslo_db.sqlalchemy import models import sqlalchemy as sa from sqlalchemy.ext import declarative class _ServiceBrokerBase(models.ModelBase): pass Base = declarative.declarative_base(cls=_ServiceBrokerBase) class CFOrganization(Base): __tablename__ = "cf_orgs" id = sa.Column(sa.String(255), primary_key=True) tenant = sa.Column(sa.String(255), nullable=False) class CFSpace(Base): __tablename__ = "cf_spaces" id = sa.Column(sa.String(255), primary_key=True) environment_id = sa.Column(sa.String(255), nullable=False) class CFServiceInstance(Base): __tablename__ = 'cf_serv_inst' id = sa.Column(sa.String(255), primary_key=True) service_id = sa.Column(sa.String(255), nullable=False) environment_id = sa.Column(sa.String(255), nullable=False) tenant = sa.Column(sa.String(255), nullable=False) def register_models(engine): """Creates database tables for all models with the given engine.""" models = (CFSpace, CFOrganization, CFServiceInstance) for model in models: model.metadata.create_all(engine) def unregister_models(engine): """Drops database tables for all models with the given engine.""" models = (CFOrganization, CFSpace, CFServiceInstance) for model in models: model.metadata.drop_all(engine) ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1696417899.7891808 murano-16.0.0/murano/db/migration/0000775000175000017500000000000000000000000017001 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/db/migration/__init__.py0000664000175000017500000000000000000000000021100 0ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/db/migration/alembic.ini0000664000175000017500000000167700000000000021111 0ustar00zuulzuul00000000000000# A generic, single database configuration. [alembic] # path to migration scripts script_location = murano/db/migration/alembic_migrations # template used to generate migration files # file_template = %%(rev)s_%%(slug)s # max length of characters to apply to the # "slug" field #truncate_slug_length = 40 # set to 'true' to run the environment during # the 'revision' command, regardless of autogenerate # revision_environment = false sqlalchemy.url = # Logging configuration [loggers] keys = root,sqlalchemy,alembic [handlers] keys = console [formatters] keys = generic [logger_root] level = WARN handlers = console qualname = [logger_sqlalchemy] level = WARN handlers = qualname = sqlalchemy.engine [logger_alembic] level = INFO handlers = qualname = alembic [handler_console] class = StreamHandler args = (sys.stderr,) level = NOTSET formatter = generic [formatter_generic] format = %(levelname)-5.5s [%(name)s] %(message)s datefmt = %H:%M:%S././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1696417899.7891808 murano-16.0.0/murano/db/migration/alembic_migrations/0000775000175000017500000000000000000000000022631 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/db/migration/alembic_migrations/README0000664000175000017500000000075400000000000023517 0ustar00zuulzuul00000000000000Please see https://alembic.readthedocs.org/en/latest/index.html for general documentation To create alembic migrations use: $ murano-db-manage revision --message --autogenerate Stamp db with most recent migration version, without actually running migrations $ murano-db-manage stamp --revision head Upgrade can be performed by: $ murano-db-manage upgrade $ murano-db-manage upgrade --revision head Downgrading db: $ murano-db-manage downgrade $ murano-db-manage downgrade --revision base ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/db/migration/alembic_migrations/env.py0000664000175000017500000000272100000000000023775 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # 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 alembic import context from sqlalchemy import create_engine, pool from murano.db import models # this is the Alembic Config object, which provides # access to the values within the .ini file in use. config = context.config murano_config = config.murano_config # add your model's MetaData object here # for 'autogenerate' support # from myapp import mymodel target_metadata = models.Base.metadata def run_migrations_online(): """Run migrations in 'online' mode. In this scenario we need to create an Engine and associate a connection with the context. """ engine = create_engine( murano_config.database.connection, poolclass=pool.NullPool) with engine.connect() as connection: context.configure(connection=connection, target_metadata=target_metadata) with context.begin_transaction(): context.run_migrations() run_migrations_online() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/db/migration/alembic_migrations/script.py.mako0000664000175000017500000000176500000000000025446 0ustar00zuulzuul00000000000000# Copyright ${create_date.year} 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. """${message} Revision ID: ${up_revision} Revises: ${down_revision} Create Date: ${create_date} """ # revision identifiers, used by Alembic. revision = ${repr(up_revision)} down_revision = ${repr(down_revision)} from alembic import op import sqlalchemy as sa ${imports if imports else ""} def upgrade(): ${upgrades if upgrades else "pass"} def downgrade(): ${downgrades if downgrades else "pass"}././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1696417899.793181 murano-16.0.0/murano/db/migration/alembic_migrations/versions/0000775000175000017500000000000000000000000024501 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/db/migration/alembic_migrations/versions/001_initial_version.py0000664000175000017500000002501500000000000030634 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or # implied. # See the License for the specific language governing permissions and # limitations under the License. """empty message Revision ID: 001 Revises: None Create Date: 2014-05-29 16:32:33.698760 """ import uuid from alembic import op from oslo_utils import timeutils import sqlalchemy as sa from sqlalchemy.sql.expression import table as sa_table from murano.common import consts from murano.db.sqla import types as st # revision identifiers, used by Alembic. revision = '001' down_revision = None MYSQL_ENGINE = 'InnoDB' MYSQL_CHARSET = 'utf8' def _create_default_categories(op): bind = op.get_bind() table = sa_table( 'category', sa.Column('id', sa.String(length=36), primary_key=True), sa.Column('created', sa.DateTime()), sa.Column('updated', sa.DateTime()), sa.Column('name', sa.String(length=80))) now = timeutils.utcnow() for category in consts.CATEGORIES: values = {'id': uuid.uuid4().hex, 'name': category, 'updated': now, 'created': now} bind.execute(table.insert(values=values)) def upgrade(): op.create_table( 'environment', sa.Column('created', sa.DateTime(), nullable=False), sa.Column('updated', sa.DateTime(), nullable=False), sa.Column('id', sa.String(length=255), nullable=False), sa.Column('name', sa.String(length=255), nullable=False), sa.Column('tenant_id', sa.String(length=36), nullable=False), sa.Column('version', sa.BigInteger(), nullable=False), sa.Column('description', sa.Text(), nullable=False), sa.Column('networking', sa.Text(), nullable=True), sa.PrimaryKeyConstraint('id'), sa.UniqueConstraint('tenant_id', 'name'), mysql_engine=MYSQL_ENGINE, mysql_charset=MYSQL_CHARSET ) op.create_table( 'tag', sa.Column('created', sa.DateTime(), nullable=False), sa.Column('updated', sa.DateTime(), nullable=False), sa.Column('id', sa.String(length=36), nullable=False), sa.Column('name', sa.String(length=80), nullable=False), sa.PrimaryKeyConstraint('id'), sa.UniqueConstraint('name'), mysql_engine=MYSQL_ENGINE, mysql_charset=MYSQL_CHARSET ) op.create_table( 'category', sa.Column('created', sa.DateTime(), nullable=False), sa.Column('updated', sa.DateTime(), nullable=False), sa.Column('id', sa.String(length=36), nullable=False), sa.Column('name', sa.String(length=80), nullable=False, unique=True), sa.PrimaryKeyConstraint('id'), mysql_engine=MYSQL_ENGINE, mysql_charset=MYSQL_CHARSET ) op.create_index('ix_category_name', 'category', ['name']) op.create_table( 'apistats', sa.Column('created', sa.DateTime(), nullable=False), sa.Column('updated', sa.DateTime(), nullable=False), sa.Column('id', sa.Integer(), nullable=False), sa.Column('host', sa.String(length=80), nullable=True), sa.Column('request_count', sa.BigInteger(), nullable=True), sa.Column('error_count', sa.BigInteger(), nullable=True), sa.Column('average_response_time', sa.Float(), nullable=True), sa.Column('requests_per_tenant', sa.Text(), nullable=True), sa.Column('requests_per_second', sa.Float(), nullable=True), sa.Column('errors_per_second', sa.Float(), nullable=True), sa.Column('cpu_count', sa.Integer(), nullable=True), sa.Column('cpu_percent', sa.Float(), nullable=True), sa.PrimaryKeyConstraint('id'), mysql_engine=MYSQL_ENGINE, mysql_charset=MYSQL_CHARSET ) op.create_table( 'instance_stats', sa.Column('environment_id', sa.String(length=255), nullable=False), sa.Column('instance_id', sa.String(length=255), nullable=False), sa.Column('instance_type', sa.Integer(), nullable=False), sa.Column('created', sa.Integer(), nullable=False), sa.Column('destroyed', sa.Integer(), nullable=True), sa.Column('type_name', sa.String(length=512), nullable=False), sa.Column('type_title', sa.String(length=512), nullable=True), sa.Column('unit_count', sa.Integer(), nullable=True), sa.Column('tenant_id', sa.String(length=36), nullable=False), sa.PrimaryKeyConstraint('environment_id', 'instance_id'), mysql_engine=MYSQL_ENGINE, mysql_charset=MYSQL_CHARSET ) op.create_table( 'package', sa.Column('created', sa.DateTime(), nullable=False), sa.Column('updated', sa.DateTime(), nullable=False), sa.Column('id', sa.String(length=36), nullable=False), sa.Column('archive', st.LargeBinary(), nullable=True), sa.Column('fully_qualified_name', sa.String(length=128), nullable=False, unique=True), sa.Column('type', sa.String(length=20), nullable=False), sa.Column('author', sa.String(length=80), nullable=True), sa.Column('name', sa.String(length=80), nullable=False), sa.Column('enabled', sa.Boolean(), nullable=True), sa.Column('description', sa.String(length=512), nullable=False), sa.Column('is_public', sa.Boolean(), nullable=True), sa.Column('logo', st.LargeBinary(), nullable=True), sa.Column('owner_id', sa.String(length=36), nullable=False), sa.Column('ui_definition', sa.Text(), nullable=True), sa.PrimaryKeyConstraint('id'), mysql_engine=MYSQL_ENGINE, mysql_charset=MYSQL_CHARSET ) op.create_index('ix_package_fqn', 'package', ['fully_qualified_name']) op.create_table( 'session', sa.Column('created', sa.DateTime(), nullable=False), sa.Column('updated', sa.DateTime(), nullable=False), sa.Column('id', sa.String(length=36), nullable=False), sa.Column('environment_id', sa.String(length=255), nullable=True), sa.Column('user_id', sa.String(length=36), nullable=False), sa.Column('state', sa.String(length=36), nullable=False), sa.Column('description', sa.Text(), nullable=False), sa.Column('version', sa.BigInteger(), nullable=False), sa.ForeignKeyConstraint(['environment_id'], ['environment.id'], ), sa.PrimaryKeyConstraint('id'), mysql_engine=MYSQL_ENGINE, mysql_charset=MYSQL_CHARSET ) op.create_table( 'deployment', sa.Column('created', sa.DateTime(), nullable=False), sa.Column('updated', sa.DateTime(), nullable=False), sa.Column('id', sa.String(length=36), nullable=False), sa.Column('started', sa.DateTime(), nullable=False), sa.Column('finished', sa.DateTime(), nullable=True), sa.Column('description', sa.Text(), nullable=False), sa.Column('environment_id', sa.String(length=255), nullable=True), sa.ForeignKeyConstraint(['environment_id'], ['environment.id'], ), sa.PrimaryKeyConstraint('id'), mysql_engine=MYSQL_ENGINE, mysql_charset=MYSQL_CHARSET ) op.create_table( 'class_definition', sa.Column('created', sa.DateTime(), nullable=False), sa.Column('updated', sa.DateTime(), nullable=False), sa.Column('id', sa.String(length=36), nullable=False), sa.Column('name', sa.String(length=128), nullable=False, unique=True), sa.Column('package_id', sa.String(length=36), nullable=True), sa.ForeignKeyConstraint(['package_id'], ['package.id'], ), sa.PrimaryKeyConstraint('id'), mysql_engine=MYSQL_ENGINE, mysql_charset=MYSQL_CHARSET ) op.create_index('ix_class_definition_name', 'class_definition', ['name']) op.create_table( 'status', sa.Column('created', sa.DateTime(), nullable=False), sa.Column('updated', sa.DateTime(), nullable=False), sa.Column('id', sa.String(length=36), nullable=False), sa.Column('entity_id', sa.String(length=255), nullable=True), sa.Column('entity', sa.String(length=10), nullable=True), sa.Column('deployment_id', sa.String(length=36), nullable=True), sa.Column('text', sa.Text(), nullable=False), sa.Column('level', sa.String(length=32), nullable=False), sa.Column('details', sa.Text(), nullable=True), sa.ForeignKeyConstraint(['deployment_id'], ['deployment.id'], ), sa.PrimaryKeyConstraint('id'), mysql_engine=MYSQL_ENGINE, mysql_charset=MYSQL_CHARSET ) op.create_table( 'package_to_tag', sa.Column('package_id', sa.String(length=36), nullable=False), sa.Column('tag_id', sa.String(length=36), nullable=False), sa.ForeignKeyConstraint(['package_id'], ['package.id'], ), sa.ForeignKeyConstraint(['tag_id'], ['tag.id'], ondelete='CASCADE'), mysql_engine=MYSQL_ENGINE, mysql_charset=MYSQL_CHARSET ) op.create_table( 'package_to_category', sa.Column('package_id', sa.String(length=36), nullable=False), sa.Column('category_id', sa.String(length=36), nullable=False), sa.ForeignKeyConstraint(['category_id'], ['category.id'], ondelete='RESTRICT'), sa.ForeignKeyConstraint(['package_id'], ['package.id'], ), mysql_engine=MYSQL_ENGINE, mysql_charset=MYSQL_CHARSET ) _create_default_categories(op) # end Alembic commands # def downgrade(): op.drop_index('ix_category_name', table_name='category') op.drop_index('ix_package_fqn', table_name='package') op.drop_index('ix_class_definition_name', table_name='class_definition') op.drop_table('status') op.drop_table('package_to_category') op.drop_table('class_definition') op.drop_table('deployment') op.drop_table('package_to_tag') op.drop_table('session') op.drop_table('instance_stats') op.drop_table('package') op.drop_table('apistats') op.drop_table('category') op.drop_table('tag') op.drop_table('environment') # end Alembic commands # ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/db/migration/alembic_migrations/versions/002_add_package_supplier_info.py0000664000175000017500000000224700000000000032602 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or # implied. # See the License for the specific language governing permissions and # limitations under the License. """empty message Revision ID: 002 Revises: None Create Date: 2014-06-23 16:34:33.698760 """ from alembic import op import sqlalchemy as sa # revision identifiers, used by Alembic. revision = '002' down_revision = '001' MYSQL_ENGINE = 'InnoDB' MYSQL_CHARSET = 'utf8' def upgrade(): op.add_column( 'package', sa.Column('supplier_logo', sa.types.LargeBinary) ) op.add_column( 'package', sa.Column('supplier', sa.types.Text()) ) # end Alembic commands # def downgrade(): op.drop_column('package', 'supplier') op.drop_column('package', 'supplier_logo') # end Alembic commands # ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/db/migration/alembic_migrations/versions/003_add_action_entry.py0000664000175000017500000000603500000000000030747 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or # implied. # See the License for the specific language governing permissions and # limitations under the License. """ Add action column to deployment table. Revision ID: 003 Revises: table deployment Create Date: 2014-07-30 16:11:33.244 """ from alembic import op import sqlalchemy as sa import murano.db.migration.helpers as helpers # revision identifiers, used by Alembic. revision = '003' down_revision = '002' MYSQL_ENGINE = 'InnoDB' MYSQL_CHARSET = 'utf8' def upgrade(): op.rename_table('deployment', 'task') op.add_column( 'task', sa.Column('action', sa.types.Text()) ) op.create_table( 'deployment', sa.Column('id', sa.String(length=36), nullable=False)) helpers.transform_table( 'status', {'deployment_id': 'task_id'}, {}, sa.Column('created', sa.DateTime(), nullable=False), sa.Column('updated', sa.DateTime(), nullable=False), sa.Column('id', sa.String(length=36), nullable=False), sa.Column('entity_id', sa.String(length=255), nullable=True), sa.Column('entity', sa.String(length=10), nullable=True), sa.Column('task_id', sa.String(length=36), nullable=True), sa.Column('text', sa.Text(), nullable=False), sa.Column('level', sa.String(length=32), nullable=False), sa.Column('details', sa.Text(), nullable=True), sa.ForeignKeyConstraint(['task_id'], ['task.id'], ), sa.PrimaryKeyConstraint('id'), mysql_engine=MYSQL_ENGINE, mysql_charset=MYSQL_CHARSET ) op.drop_table('deployment') def downgrade(): op.drop_column('task', 'action') op.rename_table('task', 'deployment') op.create_table( 'task', sa.Column('id', sa.String(length=36), nullable=False)) helpers.transform_table( 'status', {'task_id': 'deployment_id'}, {}, sa.Column('created', sa.DateTime(), nullable=False), sa.Column('updated', sa.DateTime(), nullable=False), sa.Column('id', sa.String(length=36), nullable=False), sa.Column('entity_id', sa.String(length=255), nullable=True), sa.Column('entity', sa.String(length=10), nullable=True), sa.Column('deployment_id', sa.String(length=36), nullable=True), sa.Column('text', sa.Text(), nullable=False), sa.Column('level', sa.String(length=32), nullable=False), sa.Column('details', sa.Text(), nullable=True), sa.ForeignKeyConstraint(['deployment_id'], ['deployment.id'], ), sa.PrimaryKeyConstraint('id'), mysql_engine=MYSQL_ENGINE, mysql_charset=MYSQL_CHARSET ) op.drop_table('task') # end Alembic commands # ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/db/migration/alembic_migrations/versions/004_change_package_desc_type.py0000664000175000017500000001542300000000000032402 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or # implied. # See the License for the specific language governing permissions and # limitations under the License. """ Change type of description field in package table. Revision ID: 004 Revises: table package """ from alembic import op import sqlalchemy as sa import murano.db.migration.helpers as helpers from murano.db.sqla import types as st # revision identifiers, used by Alembic. revision = '004' down_revision = '003' MYSQL_ENGINE = 'InnoDB' MYSQL_CHARSET = 'utf8' def upgrade(): engine = op.get_bind() if engine.dialect.dialect_description.startswith('mysql'): engine.execute('SET FOREIGN_KEY_CHECKS=0') if engine.dialect.dialect_description == 'postgresql+psycopg2': op.drop_constraint('package_to_tag_package_id_fkey', 'package_to_tag', 'foreignkey') op.drop_constraint('package_to_tag_tag_id_fkey', 'package_to_tag', 'foreignkey') op.drop_constraint('package_to_category_package_id_fkey', 'package_to_category', 'foreignkey') op.drop_constraint('class_definition_package_id_fkey', 'class_definition', 'foreignkey') helpers.transform_table( 'package', {}, {}, sa.Column('created', sa.DateTime(), nullable=False), sa.Column('updated', sa.DateTime(), nullable=False), sa.Column('id', sa.String(length=36), nullable=False), sa.Column('archive', st.LargeBinary(), nullable=True), sa.Column('fully_qualified_name', sa.String(length=128), nullable=False, unique=True), sa.Column('type', sa.String(length=20), nullable=False), sa.Column('author', sa.String(length=80), nullable=True), sa.Column('name', sa.String(length=80), nullable=False), sa.Column('enabled', sa.Boolean(), nullable=True), sa.Column('description', sa.Text(), nullable=False), sa.Column('is_public', sa.Boolean(), nullable=True), sa.Column('logo', st.LargeBinary(), nullable=True), sa.Column('owner_id', sa.String(length=36), nullable=False), sa.Column('ui_definition', sa.Text(), nullable=True), sa.Column('supplier_logo', sa.types.LargeBinary), sa.Column('supplier', sa.types.Text()), sa.PrimaryKeyConstraint('id'), mysql_engine=MYSQL_ENGINE, mysql_charset=MYSQL_CHARSET ) op.create_index('ix_package_fqn', 'package', ['fully_qualified_name']) if engine.dialect.dialect_description.startswith('mysql'): engine.execute('SET FOREIGN_KEY_CHECKS=1') if engine.dialect.dialect_description == 'postgresql+psycopg2': op.create_foreign_key('package_to_tag_package_id_fkey', 'package_to_tag', 'package', ['package_id'], ['id']) op.create_foreign_key('package_to_tag_tag_id_fkey', 'package_to_tag', 'tag', ['tag_id'], ['id']) op.create_foreign_key('package_to_category_package_id_fkey', 'package_to_category', 'package', ['package_id'], ['id']) op.create_foreign_key('class_definition_package_id_fkey', 'class_definition', 'package', ['package_id'], ['id']) # end Alembic commands # def downgrade(): engine = op.get_bind() if engine.dialect.dialect_description.startswith('mysql'): engine.execute('SET FOREIGN_KEY_CHECKS=0') if engine.dialect.dialect_description == 'postgresql+psycopg2': op.drop_constraint('package_to_tag_package_id_fkey', 'package_to_tag', 'foreignkey') op.drop_constraint('package_to_tag_tag_id_fkey', 'package_to_tag', 'foreignkey') op.drop_constraint('package_to_category_package_id_fkey', 'package_to_category', 'foreignkey') op.drop_constraint('class_definition_package_id_fkey', 'class_definition', 'foreignkey') helpers.transform_table( 'package', {}, {}, sa.Column('created', sa.DateTime(), nullable=False), sa.Column('updated', sa.DateTime(), nullable=False), sa.Column('id', sa.String(length=36), nullable=False), sa.Column('archive', st.LargeBinary(), nullable=True), sa.Column('fully_qualified_name', sa.String(length=128), nullable=False, unique=True), sa.Column('type', sa.String(length=20), nullable=False), sa.Column('author', sa.String(length=80), nullable=True), sa.Column('name', sa.String(length=80), nullable=False), sa.Column('enabled', sa.Boolean(), nullable=True), sa.Column('description', sa.String(length=512), nullable=False), sa.Column('is_public', sa.Boolean(), nullable=True), sa.Column('logo', st.LargeBinary(), nullable=True), sa.Column('owner_id', sa.String(length=36), nullable=False), sa.Column('ui_definition', sa.Text(), nullable=True), sa.Column('supplier_logo', sa.types.LargeBinary), sa.Column('supplier', sa.types.Text()), sa.PrimaryKeyConstraint('id'), mysql_engine=MYSQL_ENGINE, mysql_charset=MYSQL_CHARSET ) op.create_index('ix_package_fqn', 'package', ['fully_qualified_name']) if engine.dialect.dialect_description.startswith('mysql'): engine.execute('SET FOREIGN_KEY_CHECKS=1') if engine.dialect.dialect_description == 'postgresql+psycopg2': op.create_foreign_key('package_to_tag_package_id_fkey', 'package_to_tag', 'package', ['package_id'], ['id']) op.create_foreign_key('package_to_tag_tag_id_fkey', 'package_to_tag', 'tag', ['tag_id'], ['id']) op.create_foreign_key('package_to_category_package_id_fkey', 'package_to_category', 'package', ['package_id'], ['id']) op.create_foreign_key('class_definition_package_id_fkey', 'class_definition', 'package', ['package_id'], ['id']) # end Alembic commands # ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/db/migration/alembic_migrations/versions/005_environment-template.py0000664000175000017500000000345300000000000031621 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or # implied. # See the License for the specific language governing permissions and # limitations under the License. """ Create the environment-template table for the environment template functionality. Revision ID: 005 Revises: table template """ from alembic import op import sqlalchemy as sa # revision identifiers, used by Alembic. revision = '005' down_revision = '004' MYSQL_ENGINE = 'InnoDB' MYSQL_CHARSET = 'utf8' def upgrade(): """It creates the table environment-template. The name plus the tenant_id should be unique in the table, since each tenant cannot duplicate template names. """ op.create_table( 'environment-template', sa.Column('created', sa.DateTime(), nullable=False), sa.Column('updated', sa.DateTime(), nullable=False), sa.Column('id', sa.String(length=255), nullable=False), sa.Column('name', sa.String(length=255), nullable=False), sa.Column('tenant_id', sa.String(length=36), nullable=False), sa.Column('version', sa.BigInteger(), nullable=False), sa.Column('description', sa.Text(), nullable=False), sa.Column('networking', sa.Text(), nullable=True), sa.PrimaryKeyConstraint('id'), sa.UniqueConstraint('tenant_id', 'name'), mysql_engine=MYSQL_ENGINE, mysql_charset=MYSQL_CHARSET ) def downgrade(): op.drop_table('environment-template') ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/db/migration/alembic_migrations/versions/006_add_task_result.py0000664000175000017500000000175400000000000030617 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or # implied. # See the License for the specific language governing permissions and # limitations under the License. """empty message Revision ID: 006 Revises: None Create Date: 2015-02-15 12:14:12 """ from alembic import op import sqlalchemy as sa # revision identifiers, used by Alembic. revision = '006' down_revision = '005' MYSQL_ENGINE = 'InnoDB' MYSQL_CHARSET = 'utf8' def upgrade(): op.add_column('task', sa.Column('result', sa.types.Text())) # end Alembic commands # def downgrade(): op.drop_column('task', 'result') # end Alembic commands # ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/db/migration/alembic_migrations/versions/007_add_locks.py0000664000175000017500000000224100000000000027363 0ustar00zuulzuul00000000000000# Copyright 2015 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 alembic import op import sqlalchemy as sa """add_locks Revision ID: 007 Revises: 006 Create Date: 2015-04-08 14:01:06.458512 """ # revision identifiers, used by Alembic. revision = '007' down_revision = '006' MYSQL_ENGINE = 'InnoDB' MYSQL_CHARSET = 'utf8' def upgrade(): op.create_table( 'locks', sa.Column('id', sa.String(length=50), nullable=False), sa.Column('ts', sa.DateTime(), nullable=False), sa.PrimaryKeyConstraint('id'), mysql_engine=MYSQL_ENGINE, mysql_charset=MYSQL_CHARSET) def downgrade(): op.drop_table('locks') ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/db/migration/alembic_migrations/versions/008_fix_unique_constraints.py0000664000175000017500000002023200000000000032244 0ustar00zuulzuul00000000000000# Copyright 2015 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 alembic import op import sqlalchemy as sa import murano.db.migration.helpers as helpers from murano.db.sqla import types as st """fix_unique_constraints Revision ID: 008 Revises: 007 Create Date: 2015-04-08 14:01:06.458512 """ # revision identifiers, used by Alembic. revision = '008' down_revision = '007' MYSQL_ENGINE = 'InnoDB' MYSQL_CHARSET = 'utf8' def upgrade(): engine = op.get_bind() if engine.dialect.dialect_description.startswith('mysql'): engine.execute('SET FOREIGN_KEY_CHECKS=0') if engine.dialect.dialect_description == 'postgresql+psycopg2': op.drop_constraint('package_to_tag_package_id_fkey', 'package_to_tag', 'foreignkey') op.drop_constraint('package_to_tag_tag_id_fkey', 'package_to_tag', 'foreignkey') op.drop_constraint('package_to_category_package_id_fkey', 'package_to_category', 'foreignkey') op.drop_constraint('class_definition_package_id_fkey', 'class_definition', 'foreignkey') helpers.transform_table( 'package', {}, {}, sa.Column('created', sa.DateTime(), nullable=False), sa.Column('updated', sa.DateTime(), nullable=False), sa.Column('id', sa.String(length=36), nullable=False), sa.Column('archive', st.LargeBinary(), nullable=True), sa.Column('fully_qualified_name', sa.String(length=128), nullable=False), sa.Column('type', sa.String(length=20), nullable=False), sa.Column('author', sa.String(length=80), nullable=True), sa.Column('name', sa.String(length=80), nullable=False), sa.Column('enabled', sa.Boolean(), nullable=True), sa.Column('description', sa.Text(), nullable=False), sa.Column('is_public', sa.Boolean(), nullable=True), sa.Column('logo', st.LargeBinary(), nullable=True), sa.Column('owner_id', sa.String(length=36), nullable=False), sa.Column('ui_definition', sa.Text(), nullable=True), sa.Column('supplier_logo', sa.types.LargeBinary), sa.Column('supplier', sa.types.Text()), sa.PrimaryKeyConstraint('id'), mysql_engine=MYSQL_ENGINE, mysql_charset=MYSQL_CHARSET ) helpers.transform_table( 'class_definition', {}, {}, sa.Column('created', sa.DateTime(), nullable=False), sa.Column('updated', sa.DateTime(), nullable=False), sa.Column('id', sa.String(length=36), nullable=False), sa.Column('name', sa.String(length=128), nullable=False), sa.Column('package_id', sa.String(length=36), nullable=True), sa.ForeignKeyConstraint(['package_id'], ['package.id'], ), sa.PrimaryKeyConstraint('id'), mysql_engine=MYSQL_ENGINE, mysql_charset=MYSQL_CHARSET ) if engine.dialect.dialect_description.startswith('mysql'): engine.execute('SET FOREIGN_KEY_CHECKS=1') if engine.dialect.dialect_description == 'postgresql+psycopg2': op.create_foreign_key('package_to_tag_package_id_fkey', 'package_to_tag', 'package', ['package_id'], ['id']) op.create_foreign_key('package_to_tag_tag_id_fkey', 'package_to_tag', 'tag', ['tag_id'], ['id']) op.create_foreign_key('package_to_category_package_id_fkey', 'package_to_category', 'package', ['package_id'], ['id']) op.create_foreign_key('class_definition_package_id_fkey', 'class_definition', 'package', ['package_id'], ['id']) op.create_index('ix_package_fqn_and_owner', table_name='package', columns=['fully_qualified_name', 'owner_id'], unique=True) op.create_index('ix_class_definition_name', 'class_definition', ['name']) def downgrade(): op.drop_index('ix_package_fqn_and_owner', table_name='package') op.drop_index('ix_class_definition_name', table_name='class_definition') engine = op.get_bind() if engine.dialect.dialect_description.startswith('mysql'): engine.execute('SET FOREIGN_KEY_CHECKS=0') if engine.dialect.dialect_description == 'postgresql+psycopg2': op.drop_constraint('package_to_tag_package_id_fkey', 'package_to_tag', 'foreignkey') op.drop_constraint('package_to_tag_tag_id_fkey', 'package_to_tag', 'foreignkey') op.drop_constraint('package_to_category_package_id_fkey', 'package_to_category', 'foreignkey') op.drop_constraint('class_definition_package_id_fkey', 'class_definition', 'foreignkey') helpers.transform_table( 'class_definition', {}, {}, sa.Column('created', sa.DateTime(), nullable=False), sa.Column('updated', sa.DateTime(), nullable=False), sa.Column('id', sa.String(length=36), nullable=False), sa.Column('name', sa.String(length=128), nullable=False, unique=True), sa.Column('package_id', sa.String(length=36), nullable=True), sa.PrimaryKeyConstraint('id'), mysql_engine=MYSQL_ENGINE, mysql_charset=MYSQL_CHARSET ) helpers.transform_table( 'package', {}, {}, sa.Column('created', sa.DateTime(), nullable=False), sa.Column('updated', sa.DateTime(), nullable=False), sa.Column('id', sa.String(length=36), nullable=False), sa.Column('archive', st.LargeBinary(), nullable=True), sa.Column('fully_qualified_name', sa.String(length=128), nullable=False, unique=True), sa.Column('type', sa.String(length=20), nullable=False), sa.Column('author', sa.String(length=80), nullable=True), sa.Column('name', sa.String(length=80), nullable=False), sa.Column('enabled', sa.Boolean(), nullable=True), sa.Column('description', sa.Text(), nullable=False), sa.Column('is_public', sa.Boolean(), nullable=True), sa.Column('logo', st.LargeBinary(), nullable=True), sa.Column('owner_id', sa.String(length=36), nullable=False), sa.Column('ui_definition', sa.Text(), nullable=True), sa.Column('supplier_logo', sa.types.LargeBinary), sa.Column('supplier', sa.types.Text()), sa.PrimaryKeyConstraint('id'), mysql_engine=MYSQL_ENGINE, mysql_charset=MYSQL_CHARSET ) if engine.dialect.dialect_description.startswith('mysql'): engine.execute('SET FOREIGN_KEY_CHECKS=1') if engine.dialect.dialect_description == 'postgresql+psycopg2': op.create_foreign_key('package_to_tag_package_id_fkey', 'package_to_tag', 'package', ['package_id'], ['id']) op.create_foreign_key('package_to_tag_tag_id_fkey', 'package_to_tag', 'tag', ['tag_id'], ['id']) op.create_foreign_key('package_to_category_package_id_fkey', 'package_to_category', 'package', ['package_id'], ['id']) op.create_foreign_key('class_definition_package_id_fkey', 'class_definition', 'package', ['package_id'], ['id']) op.create_index('ix_class_definition_name', 'class_definition', ['name']) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/db/migration/alembic_migrations/versions/009_add_cloudfoundry_connections.py0000664000175000017500000000412700000000000033376 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or # implied. # See the License for the specific language governing permissions and # limitations under the License. """ Revision ID: 009 Revises: None Create Date: 2015-08-17 16:34:33.698760 """ from alembic import op import sqlalchemy as sa # revision identifiers, used by Alembic. revision = '009' down_revision = '008' MYSQL_ENGINE = 'InnoDB' MYSQL_CHARSET = 'utf8' def upgrade(): op.create_table( 'cf_orgs', sa.Column('id', sa.String(length=255), nullable=False), sa.Column('tenant', sa.String(length=255), nullable=False), sa.UniqueConstraint('tenant'), sa.PrimaryKeyConstraint('id'), mysql_engine=MYSQL_ENGINE, mysql_charset=MYSQL_CHARSET) op.create_table( 'cf_spaces', sa.Column('id', sa.String(length=255), nullable=False), sa.Column('environment_id', sa.String(length=255), nullable=False), sa.ForeignKeyConstraint(['environment_id'], ['environment.id'], ), sa.PrimaryKeyConstraint('id'), mysql_engine=MYSQL_ENGINE, mysql_charset=MYSQL_CHARSET) op.create_table( 'cf_serv_inst', sa.Column('id', sa.String(length=255), primary_key=True), sa.Column('service_id', sa.String(255), nullable=False), sa.Column('environment_id', sa.String(255), nullable=False), sa.Column('tenant', sa.String(255), nullable=False), sa.ForeignKeyConstraint(['environment_id'], ['environment.id'],), mysql_engine=MYSQL_ENGINE, mysql_charset=MYSQL_CHARSET) # end Alembic commands # def downgrade(): op.drop_table('cf_orgs') op.drop_table('cf_spaces') op.drop_table('cf_serv_inst') # end Alembic commands # ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/db/migration/alembic_migrations/versions/010_remove_unused_networking_column.py0000664000175000017500000000236100000000000034141 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or # implied. # See the License for the specific language governing permissions and # limitations under the License. """ Revision ID: 010 Revises: None Create Date: 2015-09-08 00:00:00.698760 """ from alembic import op import sqlalchemy as sa # revision identifiers, used by Alembic. revision = '010' down_revision = '009' MYSQL_ENGINE = 'InnoDB' MYSQL_CHARSET = 'utf8' def upgrade(): with op.batch_alter_table("environment") as batch_op: batch_op.drop_column('networking') with op.batch_alter_table("environment-template") as batch_op2: batch_op2.drop_column('networking') def downgrade(): op.add_column('environment', sa.Column('networking', sa.Text(), nullable=True)) op.add_column('environment-template', sa.Column('networking', sa.Text(), nullable=True)) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/db/migration/alembic_migrations/versions/011_add_is_public_to_template.py0000664000175000017500000000223400000000000032613 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or # implied. # See the License for the specific language governing permissions and # limitations under the License. """ Add the is_public column to the environment-template for public environment template functionality. Revision ID: 011 Revises: table template """ from alembic import op import sqlalchemy as sa # revision identifiers, used by Alembic. revision = '011' down_revision = '010' MYSQL_ENGINE = 'InnoDB' MYSQL_CHARSET = 'utf8' def upgrade(): op.add_column('environment-template', sa.Column('is_public', sa.Boolean(), default=False, nullable=True)) # end Alembic commands # def downgrade(): op.drop_column('environment-template', 'is_public') # end Alembic commands # ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/db/migration/alembic_migrations/versions/012_support_domain_users.py0000664000175000017500000000276100000000000031727 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or # implied. # See the License for the specific language governing permissions and # limitations under the License. """ Change sizes of columns that hold keystone user ID to support domain users which are 64 characters long. Revision ID: 012 Revises: table session, table package """ from alembic import op import sqlalchemy as sa # revision identifiers, used by Alembic. revision = '012' down_revision = '011' MYSQL_ENGINE = 'InnoDB' MYSQL_CHARSET = 'utf8' def upgrade(): with op.batch_alter_table("session") as batch_op: batch_op.alter_column('user_id', type_=sa.String(64), nullable=False) with op.batch_alter_table("package") as batch_op2: batch_op2.alter_column('owner_id', type_=sa.String(64), nullable=False) # end Alembic commands # def downgrade(): with op.batch_alter_table("session") as batch_op: batch_op.alter_column('user_id', type_=sa.String(36), nullable=False) with op.batch_alter_table("package") as batch_op2: batch_op2.alter_column('owner_id', type_=sa.String(36), nullable=False) # end Alembic commands # ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/db/migration/alembic_migrations/versions/013_increase_description_text_size.py0000664000175000017500000000417700000000000033741 0ustar00zuulzuul00000000000000# Copyright 2016 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. """Increase the size of the text columns storing object model Revision ID: 013 Revises: 012 Create Date: 2016-04-12 15:46:12.251155 """ from alembic import op import sqlalchemy as sa import sqlalchemy.dialects.mysql as sa_mysql # revision identifiers, used by Alembic. revision = '013' down_revision = '012' MYSQL_ENGINE = 'InnoDB' MYSQL_CHARSET = 'utf8' def upgrade(): engine = op.get_bind() if engine.dialect.dialect_description.startswith('mysql'): with op.batch_alter_table('session') as batch_op: batch_op.alter_column('description', type_=sa_mysql.LONGTEXT()) with op.batch_alter_table('environment') as batch_op: batch_op.alter_column('description', type_=sa_mysql.LONGTEXT()) with op.batch_alter_table('environment-template') as batch_op: batch_op.alter_column('description', type_=sa_mysql.LONGTEXT()) def downgrade(): engine = op.get_bind() if engine.dialect.dialect_description.startswith('mysql'): with op.batch_alter_table('session') as batch_op: batch_op.alter_column('description', type_=sa.TEXT()) with op.batch_alter_table('environment') as batch_op: batch_op.alter_column('description', type_=sa.TEXT()) with op.batch_alter_table('environment-template') as batch_op: batch_op.alter_column('description', type_=sa.TEXT()) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/db/migration/alembic_migrations/versions/014_increase_status_time_resolution.py0000664000175000017500000000350700000000000034141 0ustar00zuulzuul00000000000000# Copyright 2016 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. """Increase time resolution for status reports Revision ID: 014 Create Date: 2016-04-28 """ from alembic import op import sqlalchemy.dialects.mysql as sa_mysql # revision identifiers, used by Alembic. revision = '014' down_revision = '013' MYSQL_ENGINE = 'InnoDB' MYSQL_CHARSET = 'utf8' def _check_dbms(engine): dialect = engine.dialect.dialect_description version = engine.dialect.server_version_info if dialect.startswith('mysql') and version >= (5, 6, 4): return True if 'MariaDB' in version and version >= (5, 3): return True return False def upgrade(): engine = op.get_bind() if _check_dbms(engine): with op.batch_alter_table('status') as batch_op: batch_op.alter_column( 'created', type_=sa_mysql.DATETIME(fsp=6), nullable=False) batch_op.alter_column( 'updated', type_=sa_mysql.DATETIME(fsp=6), nullable=False) def downgrade(): engine = op.get_bind() if _check_dbms(engine): with op.batch_alter_table('status') as batch_op: batch_op.alter_column( 'created', type_=sa_mysql.DATETIME(), nullable=False) batch_op.alter_column( 'updated', type_=sa_mysql.DATETIME(), nullable=False) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/db/migration/alembic_migrations/versions/015_adding_text_description.py0000664000175000017500000000256400000000000032344 0ustar00zuulzuul00000000000000# Copyright 2016 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. """Increase time resolution for status reports Revision ID: 015 Create Date: 2016-06-17 """ from alembic import op import sqlalchemy as sa # revision identifiers, used by Alembic. revision = '015' down_revision = '014' MYSQL_ENGINE = 'InnoDB' MYSQL_CHARSET = 'utf8' def upgrade(): op.add_column('environment', sa.Column('description_text', sa.Text(), nullable=True)) op.add_column('environment-template', sa.Column('description_text', sa.Text(), nullable=True)) def downgrade(): with op.batch_alter_table("environment") as batch_op: batch_op.drop_column('description_text') with op.batch_alter_table("environment-template") as batch_op2: batch_op2.drop_column('description_text') ././@PaxHeader0000000000000000000000000000021000000000000011446 xustar0000000000000000114 path=murano-16.0.0/murano/db/migration/alembic_migrations/versions/016_increase_task_description_text_size.py 22 mtime=1696417875.0 murano-16.0.0/murano/db/migration/alembic_migrations/versions/016_increase_task_description_text_siz0000664000175000017500000000272600000000000034170 0ustar00zuulzuul00000000000000# Copyright 2016 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. """Increase the size of the text columns storing object model in the task table Revision ID: 016 Revises: 015 Create Date: 2016-08-30 10:45:00 """ from alembic import op import sqlalchemy as sa import sqlalchemy.dialects.mysql as sa_mysql # revision identifiers, used by Alembic. revision = '016' down_revision = '015' MYSQL_ENGINE = 'InnoDB' MYSQL_CHARSET = 'utf8' def upgrade(): engine = op.get_bind() if engine.dialect.dialect_description.startswith('mysql'): with op.batch_alter_table('task') as batch_op: batch_op.alter_column('description', type_=sa_mysql.LONGTEXT()) def downgrade(): engine = op.get_bind() if engine.dialect.dialect_description.startswith('mysql'): with op.batch_alter_table('task') as batch_op: batch_op.alter_column('description', type_=sa.TEXT()) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/db/migration/helpers.py0000664000175000017500000000354500000000000021024 0ustar00zuulzuul00000000000000# 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 alembic import op import sqlalchemy as sa def transform_table(name, renames, defaults, *columns, **kw): def escape(val): if isinstance(val, str): return "'{0}'".format(val) elif val is None: return 'NULL' else: return val engine = op.get_bind() meta = sa.MetaData(bind=engine) meta.reflect() new_name = name + '__tmp' old_table = meta.tables[name] mapping = dict( (renames.get(col.name, col.name), col.name) for col in old_table.c ) columns_to_select = [ old_table.c[mapping[c.name]] if c.name in mapping else escape(defaults.get(c.name)) for c in columns if isinstance(c, sa.Column) ] select_as = [ c.name for c in columns if isinstance(c, sa.Column) ] select = sa.sql.select(columns_to_select) op.create_table(new_name, *columns, **kw) meta.reflect() new_table = meta.tables[new_name] insert = sa.sql.insert(new_table) if engine.dialect.dialect_description == 'postgresql+psycopg2': insert = insert.returning(next(iter(new_table.primary_key.columns))) insert = insert.from_select(select_as, select) engine.execute(insert) op.drop_table(name) op.rename_table(new_name, name) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/db/migration/migration.py0000664000175000017500000000522500000000000021350 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # 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 alembic from alembic import config as alembic_config from alembic import migration as alembic_migration from murano.db import session as db_session def get_alembic_config(): path = os.path.join(os.path.dirname(__file__), 'alembic.ini') config = alembic_config.Config(path) config.set_main_option('script_location', 'murano.db.migration:alembic_migrations') return config def version(engine=None): """Returns current database version.""" engine = engine or db_session.get_engine() with engine.connect() as conn: context = alembic_migration.MigrationContext.configure(conn) return context.get_current_revision() def upgrade(revision, config=None): """Used for upgrading database. :param version: Desired database version :type version: string """ revision = revision or 'head' config = config or get_alembic_config() alembic.command.upgrade(config, revision) def downgrade(revision, config=None): """Used for downgrading database. :param version: Desired database version7 :type version: string """ revision = revision or 'base' config = config or get_alembic_config() return alembic.command.downgrade(config, revision) def stamp(revision, config=None): """Stamps database with provided revision. Don't run any migrations. :param revision: Should match one from repository or head - to stamp database with most recent revision :type revision: string """ config = config or get_alembic_config() return alembic.command.stamp(config, revision=revision) def revision(message=None, autogenerate=False, config=None): """Creates template for migration. :param message: Text that will be used for migration title :type message: string :param autogenerate: If True - generates diff based on current database state :type autogenerate: bool """ config = config or get_alembic_config() return alembic.command.revision(config, message=message, autogenerate=autogenerate) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/db/models.py0000664000175000017500000003426200000000000016654 0ustar00zuulzuul00000000000000# 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. """ SQLAlchemy models for murano data """ from oslo_db.sqlalchemy import models from oslo_utils import timeutils import sqlalchemy as sa from sqlalchemy.ext import declarative from sqlalchemy import orm as sa_orm from murano.common import uuidutils from murano.db.sqla import types as st class TimestampMixin(object): __protected_attributes__ = set(["created", "updated"]) created = sa.Column(sa.DateTime, default=timeutils.utcnow, nullable=False) updated = sa.Column(sa.DateTime, default=timeutils.utcnow, nullable=False, onupdate=timeutils.utcnow) def update(self, values): """dict.update() behaviour.""" self.updated = timeutils.utcnow() super(TimestampMixin, self).update(values) def __setitem__(self, key, value): self.updated = timeutils.utcnow() super(TimestampMixin, self).__setitem__(key, value) class _MuranoBase(models.ModelBase): def to_dict(self): dictionary = self.__dict__.copy() return dict((k, v) for k, v in dictionary.items() if k != '_sa_instance_state') Base = declarative.declarative_base(cls=_MuranoBase) class Environment(Base, TimestampMixin): """Represents an Environment in the metadata-store.""" __tablename__ = 'environment' __table_args__ = (sa.Index( 'ix_name_tenant_id', 'name', 'tenant_id', unique=True),) id = sa.Column(sa.String(255), primary_key=True, default=uuidutils.generate_uuid) name = sa.Column(sa.String(255), nullable=False) tenant_id = sa.Column(sa.String(36), nullable=False) description_text = sa.Column(sa.String(), nullable=False, default='') version = sa.Column(sa.BigInteger, nullable=False, default=0) description = sa.Column(st.JsonBlob(), nullable=False, default={}) sessions = sa_orm.relationship("Session", backref='environment', cascade='save-update, merge, delete') tasks = sa_orm.relationship('Task', backref='environment', cascade='save-update, merge, delete') cf_spaces = sa_orm.relationship("CFSpace", backref='environment', cascade='save-update, merge, delete') cf_serv_inst = sa_orm.relationship("CFServiceInstance", backref='environment', cascade='save-update, merge, delete') def to_dict(self): dictionary = super(Environment, self).to_dict() del dictionary['description'] return dictionary class EnvironmentTemplate(Base, TimestampMixin): """Represents an Environment Template in the metadata-store.""" __tablename__ = 'environment-template' id = sa.Column(sa.String(36), primary_key=True, default=uuidutils.generate_uuid) name = sa.Column(sa.String(255), nullable=False) tenant_id = sa.Column(sa.String(36), nullable=False) description_text = sa.Column(sa.String(), nullable=False, default='') version = sa.Column(sa.BigInteger, nullable=False, default=0) description = sa.Column(st.JsonBlob(), nullable=False, default={}) is_public = sa.Column(sa.Boolean, default=False) def to_dict(self): dictionary = super(EnvironmentTemplate, self).to_dict() if 'description' in dictionary: del dictionary['description'] return dictionary class Session(Base, TimestampMixin): __tablename__ = 'session' id = sa.Column(sa.String(36), primary_key=True, default=uuidutils.generate_uuid) environment_id = sa.Column(sa.String(255), sa.ForeignKey('environment.id')) user_id = sa.Column(sa.String(64), nullable=False) state = sa.Column(sa.String(36), nullable=False) description = sa.Column(st.JsonBlob(), nullable=False) version = sa.Column(sa.BigInteger, nullable=False, default=0) def to_dict(self): dictionary = super(Session, self).to_dict() del dictionary['description'] # object relations may be not loaded yet if 'environment' in dictionary: del dictionary['environment'] return dictionary class Task(Base, TimestampMixin): __tablename__ = 'task' id = sa.Column(sa.String(36), primary_key=True, default=uuidutils.generate_uuid) started = sa.Column(sa.DateTime, default=timeutils.utcnow, nullable=False) finished = sa.Column(sa.DateTime, default=None, nullable=True) description = sa.Column(st.JsonBlob(), nullable=False) environment_id = sa.Column(sa.String(255), sa.ForeignKey('environment.id')) action = sa.Column(st.JsonBlob()) statuses = sa_orm.relationship("Status", backref='task', cascade='save-update, merge, delete') result = sa.Column(st.JsonBlob(), nullable=True, default={}) def to_dict(self): dictionary = super(Task, self).to_dict() if 'statuses' in dictionary: del dictionary['statuses'] if 'environment' in dictionary: del dictionary['environment'] return dictionary class Status(Base, TimestampMixin): __tablename__ = 'status' id = sa.Column(sa.String(36), primary_key=True, default=uuidutils.generate_uuid) entity_id = sa.Column(sa.String(255), nullable=True) entity = sa.Column(sa.String(10), nullable=True) task_id = sa.Column(sa.String(32), sa.ForeignKey('task.id')) text = sa.Column(sa.Text(), nullable=False) level = sa.Column(sa.String(32), nullable=False) details = sa.Column(sa.Text(), nullable=True) def to_dict(self): dictionary = super(Status, self).to_dict() # object relations may be not loaded yet if 'deployment' in dictionary: del dictionary['deployment'] return dictionary class ApiStats(Base, TimestampMixin): __tablename__ = 'apistats' id = sa.Column(sa.Integer(), primary_key=True) host = sa.Column(sa.String(80)) request_count = sa.Column(sa.BigInteger()) error_count = sa.Column(sa.BigInteger()) average_response_time = sa.Column(sa.Float()) requests_per_tenant = sa.Column(sa.Text()) requests_per_second = sa.Column(sa.Float()) errors_per_second = sa.Column(sa.Float()) cpu_count = sa.Column(sa.Integer()) cpu_percent = sa.Column(sa.Float()) package_to_category = sa.Table('package_to_category', Base.metadata, sa.Column('package_id', sa.String(36), sa.ForeignKey('package.id')), sa.Column('category_id', sa.String(36), sa.ForeignKey('category.id', ondelete="RESTRICT"))) package_to_tag = sa.Table('package_to_tag', Base.metadata, sa.Column('package_id', sa.String(36), sa.ForeignKey('package.id')), sa.Column('tag_id', sa.String(36), sa.ForeignKey('tag.id', ondelete="CASCADE"))) class Instance(Base): __tablename__ = 'instance_stats' environment_id = sa.Column( sa.String(255), primary_key=True, nullable=False) instance_id = sa.Column( sa.String(255), primary_key=True, nullable=False) instance_type = sa.Column(sa.Integer, default=0, nullable=False) created = sa.Column(sa.Integer, nullable=False) destroyed = sa.Column(sa.Integer, nullable=True) type_name = sa.Column('type_name', sa.String(512), nullable=False) type_title = sa.Column('type_title', sa.String(512)) unit_count = sa.Column('unit_count', sa.Integer()) tenant_id = sa.Column('tenant_id', sa.String(36), nullable=False) class Package(Base, TimestampMixin): """Represents a meta information about application package.""" __tablename__ = 'package' __table_args__ = (sa.Index('ix_package_fqn_and_owner', 'fully_qualified_name', 'owner_id', unique=True),) id = sa.Column(sa.String(36), primary_key=True, default=uuidutils.generate_uuid) archive = sa.Column(st.LargeBinary()) fully_qualified_name = sa.Column(sa.String(128), nullable=False) type = sa.Column(sa.String(20), nullable=False, default='class') author = sa.Column(sa.String(80), default='OpenStack') supplier = sa.Column(st.JsonBlob(), nullable=True, default={}) name = sa.Column(sa.String(80), nullable=False) enabled = sa.Column(sa.Boolean, default=True) description = sa.Column(sa.Text(), nullable=False, default='The description is not provided') is_public = sa.Column(sa.Boolean, default=False) tags = sa_orm.relationship("Tag", secondary=package_to_tag, cascade='save-update, merge', lazy='joined') logo = sa.Column(st.LargeBinary(), nullable=True) owner_id = sa.Column(sa.String(64), nullable=False) ui_definition = sa.Column(sa.Text) supplier_logo = sa.Column(sa.LargeBinary, nullable=True) categories = sa_orm.relationship("Category", secondary=package_to_category, cascade='save-update, merge', lazy='joined') class_definitions = sa_orm.relationship( "Class", cascade='save-update, merge, delete', lazy='joined', backref='package') def to_dict(self): d = self.__dict__.copy() not_serializable = ['_sa_instance_state', 'archive', 'logo', 'ui_definition', 'supplier_logo'] nested_objects = ['categories', 'tags', 'class_definitions'] for key in not_serializable: if key in d.keys(): del d[key] for key in nested_objects: d[key] = [a.name for a in d.get(key, [])] return d class Category(Base, TimestampMixin): """Represents an application categories in the datastore.""" __tablename__ = 'category' id = sa.Column(sa.String(36), primary_key=True, default=uuidutils.generate_uuid) name = sa.Column(sa.String(80), nullable=False, index=True, unique=True) package_count = sa_orm.column_property( sa.select([sa.func.count(package_to_category.c.package_id)]). where(package_to_category.c.category_id == id). correlate_except(package_to_category) ) def to_dict(self): d = super(Category, self).to_dict() d['package_count'] = self.package_count return d class Tag(Base, TimestampMixin): """Represents tags in the datastore.""" __tablename__ = 'tag' id = sa.Column(sa.String(36), primary_key=True, default=uuidutils.generate_uuid) name = sa.Column(sa.String(80), nullable=False, unique=True) class Class(Base, TimestampMixin): """Represents a class definition in the datastore.""" __tablename__ = 'class_definition' id = sa.Column(sa.String(36), primary_key=True, default=uuidutils.generate_uuid) name = sa.Column(sa.String(128), nullable=False, index=True) package_id = sa.Column(sa.String(36), sa.ForeignKey('package.id')) class Lock(Base): __tablename__ = 'locks' id = sa.Column(sa.String(50), primary_key=True) ts = sa.Column(sa.DateTime, nullable=False) class CFOrganization(Base): __tablename__ = "cf_orgs" id = sa.Column(sa.String(255), primary_key=True) tenant = sa.Column(sa.String(255), nullable=False) class CFSpace(Base): __tablename__ = "cf_spaces" id = sa.Column(sa.String(255), primary_key=True) environment_id = sa.Column(sa.String(255), sa.ForeignKey('environment.id'), nullable=False) def to_dict(self): dictionary = super(CFSpace, self).to_dict() if 'environment' in dictionary: del dictionary['environment'] return dictionary class CFServiceInstance(Base): __tablename__ = 'cf_serv_inst' id = sa.Column(sa.String(255), primary_key=True) service_id = sa.Column(sa.String(255), nullable=False) environment_id = sa.Column(sa.String(255), sa.ForeignKey('environment.id'), nullable=False) tenant = sa.Column(sa.String(255), nullable=False) def to_dict(self): dictionary = super(CFServiceInstance, self).to_dict() if 'environment' in dictionary: del dictionary['environment'] return dictionary def register_models(engine): """Creates database tables for all models with the given engine.""" models = (Environment, Status, Session, Task, ApiStats, Package, Category, Class, Instance, Lock, CFSpace, CFOrganization) for model in models: model.metadata.create_all(engine) def unregister_models(engine): """Drops database tables for all models with the given engine.""" models = (Environment, Status, Session, Task, ApiStats, Package, Category, Class, Lock, CFOrganization, CFSpace) for model in models: model.metadata.drop_all(engine) ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1696417899.793181 murano-16.0.0/murano/db/services/0000775000175000017500000000000000000000000016633 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/db/services/__init__.py0000664000175000017500000000000000000000000020732 0ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/db/services/actions.py0000664000175000017500000000304200000000000020644 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # 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 murano.common.helpers import token_sanitizer from murano.db import models from murano.services import states def get_environment(session, unit): environment = unit.query(models.Environment).get( session.environment_id) return environment def update_task(action, session, task, unit): objects = session.description.get('Objects', None) session.state = states.SessionState.DELETING if objects is None \ else states.SessionState.DEPLOYING task_info = models.Task() task_info.environment_id = session.environment_id if objects: task_info.description = token_sanitizer.TokenSanitizer().sanitize( dict(session.description.get('Objects'))) task_info.action = task['action'] status = models.Status() status.text = 'Action {0} is scheduled'.format(action) status.level = 'info' task_info.statuses.append(status) with unit.begin(): unit.add(session) unit.add(task_info) return task_info.id ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/db/services/cf_connections.py0000664000175000017500000000621200000000000022200 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # 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 oslo_db import exception import sqlalchemy from murano.db import cfapi_models as models from murano.db import session as db_session def set_tenant_for_org(cf_org_id, tenant): """Store tenant-org link to db""" unit = db_session.get_session() try: with unit.begin(): org = models.CFOrganization() org.id = cf_org_id org.tenant = tenant unit.add(org) except exception.DBDuplicateEntry: unit.execute(sqlalchemy.update(models.CFOrganization).where( models.CFOrganization.id == cf_org_id).values( tenant=tenant)) def set_environment_for_space(cf_space_id, environment_id): """Store env-space link to db""" unit = db_session.get_session() try: with unit.begin(): space = models.CFSpace() space.id = cf_space_id space.environment_id = environment_id unit.add(space) except exception.DBDuplicateEntry: unit.execute(sqlalchemy.update(models.CFSpace).where( models.CFSpace.id == cf_space_id).values( environment_id=environment_id)) def set_instance_for_service(instance_id, service_id, environment_id, tenant): """Store env-space link to db""" unit = db_session.get_session() try: with unit.begin(): connection = models.CFServiceInstance() connection.id = instance_id connection.service_id = service_id connection.environment_id = environment_id connection.tenant = tenant unit.add(connection) except exception.DBDuplicateEntry: unit.execute(sqlalchemy.update(models.CFServiceInstance).where( models.CFServiceInstance.id == instance_id).values( environment_id=environment_id)) def get_environment_for_space(cf_space_id): """Take env id related to space from db""" unit = db_session.get_session() connection = unit.query(models.CFSpace).get(cf_space_id) return connection.environment_id def get_tenant_for_org(cf_org_id): """Take tenant id related to org from db""" unit = db_session.get_session() connection = unit.query(models.CFOrganization).get(cf_org_id) return connection.tenant def get_service_for_instance(instance_id): unit = db_session.get_session() connection = unit.query(models.CFServiceInstance).get(instance_id) return connection def delete_environment_from_space(environment_id): unit = db_session.get_session() unit.query(models.CFSpace).filter( models.CFSpace.environment_id == environment_id).delete() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/db/services/core_services.py0000664000175000017500000002351300000000000022044 0ustar00zuulzuul00000000000000# 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 oslo_log import log as logging from oslo_utils import timeutils from webob import exc from murano.common.i18n import _ from murano.common import utils from murano.db.services import environment_templates as env_temp from murano.db.services import environments as envs LOG = logging.getLogger(__name__) class CoreServices(object): @staticmethod def get_service_status(environment_id, service_id): """Service can have one of three distinguished statuses: - Deploying: if environment has status deploying and there is at least one orchestration engine report for this service; - Pending: if environment has status `deploying` and there is no report from orchestration engine about this service; - Ready: If environment has status ready. :param environment_id: Service environment, we always know to which environment service belongs to :param service_id: Id of service for which we checking status. :return: Service status """ # Now we assume that service has same status as environment. # TODO(ruhe): implement as designed and described above return envs.EnvironmentServices.get_status(environment_id) @staticmethod def get_data(environment_id, path, session_id=None): get_description = envs.EnvironmentServices.get_environment_description env_description = get_description(environment_id, session_id) if env_description is None: return None if 'services' not in env_description: return [] result = utils.TraverseHelper.get(path, env_description) if path == '/services': get_status = CoreServices.get_service_status for srv in result: srv['?']['status'] = get_status(environment_id, srv['?']['id']) return result @staticmethod def get_template_data(env_template_id, path): """It obtains the data for the template. It includes all the services. In case the path includes information such as the env_template_id, the information provided will be related to the entity specified in the path :param env_template_id: The env_template_id to obtain the data :param path: Id of service for which we checking status. :return: The template description """ temp_description = env_temp.EnvTemplateServices.\ get_description(env_template_id) if temp_description is None: return None if 'services' not in temp_description: return [] result = utils.TraverseHelper.get(path, temp_description) if result is None: msg = _('Environment Template is not found').format( id=env_template_id) LOG.error(msg) raise exc.HTTPNotFound(explanation=msg) return result @staticmethod def post_env_template_data(env_template_id, data, path): """It stores the template data inside the template description. :param env_template_id: The env_template_id to obtain the data :param data: the template description :param path: Id of service for which we checking status. :return: The template description """ get_description = env_temp.EnvTemplateServices.get_description save_description = env_temp.EnvTemplateServices.save_description temp_description = get_description(env_template_id) if temp_description is None: msg = _('Environment Template is not found').format( id=env_template_id) LOG.error(msg) raise exc.HTTPNotFound(explanation=msg) if 'services' not in temp_description: temp_description['services'] = [] if path == '/services': if isinstance(data, list): utils.TraverseHelper.extend(path, data, temp_description) else: utils.TraverseHelper.insert(path, data, temp_description) save_description(temp_description) return data @staticmethod def post_application_data(env_template_id, data, path): """It stores the application data inside the template description. :param env_template_id: The env_template_id to obtain the data :param data: the template description :param path: Id of service for which we checking status. :return: The template description """ get_description = env_temp.EnvTemplateServices.get_description save_description = env_temp.EnvTemplateServices.save_description temp_description = get_description(env_template_id) if temp_description is None: msg = _('Environment Template is not found').format( env_template_id) LOG.error(msg) raise exc.HTTPNotFound(explanation=msg) if 'services' not in temp_description: temp_description['services'] = [] if path == '/services': if isinstance(data, list): utils.TraverseHelper.extend(path, data, temp_description) else: utils.TraverseHelper.insert(path, data, temp_description) save_description(temp_description, env_template_id) return data @staticmethod def post_data(environment_id, session_id, data, path): get_description = envs.EnvironmentServices.get_environment_description save_description = envs.EnvironmentServices.\ save_environment_description env_description = get_description(environment_id, session_id) if env_description is None: msg = _('Environment is not found').format( environment_id) LOG.error(msg) raise exc.HTTPNotFound(explanation=msg) if 'services' not in env_description: env_description['services'] = [] if path == '/services': if isinstance(data, list): utils.TraverseHelper.extend(path, data, env_description) else: utils.TraverseHelper.insert(path, data, env_description) save_description(session_id, env_description) return data @staticmethod def put_data(environment_id, session_id, data, path): get_description = envs.EnvironmentServices.get_environment_description save_description = envs.EnvironmentServices.\ save_environment_description env_description = get_description(environment_id, session_id) utils.TraverseHelper.update(path, data, env_description) env_description['?']['updated'] = str(timeutils.utcnow()) save_description(session_id, env_description) return data @staticmethod def delete_data(environment_id, session_id, path): get_description = envs.EnvironmentServices.get_environment_description save_description = envs.EnvironmentServices.\ save_environment_description env_description = get_description(environment_id, session_id) utils.TraverseHelper.remove(path, env_description) save_description(session_id, env_description) @staticmethod def delete_env_template_data(env_template_id, path): """It deletes a template. :param env_template_id: The env_template_id to be deleted. :param path: The path to check. """ get_description = env_temp.EnvTemplateServices.get_description save_description = env_temp.EnvTemplateServices.save_description tmp_description = get_description(env_template_id) if tmp_description is None: msg = _('Environment Template is not found').format( env_template_id) LOG.error(msg) raise exc.HTTPNotFound(explanation=msg) utils.TraverseHelper.remove(path, tmp_description) save_description(tmp_description, env_template_id) return tmp_description @staticmethod def put_application_data(env_template_id, data, path): """It stores the application data inside the template description. :param env_template_id: The env_template_id to obtain the data :param data: the template description :param path: Id of service for which we checking status. :return: The template description """ get_description = env_temp.EnvTemplateServices.get_description save_description = env_temp.EnvTemplateServices.save_description temp_description = get_description(env_template_id) if temp_description is None: msg = _('Environment Template is not found').format( env_template_id) LOG.error(msg) raise exc.HTTPNotFound(explanation=msg) count = 0 id = path[1:].split('/')[-1] for service in temp_description["services"]: if service["?"]["id"]: if service["?"]["id"] == id: break count + 1 utils.TraverseHelper.update("services/{0}".format(count), data, temp_description) temp_description['updated'] = str(timeutils.utcnow()) save_description(temp_description, env_template_id) return data ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/db/services/environment_templates.py0000664000175000017500000001630500000000000023634 0ustar00zuulzuul00000000000000# Copyright (c) 2015 Telefonica I+D. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # 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 murano.common.i18n import _ from murano.common import uuidutils from murano.db import models from murano.db import session as db_session from oslo_db import exception as db_exc from oslo_log import log as logging from sqlalchemy.sql import or_ LOG = logging.getLogger(__name__) class EnvTemplateServices(object): @staticmethod def get_env_templates_by(filters): """Returns list of environment-templates. :param filters: property filters :return: Returns list of environment-templates """ unit = db_session.get_session() templates = unit.query(models.EnvironmentTemplate). \ filter_by(**filters).all() return templates @staticmethod def get_env_templates_or_by(filters): """Returns list of environment-templates. :param filters: property filters :return: Returns list of environment-templates """ unit = db_session.get_session() templates = unit.query(models.EnvironmentTemplate). \ filter(or_(*filters)).all() return templates @staticmethod def create(env_template_params, tenant_id): """Creates environment-template with specified params, in particular - name. :param env_template_params: Dict, e.g. {'name': 'temp-name'} :param tenant_id: Tenant Id :return: Created Template """ env_template_params['id'] = uuidutils.generate_uuid() env_template_params['tenant_id'] = tenant_id env_template = models.EnvironmentTemplate() env_template.update(env_template_params) unit = db_session.get_session() with unit.begin(): try: unit.add(env_template) except db_exc.DBDuplicateEntry: msg = _('Environment template specified name already exists') LOG.error(msg) raise db_exc.DBDuplicateEntry(explanation=msg) env_template.update({'description': env_template_params}) env_template.save(unit) return env_template @staticmethod def delete(env_template_id): """Deletes template. :param env_template_id: Template that is going to be deleted """ env_temp_description = EnvTemplateServices.get_description( env_template_id) env_temp_description['description'] = None EnvTemplateServices.save_description( env_temp_description, env_template_id) @staticmethod def remove(env_template_id): """It deletes the environment template from database. :param env_template_id: Template Id to be deleted. """ unit = db_session.get_session() template = unit.query(models.EnvironmentTemplate).get(env_template_id) if template: with unit.begin(): unit.delete(template) @staticmethod def update(env_template_id, body): """It updates the description of an environment template. :param env_template_id: Template Id to be deleted. :param body: The description to be updated. :return: the template description updated """ unit = db_session.get_session() template = unit.query(models.EnvironmentTemplate).get(env_template_id) template.update(body) template.save(unit) return template @staticmethod def get_description(env_template_id): """Returns environment template description for specified template. :param env_template_id: Template Id :return: environment-template Description Object """ template = EnvTemplateServices.get_env_template(env_template_id) if template is None: raise ValueError("The environment template does not exist") return template.description @staticmethod def get_application_description(env_template_id): """Returns environment template description for specified applications. :param env_template_id: Template Id :return: Template Description Object """ env_temp_desc = EnvTemplateServices.get_description(env_template_id) if "services" not in env_temp_desc: return [] else: return env_temp_desc['services'] @staticmethod def save_description(env_template_des, env_template_id=None): """Saves environment template description to specified session. :param env_template_des: Template Description :param env_template_id: The template ID. """ unit = db_session.get_session() template = unit.query(models.EnvironmentTemplate).get(env_template_id) template.update({'description': env_template_des}) template.save(unit) @staticmethod def env_template_exist(env_template_id): """It checks if the environment template exits in database. :param env_template_id: The template ID """ template = EnvTemplateServices.get_env_template(env_template_id) if template is None: return False else: return True @staticmethod def get_env_template(env_template_id): """It obtains the environment template information from the database. :param env_template_id: The template ID """ session = db_session.get_session() return session.query(models.EnvironmentTemplate).get(env_template_id) @staticmethod def clone(env_template_id, tenant_id, env_template_name, is_public): """Clones environment-template with specified params, in particular - name. :param env_template_params: Dict, e.g. {'name': 'temp-name'} :param tenant_id: Tenant Id :return: Created Template """ template = EnvTemplateServices.get_env_template(env_template_id) env_template_params = template.to_dict() env_template_params['id'] = uuidutils.generate_uuid() env_template_params['tenant_id'] = tenant_id env_template_params['name'] = env_template_name env_template_params['is_public'] = is_public env_temp_desc = EnvTemplateServices.get_description(env_template_id) if "services" in env_temp_desc: env_template_params['services'] = env_temp_desc['services'] env_template = models.EnvironmentTemplate() env_template.update(env_template_params) unit = db_session.get_session() with unit.begin(): unit.add(env_template) env_template.update({'description': env_template_params}) env_template.save(unit) return env_template ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/db/services/environments.py0000664000175000017500000002465600000000000021751 0ustar00zuulzuul00000000000000# 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 keystoneclient import exceptions as ks_exceptions from oslo_config import cfg from oslo_log import log as logging import yaml from murano.common import auth_utils from murano.common import uuidutils from murano.db import models from murano.db.services import sessions from murano.db import session as db_session from murano.services import states CONF = cfg.CONF LOG = logging.getLogger(__name__) DEFAULT_NETWORK_TYPES = { "nova": 'io.murano.resources.NovaNetwork', "neutron": 'io.murano.resources.NeutronNetwork' } class EnvironmentServices(object): @staticmethod def get_environments_by(filters): """Returns list of environments :param filters: property filters :return: Returns list of environments """ unit = db_session.get_session() environments = unit.query(models.Environment). \ filter_by(**filters).all() for env in environments: env['status'] = EnvironmentServices.get_status(env['id']) return environments @staticmethod def get_status(environment_id): """Environment can have one of the following statuses: - deploying: there is ongoing deployment for environment - deleting: environment is currently being deleted - deploy failure: last deployment session has failed - delete failure: last delete session has failed - pending: there is at least one session with status `open` and no errors in previous sessions - ready: there are no sessions for environment :param environment_id: Id of environment for which we checking status. :return: Environment status """ # Deploying: there is at least one valid session with status deploying session_list = sessions.SessionServices.get_sessions(environment_id) has_opened = False for session in session_list: if session.state == states.SessionState.DEPLOYING: return states.EnvironmentStatus.DEPLOYING elif session.state == states.SessionState.DELETING: return states.EnvironmentStatus.DELETING elif session.state == states.SessionState.DEPLOY_FAILURE: return states.EnvironmentStatus.DEPLOY_FAILURE elif session.state == states.SessionState.DELETE_FAILURE: return states.EnvironmentStatus.DELETE_FAILURE elif session.state == states.SessionState.OPENED: has_opened = True elif session.state == states.SessionState.DEPLOYED: break if has_opened: return states.EnvironmentStatus.PENDING return states.EnvironmentStatus.READY @staticmethod def create(environment_params, context): # tagging environment by tenant_id for later checks """Creates environment with specified params, in particular - name :param environment_params: Dict, e.g. {'name': 'env-name'} :param context: request context to get the tenant id and the token :return: Created Environment """ objects = {'?': { 'id': uuidutils.generate_uuid(), }} network_driver = EnvironmentServices.get_network_driver(context) objects.update(environment_params) if not objects.get('defaultNetworks'): objects['defaultNetworks'] = \ EnvironmentServices.generate_default_networks(objects['name'], network_driver) objects['?']['type'] = 'io.murano.Environment' objects['?']['metadata'] = {} data = { 'Objects': objects, 'Attributes': [], 'project_id': context.project_id, 'user_id': context.user } environment_params['tenant_id'] = context.project_id environment = models.Environment() environment.update(environment_params) unit = db_session.get_session() with unit.begin(): unit.add(environment) # saving environment as Json to itself environment.update({'description': data}) environment.save(unit) return environment @staticmethod def delete(environment_id, session_id): """Deletes environment and notify orchestration engine about deletion :param environment_id: Environment that is going to be deleted :param session_id: Session Id """ env_description = EnvironmentServices.get_environment_description( environment_id, session_id, False) env_description['Objects'] = None EnvironmentServices.save_environment_description( session_id, env_description, False) @staticmethod def remove(environment_id): unit = db_session.get_session() environment = unit.query(models.Environment).get(environment_id) if environment: with unit.begin(): unit.delete(environment) @staticmethod def get_environment_description(environment_id, session_id=None, inner=True): """Returns environment description for specified environment. If session is specified and not in deploying state function returns modified environment description, otherwise returns actual environment desc. :param environment_id: Environment Id :param session_id: Session Id :param inner: return contents of environment rather than whole Object Model structure :return: Environment Description Object """ unit = db_session.get_session() if session_id: session = unit.query(models.Session).get(session_id) if sessions.SessionServices.validate(session): if session.state != states.SessionState.DEPLOYED: env_description = session.description else: env = unit.query(models.Environment) \ .get(session.environment_id) env_description = env.description else: env = unit.query(models.Environment) \ .get(session.environment_id) env_description = env.description else: env = (unit.query(models.Environment).get(environment_id)) env_description = env.description if not inner: return env_description else: return env_description['Objects'] @staticmethod def save_environment_description(session_id, environment, inner=True): """Saves environment description to specified session. :param session_id: Session Id :param environment: Environment Description :param inner: save modifications to only content of environment rather than whole Object Model structure """ unit = db_session.get_session() session = unit.query(models.Session).get(session_id) if inner: data = session.description.copy() data['Objects'] = environment session.description = data else: session.description = environment session.save(unit) @staticmethod def generate_default_networks(env_name, network_driver): net_config = CONF.find_file( CONF.networking.network_config_file) if net_config: LOG.debug("Loading network configuration from file") with open(net_config) as f: data = yaml.safe_load(f) return EnvironmentServices._objectify(data, { 'ENV': env_name }) network_type = DEFAULT_NETWORK_TYPES[network_driver] LOG.debug("Setting '{net_type}' as environment's " "default network".format(net_type=network_type)) return { 'environment': { '?': { 'id': uuidutils.generate_uuid(), 'type': network_type }, 'name': env_name + '-network' }, 'flat': None } @staticmethod def deploy(session, unit, context): environment = unit.query(models.Environment).get( session.environment_id) if (session.description['Objects'] is None and 'ObjectsCopy' not in session.description): EnvironmentServices.remove(session.environment_id) else: sessions.SessionServices.deploy( session, environment, unit, context) @staticmethod def _objectify(data, replacements): if isinstance(data, dict): if isinstance(data.get('?'), dict): data['?']['id'] = uuidutils.generate_uuid() result = {} for key, value in data.items(): result[key] = EnvironmentServices._objectify( value, replacements) return result elif isinstance(data, list): return [EnvironmentServices._objectify(v, replacements) for v in data] elif isinstance(data, str): for key, value in replacements.items(): data = data.replace('%' + key + '%', value) return data @staticmethod def get_network_driver(context): driver = CONF.networking.driver if driver: LOG.debug("Will use {} as a network driver".format(driver)) return driver session = auth_utils.get_token_client_session( context.auth_token, context.project_id) try: session.get_endpoint(service_type='network') except ks_exceptions.EndpointNotFound: LOG.debug("Will use NovaNetwork as a network driver") return "nova" else: LOG.debug("Will use Neutron as a network driver") return "neutron" ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/db/services/instances.py0000664000175000017500000000746700000000000021212 0ustar00zuulzuul00000000000000# 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 oslo_db import exception from oslo_utils import timeutils import sqlalchemy from sqlalchemy.sql import func from murano.db import models from murano.db import session as db_session UNCLASSIFIED = 0 APPLICATION = 100 OS_INSTANCE = 200 class InstanceStatsServices(object): @staticmethod def track_instance(instance_id, environment_id, instance_type, type_name, type_title=None, unit_count=None): unit = db_session.get_session() try: with unit.begin(): env = unit.query(models.Environment).get(environment_id) instance = models.Instance() instance.instance_id = instance_id instance.environment_id = environment_id instance.tenant_id = env.tenant_id instance.instance_type = instance_type instance.created = timeutils.utcnow_ts() instance.destroyed = None instance.type_name = type_name instance.type_title = type_title instance.unit_count = unit_count unit.add(instance) except exception.DBDuplicateEntry: unit.execute( sqlalchemy.update(models.Instance).where( models.Instance.instance_id == instance_id and models.Instance.environment_id == environment_id).values( unit_count=unit_count)) @staticmethod def destroy_instance(instance_id, environment_id): unit = db_session.get_session() instance = unit.query(models.Instance).get( (environment_id, instance_id)) if instance and not instance.destroyed: instance.destroyed = timeutils.utcnow_ts() instance.save(unit) @staticmethod def get_aggregated_stats(environment_id): unit = db_session.get_session() now = timeutils.utcnow_ts() query = unit.query(models.Instance.instance_type, func.sum( func.coalesce(models.Instance.destroyed, now) - models.Instance.created), func.count()).filter( models.Instance.environment_id == environment_id) res = query.group_by(models.Instance.instance_type).all() return [{ 'type': int(record[0]), 'duration': int(record[1]), 'count': int(record[2]) } for record in res] @staticmethod def get_raw_environment_stats(environment_id, instance_id=None): unit = db_session.get_session() now = timeutils.utcnow_ts() query = unit.query(models.Instance).filter( models.Instance.environment_id == environment_id) if instance_id: query = query.filter(models.Instance.instance_id == instance_id) res = query.all() return [{ 'type': record.instance_type, 'duration': (record.destroyed or now) - record.created, 'type_name': record.type_name, 'unit_count': record.unit_count, 'instance_id': record.instance_id, 'type_title': record.type_title, 'active': True if not record.destroyed else False } for record in res] ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/db/services/sessions.py0000664000175000017500000001071300000000000021055 0ustar00zuulzuul00000000000000# 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 murano.db import models from murano.db import session as db_session from murano.services import actions from murano.services import states class SessionServices(object): @staticmethod def get_sessions(environment_id, state=None): """Get list of sessions for specified environment. :param environment_id: Environment Id :param state: murano.services.states.EnvironmentStatus :return: Sessions for specified Environment, if SessionState is not defined all sessions for specified environment is returned. """ unit = db_session.get_session() # Here we duplicate logic for reducing calls to database # Checks for validation is same as in validate. query = unit.query(models.Session).filter( # Get all session for this environment models.Session.environment_id == environment_id, # Only sessions with same version as current env version are valid ) if state: # in this state, if state is not specified return in all states query = query.filter(models.Session.state == state) return query.order_by(models.Session.version.desc(), models.Session.updated.desc()).all() @staticmethod def create(environment_id, user_id): """Creates session object for specific environment for specified user. :param environment_id: Environment Id :param user_id: User Id :return: Created session """ unit = db_session.get_session() environment = unit.query(models.Environment).get(environment_id) session = models.Session() session.environment_id = environment.id session.user_id = user_id session.state = states.SessionState.OPENED # used for checking if other sessions was deployed before this one session.version = environment.version # all changes to environment is stored here, and translated to # environment only after deployment completed session.description = environment.description with unit.begin(): unit.add(session) return session @staticmethod def validate(session): """Validates session Session is valid only if no other session for same. environment was already deployed on in deploying state, :param session: Session for validation """ # if other session is deploying now current session is invalid unit = db_session.get_session() # if environment version is higher than version on which current # session is created then other session was already deployed current_env = unit.query(models.Environment).\ get(session.environment_id) if current_env.version > session.version: return False # if other session is deploying now current session is invalid other_is_deploying = unit.query(models.Session).filter_by( environment_id=session.environment_id, state=states.SessionState.DEPLOYING ).count() > 0 if session.state == states.SessionState.OPENED and other_is_deploying: return False return True @staticmethod def deploy(session, environment, unit, context): """Prepares and deployes environment Prepares environment for deployment and send deployment command to orchestration engine :param session: session that is going to be deployed :param unit: SQLalchemy session :param token: auth token that is going to be used by orchestration """ deleted = session.description['Objects'] is None action_name = None if deleted else 'deploy' actions.ActionServices.submit_task( action_name, environment.id, {}, environment, session, context, unit) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/db/services/stats.py0000664000175000017500000000370000000000000020343 0ustar00zuulzuul00000000000000# 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 murano.db import models as m from murano.db import session as db_session class Statistics(object): @staticmethod def get_all(): db = db_session.get_session() stats = db.query(m.ApiStats).all() return stats @staticmethod def get_stats_by_id(stats_id): db = db_session.get_session() stats = db.query(m.ApiStats).get(stats_id) return stats @staticmethod def get_stats_by_host(host): db = db_session.get_session() stats = db.query(m.ApiStats).filter(m.ApiStats.host == host).first() return stats @staticmethod def create(host, request_count, error_count, average_response_time, requests_per_tenant, cpu_count, cpu_percent): stats = m.ApiStats() stats.host = host stats.request_count = request_count stats.error_count = error_count stats.average_response_time = average_response_time stats.requests_per_tenant = json.dumps(requests_per_tenant) stats.requests_per_second = 0.0 stats.errors_per_second = 0.0 stats.cpu_count = cpu_count stats.cpu_percent = cpu_percent db = db_session.get_session() stats.save(db) @staticmethod def update(host, stats): db = db_session.get_session() stats.save(db) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/db/session.py0000664000175000017500000000544000000000000017050 0ustar00zuulzuul00000000000000# 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. """Session management functions.""" import threading from oslo_config import cfg from oslo_db import exception from oslo_db import options from oslo_db.sqlalchemy import session as db_session from oslo_utils import timeutils from murano.db import models CONF = cfg.CONF options.set_defaults(CONF) _FACADE = None _LOCK = threading.Lock() MAX_LOCK_RETRIES = 10 def _create_facade_lazily(): global _LOCK, _FACADE if _FACADE is None: # FIXME(zigo): autocommit=True it's not compatible with # SQLAlchemy 2.0, and will be removed in future _FACADE = db_session.EngineFacade.from_config(CONF, autocommit=True) with _LOCK: if _FACADE is None: _FACADE = db_session.EngineFacade.from_config(CONF, sqlite_fk=True) return _FACADE def get_engine(): facade = _create_facade_lazily() return facade.get_engine() def get_session(**kwargs): facade = _create_facade_lazily() return facade.get_session(**kwargs) def get_lock(name, session=None): if session is None: session = get_session() nested = False else: nested = session.transaction is not None return _get_or_create_lock(name, session, nested) def _get_or_create_lock(name, session, nested, retry=0): if nested: session.begin_nested() else: session.begin() existing = session.query(models.Lock).get(name) if existing is None: try: # no lock found, creating a new one lock = models.Lock(id=name, ts=timeutils.utcnow()) lock.save(session) return session.transaction # lock created and acquired except exception.DBDuplicateEntry: session.rollback() if retry >= MAX_LOCK_RETRIES: raise else: # other transaction has created a lock, repeat to acquire # via update return _get_or_create_lock(name, session, nested, retry + 1) else: # lock found, acquiring by doing update existing.ts = timeutils.utcnow() existing.save(session) return session.transaction ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1696417899.793181 murano-16.0.0/murano/db/sqla/0000775000175000017500000000000000000000000015750 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/db/sqla/__init__.py0000664000175000017500000000000000000000000020047 0ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/db/sqla/types.py0000664000175000017500000000203600000000000017467 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # 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 oslo_serialization import jsonutils import sqlalchemy as sa from sqlalchemy.dialects import mysql def LargeBinary(): return sa.LargeBinary().with_variant(mysql.LONGBLOB(), 'mysql') class JsonBlob(sa.TypeDecorator): impl = sa.Text def process_bind_param(self, value, dialect): return jsonutils.dumps(value) def process_result_value(self, value, dialect): if value is not None: return jsonutils.loads(value) return None ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1696417899.8011808 murano-16.0.0/murano/dsl/0000775000175000017500000000000000000000000015205 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/dsl/__init__.py0000664000175000017500000000000000000000000017304 0ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/dsl/attribute_store.py0000664000175000017500000000463100000000000021002 0ustar00zuulzuul00000000000000# 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 from murano.dsl import dsl_types class AttributeStore(object): def __init__(self): self._attributes = collections.defaultdict(lambda: {}) @staticmethod def _get_attribute_key(tagged_object, owner_type, name): if isinstance(owner_type, dsl_types.MuranoTypeReference): owner_type = owner_type.type if isinstance(tagged_object, dsl_types.MuranoObjectInterface): tagged_object = tagged_object.object return tagged_object.object_id, (owner_type.name, name) def get(self, tagged_object, owner_type, name): key1, key2 = self._get_attribute_key(tagged_object, owner_type, name) return self._attributes[key1].get(key2) def set(self, tagged_object, owner_type, name, value): if isinstance(value, dsl_types.MuranoObjectInterface): value = value.id elif isinstance(value, dsl_types.MuranoObject): value = value.object_id key1, key2 = self._get_attribute_key(tagged_object, owner_type, name) if value is None: self._attributes[key1].pop(key2, None) else: self._attributes[key1][key2] = value def serialize(self, known_objects): return [ [key1, key2[0], key2[1], value] for key1, inner in self._attributes.items() for key2, value in inner.items() if key1 in known_objects ] def load(self, data): for item in data: if item[3] is not None: self._attributes[item[0]][(item[1], item[2])] = item[3] def forget_object(self, obj): if isinstance(obj, dsl_types.MuranoObjectInterface): obj = obj.id elif isinstance(obj, dsl_types.MuranoObject): obj = obj.object_id self._attributes.pop(obj, None) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/dsl/constants.py0000664000175000017500000000400500000000000017572 0ustar00zuulzuul00000000000000# 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 EXPRESSION_MEMORY_QUOTA = 512 * 1024 CTX_ACTIONS_ONLY = '?actionsOnly' CTX_ALLOW_PROPERTY_WRITES = '$?allowPropertyWrites' CTX_ARGUMENT_OWNER = '$?argumentOwner' CTX_CALLER_CONTEXT = '$?callerContext' CTX_CURRENT_INSTRUCTION = '$?currentInstruction' CTX_CURRENT_EXCEPTION = '$?currentException' CTX_CURRENT_METHOD = '$?currentMethod' CTX_NAMES_SCOPE = '$?namesScope' CTX_ORIGINAL_CONTEXT = '$?originalContext' CTX_SKIP_FRAME = '$?skipFrame' CTX_THIS = '$?this' CTX_TYPE = '$?type' CTX_VARIABLE_SCOPE = '$?variableScope' CTX_YAQL_ENGINE = '$?yaqlEngine' DM_OBJECTS = 'Objects' DM_OBJECTS_COPY = 'ObjectsCopy' DM_ATTRIBUTES = 'Attributes' META_MURANO_METHOD = '?muranoMethod' META_NO_TRACE = '?noTrace' META_MPL_META = 'Meta' META_USAGE = 'Usage' META_SCOPE = 'Scope' CORE_LIBRARY = 'io.murano' CORE_LIBRARY_OBJECT = 'io.murano.Object' TL_CONTEXT = '__murano_context' TL_ID = '__thread_id' TL_OBJECT_STORE = '__murano_object_store' TL_SESSION = '__murano_execution_session' TL_CONTRACT_PASSKEY = '__murano_contract_passkey' TL_OBJECTS_DRY_RUN = '__murano_objects_dry_run' RUNTIME_VERSION_1_0 = semantic_version.Version('1.0.0') RUNTIME_VERSION_1_1 = semantic_version.Version('1.1.0') RUNTIME_VERSION_1_2 = semantic_version.Version('1.2.0') RUNTIME_VERSION_1_3 = semantic_version.Version('1.3.0') RUNTIME_VERSION_1_4 = semantic_version.Version('1.4.0') RUNTIME_VERSION_1_5 = semantic_version.Version('1.5.0') ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/dsl/context_manager.py0000664000175000017500000000247400000000000020744 0ustar00zuulzuul00000000000000# 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 murano.dsl import yaql_integration # noinspection PyMethodMayBeStatic class ContextManager(object): """Context manager for the MuranoDslExecutor. DSL hosting project should subclass this and override methods in order to insert yaql function at various scopes. For example it may override create_root_context to register its own global yaql functions. """ def create_root_context(self, runtime_version): return yaql_integration.create_context(runtime_version) def create_package_context(self, package): return package.context def create_type_context(self, murano_type): return murano_type.context def create_object_context(self, obj): return None ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1696417899.8011808 murano-16.0.0/murano/dsl/contracts/0000775000175000017500000000000000000000000017205 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/dsl/contracts/__init__.py0000664000175000017500000000220400000000000021314 0ustar00zuulzuul00000000000000# 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 abc class ContractMethod(object): @abc.abstractmethod def transform(self): raise NotImplementedError() @abc.abstractmethod def validate(self): raise NotImplementedError() def finalize(self): return self.value def check_type(self): return self.validate() def generate_schema(self): return self.value def __getattr__(self, item): return self.context[item] class ObjRef(object): def __init__(self, object_id): self.object_id = object_id ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/dsl/contracts/basic.py0000664000175000017500000000733400000000000020647 0ustar00zuulzuul00000000000000# 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 murano.dsl import contracts from murano.dsl import dsl_types from murano.dsl import exceptions from murano.dsl import helpers class String(contracts.ContractMethod): name = 'string' def transform(self): if self.value is None: return None if isinstance(self.value, str): return self.value if isinstance(self.value, str) or \ isinstance(self.value, int): return str(self.value) if isinstance(self.value, dsl_types.MuranoObject): return self.value.object_id if isinstance(self.value, dsl_types.MuranoObjectInterface): return self.value.id raise exceptions.ContractViolationException( 'Value {0} violates string() contract'.format( helpers.format_scalar(self.value))) def validate(self): if self.value is None or isinstance(self.value, str): return self.value raise exceptions.ContractViolationException() def generate_schema(self): types = 'string' if '_notNull' not in self.value: types = [types] + ['null'] return { 'type': types } class Bool(contracts.ContractMethod): name = 'bool' def validate(self): if self.value is None or isinstance(self.value, bool): return self.value raise exceptions.ContractViolationException() def transform(self): if self.value is None: return None return True if self.value else False def generate_schema(self): types = 'boolean' if '_notNull' not in self.value: types = [types] + ['null'] return { 'type': types } class Int(contracts.ContractMethod): name = 'int' def validate(self): if self.value is None or isinstance( self.value, int) and not isinstance(self.value, bool): return self.value raise exceptions.ContractViolationException() def transform(self): if self.value is None: return None try: return int(self.value) except Exception: raise exceptions.ContractViolationException( 'Value {0} violates int() contract'.format( helpers.format_scalar(self.value))) def generate_schema(self): types = 'integer' if '_notNull' not in self.value: types = [types] + ['null'] return { 'type': types } class NotNull(contracts.ContractMethod): name = 'not_null' def validate(self): if self.value is None: raise exceptions.ContractViolationException( 'null value violates notNull() contract') return self.value def transform(self): return self.validate() def generate_schema(self): types = self.value.get('type') if isinstance(types, list) and 'null' in types: types.remove('null') if len(types) == 1: types = types[0] self.value['type'] = types self.value['_notNull'] = True return self.value ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/dsl/contracts/check.py0000664000175000017500000001207200000000000020636 0ustar00zuulzuul00000000000000# 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 yaql.language import exceptions as yaql_exceptions from yaql.language import expressions from yaql.language import specs from yaql.language import utils from yaql.language import yaqltypes from murano.dsl import contracts from murano.dsl import exceptions from murano.dsl import helpers class Check(contracts.ContractMethod): name = 'check' @specs.parameter('predicate', yaqltypes.YaqlExpression()) @specs.parameter('msg', yaqltypes.String(nullable=True)) def __init__(self, engine, predicate, msg=None): self.engine = engine self.predicate = predicate self.msg = msg def _call_predicate(self, value): context = self.root_context.create_child_context() context['$'] = value return self.predicate(utils.NO_VALUE, context, self.engine) def validate(self): if isinstance(self.value, contracts.ObjRef) or self._call_predicate( self.value): return self.value else: msg = self.msg if not msg: msg = "Value {0} doesn't match predicate".format( helpers.format_scalar(self.value)) raise exceptions.ContractViolationException(msg) def transform(self): return self.validate() def generate_schema(self): rest = [True] while rest: if (isinstance(self.predicate, expressions.BinaryOperator) and self.predicate.operator == 'and'): rest = self.predicate.args[1] self.predicate = self.predicate.args[0] else: rest = [] res = extract_pattern(self.predicate, self.engine, self.root_context) if res is not None: self.value.update(res) self.predicate = rest return self.value def is_dollar(expr): """Check $-expressions in YAQL AST""" return (isinstance(expr, expressions.GetContextValue) and expr.path.value in ('$', '$1')) def extract_pattern(expr, engine, context): """Translation of certain known patterns of check() contract expressions""" if isinstance(expr, expressions.BinaryOperator): ops = ('>', '<', '>=', '<=') if expr.operator in ops: op_index = ops.index(expr.operator) if is_dollar(expr.args[0]): constant = evaluate_constant(expr.args[1], engine, context) if constant is None: return None elif is_dollar(expr.args[1]): constant = evaluate_constant(expr.args[0], engine, context) if constant is None: return None op_index = -1 - op_index else: return None op = ops[op_index] if op == '>': return {'minimum': constant, 'exclusiveMinimum': True} elif op == '>=': return {'minimum': constant, 'exclusiveMinimum': False} if op == '<': return {'maximum': constant, 'exclusiveMaximum': True} elif op == '<=': return {'maximum': constant, 'exclusiveMaximum': False} elif expr.operator == 'in' and is_dollar(expr.args[0]): lst = evaluate_constant(expr.args[1], engine, context) if isinstance(lst, tuple): return {'enum': list(lst)} elif (expr.operator == '.' and is_dollar(expr.args[0]) and isinstance(expr.args[1], expressions.Function)): func = expr.args[1] if func.name == 'matches': constant = evaluate_constant(func.args[0], engine, context) if constant is not None: return {'pattern': constant} def evaluate_constant(expr, engine, context): """Evaluate yaql expression into constant value if possible""" if isinstance(expr, expressions.Constant): return expr.value context = context.create_child_context() trap = utils.create_marker('trap') context['$'] = trap @specs.parameter('name', yaqltypes.StringConstant()) @specs.name('#get_context_data') def get_context_data(name, context): res = context[name] if res is trap: raise yaql_exceptions.ResolutionError() return res context.register_function(get_context_data) try: return expressions.Statement(expr, engine).evaluate(context=context) except yaql_exceptions.YaqlException: return None ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/dsl/contracts/contracts.py0000664000175000017500000002510000000000000021555 0ustar00zuulzuul00000000000000# 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 copy from yaql.language import specs from yaql.language import utils from yaql.language import yaqltypes from murano.dsl import constants from murano.dsl.contracts import basic from murano.dsl.contracts import check from murano.dsl.contracts import instances from murano.dsl import dsl from murano.dsl import dsl_types from murano.dsl import exceptions from murano.dsl import helpers from murano.dsl import yaql_integration CONTRACT_METHODS = [ basic.String, basic.Bool, basic.Int, basic.NotNull, check.Check, instances.Class, instances.Template, instances.Owned, instances.NotOwned ] class Contract(object): def __init__(self, spec, declaring_type): self._spec = spec self._runtime_version = declaring_type.package.runtime_version @property def spec(self): return self._spec @staticmethod def _get_contract_factory(cls, action_func): def payload(context, value, *args, **kwargs): instance = object.__new__(cls) instance.value = value instance.context = context instance.__init__(*args, **kwargs) return action_func(instance) name = yaql_integration.CONVENTION.convert_function_name(cls.name) try: init_spec = specs.get_function_definition( helpers.function(cls.__init__), name=name, method=True, convention=yaql_integration.CONVENTION) except AttributeError: init_spec = specs.get_function_definition( lambda self: None, name=name, method=True) init_spec.parameters['self'] = specs.ParameterDefinition( 'self', yaqltypes.PythonType(object, nullable=True), 0) init_spec.insert_parameter(specs.ParameterDefinition( '?1', yaqltypes.Context(), 0)) init_spec.payload = payload return init_spec @staticmethod def _prepare_context(runtime_version, action): context = yaql_integration.create_context( runtime_version).create_child_context() for cls in CONTRACT_METHODS: context.register_function(Contract._get_contract_factory( cls, action)) return context @staticmethod @helpers.memoize def _prepare_transform_context(runtime_version, finalize): if finalize: def action(c): c.value = c.transform() return c.finalize() else: def action(c): return c.transform() return Contract._prepare_context( runtime_version, action) @staticmethod @helpers.memoize def _prepare_validate_context(runtime_version): return Contract._prepare_context( runtime_version, lambda c: c.validate()) @staticmethod @helpers.memoize def _prepare_check_type_context(runtime_version): return Contract._prepare_context( runtime_version, lambda c: c.check_type()) @staticmethod @helpers.memoize def _prepare_schema_generation_context(runtime_version): context = Contract._prepare_context( runtime_version, lambda c: c.generate_schema()) @specs.name('#finalize') def finalize(obj): if isinstance(obj, dict): obj.pop('_notNull', None) return obj context.register_function(finalize) return context @staticmethod @helpers.memoize def _prepare_finalize_context(runtime_version): return Contract._prepare_context( runtime_version, lambda c: c.finalize()) def _map_dict(self, data, spec, context, path): if data is None or data is dsl.NO_VALUE: data = {} if not isinstance(data, utils.MappingType): raise exceptions.ContractViolationException( 'Value {0} is not of a dictionary type'.format( helpers.format_scalar(data))) if not spec: return data result = {} yaql_key = None for key, value in spec.items(): if isinstance(key, dsl_types.YaqlExpression): if yaql_key is not None: raise exceptions.DslContractSyntaxError( 'Dictionary contract ' 'cannot have more than one expression key') else: yaql_key = key else: result[key] = self._map( data.get(key), value, context, '{0}[{1}]'.format( path, helpers.format_scalar(key))) if yaql_key is not None: yaql_value = spec[yaql_key] for key, value in data.items(): if key in result: continue key = self._map(key, yaql_key, context, path) result[key] = self._map( value, yaql_value, context, '{0}[{1}]'.format( path, helpers.format_scalar(key))) return utils.FrozenDict(result) def _map_list(self, data, spec, context, path): if utils.is_iterator(data): data = list(data) elif not utils.is_sequence(data): if data is None or data is dsl.NO_VALUE: data = [] else: data = [data] if len(spec) < 1: return data shift = 0 max_length = -1 min_length = 0 if isinstance(spec[-1], int): min_length = spec[-1] shift += 1 if len(spec) >= 2 and isinstance(spec[-2], int): max_length = min_length min_length = spec[-2] shift += 1 if max_length >= 0 and not min_length <= len(data) <= max_length: raise exceptions.ContractViolationException( 'Array length {0} is not within [{1}..{2}] range'.format( len(data), min_length, max_length)) elif not min_length <= len(data): raise exceptions.ContractViolationException( 'Array length {0} must not be less than {1}'.format( len(data), min_length)) def map_func(): for index, item in enumerate(data): spec_item = ( spec[-1 - shift] if index >= len(spec) - shift else spec[index] ) yield self._map( item, spec_item, context, '{0}[{1}]'.format(path, index)) return tuple(map_func()) def _map_scalar(self, data, spec): if data != spec: raise exceptions.ContractViolationException( 'Value {0} is not equal to {1}'.format( helpers.format_scalar(data), spec)) else: return data def _map(self, data, spec, context, path): if helpers.is_passkey(data): return data child_context = context.create_child_context() if isinstance(spec, dsl_types.YaqlExpression): child_context[''] = data try: result = spec(context=child_context) return result except exceptions.ContractViolationException as e: e.path = path raise elif isinstance(spec, utils.MappingType): return self._map_dict(data, spec, child_context, path) elif utils.is_sequence(spec): return self._map_list(data, spec, child_context, path) else: return self._map_scalar(data, spec) def _execute(self, base_context_func, data, context, default, **kwargs): # TODO(ativelkov, slagun): temporary fix, need a better way of handling # composite defaults # A bug (#1313694) has been filed if data is dsl.NO_VALUE: data = helpers.evaluate(default, context) if helpers.is_passkey(data): return data contract_context = base_context_func( self._runtime_version).create_child_context() contract_context['root_context'] = context for key, value in kwargs.items(): contract_context[key] = value contract_context[constants.CTX_NAMES_SCOPE] = \ context[constants.CTX_NAMES_SCOPE] return self._map(data, self._spec, contract_context, '') def transform(self, data, context, this, owner, default, calling_type, finalize=True): return self._execute( lambda runtime_version: self._prepare_transform_context( runtime_version, finalize), data, context, default, this=this, owner=owner, calling_type=calling_type) def validate(self, data, context, default, calling_type): self._execute(self._prepare_validate_context, data, context, default, calling_type=calling_type) return True def check_type(self, data, context, default, calling_type): if helpers.is_passkey(data): return False try: self._execute(self._prepare_check_type_context, data, context, default, calling_type=calling_type) return True except exceptions.ContractViolationException: return False def finalize(self, data, context, calling_type): return self._execute( self._prepare_finalize_context, data, context, None, calling_type=calling_type) @staticmethod def generate_expression_schema(expression, context, runtime_version, initial_schema=None): if initial_schema is None: initial_schema = {} else: initial_schema = copy.deepcopy(initial_schema) contract_context = Contract._prepare_schema_generation_context( runtime_version).create_child_context() contract_context['root_context'] = context contract_context[constants.CTX_NAMES_SCOPE] = \ context[constants.CTX_NAMES_SCOPE] contract_context['$'] = initial_schema return expression(context=contract_context) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/dsl/contracts/instances.py0000664000175000017500000002243300000000000021552 0ustar00zuulzuul00000000000000# 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 yaql.language import specs from yaql.language import utils from yaql.language import yaqltypes from murano.dsl import constants from murano.dsl import contracts from murano.dsl import dsl from murano.dsl import dsl_types from murano.dsl import exceptions from murano.dsl import helpers from murano.dsl import serializer class Class(contracts.ContractMethod): name = 'class' @specs.parameter('name', dsl.MuranoTypeParameter( nullable=False, lazy=True)) @specs.parameter('default_name', dsl.MuranoTypeParameter( nullable=True, lazy=True)) @specs.parameter('version_spec', yaqltypes.String(True)) def __init__(self, name, default_name=None, version_spec=None): self.type = name(self.context).type self.default_type = default_name(self.context) or self.type self.version_spec = version_spec def validate(self): if self.value is None or helpers.is_instance_of( self.value, self.type.name, self.version_spec or helpers.get_names_scope( self.root_context)): return self.value if not isinstance(self.value, (dsl_types.MuranoObject, dsl_types.MuranoObjectInterface)): raise exceptions.ContractViolationException( 'Value is not an object') raise exceptions.ContractViolationException( 'Object of type {0} is not compatible with ' 'requested type {1}'.format(self.value.type, self.type)) def transform(self): value = self.value object_store = helpers.get_object_store() if isinstance(self.value, contracts.ObjRef): value = self.value.object_id if value is None: return None if isinstance(value, dsl_types.MuranoObject): obj = value elif isinstance(value, dsl_types.MuranoObjectInterface): obj = value.object elif isinstance(value, utils.MappingType): obj = object_store.load( value, self.owner, context=self.root_context, default_type=self.default_type, scope_type=self.calling_type) elif isinstance(value, str): obj = object_store.get(value) if obj is None: if not object_store.initializing: raise exceptions.NoObjectFoundError(value) else: return contracts.ObjRef(value) else: raise exceptions.ContractViolationException( 'Value {0} cannot be represented as class {1}'.format( helpers.format_scalar(value), self.type)) self.value = obj return self.validate() def generate_schema(self): return self.generate_class_schema(self.value, self.type) @staticmethod def generate_class_schema(value, type_): types = 'muranoObject' if '_notNull' not in value: types = [types] + ['null'] return { 'type': types, 'muranoType': type_.name } class Template(contracts.ContractMethod): name = 'template' @specs.parameter('type_', dsl.MuranoTypeParameter( nullable=False, lazy=True)) @specs.parameter('default_type', dsl.MuranoTypeParameter( nullable=True, lazy=True)) @specs.parameter('version_spec', yaqltypes.String(True)) @specs.parameter( 'exclude_properties', yaqltypes.Sequence(nullable=True)) def __init__(self, engine, type_, default_type=None, version_spec=None, exclude_properties=None): self.type = type_(self.context).type self.default_type = default_type(self.context) or self.type self.version_spec = version_spec self.exclude_properties = exclude_properties self.engine = engine def validate(self): if self.value is None or helpers.is_instance_of( self.value, self.type.name, self.version_spec or helpers.get_names_scope( self.root_context)): return self.value if not isinstance(self.value, (dsl_types.MuranoObject, dsl_types.MuranoObjectInterface)): raise exceptions.ContractViolationException( 'Value is not an object') raise exceptions.ContractViolationException( 'Object of type {0} is not compatible with ' 'requested type {1}'.format(self.value.type, self.type)) def check_type(self): if isinstance(self.value, utils.MappingType): return self.value return self.validate() def transform(self): object_store = helpers.get_object_store() if self.value is None: return None if isinstance(self.value, dsl_types.MuranoObject): obj = self.value elif isinstance(self.value, dsl_types.MuranoObjectInterface): obj = self.value.object elif isinstance(self.value, utils.MappingType): passkey = utils.create_marker('') if self.exclude_properties: parsed = helpers.parse_object_definition( self.value, self.calling_type, self.context) props = dsl.to_mutable(parsed['properties'], self.engine) for p in self.exclude_properties: helpers.patch_dict(props, p, passkey) parsed['properties'] = props value = helpers.assemble_object_definition(parsed) else: value = self.value with helpers.thread_local_attribute( constants.TL_CONTRACT_PASSKEY, passkey): with helpers.thread_local_attribute( constants.TL_OBJECTS_DRY_RUN, True): obj = object_store.load( value, self.owner, context=self.context, default_type=self.default_type, scope_type=self.calling_type) obj.__passkey__ = passkey else: raise exceptions.ContractViolationException( 'Value {0} cannot be represented as class {1}'.format( helpers.format_scalar(self.value), self.type)) self.value = obj return self.validate() def finalize(self): if self.value is None: return None object_store = helpers.get_object_store() if object_store.initializing: return {} passkey = getattr(self.value, '__passkey__', None) with helpers.thread_local_attribute( constants.TL_CONTRACT_PASSKEY, passkey): result = serializer.serialize( self.value.real_this, object_store.executor, dsl_types.DumpTypes.Mixed) if self.exclude_properties: for p in self.exclude_properties: helpers.patch_dict(result, p, utils.NO_VALUE) return result def generate_schema(self): result = Class.generate_class_schema(self.value, self.type) result['owned'] = True if self.exclude_properties: result['excludedProperties'] = self.exclude_properties return result class Owned(contracts.ContractMethod): name = 'owned' def validate(self): if self.value is None or isinstance(self.value, contracts.ObjRef): return self.value if isinstance(self.value, dsl_types.MuranoObject): if helpers.find_object_owner(self.value, lambda t: t is self.this): return self.value raise exceptions.ContractViolationException( 'Object {0} violates owned() contract'.format(self.value)) raise exceptions.ContractViolationException( 'Value {0} is not an object'.format(self.value)) def transform(self): return self.validate() def generate_schema(self): self.value['owned'] = True return self.value class NotOwned(contracts.ContractMethod): name = 'not_owned' def validate(self): if self.value is None or isinstance(self.value, contracts.ObjRef): return self.value if isinstance(self.value, dsl_types.MuranoObject): if helpers.find_object_owner(self.value, lambda t: t is self.this): raise exceptions.ContractViolationException( 'Object {0} violates notOwned() contract'.format( self.value)) return self.value raise exceptions.ContractViolationException( 'Value {0} is not an object'.format(self.value)) def transform(self): return self.validate() def generate_schema(self): self.value['owned'] = False return self.value ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/dsl/dsl.py0000664000175000017500000003057200000000000016350 0ustar00zuulzuul00000000000000# 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 inspect import os.path import eventlet from oslo_config import cfg from yaql.language import expressions as yaql_expressions from yaql.language import specs from yaql.language import utils from yaql.language import yaqltypes from yaql import yaql_interface from murano.dsl import constants from murano.dsl import dsl_types from murano.dsl import helpers CONF = cfg.CONF NO_VALUE = utils.create_marker('NO_VALUE') def name(dsl_name): def wrapper(cls): cls.__murano_name = dsl_name return cls return wrapper class MuranoObjectParameter(yaqltypes.PythonType): def __init__(self, murano_class=None, nullable=False, version_spec=None, decorate=True): self.murano_class = murano_class self.version_spec = version_spec self.decorate = decorate super(MuranoObjectParameter, self).__init__( (dsl_types.MuranoObject, MuranoObjectInterface), nullable) def check(self, value, context, *args, **kwargs): if not super(MuranoObjectParameter, self).check( value, context, *args, **kwargs): return False if value is None or isinstance(value, yaql_expressions.Expression): return True if isinstance(value, MuranoObjectInterface): value = value.object if not isinstance(value, dsl_types.MuranoObject): return False if self.murano_class: murano_class = self.murano_class if isinstance(murano_class, str): return helpers.is_instance_of( value, murano_class, self.version_spec or helpers.get_type(context)) else: return murano_class.is_compatible(value) else: return True def convert(self, value, sender, context, function_spec, engine, *args, **kwargs): result = super(MuranoObjectParameter, self).convert( value, sender, context, function_spec, engine, *args, **kwargs) if self.decorate: return MuranoObjectInterface.create(result) else: if isinstance(result, dsl_types.MuranoObject): return result return None if result is None else result.object class ThisParameter(yaqltypes.HiddenParameterType, yaqltypes.SmartType): def __init__(self): super(ThisParameter, self).__init__(False) def convert(self, value, sender, context, function_spec, engine, *args, **kwargs): return get_this(context) class InterfacesParameter(yaqltypes.HiddenParameterType, yaqltypes.SmartType): def __init__(self): super(InterfacesParameter, self).__init__(False) def convert(self, value, sender, context, function_spec, engine, *args, **kwargs): this = helpers.get_this(context) return Interfaces(this) class MuranoTypeParameter(yaqltypes.PythonType): def __init__(self, base_type=None, nullable=False, context=None, resolve_strings=True, lazy=False): self._context = context self._base_type = base_type self._resolve_strings = resolve_strings self._lazy = lazy super(MuranoTypeParameter, self).__init__( (dsl_types.MuranoTypeReference, str), nullable) def check(self, value, context, *args, **kwargs): if not super(MuranoTypeParameter, self).check( value, context, *args, **kwargs): return False if isinstance(value, str): if not self._resolve_strings: return False value = helpers.get_class(value, context).get_reference() if isinstance(value, dsl_types.MuranoTypeReference): if not self._base_type: return True return self._base_type.is_compatible(value) return True def convert(self, value, sender, context, function_spec, engine, *args, **kwargs): def implementation(ctx=None): value2 = value if ctx is None: ctx = self._context or context if isinstance(value2, yaql_expressions.Expression): value2 = value2(utils.NO_VALUE, ctx, engine) value2 = super(MuranoTypeParameter, self).convert( value2, sender, ctx, function_spec, engine) if isinstance(value2, str): value2 = helpers.get_class(value2, ctx).get_reference() if self._base_type and not self._base_type.is_compatible(value): raise ValueError('Value must be subtype of {0}'.format( self._base_type.name )) return value2 if self._lazy: return implementation return implementation() class MuranoObjectInterface(dsl_types.MuranoObjectInterface): class DataInterface(object): def __init__(self, object_interface): object.__setattr__(self, '__object_interface', object_interface) def __getattr__(self, item): oi = getattr(self, '__object_interface') return oi[item] def __setattr__(self, key, value): oi = getattr(self, '__object_interface') oi[key] = value class CallInterface(object): def __init__(self, object_interface): self.__object_interface = object_interface def __getattr__(self, item): def func(*args, **kwargs): self._insert_instruction() with helpers.with_object_store( self.__object_interface._object_store): context = helpers.get_context() obj = self.__object_interface.object return to_mutable(obj.type.invoke( item, obj, args, kwargs, context), helpers.get_yaql_engine(context)) return func @staticmethod def _insert_instruction(): context = helpers.get_context() if context: frame = inspect.stack()[2] location = dsl_types.ExpressionFilePosition( os.path.abspath(frame[1]), frame[2], -1, frame[2], -1) context[constants.CTX_CURRENT_INSTRUCTION] = NativeInstruction( frame[4][0].strip(), location) def __init__(self, mpl_object): self._object = mpl_object self._object_store = helpers.get_object_store() @staticmethod def create(mpl_object): if mpl_object is None or isinstance(mpl_object, MuranoObjectInterface): return mpl_object return MuranoObjectInterface(mpl_object) @property def object(self): return self._object @property def id(self): return self.object.object_id @property def owner(self): owner = self.object.owner return MuranoObjectInterface.create(owner) def find_owner(self, type, optional=False): if isinstance(type, str): type = helpers.get_class(type) elif isinstance(type, dsl_types.MuranoTypeReference): type = type.type owner = helpers.find_object_owner( self.object, lambda t: type.is_compatible(t)) if owner: return MuranoObjectInterface(owner) if not optional: raise ValueError('Object is not owned by any instance of type ' '{0}'.format(type.name)) return None @property def type(self): return self.object.type @property def package(self): return self.type.package @property def properties(self): return MuranoObjectInterface.DataInterface(self) @property def name(self): return self.object.name @property def extension(self): return self.object.extension def cast(self, murano_class, version_spec=None): return MuranoObjectInterface.create( helpers.cast( self.object, murano_class, version_spec or helpers.get_type())) def is_instance_of(self, murano_class, version_spec=None): return helpers.is_instance_of( self.object, murano_class, version_spec or helpers.get_type()) def ancestors(self): return self.type.ancestors() def __getitem__(self, item): context = helpers.get_context() return to_mutable( self.object.get_property(item, context), helpers.get_yaql_engine(context)) def __setitem__(self, key, value): context = helpers.get_context() value = helpers.evaluate(value, context) self.object.set_property(key, value, context) def __call__(self): return MuranoObjectInterface.CallInterface(self) def __repr__(self): return '<{0}>'.format(repr(self.object)) def __eq__(self, other): if isinstance(other, MuranoObjectInterface): return self.object == other.object else: return False def __ne__(self, other): return not (self == other) def __hash__(self): return hash(self.object) class Interfaces(object): def __init__(self, mpl_object): self.__object = mpl_object def yaql(self, receiver=utils.NO_VALUE): return yaql_interface.YaqlInterface( helpers.get_context(), helpers.get_yaql_engine(), receiver) def this(self): return self.methods(self.__object) def methods(self, mpl_object): return MuranoObjectInterface.create(mpl_object) @property def execution_session(self): return helpers.get_execution_session() @property def caller(self): caller_context = helpers.get_caller_context() if caller_context is None: return None return get_this(caller_context) @property def attributes(self): executor = helpers.get_executor() return executor.attribute_store @property def class_config(self): return self.__object.type.package.get_class_config( self.__object.type.name) @property def package_loader(self): return helpers.get_package_loader() class NativeInstruction(object): def __init__(self, instruction, location): self.instruction = instruction self.source_file_position = location def __str__(self): return self.instruction def to_mutable(obj, yaql_engine=None): if yaql_engine is None: yaql_engine = helpers.get_yaql_engine() def converter(value, limit_func, engine, rec): if isinstance(value, dsl_types.MuranoObject): return MuranoObjectInterface.create(value) else: return utils.convert_output_data(value, limit_func, engine, rec) def limiter(it): return utils.limit_iterable(it, CONF.murano.dsl_iterators_limit) return converter(obj, limiter, yaql_engine, converter) def meta(type_name, value): def wrapper(func): fd = specs.get_function_definition(func) mpl_meta = fd.meta.get(constants.META_MPL_META, []) mpl_meta.append({type_name: value}) specs.meta(type_name, mpl_meta)(func) return wrapper def get_this(context=None): this = helpers.get_this(context) return MuranoObjectInterface.create(this) def get_execution_session(): return helpers.get_execution_session() def spawn(func, *args, **kwargs): context = helpers.get_context() object_store = helpers.get_object_store() def wrapper(): with helpers.with_object_store(object_store): with helpers.contextual(context): return func(*args, **kwargs) return eventlet.spawn(wrapper) def new(properties, owner=None, type=None): context = helpers.get_context() return helpers.get_object_store().load( properties, owner, type or get_this(context).type, context=context) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/dsl/dsl_exception.py0000664000175000017500000000540400000000000020422 0ustar00zuulzuul00000000000000# 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 sys from murano.dsl.principal_objects import stack_trace class MuranoPlException(Exception): def __init__(self, names, message, stacktrace, extra=None, cause=None): super(MuranoPlException, self).__init__( u'[{0}]: {1}'.format(', '.join(names), message)) if not isinstance(names, list): names = [names] self._names = names self._message = message self._stacktrace = stacktrace self._extra = extra or {} self._cause = cause @property def names(self): return self._names @property def message(self): return self._message @property def stacktrace(self): return self._stacktrace @property def extra(self): return self._extra @property def cause(self): return self._cause @staticmethod def from_python_exception(exception, context): stacktrace = stack_trace.create_stack_trace(context) exception_type = type(exception) builtins_module = 'builtins' module = exception_type.__module__ if module == builtins_module: names = [exception_type.__name__] else: names = ['{0}.{1}'.format(exception_type.__module__, exception_type.__name__)] result = MuranoPlException( names, str(exception), stacktrace) _, _, exc_traceback = sys.exc_info() result.original_exception = exception result.original_traceback = exc_traceback return result def _format_name(self): if not self._names: return '' elif len(self._names) == 1: return self._names[0] else: return self._names def format(self, prefix=''): text = ('{3}{0}: {1}\n' '{3}Traceback (most recent call last):\n' '{2}').format( self._format_name(), self.message, self.stacktrace().toString(prefix + ' '), prefix) if self._cause is not None: text += '\n\n{0} Caused by {1}'.format( prefix, self._cause.format(prefix + ' ').lstrip()) return text ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/dsl/dsl_types.py0000664000175000017500000000700200000000000017564 0ustar00zuulzuul00000000000000# 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 weakref class ClassUsages(object): Class = 'Class' Meta = 'Meta' All = {Class, Meta} class MetaCardinality(object): One = 'One' Many = 'Many' All = {One, Many} class MetaTargets(object): Package = 'Package' Type = 'Type' Property = 'Property' Method = 'Method' Argument = 'Argument' All = {Package, Type, Property, Method, Argument} class PropertyUsages(object): In = 'In' Out = 'Out' InOut = 'InOut' Runtime = 'Runtime' Const = 'Const' Config = 'Config' Static = 'Static' All = {In, Out, InOut, Runtime, Const, Config, Static} Writable = {Out, InOut, Runtime, Static, Config} class MethodUsages(object): Action = 'Action' Runtime = 'Runtime' Static = 'Static' Extension = 'Extension' All = {Action, Runtime, Static, Extension} InstanceMethods = {Runtime, Action} StaticMethods = {Static, Extension} class MethodScopes(object): Session = 'Session' Public = 'Public' All = {Session, Public} class MethodArgumentUsages(object): Standard = 'Standard' VarArgs = 'VarArgs' KwArgs = 'KwArgs' All = {Standard, VarArgs, KwArgs} class DumpTypes(object): Serializable = 'Serializable' Inline = 'Inline' Mixed = 'Mixed' All = {Serializable, Inline, Mixed} class MuranoType(object): pass class MuranoClass(MuranoType): pass class MuranoMetaClass(MuranoClass): pass class MuranoObject(object): pass class MuranoMethod(object): pass class MuranoMethodArgument(object): pass class MuranoPackage(object): pass class MuranoProperty(object): pass class MuranoTypeReference(object): def __init__(self, murano_type): self.__murano_type = weakref.ref(murano_type) @property def type(self): return self.__murano_type() def __repr__(self): return '*' + repr(self.type) def __eq__(self, other): if not isinstance(other, MuranoTypeReference): return False return self.type == other.type def __ne__(self, other): return not self.__eq__(other) def __hash__(self): return hash(self.type) class YaqlExpression(object): pass class MuranoObjectInterface(object): pass class ExpressionFilePosition(object): def __init__(self, file_path, start_line, start_column, end_line, end_column): self._file_path = file_path self._start_line = start_line self._start_column = start_column self._end_line = end_line self._end_column = end_column @property def file_path(self): return self._file_path @property def start_line(self): return self._start_line @property def start_column(self): return self._start_column @property def end_line(self): return self._end_line @property def end_column(self): return self._end_column ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/dsl/exceptions.py0000664000175000017500000001157300000000000017747 0ustar00zuulzuul00000000000000# 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. class InternalFlowException(Exception): pass class ReturnException(InternalFlowException): def __init__(self, value): self._value = value @property def value(self): return self._value class BreakException(InternalFlowException): pass class ContinueException(InternalFlowException): pass class DslInvalidOperationError(Exception): pass class NoMethodFound(Exception): def __init__(self, name): super(NoMethodFound, self).__init__('Method "%s" is not found' % name) class NoPropertyFound(Exception): def __init__(self, name): super(NoPropertyFound, self).__init__( 'Property "%s" is not found' % name) class NoClassFound(Exception): def __init__(self, name, packages=None): if packages is None: packages = [] packages = ', '.join("{0}/{1}".format(p.name, p.version) for p in packages) super(NoClassFound, self).__init__( 'Class "{0}" is not found in {1}'.format(name, packages)) class NoPackageFound(Exception): def __init__(self, name): super(NoPackageFound, self).__init__( 'Package "%s" is not found' % name) class NoPackageForClassFound(Exception): def __init__(self, name): super(NoPackageForClassFound, self).__init__('Package for class "%s" ' 'is not found' % name) class NoObjectFoundError(Exception): def __init__(self, object_id): super(NoObjectFoundError, self).__init__( 'Object "%s" is not found in object store' % object_id) class MethodNotExposed(Exception): pass class AmbiguousMethodName(Exception): def __init__(self, name): super(AmbiguousMethodName, self).__init__( 'Found more than one method "%s"' % name) class AmbiguousClassName(Exception): def __init__(self, name): super(AmbiguousClassName, self).__init__( 'Found more than one version of class "%s"' % name) class DslContractSyntaxError(Exception): pass class ContractViolationException(Exception): def __init__(self, *args, **kwargs): super(ContractViolationException, self).__init__(*args, **kwargs) self._path = '' @property def path(self): return self._path @path.setter def path(self, value): self._path = value class ValueIsMissingError(Exception): pass class DslSyntaxError(Exception): pass class PropertyAccessError(Exception): pass class AmbiguousPropertyNameError(PropertyAccessError): def __init__(self, name): super(AmbiguousPropertyNameError, self).__init__( 'Found more than one property "%s"' % name) class NoWriteAccess(PropertyAccessError): def __init__(self, name): super(NoWriteAccess, self).__init__( 'Property "%s" is immutable to the caller' % name) class NoWriteAccessError(PropertyAccessError): def __init__(self, name): super(NoWriteAccessError, self).__init__( 'Property "%s" is immutable to the caller' % name) class PropertyReadError(PropertyAccessError): def __init__(self, name, murano_class): super(PropertyAccessError, self).__init__( 'Property "%s" in class "%s" cannot be read' % (name, murano_class.name)) class PropertyWriteError(PropertyAccessError): def __init__(self, name, murano_class): super(PropertyAccessError, self).__init__( 'Property "%s" in class "%s" cannot be written' % (name, murano_class.name)) class UninitializedPropertyAccessError(PropertyAccessError): def __init__(self, name, murano_class): super(PropertyAccessError, self).__init__( 'Access to uninitialized property ' '"%s" in class "%s" is forbidden' % (name, murano_class.name)) class CircularExpressionDependenciesError(Exception): pass class InvalidLhsTargetError(Exception): def __init__(self, target): super(InvalidLhsTargetError, self).__init__( 'Invalid assignment target "%s"' % target) class InvalidInheritanceError(Exception): pass class ObjectDestroyedError(Exception): def __init__(self, obj): super(ObjectDestroyedError, self).__init__( 'Object {0} is already destroyed'.format(obj)) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/dsl/executor.py0000664000175000017500000004626600000000000017433 0ustar00zuulzuul00000000000000# 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 import itertools import traceback import eventlet import eventlet.event from oslo_log import log as logging from yaql.language import exceptions as yaql_exceptions from yaql.language import specs from yaql.language import utils from murano.dsl import attribute_store from murano.dsl import constants from murano.dsl import dsl from murano.dsl import dsl_exception from murano.dsl import dsl_types from murano.dsl import exceptions as dsl_exceptions from murano.dsl import helpers from murano.dsl import object_store from murano.dsl.principal_objects import stack_trace from murano.dsl import serializer from murano.dsl import yaql_integration LOG = logging.getLogger(__name__) class MuranoDslExecutor(object): def __init__(self, package_loader, context_manager, session=None): self._package_loader = package_loader self._context_manager = context_manager self._session = session self._attribute_store = attribute_store.AttributeStore() self._object_store = object_store.ObjectStore(self) self._locks = {} self._root_context_cache = {} self._static_properties = {} @property def object_store(self): return self._object_store @property def execution_session(self): return self._session @property def attribute_store(self): return self._attribute_store @property def package_loader(self): return self._package_loader @property def context_manager(self): return self._context_manager def invoke_method(self, method, this, context, args, kwargs, skip_stub=False, invoke_action=True): if isinstance(this, dsl.MuranoObjectInterface): this = this.object kwargs = utils.filter_parameters_dict(kwargs) runtime_version = method.declaring_type.package.runtime_version yaql_engine = yaql_integration.choose_yaql_engine(runtime_version) if context is None or not skip_stub: actions_only = (context is None and not method.name.startswith('.') and invoke_action) method_context = self.create_method_context( self.create_object_context(this, context), method) method_context[constants.CTX_SKIP_FRAME] = True method_context[constants.CTX_ACTIONS_ONLY] = actions_only stub = method.static_stub if isinstance( this, dsl_types.MuranoType) else method.instance_stub if stub is None: raise ValueError( 'Method {0} cannot be called on receiver {1}'.format( method, this)) real_this = this.real_this if isinstance( this, dsl_types.MuranoObject) else this.get_reference() return stub(yaql_engine, method_context, real_this)( *args, **kwargs) if context[constants.CTX_ACTIONS_ONLY] and not method.is_action: raise dsl_exceptions.MethodNotExposed( '{0} is not an action'.format(method.name)) if method.is_static: obj_context = self.create_object_context( method.declaring_type, context) else: obj_context = self.create_object_context(this, context) context = self.create_method_context(obj_context, method) if isinstance(this, dsl_types.MuranoObject): if this.destroyed: raise dsl_exceptions.ObjectDestroyedError(this) this = this.real_this if method.arguments_scheme is not None: args, kwargs = self._canonize_parameters( method.arguments_scheme, args, kwargs, method.name, this) this_lock = this arg_values_for_lock = {} method_meta = [m for m in method.get_meta(context) if m.type.name == ('io.murano.metadata.' 'engine.Synchronize')] if method_meta: method_meta = method_meta[0] if method_meta: if not method_meta.get_property('onThis', context): this_lock = None for arg_name in method_meta.get_property('onArgs', context): arg_val = kwargs.get(arg_name) if arg_val is not None: arg_values_for_lock[arg_name] = arg_val arg_values_for_lock = utils.filter_parameters_dict(arg_values_for_lock) with self._acquire_method_lock(method, this_lock, arg_values_for_lock): for i, arg in enumerate(args, 2): context[str(i)] = arg for key, value in kwargs.items(): context[key] = value def call(): if isinstance(method.body, specs.FunctionDefinition): if isinstance(this, dsl_types.MuranoType): native_this = this.get_reference() else: native_this = dsl.MuranoObjectInterface(this.cast( method.declaring_type)) return method.body( yaql_engine, context, native_this)(*args, **kwargs) else: context[constants.CTX_NAMES_SCOPE] = \ method.declaring_type return (None if method.body is None else method.body.execute(context)) if (not isinstance(method.body, specs.FunctionDefinition) or not method.body.meta.get(constants.META_NO_TRACE)): with self._log_method(context, args, kwargs) as log: result = call() log(result) return result else: return call() @contextlib.contextmanager def _acquire_method_lock(self, method, this, arg_val_dict): if this is None: if not arg_val_dict: # if neither "this" nor argument values are set then no # locking is needed key = None else: # if only the argument values are passed then find the lock # list only by the method key = (None, id(method)) else: if method.is_static: # find the lock list by the type and method key = (id(method.declaring_type), id(method)) else: # find the lock list by the object and method key = (this.object_id, id(method)) thread_id = helpers.get_current_thread_id() while True: event, event_owner = None, None if key is None: # no locking needed break lock_list = self._locks.setdefault(key, []) # lock list contains a list of tuples: # first item of each tuple is a dict with the values of locking # arguments (it is used for argument values comparison), # second item is an event to wait on, # third one is the owner thread id # If this lock list is empty it means no locks on this object and # method at all. for arg_vals, l_event, l_event_owner in lock_list: if arg_vals == arg_val_dict: event = l_event event_owner = l_event_owner break if event: if event_owner == thread_id: # this means a re-entrant lock: the tuple with the same # value of the first element exists in the list, but it was # acquired by the same green thread. We may proceed with # the call in this case event = None break else: event.wait() else: # this means either the lock list was empty or didn't contain a # tuple with the first element equal to arg_val_dict. # Then let's acquire a lock, i.e. create a new tuple and place # it into the list event = eventlet.event.Event() event_owner = thread_id lock_list.append((arg_val_dict, event, event_owner)) break try: yield finally: if event is not None: lock_list.remove((arg_val_dict, event, event_owner)) if len(lock_list) == 0: del self._locks[key] event.send() @contextlib.contextmanager def _log_method(self, context, args, kwargs): method = helpers.get_current_method(context) param_gen = itertools.chain( (str(arg) for arg in args), (u'{0} => {1}'.format(name, value) for name, value in kwargs.items())) params_str = u', '.join(param_gen) method_name = '::'.join((method.declaring_type.name, method.name)) thread_id = helpers.get_current_thread_id() caller_str = '' caller_ctx = helpers.get_caller_context(context) if caller_ctx is not None: frame = stack_trace.compose_stack_frame(caller_ctx) if frame['location']: caller_str = ' called from ' + stack_trace.format_frame(frame) LOG.trace(u'{thread}: Begin execution {method}({params}){caller}' .format(thread=thread_id, method=method_name, params=params_str, caller=caller_str)) try: def log_result(result): LOG.trace( u'{thread}: End execution {method} with result ' u'{result}'.format( thread=thread_id, method=method_name, result=result)) yield log_result except Exception as e: LOG.trace( u'{thread}: End execution {method} with exception ' u'{exc}'.format(thread=thread_id, method=method_name, exc=e)) raise @staticmethod def _canonize_parameters(arguments_scheme, args, kwargs, method_name, receiver): arg_names = list(arguments_scheme.keys()) parameter_values = {} varargs_arg = None vararg_values = [] kwargs_arg = None kwarg_values = {} for name, definition in arguments_scheme.items(): if definition.usage == dsl_types.MethodArgumentUsages.VarArgs: varargs_arg = name parameter_values[name] = vararg_values elif definition.usage == dsl_types.MethodArgumentUsages.KwArgs: kwargs_arg = name parameter_values[name] = kwarg_values for i, arg in enumerate(args): name = None if i >= len(arg_names) else arg_names[i] if name is None or name in (varargs_arg, kwargs_arg): if varargs_arg: vararg_values.append(arg) else: raise yaql_exceptions.NoMatchingMethodException( method_name, receiver) else: parameter_values[name] = arg for name, value in utils.filter_parameters_dict(kwargs).items(): if name in arguments_scheme and name not in ( varargs_arg, kwargs_arg): parameter_values[name] = value elif kwargs_arg: kwarg_values[name] = value else: raise yaql_exceptions.NoMatchingMethodException( method_name, receiver) return tuple(), parameter_values def load(self, data): with helpers.with_object_store(self.object_store): return self._load(data) def _load(self, data): if not isinstance(data, dict): raise TypeError() self._attribute_store.load(data.get(constants.DM_ATTRIBUTES) or []) model = data.get(constants.DM_OBJECTS) if model is None: result = None else: result = self._object_store.load(model, None, keep_ids=True) model_copy = data.get(constants.DM_OBJECTS_COPY) if model_copy: self._object_store.load(model_copy, None, keep_ids=True) return dsl.MuranoObjectInterface.create(result) def signal_destruction_dependencies(self, *objects): if not objects: return elif len(objects) > 1: return helpers.parallel_select( objects, self.signal_destruction_dependencies) obj = objects[0] if obj.destroyed: return for dependency in obj.destruction_dependencies: try: handler = dependency['handler'] if handler: subscriber = dependency['subscriber'] if subscriber: subscriber = subscriber() if (subscriber and subscriber.initialized and not subscriber.destroyed): method = subscriber.type.find_single_method(handler) self.invoke_method( method, subscriber, None, [obj], {}, invoke_action=False) except Exception as e: LOG.warning('Muted exception during destruction dependency ' 'execution in {0}: {1}'.format(obj, e), exc_info=True) obj.load_dependencies(None) def destroy_objects(self, *objects): if not objects: return elif len(objects) > 1: return helpers.parallel_select( objects, self.destroy_objects) obj = objects[0] if obj.destroyed: return methods = obj.type.find_methods(lambda m: m.name == '.destroy') for method in methods: try: method.invoke(obj, (), {}, None) except Exception as e: if isinstance(e, dsl_exception.MuranoPlException): tb = e.format(prefix=' ') else: tb = traceback.format_exc() LOG.warning( 'Muted exception during execution of .destroy ' 'on {0}: {1}'.format(obj, tb), exc_info=True) def create_root_context(self, runtime_version): context = self._root_context_cache.get(runtime_version) if not context: context = self.context_manager.create_root_context(runtime_version) self._root_context_cache[runtime_version] = context return context def create_package_context(self, package): root_context = self.create_root_context(package.runtime_version) context = helpers.link_contexts( root_context, self.context_manager.create_package_context(package)) return context def create_type_context(self, murano_type, caller_context=None): package_context = self.create_package_context( murano_type.package) context = helpers.link_contexts( package_context, self.context_manager.create_type_context( murano_type)).create_child_context() context[constants.CTX_TYPE] = murano_type if caller_context: context[constants.CTX_NAMES_SCOPE] = caller_context[ constants.CTX_NAMES_SCOPE] return context def create_object_context(self, obj, caller_context=None): if isinstance(obj, dsl_types.MuranoClass): obj_type = obj obj = None else: obj_type = obj.type class_context = self.create_type_context(obj_type) if obj is not None: context = helpers.link_contexts( class_context, self.context_manager.create_object_context( obj)).create_child_context() context[constants.CTX_THIS] = obj.real_this context['this'] = obj.real_this context[''] = obj.real_this else: context = class_context.create_child_context() type_ref = obj_type.get_reference() context[constants.CTX_THIS] = type_ref context['this'] = type_ref context[''] = type_ref if caller_context is not None: caller = caller_context while caller is not None and caller[constants.CTX_SKIP_FRAME]: caller = caller[constants.CTX_CALLER_CONTEXT] context[constants.CTX_NAMES_SCOPE] = caller_context[ constants.CTX_NAMES_SCOPE] context[constants.CTX_CALLER_CONTEXT] = caller context[constants.CTX_ALLOW_PROPERTY_WRITES] = caller_context[ constants.CTX_ALLOW_PROPERTY_WRITES] else: context[constants.CTX_NAMES_SCOPE] = obj_type return context @staticmethod def create_method_context(object_context, method): context = object_context.create_child_context() context[constants.CTX_CURRENT_METHOD] = method return context def run(self, cls, method_name, this, args, kwargs): with helpers.with_object_store(self.object_store): return cls.invoke(method_name, this, args, kwargs) def get_static_property(self, murano_type, name, context): prop = murano_type.find_static_property(name) cls = prop.declaring_type value = self._static_properties.get(prop, prop.default) return prop.transform(value, cls, None, context) def set_static_property(self, murano_type, name, value, context, dry_run=False): prop = murano_type.find_static_property(name) cls = prop.declaring_type value = prop.transform(value, cls, None, context) if not dry_run: self._static_properties[prop] = prop.finalize( value, cls, context) def finalize(self, model_root=None): # NOTE(ksnihyr): should be no-except try: if model_root: used_objects = serializer.collect_objects(model_root) self.object_store.prepare_finalize(used_objects) model = serializer.serialize_model(model_root, self) self.object_store.finalize() else: model = None self.object_store.prepare_finalize(None) self.object_store.finalize() self._static_properties.clear() return model except Exception as e: LOG.exception( "Exception %s occurred" " during MuranoDslExecutor finalization", e) return None def __enter__(self): return self def __exit__(self, exc_type, exc_val, exc_tb): self.finalize() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/dsl/expressions.py0000664000175000017500000000654100000000000020147 0ustar00zuulzuul00000000000000# 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 murano.dsl import dsl_exception from murano.dsl import helpers from murano.dsl import lhs_expression from murano.dsl import yaql_expression _macros = [] class InstructionStub(object): def __init__(self, title, position): self._title = title self.source_file_position = position def __str__(self): return self._title def register_macro(cls): _macros.append(cls) class DslExpression(object): def execute(self, context): pass class Statement(DslExpression): def __init__(self, statement): if isinstance(statement, yaql_expression.YaqlExpression): key = None value = statement elif isinstance(statement, dict): if len(statement) != 1: raise SyntaxError() key = list(statement.keys())[0] value = statement[key] else: raise SyntaxError() self._destination = lhs_expression.LhsExpression(key) if key else None self._expression = value @property def destination(self): return self._destination @property def expression(self): return self._expression def execute(self, context): try: result = helpers.evaluate(self.expression, context) if self.destination: self.destination(result, context) return result except dsl_exception.MuranoPlException: raise except Exception as e: raise dsl_exception.MuranoPlException.from_python_exception( e, context) def parse_expression(expr): result = None if isinstance(expr, yaql_expression.YaqlExpression): result = Statement(expr) elif isinstance(expr, dict): kwds = {} for key, value in expr.items(): if isinstance(key, yaql_expression.YaqlExpression): if result is not None: raise ValueError() result = Statement(expr) else: kwds[key] = value if result is None: for cls in _macros: try: macro = cls(**kwds) position = None title = 'block construct' if hasattr(expr, 'source_file_position'): position = expr.source_file_position if '__str__' in cls.__dict__: title = str(macro) macro.virtual_instruction = InstructionStub( title, position) return macro except TypeError: continue if result is None: raise SyntaxError( 'Syntax is incorrect in expression: {0}'.format(expr)) return result ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/dsl/helpers.py0000664000175000017500000005454500000000000017236 0ustar00zuulzuul00000000000000# 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 contextlib import functools import gc import inspect import itertools import re import sys import uuid import weakref import eventlet.greenpool import eventlet.greenthread from oslo_config import cfg import semantic_version from yaql.language import contexts import yaql.language.exceptions import yaql.language.expressions from yaql.language import utils as yaqlutils from murano.common import utils from murano.dsl import constants from murano.dsl import dsl_types from murano.dsl import exceptions _threads_sequencer = 0 # type string: ns.something.MyApp[/1.2.3-alpha][@my.package.fqn] TYPE_RE = re.compile(r'([a-zA-Z0-9_.]+)(?:/([^@]+))?(?:@([a-zA-Z0-9_.]+))?$') CONF = cfg.CONF def evaluate(value, context, freeze=True): list_type = tuple if freeze else list dict_type = yaqlutils.FrozenDict if freeze else dict set_type = frozenset if freeze else set if isinstance(value, (dsl_types.YaqlExpression, yaql.language.expressions.Statement)): return value(context) elif isinstance(value, yaqlutils.MappingType): return dict_type( (evaluate(d_key, context, freeze), evaluate(d_value, context, freeze)) for d_key, d_value in value.items()) elif yaqlutils.is_sequence(value): return list_type(evaluate(t, context, freeze) for t in value) elif isinstance(value, yaqlutils.SetType): return set_type(evaluate(t, context, freeze) for t in value) elif yaqlutils.is_iterable(value): return list_type( evaluate(t, context, freeze) for t in yaqlutils.limit_iterable( value, CONF.murano.dsl_iterators_limit)) elif isinstance(value, dsl_types.MuranoObjectInterface): return value.object else: return value def merge_lists(list1, list2): result = [] for item in list1 + list2: if item not in result: result.append(item) return result def merge_dicts(dict1, dict2, max_levels=0): result = {} for key, value1 in dict1.items(): result[key] = value1 if key in dict2: value2 = dict2[key] if type(value2) != type(value1): if ((isinstance(value1, str) or value1 is None) and (isinstance(value2, str) or value2 is None)): continue raise TypeError() if max_levels != 1 and isinstance(value2, dict): result[key] = merge_dicts( value1, value2, 0 if max_levels == 0 else max_levels - 1) elif max_levels != 1 and isinstance(value2, list): result[key] = merge_lists(value1, value2) else: result[key] = value2 for key, value1 in dict2.items(): if key not in result: result[key] = value1 return result def generate_id(): return uuid.uuid4().hex def parallel_select(collection, func, limit=1000): # workaround for eventlet issue 232 # https://github.com/eventlet/eventlet/issues/232 context = get_context() object_store = get_object_store() def wrapper(element): try: with with_object_store(object_store), contextual(context): return func(element), False, None except Exception as e: return e, True, sys.exc_info()[2] gpool = eventlet.greenpool.GreenPool(limit) result = list(gpool.imap(wrapper, collection)) try: exception = next(t for t in result if t[1]) except StopIteration: return map(lambda t: t[0], result) else: utils.reraise(type(exception[0]), exception[0], exception[2]) def enum(**enums): return type('Enum', (), enums) def get_context(): current_thread = eventlet.greenthread.getcurrent() return getattr(current_thread, constants.TL_CONTEXT, None) def get_executor(): store = get_object_store() return None if store is None else store.executor def get_type(context=None): context = context or get_context() return context[constants.CTX_TYPE] def get_execution_session(): executor = get_executor() return None if executor is None else executor.execution_session def get_object_store(): current_thread = eventlet.greenthread.getcurrent() return getattr(current_thread, constants.TL_OBJECT_STORE, None) def get_package_loader(): executor = get_executor() return None if executor is None else executor.package_loader def get_this(context=None): context = context or get_context() return context[constants.CTX_THIS] def get_caller_context(context=None): context = context or get_context() return context[constants.CTX_CALLER_CONTEXT] def get_attribute_store(): executor = get_executor() return None if executor is None else executor.attribute_store def get_current_instruction(context=None): context = context or get_context() return context[constants.CTX_CURRENT_INSTRUCTION] def get_current_method(context=None): context = context or get_context() return context[constants.CTX_CURRENT_METHOD] def get_yaql_engine(context=None): context = context or get_context() return None if context is None else context[constants.CTX_YAQL_ENGINE] def get_current_exception(context=None): context = context or get_context() return context[constants.CTX_CURRENT_EXCEPTION] def are_property_modifications_allowed(context=None): context = context or get_context() return context[constants.CTX_ALLOW_PROPERTY_WRITES] or False def get_names_scope(context=None): context = context or get_context() return context[constants.CTX_NAMES_SCOPE] def get_class(name, context=None): context = context or get_context() murano_type = get_names_scope(context) name = murano_type.namespace_resolver.resolve_name(name) return murano_type.package.find_class(name) def get_contract_passkey(): current_thread = eventlet.greenthread.getcurrent() return getattr(current_thread, constants.TL_CONTRACT_PASSKEY, None) def is_objects_dry_run_mode(): current_thread = eventlet.greenthread.getcurrent() return bool(getattr(current_thread, constants.TL_OBJECTS_DRY_RUN, False)) def get_current_thread_id(): global _threads_sequencer current_thread = eventlet.greenthread.getcurrent() thread_id = getattr(current_thread, constants.TL_ID, None) if thread_id is None: thread_id = 'T' + str(_threads_sequencer) _threads_sequencer += 1 setattr(current_thread, constants.TL_ID, thread_id) return thread_id @contextlib.contextmanager def thread_local_attribute(name, value): current_thread = eventlet.greenthread.getcurrent() old_value = getattr(current_thread, name, None) if value is not None: setattr(current_thread, name, value) elif hasattr(current_thread, name): delattr(current_thread, name) try: yield finally: if old_value is not None: setattr(current_thread, name, old_value) elif hasattr(current_thread, name): delattr(current_thread, name) def contextual(ctx): return thread_local_attribute(constants.TL_CONTEXT, ctx) def with_object_store(object_store): return thread_local_attribute(constants.TL_OBJECT_STORE, object_store) def parse_version_spec(version_spec): if isinstance(version_spec, semantic_version.Spec): return normalize_version_spec(version_spec) if isinstance(version_spec, semantic_version.Version): return normalize_version_spec( semantic_version.Spec('==' + str(version_spec))) if not version_spec: version_spec = '0' version_spec = re.sub('\s+', '', str(version_spec)) # NOTE(kzaitsev): semantic_version 2.3.X thinks that '=0' is not # a valid version spec and only accepts '==0', this regexp adds # an extra '=' before such specs version_spec = re.sub(r'^=(\d)', r'==\1', version_spec) if version_spec[0].isdigit(): version_spec = '==' + str(version_spec) version_spec = semantic_version.Spec(version_spec) return normalize_version_spec(version_spec) def parse_version(version): if isinstance(version, semantic_version.Version): return version if not version: version = '0' return semantic_version.Version.coerce(str(version)) def traverse(seed, producer=None, track_visited=True): if not yaqlutils.is_iterable(seed): seed = [seed] visited = None if not track_visited else set() queue = collections.deque(seed) while queue: item = queue.popleft() if track_visited: if item in visited: continue visited.add(item) produced = (yield item) if produced is None and producer: produced = producer(item) if produced: queue.extend(produced) def cast(obj, murano_class, pov_or_version_spec=None): if isinstance(obj, dsl_types.MuranoObjectInterface): obj = obj.object if isinstance(pov_or_version_spec, dsl_types.MuranoType): pov_or_version_spec = pov_or_version_spec.package elif isinstance(pov_or_version_spec, str): pov_or_version_spec = parse_version_spec(pov_or_version_spec) if isinstance(murano_class, dsl_types.MuranoTypeReference): murano_class = murano_class.type if isinstance(murano_class, dsl_types.MuranoType): if pov_or_version_spec is None: pov_or_version_spec = parse_version_spec(murano_class.version) murano_class = murano_class.name candidates = [] for cls in itertools.chain((obj.type,), obj.type.ancestors()): if cls.name != murano_class: continue elif isinstance(pov_or_version_spec, semantic_version.Version): if cls.version != pov_or_version_spec: continue elif isinstance(pov_or_version_spec, semantic_version.Spec): if cls.version not in pov_or_version_spec: continue elif isinstance(pov_or_version_spec, dsl_types.MuranoPackage): requirement = pov_or_version_spec.requirements.get( cls.package.name) if requirement is None: raise exceptions.NoClassFound(murano_class) if cls.version not in requirement: continue elif pov_or_version_spec is not None: raise ValueError('pov_or_version_spec of unsupported ' 'type {0}'.format(type(pov_or_version_spec))) candidates.append(cls) if not candidates: raise exceptions.NoClassFound(murano_class) elif len(candidates) > 1: raise exceptions.AmbiguousClassName(murano_class) return obj.cast(candidates[0]) def is_instance_of(obj, class_name, pov_or_version_spec=None): if not isinstance(obj, (dsl_types.MuranoObject, dsl_types.MuranoObjectInterface)): return False try: cast(obj, class_name, pov_or_version_spec) return True except (exceptions.NoClassFound, exceptions.AmbiguousClassName): return False def memoize(func): cache = {} return get_memoize_func(func, cache) def get_memoize_func(func, cache): @functools.wraps(func) def wrap(*args): if args not in cache: result = func(*args) cache[args] = result return result else: return cache[args] return wrap def normalize_version_spec(version_spec): def coerce(v): return semantic_version.Version('{0}.{1}.{2}'.format( v.major, v.minor or 0, v.patch or 0 )) def increment(v): # NOTE(ativelkov): replace these implementations with next_minor() and # next_major() calls when the semantic_version is updated in global # requirements. if v.minor is None: return semantic_version.Version( '.'.join(str(x) for x in [v.major + 1, 0, 0])) else: return semantic_version.Version( '.'.join(str(x) for x in [v.major, v.minor + 1, 0])) def extend(v): return semantic_version.Version(str(v) + '-0') transformations = { '>': [('>=', (increment, extend))], '>=': [('>=', (coerce,))], '<': [('<', (coerce, extend))], '<=': [('<', (increment, extend))], '!=': [('>=', (increment, extend))], '==': [('>=', (coerce,)), ('<', (increment, coerce, extend))] } new_parts = [] for item in version_spec.specs: if item.kind == '*': continue elif item.spec.patch is not None: new_parts.append(str(item)) else: for op, funcs in transformations[item.kind]: new_parts.append('{0}{1}'.format( op, functools.reduce(lambda v, f: f(v), funcs, item.spec) )) if not new_parts: return semantic_version.Spec('*') return semantic_version.Spec(*new_parts) semver_to_api_map = { '>': 'gt', '>=': 'ge', '<': 'lt', '<=': 'le', '!=': 'ne', '==': 'eq' } def breakdown_spec_to_query(normalized_spec): res = [] for item in normalized_spec.specs: if item.kind == '*': continue else: res.append("%s:%s" % (semver_to_api_map[item.kind], item.spec)) return res def link_contexts(parent_context, context): if not context: return parent_context return contexts.LinkedContext(parent_context, context) def inspect_is_static(cls, name): m = cls.__dict__.get(name) if m is None: return False return isinstance(m, staticmethod) def inspect_is_classmethod(cls, name): m = cls.__dict__.get(name) if m is None: return False return isinstance(m, classmethod) def inspect_is_method(cls, name): m = getattr(cls, name, None) if m is None: return False return ((inspect.isfunction(m) or inspect.ismethod(m)) and not inspect_is_static(cls, name) and not inspect_is_classmethod(cls, name)) def inspect_is_property(cls, name): m = getattr(cls, name, None) if m is None: return False return inspect.isdatadescriptor(m) def updated_dict(d, val): if d is None: d = {} else: d = d.copy() if val is not None: d.update(val) return d def resolve_type(value, scope_type, return_reference=False): if value is None: return None if isinstance(scope_type, dsl_types.MuranoTypeReference): scope_type = scope_type.type if not isinstance(value, (dsl_types.MuranoType, dsl_types.MuranoTypeReference)): name = scope_type.namespace_resolver.resolve_name(value) result = scope_type.package.find_class(name) else: result = value if isinstance(result, dsl_types.MuranoTypeReference): if return_reference: return result return result.type elif return_reference: return result.get_reference() return result def parse_object_definition(spec, scope_type, context): if not isinstance(spec, yaqlutils.MappingType): return None if context: spec = evaluate(spec, context, freeze=False) else: spec = spec.copy() system_data = None type_obj = None props = {} ns_resolver = scope_type.namespace_resolver if scope_type else None for key in spec: if (ns_resolver and ns_resolver.is_typename(key, False) or isinstance(key, (dsl_types.MuranoTypeReference, dsl_types.MuranoType))): type_obj = resolve_type(key, scope_type) props = spec.pop(key) or {} system_data = spec break if system_data is None: props = spec if '?' in spec: system_data = spec.pop('?') obj_type = system_data.get('type') if isinstance(obj_type, dsl_types.MuranoTypeReference): type_obj = obj_type.type elif isinstance(obj_type, dsl_types.MuranoType): type_obj = obj_type elif obj_type: type_str, version_str, package_str = parse_type_string( obj_type, system_data.get('classVersion'), system_data.get('package') ) version_spec = parse_version_spec(version_str) package_loader = get_package_loader() if package_str: package = package_loader.load_package( package_str, version_spec) else: package = package_loader.load_class_package( type_str, version_spec) type_obj = package.find_class(type_str, False) else: system_data = {} return { 'type': type_obj, 'properties': yaqlutils.filter_parameters_dict(props), 'id': system_data.get('id'), 'name': system_data.get('name'), 'metadata': system_data.get('metadata'), 'destroyed': system_data.get('destroyed', False), 'dependencies': system_data.get('dependencies', {}), 'extra': { key: value for key, value in system_data.items() if key.startswith('_') } } def assemble_object_definition(parsed, model_format=dsl_types.DumpTypes.Mixed): if model_format == dsl_types.DumpTypes.Inline: result = { parsed['type']: parsed['properties'], 'id': parsed['id'], 'name': parsed['name'], 'metadata': parsed['metadata'], 'dependencies': parsed['dependencies'], 'destroyed': parsed['destroyed'] } result.update(parsed['extra']) return result result = parsed['properties'] header = { 'id': parsed['id'], 'name': parsed['name'], 'metadata': parsed['metadata'] } if parsed['destroyed']: header['destroyed'] = True header.update(parsed['extra']) result['?'] = header if model_format == dsl_types.DumpTypes.Mixed: header['type'] = parsed['type'] return result elif model_format == dsl_types.DumpTypes.Serializable: cls = parsed['type'] if cls: header['type'] = format_type_string(cls) return result else: raise ValueError('Invalid Serialization Type') def function(c): if hasattr(c, '__func__'): return c.__func__ return c def list_value(v): if v is None: return [] if not yaqlutils.is_sequence(v): v = [v] return v def weak_proxy(obj): if obj is None or isinstance(obj, weakref.ProxyType): return obj if isinstance(obj, weakref.ReferenceType): obj = obj() return weakref.proxy(obj) def weak_ref(obj): class MuranoObjectWeakRef(weakref.ReferenceType): def __init__(self, murano_object): self.ref = weakref.ref(murano_object) self.object_id = murano_object.object_id def __call__(self): res = self.ref() if not res: object_store = get_object_store() if object_store: res = object_store.get(self.object_id) if res: self.ref = weakref.ref(res) return res if obj is None or isinstance(obj, weakref.ReferenceType): return obj if isinstance(obj, dsl_types.MuranoObject): return MuranoObjectWeakRef(obj) return weakref.ref(obj) def parse_type_string(type_str, default_version, default_package): res = TYPE_RE.match(type_str) if res is None: return None parsed_type = res.group(1) parsed_version = res.group(2) parsed_package = res.group(3) return ( parsed_type, default_version if parsed_version is None else parsed_version, default_package if parsed_package is None else parsed_package ) def format_type_string(type_obj): if isinstance(type_obj, dsl_types.MuranoTypeReference): type_obj = type_obj.type if isinstance(type_obj, dsl_types.MuranoType): return '{0}/{1}@{2}'.format( type_obj.name, type_obj.version, type_obj.package.name) else: raise ValueError('Invalid argument') def patch_dict(dct, path, value): parts = path.split('.') for i in range(len(parts) - 1): if not isinstance(dct, dict): dct = None break dct = dct.get(parts[i]) if isinstance(dct, dict): if value is yaqlutils.NO_VALUE: dct.pop(parts[-1]) else: dct[parts[-1]] = value def format_scalar(value): if isinstance(value, str): return "'{0}'".format(value) return str(value) def is_passkey(value): passkey = get_contract_passkey() return passkey is not None and value is passkey def find_object_owner(obj, predicate): p = obj.owner while p: if predicate(p): return p p = p.owner return None # This function is not intended to be used in the code but is very useful # for debugging object reference leaks def walk_gc(obj, towards, handler): visited = set() queue = collections.deque([(obj, [])]) while queue: item, trace = queue.popleft() if id(item) in visited: continue if handler(item): if towards: yield trace + [item] else: yield [item] + trace visited.add(id(item)) if towards: try: queue.extend( [(t, trace + [item]) for t in gc.get_referrers(item)] ) except StopIteration: return else: try: queue.extend( [(t, [item] + trace) for t in gc.get_referents(item)] ) except StopIteration: return ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/dsl/lhs_expression.py0000664000175000017500000001563500000000000020636 0ustar00zuulzuul00000000000000# 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 yaql.language import specs from yaql.language import utils from yaql.language import yaqltypes from murano.dsl import constants from murano.dsl import dsl from murano.dsl import dsl_types from murano.dsl import exceptions from murano.dsl import helpers from murano.dsl import yaql_functions from murano.dsl import yaql_integration def _prepare_context(): @specs.parameter('name', yaqltypes.StringConstant()) def get_context_data(context, name): root_context = context['#root_context'] def set_data(value): if not name or name == '$' or name == '$this': raise ValueError('Cannot assign to {0}'.format(name)) ctx = root_context while constants.CTX_VARIABLE_SCOPE not in ctx: ctx = ctx.parent ctx[name] = value return _Property(lambda: root_context[name], set_data) @specs.parameter('this', _Property) @specs.parameter('key', yaqltypes.Keyword()) def attribution(context, this, key): def setter(src_property, value): src = src_property.get() if isinstance(src, utils.MappingType): src_property.set( utils.FrozenDict( itertools.chain( src.items(), ((key, value),)))) elif isinstance(src, dsl_types.MuranoObject): src.set_property(key, value, context['#root_context']) elif isinstance(src, ( dsl_types.MuranoTypeReference, dsl_types.MuranoType)): if isinstance(src, dsl_types.MuranoTypeReference): mc = src.type else: mc = src helpers.get_executor().set_static_property( mc, key, value, context['#root_context']) else: raise ValueError( 'attribution may only be applied to ' 'objects and dictionaries') def getter(src): if isinstance(src, utils.MappingType): return src.get(key, {}) elif isinstance(src, dsl_types.MuranoObject): try: return src.get_property(key, context['#root_context']) except exceptions.UninitializedPropertyAccessError: return {} elif isinstance(src, ( dsl_types.MuranoTypeReference, dsl_types.MuranoType)): if isinstance(src, dsl_types.MuranoTypeReference): mc = src.type else: mc = src return helpers.get_executor().get_static_property( mc, key, context['#root_context']) else: raise ValueError( 'attribution may only be applied to ' 'objects and dictionaries') return _Property( lambda: getter(this.get()), lambda value: setter(this, value)) @specs.parameter('this', _Property) @specs.parameter('index', yaqltypes.Lambda(with_context=True)) def indexation(context, this, index): index = index(context['#root_context']) def getter(src): if utils.is_sequence(src): return src[index] else: raise ValueError('indexation may only be applied to lists') def setter(src_property, value): src = src_property.get() if utils.is_sequence(src): src_property.set(src[:index] + (value,) + src[index + 1:]) elif isinstance(src, utils.MappingType): attribution(context, src_property, index).set(value) if isinstance(index, int): return _Property( lambda: getter(this.get()), lambda value: setter(this, value)) else: return attribution(context, this, index) def _wrap_type_reference(tr, context): return _Property( lambda: tr, context['#self']._invalid_target) @specs.parameter('prefix', yaqltypes.Keyword()) @specs.parameter('name', yaqltypes.Keyword()) @specs.name('#operator_:') def ns_resolve(context, prefix, name): return _wrap_type_reference( yaql_functions.ns_resolve(context, prefix, name), context) @specs.parameter('name', yaqltypes.Keyword()) @specs.name('#unary_operator_:') def ns_resolve_unary(context, name): return _wrap_type_reference( yaql_functions.ns_resolve_unary(context, name), context) @specs.parameter('object_', dsl_types.MuranoObject) def type_(context, object_): return _wrap_type_reference(yaql_functions.type_(object_), context) @specs.name('type') @specs.parameter('cls', dsl.MuranoTypeParameter()) def type_from_name(context, cls): return _wrap_type_reference(cls, context) res_context = yaql_integration.create_empty_context() res_context.register_function(get_context_data, '#get_context_data') res_context.register_function(attribution, '#operator_.') res_context.register_function(indexation, '#indexer') res_context.register_function(ns_resolve) res_context.register_function(ns_resolve_unary) res_context.register_function(type_) res_context.register_function(type_from_name) return res_context class _Property(object): def __init__(self, getter, setter): self._getter = getter self._setter = setter def get(self): return self._getter() def set(self, value): self._setter(value) class LhsExpression(object): lhs_context = _prepare_context() def __init__(self, expression): self._expression = expression def _invalid_target(self, *args, **kwargs): raise exceptions.InvalidLhsTargetError(self._expression) def __call__(self, value, context): new_context = LhsExpression.lhs_context.create_child_context() new_context[''] = context['$'] new_context['#root_context'] = context new_context['#self'] = self for name in (constants.CTX_NAMES_SCOPE,): new_context[name] = context[name] prop = self._expression(context=new_context) if not isinstance(prop, _Property): self._invalid_target() prop.set(value) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/dsl/macros.py0000664000175000017500000001735200000000000017053 0ustar00zuulzuul00000000000000# 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 murano.dsl import constants from murano.dsl import dsl_exception from murano.dsl import exceptions from murano.dsl import expressions from murano.dsl import helpers from murano.dsl import yaql_expression class CodeBlock(expressions.DslExpression): def __init__(self, body): body = helpers.list_value(body) self.code_block = list(map(expressions.parse_expression, body)) def execute(self, context): for expr in self.code_block: if hasattr(expr, 'virtual_instruction'): instruction = expr.virtual_instruction context[constants.CTX_CURRENT_INSTRUCTION] = instruction try: expr.execute(context) except (dsl_exception.MuranoPlException, exceptions.InternalFlowException): raise except Exception as ex: raise dsl_exception.MuranoPlException.from_python_exception( ex, context) class MethodBlock(CodeBlock): def __init__(self, body, name=None): super(MethodBlock, self).__init__(body) self._name = name def execute(self, context): new_context = context.create_child_context() new_context[constants.CTX_VARIABLE_SCOPE] = True try: super(MethodBlock, self).execute(new_context) except exceptions.ReturnException as e: return e.value except exceptions.BreakException: raise exceptions.DslInvalidOperationError( 'Break cannot be used on method level') except exceptions.ContinueException: raise exceptions.DslInvalidOperationError( 'Continue cannot be used on method level') else: return None class ReturnMacro(expressions.DslExpression): def __init__(self, Return): self._value = Return def execute(self, context): raise exceptions.ReturnException( helpers.evaluate(self._value, context)) class BreakMacro(expressions.DslExpression): def __init__(self, Break): if Break: raise exceptions.DslSyntaxError('Break cannot have value') def execute(self, context): raise exceptions.BreakException() class ContinueMacro(expressions.DslExpression): def __init__(self, Continue): if Continue: raise exceptions.DslSyntaxError('Continue cannot have value') def execute(self, context): raise exceptions.ContinueException() class ParallelMacro(CodeBlock): def __init__(self, Parallel, Limit=None): super(ParallelMacro, self).__init__(Parallel) self._limit = Limit or len(self.code_block) def execute(self, context): if not self.code_block: return limit = helpers.evaluate(self._limit, context) helpers.parallel_select( self.code_block, lambda expr: expr.execute(context.create_child_context()), limit) class IfMacro(expressions.DslExpression): def __init__(self, If, Then, Else=None): self._code1 = CodeBlock(Then) self._code2 = None if Else is None else CodeBlock(Else) self._condition = If def execute(self, context): if helpers.evaluate(self._condition, context): self._code1.execute(context) elif self._code2 is not None: self._code2.execute(context) class WhileDoMacro(expressions.DslExpression): def __init__(self, While, Do): if not isinstance(While, yaql_expression.YaqlExpression): raise TypeError() self._code = CodeBlock(Do) self._condition = While def execute(self, context): while self._condition(context): try: self._code.execute(context) except exceptions.BreakException: break except exceptions.ContinueException: continue class ForMacro(expressions.DslExpression): def __init__(self, For, In, Do): if not isinstance(For, str): raise exceptions.DslSyntaxError( 'For value must be of string type') self._code = CodeBlock(Do) self._var = For self._collection = In def execute(self, context): collection = helpers.evaluate(self._collection, context) for t in collection: context[self._var] = t try: self._code.execute(context) except exceptions.BreakException: break except exceptions.ContinueException: continue class RepeatMacro(expressions.DslExpression): def __init__(self, Repeat, Do): if not isinstance(Repeat, (int, yaql_expression.YaqlExpression)): raise exceptions.DslSyntaxError( 'Repeat value must be either int or expression') self._count = Repeat self._code = CodeBlock(Do) def execute(self, context): count = helpers.evaluate(self._count, context) for _ in range(0, count): try: self._code.execute(context) except exceptions.BreakException: break except exceptions.ContinueException: continue class MatchMacro(expressions.DslExpression): def __init__(self, Match, Value, Default=None): if not isinstance(Match, dict): raise exceptions.DslSyntaxError( 'Match value must be of dictionary type') self._switch = Match self._value = Value self._default = None if Default is None else CodeBlock(Default) def execute(self, context): match_value = helpers.evaluate(self._value, context) for key, value in self._switch.items(): if key == match_value: CodeBlock(value).execute(context) return if self._default is not None: self._default.execute(context) class SwitchMacro(expressions.DslExpression): def __init__(self, Switch, Default=None): if not isinstance(Switch, dict): raise exceptions.DslSyntaxError( 'Switch value must be of dictionary type') self._switch = Switch self._default = None if Default is None else CodeBlock(Default) def execute(self, context): matched = False for key, value in self._switch.items(): if helpers.evaluate(key, context): matched = True CodeBlock(value).execute(context) if self._default is not None and not matched: self._default.execute(context) class DoMacro(expressions.DslExpression): def __init__(self, Do): self._code = CodeBlock(Do) def execute(self, context): self._code.execute(context) def register(): expressions.register_macro(DoMacro) expressions.register_macro(ReturnMacro) expressions.register_macro(BreakMacro) expressions.register_macro(ContinueMacro) expressions.register_macro(ParallelMacro) expressions.register_macro(IfMacro) expressions.register_macro(WhileDoMacro) expressions.register_macro(ForMacro) expressions.register_macro(RepeatMacro) expressions.register_macro(MatchMacro) expressions.register_macro(SwitchMacro) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/dsl/meta.py0000664000175000017500000001114100000000000016503 0ustar00zuulzuul00000000000000# 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 abc import operator import weakref from murano.dsl import dsl_types from murano.dsl import helpers class MetaProvider(object): @abc.abstractmethod def get_meta(self, context): raise NotImplementedError() class MetaData(MetaProvider): def __init__(self, definition, target, declaring_type): declaring_type = weakref.proxy(declaring_type) definition = helpers.list_value(definition) factories = [] used_types = set() for d in definition: if isinstance(d, dict): if len(d) != 1: raise ValueError('Invalid Meta format') name = next(iter(d.keys())) props = d[name] or {} else: name = d props = {} type_obj = helpers.resolve_type(name, declaring_type) if type_obj.usage != dsl_types.ClassUsages.Meta: raise ValueError('Only Meta classes can be attached') if target not in type_obj.targets: raise ValueError( u'Meta class {} is not applicable here'.format( type_obj.name)) if type_obj in used_types and ( type_obj.cardinality != dsl_types.MetaCardinality.Many): raise ValueError('Cannot attach several Meta instances ' 'with cardinality One') used_types.add(type_obj) def factory_maker(template): def instantiate(context): obj = helpers.get_object_store().load( template, owner=None, context=context, scope_type=declaring_type, bypass_store=True) obj.declaring_type = declaring_type return obj return instantiate factories.append(factory_maker({type_obj: props})) self._meta_factories = factories self._meta = None def get_meta(self, context): if self._meta is None: self._meta = list(map(lambda x: x(context), self._meta_factories)) return self._meta def merge_providers(initial_class, producer, context): def merger(cls_list, skip_list): result = set() all_meta = [] for cls in cls_list: cls_skip_list = skip_list.copy() provider = producer(cls) meta = [] if provider is None else provider.get_meta(context) for item in meta: cardinality = item.type.cardinality inherited = item.type.inherited if cls != initial_class and ( not inherited or item.type in skip_list): continue if cardinality == dsl_types.MetaCardinality.One: cls_skip_list.add(item.type) all_meta.append((cls, item)) all_meta.extend(merger(cls.parents, cls_skip_list)) meta_types = {} for cls, item in all_meta: entry = meta_types.get(item.type) if entry is not None: if entry != cls: raise ValueError( u'Found more than one instance of Meta {} ' u'with Cardinality One'.format(item.type.name)) else: continue if item.type.cardinality == dsl_types.MetaCardinality.One: meta_types[item.type] = cls result.add((cls, item)) return result meta = merger([initial_class], set()) return list(map(operator.itemgetter(1), meta)) def aggregate_meta(provider, context, group_by_name=True): def key_func(m): return m.type.name if group_by_name else m.type meta = provider.get_meta(context) result = {} for item in meta: if item.type.cardinality == dsl_types.MetaCardinality.One: result[key_func(item)] = item else: result.setdefault(key_func(item), []).append(item) return result ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/dsl/murano_method.py0000664000175000017500000002556600000000000020436 0ustar00zuulzuul00000000000000# 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 sys import weakref from oslo_log import log as logging from yaql.language import specs from murano.common import utils from murano.dsl import constants from murano.dsl import dsl from murano.dsl import dsl_types from murano.dsl import exceptions from murano.dsl import helpers from murano.dsl import macros from murano.dsl import meta from murano.dsl import typespec from murano.dsl import virtual_exceptions from murano.dsl import yaql_integration LOG = logging.getLogger(__name__) macros.register() virtual_exceptions.register() class MuranoMethod(dsl_types.MuranoMethod, meta.MetaProvider): def __init__(self, declaring_type, name, payload, original_name=None, ephemeral=False): self._name = name original_name = original_name or name self._declaring_type = weakref.ref(declaring_type) self._meta_values = None self_ref = self if ephemeral else weakref.proxy(self) if callable(payload): if isinstance(payload, specs.FunctionDefinition): self._body = payload else: self._body = yaql_integration.get_function_definition( payload, self_ref, original_name) self._arguments_scheme = None self._scope = self._body.meta.get(constants.META_SCOPE) if declaring_type.extension_class and any(( helpers.inspect_is_static( declaring_type.extension_class, original_name), helpers.inspect_is_classmethod( declaring_type.extension_class, original_name))): self._usage = self._body.meta.get( constants.META_USAGE, dsl_types.MethodUsages.Static) if self._usage not in dsl_types.MethodUsages.StaticMethods: raise ValueError( 'Invalid Usage for static method ' + self.name) else: self._usage = (self._body.meta.get(constants.META_USAGE) or dsl_types.MethodUsages.Runtime) if self._usage not in dsl_types.MethodUsages.InstanceMethods: raise ValueError( 'Invalid Usage for instance method ' + self.name) self._resolve_usage_and_scope() if self._scope is None: self._scope = dsl_types.MethodScopes.Session if (self._body.name.startswith('#') or self._body.name.startswith('*')): raise ValueError( 'Import of special yaql functions is forbidden') self._meta = meta.MetaData( self._body.meta.get(constants.META_MPL_META), dsl_types.MetaTargets.Method, declaring_type) else: payload = payload or {} self._body = macros.MethodBlock(payload.get('Body'), name) self._usage = payload.get( 'Usage') or dsl_types.MethodUsages.Runtime self._scope = payload.get('Scope') self._resolve_usage_and_scope() if self._scope is None: self._scope = dsl_types.MethodScopes.Session arguments_scheme = helpers.list_value(payload.get('Arguments')) if isinstance(arguments_scheme, dict): arguments_scheme = [{key: value} for key, value in arguments_scheme.items()] self._arguments_scheme = collections.OrderedDict() seen_varargs = False seen_kwargs = False args_order_error = False for record in arguments_scheme: if not isinstance(record, dict) or len(record) > 1: raise exceptions.DslSyntaxError( 'Invalid arguments declaration') name = list(record.keys())[0] argument = MuranoMethodArgument( self, self.name, name, record[name]) usage = argument.usage if (usage == dsl_types.MethodArgumentUsages.Standard and (seen_kwargs or seen_varargs)): args_order_error = True elif usage == dsl_types.MethodArgumentUsages.VarArgs: if seen_kwargs or seen_varargs: args_order_error = True seen_varargs = True elif usage == dsl_types.MethodArgumentUsages.KwArgs: if seen_kwargs: args_order_error = True seen_kwargs = True if args_order_error: raise exceptions.DslSyntaxError( 'Invalid argument order in method {0}'.format( self.name)) else: self._arguments_scheme[name] = argument self._meta = meta.MetaData( payload.get('Meta'), dsl_types.MetaTargets.Method, declaring_type) self._instance_stub, self._static_stub = \ yaql_integration.build_stub_function_definitions(self_ref) def _resolve_usage_and_scope(self): if self._usage == dsl_types.MethodUsages.Action: runtime_version = self.declaring_type.package.runtime_version if runtime_version > constants.RUNTIME_VERSION_1_3: LOG.warning('"Usage: Action" is deprecated, ' 'use "Scope: Public" instead') if self._scope == dsl_types.MethodScopes.Session: raise ValueError( 'Both "Usage: Action" and "Scope: Session" are ' 'provided for method ' + self.name) self._scope = dsl_types.MethodScopes.Public @property def name(self): return self._name @property def declaring_type(self): return self._declaring_type() @property def arguments_scheme(self): return self._arguments_scheme @property def instance_stub(self): return self._instance_stub @property def static_stub(self): return self._static_stub @property def usage(self): return self._usage @usage.setter def usage(self, value): self._usage = value @property def scope(self): return self._scope @scope.setter def scope(self, value): self._scope = value @property def body(self): return self._body @property def is_static(self): return self.usage in dsl_types.MethodUsages.StaticMethods @property def is_action(self): return (self.scope == dsl_types.MethodScopes.Public or self.usage == dsl_types.MethodUsages.Action) def get_meta(self, context): def meta_producer(cls): method = cls.methods.get(self.name) if method is None: return None return method._meta if self._meta_values is None: executor = helpers.get_executor() context = executor.create_type_context( self.declaring_type, caller_context=context) self._meta_values = meta.merge_providers( self.declaring_type, meta_producer, context) return self._meta_values def __repr__(self): return 'MuranoMethod({0}::{1})'.format( self.declaring_type.name, self.name) def invoke(self, this, args, kwargs, context=None, skip_stub=False): if isinstance(this, dsl.MuranoObjectInterface): this = this.object if this and not self.declaring_type.is_compatible(this): raise Exception("'this' must be of compatible type") if not this and not self.is_static: raise Exception("A class instance is required") if isinstance(this, dsl_types.MuranoObject): this = this.cast(self.declaring_type) else: this = self.declaring_type executor = helpers.get_executor() return executor.invoke_method( self, this, context, args, kwargs, skip_stub) class MuranoMethodArgument(dsl_types.MuranoMethodArgument, typespec.Spec, meta.MetaProvider): def __init__(self, murano_method, method_name, arg_name, declaration): super(MuranoMethodArgument, self).__init__( declaration, murano_method.declaring_type) self._method_name = method_name self._arg_name = arg_name self._murano_method = weakref.ref(murano_method) self._meta = meta.MetaData( declaration.get('Meta'), dsl_types.MetaTargets.Argument, self.murano_method.declaring_type) self._usage = declaration.get('Usage') or \ dsl_types.MethodArgumentUsages.Standard if self._usage not in dsl_types.MethodArgumentUsages.All: raise exceptions.DslSyntaxError( 'Unknown usage {0}. Must be one of ({1})'.format( self._usage, ', '.join(dsl_types.MethodArgumentUsages.All) )) def transform(self, value, this, *args, **kwargs): try: if self.murano_method.usage == dsl_types.MethodUsages.Extension: this = self.murano_method.declaring_type return super(MuranoMethodArgument, self).transform( value, this, *args, **kwargs) except exceptions.ContractViolationException as e: msg = u'[{0}::{1}({2}{3})] {4}'.format( self.murano_method.declaring_type.name, self.murano_method.name, self.name, e.path, str(e)) utils.reraise(exceptions.ContractViolationException, exceptions.ContractViolationException(msg), sys.exc_info()[2]) @property def murano_method(self): return self._murano_method() @property def name(self): return self._arg_name @property def usage(self): return self._usage def get_meta(self, context): executor = helpers.get_executor() context = executor.create_type_context( self.murano_method.declaring_type, caller_context=context) return self._meta.get_meta(context) def __repr__(self): return 'MuranoMethodArgument({method}::{name})'.format( method=self.murano_method.name, name=self.name) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/dsl/murano_object.py0000664000175000017500000004103500000000000020411 0ustar00zuulzuul00000000000000# 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 murano.dsl import constants from murano.dsl import dsl from murano.dsl import dsl_types from murano.dsl import exceptions from murano.dsl import helpers from murano.dsl.principal_objects import garbage_collector from murano.dsl import yaql_integration class MuranoObject(dsl_types.MuranoObject): def __init__(self, murano_class, owner, object_id=None, name=None, known_classes=None, this=None, metadata=None): self._initialized = False self._destroyed = False if known_classes is None: known_classes = {} if this is None: self._owner = owner self._object_id = object_id or helpers.generate_id() self._type = murano_class self._properties = {} self._parents = {} self._this = this self._name = name self._extension = None self._executor = helpers.weak_ref(helpers.get_executor()) self._config = murano_class.package.get_class_config( murano_class.name) self._metadata = metadata if not isinstance(self._config, dict): self._config = {} known_classes[murano_class.name] = self for parent_class in murano_class.parents: name = parent_class.name if name not in known_classes: obj = MuranoObject( parent_class, owner, object_id=self._object_id, known_classes=known_classes, this=self.real_this) self._parents[name] = known_classes[name] = obj else: self._parents[name] = known_classes[name] self._destruction_dependencies = [] @property def extension(self): return self._extension @property def name(self): return self.real_this._name @property def metadata(self): return self.real_this._metadata @extension.setter def extension(self, value): self._extension = value def initialize(self, context, params, used_names=None): context = context.create_child_context() context[constants.CTX_ALLOW_PROPERTY_WRITES] = True object_store = helpers.get_object_store() for property_name in self.type.properties: spec = self.type.properties[property_name] if spec.usage == dsl_types.PropertyUsages.Config: if property_name in self._config: property_value = self._config[property_name] else: property_value = dsl.NO_VALUE self.set_property(property_name, property_value, dry_run=self._initialized) init = self.type.methods.get('.init') used_names = used_names or set() names = set(self.type.properties) if init: names.update(init.arguments_scheme.keys()) last_errors = len(names) init_args = {} while True: errors = 0 for property_name in names: if init and property_name in init.arguments_scheme: spec = init.arguments_scheme[property_name] is_init_arg = True else: spec = self.type.properties[property_name] is_init_arg = False if property_name in used_names: continue if spec.usage in (dsl_types.PropertyUsages.Config, dsl_types.PropertyUsages.Static): used_names.add(property_name) continue if spec.usage == dsl_types.PropertyUsages.Runtime: if not spec.has_default: used_names.add(property_name) continue property_value = dsl.NO_VALUE else: property_value = params.get(property_name, dsl.NO_VALUE) try: if is_init_arg: init_args[property_name] = property_value else: self.set_property( property_name, property_value, context, dry_run=self._initialized) used_names.add(property_name) except exceptions.UninitializedPropertyAccessError: errors += 1 except exceptions.ContractViolationException: if spec.usage != dsl_types.PropertyUsages.Runtime: raise if not errors: break if errors >= last_errors: raise exceptions.CircularExpressionDependenciesError() last_errors = errors if (not object_store.initializing and self._extension is None and not self._initialized and not self._destroyed and not helpers.is_objects_dry_run_mode()): method = self.type.methods.get('__init__') if method: filtered_params = yaql_integration.filter_parameters( method.body, **params) yield lambda: method.invoke( self, filtered_params[0], filtered_params[1], context) for parent in self._parents.values(): for t in parent.initialize(context, params, used_names): yield t def run_init(): if init: context[constants.CTX_ARGUMENT_OWNER] = self.real_this init.invoke(self.real_this, (), init_args, context.create_child_context()) self._initialized = True if (not object_store.initializing and not helpers.is_objects_dry_run_mode() and not self._initialized and not self._destroyed): yield run_init @property def object_id(self): return self._object_id @property def type(self): return self._type @property def owner(self): if self._this is None: return self._owner else: return self.real_this.owner @property def real_this(self): return self._this or self @property def executor(self): return self._executor() @property def initialized(self): return self._initialized @property def destruction_dependencies(self): return self._destruction_dependencies def load_dependencies(self, dependencies): self._destruction_dependencies = [] if not dependencies: return destruction_dependencies = dependencies.get('onDestruction', []) object_store = helpers.get_object_store() for record in destruction_dependencies: subscriber_id = record['subscriber'] subscriber = object_store.get(subscriber_id) if not subscriber: continue garbage_collector.GarbageCollector.subscribe_destruction( self, subscriber, record.get('handler')) def get_property(self, name, context=None): start_type, derived = self.type, False caller_class = None if not context else helpers.get_type(context) if caller_class is not None and caller_class.is_compatible(self): start_type, derived = caller_class, True declared_properties = start_type.find_properties( lambda p: p.name == name) if len(declared_properties) > 0: spec = self.real_this.type.find_single_property(name) if spec.usage == dsl_types.PropertyUsages.Static: return self.executor.get_static_property( spec.declaring_type, name, context) else: return self.real_this._get_property_value(name) elif derived: return self.cast(caller_class)._get_property_value(name) else: raise exceptions.PropertyReadError(name, start_type) def _get_property_value(self, name): try: return self._properties[name] except KeyError: raise exceptions.UninitializedPropertyAccessError( name, self.type) def set_property(self, name, value, context=None, dry_run=False): start_type, derived = self.real_this.type, False caller_class = None if not context else helpers.get_type(context) if caller_class is not None and caller_class.is_compatible(self): start_type, derived = caller_class, True if context is None: context = self.executor.create_object_context(self) declared_properties = start_type.find_properties( lambda p: p.name == name) if len(declared_properties) > 0: ultimate_spec = self.real_this.type.find_single_property(name) property_list = list(self._list_properties(name)) for spec in property_list: if (caller_class is not None and not helpers.are_property_modifications_allowed(context) and (spec.usage not in dsl_types.PropertyUsages.Writable or not derived)): raise exceptions.NoWriteAccessError(name) if spec.usage == dsl_types.PropertyUsages.Static: default = None else: default = self._config.get(name, spec.default) if spec is ultimate_spec: value = spec.transform( value, self.real_this, self.real_this, context, default=default, finalize=len(property_list) == 1) else: spec.validate(value, self.real_this, context, default) if len(property_list) > 1: value = ultimate_spec.finalize(value, self.real_this, context) if ultimate_spec.usage == dsl_types.PropertyUsages.Static: self.executor.set_static_property( ultimate_spec.declaring_type, name, value, context, dry_run=dry_run) elif not dry_run: self.real_this._properties[name] = value elif derived: if not dry_run: obj = self.cast(caller_class) obj._properties[name] = value else: raise exceptions.PropertyWriteError(name, start_type) def cast(self, cls): for p in helpers.traverse(self, lambda t: t._parents.values()): if p.type == cls: return p raise TypeError('Cannot cast {0} to {1}'.format(self.type, cls)) def _list_properties(self, name): for p in helpers.traverse( self.real_this, lambda t: t._parents.values()): if name in p.type.properties: yield p.type.properties[name] def __repr__(self): return '<{0}/{1} {2} ({3})>'.format( self.type.name, self.type.version, self.object_id, id(self)) def to_dictionary(self, include_hidden=False, serialization_type=dsl_types.DumpTypes.Serializable, allow_refs=False, with_destruction_dependencies=False): context = helpers.get_context() result = {} for parent in self._parents.values(): result.update(parent.to_dictionary( include_hidden, dsl_types.DumpTypes.Serializable, allow_refs)) skip_usages = (dsl_types.PropertyUsages.Runtime, dsl_types.PropertyUsages.Config) for property_name in self.type.properties: if property_name in self.real_this._properties: spec = self.type.properties[property_name] if spec.usage not in skip_usages or include_hidden: prop_value = self.real_this._properties[property_name] if isinstance(prop_value, MuranoObject) and allow_refs: meta = [m for m in spec.get_meta(context) if m.type.name == ('io.murano.metadata.' 'engine.Serialize')] if meta and meta[0].get_property( 'as', context) == 'reference': prop_value = prop_value.object_id result[property_name] = prop_value if serialization_type == dsl_types.DumpTypes.Inline: result.pop('?') result = { self.type: result, 'id': self.object_id, 'name': self.name, 'metadata': self.metadata } header = result else: if serialization_type == dsl_types.DumpTypes.Mixed: result.update({'?': { 'type': self.type, 'id': self.object_id, 'name': self.name, 'metadata': self.metadata }}) else: result.update({'?': { 'type': helpers.format_type_string(self.type), 'id': self.object_id, 'name': self.name, 'metadata': self.metadata }}) header = result['?'] if self.destroyed: header['destroyed'] = True if with_destruction_dependencies: dds = [] for record in self.destruction_dependencies: subscriber = record['subscriber']() if not subscriber or self.executor.object_store.is_doomed( subscriber): continue dds.append({ 'subscriber': subscriber.object_id, 'handler': record['handler'] }) if dds: header.setdefault('dependencies', {})['onDestruction'] = dds return result def mark_destroyed(self, clear_data=False): self._destroyed = True self._suppress__del__ = None if clear_data or not self.initialized: self._extension = None self._properties = None self._owner = None self._destruction_dependencies = None self._this = None for p in self._parents.values(): p.mark_destroyed(clear_data) @property def destroyed(self): return self._destroyed class RecyclableMuranoObject(MuranoObject): def __init__(self, *args, **kwargs): # Create self-reference to prevent __del__ from being called # automatically when there are no other objects referring to this one. # Without this reference __del__ will get called immediately after # reference counter will go to 0 and the object will put itself into # pending list creating another reference to itself and thus preventing # its child objects from being deleted. After the .destroy method # child objects will become eligible for destruction but will be # unable to use find() method since their owner will be destroyed # and collected at that point. With this reference gc.collect() # will collect the whole object graph at once and then we could # sort it and destroy in the correct order so that child objects # will be destroyed first. self._suppress__del__ = self super(RecyclableMuranoObject, self).__init__(*args, **kwargs) def __del__(self): # For Py2 the purpose of __del__ (in combination with _suppress__del__) # method is just to prevent object from being released automatically. # In Py3 the gc.collect list will be empty and __del__ will be called # for objects that were not destroyed yet. if self._this is None and self._initialized and not self._destroyed: self.executor.object_store.schedule_object_destruction(self) def mark_destroyed(self, clear_data=False): self.executor.attribute_store.forget_object(self) super(RecyclableMuranoObject, self).mark_destroyed(clear_data) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/dsl/murano_package.py0000664000175000017500000002142700000000000020541 0ustar00zuulzuul00000000000000# 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 inspect import warnings import weakref import debtcollector import semantic_version from yaql.language import specs from yaql.language import utils from murano.dsl import constants from murano.dsl import dsl_types from murano.dsl import exceptions from murano.dsl import helpers from murano.dsl import meta as dslmeta from murano.dsl import murano_object from murano.dsl import murano_type from murano.dsl import namespace_resolver from murano.dsl import principal_objects from murano.dsl import yaql_integration class MuranoPackage(dsl_types.MuranoPackage, dslmeta.MetaProvider): def __init__(self, package_loader, name, version=None, runtime_version=None, requirements=None, meta=None): super(MuranoPackage, self).__init__() self._package_loader = weakref.proxy(package_loader) self._name = name self._version = helpers.parse_version(version) self._runtime_version = helpers.parse_version(runtime_version) self._requirements = { name: semantic_version.Spec('==' + str(self._version.major)) } if name != constants.CORE_LIBRARY: self._requirements[constants.CORE_LIBRARY] = \ semantic_version.Spec('==0') self._classes = {} self._imported_types = {object, murano_object.MuranoObject} for key, value in (requirements or {}).items(): self._requirements[key] = helpers.parse_version_spec(value) self._load_queue = {} self._native_load_queue = {} if self.name == constants.CORE_LIBRARY: principal_objects.register(self) self._package_class = self._create_package_class() self._meta = lambda: dslmeta.MetaData( meta, dsl_types.MetaTargets.Package, self._package_class) @property def package_loader(self): return self._package_loader @property def name(self): return self._name @property def version(self): return self._version @property def runtime_version(self): return self._runtime_version @property def requirements(self): return self._requirements @property def classes(self): return set(self._classes.keys()).union( self._load_queue.keys()).union(self._native_load_queue.keys()) def get_resource(self, name): raise NotImplementedError('resource API is not implemented') # noinspection PyMethodMayBeStatic def get_class_config(self, name): return {} def _register_mpl_classes(self, data, name=None): type_obj = self._classes.get(name) if type_obj is not None: return type_obj if callable(data): data = data() data = helpers.list_value(data) unnamed_class = None last_ns = {} for cls_data in data: last_ns = cls_data.setdefault('Namespaces', last_ns.copy()) if len(cls_data) == 1: continue cls_name = cls_data.get('Name') if not cls_name: if unnamed_class: raise exceptions.AmbiguousClassName(name) unnamed_class = cls_data else: ns_resolver = namespace_resolver.NamespaceResolver(last_ns) cls_name = ns_resolver.resolve_name(cls_name) if cls_name == name: type_obj = murano_type.create( cls_data, self, cls_name, ns_resolver) self._classes[name] = type_obj else: self._load_queue.setdefault(cls_name, cls_data) if type_obj is None and unnamed_class: unnamed_class['Name'] = name return self._register_mpl_classes(unnamed_class, name) return type_obj def _register_native_class(self, cls, name): if cls in self._imported_types: return self._classes[name] try: m_class = self.find_class(name, False) except exceptions.NoClassFound: m_class = self._register_mpl_classes({'Name': name}, name) m_class.extension_class = cls for method_name in dir(cls): if method_name.startswith('_'): continue method = getattr(cls, method_name) if not any(( helpers.inspect_is_method(cls, method_name), helpers.inspect_is_static(cls, method_name), helpers.inspect_is_classmethod(cls, method_name))): continue method_name_alias = (getattr( method, '__murano_name', None) or specs.convert_function_name( method_name, yaql_integration.CONVENTION)) m_class.add_method(method_name_alias, method, method_name) self._imported_types.add(cls) return m_class def register_class(self, cls, name=None): if inspect.isclass(cls): name = name or getattr(cls, '__murano_name', None) or cls.__name__ if name in self._classes: self._register_native_class(cls, name) else: self._native_load_queue.setdefault(name, cls) elif isinstance(cls, dsl_types.MuranoType): self._classes[cls.name] = cls elif name not in self._classes: self._load_queue[name] = cls def find_class(self, name, search_requirements=True): payload = self._native_load_queue.pop(name, None) if payload is not None: return self._register_native_class(payload, name) payload = self._load_queue.pop(name, None) if payload is not None: result = self._register_mpl_classes(payload, name) if result: return result result = self._classes.get(name) if result: return result if search_requirements: pkgs_for_search = [] for package_name, version_spec in self._requirements.items(): if package_name == self.name: continue referenced_package = self._package_loader.load_package( package_name, version_spec) try: return referenced_package.find_class(name, False) except exceptions.NoClassFound: if name.startswith('io.murano.extensions'): try: short_name = name.replace( 'io.murano.extensions.', '', 1) result = referenced_package.find_class( short_name, False) warnings.simplefilter("once") msg = ("Plugin %(name)s was not found, but a " "%(shorter_name)s was found instead and " "will be used. This could be caused by " "recent change in plugin naming scheme. If " "you are developing applications targeting " "this plugin consider changing its name" % {'name': name, 'shorter_name': short_name}) debtcollector.deprecate(msg) return result except exceptions.NoClassFound: pass pkgs_for_search.append(referenced_package) continue raise exceptions.NoClassFound( name, packages=pkgs_for_search + [self]) raise exceptions.NoClassFound(name, packages=[self]) @property def context(self): return None def _create_package_class(self): ns_resolver = namespace_resolver.NamespaceResolver(None) return murano_type.MuranoClass( ns_resolver, self.name, self, utils.NO_VALUE) def get_meta(self, context): if callable(self._meta): executor = helpers.get_executor() context = executor.create_package_context(self) self._meta = self._meta().get_meta(context) return self._meta def __repr__(self): return 'MuranoPackage({name})'.format(name=self.name) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/dsl/murano_property.py0000664000175000017500000000564100000000000021032 0ustar00zuulzuul00000000000000# 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 sys import weakref from murano.common import utils from murano.dsl import dsl_types from murano.dsl import exceptions from murano.dsl import helpers from murano.dsl import meta from murano.dsl import typespec class MuranoProperty(dsl_types.MuranoProperty, typespec.Spec, meta.MetaProvider): def __init__(self, declaring_type, property_name, declaration): super(MuranoProperty, self).__init__(declaration, declaring_type) self._property_name = property_name self._declaring_type = weakref.ref(declaring_type) self._usage = declaration.get('Usage') or dsl_types.PropertyUsages.In if self._usage not in dsl_types.PropertyUsages.All: raise exceptions.DslSyntaxError( 'Unknown usage {0}. Must be one of ({1})'.format( self._usage, ', '.join(dsl_types.PropertyUsages.All))) self._meta = meta.MetaData( declaration.get('Meta'), dsl_types.MetaTargets.Property, declaring_type) self._meta_values = None def transform(self, *args, **kwargs): try: return super(MuranoProperty, self).transform(*args, **kwargs) except exceptions.ContractViolationException as e: msg = u'[{0}.{1}{2}] {3}'.format( self.declaring_type.name, self.name, e.path, str(e)) utils.reraise(exceptions.ContractViolationException, exceptions.ContractViolationException(msg), sys.exc_info()[2]) @property def name(self): return self._property_name @property def usage(self): return self._usage def get_meta(self, context): def meta_producer(cls): prop = cls.properties.get(self.name) if prop is None: return None return prop._meta if self._meta_values is None: executor = helpers.get_executor() context = executor.create_type_context( self.declaring_type, caller_context=context) self._meta_values = meta.merge_providers( self.declaring_type, meta_producer, context) return self._meta_values def __repr__(self): return 'MuranoProperty({type}::{name})'.format( type=self.declaring_type.name, name=self.name) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/dsl/murano_type.py0000664000175000017500000004703000000000000020125 0ustar00zuulzuul00000000000000# 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 abc import collections import copy import weakref import semantic_version from yaql.language import utils from murano.dsl import constants from murano.dsl import dsl from murano.dsl import dsl_types from murano.dsl import exceptions from murano.dsl import helpers from murano.dsl import meta as dslmeta from murano.dsl import murano_method from murano.dsl import murano_object from murano.dsl import murano_property from murano.dsl import yaql_integration class MuranoType(dsl_types.MuranoType): def __init__(self, ns_resolver, name, package): self._namespace_resolver = ns_resolver self._name = name self._package = weakref.ref(package) @property def name(self): return self._name @property def package(self): return self._package() @property def namespace_resolver(self): return self._namespace_resolver @property @abc.abstractmethod def usage(self): raise NotImplementedError() @property def version(self): return self.package.version def get_reference(self): return dsl_types.MuranoTypeReference(self) class MuranoClass(dsl_types.MuranoClass, MuranoType, dslmeta.MetaProvider): _allowed_usages = {dsl_types.ClassUsages.Class} def __init__(self, ns_resolver, name, package, parents, meta=None, imports=None): super(MuranoClass, self).__init__(ns_resolver, name, package) self._methods = {} self._properties = {} self._config = {} self._extension_class = None if (self._name == constants.CORE_LIBRARY_OBJECT or parents is utils.NO_VALUE): self._parents = [] else: self._parents = parents or [ package.find_class(constants.CORE_LIBRARY_OBJECT)] for p in self._parents: if p.usage not in self._allowed_usages: raise exceptions.InvalidInheritanceError( u'Type {0} cannot have parent with Usage {1}'.format( self.name, p.usage)) remappings = self._build_parent_remappings() self._parents = self._adjusted_parents(remappings) self._context = None self._exported_context = None self._meta = dslmeta.MetaData(meta, dsl_types.MetaTargets.Type, self) self._meta_values = None self._imports = list(self._resolve_imports(imports)) def _adjusted_parents(self, remappings): seen = {} def altered_clone(class_): seen_class = seen.get(class_) if seen_class is not None: return seen_class cls_remapping = remappings.get(class_) if cls_remapping is not None: return altered_clone(cls_remapping) new_parents = [altered_clone(p) for p in class_._parents] if all(a is b for a, b in zip(class_._parents, new_parents)): return class_ res = copy.copy(class_) res._parents = new_parents res._meta_values = None res._context = None res._exported_context = None seen[class_] = res return res return [altered_clone(p) for p in self._parents] def __eq__(self, other): if not isinstance(other, MuranoType): return False return self.name == other.name and self.version == other.version def __ne__(self, other): return not self == other def __hash__(self): return hash((self.name, self.version)) @property def usage(self): return dsl_types.ClassUsages.Class @property def parents(self): return self._parents @property def methods(self): return self._methods @property def all_method_names(self): names = set(self.methods.keys()) for c in self.ancestors(): names.update(c.methods.keys()) return tuple(names) @property def extension_class(self): return self._extension_class @extension_class.setter def extension_class(self, cls): self._extension_class = cls ctor = yaql_integration.get_class_factory_definition(cls, self) self.add_method('__init__', ctor) def add_method(self, name, payload, original_name=None): method = murano_method.MuranoMethod(self, name, payload, original_name) self._methods[name] = method self._context = None self._exported_context = None return method @property def properties(self): return self._properties @property def all_property_names(self): names = set(self.properties.keys()) for c in self.ancestors(): names.update(c.properties.keys()) return tuple(names) def add_property(self, property_typespec): if not isinstance(property_typespec, murano_property.MuranoProperty): raise TypeError('property_typespec') self._properties[property_typespec.name] = property_typespec def _find_symbol_chains(self, func): queue = collections.deque([(self, ())]) while queue: cls, path = queue.popleft() symbol = func(cls) segment = (symbol,) if symbol is not None else () leaf = True for p in cls.parents: leaf = False queue.append((p, path + segment)) if leaf: path += segment if path: yield path def _resolve_imports(self, imports): seen = {self.name} for imp in helpers.list_value(imports): if imp in seen: continue type = helpers.resolve_type(imp, self) if type in seen: continue seen.add(imp) seen.add(type) yield type def _choose_symbol(self, func): chains = sorted( self._find_symbol_chains(func), key=lambda t: len(t)) result = [] for i in range(len(chains)): if chains[i][0] in result: continue add = True for j in range(i + 1, len(chains)): common = 0 if not add: break for p in range(len(chains[i])): if chains[i][-p - 1] is chains[j][-p - 1]: common += 1 else: break if common == len(chains[i]): add = False break if add: result.append(chains[i][0]) return result def find_method(self, name): return self._choose_symbol(lambda cls: cls.methods.get(name)) def find_property(self, name): return self._choose_symbol( lambda cls: cls.properties.get(name)) def find_static_property(self, name): def prop_func(cls): prop = cls.properties.get(name) if prop is not None and prop.usage == 'Static': return prop result = self._choose_symbol(prop_func) if len(result) < 1: raise exceptions.NoPropertyFound(name) elif len(result) > 1: raise exceptions.AmbiguousPropertyNameError(name) return result[0] def find_single_method(self, name): result = self.find_method(name) if len(result) < 1: raise exceptions.NoMethodFound(name) elif len(result) > 1: raise exceptions.AmbiguousMethodName(name) return result[0] def find_methods(self, predicate): result = list(filter(predicate, self.methods.values())) for c in self.ancestors(): for method in c.methods.values(): if predicate(method) and method not in result: result.append(method) return result def find_properties(self, predicate): result = list(filter(predicate, self.properties.values())) for c in self.ancestors(): for prop in c.properties.values(): if predicate(prop) and prop not in result: result.append(prop) return result def _iterate_unique_methods(self): for name in self.all_method_names: try: yield self.find_single_method(name) except exceptions.AmbiguousMethodName: def func(*args, **kwargs): raise yield murano_method.MuranoMethod( self, name, func, ephemeral=True) def find_single_property(self, name): result = self.find_property(name) if len(result) < 1: raise exceptions.NoPropertyFound(name) elif len(result) > 1: raise exceptions.AmbiguousPropertyNameError(name) return result[0] def invoke(self, name, this, args, kwargs, context=None): method = self.find_single_method(name) return method.invoke(this, args, kwargs, context) def is_compatible(self, obj): if isinstance(obj, (murano_object.MuranoObject, dsl.MuranoObjectInterface, dsl_types.MuranoTypeReference)): obj = obj.type if obj == self: return True return any(cls == self for cls in obj.ancestors()) def __repr__(self): return 'MuranoClass({0}/{1})'.format(self.name, self.version) def _build_parent_remappings(self): """Remaps class parents. In case of multiple inheritance class may indirectly get several versions of the same class. It is reasonable to try to replace them with single version to avoid conflicts. We can do that when within versions that satisfy our class package requirements. But in order to merge several classes that are not our parents but grand parents we will need to modify classes that may be used somewhere else (with another set of requirements). We cannot do this. So instead we build translation table that will tell which ancestor class need to be replaced with which so that we minimize number of versions used for single class (or technically packages since version is a package attribute). For translation table to work there should be a method that returns all class virtual ancestors so that everybody will see them instead of accessing class parents directly and getting declared ancestors. """ result = {} aggregation = { self.package.name: {( self.package, semantic_version.Spec('==' + str(self.package.version)) )} } for cls, parent in helpers.traverse( ((self, parent) for parent in self._parents), lambda cp: ((cp[1], anc) for anc in cp[1].parents)): if cls.package != parent.package: requirement = cls.package.requirements[parent.package.name] aggregation.setdefault(parent.package.name, set()).add( (parent.package, requirement)) package_bindings = {} for versions in aggregation.values(): mappings = self._remap_package(versions) package_bindings.update(mappings) for cls in helpers.traverse(self.parents, lambda c: c.parents): if cls.package in package_bindings: package2 = package_bindings[cls.package] cls2 = package2.classes[cls.name] result[cls] = cls2 return result @staticmethod def _remap_package(versions): result = {} reverse_mappings = {} versions_list = sorted(versions, key=lambda x: x[0].version) i = 0 while i < len(versions_list): package1, requirement1 = versions_list[i] dst_package = None for j, (package2, _) in enumerate(versions_list): if i == j: continue if package2.version in requirement1 and ( dst_package is None or dst_package.version < package2.version): dst_package = package2 if dst_package: result[package1] = dst_package reverse_mappings.setdefault(dst_package, []).append(package1) for package in reverse_mappings.get(package1, []): result[package] = dst_package del versions_list[i] else: i += 1 return result def ancestors(self): for c in helpers.traverse(self, lambda t: t.parents): if c is not self: yield c @property def context(self): if not self._context: ctx = None for imp in reversed(self._imports): if ctx is None: ctx = imp.exported_context else: ctx = helpers.link_contexts(ctx, imp.exported_context) if ctx is None: self._context = yaql_integration.create_empty_context() else: self._context = ctx.create_child_context() for m in self._iterate_unique_methods(): if m.instance_stub: self._context.register_function( m.instance_stub, name=m.instance_stub.name) if m.static_stub: self._context.register_function( m.static_stub, name=m.static_stub.name) return self._context @property def exported_context(self): if not self._exported_context: self._exported_context = yaql_integration.create_empty_context() for m in self._iterate_unique_methods(): if m.usage == dsl_types.MethodUsages.Extension: if m.instance_stub: self._exported_context.register_function( m.instance_stub, name=m.instance_stub.name) if m.static_stub: self._exported_context.register_function( m.static_stub, name=m.static_stub.name) return self._exported_context def get_meta(self, context): if self._meta_values is None: executor = helpers.get_executor() context = executor.create_type_context( self, caller_context=context) self._meta_values = dslmeta.merge_providers( self, lambda cls: cls._meta, context) return self._meta_values class MuranoMetaClass(dsl_types.MuranoMetaClass, MuranoClass): _allowed_usages = {dsl_types.ClassUsages.Meta, dsl_types.ClassUsages.Class} def __init__(self, ns_resolver, name, package, parents, meta=None, imports=None): super(MuranoMetaClass, self).__init__( ns_resolver, name, package, parents, meta, imports) self._cardinality = dsl_types.MetaCardinality.One self._targets = list(dsl_types.MetaCardinality.All) self._inherited = False @property def usage(self): return dsl_types.ClassUsages.Meta @property def cardinality(self): return self._cardinality @cardinality.setter def cardinality(self, value): self._cardinality = value @property def targets(self): return self._targets @targets.setter def targets(self, value): self._targets = value @property def inherited(self): return self._inherited @inherited.setter def inherited(self, value): self._inherited = value def __repr__(self): return 'MuranoMetaClass({0}/{1})'.format(self.name, self.version) def create(data, package, name, ns_resolver): usage = data.get('Usage', dsl_types.ClassUsages.Class) if usage == dsl_types.ClassUsages.Class: return _create_class(MuranoClass, name, ns_resolver, data, package) elif usage == dsl_types.ClassUsages.Meta: return _create_meta_class( MuranoMetaClass, name, ns_resolver, data, package) else: raise ValueError(u'Invalid type Usage: "{}"'.format(usage)) def _create_class(cls, name, ns_resolver, data, package, *args, **kwargs): parent_class_names = data.get('Extends') parent_classes = [] if parent_class_names: if not utils.is_sequence(parent_class_names): parent_class_names = [parent_class_names] for parent_name in parent_class_names: full_name = ns_resolver.resolve_name(str(parent_name)) parent_classes.append(package.find_class(full_name)) type_obj = cls( ns_resolver, name, package, parent_classes, data.get('Meta'), data.get('Import'), *args, **kwargs) properties = data.get('Properties') or {} for property_name, property_spec in properties.items(): spec = murano_property.MuranoProperty( type_obj, property_name, property_spec) type_obj.add_property(spec) methods = data.get('Methods') or data.get('Workflow') or {} method_mappings = { 'initialize': '.init', 'destroy': '.destroy' } for method_name, payload in methods.items(): type_obj.add_method( method_mappings.get(method_name, method_name), payload) return type_obj def _create_meta_class(cls, name, ns_resolver, data, package, *args, **kwargs): cardinality = data.get('Cardinality', dsl_types.MetaCardinality.One) if cardinality not in dsl_types.MetaCardinality.All: raise ValueError(u'Invalid MetaClass Cardinality "{}"'.format( cardinality)) applies_to = data.get('Applies', dsl_types.MetaTargets.All) if isinstance(applies_to, str): applies_to = [applies_to] if isinstance(applies_to, list): applies_to = set(applies_to) delta = applies_to - dsl_types.MetaTargets.All - {'All'} if delta: raise ValueError(u'Invalid MetaClass target(s) {}:'.format( ', '.join(map(u'"{}"'.format, delta))) ) if 'All' in applies_to: applies_to = dsl_types.MetaTargets.All inherited = data.get('Inherited', False) if not isinstance(inherited, bool): raise ValueError('Invalid Inherited value. Must be true or false') meta_cls = _create_class( cls, name, ns_resolver, data, package, *args, **kwargs) meta_cls.targets = list(applies_to) meta_cls.cardinality = cardinality meta_cls.inherited = inherited return meta_cls def weigh_type_hierarchy(cls): """Weighs classes in type hierarchy by their distance from the root :param cls: root of hierarchy :return: dictionary that has class name as keys and distance from the root a values. Root class has always a distance of 0. If the class (or different versions of that class) is achievable through several paths the shortest distance is used. """ result = {} for c, w in helpers.traverse( [(cls, 0)], lambda t: map( lambda p: (p, t[1] + 1), t[0].parents)): result.setdefault(c.name, w) return result ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/dsl/namespace_resolver.py0000664000175000017500000000444300000000000021441 0ustar00zuulzuul00000000000000# 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 TYPE_NAME_RE = re.compile(r'^([a-zA-Z_]\w*:|:)?[a-zA-Z_]\w*(\.[a-zA-Z_]\w*)*$') NS_RE = re.compile(r'^([a-zA-Z_]\w*(\.[a-zA-Z_]\w*)*)?$') PREFIX_RE = re.compile(r'^([a-zA-Z_]\w*|=)$') class NamespaceResolver(object): def __init__(self, namespaces): if namespaces is None: namespaces = {} for prefix, ns in namespaces.items(): if ns is None: ns = '' if PREFIX_RE.match(prefix) is None: raise ValueError( 'Invalid namespace prefix "{0}"'.format(prefix)) if NS_RE.match(ns) is None: raise ValueError('Invalid namespace "{0}"'.format(ns)) self._namespaces = namespaces.copy() self._namespaces.setdefault('=', '') self._namespaces[''] = '' def resolve_name(self, name): if not self.is_typename(name, True): raise ValueError('Invalid type name "{0}"'.format(name)) name = str(name) if ':' not in name: if '.' in name: parts = ['', name] else: parts = ['=', name] else: parts = name.split(':') if not parts[0]: parts[0] = '=' if parts[0] not in self._namespaces: raise KeyError('Unknown namespace prefix ' + parts[0]) ns = self._namespaces[parts[0]] if not ns: return parts[1] return '.'.join((ns, parts[1])) @staticmethod def is_typename(name, relaxed): if not name: return False name = str(name) if not relaxed and ':' not in name: return False return TYPE_NAME_RE.match(name) is not None ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/dsl/object_store.py0000664000175000017500000002532400000000000020247 0ustar00zuulzuul00000000000000# 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 gc import weakref from oslo_log import log as logging from murano.dsl import dsl_types from murano.dsl import helpers from murano.dsl import murano_object LOG = logging.getLogger(__name__) class ObjectStore(object): def __init__(self, executor, parent_store=None, weak_store=True): self._parent_store = parent_store self._store = weakref.WeakValueDictionary() if weak_store else {} self._designer_attributes_store = {} self._executor = weakref.ref(executor) self._pending_destruction = set() @property def executor(self): return self._executor() def get(self, object_id, check_parent_store=True): result = self._store.get(object_id) if not result and self._parent_store and check_parent_store: return self._parent_store.get(object_id, check_parent_store) return result def has(self, object_id, check_parent_store=True): if object_id in self._store: return True if check_parent_store and self._parent_store: return self._parent_store.has(object_id, check_parent_store) return False def put(self, murano_object, object_id=None): self._store[object_id or murano_object.object_id] = murano_object def schedule_object_destruction(self, murano_object): self._pending_destruction.add(murano_object) self._store[murano_object.object_id] = murano_object def iterate(self): return self._store.keys() def remove(self, object_id): self._store.pop(object_id) def load(self, value, owner, default_type=None, scope_type=None, context=None, keep_ids=False, bypass_store=False): # do the object model load in a temporary object store and copy # loaded objects here after that model_store = InitializationObjectStore( owner, self, keep_ids) with helpers.with_object_store(model_store): result = model_store.load( value, owner, scope_type=scope_type, default_type=default_type, context=context) for obj_id in model_store.iterate(): obj = model_store.get(obj_id) if obj.initialized: if not bypass_store: self.put(obj) return result @staticmethod def _get_designer_attributes(header): return dict((k, v) for k, v in header.items() if str(k).startswith('_')) def designer_attributes(self, object_id): return self._designer_attributes_store.get(object_id, {}) @property def initializing(self): return False @property def parent_store(self): return self._parent_store def cleanup(self): LOG.debug('Cleaning up orphan objects') with helpers.with_object_store(self): n = self._collect_garbage() LOG.debug('{} orphan objects were destroyed'.format(n)) return n def prepare_finalize(self, used_objects): used_objects = set(used_objects) if used_objects else [] sentenced_objects = [ obj for obj in self._store.values() if obj not in used_objects ] with helpers.with_object_store(self): if sentenced_objects: self._pending_destruction.update(sentenced_objects) for __ in self._destroy_garbage(sentenced_objects): pass def finalize(self): with helpers.with_object_store(self): for t in self._store.values(): t.mark_destroyed(True) self._pending_destruction.clear() self._store.clear() def _collect_garbage(self): repeat = True count = 0 while repeat: repeat = False gc.collect() for obj in gc.garbage: if (isinstance(obj, murano_object.RecyclableMuranoObject) and obj.executor is self._executor()): repeat = True if obj.initialized and not obj.destroyed: self.schedule_object_destruction(obj) else: obj.mark_destroyed(True) obj = None del gc.garbage[:] if self._pending_destruction: for obj in self._destroy_garbage(self._pending_destruction): if obj in self._pending_destruction: repeat = True obj.mark_destroyed() self._pending_destruction.remove(obj) count += 1 return count def is_doomed(self, obj): return obj.destroyed or obj in self._pending_destruction def _destroy_garbage(self, sentenced_objects): dd_graph = {} # NOTE(starodubcevna): construct a graph which looks like: # { # obj1: [subscriber1, subscriber2], # obj2: [subscriber2, subscriber3] # } for obj in sentenced_objects: obj_subscribers = [obj.owner] for dd in obj.destruction_dependencies: subscriber = dd['subscriber'] if subscriber: subscriber = subscriber() if subscriber and subscriber not in obj_subscribers: obj_subscribers.append(subscriber) dd_graph[obj] = obj_subscribers def topological(graph): """Topological sort implementation This implementation will work even if we have cycle dependencies, e.g. [a->b, b->c, c->a]. In this case the order of deletion will be undefined and it's okay. """ visited = collections.defaultdict(int) indexes = collections.defaultdict(int) def dfs(obj): visited[obj] += 1 subscribers = graph.get(obj) if subscribers is not None: m = 0 for i, subscriber in enumerate(subscribers): if i == 0 or not visited[subscriber]: for t in dfs(subscriber): yield t m = max(m, indexes[subscriber]) if visited.get(obj, 0) <= 2: visited[obj] += 1 indexes[obj] = m + 1 yield obj, m + 1 for i, obj in enumerate(graph.keys()): if not visited[obj]: for t in dfs(obj): yield t visited.clear() indexes.clear() order = collections.defaultdict(list) for obj, index in topological(dd_graph): order[index].append(obj) for key in sorted(order): group = order[key] self.executor.signal_destruction_dependencies(*group) for key in sorted(order, reverse=True): group = order[key] self.executor.destroy_objects(*group) for t in group: yield t # Temporary ObjectStore to load object graphs. Does 2-phase load # and maintains internal state on what phase is currently running # as well as objects that are in the middle of initialization. # Required in order to isolate semi-initialized objects from regular # objects in main ObjectStore and internal state between graph loads # in different threads. Once the load is done all objects are copied # to the parent ObjectStore class InitializationObjectStore(ObjectStore): def __init__(self, root_owner, parent_store, keep_ids): super(InitializationObjectStore, self).__init__( parent_store.executor, parent_store, weak_store=False) self._initializing = False self._root_owner = root_owner self._keep_ids = keep_ids self._initializers = [] @property def initializing(self): return self._initializing def load(self, value, owner, default_type=None, scope_type=None, context=None, **kwargs): parsed = helpers.parse_object_definition(value, scope_type, context) if not parsed: raise ValueError('Invalid object representation format') if owner is self._root_owner: self._initializing = True class_obj = parsed['type'] or default_type if not class_obj: raise ValueError( 'Invalid object representation: ' 'no type information was provided') if isinstance(class_obj, dsl_types.MuranoTypeReference): class_obj = class_obj.type object_id = parsed['id'] is_tmp_object = (object_id is None and owner is not self._root_owner and self._initializing) obj = None if object_id is None else self.get( object_id, self._keep_ids) if not obj: if is_tmp_object or helpers.is_objects_dry_run_mode(): mo_type = murano_object.MuranoObject else: mo_type = murano_object.RecyclableMuranoObject obj = mo_type( class_obj, owner, name=parsed['name'], metadata=parsed['metadata'], object_id=object_id if self._keep_ids else None) obj.load_dependencies(parsed['dependencies']) if parsed['destroyed']: obj.mark_destroyed() self.put(obj, object_id or obj.object_id) system_value = ObjectStore._get_designer_attributes( parsed['extra']) self._designer_attributes_store[object_id] = system_value if context is None: context = self.executor.create_object_context(obj) def run_initialize(): self._initializers.extend( obj.initialize(context, parsed['properties'])) run_initialize() if owner is self._root_owner: self._initializing = False run_initialize() if owner is self._root_owner: with helpers.with_object_store(self.parent_store): for fn in self._initializers: fn() return obj ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/dsl/package_loader.py0000664000175000017500000000225000000000000020477 0ustar00zuulzuul00000000000000# 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 abc class MuranoPackageLoader(object, metaclass=abc.ABCMeta): @abc.abstractmethod def load_package(self, package_name, version_spec): pass @abc.abstractmethod def load_class_package(self, class_name, version_spec): pass @abc.abstractmethod def register_package(self, package): pass @abc.abstractmethod def import_fixation_table(self, fixations): pass @abc.abstractmethod def export_fixation_table(self): pass @abc.abstractmethod def compact_fixation_table(self): pass ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1696417899.8011808 murano-16.0.0/murano/dsl/principal_objects/0000775000175000017500000000000000000000000020677 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/dsl/principal_objects/__init__.py0000664000175000017500000000205400000000000023011 0ustar00zuulzuul00000000000000# 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 murano.dsl.principal_objects import exception from murano.dsl.principal_objects import garbage_collector from murano.dsl.principal_objects import stack_trace from murano.dsl.principal_objects import sys_object def register(package): package.register_class(sys_object.SysObject) package.register_class(stack_trace.StackTrace) package.register_class(exception.DslException) package.register_class(garbage_collector.GarbageCollector) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/dsl/principal_objects/exception.py0000664000175000017500000000142600000000000023252 0ustar00zuulzuul00000000000000# 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 murano.dsl import dsl @dsl.name('io.murano.Exception') class DslException(object): def to_string(self): return self.get_property('nativeException').format() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/dsl/principal_objects/garbage_collector.py0000664000175000017500000000612000000000000024706 0ustar00zuulzuul00000000000000# 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 yaql.language import specs from yaql.language import yaqltypes from murano.dsl import dsl from murano.dsl import helpers @dsl.name('io.murano.system.GC') class GarbageCollector(object): @staticmethod @specs.parameter('publisher', dsl.MuranoObjectParameter(decorate=False)) @specs.parameter('subscriber', dsl.MuranoObjectParameter(decorate=False)) @specs.parameter('handler', yaqltypes.String(nullable=True)) def subscribe_destruction(publisher, subscriber, handler=None): publisher_this = publisher.real_this subscriber_this = subscriber.real_this if handler: subscriber.type.find_single_method(handler) dependency = GarbageCollector._find_dependency( publisher_this, subscriber_this, handler) if not dependency: dependency = {'subscriber': helpers.weak_ref(subscriber_this), 'handler': handler} publisher_this.destruction_dependencies.append(dependency) @staticmethod @specs.parameter('publisher', dsl.MuranoObjectParameter(decorate=False)) @specs.parameter('subscriber', dsl.MuranoObjectParameter(decorate=False)) @specs.parameter('handler', yaqltypes.String(nullable=True)) def unsubscribe_destruction(publisher, subscriber, handler=None): publisher_this = publisher.real_this subscriber_this = subscriber.real_this if handler: subscriber.type.find_single_method(handler) dds = publisher_this.destruction_dependencies dependency = GarbageCollector._find_dependency( publisher_this, subscriber_this, handler) if dependency: dds.remove(dependency) @staticmethod def _find_dependency(publisher, subscriber, handler): dds = publisher.destruction_dependencies for dd in dds: if dd['handler'] != handler: continue d_subscriber = dd['subscriber'] if d_subscriber: d_subscriber = d_subscriber() if d_subscriber == subscriber: return dd @staticmethod def collect(): helpers.get_executor().object_store.cleanup() @staticmethod @specs.parameter('object_', dsl.MuranoObjectParameter(decorate=False)) def is_doomed(object_): return helpers.get_object_store().is_doomed(object_) @staticmethod @specs.parameter('object_', dsl.MuranoObjectParameter(decorate=False)) def is_destroyed(object_): return object_.destroyed ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/dsl/principal_objects/stack_trace.py0000664000175000017500000000706100000000000023540 0ustar00zuulzuul00000000000000# 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 inspect import os.path from yaql import specs from murano.dsl import constants from murano.dsl import dsl from murano.dsl import dsl_types from murano.dsl import helpers from murano.dsl import yaql_integration @dsl.name('io.murano.StackTrace') class StackTrace(object): def __init__(self, this, context, include_native_frames=True): frames = [] caller_context = context while True: if not caller_context: break frame = compose_stack_frame(caller_context) frames.append(frame) caller_context = helpers.get_caller_context(caller_context) frames.reverse() frames.pop() if include_native_frames: native_frames = [] for frame in inspect.trace()[1:]: location = dsl_types.ExpressionFilePosition( os.path.abspath(frame[1]), frame[2], -1, frame[2], -1) method = frame[3] native_frames.append({ 'instruction': frame[4][0].strip(), 'location': location, 'methodName': method, 'typeName': None }) frames.extend(native_frames) this.properties.frames = frames @specs.meta(constants.META_NO_TRACE, True) @specs.meta('Usage', 'Action') def to_string(self, this, prefix=''): return '\n'.join([format_frame(t, prefix)for t in this['frames']]) def compose_stack_frame(context): instruction = helpers.get_current_instruction(context) method = helpers.get_current_method(context) return { 'instruction': None if instruction is None else str(instruction), 'location': None if instruction is None else instruction.source_file_position, 'methodName': None if method is None else method.name, 'typeName': None if method is None else method.declaring_type.name } def format_frame(frame, prefix=''): instruction = frame['instruction'] method_name = frame['methodName'] type_name = frame['typeName'] location = frame['location'] if type_name: method_name += ' of type ' + type_name if location: args = ( os.path.abspath(location.file_path), location.start_line, ':' + str(location.start_column) if location.start_column >= 0 else '', method_name, instruction, prefix ) return (u'{5}File "{0}", line {1}{2} in method {3}\n' u'{5} {4}').format(*args) else: return u'{2}File in method {0}\n{2} {1}'.format( method_name, instruction, prefix) def create_stack_trace(context, include_native_frames=True): stacktrace = yaql_integration.call_func( context, 'new', 'io.murano.StackTrace', includeNativeFrames=include_native_frames) return dsl.MuranoObjectInterface.create(stacktrace) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/dsl/principal_objects/sys_object.py0000664000175000017500000000276100000000000023423 0ustar00zuulzuul00000000000000# 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 yaql import specs from murano.dsl import dsl from murano.dsl import helpers @dsl.name('io.murano.Object') class SysObject(object): @specs.parameter('owner', dsl.MuranoTypeParameter(nullable=True)) def set_attr(self, this, context, name, value, owner=None): if owner is None: owner = helpers.get_type(helpers.get_caller_context(context)) attribute_store = helpers.get_attribute_store() attribute_store.set(this.object, owner, name, value) @specs.parameter('owner', dsl.MuranoTypeParameter(nullable=True)) def get_attr(self, this, context, name, default=None, owner=None): if owner is None: owner = helpers.get_type(helpers.get_caller_context(context)) attribute_store = helpers.get_attribute_store() result = attribute_store.get(this.object, owner, name) return default if result is None else result ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/dsl/reflection.py0000664000175000017500000001637700000000000017727 0ustar00zuulzuul00000000000000# 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 semantic_version from yaql.language import specs from yaql import yaqlization from murano.dsl import dsl from murano.dsl import dsl_types from murano.dsl import helpers from murano.dsl import meta @specs.yaql_property(dsl_types.MuranoType) @specs.name('name') def type_name(murano_type): return murano_type.name @specs.yaql_property(dsl_types.MuranoType) @specs.name('usage') def type_usage(murano_type): return murano_type.usage @specs.yaql_property(dsl_types.MuranoClass) def methods(murano_class): all_method_names = murano_class.all_method_names return tuple( murano_method for name in all_method_names if not name.startswith('__') and not name.startswith('.') for murano_method in murano_class.find_method(name) ) @specs.yaql_property(dsl_types.MuranoClass) def properties(murano_class): all_property_names = murano_class.all_property_names return tuple( prop for prop_name in all_property_names if not prop_name.startswith('__') and not prop_name.startswith('.') for prop in murano_class.find_property(prop_name) ) @specs.yaql_property(dsl_types.MuranoClass) def ancestors(murano_class): return tuple(murano_class.ancestors()) @specs.yaql_property(dsl_types.MuranoType) def package(murano_type): return murano_type.package @specs.yaql_property(dsl_types.MuranoClass) @specs.name('version') def type_version(murano_type): return murano_type.version @specs.yaql_property(dsl_types.MuranoProperty) @specs.name('name') def property_name(murano_property): return murano_property.name # TODO(ativelkov): add 'default' to return some wrapped YAQL expression # @specs.yaql_property(dsl_types.MuranoProperty) # @specs.name('default') # def property_default(murano_property): # return murano_property.default @specs.yaql_property(dsl_types.MuranoProperty) @specs.name('has_default') def property_has_default(murano_property): return murano_property.has_default @specs.yaql_property(dsl_types.MuranoProperty) @specs.name('usage') def property_usage(murano_property): return murano_property.usage @specs.yaql_property(dsl_types.MuranoProperty) @specs.name('declaring_type') def property_owner(murano_property): return murano_property.declaring_type @specs.name('get_value') @specs.parameter('property_', dsl_types.MuranoProperty) @specs.parameter('object_', dsl.MuranoObjectParameter( nullable=True, decorate=False)) @specs.method def property_get_value(context, property_, object_): if object_ is None: return helpers.get_executor().get_static_property( property_.declaring_type, name=property_.name, context=context) return object_.cast(property_.declaring_type).get_property( name=property_.name, context=context) @specs.name('set_value') @specs.parameter('property_', dsl_types.MuranoProperty) @specs.parameter('object_', dsl.MuranoObjectParameter( nullable=True, decorate=False)) @specs.method def property_set_value(context, property_, object_, value): if object_ is None: helpers.get_executor().set_static_property( property_.declaring_type, name=property_.name, value=value, context=context) else: object_.cast(property_.declaring_type).set_property( name=property_.name, value=value, context=context) @specs.yaql_property(dsl_types.MuranoMethod) @specs.name('name') def method_name(murano_method): return murano_method.name @specs.yaql_property(dsl_types.MuranoMethod) def arguments(murano_method): if murano_method.arguments_scheme is None: return None return tuple(murano_method.arguments_scheme.values()) @specs.yaql_property(dsl_types.MuranoMethod) @specs.name('declaring_type') def method_owner(murano_method): return murano_method.declaring_type @specs.parameter('method', dsl_types.MuranoMethod) @specs.parameter('__object', dsl.MuranoObjectParameter(nullable=True)) @specs.name('invoke') @specs.method def method_invoke(context, method, __object, *args, **kwargs): return method.invoke(__object, args, kwargs, context) @specs.yaql_property(dsl_types.MuranoPackage) def types(murano_package): return tuple( murano_package.find_class(cls, False) for cls in murano_package.classes ) @specs.yaql_property(dsl_types.MuranoPackage) @specs.name('name') def package_name(murano_package): return murano_package.name @specs.yaql_property(dsl_types.MuranoPackage) @specs.name('version') def package_version(murano_package): return murano_package.version @specs.yaql_property(dsl_types.MuranoMethodArgument) @specs.name('name') def argument_name(method_argument): return method_argument.name # TODO(ativelkov): add 'default' to return some wrapped YAQL expression # @specs.yaql_property(dsl_types.MuranoMethodArgument) # @specs.name('default') # def argument_default(method_argument): # return method_argument.default @specs.yaql_property(dsl_types.MuranoMethodArgument) @specs.name('has_default') def argument_has_default(method_argument): return method_argument.has_default @specs.yaql_property(dsl_types.MuranoMethodArgument) @specs.name('usage') def argument_usage(method_argument): return method_argument.usage @specs.yaql_property(dsl_types.MuranoMethodArgument) @specs.name('declaring_method') def argument_owner(method_argument): return method_argument.murano_method @specs.yaql_property(dsl_types.MuranoType) @specs.name('type') def type_to_type_ref(murano_type): return murano_type.get_reference() @specs.parameter('provider', meta.MetaProvider) @specs.name('#property#meta') def get_meta(context, provider): return provider.get_meta(context) @specs.yaql_property(dsl_types.MuranoMetaClass) def cardinality(murano_meta_class): return murano_meta_class.cardinality @specs.yaql_property(dsl_types.MuranoMetaClass) def targets(murano_meta_class): return murano_meta_class.targets @specs.yaql_property(dsl_types.MuranoMetaClass) def inherited(murano_meta_class): return murano_meta_class.inherited def register(context): funcs = ( type_name, type_usage, type_version, type_to_type_ref, methods, properties, ancestors, package, property_name, property_has_default, property_owner, property_usage, property_get_value, property_set_value, method_name, arguments, method_owner, method_invoke, types, package_name, package_version, argument_name, argument_has_default, argument_owner, argument_usage, cardinality, targets, inherited, get_meta ) for f in funcs: context.register_function(f) yaqlization.yaqlize(semantic_version.Version, whitelist=[ 'major', 'minor', 'patch', 'prerelease', 'build' ]) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/dsl/schema_generator.py0000664000175000017500000002411300000000000021066 0ustar00zuulzuul00000000000000# 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 murano.dsl.contracts import contracts from murano.dsl import dsl_types from murano.dsl import executor from murano.dsl import helpers from murano.dsl import meta as meta_module from murano.dsl import murano_type def generate_schema(pkg_loader, context_manager, class_name, method_names=None, class_version=None, package_name=None): """Generate JSON schema JSON Schema is generated either for the class with all model builders or for specified model builders only. The return value is a dictionary with keys being model builder names and the values are JSON schemas for them. The class itself is represented by an empty string key. """ if method_names and not isinstance(method_names, (list, tuple)): method_names = (method_names,) version = helpers.parse_version_spec(class_version) if package_name: package = pkg_loader.load_package(package_name, version) else: package = pkg_loader.load_class_package(class_name, version) cls = package.find_class(class_name, search_requirements=False) exc = executor.MuranoDslExecutor(pkg_loader, context_manager) with helpers.with_object_store(exc.object_store): context = exc.create_object_context(cls) model_builders = set(list_model_builders(cls, context)) method_names = model_builders.intersection( method_names or model_builders) result = { name: generate_entity_schema( get_entity(cls, name), context, cls, get_meta(cls, name, context)) for name in method_names } return result def list_model_builders(cls, context): """List model builder names of the class Yield names of all model builders (static actions marked with appropriate metadata) plus empty string for the class itself. """ yield '' for method_name in cls.all_method_names: try: method = cls.find_single_method(method_name) if not method.is_action or not method.is_static: continue meta = meta_module.aggregate_meta(method, context) is_builder = meta.get('io.murano.metadata.ModelBuilder') if is_builder and is_builder.get_property('enabled'): yield method.name except Exception: pass def get_meta(cls, method_name, context): """Get metadata dictionary for the method or class""" if not method_name: return meta_module.aggregate_meta(cls, context) method = cls.find_single_method(method_name) return meta_module.aggregate_meta(method, context) def get_entity(cls, method_name): """Get MuranoMethod of the class by its name""" if not method_name: return cls method = cls.find_single_method(method_name) return method def get_properties(entity): """Get properties/arg scheme of the class/method""" if isinstance(entity, dsl_types.MuranoType): properties = entity.all_property_names result = {} for prop_name in properties: prop = entity.find_single_property(prop_name) if prop.usage not in (dsl_types.PropertyUsages.In, dsl_types.PropertyUsages.InOut): continue result[prop_name] = prop return result return entity.arguments_scheme def generate_entity_schema(entity, context, declaring_type, meta): """Generate schema for single class or method by it DSL entity""" properties = get_properties(entity) type_weights = murano_type.weigh_type_hierarchy(declaring_type) schema = { '$schema': 'http://json-schema.org/draft-04/schema#', 'type': 'object', 'properties': { name: generate_property_schema(prop, context, type_weights) for name, prop in properties.items() }, 'additionalProperties': False, 'formSections': generate_sections(meta, type_weights) } schema.update(generate_ui_hints(entity, context, type_weights)) return schema def generate_sections(meta, type_weights): """Builds sections definitions for the schema Sections are UI hint for UI for grouping inputs into tabs/group-boxes. The code collects section definitions from type hierarchy considering that the section might be redefined in ancestor with the different section index and then re-enumerates them in a way that sections from the most base classes in hierarchy will get lower index values and there be no two sections with the same index. """ section_list = meta.get('io.murano.metadata.forms.Section', []) sections_map = {} for section in section_list: name = section.get_property('name') ex_section = sections_map.get(name) if not ex_section: pass elif (type_weights[ex_section.declaring_type.name] < type_weights[section.declaring_type.name]): continue elif (type_weights[ex_section.declaring_type.name] == type_weights[section.declaring_type.name]): index = section.get_property('index') if index is None: continue ex_index = ex_section.get_property('index') if ex_index is not None and ex_index <= index: continue sections_map[name] = section ordered_sections, unordered_sections = sort_by_index( sections_map.values(), type_weights) sections = {} index = 0 for section in ordered_sections: name = section.get_property('name') if name not in sections: sections[name] = { 'title': section.get_property('title'), 'index': index } index += 1 for section in unordered_sections: name = section.get_property('name') if name not in sections: sections[name] = { 'title': section.get_property('title') } return sections def generate_property_schema(prop, context, type_weights): """Generate schema for single property/argument""" schema = translate(prop.contract.spec, context, prop.declaring_type.package.runtime_version) if prop.has_default: schema['default'] = prop.default schema.update(generate_ui_hints(prop, context, type_weights)) return schema def generate_ui_hints(entity, context, type_weights): """Translate know property/arg meta into json-schema UI hints""" schema = {} meta = meta_module.aggregate_meta(entity, context) for cls_name, schema_prop, meta_prop in ( ('io.murano.metadata.Title', 'title', 'text'), ('io.murano.metadata.Description', 'description', 'text'), ('io.murano.metadata.HelpText', 'helpText', 'text'), ('io.murano.metadata.forms.Hidden', 'visible', 'visible')): value = meta.get(cls_name) if value is not None: schema[schema_prop] = value.get_property(meta_prop) position = meta.get('io.murano.metadata.forms.Position') if position: schema['formSection'] = position.get_property('section') index = position.get_property('index') if index is not None: schema['formIndex'] = ( (position.get_property('index') + 1) * len(type_weights) - type_weights[position.declaring_type.name]) return schema def sort_by_index(meta, type_weights, property_name='index'): """Sorts meta definitions by its distance in the class hierarchy""" has_index = filter( lambda m: m.get_property(property_name) is not None, meta) has_no_index = filter( lambda m: m.get_property(property_name) is None, meta) return ( sorted(has_index, key=lambda m: ( (m.get_property(property_name) + 1) * len(type_weights) - type_weights[m.declaring_type.name])), has_no_index) def translate(contract, context, runtime_version): """Translates contracts into json-schema equivalents""" if isinstance(contract, dict): return translate_dict(contract, context, runtime_version) elif isinstance(contract, list): return translate_list(contract, context, runtime_version) elif isinstance(contract, dsl_types.YaqlExpression): return contracts.Contract.generate_expression_schema( contract, context, runtime_version) def translate_dict(contract, context, runtime_version): """Translates dictionary contracts into json-schema objects""" properties = {} additional_properties = False for key, value in contract.items(): if isinstance(key, dsl_types.YaqlExpression): additional_properties = translate(value, context, runtime_version) else: properties[key] = translate(value, context, runtime_version) return { 'type': 'object', 'properties': properties, 'additionalProperties': additional_properties } def translate_list(contract, context, runtime_version): """Translates list contracts into json-schema arrays""" items = [] for value in contract: if isinstance(value, int): pass else: items.append(translate(value, context, runtime_version)) if len(items) == 0: return {'type': 'array'} elif len(items) == 1: return { 'type': 'array', 'items': items[0], } else: return { 'type': 'array', 'items': items, 'additionalItems': items[-1] } ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/dsl/serializer.py0000664000175000017500000002415600000000000017740 0ustar00zuulzuul00000000000000# 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 yaql import utils from murano.dsl import dsl from murano.dsl import dsl_types from murano.dsl import helpers class ObjRef(object): def __init__(self, obj): self.ref_obj = obj def serialize(obj, executor, serialization_type=dsl_types.DumpTypes.Serializable, allow_refs=True): with helpers.with_object_store(executor.object_store): return serialize_model( obj, executor, allow_refs, make_copy=False, serialize_attributes=False, serialize_actions=False, serialization_type=serialization_type, with_destruction_dependencies=False)['Objects'] def _serialize_object(root_object, designer_attributes, allow_refs, executor, serialize_actions=True, serialization_type=dsl_types.DumpTypes.Serializable, with_destruction_dependencies=True): serialized_objects = set() obj = root_object if isinstance(obj, dsl.MuranoObjectInterface): obj = obj.object parent = obj.owner if isinstance(obj, dsl_types.MuranoObject) else None while True: obj, need_another_pass = _pass12_serialize( obj, parent, serialized_objects, designer_attributes, executor, serialize_actions, serialization_type, allow_refs, with_destruction_dependencies) if not need_another_pass: break tree = [obj] _pass3_serialize(tree, serialized_objects, allow_refs) return tree[0], serialized_objects def serialize_model(root_object, executor, allow_refs=False, make_copy=True, serialize_attributes=True, serialize_actions=True, serialization_type=dsl_types.DumpTypes.Serializable, with_destruction_dependencies=True): designer_attributes = executor.object_store.designer_attributes if root_object is None: tree = None tree_copy = None attributes = [] else: with helpers.with_object_store(executor.object_store): tree, serialized_objects = _serialize_object( root_object, designer_attributes, allow_refs, executor, serialize_actions, serialization_type, with_destruction_dependencies) tree_copy = _serialize_object( root_object, None, allow_refs, executor, serialize_actions, serialization_type, with_destruction_dependencies)[0] if make_copy else None attributes = executor.attribute_store.serialize( serialized_objects) if serialize_attributes else None return { 'Objects': tree, 'ObjectsCopy': tree_copy, 'Attributes': attributes } def _serialize_available_action(obj, current_actions, executor): result = {} actions = obj.type.find_methods(lambda m: m.is_action) for action in actions: action_id = '{0}_{1}'.format(obj.object_id, action.name) entry = current_actions.get(action_id, {'enabled': True}) entry['name'] = action.name context = executor.create_type_context(action.declaring_type) meta = action.get_meta(context) meta_dict = {item.type.name: item for item in meta} title = meta_dict.get('io.murano.metadata.Title') if title: entry['title'] = title.get_property('text') else: entry['title'] = action.name description = meta_dict.get('io.murano.metadata.Description') if description: entry['description'] = description.get_property('text') help_text = meta_dict.get('io.murano.metadata.HelpText') if help_text: entry['helpText'] = help_text.get_property('text') result[action_id] = entry return result def _pass12_serialize(value, parent, serialized_objects, designer_attributes_getter, executor, serialize_actions, serialization_type, allow_refs, with_destruction_dependencies): if isinstance(value, dsl.MuranoObjectInterface): value = value.object if isinstance(value, (str, int, float, bool)) or value is None: return value, False if isinstance(value, dsl_types.MuranoObject): if value.owner is not parent or value.object_id in serialized_objects: return ObjRef(value), True elif isinstance(value, ObjRef): can_move = value.ref_obj.object_id not in serialized_objects if can_move and allow_refs and value.ref_obj.owner is not None: can_move = (is_nested_in(parent, value.ref_obj.owner) and value.ref_obj.owner.object_id in serialized_objects) if can_move: value = value.ref_obj else: return value, False if isinstance(value, (dsl_types.MuranoType, dsl_types.MuranoTypeReference)): return helpers.format_type_string(value), False if helpers.is_passkey(value): return value, False if isinstance(value, dsl_types.MuranoObject): result = value.to_dictionary( serialization_type=serialization_type, allow_refs=allow_refs, with_destruction_dependencies=with_destruction_dependencies) if designer_attributes_getter is not None: if serialization_type == dsl_types.DumpTypes.Inline: system_data = result else: system_data = result['?'] system_data.update(designer_attributes_getter(value.object_id)) if serialize_actions: # deserialize and merge list of actions system_data['_actions'] = _serialize_available_action( value, system_data.get('_actions', {}), executor) serialized_objects.add(value.object_id) return _pass12_serialize( result, value, serialized_objects, designer_attributes_getter, executor, serialize_actions, serialization_type, allow_refs, with_destruction_dependencies) elif isinstance(value, utils.MappingType): result = {} need_another_pass = False for d_key, d_value in value.items(): if (isinstance(d_key, dsl_types.MuranoType) and serialization_type == dsl_types.DumpTypes.Serializable): result_key = str(d_key) else: result_key = d_key if (result_key == 'type' and isinstance(d_value, dsl_types.MuranoType) and serialization_type == dsl_types.DumpTypes.Mixed): result_value = d_value, False else: result_value = _pass12_serialize( d_value, parent, serialized_objects, designer_attributes_getter, executor, serialize_actions, serialization_type, allow_refs, with_destruction_dependencies) result[result_key] = result_value[0] if result_value[1]: need_another_pass = True return result, need_another_pass elif utils.is_sequence(value) or isinstance(value, utils.SetType): need_another_pass = False result = [] for t in value: v, nmp = _pass12_serialize( t, parent, serialized_objects, designer_attributes_getter, executor, serialize_actions, serialization_type, allow_refs, with_destruction_dependencies) if nmp: need_another_pass = True result.append(v) return result, need_another_pass else: raise ValueError() def _pass3_serialize(value, serialized_objects, allow_refs=False): if isinstance(value, dict): for d_key, d_value in value.items(): if isinstance(d_value, ObjRef): if (d_value.ref_obj.object_id in serialized_objects or allow_refs): value[d_key] = d_value.ref_obj.object_id else: del value[d_key] else: _pass3_serialize(d_value, serialized_objects, allow_refs) elif isinstance(value, list): index = 0 while index < len(value): item = value[index] if isinstance(item, ObjRef): if item.ref_obj.object_id in serialized_objects or allow_refs: value[index] = item.ref_obj.object_id else: value.pop(index) index -= 1 else: _pass3_serialize(item, serialized_objects, allow_refs) index += 1 return value def is_nested_in(obj, ancestor): while True: if obj is ancestor: return True if obj is None: return False obj = obj.owner def collect_objects(root_object): visited = set() def rec(obj): if utils.is_sequence(obj) or isinstance(obj, utils.SetType): for value in obj: for t in rec(value): yield t elif isinstance(obj, utils.MappingType): for value in obj.values(): for t in rec(value): yield t elif isinstance(obj, dsl_types.MuranoObjectInterface): for t in rec(obj.object): yield t elif isinstance(obj, dsl_types.MuranoObject) and obj not in visited: visited.add(obj) yield obj for t in rec(obj.to_dictionary()): yield t return rec(root_object) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/dsl/session_local_storage.py0000664000175000017500000000625700000000000022152 0ustar00zuulzuul00000000000000# 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. # This code is almost a complete copy of eventlet.corolocal with only # the concept of current thread replaced with current session import collections import weakref from murano.dsl import helpers # the entire purpose of this class is to store off the constructor # arguments in a local variable without calling __init__ directly class _localbase(object): __slots__ = '_local__args', '_local__sessions' def __new__(cls, *args, **kw): self = object.__new__(cls) object.__setattr__(self, '_local__args', (args, kw)) object.__setattr__( self, '_local__sessions', weakref.WeakKeyDictionary()) if (args or kw) and (cls.__init__ is object.__init__): raise TypeError('Initialization arguments are not supported') return self def _patch(session_local): sessions_dict = object.__getattribute__(session_local, '_local__sessions') session = helpers.get_execution_session() localdict = sessions_dict.get(session) if localdict is None: # must be the first time we've seen this session, call __init__ localdict = {} sessions_dict[session] = localdict cls = type(session_local) if cls.__init__ is not object.__init__: args, kw = object.__getattribute__(session_local, '_local__args') session_local.__init__(*args, **kw) object.__setattr__(session_local, '__dict__', localdict) class _local(_localbase): def __getattribute__(self, attr): _patch(self) return object.__getattribute__(self, attr) def __setattr__(self, attr, value): _patch(self) return object.__setattr__(self, attr, value) def __delattr__(self, attr): _patch(self) return object.__delattr__(self, attr) def session_local(cls): return type(cls.__name__, (cls, _local), {}) class SessionLocalDict(collections.UserDict, object): def __init__(self, **kwargs): self.__session_data = weakref.WeakKeyDictionary() self.__default = {} super(SessionLocalDict, self).__init__(**kwargs) @property def data(self): session = helpers.get_execution_session() if session is None: return self.__default return self.__session_data.setdefault(session, {}) @data.setter def data(self, value): session = helpers.get_execution_session() if session is None: self.__default = value else: self.__session_data[session] = value def execution_session_memoize(func): cache = SessionLocalDict() return helpers.get_memoize_func(func, cache) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/dsl/typespec.py0000664000175000017500000000570600000000000017423 0ustar00zuulzuul00000000000000# 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 weakref from murano.dsl.contracts import contracts from murano.dsl import dsl_types from murano.dsl import helpers class Spec(object): def __init__(self, declaration, container_type): self._container_type = weakref.ref(container_type) self._contract = contracts.Contract( declaration['Contract'], container_type) self._has_default = 'Default' in declaration self._default = declaration.get('Default') def _get_this_context(self, this): executor = helpers.get_executor() if isinstance(this, dsl_types.MuranoType): return executor.create_object_context(this) return executor.create_object_context( this.cast(self._container_type())) def transform(self, value, this, owner, context, default=None, finalize=True): if default is None: default = self.default if isinstance(this, dsl_types.MuranoTypeReference): this = this.type if isinstance(this, dsl_types.MuranoType): return self._contract.transform( value, self._get_this_context(this), None, None, default, helpers.get_type(context), finalize=finalize) else: return self._contract.transform( value, self._get_this_context(this), this, owner, default, helpers.get_type(context), finalize=finalize) def validate(self, value, this, context, default=None): if default is None: default = self.default return self._contract.validate( value, self._get_this_context(this), default, helpers.get_type(context)) def check_type(self, value, context, default=None): if default is None: default = self.default return self._contract.check_type( value, context, default, helpers.get_type(context)) def finalize(self, value, this, context): return self._contract.finalize( value, self._get_this_context(this), helpers.get_type(context)) @property def default(self): return self._default @property def contract(self): return self._contract @property def has_default(self): return self._has_default @property def declaring_type(self): return self._container_type() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/dsl/virtual_exceptions.py0000664000175000017500000001300600000000000021506 0ustar00zuulzuul00000000000000# 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 murano.dsl import constants from murano.dsl import dsl_exception from murano.dsl import expressions from murano.dsl import helpers from murano.dsl import macros from murano.dsl.principal_objects import stack_trace from murano.dsl import yaql_integration class ThrowMacro(expressions.DslExpression): def __init__(self, Throw, Message=None, Extra=None, Cause=None): if not Throw: raise ValueError() if not isinstance(Throw, list): Throw = [Throw] self._names = Throw self._message = Message self._extra = Extra or {} self._cause = Cause def _resolve_names(self, names, context): murano_class = helpers.get_type(context) for name in names: yield murano_class.namespace_resolver.resolve_name(name) def execute(self, context): stacktrace = stack_trace.create_stack_trace(context, False) cause = None if self._cause: cause = helpers.evaluate(self._cause, context).get_property( 'nativeException') raise dsl_exception.MuranoPlException( list(self._resolve_names(helpers.evaluate(self._names, context), context)), helpers.evaluate(self._message, context), stacktrace, self._extra, cause) def __unicode__(self): if self._message: return u'Throw {0}: {1}'.format(self._names, self._message) return u'Throw ' + str(self._names) class CatchBlock(expressions.DslExpression): def __init__(self, With=None, As=None, Do=None): if With is not None and not isinstance(With, list): With = [With] self._with = With self._as = As self._code_block = None if Do is None else macros.CodeBlock(Do) def _resolve_names(self, names, context): murano_class = helpers.get_type(context) for name in names: yield murano_class.namespace_resolver.resolve_name(name) def execute(self, context): exception = helpers.get_current_exception(context) names = ( None if self._with is None else list(self._resolve_names(self._with, context)) ) for name in exception.names: if self._with is None or name in names: if self._code_block: if self._as: wrapped = self._wrap_internal_exception( exception, context, name) context[self._as] = wrapped self._code_block.execute(context) return True return False def _wrap_internal_exception(self, exception, context, name): obj = yaql_integration.call_func(context, 'new', 'io.murano.Exception') obj.set_property('name', name) obj.set_property('message', exception.message) obj.set_property('stackTrace', exception.stacktrace) obj.set_property('extra', exception.extra) obj.set_property('nativeException', exception) return obj class TryBlockMacro(expressions.DslExpression): def __init__(self, Try, Catch=None, Finally=None, Else=None): self._try_block = macros.CodeBlock(Try) self._catch_block = None if Catch is not None: if not isinstance(Catch, list): Catch = [Catch] self._catch_block = [CatchBlock(**c) for c in Catch] self._finally_block = ( None if Finally is None else macros.CodeBlock(Finally)) self._else_block = ( None if Else is None else macros.CodeBlock(Else)) def execute(self, context): try: self._try_block.execute(context) except dsl_exception.MuranoPlException as e: caught = False if self._catch_block: try: context[constants.CTX_CURRENT_EXCEPTION] = e for cb in self._catch_block: if cb.execute(context): caught = True break if not caught: raise finally: context[constants.CTX_CURRENT_EXCEPTION] = None else: raise else: if self._else_block: self._else_block.execute(context) finally: if self._finally_block: self._finally_block.execute(context) class RethrowMacro(expressions.DslExpression): def __init__(self, Rethrow): pass def execute(self, context): exception = context['$?currentException'] if not exception: raise TypeError('Rethrow must be inside Catch') raise exception def __str__(self): return 'Rethrow' def register(): expressions.register_macro(ThrowMacro) expressions.register_macro(TryBlockMacro) expressions.register_macro(RethrowMacro) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/dsl/yaql_expression.py0000664000175000017500000000531500000000000021010 0ustar00zuulzuul00000000000000# 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 from yaql.language import exceptions as yaql_exceptions from yaql.language import expressions from murano.dsl import constants from murano.dsl import dsl_types from murano.dsl import yaql_integration EXPRESSION_REGEX = re.compile('^[\s\w\d.]*$') class YaqlExpression(dsl_types.YaqlExpression): def __init__(self, expression, version): self._version = version if isinstance(expression, str): self._expression = str(expression) self._parsed_expression = yaql_integration.parse( self._expression, version) self._file_position = None elif isinstance(expression, YaqlExpression): self._expression = expression._expression self._parsed_expression = expression._parsed_expression self._file_position = expression._file_position elif isinstance(expression, expressions.Statement): self._expression = str(expression) self._parsed_expression = expression self._file_position = None else: raise TypeError('expression is not of supported types') @property def expression(self): return self._expression @property def version(self): return self._version @property def source_file_position(self): return self._file_position @source_file_position.setter def source_file_position(self, value): self._file_position = value def __repr__(self): return 'YAQL(%s)' % self._expression def __str__(self): return self._expression @staticmethod def is_expression(expression, version): if not isinstance(expression, str): return False if EXPRESSION_REGEX.match(expression): return False try: yaql_integration.parse(expression, version) return True except yaql_exceptions.YaqlParsingException: return False def __call__(self, context): if context: context[constants.CTX_CURRENT_INSTRUCTION] = self return self._parsed_expression.evaluate(context=context) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/dsl/yaql_functions.py0000664000175000017500000002457700000000000020634 0ustar00zuulzuul00000000000000# 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 eventlet from yaql.language import expressions from yaql.language import specs from yaql.language import utils from yaql.language import yaqltypes from murano.dsl import constants from murano.dsl import dsl from murano.dsl import dsl_types from murano.dsl import helpers from murano.dsl import reflection from murano.dsl import serializer @specs.parameter('object_', dsl.MuranoObjectParameter()) def id_(object_): return object_.id @specs.parameter('object_', dsl.MuranoObjectParameter()) @specs.parameter('type__', dsl.MuranoTypeParameter()) @specs.parameter('version_spec', yaqltypes.String(True)) def cast(context, object_, type__, version_spec=None): return helpers.cast( object_, type__.type.name, version_spec or helpers.get_type(context)) @specs.parameter('__type_name', dsl.MuranoTypeParameter()) @specs.parameter('__extra', utils.MappingType) @specs.parameter('__owner', dsl.MuranoObjectParameter( nullable=True, decorate=False)) @specs.parameter('__object_name', yaqltypes.String(True)) def new(__context, __type_name, __owner=None, __object_name=None, __extra=None, **parameters): data = { __type_name: parameters, 'name': __object_name } for key, value in (__extra or {}).items(): if key.startswith('_'): data[key] = value object_store = helpers.get_object_store() return object_store.load(data, __owner, context=__context, scope_type=helpers.get_names_scope(__context)) @specs.parameter('type_name', dsl.MuranoTypeParameter()) @specs.parameter('parameters', utils.MappingType) @specs.parameter('extra', utils.MappingType) @specs.parameter('owner', dsl.MuranoObjectParameter( nullable=True, decorate=False)) @specs.parameter('object_name', yaqltypes.String(True)) @specs.name('new') def new_from_dict(type_name, context, parameters, owner=None, object_name=None, extra=None): return new(context, type_name, owner, object_name, extra, **utils.filter_parameters_dict(parameters)) @specs.name('new') @specs.parameter('owner', dsl.MuranoObjectParameter( nullable=True, decorate=False)) @specs.parameter('model', utils.MappingType) def new_from_model(context, model, owner=None): object_store = helpers.get_object_store() return object_store.load(model, owner, context=context, scope_type=helpers.get_names_scope(context)) @specs.parameter('object_', dsl.MuranoObjectParameter(decorate=False)) @specs.parameter('func', yaqltypes.Lambda()) def super_(context, object_, func=None): cast_type = helpers.get_type(context) if func is None: return [object_.cast(type) for type in cast_type.parents] return map(func, super_(context, object_)) @specs.parameter('object_', dsl.MuranoObjectParameter(decorate=False)) @specs.parameter('func', yaqltypes.Lambda()) def psuper(context, object_, func=None): if func is None: return super_(context, object_) return helpers.parallel_select(super_(context, object_), func) @specs.extension_method def require(value): if value is None: raise ValueError('Required value is missing') return value @specs.parameter('obj', dsl.MuranoObjectParameter()) @specs.parameter('murano_class_ref', dsl.MuranoTypeParameter()) @specs.extension_method def find(obj, murano_class_ref): return obj.find_owner(murano_class_ref, optional=True) @specs.parameter('seconds', yaqltypes.Number()) def sleep_(seconds): eventlet.sleep(seconds) @specs.parameter('object_', dsl.MuranoObjectParameter(nullable=True)) def type_(object_): return None if object_ is None else object_.type.get_reference() @specs.name('type') @specs.parameter('object_', dsl.MuranoObjectParameter(nullable=True)) def type_legacy(object_): return None if object_ is None else object_.type.name @specs.name('type') @specs.parameter('cls', dsl.MuranoTypeParameter()) def type_from_name(cls): return cls @specs.parameter('object_', dsl.MuranoObjectParameter(nullable=True)) def typeinfo(object_): return None if object_ is None else object_.type @specs.parameter('cls', dsl.MuranoTypeParameter()) @specs.name('typeinfo') def typeinfo_for_class(cls): return cls.type @specs.parameter('object_', dsl.MuranoObjectParameter(nullable=True)) def name(object_): return None if object_ is None else object_.name @specs.parameter('object_', dsl.MuranoObjectParameter()) def metadata(object_): return object_.object.metadata @specs.parameter('obj', dsl.MuranoObjectParameter(decorate=False)) @specs.parameter('property_name', yaqltypes.Keyword()) @specs.name('#operator_.') def obj_attribution(context, obj, property_name): return obj.get_property(property_name, context) @specs.parameter('cls', dsl_types.MuranoTypeReference) @specs.parameter('property_name', yaqltypes.Keyword()) @specs.name('#operator_.') def obj_attribution_static(context, cls, property_name): return helpers.get_executor().get_static_property( cls.type, property_name, context) @specs.parameter('receiver', dsl.MuranoObjectParameter(decorate=False)) @specs.parameter('expr', yaqltypes.Lambda(method=True)) @specs.inject('operator', yaqltypes.Super(with_context=True)) @specs.name('#operator_.') def op_dot(context, receiver, expr, operator): executor = helpers.get_executor() type_context = executor.context_manager.create_type_context(receiver.type) obj_context = executor.context_manager.create_object_context(receiver) ctx2 = helpers.link_contexts( helpers.link_contexts(context, type_context), obj_context) return operator(ctx2, receiver, expr) @specs.parameter('receiver', dsl_types.MuranoTypeReference) @specs.parameter('expr', yaqltypes.Lambda(method=True)) @specs.inject('operator', yaqltypes.Super(with_context=True)) @specs.name('#operator_.') def op_dot_static(context, receiver, expr, operator): executor = helpers.get_executor() type_context = executor.context_manager.create_type_context( receiver.type) ctx2 = helpers.link_contexts(context, type_context) return operator(ctx2, receiver, expr) @specs.parameter('prefix', yaqltypes.Keyword()) @specs.parameter('name', yaqltypes.Keyword()) @specs.name('#operator_:') def ns_resolve(context, prefix, name): return helpers.get_class(prefix + ':' + name, context).get_reference() @specs.parameter('name', yaqltypes.Keyword()) @specs.name('#unary_operator_:') def ns_resolve_unary(context, name): return ns_resolve(context, '', name) @specs.parameter('obj', dsl.MuranoObjectParameter(nullable=True)) @specs.parameter('type_', dsl.MuranoTypeParameter()) @specs.name('#operator_is') def is_instance_of(obj, type_): if obj is None: return False return type_.type.is_compatible(obj) def is_object(value): return isinstance(value, (dsl_types.MuranoObject, dsl_types.MuranoTypeReference)) @specs.name('call') @specs.parameter('name', yaqltypes.String()) @specs.parameter('args', yaqltypes.Sequence()) @specs.parameter('kwargs', utils.MappingType) @specs.inject('op_dot', yaqltypes.Delegate('#operator_.', with_context=True)) @specs.inject('base', yaqltypes.Super(with_context=True)) def call_func(context, op_dot, base, name, args, kwargs, receiver=utils.NO_VALUE): if isinstance(receiver, (dsl_types.MuranoObject, dsl_types.MuranoTypeReference)): kwargs = utils.filter_parameters_dict(kwargs) args += tuple( expressions.MappingRuleExpression(expressions.KeywordConstant(key), value) for key, value in kwargs.items()) function = expressions.Function(name, *args) return op_dot(context, receiver, function) else: return base(context, name, args, kwargs, receiver) @specs.parameter('obj', dsl.MuranoObjectParameter(decorate=False)) @specs.parameter('serialization_type', yaqltypes.String()) @specs.parameter('ignore_upcasts', bool) def dump(obj, serialization_type=dsl_types.DumpTypes.Serializable, ignore_upcasts=True): if serialization_type not in dsl_types.DumpTypes.All: raise ValueError('Invalid Serialization Type') executor = helpers.get_executor() if ignore_upcasts: obj = obj.real_this return serializer.serialize(obj, executor, serialization_type) def register(context, runtime_version): context.register_function(cast) context.register_function(new) context.register_function(new_from_dict) context.register_function(new_from_model) context.register_function(id_) context.register_function(super_) context.register_function(psuper) context.register_function(require) context.register_function(find) context.register_function(sleep_) context.register_function(typeinfo) context.register_function(typeinfo_for_class) context.register_function(name) context.register_function(metadata) context.register_function(obj_attribution) context.register_function(obj_attribution_static) context.register_function(op_dot) context.register_function(op_dot_static) context.register_function(ns_resolve) context.register_function(ns_resolve_unary) reflection.register(context) context.register_function(is_instance_of) if runtime_version <= constants.RUNTIME_VERSION_1_3: context.register_function(type_legacy) else: context.register_function(type_) context.register_function(call_func) if runtime_version <= constants.RUNTIME_VERSION_1_1: context = context.create_child_context() for t in ('id', 'cast', 'super', 'psuper', 'type'): for spec in utils.to_extension_method(t, context): context.register_function(spec) context.register_function(type_from_name) context.register_function(is_object) context.register_function(dump) return context ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/dsl/yaql_integration.py0000664000175000017500000003625100000000000021137 0ustar00zuulzuul00000000000000# 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 weakref from oslo_config import cfg import yaql from yaql.language import contexts from yaql.language import conventions from yaql.language import factory from yaql.language import specs from yaql.language import utils from yaql.language import yaqltypes from yaql import legacy from murano.dsl import constants from murano.dsl import dsl from murano.dsl import dsl_types from murano.dsl import helpers from murano.dsl import yaql_functions CONF = cfg.CONF ENGINE_10_OPTIONS = { 'yaql.limitIterators': CONF.murano.dsl_iterators_limit, 'yaql.memoryQuota': constants.EXPRESSION_MEMORY_QUOTA, 'yaql.convertSetsToLists': True, 'yaql.convertTuplesToLists': True, 'yaql.iterableDicts': True } ENGINE_12_OPTIONS = { 'yaql.limitIterators': CONF.murano.dsl_iterators_limit, 'yaql.memoryQuota': constants.EXPRESSION_MEMORY_QUOTA, 'yaql.convertSetsToLists': True, 'yaql.convertTuplesToLists': True } def _add_operators(engine_factory): engine_factory.insert_operator( '>', True, 'is', factory.OperatorType.BINARY_LEFT_ASSOCIATIVE, False) engine_factory.insert_operator( None, True, ':', factory.OperatorType.BINARY_LEFT_ASSOCIATIVE, True) engine_factory.insert_operator( ':', True, ':', factory.OperatorType.PREFIX_UNARY, False) engine_factory.operators.insert(0, ()) def _create_engine(runtime_version): engine_factory = factory.YaqlFactory() _add_operators(engine_factory=engine_factory) options = (ENGINE_10_OPTIONS if runtime_version <= constants.RUNTIME_VERSION_1_1 else ENGINE_12_OPTIONS) return engine_factory.create(options=options) @specs.name('#finalize') def _finalize(obj, context): return helpers.evaluate(obj, context) CONVENTION = conventions.CamelCaseConvention() ENGINE_10 = _create_engine(constants.RUNTIME_VERSION_1_0) ENGINE_12 = _create_engine(constants.RUNTIME_VERSION_1_2) ROOT_CONTEXT_10 = legacy.create_context( convention=CONVENTION, finalizer=_finalize) ROOT_CONTEXT_12 = yaql.create_context( convention=CONVENTION, finalizer=_finalize) class ContractedValue(yaqltypes.GenericType): def __init__(self, value_spec, with_check=False): def converter(value, receiver, context, *args, **kwargs): if isinstance(receiver, dsl_types.MuranoObject): this = receiver.real_this else: this = receiver return value_spec.transform( value, this, context[constants.CTX_ARGUMENT_OWNER], context) def checker(value, context, *args, **kwargs): return value_spec.check_type(value, context) super(ContractedValue, self).__init__( True, checker if with_check else None, converter) def convert(self, value, *args, **kwargs): if value is None: return self.converter(value, *args, **kwargs) return super(ContractedValue, self).convert(value, *args, **kwargs) def create_empty_context(): context = contexts.Context(convention=CONVENTION) context.register_function(_finalize) return context @helpers.memoize def create_context(runtime_version): if runtime_version <= constants.RUNTIME_VERSION_1_1: context = ROOT_CONTEXT_10.create_child_context() else: context = ROOT_CONTEXT_12.create_child_context() context[constants.CTX_YAQL_ENGINE] = choose_yaql_engine(runtime_version) return yaql_functions.register(context, runtime_version) def choose_yaql_engine(runtime_version): return (ENGINE_10 if runtime_version <= constants.RUNTIME_VERSION_1_1 else ENGINE_12) def parse(expression, runtime_version): return choose_yaql_engine(runtime_version)(expression) def call_func(__context, __name, *args, **kwargs): engine = __context[constants.CTX_YAQL_ENGINE] return __context(__name, engine)( *args, **{CONVENTION.convert_parameter_name(key): value for key, value in kwargs.items()}) def _infer_parameter_type(name, class_name): if name == 'context': return yaqltypes.Context() if name == 'this': return dsl.ThisParameter() if name == 'interfaces': return dsl.InterfacesParameter() if name == 'yaql_engine': return yaqltypes.Engine() if name.startswith('__'): return _infer_parameter_type(name[2:], class_name) if class_name and name.startswith('_{0}__'.format(class_name)): return _infer_parameter_type(name[3 + len(class_name):], class_name) def get_function_definition(func, murano_method, original_name): cls = murano_method.declaring_type.extension_class def param_type_func(name): return None if not cls else _infer_parameter_type(name, cls.__name__) body = func if (cls is None or helpers.inspect_is_method(cls, original_name) or helpers.inspect_is_classmethod(cls, original_name)): body = helpers.function(func) fd = specs.get_function_definition( body, convention=CONVENTION, parameter_type_func=param_type_func) fd.is_method = True fd.is_function = False if not cls or helpers.inspect_is_method(cls, original_name): fd.set_parameter( 0, dsl.MuranoObjectParameter(murano_method.declaring_type), overwrite=True) if cls and helpers.inspect_is_classmethod(cls, original_name): _remove_first_parameter(fd) body = func name = getattr(func, '__murano_name', None) if name: fd.name = name fd.insert_parameter(specs.ParameterDefinition( '?1', yaqltypes.Context(), 0)) is_static = cls and ( helpers.inspect_is_static(cls, original_name) or helpers.inspect_is_classmethod(cls, original_name)) if is_static: fd.insert_parameter(specs.ParameterDefinition( '?2', yaqltypes.PythonType(object), 1)) def payload(__context, __self, *args, **kwargs): with helpers.contextual(__context): __context[constants.CTX_NAMES_SCOPE] = \ murano_method.declaring_type return body(__self.extension, *args, **kwargs) def static_payload(__context, __receiver, *args, **kwargs): with helpers.contextual(__context): __context[constants.CTX_NAMES_SCOPE] = \ murano_method.declaring_type return body(*args, **kwargs) if is_static: fd.payload = static_payload else: fd.payload = payload fd.meta[constants.META_MURANO_METHOD] = murano_method return fd def _remove_first_parameter(fd): first_param = None first_param_name = None for p_name, p in fd.parameters.items(): if isinstance(p.value_type, yaqltypes.HiddenParameterType): continue if p.position is None: continue if first_param is None or p.position < first_param.position: first_param = p first_param_name = p_name if first_param: del fd.parameters[first_param_name] for p_name, p in fd.parameters.items(): if p.position is not None and p.position > first_param.position: p.position -= 1 def build_stub_function_definitions(murano_method): if isinstance(murano_method.body, specs.FunctionDefinition): return _build_native_stub_function_definitions(murano_method) else: return _build_mpl_stub_function_definitions(murano_method) def _build_native_stub_function_definitions(murano_method): runtime_version = murano_method.declaring_type.package.runtime_version engine = choose_yaql_engine(runtime_version) @specs.method @specs.name(murano_method.name) @specs.meta(constants.META_MURANO_METHOD, murano_method) @specs.parameter('__receiver', yaqltypes.NotOfType( dsl_types.MuranoTypeReference)) def payload(__context, __receiver, *args, **kwargs): args = tuple(dsl.to_mutable(arg, engine) for arg in args) kwargs = dsl.to_mutable(kwargs, engine) return helpers.evaluate(murano_method.invoke( __receiver, args, kwargs, __context, True), __context) @specs.method @specs.name(murano_method.name) @specs.meta(constants.META_MURANO_METHOD, murano_method) @specs.parameter('__receiver', yaqltypes.NotOfType( dsl_types.MuranoTypeReference)) def extension_payload(__context, __receiver, *args, **kwargs): args = tuple(dsl.to_mutable(arg, engine) for arg in args) kwargs = dsl.to_mutable(kwargs, engine) return helpers.evaluate(murano_method.invoke( murano_method.declaring_type, (__receiver,) + args, kwargs, __context, True), __context) @specs.method @specs.name(murano_method.name) @specs.meta(constants.META_MURANO_METHOD, murano_method) @specs.parameter('__receiver', dsl_types.MuranoTypeReference) def static_payload(__context, __receiver, *args, **kwargs): args = tuple(dsl.to_mutable(arg, engine) for arg in args) kwargs = dsl.to_mutable(kwargs, engine) return helpers.evaluate(murano_method.invoke( __receiver, args, kwargs, __context, True), __context) if murano_method.usage in dsl_types.MethodUsages.InstanceMethods: return specs.get_function_definition(payload), None elif murano_method.usage == dsl_types.MethodUsages.Static: return (specs.get_function_definition(payload), specs.get_function_definition(static_payload)) elif murano_method.usage == dsl_types.MethodUsages.Extension: return (specs.get_function_definition(extension_payload), specs.get_function_definition(static_payload)) else: raise ValueError('Unknown method usage ' + murano_method.usage) def _build_mpl_stub_function_definitions(murano_method): if murano_method.usage in dsl_types.MethodUsages.InstanceMethods: return _create_instance_mpl_stub(murano_method), None elif murano_method.usage == dsl_types.MethodUsages.Static: return (_create_instance_mpl_stub(murano_method), _create_static_mpl_stub(murano_method)) elif murano_method.usage == dsl_types.MethodUsages.Extension: return (_create_extension_mpl_stub(murano_method), _create_static_mpl_stub(murano_method)) else: raise ValueError('Unknown method usage ' + murano_method.usage) def _create_instance_mpl_stub(murano_method): def payload(__context, __receiver, *args, **kwargs): return murano_method.invoke(__receiver, args, kwargs, __context, True) fd = _create_basic_mpl_stub(murano_method, 1, payload, False) receiver_type = dsl.MuranoObjectParameter( weakref.proxy(murano_method.declaring_type), decorate=False) fd.set_parameter(specs.ParameterDefinition('__receiver', receiver_type, 1)) return fd def _create_static_mpl_stub(murano_method): def payload(__context, __receiver, *args, **kwargs): return murano_method.invoke(__receiver, args, kwargs, __context, True) fd = _create_basic_mpl_stub(murano_method, 1, payload, False) receiver_type = dsl.MuranoTypeParameter( weakref.proxy(murano_method.declaring_type), resolve_strings=False) fd.set_parameter(specs.ParameterDefinition('__receiver', receiver_type, 1)) return fd def _create_extension_mpl_stub(murano_method): def payload(__context, __receiver, *args, **kwargs): return murano_method.invoke( murano_method.declaring_type, (__receiver,) + args, kwargs, __context, True) return _create_basic_mpl_stub(murano_method, 0, payload, True) def _create_basic_mpl_stub(murano_method, reserve_params, payload, check_first_arg): fd = specs.FunctionDefinition( murano_method.name, payload, is_function=False, is_method=True) i = reserve_params + 1 varargs = False kwargs = False for name, arg_spec in murano_method.arguments_scheme.items(): position = i if arg_spec.usage == dsl_types.MethodArgumentUsages.VarArgs: name = '*' varargs = True elif arg_spec.usage == dsl_types.MethodArgumentUsages.KwArgs: name = '**' position = None kwargs = True p = specs.ParameterDefinition( name, ContractedValue(arg_spec, with_check=check_first_arg), position=position, default=dsl.NO_VALUE) check_first_arg = False fd.parameters[name] = p i += 1 if not varargs: fd.parameters['*'] = specs.ParameterDefinition( '*', value_type=yaqltypes.PythonType(object, nullable=True), position=i) if not kwargs: fd.parameters['**'] = specs.ParameterDefinition( '**', value_type=yaqltypes.PythonType(object, nullable=True)) fd.set_parameter(specs.ParameterDefinition( '__context', yaqltypes.Context(), 0)) fd.meta[constants.META_MURANO_METHOD] = murano_method return fd def get_class_factory_definition(cls, murano_class): runtime_version = murano_class.package.runtime_version engine = choose_yaql_engine(runtime_version) def payload(__context, __receiver, *args, **kwargs): args = tuple(dsl.to_mutable(arg, engine) for arg in args) kwargs = dsl.to_mutable(kwargs, engine) with helpers.contextual(__context): __context[constants.CTX_NAMES_SCOPE] = murano_class result = helpers.evaluate(cls(*args, **kwargs), __context) __receiver.object.extension = result try: fd = specs.get_function_definition( helpers.function(cls.__init__), parameter_type_func=lambda name: _infer_parameter_type( name, cls.__name__), convention=CONVENTION) except AttributeError: # __init__ is a slot wrapper inherited from object or other C type fd = specs.get_function_definition(lambda self: None) fd.meta[constants.META_NO_TRACE] = True fd.insert_parameter(specs.ParameterDefinition( '?1', yaqltypes.Context(), position=0)) fd.is_method = True fd.is_function = False fd.name = '__init__' fd.payload = payload return fd def filter_parameters(__fd, *args, **kwargs): if '*' not in __fd.parameters: position_args = 0 for p in __fd.parameters.values(): if p.position is not None: position_args += 1 args = args[:position_args] kwargs = kwargs.copy() for name in list(kwargs.keys()): if not utils.is_keyword(name): del kwargs[name] if '**' not in __fd.parameters: names = {p.alias or p.name for p in __fd.parameters.values()} for name in list(kwargs.keys()): if name not in names: del kwargs[name] return args, kwargs ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1696417899.8011808 murano-16.0.0/murano/engine/0000775000175000017500000000000000000000000015670 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/engine/__init__.py0000664000175000017500000000000000000000000017767 0ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/engine/execution_session.py0000664000175000017500000000337700000000000022022 0ustar00zuulzuul00000000000000# 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 oslo_log import log as logging LOG = logging.getLogger(__name__) class ExecutionSession(object): def __init__(self): self.token = None self.project_id = None self.user_id = None self.environment_owner_project_id = None self.environment_owner_user_id = None self.trust_id = None self.system_attributes = {} self._set_up_list = [] self._tear_down_list = [] def on_session_start(self, delegate): self._set_up_list.append(delegate) def on_session_finish(self, delegate): self._tear_down_list.append(delegate) def start(self): for delegate in self._set_up_list: try: delegate() except Exception: LOG.exception('Unhandled exception on invocation of ' 'pre-execution hook') self._set_up_list = [] def finish(self): for delegate in self._tear_down_list: try: delegate() except Exception: LOG.exception('Unhandled exception on invocation of ' 'post-execution hook') self._tear_down_list = [] ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/engine/mock_context_manager.py0000664000175000017500000001305200000000000022432 0ustar00zuulzuul00000000000000# 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 yaql.language import specs from yaql.language import yaqltypes from murano.common import engine from murano.dsl import constants from murano.dsl import dsl from murano.dsl import helpers from murano.dsl import yaql_integration class MockContextManager(engine.ContextManager): def __init__(self): # { object_id: [mock_function_definitions] } self.obj_mock_ctx = {} # { class_name: [mock_function_definitions] } self.class_mock_ctx = {} def _create_new_ctx(self, mock_funcs): mock_context = yaql_integration.create_empty_context() for mock_func in mock_funcs: mock_context.register_function(mock_func) return mock_context def _create_new_ctx_for_class(self, name): new_context = None if name in self.class_mock_ctx: new_context = self._create_new_ctx(self.class_mock_ctx[name]) return new_context def _create_new_ctx_for_obj(self, obj_id): new_context = None if obj_id in self.obj_mock_ctx: new_context = self._create_new_ctx(self.obj_mock_ctx[obj_id]) return new_context def create_type_context(self, murano_type): original_context = super( MockContextManager, self).create_type_context(murano_type) mock_context = self._create_new_ctx_for_class(murano_type.name) if mock_context: result_context = helpers.link_contexts( original_context, mock_context).create_child_context() else: result_context = original_context return result_context def create_object_context(self, murano_obj): original_context = super( MockContextManager, self).create_object_context(murano_obj) mock_context = self._create_new_ctx_for_obj(murano_obj.type.name) if mock_context: result_context = helpers.link_contexts( original_context, mock_context).create_child_context() else: result_context = original_context return result_context def create_root_context(self, runtime_version): root_context = super(MockContextManager, self).create_root_context( runtime_version) constext = root_context.create_child_context() constext.register_function(inject_method_with_str, name='inject') constext.register_function(inject_method_with_yaql_expr, name='inject') constext.register_function(with_original) return constext @specs.parameter('kwargs', yaqltypes.Lambda(with_context=True)) def with_original(context, **kwargs): new_context = context.create_child_context() original_context = context[constants.CTX_ORIGINAL_CONTEXT] for k, v in kwargs.items(): new_context['$' + k] = v(original_context) return new_context @specs.parameter( 'target', yaqltypes.AnyOf(dsl.MuranoTypeParameter(), dsl.MuranoObjectParameter())) @specs.parameter('target_method', yaqltypes.String()) @specs.parameter('mock_object', dsl.MuranoObjectParameter()) @specs.parameter('mock_name', yaqltypes.String()) def inject_method_with_str(context, target, target_method, mock_object, mock_name): ctx_manager = helpers.get_executor().context_manager current_class = helpers.get_type(context) mock_func = current_class.find_single_method(mock_name) original_class = target.type original_function = original_class.find_single_method(target_method) result_fd = original_function.instance_stub.clone() def payload_adapter(__context, __sender, *args, **kwargs): return mock_func.invoke( mock_object, args, kwargs, __context, True) result_fd.payload = payload_adapter existing_mocks = ctx_manager.class_mock_ctx.setdefault( original_class.name, []) existing_mocks.append(result_fd) @specs.parameter( 'target', yaqltypes.AnyOf(dsl.MuranoTypeParameter(), dsl.MuranoObjectParameter())) @specs.parameter('target_method', yaqltypes.String()) @specs.parameter('expr', yaqltypes.Lambda(with_context=True)) def inject_method_with_yaql_expr(context, target, target_method, expr): ctx_manager = helpers.get_executor().context_manager original_class = target.type original_function = original_class.find_single_method(target_method) result_fd = original_function.instance_stub.clone() def payload_adapter(__super, __context, __sender, *args, **kwargs): new_context = context.create_child_context() new_context[constants.CTX_ORIGINAL_CONTEXT] = __context mock_obj = context[constants.CTX_THIS] new_context.register_function(lambda: __super(*args, **kwargs), name='originalMethod') return expr(new_context, mock_obj, *args, **kwargs) result_fd.payload = payload_adapter result_fd.insert_parameter('__super', yaqltypes.Super()) existing_mocks = ctx_manager.class_mock_ctx.setdefault( original_class.name, []) existing_mocks.append(result_fd) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/engine/murano_package.py0000664000175000017500000000455300000000000021225 0ustar00zuulzuul00000000000000# 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 import os from oslo_config import cfg import yaml from murano.dsl import murano_package CONF = cfg.CONF class MuranoPackage(murano_package.MuranoPackage): def __init__(self, package_loader, application_package): self.application_package = application_package super(MuranoPackage, self).__init__( package_loader, application_package.full_name, application_package.version, application_package.runtime_version, application_package.requirements, application_package.meta ) def get_class_config(self, name): version_parts = ( str(self.version.major), str(self.version.minor), str(self.version.patch) ) num_parts = len(version_parts) for i in range(num_parts + 1): for ext in ('json', 'yaml'): if i == num_parts: if version_parts[0] == '0': version_suffix = '' else: continue else: version_suffix = '-' + '.'.join( version_parts[:num_parts - i]) file_name = '{name}{version}.{extension}'.format( name=name, version=version_suffix, extension=ext) path = os.path.join(CONF.engine.class_configs, file_name) if os.path.exists(path): if ext == 'json': with open(path) as f: return json.load(f) else: with open(path) as f: return yaml.safe_load(f) return {} def get_resource(self, name): return self.application_package.get_resource(name) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/engine/package_loader.py0000664000175000017500000005753100000000000021176 0ustar00zuulzuul00000000000000# 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 itertools import os import os.path import shutil import sys import tempfile import uuid import eventlet from muranoclient.common import exceptions as muranoclient_exc from muranoclient.glance import client as glare_client import muranoclient.v1.client as muranoclient from oslo_config import cfg from oslo_log import log as logging from oslo_log import versionutils from oslo_utils import fileutils from murano.common import auth_utils from murano.common import utils from murano.dsl import constants from murano.dsl import exceptions from murano.dsl import helpers from murano.dsl import package_loader from murano.engine import murano_package from murano.engine.system import system_objects from murano.engine import yaql_yaml_loader from murano.packages import exceptions as pkg_exc from murano.packages import load_utils from murano import utils as m_utils CONF = cfg.CONF LOG = logging.getLogger(__name__) download_mem_locks = collections.defaultdict(m_utils.ReaderWriterLock) usage_mem_locks = collections.defaultdict(m_utils.ReaderWriterLock) class ApiPackageLoader(package_loader.MuranoPackageLoader): def __init__(self, execution_session, root_loader=None): self._cache_directory = self._get_cache_directory() self._class_cache = {} self._package_cache = {} self._root_loader = root_loader or self self._execution_session = execution_session self._last_glare_token = None self._glare_client = None self._murano_client = None self._murano_client_session = None self._mem_locks = [] self._ipc_locks = [] self._downloaded = [] self._fixations = collections.defaultdict(set) self._new_fixations = collections.defaultdict(set) def _get_glare_client(self): glare_settings = CONF.glare session = auth_utils.get_client_session(self._execution_session) token = session.auth.get_token(session) if self._last_glare_token != token: self._last_glare_token = token self._glare_client = None if self._glare_client is None: url = glare_settings.url if not url: url = session.get_endpoint( service_type='artifact', interface=glare_settings.endpoint_type, region_name=CONF.home_region) # TODO(gyurco): use auth_utils.get_session_client_parameters self._glare_client = glare_client.Client( endpoint=url, token=token, insecure=glare_settings.insecure, key_file=glare_settings.keyfile or None, ca_file=glare_settings.cafile or None, cert_file=glare_settings.certfile or None, type_name='murano', type_version=1) return self._glare_client @property def client(self): last_glare_client = self._glare_client if CONF.engine.packages_service in ['glance', 'glare']: if CONF.engine.packages_service == 'glance': versionutils.report_deprecated_feature( LOG, "'glance' packages_service option has been renamed " "to 'glare', please update your configuration") artifacts_client = self._get_glare_client() else: artifacts_client = None if artifacts_client != last_glare_client: self._murano_client = None if not self._murano_client: parameters = auth_utils.get_session_client_parameters( service_type='application-catalog', execution_session=self._execution_session, conf='murano' ) self._murano_client = muranoclient.Client( artifacts_client=artifacts_client, **parameters) return self._murano_client def load_class_package(self, class_name, version_spec): packages = self._class_cache.get(class_name) if packages: version = version_spec.select(packages.keys()) if version: return packages[version] filter_opts = {'class_name': class_name, 'version': helpers.breakdown_spec_to_query( version_spec)} try: package_definition = self._get_definition(filter_opts) self._lock_usage(package_definition) except LookupError: exc_info = sys.exc_info() utils.reraise(exceptions.NoPackageForClassFound, exceptions.NoPackageForClassFound(class_name), exc_info[2]) return self._to_dsl_package( self._get_package_by_definition(package_definition)) def load_package(self, package_name, version_spec): fixed_versions = self._fixations[package_name] version = version_spec.select(fixed_versions) if version: version_spec = helpers.parse_version_spec(version) packages = self._package_cache.get(package_name) if packages: version = version_spec.select(packages.keys()) if version: return packages[version] filter_opts = {'fqn': package_name, 'version': helpers.breakdown_spec_to_query( version_spec)} try: package_definition = self._get_definition(filter_opts) self._lock_usage(package_definition) except LookupError: exc_info = sys.exc_info() utils.reraise(exceptions.NoPackageFound, exceptions.NoPackageFound(package_name), exc_info[2]) else: package = self._get_package_by_definition(package_definition) self._fixations[package_name].add(package.version) self._new_fixations[package_name].add(package.version) return self._to_dsl_package(package) def register_package(self, package): for name in package.classes: self._class_cache.setdefault(name, {})[package.version] = package self._package_cache.setdefault(package.name, {})[ package.version] = package @staticmethod def _get_cache_directory(): base_directory = ( CONF.engine.packages_cache or os.path.join(tempfile.gettempdir(), 'murano-packages-cache') ) if CONF.engine.enable_packages_cache: directory = os.path.abspath(base_directory) else: directory = os.path.abspath(os.path.join(base_directory, uuid.uuid4().hex)) if not os.path.isdir(directory): fileutils.ensure_tree(directory) LOG.debug('Cache for package loader is located at: {dir}'.format( dir=directory)) return directory def _get_definition(self, filter_opts): filter_opts['catalog'] = True try: packages = list(self.client.packages.filter( **filter_opts)) if len(packages) > 1: LOG.debug('Ambiguous package resolution: more than 1 package ' 'found for query "{opts}", will resolve based on the' ' ownership'.format(opts=filter_opts)) return self._get_best_package_match(packages) elif len(packages) == 1: return packages[0] else: LOG.debug('There are no packages matching filter ' '{opts}'.format(opts=filter_opts)) raise LookupError() except muranoclient_exc.HTTPException: LOG.debug('Failed to get package definition from repository') raise LookupError() def _to_dsl_package(self, app_package): dsl_package = murano_package.MuranoPackage( self._root_loader, app_package) for name in app_package.classes: dsl_package.register_class( (lambda cls: lambda: get_class(app_package, cls))(name), name) if app_package.full_name == constants.CORE_LIBRARY: system_objects.register(dsl_package) self.register_package(dsl_package) return dsl_package def _get_package_by_definition(self, package_def): package_id = package_def.id package_directory = os.path.join( self._cache_directory, package_def.fully_qualified_name, getattr(package_def, 'version', '0.0.0'), package_id) if os.path.isdir(package_directory): try: return load_utils.load_from_dir(package_directory) except pkg_exc.PackageLoadError: LOG.exception('Unable to load package from cache. Clean-up.') shutil.rmtree(package_directory, ignore_errors=True) # the package is not yet in cache, let's try to download it. download_lock_path = os.path.join( self._cache_directory, '{}_download.lock'.format(package_id)) download_ipc_lock = m_utils.ExclusiveInterProcessLock( path=download_lock_path, sleep_func=eventlet.sleep) with download_mem_locks[package_id].write_lock(), download_ipc_lock: # NOTE(kzaitsev): # in case there were 2 concurrent threads/processes one might have # already downloaded this package. Check before trying to download if os.path.isdir(package_directory): try: return load_utils.load_from_dir(package_directory) except pkg_exc.PackageLoadError: LOG.error('Unable to load package from cache. Clean-up.') shutil.rmtree(package_directory, ignore_errors=True) # attempt the download itself try: LOG.debug("Attempting to download package {} {}".format( package_def.fully_qualified_name, package_id)) package_data = self.client.packages.download(package_id) except muranoclient_exc.HTTPException as e: msg = 'Error loading package id {0}: {1}'.format( package_id, str(e) ) exc_info = sys.exc_info() utils.reraise(pkg_exc.PackageLoadError, pkg_exc.PackageLoadError(msg), exc_info[2]) package_file = None try: with tempfile.NamedTemporaryFile(delete=False) as package_file: package_file.write(package_data) with load_utils.load_from_file( package_file.name, target_dir=package_directory, drop_dir=False) as app_package: LOG.info( "Successfully downloaded and unpacked " "package {} {}".format( package_def.fully_qualified_name, package_id)) self._downloaded.append(app_package) self.try_cleanup_cache( os.path.split(package_directory)[0], current_id=package_id) return app_package except IOError: msg = 'Unable to extract package data for %s' % package_id exc_info = sys.exc_info() utils.reraise(pkg_exc.PackageLoadError, pkg_exc.PackageLoadError(msg), exc_info[2]) finally: try: if package_file: os.remove(package_file.name) except OSError: pass def try_cleanup_cache(self, package_directory=None, current_id=None): """Attempts to cleanup cache in a given directory. :param package_directory: directory containing cached packages :param current_id: optional id of the package to exclude from the list of deleted packages """ if not package_directory: return try: pkg_ids_listed = set(os.listdir(package_directory)) except OSError: # No directory for this package, probably someone # already deleted everything. Anyway nothing to delete return # if current_id was given: ensure it's not checked for removal pkg_ids_listed -= {current_id} for pkg_id in pkg_ids_listed: stale_directory = os.path.join( package_directory, pkg_id) if not os.path.isdir(package_directory): continue usage_lock_path = os.path.join( self._cache_directory, '{}_usage.lock'.format(pkg_id)) ipc_lock = m_utils.ExclusiveInterProcessLock( path=usage_lock_path, sleep_func=eventlet.sleep) try: with usage_mem_locks[pkg_id].write_lock(False) as acquired: if not acquired: # the package is in use by other deployment in this # process will do nothing, someone else would delete it continue acquired_ipc_lock = ipc_lock.acquire(blocking=False) if not acquired_ipc_lock: # the package is in use by other deployment in another # process, will do nothing, someone else would delete continue shutil.rmtree(stale_directory, ignore_errors=True) ipc_lock.release() for lock_type in ('usage', 'download'): lock_path = os.path.join( self._cache_directory, '{}_{}.lock'.format(pkg_id, lock_type)) try: os.remove(lock_path) except OSError: LOG.warning("Couldn't delete lock file: " "{}".format(lock_path)) except RuntimeError: # couldn't upgrade read lock to write-lock. go on. continue def _get_best_package_match(self, packages): public = None other = [] for package in packages: if package.owner_id == self._execution_session.project_id: return package elif package.is_public: public = package else: other.append(package) if public is not None: return public elif other: return other[0] def _lock_usage(self, package_definition): """Locks package for usage""" # do not lock anything if we do not persist packages on disk if not CONF.engine.enable_packages_cache: return package_id = package_definition.id # A work around the fact that read_lock only supports `with` syntax. mem_lock = _with_to_generator( usage_mem_locks[package_id].read_lock()) usage_lock_path = os.path.join(self._cache_directory, '{}_usage.lock'.format(package_id)) ipc_lock = m_utils.SharedInterProcessLock( path=usage_lock_path, sleep_func=eventlet.sleep ) ipc_lock = _with_to_generator(ipc_lock) next(mem_lock) next(ipc_lock) self._mem_locks.append(mem_lock) self._ipc_locks.append(ipc_lock) def import_fixation_table(self, fixations): self._fixations = deserialize_package_fixations(fixations) def export_fixation_table(self): return serialize_package_fixations(self._fixations) def compact_fixation_table(self): self._fixations = self._new_fixations.copy() def cleanup(self): """Cleans up any lock we had acquired and removes any stale packages""" if not CONF.engine.enable_packages_cache: shutil.rmtree(self._cache_directory, ignore_errors=True) return for lock in itertools.chain(self._mem_locks, self._ipc_locks): try: next(lock) except StopIteration: continue def __enter__(self): return self def __exit__(self, exc_type, exc_val, exc_tb): self.cleanup() return False class DirectoryPackageLoader(package_loader.MuranoPackageLoader): def __init__(self, base_path, root_loader=None): self._base_path = base_path self._packages_by_class = {} self._packages_by_name = {} self._loaded_packages = set() self._root_loader = root_loader or self self._fixations = collections.defaultdict(set) self._new_fixations = collections.defaultdict(set) self._build_index() def _build_index(self): for folder in self.search_package_folders(self._base_path): try: package = load_utils.load_from_dir(folder) dsl_package = murano_package.MuranoPackage( self._root_loader, package) for class_name in package.classes: dsl_package.register_class( (lambda pkg, cls: lambda: get_class(pkg, cls))(package, class_name), class_name ) if dsl_package.name == constants.CORE_LIBRARY: system_objects.register(dsl_package) self.register_package(dsl_package) except pkg_exc.PackageLoadError: LOG.info('Unable to load package from path: {0}'.format( folder)) continue LOG.info('Loaded package from path {0}'.format(folder)) def import_fixation_table(self, fixations): self._fixations = deserialize_package_fixations(fixations) def export_fixation_table(self): return serialize_package_fixations(self._fixations) def compact_fixation_table(self): self._fixations = self._new_fixations.copy() def load_class_package(self, class_name, version_spec): packages = self._packages_by_class.get(class_name) if not packages: raise exceptions.NoPackageForClassFound(class_name) version = version_spec.select(packages.keys()) if not version: raise exceptions.NoPackageForClassFound(class_name) return packages[version] def load_package(self, package_name, version_spec): fixed_versions = self._fixations[package_name] version = version_spec.select(fixed_versions) if version: version_spec = helpers.parse_version_spec(version) packages = self._packages_by_name.get(package_name) if not packages: raise exceptions.NoPackageFound(package_name) version = version_spec.select(packages.keys()) if not version: raise exceptions.NoPackageFound(package_name) self._fixations[package_name].add(version) self._new_fixations[package_name].add(version) return packages[version] def register_package(self, package): for c in package.classes: self._packages_by_class.setdefault(c, {})[ package.version] = package self._packages_by_name.setdefault(package.name, {})[ package.version] = package @property def packages(self): for package_versions in self._packages_by_name.values(): for package in package_versions.values(): yield package @staticmethod def split_path(path): tail = True while tail: path, tail = os.path.split(path) if tail: yield path @classmethod def search_package_folders(cls, path): packages = set() for folder, _, files in os.walk(path): if 'manifest.yaml' in files: found = False for part in cls.split_path(folder): if part in packages: found = True break if not found: packages.add(folder) yield folder def cleanup(self): """A stub for possible cleanup""" pass class CombinedPackageLoader(package_loader.MuranoPackageLoader): def __init__(self, execution_session, root_loader=None): root_loader = root_loader or self self.api_loader = ApiPackageLoader(execution_session, root_loader) self.directory_loaders = [] for folder in CONF.engine.load_packages_from: if os.path.exists(folder): self.directory_loaders.append(DirectoryPackageLoader( folder, root_loader)) def load_package(self, package_name, version_spec): for loader in self.directory_loaders: try: return loader.load_package(package_name, version_spec) except exceptions.NoPackageFound: continue return self.api_loader.load_package( package_name, version_spec) def load_class_package(self, class_name, version_spec): for loader in self.directory_loaders: try: return loader.load_class_package(class_name, version_spec) except exceptions.NoPackageForClassFound: continue return self.api_loader.load_class_package( class_name, version_spec) def register_package(self, package): self.api_loader.register_package(package) def export_fixation_table(self): result = deserialize_package_fixations( self.api_loader.export_fixation_table()) for loader in self.directory_loaders: fixations = deserialize_package_fixations( loader.export_fixation_table()) for key, value in fixations.items(): result[key].update(value) return serialize_package_fixations(result) def import_fixation_table(self, fixations): self.api_loader.import_fixation_table(fixations) for loader in self.directory_loaders: loader.import_fixation_table(fixations) def compact_fixation_table(self): self.api_loader.compact_fixation_table() for loader in self.directory_loaders: loader.compact_fixation_table() def __enter__(self): return self def __exit__(self, exc_type, exc_val, exc_tb): self.cleanup() return False def cleanup(self): """Calls cleanup method of all loaders we combine""" self.api_loader.cleanup() for d_loader in self.directory_loaders: d_loader.cleanup() def get_class(package, name): version = package.runtime_version loader = yaql_yaml_loader.get_loader(version) contents, file_id = package.get_class(name) return loader(contents, file_id) def _with_to_generator(context_obj): with context_obj as obj: yield obj yield def deserialize_package_fixations(fixations): result = collections.defaultdict(set) for name, versions in fixations.items(): for version in versions: result[name].add(helpers.parse_version(version)) return result def serialize_package_fixations(fixations): return { name: list(str(v) for v in versions) for name, versions in fixations.items() } ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1696417899.805181 murano-16.0.0/murano/engine/system/0000775000175000017500000000000000000000000017214 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/engine/system/__init__.py0000664000175000017500000000000000000000000021313 0ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/engine/system/agent.py0000664000175000017500000003246700000000000020700 0ustar00zuulzuul00000000000000# 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 copy import datetime import os import os.path import time import urllib import uuid from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives.asymmetric import padding from cryptography.hazmat.primitives import hashes from cryptography.hazmat.primitives import serialization import eventlet.event from oslo_config import cfg from oslo_log import log as logging from oslo_serialization import base64 from yaql import specs import murano.common.exceptions as exceptions from murano.common.messaging import message from murano.dsl import dsl import murano.engine.system.common as common LOG = logging.getLogger(__name__) CONF = cfg.CONF class AgentException(Exception): pass @dsl.name('io.murano.system.Agent') class Agent(object): def __init__(self, host): self._enabled = False if CONF.engine.disable_murano_agent: LOG.debug('Use of murano-agent is disallowed ' 'by the server configuration') self._host = host self._enabled = not CONF.engine.disable_murano_agent env = host.find_owner('io.murano.Environment') self._queue = str('e%s-h%s' % (env.id, host.id)).lower() self._signing_key = None if CONF.engine.signing_key: key_path = os.path.expanduser(CONF.engine.signing_key) if not os.path.exists(key_path): LOG.warn("Key file %s does not exist. " "Message signing is disabled") else: with open(key_path, "rb") as key_file: key_data = key_file.read() self._signing_key = serialization.load_pem_private_key( key_data, password=None, backend=default_backend()) self._last_stamp = 0 self._initialized = False @property def enabled(self): return self._enabled @specs.parameter('line_prefix', specs.yaqltypes.String()) def signing_key(self, line_prefix=''): if not self._signing_key: return "" key = self._signing_key.public_key().public_bytes( serialization.Encoding.PEM, serialization.PublicFormat.PKCS1) return line_prefix + line_prefix.join(key.splitlines(True)) def _initialize(self): if self._initialized: return # (sjmc7) - turn this into a no-op if agents are disabled if CONF.engine.disable_murano_agent: LOG.debug('Use of murano-agent is disallowed ' 'by the server configuration') else: region = dsl.MuranoObjectInterface.create(self._host().getRegion()) with common.create_rmq_client(region) as client: client.declare(self._queue, enable_ha=True, ttl=86400000) self._initialized = True def queue_name(self): return self._queue def _check_enabled(self): if CONF.engine.disable_murano_agent: raise exceptions.PolicyViolationException( 'Use of murano-agent is disallowed ' 'by the server configuration') def _prepare_message(self, template, msg_id): msg = message.Message() msg.body = template msg.id = msg_id return msg def _send(self, template, wait_results, timeout): """Send a message over the MQ interface.""" self._initialize() region = self._host().getRegion() msg_id = template.get('ID', uuid.uuid4().hex) if wait_results: event = eventlet.event.Event() listener = region['agentListener'] listener().subscribe(msg_id, event) msg = self._prepare_message(template, msg_id) with common.create_rmq_client(region) as client: client.send(message=msg, key=self._queue, signing_func=self._sign) if wait_results: try: with eventlet.Timeout(timeout): result = event.wait() except eventlet.Timeout: listener().unsubscribe(msg_id) raise exceptions.TimeoutException( 'The murano-agent did not respond ' 'within {0} seconds'.format(timeout)) if not result: return None if result.get('FormatVersion', '1.0.0').startswith('1.'): return self._process_v1_result(result) else: return self._process_v2_result(result) else: return None @specs.parameter( 'resources', dsl.MuranoObjectParameter('io.murano.system.Resources')) def call(self, template, resources, timeout=None): if timeout is None: timeout = CONF.engine.agent_timeout self._check_enabled() plan = self.build_execution_plan(template, resources()) return self._send(plan, True, timeout) @specs.parameter( 'resources', dsl.MuranoObjectParameter('io.murano.system.Resources')) def send(self, template, resources): self._check_enabled() plan = self.build_execution_plan(template, resources()) return self._send(plan, False, 0) def call_raw(self, plan, timeout=None): if timeout is None: timeout = CONF.engine.agent_timeout self._check_enabled() return self._send(plan, True, timeout) def send_raw(self, plan): self._check_enabled() return self._send(plan, False, 0) def is_ready(self, timeout=100): try: self.wait_ready(timeout) except exceptions.TimeoutException: return False else: return True def wait_ready(self, timeout=100): self._check_enabled() template = {'Body': 'return', 'FormatVersion': '2.0.0', 'Scripts': {}} self.call_raw(template, timeout) def _sign(self, msg): if not self._signing_key: return None return self._signing_key.sign( (self._queue + msg).encode('utf-8'), padding.PKCS1v15(), hashes.SHA256()) def _process_v1_result(self, result): if result['IsException']: raise AgentException(dict(self._get_exception_info( result.get('Result', [])), source='execution_plan')) else: results = result.get('Result', []) if not result: return None value = results[-1] if value['IsException']: raise AgentException(dict(self._get_exception_info( value.get('Result', [])), source='command')) else: return value.get('Result') def _process_v2_result(self, result): error_code = result.get('ErrorCode', 0) if not error_code: return result.get('Body') else: body = result.get('Body') or {} err = { 'message': body.get('Message'), 'details': body.get('AdditionalInfo'), 'errorCode': error_code, 'time': result.get('Time') } for attr in ('Message', 'AdditionalInfo'): if attr in body: del body[attr] err['extra'] = body if body else None raise AgentException(err) def _get_array_item(self, array, index): return array[index] if len(array) > index else None def _get_exception_info(self, data): data = data or [] return { 'type': self._get_array_item(data, 0), 'message': self._get_array_item(data, 1), 'command': self._get_array_item(data, 2), 'details': self._get_array_item(data, 3), 'timestamp': datetime.datetime.now().isoformat() } def build_execution_plan(self, template, resources): template = copy.deepcopy(template) if not isinstance(template, dict): raise ValueError('Incorrect execution plan ') format_version = template.get('FormatVersion') if not format_version or format_version.startswith('1.'): return self._build_v1_execution_plan(template, resources) else: return self._build_v2_execution_plan(template, resources) def _generate_stamp(self): stamp = int(time.time() * 10000) if stamp <= self._last_stamp: stamp = self._last_stamp + 1 self._last_stamp = stamp return stamp def _build_v1_execution_plan(self, template, resources): scripts_folder = 'scripts' script_files = template.get('Scripts', []) scripts = [] for script in script_files: script_path = os.path.join(scripts_folder, script) scripts.append(base64.encode_as_text( resources.string(script_path, binary=True), encoding='latin1')) template['Scripts'] = scripts template['Stamp'] = self._generate_stamp() return template def _build_v2_execution_plan(self, template, resources): scripts_folder = 'scripts' plan_id = uuid.uuid4().hex template['ID'] = plan_id template['Stamp'] = self._generate_stamp() if 'Action' not in template: template['Action'] = 'Execute' if 'Files' not in template: template['Files'] = {} files = {} for file_id, file_descr in template['Files'].items(): files[file_descr['Name']] = file_id for name, script in template.get('Scripts', {}).items(): if 'EntryPoint' not in script: raise ValueError('No entry point in script ' + name) if 'Application' == script['Type']: if script['EntryPoint'] not in files: script['EntryPoint'] = self._place_file( scripts_folder, script['EntryPoint'], template, resources, files) else: script['EntryPoint'] = files[script['EntryPoint']] if 'Files' in script: for i, file in enumerate(script['Files']): if self._get_name(file) not in files: script['Files'][i] = self._place_file( scripts_folder, file, template, resources, files) else: script['Files'][i] = files[file] return template def _is_url(self, file): file = self._get_url(file) parts = urllib.parse.urlsplit(file) if not parts.scheme or not parts.netloc: return False else: return True def _get_url(self, file): if isinstance(file, dict): return list(file.values())[0] else: return file def _get_name(self, file): if isinstance(file, dict): name = list(file.keys())[0] else: name = file if self._is_url(name): name = name[name.rindex('/') + 1:len(name)] elif name.startswith('<') and name.endswith('>'): name = name[1: -1] return name def _get_file_value(self, file): if isinstance(file, dict): file = list(file.values())[0] return file def _get_body(self, file, resources, folder): use_base64 = self._is_base64(file) if use_base64: path = os.path.join(folder, file[1: -1]) body = resources.string(path, binary=True) body = base64.encode_as_text(body) + "\n" else: path = os.path.join(folder, file) body = resources.string(path) return body def _is_base64(self, file): return file.startswith('<') and file.endswith('>') def _get_body_type(self, file): return 'Base64' if self._is_base64(file) else 'Text' def _place_file(self, folder, file, template, resources, files): file_value = self._get_file_value(file) name = self._get_name(file) file_id = uuid.uuid4().hex if self._is_url(file_value): template['Files'][file_id] = self._get_file_des_downloadable(file) files[name] = file_id else: template['Files'][file_id] = self._get_file_description( file, resources, folder) files[name] = file_id return file_id def _get_file_des_downloadable(self, file): name = self._get_name(file) file = self._get_file_value(file) return { 'Name': str(name), 'URL': file, 'Type': 'Downloadable' } def _get_file_description(self, file, resources, folder): name = self._get_name(file) file_value = self._get_file_value(file) body_type = self._get_body_type(file_value) body = self._get_body(file_value, resources, folder) return { 'Name': name, 'BodyType': body_type, 'Body': body } ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/engine/system/agent_listener.py0000664000175000017500000000710100000000000022570 0ustar00zuulzuul00000000000000# 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 greenlet from oslo_config import cfg from oslo_log import log as logging from murano.common import exceptions from murano.dsl import dsl from murano.engine.system import common LOG = logging.getLogger(__name__) CONF = cfg.CONF class AgentListenerException(Exception): pass @dsl.name('io.murano.system.AgentListener') class AgentListener(object): def __init__(self, name): self._enabled = not CONF.engine.disable_murano_agent self._results_queue = str('-execution-results-%s' % name.lower()) self._subscriptions = {} self._receive_thread = None def _check_enabled(self): if CONF.engine.disable_murano_agent: LOG.error('Use of murano-agent is disallowed ' 'by the server configuration') raise exceptions.PolicyViolationException( 'Use of murano-agent is disallowed ' 'by the server configuration') @property def enabled(self): return self._enabled def queue_name(self): return self._results_queue def start(self): if CONF.engine.disable_murano_agent: # Noop LOG.debug("murano-agent is disabled by the server") return if self._receive_thread is None: dsl.get_execution_session().on_session_finish( lambda: self.stop()) self._receive_thread = dsl.spawn( self._receive, dsl.get_this().find_owner('io.murano.CloudRegion')) def stop(self): if CONF.engine.disable_murano_agent: # Noop LOG.debug("murano-agent is disabled by the server") return if self._receive_thread is not None: self._receive_thread.kill() try: self._receive_thread.wait() except greenlet.GreenletExit: pass finally: self._receive_thread = None def subscribe(self, message_id, event): self._check_enabled() self._subscriptions[message_id] = event self.start() def unsubscribe(self, message_id): self._check_enabled() self._subscriptions.pop(message_id) def _receive(self, region): with common.create_rmq_client(region) as client: client.declare(self._results_queue, enable_ha=True, ttl=86400000) with client.open(self._results_queue) as subscription: while True: msg = subscription.get_message() if not msg: continue msg.ack() msg_id = msg.body.get('SourceID', msg.id) LOG.debug("Got execution result: id '{msg_id}'" " body '{body}'".format(msg_id=msg_id, body=msg.body)) if msg_id in self._subscriptions: event = self._subscriptions.pop(msg_id) event.send(msg.body) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/engine/system/common.py0000664000175000017500000000162700000000000021064 0ustar00zuulzuul00000000000000# 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 oslo_config import cfg from murano.common.messaging import mqclient CONF = cfg.CONF def create_rmq_client(region): region_config = region().getConfig() rmq_settings = dict(region_config['agentRabbitMq']) rmq_settings['ca_certs'] = CONF.rabbitmq.ca_certs.strip() or None return mqclient.MqClient(**rmq_settings) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/engine/system/heat_stack.py0000664000175000017500000002573300000000000021706 0ustar00zuulzuul00000000000000# 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 copy import json import eventlet import heatclient.client as hclient import heatclient.exc as heat_exc from oslo_config import cfg from oslo_log import log as logging from murano.common import auth_utils from murano.common.helpers import token_sanitizer from murano.dsl import dsl from murano.dsl import helpers from murano.dsl import session_local_storage LOG = logging.getLogger(__name__) CONF = cfg.CONF HEAT_TEMPLATE_VERSION = '2013-05-23' class HeatStackError(Exception): pass @dsl.name('io.murano.system.HeatStack') class HeatStack(object): def __init__(self, name, description=None, region_name=None): self._name = name self._template = None self._parameters = {} self._files = {} self._hot_environment = {} self._applied = True self._description = description self._last_stack_timestamps = (None, None) self._tags = '' self._region_name = region_name self._push_thread = None def _is_push_thread_alive(self): return self._push_thread is not None and not self._push_thread.dead def _kill_push_thread(self): if self._is_push_thread_alive(): self._push_thread.cancel() self._wait_push_thread() def _wait_push_thread(self): if not self._is_push_thread_alive(): return self._push_thread.wait() @staticmethod def _create_client(session, region_name): parameters = auth_utils.get_session_client_parameters( service_type='orchestration', region=region_name, conf='heat', session=session) return hclient.Client('1', **parameters) @property def _client(self): return self._get_client(self._region_name) @staticmethod @session_local_storage.execution_session_memoize def _get_client(region_name): session = auth_utils.get_client_session(conf='heat') return HeatStack._create_client(session, region_name) def _get_token_client(self): ks_session = auth_utils.get_token_client_session(conf='heat') return self._create_client(ks_session, self._region_name) def current(self): if self._template is not None: return self._template try: stack_info = self._client.stacks.get(stack_id=self._name) template = self._client.stacks.template( stack_id='{0}/{1}'.format( stack_info.stack_name, stack_info.id)) self._template = template self._parameters.update( HeatStack._remove_system_params(stack_info.parameters)) self._applied = True return self._template.copy() except heat_exc.HTTPNotFound: self._applied = True self._template = {} self._parameters.clear() return {} def parameters(self): self.current() return self._parameters.copy() def reload(self): self._template = None self._parameters.clear() return self.current() def set_template(self, template): self._template = template self._parameters.clear() self._applied = False def set_parameters(self, parameters): self._parameters = parameters self._applied = False def set_files(self, files): self._files = files self._applied = False def set_hot_environment(self, hot_environment): self._hot_environment = hot_environment self._applied = False def update_template(self, template): template_version = template.get('heat_template_version', HEAT_TEMPLATE_VERSION) if template_version != HEAT_TEMPLATE_VERSION: err_msg = ("Currently only heat_template_version %s " "is supported." % HEAT_TEMPLATE_VERSION) raise HeatStackError(err_msg) current = self.current() self._template = helpers.merge_dicts(self._template, template) self._applied = self._template == current and self._applied @staticmethod def _remove_system_params(parameters): return dict((k, v) for k, v in parameters.items() if not k.startswith('OS::')) def _get_status(self): status = [None] def status_func(state_value): status[0] = state_value return True self._wait_state(status_func) return status[0] def _wait_state(self, status_func, wait_progress=False): tries = 4 delay = 1 while tries > 0: while True: try: stack_info = self._client.stacks.get( stack_id=self._name) status = stack_info.stack_status tries = 4 delay = 1 except heat_exc.HTTPNotFound: stack_info = None status = 'NOT_FOUND' except Exception: tries -= 1 delay *= 2 if not tries: raise eventlet.sleep(delay) break if 'IN_PROGRESS' in status: eventlet.sleep(2) continue last_stack_timestamps = self._last_stack_timestamps self._last_stack_timestamps = (None, None) if not stack_info \ else(stack_info.creation_time, stack_info.updated_time) if (wait_progress and last_stack_timestamps == self._last_stack_timestamps and last_stack_timestamps != (None, None)): eventlet.sleep(2) continue if not status_func(status): reason = ': {0}'.format( stack_info.stack_status_reason) if stack_info else '' raise EnvironmentError( "Unexpected stack state {0}{1}".format(status, reason)) try: return dict([(t['output_key'], t['output_value']) for t in stack_info.outputs]) except Exception: return {} return {} def output(self): self._wait_push_thread() return self._wait_state(lambda status: True) def _push(self, object_store=None): template = copy.deepcopy(self._template) s_template = token_sanitizer.TokenSanitizer().sanitize(template) LOG.debug('Pushing: {template}'.format( template=json.dumps(s_template))) object_store = object_store or helpers.get_object_store() while True: try: with helpers.with_object_store(object_store): current_status = self._get_status() resources = template.get('Resources') or template.get( 'resources') if current_status == 'NOT_FOUND': if resources is not None: token_client = self._get_token_client() token_client.stacks.create( stack_name=self._name, parameters=self._parameters, template=template, files=self._files, environment=self._hot_environment, disable_rollback=True, tags=self._tags) self._wait_state( lambda status: status == 'CREATE_COMPLETE') else: if resources is not None: self._client.stacks.update( stack_id=self._name, parameters=self._parameters, files=self._files, environment=self._hot_environment, template=template, disable_rollback=True, tags=self._tags) self._wait_state( lambda status: status == 'UPDATE_COMPLETE', True) else: self.delete() except heat_exc.HTTPConflict as e: LOG.warning('Conflicting operation: {msg}'.format(msg=e)) eventlet.sleep(3) else: break self._applied = self._template == template def push(self, is_async=False): if self._applied or self._template is None: return self._tags = ','.join(CONF.heat.stack_tags) if 'heat_template_version' not in self._template: self._template['heat_template_version'] = HEAT_TEMPLATE_VERSION if 'description' not in self._template and self._description: self._template['description'] = self._description self._kill_push_thread() if is_async: if self._push_thread is None: def cleanup(): try: self._wait_push_thread() finally: self._push_thread = None dsl.get_execution_session().on_session_finish(cleanup) self._push_thread =\ eventlet.greenthread.spawn_after( 1, self._push, helpers.get_object_store()) else: self._push() def delete(self): self._kill_push_thread() while True: try: if not self.current(): return self._wait_state(lambda s: True) self._client.stacks.delete(stack_id=self._name) self._wait_state( lambda status: status in ('DELETE_COMPLETE', 'NOT_FOUND'), wait_progress=True) except heat_exc.NotFound: LOG.warning('Stack {stack_name} already deleted?' .format(stack_name=self._name)) break except heat_exc.HTTPConflict as e: LOG.warning('Conflicting operation: {msg}'.format(msg=e)) eventlet.sleep(3) else: break self._template = {} self._applied = True ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/engine/system/instance_reporter.py0000664000175000017500000000454100000000000023320 0ustar00zuulzuul00000000000000# 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 oslo_config import cfg import oslo_messaging as messaging from murano.common import rpc from murano.common import uuidutils from murano.dsl import dsl CONF = cfg.CONF UNCLASSIFIED = 0 APPLICATION = 100 OS_INSTANCE = 200 @dsl.name('io.murano.system.InstanceNotifier') class InstanceReportNotifier(object): def __init__(self, environment): if not rpc.initialized(): rpc.init() self._notifier = messaging.Notifier( rpc.NOTIFICATION_TRANSPORT, publisher_id=uuidutils.generate_uuid(), topics=['murano']) self._environment_id = environment.id def _track_instance(self, instance, instance_type, type_title, unit_count): payload = { 'instance': instance.id, 'environment': self._environment_id, 'instance_type': instance_type, 'type_name': instance.type.name, 'type_title': type_title, 'unit_count': unit_count } self._notifier.info({}, 'murano.track_instance', payload) def _untrack_instance(self, instance, instance_type): payload = { 'instance': instance.id, 'environment': self._environment_id, 'instance_type': instance_type, } self._notifier.info({}, 'murano.untrack_instance', payload) def track_application(self, instance, title=None, unit_count=None): self._track_instance(instance, APPLICATION, title, unit_count) def untrack_application(self, instance): self._untrack_instance(instance, APPLICATION) def track_cloud_instance(self, instance): self._track_instance(instance, OS_INSTANCE, None, 1) def untrack_cloud_instance(self, instance): self._untrack_instance(instance, OS_INSTANCE) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/engine/system/logger.py0000664000175000017500000001053000000000000021044 0ustar00zuulzuul00000000000000# 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 oslo_log import log as logging from yaql.language import specs from yaql.language import yaqltypes from murano.dsl import dsl NAME_TEMPLATE = u'applications.{0}' inject_format = specs.inject( '_Logger__yaql_format_function', yaqltypes.Delegate('format')) @dsl.name('io.murano.system.Logger') class Logger(object): """Logger object for MuranoPL. Instance of this object returned by 'logger' YAQL function and should not be instantiated directly """ def __init__(self, logger_name): self._underlying_logger = logging.getLogger( NAME_TEMPLATE.format(logger_name)) @specs.parameter('_Logger__message', yaqltypes.String()) @inject_format def trace(__self, __yaql_format_function, __message, *args, **kwargs): __self._log(__self._underlying_logger.trace, __yaql_format_function, __message, args, kwargs) @specs.parameter('_Logger__message', yaqltypes.String()) @inject_format def debug(__self, __yaql_format_function, __message, *args, **kwargs): __self._log(__self._underlying_logger.debug, __yaql_format_function, __message, args, kwargs) @specs.parameter('_Logger__message', yaqltypes.String()) @inject_format def info(__self, __yaql_format_function, __message, *args, **kwargs): __self._log(__self._underlying_logger.info, __yaql_format_function, __message, args, kwargs) @specs.parameter('_Logger__message', yaqltypes.String()) @inject_format def warning(__self, __yaql_format_function, __message, *args, **kwargs): __self._log(__self._underlying_logger.warning, __yaql_format_function, __message, args, kwargs) @specs.parameter('_Logger__message', yaqltypes.String()) @inject_format def error(__self, __yaql_format_function, __message, *args, **kwargs): __self._log(__self._underlying_logger.error, __yaql_format_function, __message, args, kwargs) @specs.parameter('_Logger__message', yaqltypes.String()) @inject_format def critical(__self, __yaql_format_function, __message, *args, **kwargs): __self._log(__self._underlying_logger.critical, __yaql_format_function, __message, args, kwargs) @specs.parameter('_Logger__message', yaqltypes.String()) @inject_format def exception(__self, __yaql_format_function, __exc, __message, *args, **kwargs): """Print error message and stacktrace""" stack_trace_message = u'\n'.join([ __self._format_without_exceptions( __yaql_format_function, __message, args, kwargs), __exc['stackTrace']().toString() ]) __self._underlying_logger.error(stack_trace_message) def _format_without_exceptions(self, format_function, message, args, kwargs): """Wrap YAQL function "format" to suppress exceptions Wrap YAQL function "format" to suppress exceptions that may be raised when message cannot be formatted due to invalid parameters provided We do not want to break program workflow even when formatting parameters are incorrect """ try: message = format_function(message, *args, **kwargs) except (IndexError, KeyError): # NOTE(akhivin): we do not want break program workflow # even formatting parameters are incorrect self._underlying_logger.warning( u'Can not format string: {0}'.format(message)) return message def _log(self, log_function, yaql_format_function, message, args, kwargs): log_function( self._format_without_exceptions( yaql_format_function, message, args, kwargs)) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/engine/system/metadef_browser.py0000664000175000017500000000450300000000000022740 0ustar00zuulzuul00000000000000# 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 glanceclient.v2.client as gclient from oslo_config import cfg from murano.common import auth_utils from murano.dsl import dsl from murano.dsl import helpers from murano.dsl import session_local_storage CONF = cfg.CONF @dsl.name('io.murano.system.MetadefBrowser') class MetadefBrowser(object): def __init__(self, this, region_name=None, cache=True): session = helpers.get_execution_session() self._project_id = session.project_id self._region = this.find_owner('io.murano.CloudRegion') self._region_name = region_name self._cache = cache self._namespaces = {} self._objects = {} @staticmethod @session_local_storage.execution_session_memoize def _get_client(region_name): return gclient.Client(**auth_utils.get_session_client_parameters( service_type='image', region=region_name, conf='glance' )) @property def _client(self): region = self._region_name or ( None if self._region is None else self._region['name']) return self._get_client(region) def get_namespaces(self, resource_type): if not self._cache or resource_type not in self._namespaces: nss = list(self._client.metadefs_namespace.list( resource_type=resource_type)) self._namespaces[resource_type] = nss return nss else: return self._namespaces[resource_type] def get_objects(self, namespace): if not self._cache or namespace not in self._objects: objects = list(self._client.metadefs_object.list( namespace=namespace)) self._objects[namespace] = objects return objects else: return self._objects[namespace] ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/engine/system/net_explorer.py0000664000175000017500000002041700000000000022300 0ustar00zuulzuul00000000000000# 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 math import netaddr from netaddr.strategy import ipv4 import neutronclient.v2_0.client as nclient from oslo_config import cfg from oslo_log import log as logging from oslo_utils import netutils from oslo_utils import uuidutils import tenacity from murano.common import auth_utils from murano.common import exceptions as exc from murano.dsl import dsl from murano.dsl import helpers from murano.dsl import session_local_storage CONF = cfg.CONF LOG = logging.getLogger(__name__) @dsl.name('io.murano.system.NetworkExplorer') class NetworkExplorer(object): def __init__(self, this, region_name=None): session = helpers.get_execution_session() self._project_id = session.project_id self._settings = CONF.networking self._available_cidrs = self._generate_possible_cidrs() self._region = this.find_owner('io.murano.CloudRegion') self._region_name = region_name @staticmethod @session_local_storage.execution_session_memoize def _get_client(region_name): return nclient.Client(**auth_utils.get_session_client_parameters( service_type='network', region=region_name, conf='neutron' )) @property def _client(self): region = self._region_name or ( None if self._region is None else self._region['name']) return self._get_client(region) # NOTE(starodubcevna): to avoid simultaneous router requests we use retry # decorator with random delay 1-10 seconds between attempts and maximum # delay time 30 seconds. @tenacity.retry( retry=tenacity.retry_if_exception_type(exc.RouterInfoException), stop=tenacity.stop_after_delay(30), wait=tenacity.wait_random(min=1, max=10), reraise=True) def get_default_router(self): router_name = self._settings.router_name routers = self._client.list_routers( tenant_id=self._project_id, name=router_name).get('routers') if len(routers) == 0: LOG.debug('Router {name} not found'.format(name=router_name)) if self._settings.create_router: LOG.debug('Attempting to create Router {router}'. format(router=router_name)) external_network = self._settings.external_network kwargs = {'id': external_network} \ if uuidutils.is_uuid_like(external_network) \ else {'name': external_network} networks = self._client.list_networks(**kwargs).get('networks') ext_nets = list(filter(lambda n: n['router:external'], networks)) if len(ext_nets) == 0: raise KeyError('Router %s could not be created, ' 'no external network found' % router_name) nid = ext_nets[0]['id'] body_data = { 'router': { 'name': router_name, 'external_gateway_info': { 'network_id': nid }, 'admin_state_up': True, } } router = self._client.create_router( body=body_data).get('router') LOG.info('Created router: {id}'.format(id=router['id'])) return router['id'] else: raise KeyError('Router %s was not found' % router_name) else: if routers[0]['external_gateway_info'] is None: raise exc.RouterInfoException('Please set external gateway for' ' the router %s ' % router_name) router_id = routers[0]['id'] return router_id def get_available_cidr(self, router_id, net_id, ip_version=4): """Uses hash of network IDs to minimize the collisions Different nets will attempt to pick different cidrs out of available range. If the cidr is taken will pick another one. """ taken_cidrs = self._get_cidrs_taken_by_router(router_id) id_hash = hash(net_id) num_fails = 0 available_ipv6_cidrs = [] if ip_version == 6: for cidr in self._available_cidrs: available_ipv6_cidrs.append(cidr.ipv6()) self._available_cidrs = available_ipv6_cidrs while num_fails < len(self._available_cidrs): cidr = self._available_cidrs[ (id_hash + num_fails) % len(self._available_cidrs)] if any(self._cidrs_overlap(cidr, taken_cidr) for taken_cidr in taken_cidrs): num_fails += 1 else: return str(cidr) return None def get_default_dns(self, ip_version=4): dns_list = self._settings.default_dns valid_dns = [] for ip in dns_list: if ip_version == 6 and netutils.is_valid_ipv6(ip): valid_dns.append(ip) elif ip_version == 4 and netutils.is_valid_ipv4(ip): valid_dns.append(ip) else: LOG.warning('{0} is not a vaild IPV{1} address, ' 'ingore...'.format(ip, ip_version)) return valid_dns def get_external_network_id_for_router(self, router_id): router = self._client.show_router(router_id).get('router') if not router or 'external_gateway_info' not in router: return None return router['external_gateway_info'].get('network_id') def get_external_network_id_for_network(self, network_id): network = self._client.show_network(network_id).get('network') if network.get('router:external', False): return network_id # Get router interfaces of the network router_ports = self._client.list_ports( **{'device_owner': 'network:router_interface', 'network_id': network_id}).get('ports') # For each router this network is connected to # check if the router has external_gateway set for router_port in router_ports: ext_net_id = self.get_external_network_id_for_router( router_port.get('device_id')) if ext_net_id: return ext_net_id return None def _get_cidrs_taken_by_router(self, router_id): if not router_id: return [] ports = self._client.list_ports(device_id=router_id)['ports'] subnet_ids = [] for port in ports: for fixed_ip in port['fixed_ips']: subnet_ids.append(fixed_ip['subnet_id']) all_subnets = self._client.list_subnets()['subnets'] filtered_cidrs = [netaddr.IPNetwork(subnet['cidr']) for subnet in all_subnets if subnet['id'] in subnet_ids] return filtered_cidrs @staticmethod def _cidrs_overlap(cidr1, cidr2): return (cidr1 in cidr2) or (cidr2 in cidr1) def _generate_possible_cidrs(self): bits_for_envs = int( math.ceil(math.log(self._settings.max_environments, 2))) bits_for_hosts = int(math.ceil(math.log(self._settings.max_hosts, 2))) width = ipv4.width mask_width = width - bits_for_hosts - bits_for_envs net = netaddr.IPNetwork( '{0}/{1}'.format(self._settings.env_ip_template, mask_width)) return list(net.subnet(width - bits_for_hosts)) def list_networks(self): return self._client.list_networks()['networks'] def list_subnetworks(self): return self._client.list_subnets()['subnets'] def list_ports(self): return self._client.list_ports()['ports'] def list_neutron_extensions(self): return self._client.list_extensions()['extensions'] ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/engine/system/project.py0000664000175000017500000000300600000000000021233 0ustar00zuulzuul00000000000000# 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 murano.common import auth_utils from murano.dsl import dsl from murano.dsl import helpers @dsl.name('io.murano.Project') class Project(object): @classmethod def get_current(cls): fields = auth_utils.get_project( helpers.get_execution_session().project_id) return cls._to_object(fields) @classmethod def get_environment_owner(cls): fields = auth_utils.get_project( helpers.get_execution_session().environment_owner_project_id) return cls._to_object(fields) @staticmethod def _to_object(fields): for field in ('links', 'parent_id', 'enabled'): fields.pop(field, None) obj_def = { 'id': fields.pop('id'), 'name': fields.pop('name'), 'domain': fields.pop('domain_id', 'Default'), 'description': fields.pop('description', None), 'extra': fields } return dsl.new(obj_def) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/engine/system/resource_manager.py0000664000175000017500000000674100000000000023117 0ustar00zuulzuul00000000000000# 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 as jsonlib import yaml as yamllib from yaql.language import specs from yaql.language import yaqltypes from murano.dsl import constants from murano.dsl import dsl from murano.dsl import dsl_types from murano.dsl import helpers if hasattr(yamllib, 'CSafeLoader'): yaml_loader = yamllib.CSafeLoader else: yaml_loader = yamllib.SafeLoader def _construct_yaml_str(self, node): # Override the default string handling function # to always return unicode objects return self.construct_scalar(node) yaml_loader.add_constructor(u'tag:yaml.org,2002:str', _construct_yaml_str) # Unquoted dates like 2013-05-23 in yaml files get loaded as objects of type # datetime.data which causes problems in API layer when being processed by # oslo.serialization.jsonutils. Therefore, make unicode string out of # timestamps until jsonutils can handle dates. yaml_loader.add_constructor(u'tag:yaml.org,2002:timestamp', _construct_yaml_str) @dsl.name('io.murano.system.Resources') class ResourceManager(object): def __init__(self, context): murano_class = helpers.get_type(helpers.get_caller_context(context)) self._package = murano_class.package @staticmethod @specs.parameter('owner', dsl.MuranoTypeParameter(nullable=True)) @specs.inject('receiver', yaqltypes.Receiver()) @specs.meta(constants.META_NO_TRACE, True) def string(receiver, name, owner=None, binary=False): path = ResourceManager._get_package(owner, receiver).get_resource(name) mode = 'rb' if binary else 'rU' with open(path, mode) as file: return file.read() @classmethod @specs.parameter('owner', dsl.MuranoTypeParameter(nullable=True)) @specs.inject('receiver', yaqltypes.Receiver()) @specs.meta(constants.META_NO_TRACE, True) def json(cls, receiver, name, owner=None): return jsonlib.loads(cls.string(receiver, name, owner)) @classmethod @specs.parameter('owner', dsl.MuranoTypeParameter(nullable=True)) @specs.inject('receiver', yaqltypes.Receiver()) @specs.meta(constants.META_NO_TRACE, True) def yaml(cls, receiver, name, owner=None): # NOTE(kzaitsev, Sam Pilla) Bandit will raise an issue here, # because it thinks that we're using an unsafe yaml.load. # However we're passing a SafeLoader here # (see definition of `yaml_loader` in this file; L27-30) # so a `nosec` was added to ignore the false positive report. return yamllib.load( # nosec cls.string(receiver, name, owner), Loader=yaml_loader) @staticmethod def _get_package(owner, receiver): if owner is None: if isinstance(receiver, dsl_types.MuranoObjectInterface): return receiver.extension._package murano_class = helpers.get_type(helpers.get_caller_context()) else: murano_class = owner.type return murano_class.package ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/engine/system/status_reporter.py0000664000175000017500000000735700000000000023047 0ustar00zuulzuul00000000000000# 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 datetime import datetime import socket from oslo_config import cfg from oslo_log import log as logging import oslo_messaging as messaging from murano.common import rpc from murano.common import uuidutils from murano.dsl import dsl CONF = cfg.CONF LOG = logging.getLogger(__name__) @dsl.name('io.murano.system.StatusReporter') class StatusReporter(object): def __init__(self, environment): if not rpc.initialized(): rpc.init() self._notifier = messaging.Notifier( rpc.NOTIFICATION_TRANSPORT, publisher_id=uuidutils.generate_uuid(), topics=['murano']) if isinstance(environment, str): self._environment_id = environment else: self._environment_id = environment.id def _report(self, instance, msg, details=None, level='info'): body = { 'id': (self._environment_id if instance is None else instance.id), 'text': msg, 'details': details, 'level': level, 'environment_id': self._environment_id, 'timestamp': datetime.utcnow().isoformat() } self._notifier.info({}, 'murano.report_notification', body) def report(self, instance, msg): self._report(instance, msg) def report_error_(self, instance, msg): self._report(instance, msg, None, 'error') @dsl.name('report_error') def report_error(self, instance, msg): self._report(instance, msg, None, 'error') class Notification(object): def __init__(self): if not CONF.stats.env_audit_enabled: return if not rpc.initialized(): rpc.init() self._notifier = messaging.Notifier( rpc.NOTIFICATION_TRANSPORT, publisher_id=('murano.%s' % socket.gethostname()), driver='messaging') def _report(self, event_type, environment, level='info'): if not CONF.stats.env_audit_enabled: return if 'deleted' in environment: deleted_at = environment['deleted'].isoformat() else: deleted_at = None body = { 'id': environment['id'], 'level': level, 'environment_id': environment['id'], 'tenant_id': environment['tenant_id'], 'created_at': environment.get('created').isoformat(), 'deleted_at': deleted_at, 'launched_at': None, 'timestamp': datetime.utcnow().isoformat() } optional_fields = ("deployment_started", "deployment_finished") for f in optional_fields: body[f] = environment.get(f, None) LOG.debug("Sending out notification, type=%s, body=%s, level=%s", event_type, body, level) self._notifier.info({}, 'murano.%s' % event_type, body) def report(self, event_type, environment): self._report(event_type, environment) def report_error(self, event_type, environment): self._report(event_type, environment, 'error') NOTIFIER = None def get_notifier(): global NOTIFIER if not NOTIFIER: NOTIFIER = Notification() return NOTIFIER ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/engine/system/system_objects.py0000664000175000017500000000353200000000000022626 0ustar00zuulzuul00000000000000# 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 murano.engine.system import agent from murano.engine.system import agent_listener from murano.engine.system import heat_stack from murano.engine.system import instance_reporter from murano.engine.system import logger from murano.engine.system import metadef_browser from murano.engine.system import net_explorer from murano.engine.system import project from murano.engine.system import resource_manager from murano.engine.system import status_reporter from murano.engine.system import test_fixture from murano.engine.system import user from murano.engine.system import workflowclient def register(package): package.register_class(agent.Agent) package.register_class(agent_listener.AgentListener) package.register_class(heat_stack.HeatStack) package.register_class(resource_manager.ResourceManager) package.register_class(instance_reporter.InstanceReportNotifier) package.register_class(status_reporter.StatusReporter) package.register_class(net_explorer.NetworkExplorer) package.register_class(logger.Logger) package.register_class(test_fixture.TestFixture) package.register_class(workflowclient.MistralClient) package.register_class(metadef_browser.MetadefBrowser) package.register_class(user.User) package.register_class(project.Project) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/engine/system/test_fixture.py0000664000175000017500000000321400000000000022313 0ustar00zuulzuul00000000000000# 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 murano.dsl import dsl from murano.dsl import helpers @dsl.name('io.murano.test.TestFixture') class TestFixture(object): def __init__(self): self._test_case = testtools.TestCase('__init__') def load(self, model): exc = helpers.get_executor() return exc.load(model) def finish_env(self): session = helpers.get_execution_session() session.finish() def start_env(self): session = helpers.get_execution_session() session.start() def assert_equal(self, expected, observed, message=None): self._test_case.assertEqual(expected, observed, message) def assert_true(self, expr, message=None): self._test_case.assertTrue(expr, message) def assert_false(self, expr, message=None): self._test_case.assertFalse(expr, message) def assert_in(self, needle, haystack, message=None): self._test_case.assertIn(needle, haystack, message) def assert_not_in(self, needle, haystack, message=None): self._test_case.assertNotIn(needle, haystack, message) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/engine/system/user.py0000664000175000017500000000300200000000000020537 0ustar00zuulzuul00000000000000# 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 murano.common import auth_utils from murano.dsl import dsl from murano.dsl import helpers @dsl.name('io.murano.User') class User(object): @classmethod def get_current(cls): fields = auth_utils.get_user(helpers.get_execution_session().user_id) return cls._to_object(fields) @classmethod def get_environment_owner(cls): fields = auth_utils.get_user( helpers.get_execution_session().environment_owner_user_id) return cls._to_object(fields) @staticmethod def _to_object(fields): fields = dict(fields) for field in ('links', 'enabled', 'default_project_id'): fields.pop(field, None) obj_def = { 'id': fields.pop('id'), 'name': fields.pop('name'), 'domain': fields.pop('domain_id', 'Default'), 'email': fields.pop('email', None), 'extra': fields } return dsl.new(obj_def) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/engine/system/workflowclient.py0000664000175000017500000001017100000000000022637 0ustar00zuulzuul00000000000000# Copyright (c) 2015 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. import json import eventlet try: from mistralclient.api import client as mistralcli except ImportError: mistralcli = None from oslo_config import cfg from oslo_log import log as logging from murano.common import auth_utils from murano.dsl import dsl from murano.dsl import session_local_storage CONF = cfg.CONF LOG = logging.getLogger(__name__) class MistralError(Exception): pass @dsl.name('io.murano.system.MistralClient') class MistralClient(object): def __init__(self, this, region_name=None): self._owner = this.find_owner('io.murano.Environment') self._region_name = region_name @property def _client(self): region = self._region_name or ( None if self._owner is None else self._owner['region']) return self._create_client(region) @staticmethod @session_local_storage.execution_session_memoize def _create_client(region): if not mistralcli: LOG.warning("Mistral client is not available") raise ImportError("Import mistralcliet error") mistral_settings = CONF.mistral endpoint_type = mistral_settings.endpoint_type service_type = mistral_settings.service_type session = auth_utils.get_client_session() mistral_url = mistral_settings.url or session.get_endpoint( service_type=service_type, endpoint_type=endpoint_type, region_name=region) auth_ref = session.auth.get_access(session) # TODO(gyurco): use auth_utils.get_session_client_parameters return mistralcli.client( mistral_url=mistral_url, project_id=auth_ref.project_id, endpoint_type=endpoint_type, service_type=service_type, auth_token=auth_ref.auth_token, user_id=auth_ref.user_id, insecure=mistral_settings.insecure, cacert=mistral_settings.cafile ) def upload(self, definition): self._client.workflows.create(definition) def run(self, name, timeout=600, inputs=None, params=None): execution = self._client.executions.create( workflow_identifier=name, workflow_input=inputs, params=params) # For the fire and forget functionality - when we do not want to wait # for the result of the run. if timeout == 0: return execution.id state = execution.state try: # While the workflow is running we continue to wait until timeout. with eventlet.timeout.Timeout(timeout): while state not in ('ERROR', 'SUCCESS'): eventlet.sleep(2) execution = self._client.executions.get(execution.id) state = execution.state except eventlet.timeout.Timeout: error_message = ( 'Mistral run timed out. Execution id: {0}.').format( execution.id) raise MistralError(error_message) if state == 'ERROR': error_message = ('Mistral execution completed with ERROR.' ' Execution id: {0}. Output: {1}').format( execution.id, execution.output) raise MistralError(error_message) # Load the JSON we got from Mistral client to dictionary. output = json.loads(execution.output) # Clean the returned dictionary from unnecessary data. # We want to keep only flow level outputs. output.pop('openstack', None) output.pop('__execution', None) output.pop('task', None) return output ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/engine/system/yaql_functions.py0000664000175000017500000001742500000000000022635 0ustar00zuulzuul00000000000000# 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 abc import random import re import string import time import jsonpatch import jsonpointer from oslo_config import cfg as oslo_cfg from oslo_log import log as logging from oslo_serialization import base64 from yaql.language import specs from yaql.language import utils from yaql.language import yaqltypes from murano.common import config as cfg from murano.dsl import constants from murano.dsl import dsl from murano.dsl import helpers from murano.dsl import yaql_integration from castellan.common import exception as castellan_exception from castellan.common import utils as castellan_utils from castellan import key_manager from castellan import options LOG = logging.getLogger(__name__) _random_string_counter = None @specs.parameter('value', yaqltypes.String()) @specs.extension_method def base64encode(value): return base64.encode_as_text(value) @specs.parameter('value', yaqltypes.String()) @specs.extension_method def base64decode(value): return base64.decode_as_text(value) @specs.parameter('collection', yaqltypes.Iterable()) @specs.parameter('composer', yaqltypes.Lambda()) @specs.extension_method def pselect(collection, composer): return helpers.parallel_select(collection, composer) @specs.parameter('mappings', abc.Mapping) @specs.extension_method def bind(obj, mappings): if isinstance(obj, str) and obj.startswith('$'): value = _convert_macro_parameter(obj[1:], mappings) if value is not None: return value elif utils.is_sequence(obj): return [bind(t, mappings) for t in obj] elif isinstance(obj, abc.Mapping): result = {} for key, value in obj.items(): result[bind(key, mappings)] = bind(value, mappings) return result elif isinstance(obj, str) and obj.startswith('$'): value = _convert_macro_parameter(obj[1:], mappings) if value is not None: return value return obj def _convert_macro_parameter(macro, mappings): replaced = [False] def replace(match): replaced[0] = True return str(mappings.get(match.group(1))) result = re.sub('{(\\w+?)}', replace, macro) if replaced[0]: return result else: return mappings[macro] @specs.parameter('group', yaqltypes.String()) @specs.parameter('setting', yaqltypes.String()) @specs.parameter('read_as_file', bool) def config(group, setting, read_as_file=False): config_value = cfg.CONF[group][setting] if read_as_file: with open(config_value) as target_file: return target_file.read() else: return config_value @specs.parameter('setting', yaqltypes.String()) @specs.name('config') def config_default(setting): return cfg.CONF[setting] @specs.parameter('string', yaqltypes.String()) @specs.parameter('start', int) @specs.parameter('length', int) @specs.inject('delegate', yaqltypes.Delegate('substring', method=True)) @specs.extension_method def substr(delegate, string, start, length=-1): return delegate(string, start, length) @specs.extension_method def patch_(engine, obj, patch): if not isinstance(patch, tuple): patch = (patch,) patch = dsl.to_mutable(patch, engine) patch = jsonpatch.JsonPatch(patch) try: obj = dsl.to_mutable(obj, engine) return patch.apply(obj, in_place=True) except jsonpointer.JsonPointerException: return obj def _int2base(x, base): """Converts decimal integers into another number base Converts decimal integers into 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 random_name(): """Replace '#' char in pattern with supplied number Replace '#' char in pattern with supplied number. If no pattern is supplied, generate a 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 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 = _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 = _int2base(counter, 36) _random_string_counter = (counter + 1) % 1296 return prefix + timestamp + suffix @specs.parameter('collection', yaqltypes.Iterable()) @specs.parameter('default', nullable=True) @specs.extension_method def first_or_default(collection, default=None): try: return next(iter(collection)) except StopIteration: return default @specs.parameter('logger_name', yaqltypes.String(True)) def logger(context, logger_name): """Instantiate Logger""" log = yaql_integration.call_func( context, 'new', 'io.murano.system.Logger', logger_name=logger_name) return log @specs.parameter('value', yaqltypes.String()) @specs.extension_method def decrypt_data(value): options.set_defaults(oslo_cfg.CONF, barbican_endpoint_type='internal') manager = key_manager.API() try: context = castellan_utils.credential_factory(conf=cfg.CONF) except castellan_exception.AuthTypeInvalidError as e: LOG.exception(e) LOG.error("Castellan must be correctly configured in order to use " "decryptData()") raise try: data = manager.get(context, value).get_encoded() except castellan_exception.KeyManagerError as e: LOG.exception(e) raise return data @helpers.memoize def get_context(runtime_version): context = yaql_integration.create_empty_context() context.register_function(base64decode) context.register_function(base64encode) context.register_function(pselect) context.register_function(bind) context.register_function(random_name) context.register_function(patch_) context.register_function(logger) context.register_function(decrypt_data, 'decryptData') if runtime_version <= constants.RUNTIME_VERSION_1_1: context.register_function(substr) context.register_function(first_or_default) root_context = yaql_integration.create_context(runtime_version) for t in ('to_lower', 'to_upper', 'trim', 'join', 'split', 'starts_with', 'ends_with', 'matches', 'replace', 'flatten'): for spec in utils.to_extension_method(t, root_context): context.register_function(spec) return context @helpers.memoize def get_restricted_context(): context = yaql_integration.create_empty_context() context.register_function(config) context.register_function(config_default) return context ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/engine/yaql_yaml_loader.py0000664000175000017500000000525000000000000021562 0ustar00zuulzuul00000000000000# 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 yaml import yaml.composer import yaml.constructor from murano.dsl import dsl_types from murano.dsl import helpers from murano.dsl import yaql_expression @helpers.memoize def get_loader(version): version = helpers.parse_version(version) class MuranoPlDict(dict): pass class YaqlExpression(yaql_expression.YaqlExpression): @staticmethod def match(expr): return yaql_expression.YaqlExpression.is_expression(expr, version) def load(contents, file_id): def build_position(node): return dsl_types.ExpressionFilePosition( file_id, node.start_mark.line + 1, node.start_mark.column + 1, node.end_mark.line + 1, node.end_mark.column + 1) class MuranoPlYamlConstructor(yaml.constructor.SafeConstructor): def construct_yaml_map(self, node): data = MuranoPlDict() data.source_file_position = build_position(node) yield data value = self.construct_mapping(node) data.update(value) class YaqlYamlLoader(yaml.SafeLoader, MuranoPlYamlConstructor): pass YaqlYamlLoader.add_constructor( u'tag:yaml.org,2002:map', MuranoPlYamlConstructor.construct_yaml_map) # workaround for PyYAML bug: http://pyyaml.org/ticket/221 resolvers = {} for k, v in yaml.SafeLoader.yaml_implicit_resolvers.items(): resolvers[k] = v[:] YaqlYamlLoader.yaml_implicit_resolvers = resolvers def yaql_constructor(loader, node): value = loader.construct_scalar(node) result = yaql_expression.YaqlExpression(value, version) result.source_file_position = build_position(node) return result YaqlYamlLoader.add_constructor(u'!yaql', yaql_constructor) YaqlYamlLoader.add_implicit_resolver(u'!yaql', YaqlExpression, None) return list(filter( lambda t: t, yaml.load_all(contents, Loader=YaqlYamlLoader)) ) return load ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1696417899.805181 murano-16.0.0/murano/hacking/0000775000175000017500000000000000000000000016027 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/hacking/__init__.py0000664000175000017500000000000000000000000020126 0ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/hacking/checks.py0000664000175000017500000000437500000000000017652 0ustar00zuulzuul00000000000000# Copyright (c) 2015 Intel, Inc. # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. """ Guidelines for writing new hacking checks - Use only for Murano specific tests. OpenStack general tests should be submitted to the common 'hacking' module. - Pick numbers in the range M3xx. Find the current test with the highest allocated number and then pick the next value. If nova has an N3xx code for that test, use the same number. - Keep the test method code in the source file ordered based on the M3xx value. - List the new rule in the top level HACKING.rst file - Add test cases for each new rule to /tests/unit/test_hacking.py """ import re from hacking import core mutable_default_args = re.compile(r"^\s*def .+\((.+=\{\}|.+=\[\])") assert_equal_end_with_none_re = re.compile( r"(.)*assertEqual\((\w|\.|\'|\"|\[|\])+, None\)") assert_equal_start_with_none_re = re.compile( r"(.)*assertEqual\(None, (\w|\.|\'|\"|\[|\])+\)") @core.flake8ext def assert_equal_none(logical_line): """Check for assertEqual(A, None) or assertEqual(None, A) sentences M318 """ msg = ("M318: assertEqual(A, None) or assertEqual(None, A) " "sentences not allowed") res = (assert_equal_start_with_none_re.match(logical_line) or assert_equal_end_with_none_re.match(logical_line)) if res: yield (0, msg) @core.flake8ext def no_mutable_default_args(logical_line): msg = "M322: Method's default argument shouldn't be mutable!" if mutable_default_args.match(logical_line): yield (0, msg) @core.flake8ext def check_no_basestring(logical_line): if re.search(r"\bbasestring\b", logical_line): msg = ("M326: basestring is not Python3 usage, use " "str instead.") yield(0, msg) ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1696417899.805181 murano-16.0.0/murano/httpd/0000775000175000017500000000000000000000000015546 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/httpd/__init__.py0000664000175000017500000000000000000000000017645 0ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/httpd/murano_api.py0000664000175000017500000000325500000000000020257 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. """WSGI script for murano-api. Script for running murano-api under Apache2. """ from oslo_config import cfg import oslo_i18n as i18n from oslo_log import log as logging from murano.api.v1 import request_statistics from murano.common import app_loader from murano.common import config from murano.common.i18n import _ from murano.common import policy from murano.common import server def init_application(): i18n.enable_lazy() LOG = logging.getLogger('murano.api') logging.register_options(cfg.CONF) # NOTE(hberaud): Call reset to ensure the ConfigOpts object doesn't # already contain registered options if the app is reloaded. cfg.CONF.reset() cfg.CONF(project='murano') logging.setup(cfg.CONF, 'murano') config.set_middleware_defaults() request_statistics.init_stats() policy.init() server.get_notification_listener().start() server.get_rpc_server().start() port = cfg.CONF.bind_port host = cfg.CONF.bind_host LOG.info(_('Starting Murano REST API on %(host)s:%(port)s'), {'host': host, 'port': port}) return app_loader.load_paste_app('murano') ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1696417899.6731803 murano-16.0.0/murano/locale/0000775000175000017500000000000000000000000015662 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1696417899.6731803 murano-16.0.0/murano/locale/en_GB/0000775000175000017500000000000000000000000016634 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1696417899.805181 murano-16.0.0/murano/locale/en_GB/LC_MESSAGES/0000775000175000017500000000000000000000000020421 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/locale/en_GB/LC_MESSAGES/murano.po0000664000175000017500000003707500000000000022276 0ustar00zuulzuul00000000000000# OpenStack Infra , 2015. #zanata # Andi Chandler , 2017. #zanata # Andi Chandler , 2018. #zanata # Andi Chandler , 2019. #zanata # Andi Chandler , 2022. #zanata msgid "" msgstr "" "Project-Id-Version: murano VERSION\n" "Report-Msgid-Bugs-To: https://bugs.launchpad.net/openstack-i18n/\n" "POT-Creation-Date: 2022-04-20 01:19+0000\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "PO-Revision-Date: 2022-06-10 09:22+0000\n" "Last-Translator: Andi Chandler \n" "Language-Team: English (United Kingdom)\n" "Language: en_GB\n" "X-Generator: Zanata 4.3.3\n" "Plural-Forms: nplurals=2; plural=(n != 1)\n" msgid "" "'multipart/form-data' request body should contain 1 or 2 parts: json string " "and zip archive. Current body consists of {amount} part(s)" msgstr "" "'multipart/form-data' request body should contain 1 or 2 parts: json string " "and zip archive. Current body consists of {amount} part(s)" msgid "Acceptable response can not be provided" msgstr "Acceptable response cannot be provided" msgid "Attribute '{0}' is invalid" msgstr "Attribute '{0}' is invalid" msgid "Authentication URL" msgstr "Authentication URL" msgid "Authorization required" msgstr "Authorisation required" msgid "Bad value passed to filter. Got {key}, expected:{valid}" msgstr "Bad value passed to filter. Got {key}, expected:{valid}" msgid "Category '{name}' doesn't exist" msgstr "Category '{name}' doesn't exist" msgid "Category id '{id}' not found" msgstr "Category id '{id}' not found" msgid "Category name should be 80 characters maximum" msgstr "Category name should be 80 characters maximum" msgid "Category with specified name is already exist" msgstr "Category with specified name is already exist" msgid "Class name and method name must be specified for static action" msgstr "Class name and method name must be specified for static action" msgid "" "Class with the same full name is already registered in the visibility scope" msgstr "" "Class with the same full name is already registered in the visibility scope" msgid "Content-Type must be '{type}'" msgstr "Content-Type must be '{type}'" #, python-format msgid "" "Could not bind to %(host)s:%(port)s after trying for 30 seconds: Address " "already in use." msgstr "" "Could not bind to %(host)s:%(port)s after trying for 30 seconds: Address " "already in use." msgid "" "Could not open session for environment , environment has " "deploying or deleting status." msgstr "" "Could not open session for environment , environment has " "deploying or deleting status." msgid "Couldn't load package from file: {reason}" msgstr "Couldn't load package from file: {reason}" msgid "Create resources using trust token rather than user's token" msgstr "Create resources using trust token rather than user's token" msgid "Disallow the use of murano-agent" msgstr "Disallow the use of murano-agent" msgid "Domain name of the project" msgstr "Domain name of the project" msgid "Domain name of the user" msgstr "Domain name of the user" msgid "Enable model policy enforcer using Congress" msgstr "Enable model policy enforcer using Congress" msgid "" "Enables murano-engine to persist on disk packages downloaded during " "deployments. The packages would be re-used for consequent deployments." msgstr "" "Enables murano-engine to persist on disk packages downloaded during " "deployments. The packages would be re-used for consequent deployments." msgid "Env Template with specified name already exists" msgstr "Env Template with specified name already exists" msgid "Env template with specified name already exists" msgstr "Env template with specified name already exists" msgid "EnvTemplate is not found" msgstr "EnvTemplate is not found" msgid "EnvTemplate body is incorrect" msgstr "EnvTemplate body is incorrect" msgid "Environment is not found" msgstr "Environment is not found" msgid "Environment Template is not found" msgstr "Environment Template is not found" msgid "Environment Template is not found" msgstr "Environment Template is not found" msgid "Environment Template must contain at least one non-white space symbol" msgstr "Environment Template must contain at least one non-white space symbol" msgid "Environment Template with id {id} not found" msgstr "Environment Template with id {id} not found" msgid "Environment audit interval in minutes. Default value is 60 minutes." msgstr "Environment audit interval in minutes. Default value is 60 minutes." msgid "Environment name must contain at least one non-white space symbol" msgstr "Environment name must contain at least one non-white space symbol" msgid "Environment name should be 255 characters maximum" msgstr "Environment name should be 255 characters maximum" msgid "Environment template name should be 255 characters maximum" msgstr "Environment template name should be 255 characters maximum" msgid "Environment template specified name already exists" msgstr "Environment template specified name already exists" msgid "Environment with id {env_id} not found" msgstr "Environment with id {env_id} not found" msgid "Environment with specified name already exists" msgstr "Environment with specified name already exists" msgid "Host for service broker" msgstr "Host for service broker" #, python-format msgid "Invalid SSL version: %s" msgstr "Invalid SSL version: %s" #, python-format msgid "Invalid filter value %s. The quote is not closed." msgstr "Invalid filter value %s. The quote is not closed." #, python-format msgid "" "Invalid filter value %s. There is no comma after opening quotation mark." msgstr "" "Invalid filter value %s. There is no comma after opening quotation mark." #, python-format msgid "" "Invalid filter value %s. There is no comma before opening quotation mark." msgstr "" "Invalid filter value %s. There is no comma before opening quotation mark." msgid "Invalid sort direction: {0}" msgstr "Invalid sort direction: {0}" msgid "Invalid sort key: {sort_key}. Must be one of the following: {available}" msgstr "" "Invalid sort key: {sort_key}. Must be one of the following: {available}" msgid "" "It's impossible to delete categories assigned to the package, uploaded to " "the catalog" msgstr "" "It's impossible to delete categories assigned to the package, uploaded to " "the catalogue" msgid "JSON-patch must be a list." msgstr "JSON-patch must be a list." msgid "Limit param must be an integer" msgstr "Limit param must be an integer" msgid "Limit param must be positive" msgstr "Limit param must be positive" msgid "" "List of directories to load local packages from. If not provided, packages " "will be loaded only API" msgstr "" "List of directories to load local packages from. If not provided, packages " "will be loaded only API" msgid "" "Local package is not found since \"load-packages-from\" engine parameter is " "not provided and specified packages is not loaded to murano-api" msgstr "" "Local package is not found since \"load-packages-from\" engine parameter is " "not provided and specified packages is not loaded to murano-api" msgid "Malformed request body" msgstr "Malformed request body" msgid "Maximum number of elements that can be iterated per object type." msgstr "Maximum number of elements that can be iterated per object type." msgid "" "Method '{method}' is not allowed for a path with name '{name}'. Allowed " "operations are: {ops}" msgstr "" "Method '{method}' is not allowed for a path with name '{name}'. Allowed " "operations are: {ops}" msgid "Murano object model validation failed: {0}" msgstr "Murano object model validation failed: {0}" msgid "Nested paths are not allowed" msgstr "Nested paths are not allowed" msgid "No tests found for execution." msgstr "No tests found for execution." msgid "Number of API workers" msgstr "Number of API workers" msgid "Number of engine workers" msgstr "Number of engine workers" #, python-format msgid "Operation \"%s\" requires a member named \"value\"." msgstr "Operation \"%s\" requires a member named \"value\"." msgid "Operations must be JSON objects." msgstr "Operations must be JSON objects." msgid "Package '{pkg_id}' is not owned by tenant '{tenant}'" msgstr "Package '{pkg_id}' is not owned by tenant '{tenant}'" msgid "Package '{pkg_id}' is not public and not owned by tenant '{tenant}' " msgstr "Package '{pkg_id}' is not public and not owned by tenant '{tenant}' " msgid "Package id '{pkg_id}' not found" msgstr "Package id '{pkg_id}' not found" msgid "Package name should be 80 characters maximum" msgstr "Package name should be 80 characters maximum" msgid "Package schema is not valid: {reason}" msgstr "Package schema is not valid: {reason}" msgid "Package service which should be used by service broker" msgstr "Package service which should be used by service broker" msgid "Package with specified full name is already registered" msgstr "Package with specified full name is already registered" msgid "Package with the same Name is already made public" msgstr "Package with the same Name is already made public" msgid "Path to RSA key for agent message signing" msgstr "Path to RSA key for agent message signing" msgid "Path to class configuration files" msgstr "Path to class configuration files" msgid "Please, specify a name of the environment template." msgstr "Please, specify a name of the environment template." msgid "Please, specify a name of the environment to create" msgstr "Please, specify a name of the environment to create" #, python-format msgid "" "Pointer `%s` contains \"~\", which is not part of a recognized escape " "sequence." msgstr "" "Pointer `%s` contains \"~\", which is not part of a recognised escape " "sequence." #, python-format msgid "Pointer `%s` contains adjacent \"/\"." msgstr "Pointer `%s` contains adjacent \"/\"." #, python-format msgid "Pointer `%s` does not contain a valid token." msgstr "Pointer `%s` does not contain a valid token." #, python-format msgid "Pointer `%s` does not start with \"/\"." msgstr "Pointer `%s` does not start with \"/\"." #, python-format msgid "Pointer `%s` ends with \"/\"." msgstr "Pointer `%s` ends with \"/\"." msgid "Policy File JSON to YAML Migration" msgstr "Policy File JSON to YAML Migration" msgid "Port for service broker" msgstr "Port for service broker" msgid "Project for service broker" msgstr "Project for service broker" msgid "Request body is empty: please, provide application object model" msgstr "Request body is empty: please, provide application object model" msgid "Request body is empty: please, provide environment object model patch" msgstr "Request body is empty: please, provide environment object model patch" msgid "Request body must be a JSON array of operation objects." msgstr "Request body must be a JSON array of operation objects." msgid "Role used to identify an authenticated user as administrator." msgstr "Role used to identify an authenticated user as administrator." msgid "Session is already in deployment state" msgstr "Session is already in deployment state" msgid "" "Session is invalid: environment has been updated or updating " "right now with other session" msgstr "" "Session is invalid: environment has been updated or updating " "right now with other session" msgid "Session is not found" msgstr "Session is not found" msgid "Session is not found" msgstr "Session is not found" msgid "" "Session is already deployed or deployment is in progress" msgstr "" "Session is already deployed or deployment is in progress" msgid "" "Session is not tied with Environment " msgstr "" "Session is not tied with Environment " msgid "" "Session is in deploying state and could not be deleted" msgstr "" "Session is in deploying state and could not be deleted" msgid "Source object or path is malformed" msgstr "Source object or path is malformed" msgid "" "Specified package is not found: {0} were scanned together with murano " "database" msgstr "" "Specified package is not found: {0} were scanned together with Murano " "database" #, python-format msgid "Starting Murano REST API on %(host)s:%(port)s" msgstr "Starting Murano REST API on %(host)s:%(port)s" msgid "Statistics collection interval in minutes.Default value is 5 minutes." msgstr "Statistics collection interval in minutes. Default value is 5 minutes." msgid "The environment template {templ_id} does not exist" msgstr "The environment template {templ_id} does not exist" msgid "" "The service to store murano packages: murano (stands for legacy behavior " "using murano-api) or glance (stands for glance-glare artifact service)" msgstr "" "The service to store Murano packages: Murano (stands for legacy behaviour " "using murano-api) or Glance (stands for glance-glare artefact service)" msgid "The template does not exist {templ_id}" msgstr "The template does not exist {templ_id}" msgid "There is no file package with application description" msgstr "There is no file package with application description" msgid "Time for waiting for a response from murano agent during the deployment" msgstr "" "Time for waiting for a response from murano agent during the deployment" #, python-format msgid "Unable to find '%s' in JSON Schema change" msgstr "Unable to find '%s' in JSON Schema change" #, python-format msgid "" "Unable to load %(app_name)s from configuration file %(conf_file)s. \n" "Got: %(e)r" msgstr "" "Unable to load %(app_name)s from configuration file %(conf_file)s. \n" "Got: %(e)r" #, python-format msgid "Unable to locate paste config file for %s." msgstr "Unable to locate paste config file for %s." msgid "Unsupported Content-Type" msgstr "Unsupported Content-Type" msgid "Uploading file can't be empty" msgstr "Uploading file can't be empty" msgid "Uploading file is too large. The limit is {0} Mb" msgstr "Uploading file is too large. The limit is {0} Mb" msgid "" "User is not authorized to access session ." msgstr "" "User is not authorised to access session ." msgid "User has no access to these resources." msgstr "User has no access to these resources." msgid "User is not authorized to access these tenant resources" msgstr "User is not authorised to access these tenant resources" msgid "User is not authorized to access this tenant resources" msgstr "User is not authorised to access this tenant resources" msgid "Value '{value}' of property '{path}' does not exist." msgstr "Value '{value}' of property '{path}' does not exist." msgid "Whether environment audit events enabled" msgstr "Whether environment audit events enabled" msgid "X-Configuration-Session header which indicates to the session is missed" msgstr "" "X-Configuration-Session header which indicates to the session is missed" msgid "Your credentials are wrong. Please try again" msgstr "Your credentials are wrong. Please try again" msgid "cannot understand JSON" msgstr "cannot understand JSON" msgid "cannot understand XML" msgstr "cannot understand XML" msgid "pip URL/package spec for murano-agent" msgstr "pip URL/package spec for murano-agent" msgid "{0}: Unsupported Format. Only {1} allowed" msgstr "{0}: Unsupported Format. Only {1} allowed" msgid "{0}: Uploaded image size {1} is too large. Max allowed size is {2}" msgstr "{0}: Uploaded image size {1} is too large. Max allowed size is {2}" ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1696417899.6731803 murano-16.0.0/murano/locale/ru/0000775000175000017500000000000000000000000016310 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1696417899.805181 murano-16.0.0/murano/locale/ru/LC_MESSAGES/0000775000175000017500000000000000000000000020075 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/locale/ru/LC_MESSAGES/murano.po0000664000175000017500000003334600000000000021747 0ustar00zuulzuul00000000000000# OpenStack Infra , 2015. #zanata # Andreas Jaeger , 2016. #zanata msgid "" msgstr "" "Project-Id-Version: murano 3.2.1.dev28\n" "Report-Msgid-Bugs-To: https://bugs.launchpad.net/openstack-i18n/\n" "POT-Creation-Date: 2017-03-22 16:25+0000\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "PO-Revision-Date: 2016-04-12 05:46+0000\n" "Last-Translator: Copied by Zanata \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 "" "'multipart/form-data' request body should contain 1 or 2 parts: json string " "and zip archive. Current body consists of {amount} part(s)" msgstr "" "'multipart/form-data' тело запроса должно содержать 1 или 2 части: строка " "json и zip архив. Настоящее тело запроса состоит из {amount} частей" msgid "Attribute '{0}' is invalid" msgstr "Неверный атрибут '{0}'" msgid "Authentication URL" msgstr "URL аутентификации" msgid "Authorization required" msgstr "Требуется авторизация" msgid "Category '{name}' doesn't exist" msgstr "Категория '{name}' не существует" msgid "Category id '{id}' not found" msgstr "Категория с id '{id}' не найдена" msgid "Category name should be 80 characters maximum" msgstr "Имя категории не должно превышать 80 символов" msgid "Category with specified name is already exist" msgstr "Категория с указанным именем уже существует" msgid "" "Class with the same full name is already registered in the visibility scope" msgstr "" "Класс с таким полным именем уже зарегистрирован в текущей области видимости" msgid "Content-Type must be '{type}'" msgstr "Content-Type должен быть '{type}'" #, python-format msgid "" "Could not bind to %(host)s:%(port)s after trying for 30 seconds: Address " "already in use." msgstr "" "Не удалось связать с %(host)s:%(port)s в течении 30 секунд: адрес уже " "используется" msgid "Couldn't load package from file: {reason}" msgstr "Не удалось загрузить пакет из файла: {reason}" msgid "Create resources using trust token rather than user's token" msgstr "Создание ресурсов используя trust токен, а не токен пользователя" msgid "Disallow the use of murano-agent" msgstr "Запретить использование мурано агента" msgid "Enable model policy enforcer using Congress" msgstr "Включите enforcer политик, используя Congress" msgid "Env Template with specified name already exists" msgstr "Шаблон окружения с указанным именем уже существует" msgid "EnvTemplate is not found" msgstr "Шаблон окружения не найден" msgid "EnvTemplate body is incorrect" msgstr "Недопустимое тело шаблона окружения" msgid "Environment is not found" msgstr "Окружение не найдено" msgid "Environment Template is not found" msgstr "Шаблон окружения не найден" msgid "Environment Template is not found" msgstr "Шаблон окружения не найден" msgid "Environment Template must contain at least one non-white space symbol" msgstr "Шаблон окружения должен содержать хотя бы один не-пробельный символ" msgid "Environment Template with id {id} not found" msgstr "Шаблон окружения с id {id} не найден" msgid "Environment name must contain at least one non-white space symbol" msgstr "Имя окружения должно содержать хотя бы один не-пробельный символ" msgid "Environment name should be 255 characters maximum" msgstr "Имя окружения не должно превышать 255 символов" msgid "Environment template name should be 255 characters maximum" msgstr "Имя шаблона окружения не должно превышать 255 символов" msgid "Environment template specified name already exists" msgstr "Шаблон окружения с указанным именем уже существует" msgid "Environment with id {env_id} not found" msgstr "Окружение с id {env_id} не найдено" msgid "Environment with specified name already exists" msgstr "Окружение с указанным именем уже существует" msgid "Invalid sort direction: {0}" msgstr "Неверное направление сортировки: {0}" msgid "Invalid sort key: {sort_key}. Must be one of the following: {available}" msgstr "" "Недопустимый ключ сортировки: {sort_key}. Допускается один из следующих " "ключей: {available}" msgid "" "It's impossible to delete categories assigned to the package, uploaded to " "the catalog" msgstr "" "Невозможно удалить категории, присвоенные загруженному в каталог пакету" msgid "Limit param must be an integer" msgstr "Параметр limit должен быть целым числом" msgid "Limit param must be positive" msgstr "Параметр limit должен быть положительным числом" msgid "" "Local package is not found since \"load-packages-from\" engine parameter is " "not provided and specified packages is not loaded to murano-api" msgstr "" "Пакет не найдет локально, поскольку параметр \"load-packages-from\" не " "установлен, а так же пакет не был загружен через API" msgid "Malformed request body" msgstr "Неправильно сформированное тело запроса" msgid "Murano object model validation failed: {0}" msgstr "Ошибка при валидации объкетной модели мурано: {0}" msgid "Nested paths are not allowed" msgstr "Вложенные пути недопустимы" msgid "No tests found for execution." msgstr "Не найдено тестов для выполнения." #, python-format msgid "Operation \"%s\" requires a member named \"value\"." msgstr "Операции \"%s\" требуется участник с именем \"value\"." msgid "Operations must be JSON objects." msgstr "Операции должны быть объектами JSON." msgid "Package '{pkg_id}' is not owned by tenant '{tenant}'" msgstr "Пакет '{pkg_id}' не принадлежит проекту '{tenant}'" msgid "Package '{pkg_id}' is not public and not owned by tenant '{tenant}' " msgstr "" "Пакет '{pkg_id}' не является публичным и не принадлежит проекту '{tenant}'" msgid "Package id '{pkg_id}' not found" msgstr "Пакет с id '{pkg_id}' не найден" msgid "Package schema is not valid: {reason}" msgstr "Неверная схема пакета: {reason}" msgid "Package with specified full name is already registered" msgstr "Пакет с указанным полным именем уже зарегистрирован" msgid "Package with the same Name is already made public" msgstr "Пакет с таким именем уже является публичным" msgid "Path to class configuration files" msgstr "Путь к классу конфигурационных файлов" msgid "Please, specify a name of the environment template." msgstr "Укажите имя шаблона окружения" msgid "Please, specify a name of the environment to create" msgstr "Укажите имя создаваемого окружения" #, python-format msgid "Pointer `%s` contains adjacent \"/\"." msgstr "Указатель `%s` содержит смежный \"/\"." #, python-format msgid "Pointer `%s` does not start with \"/\"." msgstr "Указатель `%s` не начинается с \"/\"." msgid "Request body is empty: please, provide application object model" msgstr "Пустое тело запроса: укажите объектную модель приложения" msgid "Request body must be a JSON array of operation objects." msgstr "Тело запроса должно быть массивом JSON объектов операций." msgid "Role used to identify an authenticated user as administrator." msgstr "" "Роль, применяемая для определения идентифицированного пользователя в " "качестве администратора." msgid "Session is already in deployment state" msgstr "Сессия находится в состоянии развёртывания" msgid "" "Session is invalid: environment has been updated or updating " "right now with other session" msgstr "" "Сессия недействительна: окружение было обновлено или " "обновляется в другой сессии" msgid "Session is not found" msgstr "Сессия не найдена" msgid "Session is not found" msgstr "Сессия не найдена" msgid "" "Session is already deployed or deployment is in progress" msgstr "" "Сессия уже задеплоена или находится в состоянии " "развёртывания" msgid "" "Session is not tied with Environment " msgstr "" "Сессия не привязана к Окружению " msgid "" "Session is in deploying state and could not be deleted" msgstr "" "Сессия находится в состоянии развёртывания и не может " "быть удалена" msgid "Source object or path is malformed" msgstr "Неверный исходный объект или путь" msgid "" "Specified package is not found: {0} were scanned together with murano " "database" msgstr "" "Пакет не был найден: {0} были просканированны вместе с базой данных murano" msgid "Statistics collection interval in minutes.Default value is 5 minutes." msgstr "Интервал сбора статистики в минутах. Значение по умолчанию - 5 минут." msgid "The environment template {templ_id} does not exist" msgstr "Шаблон окружения {templ_id} не существует" msgid "The template does not exist {templ_id}" msgstr "Шаблон не существует {templ_id}" msgid "There is no file package with application description" msgstr "Отсутствует файл пакета с описанием приложения" msgid "Time for waiting for a response from murano agent during the deployment" msgstr "Время ожидания ответа от мурано агента при развёртывания" #, python-format msgid "Unable to find '%s' in JSON Schema change" msgstr "'%s' не найден в изменении схемы JSON" #, python-format msgid "" "Unable to load %(app_name)s from configuration file %(conf_file)s. \n" "Got: %(e)r" msgstr "" "Невозможно загрузить %(app_name)s из конфигурационного файла %(conf_file)s.\n" "Ошибка: %(e)r" #, python-format msgid "Unable to locate paste config file for %s." msgstr "Не удается найти файл конфигурации paste для %s." msgid "Unsupported Content-Type" msgstr "Неподдерживаемый Content-Type" msgid "Uploading file can't be empty" msgstr "Загружаемый файл не может быть пустым" msgid "Uploading file is too large. The limit is {0} Mb" msgstr "Загружаемый файл слишком большой. Значение не должно превышать {0} Mb" msgid "" "User is not authorized to access session ." msgstr "" "У пользователя нет прав на доступ к сессии ." msgid "User is not authorized to access these tenant resources" msgstr "У пользователя нет прав на доступ к этим ресурсам этого проекта" msgid "User is not authorized to access this tenant resources" msgstr "У пользователя нет прав на доступ к ресурсам этого проекта" msgid "Value '{value}' of property '{path}' does not exist." msgstr "Значение '{value}' для свойства '{path}' не существует" msgid "X-Configuration-Session header which indicates to the session is missed" msgstr "" "Заголовок X-Configuration-Session, которым определяется сессия, отсутствует" msgid "Your credentials are wrong. Please try again" msgstr "Ваша авторизация не верна. Попробуйте ещё раз" msgid "cannot understand JSON" msgstr "не удается проанализировать JSON" msgid "cannot understand XML" msgstr "не удается проанализировать XML" ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/monkey_patch.py0000664000175000017500000000222500000000000017457 0ustar00zuulzuul00000000000000# # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # 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 eventlet if os.name == 'nt': # eventlet monkey patching causes subprocess.Popen to fail on Windows # when using pipes due to missing non blocking I/O support eventlet.monkey_patch(os=False) else: eventlet.monkey_patch() # Monkey patch the original current_thread to use the up-to-date _active # global variable. See https://bugs.launchpad.net/bugs/1863021 and # https://github.com/eventlet/eventlet/issues/592 import __original_module_threading as orig_threading import threading # noqa orig_threading.current_thread.__globals__['_active'] = threading._active ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/opts.py0000664000175000017500000000651100000000000015765 0ustar00zuulzuul00000000000000# 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 copy import itertools from keystoneauth1 import loading as ks_loading import oslo_service.sslutils import murano.common.cf_config import murano.common.config import murano.common.wsgi __all__ = [ 'list_opts', 'list_cfapi_opts', ] def build_list(opt_list): return list(itertools.chain(*opt_list)) # List of *all* options in [DEFAULT] namespace of murano. # Any new option list or option needs to be registered here. _opt_lists = [ ('engine', murano.common.config.engine_opts), ('rabbitmq', murano.common.config.rabbit_opts), ('heat', murano.common.config.heat_opts + ks_loading.get_session_conf_options()), ('neutron', murano.common.config.neutron_opts + ks_loading.get_session_conf_options()), ('murano', murano.common.config.murano_opts + ks_loading.get_session_conf_options()), ('glare', murano.common.config.glare_opts + ks_loading.get_session_conf_options()), ('mistral', murano.common.config.mistral_opts + ks_loading.get_session_conf_options()), ('networking', murano.common.config.networking_opts), ('stats', murano.common.config.stats_opts), ('murano_auth', murano.common.config.murano_auth_opts + ks_loading.get_session_conf_options() + ks_loading.get_auth_common_conf_options() + ks_loading.get_auth_plugin_conf_options('password') + ks_loading.get_auth_plugin_conf_options('v2password') + ks_loading.get_auth_plugin_conf_options('v3password')), (None, build_list([ murano.common.config.metadata_dir, murano.common.config.bind_opts, murano.common.config.file_server, murano.common.wsgi.wsgi_opts, ])), ] _cfapi_opt_lists = [ ('cfapi', murano.common.cf_config.cfapi_opts), ('glare', murano.common.config.glare_opts + ks_loading.get_session_conf_options()) ] _opt_lists.extend(oslo_service.sslutils.list_opts()) def list_opts(): """Return a list of oslo.config options available in Murano. Each element of the list is a tuple. The first element is the name of the group under which the list of elements in the second element will be registered. A group name of None corresponds to the [DEFAULT] group in config files. This function is also discoverable via the 'murano' entry point under the 'oslo.config.opts' namespace. The purpose of this is to allow tools like the Oslo sample config file generator to discover the options exposed to users by Murano. :returns: a list of (group_name, opts) tuples """ return [(g, copy.deepcopy(o)) for g, o in _opt_lists] def list_cfapi_opts(): """Return a list of oslo_config options available in service broker.""" return [(g, copy.deepcopy(o)) for g, o in _cfapi_opt_lists] ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1696417899.809181 murano-16.0.0/murano/packages/0000775000175000017500000000000000000000000016201 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/packages/__init__.py0000664000175000017500000000000000000000000020300 0ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/packages/exceptions.py0000664000175000017500000000271500000000000020741 0ustar00zuulzuul00000000000000# 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 murano.common.exceptions as e class PackageException(e.Error): pass class PackageClassLoadError(PackageException): def __init__(self, class_name, message=None): msg = 'Unable to load class "{0}" from package'.format(class_name) if message: msg += ": " + message super(PackageClassLoadError, self).__init__(msg) class PackageUILoadError(PackageException): def __init__(self, message=None): msg = 'Unable to load ui definition from package' if message: msg += ": " + message super(PackageUILoadError, self).__init__(msg) class PackageLoadError(PackageException): pass class PackageFormatError(PackageLoadError): def __init__(self, message=None): msg = 'Incorrect package format' if message: msg += ': ' + message super(PackageFormatError, self).__init__(msg) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/packages/hot_package.py0000664000175000017500000004545400000000000021034 0ustar00zuulzuul00000000000000# 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 os import shutil import sys import yaml from murano.common.helpers import path from murano.packages import exceptions from murano.packages import package_base RESOURCES_DIR_NAME = 'Resources/' HOT_FILES_DIR_NAME = 'HotFiles/' HOT_ENV_DIR_NAME = 'HotEnvironments/' class YAQL(object): def __init__(self, expr): self.expr = expr class Dumper(yaml.SafeDumper): pass def yaql_representer(dumper, data): return dumper.represent_scalar(u'!yaql', data.expr) Dumper.add_representer(YAQL, yaql_representer) class HotPackage(package_base.PackageBase): def __init__(self, format_name, runtime_version, source_directory, manifest): super(HotPackage, self).__init__( format_name, runtime_version, source_directory, manifest) self._translated_class = None self._source_directory = source_directory self._translated_ui = None @property def classes(self): return self.full_name, @property def requirements(self): return {} @property def ui(self): if not self._translated_ui: self._translated_ui = self._translate_ui() return self._translated_ui def get_class(self, name): if name != self.full_name: raise exceptions.PackageClassLoadError( name, 'Class not defined in this package') if not self._translated_class: self._translate_class() return self._translated_class, '' def _translate_class(self): template_file = path.secure_join( self._source_directory, 'template.yaml') if not os.path.isfile(template_file): raise exceptions.PackageClassLoadError( self.full_name, 'File with class definition not found') shutil.copy(template_file, self.get_resource(self.full_name)) with open(template_file) as stream: hot = yaml.safe_load(stream) if 'resources' not in hot: raise exceptions.PackageFormatError('Not a HOT template') translated = { 'Name': self.full_name, 'Extends': 'io.murano.Application' } hot_envs_path = path.secure_join( self._source_directory, RESOURCES_DIR_NAME, HOT_ENV_DIR_NAME) # if using hot environments, doing parameter validation with contracts # will overwrite the parameters in the hot environment. # don't validate parameters if hot environments exist. validate_hot_parameters = (not os.path.isdir(hot_envs_path) or not os.listdir(hot_envs_path)) parameters = HotPackage._build_properties(hot, validate_hot_parameters) parameters.update(HotPackage._translate_outputs(hot)) translated['Properties'] = parameters files = HotPackage._translate_files(self._source_directory) translated.update(HotPackage._generate_workflow(hot, files)) # use default_style with double quote mark because by default PyYAML # doesn't put any quote marks ans as a result strings with e.g. dashes # may be interpreted as YAQL expressions upon load self._translated_class = yaml.dump( translated, Dumper=Dumper, default_style='"') @staticmethod def _build_properties(hot, validate_hot_parameters): result = { 'generatedHeatStackName': { 'Contract': YAQL('$.string()'), 'Usage': 'Out' }, 'hotEnvironment': { 'Contract': YAQL('$.string()'), 'Usage': 'In' }, 'name': { 'Contract': YAQL('$.string().notNull()'), 'Usage': 'In', } } if validate_hot_parameters: params_dict = {} for key, value in (hot.get('parameters') or {}).items(): param_contract = HotPackage._translate_param_to_contract(value) params_dict[key] = param_contract result['templateParameters'] = { 'Contract': params_dict, 'Default': {}, 'Usage': 'In' } else: result['templateParameters'] = { 'Contract': {}, 'Default': {}, 'Usage': 'In' } return result @staticmethod def _translate_param_to_contract(value): contract = '$' parameter_type = value['type'] if parameter_type in ('string', 'comma_delimited_list', 'json'): contract += '.string()' elif parameter_type == 'number': contract += '.int()' elif parameter_type == 'boolean': contract += '.bool()' else: raise ValueError('Unsupported parameter type ' + parameter_type) constraints = value.get('constraints') or [] for constraint in constraints: translated = HotPackage._translate_constraint(constraint) if translated: contract += translated result = YAQL(contract) return result @staticmethod def _translate_outputs(hot): contract = {} for key in (hot.get('outputs') or {}).keys(): contract[key] = YAQL("$.string()") return { 'templateOutputs': { 'Contract': contract, 'Default': {}, 'Usage': 'Out' } } @staticmethod def _translate_files(source_directory): hot_files_path = path.secure_join( source_directory, RESOURCES_DIR_NAME, HOT_FILES_DIR_NAME) return HotPackage._build_hot_resources(hot_files_path) @staticmethod def _build_hot_resources(basedir): result = [] if os.path.isdir(basedir): for root, _, files in os.walk(os.path.abspath(basedir)): for f in files: full_path = path.secure_join(root, f) relative_path = os.path.relpath(full_path, basedir) result.append(relative_path) return result @staticmethod def _translate_constraint(constraint): if 'allowed_values' in constraint: return HotPackage._translate_allowed_values_constraint( constraint['allowed_values']) elif 'length' in constraint: return HotPackage._translate_length_constraint( constraint['length']) elif 'range' in constraint: return HotPackage._translate_range_constraint( constraint['range']) elif 'allowed_pattern' in constraint: return HotPackage._translate_allowed_pattern_constraint( constraint['allowed_pattern']) @staticmethod def _translate_allowed_pattern_constraint(value): return ".check(matches($, '{0}'))".format(value) @staticmethod def _translate_allowed_values_constraint(values): return '.check($ in list({0}))'.format( ', '.join([HotPackage._format_value(v) for v in values])) @staticmethod def _translate_length_constraint(value): if 'min' in value and 'max' in value: return '.check(len($) >= {0} and len($) <= {1})'.format( int(value['min']), int(value['max'])) elif 'min' in value: return '.check(len($) >= {0})'.format(int(value['min'])) elif 'max' in value: return '.check(len($) <= {0})'.format(int(value['max'])) @staticmethod def _translate_range_constraint(value): if 'min' in value and 'max' in value: return '.check($ >= {0} and $ <= {1})'.format( int(value['min']), int(value['max'])) elif 'min' in value: return '.check($ >= {0})'.format(int(value['min'])) elif 'max' in value: return '.check($ <= {0})'.format(int(value['max'])) @staticmethod def _format_value(value): if isinstance(value, str): return str("'" + value + "'") return str(value) @staticmethod def _generate_workflow(hot, files): hot_files_map = {} for f in files: file_path = "$resources.string('{0}{1}')".format( HOT_FILES_DIR_NAME, f) hot_files_map[f] = YAQL(file_path) hot_env = YAQL("$.hotEnvironment") deploy = [ {YAQL('$environment'): YAQL( "$.find('io.murano.Environment').require()" )}, {YAQL('$reporter'): YAQL( "new('io.murano.system.StatusReporter', " "environment => $environment)")}, { 'If': YAQL('$.getAttr(generatedHeatStackName) = null'), 'Then': [ YAQL("$.setAttr(generatedHeatStackName, " "'{0}_{1}'.format(randomName(), id($environment)))") ] }, {YAQL('$stack'): YAQL( "new('io.murano.system.HeatStack', $environment, " "name => $.getAttr(generatedHeatStackName))")}, YAQL("$reporter.report($this, " "'Application deployment has started')"), {YAQL('$resources'): YAQL("new('io.murano.system.Resources')")}, {YAQL('$template'): YAQL("$resources.yaml(type($this))")}, YAQL('$stack.setTemplate($template)'), {YAQL('$parameters'): YAQL("$.templateParameters")}, YAQL('$stack.setParameters($parameters)'), {YAQL('$files'): hot_files_map}, YAQL('$stack.setFiles($files)'), {YAQL('$hotEnv'): hot_env}, { 'If': YAQL("bool($hotEnv)"), 'Then': [ {YAQL('$envRelPath'): YAQL("'{0}' + $hotEnv".format( HOT_ENV_DIR_NAME))}, {YAQL('$hotEnvContent'): YAQL("$resources.string(" "$envRelPath)")}, YAQL('$stack.setHotEnvironment($hotEnvContent)') ] }, YAQL("$reporter.report($this, 'Stack creation has started')"), { 'Try': [YAQL('$stack.push()')], 'Catch': [ { 'As': 'e', 'Do': [ YAQL("$reporter.report_error($this, $e.message)"), {'Rethrow': None} ] } ], 'Else': [ {YAQL('$.templateOutputs'): YAQL('$stack.output()')}, YAQL("$reporter.report($this, " "'Stack was successfully created')"), YAQL("$reporter.report($this, " "'Application deployment has finished')"), ] } ] destroy = [ {YAQL('$environment'): YAQL( "$.find('io.murano.Environment').require()" )}, {YAQL('$stack'): YAQL( "new('io.murano.system.HeatStack', $environment, " "name => $.getAttr(generatedHeatStackName))")}, YAQL('$stack.delete()') ] return { 'Methods': { 'deploy': { 'Body': deploy }, 'destroy': { 'Body': destroy } } } @staticmethod def _translate_ui_parameters(hot, title): groups = hot.get('parameter_groups', []) result_groups = [] predefined_fields = [ { 'name': 'title', 'type': 'string', 'required': False, 'hidden': True, 'description': title }, { 'name': 'name', 'type': 'string', 'label': 'Application Name', 'required': True, 'description': 'Enter a desired name for the application.' ' Just A-Z, a-z, 0-9, and dash are allowed' } ] used_parameters = set() hot_parameters = hot.get('parameters') or {} for group in groups: fields = [] properties = [] for parameter in group.get('parameters', []): parameter_value = hot_parameters.get(parameter) if parameter_value: fields.append(HotPackage._translate_ui_parameter( parameter, parameter_value)) used_parameters.add(parameter) properties.append(parameter) result_groups.append((fields, properties)) rest_group = [] properties = [] for key, value in hot_parameters.items(): if key not in used_parameters: rest_group.append(HotPackage._translate_ui_parameter( key, value)) properties.append(key) if rest_group: result_groups.append((rest_group, properties)) result_groups.insert(0, (predefined_fields, ['name'])) return result_groups @staticmethod def _translate_ui_parameter(name, parameter_spec): translated = { 'name': name, 'label': name.title().replace('_', ' ') } parameter_type = parameter_spec['type'] if parameter_type == 'number': translated['type'] = 'integer' elif parameter_type == 'boolean': translated['type'] = 'boolean' else: # string, json, and comma_delimited_list parameters are all # displayed as strings in UI. Any unsupported parameter would also # be displayed as strings. translated['type'] = 'string' label = parameter_spec.get('label') if label: translated['label'] = label if 'description' in parameter_spec: translated['description'] = parameter_spec['description'] if 'default' in parameter_spec: translated['initial'] = parameter_spec['default'] translated['required'] = False else: translated['required'] = True constraints = parameter_spec.get('constraints') or [] translated_constraints = [] for constraint in constraints: if 'length' in constraint: spec = constraint['length'] if 'min' in spec: translated['minLength'] = max( translated.get('minLength', -sys.maxsize - 1), int(spec['min'])) if 'max' in spec: translated['maxLength'] = min( translated.get('maxLength', sys.maxsize), int(spec['max'])) elif 'range' in constraint: spec = constraint['range'] if 'min' in spec and 'max' in spec: ui_constraint = { 'expr': YAQL('$ >= {0} and $ <= {1}'.format( spec['min'], spec['max'])) } elif 'min' in spec: ui_constraint = { 'expr': YAQL('$ >= {0}'.format(spec['min'])) } else: ui_constraint = { 'expr': YAQL('$ <= {0}'.format(spec['max'])) } if 'description' in constraint: ui_constraint['message'] = constraint['description'] translated_constraints.append(ui_constraint) elif 'allowed_values' in constraint: values = constraint['allowed_values'] ui_constraint = { 'expr': YAQL('$ in list({0})'.format(', '.join( [HotPackage._format_value(v) for v in values]))) } if 'description' in constraint: ui_constraint['message'] = constraint['description'] translated_constraints.append(ui_constraint) elif 'allowed_pattern' in constraint: pattern = constraint['allowed_pattern'] ui_constraint = { 'expr': { 'regexpValidator': pattern } } if 'description' in constraint: ui_constraint['message'] = constraint['description'] translated_constraints.append(ui_constraint) if translated_constraints: translated['validators'] = translated_constraints return translated @staticmethod def _generate_application_ui(groups, type_name, package_name=None, package_version=None): app = { '?': { 'type': type_name } } if package_name: app['?']['package'] = package_name if package_version: app['?']['classVersion'] = package_version for i, record in enumerate(groups): if i == 0: section = app else: section = app.setdefault('templateParameters', {}) for property_name in record[1]: section[property_name] = YAQL( '$.group{0}.{1}'.format(i, property_name)) app['name'] = YAQL('$.group0.name') return app def _translate_ui(self): template_file = path.secure_join( self._source_directory, 'template.yaml') if not os.path.isfile(template_file): raise exceptions.PackageClassLoadError( self.full_name, 'File with class definition not found') with open(template_file) as stream: hot = yaml.safe_load(stream) groups = HotPackage._translate_ui_parameters(hot, self.description) forms = [] for i, record in enumerate(groups): forms.append({'group{0}'.format(i): {'fields': record[0]}}) translated = { 'Version': 2, 'Application': HotPackage._generate_application_ui( groups, self.full_name, self.full_name, str(self.version)), 'Forms': forms } # see comment above about default_style return yaml.dump(translated, Dumper=Dumper, default_style='"') ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/packages/load_utils.py0000664000175000017500000000744300000000000020722 0ustar00zuulzuul00000000000000# 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 import os import shutil import sys import tempfile import zipfile import yaml from murano.common.helpers import path from murano.common.plugins import package_types_loader from murano.common import utils import murano.packages.exceptions as e import murano.packages.hot_package import murano.packages.mpl_package PLUGIN_LOADER = None def get_plugin_loader(): global PLUGIN_LOADER if PLUGIN_LOADER is None: PLUGIN_LOADER = package_types_loader.PluginLoader() for runtime_version in ('1.0', '1.1', '1.2', '1.3', '1.4'): format_string = 'MuranoPL/' + runtime_version PLUGIN_LOADER.register_format( format_string, murano.packages.mpl_package.MuranoPlPackage) PLUGIN_LOADER.register_format( 'Heat.HOT/1.0', murano.packages.hot_package.HotPackage) return PLUGIN_LOADER @contextlib.contextmanager def load_from_file(archive_path, target_dir=None, drop_dir=False): if not os.path.isfile(archive_path): raise e.PackageLoadError('Unable to find package file') created = False if not target_dir: target_dir = tempfile.mkdtemp() created = True elif not os.path.exists(target_dir): os.makedirs(target_dir) created = True else: if os.listdir(target_dir): raise e.PackageLoadError('Target directory is not empty') try: if not zipfile.is_zipfile(archive_path): raise e.PackageFormatError("Uploaded file {0} is not a " "zip archive".format(archive_path)) package = zipfile.ZipFile(archive_path) package.extractall(path=target_dir) yield load_from_dir(target_dir) except ValueError as err: raise e.PackageLoadError("Couldn't load package from file: " "{0}".format(err)) finally: if drop_dir: if created: shutil.rmtree(target_dir) else: for f in os.listdir(target_dir): os.unlink(path.secure_join(target_dir, f)) def load_from_dir(source_directory, filename='manifest.yaml'): if not os.path.isdir(source_directory) or not os.path.exists( source_directory): raise e.PackageLoadError('Invalid package directory') full_path = path.secure_join(source_directory, filename) if not os.path.isfile(full_path): raise e.PackageLoadError('Unable to find package manifest') try: with open(full_path) as stream: content = yaml.safe_load(stream) except Exception as ex: trace = sys.exc_info()[2] utils.reraise( e.PackageLoadError, e.PackageLoadError("Unable to load due to '{0}'".format(ex)), trace) else: format_spec = str(content.get('Format') or 'MuranoPL/1.0') if format_spec[0].isdigit(): format_spec = 'MuranoPL/' + format_spec plugin_loader = get_plugin_loader() handler = plugin_loader.get_package_handler(format_spec) if handler is None: raise e.PackageFormatError( 'Unsupported format {0}'.format(format_spec)) return handler(source_directory, content) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/packages/mpl_package.py0000664000175000017500000000434100000000000021020 0ustar00zuulzuul00000000000000# 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 os from murano.common.helpers import path from murano.packages import exceptions from murano.packages import package_base class MuranoPlPackage(package_base.PackageBase): def __init__(self, format_name, runtime_version, source_directory, manifest): super(MuranoPlPackage, self).__init__( format_name, runtime_version, source_directory, manifest) self._classes = manifest.get('Classes') self._ui_file = manifest.get('UI', 'ui.yaml') self._requirements = manifest.get('Require') or {} self._meta = manifest.get('Meta') @property def classes(self): return self._classes.keys() @property def ui(self): full_path = path.secure_join( self._source_directory, 'UI', self._ui_file) if not os.path.isfile(full_path): return None with open(full_path, 'rb') as stream: return stream.read() @property def requirements(self): return self._requirements def get_class(self, name): if name not in self._classes: raise exceptions.PackageClassLoadError( name, 'Class not defined in package ' + self.full_name) def_file = self._classes[name] full_path = path.secure_join( self._source_directory, 'Classes', def_file) if not os.path.isfile(full_path): raise exceptions.PackageClassLoadError( name, 'File with class definition not found') with open(full_path, 'rb') as stream: return stream.read(), full_path @property def meta(self): return self._meta ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/packages/package.py0000664000175000017500000000654200000000000020155 0ustar00zuulzuul00000000000000# 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 abc import io import os import zipfile from murano.common.helpers import path class PackageType(object): Library = 'Library' Application = 'Application' ALL = [Library, Application] class Package(object, metaclass=abc.ABCMeta): def __init__(self, format_name, runtime_version, source_directory): self._source_directory = source_directory self._format_name = format_name self._runtime_version = runtime_version self._blob_cache = None @property def format_name(self): return self._format_name @property @abc.abstractmethod def full_name(self): raise NotImplementedError() @property @abc.abstractmethod def version(self): raise NotImplementedError() @property @abc.abstractmethod def classes(self): raise NotImplementedError() @property def runtime_version(self): return self._runtime_version @property @abc.abstractmethod def requirements(self): raise NotImplementedError() @property @abc.abstractmethod def package_type(self): raise NotImplementedError() @property @abc.abstractmethod def display_name(self): raise NotImplementedError() @property @abc.abstractmethod def description(self): raise NotImplementedError() @property @abc.abstractmethod def author(self): raise NotImplementedError() @property @abc.abstractmethod def supplier(self): raise NotImplementedError() @property @abc.abstractmethod def tags(self): raise NotImplementedError() @property @abc.abstractmethod def logo(self): raise NotImplementedError() @property @abc.abstractmethod def supplier_logo(self): raise NotImplementedError() @property def blob(self): if not self._blob_cache: self._blob_cache = _pack_dir(self._source_directory) return self._blob_cache @abc.abstractmethod def get_class(self, name): raise NotImplementedError() @abc.abstractmethod def get_resource(self, name): raise NotImplementedError() @property @abc.abstractmethod def ui(self): raise NotImplementedError() @property @abc.abstractmethod def meta(self): raise NotImplementedError() def _zip_dir(base, zip_file): for root, _, files in os.walk(base): for f in files: abs_path = path.secure_join(root, f) relative_path = os.path.relpath(abs_path, base) zip_file.write(abs_path, relative_path) def _pack_dir(source_directory): blob = io.BytesIO() zip_file = zipfile.ZipFile(blob, mode='w') _zip_dir(source_directory, zip_file) zip_file.close() return blob.getvalue() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/packages/package_base.py0000664000175000017500000001232100000000000021137 0ustar00zuulzuul00000000000000# 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 abc import imghdr import os import re import sys import semantic_version from murano.common.helpers import path from murano.common.i18n import _ from murano.common import utils from murano.packages import exceptions from murano.packages import package class PackageBase(package.Package): def __init__(self, format_name, runtime_version, source_directory, manifest): super(PackageBase, self).__init__( format_name, runtime_version, source_directory) self._full_name = manifest.get('FullName') if not self._full_name: raise exceptions.PackageFormatError('FullName is not specified') self._check_full_name(self._full_name) self._version = semantic_version.Version.coerce(str(manifest.get( 'Version', '0.0.0'))) self._package_type = manifest.get('Type') if self._package_type not in package.PackageType.ALL: raise exceptions.PackageFormatError( 'Invalid package Type {0}'.format(self._package_type)) self._display_name = manifest.get('Name', self._full_name) self._description = manifest.get('Description') self._author = manifest.get('Author') self._supplier = manifest.get('Supplier') or {} self._logo = manifest.get('Logo') self._tags = manifest.get('Tags', []) self._logo_cache = None self._supplier_logo_cache = None self._source_directory = source_directory @property @abc.abstractmethod def requirements(self): raise NotImplementedError() @property @abc.abstractmethod def classes(self): raise NotImplementedError() @abc.abstractmethod def get_class(self, name): raise NotImplementedError() @property @abc.abstractmethod def ui(self): raise NotImplementedError() @property def full_name(self): return self._full_name @property def source_directory(self): return self._source_directory @property def version(self): return self._version @property def package_type(self): return self._package_type @property def display_name(self): return self._display_name @property def description(self): return self._description @property def author(self): return self._author @property def supplier(self): return self._supplier @property def tags(self): return list(self._tags) @property def logo(self): return self._load_image(self._logo, 'logo.png', 'logo') @property def meta(self): return None @property def supplier_logo(self): return self._load_image( self._supplier.get('Logo'), 'supplier_logo.png', 'supplier logo') def get_resource(self, name): resources_dir = path.secure_join(self._source_directory, 'Resources') if not os.path.exists(resources_dir): os.makedirs(resources_dir) return path.secure_join(resources_dir, name) def _load_image(self, file_name, default_name, what_image): full_path = path.secure_join( self._source_directory, file_name or default_name) if not os.path.isfile(full_path) and not file_name: return allowed_ftype = ('png', 'jpeg', 'gif') allowed_size = 500 * 1024 try: if imghdr.what(full_path) not in allowed_ftype: msg = _('{0}: Unsupported Format. Only {1} allowed').format( what_image, ', '.join(allowed_ftype)) raise exceptions.PackageLoadError(msg) fsize = os.stat(full_path).st_size if fsize > allowed_size: msg = _('{0}: Uploaded image size {1} is too large. ' 'Max allowed size is {2}').format( what_image, fsize, allowed_size) raise exceptions.PackageLoadError(msg) with open(full_path, 'rb') as stream: return stream.read() except Exception as ex: trace = sys.exc_info()[2] utils.reraise( exceptions.PackageLoadError, exceptions.PackageLoadError( 'Unable to load {0}: {1}'.format(what_image, ex)), trace) @staticmethod def _check_full_name(full_name): error = exceptions.PackageFormatError('Invalid FullName ' + full_name) if re.match(r'^[\w\.]+$', full_name): if full_name.startswith('.') or full_name.endswith('.'): raise error if '..' in full_name: raise error else: raise error ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1696417899.809181 murano-16.0.0/murano/policy/0000775000175000017500000000000000000000000015722 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/policy/__init__.py0000664000175000017500000000000000000000000020021 0ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/policy/congress_rules.py0000664000175000017500000002270300000000000021335 0ustar00zuulzuul00000000000000# Copyright (c) 2014 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. from murano.dsl import helpers class CongressRulesManager(object): """Converts murano model to list of congress rules The Congress rules are: - murano:objects+(env_id, obj_id, type_name) - murano:properties+(obj_id, prop_name, prop_value) - murano:relationships+(source, target, name) - murano:parent_types+(obj_id, parent_name) - murano:states+(env_id, state) """ _rules = [] _env_id = '' _package_loader = None def convert(self, model, package_loader=None, tenant_id=None): self._rules = [] self._package_loader = package_loader if model is None: return self._rules self._env_id = model['?']['id'] state_rule = StateRule(self._env_id, 'pending') self._rules.append(state_rule) self._walk(model, owner_id=tenant_id) # Convert MuranoProperty containing reference to another object # to MuranoRelationship. object_ids = [rule.obj_id for rule in self._rules if isinstance(rule, ObjectRule)] self._rules = [self._create_relationship(rule, object_ids) for rule in self._rules] relations = [(rel.source_id, rel.target_id) for rel in self._rules if isinstance(rel, RelationshipRule)] closure = self.transitive_closure(relations) for rel in closure: self._rules.append(ConnectedRule(rel[0], rel[1])) return self._rules @staticmethod def transitive_closure(relations): """Computes transitive closure on a directed graph. In other words computes reachability within the graph. E.g. {(1, 2), (2, 3)} -> {(1, 2), (2, 3), (1, 3)} (1, 3) was added because there is path from 1 to 3 in the graph. :param relations: list of relations/edges in form of tuples :return: transitive closure including original relations """ closure = set(relations) while True: # Attempts to discover new transitive relations # by joining 2 subsequent relations/edges within the graph. new_relations = {(x, w) for x, y in closure for q, w in closure if q == y} # Creates union with already discovered relations. closure_until_now = closure | new_relations # If no new relations were discovered in last cycle # the computation is finished. if closure_until_now == closure: return closure closure = closure_until_now def _walk(self, obj, owner_id, path=()): if obj is None: return obj = self._to_dict(obj) new_owner = self._process_item(obj, owner_id, path) or owner_id if isinstance(obj, list) or isinstance(obj, tuple): for v in obj: self._walk(v, new_owner, path) elif isinstance(obj, dict): for key, value in obj.items(): self._walk(value, new_owner, path + (key, )) def _process_item(self, obj, owner_id, path): if isinstance(obj, dict) and '?' in obj: obj_rule = self._create_object_rule(obj, owner_id) self._rules.append(obj_rule) # the environment has 'services' relationships # to all its top-level applications # traversal path is used to test whether # we are at the right place within the tree if path == ('applications',): self._rules.append(RelationshipRule(self._env_id, obj_rule.obj_id, "services")) self._rules.extend( self._create_property_rules(obj_rule.obj_id, obj)) cls = obj['?']['type'] if 'classVersion' in obj['?']: version_spec = obj['?']['classVersion'] else: version_spec = '*' types = self._get_parent_types( cls, self._package_loader, version_spec) self._rules.extend(self._create_parent_type_rules(obj['?']['id'], types)) # current object will be the owner for its subtree return obj_rule.obj_id @staticmethod def _to_dict(obj): # If we have MuranoObject class we need to convert to dictionary. if 'to_dictionary' in dir(obj): return obj.to_dictionary() else: return obj def _create_object_rule(self, app, owner_id): return ObjectRule(app['?']['id'], owner_id, app['?']['type']) def _create_property_rules(self, obj_id, obj, prefix=""): rules = [] # Skip when inside properties of other object. if '?' in obj and prefix != "": rules.append(RelationshipRule(obj_id, obj['?']['id'], prefix.split('.')[0])) return rules for key, value in obj.items(): if key == '?': continue if value is not None: value = self._to_dict(value) if isinstance(value, dict): rules.extend(self._create_property_rules( obj_id, value, prefix + key + ".")) elif isinstance(value, list) or isinstance(obj, tuple): for v in value: v = self._to_dict(v) if not isinstance(v, dict): rule = PropertyRule(obj_id, prefix + key, v) rules.append(rule) else: rule = PropertyRule(obj_id, prefix + key, value) rules.append(rule) return rules @staticmethod def _is_relationship(rule, app_ids): if not isinstance(rule, PropertyRule): return False return rule.prop_value in app_ids def _create_relationship(self, rule, app_ids): if self._is_relationship(rule, app_ids): return RelationshipRule(rule.obj_id, rule.prop_value, rule.prop_name) else: return rule @staticmethod def _get_parent_types(type_name, package_loader, version_spec): type_name, version_spec, _ = helpers.parse_type_string( type_name, version_spec, None) version_spec = helpers.parse_version_spec(version_spec) result = {type_name} if package_loader: pkg = package_loader.load_class_package(type_name, version_spec) cls = pkg.find_class(type_name, False) if cls: result.update(t.name for t in cls.ancestors()) return result @staticmethod def _create_parent_type_rules(app_id, types): rules = [] for type_name in types: rules.append(ParentTypeRule(app_id, type_name)) return rules class ObjectRule(object): def __init__(self, obj_id, owner_id, type_name): self.obj_id = obj_id self.owner_id = owner_id self.type_name = helpers.parse_type_string(type_name, None, None)[0] def __str__(self): return 'murano:objects+("{0}", "{1}", "{2}")'.format(self.obj_id, self.owner_id, self.type_name) class PropertyRule(object): def __init__(self, obj_id, prop_name, prop_value): self.obj_id = obj_id self.prop_name = prop_name self.prop_value = prop_value def __str__(self): return 'murano:properties+("{0}", "{1}", "{2}")'.format( self.obj_id, self.prop_name, self.prop_value) class RelationshipRule(object): def __init__(self, source_id, target_id, rel_name): self.source_id = source_id self.target_id = target_id self.rel_name = rel_name def __str__(self): return 'murano:relationships+("{0}", "{1}", "{2}")'.format( self.source_id, self.target_id, self.rel_name) class ConnectedRule(object): def __init__(self, source_id, target_id): self.source_id = source_id self.target_id = target_id def __str__(self): return 'murano:connected+("{0}", "{1}")'.format( self.source_id, self.target_id) class ParentTypeRule(object): def __init__(self, obj_id, type_name): self.obj_id = obj_id self.type_name = type_name def __str__(self): return 'murano:parent_types+("{0}", "{1}")'.format(self.obj_id, self.type_name) class StateRule(object): def __init__(self, obj_id, state): self.obj_id = obj_id self.state = state def __str__(self): return 'murano:states+("{0}", "{1}")'.format(self.obj_id, self.state) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/policy/model_policy_enforcer.py0000664000175000017500000001372100000000000022642 0ustar00zuulzuul00000000000000# Copyright (c) 2014 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. import re try: # integration with congress is optional import congressclient.v1.client as congress_client except ImportError: congress_client = None from oslo_log import log as logging from murano.common import auth_utils from murano.common.i18n import _ from murano.policy import congress_rules from murano.policy.modify.actions import action_manager as am LOG = logging.getLogger(__name__) class ValidationError(Exception): """Raised for validation errors.""" pass class ModelPolicyEnforcer(object): """Policy Enforcer Implementation using Congress client Converts murano model to list of congress data rules. We ask congress using simulation api of congress rest client to resolve "murano_system:predeploy_errors(env_id, obj_id, msg)" table along with congress data rules to return validation results. """ def __init__(self, execution_session, action_manager=None): self._execution_session = execution_session self._action_manager = action_manager or am.ModifyActionManager() self._client = None def _create_client(self): if not congress_client: # congress client was not imported raise ImportError("Import congresscliet error") return congress_client.Client( **auth_utils.get_session_client_parameters( service_type='policy', execution_session=self._execution_session)) @property def client(self): if self._client is None: self._client = self._create_client() return self._client def modify(self, obj, package_loader=None): """Modifies model using Congress rule engine. @type obj: object model @param obj: Representation of model starting on environment level (['Objects']) @type class_loader: murano.dsl.class_loader.MuranoClassLoader @param class_loader: Optional. Used for evaluating parent class types @raises ValidationError in case validation was not successful """ model = obj.to_dictionary() LOG.debug('Modifying model') LOG.debug(model) env_id = model['?']['id'] result = self._execute_simulation(package_loader, env_id, model, 'predeploy_modify(eid, oid, action)') raw_actions = result["result"] if raw_actions: actions = self._parse_simulation_result('predeploy_modify', env_id, raw_actions) for action in actions: self._action_manager.apply_action(obj, action) def validate(self, model, package_loader=None): """Validate model using Congress rule engine. @type model: dict @param model: Dictionary representation of model starting on environment level (['Objects']) @type package_loader: murano.dsl.package_loader.MuranoPackageLoader @param package_loader: Optional. Used for evaluating parent class types @raises ValidationError in case validation was not successful """ if model is None: return env_id = model['?']['id'] validation_result = self._execute_simulation( package_loader, env_id, model, 'predeploy_errors(eid, oid, msg)') if validation_result["result"]: messages = self._parse_simulation_result( 'predeploy_errors', env_id, validation_result["result"]) if messages: result_str = "\n ".join(map(str, messages)) msg = _("Murano object model validation failed: {0}").format( "\n " + result_str) LOG.error(msg) raise ValidationError(msg) else: LOG.info('Model valid') def _execute_simulation(self, package_loader, env_id, model, query): rules = congress_rules.CongressRulesManager().convert( model, package_loader, self._execution_session.project_id) rules_str = list(map(str, rules)) # cleanup of data populated by murano driver rules_str.insert(0, 'deleteEnv("{0}")'.format(env_id)) rules_line = " ".join(rules_str) LOG.debug('Congress rules: \n {rules} ' .format(rules='\n '.join(rules_str))) validation_result = self.client.execute_policy_action( "murano_system", "simulate", False, False, {'query': query, 'action_policy': 'murano_action', 'sequence': rules_line}) return validation_result @staticmethod def _parse_simulation_result(query, env_id, results): """Transforms the list of results Transforms a list of strings in format: ['predeploy_errors("env_id_1", "obj_id_1", "message1")', 'predeploy_errors("env_id_2", "obj_id_2", "message2")'] to a list of strings with message only filtered to provided env_id (e.g. 'env_id_1'): ['message2'] """ messages = [] regexp = query + '\("([^"]*)",\s*"([^"]*)",\s*"(.*)"\)' for result in results: match = re.search(regexp, result) if match: if env_id in match.group(1): messages.append(match.group(3)) return messages ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1696417899.809181 murano-16.0.0/murano/policy/modify/0000775000175000017500000000000000000000000017211 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/policy/modify/__init__.py0000664000175000017500000000000000000000000021310 0ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1696417899.809181 murano-16.0.0/murano/policy/modify/actions/0000775000175000017500000000000000000000000020651 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/policy/modify/actions/__init__.py0000664000175000017500000000000000000000000022750 0ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/policy/modify/actions/action_manager.py0000664000175000017500000000651300000000000024177 0ustar00zuulzuul00000000000000# Copyright (c) 2015 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. from oslo_log import log as logging from oslo_utils import importutils from stevedore import extension import yaml LOG = logging.getLogger(__name__) class ModifyActionManager(object): """Manages modify actions The manager encapsulates extensible plugin mechanism for modify actions loading. Provides ability to apply action on given object model based on action specification retrieved from congress """ def __init__(self): self._cache = {} def load_action(self, name): """Loads action by its name Loaded actions are cached. Plugin mechanism is based on distutils entry points. Entry point namespace is 'murano_policy_modify_actions' :param name: action name :return: """ if name in self._cache: return self._cache[name] action = self._load_action(name) self._cache[name] = action return action @staticmethod def _load_action(name): mgr = extension.ExtensionManager( namespace='murano_policy_modify_actions', invoke_on_load=False ) for ext in mgr.extensions: if name == ext.name: target = ext.entry_point_target.replace(':', '.') return importutils.import_class(target) raise ValueError('No such action definition: {action_name}' .format(action_name=name)) def apply_action(self, obj, action_spec): """Apply action on given model Parse action and its parameters from action specification retrieved from congress. Action specification is YAML format. E.g. remove-object: {object_id: abc123}") Action names are keys in top-level dictionary. Values are dictionaries containing key/value parameters of the action :param obj: subject of modification :param action_spec: YAML action spec :raise ValueError: in case of malformed action spec """ actions = yaml.safe_load(action_spec) if not isinstance(actions, dict): raise ValueError('Expected action spec format is ' '"action-name: {{p1: v1, ...}}" ' 'but got "{action_spec}"' .format(action_spec=action_spec)) for name, kwargs in actions.items(): LOG.debug('Executing action {name}, params {params}' .format(name=name, params=kwargs)) # loads action class action_class = self.load_action(name) # creates action instance action_instance = action_class(**kwargs) # apply action on object model action_instance.modify(obj) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/policy/modify/actions/base.py0000664000175000017500000000301300000000000022132 0ustar00zuulzuul00000000000000# Copyright (c) 2015 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. import abc class ModifyActionBase(object, metaclass=abc.ABCMeta): """Base class for model modify actions. Class is instantiated base on list of actions returned from congress then is performed on given object model. Base action class initializer doesn't have arguments. However, concrete action classes may have any number of parameters defining action behavior. These parameters must correspond to parameters returned from congress. Action must be registered/exposed to action manager via entry point 'murano_policy_modify_actions'. Only actions registered via this entry point are can be used. """ @abc.abstractmethod def modify(self, model): """Modifies given object model Action parameters are available as instance variables passed to initializer. :param model: object model to be modified """ pass ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/policy/modify/actions/default_actions.py0000664000175000017500000001523300000000000024373 0ustar00zuulzuul00000000000000# Copyright (c) 2015 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. """ Default actions reside in this module. These action are available out of the box for Murano users. """ import yaql.language.utils as utils import murano.dsl.exceptions as exceptions import murano.dsl.murano_object as mo import murano.policy.modify.actions.base as base class ActionUtils(object): def _get_objects_by_id(self, obj, objects=None): if objects is None: objects = {} if isinstance(obj, mo.MuranoObject): objects[obj.object_id] = obj for prop_name in obj.type.properties: try: prop_val = obj.get_property(prop_name) except exceptions.UninitializedPropertyAccessError: continue self._get_objects_by_id(prop_val, objects) elif isinstance(obj, tuple): for item in obj: self._get_objects_by_id(item, objects) elif isinstance(obj, utils.FrozenDict): for k, v in obj.items(): self._get_objects_by_id(k, objects) self._get_objects_by_id(v, objects) return objects @staticmethod def _check_present(obj_id, objects): if obj_id not in objects.keys(): raise ValueError('No such object, obj_id: {0}' .format(obj_id)) class RemoveObjectAction(base.ModifyActionBase, ActionUtils): """Remove object from given model""" def __init__(self, object_id): """Initializes action parameters :param object_id: id of an object """ self._object_id = object_id @staticmethod def _match_object_id(obj_id, obj): return isinstance(obj, mo.MuranoObject) and obj_id == obj.object_id def modify(self, obj): """Remove object from given model""" objects = self._get_objects_by_id(obj) self._check_present(self._object_id, objects) for _obj in objects.values(): for prop_name in _obj.type.properties: try: val = _obj.get_property(prop_name) except exceptions.UninitializedPropertyAccessError: continue if self._match_object_id(self._object_id, val): _obj.set_property(prop_name, None) # remove object from list elif isinstance(val, tuple): filtered_list = list(val) for item in val: if self._match_object_id(self._object_id, item): filtered_list.remove(item) if len(filtered_list) < len(val): _obj.set_property(prop_name, tuple(filtered_list)) # remove object from dict elif isinstance(val, utils.FrozenDict): filtered_dict = {k: v for k, v in val.items() if not self._match_object_id(self._object_id, k) and not self._match_object_id(self._object_id, v)} if len(filtered_dict) < len(val): _obj.set_property(prop_name, utils.FrozenDict(filtered_dict)) class SetPropertyAction(base.ModifyActionBase, ActionUtils): """Set property on given object""" def __init__(self, object_id, prop_name, value): """Initializes action parameters :param object_id: id of an object :param prop_name: property name :param value: new value of the property """ self._object_id = object_id self._prop_name = prop_name self._value = value def modify(self, obj): """Set property on given object in model""" objects = self._get_objects_by_id(obj) self._check_present(self._object_id, objects) target_obj = objects[self._object_id] target_obj.set_property(self._prop_name, self._value) class RemoveRelationAction(SetPropertyAction): """Remove relation from given model""" def __init__(self, object_id, prop_name): super(RemoveRelationAction, self).__init__(object_id, prop_name, None) class AddObjectAction(base.ModifyActionBase, ActionUtils): """Add new object to object model""" def __init__(self, owner_id, owner_relation, type, init_args): """Initializes action parameters :param owner_id: id of an owner :param owner_relation: name of relation on owner :param type: new object type :param init_args: properties of the new object """ self._init_args = init_args self._type = type self._owner_relation = owner_relation self._owner_id = owner_id def modify(self, model): """Creates new object and adds it to the model""" new_obj = {'type': type} new_obj.update(self._init_args) objects = self._get_objects_by_id(model) self._check_present(self._owner_id, objects) owner = objects[self._owner_id] # adding objects to list or dict is not supported for now owner.set_property(self._owner_relation, new_obj) class AddRelationAction(base.ModifyActionBase, ActionUtils): """Adds relation between two existing objects within model""" def __init__(self, source_id, relation, target_id): """Initializes action parameters :param source_id: id of an relation source :param relation: name of relation on source object :param target_id: id of an relation target """ self._source_id = source_id self._relation = relation self._target_id = target_id def modify(self, model): """Creates new object and adds it to the model""" objects = self._get_objects_by_id(model) self._check_present(self._source_id, objects) self._check_present(self._target_id, objects) source = objects[self._source_id] target = objects[self._target_id] # adding objects to list or dict is not supported for now source.set_property(self._relation, target) ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1696417899.809181 murano-16.0.0/murano/services/0000775000175000017500000000000000000000000016246 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/services/__init__.py0000664000175000017500000000000000000000000020345 0ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/services/actions.py0000664000175000017500000001101400000000000020255 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # 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 murano.common import rpc from murano.db import models from murano.db.services import actions as actions_db from murano.services import states class ActionServices(object): @staticmethod def create_action_task(action_name, target_obj, args, environment, session, context): action = None if action_name and target_obj: action = { 'object_id': target_obj, 'method': action_name, 'args': args or {} } task = { 'action': action, 'model': session.description, 'token': context.auth_token, 'project_id': context.project_id, 'user_id': context.user, 'id': environment.id } if session.description['Objects'] is not None: task['model']['Objects']['?']['id'] = environment.id task['model']['Objects']['applications'] = \ task['model']['Objects'].pop('services', []) return task @staticmethod def update_task(action, session, task, unit): session.state = states.SessionState.DEPLOYING task_info = models.Task() task_info.environment_id = session.environment_id task_info.description = dict(session.description.get('Objects')) task_info.action = task['action'] status = models.Status() status.text = 'Action {0} is scheduled'.format(action[1]['name']) status.level = 'info' task_info.statuses.append(status) with unit.begin(): unit.add(session) unit.add(task_info) @staticmethod def submit_task(action_name, target_obj, args, environment, session, context, unit): task = ActionServices.create_action_task( action_name, target_obj, args, environment, session, context) task_id = actions_db.update_task(action_name, session, task, unit) rpc.engine().handle_task(task) return task_id @staticmethod def execute(action_id, session, unit, context, args=None): if args is None: args = {} environment = actions_db.get_environment(session, unit) action = ActionServices.find_action(session.description, action_id) if action is None: raise LookupError('Action is not found') if not action[1].get('enabled', True): raise ValueError('Cannot execute disabled action') return ActionServices.submit_task( action[1]['name'], action[0], args, environment, session, context, unit) @staticmethod def find_action(model, action_id): """Find_action for object def with specified action Traverses object model looking for an object definition containing specified action :param model: object model :param action_id: ID of an action :return: tuple (object id, {"name": "action_name_in_MuranoPL", "enabled": True }) """ if isinstance(model, list): for item in model: result = ActionServices.find_action(item, action_id) if result is not None: return result elif isinstance(model, dict): if '?' in model and 'id' in model['?'] and \ '_actions' in model['?'] and \ action_id in model['?']['_actions']: return model['?']['id'], model['?']['_actions'][action_id] for obj in model.values(): result = ActionServices.find_action(obj, action_id) if result is not None: return result else: return None @staticmethod def get_result(environment_id, task_id, unit): task = unit.query(models.Task).filter_by( id=task_id, environment_id=environment_id).first() if task is not None: return task.result return None ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/services/states.py0000664000175000017500000000240500000000000020124 0ustar00zuulzuul00000000000000# 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 SessionState = collections.namedtuple('SessionState', [ 'OPENED', 'DEPLOYING', 'DEPLOYED', 'DEPLOY_FAILURE', 'DELETING', 'DELETE_FAILURE' ])( OPENED='opened', DEPLOYING='deploying', DEPLOYED='deployed', DEPLOY_FAILURE='deploy failure', DELETING='deleting', DELETE_FAILURE='delete failure' ) EnvironmentStatus = collections.namedtuple('EnvironmentStatus', [ 'READY', 'PENDING', 'DEPLOYING', 'DEPLOY_FAILURE', 'DELETING', 'DELETE_FAILURE' ])( READY='ready', PENDING='pending', DEPLOYING='deploying', DEPLOY_FAILURE='deploy failure', DELETING='deleting', DELETE_FAILURE='delete failure' ) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/services/static_actions.py0000664000175000017500000000237300000000000021634 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # 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 uuid from murano.common import rpc class StaticActionServices(object): @staticmethod def execute(method_name, class_name, pkg_name, class_version, args, credentials): action = { 'method': method_name, 'args': args or {}, 'class_name': class_name, 'pkg_name': pkg_name, 'class_version': class_version } task = { 'action': action, 'token': credentials['token'], 'project_id': credentials['project_id'], 'user_id': credentials['user_id'], 'id': str(uuid.uuid4()) } return rpc.engine().call_static_action(task) ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1696417899.809181 murano-16.0.0/murano/tests/0000775000175000017500000000000000000000000015565 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/__init__.py0000664000175000017500000000000000000000000017664 0ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1696417899.809181 murano-16.0.0/murano/tests/functional/0000775000175000017500000000000000000000000017727 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/functional/README.rst0000664000175000017500000000021100000000000021410 0ustar00zuulzuul00000000000000===== MOVED ===== The Congress and Mistral functional integration tests has moved to http://opendev.org/openstack/murano-tempest-plugin ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1696417899.813181 murano-16.0.0/murano/tests/unit/0000775000175000017500000000000000000000000016544 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/__init__.py0000664000175000017500000000000000000000000020643 0ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1696417899.813181 murano-16.0.0/murano/tests/unit/api/0000775000175000017500000000000000000000000017315 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/api/__init__.py0000664000175000017500000000000000000000000021414 0ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/api/base.py0000664000175000017500000002162600000000000020610 0ustar00zuulzuul00000000000000# Copyright (c) 2014 Hewlett-Packard Development Company, L.P. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or # implied. # See the License for the specific language governing permissions and # limitations under the License. import logging from unittest import mock import fixtures from oslo_config import cfg from oslo_utils import timeutils import routes import urllib import webob from murano.api.v1 import request_statistics from murano.api.v1 import router from murano.common import policy from murano.common import rpc from murano.common import wsgi from murano.tests.unit import base from murano.tests.unit import utils TEST_DEFAULT_LOGLEVELS = {'migrate': logging.WARN, 'sqlalchemy': logging.WARN} def test_with_middleware(self, middleware, func, req, *args, **kwargs): @webob.dec.wsgify def _app(req): return func(req, *args, **kwargs) resp = middleware(_app).process_request(req) return resp class FakeLogMixin(object): """Allows logs to be tested Allow logs to be tested (rather than just disabling logging. This is taken from heat """ def setup_logging(self): # Assign default logs to self.LOG so we can still # assert on heat logs. self.LOG = self.useFixture( fixtures.FakeLogger(level=logging.DEBUG)) base_list = set([nlog.split('.')[0] for nlog in logging.Logger.manager.loggerDict]) for base_name in base_list: if base_name in TEST_DEFAULT_LOGLEVELS: self.useFixture(fixtures.FakeLogger( level=TEST_DEFAULT_LOGLEVELS[base_name], name=base_name)) elif base_name != 'murano': self.useFixture(fixtures.FakeLogger( name=base_name)) class MuranoApiTestCase(base.MuranoWithDBTestCase, FakeLogMixin): # Set this if common.rpc is imported into other scopes so that # it can be mocked properly RPC_IMPORT = 'murano.common.rpc' def setUp(self): super(MuranoApiTestCase, self).setUp() self.setup_logging() # Mock the RPC classes self.mock_api_rpc = mock.Mock(rpc.ApiClient) self.mock_engine_rpc = mock.Mock(rpc.EngineClient) mock.patch(self.RPC_IMPORT + '.engine', return_value=self.mock_engine_rpc).start() mock.patch(self.RPC_IMPORT + '.api', return_value=self.mock_api_rpc).start() mock.patch('murano.db.services.environments.EnvironmentServices.' 'get_network_driver', return_value='neutron').start() self.addCleanup(mock.patch.stopall) def tearDown(self): super(MuranoApiTestCase, self).tearDown() timeutils.utcnow.override_time = None def _stub_uuid(self, values=None): class FakeUUID(object): def __init__(self, v): self.hex = v if values is None: values = [] mock_uuid4 = mock.patch('uuid.uuid4').start() mock_uuid4.side_effect = [FakeUUID(v) for v in values] return mock_uuid4 class ControllerTest(object): """Common utilities for testing API Controllers.""" DEFAULT_USER = 'test_user' DEFAULT_TENANT = 'test_tenant' def __init__(self, *args, **kwargs): super(ControllerTest, self).__init__(*args, **kwargs) # cfg.CONF.set_default('host', 'server.test') self.api_version = '1.0' self.tenant = 'test_tenant' self.user = 'test_user' self.mock_policy_check = None self.mapper = routes.Mapper() self.api = router.API(self.mapper) request_statistics.init_stats() def setUp(self): super(ControllerTest, self).setUp() self.is_admin = False cfg.CONF(args=[], project='murano') policy.init(use_conf=False) real_policy_check = policy.check self._policy_check_expectations = [] self._actual_policy_checks = [] def wrap_policy_check(rule, ctxt, target=None, **kwargs): if target is None: target = {} self._actual_policy_checks.append((rule, target)) return real_policy_check(rule, ctxt, target=target, **kwargs) mock.patch('murano.common.policy.check', side_effect=wrap_policy_check).start() # Deny everything self._set_policy_rules({"default": "!"}) def _environ(self, path): return { 'SERVER_NAME': 'server.test', 'SERVER_PORT': '8082', 'SERVER_PROTOCOL': 'http', 'SCRIPT_NAME': '/v1', 'PATH_INFO': path, 'wsgi.url_scheme': 'http', 'QUERY_STRING': '', 'CONTENT_TYPE': 'application/json', } def _simple_request(self, path, params=None, method='GET', user=DEFAULT_USER, tenant=DEFAULT_TENANT): """Returns a request with a fake but valid-looking context Returns a request with a fake but valid-looking context and sets the request environment variables. If `params` is given, it should be a dictionary or sequence of tuples. """ environ = self._environ(path) environ['REQUEST_METHOD'] = method if params: qs = urllib.parse.urlencode(params) environ['QUERY_STRING'] = qs req = wsgi.Request(environ) req.context = utils.dummy_context(user, tenant, is_admin=self.is_admin) self.context = req.context return req def _get(self, path, params=None, user=DEFAULT_USER, tenant=DEFAULT_TENANT): return self._simple_request(path, params=params, user=user, tenant=tenant) def _get_with_accept(self, path, params=None, user=DEFAULT_USER, tenant=DEFAULT_TENANT, accept='application/octet-stream'): req = self._simple_request(path, params=params, user=user, tenant=tenant) req.accept = accept return req def _delete(self, path, params=None, user=DEFAULT_USER, tenant=DEFAULT_TENANT): if params is None: params = {} return self._simple_request(path, params=params, method='DELETE', user=user, tenant=tenant) def _data_request(self, path, data, content_type='application/json', method='POST', params=None, user=DEFAULT_USER, tenant=DEFAULT_TENANT): if params is None: params = {} environ = self._environ(path) environ['REQUEST_METHOD'] = method req = wsgi.Request(environ) req.context = utils.dummy_context(user, tenant, is_admin=self.is_admin) self.context = req.context req.content_type = content_type req.body = data if params: qs = urllib.parse.urlencode(params) environ['QUERY_STRING'] = qs return req def _post(self, path, data, content_type='application/json', params=None, user=DEFAULT_USER, tenant=DEFAULT_TENANT): if params is None: params = {} return self._data_request(path, data, content_type, params=params, user=user, tenant=tenant) def _put(self, path, data, content_type='application/json', params=None, user=DEFAULT_USER, tenant=DEFAULT_TENANT): if params is None: params = {} return self._data_request(path, data, content_type, method='PUT', params=params, user=user, tenant=tenant) def _patch(self, path, data, content_type='application/murano-packages-json-patch', params=None, user=DEFAULT_USER, tenant=DEFAULT_TENANT): if params is None: params = {} return self._data_request(path, data, content_type, method='PATCH', params=params, user=user, tenant=tenant) def _set_policy_rules(self, rules): policy.set_rules(rules, default_rule='default') def expect_policy_check(self, action, target=None): if target is None: target = {} self._policy_check_expectations.append((action, target)) def _assert_policy_checks(self): self.assertEqual(self._policy_check_expectations, self._actual_policy_checks) def tearDown(self): self._assert_policy_checks() policy.reset() super(ControllerTest, self).tearDown() ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1696417899.813181 murano-16.0.0/murano/tests/unit/api/cmd/0000775000175000017500000000000000000000000020060 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/api/cmd/__init__.py0000664000175000017500000000000000000000000022157 0ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1696417899.813181 murano-16.0.0/murano/tests/unit/api/cmd/test_package/0000775000175000017500000000000000000000000022512 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1696417899.813181 murano-16.0.0/murano/tests/unit/api/cmd/test_package/Classes/0000775000175000017500000000000000000000000024107 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/api/cmd/test_package/Classes/Mytest1.yaml0000664000175000017500000000025600000000000026344 0ustar00zuulzuul00000000000000Namespaces: =: io.murano.test Extends: TestFixture Name: MyTest1 Methods: setUp: Body: testSimple1: Body: testSimple2: Body: tearDown: Body:././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/api/cmd/test_package/Classes/Mytest2.yaml0000664000175000017500000000023700000000000026344 0ustar00zuulzuul00000000000000Namespaces: =: io.murano.test Extends: TestFixture Name: MyTest2 Methods: testSimple1: Body: testSimple2: Body: thisIsNotAtestMethod: ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/api/cmd/test_package/Classes/Mytest3.yaml0000664000175000017500000000021100000000000026335 0ustar00zuulzuul00000000000000Namespaces: =: io.murano.test Name: MyTest3 Methods: testSimple1: Body: testSimple2: Body: thisIsNotAtestMethod: ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/api/cmd/test_package/manifest.yaml0000664000175000017500000000046700000000000025213 0ustar00zuulzuul00000000000000Format: 1.0 Type: Application FullName: io.murano.test.MyTest1 Name: Test case Example Description: | Example of test-case. Mocks are not used in this app Author: 'Mirantis, Inc' Tags: [] Classes: io.murano.test.MyTest1: Mytest1.yaml io.murano.test.MyTest2: Mytest2.yaml io.murano.test.MyTest3: Mytest3.yaml././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/api/cmd/test_test_runner.py0000664000175000017500000001564600000000000024055 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # 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 io import StringIO import os import sys from unittest import mock import fixtures from oslo_config import cfg from oslo_log import log as logging from oslo_utils import importutils import testtools from murano.cmd import test_runner from murano import version CONF = cfg.CONF logging.register_options(CONF) logging.setup(CONF, 'murano') class TestCaseShell(testtools.TestCase): def setUp(self): super(TestCaseShell, self).setUp() self.auth_params = {'username': 'test', 'password': 'test', 'project_name': 'test', 'auth_url': 'http://localhost:5000'} self.args = ['test-runner.py'] for k, v in self.auth_params.items(): k = '--os-' + k.replace('_', '-') self.args.extend([k, v]) self.useFixture(fixtures.MonkeyPatch('keystoneclient.v3.client.Client', mock.MagicMock)) dirs = [os.path.dirname(__file__), os.path.join(os.path.dirname(__file__), os.pardir, os.pardir, os.pardir, os.pardir, os.pardir, 'meta')] self.override_config('load_packages_from', dirs, 'engine') def tearDown(self): super(TestCaseShell, self).tearDown() CONF.clear() def override_config(self, name, override, group=None): CONF.set_override(name, override, group) CONF.set_override('use_stderr', True) self.addCleanup(CONF.clear_override, name, group) def shell(self, cmd_args=None, exitcode=0): stdout = StringIO() stderr = StringIO() args = self.args if cmd_args: cmd_args = cmd_args.split() args.extend(cmd_args) with mock.patch.object(sys, 'stdout', stdout): with mock.patch.object(sys, 'stderr', stderr): with mock.patch.object(sys, 'argv', args): result = self.assertRaises(SystemExit, test_runner.main) self.assertEqual(result.code, exitcode, 'Command finished with error.') stdout = stdout.getvalue() stderr = stderr.getvalue() return (stdout, stderr) def test_help(self): stdout, _ = self.shell('--help') self.assertIn('', stdout) self.assertIn('className.testMethod', stdout) def test_version(self): stdout, stderr = self.shell('--version') output = stdout self.assertIn(version.version_string, output) @mock.patch.object(test_runner, 'LOG') def test_increase_verbosity(self, mock_log): self.shell('io.murano.test.MyTest1 -v') mock_log.logger.setLevel.assert_called_with(logging.DEBUG) @mock.patch('keystoneclient.v3.client.Client') def test_os_params_replaces_config(self, mock_client): # Load keystone configuration parameters from config importutils.import_module('keystonemiddleware.auth_token') self.override_config('admin_user', 'new_value', 'keystone_authtoken') self.shell('io.murano.test.MyTest1 io.murano.test.MyTest2') mock_client.assert_has_calls([mock.call(**self.auth_params)]) def test_package_all_tests(self): _, stderr = self.shell('io.murano.test.MyTest1 -v') # NOTE(efedorova): May be, there is a problem with test-runner, since # all logs are passed to stderr self.assertIn('Test io.murano.test.MyTest1.testSimple1 successful', stderr) self.assertIn('Test io.murano.test.MyTest1.testSimple2 successful', stderr) self.assertIn('Test io.murano.test.MyTest2.testSimple1 successful', stderr) self.assertIn('Test io.murano.test.MyTest2.testSimple2 successful', stderr) self.assertNotIn('thisIsNotAtestMethod', stderr) def test_package_by_class(self): _, stderr = self.shell( 'io.murano.test.MyTest1 io.murano.test.MyTest2 -v') self.assertNotIn('Test io.murano.test.MyTest1.testSimple1 successful', stderr) self.assertNotIn('Test io.murano.test.MyTest1.testSimple2 successful', stderr) self.assertIn('Test io.murano.test.MyTest2.testSimple1 successful', stderr) self.assertIn('Test io.murano.test.MyTest2.testSimple2 successful', stderr) def test_package_by_test_name(self): _, stderr = self.shell( 'io.murano.test.MyTest1 testSimple1 -v') self.assertIn('Test io.murano.test.MyTest1.testSimple1 successful', stderr) self.assertNotIn('Test io.murano.test.MyTest1.testSimple2 successful', stderr) self.assertIn('Test io.murano.test.MyTest2.testSimple1 successful', stderr) self.assertNotIn('Test io.murano.test.MyTest2.testSimple2 successful', stderr) def test_package_by_test_and_class_name(self): _, stderr = self.shell( 'io.murano.test.MyTest1 io.murano.test.MyTest2.testSimple1 -v') self.assertNotIn('Test io.murano.test.MyTest1.testSimple1 successful', stderr) self.assertNotIn('Test io.murano.test.MyTest1.testSimple2 successful', stderr) self.assertIn('Test io.murano.test.MyTest2.testSimple1 successful', stderr) self.assertNotIn('Test io.murano.test.MyTest2.testSimple2 successful', stderr) def test_service_methods(self): _, stderr = self.shell( 'io.murano.test.MyTest1 io.murano.test.MyTest1.testSimple1 -v') self.assertIn('Executing: io.murano.test.MyTest1.setUp', stderr) self.assertIn('Executing: io.murano.test.MyTest1.tearDown', stderr) def test_package_is_not_provided(self): _, stderr = self.shell(exitcode=2) err = 'the following arguments are required: ' self.assertIn('murano-test-runner: error: %s' % err, stderr) def test_wrong_parent(self): _, stderr = self.shell( 'io.murano.test.MyTest1 io.murano.test.MyTest3 -v', exitcode=1) self.assertIn('Class io.murano.test.MyTest3 is not inherited from' ' io.murano.test.TestFixture. Skipping it.', stderr) self.assertIn('No tests found for execution.', stderr) ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1696417899.813181 murano-16.0.0/murano/tests/unit/api/middleware/0000775000175000017500000000000000000000000021432 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/api/middleware/__init__.py0000664000175000017500000000000000000000000023531 0ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/api/middleware/test_context.py0000664000175000017500000000277600000000000024543 0ustar00zuulzuul00000000000000# Copyright 2016 AT&T Corp # 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. import webob from murano.api.middleware import context from murano.tests.unit import base from oslo_config import cfg CONF = cfg.CONF class MiddlewareContextTest(base.MuranoTestCase): def test_middleware_context_default(self): middleware = context.ContextMiddleware(None) request_headers = { 'X-Roles': 'admin', 'X-User-Id': "", 'X-Project-Id': "", 'X-Configuration-Session': "", } request = webob.Request.blank('/environments', headers=request_headers) self.assertFalse(hasattr(request, 'context')) middleware.process_request(request) self.assertTrue(hasattr(request, 'context')) def test_factory_returns_filter(self): middleware = context.ContextMiddleware(None) result = middleware.factory(CONF) self.assertIsNotNone(result) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/api/middleware/test_ext_context.py0000664000175000017500000001161300000000000025411 0ustar00zuulzuul00000000000000# Copyright 2016 AT&T Corp # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from unittest import mock import webob from keystoneauth1 import exceptions from murano.api.middleware import ext_context from murano.tests.unit import base from oslo_serialization import base64 class MiddlewareExtContextTest(base.MuranoTestCase): def test_middleware_ext_context_default(self): middleware = ext_context.ExternalContextMiddleware(None) middleware.get_keystone_token = mock.MagicMock(return_value="token?") auth = 'Basic {}'.format(base64.encode_as_text(b'test:test')) request_headers = { 'Authorization': auth, } request = webob.Request.blank('/environments', headers=request_headers) middleware.process_request(request) self.assertEqual(request.headers.get('X-Auth-Token'), "token?") def test_middleware_ext_context_except_key_error(self): middleware = ext_context.ExternalContextMiddleware(None) middleware.get_keystone_token = mock.MagicMock( side_effect=KeyError('test key error') ) auth = 'Basic {}'.format(base64.encode_as_text(b'test:test')) request_headers = { 'Authorization': auth, } request = webob.Request.blank('/environments', headers=request_headers) self.assertRaises(webob.exc.HTTPUnauthorized, middleware.process_request, request) def test_middleware_ext_context_except_unauthorized(self): middleware = ext_context.ExternalContextMiddleware(None) middleware.get_keystone_token = mock.MagicMock( side_effect=exceptions.Unauthorized('') ) auth = 'Basic {}'.format(base64.encode_as_text(b'test:test')) request_headers = { 'Authorization': auth, } request = webob.Request.blank('/environments', headers=request_headers) self.assertRaises(webob.exc.HTTPUnauthorized, middleware.process_request, request) @mock.patch('murano.api.middleware.ext_context.ks_session') @mock.patch('murano.api.middleware.ext_context.v3') @mock.patch('murano.api.middleware.ext_context.CONF') def test_get_keystone_token(self, mock_conf, mock_v3, mock_ks_session): mock_ks_session.Session().get_token.return_value = 'test_token' test_auth_urls = ['test_url', 'test_url/v2.0', 'test_url/v3'] for url in test_auth_urls: mock_conf.cfapi = mock.MagicMock(auth_url=url, tenant='test_tenant', user_domain_name='test_udn', project_domain_name='test_pdn') middleware = ext_context.ExternalContextMiddleware(None) token = middleware.get_keystone_token('test_user', 'test_password') self.assertEqual('test_token', token) expected_kwargs = { 'auth_url': 'test_url/v3', 'username': 'test_user', 'password': 'test_password', 'project_name': mock_conf.cfapi.tenant, 'user_domain_name': mock_conf.cfapi.user_domain_name, 'project_domain_name': mock_conf.cfapi.project_domain_name } mock_v3.Password.assert_called_once_with(**expected_kwargs) mock_v3.Password.reset_mock() def test_query_endpoints_except_endpoint_not_found(self): middleware = ext_context.ExternalContextMiddleware(None) if hasattr(middleware, '_murano_endpoint'): setattr(middleware, '_murano_endpoint', None) mock_auth = mock.MagicMock() mock_auth.get_endpoint.side_effect = exceptions.EndpointNotFound middleware._query_endpoints(mock_auth, 'test_session') mock_auth.get_endpoint.assert_any_call('test_session', 'application-catalog') if hasattr(middleware, '_glare_endpoint'): setattr(middleware, '_glare_endpoint', None) setattr(middleware, '_murano_endpoint', 'test_endpoint') middleware._query_endpoints(mock_auth, 'test_session') mock_auth.get_endpoint.assert_any_call('test_session', 'artifact') ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/api/middleware/test_fault_wrapper.py0000664000175000017500000001004500000000000025716 0ustar00zuulzuul00000000000000# Copyright 2016 AT&T Corp # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from unittest import mock from murano.api.middleware import fault from murano.common import wsgi from murano.packages import exceptions from murano.tests.unit import base from oslo_serialization import jsonutils from webob import exc class FaultWrapperTest(base.MuranoTestCase): @mock.patch('traceback.format_exc') def test_error_500(self, mock_trace): mock_trace.return_value = "test trace" fault_wrapper = fault.FaultWrapper(None) result = fault_wrapper._error(exc.HTTPInternalServerError()) self.assertEqual(result['code'], 500) self.assertEqual(result['explanation'], 'The server has either erred or is incapable' ' of performing the requested operation.') @mock.patch('traceback.format_exc') def test_error_value_error(self, mock_trace): mock_trace.return_value = "test trace" fault_wrapper = fault.FaultWrapper(None) exception = exceptions.PackageClassLoadError("test") exception.message = "Unable to load class 'test' from package" result = fault_wrapper._error(exception) self.assertEqual(result['code'], 400) self.assertEqual(result['error']['message'], "Unable to load class 'test' from package") @mock.patch('traceback.format_exc') def test_fault_wrapper(self, mock_trace): mock_trace.return_value = "test trace" fault_wrapper = fault.FaultWrapper(None) exception_disguise = fault.HTTPExceptionDisguise( exc.HTTPInternalServerError()) result = fault_wrapper._error(exception_disguise) self.assertEqual(result['code'], 500) self.assertEqual(result['explanation'], 'The server has either erred or is incapable' ' of performing the requested operation.') def test_process_request(self): fault_wrapper = fault.FaultWrapper(None) environ = { 'SERVER_NAME': 'server.test', 'SERVER_PORT': '8082', 'SERVER_PROTOCOL': 'http', 'SCRIPT_NAME': '/', 'PATH_INFO': '/asdf/asdf/asdf/asdf', 'wsgi.url_scheme': 'http', 'QUERY_STRING': '', 'CONTENT_TYPE': 'application/json', 'REQUEST_METHOD': 'HEAD' } req = wsgi.Request(environ) req.get_response = mock.MagicMock(side_effect=exc. HTTPInternalServerError()) self.assertRaises(exc.HTTPInternalServerError, fault_wrapper.process_request, req) @mock.patch('traceback.format_exc') def test_fault_call(self, mock_trace): mock_trace.return_value = "test trace" fault_wrapper = fault.FaultWrapper(None) exception = exceptions.PackageClassLoadError("test") exception.message = "Unable to load class 'test' from package" test_fault = fault.Fault(fault_wrapper._error(exception)) environ = { 'SERVER_NAME': 'server.test', 'SERVER_PORT': '8082', 'SERVER_PROTOCOL': 'http', 'SCRIPT_NAME': '/', 'PATH_INFO': '/', 'wsgi.url_scheme': 'http', 'QUERY_STRING': '', 'CONTENT_TYPE': 'application/json', 'REQUEST_METHOD': 'HEAD' } req = wsgi.Request(environ) response = jsonutils.loads(test_fault(req).body) self.assertEqual(response['code'], 400) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/api/middleware/test_version_negotiation.py0000664000175000017500000000667600000000000027147 0ustar00zuulzuul00000000000000# Copyright 2016 AT&T Corp # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from unittest import mock import webob from murano.api.middleware import version_negotiation from murano.api import versions from murano.tests.unit import base class MiddlewareVersionNegotiationTest(base.MuranoTestCase): @mock.patch.object(version_negotiation, 'LOG') def test_middleware_version_negotiation_default(self, mock_log): middleware_vn = version_negotiation.VersionNegotiationFilter(None) request = webob.Request.blank('/environments') result = middleware_vn.process_request(request) self.assertIsInstance(result, versions.Controller) mock_log.warning.assert_called_once_with( "Unknown version. Returning version choices.") @mock.patch.object(version_negotiation, 'LOG') def test_process_request(self, mock_log): """Test process_request using different valid paths.""" middleware_vn = version_negotiation.VersionNegotiationFilter(None) for path in ('v1', '/v1', '///v1'): request = webob.Request.blank(path) request.method = 'GET' request.environ = {} result = middleware_vn.process_request(request) self.assertIsNone(result) self.assertIn('api.version', request.environ) self.assertEqual(1, request.environ['api.version']) self.assertEqual('/v1', request.path_info) mock_log.debug.assert_any_call( "Matched version: v{version}".format(version=1)) mock_log.debug.assert_any_call( 'new path {path}'.format(path='/v1')) request = webob.Request.blank('/v1/') request.method = 'GET' request.environ = {} result = middleware_vn.process_request(request) self.assertIsNone(result) self.assertIn('api.version', request.environ) self.assertEqual(1, request.environ['api.version']) self.assertEqual('/v1/', request.path_info) mock_log.debug.assert_any_call( "Matched version: v{version}".format(version=1)) mock_log.debug.assert_any_call( 'new path {path}'.format(path='/v1/')) @mock.patch.object(version_negotiation, 'LOG') def test_process_request_without_path(self, mock_log): middleware_vn = version_negotiation.VersionNegotiationFilter(None) request = webob.Request.blank('') request.method = 'GET' request.environ = {} result = middleware_vn.process_request(request) self.assertIsInstance(result, versions.Controller) mock_log.warning.assert_called_once_with( "Unknown version. Returning version choices.") def test_factory(self): app = version_negotiation.VersionNegotiationFilter.factory(None) self.assertIsNotNone(app) self.assertEqual( version_negotiation.VersionNegotiationFilter, type(app(None))) ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1696417899.8171809 murano-16.0.0/murano/tests/unit/api/v1/0000775000175000017500000000000000000000000017643 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/api/v1/__init__.py0000664000175000017500000000000000000000000021742 0ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1696417899.8171809 murano-16.0.0/murano/tests/unit/api/v1/cloudfoundry/0000775000175000017500000000000000000000000022360 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/api/v1/cloudfoundry/__init__.py0000664000175000017500000000000000000000000024457 0ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/api/v1/cloudfoundry/test_cfapi.py0000664000175000017500000005672400000000000025071 0ustar00zuulzuul00000000000000# Copyright (c) 2015 Mirantis, Inc. # Copyright 2017 AT&T Corporation # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import json from unittest import mock from oslo_serialization import base64 from webob import response from murano.cfapi import cfapi as api from murano.common import wsgi from murano.db import models from murano.tests.unit import base from muranoclient.common import exceptions class TestController(base.MuranoTestCase): def setUp(self): super(TestController, self).setUp() self.controller = api.Controller() self.request = mock.MagicMock() auth = 'Basic {}'.format(base64.encode_as_text(b'test:test')) self.request.headers = {'Authorization': auth, 'X-Auth-Token': 'foo-bar', 'X-Project-Id': 'bar-baz'} self.addCleanup(mock.patch.stopall) @mock.patch('murano.common.policy.check_is_admin') @mock.patch('murano.cfapi.cfapi._get_muranoclient') def test_list(self, mock_client, mock_policy): pkg0 = mock.MagicMock() pkg0.id = 'xxx' pkg0.name = 'foo' pkg0.description = 'stub pkg' mock_client.return_value.packages.filter =\ mock.MagicMock(return_value=[pkg0]) answer = {'services': [{'bindable': True, 'description': pkg0.description, 'id': pkg0.id, 'name': pkg0.name, 'plans': [{'description': ('Default plan for ' 'the service ' '{name}').format( name=pkg0.name), 'id': 'xxx-1', 'name': 'default'}], 'tags': []}]} resp = self.controller.list(self.request) self.assertEqual(answer, resp) @mock.patch('murano.common.policy.check_is_admin') @mock.patch('murano.cfapi.cfapi._get_muranoclient') @mock.patch('murano.db.services.cf_connections.set_instance_for_service') @mock.patch('murano.db.services.cf_connections.get_environment_for_space') @mock.patch('murano.db.services.cf_connections.get_tenant_for_org') def test_provision_from_scratch(self, mock_get_tenant, mock_get_environment, mock_is, mock_client, mock_policy): body = {"space_guid": "s1-p1", "organization_guid": "o1-r1", "plan_id": "p1-l1", "service_id": "s1-e1", "parameters": {'some_parameter': 'value', '?': {}}} self.request.body = json.dumps(body) mock_get_environment.return_value = '555-555' mock_client.return_value = mock.MagicMock() resp = self.controller.provision(self.request, {}, '111-111') self.assertIsInstance(resp, response.Response) @mock.patch('murano.common.policy.check_is_admin') @mock.patch('murano.cfapi.cfapi._get_muranoclient') @mock.patch('murano.db.services.cf_connections.set_instance_for_service') @mock.patch('murano.db.services.cf_connections.set_environment_for_space') @mock.patch('murano.db.services.cf_connections.set_tenant_for_org') @mock.patch('murano.db.services.cf_connections.get_environment_for_space') @mock.patch('murano.db.services.cf_connections.get_tenant_for_org') def test_provision_existent(self, mock_get_tenant, mock_get_environment, mock_set_tenant, mock_set_environment, mock_is, mock_client, mock_policy): body = {"space_guid": "s1-p1", "organization_guid": "o1-r1", "plan_id": "p1-l1", "service_id": "s1-e1", "parameters": {'some_parameter': 'value', '?': {}}} self.request.body = json.dumps(body) mock_package = mock_client.return_value.packages.get mock_package.return_value = mock.MagicMock() mock_get_environment.side_effect = AttributeError mock_get_tenant.side_effect = AttributeError resp = self.controller.provision(self.request, {}, '111-111') self.assertIsInstance(resp, response.Response) @mock.patch('murano.cfapi.cfapi._get_muranoclient') @mock.patch('murano.db.services.cf_connections.get_service_for_instance') def test_deprovision(self, mock_get_si, mock_client): service = mock.MagicMock() service.service_id = '111-111' service.tenant_id = '222-222' service.env_id = '333-333' mock_get_si.return_value = service resp = self.controller.deprovision(self.request, '555-555') self.assertIsInstance(resp, response.Response) @mock.patch('murano.cfapi.cfapi._get_muranoclient') @mock.patch('murano.db.services.cf_connections.get_service_for_instance') def test_bind(self, mock_get_si, mock_client): service = mock.MagicMock() service.service_id = '111-111' service.tenant_id = '222-222' service.env_id = '333-333' mock_get_si.return_value = service services = [{'id': 'xxx-xxx-xxx', '?': {'id': '111-111', '_actions': { 'dafsa_getCredentials': { 'name': 'getCredentials'}}}, 'instance': {}}] mock_client.return_value.environments.get =\ mock.MagicMock(return_value=mock.MagicMock(services=services)) mock_client.return_value.actions.get_result =\ mock.MagicMock(return_value={'result': {'smthg': 'nothing'}}) nice_resp = {'credentials': {'smthg': 'nothing'}} resp = self.controller.bind(self.request, {}, '555-555', '666-666') self.assertEqual(nice_resp, resp) def test_package_to_service(self): mock_package = mock.Mock( spec_set=models.Package, id='foo_package_id', description='a' * 257, tags=[mock.sentinel.package_tag]) mock_package.configure_mock(name=mock.sentinel.package_name) expected_service = { 'id': 'foo_package_id', 'name': mock.sentinel.package_name, 'description': 'a' * 253 + ' ...', 'bindable': True, 'tags': [mock.sentinel.package_tag], 'plans': [{ 'id': 'foo_package_id-1', 'name': 'default', 'description': 'Default plan for the service sentinel.package_name' }] } service = self.controller._package_to_service(mock_package) for key, val in expected_service.items(): if key != 'plans': self.assertEqual(val, service[key]) else: self.assertEqual(1, len(service['plans'])) for plan_key, plan_val in expected_service['plans'][0].items(): self.assertEqual(plan_val, service['plans'][0][plan_key]) def test_get_service_return_none(self): mock_env = mock.Mock(services=[]) self.assertIsNone(self.controller._get_service(mock_env, None)) @mock.patch.object(api, 'LOG', autospec=True) @mock.patch.object(api, 'db_cf', autospec=True) @mock.patch.object(api, '_get_muranoclient', autospec=True) def test_provision_except_http_not_found(self, mock_get_muranoclient, mock_db_cf, mock_log): test_body = { 'space_guid': 'foo_space_guid', 'organization_guid': 'foo_organization_guid', 'plan_id': 'foo_plan_id', 'service_id': 'foo_service_id', 'parameters': {} } test_headers = { 'X-Auth-Token': mock.sentinel.token } mock_request = mock.Mock(body=json.dumps(test_body), headers=test_headers) mock_muranoclient = mock.Mock() mock_muranoclient.environments.get.side_effect = \ exceptions.HTTPNotFound mock_muranoclient.environments.create.return_value = \ mock.Mock(id=mock.sentinel.alt_environment_id) mock_get_muranoclient.return_value = mock_muranoclient mock_db_cf.get_environment_for_space.return_value = \ mock.sentinel.environment_id resp = self.controller.provision(mock_request, None, None) self.assertEqual(202, resp.status_code) self.assertEqual({}, resp.json_body) mock_db_cf.get_environment_for_space.assert_called_once_with( 'foo_space_guid') mock_muranoclient.environments.get.assert_called_once_with( mock.sentinel.environment_id) mock_log.info.assert_has_calls([ mock.call("Can not find environment_id sentinel.environment_id" ", will create a new one."), mock.call("Cloud Foundry foo_space_guid remapped to " "sentinel.alt_environment_id") ]) @mock.patch.object(api, 'uuid', autospec=True) @mock.patch.object(api, 'db_cf', autospec=True) @mock.patch.object(api, '_get_muranoclient', autospec=True) def test_provision_with_dict_parameters(self, mock_get_muranoclient, mock_db_cf, mock_uuid): test_body = { 'space_guid': 'foo_space_guid', 'organization_guid': 'foo_organization_guid', 'plan_id': 'foo_plan_id', 'service_id': 'foo_service_id', 'parameters': { 'foo': { '?': { 'bar': 'baz' } } } } test_headers = { 'X-Auth-Token': mock.sentinel.token } mock_request = mock.Mock(body=json.dumps(test_body), headers=test_headers) mock_package = mock_get_muranoclient.return_value.packages.get.\ return_value mock_service = mock.MagicMock() self.controller._make_service = mock.Mock(return_value=mock_service) mock_uuid.uuid4.return_value.hex = mock.sentinel.uuid resp = self.controller.provision(mock_request, None, None) self.assertEqual(202, resp.status_code) self.assertEqual({}, resp.json_body) self.controller._make_service.assert_called_once_with( 'foo_space_guid', mock_package, 'foo_plan_id') mock_service.update.assert_called_once_with( {'foo': {'?': {'bar': 'baz', 'id': mock.sentinel.uuid}}}) @mock.patch.object(api, 'db_cf', autospec=True) def test_deprovision_with_missing_service(self, mock_db_cf): mock_db_cf.get_service_for_instance.return_value = None self.assertEqual({}, self.controller.deprovision(None, None)) mock_db_cf.get_service_for_instance.assert_called_once_with(None) @mock.patch.object(api, 'db_cf', autospec=True) def test_bind_with_missing_service(self, mock_db_cf): mock_db_cf.get_service_for_instance.return_value = None self.assertEqual({}, self.controller.bind(None, None, None, None)) mock_db_cf.get_service_for_instance.assert_called_once_with(None) @mock.patch.object(api, 'LOG', autospec=True) @mock.patch.object(api.Controller, '_get_service', autospec=True) @mock.patch.object(api, '_get_muranoclient', autospec=True) @mock.patch.object(api, 'db_cf', autospec=True) def test_bind_except_key_error(self, mock_db_cf, mock_get_muranoclient, mock_get_service, mock_log): mock_db_service = mock.Mock( service_id=mock.sentinel.service_id, environment_id=mock.sentinel.environment_id) test_service = {} test_request = mock.Mock( headers={'X-Auth-Token': mock.sentinel.auth_token}) mock_db_cf.get_service_for_instance.return_value = mock_db_service mock_get_service.return_value = test_service m_cli = mock_get_muranoclient.return_value m_env = m_cli.environments.get.return_value resp = self.controller.bind(test_request, None, None, None) self.assertEqual(500, resp.status_code) m_cli.environments.get.assert_called_once_with( mock.sentinel.environment_id, mock.ANY) mock_get_service.assert_called_once_with(self.controller, m_env, mock.sentinel.service_id) mock_log.warning.assert_called_once_with( "This application doesn't have actions at all") @mock.patch.object(api, 'LOG', autospec=True) @mock.patch.object(api.Controller, '_get_service', autospec=True) @mock.patch.object(api, '_get_muranoclient', autospec=True) @mock.patch.object(api, 'db_cf', autospec=True) def test_bind_without_get_credentials(self, mock_db_cf, mock_get_muranoclient, mock_get_service, mock_log): mock_db_service = mock.Mock( service_id=mock.sentinel.service_id, environment_id=mock.sentinel.environment_id) test_service = {'?': {'_actions': []}} test_request = mock.Mock( headers={'X-Auth-Token': mock.sentinel.auth_token}) mock_db_cf.get_service_for_instance.return_value = mock_db_service mock_get_service.return_value = test_service m_cli = mock_get_muranoclient.return_value m_env = m_cli.environments.get.return_value resp = self.controller.bind(test_request, None, None, None) self.assertEqual(500, resp.status_code) m_cli.environments.get.assert_called_once_with( mock.sentinel.environment_id, mock.ANY) mock_get_service.assert_called_once_with(self.controller, m_env, mock.sentinel.service_id) mock_log.warning.assert_called_once_with( "This application doesn't have action getCredentials") def test_unbind(self): self.assertEqual({}, self.controller.unbind(None, None, None)) @mock.patch.object(api, 'LOG', autospec=True) @mock.patch.object(api, 'db_cf', autospec=True) def test_get_last_operation_without_service(self, mock_db_cf, mock_log): mock_db_cf.get_service_for_instance.return_value = None resp = self.controller.get_last_operation( None, mock.sentinel.instance_id) self.assertEqual(410, resp.status_code) self.assertEqual({}, resp.json_body) mock_log.warning.assert_called_once_with( 'Requested service for instance sentinel.instance_id is not ' 'found') @mock.patch.object(api, '_get_muranoclient', autospec=True) @mock.patch.object(api, 'db_cf', autospec=True) def test_get_last_operation_succeeded(self, mock_db_cf, mock_get_muranoclient): mock_service = mock.Mock( environment_id=mock.sentinel.environment_id) mock_request = mock.Mock( headers={'X-Auth-Token': mock.sentinel.auth_token}) mock_db_cf.get_service_for_instance.return_value = mock_service m_cli = mock_get_muranoclient.return_value m_env = m_cli.environments.get.return_value m_env.status = 'ready' resp = self.controller.get_last_operation( mock_request, mock.sentinel.instance_id) self.assertEqual(200, resp.status_code) self.assertEqual( {'state': 'succeeded', 'description': 'operation succeed'}, resp.json_body) mock_get_muranoclient.assert_called_once_with(mock.sentinel.auth_token, mock_request) m_cli.environments.get.assert_called_once_with( mock.sentinel.environment_id) @mock.patch.object(api, '_get_muranoclient', autospec=True) @mock.patch.object(api, 'db_cf', autospec=True) def test_get_last_operation_in_progress(self, mock_db_cf, mock_get_muranoclient): mock_service = mock.Mock( environment_id=mock.sentinel.environment_id) mock_request = mock.Mock( headers={'X-Auth-Token': mock.sentinel.auth_token}) mock_db_cf.get_service_for_instance.return_value = mock_service m_cli = mock_get_muranoclient.return_value m_env = m_cli.environments.get.return_value progress_statuses = ['pending', 'deleting', 'deploying'] for status in progress_statuses: m_env.status = status resp = self.controller.get_last_operation( mock_request, mock.sentinel.instance_id) self.assertEqual(202, resp.status_code) self.assertEqual( {'state': 'in progress', 'description': 'operation in progress'}, resp.json_body) mock_get_muranoclient.assert_called_once_with( mock.sentinel.auth_token, mock_request) m_cli.environments.get.assert_called_once_with( mock.sentinel.environment_id) mock_get_muranoclient.reset_mock() m_cli.environments.get.reset_mock() @mock.patch.object(api, '_get_muranoclient', autospec=True) @mock.patch.object(api, 'db_cf', autospec=True) def test_get_last_operation_failed(self, mock_db_cf, mock_get_muranoclient): mock_service = mock.Mock( environment_id=mock.sentinel.environment_id) mock_request = mock.Mock( headers={'X-Auth-Token': mock.sentinel.auth_token}) mock_db_cf.get_service_for_instance.return_value = mock_service m_cli = mock_get_muranoclient.return_value m_env = m_cli.environments.get.return_value failed_statuses = ['deploy failure', 'delete failure'] for status in failed_statuses: m_env.status = status resp = self.controller.get_last_operation( mock_request, mock.sentinel.instance_id) self.assertEqual(200, resp.status_code) self.assertEqual( {'state': 'failed', 'description': '{0}. Please correct it manually' .format(status)}, resp.json_body) mock_get_muranoclient.assert_called_once_with( mock.sentinel.auth_token, mock_request) m_cli.environments.get.assert_called_once_with( mock.sentinel.environment_id) mock_get_muranoclient.reset_mock() m_cli.environments.get.reset_mock() @mock.patch.object(api, '_get_muranoclient', autospec=True) @mock.patch.object(api, 'db_cf', autospec=True) def test_get_last_operation_unknown_status(self, mock_db_cf, mock_get_muranoclient): mock_service = mock.Mock( environment_id=mock.sentinel.environment_id) mock_request = mock.Mock( headers={'X-Auth-Token': mock.sentinel.auth_token}) mock_db_cf.get_service_for_instance.return_value = mock_service m_cli = mock_get_muranoclient.return_value m_env = m_cli.environments.get.return_value m_env.status = 'unknown' resp = self.controller.get_last_operation( mock_request, mock.sentinel.instance_id) self.assertEqual(500, resp.status_code) self.assertEqual( {'state': 'unknown', 'description': 'operation unknown'}, resp.json_body) mock_get_muranoclient.assert_called_once_with( mock.sentinel.auth_token, mock_request) m_cli.environments.get.assert_called_once_with( mock.sentinel.environment_id) @mock.patch.object(api, 'muranoclient', autospec=True) @mock.patch.object(api, 'glare_client', autospec=True) def test_get_muranoclient(self, mock_glare_client, mock_muranoclient): self._override_conf() m_artifacts_client = mock.Mock() m_muranoclient = mock.Mock() mock_glare_client.Client.return_value = m_artifacts_client mock_muranoclient.Client.return_value = m_muranoclient client = api._get_muranoclient(mock.sentinel.token_id, None) self.assertEqual(m_muranoclient, client) mock_glare_client.Client.assert_called_once_with( endpoint='foo_glare_url', token=mock.sentinel.token_id, insecure=True, key_file='foo_key_file', ca_file='foo_ca_file', cert_file='foo_cert_file', type_name='murano', type_version=1) mock_muranoclient.Client.assert_called_once_with( 1, 'foo_murano_url', token=mock.sentinel.token_id, artifacts_client=m_artifacts_client) @mock.patch.object(api, 'LOG', autospec=True) @mock.patch.object(api, 'muranoclient', autospec=True) @mock.patch.object(api, 'glare_client', autospec=True) def test_get_muranoclient_without_urls(self, mock_glare_client, mock_muranoclient, mock_log): self._override_conf(without_urls=True) m_artifacts_client = mock.Mock() m_muranoclient = mock.Mock() mock_glare_client.Client.return_value = m_artifacts_client mock_muranoclient.Client.return_value = m_muranoclient mock_request = mock.Mock(endpoints={'murano': None}) client = api._get_muranoclient(mock.sentinel.token_id, mock_request) self.assertEqual(m_muranoclient, client) mock_glare_client.Client.assert_called_once_with( endpoint=None, token=mock.sentinel.token_id, insecure=True, key_file='foo_key_file', ca_file='foo_ca_file', cert_file='foo_cert_file', type_name='murano', type_version=1) mock_muranoclient.Client.assert_called_once_with( 1, None, token=mock.sentinel.token_id, artifacts_client=m_artifacts_client) mock_log.error.assert_has_calls([ mock.call('No glare url is specified and no "artifact" ' 'service is registered in keystone.'), mock.call('No murano url is specified and no ' '"application-catalog" service is registered in ' 'keystone.') ]) def _override_conf(self, without_urls=False): if without_urls: self.override_config('url', None, group='glare') self.override_config('url', None, group='murano') else: self.override_config('url', 'foo_glare_url', group='glare') self.override_config('url', 'foo_murano_url', group='murano') for arg, group, override_val in ( ('packages_service', 'engine', 'glare'), ('insecure', 'glare', True), ('key_file', 'glare', 'foo_key_file'), ('ca_file', 'glare', 'foo_ca_file'), ('cert_file', 'glare', 'foo_cert_file')): self.override_config(arg, override_val, group=group) def test_resource(self): self.assertIsInstance(api.create_resource(), wsgi.Resource) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/api/v1/test_actions.py0000664000175000017500000002261500000000000022722 0ustar00zuulzuul00000000000000# Copyright (c) 2014 Hewlett-Packard Development Company, L.P. # 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 unittest import mock from oslo_utils import timeutils from webob import exc from murano.api.v1 import actions from murano.common import policy from murano.db import models from murano.db import session as db_session from murano.services import states import murano.tests.unit.api.base as tb import murano.tests.unit.utils as test_utils @mock.patch.object(policy, 'check') class TestActionsApi(tb.ControllerTest, tb.MuranoApiTestCase): def setUp(self): super(TestActionsApi, self).setUp() self.controller = actions.Controller() def test_execute_action(self, mock_policy_check): """Test that action execution results in the correct rpc call.""" self._set_policy_rules( {'execute_action': '@'} ) fake_now = timeutils.utcnow() expected = dict( id='12345', name='my-env', version=0, created=fake_now, updated=fake_now, tenant_id=self.tenant, description={ 'Objects': { '?': {'id': '12345', '_actions': { 'actionsID_action': { 'enabled': True, 'name': 'Testaction' } }} }, 'Attributes': {} } ) e = models.Environment(**expected) test_utils.save_models(e) rpc_task = { 'action': { 'args': '{}', 'method': 'Testaction', 'object_id': '12345' }, 'project_id': self.tenant, 'user_id': self.user, 'model': { 'Attributes': {}, 'Objects': { 'applications': [], '?': { '_actions': { 'actionsID_action': { 'enabled': True, 'name': 'Testaction' } }, 'id': '12345' } } }, 'token': None, 'id': '12345' } req = self._post('/environments/12345/actions/actionID_action', b'{}') result = self.controller.execute(req, '12345', 'actionsID_action', '{}') self.mock_engine_rpc.handle_task.assert_called_once_with(rpc_task) self.assertIn('task_id', result) def _create_session_with_state(self, environment, user_id, state): unit = db_session.get_session() session = models.Session() session.environment_id = environment.id session.user_id = user_id session.state = state session.version = environment.version with unit.begin(): unit.add(session) def test_execute_action_with_session_in_deploying_state(self, _): """Test whether session in the deploying state throws error.""" self._set_policy_rules( {'execute_action': '@'} ) fake_now = timeutils.utcnow() expected = dict( id='12345', name='my-env', version=0, created=fake_now, updated=fake_now, tenant_id=self.tenant, description={ 'Objects': { '?': {'id': '12345', '_actions': { 'actionsID_action': { 'enabled': True, 'name': 'Testaction' } }} }, 'Attributes': {} } ) environment = models.Environment(**expected) test_utils.save_models(environment) req = self._post('/environments/12345/actions/actionID_action', b'{}') user_id = req.context.user self._create_session_with_state(environment, user_id, states.SessionState.DEPLOYING) self.assertRaises(exc.HTTPForbidden, self.controller.execute, req, '12345', 'actionsID_action', {}) def test_execute_action_with_session_in_deleting_state(self, _): """Test whether session in deleting state throws error.""" self._set_policy_rules( {'execute_action': '@'} ) fake_now = timeutils.utcnow() expected = dict( id='12345', name='my-env', version=0, created=fake_now, updated=fake_now, tenant_id=self.tenant, description={ 'Objects': { '?': {'id': '12345', '_actions': { 'actionsID_action': { 'enabled': True, 'name': 'Testaction' } }} }, 'Attributes': {} } ) environment = models.Environment(**expected) test_utils.save_models(environment) req = self._post('/environments/12345/actions/actionID_action', b'{}') user_id = req.context.user self._create_session_with_state(environment, user_id, states.SessionState.DELETING) self.assertRaises(exc.HTTPForbidden, self.controller.execute, req, '12345', 'actionsID_action', {}) @mock.patch('murano.db.services.sessions.SessionServices.validate') def test_execute_action_with_invalid_session_version(self, mocked_function, _): """Test whether validate session function throws error.""" self._set_policy_rules( {'execute_action': '@'} ) fake_now = timeutils.utcnow() expected = dict( id='12345', name='my-env', version=0, created=fake_now, updated=fake_now, tenant_id=self.tenant, description={ 'Objects': { '?': {'id': '12345', '_actions': { 'actionsID_action': { 'enabled': True, 'name': 'Testaction' } }} }, 'Attributes': {} } ) environment = models.Environment(**expected) test_utils.save_models(environment) req = self._post('/environments/12345/actions/actionID_action', b'{}') mocked_function.return_value = False self.assertRaises(exc.HTTPForbidden, self.controller.execute, req, '12345', 'actionsID_action', {}) def test_get_result(self, _): """Result of task with given id and environment id is returned.""" now = timeutils.utcnow() expected_environment_id = 'test_environment' expected_task_id = 'test_task' expected_result = {'test_result': 'test_result'} environment = models.Environment( id=expected_environment_id, name='test_environment', created=now, updated=now, tenant_id=self.tenant ) task = models.Task( id=expected_task_id, started=now, finished=now, result=expected_result, environment_id=expected_environment_id ) test_utils.save_models(environment, task) request = self._get( '/environments/{environment_id}/actions/{task_id}' .format(environment_id=expected_environment_id, task_id=expected_task_id), ) response = request.get_response(self.api) self.assertEqual(200, response.status_code) self.assertEqual(expected_result, response.json) def test_get_result_not_found(self, _): """Return 404 on null task If task does not exist, it should be handled correctly and API should return 404. """ expected_environment_id = 'test_environment' environment = models.Environment( id=expected_environment_id, name='test_environment', tenant_id=self.tenant ) test_utils.save_models(environment) request = self._get( '/environments/{environment_id}/actions/{task_id}' .format(environment_id=expected_environment_id, task_id='not_existent_task_id'), ) response = request.get_response(self.api) self.assertEqual(404, response.status_code) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/api/v1/test_catalog.py0000664000175000017500000016124000000000000022672 0ustar00zuulzuul00000000000000# Copyright (c) 2014 Hewlett-Packard Development Company, L.P. # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT 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 cgi from datetime import datetime import imghdr from io import StringIO import os import tempfile from unittest import mock import uuid from oslo_config import cfg from oslo_serialization import jsonutils from oslo_utils import timeutils from webob import exc from murano.api.v1 import catalog from murano.api.v1 import PKG_PARAMS_MAP from murano.common import exceptions as common_exc from murano.db.catalog import api as db_catalog_api from murano.db import models from murano.packages import exceptions from murano.packages import load_utils import murano.tests.unit.api.base as test_base import murano.tests.unit.utils as test_utils CONF = cfg.CONF class TestCatalogApi(test_base.ControllerTest, test_base.MuranoApiTestCase): def setUp(self): super(TestCatalogApi, self).setUp() self.controller = catalog.Controller() _, self.test_package = self._test_package() def _add_pkg(self, tenant_name, public=False, classes=None, **kwargs): package_to_upload = self.test_package.copy() package_to_upload['is_public'] = public package_to_upload['fully_qualified_name'] = str(uuid.uuid4()) if classes: package_to_upload['class_definitions'] = classes else: package_to_upload['class_definitions'] = [] package_to_upload.update(kwargs) return db_catalog_api.package_upload( package_to_upload, tenant_name) def test_packages_filtering_admin(self): self.is_admin = True self._set_policy_rules( {'get_package': '', 'manage_public_package': ''} ) for dummy in range(7): self.expect_policy_check('get_package') self.expect_policy_check('manage_public_package') pkg = self._add_pkg('test_tenant', type='Library') self._add_pkg('test_tenant') self._add_pkg('test_tenant2', public=True, type='Library') self._add_pkg('test_tenant3') result = self.controller.search(self._get( '/v1/catalog/packages/', params={'catalog': 'False', 'owned': 'False'})) self.assertEqual(4, len(result['packages'])) result = self.controller.search(self._get( '/v1/catalog/packages/', params={'catalog': 'False', 'owned': 'True'})) self.assertEqual(2, len(result['packages'])) result = self.controller.search(self._get( '/v1/catalog/packages/', params={'catalog': 'True', 'owned': 'False'})) self.assertEqual(3, len(result['packages'])) result = self.controller.search(self._get( '/v1/catalog/packages/', params={'catalog': 'True', 'owned': 'True'})) self.assertEqual(2, len(result['packages'])) result = self.controller.search(self._get( '/v1/catalog/packages/', params={ 'owned': 'True', 'fqn': pkg.fully_qualified_name})) self.assertEqual(1, len(result['packages'])) self.assertEqual(pkg.fully_qualified_name, result['packages'][0]['fully_qualified_name']) result = self.controller.search(self._get( '/v1/catalog/packages/', params={ 'owned': 'True', 'type': 'Library'})) self.assertEqual(1, len(result['packages'])) self.assertEqual(pkg.fully_qualified_name, result['packages'][0]['fully_qualified_name']) result = self.controller.search(self._get( '/v1/catalog/packages/', params={ 'type': 'Library'})) self.assertEqual(2, len(result['packages'])) def test_packages_filtering_non_admin(self): self.is_admin = False self._set_policy_rules( {'get_package': '', 'manage_public_package': ''} ) for dummy in range(8): self.expect_policy_check('get_package') self.expect_policy_check('manage_public_package') pkg = self._add_pkg('test_tenant', type='Library') self._add_pkg('test_tenant') self._add_pkg('test_tenant2', public=True, type='Library') self._add_pkg('test_tenant3') result = self.controller.search(self._get( '/v1/catalog/packages/', params={'catalog': 'False', 'owned': 'False'})) self.assertEqual(3, len(result['packages'])) result = self.controller.search(self._get( '/v1/catalog/packages/', params={'catalog': 'False', 'owned': 'True'})) self.assertEqual(2, len(result['packages'])) result = self.controller.search(self._get( '/v1/catalog/packages/', params={'catalog': 'True', 'owned': 'False'})) self.assertEqual(3, len(result['packages'])) result = self.controller.search(self._get( '/v1/catalog/packages/', params={'catalog': 'True', 'owned': 'True'})) self.assertEqual(2, len(result['packages'])) result = self.controller.search(self._get( '/v1/catalog/packages/', params={ 'owned': 'True', 'fqn': pkg.fully_qualified_name})) self.assertEqual(1, len(result['packages'])) self.assertEqual(pkg.fully_qualified_name, result['packages'][0]['fully_qualified_name']) result = self.controller.search(self._get( '/v1/catalog/packages/', params={ 'owned': 'True', 'type': 'Library'})) self.assertEqual(1, len(result['packages'])) self.assertEqual(pkg.fully_qualified_name, result['packages'][0]['fully_qualified_name']) result = self.controller.search(self._get( '/v1/catalog/packages/', params={ 'type': 'Library'})) self.assertEqual(2, len(result['packages'])) self._set_policy_rules({'get_package': '', 'manage_public_package': '!'}) result = self.controller.search(self._get( '/v1/catalog/packages/', params={'catalog': 'False'})) self.assertEqual(2, len(result['packages'])) def test_packages_filter_by_id(self): """Test that packages are filtered by ID GET /catalog/packages with parameter "id" returns packages filtered by id. """ self._set_policy_rules( {'get_package': '', 'manage_public_package': ''} ) _, package1_data = self._test_package() _, package2_data = self._test_package() package1_data['fully_qualified_name'] += '_1' package1_data['name'] += '_1' package1_data['class_definitions'] = (u'test.mpl.v1.app.Thing1',) package2_data['fully_qualified_name'] += '_2' package2_data['name'] += '_2' package2_data['class_definitions'] = (u'test.mpl.v1.app.Thing2',) expected_package = db_catalog_api.package_upload(package1_data, '') db_catalog_api.package_upload(package2_data, '') req = self._get('/catalog/packages', params={'id': expected_package.id}) self.expect_policy_check('get_package') self.expect_policy_check('manage_public_package') res = req.get_response(self.api) self.assertEqual(200, res.status_code) self.assertEqual(1, len(res.json['packages'])) found_package = res.json['packages'][0] self.assertEqual(expected_package.id, found_package['id']) def test_packages_filter_by_in_category(self): """Test that packages are filtered by in:cat1,cat2,cat3 GET /catalog/packages with parameter "category=in:cat1,cat2,cat3" returns packages filtered by category. """ names = ['cat1', 'cat2', 'cat3', 'cat4'] for name in names: db_catalog_api.category_add(name) self._set_policy_rules( {'get_package': '', 'manage_public_package': ''} ) _, package1_data = self._test_package() _, package2_data = self._test_package() _, package3_data = self._test_package() package1_data['fully_qualified_name'] += '_1' package1_data['name'] += '_1' package1_data['class_definitions'] = (u'test.mpl.v1.app.Thing1',) package1_data['categories'] = ('cat1', 'cat2') package2_data['fully_qualified_name'] += '_2' package2_data['name'] += '_2' package2_data['class_definitions'] = (u'test.mpl.v1.app.Thing2',) package2_data['categories'] = ('cat2', 'cat3') package3_data['fully_qualified_name'] += '_3' package3_data['name'] += '_3' package3_data['class_definitions'] = (u'test.mpl.v1.app.Thing3',) package3_data['categories'] = ('cat2', 'cat4') expected_packages = [db_catalog_api.package_upload(package1_data, ''), db_catalog_api.package_upload(package2_data, '')] db_catalog_api.package_upload(package3_data, '') categories_in = "in:cat1,cat3" req = self._get('/catalog/packages', params={'category': categories_in}) self.expect_policy_check('get_package') self.expect_policy_check('manage_public_package') res = req.get_response(self.api) self.assertEqual(200, res.status_code) self.assertEqual(2, len(res.json['packages'])) found_packages = res.json['packages'] self.assertEqual([pack.id for pack in expected_packages], [pack['id'] for pack in found_packages]) def test_packages_filter_by_in_tag(self): """Test that packages are filtered by in:tag1,tag2,tag3 GET /catalog/packages with parameter "tag=in:tag1,tag2,tag3" returns packages filtered by category. """ self._set_policy_rules( {'get_package': '', 'manage_public_package': ''} ) _, package1_data = self._test_package() _, package2_data = self._test_package() _, package3_data = self._test_package() package1_data['fully_qualified_name'] += '_1' package1_data['name'] += '_1' package1_data['class_definitions'] = (u'test.mpl.v1.app.Thing1',) package1_data['tags'] = ('tag1', 'tag2') package2_data['fully_qualified_name'] += '_2' package2_data['name'] += '_2' package2_data['class_definitions'] = (u'test.mpl.v1.app.Thing2',) package2_data['tags'] = ('tag2', 'tag3') package3_data['fully_qualified_name'] += '_3' package3_data['name'] += '_3' package3_data['class_definitions'] = (u'test.mpl.v1.app.Thing3',) package3_data['tags'] = ('tag2', 'tag4') expected_packages = [db_catalog_api.package_upload(package1_data, ''), db_catalog_api.package_upload(package2_data, '')] db_catalog_api.package_upload(package3_data, '') tag_in = "in:tag1,tag3" req = self._get('/catalog/packages', params={'tag': tag_in}) self.expect_policy_check('get_package') self.expect_policy_check('manage_public_package') res = req.get_response(self.api) self.assertEqual(200, res.status_code) self.assertEqual(2, len(res.json['packages'])) found_packages = res.json['packages'] self.assertEqual([pack.id for pack in expected_packages], [pack['id'] for pack in found_packages]) def test_packages_filter_by_in_id(self): """Test that packages are filtered by in:id1,id2,id3 GET /catalog/packages with parameter "id=in:id1,id2" returns packages filtered by id. """ self._set_policy_rules( {'get_package': '', 'manage_public_package': ''} ) _, package1_data = self._test_package() _, package2_data = self._test_package() _, package3_data = self._test_package() package1_data['fully_qualified_name'] += '_1' package1_data['name'] += '_1' package1_data['class_definitions'] = (u'test.mpl.v1.app.Thing1',) package2_data['fully_qualified_name'] += '_2' package2_data['name'] += '_2' package2_data['class_definitions'] = (u'test.mpl.v1.app.Thing2',) package3_data['fully_qualified_name'] += '_3' package3_data['name'] += '_3' package3_data['class_definitions'] = (u'test.mpl.v1.app.Thing3',) expected_packages = [db_catalog_api.package_upload(package1_data, ''), db_catalog_api.package_upload(package2_data, '')] db_catalog_api.package_upload(package3_data, '') id_in = "in:" + ",".join(pack.id for pack in expected_packages) req = self._get('/catalog/packages', params={'id': id_in}) self.expect_policy_check('get_package') self.expect_policy_check('manage_public_package') res = req.get_response(self.api) self.assertEqual(200, res.status_code) self.assertEqual(2, len(res.json['packages'])) found_packages = res.json['packages'] self.assertEqual([pack.id for pack in expected_packages], [pack['id'] for pack in found_packages]) def test_packages_filter_by_in_id_empty(self): """Test that packages are filtered by "id=in:" GET /catalog/packages with parameter "id=in:" returns packages filtered by id, in this case no packages should be returned. """ self._set_policy_rules( {'get_package': '', 'manage_public_package': ''} ) _, package1_data = self._test_package() db_catalog_api.package_upload(package1_data, '') req = self._get('/catalog/packages', params={'id': "in:"}) self.expect_policy_check('get_package') self.expect_policy_check('manage_public_package') res = req.get_response(self.api) self.assertEqual(200, res.status_code) self.assertEqual(0, len(res.json['packages'])) def test_packages_filter_by_name(self): """Test that packages are filtered by name GET /catalog/packages with parameter "name" returns packages filtered by name. """ self._set_policy_rules( {'get_package': '', 'manage_public_package': ''} ) expected_pkg1 = self._add_pkg("test_tenant", name="test_pkgname_1") expected_pkg2 = self._add_pkg("test_tenant", name="test_pkgname_2") req_pkgname1 = self._get('/catalog/packages', params={'name': expected_pkg1.name}) req_pkgname2 = self._get('/catalog/packages', params={'name': expected_pkg2.name}) for dummy in range(2): self.expect_policy_check('get_package') self.expect_policy_check('manage_public_package') res_pkgname1 = req_pkgname1.get_response(self.api) self.assertEqual(200, res_pkgname1.status_code) self.assertEqual(1, len(res_pkgname1.json['packages'])) self.assertEqual(expected_pkg1.name, res_pkgname1.json['packages'][0]['name']) res_pkgname2 = req_pkgname2.get_response(self.api) self.assertEqual(200, res_pkgname2.status_code) self.assertEqual(1, len(res_pkgname2.json['packages'])) self.assertEqual(expected_pkg2.name, res_pkgname2.json['packages'][0]['name']) def test_packages_filter_by_type(self): """Test that packages are filtered by type GET /catalog/packages with parameter "type" returns packages filtered by type. """ self._set_policy_rules( {'get_package': '', 'manage_public_package': ''} ) excepted_pkg1 = self._add_pkg("test_tenant", type='Library') excepted_pkg2 = self._add_pkg("test_tenant", type='Library') excepted_pkg3 = self._add_pkg("test_tenant", type='Application') # filter by type=Library can see 2 pkgs req_lib = self._get('/catalog/packages', params={'type': 'Library'}) # filter by type=Application only see 1 pkgs req_app = self._get('/catalog/packages', params={'type': 'Application'}) for dummy in range(2): self.expect_policy_check('get_package') self.expect_policy_check('manage_public_package') res_lib = req_lib.get_response(self.api) self.assertEqual(200, res_lib.status_code) self.assertEqual(2, len(res_lib.json['packages'])) self.assertEqual('Library', res_lib.json['packages'][0]['type']) self.assertEqual(excepted_pkg1.name, res_lib.json['packages'][0]['name']) self.assertEqual('Library', res_lib.json['packages'][1]['type']) self.assertEqual(excepted_pkg2.name, res_lib.json['packages'][1]['name']) res_app = req_app.get_response(self.api) self.assertEqual(200, res_app.status_code) self.assertEqual(1, len(res_app.json['packages'])) self.assertEqual('Application', res_app.json['packages'][0]['type']) self.assertEqual(excepted_pkg3.name, res_app.json['packages'][0]['name']) @mock.patch('murano.api.v1.catalog.LOG') def test_packages_filter_by_order_by(self, mock_log): warnings = [] mock_log.warning = lambda msg: warnings.append(msg) self._set_policy_rules( {'get_package': '', 'manage_public_package': ''} ) self._add_pkg("test_tenant", type='Library') # Test whether a valid order by value works. order_by = 'name' request = self._get('/catalog/packages', params={'order_by': order_by}) self.expect_policy_check('get_package') self.expect_policy_check('manage_public_package') self.controller.search(request) self.assertEqual(len(warnings), 0) # Test whether an invalid order by value fails. order_by = 'TEST ORDER BY' request = self._get('/catalog/packages', params={'order_by': order_by}) self.expect_policy_check('get_package') self.expect_policy_check('manage_public_package') self.controller.search(request) self.assertEqual(len(warnings), 1) self.assertIn('parameter is not valid', warnings[0]) def test_packages_filter_by_limit(self): """Test that packages are filtered by limit.""" self._set_policy_rules( {'get_package': '', 'manage_public_package': ''} ) pkg = self._add_pkg("test_tenant", type='Library') request = self._get('/catalog/packages', params={'limit': '1'}) self.expect_policy_check('get_package') self.expect_policy_check('manage_public_package') res = self.controller.search(request) self.assertIn('next_marker', res) self.assertEqual(res['next_marker'], pkg['id']) def test_packages_filter_by_limit_negative_cases(self): """Test whether invalid limit values throw expected exceptions.""" self._set_policy_rules( {'get_package': '', 'manage_public_package': ''} ) self._add_pkg("test_tenant", type='Library') # Test wheter non-number value throws exception request = self._get('/catalog/packages', params={'limit': 'not a number'}) self.expect_policy_check('get_package') self.expect_policy_check('manage_public_package') e = self.assertRaises(exc.HTTPBadRequest, self.controller.search, request) self.assertEqual(e.explanation, 'Limit param must be an integer') # Test whether below-zero value throws exception request = self._get('/catalog/packages', params={'limit': '-1'}) self.expect_policy_check('get_package') self.expect_policy_check('manage_public_package') e = self.assertRaises(exc.HTTPBadRequest, self.controller.search, request) self.assertEqual(e.explanation, 'Limit param must be positive') @mock.patch('murano.common.utils.split_for_quotes') @mock.patch('murano.api.v1.catalog.LOG') def test_packages_filter_handle_value_error(self, mock_log, mock_func): warnings = [] mock_func.side_effect = ValueError mock_log.warning = lambda msg: warnings.append(msg) self._set_policy_rules( {'get_package': '', 'manage_public_package': ''} ) self._add_pkg("test_tenant", type='Library') tag_in = "in:tag1,tag3" self._get('/catalog/packages', params={'tag': tag_in}) request = self._get('/catalog/packages', params={'tag': tag_in}) self.expect_policy_check('get_package') self.expect_policy_check('manage_public_package') self.controller.search(request) self.assertEqual(len(warnings), 1) self.assertIn("Search by parameter 'tag' caused an error", warnings[0]) def test_packages_filter_by_search(self): self._set_policy_rules( {'get_package': '', 'manage_public_package': ''} ) excepted_pkg1 = self._add_pkg("test_tenant", type='Application', name='awcloud', description='some context') excepted_pkg2 = self._add_pkg("test_tenant", type='Application', name='mysql', description='awcloud product') excepted_pkg3 = self._add_pkg("test_tenant", type='Application', name='package3', author='mysql author') # filter by search=awcloud can see 2 pkgs req_awc = self._get('/catalog/packages', params={'search': 'awcloud'}) # filter by search=mysql only see 2 pkgs req_mysql = self._get('/catalog/packages', params={'search': 'mysql'}) for dummy in range(2): self.expect_policy_check('get_package') self.expect_policy_check('manage_public_package') res_awc = req_awc.get_response(self.api) self.assertEqual(200, res_awc.status_code) self.assertEqual(2, len(res_awc.json['packages'])) self.assertEqual(excepted_pkg1.name, res_awc.json['packages'][0]['name']) self.assertEqual(excepted_pkg2.name, res_awc.json['packages'][1]['name']) res_mysql = req_mysql.get_response(self.api) self.assertEqual(200, res_mysql.status_code) self.assertEqual(2, len(res_mysql.json['packages'])) self.assertEqual(excepted_pkg2.name, res_mysql.json['packages'][0]['name']) self.assertEqual(excepted_pkg3.name, res_mysql.json['packages'][1]['name']) def test_packages(self): self._set_policy_rules( {'get_package': '', 'manage_public_package': ''} ) for dummy in range(9): self.expect_policy_check('get_package') self.expect_policy_check('manage_public_package') result = self.controller.search(self._get('/v1/catalog/packages/')) self.assertEqual(0, len(result['packages'])) self._add_pkg('test_tenant') self._add_pkg('test_tenant') self._add_pkg('other_tenant') self._add_pkg('other_tenant') # non-admin should only see 2 pkgs he can edit. self.is_admin = False result = self.controller.search(self._get('/v1/catalog/packages/')) self.assertEqual(2, len(result['packages'])) # can only deploy his + public result = self.controller.search(self._get( '/v1/catalog/packages/', params={'catalog': 'True'})) self.assertEqual(2, len(result['packages'])) # admin can edit anything self.is_admin = True result = self.controller.search(self._get('/v1/catalog/packages/')) self.assertEqual(4, len(result['packages'])) # admin can only deploy his + public result = self.controller.search(self._get( '/v1/catalog/packages/', params={'catalog': 'True'})) self.assertEqual(2, len(result['packages'])) self._add_pkg('test_tenant', public=True) self._add_pkg('other_tenant', public=True) # non-admin are allowed to edit public packages by policy self.is_admin = False result = self.controller.search(self._get('/v1/catalog/packages/')) self.assertEqual(4, len(result['packages'])) # can deploy mine + other public result = self.controller.search(self._get( '/v1/catalog/packages/', params={'catalog': 'True'})) self.assertEqual(4, len(result['packages'])) # admin can edit anything self.is_admin = True result = self.controller.search(self._get('/v1/catalog/packages/')) self.assertEqual(6, len(result['packages'])) # can deploy mine + public result = self.controller.search(self._get( '/v1/catalog/packages/', params={'catalog': 'True'})) self.assertEqual(4, len(result['packages'])) def _test_package(self, manifest='manifest.yaml'): package_dir = os.path.abspath( os.path.join( __file__, '../../../packages/test_packages/test.mpl.v1.app', ) ) pkg = load_utils.load_from_dir( package_dir, filename=manifest ) package = { 'fully_qualified_name': pkg.full_name, 'type': pkg.package_type, 'author': pkg.author, 'supplier': pkg.supplier, 'name': pkg.display_name, 'description': pkg.description, 'is_public': True, 'tags': pkg.tags, 'logo': pkg.logo, 'supplier_logo': pkg.supplier_logo, 'ui_definition': pkg.ui, 'class_definitions': tuple(pkg.classes), 'archive': pkg.blob, 'categories': [], } return pkg, package def test_modify_package_tags(self): self._set_policy_rules( {'modify_package': '', 'manage_public_package': '', 'publicize_package': ''} ) saved_package = self._add_pkg('test_tenant') saved_package_pub = self._add_pkg('test_tenant', public=True) self.expect_policy_check('modify_package', {'package_id': saved_package.id}) url = '/v1/catalog/packages/' + str(saved_package.id) data = [] data.append({'op': 'add', 'path': ['tags'], 'value': ["foo", "bar"]}) req = self._patch(url, jsonutils.dump_as_bytes(data)) result = self.controller.update(req, data, saved_package.id) self.assertIn('foo', result['tags']) self.assertIn('bar', result['tags']) self.expect_policy_check('modify_package', {'package_id': saved_package_pub.id}) self.expect_policy_check('manage_public_package', {}) url = '/v1/catalog/packages/' + str(saved_package_pub.id) data = [] data.append({'op': 'add', 'path': ['tags'], 'value': ["foo", "bar"]}) req = self._patch(url, jsonutils.dump_as_bytes(data)) result = self.controller.update(req, data, saved_package_pub.id) self.assertIn('foo', result['tags']) self.assertIn('bar', result['tags']) def test_modify_package_name(self): self._set_policy_rules( {'modify_package': ''} ) saved_package = self._add_pkg('test_tenant') self.expect_policy_check('modify_package', {'package_id': saved_package.id}) url = '/v1/catalog/packages/' + str(saved_package.id) data = [] data.append({'op': 'replace', 'path': ['name'], 'value': 'test_name'}) req = self._patch(url, jsonutils.dump_as_bytes(data)) result = self.controller.update(req, data, saved_package.id) self.assertEqual('test_name', result['name']) self.expect_policy_check('modify_package', {'package_id': saved_package.id}) data = [] data.append({'op': 'replace', 'path': ['name'], 'value': 'a' * 81}) req = self._patch(url, jsonutils.dump_as_bytes(data)) self.assertRaises(exc.HTTPBadRequest, self.controller.update, req, data, saved_package.id) def test_modify_package_no_body(self): self._set_policy_rules( {'modify_package': ''} ) saved_package = self._add_pkg('test_tenant') self.expect_policy_check('modify_package', {'package_id': saved_package.id}) url = '/v1/catalog/packages/' + str(saved_package.id) req = self._patch(url, jsonutils.dump_as_bytes(None)) self.assertRaises(exc.HTTPBadRequest, self.controller.update, req, None, saved_package.id) def test_modify_package_is_public(self): self._set_policy_rules( {'modify_package': '', 'manage_public_package': '', 'publicize_package': ''} ) saved_package = self._add_pkg('test_tenant') url = '/v1/catalog/packages/' + str(saved_package.id) self.expect_policy_check('modify_package', {'package_id': saved_package.id}) self.expect_policy_check('publicize_package', {}) data = [] data.append({'op': 'replace', 'path': ['is_public'], 'value': True}) req = self._patch(url, jsonutils.dump_as_bytes(data)) result = self.controller.update(req, data, saved_package.id) self.assertTrue(result['is_public']) def test_modify_package_bad_content_type(self): self._set_policy_rules( {'modify_package': '', 'manage_public_package': '', 'publicize_package': ''} ) saved_package = self._add_pkg('test_tenant') url = '/v1/catalog/packages/' + str(saved_package.id) self.expect_policy_check('modify_package', {'package_id': saved_package.id}) self.expect_policy_check('publicize_package', {}) data = [] data.append({'op': 'replace', 'path': ['is_public'], 'value': True}) req = self._patch(url, jsonutils.dump_as_bytes(data)) result = self.controller.update(req, data, saved_package.id) self.assertTrue(result['is_public']) def test_modify_package_with_incorrect_content_type(self): self._set_policy_rules( {'modify_package': ''} ) saved_package = self._add_pkg('test_tenant') self.expect_policy_check('modify_package', {'package_id': saved_package.id}) url = '/v1/catalog/packages/' + str(saved_package.id) data = [] data.append({'op': 'replace', 'path': ['name'], 'value': 'test_name'}) req = self._patch(url, jsonutils.dump_as_bytes(data)) req.get_content_type = mock.MagicMock( side_effect=common_exc.UnsupportedContentType) self.assertRaises(exc.HTTPBadRequest, self.controller.update, req, data, saved_package.id) def test_not_valid_logo(self): self.assertRaises(exceptions.PackageLoadError, self._test_package, 'manifest_with_broken_logo.yaml') def test_load_package_with_supplier_info(self): self._set_policy_rules( {'get_package': '@'} ) _, package = self._test_package() saved_package = db_catalog_api.package_upload(package, '') self.expect_policy_check('get_package', {'package_id': saved_package.id}) req = self._get('/v1/catalog/packages/%s' % saved_package.id) result = self.controller.get(req, saved_package.id) self.assertEqual(package['supplier'], result['supplier']) req = self._get( '/v1/catalog/packages/%s/supplier_logo' % saved_package.id ) result = self.controller.get_supplier_logo(req, saved_package.id) self.assertEqual('png', imghdr.what('', result)) def test_download_package(self): self._set_policy_rules( {'download_package': '@'} ) _, package = self._test_package() saved_package = db_catalog_api.package_upload(package, '') self.expect_policy_check('download_package', {'package_id': saved_package.id}) req = self._get_with_accept('/catalog/packages/%s/download' % saved_package.id, accept='application/octet-stream') result = req.get_response(self.api) self.assertEqual(200, result.status_code) def test_download_package_negative(self): _, package = self._test_package() saved_package = db_catalog_api.package_upload(package, '') req = self._get_with_accept('/catalog/packages/%s/download' % saved_package.id, accept='application/foo') result = req.get_response(self.api) self.assertEqual(406, result.status_code) self.assertIn(b'Acceptable response can not be provided', result.body) @mock.patch('murano.api.v1.catalog._validate_body') @mock.patch('murano.common.policy.check') def test_upload_package_with_invalid_schema(self, mock_policy_check, mock_validate_body): invalid_pkg_upload_schema = {"type": None} mock_policy_check.return_value = True mock_validate_body.return_value = (None, invalid_pkg_upload_schema) mock_request = mock.MagicMock(context={}) e = self.assertRaises(exc.HTTPBadRequest, self.controller.upload, mock_request) self.assertIn( "Package schema is not valid", e.explanation ) @mock.patch('murano.api.v1.catalog._validate_body') @mock.patch('murano.common.policy.check') def test_upload_package_with_invalid_file_content(self, mock_policy_check, mock_validate_body): with tempfile.NamedTemporaryFile(delete=True) as temp_file: mock_policy_check.return_value = True mock_validate_body.return_value = (mock.MagicMock(file=temp_file), None) mock_request = mock.MagicMock(context={}) e = self.assertRaises(exc.HTTPBadRequest, self.controller.upload, mock_request) self.assertIn( "Uploading file can't be empty", e.explanation ) @mock.patch('murano.api.v1.catalog.PKG_PARAMS_MAP') @mock.patch('murano.packages.load_utils.load_from_file') @mock.patch('murano.api.v1.catalog._validate_body') @mock.patch('murano.common.policy.check') def test_upload_package_with_oversized_file_name(self, mock_policy_check, mock_validate_body, mock_load_from_file, mock_pkg_params_map): mock_policy_check.return_value = True mock_load_from_file.return_value = mock.MagicMock( __exit__=lambda obj, type, value, tb: False) mock_pkg_params_map.return_value = {} mock_request = mock.MagicMock(context=mock.MagicMock( tenant=self.tenant)) test_package_meta = {'name': 'a' * 81} with tempfile.NamedTemporaryFile(delete=True) as temp_file: temp_file.write(b"Random test content\n") temp_file.seek(0) mock_validate_body.return_value = \ (mock.MagicMock(file=temp_file), test_package_meta) e = self.assertRaises(exc.HTTPBadRequest, self.controller.upload, mock_request) self.assertIn( "Package name should be 80 characters maximum", e.explanation ) @mock.patch('murano.packages.load_utils.load_from_file') @mock.patch('murano.api.v1.catalog._validate_body') @mock.patch('murano.common.policy.check') def test_upload_package_handle_package_load_error(self, mock_policy_check, mock_validate_body, mock_load_from_file): pkg_to_upload = mock.MagicMock( __exit__=lambda obj, type, value, tb: False) mock_request = mock.MagicMock(context=mock.MagicMock( tenant=self.tenant)) mock_load_from_file.return_value = pkg_to_upload mock_load_from_file.side_effect = exceptions.PackageLoadError mock_policy_check.return_value = True with tempfile.NamedTemporaryFile(delete=True) as temp_file: temp_file.write(b"Random test content\n") temp_file.seek(0) mock_validate_body.return_value = \ (mock.MagicMock(file=temp_file), None) e = self.assertRaises(exc.HTTPBadRequest, self.controller.upload, mock_request) self.assertIn( "Couldn't load package from file", e.explanation ) @mock.patch('murano.packages.load_utils.load_from_file') @mock.patch('murano.api.v1.catalog._validate_body') @mock.patch('murano.common.policy.check') def test_upload_package_handle_duplicate_exception(self, mock_policy_check, mock_validate_body, mock_load_from_file): """Test whether duplicate error is correctly thrown.""" # Save the first package entry to the DB test_package_meta = self.test_package.copy() test_package_meta['name'] = 'test_package' test_package_meta['fully_qualified_name'] = str(uuid.uuid4()) test_package_meta['description'] = 'test_description' test_package_meta['enabled'] = False test_package_meta['is_public'] = False db_catalog_api.package_upload(test_package_meta, self.tenant) # Reverse the operation performed by upload for copying values from # pkg_to_upload into package_meta dict. pkg_to_upload = mock.MagicMock( __exit__=lambda obj, type, value, tb: False) for k, v in PKG_PARAMS_MAP.items(): if v in test_package_meta.keys(): val = test_package_meta[v] setattr(pkg_to_upload.__enter__(), k, val) # Delete extra properties so validation in upload passes. for attr in ['fully_qualified_name', 'ui_definition', 'author', 'supplier_logo', 'supplier', 'logo', 'type', 'archive']: if attr in test_package_meta.keys(): del test_package_meta[attr] mock_request = mock.MagicMock(context=mock.MagicMock( project_id=self.tenant)) mock_load_from_file.return_value = pkg_to_upload mock_policy_check.return_value = True with tempfile.NamedTemporaryFile(delete=True) as temp_file: temp_file.write(b"Random test content\n") temp_file.seek(0) mock_validate_body.return_value = \ (mock.MagicMock(file=temp_file), test_package_meta) e = self.assertRaises(exc.HTTPConflict, self.controller.upload, mock_request) self.assertIn( "Package with specified full name is already registered", e.detail ) @mock.patch('murano.common.policy.check') def test_upload_package_with_oversized_body(self, mock_policy_check): mock_policy_check.return_value = True packages_to_upload = {'a': 0, 'b': 1, 'c': 2} mock_request = mock.MagicMock(context={}) e = self.assertRaises(exc.HTTPBadRequest, self.controller.upload, mock_request, body=packages_to_upload) self.assertIn( "'multipart/form-data' request body should contain 1 or 2 " "parts: json string and zip archive.", e.explanation ) @mock.patch('murano.common.policy.check') def test_upload_package_with_empty_body(self, mock_policy_check): mock_policy_check.return_value = True packages_to_upload = {} mock_request = mock.MagicMock(context={}) e = self.assertRaises(exc.HTTPBadRequest, self.controller.upload, mock_request, body=packages_to_upload) self.assertEqual( 'There is no file package with application description', e.explanation) def test_get_ui_definition(self): self._set_policy_rules( {'get_package': '@'} ) _, package = self._test_package() saved_package = db_catalog_api.package_upload(package, '') self.expect_policy_check('get_package', {'package_id': saved_package.id}) req = self._get_with_accept('/catalog/packages/%s/ui' % saved_package.id, accept="text/plain") result = req.get_response(self.api) self.assertEqual(200, result.status_code) def test_get_ui_definition_negative(self): _, package = self._test_package() saved_package = db_catalog_api.package_upload(package, '') req = self._get_with_accept('/catalog/packages/%s/ui' % saved_package.id, accept='application/foo') result = req.get_response(self.api) self.assertEqual(406, result.status_code) self.assertIn(b'Acceptable response can not be provided', result.body) def test_get_logo(self): self._set_policy_rules( {'get_package': '@'} ) _, package = self._test_package() saved_package = db_catalog_api.package_upload(package, '') self.expect_policy_check('get_package', {'package_id': saved_package.id}) req = self._get_with_accept('/catalog/packages/%s/logo' % saved_package.id, accept="application/octet-stream") result = req.get_response(self.api) self.assertEqual(200, result.status_code) self.assertEqual(package['logo'], result.body) def test_get_logo_negative(self): _, package = self._test_package() saved_package = db_catalog_api.package_upload(package, '') req = self._get_with_accept('/catalog/packages/%s/logo' % saved_package.id, accept='application/foo') result = req.get_response(self.api) self.assertEqual(406, result.status_code) self.assertIn(b'Acceptable response can not be provided', result.body) def test_add_public_unauthorized(self): self._set_policy_rules({ 'upload_package': '@', 'publicize_package': 'is_admin:True', 'delete_package': 'is_admin:True', }) self.expect_policy_check('upload_package') self.expect_policy_check('delete_package', mock.ANY) self.expect_policy_check('upload_package') self.expect_policy_check('publicize_package') self.expect_policy_check('upload_package') self.expect_policy_check('publicize_package') file_obj_str = StringIO("This is some dummy data") file_obj = mock.MagicMock(cgi.FieldStorage) file_obj.file = file_obj_str package_from_dir, _ = self._test_package() body_fmt = '''\ --BOUNDARY Content-Disposition: form-data; name="__metadata__" {0} --BOUNDARY Content-Disposition: form-data; name="ziparchive"; filename="file.zip" This is a fake zip archive --BOUNDARY--''' def format_body(content): content = jsonutils.dumps(content) body = body_fmt.format(content) body = body.encode('utf-8') return body with mock.patch('murano.packages.load_utils.load_from_file') as lff: ctxmgr = mock.Mock() ctxmgr.__enter__ = mock.Mock(return_value=package_from_dir) ctxmgr.__exit__ = mock.Mock(return_value=False) lff.return_value = ctxmgr # Uploading a non-public package req = self._post( '/catalog/packages', format_body({'is_public': False}), content_type='multipart/form-data; ; boundary=BOUNDARY', ) res = req.get_response(self.api) self.assertEqual(200, res.status_code) self.is_admin = True app_id = jsonutils.loads(res.body)['id'] req = self._delete('/catalog/packages/{0}'.format(app_id)) res = req.get_response(self.api) self.is_admin = False # Uploading a public package fails req = self._post( '/catalog/packages', format_body({'is_public': True}), content_type='multipart/form-data; ; boundary=BOUNDARY', ) res = req.get_response(self.api) self.assertEqual(403, res.status_code) # Uploading a public package passes for admin self.is_admin = True req = self._post( '/catalog/packages', format_body({'is_public': True}), content_type='multipart/form-data; ; boundary=BOUNDARY', ) res = req.get_response(self.api) self.assertEqual(200, res.status_code) def test_upload_pkg_with_tag(self): """Check upload package with tags successfully""" self._set_policy_rules({'upload_package': '@'}) self.expect_policy_check('upload_package') file_obj_str = StringIO("This is some dummy data") file_obj = mock.MagicMock(cgi.FieldStorage) file_obj.file = file_obj_str package_from_dir, _ = self._test_package() body_fmt = '''\ --BOUNDARY Content-Disposition: form-data; name="__metadata__" {0} --BOUNDARY Content-Disposition: form-data; name="ziparchive"; filename="file.zip" This is a fake zip archive --BOUNDARY--''' def format_body(content): content = jsonutils.dumps(content) body = body_fmt.format(content) body = body.encode('utf-8') return body with mock.patch('murano.packages.load_utils.load_from_file') as lff: ctxmgr = mock.Mock() ctxmgr.__enter__ = mock.Mock(return_value=package_from_dir) ctxmgr.__exit__ = mock.Mock(return_value=False) lff.return_value = ctxmgr # Uploading a package with foo_tag req = self._post( '/catalog/packages', format_body({'tags': ['foo_tag']}), content_type='multipart/form-data; ; boundary=BOUNDARY', ) res = req.get_response(self.api) processed_result = jsonutils.loads(res.body) # check user set foo_tag in result self.assertIn('foo_tag', processed_result["tags"]) # check tag Linux from package in result self.assertIn('Linux', processed_result["tags"]) def test_add_category(self): """Check that category added successfully""" self._set_policy_rules({'add_category': '@'}) self.expect_policy_check('add_category') fake_now = timeutils.utcnow() timeutils.utcnow.override_time = fake_now expected = { 'name': 'new_category', 'created': datetime.isoformat(fake_now)[:-7], 'updated': datetime.isoformat(fake_now)[:-7], 'package_count': 0, } body = {'name': 'new_category'} req = self._post('/catalog/categories', jsonutils.dump_as_bytes(body)) result = req.get_response(self.api) processed_result = jsonutils.loads(result.body) self.assertIn('id', processed_result.keys()) expected['id'] = processed_result['id'] self.assertEqual(expected, processed_result) def test_get_category(self): """Check that get category executed successfully""" self._set_policy_rules({'add_category': '@', 'get_category': '@'}) self.expect_policy_check('add_category') fake_now = timeutils.utcnow() timeutils.utcnow.override_time = fake_now expected = { 'name': 'new_category', 'created': datetime.isoformat(fake_now)[:-7], 'updated': datetime.isoformat(fake_now)[:-7], 'package_count': 0, 'packages': [] } body = {'name': 'new_category'} req = self._post('/catalog/categories', jsonutils.dump_as_bytes(body)) result = req.get_response(self.api) expected['id'] = jsonutils.loads(result.body)['id'] self.expect_policy_check('get_category') req = self._get('/catalog/categories/%s' % expected['id']) retrieved_category = jsonutils.loads(req.get_response(self.api).body) self.assertEqual(retrieved_category, expected) def test_delete_category(self): """Check that category deleted successfully""" self._set_policy_rules({'delete_category': '@'}) self.expect_policy_check('delete_category', {'category_id': '12345'}) fake_now = timeutils.utcnow() expected = {'name': 'new_category', 'created': fake_now, 'updated': fake_now, 'id': '12345'} e = models.Category(**expected) test_utils.save_models(e) req = self._delete('/catalog/categories/12345') processed_result = req.get_response(self.api) self.assertEqual(b'', processed_result.body) self.assertEqual(200, processed_result.status_code) @mock.patch('murano.db.catalog.api.category_get') def test_delete_category_with_packages_negative(self, mock_get_category): """Check that deleting category that has assigned packages fails.""" mock_get_category.return_value = mock.MagicMock( packages=['test_package']) self._set_policy_rules({'delete_category': '@'}) self.expect_policy_check('delete_category', {'category_id': '12345'}) fake_now = timeutils.utcnow() expected = {'name': 'new_category', 'created': fake_now, 'updated': fake_now, 'id': '12345'} category = models.Category(**expected) test_utils.save_models(category) req = self._delete('/catalog/categories/12345') e = self.assertRaises(exc.HTTPForbidden, self.controller.delete_category, req, '12345') self.assertEqual(e.explanation, "It's impossible to delete categories assigned to the" " package, uploaded to the catalog") def test_add_category_without_name(self): """Test whether adding a category without a name throws exception.""" self._set_policy_rules({'add_category': '@'}) self.expect_policy_check('add_category') body = {'name': ''} req = self._post('/catalog/categories', jsonutils.dump_as_bytes(body)) e = self.assertRaises(exc.HTTPBadRequest, self.controller.add_category, req, body) self.assertEqual("Please, specify a name of the category to create", e.explanation) def test_add_category_handle_duplicate_exception(self): """Test whether creating duplicate categories throws exception.""" self._set_policy_rules({'add_category': '@'}) self.expect_policy_check('add_category') self.expect_policy_check('add_category') body = {'name': 'new_category'} req = self._post('/catalog/categories', jsonutils.dump_as_bytes(body)) self.controller.add_category(req, body) req = self._post('/catalog/categories', jsonutils.dump_as_bytes(body)) e = self.assertRaises(exc.HTTPConflict, self.controller.add_category, req, body) self.assertEqual("Category with specified name is already exist", e.explanation) def test_add_category_failed_for_non_admin(self): """Check that non admin user couldn't add new category""" self._set_policy_rules({'add_category': 'role:context_admin'}) self.is_admin = False self.expect_policy_check('add_category') fake_now = timeutils.utcnow() timeutils.utcnow.override_time = fake_now body = {'name': 'new_category'} req = self._post('/catalog/categories', jsonutils.dump_as_bytes(body)) result = req.get_response(self.api) self.assertEqual(403, result.status_code) def test_add_long_category(self): """Test that category name does not exceed 80 characters Check that a category that contains more than 80 characters fails to be added """ self._set_policy_rules({'add_category': '@'}) self.expect_policy_check('add_category') fake_now = timeutils.utcnow() timeutils.utcnow.override_time = fake_now body = {'name': 'cat' * 80} req = self._post('/catalog/categories', jsonutils.dump_as_bytes(body)) result = req.get_response(self.api) self.assertEqual(400, result.status_code) result_message = result.text.replace('\n', '') self.assertIn('Category name should be 80 characters maximum', result_message) def test_list_categories(self): names = ['cat1', 'cat2'] for name in names: db_catalog_api.category_add(name) self._set_policy_rules({'get_category': '@'}) self.expect_policy_check('get_category') req = self._get('/catalog/categories') result = req.get_response(self.api) self.assertEqual(200, result.status_code) result_categories = jsonutils.loads(result.body)['categories'] self.assertEqual(2, len(result_categories)) self.assertEqual(names, [c['name'] for c in result_categories]) params = {'sort_keys': 'created, id'} req = self._get('/catalog/categories', params) self.expect_policy_check('get_category') result = req.get_response(self.api) self.assertEqual(200, result.status_code) result_categories = jsonutils.loads(result.body)['categories'] self.assertEqual(names, [c['name'] for c in result_categories]) names.reverse() params = {'sort_dir': 'desc'} req = self._get('/catalog/categories', params) self.expect_policy_check('get_category') result = req.get_response(self.api) self.assertEqual(200, result.status_code) result_categories = jsonutils.loads(result.body)['categories'] self.assertEqual(names, [c['name'] for c in result_categories]) def test_list_categories_negative(self): self._set_policy_rules({'get_category': '@'}) self.expect_policy_check('get_category') req = self._get('/catalog/categories', {'sort_dir': 'test'}) result = req.get_response(self.api) self.assertEqual(400, result.status_code) self.expect_policy_check('get_category') req = self._get('/catalog/categories', {'sort_keys': 'test'}) result = req.get_response(self.api) self.assertEqual(400, result.status_code) self.expect_policy_check('get_category') req = self._get('/catalog/categories', {'test': ['test']}) result = req.get_response(self.api) self.assertEqual(400, result.status_code) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/api/v1/test_deployments.py0000664000175000017500000003267500000000000023634 0ustar00zuulzuul00000000000000# 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 murano.tests.unit.api.base as tb from unittest import mock from oslo_config import fixture as config_fixture from oslo_serialization import jsonutils from murano.api.v1 import deployments from murano.api.v1 import environments from webob import exc class TestDeploymentsApi(tb.ControllerTest, tb.MuranoApiTestCase): def setUp(self): super(TestDeploymentsApi, self).setUp() self.environments_controller = environments.Controller() self.deployments_controller = deployments.Controller() self.fixture = self.useFixture(config_fixture.Config()) self.fixture.conf(args=[]) def test_deployments_index(self): CREDENTIALS = {'tenant': 'test_tenant_1', 'user': 'test_user_1'} self._set_policy_rules( {'create_environment': '@', 'list_deployments': '@'} ) # Create an environment. self.expect_policy_check('create_environment') request = self._post( '/environments', jsonutils.dump_as_bytes({'name': 'test_environment_1'}), **CREDENTIALS ) response_body = jsonutils.loads(request.get_response(self.api).body) self.assertEqual(CREDENTIALS['tenant'], response_body['tenant_id']) self.assertEqual('test_environment_1', response_body['name']) ENVIRONMENT_ID = response_body['id'] # Verify that the environment has not yet been deployed. self.expect_policy_check('list_deployments', {'environment_id': ENVIRONMENT_ID}) result = self.deployments_controller.index(request, ENVIRONMENT_ID) self.assertEqual([], result['deployments']) # Deploy the environment. request = self._post('/environments/{environment_id}/configure' .format(environment_id=ENVIRONMENT_ID), b'', **CREDENTIALS) response_body = jsonutils.loads(request.get_response(self.api).body) SESSION_ID = response_body['id'] request = self._post('/environments/{environment_id}/sessions/' '{session_id}/deploy' .format(environment_id=ENVIRONMENT_ID, session_id=SESSION_ID), b'', **CREDENTIALS) result = request.get_response(self.api) self.assertEqual('200 OK', result.status) # Verify that the environment was deployed. self.expect_policy_check('list_deployments', {'environment_id': ENVIRONMENT_ID}) result = self.deployments_controller.index(request, ENVIRONMENT_ID) deployment_id = result['deployments'][0]['id'] self.assertEqual(1, len(result['deployments'])) self.assertIsNotNone(deployment_id) def test_deployments_all_environments(self): """Test list deployments for all environments. Create 2 environments, deploy both, and check that 2 deployments exist. """ CREDENTIALS = {'tenant': 'test_tenant_1', 'user': 'test_user_1'} self._set_policy_rules( {'create_environment': '@', 'list_deployments_all_environments': '@'} ) for count in range(2): # Create environment. self.expect_policy_check('create_environment') request = self._post( '/environments', jsonutils.dump_as_bytes( {'name': 'test_environment_{0}'.format(count)}), **CREDENTIALS ) response_body = jsonutils.loads( request.get_response(self.api).body) self.assertEqual(CREDENTIALS['tenant'], response_body['tenant_id']) self.assertEqual('test_environment_{0}'.format(count), response_body['name']) ENVIRONMENT_ID = response_body['id'] # Deploy environment. request = self._post('/environments/{environment_id}/configure' .format(environment_id=ENVIRONMENT_ID), b'', **CREDENTIALS) response_body = jsonutils.loads( request.get_response(self.api).body) SESSION_ID = response_body['id'] request = self._post('/environments/{environment_id}/sessions/' '{session_id}/deploy' .format(environment_id=ENVIRONMENT_ID, session_id=SESSION_ID), b'', **CREDENTIALS) result = request.get_response(self.api) self.assertEqual('200 OK', result.status) # Check that 2 deployments exist. self.expect_policy_check('list_deployments_all_environments') result = self.deployments_controller.index(request, None) self.assertEqual(2, len(result['deployments'])) for deployment in result['deployments']: self.assertIsNotNone(deployment) self.assertNotEqual(result['deployments'][0], result['deployments'][1]) def test_deployments_all_environments_different_tenants(self): """Test list deployments for all environments in different tenants. Should only return return environments for current tenant. """ CREDENTIALS = {'tenant': 'test_tenant_1', 'user': 'test_user_1'} ALT_CREDENTIALS = {'tenant': 'test_tenant_2', 'user': 'test_user_2'} self._set_policy_rules( {'create_environment': '@', 'list_deployments_all_environments': '@'} ) deployments = [] # Create the first environment inside first tenant and the second # environments inside the alternate tenant. Then deploy both. for count, creds in enumerate([CREDENTIALS, ALT_CREDENTIALS]): # Create each environment. self.expect_policy_check('create_environment') request = self._post( '/environments', jsonutils.dump_as_bytes( {'name': 'test_environment_{0}'.format(count)}), **creds ) response_body = jsonutils.loads( request.get_response(self.api).body) self.assertEqual(creds['tenant'], response_body['tenant_id']) self.assertEqual('test_environment_{0}'.format(count), response_body['name']) ENVIRONMENT_ID = response_body['id'] # Deploy each environment. request = self._post('/environments/{environment_id}/configure' .format(environment_id=ENVIRONMENT_ID), b'', **creds) response_body = jsonutils.loads( request.get_response(self.api).body) SESSION_ID = response_body['id'] request = self._post('/environments/{environment_id}/sessions/' '{session_id}/deploy' .format(environment_id=ENVIRONMENT_ID, session_id=SESSION_ID), b'', **creds) result = request.get_response(self.api) self.assertEqual('200 OK', result.status) # Check that each tenant only returns one deployment. self.expect_policy_check('list_deployments_all_environments') result = self.deployments_controller.index(request, None) self.assertEqual(1, len(result['deployments'])) deployment_id = result['deployments'][0]['id'] self.assertIsNotNone(deployment_id) deployments.append(deployment_id) self.assertNotEqual(deployments[0], deployments[1]) def test_deployments_not_found_statuses(self): CREDENTIALS = {'tenant': 'test_tenant_1', 'user': 'test_user_1'} self._set_policy_rules( {'create_environment': '@', 'statuses_deployments': '@'} ) self.expect_policy_check('create_environment') # Create environment request = self._post('/environments', jsonutils. dump_as_bytes({'name': 'test_environment_1'}), **CREDENTIALS) response_body = jsonutils.loads(request.get_response(self.api).body) self.assertEqual(CREDENTIALS['tenant'], response_body['tenant_id']) ENVIRONMENT_ID = response_body['id'] deploy_id = '1' self.expect_policy_check('statuses_deployments', {'deployment_id': deploy_id, 'environment_id': ENVIRONMENT_ID}) self.assertRaises(exc.HTTPNotFound, self.deployments_controller.statuses, request, ENVIRONMENT_ID, deploy_id) def test_deployments_status(self): CREDENTIALS = {'tenant': 'test_tenant', 'user': 'test_user'} self._set_policy_rules( {'create_environment': '@', 'statuses_deployments': '@', 'list_deployments': '@'} ) self.expect_policy_check('create_environment') # Create environment request = self._post( '/environments', jsonutils.dump_as_bytes({'name': 'test_environment'}), **CREDENTIALS ) response_body = jsonutils.loads(request.get_response(self.api).body) self.assertEqual(CREDENTIALS['tenant'], response_body['tenant_id']) ENVIRONMENT_ID = response_body['id'] # Create session request = self._post('/environments/{environment_id}/configure' .format(environment_id=ENVIRONMENT_ID), b'', **CREDENTIALS) response_body = jsonutils.loads(request.get_response(self.api).body) SESSION_ID = response_body['id'] request = self._post('/environments/{environment_id}/sessions/' '{session_id}/deploy' .format(environment_id=ENVIRONMENT_ID, session_id=SESSION_ID), b'', **CREDENTIALS) request.get_response(self.api) self.expect_policy_check('list_deployments', {'environment_id': ENVIRONMENT_ID}) result = self.deployments_controller.index(request, ENVIRONMENT_ID) deploy_id = result['deployments'][0]['id'] self.expect_policy_check('statuses_deployments', {'deployment_id': deploy_id, 'environment_id': ENVIRONMENT_ID}) result = self.deployments_controller.statuses(request, ENVIRONMENT_ID, deploy_id) self.assertNotEqual(result['reports'], []) request.GET['service_id'] = "12" self.expect_policy_check('statuses_deployments', {'deployment_id': deploy_id, 'environment_id': ENVIRONMENT_ID}) result = self.deployments_controller.statuses(request, ENVIRONMENT_ID, deploy_id) self.assertEqual(result['reports'], []) self.expect_policy_check('statuses_deployments', {'deployment_id': deploy_id, 'environment_id': '12'}) self.assertRaises(exc.HTTPBadRequest, self.deployments_controller.statuses, request, "12", deploy_id) def test_set_dep_state(self): deployment = mock.Mock() deployment.id = "1" deployment.description = {'applications': []} unit = mock.Mock() query_result = mock.Mock() filter_by_result = mock.Mock() filter_by_result.count.return_value = 0 query_result.filter_by.return_value = filter_by_result unit.query.return_value = query_result result = deployments.set_dep_state(deployment, unit) self.assertEqual('success', result.state) deployment.description = None result = deployments.set_dep_state(deployment, unit) self.assertEqual('success', result.state) filter_by_result.count.return_value = 1 query_result.filter_by.return_value = filter_by_result result = deployments.set_dep_state(deployment, unit) self.assertEqual('completed_w_errors', result.state) filter_by_result.count.return_value = 0 query_result.filter_by.return_value = filter_by_result deployment.finished = False result = deployments.set_dep_state(deployment, unit) self.assertEqual('running', result.state) filter_by_result.count.return_value = 1 query_result.filter_by.return_value = filter_by_result result = deployments.set_dep_state(deployment, unit) self.assertEqual('running_w_errors', result.state) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/api/v1/test_env_templates.py0000664000175000017500000011136200000000000024126 0ustar00zuulzuul00000000000000# Copyright (c) 2015 Telefonica I+D. # 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 datetime import datetime from unittest import mock from oslo_config import fixture as config_fixture from oslo_db import exception as db_exc from oslo_serialization import jsonutils from oslo_utils import timeutils from murano.api.v1 import templates from murano.db import models import murano.tests.unit.api.base as tb import murano.tests.unit.utils as test_utils class TestEnvTemplateApi(tb.ControllerTest, tb.MuranoApiTestCase): def setUp(self): super(TestEnvTemplateApi, self).setUp() self.controller = templates.Controller() self.uuids = ['env_template_id', 'other', 'network_id', 'environment_id', 'session_id'] self.mock_uuid = self._stub_uuid(self.uuids) def test_list_empty_env_templates(self): """Check that with no templates an empty list is returned.""" self._set_policy_rules( {'list_env_templates': '@'} ) self.expect_policy_check('list_env_templates') req = self._get('/templates') result = req.get_response(self.api) self.assertEqual({'templates': []}, jsonutils.loads(result.body)) def test_create_env_templates(self): """Create an template, test template.show().""" self._set_policy_rules( {'list_env_templates': '@', 'create_env_template': '@', 'show_env_template': '@'} ) self.expect_policy_check('create_env_template') fake_now = timeutils.utcnow() timeutils.utcnow.override_time = fake_now expected = {'tenant_id': self.tenant, 'id': 'env_template_id', 'is_public': False, 'name': 'mytemp', 'description_text': 'description', 'version': 0, 'created': datetime.isoformat(fake_now)[:-7], 'updated': datetime.isoformat(fake_now)[:-7]} body = {'name': 'mytemp', 'description_text': 'description'} req = self._post('/templates', jsonutils.dump_as_bytes(body)) result = req.get_response(self.api) self.assertEqual(expected, jsonutils.loads(result.body)) # Reset the policy expectation self.expect_policy_check('list_env_templates') req = self._get('/templates') result = req.get_response(self.api) self.assertEqual(200, result.status_code) self.assertEqual({'templates': [expected]}, jsonutils.loads(result.body)) expected['services'] = [] self.expect_policy_check('show_env_template', {'env_template_id': self.uuids[0]}) req = self._get('/templates/%s' % self.uuids[0]) result = req.get_response(self.api) self.assertEqual(expected, jsonutils.loads(result.body)) @mock.patch('murano.db.services.environment_templates.EnvTemplateServices.' 'create') def test_create_env_templates_handle_duplicate_exc(self, mock_function): """Create an template, test template.show().""" self._set_policy_rules( {'create_env_template': '@'} ) self.expect_policy_check('create_env_template') mock_function.side_effect = db_exc.DBDuplicateEntry body = {'name': 'mytemp', 'description_text': 'description'} req = self._post('/templates', jsonutils.dump_as_bytes(body)) result = req.get_response(self.api) self.assertEqual(409, result.status_code) def test_list_public_env_templates(self): """Create an template, test templates.public().""" self._set_policy_rules( {'create_env_template': '@', 'list_env_templates': '@'} ) self.expect_policy_check('create_env_template') body = {'name': 'mytemp2', 'is_public': True} req = self._post('/templates', jsonutils.dump_as_bytes(body)) result = req.get_response(self.api) self.assertTrue(jsonutils.loads(result.body)['is_public']) self.expect_policy_check('list_env_templates') req = self._get('/templates', {'is_public': True}) result = req.get_response(self.api) data = jsonutils.loads(result.body) self.assertEqual(1, len(data)) self.assertTrue(data['templates'][0]['is_public']) def test_clone_env_templates(self): """Create an template, test templates.public().""" self._set_policy_rules( {'create_env_template': '@', 'clone_env_template': '@'} ) self.expect_policy_check('create_env_template') body = {'name': 'mytemp2', 'is_public': True} req = self._post('/templates', jsonutils.dump_as_bytes(body)) result = req.get_response(self.api) env_template_id = jsonutils.loads(result.body)['id'] self.assertTrue(jsonutils.loads(result.body)['is_public']) self.expect_policy_check('clone_env_template') body = {'name': 'clone', 'is_public': False} req = self._post('/templates/%s/clone' % env_template_id, jsonutils.dump_as_bytes(body)) result = req.get_response(self.api) self.assertFalse(jsonutils.loads(result.body)['is_public']) self.assertEqual('clone', jsonutils.loads(result.body)['name']) def test_clone_env_templates_private(self): """Create an template, test templates.public().""" self._set_policy_rules( {'create_env_template': '@', 'clone_env_template': '@'} ) self.expect_policy_check('create_env_template') body = {'name': 'mytemp2', 'is_public': False} req = self._post('/templates', jsonutils.dump_as_bytes(body)) result = req.get_response(self.api) env_template_id = jsonutils.loads(result.body)['id'] self.assertFalse(jsonutils.loads(result.body)['is_public']) self.expect_policy_check('clone_env_template') body = {'name': 'clone', 'is_public': False} req = self._post('/templates/%s/clone' % env_template_id, jsonutils.dump_as_bytes(body)) result = req.get_response(self.api) self.assertEqual(result.status_code, 403) @mock.patch('murano.db.services.environment_templates.EnvTemplateServices.' 'clone') def test_clone_env_templates_handle_duplicate_exc(self, mock_function): """Test whether clone duplication exception is handled correctly.""" mock_function.side_effect = db_exc.DBDuplicateEntry self._set_policy_rules( {'create_env_template': '@', 'clone_env_template': '@'} ) self.expect_policy_check('create_env_template') body = {'name': 'mytemp2', 'is_public': True} req = self._post('/templates', jsonutils.dump_as_bytes(body)) result = req.get_response(self.api) env_template_id = jsonutils.loads(result.body)['id'] self.expect_policy_check('clone_env_template') body = {'name': 'clone', 'is_public': False} req = self._post('/templates/%s/clone' % env_template_id, jsonutils.dump_as_bytes(body)) result = req.get_response(self.api) self.assertEqual(409, result.status_code) def test_list_public_env_templates_default(self): """Test listing public templates when there aren't any Create a template; test list public with no public templates. """ self._set_policy_rules( {'create_env_template': '@', 'list_env_templates': '@'} ) self.expect_policy_check('create_env_template') body = {'name': 'mytemp'} req = self._post('/templates', jsonutils.dump_as_bytes(body)) result = req.get_response(self.api) self.assertFalse(jsonutils.loads(result.body)['is_public']) self.expect_policy_check('list_env_templates') req = self._get('/templates', {'is_public': True}) result = req.get_response(self.api) self.assertFalse(0, len(jsonutils.loads(result.body))) def test_list_private_env_templates(self): """Test listing private templates Create a public template and a private template; test list private templates. """ self._set_policy_rules( {'create_env_template': '@', 'list_env_templates': '@'} ) self.expect_policy_check('create_env_template') body = {'name': 'mytemp', 'is_public': False} req = self._post('/templates', jsonutils.dump_as_bytes(body)) result = req.get_response(self.api) self.assertFalse(jsonutils.loads(result.body)['is_public']) self.expect_policy_check('create_env_template') body = {'name': 'mytemp1', 'is_public': True} req = self._post('/templates', jsonutils.dump_as_bytes(body)) result = req.get_response(self.api) self.assertTrue(jsonutils.loads(result.body)['is_public']) self.expect_policy_check('list_env_templates') req = self._get('/templates', {'is_public': False}) result = req.get_response(self.api) self.assertEqual(1, len(jsonutils.loads(result.body)['templates'])) def test_list_env_templates(self): """Test listing public templates when there aren't any Create a template; test list public with no public templates. """ self._set_policy_rules( {'create_env_template': '@', 'list_env_templates': '@'} ) self.expect_policy_check('create_env_template') body = {'name': 'mytemp', 'is_public': False} req = self._post('/templates', jsonutils.dump_as_bytes(body)) result = req.get_response(self.api) self.assertFalse(jsonutils.loads(result.body)['is_public']) self.expect_policy_check('create_env_template') body = {'name': 'mytemp1', 'is_public': True} req = self._post('/templates', jsonutils.dump_as_bytes(body)) result = req.get_response(self.api) self.assertTrue(jsonutils.loads(result.body)['is_public']) self.expect_policy_check('list_env_templates') req = self._get('/templates') result = req.get_response(self.api) self.assertEqual(2, len(jsonutils.loads(result.body)['templates'])) def test_list_env_templates_with_different_tenant(self): """Test listing public template from another tenant Create two template in two different tenants; test list public template from another tenant. """ self._set_policy_rules( {'create_env_template': '@', 'list_env_templates': '@'} ) self.expect_policy_check('create_env_template') body = {'name': 'mytemp', 'is_public': False} req = self._post('/templates', jsonutils.dump_as_bytes(body), tenant='first_tenant') result = req.get_response(self.api) self.assertFalse(jsonutils.loads(result.body)['is_public']) self.expect_policy_check('create_env_template') body = {'name': 'mytemp1', 'is_public': True} req = self._post('/templates', jsonutils.dump_as_bytes(body), tenant='second_tenant') result = req.get_response(self.api) self.assertTrue(jsonutils.loads(result.body)['is_public']) self.expect_policy_check('list_env_templates') req = self._get('/templates', tenant='first_tenant') result = req.get_response(self.api) self.assertEqual(2, len(jsonutils.loads(result.body)['templates'])) def test_illegal_template_name_create(self): """Check that an illegal temp name results in an HTTPClientError.""" self._set_policy_rules( {'list_env_templates': '@', 'create_env_template': '@', 'show_env_template': '@'} ) self.expect_policy_check('create_env_template') body = {'name': ' '} req = self._post('/templates', jsonutils.dump_as_bytes(body)) result = req.get_response(self.api) self.assertEqual(400, result.status_code) def test_too_long_template_name_create(self): """Check that a long template name results in an HTTPBadResquest.""" self._set_policy_rules( {'list_env_templates': '@', 'create_env_template': '@', 'show_env_template': '@'} ) self.expect_policy_check('create_env_template') body = {'name': 'a' * 256} req = self._post('/templates', jsonutils.dump_as_bytes(body)) result = req.get_response(self.api) self.assertEqual(400, result.status_code) result_msg = result.text.replace('\n', '') self.assertIn('Environment template name should be 255 characters ' 'maximum', result_msg) def test_mallformed_body(self): """Check that an illegal temp name results in an HTTPClientError.""" self._set_policy_rules( {'create_env_template': '@'} ) self.expect_policy_check('create_env_template') body = {'invalid': 'test'} req = self._post('/templates', jsonutils.dump_as_bytes(body)) result = req.get_response(self.api) self.assertEqual(400, result.status_code) def test_missing_env_template(self): """Check that a missing env template results in an HTTPNotFound.""" self._set_policy_rules( {'show_env_template': '@'} ) self.expect_policy_check('show_env_template', {'env_template_id': 'no-such-id'}) req = self._get('/templates/no-such-id') result = req.get_response(self.api) self.assertEqual(404, result.status_code) def test_update_env_template(self): """Check that environment rename works.""" self._set_policy_rules( {'show_env_template': '@', 'update_env_template': '@'} ) self.expect_policy_check('update_env_template', {'env_template_id': '12345'}) fake_now = timeutils.utcnow() timeutils.utcnow.override_time = fake_now expected = dict( id='12345', is_public=False, name='my-temp', version=0, created=fake_now, updated=fake_now, tenant_id=self.tenant, description_text='', description={ 'name': 'my-temp', '?': {'id': '12345'} } ) e = models.EnvironmentTemplate(**expected) test_utils.save_models(e) fake_now = timeutils.utcnow() timeutils.utcnow.override_time = fake_now del expected['description'] expected['services'] = [] expected['name'] = 'renamed_temp' expected['updated'] = fake_now body = { 'name': 'renamed_temp' } req = self._put('/templates/12345', jsonutils.dump_as_bytes(body)) result = req.get_response(self.api) self.assertEqual(200, result.status_code) self.expect_policy_check('show_env_template', {'env_template_id': '12345'}) req = self._get('/templates/12345') result = req.get_response(self.api) self.assertEqual(200, result.status_code) expected['created'] = datetime.isoformat(expected['created'])[:-7] expected['updated'] = datetime.isoformat(expected['updated'])[:-7] self.assertEqual(expected, jsonutils.loads(result.body)) def test_update_env_template_with_inappropriate_name(self): """Check that environment rename works.""" self._set_policy_rules( {'show_env_template': '@', 'update_env_template': '@'} ) self.expect_policy_check('update_env_template', {'env_template_id': '12345'}) fake_now = timeutils.utcnow() timeutils.utcnow.override_time = fake_now expected = dict( id='12345', is_public=False, name='my-temp', version=0, created=fake_now, updated=fake_now, tenant_id=self.tenant ) e = models.EnvironmentTemplate(**expected) test_utils.save_models(e) # Attempt to update the environment template with invalid name. body = {'name': ''} req = self._put('/templates/12345', jsonutils.dump_as_bytes(body)) result = req.get_response(self.api) self.assertEqual(400, result.status_code) self.assertIn(b'EnvTemplate body is incorrect', result.body) # Verify that the name was not changed. self.expect_policy_check('show_env_template', {'env_template_id': '12345'}) req = self._get('/templates/12345') result = req.get_response(self.api) self.assertEqual(200, result.status_code) self.assertEqual(expected['name'], jsonutils.loads(result.body)['name']) def test_delete_env_templates(self): """Test that environment deletion results in the correct rpc call.""" self._set_policy_rules( {'delete_env_template': '@'} ) self.expect_policy_check( 'delete_env_template', {'env_template_id': '12345'} ) fake_now = timeutils.utcnow() expected = dict( id='12345', name='my-temp', version=0, created=fake_now, updated=fake_now, tenant_id=self.tenant, description={ 'name': 'my-temp', '?': {'id': '12345'} } ) e = models.EnvironmentTemplate(**expected) test_utils.save_models(e) req = self._delete('/templates/12345') result = req.get_response(self.api) # Should this be expected behavior? self.assertEqual(b'', result.body) self.assertEqual(200, result.status_code) def test_create_env_templates_with_applications(self): """Create an template, test template.show().""" self._set_policy_rules( {'list_env_templates': '@', 'create_env_template': '@', 'show_env_template': '@'} ) self.expect_policy_check('create_env_template') fake_now = timeutils.utcnow() timeutils.utcnow.override_time = fake_now expected = {'tenant_id': self.tenant, 'id': self.uuids[0], 'is_public': False, 'name': 'env_template_name', 'description_text': '', 'version': 0, 'created': datetime.isoformat(fake_now)[:-7], 'updated': datetime.isoformat(fake_now)[:-7]} services = [ { "instance": { "assignFloatingIp": "true", "keyname": "mykeyname", "image": "cloud-fedora-v3", "flavor": "m1.medium", "?": { "type": "io.murano.resources.Linux", "id": "ef984a74-29a4-45c0-b1dc-2ab9f075732e" } }, "name": "orion", "port": "8080", "?": { "type": "io.murano.apps.apache.Tomcat", "id": "54cea43d-5970-4c73-b9ac-fea656f3c722" } } ] expected['services'] = services body = { "name": "env_template_name", "services": [ { "instance": { "assignFloatingIp": "true", "keyname": "mykeyname", "image": "cloud-fedora-v3", "flavor": "m1.medium", "?": { "type": "io.murano.resources.Linux", "id": "ef984a74-29a4-45c0-b1dc-2ab9f075732e" } }, "name": "orion", "port": "8080", "?": { "type": "io.murano.apps.apache.Tomcat", "id": "54cea43d-5970-4c73-b9ac-fea656f3c722" } } ] } req = self._post('/templates', jsonutils.dump_as_bytes(body)) result = req.get_response(self.api) self.assertEqual(expected, jsonutils.loads(result.body)) # Reset the policy expectation self.expect_policy_check('list_env_templates') req = self._get('/templates') result = req.get_response(self.api) del expected['services'] self.assertEqual(200, result.status_code) self.assertEqual({'templates': [expected]}, jsonutils.loads(result.body)) # Reset the policy expectation self.expect_policy_check('show_env_template', {'env_template_id': self.uuids[0]}) expected['services'] = services req = self._get('/templates/%s' % self.uuids[0]) result = req.get_response(self.api) self.assertEqual(expected, jsonutils.loads(result.body)) def test_add_application_to_template(self): """Create an template, test template.show().""" self._set_policy_rules( {'create_env_template': '@', 'add_application': '@'} ) self.expect_policy_check('create_env_template') fake_now = timeutils.utcnow() timeutils.utcnow.override_time = fake_now services = [ { "instance": { "assignFloatingIp": "true", "keyname": "mykeyname", "image": "cloud-fedora-v3", "flavor": "m1.medium", "?": { "type": "io.murano.resources.Linux", "id": "ef984a74-29a4-45c0-b1dc-2ab9f075732e" } }, "name": "orion", "port": "8080", "?": { "type": "io.murano.apps.apache.Tomcat", "id": "54cea43d-5970-4c73-b9ac-fea656f3c722" } } ] body = { "name": "template_name", } req = self._post('/templates', jsonutils.dump_as_bytes(body)) result = req.get_response(self.api) self.assertEqual(200, result.status_code) body = services req = self._post('/templates/%s/services' % self.uuids[0], jsonutils.dump_as_bytes(body)) result = req.get_response(self.api) self.assertEqual(200, result.status_code) self.assertEqual(services, jsonutils.loads(result.body)) req = self._get('/templates/%s/services' % self.uuids[0]) result = req.get_response(self.api) self.assertEqual(200, result.status_code) self.assertEqual(1, len(jsonutils.loads(result.body))) service_no_instance = [ { "instance": "ef984a74-29a4-45c0-b1dc-2ab9f075732e", "name": "tomcat", "port": "8080", "?": { "type": "io.murano.apps.apache.Tomcat", "id": "54cea43d-5970-4c73-b9ac-fea656f3c722" } } ] req = self._post('/templates/%s/services' % self.uuids[0], jsonutils.dump_as_bytes(service_no_instance)) result = req.get_response(self.api) self.assertEqual(200, result.status_code) req = self._get('/templates/%s/services' % self.uuids[0]) result = req.get_response(self.api) self.assertEqual(200, result.status_code) self.assertEqual(2, len(jsonutils.loads(result.body))) def test_delete_application_in_template(self): """Create an template, test template.show().""" self._set_policy_rules( {'create_env_template': '@', 'delete_env_application': '@'} ) self.expect_policy_check('create_env_template') fake_now = timeutils.utcnow() timeutils.utcnow.override_time = fake_now body = { "name": "mytemplate", "services": [ { "name": "tomcat", "port": "8080", "?": { "type": "io.murano.apps.apache.Tomcat", "id": "54cea43d-5970-4c73-b9ac-fea656f3c722" } } ] } req = self._post('/templates', jsonutils.dump_as_bytes(body)) result = req.get_response(self.api) self.assertEqual(200, result.status_code) self.assertEqual(1, len(jsonutils.loads(result.body)['services'])) req = self._get('/templates/%s/services' % self.uuids[0]) result = req.get_response(self.api) self.assertEqual(200, result.status_code) self.assertEqual(1, len(jsonutils.loads(result.body))) service_id = '54cea43d-5970-4c73-b9ac-fea656f3c722' req = self._get('/templates/' + self.uuids[0] + '/services/' + service_id) result = req.get_response(self.api) self.assertEqual(200, result.status_code) req = self._delete('/templates/' + self.uuids[0] + '/services/' + service_id) result = req.get_response(self.api) self.assertEqual(200, result.status_code) self.assertEqual(0, len(jsonutils.loads(result.body)['services'])) req = self._get('/templates/' + self.uuids[0] + '/services/' + service_id) result = req.get_response(self.api) self.assertEqual(404, result.status_code) def test_create_environment(self): """Test that environment is created, session configured.""" self.fixture = self.useFixture(config_fixture.Config()) self.fixture.conf(args=[]) self._set_policy_rules( {'create_env_template': '@', 'create_environment': '@'} ) self._create_env_template_no_service() body_env = {'name': 'my_template'} self.expect_policy_check('create_environment', {'env_template_id': self.uuids[0]}) req = self._post('/templates/%s/create-environment' % self.uuids[0], jsonutils.dump_as_bytes(body_env)) session_result = req.get_response(self.api) self.assertEqual(200, session_result.status_code) self.assertIsNotNone(session_result) body_returned = jsonutils.loads(session_result.body) self.assertEqual(self.uuids[4], body_returned['session_id']) self.assertEqual(self.uuids[3], body_returned['environment_id']) @mock.patch('murano.db.services.environments.EnvironmentServices.create') def test_create_environment_handle_duplicate_exc(self, mock_function): """Test that duplicate entry exception is correctly handled.""" mock_function.side_effect = db_exc.DBDuplicateEntry self.fixture = self.useFixture(config_fixture.Config()) self.fixture.conf(args=[]) self._set_policy_rules( {'create_env_template': '@', 'create_environment': '@'} ) self._create_env_template_no_service() body_env = {'name': 'my_template'} self.expect_policy_check('create_environment', {'env_template_id': self.uuids[0]}) req = self._post('/templates/%s/create-environment' % self.uuids[0], jsonutils.dump_as_bytes(body_env)) session_result = req.get_response(self.api) self.assertEqual(409, session_result.status_code) def test_create_env_with_template_and_services(self): """Test env and session creation with services Test that environment is created and session with template with services. """ self.fixture = self.useFixture(config_fixture.Config()) self.fixture.conf(args=[]) self._set_policy_rules( {'create_env_template': '@', 'create_environment': '@'} ) self._create_env_template_services() self.expect_policy_check('create_environment', {'env_template_id': self.uuids[0]}) body = {'name': 'my_template'} req = self._post('/templates/%s/create-environment' % self.uuids[0], jsonutils.dump_as_bytes(body)) result = req.get_response(self.api) self.assertIsNotNone(result) self.assertEqual(200, result.status_code) body_returned = jsonutils.loads(result.body) self.assertEqual(self.uuids[4], body_returned['session_id']) self.assertEqual(self.uuids[3], body_returned['environment_id']) def test_create_env_with_template_no_services(self): """Test env and session creation without services Test that environment is created and session with template without services. """ self.fixture = self.useFixture(config_fixture.Config()) self.fixture.conf(args=[]) self._set_policy_rules( {'create_env_template': '@', 'create_environment': '@'} ) self._create_env_template_no_service() self.expect_policy_check('create_environment', {'env_template_id': self.uuids[0]}) body = {'name': 'my_template'} req = self._post('/templates/%s/create-environment' % self.uuids[0], jsonutils.dump_as_bytes(body)) result = req.get_response(self.api) self.assertIsNotNone(result) self.assertEqual(200, result.status_code) body_returned = jsonutils.loads(result.body) self.assertEqual(self.uuids[4], body_returned['session_id']) self.assertEqual(self.uuids[3], body_returned['environment_id']) def test_update_service_in_template(self): """Test the service is updated in the environment template""" self.fixture = self.useFixture(config_fixture.Config()) self.fixture.conf(args=[]) self._set_policy_rules( {'create_env_template': '@', 'update_service_env_template': '@'} ) updated_env = "UPDATED_ENV" env_template = self._create_env_template_services() self.expect_policy_check('update_service_env_template') env_template["name"] = updated_env req = self._put('/templates/{0}/services/{1}'. format(self.uuids[0], "service_id"), jsonutils.dump_as_bytes(env_template)) result = req.get_response(self.api) self.assertIsNotNone(result) self.assertEqual(200, result.status_code) body_returned = jsonutils.loads(result.body) self.assertEqual(updated_env, body_returned['name']) def test_mallformed_env_body(self): """Check that an illegal temp name results in an HTTPClientError.""" self._set_policy_rules( {'create_env_template': '@', 'create_environment': '@'} ) self._create_env_template_no_service() self.expect_policy_check('create_environment', {'env_template_id': self.uuids[0]}) body = {'invalid': 'test'} req = self._post('/templates/%s/create-environment' % self.uuids[0], jsonutils.dump_as_bytes(body)) result = req.get_response(self.api) self.assertEqual(400, result.status_code) def test_delete_notexisting_service(self): """Check deleting a not existing service, return a 404 error.""" self._set_policy_rules( {'create_env_template': '@', 'delete_env_application': '@'} ) self.expect_policy_check('create_env_template') fake_now = timeutils.utcnow() timeutils.utcnow.override_time = fake_now body = { "name": "mytemplate", "services": [ { "name": "tomcat", "port": "8080", "?": { "type": "io.murano.apps.apache.Tomcat", "id": "ID1" } } ] } req = self._post('/templates', jsonutils.dump_as_bytes(body)) result = req.get_response(self.api) self.assertEqual(200, result.status_code) self.assertEqual(1, len(jsonutils.loads(result.body)['services'])) req = self._delete('/templates/{0}/services/{1}'.format(self.uuids[0], "NO_EXIST")) result = req.get_response(self.api) self.assertEqual(404, result.status_code) @mock.patch('murano.db.services.environment_templates.EnvTemplateServices.' 'get_env_template') def test_validate_request_handle_forbidden_exc(self, mock_function): """Test whether forbidden exception is thrown with different tenant.""" self._set_policy_rules( {'create_env_template': '@', 'show_env_template': '@', 'show_env_template': '@'} ) # If is_public is False, then exception should be thrown. mock_env_template = mock.MagicMock(is_public=False, tenant_id=-1) mock_function.return_value = mock_env_template self._create_env_template_no_service() self.expect_policy_check('show_env_template', {'env_template_id': self.uuids[0]}) req = self._get('/templates/%s' % self.uuids[0]) result = req.get_response(self.api) self.assertEqual(result.status_code, 403) # If is_public is True, then no exception should be thrown. mock_env_template = mock.MagicMock(is_public=True, tenant_id=-1) mock_function.return_value = mock_env_template self.expect_policy_check('show_env_template', {'env_template_id': self.uuids[0]}) req = self._get('/templates/%s' % self.uuids[0]) result = req.get_response(self.api) self.assertEqual(result.status_code, 200) def test_create_env_notexisting_templatebody(self): """Check that an illegal temp name results in an HTTPClientError.""" self._set_policy_rules( {'create_environment': '@'} ) env_template_id = 'noexisting' self.expect_policy_check('create_environment', {'env_template_id': env_template_id}) body = {'name': 'test'} req = self._post('/templates/%s/create-environment' % env_template_id, jsonutils.dump_as_bytes(body)) result = req.get_response(self.api) self.assertEqual(404, result.status_code) def _create_env_template_no_service(self): self.expect_policy_check('create_env_template') fake_now = timeutils.utcnow() timeutils.utcnow.override_time = fake_now req = self._post('/templates', jsonutils.dump_as_bytes({'name': 'name'})) result = req.get_response(self.api) self.assertEqual(200, result.status_code) def _create_env_template_services(self): fake_now = timeutils.utcnow() timeutils.utcnow.override_time = fake_now self.expect_policy_check('create_env_template') fake_now = timeutils.utcnow() timeutils.utcnow.override_time = fake_now body = { "name": "env_template_name", "services": [ { "instance": { "assignFloatingIp": "true", "keyname": "mykeyname", "image": "cloud-fedora-v3", "flavor": "m1.medium", "?": { "type": "io.murano.resources.Linux", "id": "ef984a74-29a4-45c0-b1dc-2ab9f075732e" } }, "name": "orion", "port": "8080", "?": { "type": "io.murano.apps.apache.Tomcat", "id": "service_id" } } ] } req = self._post('/templates', jsonutils.dump_as_bytes(body)) result = req.get_response(self.api) return result.json ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/api/v1/test_environments.py0000664000175000017500000007650500000000000024020 0ustar00zuulzuul00000000000000# # Copyright (c) 2014 Hewlett-Packard Development Company, L.P. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT 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 datetime import datetime from oslo_config import fixture as config_fixture from oslo_serialization import jsonutils from oslo_utils import timeutils from murano.api.v1 import environments from murano.api.v1 import sessions from murano.db import models from murano.services import states import murano.tests.unit.api.base as tb import murano.tests.unit.utils as test_utils class TestEnvironmentApi(tb.ControllerTest, tb.MuranoApiTestCase): def setUp(self): super(TestEnvironmentApi, self).setUp() self.controller = environments.Controller() self.sessions_controller = sessions.Controller() self.fixture = self.useFixture(config_fixture.Config()) self.fixture.conf(args=[]) def test_list_empty_environments(self): """Check that with no environments an empty list is returned.""" self._set_policy_rules( {'list_environments': '@'} ) self.expect_policy_check('list_environments') req = self._get('/environments') result = req.get_response(self.api) self.assertEqual({'environments': []}, jsonutils.loads(result.body)) def test_list_all_tenants(self): """Check whether all_tenants param is taken into account.""" self._set_policy_rules( {'list_environments': '@', 'create_environment': '@', 'list_environments_all_tenants': '@'} ) self.expect_policy_check('create_environment') body = {'name': 'my_env'} req = self._post('/environments', jsonutils.dump_as_bytes(body), tenant="other") req.get_response(self.api) self._check_listing(False, None, 'list_environments', 0) self._check_listing(True, None, 'list_environments_all_tenants', 1) def test_list_given_tenant(self): self._set_policy_rules( {'list_environments': '@', 'create_environment': '@', 'list_environments_all_tenants': '@'} ) self.expect_policy_check('create_environment') self.expect_policy_check('create_environment') self.expect_policy_check('create_environment') body = {'name': 'my_env1'} req = self._post('/environments', jsonutils.dump_as_bytes(body), tenant="foo") req.get_response(self.api) body = {'name': 'my_env2'} req = self._post('/environments', jsonutils.dump_as_bytes(body), tenant="bar") req.get_response(self.api) body = {'name': 'my_env3'} req = self._post('/environments', jsonutils.dump_as_bytes(body), tenant="bar") req.get_response(self.api) self._check_listing(False, "foo", 'list_environments_all_tenants', 1) self._check_listing(False, "bar", 'list_environments_all_tenants', 2) self._check_listing(False, "other", 'list_environments_all_tenants', 0) def _check_listing(self, all_tenants, tenant, expected_check, expected_count): self.expect_policy_check(expected_check) params = {'all_tenants': all_tenants} if tenant: params['tenant'] = tenant req = self._get('/environments', params) response = req.get_response(self.api) body = jsonutils.loads(response.body) self.assertEqual(200, response.status_code) self.assertEqual(expected_count, len(body['environments'])) def test_create_environment(self): """Create an environment, test environment.show().""" self._set_policy_rules( {'list_environments': '@', 'create_environment': '@', 'show_environment': '@'} ) self.expect_policy_check('create_environment') fake_now = timeutils.utcnow() timeutils.utcnow.override_time = fake_now uuids = ('env_object_id', 'network_id', 'environment_id') mock_uuid = self._stub_uuid(uuids) expected = {'tenant_id': self.tenant, 'id': 'environment_id', 'name': 'my_env', 'description_text': 'description', 'version': 0, 'created': datetime.isoformat(fake_now)[:-7], 'updated': datetime.isoformat(fake_now)[:-7], } body = {'name': 'my_env', 'description_text': 'description'} req = self._post('/environments', jsonutils.dump_as_bytes(body)) result = req.get_response(self.api) self.assertEqual(expected, jsonutils.loads(result.body)) expected['status'] = 'ready' # Reset the policy expectation self.expect_policy_check('list_environments') req = self._get('/environments') result = req.get_response(self.api) self.assertEqual(200, result.status_code) self.assertEqual({'environments': [expected]}, jsonutils.loads(result.body)) expected['services'] = [] expected['acquired_by'] = None # Reset the policy expectation self.expect_policy_check('show_environment', {'environment_id': uuids[-1]}) req = self._get('/environments/%s' % uuids[-1]) result = req.get_response(self.api) self.assertEqual(expected, jsonutils.loads(result.body)) self.assertEqual(3, mock_uuid.call_count) def test_illegal_environment_name_create(self): """Check that an illegal env name results in an HTTPClientError.""" self._set_policy_rules( {'list_environments': '@', 'create_environment': '@', 'show_environment': '@'} ) self.expect_policy_check('create_environment') body = {'name': ' '} req = self._post('/environments', jsonutils.dump_as_bytes(body)) result = req.get_response(self.api) self.assertEqual(400, result.status_code) def test_unicode_environment_name_create(self): """Check that an unicode env name doesn't raise an HTTPClientError.""" self._set_policy_rules( {'list_environments': '@', 'create_environment': '@', 'show_environment': '@'} ) self.expect_policy_check('create_environment') body = {'name': u'$yaql \u2665 unicode'} req = self._post('/environments', jsonutils.dump_as_bytes(body)) result = req.get_response(self.api) self.assertEqual(200, result.status_code) def test_no_environment_name_create(self): """Check that no env name provided results in an HTTPBadResquest.""" self._set_policy_rules( {'list_environments': '@', 'create_environment': '@', 'show_environment': '@'} ) self.expect_policy_check('create_environment') body = {'no_name': 'fake'} req = self._post('/environments', jsonutils.dump_as_bytes(body)) result = req.get_response(self.api) self.assertEqual(400, result.status_code) result_msg = result.text.replace('\n', '') self.assertIn('Please, specify a name of the environment to create', result_msg) def test_too_long_environment_name_create(self): """Check that a too long env name results in an HTTPBadResquest.""" self._set_policy_rules( {'list_environments': '@', 'create_environment': '@', 'show_environment': '@'} ) self.expect_policy_check('create_environment') body = {'name': 'a' * 256} req = self._post('/environments', jsonutils.dump_as_bytes(body)) result = req.get_response(self.api) self.assertEqual(400, result.status_code) result_msg = result.text.replace('\n', '') self.assertIn('Environment name should be 255 characters maximum', result_msg) def test_create_environment_with_empty_body(self): """Check that empty request body results in an HTTPBadResquest.""" body = b'' req = self._post('/environments', body) result = req.get_response(self.api) self.assertEqual(400, result.status_code) result_msg = result.text.replace('\n', '') self.assertIn('The server could not comply with the request since it ' 'is either malformed or otherwise incorrect.', result_msg) def test_create_environment_duplicate_name(self): """Check that duplicate names results in HTTPConflict""" self._set_policy_rules( {'list_environments': '@', 'create_environment': '@', 'show_environment': '@'} ) self.expect_policy_check('create_environment') body = {'name': u'my_env_dup'} req = self._post('/environments', jsonutils.dump_as_bytes(body)) result = req.get_response(self.api) self.assertEqual(200, result.status_code) self.expect_policy_check('create_environment') body = {'name': u'my_env_dup'} req = self._post('/environments', jsonutils.dump_as_bytes(body)) result = req.get_response(self.api) self.assertEqual(409, result.status_code) result_msg = result.text.replace('\n', '') self.assertIn('Environment with specified name already exists', result_msg) def test_missing_environment(self): """Check that a missing environment results in an HTTPNotFound. Environment check will be made in the decorator and raises, no need to check policy in this testcase. """ req = self._get('/environments/no-such-id') result = req.get_response(self.api) self.assertEqual(404, result.status_code) def test_update_environment(self): """Check that environment rename works.""" self._set_policy_rules( {'show_environment': '@', 'update_environment': '@'} ) self.expect_policy_check('update_environment', {'environment_id': '12345'}) fake_now = timeutils.utcnow() timeutils.utcnow.override_time = fake_now expected = dict( id='12345', name='my-env', version=0, description_text='', created=fake_now, updated=fake_now, tenant_id=self.tenant, description={ 'Objects': { '?': {'id': '12345'} }, 'Attributes': [] } ) e = models.Environment(**expected) test_utils.save_models(e) fake_now = timeutils.utcnow() timeutils.utcnow.override_time = fake_now del expected['description'] expected['services'] = [] expected['status'] = 'ready' expected['name'] = 'renamed_env' expected['updated'] = fake_now body = { 'name': 'renamed_env' } req = self._put('/environments/12345', jsonutils.dump_as_bytes(body)) result = req.get_response(self.api) self.assertEqual(200, result.status_code) self.expect_policy_check('show_environment', {'environment_id': '12345'}) req = self._get('/environments/12345') result = req.get_response(self.api) self.assertEqual(200, result.status_code) expected['created'] = datetime.isoformat(expected['created'])[:-7] expected['updated'] = datetime.isoformat(expected['updated'])[:-7] expected['acquired_by'] = None self.assertEqual(expected, jsonutils.loads(result.body)) def test_update_environment_with_invalid_name(self): """Test that invalid env name returns HTTPBadRequest Check that update an invalid env name results in an HTTPBadRequest. """ self._set_policy_rules( {'update_environment': '@'} ) self._create_fake_environment('env1', '111') self.expect_policy_check('update_environment', {'environment_id': '111'}) body = { 'name': ' ' } req = self._put('/environments/111', jsonutils.dump_as_bytes(body)) result = req.get_response(self.api) self.assertEqual(400, result.status_code) result_msg = result.text.replace('\n', '') msg = ('Environment name must contain at least one ' 'non-white space symbol') self.assertIn(msg, result_msg) def test_update_environment_with_existing_name(self): self._set_policy_rules( {'update_environment': '@'} ) self._create_fake_environment('env1', '111') self._create_fake_environment('env2', '222') self.expect_policy_check('update_environment', {'environment_id': '111'}) body = { 'name': 'env2' } req = self._put('/environments/111', jsonutils.dump_as_bytes(body)) result = req.get_response(self.api) self.assertEqual(409, result.status_code) def test_too_long_environment_name_update(self): """Test updating too long env name Check that update a too long env name results in an HTTPBadResquest. """ self._set_policy_rules( {'update_environment': '@'} ) self._create_fake_environment('env1', '111') self.expect_policy_check('update_environment', {'environment_id': '111'}) new_name = 'env1' * 64 body = { 'name': new_name } req = self._put('/environments/111', jsonutils.dump_as_bytes(body)) result = req.get_response(self.api) self.assertEqual(400, result.status_code) result_msg = result.text.replace('\n', '') self.assertIn('Environment name should be 255 characters maximum', result_msg) def test_delete_environment(self): """Test that environment deletion results in the correct rpc call.""" result = self._test_delete_or_abandon(abandon=False) self.assertEqual(b'', result.body) self.assertEqual(200, result.status_code) def test_abandon_environment(self): """Check that abandon feature works""" result = self._test_delete_or_abandon(abandon=True) self.assertEqual(b'', result.body) self.assertEqual(200, result.status_code) def test_abandon_environment_of_different_tenant(self): """Test abandon environment belongs to another tenant.""" result = self._test_delete_or_abandon(abandon=True, tenant='not_match') self.assertEqual(403, result.status_code) self.assertIn((b'User is not authorized to access these' b' tenant resources'), result.body) def test_get_last_status_of_different_tenant(self): """Test get last services status of env belongs to another tenant.""" self._create_fake_environment('env1', '111') req = self._get('/environments/111/lastStatus', tenant='not_match') result = req.get_response(self.api) self.assertEqual(403, result.status_code) self.assertIn((b'User is not authorized to access these' b' tenant resources'), result.body) def test_get_environment(self): """Test GET request of an environment in ready status""" self._set_policy_rules( {'show_environment': '@'} ) self.expect_policy_check('show_environment', {'environment_id': '123'}) fake_now = timeutils.utcnow() timeutils.utcnow.override_time = fake_now env_id = '123' self._create_fake_environment(env_id=env_id) req = self._get('/environments/{0}'.format(env_id)) result = req.get_response(self.api) self.assertEqual(200, result.status_code) expected = {'tenant_id': self.tenant, 'id': env_id, 'name': 'my-env', 'version': 0, 'description_text': '', 'created': datetime.isoformat(fake_now)[:-7], 'updated': datetime.isoformat(fake_now)[:-7], 'acquired_by': None, 'services': [], 'status': 'ready', } self.assertEqual(expected, jsonutils.loads(result.body)) def test_get_environment_acquired(self): """Test GET request of an environment in deploying status""" self._set_policy_rules( {'show_environment': '@'} ) self.expect_policy_check('show_environment', {'environment_id': '1234'}) fake_now = timeutils.utcnow() timeutils.utcnow.override_time = fake_now env_id = '1234' self._create_fake_environment(env_id=env_id) sess_id = '321' expected = dict( id=sess_id, environment_id=env_id, version=0, state=states.SessionState.DEPLOYING, user_id=self.tenant, description={ 'Objects': { '?': {'id': '{0}'.format(env_id)} }, 'Attributes': {} } ) s = models.Session(**expected) test_utils.save_models(s) req = self._get('/environments/{0}'.format(env_id)) result = req.get_response(self.api) self.assertEqual(200, result.status_code) expected = {'tenant_id': self.tenant, 'id': env_id, 'name': 'my-env', 'version': 0, 'description_text': '', 'created': datetime.isoformat(fake_now)[:-7], 'updated': datetime.isoformat(fake_now)[:-7], 'acquired_by': sess_id, 'services': [], 'status': states.EnvironmentStatus.DEPLOYING, } self.assertEqual(expected, jsonutils.loads(result.body)) def _create_fake_environment(self, env_name='my-env', env_id='123'): fake_now = timeutils.utcnow() expected = dict( id=env_id, name=env_name, version=0, created=fake_now, updated=fake_now, tenant_id=self.tenant, description={ 'Objects': { '?': {'id': '{0}'.format(env_id)} }, 'Attributes': {} } ) e = models.Environment(**expected) test_utils.save_models(e) def _test_delete_or_abandon(self, abandon, env_name='my-env', env_id='123', tenant=None): self._set_policy_rules( {'delete_environment': '@'} ) self.expect_policy_check( 'delete_environment', {'environment_id': '{0}'.format(env_id)} ) self._create_fake_environment(env_name, env_id) path = '/environments/{0}'.format(env_id) req = self._delete(path, params={'abandon': abandon}, tenant=tenant or self.tenant) result = req.get_response(self.api) return result def test_last_status_session(self): CREDENTIALS = {'tenant': 'test_tenant_1', 'user': 'test_user_1'} self._set_policy_rules({'create_environment': '@'}) self.expect_policy_check('create_environment') # Create environment request = self._post( '/environments', jsonutils.dump_as_bytes({'name': 'test_environment_1'}), **CREDENTIALS ) response_body = jsonutils.loads(request.get_response(self.api).body) self.assertEqual(CREDENTIALS['tenant'], response_body['tenant_id']) ENVIRONMENT_ID = response_body['id'] # Create session request = self._post( '/environments/{environment_id}/configure' .format(environment_id=ENVIRONMENT_ID), b'', **CREDENTIALS ) response_body = jsonutils.loads(request.get_response(self.api).body) SESSION_ID = response_body['id'] # Test getting last status doesn't error request = self._get( '/environments/{environment_id}/lastStatus' .format(environment_id=ENVIRONMENT_ID), **CREDENTIALS ) request.headers['X-Configuration-Session'] = str(SESSION_ID) request.context.session = SESSION_ID response_body = jsonutils.loads(request.get_response(self.api).body) self.assertIsNotNone(response_body) def test_show_environments_session(self): CREDENTIALS = {'tenant': 'test_tenant_1', 'user': 'test_user_1'} self._set_policy_rules( {'create_environment': '@', 'list_environments': '@', 'show_environment': '@'} ) self.expect_policy_check('create_environment') # Create environment request = self._post( '/environments', jsonutils.dump_as_bytes({'name': 'test_environment_1'}), **CREDENTIALS ) response_body = jsonutils.loads(request.get_response(self.api).body) self.assertEqual(CREDENTIALS['tenant'], response_body['tenant_id']) ENVIRONMENT_ID = response_body['id'] # Create session self.expect_policy_check( 'show_environment', {'environment_id': ENVIRONMENT_ID}) request = self._post( '/environments/{environment_id}/configure' .format(environment_id=ENVIRONMENT_ID), b'', **CREDENTIALS ) response_body = jsonutils.loads(request.get_response(self.api).body) SESSION_ID = response_body['id'] # Show the environment and test that it is correct. request = self._get( '/environments/{environment_id}' .format(environment_id=ENVIRONMENT_ID), **CREDENTIALS ) request.headers['X-Configuration-Session'] = str(SESSION_ID) request.context.session = SESSION_ID response_body = jsonutils.loads(request.get_response(self.api).body) self.assertEqual(ENVIRONMENT_ID, response_body['id']) def _create_env_and_session(self): creds = {'tenant': 'test_tenant', 'user': 'test_user'} self._set_policy_rules( {'show_environment': '@', 'update_environment': '@'} ) env_id = '123' self._create_fake_environment(env_id=env_id) # Create session request = self._post('/environments/{environment_id}/configure' .format(environment_id=env_id), b'', **creds) response_body = jsonutils.loads(request.get_response(self.api).body) session_id = response_body['id'] return env_id, session_id def test_get_and_update_environment_model(self): """Test GET and PATCH requests of an environment object model""" env_id, session_id = self._create_env_and_session() # Get entire env's model self.expect_policy_check('show_environment', {'environment_id': '123'}) req = self._get('/environments/{0}/model/'.format(env_id)) result = req.get_response(self.api) self.assertEqual(200, result.status_code) expected = {'?': {'id': '{0}'.format(env_id)}} self.assertEqual(expected, jsonutils.loads(result.body)) # Add some data to the '?' section of env's model self.expect_policy_check('update_environment', {'environment_id': '123'}) data = [{ "op": "add", "path": "/?/name", "value": 'my_env' }] expected = { 'id': '{0}'.format(env_id), 'name': 'my_env' } req = self._patch('/environments/{0}/model/'.format(env_id), jsonutils.dump_as_bytes(data), content_type='application/env-model-json-patch') req.headers['X-Configuration-Session'] = str(session_id) req.context.session = session_id result = req.get_response(self.api) self.assertEqual(200, result.status_code) observed = jsonutils.loads(result.body)['?'] self.assertEqual(expected, observed) # Check that changes are stored in session self.expect_policy_check('show_environment', {'environment_id': '123'}) req = self._get('/environments/{0}/model/{1}'.format( env_id, '/?')) req.headers['X-Configuration-Session'] = str(session_id) req.context.session = session_id result = req.get_response(self.api) self.assertEqual(200, result.status_code) self.assertEqual(expected, jsonutils.loads(result.body)) # Check that actual model remains unchanged self.expect_policy_check('show_environment', {'environment_id': '123'}) req = self._get('/environments/{0}/model/{1}'.format( env_id, '/?')) result = req.get_response(self.api) self.assertEqual(200, result.status_code) expected = {'id': '{0}'.format(env_id)} self.assertEqual(expected, jsonutils.loads(result.body)) def test_get_environment_model_non_existing_path(self): env_id, session_id = self._create_env_and_session() # Try to get non-existing section of env's model self.expect_policy_check('show_environment', {'environment_id': '123'}) path = 'foo/bar' req = self._get('/environments/{0}/model/{1}'.format( env_id, path)) result = req.get_response(self.api) self.assertEqual(404, result.status_code) def test_update_environment_model_empty_body(self): env_id, session_id = self._create_env_and_session() data = None req = self._patch('/environments/{0}/model/'.format(env_id), jsonutils.dump_as_bytes(data), content_type='application/env-model-json-patch') req.headers['X-Configuration-Session'] = str(session_id) req.context.session = session_id result = req.get_response(self.api) self.assertEqual(400, result.status_code) result_msg = result.text.replace('\n', '') msg = "JSON-patch must be a list." self.assertIn(msg, result_msg) def test_update_environment_model_no_patch(self): env_id, session_id = self._create_env_and_session() data = ["foo"] req = self._patch('/environments/{0}/model/'.format(env_id), jsonutils.dump_as_bytes(data), content_type='application/env-model-json-patch') req.headers['X-Configuration-Session'] = str(session_id) req.context.session = session_id result = req.get_response(self.api) self.assertEqual(400, result.status_code) result_msg = result.text.replace('\n', '') msg = "Operations must be JSON objects." self.assertIn(msg, result_msg) def test_update_environment_model_no_op(self): env_id, session_id = self._create_env_and_session() data = [{ "path": "/?/name", "value": 'my_env' }] req = self._patch('/environments/{0}/model/'.format(env_id), jsonutils.dump_as_bytes(data), content_type='application/env-model-json-patch') req.headers['X-Configuration-Session'] = str(session_id) req.context.session = session_id result = req.get_response(self.api) self.assertEqual(400, result.status_code) result_msg = result.text.replace('\n', '') msg = "Unable to find 'op' in JSON Schema change" self.assertIn(msg, result_msg) def test_update_environment_model_no_path(self): env_id, session_id = self._create_env_and_session() data = [{ "op": "add", "value": 'my_env' }] req = self._patch('/environments/{0}/model/'.format(env_id), jsonutils.dump_as_bytes(data), content_type='application/env-model-json-patch') req.headers['X-Configuration-Session'] = str(session_id) req.context.session = session_id result = req.get_response(self.api) self.assertEqual(400, result.status_code) result_msg = result.text.replace('\n', '') msg = "Unable to find 'path' in JSON Schema change" self.assertIn(msg, result_msg) def test_update_environment_model_no_value(self): env_id, session_id = self._create_env_and_session() data = [{ "op": "add", "path": "/?/name" }] req = self._patch('/environments/{0}/model/'.format(env_id), jsonutils.dump_as_bytes(data), content_type='application/env-model-json-patch') req.headers['X-Configuration-Session'] = str(session_id) req.context.session = session_id result = req.get_response(self.api) self.assertEqual(400, result.status_code) result_msg = result.text.replace('\n', '') msg = 'Operation "add" requires a member named "value".' self.assertIn(msg, result_msg) def test_update_environment_model_forbidden_operation(self): env_id, session_id = self._create_env_and_session() data = [{ "op": "add", "path": "/?/id", "value": "foo" }] req = self._patch('/environments/{0}/model/'.format(env_id), jsonutils.dump_as_bytes(data), content_type='application/env-model-json-patch') req.headers['X-Configuration-Session'] = str(session_id) req.context.session = session_id result = req.get_response(self.api) self.assertEqual(403, result.status_code) result_msg = result.text.replace('\n', '') msg = ("Method 'add' is not allowed for a path with name '?/id'. " "Allowed operations are: no operations") self.assertIn(msg, result_msg) def test_update_environment_model_invalid_schema(self): env_id, session_id = self._create_env_and_session() data = [{ "op": "add", "path": "/?/name", "value": 111 }] req = self._patch('/environments/{0}/model/'.format(env_id), jsonutils.dump_as_bytes(data), content_type='application/env-model-json-patch') req.headers['X-Configuration-Session'] = str(session_id) req.context.session = session_id result = req.get_response(self.api) self.assertEqual(400, result.status_code) result_msg = result.text.replace('\n', '') msg = "111 is not of type 'string'" self.assertIn(msg, result_msg) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/api/v1/test_instance_statistics.py0000664000175000017500000000464200000000000025340 0ustar00zuulzuul00000000000000# 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. from murano.api.v1 import instance_statistics import murano.tests.unit.api.base as tb class TestInstanceStatistics(tb.ControllerTest, tb.MuranoApiTestCase): def setUp(self): super(TestInstanceStatistics, self).setUp() self.controller = instance_statistics.Controller() def test_get_aggregated(self): self._set_policy_rules( {"get_aggregated_statistics": "@"} ) self.expect_policy_check("get_aggregated_statistics", {'environment_id': u'12344'}) env_id = 12344 req = self._get('/environments/{env_id}/' 'instance-statistics/aggregated' .format(env_id=env_id)) result = req.get_response(self.api) self.assertEqual(200, result.status_code) def test_get_for_instance(self): self._set_policy_rules( {"get_instance_statistics": "@"} ) self.expect_policy_check("get_instance_statistics", {'environment_id': u'12345', 'instance_id': u'12'}) env_id = 12345 ins_id = 12 req = self._get('/environments/{env_id}/' 'instance-statistics/raw/{ins_id}' .format(env_id=env_id, ins_id=ins_id)) result = req.get_response(self.api) self.assertEqual(200, result.status_code) def test_get_for_env(self): self._set_policy_rules( {"get_statistics": "@"} ) self.expect_policy_check("get_statistics", {'environment_id': u'12346'}) env_id = 12346 req = self._get('/environments/{env_id}/instance-statistics/raw' .format(env_id=env_id)) result = req.get_response(self.api) self.assertEqual(200, result.status_code) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/api/v1/test_schemas.py0000664000175000017500000000604600000000000022705 0ustar00zuulzuul00000000000000# 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 unittest import mock from oslo_messaging.rpc import client from webob import exc from murano.api.v1 import schemas import murano.tests.unit.base as test_base from murano.tests.unit import utils as test_utils @mock.patch('murano.api.v1.schemas.policy') @mock.patch('murano.api.v1.schemas.request_statistics.update_error_count') @mock.patch('murano.api.v1.schemas.request_statistics.update_count') class TestSchemas(test_base.MuranoTestCase): @classmethod def setUpClass(cls): super(TestSchemas, cls).setUpClass() cls.controller = schemas.Controller() @mock.patch('murano.api.v1.schemas.rpc') def test_get_schema(self, mock_rpc, *args): dummy_context = test_utils.dummy_context() dummy_context.GET = { 'classVersion': 'test_class_version', 'packageName': 'test_package_name' } mock_request = mock.MagicMock(context=dummy_context) mock_rpc.engine().generate_schema.return_value = 'test_schema' result = self.controller.get_schema(mock_request, 'test_class') self.assertEqual('test_schema', result) @mock.patch('murano.api.v1.schemas.rpc') def test_get_schema_negative(self, mock_rpc, *args): dummy_context = test_utils.dummy_context() dummy_context.GET = { 'classVersion': 'test_class_version', 'packageName': 'test_package_name' } mock_request = mock.MagicMock(context=dummy_context) # Test exception handling for pre-defined exception types. exc_types = ('NoClassFound', 'NoPackageForClassFound', 'NoPackageFound') for exc_type in exc_types: dummy_error = client.RemoteError(exc_type=exc_type, value='dummy_value') mock_rpc.engine().generate_schema.side_effect = dummy_error with self.assertRaisesRegex(exc.HTTPNotFound, dummy_error.value): self.controller.get_schema(mock_request, 'test_class') # Test exception handling for miscellaneous exception type. dummy_error = client.RemoteError(exc_type='TestExcType', value='dummy_value') mock_rpc.engine().generate_schema.side_effect = dummy_error with self.assertRaisesRegex(client.RemoteError, dummy_error.value): self.controller.get_schema(mock_request, 'test_class') ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/api/v1/test_services.py0000664000175000017500000002066700000000000023112 0ustar00zuulzuul00000000000000# 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. from oslo_config import fixture as config_fixture from oslo_serialization import jsonutils from webob import exc from murano.api.v1 import environments from murano.api.v1 import services from murano.api.v1 import sessions import murano.tests.unit.api.base as tb class TestServicesApi(tb.ControllerTest, tb.MuranoApiTestCase): def setUp(self): super(TestServicesApi, self).setUp() self.environments_controller = environments.Controller() self.sessions_controller = sessions.Controller() self.services_controller = services.Controller() self.fixture = self.useFixture(config_fixture.Config()) self.fixture.conf(args=[]) def test_can_post(self): CREDENTIALS_1 = {'tenant': 'test_tenant_1', 'user': 'test_user_1'} self._set_policy_rules( {'create_environment': '@'} ) self.expect_policy_check('create_environment') # Create environment request = self._post( '/environments', jsonutils.dump_as_bytes({'name': 'test_environment_1'}), **CREDENTIALS_1 ) response_body = jsonutils.loads(request.get_response(self.api).body) self.assertEqual(CREDENTIALS_1['tenant'], response_body['tenant_id']) environment_id = response_body['id'] # Create session request = self._post('/environments/{environment_id}/configure' .format(environment_id=environment_id), b'', **CREDENTIALS_1) response_body = jsonutils.loads(request.get_response(self.api).body) session_id = response_body['id'] path = "/" request = self._post('/v1/environments/{0}/services'. format(environment_id), b'', **CREDENTIALS_1) request.headers['X-Configuration-Session'] = str(session_id) request.context.session = session_id self.assertRaises(exc.HTTPBadRequest, self.services_controller.post, request, environment_id, path) response = self.services_controller.post(request, environment_id, path, "test service") self.assertEqual("test service", response) def test_can_put(self): CREDENTIALS_1 = {'tenant': 'test_tenant_1', 'user': 'test_user_1'} self._set_policy_rules( {'create_environment': '@'} ) self.expect_policy_check('create_environment') # Create environment request = self._post( '/environments', jsonutils.dump_as_bytes({'name': 'test_environment_1'}), **CREDENTIALS_1 ) response_body = jsonutils.loads(request.get_response(self.api).body) self.assertEqual(CREDENTIALS_1['tenant'], response_body['tenant_id']) environment_id = response_body['id'] # Create session request = self._post('/environments/{environment_id}/configure' .format(environment_id=environment_id), b'', **CREDENTIALS_1) response_body = jsonutils.loads(request.get_response(self.api).body) session_id = response_body['id'] path = "/" request = self._put('/v1/environments/{0}/services'. format(environment_id), b'', **CREDENTIALS_1) request.headers['X-Configuration-Session'] = str(session_id) request.context.session = session_id # Check that empty body can be put response = self.services_controller.put(request, environment_id, path, []) self.assertEqual([], response) response = self.services_controller.put(request, environment_id, path, "test service") self.assertEqual("test service", response) def test_can_get(self): CREDENTIALS_1 = {'tenant': 'test_tenant_1', 'user': 'test_user_1'} self._set_policy_rules( {'create_environment': '@'} ) self.expect_policy_check('create_environment') # Create environment request = self._post( '/environments', jsonutils.dump_as_bytes({'name': 'test_environment_1'}), **CREDENTIALS_1 ) response_body = jsonutils.loads(request.get_response(self.api).body) self.assertEqual(CREDENTIALS_1['tenant'], response_body['tenant_id']) environment_id = response_body['id'] # Create session request = self._post('/environments/{environment_id}/configure' .format(environment_id=environment_id), b'', **CREDENTIALS_1) response_body = jsonutils.loads(request.get_response(self.api).body) session_id = response_body['id'] # Create service path = '/' request = self._post('/v1/environments/{0}/services'. format(environment_id), b'', **CREDENTIALS_1) request.headers['X-Configuration-Session'] = str(session_id) request.context.session = session_id response = self.services_controller.post(request, environment_id, path, "test service") # Get service request = self._get('/v1/environments/{0}/services'. format(environment_id), b'', **CREDENTIALS_1) response = self.services_controller.get(request, environment_id, path) self.assertEqual([], response) request.headers['X-Configuration-Session'] = str(session_id) request.context.session = session_id response = self.services_controller.get(request, environment_id, path) self.assertNotEqual([], response) def test_can_delete(self): CREDENTIALS_1 = {'tenant': 'test_tenant_1', 'user': 'test_user_1'} self._set_policy_rules( {'create_environment': '@'} ) self.expect_policy_check('create_environment') # Create environment request = self._post( '/environments', jsonutils.dump_as_bytes({'name': 'test_environment_1'}), **CREDENTIALS_1 ) response_body = jsonutils.loads(request.get_response(self.api).body) self.assertEqual(CREDENTIALS_1['tenant'], response_body['tenant_id']) environment_id = response_body['id'] # Create session request = self._post( '/environments/{environment_id}/configure' .format(environment_id=environment_id), b'', **CREDENTIALS_1 ) response_body = jsonutils.loads(request.get_response(self.api).body) session_id = response_body['id'] # Create service path = '/' request = self._post('/v1/environments/{0}/services'. format(environment_id), b'', **CREDENTIALS_1) request.headers['X-Configuration-Session'] = str(session_id) request.context.session = session_id self.services_controller.post(request, environment_id, path, "test service") # Delete service request = self._delete('/v1/environments/{0}/services'. format(environment_id), b'', **CREDENTIALS_1) self.assertRaises(exc.HTTPBadRequest, self.services_controller.delete, request, environment_id, path) request.headers['X-Configuration-Session'] = str(session_id) request.context.session = session_id self.assertRaises(exc.HTTPNotFound, self.services_controller.delete, request, environment_id, path) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/api/v1/test_sessions.py0000664000175000017500000003416000000000000023126 0ustar00zuulzuul00000000000000# 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. from unittest import mock from oslo_config import fixture as config_fixture from oslo_serialization import jsonutils from webob import exc from murano.api.v1 import environments from murano.api.v1 import sessions from murano.services import states import murano.tests.unit.api.base as tb from murano.tests.unit import utils as test_utils class TestSessionsApi(tb.ControllerTest, tb.MuranoApiTestCase): def setUp(self): super(TestSessionsApi, self).setUp() self.environments_controller = environments.Controller() self.sessions_controller = sessions.Controller() self.fixture = self.useFixture(config_fixture.Config()) self.fixture.conf(args=[]) def test_cant_deploy_from_another_tenant(self): """Test to prevent deployment under another tenant user's creds If user from one tenant uses session id and environment id of user from another tenant - he is not able to deploy the environment. Bug: #1382026 """ CREDENTIALS_1 = {'tenant': 'test_tenant_1', 'user': 'test_user_1'} CREDENTIALS_2 = {'tenant': 'test_tenant_2', 'user': 'test_user_2'} self._set_policy_rules( {'create_environment': '@'} ) self.expect_policy_check('create_environment') # Create environment for user #1 request = self._post( '/environments', jsonutils.dump_as_bytes({'name': 'test_environment_1'}), **CREDENTIALS_1 ) response_body = jsonutils.loads(request.get_response(self.api).body) self.assertEqual(CREDENTIALS_1['tenant'], response_body['tenant_id']) ENVIRONMENT_ID = response_body['id'] # Create session of user #1 request = self._post( '/environments/{environment_id}/configure' .format(environment_id=ENVIRONMENT_ID), b'', **CREDENTIALS_1 ) response_body = jsonutils.loads(request.get_response(self.api).body) SESSION_ID = response_body['id'] # Deploy the environment using environment id and session id of user #1 # by user #2 request = self._post( '/environments/{environment_id}/sessions/' '{session_id}/deploy' .format(environment_id=ENVIRONMENT_ID, session_id=SESSION_ID), b'', **CREDENTIALS_2 ) response = request.get_response(self.api) # Should be forbidden! self.assertEqual(403, response.status_code) def test_session_show(self): CREDENTIALS_1 = {'tenant': 'test_tenant_1', 'user': 'test_user_1'} CREDENTIALS_2 = {'tenant': 'test_tenant_2', 'user': 'test_user_2'} self._set_policy_rules( {'create_environment': '@'} ) self.expect_policy_check('create_environment') # Create environment for user #1 request = self._post( '/environments', jsonutils.dump_as_bytes({'name': 'test_environment_1'}), **CREDENTIALS_1 ) response_body = jsonutils.loads(request.get_response(self.api).body) self.assertEqual(CREDENTIALS_1['tenant'], response_body['tenant_id']) ENVIRONMENT_ID = response_body['id'] # Create session of user #1 request = self._post( '/environments/{environment_id}/configure' .format(environment_id=ENVIRONMENT_ID), b'', **CREDENTIALS_1 ) response_body = jsonutils.loads(request.get_response(self.api).body) SESSION_ID = response_body['id'] # Show environment with correct credentials request = self._get( '/environments/{environment_id}/sessions/{session_id}' .format(environment_id=ENVIRONMENT_ID, session_id=SESSION_ID), b'', **CREDENTIALS_1 ) response_body = jsonutils.loads(request.get_response(self.api).body) self.assertEqual(SESSION_ID, response_body['id']) # Show environment with incorrect credentials request = self._get( '/environments/{environment_id}/sessions/{session_id}' .format(environment_id=ENVIRONMENT_ID, session_id=SESSION_ID), b'', **CREDENTIALS_2 ) response = request.get_response(self.api) self.assertEqual(403, response.status_code) def test_session_delete(self): CREDENTIALS = {'tenant': 'test_tenant_1', 'user': 'test_user_1'} self._set_policy_rules( {'create_environment': '@'} ) self.expect_policy_check('create_environment') # Create environment request = self._post( '/environments', jsonutils.dump_as_bytes({'name': 'test_environment_1'}), **CREDENTIALS ) response_body = jsonutils.loads(request.get_response(self.api).body) self.assertEqual(CREDENTIALS['tenant'], response_body['tenant_id']) ENVIRONMENT_ID = response_body['id'] # Create session request = self._post( '/environments/{environment_id}/configure' .format(environment_id=ENVIRONMENT_ID), b'', **CREDENTIALS ) response_body = jsonutils.loads(request.get_response(self.api).body) SESSION_ID = response_body['id'] # Delete session request = self._delete( '/environments/{environment_id}/delete/{session_id}' .format(environment_id=ENVIRONMENT_ID, session_id=SESSION_ID), b'', **CREDENTIALS ) response = self.sessions_controller.delete( request, ENVIRONMENT_ID, SESSION_ID) # Make sure the session was deleted request = self._get( '/environments/{environment_id}/sessions/{session_id}' .format(environment_id=ENVIRONMENT_ID, session_id=SESSION_ID), b'', **CREDENTIALS ) response = request.get_response(self.api) self.assertEqual(404, response.status_code) @mock.patch('murano.api.v1.sessions.sessions.SessionServices') @mock.patch('murano.api.v1.sessions.envs') @mock.patch('murano.api.v1.sessions.check_env') def test_configure(self, _, mock_envs, mock_session_services): mock_request = mock.MagicMock(context=test_utils.dummy_context()) mock_session = mock.MagicMock(to_dict=mock.MagicMock( return_value={'test_env_id', 'test_user_id'})) mock_session_services.create.return_value = mock_session result = self.sessions_controller.configure( mock_request, 'test_env_id') self.assertEqual({'test_env_id', 'test_user_id'}, result) @mock.patch('murano.api.v1.sessions.envs') @mock.patch('murano.api.v1.sessions.check_env') def test_configure_with_env_in_illegal_state(self, _, mock_envs): mock_request = mock.MagicMock(context=test_utils.dummy_context()) illegal_states = [states.EnvironmentStatus.DEPLOYING, states.EnvironmentStatus.DELETING] expected_error_msg = 'Could not open session for environment '\ ', environment has '\ 'deploying or deleting status.'\ .format(env_id='test_env_id') for state in illegal_states: mock_envs.EnvironmentServices.get_status.return_value = state with self.assertRaisesRegex(exc.HTTPForbidden, expected_error_msg): self.sessions_controller.configure(mock_request, 'test_env_id') @mock.patch('murano.api.v1.sessions.check_session') @mock.patch('murano.api.v1.sessions.db_session') def test_show_with_invalid_user(self, mock_db_session, _): mock_session = mock.MagicMock(user_id='test_user_id') mock_db_session.get_session().query().get.return_value = mock_session mock_request = mock.MagicMock(context=test_utils.dummy_context()) mock_request.context.user = 'another_test_user_id' expected_error_msg = 'User is not authorized to '\ 'access session .'\ .format(mock_request.context.user, 'test_sess_id') with self.assertRaisesRegex(exc.HTTPUnauthorized, expected_error_msg): self.sessions_controller.show(mock_request, None, 'test_sess_id') @mock.patch('murano.api.v1.sessions.check_session') @mock.patch('murano.api.v1.sessions.sessions.SessionServices') @mock.patch('murano.api.v1.sessions.db_session') def test_show_with_invalid_session(self, mock_db_session, mock_session_services, _): mock_session = mock.MagicMock(user_id='test_user_id') mock_db_session.get_session().query().get.return_value = mock_session mock_session_services.validate.return_value = False mock_request = mock.MagicMock(context=test_utils.dummy_context()) mock_request.context.user = 'test_user_id' expected_error_msg = 'Session is invalid: environment'\ ' has been updated or updating right now with'\ ' other session'.format('test_sess_id') with self.assertRaisesRegex(exc.HTTPForbidden, expected_error_msg): self.sessions_controller.show(mock_request, None, 'test_sess_id') @mock.patch('murano.api.v1.sessions.check_session') @mock.patch('murano.api.v1.sessions.db_session') def test_delete_with_invalid_user(self, mock_db_session, _): mock_session = mock.MagicMock(user_id='test_user_id') mock_db_session.get_session().query().get.return_value = mock_session mock_request = mock.MagicMock(context=test_utils.dummy_context()) mock_request.context.user = 'another_test_user_id' expected_error_msg = 'User is not authorized to '\ 'access session .'\ .format(mock_request.context.user, 'test_sess_id') with self.assertRaisesRegex(exc.HTTPUnauthorized, expected_error_msg): self.sessions_controller.delete(mock_request, None, 'test_sess_id') @mock.patch('murano.api.v1.sessions.check_session') @mock.patch('murano.api.v1.sessions.sessions.SessionServices') @mock.patch('murano.api.v1.sessions.db_session') def test_delete_with_deploying_session(self, mock_db_session, mock_session_services, _): mock_session = mock.MagicMock(user_id='test_user_id', state=states.SessionState.DEPLOYING) mock_db_session.get_session().query().get.return_value = mock_session mock_session_services.validate.return_value = False mock_request = mock.MagicMock(context=test_utils.dummy_context()) mock_request.context.user = 'test_user_id' expected_error_msg = 'Session is in deploying '\ 'state and could not be deleted'\ .format('test_sess_id') with self.assertRaisesRegex(exc.HTTPForbidden, expected_error_msg): self.sessions_controller.delete(mock_request, None, 'test_sess_id') @mock.patch('murano.api.v1.sessions.check_session') @mock.patch('murano.api.v1.sessions.sessions.SessionServices') @mock.patch('murano.api.v1.sessions.db_session') def test_deploy_with_invalid_session(self, mock_db_session, mock_session_services, _): mock_db_session.get_session().query().get.return_value = None mock_session_services.validate.return_value = False mock_request = mock.MagicMock(context=test_utils.dummy_context()) expected_error_msg = 'Session is invalid: environment'\ ' has been updated or updating right now with'\ ' other session'.format('test_sess_id') with self.assertRaisesRegex(exc.HTTPForbidden, expected_error_msg): self.sessions_controller.deploy(mock_request, None, 'test_sess_id') @mock.patch('murano.api.v1.sessions.check_session') @mock.patch('murano.api.v1.sessions.sessions.SessionServices') @mock.patch('murano.api.v1.sessions.db_session') def test_deploy_with_session_in_invalid_state(self, mock_db_session, mock_session_services, _): mock_session_services.validate.return_value = True mock_request = mock.MagicMock(context=test_utils.dummy_context()) expected_error_msg = 'Session is already deployed or '\ 'deployment is in progress'.format('test_sess_id') invalid_states = [states.SessionState.DEPLOYING, states.SessionState.DEPLOYED, states.SessionState.DEPLOY_FAILURE, states.SessionState.DELETING, states.SessionState.DELETE_FAILURE] for state in invalid_states: mock_session = mock.MagicMock(state=state) mock_db_session.get_session().query().get.return_value =\ mock_session with self.assertRaisesRegex(exc.HTTPForbidden, expected_error_msg): self.sessions_controller.deploy( mock_request, None, 'test_sess_id') ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/api/v1/test_static_actions.py0000664000175000017500000001221500000000000024264 0ustar00zuulzuul00000000000000# Copyright (c) 2016 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. from unittest import mock from oslo_messaging.rpc import client from oslo_serialization import jsonutils from webob import exc from murano.api.v1 import static_actions from murano.common import policy import murano.tests.unit.api.base as tb @mock.patch.object(policy, 'check') class TestStaticActionsApi(tb.ControllerTest, tb.MuranoApiTestCase): def setUp(self): super(TestStaticActionsApi, self).setUp() self.controller = static_actions.Controller() def test_execute_static_action(self, mock_policy_check): """Test that action execution results in the correct rpc call.""" self._set_policy_rules( {'execute_action': '@'} ) action = { 'method': 'TestAction', 'args': {'name': 'John'}, 'class_name': 'TestClass', 'pkg_name': 'TestPackage', 'class_version': '=0' } rpc_task = { 'action': action, 'token': None, 'project_id': 'test_tenant', 'user_id': 'test_user', 'id': mock.ANY } request_data = { "className": 'TestClass', "methodName": 'TestAction', "packageName": 'TestPackage', "classVersion": '=0', "parameters": {'name': 'John'} } req = self._post('/actions', jsonutils.dump_as_bytes(request_data)) try: self.controller.execute(req, request_data) except TypeError: pass self.mock_engine_rpc.call_static_action.assert_called_once_with( rpc_task) def test_execute_static_action_handle_bad_data_exc(self, _): request_data = { "className": None, "methodName": 'TestAction' } req = self._post('/actions', jsonutils.dump_as_bytes(request_data)) self.assertRaises(exc.HTTPBadRequest, self.controller.execute, req, request_data) request_data = { "className": 'TestClass', "methodName": None } req = self._post('/actions', jsonutils.dump_as_bytes(request_data)) self.assertRaises(exc.HTTPBadRequest, self.controller.execute, req, request_data) @mock.patch('murano.services.static_actions.StaticActionServices.execute') def test_execute_static_action_handle_execute_excs(self, mock_execute, _): """Test whether execute handles all exceptions thrown correctly.""" request_data = { "className": 'TestClass', "methodName": 'TestAction', "packageName": 'TestPackage', "classVersion": '=0', "parameters": {'name': 'John'} } exc_types = ['NoClassFound', 'NoMethodFound', 'NoPackageFound', 'NoPackageForClassFound', 'MethodNotExposed', 'NoMatchingMethodException'] for exc_type in exc_types: mock_execute.side_effect = client.RemoteError(exc_type=exc_type) req = self._post('/actions', jsonutils.dump_as_bytes(request_data)) self.assertRaises(exc.HTTPNotFound, self.controller.execute, req, request_data) self.assertEqual(mock_execute.call_count, len(exc_types)) exc_type = 'ContractViolationException' mock_execute.side_effect = client.RemoteError(exc_type=exc_type) req = self._post('/actions', jsonutils.dump_as_bytes(request_data)) self.assertRaises(exc.HTTPBadRequest, self.controller.execute, req, request_data) exc_types.append(exc_type) self.assertEqual(mock_execute.call_count, len(exc_types)) exc_type = 'ThisIsARandomTestException' mock_execute.side_effect = client.RemoteError(exc_type=exc_type) req = self._post('/actions', jsonutils.dump_as_bytes(request_data)) self.assertRaises(exc.HTTPServiceUnavailable, self.controller.execute, req, request_data) exc_types.append(exc_type) self.assertEqual(mock_execute.call_count, len(exc_types)) try: int('this will throw a value error') except ValueError as e: setattr(e, 'message', None) exc_type = e mock_execute.side_effect = exc_type req = self._post('/actions', jsonutils.dump_as_bytes(request_data)) self.assertRaises(exc.HTTPBadRequest, self.controller.execute, req, request_data) exc_types.append(exc_type) self.assertEqual(mock_execute.call_count, len(exc_types)) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/base.py0000664000175000017500000000336000000000000020032 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT 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 fixtures from oslo_log import log as logging import testtools from murano.common import config from murano.db import api as db_api CONF = config.CONF logging.register_options(CONF) logging.setup(CONF, 'murano') class MuranoTestCase(testtools.TestCase): def setUp(self): super(MuranoTestCase, self).setUp() self.useFixture(fixtures.FakeLogger('murano')) def override_config(self, name, override, group=None): CONF.set_override(name, override, group) self.addCleanup(CONF.clear_override, name, group) class MuranoWithDBTestCase(MuranoTestCase): def setUp(self): super(MuranoWithDBTestCase, self).setUp() self.override_config('connection', "sqlite://", group='database') db_api.setup_db() self.addCleanup(db_api.drop_db) self.override_config('env_audit_enabled', False, group='stats') class MuranoNotifyWithDBTestCase(MuranoWithDBTestCase): def setUp(self): super(MuranoNotifyWithDBTestCase, self).setUp() self.override_config('connection', "sqlite://", group='database') db_api.setup_db() self.addCleanup(db_api.drop_db) self.override_config('env_audit_enabled', True, group='stats') ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1696417899.8171809 murano-16.0.0/murano/tests/unit/cmd/0000775000175000017500000000000000000000000017307 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/cmd/__init__.py0000664000175000017500000000000000000000000021406 0ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/cmd/test_api_workers.py0000664000175000017500000000566400000000000023260 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # 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 sys from unittest import mock from oslo_concurrency import processutils from oslo_log import log as logging from murano.cmd import api from murano.common import app_loader from murano.common import config from murano.common import policy from murano.tests.unit import base class TestAPIWorkers(base.MuranoTestCase): def setUp(self): super(TestAPIWorkers, self).setUp() sys.argv = ['murano'] @mock.patch.object(config, 'parse_args') @mock.patch.object(logging, 'setup') @mock.patch.object(policy, 'init') @mock.patch.object(config, 'set_middleware_defaults') @mock.patch.object(app_loader, 'load_paste_app') @mock.patch('oslo_service.service.launch') def test_workers_default(self, launch, setup, parse_args, init, load_paste_app, set_middleware_defaults): api.main() launch.assert_called_once_with(mock.ANY, mock.ANY, workers=processutils.get_worker_count(), restart_method='mutate') @mock.patch.object(config, 'parse_args') @mock.patch.object(logging, 'setup') @mock.patch.object(policy, 'init') @mock.patch.object(config, 'set_middleware_defaults') @mock.patch.object(app_loader, 'load_paste_app') @mock.patch('oslo_service.service.launch') def test_workers_good_setting(self, launch, setup, parse_args, init, load_paste_app, set_middleware_defaults): self.override_config("api_workers", 8, "murano") api.main() launch.assert_called_once_with(mock.ANY, mock.ANY, workers=8, restart_method='mutate') @mock.patch.object(config, 'parse_args') @mock.patch.object(logging, 'setup') @mock.patch.object(policy, 'init') @mock.patch.object(config, 'set_middleware_defaults') @mock.patch.object(app_loader, 'load_paste_app') @mock.patch('oslo_service.service.launch') def test_workers_zero_setting(self, launch, setup, parse_args, init, load_paste_app, set_middleware_defaults): self.override_config("api_workers", 0, "murano") api.main() launch.assert_called_once_with(mock.ANY, mock.ANY, workers=processutils.get_worker_count(), restart_method='mutate') ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/cmd/test_engine_workers.py0000664000175000017500000000425000000000000023742 0ustar00zuulzuul00000000000000# Copyright (c) 2016 NEC Corporation. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from unittest import mock from oslo_concurrency import processutils from oslo_log import log as logging from murano.cmd import engine from murano.common import config from murano.tests.unit import base class TestEngineWorkers(base.MuranoTestCase): @mock.patch.object(config, 'parse_args') @mock.patch.object(logging, 'setup') @mock.patch('oslo_service.service.launch') def test_workers_default(self, launch, setup, parse_args): engine.main() launch.assert_called_once_with(mock.ANY, mock.ANY, workers=processutils.get_worker_count(), restart_method='mutate') @mock.patch.object(config, 'parse_args') @mock.patch.object(logging, 'setup') @mock.patch('oslo_service.service.launch') def test_workers_good_setting(self, launch, setup, parse_args): self.override_config("engine_workers", 8, "engine") engine.main() launch.assert_called_once_with(mock.ANY, mock.ANY, workers=8, restart_method='mutate') @mock.patch.object(config, 'parse_args') @mock.patch.object(logging, 'setup') @mock.patch('oslo_service.service.launch') def test_workers_zero_setting(self, launch, setup, parse_args): self.override_config("engine_workers", 0, "engine") engine.main() launch.assert_called_once_with(mock.ANY, mock.ANY, workers=processutils.get_worker_count(), restart_method='mutate') ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/cmd/test_manage.py0000664000175000017500000002222000000000000022146 0ustar00zuulzuul00000000000000# Copyright (c) 2016 AT&T Corp # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from unittest import mock from oslo_config import cfg try: from StringIO import StringIO except ImportError: from io import StringIO from murano.cmd import manage from murano.db.catalog import api as db_catalog_api from murano.db import models from murano.db import session as db_session from murano.tests.unit import base as test_base CONF = cfg.CONF class TestManage(test_base.MuranoWithDBTestCase): def setUp(self): super(TestManage, self).setUp() session = db_session.get_session() # Create environment. self.test_environment = models.Environment( name=b'test_environment', tenant_id=b'test_tenant_id', version=1 ) # Create categories. self.test_categories = [ models.Category(name=b'test_category_1'), models.Category(name=b'test_category_2') ] # Create tags. self.test_tags = [ models.Tag(name=b'test_tag_1'), models.Tag(name=b'test_tag_2') ] # Add environment, categories and tags to DB. with session.begin(): session.add(self.test_environment) session.add_all(self.test_categories) session.add_all(self.test_tags) # Create package. self.test_package = models.Package( fully_qualified_name=b'test_fqn', name=b'test_name', logo=b'test_logo', supplier_logo=b'test_supplier_logo', type=b'test_type', description=b'test_desc', is_public=True, archive=b'test_archive', ui_definition=b'test_ui_definition', categories=self.test_categories, tags=self.test_tags, owner_id=self.test_environment.tenant_id,) # Add the package to the DB. with session.begin(): session.add(self.test_package) # Create class definitions and assign their FKs to test_package.id. self.test_class_definitions = [ models.Class(name=b'test_class_definition_1', package_id=self.test_package.id), models.Class(name=b'test_class_definition_2', package_id=self.test_package.id) ] # Add the class definitions to the DB and update the FK reference for # test_package.class_definitions. with session.begin(): session.add_all(self.test_class_definitions) self.test_package.class_definitions = self.test_class_definitions session.add(self.test_package) # Create mock object that resembles loaded package from # load_utils.load_from_dir self.mock_loaded_package = mock.MagicMock( full_name=self.test_package.fully_qualified_name, display_name=self.test_package.name, package_type=self.test_package.type, author=self.test_package.author, supplier=self.test_package.supplier, description=self.test_package.description, tags=[tag.name for tag in self.test_package.tags], classes=[cls.name for cls in self.test_package.class_definitions], logo=self.test_package.logo, supplier_logo=self.test_package.supplier_logo, ui=self.test_package.ui_definition, blob=self.test_package.archive) @mock.patch('murano.cmd.manage.LOG') @mock.patch('murano.cmd.manage.load_utils') def test_do_import_package(self, mock_load_utils, mock_log): manage.CONF = mock.MagicMock() manage.CONF.command = mock.MagicMock( directory='test_dir', categories=[cat.name for cat in self.test_package.categories], update=True) mock_load_utils.load_from_dir.return_value = self.mock_loaded_package manage.do_import_package() # Assert that the function ran to completion. self.assertIn("Finished import of package", str(mock_log.info.mock_calls[0])) # Check that the package was uploaded to the DB. filter_params = { 'name': self.test_package.name, 'fully_qualified_name': self.test_package.fully_qualified_name, 'type': self.test_package.type, 'description': self.test_package.description } retrieved_package = None session = db_session.get_session() with session.begin(): retrieved_package = session.query(models.Package)\ .filter_by(**filter_params).first() self.assertIsNotNone(retrieved_package) self.assertNotEqual(self.test_package.id, retrieved_package.id) @mock.patch('murano.cmd.manage.LOG') @mock.patch('murano.cmd.manage.load_utils') @mock.patch('murano.cmd.manage.db_catalog_api') def test_do_import_package_without_update(self, mock_db_catalog_api, mock_load_utils, mock_log): mock_db_catalog_api.package_search.return_value =\ [self.test_package] mock_load_utils.load_from_dir.return_value =\ mock.MagicMock(full_name='test_full_name') manage.CONF = mock.MagicMock() manage.CONF.command = mock.MagicMock( directory='test_dir', categories=[], update=False) manage.do_import_package() mock_log.error.assert_called_once_with( "Package '{name}' exists ({pkg_id}). Use --update." .format(name='test_full_name', pkg_id=self.test_package.id)) @mock.patch('sys.stdout', new_callable=StringIO) def test_do_list_categories(self, mock_stdout): expected_output = ">> Murano package categories:* "\ "test_category_1* test_category_2" manage.do_list_categories() self.assertEqual(expected_output, mock_stdout.getvalue().replace('\n', '') .replace('b\'', '').replace('\'', '')) @mock.patch('murano.cmd.manage.db_catalog_api') @mock.patch('sys.stdout', new_callable=StringIO) def test_do_list_categories_with_no_categories(self, mock_stdout, mock_db_catalog_api): mock_db_catalog_api.category_get_names.return_value = [] expected_output = "No categories were found" manage.do_list_categories() self.assertEqual( expected_output, mock_stdout.getvalue().replace('\n', '')) @mock.patch('sys.stdout', new_callable=StringIO) def test_do_add_category(self, mock_stdout): manage.CONF = mock.MagicMock() manage.CONF.command.category_name = 'test_category_name' expected_output = ">> Successfully added category test_category_name" manage.do_add_category() self.assertEqual(expected_output, mock_stdout.getvalue().replace('\n', '')) @mock.patch('sys.stdout', new_callable=StringIO) def test_do_add_category_except_duplicate_error(self, mock_stdout): manage.CONF = mock.MagicMock() manage.CONF.command.category_name = 'test_category_name' expected_output = ">> ERROR: Category \'test_category_name\' already "\ "exists" db_catalog_api.category_add('test_category_name') manage.do_add_category() self.assertEqual(expected_output, mock_stdout.getvalue().replace('\n', '')) def test_add_command_parsers(self): mock_parser = mock.MagicMock() mock_subparsers = mock.MagicMock() mock_subparsers.add_parser.return_value = mock_parser manage.add_command_parsers(mock_subparsers) mock_subparsers.add_parser.assert_any_call('import-package') mock_subparsers.add_parser.assert_any_call('category-list') mock_subparsers.add_parser.assert_any_call('category-add') mock_parser.set_defaults.assert_any_call(func=manage.do_import_package) mock_parser.set_defaults.assert_any_call( func=manage.do_list_categories) mock_parser.set_defaults.assert_any_call(func=manage.do_add_category) self.assertEqual(4, mock_parser.add_argument.call_count) @mock.patch('murano.cmd.manage.CONF') def test_main_except_runtime_error(self, mock_conf): mock_conf.side_effect = RuntimeError with self.assertRaisesRegex(SystemExit, 'ERROR:'): manage.main() @mock.patch('murano.cmd.manage.CONF') def test_main_except_general_exception(self, mock_conf): mock_conf.command.func.side_effect = Exception expected_err_msg = "murano-manage command failed:" with self.assertRaisesRegex(SystemExit, expected_err_msg): manage.main() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/cmd/test_status.py0000664000175000017500000000236300000000000022247 0ustar00zuulzuul00000000000000# Copyright (c) 2018 NEC, 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 oslo_config import cfg from oslo_upgradecheck.upgradecheck import Code from murano.cmd import status from murano.tests.unit import base class TestUpgradeChecks(base.MuranoTestCase): def setUp(self): super(TestUpgradeChecks, self).setUp() self.cmd = status.Checks() def test_checks(self): cfg.CONF(args=[], project='murano') for name, func in self.cmd._upgrade_checks: if isinstance(func, tuple): func_name, kwargs = func result = func_name(self, **kwargs) else: result = func(self) self.assertEqual(Code.SUCCESS, result.code) ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1696417899.821181 murano-16.0.0/murano/tests/unit/common/0000775000175000017500000000000000000000000020034 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/common/__init__.py0000664000175000017500000000000000000000000022133 0ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1696417899.821181 murano-16.0.0/murano/tests/unit/common/helpers/0000775000175000017500000000000000000000000021476 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/common/helpers/__init__.py0000664000175000017500000000000000000000000023575 0ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/common/helpers/test_token_sanitizer.py0000664000175000017500000000362700000000000026327 0ustar00zuulzuul00000000000000# 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 murano.common.helpers import token_sanitizer from murano.tests.unit import base class TokenSanitizerTests(base.MuranoTestCase): sanitizer = token_sanitizer.TokenSanitizer() def test_dict_with_one_value(self): source = {'token': 'value'} value = self.sanitizer.sanitize(source) self.assertEqual(self.sanitizer.message, value['token']) def test_dict_with_few_value(self): source = {'token': 'value', 'pass': 'value', 'TrustId': 'value'} value = self.sanitizer.sanitize(source) self.assertEqual(self.sanitizer.message, value['token']) self.assertEqual(self.sanitizer.message, value['pass']) self.assertEqual(self.sanitizer.message, value['TrustId']) def test_dict_with_nested_dict(self): source = {'obj': {'pass': 'value'}} value = self.sanitizer.sanitize(source) self.assertEqual(self.sanitizer.message, value['obj']['pass']) def test_dict_with_nested_list(self): source = {'obj': [{'pass': 'value'}]} value = self.sanitizer.sanitize(source) self.assertEqual(self.sanitizer.message, value['obj'][0]['pass']) def test_leave_out_other_values(self): source = {'obj': ['value']} value = self.sanitizer.sanitize(source) self.assertEqual('value', value['obj'][0]) ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1696417899.821181 murano-16.0.0/murano/tests/unit/common/messaging/0000775000175000017500000000000000000000000022011 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/common/messaging/__init__.py0000664000175000017500000000000000000000000024110 0ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/common/messaging/test_mqclient.py0000664000175000017500000002554400000000000025250 0ustar00zuulzuul00000000000000# Copyright 2016 AT&T Corp # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from unittest import mock from oslo_config import cfg from oslo_serialization import jsonutils import ssl as ssl_module from murano.common.i18n import _ from murano.common.messaging import mqclient from murano.tests.unit import base CONF = cfg.CONF class MQClientTest(base.MuranoTestCase): @mock.patch('murano.common.messaging.mqclient.kombu') def setUp(self, mock_kombu): super(MQClientTest, self).setUp() self.ssl_client = mqclient.MqClient(login='test_login', password='test_password', host='test_host', port='test_port', virtual_host='test_virtual_host', ssl=True, ca_certs=['cert1'], insecure=False) mock_kombu.Connection.assert_called_once_with( 'amqp://{0}:{1}@{2}:{3}/{4}'.format('test_login', 'test_password', 'test_host', 'test_port', 'test_virtual_host'), ssl={'ca_certs': ['cert1'], 'cert_reqs': ssl_module.CERT_REQUIRED}) self.assertEqual(mock_kombu.Connection(), self.ssl_client._connection) self.assertIsNone(self.ssl_client._channel) self.assertFalse(self.ssl_client._connected) @mock.patch('murano.common.messaging.mqclient.kombu', autospec=True) def test_client_initialization_with_ssl_version(self, mock_kombu): ssl_versions = ( ('tlsv1', getattr(ssl_module, 'PROTOCOL_TLSv1', None)), ('tlsv1_1', getattr(ssl_module, 'PROTOCOL_TLSv1_1', None)), ('tlsv1_2', getattr(ssl_module, 'PROTOCOL_TLSv1_2', None)), ('sslv2', getattr(ssl_module, 'PROTOCOL_SSLv2', None)), ('sslv23', getattr(ssl_module, 'PROTOCOL_SSLv23', None)), ('sslv3', getattr(ssl_module, 'PROTOCOL_SSLv3', None))) exception_count = 0 for ssl_name, ssl_version in ssl_versions: ssl_kwargs = { 'login': 'test_login', 'password': 'test_password', 'host': 'test_host', 'port': 'test_port', 'virtual_host': 'test_virtual_host', 'ssl': True, 'ssl_version': ssl_name, 'ca_certs': ['cert1'], 'insecure': False } # If a ssl_version is not valid, a RuntimeError is thrown. # According to the ssl_version docs in config.py, certain versions # of TLS may be available depending on the system. So, just # check that at least 1 ssl_version works. if ssl_version is None: e = self.assertRaises(RuntimeError, mqclient.MqClient, **ssl_kwargs) self.assertEqual(_('Invalid SSL version: %s') % ssl_name, e.__str__()) exception_count += 1 continue self.ssl_client = mqclient.MqClient(**ssl_kwargs) mock_kombu.Connection.assert_called_once_with( 'amqp://{0}:{1}@{2}:{3}/{4}'.format( 'test_login', 'test_password', 'test_host', 'test_port', 'test_virtual_host'), ssl={'ca_certs': ['cert1'], 'cert_reqs': ssl_module.CERT_REQUIRED, 'ssl_version': ssl_version}) self.assertEqual( mock_kombu.Connection(), self.ssl_client._connection) self.assertIsNone(self.ssl_client._channel) self.assertFalse(self.ssl_client._connected) mock_kombu.Connection.reset_mock() # Check that at least one ssl_version worked. self.assertGreater(len(ssl_versions), exception_count) @mock.patch('murano.common.messaging.mqclient.kombu') def test_alternate_client_initializations(self, mock_kombu): for ca_cert in ['cert1', None]: client = mqclient.MqClient(login='test_login', password='test_password', host='test_host', port='test_port', virtual_host='test_virtual_host', ssl=True, ca_certs=ca_cert, insecure=True) mock_kombu.Connection.assert_called_once_with( 'amqp://{0}:{1}@{2}:{3}/{4}'.format('test_login', 'test_password', 'test_host', 'test_port', 'test_virtual_host'), ssl={'ca_certs': ca_cert, 'cert_reqs': ssl_module.CERT_OPTIONAL if ca_cert else ssl_module.CERT_NONE}) self.assertEqual(mock_kombu.Connection(), client._connection) mock_kombu.Connection.reset_mock() client = mqclient.MqClient(login='test_login', password='test_password', host='test_host', port='test_port', virtual_host='test_virtual_host', ssl=False, ca_certs=None, insecure=False) mock_kombu.Connection.assert_called_once_with( 'amqp://{0}:{1}@{2}:{3}/{4}'.format('test_login', 'test_password', 'test_host', 'test_port', 'test_virtual_host'), ssl=None) self.assertEqual(mock_kombu.Connection(), client._connection) def test_connect(self): self.ssl_client.connect() self.ssl_client._connection.connect.assert_called_once_with() self.ssl_client._connection.channel.assert_called_once_with() self.assertEqual(self.ssl_client._connection.channel(), self.ssl_client._channel) self.assertTrue(self.ssl_client._connected) def test_close(self): self.ssl_client.close() self.ssl_client._connection.close.assert_called_once_with() self.assertFalse(self.ssl_client._connected) def test_enter_and_exit(self): with self.ssl_client: pass self.ssl_client._connection.connect.assert_called_once_with() self.ssl_client._connection.close.assert_called_once_with() @mock.patch('murano.common.messaging.mqclient.kombu') def test_declare(self, mock_kombu): self.ssl_client.connect() self.ssl_client.declare(queue='test_queue', exchange='test_exchange', enable_ha=True, ttl=1) queue_args = { 'x-ha-policy': 'all', 'x-expires': 1 } mock_kombu.Exchange.assert_called_once_with( 'test_exchange', type='direct', durable=True) mock_kombu.Queue.assert_called_once_with('test_queue', mock_kombu.Exchange(), 'test_queue', durable=True, queue_arguments=queue_args) mock_kombu.Queue()().declare.assert_called_once_with() mock_kombu.reset_mock() self.ssl_client.declare(queue='test_queue', exchange='test_exchange', enable_ha=False, ttl=0) mock_kombu.Exchange.assert_called_once_with( 'test_exchange', type='direct', durable=True) mock_kombu.Queue.assert_called_once_with('test_queue', mock_kombu.Exchange(), 'test_queue', durable=True, queue_arguments={}) def test_declare_except_runtime_error(self): with self.assertRaisesRegex(RuntimeError, 'Not connected to RabbitMQ'): self.ssl_client.declare(None) @mock.patch('murano.common.messaging.mqclient.kombu') def test_send(self, mock_kombu): mock_message = mock.MagicMock(body='test_message', id=3) self.ssl_client.connect() self.ssl_client.send(mock_message, 'test_key', 'test_exchange') mock_kombu.Producer.assert_called_once_with( self.ssl_client._connection) mock_kombu.Producer().publish.assert_called_once_with( exchange='test_exchange', routing_key='test_key', body=jsonutils.dumps('test_message'), message_id='3', headers=None) @mock.patch('murano.common.messaging.mqclient.kombu') def test_send_signed(self, mock_kombu): mock_message = mock.MagicMock(body='test_message', id=3) signer = mock.MagicMock() signer.return_value = "SIGNATURE" self.ssl_client.connect() self.ssl_client.send(mock_message, 'test_key', 'test_exchange', signer) mock_kombu.Producer.assert_called_once_with( self.ssl_client._connection) mock_kombu.Producer().publish.assert_called_once_with( exchange='test_exchange', routing_key='test_key', body=jsonutils.dumps('test_message'), message_id='3', headers={'signature': 'SIGNATURE'}) def test_send_except_runtime_error(self): with self.assertRaisesRegex(RuntimeError, 'Not connected to RabbitMQ'): self.ssl_client.send(None, None) @mock.patch('murano.common.messaging.mqclient.subscription') def test_open(self, mock_subscription): self.ssl_client.connect() self.ssl_client.open('test_queue', prefetch_count=2) mock_subscription.Subscription.assert_called_once_with( self.ssl_client._connection, 'test_queue', 2) def test_open_except_runtime_error(self): with self.assertRaisesRegex(RuntimeError, 'Not connected to RabbitMQ'): self.ssl_client.open(None) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/common/test_app_loader.py0000664000175000017500000000563100000000000023560 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # 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 unittest import mock from oslo_config import cfg from paste import deploy from murano.common import app_loader from murano.common import config # noqa from murano.tests.unit import base CONF = cfg.CONF class AppLoaderTest(base.MuranoTestCase): def setUp(self): super(AppLoaderTest, self).setUp() self.override_config('flavor', 'myflavor', 'paste_deploy') self.override_config('config_file', 'path/to/myapp-paste.ini', 'paste_deploy') CONF.config_file = ['myapp.conf'] CONF.prog = 'myapp' CONF.find_file = mock.MagicMock(return_value='path/to/myapp-paste.ini') @mock.patch.object(deploy, 'loadapp', return_value=mock.sentinel.myapp) def _test_load_paste_app(self, mock_loadapp, appname='myapp', fullname='myapp-myflavor', path='path/to/myapp-paste.ini'): expected_config_path = 'config:%s/%s' % (os.path.abspath('.'), path,) app = app_loader.load_paste_app(appname) mock_loadapp.assert_called_with(expected_config_path, name=fullname) self.assertEqual(mock.sentinel.myapp, app) def test_load_paste_app(self): self._test_load_paste_app() def test_load_paste_app_no_name(self): self._test_load_paste_app(appname=None) def test_load_paste_app_no_flavor(self): self.override_config('flavor', None, 'paste_deploy') self._test_load_paste_app(fullname='myapp') def test_load_paste_app_no_pastedep_cfg_opt(self): self.override_config('config_file', None, 'paste_deploy') self._test_load_paste_app() def test_load_paste_app_no_pastedep_cfg_opt_and_cfg_opt(self): self.override_config('config_file', None, 'paste_deploy') CONF.config_file = [] self._test_load_paste_app() CONF.find_file.assert_called_with('myapp-paste.ini') def test_load_paste_app_no_cfg_at_all(self): self.override_config('config_file', None, 'paste_deploy') CONF.find_file.return_value = None self.assertRaises(RuntimeError, app_loader.load_paste_app, 'myapp') def test_load_paste_app_deploy_error(self): deploy.loadapp = mock.MagicMock() deploy.loadapp.side_effect = LookupError('Oops') self.assertRaises(RuntimeError, app_loader.load_paste_app, 'myapp') ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/common/test_auth_utils.py0000664000175000017500000003457200000000000023641 0ustar00zuulzuul00000000000000# 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 unittest import mock from keystoneauth1 import loading as ka_loading from murano.common import auth_utils from murano.tests.unit import base from oslo_config import cfg class TestAuthUtils(base.MuranoTestCase): def setUp(self): super(TestAuthUtils, self).setUp() # Register the Password auth plugin options, # so we can use CONF.set_override password_option = ka_loading.get_auth_plugin_conf_options('password') cfg.CONF.register_opts(password_option, group=auth_utils.CFG_MURANO_AUTH_GROUP) self.addCleanup(mock.patch.stopall) def _init_mock_cfg(self): mock_auth_obj = mock.patch.object(auth_utils, 'ka_loading', spec_set=ka_loading).start() mock_auth_obj.load_auth_from_conf_options.return_value = \ mock.sentinel.auth mock_auth_obj.load_session_from_conf_options.\ return_value = mock.sentinel.session cfg.CONF.set_override('auth_type', 'password', auth_utils.CFG_MURANO_AUTH_GROUP) cfg.CONF.set_override('www_authenticate_uri', 'foo_www_authenticate_uri', auth_utils.CFG_MURANO_AUTH_GROUP) cfg.CONF.set_override('auth_url', 'foo_auth_url', auth_utils.CFG_MURANO_AUTH_GROUP) cfg.CONF.set_override('username', 'fakeuser', auth_utils.CFG_MURANO_AUTH_GROUP) cfg.CONF.set_override('password', 'fakepass', auth_utils.CFG_MURANO_AUTH_GROUP) cfg.CONF.set_override('user_domain_name', 'Default', auth_utils.CFG_MURANO_AUTH_GROUP) cfg.CONF.set_override('project_domain_name', 'Default', auth_utils.CFG_MURANO_AUTH_GROUP) cfg.CONF.set_override('project_name', 'fakeproj', auth_utils.CFG_MURANO_AUTH_GROUP) return mock_auth_obj def test_get_keystone_auth(self): mock_identity = self._init_mock_cfg() expected_auth = mock.sentinel.auth actual_auth = auth_utils._get_keystone_auth() self.assertEqual(expected_auth, actual_auth) mock_identity.load_auth_from_conf_options.assert_called_once_with( cfg.CONF, auth_utils.CFG_MURANO_AUTH_GROUP) def test_get_keystone_with_trust_id(self): mock_ka_loading = self._init_mock_cfg() expected_kwargs = { 'project_name': None, 'project_domain_name': None, 'project_id': None, 'trust_id': mock.sentinel.trust_id } expected_auth = mock.sentinel.auth actual_auth = auth_utils._get_keystone_auth(mock.sentinel.trust_id) self.assertEqual(expected_auth, actual_auth) mock_ka_loading.load_auth_from_conf_options.assert_called_once_with( cfg.CONF, auth_utils.CFG_MURANO_AUTH_GROUP, **expected_kwargs) @mock.patch.object(auth_utils, 'ks_client', autospec=True) @mock.patch.object(auth_utils, '_get_session', autospec=True) def test_create_keystone_admin_client(self, mock_get_sess, mock_ks_client): self._init_mock_cfg() mock_get_sess.return_value = mock.sentinel.session mock_ks_client.Client.return_value = mock.sentinel.ks_admin_client result = auth_utils._create_keystone_admin_client() self.assertEqual(result, mock.sentinel.ks_admin_client) self.assertTrue(mock_get_sess.called) mock_ks_client.Client.assert_called_once_with( session=mock.sentinel.session) @mock.patch.object(auth_utils, '_get_session', autospec=True) @mock.patch.object(auth_utils, '_get_keystone_auth', autospec=True) @mock.patch.object( auth_utils.helpers, 'get_execution_session', autospec=True) def test_get_client_session(self, mock_get_execution_session, mock_get_keystone_auth, mock_get_session): mock_exec_session = mock.Mock(trust_id=mock.sentinel.trust_id) mock_get_execution_session.return_value = mock_exec_session mock_get_keystone_auth.return_value = mock.sentinel.auth mock_get_session.return_value = mock.sentinel.session session = auth_utils.get_client_session(conf=mock.sentinel.conf) self.assertEqual(mock.sentinel.session, session) mock_get_execution_session.assert_called_once_with() mock_get_keystone_auth.assert_called_once_with(mock.sentinel.trust_id) mock_get_session.assert_called_once_with( auth=mock.sentinel.auth, conf_section=mock.sentinel.conf) @mock.patch.object(auth_utils, 'get_token_client_session', autospec=True) @mock.patch.object( auth_utils.helpers, 'get_execution_session', autospec=True) def test_get_client_session_without_trust_id( self, mock_get_execution_session, mock_get_token_client_session): mock_get_token_client_session.return_value = mock.sentinel.session mock_exec_session = mock.Mock(trust_id=None, token=mock.sentinel.token, project_id=mock.sentinel.project_id) session = auth_utils.get_client_session( execution_session=mock_exec_session, conf=mock.sentinel.conf) self.assertEqual(mock.sentinel.session, session) self.assertFalse(mock_get_execution_session.called) mock_get_token_client_session.assert_called_once_with( token=mock.sentinel.token, project_id=mock.sentinel.project_id) @mock.patch.object(auth_utils, '_get_session', autospec=True) @mock.patch.object(auth_utils, 'identity', autospec=True) @mock.patch.object( auth_utils.helpers, 'get_execution_session', autospec=True) def test_get_token_client_session( self, mock_get_execution_session, mock_identity, mock_get_session): cfg.CONF.set_override('www_authenticate_uri', 'foo_www_authenticate_uri/v2.0', auth_utils.CFG_MURANO_AUTH_GROUP) mock_get_execution_session.return_value = \ mock.Mock(token=mock.sentinel.token, project_id=mock.sentinel.project_id) mock_identity.Token.return_value = mock.sentinel.auth mock_get_session.return_value = mock.sentinel.session session = auth_utils.get_token_client_session() self.assertEqual(mock.sentinel.session, session) mock_get_execution_session.assert_called_once_with() mock_identity.Token.assert_called_once_with( 'foo_www_authenticate_uri/v3', token=mock.sentinel.token, project_id=mock.sentinel.project_id) mock_get_session.assert_called_once_with( auth=mock.sentinel.auth, conf_section=None) @mock.patch.object(auth_utils, 'get_token_client_session', autospec=True) @mock.patch.object(auth_utils, 'ks_client', autospec=True) def test_create_keystone_client(self, mock_ks_client, mock_get_token_client_session): mock_ks_client.Client.return_value = mock.sentinel.ks_client mock_session = mock.Mock( token=mock.sentinel.token, project_id=mock.sentinel.project_id, conf=mock.sentinel.conf) mock_get_token_client_session.return_value = mock_session ks_client = auth_utils.create_keystone_client( mock.sentinel.token, mock.sentinel.project_id, mock.sentinel.conf) self.assertEqual(mock.sentinel.ks_client, ks_client) mock_ks_client.Client.assert_called_once_with(session=mock_session) @mock.patch.object(auth_utils, 'create_keystone_client', autospec=True) @mock.patch.object( auth_utils, '_create_keystone_admin_client', autospec=True) def test_create_trust(self, mock_create_ks_admin_client, mock_create_ks_client): mock_auth_ref = mock.Mock(user_id=mock.sentinel.trustor_user, project_id=mock.sentinel.project_id, role_names=mock.sentinel.role_names) mock_admin_session = mock.Mock(**{ 'auth.get_user_id.return_value': mock.sentinel.trustee_user }) mock_user_session = mock.Mock(**{ 'auth.get_access.return_value': mock_auth_ref }) mock_trust = mock.Mock(id=mock.sentinel.trust_id) mock_admin_client = mock.Mock(session=mock_admin_session) mock_user_client = mock.Mock( session=mock_user_session, **{'trusts.create.return_value': mock_trust}) mock_create_ks_admin_client.return_value = mock_admin_client mock_create_ks_client.return_value = mock_user_client trust_id = auth_utils.create_trust( trustee_token=mock.sentinel.trustee_token, trustee_project_id=mock.sentinel.trustee_project_id) self.assertEqual(mock.sentinel.trust_id, trust_id) mock_create_ks_admin_client.assert_called_once_with() mock_create_ks_client.assert_called_once_with( token=mock.sentinel.trustee_token, project_id=mock.sentinel.trustee_project_id) mock_admin_client.session.auth.get_user_id.assert_called_once_with( mock_admin_session) mock_user_client.session.auth.get_access.assert_called_once_with( mock_user_session) mock_user_client.trusts.create.assert_called_once_with( trustor_user=mock.sentinel.trustor_user, trustee_user=mock.sentinel.trustee_user, impersonation=True, role_names=mock.sentinel.role_names, project=mock.sentinel.project_id) @mock.patch.object(auth_utils, 'create_keystone_client', autospec=True) def test_delete_trust(self, mock_ks_client): mock_auth_ref = mock.Mock(trust_id=mock.sentinel.trust_id, token=mock.sentinel.token, project_id=mock.sentinel.project_id) mock_user_session = mock.Mock(**{ 'auth.get_access.return_value': mock_auth_ref }) mock_user_client = mock.Mock( session=mock_user_session) mock_ks_client.return_value = mock_user_client auth_utils.delete_trust(mock_auth_ref) mock_user_client.trusts.delete.assert_called_once_with( mock_auth_ref.trust_id) def test_get_config_option(self): cfg.CONF.set_override('url', 'foourl', 'murano') self.assertEqual('foourl', auth_utils._get_config_option( 'murano', 'url')) def test_get_config_option_return_default(self): self.assertIsNone(auth_utils._get_config_option(None, 'url')) def test_get_session(self): mock_ka_loading = self._init_mock_cfg() session = auth_utils._get_session(mock.sentinel.auth) self.assertEqual(mock.sentinel.session, session) mock_ka_loading.load_session_from_conf_options.\ assert_called_once_with(auth=mock.sentinel.auth, conf=cfg.CONF, group=auth_utils.CFG_MURANO_AUTH_GROUP) def test_get_session_client_parameters(self): cfg.CONF.set_override('url', 'foourl', 'murano') expected_result = { 'session': mock.sentinel.session, 'endpoint_override': 'foourl' } result = auth_utils.get_session_client_parameters( conf='murano', service_type=mock.sentinel.service_type, service_name=mock.sentinel.service_name, session=mock.sentinel.session) for key, val in expected_result.items(): self.assertEqual(val, result[key]) def test_get_session_client_parameters_without_url(self): cfg.CONF.set_override('home_region', 'fooregion') expected_result = { 'session': mock.sentinel.session, 'service_type': mock.sentinel.service_type, 'service_name': mock.sentinel.service_name, 'interface': mock.sentinel.endpoint_type, 'region_name': 'fooregion' } result = auth_utils.get_session_client_parameters( service_type=mock.sentinel.service_type, service_name=mock.sentinel.service_name, interface=mock.sentinel.endpoint_type, session=mock.sentinel.session) for key, val in expected_result.items(): self.assertEqual(val, result[key]) @mock.patch.object( auth_utils, '_create_keystone_admin_client', autospec=True) def test_get_user(self, mock_create_ks_admin_client): mock_client = mock.Mock( **{'users.get.return_value.to_dict.return_value': mock.sentinel.user}) mock_create_ks_admin_client.return_value = mock_client user = auth_utils.get_user(mock.sentinel.uid) self.assertEqual(mock.sentinel.user, user) mock_client.users.get.assert_called_once_with(mock.sentinel.uid) mock_client.users.get.return_value.to_dict.assert_called_once_with() @mock.patch.object( auth_utils, '_create_keystone_admin_client', autospec=True) def test_get_project(self, mock_create_ks_admin_client): mock_client = mock.Mock( **{'projects.get.return_value.to_dict.return_value': mock.sentinel.project}) mock_create_ks_admin_client.return_value = mock_client project = auth_utils.get_project(mock.sentinel.pid) self.assertEqual(mock.sentinel.project, project) mock_client.projects.get.assert_called_once_with(mock.sentinel.pid) mock_client.projects.get.return_value.to_dict.assert_called_once_with() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/common/test_engine.py0000664000175000017500000003404400000000000022717 0ustar00zuulzuul00000000000000# Copyright 2016 AT&T Corp # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from unittest import mock from oslo_service import service from murano.common import engine from murano.dsl import constants from murano.dsl import helpers from murano.dsl import murano_package from murano.engine import mock_context_manager from murano.engine import package_loader from murano.tests.unit import base class TestEngineService(base.MuranoTestCase): def setUp(self): super(TestEngineService, self).setUp() self.engine = engine.EngineService() self.addCleanup(mock.patch.stopall) @mock.patch.object(service.Service, 'reset') @mock.patch.object(service.Service, 'stop') @mock.patch.object(service.Service, 'start') @mock.patch('murano.common.rpc.get_server') def test_start_stop_reset(self, mock_get_server, mock_start, mock_stop, mock_reset): self.engine.start() self.assertTrue(mock_get_server.called) self.assertTrue(mock_start.called) self.engine.stop() self.assertTrue(mock_stop.called) self.engine.reset() self.assertTrue(mock_reset.called) @mock.patch.object(service.Service, 'stop') @mock.patch.object(service.Service, 'start') @mock.patch('murano.common.rpc.get_server') def test_stop_graceful(self, mock_get_server, mock_start, mock_stop): self.engine.start() self.assertTrue(mock_get_server.called) self.assertTrue(mock_start.called) self.engine.stop(graceful=True) self.assertTrue(mock_stop.called) class TestTaskExecutor(base.MuranoTestCase): def setUp(self): super(TestTaskExecutor, self).setUp() self.task = { 'action': { 'args': None, 'object_id': 'my_obj_id', 'method': 'my_method' }, 'model': { 'SystemData': { 'Packages': 'my_packages' }, 'project_id': 'my_tenant_id', 'user_id': 'my_user_id' }, 'token': 'my_token', 'project_id': 'my_tenant_id', 'user_id': 'my_user_id', 'id': 'my_env_id' } self.task_executor = engine.TaskExecutor(self.task) self.task_executor._model = self.task['model'] self.task_executor._session.token = self.task['token'] self.task_executor._session.project_id = self.task['project_id'] self.task_executor._session.user_id = self.task['user_id'] self.task_executor._session.environment_owner_project_id_ = \ self.task['model']['project_id'] self.task_executor._session.environment_owner_user_id = \ self.task['model']['user_id'] (self.task_executor._session .system_attributes) = (self.task_executor._model. get('SystemData', {})) self.addCleanup(mock.patch.stopall) def test_properties(self): self.assertEqual(self.task['action'], self.task_executor.action) self.assertEqual(self.task_executor._session, self.task_executor.session) self.assertEqual(self.task['model'], self.task_executor.model) @mock.patch('murano.common.engine.auth_utils.delete_trust') @mock.patch('murano.common.engine.auth_utils.create_trust') @mock.patch('murano.common.engine.package_loader.' 'CombinedPackageLoader.import_fixation_table') @mock.patch('murano.common.engine.TaskExecutor._execute') def test_execute(self, mock_execute, mock_loader, mock_create, mock_delete): mock_loader.return_value = {'SystemData': 'my_sys_data'} mock_create.return_value = 'trust_id' expected = { 'action': { 'result': '?', 'isException': False } } mock_execute.return_value = expected result = self.task_executor.execute() self.assertEqual(expected, result) self.assertTrue(mock_execute.called) self.assertTrue(mock_loader.called) self.assertTrue(mock_create.called) self.assertTrue(mock_delete.called) def test_private_execute(self): mock_loader = mock.Mock() result = self.task_executor._execute(mock_loader) expected = { 'action': { 'result': None, 'isException': False } } self.assertEqual(expected, result) @mock.patch('murano.common.engine.auth_utils.delete_trust') @mock.patch('murano.common.engine.auth_utils.create_trust') def test_trust(self, mock_create, mock_delete): mock_create.return_value = 'trust_id' self.task_executor._create_trust() self.assertEqual('trust_id', self.task_executor._session.trust_id) self.assertTrue(mock_create.called) self.task_executor._delete_trust() self.assertIsNone(self.task_executor._session.trust_id) self.assertTrue(mock_delete.called) class TestStaticActionExecutor(base.MuranoTestCase): def setUp(self): super(TestStaticActionExecutor, self).setUp() self.action = { 'method': 'TestAction', 'args': {'name': 'foo'}, 'class_name': 'TestClass', 'pkg_name': 'TestPackage', 'class_version': '=0' } self.task = { 'action': self.action, 'token': 'test_token', 'project_id': 'test_tenant', 'user_id': 'test_user', 'id': 'test_task_id' } self.task_executor = engine.StaticActionExecutor(self.task) self.assertIsInstance(self.task_executor._reporter, engine.status_reporter.StatusReporter) self.assertEqual(self.task['id'], self.task_executor._reporter._environment_id) def test_action_property(self): self.assertEqual(self.action, self.task_executor.action) def test_session_property(self): self.assertIsInstance(self.task_executor.session, engine.execution_session.ExecutionSession) self.assertEqual('test_token', self.task_executor.session.token) self.assertEqual('test_tenant', self.task_executor.session.project_id) self.assertIsInstance(self.task_executor._model_policy_enforcer, engine.enforcer.ModelPolicyEnforcer) self.assertEqual( self.task_executor.session, self.task_executor._model_policy_enforcer._execution_session) @mock.patch.object(engine, 'serializer') @mock.patch.object(engine.dsl_executor.MuranoDslExecutor, 'package_loader') def test_execute(self, mock_package_loader, mock_serializer): mock_class = mock.Mock() mock_package = mock.Mock(spec=murano_package.MuranoPackage) mock_package.find_class.return_value = mock_class mock_package_loader.load_package.return_value = mock_package version_spec = helpers.parse_version_spec(self.action['class_version']) self.task_executor.execute() mock_package_loader.load_package.assert_called_once_with( 'TestPackage', version_spec) mock_package.find_class.assert_called_once_with( self.action['class_name'], search_requirements=False) mock_class.invoke.assert_called_once_with('TestAction', None, (), {'name': 'foo'}) self.assertTrue(mock_serializer.serialize.called) @mock.patch.object(engine, 'serializer') @mock.patch.object(engine.dsl_executor.MuranoDslExecutor, 'package_loader') def test_execute_without_package_name(self, mock_package_loader, mock_serializer): mock_class = mock.Mock() mock_package = mock.Mock(spec=murano_package.MuranoPackage) mock_package.find_class.return_value = mock_class mock_package_loader.load_class_package.return_value = mock_package version_spec = helpers.parse_version_spec(self.action['class_version']) self.task_executor.action['pkg_name'] = None self.task_executor.execute() mock_package_loader.load_class_package.assert_called_once_with( 'TestClass', version_spec) mock_package.find_class.assert_called_once_with( self.action['class_name'], search_requirements=False) mock_class.invoke.assert_called_once_with('TestAction', None, (), {'name': 'foo'}) self.assertTrue(mock_serializer.serialize.called) class TestSchemaEndpoint(base.MuranoTestCase): def setUp(self): super(TestSchemaEndpoint, self).setUp() context_manager = mock_context_manager.MockContextManager() self.context = context_manager.create_root_context( constants.RUNTIME_VERSION_1_5) @mock.patch('murano.common.engine.schema_generator') @mock.patch('murano.common.engine.package_loader') def test_generate_schema(self, mock_package_loader, mock_schema_generator): mock_pkg_loader = mock.Mock( spec=package_loader.CombinedPackageLoader) mock_package_loader.CombinedPackageLoader().__enter__.return_value =\ mock_pkg_loader mock_schema_generator.generate_schema.return_value = 'test_schema' arg1, arg2, arg3 = mock.Mock(), mock.Mock(), mock.Mock() test_args = (arg1, arg2, arg3) test_kwargs = {'foo': 'bar', 'class_name': 'test_class_name'} result = engine.SchemaEndpoint.generate_schema( self.context, *test_args, **test_kwargs) self.assertEqual('test_schema', result) mock_schema_generator.generate_schema.assert_called_once_with( mock_pkg_loader, mock.ANY, *test_args, **test_kwargs) class TestTaskProcessingEndpoint(base.MuranoTestCase): def setUp(self): super(TestTaskProcessingEndpoint, self).setUp() self.action = { 'method': 'TestAction', 'args': {'name': 'foo'}, 'class_name': 'TestClass', 'pkg_name': 'TestPackage', 'class_version': '=0', 'object_id': 'test_object_id' } self.task = { 'action': self.action, 'model': { 'SystemData': {'TrustId': 'test_trust_id'}, 'project_id': 'test_tenant', 'user_id': 'test_user' }, 'token': 'test_token', 'project_id': 'test_tenant', 'user_id': 'test_user', 'id': 'test_task_id' } context_manager = mock_context_manager.MockContextManager() self.context = context_manager.create_root_context( constants.RUNTIME_VERSION_1_5) @mock.patch.object(engine.TaskExecutor, '_delete_trust') @mock.patch.object(engine, 'rpc') @mock.patch.object(engine.dsl_executor.MuranoDslExecutor, 'finalize') @mock.patch.object(engine, 'LOG') def test_handle_task(self, mock_log, mock_finalize, mock_rpc, mock_delete_trust): mock_finalize.return_value = self.task['model'] handle_task = engine.TaskProcessingEndpoint.handle_task handle_task(self.context, self.task) mock_delete_trust.assert_called_once_with() mock_rpc.api().process_result.assert_called_once_with( mock.ANY, 'test_task_id') self.assertEqual(2, mock_log.info.call_count) self.assertIn('Starting processing task:', str(mock_log.info.mock_calls[0])) self.assertIn('Finished processing task:', str(mock_log.info.mock_calls[1])) class TestStaticActionEndpoint(base.MuranoTestCase): def setUp(self): super(TestStaticActionEndpoint, self).setUp() self.action = { 'method': 'TestAction', 'args': {'name': 'foo'}, 'class_name': 'TestClass', 'pkg_name': 'TestPackage', 'class_version': '=0', 'object_id': 'test_object_id' } self.task = { 'action': self.action, 'model': {'SystemData': {'TrustId': 'test_trust_id'}}, 'token': 'test_token', 'project_id': 'test_tenant', 'user_id': 'test_user', 'id': 'test_task_id' } context_manager = mock_context_manager.MockContextManager() self.context = context_manager.create_root_context( constants.RUNTIME_VERSION_1_5) @mock.patch('murano.dsl.serializer.serialize') @mock.patch('murano.common.engine.package_loader') @mock.patch.object(engine, 'LOG') def test_call_static_action(self, mock_log, mock_package_loader, mock_serialize): mock_pkg_loader = mock.Mock( spec=package_loader.CombinedPackageLoader) mock_package_loader.CombinedPackageLoader().__enter__.return_value =\ mock_pkg_loader expected = { 'Objects': ['foo'], 'ObjectsCopy': ['bar'], 'Attributes': ['baz'] } mock_serialize.return_value = expected call_static_action = engine.StaticActionEndpoint.call_static_action result = call_static_action(self.context, self.task) self.assertEqual(expected, result) self.assertEqual(2, mock_log.info.call_count) self.assertIn('Starting execution of static action:', str(mock_log.info.mock_calls[0])) self.assertIn('Finished execution of static action:', str(mock_log.info.mock_calls[1])) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/common/test_plugin_loader.py0000664000175000017500000000743200000000000024277 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from unittest import mock from oslo_config import cfg from murano.common.plugins import extensions_loader from murano.tests.unit import base CONF = cfg.CONF class PluginLoaderTest(base.MuranoTestCase): @mock.patch('stevedore.extension.Extension') def test_load_extension(self, ext): """Test PluginLoader.load_extension. Check that stevedore plugin loading creates instance of PackageDefinition class, new class are added to that package and name mapping between class and plugin are updated. """ ext.entry_point.dist.project_name = 'plugin1' ext.entry_point.name = 'Test' name_map = {} test_obj = extensions_loader.PluginLoader('test.namespace') test_obj.load_extension(ext, name_map) self.assertEqual(1, len(test_obj.packages)) loaded_pkg = list(test_obj.packages.values())[0] self.assertIsInstance(loaded_pkg, extensions_loader.PackageDefinition) self.assertEqual('Test', list(loaded_pkg.classes.keys())[0]) result = {'Test': list(test_obj.packages.keys())} self.assertEqual(result, name_map) def test_cleanup_duplicates(self): """Test PluginLoader.cleanup_duplicates. Check loading two plugins with same 'Test1' classes inside initiates removing of all duplicated classes. """ name_map = {} ext1 = mock.MagicMock(name='ext1') ext1.entry_point.name = 'Test1' ext2 = mock.MagicMock(name='ext2') ext2.entry_point.name = 'Test1' test_obj = extensions_loader.PluginLoader() test_obj.load_extension(ext1, name_map) test_obj.load_extension(ext2, name_map) dist1 = ext1.entry_point.dist dist2 = ext2.entry_point.dist self.assertEqual(1, len(test_obj.packages[str(dist1)].classes)) self.assertEqual(1, len(test_obj.packages[str(dist2)].classes)) test_obj.cleanup_duplicates(name_map) self.assertEqual(0, len(test_obj.packages[str(dist1)].classes)) self.assertEqual(0, len(test_obj.packages[str(dist2)].classes)) def test_load_plugin_with_inappropriate_class_name(self): """Negative test load_extension. Check plugin that contains incorrect MuranoPL class name won't be loaded. """ name_map = {} ext = mock.MagicMock(name='ext') ext.entry_point.name = 'murano-pl-class' test_obj = extensions_loader.PluginLoader() test_obj.load_extension(ext, name_map) # No packages are loaded self.assertEqual(0, len(test_obj.packages)) @mock.patch('stevedore.extension.Extension') def test_is_plugin_enabled(self, ext): """Test is_plugin_enabled. Check that only plugins specified in config file can be loaded. """ self.override_config('enabled_plugins', 'plugin1, plugin2', group='murano') ext.entry_point.dist.project_name = 'test' test_method = extensions_loader.PluginLoader.is_plugin_enabled self.assertFalse(test_method(ext)) ext.entry_point.dist.project_name = 'plugin1' self.assertTrue(test_method(ext)) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/common/test_server.py0000664000175000017500000003205300000000000022756 0ustar00zuulzuul00000000000000# Copyright 2016 AT&T Corp # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from datetime import datetime from unittest import mock from murano.common import server from murano.services import states from murano.tests.unit import base from murano.tests.unit import utils as test_utils class ServerTest(base.MuranoTestCase): @classmethod def setUpClass(cls): super(ServerTest, cls).setUpClass() cls.result_endpoint = server.ResultEndpoint() cls.dummy_context = test_utils.dummy_context() @mock.patch('murano.common.server.status_reporter.get_notifier') @mock.patch('murano.common.server.LOG') @mock.patch('murano.common.server.get_last_deployment') @mock.patch('murano.common.server.models') @mock.patch('murano.common.server.session') def test_process_result(self, mock_db_session, mock_models, mock_last_deployment, mock_log, mock_notifier): test_result = { 'model': { 'Objects': { 'applications': ['app1', 'app2'], 'services': ['service1', 'service2'] } }, 'action': { 'isException': False } } mock_env = mock.MagicMock(id='test_env_id', tenant_id='test_tenant_id', description=None, version=1) mock_db_session.get_session().query().get.return_value = mock_env mock_db_session.get_session().query().filter_by().count.\ return_value = 0 self.result_endpoint.process_result(self.dummy_context, test_result, 'test_env_id') self.assertEqual(mock_env.description, test_result['model']) self.assertEqual(2, mock_env.version) self.assertEqual(test_result['action'], mock_last_deployment().result) self.assertEqual('Deployment finished', mock_models.Status().text) self.assertEqual('info', mock_models.Status().level) mock_last_deployment().statuses.append.assert_called_once_with( mock_models.Status()) mock_db_session.get_session().query().filter_by.assert_any_call( **{'environment_id': mock_env.id, 'state': states.SessionState.DEPLOYING}) self.assertEqual( states.SessionState.DEPLOYED, mock_db_session.get_session().query().filter_by().first().state) mock_log.info.assert_called_once_with( 'EnvId: {env_id} TenantId: {tenant_id} Status: ' 'Successful Apps: {services}' .format(env_id=mock_env.id, tenant_id=mock_env.tenant_id, services=test_result['model']['Objects']['services'])) mock_notifier.return_value.report.assert_called_once_with( 'environment.deploy.end', mock_db_session.get_session().query().get(mock_env.id).to_dict()) @mock.patch('murano.common.server.LOG') @mock.patch('murano.common.server.get_last_deployment') @mock.patch('murano.common.server.models') @mock.patch('murano.common.server.session') def test_process_result_with_errors(self, mock_db_session, mock_models, mock_last_deployment, mock_log): test_result = { 'model': { 'Objects': { 'applications': ['app1', 'app2'], 'services': ['service1', 'service2'] } }, 'action': { 'isException': True } } mock_env = mock.MagicMock(id='test_env_id', tenant_id='test_tenant_id', description=None, version=1) mock_db_session.get_session().query().get.return_value = mock_env mock_db_session.get_session().query().filter_by().count.\ return_value = 1 self.result_endpoint.process_result(self.dummy_context, test_result, 'test_env_id') self.assertEqual(mock_env.description, test_result['model']) self.assertEqual(test_result['action'], mock_last_deployment().result) self.assertEqual('Deployment finished with errors', mock_models.Status().text) mock_last_deployment().statuses.append.assert_called_once_with( mock_models.Status()) mock_db_session.get_session().query().filter_by.assert_any_call( **{'environment_id': mock_env.id, 'state': states.SessionState.DEPLOYING}) self.assertEqual( states.SessionState.DEPLOY_FAILURE, mock_db_session.get_session().query().filter_by().first().state) mock_log.warning.assert_called_once_with( 'EnvId: {env_id} TenantId: {tenant_id} Status: ' 'Failed Apps: {services}' .format(env_id=mock_env.id, tenant_id=mock_env.tenant_id, services=test_result['model']['Objects']['services'])) @mock.patch('murano.common.server.LOG') @mock.patch('murano.common.server.get_last_deployment') @mock.patch('murano.common.server.models') @mock.patch('murano.common.server.session') def test_process_result_with_warnings(self, mock_db_session, mock_models, mock_last_deployment, mock_log): test_result = { 'model': { 'Objects': None, 'ObjectsCopy': ['object1', 'object2'] }, 'action': { 'isException': True } } mock_env = mock.MagicMock(id='test_env_id', tenant_id='test_tenant_id', description=None, version=1) mock_db_session.get_session().query().get.return_value = mock_env # num_errors will be initialized to 0, num_warnings to 1 mock_db_session.get_session().query().filter_by().count.\ side_effect = [0, 1] self.result_endpoint.process_result(self.dummy_context, test_result, 'test_env_id') self.assertEqual(mock_env.description, test_result['model']) self.assertEqual(test_result['action'], mock_last_deployment().result) self.assertEqual('Deletion finished with warnings', mock_models.Status().text) mock_last_deployment().statuses.append.assert_called_once_with( mock_models.Status()) mock_db_session.get_session().query().filter_by.assert_any_call( **{'environment_id': mock_env.id, 'state': states.SessionState.DELETING}) self.assertEqual( states.SessionState.DELETE_FAILURE, mock_db_session.get_session().query().filter_by().first().state) mock_log.warning.assert_called_once_with( 'EnvId: {env_id} TenantId: {tenant_id} Status: ' 'Failed Apps: {services}' .format(env_id=mock_env.id, tenant_id=mock_env.tenant_id, services=[])) @mock.patch('murano.common.server.LOG') @mock.patch('murano.common.server.session') def test_process_result_with_no_environment(self, mock_db_session, mock_log): test_result = {'model': None} mock_db_session.get_session().query().get.return_value = None result = self.result_endpoint.process_result(self.dummy_context, test_result, 'test_env_id') self.assertIsNone(result) mock_log.warning.assert_called_once_with( 'Environment result could not be handled, ' 'specified environment not found in database') @mock.patch('murano.common.server.environments') @mock.patch('murano.common.server.session') def test_process_result_with_no_objects(self, mock_db_session, mock_environments): test_result = {'model': {'Objects': None, 'ObjectsCopy': None}} result = self.result_endpoint.process_result(self.dummy_context, test_result, 'test_env_id') self.assertIsNone(result) mock_environments.EnvironmentServices.remove.assert_called_once_with( 'test_env_id') @mock.patch('murano.common.server.instances') def test_track_instance(self, mock_instances): test_payload = { 'instance': 'test_instance', 'instance_type': 'test_instance_type', 'environment': 'test_environment', 'unit_count': 'test_unit_count', 'type_name': 'test_type_name', 'type_title': 'test_type_title' } server.track_instance(test_payload) mock_instances.InstanceStatsServices.track_instance.\ assert_called_once_with(test_payload['instance'], test_payload['environment'], test_payload['instance_type'], test_payload['type_name'], test_payload['type_title'], test_payload['unit_count']) @mock.patch('murano.common.server.instances') def test_untrack_instance(self, mock_instances): test_payload = { 'instance': 'test_instance', 'environment': 'test_environment' } server.untrack_instance(test_payload) mock_instances.InstanceStatsServices.destroy_instance.\ assert_called_once_with(test_payload['instance'], test_payload['environment']) @mock.patch('murano.common.server.get_last_deployment') @mock.patch('murano.common.server.session') @mock.patch('murano.common.server.models') def test_report_notification(self, mock_models, mock_db_session, mock_last_deployment): mock_last_deployment.return_value = mock.MagicMock( id='test_deployment_id') test_report = { 'id': 'test_report_id', 'timestamp': datetime.now().isoformat(), 'created': None } server.report_notification(test_report) self.assertIsNotNone(test_report['created']) mock_models.Status().update.assert_called_once_with(test_report) self.assertEqual('test_deployment_id', mock_models.Status().task_id) mock_db_session.get_session().add.assert_called_once_with( mock_models.Status()) def test_get_last_deployment(self): mock_unit = mock.MagicMock() result = server.get_last_deployment(mock_unit, 'test_env_id') self.assertEqual(mock_unit.query().filter_by().order_by().first(), result) mock_unit.query().filter_by.assert_any_call( environment_id='test_env_id') def test_service_class(self): service = server.Service() self.assertIsNone(service.server) # Test stop server. service.server = mock.MagicMock() service.stop(graceful=True) service.server.stop.assert_called_once_with() service.server.wait.assert_called_once_with() # Test reset server. service.reset() service.server.reset.assert_called_once_with() @mock.patch('murano.common.rpc.messaging') def test_notification_service_class(self, mock_messaging): mock_server = mock.MagicMock() mock_messaging.get_notification_listener.return_value = mock_server notification_service = server.NotificationService() self.assertIsNone(notification_service.server) notification_service.start() self.assertEqual(1, mock_messaging.get_notification_listener.call_count) mock_server.start.assert_called_once_with() @mock.patch('murano.common.rpc.messaging') def test_api_service_class(self, mock_messaging): mock_server = mock.MagicMock() mock_messaging.get_rpc_server.return_value = mock_server api_service = server.ApiService() api_service.start() self.assertEqual(1, mock_messaging.get_rpc_server.call_count) mock_server.start.assert_called_once_with() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/common/test_statservice.py0000664000175000017500000002076700000000000024015 0ustar00zuulzuul00000000000000# Copyright 2016 AT&T Corp # 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. import datetime as dt import time from unittest import mock from oslo_utils import timeutils from murano.common import statservice from murano.db import models from murano.db import session as db_session from murano.services import states from murano.tests.unit import base @mock.patch('murano.common.statservice.v1.stats', request_count=11, error_count=22, average_time=33, requests_per_tenant=44) class StatsCollectingServiceTest(base.MuranoTestCase): def setUp(self): super(StatsCollectingServiceTest, self).setUp() self.service = statservice.StatsCollectingService() self.service._prev_time = 0 self.mock_new_stats = mock.MagicMock(request_count=1, error_count=2) self.service._stats_db = mock.MagicMock( get_stats_by_host=mock.MagicMock(return_value=self.mock_new_stats)) statservice.LOG = mock.MagicMock() def test_service_start_and_stop(self, _): self.assertEqual(0, len(self.service.tg.threads)) self.service.start() self.assertEqual(2, len(self.service.tg.threads)) self.service.stop() self.assertEqual(0, len(self.service.tg.threads)) @mock.patch('murano.common.statservice.time') def test_update_stats(self, mock_time, mock_stats): now = time.time() mock_time.time.return_value = now self.service.update_stats() statservice.LOG.debug.assert_any_call( 'Stats: (Requests: 11 Errors: 22 Ave.Res.Time 33.0000\n Per ' 'tenant: 44)') self.assertEqual(now, self.service._prev_time) self.assertEqual(mock_stats.request_count, self.mock_new_stats.request_count) self.assertEqual(mock_stats.error_count, self.mock_new_stats.error_count) self.assertEqual(mock_stats.average_time, self.mock_new_stats.average_response_time) self.assertEqual(str(mock_stats.requests_per_tenant), self.mock_new_stats.requests_per_tenant) self.assertEqual((11 - 1) / now, self.mock_new_stats.requests_per_second) self.assertEqual((22 - 2) / now, self.mock_new_stats.errors_per_second) self.service._stats_db.update.assert_called_once_with( self.service._hostname, self.mock_new_stats) @mock.patch('murano.common.statservice.multiprocessing.cpu_count', return_value=5) @mock.patch('murano.common.statservice.psutil.cpu_percent', return_value=12.3) def test_update_stats_with_create_stats_db(self, _, __, mock_stats): self.service._stats_db.get_stats_by_host.return_value = None result = self.service.update_stats() self.assertIsNone(result) self.service._stats_db.create.assert_called_once_with( self.service._hostname, mock_stats.request_count, mock_stats.error_count, mock_stats.average_time, mock_stats.requests_per_tenant, 5, 12.3 ) @mock.patch('murano.common.statservice.LOG') def test_update_stats_handle_exception(self, mock_log, _): self.service._stats_db.update.side_effect =\ Exception('test_error_code') self.service.update_stats() mock_log.exception.assert_called_once_with( "Failed to get statistics object from a " "database. {error_code}".format(error_code='test_error_code')) class EnvReportingTest(base.MuranoNotifyWithDBTestCase): def setUp(self): super(EnvReportingTest, self).setUp() self.service = statservice.StatsCollectingService() @mock.patch('murano.common.statservice.status_reporter.' 'Notification.report') def test_report_env_stats(self, mock_notifier): now = timeutils.utcnow() later = now + dt.timedelta(minutes=1) session = db_session.get_session() environment1 = models.Environment( name='test_environment1', tenant_id='test_tenant_id1', version=2, id='test_env_id_1', created=now, updated=later, description={ 'Objects': { 'applications': ['app1'], 'services': ['service1'] } } ) environment2 = models.Environment( name='test_environment2', tenant_id='test_tenant_id2', version=1, id='test_env_id_2', created=now, updated=later, description={ 'Objects': { 'applications': ['app2'], 'services': ['service3'] } } ) environment3 = models.Environment( name='test_environment3', tenant_id='test_tenant_id2', version=1, id='test_env_id_3', created=now, updated=later, description={} ) session_1 = models.Session( environment=environment1, user_id='test_user_id', description={}, state=states.SessionState.DEPLOYED, version=1 ) session_2 = models.Session( environment=environment2, user_id='test_user_id', description={}, state=states.SessionState.DEPLOYED, version=0 ) session_3 = models.Session( environment=environment3, user_id='test_user_id', description={}, state=states.SessionState.DEPLOY_FAILURE, version=1 ) task_1 = models.Task( id='task_id_1', environment=environment1, description={}, created=now, started=now, updated=later, finished=later ) task_2 = models.Task( id='task_id_2', environment=environment2, description={}, created=now, started=now, updated=later, finished=later ) task_3 = models.Task( id='task_id_3', environment=environment3, description={}, created=now, started=now, updated=later, finished=later ) status_1 = models.Status( id='status_id_1', task_id='task_id_1', text='Deployed', level='info' ) status_2 = models.Status( id='status_id_2', task_id='task_id_2', text='Deployed', level='info' ) status_3 = models.Status( id='status_id_3', task_id='task_id_3', text='Something was wrong', level='error' ) session.add_all([environment1, environment2, environment3]) session.add_all([session_1, session_2, session_3]) session.add_all([task_1, task_2, task_3]) session.add_all([status_1, status_2, status_3]) session.flush() self.service.report_env_stats() self.assertEqual(mock_notifier.call_count, 2) dict_env_1 = {'version': 2, 'updated': later, 'tenant_id': u'test_tenant_id1', 'created': now, 'description_text': u'', 'status': 'ready', 'id': u'test_env_id_1', 'name': u'test_environment1'} dict_env_2 = {'version': 1, 'updated': later, 'tenant_id': u'test_tenant_id2', 'created': now, 'description_text': u'', 'status': 'ready', 'id': u'test_env_id_2', 'name': u'test_environment2'} calls = [mock.call('environment.exists', dict_env_1), mock.call('environment.exists', dict_env_2)] mock_notifier.assert_has_calls(calls) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/common/test_traverse_helper.py0000664000175000017500000001160500000000000024642 0ustar00zuulzuul00000000000000# 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 murano.common import utils from murano.tests.unit import base class TraverseHelperTests(base.MuranoTestCase): def test_root_get_with_dict(self): source = {'attr': True} value = utils.TraverseHelper.get('/', source) self.assertEqual(value, source) def test_root_get_with_list(self): source = [{'attr': True}] value = utils.TraverseHelper.get('/', source) self.assertListEqual(value, source) def test_root_get_with_value_type(self): source = 'source' value = utils.TraverseHelper.get('/', source) self.assertEqual(source, value) def test_attribute_get(self): source = {'attr': True} value = utils.TraverseHelper.get('/attr', source) self.assertTrue(value) def test_nested_attribute_get(self): source = {'obj': {'attr': True}} value = utils.TraverseHelper.get('/obj/attr', source) self.assertTrue(value) def test_list_item_attribute_get(self): source = {'obj': [ {'?': {'id': '1'}, 'value': 1}, {'?': {'id': '2s'}, 'value': 2}, ]} value = utils.TraverseHelper.get('/obj/2s/value', source) self.assertEqual(2, value) def test_list_item_attribute_get_by_index(self): source = {'obj': [ {'?': {'id': 'guid1'}, 'value': 1}, {'?': {'id': 'guid2'}, 'value': 2} ]} value = utils.TraverseHelper.get('/obj/1/value', source) self.assertEqual(2, value) def test_attribute_set(self): source = {'attr': True} utils.TraverseHelper.update('/newAttr', False, source) value = utils.TraverseHelper.get('/newAttr', source) self.assertFalse(value) def test_attribute_update(self): source = {'attr': True} utils.TraverseHelper.update('/attr', False, source) value = utils.TraverseHelper.get('/attr', source) self.assertFalse(value) def test_nested_attribute_update(self): source = {'obj': {'attr': True}} utils.TraverseHelper.update('/obj/attr', False, source) value = utils.TraverseHelper.get('/obj/attr', source) self.assertFalse(value) def test_adding_item_to_list(self): source = {'attr': [1, 2, 3]} utils.TraverseHelper.insert('/attr', 4, source) value = utils.TraverseHelper.get('/attr', source) self.assertListEqual(value, [1, 2, 3, 4]) def test_nested_adding_item_to_list(self): source = {'obj': {'attr': [1, 2, 3]}} utils.TraverseHelper.insert('/obj/attr', 4, source) value = utils.TraverseHelper.get('/obj/attr', source) self.assertListEqual(value, [1, 2, 3, 4]) def test_extending_list_with_list(self): source = {'attr': [1, 2, 3]} utils.TraverseHelper.extend('/attr', [4, 5], source) value = utils.TraverseHelper.get('/attr', source) self.assertListEqual(value, [1, 2, 3, 4, 5]) def test_nested_extending_list_with_list(self): source = {'obj': {'attr': [1, 2, 3]}} utils.TraverseHelper.extend('/obj/attr', [4, 5], source) value = utils.TraverseHelper.get('/obj/attr', source) self.assertListEqual(value, [1, 2, 3, 4, 5]) def test_attribute_remove_from_dict(self): source = {'attr1': False, 'attr2': True} utils.TraverseHelper.remove('/attr1', source) value = utils.TraverseHelper.get('/', source) self.assertEqual(value, {'attr2': True}) def test_nested_attribute_remove_from_dict(self): source = {'obj': {'attr1': False, 'attr2': True}} utils.TraverseHelper.remove('/obj/attr1', source) value = utils.TraverseHelper.get('/obj', source) self.assertEqual(value, {'attr2': True}) def test_nested_attribute_remove_from_list_by_id(self): source = {'obj': [{'?': {'id': 'id1'}}, {'?': {'id': 'id2'}}]} utils.TraverseHelper.remove('/obj/id1', source) value = utils.TraverseHelper.get('/obj', source) self.assertListEqual(value, [{'?': {'id': 'id2'}}]) def test_nested_attribute_remove_from_list_by_index(self): source = {'obj': [{'?': {'id': 'id1'}}, {'?': {'id': 'id2'}}]} utils.TraverseHelper.remove('/obj/0', source) value = utils.TraverseHelper.get('/obj', source) self.assertListEqual(value, [{'?': {'id': 'id2'}}]) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/common/test_utils.py0000664000175000017500000001064600000000000022614 0ustar00zuulzuul00000000000000# 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 datetime import json from murano.common import utils from murano.tests.unit import base class UtilsTests(base.MuranoTestCase): def test_validate_quotes(self): self.assertTrue(utils.validate_quotes('"ab"')) def test_validate_quotes_not_closed_quotes(self): self.assertRaises(ValueError, utils.validate_quotes, '"ab","b""') def test_validate_quotes_not_opened_quotes(self): self.assertRaises(ValueError, utils.validate_quotes, '""ab","b"') def test_validate_quotes_no_coma_before_opening_quotes(self): self.assertRaises(ValueError, utils.validate_quotes, '"ab""b"') def test_split_for_quotes(self): self.assertEqual(["a,b", "ac"], utils.split_for_quotes('"a,b","ac"')) def test_split_for_quotes_with_backslash(self): self.assertEqual(['a"bc', 'de', 'fg,h', r'klm\\', '"nop'], utils.split_for_quotes(r'"a\"bc","de",' r'"fg,h","klm\\","\"nop"')) def test_validate_body(self): json_schema = json.dumps(['foo', {'bar': ('baz', None, 1.0, 2)}]) self.assertIsNotNone(utils.validate_body(json_schema)) json_schema = json.dumps(['body', {'body': ('baz', None, 1.0, 2)}]) self.assertIsNotNone(utils.validate_body(json_schema)) def test_build_entity_map(self): entity = {"?": {"fun": "id"}} self.assertEqual({}, utils.build_entity_map(entity)) entity = {"?": {"id": "id"}} self.assertEqual({'id': {'?': {'id': 'id'}}}, utils.build_entity_map(entity)) entity = [{"?": {"id": "id1"}}, {"?": {"id": "id2"}}] self.assertEqual({'id1': {'?': {'id': 'id1'}}, 'id2': {'?': {'id': 'id2'}}}, utils.build_entity_map(entity)) def test_is_different(self): t1 = "Hello" t2 = "World" self.assertTrue(utils.is_different(t1, t2)) t1 = "Hello" t2 = "Hello" self.assertFalse(utils.is_different(t1, t2)) t1 = {1, 2, 3, 4} t2 = t1 self.assertFalse(utils.is_different(t1, t2)) t2 = {1, 2, 3} self.assertTrue(utils.is_different(t1, t2)) t1 = [1, 2, {1, 2, 3, 4}] t1[0] = t1 self.assertTrue(utils.is_different(t1, t2)) t1 = [t2] t2 = [t1] self.assertTrue(utils.is_different(t1, t2)) t1 = [{1, 2, 3}, {1, 2, 3}] t2 = [{1, 2, 3}, {1, 2}] self.assertTrue(utils.is_different(t1, t2)) t1 = datetime.date(2016, 8, 8) t2 = datetime.date(2016, 8, 7) self.assertTrue(utils.is_different(t1, t2)) t1 = {1: 1, 2: 2, 3: 3} t2 = {1: 1, 2: 4, 3: 3} self.assertTrue(utils.is_different(t1, t2)) t1 = {1: 1, 2: 2, 3: 3, 4: {"a": "hello", "b": [1, 2, 3]}} t2 = {1: 1, 2: 2, 3: 3, 4: {"a": "hello", "b": "world\n\n\nEnd"}} self.assertTrue(utils.is_different(t1, t2)) t1 = {1: 1, 2: 2, 3: 3, 4: {"a": "hello", "b": [1, 2, 5]}} t2 = {1: 1, 2: 2, 3: 3, 4: {"a": "hello", "b": [1, 3, 2, 5]}} self.assertTrue(utils.is_different(t1, t2)) class ClassA(object): __slots__ = ['x', 'y'] def __init__(self, x, y): self.x = x self.y = y t1 = ClassA(1, 1) t2 = ClassA(1, 2) self.assertTrue(utils.is_different(t1, t2)) t1 = [1, 2, 3] t1.append(t1) t2 = [1, 2, 4] t2.append(t2) self.assertTrue(utils.is_different(t1, t2)) t1 = [1, 2, 3] t2 = [1, 2, 4] t2.append(t1) t1.append(t2) self.assertTrue(utils.is_different(t1, t2)) t1 = utils t2 = datetime self.assertTrue(utils.is_different(t1, t2)) t2 = "Not a module" self.assertTrue(utils.is_different(t1, t2)) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/common/test_wsgi.py0000664000175000017500000005651700000000000022434 0ustar00zuulzuul00000000000000# Copyright (c) 2016 AT&T inc. # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import errno import eventlet import socket from unittest import mock import webob from oslo_config import cfg from xml.dom import minidom from murano.common import exceptions from murano.common.i18n import _ from murano.common import wsgi from murano.tests.unit import base CONF = cfg.CONF class TestServiceBrokerResponseSerializer(base.MuranoTestCase): def test_service_broker_response_serializer(self): self.sbrs = wsgi.ServiceBrokerResponseSerializer() result = self.sbrs.serialize("test_response", "application/json") self.assertEqual(200, result.status_code) response = webob.Response("test_body") response.data = "test_data" result = self.sbrs.serialize(response, "application/json") self.assertEqual(200, result.status_code) class TestResponseSerializer(base.MuranoTestCase): def test_response_serializer(self): self.response_serializer = wsgi.ResponseSerializer(None) self.assertRaises(exceptions.UnsupportedContentType, self.response_serializer.get_body_serializer, "not_valid") class TestRequestDeserializer(base.MuranoTestCase): def test_request_deserializer_deserialize_body(self): self.request_deserializer = wsgi.RequestDeserializer() request = mock.Mock() request.get_content_type.side_effect = ( exceptions.UnsupportedContentType ) request.body = "body" self.assertRaises(exceptions.UnsupportedContentType, self.request_deserializer.deserialize_body, request, "act") request.get_content_type.side_effect = None request.get_content_type.return_value = None result = self.request_deserializer.deserialize_body(request, "act") self.assertEqual({}, result) request.get_content_type.return_value = "" self.assertRaises(exceptions.UnsupportedContentType, self.request_deserializer.deserialize_body, request, "act") def test_request_deserializer_get_action_args(self): self.request_deserializer = wsgi.RequestDeserializer() request_environment = [] result = self.request_deserializer.get_action_args(request_environment) self.assertEqual({}, result) request_environment = {'wsgiorg.routing_args': ["", {}]} result = self.request_deserializer.get_action_args(request_environment) self.assertEqual({}, result) class TestService(base.MuranoTestCase): def setUp(self): super(TestService, self).setUp() # Greatly speed up the time it takes to run the tests that call # wsgi._get_socket below: retry_until will be set to 31, the 1st # iteration of the while loop will execute because it will evaluate to # 30 < 31, but 2nd one will not because it will evaluate to 31 < 31. self.mock_time = mock.patch.object( wsgi, 'time', **{'time.side_effect': [1, 30, 31]}).start() self.addCleanup(mock.patch.stopall) @mock.patch("murano.common.wsgi.socket") def test_service_get_socket(self, socket): self.service = wsgi.Service(None, 1) new_socket = self.service._get_socket(None, None, None) self.assertIsInstance(new_socket, eventlet.greenio.base.GreenSocket) self.mock_time.time.side_effect = [1, 30, 31] socket.TCP_KEEPIDLE = True new_socket_2 = self.service._get_socket(None, None, None) self.assertIsInstance(new_socket_2, eventlet.greenio.base.GreenSocket) @mock.patch("murano.common.wsgi.socket") @mock.patch("murano.common.wsgi.sslutils") def test_service_get_socket_sslutils_enabled(self, sslutils, mock_socket): self.service = wsgi.Service(None, 1) new_socket = self.service._get_socket(None, None, None) self.assertIsNotNone(new_socket) @mock.patch("murano.common.wsgi.socket") @mock.patch("murano.common.wsgi.eventlet") def test_service_get_socket_no_bind(self, eventlet, mock_socket): self.service = wsgi.Service(None, 1) eventlet.listen.return_value = None self.assertRaises(RuntimeError, self.service._get_socket, None, None, None) @mock.patch("murano.common.wsgi.socket") @mock.patch("murano.common.wsgi.eventlet") def test_service_get_socket_os_error(self, eventlet, mock_socket): mock_socket.error = socket.error self.service = wsgi.Service(None, 1) sock_err = socket.error(1) eventlet.listen.side_effect = sock_err self.assertRaises(socket.error, self.service._get_socket, None, None, None) @mock.patch("murano.common.wsgi.socket") @mock.patch("murano.common.wsgi.eventlet") def test_service_get_socket_socket_error_EADDRINUSE(self, eventlet, mock_socket): mock_socket.error = socket.error self.service = wsgi.Service(None, 1) sock_err = socket.error(errno.EADDRINUSE) eventlet.listen.side_effect = sock_err self.assertRaises(RuntimeError, self.service._get_socket, None, None, None) @mock.patch("murano.common.wsgi.eventlet") def test_service_start(self, eventlet): self.service = wsgi.Service(None, 1) self.service.start() self.assertTrue(eventlet.spawn.called) def test_service_stop(self): self.service = wsgi.Service(None, 1) self.service.greenthread = mock.Mock() self.service.stop() self.assertTrue(self.service.greenthread.kill.called) def test_service_properties(self): self.service = wsgi.Service(None, 1) self.service._socket = mock.Mock() self.service._socket.getsockname.return_value = ["host", "port"] host = self.service.host port = self.service.port self.assertEqual("host", host) self.assertEqual("port", port) @mock.patch("murano.common.wsgi.logging") def test_service_reset(self, logging): self.service = wsgi.Service(None, 1) self.service.reset() self.assertTrue(logging.setup.called) @mock.patch("murano.common.wsgi.eventlet") def test_service_run(self, eventlet): self.service = wsgi.Service(None, 1) self.service._run(None, None) self.assertTrue(eventlet.wsgi.server.called) def test_backlog_prop(self): service = wsgi.Service(None, None) service._backlog = mock.sentinel.backlog self.assertEqual(mock.sentinel.backlog, service.backlog) class TestMiddleware(base.MuranoTestCase): def test_middleware_call(self): self.middleware = wsgi.Middleware(None) mock_request = mock.Mock() mock_request.get_response.return_value = "a response" self.assertEqual("a response", self.middleware(mock_request)) def test_call_with_response(self): middleware = wsgi.Middleware(None) middleware.process_request = mock.Mock(return_value=mock.sentinel.resp) resp = middleware(mock.sentinel.req) self.assertEqual(mock.sentinel.resp, resp) middleware.process_request.assert_called_once_with(mock.sentinel.req) class TestDebug(base.MuranoTestCase): def test_debug_call(self): self.debug = wsgi.Debug(None) mock_request = mock.Mock() mock_request.environ = {"one": 1, "two": 2} mock_response = mock.Mock() mock_response.headers = {"one": 1, "two": 2} mock_request.get_response.return_value = mock_response self.assertEqual(mock_response, self.debug(mock_request)) @mock.patch('sys.stdout') def test_print_generator(self, mock_stdout): for x in wsgi.Debug.print_generator(['foo', 'bar', 'baz']): pass mock_stdout.write.assert_has_calls([ mock.call('**************************************** BODY'), mock.call('\n'), mock.call('foo'), mock.call('bar'), mock.call('baz'), mock.call(''), mock.call('\n') ]) class TestRequest(base.MuranoTestCase): @mock.patch.object(wsgi.webob.BaseRequest, 'path', **{'rsplit.return_value': ['foo.bar', 'baz']}) def test_best_match_content_type_with_multi_part_path(self, mock_path): request = wsgi.Request({}) supported_content_types = ['application/baz'] result = request.best_match_content_type(None, supported_content_types) self.assertEqual('application/baz', result) class TestResource(base.MuranoTestCase): def test_resource_call_exceptions(self): self.resource = wsgi.Resource(None) self.resource.deserialize_request = mock.Mock() self.resource.deserialize_request.side_effect = ( exceptions.UnsupportedContentType ) mock_request = mock.Mock() mock_request.headers = {} result = self.resource(mock_request) self.assertEqual(415, result.status_code) self.resource.deserialize_request.side_effect = ( exceptions.MalformedRequestBody ) result = self.resource(mock_request) self.assertEqual(400, result.status_code) self.resource.deserialize_request.side_effect = None self.resource.deserialize_request.return_value = ["", {"k": "v"}, ""] self.resource.execute_action = mock.Mock() self.resource.serialize_response = mock.Mock() self.resource.serialize_response.side_effect = Exception result = self.resource(mock_request) self.assertIsNotNone(result) def test_get_action_args(self): self.resource = wsgi.Resource(None) result = self.resource.get_action_args(None) self.assertEqual({}, result) request_environment = {'wsgiorg.routing_args': ["arg_0", {"k": "v"}]} result = self.resource.get_action_args(request_environment) self.assertEqual({"k": "v"}, result) def test_dispatch_except_attribute_error(self): mock_obj = mock.Mock(spec=wsgi.Resource) setattr(mock_obj, 'default', mock.Mock(return_value=mock.sentinel.ret_value)) resource = wsgi.Resource(None) result = resource.dispatch(mock_obj, 'invalid_action') self.assertEqual(mock.sentinel.ret_value, result) mock_obj.default.assert_called_once_with() class TestActionDispatcher(base.MuranoTestCase): def test_default(self): action_dispatcher = wsgi.ActionDispatcher() self.assertRaises(NotImplementedError, action_dispatcher.default, None) class TestDictSerializer(base.MuranoTestCase): def test_default(self): dict_serializer = wsgi.DictSerializer() self.assertEqual("", dict_serializer.default(None)) class TestXMLDictSerializer(base.MuranoTestCase): def test_router_dispatch_not_found(self): self.router = wsgi.Router(None) req = mock.Mock() req.environ = {'wsgiorg.routing_args': [False, False]} response = self.router._dispatch(req) self.assertIsInstance(response, webob.exc.HTTPNotFound) def test_XML_Dict_Serializer_default_string(self): self.serializer = wsgi.XMLDictSerializer() actual_xml = self.serializer.default({"XML Root": "some data"}) expected_xml = b'some data\n' self.assertEqual(expected_xml, actual_xml) def test_XML_Dict_Serializer_node_dict(self): self.serializer = wsgi.XMLDictSerializer() doc = minidom.Document() metadata = mock.Mock() metadata.get.return_value = {"node": {"item_name": "name", "item_key": "key"}} nodename = "node" data = {"data_key": "data_value"} result = self.serializer._to_xml_node(doc, metadata, nodename, data) self.assertEqual('data_value', result.childNodes[0].toxml()) mock_get_nodename = mock.Mock() mock_get_nodename.get.return_vale = "k" metadata.get.return_value = {"k": "v"} nodename = "node" data = {"data_key": "data_value"} result = self.serializer._to_xml_node(doc, metadata, nodename, data) self.assertEqual("node", result.nodeName) metadata.get.return_value = {} nodename = "node" data = {"data_key": "data_value"} result = self.serializer._to_xml_node(doc, metadata, nodename, data) self.assertEqual('data_value', result.childNodes[0].toxml()) def test_XML_Dict_Serializer_node_list(self): self.serializer = wsgi.XMLDictSerializer() doc = minidom.Document() metadata = mock.Mock() metadata.get.return_value = {"node": {"item_name": "name", "item_key": "key"}} nodename = "node" data = ["data_1", "data_2", "data_3"] result = self.serializer._to_xml_node(doc, metadata, nodename, data) self.assertEqual('', result.childNodes[0].toxml()) nodename = "not_node" data = ["data_1", "data_2", "data_3"] result = self.serializer._to_xml_node(doc, metadata, nodename, data) self.assertEqual(3, len(result.childNodes)) nodename = "nodes" data = ["data_1", "data_2", "data_3"] result = self.serializer._to_xml_node(doc, metadata, nodename, data) self.assertEqual(3, len(result.childNodes)) def test_XML_Dict_Serializer_create_link_nodes(self): self.serializer = wsgi.XMLDictSerializer() xml_doc = minidom.Document() links = [{"rel": "rel", "href": "href", "type": "type"}] link_nodes = self.serializer._create_link_nodes(xml_doc, links) result = link_nodes[0].toxml() for item in ['down>up mode. """ env = alembic_script.ScriptDirectory.from_config(self.ALEMBIC_CONFIG) versions = [] for rev in env.walk_revisions(): versions.append((rev.revision, rev.down_revision or '-1')) versions.reverse() return versions def walk_versions(self, engine=None, snake_walk=False, downgrade=True): # Determine latest version script from the repo, then # upgrade from 1 through to the latest, with no data # in the databases. This just checks that the schema itself # upgrades successfully. self._configure(engine) up_and_down_versions = self._up_and_down_versions() for ver_up, ver_down in up_and_down_versions: # upgrade -> downgrade -> upgrade self._migrate_up(engine, ver_up, with_data=True) if snake_walk: downgraded = self._migrate_down(engine, ver_down, with_data=True, next_version=ver_up) if downgraded: self._migrate_up(engine, ver_up) if downgrade: # Now walk it back down to 0 from the latest, testing # the downgrade paths. up_and_down_versions.reverse() for ver_up, ver_down in up_and_down_versions: # downgrade -> upgrade -> downgrade downgraded = self._migrate_down(engine, ver_down, next_version=ver_up) if snake_walk and downgraded: self._migrate_up(engine, ver_up) self._migrate_down(engine, ver_down, next_version=ver_up) def _get_version_from_db(self, engine): """Fetches latest version of migrate repo from database For each type of migrate repo, the latest version from db will be returned. """ conn = engine.connect() try: context = migration.MigrationContext.configure(conn) version = context.get_current_revision() or '-1' finally: conn.close() return version def _migrate(self, engine, version, cmd): """Base method for manipulation with migrate repo It will upgrade or downgrade the actual database. """ self._alembic_command(cmd, engine, self.ALEMBIC_CONFIG, version) def _migrate_down(self, engine, version, with_data=False, next_version=None): try: self._migrate(engine, version, 'downgrade') except NotImplementedError: # NOTE(sirp): some migrations, namely release-level # migrations, don't support a downgrade. return False self.assertEqual(version, self._get_version_from_db(engine)) # NOTE(sirp): `version` is what we're downgrading to (i.e. the 'target' # version). So if we have any downgrade checks, they need to be run for # the previous (higher numbered) migration. if with_data: post_downgrade = getattr( self, "_post_downgrade_%s" % next_version, None) if post_downgrade: post_downgrade(engine) return True def _migrate_up(self, engine, version, with_data=False): """Migrate up to a new version of the db. We allow for data insertion and post checks at every migration version with special _pre_upgrade_### and _check_### functions in the main test. """ # NOTE(sdague): try block is here because it's impossible to debug # where a failed data migration happens otherwise check_version = version try: if with_data: data = None pre_upgrade = getattr( self, "_pre_upgrade_%s" % check_version, None) if pre_upgrade: data = pre_upgrade(engine) self._migrate(engine, version, 'upgrade') self.assertEqual(version, self._get_version_from_db(engine)) if with_data: check = getattr(self, "_check_%s" % check_version, None) if check: check(engine, data) except Exception: LOG.error("Failed to migrate to version {ver} on engine {eng}" .format(ver=version, eng=engine)) raise ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1696417899.825181 murano-16.0.0/murano/tests/unit/db/services/0000775000175000017500000000000000000000000020754 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/db/services/__init__.py0000664000175000017500000000000000000000000023053 0ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/db/services/environment_templates.py0000664000175000017500000001400000000000000025743 0ustar00zuulzuul00000000000000# Copyright (c) 2015 Telefonica I+D. # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT 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 fixtures class EmptyEnvironmentFixture(fixtures.Fixture): def setUp(self): super(EmptyEnvironmentFixture, self).setUp() self.env_desc = { "tenant_id": "tenant_id", "name": "my_environment", "id": "template_id" } self.addCleanup(delattr, self, 'env_desc') class EmptyEnvironmentTemplateFixture(fixtures.Fixture): def setUp(self): super(EmptyEnvironmentTemplateFixture, self).setUp() self.environment_template_desc = { "tenant_id": "tenant_id", "name": "my_template", "id": "template_id" } self.addCleanup(delattr, self, 'environment_template_desc') class AppEnvTemplateFixture(fixtures.Fixture): def setUp(self): super(AppEnvTemplateFixture, self).setUp() self.env_template_desc = \ { "services": [ { "instance": { "assignFloatingIp": "true", "keyname": "mykeyname", "image": "cloud-fedora-v3", "flavor": "m1.medium", "?": { "type": "io.murano.resources.LinuxInstance", "id": "ef984a74-29a4-45c0-b1dc-2ab9f075732e" } }, "name": "orion", "?": { "_26411a1861294160833743e45d0eaad9": { "name": "tomcat" }, "type": "io.murano.apps.apache.Tomcat", "id": "tomcat_id" }, "port": "8080" }, { "instance": "ef984a74-29a4-45c0-b1dc-2ab9f075732e", "password": "XXX", "name": "mysql", "?": { "_26411a1861294160833743e45d0eaad9": { "name": "mysql" }, "type": "io.murano.apps.database.MySQL", "id": "54aaa43d-5970" } } ], "tenant_id": "tenant_id", "name": "template_name", 'id': 'template_id' } self.addCleanup(delattr, self, 'env_template_desc') class ApplicationsFixture(fixtures.Fixture): def setUp(self): super(ApplicationsFixture, self).setUp() self.applications_desc = [ { "instance": { "assignFloatingIp": "true", "keyname": "mykeyname", "image": "cloud-fedora-v3", "flavor": "m1.medium", "?": { "type": "io.murano.resources.LinuxInstance", "id": "ef984a74-29a4-45c0-b1dc-2ab9f075732e" } }, "name": "orion", "?": { "_26411a1861294160833743e45d0eaad9": { "name": "tomcat" }, "type": "io.murano.apps.apache.Tomcat", "id": "tomcat_id" }, "port": "8080" }, { "instance": "ef984a74-29a4-45c0-b1dc-2ab9f075732e", "password": "XXX", "name": "mysql", "?": { "_26411a1861294160833743e45d0eaad9": { "name": "mysql" }, "type": "io.murano.apps.database.MySQL", "id": "54aaa43d-5970" } } ] self.addCleanup(delattr, self, 'applications_desc') class ApplicationTomcatFixture(fixtures.Fixture): def setUp(self): super(ApplicationTomcatFixture, self).setUp() self.application_tomcat_desc = { "instance": { "assignFloatingIp": "true", "keyname": "mykeyname", "image": "cloud-fedora-v3", "flavor": "m1.medium", "?": { "type": "io.murano.resources.LinuxInstance", "id": "ef984a74-29a4-45c0-b1dc-2ab9f075732e" } }, "name": "orion", "?": { "_26411a1861294160833743e45d0eaad9": { "name": "tomcat" }, "type": "io.murano.apps.apache.Tomcat", "id": "tomcat_id" }, "port": "8080" } self.addCleanup(delattr, self, 'application_tomcat_desc') class ApplicationMysqlFixture(fixtures.Fixture): def setUp(self): super(ApplicationMysqlFixture, self).setUp() self.application_mysql_desc = { "instance": "ef984a74-29a4-45c0-b1dc-2ab9f075732e", "password": "XXX", "name": "mysql", "?": { "_26411a1861294160833743e45d0eaad9": { "name": "mysql" }, "type": "io.murano.apps.database.MySQL", "id": "54aaa43d-5970" } } self.addCleanup(delattr, self, 'application_mysql_desc') ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/db/services/test_cf_connections.py0000664000175000017500000001515000000000000025361 0ustar00zuulzuul00000000000000# Copyright 2016 AT&T Corp # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from murano.db import models from murano.db.services import cf_connections from murano.db.services import environments from murano.db import session as db_session from murano.tests.unit import base class TestCFConnection(base.MuranoWithDBTestCase): def setUp(self): super(TestCFConnection, self).setUp() self.resources = [] self.tenant_id = 'test_tenant_id' environment = models.Environment( name='test_environment', tenant_id='test_tenant_id', version=1 ) alt_environment = models.Environment( name='alt_test_environment', tenant_id='alt_test_tenant_id', version=1 ) unit = db_session.get_session() with unit.begin(): unit.add_all([environment, alt_environment]) self.environment_id = environment.id self.alt_environment_id = alt_environment.id def tearDown(self): super(TestCFConnection, self).tearDown() unit = db_session.get_session() if self.environment_id: environments.EnvironmentServices.remove(self.environment_id) if self.alt_environment_id: environments.EnvironmentServices.remove(self.alt_environment_id) for resource in self.resources: if resource: with unit.begin(): unit.delete(resource) def _create_resource(self, resource_name, **kwargs): unit = db_session.get_session() model = None new_resource_id = 1 if resource_name == 'cf_org': model = models.CFOrganization elif resource_name == 'cf_space': model = models.CFSpace elif resource_name == 'cf_service': model = models.CFServiceInstance resource = unit.query(model).order_by(model.id.desc()).first() if resource: new_resource_id = int(resource.id) + 1 new_resource = model(id=new_resource_id, **kwargs) with unit.begin(): unit.add(new_resource) created_resource = unit.query(model)\ .order_by(model.id.desc()).first() self.assertIsNotNone(created_resource) self.assertEqual(created_resource.id, str(new_resource.id)) self.resources.append(created_resource) return created_resource def test_set_tenant_for_org(self): cf_org = self._create_resource('cf_org', tenant=self.tenant_id) cf_connections.set_tenant_for_org(cf_org.id, 'another_test_tenant_id') duplicate_cf_org = None unit = db_session.get_session() with unit.begin(): duplicate_cf_org = unit.query(models.CFOrganization).get(cf_org.id) self.assertIsNotNone(duplicate_cf_org) self.assertEqual('another_test_tenant_id', duplicate_cf_org.tenant) def test_set_environment_for_space(self): cf_space = self._create_resource('cf_space', environment_id=self.environment_id) cf_connections.set_environment_for_space(cf_space.id, self.alt_environment_id) duplicate_cf_space = None unit = db_session.get_session() with unit.begin(): duplicate_cf_space = unit.query(models.CFSpace).get(cf_space.id) self.assertIsNotNone(duplicate_cf_space) self.assertEqual(self.alt_environment_id, duplicate_cf_space.environment_id) def test_set_instance_for_service(self): cf_service = self._create_resource('cf_service', service_id='123', environment_id=self.environment_id, tenant=self.tenant_id) cf_connections.set_instance_for_service( instance_id=cf_service.id, service_id='123', environment_id=self.alt_environment_id, tenant=self.tenant_id) duplicate_cf_service = None unit = db_session.get_session() with unit.begin(): duplicate_cf_service = unit.query(models.CFServiceInstance).\ get(cf_service.id) self.assertIsNotNone(duplicate_cf_service) self.assertEqual(self.alt_environment_id, duplicate_cf_service.environment_id) def test_get_environment_for_space(self): cf_space = self._create_resource('cf_space', environment_id=self.environment_id) retrieved_env_id =\ cf_connections.get_environment_for_space(cf_space.id) self.assertEqual(cf_space.environment_id, retrieved_env_id) def test_get_tenant_for_org(self): cf_org = self._create_resource('cf_org', tenant=self.tenant_id) retrieved_tenant =\ cf_connections.get_tenant_for_org(cf_org.id) self.assertEqual(self.tenant_id, retrieved_tenant) def test_get_service_for_instance(self): cf_service = self._create_resource('cf_service', service_id='123', environment_id=self.environment_id, tenant=self.tenant_id) retrieved_service =\ cf_connections.get_service_for_instance(cf_service.id) for attr in ['id', 'environment_id', 'service_id', 'tenant']: self.assertEqual(getattr(cf_service, attr), getattr(retrieved_service, attr)) def test_delete_environment_from_space(self): environment = models.Environment( name='test_environment_1', tenant_id='test_tenant_id_1', version=1 ) unit = db_session.get_session() with unit.begin(): unit.add(environment) cf_connections.delete_environment_from_space(environment.id) with unit.begin(): retrieved_env = unit.query(models.Environment).get(environment.id) self.assertIsNotNone(retrieved_env) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/db/services/test_core_service.py0000664000175000017500000002151500000000000025041 0ustar00zuulzuul00000000000000# Copyright (c) 2015 Telefonica I+D. # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or # implied. # See the License for the specific language governing permissions and # limitations under the License. from unittest import mock from webob import exc from murano.db.services import core_services from murano.tests.unit import base from murano.tests.unit.db.services import environment_templates as et class TestCoreServices(base.MuranoTestCase): def setUp(self): super(TestCoreServices, self).setUp() self.core_services = core_services.CoreServices self.addCleanup(mock.patch.stopall) @mock.patch('murano.db.services.environment_templates.EnvTemplateServices') def test_empty_template(self, template_services_mock): """Check obtaining the template description without services.""" fixture = self.useFixture(et.EmptyEnvironmentTemplateFixture()) template_services_mock.get_description.return_value = \ fixture.environment_template_desc template_des = self.core_services.get_template_data('any', '/services') self.assertEqual([], template_des) template_services_mock.get_description.assert_called_with('any') @mock.patch('murano.db.services.environment_templates.EnvTemplateServices') def test_template_services(self, template_services_mock): """Check obtaining the template description with services.""" fixture_apps = self.useFixture(et.ApplicationsFixture()) fixture_env_apps = self.useFixture(et.AppEnvTemplateFixture()) template_services_mock.get_description.return_value = \ fixture_env_apps.env_template_desc template_des = self.core_services.get_template_data('any', '/services') self.assertEqual(fixture_apps.applications_desc, template_des) template_services_mock.get_description.assert_called_with('any') @mock.patch('murano.db.services.environment_templates.EnvTemplateServices') def test_template_get_service(self, template_services_mock): """Check obtaining the service description.""" fixture = self.useFixture(et.AppEnvTemplateFixture()) fixture2 = self.useFixture(et.ApplicationTomcatFixture()) template_services_mock.get_description.return_value = \ fixture.env_template_desc template_des = \ self.core_services.get_template_data('any', '/services/tomcat_id') self.assertEqual(fixture2.application_tomcat_desc, template_des) template_services_mock.get_description.assert_called_with('any') @mock.patch('murano.db.services.environment_templates.EnvTemplateServices') def test_template_post_services(self, template_services_mock): """Check adding a service to a template.""" fixture = self.useFixture(et.EmptyEnvironmentTemplateFixture()) fixture2 = self.useFixture(et.AppEnvTemplateFixture()) template_services_mock.get_description.return_value = \ fixture.environment_template_desc template_des = self.core_services.\ post_env_template_data('any', fixture2.env_template_desc, '/services') self.assertEqual(fixture2.env_template_desc, template_des) template_services_mock.get_description.assert_called_with('any') @mock.patch('murano.db.services.environment_templates.EnvTemplateServices') def test_template_delete_services(self, template_services_mock): """Check deleting a service in a template.""" fixture2 = self.useFixture(et.AppEnvTemplateFixture()) fixture = self.useFixture(et.ApplicationTomcatFixture()) template_services_mock.get_description.return_value = \ fixture2.env_template_desc self.core_services.\ delete_env_template_data('any', '/services/54aaa43d-5970') template_des = self.core_services.get_template_data('any', '/services') self.assertEqual([fixture.application_tomcat_desc], template_des) template_services_mock.get_description.assert_called_with('any') @mock.patch('murano.db.services.environment_templates.EnvTemplateServices') def test_get_template_no_exists(self, template_services_mock): """Check obtaining a non-existing service.""" fixture2 = self.useFixture(et.AppEnvTemplateFixture()) template_services_mock.get_description.return_value = \ fixture2.env_template_desc self.assertRaises(exc.HTTPNotFound, self.core_services.get_template_data, 'any', '/services/noexists') template_services_mock.get_description.assert_called_with('any') @mock.patch('murano.db.services.environment_templates.EnvTemplateServices') def test_adding_services(self, template_services_mock): """Check adding services to a template.""" ftomcat = self.useFixture(et.ApplicationTomcatFixture()) fmysql = self.useFixture(et.ApplicationMysqlFixture()) fixture = self.useFixture(et.EmptyEnvironmentTemplateFixture()) fservices = self.useFixture(et.ApplicationsFixture()) template_services_mock.get_description.return_value =\ fixture.environment_template_desc self.core_services.\ post_env_template_data('any', ftomcat.application_tomcat_desc, '/services') self.core_services.\ post_env_template_data('any', fmysql.application_mysql_desc, '/services') template_des = \ self.core_services.get_template_data('any', '/services') self.assertEqual(fservices.applications_desc, template_des) template_services_mock.get_description.assert_called_with('any') @mock.patch('murano.db.services.environment_templates.EnvTemplateServices') def test_none_temp_description(self, template_services_mock): fixture = self.useFixture(et.EmptyEnvironmentTemplateFixture()) template_services_mock.get_description.return_value = None self.assertIsNone(self.core_services. get_template_data('any', '/services')) self.assertRaises(exc.HTTPNotFound, self.core_services.post_env_template_data, 'any', fixture.environment_template_desc, '/services') self.assertRaises(exc.HTTPNotFound, self.core_services.post_application_data, 'any', fixture.environment_template_desc, '/services') self.assertRaises(exc.HTTPNotFound, self.core_services.delete_env_template_data, 'any', '/services') self.assertRaises(exc.HTTPNotFound, self.core_services.put_application_data, 'any', fixture.environment_template_desc, '/services') @mock.patch('murano.common.utils.TraverseHelper') @mock.patch('murano.db.services.environments.EnvironmentServices') def test_post_data(self, env_services_mock, source_mock): fixture = self.useFixture(et.EmptyEnvironmentFixture()) env_services_mock.get_description.return_value = fixture.env_desc session_id = None ftomcat = self.useFixture(et.ApplicationTomcatFixture()) self.assertEqual(self.core_services.post_data('any', session_id, ftomcat.application_tomcat_desc, '/services'), ftomcat.application_tomcat_desc) self.assertTrue(source_mock.insert.called) self.assertEqual(self.core_services.put_data('any', session_id, ftomcat.application_tomcat_desc, '/services'), ftomcat.application_tomcat_desc) self.assertTrue(source_mock.update.called) self.core_services.delete_data('any', session_id, '/services') self.assertTrue(source_mock.remove.called) @mock.patch('murano.db.services.environments.EnvironmentServices') def test_get_service_status(self, env_services_mock): service_id = 12 fixture = self.useFixture(et.EmptyEnvironmentFixture()) env_services_mock.get_description.return_value = fixture.env_desc self.core_services.get_service_status('any', service_id) self.assertTrue(env_services_mock.get_status.called) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/db/services/test_environments.py0000664000175000017500000001055500000000000025122 0ustar00zuulzuul00000000000000# 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 datetime as dt from unittest import mock import uuid from oslo_utils import timeutils from murano.db import models from murano.db.services import environments from murano.db import session as db_session from murano.services import states from murano.tests.unit import base from murano.tests.unit import utils OLD_VERSION = 0 LATEST_VERSION = 1 class TestEnvironmentServices(base.MuranoWithDBTestCase): def setUp(self): super(TestEnvironmentServices, self).setUp() self.environment = models.Environment( name='test_environment', tenant_id='test_tenant_id', version=LATEST_VERSION ) self.env_services = environments.EnvironmentServices() def test_environment_ready_if_last_session_deployed_after_failed(self): """Test environment ready status If last session was deployed successfully and other session was failed - environment must have status "ready". Bug: #1413260 """ session = db_session.get_session() session.add(self.environment) now = timeutils.utcnow() session_1 = models.Session( environment=self.environment, user_id='test_user_id_1', version=OLD_VERSION, state=states.SessionState.DEPLOY_FAILURE, updated=now, description={} ) session_2 = models.Session( environment=self.environment, user_id='test_user_id_2', version=LATEST_VERSION, state=states.SessionState.DEPLOYED, updated=now + dt.timedelta(minutes=1), description={} ) session.add_all([session_1, session_2]) session.flush() expected_status = states.EnvironmentStatus.READY actual_status = environments.EnvironmentServices.get_status( self.environment.id ) self.assertEqual(expected_status, actual_status) @mock.patch("murano.db.services.environments.auth_utils") def test_get_network_driver(self, mock_authentication): self.tenant_id = str(uuid.uuid4()) self.context = utils.dummy_context(tenant_id=self.tenant_id) driver_context = self.env_services.get_network_driver(self.context) self.assertEqual(driver_context, "neutron") def test_get_status(self): session = db_session.get_session() session.add(self.environment) now = timeutils.utcnow() session_1 = models.Session( environment=self.environment, user_id='test_user_id_1', version=OLD_VERSION, state=states.SessionState.DEPLOY_FAILURE, updated=now, description={} ) session.add(session_1) session.flush() expected_status = states.EnvironmentStatus.DEPLOY_FAILURE self.assertEqual(expected_status, self.env_services.get_status( self.environment.id)) def test_delete_failure_get_description(self): session = db_session.get_session() session.add(self.environment) now = timeutils.utcnow() session_1 = models.Session( environment=self.environment, user_id='test_user_id_1', version=OLD_VERSION, state=states.SessionState.DELETE_FAILURE, updated=now, description={} ) session.add(session_1) session.flush() expected_status = states.EnvironmentStatus.DELETE_FAILURE self.assertEqual(expected_status, self.env_services.get_status( self.environment.id)) env_id = self.environment.id description = (self.env_services. get_environment_description(env_id, session_id=None, inner=False)) self.assertEqual({}, description) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/db/services/test_instances.py0000664000175000017500000001536000000000000024361 0ustar00zuulzuul00000000000000# Copyright 2016 AT&T Corp # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from unittest import mock from oslo_db import exception from oslo_utils import timeutils from murano.db.services import instances from murano.tests.unit import base as test_base @mock.patch('murano.db.services.instances.db_session') class TestInstances(test_base.MuranoTestCase): @mock.patch('murano.db.services.instances.timeutils') @mock.patch('murano.db.services.instances.models') def test_track_instance(self, mock_models, mock_timeutils, mock_db_session): mock_env = mock.MagicMock(tenant_id='test_tenant_id') mock_db_session.get_session().query().get.return_value = mock_env now = timeutils.utcnow_ts() mock_timeutils.utcnow_ts.return_value = now track_instance = instances.InstanceStatsServices.track_instance track_instance('test_instance_id', 'test_env_id', 'test_type', 'test_type_name', 'test_type_title', unit_count=1) mock_models.Instance.assert_called_once_with() self.assertEqual('test_instance_id', mock_models.Instance().instance_id) self.assertEqual('test_env_id', mock_models.Instance().environment_id) self.assertEqual('test_instance_id', mock_models.Instance().instance_id) self.assertEqual('test_tenant_id', mock_models.Instance().tenant_id) self.assertEqual('test_type', mock_models.Instance().instance_type) self.assertEqual(now, mock_models.Instance().created) self.assertIsNone(mock_models.Instance().destroyed) self.assertEqual('test_type_name', mock_models.Instance().type_name) self.assertEqual('test_type_title', mock_models.Instance().type_title) self.assertEqual(1, mock_models.Instance().unit_count) mock_db_session.get_session().add.assert_called_once_with( mock_models.Instance()) @mock.patch('murano.db.services.instances.sqlalchemy') @mock.patch('murano.db.services.instances.models') def test_track_instance_except_duplicate_entry(self, _, mock_sqlalchemy, mock_db_session): mock_db_session.get_session().add.side_effect =\ exception.DBDuplicateEntry track_instance = instances.InstanceStatsServices.track_instance track_instance('test_instance_id', 'test_env_id', 'test_type', 'test_type_name', 'test_type_title') self.assertEqual(1, mock_db_session.get_session().execute.call_count) self.assertEqual(1, mock_sqlalchemy.update().where.call_count) @mock.patch('murano.db.services.instances.timeutils') def test_destroy_instance(self, mock_timeutils, mock_db_session): mock_instance = mock.MagicMock(destroyed=False) mock_db_session.get_session().query().get.return_value = mock_instance now = timeutils.utcnow_ts() mock_timeutils.utcnow_ts.return_value = now destroy_instance = instances.InstanceStatsServices.destroy_instance destroy_instance('test_instance_id', 'test_environment_id') self.assertEqual(now, mock_instance.destroyed) mock_instance.save.assert_called_once_with( mock_db_session.get_session()) def test_get_aggregated_stats(self, mock_db_session): mock_db_session.get_session().query().filter().group_by().all\ .return_value = [['1', '2', '3'], ['4', '5', '6']] get_stats = instances.InstanceStatsServices.get_aggregated_stats result = get_stats('test_env_id') expected_result = [ {'type': 1, 'duration': 2, 'count': 3}, {'type': 4, 'duration': 5, 'count': 6}, ] self.assertEqual(expected_result, result) @mock.patch('murano.db.services.instances.timeutils') def test_get_raw_environment_stats(self, mock_timeutils, mock_db_session): now = timeutils.utcnow_ts() mock_timeutils.utcnow_ts.return_value = now mock_db_session.get_session().query().filter().filter().all.\ return_value = [ mock.MagicMock(instance_type='test_instance_type', created=now - 1000, destroyed=now, type_name='test_type_name', unit_count=1, instance_id='test_instance_id', type_title='test_type_title'), mock.MagicMock(instance_type='test_instance_type_2', created=now - 2000, destroyed=None, type_name='test_type_name_2', unit_count=2, instance_id='test_instance_id_2', type_title='test_type_title_2') ] mock_db_session.reset_mock() get_env_stats = instances.InstanceStatsServices.\ get_raw_environment_stats result = get_env_stats('test_env_id', 'test_instance_id') expected_result = [ { 'type': 'test_instance_type', 'duration': 1000, # now - (now - 1000) = 1000 'type_name': 'test_type_name', 'unit_count': 1, 'instance_id': 'test_instance_id', 'type_title': 'test_type_title', 'active': False }, { 'type': 'test_instance_type_2', 'duration': 2000, # now - (now - 2000) = 2000 'type_name': 'test_type_name_2', 'unit_count': 2, 'instance_id': 'test_instance_id_2', 'type_title': 'test_type_title_2', 'active': True }, ] self.assertEqual(expected_result, result) # Assert that two filters were performed, the second one for # the instance_id argument. self.assertEqual(1, mock_db_session.get_session().query().filter. call_count) self.assertEqual(1, mock_db_session.get_session().query().filter(). filter.call_count) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/db/services/test_stats.py0000664000175000017500000001306600000000000023531 0ustar00zuulzuul00000000000000# Copyright 2016 AT&T Corp # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from murano.db import models from murano.db.services import stats as statistics from murano.db import session as db_session from murano.tests.unit import base class TestStats(base.MuranoWithDBTestCase): def setUp(self): super(TestStats, self).setUp() self.stats_list = [] self.stats_kwargs = { 'host': 'test_host', 'request_count': 5, 'error_count': 2, 'average_response_time': 0.5, 'requests_per_tenant': 1, 'cpu_count': 4, 'cpu_percent': 0.3 } self.stats_kwargs_2 = { 'host': 'test_host_2', 'request_count': 6, 'error_count': 3, 'average_response_time': 0.6, 'requests_per_tenant': 2, 'cpu_count': 5, 'cpu_percent': 0.4 } def tearDown(self): super(TestStats, self).tearDown() unit = db_session.get_session() for stat in self.stats_list: with unit.begin(): unit.delete(stat) def _create_stats(self, which_kwargs): stats = models.ApiStats() for key, val in which_kwargs.items(): setattr(stats, key, val) stats.requests_per_second = 1.2 stats.errors_per_second = 2.3 unit = db_session.get_session() with unit.begin(): unit.add(stats) self.assertIsNotNone(stats) self.stats_list.append(stats) return stats def _are_stats_equal(self, x, y, check_type=False): comparison_attributes = ['host', 'request_count', 'error_count', 'average_response_time', 'requests_per_tenant', 'requests_per_second', 'errors_per_second', 'cpu_count', 'cpu_percent'] if check_type: if type(x) != type(y): return False for attr in comparison_attributes: try: if str(getattr(x, attr)) != str(getattr(y, attr)): return False except AttributeError: if str(x[attr]) != str(getattr(y, attr)): return False return True def test_create(self): statistics.Statistics.create(**self.stats_kwargs) unit = db_session.get_session() retrieved_stats = None with unit.begin(): retrieved_stats = unit.query(models.ApiStats)\ .order_by(models.ApiStats.id.desc()).first() self.stats_kwargs['requests_per_second'] = 0.0 self.stats_kwargs['errors_per_second'] = 0.0 self.assertIsNotNone(retrieved_stats) self.assertTrue( self._are_stats_equal(self.stats_kwargs, retrieved_stats)) def test_update(self): """Note: this test expects to test update functionality. However, the current implementation of Statistics.update() does not actually update the host of the statistics object passed in. It just saves the object that is passed in, which appears to contradict its intended use case. """ stats = models.ApiStats() for key, val in self.stats_kwargs.items(): setattr(stats, key, val) statistics.Statistics.update('test_host', stats) unit = db_session.get_session() retrieved_stats = None with unit.begin(): retrieved_stats = unit.query(models.ApiStats)\ .order_by(models.ApiStats.id.desc()).first() self.assertIsNotNone(retrieved_stats) self.assertTrue( self._are_stats_equal(stats, retrieved_stats, check_type=True)) def test_get_stats_by_id(self): stats = self._create_stats(self.stats_kwargs) retrieved_stats = statistics.Statistics.get_stats_by_id(stats.id) self.assertIsNotNone(retrieved_stats) self.assertTrue( self._are_stats_equal(stats, retrieved_stats, check_type=True)) def test_get_stats_by_host(self): stats = self._create_stats(self.stats_kwargs) retrieved_stats = statistics.Statistics.get_stats_by_host(stats.host) self.assertIsNotNone(retrieved_stats) self.assertTrue( self._are_stats_equal(stats, retrieved_stats, check_type=True)) def test_get_all(self): stats = self._create_stats(self.stats_kwargs) retrieved_stats = statistics.Statistics.get_all() self.assertIsInstance(retrieved_stats, list) self.assertEqual(1, len(retrieved_stats)) self.assertTrue( self._are_stats_equal(stats, retrieved_stats[0], check_type=True)) stats = self._create_stats(self.stats_kwargs_2) retrieved_stats = statistics.Statistics.get_all() self.assertIsInstance(retrieved_stats, list) self.assertEqual(2, len(retrieved_stats)) self.assertTrue( self._are_stats_equal(stats, retrieved_stats[1], check_type=True)) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/db/services/test_templates_service.py0000664000175000017500000001123100000000000026101 0ustar00zuulzuul00000000000000# Copyright (c) 2015 Telefonica I+D. # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or # implied. # See the License for the specific language governing permissions and # limitations under the License. from unittest import mock import fixtures from murano.db.services import environment_templates as env_temp from murano.tests.unit import base from murano.tests.unit.db.services import environment_templates as et class TestTemplateServices(base.MuranoWithDBTestCase, fixtures.TestWithFixtures): def setUp(self): super(TestTemplateServices, self).setUp() self.template_services = env_temp.EnvTemplateServices self.uuids = ['template_id', 'template_id2'] self.mock_uuid = self._stub_uuid(self.uuids) self.addCleanup(mock.patch.stopall) def test_create_template(self): """Check creating a template without services.""" fixture = self.useFixture(et.EmptyEnvironmentTemplateFixture()) """Check the creation of a template.""" body = { "name": "my_template" } template_des = self.template_services.create(body, 'tenant_id') self.assertEqual(fixture.environment_template_desc, template_des.description) def test_clone_template(self): """Check the clonation of a template.""" body = { "name": "my_template" } template = self.template_services.create(body, 'tenant_id') cloned_template = self.template_services.clone(template['id'], 'id2', "my_template2", False) self.assertEqual(cloned_template.description['name'], 'my_template2') self.assertEqual(cloned_template.description['tenant_id'], 'id2') def test_get_empty_template(self): """Check obtaining information about a template without services.""" fixture = self.useFixture(et.EmptyEnvironmentTemplateFixture()) self.test_create_template() template = \ self.template_services.get_description("template_id") self.assertEqual(fixture.environment_template_desc, template) def test_get_template_services(self): """Check obtaining information about a template with services.""" fixture = self.useFixture(et.AppEnvTemplateFixture()) template = self.template_services.create(fixture.env_template_desc, 'tenant_id') self.assertEqual(fixture.env_template_desc, template.description) template_des = \ self.template_services.get_description("template_id") self.assertEqual(fixture.env_template_desc, template_des) def test_get_template_no_exists(self): """Check obtaining information about a non-existent template Check obtaining information about a template which does not exist. """ self.assertRaises(ValueError, self.template_services.get_description, 'no_exists') def test_delete_template(self): """Check deleting a template.""" self.test_create_template() self.template_services.delete("template_id") def _stub_uuid(self, values=None): class FakeUUID(object): def __init__(self, v): self.hex = v if values is None: values = [] mock_uuid4 = mock.patch('uuid.uuid4').start() mock_uuid4.side_effect = [FakeUUID(v) for v in values] return mock_uuid4 def test_get_application_description(self): fixture = self.useFixture(et.AppEnvTemplateFixture()) self.template_services.create(fixture.env_template_desc, 'tenant_id') template_app_des = (self.template_services. get_application_description("template_id")) self.assertEqual(template_app_des, fixture.env_template_desc['services']) body = { "name": "my_template" } self.template_services.create(body, 'tenant_id') template_app_des_2 = (self.template_services. get_application_description("template_id2")) self.assertEqual([], template_app_des_2) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/db/test_catalog.py0000664000175000017500000005373000000000000022164 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT 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 uuid from oslo_db import exception as db_exception from webob import exc from murano.db.catalog import api from murano.tests.unit import base from murano.tests.unit import utils class CatalogDBTestCase(base.MuranoWithDBTestCase): def setUp(self): super(CatalogDBTestCase, self).setUp() self.tenant_id = str(uuid.uuid4()) self.tenant_id_2 = str(uuid.uuid4()) self.context = utils.dummy_context(tenant_id=self.tenant_id) self.context_2 = utils.dummy_context(tenant_id=self.tenant_id_2) self.context_admin = utils.dummy_context(tenant_id=self.tenant_id) self.context_admin.is_admin = True def _create_categories(self): api.category_add('cat1') api.category_add('cat2') def _stub_package(self, **kwargs): base = { 'archive': b"archive blob here", 'fully_qualified_name': 'com.example.package', 'type': 'class', 'author': 'OpenStack', 'name': 'package', 'enabled': True, 'description': 'some text', 'is_public': False, 'tags': ['tag1', 'tag2'], 'logo': b"logo blob here", 'ui_definition': '{}', } base.update(**kwargs) return base def get_change(self, op, path, value): return { 'op': op, 'path': path, 'value': value } def test_order_by(self): pkgs = [] for dummy in range(10): package = api.package_upload(self._stub_package( name=str(uuid.uuid4()), fully_qualified_name=str(uuid.uuid4())), self.tenant_id) pkgs.append(package) pkg_created = [pkg.id for pkg in sorted( pkgs, key=lambda _pkg: _pkg.created)] pkg_name = [pkg.id for pkg in sorted( pkgs, key=lambda _pkg: _pkg.name)] pkg_fqn = [pkg.id for pkg in sorted( pkgs, key=lambda _pkg: _pkg.fully_qualified_name)] for order, pkg_ids in zip(['created', 'name', 'fqn'], [pkg_created, pkg_name, pkg_fqn]): res = api.package_search( {'order_by': [order]}, self.context, limit=10) self.assertEqual(10, len(res)) self.assertEqual(pkg_ids, [r.id for r in res]) def test_order_by_compound(self): pkgs_a, pkgs_z = [], [] for _ in range(5): package = api.package_upload(self._stub_package( name='z', fully_qualified_name=str(uuid.uuid4())), self.tenant_id) pkgs_z.append(package) for _ in range(5): package = api.package_upload(self._stub_package( name='a', fully_qualified_name=str(uuid.uuid4())), self.tenant_id) pkgs_a.append(package) # sort pkg ids by pkg created pkg_a_id = [pkg.id for pkg in sorted( pkgs_a, key=lambda _pkg: _pkg.created)] pkg_z_id = [pkg.id for pkg in sorted( pkgs_z, key=lambda _pkg: _pkg.created)] res = api.package_search( {'order_by': ['name', 'created']}, self.context, limit=10) self.assertEqual(10, len(res)) self.assertEqual(pkg_a_id + pkg_z_id, [r.id for r in res]) def test_pagination_backwards(self): """Tests backwards pagination Creates 10 packages with unique names and iterates backwards, checking that package order is correct. """ pkgs = [] for dummy in range(10): package = api.package_upload(self._stub_package( name=str(uuid.uuid4()), fully_qualified_name=str(uuid.uuid4())), self.tenant_id) pkgs.append(package) # sort pkg ids by pkg name pkg_ids = [pkg.id for pkg in sorted(pkgs, key=lambda _pkg: _pkg.name)] res = api.package_search({}, self.context, limit=10) self.assertEqual(10, len(res)) self.assertEqual(pkg_ids, [r.id for r in res]) marker = res[-1].id res = api.package_search( {'marker': marker, 'sort_dir': 'desc'}, self.context, limit=5) self.assertEqual(5, len(res)) self.assertEqual(list(reversed(pkg_ids[4:9])), [r.id for r in res]) marker = res[-1].id res = api.package_search( {'marker': marker, 'sort_dir': 'desc'}, self.context, limit=5) self.assertEqual(4, len(res)) self.assertEqual(list(reversed(pkg_ids[0:4])), [r.id for r in res]) marker = res[-1].id res = api.package_search( {'marker': marker, 'sort_dir': 'desc'}, self.context, limit=5) self.assertEqual(0, len(res)) def test_pagination(self): """Tests that package order is correct Creates 10 packages with unique names and iterates through them, checking that package order is correct. """ pkgs = [] for dummy in range(10): package = api.package_upload(self._stub_package( name=str(uuid.uuid4()), fully_qualified_name=str(uuid.uuid4())), self.tenant_id) pkgs.append(package) # sort pkg ids by pkg name pkg_ids = [pkg.id for pkg in sorted(pkgs, key=lambda _pkg: _pkg.name)] res = api.package_search({}, self.context, limit=4) self.assertEqual(4, len(res)) self.assertEqual(pkg_ids[0:4], [r.id for r in res]) marker = res[-1].id res = api.package_search({'marker': marker}, self.context, limit=4) self.assertEqual(4, len(res)) self.assertEqual(pkg_ids[4:8], [r.id for r in res]) marker = res[-1].id res = api.package_search({'marker': marker}, self.context, limit=4) self.assertEqual(2, len(res)) self.assertEqual(pkg_ids[8:10], [r.id for r in res]) marker = res[-1].id res = api.package_search({'marker': marker}, self.context, limit=4) self.assertEqual(0, len(res)) def test_pagination_loops_through_names(self): """Tests that packages with same display name are not skipped Creates 10 packages with the same display name and iterates through them, checking that package are not skipped. """ for dummy in range(10): api.package_upload( self._stub_package(name='test', fully_qualified_name=str(uuid.uuid4())), self.tenant_id) res = api.package_search({}, self.context, limit=4) self.assertEqual(4, len(res)) marker = res[-1].id res = api.package_search({'marker': marker}, self.context, limit=4) self.assertEqual(4, len(res)) marker = res[-1].id res = api.package_search({'marker': marker}, self.context, limit=4) self.assertEqual(2, len(res)) marker = res[-1].id res = api.package_search({'marker': marker}, self.context, limit=4) self.assertEqual(0, len(res)) def test_package_search_search_order(self): pkg1 = api.package_upload( self._stub_package( fully_qualified_name=str(uuid.uuid4()), name='mysql', description='awcloud'), self.tenant_id) pkg2 = api.package_upload( self._stub_package( fully_qualified_name=str(uuid.uuid4()), name='awcloud', description='mysql'), self.tenant_id) api.package_upload( self._stub_package( tags=[], fully_qualified_name=str(uuid.uuid4())), self.tenant_id) res = api.package_search( {'search': 'mysql'}, self.context) self.assertEqual(2, len(res)) self.assertEqual(pkg1.name, res[0].name) self.assertEqual(pkg2.description, res[1].description) def test_package_search_search(self): pkg1 = api.package_upload( self._stub_package( fully_qualified_name=str(uuid.uuid4())), self.tenant_id) pkg2 = api.package_upload( self._stub_package( tags=[], fully_qualified_name=str(uuid.uuid4())), self.tenant_id) res = api.package_search( {'search': 'tag1'}, self.context) self.assertEqual(1, len(res)) res = api.package_search( {'search': pkg1.fully_qualified_name}, self.context) self.assertEqual(1, len(res)) res = api.package_search( {'search': pkg2.fully_qualified_name}, self.context) self.assertEqual(1, len(res)) res = api.package_search( {'search': 'not_a_valid_uuid'}, self.context) self.assertEqual(0, len(res)) res = api.package_search( {'search': 'some text'}, self.context) self.assertEqual(2, len(res)) def test_package_search_tags(self): api.package_upload( self._stub_package( fully_qualified_name=str(uuid.uuid4())), self.tenant_id) api.package_upload( self._stub_package( tags=[], fully_qualified_name=str(uuid.uuid4())), self.tenant_id) res = api.package_search( {'tag': ['tag1']}, self.context) self.assertEqual(1, len(res)) res = api.package_search( {'tag': ['tag2']}, self.context) self.assertEqual(1, len(res)) res = api.package_search( {'tag': ['tag3']}, self.context) self.assertEqual(0, len(res)) def test_package_search_type(self): api.package_upload( self._stub_package( type="Application", fully_qualified_name=str(uuid.uuid4())), self.tenant_id) api.package_upload( self._stub_package( type="Library", fully_qualified_name=str(uuid.uuid4())), self.tenant_id) res = api.package_search( {'type': 'Library'}, self.context) self.assertEqual(1, len(res)) res = api.package_search( {'type': 'Application'}, self.context) self.assertEqual(1, len(res)) def test_package_search_disabled(self): api.package_upload( self._stub_package( is_public=True, enabled=True, fully_qualified_name=str(uuid.uuid4())), self.tenant_id) api.package_upload( self._stub_package( is_public=True, enabled=False, fully_qualified_name=str(uuid.uuid4())), self.tenant_id) res = api.package_search( {'include_disabled': 'false'}, self.context) self.assertEqual(1, len(res)) res = api.package_search( {'include_disabled': 'true'}, self.context) self.assertEqual(2, len(res)) def test_package_search_owned(self): api.package_upload( self._stub_package( is_public=True, fully_qualified_name=str(uuid.uuid4())), self.tenant_id) api.package_upload( self._stub_package( is_public=True, fully_qualified_name=str(uuid.uuid4())), self.tenant_id_2) res = api.package_search({'owned': 'true'}, self.context_admin) self.assertEqual(1, len(res)) res = api.package_search({'owned': 'false'}, self.context_admin) self.assertEqual(2, len(res)) def test_package_search_no_filters_catalog(self): res = api.package_search({}, self.context, catalog=True) self.assertEqual(0, len(res)) api.package_upload( self._stub_package( is_public=True, fully_qualified_name=str(uuid.uuid4())), self.tenant_id) api.package_upload( self._stub_package( is_public=False, fully_qualified_name=str(uuid.uuid4())), self.tenant_id) api.package_upload( self._stub_package( is_public=True, fully_qualified_name=str(uuid.uuid4())), self.tenant_id_2) api.package_upload( self._stub_package( is_public=False, fully_qualified_name=str(uuid.uuid4())), self.tenant_id_2) # catalog=True should show public + mine res = api.package_search({}, self.context, catalog=True) self.assertEqual(3, len(res)) res = api.package_search({}, self.context_admin, catalog=True) self.assertEqual(3, len(res)) def test_package_search_no_filters(self): res = api.package_search({}, self.context) self.assertEqual(0, len(res)) api.package_upload( self._stub_package( is_public=True, fully_qualified_name=str(uuid.uuid4())), self.tenant_id) api.package_upload( self._stub_package( is_public=False, fully_qualified_name=str(uuid.uuid4())), self.tenant_id) api.package_upload( self._stub_package( is_public=True, fully_qualified_name=str(uuid.uuid4())), self.tenant_id_2) api.package_upload( self._stub_package( is_public=False, fully_qualified_name=str(uuid.uuid4())), self.tenant_id_2) # I can only edit mine pkgs res = api.package_search({}, self.context) self.assertEqual(2, len(res)) for pkg in res: self.assertEqual(self.tenant_id, pkg.owner_id) # Admin can see everything res = api.package_search({}, self.context_admin) self.assertEqual(4, len(res)) def test_list_empty_categories(self): res = api.category_get_names() self.assertEqual(0, len(res)) def test_add_list_categories(self): self._create_categories() res = api.categories_list() self.assertEqual(2, len(res)) for cat in res: self.assertIsNotNone(cat.id) self.assertTrue(cat.name.startswith('cat')) def test_package_upload(self): self._create_categories() values = self._stub_package() package = api.package_upload(values, self.tenant_id) self.assertIsNotNone(package.id) for k in values.keys(): self.assertEqual(values[k], package[k]) def test_package_fqn_is_unique(self): self._create_categories() values = self._stub_package() api.package_upload(values, self.tenant_id) self.assertRaises(db_exception.DBDuplicateEntry, api.package_upload, values, self.tenant_id) def test_package_delete(self): values = self._stub_package() package = api.package_upload(values, self.tenant_id) api.package_delete(package.id, self.context) self.assertRaises(exc.HTTPNotFound, api.package_get, package.id, self.context) def test_package_upload_to_different_tenants_with_same_fqn(self): values = self._stub_package() api.package_upload(values, self.tenant_id) api.package_upload(values, self.tenant_id_2) def test_package_upload_public_public_fqn_violation(self): values = self._stub_package(is_public=True) api.package_upload(values, self.tenant_id) values = self._stub_package(is_public=True) self.assertRaises(exc.HTTPConflict, api.package_upload, values, self.tenant_id_2) def test_package_upload_public_private_no_fqn_violation(self): values = self._stub_package(is_public=True) api.package_upload(values, self.tenant_id) values = self._stub_package(is_public=False) api.package_upload(values, self.tenant_id_2) def test_package_upload_private_public_no_fqn_violation(self): values = self._stub_package() api.package_upload(values, self.tenant_id) values = self._stub_package(is_public=True) api.package_upload(values, self.tenant_id_2) def test_class_name_is_unique(self): value = self._stub_package(class_definitions=('foo', 'bar')) api.package_upload(value, self.tenant_id) value = self._stub_package(class_definitions=('bar', 'baz'), fully_qualified_name='com.example.package2') self.assertRaises(exc.HTTPConflict, api.package_upload, value, self.tenant_id) def test_class_name_uniqueness_not_enforced_in_different_tenants(self): value = self._stub_package(class_definitions=('foo', 'bar')) api.package_upload(value, self.tenant_id) value = self._stub_package(class_definitions=('foo', 'bar'), fully_qualified_name='com.example.package2') api.package_upload(value, self.tenant_id_2) def test_class_name_public_public_violation(self): value = self._stub_package(class_definitions=('foo', 'bar'), is_public=True) api.package_upload(value, self.tenant_id) value = self._stub_package(class_definitions=('foo', 'bar'), is_public=True, fully_qualified_name='com.example.package2') self.assertRaises(exc.HTTPConflict, api.package_upload, value, self.tenant_id_2) def test_class_name_public_private_no_violation(self): value = self._stub_package(class_definitions=('foo', 'bar'), is_public=True) api.package_upload(value, self.tenant_id) value = self._stub_package(class_definitions=('foo', 'bar'), is_public=False, fully_qualified_name='com.example.package2') api.package_upload(value, self.tenant_id_2) def test_class_name_private_public_no_violation(self): value = self._stub_package(class_definitions=('foo', 'bar'), is_public=False) api.package_upload(value, self.tenant_id) value = self._stub_package(class_definitions=('foo', 'bar'), is_public=True, fully_qualified_name='com.example.package2') api.package_upload(value, self.tenant_id_2) def test_package_make_public(self): id = api.package_upload(self._stub_package(), self.tenant_id).id patch = self.get_change('replace', ['is_public'], True) api.package_update(id, [patch], self.context) package = api.package_get(id, self.context) self.assertTrue(package.is_public) def test_package_update_public_public_fqn_violation(self): id1 = api.package_upload(self._stub_package(), self.tenant_id).id id2 = api.package_upload(self._stub_package(), self.tenant_id_2).id patch = self.get_change('replace', ['is_public'], True) api.package_update(id1, [patch], self.context) self.assertRaises(exc.HTTPConflict, api.package_update, id2, [patch], self.context_2) def test_package_update_public_public_class_name_violation(self): id1 = api.package_upload(self._stub_package( class_definitions=('foo', 'bar')), self.tenant_id).id id2 = api.package_upload(self._stub_package( class_definitions=('foo', 'bar'), fully_qualified_name='com.example.package2'), self.tenant_id_2).id patch = self.get_change('replace', ['is_public'], True) api.package_update(id1, [patch], self.context) self.assertRaises(exc.HTTPConflict, api.package_update, id2, [patch], self.context_2) def test_category_paginate(self): """Paginate through a list of categories using limit and marker""" category_names = ['cat1', 'cat2', 'cat3', 'cat4', 'cat5'] categories = [] for name in category_names: categories.append(api.category_add(name)) uuids = [c.id for c in categories] page = api.categories_list(limit=2) self.assertEqual(category_names[:2], [c.name for c in page]) last = page[-1].id page = api.categories_list(limit=3, marker=last) self.assertEqual(category_names[2:5], [c.name for c in page]) page = api.categories_list(marker=uuids[-1]) self.assertEqual([], page) category_names.reverse() page = api.categories_list({'sort_dir': 'desc'}) self.assertEqual(category_names, [c.name for c in page]) def test_category_get_delete_error(self): category_id = 12 self.assertRaises(exc.HTTPNotFound, api.category_get, category_id) self.assertRaises(exc.HTTPNotFound, api.category_delete, category_id) def test_get_categories_error(self): category_names = ['cat1', 'cat2', 'cat3', 'cat4', 'cat5'] cat_session = None self.assertRaises(exc.HTTPBadRequest, api._get_categories, category_names, cat_session) def test_authorize_package_delete_error(self): values = self._stub_package() package = api.package_upload(values, self.tenant_id) self.assertRaises(exc.HTTPForbidden, api._authorize_package, package, self.context_2) self.assertRaises(exc.HTTPForbidden, api.package_delete, package.id, self.context_2) id = package.id patch = self.get_change('replace', ['is_public'], False) api.package_update(id, [patch], self.context) self.assertRaises(exc.HTTPForbidden, api._authorize_package, package, self.context_2, allow_public=True) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/db/test_models.py0000664000175000017500000000255600000000000022035 0ustar00zuulzuul00000000000000# Copyright (c) 2014 Hewlett-Packard Development Company, L.P. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT 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 murano.db import models from murano.db import session from murano.tests.unit import base class TestModels(base.MuranoWithDBTestCase): def test_missing_blob(self): """Fake a package with NULL supplier JSON blob to test bug 1342306.""" con = session.get_session().connection() con.execute("INSERT INTO package(id, fully_qualified_name, " "owner_id, name, description, created, updated, type, " "supplier) " "VALUES (1, 'blob.test', 1, 'blob test', 'Desc', " "'2014-07-15 00:00:00', '2014-07-15 00:00:00', " "'Application', NULL)") loaded_e = session.get_session().query(models.Package).get(1) self.assertIsNone(loaded_e.supplier) ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1696417899.8331811 murano-16.0.0/murano/tests/unit/dsl/0000775000175000017500000000000000000000000017326 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/dsl/__init__.py0000664000175000017500000000000000000000000021425 0ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1696417899.8331811 murano-16.0.0/murano/tests/unit/dsl/foundation/0000775000175000017500000000000000000000000021474 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/dsl/foundation/__init__.py0000664000175000017500000000000000000000000023573 0ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/dsl/foundation/object_model.py0000664000175000017500000000464400000000000024504 0ustar00zuulzuul00000000000000# 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 murano.dsl import helpers class Object(object): def __init__(self, __name, __id=None, class_version=None, **kwargs): self.data = { '?': { 'type': __name, 'id': __id or helpers.generate_id() } } if class_version is not None: self.data['?']['classVersion'] = class_version self.data.update(kwargs) @property def id(self): return self.data['?']['id'] @property def type_name(self): return self.data['?']['type'] def __getitem__(self, item): return self.data[item] def __setitem__(self, key, value): self.data[key] = value def __contains__(self, item): return item in self.data def __delitem__(self, key): del self.data[key] class Attribute(object): def __init__(self, obj, key, value): self._value = value self._key = key self._obj = obj @property def obj(self): return self._obj @property def key(self): return self._key @property def value(self): return self._value class Ref(object): def __init__(self, obj): if isinstance(obj, str): self._id = obj else: self._id = obj.id @property def id(self): return self._id def build_model(root): if isinstance(root, dict): for key, value in root.items(): root[key] = build_model(value) elif isinstance(root, list): for i in range(len(root)): root[i] = build_model(root[i]) elif isinstance(root, Object): return build_model(root.data) elif isinstance(root, Ref): return root.id elif isinstance(root, Attribute): return [root.obj.id, root.obj.type_name, root.key, root.value] return root ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/dsl/foundation/runner.py0000664000175000017500000001336700000000000023371 0ustar00zuulzuul00000000000000# 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 sys from murano.common import utils from murano.dsl import context_manager from murano.dsl import dsl from murano.dsl import dsl_exception from murano.dsl import dsl_types from murano.dsl import executor from murano.dsl import helpers from murano.dsl import murano_object from murano.dsl import serializer from murano.dsl import yaql_integration from murano.engine import execution_session from murano.engine.system import yaql_functions from murano.tests.unit.dsl.foundation import object_model class TestContextManager(context_manager.ContextManager): def __init__(self, functions): self.__functions = functions def create_root_context(self, runtime_version): root_context = super(TestContextManager, self).create_root_context( runtime_version) context = helpers.link_contexts( root_context, yaql_functions.get_context(runtime_version)) context = context.create_child_context() for name, func in self.__functions.items(): context.register_function(func, name) return context class Runner(object): class DslObjectWrapper(object): def __init__(self, obj, runner): self._runner = runner if isinstance(obj, (str,) + (dsl_types.MuranoType,)): pass elif isinstance(obj, (object_model.Object, object_model.Ref)): obj = obj.id elif isinstance(obj, murano_object.MuranoObject): obj = obj.object_id else: raise ValueError( 'obj must be object ID string, MuranoObject, MuranoType ' 'or one of object_model helper classes (Object, Ref)') if isinstance(obj, str): self._receiver = runner.executor.object_store.get(obj) else: self._receiver = obj self._preserve_exception = False def __getattr__(self, item): def call(*args, **kwargs): return self._runner._execute( item, self._receiver, *args, **kwargs) if item.startswith('test'): return call def __init__(self, model, package_loader, functions): if isinstance(model, str): model = object_model.Object(model) model = object_model.build_model(model) if 'Objects' not in model: model = {'Objects': model} self.executor = executor.MuranoDslExecutor( package_loader, TestContextManager(functions), execution_session.ExecutionSession()) self._root = self.executor.load(model) if self._root: self._root = self._root.object if 'ObjectsCopy' in model: self.executor.object_store.cleanup() def _execute(self, name, obj, *args, **kwargs): try: final_args = [] final_kwargs = {} for arg in args: if isinstance(arg, object_model.Object): arg = object_model.build_model(arg) final_args.append(arg) for name, arg in kwargs.items(): if isinstance(arg, object_model.Object): arg = object_model.build_model(arg) final_kwargs[name] = arg cls = obj if isinstance(obj, dsl_types.MuranoType) else obj.type runtime_version = cls.package.runtime_version yaql_engine = yaql_integration.choose_yaql_engine(runtime_version) with helpers.with_object_store(self.executor.object_store): return dsl.to_mutable(cls.invoke( name, obj, tuple(final_args), final_kwargs), yaql_engine) except dsl_exception.MuranoPlException as e: if not self.preserve_exception: original_exception = getattr(e, 'original_exception', None) if original_exception and not isinstance( original_exception, dsl_exception.MuranoPlException): exc_traceback = getattr( e, 'original_traceback', None) or sys.exc_info()[2] utils.reraise( type(original_exception), original_exception, exc_traceback) raise def __getattr__(self, item): if item.startswith('test'): return getattr(Runner.DslObjectWrapper(self._root, self), item) def on(self, obj): return Runner.DslObjectWrapper(obj, self) def on_class(self, class_name): cls = self.executor.package_loader.load_class_package( class_name, helpers.parse_version_spec(None)).find_class( class_name, False) return Runner.DslObjectWrapper(cls, self) @property def root(self): return self._root @property def serialized_model(self): return serializer.serialize_model(self._root, self.executor) @property def preserve_exception(self): return self._preserve_exception @preserve_exception.setter def preserve_exception(self, value): self._preserve_exception = value def session(self): return helpers.with_object_store(self.executor.object_store) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/dsl/foundation/test_case.py0000664000175000017500000000460500000000000024025 0ustar00zuulzuul00000000000000# 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 inspect import os.path import eventlet.debug from murano.tests.unit import base from murano.tests.unit.dsl.foundation import runner from murano.tests.unit.dsl.foundation import test_package_loader class DslTestCase(base.MuranoTestCase): def setUp(self): super(DslTestCase, self).setUp() directory = os.path.join(os.path.dirname( inspect.getfile(self.__class__)), 'meta') root_meta_directory = os.path.join( os.path.dirname(__file__), '../../../../../meta') sys_package_loader = test_package_loader.TestPackageLoader( os.path.join(root_meta_directory, 'io.murano/Classes'), 'io.murano') self._package_loader = test_package_loader.TestPackageLoader( directory, 'tests', sys_package_loader) self._functions = {} self.register_function( lambda data: self._traces.append(data), 'trace') self._traces = [] self._runners = [] eventlet.debug.hub_exceptions(False) def new_runner(self, model): r = runner.Runner(model, self.package_loader, self._functions) self._runners.append(r) return r def tearDown(self): super(DslTestCase, self).tearDown() for r in self._runners: r.executor.finalize(r.root) @property def traces(self): return self._traces @traces.deleter def traces(self): self._traces = [] @property def package_loader(self): return self._package_loader def register_function(self, func, name): self._functions[name] = func @staticmethod def find_attribute(model, obj_id, obj_type, name): for entry in model['Attributes']: if tuple(entry[:3]) == (obj_id, obj_type, name): return entry[3] ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/dsl/foundation/test_package_loader.py0000664000175000017500000001054200000000000026030 0ustar00zuulzuul00000000000000# 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 fnmatch import os.path from murano.dsl import constants from murano.dsl import murano_package from murano.dsl import namespace_resolver from murano.dsl import package_loader from murano.engine import yaql_yaml_loader from murano.tests.unit.dsl.foundation import object_model class TestPackage(murano_package.MuranoPackage): def __init__(self, pkg_loader, name, version, runtime_version, requirements, configs, meta): self.__configs = configs super(TestPackage, self).__init__( pkg_loader, name, version, runtime_version, requirements, meta) def get_class_config(self, name): return self.__configs.get(name, {}) def get_resource(self, name): pass class TestPackageLoader(package_loader.MuranoPackageLoader): _classes_cache = {} def __init__(self, directory, package_name, parent_loader=None, meta=None): self._package_name = package_name self._yaml_loader = yaql_yaml_loader.get_loader('1.0') if directory in TestPackageLoader._classes_cache: self._classes = TestPackageLoader._classes_cache[directory] else: self._classes = {} self._build_index(directory) TestPackageLoader._classes_cache[directory] = self._classes self._parent = parent_loader self._configs = {} self._package = TestPackage( self, package_name, None, constants.RUNTIME_VERSION_1_0, None, self._configs, meta) for name, payload in self._classes.items(): self._package.register_class(payload, name) super(TestPackageLoader, self).__init__() def load_package(self, package_name, version_spec): if package_name == self._package_name: return self._package elif self._parent: return self._parent.load_package(package_name, version_spec) else: raise KeyError(package_name) def load_class_package(self, class_name, version_spec): if class_name in self._classes: return self._package elif self._parent: return self._parent.load_class_package(class_name, version_spec) else: raise KeyError(class_name) def export_fixation_table(self): return {} def import_fixation_table(self, fixations): pass def compact_fixation_table(self): pass def _build_index(self, directory): yamls = [ os.path.join(dirpath, f) for dirpath, _, files in os.walk(directory) for f in fnmatch.filter(files, '*.yaml') if f != 'manifest.yaml' ] for class_def_file in yamls: self._load_classes(class_def_file) def _load_classes(self, class_def_file): with open(class_def_file, 'rb') as stream: data_lst = self._yaml_loader(stream.read(), class_def_file) last_ns = {} for data in data_lst: last_ns = data.get('Namespaces', last_ns.copy()) if 'Name' not in data: continue for name, method in (data.get('Methods') or data.get( 'Workflow') or {}).items(): if name.startswith('test'): method['Scope'] = 'Public' ns = namespace_resolver.NamespaceResolver(last_ns) class_name = ns.resolve_name(data['Name']) self._classes[class_name] = data_lst def set_config_value(self, class_name, property_name, value): if isinstance(class_name, object_model.Object): class_name = class_name.type_name self._configs.setdefault(class_name, {})[ property_name] = value def register_package(self, package): super(TestPackageLoader, self).register_package(package) ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1696417899.841181 murano-16.0.0/murano/tests/unit/dsl/meta/0000775000175000017500000000000000000000000020254 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/dsl/meta/AgentListenerTests.yaml0000664000175000017500000000045300000000000024731 0ustar00zuulzuul00000000000000Name: AgentListenerTests Namespaces: sys: io.murano.system Properties: agentListener: Contract: $.class(sys:AgentListener) Usage: Runtime Methods: testAgentListener: Body: - $.agentListener: new(sys:AgentListener, $this, name => 'hello') - Return: $.agentListener ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/dsl/meta/CommonParent.yaml0000664000175000017500000000100100000000000023532 0ustar00zuulzuul00000000000000Name: CommonParent Properties: rootProperty: Contract: $.string() templateProperty: Contract: $.template('io.murano.Object') Methods: testRootMethod: Body: - trace('CommonParent::testRootMethod') - trace($.rootProperty) setPrivatePropertyChain: Body: - $.privateName: 'CommonParent' - trace($.privateName) virtualMethod: Body: - trace('CommonParent::virtualMethod') --- Name: TemplateTestParent Properties: foo: Contract: $.int().notNull()././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/dsl/meta/ConcurrencyTest.yaml0000664000175000017500000000603700000000000024300 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. Namespaces: std: io.murano m: io.murano.metadata.engine Name: TestConcurrency Methods: isolated: Arguments: - call: Contract: $.string() Body: - trace(format('call-{0}-before', $call)) - yield() - trace(format('call-{0}-after', $call)) isolatedWithDefault: Meta: - m:Synchronize: Arguments: - call: Contract: $.string() Body: - trace(format('call-{0}-before', $call)) - yield() - trace(format('call-{0}-after', $call)) concurrentExplicit: Meta: - m:Synchronize: onThis: false Arguments: - call: Contract: $.string() Body: - trace(format('call-{0}-before', $call)) - yield() - trace(format('call-{0}-after', $call)) isolatedExplicit: Meta: - m:Synchronize: onThis: true Arguments: - call: Contract: $.string() Body: - trace(format('call-{0}-before', $call)) - yield() - trace(format('call-{0}-after', $call)) argbasedPrimitive: Meta: - m:Synchronize: onArgs: flag Arguments: - call: Contract: $.string() - flag: Contract: $.string() Body: - trace(format('call-{0}-before', $call)) - yield() - trace(format('call-{0}-after', $call)) argBasedWithObject: Meta: - m:Synchronize: onArgs: flag Arguments: - call: Contract: $.string() - flag: Contract: $.class(std:Object) Body: - trace(format('call-{0}-before', $call)) - yield() - trace(format('call-{0}-after', $call)) testCallIsolated: Body: - list(1, 2, 3).pselect($this.isolated($)) testCallIsolatedWithDefault: Body: - list(1, 2, 3).pselect($this.isolatedWithDefault($)) testCallConcurrentExplicit: Body: - list(1, 2, 3).pselect($this.concurrentExplicit($)) testCallIsolatedExplicit: Body: - list(1, 2, 3).pselect($this.isolatedExplicit($)) testCallArgbasedPrimitiveIsolated: Body: - list(1, 2, 3).pselect($this.argbasedPrimitive($, same)) testCallArgbasedPrimitiveConcurrent: Body: - list(1, 2, 3).pselect($this.argbasedPrimitive($, $)) testCallArgbasedWithObjectIsolated: Body: - $o: new(std:Object) - list(1, 2, 3).pselect($this.argBasedWithObject($, $o)) testCallArgbasedWithObjectConcurrent: Body: - list(1, 2, 3).pselect($this.argBasedWithObject($, new(std:Object))) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/dsl/meta/ConfigProperties.yaml0000664000175000017500000000045000000000000024421 0ustar00zuulzuul00000000000000Name: ConfigProperties Properties: cfgProperty: Usage: Config Contract: $.int().notNull() Default: 123 normalProperty: Contract: $.string().notNull() Default: DEFAULT Methods: testPropertyValues: Body: - trace($.cfgProperty) - trace($.normalProperty) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/dsl/meta/ContractExamples.yaml0000664000175000017500000001270700000000000024423 0ustar00zuulzuul00000000000000Name: ContractExamples Extends: CommonParent Properties: sampleClass: Contract: $.class(SampleClass1) ordinaryProperty: Contract: $.string() templateProperty: Contract: $.template(TemplateTestChild, excludeProperties => [bar]) Methods: testStringContract: Arguments: arg: Contract: $.string() Body: Return: $arg testIntContract: Arguments: arg: Contract: $.int() Body: Return: $arg testBoolContract: Arguments: arg: Contract: $.bool() Body: Return: $arg testClassContract: Arguments: arg: Contract: $.class(SampleClass2) Body: Return: $arg testTemplateContract: Arguments: arg: Contract: $.template(CreatedClass2) Body: Return: $arg testTemplateContractExcludePropertyFromMpl: Body: - $model: :CreatedClass2: property1: qwerty property2: 'not integer' - Return: $.testTemplateContractExcludeProperty($model) testTemplateContractExcludeProperty: Arguments: arg: Contract: $.template(CreatedClass2, excludeProperties => [property2]) Body: Return: $arg testClassFromIdContract: Arguments: arg: Contract: $.class(SampleClass1) Body: Return: $arg testCheckContract: Arguments: - arg1: Contract: $.class(SampleClass2).check($.class2Property = qwerty) - arg2: Contract: $.int().check($ > 10) testOwnedContract: Arguments: - arg1: Contract: $.class(SampleClass1).owned() - arg2: Contract: $.class(SampleClass2).owned() testNotOwnedContract: Arguments: - arg1: Contract: $.class(SampleClass1).notOwned() - arg2: Contract: $.class(SampleClass2).notOwned() testScalarContract: Arguments: - arg1: Contract: 'fixed' - arg2: Contract: 456 - arg3: Contract: true Body: Return: $arg1 testListContract: Arguments: - arg: Contract: [$.int()] Body: Return: $arg testListWithMinLengthContract: Arguments: - arg: Contract: [$.int(), 3] Body: Return: $arg testListWithMinMaxLengthContract: Arguments: - arg: Contract: [$.int(), 2, 4] Body: Return: $arg testDictContract: Arguments: - arg: Contract: A: $.string() B: $.int() Body: Return: $arg testDictExprContract: Arguments: - arg: Contract: $.int(): $.string() B: $.int() Body: Return: $arg testDictMultiExprContract: Arguments: - arg: Contract: $.int(): $.string() $.string(): $.int() Body: Return: $arg testNotNullContract: Arguments: - arg: Contract: $.notNull() Body: Return: $arg testDefault: Arguments: - arg: Contract: $.string() Default: DEFAULT Body: Return: $arg testDefaultExpression: Arguments: - arg: Contract: $.string() Default: $.ordinaryProperty Body: Return: $arg testActionMeta: Scope: Public Meta: - io.murano.metadata.Title: text: "Title of the method" - io.murano.metadata.Description: text: "Description of the method" - io.murano.metadata.HelpText: text: "HelpText of the method" notAction: Scope: Session testAction: Scope: Public --- Name: TestedTarget Properties: prop: Contract: - $.string() Methods: foo: Arguments: - contracted: Contract: - $.string() Body: - Return: $contracted[2] wildList: Arguments: - contracted: Contract: - $ Body: - Return: $contracted[1][2] wildContract: Arguments: - untyped: Contract: $ Body: - Return: $untyped[1] typedList: Arguments: - contracted: Contract: - [$.string()] Body: - Return: $contracted[1][2] dictArgs: Arguments: - arg: Contract: {$.string(): [$.int()]} Body: - Return: $arg.get('a')[1] --- Name: TestIteratorsTransform Methods: testProperties: Body: - $.target: new(TestedTarget, $this, prop => ['1', '2', '3'].where($)) - Return: $.target.prop[2] testArgs: Body: - $.target: new(TestedTarget, $this) - Return: $.target.foo(['1', '2', '3'].where($)) testUntypedArgs: Body: - $.target: new(TestedTarget, $this) - Return: $.target.wildContract(['1', '2', '3'].where($)) testNotTypedListArgs: Body: - $.target: new(TestedTarget, $this) - Return: $.target.wildList([['1', '2', '3'].where($), ['4', '5', '6'].where($)].where($)) testTypedList: Body: - $.target: new(TestedTarget, $this) - Return: $.target.typedList([['1', '2', '3'].where($), ['4', '5', '6'].where($)].where($)) testListDict: Body: - $.target: new(TestedTarget, $this) - Return: $.target.dictArgs({'a' => [1, 2, 4].where($)}) --- Name: TemplateTestChild Properties: bar: Contract: $.int().notNull() --- Name: TemplatePropertyClass Properties: owned: Contract: $.class(Node) template: Contract: $.template(Node) Methods: testTemplateWithExternallyOwnedObject: Body: - Return: new($.template).nodes.select(id($)) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/dsl/meta/CreatedClass1.yaml0000664000175000017500000000111400000000000023553 0ustar00zuulzuul00000000000000Name: CreatedClass1 Properties: property1: Contract: $.string() property2: Contract: $.int() xxx: Contract: $ Usage: Out Methods: .init: Arguments: - property1: Contract: $.string() Body: - trace('CreatedClass1::.init') - trace($property1) - $.property1: STRING - trace($.property1) - trace($.property2) createClass2: Arguments: - parent: Contract: $.class(CreatingClass) Body: - $.xxx: new(CreatedClass2, $parent, QQQ, property1 => STR, property2 => 99) - Return: $././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/dsl/meta/CreatedClass2.yaml0000664000175000017500000000033400000000000023557 0ustar00zuulzuul00000000000000Name: CreatedClass2 Properties: property1: Contract: $.string() property2: Contract: $.int() Methods: .init: Body: - $.find(CreatingClass).require() - trace('CreatedClass2::.init') ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/dsl/meta/CreatingClass.yaml0000664000175000017500000000567300000000000023675 0ustar00zuulzuul00000000000000Namespaces: std: io.murano Name: CreatingClass Properties: yyy: Contract: $ Usage: Out Methods: .init: Body: trace('CreatingClass::.init') testNew: Body: - new(CreatedClass1, property1 => string, property2 => 123) testNewWithOwnership: Body: - $.yyy: new(CreatedClass1, property1 => string, property2 => 123) - Return: $.yyy.createClass2($this) testNewWithDict: Body: - $dict: property1: string property2: 123 - Return: new(CreatedClass1, $dict, owner=>$this) testLoadCompexModel: Body: - $model: :Node: value: rootNode nodes: - :Node: value: childNode1 nodes: [node0, node2] id: node1 - '?': id: node2 type: Node value: childNode2 nodes: [node1, node2] - node2 id: node0 - $obj: new($model, $this) - Return: - id($obj) - id($obj.nodes[0]) - id($obj.nodes[1]) - id($obj.find(std:Object)) - $obj.value - $obj.nodes.select($.value) - $obj.nodes[0].nodes[0] = $obj - $obj.nodes[0].nodes[1] = $obj.nodes[1] - $obj.nodes[2] = $obj.nodes[1] - $obj.nodes[1].nodes[0] = $obj.nodes[0] - $obj.nodes[1].nodes[1] = $obj.nodes[1] - $obj.nodes[0].nodes[0].value - $obj.nodes[0].nodes[1].value - $obj.nodes[1].nodes[0].value testSingleContractInstantiation: Body: - $model: :ConstructionSample: - new(:ConstructionChild, prop => $model) testNestedNewLoadsInSeparateStore: Body: Return: new(:ConstructionFromInit).out.nodes[1] testReferenceAccessFromInit: Body: - $model: :Node: value: rootNode nodes: - childNode - :NodeWithReferenceAccess: value: childNode id: childNode - $.out: new($model, $this) --- Name: ConstructionSample Methods: .init: Body: trace('ConstructionSample::init') --- Name: ConstructionParent Properties: prop: Contract: $.class(ConstructionSample) --- Name: ConstructionChild Extends: ConstructionParent Properties: prop: Contract: $.class(ConstructionSample) --- Name: ConstructionFromInit Properties: out: Contract: $.class(Node) Usage: Out Methods: .init: Body: - $model: :Node: value: rootNode nodes: - :Node: value: childNode1 nodes: [childNode2] id: childNode1 - :Node: value: childNode2 nodes: [childNode1] id: childNode2 - $.out: new($model, $this) --- Name: NodeWithReferenceAccess Extends: Node Methods: .init: Body: $.find(Node).nodes.select(trace($.value))././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/dsl/meta/DerivedFrom2Classes.yaml0000664000175000017500000000435100000000000024751 0ustar00zuulzuul00000000000000Name: DerivedFrom2Classes Extends: [ParentClass1, ParentClass2] Properties: ambiguousProperty: Contract: $.string() Usage: InOut usageTestProperty1: Contract: $.int() usageTestProperty2: Contract: $.int() Usage: In usageTestProperty3: Contract: $.int() Usage: InOut usageTestProperty4: Contract: $.int() Usage: Out usageTestProperty5: Contract: $.int() Usage: Runtime usageTestProperty6: Contract: $.int() Usage: Const usageTestProperty7: Contract: $.int() Usage: Config Methods: setPrivateProperty: Body: - $.privateProperty: 99 testAccessAmbiguousPropertyWithResolver: Body: Return: $.ambiguousProperty testPropertyMerge: Body: - trace($.ambiguousProperty) - $.setAmbiguousProperty() - trace($.ambiguousProperty) - trace($.getAmbiguousProperty()) - trace($.cast(ParentClass1).ambiguousProperty) - trace($.cast(ParentClass2).ambiguousProperty) - Return: $.ambiguousProperty testModifyUsageTestProperty1: Body: - $.usageTestProperty1: 11 - Return: $.usageTestProperty1 testModifyUsageTestProperty2: Body: - $.usageTestProperty2: 22 - Return: $.usageTestProperty2 testModifyUsageTestProperty3: Body: - $.usageTestProperty3: 33 - Return: $.usageTestProperty3 testModifyUsageTestProperty4: Body: - $.usageTestProperty4: 44 - Return: $.usageTestProperty4 testModifyUsageTestProperty5: Body: - $.usageTestProperty5: 55 - Return: $.usageTestProperty5 testModifyUsageTestProperty6: Body: - $.usageTestProperty6: 66 - Return: $.usageTestProperty6 testModifyUsageTestProperty7: Body: - $.usageTestProperty7: 77 - Return: $.usageTestProperty7 testMixinOverride: Body: - $.virtualMethod() - trace('-') - cast($, CommonParent).virtualMethod() - trace('-') - $.cast(ParentClass1).virtualMethod() - trace('-') - $.cast(ParentClass2).virtualMethod() testSuper: Body: - super($, $.virtualMethod()) - $.super($.virtualMethod()) testPsuper: Body: - psuper($, $.virtualMethod()) - $.psuper($.virtualMethod()) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/dsl/meta/Empty.yaml0000664000175000017500000000001400000000000022231 0ustar00zuulzuul00000000000000Name: Empty ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/dsl/meta/ExceptionHandling.yaml0000664000175000017500000000221600000000000024544 0ustar00zuulzuul00000000000000Name: ExceptionHandling Methods: testThrow: Arguments: - enum: Contract: $.int().notNull() Body: Try: - trace('enter try') - $.doThrow($enum) - trace('exit try') Catch: - With: exceptionName As: e Do: - trace($e.message) - With: anotherExceptionName As: e Do: - trace($e.message) - trace(rethrow) - Rethrow: - As: e Do: - trace('catch all') - trace($e.message) Else: - trace('else section') Finally: - trace('finally section') doThrow: Arguments: - enum: Contract: $.int().notNull() Body: - Match: 1: - Throw: exceptionName Message: exception message 2: - Throw: anotherExceptionName Message: exception message 2 3: - Throw: thirdExceptionName Message: exception message 3 4: - Return: Value: $enum testStackTrace: Body: raisePythonException() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/dsl/meta/MacroExamples.yaml0000664000175000017500000001053000000000000023677 0ustar00zuulzuul00000000000000Name: MacroExamples Methods: testIf: Arguments: arg: Contract: $.int() Body: - If: $arg > 5 Then: Return: gt - Return: def testIfElse: Arguments: arg: Contract: $.int() Body: - If: $arg > 5 Then: Return: gt Else: Return: lt - Return: def testIfNonBoolean: Body: - $res: 0 - If: null Then: $res: $res + 1 - If: list() Then: $res: $res + 10 - If: qwerty Then: $res: $res + 100 - If: 123 Then: $res: $res + 1000 - Return: $res testWhile: Arguments: arg: Contract: $.int() Body: - While: $arg > 0 Do: - trace($arg) - $arg: $arg - 1 - Return: $arg testWhileNonBoolean: Body: - $lst: list(1, 2, 3) - While: $lst Do: - $lst: $lst.delete(0) - Return: $lst testFor: Body: - For: t In: [x, y, z] Do: - trace($t) - $col: [1, 2, 3] - For: t In: $col.select($ * $) Do: - trace($t + 1) testRepeat: Arguments: count: Contract: $.int() Body: - Repeat: $count Do: - trace(run) testBreak: Body: - For: t In: range(0, 7) Do: - If: $t = 3 Then: - trace(breaking) - Break: - trace($t) - trace(method_break) - Break: testContinue: Body: - For: t In: range(0, 7) Do: - If: $t >= 3 and $t < 5 Then: - Continue: - trace($t) - trace(method_continue) - Continue: testMatch: Arguments: arg: Contract: $.int() Body: - Match: 2: Return: x 1: Return: y 3: Return: z Value: $arg testMatchDefault: Arguments: arg: Contract: $.int() Body: - Match: 2: Return: x 1: - Return: y 3: Return: z Default: - Return: def Value: $arg testSwitch: Arguments: arg: Contract: $.int() Body: - Switch: $arg > 10: trace(gt) $arg < 10: trace(lt) $arg > 100: trace(gt100) testSwitchNonBoolean: Body: - $res: 0 - Switch: 0: $res: $res + 1 null: $res: $res + 10 list(): $res: $res + 100 '': $res: $res + 1000 qwerty: $res: $res + 10000 list(1): $res: $res + 100000 12: $res: $res + 1000000 - Return: $res testSwitchDefault: Arguments: arg: Contract: $.int() Body: - Switch: $arg > 10: trace(gt) $arg < 0: - trace(lt) $arg > 100: trace(gt100) Default: - trace(def) testCodeBlock: Body: - Do: - trace(a) - $res: 123 - trace($res) - Return: $res testParallel: Body: Parallel: - Do: - trace(enter) - sleep(0) - trace(exit) - Do: - trace(enter) - sleep(0) - trace(exit) testParallelWithLimit: Body: Parallel: - Do: - trace(enter) - sleep(0) - trace(exit) - Do: - trace(enter) - sleep(0) - trace(exit) - Do: - trace(enter) - sleep(0) - trace(exit) Limit: 2 testScopeWithinMacro: Body: - $x: 0 - $c: 1 - If: $x = 0 Then: $x: $x + 1 - While: $x = 1 Do: $x: $x + 20 - For: t In: [1] Do: $x: $x + 300 - Repeat: 1 Do: $x: $x + 4000 - Match: 1: $x: $x + 50000 Value: $c - Switch: $c > 0: $x: $x + 600000 - Do: $x: $x + 7000000 - Parallel: - Do: $x: $x + 80000000/2 - Do: $x: $x + 80000000/2 - Return: $x././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/dsl/meta/Node.yaml0000664000175000017500000000014700000000000022027 0ustar00zuulzuul00000000000000Name: Node Properties: nodes: Contract: - $.class(Node) value: Contract: $.string() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/dsl/meta/ParentClass1.yaml0000664000175000017500000000064100000000000023441 0ustar00zuulzuul00000000000000Name: ParentClass1 Extends: CommonParent Properties: ambiguousProperty: Contract: $.string() Usage: InOut Methods: method1: Body: - trace('ParentClass1::method1') setPrivatePropertyChain: Body: - $.privateName: 'ParentClass1' - $.cast(CommonParent).setPrivatePropertyChain() - trace($.privateName) setAmbiguousProperty: Body: $.ambiguousProperty: '555' ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/dsl/meta/ParentClass2.yaml0000664000175000017500000000051200000000000023437 0ustar00zuulzuul00000000000000Name: ParentClass2 Extends: CommonParent Properties: ambiguousProperty: Contract: $.string() Usage: InOut Methods: method2: Body: - trace('ParentClass2::method2') getAmbiguousProperty: Body: Return: $.ambiguousProperty virtualMethod: Body: - trace('ParentClass2::virtualMethod') ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/dsl/meta/PropertyInit.yaml0000664000175000017500000000206500000000000023613 0ustar00zuulzuul00000000000000Name: PropertyInit Properties: runtimePropertyWithoutDefault: Usage: Runtime Contract: $.string() runtimePropertyWithStrictContractWithoutDefault: Usage: Runtime Contract: $.string().notNull() runtimeProperty2WithStrictContractWithoutDefault: Usage: Runtime Contract: $.string().notNull() agentListener: Contract: $.class('io.murano.system.AgentListener') runtimePropertyWithStrictContractAndDefault: Usage: Runtime Contract: $.string().notNull() Default: DEFAULT Methods: initialize: Body: $.runtimePropertyWithStrictContractWithoutDefault: VALUE testRuntimePropertyWithoutDefault: Body: - Return: $this.runtimePropertyWithoutDefault testRuntimePropertyDefault: Body: - Return: $this.runtimePropertyWithStrictContractAndDefault testRuntimePropertyWithStrictContractWithoutDefault: Body: - Return: $this.runtimePropertyWithStrictContractWithoutDefault testUninitializedRuntimeProperty: Body: - Return: $this.runtimeProperty2WithStrictContractWithoutDefault ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/dsl/meta/SampleClass1.yaml0000664000175000017500000000410200000000000023425 0ustar00zuulzuul00000000000000Name: SampleClass1 Properties: stringProperty: Contract: $.string().notNull() classProperty: Contract: $.class(SampleClass2).notNull() assignedProperty: Contract: $ Usage: Runtime arbitraryProperty: Contract: $ Workflow: testTrace: Arguments: - intArg: Contract: $.int().notNull() Body: - trace($intArg) - trace($.stringProperty) - trace($.classProperty.class2Property) testException: Body: - raiseException() testReturn: Arguments: - intArg: Contract: $.int().notNull() Body: Return: $intArg testCallAnotherMethod: Body: - trace(method1) - $.anotherMethod() anotherMethod: Body: - trace(method2) testAttributes: Arguments: - arg: Contract: $.string() Body: - $.setAttr(att1, $arg) - $x: $.getAttr(att1) - $y: $.getAttr(att2, ' Doe') - Return: $x + $y testAssignment: Body: - $result: {} - $result.Arr: [1, 2, [10, 11]] - $index: 1 - $result.Arr[0]: 3 - $result.Arr[$index - 1]: 5 - $result.Arr[$index + 1][1]: 123 #- $result.Dict: {} - $result.Dict.Key1: V1 - $keyName: Key2 - $result.Dict[$keyName]: {} - $result.Dict[$keyName]['a_b']: V2 - $result.Dict[$keyName][toUpper($keyName)]: V3 - Return: $result testAssignmentOnProperty: Body: #- $.assignedProperty: {} - $.assignedProperty.Arr: [1, 2, [10, 11]] - $index: 1 - $.assignedProperty.Arr[0]: 3 - $.assignedProperty.Arr[$index - 1]: 5 - $.assignedProperty.Arr[$index + 1][1]: 123 #- $.assignedProperty.Dict: {} - $.assignedProperty.Dict.Key1: V1 - $keyName: Key2 - $.assignedProperty.Dict[$keyName]: {} - $.assignedProperty.Dict[$keyName]['a_b']: V2 - $.assignedProperty.Dict[$keyName][toUpper($keyName)]: V3 - Return: $.assignedProperty testAssignByCopy: Arguments: - arg: Contract: [$.int()] Body: - $x: $arg - $x[0]: 321 - Return: $arg ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/dsl/meta/SampleClass2.yaml0000664000175000017500000000042200000000000023427 0ustar00zuulzuul00000000000000Name: SampleClass2 Properties: class2Property: Contract: $.string().notNull() Methods: testMethod: Body: Return: key1: abc key2: [a, b, c] key3: null key4: false key5: x: y key6: - w: q ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/dsl/meta/SampleClass3.yaml0000664000175000017500000000153100000000000023432 0ustar00zuulzuul00000000000000Name: SampleClass3 Properties: multiClassProperty: Contract: $.class(ParentClass1).class(ParentClass2) Methods: testMultiContract: Body: - $.multiClassProperty.method1() - $.multiClassProperty.method2() testPropertyAccessibleOnSeveralPaths: Body: Return: $.multiClassProperty.rootProperty testPrivateProperty: Body: - $.privateName: 'SampleClass3' - $.multiClassProperty.setPrivatePropertyChain() - trace($.privateName) testUninitializedPrivatePropertyAccess: Body: Return: $.privateName testReadOfPrivatePropertyOfOtherClass: Body: - $.multiClassProperty.setPrivateProperty() - trace('accessing property') - Return: $.multiClassProperty.privateProperty testWriteOfPrivatePropertyOfOtherClass: Body: $.multiClassProperty.privateProperty: 123 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/dsl/meta/SingleInheritanceChild.yaml0000664000175000017500000000050100000000000025473 0ustar00zuulzuul00000000000000Name: SingleInheritanceChild Extends: SingleInheritanceParent Methods: testVirtualCalls: Body: $.method1() method1: Body: - trace('SingleInheritanceChild::method1') - super($, $.method1()) method2: Body: - trace('SingleInheritanceChild::method2') - $.super($.method2())././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/dsl/meta/SingleInheritanceParent.yaml0000664000175000017500000000031000000000000025677 0ustar00zuulzuul00000000000000Name: SingleInheritanceParent Methods: method1: Body: - trace('SingleInheritanceParent::method1') - $.method2() method2: Body: trace('SingleInheritanceParent::method2')././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/dsl/meta/TestCall.yaml0000664000175000017500000000162400000000000022656 0ustar00zuulzuul00000000000000Name: OtherClass Methods: toCall: Arguments: - source: Contract: $.string() - foo: Contract: $ - bar: Contract: $ - baz: Contract: $ Body: - trace('called as ' + $source) staticMethod: Usage: Static Body: - trace('called as static') --- # ------------------------------------------------------------------------ Name: TestCall Methods: .init: Body: - $.other: new(OtherClass) testCall: Body: - call('toCall', ['call', 'foo'], {baz=>1, bar=>2}, $.other) testMethodInvocation: Body: - $.other.toCall('method', 'foo', baz=>baz, bar=>bar) testCallStatic: Body: - call('staticMethod', [], {}, :OtherClass) testCallStaticAsInstance: Body: - call('staticMethod', [], {}, $.other) --- # ------------------------------------------------------------------------ ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/dsl/meta/TestDump.yaml0000664000175000017500000000463200000000000022712 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. Namespaces: =: dumptests std: io.murano m: io.murano.metadata.engine --- # ------------------------------------------------------------------ # --- Name: DumpTarget1 Properties: foo: Contract: $.string() bar: Contract: - $.int() baz: Contract: $.string(): $.int() Methods: getOwner: Body: - Return: $.find(DumpTarget2).require() --- # ------------------------------------------------------------------ # --- Name: DumpTarget2 Properties: nested: Usage: InOut Contract: $.class(std:Object) another: Contract: $.class(DumpTarget1) ref: Usage: InOut Contract: $.class(std:Object) --- # ------------------------------------------------------------------ # --- Name: DumpTarget3 Properties: a: Meta: - m:Serialize: as: copy Contract: $.class(DumpTarget1) b: Meta: - m:Serialize: as: reference Contract: $.class(DumpTarget1) --- # ------------------------------------------------------------------ # --- Name: DumpTarget4 Extends: DumpTarget1 Properties: qux: Contract: $.string().notNull() --- # ------------------------------------------------------------------ # --- Name: TestDump Methods: testDump: Arguments: - object: Contract: $.class(std:Object).notNull() - serializationType: Contract: $.string().check($ in [Serializable, Mixed, Inline]) Default: 'Inline' Body: - Return: dump($object, $serializationType, true) testDumpWithUpcast: Arguments: - object: Contract: $.class(std:Object).notNull() - doUpcast: Contract: $.bool().notNull() - passIgnoreUpcast: Contract: $.bool().notNull() Body: - If: $doUpcast Then: - $object: $object.cast(DumpTarget1) - Return: dump($object, Inline, $passIgnoreUpcast) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/dsl/meta/TestEngineFunctions.yaml0000664000175000017500000001440600000000000025103 0ustar00zuulzuul00000000000000Namespaces: sys: io.murano.system std: io.murano Name: TestEngineFunctions Properties: target: Usage: Runtime Contract: $.class(std:Object) Methods: testJoin: Body: - $arr: [xx, 123] - Return: (' '.join($arr)) testSplit: Body: - Return: ('x yy 123').split(' ') testLen: Body: - $a: str - $b: [1, 2, 3, 4] - $c: {'a': 'xxx' } - Return: len($a) + len($b) + len($c) testCoalesce: Arguments: - arg1: Contract: $.string() - arg2: Contract: $.string() - arg3: Contract: $.string() Body: Return: coalesce($arg1, $arg2, $arg3) testBase64Encode: Arguments: - arg: Contract: $.string() Body: Return: base64encode($arg) testBase64Decode: Arguments: - arg: Contract: $.string() Body: Return: base64decode($arg) testFormat: Arguments: - format: Contract: $.string() - arg1: Contract: $.string() - arg2: Contract: $.string() Body: Return: $format.format($arg1, $arg2) testReplaceStr: Arguments: - what: Contract: $.string() - old: Contract: $.string() - new: Contract: $.string() Body: Return: $what.replace($old, $new) testReplaceDict: Arguments: - what: Contract: $.string() - with: Contract: $.string(): $.string() Body: Return: $what.replace($with) testToLower: Arguments: - arg: Contract: $.string() Body: Return: toLower($arg) testToUpper: Arguments: - arg: Contract: $.string() Body: Return: toUpper($arg) testStartsWith: Arguments: - what: Contract: $.string() - arg: Contract: $.string() Body: Return: $what.startsWith($arg) testEndsWith: Arguments: - what: Contract: $.string() - arg: Contract: $.string() Body: Return: $what.endsWith($arg) testTrim: Arguments: - arg: Contract: $.string() Body: Return: trim($arg) testSubstr: Arguments: - str: Contract: $.string() - arg1: Contract: $.int() - arg2: Contract: $.int() Body: Return: $str.substr(0, $arg1) + $str.substr($arg1, $arg2) + $str.substr($arg1 + $arg2) testStr: Arguments: - arg: Contract: $ Body: Return: str($arg) testInt: Arguments: - arg: Contract: $.string() Body: Return: int($arg) testKeys: Arguments: - arg: Contract: {} Body: Return: $arg.keys() testValues: Arguments: - arg: Contract: {} Body: Return: $arg.values() testFlatten: Arguments: - arg: Contract: [] Body: Return: $arg.flatten() testDictGet: Arguments: - dict: Contract: $.string(): $ - key: Contract: $.string().notNull() Body: Return: $dict.get($key) testRandomName: Body: Return: randomName() testPSelect: Arguments: - arg: Contract: [$.int().notNull()] Body: Return: $arg.pselect($ * $) testBind: Arguments: - template: Contract: {} - args: Contract: {} Body: Return: $template.bind($args) testPatch: Body: - $patches: - op: add path: '/foo' value: bar - op: add path: '/baz' value: [1, 2, 3] - op: remove path: '/baz/1' - op: test path: '/baz' value: [1, 3] - op: replace path: '/baz/0' value: 42 - op: remove path: '/baz/1' - $doc: {} - Return: $doc.patch($patches) testTake: Arguments: - list: Contract: [$.int()] - count: Contract: $.int() Body: - Return: $list.take($count) testSkip: Arguments: - list: Contract: [$.int()] - count: Contract: $.int() Body: - Return: $list.skip($count) testSkipTake: Arguments: - list: Contract: [$.int()] - start: Contract: $.int() - count: Contract: $.int() Body: - $l: $list.skip($start) - Return: $l.take($count) testSkipTakeChained: Arguments: - list: Contract: [$.int()] - start: Contract: $.int() - count: Contract: $.int() Body: - Return: $list.skip($start).take($count) testAggregate: Arguments: - list: Contract: [$.int()] Body: - Return: $list.aggregate($1 + $2) testAggregateWithInitializer: Arguments: - list: Contract: [$.int()] - initializer: Contract: $.int() Body: - Return: $list.aggregate($1 + $2, $initializer) testId: Body: Return: id($) + $.id() testType: Body: Return: type($) + $.type() testIsOperator: Body: - $logger: new(sys:Logger) - $.innerVariable: new(sys:Logger) - $derivedClassObject: new(DerivedFrom2Classes) - Return: $logger is sys:Logger and $ is 'TestEngineFunctions' and $.innerVariable is 'io.murano.system.Logger' and $derivedClassObject is ParentClass1 and $derivedClassObject is ParentClass2 testNegativeIsOperator: Arguments: - nullArg: Contract: $.class(sys:Logger) Default: null Body: - $nullVariable: null - Return: $nullVariable is sys:Logger or $nullArg is sys:Logger testNewObjectAssignment: Body: - $newObject: new(std:Object) # Assignment of object-contracted properties by IDs is possible only # if the object with the given id is present in the object store. # Thus this assignment tests the fix for bug #1597452 - $this.target: id($newObject) - Return: $this.target = $newObject testDecryptData: Arguments: - value: Contract: $.string().notNull() Body: - Return: decryptData($value) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/dsl/meta/TestExtensionMethods.yaml0000664000175000017500000000465300000000000025310 0ustar00zuulzuul00000000000000Namespaces: =: extcls --- # ------------------------------------------------------------------ # --- Name: Extended Properties: prop: Contract: $.int() Default: 123 Methods: method: Body: Return: $.prop --- # ------------------------------------------------------------------ # --- Name: Extender Methods: importedExtensionMethod: Usage: Extension Arguments: - obj: Contract: $.class(Extended).notNull() - n: Contract: $.int().notNull() Body: Return: [$obj.prop * $n, $obj.method() * $n] nullableExtension: Usage: Extension Arguments: - obj: Contract: $.class(Extended) Body: Return: $obj?.prop extensionMethod: Usage: Extension Arguments: - obj: Contract: $.class(Extended).notNull() Body: Return: 222 toTileCase: Usage: Extension Arguments: - str: Contract: $.string().notNull() Body: Return: join($str.toCharArray().select( selectCase($.toLower() = $).switchCase($.toUpper(), $.toLower())), '') --- # ------------------------------------------------------------------ # --- Name: TestClass Import: Extender Methods: testSelfExtensionMethod: Body: Return: new(Extended).selfExtensionMethod() testImportedExtensionMethod: Body: Return: new(Extended).importedExtensionMethod(2) testNullableExtensionMethod: Body: Return: - new(Extended).nullableExtension() - null.nullableExtension() testExtensionsPrecedence: Body: Return: new(Extended).extensionMethod() testCallOnPrimitiveTypes: Body: Return: QwertY.toTileCase() testCallExtensionExplicitly: Body: Return: :Extender.extensionMethod(new(:Extended)) testExplicitCallDoenstWorkOnInstance: Body: Return: new(Extended).extensionMethod(new(Extended)) testCallPythonExtension: Body: Return: 4.pythonExtension() testCallPythonExtensionExplicitly: Body: Return: :Extender.pythonExtension(5) testCallPythonClassmethodExtension: Body: Return: 7.pythonExtension2() selfExtensionMethod: Usage: Extension Arguments: - obj: Contract: $.class(Extended).notNull() Body: Return: [$obj.prop, $obj.method()] extensionMethod: Usage: Extension Arguments: - obj: Contract: $.class(Extended).notNull() Body: Return: 111 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/dsl/meta/TestFindClass.yaml0000664000175000017500000000047200000000000023651 0ustar00zuulzuul00000000000000Name: TestFindClass Methods: testFindClassWithPrefix: Body: - new('io.murano.extensions.io.murano.test.TestFixture') testFindClassShortName: Body: - new('io.murano.test.TestFixture') testClassWithPrefixNotFound: Body: - new('io.murano.extensions.io.murano.test.TestFixture1')././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/dsl/meta/TestGC.yaml0000664000175000017500000000711200000000000022272 0ustar00zuulzuul00000000000000Namespaces: sys: io.murano.system std: io.murano Name: TestGCNode Extends: Node Properties: runtimeProperty: Usage: Runtime Contract: $ Methods: .init: Body: - $this.runtimeProperty: new(Node) foo: Body: - trace(foo) destructionHandler: Arguments: - obj: Contract: $.class(TestGCNode).notNull() Body: - trace('Destruction of {0}'.format($obj.value)) .destroy: Body: - trace($.value) --- Name: TestGCNode2 Extends: TestGCNode Methods: .destroy: Body: - trace(list($.nodes.select(sys:GC.isDoomed($)))) - $owner: $.find(std:Object) - trace(sys:GC.isDoomed($owner)) --- Name: TestGCNode3 Extends: TestGCNode Methods: .init: Body: - $this.undeclaredProp: new(TestGCNode, value => C) --- Name: TestGC Properties: outNode: Usage: Out Contract: $.class(TestGCNode) staticPropertyNode: Usage: Static Contract: $.class(TestGCNode) Methods: testObjectsCollect: Body: - $model: :TestGCNode: value: A nodes: - :TestGCNode: value: B - new($model) - $localAssignedVariable: new($model) - sys:GC.collect() testObjectsCollectWithSubscription: Body: - $model: :TestGCNode: value: A nodes: :TestGCNode: value: B - $x: new($model) - sys:GC.subscribeDestruction($x, $this, _handler) - sys:GC.subscribeDestruction($x.nodes[0], $this, _handler) - sys:GC.subscribeDestruction($x.nodes[0], $x, destructionHandler) - $x: null - sys:GC.collect() _handler: Arguments: - obj: Contract: $.class(TestGCNode).notNull() Body: - trace('Destroy ' + $obj.value) testCallOnDestroyedObject: Body: - $val: new(TestGCNode, value => X) - sys:GC.subscribeDestruction($val, $this, _handler2) - $val: null - sys:GC.collect() - $this.destroyed.foo() _handler2: Arguments: - obj: Contract: $.class(TestGCNode).notNull() Body: - $obj.foo() - $this.destroyed: $obj testIsDoomed: Body: - $model: :TestGCNode2: value: A nodes: - :TestGCNode2: value: B - new($model, $this) - sys:GC.collect() testIsDestroyed: Body: - $val: new(Node, value => X) - sys:GC.subscribeDestruction($val, $this, _handler3) - $val: null - sys:GC.collect() - trace(sys:GC.isDestroyed($this.destroyed)) _handler3: Arguments: - obj: Contract: $.class(Node).notNull() Body: - trace(sys:GC.isDestroyed($obj)) - $this.destroyed: $obj testDestructionDependencySerialization: Body: - $model: :TestGCNode: value: A nodes: :TestGCNode: value: B - $.outNode: new($model) - sys:GC.subscribeDestruction($.outNode, $this, _handler) - sys:GC.subscribeDestruction($.outNode.nodes[0], $this, _handler) testStaticProperties: Body: - :TestGC.staticPropertyNode: new(TestGCNode, value => A) - sys:GC.collect() methodWithArgs: Arguments: - obj: Contract: $.class(TestGCNode).notNull() Body: - sys:GC.collect() testDestroyArgs: Body: - $.methodWithArgs(new(TestGCNode, value => A)) testReachableRuntimeProperties: Body: - $node: new(TestGCNode3, value => A) - sys:GC.collect() - trace(sys:GC.isDestroyed($node.runtimeProperty)) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/dsl/meta/TestLogger.yaml0000664000175000017500000000515200000000000023222 0ustar00zuulzuul00000000000000 Namespaces: sys: io.murano.system Name: TestLogger Methods: testCreate: Body: - Return: logger('name') testDebug: Arguments: - log: Contract: $.class(sys:Logger).notNull() Body: - $log.debug('str') - $log.debug('тест') - $log.debug('str', 1) - $log.debug('str {0}', message) - $log.debug('str {message}', message=>message) - $log.debug('str {message}{0}') testTrace: Arguments: - log: Contract: $.class(sys:Logger).notNull() Body: - $log.trace('str') - $log.trace('тест') - $log.trace('str', 1) - $log.trace('str {0}', message) - $log.trace('str {message}', message=>message) - $log.trace('str {message}{0}') testInfo: Arguments: - log: Contract: $.class(sys:Logger).notNull() Body: - $log.info('str') - $log.info('тест') - $log.info('str', 1) - $log.info('str {0}', message) - $log.info('str {message}', message=>message) - $log.info('str {message}{0}') testWarning: Arguments: - log: Contract: $.class(sys:Logger).notNull() Body: - $log.warning('str') - $log.warning('тест') - $log.warning('str', 1) - $log.warning('str {0}', message) - $log.warning('str {message}', message=>message) - $log.warning('str {message}{0}') testError: Arguments: - log: Contract: $.class(sys:Logger).notNull() Body: - $log.error('str') - $log.error('тест') - $log.error('str', 1) - $log.error('str {0}', message) - $log.error('str {message}', message=>message) - $log.error('str {message}{0}') testCritical: Arguments: - log: Contract: $.class(sys:Logger).notNull() Body: - $log.critical('str') - $log.critical('тест') - $log.critical('str', 1) - $log.critical('str {0}', message) - $log.critical('str {message}', message=>message) - $log.critical('str {message}{0}') testException: Arguments: - log: Contract: $.class(sys:Logger).notNull() Body: Try: - $.doThrow() Catch: With: exceptionName As: e Do: - $log.exception($e, 'str') - $log.exception($e, 'тест') - $log.exception($e, 'str', 1) - $log.exception($e, 'str {0}', message) - $log.exception($e, 'str {message}', message=>message) - $log.exception($e, 'str {message}{0}') Finally: doThrow: Body: - Throw: exceptionName Message: exception message ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/dsl/meta/TestMeta.yaml0000664000175000017500000001217300000000000022672 0ustar00zuulzuul00000000000000Namespaces: =: metatests Name: InheritedMultiMeta Usage: Meta Cardinality: Many Applies: All Inherited: true Properties: val: Contract: $.int().notNull() Default: 111 --- # ------------------------------------------------------------------------ Name: InheritedSingleMeta Usage: Meta Cardinality: One Applies: All Inherited: true Properties: val: Contract: $.int().notNull() Default: 222 --- # ------------------------------------------------------------------------ Name: InheritedSingleMeta2 Usage: Meta Cardinality: One Applies: All Inherited: true Extends: InheritedSingleMeta --- # ------------------------------------------------------------------------ Name: MultiMeta Usage: Meta Cardinality: Many Applies: All Inherited: false Properties: val: Contract: $.int().notNull() Default: 333 --- # ------------------------------------------------------------------------ Name: SingleMeta Usage: Meta Cardinality: One Applies: All Inherited: false Properties: val: Contract: $.int().notNull() Default: 444 --- # ------------------------------------------------------------------------ Name: SingleMeta2 Usage: Meta Cardinality: One Applies: All Inherited: false Extends: SingleMeta --- # ------------------------------------------------------------------------ Name: ComplexMeta Usage: Meta Cardinality: Many Properties: cls: Contract: $.class(PropertyType).notNull() --- # ------------------------------------------------------------------------ Name: ParentClass0 Meta: [InheritedMultiMeta, SingleMeta] Properties: prop1: Contract: $ Meta: - SingleMeta: val: 1 Methods: foo: Meta: - InheritedMultiMeta: val: 1 - InheritedSingleMeta: val: 2 - SingleMeta: val: 3 - InheritedSingleMeta2: val: 10 - SingleMeta2: val: 11 Arguments: arg: Contract: $.string() --- # ------------------------------------------------------------------------ Name: ParentClass1 Extends: ParentClass0 Meta: - InheritedMultiMeta: val: 1 - InheritedSingleMeta: val: 6 - SingleMeta: val: 7 Methods: foo: Meta: - InheritedMultiMeta: val: 4 - InheritedSingleMeta: val: 5 - SingleMeta: val: 6 Arguments: arg: Contract: $.string() --- # ------------------------------------------------------------------------ Name: ParentClass2 Extends: ParentClass0 Usage: Class Meta: - InheritedMultiMeta: val: 2 - SingleMeta: val: 3 Properties: prop2: Contract: $ Meta: - InheritedMultiMeta: val: 1 - MultiMeta: val: 2 - SingleMeta: val: 3 --- # ------------------------------------------------------------------------ Name: TestMeta Extends: [ParentClass1, ParentClass2] Meta: - 'metatests.InheritedMultiMeta': val: 4 - :SingleMeta: val: 5 Properties: prop2: Contract: $ Meta: - InheritedMultiMeta: val: 4 Methods: testClassMultiMeta: Body: - Return: typeinfo($).meta.where($ is InheritedMultiMeta).val testClassSingleMeta: Body: - Return: typeinfo($).meta. where($ is SingleMeta or $ is InheritedSingleMeta).val testParentClassNotInheritedMeta: Body: - Return: typeinfo(ParentClass2).meta. where(not typeinfo($).inherited).single().val testMethodMeta: Body: - Return: typeinfo($).methods.where($.name = foo).single().meta.val testMethodArgumentMeta: Body: - Return: typeinfo($). methods.where($.name = foo).single(). arguments.single().meta.val testInheritedPropertyMeta: Body: - Return: typeinfo($).properties. where($.name = prop1).single().meta.val testOverriddenPropertyMeta: Body: - Return: typeinfo($).properties. where($.name = prop2).single().meta.val testPackageMeta: Body: - Return: typeinfo($).package.meta testComplexMeta: Body: - Return: typeinfo($). methods.where($.name = bar).single().meta.cls. select([$.prop, typeinfo($).name]) foo: Meta: - InheritedMultiMeta: val: 7 - InheritedSingleMeta: val: 8 - SingleMeta: val: 8 + 1 Arguments: arg: Contract: $.string() Meta: - SingleMeta: val: 1 - MultiMeta: val: 2 - MultiMeta: val: 3 bar: Meta: - ComplexMeta: cls: :PropertyType: prop: 1 - ComplexMeta: cls: prop: 2 - :ComplexMeta: cls: :PropertyType2: prop: 3 - 'metatests.ComplexMeta': cls: prop: 4 - ComplexMeta: cls: ?: type: 'metatests.PropertyType2' prop: 5 --- # ------------------------------------------------------------------------ Name: PropertyType Properties: prop: Contract: $.int().notNull() Default: 44 --- # ------------------------------------------------------------------------ Name: PropertyType2 Extends: PropertyType ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/dsl/meta/TestMethodParamInheritance.yaml0000664000175000017500000000101000000000000026343 0ustar00zuulzuul00000000000000Name: TestMethodParamInheritanceBase Methods: testRunWithParam: Body: - $this.method1('foo') testRunWithoutParam: Body: - $this.method2() method1: Arguments: - foo: Contract: $.string().notNull() method2: --- # ------------------------------------------------------------------ # --- Name: TestMethodParamInheritanceDerived Extends: TestMethodParamInheritanceBase Methods: method1: method2: Arguments: - foo: Contract: $.string().notNull() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/dsl/meta/TestObjectsCopyMerge.yaml0000664000175000017500000000062700000000000025211 0ustar00zuulzuul00000000000000Name: TestObjectsCopyMergeSampleClass Extends: Node Methods: .init: Body: - $.testValue: null .destroy: Body: - trace($.value) - $.nodes.select(trace($.value)) - $.nodes.first().setValue('It works!') setValue: Arguments: - arg: Contract: $.string().notNull() Body: - $.testValue: $arg testMethod: Body: Return: $.testValue ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/dsl/meta/TestReflection.yaml0000664000175000017500000000500200000000000024067 0ustar00zuulzuul00000000000000Name: TestReflection Properties: property: Contract: $.string() Default: object Usage: InOut staticProperty: Contract: $.string() Default: static Usage: Static Methods: testTypeInfo: Body: - $typeinfo: typeinfo($) - Return: name: $typeinfo.name versionStr: str($typeinfo.version) versionMajor: $typeinfo.version.major versionMinor: $typeinfo.version.minor versionPatch: $typeinfo.version.patch ancestors: $typeinfo.ancestors.name properties: $typeinfo.properties.name.orderBy($) methods: $typeinfo.methods.name.where(not $.startsWith(test)).orderBy($) package: $typeinfo.package.name testMethodInfo: Body: - $method: typeinfo($).methods.where($.name = foo).single() - $bar: $method.arguments.where($.name = bar).single() - $baz: $method.arguments.where($.name = baz).single() - Return: name: $method.name arguments: $method.arguments.name barHasDefault: $bar.hasDefault bazHasDefault: $baz.hasDefault declaringType: $method.declaringType.name barMethod: $bar.declaringMethod.name bazMethod: $baz.declaringMethod.name testPropertyInfo: Body: - $property: typeinfo($).properties.orderBy($.name).first() - Return: name: $property.name hasDefault: $property.hasDefault usage: $property.usage foo: Arguments: - bar: Contract: $.string() Default: null - baz: Contract: $.string() Body: Return: $bar + $baz testPropertyRead: Body: - $instanceValues: typeinfo($).properties.orderBy($.name). select($.getValue($this)) - $staticValues: typeinfo($).properties.orderBy($.name). where($.usage = Static).select($.getValue(null)) - Return: [$instanceValues, $staticValues] testPropertyWrite: Body: - $instanceProperty: typeinfo($).properties.where($.name = property).single() - $instanceProperty.setValue($, 'new object') - $staticProperty: typeinfo($).properties.where($.name = staticProperty).single() - $staticProperty.setValue($, 'new static') - Return: $.testPropertyRead() testMethodInvoke: Body: - $method: typeinfo($).methods.where($.name = foo).single() - Return: $method.invoke($, 'bar ', baz => baz) testInstanceCreate: Body: - $obj: new(typeinfo($).type, property => test) - Return: $obj.property ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/dsl/meta/TestSchema.yaml0000664000175000017500000000466400000000000023212 0ustar00zuulzuul00000000000000Namespaces: m: io.murano.metadata mf: io.murano.metadata.forms Name: TestSchema Meta: - mf:Section: name: mySection title: Section Title index: 1 - mf:Section: name: mySection title: Another Section Title index: 2 Properties: stringProperty: Contract: $.string() stringNotNullProperty: Contract: $.string().notNull() intProperty: Contract: $.int() intNotNullProperty: Contract: $.int().notNull() boolProperty: Contract: $.bool() boolNotNullProperty: Contract: $.bool().notNull() listProperty: Contract: - $.string().notNull() dictProperty: Contract: key1: $.string().notNull() key2: $.string().notNull() $.string().notNull(): $.int() classProperty: Contract: $.class(SampleClass1) templateProperty: Contract: $.template(SampleClass1, excludeProperties => [stringProperty]) defaultProperty: Contract: $.int() Default: 999 complexProperty: Contract: $.string(): [$.int().notNull()] minimumContract: Contract: $.int().notNull().check($ >= 5) maximumContract: Contract: $.int().notNull().check($ < 15) rangeContract: Contract: $.int().notNull().check($ > 0 and $ <= 10) chainContract: Contract: $.int().notNull().check($ > 0).check($ <= 10) regexContract: Contract: $.string().notNull().check($.matches(`\d+`)) enumContract: Contract: $.string().notNull().check($ in [a, b]) enumFuncContract: Contract: $.string().notNull().check($ in $this.staticEnumMethod()) decoratedProperty: Contract: $.string().notNull() Meta: - m:Title: text: Title! - m:Description: text: Description! - m:HelpText: text: Help! - mf:Hidden - mf:Position: index: 1 section: mySection Methods: staticEnumMethod: Body: Return: - x - y Usage: Static modelBuilder: Meta: - m:ModelBuilder - m:Title: text: Model Builder! Usage: Static Scope: Public Arguments: - arg1: Meta: m:Title: text: Arg1! Contract: $.string().notNull() - arg2: Contract: $.int().notNull() invalidModelBuilder1: Meta: - m:ModelBuilder invalidModelBuilder2: Meta: - m:ModelBuilder Usage: Static invalidModelBuilder3: Meta: - m:ModelBuilder Scope: Public ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/dsl/meta/TestStatics.yaml0000664000175000017500000001410500000000000023413 0ustar00zuulzuul00000000000000Namespaces: ns: test =: test e: '' --- # --------------------------------------------------------------------- # TestStaticsBase class - base class for TestStatics to test how static # entities work in respect to class inheritance --- # --------------------------------------------------------------------- Name: TestStaticsBase Properties: baseStaticProperty: Contract: $.string() Default: baseStaticProperty Usage: Static conflictingStaticProperty: Contract: $.string() Default: 'conflictingStaticProperty-base' Usage: Static --- # --------------------------------------------------------------------- # TestStatics class - main class for the static tests --- # --------------------------------------------------------------------- Name: TestStatics Extends: TestStaticsBase Properties: staticProperty: Contract: $.string() Usage: Static Default: xxx staticDictProperty: Contract: {} Usage: Static conflictingStaticProperty: Contract: $.string() Default: 'conflictingStaticProperty-child' Usage: Static instanceProperty: Contract: $.int() Default: 555 staticProperty2: Contract: $.string() Default: staticProperty Usage: Static Methods: testStaticTest: Usage: Static Body: Return: $ testCallStaticMethodOnObject: Body: Return: $.simpleStaticMethod() testCallStaticMethodOnClassName: Body: Return: :TestStatics.simpleStaticMethod() testCallStaticMethodOnInvalidClass: Body: Return: e:TestUnicode.simpleStaticMethod() testCallStaticMethodOnClassNameWithNs: Body: Return: ns:TestStatics.simpleStaticMethod($.instanceProperty) testCallStaticMethodFromAnotherMethod: Body: Return: ns:TestStatics.simpleStaticMethod2() testStaticThis: Body: Return: $.returnStaticThis() testNoAccessToInstanceProperties: Body: Return: $.accessInstanceProperty() testAccessStaticPropertyFromInstanceMethod: Body: Return: $.staticProperty testAccessStaticPropertyFromStaticMethod: Body: Return: $.accessStaticProperty() simpleStaticMethod: Usage: Static Arguments: arg: Contract: $.int() Default: 0 Body: Return: 123 + $arg simpleStaticMethod2: Usage: Static Body: Return: $.simpleStaticMethod() + $this.simpleStaticMethod() + ns:TestStatics.simpleStaticMethod() + :TestStatics.simpleStaticMethod() + type('test.TestStatics').simpleStaticMethod() returnStaticThis: Usage: Static Body: Return: $ accessInstanceProperty: Usage: Static Body: Return: $.instanceProperty accessStaticProperty: Usage: Static Body: Return: $.staticProperty testModifyStaticPropertyUsingDollar: Body: Return: $.modifyStaticPropertyUsingDollar() modifyStaticPropertyUsingDollar: Usage: Static Body: - $.staticProperty: qq - Return: $.staticProperty testModifyStaticPropertyUsingThis: Body: Return: $.modifyStaticPropertyUsingThis() modifyStaticPropertyUsingThis: Usage: Static Body: - $this.staticProperty: qq - Return: $this.staticProperty testModifyStaticPropertyUsingClassName: Body: Return: $.modifyStaticPropertyUsingClassName() modifyStaticPropertyUsingClassName: Usage: Static Body: - :TestStatics.staticProperty: qq - Return: :TestStatics.staticProperty testModifyStaticPropertyUsingNsClassName: Body: Return: $.modifyStaticPropertyUsingNsClassName() modifyStaticPropertyUsingNsClassName: Usage: Static Body: - ns:TestStatics.staticProperty: qq - Return: ns:TestStatics.staticProperty testModifyStaticPropertyUsingTypeFunc: Body: Return: $.modifyStaticPropertyUsingTypeFunc() modifyStaticPropertyUsingTypeFunc: Usage: Static Body: - type('test.TestStatics').staticProperty: qq - Return: type('test.TestStatics').staticProperty testModifyStaticDictProperty: Body: Return: $.modifyStaticDictProperty() modifyStaticDictProperty: Usage: Static Body: - :TestStatics.staticDictProperty.key: value - Return: $.staticDictProperty testPropertyIsStatic: Body: Return: $.modifyStaticPropertyOnInstance() modifyStaticPropertyOnInstance: Usage: Static Body: - $obj1: new(TestStatics) - $obj2: new(TestStatics) - $obj1.modifyStaticPropertyUsingClassName() - Return: $obj2.staticProperty testStaticPropertisNotLoaded: Body: Return: $.staticProperty2 testTypeIsSingleton: Body: - $t11: :TestStatics - $t12: :TestStatics - $t21: ns:TestStatics - $t22: ns:TestStatics - $t31: type('test.TestStatics') - $t32: type('test.TestStatics') - Return: $t11 = $t12 and $t21 = $t22 and $t31 = $t32 testStaticPropertyInheritance: Body: Return: $.baseStaticProperty + :TestStaticsBase.baseStaticProperty + :TestStatics.baseStaticProperty testStaticPropertyOverride: Body: Return: - $.conflictingStaticProperty - :TestStatics.conflictingStaticProperty - :TestStaticsBase.conflictingStaticProperty - type('test.TestStatics').conflictingStaticProperty - type('test.TestStaticsBase').conflictingStaticProperty testTypeinfoOfType: Body: - $typeObj: type('test.TestStatics') - $typeInfoOfType: typeinfo($typeObj) - $obj: new('TestStatics') - Return: typeinfo($obj) = $typeInfoOfType testCallPythonStaticMethod: Body: Return: - $.staticPythonMethod(111) - :TestStatics.staticPythonMethod(111) - ns:TestStatics.staticPythonMethod(111) - type('test.TestStatics').staticPythonMethod(111) testCallPythonClassMethod: Body: Return: - $.classmethodPythonMethod('!') - :TestStatics.classmethodPythonMethod('!') - ns:TestStatics.classmethodPythonMethod('!') - type('test.TestStatics').classmethodPythonMethod('!') testStaticAction: Usage: Static Body: Return: 'It works!'././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/dsl/meta/TestUnicode.yaml0000664000175000017500000000076200000000000023373 0ustar00zuulzuul00000000000000Name: TestUnicode Methods: testLiteral: Body: Return: солнце ♥ φεγγάρι testExpression: Body: - Return: ('солнце ♥' + ' φεγγάρι').toUpper() testParameter: Body: - Return: $.foo('солнце ♥ φεγγάρι') testException: Body: - Throw: Exception Message: солнце ♥ φεγγάρι foo: Arguments: arg: Contract: $.string().notNull() Body: Return: $arg.toUpper()././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/dsl/meta/TestVarKwArgs.yaml0000664000175000017500000000230600000000000023650 0ustar00zuulzuul00000000000000Name: TestVarKwArgs Methods: testVarArgs: Body: Return: $.varArgsMethod(1, 2, 3, 4) testVarArgsContract: Body: Return: $.varArgsMethod(1, string) testDuplicateVarArgs: Body: Return: $.varArgsMethod(1, arg1 => 2) testExplicitVarArgs: Body: Return: $.varArgsMethod(1, rest => 2) varArgsMethod: Arguments: - arg1: Contract: $.int() - rest: Contract: $.int() Usage: VarArgs Body: Return: $rest testKwArgs: Body: Return: $.kwArgsMethod(arg1 => 1, arg2 => 2, arg3 => 3) testKwArgsContract: Body: Return: $.kwArgsMethod(arg1 => 1, arg2 => string) testDuplicateKwArgs: Body: Return: $.kwArgsMethod(1, arg1 => 2) kwArgsMethod: Arguments: - arg1: Contract: $.int() - rest: Contract: $.int() Usage: KwArgs Body: Return: $rest testArgs: Body: Return: $.argsMethod(1, 2, 3, arg1 => 4, arg2 => 5, arg3 => 6) argsMethod: Arguments: - args: Contract: $.int() Usage: VarArgs - kwargs: Contract: $.int() Usage: KwArgs Body: Return: [$args, $kwargs] ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/dsl/test_agent.py0000664000175000017500000000646400000000000022047 0ustar00zuulzuul00000000000000# Copyright (c) 2014 Hewlett-Packard Development Company, L.P. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or # implied. # See the License for the specific language governing permissions and # limitations under the License. from unittest import mock from murano.common import exceptions as exc from murano.dsl import constants from murano.dsl import dsl from murano.dsl import helpers from murano.dsl import yaql_integration from murano.engine.system import agent from murano.engine.system import agent_listener from murano.tests.unit.dsl.foundation import object_model as om from murano.tests.unit.dsl.foundation import test_case class TestAgentListener(test_case.DslTestCase): def setUp(self): super(TestAgentListener, self).setUp() # Register Agent class self.package_loader.load_package('io.murano', None).register_class( agent_listener.AgentListener) model = om.Object( 'AgentListenerTests') self.runner = self.new_runner(model) self.context = yaql_integration.create_empty_context() self.context[constants.CTX_THIS] = mock.MagicMock( dsl.MuranoObjectInterface) def test_listener_enabled(self): self.override_config('disable_murano_agent', False, 'engine') al = self.runner.testAgentListener().extension self.assertTrue(al.enabled) with self.runner.session(), helpers.contextual(self.context): try: al.subscribe('msgid', 'event') self.assertEqual({'msgid': 'event'}, al._subscriptions) finally: al.stop() def test_listener_disabled(self): self.override_config('disable_murano_agent', True, 'engine') al = self.runner.testAgentListener().extension self.assertFalse(al.enabled) self.assertRaises(exc.PolicyViolationException, al.subscribe, 'msgid', 'event') class TestAgent(test_case.DslTestCase): def test_agent_enabled(self): self.override_config('disable_murano_agent', False, 'engine') self.override_config('signing_key', False, group='engine') agent_cls = 'murano.engine.system.agent.Agent' a = agent.Agent(mock.MagicMock()) self.assertTrue(a.enabled) with mock.patch(agent_cls + '._send') as s: s.return_value = mock.MagicMock() a.send_raw({}) s.assert_called_with({}, False, 0) def test_agent_disabled(self): self.override_config('disable_murano_agent', True, 'engine') self.override_config('signing_key', False, group='engine') a = agent.Agent(mock.MagicMock()) self.assertFalse(a.enabled) self.assertRaises(exc.PolicyViolationException, a.call, {}, None) self.assertRaises(exc.PolicyViolationException, a.send, {}, None) self.assertRaises(exc.PolicyViolationException, a.call_raw, {}) self.assertRaises(exc.PolicyViolationException, a.send_raw, {}) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/dsl/test_assignments.py0000664000175000017500000000350500000000000023275 0ustar00zuulzuul00000000000000# 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 murano.tests.unit.dsl.foundation import object_model as om from murano.tests.unit.dsl.foundation import test_case class TestAssignments(test_case.DslTestCase): def setUp(self): super(TestAssignments, self).setUp() self._runner = self.new_runner( om.Object( 'SampleClass1', stringProperty='string', classProperty=om.Object( 'SampleClass2', class2Property='another string'))) def test_assignment(self): self.assertEqual( { 'Arr': [5, 2, [10, 123]], 'Dict': { 'Key1': 'V1', 'Key2': {'KEY2': 'V3', 'a_b': 'V2'} } }, self._runner.testAssignment()) def test_assignment_on_property(self): self.assertEqual( { 'Arr': [5, 2, [10, 123]], 'Dict': { 'Key1': 'V1', 'Key2': {'KEY2': 'V3', 'a_b': 'V2'} } }, self._runner.testAssignmentOnProperty()) def test_assign_by_copy(self): self.assertEqual( [1, 2, 3], self._runner.testAssignByCopy([1, 2, 3])) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/dsl/test_attribute_store.py0000664000175000017500000001245200000000000024162 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from unittest import mock from murano.dsl import attribute_store from murano.dsl import dsl from murano.dsl import dsl_types from murano.tests.unit.dsl.foundation import test_case class TestAttributeStore(test_case.DslTestCase): def setUp(self): super(TestAttributeStore, self).setUp() self.attribute_store = attribute_store.AttributeStore() self.fake_object = mock.MagicMock(object_id=mock.sentinel.oid) self.tagged_obj = dsl.MuranoObjectInterface(self.fake_object) self.owner_type = dsl_types.MuranoTypeReference(self.fake_object) self.owner_type.type.name = mock.sentinel.typename self.name = 'foobar' def test_get_attribute_key(self): oid, typename = mock.sentinel.oid, mock.sentinel.typename key = self.attribute_store._get_attribute_key( self.tagged_obj, self.owner_type, self.name) expected_key = (oid, (typename, 'foobar')) self.assertEqual(expected_key, key) @mock.patch.object(attribute_store.AttributeStore, '_get_attribute_key', return_value=(mock.sentinel.key1, mock.sentinel.key2)) def test_get(self, mock_get_attr_key): key1, key2 = mock.sentinel.key1, mock.sentinel.key2 get_val = mock.sentinel.get_val self.attribute_store._attributes = mock.MagicMock() self.attribute_store._attributes[key1].get.return_value = get_val val = self.attribute_store.get( self.tagged_obj, self.owner_type, self.name) mock_get_attr_key.assert_called_with( self.tagged_obj, self.owner_type, self.name) self.attribute_store._attributes[key1].get.assert_called_with(key2) self.assertEqual(get_val, val) @mock.patch.object(attribute_store.AttributeStore, '_get_attribute_key', return_value=(mock.sentinel.key1, mock.sentinel.key2)) def test_set_object_if(self, mock_get_attr_key): val = dsl.MuranoObjectInterface(self.fake_object) self.attribute_store._attributes = mock.MagicMock() self.attribute_store.set( self.tagged_obj, self.owner_type, self.name, val) @mock.patch.object(attribute_store.AttributeStore, '_get_attribute_key', return_value=(mock.sentinel.key1, mock.sentinel.key2)) def test_set_object(self, mock_get_attr_key): key1, key2 = mock.sentinel.key1, mock.sentinel.key2 val = dsl_types.MuranoObject() val.object_id = mock.sentinel.oid self.attribute_store.set( self.tagged_obj, self.owner_type, self.name, val) self.assertEqual(self.attribute_store._attributes[key1][key2], mock.sentinel.oid) @mock.patch.object(attribute_store.AttributeStore, '_get_attribute_key', return_value=(mock.sentinel.key1, mock.sentinel.key2)) def test_set_none(self, mock_get_attr_key): key1, key2 = mock.sentinel.key1, mock.sentinel.key2 val = None self.attribute_store._attributes = mock.MagicMock() self.attribute_store.set( self.tagged_obj, self.owner_type, self.name, val) self.attribute_store._attributes[key1].pop.assert_called_with( key2, None) def test_serialize(self): known_objects = ['obj1', 'obj3'] self.attribute_store._attributes = { 'obj1': { ('foo', 'obj11'): 11, ('bar', 'obj12'): 12 }, 'obj2': { ('baz', 'obj21'): 21 }, 'obj3': { ('foobar', 'obj31'): 31 } } val = self.attribute_store.serialize(known_objects) expected = [ ['obj1', 'foo', 'obj11', 11], ['obj1', 'bar', 'obj12', 12], ['obj3', 'foobar', 'obj31', 31]] self.assertEqual(sorted(expected), sorted(val)) def test_load(self): data = [ ['a', 'b', 'c', 'd'], ['a', 'f', 'g', None], ['b', 'i', 'j', 'k'] ] self.attribute_store.load(data) expected = {'a': {('b', 'c'): 'd'}, 'b': {('i', 'j'): 'k'}} self.assertEqual(expected, self.attribute_store._attributes) def test_forget_object_if(self): obj = dsl.MuranoObjectInterface(mock.MagicMock(object_id='bar')) self.attribute_store._attributes = {'foo': 42, 'bar': 43} self.attribute_store.forget_object(obj) self.assertEqual({'foo': 42}, self.attribute_store._attributes) def test_forget_object(self): obj = dsl_types.MuranoObject() obj.object_id = 'foo' self.attribute_store._attributes = {'foo': 42, 'bar': 43} self.attribute_store.forget_object(obj) self.assertEqual({'bar': 43}, self.attribute_store._attributes) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/dsl/test_call.py0000664000175000017500000000262400000000000021656 0ustar00zuulzuul00000000000000# 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 murano.tests.unit.dsl.foundation import object_model as om from murano.tests.unit.dsl.foundation import test_case class TestCall(test_case.DslTestCase): def setUp(self): super(TestCall, self).setUp() self._runner = self.new_runner(om.Object('TestCall')) def test_call(self): self._runner.testCall() self.assertEqual(['called as call'], self._traces) def test_method(self): self._runner.testMethodInvocation() self.assertEqual(['called as method'], self._traces) def test_call_static(self): self._runner.testCallStatic() self.assertEqual(['called as static'], self._traces) def test_call_static_as_instance(self): self._runner.testCallStaticAsInstance() self.assertEqual(['called as static'], self._traces) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/dsl/test_concurrency.py0000664000175000017500000000603200000000000023272 0ustar00zuulzuul00000000000000# 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 eventlet from murano.tests.unit.dsl.foundation import object_model as om from murano.tests.unit.dsl.foundation import test_case class TestConcurrency(test_case.DslTestCase): def setUp(self): super(TestConcurrency, self).setUp() def yield_(): self.traces.append('yield') eventlet.sleep(0) self.register_function(yield_, 'yield') self._runner = self.new_runner(om.Object('TestConcurrency')) def check_isolated_traces(self): for i in range(0, len(self.traces), 3): before = self.traces[i] switch = self.traces[i + 1] after = self.traces[i + 2] self.assertEqual('yield', switch) self.assertEqual(before[0:6], after[0:6]) self.assertTrue(before.endswith('-before')) self.assertTrue(after.endswith('-after')) def check_concurrent_traces(self): self.assertTrue(self.traces[0].endswith('-before')) self.assertEqual('yield', self.traces[1]) self.assertTrue(self.traces[2].endswith('-before')) self.assertEqual('yield', self.traces[3]) self.assertTrue(self.traces[4].endswith('-before')) self.assertEqual('yield', self.traces[5]) self.assertTrue(self.traces[6].endswith('-after')) self.assertTrue(self.traces[7].endswith('-after')) self.assertTrue(self.traces[8].endswith('-after')) def test_isolated(self): self._runner.testCallIsolated() self.check_isolated_traces() def test_isolated_default(self): self._runner.testCallIsolatedWithDefault() self.check_isolated_traces() def test_concurrent_explicit(self): self._runner.testCallConcurrentExplicit() self.check_concurrent_traces() def test_isolated_explicit(self): self._runner.testCallIsolatedExplicit() self.check_isolated_traces() def test_argbased_primitive_isolated(self): self._runner.testCallArgbasedPrimitiveIsolated() self.check_isolated_traces() def test_argbased_primitive_concurrent(self): self._runner.testCallArgbasedPrimitiveConcurrent() self.check_concurrent_traces() def test_argbased_object_isolated(self): self._runner.testCallArgbasedWithObjectIsolated() self.check_isolated_traces() def test_argbased_object_concurrent(self): self._runner.testCallArgbasedWithObjectConcurrent() self.check_concurrent_traces() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/dsl/test_config_properties.py0000664000175000017500000000403100000000000024456 0ustar00zuulzuul00000000000000# 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 murano.tests.unit.dsl.foundation import object_model as om from murano.tests.unit.dsl.foundation import test_case class TestConfigProperties(test_case.DslTestCase): def test_config_property(self): obj = om.Object('ConfigProperties') self.package_loader.set_config_value(obj, 'cfgProperty', '987') runner = self.new_runner(obj) runner.testPropertyValues() self.assertEqual( [987, 'DEFAULT'], self.traces ) def test_config_property_exclusion_from_obect_model(self): obj = om.Object('ConfigProperties', cfgProperty=555) runner = self.new_runner(obj) runner.testPropertyValues() self.assertEqual( [123, 'DEFAULT'], self.traces ) def test_config_affects_default(self): obj = om.Object('ConfigProperties') self.package_loader.set_config_value(obj, 'normalProperty', 'custom') runner = self.new_runner(obj) runner.testPropertyValues() self.assertEqual( [123, 'custom'], self.traces ) def test_config_not_affects_in_properties(self): obj = om.Object('ConfigProperties', normalProperty='qq') self.package_loader.set_config_value(obj, 'normalProperty', 'custom') runner = self.new_runner(obj) runner.testPropertyValues() self.assertEqual( [123, 'qq'], self.traces ) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/dsl/test_construction.py0000664000175000017500000000550100000000000023472 0ustar00zuulzuul00000000000000# 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 testtools import matchers from murano.dsl import dsl from murano.dsl import serializer from murano.tests.unit.dsl.foundation import object_model as om from murano.tests.unit.dsl.foundation import test_case class TestConstruction(test_case.DslTestCase): def setUp(self): super(TestConstruction, self).setUp() self._runner = self.new_runner(om.Object('CreatingClass')) def test_new(self): self._runner.testNew() self.assertEqual( ['CreatingClass::.init', 'CreatedClass1::.init', 'string', 'STRING', 123], self.traces) def test_new_with_ownership(self): obj = serializer.serialize(self._runner.testNewWithOwnership(), self._runner.executor, allow_refs=False) self.assertEqual('STRING', obj.get('property1')) self.assertIsNotNone('string', obj.get('xxx')) self.assertEqual('STR', obj['xxx'].get('property1')) self.assertEqual('QQQ', obj['xxx']['?'].get('name')) def test_new_with_dict(self): self._runner.testNewWithDict() self.assertEqual( ['CreatingClass::.init', 'CreatedClass1::.init', 'string', 'STRING', 123], self.traces) def test_model_load(self): res = self._runner.testLoadCompexModel() for i in range(3): self.assertThat(res[i], matchers.Not(matchers.Contains('node'))) self.assertEqual(self._runner.root.object_id, res[3]) self.assertEqual( [ 'rootNode', ['childNode1', 'childNode2', 'childNode2'], True, True, True, True, True, 'rootNode', 'childNode2', 'childNode1' ], res[4:]) def test_single_contract_instantiation(self): self._runner.testSingleContractInstantiation() self.assertEqual(1, self.traces.count('ConstructionSample::init')) def test_nested_new_loads_in_separate_store(self): res = self._runner.testNestedNewLoadsInSeparateStore() self.assertIsInstance(res, dsl.MuranoObjectInterface) def test_reference_access_from_init(self): self._runner.testReferenceAccessFromInit() self.assertEqual(2, self.traces.count('childNode')) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/dsl/test_context_manager.py0000664000175000017500000000340100000000000024113 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from unittest import mock from murano.dsl import context_manager from murano.dsl import yaql_integration from murano.tests.unit.dsl.foundation import test_case class TestContextManager(test_case.DslTestCase): def setUp(self): super(TestContextManager, self).setUp() self.context_manager = context_manager.ContextManager() @mock.patch.object(yaql_integration, 'create_context', return_value='foo') def test_create_root_context(self, mock_create_context): val = self.context_manager.create_root_context('myrunver') self.assertEqual('foo', val) mock_create_context.assert_called_with('myrunver') def test_create_package_context(self): package = mock.MagicMock(context='mycontext') self.assertEqual('mycontext', self.context_manager.create_package_context(package)) def test_create_type_context(self): murano_type = mock.MagicMock(context='mycontext') self.assertEqual('mycontext', self.context_manager.create_type_context(murano_type)) def test_create_object_context(self): obj = 'obj' self.assertIsNone(self.context_manager.create_object_context(obj)) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/dsl/test_contracts.py0000664000175000017500000003276400000000000022753 0ustar00zuulzuul00000000000000# 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 murano.dsl import dsl from murano.dsl import exceptions from murano.tests.unit.dsl.foundation import object_model as om from murano.tests.unit.dsl.foundation import test_case class TestContracts(test_case.DslTestCase): def setUp(self): super(TestContracts, self).setUp() self._runner = self.new_runner( om.Object( 'ContractExamples', ordinaryProperty='PROPERTY', sampleClass=om.Object( 'SampleClass1', stringProperty='string1', classProperty=om.Object( 'SampleClass2', class2Property='string2')))) def test_string_contract(self): result = self._runner.testStringContract('qwerty') self.assertIsInstance(result, str) self.assertEqual('qwerty', result) def test_string_from_number_contract(self): result = self._runner.testStringContract(123) self.assertIsInstance(result, str) self.assertEqual('123', result) def test_string_null_contract(self): self.assertIsNone(self._runner.testStringContract(None)) def test_int_contract(self): result = self._runner.testIntContract(123) self.assertIsInstance(result, int) self.assertEqual(123, result) def test_int_from_string_contract(self): result = self._runner.testIntContract('456') self.assertIsInstance(result, int) self.assertEqual(456, result) def test_int_from_string_contract_failure(self): self.assertRaises(exceptions.ContractViolationException, self._runner.testIntContract, 'nan') def test_int_null_contract(self): self.assertIsNone(self._runner.testIntContract(None)) def test_bool_contract(self): result = self._runner.testBoolContract(True) self.assertIsInstance(result, bool) self.assertTrue(result) result = self._runner.testBoolContract(False) self.assertIsInstance(result, bool) self.assertFalse(result) def test_bool_from_int_contract(self): result = self._runner.testBoolContract(10) self.assertIsInstance(result, bool) self.assertTrue(result) result = self._runner.testBoolContract(0) self.assertIsInstance(result, bool) self.assertFalse(result) def test_bool_from_string_contract(self): result = self._runner.testBoolContract('something') self.assertIsInstance(result, bool) self.assertTrue(result) result = self._runner.testBoolContract('') self.assertIsInstance(result, bool) self.assertFalse(result) def test_bool_null_contract(self): self.assertIsNone(self._runner.testIntContract(None)) def test_class_contract(self): arg = om.Object('SampleClass2', class2Property='qwerty') result = self._runner.testClassContract(arg) self.assertIsInstance(result, dsl.MuranoObjectInterface) def test_class_contract_by_ref(self): arg = om.Object('SampleClass2', class2Property='qwerty') result = self._runner.testClassContract(arg) self.assertNotEqual(arg.id, result.id) def test_class_contract_failure(self): self.assertRaises( exceptions.ContractViolationException, self._runner.testClassContract, ['invalid type']) def test_class_contract_by_ref_failure(self): self.assertRaises( exceptions.NoObjectFoundError, self._runner.testClassContract, 'NoSuchIdExists') def test_class_contract_from_dict(self): self.assertEqual( 'SampleClass2', self._runner.testClassContract({ 'class2Property': 'str'}).type.name) def test_class_from_id_contract(self): object_id = self._runner.root.get_property('sampleClass').object_id result = self._runner.testClassFromIdContract(object_id) self.assertIsInstance(result, dsl.MuranoObjectInterface) self.assertEqual(object_id, result.id) def test_template_contract(self): arg = om.Object('CreatedClass2', property1='qwerty', property2=123) result = self._runner.testTemplateContract(arg) self.assertIsInstance(result, dict) self.assertCountEqual(['?', 'property1', 'property2'], result.keys()) def test_template_property_contract(self): template = { 'foo': 123 } self.new_runner( om.Object('ContractExamples', templateProperty=template)) def test_template_contract_fail_on_type(self): arg = om.Object('SampleClass2', class2Property='qwerty') self.assertRaises( exceptions.ContractViolationException, self._runner.testTemplateContract, arg) def test_template_contract_with_property_exclusion(self): arg = om.Object('CreatedClass2', property1='qwerty', property2='INVALID') result = self._runner.testTemplateContractExcludeProperty(arg) self.assertIsInstance(result, dict) self.assertCountEqual(['?', 'property1'], result.keys()) def test_template_contract_with_property_exclusion_from_mpl(self): result = self._runner.testTemplateContractExcludePropertyFromMpl() self.assertIsInstance(result, dict) self.assertCountEqual(['?', 'property1'], result.keys()) def test_check_contract(self): arg = om.Object('SampleClass2', class2Property='qwerty') self.assertIsNone(self._runner.testCheckContract(arg, 100)) def test_check_contract_failure(self): invalid_arg = om.Object('SampleClass2', class2Property='not qwerty') self.assertRaises(exceptions.ContractViolationException, self._runner.testCheckContract, invalid_arg, 100) def test_owned_contract(self): arg1 = self._runner.root.get_property('sampleClass') arg2 = arg1.get_property('classProperty') self.assertIsNone(self._runner.testOwnedContract(arg1, arg2)) def test_owned_contract_on_null(self): self.assertIsNone(self._runner.testOwnedContract(None, None)) def test_owned_contract_failure(self): arg1 = self._runner.root.get_property('sampleClass') arg2 = arg1.get_property('classProperty') invalid_arg2 = om.Object('SampleClass2', class2Property='string2') invalid_arg1 = om.Object( 'SampleClass1', stringProperty='string1', classProperty=invalid_arg2) self.assertRaises(exceptions.ContractViolationException, self._runner.testOwnedContract, invalid_arg1, arg2) self.assertRaises(exceptions.ContractViolationException, self._runner.testOwnedContract, invalid_arg2, arg1) def test_not_owned_contract(self): arg2 = om.Object('SampleClass2', class2Property='string2') arg1 = om.Object( 'SampleClass1', stringProperty='string1', classProperty=arg2) self.assertIsNone(self._runner.testNotOwnedContract(arg1, arg2)) def test_not_owned_contract_on_null(self): self.assertIsNone(self._runner.testNotOwnedContract(None, None)) def test_not_owned_contract_failure(self): invalid_arg1 = self._runner.root.get_property('sampleClass') invalid_arg2 = invalid_arg1.get_property('classProperty') arg2 = om.Object('SampleClass2', class2Property='string2') arg1 = om.Object( 'SampleClass1', stringProperty='string1', classProperty=arg2) self.assertRaises( exceptions.ContractViolationException, self._runner.testNotOwnedContract, invalid_arg1, arg2) self.assertRaises( exceptions.ContractViolationException, self._runner.testNotOwnedContract, invalid_arg2, arg1) def test_scalar_contract(self): self.assertEqual('fixed', self._runner.testScalarContract( 'fixed', 456, True)) def test_scalar_contract_failure(self): self.assertRaises( exceptions.ContractViolationException, self._runner.testScalarContract, 'wrong', 456, True) self.assertRaises( exceptions.ContractViolationException, self._runner.testScalarContract, 'fixed', 123, True) self.assertRaises( exceptions.ContractViolationException, self._runner.testScalarContract, 'fixed', 456, False) def test_list_contract(self): self.assertEqual([3, 2, 1], self._runner.testListContract( ['3', 2, '1'])) def test_list_contract_from_scalar(self): self.assertEqual([99], self._runner.testListContract('99')) def test_list_contract_from_null(self): self.assertEqual([], self._runner.testListContract(None)) def test_list_with_min_length_contract(self): self.assertEqual( [1, 2, 3], self._runner.testListWithMinLengthContract([1, 2, 3])) self.assertEqual( [1, 2, 3, 4], self._runner.testListWithMinLengthContract([1, 2, 3, 4])) def test_list_with_min_length_contract_failure(self): self.assertRaises( exceptions.ContractViolationException, self._runner.testListWithMinLengthContract, None) self.assertRaises( exceptions.ContractViolationException, self._runner.testListWithMinLengthContract, [1, 2]) def test_list_with_min_max_length_contract(self): self.assertEqual( [1, 2], self._runner.testListWithMinMaxLengthContract([1, 2])) self.assertEqual( [1, 2, 3, 4], self._runner.testListWithMinMaxLengthContract([1, 2, 3, 4])) def test_list_with_min_max_length_contract_failure(self): self.assertRaises( exceptions.ContractViolationException, self._runner.testListWithMinMaxLengthContract, [1]) self.assertRaises( exceptions.ContractViolationException, self._runner.testListWithMinMaxLengthContract, [1, 2, 3, 4, 5]) def test_dict_contract(self): self.assertEqual( {'A': '123', 'B': 456}, self._runner.testDictContract({'A': '123', 'B': '456'})) self.assertEqual( {'A': '123', 'B': 456}, self._runner.testDictContract({'A': '123', 'B': '456', 'C': 'qq'})) self.assertEqual( {'A': '123', 'B': None}, self._runner.testDictContract({'A': '123'})) def test_dict_contract_failure(self): self.assertRaises( exceptions.ContractViolationException, self._runner.testDictContract, 'str') def test_dict_expressions_contract(self): self.assertEqual( {321: 'qwerty', 99: 'val', 'B': 456}, self._runner.testDictExprContract({ '321': 'qwerty', '99': 'val', 'B': 456})) def test_dict_expressions_contract_failure(self): self.assertRaises( exceptions.ContractViolationException, self._runner.testDictExprContract, {'321': 'qwerty', 'str': 'val', 'B': 456}) def test_invalid_dict_expr_contract(self): self.assertRaises( exceptions.DslContractSyntaxError, self._runner.testDictMultiExprContract, {'321': 'qwerty', 'str': 'val', 'B': 456}) def test_not_null_contract(self): self.assertEqual('value', self._runner.testNotNullContract('value')) def test_not_null_contract_failure(self): self.assertRaises( exceptions.ContractViolationException, self._runner.testNotNullContract, None) def test_default(self): self.assertEqual('value', self._runner.testDefault('value')) self.assertEqual('DEFAULT', self._runner.testDefault()) def test_default_expression(self): self.assertEqual('PROPERTY', self._runner.testDefaultExpression()) self.assertEqual('value', self._runner.testDefaultExpression('value')) def test_template_with_externally_owned_object(self): node = om.Object('Node', 'OBJ_ID') node_template = om.Object('Node', nodes=['OBJ_ID']) model = om.Object( 'TemplatePropertyClass', owned=node, template=node_template) runner = self.new_runner(model) self.assertEqual( ['OBJ_ID'], runner.testTemplateWithExternallyOwnedObject()) class TestContractsTransform(test_case.DslTestCase): def setUp(self): super(TestContractsTransform, self).setUp() self._runner = self.new_runner(om.Object('TestIteratorsTransform')) def test_property(self): self.assertEqual('3', self._runner.testProperties()) def test_argument(self): self.assertEqual('3', self._runner.testArgs()) self.assertEqual('2', self._runner.testUntypedArgs()) self.assertEqual('6', self._runner.testNotTypedListArgs()) self.assertEqual('6', self._runner.testTypedList()) self.assertEqual(2, self._runner.testListDict()) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/dsl/test_dump.py0000664000175000017500000001231700000000000021710 0ustar00zuulzuul00000000000000# 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 murano.dsl import dsl_types from murano.tests.unit.dsl.foundation import object_model as om from murano.tests.unit.dsl.foundation import test_case class TestDump(test_case.DslTestCase): def setUp(self): super(TestDump, self).setUp() self._runner = self.new_runner(om.Object('dumptests.TestDump')) def test_dump_simple_inline(self): source = om.Object('dumptests.DumpTarget1', foo='FOO', bar=[40, 41, 42], baz={'BAZ': 99}) result = self._runner.testDump(source, 'Inline') self.assertIn('id', result) res = self._get_body(result) self.assertEqual('FOO', res['foo']) self.assertEqual([40, 41, 42], res['bar']) self.assertEqual({'BAZ': 99}, res['baz']) def test_dump_simple_serializable(self): source = om.Object('dumptests.DumpTarget1', foo='FOO', bar=[40, 41, 42], baz={'BAZ': 99}) result = self._runner.testDump(source, 'Serializable') self.assertIn('?', result) self.assertEqual('dumptests.DumpTarget1/0.0.0@tests', result['?']['type']) def test_dump_simple_full_mixed(self): source = om.Object('dumptests.DumpTarget1', foo='FOO', bar=[40, 41, 42], baz={'BAZ': 99}) result = self._runner.testDump(source, 'Mixed') self.assertIn('?', result) self.assertNotIn('classVersion', result['?']) self.assertNotIn('package', result['?']) self.assertIsInstance(result['?']['type'], dsl_types.MuranoType) self.assertEqual('dumptests.DumpTarget1', result['?']['type'].name) def test_nested(self): n1 = om.Object('dumptests.DumpTarget1', foo='FOO') n2 = om.Object('dumptests.DumpTarget1', foo='BAR') n3 = om.Object('dumptests.DumpTarget1', foo='BAZ') source = om.Object('dumptests.DumpTarget2', nested=n1, another=n2, ref=n3) result = self._runner.testDump(source) res = self._get_body(result) self.assertIsNotNone(res['ref']) self.assertIsNotNone(res['another']) self.assertIsNotNone(res['nested']) self.assertEqual('FOO', self._get_body(res['nested'])['foo']) self.assertEqual('BAR', self._get_body(res['another'])['foo']) self.assertEqual('BAZ', self._get_body(res['ref'])['foo']) def test_same_ref_dump(self): nested = om.Object('dumptests.DumpTarget1', foo='FOO') source = om.Object('dumptests.DumpTarget2', nested=nested, another=nested, ref=nested) result = self._runner.testDump(source) res = self._get_body(result) string_keys = [k for k in res.keys() if isinstance(res[k], str)] obj_keys = [k for k in res.keys() if isinstance(res[k], dict)] self.assertEqual(2, len(string_keys)) self.assertEqual(1, len(obj_keys)) obj = self._get_body(res[obj_keys[0]]) self.assertEqual('FOO', obj['foo']) for ref_id in string_keys: self.assertEqual(res[obj_keys[0]]['id'], res[ref_id]) def test_dump_with_meta_attributes(self): n1 = om.Object('dumptests.DumpTarget1', foo='FOO') n2 = om.Object('dumptests.DumpTarget1', foo='Bar') source = om.Object('dumptests.DumpTarget3', a=n1, b=n2) result = self._runner.testDump(source) res = self._get_body(result) self._get_body(res['a']) self.assertIsInstance(res['b'], str) def test_dump_with_inheritance(self): source = om.Object('dumptests.DumpTarget4', foo='FOO', qux='QUX') result = self._runner.testDump(source) res = self._get_body(result) self.assertEqual('FOO', res['foo']) self.assertEqual('QUX', res['qux']) def test_dump_with_inheritance_upcast_ignored(self): source = om.Object('dumptests.DumpTarget4', foo='FOO', qux='QUX') result = self._runner.testDumpWithUpcast(source, True, True) res = self._get_body(result) self.assertEqual('FOO', res['foo']) self.assertEqual('QUX', res['qux']) def test_dump_with_inheritance_upcast_allowed(self): source = om.Object('dumptests.DumpTarget4', foo='FOO', qux='QUX') result = self._runner.testDumpWithUpcast(source, True, False) res = self._get_body(result) self.assertEqual('FOO', res['foo']) self.assertNotIn('qux', res) def _get_body(self, obj): body_key = [k for k in obj.keys() if k not in ('id', 'name', 'metadata')][0] self.assertIsInstance(body_key, dsl_types.MuranoType) return obj[body_key] ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/dsl/test_engine_yaql_functions.py0000664000175000017500000002116600000000000025330 0ustar00zuulzuul00000000000000# 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 testtools import matchers from unittest import mock from yaql.language import exceptions as yaql_exceptions from murano.tests.unit.dsl.foundation import object_model as om from murano.tests.unit.dsl.foundation import test_case from castellan.common import exception as castellan_exception class TestEngineYaqlFunctions(test_case.DslTestCase): def setUp(self): super(TestEngineYaqlFunctions, self).setUp() self._runner = self.new_runner(om.Object('TestEngineFunctions')) def test_join(self): self.assertEqual('xx 123', self._runner.testJoin()) def test_split(self): self.assertEqual( ['x', 'yy', '123'], self._runner.testSplit()) def test_len(self): self.assertEqual(8, self._runner.testLen()) def test_coalesce(self): self.assertEqual('a', self._runner.testCoalesce('a', 'b', 'c')) self.assertEqual('b', self._runner.testCoalesce(None, 'b', 'c')) self.assertEqual('c', self._runner.testCoalesce(None, None, 'c')) def test_base64(self): encoded = 'VEVTVA==' self.assertEqual( encoded, self._runner.testBase64Encode('TEST')) self.assertEqual( 'TEST', self._runner.testBase64Decode(encoded)) def test_format(self): self.assertEqual( '2 + 3', self._runner.testFormat('{0} + {1}', 2, 3)) def test_replace_str(self): self.assertEqual( 'John Doe', self._runner.testReplaceStr('John Kennedy', 'Kennedy', 'Doe')) self.assertRaises( yaql_exceptions.NoMatchingMethodException, self._runner.testReplaceStr, None, 'Kennedy', 'Doe') def test_replace_dict(self): self.assertEqual( 'Marilyn Monroe', self._runner.testReplaceDict('John Kennedy', { 'John': 'Marilyn', 'Kennedy': 'Monroe' })) def test_lower(self): self.assertEqual( 'test', self._runner.testToLower('TESt')) def test_upper(self): self.assertEqual( 'TEST', self._runner.testToUpper('tEst')) def test_starts_with(self): self.assertIs( True, self._runner.testStartsWith('TEST', 'TE')) self.assertIs( False, self._runner.testStartsWith('TEST', 'te')) def test_ends_with(self): self.assertIs( True, self._runner.testEndsWith('TEST', 'ST')) self.assertIs( False, self._runner.testEndsWith('TEST', 'st')) def test_trim(self): self.assertEqual( 'test', self._runner.testTrim('\n\t test \t\n')) def test_substr(self): self.assertEqual( 'teststr', self._runner.testSubstr('teststr', 2, 3)) def test_str(self): self.assertEqual( '123', self._runner.testStr(123)) self.assertEqual( 'true', self._runner.testStr(True)) self.assertEqual( 'false', self._runner.testStr(False)) self.assertEqual( 'null', self._runner.testStr(None)) def test_int(self): self.assertEqual( 123, self._runner.testInt('123')) self.assertEqual( 0, self._runner.testInt(None)) def test_keys(self): self.assertThat( self._runner.testKeys({True: 123, 5: 'Q', 'y': False}), matchers.MatchesSetwise( matchers.Equals('y'), matchers.Is(True), matchers.Equals(5))) def test_values(self): self.assertThat( self._runner.testValues({True: 123, 5: 'Q', 'y': False}), matchers.MatchesSetwise( matchers.Is(False), matchers.Equals(123), matchers.Equals('Q'))) def test_flatten(self): self.assertEqual( [1, 2, 3, 4], self._runner.testFlatten([[1, 2], [3, 4]])) def test_dict_get(self): self.assertEqual( 2, self._runner.testDictGet({'a': 'x', 'y': 2}, 'y')) def test_random_name(self): name1 = self._runner.testRandomName() name2 = self._runner.testRandomName() self.assertIsInstance(name1, str) self.assertIsInstance(name2, str) self.assertThat(len(name1), matchers.GreaterThan(12)) self.assertThat(len(name2), matchers.GreaterThan(12)) self.assertThat(name1, matchers.NotEquals(name2)) def test_pselect(self): self.assertEqual( [1, 4, 9, 16, 25, 36, 49, 64, 81, 100], self._runner.testPSelect([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])) def test_bind(self): self.assertEqual( { 'a': 5, 'b': True, 'c': { 'd': 'x' }, 'e': ['qq-123'] }, self._runner.testBind( { 'a': '$a', '$Z': True, 'c': { 'd': '$value of d' }, 'e': ['$qq-{suffix}'] }, {'a': 5, 'Z': 'b', 'value of d': 'x', 'suffix': 123})) def test_patch(self): self.assertEqual( {'foo': 'bar', 'baz': [42]}, self._runner.testPatch()) def test_skip(self): self.assertEqual( [3, 4, 5, 6], self._runner.testSkip([1, 2, 3, 4, 5, 6], 2) ) def test_take(self): self.assertEqual( [1, 2, 3], self._runner.testTake([1, 2, 3, 4, 5, 6], 3) ) def test_skip_take(self): self.assertEqual( [3, 4, 5], self._runner.testSkipTake([1, 2, 3, 4, 5, 6, 7, 8], 2, 3) ) def test_skip_take_chained(self): self.assertEqual( [3, 4, 5], self._runner.testSkipTakeChained( [1, 2, 3, 4, 5, 6, 7, 8], 2, 3) ) def test_aggregate(self): self.assertEqual( 10, self._runner.testAggregate([1, 2, 3, 4]) ) def test_aggregate_with_initializer(self): self.assertEqual( 21, self._runner.testAggregateWithInitializer([1, 2, 3, 4], 11) ) def test_id(self): obj_id = self._runner.root.object_id self.assertEqual(obj_id * 2, self._runner.testId()) def test_type(self): self.assertEqual('TestEngineFunctions' * 2, self._runner.testType()) def test_is_operator(self): self.assertTrue(self._runner.testIsOperator()) self.assertFalse(self._runner.testNegativeIsOperator()) def test_new_object_assignment(self): self.assertTrue(self._runner.testNewObjectAssignment()) @mock.patch('murano.engine.system.yaql_functions.key_manager') @mock.patch('murano.engine.system.yaql_functions.castellan_utils') def test_decrypt_data(self, mock_castellan_utils, mock_key_manager): dummy_context = mock.MagicMock() mock_castellan_utils.credential_factory.return_value = dummy_context encrypted_value = '91f784d0-5ef1-4b6f-9311-9b5a33d828d8' decrypted_value = 'secret_password' mock_key_manager.API().get.return_value.get_encoded.return_value =\ decrypted_value self.assertEqual(decrypted_value, self._runner.testDecryptData(encrypted_value)) mock_key_manager.API().get.assert_called_once_with(dummy_context, encrypted_value) @mock.patch('murano.engine.system.yaql_functions.LOG') def test_decrypt_data_not_configured(self, mock_log): encrypted_value = '91f784d0-5ef1-4b6f-9311-9b5a33d828d8' self.assertRaises(castellan_exception.AuthTypeInvalidError, self._runner.testDecryptData, encrypted_value) mock_log.error.assert_called() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/dsl/test_exceptions.py0000664000175000017500000000656600000000000023135 0ustar00zuulzuul00000000000000# 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 inspect import os.path import re from testtools import matchers from murano.dsl import dsl_exception from murano.tests.unit.dsl.foundation import object_model as om from murano.tests.unit.dsl.foundation import test_case class TestExceptions(test_case.DslTestCase): def setUp(self): super(TestExceptions, self).setUp() def exception_func(): exc = LookupError('just random Python exception') frameinfo = inspect.getframeinfo(inspect.currentframe()) exc._position = \ os.path.basename(frameinfo.filename), frameinfo.lineno + 4 # line below must be exactly 4 lines after currentframe() raise exc self.register_function(exception_func, 'raisePythonException') self._runner = self.new_runner(om.Object('ExceptionHandling')) def test_throw_catch(self): self._runner.testThrow(1) self.assertEqual( ['enter try', u'exception message', 'finally section'], self.traces) def test_rethrow(self): e = self.assertRaises( dsl_exception.MuranoPlException, self._runner.testThrow, 2) self.assertEqual(['anotherExceptionName'], e.names) self.assertEqual('exception message 2', e.message) self.assertEqual('[anotherExceptionName]: exception message 2', str(e)) self.assertEqual( ['enter try', 'exception message 2', 'rethrow', 'finally section'], self.traces) def test_catch_all_catch(self): self._runner.testThrow(3) self.assertEqual( ['enter try', 'catch all', 'exception message 3', 'finally section'], self.traces) def test_no_throw(self): self._runner.testThrow(4) self.assertEqual( ['enter try', 'exit try', 'else section', 'finally section'], self.traces) def test_stack_trace(self): self._runner.preserve_exception = True e = self.assertRaises( dsl_exception.MuranoPlException, self._runner.testStackTrace) call_stack = e.format() self.assertThat( call_stack, matchers.StartsWith( 'LookupError: just random Python exception')) self.assertIsInstance(e.original_exception, LookupError) filename, line = e.original_exception._position self.assertThat( call_stack, matchers.MatchesRegex( r'.*^ File \".*ExceptionHandling\.yaml\", ' r'line \d+:\d+ in method testStackTrace .*' r'of type ExceptionHandling$.*' r'^ File \".*{0}\", line {1} ' r'in method exception_func$.*'.format(filename, line), re.MULTILINE | re.DOTALL)) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/dsl/test_execution.py0000664000175000017500000000431700000000000022747 0ustar00zuulzuul00000000000000# 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 murano.dsl import dsl_exception from murano.dsl import exceptions from murano.tests.unit.dsl.foundation import object_model as om from murano.tests.unit.dsl.foundation import test_case class TestExecution(test_case.DslTestCase): def _load(self): return self.new_runner( om.Object('SampleClass1', stringProperty='STRING', classProperty=om.Object( 'SampleClass2', class2Property='ANOTHER_STRING'))) def test_load(self): self._load() def test_load_failure(self): self.assertRaises( exceptions.ContractViolationException, self.new_runner, om.Object('SampleClass1')) def test_trace(self): runner = self._load() self.assertEqual([], self.traces) runner.testTrace(123) self.assertEqual([123, 'STRING', 'ANOTHER_STRING'], self.traces) runner.testTrace(321) self.assertEqual([123, 'STRING', 'ANOTHER_STRING', 321, 'STRING', 'ANOTHER_STRING'], self.traces) def test_exception(self): class CustomException(Exception): pass def raise_exception(): raise CustomException() self.register_function(raise_exception, 'raiseException') runner = self._load() self.assertRaises(CustomException, runner.testException) runner.preserve_exception = True self.assertRaises(dsl_exception.MuranoPlException, runner.testException) def test_return(self): self.assertEqual(3, self._load().testReturn(3)) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/dsl/test_extension_methods.py0000664000175000017500000000577400000000000024513 0ustar00zuulzuul00000000000000# 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 yaql.language import exceptions from yaql.language import specs from yaql.language import yaqltypes from murano.dsl import dsl from murano.tests.unit.dsl.foundation import object_model as om from murano.tests.unit.dsl.foundation import test_case class TestExtensionMethods(test_case.DslTestCase): def setUp(self): @dsl.name('extcls.Extender') class PythonClass(object): def __init__(self, arg): self.value = arg @staticmethod @specs.meta('Usage', 'Extension') @specs.parameter('arg', yaqltypes.Integer()) def python_extension(arg): return arg * arg @classmethod @specs.meta('Usage', 'Extension') @specs.parameter('arg', yaqltypes.Integer()) def python_extension2(cls, arg): return cls(2 * arg).value super(TestExtensionMethods, self).setUp() self.package_loader.load_class_package( 'extcls.Extender', None).register_class(PythonClass) self._runner = self.new_runner(om.Object('extcls.TestClass')) def test_call_self_extension_method(self): self.assertEqual([123, 123], self._runner.testSelfExtensionMethod()) def test_call_imported_extension_method(self): self.assertEqual( [246, 246], self._runner.testImportedExtensionMethod()) def test_call_nullable_extension_method(self): self.assertEqual( [123, None], self._runner.testNullableExtensionMethod()) def test_extensions_precedence(self): self.assertEqual(111, self._runner.testExtensionsPrecedence()) def test_explicit_call(self): self.assertEqual(222, self._runner.testCallExtensionExplicitly()) def test_explicit_call_on_instance_fails(self): self.assertRaises( exceptions.NoMatchingMethodException, self._runner.testExplicitCallDoenstWorkOnInstance) def test_call_on_primitive_types(self): self.assertEqual('qWERTy', self._runner.testCallOnPrimitiveTypes()) def test_call_python_extension(self): self.assertEqual(16, self._runner.testCallPythonExtension()) def test_call_python_extension_explicitly(self): self.assertEqual(25, self._runner.testCallPythonExtensionExplicitly()) def test_call_python_classmethod_extension(self): self.assertEqual(14, self._runner.testCallPythonClassmethodExtension()) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/dsl/test_find_class.py0000664000175000017500000000424300000000000023047 0ustar00zuulzuul00000000000000# 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 warnings from murano.dsl import exceptions from murano.tests.unit.dsl.foundation import object_model as om from murano.tests.unit.dsl.foundation import test_case class TestFindClass(test_case.DslTestCase): def setUp(self): super(TestFindClass, self).setUp() self._runner = self.new_runner(om.Object('TestFindClass')) def test_find_class_with_prefix(self): with warnings.catch_warnings(record=True) as capture: self.assertIsNone(self._runner.testFindClassWithPrefix()) self.assertEqual(DeprecationWarning, capture[-1].category) observed = capture[-1].message expected = ("Plugin io.murano.extensions.io.murano.test.TestFixture " "was not found, but a io.murano.test.TestFixture was " "found instead and will be used. This could be caused by " "recent change in plugin naming scheme. If you are " "developing applications targeting this plugin consider " "changing its name") self.assertEqual(expected, str(observed)) def test_find_class_short_name(self): self.assertIsNone(self._runner.testFindClassShortName()) def test_class_with_prefix_not_found(self): observed = self.assertRaises(exceptions.NoClassFound, self._runner.testClassWithPrefixNotFound) expected = ('Class "io.murano.extensions.io.murano.test.TestFixture1" ' 'is not found in io.murano/0.0.0, tests/0.0.0') self.assertEqual(expected, str(observed)) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/dsl/test_gc.py0000664000175000017500000000736600000000000021344 0ustar00zuulzuul00000000000000# 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 murano.dsl import exceptions from murano.dsl.principal_objects import garbage_collector from murano.tests.unit.dsl.foundation import object_model as om from murano.tests.unit.dsl.foundation import test_case class TestGC(test_case.DslTestCase): def setUp(self): super(TestGC, self).setUp() self.package_loader.load_package('io.murano', None).register_class( garbage_collector.GarbageCollector) self.runner = self.new_runner(om.Object('TestGC')) def test_model_destroyed(self): model = om.Object( 'TestGCNode', 'root', value='root', nodes=[ om.Object( 'TestGCNode', 'node1', value='node1', nodes=['root', 'node2'] ), om.Object( 'TestGCNode', 'node2', value='node2', nodes=['root', 'node1'] ), ] ) model = {'Objects': None, 'ObjectsCopy': model} self.new_runner(model) self.assertCountEqual(['node1', 'node2'], self.traces[:2]) self.assertEqual('root', self.traces[-1]) def test_collect_from_code(self): self.runner.testObjectsCollect() self.assertEqual(['B', 'A'], self.traces) def test_collect_with_subscription(self): self.runner.testObjectsCollectWithSubscription() self.assertEqual( ['Destroy A', 'Destroy B', 'Destruction of B', 'B', 'A'], self.traces) def test_call_on_destroyed_object(self): self.assertRaises( exceptions.ObjectDestroyedError, self.runner.testCallOnDestroyedObject) self.assertEqual(['foo', 'X'], self.traces) def test_destruction_dependencies_serialization(self): self.runner.testDestructionDependencySerialization() node1 = self.runner.serialized_model['Objects']['outNode'] node2 = node1['nodes'][0] deps = { 'onDestruction': [{ 'subscriber': self.runner.root.object_id, 'handler': '_handler' }] } self.assertEqual(deps, node1['?'].get('dependencies')) self.assertEqual( node1['?'].get('dependencies'), node2['?'].get('dependencies')) model = self.runner.serialized_model model['Objects']['outNode'] = None self.new_runner(model) self.assertEqual(['Destroy A', 'Destroy B', 'B', 'A'], self.traces) def test_is_doomed(self): self.runner.testIsDoomed() self.assertEqual([[], True, 'B', [True], False, 'A'], self.traces) def test_is_destroyed(self): self.runner.testIsDestroyed() self.assertEqual([False, True], self.traces) def test_static_property_not_destroyed(self): self.runner.testStaticProperties() self.assertEqual([], self.traces) def test_args_not_destroyed(self): self.runner.testDestroyArgs() self.assertEqual([], self.traces) def test_runtime_property_not_destroyed(self): self.runner.testReachableRuntimeProperties() self.assertEqual([False, ], self.traces) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/dsl/test_helpers.py0000664000175000017500000004073000000000000022405 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or # implied. # See the License for the specific language governing permissions and # limitations under the License. import copy import semantic_version import types from unittest import mock import weakref from oslo_utils.uuidutils import generate_uuid from murano.dsl import dsl_types from murano.dsl import exceptions from murano.dsl import helpers from murano.tests.unit import base class TestDSLHelpers(base.MuranoTestCase): @mock.patch.object(helpers, 'with_object_store', autospec=True) def test_parallel_select_except_exception(self, mock_with_object_store): mock_with_object_store.side_effect = ValueError self.assertRaises(ValueError, helpers.parallel_select, [mock.sentinel.foo], lambda: None) def test_enum(self): self.assertEqual('Enum', helpers.enum().__name__) def test_cast_with_murano_type(self): mock_attrs = { 'name': mock.sentinel.class_type, 'version': semantic_version.Version('1.0.0'), 'ancestors.return_value': [] } mock_type = mock.Mock() mock_type.configure_mock(**mock_attrs) mock_obj = mock.Mock(type=mock_type) mock_obj.cast.return_value = mock.sentinel.foo_cast_value mock_murano_class = mock.Mock(spec=dsl_types.MuranoType) mock_murano_class.name = mock.sentinel.class_type mock_murano_class.version = semantic_version.Version('1.0.0') result = helpers.cast(mock_obj, mock_murano_class, pov_or_version_spec=None) self.assertEqual(mock.sentinel.foo_cast_value, result) mock_obj.cast.assert_called_once_with(mock_type) def test_cast_except_value_error(self): mock_attrs = { 'name': mock.sentinel.class_type, 'version': semantic_version.Version('1.0.0'), 'ancestors.return_value': [] } mock_type = mock.Mock() mock_type.configure_mock(**mock_attrs) mock_obj = mock.Mock(type=mock_type) mock_murano_class = mock.Mock(spec=dsl_types.MuranoType) mock_murano_class.name = mock.sentinel.class_type e = self.assertRaises(ValueError, helpers.cast, mock_obj, mock_murano_class, pov_or_version_spec=mock.Mock()) self.assertEqual('pov_or_version_spec of unsupported type {0}' .format(type(mock.Mock())), str(e)) def test_cast_except_no_class_found(self): mock_attrs = { 'name': mock.sentinel.name, 'package.name': mock.sentinel.package_name, 'version': mock.sentinel.version, 'ancestors.return_value': [] } mock_type = mock.Mock() mock_type.configure_mock(**mock_attrs) mock_obj = mock.Mock(type=mock_type) mock_murano_class = mock.Mock(spec=dsl_types.MuranoTypeReference) mock_murano_class.type = mock.sentinel.foo_class mock_version_spec = mock.Mock(spec=dsl_types.MuranoPackage) e = self.assertRaises(exceptions.NoClassFound, helpers.cast, mock_obj, mock_murano_class, pov_or_version_spec=mock_version_spec) self.assertIn('Class "sentinel.foo_class" is not found', str(e)) def test_cast_except_ambiguous_class_name(self): mock_attrs = { 'name': mock.sentinel.class_type, 'version': semantic_version.Version('1.0.0') } mock_ancestor = mock.Mock() mock_ancestor.configure_mock(**mock_attrs) mock_attrs['ancestors.return_value'] = [mock_ancestor] mock_type = mock.Mock() mock_type.configure_mock(**mock_attrs) mock_obj = mock.Mock(type=mock_type) mock_murano_class = mock.Mock(spec=dsl_types.MuranoTypeReference) mock_murano_class.type = mock.sentinel.class_type # pov_or_version_spec of '1' will be converted to # semantic_version.Spec('>=1.0.0,<2.0.0-0') self.assertRaises(exceptions.AmbiguousClassName, helpers.cast, mock_obj, mock_murano_class, pov_or_version_spec='1') def test_inspect_is_method(self): mock_cls = mock.Mock(foo=lambda: None, bar=None) self.assertTrue(helpers.inspect_is_method(mock_cls, 'foo')) self.assertFalse(helpers.inspect_is_method(mock_cls, 'bar')) def test_inspect_is_property(self): data_descriptor = mock.MagicMock(__get__=None, __set__=None) mock_cls = mock.Mock(foo=data_descriptor, bar=None) self.assertTrue(helpers.inspect_is_property(mock_cls, 'foo')) self.assertFalse(helpers.inspect_is_property(mock_cls, 'bar')) def test_updated_dict(self): dict_ = {'foo': 'bar'} self.assertEqual(dict_, helpers.updated_dict(dict_, {})) def test_updated_dict_with_null_arg(self): dict_ = {'foo': 'bar'} self.assertEqual(dict_, helpers.updated_dict(None, dict_)) def test_resolve_with_return_reference_true(self): mock_value = mock.Mock(spec=dsl_types.MuranoTypeReference) mock_scope_type = mock.Mock(spec=dsl_types.MuranoTypeReference) result = helpers.resolve_type(mock_value, mock_scope_type, True) self.assertEqual(mock_value, result) mock_value = mock.Mock() mock_value.get_reference.return_value = mock.sentinel.foo_reference mock_scope_type = mock.Mock() mock_scope_type.package.find_class.return_value = mock_value result = helpers.resolve_type(mock_value, mock_scope_type, True) self.assertEqual(mock.sentinel.foo_reference, result) def test_resolve_type_with_null_value(self): self.assertIsNone(helpers.resolve_type(None, None)) def test_assemble_object_definition(self): test_parsed = { 'type': mock.sentinel.type, 'properties': {}, 'id': mock.sentinel.id, 'name': mock.sentinel.name, 'metadata': mock.sentinel.metadata, 'destroyed': True, 'extra': {} } expected = { '?': { 'type': mock.sentinel.type, 'id': mock.sentinel.id, 'name': mock.sentinel.name, 'metadata': mock.sentinel.metadata, 'destroyed': True } } result = helpers.assemble_object_definition(test_parsed) for key, val in expected.items(): self.assertEqual(val, result[key]) @mock.patch.object(helpers, 'format_type_string', autospec=True) def test_assemble_object_definition_with_serializable_model_format( self, mock_format_type_string): mock_format_type_string.return_value = mock.sentinel.type test_parsed = { 'type': mock.sentinel.type, 'properties': {}, 'id': mock.sentinel.id, 'name': mock.sentinel.name, 'metadata': mock.sentinel.metadata, 'destroyed': True, 'extra': {} } expected = { '?': { 'type': mock.sentinel.type, 'id': mock.sentinel.id, 'name': mock.sentinel.name, 'metadata': mock.sentinel.metadata, 'destroyed': True } } model_format = dsl_types.DumpTypes.Serializable result = helpers.assemble_object_definition(test_parsed, model_format) for key, val in expected['?'].items(): self.assertEqual(val, result['?'][key]) def test_assemble_object_definition_with_inline_model_format(self): test_parsed = { 'type': mock.sentinel.type, 'properties': mock.sentinel.properties, 'id': mock.sentinel.id, 'name': mock.sentinel.name, 'metadata': mock.sentinel.metadata, 'dependencies': mock.sentinel.dependencies, 'destroyed': mock.sentinel.destroyed, 'extra': {} } model_format = dsl_types.DumpTypes.Inline expected = copy.copy(test_parsed) expected[mock.sentinel.type] = mock.sentinel.properties for key in ['type', 'extra', 'properties']: expected.pop(key) result = helpers.assemble_object_definition(test_parsed, model_format) for key, val in expected.items(): self.assertEqual(val, result[key]) def test_assemble_object_definition_except_value_error(self): test_parsed = { 'type': mock.sentinel.type, 'properties': {}, 'id': mock.sentinel.id, 'name': mock.sentinel.name, 'metadata': mock.sentinel.metadata, 'destroyed': True, 'extra': {} } e = self.assertRaises(ValueError, helpers.assemble_object_definition, test_parsed, None) self.assertEqual('Invalid Serialization Type', str(e)) def test_function(self): def f(): return self.assertTrue(isinstance(helpers.function(f), types.FunctionType)) def test_function_from_method(self): class C: def m(self): return c = C() self.assertTrue(isinstance(helpers.function(c.m), types.FunctionType)) def test_weak_proxy(self): self.assertIsNone(helpers.weak_proxy(None)) def test_weak_proxy_with_reference_type(self): result = helpers.weak_proxy(weakref.ReferenceType(int)) self.assertEqual('int', result.__name__) @mock.patch.object(helpers, 'get_object_store', autospec=True) def test_weak_ref(self, mock_get_object_store): mock_object_store = mock.Mock( **{'get.return_value': mock.sentinel.res}) mock_get_object_store.return_value = mock_object_store test_obj = dsl_types.MuranoObject() setattr(test_obj, 'object_id', generate_uuid()) murano_object_weak_ref = helpers.weak_ref(test_obj) setattr(murano_object_weak_ref, 'ref', lambda *args: None) result = murano_object_weak_ref.__call__() self.assertEqual(mock.sentinel.res, result) self.assertEqual(weakref.ReferenceType.__name__, murano_object_weak_ref.ref.__class__.__name__) def test_weak_ref_with_null_obj(self): self.assertIsNone(helpers.weak_ref(None)) @mock.patch.object(helpers, 're', autospec=True) def test_parse_type_string_with_null_res(self, mock_re): mock_re.compile.return_value = mock.Mock( **{'match.return_value': None}) self.assertIsNone(helpers.parse_type_string('', None, None)) def test_format_type_string(self): inner_type_obj = mock.Mock(spec=dsl_types.MuranoType) inner_type_obj.configure_mock(**{'name': 'foo', 'version': 'foo_ver'}) inner_type_obj_pkg = mock.Mock() inner_type_obj_pkg.configure_mock(name='foo_pkg') setattr(inner_type_obj, 'package', inner_type_obj_pkg) type_obj = mock.Mock(spec=dsl_types.MuranoTypeReference, type=inner_type_obj) result = helpers.format_type_string(type_obj) self.assertEqual('foo/foo_ver@foo_pkg', result) def test_format_type_string_except_value_error(self): type_obj = mock.Mock(spec=dsl_types.MuranoTypeReference, type=None) e = self.assertRaises(ValueError, helpers.format_type_string, type_obj) self.assertEqual('Invalid argument', str(e)) def test_patch_dict(self): path = 'foo.bar.baz' fake_dict = mock.MagicMock(spec=dict) # Make the dict return itself to test whether all the parts are called. fake_dict.get.return_value = fake_dict helpers.patch_dict(fake_dict, path, None) fake_dict.get.assert_has_calls([mock.call('foo'), mock.call('bar')]) fake_dict.pop.assert_not_called() def test_patch_dict_without_dict(self): path = 'foo.bar.baz' not_a_dict = mock.Mock() helpers.patch_dict(not_a_dict, path, None) not_a_dict.get.assert_not_called() not_a_dict.pop.assert_not_called() @mock.patch.object(helpers, 'gc') def test_walk_gc_with_towards_true(self, mock_gc, autospec=True): mock_gc.get_referrers.side_effect = [ [mock.sentinel.second], [mock.sentinel.third] ] first_obj = mock.sentinel.first handler = mock.MagicMock() handler.return_value = True expected = [ [mock.sentinel.first], [mock.sentinel.first, mock.sentinel.second], [mock.sentinel.first, mock.sentinel.second, mock.sentinel.third] ] actual = [] for obj in helpers.walk_gc(first_obj, True, handler): actual.append(obj) self.assertEqual(expected, actual) @mock.patch.object(helpers, 'gc', autospec=True) def test_walk_gc_with_towards_false(self, mock_gc): mock_gc.get_referents.side_effect = [ # Trigger the continue by duplicating entries. [mock.sentinel.second], [mock.sentinel.second] ] first_obj = mock.sentinel.first handler = mock.MagicMock() handler.return_value = True expected = [ [mock.sentinel.first], [mock.sentinel.second, mock.sentinel.first] ] actual = [] for obj in helpers.walk_gc(first_obj, False, handler): actual.append(obj) self.assertEqual(expected, actual) class TestMergeDicts(base.MuranoTestCase): def check(self, dict1, dict2, expected): result = helpers.merge_dicts(dict1, dict2) self.assertEqual(expected, result) def test_dicts_plain(self): dict1 = {"a": "1"} dict2 = {"a": "100", "ab": "12"} expected = {"a": "100", "ab": "12"} self.check(dict1, dict2, expected) def test_different_types_none(self): dict1 = {"a": "1"} dict2 = {"a": None, "ab": "12"} expected = {"a": "1", "ab": "12"} self.check(dict1, dict2, expected) def test_different_types_of_iterable(self): dict1 = {"a": {"ab": "1"}} dict2 = {"a": ["ab", "1"]} self.assertRaises(TypeError, helpers.merge_dicts, dict1, dict2) def test_merge_nested_dicts(self): dict1 = {"a": {"ab": {}, "abc": "1"}} dict2 = {"a": {"abc": "123"}} expected = {"a": {"ab": {}, "abc": "123"}} self.check(dict1, dict2, expected) def test_merge_nested_dicts_with_max_levels(self): dict1 = {"a": {"ab": {"abcd": "1234"}, "abc": "1"}} dict2 = {"a": {"ab": {"y": "9"}, "abc": "123"}} expected = {"a": {"ab": {"y": "9"}, "abc": "123"}} result = helpers.merge_dicts(dict1, dict2, max_levels=2) self.assertEqual(expected, result) def test_merge_with_lists(self): dict1 = {"a": [1, 2]} dict2 = {"a": [1, 3, 2, 4]} expected = {"a": [1, 2, 3, 4]} self.check(dict1, dict2, expected) class TestParseVersionSpec(base.MuranoTestCase): def check(self, expected, version_spec): self.assertEqual(expected, helpers.parse_version_spec(version_spec)) def test_empty_version_spec(self): version_spec = "" expected = semantic_version.Spec('>=0.0.0', '<1.0.0') self.check(expected, version_spec) def test_empty_kind(self): version_spec = "1.11.111" expected = semantic_version.Spec('==1.11.111') self.check(expected, version_spec) def test_implicit_major(self): version_spec = ">=2" expected = semantic_version.Spec('>=2.0.0') self.check(expected, version_spec) def test_implicit_minor(self): version_spec = ">=2.1" expected = semantic_version.Spec('>=2.1.0') self.check(expected, version_spec) def test_remove_spaces(self): version_spec = "< = 2 .1" expected = semantic_version.Spec('<2.2.0') self.check(expected, version_spec) def test_input_version(self): version_spec = semantic_version.Version('1.11.111') expected = semantic_version.Spec('==1.11.111') self.check(expected, version_spec) def test_input_spec(self): version_spec = semantic_version.Spec('<=1', '<=1.11') expected = semantic_version.Spec('<1.12.0', '<2.0.0') self.check(expected, version_spec) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/dsl/test_logger.py0000664000175000017500000001236200000000000022222 0ustar00zuulzuul00000000000000# coding: utf-8 # 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 unittest import mock from murano.dsl import helpers from murano.engine.system import logger from murano.tests.unit.dsl.foundation import object_model as om from murano.tests.unit.dsl.foundation import test_case class TestLogger(test_case.DslTestCase): FORMAT_CALLS = [ mock.call(mock.ANY, 'str', (), {}), mock.call(mock.ANY, u'тест', (), {}), mock.call(mock.ANY, 'str', (1,), {}), mock.call(mock.ANY, 'str {0}', ('message',), {}), mock.call(mock.ANY, 'str {message}', (), {'message': 'message'}), mock.call(mock.ANY, 'str {message}{0}', (), {})] LOG_CALLS = FORMAT_CALLS def setUp(self): super(TestLogger, self).setUp() self._runner = self.new_runner(om.Object('TestLogger')) self.package_loader.load_package('io.murano', None).register_class( logger.Logger) def test_create(self): logger_instance = self._runner.testCreate() self.assertTrue( helpers.is_instance_of(logger_instance, 'io.murano.system.Logger'), 'Function should return io.murano.system.Logger instance') def _create_logger_mock(self): logger_instance = self._runner.testCreate() logger_ext = logger_instance.extension underlying_logger_mock = mock.MagicMock() logger_ext._underlying_logger = underlying_logger_mock logger_ext._underlying_logger.return_value = None format_mock = mock.MagicMock(return_value='format_mock') # do not verify number of conversions to string format_mock.__str__ = mock.MagicMock(return_value='format_mock') format_mock.__unicode__ = mock.MagicMock(return_value='format_mock') logger_ext._format_without_exceptions = format_mock return logger_instance, format_mock, underlying_logger_mock def test_trace(self): logger_instance, format_mock, underlying_logger_mock \ = self._create_logger_mock() log_method = mock.MagicMock() underlying_logger_mock.trace = log_method self._runner.testTrace(logger_instance) format_mock.assert_has_calls(self.FORMAT_CALLS, any_order=False) self.assertEqual(log_method.call_count, len(self.LOG_CALLS)) def test_debug(self): logger_instance, format_mock, underlying_logger_mock \ = self._create_logger_mock() log_method = mock.MagicMock() underlying_logger_mock.debug = log_method self._runner.testDebug(logger_instance) format_mock.assert_has_calls(self.FORMAT_CALLS, any_order=False) self.assertEqual(log_method.call_count, len(self.LOG_CALLS)) def test_info(self): logger_instance, format_mock, underlying_logger_mock \ = self._create_logger_mock() log_method = mock.MagicMock() underlying_logger_mock.info = log_method self._runner.testInfo(logger_instance) format_mock.assert_has_calls(self.FORMAT_CALLS, any_order=False) self.assertEqual(log_method.call_count, len(self.LOG_CALLS)) def test_warning(self): logger_instance, format_mock, underlying_logger_mock \ = self._create_logger_mock() log_method = mock.MagicMock() underlying_logger_mock.warning = log_method self._runner.testWarning(logger_instance) format_mock.assert_has_calls(self.FORMAT_CALLS, any_order=False) self.assertEqual(log_method.call_count, len(self.LOG_CALLS)) def test_error(self): logger_instance, format_mock, underlying_logger_mock \ = self._create_logger_mock() log_method = mock.MagicMock() underlying_logger_mock.error = log_method self._runner.testError(logger_instance) format_mock.assert_has_calls(self.FORMAT_CALLS, any_order=False) self.assertEqual(log_method.call_count, len(self.FORMAT_CALLS)) def test_critical(self): logger_instance, format_mock, underlying_logger_mock \ = self._create_logger_mock() log_method = mock.MagicMock() underlying_logger_mock.critical = log_method self._runner.testCritical(logger_instance) format_mock.assert_has_calls(self.FORMAT_CALLS, any_order=False) self.assertEqual(log_method.call_count, len(self.LOG_CALLS)) def test_exception(self): logger_instance, format_mock, underlying_logger_mock = \ self._create_logger_mock() log_method = mock.MagicMock() underlying_logger_mock.error = log_method self._runner.testException(logger_instance) format_mock.assert_has_calls(self.FORMAT_CALLS, any_order=False) self.assertEqual(log_method.call_count, len(self.LOG_CALLS)) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/dsl/test_macros.py0000664000175000017500000001114600000000000022226 0ustar00zuulzuul00000000000000# 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 testtools import matchers from murano.dsl import exceptions from murano.tests.unit.dsl.foundation import object_model as om from murano.tests.unit.dsl.foundation import test_case class TestMacros(test_case.DslTestCase): def setUp(self): super(TestMacros, self).setUp() self._runner = self.new_runner(om.Object('MacroExamples')) def test_if(self): self.assertEqual('gt', self._runner.testIf(6)) self.assertEqual('def', self._runner.testIf(4)) self.assertEqual('gt', self._runner.testIfElse(6)) self.assertEqual('lt', self._runner.testIfElse(4)) def test_if_non_boolean(self): self.assertEqual(1100, self._runner.testIfNonBoolean()) def test_while(self): self.assertEqual(0, self._runner.testWhile(3)) self.assertEqual([3, 2, 1], self.traces) def test_while_non_boolean(self): self.assertEqual([], self._runner.testWhileNonBoolean()) def test_for(self): self.assertIsNone(self._runner.testFor()) self.assertEqual(['x', 'y', 'z', 2, 5, 10], self.traces) def test_repeat(self): self._runner.testRepeat(4) self.assertEqual(['run', 'run', 'run', 'run'], self.traces) def test_break(self): self.assertRaises(exceptions.DslInvalidOperationError, self._runner.testBreak) self.assertEqual([0, 1, 2, 'breaking', 'method_break'], self.traces) def test_continue(self): self.assertRaises(exceptions.DslInvalidOperationError, self._runner.testContinue) self.assertEqual([0, 1, 2, 5, 6, 'method_continue'], self.traces) def test_match(self): self.assertEqual('y', self._runner.testMatch(1)) self.assertEqual('x', self._runner.testMatch(2)) self.assertEqual('z', self._runner.testMatch(3)) self.assertIsNone(self._runner.testMatch(0)) self.assertEqual('y', self._runner.testMatchDefault(1)) self.assertEqual('x', self._runner.testMatchDefault(2)) self.assertEqual('z', self._runner.testMatchDefault(3)) self.assertEqual('def', self._runner.testMatchDefault(0)) def test_switch(self): self.assertIsNone(self._runner.testSwitch(20)) self.assertEqual(['gt'], self.traces) del self.traces self.assertIsNone(self._runner.testSwitch(200)) self.assertThat( self.traces, matchers.MatchesSetwise( matchers.Equals('gt100'), matchers.Equals('gt'))) del self.traces self.assertIsNone(self._runner.testSwitch(2)) self.assertEqual(['lt'], self.traces) def test_switch_with_default(self): self.assertIsNone(self._runner.testSwitchDefault(20)) self.assertEqual(['gt'], self.traces) del self.traces self.assertIsNone(self._runner.testSwitchDefault(200)) self.assertThat( self.traces, matchers.MatchesSetwise( matchers.Equals('gt100'), matchers.Equals('gt'))) del self.traces self.assertIsNone(self._runner.testSwitchDefault(-20)) self.assertEqual(['lt'], self.traces) del self.traces self.assertIsNone(self._runner.testSwitchDefault(5)) self.assertEqual(['def'], self.traces) def test_switch_non_boolean(self): self.assertEqual(1110000, self._runner.testSwitchNonBoolean()) def test_code_block(self): self.assertEqual(123, self._runner.testCodeBlock()) self.assertEqual(['a', 123], self.traces) def test_parallel(self): self.assertIsNone(self._runner.testParallel()) self.assertEqual(['enter', 'enter', 'exit', 'exit'], self.traces) def test_parallel_with_limit(self): self.assertIsNone(self._runner.testParallelWithLimit()) self.assertEqual(['enter', 'enter', 'exit', 'exit', 'enter', 'exit'], self.traces) def test_scope_within_macro(self): self.assertEqual( 87654321, self._runner.testScopeWithinMacro()) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/dsl/test_meta.py0000664000175000017500000000432000000000000021664 0ustar00zuulzuul00000000000000# 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 murano.tests.unit.dsl.foundation import object_model as om from murano.tests.unit.dsl.foundation import test_case class TestMeta(test_case.DslTestCase): def setUp(self): super(TestMeta, self).setUp() self._runner = self.new_runner(om.Object('metatests.TestMeta')) def test_class_multi_meta(self): self.assertCountEqual( [4, 1, 111, 2], self._runner.testClassMultiMeta()) def test_class_single_meta(self): self.assertCountEqual( [5, 6], self._runner.testClassSingleMeta()) def test_parent_class_not_inherited_meta(self): self.assertEqual(3, self._runner.testParentClassNotInheritedMeta()) def test_method_meta(self): self.assertCountEqual( [7, 8, 9, 4, 1, 10], self._runner.testMethodMeta()) def test_method_argument_meta(self): self.assertCountEqual( [1, 2, 3], self._runner.testMethodArgumentMeta()) def test_inherited_property_meta(self): self.assertEqual( [1], self._runner.testInheritedPropertyMeta()) def test_overridden_property_meta(self): self.assertCountEqual( [1, 4], self._runner.testOverriddenPropertyMeta()) def test_package_meta(self): self.assertEqual( [], self._runner.testPackageMeta()) def test_complex_meta(self): self.assertCountEqual([ [1, 'metatests.PropertyType'], [2, 'metatests.PropertyType'], [3, 'metatests.PropertyType2'], [4, 'metatests.PropertyType'], [5, 'metatests.PropertyType2'] ], self._runner.testComplexMeta()) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/dsl/test_method_param_inheritance.py0000664000175000017500000000257000000000000025754 0ustar00zuulzuul00000000000000# 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 yaql.language import exceptions as yaql_exceptions from murano.dsl import exceptions as dsl_exceptions from murano.tests.unit.dsl.foundation import object_model as om from murano.tests.unit.dsl.foundation import test_case class TestMethodParamInheritance(test_case.DslTestCase): def setUp(self): super(TestMethodParamInheritance, self).setUp() model = om.Object('TestMethodParamInheritanceDerived') self._runner = self.new_runner(model) def test_different_set_of_params_causes_exception(self): self.assertRaises( yaql_exceptions.NoMatchingMethodException, self._runner.testRunWithParam) self.assertRaises( dsl_exceptions.ContractViolationException, self._runner.testRunWithoutParam) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/dsl/test_multiple_inheritance.py0000664000175000017500000000513100000000000025143 0ustar00zuulzuul00000000000000# 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 murano.tests.unit.dsl.foundation import object_model as om from murano.tests.unit.dsl.foundation import test_case class TestMultipleInheritance(test_case.DslTestCase): def setUp(self): super(TestMultipleInheritance, self).setUp() self._multi_derived = om.Object( 'DerivedFrom2Classes', rootProperty='ROOT') model = om.Object( 'SampleClass3', multiClassProperty=self._multi_derived ) self._runner = self.new_runner(model) def test_multi_class_contract(self): self._runner.testMultiContract() self.assertEqual( ['ParentClass1::method1', 'ParentClass2::method2'], self.traces) def test_root_method_resolution(self): self._runner.on(self._multi_derived).testRootMethod() self.assertEqual( ['CommonParent::testRootMethod', 'ROOT'], self.traces) def test_property_accessible_on_several_paths(self): self.assertEqual( 'ROOT', self._runner.testPropertyAccessibleOnSeveralPaths()) def test_specialized_mixin_override(self): self._runner.on(self._multi_derived).testMixinOverride() self.assertEqual( ['ParentClass2::virtualMethod', '-', 'CommonParent::virtualMethod', '-', 'CommonParent::virtualMethod', '-', 'ParentClass2::virtualMethod'], self.traces) def test_super(self): self._runner.on(self._multi_derived).testSuper() self.assertCountEqual( ['CommonParent::virtualMethod', 'ParentClass2::virtualMethod', 'CommonParent::virtualMethod', 'ParentClass2::virtualMethod'], self.traces) def test_psuper(self): self._runner.on(self._multi_derived).testPsuper() self.assertCountEqual( ['CommonParent::virtualMethod', 'ParentClass2::virtualMethod', 'CommonParent::virtualMethod', 'ParentClass2::virtualMethod'], self.traces) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/dsl/test_objects_copy_merge.py0000664000175000017500000000276700000000000024615 0ustar00zuulzuul00000000000000# 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 copy from murano.tests.unit.dsl.foundation import object_model as om from murano.tests.unit.dsl.foundation import test_case class TestObjectsCopyMerge(test_case.DslTestCase): def test_merged(self): gen1_model = om.Object( 'TestObjectsCopyMergeSampleClass', 'rootNode', value='node1', nodes=[ om.Object('TestObjectsCopyMergeSampleClass', value='node2', nodes=[om.Ref('rootNode')]) ]) gen2_model = copy.deepcopy(gen1_model) gen2_model['nodes'] = [] gen2_model['value'] = 'node1-updated' model = { 'Objects': gen2_model, 'ObjectsCopy': gen1_model } runner = self.new_runner(model) self.assertEqual(['node2', 'node1-updated'], self.traces) self.assertEqual('It works!', runner.testMethod()) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/dsl/test_property_access.py0000664000175000017500000001011400000000000024141 0ustar00zuulzuul00000000000000# 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 murano.dsl import exceptions from murano.tests.unit.dsl.foundation import object_model as om from murano.tests.unit.dsl.foundation import test_case class TestPropertyAccess(test_case.DslTestCase): def setUp(self): super(TestPropertyAccess, self).setUp() self._multi_derived = om.Object( 'DerivedFrom2Classes', rootProperty='ROOT', ambiguousProperty=321) model = om.Object( 'SampleClass3', multiClassProperty=self._multi_derived ) self._runner = self.new_runner(model) def test_private_property_access(self): self.assertIsNone(self._runner.testPrivateProperty()) self.assertEqual( ['CommonParent', 'ParentClass1', 'SampleClass3'], self.traces) def test_private_property_access_failure(self): e = self.assertRaises( exceptions.UninitializedPropertyAccessError, self._runner.testUninitializedPrivatePropertyAccess) self.assertEqual( 'Access to uninitialized property "privateName" ' 'in class "SampleClass3" is forbidden', str(e)) def test_read_of_private_property_of_other_class(self): e = self.assertRaises( exceptions.PropertyAccessError, self._runner.testReadOfPrivatePropertyOfOtherClass) self.assertEqual( 'Property "privateProperty" in class "DerivedFrom2Classes" ' 'cannot be read', str(e)) self.assertEqual(['accessing property'], self.traces) def test_write_of_private_property_of_other_class(self): e = self.assertRaises( exceptions.PropertyAccessError, self._runner.testWriteOfPrivatePropertyOfOtherClass) self.assertEqual( 'Property "privateProperty" in class "DerivedFrom2Classes" ' 'cannot be written', str(e)) def test_access_ambiguous_property_with_resolver(self): self.assertEqual( '321', self._runner.on(self._multi_derived). testAccessAmbiguousPropertyWithResolver()) def test_property_merge(self): self.assertEqual( '555', self._runner.on(self._multi_derived). testPropertyMerge()) self.assertEqual( ['321', '555', '555', '555', '555'], self.traces) def test_property_usage(self): e = self.assertRaises( exceptions.NoWriteAccessError, self._runner.on(self._multi_derived). testModifyUsageTestProperty1) self.assertEqual( 'Property "usageTestProperty1" is immutable to the caller', str(e)) self.assertRaises( exceptions.NoWriteAccessError, self._runner.on(self._multi_derived). testModifyUsageTestProperty2) self.assertEqual( 33, self._runner.on(self._multi_derived). testModifyUsageTestProperty3()) self.assertEqual( 44, self._runner.on(self._multi_derived). testModifyUsageTestProperty4()) self.assertEqual( 55, self._runner.on(self._multi_derived). testModifyUsageTestProperty5()) self.assertRaises( exceptions.NoWriteAccessError, self._runner.on(self._multi_derived). testModifyUsageTestProperty6) self.assertEqual( 77, self._runner.on( self._multi_derived).testModifyUsageTestProperty7()) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/dsl/test_property_inititialization.py0000664000175000017500000000336000000000000026271 0ustar00zuulzuul00000000000000# 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 murano.dsl import exceptions from murano.tests.unit.dsl.foundation import object_model as om from murano.tests.unit.dsl.foundation import test_case class TestPropertyInitialization(test_case.DslTestCase): def setUp(self): super(TestPropertyInitialization, self).setUp() model = om.Object( 'PropertyInit' ) self._runner = self.new_runner(model) def test_runtime_property_default(self): self.assertEqual( 'DEFAULT', self._runner.testRuntimePropertyDefault()) def test_runtime_property_without_default(self): self.assertRaises( exceptions.UninitializedPropertyAccessError, self._runner.testRuntimePropertyWithoutDefault) def test_runtime_property_with_strict_contract_without_default(self): self.assertEqual( 'VALUE', self._runner.testRuntimePropertyWithStrictContractWithoutDefault()) def test_uninitialized_runtime_property_with_strict_contract(self): self.assertRaises( exceptions.UninitializedPropertyAccessError, self._runner.testUninitializedRuntimeProperty) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/dsl/test_reflection.py0000664000175000017500000000506000000000000023072 0ustar00zuulzuul00000000000000# 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 murano.tests.unit.dsl.foundation import object_model as om from murano.tests.unit.dsl.foundation import test_case class TestReflection(test_case.DslTestCase): def setUp(self): super(TestReflection, self).setUp() self._runner = self.new_runner(om.Object('TestReflection')) def test_type_info(self): self.assertEqual( { 'name': 'TestReflection', 'versionStr': '0.0.0', 'versionMajor': 0, 'versionMinor': 0, 'versionPatch': 0, 'ancestors': ['io.murano.Object'], 'properties': ['property', 'staticProperty'], 'package': 'tests', 'methods': ['foo', 'getAttr', 'setAttr'] }, self._runner.testTypeInfo()) def test_method_info(self): self.assertEqual( { 'name': 'foo', 'arguments': ['bar', 'baz'], 'barHasDefault': True, 'bazHasDefault': False, 'barMethod': 'foo', 'bazMethod': 'foo', 'declaringType': 'TestReflection' }, self._runner.testMethodInfo()) def test_property_info(self): self.assertEqual( { 'name': 'property', 'hasDefault': True, 'usage': 'InOut' }, self._runner.testPropertyInfo()) def test_property_read(self): self.assertEqual( [['object', 'static'], ['static']], self._runner.testPropertyRead()) def test_property_write(self): self.assertEqual( [['new object', 'new static'], ['new static']], self._runner.testPropertyWrite()) def test_method_invoke(self): self.assertEqual('bar baz', self._runner.testMethodInvoke()) def test_instance_create(self): self.assertEqual('test', self._runner.testInstanceCreate()) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/dsl/test_results_serializer.py0000664000175000017500000001530100000000000024671 0ustar00zuulzuul00000000000000# 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 testtools import matchers from murano.dsl import serializer from murano.tests.unit.dsl.foundation import object_model as om from murano.tests.unit.dsl.foundation import test_case # Tests for correctness of serialization of MuranoPL object tree # back to Object Model class TestResultsSerializer(test_case.DslTestCase): def setUp(self): super(TestResultsSerializer, self).setUp() self._class2 = om.Object('SampleClass2', class2Property='string2') self._class1 = om.Object( 'SampleClass1', 'def', stringProperty='string1', arbitraryProperty={'a': [1, 2]}, classProperty=self._class2) self._root_class = om.Object('ContractExamples', 'abc', sampleClass=self._class1) self._runner = self.new_runner(self._root_class) def _test_data_in_section(self, name, serialized): """Test that Model -> Load -> Serialize = Model Test that a section of Object Model has expected structure and property values that are equal to those originally loaded (e.g. that Model -> Load -> Serialize = Model) :param name: section name :param serialized: serialized Object Model """ self.assertEqual('abc', serialized[name]['?']['id']) self.assertEqual('ContractExamples/0.0.0@tests', serialized['Objects']['?']['type']) self.assertIsInstance(serialized[name]['sampleClass'], dict) self.assertEqual('def', serialized[name]['sampleClass']['?']['id']) self.assertEqual('SampleClass1/0.0.0@tests', serialized[name]['sampleClass']['?']['type']) self.assertEqual('string1', serialized[name]['sampleClass']['stringProperty']) def test_results_serialize(self): """Test that serialized contains same values and headers Test that serialized Object Model has both Objects and ObjectsCopy sections and they both contain the same property values and object headers. Note, that Objects section may contain additional designer metadata and information on available actions that is not needed in ObjectsCopy thus we cannot test for ObjectsCopy be strictly equal to Objects """ serialized = self._runner.serialized_model self.assertIn('Objects', serialized) self.assertIn('ObjectsCopy', serialized) self._test_data_in_section('Objects', serialized) self._test_data_in_section('ObjectsCopy', serialized) def test_actions(self): """Test that information on actions can be invoked Test that information on actions can be invoked on each MuranoPL object are persisted into object header ('?' key) during serialization of Objects section of Object Model """ serialized = self._runner.serialized_model actions = serialized['Objects']['?'].get('_actions') self.assertIsInstance(actions, dict) action_names = [action['name'] for action in actions.values()] self.assertIn('testAction', action_names) self.assertNotIn('notAction', action_names) self.assertIn('testRootMethod', action_names) action_meta = None for action in actions.values(): self.assertIsInstance(action.get('enabled'), bool) self.assertIsInstance(action.get('name'), str) self.assertThat( action['name'], matchers.StartsWith('test')) if action['name'] == 'testActionMeta': action_meta = action else: self.assertEqual(action['title'], action['name']) self.assertIsNotNone(action_meta) self.assertEqual(action_meta['title'], "Title of the method") self.assertEqual(action_meta['description'], "Description of the method") self.assertEqual(action_meta['helpText'], "HelpText of the method") def test_attribute_serialization(self): """Test that attributes produced by MuranoPL code are persisted Test that attributes produced by MuranoPL code are persisted in dedicated section of Object Model. Attributes are values that are stored in special key-value storage that is private to each class. Classes can store state data there. Attributes are persisted across deployment sessions but are not exposed via API (thus cannot be accessed by dashboard) """ self._runner.on(self._class1).testAttributes('VALUE') serialized = self._runner.serialized_model self.assertIsInstance(serialized.get('Attributes'), list) self.assertThat( serialized['Attributes'], matchers.HasLength(1)) self.assertEqual( [self._class1.id, 'SampleClass1', 'att1', 'VALUE'], serialized['Attributes'][0]) def test_attribute_deserialization(self): """Test that attributes are available Test that attributes that are put into Attributes section of Object Model become available to appropriate MuranoPL classes """ serialized = self._runner.serialized_model serialized['Attributes'].append( [self._class1.id, 'SampleClass1', 'att2', ' Snow']) runner2 = self.new_runner(serialized) self.assertEqual( 'John Snow', runner2.on(self._class1).testAttributes('John')) def test_value_deserialization(self): """Test serialization of arbitrary values Test serialization of arbitrary values that can be returned from action methods """ runner = self.new_runner(self._class2) result = runner.testMethod() self.assertEqual( { 'key1': 'abc', 'key2': ['a', 'b', 'c'], 'key3': None, 'key4': False, 'key5': {'x': 'y'}, 'key6': [{'w': 'q'}] }, serializer.serialize(result, runner.executor)) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/dsl/test_schema_generation.py0000664000175000017500000002343000000000000024414 0ustar00zuulzuul00000000000000# 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 testtools import matchers from murano.dsl import schema_generator from murano.tests.unit.dsl.foundation import runner from murano.tests.unit.dsl.foundation import test_case class TestSchemaGeneration(test_case.DslTestCase): def setUp(self): super(TestSchemaGeneration, self).setUp() schema = schema_generator.generate_schema( self.package_loader, runner.TestContextManager({}), 'TestSchema') self._class_schema = schema.pop('') self._model_builders_schema = schema def test_general_structure(self): self.assertIn('$schema', self._class_schema) self.assertIn('type', self._class_schema) self.assertIn('properties', self._class_schema) self.assertEqual( 'http://json-schema.org/draft-04/schema#', self._class_schema['$schema']) self.assertEqual('object', self._class_schema['type']) def _test_simple_property(self, name_or_schema, types): if not isinstance(name_or_schema, dict): props = self._class_schema['properties'] self.assertIn(name_or_schema, props) schema = props[name_or_schema] else: schema = name_or_schema self.assertIn('type', schema) if isinstance(types, list): self.assertCountEqual(schema['type'], types) else: self.assertEqual(schema['type'], types) return schema def test_string_property(self): self._test_simple_property('stringProperty', ['null', 'string']) def test_not_null_string_property(self): self._test_simple_property('stringNotNullProperty', 'string') def test_int_property(self): self._test_simple_property('intProperty', ['null', 'integer']) def test_not_null_int_property(self): self._test_simple_property('intNotNullProperty', 'integer') def test_bool_property(self): self._test_simple_property('boolProperty', ['null', 'boolean']) def test_not_null_bool_property(self): self._test_simple_property('boolNotNullProperty', 'boolean') def test_class_property(self): schema = self._test_simple_property( 'classProperty', ['null', 'muranoObject']) self.assertEqual('SampleClass1', schema.get('muranoType')) def test_template_property(self): schema = self._test_simple_property( 'templateProperty', ['null', 'muranoObject']) self.assertEqual('SampleClass1', schema.get('muranoType')) self.assertTrue(schema.get('owned')) self.assertCountEqual( ['stringProperty'], schema.get('excludedProperties')) def test_default_property(self): schema = self._test_simple_property( 'defaultProperty', ['null', 'integer']) self.assertEqual(999, schema.get('default')) def test_list_property(self): schema = self._test_simple_property('listProperty', 'array') self.assertIn('items', schema) items = schema['items'] self._test_simple_property(items, 'string') def test_dict_property(self): schema = self._test_simple_property('dictProperty', 'object') self.assertIn('properties', schema) props = schema['properties'] self.assertIn('key1', props) self._test_simple_property(props['key1'], 'string') self.assertIn('key2', props) self._test_simple_property(props['key2'], 'string') self.assertIn('additionalProperties', schema) extra_props = schema['additionalProperties'] self._test_simple_property(extra_props, ['null', 'integer']) def test_complex_property(self): schema = self._test_simple_property('complexProperty', 'object') self.assertIn('properties', schema) self.assertEqual({}, schema['properties']) self.assertIn('additionalProperties', schema) extra_props = schema['additionalProperties'] self._test_simple_property(extra_props, 'array') self.assertIn('items', extra_props) items = extra_props['items'] self._test_simple_property(items, 'integer') def test_minimum_contract(self): schema = self._test_simple_property('minimumContract', 'integer') self.assertFalse(schema.get('exclusiveMinimum', True)) self.assertEqual(5, schema.get('minimum')) def test_maximum_contract(self): schema = self._test_simple_property('maximumContract', 'integer') self.assertTrue(schema.get('exclusiveMaximum', False)) self.assertEqual(15, schema.get('maximum')) def test_range_contract(self): schema = self._test_simple_property('rangeContract', 'integer') self.assertFalse(schema.get('exclusiveMaximum', True)) self.assertTrue(schema.get('exclusiveMinimum', False)) self.assertEqual(0, schema.get('minimum')) self.assertEqual(10, schema.get('maximum')) def test_chain_contract(self): schema = self._test_simple_property('chainContract', 'integer') self.assertFalse(schema.get('exclusiveMaximum', True)) self.assertTrue(schema.get('exclusiveMinimum', False)) self.assertEqual(0, schema.get('minimum')) self.assertEqual(10, schema.get('maximum')) def test_regex_contract(self): schema = self._test_simple_property('regexContract', 'string') self.assertEqual(r'\d+', schema.get('pattern')) def test_enum_contract(self): schema = self._test_simple_property('enumContract', 'string') self.assertEqual(['a', 'b'], schema.get('enum')) def test_enum_func_contract(self): schema = self._test_simple_property('enumFuncContract', 'string') self.assertEqual(['x', 'y'], schema.get('enum')) def test_ui_hints(self): schema = self._test_simple_property('decoratedProperty', 'string') self.assertEqual('Title!', schema.get('title')) self.assertEqual('Description!', schema.get('description')) self.assertEqual('Help!', schema.get('helpText')) self.assertFalse(schema.get('visible')) self.assertThat(schema.get('formIndex'), matchers.GreaterThan(-1)) self.assertEqual('mySection', schema.get('formSection')) sections = self._class_schema.get('formSections') self.assertIsInstance(sections, dict) section = sections.get('mySection') self.assertIsInstance(section, dict) self.assertThat(section.get('index'), matchers.GreaterThan(-1)) self.assertEqual('Section Title', section.get('title')) def test_model_builders(self): self.assertEqual(1, len(self._model_builders_schema)) schema = self._model_builders_schema.get('modelBuilder') self.assertIsInstance(schema, dict) self._class_schema = schema self.test_general_structure() self.assertEqual('Model Builder!', schema.get('title')) args = schema['properties'] self._test_simple_property(args.get('arg1'), 'string') self._test_simple_property(args.get('arg2'), 'integer') arg1 = args['arg1'] self.assertEqual('Arg1!', arg1.get('title')) def test_generate_schema_with_extra_params(self): schema = schema_generator.generate_schema( self.package_loader, runner.TestContextManager({}), 'TestSchema', method_names='modelBuilder', package_name='tests') expected_schema = { '$schema': 'http://json-schema.org/draft-04/schema#', 'additionalProperties': False, 'formSections': {}, 'properties': {'arg1': {'title': 'Arg1!', 'type': 'string'}, 'arg2': {'type': 'integer'}}, 'title': 'Model Builder!', 'type': 'object' } self.assertIn('modelBuilder', schema) self.assertEqual(expected_schema, schema['modelBuilder']) def test_translate_list(self): contract = [1, 2, 3] result = schema_generator.translate_list(contract, None, None) self.assertEqual({'type': 'array'}, result) contract = [1, 2, 3, {'foo': 'bar'}] result = schema_generator.translate_list(contract, None, None) expected = { 'items': {'additionalProperties': False, 'properties': {'foo': None}, 'type': 'object'}, 'type': 'array' } self.assertEqual(sorted(expected.keys()), sorted(result.keys())) for key, val in expected.items(): self.assertEqual(val, result[key]) contract = [1, 2, 3, {'foo': 'bar'}, ['baz']] result = schema_generator.translate_list(contract, None, None) expected = { 'additionalItems': {'items': None, 'type': 'array'}, 'items': [{'additionalProperties': False, 'properties': {'foo': None}, 'type': 'object'}, {'items': None, 'type': 'array'}], 'type': 'array' } self.assertEqual(sorted(expected.keys()), sorted(result.keys())) for key, val in expected.items(): if isinstance(val, dict): for key_, val_ in val.items(): self.assertEqual(val_, val[key_]) else: self.assertEqual(val, result[key]) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/dsl/test_session_local_storage.py0000664000175000017500000000734600000000000025332 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from unittest import mock from murano.dsl import helpers from murano.dsl import session_local_storage from murano.tests.unit.dsl.foundation import test_case class FakeWithInit(session_local_storage._localbase): def __init__(self, *args, **kwargs): pass class FakeNoInit(session_local_storage._localbase): pass class TestLocalbase(test_case.DslTestCase): def test_new(self): lb = FakeWithInit.__new__( FakeWithInit, 42, 'foo', bar='baz') self.assertEqual(((42, 'foo',), {'bar': 'baz'}), lb._local__args) def test_new_bad(self): self.assertRaises( TypeError, FakeNoInit.__name__) class TestPatch(test_case.DslTestCase): @mock.patch.object(helpers, 'get_execution_session', return_value=mock.sentinel.session) def test_patch(self, mock_ges): fwi = FakeWithInit(foo='bar') session_local_storage._patch(fwi) self.assertEqual({}, fwi.__dict__) class TestLocal(test_case.DslTestCase): class FakeLocal(session_local_storage._local): def __init__(self, foo): self.foo = foo @mock.patch.object(session_local_storage, '_patch') def setUp(self, mock_patch): super(TestLocal, self).setUp() self.fl = self.FakeLocal('bar') @mock.patch.object(session_local_storage, '_patch') def test_getattribute(self, mock_patch): self.assertEqual('bar', self.fl.foo) mock_patch.assert_called_with(self.fl) @mock.patch.object(session_local_storage, '_patch') def test_setattribute(self, mock_patch): self.fl.foo = 'baz' mock_patch.assert_called_with(self.fl) @mock.patch.object(session_local_storage, '_patch') def test_delattribute(self, mock_patch): del self.fl.foo mock_patch.assert_called_with(self.fl) class TestSessionLocalDict(test_case.DslTestCase): def setUp(self): super(TestSessionLocalDict, self).setUp() self.sld = session_local_storage.SessionLocalDict(foo='bar') @mock.patch.object(helpers, 'get_execution_session', return_value=None) def test_data_no_session(self, mock_ges): self.assertEqual({'foo': 'bar'}, self.sld.data) self.sld.data = {'foo': 'baz'} mock_ges.assert_called_with() self.assertEqual({'foo': 'baz'}, self.sld.data) @mock.patch.object(helpers, 'get_execution_session', return_value=mock.sentinel.session) def test_data(self, mock_ges): self.sld.data = mock.sentinel.data mock_ges.assert_called_with() self.assertEqual(mock.sentinel.data, self.sld.data) class TestExecutionSessionMemoize(test_case.DslTestCase): @mock.patch.object(helpers, 'get_memoize_func', return_value=mock.sentinel.mem_func) def test_execution_session_memoize(self, mock_gef): f = mock.MagicMock() f.return_value = 'im a function' new_f = session_local_storage.execution_session_memoize(f) self.assertEqual(f, mock_gef.call_args[0][0]) self.assertIsInstance(mock_gef.call_args[0][1], session_local_storage.SessionLocalDict) self.assertEqual(mock.sentinel.mem_func, new_f) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/dsl/test_single_inheritance.py0000664000175000017500000000242100000000000024570 0ustar00zuulzuul00000000000000# 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 murano.tests.unit.dsl.foundation import object_model as om from murano.tests.unit.dsl.foundation import test_case class TestSingleInheritance(test_case.DslTestCase): def setUp(self): super(TestSingleInheritance, self).setUp() model = om.Object( 'SingleInheritanceChild' ) self._runner = self.new_runner(model) def test_virtual_calls(self): self._runner.testVirtualCalls() self.assertEqual( ['SingleInheritanceChild::method1', 'SingleInheritanceParent::method1', 'SingleInheritanceChild::method2', 'SingleInheritanceParent::method2'], self.traces) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/dsl/test_statics.py0000664000175000017500000001350000000000000022410 0ustar00zuulzuul00000000000000# 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 yaql.language import exceptions as yaql_exceptions from yaql.language import specs from yaql.language import yaqltypes from murano.dsl import dsl from murano.dsl import dsl_types from murano.dsl import exceptions from murano.tests.unit.dsl.foundation import object_model as om from murano.tests.unit.dsl.foundation import test_case class TestStatics(test_case.DslTestCase): def setUp(self): @dsl.name('test.TestStatics') class PythonClass(object): @staticmethod @specs.parameter('arg', yaqltypes.Integer()) @specs.inject('receiver', yaqltypes.Receiver()) def static_python_method(arg, receiver): if isinstance(receiver, dsl_types.MuranoObjectInterface): return 3 * arg return 7 * arg @classmethod @specs.inject('receiver', yaqltypes.Receiver()) def classmethod_python_method(cls, arg, receiver): if isinstance(receiver, dsl_types.MuranoObjectInterface): return cls.__name__.upper() + str(arg) return cls.__name__ + str(arg) super(TestStatics, self).setUp() self.package_loader.load_class_package( 'test.TestStatics', None).register_class(PythonClass) self._runner = self.new_runner( om.Object('test.TestStatics', staticProperty2='INVALID')) def test_call_static_method_on_object(self): self.assertEqual(123, self._runner.testCallStaticMethodOnObject()) def test_call_static_method_on_class_name(self): self.assertEqual(123, self._runner.testCallStaticMethodOnClassName()) def test_call_static_method_on_class_name_with_ns(self): self.assertEqual( 678, self._runner.testCallStaticMethodOnClassNameWithNs()) def test_call_static_method_from_another_method(self): self.assertEqual( 123 * 5, self._runner.testCallStaticMethodFromAnotherMethod()) def test_static_this(self): self.assertIsInstance( self._runner.testStaticThis(), dsl_types.MuranoTypeReference) def test_no_access_to_instance_properties(self): self.assertRaises( exceptions.NoPropertyFound, self._runner.testNoAccessToInstanceProperties) def test_access_static_property_from_instance_method(self): self.assertEqual( 'xxx', self._runner.testAccessStaticPropertyFromInstanceMethod()) def test_access_static_property_from_static_method(self): self.assertEqual( 'xxx', self._runner.testAccessStaticPropertyFromStaticMethod()) def test_modify_static_property_using_dollar(self): self.assertEqual( 'qq', self._runner.testModifyStaticPropertyUsingDollar()) def test_modify_static_property_using_this(self): self.assertEqual( 'qq', self._runner.testModifyStaticPropertyUsingThis()) def test_modify_static_property_using_class_name(self): self.assertEqual( 'qq', self._runner.testModifyStaticPropertyUsingClassName()) def test_modify_static_property_using_ns_class_name(self): self.assertEqual( 'qq', self._runner.testModifyStaticPropertyUsingNsClassName()) def test_modify_static_property_using_type_func(self): self.assertEqual( 'qq', self._runner.testModifyStaticPropertyUsingTypeFunc()) def test_modify_static_dict_property(self): self.assertEqual( {'key': 'value'}, self._runner.testModifyStaticDictProperty()) def test_property_is_static(self): self.assertEqual('qq', self._runner.testPropertyIsStatic()) def test_static_properties_excluded_from_object_model(self): self.assertEqual( 'staticProperty', self._runner.testStaticPropertisNotLoaded()) def test_type_is_singleton(self): self.assertTrue(self._runner.testTypeIsSingleton()) def test_static_property_inheritance(self): self.assertEqual( 'baseStaticProperty' * 3, self._runner.testStaticPropertyInheritance()) def test_static_property_override(self): self.assertEqual( [ 'conflictingStaticProperty-child', 'conflictingStaticProperty-child', 'conflictingStaticProperty-base', 'conflictingStaticProperty-child', 'conflictingStaticProperty-base' ], self._runner.testStaticPropertyOverride()) def test_type_info_of_type(self): self.assertTrue(self._runner.testTypeinfoOfType()) def test_call_python_static_method(self): self.assertEqual( [333] + [777] * 3, self._runner.testCallPythonStaticMethod()) def test_call_python_classmethod(self): self.assertEqual( ['PYTHONCLASS!'] + ['PythonClass!'] * 3, self._runner.testCallPythonClassMethod()) def test_call_static_method_on_invalid_class(self): self.assertRaises( yaql_exceptions.NoMatchingMethodException, self._runner.testCallStaticMethodOnInvalidClass) def test_static_method_callable_from_python(self): self.assertEqual( 'It works!', self._runner.on_class('test.TestStatics').testStaticAction()) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/dsl/test_unicode.py0000664000175000017500000000311400000000000022364 0ustar00zuulzuul00000000000000# coding: utf-8 # 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 murano.dsl import dsl_exception from murano.tests.unit.dsl.foundation import object_model as om from murano.tests.unit.dsl.foundation import test_case class TestUnicode(test_case.DslTestCase): def setUp(self): super(TestUnicode, self).setUp() self._runner = self.new_runner(om.Object('TestUnicode')) def test_literal(self): self.assertEqual( u"солнце ♥ φεγγάρι", self._runner.testLiteral()) def test_expression(self): self.assertEqual( u"СОЛНЦЕ ♥ ΦΕΓΓΆΡΙ", self._runner.testExpression()) def test_parameter(self): self.assertEqual( u"СОЛНЦЕ ♥ ΦΕΓΓΆΡΙ", self._runner.testParameter()) def test_exception(self): x = self.assertRaises( dsl_exception.MuranoPlException, self._runner.testException) self.assertEqual(u"солнце ♥ φεγγάρι", x.message) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/dsl/test_varkwargs.py0000664000175000017500000000422000000000000022744 0ustar00zuulzuul00000000000000# 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 yaql.language import exceptions as yaql_exceptions from murano.dsl import exceptions as dsl_exceptions from murano.tests.unit.dsl.foundation import object_model as om from murano.tests.unit.dsl.foundation import test_case class TestVarKwArgs(test_case.DslTestCase): def setUp(self): super(TestVarKwArgs, self).setUp() self._runner = self.new_runner(om.Object('TestVarKwArgs')) def test_varargs(self): self.assertEqual([2, 3, 4], self._runner.testVarArgs()) def test_kwargs(self): self.assertEqual({'arg2': 2, 'arg3': 3}, self._runner.testKwArgs()) def test_duplicate_kwargs(self): self.assertRaises( yaql_exceptions.NoMatchingMethodException, self._runner.testDuplicateKwArgs) def test_duplicate_varargs(self): self.assertRaises( yaql_exceptions.NoMatchingMethodException, self._runner.testDuplicateVarArgs) def test_explicit_varargs(self): self.assertRaises( yaql_exceptions.NoMatchingMethodException, self._runner.testExplicitVarArgs) def test_args(self): self.assertEqual( [[1, 2, 3], {'arg1': 4, 'arg2': 5, 'arg3': 6}], self._runner.testArgs()) def test_varargs_contract(self): self.assertRaises( dsl_exceptions.ContractViolationException, self._runner.testVarArgsContract) def test_kwargs_contract(self): self.assertRaises( dsl_exceptions.ContractViolationException, self._runner.testKwArgsContract) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/dsl/test_versioning.py0000664000175000017500000000371200000000000023125 0ustar00zuulzuul00000000000000# 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 semantic_version from unittest import mock from murano.tests.unit.dsl.foundation import object_model as om from murano.tests.unit.dsl.foundation import test_case class TestVersioning(test_case.DslTestCase): def test_provided_version(self): version = '1.11.111' sem_version = semantic_version.Spec('==' + version) model = om.Object('Empty', class_version=version) m = mock.MagicMock(return_value=self._package_loader._package) self._package_loader.load_class_package = m self.new_runner(model) m.assert_called_once_with('Empty', sem_version) def test_empty_provided_version(self): version = '' sem_version = semantic_version.Spec('>=0.0.0', '<1.0.0') model = om.Object('Empty', class_version=version) m = mock.MagicMock(return_value=self._package_loader._package) self._package_loader.load_class_package = m self.new_runner(model) m.assert_called_once_with('Empty', sem_version) def test_several_in_row(self): version = '>3.0.0,<=4.1' sem_version = semantic_version.Spec('>3.0.0', '<4.2.0') model = om.Object('Empty', class_version=version) m = mock.MagicMock(return_value=self._package_loader._package) self._package_loader.load_class_package = m self.new_runner(model) m.assert_called_once_with('Empty', sem_version) ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1696417899.841181 murano-16.0.0/murano/tests/unit/engine/0000775000175000017500000000000000000000000020011 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/engine/__init__.py0000664000175000017500000000000000000000000022110 0ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1696417899.845181 murano-16.0.0/murano/tests/unit/engine/meta/0000775000175000017500000000000000000000000020737 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1696417899.845181 murano-16.0.0/murano/tests/unit/engine/meta/Classes/0000775000175000017500000000000000000000000022334 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/engine/meta/Classes/Mytest.yaml0000664000175000017500000000012400000000000024502 0ustar00zuulzuul00000000000000Namespaces: =: io.murano.test Extends: sys:TestFixture Name: MyTest Methods: ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/engine/meta/TestMock.yaml0000664000175000017500000000441300000000000023356 0ustar00zuulzuul00000000000000Namespaces: test: io.murano.test Name: TestMocks Extends: test:TestFixture Properties: logMessage: Contract: $.string() Default: 'Mock from property' Methods: initialize: Body: - $.originalClass: new(TestMocksFixture) mock1: Body: - Return: 'This is mock1' testInjectMethodWithString: Body: - inject(TestMocksFixture, simpleMethod1, $this, mock1) - $output: $.originalClass.simpleMethod1() - $.assertEqual('This is mock1', $output) testInjectObjectWithString: Body: - inject($.originalClass, simpleMethod1, $this, mock1) - $output: $.originalClass.simpleMethod1() - $.assertEqual('This is mock1', $output) testInjectMethodWithYaqlExpr: Body: # Calling original method without mocking - $output: $.originalClass.simpleMethod1() - $.assertEqual('method1', $output) - $mockText: 'I am mock' - inject(TestMocksFixture, simpleMethod1, $mockText) # Calling original method after mocking - $output: $.originalClass.simpleMethod1() - $.assertEqual('I am mock', $output) testInjectMethodWithYaqlExpr2: Body: # Calling original method without mocking - $output: $.originalClass.simpleMethod1() - $.assertEqual('method1', $output) - inject(TestMocksFixture, simpleMethod1, $.logMessage) # Calling mocked method - $output: $.originalClass.simpleMethod1() - $.assertEqual('Mock from property', $output) testInjectObjectWithYaqlExpr: Body: # Calling original method without mocking - $output: $.originalClass.simpleMethod1() - $.assertEqual('method1', $output) - $mockText: 'I am mock' - inject($.originalClass, simpleMethod1, $mockText) # Calling original method after mocking - $output: $.originalClass.simpleMethod1() - $.assertEqual('I am mock', $output) testWithoriginal: Body: - inject(TestMocksFixture, simpleMethod1, withOriginal(t => $.originalClass.someProperty) -> $t) - $output: $.originalClass.simpleMethod1() - $.assertEqual(DEFAULT, $output) testOriginalMethod: Body: - inject(TestMocksFixture, simpleMethod1, originalMethod()) - $output: $.originalClass.simpleMethod1() - $.assertEqual('method1', $output)././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/engine/meta/TestMockFixture.yaml0000664000175000017500000000025300000000000024723 0ustar00zuulzuul00000000000000Name: TestMocksFixture Properties: someProperty: Contract: $.string().notNull() Default: DEFAULT Methods: simpleMethod1: Body: - Return: 'method1' ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/engine/meta/manifest.yaml0000664000175000017500000000031600000000000023431 0ustar00zuulzuul00000000000000Format: 1.0 Type: Application FullName: io.murano.test.MyTest Name: Test case Example Description: | Example of simple package Author: 'Mirantis, Inc' Tags: [] Classes: io.murano.test.MyTest: Mytest.yaml ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1696417899.845181 murano-16.0.0/murano/tests/unit/engine/system/0000775000175000017500000000000000000000000021335 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/engine/system/__init__.py0000664000175000017500000000000000000000000023434 0ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1696417899.845181 murano-16.0.0/murano/tests/unit/engine/system/execution_plans/0000775000175000017500000000000000000000000024535 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/engine/system/execution_plans/DeployTelnet.template0000664000175000017500000000052700000000000030706 0ustar00zuulzuul00000000000000FormatVersion: 2.0.0 Version: 1.0.0 Name: Deploy Telnet Parameters: appName: $appName Body: | return deploy(args.appName).stdout Scripts: deploy: Type: Application Version: 1.0.0 EntryPoint: deployTelnet.sh Files: - installer.sh - common.sh Options: captureStdout: true captureStderr: true ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/engine/system/execution_plans/DeployTomcat.template0000664000175000017500000000044200000000000030676 0ustar00zuulzuul00000000000000FormatVersion: 2.0.0 Version: 1.0.0 Name: Deploy Tomcat Parameters: appName: $appName Body: | deploy(args.appName) Scripts: deploy: Type: Application Version: 1.0.0 EntryPoint: deployTomcat.sh Files: [] Options: captureStdout: false captureStderr: true././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/engine/system/execution_plans/application.template0000664000175000017500000000054600000000000030602 0ustar00zuulzuul00000000000000FormatVersion: 2.0.0 Version: 1.0.0 Name: Deploy Tomcat Parameters: appName: $appName Body: | return deploy(args.appName).stdout Scripts: deploy: Type: Application Version: 1.0.0 EntryPoint: deployTomcat.sh Files: - installer: - Options: captureStdout: true captureStderr: true ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/engine/system/execution_plans/application_without_files.template0000664000175000017500000000046200000000000033544 0ustar00zuulzuul00000000000000FormatVersion: 2.0.0 Version: 1.0.0 Name: Deploy Tomcat Parameters: appName: $appName Body: | return deploy(args.appName).stdout Scripts: deploy: Type: Application Version: 1.0.0 EntryPoint: deployTomcat.sh Files: [] Options: captureStdout: true captureStderr: true././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/engine/system/execution_plans/chef.template0000664000175000017500000000056600000000000027206 0ustar00zuulzuul00000000000000FormatVersion: 2.0.0 Version: 1.0.0 Name: Deploy Chef Parameters: appName: $appName Body: | return deploy(args.appName).stdout Scripts: deploy: Type: Chef Version: 1.0.0 EntryPoint: cookbook/recipe Files: - https://github.com/tomcat.git - java: https://github.com/java.git Options: captureStdout: true captureStderr: true././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/engine/system/execution_plans/template_with_files.template0000664000175000017500000000063000000000000032321 0ustar00zuulzuul00000000000000FormatVersion: 2.0.0 Version: 1.0.0 Name: Deploy Tomcat Parameters: appName: $appName Body: | return deploy(args.appName).stdout Files: updateScript: BodyType: Text Name: updateScript Body: text Scripts: deploy: Type: Application Version: 1.0.0 EntryPoint: deployTomcat.sh Files: - updateScript Options: captureStdout: true captureStderr: true ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/engine/system/test_agent.py0000664000175000017500000006327600000000000024062 0ustar00zuulzuul00000000000000# Copyright (c) 2015 Telefonica I+D # 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 copy import datetime import json import os import tempfile from unittest import mock from oslo_serialization import base64 import yaml as yamllib from murano.common import exceptions from murano.dsl import dsl from murano.dsl import murano_object from murano.dsl import murano_type from murano.dsl import object_store from murano.engine.system import agent from murano.engine.system import resource_manager from murano.tests.unit import base class TestAgent(base.MuranoTestCase): def setUp(self): super(TestAgent, self).setUp() if hasattr(yamllib, 'CSafeLoader'): self.yaml_loader = yamllib.CSafeLoader else: self.yaml_loader = yamllib.SafeLoader self.override_config('disable_murano_agent', False, group='engine') self.override_config('signing_key', False, group='engine') mock_host = mock.MagicMock() mock_host.id = '1234' mock_host.find_owner = lambda *args, **kwargs: mock_host mock_host().getRegion.return_value = mock.MagicMock( __class__=dsl.MuranoObjectInterface) self.rabbit_mq_settings = { 'agentRabbitMq': {'login': 'test_login', 'password': 'test_password', 'host': 'test_host', 'port': 123, 'virtual_host': 'test_virtual_host'} } mock_host().getRegion()().getConfig.return_value =\ self.rabbit_mq_settings self.agent = agent.Agent(mock_host) self.resources = mock.Mock(spec=resource_manager.ResourceManager) self.resources.string.return_value = 'text' self.addCleanup(mock.patch.stopall) def _read(self, path): execution_plan_dir = os.path.abspath( os.path.join(__file__, '../execution_plans/') ) with open(execution_plan_dir + "/" + path) as file: return file.read() @mock.patch('murano.common.messaging.mqclient.kombu') def test_send_creates_queue(self, mock_kombu): self.agent.send_raw({}) # Verify that MQClient was instantiated, by checking whether # kombu.Connection was called. mock_kombu.Connection.assert_called_with( 'amqp://{login}:{password}@{host}:{port}/{virtual_host}'.format( **self.rabbit_mq_settings['agentRabbitMq'] ), ssl=None) # Verify that client.declare() was called by checking whether kombu # functions were called. self.assertEqual(1, mock_kombu.Exchange.call_count) self.assertEqual(1, mock_kombu.Queue.call_count) @mock.patch('murano.engine.system.agent.LOG') def test_send_with_murano_agent_disabled(self, mock_log): self.override_config('disable_murano_agent', True, group='engine') self.assertRaises(exceptions.PolicyViolationException, self.agent.send_raw, {}) @mock.patch('murano.engine.system.agent.Agent._sign') @mock.patch('murano.common.messaging.mqclient.kombu') def test_send(self, mock_kombu, mock_sign): template = yamllib.load( self._read('template_with_files.template'), Loader=self.yaml_loader) self.agent._queue = 'test_queue' mock_sign.return_value = 'SIGNATURE' plan = self.agent.build_execution_plan(template, self.resources) with mock.patch.object(self.agent, 'build_execution_plan', return_value=plan): self.agent.send(template, self.resources) self.assertEqual(1, mock_kombu.Producer.call_count) mock_kombu.Producer().publish.assert_called_once_with( exchange='', routing_key='test_queue', body=json.dumps(plan), message_id=plan['ID'], headers={'signature': 'SIGNATURE'} ) @mock.patch('murano.engine.system.agent.eventlet.event.Event') @mock.patch('murano.common.messaging.mqclient.kombu') def test_is_ready(self, mock_kombu, mock_event): v2_result = yamllib.load( self._read('application.template'), Loader=self.yaml_loader) mock_event().wait.side_effect = None mock_event().wait.return_value = v2_result self.assertTrue(self.agent.is_ready(1)) mock_event().wait.side_effect = agent.eventlet.Timeout self.assertFalse(self.agent.is_ready(1)) @mock.patch('murano.engine.system.agent.Agent._sign') @mock.patch('murano.engine.system.agent.eventlet.event.Event') @mock.patch('murano.common.messaging.mqclient.kombu') def test_call_with_v1_result(self, mock_kombu, mock_event, mock_sign): template = yamllib.load( self._read('template_with_files.template'), Loader=self.yaml_loader) test_v1_result = { 'FormatVersion': '1.0.0', 'IsException': False, 'Result': [ { 'IsException': False, 'Result': 'test_result' } ] } mock_event().wait.side_effect = None mock_event().wait.return_value = test_v1_result mock_sign.return_value = 'SIGNATURE' self.agent._queue = 'test_queue' plan = self.agent.build_execution_plan(template, self.resources) mock.patch.object( self.agent, 'build_execution_plan', return_value=plan).start() result = self.agent.call(template, self.resources, None) self.assertIsNotNone(result) self.assertEqual('test_result', result) self.assertEqual(1, mock_kombu.Producer.call_count) mock_kombu.Producer().publish.assert_called_once_with( exchange='', routing_key='test_queue', body=json.dumps(plan), message_id=plan['ID'], headers={'signature': 'SIGNATURE'} ) @mock.patch('murano.engine.system.agent.Agent._sign') @mock.patch('murano.engine.system.agent.eventlet.event.Event') @mock.patch('murano.common.messaging.mqclient.kombu') def test_call_with_v2_result(self, mock_kombu, mock_event, mock_sign): template = yamllib.load( self._read('template_with_files.template'), Loader=self.yaml_loader) v2_result = yamllib.load( self._read('application.template'), Loader=self.yaml_loader) mock_event().wait.side_effect = None mock_event().wait.return_value = v2_result mock_sign.return_value = 'SIGNATURE' self.agent._queue = 'test_queue' plan = self.agent.build_execution_plan(template, self.resources) mock.patch.object( self.agent, 'build_execution_plan', return_value=plan).start() result = self.agent.call(template, self.resources, None) self.assertIsNotNone(result) self.assertEqual(v2_result['Body'], result) self.assertEqual(1, mock_kombu.Producer.call_count) mock_kombu.Producer().publish.assert_called_once_with( exchange='', routing_key='test_queue', body=json.dumps(plan), message_id=plan['ID'], headers={'signature': 'SIGNATURE'} ) @mock.patch('murano.engine.system.agent.eventlet.event.Event') @mock.patch('murano.common.messaging.mqclient.kombu') def test_call_with_no_result(self, mock_kombu, mock_event): template = yamllib.load( self._read('template_with_files.template'), Loader=self.yaml_loader) mock_event().wait.side_effect = None mock_event().wait.return_value = None result = self.agent.call(template, self.resources, None) self.assertIsNone(result) @mock.patch('murano.engine.system.agent.eventlet.event.Event') @mock.patch('murano.common.messaging.mqclient.kombu') def test_call_except_timeout(self, mock_kombu, mock_event): self.override_config('agent_timeout', 1, group='engine') mock_event().wait.side_effect = agent.eventlet.Timeout template = yamllib.load( self._read('template_with_files.template'), Loader=self.yaml_loader) expected_error_msg = 'The murano-agent did not respond within 1 '\ 'seconds' with self.assertRaisesRegex(exceptions.TimeoutException, expected_error_msg): self.agent.call(template, self.resources, None) @mock.patch('murano.engine.system.agent.datetime') def test_process_v1_result_with_error_code(self, mock_datetime): now = datetime.datetime.now().isoformat() mock_datetime.datetime.now().isoformat.return_value = now v1_result = { 'IsException': True, 'Result': [ 'Error Type', 'Error Message', 'Error Command', 'Error Details' ] } expected_error = { 'source': 'execution_plan', 'command': 'Error Command', 'details': 'Error Details', 'message': 'Error Message', 'type': 'Error Type', 'timestamp': now } self.assertTrue(self._are_exceptions_equal( agent.AgentException, expected_error, self.agent._process_v1_result, v1_result)) v1_result = { 'IsException': False, 'Result': [ 'Error Type', 'Error Message', 'Error Command', 'Error Details', { 'IsException': True, 'Result': [ 'Nested Error Type', 'Nested Error Message', 'Nested Error Command', 'Nested Error Details' ] } ] } expected_error = { 'source': 'command', 'command': 'Nested Error Command', 'details': 'Nested Error Details', 'message': 'Nested Error Message', 'type': 'Nested Error Type', 'timestamp': now } self.assertTrue(self._are_exceptions_equal( agent.AgentException, expected_error, self.agent._process_v1_result, v1_result)) def test_process_v2_result_with_error_code(self): v2_result = { 'Body': { 'Message': 'Test Error Message', 'AdditionalInfo': 'Test Additional Info', 'ExtraAttr': 'Test extra data' }, 'FormatVersion': '2.0.0', 'Name': 'TestApp', 'ErrorCode': 123, 'Time': 'Right now' } expected_error = { 'errorCode': 123, 'message': 'Test Error Message', 'details': 'Test Additional Info', 'time': 'Right now', 'extra': {'ExtraAttr': 'Test extra data'} } self.assertTrue(self._are_exceptions_equal( agent.AgentException, expected_error, self.agent._process_v2_result, v2_result)) def _are_exceptions_equal(self, exception, expected_error, function, result): """Checks whether expected and returned dict from exception are equal. Because casting dicts to strings changes the ordering of the keys, manual comparison of the result and expected result is performed. """ try: # deepcopy must be performed because _process_v1_result # deletes attrs from the original dict passed in. self.assertRaises(exception, function, copy.deepcopy(result)) function(result) except exception as e: e_string = str(e).replace("'", "\"").replace('None', 'null') e_dict = json.loads(e_string) self.assertEqual(sorted(expected_error.keys()), sorted(e_dict.keys())) for key, val in expected_error.items(): self.assertEqual(val, e_dict[key]) except Exception: return False return True class TestExecutionPlan(base.MuranoTestCase): def setUp(self): super(TestExecutionPlan, self).setUp() if hasattr(yamllib, 'CSafeLoader'): self.yaml_loader = yamllib.CSafeLoader else: self.yaml_loader = yamllib.SafeLoader self.override_config('signing_key', False, group='engine') self.mock_murano_class = mock.Mock(spec=murano_type.MuranoClass) self.mock_murano_class.name = 'io.murano.system.Agent' self.mock_murano_class.declared_parents = [] self.mock_object_store = mock.Mock(spec=object_store.ObjectStore) object_interface = mock.Mock(spec=murano_object.MuranoObject) object_interface.id = '1234' object_interface.find_owner = lambda *args, **kwargs: object_interface self.agent = agent.Agent(object_interface) self.resources = mock.Mock(spec=resource_manager.ResourceManager) self.resources.string.return_value = 'text' self.uuids = ['ID1', 'ID2', 'ID3', 'ID4'] self.mock_uuid = self._stub_uuid(self.uuids) time_mock = mock.patch('time.time').start() time_mock.return_value = 2 self.addCleanup(mock.patch.stopall) def _read(self, path): execution_plan_dir = os.path.abspath( os.path.join(__file__, '../execution_plans/') ) with open(execution_plan_dir + "/" + path) as file: return file.read() def test_execution_plan_v2_application_type(self): template = yamllib.load( self._read('application.template'), Loader=self.yaml_loader) template = self.agent.build_execution_plan(template, self.resources) self.assertEqual(self._get_application(), template) def test_execution_plan_v2_chef_type(self): template = yamllib.load( self._read('chef.template'), Loader=self.yaml_loader) template = self.agent.build_execution_plan(template, self.resources) self.assertEqual(self._get_chef(), template) def test_execution_plan_v2_telnet_application(self): template = yamllib.load( self._read('DeployTelnet.template'), Loader=self.yaml_loader) template = self.agent.build_execution_plan(template, self.resources) self.assertEqual(self._get_telnet_application(), template) def test_execution_plan_v2_tomcat_application(self): template = yamllib.load( self._read('DeployTomcat.template'), Loader=self.yaml_loader) template = self.agent.build_execution_plan(template, self.resources) def test_execution_plan_v2_app_without_files(self): template = yamllib.load( self._read('application_without_files.template'), Loader=self.yaml_loader) template = self.agent.build_execution_plan(template, self.resources) self.assertEqual(self._get_app_without_files(), template) def test_execution_plan_v2_app_with_file_in_template(self): template = yamllib.load( self._read('template_with_files.template'), Loader=self.yaml_loader) template = self.agent.build_execution_plan(template, self.resources) self.assertEqual(self._get_app_with_files_in_template(), template) def _get_application(self): return { 'Action': 'Execute', 'Body': 'return deploy(args.appName).stdout\n', 'Files': { self.uuids[1]: { 'Body': 'text', 'BodyType': 'Text', 'Name': 'deployTomcat.sh' }, self.uuids[2]: { 'Body': 'dGV4dA==\n', 'BodyType': 'Base64', 'Name': 'installer' }, self.uuids[3]: { 'Body': 'dGV4dA==\n', 'BodyType': 'Base64', 'Name': 'common.sh' } }, 'FormatVersion': '2.0.0', 'ID': self.uuids[0], 'Stamp': 20000, 'Name': 'Deploy Tomcat', 'Parameters': { 'appName': '$appName' }, 'Scripts': { 'deploy': { 'EntryPoint': self.uuids[1], 'Files': [ self.uuids[2], self.uuids[3] ], 'Options': { 'captureStderr': True, 'captureStdout': True }, 'Type': 'Application', 'Version': '1.0.0' } }, 'Version': '1.0.0' } def _get_app_with_files_in_template(self): return { 'Action': 'Execute', 'Body': 'return deploy(args.appName).stdout\n', 'Files': { self.uuids[1]: { 'Body': 'text', 'BodyType': 'Text', 'Name': 'deployTomcat.sh' }, 'updateScript': { 'Body': 'text', 'BodyType': 'Text', 'Name': 'updateScript' }, }, 'FormatVersion': '2.0.0', 'ID': self.uuids[0], 'Stamp': 20000, 'Name': 'Deploy Tomcat', 'Parameters': { 'appName': '$appName' }, 'Scripts': { 'deploy': { 'EntryPoint': self.uuids[1], 'Files': [ 'updateScript' ], 'Options': { 'captureStderr': True, 'captureStdout': True }, 'Type': 'Application', 'Version': '1.0.0' } }, 'Version': '1.0.0' } def _get_app_without_files(self): return { 'Action': 'Execute', 'Body': 'return deploy(args.appName).stdout\n', 'Files': { self.uuids[1]: { 'Body': 'text', 'BodyType': 'Text', 'Name': 'deployTomcat.sh' }, }, 'FormatVersion': '2.0.0', 'ID': self.uuids[0], 'Stamp': 20000, 'Name': 'Deploy Tomcat', 'Parameters': { 'appName': '$appName' }, 'Scripts': { 'deploy': { 'EntryPoint': self.uuids[1], 'Files': [], 'Options': { 'captureStderr': True, 'captureStdout': True }, 'Type': 'Application', 'Version': '1.0.0' } }, 'Version': '1.0.0' } def _get_chef(self): return { 'Action': 'Execute', 'Body': 'return deploy(args.appName).stdout\n', 'Files': { self.uuids[1]: { 'Name': 'tomcat.git', 'Type': 'Downloadable', 'URL': 'https://github.com/tomcat.git' }, self.uuids[2]: { 'Name': 'java', 'Type': 'Downloadable', 'URL': 'https://github.com/java.git' }, }, 'FormatVersion': '2.0.0', 'ID': self.uuids[0], 'Stamp': 20000, 'Name': 'Deploy Chef', 'Parameters': { 'appName': '$appName' }, 'Scripts': { 'deploy': { 'EntryPoint': 'cookbook/recipe', 'Files': [ self.uuids[1], self.uuids[2] ], 'Options': { 'captureStderr': True, 'captureStdout': True }, 'Type': 'Chef', 'Version': '1.0.0' } }, 'Version': '1.0.0' } def _get_telnet_application(self): return { 'Action': 'Execute', 'Body': 'return deploy(args.appName).stdout\n', 'Files': { self.uuids[1]: { 'Body': 'text', 'BodyType': 'Text', 'Name': 'deployTelnet.sh' }, self.uuids[2]: { 'Body': 'text', 'BodyType': 'Text', 'Name': 'installer.sh' }, self.uuids[3]: { 'Body': 'text', 'BodyType': 'Text', 'Name': 'common.sh' } }, 'FormatVersion': '2.0.0', 'ID': self.uuids[0], 'Stamp': 20000, 'Name': 'Deploy Telnet', 'Parameters': { 'appName': '$appName' }, 'Scripts': { 'deploy': { 'EntryPoint': self.uuids[1], 'Files': [ self.uuids[2], self.uuids[3] ], 'Options': { 'captureStderr': True, 'captureStdout': True }, 'Type': 'Application', 'Version': '1.0.0' } }, 'Version': '1.0.0' } def _stub_uuid(self, values=None): class FakeUUID(object): def __init__(self, v): self.hex = v if values is None: values = [] mock_uuid4 = mock.patch('uuid.uuid4').start() mock_uuid4.side_effect = [FakeUUID(v) for v in values] return mock_uuid4 @mock.patch('murano.engine.system.resource_manager.ResourceManager' '._get_package') def test_file_line_endings(self, _get_package): class FakeResources(object): """Class with only string() method from ResourceManager class""" @staticmethod def string(name, owner=None, binary=False): return resource_manager.ResourceManager.string( receiver=None, name=name, owner=owner, binary=binary) # make path equal to provided name inside resources.string() package = mock.Mock() package.get_resource.side_effect = lambda m: m _get_package.return_value = package text = b"First line\nSecond line\rThird line\r\nFourth line" modified_text = u"First line\nSecond line\nThird line\nFourth line" encoded_text = base64.encode_as_text(text) + "\n" resources = FakeResources() with tempfile.NamedTemporaryFile() as script_file: script_file.write(text) script_file.file.flush() os.fsync(script_file.file.fileno()) # check that data has been written correctly script_file.seek(0) file_data = script_file.read() self.assertEqual(text, file_data) # check resources.string() output # text file result = resources.string(script_file.name) self.assertEqual(modified_text, result) # binary file result = resources.string(script_file.name, binary=True) self.assertEqual(text, result) # check _get_body() output filename = os.path.basename(script_file.name) folder = os.path.dirname(script_file.name) # text file body = self.agent._get_body(filename, resources, folder) self.assertEqual(modified_text, body) # binary file filename = '<{0}>'.format(filename) body = self.agent._get_body(filename, resources, folder) self.assertEqual(encoded_text, body) def test_queue_name(self): self.agent._queue = 'test_queue' self.assertEqual(self.agent.queue_name(), self.agent._queue) def test_prepare_message(self): template = {'test'} msg_id = 12345 msg = self.agent._prepare_message(template, msg_id) self.assertEqual(msg.id, msg_id) self.assertEqual(msg._body, template) def test_execution_plan_v1(self): template = yamllib.load( self._read('application.template'), Loader=self.yaml_loader) rtn_template = self.agent._build_v1_execution_plan(template, self.resources) self.assertEqual(template, rtn_template) def test_get_array_item(self): array = [1, 2, 3] index = 2 self.assertEqual(array[2], self.agent._get_array_item(array, index)) index = 3 self.assertIsNone(self.agent._get_array_item(array, index)) def test_execution_plan_error(self): template = None self.assertRaises(ValueError, self.agent.build_execution_plan, template, self.resources) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/engine/system/test_agent_listener.py0000664000175000017500000000365400000000000025761 0ustar00zuulzuul00000000000000# 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. from unittest import mock from murano.engine.system import agent_listener from murano.tests.unit import base class TestExecutionPlan(base.MuranoTestCase): def setUp(self): super(TestExecutionPlan, self).setUp() self.override_config("disable_murano_agent", False, "engine") self.agent = agent_listener.AgentListener("test") self.addCleanup(mock.patch.stopall) def test_agent_ready(self): self.assertEqual({}, self.agent._subscriptions) results_queue = str('-execution-results-test') self.assertEqual(results_queue, self.agent._results_queue) self.assertTrue(self.agent._enabled) self.assertIsNone(self.agent._receive_thread) def test_queue_name(self): self.assertEqual(self.agent._results_queue, self.agent.queue_name()) @mock.patch("murano.engine.system.agent_listener.dsl.get_this") @mock.patch("murano.engine.system." "agent_listener.dsl.get_execution_session") def test_subscribe_unsubscribe(self, execution_session, mock_this): self.agent.subscribe('msg_id', 'event') self.assertIn('msg_id', self.agent._subscriptions) self.agent.unsubscribe('msg_id') self.assertNotIn('msg_id', self.agent._subscriptions) self.assertTrue(execution_session.called) self.assertTrue(mock_this.called) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/engine/system/test_garbage_collector.py0000664000175000017500000000607300000000000026412 0ustar00zuulzuul00000000000000# 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 unittest import mock import weakref from testtools import matchers from murano.dsl import exceptions from murano.dsl import murano_method from murano.dsl import murano_object from murano.dsl import murano_type from murano.dsl.principal_objects import garbage_collector from murano.tests.unit import base class TestGarbageCollector(base.MuranoTestCase): def setUp(self): super(TestGarbageCollector, self).setUp() self.subscriber = mock.MagicMock(spec=murano_object.MuranoObject) self.subscriber.real_this = self.subscriber mock_class = mock.MagicMock(spec=murano_type.MuranoClass) mock_method = mock.MagicMock(spec=murano_method.MuranoMethod) mock_method.name = "mockHandler" mock_class.methods = mock.PropertyMock( return_value={"mockHandler": mock_method}) def find_single_method(name): if name != 'mockHandler': raise exceptions.NoMethodFound(name) mock_class.find_single_method = find_single_method self.subscriber.type = mock_class self.publisher = mock.MagicMock(spec=murano_object.MuranoObject) self.publisher.real_this = self.publisher def test_set_dd(self): self.publisher.destruction_dependencies = [] garbage_collector.GarbageCollector.subscribe_destruction( self.publisher, self.subscriber, handler="mockHandler") dep = self.publisher.destruction_dependencies self.assertThat(dep, matchers.HasLength(1)) dep = dep[0] self.assertEqual("mockHandler", dep["handler"]) self.assertEqual(self.subscriber, dep["subscriber"]()) def test_unset_dd(self): self.publisher.destruction_dependencies = [{ "subscriber": weakref.ref(self.subscriber), "handler": "mockHandler" }] garbage_collector.GarbageCollector.unsubscribe_destruction( self.publisher, self.subscriber, handler="mockHandler") self.assertEqual( [], self.publisher.destruction_dependencies) def test_set_wrong_handler(self): self.assertRaises( exceptions.NoMethodFound, garbage_collector.GarbageCollector.subscribe_destruction, self.publisher, self.subscriber, handler="invalidHandler") self.assertRaises( exceptions.NoMethodFound, garbage_collector.GarbageCollector.unsubscribe_destruction, self.publisher, self.subscriber, handler="invalidHandler") ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/engine/system/test_instance_reporter.py0000664000175000017500000000362000000000000026475 0ustar00zuulzuul00000000000000# 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. from unittest import mock from murano.db import models from murano.engine.system import instance_reporter from murano.tests.unit import base LATEST_VERSION = 1 class TestInstanceReporter(base.MuranoTestCase): def setUp(self): super(TestInstanceReporter, self).setUp() self.environment = models.Environment( name='test_environment', tenant_id='test_tenant_id', version=LATEST_VERSION ) self.addCleanup(mock.patch.stopall) @mock.patch("murano.db.models") def test_track_untrack_application(self, mock_models): instance = mock_models.Instance() self.i_r = instance_reporter.InstanceReportNotifier(self.environment) self.assertEqual(self.environment.id, self.i_r._environment_id) self.assertIsNone(self.i_r.track_application(instance)) self.assertIsNone(self.i_r.untrack_application(instance)) @mock.patch("murano.db.models") def test_track_untrack_cloud_instance(self, mock_models): instance = mock_models.Instance() self.i_r = instance_reporter.InstanceReportNotifier(self.environment) self.assertEqual(self.environment.id, self.i_r._environment_id) self.assertIsNone(self.i_r.track_cloud_instance(instance)) self.assertIsNone(self.i_r.untrack_cloud_instance(instance)) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/engine/system/test_metadef_browser.py0000664000175000017500000000416300000000000026122 0ustar00zuulzuul00000000000000# 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. from unittest import mock from murano.dsl import murano_object from murano.engine.system import metadef_browser from murano.tests.unit import base class TestMetadefBrowser(base.MuranoTestCase): def setUp(self): super(TestMetadefBrowser, self).setUp() self.this = mock.MagicMock() self.glance_client_mock = mock.MagicMock() metadef_browser.MetadefBrowser._get_client = mock.MagicMock( return_value=self.glance_client_mock) metadef_browser.MetadefBrowser._client = mock.MagicMock( return_value=self.glance_client_mock) self.mock_object = mock.Mock(spec=murano_object.MuranoObject) @mock.patch("murano.dsl.helpers.get_execution_session") def test_get_objects(self, execution_session): namespace = None self.metadef = metadef_browser.MetadefBrowser(self.this) self.assertIsNotNone(self.metadef.get_objects(namespace)) self.assertTrue(execution_session.called) self.assertTrue(metadef_browser.MetadefBrowser. _client.metadefs_object.list.called) @mock.patch("murano.dsl.helpers.get_execution_session") def test_get_namespaces(self, execution_session): self.metadef = metadef_browser.MetadefBrowser(self.this) resource_type = self.mock_object self.assertIsNotNone(self.metadef.get_namespaces(resource_type)) self.assertTrue(execution_session.called) self.assertTrue(metadef_browser.MetadefBrowser. _client.metadefs_namespace.list.called) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/engine/system/test_net_explorer.py0000664000175000017500000001063100000000000025455 0ustar00zuulzuul00000000000000# 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. from unittest import mock from oslo_config import cfg from murano.dsl import murano_method from murano.dsl import murano_type from murano.engine.system import net_explorer from murano.tests.unit import base CONF = cfg.CONF class TestNetExplorer(base.MuranoTestCase): def setUp(self): super(TestNetExplorer, self).setUp() self.mock_class = mock.MagicMock(spec=murano_type.MuranoClass) self.mock_method = mock.MagicMock(spec=murano_method.MuranoMethod) self._this = mock.MagicMock() self.region_name = "test-region" self.addCleanup(mock.patch.stopall) @mock.patch("murano.engine.system.net_explorer.nclient") @mock.patch("murano.engine.system.net_explorer.auth_utils") @mock.patch("murano.dsl.helpers.get_execution_session") def test_get_available_cidr(self, execution_session, mock_authentication, mock_nclient): ne = net_explorer.NetworkExplorer(self._this, self.region_name) router_id = 12 net_id = 144 self.assertIsNotNone(ne.get_available_cidr(router_id, net_id)) self.assertTrue(execution_session.called) @mock.patch("murano.engine.system.net_explorer.nclient") @mock.patch("murano.engine.system.net_explorer.auth_utils") @mock.patch("murano.dsl.helpers.get_execution_session") def test_list(self, execution_session, mock_authentication, mock_nclient): ne = net_explorer.NetworkExplorer(self._this, self.region_name) self.assertEqual(ne.list_networks(), ne._client.list_networks()['networks']) self.assertEqual(ne.list_subnetworks(), ne._client.list_subnets()['subnets']) self.assertEqual(ne.list_ports(), ne._client.list_ports()['ports']) self.assertEqual(ne.list_neutron_extensions(), ne._client.list_extensions()['extensions']) self.assertEqual(ne.get_default_dns(), ne._settings.default_dns) @mock.patch("murano.engine.system.net_explorer.nclient") @mock.patch("murano.engine.system.net_explorer.auth_utils") @mock.patch("murano.dsl.helpers.get_execution_session") def test_get_router_error(self, execution_session, mock_authentication, mock_nclient): ne = net_explorer.NetworkExplorer(self._this, self.region_name) self.assertRaises(KeyError, ne.get_default_router) @mock.patch("murano.engine.system.net_explorer.nclient") @mock.patch("murano.engine.system.net_explorer.auth_utils") @mock.patch("murano.dsl.helpers.get_execution_session") def test_get_ext_network_id_router(self, execution_session, mock_authentication, mock_nclient): ne = net_explorer.NetworkExplorer(self._this, self.region_name) router_id = 12 self.assertIsNone(ne.get_external_network_id_for_router(router_id)) @mock.patch("murano.engine.system.net_explorer.nclient") @mock.patch("murano.engine.system.net_explorer.auth_utils") @mock.patch("murano.dsl.helpers.get_execution_session") def test_get_ext_network_id_network(self, execution_session, mock_authentication, mock_nclient): ne = net_explorer.NetworkExplorer(self._this, self.region_name) net_id = 144 self.assertEqual(net_id, ne.get_external_network_id_for_network(net_id)) @mock.patch("murano.engine.system.net_explorer.nclient") @mock.patch("murano.engine.system.net_explorer.auth_utils") @mock.patch("murano.dsl.helpers.get_execution_session") def test_get_cidr_none_router(self, execution_session, mock_authentication, mock_nclient): ne = net_explorer.NetworkExplorer(self._this, self.region_name) router_id = None self.assertEqual([], ne._get_cidrs_taken_by_router(router_id)) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/engine/system/test_test_fixture.py0000664000175000017500000000726200000000000025502 0ustar00zuulzuul00000000000000# 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 unittest import mock from murano.dsl import murano_method from murano.dsl import murano_type from murano.dsl import object_store from murano.engine.system import test_fixture from murano.tests.unit import base class TestTestFixture(base.MuranoTestCase): def setUp(self): super(TestTestFixture, self).setUp() self.mock_class = mock.MagicMock(spec=murano_type.MuranoClass) self.mock_method = mock.MagicMock(spec=murano_method.MuranoMethod) self.mock_object_store = mock.Mock(spec=object_store.ObjectStore) self.test_fixture = test_fixture.TestFixture() self.addCleanup(mock.patch.stopall) @mock.patch("murano.dsl.helpers.get_execution_session") def test_finish_env(self, execution_session): self.assertIsNone(self.test_fixture.finish_env()) self.assertTrue(execution_session.called) @mock.patch("murano.dsl.helpers.get_execution_session") def test_start_env(self, execution_session): self.assertIsNone(self.test_fixture.start_env()) self.assertTrue(execution_session.called) @mock.patch("murano.dsl.helpers.get_executor") def test_load(self, executor): executor.return_value = self.mock_object_store model = "test" tf_load = self.test_fixture.load(model) self.assertEqual(self.test_fixture.load(model), tf_load) def test_assert_true(self): expr = (7 > 3) message = None # Calls assertTrue in super class self.test_fixture.assert_true(expr, message) def test_assert_false(self): expr = (3 != 3) message = None # Calls assertFalse in super class self.test_fixture.assert_false(expr, message) def test_assert_in(self): needle = 7 haystack = [3, 7, 8, 9, 22] message = None # Calls assertIn in super class self.test_fixture.assert_in(needle, haystack, message) def test_assert_not_in(self): needle = 16 haystack = [3, 7, 8, 9, 22] message = None # Calls assertNotIn in super class self.test_fixture.assert_not_in(needle, haystack, message) def test_assert_true_fails(self): expr = (7 < 3) message = None self.assertRaises(AssertionError, self.test_fixture.assert_true, expr, message) def test_assert_false_fails(self): expr = (3 == 3) message = None self.assertRaises(AssertionError, self.test_fixture.assert_false, expr, message) def test_assert_in_fails(self): needle = 25 haystack = [3, 7, 8, 9, 22] message = None self.assertRaises(testtools.matchers._impl.MismatchError, self.test_fixture.assert_in, needle, haystack, message) def test_assert_not_in_fails(self): needle = 22 haystack = [3, 7, 8, 9, 22] message = None self.assertRaises(testtools.matchers._impl.MismatchError, self.test_fixture.assert_not_in, needle, haystack, message) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/engine/system/test_workflowclient.py0000664000175000017500000001421500000000000026022 0ustar00zuulzuul00000000000000# Copyright (c) 2016 AT&T # 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. import random from unittest import mock try: from mistralclient.api import client as mistralcli except ImportError: mistralcli = None from oslo_config import cfg from murano.dsl import murano_method from murano.dsl import murano_type from murano.engine.system import workflowclient from murano.tests.unit import base CONF = cfg.CONF def rand_name(name='murano'): """Generates random string. :param name: Basic name :return: """ return name + str(random.randint(1, 0x7fffffff)) class TestMistralClient(base.MuranoTestCase): def setUp(self): super(TestMistralClient, self).setUp() self.mistral_client_mock = mock.Mock() self.mistral_client_mock.client = mock.MagicMock( spec=mistralcli.client) self._patch_client() self.mock_class = mock.MagicMock(spec=murano_type.MuranoClass) self.mock_method = mock.MagicMock(spec=murano_method.MuranoMethod) self._this = mock.MagicMock() self._this.owner = None self.addCleanup(mock.patch.stopall) def _patch_client(self): self.mock_client = mock.Mock(return_value=self.mistral_client_mock) self.client_patcher = mock.patch.object(workflowclient.MistralClient, '_client', self.mock_client) self.client_patcher.start() self.mock_create_client = mock.Mock( return_value=self.mistral_client_mock) self.create_client_patcher = mock.patch.object( workflowclient.MistralClient, '_create_client', self.mock_create_client) self.create_client_patcher.start() def _unpatch_client(self): self.client_patcher.stop() self.create_client_patcher.stop() def test_run_with_execution_success_state(self): test_output = '{"openstack": "foo", "__execution": "bar", "task":'\ ' "baz"}' mock_execution = mock.MagicMock( id='123', state='SUCCESS', output=test_output) self.mock_client.executions.create.return_value = mock_execution self.mock_client.executions.get.return_value = mock_execution run_name = rand_name('test') timeout = 1 mc = workflowclient.MistralClient(self._this, 'regionOne') output = mc.run(run_name, timeout) for prop in ['openstack', '__execution', 'task']: self.assertFalse(hasattr(output, prop)) self.assertEqual({}, output) def test_run_with_execution_error_state(self): mock_execution = mock.MagicMock( id='123', state='ERROR', output="{'test_attr': 'test_val'}") self.mock_client.executions.create.return_value = mock_execution self.mock_client.executions.get.return_value = mock_execution run_name = rand_name('test') timeout = 1 mc = workflowclient.MistralClient(self._this, 'regionOne') expected_error_msg = 'Mistral execution completed with ERROR.'\ ' Execution id: {0}. Output: {1}'\ .format(mock_execution.id, mock_execution.output) with self.assertRaisesRegex(workflowclient.MistralError, expected_error_msg): mc.run(run_name, timeout) def test_run_except_timeout_error(self): mock_execution = mock.MagicMock( id='123', state='TEST_STATE', output="{'test_attr': 'test_val'}") self.mock_client.executions.create.return_value = mock_execution self.mock_client.executions.get.return_value = mock_execution run_name = rand_name('test') timeout = 1 mc = workflowclient.MistralClient(self._this, 'regionOne') expected_error_msg = 'Mistral run timed out. Execution id: {0}.'\ .format(mock_execution.id) with self.assertRaisesRegex(workflowclient.MistralError, expected_error_msg): mc.run(run_name, timeout) def test_run_with_immediate_timeout(self): mock_execution = mock.MagicMock( id='123', state='ERROR', output="{'test_attr': 'test_val'}") self.mock_client.executions.create.return_value = mock_execution run_name = rand_name('test') timeout = 0 mc = workflowclient.MistralClient(self._this, 'regionOne') self.assertEqual(mock_execution.id, mc.run(run_name, timeout)) def test_upload(self): mc = workflowclient.MistralClient(self._this, 'regionOne') definition = rand_name('test') self.assertIsNone(mc.upload(definition)) self.assertTrue(workflowclient.MistralClient. _client.workflows.create.called) @mock.patch('murano.engine.system.workflowclient.auth_utils') def test_client_property(self, _): self._unpatch_client() test_mistral_settings = { 'url': rand_name('test_mistral_url'), 'project_id': rand_name('test_project_id'), 'endpoint_type': rand_name('test_endpoint_type'), 'auth_token': rand_name('test_auth_token'), 'user_id': rand_name('test_user_id'), 'insecure': rand_name('test_insecure'), 'cacert': rand_name('test_ca_cert') } with mock.patch('murano.engine.system.workflowclient.CONF')\ as mock_conf: mock_conf.mistral = mock.MagicMock(**test_mistral_settings) region_name = rand_name('test_region_name') mc = workflowclient.MistralClient(self._this, region_name) mistral_client = mc._client self.assertIsNotNone(mistral_client) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/engine/test_mock_context_manager.py0000664000175000017500000001160000000000000025607 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from unittest import mock from yaql import contexts from yaql import specs from murano.dsl import constants from murano.dsl import executor from murano.dsl import murano_type from murano.engine import execution_session from murano.engine import mock_context_manager from murano.engine.system import test_fixture from murano.tests.unit import base from murano.tests.unit.dsl.foundation import object_model as om from murano.tests.unit.dsl.foundation import runner from murano.tests.unit.dsl.foundation import test_case FIXTURE_CLASS = 'io.murano.system.Agent' FIXTURE_FUNC = 'call' def _get_fd(set_to_extract): return list(set_to_extract)[0] class TestMockContextManager(mock_context_manager.MockContextManager): def __init__(self, functions): super(TestMockContextManager, self).__init__() self.__functions = functions def create_root_context(self, runtime_version): root_context = super(TestMockContextManager, self).create_root_context( runtime_version) context = root_context.create_child_context() for name, func in self.__functions.items(): context.register_function(func, name) return context class MockRunner(runner.Runner): def __init__(self, model, package_loader, functions): if isinstance(model, str): model = om.Object(model) model = om.build_model(model) if 'Objects' not in model: model = {'Objects': model} self.executor = executor.MuranoDslExecutor( package_loader, TestMockContextManager(functions), execution_session.ExecutionSession()) self._root = self.executor.load(model).object class TestMockManager(base.MuranoTestCase): def test_create_type_context(self): mock_manager = mock_context_manager.MockContextManager() mock_murano_class = mock.MagicMock(spec=murano_type.MuranoClass) mock_murano_class.name = FIXTURE_CLASS original_function = mock.MagicMock(spec=specs.FunctionDefinition) original_function.is_method = True original_function.name = FIXTURE_FUNC original_context = contexts.Context() p = mock.patch("inspect.getargspec", new=mock.MagicMock()) p.start() original_context.register_function(original_function) mock_murano_class.context = original_context p.stop() mock_function = mock.MagicMock(spec=specs.FunctionDefinition) mock_function.is_method = True mock_function.name = FIXTURE_FUNC mock_manager.class_mock_ctx[FIXTURE_CLASS] = [mock_function] result_context = mock_manager.create_type_context(mock_murano_class) all_functions = result_context.collect_functions(FIXTURE_FUNC) # Mock function should go first, but result context should contain both self.assertIs(mock_function, _get_fd(all_functions[0])) self.assertIs(original_function, _get_fd(all_functions[1])) def test_create_root_context(self): mock_manager = mock_context_manager.MockContextManager() ctx_to_check = mock_manager.create_root_context( constants.RUNTIME_VERSION_1_1) inject_count = ctx_to_check.collect_functions('inject') with_original_count = ctx_to_check.collect_functions('withOriginal') self.assertEqual(2, len(inject_count[0])) self.assertEqual(1, len(with_original_count[0])) class TestMockYaqlFunctions(test_case.DslTestCase): def setUp(self): super(TestMockYaqlFunctions, self).setUp() self.package_loader.load_package('io.murano', None).register_class( test_fixture.TestFixture) self.runner = MockRunner(om.Object('TestMocks'), self.package_loader, self._functions) def test_inject_method_with_str(self): self.runner.testInjectMethodWithString() def test_inject_object_with_str(self): self.runner.testInjectObjectWithString() def test_inject_method_with_yaql_expr(self): self.runner.testInjectMethodWithYaqlExpr() def test_inject_method_with_yaql_expr2(self): self.runner.testInjectMethodWithYaqlExpr2() def test_inject_object_with_yaql_expr(self): self.runner.testInjectObjectWithYaqlExpr() def test_with_original(self): self.runner.testWithoriginal() def test_original_method(self): self.runner.testOriginalMethod() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/engine/test_package_loader.py0000664000175000017500000006406000000000000024351 0ustar00zuulzuul00000000000000# Copyright 2016 AT&T Corp # 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. import collections from muranoclient.common import exceptions as muranoclient_exc import os import shutil import tempfile from unittest import mock import semantic_version import testtools from murano.dsl import exceptions as dsl_exceptions from murano.dsl import murano_package as dsl_package from murano.engine import package_loader from murano.packages import exceptions as pkg_exc from murano.tests.unit import base from murano.tests.unit import utils class TestPackageCache(base.MuranoTestCase): def setUp(self): super(TestPackageCache, self).setUp() self.location = tempfile.mkdtemp() self.override_config('enable_packages_cache', True, 'engine') self.override_config('packages_cache', self.location, 'engine') self._patch_loader_client() self.loader = package_loader.ApiPackageLoader(None) def tearDown(self): shutil.rmtree(self.location, ignore_errors=True) super(TestPackageCache, self).tearDown() def _patch_loader_client(self): self.murano_client_patcher = mock.patch( 'murano.engine.package_loader.ApiPackageLoader.client') self.murano_client_patcher.start() self.murano_client = package_loader.ApiPackageLoader.client def _unpatch_loader_client(self): self.murano_client_patcher.stop() @mock.patch('murano.engine.package_loader.auth_utils') @mock.patch('murano.engine.package_loader.versionutils') def test_client_property(self, mock_versionutils, mock_auth_utils): self._unpatch_loader_client() session = mock_auth_utils.get_client_session() session_params = mock_auth_utils.get_session_client_parameters session.auth.get_token.return_value = 'test_token' session.get_endpoint.return_value = 'test_endpoint/v3' session_params.return_value = {'endpoint': 'test_endpoint/v3'} self.override_config('packages_service', 'glance', group='engine') client = self.loader.client mock_versionutils.report_deprecated_feature.assert_called_once_with( package_loader.LOG, "'glance' packages_service option has been renamed " "to 'glare', please update your configuration") self.assertIsNotNone(client) self.assertIsNotNone(self.loader._glare_client) # Test whether client is initialized using different CONF. self.override_config('packages_service', 'test_service', group='engine') client = self.loader.client self.assertIsNotNone(client) def test_import_fixations_table(self): test_fixations = { 'test_package_1': [semantic_version.Version('1.1.0'), semantic_version.Version('1.1.0')], 'test_package_2': [semantic_version.Version('2.1.0'), semantic_version.Version('2.4.3')] } expected = collections.defaultdict(set) expected['test_package_1'] = set([semantic_version.Version('1.1.0')]) expected['test_package_2'] = set([semantic_version.Version('2.1.0'), semantic_version.Version('2.4.3')]) self.loader.import_fixation_table(test_fixations) self.assertEqual(expected, self.loader._fixations) def test_register_package(self): test_version = semantic_version.Version('1.1.0') package = mock.MagicMock() package.name = 'test_package_name' package.version = test_version package.classes = ['test_class_1', 'test_class_2'] self.loader.register_package(package) self.assertEqual( package, self.loader._package_cache['test_package_name'][test_version]) for class_name in package.classes: self.assertEqual( package, self.loader._class_cache[class_name][test_version]) def test_load_package(self): test_version = semantic_version.Version('1.1.0') package = mock.MagicMock() package.name = 'test_package_name' package.version = test_version self.loader.import_fixation_table({package.name: [test_version]}) self.loader.register_package(package) version_spec = semantic_version.Spec('>=1.0.0,<2.4.0') retrieved_pkg = self.loader.load_package(package.name, version_spec) self.assertEqual(retrieved_pkg, package) @testtools.skipIf(os.name == 'nt', "Doesn't work on Windows") @mock.patch('murano.engine.package_loader.ApiPackageLoader.' '_to_dsl_package') def test_load_package_with_get_definiton(self, mock_to_dsl_package): fqn = 'io.murano.apps.test_package' package = mock.MagicMock() package.id = 'test_package_id' package.name = 'test_package_name' package.fully_qualified_name = fqn package.version = '2.5.3' path, _ = utils.compose_package( 'test_package', self.location, archive_dir=self.location, version=package.version) with open(path, 'rb') as f: package_data = f.read() self.murano_client.packages.filter = mock.MagicMock( return_value=[package]) self.murano_client.packages.download = mock.MagicMock( return_value=package_data) mock_to_dsl_package.return_value = package spec = semantic_version.Spec('*') retrieved_pkg = self.loader.load_package(fqn, spec) self.assertEqual(retrieved_pkg, package) self.assertTrue(os.path.isdir(os.path.join( self.location, fqn))) self.assertTrue(os.path.isdir(os.path.join( self.location, fqn, package.version))) self.assertTrue(os.path.isdir(os.path.join( self.location, fqn, package.version, package.id))) self.assertTrue(os.path.isfile(os.path.join( self.location, fqn, package.version, package.id, 'manifest.yaml'))) self.murano_client.packages.download.assert_called_once_with( package.id) expected_fixations = collections.defaultdict(set) expected_fixations[fqn] = set( [semantic_version.Version(package.version)]) self.assertEqual(expected_fixations, self.loader._fixations) self.loader.cleanup() def test_load_package_except_lookup_error(self): expected_error_msg = 'Package "test_package_name" is not found' invalid_specs = [semantic_version.Spec('>=1.1.1'), semantic_version.Spec('<1.1.0'), semantic_version.Spec('>=1.1.1,<1.1.0'), semantic_version.Spec('==1.1.1')] fqn = 'io.murano.apps.test' test_version = semantic_version.Version('1.1.0') package = mock.MagicMock() package.name = fqn package.fully_qualified_name = fqn package.version = test_version self.loader.import_fixation_table({fqn: [test_version]}) self.loader.register_package(package) with self.assertRaisesRegex(dsl_exceptions.NoPackageFound, expected_error_msg): for spec in invalid_specs: self.loader.load_package('test_package_name', spec) def test_load_class_package(self): fqn = 'io.murano.apps.test' package = mock.MagicMock() package.fully_qualified_name = fqn package.classes = ['test_class_1', 'test_class_2'] test_version = semantic_version.Version('1.1.0') package.version = test_version self.loader.register_package(package) spec = semantic_version.Spec('*') for class_name in ['test_class_1', 'test_class_2']: retrieved_pkg = self.loader.load_class_package(class_name, spec) self.assertEqual(retrieved_pkg, package) @testtools.skipIf(os.name == 'nt', "Doesn't work on Windows") def test_load_class_package_with_get_definition(self): fqn = 'io.murano.apps.test' path, name = utils.compose_package( 'test', self.location, archive_dir=self.location) with open(path, 'rb') as f: package_data = f.read() spec = semantic_version.Spec('*') first_id, second_id, third_id = '123', '456', '789' package = mock.MagicMock() package.fully_qualified_name = fqn package.id = first_id package.version = '0.0.1' self.murano_client.packages.filter = mock.MagicMock( return_value=[package]) self.murano_client.packages.download = mock.MagicMock( return_value=package_data) # load the package self.loader.load_class_package(fqn, spec) # assert that everything got created self.assertTrue(os.path.isdir(os.path.join( self.location, fqn))) self.assertTrue(os.path.isdir(os.path.join( self.location, fqn, package.version))) self.assertTrue(os.path.isdir(os.path.join( self.location, fqn, package.version, first_id))) self.assertTrue(os.path.isfile(os.path.join( self.location, fqn, package.version, first_id, 'manifest.yaml'))) # assert that we called download self.assertEqual(self.murano_client.packages.download.call_count, 1) # now that the cache is in place, call it for the 2d time self.loader._package_cache = {} self.loader._class_cache = {} self.loader.load_class_package(fqn, spec) # check that we didn't download a thing self.assertEqual(self.murano_client.packages.download.call_count, 1) # changing id, new package would be downloaded. package.id = second_id self.loader._package_cache = {} self.loader._class_cache = {} self.loader.load_class_package(fqn, spec) # check that we didn't download a thing self.assertEqual(self.murano_client.packages.download.call_count, 2) # check that old directories were not deleted # we did not call cleanup and did not release the locks self.assertTrue(os.path.isdir(os.path.join( self.location, fqn, package.version, first_id))) # check that new directories got created correctly self.assertTrue(os.path.isdir(os.path.join( self.location, fqn))) self.assertTrue(os.path.isdir(os.path.join( self.location, fqn, package.version))) self.assertTrue(os.path.isdir(os.path.join( self.location, fqn, package.version, second_id))) self.assertTrue(os.path.isfile(os.path.join( self.location, fqn, package.version, second_id, 'manifest.yaml'))) self.assertTrue(os.path.isdir(os.path.join( self.location, fqn, package.version))) self.assertTrue(os.path.isdir(os.path.join( self.location, fqn, package.version, second_id))) # changing id, new package would be downloaded. package.id = third_id self.loader._package_cache = {} self.loader._class_cache = {} # release all the locks self.loader.cleanup() self.loader.load_class_package(fqn, spec) # check that we didn't download a thing self.assertEqual(self.murano_client.packages.download.call_count, 3) # check that old directories were *deleted* self.assertFalse(os.path.isdir(os.path.join( self.location, fqn, package.version, first_id))) self.assertFalse(os.path.isdir(os.path.join( self.location, fqn, package.version, second_id))) # check that new directories got created correctly self.assertTrue(os.path.isdir(os.path.join( self.location, fqn, package.version, third_id))) self.assertTrue(os.path.isfile(os.path.join( self.location, fqn, package.version, third_id, 'manifest.yaml'))) def test_load_class_package_except_lookup_error(self): invalid_specs = [semantic_version.Spec('>=1.1.1'), semantic_version.Spec('<1.1.0'), semantic_version.Spec('>=1.1.1,<1.1.0'), semantic_version.Spec('==1.1.1')] fqn = 'io.murano.apps.test' test_version = semantic_version.Version('1.1.0') package = mock.MagicMock() package.name = fqn package.fully_qualified_name = fqn package.version = test_version package.classes = ['test_class_1', 'test_class_2'] self.loader.import_fixation_table({fqn: [test_version]}) self.loader.register_package(package) for class_ in package.classes: expected_error_msg = 'Package for class "{0}" is not found'\ .format(class_) with self.assertRaisesRegex(dsl_exceptions.NoPackageForClassFound, expected_error_msg): for spec in invalid_specs: self.loader.load_class_package(class_, spec) @mock.patch('murano.engine.package_loader.LOG') def test_get_definition_with_multiple_packages_returned(self, mock_log): self.loader._execution_session = mock.MagicMock() self.loader._execution_session.project_id = 'test_project_id' matching_pkg = mock.MagicMock(owner_id='test_project_id', is_public=False) public_pkg = mock.MagicMock(owner_id='another_test_project_id', is_public=True) other_pkg = mock.MagicMock(owner_id='another_test_project_id', is_public=False) # Test package with matching owner_id is returned, despite ordering. self.loader.client.packages.filter.return_value =\ [public_pkg, matching_pkg, other_pkg] expected_pkg = matching_pkg retrieved_pkg = self.loader._get_definition({}) self.assertEqual(expected_pkg, retrieved_pkg) # Test public package is returned, despite ordering. self.loader.client.packages.filter.return_value =\ [other_pkg, public_pkg] expected_pkg = public_pkg retrieved_pkg = self.loader._get_definition({}) self.assertEqual(expected_pkg, retrieved_pkg) # Test other package is returned. self.loader.client.packages.filter.return_value =\ [other_pkg, other_pkg] expected_pkg = other_pkg retrieved_pkg = self.loader._get_definition({}) self.assertEqual(expected_pkg, retrieved_pkg) mock_log.debug.assert_any_call( 'Ambiguous package resolution: more than 1 package found for query' ' "{opts}", will resolve based on the ownership' .format(opts={'catalog': True})) @mock.patch('murano.engine.package_loader.LOG') def test_get_definition_except_lookup_error(self, mock_log): self.loader.client.packages.filter.return_value = [] with self.assertRaisesRegex(LookupError, None): self.loader._get_definition({}) mock_log.debug.assert_called_once_with( "There are no packages matching filter {opts}" .format(opts={'catalog': True})) mock_log.debug.reset_mock() self.loader.client.packages.filter.side_effect =\ muranoclient_exc.HTTPException with self.assertRaisesRegex(LookupError, None): self.loader._get_definition({}) mock_log.debug.assert_called_once_with( 'Failed to get package definition from repository') @testtools.skipIf(os.name == 'nt', "Doesn't work on Windows") @mock.patch('murano.engine.package_loader.LOG') @mock.patch('murano.engine.package_loader.os') @mock.patch('murano.engine.package_loader.load_utils') def test_get_package_by_definition_except_package_load_error( self, mock_load_utils, mock_os, mock_log): # Test that the first instance of the exception is caught. temp_directory = tempfile.mkdtemp(prefix='test-package-loader-', dir=tempfile.tempdir) mock_os.path.isdir.return_value = True mock_os.path.join.return_value = temp_directory mock_load_utils.load_from_dir.side_effect = pkg_exc.PackageLoadError fqn = 'io.murano.apps.test' path, _ = utils.compose_package( 'test', self.location, archive_dir=self.location) with open(path, 'rb') as f: package_data = f.read() package = mock.MagicMock() package.fully_qualified_name = fqn package.id = '123' package.version = '0.0.1' self.murano_client.packages.download = mock.MagicMock( return_value=package_data) self.loader._get_package_by_definition(package) mock_log.exception.assert_called_once_with( 'Unable to load package from cache. Clean-up.') mock_log.exception.reset_mock() # Test that the second instance of the exception is caught. mock_os.path.isdir.return_value = [False, True] self.loader._get_package_by_definition(package) mock_log.exception.assert_called_once_with( 'Unable to load package from cache. Clean-up.') os.remove(temp_directory) @testtools.skipIf(os.name == 'nt', "Doesn't work on Windows") def test_get_package_by_definition_except_http_exception(self): fqn = 'io.murano.apps.test' path, _ = utils.compose_package( 'test', self.location, archive_dir=self.location) package = mock.MagicMock() package.fully_qualified_name = fqn package.id = '123' package.version = '0.0.1' self.murano_client.packages.download.side_effect =\ muranoclient_exc.HTTPException expected_error_msg = 'Error loading package id {0}:'.format(package.id) with self.assertRaisesRegex(pkg_exc.PackageLoadError, expected_error_msg): self.loader._get_package_by_definition(package) @testtools.skipIf(os.name == 'nt', "Doesn't work on Windows") @mock.patch('murano.engine.package_loader.LOG') @mock.patch('murano.engine.package_loader.tempfile') def test_get_package_by_definition_except_io_error(self, mock_tempfile, mock_log): fqn = 'io.murano.apps.test' path, _ = utils.compose_package( 'test', self.location, archive_dir=self.location) with open(path, 'rb') as f: package_data = f.read() package = mock.MagicMock() package.fully_qualified_name = fqn package.id = '123' package.version = '0.0.1' self.murano_client.packages.download = mock.MagicMock( return_value=package_data) mock_package_file = mock.MagicMock( write=mock.MagicMock(side_effect=IOError)) mock_package_file.configure_mock(name='test_package_file') mock_tempfile.NamedTemporaryFile().__enter__.return_value =\ mock_package_file expected_error_msg = 'Unable to extract package data for {0}'\ .format(package.id) with self.assertRaisesRegex(pkg_exc.PackageLoadError, expected_error_msg): self.loader._get_package_by_definition(package) def test_try_cleanup_cache_with_null_package_directory(self): # Test null package directory causes early return. result = self.loader.try_cleanup_cache(None, None) self.assertIsNone(result) @mock.patch('murano.engine.package_loader.shutil') @mock.patch('murano.engine.package_loader.m_utils') @mock.patch('murano.engine.package_loader.usage_mem_locks') @mock.patch('murano.engine.package_loader.LOG') @mock.patch('murano.engine.package_loader.os') def test_try_cleanup_cache_except_os_error(self, mock_os, mock_log, mock_usage_mem_locks, *args): # Test first instance of OSError is handled. mock_os.listdir.side_effect = OSError result = self.loader.try_cleanup_cache(None, None) self.assertIsNone(result) # Test second instance of OSError is handled. mock_os.reset_mock() mock_os.listdir.return_value = {'1', '2'} mock_os.listdir.side_effect = None mock_os.remove.side_effect = OSError mock_usage_mem_locks.__getitem__().write_lock().__enter__.\ return_value = True result = self.loader.try_cleanup_cache('test_package_directory', None) self.assertIsNone(result) self.assertIn("Couldn't delete lock file:", str(mock_log.warning.mock_calls[0])) class TestCombinedPackageLoader(base.MuranoTestCase): def setUp(self): super(TestCombinedPackageLoader, self).setUp() location = os.path.dirname(__file__) self.override_config('load_packages_from', [location], 'engine') self.execution_session = mock.MagicMock() self.loader = package_loader.CombinedPackageLoader( self.execution_session) self._patch_api_loader() self.local_pkg_name = 'io.murano.test.MyTest' self.api_pkg_name = 'test.mpl.v1.app.Thing' def _patch_api_loader(self): self.api_loader_patcher = mock.patch.object( self.loader, 'api_loader', return_value=mock.MagicMock()) self.api_loader_patcher.start() def _unpatch_api_loader(self): self.api_loader_patcher.stop() def test_loaders_initialized(self): self.assertEqual(1, len(self.loader.directory_loaders), 'One directory class loader should be initialized' ' since there is one valid murano pl package in the' ' provided directory in config.') self.assertIsInstance(self.loader.directory_loaders[0], package_loader.DirectoryPackageLoader) def test_get_package_by_class_directory_loader(self): spec = semantic_version.Spec('*') result = self.loader.load_class_package(self.local_pkg_name, spec) self.assertIsInstance(result, dsl_package.MuranoPackage) def test_get_package_by_name_directory_loader(self): spec = semantic_version.Spec('*') result = self.loader.load_package(self.local_pkg_name, spec) self.assertIsInstance(result, dsl_package.MuranoPackage) def test_get_package_by_class_api_loader(self): spec = semantic_version.Spec('*') self.loader.load_package(self.api_pkg_name, spec) self.loader.api_loader.load_package.assert_called_with( self.api_pkg_name, spec) def test_get_package_api_loader(self): spec = semantic_version.Spec('*') self.loader.load_class_package(self.api_pkg_name, spec) self.loader.api_loader.load_class_package.assert_called_with( self.api_pkg_name, spec) def test_register_package(self): test_package = mock.MagicMock() self.loader.register_package(test_package) self.loader.api_loader.register_package.assert_called_once_with( test_package) def test_import_fixation_table(self): self.loader.directory_loaders = [ mock.MagicMock(), mock.MagicMock(), mock.MagicMock() ] test_fixations = { 'test_package_1': [semantic_version.Version('1.1.0')], 'test_package_2': [semantic_version.Version('2.1.0'), semantic_version.Version('2.4.3')] } self.loader.import_fixation_table(test_fixations) self.loader.api_loader.import_fixation_table.assert_called_once_with( test_fixations) for loader in self.loader.directory_loaders: loader.import_fixation_table.assert_called_once_with( test_fixations) def test_compact_fixation_table(self): self.loader.directory_loaders = [ mock.MagicMock(), mock.MagicMock(), mock.MagicMock() ] self.loader.compact_fixation_table() self.loader.api_loader.compact_fixation_table.assert_called_once_with() for loader in self.loader.directory_loaders: loader.compact_fixation_table.assert_called_once_with() def test_export_fixation_table(self): self._unpatch_api_loader() test_fixations = { 'test_package_1': [semantic_version.Version('1.1.1')], 'test_package_2': [semantic_version.Version('2.2.2'), semantic_version.Version('3.3.3')] } self.loader.api_loader.import_fixation_table(test_fixations) expected_table = {'test_package_1': ['1.1.1'], 'test_package_2': sorted(['2.2.2', '3.3.3'])} table = self.loader.export_fixation_table() table['test_package_2'] = sorted(table['test_package_2']) self.assertEqual(sorted(expected_table.items()), sorted(table.items())) test_fixations = { 'test_package_1': [semantic_version.Version('4.4.4')], 'test_package_2': [semantic_version.Version('5.5.5'), semantic_version.Version('6.6.6')], 'test_package_3': [semantic_version.Version('7.7.7')] } self.loader.directory_loaders[0].import_fixation_table(test_fixations) expected_table = {'test_package_1': ['1.1.1', '4.4.4'], 'test_package_2': ['2.2.2', '3.3.3', '5.5.5', '6.6.6'], 'test_package_3': ['7.7.7']} table = self.loader.export_fixation_table() for key, value in table.items(): table[key] = sorted(value) self.assertEqual(sorted(expected_table.items()), sorted(table.items())) ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1696417899.8491812 murano-16.0.0/murano/tests/unit/packages/0000775000175000017500000000000000000000000020322 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/packages/__init__.py0000664000175000017500000000000000000000000022421 0ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1696417899.8491812 murano-16.0.0/murano/tests/unit/packages/hot_package/0000775000175000017500000000000000000000000022567 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/packages/hot_package/__init__.py0000664000175000017500000000000000000000000024666 0ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1696417899.8491812 murano-16.0.0/murano/tests/unit/packages/hot_package/test.hot.1/0000775000175000017500000000000000000000000024476 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1696417899.8491812 murano-16.0.0/murano/tests/unit/packages/hot_package/test.hot.1/Resources/0000775000175000017500000000000000000000000026450 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/packages/hot_package/test.hot.1/Resources/FullTestName0000664000175000017500000000307500000000000030743 0ustar00zuulzuul00000000000000FullName: test resources: parameters: foo: type: number label: foo_label description: foo_description default: Default Value constraints: - length: min: 0 max: 5 - range: min: 0 max: 5 description: Range Description - allowed_values: [0, 1, 2, 3, 4] description: Allowed Values Description - allowed_pattern: "[A-Za-z0-9]" description: Allowed Pattern Description bar: type: boolean constraints: - length: min: 0 - range: min: 0 baz: type: string constraints: - length: max: 5 - range: max: 5 description: Range Description parameter_groups: - group1: parameters: foo: type: number label: foo_label description: foo_description default: Default Value constraints: - length: min: 0 max: 5 - range: min: 0 max: 5 description: Range Description - allowed_values: [0, 1, 2, 3, 4] description: Allowed Values Description - allowed_pattern: "[A-Za-z0-9]" description: Allowed Pattern Description bar: type: boolean constraints: - length: min: 0 - range: min: 0 baz: type: string constraints: - length: max: 5 - range: max: 5 description: Range Description ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/packages/hot_package/test.hot.1/properties_manifest.yaml0000664000175000017500000000120000000000000031435 0ustar00zuulzuul00000000000000parameters: param1: type: boolean constraints: - allowed_values: [True, False] param2: type: string constraints: - allowed_values: [bar] - length: max: 50 - length: min: 0 - allowed_pattern: "[A-Za-z0-9]" param3: type: number constraints: - allowed_values: [0, 1, 2, 3, 4] - length: min: 0 max: 5 - range: min: 0 max: 4 param4: type: number constraints: - range: min: -1000 - range: max: 1000 param5: type: json param6: type: comma_delimited_list ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/packages/hot_package/test.hot.1/template.yaml0000664000175000017500000000307500000000000027202 0ustar00zuulzuul00000000000000FullName: test resources: parameters: foo: type: number label: foo_label description: foo_description default: Default Value constraints: - length: min: 0 max: 5 - range: min: 0 max: 5 description: Range Description - allowed_values: [0, 1, 2, 3, 4] description: Allowed Values Description - allowed_pattern: "[A-Za-z0-9]" description: Allowed Pattern Description bar: type: boolean constraints: - length: min: 0 - range: min: 0 baz: type: string constraints: - length: max: 5 - range: max: 5 description: Range Description parameter_groups: - group1: parameters: foo: type: number label: foo_label description: foo_description default: Default Value constraints: - length: min: 0 max: 5 - range: min: 0 max: 5 description: Range Description - allowed_values: [0, 1, 2, 3, 4] description: Allowed Values Description - allowed_pattern: "[A-Za-z0-9]" description: Allowed Pattern Description bar: type: boolean constraints: - length: min: 0 - range: min: 0 baz: type: string constraints: - length: max: 5 - range: max: 5 description: Range Description ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1696417899.8491812 murano-16.0.0/murano/tests/unit/packages/hot_package/test.hot.2/0000775000175000017500000000000000000000000024477 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1696417899.8491812 murano-16.0.0/murano/tests/unit/packages/hot_package/test.hot.2/Resources/0000775000175000017500000000000000000000000026451 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/packages/hot_package/test.hot.2/Resources/FullTestName0000664000175000017500000000004600000000000030737 0ustar00zuulzuul00000000000000FullName: template with invalid format././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/packages/hot_package/test.hot.2/template.yaml0000664000175000017500000000004600000000000027176 0ustar00zuulzuul00000000000000FullName: template with invalid format././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/packages/hot_package/test_hot_package.py0000664000175000017500000001711300000000000026450 0ustar00zuulzuul00000000000000# Copyright 2016 AT&T Corp # 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. import os import yaml from murano.packages import exceptions import murano.packages.hot_package import murano.packages.load_utils as load_utils import murano.tests.unit.base as test_base class TestHotPackage(test_base.MuranoTestCase): def _get_hot_package(self, source_directory): manifest = { 'FullName': 'FullTestName', 'Version': '1.0.0', 'Type': 'Application', 'Name': 'TestName', 'Description': 'TestDescription', 'Author': 'TestAuthor', 'Supplier': 'TestSupplier', 'Logo:': 'TestLogo', 'Tags': ['Tag1', 'Tag2'] } return murano.packages.hot_package.HotPackage( None, None, source_directory=source_directory, manifest=manifest ) @classmethod def setUpClass(cls): super(TestHotPackage, cls).setUpClass() this_dir = os.path.dirname(os.path.realpath(__file__)) cls.test_dirs = [ os.path.join(this_dir, 'test.hot.1'), os.path.join(this_dir, 'test.hot.2'), os.path.join(this_dir, 'test.hot.3') ] manifest_path = os.path.join(cls.test_dirs[0], 'template.yaml') cls.manifest = {} with open(manifest_path) as manifest_file: for key, value in yaml.safe_load(manifest_file).items(): cls.manifest[key] = value properties_manifest_path = os.path.join(cls.test_dirs[0], 'properties_manifest.yaml') cls.properties_manifest = {} with open(properties_manifest_path) as manifest_file: for key, value in yaml.safe_load(manifest_file).items(): cls.properties_manifest[key] = value def test_heat_files_generated(self): package_dir = os.path.abspath( os.path.join(__file__, '../../test_packages/test.hot.v1.app_with_files') ) load_utils.load_from_dir(package_dir) files = murano.packages.hot_package.HotPackage._translate_files( package_dir) expected_result = { "testHeatFile", "middle_file/testHeatFile", "middle_file/inner_file/testHeatFile", "middle_file/inner_file2/testHeatFile" } msg = "hot files were not generated correctly" self.assertSetEqual(expected_result, set(files), msg) def test_heat_files_generated_empty(self): package_dir = os.path.abspath( os.path.join(__file__, '../../test_packages/test.hot.v1.app') ) load_utils.load_from_dir(package_dir) files = murano.packages.hot_package.HotPackage \ ._translate_files(package_dir) msg = "heat files were not generated correctly. Expected empty list" self.assertEqual([], files, msg) def test_build_properties(self): result = murano.packages.hot_package.HotPackage._build_properties( self.properties_manifest, validate_hot_parameters=True) self.assertIn('templateParameters', result) params = result['templateParameters'] self.assertEqual(6, len(params['Contract'])) param1 = params['Contract']['param1'] param2 = params['Contract']['param2'] param3 = params['Contract']['param3'] param4 = params['Contract']['param4'] param5 = params['Contract']['param5'] param6 = params['Contract']['param6'] self.assertEqual("$.bool().check($ in list(True, False))", param1.expr) self.assertEqual("$.string().check($ in list('bar'))." "check(len($) <= 50).check(len($) >= 0)." "check(matches($, '[A-Za-z0-9]'))", param2.expr) self.assertEqual("$.int().check($ in list(0, 1, 2, 3, 4))" ".check(len($) >= 0 and len($) <= 5)." "check($ >= 0 and $ <= 4)", param3.expr) self.assertEqual("$.int().check($ >= -1000).check($ <= " "1000)", param4.expr) self.assertEqual("$.string()", param5.expr) self.assertEqual("$.string()", param6.expr) result = murano.packages.hot_package.HotPackage._build_properties( self.properties_manifest, validate_hot_parameters=False) expected_result = { 'Contract': {}, 'Default': {}, 'Usage': 'In' } self.assertEqual(expected_result, result['templateParameters']) def test_translate_param_to_contract_with_inappropriate_value(self): self.assertRaisesRegex( ValueError, 'Unsupported parameter type', murano.packages.hot_package.HotPackage. _translate_param_to_contract, {'type': 'Inappropriate value'} ) def test_get_class_name(self): hot_package = self._get_hot_package(self.test_dirs[0]) translated_class, _ = hot_package.get_class(hot_package.full_name) self.assertIsNotNone(translated_class) self.assertEqual(translated_class, hot_package._translated_class) def test_get_class_name_with_invalid_template_name(self): hot_package = self._get_hot_package(self.test_dirs[0]) self.assertRaisesRegex( exceptions.PackageClassLoadError, 'Class not defined in this package', hot_package.get_class, None) def test_get_class_name_with_invalid_template_format(self): hot_package = self._get_hot_package(self.test_dirs[1]) self.assertRaisesRegex( exceptions.PackageFormatError, 'Not a HOT template', hot_package.get_class, hot_package.full_name) def test_translate_ui(self): hot_package = self._get_hot_package(self.test_dirs[0]) yaml = hot_package._translate_ui() self.assertIsNotNone(yaml) expected_application = ''' "Application": "?": "classVersion": "1.0.0" "package": "FullTestName" "type": "FullTestName" "name": !yaql "$.group0.name" "templateParameters": "bar": !yaql "$.group1.bar" "baz": !yaql "$.group1.baz" "foo": !yaql "$.group1.foo" ''' self.assertIn(expected_application.replace(' ', '').replace('\n', ''), yaml.replace(' ', '').replace('\n', '')) def test_translate_ui_with_nonexistent_template(self): hot_package = self._get_hot_package(self.test_dirs[2]) self.assertRaisesRegex( exceptions.PackageClassLoadError, 'File with class definition not found', hot_package._translate_ui) def test_translate_class_with_nonexistent_template(self): hot_package = self._get_hot_package(self.test_dirs[2]) self.assertRaisesRegex( exceptions.PackageClassLoadError, 'File with class definition not found', hot_package._translate_class) ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1696417899.8491812 murano-16.0.0/murano/tests/unit/packages/mpl_package/0000775000175000017500000000000000000000000022565 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1696417899.8491812 murano-16.0.0/murano/tests/unit/packages/mpl_package/Classes/0000775000175000017500000000000000000000000024162 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/packages/mpl_package/Classes/test.class10000664000175000017500000000001400000000000026244 0ustar00zuulzuul00000000000000test.class1 ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1696417899.8491812 murano-16.0.0/murano/tests/unit/packages/mpl_package/UI/0000775000175000017500000000000000000000000023102 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/packages/mpl_package/UI/ui.yaml0000664000175000017500000000137000000000000024404 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. Version: 2.2 Forms: - appConfiguration: fields: - name: license type: string description: Apache License, Version 2.0 hidden: false required: false ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/packages/mpl_package/__init__.py0000664000175000017500000000000000000000000024664 0ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/packages/mpl_package/manifest.yaml0000664000175000017500000000033000000000000025253 0ustar00zuulzuul00000000000000Type: Application FullName: test.pl.v1.app Name: Test PL v1 App Description: Test PL v1 Application Author: Test Runner Tags: [Linux] Classes: Class1: test.class1 Class2: test.class2 UI: ui.yaml Meta: test.meta ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/packages/mpl_package/test_mpl_package.py0000664000175000017500000001054600000000000026447 0ustar00zuulzuul00000000000000# Copyright 2016 AT&T Corp # 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. import os import yaml from murano.packages import exceptions import murano.packages.mpl_package as mpl_package import murano.tests.unit.base as test_base class TestMPLPackage(test_base.MuranoTestCase): def setUp(cls): super(TestMPLPackage, cls).setUp() cls.source_directory = os.path.dirname(os.path.realpath(__file__)) manifest_path = os.path.join(cls.source_directory, 'manifest.yaml') cls.manifest = {} with open(manifest_path) as manifest_file: for key, value in yaml.safe_load(manifest_file).items(): cls.manifest[key] = value def test_classes_property(self): package = mpl_package.MuranoPlPackage(None, None, self.source_directory, self.manifest) classes = package.classes self.assertIn('Class1', classes) self.assertIn('Class2', classes) def test_ui_property(self): package = mpl_package.MuranoPlPackage(None, None, self.source_directory, self.manifest) ui = package.ui self.assertIsNotNone(ui) self.assertIn(b'name: license', ui) self.assertIn(b'type: string', ui) self.assertIn(b'description: Apache License, Version 2.0', ui) self.assertIn(b'hidden: false', ui) self.assertIn(b'required: false', ui) def test_requirements_property(self): package = mpl_package.MuranoPlPackage(None, None, self.source_directory, self.manifest) requirements = package.requirements self.assertEqual(requirements, {}) # Override the Require property in cls.manifest and check new value. self.manifest['Require'] = {'murano.plugins.example': 0} package = mpl_package.MuranoPlPackage(None, None, self.source_directory, self.manifest) requirements = package.requirements self.assertEqual(requirements, {'murano.plugins.example': 0}) del self.manifest['Require'] def test_meta_property(self): package = mpl_package.MuranoPlPackage(None, None, self.source_directory, self.manifest) meta = package.meta self.assertEqual(meta, 'test.meta') def test_get_class(self): package = mpl_package.MuranoPlPackage(None, None, self.source_directory, self.manifest) stream, path = package.get_class('Class1') expected_path = os.path.join(self.source_directory, 'Classes', 'test.class1') self.assertIn(b'test.class1', stream) self.assertEqual(path, expected_path) def test_get_class_with_inappropriate_name(self): package = mpl_package.MuranoPlPackage(None, None, self.source_directory, self.manifest) self.assertRaises(exceptions.PackageClassLoadError, package.get_class, 'Invalid name') def test_get_class_with_nonexistent_class(self): package = mpl_package.MuranoPlPackage(None, None, self.source_directory, self.manifest) self.assertRaises(exceptions.PackageClassLoadError, package.get_class, 'Class2') ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/packages/test_exceptions.py0000664000175000017500000000360400000000000024117 0ustar00zuulzuul00000000000000# Copyright (c) 2016 AT&T Corp # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from murano.packages import exceptions import murano.tests.unit.base as test_base class TestExceptions(test_base.MuranoTestCase): def test_package_class_load_error(self): class_name = 'test class name' message = 'test message' error = exceptions.PackageClassLoadError(class_name=class_name, message=message) expected = 'Unable to load class "{0}" from package: {1}'\ .format(class_name, message) self.assertEqual(expected, error.args[0]) def test_package_ui_load_error(self): messages = ['', 'test_message'] for message in messages: error = exceptions.PackageUILoadError(message=message) expected = 'Unable to load ui definition from package' if message: expected += ': {0}'.format(message) self.assertEqual(expected, error.args[0]) def test_package_format_error(self): messages = ['', 'test_message'] for message in messages: error = exceptions.PackageFormatError(message=message) expected = 'Incorrect package format' if message: expected += ': {0}'.format(message) self.assertEqual(expected, error.args[0]) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/packages/test_load_utils.py0000664000175000017500000002150000000000000024070 0ustar00zuulzuul00000000000000# Copyright (c) 2016 AT&T Corp # 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. import os import random import shutil import string import tempfile from unittest import mock import yaml import zipfile from murano.packages import exceptions from murano.packages import load_utils import murano.tests.unit.base as test_base class TestLoadUtils(test_base.MuranoTestCase): def setUp(cls): super(TestLoadUtils, cls).setUp() cls.temp_directories = [] cls.temp_files = [] def _create_temp_dir(self): temp_directory = tempfile.mkdtemp() self.temp_directories.append(temp_directory) return temp_directory def _create_temp_file(self): temp_file = tempfile.NamedTemporaryFile(delete=True) self.temp_files.append(temp_file) return temp_file def _create_temp_zip_file(self, zip_path, manifest_path, arcname='manifest.yaml'): zip_ = zipfile.ZipFile(zip_path, 'w') zip_.write(manifest_path, arcname=arcname) zip_.close() self.temp_files.append(zip_) return zip_ def tearDown(cls): super(TestLoadUtils, cls).tearDown() for directory in cls.temp_directories: if os.path.isdir(directory): shutil.rmtree(directory) for file in cls.temp_files: if isinstance(file, zipfile.ZipFile): if zipfile.is_zipfile(file.filename): os.remove(file) else: if os.path.isfile(file.name): os.remove(file.name) def _test_load_from_file(self, target_dir=None, drop_dir=True): manifest_file_contents = dict( Format='MuranoPL/1.1', FullName='test_full_name', Type='Application', Description='test_description', Author='test_author', Supplier='test_supplier', Tags=[] ) test_directory = self._create_temp_dir() manifest_path = os.path.join(test_directory, 'manifest.yaml') zip_path = os.path.join(test_directory, 'test_zip_load_utils.zip') with open(manifest_path, 'w') as manifest_file: yaml.dump(manifest_file_contents, manifest_file, default_flow_style=True) self._create_temp_zip_file(zip_path, manifest_path) with load_utils.load_from_file(archive_path=zip_path, target_dir=target_dir, drop_dir=drop_dir) as plugin: self.assertEqual('MuranoPL', plugin.format_name) self.assertEqual('1.1.0', str(plugin.runtime_version)) self.assertEqual(manifest_file_contents['FullName'], plugin.full_name) self.assertEqual(manifest_file_contents['Description'], plugin.description) self.assertEqual(manifest_file_contents['Author'], plugin.author) self.assertEqual(manifest_file_contents['Supplier'], plugin.supplier) self.assertEqual(manifest_file_contents['Tags'], plugin.tags) def test_load_from_file(self): self._test_load_from_file(target_dir=None, drop_dir=True) def test_load_from_file_with_custom_target_directory(self): target_dir = self._create_temp_dir() self._test_load_from_file(target_dir=target_dir, drop_dir=True) @mock.patch('murano.packages.load_utils.get_plugin_loader') def test_load_from_file_with_invalid_handler(self, mock_plugin_loader): mock_plugin_loader().get_package_handler = mock.MagicMock( return_value=None) test_format = 'Invalid Format' manifest_file_contents = dict( Format=test_format, FullName='test_full_name', Type='Application', Description='test_description', Author='test_author', Supplier='test_supplier', Tags=[] ) test_directory = self._create_temp_dir() target_dir = self._create_temp_dir() manifest_path = os.path.join(test_directory, 'manifest.yaml') zip_path = os.path.join(test_directory, 'test_zip_load_utils.zip') with open(manifest_path, 'w') as manifest_file: yaml.dump(manifest_file_contents, manifest_file, default_flow_style=True) self._create_temp_zip_file(zip_path, manifest_path) expected_error_msg = "Unsupported format {0}".format(test_format) with self.assertRaisesRegex(exceptions.PackageLoadError, expected_error_msg): with load_utils.load_from_file(archive_path=zip_path, target_dir=target_dir, drop_dir=True): pass mock_plugin_loader().get_package_handler.assert_called_once_with( test_format) def test_load_from_file_with_invalid_archive_path(self): expected_error_msg = "Unable to find package file" with self.assertRaisesRegex(exceptions.PackageLoadError, expected_error_msg): with load_utils.load_from_file('invalid file path'): pass @mock.patch('murano.packages.load_utils.os') def test_load_from_file_with_nonempty_target_directory(self, mock_os): mock_os.listdir = mock.MagicMock(return_value=True) temp_file = self._create_temp_file() expected_error_msg = "Target directory is not empty" with self.assertRaisesRegex(exceptions.PackageLoadError, expected_error_msg): this_dir = os.path.dirname(os.path.realpath(__file__)) with load_utils.load_from_file(temp_file.name, target_dir=this_dir): pass def test_load_from_file_without_zip_file(self): temp_file = self._create_temp_file() expected_error_msg = "Uploaded file {0} is not a zip archive".\ format(temp_file.name) with self.assertRaisesRegex(exceptions.PackageLoadError, expected_error_msg): with load_utils.load_from_file(temp_file.name): pass @mock.patch('murano.packages.load_utils.zipfile') def test_load_from_file_handle_value_error(self, mock_zipfile): test_error_msg = 'Random error message.' expected_error_msg = "Couldn't load package from file: {0}".\ format(test_error_msg) mock_zipfile.is_zipfile = mock.MagicMock( side_effect=ValueError(test_error_msg)) temp_file = self._create_temp_file() with self.assertRaisesRegex(exceptions.PackageLoadError, expected_error_msg): with load_utils.load_from_file(temp_file.name): pass mock_zipfile.is_zipfile.assert_called_once_with( temp_file.name) def test_load_from_dir_without_source_directory(self): expected_error_msg = 'Invalid package directory' with self.assertRaisesRegex(exceptions.PackageLoadError, expected_error_msg): load_utils.load_from_dir('random_test_directory') def test_load_from_dir_with_invalid_source_directory(self): source_directory = self._create_temp_dir() expected_error_msg = 'Unable to find package manifest' with self.assertRaisesRegex(exceptions.PackageLoadError, expected_error_msg): load_utils.load_from_dir(source_directory) @mock.patch('murano.packages.load_utils.os.path.isfile') def test_load_from_dir_open_file_negative(self, mock_isfile): mock_isfile.return_value = True source_directory = self._create_temp_dir() random_filename = ''.join(random.choice(string.ascii_lowercase) for i in range(20)) expected_error_msg = 'Unable to load due to' with self.assertRaisesRegex(exceptions.PackageLoadError, expected_error_msg): load_utils.load_from_dir(source_directory, filename=random_filename) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/packages/test_package_base.py0000664000175000017500000001706100000000000024325 0ustar00zuulzuul00000000000000# Copyright (c) 2016 AT&T Corp # 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. import os import random import semantic_version import shutil import string import tempfile from unittest import mock from murano.packages import exceptions from murano.packages import package_base import murano.tests.unit.base as test_base class TestPackageBase(test_base.MuranoTestCase): @classmethod def setUpClass(cls): super(TestPackageBase, cls).setUpClass() package_base.PackageBase.__abstractmethods__ = set() cls.source_directory = tempfile.mkdtemp(dir=tempfile.tempdir) cls.version = semantic_version.Version.coerce('1.2.3') cls.mock_manifest = { 'Name': 'mock_display_name', 'FullName': 'mock_full_name', 'Type': 'Application', 'Version': '1.2.3', 'Description': 'test_description', 'Author': 'test_author', 'Supplier': 'test_supplier', 'Tags': ['tag1', 'tag2', 'tag3'], 'Logo': None } cls.package_base = package_base.PackageBase('test_format', 'test_runtime_version', cls.source_directory, cls.mock_manifest) @classmethod def tearDownClass(cls): if os.path.isdir(cls.source_directory): shutil.rmtree(cls.source_directory) def test_create_package_base_without_full_name(self): with self.assertRaisesRegex(exceptions.PackageFormatError, 'FullName is not specified'): package_base.PackageBase('test_format', 'test_runtime_version', 'test_source_directory', manifest={'FullName': None}) def test_create_package_base_with_invalid_full_name(self): full_names = ['.invalid_name_1', 'invalid..name..2', 'invalid name 3'] for full_name in full_names: expected_error_message = 'Invalid FullName {0}'.format(full_name) with self.assertRaisesRegex(exceptions.PackageFormatError, expected_error_message): package_base.PackageBase('test_format', 'test_runtime_version', 'test_source_directory', manifest={'FullName': full_name}) def test_create_package_base_with_invalid_type(self): package_type = 'Invalid' with self.assertRaisesRegex(exceptions.PackageFormatError, 'Invalid package Type {0}' .format(package_type)): package_base.PackageBase('test_format', 'test_runtime_version', 'test_source_directory', manifest={'FullName': 'mock_full_name', 'Type': package_type}) def test_requirements_negative(self): with self.assertRaisesRegex(NotImplementedError, None): self.package_base.requirements def test_classes_negative(self): with self.assertRaisesRegex(NotImplementedError, None): self.package_base.classes def test_get_class_negative(self): with self.assertRaisesRegex(NotImplementedError, None): self.package_base.get_class(None) def test_ui_negative(self): with self.assertRaisesRegex(NotImplementedError, None): self.package_base.ui def test_full_name(self): self.assertEqual(self.mock_manifest['FullName'], self.package_base.full_name) def test_source_directory(self): self.assertEqual(self.source_directory, self.package_base.source_directory) def test_version(self): self.assertEqual(self.version, self.package_base.version) def test_package_type(self): self.assertEqual(self.mock_manifest['Type'], self.package_base.package_type) def test_display_name(self): self.assertEqual(self.mock_manifest['Name'], self.package_base.display_name) def test_description(self): self.assertEqual(self.mock_manifest['Description'], self.package_base.description) def test_author(self): self.assertEqual(self.mock_manifest['Author'], self.package_base.author) def test_supplier(self): self.assertEqual(self.mock_manifest['Supplier'], self.package_base.supplier) def test_tags(self): self.assertEqual(self.mock_manifest['Tags'], self.package_base.tags) def test_logo_without_file_name(self): self.assertIsNone(self.package_base.logo) def test_logo_with_invalid_logo_path(self): expected_error_message = 'Unable to load logo' self.package_base._logo = ''.join(random.choice(string.ascii_letters) for _ in range(10)) with self.assertRaisesRegex(exceptions.PackageLoadError, expected_error_message): self.package_base.logo self.package_base._logo = self.mock_manifest['Logo'] @mock.patch('murano.packages.package_base.imghdr', what=mock.MagicMock(return_value='xyz')) def test_load_image_with_invalid_extension(self, mock_imghdr): expected_error_message = 'Unsupported Format.' with self.assertRaisesRegex(exceptions.PackageLoadError, expected_error_message): self.package_base._load_image('logo.xyz', 'logo.xyz', 'logo') full_path = os.path.join(self.package_base._source_directory, 'logo.xyz') mock_imghdr.what.assert_called_once_with(full_path) @mock.patch('murano.packages.package_base.imghdr', what=mock.MagicMock(return_value='png')) @mock.patch('murano.packages.package_base.os') def test_load_image_with_oversized_image(self, mock_os, mock_imghdr): mock_os.stat.return_value = mock.MagicMock(st_size=5000 * 1024) mock_os.isfile = mock.MagicMock(return_value=True) expected_error_message = 'Max allowed size is {0}'.format(500 * 1024) with self.assertRaisesRegex(exceptions.PackageLoadError, expected_error_message): self.package_base._load_image('logo.xyz', 'logo.xyz', 'logo') def test_meta(self): self.assertIsNone(self.package_base.meta) def test_get_resource(self): test_name = 'test_resource_name' expected_dir = os.path.join(self.source_directory, 'Resources', test_name) self.assertEqual(expected_dir, self.package_base.get_resource( test_name)) ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1696417899.6771803 murano-16.0.0/murano/tests/unit/packages/test_packages/0000775000175000017500000000000000000000000023137 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1696417899.8491812 murano-16.0.0/murano/tests/unit/packages/test_packages/test.hot.v1.app/0000775000175000017500000000000000000000000026013 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/packages/test_packages/test.hot.v1.app/manifest.yaml0000664000175000017500000000062300000000000030506 0ustar00zuulzuul00000000000000Format: Heat.HOT/1.0 Type: Application FullName: test.hot.v1.app Name: Test HOT v1 App Description: Test HOT v1 Application Author: Test Runner Tags: [Linux] Logo: test_logo.png Supplier: Name: Supplier Name CompanyUrl: Text: Example Company Link: http://example.com Logo: test_supplier_logo.png Summary: Company summary goes here Description: Marked up company description goes here ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/packages/test_packages/test.hot.v1.app/template.yaml0000664000175000017500000000027000000000000030511 0ustar00zuulzuul00000000000000heat_template_version: '2013-05-23' resources: test-server: type: OS::Nova::Server properties: flavor: test.flavor image: Some image name key_name: default ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/packages/test_packages/test.hot.v1.app/test_logo.png0000664000175000017500000000023500000000000030520 0ustar00zuulzuul00000000000000PNG  IHDRwSsRGBgAMA a pHYsodtEXtSoftwarePaint.NET v3.5.11GB7 IDATWc?5IENDB`././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/packages/test_packages/test.hot.v1.app/test_supplier_logo.png0000664000175000017500000000023500000000000032443 0ustar00zuulzuul00000000000000PNG  IHDRwSsRGBgAMA a pHYsodtEXtSoftwarePaint.NET v3.5.11GB7 IDATWc?5IENDB`././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1696417899.8531811 murano-16.0.0/murano/tests/unit/packages/test_packages/test.hot.v1.app_with_files/0000775000175000017500000000000000000000000030230 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1696417899.6771803 murano-16.0.0/murano/tests/unit/packages/test_packages/test.hot.v1.app_with_files/Resources/0000775000175000017500000000000000000000000032202 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000021300000000000011451 xustar0000000000000000111 path=murano-16.0.0/murano/tests/unit/packages/test_packages/test.hot.v1.app_with_files/Resources/HotFiles/ 28 mtime=1696417899.8531811 murano-16.0.0/murano/tests/unit/packages/test_packages/test.hot.v1.app_with_files/Resources/HotFiles0000775000175000017500000000000000000000000033640 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000022700000000000011456 xustar0000000000000000123 path=murano-16.0.0/murano/tests/unit/packages/test_packages/test.hot.v1.app_with_files/Resources/HotFiles/middle_file/ 28 mtime=1696417899.8531811 murano-16.0.0/murano/tests/unit/packages/test_packages/test.hot.v1.app_with_files/Resources/HotFiles0000775000175000017500000000000000000000000033640 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000024200000000000011453 xustar0000000000000000134 path=murano-16.0.0/murano/tests/unit/packages/test_packages/test.hot.v1.app_with_files/Resources/HotFiles/middle_file/inner_file/ 28 mtime=1696417899.8531811 murano-16.0.0/murano/tests/unit/packages/test_packages/test.hot.v1.app_with_files/Resources/HotFiles0000775000175000017500000000000000000000000033640 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000025000000000000011452 xustar0000000000000000146 path=murano-16.0.0/murano/tests/unit/packages/test_packages/test.hot.v1.app_with_files/Resources/HotFiles/middle_file/inner_file/testHeatFile 22 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/packages/test_packages/test.hot.v1.app_with_files/Resources/HotFiles0000664000175000017500000000001200000000000033633 0ustar00zuulzuul00000000000000inner file././@PaxHeader0000000000000000000000000000024300000000000011454 xustar0000000000000000135 path=murano-16.0.0/murano/tests/unit/packages/test_packages/test.hot.v1.app_with_files/Resources/HotFiles/middle_file/inner_file2/ 28 mtime=1696417899.8531811 murano-16.0.0/murano/tests/unit/packages/test_packages/test.hot.v1.app_with_files/Resources/HotFiles0000775000175000017500000000000000000000000033640 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000025100000000000011453 xustar0000000000000000147 path=murano-16.0.0/murano/tests/unit/packages/test_packages/test.hot.v1.app_with_files/Resources/HotFiles/middle_file/inner_file2/testHeatFile 22 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/packages/test_packages/test.hot.v1.app_with_files/Resources/HotFiles0000664000175000017500000000001400000000000033635 0ustar00zuulzuul00000000000000inner file 2././@PaxHeader0000000000000000000000000000023500000000000011455 xustar0000000000000000135 path=murano-16.0.0/murano/tests/unit/packages/test_packages/test.hot.v1.app_with_files/Resources/HotFiles/middle_file/testHeatFile 22 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/packages/test_packages/test.hot.v1.app_with_files/Resources/HotFiles0000664000175000017500000000001300000000000033634 0ustar00zuulzuul00000000000000middle file././@PaxHeader0000000000000000000000000000022100000000000011450 xustar0000000000000000123 path=murano-16.0.0/murano/tests/unit/packages/test_packages/test.hot.v1.app_with_files/Resources/HotFiles/testHeatFile 22 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/packages/test_packages/test.hot.v1.app_with_files/Resources/HotFiles0000664000175000017500000000000400000000000033634 0ustar00zuulzuul00000000000000file././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/packages/test_packages/test.hot.v1.app_with_files/manifest.yaml0000664000175000017500000000062300000000000032723 0ustar00zuulzuul00000000000000Format: Heat.HOT/1.0 Type: Application FullName: test.hot.v1.app Name: Test HOT v1 App Description: Test HOT v1 Application Author: Test Runner Tags: [Linux] Logo: test_logo.png Supplier: Name: Supplier Name CompanyUrl: Text: Example Company Link: http://example.com Logo: test_supplier_logo.png Summary: Company summary goes here Description: Marked up company description goes here ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/packages/test_packages/test.hot.v1.app_with_files/template.yaml0000664000175000017500000000027000000000000032726 0ustar00zuulzuul00000000000000heat_template_version: '2013-05-23' resources: test-server: type: OS::Nova::Server properties: flavor: test.flavor image: Some image name key_name: default ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1696417899.8531811 murano-16.0.0/murano/tests/unit/packages/test_packages/test.mpl.v1.app/0000775000175000017500000000000000000000000026011 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1696417899.8531811 murano-16.0.0/murano/tests/unit/packages/test_packages/test.mpl.v1.app/Classes/0000775000175000017500000000000000000000000027406 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/packages/test_packages/test.mpl.v1.app/Classes/Thing.yaml0000664000175000017500000000005700000000000031345 0ustar00zuulzuul00000000000000Namespaces: =: test.mpl.v1.app Name: Thing ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/packages/test_packages/test.mpl.v1.app/manifest.yaml0000664000175000017500000000065200000000000030506 0ustar00zuulzuul00000000000000Format: 1.0 Type: Application FullName: test.mpl.v1.app Description: Test V1 Application Author: Test runner Tags: [Linux] Classes: test.mpl.v1.app.Thing: Thing.yaml Logo: test_logo.png UI: ui.yaml Supplier: Name: Supplier Name CompanyUrl: Text: Example Company Link: http://example.com Logo: test_supplier_logo.png Summary: Company summary goes here Description: Marked up company description goes here ././@PaxHeader0000000000000000000000000000020500000000000011452 xustar0000000000000000111 path=murano-16.0.0/murano/tests/unit/packages/test_packages/test.mpl.v1.app/manifest_with_broken_logo.yaml 22 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/packages/test_packages/test.mpl.v1.app/manifest_with_broken_logo.yam0000664000175000017500000000066400000000000033750 0ustar00zuulzuul00000000000000Format: 1.0 Type: Application FullName: test.mpl.v1.app Description: Test V1 Application Author: Test runner Tags: [Linux] Classes: test.mpl.v1.app.Thing: Thing.yaml Logo: test_logo.png.not_valid UI: ui.yaml Supplier: Name: Supplier Name CompanyUrl: Text: Example Company Link: http://example.com Logo: test_supplier_logo.png Summary: Company summary goes here Description: Marked up company description goes here ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/packages/test_packages/test.mpl.v1.app/test_logo.png0000664000175000017500000000023500000000000030516 0ustar00zuulzuul00000000000000PNG  IHDRwSsRGBgAMA a pHYsodtEXtSoftwarePaint.NET v3.5.11GB7 IDATWc?5IENDB`././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/packages/test_packages/test.mpl.v1.app/test_logo.png.not_valid0000664000175000017500000000032200000000000032471 0ustar00zuulzuul00000000000000�NOT_VALID  IHDR�wS�sRGB���gAMA�� �a pHYs���o�dtEXtSoftwarePaint.NET v3.5.11G�B7 IDATWc���?���5��IEND�B`�././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/packages/test_packages/test.mpl.v1.app/test_supplier_logo.png0000664000175000017500000000023500000000000032441 0ustar00zuulzuul00000000000000PNG  IHDRwSsRGBgAMA a pHYsodtEXtSoftwarePaint.NET v3.5.11GB7 IDATWc?5IENDB`././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1696417899.8531811 murano-16.0.0/murano/tests/unit/packages/versions/0000775000175000017500000000000000000000000022172 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/packages/versions/__init__.py0000664000175000017500000000000000000000000024271 0ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/packages/versions/test_hot_v1.py0000664000175000017500000000315100000000000025003 0ustar00zuulzuul00000000000000# # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # 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 imghdr import os import murano.packages.load_utils as load_utils import murano.tests.unit.base as test_base class TestHotV1(test_base.MuranoTestCase): def test_supplier_info_load(self): package_dir = os.path.abspath( os.path.join(__file__, '../../test_packages/test.hot.v1.app') ) package = load_utils.load_from_dir(package_dir) self.assertIsNotNone(package.supplier) self.assertEqual('Supplier Name', package.supplier['Name']) self.assertEqual({'Link': 'http://example.com', 'Text': 'Example Company'}, package.supplier['CompanyUrl']) self.assertEqual( 'Company summary goes here', package.supplier['Summary'] ) self.assertEqual( 'Marked up company description goes here', package.supplier['Description'] ) self.assertEqual('test_supplier_logo.png', package.supplier['Logo']) self.assertEqual('png', imghdr.what('', package.supplier_logo)) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/packages/versions/test_mpl_v1.py0000664000175000017500000000314700000000000025006 0ustar00zuulzuul00000000000000# # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # 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 imghdr import os import murano.packages.load_utils as load_utils import murano.tests.unit.base as test_base class TestMplV1(test_base.MuranoTestCase): def test_supplier_info_load(self): package_dir = os.path.abspath( os.path.join(__file__, '../../test_packages/test.mpl.v1.app') ) package = load_utils.load_from_dir(package_dir) self.assertIsNotNone(package.supplier) self.assertEqual('Supplier Name', package.supplier['Name']) self.assertEqual({'Link': 'http://example.com', 'Text': 'Example Company'}, package.supplier['CompanyUrl']) self.assertEqual( 'Company summary goes here', package.supplier['Summary'] ) self.assertEqual( 'Marked up company description goes here', package.supplier['Description'] ) self.assertEqual('test_supplier_logo.png', package.supplier['Logo']) self.assertEqual('png', imghdr.what('', package.supplier_logo)) ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1696417899.857181 murano-16.0.0/murano/tests/unit/policy/0000775000175000017500000000000000000000000020043 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/policy/__init__.py0000664000175000017500000000000000000000000022142 0ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/policy/expected_rules_model.txt0000664000175000017500000000064200000000000025001 0ustar00zuulzuul00000000000000murano:objects+("0c810278-7282-4e4a-9d69-7b4c36b6ce6f", "c86104748a0c4907b4c5981e6d3bce9f", "io.murano.apps.linux.Git") murano:properties+("0c810278-7282-4e4a-9d69-7b4c36b6ce6f", "name", "git1") murano:objects+("b840b71e-1805-46c5-9e6f-5a3d2c8d773e", "0c810278-7282-4e4a-9d69-7b4c36b6ce6f", "io.murano.resources.LinuxMuranoInstance") murano:properties+("b840b71e-1805-46c5-9e6f-5a3d2c8d773e", "name", "whjiyi3uzhxes6")././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/policy/expected_rules_model_complex.txt0000664000175000017500000000072200000000000026527 0ustar00zuulzuul00000000000000murano:properties+("ade378ce-00d4-4a33-99eb-7b4b6ea3ab97", "ipAddresses", "10.0.1.13") murano:properties+("ade378ce-00d4-4a33-99eb-7b4b6ea3ab97", "ipAddresses", "16.60.90.90") murano:properties+("ade378ce-00d4-4a33-99eb-7b4b6ea3ab97", "networks.customNetworks", "10.0.1.0") murano:properties+("ade378ce-00d4-4a33-99eb-7b4b6ea3ab97", "networks.customNetworks", "10.0.2.0") murano:properties+("ade378ce-00d4-4a33-99eb-7b4b6ea3ab97", "networks.customProp1.prop", "val")././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/policy/expected_rules_model_renamed.txt0000664000175000017500000000064200000000000026474 0ustar00zuulzuul00000000000000murano:objects+("0c810278-7282-4e4a-9d69-7b4c36b6ce6f", "c86104748a0c4907b4c5981e6d3bce9f", "io.murano.apps.linux.Git") murano:properties+("0c810278-7282-4e4a-9d69-7b4c36b6ce6f", "name", "git1") murano:objects+("b840b71e-1805-46c5-9e6f-5a3d2c8d773e", "0c810278-7282-4e4a-9d69-7b4c36b6ce6f", "io.murano.resources.LinuxMuranoInstance") murano:properties+("b840b71e-1805-46c5-9e6f-5a3d2c8d773e", "name", "whjiyi3uzhxes6")././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/policy/expected_rules_model_two_instances.txt0000664000175000017500000000024300000000000027736 0ustar00zuulzuul00000000000000murano:properties+("824b1718-09d8-4dd3-be32-9886f0d146d7", "flavor", "m1.medium") murano:properties+("afa3266c-e2a7-4822-a176-11a48cdd7949", "flavor", "m1.medium")././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/policy/expected_rules_wordpress.txt0000664000175000017500000003202400000000000025730 0ustar00zuulzuul00000000000000murano:objects+("83bff5acf8354816b08cf9b4917c898d", "de305d5475b4431badb2eb6b9e546013", "io.murano.Environment") murano:properties+("83bff5acf8354816b08cf9b4917c898d", "name", "wordpress-env") murano:parent_types+("83bff5acf8354816b08cf9b4917c898d", "io.murano.Object") murano:objects+("c46770dec1db483ca2322914b842e50f", "83bff5acf8354816b08cf9b4917c898d", "io.murano.resources.NeutronNetwork") murano:properties+("c46770dec1db483ca2322914b842e50f", "name", "wordpress-env-network") murano:properties+("c46770dec1db483ca2322914b842e50f", "autogenerateSubnet", "True") murano:properties+("c46770dec1db483ca2322914b842e50f", "autoUplink", "True") murano:parent_types+("c46770dec1db483ca2322914b842e50f", "io.murano.resources.Network") murano:parent_types+("c46770dec1db483ca2322914b842e50f", "io.murano.Object") murano:objects+("e7a13d3c-b3c9-42fa-975d-a47b142fd233", "83bff5acf8354816b08cf9b4917c898d", "io.murano.databases.MySql") murano:properties+("e7a13d3c-b3c9-42fa-975d-a47b142fd233", "username", "admin") murano:properties+("e7a13d3c-b3c9-42fa-975d-a47b142fd233", "name", "MySqlDB") murano:properties+("e7a13d3c-b3c9-42fa-975d-a47b142fd233", "database", "wordpress") murano:properties+("e7a13d3c-b3c9-42fa-975d-a47b142fd233", "password", "Adminadmin#1") murano:parent_types+("e7a13d3c-b3c9-42fa-975d-a47b142fd233", "io.murano.databases.SqlDatabase") murano:parent_types+("e7a13d3c-b3c9-42fa-975d-a47b142fd233", "io.murano.Object") murano:parent_types+("e7a13d3c-b3c9-42fa-975d-a47b142fd233", "io.murano.Application") murano:objects+("825dc61d-217a-4fd8-80fc-43807f8d6fa2", "e7a13d3c-b3c9-42fa-975d-a47b142fd233", "io.murano.resources.LinuxMuranoInstance") murano:properties+("825dc61d-217a-4fd8-80fc-43807f8d6fa2", "name", "qgijhi4uwe5wd8") murano:properties+("825dc61d-217a-4fd8-80fc-43807f8d6fa2", "assignFloatingIp", "False") murano:properties+("825dc61d-217a-4fd8-80fc-43807f8d6fa2", "networks.useFlatNetwork", "False") murano:properties+("825dc61d-217a-4fd8-80fc-43807f8d6fa2", "networks.useEnvironmentNetwork", "True") murano:properties+("825dc61d-217a-4fd8-80fc-43807f8d6fa2", "flavor", "m1.small") murano:properties+("825dc61d-217a-4fd8-80fc-43807f8d6fa2", "image", "murano-ubuntu") murano:parent_types+("825dc61d-217a-4fd8-80fc-43807f8d6fa2", "io.murano.resources.LinuxInstance") murano:parent_types+("825dc61d-217a-4fd8-80fc-43807f8d6fa2", "io.murano.Object") murano:parent_types+("825dc61d-217a-4fd8-80fc-43807f8d6fa2", "io.murano.resources.Instance") murano:objects+("d224db7d-081d-47a4-9333-9d2677b90b1f", "83bff5acf8354816b08cf9b4917c898d", "io.murano.apps.apache.ApacheHttpServer") murano:properties+("d224db7d-081d-47a4-9333-9d2677b90b1f", "name", "ApacheHttpServer") murano:properties+("d224db7d-081d-47a4-9333-9d2677b90b1f", "enablePHP", "True") murano:parent_types+("d224db7d-081d-47a4-9333-9d2677b90b1f", "io.murano.Object") murano:parent_types+("d224db7d-081d-47a4-9333-9d2677b90b1f", "io.murano.Application") murano:objects+("3ddd4945-e4b8-4dac-9f85-537fc0957151", "d224db7d-081d-47a4-9333-9d2677b90b1f", "io.murano.resources.LinuxMuranoInstance") murano:properties+("3ddd4945-e4b8-4dac-9f85-537fc0957151", "name", "yeqsbi4uwejfg7") murano:properties+("3ddd4945-e4b8-4dac-9f85-537fc0957151", "assignFloatingIp", "False") murano:properties+("3ddd4945-e4b8-4dac-9f85-537fc0957151", "networks.useFlatNetwork", "False") murano:properties+("3ddd4945-e4b8-4dac-9f85-537fc0957151", "networks.useEnvironmentNetwork", "True") murano:properties+("3ddd4945-e4b8-4dac-9f85-537fc0957151", "flavor", "m1.small") murano:properties+("3ddd4945-e4b8-4dac-9f85-537fc0957151", "image", "murano-ubuntu") murano:parent_types+("3ddd4945-e4b8-4dac-9f85-537fc0957151", "io.murano.resources.LinuxInstance") murano:parent_types+("3ddd4945-e4b8-4dac-9f85-537fc0957151", "io.murano.Object") murano:parent_types+("3ddd4945-e4b8-4dac-9f85-537fc0957151", "io.murano.resources.Instance") murano:objects+("33e91790-5c44-40ce-9292-9dd4856325a0", "83bff5acf8354816b08cf9b4917c898d", "io.murano.apps.ZabbixServer") murano:properties+("33e91790-5c44-40ce-9292-9dd4856325a0", "username", "zabbix") murano:properties+("33e91790-5c44-40ce-9292-9dd4856325a0", "name", "ZabbixServer") murano:properties+("33e91790-5c44-40ce-9292-9dd4856325a0", "database", "zabbix") murano:properties+("33e91790-5c44-40ce-9292-9dd4856325a0", "password", "Adminadmin#1") murano:parent_types+("33e91790-5c44-40ce-9292-9dd4856325a0", "io.murano.Object") murano:parent_types+("33e91790-5c44-40ce-9292-9dd4856325a0", "io.murano.Application") murano:objects+("0b568a74-66c9-4e73-84d8-7dd1b96066ec", "33e91790-5c44-40ce-9292-9dd4856325a0", "io.murano.resources.LinuxMuranoInstance") murano:properties+("0b568a74-66c9-4e73-84d8-7dd1b96066ec", "name", "gzxgdi4uwfjt57") murano:properties+("0b568a74-66c9-4e73-84d8-7dd1b96066ec", "assignFloatingIp", "False") murano:properties+("0b568a74-66c9-4e73-84d8-7dd1b96066ec", "networks.useFlatNetwork", "False") murano:properties+("0b568a74-66c9-4e73-84d8-7dd1b96066ec", "networks.useEnvironmentNetwork", "True") murano:properties+("0b568a74-66c9-4e73-84d8-7dd1b96066ec", "flavor", "m1.small") murano:properties+("0b568a74-66c9-4e73-84d8-7dd1b96066ec", "image", "murano-ubuntu") murano:parent_types+("0b568a74-66c9-4e73-84d8-7dd1b96066ec", "io.murano.resources.LinuxInstance") murano:parent_types+("0b568a74-66c9-4e73-84d8-7dd1b96066ec", "io.murano.Object") murano:parent_types+("0b568a74-66c9-4e73-84d8-7dd1b96066ec", "io.murano.resources.Instance") murano:objects+("19a87de5-41ce-4e63-bf43-0e25dd409a1e", "83bff5acf8354816b08cf9b4917c898d", "io.murano.apps.ZabbixAgent") murano:properties+("19a87de5-41ce-4e63-bf43-0e25dd409a1e", "probe", "ICMP") murano:properties+("19a87de5-41ce-4e63-bf43-0e25dd409a1e", "hostname", "zabbix") murano:properties+("19a87de5-41ce-4e63-bf43-0e25dd409a1e", "name", "ZabbixAgent") murano:parent_types+("19a87de5-41ce-4e63-bf43-0e25dd409a1e", "io.murano.Object") murano:parent_types+("19a87de5-41ce-4e63-bf43-0e25dd409a1e", "io.murano.Application") murano:objects+("33e91790-5c44-40ce-9292-9dd4856325a0", "83bff5acf8354816b08cf9b4917c898d", "io.murano.apps.ZabbixServer") murano:properties+("33e91790-5c44-40ce-9292-9dd4856325a0", "username", "zabbix") murano:properties+("33e91790-5c44-40ce-9292-9dd4856325a0", "name", "ZabbixServer") murano:properties+("33e91790-5c44-40ce-9292-9dd4856325a0", "database", "zabbix") murano:properties+("33e91790-5c44-40ce-9292-9dd4856325a0", "password", "Adminadmin#1") murano:parent_types+("33e91790-5c44-40ce-9292-9dd4856325a0", "io.murano.Object") murano:parent_types+("33e91790-5c44-40ce-9292-9dd4856325a0", "io.murano.Application") murano:objects+("0b568a74-66c9-4e73-84d8-7dd1b96066ec", "33e91790-5c44-40ce-9292-9dd4856325a0", "io.murano.resources.LinuxMuranoInstance") murano:properties+("0b568a74-66c9-4e73-84d8-7dd1b96066ec", "name", "gzxgdi4uwfjt57") murano:properties+("0b568a74-66c9-4e73-84d8-7dd1b96066ec", "assignFloatingIp", "False") murano:properties+("0b568a74-66c9-4e73-84d8-7dd1b96066ec", "networks.useFlatNetwork", "False") murano:properties+("0b568a74-66c9-4e73-84d8-7dd1b96066ec", "networks.useEnvironmentNetwork", "True") murano:properties+("0b568a74-66c9-4e73-84d8-7dd1b96066ec", "flavor", "m1.small") murano:properties+("0b568a74-66c9-4e73-84d8-7dd1b96066ec", "image", "murano-ubuntu") murano:parent_types+("0b568a74-66c9-4e73-84d8-7dd1b96066ec", "io.murano.resources.LinuxInstance") murano:parent_types+("0b568a74-66c9-4e73-84d8-7dd1b96066ec", "io.murano.Object") murano:parent_types+("0b568a74-66c9-4e73-84d8-7dd1b96066ec", "io.murano.resources.Instance") murano:objects+("fec71a35-8abc-4a8f-a5e4-91e77854d761", "83bff5acf8354816b08cf9b4917c898d", "io.murano.apps.WordPress") murano:properties+("fec71a35-8abc-4a8f-a5e4-91e77854d761", "name", "WordPress") murano:properties+("fec71a35-8abc-4a8f-a5e4-91e77854d761", "dbPassword", "Adminadmin#1") murano:properties+("fec71a35-8abc-4a8f-a5e4-91e77854d761", "dbUser", "admin") murano:properties+("fec71a35-8abc-4a8f-a5e4-91e77854d761", "dbName", "wordpress") murano:parent_types+("fec71a35-8abc-4a8f-a5e4-91e77854d761", "io.murano.Object") murano:parent_types+("fec71a35-8abc-4a8f-a5e4-91e77854d761", "io.murano.Application") murano:objects+("19a87de5-41ce-4e63-bf43-0e25dd409a1e", "83bff5acf8354816b08cf9b4917c898d", "io.murano.apps.ZabbixAgent") murano:properties+("19a87de5-41ce-4e63-bf43-0e25dd409a1e", "probe", "ICMP") murano:properties+("19a87de5-41ce-4e63-bf43-0e25dd409a1e", "hostname", "zabbix") murano:properties+("19a87de5-41ce-4e63-bf43-0e25dd409a1e", "name", "ZabbixAgent") murano:parent_types+("19a87de5-41ce-4e63-bf43-0e25dd409a1e", "io.murano.Object") murano:parent_types+("19a87de5-41ce-4e63-bf43-0e25dd409a1e", "io.murano.Application") murano:objects+("33e91790-5c44-40ce-9292-9dd4856325a0", "83bff5acf8354816b08cf9b4917c898d", "io.murano.apps.ZabbixServer") murano:properties+("33e91790-5c44-40ce-9292-9dd4856325a0", "username", "zabbix") murano:properties+("33e91790-5c44-40ce-9292-9dd4856325a0", "name", "ZabbixServer") murano:properties+("33e91790-5c44-40ce-9292-9dd4856325a0", "database", "zabbix") murano:properties+("33e91790-5c44-40ce-9292-9dd4856325a0", "password", "Adminadmin#1") murano:parent_types+("33e91790-5c44-40ce-9292-9dd4856325a0", "io.murano.Object") murano:parent_types+("33e91790-5c44-40ce-9292-9dd4856325a0", "io.murano.Application") murano:objects+("0b568a74-66c9-4e73-84d8-7dd1b96066ec", "33e91790-5c44-40ce-9292-9dd4856325a0", "io.murano.resources.LinuxMuranoInstance") murano:properties+("0b568a74-66c9-4e73-84d8-7dd1b96066ec", "name", "gzxgdi4uwfjt57") murano:properties+("0b568a74-66c9-4e73-84d8-7dd1b96066ec", "assignFloatingIp", "False") murano:properties+("0b568a74-66c9-4e73-84d8-7dd1b96066ec", "networks.useFlatNetwork", "False") murano:properties+("0b568a74-66c9-4e73-84d8-7dd1b96066ec", "networks.useEnvironmentNetwork", "True") murano:properties+("0b568a74-66c9-4e73-84d8-7dd1b96066ec", "flavor", "m1.small") murano:properties+("0b568a74-66c9-4e73-84d8-7dd1b96066ec", "image", "murano-ubuntu") murano:parent_types+("0b568a74-66c9-4e73-84d8-7dd1b96066ec", "io.murano.resources.LinuxInstance") murano:parent_types+("0b568a74-66c9-4e73-84d8-7dd1b96066ec", "io.murano.Object") murano:parent_types+("0b568a74-66c9-4e73-84d8-7dd1b96066ec", "io.murano.resources.Instance") murano:objects+("e7a13d3c-b3c9-42fa-975d-a47b142fd233", "83bff5acf8354816b08cf9b4917c898d", "io.murano.databases.MySql") murano:properties+("e7a13d3c-b3c9-42fa-975d-a47b142fd233", "username", "admin") murano:properties+("e7a13d3c-b3c9-42fa-975d-a47b142fd233", "name", "MySqlDB") murano:properties+("e7a13d3c-b3c9-42fa-975d-a47b142fd233", "database", "wordpress") murano:properties+("e7a13d3c-b3c9-42fa-975d-a47b142fd233", "password", "Adminadmin#1") murano:parent_types+("e7a13d3c-b3c9-42fa-975d-a47b142fd233", "io.murano.databases.SqlDatabase") murano:parent_types+("e7a13d3c-b3c9-42fa-975d-a47b142fd233", "io.murano.Object") murano:parent_types+("e7a13d3c-b3c9-42fa-975d-a47b142fd233", "io.murano.Application") murano:objects+("825dc61d-217a-4fd8-80fc-43807f8d6fa2", "e7a13d3c-b3c9-42fa-975d-a47b142fd233", "io.murano.resources.LinuxMuranoInstance") murano:properties+("825dc61d-217a-4fd8-80fc-43807f8d6fa2", "name", "qgijhi4uwe5wd8") murano:properties+("825dc61d-217a-4fd8-80fc-43807f8d6fa2", "assignFloatingIp", "False") murano:properties+("825dc61d-217a-4fd8-80fc-43807f8d6fa2", "networks.useFlatNetwork", "False") murano:properties+("825dc61d-217a-4fd8-80fc-43807f8d6fa2", "networks.useEnvironmentNetwork", "True") murano:properties+("825dc61d-217a-4fd8-80fc-43807f8d6fa2", "flavor", "m1.small") murano:properties+("825dc61d-217a-4fd8-80fc-43807f8d6fa2", "image", "murano-ubuntu") murano:parent_types+("825dc61d-217a-4fd8-80fc-43807f8d6fa2", "io.murano.resources.LinuxInstance") murano:parent_types+("825dc61d-217a-4fd8-80fc-43807f8d6fa2", "io.murano.Object") murano:parent_types+("825dc61d-217a-4fd8-80fc-43807f8d6fa2", "io.murano.resources.Instance") murano:objects+("d224db7d-081d-47a4-9333-9d2677b90b1f", "83bff5acf8354816b08cf9b4917c898d", "io.murano.apps.apache.ApacheHttpServer") murano:properties+("d224db7d-081d-47a4-9333-9d2677b90b1f", "name", "ApacheHttpServer") murano:properties+("d224db7d-081d-47a4-9333-9d2677b90b1f", "enablePHP", "True") murano:parent_types+("d224db7d-081d-47a4-9333-9d2677b90b1f", "io.murano.Object") murano:parent_types+("d224db7d-081d-47a4-9333-9d2677b90b1f", "io.murano.Application") murano:objects+("3ddd4945-e4b8-4dac-9f85-537fc0957151", "d224db7d-081d-47a4-9333-9d2677b90b1f", "io.murano.resources.LinuxMuranoInstance") murano:properties+("3ddd4945-e4b8-4dac-9f85-537fc0957151", "name", "yeqsbi4uwejfg7") murano:properties+("3ddd4945-e4b8-4dac-9f85-537fc0957151", "assignFloatingIp", "False") murano:properties+("3ddd4945-e4b8-4dac-9f85-537fc0957151", "networks.useFlatNetwork", "False") murano:properties+("3ddd4945-e4b8-4dac-9f85-537fc0957151", "networks.useEnvironmentNetwork", "True") murano:properties+("3ddd4945-e4b8-4dac-9f85-537fc0957151", "flavor", "m1.small") murano:properties+("3ddd4945-e4b8-4dac-9f85-537fc0957151", "image", "murano-ubuntu") murano:parent_types+("3ddd4945-e4b8-4dac-9f85-537fc0957151", "io.murano.resources.LinuxInstance") murano:parent_types+("3ddd4945-e4b8-4dac-9f85-537fc0957151", "io.murano.Object") murano:parent_types+("3ddd4945-e4b8-4dac-9f85-537fc0957151", "io.murano.resources.Instance")././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/policy/model.yaml0000664000175000017500000000123200000000000022025 0ustar00zuulzuul00000000000000'?': {id: c86104748a0c4907b4c5981e6d3bce9f, type: io.murano.Environment} applications: - '?': _26411a1861294160833743e45d0eaad9: {name: Git} id: 0c810278-7282-4e4a-9d69-7b4c36b6ce6f type: io.murano.apps.linux.Git instance: '?': {id: b840b71e-1805-46c5-9e6f-5a3d2c8d773e, type: io.murano.resources.LinuxMuranoInstance} assignFloatingIp: false flavor: m1.small image: murano-ubuntu keyname: '' name: whjiyi3uzhxes6 name: git1 repo: default.git defaultNetworks: environment: '?': {id: 9d89844f20a642e0a1148e1fb8fd9e2e, type: io.murano.resources.NeutronNetwork} name: quick-env-1-network flat: null name: quick-env-1././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/policy/model_complex.yaml0000664000175000017500000000277700000000000023573 0ustar00zuulzuul00000000000000'?': {id: 75a7423a67384ca3a6ee9f7350f3552b, type: io.murano.Environment} applications: - '?': {id: 7cb96c80-1cd7-40bc-88fe-47b905069890, type: io.murano.apps.apache.TomcatCluster} balancer_ip: null clustered: null instances: - '?': {id: be3c5155-6670-4cf6-9a28-a4574ff70b71, type: io.murano.resources.LinuxMuranoInstance} assignFloatingIp: false flavor: m1.medium floatingIpAddress: null image: murano-ubuntu ipAddresses: [] keyname: '' name: gcthmi4jn23l68 networks: customNetworks: [] primaryNetwork: null useEnvironmentNetwork: true useFlatNetwork: false securityGroupName: null sharedIps: [] - '?': {id: ade378ce-00d4-4a33-99eb-7b4b6ea3ab97, type: io.murano.resources.LinuxMuranoInstance} assignFloatingIp: false flavor: m1.medium floatingIpAddress: null image: murano-ubuntu ipAddresses: [10.0.1.13, 16.60.90.90] keyname: '' name: bmtjbi4jn23l89 networks: customNetworks: [10.0.1.0, 10.0.2.0] primaryNetwork: null useEnvironmentNetwork: true useFlatNetwork: false customProp1: prop: val securityGroupName: null sharedIps: [] name: Tomcat port: 8080 public_ip: null defaultNetworks: environment: '?': {id: 7bac482a4c194b6e8dea81e6ce606db6, type: io.murano.resources.NeutronNetwork} autoUplink: true autogenerateSubnet: true dnsNameservers: null externalRouterId: null name: quick-env-12-network subnetCidr: null flat: null name: quick-env-12././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/policy/model_renamed.yaml0000664000175000017500000000122200000000000023517 0ustar00zuulzuul00000000000000'?': {id: c86104748a0c4907b4c5981e6d3bce9f, type: io.murano.Environment} apps: - '?': _26411a1861294160833743e45d0eaad9: {name: Git} id: 0c810278-7282-4e4a-9d69-7b4c36b6ce6f type: io.murano.apps.linux.Git hostedOn: '?': {id: b840b71e-1805-46c5-9e6f-5a3d2c8d773e, type: io.murano.resources.LinuxMuranoInstance} assignFloatingIp: false flavor: m1.small image: murano-ubuntu keyname: '' name: whjiyi3uzhxes6 name: git1 repo: default.git defaultNetworks: environment: '?': {id: 9d89844f20a642e0a1148e1fb8fd9e2e, type: io.murano.resources.NeutronNetwork} name: quick-env-1-network flat: null name: quick-env-1././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/policy/model_two_instances.yaml0000664000175000017500000000160600000000000024772 0ustar00zuulzuul00000000000000'?': {id: 00cc8fdf4cc54addb29133234d485edd, type: io.murano.Environment} applications: - '?': _26411a1861294160833743e45d0eaad9: {name: Apache Tomcat Cluster} id: 222256b6-07ef-4a69-a836-a6fe0df219e6 type: io.murano.apps.apache.TomcatCluster instances: - '?': {id: afa3266c-e2a7-4822-a176-11a48cdd7949, type: io.murano.resources.LinuxMuranoInstance} assignFloatingIp: false flavor: m1.medium image: murano-ubuntu keyname: '' name: qvamli4jgrkjn3 - '?': {id: 824b1718-09d8-4dd3-be32-9886f0d146d7, type: io.murano.resources.LinuxMuranoInstance} assignFloatingIp: false flavor: m1.medium image: murano-ubuntu keyname: '' name: krqlei4jgrkjo4 name: Tomcat-cluster defaultNetworks: environment: '?': {id: 854caae929e94290a2671281aef7ca9f, type: io.murano.resources.NeutronNetwork} name: quick-env-3-network flat: null name: quick-env-3././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/policy/model_with_relations.yaml0000664000175000017500000000267300000000000025152 0ustar00zuulzuul00000000000000'?': {id: 3409bdd0590e4c60b70fda5e6777ff96, type: io.murano.Environment} applications: - '?': _26411a1861294160833743e45d0eaad9: {name: MySQL} id: 0aafd67e-72e9-4ae0-bb62-fe724f77df2a type: io.murano.databases.MySql database: wordpress instance: '?': {id: ed8df2b0-ddd2-4009-b3c9-2e7a368f3cb8, type: io.murano.resources.LinuxMuranoInstance} assignFloatingIp: false flavor: m1.medium image: murano-ubuntu keyname: '' name: cgnvki4ji4tzo2 name: MySqlDB password: admin username: admin - '?': _26411a1861294160833743e45d0eaad9: {name: Apache HTTP Server} id: 8ce94f23-f16a-40a1-9d9d-a877266c315d type: io.murano.apps.apache.ApacheHttpServer enablePHP: true instance: '?': {id: fc6b8c41-166f-4fc9-a640-d82009e0a03d, type: io.murano.resources.LinuxMuranoInstance} assignFloatingIp: false flavor: m1.medium image: murano-ubuntu keyname: '' name: ntxoai4ji58e53 name: ApacheHttpServer - '?': _26411a1861294160833743e45d0eaad9: {name: WordPress} id: 50fa68ff-cd9a-4845-b573-2c80879d158d type: io.murano.apps.WordPress database: 0aafd67e-72e9-4ae0-bb62-fe724f77df2a dbName: wordpress dbPassword: admin dbUser: admin monitoring: null server: 8ce94f23-f16a-40a1-9d9d-a877266c315d defaultNetworks: environment: '?': {id: 119de5707de34fca92fd2d6cd48bc26c, type: io.murano.resources.NeutronNetwork} name: wordpress-env-network flat: null name: wordpress-env././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1696417899.857181 murano-16.0.0/murano/tests/unit/policy/modify/0000775000175000017500000000000000000000000021332 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/policy/modify/__init__.py0000664000175000017500000000000000000000000023431 0ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1696417899.857181 murano-16.0.0/murano/tests/unit/policy/modify/actions/0000775000175000017500000000000000000000000022772 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/policy/modify/actions/__init__.py0000664000175000017500000000000000000000000025071 0ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1696417899.857181 murano-16.0.0/murano/tests/unit/policy/modify/actions/meta/0000775000175000017500000000000000000000000023720 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/policy/modify/actions/meta/ModelExamples.yaml0000664000175000017500000000032700000000000027345 0ustar00zuulzuul00000000000000Name: ModelExamples Properties: sampleClass: Contract: $.class(SampleClass1) anotherSampleClass: Contract: $.class(SampleClass1) uninitialized: Contract: $.class(SampleClass1) Usage: Runtime ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/policy/modify/actions/meta/SampleClass1.yaml0000664000175000017500000000062400000000000027076 0ustar00zuulzuul00000000000000Name: SampleClass1 Properties: stringProperty: Contract: $.string().notNull() numberProperty: Contract: $.int().notNull() numberProperty: Contract: $.int() boolProperty: Contract: $.bool() classProperty: Contract: [$.class(SampleClass2).notNull()] dictProperty: Contract: {$.int(): $.string()} dictClassProperty: Contract: {$.string(): $.class(SampleClass2)} ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/policy/modify/actions/meta/SampleClass2.yaml0000664000175000017500000000012600000000000027074 0ustar00zuulzuul00000000000000Name: SampleClass2 Properties: class2Property: Contract: $.string().notNull() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/policy/modify/actions/test_action_manager.py0000664000175000017500000000506400000000000027357 0ustar00zuulzuul00000000000000# Copyright (c) 2015 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. from unittest import mock import murano.policy.modify.actions.action_manager as am from murano.policy.modify.actions import default_actions as da import murano.tests.unit.policy.modify.actions.test_default_actions as tda class TestActionManager(tda.ModifyActionTestCase): def test_loading(self): self.skipTest('skipped until proper fix') manager = am.ModifyActionManager() self.assertEqual(da.RemoveObjectAction, manager.load_action('remove-object')) self.assertEqual(da.SetPropertyAction, manager.load_action('set-property')) self.assertEqual(da.AddObjectAction, manager.load_action('add-object')) self.assertEqual(da.RemoveRelationAction, manager.load_action('remove-relation')) def test_caching(self): self.skipTest('skipped until proper fix') manager = am.ModifyActionManager() manager._load_action = mock.MagicMock(wraps=manager._load_action) manager.load_action('remove-object') # second call is expected to get cached value manager.load_action('remove-object') manager._load_action.assert_called_once_with('remove-object') def test_no_such_action(self): manager = am.ModifyActionManager() self.assertRaises(ValueError, manager.load_action, 'no-such-action') def test_action_apply(self): self.skipTest('skipped until proper fix') with self._runner.session(): manager = am.ModifyActionManager() obj_id = self._dict_member.id action_spec = 'remove-object: {object_id: %s}' % obj_id manager.apply_action(self._obj, action_spec) def test_action_apply_invalid_spec(self): manager = am.ModifyActionManager() self.assertRaises(ValueError, manager.apply_action, self._obj, 'remove-object') ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/policy/modify/actions/test_default_actions.py0000664000175000017500000001376200000000000027560 0ustar00zuulzuul00000000000000# Copyright (c) 2015 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. import murano.policy.modify.actions.default_actions as da import murano.tests.unit.dsl.foundation.object_model as om import murano.tests.unit.dsl.foundation.test_case as tc class ModifyActionTestCase(tc.DslTestCase): def setUp(self): super(ModifyActionTestCase, self).setUp() self._list_member = om.Object('SampleClass2', class2Property='string2') self._dict_member = om.Object('SampleClass2', class2Property='string2') self._runner = self.new_runner( om.Object( 'ModelExamples', sampleClass=om.Object( 'SampleClass1', stringProperty='string1', dictProperty={1: 'a', 2: 'b'}, dictClassProperty={ 'key': self._dict_member}, classProperty=[self._list_member]))) self._obj = self._runner.root class TestRemoveObjectAction(ModifyActionTestCase): def test_remove(self): with self._runner.session(): self.assertIsNotNone(self._obj.get_property('sampleClass')) object_id = self._obj.get_property('sampleClass').object_id da.RemoveObjectAction(object_id=object_id).modify(self._obj) self.assertIsNone(self._obj.get_property('sampleClass')) def test_remove_from_list(self): with self._runner.session(): self.assertEqual(1, len( self._obj.get_property('sampleClass') .get_property('classProperty'))) da.RemoveObjectAction(object_id=self._list_member.id).modify( self._obj) self.assertEqual(0, len( self._obj.get_property('sampleClass') .get_property('classProperty'))) self.assertNotIn(self._list_member.id, repr(self._obj)) def test_remove_from_dict(self): with self._runner.session(): self.assertEqual(1, len( self._obj.get_property('sampleClass') .get_property('dictClassProperty'))) da.RemoveObjectAction(object_id=self._dict_member.id).modify( self._obj) self.assertEqual(0, len( self._obj.get_property('sampleClass') .get_property('dictClassProperty'))) self.assertNotIn(self._dict_member.id, repr(self._obj)) def test_remove_not_exists(self): with self._runner.session(): action = da.RemoveObjectAction(object_id='not_exists') self.assertRaises(ValueError, action.modify, self._obj) class TestSetPropertyAction(ModifyActionTestCase): def test_set_str(self): self._test_set_value('stringProperty', 'test_string', 'test_string') def test_set_number(self): self._test_set_value('numberProperty', '15', 15) self._test_set_value('numberProperty', '50', 50) self._test_set_value('numberProperty', 40, 40) self._test_set_value('numberProperty', '-5', -5) def test_set_boolean(self): self._test_set_value('boolProperty', True, True) self._test_set_value('boolProperty', False, False) def test_set_dict(self): self._test_set_value('dictProperty', {1: 'a'}, {1: 'a'}) self._test_set_value('dictProperty', {1: 'b', 2: 'c'}, {1: 'b', 2: 'c'}) def _test_set_value(self, property, raw_input, expected): with self._runner.session(): sample = self._obj.get_property('sampleClass') da.SetPropertyAction(sample.object_id, prop_name=property, value=raw_input).modify(self._obj) self.assertEqual(expected, sample.get_property(property)) class TestRemoveRelationAction(ModifyActionTestCase): def test_remove(self): with self._runner.session(): self.assertIsNotNone(self._obj.get_property('sampleClass')) da.RemoveRelationAction(self._obj.object_id, prop_name='sampleClass').modify(self._obj) self.assertIsNone(self._obj.get_property('sampleClass')) class TestAddRelationAction(ModifyActionTestCase): def test_add(self): with self._runner.session(): sample = self._obj.get_property('sampleClass') self.assertIsNone(self._obj.get_property('anotherSampleClass')) da.AddRelationAction(source_id=self._obj.object_id, relation='anotherSampleClass', target_id=sample.object_id).modify(self._obj) self.assertIsNotNone(self._obj.get_property('anotherSampleClass')) rel_target = self._obj.get_property('anotherSampleClass').object_id self.assertEqual(sample.object_id, rel_target) class TestAddObjectAction(ModifyActionTestCase): def test_add_object(self): with self._runner.session(): self._obj.set_property('sampleClass', None) self.assertIsNone(self._obj.get_property('sampleClass')) da.AddObjectAction( self._obj.object_id, 'sampleClass', 'SampleClass1', {'stringProperty': 'test_add_obj'}).modify(self._obj) self.assertIsNotNone(self._obj.get_property('sampleClass')) self.assertEqual('test_add_obj', self._obj.get_property('sampleClass') .get_property('stringProperty')) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/policy/test_congress_rules.py0000664000175000017500000002457700000000000024530 0ustar00zuulzuul00000000000000# Copyright (c) 2014 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. import inspect import os.path import unittest import yaml from murano.common import uuidutils from murano.dsl import helpers from murano.dsl import package_loader import murano.policy.congress_rules as congress TENANT_ID = 'de305d5475b4431badb2eb6b9e546013' class MockPackageLoader(package_loader.MuranoPackageLoader): def __init__(self, rules): """Create rules like this: ['child->parent', 'child->parent2'].""" self._classes = {} rules_dict = {} for rule in rules: split = rule.split('->') rules_dict.setdefault(split[0], []).append(split[1]) classes = (self.get_class(cls, rules_dict) for cls in rules_dict) self._package = MockPackage(classes) def get_class(self, name, rules_dict): if name in self._classes: return self._classes[name] parents = [self.get_class(parent, rules_dict) for parent in rules_dict.get(name, [])] result = MockClass({'name': name, 'declared_parents': parents}) self._classes[name] = result return result def register_package(self, package): pass def load_class_package(self, class_name, version_spec): return self._package def load_package(self, package_name, version_spec): return self._package def export_fixation_table(self): pass def import_fixation_table(self, fixations): pass def compact_fixation_table(self): pass class MockPackage(object): def __init__(self, classes): self._classes = {} for cls in classes: self._classes[cls.name] = cls @property def classes(self): return self._classes.keys() def find_class(self, name, *args, **kwargs): return self._classes.get(name) class MockClass(object): def __init__(self, entries): self.__dict__.update(entries) def ancestors(self): return helpers.traverse(self, lambda t: t.declared_parents) class TestCongressRules(unittest.TestCase): def _load_file(self, file_name): model_file = os.path.join( os.path.dirname(inspect.getfile(self.__class__)), file_name) with open(model_file) as stream: return yaml.safe_load(stream) def _create_rules_str(self, model_file, package_loader=None): model = self._load_file(model_file) congress_rules = congress.CongressRulesManager() rules = congress_rules.convert(model, package_loader, tenant_id=TENANT_ID) rules_str = ", \n".join(map(str, rules)) return rules_str def test_transitive_closure(self): closure = congress.CongressRulesManager.transitive_closure( [(1, 2), (2, 3), (3, 4)]) self.assertIn((1, 4), closure) self.assertIn((2, 4), closure) def test_empty_model(self): congress_rules = congress.CongressRulesManager() rules = congress_rules.convert(None) self.assertEqual(0, len(rules)) def test_convert_simple_app(self): rules_str = self._create_and_check_rules_str('model') self.assertNotIn("instance.", rules_str) def test_convert_model_two_instances(self): rules_str = self._create_and_check_rules_str('model_two_instances') self.assertNotIn("\"instances\"", rules_str) def test_convert_model_with_relations(self): rules_str = self._create_rules_str('model_with_relations.yaml') self.assertNotIn( 'murano:properties+("50fa68ff-cd9a-4845-b573-2c80879d158d", ' '"server", "8ce94f23-f16a-40a1-9d9d-a877266c315d")', rules_str) self.assertIn( 'murano:relationships+("50fa68ff-cd9a-4845-b573-2c80879d158d", ' '"8ce94f23-f16a-40a1-9d9d-a877266c315d", "server")', rules_str) self.assertIn( 'murano:relationships+("0aafd67e-72e9-4ae0-bb62-fe724f77df2a", ' '"ed8df2b0-ddd2-4009-b3c9-2e7a368f3cb8", "instance")', rules_str) def test_convert_model_transitive_relationships(self): rules_str = self._create_rules_str('model_with_relations.yaml') self.assertIn( 'murano:connected+("50fa68ff-cd9a-4845-b573-2c80879d158d", ' '"8ce94f23-f16a-40a1-9d9d-a877266c315d")', rules_str) self.assertIn( 'murano:connected+("8ce94f23-f16a-40a1-9d9d-a877266c315d", ' '"fc6b8c41-166f-4fc9-a640-d82009e0a03d")', rules_str) def test_convert_model_services_relationship(self): rules_str = self._create_rules_str('model_with_relations.yaml') self.assertIn( 'murano:relationships+("3409bdd0590e4c60b70fda5e6777ff96", ' '"8ce94f23-f16a-40a1-9d9d-a877266c315d", "services")', rules_str) self.assertIn( 'murano:relationships+("3409bdd0590e4c60b70fda5e6777ff96", ' '"50fa68ff-cd9a-4845-b573-2c80879d158d", "services")', rules_str) def test_convert_model_complex(self): self._create_and_check_rules_str('model_complex') def test_convert_renamed_app(self): self._create_and_check_rules_str('model_renamed') def test_parent_types(self): # grand-parent # / \ # parent1 parent2 # \ / # io.murano.apps.linux.Git package_loader = MockPackageLoader([ 'io.murano.apps.linux.Git->parent1', 'io.murano.apps.linux.Git->parent2', 'parent1->grand-parent', 'parent2->grand-parent' ]) rules_str = self._create_rules_str('model.yaml', package_loader) self.assertIn( 'murano:parent_types+("0c810278-7282-4e4a-9d69-7b4c36b6ce6f",' ' "parent1")', rules_str) self.assertIn( 'murano:parent_types+("0c810278-7282-4e4a-9d69-7b4c36b6ce6f",' ' "parent2")', rules_str) self.assertIn( 'murano:parent_types+("0c810278-7282-4e4a-9d69-7b4c36b6ce6f",' ' "grand-parent")', rules_str) self.assertIn( 'murano:parent_types+("0c810278-7282-4e4a-9d69-7b4c36b6ce6f",' ' "io.murano.apps.linux.Git")', rules_str) def test_to_dictionary(self): """test to_dictionary If model contains object entry (not dict) we try to convert to dict using 'to_dictionary' method. """ class Struct(object): def __init__(self, d): self.__dict__ = d def to_dictionary(self): return self.__dict__ def __getitem__(self, item): return self.__dict__[item] d = {'?': {'id': '1', 'type': 't1'}, 'apps': [Struct({'?': {'id': '2', 'type': 't2'}, 'instances': [Struct( {'?': {'id': '3', 'type': 't3'}})]})] } model = Struct(d) congress_rules = congress.CongressRulesManager() tenant_id = uuidutils.generate_uuid() rules = congress_rules.convert(model, tenant_id=tenant_id) rules_str = ", \n".join(map(str, rules)) self.assertIn('murano:objects+("1", "{0}", "t1")'.format(tenant_id), rules_str) self.assertIn('murano:objects+("2", "1", "t2")', rules_str) self.assertIn('murano:objects+("3", "2", "t3")', rules_str) def test_environment_owner(self): model = self._load_file("model.yaml") congress_rules = congress.CongressRulesManager() rules = congress_rules.convert(model, tenant_id='tenant1') rules_str = ", \n".join(map(str, rules)) self.assertIn('murano:objects+("c86104748a0c4907b4c5981e6d3bce9f", ' '"tenant1", "io.murano.Environment")', rules_str) def test_wordpress(self): package_loader = MockPackageLoader([ 'io.murano.Environment->io.murano.Object', 'io.murano.resources.NeutronNetwork->io.murano.resources.Network', 'io.murano.resources.Network->io.murano.Object', 'io.murano.databases.MySql->io.murano.databases.SqlDatabase', 'io.murano.databases.MySql->io.murano.Application', 'io.murano.databases.SqlDatabase->io.murano.Object', 'io.murano.resources.LinuxInstance->io.murano.resources.Instance', 'io.murano.resources.Instance->io.murano.Object', 'io.murano.Application->io.murano.Object', 'io.murano.apps.apache.ApacheHttpServer->io.murano.Application', 'io.murano.apps.ZabbixServer->io.murano.Application', 'io.murano.apps.ZabbixAgent->io.murano.Application', 'io.murano.apps.WordPress->io.murano.Application', 'io.murano.resources.LinuxMuranoInstance->' 'io.murano.resources.LinuxInstance' ]) self._create_and_check_rules_str('wordpress', package_loader) def _create_and_check_rules_str(self, model_name, package_loader=None): rules_str = self._create_rules_str( '{0}.yaml'.format(model_name), package_loader) self._check_expected_rules(rules_str, 'expected_rules_{0}.txt'.format(model_name)) return rules_str def _check_expected_rules(self, rules_str, expected_rules_file_name): expected_rules_file = os.path.join( os.path.dirname(inspect.getfile(self.__class__)), expected_rules_file_name) s = '' with open(expected_rules_file) as f: for line in f: line = line.rstrip('\n') if line not in rules_str: s += 'Expected rule not found:\n\t' + line + '\n' if len(s) > 0: self.fail(s) def test_state_rule(self): rules_str = self._create_rules_str('model.yaml') self.assertIn( 'murano:states+("c86104748a0c4907b4c5981e6d3bce9f", "pending")', rules_str) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/policy/test_model_policy_enforcer.py0000664000175000017500000001256300000000000026025 0ustar00zuulzuul00000000000000# Copyright (c) 2014 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. try: import congressclient except ImportError: congressclient = None from unittest import mock from oslo_config import cfg from murano.common import engine from murano.policy import model_policy_enforcer from murano.tests.unit import base CONF = cfg.CONF class TestModelPolicyEnforcer(base.MuranoTestCase): def setUp(self): if not congressclient: self.skipTest('skipped ModelPolicyEnforcer tests') super(TestModelPolicyEnforcer, self).setUp() self.obj = mock.Mock() self.package_loader = mock.Mock() self.model_dict = mock.Mock() self.obj.to_dictionary = mock.Mock(return_value=self.model_dict) self.task = { 'action': {'method': 'deploy'}, 'model': {'Objects': None, 'project_id': 'tenant', 'user_id': 'user' }, 'token': 'token', 'project_id': 'tenant', 'user_id': 'user', 'id': 'environment.id' } self.congress_client_mock = \ mock.Mock(spec=congressclient.v1.client.Client) model_policy_enforcer.ModelPolicyEnforcer._create_client = mock.Mock( return_value=self.congress_client_mock) def test_enforcer_disabled(self): executor = engine.TaskExecutor(self.task) executor._model_policy_enforcer = mock.Mock() CONF.engine.enable_model_policy_enforcer = False executor._validate_model(self.obj, self.package_loader, None) self.assertFalse(executor._model_policy_enforcer.validate.called) def test_enforcer_enabled(self): executor = engine.TaskExecutor(self.task) executor._model_policy_enforcer = mock.Mock() CONF.engine.enable_model_policy_enforcer = True dsl_executor = mock.Mock() executor._validate_model(self.obj, self.package_loader, dsl_executor) executor._model_policy_enforcer \ .validate.assert_called_once_with(self.model_dict, self.package_loader) def test_validation_pass(self): self.congress_client_mock.execute_policy_action.return_value = \ {"result": []} model = {'?': {'id': '123', 'type': 'class'}} enforcer = model_policy_enforcer.ModelPolicyEnforcer(mock.Mock()) enforcer.validate(model) def test_validation_failure(self): self.congress_client_mock.execute_policy_action.return_value = \ {"result": ['predeploy_errors("123","instance1","failure")']} model = {'?': {'id': '123', 'type': 'class'}} enforcer = model_policy_enforcer.ModelPolicyEnforcer(mock.Mock()) self.assertRaises(model_policy_enforcer.ValidationError, enforcer.validate, model) def test_modify(self): model = {'?': {'id': '123', 'type': 'class'}} obj = mock.MagicMock() obj.to_dictionary = mock.Mock(return_value=model) self.congress_client_mock.execute_policy_action.return_value = \ {"result": [ 'predeploy_modify("123","instance1",' '"remove-object: {object_id: "12"}")']} action_manager = mock.MagicMock() enforcer = model_policy_enforcer.ModelPolicyEnforcer( mock.Mock(), action_manager) enforcer.modify(obj) self.assertTrue(action_manager.apply_action.called) def test_parse_result(self): congress_response = [ 'unexpected response', 'predeploy_errors("env1","instance1","Instance 1 has problem")', 'predeploy_errors("env1","instance1","Instance 2 has problem")', 'predeploy_errors("env2","instance1","Instance 3 has problem")' ] enforcer = model_policy_enforcer.ModelPolicyEnforcer(None) result = enforcer._parse_simulation_result( 'predeploy_errors', 'env1', congress_response) self.assertNotIn("unexpected response", result) self.assertIn("Instance 1 has problem", result) self.assertIn("Instance 2 has problem", result) self.assertNotIn("Instance 3 has problem", result) def test_none_model(self): executor = engine.TaskExecutor(self.task) executor._model_policy_enforcer = mock.Mock() CONF.engine.enable_model_policy_enforcer = True dsl_executor = mock.Mock() executor._validate_model(None, self.package_loader, dsl_executor) self.assertFalse(executor._model_policy_enforcer.modify.called) self.assertFalse(executor._model_policy_enforcer.validate.called) executor._validate_model(self.obj, self.package_loader, dsl_executor) self.assertTrue(executor._model_policy_enforcer.modify.called) self.assertTrue(executor._model_policy_enforcer.validate.called) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/policy/wordpress.yaml0000664000175000017500000001235200000000000022762 0ustar00zuulzuul00000000000000defaultNetworks: environment: '?': {id: c46770dec1db483ca2322914b842e50f, type: io.murano.resources.NeutronNetwork} autoUplink: true autogenerateSubnet: true dnsNameservers: null externalRouterId: null name: wordpress-env-network subnetCidr: null flat: null name: wordpress-env '?': {type: io.murano.Environment, id: 83bff5acf8354816b08cf9b4917c898d} applications: - '?': {id: e7a13d3c-b3c9-42fa-975d-a47b142fd233, type: io.murano.databases.MySql} database: wordpress instance: '?': {id: 825dc61d-217a-4fd8-80fc-43807f8d6fa2, type: io.murano.resources.LinuxMuranoInstance} assignFloatingIp: false flavor: m1.small floatingIpAddress: null image: murano-ubuntu ipAddresses: [] keyname: '' name: qgijhi4uwe5wd8 networks: customNetworks: [] primaryNetwork: null useEnvironmentNetwork: true useFlatNetwork: false securityGroupName: null sharedIps: [] name: MySqlDB password: Adminadmin#1 username: admin - '?': {id: d224db7d-081d-47a4-9333-9d2677b90b1f, type: io.murano.apps.apache.ApacheHttpServer} enablePHP: true instance: '?': {id: 3ddd4945-e4b8-4dac-9f85-537fc0957151, type: io.murano.resources.LinuxMuranoInstance} assignFloatingIp: false flavor: m1.small floatingIpAddress: null image: murano-ubuntu ipAddresses: [] keyname: '' name: yeqsbi4uwejfg7 networks: customNetworks: [] primaryNetwork: null useEnvironmentNetwork: true useFlatNetwork: false securityGroupName: null sharedIps: [] name: ApacheHttpServer - '?': {id: 33e91790-5c44-40ce-9292-9dd4856325a0, type: io.murano.apps.ZabbixServer} database: zabbix instance: '?': {id: 0b568a74-66c9-4e73-84d8-7dd1b96066ec, type: io.murano.resources.LinuxMuranoInstance} assignFloatingIp: false flavor: m1.small floatingIpAddress: null image: murano-ubuntu ipAddresses: [] keyname: '' name: gzxgdi4uwfjt57 networks: customNetworks: [] primaryNetwork: null useEnvironmentNetwork: true useFlatNetwork: false securityGroupName: null sharedIps: [] name: ZabbixServer password: Adminadmin#1 username: zabbix - '?': {id: 19a87de5-41ce-4e63-bf43-0e25dd409a1e, type: io.murano.apps.ZabbixAgent} hostname: zabbix name: ZabbixAgent probe: ICMP server: '?': {id: 33e91790-5c44-40ce-9292-9dd4856325a0, type: io.murano.apps.ZabbixServer} database: zabbix instance: '?': {id: 0b568a74-66c9-4e73-84d8-7dd1b96066ec, type: io.murano.resources.LinuxMuranoInstance} assignFloatingIp: false flavor: m1.small floatingIpAddress: null image: murano-ubuntu ipAddresses: [] keyname: '' name: gzxgdi4uwfjt57 networks: customNetworks: [] primaryNetwork: null useEnvironmentNetwork: true useFlatNetwork: false securityGroupName: null sharedIps: [] name: ZabbixServer password: Adminadmin#1 username: zabbix - '?': {id: fec71a35-8abc-4a8f-a5e4-91e77854d761, type: io.murano.apps.WordPress} database: '?': {id: e7a13d3c-b3c9-42fa-975d-a47b142fd233, type: io.murano.databases.MySql} database: wordpress instance: '?': {id: 825dc61d-217a-4fd8-80fc-43807f8d6fa2, type: io.murano.resources.LinuxMuranoInstance} assignFloatingIp: false flavor: m1.small floatingIpAddress: null image: murano-ubuntu ipAddresses: [] keyname: '' name: qgijhi4uwe5wd8 networks: customNetworks: [] primaryNetwork: null useEnvironmentNetwork: true useFlatNetwork: false securityGroupName: null sharedIps: [] name: MySqlDB password: Adminadmin#1 username: admin dbName: wordpress dbPassword: Adminadmin#1 dbUser: admin monitoring: '?': {id: 19a87de5-41ce-4e63-bf43-0e25dd409a1e, type: io.murano.apps.ZabbixAgent} hostname: zabbix name: ZabbixAgent probe: ICMP server: '?': {id: 33e91790-5c44-40ce-9292-9dd4856325a0, type: io.murano.apps.ZabbixServer} database: zabbix instance: '?': {id: 0b568a74-66c9-4e73-84d8-7dd1b96066ec, type: io.murano.resources.LinuxMuranoInstance} assignFloatingIp: false flavor: m1.small floatingIpAddress: null image: murano-ubuntu ipAddresses: [] keyname: '' name: gzxgdi4uwfjt57 networks: customNetworks: [] primaryNetwork: null useEnvironmentNetwork: true useFlatNetwork: false securityGroupName: null sharedIps: [] name: ZabbixServer password: Adminadmin#1 username: zabbix name: WordPress server: '?': {id: d224db7d-081d-47a4-9333-9d2677b90b1f, type: io.murano.apps.apache.ApacheHttpServer} enablePHP: true instance: '?': {id: 3ddd4945-e4b8-4dac-9f85-537fc0957151, type: io.murano.resources.LinuxMuranoInstance} assignFloatingIp: false flavor: m1.small floatingIpAddress: null image: murano-ubuntu ipAddresses: [] keyname: '' name: yeqsbi4uwejfg7 networks: customNetworks: [] primaryNetwork: null useEnvironmentNetwork: true useFlatNetwork: false securityGroupName: null sharedIps: [] name: ApacheHttpServer././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1696417899.857181 murano-16.0.0/murano/tests/unit/services/0000775000175000017500000000000000000000000020367 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/services/__init__.py0000664000175000017500000000000000000000000022466 0ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/services/test_actions.py0000664000175000017500000002347600000000000023454 0ustar00zuulzuul00000000000000# Copyright (c) 2016 AT&T Corp # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from unittest import mock from murano.services import actions from murano.services import states import murano.tests.unit.base as test_base class TestActions(test_base.MuranoTestCase): def test_create_action_task(self): mock_action_name = 'test_action_name' mock_target_obj = '123' mock_args = {'param1': 'something', 'param2': 'something else'} mock_environment = mock.MagicMock(id='456', tenant_id='789') mock_description = { 'Objects': { '?': { 'id': '456' }, 'applications': [], 'services': ['service1', 'service2'] }, 'project_id': 'XXX', 'user_id': 'YYY' } mock_session = mock.MagicMock(description=mock_description) mock_context = mock.Mock(auth_token='test_token', project_id='test_tenant', user='test_user') expected_task = { 'action': { 'object_id': mock_target_obj, 'method': mock_action_name, 'args': mock_args }, 'model': { 'Objects': { '?': { 'id': mock_environment.id }, 'applications': mock_session.description['Objects']['services'] }, 'project_id': 'XXX', 'user_id': 'YYY' }, 'token': 'test_token', 'project_id': 'test_tenant', 'user_id': 'test_user', 'id': mock_environment.id } task = actions.ActionServices.create_action_task(mock_action_name, mock_target_obj, mock_args, mock_environment, mock_session, mock_context) self.assertEqual(expected_task, task) @mock.patch('murano.services.actions.models') def test_update_task(self, mock_models): mock_models.Task = mock.MagicMock() mock_models.Status = mock.MagicMock() mock_action = [{}, {'name': 'test_action_name'}] mock_session = mock.MagicMock(environment_id='123', state=states.SessionState.OPENED, description={'Objects': ['o1', 'o2']}) mock_task = {'action': 'test_action_name'} mock_unit = mock.MagicMock(__enter__=mock.MagicMock(), __exit__=mock.MagicMock()) actions.ActionServices.update_task(mock_action, mock_session, mock_task, mock_unit) self.assertEqual(mock_session.environment_id, mock_models.Task().environment_id) self.assertEqual(dict(mock_session.description['Objects']), mock_models.Task().description) self.assertEqual(mock_task['action'], mock_models.Task().action) self.assertIn(mock_action[1]['name'], mock_models.Status().text) self.assertEqual('info', mock_models.Status().level) mock_models.Task().statuses.append.assert_called_once_with( mock_models.Status()) self.assertEqual(2, mock_unit.add.call_count) expected_session = mock_session expected_session.state = states.SessionState.DEPLOYED mock_unit.add.assert_any_call(expected_session) mock_unit.add.assert_called_with(mock_models.Task()) @mock.patch('murano.services.actions.rpc') @mock.patch('murano.services.actions.actions_db.update_task') @mock.patch('murano.services.actions.ActionServices.create_action_task') def test_submit_task(self, mock_create_action_task, mock_update_task, mock_rpc): mock_task = mock.MagicMock() mock_create_action_task.return_value = mock_task mock_update_task.return_value = '123' mock_rpc.engine().handle_task = mock.MagicMock() test_action_name = 'test_action_name' test_target_obj = 'test_target_obj' test_args = 'test_args' test_environment = 'test_environment' test_session = 'test_session' context = mock.Mock() context.auth_token = 'test_token' context.project_id = 'test_tenant' context.user = 'test_user' test_unit = 'test_unit' task_id = actions.ActionServices.submit_task(test_action_name, test_target_obj, test_args, test_environment, test_session, context, test_unit) self.assertEqual('123', task_id) mock_create_action_task.assert_called_once_with(test_action_name, test_target_obj, test_args, test_environment, test_session, context) mock_update_task.assert_called_once_with(test_action_name, test_session, mock_task, test_unit) mock_rpc.engine().handle_task.assert_called_once_with(mock_task) @mock.patch('murano.services.actions.actions_db.get_environment') @mock.patch('murano.services.actions.ActionServices.submit_task') def test_execute(self, mock_submit_task, mock_get_environment): mock_environment = mock.MagicMock() mock_task_id = 'test_task_id' mock_get_environment.return_value = mock_environment mock_submit_task.return_value = mock_task_id test_action_id = 'test_action_id' test_description = [{ '?': { 'id': 'test_obj_id', '_actions': { test_action_id: { 'name': 'test_action_1', 'enabled': True } } } }] test_session = mock.MagicMock(description=test_description) test_unit = 'test_unit' test_token = 'test_token' test_args = None expected_action_name = 'test_action_1' expected_target_obj = 'test_obj_id' task_id = actions.ActionServices.execute(test_action_id, test_session, test_unit, test_token, test_args) self.assertEqual(mock_task_id, task_id) mock_submit_task.assert_called_once_with(expected_action_name, expected_target_obj, {}, mock_environment, test_session, test_token, test_unit) @mock.patch('murano.services.actions.actions_db.get_environment') def test_execute_with_invalid_action_id(self, mock_get_environment): test_action_id = 'test_action_id' test_session = mock.MagicMock(description=[]) with self.assertRaisesRegex(LookupError, 'Action is not found'): actions.ActionServices.execute(test_action_id, test_session, None, None, None) @mock.patch('murano.services.actions.actions_db.get_environment') def test_execute_with_disabled_action(self, mock_get_environment): test_action_id = 'test_action_id' test_description = [{ '?': { 'id': 'test_obj_id', '_actions': { test_action_id: { 'name': 'test_action_1', 'enabled': False } } } }] test_session = mock.MagicMock(description=test_description) with self.assertRaisesRegex(ValueError, 'Cannot execute disabled action'): actions.ActionServices.execute(test_action_id, test_session, None, None, None) def test_get_result(self): mock_task = mock.MagicMock(result='test_result') mock_unit = mock.MagicMock() mock_unit.query().filter_by().first.return_value = mock_task result = actions.ActionServices.get_result('eid', 'tid', mock_unit) self.assertEqual(mock_task.result, result) mock_unit.query().filter_by().first.return_value = None task = actions.ActionServices.get_result('eid', 'tid', mock_unit) self.assertIsNone(task) mock_unit.query().filter_by.assert_any_call(id='tid', environment_id='eid') ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/test_actions.py0000664000175000017500000000334100000000000021616 0ustar00zuulzuul00000000000000# 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 murano.services import actions from murano.tests.unit import base class TestActionFinder(base.MuranoTestCase): def test_simple_root_level_search(self): model = { '?': { 'id': 'id1', '_actions': { 'ad_deploy': { 'enabled': True, 'name': 'deploy' } } } } action = actions.ActionServices.find_action(model, 'ad_deploy') self.assertEqual('deploy', action[1]['name']) def test_recursive_action_search(self): model = { '?': { 'id': 'id1', '_actions': {'ad_deploy': {'enabled': True, 'name': 'deploy'}} }, 'property': { '?': { 'id': 'id2', '_actions': { 'ad_scale': {'enabled': True, 'name': 'scale'} } }, } } action = actions.ActionServices.find_action(model, 'ad_scale') self.assertEqual('scale', action[1]['name']) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/test_engine.py0000664000175000017500000001360000000000000021422 0ustar00zuulzuul00000000000000# 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 from unittest import mock import semantic_version import yaql from yaql.language import exceptions from yaql.language import utils import murano.dsl.helpers as helpers import murano.dsl.namespace_resolver as ns_resolver import murano.dsl.yaql_expression as yaql_expression from murano.tests.unit import base ROOT_CLASS = 'io.murano.Object' class TestNamespaceResolving(base.MuranoTestCase): def test_fails_w_empty_name(self): resolver = ns_resolver.NamespaceResolver({'=': 'com.example.murano'}) self.assertRaises(ValueError, resolver.resolve_name, None) def test_fails_w_unknown_prefix(self): resolver = ns_resolver.NamespaceResolver({'=': 'com.example.murano'}) name = 'unknown_prefix:example.murano' self.assertRaises(KeyError, resolver.resolve_name, name) def test_fails_w_prefix_wo_name(self): resolver = ns_resolver.NamespaceResolver({'=': 'com.example.murano'}) name = 'sys:' self.assertRaises(ValueError, resolver.resolve_name, name) def test_fails_w_excessive_prefix(self): ns = {'sys': 'com.example.murano.system'} resolver = ns_resolver.NamespaceResolver(ns) invalid_name = 'sys:excessive_ns:muranoResource' self.assertRaises(ValueError, resolver.resolve_name, invalid_name) def test_empty_prefix_is_default(self): resolver = ns_resolver.NamespaceResolver({'=': 'com.example.murano'}) # name without prefix delimiter name = 'some.arbitrary.name' resolved_name = resolver.resolve_name(':' + name) self.assertEqual( 'com.example.murano.some.arbitrary.name', resolved_name) def test_resolves_specified_ns_prefix(self): ns = {'sys': 'com.example.murano.system'} resolver = ns_resolver.NamespaceResolver(ns) short_name, full_name = 'sys:File', 'com.example.murano.system.File' resolved_name = resolver.resolve_name(short_name) self.assertEqual(full_name, resolved_name) def test_resolves_current_ns(self): resolver = ns_resolver.NamespaceResolver({'=': 'com.example.murano'}) short_name, full_name = 'Resource', 'com.example.murano.Resource' resolved_name = resolver.resolve_name(short_name) self.assertEqual(full_name, resolved_name) def test_resolves_w_empty_namespaces(self): resolver = ns_resolver.NamespaceResolver({}) resolved_name = resolver.resolve_name('Resource') self.assertEqual('Resource', resolved_name) class TestHelperFunctions(base.MuranoTestCase): def test_generate_id(self): generated_id = helpers.generate_id() self.assertTrue(re.match(r'[a-z0-9]{32}', generated_id)) def test_evaluate(self): yaql_value = mock.Mock(yaql_expression.YaqlExpression, return_value='atom') complex_value = {yaql_value: ['some', (1, yaql_value), 'hi!'], 'sample': [yaql_value, range(5)]} complex_literal = utils.FrozenDict({ 'atom': ('some', (1, 'atom'), 'hi!'), 'sample': ('atom', (0, 1, 2, 3, 4)) }) context = yaql.create_context() evaluated_value = helpers.evaluate(yaql_value, context) evaluated_complex_value = helpers.evaluate(complex_value, context) self.assertEqual('atom', evaluated_value) self.assertEqual(complex_literal, evaluated_complex_value) class TestYaqlExpression(base.MuranoTestCase): def setUp(self): self._version = semantic_version.Version.coerce('1.0') super(TestYaqlExpression, self).setUp() def test_expression(self): yaql_expr = yaql_expression.YaqlExpression('string', self._version) self.assertEqual('string', yaql_expr.expression) def test_evaluate_calls(self): string = 'string' expected_calls = [mock.call(string, self._version), mock.call().evaluate(context=None)] with mock.patch('murano.dsl.yaql_integration.parse') as mock_parse: yaql_expr = yaql_expression.YaqlExpression(string, self._version) yaql_expr(None) self.assertEqual(expected_calls, mock_parse.mock_calls) def test_is_expression_returns(self): expr = yaql_expression.YaqlExpression('string', self._version) with mock.patch('murano.dsl.yaql_integration.parse'): self.assertTrue(expr.is_expression('$some', self._version)) self.assertTrue(expr.is_expression('$.someMore', self._version)) with mock.patch('murano.dsl.yaql_integration.parse') as parse_mock: parse_mock.side_effect = exceptions.YaqlGrammarException self.assertFalse(expr.is_expression('', self._version)) with mock.patch('murano.dsl.yaql_integration.parse') as parse_mock: parse_mock.side_effect = exceptions.YaqlLexicalException self.assertFalse(expr.is_expression('', self._version)) def test_property(self): self.assertRaises(TypeError, yaql_expression.YaqlExpression, None, self._version) expr = yaql_expression.YaqlExpression('string', self._version) self.assertEqual(expr._version, expr.version) self.assertIsNone(expr._file_position) yaql_rep = expr.__repr__() self.assertEqual('YAQL(%s)' % expr._expression, yaql_rep) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/test_hacking.py0000664000175000017500000001066400000000000021570 0ustar00zuulzuul00000000000000# Copyright 2015 Intel, 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 pycodestyle import textwrap from unittest import mock from murano.hacking import checks from murano.tests.unit import base class HackingTestCase(base.MuranoTestCase): """Tests the hacking checks in murano.hacking.checks This class tests the hacking checks in murano.hacking.checks by passing strings to the check methods like the pycodestyle/flake8 parser would. The parser loops over each line in the file and then passes the parameters to the check method. The parameter names in the check method dictate what type of object is passed to the check method. The parameter types are:: logical_line: A processed line with the following modifications: - Multi-line statements converted to a single line. - Stripped left and right. - Contents of strings replaced with "xxx" of same length. - Comments removed. physical_line: Raw line of text from the input file. lines: a list of the raw lines from the input file tokens: the tokens that contribute to this logical line line_number: line number in the input file total_lines: number of lines in the input file blank_lines: blank lines before this one indent_char: indentation character in this file (" " or "\t") indent_level: indentation (with tabs expanded to multiples of 8) previous_indent_level: indentation on previous line previous_logical: previous logical line filename: Path of the file being run through pycodestyle When running a test on a check method the return will be False/None if there is no violation in the sample input. If there is an error a tuple is returned with a position in the line, and a message. So to check the result just assertTrue if the check is expected to fail and assertFalse if it should pass. """ # We are patching pycodestyle so that only the check under test is actually # installed. @mock.patch('pycodestyle._checks', {'physical_line': {}, 'logical_line': {}, 'tree': {}}) def _run_check(self, code, checker, filename=None): pycodestyle.register_check(checker) lines = textwrap.dedent(code).strip().splitlines(True) checker = pycodestyle.Checker(filename=filename, lines=lines) checker.check_all() checker.report._deferred_print.sort() return checker.report._deferred_print def _assert_has_errors(self, code, checker, expected_errors=None, filename=None): actual_errors = [e[:3] for e in self._run_check(code, checker, filename)] self.assertEqual(expected_errors or [], actual_errors) def _assert_has_no_errors(self, code, checker, filename=None): self._assert_has_errors(code, checker, filename=filename) def test_assert_equal_none(self): errors = [(1, 0, "M318")] check = checks.assert_equal_none code = "self.assertEqual(A, None)" self._assert_has_errors(code, check, errors) code = "self.assertEqual(None, A)" self._assert_has_errors(code, check, errors) code = "self.assertIsNone()" self._assert_has_no_errors(code, check) def test_no_mutable_default_args(self): self.assertEqual(1, len(list(checks.no_mutable_default_args( "def get_info_from_bdm(virt_type, bdm, mapping=[])")))) self.assertEqual(0, len(list(checks.no_mutable_default_args( "defined = []")))) self.assertEqual(0, len(list(checks.no_mutable_default_args( "defined, undefined = [], {}")))) def test_check_no_basestring(self): self.assertEqual(1, len(list(checks.check_no_basestring( "isinstance('foo', basestring)")))) self.assertEqual(0, len(list(checks.check_no_basestring( "isinstance('foo', str)")))) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/test_heat_stack.py0000664000175000017500000006414600000000000022276 0ustar00zuulzuul00000000000000# Copyright (c) 2014 Hewlett-Packard Development Company, L.P. # 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 heatclient.v1 import stacks from unittest import mock from oslo_config import cfg from murano.engine.system import heat_stack from murano.tests.unit import base CLS_NAME = 'murano.engine.system.heat_stack.HeatStack' CONF = cfg.CONF class TestHeatStack(base.MuranoTestCase): def setUp(self): super(TestHeatStack, self).setUp() self.heat_client_mock = mock.Mock() self.heat_client_mock.stacks = mock.MagicMock(spec=stacks.StackManager) self.override_config('stack_tags', ['test-murano'], 'heat') self.mock_tag = ','.join(CONF.heat.stack_tags) self._patch_get_client() def tearDown(self): super(TestHeatStack, self).tearDown() self.addCleanup(mock.patch.stopall) def _patch_get_client(self): self.get_client_patcher = mock.patch( 'murano.engine.system.heat_stack.HeatStack._get_client', return_value=self.heat_client_mock) self.get_token_client_patcher = mock.patch.object( heat_stack.HeatStack, '_get_token_client', return_value=self.heat_client_mock) self.get_client_patcher.start() self.get_token_client_patcher.start() def _unpatch_get_client(self): self.get_client_patcher.stop() self.get_token_client_patcher.stop() @mock.patch(CLS_NAME + '._wait_state') @mock.patch(CLS_NAME + '._get_status') def test_push_adds_version(self, status_get, wait_st): """Assert that if heat_template_version is omitted, it's added.""" status_get.return_value = 'NOT_FOUND' wait_st.return_value = {} hs = heat_stack.HeatStack('test-stack', 'Generated by TestHeatStack') hs._template = {'resources': {'test': 1}} hs._files = {} hs._hot_environment = {} hs._parameters = {} hs._applied = False hs.push() hs = heat_stack.HeatStack( 'test-stack', 'Generated by TestHeatStack') hs._template = {'resources': {'test': 1}} hs._files = {} hs._parameters = {} hs._applied = False hs.push() expected_template = { 'heat_template_version': '2013-05-23', 'description': 'Generated by TestHeatStack', 'resources': {'test': 1} } self.heat_client_mock.stacks.create.assert_called_with( stack_name='test-stack', disable_rollback=True, parameters={}, template=expected_template, files={}, environment={}, tags=self.mock_tag ) self.assertTrue(hs._applied) @mock.patch(CLS_NAME + '._wait_state') @mock.patch(CLS_NAME + '._get_status') def test_description_is_optional(self, status_get, wait_st): """Assert that if heat_template_version is omitted, it's added.""" status_get.return_value = 'NOT_FOUND' wait_st.return_value = {} hs = heat_stack.HeatStack('test-stack', None) hs._template = {'resources': {'test': 1}} hs._files = {} hs._hot_environment = '' hs._parameters = {} hs._applied = False hs.push() expected_template = { 'heat_template_version': '2013-05-23', 'resources': {'test': 1} } self.heat_client_mock.stacks.create.assert_called_with( stack_name='test-stack', disable_rollback=True, parameters={}, template=expected_template, files={}, environment='', tags=self.mock_tag ) self.assertTrue(hs._applied) @mock.patch(CLS_NAME + '._wait_state') @mock.patch(CLS_NAME + '._get_status') def test_heat_files_are_sent(self, status_get, wait_st): """Assert that if heat_template_version is omitted, it's added.""" status_get.return_value = 'NOT_FOUND' wait_st.return_value = {} hs = heat_stack.HeatStack('test-stack', None) hs._description = None hs._template = {'resources': {'test': 1}} hs._files = {"heatFile": "file"} hs._hot_environment = '' hs._parameters = {} hs._applied = False hs.push() expected_template = { 'heat_template_version': '2013-05-23', 'resources': {'test': 1} } self.heat_client_mock.stacks.create.assert_called_with( stack_name='test-stack', disable_rollback=True, parameters={}, template=expected_template, files={"heatFile": "file"}, environment='', tags=self.mock_tag ) self.assertTrue(hs._applied) @mock.patch(CLS_NAME + '._wait_state') @mock.patch(CLS_NAME + '._get_status') def test_heat_environments_are_sent(self, status_get, wait_st): """Assert that if heat_template_version is omitted, it's added.""" status_get.return_value = 'NOT_FOUND' wait_st.return_value = {} hs = heat_stack.HeatStack('test-stack', None) hs._description = None hs._template = {'resources': {'test': 1}} hs._files = {"heatFile": "file"} hs._hot_environment = 'environments' hs._parameters = {} hs._applied = False hs.push() expected_template = { 'heat_template_version': '2013-05-23', 'resources': {'test': 1} } self.heat_client_mock.stacks.create.assert_called_with( stack_name='test-stack', disable_rollback=True, parameters={}, template=expected_template, files={"heatFile": "file"}, environment='environments', tags=self.mock_tag ) self.assertTrue(hs._applied) @mock.patch(CLS_NAME + '._wait_state') @mock.patch(CLS_NAME + '._get_status') def test_heat_async_push(self, status_get, wait_st): """Assert that if heat_template_version is omitted, it's added.""" status_get.return_value = 'NOT_FOUND' wait_st.return_value = {} hs = heat_stack.HeatStack('test-stack', None) hs._description = None hs._template = {'resources': {'test': 1}} hs._files = {"heatFile": "file"} hs._hot_environment = 'environments' hs._parameters = {} hs._applied = False with mock.patch('murano.dsl.dsl.get_execution_session'): hs.push(is_async=True) expected_template = { 'heat_template_version': '2013-05-23', 'resources': {'test': 1} } self.heat_client_mock.stacks.create.assert_not_called() hs.output() self.heat_client_mock.stacks.create.assert_called_with( stack_name='test-stack', disable_rollback=True, parameters={}, template=expected_template, files={"heatFile": "file"}, environment='environments', tags=self.mock_tag ) self.assertTrue(hs._applied) @mock.patch(CLS_NAME + '._wait_state') @mock.patch(CLS_NAME + '._get_status') @mock.patch.object(heat_stack, 'LOG') def test_push_except_http_conflict(self, mock_log, status_get, wait_st): status_get.return_value = 'NOT_FOUND' wait_st.return_value = {} hs = heat_stack.HeatStack('test-stack', None) hs._description = None hs._template = {'resources': {'test': 1}} hs._files = {"heatFile": "file"} hs._hot_environment = 'environments' hs._parameters = {} hs._applied = False hs._get_token_client().stacks.create.side_effect = [ heat_stack.heat_exc.HTTPConflict('test_error_msg'), None ] hs.push() mock_log.warning.assert_called_with( 'Conflicting operation: ERROR: test_error_msg') @mock.patch(CLS_NAME + '.current') def test_update_wrong_template_version(self, current): """Template version other than expected should cause error.""" hs = heat_stack.HeatStack( 'test-stack', 'Generated by TestHeatStack') hs._template = {'resources': {'test': 1}} invalid_template = { 'heat_template_version': 'something else' } current.return_value = {} e = self.assertRaises(heat_stack.HeatStackError, hs.update_template, invalid_template) err_msg = "Currently only heat_template_version 2013-05-23 "\ "is supported." self.assertEqual(err_msg, str(e)) # Check it's ok without a version hs.update_template({}) expected = {'resources': {'test': 1}} self.assertEqual(expected, hs._template) # .. or with a good version hs.update_template({'heat_template_version': '2013-05-23'}) expected['heat_template_version'] = '2013-05-23' self.assertEqual(expected, hs._template) @mock.patch(CLS_NAME + '._wait_state') @mock.patch(CLS_NAME + '._get_status') def test_heat_stack_tags_are_sent(self, status_get, wait_st): """Assert heat_stack tags are sent Assert that heat_stack `tags` parameter get push & with value from config parameter `stack_tags`. """ status_get.return_value = 'NOT_FOUND' wait_st.return_value = {} self.override_config('stack_tags', ['test-murano', 'murano-tag'], 'heat') hs = heat_stack.HeatStack('test-stack', None) hs._description = None hs._template = {'resources': {'test': 1}} hs._files = {} hs._hot_environment = '' hs._parameters = {} hs._applied = False hs._tags = ','.join(CONF.heat.stack_tags) hs.push() expected_template = { 'heat_template_version': '2013-05-23', 'resources': {'test': 1} } self.heat_client_mock.stacks.create.assert_called_with( stack_name='test-stack', disable_rollback=True, parameters={}, template=expected_template, files={}, environment='', tags=','.join(CONF.heat.stack_tags) ) self.assertTrue(hs._applied) @mock.patch(CLS_NAME + '._wait_state') @mock.patch(CLS_NAME + '._get_status') def test_parameters(self, status_get, wait_st): status_get.return_value = 'NOT_FOUND' wait_st.return_value = {} self.override_config('stack_tags', ['test-murano', 'murano-tag'], 'heat') hs = heat_stack.HeatStack('test-stack', None) hs._description = None hs._template = {'resources': {'test': 1}} hs._files = {} hs._hot_environment = '' hs._parameters = {} hs._applied = False hs._tags = ','.join(CONF.heat.stack_tags) hs.push() expected_template = { 'heat_template_version': '2013-05-23', 'resources': {'test': 1} } self.heat_client_mock.stacks.create.assert_called_with( stack_name='test-stack', disable_rollback=True, parameters={}, template=expected_template, files={}, environment='', tags=','.join(CONF.heat.stack_tags) ) self.assertEqual(hs.parameters(), hs._parameters) @mock.patch(CLS_NAME + '._wait_state') @mock.patch(CLS_NAME + '._get_status') def test_reload(self, status_get, wait_st): status_get.return_value = 'NOT_FOUND' wait_st.return_value = {} self.override_config('stack_tags', ['test-murano', 'murano-tag'], 'heat') hs = heat_stack.HeatStack('test-stack', None) hs._description = None hs._template = {'resources': {'test': 1}} hs._files = {} hs._hot_environment = '' hs._parameters = {} hs._applied = False hs._tags = ','.join(CONF.heat.stack_tags) hs.push() expected_template = { 'heat_template_version': '2013-05-23', 'resources': {'test': 1} } self.heat_client_mock.stacks.create.assert_called_with( stack_name='test-stack', disable_rollback=True, parameters={}, template=expected_template, files={}, environment='', tags=','.join(CONF.heat.stack_tags) ) hs.reload() stack_info = self.heat_client_mock.stacks.get(stack_id=hs._name) self.assertEqual(hs._template, hs._client.stacks.template( stack_id='{0}/{1}'.format( stack_info.stack_name, stack_info.id))) @mock.patch(CLS_NAME + '._wait_state') @mock.patch(CLS_NAME + '._get_status') def test_delete(self, status_get, wait_st): status_get.return_value = 'NOT_FOUND' wait_st.return_value = {} self.override_config('stack_tags', ['test-murano', 'murano-tag'], 'heat') hs = heat_stack.HeatStack('test-stack', None) hs._description = None hs._template = {'resources': {'test': 1}} hs._files = {} hs._hot_environment = '' hs._parameters = {} hs._applied = False hs._tags = ','.join(CONF.heat.stack_tags) hs.push() expected_template = { 'heat_template_version': '2013-05-23', 'resources': {'test': 1} } self.heat_client_mock.stacks.create.assert_called_with( stack_name='test-stack', disable_rollback=True, parameters={}, template=expected_template, files={}, environment='', tags=','.join(CONF.heat.stack_tags) ) hs.delete() self.assertEqual({}, hs._template) self.assertTrue(hs._applied) @mock.patch(CLS_NAME + '._wait_state') @mock.patch(CLS_NAME + '._get_status') @mock.patch.object(heat_stack, 'LOG') def test_delete_except_not_found(self, mock_log, status_get, wait_st): status_get.return_value = 'NOT_FOUND' wait_st.return_value = {} hs = heat_stack.HeatStack('test-stack', None) hs._template = {'resources': {'test': 1}} hs._files = {} hs._hot_environment = '' hs._parameters = {} hs._applied = False hs.push() hs._client.stacks.delete.side_effect =\ heat_stack.heat_exc.NotFound hs.delete() self.assertTrue(hs._applied) self.assertEqual({}, hs._template) mock_log.warning.assert_called_with( 'Stack test-stack already deleted?') @mock.patch(CLS_NAME + '._wait_state') @mock.patch(CLS_NAME + '._get_status') @mock.patch.object(heat_stack, 'LOG') def test_delete_except_http_conflict(self, mock_log, status_get, wait_st): status_get.return_value = 'NOT_FOUND' wait_st.return_value = {} hs = heat_stack.HeatStack('test-stack', None) hs._template = {'resources': {'test': 1}} hs._files = {} hs._hot_environment = '' hs._parameters = {} hs._applied = False hs.push() hs._client.stacks.delete.side_effect = [ heat_stack.heat_exc.HTTPConflict('test_error_msg'), None ] hs.delete() self.assertTrue(hs._applied) self.assertEqual({}, hs._template) mock_log.warning.assert_called_with('Conflicting operation: ' 'ERROR: test_error_msg') @mock.patch(CLS_NAME + '._wait_state') @mock.patch(CLS_NAME + '._get_status') def test_set_template_and_params(self, status_get, wait_st): status_get.return_value = 'NOT_FOUND' wait_st.return_value = {} self.override_config('stack_tags', ['test-murano', 'murano-tag'], 'heat') hs = heat_stack.HeatStack('test-stack', None) hs._description = None hs._template = {'resources': {'test': 1}} hs._files = {} hs._hot_environment = '' hs._parameters = {} hs._applied = False hs._tags = ','.join(CONF.heat.stack_tags) hs.push() expected_template = { 'heat_template_version': '2013-05-23', 'resources': {'test': 1} } self.heat_client_mock.stacks.create.assert_called_with( stack_name='test-stack', disable_rollback=True, parameters={}, template=expected_template, files={}, environment='', tags=','.join(CONF.heat.stack_tags) ) new_template = {'resources': {'test': 2}} new_parameters = {'parameters': {'test': 1}} hs.set_template(new_template) self.assertEqual(new_template, hs._template) hs.set_parameters(new_parameters) self.assertEqual(new_parameters, hs._parameters) @mock.patch(CLS_NAME + '._wait_state') @mock.patch(CLS_NAME + '._get_status') def test_set_hot_env_and_files(self, status_get, wait_st): status_get.return_value = 'NOT_FOUND' wait_st.return_value = {} self.override_config('stack_tags', ['test-murano', 'murano-tag'], 'heat') hs = heat_stack.HeatStack('test-stack', None) hs._description = None hs._template = {'resources': {'test': 1}} hs._files = {} hs._hot_environment = '' hs._parameters = {} hs._applied = False hs._tags = ','.join(CONF.heat.stack_tags) hs.push() expected_template = { 'heat_template_version': '2013-05-23', 'resources': {'test': 1} } self.heat_client_mock.stacks.create.assert_called_with( stack_name='test-stack', disable_rollback=True, parameters={}, template=expected_template, files={}, environment='', tags=','.join(CONF.heat.stack_tags) ) new_hot_env = 'test' new_files = {'files': {'test': 1}} hs.set_hot_environment(new_hot_env) self.assertEqual(new_hot_env, hs._hot_environment) hs.set_files(new_files) self.assertEqual(new_files, hs._files) @mock.patch(CLS_NAME + '._wait_state') @mock.patch(CLS_NAME + '._get_status') def test_none_template(self, status_get, wait_st): status_get.return_value = 'NOT_FOUND' wait_st.return_value = {} self.override_config('stack_tags', ['test-murano', 'murano-tag'], 'heat') hs = heat_stack.HeatStack('test-stack', None) hs._description = None hs._template = None hs._files = {} hs._hot_environment = '' hs._parameters = {} hs._applied = True hs._tags = ','.join(CONF.heat.stack_tags) self.assertIsNone(hs.push()) @mock.patch(CLS_NAME + '._wait_state') def test_get_hot_status(self, wait_st): wait_st.return_value = {} self.override_config('stack_tags', ['test-murano', 'murano-tag'], 'heat') hs = heat_stack.HeatStack('test-stack', None) hs._description = None hs._template = {'resources': {'test': 1}} hs._files = {} hs._hot_environment = '' hs._parameters = {} hs._applied = False hs._tags = ','.join(CONF.heat.stack_tags) hs.push() self.assertIsNone(hs._get_status()) self.assertTrue(wait_st.called) self.assertEqual({}, hs.output()) def test_current_except_http_notfound(self): hs = heat_stack.HeatStack( 'test-stack', 'Generated by TestHeatStack') hs._template = None hs._applied = False hs._parameters = {'param1': 'val1', 'param2': 'val2'} hs._client.stacks.get.side_effect = heat_stack.heat_exc.HTTPNotFound current = hs.current() self.assertEqual({}, current) self.assertEqual(True, hs._applied) self.assertEqual({}, hs._template) self.assertEqual({}, hs._parameters) @mock.patch.object(heat_stack, 'auth_utils') def test_get_client(self, mock_auth_utils): self._unpatch_get_client() mock_auth_utils.get_session_client_parameters.return_value =\ {'endpoint': 'test_endpoint/v1'} client = heat_stack.HeatStack._get_client('test_region_name') self.assertIsNotNone(client) self.assertEqual("", str(client.__class__)) mock_auth_utils.get_client_session.assert_called_with( conf='heat') @mock.patch.object(heat_stack, 'auth_utils') def test_get_token_client(self, mock_auth_utils): self._unpatch_get_client() mock_auth_utils.get_session_client_parameters.return_value =\ {'endpoint': 'test_endpoint/v1'} hs = heat_stack.HeatStack('test-stack', 'Generated by TestHeatStack') token_client = hs._get_token_client() self.assertIsNotNone(token_client) self.assertEqual("", str(token_client.__class__)) mock_auth_utils.get_token_client_session.assert_called_with( conf='heat') def test_wait_state(self): hs = heat_stack.HeatStack('test-stack', None) hs._client.stacks.get.return_value =\ mock.Mock(stack_status='CREATE_COMPLETE') result = hs._wait_state(lambda status: status == 'CREATE_COMPLETE') self.assertEqual({}, result) def test_wait_state_with_outputs(self): hs = heat_stack.HeatStack('test-stack', None) hs._client.stacks.get.side_effect = [ mock.Mock(stack_status='IN_PROGRESS'), mock.Mock(stack_status='CREATE_COMPLETE', outputs=[{'output_key': 'key1', 'output_value': 'val1'}, {'output_key': 'key2', 'output_value': 'val2'}]) ] result = hs._wait_state(lambda status: status == 'CREATE_COMPLETE') self.assertNotEqual({}, result) self.assertEqual({'key1': 'val1', 'key2': 'val2'}, result) def test_wait_state_with_multiple_states(self): """Test that only the first state is checked.""" hs = heat_stack.HeatStack('test-stack', None) hs._client.stacks.get.side_effect = [ mock.Mock(stack_status=['IN_PROGRESS', 'NOT_FOUND']), mock.Mock(stack_status='CREATE_COMPLETE') ] result = hs._wait_state(lambda status: status == 'CREATE_COMPLETE') self.assertEqual({}, result) @mock.patch.object(heat_stack, 'eventlet') def test_wait_state_with_wait_progress_true(self, mock_eventlet): hs = heat_stack.HeatStack('test-stack', None) hs._last_stack_timestamps = ('creation_time', 'updated_time') hs._client.stacks.get.side_effect = [ mock.Mock(stack_status='TEST_STATUS', creation_time='creation_time', updated_time='updated_time'), mock.Mock(stack_status='TEST_STATUS', creation_time='creation_time', updated_time='updated_time'), mock.Mock(stack_status='CREATE_COMPLETE') ] result = hs._wait_state(lambda status: status == 'CREATE_COMPLETE', wait_progress=True) self.assertEqual({}, result) self.assertEqual(3, hs._client.stacks.get.call_count) self.assertEqual(2, mock_eventlet.sleep.call_count) expected_calls = [mock.call.sleep(2), mock.call.sleep(2)] self.assertEqual(expected_calls, mock_eventlet.sleep.mock_calls) def test_wait_state_except_http_not_found(self): hs = heat_stack.HeatStack('test-stack', None) hs._client.stacks.get.side_effect = heat_stack.heat_exc.HTTPNotFound # If NOT FOUND is the expected status, then should run successfully. result = hs._wait_state(lambda status: status == 'NOT_FOUND') self.assertEqual({}, result) # Else EnvironmentError should be thrown. expected_error_msg = "Unexpected stack state {0}"\ .format('NOT_FOUND') with self.assertRaisesRegex(EnvironmentError, expected_error_msg): hs._wait_state(lambda status: status == 'CREATE_COMPLETE') @mock.patch.object(heat_stack, 'eventlet') def test_wait_state_except_general_exception(self, mock_eventlet): """Test whether 4 tries are executed before exception raised.""" hs = heat_stack.HeatStack('test-stack', None) hs._client.stacks.get.side_effect = Exception('test_exception_msg') with self.assertRaisesRegex(Exception, 'test_exception_msg'): hs._wait_state(lambda status: status == 'CREATE_COMPLETE') expected_calls = [mock.call.sleep(2), mock.call.sleep(4), mock.call.sleep(8)] self.assertEqual(4, hs._client.stacks.get.call_count) self.assertEqual(3, mock_eventlet.sleep.call_count) self.assertEqual(expected_calls, mock_eventlet.sleep.mock_calls) def test_wait_state_except_environment_error(self): hs = heat_stack.HeatStack('test-stack', None) hs._client.stacks.get.side_effect = [ mock.Mock(stack_status='IN_PROGRESS'), mock.Mock(stack_status='UNEXPECTED_STATUS', stack_status_reason='test_reason') ] expected_error_msg = "Unexpected stack state {0}: {1}"\ .format('UNEXPECTED_STATUS', 'test_reason') with self.assertRaisesRegex(EnvironmentError, expected_error_msg): hs._wait_state(lambda status: status == 'CREATE_COMPLETE') ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/test_utils.py0000664000175000017500000002234300000000000021321 0ustar00zuulzuul00000000000000# Copyright (c) 2016 AT&T Corp # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from unittest import mock from webob import exc from murano.services import states import murano.tests.unit.base as test_base from murano.tests.unit import utils as test_utils from murano import utils class TestUtils(test_base.MuranoTestCase): @mock.patch('murano.utils.db_session') def test_check_env(self, mock_db_session): """Test check env.""" mock_request = mock.MagicMock(context=test_utils.dummy_context()) mock_env = mock.MagicMock(environment_id='test_env_id', tenant_id=mock_request.context.project_id) mock_db_session.get_session().query().get.return_value = mock_env env = utils.check_env(mock_request, mock_env.environment_id) self.assertEqual(mock_env, env) @mock.patch('murano.utils.db_session') def test_check_env_with_null_environment_id(self, mock_db_session): """Test check env with null environment id throws exception.""" mock_request = mock.MagicMock(context=test_utils.dummy_context()) mock_db_session.get_session().query().get.return_value = None test_env_id = 'test_env_id' expected_error_message = 'Environment with id {env_id} not found'\ .format(env_id=test_env_id) with self.assertRaisesRegex(exc.HTTPNotFound, expected_error_message): utils.check_env(mock_request, test_env_id) @mock.patch('murano.utils.db_session') def test_check_env_with_mismatching_tenant_id(self, mock_db_session): """Test check env without matching tenant ids throws exception.""" mock_request = mock.MagicMock(context=test_utils.dummy_context()) mock_env = mock.MagicMock(environment_id='test_env_id', tenant_id='another_test_tenant_id') mock_db_session.get_session().query().get.return_value = mock_env expected_error_message = 'User is not authorized to access these '\ 'tenant resources' with self.assertRaisesRegex(exc.HTTPForbidden, expected_error_message): utils.check_env(mock_request, mock_env.environment_id) def test_check_session_with_null_session(self): """Test check session with null session throws exception.""" expected_error_message = 'Session is not found'\ .format(id=None) with self.assertRaisesRegex(exc.HTTPNotFound, expected_error_message): utils.check_session(None, None, None, None) @mock.patch('murano.utils.check_env') def test_check_session_with_mismatching_environment_id(self, _): """Test check session without matching env ids throws exception.""" mock_session = mock.MagicMock(session_id='session_id', environment_id='environment_id') environment_id = 'another_environment_id' expected_error_msg = 'Session is not tied '\ 'with Environment '\ .format(session_id=mock_session.session_id, environment_id=environment_id) with self.assertRaisesRegex(exc.HTTPBadRequest, expected_error_msg): utils.check_session(None, environment_id, mock_session, mock_session.session_id) def test_verify_session_with_invalid_request_header(self): """Test session with invalid request header throws exception.""" dummy_context = test_utils.dummy_context() if dummy_context.session: dummy_context.session = None mock_request = mock.MagicMock(context=dummy_context) expected_error_message = 'X-Configuration-Session header which '\ 'indicates to the session is missed' with self.assertRaisesRegex(exc.HTTPBadRequest, expected_error_message): self._test_verify_session(mock_request) @mock.patch('murano.utils.db_session') def test_verify_session_with_null_session(self, mock_db_session): """Test null session throws expected exception.""" mock_request = mock.MagicMock(context=test_utils.dummy_context()) mock_request.context.session = mock.MagicMock( return_value='test_sess_id') mock_db_session.get_session().query().get.return_value = None expected_error_message = 'Session is not found'\ .format(mock_request.context.session) with self.assertRaisesRegex(exc.HTTPNotFound, expected_error_message): self._test_verify_session(mock_request) @mock.patch('murano.utils.db_session') def test_verify_env_template_with_invalid_tenant(self, mock_db_session): """Test session validation failure throws expected exception.""" mock_request = mock.MagicMock(context=test_utils.dummy_context()) mock_request.context.project_id = mock.MagicMock( return_value='test_tenant_id') mock_env_template = mock.MagicMock(tenant_id='another_test_tenant_id') mock_db_session.get_session().query().get.return_value =\ mock_env_template expected_error_message = 'User is not authorized to access this'\ ' tenant resources' with self.assertRaisesRegex(exc.HTTPForbidden, expected_error_message): self._test_verify_env_template(mock_request, None) @mock.patch('murano.utils.db_session') def test_verify_env_template_with_null_template(self, mock_db_session): """Test null env template throws expected exception.""" mock_db_session.get_session().query().get.return_value = None expected_error_message = 'Environment Template with id {id} not found'\ .format(id='test_env_template_id') with self.assertRaisesRegex(exc.HTTPNotFound, expected_error_message): self._test_verify_env_template(None, 'test_env_template_id') @utils.verify_env_template def _test_verify_env_template(self, request, env_template_id): """Helper function for testing above decorator.""" pass @mock.patch('murano.utils.sessions.SessionServices.validate') @mock.patch('murano.utils.db_session') def test_verify_session_with_invalid_session(self, mock_db_session, mock_validate): """Test session validation failure throws expected exception.""" mock_validate.return_value = False mock_request = mock.MagicMock(context=test_utils.dummy_context()) mock_request.context.session = mock.MagicMock( return_value='test_sess_id') mock_db_session.get_session().query().get.return_value =\ mock.MagicMock() expected_error_message = 'Session is invalid: '\ 'environment has been updated or '\ 'updating right now with other session'\ .format(mock_request.context.session) with self.assertRaisesRegex(exc.HTTPForbidden, expected_error_message): self._test_verify_session(mock_request) @mock.patch('murano.utils.sessions.SessionServices.validate') @mock.patch('murano.utils.db_session') def test_verify_session_in_deploying_state(self, mock_db_session, mock_validate): """Test deploying session throws expected exception.""" mock_validate.return_value = True mock_request = mock.MagicMock(context=test_utils.dummy_context()) mock_request.context.session = mock.MagicMock( return_value='test_sess_id') mock_db_session.get_session().query().get.return_value =\ mock.MagicMock(state=states.SessionState.DEPLOYING) expected_error_message = 'Session is already in '\ 'deployment state'\ .format(mock_request.context.session) with self.assertRaisesRegex(exc.HTTPForbidden, expected_error_message): self._test_verify_session(mock_request) @utils.verify_session def _test_verify_session(self, request): """Helper function for testing above decorator.""" pass ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/tests/unit/utils.py0000664000175000017500000001023300000000000020255 0ustar00zuulzuul00000000000000# Copyright (c) 2014 Hewlett-Packard Development Company, L.P. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT 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 shutil import yaml import zipfile from murano import context from murano.db import session MANIFEST = {'Format': 'MuranoPL/1.0', 'Type': 'Application', 'Description': 'MockApp for API tests', 'Author': 'Mirantis, Inc'} def dummy_context(user='test_username', tenant_id='test_tenant_id', request_id='dummy-request', **kwargs): # NOTE(kzaitsev) we need to pass non-False value to request_id, to # prevent it being generated by oslo during tests. params = { 'request_id': request_id, 'project_id': tenant_id, 'user': user, } params.update(kwargs) return context.RequestContext.from_dict(params) def save_models(*models): s = session.get_session() for m in models: m.save(s) def compose_package(app_name, package_dir, require=None, archive_dir=None, add_class_name=False, manifest_required=True, version=None): """Composes a murano package Composes package `app_name` manifest and files from `package_dir`. Includes `require` section if any in the manifest file. Puts the resulting .zip file into `archive_dir` if present or in the `package_dir`. """ tmp_package_dir = os.path.join(archive_dir, os.path.basename(package_dir)) shutil.copytree(package_dir, tmp_package_dir) package_dir = tmp_package_dir if manifest_required: manifest = os.path.join(package_dir, "manifest.yaml") 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 mfest_copy['Classes'] = {fqn: 'mock_muranopl.yaml'} if require: mfest_copy['Require'] = {str(name): version for name, version in require} if version: mfest_copy['Version'] = version f.write(yaml.dump(mfest_copy, default_flow_style=False)) if add_class_name: class_file = os.path.join(package_dir, 'Classes', 'mock_muranopl.yaml') with open(class_file, 'r') as f: contents = f.read() index = contents.index('Extends') contents = "{0}Name: {1}\n\n{2}".format(contents[:index], app_name, contents[index:]) with open(class_file, 'w') as f: f.write(contents) if require: class_file = os.path.join(package_dir, 'Classes', 'mock_muranopl.yaml') with open(class_file, 'r') as f: content = f.read() index_string = 'deploy:\n Body:\n ' index = content.index(index_string) + len(index_string) class_names = [req[0][req[0].rfind('.') + 1:] for req in require] addition = "".join(["- new({})\n".format(name) + ' ' * 6 for name in class_names]) content = content[:index] + addition + content[index:] with open(class_file, 'w') as f: f.write(content) 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, name ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/utils.py0000664000175000017500000001767200000000000016152 0ustar00zuulzuul00000000000000# 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 functools import os from oslo_concurrency import lockutils from oslo_log import log as logging from oslo_utils import fileutils from webob import exc from murano.common.i18n import _ from murano.db import models from murano.db.services import sessions from murano.db import session as db_session from murano.services import states LOG = logging.getLogger(__name__) def check_env(request, environment_id): unit = db_session.get_session() environment = unit.query(models.Environment).get(environment_id) if environment is None: msg = _('Environment with id {env_id}' ' not found').format(env_id=environment_id) LOG.error(msg) raise exc.HTTPNotFound(explanation=msg) if hasattr(request, 'context'): if (environment.tenant_id != request.context.project_id and not request.context.is_admin): msg = _('User is not authorized to access' ' these tenant resources') LOG.error(msg) raise exc.HTTPForbidden(explanation=msg) return environment def check_session(request, environment_id, session, session_id): """Validate, that a session is ok.""" if session is None: msg = _('Session is not found').format(id=session_id) LOG.error(msg) raise exc.HTTPNotFound(explanation=msg) if session.environment_id != environment_id: msg = _('Session is not tied ' 'with Environment ').format( session_id=session_id, environment_id=environment_id) LOG.error(msg) raise exc.HTTPBadRequest(explanation=msg) check_env(request, environment_id) def verify_env(func): @functools.wraps(func) def __inner(self, request, environment_id, *args, **kwargs): check_env(request, environment_id) return func(self, request, environment_id, *args, **kwargs) return __inner def verify_env_template(func): @functools.wraps(func) def __inner(self, request, env_template_id, *args, **kwargs): unit = db_session.get_session() template = unit.query(models.EnvironmentTemplate).get(env_template_id) if template is None: msg = _('Environment Template with id {id} not found' ).format(id=env_template_id) LOG.error(msg) raise exc.HTTPNotFound(explanation=msg) if hasattr(request, 'context'): if template.tenant_id != request.context.project_id: msg = _('User is not authorized to access' ' this tenant resources') LOG.error(msg) raise exc.HTTPForbidden(explanation=msg) return func(self, request, env_template_id, *args, **kwargs) return __inner def verify_session(func): @functools.wraps(func) def __inner(self, request, *args, **kwargs): if hasattr(request, 'context') and not request.context.session: msg = _('X-Configuration-Session header which indicates' ' to the session is missed') LOG.error(msg) raise exc.HTTPBadRequest(explanation=msg) session_id = request.context.session unit = db_session.get_session() session = unit.query(models.Session).get(session_id) if session is None: msg = _('Session is not found').format(session_id) LOG.error(msg) raise exc.HTTPNotFound(explanation=msg) if not sessions.SessionServices.validate(session): msg = _('Session ' 'is invalid: environment has been updated or ' 'updating right now with other session').format(session_id) LOG.error(msg) raise exc.HTTPForbidden(explanation=msg) if session.state == states.SessionState.DEPLOYING: msg = _('Session is already in deployment state' ).format(session_id) raise exc.HTTPForbidden(explanation=msg) return func(self, request, *args, **kwargs) return __inner ExclusiveInterProcessLock = lockutils.InterProcessLock if os.name == 'nt': # no shared locks on windows SharedInterProcessLock = lockutils.InterProcessLock else: import fcntl class SharedInterProcessLock(lockutils.InterProcessLock): def trylock(self): # LOCK_EX instead of LOCK_EX fcntl.lockf(self.lockfile, fcntl.LOCK_SH | fcntl.LOCK_NB) def _do_open(self): # the file has to be open in read mode, therefore this method has # to be overridden basedir = os.path.dirname(self.path) if basedir: made_basedir = fileutils.ensure_tree(basedir) if made_basedir: self.logger.debug( 'Created lock base path `%s`', basedir) # The code here is mostly copy-pasted from oslo_concurrency and # fasteners. The file has to be open with read permissions to be # suitable for shared locking if self.lockfile is None or self.lockfile.closed: try: # ensure the file is there, but do not obtain an extra file # descriptor, as closing it would release fcntl lock fd = os.open(self.path, os.O_CREAT | os.O_EXCL) os.close(fd) except OSError: pass self.lockfile = open(self.path, 'r') class ReaderWriterLock(lockutils.ReaderWriterLock): @contextlib.contextmanager def write_lock(self, blocking=True): """Context manager that grants a write lock. Will wait until no active readers. Blocks readers after acquiring. Raises a ``RuntimeError`` if an active reader attempts to acquire a lock. """ timeout = None if blocking else 0.00001 me = self._current_thread() i_am_writer = self.is_writer(check_pending=False) if self.is_reader() and not i_am_writer: raise RuntimeError("Reader %s to writer privilege" " escalation not allowed" % me) if i_am_writer: # Already the writer; this allows for basic reentrancy. yield self else: with self._cond: self._pending_writers.append(me) while True: # No readers, and no active writer, am I next?? if len(self._readers) == 0 and self._writer is None: if self._pending_writers[0] == me: self._writer = self._pending_writers.popleft() break # NOTE(kzaitsev): this actually means, that we can wait # more than timeout times, since if we get True value we # would get another spin inside of the while loop # Should we do anything about it? acquired = self._cond.wait(timeout) if not acquired: yield False return try: yield True finally: with self._cond: self._writer = None self._cond.notify_all() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano/version.py0000664000175000017500000000134200000000000016462 0ustar00zuulzuul00000000000000# 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 pbr import version version_info = version.VersionInfo('murano') version_string = version_info.cached_version_string() ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1696417899.7731807 murano-16.0.0/murano.egg-info/0000775000175000017500000000000000000000000016115 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417899.0 murano-16.0.0/murano.egg-info/PKG-INFO0000664000175000017500000000460000000000000017212 0ustar00zuulzuul00000000000000Metadata-Version: 1.2 Name: murano Version: 16.0.0 Summary: Murano API Home-page: https://www.openstack.org/software/releases/mitaka/components/murano Author: OpenStack Author-email: openstack-discuss@lists.openstack.org License: Apache License, Version 2.0 Description: ======================== Team and repository tags ======================== .. image:: https://governance.openstack.org/tc/badges/murano.svg :target: https://governance.openstack.org/tc/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. Project Resources ----------------- * `Murano Official Documentation `_ * Project status, bugs, and blueprints are tracked on `Launchpad `_ * Additional resources are linked from the project `Wiki `_ page * `Python client `_ License ------- Apache License Version 2.0 http://www.apache.org/licenses/LICENSE-2.0 Release Notes ------------- Release Notes may be found here: https://docs.openstack.org/releasenotes/murano Platform: UNKNOWN Classifier: Development Status :: 5 - Production/Stable Classifier: Environment :: OpenStack Classifier: Intended Audience :: Developers Classifier: Intended Audience :: Information Technology Classifier: License :: OSI Approved :: Apache Software License Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: Implementation :: CPython Classifier: Programming Language :: Python :: 3 :: Only Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.8 Requires-Python: >=3.8 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417899.0 murano-16.0.0/murano.egg-info/SOURCES.txt0000664000175000017500000013426100000000000020010 0ustar00zuulzuul00000000000000.coveragerc .stestr.conf .zuul.yaml AUTHORS CONTRIBUTING.rst ChangeLog HACKING.rst LICENSE README.rst bandit.yaml bindep.txt requirements.txt setup.cfg setup.py test-requirements.txt tox.ini api-ref/source/conf.py api-ref/source/index.rst api-ref/source/v1/actions.inc api-ref/source/v1/categories.inc api-ref/source/v1/deployments.inc api-ref/source/v1/environments.inc api-ref/source/v1/index.rst api-ref/source/v1/packages.inc api-ref/source/v1/parameters.yaml api-ref/source/v1/sessions.inc api-ref/source/v1/status.yaml api-ref/source/v1/templates.inc api-ref/source/v1/samples/category-create-response.json api-ref/source/v1/samples/category-list-response.json api-ref/source/v1/samples/category-show-response.json api-ref/source/v1/samples/deployments-list-response.json api-ref/source/v1/samples/environment-create-request.json api-ref/source/v1/samples/environment-create-response.json api-ref/source/v1/samples/environment-last-status-response.json api-ref/source/v1/samples/environment-model-update-request.json api-ref/source/v1/samples/environment-show-response.json api-ref/source/v1/samples/environment-update-request.json api-ref/source/v1/samples/environment-update-response.json api-ref/source/v1/samples/environments-list-response.json api-ref/source/v1/samples/environments-model-response.json api-ref/source/v1/samples/execute-action-response.json api-ref/source/v1/samples/package-create-response.json api-ref/source/v1/samples/package-show-response.json api-ref/source/v1/samples/package-update-request.json api-ref/source/v1/samples/package-update-response.json api-ref/source/v1/samples/packages-list-response.json api-ref/source/v1/samples/session-create-response.json api-ref/source/v1/samples/session-show-response.json api-ref/source/v1/samples/static-action-request.json api-ref/source/v1/samples/static-action-response.json api-ref/source/v1/samples/template-add-app-request.json api-ref/source/v1/samples/template-add-app-response.json api-ref/source/v1/samples/template-clone-request.json api-ref/source/v1/samples/template-clone-response.json api-ref/source/v1/samples/template-create-env-request.json api-ref/source/v1/samples/template-create-env-response.json api-ref/source/v1/samples/template-create-request.json api-ref/source/v1/samples/template-create-response.json api-ref/source/v1/samples/template-list-apps-response.json api-ref/source/v1/samples/template-show-response.json api-ref/source/v1/samples/template-update-app-request.json api-ref/source/v1/samples/template-update-app-response.json api-ref/source/v1/samples/templates-list-response.json contrib/elements/docker/README.md contrib/elements/docker/install.d/56-docker contrib/elements/kubernetes/README.md contrib/elements/kubernetes/element-deps contrib/elements/kubernetes/install.d/57-kubernetes contrib/glance/setup.cfg contrib/glance/setup.py contrib/glance/muranoartifact/__init__.py contrib/glance/muranoartifact/v1/__init__.py contrib/glance/muranoartifact/v1/package.py contrib/packages/EncryptionDemo/manifest.yaml contrib/packages/EncryptionDemo/Classes/EncryptionDemo.yaml contrib/packages/EncryptionDemo/UI/ui.yaml contrib/plugins/cloudify_plugin/LICENSE contrib/plugins/cloudify_plugin/README.rst contrib/plugins/cloudify_plugin/requirements.txt contrib/plugins/cloudify_plugin/setup.cfg contrib/plugins/cloudify_plugin/setup.py contrib/plugins/cloudify_plugin/cloudify_applications_library/manifest.yaml contrib/plugins/cloudify_plugin/cloudify_applications_library/Classes/CloudifyApplication.yaml contrib/plugins/cloudify_plugin/murano_cloudify_plugin/__init__.py contrib/plugins/cloudify_plugin/murano_cloudify_plugin/cfg.py contrib/plugins/cloudify_plugin/murano_cloudify_plugin/cloudify_client.py contrib/plugins/cloudify_plugin/murano_cloudify_plugin/cloudify_tosca_package.py contrib/plugins/cloudify_plugin/nodecellar_example_application/LICENSE contrib/plugins/cloudify_plugin/nodecellar_example_application/README.rst contrib/plugins/cloudify_plugin/nodecellar_example_application/logo.png contrib/plugins/cloudify_plugin/nodecellar_example_application/manifest.yaml contrib/plugins/magnum_plugin/LICENSE contrib/plugins/magnum_plugin/requirements.txt contrib/plugins/magnum_plugin/setup.cfg contrib/plugins/magnum_plugin/setup.py contrib/plugins/magnum_plugin/magnum-app/com.intel.magnum.plugin.MagnumApp/logo.png contrib/plugins/magnum_plugin/magnum-app/com.intel.magnum.plugin.MagnumApp/manifest.yaml contrib/plugins/magnum_plugin/magnum-app/com.intel.magnum.plugin.MagnumApp/Classes/MagnumBayApp.yaml contrib/plugins/magnum_plugin/magnum-app/com.intel.magnum.plugin.MagnumApp/Classes/MagnumBaymodel.yaml contrib/plugins/magnum_plugin/magnum-app/com.intel.magnum.plugin.MagnumApp/UI/ui.yaml contrib/plugins/magnum_plugin/magnum_plugin/__init__.py contrib/plugins/magnum_plugin/magnum_plugin/cfg.py contrib/plugins/murano_exampleplugin/requirements.txt contrib/plugins/murano_exampleplugin/setup.cfg contrib/plugins/murano_exampleplugin/setup.py contrib/plugins/murano_exampleplugin/example-app/io.murano.apps.demo.DemoApp/logo.png contrib/plugins/murano_exampleplugin/example-app/io.murano.apps.demo.DemoApp/manifest.yaml contrib/plugins/murano_exampleplugin/example-app/io.murano.apps.demo.DemoApp/Classes/DemoApp.yaml contrib/plugins/murano_exampleplugin/example-app/io.murano.apps.demo.DemoApp/Classes/DemoInstance.yaml contrib/plugins/murano_exampleplugin/example-app/io.murano.apps.demo.DemoApp/Classes/ImageValidatorMixin.yaml contrib/plugins/murano_exampleplugin/example-app/io.murano.apps.demo.DemoApp/UI/ui.yaml contrib/plugins/murano_exampleplugin/murano_exampleplugin/__init__.py contrib/plugins/murano_exampleplugin/murano_exampleplugin/cfg.py contrib/plugins/murano_heat-translator_plugin/README.rst contrib/plugins/murano_heat-translator_plugin/requirements.txt contrib/plugins/murano_heat-translator_plugin/setup.cfg contrib/plugins/murano_heat-translator_plugin/setup.py contrib/plugins/murano_heat-translator_plugin/plugin/__init__.py contrib/plugins/murano_heat-translator_plugin/plugin/cfg.py contrib/plugins/murano_heat-translator_plugin/plugin/csar_package.py contrib/plugins/murano_heat-translator_plugin/sample/hello_world/README.rst contrib/plugins/murano_heat-translator_plugin/sample/hello_world/logo.png contrib/plugins/murano_heat-translator_plugin/sample/hello_world/manifest.yaml contrib/plugins/murano_heat-translator_plugin/sample/wordpress/README.rst contrib/plugins/murano_heat-translator_plugin/sample/wordpress/logo.png contrib/plugins/murano_heat-translator_plugin/sample/wordpress/manifest.yaml devstack/README.rst devstack/plugin.sh devstack/settings devstack/files/apache-murano-api.template devstack/files/debs/murano devstack/files/rpms/murano devstack/upgrade/resources.sh devstack/upgrade/settings devstack/upgrade/shutdown.sh devstack/upgrade/upgrade.sh doc/requirements.txt doc/source/conf.py doc/source/index.rst doc/source/_templates/sidebarlinks.html doc/source/admin/admin_troubleshooting.rst doc/source/admin/config-wsgi.rst doc/source/admin/configure_cloud_foundry_service_broker.rst doc/source/admin/deploy_murano.rst doc/source/admin/index.rst doc/source/admin/manage_categories.rst doc/source/admin/manage_images.rst doc/source/admin/manage_packages.rst doc/source/admin/murano_agent.rst doc/source/admin/murano_policies.rst doc/source/admin/murano_repository.rst doc/source/admin/net_configuration.rst doc/source/admin/policy_enf.rst doc/source/admin/prepare_lab.rst doc/source/admin/using_glare.rst doc/source/admin/appdev-guide/app_debugging.rst doc/source/admin/appdev-guide/app_development_framework.rst doc/source/admin/appdev-guide/app_migrating.rst doc/source/admin/appdev-guide/app_unit_tests.rst doc/source/admin/appdev-guide/cinder_volume_supporting.rst doc/source/admin/appdev-guide/developer_index.rst doc/source/admin/appdev-guide/encrypting_properties.rst doc/source/admin/appdev-guide/examples.rst doc/source/admin/appdev-guide/exec_plan.rst doc/source/admin/appdev-guide/faq.rst doc/source/admin/appdev-guide/garbage_collection.rst doc/source/admin/appdev-guide/hot_packages.rst doc/source/admin/appdev-guide/multi_region.rst doc/source/admin/appdev-guide/murano_bundles.rst doc/source/admin/appdev-guide/murano_packages.rst doc/source/admin/appdev-guide/murano_pl.rst doc/source/admin/appdev-guide/use_cases.rst doc/source/admin/appdev-guide/app_migrating/app_migrate_to_juno.rst doc/source/admin/appdev-guide/app_migrating/app_migrate_to_kilo.rst doc/source/admin/appdev-guide/app_migrating/app_migrate_to_liberty.rst doc/source/admin/appdev-guide/app_migrating/app_migrate_to_newton.rst doc/source/admin/appdev-guide/figures/chef_server.png doc/source/admin/appdev-guide/figures/chef_server_form.png doc/source/admin/appdev-guide/figures/logo.png doc/source/admin/appdev-guide/figures/step_1.png doc/source/admin/appdev-guide/figures/step_2.png doc/source/admin/appdev-guide/figures/structure.png doc/source/admin/appdev-guide/figures/structure.vdx doc/source/admin/appdev-guide/murano_pl/actions.rst doc/source/admin/appdev-guide/murano_pl/class_templ.rst doc/source/admin/appdev-guide/murano_pl/core_lib.rst doc/source/admin/appdev-guide/murano_pl/metadata.rst doc/source/admin/appdev-guide/murano_pl/reflection.rst doc/source/admin/appdev-guide/murano_pl/statics.rst doc/source/admin/appdev-guide/murano_pl/versioning.rst doc/source/admin/appdev-guide/murano_pl/yaml.rst doc/source/admin/appdev-guide/murano_pl/yaql.rst doc/source/admin/appdev-guide/muranopackages/dynamic_ui.rst doc/source/admin/appdev-guide/muranopackages/package_structure.rst doc/source/admin/appdev-guide/muranopackages/repository.rst doc/source/admin/appdev-guide/step-by-step/configure-step1.png doc/source/admin/appdev-guide/step-by-step/configure-step2.png doc/source/admin/appdev-guide/step-by-step/hello-world-desc.png doc/source/admin/appdev-guide/step-by-step/hello-world-screen-1.png doc/source/admin/appdev-guide/step-by-step/hello-world-screen-2.png doc/source/admin/appdev-guide/step-by-step/new-env-1.png doc/source/admin/appdev-guide/step-by-step/new-env-2.png doc/source/admin/appdev-guide/step-by-step/new-env-3.png doc/source/admin/appdev-guide/step-by-step/part1.rst doc/source/admin/appdev-guide/step-by-step/part2.rst doc/source/admin/appdev-guide/step-by-step/part3.rst doc/source/admin/appdev-guide/step-by-step/part4.rst doc/source/admin/appdev-guide/step-by-step/plone-admin.png doc/source/admin/appdev-guide/step-by-step/plone-logo.png doc/source/admin/appdev-guide/step-by-step/plone-ready.png doc/source/admin/appdev-guide/step-by-step/plone-simple-step1.png doc/source/admin/appdev-guide/step-by-step/plone-simple-step2.png doc/source/admin/appdev-guide/step-by-step/step_by_step.rst doc/source/admin/deploy_murano/configure_ssl.rst doc/source/admin/deploy_murano/devstack.rst doc/source/admin/deploy_murano/install_manually.rst doc/source/admin/deploy_murano/prerequisites.rst doc/source/admin/figures/add-interface.png doc/source/admin/figures/deploy-log.png doc/source/admin/figures/network-topology-1.png doc/source/admin/figures/network-topology-2.png doc/source/admin/figures/new-inst.png doc/source/admin/policy_enforcement/policy_enf_dev.rst doc/source/admin/policy_enforcement/policy_enf_modify.rst doc/source/admin/policy_enforcement/policy_enf_rules.rst doc/source/admin/policy_enforcement/policy_enf_setup.rst doc/source/cli/index.rst doc/source/cli/murano-status.rst doc/source/configuration/config-options.rst doc/source/configuration/index.rst doc/source/configuration/sample_config.rst doc/source/configuration/sample_policy.rst doc/source/contributor/contributing.rst doc/source/contributor/contributor_index.rst doc/source/contributor/dev_env.rst doc/source/contributor/dev_guidelines.rst doc/source/contributor/doc_guidelines.rst doc/source/contributor/how_to_contribute.rst doc/source/contributor/plugins.rst doc/source/contributor/stable_branches.rst doc/source/contributor/testing.rst doc/source/contributor/plugins/manage_plugins.rst doc/source/contributor/plugins/murano_plugins.rst doc/source/first-app/Before_the_start.rst doc/source/first-app/Debugging_and_troubleshooting_your_murano_app.rst doc/source/first-app/Develop_murano_app_for_plone.rst doc/source/first-app/Publish_your_murano_app_in_the_application_catalog.rst doc/source/first-app/README.rst doc/source/first-app/What_is_the_use_case.rst doc/source/first-app/What_you_will_learn.rst doc/source/first-app/Who_is_this_guide_for.rst doc/source/first-app/index.rst doc/source/install/common_prerequisites.rst doc/source/install/enable-ssl.rst doc/source/install/from-source.rst doc/source/install/get_started.rst doc/source/install/import-murano-apps.rst doc/source/install/index.rst doc/source/install/install-api.rst doc/source/install/install-dashboard.rst doc/source/install/install-network-config.rst doc/source/install/install.rst doc/source/install/next-steps.rst doc/source/install/verify.rst doc/source/reference/architecture.png doc/source/reference/architecture.rst doc/source/reference/key_features.rst doc/source/reference/overview_index.rst doc/source/reference/target_users.rst doc/source/reference/use_cases.rst doc/source/reference/appendix/appendix_index.rst doc/source/reference/appendix/cli_ref.rst doc/source/reference/appendix/glossary.rst doc/source/reference/appendix/murano_concepts.rst doc/source/reference/appendix/rest_api_spec.rst doc/source/reference/appendix/tutorials.rst doc/source/reference/appendix/articles/articles_index.rst doc/source/reference/appendix/articles/guidelines.rst doc/source/reference/appendix/articles/multi_region.rst doc/source/reference/appendix/articles/murano_gerrit_dashboard.rst doc/source/reference/appendix/articles/telnet_example.rst doc/source/reference/appendix/articles/test_docs.rst doc/source/reference/appendix/articles/workflow.rst doc/source/reference/appendix/articles/image_builders/index.rst doc/source/reference/appendix/articles/image_builders/linux.rst doc/source/reference/appendix/articles/image_builders/upload.rst doc/source/reference/appendix/articles/image_builders/windows.rst doc/source/reference/appendix/articles/specification/index.rst doc/source/reference/appendix/articles/specification/murano-api.rst doc/source/reference/appendix/articles/specification/murano-env-temp.rst doc/source/reference/appendix/articles/specification/murano-repository.rst doc/source/reference/appendix/articles/specification/overview.rst doc/source/user/user_index.rst doc/source/user/figures/add_key_pair.png doc/source/user/figures/add_pkg_info.png doc/source/user/figures/app_category.png doc/source/user/figures/app_details.png doc/source/user/figures/app_filter.png doc/source/user/figures/app_filter_example.png doc/source/user/figures/app_logs.png doc/source/user/figures/browse_zip_file.png doc/source/user/figures/bundle_name.png doc/source/user/figures/component-details.png doc/source/user/figures/delete_application.png doc/source/user/figures/deploy_env.png doc/source/user/figures/deploy_env_2.png doc/source/user/figures/env-component-logs.png doc/source/user/figures/env_default_network.png doc/source/user/figures/environments.png doc/source/user/figures/import_bundle.png doc/source/user/figures/import_package.png doc/source/user/figures/logs.png doc/source/user/figures/murano_actions.png doc/source/user/figures/qs_app_category.png doc/source/user/figures/qs_apps.png doc/source/user/figures/qs_package_details.png doc/source/user/figures/qs_package_import.png doc/source/user/figures/qs_package_url.png doc/source/user/figures/qs_quick_deploy.png doc/source/user/figures/qs_quick_deploy_2.png doc/source/user/figures/qs_quick_env.png doc/source/user/figures/repository.png doc/source/user/figures/select_packages.png doc/source/user/figures/topology_element_1.png doc/source/user/figures/topology_element_2.png doc/source/user/figures/topology_kubernetes.png doc/source/user/figures/topology_wordpress.png doc/source/user/figures/add_to_env/add_component.png doc/source/user/figures/add_to_env/add_from_cat.png doc/source/user/figures/add_to_env/add_more_apps.png doc/source/user/figures/add_to_env/add_to_env.png doc/source/user/figures/add_to_env/configure_app.png doc/source/user/figures/add_to_env/drag_and_drop.png doc/source/user/figures/add_to_env/quick_deploy.png doc/source/user/figures/add_to_env/quick_env.png doc/source/user/quickstart/quickstart.rst doc/source/user/userguide/deploying_using_cli.rst doc/source/user/userguide/install_client.rst doc/source/user/userguide/log_in_to_murano_instance.rst doc/source/user/userguide/manage_applications.rst doc/source/user/userguide/manage_environments.rst doc/source/user/userguide/use_cli.rst etc/murano/README-murano.conf.txt etc/murano/logging.conf.sample etc/murano/murano-cfapi-paste.ini etc/murano/murano-paste.ini etc/murano/netconfig.yaml.sample etc/oslo-config-generator/murano-cfapi.conf etc/oslo-config-generator/murano.conf etc/oslo-policy-generator/murano-policy-generator.conf meta/README.rst meta/io.murano/LICENSE meta/io.murano/manifest.yaml meta/io.murano.applications/LICENSE meta/io.murano.applications/manifest.yaml meta/io.murano.applications/Classes/baseapps.yaml meta/io.murano.applications/Classes/component.yaml meta/io.murano.applications/Classes/events.yaml meta/io.murano.applications/Classes/replication.yaml meta/io.murano.applications/Classes/servers.yaml meta/io.murano.applications/Classes/tests/TestEvents.yaml meta/io.murano.applications/Classes/tests/TestReplication.yaml meta/io.murano.applications/Classes/tests/TestServerProviders.yaml meta/io.murano.applications/Classes/tests/TestSoftwareComponent.yaml meta/io.murano/Classes/Application.yaml meta/io.murano/Classes/CloudRegion.yaml meta/io.murano/Classes/CloudResource.yaml meta/io.murano/Classes/Environment.yaml meta/io.murano/Classes/Exception.yaml meta/io.murano/Classes/File.yaml meta/io.murano/Classes/Object.yaml meta/io.murano/Classes/Project.yaml meta/io.murano/Classes/SharedIp.yaml meta/io.murano/Classes/SharedIpRange.yaml meta/io.murano/Classes/StackTrace.yaml meta/io.murano/Classes/User.yaml meta/io.murano/Classes/configuration/Linux.yaml meta/io.murano/Classes/metadata/Description.yaml meta/io.murano/Classes/metadata/HelpText.yaml meta/io.murano/Classes/metadata/ModelBuilder.yaml meta/io.murano/Classes/metadata/Title.yaml meta/io.murano/Classes/metadata/engine/Serialize.yaml meta/io.murano/Classes/metadata/engine/Synchronize.yaml meta/io.murano/Classes/metadata/forms/Hidden.yaml meta/io.murano/Classes/metadata/forms/Position.yaml meta/io.murano/Classes/metadata/forms/Section.yaml meta/io.murano/Classes/resources/CinderVolume.yaml meta/io.murano/Classes/resources/CinderVolumeBackup.yaml meta/io.murano/Classes/resources/CinderVolumeSnapshot.yaml meta/io.murano/Classes/resources/ConfLangInstance.yaml meta/io.murano/Classes/resources/ExistingCinderVolume.yaml meta/io.murano/Classes/resources/ExistingNeutronNetwork.yaml meta/io.murano/Classes/resources/HeatSWConfigInstance.yaml meta/io.murano/Classes/resources/HeatSWConfigLinuxInstance.yaml meta/io.murano/Classes/resources/Instance.yaml meta/io.murano/Classes/resources/InstanceAffinityGroup.yaml meta/io.murano/Classes/resources/LinuxInstance.yaml meta/io.murano/Classes/resources/LinuxMuranoInstance.yaml meta/io.murano/Classes/resources/LinuxUDInstance.yaml meta/io.murano/Classes/resources/MetadataAware.yaml meta/io.murano/Classes/resources/Network.yaml meta/io.murano/Classes/resources/NeutronNetwork.yaml meta/io.murano/Classes/resources/NeutronNetworkBase.yaml meta/io.murano/Classes/resources/NovaNetwork.yaml meta/io.murano/Classes/resources/Volume.yaml meta/io.murano/Classes/resources/WindowsInstance.yaml meta/io.murano/Classes/system/Agent.yaml meta/io.murano/Classes/system/AgentListener.yaml meta/io.murano/Classes/system/AwsSecurityGroupManager.yaml meta/io.murano/Classes/system/DummySecurityGroupManager.yaml meta/io.murano/Classes/system/HeatStack.yaml meta/io.murano/Classes/system/InstanceNotifier.yaml meta/io.murano/Classes/system/Logger.yaml meta/io.murano/Classes/system/MetadefBrowser.yaml meta/io.murano/Classes/system/MistralClient.yaml meta/io.murano/Classes/system/NetworkExplorer.yaml meta/io.murano/Classes/system/NeutronSecurityGroupManager.yaml meta/io.murano/Classes/system/Resources.yaml meta/io.murano/Classes/system/SecurityGroupManager.yaml meta/io.murano/Classes/system/StatusReporter.yaml meta/io.murano/Classes/test/TestFixture.yaml meta/io.murano/Resources/Agent-v1.template meta/io.murano/Resources/Agent-v2.template meta/io.murano/Resources/PutFile.template meta/io.murano/Resources/RunCommand.template meta/io.murano/Resources/conflang.conf meta/io.murano/Resources/linux-init.sh meta/io.murano/Resources/murano-agent meta/io.murano/Resources/murano-agent.conf meta/io.murano/Resources/murano-agent.service meta/io.murano/Resources/murano-init.conf meta/io.murano/Resources/murano-init.sh meta/io.murano/Resources/windows-init.ps1 meta/io.murano/Resources/scripts/putFile.sh murano/__init__.py murano/context.py murano/monkey_patch.py murano/opts.py murano/utils.py murano/version.py murano.egg-info/PKG-INFO murano.egg-info/SOURCES.txt murano.egg-info/dependency_links.txt murano.egg-info/entry_points.txt murano.egg-info/not-zip-safe murano.egg-info/pbr.json murano.egg-info/requires.txt murano.egg-info/top_level.txt murano/api/__init__.py murano/api/versions.py murano/api/middleware/__init__.py murano/api/middleware/context.py murano/api/middleware/ext_context.py murano/api/middleware/fault.py murano/api/middleware/version_negotiation.py murano/api/v1/__init__.py murano/api/v1/actions.py murano/api/v1/catalog.py murano/api/v1/deployments.py murano/api/v1/environments.py murano/api/v1/instance_statistics.py murano/api/v1/request_statistics.py murano/api/v1/router.py murano/api/v1/schemas.py murano/api/v1/services.py murano/api/v1/sessions.py murano/api/v1/static_actions.py murano/api/v1/template_applications.py murano/api/v1/templates.py murano/api/v1/validation_schemas.py murano/cfapi/__init__.py murano/cfapi/cfapi.py murano/cfapi/router.py murano/cmd/__init__.py murano/cmd/api.py murano/cmd/cfapi.py murano/cmd/cfapi_db_manage.py murano/cmd/db_manage.py murano/cmd/engine.py murano/cmd/manage.py murano/cmd/status.py murano/cmd/test_runner.py murano/common/__init__.py murano/common/app_loader.py murano/common/auth_utils.py murano/common/cf_config.py murano/common/config.py murano/common/consts.py murano/common/engine.py murano/common/exceptions.py murano/common/i18n.py murano/common/policy.py murano/common/rpc.py murano/common/server.py murano/common/statservice.py murano/common/utils.py murano/common/uuidutils.py murano/common/wsgi.py murano/common/xmlutils.py murano/common/helpers/__init__.py murano/common/helpers/path.py murano/common/helpers/token_sanitizer.py murano/common/messaging/__init__.py murano/common/messaging/message.py murano/common/messaging/mqclient.py murano/common/messaging/subscription.py murano/common/plugins/__init__.py murano/common/plugins/extensions_loader.py murano/common/plugins/package_types_loader.py murano/common/policies/__init__.py murano/common/policies/action.py murano/common/policies/base.py murano/common/policies/category.py murano/common/policies/deployment.py murano/common/policies/env_template.py murano/common/policies/environment.py murano/common/policies/package.py murano/db/__init__.py murano/db/api.py murano/db/cfapi_models.py murano/db/models.py murano/db/session.py murano/db/catalog/__init__.py murano/db/catalog/api.py murano/db/cfapi_migration/__init__.py murano/db/cfapi_migration/alembic.ini murano/db/cfapi_migration/migration.py murano/db/cfapi_migration/alembic_migrations/README murano/db/cfapi_migration/alembic_migrations/env.py murano/db/cfapi_migration/alembic_migrations/script.py.mako murano/db/cfapi_migration/alembic_migrations/versions/001_initial_version.py murano/db/migration/__init__.py murano/db/migration/alembic.ini murano/db/migration/helpers.py murano/db/migration/migration.py murano/db/migration/alembic_migrations/README murano/db/migration/alembic_migrations/env.py murano/db/migration/alembic_migrations/script.py.mako murano/db/migration/alembic_migrations/versions/001_initial_version.py murano/db/migration/alembic_migrations/versions/002_add_package_supplier_info.py murano/db/migration/alembic_migrations/versions/003_add_action_entry.py murano/db/migration/alembic_migrations/versions/004_change_package_desc_type.py murano/db/migration/alembic_migrations/versions/005_environment-template.py murano/db/migration/alembic_migrations/versions/006_add_task_result.py murano/db/migration/alembic_migrations/versions/007_add_locks.py murano/db/migration/alembic_migrations/versions/008_fix_unique_constraints.py murano/db/migration/alembic_migrations/versions/009_add_cloudfoundry_connections.py murano/db/migration/alembic_migrations/versions/010_remove_unused_networking_column.py murano/db/migration/alembic_migrations/versions/011_add_is_public_to_template.py murano/db/migration/alembic_migrations/versions/012_support_domain_users.py murano/db/migration/alembic_migrations/versions/013_increase_description_text_size.py murano/db/migration/alembic_migrations/versions/014_increase_status_time_resolution.py murano/db/migration/alembic_migrations/versions/015_adding_text_description.py murano/db/migration/alembic_migrations/versions/016_increase_task_description_text_size.py murano/db/services/__init__.py murano/db/services/actions.py murano/db/services/cf_connections.py murano/db/services/core_services.py murano/db/services/environment_templates.py murano/db/services/environments.py murano/db/services/instances.py murano/db/services/sessions.py murano/db/services/stats.py murano/db/sqla/__init__.py murano/db/sqla/types.py murano/dsl/__init__.py murano/dsl/attribute_store.py murano/dsl/constants.py murano/dsl/context_manager.py murano/dsl/dsl.py murano/dsl/dsl_exception.py murano/dsl/dsl_types.py murano/dsl/exceptions.py murano/dsl/executor.py murano/dsl/expressions.py murano/dsl/helpers.py murano/dsl/lhs_expression.py murano/dsl/macros.py murano/dsl/meta.py murano/dsl/murano_method.py murano/dsl/murano_object.py murano/dsl/murano_package.py murano/dsl/murano_property.py murano/dsl/murano_type.py murano/dsl/namespace_resolver.py murano/dsl/object_store.py murano/dsl/package_loader.py murano/dsl/reflection.py murano/dsl/schema_generator.py murano/dsl/serializer.py murano/dsl/session_local_storage.py murano/dsl/typespec.py murano/dsl/virtual_exceptions.py murano/dsl/yaql_expression.py murano/dsl/yaql_functions.py murano/dsl/yaql_integration.py murano/dsl/contracts/__init__.py murano/dsl/contracts/basic.py murano/dsl/contracts/check.py murano/dsl/contracts/contracts.py murano/dsl/contracts/instances.py murano/dsl/principal_objects/__init__.py murano/dsl/principal_objects/exception.py murano/dsl/principal_objects/garbage_collector.py murano/dsl/principal_objects/stack_trace.py murano/dsl/principal_objects/sys_object.py murano/engine/__init__.py murano/engine/execution_session.py murano/engine/mock_context_manager.py murano/engine/murano_package.py murano/engine/package_loader.py murano/engine/yaql_yaml_loader.py murano/engine/system/__init__.py murano/engine/system/agent.py murano/engine/system/agent_listener.py murano/engine/system/common.py murano/engine/system/heat_stack.py murano/engine/system/instance_reporter.py murano/engine/system/logger.py murano/engine/system/metadef_browser.py murano/engine/system/net_explorer.py murano/engine/system/project.py murano/engine/system/resource_manager.py murano/engine/system/status_reporter.py murano/engine/system/system_objects.py murano/engine/system/test_fixture.py murano/engine/system/user.py murano/engine/system/workflowclient.py murano/engine/system/yaql_functions.py murano/hacking/__init__.py murano/hacking/checks.py murano/httpd/__init__.py murano/httpd/murano_api.py murano/locale/en_GB/LC_MESSAGES/murano.po murano/locale/ru/LC_MESSAGES/murano.po murano/packages/__init__.py murano/packages/exceptions.py murano/packages/hot_package.py murano/packages/load_utils.py murano/packages/mpl_package.py murano/packages/package.py murano/packages/package_base.py murano/policy/__init__.py murano/policy/congress_rules.py murano/policy/model_policy_enforcer.py murano/policy/modify/__init__.py murano/policy/modify/actions/__init__.py murano/policy/modify/actions/action_manager.py murano/policy/modify/actions/base.py murano/policy/modify/actions/default_actions.py murano/services/__init__.py murano/services/actions.py murano/services/states.py murano/services/static_actions.py murano/tests/__init__.py murano/tests/functional/README.rst murano/tests/unit/__init__.py murano/tests/unit/base.py murano/tests/unit/test_actions.py murano/tests/unit/test_engine.py murano/tests/unit/test_hacking.py murano/tests/unit/test_heat_stack.py murano/tests/unit/test_utils.py murano/tests/unit/utils.py murano/tests/unit/api/__init__.py murano/tests/unit/api/base.py murano/tests/unit/api/cmd/__init__.py murano/tests/unit/api/cmd/test_test_runner.py murano/tests/unit/api/cmd/test_package/manifest.yaml murano/tests/unit/api/cmd/test_package/Classes/Mytest1.yaml murano/tests/unit/api/cmd/test_package/Classes/Mytest2.yaml murano/tests/unit/api/cmd/test_package/Classes/Mytest3.yaml murano/tests/unit/api/middleware/__init__.py murano/tests/unit/api/middleware/test_context.py murano/tests/unit/api/middleware/test_ext_context.py murano/tests/unit/api/middleware/test_fault_wrapper.py murano/tests/unit/api/middleware/test_version_negotiation.py murano/tests/unit/api/v1/__init__.py murano/tests/unit/api/v1/test_actions.py murano/tests/unit/api/v1/test_catalog.py murano/tests/unit/api/v1/test_deployments.py murano/tests/unit/api/v1/test_env_templates.py murano/tests/unit/api/v1/test_environments.py murano/tests/unit/api/v1/test_instance_statistics.py murano/tests/unit/api/v1/test_schemas.py murano/tests/unit/api/v1/test_services.py murano/tests/unit/api/v1/test_sessions.py murano/tests/unit/api/v1/test_static_actions.py murano/tests/unit/api/v1/cloudfoundry/__init__.py murano/tests/unit/api/v1/cloudfoundry/test_cfapi.py murano/tests/unit/cmd/__init__.py murano/tests/unit/cmd/test_api_workers.py murano/tests/unit/cmd/test_engine_workers.py murano/tests/unit/cmd/test_manage.py murano/tests/unit/cmd/test_status.py murano/tests/unit/common/__init__.py murano/tests/unit/common/test_app_loader.py murano/tests/unit/common/test_auth_utils.py murano/tests/unit/common/test_engine.py murano/tests/unit/common/test_plugin_loader.py murano/tests/unit/common/test_server.py murano/tests/unit/common/test_statservice.py murano/tests/unit/common/test_traverse_helper.py murano/tests/unit/common/test_utils.py murano/tests/unit/common/test_wsgi.py murano/tests/unit/common/helpers/__init__.py murano/tests/unit/common/helpers/test_token_sanitizer.py murano/tests/unit/common/messaging/__init__.py murano/tests/unit/common/messaging/test_mqclient.py murano/tests/unit/core_library/__init__.py murano/tests/unit/core_library/instance/__init__.py murano/tests/unit/core_library/instance/test_destroy/__init__.py murano/tests/unit/core_library/instance/test_destroy/test_destroy.py murano/tests/unit/core_library/instance/test_destroy/meta/Agent.yaml murano/tests/unit/core_library/instance/test_destroy/meta/Environment.yaml murano/tests/unit/core_library/instance/test_destroy/meta/HeatStack.yaml murano/tests/unit/core_library/instance/test_destroy/meta/InstanceNotifier.yaml murano/tests/unit/core_library/instance/test_destroy/meta/Resources.yaml murano/tests/unit/db/__init__.py murano/tests/unit/db/test_catalog.py murano/tests/unit/db/test_models.py murano/tests/unit/db/migration/__init__.py murano/tests/unit/db/migration/test_migrations.py murano/tests/unit/db/migration/test_migrations_base.py murano/tests/unit/db/services/__init__.py murano/tests/unit/db/services/environment_templates.py murano/tests/unit/db/services/test_cf_connections.py murano/tests/unit/db/services/test_core_service.py murano/tests/unit/db/services/test_environments.py murano/tests/unit/db/services/test_instances.py murano/tests/unit/db/services/test_stats.py murano/tests/unit/db/services/test_templates_service.py murano/tests/unit/dsl/__init__.py murano/tests/unit/dsl/test_agent.py murano/tests/unit/dsl/test_assignments.py murano/tests/unit/dsl/test_attribute_store.py murano/tests/unit/dsl/test_call.py murano/tests/unit/dsl/test_concurrency.py murano/tests/unit/dsl/test_config_properties.py murano/tests/unit/dsl/test_construction.py murano/tests/unit/dsl/test_context_manager.py murano/tests/unit/dsl/test_contracts.py murano/tests/unit/dsl/test_dump.py murano/tests/unit/dsl/test_engine_yaql_functions.py murano/tests/unit/dsl/test_exceptions.py murano/tests/unit/dsl/test_execution.py murano/tests/unit/dsl/test_extension_methods.py murano/tests/unit/dsl/test_find_class.py murano/tests/unit/dsl/test_gc.py murano/tests/unit/dsl/test_helpers.py murano/tests/unit/dsl/test_logger.py murano/tests/unit/dsl/test_macros.py murano/tests/unit/dsl/test_meta.py murano/tests/unit/dsl/test_method_param_inheritance.py murano/tests/unit/dsl/test_multiple_inheritance.py murano/tests/unit/dsl/test_objects_copy_merge.py murano/tests/unit/dsl/test_property_access.py murano/tests/unit/dsl/test_property_inititialization.py murano/tests/unit/dsl/test_reflection.py murano/tests/unit/dsl/test_results_serializer.py murano/tests/unit/dsl/test_schema_generation.py murano/tests/unit/dsl/test_session_local_storage.py murano/tests/unit/dsl/test_single_inheritance.py murano/tests/unit/dsl/test_statics.py murano/tests/unit/dsl/test_unicode.py murano/tests/unit/dsl/test_varkwargs.py murano/tests/unit/dsl/test_versioning.py murano/tests/unit/dsl/foundation/__init__.py murano/tests/unit/dsl/foundation/object_model.py murano/tests/unit/dsl/foundation/runner.py murano/tests/unit/dsl/foundation/test_case.py murano/tests/unit/dsl/foundation/test_package_loader.py murano/tests/unit/dsl/meta/AgentListenerTests.yaml murano/tests/unit/dsl/meta/CommonParent.yaml murano/tests/unit/dsl/meta/ConcurrencyTest.yaml murano/tests/unit/dsl/meta/ConfigProperties.yaml murano/tests/unit/dsl/meta/ContractExamples.yaml murano/tests/unit/dsl/meta/CreatedClass1.yaml murano/tests/unit/dsl/meta/CreatedClass2.yaml murano/tests/unit/dsl/meta/CreatingClass.yaml murano/tests/unit/dsl/meta/DerivedFrom2Classes.yaml murano/tests/unit/dsl/meta/Empty.yaml murano/tests/unit/dsl/meta/ExceptionHandling.yaml murano/tests/unit/dsl/meta/MacroExamples.yaml murano/tests/unit/dsl/meta/Node.yaml murano/tests/unit/dsl/meta/ParentClass1.yaml murano/tests/unit/dsl/meta/ParentClass2.yaml murano/tests/unit/dsl/meta/PropertyInit.yaml murano/tests/unit/dsl/meta/SampleClass1.yaml murano/tests/unit/dsl/meta/SampleClass2.yaml murano/tests/unit/dsl/meta/SampleClass3.yaml murano/tests/unit/dsl/meta/SingleInheritanceChild.yaml murano/tests/unit/dsl/meta/SingleInheritanceParent.yaml murano/tests/unit/dsl/meta/TestCall.yaml murano/tests/unit/dsl/meta/TestDump.yaml murano/tests/unit/dsl/meta/TestEngineFunctions.yaml murano/tests/unit/dsl/meta/TestExtensionMethods.yaml murano/tests/unit/dsl/meta/TestFindClass.yaml murano/tests/unit/dsl/meta/TestGC.yaml murano/tests/unit/dsl/meta/TestLogger.yaml murano/tests/unit/dsl/meta/TestMeta.yaml murano/tests/unit/dsl/meta/TestMethodParamInheritance.yaml murano/tests/unit/dsl/meta/TestObjectsCopyMerge.yaml murano/tests/unit/dsl/meta/TestReflection.yaml murano/tests/unit/dsl/meta/TestSchema.yaml murano/tests/unit/dsl/meta/TestStatics.yaml murano/tests/unit/dsl/meta/TestUnicode.yaml murano/tests/unit/dsl/meta/TestVarKwArgs.yaml murano/tests/unit/engine/__init__.py murano/tests/unit/engine/test_mock_context_manager.py murano/tests/unit/engine/test_package_loader.py murano/tests/unit/engine/meta/TestMock.yaml murano/tests/unit/engine/meta/TestMockFixture.yaml murano/tests/unit/engine/meta/manifest.yaml murano/tests/unit/engine/meta/Classes/Mytest.yaml murano/tests/unit/engine/system/__init__.py murano/tests/unit/engine/system/test_agent.py murano/tests/unit/engine/system/test_agent_listener.py murano/tests/unit/engine/system/test_garbage_collector.py murano/tests/unit/engine/system/test_instance_reporter.py murano/tests/unit/engine/system/test_metadef_browser.py murano/tests/unit/engine/system/test_net_explorer.py murano/tests/unit/engine/system/test_test_fixture.py murano/tests/unit/engine/system/test_workflowclient.py murano/tests/unit/engine/system/execution_plans/DeployTelnet.template murano/tests/unit/engine/system/execution_plans/DeployTomcat.template murano/tests/unit/engine/system/execution_plans/application.template murano/tests/unit/engine/system/execution_plans/application_without_files.template murano/tests/unit/engine/system/execution_plans/chef.template murano/tests/unit/engine/system/execution_plans/template_with_files.template murano/tests/unit/packages/__init__.py murano/tests/unit/packages/test_exceptions.py murano/tests/unit/packages/test_load_utils.py murano/tests/unit/packages/test_package_base.py murano/tests/unit/packages/hot_package/__init__.py murano/tests/unit/packages/hot_package/test_hot_package.py murano/tests/unit/packages/hot_package/test.hot.1/properties_manifest.yaml murano/tests/unit/packages/hot_package/test.hot.1/template.yaml murano/tests/unit/packages/hot_package/test.hot.1/Resources/FullTestName murano/tests/unit/packages/hot_package/test.hot.2/template.yaml murano/tests/unit/packages/hot_package/test.hot.2/Resources/FullTestName murano/tests/unit/packages/mpl_package/__init__.py murano/tests/unit/packages/mpl_package/manifest.yaml murano/tests/unit/packages/mpl_package/test_mpl_package.py murano/tests/unit/packages/mpl_package/Classes/test.class1 murano/tests/unit/packages/mpl_package/UI/ui.yaml murano/tests/unit/packages/test_packages/test.hot.v1.app/manifest.yaml murano/tests/unit/packages/test_packages/test.hot.v1.app/template.yaml murano/tests/unit/packages/test_packages/test.hot.v1.app/test_logo.png murano/tests/unit/packages/test_packages/test.hot.v1.app/test_supplier_logo.png murano/tests/unit/packages/test_packages/test.hot.v1.app_with_files/manifest.yaml murano/tests/unit/packages/test_packages/test.hot.v1.app_with_files/template.yaml murano/tests/unit/packages/test_packages/test.hot.v1.app_with_files/Resources/HotFiles/testHeatFile murano/tests/unit/packages/test_packages/test.hot.v1.app_with_files/Resources/HotFiles/middle_file/testHeatFile murano/tests/unit/packages/test_packages/test.hot.v1.app_with_files/Resources/HotFiles/middle_file/inner_file/testHeatFile murano/tests/unit/packages/test_packages/test.hot.v1.app_with_files/Resources/HotFiles/middle_file/inner_file2/testHeatFile murano/tests/unit/packages/test_packages/test.mpl.v1.app/manifest.yaml murano/tests/unit/packages/test_packages/test.mpl.v1.app/manifest_with_broken_logo.yaml murano/tests/unit/packages/test_packages/test.mpl.v1.app/test_logo.png murano/tests/unit/packages/test_packages/test.mpl.v1.app/test_logo.png.not_valid murano/tests/unit/packages/test_packages/test.mpl.v1.app/test_supplier_logo.png murano/tests/unit/packages/test_packages/test.mpl.v1.app/Classes/Thing.yaml murano/tests/unit/packages/versions/__init__.py murano/tests/unit/packages/versions/test_hot_v1.py murano/tests/unit/packages/versions/test_mpl_v1.py murano/tests/unit/policy/__init__.py murano/tests/unit/policy/expected_rules_model.txt murano/tests/unit/policy/expected_rules_model_complex.txt murano/tests/unit/policy/expected_rules_model_renamed.txt murano/tests/unit/policy/expected_rules_model_two_instances.txt murano/tests/unit/policy/expected_rules_wordpress.txt murano/tests/unit/policy/model.yaml murano/tests/unit/policy/model_complex.yaml murano/tests/unit/policy/model_renamed.yaml murano/tests/unit/policy/model_two_instances.yaml murano/tests/unit/policy/model_with_relations.yaml murano/tests/unit/policy/test_congress_rules.py murano/tests/unit/policy/test_model_policy_enforcer.py murano/tests/unit/policy/wordpress.yaml murano/tests/unit/policy/modify/__init__.py murano/tests/unit/policy/modify/actions/__init__.py murano/tests/unit/policy/modify/actions/test_action_manager.py murano/tests/unit/policy/modify/actions/test_default_actions.py murano/tests/unit/policy/modify/actions/meta/ModelExamples.yaml murano/tests/unit/policy/modify/actions/meta/SampleClass1.yaml murano/tests/unit/policy/modify/actions/meta/SampleClass2.yaml murano/tests/unit/services/__init__.py murano/tests/unit/services/test_actions.py murano_tempest_tests/README.rst rally-jobs/README.rst rally-jobs/task-murano.yaml rally-jobs/extra/README.rst rally-jobs/extra/applications/README.rst rally-jobs/extra/applications/HelloReporter/io.murano.apps.HelloReporter/manifest.yaml rally-jobs/extra/applications/HelloReporter/io.murano.apps.HelloReporter/Classes/HelloReporter.yaml rally-jobs/extra/applications/HelloReporter/io.murano.apps.HelloReporter/UI/ui.yaml rally-jobs/plugins/README.rst rally-jobs/plugins/__init__.py releasenotes/notes/.placeholder releasenotes/notes/action-syntax-3f2cbe843801f80d.yaml releasenotes/notes/add-default-security-group-78855a66b960840a.yaml releasenotes/notes/add-upgrade-check-framework-1c069e5a54125d1b.yaml releasenotes/notes/add_api_in_operator-371e3a1d2aec6421.yaml releasenotes/notes/add_timeout_to_linux_class-05d1f573a883f3ce.yaml releasenotes/notes/agent-source-0d2b21262ed10d3e.yaml releasenotes/notes/application_catalog-to-application-catalog-f61d12454a557f79.yaml releasenotes/notes/attributes-owner-type-c321e82f99f96cf1.yaml releasenotes/notes/better-detect-agent-9ef8892a4bfb72cd.yaml releasenotes/notes/bug-1654103-f39ee721d1b90b68.yaml releasenotes/notes/bug-1690179-375599ff3e8f2cd9.yaml releasenotes/notes/cinder-volumes-0412875c1011f8eb.yaml releasenotes/notes/class-config-versioning-23f1d676a3d54c0c.yaml releasenotes/notes/config-network-driver-77c82d151dead620.yaml releasenotes/notes/configure-notifications-0c84a5085c25f6e7.yaml releasenotes/notes/csar-template-plugin-f1682bfee213ae37.yaml releasenotes/notes/decrypt-yaql-function-6651d0f5d73bd58d.yaml releasenotes/notes/delete-app-in-env-template-d8e07d3b860f0441.yaml releasenotes/notes/deployment-list-8c2da5a5efc6dbac.yaml releasenotes/notes/deprecate-json-formatted-policy-file-b41728d03ee008e8.yaml releasenotes/notes/devstack_using_heat_plugin-3dc9feeed36f24ec.yaml releasenotes/notes/drop-py-2-7-37d8f1a13e867edb.yaml releasenotes/notes/drop-python-3-6-and-3-7-77af6bd3473ea5ba.yaml releasenotes/notes/enable-hot-for-glare-8026f2dccad1732e.yaml releasenotes/notes/enable-mocks-a156e7cc1b1d5066.yaml releasenotes/notes/environment-edit-213789159902d4c3.yaml releasenotes/notes/existing-sec-group-522d58bb2fe689a4.yaml releasenotes/notes/extension-methods-f674c2d342670e95.yaml releasenotes/notes/filter-in-package-definition-43edaf12rad81b88.yaml releasenotes/notes/fip-multiple-external-networks-a6f99103ba3b3015.yaml releasenotes/notes/fix-1498097.yaml releasenotes/notes/fix-1528452-0e3bcee9bba89ffa.yaml releasenotes/notes/fix-py3-yaql-function-error-e0b0f8547956f5a6.yaml releasenotes/notes/fixed-adding_text_description-25bd77f36ee370ba.yaml releasenotes/notes/garbage-collection-50e78c4c9d47eba6.yaml releasenotes/notes/gc-collect-165e73bbaf345d74.yaml releasenotes/notes/gc-isdoomed-isdestroyed-9598a6e15dbf36a0.yaml releasenotes/notes/heat_push_async-da3f31b63284a0ea.yaml releasenotes/notes/hot-outputs-merge-eeb9d12356560b48.yaml releasenotes/notes/implement-environment-audit-reports-23bb8009d1dfaecc.yaml releasenotes/notes/keystone-v3-0e287679f7f40a2a.yaml releasenotes/notes/linux-helpers-async-243fc1adbbe5c512.yaml releasenotes/notes/list-environments-of-a-given-project-e45315561478c8a2.yaml releasenotes/notes/magnum-plugin-f372caac83d2cd78.yaml releasenotes/notes/message-signing-07b09e541c2d94d6.yaml releasenotes/notes/meta-e76d5c747b0a0fb6.yaml releasenotes/notes/meta-for-ui-72f5b58c6d17599f.yaml releasenotes/notes/metadata-aware-mixin-41777dd8d1802908.yaml releasenotes/notes/metadata-getter-76907aa1f0325adc.yaml releasenotes/notes/model-load-c1eb24843d30e414.yaml releasenotes/notes/multi-class-yamls-cbb3ef1d8578f41a.yaml releasenotes/notes/multi-regional-apps-b64afbaeafd5b9c5.yaml releasenotes/notes/multiple-api-workers-60492ddc2e3ff0aa.yaml releasenotes/notes/multiple-engine-workers-7fec79572a6a9d01.yaml releasenotes/notes/murano-object-interface-equality-9fc8048be61bd539.yaml releasenotes/notes/muranopl-forms-4a3fb8153f26bbcf.yaml releasenotes/notes/new-contract-framework-1dede2d16b2e9c71.yaml releasenotes/notes/new-objects-resource-leak-fix-33a2eca3a4ccb8af.yaml releasenotes/notes/new-type-format-in-object-model-da6976291057ab31.yaml releasenotes/notes/no-neutron-sec-group-support-2d69082b7226d6c0.yaml releasenotes/notes/objects-copy-objects-merge-8f2752b1a1a18af0.yaml releasenotes/notes/operator-is-9b2b554d3487924d.yaml releasenotes/notes/package_cache-68495dcde223c167.yaml releasenotes/notes/public-template-a8853ac02dcf9396.yaml releasenotes/notes/put-empty-body-d605c2083b239f76.yaml releasenotes/notes/reflection-2fc43b990ea6b980.yaml releasenotes/notes/region-aware-shared-ip-4441113c7cdd3c62.yaml releasenotes/notes/release-cinder-volumes-01c29d28031a94dd.yaml releasenotes/notes/remove-show-categories-42636e9c24c33105.yaml releasenotes/notes/roles-for-requestcontext-43d32d88c3eaaa95.yaml releasenotes/notes/safeloader-cve-2016-4972-19035a2a091ec30a.yaml releasenotes/notes/script-line-endings-db632db9e24237a3.yaml releasenotes/notes/separate-service-broker-from-murano-f6ee48576f51d893.yaml releasenotes/notes/shared-net-port-creation-0eda66be4444cf2f.yaml releasenotes/notes/spec-semver-library-436b0db35fbd4c37.yaml releasenotes/notes/static-actions-61759be796299039.yaml releasenotes/notes/statics-9943fe9873138dac.yaml releasenotes/notes/string-logging-20b8e60a957ba6b7.yaml releasenotes/notes/tag-heat-stacks-3345eb1bda531a6f.yaml releasenotes/notes/template-contract-b71840cbc35eb478.yaml releasenotes/notes/test-runner-output-fix-e942e221be189424.yaml releasenotes/notes/test-runner-set-up-tear-down-a269a31734544a3a.yaml releasenotes/notes/two-phase-instance-deploy-81d37e7987abc792.yaml releasenotes/notes/update-app-in-env-template-08d92b22bd1355f5.yaml releasenotes/notes/use_http_proxy_to_wsgi-9b22d3e60c045689.yaml releasenotes/notes/user-project-6173d7282765b5ca.yaml releasenotes/notes/var-kw-args-c42c31678d8bc747.yaml releasenotes/notes/yaql11-822b503f13992890.yaml releasenotes/source/2023.1.rst 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/queens.rst releasenotes/source/rocky.rst releasenotes/source/stein.rst releasenotes/source/train.rst releasenotes/source/unreleased.rst releasenotes/source/ussuri.rst releasenotes/source/victoria.rst releasenotes/source/wallaby.rst releasenotes/source/xena.rst releasenotes/source/yoga.rst releasenotes/source/zed.rst releasenotes/source/_static/.placeholder releasenotes/source/_templates/.placeholder releasenotes/source/locale/de/LC_MESSAGES/releasenotes.po releasenotes/source/locale/en_GB/LC_MESSAGES/releasenotes.po releasenotes/source/locale/fr/LC_MESSAGES/releasenotes.po tools/lintstack.py tools/lintstack.sh tools/test-setup.sh././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417899.0 murano-16.0.0/murano.egg-info/dependency_links.txt0000664000175000017500000000000100000000000022163 0ustar00zuulzuul00000000000000 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417899.0 murano-16.0.0/murano.egg-info/entry_points.txt0000664000175000017500000000227000000000000021414 0ustar00zuulzuul00000000000000[console_scripts] murano-api = murano.cmd.api:main murano-cfapi = murano.cmd.cfapi:main murano-cfapi-db-manage = murano.cmd.cfapi_db_manage:main murano-db-manage = murano.cmd.db_manage:main murano-engine = murano.cmd.engine:main murano-manage = murano.cmd.manage:main murano-status = murano.cmd.status:main murano-test-runner = murano.cmd.test_runner:main [murano_policy_modify_actions] add-object = murano.policy.modify.actions.default_actions:AddObjectAction add-relation = murano.policy.modify.actions.default_actions:AddRelationAction remove-object = murano.policy.modify.actions.default_actions:RemoveObjectAction remove-relation = murano.policy.modify.actions.default_actions:RemoveRelationAction set-property = murano.policy.modify.actions.default_actions:SetPropertyAction [oslo.config.opts] castellan.config = castellan.options:list_opts keystone_authtoken = keystonemiddleware.opts:list_auth_token_opts murano = murano.opts:list_opts murano.cfapi = murano.opts:list_cfapi_opts [oslo.config.opts.defaults] murano = murano.common.config:set_lib_defaults [oslo.policy.policies] murano = murano.common.policies:list_rules [wsgi_scripts] murano-wsgi-api = murano.httpd.murano_api:init_application ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417899.0 murano-16.0.0/murano.egg-info/not-zip-safe0000664000175000017500000000000100000000000020343 0ustar00zuulzuul00000000000000 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417899.0 murano-16.0.0/murano.egg-info/pbr.json0000664000175000017500000000005700000000000017575 0ustar00zuulzuul00000000000000{"git_version": "c898a310", "is_release": true}././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417899.0 murano-16.0.0/murano.egg-info/requires.txt0000664000175000017500000000160400000000000020516 0ustar00zuulzuul00000000000000Babel!=2.4.0,>=2.3.4 Paste>=2.0.2 PasteDeploy>=1.5.0 PyYAML>=5.1 Routes>=2.3.1 SQLAlchemy!=1.1.5,!=1.1.6,!=1.1.7,!=1.1.8,>=1.0.10 WebOb>=1.7.1 alembic>=0.9.6 castellan>=0.18.0 cryptography>=2.7 debtcollector>=1.2.0 eventlet>=0.26.0 jsonpatch!=1.20,>=1.16 jsonschema>=3.2.0 keystoneauth1>=3.8.0 keystonemiddleware>=4.17.0 kombu>=4.6.1 netaddr>=0.7.18 oslo.concurrency>=3.26.0 oslo.config>=6.8.0 oslo.context>=2.22.0 oslo.db>=4.44.0 oslo.i18n>=3.15.3 oslo.log>=3.36.0 oslo.messaging>=5.29.0 oslo.middleware>=3.31.0 oslo.policy>=3.6.0 oslo.serialization!=2.19.1,>=2.18.0 oslo.service>=1.31.0 oslo.upgradecheck>=1.3.0 oslo.utils>=4.5.0 pbr!=2.1.0,>=2.0.0 psutil>=3.2.2 python-heatclient>=1.10.0 python-keystoneclient>=3.17.0 python-mistralclient!=3.2.0,>=3.1.0 python-muranoclient>=0.8.2 python-neutronclient>=6.7.0 semantic-version>=2.8.2 stevedore>=1.20.0 tenacity>=4.12.0 testtools>=2.2.0 yaql>=1.1.3 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417899.0 murano-16.0.0/murano.egg-info/top_level.txt0000664000175000017500000000000700000000000020644 0ustar00zuulzuul00000000000000murano ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1696417899.8611813 murano-16.0.0/murano_tempest_tests/0000775000175000017500000000000000000000000017406 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/murano_tempest_tests/README.rst0000664000175000017500000000015500000000000021076 0ustar00zuulzuul00000000000000===== MOVED ===== The murano tempest plugin has moved to http://opendev.org/openstack/murano-tempest-plugin ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1696417899.8611813 murano-16.0.0/rally-jobs/0000775000175000017500000000000000000000000015200 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/rally-jobs/README.rst0000664000175000017500000000170200000000000016667 0ustar00zuulzuul00000000000000Rally job related files ======================= This directory contains rally tasks and plugins that are run by OpenStack CI. Structure --------- * **task-murano.yaml** is a task that will be run in gates against OpenStack deployed by DevStack with installed Rally & Murano. * **plugins** - directory where you can add rally plugins. Almost everything in Rally is plugin. Benchmark context, Benchmark scenario, SLA checks, Generic cleanup resources, .... * **extra** - all files from this directory will be copy-pasted to gates, which makes it possible to use absolute paths in rally tasks. Files will be in ~/.rally/extra/* Useful links ------------ * More about rally: https://rally.readthedocs.org/en/latest/ * How to add rally-gates: https://rally.readthedocs.org/en/latest/gates.html * About plugins: https://rally.readthedocs.org/en/latest/plugins.html * Plugin samples: https://github.com/openstack/rally/tree/master/samples/plugins ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1696417899.8611813 murano-16.0.0/rally-jobs/extra/0000775000175000017500000000000000000000000016323 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/rally-jobs/extra/README.rst0000664000175000017500000000026500000000000020015 0ustar00zuulzuul00000000000000Extra files =========== All files from this directory will be copy-pasted to gates, which makes it possible to use absolute paths in rally tasks. Files will be in ~/.rally/extra/* ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1696417899.8611813 murano-16.0.0/rally-jobs/extra/applications/0000775000175000017500000000000000000000000021011 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1696417899.6811805 murano-16.0.0/rally-jobs/extra/applications/HelloReporter/0000775000175000017500000000000000000000000023577 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1696417899.8611813 murano-16.0.0/rally-jobs/extra/applications/HelloReporter/io.murano.apps.HelloReporter/0000775000175000017500000000000000000000000031235 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1696417899.8611813 murano-16.0.0/rally-jobs/extra/applications/HelloReporter/io.murano.apps.HelloReporter/Classes/0000775000175000017500000000000000000000000032632 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000022100000000000011450 xustar0000000000000000123 path=murano-16.0.0/rally-jobs/extra/applications/HelloReporter/io.murano.apps.HelloReporter/Classes/HelloReporter.yaml 22 mtime=1696417875.0 murano-16.0.0/rally-jobs/extra/applications/HelloReporter/io.murano.apps.HelloReporter/Classes/Hello0000664000175000017500000000072100000000000033620 0ustar00zuulzuul00000000000000Namespaces: =: io.murano.apps std: io.murano sys: io.murano.system Name: HelloReporter Extends: std:Application Properties: name: Contract: $.string().notNull() Workflow: initialize: Body: - $.environment: $.find(std:Environment).require() deploy: Body: - If: not $.getAttr(deployed, false) Then: - $.environment.reporter.report($this, 'Starting deployment! Hello!') - $.setAttr(deployed, True) ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1696417899.8611813 murano-16.0.0/rally-jobs/extra/applications/HelloReporter/io.murano.apps.HelloReporter/UI/0000775000175000017500000000000000000000000031552 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/rally-jobs/extra/applications/HelloReporter/io.murano.apps.HelloReporter/UI/ui.yaml0000664000175000017500000000070100000000000033051 0ustar00zuulzuul00000000000000Version: 2 Application: ?: type: io.murano.apps.HelloReporter name: $.appConfiguration.name Forms: - appConfiguration: fields: - name: name type: string label: Application Name description: >- Enter a desired name for the application. Just A-Z, a-z, 0-9, dash and underline are allowed - name: unitNamingPattern type: string required: false ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/rally-jobs/extra/applications/HelloReporter/io.murano.apps.HelloReporter/manifest.yaml0000664000175000017500000000036200000000000033730 0ustar00zuulzuul00000000000000Format: 1.0 Type: Application FullName: io.murano.apps.HelloReporter Name: HelloReporter Description: | HelloReporter test app. Author: 'Mirantis, Inc' Tags: [App, Test, HelloWorld] Classes: io.murano.apps.HelloReporter: HelloReporter.yaml ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/rally-jobs/extra/applications/README.rst0000664000175000017500000000064400000000000022504 0ustar00zuulzuul00000000000000Murano applications =================== Files for Murano benchmarking Structure --------- * / directories. Each directory stores a simple Murano package that is used to prepare the Murano context that is used to deploy an environment with a package. Other files needed for applications can be placed here as well. Useful links ------------ * More about Murano: http://murano.readthedocs.org/ ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1696417899.8611813 murano-16.0.0/rally-jobs/plugins/0000775000175000017500000000000000000000000016661 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/rally-jobs/plugins/README.rst0000664000175000017500000000063000000000000020347 0ustar00zuulzuul00000000000000Rally plugins ============= All *.py modules from this directory will be auto-loaded by Rally and all plugins will be discoverable. There is no need of any extra configuration and there is no difference between writing them here and in the Rally code base. Note, however, that it is better to push all interesting and useful benchmarks to the Rally code base: this simplifies administration for Operators. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/rally-jobs/plugins/__init__.py0000664000175000017500000000000000000000000020760 0ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/rally-jobs/task-murano.yaml0000664000175000017500000000174500000000000020334 0ustar00zuulzuul00000000000000--- MuranoEnvironments.list_environments: - runner: type: "constant" times: 30 concurrency: 4 context: users: tenants: 2 users_per_tenant: 2 sla: failure_rate: max: 0 MuranoEnvironments.create_and_delete_environment: - runner: type: "constant" times: 20 concurrency: 2 context: users: tenants: 2 users_per_tenant: 2 sla: failure_rate: max: 0 MuranoEnvironments.create_and_deploy_environment: - args: packages_per_env: 2 runner: type: "constant" times: 8 concurrency: 2 context: users: tenants: 2 users_per_tenant: 2 murano_packages: app_package: "~/.rally/extra/applications/HelloReporter/io.murano.apps.HelloReporter/" roles: - "admin" sla: failure_rate: max: 0 ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1696417899.6811805 murano-16.0.0/releasenotes/0000775000175000017500000000000000000000000015613 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1696417899.8811812 murano-16.0.0/releasenotes/notes/0000775000175000017500000000000000000000000016743 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/releasenotes/notes/.placeholder0000664000175000017500000000000000000000000021214 0ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/releasenotes/notes/action-syntax-3f2cbe843801f80d.yaml0000664000175000017500000000045000000000000024643 0ustar00zuulzuul00000000000000--- features: - Added a new manifest format 1.4.0. Introduced the 'Scope' keyword for class methods to declare a method's accessibility from outside through the API call. deprecations: - Deprecated the 'Usage Action' keyword. For format versions >= 1.4.0, use 'Scope Public' instead. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/releasenotes/notes/add-default-security-group-78855a66b960840a.yaml0000664000175000017500000000040700000000000027012 0ustar00zuulzuul00000000000000--- fixes: - added default rules to NeutronSecurityGroupManager to avoid error if `createDefaultInstanceSecurityGroupRules()` method isn't extended in inheritor and SecurityGroups isn't created in application with call of `addGroupIngress()` method.././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/releasenotes/notes/add-upgrade-check-framework-1c069e5a54125d1b.yaml0000664000175000017500000000072300000000000027177 0ustar00zuulzuul00000000000000--- prelude: > Added new tool ``murano-status upgrade check``. features: - | New framework for ``murano-status upgrade check`` command is added. This framework allows adding various checks which can be run before a Murano upgrade to ensure if the upgrade can be performed safely. upgrade: - | Operator can now use new CLI tool ``murano-status upgrade check`` to check if Murano deployment can be safely upgraded from N-1 to N release. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/releasenotes/notes/add_api_in_operator-371e3a1d2aec6421.yaml0000664000175000017500000000053100000000000026006 0ustar00zuulzuul00000000000000--- features: - Implemented the capability for API endpoint ``/catalog/packages`` to filter 'id', 'category', 'tag' properties using the 'in' operator. An example of using the 'in' operator for 'id' is 'id=in:id1,id2,id3'. This filter is added using syntax that conforms to the latest guidelines from the OpenStack API-WG. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/releasenotes/notes/add_timeout_to_linux_class-05d1f573a883f3ce.yaml0000664000175000017500000000022000000000000027446 0ustar00zuulzuul00000000000000--- features: - Added the ``timeout`` parameter to ``runCommand`` and ``putFile`` methods of the ``io.murano.configuration.Linux`` class. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/releasenotes/notes/agent-source-0d2b21262ed10d3e.yaml0000664000175000017500000000050400000000000024412 0ustar00zuulzuul00000000000000--- features: - | A configuration file setting `[engine]/agent_source` was added. The value is then used for the `pip install` command to install the murano-agent. Since pip accepts http and git URLs, this can be used to install agent from the custom git repo or install version other than the latest. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/releasenotes/notes/application_catalog-to-application-catalog-f61d12454a557f79.yaml0000664000175000017500000000046700000000000032252 0ustar00zuulzuul00000000000000--- fixes: - Murano is now able to work with keystone configured to use a templated catalog. upgrade: - When updating to Mitaka, the operator should update service name and type for endpoint in keystone from "application_catalog" to "application-catalog" if SQL is used for catalog back-end driver. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/releasenotes/notes/attributes-owner-type-c321e82f99f96cf1.yaml0000664000175000017500000000055300000000000026363 0ustar00zuulzuul00000000000000--- features: - Now all native MuranoPL methods (those that are written in Python) have "?muranoMethod" metadata key referring to MuranoMethod instance for the method. fixes: - It was impossible to explicitly provide attribute owner class to getAttr/setAttr methods without using namespace prefix or if the type was not from the core library. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/releasenotes/notes/better-detect-agent-9ef8892a4bfb72cd.yaml0000664000175000017500000000047600000000000026060 0ustar00zuulzuul00000000000000--- fixes: - Core Library's init scripts used to have various problems detecting pre-installed (by DIB) murano-agent on non-ubuntu images. Agent setup script now checks wider list of directories before attempting to install murano-agnet and service script now does not impose strict script location. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/releasenotes/notes/bug-1654103-f39ee721d1b90b68.yaml0000664000175000017500000000007700000000000023454 0ustar00zuulzuul00000000000000--- fixes: - | Now admin can delete user's environments. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/releasenotes/notes/bug-1690179-375599ff3e8f2cd9.yaml0000664000175000017500000000033000000000000023505 0ustar00zuulzuul00000000000000--- fixes: - | Remove hardcoded constant called 'ITERATORS_LIMIT', that can be exceeded (2000) having big amount of objects. Introduce dsl_iterators_limit configuration option instead of constant. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/releasenotes/notes/cinder-volumes-0412875c1011f8eb.yaml0000664000175000017500000000040700000000000024627 0ustar00zuulzuul00000000000000--- features: - Classes to work with Cinder volumes were added to core library. Now it is possible to create new volume from various sources or use existing volume. Also it is possible to attach volumes to instances and boot instances from volumes. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/releasenotes/notes/class-config-versioning-23f1d676a3d54c0c.yaml0000664000175000017500000000065300000000000026573 0ustar00zuulzuul00000000000000--- features: - "Class configs are now also versioned. For class foo.bar version 1.2.3 the following file names will be examined: foo.bar-1.2.3.json foo.bar-1.2.3.yaml foo.bar-1.2.json foo.bar-1.2.yaml foo.bar-1.json foo.bar-1.yaml In addition for classes of version 0.x.y file name without version suffix are also examined as a last attempt so the backward compatibility is retained" ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/releasenotes/notes/config-network-driver-77c82d151dead620.yaml0000664000175000017500000000050700000000000026267 0ustar00zuulzuul00000000000000--- features: - Added the ``driver`` configuration option to the ``networking`` group. It allows to explicitly select the networking driver. It supports 'neutron' and 'nova' options. If set to ``None`` (default), murano attempts to use 'neutron' if available, 'nova' otherwise. The change is backward compatible. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/releasenotes/notes/configure-notifications-0c84a5085c25f6e7.yaml0000664000175000017500000000053000000000000026616 0ustar00zuulzuul00000000000000features: - It is now possible to configure the notifications to use a different transport URL than the RPCs. These could potentially be completely different message broker hosts (though they doesn't need to be). If the notification-specific configuration is not provided, the notifier will use the same transport as the RPCs.././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/releasenotes/notes/csar-template-plugin-f1682bfee213ae37.yaml0000664000175000017500000000021300000000000026154 0ustar00zuulzuul00000000000000--- features: - New plugin 'murano_heat-translator_plugin' was added. Now it is possible to deploy applications from CSAR templates. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/releasenotes/notes/decrypt-yaql-function-6651d0f5d73bd58d.yaml0000664000175000017500000000052500000000000026315 0ustar00zuulzuul00000000000000--- features: - | Added a new yaql function 'decryptData' which pairs with 'encryptData' on the dashboard side. Application authors can use these functions to secure sensitive input to their Murano applications such as passwords. Requires a valid secret storage backend (e.g. Barbican) to be configured via Castellan. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/releasenotes/notes/delete-app-in-env-template-d8e07d3b860f0441.yaml0000664000175000017500000000020500000000000027002 0ustar00zuulzuul00000000000000fixes: - API call for deleting a service from environment template did not return result of its operation. The issue is fixed. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/releasenotes/notes/deployment-list-8c2da5a5efc6dbac.yaml0000664000175000017500000000030300000000000025544 0ustar00zuulzuul00000000000000--- features: - It is now possible to make a GET request to '/deployments' endpoint. This will result in deployments for all environments in a specific project (tenant) being returned. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/releasenotes/notes/deprecate-json-formatted-policy-file-b41728d03ee008e8.yaml0000664000175000017500000000176000000000000031066 0ustar00zuulzuul00000000000000--- upgrade: - | The default value of ``[oslo_policy] policy_file`` config option has been changed from ``policy.json`` to ``policy.yaml``. Operators who are utilizing customized or previously generated static policy JSON files (which are not needed by default), should generate new policy files or convert them in YAML format. Use the `oslopolicy-convert-json-to-yaml `_ tool to convert a JSON to YAML formatted policy file in backward compatible way. deprecations: - | Use of JSON policy files was deprecated by the ``oslo.policy`` library during the Victoria development cycle. As a result, this deprecation is being noted in the Wallaby cycle with an anticipated future removal of support by ``oslo.policy``. As such operators will need to convert to YAML policy files. Please see the upgrade notes for details on migration of any custom policy files. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/releasenotes/notes/devstack_using_heat_plugin-3dc9feeed36f24ec.yaml0000664000175000017500000000020200000000000027737 0ustar00zuulzuul00000000000000--- other: - Since Newton release, heat is available as a devstack plugin. So we remove heat as enable_service in devstack. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/releasenotes/notes/drop-py-2-7-37d8f1a13e867edb.yaml0000664000175000017500000000031200000000000024024 0ustar00zuulzuul00000000000000--- upgrade: - | Python 2.7 support has been dropped. Last release of Murano to support python 2.7 is OpenStack Train. The minimum version of Python now supported by Murano is Python 3.6. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/releasenotes/notes/drop-python-3-6-and-3-7-77af6bd3473ea5ba.yaml0000664000175000017500000000020000000000000026026 0ustar00zuulzuul00000000000000--- upgrade: - | Python 3.6 & 3.7 support has been dropped. The minimum version of Python now supported is Python 3.8.././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/releasenotes/notes/enable-hot-for-glare-8026f2dccad1732e.yaml0000664000175000017500000000046500000000000026014 0ustar00zuulzuul00000000000000--- fixes: - Fixed a bug when the UI dialog was not displayed in Murano Dashboard for applications which don't have UI definitions bundled in the package but generate them based on the package contents instead. This usually affected HOT-based packages and other non-muranopl-based applications. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/releasenotes/notes/enable-mocks-a156e7cc1b1d5066.yaml0000664000175000017500000000047500000000000024402 0ustar00zuulzuul00000000000000--- features: - Enable mocks in MuranoPL tests cases. - Added :command:`murano-test-tunner` command to run murano package tests. - Introduced two YAQL *inject* functions to enable mocks ``def inject(target, target_method, mock_object, mock_name)`` and ``def inject(target, target_method, yaql_expr)``. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/releasenotes/notes/environment-edit-213789159902d4c3.yaml0000664000175000017500000000042400000000000025041 0ustar00zuulzuul00000000000000--- features: - /environments/ENV_ID/model/PATH endpoint added. GET request responds with the subsection of ENV_ID's object model located in its PATH. PATCH request applies json-patch from request body to ENV_ID's model. It does not contain PATH in the URL. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/releasenotes/notes/existing-sec-group-522d58bb2fe689a4.yaml0000664000175000017500000000025700000000000025613 0ustar00zuulzuul00000000000000--- features: - | Users can now assign an existing security group to an application as an alternative to using the one created by Murano's ``SecurityGroupManager``. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/releasenotes/notes/extension-methods-f674c2d342670e95.yaml0000664000175000017500000000131000000000000025365 0ustar00zuulzuul00000000000000--- features: - > New method type: extension methods. Extension methods enable you to "add" methods to existing types without modifying the original type. Extension methods are a special kind of static method, but they are called as if they were instance methods on the extended type. Extension methods are identified by "Usage: Extension" and the type they extend is determined by their first argument contract. Thus such methods must have at lease one parameter. - > New type-level keyword "Import" which can be either list or scalar that specifies type names which extensions methods should be imported into class context and thus become available to type members. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/releasenotes/notes/filter-in-package-definition-43edaf12rad81b88.yaml0000664000175000017500000000011200000000000027622 0ustar00zuulzuul00000000000000--- features: - Added filter by 'Name' which only matches package name. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/releasenotes/notes/fip-multiple-external-networks-a6f99103ba3b3015.yaml0000664000175000017500000000026200000000000030051 0ustar00zuulzuul00000000000000--- fixes: - Murano is now able to assign correct floating IPs when using multiple external networks. It attempts to choose one that shares a router with internal network. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/releasenotes/notes/fix-1498097.yaml0000664000175000017500000000014600000000000021261 0ustar00zuulzuul00000000000000fixes: - Avoid race condition during parallel upload of packages, when packages have same tags. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/releasenotes/notes/fix-1528452-0e3bcee9bba89ffa.yaml0000664000175000017500000000014500000000000024045 0ustar00zuulzuul00000000000000fixes: - Fixed incorrect murano behaviour if deployed on devstack with keystone v3 by default. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/releasenotes/notes/fix-py3-yaql-function-error-e0b0f8547956f5a6.yaml0000664000175000017500000000021100000000000027206 0ustar00zuulzuul00000000000000--- fixes: - Avoid the `'method' object has no attribute '__yaql_function__'` error when calling some YAQL functions with Python 3 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/releasenotes/notes/fixed-adding_text_description-25bd77f36ee370ba.yaml0000664000175000017500000000030700000000000030117 0ustar00zuulzuul00000000000000features: - Added the ``description_text`` field to environment and environment templates database tables and respective API objects. upgrade: - New database migration 015 has to be applied. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/releasenotes/notes/garbage-collection-50e78c4c9d47eba6.yaml0000664000175000017500000000104400000000000025655 0ustar00zuulzuul00000000000000--- features: - Introduced a new MuranoPL class ``io.murano.system.GC`` Now MuranoPL garbage collector can be used to set up destruction dependencies between murano objects. If object Foo is subscribed to object Bar's destruction, it will be notified through a specific handler. If both Foo and Bar are going to be destroyed during one execution session, Foo will be destroyed after Bar. You can omit the handler, in this case destruction order will also be preserved. Handler can be a static or a usual function. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/releasenotes/notes/gc-collect-165e73bbaf345d74.yaml0000664000175000017500000000141500000000000024062 0ustar00zuulzuul00000000000000--- features: - New on-request garbage collector for MuranoPL objects were implemented. Garbage collection is triggered by io.murano.system.GC.collect() static method. Garbage collector destroys all object that are not reachable anymore. GC can handle objects with cross-references and isolated object graphs. When portion of object model becomes not reachable it destroyed in predictable order such that child objects get destroyed before their parents and, when possible, before objects that are subscribed to their destruction notifications. - Internally, both pre-deployment garbage collection (that was done by comparison of ``Objects`` and ``ObjectsCopy``) and post-deployment orphan object collection are now done through the new GC. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/releasenotes/notes/gc-isdoomed-isdestroyed-9598a6e15dbf36a0.yaml0000664000175000017500000000051200000000000026577 0ustar00zuulzuul00000000000000--- features: - io.murano.system.GC.isDoomed() static method was added. It can be used within the ``.destroy`` method to test if other object is also going to be destroyed. - io.murano.system.GC.isDestroyed() static method was added. It checks if the object is destroyed and thus no methods can be invoked on it. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/releasenotes/notes/heat_push_async-da3f31b63284a0ea.yaml0000664000175000017500000000016200000000000025262 0ustar00zuulzuul00000000000000--- features: - io.murano.system.HeatStack.push can be called with async => true flag for asynchronous push ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/releasenotes/notes/hot-outputs-merge-eeb9d12356560b48.yaml0000664000175000017500000000033700000000000025402 0ustar00zuulzuul00000000000000--- features: - All HOT template outputs are put into a single dictionary property 'templateOutputs' rather than in a generated property per each output. As a result there are no more constraints on output names. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/releasenotes/notes/implement-environment-audit-reports-23bb8009d1dfaecc.yaml0000664000175000017500000000065000000000000031406 0ustar00zuulzuul00000000000000--- features: - | Add notifications about environment events that are required for tracking. These are AMQP notifications and oslo.messaging library is used for sending them. The follow event types are provided: environment.deploy.end, environment.delete.end, environment.exists There are 2 new configuration options controlling these notifications: stats.env_audit_period, env_audit_enabled. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/releasenotes/notes/keystone-v3-0e287679f7f40a2a.yaml0000664000175000017500000000047500000000000024170 0ustar00zuulzuul00000000000000--- fixes: - Removed the need for Keystone v2 options (admin_user, admin_password, admin_tenant_name) when Keystone v3 is in use. - Previously murano assumed that the service user and service project are in the 'Default' domain. These values can now be set in ``keystone_authtoken`` config group. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/releasenotes/notes/linux-helpers-async-243fc1adbbe5c512.yaml0000664000175000017500000000022000000000000026071 0ustar00zuulzuul00000000000000--- features: - Implemented the capability for the helper methods of Linux class to run concurrently if executed for different VM agents. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/releasenotes/notes/list-environments-of-a-given-project-e45315561478c8a2.yaml0000664000175000017500000000017200000000000030722 0ustar00zuulzuul00000000000000--- features: - > "List Environments" API call is now able to filter environments by an owner project (tenant). ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/releasenotes/notes/magnum-plugin-f372caac83d2cd78.yaml0000664000175000017500000000017600000000000024771 0ustar00zuulzuul00000000000000--- features: - Added magnum plugin to murano, that allows creating/deleting of magnum baymodels and bays from MuranoPL ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/releasenotes/notes/message-signing-07b09e541c2d94d6.yaml0000664000175000017500000000045500000000000025051 0ustar00zuulzuul00000000000000--- features: - | Murano engine can be configured to sign all the RabbitMQ messages sent to the agents. When the RSA key is provided, engine will provide agents with its public part and sign all the messages sent. Agents then will ignore any command that was not sent by the engine. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/releasenotes/notes/meta-e76d5c747b0a0fb6.yaml0000664000175000017500000000113400000000000023052 0ustar00zuulzuul00000000000000--- features: - Added ability to extend MuranoPL entities with custom metadata. - > For MuranoPL classes new key "Usage" was added. By default it is "Class". But it can also be "Meta" to define meta-class. Meta-class has all the capabilities of regular classes and in addition has 3 new attributes: Cardinality, Applies and Inherited. - It is possible to attach meta-class instances to packages, classes (including other meta-classes), properties, methods and method arguments. Each of them got new "Meta" key containing list (or single scalar) of meta-class instances. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/releasenotes/notes/meta-for-ui-72f5b58c6d17599f.yaml0000664000175000017500000000044700000000000024143 0ustar00zuulzuul00000000000000--- features: - Added the following meta-classes to the core library - ``Title`` ``Description`` ``HelpText`` ``Hidden`` ``Section`` ``Position`` ``ModelBuilder``. These classes will later be used to implement dynamic object model generation. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/releasenotes/notes/metadata-aware-mixin-41777dd8d1802908.yaml0000664000175000017500000000022700000000000025636 0ustar00zuulzuul00000000000000--- features: - Added a MetadataAware mixin class capable to retrieve the metadata attributes from the implementing objects and all its parents. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/releasenotes/notes/metadata-getter-76907aa1f0325adc.yaml0000664000175000017500000000033100000000000025101 0ustar00zuulzuul00000000000000--- features: - Added a ``metadata()`` yaql function to retrieve the meta information about the object, stored in the "?/metadata" section of object model. other: - Bumped the RUNTIME_VERSION attribute to 1.5 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/releasenotes/notes/model-load-c1eb24843d30e414.yaml0000664000175000017500000000102000000000000023757 0ustar00zuulzuul00000000000000--- features: - Added an overload of the new function - ``new($model, $owner)``. It loads complete object graph in a single call. Objects in the model can have cross references. In that case, this is the only way to instantiate the graph. Objects might be specified either in object model format (with '?' attribute or in MuranoPL format (used for Meta definitions). - The contract ``class()`` now uses the same approach to load classes from dictionaries. Thus the same two syntaxes apply there as well. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/releasenotes/notes/multi-class-yamls-cbb3ef1d8578f41a.yaml0000664000175000017500000000076200000000000025573 0ustar00zuulzuul00000000000000--- features: - Now it is possible to have several classes in one YAML file. Classes are separated using YAML document separator (3 dashes). Empty documents are skipped. If the class doesn't have Namespace section corresponding section from the previous class in the same file is used. Thus it is possible to declare namespace prefixes once at the file header. Even if there are several classes in one file all of them are still required to be declared in manifest file. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/releasenotes/notes/multi-regional-apps-b64afbaeafd5b9c5.yaml0000664000175000017500000000171500000000000026317 0ustar00zuulzuul00000000000000--- features: - Added Support for application deployment across OpenStack regions. Now, all OpenStack resource classes inherit from ``io.murano.CloudResource`` that provides ``.getRegion()`` method and ``regionName`` property. This allows to assign resources to different regions. ``.getRegion()`` returns ``io.murano.CloudRegion`` instance that resource or its parent belongs to. ``CloudRegion`` has interface similar to ``Environment`` class and is the correct way to get ``HeatStack`` instance associated with the region, default network configuration, security group manager and agent listener instances. ``Environment`` now acts as default region so backward compatibility is not broken. However new applications should not use environment to set security group rules but rather a region(s) of their instance(s) in order to work correctly when their instances were configured to use region other than the default. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/releasenotes/notes/multiple-api-workers-60492ddc2e3ff0aa.yaml0000664000175000017500000000055200000000000026272 0ustar00zuulzuul00000000000000--- features: - Added the ``api_workers`` option to ``murano`` config group. It controls the number of API workers launched by murano. If not set, it would default to the number of CPUs available. deprecations: - Renamed the ``workers`` option from the ``engine`` group to ``engine_workers`` to reduce ambiguity with the ``api_workers`` option. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/releasenotes/notes/multiple-engine-workers-7fec79572a6a9d01.yaml0000664000175000017500000000021100000000000026637 0ustar00zuulzuul00000000000000--- features: - Add multiple engine workers issues: - Enabling multiple workers might break workflows under BSD and Windows systems ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/releasenotes/notes/murano-object-interface-equality-9fc8048be61bd539.yaml0000664000175000017500000000015600000000000030417 0ustar00zuulzuul00000000000000--- fixes: - Equality check (assertEqual) in test-runner can now properly compare two MuranoPl objects. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/releasenotes/notes/muranopl-forms-4a3fb8153f26bbcf.yaml0000664000175000017500000000062200000000000025161 0ustar00zuulzuul00000000000000--- features: - Added a new engine RPC call to generate json-schema from MuranoPL class. The schema may be generated either from the entire class or for specific model builders - static actions that can be used to generate object model from their input. Class schema is built by inspecting class properties and method schema using the same algorithm but applied to its arguments. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/releasenotes/notes/new-contract-framework-1dede2d16b2e9c71.yaml0000664000175000017500000000450400000000000026603 0ustar00zuulzuul00000000000000--- features: - Implemented a new framework for MuranoPL contracts. Now, instead of several independent implementations of the same yaql methods (string(), class() etc.) all implementations of the same method are combined into single class. Therefore, we now have a class per contract method. This also simplifies development of new contracts. Each such class can provide methods for data transformation (default contract usage), validation that is used to decide if the method can be considered an extension method for the value, and json schema generation method that was moved from the schema generator script. - Previously, when a class overrode a property from its parent class the value was stored separately for both of them, transformed by each of the contracts. Thus each class saw the value of its contract. In absolute majority of the cases, the observed value was the same. However, if the contracts were compatible on the provided value (say int() and string() contracts on the value "123") they were different. This is considered to be a bad pattern. Now, the value is stored only once per object and transformed by the contract defined in the actual object type. All base contracts are used to validate the transformed object thus this pattern will not work anymore. - The value that is stored in the object's properties is obtained by executing special "finalize" contract implementation which by default returns the input value unmodified. Because validation happens on the transformed value before it gets finalized it is possible for transformation to return a value that will pass the validation though the final value won't. This is used to relax the template() contract limitation that prevented child class from excluding additional properties from the template. - The ``string()`` contract no longer converts everything to string values. Now it only converts scalar values to strings. Previous behavior allowed ``string()`` property to accept lists and convert them to their Python string representation which is clearly not what developers expected. - Due to refactoring, contracts work a little bit faster because there is no more need to generate yaql function definition for each contract method on each call. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/releasenotes/notes/new-objects-resource-leak-fix-33a2eca3a4ccb8af.yaml0000664000175000017500000000016300000000000030070 0ustar00zuulzuul00000000000000--- fixes: - Prevented the resource leak for objects created during deployment with ``new()`` function call. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/releasenotes/notes/new-type-format-in-object-model-da6976291057ab31.yaml0000664000175000017500000000101300000000000027703 0ustar00zuulzuul00000000000000--- features: - Changed the type representation in object model. Previous format was to have three attributes in "?" section of the object - type, classVersion and package where only the "type" is mandatory. Now they are merged into single attribute "type" that has a format ``typeName/version@package``. Version and package parts are still optional. upgrades: - Any of the tools, that inspected object model should be updated to expect new object format representation ``typeName/version@package`` ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/releasenotes/notes/no-neutron-sec-group-support-2d69082b7226d6c0.yaml0000664000175000017500000000030600000000000027422 0ustar00zuulzuul00000000000000--- fixes: - Murano is now able to deploy applications in the environments with disabled Neutron Security Groups. Detection is based on the presence of 'security-group' Neutron extension. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/releasenotes/notes/objects-copy-objects-merge-8f2752b1a1a18af0.yaml0000664000175000017500000000215600000000000027161 0ustar00zuulzuul00000000000000--- features: - Previously, when pre-deployment garbage collection occurred it executed ``.destroy`` method for objects that were present in the ``ObjectsCopy`` section of the object model (which is the snapshot of the model after last deployment) and not present in the current model anymore (because they were deleted through the API between deployments). If the destroyed objects were to access another object that was not deleted it was accessing its copy from the ``ObjectsCopy``. Thus any changes to the internal state made by that object were lost after the garbage collection finished (that is, before the ``.deploy`` method call) and could not affect the deployment. Now, if the object is present in both ``Objects`` and ``ObjectsCopy``, a single instance (the one from ``Objects``) is used for both garbage collection and deployment. As a consequence, instances (in their ``.destroy`` method) now may observe changes made to other objects they refer if they were not deleted, but modified through the API. In some rare cases, it may break existing applications. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/releasenotes/notes/operator-is-9b2b554d3487924d.yaml0000664000175000017500000000020400000000000024156 0ustar00zuulzuul00000000000000--- features: - New operator *is* was added to MuranoPL. Now it is possible to test if MuranoPL object is of particular type. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/releasenotes/notes/package_cache-68495dcde223c167.yaml0000664000175000017500000000063400000000000024515 0ustar00zuulzuul00000000000000--- features: - Murano engine is now capable of caching packages on disk for reuse. This is controlled by `packages_cache` directory path and `enable_packages_cache` boolean parameter (true by default). The packages are cached in an eventlet/inter-process safe manner and are cleaned up as soon as newer version of the package becomes available (unless it's used by ongoing deployment) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/releasenotes/notes/public-template-a8853ac02dcf9396.yaml0000664000175000017500000000037100000000000025143 0ustar00zuulzuul00000000000000--- features: - Added public field to environment templates. GET method for api now displays public templates from other projects(tenants). - Added public filter to environment templates api. - Added clone action to environment templates. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/releasenotes/notes/put-empty-body-d605c2083b239f76.yaml0000664000175000017500000000041400000000000024604 0ustar00zuulzuul00000000000000--- fixes: - It is now possible to make a PUT request with body equal to '[]' to '/environments//services' endpoint. This will result in removing all apps from current session. This allows deleting the last application from environment from CLI. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/releasenotes/notes/reflection-2fc43b990ea6b980.yaml0000664000175000017500000000057600000000000024213 0ustar00zuulzuul00000000000000--- features: - Basic reflection capabilities were added to MuranoPL. Now it is possible to get type info with typeinfo() function and using it as a starting point obtain information about the class, its methods and properties as well as the package of the class. Reflected properties can be used to obtain or set its value in a given object or invoke its method. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/releasenotes/notes/region-aware-shared-ip-4441113c7cdd3c62.yaml0000664000175000017500000000014200000000000026200 0ustar00zuulzuul00000000000000--- fixes: - Fixed 'io.murano.SharedIp' class to properly wotk in muti-region environments. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/releasenotes/notes/release-cinder-volumes-01c29d28031a94dd.yaml0000664000175000017500000000021400000000000026322 0ustar00zuulzuul00000000000000--- fixes: - Previously Cinder Volumes created in MuranoPL were not released correctly on object destruction. The issue is now fixed. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/releasenotes/notes/remove-show-categories-42636e9c24c33105.yaml0000664000175000017500000000026600000000000026221 0ustar00zuulzuul00000000000000--- deprecations: - | Removed `show_categories` endpoint from the application catalog API which has been deprecated since the Liberty cycle. Use `list_categories` instead. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/releasenotes/notes/roles-for-requestcontext-43d32d88c3eaaa95.yaml0000664000175000017500000000032400000000000027126 0ustar00zuulzuul00000000000000--- fixes: - RequestContext now serialises it's roles. This should allow murano to work correctly (and allow rules like "role:xxx" in policy.json) when using oslo.context prior to 2.2.0 and oslo.policy ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/releasenotes/notes/safeloader-cve-2016-4972-19035a2a091ec30a.yaml0000664000175000017500000000062500000000000025614 0ustar00zuulzuul00000000000000--- security: - cve-2016-4972 has been addressed. In ceveral 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. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/releasenotes/notes/script-line-endings-db632db9e24237a3.yaml0000664000175000017500000000034100000000000025713 0ustar00zuulzuul00000000000000--- fixes: - Whenever murano-engine accesses script files, text script files are opened in 'rU' mode which recognizes all types of newlines, and binary files are opened in 'rb' mode to prevent their corruption. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/releasenotes/notes/separate-service-broker-from-murano-f6ee48576f51d893.yaml0000664000175000017500000000024500000000000031001 0ustar00zuulzuul00000000000000--- features: - Separated murano service broker from murano-api into a murano-cfapi service. Created a separate database and ``paste.ini`` for service broker. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/releasenotes/notes/shared-net-port-creation-0eda66be4444cf2f.yaml0000664000175000017500000000070600000000000027023 0ustar00zuulzuul00000000000000--- issues: - If a VM being a part of some shared-ip group is attached to the network which is not owned by the current tenant (shared network) a policy violation may occur thus failing the deployment. fixes: - Murano no longer specifies fixed-ip parameter for ports when creating VMs attached to networks owned and shared by other tenants. Specifying this parameter for non-owned networks could cause violation of neutron policies. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/releasenotes/notes/spec-semver-library-436b0db35fbd4c37.yaml0000664000175000017500000000034300000000000026011 0ustar00zuulzuul00000000000000--- fixes: - It is now possible to use version specifications like '=0.0.0' when ``semantic_version`` library version '2.3.1' is installed. Previously such specifications caused an error and '==0.0.0' had to be used. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/releasenotes/notes/static-actions-61759be796299039.yaml0000664000175000017500000000046200000000000024524 0ustar00zuulzuul00000000000000--- features: - Added a new API endpoint ``v1/actions`` to call static public methods. It accepts class name, method name, method arguments, and optionally package name and class version in the request body. This call does not create an environment, object instances or database records. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/releasenotes/notes/statics-9943fe9873138dac.yaml0000664000175000017500000000066000000000000023463 0ustar00zuulzuul00000000000000--- features: - "Static methods and properties were introduced. Both properties and methods can be marked as Usage: Static Statics can be accessed using ns:Class.property / ns:Class.method(), :Class.property / :Class.method() to access class from current namespace or type('full.name').property / type('full.name').method() to use full type name." - io.murano.configuration.Linux methods are now static ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/releasenotes/notes/string-logging-20b8e60a957ba6b7.yaml0000664000175000017500000000035500000000000025000 0ustar00zuulzuul00000000000000--- fixes: - Murano engine no longer logs methods ``string()``, ``json()``, and ``yaml()`` of the 'io.murano.system.Resources' class. This is done to prevent UnicodeDecodeError's when transferring binary files to murano agent. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/releasenotes/notes/tag-heat-stacks-3345eb1bda531a6f.yaml0000664000175000017500000000025600000000000025073 0ustar00zuulzuul00000000000000--- features: - Heat stacks created by murano during environment deployment now have 'murano' tag by default. This is controlled by ``stack_tags`` config parameter.././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/releasenotes/notes/template-contract-b71840cbc35eb478.yaml0000664000175000017500000000122100000000000025466 0ustar00zuulzuul00000000000000--- features: - Implemented a new contract function ``template()``. ``template()`` works similar to the ``class()`` in regards to the data validation but does not instantiate objects. Instead, the data is left in the object model in dictionary format so that it could be instantiated later with the ``new()`` function. Additionally, the function allows excluding specified properties from validation and from the resulting template so that they could be provided later. Objects that are assigned to the property or argument with ``template()`` contract will be automatically converted to their object model representation. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/releasenotes/notes/test-runner-output-fix-e942e221be189424.yaml0000664000175000017500000000042400000000000026315 0ustar00zuulzuul00000000000000--- fixes: - The test-runner now outputs the tests it runs and their results to stdout directly, instead of the logging system. - The test-runner now does not output logs to stderr by default unless a 'use_stderr' parameter is specified in the configuration file. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/releasenotes/notes/test-runner-set-up-tear-down-a269a31734544a3a.yaml0000664000175000017500000000022700000000000027273 0ustar00zuulzuul00000000000000--- fixes: - Fixed the issue that prevented the test-runner from properly invoking ``setUp`` and ``tearDown`` methods of fixtures in some cases. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/releasenotes/notes/two-phase-instance-deploy-81d37e7987abc792.yaml0000664000175000017500000000040100000000000027005 0ustar00zuulzuul00000000000000--- features: - Split ``Instance``'s ``.deploy()`` method into two phases - ``beginDeploy()`` and ``endDeploy()``. This allows the application developer to provision multiple instances at once without the need to push the stack for each instance. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/releasenotes/notes/update-app-in-env-template-08d92b22bd1355f5.yaml0000664000175000017500000000022500000000000027022 0ustar00zuulzuul00000000000000--- features: - Added API endpoint ``/templates/{env_template_id}/services/{path:.*?}`` for environment template application update operation. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/releasenotes/notes/use_http_proxy_to_wsgi-9b22d3e60c045689.yaml0000664000175000017500000000156600000000000026547 0ustar00zuulzuul00000000000000--- features: - | Murano switched to using standard oslo middleware HTTPProxyToWSGI instead of custom implementation. This middleware parses the X-Forwarded-Proto HTTP header or the Proxy protocol in order to help murano respond with the correct URL refs when it's put behind a TLS proxy (such as HAProxy). This middleware is disabled by default, but can be enabled via a configuration option in the oslo_middleware group. upgrade: - | File ``murano-paste.ini has been updated to use oslo HTTPProxyToWSGI middleware. Config option ``secure_proxy_ssl_header`` has been removed. Please refer to oslo_middleware configuration options if you wish deploy murano behind TLS proxy. Most notably you would need to set ``enable_proxy_headers_parsing`` under group ``oslo_middleware`` to True, to enable header parsing. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/releasenotes/notes/user-project-6173d7282765b5ca.yaml0000664000175000017500000000031400000000000024331 0ustar00zuulzuul00000000000000--- features: - Added classes that represent OpenStack user and project - Added ability to retrieve current user and project info - Added ability to retrieve environment owner user and project info ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/releasenotes/notes/var-kw-args-c42c31678d8bc747.yaml0000664000175000017500000000111500000000000024133 0ustar00zuulzuul00000000000000--- features: - > Added the capability to declare MuranoPL YAML methods with variable length positional and keyword arguments. This is done using argument ``Usage`` attribute. Regular arguments have Standard usage which is the default. Variable length args (args in Python) should have "Usage: VarArgs" and keyword args (kwargs) are declared with "Usage: KwArgs". Inside the method they are seen as a list and a dictionary correspondingly. For such arguments contracts are written for individual argument values thus no need to write them as lists/dicts. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/releasenotes/notes/yaql11-822b503f13992890.yaml0000664000175000017500000000054200000000000022662 0ustar00zuulzuul00000000000000--- features: - Murano was migrated to yaql 1.1 - New format MuranoPL/1.3 can be specified in manifest files. MuranoPL/1.3 is identical to MuranoPL/1.2 but except for the fact that MuranoPL/1.3 packages cannot be imported to earlier Murano versions. Thus applications that use new features of yaql 1.1 should use this format version. ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1696417899.8811812 murano-16.0.0/releasenotes/source/0000775000175000017500000000000000000000000017113 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/releasenotes/source/2023.1.rst0000664000175000017500000000020200000000000020364 0ustar00zuulzuul00000000000000=========================== 2023.1 Series Release Notes =========================== .. release-notes:: :branch: stable/2023.1 ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1696417899.8811812 murano-16.0.0/releasenotes/source/_static/0000775000175000017500000000000000000000000020541 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/releasenotes/source/_static/.placeholder0000664000175000017500000000000000000000000023012 0ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1696417899.8851812 murano-16.0.0/releasenotes/source/_templates/0000775000175000017500000000000000000000000021250 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/releasenotes/source/_templates/.placeholder0000664000175000017500000000000000000000000023521 0ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/releasenotes/source/conf.py0000664000175000017500000002074000000000000020415 0ustar00zuulzuul00000000000000# -*- 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. copyright = u'2015, Murano Developers' # openstackdocstheme options openstackdocs_repo_name = 'openstack/murano' openstackdocs_bug_project = 'murano' openstackdocs_bug_tag = '' # Release notes are version independent. # The short X.Y version. # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. # language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: # today = '' # Else, today_fmt is used as the format for a strftime call. # today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. exclude_patterns = [] # The reST default role (used for this markup: `text`) to use for all # documents. # default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. # add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). # add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. # show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'native' # A list of ignored prefixes for module index sorting. # modindex_common_prefix = [] # If true, keep warnings as "system message" paragraphs in the built documents. # keep_warnings = False # -- Options for HTML output ---------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. html_theme = 'openstackdocs' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. # html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. # html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " 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 = 'MuranoReleaseNotesdoc' # -- Options for LaTeX output --------------------------------------------- # 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', 'MuranoReleaseNotes.tex', u'Murano 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', 'muranoreleasenotes', u'Murano 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', 'MuranoReleaseNotes', u'Murano Release Notes Documentation', u'Murano Developers', 'MuranoReleaseNotes', '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/'] ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/releasenotes/source/index.rst0000664000175000017500000000151600000000000020757 0ustar00zuulzuul00000000000000.. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 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 ====================== .. toctree:: :maxdepth: 2 unreleased 2023.1 zed yoga xena wallaby victoria ussuri train stein rocky queens pike ocata newton mitaka liberty ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/releasenotes/source/liberty.rst0000664000175000017500000000022200000000000021313 0ustar00zuulzuul00000000000000============================== Liberty Series Release Notes ============================== .. release-notes:: :branch: origin/stable/liberty ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1696417899.6811805 murano-16.0.0/releasenotes/source/locale/0000775000175000017500000000000000000000000020352 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1696417899.6811805 murano-16.0.0/releasenotes/source/locale/de/0000775000175000017500000000000000000000000020742 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1696417899.8851812 murano-16.0.0/releasenotes/source/locale/de/LC_MESSAGES/0000775000175000017500000000000000000000000022527 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/releasenotes/source/locale/de/LC_MESSAGES/releasenotes.po0000664000175000017500000000321300000000000025557 0ustar00zuulzuul00000000000000# Andreas Jaeger , 2019. #zanata msgid "" msgstr "" "Project-Id-Version: murano\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2019-09-25 11:20+0000\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "PO-Revision-Date: 2019-09-26 12:52+0000\n" "Last-Translator: Andreas Jaeger \n" "Language-Team: German\n" "Language: de\n" "X-Generator: Zanata 4.3.3\n" "Plural-Forms: nplurals=2; plural=(n != 1)\n" msgid "1.0.2" msgstr "1.0.2" msgid "1.0.3" msgstr "1.0.3" msgid "2.0.0" msgstr "2.0.0" msgid "2.0.1" msgstr "2.0.1" msgid "2.0.2" msgstr "2.0.2" msgid "2.0.2-12" msgstr "2.0.2-12" msgid "3.0.0" msgstr "3.0.0" msgid "3.0.0-15" msgstr "3.0.0-15" msgid "3.1.0" msgstr "3.1.0" msgid "3.2.0" msgstr "3.2.0" msgid "4.0.0" msgstr "4.0.0" msgid "5.0.0" msgstr "5.0.0" msgid "7.0.0" msgstr "7.0.0" msgid "Current Series Release Notes" msgstr "Aktuelle Serie Releasenotes" msgid "Liberty Series Release Notes" msgstr "Liberty Serie Releasenotes" msgid "Mitaka Series Release Notes" msgstr "Mitaka Serie Releasenotes" msgid "Murano Release Notes" msgstr "Murano Releasenotes" msgid "Newton Series Release Notes" msgstr "Newton Serie Releasenotes" msgid "Ocata Series Release Notes" msgstr "Ocata Serie Releasenotes" msgid "Pike Series Release Notes" msgstr "Pike Serie Releasenotes" msgid "Queens Series Release Notes" msgstr "Queens Serie Releasenotes" msgid "Rocky Series Release Notes" msgstr "Rocky Serie Releasenotes" msgid "Stein Series Release Notes" msgstr "Stein Serie Releasenotes" msgid "Train Series Release Notes" msgstr "Train Serie Releasenotes" ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1696417899.6811805 murano-16.0.0/releasenotes/source/locale/en_GB/0000775000175000017500000000000000000000000021324 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1696417899.8851812 murano-16.0.0/releasenotes/source/locale/en_GB/LC_MESSAGES/0000775000175000017500000000000000000000000023111 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/releasenotes/source/locale/en_GB/LC_MESSAGES/releasenotes.po0000664000175000017500000016516700000000000026162 0ustar00zuulzuul00000000000000# Andi Chandler , 2017. #zanata # Andi Chandler , 2018. #zanata # Andi Chandler , 2019. #zanata # Andi Chandler , 2020. #zanata # Andi Chandler , 2022. #zanata # Andi Chandler , 2023. #zanata msgid "" msgstr "" "Project-Id-Version: murano\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2023-04-26 08:25+0000\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "PO-Revision-Date: 2023-05-08 11:46+0000\n" "Last-Translator: Andi Chandler \n" "Language-Team: English (United Kingdom)\n" "Language: en_GB\n" "X-Generator: Zanata 4.3.3\n" "Plural-Forms: nplurals=2; plural=(n != 1)\n" msgid "" "\"List Environments\" API call is now able to filter environments by an " "owner project (tenant)." msgstr "" "\"List Environments\" API call is now able to filter environments by an " "owner project (tenant)." msgid "" "/environments/ENV_ID/model/PATH endpoint added. GET request responds with " "the subsection of ENV_ID's object model located in its PATH. PATCH request " "applies json-patch from request body to ENV_ID's model. It does not contain " "PATH in the URL." msgstr "" "/environments/ENV_ID/model/PATH endpoint added. GET request responds with " "the subsection of ENV_ID's object model located in its PATH. PATCH request " "applies json-patch from request body to ENV_ID's model. It does not contain " "PATH in the URL." msgid "1.0.2" msgstr "1.0.2" msgid "1.0.3" msgstr "1.0.3" msgid "11.0.0" msgstr "11.0.0" msgid "15.0.0" msgstr "15.0.0" msgid "2.0.0" msgstr "2.0.0" msgid "2.0.1" msgstr "2.0.1" msgid "2.0.2" msgstr "2.0.2" msgid "2.0.2-12" msgstr "2.0.2-12" msgid "2023.1 Series Release Notes" msgstr "2023.1 Series Release Notes" msgid "3.0.0" msgstr "3.0.0" msgid "3.0.0-15" msgstr "3.0.0-15" msgid "3.1.0" msgstr "3.1.0" msgid "3.2.0" msgstr "3.2.0" msgid "4.0.0" msgstr "4.0.0" msgid "5.0.0" msgstr "5.0.0" msgid "7.0.0" msgstr "7.0.0" msgid "9.0.0" msgstr "9.0.0" msgid "" "A configuration file setting `[engine]/agent_source` was added. The value is " "then used for the `pip install` command to install the murano-agent. Since " "pip accepts http and git URLs, this can be used to install agent from the " "custom git repo or install version other than the latest." msgstr "" "A configuration file setting `[engine]/agent_source` was added. The value is " "then used for the `pip install` command to install the murano-agent. Since " "pip accepts HTTP and git URLs, this can be used to install agent from the " "custom git repo or install version other than the latest." msgid "" "API call for deleting a service from environment template did not return " "result of its operation. The issue is fixed." msgstr "" "API call for deleting a service from environment template did not return " "result of its operation. The issue is fixed." msgid "Add multiple engine workers" msgstr "Add multiple engine workers" msgid "" "Add notifications about environment events that are required for tracking. " "These are AMQP notifications and oslo.messaging library is used for sending " "them. The follow event types are provided: environment.deploy.end, " "environment.delete.end, environment.exists There are 2 new configuration " "options controlling these notifications: stats.env_audit_period, " "env_audit_enabled." msgstr "" "Add notifications about environment events that are required for tracking. " "These are AMQP notifications and oslo.messaging library is used for sending " "them. The follow event types are provided: environment.deploy.end, " "environment.delete.end, environment.exists There are 2 new configuration " "options controlling these notifications: stats.env_audit_period, " "env_audit_enabled." msgid "" "Added :command:`murano-test-tunner` command to run murano package tests." msgstr "" "Added :command:`murano-test-tunner` command to run Murano package tests." msgid "" "Added API endpoint ``/templates/{env_template_id}/services/{path:.*?}`` for " "environment template application update operation." msgstr "" "Added API endpoint ``/templates/{env_template_id}/services/{path:.*?}`` for " "environment template application update operation." msgid "" "Added Support for application deployment across OpenStack regions. Now, all " "OpenStack resource classes inherit from ``io.murano.CloudResource`` that " "provides ``.getRegion()`` method and ``regionName`` property. This allows to " "assign resources to different regions. ``.getRegion()`` returns ``io.murano." "CloudRegion`` instance that resource or its parent belongs to. " "``CloudRegion`` has interface similar to ``Environment`` class and is the " "correct way to get ``HeatStack`` instance associated with the region, " "default network configuration, security group manager and agent listener " "instances. ``Environment`` now acts as default region so backward " "compatibility is not broken. However new applications should not use " "environment to set security group rules but rather a region(s) of their " "instance(s) in order to work correctly when their instances were configured " "to use region other than the default." msgstr "" "Added Support for application deployment across OpenStack regions. Now, all " "OpenStack resource classes inherit from ``io.murano.CloudResource`` that " "provides ``.getRegion()`` method and ``regionName`` property. This allows to " "assign resources to different regions. ``.getRegion()`` returns ``io.murano." "CloudRegion`` instance that resource or its parent belongs to. " "``CloudRegion`` has interface similar to ``Environment`` class and is the " "correct way to get ``HeatStack`` instance associated with the region, " "default network configuration, security group manager and agent listener " "instances. ``Environment`` now acts as default region so backward " "compatibility is not broken. However new applications should not use " "environment to set security group rules but rather a region(s) of their " "instance(s) in order to work correctly when their instances were configured " "to use region other than the default." msgid "" "Added a MetadataAware mixin class capable to retrieve the metadata " "attributes from the implementing objects and all its parents." msgstr "" "Added a MetadataAware mixin class capable to retrieve the metadata " "attributes from the implementing objects and all its parents." msgid "" "Added a ``metadata()`` yaql function to retrieve the meta information about " "the object, stored in the \"?/metadata\" section of object model." msgstr "" "Added a ``metadata()`` yaql function to retrieve the meta information about " "the object, stored in the \"?/metadata\" section of object model." msgid "" "Added a new API endpoint ``v1/actions`` to call static public methods. It " "accepts class name, method name, method arguments, and optionally package " "name and class version in the request body. This call does not create an " "environment, object instances or database records." msgstr "" "Added a new API endpoint ``v1/actions`` to call static public methods. It " "accepts class name, method name, method arguments, and optionally package " "name and class version in the request body. This call does not create an " "environment, object instances or database records." msgid "" "Added a new engine RPC call to generate json-schema from MuranoPL class. The " "schema may be generated either from the entire class or for specific model " "builders - static actions that can be used to generate object model from " "their input. Class schema is built by inspecting class properties and method " "schema using the same algorithm but applied to its arguments." msgstr "" "Added a new engine RPC call to generate json-schema from MuranoPL class. The " "schema may be generated either from the entire class or for specific model " "builders - static actions that can be used to generate object model from " "their input. Class schema is built by inspecting class properties and method " "schema using the same algorithm but applied to its arguments." msgid "" "Added a new manifest format 1.4.0. Introduced the 'Scope' keyword for class " "methods to declare a method's accessibility from outside through the API " "call." msgstr "" "Added a new manifest format 1.4.0. Introduced the 'Scope' keyword for class " "methods to declare a method's accessibility from outside through the API " "call." msgid "" "Added a new yaql function 'decryptData' which pairs with 'encryptData' on " "the dashboard side. Application authors can use these functions to secure " "sensitive input to their Murano applications such as passwords." msgstr "" "Added a new YAQL function 'decryptData' which pairs with 'encryptData' on " "the dashboard side. Application authors can use these functions to secure " "sensitive input to their Murano applications such as passwords." msgid "Added ability to extend MuranoPL entities with custom metadata." msgstr "Added ability to extend MuranoPL entities with custom metadata." msgid "Added ability to retrieve current user and project info" msgstr "Added ability to retrieve current user and project info" msgid "Added ability to retrieve environment owner user and project info" msgstr "Added ability to retrieve environment owner user and project info" msgid "" "Added an overload of the new function - ``new($model, $owner)``. It loads " "complete object graph in a single call. Objects in the model can have cross " "references. In that case, this is the only way to instantiate the graph. " "Objects might be specified either in object model format (with '?' attribute " "or in MuranoPL format (used for Meta definitions)." msgstr "" "Added an overload of the new function - ``new($model, $owner)``. It loads " "complete object graph in a single call. Objects in the model can have cross " "references. In that case, this is the only way to instantiate the graph. " "Objects might be specified either in object model format (with '?' attribute " "or in MuranoPL format (used for Meta definitions)." msgid "Added classes that represent OpenStack user and project" msgstr "Added classes that represent OpenStack user and project" msgid "Added clone action to environment templates." msgstr "Added clone action to environment templates." msgid "Added filter by 'Name' which only matches package name." msgstr "Added filter by 'Name' which only matches package name." msgid "" "Added magnum plugin to murano, that allows creating/deleting of magnum " "baymodels and bays from MuranoPL" msgstr "" "Added Magnum plugin to Murano, that allows creating/deleting of Magnum " "baymodels and bays from MuranoPL" msgid "Added new tool ``murano-status upgrade check``." msgstr "Added new tool ``murano-status upgrade check``." msgid "" "Added public field to environment templates. GET method for api now displays " "public templates from other projects(tenants)." msgstr "" "Added public field to environment templates. GET method for API now displays " "public templates from other projects(tenants)." msgid "Added public filter to environment templates api." msgstr "Added public filter to environment templates API." msgid "" "Added the ``api_workers`` option to ``murano`` config group. It controls the " "number of API workers launched by murano. If not set, it would default to " "the number of CPUs available." msgstr "" "Added the ``api_workers`` option to ``murano`` config group. It controls the " "number of API workers launched by Murano. If not set, it would default to " "the number of CPUs available." msgid "" "Added the ``description_text`` field to environment and environment " "templates database tables and respective API objects." msgstr "" "Added the ``description_text`` field to environment and environment " "templates database tables and respective API objects." msgid "" "Added the ``driver`` configuration option to the ``networking`` group. It " "allows to explicitly select the networking driver. It supports 'neutron' and " "'nova' options. If set to ``None`` (default), murano attempts to use " "'neutron' if available, 'nova' otherwise. The change is backward compatible." msgstr "" "Added the ``driver`` configuration option to the ``networking`` group. It " "allows to explicitly select the networking driver. It supports 'neutron' and " "'nova' options. If set to ``None`` (default), Murano attempts to use " "'neutron' if available, 'nova' otherwise. The change is backward compatible." msgid "" "Added the ``timeout`` parameter to ``runCommand`` and ``putFile`` methods of " "the ``io.murano.configuration.Linux`` class." msgstr "" "Added the ``timeout`` parameter to ``runCommand`` and ``putFile`` methods of " "the ``io.murano.configuration.Linux`` class." msgid "" "Added the capability to declare MuranoPL YAML methods with variable length " "positional and keyword arguments. This is done using argument ``Usage`` " "attribute. Regular arguments have Standard usage which is the default. " "Variable length args (args in Python) should have \"Usage: VarArgs\" and " "keyword args (kwargs) are declared with \"Usage: KwArgs\". Inside the method " "they are seen as a list and a dictionary correspondingly. For such arguments " "contracts are written for individual argument values thus no need to write " "them as lists/dicts." msgstr "" "Added the capability to declare MuranoPL YAML methods with variable length " "positional and keyword arguments. This is done using argument ``Usage`` " "attribute. Regular arguments have Standard usage which is the default. " "Variable length args (args in Python) should have \"Usage: VarArgs\" and " "keyword args (kwargs) are declared with \"Usage: KwArgs\". Inside the method " "they are seen as a list and a dictionary correspondingly. For such arguments " "contracts are written for individual argument values thus no need to write " "them as lists/dicts." msgid "" "Added the following meta-classes to the core library - ``Title`` " "``Description`` ``HelpText`` ``Hidden`` ``Section`` ``Position`` " "``ModelBuilder``. These classes will later be used to implement dynamic " "object model generation." msgstr "" "Added the following meta-classes to the core library - ``Title`` " "``Description`` ``HelpText`` ``Hidden`` ``Section`` ``Position`` " "``ModelBuilder``. These classes will later be used to implement dynamic " "object model generation." msgid "" "All HOT template outputs are put into a single dictionary property " "'templateOutputs' rather than in a generated property per each output. As a " "result there are no more constraints on output names." msgstr "" "All HOT template outputs are put into a single dictionary property " "'templateOutputs' rather than in a generated property per each output. As a " "result there are no more constraints on output names." msgid "" "Avoid race condition during parallel upload of packages, when packages have " "same tags." msgstr "" "Avoid race condition during parallel upload of packages, when packages have " "same tags." msgid "" "Avoid the `'method' object has no attribute '__yaql_function__'` error when " "calling some YAQL functions with Python 3" msgstr "" "Avoid the `'method' object has no attribute '__yaql_function__'` error when " "calling some YAQL functions with Python 3" msgid "" "Basic reflection capabilities were added to MuranoPL. Now it is possible to " "get type info with typeinfo() function and using it as a starting point " "obtain information about the class, its methods and properties as well as " "the package of the class. Reflected properties can be used to obtain or set " "its value in a given object or invoke its method." msgstr "" "Basic reflection capabilities were added to MuranoPL. Now it is possible to " "get type info with typeinfo() function and using it as a starting point " "obtain information about the class, its methods and properties as well as " "the package of the class. Reflected properties can be used to obtain or set " "its value in a given object or invoke its method." msgid "Bug Fixes" msgstr "Bug Fixes" msgid "Bumped the RUNTIME_VERSION attribute to 1.5" msgstr "Bumped the RUNTIME_VERSION attribute to 1.5" msgid "" "Changed the type representation in object model. Previous format was to have " "three attributes in \"?\" section of the object - type, classVersion and " "package where only the \"type\" is mandatory. Now they are merged into " "single attribute \"type\" that has a format ``typeName/version@package``. " "Version and package parts are still optional." msgstr "" "Changed the type representation in object model. Previous format was to have " "three attributes in \"?\" section of the object - type, classVersion and " "package where only the \"type\" is mandatory. Now they are merged into " "single attribute \"type\" that has a format ``typeName/version@package``. " "Version and package parts are still optional." msgid "" "Class configs are now also versioned. For class foo.bar version 1.2.3 the " "following file names will be examined: foo.bar-1.2.3.json foo.bar-1.2.3.yaml " "foo.bar-1.2.json foo.bar-1.2.yaml foo.bar-1.json foo.bar-1.yaml In addition " "for classes of version 0.x.y file name without version suffix are also " "examined as a last attempt so the backward compatibility is retained" msgstr "" "Class configs are now also versioned. For class foo.bar version 1.2.3 the " "following file names will be examined: foo.bar-1.2.3.json foo.bar-1.2.3.yaml " "foo.bar-1.2.json foo.bar-1.2.yaml foo.bar-1.json foo.bar-1.yaml In addition " "for classes of version 0.x.y file name without version suffix are also " "examined as a last attempt so the backward compatibility is retained" msgid "" "Classes to work with Cinder volumes were added to core library. Now it is " "possible to create new volume from various sources or use existing volume. " "Also it is possible to attach volumes to instances and boot instances from " "volumes." msgstr "" "Classes to work with Cinder volumes were added to core library. Now it is " "possible to create new volume from various sources or use existing volume. " "Also it is possible to attach volumes to instances and boot instances from " "volumes." msgid "" "Core Library's init scripts used to have various problems detecting pre-" "installed (by DIB) murano-agent on non-ubuntu images. Agent setup script now " "checks wider list of directories before attempting to install murano-agnet " "and service script now does not impose strict script location." msgstr "" "Core Library's init scripts used to have various problems detecting pre-" "installed (by DIB) murano-agent on non-ubuntu images. Agent setup script now " "checks wider list of directories before attempting to install murano-agnet " "and service script now does not impose strict script location." msgid "Current Series Release Notes" msgstr "Current Series Release Notes" msgid "" "Deprecated the 'Usage Action' keyword. For format versions >= 1.4.0, use " "'Scope Public' instead." msgstr "" "Deprecated the 'Usage Action' keyword. For format versions >= 1.4.0, use " "'Scope Public' instead." msgid "Deprecation Notes" msgstr "Deprecation Notes" msgid "" "Due to refactoring, contracts work a little bit faster because there is no " "more need to generate yaql function definition for each contract method on " "each call." msgstr "" "Due to refactoring, contracts work a little bit faster because there is no " "more need to generate a YAQL function definition for each contract method on " "each call." msgid "Enable mocks in MuranoPL tests cases." msgstr "Enable mocks in MuranoPL tests cases." msgid "" "Enabling multiple workers might break workflows under BSD and Windows systems" msgstr "" "Enabling multiple workers might break workflows under BSD and Windows systems" msgid "" "Equality check (assertEqual) in test-runner can now properly compare two " "MuranoPl objects." msgstr "" "Equality check (assertEqual) in test-runner can now properly compare two " "MuranoPl objects." msgid "" "File ``murano-paste.ini has been updated to use oslo HTTPProxyToWSGI " "middleware. Config option ``secure_proxy_ssl_header`` has been removed. " "Please refer to oslo_middleware configuration options if you wish deploy " "murano behind TLS proxy. Most notably you would need to set " "``enable_proxy_headers_parsing`` under group ``oslo_middleware`` to True, to " "enable header parsing." msgstr "" "File ``murano-paste.ini has been updated to use oslo HTTPProxyToWSGI " "middleware. Config option ``secure_proxy_ssl_header`` has been removed. " "Please refer to oslo_middleware configuration options if you wish deploy " "murano behind TLS proxy. Most notably you would need to set " "``enable_proxy_headers_parsing`` under group ``oslo_middleware`` to True, to " "enable header parsing." msgid "" "Fixed 'io.murano.SharedIp' class to properly wotk in muti-region " "environments." msgstr "" "Fixed 'io.murano.SharedIp' class to properly wotk in muti-region " "environments." msgid "" "Fixed a bug when the UI dialog was not displayed in Murano Dashboard for " "applications which don't have UI definitions bundled in the package but " "generate them based on the package contents instead. This usually affected " "HOT-based packages and other non-muranopl-based applications." msgstr "" "Fixed a bug when the UI dialogue was not displayed in Murano Dashboard for " "applications which don't have UI definitions bundled in the package but " "generate them based on the package contents instead. This usually affected " "HOT-based packages and other non-muranopl-based applications." msgid "" "Fixed incorrect murano behaviour if deployed on devstack with keystone v3 by " "default." msgstr "" "Fixed incorrect Murano behaviour if deployed on devstack with keystone v3 by " "default." msgid "" "Fixed the issue that prevented the test-runner from properly invoking " "``setUp`` and ``tearDown`` methods of fixtures in some cases." msgstr "" "Fixed the issue that prevented the test-runner from properly invoking " "``setUp`` and ``tearDown`` methods of fixtures in some cases." msgid "" "For MuranoPL classes new key \"Usage\" was added. By default it is \"Class" "\". But it can also be \"Meta\" to define meta-class. Meta-class has all the " "capabilities of regular classes and in addition has 3 new attributes: " "Cardinality, Applies and Inherited." msgstr "" "For MuranoPL classes new key \"Usage\" was added. By default it is \"Class" "\". But it can also be \"Meta\" to define meta-class. Meta-class has all the " "capabilities of regular classes and in addition has 3 new attributes: " "Cardinality, Applies and Inherited." msgid "" "Heat stacks created by murano during environment deployment now have " "'murano' tag by default. This is controlled by ``stack_tags`` config " "parameter." msgstr "" "Heat stacks created by Murano during environment deployment now have " "'murano' tag by default. This is controlled by ``stack_tags`` config " "parameter." msgid "" "If a VM being a part of some shared-ip group is attached to the network " "which is not owned by the current tenant (shared network) a policy violation " "may occur thus failing the deployment." msgstr "" "If a VM being a part of some shared-ip group is attached to the network " "which is not owned by the current tenant (shared network) a policy violation " "may occur thus failing the deployment." msgid "" "Implemented a new contract function ``template()``. ``template()`` works " "similar to the ``class()`` in regards to the data validation but does not " "instantiate objects. Instead, the data is left in the object model in " "dictionary format so that it could be instantiated later with the ``new()`` " "function. Additionally, the function allows excluding specified properties " "from validation and from the resulting template so that they could be " "provided later. Objects that are assigned to the property or argument with " "``template()`` contract will be automatically converted to their object " "model representation." msgstr "" "Implemented a new contract function ``template()``. ``template()`` works " "similar to the ``class()`` in regards to the data validation but does not " "instantiate objects. Instead, the data is left in the object model in " "dictionary format so that it could be instantiated later with the ``new()`` " "function. Additionally, the function allows excluding specified properties " "from validation and from the resulting template so that they could be " "provided later. Objects that are assigned to the property or argument with " "``template()`` contract will be automatically converted to their object " "model representation." msgid "" "Implemented a new framework for MuranoPL contracts. Now, instead of several " "independent implementations of the same yaql methods (string(), class() " "etc.) all implementations of the same method are combined into single class. " "Therefore, we now have a class per contract method. This also simplifies " "development of new contracts. Each such class can provide methods for data " "transformation (default contract usage), validation that is used to decide " "if the method can be considered an extension method for the value, and json " "schema generation method that was moved from the schema generator script." msgstr "" "Implemented a new framework for MuranoPL contracts. Now, instead of several " "independent implementations of the same YAQL methods (string(), class() " "etc.) all implementations of the same method are combined into single class. " "Therefore, we now have a class per contract method. This also simplifies " "development of new contracts. Each such class can provide methods for data " "transformation (default contract usage), validation that is used to decide " "if the method can be considered an extension method for the value, and JSON " "schema generation method that was moved from the schema generator script." msgid "" "Implemented the capability for API endpoint ``/catalog/packages`` to filter " "'id', 'category', 'tag' properties using the 'in' operator. An example of " "using the 'in' operator for 'id' is 'id=in:id1,id2,id3'. This filter is " "added using syntax that conforms to the latest guidelines from the OpenStack " "API-WG." msgstr "" "Implemented the capability for API endpoint ``/catalog/packages`` to filter " "'id', 'category', 'tag' properties using the 'in' operator. An example of " "using the 'in' operator for 'id' is 'id=in:id1,id2,id3'. This filter is " "added using syntax that conforms to the latest guidelines from the OpenStack " "API-WG." msgid "" "Implemented the capability for the helper methods of Linux class to run " "concurrently if executed for different VM agents." msgstr "" "Implemented the capability for the helper methods of Linux class to run " "concurrently if executed for different VM agents." msgid "" "Internally, both pre-deployment garbage collection (that was done by " "comparision of ``Objects`` and ``ObjectsCopy``) and post-deployment orphan " "object collection are now done through the new GC." msgstr "" "Internally, both pre-deployment garbage collection (that was done by " "comparison of ``Objects`` and ``ObjectsCopy``) and post-deployment orphan " "object collection are now done through the new GC." msgid "" "Introduced a new MuranoPL class ``io.murano.system.GC`` Now MuranoPL garbage " "collector can be used to set up destruction dependencies between murano " "objects. If object Foo is subscribed to object Bar's destruction, it will be " "notified through a specific handler. If both Foo and Bar are going to be " "destroyed during one execution session, Foo will be destroyed after Bar. You " "can omit the handler, in this case destruction order will also be preserved. " "Handler can be a static or a usual function." msgstr "" "Introduced a new MuranoPL class ``io.murano.system.GC`` Now MuranoPL garbage " "collector can be used to set up destruction dependencies between Murano " "objects. If object Foo is subscribed to object Bar's destruction, it will be " "notified through a specific handler. If both Foo and Bar are going to be " "destroyed during one execution session, Foo will be destroyed after Bar. You " "can omit the handler, in this case destruction order will also be preserved. " "Handler can be a static or a usual function." msgid "" "Introduced two YAQL *inject* functions to enable mocks ``def inject(target, " "target_method, mock_object, mock_name)`` and ``def inject(target, " "target_method, yaql_expr)``." msgstr "" "Introduced two YAQL *inject* functions to enable mocks ``def inject(target, " "target_method, mock_object, mock_name)`` and ``def inject(target, " "target_method, yaql_expr)``." msgid "" "It is now possible to configure the notifications to use a different " "transport URL than the RPCs. These could potentially be completely different " "message broker hosts (though they doesn't need to be). If the notification-" "specific configuration is not provided, the notifier will use the same " "transport as the RPCs." msgstr "" "It is now possible to configure the notifications to use a different " "transport URL than the RPCs. These could potentially be completely different " "message broker hosts (though they doesn't need to be). If the notification-" "specific configuration is not provided, the notifier will use the same " "transport as the RPCs." msgid "" "It is now possible to make a GET request to '/deployments' endpoint. This " "will result in deployments for all environments in a specific project " "(tenant) being returned." msgstr "" "It is now possible to make a GET request to '/deployments' endpoint. This " "will result in deployments for all environments in a specific project " "(tenant) being returned." msgid "" "It is now possible to make a PUT request with body equal to '[]' to '/" "environments//services' endpoint. This will result in removing all " "apps from current session. This allows deleting the last application from " "environment from CLI." msgstr "" "It is now possible to make a PUT request with body equal to '[]' to '/" "environments//services' endpoint. This will result in removing all " "apps from current session. This allows deleting the last application from " "environment from CLI." msgid "" "It is now possible to use version specifications like '=0.0.0' when " "``semantic_version`` library version '2.3.1' is installed. Previously such " "specifications caused an error and '==0.0.0' had to be used." msgstr "" "It is now possible to use version specifications like '=0.0.0' when " "``semantic_version`` library version '2.3.1' is installed. Previously such " "specifications caused an error and '==0.0.0' had to be used." msgid "" "It is possible to attach meta-class instances to packages, classes " "(including other meta-classes), properties, methods and method arguments. " "Each of them got new \"Meta\" key containing list (or single scalar) of meta-" "class instances." msgstr "" "It is possible to attach meta-class instances to packages, classes " "(including other meta-classes), properties, methods and method arguments. " "Each of them got new \"Meta\" key containing list (or single scalar) of meta-" "class instances." msgid "" "It was impossible to explicitly provide attribute owner class to getAttr/" "setAttr methods without using namespace prefix or if the type was not from " "the core library." msgstr "" "It was impossible to explicitly provide attribute owner class to getAttr/" "setAttr methods without using namespace prefix or if the type was not from " "the core library." msgid "Known Issues" msgstr "Known Issues" msgid "Liberty Series Release Notes" msgstr "Liberty Series Release Notes" msgid "Mitaka Series Release Notes" msgstr "Mitaka Series Release Notes" msgid "Murano Release Notes" msgstr "Murano Release Notes" msgid "" "Murano engine can be configured to sign all the RabbitMQ messages sent to " "the agents. When the RSA key is provided, engine will provide agents with " "its public part and sign all the messages sent. Agents then will ignore any " "command that was not sent by the engine." msgstr "" "Murano engine can be configured to sign all the RabbitMQ messages sent to " "the agents. When the RSA key is provided, engine will provide agents with " "its public part and sign all the messages sent. Agents then will ignore any " "command that was not sent by the engine." msgid "" "Murano engine is now capable of caching packages on disk for reuse. This is " "controlled by `packages_cache` directory path and `enable_packages_cache` " "boolean parameter (true by default). The packages are cached in a eventlet/" "inter-process safe manner and are cleaned up as soon as newer version of the " "package becomes available (unless it's used by ongoing deployment)" msgstr "" "Murano engine is now capable of caching packages on disk for reuse. This is " "controlled by `packages_cache` directory path and `enable_packages_cache` " "boolean parameter (true by default). The packages are cached in a eventlet/" "inter-process safe manner and are cleaned up as soon as newer version of the " "package becomes available (unless it's used by ongoing deployment)" msgid "" "Murano engine no longer logs methods ``string()``, ``json()``, and " "``yaml()`` of the 'io.murano.system.Resources' class. This is done to " "prevent UnicodeDecodeError's when transferring binary files to murano agent." msgstr "" "Murano engine no longer logs methods ``string()``, ``json()``, and " "``yaml()`` of the 'io.murano.system.Resources' class. This is done to " "prevent UnicodeDecodeError's when transferring binary files to Murano agent." msgid "" "Murano is now able to assign correct floating IPs when using multiple " "external networks. It attempts to choose one that shares a router with " "internal network." msgstr "" "Murano is now able to assign correct Floating IPs when using multiple " "external networks. It attempts to choose one that shares a router with " "internal network." msgid "" "Murano is now able to deploy applications in the environments with disabled " "Neutron Security Groups. Detection is based on the presence of 'security-" "group' Neutron extension." msgstr "" "Murano is now able to deploy applications in the environments with disabled " "Neutron Security Groups. Detection is based on the presence of 'security-" "group' Neutron extension." msgid "" "Murano is now able to work with keystone configured to use a templated " "catalog." msgstr "" "Murano is now able to work with Keystone configured to use a templated " "catalogue." msgid "" "Murano no longer specifies fixed-ip parameter for ports when creating VMs " "attached to networks owned and shared by other tenants. Specifying this " "parameter for non-owned networks could cause violation of neutron policies." msgstr "" "Murano no longer specifies fixed-ip parameter for ports when creating VMs " "attached to networks owned and shared by other tenants. Specifying this " "parameter for non-owned networks could cause violation of Neutron policies." msgid "" "Murano switched to using standard oslo middleware HTTPProxyToWSGI instead of " "custom implementation. This middleware parses the X-Forwarded-Proto HTTP " "header or the Proxy protocol in order to help murano respond with the " "correct URL refs when it's put behind a TLS proxy (such as HAProxy). This " "middleware is disabled by default, but can be enabled via a configuration " "option in the oslo_middleware group." msgstr "" "Murano switched to using standard Oslo middleware HTTPProxyToWSGI instead of " "custom implementation. This middleware parses the X-Forwarded-Proto HTTP " "header or the Proxy protocol in order to help Murano respond with the " "correct URL refs when it's put behind a TLS proxy (such as HAProxy). This " "middleware is disabled by default, but can be enabled via a configuration " "option in the oslo_middleware group." msgid "Murano was migrated to yaql 1.1" msgstr "Murano was migrated to YAQL 1.1" msgid "New Features" msgstr "New Features" msgid "New database migration 015 has to be applied." msgstr "New database migration 015 has to be applied." msgid "" "New format MuranoPL/1.3 can be specified in manifest files. MuranoPL/1.3 is " "identical to MuranoPL/1.2 but except for the fact that MuranoPL/1.3 packages " "cannot be imported to earlier Murano versions. Thus applications that use " "new features of yaql 1.1 should use this format version." msgstr "" "New format MuranoPL/1.3 can be specified in manifest files. MuranoPL/1.3 is " "identical to MuranoPL/1.2 but except for the fact that MuranoPL/1.3 packages " "cannot be imported to earlier Murano versions. Thus applications that use " "new features of YAQL 1.1 should use this format version." msgid "" "New framework for ``murano-status upgrade check`` command is added. This " "framework allows adding various checks which can be run before a Murano " "upgrade to ensure if the upgrade can be performed safely." msgstr "" "New framework for ``murano-status upgrade check`` command is added. This " "framework allows adding various checks which can be run before a Murano " "upgrade to ensure if the upgrade can be performed safely." msgid "" "New method type: extension methods. Extension methods enable you to \"add\" " "methods to existing types without modifying the original type. Extension " "methods are a special kind of static method, but they are called as if they " "were instance methods on the extended type. Extension methods are identified " "by \"Usage: Extension\" and the type they extend is determined by their " "first argument contract. Thus such methods must have at lease one parameter." msgstr "" "New method type: extension methods. Extension methods enable you to \"add\" " "methods to existing types without modifying the original type. Extension " "methods are a special kind of static method, but they are called as if they " "were instance methods on the extended type. Extension methods are identified " "by \"Usage: Extension\" and the type they extend is determined by their " "first argument contract. Thus such methods must have at lease one parameter." msgid "" "New on-request garbage collector for MuranoPL objects were implemented. " "Garbage collection is triggered by io.murano.system.GC.collect() static " "method. Garbage collector destroys all object that are not reachable " "anymore. GC can handle objects with cross-references and isolated object " "graphs. When portion of object model becomes not reachable it destroyed in " "predictable order such that child objects get destroyed before their parents " "and, when possible, before objects that are subscribed to their destruction " "notifications." msgstr "" "New on-request garbage collector for MuranoPL objects were implemented. " "Garbage collection is triggered by io.murano.system.GC.collect() static " "method. Garbage collector destroys all object that are not reachable any " "more. GC can handle objects with cross-references and isolated object " "graphs. When portion of object model becomes not reachable it destroyed in " "predictable order such that child objects get destroyed before their parents " "and, when possible, before objects that are subscribed to their destruction " "notifications." msgid "" "New operator *is* was added to MuranoPL. Now it is possible to test if " "MuranoPL object is of particular type." msgstr "" "New operator *is* was added to MuranoPL. Now it is possible to test if " "MuranoPL object is of particular type." msgid "" "New plugin 'murano_heat-translator_plugin' was added. Now it is possible to " "deploy applications from CSAR templates." msgstr "" "New plugin 'murano_heat-translator_plugin' was added. Now it is possible to " "deploy applications from CSAR templates." msgid "" "New type-level keyword \"Import\" which can be either list or scalar that " "specifies type names which extensions methods should be imported into class " "context and thus become available to type members." msgstr "" "New type-level keyword \"Import\" which can be either list or scalar that " "specifies type names which extensions methods should be imported into class " "context and thus become available to type members." msgid "Newton Series Release Notes" msgstr "Newton Series Release Notes" msgid "Now admin can delete user's environments." msgstr "Now admin can delete user's environments." msgid "" "Now all native MuranoPL methods (those that are written in Python) have \"?" "muranoMethod\" metadata key referring to MuranoMethod instance for the " "method." msgstr "" "Now all native MuranoPL methods (those that are written in Python) have \"?" "muranoMethod\" metadata key referring to MuranoMethod instance for the " "method." msgid "" "Now it is possible to have several classes in one YAML file. Classes are " "separated using YAML document separator (3 dashes). Empty documents are " "skipped. If the class doesn't have Namespace section corresponding section " "from the previous class in the same file is used. Thus it is possible to " "declare namespace prefixes once at the file header. Even if there are " "several classes in one file all of them are still required to be declared in " "manifest file." msgstr "" "Now it is possible to have several classes in one YAML file. Classes are " "separated using YAML document separator (3 dashes). Empty documents are " "skipped. If the class doesn't have Namespace section corresponding section " "from the previous class in the same file is used. Thus it is possible to " "declare namespace prefixes once at the file header. Even if there are " "several classes in one file all of them are still required to be declared in " "manifest file." msgid "Ocata Series Release Notes" msgstr "Ocata Series Release Notes" msgid "" "Operator can now use new CLI tool ``murano-status upgrade check`` to check " "if Murano deployment can be safely upgraded from N-1 to N release." msgstr "" "Operator can now use new CLI tool ``murano-status upgrade check`` to check " "if Murano deployment can be safely upgraded from N-1 to N release." msgid "Other Notes" msgstr "Other Notes" msgid "Pike Series Release Notes" msgstr "Pike Series Release Notes" msgid "Prelude" msgstr "Prelude" msgid "" "Prevented the resource leak for objects created during deployment with " "``new()`` function call." msgstr "" "Prevented the resource leak for objects created during deployment with " "``new()`` function call." msgid "" "Previously Cinder Volumes created in MuranoPL were not released correctly on " "object destruction. The issue is now fixed." msgstr "" "Previously Cinder Volumes created in MuranoPL were not released correctly on " "object destruction. The issue is now fixed." msgid "" "Previously murano assumed that the service user and service project are in " "the 'Default' domain. These values can now be set in ``keystone_authtoken`` " "config group." msgstr "" "Previously Murano assumed that the service user and service project are in " "the 'Default' domain. These values can now be set in ``keystone_authtoken`` " "config group." msgid "" "Previously, when a class overrode a property from its parent class the value " "was stored separately for both of them, transformed by each of the " "contracts. Thus each class saw the value of its contract. In absolute " "majority of the cases, the observed value was the same. However, if the " "contracts were compatible on the provided value (say int() and string() " "contracts on the value \"123\") they were different. This is considered to " "be a bad pattern. Now, the value is stored only once per object and " "transformed by the contract defined in the actual object type. All base " "contracts are used to validate the transformed object thus this pattern will " "not work anymore." msgstr "" "Previously, when a class overrode a property from its parent class the value " "was stored separately for both of them, transformed by each of the " "contracts. Thus each class saw the value of its contract. In absolute " "majority of the cases, the observed value was the same. However, if the " "contracts were compatible on the provided value (say int() and string() " "contracts on the value \"123\") they were different. This is considered to " "be a bad pattern. Now, the value is stored only once per object and " "transformed by the contract defined in the actual object type. All base " "contracts are used to validate the transformed object thus this pattern will " "not work any more." msgid "" "Previously, when pre-deployment garbage collection occurred it executed ``." "destroy`` method for objects that were present in the ``ObjectsCopy`` " "section of the object model (which is the the snapshot of the model after " "last deployment) and not present in the current model anymore (because they " "were deleted through the API between deployments). If the destroyed objects " "were to access another object that was not deleted it was accessing its copy " "from the ``ObjectsCopy``. Thus any changes to the internal state made by " "that object were lost after the garbage collection finished (that is, before " "the ``.deploy`` method call) and could not affect the deployment. Now, if " "the object is present in both ``Objects`` and ``ObjectsCopy``, a single " "instance (the one from ``Objects``) is used for both garbage collection and " "deployment. As a consequence, instances (in their ``.destroy`` method) now " "may observe changes made to other objects they refer if they were not " "deleted, but modified through the API. In some rare cases, it may break " "existing applications." msgstr "" "Previously, when pre-deployment garbage collection occurred it executed ``." "destroy`` method for objects that were present in the ``ObjectsCopy`` " "section of the object model (which is the the snapshot of the model after " "last deployment) and not present in the current model any more (because they " "were deleted through the API between deployments). If the destroyed objects " "were to access another object that was not deleted it was accessing its copy " "from the ``ObjectsCopy``. Thus any changes to the internal state made by " "that object were lost after the garbage collection finished (that is, before " "the ``.deploy`` method call) and could not affect the deployment. Now, if " "the object is present in both ``Objects`` and ``ObjectsCopy``, a single " "instance (the one from ``Objects``) is used for both garbage collection and " "deployment. As a consequence, instances (in their ``.destroy`` method) now " "may observe changes made to other objects they refer if they were not " "deleted, but modified through the API. In some rare cases, it may break " "existing applications." msgid "" "Python 2.7 support has been dropped. Last release of Murano to support " "python 2.7 is OpenStack Train. The minimum version of Python now supported " "by Murano is Python 3.6." msgstr "" "Python 2.7 support has been dropped. Last release of Murano to support " "python 2.7 is OpenStack Train. The minimum version of Python now supported " "by Murano is Python 3.6." msgid "" "Python 3.6 & 3.7 support has been dropped. The minimum version of Python now " "supported is Python 3.8." msgstr "" "Python 3.6 & 3.7 support has been dropped. The minimum version of Python now " "supported is Python 3.8." msgid "Queens Series Release Notes" msgstr "Queens Series Release Notes" msgid "" "Remove hardcoded constant called 'ITERATORS_LIMIT', that can be exceeded " "(2000) having big amount of objects. Introduce dsl_iterators_limit " "configuration option instead of constant." msgstr "" "Remove hardcoded constant called 'ITERATORS_LIMIT', that can be exceeded " "(2000) having big amount of objects. Introduce dsl_iterators_limit " "configuration option instead of constant." msgid "" "Removed `show_categories` endpoint from the application catalog API which " "has been deprecated since the Liberty cycle. Use `list_categories` instead." msgstr "" "Removed `show_categories` endpoint from the application catalogue API which " "has been deprecated since the Liberty cycle. Use `list_categories` instead." msgid "" "Removed the need for Keystone v2 options (admin_user, admin_password, " "admin_tenant_name) when Keystone v3 is in use." msgstr "" "Removed the need for Keystone v2 options (admin_user, admin_password, " "admin_tenant_name) when Keystone v3 is in use." msgid "" "Renamed the ``workers`` option from the ``engine`` group to " "``engine_workers`` to reduce ambiguity with the ``api_workers`` option." msgstr "" "Renamed the ``workers`` option from the ``engine`` group to " "``engine_workers`` to reduce ambiguity with the ``api_workers`` option." msgid "" "RequestContext now serialises it's roles. This should allow murano to work " "correctly (and allow rules like \"role:xxx\" in policy.json) when using oslo." "context prior to 2.2.0 and oslo.policy" msgstr "" "RequestContext now serialises it's roles. This should allow Murano to work " "correctly (and allow rules like \"role:xxx\" in policy.json) when using oslo." "context prior to 2.2.0 and oslo.policy" msgid "" "Requires a valid secret storage backend (e.g. Barbican) to be configured via " "Castellan." msgstr "" "Requires a valid secret storage backend (e.g. Barbican) to be configured via " "Castellan." msgid "Rocky Series Release Notes" msgstr "Rocky Series Release Notes" msgid "Security Issues" msgstr "Security Issues" msgid "" "Separated murano service broker from murano-api into a murano-cfapi service. " "Created a separate database and ``paste.ini`` for service broker." msgstr "" "Separated Murano service broker from murano-api into a murano-cfapi service. " "Created a separate database and ``paste.ini`` for service broker." msgid "" "Since Newton release, heat is available as a devstack plugin. So we remove " "heat as enable_service in devstack." msgstr "" "Since Newton release, heat is available as a devstack plugin. So we remove " "heat as enable_service in devstack." msgid "" "Split ``Instance``'s ``.deploy()`` method into two phases - " "``beginDeploy()`` and ``endDeploy()``. This allows the application developer " "to provision multiple instances at once without the need to push the stack " "for each instance." msgstr "" "Split ``Instance``'s ``.deploy()`` method into two phases - " "``beginDeploy()`` and ``endDeploy()``. This allows the application developer " "to provision multiple instances at once without the need to push the stack " "for each instance." msgid "" "Static methods and properties were introduced. Both properties and methods " "can be marked as Usage: Static Statics can be accessed using ns:Class." "property / ns:Class.method(), :Class.property / :Class.method() to access " "class from current namespace or type('full.name').property / type('full." "name').method() to use full type name." msgstr "" "Static methods and properties were introduced. Both properties and methods " "can be marked as Usage: Static Statics can be accessed using ns:Class." "property / ns:Class.method(), :Class.property / :Class.method() to access " "class from current namespace or type('full.name').property / type('full." "name').method() to use full type name." msgid "Stein Series Release Notes" msgstr "Stein Series Release Notes" msgid "" "The ``string()`` contract no longer converts everything to string values. " "Now it only converts scalar values to strings. Previous behavior allowed " "``string()`` property to accept lists and convert them to their Python " "string representation which is clearly not what developers expected." msgstr "" "The ``string()`` contract no longer converts everything to string values. " "Now it only converts scalar values to strings. Previous behaviour allowed " "``string()`` property to accept lists and convert them to their Python " "string representation which is clearly not what developers expected." msgid "" "The contract ``class()`` now uses the same approach to load classes from " "dictionaries. Thus the same two syntaxes apply there as well." msgstr "" "The contract ``class()`` now uses the same approach to load classes from " "dictionaries. Thus the same two syntaxes apply there as well." msgid "" "The default value of ``[oslo_policy] policy_file`` config option has been " "changed from ``policy.json`` to ``policy.yaml``. Operators who are utilizing " "customized or previously generated static policy JSON files (which are not " "needed by default), should generate new policy files or convert them in YAML " "format. Use the `oslopolicy-convert-json-to-yaml `_ tool to " "convert a JSON to YAML formatted policy file in backward compatible way." msgstr "" "The default value of ``[oslo_policy] policy_file`` config option has been " "changed from ``policy.json`` to ``policy.yaml``. Operators who are utilising " "customised or previously generated static policy JSON files (which are not " "needed by default), should generate new policy files or convert them in YAML " "format. Use the `oslopolicy-convert-json-to-yaml `_ tool to " "convert a JSON to YAML formatted policy file in backward compatible way." msgid "" "The test-runner now does not output logs to stderr by default unless a " "'use_stderr' parameter is specified in the configuration file." msgstr "" "The test-runner now does not output logs to stderr by default unless a " "'use_stderr' parameter is specified in the configuration file." msgid "" "The test-runner now outputs the tests it runs and their results to stdout " "directly, instead of the logging system." msgstr "" "The test-runner now outputs the tests it runs and their results to stdout " "directly, instead of the logging system." msgid "" "The value that is stored in the object's properties is obtained by executing " "special \"finalize\" contract implementation which by default returns the " "input value unmodified. Because validation happens on the transformed value " "before it gets finalized it is possible for transformation to return a value " "that will pass the validation though the final value won't. This is used to " "relax the template() contract limitation that prevented child class from " "excluding additional properties from the template." msgstr "" "The value that is stored in the object's properties is obtained by executing " "special \"finalise\" contract implementation which by default returns the " "input value unmodified. Because validation happens on the transformed value " "before it gets finalized it is possible for transformation to return a value " "that will pass the validation though the final value won't. This is used to " "relax the template() contract limitation that prevented child class from " "excluding additional properties from the template." msgid "Train Series Release Notes" msgstr "Train Series Release Notes" msgid "Upgrade Notes" msgstr "Upgrade Notes" msgid "" "Use of JSON policy files was deprecated by the ``oslo.policy`` library " "during the Victoria development cycle. As a result, this deprecation is " "being noted in the Wallaby cycle with an anticipated future removal of " "support by ``oslo.policy``. As such operators will need to convert to YAML " "policy files. Please see the upgrade notes for details on migration of any " "custom policy files." msgstr "" "Use of JSON policy files was deprecated by the ``oslo.policy`` library " "during the Victoria development cycle. As a result, this deprecation is " "being noted in the Wallaby cycle with an anticipated future removal of " "support by ``oslo.policy``. As such operators will need to convert to YAML " "policy files. Please see the upgrade notes for details on the migration of " "any custom policy files." msgid "" "Users can now assign an existing security group to an application as an " "alternative to using the one created by Murano's ``SecurityGroupManager``." msgstr "" "Users can now assign an existing security group to an application as an " "alternative to using the one created by Murano's ``SecurityGroupManager``." msgid "Ussuri Series Release Notes" msgstr "Ussuri Series Release Notes" msgid "Victoria Series Release Notes" msgstr "Victoria Series Release Notes" msgid "Wallaby Series Release Notes" msgstr "Wallaby Series Release Notes" msgid "" "When updating to Mitaka, the operator should update service name and type " "for endpoint in keystone from \"application_catalog\" to \"application-" "catalog\" if SQL is used for catalog back-end driver." msgstr "" "When updating to Mitaka, the operator should update service name and type " "for endpoint in Keystone from \"application_catalog\" to \"application-" "catalog\" if SQL is used for catalogue back-end driver." msgid "" "Whenever murano-engine accesses script files, text script files are opened " "in 'rU' mode which recognizes all types of newlines, and binary files are " "opened in 'rb' mode to prevent their corruption." msgstr "" "Whenever murano-engine accesses script files, text script files are opened " "in 'rU' mode which recognises all types of newlines, and binary files are " "opened in 'rb' mode to prevent their corruption." msgid "Xena Series Release Notes" msgstr "Xena Series Release Notes" msgid "Yoga Series Release Notes" msgstr "Yoga Series Release Notes" msgid "Zed Series Release Notes" msgstr "Zed Series Release Notes" msgid "" "added default rules to NeutronSecurityGroupManager to avoid error if " "`createDefaultInstanceSecurityGroupRules()` method isn't extended in " "inheritor and SecurityGroups isn't created in application with call of " "`addGroupIngress()` method." msgstr "" "added default rules to NeutronSecurityGroupManager to avoid error if " "`createDefaultInstanceSecurityGroupRules()` method isn't extended in " "inheritor and SecurityGroups isn't created in application with call of " "`addGroupIngress()` method." msgid "" "cve-2016-4972 has been addressed. In ceveral 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." msgstr "" "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." msgid "io.murano.configuration.Linux methods are now static" msgstr "io.murano.configuration.Linux methods are now static" msgid "" "io.murano.system.GC.isDestroyed() static method was added. It checks if the " "object is destroyed and thus no methods can be invoked on it." msgstr "" "io.murano.system.GC.isDestroyed() static method was added. It checks if the " "object is destroyed and thus no methods can be invoked on it." msgid "" "io.murano.system.GC.isDoomed() static method was added. It can be used " "within the ``.destroy`` method to test if other object is also going to be " "destroyed." msgstr "" "io.murano.system.GC.isDoomed() static method was added. It can be used " "within the ``.destroy`` method to test if other object is also going to be " "destroyed." msgid "" "io.murano.system.HeatStack.push can be called with async => true flag for " "asynchronous push" msgstr "" "io.murano.system.HeatStack.push can be called with async => true flag for " "asynchronous push" ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1696417899.6811805 murano-16.0.0/releasenotes/source/locale/fr/0000775000175000017500000000000000000000000020761 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1696417899.8851812 murano-16.0.0/releasenotes/source/locale/fr/LC_MESSAGES/0000775000175000017500000000000000000000000022546 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/releasenotes/source/locale/fr/LC_MESSAGES/releasenotes.po0000664000175000017500000000260300000000000025600 0ustar00zuulzuul00000000000000# Gérald LONLAS , 2016. #zanata msgid "" msgstr "" "Project-Id-Version: murano\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2023-04-26 08:25+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-22 04:53+0000\n" "Last-Translator: Gérald LONLAS \n" "Language-Team: French\n" "Language: fr\n" "X-Generator: Zanata 4.3.3\n" "Plural-Forms: nplurals=2; plural=(n > 1)\n" msgid "1.0.2" msgstr "1.0.2" msgid "1.0.3" msgstr "1.0.3" msgid "2.0.0" msgstr "2.0.0" msgid "2.0.1" msgstr "2.0.1" msgid "2.0.2" msgstr "2.0.2" msgid "3.0.0" msgstr "3.0.0" msgid "Bug Fixes" msgstr "Corrections de bugs" msgid "Current Series Release Notes" msgstr "Note de la release actuelle" msgid "Deprecation Notes" msgstr "Notes dépréciées " msgid "Known Issues" msgstr "Problèmes connus" msgid "Liberty Series Release Notes" msgstr "Note de release pour Liberty" msgid "Mitaka Series Release Notes" msgstr "Note de release pour Mitaka" msgid "Murano Release Notes" msgstr "Note de release de Murano" msgid "New Features" msgstr "Nouvelles fonctionnalités" msgid "Newton Series Release Notes" msgstr "Note de release pour Newton" msgid "Other Notes" msgstr "Autres notes" msgid "Security Issues" msgstr "Problèmes de sécurités" msgid "Upgrade Notes" msgstr "Notes de mises à jours" ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/releasenotes/source/mitaka.rst0000664000175000017500000000023200000000000021110 0ustar00zuulzuul00000000000000=================================== Mitaka Series Release Notes =================================== .. release-notes:: :branch: origin/stable/mitaka ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/releasenotes/source/newton.rst0000664000175000017500000000023200000000000021154 0ustar00zuulzuul00000000000000=================================== Newton Series Release Notes =================================== .. release-notes:: :branch: origin/stable/newton ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/releasenotes/source/ocata.rst0000664000175000017500000000023000000000000020727 0ustar00zuulzuul00000000000000=================================== Ocata Series Release Notes =================================== .. release-notes:: :branch: origin/stable/ocata ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/releasenotes/source/pike.rst0000664000175000017500000000021700000000000020575 0ustar00zuulzuul00000000000000=================================== Pike Series Release Notes =================================== .. release-notes:: :branch: stable/pike ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/releasenotes/source/queens.rst0000664000175000017500000000022300000000000021142 0ustar00zuulzuul00000000000000=================================== Queens Series Release Notes =================================== .. release-notes:: :branch: stable/queens ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/releasenotes/source/rocky.rst0000664000175000017500000000022100000000000020767 0ustar00zuulzuul00000000000000=================================== Rocky Series Release Notes =================================== .. release-notes:: :branch: stable/rocky ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/releasenotes/source/stein.rst0000664000175000017500000000022100000000000020762 0ustar00zuulzuul00000000000000=================================== Stein Series Release Notes =================================== .. release-notes:: :branch: stable/stein ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/releasenotes/source/train.rst0000664000175000017500000000017600000000000020766 0ustar00zuulzuul00000000000000========================== Train Series Release Notes ========================== .. release-notes:: :branch: stable/train ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/releasenotes/source/unreleased.rst0000664000175000017500000000016000000000000021771 0ustar00zuulzuul00000000000000============================== Current Series Release Notes ============================== .. release-notes:: ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/releasenotes/source/ussuri.rst0000664000175000017500000000020200000000000021171 0ustar00zuulzuul00000000000000=========================== Ussuri Series Release Notes =========================== .. release-notes:: :branch: stable/ussuri ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/releasenotes/source/victoria.rst0000664000175000017500000000021200000000000021460 0ustar00zuulzuul00000000000000============================= Victoria Series Release Notes ============================= .. release-notes:: :branch: stable/victoria ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/releasenotes/source/wallaby.rst0000664000175000017500000000020600000000000021276 0ustar00zuulzuul00000000000000============================ Wallaby Series Release Notes ============================ .. release-notes:: :branch: stable/wallaby ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/releasenotes/source/xena.rst0000664000175000017500000000017200000000000020600 0ustar00zuulzuul00000000000000========================= Xena Series Release Notes ========================= .. release-notes:: :branch: stable/xena ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/releasenotes/source/yoga.rst0000664000175000017500000000017200000000000020604 0ustar00zuulzuul00000000000000========================= Yoga Series Release Notes ========================= .. release-notes:: :branch: stable/yoga ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/releasenotes/source/zed.rst0000664000175000017500000000016600000000000020432 0ustar00zuulzuul00000000000000======================== Zed Series Release Notes ======================== .. release-notes:: :branch: stable/zed ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/requirements.txt0000664000175000017500000000350100000000000016405 0ustar00zuulzuul00000000000000# Requirements lower bounds listed here are our best effort to keep them up to # date but we do not test them so no guarantee of having them all correct. If # you find any incorrect lower bounds, let us know or propose a fix. # The order of packages is significant, because pip processes them in the order # of appearance. Changing the order has an impact on the overall integration # process, which may cause wedges in the gate later. pbr!=2.1.0,>=2.0.0 # Apache-2.0 Babel!=2.4.0,>=2.3.4 # BSD SQLAlchemy!=1.1.5,!=1.1.6,!=1.1.7,!=1.1.8,>=1.0.10 # MIT stevedore>=1.20.0 # Apache-2.0 alembic>=0.9.6 # MIT eventlet>=0.26.0 # MIT PasteDeploy>=1.5.0 # MIT Routes>=2.3.1 # MIT tenacity>=4.12.0 # Apache-2.0 WebOb>=1.7.1 # MIT kombu>=4.6.1 # BSD psutil>=3.2.2 # BSD netaddr>=0.7.18 # BSD PyYAML>=5.1 # MIT jsonpatch!=1.20,>=1.16 # BSD keystoneauth1>=3.8.0 # Apache-2.0 keystonemiddleware>=4.17.0 # Apache-2.0 testtools>=2.2.0 # MIT yaql>=1.1.3 # Apache 2.0 License debtcollector>=1.2.0 # Apache-2.0 cryptography>=2.7 # BSD/Apache-2.0 # For paste.util.template used in keystone.common.template Paste>=2.0.2 # MIT jsonschema>=3.2.0 # MIT python-keystoneclient>=3.17.0 # Apache-2.0 python-heatclient>=1.10.0 # Apache-2.0 python-neutronclient>=6.7.0 # Apache-2.0 python-muranoclient>=0.8.2 # Apache-2.0 python-mistralclient!=3.2.0,>=3.1.0 # Apache-2.0 oslo.db>=4.44.0 # Apache-2.0 oslo.config>=6.8.0 # Apache-2.0 oslo.concurrency>=3.26.0 # Apache-2.0 oslo.context>=2.22.0 # Apache-2.0 oslo.policy>=3.6.0 # Apache-2.0 oslo.messaging>=5.29.0 # Apache-2.0 oslo.middleware>=3.31.0 # Apache-2.0 oslo.serialization!=2.19.1,>=2.18.0 # Apache-2.0 oslo.service>=1.31.0 # Apache-2.0 oslo.utils>=4.5.0 # Apache-2.0 oslo.i18n>=3.15.3 # Apache-2.0 oslo.log>=3.36.0 # Apache-2.0 oslo.upgradecheck>=1.3.0 # Apache-2.0 semantic-version>=2.8.2 # BSD castellan>=0.18.0 # Apache-2.0 ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1696417899.8851812 murano-16.0.0/setup.cfg0000664000175000017500000000416200000000000014746 0ustar00zuulzuul00000000000000[metadata] name = murano summary = Murano API description-file = README.rst license = Apache License, Version 2.0 author = OpenStack author-email = openstack-discuss@lists.openstack.org home-page = https://www.openstack.org/software/releases/mitaka/components/murano python-requires = >=3.8 classifier = Development Status :: 5 - Production/Stable Environment :: OpenStack Intended Audience :: Developers Intended Audience :: Information Technology License :: OSI Approved :: Apache Software License Operating System :: OS Independent Programming Language :: Python Programming Language :: Python :: Implementation :: CPython Programming Language :: Python :: 3 :: Only Programming Language :: Python :: 3 Programming Language :: Python :: 3.8 [files] packages = murano data_files = etc/murano = etc/murano/murano-cfapi-paste.ini etc/murano/murano-paste.ini [entry_points] console_scripts = murano-api = murano.cmd.api:main murano-engine = murano.cmd.engine:main murano-manage = murano.cmd.manage:main murano-db-manage = murano.cmd.db_manage:main murano-cfapi-db-manage = murano.cmd.cfapi_db_manage:main murano-test-runner = murano.cmd.test_runner:main murano-cfapi = murano.cmd.cfapi:main murano-status = murano.cmd.status:main wsgi_scripts = murano-wsgi-api = murano.httpd.murano_api:init_application oslo.config.opts = murano = murano.opts:list_opts keystone_authtoken = keystonemiddleware.opts:list_auth_token_opts murano.cfapi = murano.opts:list_cfapi_opts castellan.config = castellan.options:list_opts oslo.config.opts.defaults = murano = murano.common.config:set_lib_defaults oslo.policy.policies = murano = murano.common.policies:list_rules murano_policy_modify_actions = remove-object = murano.policy.modify.actions.default_actions:RemoveObjectAction add-object = murano.policy.modify.actions.default_actions:AddObjectAction set-property = murano.policy.modify.actions.default_actions:SetPropertyAction remove-relation = murano.policy.modify.actions.default_actions:RemoveRelationAction add-relation = murano.policy.modify.actions.default_actions:AddRelationAction [egg_info] tag_build = tag_date = 0 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/setup.py0000664000175000017500000000127100000000000014635 0ustar00zuulzuul00000000000000# Copyright (c) 2013 Hewlett-Packard Development Company, L.P. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT 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 setuptools setuptools.setup( setup_requires=['pbr>=2.0.0'], pbr=True) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/test-requirements.txt0000664000175000017500000000145500000000000017370 0ustar00zuulzuul00000000000000# The order of packages is significant, because pip processes them in the order # of appearance. Changing the order has an impact on the overall integration # process, which may cause wedges in the gate later. hacking>=3.0.1,<3.1.0 # Apache-2.0 coverage!=4.4,>=4.0 # Apache-2.0 fixtures>=3.0.0 # Apache-2.0/BSD nose>=1.3.7 # LGPL oslotest>=4.4.1 # Apache-2.0 sqlalchemy-migrate>=0.11.0 # Apache-2.0 testrepository>=0.0.18 # Apache-2.0/BSD testresources>=2.0.0 # Apache-2.0/BSD testscenarios>=0.4 # Apache-2.0/BSD pylint==1.4.5 # GPLv2 pycodestyle>=2.5.0 # MIT License requests>=2.20.0 # Apache-2.0 stestr>=2.0.0 # Apache-2.0 murano-pkg-check>=0.3.0 # Apache-2.0 bandit>=1.1.0,!=1.6.0 # Apache-2.0 # Some of the tests use real MySQL and Postgres databases PyMySQL>=0.8.0 # MIT License psycopg2>=2.8.5 # LGPL/ZPL ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1696417899.8851812 murano-16.0.0/tools/0000775000175000017500000000000000000000000014262 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/tools/lintstack.py0000775000175000017500000001673300000000000016645 0ustar00zuulzuul00000000000000#!/usr/bin/env python # Copyright (c) 2015 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. """pylint error checking.""" import io import json import os import re import sys from pylint import lint from pylint.reporters import text # enabled checks # http://pylint-messages.wikidot.com/all-codes ENABLED_CODES = ( # refactor category "R0801", "R0911", "R0912", "R0913", "R0914", "R0915", # warning category "W0612", "W0613", "W0703", # convention category "C1001") LINE_PATTERN = r"(\S+):(\d+): \[(\S+)(, \S*)?] (.*)" KNOWN_PYLINT_EXCEPTIONS_FILE = "tools/pylint_exceptions" class LintOutput(object): _cached_filename = None _cached_content = None def __init__(self, filename, lineno, line_content, code, message, lintoutput, additional_content): self.filename = filename self.lineno = lineno self.line_content = line_content self.code = code self.message = message self.lintoutput = lintoutput self.additional_content = additional_content @classmethod def get_duplicate_code_location(cls, remaining_lines): module, lineno = remaining_lines.pop(0)[2:].split(":") filename = module.replace(".", os.sep) + ".py" return filename, int(lineno) @classmethod def get_line_content(cls, filename, lineno): if cls._cached_filename != filename: with open(filename) as f: cls._cached_content = list(f.readlines()) cls._cached_filename = filename # find first non-empty line lineno -= 1 while True: line_content = cls._cached_content[lineno].rstrip() lineno += 1 if line_content: return line_content @classmethod def from_line(cls, line, remaining_lines): m = re.search(LINE_PATTERN, line) if not m: return None matched = m.groups() filename, lineno, code, message = (matched[0], int(matched[1]), matched[2], matched[-1]) additional_content = None # duplicate code output needs special handling if "duplicate-code" in code: line_count = 0 for next_line in remaining_lines: if re.search(LINE_PATTERN, next_line): break line_count += 1 if line_count: additional_content = remaining_lines[0:line_count] filename, lineno = cls.get_duplicate_code_location(remaining_lines) # fixes incorrectly reported file path line = line.replace(matched[0], filename) line = line.replace(":%s:" % matched[1], "") line_content = cls.get_line_content(filename, lineno) return cls(filename, lineno, line_content, code, message, line.rstrip(), additional_content) @classmethod def from_msg_to_dict(cls, msg): """Creates dict from pylint msg From the output of pylint msg, to a dict, where each key is a unique error identifier, value is a list of LintOutput """ result = {} lines = msg.splitlines() while lines: line = lines.pop(0) obj = cls.from_line(line, lines) if not obj: continue key = obj.key() if key not in result: result[key] = [] result[key].append(obj) return result def key(self): return self.message, self.line_content.strip() def json(self): return json.dumps(self.__dict__) def review_str(self): kargs = {"filename": self.filename, "lineno": self.lineno, "line_content": self.line_content, "code": self.code, "message": self.message} return ("File %(filename)s\nLine %(lineno)d:%(line_content)s\n" "%(code)s: %(message)s" % kargs) class ErrorKeys(object): @classmethod def print_json(cls, errors, output=sys.stdout): print("# automatically generated by tools/lintstack.py", file=output) for i in sorted(errors.keys()): print(json.dumps(i), file=output) @classmethod def from_file(cls, filename): keys = set() for line in open(filename): if line and line[0] != "#": d = json.loads(line) keys.add(tuple(d)) return keys def run_pylint(): buff = io.StringIO() reporter = text.ParseableTextReporter(output=buff) args = ["-rn", "--disable=all", "--enable=" + ",".join(ENABLED_CODES), "murano"] lint.Run(args, reporter=reporter, exit=False) val = buff.getvalue() buff.close() return val def generate_error_keys(msg=None): print("Generating", KNOWN_PYLINT_EXCEPTIONS_FILE) if msg is None: msg = run_pylint() errors = LintOutput.from_msg_to_dict(msg) with open(KNOWN_PYLINT_EXCEPTIONS_FILE, "w") as f: ErrorKeys.print_json(errors, output=f) def validate(newmsg=None): print("Loading", KNOWN_PYLINT_EXCEPTIONS_FILE) known = ErrorKeys.from_file(KNOWN_PYLINT_EXCEPTIONS_FILE) if newmsg is None: print("Running pylint. Be patient...") newmsg = run_pylint() errors = LintOutput.from_msg_to_dict(newmsg) print() print("Unique errors reported by pylint: was %d, now %d." % (len(known), len(errors))) passed = True for err_key, err_list in errors.items(): for err in err_list: if err_key not in known: print() print(err.lintoutput) print(err.review_str()) if err.additional_content: max_len = max(map(len, err.additional_content)) print("-" * max_len) print(os.linesep.join(err.additional_content)) print("-" * max_len) passed = False if passed: print("Congrats! pylint check passed.") redundant = known - set(errors.keys()) if redundant: print("Extra credit: some known pylint exceptions disappeared.") for i in sorted(redundant): print(json.dumps(i)) print("Consider regenerating the exception file if you will.") else: print() print("Please fix the errors above. If you believe they are false " "positives, run 'tools/lintstack.py generate' to overwrite.") sys.exit(1) def usage(): print("""Usage: tools/lintstack.py [generate|validate] To generate pylint_exceptions file: tools/lintstack.py generate To validate the current commit: tools/lintstack.py """) def main(): option = "validate" if len(sys.argv) > 1: option = sys.argv[1] if option == "generate": generate_error_keys() elif option == "validate": validate() else: usage() if __name__ == "__main__": main() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/tools/lintstack.sh0000775000175000017500000000275700000000000016630 0ustar00zuulzuul00000000000000#!/usr/bin/env bash # Use lintstack.py to compare pylint errors. # We run pylint twice, once on HEAD, once on the code before the latest # commit for review. set -e TOOLS_DIR=$(cd $(dirname "$0") && pwd) # Get the current branch name. GITHEAD=`git rev-parse --abbrev-ref HEAD` if [[ "$GITHEAD" == "HEAD" ]]; then # In detached head mode, get revision number instead GITHEAD=`git rev-parse HEAD` echo "Currently we are at commit $GITHEAD" else echo "Currently we are at branch $GITHEAD" fi cp -f $TOOLS_DIR/lintstack.py $TOOLS_DIR/lintstack.head.py if git rev-parse HEAD^2 2>/dev/null; then # The HEAD is a Merge commit. Here, the patch to review is # HEAD^2, the master branch is at HEAD^1, and the patch was # written based on HEAD^2~1. PREV_COMMIT=`git rev-parse HEAD^2~1` git checkout HEAD~1 # The git merge is necessary for reviews with a series of patches. # If not, this is a no-op so won't hurt either. git merge $PREV_COMMIT else # The HEAD is not a merge commit. This won't happen on gerrit. # Most likely you are running against your own patch locally. # We assume the patch to examine is HEAD, and we compare it against # HEAD~1 git checkout HEAD~1 fi # First generate tools/pylint_exceptions from HEAD~1 $TOOLS_DIR/lintstack.head.py generate # Then use that as a reference to compare against HEAD git checkout $GITHEAD $TOOLS_DIR/lintstack.head.py echo "Check passed. FYI: the pylint exceptions are:" cat $TOOLS_DIR/pylint_exceptions ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/tools/test-setup.sh0000775000175000017500000000373700000000000016750 0ustar00zuulzuul00000000000000#!/bin/bash -xe # This script will be run by OpenStack CI before unit tests are run, # it sets up the test system as needed. # Developers should setup their test systems in a similar way. # This setup needs to be run as a user that can run sudo. # The root password for the MySQL database; pass it in via # MYSQL_ROOT_PW. DB_ROOT_PW=${MYSQL_ROOT_PW:-insecure_slave} # This user and its password are used by the tests, if you change it, # your tests might fail. DB_USER=openstack_citest DB_PW=openstack_citest sudo -H mysqladmin -u root password $DB_ROOT_PW # It's best practice to remove anonymous users from the database. If # an anonymous user exists, then it matches first for connections and # other connections from that host will not work. sudo -H mysql -u root -p$DB_ROOT_PW -h localhost -e " DELETE FROM mysql.user WHERE User=''; FLUSH PRIVILEGES; CREATE USER '$DB_USER'@'%' IDENTIFIED BY '$DB_PW'; GRANT ALL PRIVILEGES ON *.* TO '$DB_USER'@'%' WITH GRANT OPTION;" # Now create our database. mysql -u $DB_USER -p$DB_PW -h 127.0.0.1 -e " SET default_storage_engine=MYISAM; DROP DATABASE IF EXISTS openstack_citest; CREATE DATABASE openstack_citest CHARACTER SET utf8;" # Same for PostgreSQL # The root password for the PostgreSQL database; pass it in via # POSTGRES_ROOT_PW. DB_ROOT_PW=${POSTGRES_ROOT_PW:-insecure_slave} # Setup user root_roles=$(sudo -H -u postgres psql -t -c " SELECT 'HERE' from pg_roles where rolname='$DB_USER'") if [[ ${root_roles} == *HERE ]];then sudo -H -u postgres psql -c "ALTER ROLE $DB_USER WITH SUPERUSER LOGIN PASSWORD '$DB_PW'" else sudo -H -u postgres psql -c "CREATE ROLE $DB_USER WITH SUPERUSER LOGIN PASSWORD '$DB_PW'" fi # Store password for tests cat << EOF > $HOME/.pgpass *:*:*:$DB_USER:$DB_PW EOF chmod 0600 $HOME/.pgpass # Now create our database psql -h 127.0.0.1 -U $DB_USER -d template1 -c "DROP DATABASE IF EXISTS openstack_citest" createdb -h 127.0.0.1 -U $DB_USER -l C -T template0 -E utf8 openstack_citest ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/tox.ini0000664000175000017500000000666700000000000014454 0ustar00zuulzuul00000000000000[tox] envlist = py38,pep8 minversion = 2.0 skipsdist = True [testenv] usedevelop = True setenv = VIRTUAL_ENV={envdir} passenv = http_proxy HTTP_PROXY https_proxy HTTPS_PROXY no_proxy NO_PROXY deps = -c{env:TOX_CONSTRAINTS_FILTOX_CONSTRAINTS_FILEE:https://releases.openstack.org/constraints/upper/master} -r{toxinidir}/requirements.txt -r{toxinidir}/test-requirements.txt commands = rm -f .testrepository/times.dbm stestr run {posargs} allowlist_externals = bash find rm bandit [testenv:murano-test-runner] commands = murano-test-runner {posargs} [testenv:pep8] commands = flake8 {posargs} {[testenv:bandit]commands} [testenv:bandit] commands = bandit -c bandit.yaml -r murano -x tests -n 5 -ll [testenv:venv] commands = {posargs} [testenv:cover] setenv = {[testenv]setenv} PYTHON=coverage run --source murano --parallel-mode commands = stestr run {posargs} coverage combine coverage html -d cover coverage xml -o cover/coverage.xml [testenv:debug] commands = find . -type f -name "*.pyc" -delete oslo_debug_helper {posargs} [testenv:docs] deps = -r{toxinidir}/doc/requirements.txt commands = sphinx-build -a -E -W -d doc/build/doctrees -b html doc/source doc/build/html allowlist_externals = sphinx-build [testenv:pdf-docs] deps = {[testenv:docs]deps} allowlist_externals = make sphinx-build commands = sphinx-build -W -b latex doc/source doc/build/pdf make -C doc/build/pdf [testenv:pyflakes] deps = flake8 commands = flake8 [testenv:pylint] setenv = VIRTUAL_ENV={envdir} commands = bash tools/lintstack.sh [testenv:genconfig] commands = oslo-config-generator --config-file etc/oslo-config-generator/murano.conf [testenv:gencfconfig] commands = oslo-config-generator --config-file etc/oslo-config-generator/murano-cfapi.conf [testenv:genpolicy] commands = oslopolicy-sample-generator --config-file etc/oslo-policy-generator/murano-policy-generator.conf [testenv:releasenotes] commands = sphinx-build -a -E -W -d releasenotes/build/doctrees -b html releasenotes/source releasenotes/build/html [testenv:api-ref] # This environment is called from CI scripts to test and publish # the API Ref to docs.openstack.org. deps = -r{toxinidir}/doc/requirements.txt commands = rm -rf api-ref/build sphinx-build -W -b html -d api-ref/build/doctrees api-ref/source api-ref/build/html whitelist_externals = rm [flake8] show-source = true builtins = _ # W605 invalid escape sequence # E402 fires an error on eventlet import. Should probably remove condition alltogether ignore = W605,W504,W503,E123,H405,E402 exclude=.venv,.git,.tox,dist,doc,*lib/python*,*egg,build [hacking] import_exceptions = oslo.db.sqlalchemy.test_base, murano.common.i18n [flake8:local-plugins] extension = M318 = checks:assert_equal_none M322 = checks:no_mutable_default_args M326 = checks:check_no_basestring paths = ./murano/hacking [testenv:bindep] # Do not install any requirements. We want this to be fast and work even if # system dependencies are missing, since it's used to tell you what system # dependencies are missing! This also means that bindep must be installed # separately, outside of the requirements files, and develop mode disabled # explicitly to avoid unnecessarily installing the checked-out repo too (this # further relies on "tox.skipsdist = True" above). deps = bindep commands = bindep test usedevelop = False

    &?qc^-I0 F\;)j(WK~QNW0^ha~aO'N+dZ=K.c*{I$hm݂Zcj"1V+?w#%5ok"[i`?z3jYV>sǻ}i>Ne"xK|޸ծvHQT tiR`"Ezm|wD|ZF2|b1Qܜbh(;#eb$7@+"_RR 7wII9CFzA3I$~m+H bKQ@ܞAMq-"t8:yA3JRg=M3; 8꫗Gxh:|>Ӊ:vW?uەS zǢ? K}[Rk#:H|2潓ęNIN1=;6u1-|2&tzXkƊգbғgz {/u#]qёe1skho M~}£+OJ5(: G3zGϝ"-Y=l]v]Zzܘ, wxIaԦngńNGI\z?rte1s'kf\7͠> )[oE32K!гI'L>R|£+bOJЀ赀qb0 "vE|/ o&4ڐK$.O,vdŊ#\uci[X:1pt >_ AkCׇMDF r=]AFx,j:V1|ݳ7XF x\5=z=NϨYVaK{ޯ3'$u:OdaP={X,V(NNN7n *ʪ ;;;x֭[Ummĉ RUUyL26lؠjׯ_OTlK]tjf4ڈ13'<}yw饹sIk<*>;Zuc:I6z񽘨n tXOӿN_ħPUinh1 ]6{-{x͌: yřBVK444<U @ϲ[G9چzCCCCNbMS$ e>'NtrD"2eJNNX, 1L _@QoϞ=㯫f]9Nn6ha#m68|[voIV:\nVR0u#әtzZ͚o6xp(&:NSvSSU u)t޺Txj.":zeYÃU,˲,KqP'06F' !B!wugȋ M^anεApy |>a%B!r^mvPB#,, > = B!B7.B!BςEB!BB!BsB!B!]!B! !B! ޭ[B!B%.D²lIzZrA,zNh4:B!RK `Bnt:x!B.>]'0e\NkB!R=2 cVۿz=/B!ZrX OYA!B]0w \bH\&/oEB!%aMZ~6.B!5sB!BaB!B!]!B!0w!B!Пp$'bhłB!Z"@!B!]!B! !B!.B!܅B!BB!B='^NNKtFcQ B?!Iyv, ,8`YVt:N0Z9rs.Z%B!xfff!kh4X!BO03Ή aBB!뢘B!B܅B!BB!BsB!B!]!B! !B!0w!B!܅B!BB!BsB]f¤7\.J澵'酠mjw>pM!0wڿ%b6l=j+]ܦK>Ys}7\U~{#M.]Lۺ/I˭` Pw|ASJ/5 5 IDAT 9(&jZay%9c/]) rЪu[WT~(dIβ#B;!㚂E `\ rr֗̉I.U@ X7v7̋՛"f9AQGV Z%P~imGC̄fxeB!Ž`?C[/^R-:#Yۍ`Μ3>).fd:Wل FUi~sۏ3H~q$~ߋٶm$s-W(q۞c]S$w<:'?݋~QonNMH~ؽN%FEw'M5ux̻?wOYoUQM`XvӀ7]fM' }@YV~Oژ~kJ}K=Md7Lu|ɭ}>ǼUZ74!_\<[=*i ]TrhI𐈨O[灶2Y?>PqmEU2tUʫF  mwvguYyΙlTjag{>N- ?Uu & ߜ Ow `ТtCZc'tpҺ9n }ol9{K7kKo f}'Dռч)o}`Á6i[pnTǪ͌ @ӆۨaؽx=-ny@f9J=44Pϟd%śC7:t ِ|5Ckagr}߁"њwE/vrUk)ϙ|]}ZӤv|~Џ}`?G6˲w}x}E!=ss< jM\qvKqYo9ҦRu3,ksJi;3vk{cG[{_aٱҶY`t\h9#]1tuuXNe;S9,_hy(LyDe@R!({ . I[EXC+hoiHR9%9BHCt9 7A(h $[Viؾ`h9ou:3l>3B)R!STRH5\'P0dPs!){eb4%?绸˖<[˴UPӱ'@A,h|-\==\4UH|.ѲQ m{ ifk{SXK=w>b9U5\qţ)NV[|3[3st3x|'ڊ}Z~")?}HHJ\hCkV fZ·2BwKջ|h,;h*<1V<8'˚O!{K{#->...n㷴f'|mU \dЬlҶ oM<i2mΰ|+q(Jpp?O>ernNuiF?GO\J5< ԥsR_7ÛWw7%Wڱ.Sڅ:1nIH:ZA>!*'c0ta?|T~"5O,F)5YW84n}T? )뱫fK.D=^TPWr0"$#!%R@]S%eoIՠ~qSۄp!- \߄~BmD~<@]~b>5UP}(Wǿ9ތ*0zCDO hPsF@slW0B?:}yWGdc&Isrܻ͜\ך wHDf͵4$Vs'CͪN4N~PiHWNv^Sgvwn\hd|J hj ,ySY"OQjm} -2ԞޝGY4N\^#By39H`YUZU L$l=3[sh#py0syVg9[  _-H05Ⱦ?.ݢXK7snC_o-,*ܸ $8|1vye>_wC?C;!%Hؽw?_:iѸ}dvह>vl9E=?^5sYS9t()6x"2h~l7BUx'-zСퟜn07nTcB+"x 2׵Iq5y.d+9q2^oRŌBhj#:.zO)s/]YnzO/.;[)|})vENg_gl*X:N1 V+**Fkx'u?.ݏ+O2R[ obk-01Xy/\W(;skZJ:l|0% rNZS_UHߔbB!Ыy_[o)Oi;u>quVK#hnjՠ' oXXNd܌(ذG:H"Buy7j{)+--ks+3D!B Mєߟ[ f0t!B!^ 쯤_[B!zpB!BsB!BaB!B! !B!0w!`B!Pgb!BA$.Jr[B!ᘙ={-E@rX!B4,B!܅B!BB!BaB!B?/''K!B!^b% )X:N1 V+**XB!P B!BsB!BaB!B! !B!0w!B!.B!BB!BsB!BaB!B! !B!0w!B!a B!ӪV;êX I}%NNN\.sB!Bl._ZXB+,?4w :a gE*r> =V9>,ӔX| BWD <"xCC}}3RN-D~I?cZeazzYΘKqI}%.?5]轣 6lذq7~ꂰQÆ 6lԪ3 -B!P]܋*&& ȸ# vvCCZѰ }7,'z=wBubFpDAvMM+kj@FDæsi5*xOVgw|o;RSw9ڋ;SO .[2蒧Fm?~ !B]8wid9WO}w8(5fU  .X<3>ۚ<VBs33f'ePh_cf9O?Zc#v]odߏ[Zy=޻emV2qgk*7NjoVUCg5}߬ `._vޞalGiCNm%>qGgh:8-#2;{w*rfv:2h)~.~pro"L+ 2:Spca 0e?kR =د|A _j}^UjΫL,QWr6󳳙_ ]9aagO#B5&f.Սo"B?;[~*]Ur6lWg%$i@@0LZuht(/lYI")#ӧٕ g^ه og;m]lB!:WRy4~n3˫lXШJJa ޓ+9,nprթmbEO~E|,kݭT6L/7(kSޱ{I`QKQ2c{$>gmQ#t8^ǔyۂAxv"BRKɚ|#ܙaB]FE~ָ.ݹ3иjeĎevA<+`50W3.a.bZCuxZ8~QYȡvĎy}ͪ9]5D)#]\PWy%;so*` ]FnfUۇHc&;K`/UՕov.Y~;u~,pvlM4FV56w!BhYwm&%?Xk-=j[%fz ^Uc~j? Cz|mjSWJ⣯$qBm9_o!})8 _ a7e5g;U{7Aο>״z6 @y*#V2K\Bo;j8] +/L-wvI)<zOϕվ`?Ӧ42doGbB]vcbmhM*}.ڛyy;qp%drGҒ߇a0wQnшefys觾aª].VfN+aK ̚r<[~F2d}O" IDAT SY^ kWQQ`{BW{wxp^X48d,$]iY8O~]|.hcX#dݶ>w.jsUhl1ڑik,]Q0oqǫѷ2" ۱>8i{Ĩ/..6 p2OwV}sL?Zػ?MU,4 ԘG ]- 0ݙ!`g%٣w: CiׅrS1:̎NK'C)}#v ']P#F u';W NZPPMiD!^%^tЂVꢇaJ3̩{MAK/\S ] u) QWms5t7j_`S,Fr ۱6tp݋JpkmrQ/yU!>{q~M߬Z^wĎsx}jkwe6Pkcas瑦 [PY)udGcW1ɱe8C|"pg^*bÎj tt{G!!lw:/-anoؚm=[3O}Smnf`i:I|/jÝ'99kwc 'i}.tiwpͿWlB};>.A`b ^>\i*;zgWNNm]/!`e8u >ԥIjZSDho:~t}(9|r}Jdy8.Lǰ'pnqy0!euٯ*v?BW!n=|ڪ姪]zhl*AoB}ۍЫ%doqO9@{_YШJRmѠ]J_}ܸ}Ui=r}͚hxMnOk H5(@TTX Gl׸B-]-Sh+z~(uowl.t[*J(AL@ET~/_m<]|5Ӛ^G. ^+skn]:~=7T*i%%<&Em #``GTc ޔFުP׃/MuC2ҕ u؁F"&+? խ25]mukn3%8tvWZ_nd7+6}fd2}6,~cDD{քLɚo s>wZuh<10f{TSԩ݃ *xw%r]~!] q> |-[T$(BSsZcoD}E_juD\ck@d}X};%73XvFDD+wF$W%% 魔N((Oc9   $CU{vZWJrcRDEJi^ lX>W;F$O(f-f͝kՍ֞xF<'*erՎ-""CaB¨aH"SFzQm^Y+A?B+W|P&Be wT$ryN6^ 0U'zpyՔiF"/S[vf Iyڡ!S?%)XHDĕ)G;r쫐j kٟe*kWFT$&Į^Ryo.V[.r?WSy)wT_u5MszE-e~т-wלMZ"g*՜kDe<<]m&NGD#,;RS5*.l1vANT+k/sl8PkGJ9g53\WlM>wΗ껖OKhwrKĈOH+pt_*R4,OO^2뾫$'|RWuUs񈨹gUԯ伶c2~ >;XR1쒫fկ ۜת?-l#Mz_=%ݘJ_=nϭYKsDvvDԦ>1-tctdn*k 6 F[~pC}E]Mb_.kS$TgxfJ[ ~ps\h &fʜ7=k^ 9%Br6LEü]#"GԙG+x=tEL{"_;]b%ٹ͛iovT~L<|A6kڢ_=q1ZK%Ꞁ( ZP}sU3CDc؞.m;3."r:'t9z_RQU5;ȶu1Ύ֍wtk]UXN<"1<"2CWtV_ى笊\1 Gj>ǫ ~pi8}%'߰tꆞ^fۓٚtq73DĶfj;wD-ۯTﴎؑ::$ttY$D]q3٠^GK~o2T{4t,z1ʗWG]=KrY̙^3gz/&˃ku~Eɹ狿fdw>$MFY^p|Ig_j0d%q`1vDRUy &:5|uKN+F4iv9')wN3Q#bd1J zE"@9\L%_^5dn0|Ӡl!ih8Ou.A}0˝OJ>=aasBRW  s/DBz*d_+#u%y0TAcuA7O](i?g/?#bĞOBIn}J{ٟMZ\)E#Yk^#nж5imU:XT–U}> (% Z.E~kQ, +{5i͔fW|쓵L(ӡ=>MFߐ?"tDnn7.H梌"|$Ξ0QO+ά; d7Yj3uSZ]Wx67*4@+{z{0ɢ6/"|e']Z|봷 ?޼yp7OG>1L:+j֤ʧ{g= ޵3[Ww/j=''D q qdjZ>m\h_D1~NS2cܦq]U=̜)*.q&Sy򈈄1Sֿd]%";9|lؕVۮpκ6d||Jiwu w4rm&l)<9Y}D۟̕V]g1]l/ Em}d&7VN_2֏uOk1ԠVr2i2\U׵z_^7WB}qV?f"gyԺœg4XS?Ğ J~ڟtj{5d&M{eք2>&co Kdǟp(s}]WLG>7#ƽc働&HiaӚfԖ!23sw<&[aWZmBlxߪft^ꢋ.=G;Ac }1qK0|ٌ(^Ís_lS._u8D5ݴ01={:s/g48"Xsӗ02o즷ب>*瞂c>8 7V8uvt2ʧT w6 ',X轇Wgfopfr%˹3[ֹܲ'UO&[(Ɔ.~3nS Wz|"dxͦ鐖ؒXp/u&_r/GH r(xĘk̓}7_&H<:>y_n ^dht|co֯5w(e]/ Y'yn;^1KU,3rλȊOxG{_trd+^3/Mi (J巿͍d{'zxi>'ǫBװڪ.jks㵤y G`@/m??=baG[똉*7>c/QnYLDc+>Ea2:t';pxDd&]HD$ ,g04mjԖĞ!vfa>rV(qږ 63ocDMgzNDDN~O{m)""'Rs}eKwөƩDėK*:cװcMMj{*='I_N~o"25nt\xζ'"^\)-g~vv˝C//w.d$sON5KDkp-ĶDS\|'["[O]&Unais]MmR=1!A Cݞ8bnKbgbȎcly?X\F G/ V@qmMԗ@&lS?;ELf7Fؿٴĵ}TttG/˙}珝Yu.bP0'ŶYf08 sy2\Q6鞢p'2\?fx|R{잮Bw5!m 'B;f.S~깥€Xswα+'勝=l=3z GGވXX"{""3<^h";gۉvbpqm PLPt':ƨdDd6F3O'Ԥ͉m<>h3|4g̥:'6L5u۫k|ĴwRY'Y5 B4]@)-񄪙6o;ι/1BanӤsvLFd:w›fv‘MnxzuRoו/] 3daކ3W'8+膹V™Sf5i'o/I=_lC9k1X  x c}{]ۇ4E-&z 96ۺ1BW'۞h""hFǑu1pDӑH>Gb6GTp}?tz;dG/q?ek*zpq-ʮ|#"{B?QMD&?Q\_TU*O69&0y,XvkJNI|? R?)M7*}7Uh~ݧV.enfN.ljZ"S6=yQ:q* aNYǚϩ[LDs]čս#Kޘd6cZۈRFxFv6z~xD\G:, I[2 f"ބv羽`&޶ 2cT*yp2oWȎv3i9KޮBw3!sd.!")U^Gc??")Ƿ8 h{_\]Kva˟?U4i!.EիzD߭Ƕ`%_qxF"yڤ4FF͚@$~̬Lē5&z#0BnۈΦn}~?9쨥YTʲ3>̥=gVF.._͜EoÜjZMfu;Ik}h 2xupED[P_ϕ#}ؽ]l-6!?7/_: +\fԵ< lP @y|8[c_#;\ti?ʬM̭{Qxe}r[t'?7:=P~{ݭġY 3!޴14D2ZŽ+*/;rhgBoԙLߟ~N.7~2!#KyE4gjƋkR :zug]qbv0>g~~Io!g8ߤ9ZjԆ 3'>i$EpϦO8B$D" wӘZ*\!"Hd8~'ڧ/ ~*IZ*.̨L-_s扚k,Y2ϝ={I񶓴}5Q4.cMZ"٪JDD+BϬvHfny7iΊkʴ=k&MԶگ2}Qn&/ZN;ܲk=DW=ljXUf5f{ټ'M95>v!VL9rf}ϵ5d-s0ԣo>^tBODms<<"j;sJa0ڊcf"cKCz_ho.:[$j9qTK$[:O:-*#Exg5[p6>7xFvh[\Yo/i:lSϿl';EWhΫ=o񺕎Tw^?3dє*55+&~_u̷;+#<l(`ƕot8z/]!t/3^pue]9^\tc&7ןBGƳQ3D1QA}rr w w wrrR KamfqAw,GLE5Mz=ͷ MfF5+Ih>?y`U[U) Y2^rуgSdܸgR@殆BNA'q@$ :pyKO[,.<쾦$O@n-+>swB6w^5K x5JB"lŪbuq[ ӊkELҎh?}jU$351*#(37NIYNm΋+3 kuDWʍ[d [f:PuFNVgT[v/76#/Y'RD%+JSR+u$_;ABDZ^Ro xFGH_>$Q~`zYǹǥ$\5)kl&z=$wKnz`I3ɢE0V[)9@Bԭt&1,H_>uA YK*JWZKS,He C}eJKyRdCKZJǑUݮ]Kyyh_uٚU C3s,N $r-|<i%YyEI^5{s$S 9U-U9H,0Jr"Z~go|тwaaDynMaUܑWP*3 $g԰DItWT^~I=[H@d(NT)(.͍$'2qٻJ]UEm\}k͗6g;Ey[{J Č,(;K~k9縼tK"}ZEY1MSwQD(G\W6 z$D)ãsj8 `_Z<&9ځ1a2DP95W67/}UV7"xhxEED"ZQ`\RUq*œZkl*bVz3D$R tn`A.DJP9Cȕr8bkzƫED"&N)(;8F1A.DD2҃kuC;yboM@ڲZApLHj r#]Y=b½]D"yXBqSvD[kiZ!)6ZGќaoW.A ̱n+/y巬,ّu Q%*N~);<Я<D $"p#CD,879%UqYjNBi$uK?e8K8H2\!##kuT̼qQupiuQxH8R%irZ n;,ܛeM=R&Ȍ/u\kT.x U8?G۔HRPBZT 4:P96KjgbL)NJTm(цd8 `,rH&B@/SYjN4L?AEC׎jDi~ jee9*1Yٱe*ԗFdy[-""69:dA\l2KB=;4<`Ǖ9j.0BqH\IW|HW% GW? il¶jK[H:LuuxC,{=۵޺>^VQUjd)[j7[[2.2Rsz:_d@)g*"V^y}5:r trkC%ĸ2OtHei,&'`HoU4u+VfmZj}Z뭫^pep$`e+˶Vf7ߐKԭDD9\Z|09UnښYH% wӰD8)z՚d72HNM-Ey?#ٴ5RڪUg%$Qpt vx0!< %)2ucv^"ܦ2D߻(t KڧKNNxfHc_}爫 ɗ$bma{>IX䱙_p!~IdhwCEoOf~9y&CD.ovMW͉'}Ϳ\j/KSMDd3iIbck/g.9?d?gKR\^Ή\Fۗ+eMW|k!"Is9ү|lt×W<7NMzZ־oW\G>Mݩ>";%> 2#].3/gt_κnc;-ub?% =.t#}3QoI];|k㈈;MDDSqk qݯ4dnW3d;{UޑL%9 (na_O0uK""ðu_喙SI?'`lD )GMbWD`A+0xkrÔAASO)ZWv;V4o@PxtBziR)}*;7@L?)w畖()LHi9ۥ;1duI*Aަ5qŭ%]N˔"""F%3{GLz\IsYš-թÕJ2(,:膱XeddũJR M ʰ5F/^^Q59 aaaJeV"b+RGFE&\5̊S)aaʰ< Kt2}i2)܅\#\3IFDD2ՎMX"*~Te"CDP6Ջ:^5)k6;QLNX8phg0NTWT0B8v/sU&Y/MDžv]elޜZ%cN&C_PiiiX,}}}}}}ؠ>]666666vvv666}}}f@sqg'Wbb?[v|iWѥ~t/!/qʼn.n* wAESWΝ:֛w__[OT5M_fq2;Ɇu8ѳn Z՝׿7O.}nmG~veM_VNNa! wӣ~ɪuW{kTZj^&\C-]9S^64oZtmXl+kbo[hÇWun_JyZ",<3Ꮶ>wrKVWN~8$M]:=Quz]y3+~ڥ>ˈN[#amE2{&&?:O1w=%ߵxs_A492ώ$q6"]~M+mku.%%k^>:_ز}_ˇߟijX 1ܢE#qׯ]v54 øpA >pB9w.*V1xJ~؇z]Oc% .QԵOdicP,AO ]PӺ戮%~|y7<|"3'/'go!9g\K/ԍ_ͯ6N{x<:=^_O 7MwlqE gĿLho˭s7m$ .ZoWׯ__rr♆+W2K߭m_=ŪyѮP ]WM\h] ƶ(kW:}ea͗ri˷-."B'\70ҿ|iP_\Ϫ/o;)<_edEFrYoܸZkkA sg~V2|/.!"E6קtn+Чy]55/5Qk[:%k ڧgx+/]B4uq􌎎ϭ[kxrbG^Yv ]N:7'?w>(_&V۲]l^FWN_Vm?w:WslΎ__0ѥgGիuKߍnZAOqϭu#|&>:˞sptv$űӗin{e};_]3uTWH-^mƕuz+w)|;GիmپV.h1Moڹ;뗞G~q,f/8) C]½'>ruiٷ>U'"jQxR׉aYm鸆DDSNDWG{E/ӧ.Ggً>Wi۶]Ԍ+wQm]> +g9\[?|n6FltN؛vOal ].g{M 9Jd(h WD2eȐ#~1C_cb6~5^ԉr6!NDDF)*cFTJIΪ.AA9J)UQ5ZڼcPVm__|< S1>hN1b& ]F)SJ-WA5#%e ͜.'Sqqi)}I鶹pH"Qr4$ U"bsE=x =m d{:{4/hB҉X~G|_ϑ4CE;xhrɡHN|&H*q3<3 y{<2I-y)/j+Us.՝`FaRl`8P!u5]>\j>R1 gijTi wӋudq)N8Y":9Me4!`8剈51fxA=m- ڳhn}N MkLn<5%d>q/b9a:&}#[V;5Kf[v:C1lŰJJtνnVꌅ <˜i0:qrΘe6Kݱj?ۄ]m,ʏ'(eNhΦ.e !5wf,>ƻ)aGt0^Q/&\[o.fOܟxKT^){w"3Kˁ7 ⊜Sqg)+Tq$+˹Ѯy{ͤ*gj9ªHfKMhcm77Ry[ C-$b\4&Rj]$25{fT{7n|Uԝ8qO^ tj""5[ e(=6aZO M/7TFbr3rDۦ6Z8s\e3Bqg6y+l&\*Ǻ[yֺnR@cl]Ks͆H)5SV€sٸ("N*edWm(ހEɄCCbpL"g>Zžb99byWi,K[[ elYnNgD+.1/Zč7_~ڵk׮iƅ ~cGcmBnwBjm߳!wwwYcWw[W5!aY\m1'Gd(.&u4漵UhM6]]H*tr[.أ3 cq}V""'6췾=U̴f`2D>X7 Dx#Vu;Yڻۢѭ.Y$[l,{+Wv}ݭYnFDg9W9\LeĺÎ4>E]X.sǯ^\֑'C-)c4[K|҅O#C.x\r yaj,0@xro|c/y)%B>^6+_]]OD|?yDzg^Kh򣟾.|unM۷]&zNm៬m (g^?Zy;}4q+m.615O9:|FJT9&qSa(96=uVŶu۾#6/!+c?>|t嫴^\Q?~v{+ϼ!kg׽~爦ϟx.u3-vpәoe?[hΏ|4MׯsG=߰;vnO\Z]lgǖ5t"'Z>=ww~cU#cDYZ}׮WG:):"|gc+y?dSSºuhZѩDK7nzqժW6gN^hsS4eNTW^ 7Gș牖mZ|G#}/+7}cV^ϝV.+kWzqn}\9$<{Vűԛӳő?x,*i'CP^'CbFgbl)S-U\9O_hD6]=?ID{;y^ǽgNRѥDT~߽%)u"sV6,m /ON♬W0]է&I"Rw֡^""nRt&[>x0M;?8xˮM+xk9ʮ8!rx<9;ꡪ7絲z/~V8OTm_|җwfj{Ύn(tK4հl1~L IDAT]z])[@tUXMuKN_8IiZth1P߰m펙[/m+]–I=UmGKFĊ663. md2Rlssh-tEEL-ܬIYK uεAX dgՓ܇\;ODŐ//DDĄ<<qDl1| ٜ+2qxoQܛ((A޶>gmݜ a}|<cv{)H7ۖCJ$DA1Xnwqħg8|pرᏩ-o%׭$G&/}T8VhyhuI=zJ׭ zۺM?E]WGousϯ]c;٣g7fR=16G%[^Qyr"udT'/MuI|U."cl`"x~C/edy5'Wk,7lys$(RyIUz 1+E-{eqǃM}K/ڵ,4n}<'I${v-`u)jIy)K.I'"֨5;jlJR57Zhmz*ҧtGLG 웋vɮ#L6%6E4d}9I2;xVeY.S\ظ㻯hg΍3gVo^6<ˉlԶԯڼUpO\^o[kpw~cRuGs/عK9ұw^^[o Z;^~nz.rW69MLnY20!Jd(#,'F,{L̯.eU#b-E}D5k*4Wp6!rc󍀍JAf7E+*Qgg716Wʖ`#"tiRQ%bm.6o: UD2dIaS+U"R XW<t#"ePt=yxϿw{MG7-|VmmkhnU{ꯆ )[׷[|6ZJ##ǿ;aB GCc44BB7U\E'&5&Pew)5bY|RfesJ[<4޼Yۿ*%#xFihfcgjnXؙ<ŲDdhĹB!"2Mb  gkfZ߈*z]NH/elI#1 .+p\JWpJ wHBgI/&\pk%Cc E5#2td9>`Xn^Qإ/pL2qp 9bidZm;,l<фf06sv--W5;92ʩ>}Wf0`cr|n`]ulA%zMc龂),hD20uc~xKPlJAxaս+d ]7X, %w${|Kq|@>-;q͖ "2l,U "CIknqjفNDU)#ւ@:3jmjm-,* Ϝ @J)e2DvZ=Yޞ&ke".ẕUIic=^竜7l[Aε;߈{Ǣ~A,>H'u5FUںks.Ѯ\b3KDls[?q_ ,\h:7n\~ڵk׮]3M0 .x0Xh܅yC.xRπѲ >56'41~v.xa=CϜ-+ Ow wZr:nfo0\k6 f hBd@|r6* n-ADz>#,鵦%~%V/ޡ.w <J.*n?Zh!E]Nn˩N[ɲvs{L.}xjF"g ٓW_A#_C.'^mgIFW¸+`\Z垎*tuIU"}´zO8j~oGѓבg1JD6^$*=hDt9:::|EIvdU" {ٶ" zS2'W}cR_cGpَ{r89j+!%Il3}e]O,5,4JE#l.1޻9b ƓPéژcX^%"R3H: !*D˩p@ gDsʕrjw<>Ծ5"d(h WD2oz9+ќ2K^JpVRBYb2$xJԙg+ 846\l[uGK,/xxNs,.7ОF/!"йPU˙ܪ #Zl1JVn +9աLC3y%3B"n5.>d$g$P>}L[L[crn ^Wԙ+.~/Ό#C-`B((g+W1u۸T֫bo :9"y%WcJDltb,1bG`8wx0dX4eƥYXXGPtBJEf Muv8Gt5Ei]$|Cruqqr >!̍(Ɏ9Rjw98*P;Q{`QsXiwr΅XW<t#"ePt5U%ZLg*6c172c&6h?4Y"J9ۄ&br2͙PcC%K6, vy[HcAq@3U 6JD+sascF2tst7z/mN--62<導<<8 G2A47L(:q61F cBͱB&L^gLh`Y"";5Kf[v:T\E'&5&He6vKd#C!f25Z{ޱd3w49DD͜+˕MG<UX818jgʹ˜|/kkO{9"BDz)HfK19>n&;hG M6o`o-\5޺W fUPΒiDl-,ljGb_hb"Xf#1d'\k!G7 x%CC׉M㈪is&HX8n&L5My_`Ls&XS9A,Ǩxhȳou 66.''w}ΕoH)9I9'Pu%5FW$+̍B5;hJRUN#'}G/&\pk%Ccdc(@WDXZ{=P4b,)fMM]C=dgnre乙 `.I={ k:Gd4^dKJ~ifGWw TG+3F9uIDX&P'X "b9+SY_+%;oN$%"S&nFMjQҚ#{jq\*v2޺oᲿu_CHqhe^fVJ&& g4 ޶HUҸNԒySNV,%"9ࡎBm"t<ĉxb|tijiht{[lDdj#1=TihL LO+t"۴+_ >>ʳUv+jutltV"CJMh,JFz1BynPCR Ʒ 9[sJP2yB6HFuIfa;`Q_Vvi蓂q-g|pX ;5ҝv4!gF.Z-f˜kO-uʈf9j͍Fg z2(Qz5oKuDdV-~Ѧ9lKˌ m1/gdx`_T_ 9|ߐn!S P$Yo 5ıV/Zč7_~ڵk׮iƅ 8WPʶ?FXŽTY4lU-D$vwV:>sݶƻX{Ht7 X\mqHZ»=T*7={wGcmVoɬE-6*tr[LL]ݱ{MvA*IDXXg7wEV_w̸{xio֗hoLd 9R̎M;sAFG{-0h'-x]\aqYչEٝt _F/l1g;nvw3Q0D7O &G%b!&q& ,;,؇@wkdq.sݹ`jլ$@i\:S^-FA'bm]q;Kd㝥K=f*W[we)Z/pc$rǰ!jb>ݑ e`2=bYO{Jm9NzKhV%"];o+ؑ2C'G3!y 񡏽[C;\KDT!zᔬQJ^h TھLogFZ! ^>We]AV ^! ʼn&γ+ᴊf1CMs2$Bk 4 >sZ6ZҨţhR6lOߑs$ׂ^S+xN*i)$zJD mz1y)+M6kcHWrxt[3AmrZ6PK gܚ42nTdžJ% `ˬojxkn9وyTyT']u"CP&`b~ƭ{ˮ=^nCƜ`j >n.GeƜ0g4'LcI׉fq,Npp+˜i̫MMusv354Vx3=!q} RRDDԱM>`!KbB7lb|%WBF"^ɹAtYrI&e rr-muSӎÃ펶d{4RYB$J W&''-ZGƍׯ_vڵkL4 … >w_#LG C.@@..O/g!n!NJC(F1^=zS 5@QJNbqtdfVR`\Zt_V1dN#븰n2UE k;<cX~&Ux/x^8%DJZGZymk2 ZBYUn|fW5ZUcWL/2@ l %m _L٠[lx2j~8U҉r2eV/H䢭 ^!0S t*n dCJb{Q NV䃻#t7T8 ~ARnmj1}**r<8g$tXDfeHvt\|>SѬJT퍍 l4)/ XR4^&noVjYFRHOQF CD,-L+ DjKo^KZ': ˘F(؛l,HOg17#DɕKpғ(T1ɶ7 jt.ݢeӲAz1ўf`2Б,;1[HM 6fKPSgNK]uImF MtMOE\Fyk_ɼbU!trD, JcC%KYxnޥN3-xQ ^Y"4vibgƥNDpqD4܄ Ã'K^')3 l0c%"bji( V"\ S ^cd΀ޗnM64*tqD5W*TڦWʖ`#"tiR7S]O8mz[dTR_+[4*$Ȝ0yM788n&w -]"}L8[1f$Ҹfrڻf,CXfX-ZFqF PR@/Wщq 617_隦/cNpĽ;qmMJ \cVfhĹBzHŦ7dTkuZc\\0']GfhODs C3o;{&#>k ge*i%sR]}u[2eݬwB)Ӹ`^ы?Zɐ#'wg4 F8]~!"b8 wm>n2~f4d26\a Li@։Phdb|u 66,jZrlAw e8PْADae @x޸w<zv_wF,N~(!iymD@%w]ZKRxDD'ii pkq5ۗ&#_" Oq&m~-& 6lV/(g;*i$-j '<%Cv +bg ;Q㼱d*B v%),UVqX/?c|s )hrrrѢEx$nܸqk׮]v4M0.\x ~joy|No2>t) fDWjܕb<G%a>Ά.%% 9ls5s#xxٺ[EŎVo Z% 2J1?V+7|UpnAf˳ ʩH)R&" n*d /dCxݷf9oV;\?;VS hOmR2B ÈBDוz)n5W|zt9uza nhOOja5ufn!SoGĄDexu@$3BKv[޺R 5knhOsW7F9nW#O~}rAr瘚*r2UH%9_}J}?Օ.\O0s=5{[o;1j$IuZN|WGGsqg1vv.':,)20`5JDh! qᔯȪ{; MGOTٸW uiOi3Tidw(-|KZ6񣣣'h_ 2JPLnjL*-'GP4q3y@Wd ӑ2C'G3!l,$ w9/NFڞ?Z H˵bR9uZE"JxƀOX$]Ֆ~DtYs]v ]8wXvq7+˛,V;#jA;aܻ.K>'ޯ{Fk'w Z7m_n2.JjIBμj<\ۦɔD"Q %T/$MM9PMANn6zw‚g̠3nm\Hɹ}U쌈hd-_DH+D")~qs=S:Y.&٩1MG1_Nͨynu:rFly:J9=穤`m Z0ZŀY.wB̶\ Hg*Aح7V\O|mTbT*Ŗl Dl{yZ@rzgR&,mZ'WiAmcu1 7HO;=G %/lFDvsm[^?Ɋ094/$3>>-$ G">9.NvKDސgKiJ|Y`/'#yQ#go^dD.aى157+sHE<_Læ#6ӓη\.D__,|}1$“9GpD$QE$< :;xvB(4LI`{=y1:)6wr҉Jt=cؓH3>u+DRHө*6rQCޯ"7ʋgUZ"$8 ' ˭)F5/Ij`6->"""/{yq^ 'AU<ęKV*4N3~@l/sB7jKr[riksAp}7HD ' Dg""ue?D|J =WyNm5 "AfYpl!6y+DזּI ^Nm+nt!򫦶u?Xڼ9XwǮE"diZ8,%A$!XTD"1}\K$Kk 0FGQ:1RP^ZI FxI$u7JՎI+'(KJR(+zP6jnc.$+O㊲縲I$uGe( ^X3O*""RRQ]^w%$eǝs1kܪ+1/$(>vQR'$ w|:F4зvA캾Ypu \?4 Z*e FFfXK?H}Pedճ=)CKI$3\<׶~pHR/D!]*3?*Obȴ 4sADy'FH>}^(Q'Ōii)fݾ:n[g/ITi(j/e凞z%$kbw&0.trVhKn/ww>-R$9r\"E{i1kQ(ɲ/q{'{v[F D5(zTxcհyGǐ!DDvo7HiZ˫1Nn2QD|tyt>5g@ ]AV"ѨrF$BRiվIŠh8qQ1uLe F^k~aIE 46:2j F±o>|t:uգnSη"䝐HXdC9]Nڑ;x=YHiR wLF4N iے:.vv\peUxNsLjF` ,S KMD$z2#9X̝hjY3zHՏu+*rЗ}*k[KDb vwƈվ{E%ƶhR|3O*fl>#Qe- 1HXL)Z&{̬+u-]PTl""o[ŽƈD%+*"1sc4_of'm:BiFum]3'=K wwNx_$\ #QUd _/ "eח %<..Ϊ6?öJcUN$#Brh/-8/~Kdu*ސ\%"IoK º&}lb[pHVuUdQLҶ+jlh&_֪>EMej.a٧?|w:E+;͌=|BdjfRD.t]CD>ya\1'"5qJ1 'UuJtkL$Ͽw._ݝün[B`zL<K;#" wnZBpk''l [6Na5☳K xrW73cc1U0}xYڵ3ӟcvmuU(j`78_nޯhv=$Z|wwVFֳnf5 zf>VW|v=٠io~,T8i)w}a͜/u)c .7/ͪ25Zw]4%rgèUHj-˝qkύ ʕͭq3W,Wl=N/=%uEЕ{ѨSNc;r++ŢŰ >s[}rTθ v1%r|@q"oBzc0{t,13+U ˁ^`fqYIhD 9b*E"XbSh(+ C2zLDz׳iZDK'y+f5H_[T\~&HZg +IŠ9ǝQOޯ {r3kH$*dgA4iO}>UDDRHS\bO[UƼ&GśqE$ 3uJRl^oDJf1 Q Jq/'?qz#ѯLCQϡQFZeVV-"0g7"TeONE"bqKjTgnK )ҙG6I2ג1M%L%Cfdjasbyg3%4 Uvj57dg/fpSɘWǂoj&S.^iԼQn:{9O#_69͵m9{]=kGwqk-fm#w6zeb,:b!ll[b'JJg{՘F1VҌzfn^S*f!߾Phv:@fYjA;aܻ.KI՟V(Bank\-Vvglˉ޶ɥ[M? zKDJ|YM' 4͒YTR#IP|D_N"9 ^3OW)g$iZDQq:^ļDOV8I]˂;`.Fڱ8 4we}z|f=3UɤI;wW]ٌTabo⋟$N9v:n>ZUSۺ I_K_[mݜef%OܬJ& ݬeCxprg ܞ%6 }iw}9܊xybD ,ʢ {t`\%4 -Q/PJpl:bmgD+Xٵx6 {1SSF$qfD5w8_)KW5cɕz=iRvp|(IĎmfg4"Ɇt5h<ܤӼ7nq_[) u&0?Ɉ~ֺQo)eԼqNmN-[!/v:kWUK- ;rt>iyMw}Nĭ(rDz.g<wP[LFD49Obfl>#Qe#*y`TiQ EQg{wlfWѱ&4r`z+ [bdVxP]wAdjfRD.t]"nɯ {gt9 K wwNf,K ǧƋ$")ލ|LcDѫok__pDq]sEFi5(~+"=-^]&OtUTE gK﮹…HK:oxohNMSBTH"zAVn6yy4}#pz<^VWc j~l@D@U,Gg+QeeST&fKbD97g/ylGDDd~}Dܽ]_)җnn <9Erk Y`"Wc1٧h3|fcB*%煥4x2JD$/n;{{L{ 뺜Gsnw9yqo~^%@@..O3RnOh|k/֑7&u 鲛݌yQ @~-D+2y [Fwri׋L-muODWoJi3Hݮen S) 1Zh[k^//2=r3_k ,涊1Yxr/ڌH88?3ן;l5L8؍յZiZQop"h76] A$krNYݫz-q.U'VXkn0;Sm۳EDxa}%ZZl20佑k!HT jW}8x   xH fwZ^"4?w&ʪ>/ "ˡh@$|Z&<!0^{Ha''IW+LP$ܽ%4IfcWqy#PڵÝDrI&͈ V\\\g Nw"xPxj3E@x}|+W+䵒SH%A67㏪Zw@męٍlio$"u}Ǿ7BT7j&Jִ8^-_hDD6j,|Ƴ8E60 IDATQg{wv<{_2;=,h e*e*}sn:iZG%Jpnod >V-Y +E?(g0w w̘ZxZ͌:ΈȮZe?ԶeT$Arla!xR}S y.\i$O߳͗lbdީvWOԚte7^ z{0 % TH‰o<37-ۋ-el%juKZi`fL1+;eIzskcD_7NNi]Ax'Qpج7wSаݵ^kF0#*RoI2.(Z~"IiصzRk=j6V)߰7͖a¼m! +S0r=-,1"m;euZqbR&,mZa"r[#'qrwmJ"D䏾8ƞMy'D%k|FH4 1M#^"Q/³aF/D"g׷o%H i>wpbCߢD`tqB@xhdZDH"hB=F gsI͔w;$*j$4k2守ۼfT/D_FjbtY&. fB/#;DQv,N]Y'#I d| 4aڒV|8Ll7j^.Sjw^8ϯߌo_+3/ M^""K rK:IrMDWSoKŻ͈^b77mu=8HFGVFh^ IZ"=1IDM#Ωms$tk"f5 |YW=΋9uά$Y%nd"<go.YMqmz[8'"3澸59MsDĭ޳Not"WF=kTbY2z,bzcDOvi㷬f>Ӵӳ>'"ǎ_?ӲYIǵH$KѤ[ HL|5AkǪ%d\nԤ'k|څyP˓1v=T[|rrJVJKjt*bZϮoL*99 FD6%"IM\̦ujjrVl=TqE$"=13zכp}=gGr!/SIYRvwDA@W_JD$il1N gsI͔ԇyWs/W w"PڵÝDrxic՘6yKtU:w+ʞ{Y"pSqBY/EIFڱ8 4we}@$asQ|:TیxX^xfV}RE-EjRz/5W ^am4w~&"f6(_Ga:t3aɬm"F ,KD^e%8 9:sH$"k?p9}%>J\um[|#NUz$EȫF}vDno[',C!"n;g{"*y`Tio\:wffH؜nsߝrي,\JpV&7aН2/vk{cfVf&U茈H)~H=q?W1x+; Kz7dD$V֗W'BH"֝P̬/IL$*\1%-^@\K䊋7./8 ,nkD;~~w w/*_ԕJTUJZE29uT6'Hb@]WOD)Z}y-3RNvISdkf(*"~mYk" Ds!QE嘹u~c { 뺜Gsnw9yqo܎yd"]ד''&D d7r2_ S`ՒRX Mo #]]?//,YLxe6‚}𯳚^_b_N3""k5Ͽϗ<7hٛk^ &Ec{2c 3lN]12>3rwݕh+| u/%E~04 Q_^NJ=~Y]4MZxdωxGXӴӳiZDg}kkp75Y3˩XDz1/8M?OV*,3>'⽼+끹'_>%ͧGFF14MbbkDDzŴ>) wU W&VIad}=Iv$FYT݁+D$Ak25*ȯ.^\Xj'[ffh[qu'bRVZq3:(Y0SLNY{|'ޯ{Fk'w <";0}zp6Jʕ̈́r-{wmcZ*rN%;n`eFA&)ejV۫f# w%j`dD$xeڻFĽZU9i2)D䏦qEd'0g<+Θ+")gmɆD"Q$gwLHT˪ "OUe$#\?96[IE$B:O($lMdsmeKD(M pk;>ccŽZnR͜|ew\I|eCKACICJY$+ :ۥjsHgt]<'?NJK4_Nn@}WO}ܟ]jai͑T=l9ӽk DD;13.x큺~2:٥Q\J,[eL6R3`u8D%2/qqG+E)工fK nMwoښ {6b:lIv*nIzOeQ+7BJH"JIRPlqϽwYMqmz[8'"31 ˡo#"nnω6E w:sz:O"b 3lD_ DUטv..xcWnS8IxnH$Kl1 (Ͼl :UX|T_In:i PHdttR/EXn11r] Xl]pAuWKWJ׶q?e-grVuniWTK^D"Zz7ͅ%IͭN)iZqX ` c=~xnnWu]Gyw9yqob:lUpzQ f3sY+Nj1 '*GJŘ#⽦(}&tٍ[.2 gR8 e3KͮǼhnXM^ ƶ>+0^:!Cù9z9~|W%+_Q..8p-::#"#MۿЫ|5 wl?=8u'}.xޯ9ED?pGgo~~_-oG}dUڗw0/O_G5_$|I|[o͝G}r{kǿ;_ji IDAT[=~xn 뺜GaTʼn?v_?~Ͽ~W>[DtppCGDG|A/_ [DsO?}_Kg??oIm諿_&:;_?kC?9x}t47-w:+_xW?ͯ}w+{???;occOƻ^=` %G]++_W&WwzνMS_Gt4?#ǿ9g,F~W裃?_!z wr_;|[O#xMB/ܒL#@Ե9R~2&ӘUy#Qve*68x[+)J<\n('@n 8 Ir)\ NR$8 AܺT.))KǞlJΥdrw0/zy\ם}|aʖ r1BHY6-kr~K%UK!8'!Rp)\XYs!}KG# c1u á^= s~r]OMHp,h =}ayI3rk7n{tCԜE-Ip)Ukz9q.k"ySznuQuII9.)͝}CK^m|vbfowڃkptk FiBȪwv%6Cd%(n0g2)<Ʋپ^X}OϫjړAockk oox&H{0ۇa/ 6r!p>>Zk<,^^o2$ZA^$Rl빩ΪҬ=NEam=yǓv +6'19a/;f/+1&GHK_>fi3|7n 1l醦G׼OƷ>-IKsRff((Kz]6UQysc9%ul&e?_1"±3G^ٸRγNuۓչH)#]Hs  wz}"ż,"jl5h2BLLG[$˱'V._8—VJYpxaYsL)_ڰg{ GkUͭ7V`ɻ_?͗(K`fUa5U]pœf._=;΅x6MfJ)I2"f82=-1SJ)I]\a%x歺wMykWFqL✸ɡ@X + D"x.'r<^C׮?oIN׺?y]_K$1P].M$$O;yc(/ڳu%777%7%7'%7 čw %^ϭYвc'rb$ ӪPlաHl҂UvF,Ns(BB )\r~%7l{vmx(sWÚd8ʼnhڭV2 Xv4&Gg*JΉBÑDv陋wu=r4q񭐥Y1+V>-o7\ֶO;EҮkD\ nVSrQ0Bmz8w< 655x/BUU;抳jF'+ Vehx$LZf̙O_"R4}$לp| aX Xnq P@Cq,vXfgza6fM|jYeh^O_ [EO, Fcw3I|m_:Ҽ.؀P$"9wdg |CV*x'?h⒳Y/0JJJꯊWkLs;m-'-􂜂~KO>rL!.m~ea{O* krt+`<;lCkW[3 ǚshl`hXrVݶX5i[yXߺ<I )Ij;on8c|iK=OIIIuuuuuŋ+++Ie˗/_|yMM5ЎUz'zq7).e&m3rrӽϮ958w񞁑۟}s햝DF8yf"TdkO-;{F~l֎^es9lÁ` &97xg^~4Iz)fI[-6pvE)mxxOkkkwu(gTB5EKmkidi_PY|/>׾r/.*K|eY]H$=n,f;O=JyIaeyɑ%竬gen4qfKͯ,Ju;{}C)g1b).EKGl:9@8zƢ97~gk#cSj* 2؞Ȃ7Sb)r'}$<3mzWo,[)j$ffgfpT.8,-SM7% }v#==}Μ9'x\p 7_qw O =ɷ8톦(&G8S3w}Ͽqrq5y&֮\#-w?Y^7o4׺_{iSk;^]Zp6W\os׃O|kW )p$%msNIKF".[2S>##c&Sc,L8'}nh@;Ɵ]SΌ%UYD+ bUj-wwՏ^&Jcs+I)%Ca㱛7sGcQ)EyNWUus'0o?xf7på^lٲ)hټygoEk .84 ]K5Uy7"XNf?]eRpk vI N+)5_t/;#rsk 7?v9Y'=݂ o]N&>4S\w8 _zgu֑ ZRJ)G?*?b-M| _;oWMhpQo8IRJ9B @f7>򺲂ʲx,RĹIB&;[; r2fN/\Q` *>'%IR~8> TE <7\}uW^+z%^pr,~r W^Wҽ{Vqp,￳}W܏÷ S+GX7P|R 3[ 92u딂S jvH{47s0@@Dc9ooo.]87;3}ִ=Mm{$2O{ws 3_zyi{|).% ABX%)H!)sfV*/).w6?Ԕcrwu ŷ[{;v?0hw=/}wo{L-(ņȑ K-W2fu Sp\[geeL>oPz51Nfxv1#Q 쌴tv0ݏ zr5sn<r烝=}6l6N$VG%^r|cea&ۃoo|od/~쬌鮾w~uQXpTUEajN|hgg_$T FbB*槲oK/{QxڹӬD߽7=AG1W^VJϦeS?,g5UQSUe'ڋ_|;xRJYQV|M7X5] N$a}NUٹj")*|$EtWhB[yM~Ϊ.oxWk.: U=f?911iŭvvx6EaD?L$UP0=70 Z wί,r:cs`4/՛M;n~k$"b 1Et|=牤9e{yʴ 7?=i@ϖͦ/:aGN&J _韾rInH4rnܰh,OB^(B)I|WV/?:YJ%l%ZmkMi^1 8Hxbjf1P#b?R Rp+$"h?% B_-7`ܙ7̌~?gf*h "hoJYblM\!؄CmV^\ ??7꯷x$%œI))_YdrI-;Ryt17$OBԷvEb "BVEDI/gMl(|ӟdSJIS|XjD<rVk׮}g׮]~/~xY'߯~qg.;~o ɵu; ZȈRIArt*~' #=$I1G&8&iVUUaڗK|NJ}ᵵW]z \.e{i_hD^ڑ@P 0tUUtMi%s*/Y`f4 [߿GWng9m)d"i*,Q?ڦ^W4(aFDiW` VCsX[Ėgo vdK#b2ܽ 40&C!Cwwx+~Wع-7'VȓkֽE'\uEz/\X-fTWHra'%ۭXl;|z[HyW\wŅv}p)) VsJ|/l)(L!"Nts`$k{{OQ>4:B^Gh9G,D2讱RJL4@+XbucTU)*{8S|ߨ}sgLg,q6MQ">PBuO|q=^E Uͱ^"%q!ڌ8+՝q 8{Lk@tE%P,aŪt37=?~{[+efxGsߪɕKFfJ>&>y2:-92Ȍ]+{si19OjO~ʌ16R1@^(gLï~u[7ֿzWyIQyiQYI/77;+31 D ڶ{W[ݱD|?wT3olj)eܻ(.J:mqFW61Qʳrt6Mr>FM׈H&B_%ë'G 5ucJJRuu7?'YBh눩쯰 8|;dWTR|XK`N"nR 1@S__o Ø\~[ZV_}WGH A )*#K //hu\ATBD+˯o霖qfQMQ캦(sA:1ƪ|xmf72s U'޼):iN2_3NDZH q&H1$LfJ)%bH'.6l]1Mrt'-mdLJk9O$x#C+VK.4ljbή=]=}p$i&a8ܜ_FF°[8]})vC'h<8YnR(JI%V1on2{:STbhL p˺]SSsU5KR a\R­ςR9޾݋f~/wWJ|C5{1$3Bke!Ugàqw3FbW2S."bκZ }ZY-X^Fi'̈&Yn`F(i6c pzbFEEESS555!w|`}-ӃA<_ 1 ]pXgmiiP|neddG/ reu/|WQ</y' rED^777cURRv(Hb@BPh3%=h(ZGx<%/@O2h{άYh8ՃOj(=>Rs_8}dUVV~_ TUUY70 CS6vN31chx3ąO~FF 3]S5Ѣ `"\láx***bW}}e0&G;3.G3N=u]phMj=>B}A_%U^3b$)G_ 2\UU~<P)/C{( w w!wgk|rWW 10***(yWH"cII}RJ)$F Sb 3xۉ{(0fbp`$8|1ƻk% 9S/tI)d8uf) m]e RSݪp4McxlG8V|D"R7Ȱ#OWc8@ PXXafB AEU nθ%[&;7% 9ZcL,?50 (zL11MS.ZW_PYQޮ\kJow!WEU:3mt=fsa*( GʺP( +++1&]WCJ]Q |y\tm_{卶7RR~g|@W4(<#2-۰emn躦i{Xˊ'zQCCrrI2-55m9LY=԰߿= Yn˳SD4Aw CɒxEMWB-Oux]X.hajr|.OZ%%Hi5v{S2dO/"e1^֣HăC 3tE!"EeLטMWu]4M5MUUQ) SrSS555!w w|:"4@ ($=cr)!H]UrSQS'Ғ]#ذsi PcZ.+*WK 2;$Dǀ3M8Ul%`$Lz hìoRHfzɜd~~,\R) kvf8vfSUH_g}ᰨ^j555͟?0 r'3lzna4]n64"}gz21Sl:3YtV!9(vŞGtmkpX$^*BƋeD}di ؒA9X@exVQ7e:=^ݮ[u0Ea)1+sݡP(H!r ^D]aN-5+4pRp*X]MKgiXE_XGX{eC2O]"Xz3;;a{vkz8c#ofFVf6pTTTXgdd`Lez j0<^w/#2;,Imk(-6(N'OgΌW{oNP Nğň)KNxJp.snl . Ng|e˖aL> fm4ÑfDX$ CQ +=!u P*4g0޲5thmKIJs"91_Yh;(VUUSUMTUʏ-D.8"sWSSŋP #m'9zcFR:RJ)8Bpi !;%OUcObh E8ąl6;v=f3l6s>y&\]]1@&!bx<.up躾?FĈ$(HU%+s<1ƤR!X,tLMMII4ִ} ˊ ..CĤ2ؾiX,;*'7D!FdzXJRJHQT"X|/byyUW]b$/ǂۄC8*++=rM3 X3gμ[4M#)V.RH!EQUUQU M.vaV5͚1h8BJ!D8nƛo3:*1&9BƘۛZ"jhh@@8ƒwuw7576[sssk{یYlG2朻\`0^OvvBC@0jNnnjJbEp8  uBI`0fn۰d(CCCN3'''2.V+HD"q%<rA.!D<oljjmmSU(ڵk׾`B ~Gcsnd)SO=w(EEE_~̙3c?@vvg9'᧟}vpp.Xp5x<^{u~ZdgQZZp8tڀ#0&)U.$M)%7H8ygwWN>p͵}s#Ъ+lIOOzs)OyyYwWO&SSZ[Wڐ_2 yϭZt:gϙۭyioܸgϜY>nc(gnnnaeTAGD?7|> rIIRR2i٬zzZZqQj޵ i(R\Tt%Xb[tI999==_|?~s󮧞|v_wu,HWUM(3+nxwFwwus9i+WTW&--MSU\&8RzP(4CDD2D~իWwvv&Mߞ;wn#"CgϞ]YQq6oz6l`t]'p$ FKaUU(竨|9CCpp$ֺu{/ EQDfbGp500!@pX%m;v45l_گ.8QX2ŢSUf۰q /}98M[ RT0Q4P4)`L1M9áhuXAlN{aMMqq(UVV|3lGHFFFMMڵk'}..$%q>z(456ƢN9K.Qe׮];vnnlTb?u;::j׭+*,,*(hnnBu%LJ]UgTTdfnomvyf"1)")D.cE^W MxƬUa]p$TVV& >~? f0EOw sfϞ[5G׍ᄏviDĈod2p8.X|Ѣsssׯ_m۶eeeVAUUKJJ~ӟzk׮}wrssze-a0+w)JA~_gϞkؾ]QpUUU}G `ah>@%)O$x#y`7D=[nQuƌ[{RS .袑n, N^1cƌX,Ҳe0͝a撓Nr\###]]044[UUٓVQQa7o_^^^^^.nmmkmi!Ҳ*_^nWŪwa!A?DW^y w wslvaLӌbD"L wy###? 4MpbiFS6x<LuMlDd6)J<3MS7 K$h4LafJ$ώ;7nh%0uMh,0"H3 m}kPknK)cxs'N'TUu32FiRNjTϬH~Kds8Jn'X/.aj0 SNYr`#!z,6i@#҈vmC2,+-u@/ɞqyY8x{y]igѢEsf&9z-]t|WW`@[8 v{^ݮ*r|UVVY`@ NL}w}iB}h]Suϒj":\s +1] ]؃S8z-X]]^زk5uMnf->e[8on#G1"ԑH$zD"4d޸a!}UW1eߵMD Xx[zqˮY!&S.}L%a$R_x89{*VQvza7ݹ-k_ز/Ț.8P !9?"]0uki]ꈨ =W_`3 >eM+3CD+z/.@c$dMͷR`ʸ[\~z W֜puéWK?8 wrC"8 ."F"/lY뱻yڗyڕ㵇x`,|μ8@@#[4rb04t4~jT{߷o7t4^x7𜂊{}DSӴ)S No5i]0SX7+gLot꯮jh wT;'7/(IDATt N DC/:8=O.[^f(LQ> Νs܅kSnx_X?둷oYXoe8:0} [~>0 ^{Osw۵^_[# .rY70pTݪ;5]?tY~[k~N\.[puCJ҂)n]S]Wx{}:o챻Zb]S. ry^ƦM00}$Ñm^7 w@YYuS a;;5Oi_>'Ӿ<`zw+1 @0 #&_GSnѝF .TCVԮ DCgg?SXY3@4v%.r`!L}w^ADSaD$Æ]OC/sgrUQ=~p^7ѦM`XTGD5O8Ow|RSªi|fjwnZT7ՖLgjS4w5+v C;O;8pgZf' w|t5#<}!<`}o$blt5)03g_!ox]?9)N7..}Y50k}WQ#ú5Es?$;f唖|V' wHַg33Kc#OJ. :}v"8^Qٟ&r@$P\(K+ 86Sfg&b 6&j:\t"x3{m@sK(^&Ru=0Xb%&j75TeQt||>d_~>wSٺSӶUW `$4[l\oM*jTv lѲjV}-ѱe9_ };3Uh E.P& DIؓeqbY҆οٲj;FV|} oܽX;')x!o o lnonOB6NzWg4u:]83gdgg3&HBKy^ {zWwe}yNPFmm|x,27[hX8qt>rl9TRCS']L @f)%So#&n%vi~HsĤ.n[ j -7G=rzZDoYh)`:rSHAť>&"u/ls*Nem"kc<( w0_gdD Զe{==70@f3$dppPDb/~P($"sed\7d-l>zCOUvluj:Ң>"y[զ4]L9Ѩh񸈨K` n{ue~N}m|ݓS l9T@Ikؽqa]LՏ]Hv_C@o6o/ ں;ZOMZë4n`08::8 Z N?cS{l+y{M~gvN?c#t7F5 X,EH$(jedsΝ8qZsũ >q>tZD,Y9K*K+V& ]]=}.5oɞ$.rZn܅ix֦/UEBK%+'7k,6eMPȐ {݊Tz5o%'@ n_|10 {7UE+U25 wl.LSPp*.s9DZlЙDD]*//vg@Brrr GxNTY\ZY\PIA?CrJN8]Bk֬a.n!X\\;waJFFƕx"c w0V^}嵢( 3 ,P_}V^`0h4E a[zDĒ>yv9g" LBvvYbh`W?I@Uhί]zYޒBKc.p=P[g܃ʛy:,/f!"pȐstED,Y9Ej`fӄaF@ )x,bh4(bZ)ں;ZOEĤ5TV./p'>gyZ]k] >MM\V5NeXoу"36o/%} wBs*[=q5oOa{jze} ws9^v4)ItOiwl]of`Ikjj"w!K/oX)nx躥UNwf_:_"u!׳SNٵ F۫-sT{^0cbgmmN潷4t[_ml5i ];^na n_ml,.=7-zK&Yr3SqED~Zc07kᫍ"|`\  <Բn*MJwBKO~,";8Af]4uViLV ՛j`݁Ǚ w0']ְngr+Sh[x=6r3΃"{]c*q^]/";B]vIuJSUDںYrޱYlK.U`*8)/ w0"Rk]jU^)@`;>+"KRR9>&[ 46u+^y^v [UBXik6CtwMh4sɛޡ5nYY (.=>fWOeb]cdot6O}BUqΎ 1S0$?z+?;vϑ3oLxYnBq# w0]A)4_2Oͯ<ES.f8u)]hOfӿskyKDdf`|=.k^4i_սysү{.uI]P.OyH$2Bcp|3.f uiv]{KD x㥚{^"sޚo/|wo4|j6[__\.>x=\%C潖}/jyo^.oܽ⨿vك_+7CgXd] ֐:pPDq]Ѽ~'K uh8Cg4B˜Q׎[Ύ u$^y^6s]fHb]QbsӼ}D/;? FBmoGzv бA'ֻ(.=|ކ W@#2x]/v\l<^yp9p_礇"Y醓.]'UirHeJf ]LWť"b: 2?`AFֳ!>*Vڬ4VM,V<ץ^V8>fˡf1 &]=MZTUQ\*"jtq{ +.uSGԩ uwwB]LcY -ԩDXp ]kMZSGi(Bv^] SoR7\*z*Zb9כ.GݖJ`у"yr3G޴Nq% }C-EdSYmnB](ťjC-}@RBך=,v iOM㪢ppM2׎[#u; w0c۹؜qm9^[=a4i iԛ w0ceMi6i ]kZ6߆)BVu /7kglW6^]j*0@ !Sozek뺥UplS\Qꙮ]0hFa x|||<bh4DEZ M}V9W֩zSlz.[Z3]@]47rἈ,/ٽY;v;EĤ5lo(  wmM.H9veyK`^޾AG@74g ] > ZOxIM_"bʩ(*ԛ:Ćyq 9r8G\W@]0c=}.GW0&ty~ ] T\_(x^؇N5odꍅ,^IBp -*. w @r. w @r@%DIENDB`././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1696417899.7531807 murano-16.0.0/doc/source/user/quickstart/0000775000175000017500000000000000000000000020337 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/doc/source/user/quickstart/quickstart.rst0000664000175000017500000000750200000000000023267 0ustar00zuulzuul00000000000000.. _quickstart: ========== QuickStart ========== This is a brief walkthrough to quickly get you familiar with the basic operations you can perform when using the Application catalog directly from the dashboard. For the detailed instructions on how to :ref:`manage your environments ` and :ref:`applications `, please proceed with dedicated sections. Upload an application ~~~~~~~~~~~~~~~~~~~~~ To upload an application to the catalog: #. Log in to the OpenStack dashboard. #. Navigate to :menuselection:`Applications > Manage > Packages`. #. Click on the :guilabel:`Import Package` button: .. image:: ../figures/qs_package_import.png :alt: Packages page :width: 600 px #. In the :guilabel:`Import Package` dialog: * Select ``URL`` from the ``Package Source`` drop-down list; * Specify the URL in the :guilabel:`Package URL` field. Lets upload the Apache HTTP Server package using http://storage.apps.openstack.org/apps/com.example.apache.ApacheHttpServer.zip; * Click :guilabel:`Next` to continue: .. image:: ../figures/qs_package_url.png :width: 600 px :alt: Import Package dialog 1 #. View the package details in the new dialog, click :guilabel:`Next` to continue: .. image:: ../figures/qs_package_details.png :width: 600 px :alt: Import Package dialog 2 #. Select the :guilabel:`Application Servers` from the application category list, click :guilabel:`Create` to import the application package: .. image:: ../figures/qs_app_category.png :width: 600 px :alt: Import Package dialog 3 #. Now your application is available from :menuselection:`Applications > Catalog > Browse` page. Deploy an application ~~~~~~~~~~~~~~~~~~~~~ To add an application to an environment's component list and deploy the environment: #. Log in to the OpenStack dashboard. #. Navigate to :menuselection:`Applications > Catalog > Browse`. #. Click on the :guilabel:`Quick Deploy` button from the required application from the list. Lets deploy Apache HTTP Server, for example: .. image:: ../figures/qs_apps.png :width: 600 px :alt: Applications page #. Check :guilabel:`Assign Floating IP` and click :guilabel:`Next` to proceed: .. image:: ../figures/qs_quick_deploy.png :width: 600 px :alt: Configure Application dialog 1 #. Select the :guilabel:`Instance Image` from the drop-down list and click :guilabel:`Create`: .. image:: ../figures/qs_quick_deploy_2.png :width: 600 px :alt: Configure Application dialog 2 #. Now the Apache HTTP Server application is successfully added to the newly created ``quick-env-4`` environment. Click the :guilabel:`Deploy This Environment` button to start the deployment: .. image:: ../figures/qs_quick_env.png :width: 600 px :alt: Environment "quick-env-1" page It may take some time for the environment to deploy. Wait until the status is changed from ``Deploying`` to ``Ready``. #. Navigate to :menuselection:`Applications > Catalog > Environments` to view the details. Delete an application ~~~~~~~~~~~~~~~~~~~~~ To delete an application that belongs to the environment: #. Log in to the OpenStack dashboard. #. Navigate to :menuselection:`Applications > Catalog > Environments`. #. Click on the name of the environment to view its details, which include components, topology, and deployment history. #. In the :guilabel:`Component List` section, click on the :guilabel:`Delete Component` button next to the application to be deleted. Confirm the deletion. .. note:: If an application that you are deleting has already been deployed, you should redeploy it to apply the recent changes. If the environment has not been deployed with this component, the changes are applied immediately on receiving the confirmation.././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/doc/source/user/user_index.rst0000664000175000017500000000042400000000000021044 0ustar00zuulzuul00000000000000.. _user-guide: User Guide ~~~~~~~~~~ .. toctree:: :maxdepth: 2 userguide/manage_environments userguide/manage_applications userguide/log_in_to_murano_instance userguide/use_cli userguide/deploying_using_cli ../reference/appendix/articles/multi_region ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1696417899.7531807 murano-16.0.0/doc/source/user/userguide/0000775000175000017500000000000000000000000020141 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/doc/source/user/userguide/deploying_using_cli.rst0000664000175000017500000001444400000000000024730 0ustar00zuulzuul00000000000000.. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http//www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. .. _deploying-using-cli: ================================ Deploying environments using CLI ================================ The main tool for deploying murano environments is murano-dashboard. It is designed to be easy-to-use and intuitive. But it is not the only tool you can use to deploy a murano environment, murano CLI client also possesses required functionality for the task. This is an advanced scenario, however, that requires knowledge of :ref:`internal murano workflow `, :ref:`murano object model `, and :ref:`murano environment ` lifecycle. This scenario is suitable for deployments without horizon or deployment automation. .. note:: This is an advanced mechanism and you should use it only when you are confident in what you are doing. Otherwise, it is recommended that you use murano-dashboard. Create an environment ~~~~~~~~~~~~~~~~~~~~~ The following command creates a new murano environment that is ready for configuration. For convenience, this guide refers to environment ID as ``$ENV_ID``. .. code-block:: console $ murano environment-create deployed_from_cli +----------------------------------+-------------------+---------------------+---------------------+ | ID | Name | Created | Updated | +----------------------------------+-------------------+---------------------+---------------------+ | a66e5ea35e9d4da48c2abc37b5a9753a | deployed_from_cli | 2015-10-06T13:50:45 | 2015-10-06T13:50:45 | +----------------------------------+-------------------+---------------------+---------------------+ Create a configuration session ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Murano uses configuration sessions to allow several users to edit and configure the same environment concurrently. Most of environment-related commands require the ``--session-id`` parameter. For convenience, this guide refers to session ID as ``$SESS_ID``. To create a configuration session, use the :command:`murano environment-session-create $ENV_ID` command: .. code-block:: console $ murano environment-session-create $ENV_ID +----------+----------------------------------+ | Property | Value | +----------+----------------------------------+ | id | 5cbe7e561ffc484ebf11aabf83f9f4c6 | +----------+----------------------------------+ Add applications to an environment ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ To manipulate environments object model from CLI, use the :command:`environment-apps-edit` command: .. code-block:: console $ murano environment-apps-edit --session-id $SESS_ID $ENV_ID object_model_patch.json The :file:`object_model_patch.json` contains the ``jsonpatch`` object. This object is applied to the ``/services`` key of the environment in question. Below is an example of the :file:`object_model_patch.json` file content: .. code-block:: json [ { "op": "add", "path": "/-", "value": { "instance": { "availabilityZone": "nova", "name": "xwvupifdxq27t1", "image": "fa578106-b3c1-4c42-8562-4e2e2d2a0a0c", "keyname": "", "flavor": "m1.small", "assignFloatingIp": false, "?": { "type": "io.murano.resources.LinuxMuranoInstance", "id": "===id1===" } }, "name": "ApacheHttpServer", "enablePHP": true, "?": { "type": "com.example.apache.ApacheHttpServer", "id": "===id2===" } } } ] For convenience, the murano client replaces the ``"===id1==="``, ``"===id2==="`` (and so on) strings with UUIDs. This way you can ensure that object IDs inside your object model are unique. To learn more about jsonpatch, consult jsonpatch.com_ and `RFC 6902`_. The :command:`environment-apps-edit` command fully supports jsonpatch. This means that you can alter, add, or remove parts of your applications object model. Verify your object model ~~~~~~~~~~~~~~~~~~~~~~~~ To verify whether your object model is correct, check the environment by running the :command:`environment-show` command with the ``--session-id`` parameter: .. code-block:: console $ murano environment-show $ENV_ID --session-id $SESS_ID --only-apps [ { "instance": { "availabilityZone": "nova", "name": "xwvupifdxq27t1", "assignFloatingIp": false, "keyname": "", "flavor": "m1.small", "image": "fa578106-b3c1-4c42-8562-4e2e2d2a0a0c", "?": { "type": "io.murano.resources.LinuxMuranoInstance", "id": "fc4fe975f5454bab99bb0e309249e2d2" } }, "?": { "status": "pending", "type": "com.example.apache.ApacheHttpServer", "id": "69cdf10d31e64196b4de894e7ea4f1be" }, "enablePHP": true, "name": "ApacheHttpServer" } ] Deploy your environment ~~~~~~~~~~~~~~~~~~~~~~~ To deploy a session ``$SESS_ID`` of your environment, use the :command:`murano environment-deploy` command: .. code-block:: console $ murano environment-deploy $ENV_ID --session-id $SESS_ID You can later use the :command:`murano environment-show` command to track the deployment status. To view the deployed applications of a particular environment, use the :command:`murano environment-show` command with the ``--only-apps`` parameter and specifying the environment ID: .. code-block:: console $ murano environment-show $ENV_ID --only-apps .. _jsonpatch.com: http://jsonpatch.com .. _RFC 6902: http://tools.ietf.org/html/rfc6902 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/doc/source/user/userguide/install_client.rst0000664000175000017500000000715200000000000023704 0ustar00zuulzuul00000000000000.. _install-client: Install and use the murano client ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ The Application Catalog project provides a command-line client, python-muranoclient, which enables you to access the project API. For prerequisites, see `Install the prerequisite software `_. To install the latest murano CLI client, run the following command in your terminal: .. code-block:: console $ pip install python-muranoclient Discover the client version number ---------------------------------- To discover the version number for the python-muranoclient, run the following command: .. code-block:: console $ murano --version To check the latest version, see `Client library for Murano API `_. Upgrade or remove the client ---------------------------- To upgrade or remove the python-muranoclient, use the corresponding commands. **To upgrade the client:** .. code-block:: console $ pip install --upgrade python-muranoclient **To remove the client:** .. code-block:: console $ pip uninstall python-muranoclient Set environment variables ------------------------- To use the murano client, you must set the environment variables. To do this, download and source the OpenStack RC file. For more information, see `Download and source the OpenStack RC file `_. Alternatively, create the ``PROJECT-openrc.sh`` file from scratch. For this, perform the following steps: #. In a text editor, create a file named ``PROJECT-openrc.sh`` containing the following authentication information: .. code-block:: console export OS_USERNAME=user export OS_PASSWORD=password export OS_PROJECT_NAME=tenant export OS_USER_DOMAIN_NAME=Default export OS_PROJECT_DOMAIN_NAME=Default export OS_AUTH_URL=http://auth.example.com:5000/v3 export MURANO_URL=http://murano.example.com:8082/ #. In the terminal, source the ``PROJECT-openrc.sh`` file. For example: .. code-block:: console $ . admin-openrc.sh Once you have configured your authentication parameters, run :command:`murano help` to see a complete list of available commands and arguments. Use :command:`murano help ` to get help on a specific subcommand. .. seealso:: `Set environment variables using the OpenStack RC file `_. Bash completion --------------- To get the latest bash completion script, download `murano.bash_completion `_ from the source repository and add it to your completion scripts. If you are not aware of the completion scripts location, perform the following steps: #. Create a new directory: .. code-block:: console $ mkdir -p ~/.bash_completion/ #. Create a file containing the bash completion script: .. code-block:: console $ curl https://opendev.org/openstack/python-muranoclient/raw/branch/master/tools/murano.bash_completion > ~/.bash_completion/murano.sh #. Add the following code to the ``~/.profile`` file: .. code-block:: bash for file in $HOME/.bash_completion/*.sh; do if [ -f "$file" ]; then . "$file" fi done #. In the current terminal, run: .. code-block:: console $ . ~/.bash_completion/murano.sh ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/doc/source/user/userguide/log_in_to_murano_instance.rst0000664000175000017500000000316300000000000026114 0ustar00zuulzuul00000000000000.. _login-murano-instance: ================================= Log in to murano-spawned instance ================================= After the application is successfully deployed, you may need to log in to the virtual machine with the installed application. All cloud images, including images imported from the `OpenStack Application Catalog `_, have password authentication turned off. Therefore, it is not possible to log in from the dashboard console. SSH is used to reach an instance spawned by murano. Possible default image users are: * *ec2-user* * *ubuntu* or *debian* (depending on the operating system) To log in to murano-spawned instance, perform the following steps: #. Prepare a key pair. To log in through SSH, provide a key pair during the application creation. If you do not have a key pair, click the plus sign to create one directly from the :guilabel:`Configure Application` dialog. .. image:: ../figures/add_key_pair.png :alt: Application creation: key pair :width: 630 px #. After the deployment is completed, find out the instance IP address. For this, see: * Deployment logs .. image:: ../figures/app_logs.png :alt: Application logs: IP is provided :width: 630 px * Detailed instance parameters See the :guilabel:`Instance name` link on the :guilabel:`Component Details` page. .. image:: ../figures/app_details.png :alt: Application details: instance details link :width: 630 px #. To connect to the instance through SSH with the key pair, run: .. code-block:: console $ ssh @ -i ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/doc/source/user/userguide/manage_applications.rst0000664000175000017500000004360300000000000024677 0ustar00zuulzuul00000000000000.. _manage_applications: ===================== Managing applications ===================== In murano, each application, as well as the form of application data entry, is defined by its package. The murano dashboard allows you to import and manage packages as well as search, filter, and add applications from catalog to environments. This section provides detailed instructions on how to import application packages into murano and then add applications to an environment and deploy it. This section also shows you how to find component details, application topology, and deployment logs. Import an application package ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ There are several ways of importing an application package into murano: * :ref:`from a zip file ` * :ref:`from murano applications repository ` * :ref:`from bundles of applications ` .. _ui_zip: From a zip file --------------- Perform the following steps to import an application package from a .zip file: #. In OpenStack dashboard, navigate to :menuselection:`Applications > Manage > Packages`. #. Click the :guilabel:`Import Package` button on the top right of the page. .. image:: ../figures/import_package.png :alt: Packages page: Import Package 1 :width: 630 px #. From the :guilabel:`Package source` drop-down list choose :guilabel:`File`, then click :guilabel:`Browse` to select a .zip file you want to import, and then click :guilabel:`Next`. .. image:: ../figures/browse_zip_file.png :alt: Import Package dialog: zip file :width: 630 px #. At this step, the package is already uploaded. Choose a category from the :guilabel:`Application Category` menu. You can select multiple categories while holding down the :kbd:`Ctrl` key. If necessary, verify and update the information about the package, then click the :guilabel:`Create` button. .. image:: ../figures/add_pkg_info.png :alt: Import Package dialog: Description :width: 630 px .. note:: Though specifying a category is optional, we recommend that you specify at least one. It helps to filter applications in the catalog. | Green messages appear at the top right corner when the application is successfully uploaded. In case of a failure, you will see a red message with the problem description. For more information, please refer to the logs. .. _ui_repo: From a repository ----------------- Perform the following steps to import an application package from murano applications repository: .. note:: To import an application package from a repository, you need to know the full name of the package. For the packages names, go to http://apps.openstack.org/#tab=murano-apps and click on the desired package to see its full name. #. In OpenStack dashboard, navigate to :menuselection:`Applications > Manage > Packages`. #. Click the :guilabel:`Import Package` button on the top right of the page. .. image:: ../figures/import_package.png :alt: Packages page: Import Package 2 :width: 630 px #. From the :guilabel:`Package source` drop-down list, choose :guilabel:`Repository`, enter the package name, and then click :guilabel:`Next`. Note that you may also specify the version of the package. .. image:: ../figures/repository.png :alt: Import Package dialog: Repository :width: 630 px #. At this step, the package is already uploaded. Choose a category from the :guilabel:`Application Category` menu. You can select multiple categories while holding down the :kbd:`Ctrl` key. If necessary, verify and update the information about the package, then click the :guilabel:`Create` button. .. image:: ../figures/add_pkg_info.png :alt: Import Package dialog: Description :width: 630 px .. _ui_bundles: From a bundle of applications ----------------------------- Perform the following steps to import a bundle of applications: .. note:: To import an application bundle from a repository, you need to know the full name of the package bundle. To find it out, go to http://apps.openstack.org/#tab=murano-apps and click on the desired bundle to see its full name. #. In OpenStack dashboard, navigate to :menuselection:`Applications > Manage > Packages`. #. Click the :guilabel:`Import Bundle` button on the top right of the page. .. image:: ../figures/import_bundle.png :alt: Packages page: Import Bundle :width: 630 px #. From the :guilabel:`Package Bundle Source` drop-down list, choose :guilabel:`Repository`, enter the bundle name, and then click :guilabel:`Create`. .. image:: ../figures/bundle_name.png :alt: Import Bundle dialog :width: 630 px Search for an application in the catalog ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ When you have imported many applications and want to quickly find a required one, you can filter them by category, tags and words that the application name or description contains: In OpenStack dashboard, navigate to :menuselection:`Applications > Catalog > Browse`. The page is divided into two sections: * **Recent Activity** shows the most recently imported or deployed applications. * The bottom section contains all the available applications sorted alphabetically. To view all the applications of a specific category, select it from the :guilabel:`App Category` drop-down list: .. image:: ../figures/app_category.png :alt: Applications page: App Category :width: 630 px To filter applications by tags or words from the application name or description, use the rightmost filter: .. image:: ../figures/app_filter.png :alt: Applications page: Filter :width: 630 px .. note:: Tags can be specified during the import of an application package. For example, there is an application that has the word *document-oriented* in description. Let's find it with the filter. The following screenshot shows you the result. .. image:: ../figures/app_filter_example.png :alt: Applications page: example :width: 630 px Delete an application package ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ To delete an application package from the catalog, please perform the following steps: #. In OpenStack dashboard, navigate to :menuselection:`Applications > Manage > Packages`. #. Select a package or multiple packages you want to delete and click :guilabel:`Delete Packages`. .. image:: ../figures/select_packages.png :alt: Packages page: Select packages :width: 630 px #. Confirm the deletion. Add an application to environment ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ After uploading an application, the second step is to add it to an environment. You can do this: * :ref:`from environment details page ` * :ref:`from applications catalog page ` .. _from_env: From environment details page ----------------------------- #. In OpenStack dashboard, navigate to :menuselection:`Applications > Catalog > Environments`. #. Find the environment you want to manage and click :guilabel:`Manage Components`, or simply click on the environment's name. #. Procced with the :ref:`Drop Components here ` field or the :ref:`Add Component ` button. .. _drag_and_drop: **Use of Drop Components here field** #. On the Environment Components page, drag and drop a desired application into the :guilabel:`Drop Components here` field under the :guilabel:`Application Components` section. .. image:: ../figures/add_to_env/drag_and_drop.png :alt: Environment Components page: Drag and drop a component :width: 630 px #. Configure the application. Note that the settings may vary from app to app and are predefined by the application author. When done, click :guilabel:`Next`, then click :guilabel:`Create`. Now the application appears in the :guilabel:`Component List` section on the Environment Components page. .. _add_component: **Use of Add Component button** #. On the Environment Components page, click :guilabel:`Add Component`. .. image:: ../figures/add_to_env/add_component.png :alt: Environment Components page: Add component :width: 630 px #. Find the application you want to add and click :guilabel:`Add to Env`. .. image:: ../figures/add_to_env/add_to_env.png :alt: Applications page: Add to Env :width: 630 px #. Configure the application and click :guilabel:`Next`. Note that the settings may vary from app to app and are predefined by the application author. #. To add more applications, check :guilabel:`Continue application adding`, then click :guilabel:`Create` and repeat the steps above. Otherwise, just click :guilabel:`Create`. .. image:: ../figures/add_to_env/add_more_apps.png :alt: Configure Application dialog: Add more applications :width: 630 px Now the application appears in the :guilabel:`Component List` section on the Environment Components page. .. _from_cat: From applications catalog page ------------------------------ #. In OpenStack dashboard, navigate to :menuselection:`Applications > Catalog > Browse`. #. On the Applications catalog page, use one of the following methods: * `Quick deploy`_. Automatically creates an environment, adds the selected application, and redirects you to the page with the environment components. * `Add to Env`_. Adds an application to an already existing environment. .. _Quick deploy: **Quick Deploy button** #. Find the application you want to add and click :guilabel:`Quick Deploy`. Let's add Apache Tomcat, for example. .. image:: ../figures/add_to_env/quick_deploy.png :alt: Applications page: Quick Deploy :width: 630 px #. Configure the application. Note that the settings may vary from app to app and are predefined by the application author. When done, click :guilabel:`Next`, then click :guilabel:`Create`. In the example below we assign a floating IP address. .. image:: ../figures/add_to_env/configure_app.png :alt: Configure Application dialog :width: 630 px Now the Apache Tomcat application is successfully added to an automatically created ``quick-env-1`` environment. .. image:: ../figures/add_to_env/quick_env.png :alt: Environment Components page: Select packages :width: 630 px .. _Add to Env: **Add to Env button** #. From the :guilabel:`Environment` drop-down list, select the required environment. .. image:: ../figures/add_to_env/add_from_cat.png :alt: Applications page: Select environment :width: 630 px #. Find the application you want to add and click :guilabel:`Add to Env`. Let's add Apache Tomcat, for example. .. image:: ../figures/add_to_env/add_to_env.png :alt: Applications page: Add to Env :width: 630 px #. Configure the application and click :guilabel:`Next`. Note that the settings may vary from app to app and are predefined by the application author. In the example below we assign a floating IP address. .. image:: ../figures/add_to_env/configure_app.png :alt: Configure Application dialog :width: 630 px #. To add more applications, check :guilabel:`Add more applications to the environment`, then click :guilabel:`Create` and repeat the steps above. Otherwise, just click :guilabel:`Create`. .. image:: ../figures/add_to_env/add_more_apps.png :alt: Configure Application dialog: Add more applications :width: 630 px Deploy an environment ~~~~~~~~~~~~~~~~~~~~~ Make sure to add necessary applications to your environment, then deploy it following one of the options below: * Deploy an environment from the Environments page #. In OpenStack dashboard, navigate to :menuselection:`Applications > Catalog > Environments`. #. Select :guilabel:`Deploy Environment` from the Actions drop-down list next to the environment you want to deploy. .. image:: ../figures/deploy_env_2.png :width: 630 px :alt: Environments page It may take some time for the environment to deploy. Wait for the status to change from `Deploying` to `Ready`. You cannot add applications to your environment during deployment. * Deploy an environment from the Environment Components page #. In OpenStack dashboard, navigate to :menuselection:`Applications > Catalog > Environments`. #. Click the name of the environment you want to deploy. .. image:: ../figures/environments.png :width: 630 px :alt: Environments page #. On the Environment Components page, click :guilabel:`Deploy This Environment` to start the deployment. .. image:: ../figures/deploy_env.png :width: 630 px :alt: Environment Components page It may take some time for the environment to deploy. You cannot add applications to your environment during deployment. Wait for the status to change from `Deploying` to `Ready`. You can check the status either on the Environments page or on the Environment Components page. .. _component-details: Browse component details ------------------------ You can browse component details to find the following information about a component: * Name * ID * Type * Instance name (available only after deployment) * Heat orchestration stack name (available only after deployment) To browse a component details, perform the following steps: #. In OpenStack dashboard, navigate to :menuselection:`Applications > Catalog > Environments`. #. Click the name of the required environment. #. In the :guilabel:`Component List` section, click the name of the required component. .. image:: ../figures/component-details.png :width: 630 px :alt: Components details The links redirect to corresponding horizon pages with the detailed information on instance and heat stack. .. _application-topology: Application topology -------------------- Once you add an application to your environment, the application topology of this environment becomes available in a separate tab. The topology represents an elastic diagram showing the relationship between a component and the infrastructure it runs on. To view the topology: #. In OpenStack dashboard, navigate to :menuselection:`Applications > Catalog > Environments`. #. Click the name of the necessary environment. #. Click the :guilabel:`Topology` tab. The topology is helpful to visually display complex components, for example Kubernetes. The red icons reflect errors during the deployment while the green ones show success. .. image:: ../figures/topology_kubernetes.png :alt: Topology tab: Deployment failed :width: 630 px The following elements of the topology are virtual machine and an instance of dependent MuranoPL class: +---------------------------------------------+----------------------------+ | Element | Meaning | +=============================================+============================+ | .. image:: ../figures/topology_element_1.png| Virtual machine | +---------------------------------------------+----------------------------+ | .. image:: ../figures/topology_element_2.png| Instance | +---------------------------------------------+----------------------------+ Position your mouse pointer over an element to see its name, ID, and other details. .. image:: ../figures/topology_wordpress.png :alt: Topology tab: Deployment successful :width: 630 px Deployment logs --------------- To get detailed information on a deployment, use: * :ref:`Deployment history `, which contains logs and deployment structure of an environment. * :ref:`Latest deployment log `, which contains information on the latest deployment of an environment. * :ref:`Component logs `, which contain logs on a particular component in an environment. .. _depl-history: **Deployment history** To see the log of a particular deployment, proceed with the steps below: #. In OpenStack dashboard, navigate to :menuselection:`Applications > Catalog > Environments`. #. Click the name of the required environment. #. Click the :guilabel:`Deployment History` tab. #. Find the required deployment and click :guilabel:`Show Details`. #. Click the :guilabel:`Logs` tab to see the logs. .. image:: ../figures/logs.png :alt: Deployment Logs page :width: 630 px .. _latest-log: **Latest deployment log** To see the latest deployment log, proceed with the steps below: #. In OpenStack dashboard, navigate to :menuselection:`Applications > Catalog > Environments`. #. Click the name of the required environment. #. Click the :guilabel:`Latest Deployment Log` tab to see the logs. .. _component-logs: **Component logs** To see the logs of a particular component of an environment, proceed with the steps below: #. In OpenStack dashboard, navigate to :menuselection:`Applications > Catalog > Environments`. #. Click the name of the required environment. #. In the :guilabel:`Component List` section, click the required component. #. Click the :guilabel:`Logs` tab to see the component logs. .. image:: ../figures/env-component-logs.png :alt: Component Logs page :width: 630 px Delete an application ~~~~~~~~~~~~~~~~~~~~~ To delete an application that belongs to the environment: #. In OpenStack dashboard, navigate to :menuselection:`Applications > Catalog > Environments`. #. Click on the name of the environment you want to delete an application from. .. image:: ../figures/environments.png :width: 630 px :alt: Environments page #. In the :guilabel:`Component List` section, click the :guilabel:`Delete Component` button next to the application you want to delete. Then confirm the deletion. .. image:: ../figures/delete_application.png :width: 630 px :alt: Environment Components page .. note:: If the application that you are deleting has already been deployed, you should redeploy the environment to apply the recent changes. If the environment has not been deployed with this component, the changes are applied immediately on receiving the confirmation.././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/doc/source/user/userguide/manage_environments.rst0000664000175000017500000001022700000000000024734 0ustar00zuulzuul00000000000000.. _manage-environments: ===================== Managing environments ===================== An environment is a set of logically connected applications that are grouped together for an easy management. By default, each environment has a single network for all its applications, and the deployment of the environment is defined in a single heat stack. Applications in different environments are always independent from one another. An environment is a single unit of deployment. This means that you deploy not an application but an environment that contains one or multiple applications. Using OpenStack dashboard you can easily perform such actions with an environment as creating, editing, reviewing, deploying, and others. Create an environment ~~~~~~~~~~~~~~~~~~~~~ To create an environment, perform the following steps: #. In OpenStack dashboard, navigate to Applications > Catalog > Environments. #. On the :guilabel:`Environments` page, click the :guilabel:`Add New` button. #. In the :guilabel:`Environment Name` field, enter the name for the new environment. #. From the :guilabel:`Environment Default Network` drop-down list, choose a specific network, if necessary, or leave the default :guilabel:`Create New` option to generate a new network. .. image:: ../figures/env_default_network.png :alt: Create an environment: Environment Default Network :width: 630 px #. Click the rightmost :guilabel:`Create` button. You will be redirected to the page with the environment components. Alternatively, you can create an environment automatically using the :guilabel:`Quick Deploy` button below any application in the Application Catalog. For more information, see: :ref:`Quick deploy `. Edit an environment ~~~~~~~~~~~~~~~~~~~ You can edit the name of an environment. For this, perform the following steps: #. In OpenStack dashboard, navigate to Applications > Catalog > Environments. #. Position your mouse pointer over the environment name and click the appeared pencil icon. #. Edit the name of the environment. #. Click the tick icon to apply the change. Review an environment ~~~~~~~~~~~~~~~~~~~~~ This section provides a general overview of an environment, its structure, possible statuses, and actions. An environment groups applications together. An application that is added to an environment is called a component. To see an environment status, navigate to :menuselection:`Applications > Catalog > Environments`. Environments may have one of the following statuses: * **Ready to configure**. When the environment is new and contains no components. * **Ready to deploy**. When the environment contains a component or multiple components and is ready for deployment. * **Ready**. When the environment has been successfully deployed. * **Deploying**. When the deploying is in progress. * **Deploy FAILURE**. When the deployment finished with errors. * **Deleting**. When deleting of an environment is in progress. * **Delete FAILURE**. You can abandon the environment in this case. Currently, the component status corresponds to the environment status. To review an environment and its components, or reconfigure the environment, click the name of an environment or simply click the rightmost :guilabel:`Manage Components` button. * On the :guilabel:`Components` tab you can: * Add or delete a component from an environment * Send an environment to deploy * Track a component status * Call murano actions of a particular application in a deployed environment: .. figure:: ../figures/murano_actions.png :width: 100% For more information on murano actions, see: :ref:`Murano actions `. * On the :guilabel:`Topology`, :guilabel:`Deployment History`, and :guilabel:`Latest Deployment Log` tabs of the environment page you can view the following: * The application topology of an environment. For more information, see: :ref:`Application topology `. * The log of a particular deployment. For more information, see: :ref:`Deployment history `. * The information on the latest deployment of an environment. For more information, see: :ref:`Latest deployment log `.././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/doc/source/user/userguide/use_cli.rst0000664000175000017500000004572500000000000022333 0ustar00zuulzuul00000000000000.. _use-cli: ========= Using CLI ========= This section provides murano end users with information on how they can use the Application Catalog through the command-line interface (CLI). Using python-muranoclient, the CLI client for murano, you can easily manage your environments, packages, categories, and deploy environments. .. toctree:: :maxdepth: 1 install_client Manage environments ~~~~~~~~~~~~~~~~~~~ An environment is a set of logically connected applications that are grouped together for an easy management. By default, each environment has a single network for all its applications, and the deployment of the environment is defined in a single heat stack. Applications in different environments are always independent from one another. An environment is a single unit of deployment. This means that you deploy not an application but an environment that contains one or multiple applications. Using CLI, you can easily perform such actions with an environment as creating, renaming, editing, viewing, and others. Create an environment --------------------- To create an environment, use the following command specifying the environment name: .. code-block:: console $ murano environment-create Rename an environment --------------------- To rename an environment, use the following command specifying the old name of the environment or its ID and the new name: .. code-block:: console $ murano environment-rename Delete an environment --------------------- To delete an environment, use the following command specifying the environment name or ID: .. code-block:: console $ murano environment-delete List deployments for an environment ----------------------------------- To get a list of deployments for a particular environment, use the following command specifying the environment name or ID: .. code-block:: console $ murano deployment-list List the environments --------------------- To get a list of all existing environments, run: .. code-block:: console $ murano environment-list Show environment object model ----------------------------- To get a complete object model of the environment, run: .. code-block:: console $ murano environment-model-show To get some part of the environment model, run: .. code-block:: console $ murano environment-model-show --path For example: $ murano environment-model-show 534bcf2f2fc244f2b94ad55ff0f24a42 --path /defaultNetworks/environment To get a draft of an object model of environment in pending state, also specify id of the session: .. code-block:: console $ murano environment-model-show --path --session-id Edit environment object model ----------------------------- To edit an object model of the environment, run: .. code-block:: console $ murano environment-model-edit --session-id is the path to the file with the JSON-patch to modify the object model. JSON-patch is a valid JSON that contains a list of changes to be applied to the current object. Each change contains a dictionary with three keys: ``op``, ``path`` and ``value``. ``op`` (operation) can be one of the three values: `add`, `replace` or remove`. Allowed operations for paths: * "" (model root): no operations * "defaultNetworks": "replace" * "defaultNetworks/environment": "replace" * "defaultNetworks/environment/?/id": no operations * "defaultNetworks/flat": "replace" * "name": "replace" * "region": "replace" * "?/type": "replace" * "?/id": no operations For other paths any operation (add, replace or remove) is allowed. Example of JSON-patch: .. code-block:: javascript [{ "op": "replace", "path": "/defaultNetworks/flat", "value": true }] The patch above changes the value of the ``flat`` property of the environment's ``defaultNetworks`` property to `true`. Manage packages ~~~~~~~~~~~~~~~ This section describes how to manage packages using the command line interface. You can easily: * :ref:`import a package ` or :ref:`bundles of packages ` * :ref:`list the existing packages ` * :ref:`display details for a package ` * :ref:`download a package ` * :ref:`delete a package ` * :ref:`create a package ` .. _cli_import: Import a package ---------------- With the :command:`package-import` command you can import packages into murano in several different ways: * :ref:`from a local .zip file ` * :ref:`from murano app repository ` * :ref:`from an http URL ` .. _cli_zip: **From a local .zip file** To import a package from a local .zip file, run: .. code-block:: console $ murano package-import /path/to/PACKAGE.zip where ``PACKAGE`` is the name of the package stored on your computer. For example: .. code-block:: console $ murano package-import /home/downloads/mysql.zip Importing package com.example.databases.MySql +---------------------------------+------+----------------------------+--------------+---------+ | ID | Name | FQN | Author |Is Public| +---------------------------------+------+----------------------------+--------------+---------+ | 83e4038885c248e3a758f8217ff8241f| MySQL| com.example.databases.MySql| Mirantis, Inc| | +---------------------------------+------+----------------------------+--------------+---------+ To make the package available for users from other projects (tenants), use the ``--is-public`` parameter. For example: .. code-block:: console $ murano package-import --is-public mysql.zip .. note:: The :command:`package-import` command supports multiple positional arguments. This means that you can import several packages at once. .. _cli_repo: **From murano app repository** .. |link_location| raw:: html murano applications repository To import a package from murano applications repository, specify the URL of the repository with ``--murano-repo-url`` and a fully qualified package name. For package names, go to |link_location|, and click on the desired package to see its full name. .. note:: You can also specify the URL of the repository with the corresponding MURANO_REPO_URL environment variable. The following example shows how to import the MySQL package from the murano applications repository: .. code-block:: console $ murano --murano-repo-url=http://storage.apps.openstack.org \ package-import com.example.databases.MySql This command supports an optional ``--package-version`` parameter that instructs murano client to download a specified package version. The :command:`package-import` command inspects package requirements specified in the package's manifest under the *Require* section, and attempts to import them from murano repository. The :command:`package-import` command also inspects any image prerequisites mentioned in the :file:`images.lst` file in the package. If there are any image requirements, client would inspect images already present in the image database. Unless image with the specific name is present, client would attempt to download it. .. TODO: Add a ref link to step-by-step (on specifying images and requirements for packages). If any of the packages being installed is already registered in murano, the client asks you what to do with it. You can specify the default action with ``--exists-action``, passing ``s`` - for skip, ``u`` - for update, and ``a`` - for abort. .. _cli_url: **From an URL** To import an application package from an URL, use the following command: .. code-block:: console $ murano package-import http://example.com/path/to/PACKAGE.zip The example below shows how to import a MySQL package from the murano applications repository using the package URL: .. code-block:: console $ murano package-import http://storage.apps.openstack.org/apps/com.example.databases.MySql.zip Inspecting required images Importing package com.example.databases.MySql +----------------------------------+-------+----------------------------+--------------+--------+----------+------------+ | ID | Name | FQN | Author | Active | Is Public| Type | +----------------------------------+-------+----------------------------+--------------+--------+----------+------------+ | 1aa62196595f411399e4e48cc2f6a512 | MySQL | com.example.databases.MySql| Mirantis, Inc| True | | Application| +----------------------------------+-------+----------------------------+--------------+--------+----------+------------+ .. _cli_bundles: Import bundles of packages -------------------------- With the :command:`bundle-import` command you can install packages in several different ways: * :ref:`from a local bundle ` * :ref:`from an URL ` * :ref:`from murano app repository ` When importing bundles, you can set their publicity with ``--is-public``. .. _cli_local_bundle: **From a local bundle** To import a bundle from the a local file system, use the following command: .. code-block:: console $ murano bundle-import /path/to/bundle/BUNDLE_NAME This command imports all the requirements of packages and images. When importing a bundle from a file system, the murano client searches for packages in a directory relative to the bundle location before attempting to download a package from repository. This facilitates cases with no Internet access. The following example shows the import of a monitoring bundle: .. code-block:: console $ murano bundle-import /home/downloads/monitoring.bundle Inspecting required images Importing package com.example.ZabbixServer Importing package com.example.ZabbixAgent +----------------------------------+---------------+--------------------------+---------------+--------+----------+------------+ | ID | Name | FQN | Author | Active | Is Public| Type | +----------------------------------+---------------+--------------------------+---------------+--------+----------+------------+ | fb0b35359e384fe18158ff3ed8f969b5 | Zabbix Agent | com.example.ZabbixAgent | Mirantis, Inc | True | | Application| | 00a77e302a65420c8080dc97cc0f2723 | Zabbix Server | com.example.ZabbixServer | Mirantis, Inc | True | | Application| +----------------------------------+---------------+--------------------------+---------------+--------+----------+------------+ .. note:: The :command:`bundle-import` command supports multiple positional arguments. This means that you can import several bundles at once. .. _cli_bundle_url: **From an URL** To import a bundle from an URL, use the following command: .. code-block:: console $ murano bundle-import http://example.com/path/to/bundle/BUNDLE_NAME Where ``http://example.com/path/to/bundle/BUNDLE_NAME`` is any external http/https URL to load the bundle from. For example: .. code-block:: console $ murano bundle-import http://storage.apps.openstack.org/bundles/monitoring.bundle .. _cli_bundle_repo: **From murano applications repository** To import a bundle from murano applications repository, use the following command, where ``bundle_name`` stands for the bundle name: .. code-block:: console $ murano bundle-import BUNDLE_NAME For example: .. code-block:: console $ murano bundle-import monitoring .. |location| raw:: html murano applications repository .. note:: For bundle names, go to |location|, click the **Format** tab to show bundles first, and then click on the desired bundle to see its name. .. _cli_list: List packages ------------- To list all the existing packages you have, use the :command:`package-list` command. The result will show you the package ID, name, author and if it is public or not. For example: .. code-block:: console $ murano package-list +----------------------------------+--------------------+-------------------------------------+---------------+--------+----------+------------+ | ID | Name | FQN | Author | Active | Is Public| Type | +----------------------------------+--------------------+-------------------------------------+---------------+--------+----------+------------+ | daa46cfd78c74c11bcbe66d3239e546e | Apache HTTP Server | com.example.apache.ApacheHttpServer | Mirantis, Inc | True | | Application| | 5252c9897e864c9f940e08500056f155 | Cloud Foundry | com.example.paas.CloudFoundry | Mirantis, Inc | True | | Application| | 1aa62196595f411399e4e48cc2f6a512 | MySQL | com.example.databases.MySql | Mirantis, Inc | True | | Application| | 11d73cfdc6d7447a910984d95090463b | SQL Library | com.example.databases | Mirantis, Inc | True | | Application| | fb0b35359e384fe18158ff3ed8f969b5 | Zabbix Agent | com.example.ZabbixAgent | Mirantis, Inc | True | | Application| | 00a77e302a65420c8080dc97cc0f2723 | Zabbix Server | com.example.ZabbixServer | Mirantis, Inc | True | | Application| +----------------------------------+--------------------+-------------------------------------+---------------+--------+----------+------------+ .. _cli_display: Show packages ------------- To get full information about a package, use the :command:`package-show` command. For example: .. code-block:: console $ murano package-show 1aa62196595f411399e4e48cc2f6a512 +----------------------+-----------------------------------------------------+ | Property | Value | +----------------------+-----------------------------------------------------+ | categories | | | class_definitions | com.example.databases.MySql | | description | MySql is a relational database management system | | | (RDBMS), and ships with no GUI tools to administer | | | MySQL databases or manage data contained within the | | | databases. | | enabled | True | | fully_qualified_name | com.example.databases.MySql | | id | 1aa62196595f411399e4e48cc2f6a512 | | is_public | False | | name | MySQL | | owner_id | 1ddb2c610d4e4c5dab5185e32554560a | | tags | Database, MySql, SQL, RDBMS | | type | Application | +----------------------+-----------------------------------------------------+ .. _cli_delete: Delete a package ---------------- To delete a package, use the following command: .. code-block:: console $ murano package-delete PACKAGE_ID .. _cli_download: Download a package ------------------ With the following command you can download a .zip archive with a specified package: .. code-block:: console $ murano package-download PACKAGE_ID > FILE.zip You need to specify the package ID and enter the .zip file name under which to save the package. For example: .. code-block:: console $ murano package-download e44a3f526dfb4e08b3c1018c9968d911 > Wordpress.zip .. _cli_create: Create a package ---------------- With the murano client you can create application packages from package source files or directories. The :command:`package-create` command is useful when application package files are spread across several directories. This command has the following required parameters:: -r RESOURCES_DIRECTORY -c CLASSES_DIRECTORY --type TYPE -o PACKAGE_NAME.zip -f FULL_NAME -n DISPLAY_NAME Example: .. code-block:: console $ murano package-create -c Downloads/Folder1/Classes -r Downloads/Folder2/Resources \ -n mysql -f com.example.MySQL -d Package -o MySQL.zip --type Library Application package is available at /home/Downloads/MySQL.zip After this, the package is ready to be imported to the application catalog. The :command:`package-create` command is also useful for autogenerating packages from heat templates. In this case you do not need to manually specify so many parameters. For more information on automatic package composition, please see :ref:`Automatic package composing `. Manage categories ~~~~~~~~~~~~~~~~~ In murano, applications can belong to a category or multiple categories. Administrative users can create and delete a category as well as list available categories and view details for a particular category. Create a category ----------------- To create a category, use the following command specifying the category name: .. code-block:: console $ murano category-create List available categories ------------------------- To get a list of all existing categories, run: .. code-block:: console $ murano category-list Show category details --------------------- To see packages that belong to a particular category, use the following command specifying the category ID: .. code-block:: console $ murano category-show Delete a category ----------------- To delete a category, use the following command specifying the ID of a category or multiple categories to delete: .. code-block:: console $ murano category-delete [ ...] .. note:: Verify that no packages belong to the category to be deleted, otherwise an error appears. For this, use the :command:`murano category-show ` command. Manage environment templates ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ To manage environment templates, use the following commands specifying appropriate values: :command:`murano env-template-create ` Creates an environment template. :command:`murano env-template-clone ` Creates a new template, cloned from an existing template. :command:`murano env-template-create-env ` Creates a new environment from template. :command:`murano env-template-add-app ` Adds an application or multiple applications to the environment template. :command:`murano env-template-del-app ` Deletes an application from the environment template. :command:`murano env-template-list` Lists the environments templates. :command:`murano env-template-show ` Displays environment template details. :command:`murano env-template-update ` Updates an environment template. :command:`murano env-template-delete ` Deletes an environment template. .. seealso:: `Application Catalog service command-line client `_. ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1696417899.6651802 murano-16.0.0/etc/0000775000175000017500000000000000000000000013675 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1696417899.7571807 murano-16.0.0/etc/murano/0000775000175000017500000000000000000000000015176 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/etc/murano/README-murano.conf.txt0000664000175000017500000000020000000000000021107 0ustar00zuulzuul00000000000000To generate the sample murano.conf file, run the following command from the top level of the murano directory: tox -egenconfig ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/etc/murano/logging.conf.sample0000664000175000017500000000313400000000000020754 0ustar00zuulzuul00000000000000[loggers] keys: root,murano,applications [handlers] keys: watchedfile, applications, stderr, stdout, null [formatters] keys: context, default [logger_root] level = WARNING handlers = watchedfile [logger_applications] level = DEBUG handlers = applications qualname = applications [logger_murano] level = INFO handlers = watchedfile qualname = murano [logger_amqp] level = WARNING handlers = stderr qualname = amqp [logger_amqplib] level = WARNING handlers = stderr qualname = amqplib [logger_sqlalchemy] level = WARNING handlers = stderr qualname = sqlalchemy # "level = INFO" logs SQL queries. # "level = DEBUG" logs SQL queries and results. # "level = WARNING" logs neither. (Recommended for production systems.) [logger_eventletwsgi] level = WARNING handlers = stderr qualname = eventlet.wsgi.server [logger_messaging] level = WARNING handlers = stderr qualname = oslo.messaging [handler_null] class = oslo_log.handlers.NullHandler formatter = default args = () [handler_stderr] class = StreamHandler args = (sys.stderr,) formatter = context [handler_stdout] class = StreamHandler args = (sys.stdout,) formatter = context [handler_watchedfile] class: handlers.WatchedFileHandler args: ('murano.log',) formatter: context [handler_applications] class: handlers.WatchedFileHandler args: ('applications.log',) formatter: context [formatter_default] format = %(message)s [formatter_context] class: oslo_log.formatters.ContextFormatter args: (datefmt=datefmt) format: %(asctime)s.%(msecs)03d %(process)d %(levelname)s %(name)s [%(request_id)s %(user)s %(tenant)s] %(instance)s%(message)s datefmt: %Y-%m-%d %H:%M:%S ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/etc/murano/murano-cfapi-paste.ini0000664000175000017500000000215600000000000021376 0ustar00zuulzuul00000000000000[pipeline:cloudfoundry] pipeline = cors http_proxy_to_wsgi request_id ext_context authtoken context cloudfoundryapi [filter:context] paste.filter_factory = murano.api.middleware.context:ContextMiddleware.factory #For more information see Auth-Token Middleware with Username and Password #https://docs.openstack.org/keystone/latest/configuration.html#service-catalog [filter:authtoken] paste.filter_factory = keystonemiddleware.auth_token:filter_factory [app:cloudfoundryapi] paste.app_factory = murano.cfapi.router:API.factory [filter:faultwrap] paste.filter_factory = murano.api.middleware.fault:FaultWrapper.factory # Middleware to set x-openstack-request-id in http response header [filter:request_id] paste.filter_factory = oslo_middleware.request_id:RequestId.factory [filter:ext_context] paste.filter_factory = murano.api.middleware.ext_context:ExternalContextMiddleware.factory [filter:cors] paste.filter_factory = oslo_middleware.cors:filter_factory oslo_config_project = murano [filter:http_proxy_to_wsgi] paste.filter_factory = oslo_middleware.http_proxy_to_wsgi:HTTPProxyToWSGI.factory oslo_config_project = murano ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/etc/murano/murano-paste.ini0000664000175000017500000000241500000000000020314 0ustar00zuulzuul00000000000000[pipeline:murano] pipeline = cors http_proxy_to_wsgi request_id versionnegotiation faultwrap authtoken context rootapp [filter:context] paste.filter_factory = murano.api.middleware.context:ContextMiddleware.factory #For more information see Auth-Token Middleware with Username and Password #https://docs.openstack.org/keystone/latest/configuration.html#service-catalog [filter:authtoken] paste.filter_factory = keystonemiddleware.auth_token:filter_factory [composite:rootapp] use = egg:Paste#urlmap /: apiversions /v1: apiv1app [app:apiversions] paste.app_factory = murano.api.versions:create_resource [app:apiv1app] paste.app_factory = murano.api.v1.router:API.factory [filter:versionnegotiation] paste.filter_factory = murano.api.middleware.version_negotiation:VersionNegotiationFilter.factory [filter:faultwrap] paste.filter_factory = murano.api.middleware.fault:FaultWrapper.factory # Middleware to set x-openstack-request-id in http response header [filter:request_id] paste.filter_factory = oslo_middleware.request_id:RequestId.factory [filter:cors] paste.filter_factory = oslo_middleware.cors:filter_factory oslo_config_project = murano [filter:http_proxy_to_wsgi] paste.filter_factory = oslo_middleware.http_proxy_to_wsgi:HTTPProxyToWSGI.factory oslo_config_project = murano ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/etc/murano/netconfig.yaml.sample0000664000175000017500000000027000000000000021315 0ustar00zuulzuul00000000000000environment: ?: type: io.murano.resources.ExistingNeutronNetwork internalNetworkName: internal # internalSubnetworkName: subnet1 # externalNetworkName: ext_net flat: null ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1696417899.7571807 murano-16.0.0/etc/oslo-config-generator/0000775000175000017500000000000000000000000020100 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/etc/oslo-config-generator/murano-cfapi.conf0000664000175000017500000000023500000000000023330 0ustar00zuulzuul00000000000000[DEFAULT] output_file = etc/murano/murano-cfapi.conf.sample namespace = keystone_authtoken namespace = murano.cfapi namespace = oslo.db namespace = oslo.log ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/etc/oslo-config-generator/murano.conf0000664000175000017500000000044300000000000022251 0ustar00zuulzuul00000000000000[DEFAULT] output_file = etc/murano/murano.conf.sample namespace = keystone_authtoken namespace = murano namespace = oslo.db namespace = oslo.log namespace = oslo.messaging namespace = oslo.middleware.cors namespace = oslo.policy namespace = oslo.service.service namespace = castellan.config ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1696417899.7571807 murano-16.0.0/etc/oslo-policy-generator/0000775000175000017500000000000000000000000020132 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/etc/oslo-policy-generator/murano-policy-generator.conf0000664000175000017500000000011100000000000025554 0ustar00zuulzuul00000000000000[DEFAULT] output_file = etc/murano.policy.yaml.sample namespace = murano ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1696417899.7571807 murano-16.0.0/meta/0000775000175000017500000000000000000000000014050 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/meta/README.rst0000664000175000017500000000067200000000000015544 0ustar00zuulzuul00000000000000=================== Murano Core Classes =================== This folder contains common Murano classes combined to *Core Library*. The content of this folder needs to be zipped and imported into Murano. After that Murano applications can be deployed. To find murano-applications and explore how the common classes are used in Murano Applications, please refer to `Murano Application Repository `_ ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1696417899.7571807 murano-16.0.0/meta/io.murano/0000775000175000017500000000000000000000000015757 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1696417899.7611806 murano-16.0.0/meta/io.murano/Classes/0000775000175000017500000000000000000000000017354 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/meta/io.murano/Classes/Application.yaml0000664000175000017500000000200300000000000022476 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. Namespaces: =: io.murano Name: Application Methods: reportDeployed: Arguments: - title: Contract: $.string() Default: null - unitCount: Contract: $.int() Default: null Body: - $this.find(Environment).instanceNotifier.trackApplication($this, $title, $unitCount) reportDestroyed: Body: - $this.find(Environment).instanceNotifier.untrackApplication($this) deploy:././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/meta/io.murano/Classes/CloudRegion.yaml0000664000175000017500000000315000000000000022451 0ustar00zuulzuul00000000000000Namespaces: res: io.murano.resources sys: io.murano.system =: io.murano Name: CloudRegion Properties: name: Contract: $.string() agentListener: Contract: $.class(sys:AgentListener) Usage: Runtime stack: Contract: $.class(sys:HeatStack) Usage: Runtime defaultNetworks: Contract: environment: $.class(res:Network) flat: $.class(res:Network) securityGroupManager: Contract: $.class(sys:SecurityGroupManager) Usage: Runtime Methods: getConfig: Body: - Return: $._environment.regionConfigs.get( $.name, $._environment.regionConfigs.get('')) .init: Body: - $._environment: $.find(Environment).require() - $generatedStackName: $.getAttr(generatedStackName) - If: $generatedStackName = null Then: - $generatedStackName: list($.name, randomName()).join('-') - $.setAttr(generatedStackName, $generatedStackName) - $this.agentListener: new(sys:AgentListener, $this, name => $generatedStackName) - $stackDescriptionFormat: 'This stack was generated by Murano for environment {0} (ID: {1}) - region {2}' - $this.stack: new(sys:HeatStack, regionName => $.name, name => 'murano-' + $generatedStackName, description => $stackDescriptionFormat.format($._environment.name, id($._environment), $.name)) - sys:GC.subscribeDestruction($this, $this.stack) - $this.securityGroupManager: coalesce($.defaultNetworks.environment, $.defaultNetworks.flat)?. generateSecurityGroupManager() .destroy: Body: - $.stack.delete() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/meta/io.murano/Classes/CloudResource.yaml0000664000175000017500000000146200000000000023021 0ustar00zuulzuul00000000000000Namespaces: =: io.murano sys: io.murano.system Name: CloudResource Properties: regionName: Contract: $.string() Methods: .init: Body: $._region: null getRegion: Meta: 'io.murano.metadata.engine.Synchronize': onThis: false Body: - If: $._region = null Then: - $env: $.find(Environment).require() - $regionName: generate($this, $ != null, $.find(CloudResource)). select($.regionName).where($ != null).first($env.region) - $._region: $.find(CloudRegion) - If: $._region = null or $._region.name != $regionName Then: $._region: $env.regions[$regionName] - If: $._region != null Then: - sys:GC.subscribeDestruction($this, $._region) - Return: $._region ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/meta/io.murano/Classes/Environment.yaml0000664000175000017500000000753100000000000022552 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. Namespaces: =: io.murano res: io.murano.resources sys: io.murano.system Name: Environment Properties: name: Contract: $.string().notNull() applications: Contract: [$.class(Application).owned().notNull()] agentListener: Contract: $.class(sys:AgentListener) Usage: Runtime stack: Contract: $.class(sys:HeatStack) Usage: Runtime instanceNotifier: Contract: $.class(sys:InstanceNotifier) Usage: Runtime defaultNetworks: Contract: environment: $.template(res:Network) flat: $.template(res:Network) securityGroupManager: Contract: $.class(sys:SecurityGroupManager) Usage: Runtime reporter: Contract: $.class(sys:StatusReporter) Usage: Runtime regionConfigs: Contract: $.string(): agentRabbitMq: host: $.string().notNull() port: $.int() or 5672 login: $.string().notNull() password: $.string().notNull() virtual_host: $.string() or '/' ssl: $.bool() or false insecure: $.bool() or false Usage: Config region: Contract: $.string() Usage: InOut homeRegionName: Contract: $.string() Usage: Runtime regions: Contract: $.string(): $.class(CloudRegion) Usage: InOut Methods: .init: Body: - $.homeRegionName: config(home_region) or '' - $._assignRegions() - $.instanceNotifier: new(sys:InstanceNotifier, environment => $this) - $.reporter: new(sys:StatusReporter, environment => $this) - $.regions: $.regions + $.regionConfigs.keys(). where($ and not $this.regions.containsKey($)). select($this._createRegion($)). toDict($.name) - If: not $.regions.containsKey('') Then: - If: $.homeRegionName Then: $.regions['']: $.regions[$.homeRegionName] Else: $.regions['']: $._createRegion('') - $defaultRegion: $.regions[''] - $.stack: $defaultRegion.stack - $.securityGroupManager: $defaultRegion.securityGroupManager _createRegion: Arguments: regionName: Contract: $.string() Body: - $envNet: $.defaultNetworks.environment?.set(regionName => $regionName) - $flatNet: $.defaultNetworks.flat?.set(regionName => $regionName) - Return: new(CloudRegion, $this, name => $regionName, defaultNetworks => { environment => $envNet, flat => $flatNet } ) deploy: Scope: Public Body: - $.applications.pselect($.deploy()) _assignRegions: Body: - If: $.region = null Then: $.region: $.homeRegionName - $defaultRegionConfig: agentRabbitMq: host: config(rabbitmq, host) port: config(rabbitmq, port) login: config(rabbitmq, login) password: config(rabbitmq, password) virtual_host: config(rabbitmq, virtual_host) ssl: config(rabbitmq, ssl) - If: not $.regionConfigs.containsKey('') Then: - $.regionConfigs: $.regionConfigs.set('' => $defaultRegionConfig) - If: $.homeRegionName and not $.regionConfigs.containsKey($.homeRegionName) Then: - $.regionConfigs: $.regionConfigs.set($.homeRegionName => $defaultRegionConfig) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/meta/io.murano/Classes/Exception.yaml0000664000175000017500000000164500000000000022204 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. Namespaces: =: io.murano Name: Exception Properties: name: Contract: $.string() Usage: Runtime message: Contract: $.string() Usage: Runtime stackTrace: Contract: $ Usage: Runtime extra: Contract: {} Usage: Runtime nativeException: Contract: $ Usage: Runtime cause: Contract: $.class(Exception) Usage: Runtime ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/meta/io.murano/Classes/File.yaml0000664000175000017500000000144100000000000021117 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. Namespaces: =: io.murano Name: File Properties: base64Content: Contract: $.string().notNull() Default: '' mimeType: Contract: $.string().notNull() Default: 'application/octet-stream' filename: Contract: $.string() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/meta/io.murano/Classes/Object.yaml0000664000175000017500000000110200000000000021440 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. Name: io.murano.Object ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/meta/io.murano/Classes/Project.yaml0000664000175000017500000000040300000000000021643 0ustar00zuulzuul00000000000000Name: io.murano.Project Properties: id: Contract: $.string().notNull() name: Contract: $.string().notNull() domain: Contract: $.string().notNull() description: Contract: $.string() extra: Contract: $.string().notNull(): $ ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/meta/io.murano/Classes/SharedIp.yaml0000664000175000017500000001105100000000000021735 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. Namespaces: =: io.murano res: io.murano.resources Name: SharedIp Extends: CloudResource Properties: assignFloatingIp: Contract: $.bool().notNull() Default: false virtualIp: Contract: $.string() Usage: Out floatingIpAddress: Contract: $.string() Usage: Out network: Contract: $.class(res:Network) Usage: InOut Methods: initialize: Body: - $._environment: $.find(Environment).require() - $.instances: [] deployNetwork: Body: - If: $.network = null Then: $.network: $.getRegion().defaultNetworks.environment - $.network.deploy() deploy: Body: - If: not $.getAttr(deployed, false) Then: - $region: $.getRegion() - $reporter: $._environment.reporter - $.deployNetwork() - $networkData: $.network.describe() - $aapPortName: format('AllowedAddressPairsPort-{0}', id($)) - $template: resources: $aapPortName: type: 'OS::Neutron::Port' properties: network_id: $networkData.netId replacement_policy: AUTO outputs: $aapPortName+'-virtualIp': value: get_attr: [$aapPortName, fixed_ips, 0, ip_address] description: format('SharedIP Address of SharedIp group {0}', id($)) - If: $networkData.subnetId Then: - $t: resources: $aapPortName: properties: fixed_ips: - subnet_id: $networkData.subnetId - $template: $template.mergeWith($t) - $region.stack.updateTemplate($template) - If: $.assignFloatingIp and $networkData.floatingIpNetId Then: - $extNetId: $networkData.floatingIpNetId - $fipName: format('Shared-Floating-ip-{0}', id($)) - $template: resources: $fipName: type: 'OS::Neutron::FloatingIP' properties: floating_network_id: $extNetId port_id: get_resource: $aapPortName outputs: $fipName + '-val': value: get_attr: [$fipName, floating_ip_address] description: Shared Floating IP assigned - $region.stack.updateTemplate($template) - $reporter.report($this, 'Allocating shared ip address') - $region.stack.push() - $outputs: $region.stack.output() - $.virtualIp: $outputs.get(format('AllowedAddressPairsPort-{0}-virtualIp', id($))) - $.floatingIpAddress: $outputs.get(format('Shared-Floating-ip-{0}-val', id($))) - $reporter.report($this, format('Shared IP allocated at {0}', $.virtualIp)) - If: $.assignFloatingIp Then: - $reporter.report($this, format('Floating shared IP is {0}', $.floatingIpAddress)) - $.setAttr(deployed, true) getSharedIpRef: Body: - $aapPortName: format('AllowedAddressPairsPort-{0}', id($)) - Return: get_attr: [$aapPortName, fixed_ips, 0, ip_address] releaseResources: Body: - $region: $.getRegion() - $template: $region.stack.current() - $template.resources: $template.resources.delete(format('AllowedAddressPairsPort-{0}', id($))) - $template.outputs: $template.outputs.delete(format('AllowedAddressPairsPort-{0}-virtualIp', id($))) - If: $.assignFloatingIp Then: - $template.resources: $template.resources.delete(format('Shared-Floating-ip-{0}', id($))) - $template.outputs: $template.outputs.delete(format('Shared-Floating-ip-{0}-val', id($))) - $region.stack.setTemplate($template) - $region.stack.push() - $.floatingIpAddress: null - $.virtualIp: null ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/meta/io.murano/Classes/SharedIpRange.yaml0000664000175000017500000000504400000000000022717 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. Namespaces: =: io.murano Name: SharedIpRange Extends: SharedIp Properties: cidr: Contract: $.string().notNull() Usage: InOut Methods: initialize: Body: - $._environment: $.find(Environment).require() deploy: Body: - If: not $.getAttr(deployed, false) Then: - $region: $.getRegion() - $reporter: $._environment.reporter - $.deployNetwork() - $networkData: $.network.describe() - $aapSubnetName: format('AllowedAddressPairsSubnet-{0}', id($)) - $template: resources: $aapSubnetName: type: 'OS::Neutron::Subnet' properties: enable_dhcp: false network: $networkData.netId cidr: $.cidr outputs: $aapSubnetName+'-cidr': value: get_attr: [$aapSubnetName, cidr] description: format('Shared IP Range of group {0}', id($)) - $region.stack.updateTemplate($template) - $region.stack.push() - $outputs: $region.stack.output() - $.cidr: $outputs.get(format('AllowedAddressPairsSubnet-{0}-cidr', id($))) - $.virtualIp: $outputs.get(format('AllowedAddressPairsSubnet-{0}-cidr', id($))) - $reporter.report($this, format('Shared IP Range allocated at {0}', $.cidr)) - $.setAttr(deployed, true) getSharedIpRef: Body: - $aapSubnetName: format('AllowedAddressPairsSubnet-{0}', id($)) - Return: get_attr: [$aapSubnetName, cidr] releaseResources: Body: - $region: $.getRegion() - $template: $region.stack.current() - $template.resources: $template.resources.delete(format('AllowedAddressPairsSubnet-{0}', id($))) - $template.outputs: $template.outputs.delete(format('AllowedAddressPairsSubnet-{0}-cidr', id($))) - $region.stack.setTemplate($template) - $region.stack.push() - $.cidr: null././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/meta/io.murano/Classes/StackTrace.yaml0000664000175000017500000000133300000000000022264 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. Namespaces: =: io.murano Name: StackTrace Properties: frames: Contract: - instruction: $.string() location: $ methodName: $ typeName: $ ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/meta/io.murano/Classes/User.yaml0000664000175000017500000000037200000000000021160 0ustar00zuulzuul00000000000000Name: io.murano.User Properties: id: Contract: $.string().notNull() name: Contract: $.string().notNull() domain: Contract: $.string().notNull() email: Contract: $.string() extra: Contract: $.string().notNull(): $ ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1696417899.7611806 murano-16.0.0/meta/io.murano/Classes/configuration/0000775000175000017500000000000000000000000022223 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/meta/io.murano/Classes/configuration/Linux.yaml0000664000175000017500000000546700000000000024222 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. Namespaces: =: io.murano.configuration std: io.murano sys: io.murano.system m: io.murano.metadata.engine Name: Linux Methods: runCommand: Meta: - m:Synchronize: onArgs: agent Usage: Static Arguments: - agent: Contract: $.class(sys:Agent) - command: Contract: $.string().notNull() - helpText: Contract: $.string() Default: null - captureStderr: Contract: $.bool().notNull() Default: true - captureStdout: Contract: $.bool().notNull() Default: true - ignoreErrors: Contract: $.bool().notNull() Default: false - timeout: Contract: $.int() Default: null Body: - $resources: new(sys:Resources) - If: $helpText != null Then: - $planName: $helpText Else: - $planName: format('Execute {0}', $command) - $template: $resources.yaml('RunCommand.template').bind(dict( command => $command, planName => $planName, captureStderr => $captureStderr, captureStdout => $captureStdout, verifyExitcode => not $ignoreErrors )) - Return: $agent.call($template, $resources, $timeout) putFile: Usage: Static Meta: - m:Synchronize: onArgs: agent Arguments: - agent: Contract: $.class(sys:Agent) - fileContent: Contract: $.string().notNull() - path: Contract: $.string().notNull() - helpText: Contract: $.string() Default: null - ignoreErrors: Contract: $.bool().notNull() Default: false - timeout: Contract: $.int() Default: null Body: - $data: base64encode($fileContent) - $resources: new(sys:Resources) - If: $helpText != null Then: - $planName: $helpText Else: - $planName: format('Put to {0}', $path) - $template: $resources.yaml('PutFile.template').bind(dict( path => $path, fileContent => $data, planName => $planName, verifyExitcode => not $ignoreErrors )) - Return: $agent.call($template, $resources, $timeout) ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1696417899.7611806 murano-16.0.0/meta/io.murano/Classes/metadata/0000775000175000017500000000000000000000000021134 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/meta/io.murano/Classes/metadata/Description.yaml0000664000175000017500000000126600000000000024310 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. Namespaces: =: io.murano.metadata Name: Description Usage: Meta Inherited: true Properties: text: Contract: $.string().notNull() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/meta/io.murano/Classes/metadata/HelpText.yaml0000664000175000017500000000126300000000000023557 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. Namespaces: =: io.murano.metadata Name: HelpText Usage: Meta Inherited: true Properties: text: Contract: $.string().notNull() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/meta/io.murano/Classes/metadata/ModelBuilder.yaml0000664000175000017500000000133100000000000024365 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. Namespaces: =: io.murano.metadata Name: ModelBuilder Usage: Meta Applies: Method Inherited: true Properties: enabled: Contract: $.bool().notNull() Default: true././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/meta/io.murano/Classes/metadata/Title.yaml0000664000175000017500000000126000000000000023100 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. Namespaces: =: io.murano.metadata Name: Title Usage: Meta Inherited: true Properties: text: Contract: $.string().notNull() ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1696417899.7611806 murano-16.0.0/meta/io.murano/Classes/metadata/engine/0000775000175000017500000000000000000000000022401 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/meta/io.murano/Classes/metadata/engine/Serialize.yaml0000664000175000017500000000135700000000000025222 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. Namespaces: =: io.murano.metadata.engine Name: Serialize Usage: Meta Cardinality: One Inherited: true Applies: - Property Properties: as: Contract: $.check($ in ['reference', 'copy']) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/meta/io.murano/Classes/metadata/engine/Synchronize.yaml0000664000175000017500000000144600000000000025605 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. Namespaces: =: io.murano.metadata.engine Name: Synchronize Usage: Meta Inherited: true Cardinality: One Applies: [Method] Properties: onThis: Contract: $.bool().notNull() Default: true onArgs: Contract: - $.string().notNull() ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1696417899.7611806 murano-16.0.0/meta/io.murano/Classes/metadata/forms/0000775000175000017500000000000000000000000022262 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/meta/io.murano/Classes/metadata/forms/Hidden.yaml0000664000175000017500000000135100000000000024341 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. Namespaces: =: io.murano.metadata.forms Name: Hidden Usage: Meta Applies: [Property, Argument] Inherited: true Properties: visible: Contract: $.bool().notNull() Default: false ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/meta/io.murano/Classes/metadata/forms/Position.yaml0000664000175000017500000000142400000000000024753 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. Namespaces: =: io.murano.metadata.forms Name: Position Usage: Meta Applies: [Property, Argument] Inherited: true Properties: index: Contract: $.int() Default: null section: Contract: $.string() Default: null ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/meta/io.murano/Classes/metadata/forms/Section.yaml0000664000175000017500000000151300000000000024552 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. Namespaces: =: io.murano.metadata.forms Name: Section Usage: Meta Cardinality: Many Applies: [Type, Method] Inherited: true Properties: name: Contract: $.string().notNull() title: Contract: $.string() Default: $.name index: Contract: $.int() Default: null ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1696417899.7651808 murano-16.0.0/meta/io.murano/Classes/resources/0000775000175000017500000000000000000000000021366 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/meta/io.murano/Classes/resources/CinderVolume.yaml0000664000175000017500000000757600000000000024665 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. Namespaces: std: io.murano =: io.murano.resources sys: io.murano.system Name: CinderVolume Extends: - Volume - MetadataAware Properties: name: Contract: $.string() size: Contract: $.int().notNull().check($ >= 1) availabilityZone: Contract: $.string() readOnly: Contract: $.bool().notNull() Default: false sourceImage: Contract: $.string() sourceVolume: Contract: $.class(Volume) sourceSnapshot: Contract: $.class(CinderVolumeSnapshot) sourceVolumeBackup: Contract: $.class(CinderVolumeBackup) attachments: Contract: [] Usage: Out Methods: buildResourceDefinition: Body: - $properties: size: $.size metadata: $this.getMetadata($this.getRegion()) - If: $.availabilityZone != null Then: $properties.availability_zone: $.availabilityZone - If: $.name != null Then: $properties.name: $.name - If: $.sourceVolumeBackup != null Then: $properties.backup_id: $.sourceVolumeBackup.openstackId - If: $.sourceImage != null Then: $properties.image: $.sourceImage - If: $.sourceSnapshot != null Then: $properties.snapshot_id: $.sourceSnapshot.openstackId - If: $.sourceVolume != null Then: $properties.source_volid: $.sourceVolume.openstackId # Available only since Heat 5.0.0 (Liberty) - If: $.readOnly Then: $properties.read_only: $.readOnly - Return: resources: format('vol-{0}', id($)): type: $this.getResourceType() properties: $properties outputs: format('vol-{0}-id', id($)): value: $.getRef() format('vol-{0}-attachments', id($)): value: get_attr: [$.getResourceName(), attachments_list] deploy: Body: - $region: $.getRegion() - If: $.sourceSnapshot != null Then: $.sourceSnapshot.validate() - If: $.sourceVolumeBackup != null Then: $.sourceVolumeBackup.validate() - If: $.sourceVolume != null Then: $.sourceVolume.deploy() - $snippet: $.buildResourceDefinition() - If: $.getAttr(lastTemplate) != $snippet Then: - $template: $region.stack.current() - $template: $template.mergeWith($snippet, maxLevels => 2) - $region.stack.setTemplate($template) - $region.stack.push() - $outputs: $region.stack.output() - $.openstackId: $outputs.get(format('vol-{0}-id', id($))) - $.setAttr(lastTemplate, $snippet) releaseResources: Body: - If: $.getAttr(lastTemplate) != null Then: - $region: $.getRegion() - $template: $region.stack.current() - $template.resources: $template.resources.delete(format('vol-{0}', id($))) - $template.outputs: $template.outputs.delete(format('vol-{0}-id', id($))) - $region.stack.setTemplate($template) - $region.stack.push() - $.setAttr(lastTemplate, null) - $.openstackId: null - $.attachments: null getRef: Body: Return: get_resource: format('vol-{0}', id($)) getResourceName: Body: Return: format('vol-{0}', id($)) getResourceType: Body: - Return: 'OS::Cinder::Volume' setAttachments: Arguments: - attachments: Contract: [] Body: - $.attachments: $attachments ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/meta/io.murano/Classes/resources/CinderVolumeBackup.yaml0000664000175000017500000000137400000000000026001 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. Namespaces: =: io.murano.resources Name: CinderVolumeBackup Properties: openstackId: Contract: $.string().notNull() Methods: validate: Body: # TODO: add validation that backup does exist././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/meta/io.murano/Classes/resources/CinderVolumeSnapshot.yaml0000664000175000017500000000140000000000000026361 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. Namespaces: =: io.murano.resources Name: CinderVolumeSnapshot Properties: openstackId: Contract: $.string().notNull() Methods: validate: Body: # TODO: add validation that snapshot does exist././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/meta/io.murano/Classes/resources/ConfLangInstance.yaml0000664000175000017500000000321200000000000025424 0ustar00zuulzuul00000000000000Namespaces: =: io.murano.resources sys: io.murano.system std: io.murano Name: ConfLangInstance Extends: - LinuxMuranoInstance Methods: prepareUserData: Body: - $userData: $.generateUserData() - Return: data: $._generateInstanceConfigResources($userData) format: RAW _generateInstanceConfigResources: Arguments: - userData: Contract: $.string().notNull() Body: - $region: $.getRegion() - $cloudInitConf: $.generateCloudConfig() - $bootConfigResourceName: format('boot_config_{0}', $.name) - $bootScriptResourceName: format('boot_script_{0}', $.name) - $userDataResourceName: format('user_data-{0}', $.name) - $template: resources: $bootConfigResourceName: type: 'OS::Heat::CloudConfig' properties: cloud_config: $cloudInitConf $bootScriptResourceName: type: 'OS::Heat::SoftwareConfig' properties: group: ungrouped config: $userData $userDataResourceName: type: 'OS::Heat::MultipartMime' properties: parts: - config: {get_resource: $bootConfigResourceName} - config: {get_resource: $bootScriptResourceName} - $region.stack.updateTemplate($template) - Return: {get_resource: $userDataResourceName} generateCloudConfig: Body: - $cloudConfigData: cast($, LinuxMuranoInstance).generateCloudConfig() - $confLang: sys:Resources.yaml('conflang.conf') - $cloudInitConf: $cloudConfigData.mergeWith($confLang) - Return: $cloudInitConf ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/meta/io.murano/Classes/resources/ExistingCinderVolume.yaml0000664000175000017500000000136600000000000026367 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. Namespaces: =: io.murano.resources Name: ExistingCinderVolume Extends: Volume Properties: openstackId: Contract: $.string().notNull() Methods: getRef: Body: Return: $.openstackId ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/meta/io.murano/Classes/resources/ExistingNeutronNetwork.yaml0000664000175000017500000001341000000000000026770 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. Namespaces: =: io.murano.resources std: io.murano sys: io.murano.system Name: ExistingNeutronNetwork Extends: NeutronNetworkBase Properties: internalNetworkName: Contract: $.string() Default: null Usage: InOut internalSubnetworkName: Contract: $.string() Default: null Usage: InOut externalNetworkName: Contract: $.string() Default: null Usage: InOut Workflow: initialize: Body: - $._networks: null - $._subnetworks: null - $._ports: null - $._internalNetworkId: null - $._internalSubnetworkId: null - $._externalNetworkId: null deploy: Body: - $netExplorer: $._getNetExplorer() - If: $.internalNetworkName = null Then: $.internalNetworkName: $._getNetworks().where( $.get('router:external') = false).first().name - If: $._internalNetworkId = null Then: $._internalNetworkId: $._getNetworks().where( $.name = $this.internalNetworkName or $.id = $this.internalNetworkName).first().id - If: $._internalNetworkId = $.internalNetworkName Then: $._internalNetworkName: $._getNetworks().where( $.id = $this._internalNetworkId).first().name Else: $._internalNetworkName: $.internalNetworkName - If: $.internalSubnetworkName = null Then: $.internalSubnetworkName: $._getSubnetworks().where( $.network_id = $this._internalNetworkId).first().name - If: $._internalSubnetworkId = null Then: # Specify subnetwork id only if the network is owned by the # environment owner tenant (otherwise we may not be allowed to create # a port to that specific subnet) - $net: $this._getNetworks().where($.id = $this._internalNetworkId).first() - If: $net.tenant_id = std:Project.getEnvironmentOwner().id Then: - $._internalSubnetworkId: $._getSubnetworks().where( ($.name = $this.internalSubnetworkName or $.id = $this.internalSubnetworkName) and $.network_id = $this._internalNetworkId).first().id Else: - $._internalSubnetworkId: null - If: $.externalNetworkName = null and $._internalNetworkId != null Then: - $ports: $netExplorer.listPorts() - $routerCandidates: $ports.where( $.network_id = $this._internalNetworkId and $.device_owner = 'network:router_interface'). select($.device_id) - $networkCandidates: $ports.where( $.device_id in $routerCandidates and $.network_id != $this._internalNetworkId). select($.network_id) - $externalNetwork: $._getNetworks().where( $.get('router:external') = true and $.id in $networkCandidates).first(null) - If: $externalNetwork != null Then: - $.externalNetworkName: $externalNetwork.name - $._externalNetworkId: $externalNetwork.id - If: $.externalNetworkName = null Then: $.externalNetworkName: $._getNetworks().where( $.get('router:external') = true).select($.name).first(null) - If: $._externalNetworkId = null and $.externalNetworkName != null Then: $._externalNetworkId: $._getNetworks().where( $.name = $this.externalNetworkName or $.id = $this.externalNetworkName).first().id _getNetworks: Body: - If: not $._networks Then: $._networks: $._getNetExplorer().listNetworks() - Return: $._networks _getSubnetworks: Body: - If: $._subnetworks = null Then: $._subnetworks : $._getNetExplorer().listSubnetworks() - Return: $._subnetworks joinInstance: Arguments: - instance: Contract: $.class(Instance).notNull() - securityGroupName: Contract: $.string() - securityGroups: Contract: [$.string()] - assignFloatingIp: Contract: $.bool().notNull() - sharedIps: Contract: - $.class(std:SharedIp) Body: - $.deploy() - $fipName: null - $floatingIpNetRef: null - If: $assignFloatingIp Then: - $floatingIpNetRef: $._externalNetworkId - $fipName: format('fip-{0}-{1}', id($), $instance.name) - Return: $.joinInstanceToNetwork( instance => $instance, securityGroupName => $securityGroupName, sharedIps => $sharedIps, netRef => $._internalNetworkId, subnetRef => $._internalSubnetworkId, floatingIpResourceName => $fipName, floatingIpNetRef => $floatingIpNetRef, netName => $._internalNetworkName ) describe: Body: - $.deploy() - $subnet: $._getSubnetworks().where( $.network_id = $this._internalNetworkId).first() - Return: provider: Neutron netId: $._internalNetworkId netName: $.internalNetworkName subnetId: $._internalSubnetworkId cidr: $subnet.cidr dns: $subnet.dns_nameservers gateway: $subnet.gateway_ip floatingIpNetId: $._externalNetworkId ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/meta/io.murano/Classes/resources/HeatSWConfigInstance.yaml0000664000175000017500000001212700000000000026223 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. Namespaces: =: io.murano.resources sys: io.murano.system std: io.murano Name: HeatSWConfigInstance Extends: - Instance Methods: initialize: Body: - $.softwareConfigs: [] # configName will be prepended with the instance name # configSection should be a map representing the 'config' # fragment in a StructuredConfig # inputValues should be a map with any required inputs # signalTransport: null (==CFN_SIGNAL), HEAT_SIGNAL, NO_TRANSPORT # # A StructuredConfig and StructuredDeployment will be added # to the Instance addStructuredConfig: Arguments: - configName: Contract: $.string().notNull() - configSection: Contract: {} - inputValues: Contract: {} Default: {} - signalTransport: Contract: $.string() Default: null Body: - $group: Heat::Ungrouped - $.addSoftwareConfig($configName, $configSection, inputValues=>$inputValues, configGroup=>$group, isStructured=>True, signalTransport=>$signalTransport) # Adds a SoftwareConfig and SoftwareDeployment. # configName will be prepended with the instance name # configSection should be of a suitable form (structured config takes maps, # ordinary software config can take a string or a map), # configGroup can be Heat::Ungrouped, script, puppet etc # inputValues should be a map with any inputs required by the Config # signalTransport: null (==CFN_SIGNAL), HEAT_SIGNAL, NO_TRANSPORT addSoftwareConfig: Arguments: - configName: Contract: $.string().notNull() - configSection: # Should be string unless for a structured config Contract: $.notNull() - inputValues: Contract: {} Default: {} - configGroup: Contract: $.string() Default: Heat::Ungrouped - isStructured: Contract: $.bool() Default: False - signalTransport: Contract: $.string() Default: null Body: - $full_config_name: $.name + '-' + $configName - $deployment_name: $full_config_name + '-deployment' - $deployment_stderr: $deployment_name + '-stderr' - $deployment_stdout: $deployment_name + '-stdout' - $injectConfig: $configSection - $configType: OS::Heat::SoftwareConfig - $deploymentType: OS::Heat::SoftwareDeployment - If: $isStructured Then: - $configType: OS::Heat::StructuredConfig - $deploymentType: OS::Heat::StructuredDeployment - $injectConfig['completion-signal']: {get_input: deploy_signal_id} - $fragment: resources: $full_config_name: type: $configType properties: group: $configGroup config: $injectConfig $deployment_name: type: $deploymentType properties: config: { get_resource: $full_config_name } server: { get_resource: $.name } signal_transport: $signalTransport input_values: $inputValues outputs: $deployment_stdout: value: {get_attr: [$deployment_name, deploy_stdout]} $deployment_stderr: value: {get_attr: [$deployment_name, deploy_stderr]} - $.softwareConfigs: $.softwareConfigs + list($fragment) - $.setAttr(scResources, $.getAttr(scResources, []).concat([$full_config_name, $deployment_name])) - $.setAttr(scOutputs, $.getAttr(scOutputs, []).concat([$deployment_stdout, $deployment_stderr])) # Adds to the stack any heat SW config elements prepareStackTemplate: Arguments: instanceTemplate: Contract: {} Body: - For: fragment In: $.softwareConfigs Do: - $instanceTemplate: $instanceTemplate.mergeWith($fragment) - Return: $instanceTemplate prepareUserData: Body: - Return: data: format: SOFTWARE_CONFIG releaseResources: Body: - $region: $.getRegion() - $template: $region.stack.current() - If: $template.get(resources) and $template.get(outputs) Then: - $template.resources: $template.resources.deleteAll($.getAttr(scResources, [])) - $template.outputs: $template.outputs.deleteAll($.getAttr(scOutputs, [])) - $region.stack.setTemplate($template) - super($, $.releaseResources()) - $.setAttr(scResources, []) - $.setAttr(scOutputs, []) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/meta/io.murano/Classes/resources/HeatSWConfigLinuxInstance.yaml0000664000175000017500000000132000000000000027234 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. Namespaces: =: io.murano.resources sys: io.murano.system std: io.murano Name: HeatSWConfigLinuxInstance Extends: - LinuxInstance - HeatSWConfigInstance ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/meta/io.murano/Classes/resources/Instance.yaml0000664000175000017500000003301500000000000024020 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. Namespaces: =: io.murano.resources std: io.murano sys: io.murano.system Name: Instance Extends: - std:CloudResource - MetadataAware Properties: name: Contract: $.string().notNull() flavor: Contract: $.string().notNull() image: Contract: $.string() Default: null keyname: Contract: $.string() Default: null openstackId: Contract: $.string() Usage: Out availabilityZone: Contract: $.string().notNull() Default: nova agent: Contract: $.class(sys:Agent) Usage: Runtime ipAddresses: Contract: [$.string()] Usage: Out networks: Contract: useEnvironmentNetwork: $.bool().notNull() useFlatNetwork: $.bool().notNull() customNetworks: [$.class(Network).notNull()] primaryNetwork: $.class(Network).notOwned() Default: useEnvironmentNetwork: true useFlatNetwork: false customNetworks: [] primaryNetwork: null assignFloatingIp: Contract: $.bool().notNull() Default: false floatingIpAddress: Contract: $.string() Usage: Out securityGroupName: Contract: $.string() Default: null securityGroups: Contract: [$.string()] Default: [] sharedIps: Contract: - $.class(std:SharedIp) Usage: InOut # as it is set in setSharedIps volumes: Contract: $.string().notNull(): $.class(Volume).notNull() blockDevices: Contract: - volume: $.class(Volume).notNull() deviceName: $.string() deviceType: $.string().check($ in list(disk, cdrom, null)) bootIndex: $.int() Default: [] joinedNetworks: Contract: - network: $.class(Network).notNull() ipList: [$.string().notNull()] Usage: Out #policies: anti-affinity, affinity instanceAffinityGroup: Contract: $.class(InstanceAffinityGroup) Methods: .init: Body: - $._environment: $.find(std:Environment).require() - $.agent: new(sys:Agent, host => $) - $.resources: new(sys:Resources) - $.instanceTemplate: {} - $._floatingIpOutputName: null - $.volumes.values().concat($this.blockDevices.volume).select( sys:GC.subscribeDestruction($, $this, _onReferencedResourceDelete)) - If: $.instanceAffinityGroup != null Then: sys:GC.subscribeDestruction( $this.instanceAffinityGroup, $this, _onReferencedResourceDelete) # Called after the Instance template pieces are in place. It # is at this stage alterations to the template should be made prepareStackTemplate: Arguments: instanceTemplate: Contract: {} Body: - Return: $instanceTemplate setSharedIps: Arguments: ips: Contract: - $.class(std:SharedIp) Body: $.sharedIps: $ips beginDeploy: Body: - $.validateBootSource() - $region: $.getRegion() - $securityGroupName: coalesce( $.securityGroupName, $region.securityGroupManager.defaultGroupName ) - $.createDefaultInstanceSecurityGroupRules($securityGroupName) - $.detectPrimaryNetwork() - $.ensureSharedIpsDeployed() - $.ensureNetworksDeployed() - If: $.networks.useEnvironmentNetwork and $region.defaultNetworks.environment!=null Then: $.joinNet($region.defaultNetworks.environment, $securityGroupName, $this.securityGroups) - If: $.networks.useFlatNetwork and $region.defaultNetworks.flat!=null Then: $.joinNet($region.defaultNetworks.flat, $securityGroupName, $this.securityGroups) - $.networks.customNetworks.select($this.joinNet($, $securityGroupName, $this.securityGroups)) - $preparedUserData: $.prepareUserData() - $properties: name: $.name flavor: $.flavor availability_zone: $.availabilityZone user_data: $preparedUserData.data user_data_format: $preparedUserData.format key_name: $.keyname metadata: $this.getMetadata($region) - If: len($.blockDevices) > 0 Then: - $bdmDefinition: $.blockDevices.select($this.buidBlockDeviceDefinition($)) - $properties.block_device_mapping_v2: $bdmDefinition Else: $properties.image: $.image - If: $.instanceAffinityGroup != null Then: - $.instanceAffinityGroup.deploy() - $properties.scheduler_hints: group: $.instanceAffinityGroup.getRef() - $template: resources: $.name: type: $this.getResourceType() properties: $properties outputs: format('{0}-id', $.name): description: format('ID of {0} instance', $.name) value: get_resource: $.name - $.instanceTemplate: $.instanceTemplate.mergeWith($template) - $.volumes.items().select( $.unpack(path, volume) -> $this.attachVolume($path, $volume)) # Any additional template preparation - $.instanceTemplate: $.prepareStackTemplate($.instanceTemplate) - $region.stack.updateTemplate($.instanceTemplate) endDeploy: Body: - $region: $.getRegion() - $region.stack.push() - $outputs: $region.stack.output() - For: blockDevice In: $.blockDevices Do: - $blockDevice.volume.setAttachments($outputs.get(format('{0}-attachments', $blockDevice.volume.getResourceName()))) - $.openstackId: $outputs.get(format('{0}-id', $this.name)) - If: $._floatingIpOutputName != null Then: - $.floatingIpAddress: $outputs.get($._floatingIpOutputName) - If: $.floatingIpAddress != null Then: $.setAttr(fipAssigned, true) - $._environment.instanceNotifier.trackCloudInstance($this) - $.joinedNetworks: $this.joinedNetworks.distinct().select({ network => $.network, ipList => $.network.getInstanceIpList($this)}) - $.ipAddresses: $this.joinedNetworks.selectMany($.ipList).where($ != $this.floatingIpAddress).distinct() - If: $.floatingIpAddress != null Then: - $.ipAddresses: $.ipAddresses.append($.floatingIpAddress) deploy: Body: - $this.beginDeploy() - $this.endDeploy() detectPrimaryNetwork: Body: - $region: $.getRegion() - $._primaryNetwork: null - If: $.networks.primaryNetwork != null Then: - $._primaryNetwork: $.networks.primaryNetwork Else: - If: $.networks.useEnvironmentNetwork and $region.defaultNetworks.environment!=null Then: - $._primaryNetwork: $region.defaultNetworks.environment Else: - If: $.networks.useFlatNetwork and $region.defaultNetworks.flat!=null Then: - $._primaryNetwork: $region.defaultNetworks.flat Else: - If: $.networks.customNetworks!= null Then: - $._primaryNetwork: $.networks.customNetworks.first(null) ensureNetworksDeployed: Body: - $region: $.getRegion() - If: $.networks.useEnvironmentNetwork and $region.defaultNetworks.environment!=null Then: - $region.defaultNetworks.environment.deploy() - If: $.networks.useFlatNetwork and $region.defaultNetworks.flat!=null Then: - $region.defaultNetworks.flat.deploy() - $.networks.customNetworks.pselect($.deploy()) ensureSharedIpsDeployed: Body: - $.sharedIps.pselect($.deploy()) joinNet: Arguments: - net: Contract: $.class(Network).notNull() - securityGroupName: Contract: $.string() - securityGroups: Contract: [$.string()] Body: - $primary: $net = $._primaryNetwork - $assignFip: $primary and $.assignFloatingIp and not $.getAttr(fipAssigned, false) - $sharedIps: [] - For: sharedIp In: $.sharedIps Do: - If: $sharedIp.network = $net Then: - $sharedIps: $sharedIps.append($sharedIp) - $joinResult: $net.joinInstance( instance => $this, securityGroupName => $securityGroupName, securityGroups => $securityGroups, assignFloatingIp => $assignFip, sharedIps => $sharedIps ) - $.setAttr(instanceResources, $.getAttr(instanceResources, []).concat($joinResult.get(instanceResources, []))) - $.setAttr(instanceOutputs, $.getAttr(instanceOutputs, []).concat($joinResult.get(instanceOutputs, []))) - If: $joinResult.template != null Then: - $.instanceTemplate: $.instanceTemplate.mergeWith($joinResult.template) - If: $joinResult.get(portRef) != null Then: - $template: resources: $.name: properties: networks: - port: $joinResult.portRef - $.instanceTemplate: $.instanceTemplate.mergeWith($template) - If: $joinResult.get(secGroupName) != null Then: - $template: resources: $.name: properties: security_groups: - $joinResult.secGroupName - $.instanceTemplate: $.instanceTemplate.mergeWith($template) - $._floatingIpOutputName: coalesce($._floatingIpOutputName, $joinResult.instanceFipOutput) - $this.joinedNetworks: $this.joinedNetworks.append({network => $net}) attachVolume: Arguments: - path: Contract: $.string().notNull() - volume: Contract: $.class(Volume).notNull() Body: - $attachment: $volume.attachTo($this, $path) - $.instanceTemplate: $.instanceTemplate.mergeWith($attachment.template) - $.setAttr(instanceResources, $.getAttr(instanceResources, []).concat($attachment.get(instanceResources, []))) - $.setAttr(instanceOutputs, $.getAttr(instanceOutputs, []).concat($attachment.get(instanceOutputs, []))) buidBlockDeviceDefinition: Arguments: blockDevice: Contract: volume: $.class(Volume).notNull() deviceName: $.string() deviceType: $.string().check($ in list(disk, cdrom, null)) bootIndex: $.int() Body: - $blockDevice.volume.deploy() - Return: device_name: $blockDevice.deviceName volume_id: $blockDevice.volume.getRef() device_type: $blockDevice.deviceType boot_index: $blockDevice.bootIndex beginReleaseResources: Body: - $region: $.getRegion() - $template: $region.stack.current() - If: $template.get(resources) and $template.get(outputs) Then: - $resourcesToDelete: list($.name).concat($.getAttr(instanceResources, [])) - $lenBefore: len($template.resources) - $template.resources: $template.resources.deleteAll($resourcesToDelete) - If: $lenBefore > len($template.resources) Then: - $._environment.instanceNotifier.untrackCloudInstance($this) - $outputsToDelete: list( '{0}-id'.format($.name)).concat($.getAttr(instanceOutputs, [])) - $template.outputs: $template.outputs.deleteAll($outputsToDelete) - $region.stack.setTemplate($template) _onReferencedResourceDelete: Arguments: - resource: Contract: $.class(std:CloudResource).notNull() Body: - If: sys:GC.isDoomed($this) Then: - $.beginReleaseResources() endReleaseResources: Body: - $region: $.getRegion() - $template: $region.stack.current() - If: $template.get(resources) and $template.get(outputs) and not sys:GC.isDoomed($region) Then: - $region.stack.push(async => true) - $.setAttr(instanceResources, []) - $.setAttr(instanceOutputs, []) - $.setAttr(fipAssigned, false) - $.openstackId: null - $.ipAddresses: [] - $.floatingIpAddress: null releaseResources: Body: - $this.beginReleaseResources() - $this.endReleaseResources() validateBootSource: Body: - If: $.image = null and len($.blockDevices) = 0 Then: - $msg: format('Neither image nor bootable volumes is specified for instance {0}', $.name) - Throw: ResourceNotFound Message: $msg - If: $.image != null and len($.blockDevices) > 0 Then: - $msg: 'Both image and list of bootable volumes are specified' - Throw: ResourceConflict Message: $msg destroy: Body: - $.releaseResources() createDefaultInstanceSecurityGroupRules: Arguments: - groupName: Contract: $.string().notNull() prepareUserData: Body: Return: data: null # Valid values are HEAT_CFNTOOLS, RAW and SOFTWARE_CONFIG format: HEAT_CFNTOOLS isDeployed: Body: - $region: $.getRegion() - $template: $region.stack.current() - Return: $template.get(resources, {}).containsKey($.name) getRef: Body: Return: get_resource: $.name getResourceType: Body: - Return: 'OS::Nova::Server' ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/meta/io.murano/Classes/resources/InstanceAffinityGroup.yaml0000664000175000017500000000332200000000000026525 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. Namespaces: std: io.murano =: io.murano.resources Name: InstanceAffinityGroup Extends: std:CloudResource Properties: affinity: Contract: $.bool().notNull() Default: false #affinity: true, anti-affinity: false Methods: deploy: Body: - If: $.affinity Then: - $policies: ['affinity'] Else: - $policies: ['anti-affinity'] - $name: $._getHeatName() - $affinityTemplate: resources: $name: type: 'OS::Nova::ServerGroup' properties: name: $name policies: $policies - $region: $.getRegion() - $region.stack.updateTemplate($affinityTemplate) - $region.stack.push(async => true) .destroy: Body: - $name: $._getHeatName() - $region: $.getRegion() - $template: $region.stack.current() - $template.resources: $template.resources.delete($name) - $region.stack.setTemplate($template) - $region.stack.push(async => true) getRef: Body: - Return: get_resource: $._getHeatName() _getHeatName: Body: - Return: format('NovaServerGroup-{0}', id($this)) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/meta/io.murano/Classes/resources/LinuxInstance.yaml0000664000175000017500000000201300000000000025032 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. Namespaces: =: io.murano.resources std: io.murano Name: LinuxInstance Extends: Instance Methods: createDefaultInstanceSecurityGroupRules: Arguments: - groupName: Contract: $.string().notNull() Body: - $region: $.getRegion() - $rules: - ToPort: 22 IpProtocol: tcp FromPort: 22 External: true - $region.securityGroupManager.addGroupIngress( rules => $rules, groupName => $groupName) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/meta/io.murano/Classes/resources/LinuxMuranoInstance.yaml0000664000175000017500000001034300000000000026221 0ustar00zuulzuul00000000000000Namespaces: =: io.murano.resources sys: io.murano.system std: io.murano Name: LinuxMuranoInstance Extends: - LinuxInstance Methods: prepareUserData: Body: - $userData: $.generateUserData() - Return: data: $._generateInstanceConfigResources($userData) format: RAW _generateInstanceConfigResources: Arguments: - userData: Contract: $.string().notNull() Body: - $region: $.getRegion() - $resources: new(sys:Resources) - $muranoInitConf: $.generateCloudConfig() - $bootScriptResourceName: format('boot_script_{0}', $.name) - $userDataResourceName: format('user_data-{0}', $.name) - $bootConfigResourceName: format('boot_config-{0}', $.name) - $template: resources: $bootConfigResourceName: type: 'OS::Heat::CloudConfig' properties: cloud_config: $muranoInitConf $bootScriptResourceName: type: 'OS::Heat::SoftwareConfig' properties: group: ungrouped config: $userData $userDataResourceName: type: 'OS::Heat::MultipartMime' properties: parts: - config: {get_resource: $bootConfigResourceName} - config: {get_resource: $bootScriptResourceName} - $.setAttr(resourceCloudConfig, [$bootScriptResourceName, $userDataResourceName, $bootConfigResourceName]) - $region.stack.updateTemplate($template) - Return: {get_resource: $userDataResourceName} generateCloudConfig: Body: - $resources: new(sys:Resources) - $muranoInitConf: $resources.yaml('murano-init.conf').bind(dict( instanceHostname => $.name )) - Return: $muranoInitConf generateUserData: Body: - $region: $.getRegion() - $rabbitMqParams: $region.getConfig().agentRabbitMq - $resources: new(sys:Resources) - $configFile: $resources.string('Agent-v2.template') - $initScript: $resources.string('linux-init.sh') - $muranoScript: $resources.string('murano-init.sh') - $muranoAgentConf: $resources.string('murano-agent.conf') - $muranoAgentService: $resources.string('murano-agent.service') - $muranoAgent: $resources.string('murano-agent') - $configReplacements: "%RABBITMQ_HOST%": $rabbitMqParams.host "%RABBITMQ_PORT%": $rabbitMqParams.port "%RABBITMQ_USER%": $rabbitMqParams.login "%RABBITMQ_PASSWORD%": $rabbitMqParams.password "%RABBITMQ_VHOST%": $rabbitMqParams.virtual_host "%RABBITMQ_SSL%": str($rabbitMqParams.ssl).toLower() "%RABBITMQ_INSECURE%": str($rabbitMqParams.insecure).toLower() "%RABBITMQ_INPUT_QUEUE%": $.agent.queueName() "%RESULT_QUEUE%": $region.agentListener.queueName() "%SIGNING_KEY%": $.agent.signingKey('\t') - $scriptReplacements: "%AGENT_CONFIG_BASE64%": base64encode($configFile.replace($configReplacements)) "%INTERNAL_HOSTNAME%": $.name "%MURANO_SERVER_ADDRESS%": coalesce(config(file_server), $rabbitMqParams.host) - If: config(rabbitmq, ca_certs) Then: - $scriptReplacements["%CA_ROOT_CERT_BASE64%"]: base64encode(config(rabbitmq, ca_certs, true)) Else: - $scriptReplacements["%CA_ROOT_CERT_BASE64%"]: '' - $muranoReplacements: "%MURANO_AGENT_CONF%": base64encode($muranoAgentConf) "%MURANO_AGENT_SERVICE%": base64encode($muranoAgentService) "%MURANO_AGENT%": base64encode($muranoAgent) "%PIP_SOURCE%": config(engine, agent_source) - $userData: $muranoScript.replace($muranoReplacements) + $initScript.replace($scriptReplacements) - Return: $userData releaseResources: Body: - $region: $.getRegion() - $template: $region.stack.current() - If: $template.get(resources) and $template.get(outputs) Then: - $resourcesToDelete: $.getAttr(resourceCloudConfig, []) - $template.resources: $template.resources.deleteAll($resourcesToDelete) - $region.stack.setTemplate($template) - super($, $.releaseResources()) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/meta/io.murano/Classes/resources/LinuxUDInstance.yaml0000664000175000017500000000170100000000000025266 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. Namespaces: =: io.murano.resources std: io.murano Name: LinuxUDInstance Extends: - LinuxInstance Methods: initialize: Body: - $.customUserData: null prepareUserData: Body: - Return: data: $.customUserData format: HEAT_CFNTOOLS setCustomUserData: Arguments: - data: Contract: $.notNull() Body: - $.customUserData: $data ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/meta/io.murano/Classes/resources/MetadataAware.yaml0000664000175000017500000000365000000000000024756 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. Namespaces: =: io.murano.resources std: io.murano sys: io.murano.system Name: MetadataAware Properties: checkApplicability: Contract: $.bool().notNull() Default: true Methods: .init: Body: - $this._metadefBrowsers: {} getMetadefBrowser: Arguments: - region: Contract: $.class(std:CloudRegion).notNull() Body: - $browser: $this._metadefBrowsers.get($region.name) - If: $browser = null Then: - $browser: new(sys:MetadefBrowser, $region) - $this._metadefBrowsers[$region.name]: $browser - Return: $browser getMetadata: Arguments: - region: Contract: $.class(std:CloudRegion).notNull() Body: - $thisMeta: metadata($this) or {} - $parentsMeta: {} - $p: $this.find(std:Object) - While: $p != null Do: - $pmeta: metadata($p) or {} - $pmeta: dict($pmeta.items().where(not $[0] in $parentsMeta.keys())) - $parentsMeta: $parentsMeta.set($pmeta) - $p: $p.find(std:Object) - $resourceType: $this.getResourceType() - If: $this.checkApplicability and $resourceType Then: - $browser: $this.getMetadefBrowser($region) - $parentsMeta: dict($parentsMeta.items().where($browser.canBeAppliedTo($[0], $resourceType))) - Return: $parentsMeta.set($thisMeta) getResourceType: ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/meta/io.murano/Classes/resources/Network.yaml0000664000175000017500000000214500000000000023705 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. Namespaces: =: io.murano.resources std: io.murano Name: Network Extends: std:CloudResource Methods: deploy: joinInstance: Arguments: - instance: Contract: $.class(Instance).notNull() - securityGroupName: Contract: $.string() - assignFloatingIp: Contract: $.bool().notNull() - sharedIps: Contract: - $.class(std:SharedIp) generateSecurityGroupManager: describe: getInstanceIpList: Arguments: - instance: Contract: $.class(Instance).notNull() Body:././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/meta/io.murano/Classes/resources/NeutronNetwork.yaml0000664000175000017500000001456600000000000025272 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. Namespaces: =: io.murano.resources std: io.murano sys: io.murano.system Name: NeutronNetwork Extends: NeutronNetworkBase Properties: name: Contract: $.string().notNull() ipVersion: Contract: $.int() Default: 4 externalRouterId: Contract: $.string() Usage: InOut autoUplink: Contract: $.bool().notNull() Default: true autogenerateSubnet: Contract: $.bool().notNull() Default: true openstackId: Contract: $.string() Usage: Out subnetCidr: Contract: $.string() Usage: InOut dnsNameservers: # This property is optional, # since neutron default dns will be used in case of empty Contract: [$.string()] Usage: InOut Methods: deploy: Body: - If: not $.getAttr(deployed, false) Then: - $netExplorer: $._getNetExplorer() - If: len($.dnsNameservers) = 0 Then: - $.dnsNameservers: $netExplorer.getDefaultDns($.ipVersion) - $template: $._createNetwork() - If: $.autoUplink and (not bool($.externalRouterId)) Then: - $.externalRouterId: $netExplorer.getDefaultRouter() - If: $.autogenerateSubnet and (not bool($.subnetCidr)) Then: - $.subnetCidr: $netExplorer.getAvailableCidr($.externalRouterId, id($), $.ipVersion) - $template: $template.mergeWith($._createSubnet()) - If: $.externalRouterId != null Then: - $template: $template.mergeWith($._createRouterInterface()) - $region: $.getRegion() - $region.stack.updateTemplate($template) - $region.stack.push() - $outputs: $region.stack.output() - $.openstackId: $outputs.get(format('{0}-id', $this.name)) - $.setAttr(deployed, true) _createNetwork: Body: - $netName: $._getNetworkName() - $template: resources: $netName: type: 'OS::Neutron::Net' properties: name: $._getHeatName() outputs: format('{0}-id', $.name): description: format('ID of {0} network', $.name) value: get_resource: $netName - Return: $template _createSubnet: Body: - Return: resources: $._getSubnetName(): type: 'OS::Neutron::Subnet' properties: network: { get_resource: $._getNetworkName() } ip_version: $.ipVersion dns_nameservers: $.dnsNameservers cidr: $.subnetCidr _createRouterInterface: Body: - Return: resources: $._getRouterInterfaceName(): type: 'OS::Neutron::RouterInterface' properties: router_id: $.externalRouterId subnet: { get_resource: $._getSubnetName() } joinInstance: Arguments: - instance: Contract: $.class(Instance).notNull() - securityGroupName: Contract: $.string() - securityGroups: Contract: [$.string()] - assignFloatingIp: Contract: $.bool().notNull() - sharedIps: Contract: - $.class(std:SharedIp) Body: - $.deploy() - $netRef: { get_resource: $._getNetworkName() } - $subnetRef: { get_resource: $._getSubnetName() } - $extNetId: null - $fipName: null - If: $assignFloatingIp Then: - $extNetId: $._getExternalNetId() - $fipName: format('fip-{0}-{1}', id($), $instance.name) - $result: $.joinInstanceToNetwork( instance => $instance, securityGroupName => $securityGroupName, securityGroups => $securityGroups, sharedIps => $sharedIps, netRef => $netRef, subnetRef => $subnetRef, floatingIpResourceName => $fipName, floatingIpNetRef => $extNetId, netName => $._getHeatName() ) # (sjmc7) This is a workaround for https://bugs.launchpad.net/heat/+bug/1299259 - If: $externalRouterId != null Then: - $template: resources: $fipName: depends_on: - $._getRouterInterfaceName() - $result.template: $result.template.mergeWith($template) - Return: $result describe: Body: - $.deploy() - $subnet: $._getNetExplorer().listSubnetworks().where( $.network_id = $this.openstackId).first() - Return: provider: Neutron netName: $.name netId: $.openstackId subnetId: $subnet.id cidr: $subnet.cidr dns: $subnet.dns_nameservers gateway: $subnet.gateway_ip floatingIpNetId: $._getExternalNetId() releaseResources: Body: - $region: $.getRegion() - $template: $region.stack.current() - $template.resources: $template.resources.delete($._getHeatName()) - $template.resources: $template.resources.delete($._getSubnetName()) - $template.outputs: $template.outputs.delete(format('{0}-id', $.name)) - If: $.externalRouterId != null Then: $template.resources: $template.resources.delete($._getRouterInterfaceName()) - $regiont.stack.setTemplate($template) - $region.stack.push() - $.openstackId: null _getRouterInterfaceName: Body: Return: format('ri-{0}', id($)) _getNetworkName: Body: Return: format('network-{0}', id($)) _getSubnetName: Body: Return: format('subnet-{0}', id($)) _getExternalNetId: Body: - If: $.externalRouterId != null Then: Return: $._getNetExplorer().getExternalNetworkIdForRouter($.externalRouterId) Else: Return: null _getHeatName: Body: Return: format('{0}-{1}', $.name, id($)) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/meta/io.murano/Classes/resources/NeutronNetworkBase.yaml0000664000175000017500000001405200000000000026053 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. Namespaces: =: io.murano.resources std: io.murano sys: io.murano.system Name: NeutronNetworkBase Properties: port_security_disable: Contract: $.bool() Extends: Network Methods: .init: Body: - $._netExplorer: null - $._environment: $.find(std:Environment) _getNetExplorer: Body: - If: $._netExplorer = null Then: - $._netExplorer: new(sys:NetworkExplorer, $this.getRegion()) - Return: $._netExplorer joinInstanceToNetwork: Arguments: - instance: Contract: $.class(Instance).notNull() - securityGroupName: Contract: $.string() - securityGroups: Contract: [$.string()] - sharedIps: Contract: - $.class(std:SharedIp) - netRef: Contract: $ - subnetRef: Contract: $ - floatingIpResourceName: Contract: $.string() - floatingIpNetRef: Contract: $ - netName: Contract: $.string().notNull() Body: - $netExplorer: $._getNetExplorer() - $securityGroupsEnabled: $netExplorer.listNeutronExtensions().alias.contains('security-group') - $portName: format('port-{0}-{1}', id($), $instance.name) - $addressesOutputName: format('addresses-{0}-{1}', $instance.name, id($this)) - $patchTemplate: resources: $portName: type: 'OS::Neutron::Port' properties: network: $netRef replacement_policy: AUTO outputs: $addressesOutputName: description: format('Addresses for {0} in {1}', $instance.name, $netName) value: get_attr: [$instance.name, 'addresses', $netName] - If: $subnetRef Then: - $template: resources: $portName: properties: fixed_ips: - subnet: $subnetRef - $patchTemplate: $patchTemplate.mergeWith($template) - If: $securityGroupsEnabled and not $.port_security_disable Then: - If: len($securityGroups) > 0 and $securityGroups[0] != "" Then: - For: securityGroup In: $securityGroups Do: - $template: resources: $portName: properties: security_groups: - $securityGroup - $patchTemplate: $patchTemplate.mergeWith($template) Else: - If: bool($securityGroupName) Then: - $template: resources: $portName: properties: security_groups: - get_resource: $securityGroupName - $patchTemplate: $patchTemplate.mergeWith($template) - If: $.port_security_disable Then: - $template: resources: $portName: properties: port_security_enabled: false - $patchTemplate: $patchTemplate.mergeWith($template) - $instanceResources: [$portName] - $instanceOutputs: [$addressesOutputName] - For: sip In: $sharedIps Do: - $template: resources: $portName: properties: allowed_address_pairs: - ip_address: $sip.virtualIp + '/32' - $patchTemplate: $patchTemplate.mergeWith($template) - $instanceFipOutput: null - If: $floatingIpResourceName != null and $floatingIpNetRef != null Then: - $instanceFipOutput: $instance.name + '-floatingIPaddress' - $template: resources: $floatingIpResourceName: type: 'OS::Neutron::FloatingIP' properties: floating_network: $floatingIpNetRef port_id: get_resource: $portName outputs: $instanceFipOutput: value: get_attr: [$floatingIpResourceName, floating_ip_address] description: format('Floating IP of {0}', $instance.name) - $instanceResources: $instanceResources.append($floatingIpResourceName) - $instanceOutputs: $instanceOutputs.append($instanceFipOutput) - $patchTemplate: $patchTemplate.mergeWith($template) - Return: template: $patchTemplate portRef: get_resource: $portName instanceFipOutput: $instanceFipOutput instanceResources: $instanceResources instanceOutputs: $instanceOutputs generateSecurityGroupManager: Body: - $region: $.getRegion() - $netExplorer: $._getNetExplorer() - $securityGroupsEnabled: $netExplorer.listNeutronExtensions().alias.contains('security-group') - If: $securityGroupsEnabled Then: - Return: new(sys:NeutronSecurityGroupManager, $region) Else: - $._environment.reporter.report($this, "Warning! Security groups are disabled!") - Return: new(sys:DummySecurityGroupManager, $region) getInstanceIpList: Arguments: - instance: Contract: $.class(Instance).notNull() Body: - Return: $.getRegion().stack.output()[ format('addresses-{0}-{1}', $instance.name, id($this))].addr.distinct() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/meta/io.murano/Classes/resources/NovaNetwork.yaml0000664000175000017500000000620500000000000024532 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. Namespaces: =: io.murano.resources std: io.murano sys: io.murano.system Name: NovaNetwork Extends: Network Methods: joinInstance: Arguments: - instance: Contract: $.class(Instance).notNull() - securityGroupName: Contract: $.string() - securityGroups: Contract: [$.string()] - assignFloatingIp: Contract: $.bool().notNull() - sharedIps: Contract: - $.class(std:SharedIp) Body: - $fipName: null - $template: null - $instanceFipOutput: null - $instanceResources: [] - $instanceOutputs: [] - $instanceNetworkOutput: format('{0}-assigned-ips', $instance.name) - $template: outputs: $instanceNetworkOutput: description: format('Network IPs assigned to {0} instance', $instance.name) value: get_attr: [ $instance.name, networks ] - If: $assignFloatingIp Then: - $instanceFipOutput: $instance.name + '-floatingIPaddress' - $fipName: format('fip-nn-{0}', $instance.name) - $fipTemplate: resources: $fipName: type: 'OS::Nova::FloatingIP' $fipName + 'Assignment': type: 'OS::Nova::FloatingIPAssociation' properties: floating_ip: get_resource: $fipName server_id: get_resource: $instance.name outputs: $instanceFipOutput: value: get_attr: [$fipName, ip] description: format('Floating IP of {0}', $instance.name) - $template: $template.mergeWith($fipTemplate) - $instanceResources: [$fipName, $fipName + 'Assignment'] - $instanceOutputs: [$instanceFipOutput, $instanceNetworkOutput] - Return: template: $template secGroupName: get_resource: $securityGroupName instanceFipOutput: $instanceFipOutput instanceResources: $instanceResources instanceOutputs: $instanceOutputs generateSecurityGroupManager: Body: - Return: new(sys:AwsSecurityGroupManager, $this) describe: Body: - Return: provider: NovaNetwork getInstanceIpList: Arguments: - instance: Contract: $.class(Instance).notNull() Body: - Return: $instance.getRegion().stack.output().get( format('{0}-assigned-ips', $instance.name) ).values().flatten().distinct() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/meta/io.murano/Classes/resources/Volume.yaml0000664000175000017500000000261200000000000023522 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. Namespaces: =: io.murano.resources std: io.murano Name: Volume Extends: std:CloudResource Properties: openstackId: Contract: $.string() Usage: Out Methods: deploy: getRef: releaseResources: .destroy: Body: - $.releaseResources() attachTo: Arguments: - instance: Contract: $.class(Instance).notNull() - path: Contract: $.string().notNull() Body: - $.deploy() - $resourceName: format('vol-attachment-{0}-{1}', id($), $instance.name) - Return: template: resources: $resourceName: type: 'OS::Cinder::VolumeAttachment' properties: instance_uuid: $instance.getRef() mountpoint: $path volume_id: $.getRef() instanceResources: [$resourceName] instanceOutputs: [] ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/meta/io.murano/Classes/resources/WindowsInstance.yaml0000664000175000017500000000432600000000000025376 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. Namespaces: =: io.murano.resources std: io.murano sys: io.murano.system Name: WindowsInstance Extends: Instance Methods: createDefaultInstanceSecurityGroupRules: Arguments: - groupName: Contract: $.string().notNull() Body: - $region: $.getRegion() - $rules: - ToPort: 3389 IpProtocol: tcp FromPort: 3389 External: true - $region.securityGroupManager.addGroupIngress( rules => $rules, groupName => $groupName) prepareUserData: Body: - $region: $.getRegion() - $rabbitMqParams: $region.getConfig().agentRabbitMq - $resources: new(sys:Resources) - $configFile: $resources.string('Agent-v1.template') - $initScript: $resources.string('windows-init.ps1') - $configReplacements: "%RABBITMQ_HOST%": $rabbitMqParams.host "%RABBITMQ_PORT%": $rabbitMqParams.port "%RABBITMQ_USER%": $rabbitMqParams.login "%RABBITMQ_PASSWORD%": $rabbitMqParams.password "%RABBITMQ_VHOST%": $rabbitMqParams.virtual_host "%RABBITMQ_SSL%": str($rabbitMqParams.ssl).toLower() "%RABBITMQ_INPUT_QUEUE%": $.agent.queueName() "%RESULT_QUEUE%": $region.agentListener.queueName() "%SIGNING_KEY%": $.agent.signingKey('') - $scriptReplacements: "%AGENT_CONFIG_BASE64%": base64encode($configFile.replace($configReplacements)) "%INTERNAL_HOSTNAME%": $.name "%MURANO_SERVER_ADDRESS%": coalesce(config(file_server), $rabbitMqParams.host) "%CA_ROOT_CERT_BASE64%": "" - Return: data: $initScript.replace($scriptReplacements) format: HEAT_CFNTOOLS ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1696417899.7691808 murano-16.0.0/meta/io.murano/Classes/system/0000775000175000017500000000000000000000000020700 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/meta/io.murano/Classes/system/Agent.yaml0000664000175000017500000000123500000000000022623 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. # Blank class for in-python system libraries, for caching purposes Namespaces: =: io.murano.system Name: Agent ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/meta/io.murano/Classes/system/AgentListener.yaml0000664000175000017500000000124500000000000024332 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. # Blank class for in-python system libraries, for caching purposes Namespaces: =: io.murano.system Name: AgentListener ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/meta/io.murano/Classes/system/AwsSecurityGroupManager.yaml0000664000175000017500000000741000000000000026360 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. Namespaces: =: io.murano.system std: io.murano Name: AwsSecurityGroupManager Extends: SecurityGroupManager Methods: .init: Body: - $._environment: $.find(std:Environment) - $._region: $.find(std:CloudRegion).require() addGroupIngress: Arguments: - rules: Contract: - FromPort: $.int().notNull() ToPort: $.int().notNull() IpProtocol: $.string().notNull() External: $.bool().notNull() Ethertype: $.string().check($ in list(null, 'IPv4', 'IPv6')) - groupName: Contract: $.string().notNull() Default: $this.defaultGroupName Body: - $._addGroup(ingress, $rules, $groupName) addGroupEgress: Arguments: - rules: Contract: - FromPort: $.int().notNull() ToPort: $.int().notNull() IpProtocol: $.string().notNull() External: $.bool().notNull() Ethertype: $.string().check($ in list(null, 'IPv4', 'IPv6')) - groupName: Contract: $.string().notNull() Default: $this.defaultGroupName Body: - $._addGroup(egress, $rules, $groupName) _addGroup: Arguments: - direction: Contract: $.string().notNull().check($ in list(ingress, egress)) Default: ingress - rules: Contract: - FromPort: $.int().notNull() ToPort: $.int().notNull() IpProtocol: $.string().notNull() External: $.bool().notNull() Ethertype: $.string().check($ in list(null, 'IPv4', 'IPv6')) - groupName: Contract: $.string().notNull() Default: $this.defaultGroupName Body: - $ext_keys: true: ext_key: remote_ip_prefix ext_val: '0.0.0.0/0' false: ext_key: remote_mode ext_val: remote_group_id - $ethertype: $rules.where($.get(Ethertype) = IPv6) - If: len($ethertype) > 0 Then: - $msg: 'Unable to add security group. IPv6 is not supported.' - $._environment.reporter.report_error($this, $msg) - Throw: UnsupportedPropertyValue Message: $msg - $groupDirection: dict(egress => SecurityGroupEgress).get($direction, SecurityGroupIngress) - $stack: $._region.stack - $template: resources: $groupName: type: 'AWS::EC2::SecurityGroup' properties: GroupDescription: format('Composite security group of Murano environment {0}', $._environment.name) $groupDirection: - FromPort: '-1' ToPort: '-1' IpProtocol: icmp CidrIp: '0.0.0.0/0' - $._region.stack.updateTemplate($template) - $rulesList: $rules.select(dict( FromPort => str($.FromPort), ToPort => str($.ToPort), IpProtocol => $.IpProtocol, CidrIp => '0.0.0.0/0' )) - $template: resources: $groupName: type: 'AWS::EC2::SecurityGroup' properties: $groupDirection: $rulesList - $._region.stack.updateTemplate($template) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/meta/io.murano/Classes/system/DummySecurityGroupManager.yaml0000664000175000017500000000164100000000000026721 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. Namespaces: =: io.murano.system std: io.murano Name: DummySecurityGroupManager Extends: SecurityGroupManager # This class actually adds nothing to the base SecurityGroupManager, # so a base class could be used instead. However, it's better to explicitly # declare this class and use it, since the base one is supposed to remain # "abstract" and never be instantiated. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/meta/io.murano/Classes/system/HeatStack.yaml0000664000175000017500000000124100000000000023431 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. # Blank class for in-python system libraries, for caching purposes Namespaces: =: io.murano.system Name: HeatStack ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/meta/io.murano/Classes/system/InstanceNotifier.yaml0000664000175000017500000000125000000000000025026 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. # Blank class for in-python system libraries, for caching purposes Namespaces: =: io.murano.system Name: InstanceNotifier ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/meta/io.murano/Classes/system/Logger.yaml0000664000175000017500000000123600000000000023005 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. # Blank class for in-python system libraries, for caching purposes Namespaces: =: io.murano.system Name: Logger ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/meta/io.murano/Classes/system/MetadefBrowser.yaml0000664000175000017500000000174200000000000024501 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. Namespaces: =: io.murano.system Name: MetadefBrowser Methods: canBeAppliedTo: Arguments: - tag: Contract: $.string().notNull() - resourceType: Contract: $.string().notNull() Body: - $nss: $this.getNamespaces($resourceType) - $objects: $nss.select($this.getObjects($.namespace)).flatten() - $keys: $objects.properties.select($.keys()).flatten() - Return: $tag in $keys ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/meta/io.murano/Classes/system/MistralClient.yaml0000664000175000017500000000124500000000000024340 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. # Blank class for in-python system libraries, for caching purposes Namespaces: =: io.murano.system Name: MistralClient ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/meta/io.murano/Classes/system/NetworkExplorer.yaml0000664000175000017500000000124700000000000024742 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. # Blank class for in-python system libraries, for caching purposes Namespaces: =: io.murano.system Name: NetworkExplorer ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/meta/io.murano/Classes/system/NeutronSecurityGroupManager.yaml0000664000175000017500000000661000000000000027261 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. Namespaces: =: io.murano.system std: io.murano Name: NeutronSecurityGroupManager Extends: SecurityGroupManager Methods: .init: Body: - $._environment: $.find(std:Environment) - $._region: $.find(std:CloudRegion).require() - $.addGroupIngress() addGroupIngress: Arguments: - rules: Contract: - FromPort: $.int().notNull() ToPort: $.int().notNull() IpProtocol: $.string().notNull() External: $.bool().notNull() Ethertype: $.string().check($ in list(null, 'IPv4', 'IPv6')) - groupName: Contract: $.string().notNull() Default: $this.defaultGroupName Body: - $._addGroup(ingress, $rules, $groupName) addGroupEgress: Arguments: - rules: Contract: - FromPort: $.int().notNull() ToPort: $.int().notNull() IpProtocol: $.string().notNull() External: $.bool().notNull() Ethertype: $.string().check($ in list(null, 'IPv4', 'IPv6')) - groupName: Contract: $.string().notNull() Default: $this.defaultGroupName Body: - $._addGroup(egress, $rules, $groupName) _addGroup: Arguments: - direction: Contract: $.string().notNull().check($ in list(ingress, egress)) - rules: Contract: - FromPort: $.int().notNull() ToPort: $.int().notNull() IpProtocol: $.string().notNull() External: $.bool().notNull() Ethertype: $.string().check($ in list(null, 'IPv4', 'IPv6')) - groupName: Contract: $.string().notNull() Body: - $ext_keys: true: ext_key: remote_ip_prefix ext_val: '0.0.0.0/0' false: ext_key: remote_mode ext_val: remote_group_id - $template: resources: $groupName: type: 'OS::Neutron::SecurityGroup' properties: description: format( 'Composite security group of Murano environment {0}', $._environment.name) rules: - protocol: icmp remote_ip_prefix: '0.0.0.0/0' - $._region.stack.updateTemplate($template) - $rulesList: $rules.select(dict( port_range_min => $.FromPort, port_range_max => $.ToPort, protocol => $.IpProtocol, ethertype => $.get(Ethertype, IPv4), $ext_keys.get($.External).ext_key => $ext_keys.get($.External).ext_val, direction => $direction )) - $template: resources: $groupName: type: 'OS::Neutron::SecurityGroup' properties: rules: $rulesList - $._region.stack.updateTemplate($template)././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/meta/io.murano/Classes/system/Resources.yaml0000664000175000017500000000124100000000000023534 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. # Blank class for in-python system libraries, for caching purposes Namespaces: =: io.murano.system Name: Resources ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/meta/io.murano/Classes/system/SecurityGroupManager.yaml0000664000175000017500000000305700000000000025710 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. Namespaces: =: io.murano.system std: io.murano Name: SecurityGroupManager Properties: defaultGroupName: Contract: $.string() Default: id($.find(std:CloudRegion)) Methods: addGroupIngress: Arguments: - rules: Contract: - FromPort: $.int().notNull() ToPort: $.int().notNull() IpProtocol: $.string().notNull() External: $.bool().notNull() Ethertype: $.string().check($ in list(null, 'IPv4', 'IPv6')) - groupName: Contract: $.string().notNull() Default: $this.defaultGroupName addGroupEgress: Arguments: - rules: Contract: - FromPort: $.int().notNull() ToPort: $.int().notNull() IpProtocol: $.string().notNull() External: $.bool().notNull() Ethertype: $.string().check($ in list(null, 'IPv4', 'IPv6')) - groupName: Contract: $.string().notNull() Default: $this.defaultGroupName ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/meta/io.murano/Classes/system/StatusReporter.yaml0000664000175000017500000000124600000000000024575 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. # Blank class for in-python system libraries, for caching purposes Namespaces: =: io.murano.system Name: StatusReporter ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1696417899.7691808 murano-16.0.0/meta/io.murano/Classes/test/0000775000175000017500000000000000000000000020333 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/meta/io.murano/Classes/test/TestFixture.yaml0000664000175000017500000000415600000000000023513 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. Namespaces: =: io.murano.test std: io.murano sys: io.murano.system res: io.murano.resources --- # ------------------------------------------------------------------ # --- Name: TestFixture Methods: setUp: Body: tearDown: Body: --- # ------------------------------------------------------------------ # --- Name: DummyNetwork Extends: res:Network Methods: joinInstance: Arguments: - instance: Contract: $.class(res:Instance).notNull() - securityGroupName: Contract: $.string() - assignFloatingIp: Contract: $.bool().notNull() - sharedIps: Contract: - $.class(std:SharedIp) Body: - Return: template: {} portRef: instanceFipOutput: generateSecurityGroupManager: Body: - Return: new(sys:DummySecurityGroupManager, $this) describe: Body: - Return: {} --- # ------------------------------------------------------------------ # --- Name: TestFixtureWithEnvironment Extends: TestFixture Properties: environment: Usage: Runtime Contract: $.class(std:Environment) Methods: setUp: Body: - $testClassName: typeinfo($).name - $envName: format('environment-of-testclass-{0}', $testClassName) - $netName: format('default-network-of-testclass-{0}', $testClassName) - $envSnippet: Objects: std:Environment: name: $envName defaultNetworks: environment: :DummyNetwork: - $this.environment: $this.load($envSnippet) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/meta/io.murano/LICENSE0000664000175000017500000002363600000000000016776 0ustar00zuulzuul00000000000000 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. ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1696417899.7691808 murano-16.0.0/meta/io.murano/Resources/0000775000175000017500000000000000000000000017731 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1696417875.0 murano-16.0.0/meta/io.murano/Resources/Agent-v1.template0000664000175000017500000000262300000000000023053 0ustar00zuulzuul00000000000000