Skip to content

Commit

Permalink
Merge pull request Azure#3236 from Azure/hotfix-2.11.1.9
Browse files Browse the repository at this point in the history
Merge Hotfix 2.11.1.9 (2.11.1.12) to master
  • Loading branch information
maddieford authored Sep 27, 2024
2 parents 2b21de5 + 86b0d53 commit acd2f73
Show file tree
Hide file tree
Showing 15 changed files with 211 additions and 33 deletions.
3 changes: 2 additions & 1 deletion .github/workflows/ci_pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ jobs:

env:
NOSEOPTS: "--verbose"

ACTIONS_ALLOW_USE_UNSECURE_NODE_VERSION: true

steps:
- uses: actions/checkout@v3

Expand Down
2 changes: 1 addition & 1 deletion azurelinuxagent/common/version.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
12 changes: 10 additions & 2 deletions azurelinuxagent/ga/exthandlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion tests_e2e/orchestrator/docker/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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 && \
Expand Down
19 changes: 19 additions & 0 deletions tests_e2e/orchestrator/scripts/install-agent
Original file line number Diff line number Diff line change
Expand Up @@ -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

#
Expand Down
3 changes: 3 additions & 0 deletions tests_e2e/pipeline/scripts/execute_tests.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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" \
Expand All @@ -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 \
Expand Down
7 changes: 6 additions & 1 deletion tests_e2e/test_suites/agent_update.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,9 @@ locations: "AzureCloud:eastus2euap"
owns_vm: true
skip_on_clouds:
- "AzureChinaCloud"
- "AzureUSGovernment"
- "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"
2 changes: 1 addition & 1 deletion tests_e2e/test_suites/agent_wait_for_cloud_init.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion tests_e2e/tests/agent_publish/agent_publish.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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": [
# ...
# ],
Expand All @@ -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}")
Expand Down
2 changes: 1 addition & 1 deletion tests_e2e/tests/ext_sequencing/ext_sequencing.py
Original file line number Diff line number Diff line change
Expand Up @@ -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<code>.*)\"[\s\S]* \"message\": \"(?P<msg>.*)\"[\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))
Expand Down
14 changes: 10 additions & 4 deletions tests_e2e/tests/lib/network_security_rule.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
#
Expand All @@ -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:
Expand Down
19 changes: 12 additions & 7 deletions tests_e2e/tests/lib/update_arm_template.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
#

from abc import ABC, abstractmethod
from typing import Any, Dict, List
from typing import Any, Dict


class UpdateArmTemplate(ABC):
Expand All @@ -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
Expand All @@ -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:
Expand Down Expand Up @@ -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}')
Expand Down
130 changes: 130 additions & 0 deletions tests_e2e/tests/lib/virtual_machine_runcommand_client.py
Original file line number Diff line number Diff line change
@@ -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}"
Loading

0 comments on commit acd2f73

Please sign in to comment.