diff --git a/.gitignore b/.gitignore index e3d7eed..4ed04dd 100644 --- a/.gitignore +++ b/.gitignore @@ -66,4 +66,5 @@ qpm.ini # zip files *.zip SandboxOrchestration/environment_scripts/env_setup/data.json - +sandbox_scripts\QualiEnvironmentUtils\tests\DebugInteractive_setup_resources.py +sandbox_scripts\QualiEnvironmentUtils\tests\DebugInteractive_teardown_resources.py diff --git a/SandboxOrchestrationPackage/Topology Scripts/Default Sandbox Setup.zip b/SandboxOrchestrationPackage/Topology Scripts/Default Sandbox Setup.zip index 38b1e9b..ad5c137 100644 Binary files a/SandboxOrchestrationPackage/Topology Scripts/Default Sandbox Setup.zip and b/SandboxOrchestrationPackage/Topology Scripts/Default Sandbox Setup.zip differ diff --git a/SandboxOrchestrationPackage/Topology Scripts/Default Sandbox Teardown.zip b/SandboxOrchestrationPackage/Topology Scripts/Default Sandbox Teardown.zip index 8521e55..7ab80e1 100644 Binary files a/SandboxOrchestrationPackage/Topology Scripts/Default Sandbox Teardown.zip and b/SandboxOrchestrationPackage/Topology Scripts/Default Sandbox Teardown.zip differ diff --git a/pack.bat b/pack.bat index 90ecc72..65c78d2 100644 --- a/pack.bat +++ b/pack.bat @@ -1,4 +1,6 @@ @echo off -python -m pip install qpm --no-cache-dir --upgrade +Path = %Path%;C:\Python27 +echo %Path% +python.exe -m pip install qpm --no-cache-dir --upgrade copy version.txt SandboxOrchestrationPackage/version.txt /Y -python -m qpm pack --package_name SandboxOrchestration +python.exe -m qpm pack --package_name SandboxOrchestration diff --git a/sandbox_scripts/QualiEnvironmentUtils/Resource.py b/sandbox_scripts/QualiEnvironmentUtils/Resource.py index bb19299..b3a9f2a 100644 --- a/sandbox_scripts/QualiEnvironmentUtils/Resource.py +++ b/sandbox_scripts/QualiEnvironmentUtils/Resource.py @@ -5,9 +5,9 @@ from cloudshell.api.cloudshell_api import * from cloudshell.api.common_cloudshell_api import * from QualiUtils import * -import datetime +import datetime, time import json - +from time import sleep class ResourceBase(object): def __init__(self, resource_name, resource_alias=''): @@ -96,23 +96,28 @@ def get_neighbors(self, reservation_id): # ---------------------------------- # ---------------------------------- - def health_check(self,reservation_id): + def health_check(self,reservation_id, health_check_attempts=1): """ - Run the healthCheck command on all the devices + Run the healthCheck command on the device :param str reservation_id: Reservation id. """ if self.has_command('health_check'): - try: - # Return a detailed description in case of a failure - out = self.execute_command(reservation_id, 'health_check', printOutput=False) - if out.Output.find(' passed') == -1: - err = "Health check did not pass for device " + self.name + ". " + out.Output + for attempts in range(0, int(health_check_attempts)): + try: + # Return a detailed description in case of a failure + out = self.execute_command(reservation_id, 'health_check', printOutput=True) #.Output() + if out.Output.find(' passed') == -1 and attempts == (int(health_check_attempts) -1): + err = "Health check did not pass for device " + self.name + ". " + out.Output + return err + if out.Output.find(' passed') == -1: + time.sleep(30) + else: + return "" + except QualiError as qe: + err = "Health check did not pass for device " + self.name + ". " + str(qe) return err - - except QualiError as qe: - err = "Health check did not pass for device " + self.name + ". " + str(qe) - return err - return "" + else: + return "" # ----------------------------------------- # ----------------------------------------- diff --git a/sandbox_scripts/QualiEnvironmentUtils/Sandbox.py b/sandbox_scripts/QualiEnvironmentUtils/Sandbox.py index ff74b35..28415bb 100644 --- a/sandbox_scripts/QualiEnvironmentUtils/Sandbox.py +++ b/sandbox_scripts/QualiEnvironmentUtils/Sandbox.py @@ -27,7 +27,7 @@ def __init__(self, reservation_id, logger): self.owner = context.owner_user self.Blueprint_name = context.environment_name if self.Blueprint_name == '': - raise QualiError("Blueprint name empty (from env name)") + raise QualiError("NameError","Blueprint name empty (from env name)") full_path = None tp = self.api_session.GetActiveTopologyNames() @@ -244,6 +244,7 @@ def clear_all_resources_live_status(self): """ Clear the live status from all the devices """ + #TODO change to honor ignor_models root_resources = self.get_root_resources() for resource in root_resources: self.api_session.SetResourceLiveStatus(resource.name, liveStatusName="Info", diff --git a/sandbox_scripts/QualiEnvironmentUtils/tests/DebugInteractive_setup_resources.py b/sandbox_scripts/QualiEnvironmentUtils/tests/DebugInteractive_setup_resources.py index ba24f24..bb1f6b8 100644 --- a/sandbox_scripts/QualiEnvironmentUtils/tests/DebugInteractive_setup_resources.py +++ b/sandbox_scripts/QualiEnvironmentUtils/tests/DebugInteractive_setup_resources.py @@ -1,10 +1,13 @@ import cloudshell.helpers.scripts.cloudshell_dev_helpers as dev_helpers from sandbox_scripts.environment.setup.setup_resources import * -dev_helpers.attach_to_cloudshell_as(user="admin", password="admin", domain="Global", - reservation_id="c04d3da4-8025-4efe-9f4d-820ba19d20af", - server_address="localhost") -os.environ["environment_name"] = "Abstract-ALL" +# change line 155....in helpers to environment_name= os.environ["environmentName"], +os.environ["environmentName"] = "Just1Res" + + +dev_helpers.attach_to_cloudshell_as(user="admin", password="xxxx", domain="Global", + reservation_id="ad024811-7528-42eb-b2c4-d50003951278", + server_address="svl-dev-quali") x = EnvironmentSetupResources() x.execute() diff --git a/sandbox_scripts/QualiEnvironmentUtils/tests/DebugInteractive_teardown_resources.py b/sandbox_scripts/QualiEnvironmentUtils/tests/DebugInteractive_teardown_resources.py index ef0874a..1b99f0a 100644 --- a/sandbox_scripts/QualiEnvironmentUtils/tests/DebugInteractive_teardown_resources.py +++ b/sandbox_scripts/QualiEnvironmentUtils/tests/DebugInteractive_teardown_resources.py @@ -1,8 +1,12 @@ import cloudshell.helpers.scripts.cloudshell_dev_helpers as dev_helpers from sandbox_scripts.environment.teardown.teardown_resources import * +import os -dev_helpers.attach_to_cloudshell_as(user="admin", password="xx", domain="Global", - reservation_id="bc0517e5-7240-4184-b04d-19e755f9c9a7", + +os.environ["environmentName"] = "Just1Res" + +dev_helpers.attach_to_cloudshell_as(user="admin", password="xxxx", domain="Global", + reservation_id="2bc2e45f-63e6-41cb-a8f5-fe34e042a75e", server_address="svl-dev-quali") x = EnvironmentTeardownResources() diff --git a/sandbox_scripts/QualiEnvironmentUtils/tests/test_Resource.py b/sandbox_scripts/QualiEnvironmentUtils/tests/test_Resource.py index 86e4c63..4794504 100644 --- a/sandbox_scripts/QualiEnvironmentUtils/tests/test_Resource.py +++ b/sandbox_scripts/QualiEnvironmentUtils/tests/test_Resource.py @@ -176,6 +176,39 @@ def test_health_check_failed(self): ret = self.resource.health_check("5487c6ce-d0b3-43e9-8ee7-e27af8406905") self.assertEqual('Health check did not pass for device r1. Health check failed',ret) + @patch('time.sleep') + def test_health_check_failed_twice(self, mock_time): + command1 = Mock() + command1.Name = 'health_check' + self.resource.commands = [command1] + + rd = Mock() + rd.Output = "Health check failed" + self.mock_api_session.return_value.ExecuteCommand = Mock(return_value=rd) + ret = self.resource.health_check("5487c6ce-d0b3-43e9-8ee7-e27af8406905", health_check_attempts=2) + self.assertEqual('Health check did not pass for device r1. Health check failed',ret) + self.assertEqual(mock_time.call_count,1) + + @patch('time.sleep') + def test_health_check_failed_once_succeeds_on_second(self, mock_time): + command1 = Mock() + command1.Name = 'health_check' + self.resource.commands = [command1] + self.count = 0 + def execute_command_return_value(res_id, resource_name, resource_type, command_name, inputs, print_output): + rd = Mock() + if self.count == 0: + rd.Output = "Health check failed" + self.count += 1 + else: + rd.Output = "Health check passed" + return rd + + self.mock_api_session.return_value.ExecuteCommand.side_effect = execute_command_return_value + ret = self.resource.health_check("5487c6ce-d0b3-43e9-8ee7-e27af8406905", health_check_attempts=2) + self.assertEqual('',ret, "command was expected to be pass but wasn't") + self.assertEqual(mock_time.call_count,1) + def test_health_check_not_found(self): ret = self.resource.health_check("5487c6ce-d0b3-43e9-8ee7-e27af8406905") self.assertEqual('',ret, "command was expected to be found but wasn't") diff --git a/sandbox_scripts/environment/setup/setup_script.py b/sandbox_scripts/environment/setup/setup_script.py index 95d0e9a..0ed694d 100644 --- a/sandbox_scripts/environment/setup/setup_script.py +++ b/sandbox_scripts/environment/setup/setup_script.py @@ -1,12 +1,10 @@ from multiprocessing.pool import ThreadPool from threading import Lock - from cloudshell.helpers.scripts import cloudshell_scripts_helpers as helpers from cloudshell.api.cloudshell_api import * from cloudshell.api.common_cloudshell_api import CloudShellAPIError from cloudshell.core.logger.qs_logger import get_qs_logger from remap_child_resources_constants import * - from sandbox_scripts.helpers.resource_helpers import * from sandbox_scripts.profiler.env_profiler import profileit @@ -27,7 +25,7 @@ def execute(self): resource_details_cache = {} api.WriteMessageToReservationOutput(reservationId=self.reservation_id, - message='Beginning sandbox setup') + message= 'Beginning sandbox setup') self._prepare_connectivity(api, self.reservation_id) @@ -67,7 +65,8 @@ def _prepare_connectivity(self, api, reservation_id): :param str reservation_id: """ self.logger.info("Preparing connectivity for reservation {0}".format(self.reservation_id)) - api.WriteMessageToReservationOutput(reservationId=self.reservation_id, message='Preparing connectivity') + api.WriteMessageToReservationOutput(reservationId=self.reservation_id, + message='Preparing connectivity') api.PrepareSandboxConnectivity(reservation_id) def _try_exeucte_autoload(self, api, deploy_result, resource_details_cache): @@ -81,7 +80,8 @@ def _try_exeucte_autoload(self, api, deploy_result, resource_details_cache): if deploy_result is None: self.logger.info("No apps to discover") - api.WriteMessageToReservationOutput(reservationId=self.reservation_id, message='No apps to discover') + api.WriteMessageToReservationOutput(reservationId=self.reservation_id, + message='No apps to discover') return message_written = False @@ -140,14 +140,14 @@ def _deploy_apps_in_reservation(self, api, reservation_details): if not apps or (len(apps) == 1 and not apps[0].Name): self.logger.info("No apps found in reservation {0}".format(self.reservation_id)) api.WriteMessageToReservationOutput(reservationId=self.reservation_id, - message='No apps to deploy') + message= 'No apps to deploy') return None app_names = map(lambda x: x.Name, apps) app_inputs = map(lambda x: DeployAppInput(x.Name, "Name", x.Name), apps) api.WriteMessageToReservationOutput(reservationId=self.reservation_id, - message='Apps deployment started') + message= 'Apps deployment started') self.logger.info( "Deploying apps for reservation {0}. App names: {1}".format(reservation_details, ", ".join(app_names))) @@ -248,8 +248,8 @@ def _configure_apps(self, api, reservation_id): failed_apps.append(conf_res.AppName) if not failed_apps: - api.WriteMessageToReservationOutput(reservationId=reservation_id, message= - 'Apps were configured successfully.') + api.WriteMessageToReservationOutput(reservationId=reservation_id, + message='Apps were configured successfully.') else: api.WriteMessageToReservationOutput(reservationId=reservation_id, message= 'Apps: {0} configuration failed. See logs for more details'.format( diff --git a/sandbox_scripts/environment/teardown/teardown_VM.py b/sandbox_scripts/environment/teardown/teardown_VM.py index 765b7e9..bd0bbe2 100644 --- a/sandbox_scripts/environment/teardown/teardown_VM.py +++ b/sandbox_scripts/environment/teardown/teardown_VM.py @@ -1,10 +1,9 @@ from cloudshell.core.logger import qs_logger from sandbox_scripts.helpers.Networking.save_restore_mgr import SaveRestoreManager from sandbox_scripts.helpers.Networking.NetworkingSaveNRestore import * -from cloudshell.helpers.scripts import cloudshell_scripts_helpers as helpers from sandbox_scripts.QualiEnvironmentUtils.Sandbox import SandboxBase -from cloudshell.api.common_cloudshell_api import CloudShellAPIError -from sandbox_scripts.QualiEnvironmentUtils.Resource import ResourceBase +from cloudshell.helpers.scripts import cloudshell_scripts_helpers as helpers +from sandbox_scripts.QualiEnvironmentUtils.QualiUtils import QualiError class EnvironmentTeardownVM: @@ -15,15 +14,20 @@ def __init__(self): self.logger = qs_logger.get_qs_logger(log_file_prefix="CloudShell Sandbox Teardown", log_group=self.reservation_id, log_category='Teardown') - self.sandbox = SandboxBase(self.reservation_id, self.logger) + self.sandbox = None - # --------------------------- - # --------------------------- def execute(self): + + self.sandbox = SandboxBase(self.reservation_id, self.logger) + self.sandbox.report_info("Beginning VMs cleanup") + + reservation_details = self.sandbox.api_session.GetReservationDetails(self.reservation_id) + saveNRestoreTool = SaveRestoreManager(self.sandbox) filename = "Snapshot_"+self.reservation_id+".txt" + is_snapshot = False # if the current reservation was saved as snapshot we look it by reservation_id @@ -36,19 +40,20 @@ def execute(self): is_snapshot = True if is_snapshot: - self.delete_VM_or_Power_off(to_delete=False) + self.delete_VM_or_Power_off(reservation_details, to_delete = False) + else: - self.delete_VM_or_Power_off(to_delete=True) + self.delete_VM_or_Power_off(reservation_details, to_delete =True) + + def delete_VM_or_Power_off(self, reservation_details, to_delete = False): - # --------------------------- - # --------------------------- - def delete_VM_or_Power_off(self, to_delete=False): """ - :param bool to_delete: + :param GetReservationDescriptionResponseInfo reservation_details: + :param str reservation_id: :return: """ # filter out resources not created in this reservation - resources = self.sandbox.get_root_vm_resources() + resources = reservation_details.ReservationDescription.Resources pool = ThreadPool() async_results = [] @@ -59,9 +64,12 @@ def delete_VM_or_Power_off(self, to_delete=False): } for resource in resources: - result_obj = pool.apply_async(self._power_off_or_delete_deployed_app, - (resource, lock, message_status,to_delete)) - async_results.append(result_obj) + resource_details = self.sandbox.api_session.GetResourceDetails(resource.Name) + vm_details = resource_details.VmDetails + if vm_details and hasattr(vm_details, "UID") and vm_details.UID: + result_obj = pool.apply_async(self._power_off_or_delete_deployed_app, + (resource_details, lock, message_status,to_delete)) + async_results.append(result_obj) pool.close() pool.join() @@ -76,7 +84,7 @@ def delete_VM_or_Power_off(self, to_delete=False): if resource_to_delete: try: self.sandbox.api_session.RemoveResourcesFromReservation(self.reservation_id, resource_to_delete) - except CloudShellAPIError as exc: + except QualiError as exc: if exc.code == EnvironmentTeardownVM.REMOVE_DEPLOYED_RESOURCE_ERROR: self.sandbox.report_error(error_message=exc.message, log_message="Error executing RemoveResourcesFromReservation command. " @@ -84,19 +92,20 @@ def delete_VM_or_Power_off(self, to_delete=False): raise_error=True, write_to_output_window=True) - # --------------------------- - # --------------------------- - def _power_off_or_delete_deployed_app(self, resource, lock, message_status, to_delete): + def _power_off_or_delete_deployed_app(self, resource_info, lock, message_status, to_delete): """ :param Lock lock: :param (dict of str: Boolean) message_status: - :param ResourceBase resource: + :param ResourceInfo resource_info: :return: """ - if resource.model.lower() =="vcenter static vm": + resource_name = resource_info.Name + + if resource_info.ResourceModelName.lower() =="vcenter static vm": to_delete = False try: + if to_delete: with lock: if not message_status['delete']: @@ -104,8 +113,10 @@ def _power_off_or_delete_deployed_app(self, resource, lock, message_status, to_d self.sandbox.report_info("Apps are being powered off and deleted...", write_to_output_window=True) - self.sandbox.report_info("Executing 'Delete' on deployed app {0}".format(resource.name)) - return resource.name + self.logger.info("Executing 'Delete' on deployed app {0} in reservation {1}" + .format(resource_name, self.reservation_id)) + + return resource_name else: with lock: @@ -115,15 +126,15 @@ def _power_off_or_delete_deployed_app(self, resource, lock, message_status, to_d write_to_output_window=True) with lock: - self.sandbox.report_info("Executing 'Power Off' on deployed app {0}" - .format(resource.name)) - resource.execute_connected_command(self.sandbox.id,"PowerOff", "power") + self.logger.info("Executing 'Power Off' on deployed app {0}" + .format(resource_name, self.reservation_id)) + self.sandbox.api_session.ExecuteResourceConnectedCommand(self.reservation_id, resource_name, + "PowerOff", "power") return None except Exception as exc: - err_msg = "Error deleting or powering off deployed app {0}. Error: {1}".format( - resource.name, str(exc)) - self.sandbox.report_error(err_msg,raise_error=False) + self.logger.error("Error deleting or powering off deployed app {0} in reservation {1}. Error: {2}" + .format(resource_name, self.reservation_id, str(exc))) return None diff --git a/sandbox_scripts/helpers/Networking/NetworkingSaveNRestore.py b/sandbox_scripts/helpers/Networking/NetworkingSaveNRestore.py index 45506bb..7bd0299 100644 --- a/sandbox_scripts/helpers/Networking/NetworkingSaveNRestore.py +++ b/sandbox_scripts/helpers/Networking/NetworkingSaveNRestore.py @@ -4,7 +4,6 @@ import tempfile from multiprocessing.pool import ThreadPool from threading import Lock - from sandbox_scripts.QualiEnvironmentUtils.ConfigFileManager import ConfigFileManager from sandbox_scripts.QualiEnvironmentUtils.ConfigPoolManager import ConfigPoolManager from sandbox_scripts.helpers.Networking.base_save_restore import * @@ -63,11 +62,16 @@ def load_config(self, config_stage, config_type, restore_method="Override", conf root_path = self.config_files_root + '/' + config_stage + '/' if config_set_name != '': root_path = root_path + config_set_name.strip() + '/' - + configsetpool = self.sandbox.get_config_set_pool_resource() + if configsetpool is not None and configsetpool.attribute_exist("Health Check Attempts"): + health_check_attempts = configsetpool.get_attribute("Health Check Attempts") + else: + health_check_attempts = 1 + self.sandbox.report_info("Health Check Attempts set to %s" % (health_check_attempts)) root_path = root_path.replace(' ', '_') self.sandbox.report_info("RootPath: " + root_path, write_to_output_window=True) images_path_dict = self._get_images_path_dict(root_path) - self.sandbox.report_info("\nLoading image and configuration on the devices. This action may take some time", + self.sandbox.report_info("Loading image and configuration on the devices. This action may take some time.", write_to_output_window=True) root_resources = self.sandbox.get_root_networking_resources() """:type : list[ResourceBase]""" @@ -75,7 +79,8 @@ def load_config(self, config_stage, config_type, restore_method="Override", conf pool = ThreadPool(len(root_resources)) lock = Lock() async_results = [pool.apply_async(self._run_asynch_load, - (resource, images_path_dict, root_path, ignore_models, config_stage, lock, + (resource, images_path_dict, root_path, ignore_models, config_stage, + health_check_attempts, lock, use_Config_file_path_attr)) for resource in root_resources] @@ -114,7 +119,8 @@ def _remove_temp_config_files(self): # ---------------------------------- # ---------------------------------- - def _run_asynch_load(self, resource, images_path_dict, root_path, ignore_models, config_stage, lock, + def _run_asynch_load(self, resource, images_path_dict, root_path, ignore_models, config_stage, + health_check_attempts, lock, use_Config_file_path_attr): message = "" # run_status = True @@ -126,14 +132,14 @@ def _run_asynch_load(self, resource, images_path_dict, root_path, ignore_models, if load_config_to_device: self.sandbox.report_info(resource.name + " starting health check", write_to_output_window=True) - health_check_result = resource.health_check(self.sandbox.id) + health_check_result = resource.health_check(self.sandbox.id, health_check_attempts) if health_check_result == "": self.sandbox.report_info(resource.name + " -- Initial Health Check Passed.") try: config_path = '' with lock: config_path = self._get_concrete_config_file_path(root_path, resource, config_stage, - write_to_output=True) + write_to_output=False) if use_Config_file_path_attr: resource.set_attribute_value('Config file path', config_path) # TODO - Snapshots currently only restore configuration. We need to restore firmware as well @@ -149,8 +155,8 @@ def _run_asynch_load(self, resource, images_path_dict, root_path, ignore_models, if len(images_path_dict) > 0: # check what the device FW version is currently. version = resource.get_version(self.sandbox.id) - self.sandbox.report_info(resource.name + ": current version: " + version, - write_to_output_window=False) + self.sandbox.report_info(resource.name + " current version: " + version, + write_to_output_window=True) # First try with an firmware image key of concrete resource name!! dict_img_version = '' image_key = '' @@ -172,14 +178,13 @@ def _run_asynch_load(self, resource, images_path_dict, root_path, ignore_models, message += "\n" + resource.name + ": NO firmware version specified in Base FirmwareData.csv" # same image version - Only load config (running override) - message += "\n" + resource.name + ": loading configuration from: " + config_path + message += resource.name + ": loading configuration from " + config_path if dict_img_version.lower() == version.lower(): resource.load_network_config(self.sandbox.id, config_path=config_path, config_type='Running', restore_method='Override') # Different image - Load config to the RUNNING ALSO and load the image else: - message += "\n" + resource.name + ": loading configuration from:" + config_path resource.load_network_config(self.sandbox.id, config_path, config_type='Running', restore_method='Override') @@ -198,7 +203,7 @@ def _run_asynch_load(self, resource, images_path_dict, root_path, ignore_models, message += "\n" + resource.name + ": loading config from:" + config_path resource.load_network_config(self.sandbox.id, config_path, 'Running', 'Override') - health_check_result = resource.health_check(self.sandbox.id) + health_check_result = resource.health_check(self.sandbox.id, health_check_attempts=1) if health_check_result != '': raise QualiError(self.sandbox.id, resource.name + " did not pass health check after loading configuration") @@ -216,7 +221,9 @@ def _run_asynch_load(self, resource, images_path_dict, root_path, ignore_models, ". Unexpected error: " + str(ex) message += err else: - self.sandbox.report_error(resource.name + " health check failed.", write_to_output_window=True) + self.sandbox.report_error(resource.name + " health check failed.", + write_to_output_window=True, + send_email=True) load_result.run_result = False err = resource.name + " did not pass health check. Configuration will not be " \ "loaded to the device.\nHealth check error is: " + health_check_result @@ -367,7 +374,7 @@ def save_config(self, snapshot_name, config_type, ignore_models=None, write_to_o if not res.run_result: err = "Failed to save configuration on device " + res.resource_name self.sandbox.report_error(err, write_to_output_window=write_to_output, raise_error=False) - self.sandbox.report_error(res.message, raise_error=False) + self.sandbox.report_error(res.message, raise_error=False, send_email=True) elif res.message != '': self.sandbox.report_info(res.resource_name + "\n" + res.message) diff --git a/sandbox_scripts/helpers/Networking/vm_save_restore.py b/sandbox_scripts/helpers/Networking/vm_save_restore.py index 397e725..3f7bac2 100644 --- a/sandbox_scripts/helpers/Networking/vm_save_restore.py +++ b/sandbox_scripts/helpers/Networking/vm_save_restore.py @@ -38,7 +38,7 @@ def load_config(self, config_stage, config_set_name='', ignore_models=None, root_path = root_path.replace(' ', '_') self.sandbox.report_info( - "Loading image on the devices. This action may take some time",write_to_output_window=True) + "Loading image on the VMs. This action may take some time.",write_to_output_window=True) root_resources = self.sandbox.get_root_vm_resources() """:type : list[ResourceBase]""" if len(root_resources) > 0: