Skip to content

Commit

Permalink
Merge branch 'develop' into common
Browse files Browse the repository at this point in the history
  • Loading branch information
narrieta authored Aug 21, 2023
2 parents f4ba5e1 + 14f6124 commit a752ae8
Show file tree
Hide file tree
Showing 7 changed files with 417 additions and 3 deletions.
2 changes: 1 addition & 1 deletion tests_e2e/orchestrator/runbook.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ variable:
#
# The test suites to execute
- name: test_suites
value: "agent_bvt, no_outbound_connections, extensions_disabled, agent_not_provisioned, fips, agent_ext_workflow, agent_update"
value: "agent_bvt, no_outbound_connections, extensions_disabled, agent_not_provisioned, fips, agent_ext_workflow, agent_update, agent_status, multi_config_ext"
- name: cloud
value: "AzureCloud"
is_case_visible: true
Expand Down
9 changes: 9 additions & 0 deletions tests_e2e/test_suites/agent_status.yml
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"
9 changes: 9 additions & 0 deletions tests_e2e/test_suites/multi_config_ext.yml
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"
195 changes: 195 additions & 0 deletions tests_e2e/tests/agent_status/agent_status.py
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()
4 changes: 2 additions & 2 deletions tests_e2e/tests/lib/virtual_machine_extension_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,11 @@ class VirtualMachineExtensionClient(AzureClient):
"""
Client for operations virtual machine extensions.
"""
def __init__(self, vm: VmIdentifier, extension: VmExtensionIdentifier, resource_name: str):
def __init__(self, vm: VmIdentifier, extension: VmExtensionIdentifier, resource_name: str = None):
super().__init__()
self._vm: VmIdentifier = vm
self._identifier = extension
self._resource_name = resource_name
self._resource_name = resource_name or extension.type
cloud: Cloud = AZURE_CLOUDS[vm.cloud]
credential: DefaultAzureCredential = DefaultAzureCredential(authority=cloud.endpoints.active_directory)
self._compute_client: ComputeManagementClient = ComputeManagementClient(
Expand Down
154 changes: 154 additions & 0 deletions tests_e2e/tests/multi_config_ext/multi_config_ext.py
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()
Loading

0 comments on commit a752ae8

Please sign in to comment.