diff --git a/.github/workflows/ci_pr.yml b/.github/workflows/ci_pr.yml index 84b3ab68e..bf454f45b 100644 --- a/.github/workflows/ci_pr.yml +++ b/.github/workflows/ci_pr.yml @@ -29,7 +29,8 @@ jobs: env: NOSEOPTS: "--verbose" - + ACTIONS_ALLOW_USE_UNSECURE_NODE_VERSION: true + steps: - uses: actions/checkout@v3 diff --git a/azurelinuxagent/common/version.py b/azurelinuxagent/common/version.py index c1715ff76..d9c088b4a 100644 --- a/azurelinuxagent/common/version.py +++ b/azurelinuxagent/common/version.py @@ -209,7 +209,7 @@ def has_logrotate(): # # When doing a release, be sure to use the actual agent version. Current agent version: 2.4.0.0 # -AGENT_VERSION = '2.11.1.4' +AGENT_VERSION = '2.11.1.12' AGENT_LONG_VERSION = "{0}-{1}".format(AGENT_NAME, AGENT_VERSION) AGENT_DESCRIPTION = """ The Azure Linux Agent supports the provisioning and running of Linux diff --git a/azurelinuxagent/ga/exthandlers.py b/azurelinuxagent/ga/exthandlers.py index fcb14d22b..fe43cfcfd 100644 --- a/azurelinuxagent/ga/exthandlers.py +++ b/azurelinuxagent/ga/exthandlers.py @@ -1417,9 +1417,17 @@ def disable(self, extension=None, ignore_error=False): self.report_event(name=self.get_extension_full_name(extension), message=msg, is_success=False, log_event=False) - # Clean extension state For Multi Config extensions on Disable + # + # In the case of multi-config handlers, we keep the state of each extension individually. + # Disable can be called when the extension is deleted (the extension state in the goal state is set to "disabled"), + # or as part of the Uninstall and Update sequences. When the extension is deleted, we need to remove its state, along + # with its status and settings files. Otherwise, we need to set the state to "disabled". + # if self.should_perform_multi_config_op(extension): - self.__remove_extension_state_files(extension) + if extension.state == ExtensionRequestedState.Disabled: + self.__remove_extension_state_files(extension) + else: + self.__set_extension_state(extension, ExtensionState.Disabled) # For Single config, dont check enabled_extensions because no extension state is maintained. # For MultiConfig, Set the handler state to Installed only when all extensions have been disabled diff --git a/tests_e2e/orchestrator/docker/Dockerfile b/tests_e2e/orchestrator/docker/Dockerfile index 597e57418..f71d6c02e 100644 --- a/tests_e2e/orchestrator/docker/Dockerfile +++ b/tests_e2e/orchestrator/docker/Dockerfile @@ -67,7 +67,7 @@ RUN \ cd $HOME && \ git clone https://github.com/microsoft/lisa.git && \ cd lisa && \ - git checkout 2c16e32001fdefb9572dff61241451b648259dbf && \ + git checkout 0e37ed07304b74362cfb3d3c55ac932d3bdc660c && \ \ python3 -m pip install --upgrade pip && \ python3 -m pip install --editable .[azure,libvirt] --config-settings editable_mode=compat && \ diff --git a/tests_e2e/orchestrator/scripts/install-agent b/tests_e2e/orchestrator/scripts/install-agent index 61181b44d..d28164f6d 100755 --- a/tests_e2e/orchestrator/scripts/install-agent +++ b/tests_e2e/orchestrator/scripts/install-agent @@ -140,6 +140,25 @@ if [[ $(uname -a) == *"flatcar"* ]]; then if [[ ! -f /usr/share/oem/waagent.conf ]]; then ln -s "$waagent_conf_path" /usr/share/oem/waagent.conf fi + + # New flatcar images set the uphold property for agent service that is causing automatic restart on stop cmd + # [Upholds= dependency on it has a continuous effect, constantly restarting the unit if necessary] + # Resetting the uphold property as workaround for now + uphold_target=$(systemctl show waagent --property=UpheldBy) + # example output: UpheldBy=multi-user.target + if [[ $uphold_target == *".target"* ]]; then + target_name="${uphold_target#*=}" + if [[ ! -d /etc/systemd/system/$target_name.d ]]; then + mkdir -p /etc/systemd/system/$target_name.d + fi + echo -e "[Unit]\nUpholds=" > /etc/systemd/system/$target_name.d/10-waagent-sysext.conf + systemctl daemon-reload + fi + # Flatcar images does automatic reboot without user input, so turning it off + # Broadcast message from locksmithd at 2024-02-23 19:48:55.478412272 +0000 UTC m= + # System reboot in 5 minutes! + echo "REBOOT_STRATEGY=off" > /etc/flatcar/update.conf + systemctl restart locksmithd fi # diff --git a/tests_e2e/pipeline/scripts/execute_tests.sh b/tests_e2e/pipeline/scripts/execute_tests.sh index d3e862b4a..bcba9710a 100755 --- a/tests_e2e/pipeline/scripts/execute_tests.sh +++ b/tests_e2e/pipeline/scripts/execute_tests.sh @@ -71,6 +71,8 @@ IP_ADDRESS=$(curl -4 ifconfig.io/ip) # certificate location in the container AZURE_CLIENT_CERTIFICATE_PATH="/home/waagent/app/cert.pem" +# Need to set this to True if we sue SNI based authentication for certificate +AZURE_CLIENT_SEND_CERTIFICATE_CHAIN="True" docker run --rm \ --volume "$BUILD_SOURCESDIRECTORY:/home/waagent/WALinuxAgent" \ @@ -80,6 +82,7 @@ docker run --rm \ --env AZURE_CLIENT_ID \ --env AZURE_TENANT_ID \ --env AZURE_CLIENT_CERTIFICATE_PATH=$AZURE_CLIENT_CERTIFICATE_PATH \ + --env AZURE_CLIENT_SEND_CERTIFICATE_CHAIN=$AZURE_CLIENT_SEND_CERTIFICATE_CHAIN \ waagenttests.azurecr.io/waagenttests \ bash --login -c \ "lisa \ diff --git a/tests_e2e/test_suites/agent_update.yml b/tests_e2e/test_suites/agent_update.yml index 3d3d4918f..e53f2f921 100644 --- a/tests_e2e/test_suites/agent_update.yml +++ b/tests_e2e/test_suites/agent_update.yml @@ -12,4 +12,9 @@ locations: "AzureCloud:eastus2euap" owns_vm: true skip_on_clouds: - "AzureChinaCloud" - - "AzureUSGovernment" \ No newline at end of file + - "AzureUSGovernment" +# Since Flatcar read-only filesystem, we can't edit the version file. This test relies on the version to be updated in version file. +# TODO: Enable once we find workaround for this +skip_on_images: + - "flatcar" + - "flatcar_arm64" \ No newline at end of file diff --git a/tests_e2e/test_suites/agent_wait_for_cloud_init.yml b/tests_e2e/test_suites/agent_wait_for_cloud_init.yml index 727803811..09c00aa7e 100644 --- a/tests_e2e/test_suites/agent_wait_for_cloud_init.yml +++ b/tests_e2e/test_suites/agent_wait_for_cloud_init.yml @@ -2,7 +2,7 @@ # This test verifies that the Agent waits for cloud-init to complete before it starts processing extensions. # # NOTE: This test is not fully automated. It requires a custom image where the test Agent has been installed and Extensions.WaitForCloudInit is enabled in waagent.conf. -# To execute it manually, create a custom image and use the 'image' runbook parameter, for example: "-v: image:gallery/wait-cloud-init/1.0.1". +# To execute it manually, create a custom image and use the 'image' runbook parameter, for example: "-v: image:gallery/wait-cloud-init/1.0.2". # name: "AgentWaitForCloudInit" tests: diff --git a/tests_e2e/tests/agent_publish/agent_publish.py b/tests_e2e/tests/agent_publish/agent_publish.py index 0cf51c331..33ba65db7 100644 --- a/tests_e2e/tests/agent_publish/agent_publish.py +++ b/tests_e2e/tests/agent_publish/agent_publish.py @@ -62,7 +62,7 @@ def _get_agent_info(self) -> None: def _prepare_agent(self) -> None: log.info("Modifying agent update related config flags and renaming the log file") - self._run_remote_test(self._ssh_client, "sh -c 'agent-service stop && mv /var/log/waagent.log /var/log/waagent.$(date --iso-8601=seconds).log && update-waagent-conf AutoUpdate.UpdateToLatestVersion=y AutoUpdate.GAFamily=Test AutoUpdate.Enabled=y Extensions.Enabled=y'", use_sudo=True) + self._run_remote_test(self._ssh_client, "sh -c 'agent-service stop && mv /var/log/waagent.log /var/log/waagent.$(date --iso-8601=seconds).log && update-waagent-conf AutoUpdate.UpdateToLatestVersion=y AutoUpdate.GAFamily=Test AutoUpdate.Enabled=y Extensions.Enabled=y Debug.EnableGAVersioning=n'", use_sudo=True) log.info('Renamed log file and updated agent-update DownloadNewAgents GAFamily config flags') def _check_update(self) -> None: diff --git a/tests_e2e/tests/agent_wait_for_cloud_init/add_cloud_init_script.py b/tests_e2e/tests/agent_wait_for_cloud_init/add_cloud_init_script.py index 1fbc60adc..0c1a6611b 100755 --- a/tests_e2e/tests/agent_wait_for_cloud_init/add_cloud_init_script.py +++ b/tests_e2e/tests/agent_wait_for_cloud_init/add_cloud_init_script.py @@ -35,10 +35,10 @@ def update(self, template: Dict[str, Any], is_lisa_template: bool) -> None: # # cloud-init configuration needs to be added in the osProfile.customData property as a base64-encoded string. # - # LISA uses the getOSProfile function to generate the value for osProfile; add customData to its output, checking that we do not + # LISA uses the generateOsProfile function to generate the value for osProfile; add customData to its output, checking that we do not # override any existing value (the current LISA template does not have any). # - # "getOSProfile": { + # "generateOsProfile": { # "parameters": [ # ... # ], @@ -55,7 +55,7 @@ def update(self, template: Dict[str, Any], is_lisa_template: bool) -> None: # encoded_script = base64.b64encode(AgentWaitForCloudInit.CloudInitScript.encode('utf-8')).decode('utf-8') - get_os_profile = self.get_lisa_function(template, 'getOSProfile') + get_os_profile = self.get_lisa_function(template, 'generateOsProfile') output = self.get_function_output(get_os_profile) if output.get('customData') is not None: raise Exception(f"The getOSProfile function already has a 'customData'. Won't override it. Definition: {get_os_profile}") diff --git a/tests_e2e/tests/ext_sequencing/ext_sequencing.py b/tests_e2e/tests/ext_sequencing/ext_sequencing.py index e50b0d6ab..69c3a7291 100644 --- a/tests_e2e/tests/ext_sequencing/ext_sequencing.py +++ b/tests_e2e/tests/ext_sequencing/ext_sequencing.py @@ -226,7 +226,7 @@ def run(self): # fail. We know an extension should fail if "failing" is in the case name. Otherwise, report the # failure. deployment_failure_pattern = r"[\s\S]*\"details\": [\s\S]* \"code\": \"(?P.*)\"[\s\S]* \"message\": \"(?P.*)\"[\s\S]*" - msg_pattern = r"Multiple VM extensions failed to be provisioned on the VM. Please see the VM extension instance view for other failures. The first extension failed due to the error: VM Extension '.*' is marked as failed since it depends upon the VM Extension 'CustomScript' which has failed." + msg_pattern = r"Multiple VM extensions failed to be provisioned on the VM.*VM Extension '.*' is marked as failed since it depends upon the VM Extension 'CustomScript' which has failed." deployment_failure_match = re.match(deployment_failure_pattern, str(e)) if "failing" not in case.__name__: fail("Extension template deployment unexpectedly failed: {0}".format(e)) diff --git a/tests_e2e/tests/lib/network_security_rule.py b/tests_e2e/tests/lib/network_security_rule.py index 8df51b204..6fe8ee296 100644 --- a/tests_e2e/tests/lib/network_security_rule.py +++ b/tests_e2e/tests/lib/network_security_rule.py @@ -17,7 +17,7 @@ import json -from typing import Any, Dict, List +from typing import Any, Dict from tests_e2e.tests.lib.update_arm_template import UpdateArmTemplate @@ -55,7 +55,7 @@ def add_security_rule(self, security_rule: Dict[str, Any]) -> None: self._get_network_security_group()["properties"]["securityRules"].append(security_rule) def _get_network_security_group(self) -> Dict[str, Any]: - resources: List[Dict[str, Any]] = self._template["resources"] + resources: Any = self._template["resources"] # # If the NSG already exists, just return it # @@ -76,14 +76,20 @@ def _get_network_security_group(self) -> Dict[str, Any]: "securityRules": [] }} }}""") - resources.append(network_security_group) + + # resources is a dictionary in LISA's ARM template, but a list in the template for scale sets + if isinstance(resources, dict): + nsg_reference = "network_security_groups" + resources[nsg_reference] = network_security_group + else: + nsg_reference = f"[resourceId('Microsoft.Network/networkSecurityGroups', '{self._NETWORK_SECURITY_GROUP}')]" + resources.append(network_security_group) # # Add a dependency on the NSG to the virtual network # network_resource = UpdateArmTemplate.get_resource(resources, "Microsoft.Network/virtualNetworks") network_resource_dependencies = network_resource.get("dependsOn") - nsg_reference = f"[resourceId('Microsoft.Network/networkSecurityGroups', '{self._NETWORK_SECURITY_GROUP}')]" if network_resource_dependencies is None: network_resource["dependsOn"] = [nsg_reference] else: diff --git a/tests_e2e/tests/lib/update_arm_template.py b/tests_e2e/tests/lib/update_arm_template.py index 010178ab9..ef3dfd1d9 100644 --- a/tests_e2e/tests/lib/update_arm_template.py +++ b/tests_e2e/tests/lib/update_arm_template.py @@ -16,7 +16,7 @@ # from abc import ABC, abstractmethod -from typing import Any, Dict, List +from typing import Any, Dict class UpdateArmTemplate(ABC): @@ -32,24 +32,28 @@ def update(self, template: Dict[str, Any], is_lisa_template: bool) -> None: """ @staticmethod - def get_resource(resources: List[Dict[str, Any]], type_name: str) -> Any: + def get_resource(resources: Any, type_name: str) -> Any: """ - Returns the first resource of the specified type in the given 'resources' list. + Returns the first resource of the specified type in the given 'resources' list/dict. Raises KeyError if no resource of the specified type is found. """ + if isinstance(resources, dict): + resources = resources.values() for item in resources: if item["type"] == type_name: return item raise KeyError(f"Cannot find a resource of type {type_name} in the ARM template") @staticmethod - def get_resource_by_name(resources: List[Dict[str, Any]], resource_name: str, type_name: str) -> Any: + def get_resource_by_name(resources: Any, resource_name: str, type_name: str) -> Any: """ - Returns the first resource of the specified type and name in the given 'resources' list. + Returns the first resource of the specified type and name in the given 'resources' list/dict. Raises KeyError if no resource of the specified type and name is found. """ + if isinstance(resources, dict): + resources = resources.values() for item in resources: if item["type"] == type_name and item["name"] == resource_name: return item @@ -58,7 +62,8 @@ def get_resource_by_name(resources: List[Dict[str, Any]], resource_name: str, ty @staticmethod def get_lisa_function(template: Dict[str, Any], function_name: str) -> Dict[str, Any]: """ - Looks for the given function name in the LISA namespace and returns its definition. Raises KeyError if the function is not found. + Looks for the given function name in the bicep namespace and returns its definition. Raises KeyError if the function is not found. + Note: LISA leverages the bicep language to define the ARM templates.Now namespace is changed to __bicep instead lisa """ # # NOTE: LISA's functions are in the "lisa" namespace, for example: @@ -96,7 +101,7 @@ def get_lisa_function(template: Dict[str, Any], function_name: str) -> Dict[str, name = namespace.get("namespace") if name is None: raise Exception(f'Cannot find "namespace" in the LISA template: {namespace}') - if name == "lisa": + if name == "__bicep": lisa_functions = namespace.get('members') if lisa_functions is None: raise Exception(f'Cannot find the members of the lisa namespace in the LISA template: {namespace}') diff --git a/tests_e2e/tests/lib/virtual_machine_runcommand_client.py b/tests_e2e/tests/lib/virtual_machine_runcommand_client.py new file mode 100644 index 000000000..7858c6fc9 --- /dev/null +++ b/tests_e2e/tests/lib/virtual_machine_runcommand_client.py @@ -0,0 +1,130 @@ +# Microsoft Azure Linux Agent +# +# Copyright 2018 Microsoft Corporation +# +# 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. +# + +# +# This module includes facilities to execute VM extension runcommand operations (enable, remove, etc). +# +import json +from typing import Any, Dict, Callable +from assertpy import soft_assertions, assert_that + +from azure.mgmt.compute import ComputeManagementClient +from azure.mgmt.compute.models import VirtualMachineRunCommand, VirtualMachineRunCommandScriptSource, VirtualMachineRunCommandInstanceView + +from tests_e2e.tests.lib.azure_sdk_client import AzureSdkClient +from tests_e2e.tests.lib.logging import log +from tests_e2e.tests.lib.retry import execute_with_retry +from tests_e2e.tests.lib.virtual_machine_client import VirtualMachineClient +from tests_e2e.tests.lib.vm_extension_identifier import VmExtensionIdentifier + + +class VirtualMachineRunCommandClient(AzureSdkClient): + """ + Client for operations virtual machine RunCommand extensions. + """ + def __init__(self, vm: VirtualMachineClient, extension: VmExtensionIdentifier, resource_name: str = None): + super().__init__() + self._vm: VirtualMachineClient = vm + self._identifier = extension + self._resource_name = resource_name or extension.type + self._compute_client: ComputeManagementClient = AzureSdkClient.create_client(ComputeManagementClient, self._vm.cloud, self._vm.subscription) + + def get_instance_view(self) -> VirtualMachineRunCommandInstanceView: + """ + Retrieves the instance view of the run command extension + """ + log.info("Retrieving instance view for %s...", self._identifier) + + return execute_with_retry(lambda: self._compute_client.virtual_machine_run_commands.get_by_virtual_machine( + resource_group_name=self._vm.resource_group, + vm_name=self._vm.name, + run_command_name=self._resource_name, + expand="instanceView" + ).instance_view) + + def enable( + self, + settings: Dict[str, Any] = None, + timeout: int = AzureSdkClient._DEFAULT_TIMEOUT + ) -> None: + """ + Performs an enable operation on the run command extension. + """ + run_command_parameters = VirtualMachineRunCommand( + location=self._vm.location, + source=VirtualMachineRunCommandScriptSource( + script=settings.get("source") if settings is not None else settings + ) + ) + + log.info("Enabling %s", self._identifier) + log.info("%s", run_command_parameters) + + result: VirtualMachineRunCommand = self._execute_async_operation( + lambda: self._compute_client.virtual_machine_run_commands.begin_create_or_update( + self._vm.resource_group, + self._vm.name, + self._resource_name, + run_command_parameters), + operation_name=f"Enable {self._identifier}", + timeout=timeout) + + log.info("Provisioning state: %s", result.provisioning_state) + + def delete(self, timeout: int = AzureSdkClient._DEFAULT_TIMEOUT) -> None: + """ + Performs a delete operation on the run command extension + """ + self._execute_async_operation( + lambda: self._compute_client.virtual_machine_run_commands.begin_delete( + self._vm.resource_group, + self._vm.name, + self._resource_name), + operation_name=f"Delete {self._identifier}", + timeout=timeout) + + def assert_instance_view( + self, + expected_status_code: str = "Succeeded", + expected_exit_code: int = 0, + expected_message: str = None, + assert_function: Callable[[VirtualMachineRunCommandInstanceView], None] = None + ) -> None: + """ + Asserts that the run command's instance view matches the given expected values. If 'expected_message' is + omitted, it is not validated. + + If 'assert_function' is provided, it is invoked passing as parameter the instance view. This function can be used to perform + additional validations. + """ + instance_view = self.get_instance_view() + log.info("Instance view:\n%s", json.dumps(instance_view.serialize(), indent=4)) + + with soft_assertions(): + if expected_message is not None: + assert_that(expected_message in instance_view.output).described_as(f"{expected_message} should be in the InstanceView message ({instance_view.output})").is_true() + + assert_that(instance_view.execution_state).described_as("InstanceView execution state").is_equal_to(expected_status_code) + assert_that(instance_view.exit_code).described_as("InstanceView exit code").is_equal_to(expected_exit_code) + + if assert_function is not None: + assert_function(instance_view) + + log.info("The instance view matches the expected values") + + def __str__(self): + return f"{self._identifier}" diff --git a/tests_e2e/tests/multi_config_ext/multi_config_ext.py b/tests_e2e/tests/multi_config_ext/multi_config_ext.py index 4df75fd2b..d9315dea5 100644 --- a/tests_e2e/tests/multi_config_ext/multi_config_ext.py +++ b/tests_e2e/tests/multi_config_ext/multi_config_ext.py @@ -28,6 +28,8 @@ from azure.mgmt.compute.models import VirtualMachineInstanceView from tests_e2e.tests.lib.agent_test import AgentVmTest +from tests_e2e.tests.lib.azure_sdk_client import AzureSdkClient +from tests_e2e.tests.lib.virtual_machine_runcommand_client import VirtualMachineRunCommandClient from tests_e2e.tests.lib.vm_extension_identifier import VmExtensionIds from tests_e2e.tests.lib.logging import log from tests_e2e.tests.lib.virtual_machine_client import VirtualMachineClient @@ -36,7 +38,7 @@ class MultiConfigExt(AgentVmTest): class TestCase: - def __init__(self, extension: VirtualMachineExtensionClient, get_settings: Callable[[str], Dict[str, str]]): + def __init__(self, extension: AzureSdkClient, get_settings: Callable[[str], Dict[str, str]]): self.extension = extension self.get_settings = get_settings self.test_guid: str = str(uuid.uuid4()) @@ -89,19 +91,18 @@ def run(self): # Create 3 different RCv2 extensions and a single config extension (CSE) and assign each a unique guid. Each # extension will have settings that echo its assigned guid. We will use this guid to verify the extension # statuses later. - mc_settings: Callable[[Any], Dict[str, Dict[str, str]]] = lambda s: { - "source": {"script": f"echo {s}"}} + mc_settings: Callable[[Any], Dict[str, str]] = lambda s: {"source": f"echo {s}"} sc_settings: Callable[[Any], Dict[str, str]] = lambda s: {'commandToExecute': f"echo {s}"} test_cases: Dict[str, MultiConfigExt.TestCase] = { "MCExt1": MultiConfigExt.TestCase( - VirtualMachineExtensionClient(self._context.vm, VmExtensionIds.RunCommandHandler, + VirtualMachineRunCommandClient(self._context.vm, VmExtensionIds.RunCommandHandler, resource_name="MCExt1"), mc_settings), "MCExt2": MultiConfigExt.TestCase( - VirtualMachineExtensionClient(self._context.vm, VmExtensionIds.RunCommandHandler, + VirtualMachineRunCommandClient(self._context.vm, VmExtensionIds.RunCommandHandler, resource_name="MCExt2"), mc_settings), "MCExt3": MultiConfigExt.TestCase( - VirtualMachineExtensionClient(self._context.vm, VmExtensionIds.RunCommandHandler, + VirtualMachineRunCommandClient(self._context.vm, VmExtensionIds.RunCommandHandler, resource_name="MCExt3"), mc_settings), "CSE": MultiConfigExt.TestCase( VirtualMachineExtensionClient(self._context.vm, VmExtensionIds.CustomScript), sc_settings) @@ -116,10 +117,10 @@ def run(self): # Update MCExt3 and CSE with new guids and add a new instance of RCv2 to the VM updated_test_cases: Dict[str, MultiConfigExt.TestCase] = { "MCExt3": MultiConfigExt.TestCase( - VirtualMachineExtensionClient(self._context.vm, VmExtensionIds.RunCommandHandler, + VirtualMachineRunCommandClient(self._context.vm, VmExtensionIds.RunCommandHandler, resource_name="MCExt3"), mc_settings), "MCExt4": MultiConfigExt.TestCase( - VirtualMachineExtensionClient(self._context.vm, VmExtensionIds.RunCommandHandler, + VirtualMachineRunCommandClient(self._context.vm, VmExtensionIds.RunCommandHandler, resource_name="MCExt4"), mc_settings), "CSE": MultiConfigExt.TestCase( VirtualMachineExtensionClient(self._context.vm, VmExtensionIds.CustomScript), sc_settings) @@ -138,10 +139,10 @@ def run(self): log.info("Add only multi-config extensions to the VM...") mc_test_cases: Dict[str, MultiConfigExt.TestCase] = { "MCExt5": MultiConfigExt.TestCase( - VirtualMachineExtensionClient(self._context.vm, VmExtensionIds.RunCommandHandler, + VirtualMachineRunCommandClient(self._context.vm, VmExtensionIds.RunCommandHandler, resource_name="MCExt5"), mc_settings), "MCExt6": MultiConfigExt.TestCase( - VirtualMachineExtensionClient(self._context.vm, VmExtensionIds.RunCommandHandler, + VirtualMachineRunCommandClient(self._context.vm, VmExtensionIds.RunCommandHandler, resource_name="MCExt6"), mc_settings) } self.enable_and_assert_test_cases(cases_to_enable=mc_test_cases, cases_to_assert=mc_test_cases,