diff --git a/devstack/plugin.sh b/devstack/plugin.sh index 288c8df5..d49995ce 100755 --- a/devstack/plugin.sh +++ b/devstack/plugin.sh @@ -176,7 +176,7 @@ function configure_murano { iniset $MURANO_CONF_FILE DEFAULT home_region $REGION_NAME # Murano Policy Enforcement Configuration - if [[ -n "$MURANO_ENABLE_MODEL_POLICY_ENFORCEMENT" ]]; then + if [[ "$MURANO_ENABLE_MODEL_POLICY_ENFORCEMENT" == "True" ]]; then iniset $MURANO_CONF_FILE engine enable_model_policy_enforcer $MURANO_ENABLE_MODEL_POLICY_ENFORCEMENT fi @@ -358,6 +358,7 @@ function setup_core_library() { remove_core_apps_zip } + # install_murano() - Collect source and prepare function install_murano() { install_murano_pythonclient @@ -443,6 +444,15 @@ function configure_murano_tempest_plugin() { 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 } diff --git a/devstack/settings b/devstack/settings index 469065df..24c871f1 100644 --- a/devstack/settings +++ b/devstack/settings @@ -17,8 +17,13 @@ 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_POLICY_FILE=${MURANO_CONF_DIR}/policy.json -MURANO_DEBUG=${MURANO_DEBUG:-True} -MURANO_ENABLE_MODEL_POLICY_ENFORCEMENT=${MURANO_ENABLE_MODEL_POLICY_ENFORCEMENT:-False} +MURANO_DEBUG=$(trueorfalse True MURANO_DEBUG) +MURANO_ENABLE_MODEL_POLICY_ENFORCEMENT=$(trueorfalse False MURANO_ENABLE_MODEL_POLICY_ENFORCEMENT) + +# Since Murano support raw cloud images, we can allow download any image with cloud init +UBUNTU_CLOUD_IMAGE_URL=${UBUNTU_CLOUD_IMAGE_URL:-'http://storage.apps.openstack.org/images/ubuntu-14.04-m-agent.qcow2'} +IMAGE_URLS+=",${UBUNTU_CLOUD_IMAGE_URL}" +CLOUD_IMAGE_NAME=$(basename "$UBUNTU_CLOUD_IMAGE_URL" ".qcow2") # Set up murano service endpoint MURANO_SERVICE_HOST=${MURANO_SERVICE_HOST:-$SERVICE_HOST} @@ -37,7 +42,7 @@ MURANO_KEYSTONE_SIGNING_DIR=${MURANO_KEYSTONE_SIGNING_DIR:-/tmp/keystone-signing # Set up murano networking settings MURANO_DEFAULT_ROUTER=${MURANO_DEFAULT_ROUTER:-''} MURANO_EXTERNAL_NETWORK=${MURANO_EXTERNAL_NETWORK:-''} -MURANO_DEFAULT_DNS=${MURANO_DEFAULT_DNS:-''} +MURANO_DEFAULT_DNS=${MURANO_DEFAULT_DNS:-'8.8.8.8'} # Choose applications for installation MURANO_APPS=${MURANO_APPS:-''} @@ -58,6 +63,8 @@ MURANO_RABBIT_VHOST=${MURANO_RABBIT_VHOST:-''} 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. diff --git a/murano_tempest_tests/extras/io.murano.apps.test.VM/Classes/VM.yaml b/murano_tempest_tests/extras/io.murano.apps.test.VM/Classes/VM.yaml new file mode 100644 index 00000000..47822946 --- /dev/null +++ b/murano_tempest_tests/extras/io.murano.apps.test.VM/Classes/VM.yaml @@ -0,0 +1,66 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# 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.apps.test + std: io.murano + res: io.murano.resources + sys: io.murano.system + conf: io.murano.configuration + + +Name: VM + +Extends: std:Application + +Properties: + name: + Contract: $.string().notNull() + + instance: + Contract: $.class(res:Instance).notNull() + + userName: + Contract: $.string() + +Methods: + initialize: + Body: + - $._environment: $.find(std:Environment).require() + + deploy: + Body: + - If: not $.getAttr(deployed, false) + Then: + - $._environment.reporter.report($this, 'Creating VM') + - $securityGroupIngress: + - ToPort: 80 + FromPort: 80 + IpProtocol: tcp + External: true + - ToPort: 443 + FromPort: 443 + IpProtocol: tcp + External: true + - $._environment.securityGroupManager.addGroupIngress($securityGroupIngress) + - $.instance.deploy() + - $._environment.reporter.report($this, 'Instance is created.') + + - $resources: new(sys:Resources) + - $linux: new(conf:Linux) + + - If: $.instance.assignFloatingIp + Then: + - $host: $.instance.floatingIpAddress + Else: + - $host: $.instance.ipAddresses[0] + - $.setAttr(deployed, true) diff --git a/murano_tempest_tests/extras/io.murano.apps.test.VM/Resources/index.html b/murano_tempest_tests/extras/io.murano.apps.test.VM/Resources/index.html new file mode 100644 index 00000000..68436975 --- /dev/null +++ b/murano_tempest_tests/extras/io.murano.apps.test.VM/Resources/index.html @@ -0,0 +1,8 @@ + + + + Hello World + +Hello world. This is my first web page. My name is %USER_NAME%. + + diff --git a/murano_tempest_tests/extras/io.murano.apps.test.VM/manifest.yaml b/murano_tempest_tests/extras/io.murano.apps.test.VM/manifest.yaml new file mode 100644 index 00000000..7b1a21c5 --- /dev/null +++ b/murano_tempest_tests/extras/io.murano.apps.test.VM/manifest.yaml @@ -0,0 +1,22 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# 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.0 +Type: Application +FullName: io.murano.test.VM.VM +Name: VM +Description: | + application which simply boot a virtual machine +Author: 'Mirantis, Inc' +Tags: [Server] +Classes: + io.murano.apps.test.VM: VM.yaml diff --git a/murano_tempest_tests/tests/scenario/application_catalog/base.py b/murano_tempest_tests/tests/scenario/application_catalog/base.py index ac47d1c3..cb5800b7 100644 --- a/murano_tempest_tests/tests/scenario/application_catalog/base.py +++ b/murano_tempest_tests/tests/scenario/application_catalog/base.py @@ -94,6 +94,7 @@ class BaseApplicationCatalogScenarioTest(base.BaseTestCase): creds = cls.get_configured_isolated_creds(type_of_creds='primary') cls.os = clients.Manager(credentials=creds) cls.services_manager = services_manager(creds) + cls.linux_image = CONF.application_catalog.linux_image cls.application_catalog_client = cls.os.application_catalog_client cls.artifacts_client = cls.os.artifacts_client @@ -102,6 +103,13 @@ class BaseApplicationCatalogScenarioTest(base.BaseTestCase): cls.snapshots_client = cls.services_manager.snapshots_client cls.volumes_client = cls.services_manager.volumes_client cls.backups_client = cls.services_manager.backups_client + cls.images_client = cls.services_manager.image_client_v2 + cls.cirros_image = cls.get_required_image_name() + + @classmethod + def get_required_image_name(cls): + image = cls.images_client.show_image(CONF.compute.image_ref) + return image['name'] @classmethod def resource_cleanup(cls): @@ -152,8 +160,8 @@ class BaseApplicationCatalogScenarioTest(base.BaseTestCase): if name in instance['name']: return instance['id'] - def apache_cinder( - self, attributes=None, userName=None, flavor='m1.medium'): + def apache( + self, attributes=None, userName=None, flavor='m1.small'): post_body = { "instance": { "flavor": flavor, @@ -179,7 +187,34 @@ class BaseApplicationCatalogScenarioTest(base.BaseTestCase): } return post_body - def update_executor(self, flavor='m1.medium'): + def vm_cinder( + self, attributes=None, userName=None, flavor='m1.small'): + post_body = { + "instance": { + "flavor": flavor, + "image": self.cirros_image, + "assignFloatingIp": True, + "availabilityZone": "nova", + "volumes": attributes, + "?": { + "type": "io.murano.resources.LinuxMuranoInstance", + "id": utils.generate_uuid() + }, + "name": utils.generate_name("testMurano") + }, + "name": utils.generate_name("VM"), + "userName": userName, + "?": { + "_{id}".format(id=utils.generate_uuid()): { + "name": "VM" + }, + "type": "io.murano.apps.test.VM", + "id": utils.generate_uuid() + } + } + return post_body + + def update_executor(self, flavor='m1.small'): post_body = { "instance": { "flavor": flavor, @@ -203,8 +238,9 @@ class BaseApplicationCatalogScenarioTest(base.BaseTestCase): self.application_catalog_client.deploy_session(environment['id'], session['id']) timeout = 1800 - deployed_env = utils.wait_for_environment_deploy(environment, - timeout=timeout) + deployed_env = utils.wait_for_environment_deploy( + self.application_catalog_client, environment['id'], + timeout=timeout) if deployed_env['status'] == 'ready': return deployed_env elif deployed_env['status'] == 'deploying': @@ -257,8 +293,8 @@ class BaseApplicationCatalogScenarioTest(base.BaseTestCase): self.assertEqual(0, result, '%s port is closed on instance' % port) @classmethod - def create_volume(cls): - volume = cls.volumes_client.create_volume()['volume'] + def create_volume(cls, **kwargs): + volume = cls.volumes_client.create_volume(**kwargs)['volume'] waiters.wait_for_volume_status(cls.volumes_client, volume['id'], 'available') return volume['id'] @@ -291,12 +327,13 @@ class BaseApplicationCatalogScenarioTest(base.BaseTestCase): backup = self.backups_client.create_backup( volume_id=volume_id, force=True)['backup'] - self.backups_client.wait_for_backup_status(backup['id'], 'available') + waiters.wait_for_backup_status(self.backups_client, + backup['id'], 'available') return backup['id'] def delete_backup(self, backup_id): self.backups_client.delete_backup(backup_id) - return self.backups_client.wait_for_backup_deletion(backup_id) + return self.backups_client.wait_for_resource_deletion(backup_id) def get_volume(self, environment_id): stack = self.get_stack_id(environment_id) diff --git a/murano_tempest_tests/tests/scenario/application_catalog/test_cinder_volumes.py b/murano_tempest_tests/tests/scenario/application_catalog/test_cinder_volumes.py index fd2b241d..17ea9008 100644 --- a/murano_tempest_tests/tests/scenario/application_catalog/test_cinder_volumes.py +++ b/murano_tempest_tests/tests/scenario/application_catalog/test_cinder_volumes.py @@ -32,11 +32,11 @@ class TestCinderVolumes(base.BaseApplicationCatalogScenarioTest): raise cls.skipException(msg) super(TestCinderVolumes, cls).resource_setup() cls.linux = CONF.application_catalog.linux_image - application_name = utils.generate_name('Apache_custom') + application_name = utils.generate_name('VM') cls.abs_archive_path, dir_with_archive, archive_name = \ utils.prepare_package( application_name, - app='io.murano.apps.test.ApacheHttpServerCustom', + app='io.murano.apps.test.VM', manifest_required=False) if CONF.application_catalog.glare_backend: cls.client = cls.artifacts_client @@ -45,7 +45,7 @@ class TestCinderVolumes(base.BaseApplicationCatalogScenarioTest): cls.package = cls.client.upload_package( application_name, archive_name, dir_with_archive, {"categories": ["Web"], "tags": ["test"]}) - cls.volume = cls.create_volume() + cls.volume = cls.create_volume(size='1') @classmethod def resource_cleanup(cls): @@ -62,7 +62,7 @@ class TestCinderVolumes(base.BaseApplicationCatalogScenarioTest): Scenario: 1. Create environment - 2. Add ApacheHTTPServer application with ability to boot instance + 2. Add VM application with ability to boot instance from Cinder volume 3. Deploy environment 4. Make sure that deployment finished successfully @@ -79,14 +79,14 @@ class TestCinderVolumes(base.BaseApplicationCatalogScenarioTest): create_session(environment['id']) post_body = { "instance": { - "flavor": "m1.medium", + "flavor": "m1.small", "blockDevices": { "volume": { "?": { "type": "io.murano.resources.CinderVolume" }, "size": 4, - "sourceImage": self.linux + "sourceImage": self.cirros_image }, "bootIndex": 0, "deviceName": "vda", @@ -100,12 +100,12 @@ class TestCinderVolumes(base.BaseApplicationCatalogScenarioTest): }, "name": utils.generate_name("testMurano") }, - "name": utils.generate_name("ApacheHTTPServer"), + "name": utils.generate_name("VM"), "?": { "_{id}".format(id=utils.generate_uuid()): { - "name": "ApacheHTTPServer" + "name": "VM" }, - "type": "io.murano.apps.test.ApacheHttpServerCustom", + "type": "io.murano.apps.test.VM", "id": utils.generate_uuid() } } @@ -114,12 +114,11 @@ class TestCinderVolumes(base.BaseApplicationCatalogScenarioTest): post_body) self.deploy_environment(environment, session) - self.status_check(environment['id'], [['testMurano', 22, 80]]) volume_data = self.get_volume(environment['id']) self.check_volume_attached('testMurano', volume_data['id']) self.assertEqual(volume_data['size'], 4) self.assertEqual(volume_data['volume_image_metadata']['image_name'], - self.linux) + self.cirros_image) server = self.get_instance_id('testMurano') self.assertFalse( self.servers_client.show_server(server)['server']['image']) @@ -131,7 +130,7 @@ class TestCinderVolumes(base.BaseApplicationCatalogScenarioTest): Scenario: 1. Create environment - 2. Add ApacheHTTPServer application with ability to attach existing + 2. Add VM application with ability to attach existing Cinder volume to the instance 3. Deploy environment 4. Make sure that deployment finished successfully @@ -153,13 +152,12 @@ class TestCinderVolumes(base.BaseApplicationCatalogScenarioTest): "openstackId": self.volume } } - post_body = self.apache_cinder(attributes=volume_attributes) + post_body = self.vm_cinder(attributes=volume_attributes) self.application_catalog_client.\ create_service(environment['id'], session['id'], post_body) self.deploy_environment(environment, session) - self.status_check(environment['id'], [['testMurano', 22, 80]]) self.check_volume_attached('testMurano', self.volume) @testtools.testcase.attr('smoke') @@ -169,7 +167,7 @@ class TestCinderVolumes(base.BaseApplicationCatalogScenarioTest): Scenario: 1. Create environment - 2. Add ApacheHTTPServer application with ability to create and + 2. Add VM application with ability to create and attach Cinder volume with size 1 GiB to the instance 3. Deploy environment 4. Make sure that deployment finished successfully @@ -191,13 +189,12 @@ class TestCinderVolumes(base.BaseApplicationCatalogScenarioTest): "size": 1 } } - post_body = self.apache_cinder(attributes=volume_attributes) + post_body = self.vm_cinder(attributes=volume_attributes) self.application_catalog_client.\ create_service(environment['id'], session['id'], post_body) self.deploy_environment(environment, session) - self.status_check(environment['id'], [['testMurano', 22, 80]]) volume_data = self.get_volume(environment['id']) self.check_volume_attached('testMurano', volume_data['id']) self.assertEqual(volume_data['size'], 1) @@ -209,7 +206,7 @@ class TestCinderVolumes(base.BaseApplicationCatalogScenarioTest): Scenario: 1. Create environment - 2. Add ApacheHTTPServer application with ability to create Cinder + 2. Add VM application with ability to create Cinder volume with size 2 GiB from image and attach it to the instance 3. Deploy environment 4. Make sure that deployment finished successfully @@ -230,21 +227,20 @@ class TestCinderVolumes(base.BaseApplicationCatalogScenarioTest): "type": "io.murano.resources.CinderVolume" }, "size": 4, - "sourceImage": self.linux + "sourceImage": self.cirros_image } } - post_body = self.apache_cinder(volume_attributes) + post_body = self.vm_cinder(volume_attributes) self.application_catalog_client.\ create_service(environment['id'], session['id'], post_body) self.deploy_environment(environment, session) - self.status_check(environment['id'], [['testMurano', 22, 80]]) volume_data = self.get_volume(environment['id']) self.check_volume_attached('testMurano', volume_data['id']) self.assertEqual(volume_data['size'], 4) self.assertEqual(volume_data['volume_image_metadata']['image_name'], - self.linux) + self.cirros_image) @testtools.testcase.attr('smoke') @testtools.testcase.attr('scenario') @@ -253,7 +249,7 @@ class TestCinderVolumes(base.BaseApplicationCatalogScenarioTest): Scenario: 1. Create environment - 2. Add ApacheHTTPServer application with ability to create Cinder + 2. Add VM application with ability to create Cinder volume with size 1 GiB from existing volume and attach it to the instance 3. Deploy environment @@ -283,13 +279,12 @@ class TestCinderVolumes(base.BaseApplicationCatalogScenarioTest): } } } - post_body = self.apache_cinder(volume_attributes) + post_body = self.vm_cinder(volume_attributes) self.application_catalog_client.\ create_service(environment['id'], session['id'], post_body) self.deploy_environment(environment, session) - self.status_check(environment['id'], [['testMurano', 22, 80]]) volume_data = self.get_volume(environment['id']) self.check_volume_attached('testMurano', volume_data['id']) self.assertEqual(volume_data['size'], 1) @@ -303,7 +298,7 @@ class TestCinderVolumes(base.BaseApplicationCatalogScenarioTest): Scenario: 1. Make snapshot from volume 2. Create environment - 3. Add ApacheHTTPServer application with ability to create + 3. Add VM application with ability to create Cinder volume with size 1 GiB from existing volume snapshot and attach it to the instance 4. Deploy environment @@ -335,13 +330,12 @@ class TestCinderVolumes(base.BaseApplicationCatalogScenarioTest): } } } - post_body = self.apache_cinder(volume_attributes) + post_body = self.vm_cinder(volume_attributes) self.application_catalog_client.\ create_service(environment['id'], session['id'], post_body) self.deploy_environment(environment, session) - self.status_check(environment['id'], [['testMurano', 22, 80]]) volume_data = self.get_volume(environment['id']) self.check_volume_attached('testMurano', volume_data['id']) self.assertEqual(volume_data['size'], 1) @@ -355,7 +349,7 @@ class TestCinderVolumes(base.BaseApplicationCatalogScenarioTest): Scenario: 1. Make backup from volume 2. Create environment - 3. Add ApacheHTTPServer application with ability to create Cinder + 3. Add VM application with ability to create Cinder volume with size 1 GiB from existing volume backup and attach it to the instance 4. Deploy environment @@ -395,13 +389,12 @@ class TestCinderVolumes(base.BaseApplicationCatalogScenarioTest): } } } - post_body = self.apache_cinder(volume_attributes) + post_body = self.vm_cinder(volume_attributes) self.application_catalog_client.\ create_service(environment['id'], session['id'], post_body) self.deploy_environment(environment, session) - self.status_check(environment['id'], [['testMurano', 22, 80]]) volume_data = self.get_volume(environment['id']) self.check_volume_attached('testMurano', volume_data['id']) self.assertEqual(volume_data['size'], 1) @@ -417,11 +410,11 @@ class TestCinderVolumeIsolatedAdmin( msg = "Cinder volumes attachment tests will be skipped." raise cls.skipException(msg) super(TestCinderVolumeIsolatedAdmin, cls).resource_setup() - application_name = utils.generate_name('Apache_custom') + application_name = utils.generate_name('VM') cls.abs_archive_path, dir_with_archive, archive_name = \ utils.prepare_package( application_name, - app='io.murano.apps.test.ApacheHttpServerCustom', + app='io.murano.apps.test.VM', manifest_required=False) if CONF.application_catalog.glare_backend: cls.client = cls.artifacts_client @@ -445,7 +438,7 @@ class TestCinderVolumeIsolatedAdmin( Scenario: 1. Create environment - 2. Add ApacheHTTPServer application with ability to create and + 2. Add VM application with ability to create and attach Cinder volume with size 1 GiB, multiattach and readonly properties to the instance 3. Deploy environment @@ -471,13 +464,12 @@ class TestCinderVolumeIsolatedAdmin( "multiattach": True } } - post_body = self.apache_cinder(attributes=volume_attributes) + post_body = self.vm_cinder(attributes=volume_attributes) self.application_catalog_client.\ create_service(environment['id'], session['id'], post_body) self.deploy_environment(environment, session) - self.status_check(environment['id'], [['testMurano', 22, 80]]) volume_data = self.get_volume(environment['id']) self.check_volume_attached('testMurano', volume_data['id']) self.assertEqual(volume_data['size'], 1) diff --git a/murano_tempest_tests/tests/scenario/application_catalog/test_deployment.py b/murano_tempest_tests/tests/scenario/application_catalog/test_deployment.py index 82a498bb..69c2510a 100644 --- a/murano_tempest_tests/tests/scenario/application_catalog/test_deployment.py +++ b/murano_tempest_tests/tests/scenario/application_catalog/test_deployment.py @@ -27,7 +27,8 @@ class TestMuranoDeployment(base.BaseApplicationCatalogScenarioTest): @classmethod def resource_setup(cls): - if not CONF.application_catalog.deployment_tests: + if not CONF.application_catalog.deployment_tests or \ + not CONF.application_catalog.linux_image: msg = "Application Catalog Scenario Deployment Tests will be " \ "skipped." raise cls.skipException(msg) @@ -97,7 +98,7 @@ class TestMuranoDeployment(base.BaseApplicationCatalogScenarioTest): 6. Delete environment """ - post_body = self.apache_cinder() + post_body = self.apache() environment_name = utils.generate_name('Test_Murano') environment = self.application_catalog_client.create_environment( name=environment_name) @@ -130,8 +131,8 @@ class TestMuranoDeployment(base.BaseApplicationCatalogScenarioTest): 10. Delete environment """ - app_1_post_body = self.apache_cinder() - app_2_post_body = self.apache_cinder() + app_1_post_body = self.apache() + app_2_post_body = self.apache() environment_name = utils.generate_name('Test_Murano') environment = self.application_catalog_client.create_environment( @@ -231,7 +232,7 @@ class TestMuranoDeployment(base.BaseApplicationCatalogScenarioTest): 7. Delete environment """ - post_body = self.apache_cinder(userName=utils.generate_name('user')) + post_body = self.apache(userName=utils.generate_name('user')) username = post_body["userName"] environment_name = utils.generate_name('SSC-murano') environment = self.application_catalog_client.create_environment(