Add base and packages for tempest murano scenario tests

This commit adds implementation of methods and resources
which are used for tempest app deployment and cinder volumes
support tests

Change-Id: Ibd70aa56987d8f69547fb9f114b6a1a69cfcbe0c
targets: bp normalize-murano-tests
This commit is contained in:
MStolyarenko 2016-07-14 13:29:39 +03:00 committed by olehbaran
parent 6b66e9914d
commit 36915a927d
15 changed files with 686 additions and 17 deletions

View File

@ -49,6 +49,10 @@ ApplicationCatalogGroup = [
"If no such region is found in the service catalog, "
"the first found one is used."),
cfg.StrOpt("linux_image",
default="debian-8-m-agent.qcow2",
help="Image for linux services"),
cfg.StrOpt("catalog_type",
default="application-catalog",
help="Catalog type of Application Catalog."),
@ -70,7 +74,14 @@ ApplicationCatalogGroup = [
cfg.BoolOpt("glare_backend",
default=False,
help="Tells tempest about murano glare backend "
"configuration.")
"configuration."),
cfg.BoolOpt("cinder_volume_tests",
default=False,
help="Whether or not cinder volumes attachment tests "
"are expected to run"),
cfg.BoolOpt("deployment_tests",
default=False,
help="Whether or not deployment tests are expected to run")
]
ServiceBrokerGroup = [

View File

@ -0,0 +1,81 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# 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: ApacheHttpServerCustom
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 for Apache Server.')
- $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. Deploying Apache')
- $resources: new(sys:Resources)
- $linux: new(conf:Linux)
- $linux.runCommand($.instance.agent, 'apt-get -y install apache2')
- $linux.runCommand($.instance.agent, 'iptables -I INPUT 1 -p tcp --dport 443 -j ACCEPT')
- $linux.runCommand($.instance.agent, 'iptables -I INPUT 1 -p tcp --dport 80 -j ACCEPT')
- $._environment.reporter.report($this, 'Apache is installed.')
- If: $.userName != ''
Then:
- $linux.runCommand($.instance.agent, 'service apache2 stop')
- $fileReplacements:
"%USER_NAME%": $.userName
- $fileContent: $resources.string('index.html').replace($fileReplacements)
- $linux.putFile($.instance.agent, $fileContent, '/var/www/html/index.html')
- $linux.runCommand($.instance.agent, 'service apache2 start')
- If: $.instance.assignFloatingIp
Then:
- $host: $.instance.floatingIpAddress
Else:
- $host: $.instance.ipAddresses[0]
- $._environment.reporter.report($this, format('Apache is available at http://{0}', $host))
- $.setAttr(deployed, true)

View File

@ -0,0 +1,8 @@
<!DOCTYPE html>
<html>
<head>
<title> Hello World</title>
</head>
<body>Hello world. This is my first web page. My name is %USER_NAME%.
</body>
</html>

View File

@ -0,0 +1,28 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# 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.apache.ApacheHttpServerCustom
Name: Apache HTTP Server Custom
Description: |
The Apache HTTP Server Project is an effort to develop and maintain an
open-source HTTP server for modern operating systems including UNIX and
Windows NT. The goal of this project is to provide a secure, efficient and
extensible server that provides HTTP services in sync with the current HTTP
standards.
Apache httpd has been the most popular web server on the Internet since
April 1996, and celebrated its 17th birthday as a project this February.
Author: 'Mirantis, Inc'
Tags: [HTTP, Server, WebServer, HTML, Apache]
Classes:
io.murano.apps.test.ApacheHttpServerCustom: ApacheHttpServer.yaml

View File

@ -0,0 +1,55 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# 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
sys: io.murano.system
Name: Lighttpd
Extends: std:Application
Properties:
updater:
Contract: $.class(UpdateExecutor).notNull()
Methods:
initialize:
Body:
- $._environment: $.find(std:Environment).require()
deploy:
Body:
- If: not $.getAttr(deployed, false)
Then:
- $securityGroupIngress:
- ToPort: 80
FromPort: 80
IpProtocol: tcp
External: true
- ToPort: 443
FromPort: 443
IpProtocol: tcp
External: true
- $._environment.securityGroupManager.addGroupIngress($securityGroupIngress)
- $._environment.reporter.report($this, 'Ensuring Updater is deployed.')
- $.updater.deploy()
- $resources: new(sys:Resources)
- $template: $resources.yaml('DeployLighttpd.template')
- $.updater.instance.agent.call($template, $resources)
- If: $.updater.instance.assignFloatingIp
Then:
- $address: $.updater.instance.floatingIpAddress
- $._environment.reporter.report($this, format('Running at http://{0}', $address))
- $.setAttr(deployed, true)

View File

@ -0,0 +1,27 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# 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: Deploy Lighttpd
Body: |
deploy()
Scripts:
deploy:
Type: Application
Version: 1.0.0
EntryPoint: deployLighttpd.sh
Options:
captureStdout: true
captureStderr: true

View File

@ -0,0 +1,14 @@
#!/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.
sudo apt-get -y -q install lighttpd

View File

@ -0,0 +1,24 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# 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.apps.test.Lighttpd
Name: Lighttpd
Description: |
Lighttpd... :)
Author: 'Mirantis, Inc'
Tags: [Web]
Classes:
io.murano.apps.test.Lighttpd: Lighttpd.yaml
Require:
io.murano.apps.test.UpdateExecutor:

View File

@ -0,0 +1,47 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# 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: UpdateExecutor
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.')
- $.instance.deploy()
- $._environment.reporter.report($this, 'Starting packages updating.')
- $file: sys:Resources.string('scripts/update.sh')
- conf:Linux.runCommand($.instance.agent, $file)
- $._environment.reporter.report($this, 'Update completed.')
- $.setAttr(deployed, true)

View File

@ -0,0 +1,14 @@
#!/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.
sudo apt-get update

View File

@ -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.apps.test.UpdateExecutor
Name: Update Executor
Description: |
Test application, which updates packages on VM
Author: 'Mirantis, Inc'
Tags: [application]
Classes:
io.murano.apps.test.UpdateExecutor: UpdateExecutor.yaml

View File

@ -0,0 +1,335 @@
# Copyright (c) 2016 Mirantis, 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 requests
import socket
import time
from tempest.clients import Manager as services_manager
from tempest.common import credentials_factory as common_creds
from tempest.common import dynamic_creds
from tempest.common import waiters
from tempest import config
from tempest.lib import base
from tempest.lib import exceptions
from murano_tempest_tests import clients
from murano_tempest_tests import utils
CONF = config.CONF
class BaseApplicationCatalogScenarioTest(base.BaseTestCase):
"""Base test class for Murano Application Catalog Scenario tests."""
@classmethod
def setUpClass(cls):
super(BaseApplicationCatalogScenarioTest, cls).setUpClass()
cls.resource_setup()
@classmethod
def tearDownClass(cls):
cls.resource_cleanup()
super(BaseApplicationCatalogScenarioTest, cls).tearDownClass()
@classmethod
def get_client_with_isolated_creds(cls, type_of_creds="admin"):
creds = cls.get_configured_isolated_creds(type_of_creds=type_of_creds)
os = clients.Manager(credentials=creds)
client = os.application_catalog_client
return client
@classmethod
def get_configured_isolated_creds(cls, type_of_creds='admin'):
identity_version = CONF.identity.auth_version
if identity_version == 'v3':
cls.admin_role = CONF.identity.admin_role
else:
cls.admin_role = 'admin'
if not hasattr(cls, 'dynamic_cred'):
cls.dynamic_cred = dynamic_creds.DynamicCredentialProvider(
identity_version=CONF.identity.auth_version,
name=cls.__name__, admin_role=cls.admin_role,
admin_creds=common_creds.get_configured_admin_credentials(
'identity_admin'))
if type_of_creds == 'primary':
creds = cls.dynamic_cred.get_primary_creds()
elif type_of_creds == 'admin':
creds = cls.dynamic_cred.get_admin_creds()
elif type_of_creds == 'alt':
creds = cls.dynamic_cred.get_alt_creds()
else:
creds = cls.dynamic_cred.get_credentials(type_of_creds)
cls.dynamic_cred.type_of_creds = type_of_creds
return creds.credentials
@classmethod
def verify_nonempty(cls, *args):
if not all(args):
msg = "Missing API credentials in configuration."
raise cls.skipException(msg)
@classmethod
def resource_setup(cls):
if not CONF.service_available.murano:
skip_msg = "Murano is disabled"
raise cls.skipException(skip_msg)
if not hasattr(cls, "os"):
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
cls.servers_client = cls.services_manager.servers_client
cls.orchestration_client = cls.services_manager.orchestration_client
cls.snapshots_client = cls.services_manager.snapshots_client
cls.volumes_client = cls.services_manager.volumes_client
cls.backups_client = cls.services_manager.backups_client
@classmethod
def resource_cleanup(cls):
cls.clear_isolated_creds()
@classmethod
def clear_isolated_creds(cls):
if hasattr(cls, "dynamic_cred"):
cls.dynamic_cred.clear_creds()
def environment_delete(self, environment_id, timeout=180):
self.application_catalog_client.delete_environment(environment_id)
start_time = time.time()
while time.time() - start_time > timeout:
try:
self.application_catalog_client.get_environment(environment_id)
except exceptions.NotFound:
return
@classmethod
def purge_stacks(cls):
stacks = cls.orchestration_client.list_stacks()['stacks']
for stack in stacks:
cls.orchestration_client.delete_stack(stack['id'])
cls.orchestration_client.wait_for_stack_status(stack['id'],
'DELETE_COMPLETE')
def get_service(self, environment, session, service_name):
for service in self.application_catalog_client.get_services_list(
environment, session):
if service['name'] == service_name:
return service
def get_stack_id(self, environment_id):
stacks = self.orchestration_client.list_stacks()['stacks']
for stack in stacks:
if environment_id in self.orchestration_client.show_stack(
stack['id'])['stack']['description']:
return stack['id']
def get_stack_template(self, stack):
return self.orchestration_client.show_template(stack)
def get_instance_id(self, name):
instance_list = self.servers_client.list_servers()['servers']
for instance in instance_list:
if name in instance['name']:
return instance['id']
def apache_cinder(
self, attributes=None, userName=None, flavor='m1.medium'):
post_body = {
"instance": {
"flavor": flavor,
"image": self.linux_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("ApacheHTTPServer"),
"userName": userName,
"?": {
"_{id}".format(id=utils.generate_uuid()): {
"name": "ApacheHTTPServer"
},
"type": "io.murano.apps.test.ApacheHttpServerCustom",
"id": utils.generate_uuid()
}
}
return post_body
def update_executor(self, flavor='m1.medium'):
post_body = {
"instance": {
"flavor": flavor,
"image": self.linux_image,
"assignFloatingIp": True,
"?": {
"type": "io.murano.resources.LinuxMuranoInstance",
"id": utils.generate_uuid()
},
"name": utils.generate_name('testMurano')
},
"name": utils.generate_name('dummy'),
"?": {
"type": "io.murano.apps.test.UpdateExecutor",
"id": utils.generate_uuid()
}
}
return post_body
def deploy_environment(self, environment, session):
self.application_catalog_client.deploy_session(environment['id'],
session['id'])
return self.wait_for_environment_deploy(environment)
def wait_for_environment_deploy(self, environment):
start_time = time.time()
status = self.application_catalog_client.\
get_environment(environment['id'])['status']
while status != 'ready':
status = self.application_catalog_client.\
get_environment(environment['id'])['status']
if time.time() - start_time > 1800:
time.sleep(60)
self.fail(
'Environment deployment is not finished in 1200 seconds')
elif status == 'deploy failure':
time.sleep(60)
self.fail('Environment has incorrect status {0}'.
format(status))
time.sleep(5)
return self.application_catalog_client.\
get_environment(environment['id'])
def status_check(self, environment_id, configurations):
for configuration in configurations:
inst_name = configuration[0]
ports = configuration[1:]
ip = self.get_ip_by_instance_name(environment_id, inst_name)
if ip and ports:
for port in ports:
self.check_port_access(ip, port)
else:
self.fail('Instance does not have floating IP')
def check_path(self, environment_id, path, inst_name=None):
environment = self.application_catalog_client.\
get_environment(environment_id)
if inst_name:
ip = self.get_ip_by_instance_name(environment_id, inst_name)
else:
ip = environment.services[0]['instance']['floatingIpAddress']
resp = requests.get('http://{0}/{1}'.format(ip, path))
if resp.status_code == 200:
return resp
else:
self.fail("Service path unavailable")
def get_ip_by_instance_name(self, environment_id, inst_name):
for service in self.application_catalog_client.\
get_services_list(environment_id):
if inst_name in service['instance']['name']:
return service['instance']['floatingIpAddress']
def check_port_access(self, ip, port):
result = 1
start_time = time.time()
while time.time() - start_time < 600:
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
result = sock.connect_ex((str(ip), port))
sock.close()
if result == 0:
break
time.sleep(5)
self.assertEqual(0, result, '%s port is closed on instance' % port)
@classmethod
def create_volume(cls):
volume = cls.volumes_client.create_volume()['volume']
waiters.wait_for_volume_status(cls.volumes_client,
volume['id'], 'available')
return volume['id']
@classmethod
def delete_volume(cls, volume_id):
cls.volumes_client.delete_volume(volume_id)
is_volume_deleted = False
while not is_volume_deleted:
is_volume_deleted = cls.volumes_client.\
is_resource_deleted(volume_id)
time.sleep(1)
def create_snapshot(self, volume_id):
snapshot = self.snapshots_client.\
create_snapshot(volume_id=volume_id)['snapshot']
waiters.wait_for_snapshot_status(self.snapshots_client,
snapshot['id'], 'available')
return snapshot['id']
def delete_snapshot(self, snapshot_id):
self.snapshots_client.delete_snapshot(snapshot_id)
is_snapshot_deleted = False
while not is_snapshot_deleted:
is_snapshot_deleted = self.snapshots_client.\
is_resource_deleted(snapshot_id)
time.sleep(1)
def create_backup(self, volume_id):
backup = self.backups_client.create_backup(
volume_id=volume_id,
force=True)['backup']
self.backups_client.wait_for_backup_status(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)
def get_volume(self, environment_id):
stack = self.get_stack_id(environment_id)
stack_outputs = self.orchestration_client.\
show_stack(stack)['stack']['outputs']
for output in stack_outputs:
if 'vol' in output['output_key']:
volume_id = output['output_value']
return self.volumes_client.show_volume(volume_id)['volume']
def check_volume_attached(self, name, volume_id):
instance_id = self.get_instance_id(name)
attached_volumes = self.servers_client.\
list_volume_attachments(instance_id)['volumeAttachments']
assert attached_volumes[0]['id'] == volume_id
class BaseApplicationCatalogScenarioIsolatedAdminTest(
BaseApplicationCatalogScenarioTest):
@classmethod
def resource_setup(cls):
creds = cls.get_configured_isolated_creds(type_of_creds='admin')
cls.os = clients.Manager(credentials=creds)
cls.services_manager = services_manager(creds)
super(BaseApplicationCatalogScenarioIsolatedAdminTest, cls).\
resource_setup()

View File

@ -34,7 +34,8 @@ MANIFEST = {'Format': 'MuranoPL/1.0',
def compose_package(app_name, manifest, package_dir,
require=None, archive_dir=None, add_class_name=False):
require=None, archive_dir=None, add_class_name=False,
manifest_required=True):
"""Composes a murano package
Composes package `app_name` with `manifest` file as a template for the
@ -43,6 +44,7 @@ def compose_package(app_name, manifest, package_dir,
Puts the resulting .zip file into `acrhive_dir` if present or in the
`package_dir`.
"""
if manifest_required:
with open(manifest, 'w') as f:
fqn = 'io.murano.apps.' + app_name
mfest_copy = MANIFEST.copy()
@ -82,7 +84,8 @@ def compose_package(app_name, manifest, package_dir,
return archive_path, name
def prepare_package(name, require=None, add_class_name=False):
def prepare_package(name, require=None, add_class_name=False,
app='MockApp', manifest_required=True):
"""Prepare package.
:param name: Package name to compose
@ -90,13 +93,13 @@ def prepare_package(name, require=None, add_class_name=False):
:param add_class_name: Option to write class name to class file
:return: Path to archive, directory with archive, filename of archive
"""
app_dir = acquire_package_directory()
target_arc_path = app_dir.rsplit('MockApp', 1)[0]
app_dir = acquire_package_directory(app=app)
target_arc_path = app_dir.rsplit(app, 1)[0]
arc_path, filename = compose_package(
name, os.path.join(app_dir, 'manifest.yaml'),
app_dir, require=require, archive_dir=target_arc_path,
add_class_name=add_class_name)
add_class_name=add_class_name, manifest_required=manifest_required)
return arc_path, target_arc_path, filename
@ -111,7 +114,7 @@ def generate_name(prefix):
return '{0}_{1}'.format(prefix, suffix)
def acquire_package_directory():
def acquire_package_directory(app='MockApp'):
"""Obtain absolutely directory with package files.
Should be called inside tests dir.
@ -119,7 +122,7 @@ def acquire_package_directory():
"""
top_plugin_dir = os.path.realpath(os.path.join(os.getcwd(),
os.path.dirname(__file__)))
expected_package_dir = '/extras/MockApp'
expected_package_dir = '/extras/' + app
app_dir = top_plugin_dir + expected_package_dir
return app_dir