mirror of
https://github.com/apache/cloudstack.git
synced 2025-10-26 08:42:29 +01:00
The Extensions Framework in Apache CloudStack is designed to provide a flexible and standardised mechanism for integrating external systems and custom workflows into CloudStack’s orchestration process. By defining structured hook points during key operations—such as virtual machine deployment, resource preparation, and lifecycle events—the framework allows administrators and developers to extend CloudStack’s behaviour without modifying its core codebase.
481 lines
19 KiB
Python
481 lines
19 KiB
Python
# Licensed to the Apache Software Foundation (ASF) under one
|
|
# or more contributor license agreements. See the NOTICE file
|
|
# distributed with this work for additional information
|
|
# regarding copyright ownership. The ASF licenses this file
|
|
# to you under the Apache License, Version 2.0 (the
|
|
# "License"); you may not use this file except in compliance
|
|
# with the License. You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing,
|
|
# software distributed under the License is distributed on an
|
|
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
# KIND, either express or implied. See the License for the
|
|
# specific language governing permissions and limitations
|
|
# under the License.
|
|
""" BVT tests for extensions lifecycle functionalities
|
|
"""
|
|
# Import Local Modules
|
|
from marvin.cloudstackTestCase import cloudstackTestCase
|
|
from marvin.cloudstackAPI import (listInfrastructure,
|
|
listManagementServers)
|
|
from marvin.cloudstackException import CloudstackAPIException
|
|
from marvin.lib.base import (Extension,
|
|
Pod,
|
|
Cluster,
|
|
Host,
|
|
Configurations,
|
|
Template,
|
|
ServiceOffering,
|
|
NetworkOffering,
|
|
Network,
|
|
VirtualMachine)
|
|
from marvin.lib.common import (get_zone)
|
|
from marvin.lib.utils import (random_gen)
|
|
from marvin.sshClient import SshClient
|
|
from nose.plugins.attrib import attr
|
|
# Import System modules
|
|
import logging
|
|
from pathlib import Path
|
|
import random
|
|
import string
|
|
import time
|
|
|
|
_multiprocess_shared_ = True
|
|
|
|
CONFIG_EXTENSION_PATH_STATE_CHECK_NAME = "extension.path.state.check.interval"
|
|
|
|
class TestExtensions(cloudstackTestCase):
|
|
|
|
@classmethod
|
|
def setUpClass(cls):
|
|
testClient = super(TestExtensions, cls).getClsTestClient()
|
|
cls.apiclient = testClient.getApiClient()
|
|
cls.services = testClient.getParsedTestDataConfig()
|
|
|
|
# Get Zone
|
|
cls.zone = get_zone(cls.apiclient, testClient.getZoneForTests())
|
|
|
|
cls.mgtSvrDetails = cls.config.__dict__["mgtSvr"][0].__dict__
|
|
|
|
cls._cleanup = []
|
|
cls.logger = logging.getLogger('TestExtensions')
|
|
cls.logger.setLevel(logging.DEBUG)
|
|
|
|
@classmethod
|
|
def tearDownClass(cls):
|
|
super(TestExtensions, cls).tearDownClass()
|
|
|
|
def setUp(self):
|
|
self.changedConfigurations = {}
|
|
self.staticConfigurations = []
|
|
self.cleanup = []
|
|
|
|
def tearDown(self):
|
|
restartServer = False
|
|
for config in self.changedConfigurations:
|
|
value = self.changedConfigurations[config]
|
|
logging.info(f"Reverting value of config: {config} to {value}")
|
|
Configurations.update(self.apiclient,
|
|
config,
|
|
value=value)
|
|
if config in self.staticConfigurations:
|
|
restartServer = True
|
|
if restartServer:
|
|
self.restartAllManagementServers()
|
|
super(TestExtensions, self).tearDown()
|
|
|
|
def isManagementUp(self):
|
|
try:
|
|
self.apiclient.listInfrastructure(listInfrastructure.listInfrastructureCmd())
|
|
return True
|
|
except Exception:
|
|
return False
|
|
|
|
def getManagementServerIps(self):
|
|
if self.mgtSvrDetails["mgtSvrIp"] in ('localhost', '127.0.0.1'):
|
|
return None
|
|
cmd = listManagementServers.listManagementServersCmd()
|
|
servers = self.apiclient.listManagementServers(cmd)
|
|
active_server_ips = []
|
|
active_server_ips.append(self.mgtSvrDetails["mgtSvrIp"])
|
|
for idx, server in enumerate(servers):
|
|
if server.state == 'Up' and server.ipaddress != self.mgtSvrDetails["mgtSvrIp"]:
|
|
active_server_ips.append(server.ipaddress)
|
|
return active_server_ips
|
|
|
|
def restartAllManagementServers(self):
|
|
"""Restart all management servers
|
|
Assumes all servers have same username and password"""
|
|
server_ips = self.getManagementServerIps()
|
|
if server_ips is None:
|
|
self.staticConfigurations.clear()
|
|
self.fail(f"MS restarts cannot be done on {self.mgtSvrDetails['mgtSvrIp']}")
|
|
return False
|
|
logging.info("Restarting all management server")
|
|
for idx, server_ip in enumerate(server_ips):
|
|
logging.info(f"Restarting management server #{idx} with IP {server_ip}")
|
|
sshClient = SshClient(
|
|
server_ip,
|
|
22,
|
|
self.mgtSvrDetails["user"],
|
|
self.mgtSvrDetails["passwd"]
|
|
)
|
|
command = "service cloudstack-management stop"
|
|
sshClient.execute(command)
|
|
command = "service cloudstack-management start"
|
|
sshClient.execute(command)
|
|
if idx == 0:
|
|
# Wait before restarting other management servers to make the first as oldest running
|
|
time.sleep(10)
|
|
|
|
# Waits for management to come up in 10 mins, when it's up it will continue
|
|
timeout = time.time() + (10 * 60)
|
|
while time.time() < timeout:
|
|
if self.isManagementUp() is True: return True
|
|
time.sleep(5)
|
|
logging.info("Management server did not come up, failing")
|
|
return False
|
|
|
|
def changeConfiguration(self, name, value):
|
|
current_config = Configurations.list(self.apiclient, name=name)[0]
|
|
if current_config.value == str(value):
|
|
return
|
|
logging.info(f"Current value for config: {name} is {current_config.value}, changing it to {value}")
|
|
self.changedConfigurations[name] = current_config.value
|
|
if current_config.isdynamic == False:
|
|
self.staticConfigurations.append(name)
|
|
Configurations.update(self.apiclient,
|
|
name,
|
|
value=value)
|
|
|
|
def create_cluster(self):
|
|
pod_list = Pod.list(self.apiclient)
|
|
if len(pod_list) <= 0:
|
|
self.fail("No Pods found")
|
|
pod_id = pod_list[0].id
|
|
cluster_services = {
|
|
'clustername': 'C0-Test-' + random_gen(),
|
|
'clustertype': 'CloudManaged'
|
|
}
|
|
self.cluster = Cluster.create(
|
|
self.apiclient,
|
|
cluster_services,
|
|
self.zone.id,
|
|
pod_id,
|
|
'External'
|
|
)
|
|
self.cleanup.append(self.cluster)
|
|
|
|
def move_extension_path_locally(self, source, target):
|
|
try:
|
|
Path(source).rename(target)
|
|
except Exception as e:
|
|
self.fail(f"Failed to move extension path on localhost: {str(e)}")
|
|
|
|
def move_extension_path(self, path, restore=False):
|
|
logging.info(f"Moving path {path}")
|
|
source = path
|
|
target = f"{path}-backup"
|
|
if restore:
|
|
source = target
|
|
target = path
|
|
server_ips = self.getManagementServerIps()
|
|
if server_ips is None:
|
|
if self.mgtSvrDetails["mgtSvrIp"] in ('localhost', '127.0.0.1'):
|
|
self.move_extension_path_locally(source, target)
|
|
return
|
|
self.fail(f"Extension path cannot be moved on {cls.mgtSvrDetails['mgtSvrIp']}")
|
|
logging.info(f"Moving {path} on all management servers")
|
|
command = f"mv {source} {target}"
|
|
for idx, server_ip in enumerate(server_ips):
|
|
logging.info(f"Moving {path} on management server #{idx} with IP {server_ip}")
|
|
sshClient = SshClient(
|
|
server_ip,
|
|
22,
|
|
self.mgtSvrDetails["user"],
|
|
self.mgtSvrDetails["passwd"]
|
|
)
|
|
sshClient.execute(command)
|
|
|
|
@attr(tags=["devcloud", "advanced", "advancedns", "smoke", "basic", "sg"], required_hardware="false")
|
|
def test_01_create_extension(self):
|
|
name = random_gen()
|
|
type = 'Orchestrator'
|
|
details = [{}]
|
|
details[0]['abc'] = 'xyz'
|
|
self.extension = Extension.create(
|
|
self.apiclient,
|
|
name=name,
|
|
type=type,
|
|
details=details
|
|
)
|
|
self.cleanup.append(self.extension)
|
|
self.assertEqual(
|
|
name,
|
|
self.extension.name,
|
|
"Check extension name failed"
|
|
)
|
|
self.assertEqual(
|
|
type,
|
|
self.extension.type,
|
|
"Check extension type failed"
|
|
)
|
|
self.assertEqual(
|
|
'Enabled',
|
|
self.extension.state,
|
|
"Check extension state failed"
|
|
)
|
|
self.assertTrue(
|
|
self.extension.pathready,
|
|
"Check extension path ready failed"
|
|
)
|
|
extension_details = self.extension.details.__dict__
|
|
for k, v in details[0].items():
|
|
self.assertIn(k, extension_details, f"Key '{k}' should be present in details")
|
|
self.assertEqual(v, extension_details[k], f"Value for key '{k}' should be '{v}'")
|
|
|
|
@attr(tags=["devcloud", "advanced", "advancedns", "smoke", "basic", "sg"], required_hardware="false")
|
|
def test_02_create_extension_type_fail(self):
|
|
type = 'Random'
|
|
try:
|
|
self.extension = Extension.create(
|
|
self.apiclient,
|
|
name=random_gen(),
|
|
type=type
|
|
)
|
|
self.cleanup.append(self.extension)
|
|
self.fail(f"Unknown type: {type} extension created")
|
|
except CloudstackAPIException: pass
|
|
|
|
@attr(tags=["devcloud", "advanced", "advancedns", "smoke", "basic", "sg"], required_hardware="false")
|
|
def test_03_create_extension_name_fail(self):
|
|
name = random_gen()
|
|
self.extension = Extension.create(
|
|
self.apiclient,
|
|
name=name,
|
|
type='Orchestrator'
|
|
)
|
|
self.cleanup.append(self.extension)
|
|
try:
|
|
self.extension1 = Extension.create(
|
|
self.apiclient,
|
|
name=name,
|
|
type='Orchestrator'
|
|
)
|
|
self.cleanup.append(self.extension1)
|
|
self.fail(f"Same name: {name} extension created twice")
|
|
except CloudstackAPIException: pass
|
|
|
|
@attr(tags=["devcloud", "advanced", "advancedns", "smoke", "basic", "sg"], required_hardware="false")
|
|
def test_04_update_extension(self):
|
|
details = [{}]
|
|
details[0]['abc'] = 'xyz'
|
|
self.extension = Extension.create(
|
|
self.apiclient,
|
|
name=random_gen(),
|
|
type='Orchestrator',
|
|
description='Test description',
|
|
state='Disabled',
|
|
details=details
|
|
)
|
|
self.cleanup.append(self.extension)
|
|
|
|
details[0]['bca'] = 'yzx'
|
|
description = 'Updated test description'
|
|
state = 'Enabled'
|
|
updated_extension = self.extension.update(
|
|
self.apiclient,
|
|
description=description,
|
|
state=state,
|
|
details=details)
|
|
self.assertEqual(
|
|
description,
|
|
updated_extension.description,
|
|
"Check extension description"
|
|
)
|
|
self.assertEqual(
|
|
state,
|
|
updated_extension.state,
|
|
"Check extension state"
|
|
)
|
|
self.assertTrue(
|
|
updated_extension.details is not None,
|
|
"Check extension details not None"
|
|
)
|
|
updated_details = updated_extension.details.__dict__
|
|
for k, v in details[0].items():
|
|
self.assertIn(k, updated_details, f"Key '{k}' should be present in updated details")
|
|
self.assertEqual(v, updated_details[k], f"Value for key '{k}' should be '{v}'")
|
|
|
|
@attr(tags=["devcloud", "advanced", "advancedns", "smoke", "basic", "sg"], required_hardware="false")
|
|
def test_05_register_unregister_extension(self):
|
|
self.create_cluster()
|
|
self.extension = Extension.create(
|
|
self.apiclient,
|
|
name=random_gen(),
|
|
type='Orchestrator'
|
|
)
|
|
self.cleanup.append(self.extension)
|
|
registered_extension = self.extension.register(self.apiclient, self.cluster.id, 'Cluster')
|
|
resource_found = False
|
|
if registered_extension is not None and registered_extension.resources is not None and len(registered_extension.resources) > 0:
|
|
for resource in registered_extension.resources:
|
|
if resource.id == self.cluster.id and resource.type == 'Cluster':
|
|
resource_found = True
|
|
break
|
|
self.assertTrue(resource_found, "Registered resource not found")
|
|
self.extension.unregister(self.apiclient, self.cluster.id, 'Cluster')
|
|
|
|
@attr(tags=["devcloud", "advanced", "advancedns", "smoke", "basic", "sg"], required_hardware="false")
|
|
def test_06_register_extension_already_fail(self):
|
|
self.create_cluster()
|
|
self.extension = Extension.create(
|
|
self.apiclient,
|
|
name=random_gen(),
|
|
type='Orchestrator'
|
|
)
|
|
self.cleanup.append(self.extension)
|
|
registered_extension = self.extension.register(self.apiclient,self.cluster.id, 'Cluster')['extension']
|
|
self.extension1 = Extension.create(
|
|
self.apiclient,
|
|
name=random_gen(),
|
|
type='Orchestrator'
|
|
)
|
|
self.cleanup.append(self.extension1)
|
|
try:
|
|
self.extension1.register(self.apiclient, self.cluster.id, 'Cluster')
|
|
self.fail(f"Same cluster: {cluster.id} registered with two extensions")
|
|
except CloudstackAPIException: pass
|
|
|
|
@attr(tags=["devcloud", "advanced", "advancedns", "smoke", "basic", "sg"], required_hardware="false")
|
|
def test_07_delete_extension_registered_resource_fail(self):
|
|
self.create_cluster()
|
|
self.extension = Extension.create(
|
|
self.apiclient,
|
|
name=random_gen(),
|
|
type='Orchestrator'
|
|
)
|
|
self.cleanup.append(self.extension)
|
|
self.extension.register(self.apiclient, self.cluster.id, 'Cluster')
|
|
try:
|
|
self.extension.delete(self.apiclient, False)
|
|
self.fail("Deleted extensions which is registered to a resource")
|
|
except CloudstackAPIException: pass
|
|
|
|
@attr(tags=["devcloud", "advanced", "advancedns", "smoke", "basic", "sg"], required_hardware="false")
|
|
def test_08_extension_sync(self):
|
|
sync_task_interval = 60
|
|
if self.mgtSvrDetails["mgtSvrIp"] in ('localhost', '127.0.0.1'):
|
|
sync_config = Configurations.list(self.apiclient, name=CONFIG_EXTENSION_PATH_STATE_CHECK_NAME)[0]
|
|
if sync_config.value != str(sync_task_interval):
|
|
self.skipTest("Test running on localhost, MS cannot be restarted to change config")
|
|
self.extension = Extension.create(
|
|
self.apiclient,
|
|
name=random_gen(),
|
|
type='Orchestrator'
|
|
)
|
|
self.assertTrue(self.extension.pathready, "Path not ready")
|
|
self.changeConfiguration(CONFIG_EXTENSION_PATH_STATE_CHECK_NAME, sync_task_interval)
|
|
if len(self.staticConfigurations) > 0:
|
|
self.restartAllManagementServers()
|
|
self.move_extension_path(self.extension.path)
|
|
wait_multiple = 1.5
|
|
wait = wait_multiple * sync_task_interval
|
|
logging.info(f"Waiting for {wait_multiple}x{sync_task_interval} = {wait} seconds for background task to execute")
|
|
time.sleep(wait)
|
|
ext = Extension.list(
|
|
self.apiclient,
|
|
id=self.extension.id
|
|
)[0]
|
|
self.assertFalse(ext.pathready, "Path is still ready")
|
|
self.move_extension_path(self.extension.path, True)
|
|
logging.info(f"Waiting for {wait_multiple}x{sync_task_interval} = {wait} seconds for background task to execute")
|
|
time.sleep(wait)
|
|
ext = Extension.list(
|
|
self.apiclient,
|
|
id=self.extension.id
|
|
)[0]
|
|
self.assertTrue(ext.pathready, "Path not ready")
|
|
|
|
@attr(tags=["devcloud", "advanced", "advancedns", "smoke", "basic", "sg"], required_hardware="false")
|
|
def test_09_extension_deploy_vm(self):
|
|
self.create_cluster()
|
|
self.extension = Extension.create(
|
|
self.apiclient,
|
|
name=random_gen(),
|
|
type='Orchestrator'
|
|
)
|
|
self.cleanup.append(self.extension)
|
|
self.extension.register(self.apiclient, self.cluster.id, 'Cluster')
|
|
details = {
|
|
'url': random_gen(),
|
|
'zoneid': self.cluster.zoneid,
|
|
'podid': self.cluster.podid,
|
|
'username': 'External',
|
|
'password': 'External'
|
|
}
|
|
self.host = Host.create(
|
|
self.apiclient,
|
|
self.cluster,
|
|
details,
|
|
hypervisor=self.cluster.hypervisortype
|
|
)
|
|
self.cleanup.append(self.host)
|
|
template_name = "ext-" + random_gen()
|
|
template_data = {
|
|
"name": template_name,
|
|
"displaytext": template_name,
|
|
"format": self.host.hypervisor,
|
|
"hypervisor": self.host.hypervisor,
|
|
"ostype": "Other Linux (64-bit)",
|
|
"url": template_name,
|
|
"requireshvm": "True",
|
|
"ispublic": "True",
|
|
"isextractable": "True",
|
|
"extensionid": self.extension.id
|
|
}
|
|
self.template = Template.register(
|
|
self.apiclient,
|
|
template_data,
|
|
zoneid=self.zone.id,
|
|
hypervisor=self.cluster.hypervisortype
|
|
)
|
|
self.cleanup.append(self.template)
|
|
logging.info("Waiting for 3 seconds for template to be ready")
|
|
time.sleep(3)
|
|
self.compute_offering = ServiceOffering.create(
|
|
self.apiclient,
|
|
self.services["service_offerings"]["tiny"])
|
|
self.cleanup.append(self.compute_offering)
|
|
self.network_offering = NetworkOffering.create(
|
|
self.apiclient,
|
|
self.services["l2-network_offering"],
|
|
)
|
|
self.cleanup.append(self.network_offering)
|
|
self.network_offering.update(self.apiclient, state='Enabled')
|
|
self.services["network"]["networkoffering"] = self.network_offering.id
|
|
self.l2_network = Network.create(
|
|
self.apiclient,
|
|
self.services["l2-network"],
|
|
zoneid=self.zone.id,
|
|
networkofferingid=self.network_offering.id
|
|
)
|
|
self.cleanup.append(self.l2_network)
|
|
self.services["virtual_machine"]["zoneid"] = self.zone.id
|
|
self.services["virtual_machine"]["template"] = self.template.id
|
|
self.virtual_machine = VirtualMachine.create(
|
|
self.apiclient,
|
|
self.services["virtual_machine"],
|
|
templateid=self.template.id,
|
|
serviceofferingid=self.compute_offering.id,
|
|
networkids=[self.l2_network.id]
|
|
)
|
|
self.cleanup.append(self.virtual_machine)
|
|
self.assertEqual(
|
|
self.virtual_machine.state,
|
|
'Running',
|
|
"VM not in Running state"
|
|
)
|