diff --git a/muranodashboard/dynamic_ui/fields.py b/muranodashboard/dynamic_ui/fields.py index c78d94ee0..05a9d1682 100644 --- a/muranodashboard/dynamic_ui/fields.py +++ b/muranodashboard/dynamic_ui/fields.py @@ -33,7 +33,7 @@ from openstack_dashboard.api import glance from openstack_dashboard.api import nova from oslo_log import log as logging import six -import yaql +from yaql import legacy from muranoclient.common import exceptions as muranoclient_exc from muranodashboard.api import packages as pkg_api @@ -77,7 +77,7 @@ def make_yaql_validator(validator_property): message = validator_property.get('message', '') def validator_func(value): - context = yaql.create_context() + context = legacy.create_context() context['$'] = value if not expr.evaluate(context=context): raise forms.ValidationError(message) diff --git a/muranodashboard/dynamic_ui/forms.py b/muranodashboard/dynamic_ui/forms.py index ab0e207d9..dfdc7ab28 100644 --- a/muranodashboard/dynamic_ui/forms.py +++ b/muranodashboard/dynamic_ui/forms.py @@ -62,6 +62,19 @@ TYPES_KWARGS = { def _collect_fields(field_specs, form_name, service): + def careful_deepcopy(x): + """Careful handling of deepcopy object with recursive. + + There is a recursive reference in YAQL expression + (since 1.0 version) and standard deepcopy can't handle this. + """ + original_validators = x.pop('validators', None) + + result = copy.deepcopy(x) + if original_validators: + result['validators'] = original_validators + return result + def process_widget(cls, kwargs): if isinstance(cls, tuple): cls, _w = cls @@ -127,7 +140,7 @@ def _collect_fields(field_specs, form_name, service): return name, cls(**kwargs) - return [make_field(copy.deepcopy(_spec)) for _spec in field_specs] + return [make_field(careful_deepcopy(_spec)) for _spec in field_specs] class DynamicFormMetaclass(forms.forms.DeclarativeFieldsMetaclass): diff --git a/muranodashboard/tests/functional/HotApp/template.yaml b/muranodashboard/tests/functional/HotApp/template.yaml new file mode 100644 index 000000000..e46ef88bf --- /dev/null +++ b/muranodashboard/tests/functional/HotApp/template.yaml @@ -0,0 +1,21 @@ +heat_template_version: 2013-05-23 + +description: > + Hello world HOT template that just defines a single server. + Contains just base features to verify base HOT support. + +parameters: + flavor: + type: string + description: Flavor for the server to be created + default: m1.small + constraints: + - allowed_values: [ m1.small, m1.medium, m1.large ] + +resources: + server: + type: OS::Nova::Server + properties: + image: cirros-0.3.4-x86_64-uec + flavor: { get_param: flavor } + diff --git a/muranodashboard/tests/functional/base.py b/muranodashboard/tests/functional/base.py index 2f661f033..c070d1848 100644 --- a/muranodashboard/tests/functional/base.py +++ b/muranodashboard/tests/functional/base.py @@ -258,12 +258,17 @@ class PackageBase(UITestCase): cls.murano_client, "PostgreSQL", {"categories": ["Databases"], "tags": ["tag"]}) + cls.hot_app_id = utils.upload_app_package( + cls.murano_client, + "HotExample", + {"tags": ["hot"]}, hot=True) @classmethod def tearDownClass(cls): super(PackageBase, cls).tearDownClass() cls.murano_client.packages.delete(cls.mockapp_id) cls.murano_client.packages.delete(cls.postgre_id) + cls.murano_client.packages.delete(cls.hot_app_id) class ImageTestCase(PackageBase): @@ -399,8 +404,8 @@ class PackageTestCase(ApplicationTestCase): super(ApplicationTestCase, cls).setUpClass() cls.archive_name = "ToUpload" cls.alt_archive_name = "ModifiedAfterUpload" - cls.archive = utils.compose_package(cls.archive_name, - consts.Manifest, + cls.manifest = os.path.join(consts.PackageDir, 'manifest.yaml') + cls.archive = utils.compose_package(cls.archive_name, cls.manifest, consts.PackageDir) def tearDown(self): @@ -413,7 +418,7 @@ class PackageTestCase(ApplicationTestCase): @classmethod def tearDownClass(cls): super(ApplicationTestCase, cls).tearDownClass() - if os.path.exists(consts.Manifest): - os.remove(consts.Manifest) + if os.path.exists(cls.manifest): + os.remove(cls.manifest) if os.path.exists(cls.archive): os.remove(cls.archive) diff --git a/muranodashboard/tests/functional/consts.py b/muranodashboard/tests/functional/consts.py index 6c857b10a..a65de8b46 100644 --- a/muranodashboard/tests/functional/consts.py +++ b/muranodashboard/tests/functional/consts.py @@ -2,7 +2,8 @@ import os PackageDir = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'MockApp') -Manifest = os.path.join(PackageDir, 'manifest.yaml') +HotPackageDir = os.path.join(os.path.dirname(os.path.realpath(__file__)), + 'HotApp') CategorySelector = "//a[contains(text(), '{0}')][contains(@class, 'dropdown-toggle')]" # noqa App = "//div[contains(@class, 'app-list')]//h4[contains(text(), '{0}')]" @@ -18,6 +19,7 @@ CellStatus = "//td[contains(@class, 'status_{0}')]" ErrorMessage = '//span[contains(@class, "help-block") and contains(text(), "{0}")]' # noqa DatabaseCategory = "select[name='add_category-categories'] > option[value='Databases']" # noqa Action = '//a[contains(@class, "murano_action") and contains(text(), "testAction")]' # noqa +HotFlavorField = '//div[contains(@class, "has-error")]//input' # Buttons ButtonSubmit = ".//*[@name='wizard_goto_step'][2]" diff --git a/muranodashboard/tests/functional/sanity_check.py b/muranodashboard/tests/functional/sanity_check.py index a564adb89..1b9bd08c9 100644 --- a/muranodashboard/tests/functional/sanity_check.py +++ b/muranodashboard/tests/functional/sanity_check.py @@ -610,6 +610,30 @@ class TestSuiteApplications(base.ApplicationTestCase): self.assertIn('Follow the white rabbit', self.driver.find_element_by_class_name('logs').text) + def test_hot_application(self): + """Checks that UI got the hot app is rendered correctly + + Scenario: + 1. Navigate Applications and click Hot app 'Quick Deploy' + 2. Check for YAQL validator + 3. Check that app is added to the environment + """ + self.go_to_submenu('Applications') + self.select_and_click_action_for_app('quick-add', self.hot_app_id) + field_id = "{0}_0-name".format(self.hot_app_id) + self.fill_field(by.By.ID, field_id, value='TestHotApp') + self.driver.find_element_by_xpath(c.ButtonSubmit).click() + self.fill_field(by.By.CSS_SELECTOR, + 'input[id$="flavor"]', + value='testFlavor') + self.driver.find_element_by_xpath(c.InputSubmit).click() + self.check_element_on_page(by.By.XPATH, c.HotFlavorField) + self.fill_field(by.By.CSS_SELECTOR, + 'input[id$="flavor"]', + value='m1.small') + self.driver.find_element_by_xpath(c.InputSubmit).click() + self.check_element_on_page(by.By.LINK_TEXT, 'TestHotApp') + class TestSuitePackages(base.PackageTestCase): def test_modify_package_name(self): diff --git a/muranodashboard/tests/functional/utils.py b/muranodashboard/tests/functional/utils.py index 0d1d01fe6..16515d5e1 100644 --- a/muranodashboard/tests/functional/utils.py +++ b/muranodashboard/tests/functional/utils.py @@ -30,19 +30,24 @@ class ImageException(Exception): return self._error_string -def upload_app_package(client, app_name, data): +def upload_app_package(client, app_name, data, hot=False): try: - archive = compose_package(app_name, consts.Manifest, - consts.PackageDir) + if not hot: + manifest = os.path.join(consts.PackageDir, 'manifest.yaml') + archive = compose_package(app_name, manifest, consts.PackageDir) + else: + manifest = os.path.join(consts.HotPackageDir, 'manifest.yaml') + archive = compose_package(app_name, manifest, + consts.HotPackageDir, hot=True) package = client.packages.create(data, {app_name: open(archive, 'rb')}) return package.id finally: os.remove(archive) - os.remove(consts.Manifest) + os.remove(manifest) def compose_package(app_name, manifest, package_dir, - require=None, archive_dir=None): + require=None, archive_dir=None, hot=False): """Composes a murano package Composes package `app_name` with `manifest` file as a template for the @@ -56,7 +61,11 @@ def compose_package(app_name, manifest, package_dir, mfest_copy = MANIFEST.copy() mfest_copy['FullName'] = fqn mfest_copy['Name'] = app_name - mfest_copy['Classes'] = {fqn: 'mock_muranopl.yaml'} + if hot: + mfest_copy['Format'] = 'Heat.HOT/1.0' + else: + mfest_copy['Format'] = '1.0' + mfest_copy['Classes'] = {fqn: 'mock_muranopl.yaml'} if require: mfest_copy['Require'] = require f.write(yaml.dump(mfest_copy, default_flow_style=False))