mirror of https://github.com/apache/cloudstack.git
620 lines
20 KiB
Python
620 lines
20 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 custom extension
|
|
"""
|
|
# Import Local Modules
|
|
from marvin.cloudstackTestCase import cloudstackTestCase
|
|
from marvin.cloudstackAPI import listManagementServers
|
|
from marvin.lib.base import (Extension,
|
|
Pod,
|
|
Cluster,
|
|
Host,
|
|
Template,
|
|
ServiceOffering,
|
|
NetworkOffering,
|
|
Network,
|
|
VirtualMachine,
|
|
ExtensionCustomAction)
|
|
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 json
|
|
import logging
|
|
from pathlib import Path
|
|
import random
|
|
import string
|
|
import time
|
|
|
|
_multiprocess_shared_ = True
|
|
|
|
CUSTOM_EXTENSION_CONTENT = """#!/bin/bash
|
|
|
|
parse_json() {
|
|
local json_string=$1
|
|
echo "$json_string" | jq '.' > /dev/null || { echo '{"error":"Invalid JSON input"}'; exit 1; }
|
|
}
|
|
|
|
get_vm_name() {
|
|
local input_json=$1
|
|
local name
|
|
name=$(jq -r '.virtualmachinename' <<< "$input_json")
|
|
if [[ -z "$name" || "$name" == "null" ]]; then
|
|
echo '{"status":"error","message":"virtualmachinename missing in JSON"}'
|
|
exit 1
|
|
fi
|
|
echo "$name"
|
|
}
|
|
|
|
get_vm_file() {
|
|
local name=$1
|
|
local script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
echo "$script_dir/$name"
|
|
}
|
|
|
|
validate_vm_file_exists() {
|
|
local file=$1
|
|
if [[ ! -f "$file" ]]; then
|
|
echo '{"status":"error","message":"Instance file not found"}'
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
update_vm_status() {
|
|
local file=$1
|
|
local status=$2
|
|
local updated_json
|
|
updated_json=$(jq --arg status "$status" '.status = $status' "$file")
|
|
echo "$updated_json" > "$file"
|
|
}
|
|
|
|
prepare() {
|
|
echo ""
|
|
}
|
|
|
|
create() {
|
|
parse_json "$1"
|
|
local vm_name updated_json file
|
|
vm_name=$(get_vm_name "$1")
|
|
file=$(get_vm_file "$vm_name")
|
|
|
|
updated_json=$(jq '. + {status: "Running"}' <<< "$1")
|
|
echo "$updated_json" > "$file"
|
|
|
|
jq -n --arg file "$file" \
|
|
'{status: "success", message: "Instance created", file: $file}'
|
|
}
|
|
|
|
delete() {
|
|
parse_json "$1"
|
|
local vm_name file
|
|
vm_name=$(get_vm_name "$1")
|
|
file=$(get_vm_file "$vm_name")
|
|
|
|
if [[ -f "$file" ]]; then
|
|
rm -f "$file"
|
|
fi
|
|
jq -n --arg file "$file" \
|
|
'{status: "success", message: "Instance deleted", file: $file}'
|
|
}
|
|
|
|
start() {
|
|
parse_json "$1"
|
|
local vm_name file
|
|
vm_name=$(get_vm_name "$1")
|
|
file=$(get_vm_file "$vm_name")
|
|
validate_vm_file_exists "$file"
|
|
|
|
update_vm_status "$file" "Running"
|
|
echo '{"status": "success", "message": "Instance started"}'
|
|
}
|
|
|
|
stop() {
|
|
parse_json "$1"
|
|
local vm_name file
|
|
vm_name=$(get_vm_name "$1")
|
|
file=$(get_vm_file "$vm_name")
|
|
|
|
if [[ -f "$file" ]]; then
|
|
update_vm_status "$file" "Stopped"
|
|
fi
|
|
echo '{"status": "success", "message": "Instance stopped"}'
|
|
}
|
|
|
|
reboot() {
|
|
parse_json "$1"
|
|
local vm_name file
|
|
vm_name=$(get_vm_name "$1")
|
|
file=$(get_vm_file "$vm_name")
|
|
validate_vm_file_exists "$file"
|
|
|
|
update_vm_status "$file" "Running"
|
|
echo '{"status": "success", "message": "Instance rebooted"}'
|
|
}
|
|
|
|
status() {
|
|
parse_json "$1"
|
|
local vm_name file vm_status
|
|
vm_name=$(get_vm_name "$1")
|
|
file=$(get_vm_file "$vm_name")
|
|
validate_vm_file_exists "$file"
|
|
|
|
vm_status=$(jq -r '.status' "$file")
|
|
[[ -z "$vm_status" || "$vm_status" == "null" ]] && vm_status="unknown"
|
|
|
|
case "${vm_status,,}" in
|
|
"running"|"poweron")
|
|
power_state="PowerOn"
|
|
;;
|
|
"stopped"|"shutdown"|"poweroff")
|
|
power_state="PowerOff"
|
|
;;
|
|
*)
|
|
power_state="$vm_status"
|
|
;;
|
|
esac
|
|
|
|
jq -n --arg ps "$power_state" '{status: "success", power_state: $ps}'
|
|
}
|
|
|
|
testaction() {
|
|
parse_json "$1"
|
|
local vm_name param_val
|
|
vm_name=$(get_vm_name "$1")
|
|
param_val=$(jq -r '.parameters.Name' <<< "$1")
|
|
|
|
echo "$param_val for $vm_name"
|
|
}
|
|
|
|
action=$1
|
|
parameters_file="$2"
|
|
wait_time="$3"
|
|
|
|
if [[ -z "$action" || -z "$parameters_file" ]]; then
|
|
echo '{"error":"Missing required arguments"}'
|
|
exit 1
|
|
fi
|
|
|
|
if [[ ! -r "$parameters_file" ]]; then
|
|
echo '{"error":"File not found or unreadable"}'
|
|
exit 1
|
|
fi
|
|
|
|
parameters=$(<"$parameters_file")
|
|
|
|
case $action in
|
|
prepare) prepare "$parameters" ;;
|
|
create) create "$parameters" ;;
|
|
delete) delete "$parameters" ;;
|
|
start) start "$parameters" ;;
|
|
stop) stop "$parameters" ;;
|
|
reboot) reboot "$parameters" ;;
|
|
status) status "$parameters" ;;
|
|
testaction) testaction "$parameters" ;;
|
|
*) echo '{"error":"Invalid action"}'; exit 1 ;;
|
|
esac
|
|
|
|
exit 0
|
|
|
|
"""
|
|
|
|
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)
|
|
|
|
cls.compute_offering = ServiceOffering.create(
|
|
cls.apiclient,
|
|
cls.services["service_offerings"]["tiny"])
|
|
cls._cleanup.append(cls.compute_offering)
|
|
cls.network_offering = NetworkOffering.create(
|
|
cls.apiclient,
|
|
cls.services["l2-network_offering"],
|
|
)
|
|
cls._cleanup.append(cls.network_offering)
|
|
cls.network_offering.update(cls.apiclient, state='Enabled')
|
|
cls.services["network"]["networkoffering"] = cls.network_offering.id
|
|
cls.l2_network = Network.create(
|
|
cls.apiclient,
|
|
cls.services["l2-network"],
|
|
zoneid=cls.zone.id,
|
|
networkofferingid=cls.network_offering.id
|
|
)
|
|
cls._cleanup.append(cls.l2_network)
|
|
|
|
cls.resource_name_suffix = random_gen()
|
|
cls.create_cluster()
|
|
cls.extension = Extension.create(
|
|
cls.apiclient,
|
|
name=f"ext-{cls.resource_name_suffix}",
|
|
type='Orchestrator'
|
|
)
|
|
cls._cleanup.append(cls.extension)
|
|
cls.update_extension_path(cls.extension.path)
|
|
cls.extension.register(cls.apiclient, cls.cluster.id, 'Cluster')
|
|
details = {
|
|
'url': f"host-{cls.resource_name_suffix}",
|
|
'zoneid': cls.cluster.zoneid,
|
|
'podid': cls.cluster.podid,
|
|
'username': 'External',
|
|
'password': 'External'
|
|
}
|
|
cls.host = Host.create(
|
|
cls.apiclient,
|
|
cls.cluster,
|
|
details,
|
|
hypervisor=cls.cluster.hypervisortype
|
|
)
|
|
cls._cleanup.append(cls.host)
|
|
template_name = f"template-{cls.resource_name_suffix}"
|
|
template_data = {
|
|
"name": template_name,
|
|
"displaytext": template_name,
|
|
"format": cls.host.hypervisor,
|
|
"hypervisor": cls.host.hypervisor,
|
|
"ostype": "Other Linux (64-bit)",
|
|
"url": template_name,
|
|
"requireshvm": "True",
|
|
"ispublic": "True",
|
|
"isextractable": "True",
|
|
"extensionid": cls.extension.id
|
|
}
|
|
cls.template = Template.register(
|
|
cls.apiclient,
|
|
template_data,
|
|
zoneid=cls.zone.id,
|
|
hypervisor=cls.cluster.hypervisortype
|
|
)
|
|
cls._cleanup.append(cls.template)
|
|
logging.info("Waiting for 3 seconds for template to be ready")
|
|
time.sleep(3)
|
|
cls.services["virtual_machine"]["zoneid"] = cls.zone.id
|
|
cls.services["virtual_machine"]["template"] = cls.template.id
|
|
|
|
@classmethod
|
|
def tearDownClass(cls):
|
|
super(TestExtensions, cls).tearDownClass()
|
|
|
|
@classmethod
|
|
def create_cluster(cls):
|
|
pod_list = Pod.list(cls.apiclient)
|
|
if len(pod_list) <= 0:
|
|
cls.fail("No Pods found")
|
|
pod_id = pod_list[0].id
|
|
cluster_services = {
|
|
'clustername': 'cluster-' + cls.resource_name_suffix,
|
|
'clustertype': 'CloudManaged'
|
|
}
|
|
cls.cluster = Cluster.create(
|
|
cls.apiclient,
|
|
cluster_services,
|
|
cls.zone.id,
|
|
pod_id,
|
|
'External'
|
|
)
|
|
cls._cleanup.append(cls.cluster)
|
|
|
|
@classmethod
|
|
def getManagementServerIps(cls):
|
|
if cls.mgtSvrDetails["mgtSvrIp"] in ('localhost', '127.0.0.1'):
|
|
return None
|
|
cmd = listManagementServers.listManagementServersCmd()
|
|
servers = cls.apiclient.listManagementServers(cmd)
|
|
active_server_ips = []
|
|
active_server_ips.append(cls.mgtSvrDetails["mgtSvrIp"])
|
|
for idx, server in enumerate(servers):
|
|
if server.state == 'Up' and server.ipaddress != cls.mgtSvrDetails["mgtSvrIp"]:
|
|
active_server_ips.append(server.ipaddress)
|
|
return active_server_ips
|
|
|
|
@classmethod
|
|
def update_path_locally(cls, path):
|
|
try:
|
|
file = Path(path)
|
|
file.write_text(CUSTOM_EXTENSION_CONTENT)
|
|
file.chmod(file.stat().st_mode | 0o111) # Make executable
|
|
except Exception as e:
|
|
cls.fail(f"Failed to update path on localhost: {str(e)}")
|
|
|
|
@classmethod
|
|
def update_extension_path(cls, path):
|
|
logging.info(f"Updating extension path {path}")
|
|
server_ips = cls.getManagementServerIps()
|
|
if server_ips is None:
|
|
if cls.mgtSvrDetails["mgtSvrIp"] in ('localhost', '127.0.0.1'):
|
|
cls.update_path_locally(path)
|
|
return
|
|
cls.fail(f"Extension path update cannot be done on {cls.mgtSvrDetails['mgtSvrIp']}")
|
|
logging.info("Updating extension path on all management server")
|
|
command = (
|
|
f"cat << 'EOF' > {path}\n{CUSTOM_EXTENSION_CONTENT}\nEOF\n"
|
|
f"chmod +x {path}"
|
|
)
|
|
for idx, server_ip in enumerate(server_ips):
|
|
logging.info(f"Updating extension path on management server #{idx} with IP {server_ip}")
|
|
sshClient = SshClient(
|
|
server_ip,
|
|
22,
|
|
cls.mgtSvrDetails["user"],
|
|
cls.mgtSvrDetails["passwd"]
|
|
)
|
|
sshClient.execute(command)
|
|
|
|
def setUp(self):
|
|
self.cleanup = []
|
|
|
|
def tearDown(self):
|
|
super(TestExtensions, self).tearDown()
|
|
|
|
def get_vm_content_path(self, extension_path, vm_name):
|
|
directory = extension_path.rsplit('/', 1)[0] if '/' in extension_path else '.'
|
|
path = f"{directory}/{vm_name}"
|
|
return path
|
|
|
|
def get_vm_content_locally(self, path):
|
|
try:
|
|
file = Path(path)
|
|
if not file.exists():
|
|
return None
|
|
return file.read_text().strip()
|
|
except Exception:
|
|
return None
|
|
|
|
def get_vm_content(self, extension_path, vm_name):
|
|
path = self.get_vm_content_path(extension_path, vm_name)
|
|
server_ips = self.getManagementServerIps()
|
|
if not server_ips:
|
|
if self.mgtSvrDetails["mgtSvrIp"] in ('localhost', '127.0.0.1'):
|
|
return self.get_vm_content_locally(path)
|
|
return None
|
|
logging.info("Trying to get VM content from all management server")
|
|
command = f"cat {path}"
|
|
for idx, server_ip in enumerate(server_ips):
|
|
logging.info(f"Trying to get VM content from management server #{idx} with IP {server_ip}")
|
|
sshClient = SshClient(
|
|
server_ip,
|
|
22,
|
|
self.mgtSvrDetails["user"],
|
|
self.mgtSvrDetails["passwd"]
|
|
)
|
|
results = sshClient.execute(command)
|
|
if isinstance(results, list) and len(results) > 0:
|
|
return '\n'.join(line.strip() for line in results)
|
|
return None
|
|
|
|
def check_vm_content_values(self, content):
|
|
if content is None:
|
|
self.fail("VM content is empty")
|
|
|
|
try:
|
|
data = json.loads(content)
|
|
except json.JSONDecodeError as e:
|
|
self.fail(f"Invalid JSON for the VM: {e}")
|
|
|
|
if not data:
|
|
self.fail("Empty JSON for the VM")
|
|
|
|
required_keys = ['externaldetails', 'cloudstack.vm.details', 'virtualmachineid', 'virtualmachinename', 'status']
|
|
if not all(key in data for key in required_keys):
|
|
self.fail("Missing one or more required keys.")
|
|
|
|
memory = self.compute_offering.memory * 1024 * 1024
|
|
vm_details = data['cloudstack.vm.details']
|
|
|
|
self.assertEqual(
|
|
memory,
|
|
vm_details['minRam'],
|
|
"VM memory mismatch"
|
|
)
|
|
self.assertEqual(
|
|
self.compute_offering.cpunumber,
|
|
vm_details['cpus'],
|
|
"VM CPU count mismatch"
|
|
)
|
|
self.assertEqual(
|
|
'Running',
|
|
data['status'],
|
|
"VM status mismatch"
|
|
)
|
|
|
|
def check_vm_status_from_content(self, extension_path, vm_name, status):
|
|
content = self.get_vm_content(extension_path, vm_name)
|
|
if content is None:
|
|
self.fail("VM content is empty")
|
|
|
|
try:
|
|
data = json.loads(content)
|
|
except json.JSONDecodeError as e:
|
|
self.fail(f"Invalid JSON for the VM: {e}")
|
|
|
|
if not data:
|
|
self.fail("Empty JSON for the VM")
|
|
|
|
self.assertEqual(
|
|
status,
|
|
data['status'],
|
|
"VM status mismatch"
|
|
)
|
|
|
|
def check_vm_content_exist_locally(self, path):
|
|
try:
|
|
file = Path(path)
|
|
return file.exists()
|
|
except Exception:
|
|
return None
|
|
|
|
def check_vm_content_exist(self, extension_path, vm_name):
|
|
path = self.get_vm_content_path(extension_path, vm_name)
|
|
server_ips = self.getManagementServerIps()
|
|
if not server_ips:
|
|
if self.mgtSvrDetails["mgtSvrIp"] in ('localhost', '127.0.0.1'):
|
|
return self.check_vm_content_exist_locally(path)
|
|
return None
|
|
logging.info("Trying to check VM content exists from all management server")
|
|
command = f"test -e '{path}' && echo EXISTS || echo MISSING"
|
|
for idx, server_ip in enumerate(server_ips):
|
|
logging.info(f"Trying to check VM content exists from management server #{idx} with IP {server_ip}")
|
|
sshClient = SshClient(
|
|
server_ip,
|
|
22,
|
|
self.mgtSvrDetails["user"],
|
|
self.mgtSvrDetails["passwd"]
|
|
)
|
|
results = sshClient.execute(command)
|
|
if results is not None and results[0].strip() == "EXISTS":
|
|
return True
|
|
return False
|
|
|
|
def popItemFromCleanup(self, item_id):
|
|
for idx, x in enumerate(self.cleanup):
|
|
if x.id == item_id:
|
|
self.cleanup.pop(idx)
|
|
break
|
|
|
|
@attr(tags=["devcloud", "advanced", "advancedns", "smoke", "basic", "sg"], required_hardware="false")
|
|
def test_01_extension_vm_lifecycle(self):
|
|
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"
|
|
)
|
|
content = self.get_vm_content(self.extension.path, self.virtual_machine.instancename)
|
|
self.check_vm_content_values(content)
|
|
self.virtual_machine.stop(self.apiclient)
|
|
self.check_vm_status_from_content(self.extension.path, self.virtual_machine.instancename, 'Stopped')
|
|
self.virtual_machine.start(self.apiclient)
|
|
self.check_vm_status_from_content(self.extension.path, self.virtual_machine.instancename, 'Running')
|
|
self.virtual_machine.reboot(self.apiclient)
|
|
self.check_vm_status_from_content(self.extension.path, self.virtual_machine.instancename, 'Running')
|
|
self.virtual_machine.delete(self.apiclient, expunge=True)
|
|
self.popItemFromCleanup(self.virtual_machine.id)
|
|
self.assertFalse(
|
|
self.check_vm_content_exist(self.extension.path, self.virtual_machine.instancename),
|
|
"VM content exist event after expunge"
|
|
)
|
|
|
|
@attr(tags=["devcloud", "advanced", "advancedns", "smoke", "basic", "sg"], required_hardware="false")
|
|
def test_02_run_custom_action(self):
|
|
name = 'testaction'
|
|
details = [{}]
|
|
details[0]['abc'] = 'xyz'
|
|
parameters = [{}]
|
|
parameter0 = {
|
|
'name': 'Name',
|
|
'type': 'STRING',
|
|
'validationformat': 'NONE',
|
|
'required': True
|
|
}
|
|
parameters[0] = parameter0
|
|
self.custom_action = ExtensionCustomAction.create(
|
|
self.apiclient,
|
|
extensionid=self.extension.id,
|
|
name=name,
|
|
enabled=True,
|
|
details=details,
|
|
parameters=parameters,
|
|
successmessage='Successfully completed {{actionName}}'
|
|
)
|
|
self.cleanup.append(self.custom_action)
|
|
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)
|
|
param_val=random_gen()
|
|
run_parameters = [{}]
|
|
run_parameters[0] = {
|
|
'Name': param_val
|
|
}
|
|
run_response = self.custom_action.run(
|
|
self.apiclient,
|
|
resourceid=self.virtual_machine.id,
|
|
parameters=run_parameters
|
|
)
|
|
self.assertTrue(
|
|
run_response.success,
|
|
"Action run status not success"
|
|
)
|
|
self.assertEquals(
|
|
f"Successfully completed {name}",
|
|
run_response.result.message,
|
|
"Action run status not success"
|
|
)
|
|
data = run_response.result.details
|
|
self.assertEquals(
|
|
f"{param_val} for {self.virtual_machine.instancename}",
|
|
run_response.result.details,
|
|
"Action response details not match"
|
|
)
|
|
|
|
@attr(tags=["devcloud", "advanced", "advancedns", "smoke", "basic", "sg"], required_hardware="false")
|
|
def test_03_run_invalid_custom_action(self):
|
|
name = 'invalidaction'
|
|
self.custom_action = ExtensionCustomAction.create(
|
|
self.apiclient,
|
|
extensionid=self.extension.id,
|
|
name=name,
|
|
enabled=True,
|
|
errormessage='Failed {{actionName}}'
|
|
)
|
|
self.cleanup.append(self.custom_action)
|
|
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)
|
|
run_response = self.custom_action.run(
|
|
self.apiclient,
|
|
resourceid=self.virtual_machine.id
|
|
)
|
|
self.assertFalse(
|
|
run_response.success,
|
|
"Action run status not failure"
|
|
)
|
|
self.assertEquals(
|
|
f"Failed {name}",
|
|
run_response.result.message,
|
|
"Action run status not success"
|
|
)
|