-
Notifications
You must be signed in to change notification settings - Fork 372
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
7 changed files
with
417 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
# | ||
# This scenario validates the agent status is updated without any goal state changes | ||
# | ||
name: "AgentStatus" | ||
tests: | ||
- "agent_status/agent_status.py" | ||
images: | ||
- "endorsed" | ||
- "endorsed-arm64" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
# | ||
# Multi-config extensions are no longer supported but there are still customers running RCv2 and we don't want to break | ||
# them. This test suite is used to verify that the agent processes RCv2 (a multi-config extension) as expected. | ||
# | ||
name: "MultiConfigExt" | ||
tests: | ||
- "multi_config_ext/multi_config_ext.py" | ||
images: | ||
- "endorsed" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,195 @@ | ||
#!/usr/bin/env python3 | ||
|
||
# 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. | ||
# | ||
|
||
# | ||
# Validates the agent status is updated without processing additional goal states (aside from the first goal state | ||
# from fabric) | ||
# | ||
|
||
from azure.mgmt.compute.models import VirtualMachineInstanceView, InstanceViewStatus, VirtualMachineAgentInstanceView | ||
from assertpy import assert_that | ||
from datetime import datetime, timedelta | ||
from time import sleep | ||
import json | ||
|
||
from tests_e2e.tests.lib.agent_test import AgentTest | ||
from tests_e2e.tests.lib.agent_test_context import AgentTestContext | ||
from tests_e2e.tests.lib.logging import log | ||
from tests_e2e.tests.lib.virtual_machine_client import VirtualMachineClient | ||
|
||
|
||
class RetryableAgentStatusException(BaseException): | ||
pass | ||
|
||
|
||
class AgentStatus(AgentTest): | ||
def __init__(self, context: AgentTestContext): | ||
super().__init__(context) | ||
self._ssh_client = self._context.create_ssh_client() | ||
|
||
def validate_instance_view_vmagent_status(self, instance_view: VirtualMachineInstanceView): | ||
status: InstanceViewStatus = instance_view.vm_agent.statuses[0] | ||
|
||
# Validate message field | ||
if status.message is None: | ||
raise RetryableAgentStatusException("Agent status is invalid: 'message' property in instance view is None") | ||
elif 'unresponsive' in status.message: | ||
raise RetryableAgentStatusException("Agent status is invalid: Instance view shows unresponsive agent") | ||
|
||
# Validate display status field | ||
if status.display_status is None: | ||
raise RetryableAgentStatusException("Agent status is invalid: 'display_status' property in instance view is None") | ||
elif 'Not Ready' in status.display_status: | ||
raise RetryableAgentStatusException("Agent status is invalid: Instance view shows agent status is not ready") | ||
|
||
# Validate time field | ||
if status.time is None: | ||
raise RetryableAgentStatusException("Agent status is invalid: 'time' property in instance view is None") | ||
|
||
def validate_instance_view_vmagent(self, instance_view: VirtualMachineInstanceView): | ||
""" | ||
Checks that instance view has vm_agent.statuses and vm_agent.vm_agent_version properties which report the Guest | ||
Agent as running and Ready: | ||
"vm_agent": { | ||
"extension_handlers": [], | ||
"vm_agent_version": "9.9.9.9", | ||
"statuses": [ | ||
{ | ||
"level": "Info", | ||
"time": "2023-08-11T09:13:01.000Z", | ||
"message": "Guest Agent is running", | ||
"code": "ProvisioningState/succeeded", | ||
"display_status": "Ready" | ||
} | ||
] | ||
} | ||
""" | ||
# Using dot operator for properties here because azure.mgmt.compute.models has classes for InstanceViewStatus | ||
# and VirtualMachineAgentInstanceView. All the properties we validate are attributes of these classes and | ||
# initialized to None | ||
if instance_view.vm_agent is None: | ||
raise RetryableAgentStatusException("Agent status is invalid: 'vm_agent' property in instance view is None") | ||
|
||
# Validate vm_agent_version field | ||
vm_agent: VirtualMachineAgentInstanceView = instance_view.vm_agent | ||
if vm_agent.vm_agent_version is None: | ||
raise RetryableAgentStatusException("Agent status is invalid: 'vm_agent_version' property in instance view is None") | ||
elif 'Unknown' in vm_agent.vm_agent_version: | ||
raise RetryableAgentStatusException("Agent status is invalid: Instance view shows agent version is unknown") | ||
|
||
# Validate statuses field | ||
if vm_agent.statuses is None: | ||
raise RetryableAgentStatusException("Agent status is invalid: 'statuses' property in instance view is None") | ||
elif len(instance_view.vm_agent.statuses) < 1: | ||
raise RetryableAgentStatusException("Agent status is invalid: Instance view is missing an agent status entry") | ||
else: | ||
self.validate_instance_view_vmagent_status(instance_view=instance_view) | ||
|
||
log.info("Instance view has valid agent status, agent version: {0}, status: {1}" | ||
.format(vm_agent.vm_agent_version, vm_agent.statuses[0].display_status)) | ||
|
||
def check_status_updated(self, status_timestamp: datetime, prev_status_timestamp: datetime, gs_processed_log: str, prev_gs_processed_log: str): | ||
log.info("") | ||
log.info("Check that the agent status updated without processing any additional goal states...") | ||
|
||
# If prev_ variables are not updated, then this is the first reported agent status | ||
if prev_status_timestamp is not None and prev_gs_processed_log is not None: | ||
# The agent status timestamp should be greater than the prev timestamp | ||
if status_timestamp > prev_status_timestamp: | ||
log.info( | ||
"Current agent status timestamp {0} is greater than previous status timestamp {1}" | ||
.format(status_timestamp, prev_status_timestamp)) | ||
else: | ||
raise RetryableAgentStatusException("Agent status failed to update: Current agent status timestamp {0} " | ||
"is not greater than previous status timestamp {1}" | ||
.format(status_timestamp, prev_status_timestamp)) | ||
|
||
# The last goal state processed in the agent log should be the same as before | ||
if prev_gs_processed_log == gs_processed_log: | ||
log.info( | ||
"The last processed goal state is the same as the last processed goal state in the last agent " | ||
"status update: \n{0}".format(gs_processed_log) | ||
.format(status_timestamp, prev_status_timestamp)) | ||
else: | ||
raise Exception("Agent status failed to update without additional goal state: The agent processed an " | ||
"additional goal state since the last agent status update. \n{0}" | ||
"".format(gs_processed_log)) | ||
|
||
log.info("") | ||
log.info("The agent status successfully updated without additional goal states") | ||
|
||
def run(self): | ||
log.info("") | ||
log.info("*******Verifying the agent status updates 3 times*******") | ||
|
||
vm = VirtualMachineClient(self._context.vm) | ||
|
||
timeout = datetime.now() + timedelta(minutes=6) | ||
instance_view_exception = None | ||
status_updated = 0 | ||
prev_status_timestamp = None | ||
prev_gs_processed_log = None | ||
|
||
# Retry validating agent status updates 2 times with timeout of 6 minutes | ||
while datetime.now() <= timeout and status_updated < 2: | ||
instance_view = vm.get_instance_view() | ||
log.info("") | ||
log.info( | ||
"Check instance view to validate that the Guest Agent reports valid status...") | ||
log.info("Instance view of VM is:\n%s", json.dumps(instance_view.serialize(), indent=2)) | ||
|
||
try: | ||
# Validate the guest agent reports valid status | ||
self.validate_instance_view_vmagent(instance_view) | ||
|
||
status_timestamp = instance_view.vm_agent.statuses[0].time | ||
gs_processed_log = self._ssh_client.run_command( | ||
"agent_status-get_last_gs_processed.py", use_sudo=True) | ||
|
||
self.check_status_updated(status_timestamp, prev_status_timestamp, gs_processed_log, prev_gs_processed_log) | ||
|
||
# Update variables with timestamps for this update | ||
status_updated += 1 | ||
prev_status_timestamp = status_timestamp | ||
prev_gs_processed_log = gs_processed_log | ||
|
||
# Sleep 30s to allow agent status to update before we check again | ||
sleep(30) | ||
|
||
except RetryableAgentStatusException as e: | ||
instance_view_exception = str(e) | ||
log.info("") | ||
log.info(instance_view_exception) | ||
log.info("Waiting 30s before retry...") | ||
sleep(30) | ||
|
||
# If status_updated is 0, we know the agent status in the instance view was never valid | ||
log.info("") | ||
assert_that(status_updated > 0).described_as( | ||
"Timeout has expired, instance view has invalid agent status: {0}".format( | ||
instance_view_exception)).is_true() | ||
|
||
# Fail the test if we weren't able to validate the agent status updated 3 times | ||
assert_that(status_updated == 2).described_as( | ||
"Timeout has expired, the agent status failed to update 2 times").is_true() | ||
|
||
|
||
if __name__ == "__main__": | ||
AgentStatus.run_from_command_line() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,154 @@ | ||
#!/usr/bin/env python3 | ||
|
||
# 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 test adds multiple instances of RCv2 and verifies that the extensions are processed and deleted as expected. | ||
# | ||
|
||
import uuid | ||
from typing import Dict, Callable, Any | ||
|
||
from assertpy import fail | ||
from azure.mgmt.compute.models import VirtualMachineInstanceView | ||
|
||
from tests_e2e.tests.lib.agent_test import AgentTest | ||
from tests_e2e.tests.lib.identifiers import VmExtensionIds | ||
from tests_e2e.tests.lib.logging import log | ||
from tests_e2e.tests.lib.virtual_machine_client import VirtualMachineClient | ||
from tests_e2e.tests.lib.virtual_machine_extension_client import VirtualMachineExtensionClient | ||
|
||
|
||
class MultiConfigExt(AgentTest): | ||
class TestCase: | ||
def __init__(self, extension: VirtualMachineExtensionClient, get_settings: Callable[[str], Dict[str, str]]): | ||
self.extension = extension | ||
self.get_settings = get_settings | ||
self.test_guid: str = str(uuid.uuid4()) | ||
|
||
def enable_and_assert_test_cases(self, cases_to_enable: Dict[str, TestCase], cases_to_assert: Dict[str, TestCase], delete_extensions: bool = False): | ||
for resource_name, test_case in cases_to_enable.items(): | ||
log.info("") | ||
log.info("Adding {0} to the test VM. guid={1}".format(resource_name, test_case.test_guid)) | ||
test_case.extension.enable(settings=test_case.get_settings(test_case.test_guid)) | ||
test_case.extension.assert_instance_view() | ||
|
||
log.info("") | ||
log.info("Check that each extension has the expected guid in its status message...") | ||
for resource_name, test_case in cases_to_assert.items(): | ||
log.info("") | ||
log.info("Checking {0} has expected status message with {1}".format(resource_name, test_case.test_guid)) | ||
test_case.extension.assert_instance_view(expected_message=f"{test_case.test_guid}") | ||
|
||
# Delete each extension on the VM | ||
if delete_extensions: | ||
log.info("") | ||
log.info("Delete each extension...") | ||
self.delete_extensions(cases_to_assert) | ||
|
||
def delete_extensions(self, test_cases: Dict[str, TestCase]): | ||
for resource_name, test_case in test_cases.items(): | ||
log.info("") | ||
log.info("Deleting {0} from the test VM".format(resource_name)) | ||
test_case.extension.delete() | ||
|
||
log.info("") | ||
vm: VirtualMachineClient = VirtualMachineClient(self._context.vm) | ||
instance_view: VirtualMachineInstanceView = vm.get_instance_view() | ||
if instance_view.extensions is not None: | ||
for ext in instance_view.extensions: | ||
if ext.name in test_cases.keys(): | ||
fail("Extension was not deleted: \n{0}".format(ext)) | ||
log.info("") | ||
log.info("All extensions were successfully deleted.") | ||
|
||
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}"}} | ||
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, | ||
resource_name="MCExt1"), mc_settings), | ||
"MCExt2": MultiConfigExt.TestCase( | ||
VirtualMachineExtensionClient(self._context.vm, VmExtensionIds.RunCommandHandler, | ||
resource_name="MCExt2"), mc_settings), | ||
"MCExt3": MultiConfigExt.TestCase( | ||
VirtualMachineExtensionClient(self._context.vm, VmExtensionIds.RunCommandHandler, | ||
resource_name="MCExt3"), mc_settings), | ||
"CSE": MultiConfigExt.TestCase( | ||
VirtualMachineExtensionClient(self._context.vm, VmExtensionIds.CustomScript), sc_settings) | ||
} | ||
|
||
# Add each extension to the VM and validate the instance view has succeeded status with its assigned guid in the | ||
# status message | ||
log.info("") | ||
log.info("Add CSE and 3 instances of RCv2 to the VM. Each instance will echo a unique guid...") | ||
self.enable_and_assert_test_cases(cases_to_enable=test_cases, cases_to_assert=test_cases) | ||
|
||
# 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, | ||
resource_name="MCExt3"), mc_settings), | ||
"MCExt4": MultiConfigExt.TestCase( | ||
VirtualMachineExtensionClient(self._context.vm, VmExtensionIds.RunCommandHandler, | ||
resource_name="MCExt4"), mc_settings), | ||
"CSE": MultiConfigExt.TestCase( | ||
VirtualMachineExtensionClient(self._context.vm, VmExtensionIds.CustomScript), sc_settings) | ||
} | ||
test_cases.update(updated_test_cases) | ||
|
||
# Enable only the updated extensions, verify every extension has the correct test guid is in status message, and | ||
# remove all extensions from the test vm | ||
log.info("") | ||
log.info("Update MCExt3 and CSE with new guids and add a new instance of RCv2 to the VM...") | ||
self.enable_and_assert_test_cases(cases_to_enable=updated_test_cases, cases_to_assert=test_cases, | ||
delete_extensions=True) | ||
|
||
# Enable, verify, and remove only multi config extensions | ||
log.info("") | ||
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, | ||
resource_name="MCExt5"), mc_settings), | ||
"MCExt6": MultiConfigExt.TestCase( | ||
VirtualMachineExtensionClient(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, | ||
delete_extensions=True) | ||
|
||
# Enable, verify, and delete only single config extensions | ||
log.info("") | ||
log.info("Add only single-config extension to the VM...") | ||
sc_test_cases: Dict[str, MultiConfigExt.TestCase] = { | ||
"CSE": MultiConfigExt.TestCase( | ||
VirtualMachineExtensionClient(self._context.vm, VmExtensionIds.CustomScript), sc_settings) | ||
} | ||
self.enable_and_assert_test_cases(cases_to_enable=sc_test_cases, cases_to_assert=sc_test_cases, | ||
delete_extensions=True) | ||
|
||
|
||
if __name__ == "__main__": | ||
MultiConfigExt.run_from_command_line() |
Oops, something went wrong.