From 77f52c653b6a6595df4f7cdc3b44843eead079a3 Mon Sep 17 00:00:00 2001 From: Norberto Arrieta Date: Wed, 28 Sep 2022 13:56:14 -0700 Subject: [PATCH 01/63] Add logging statements for mrseq migration during update (#2667) (#2672) Co-authored-by: narrieta (cherry picked from commit 8a79ea77371859bde3ae1e0884c59f7530ea0d0d) --- azurelinuxagent/ga/exthandlers.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/azurelinuxagent/ga/exthandlers.py b/azurelinuxagent/ga/exthandlers.py index 99f3809446..c01fc15bca 100644 --- a/azurelinuxagent/ga/exthandlers.py +++ b/azurelinuxagent/ga/exthandlers.py @@ -1210,7 +1210,10 @@ def copy_status_files(self, old_ext_handler_i): old_ext_mrseq_file = os.path.join(old_ext_dir, "mrseq") if os.path.isfile(old_ext_mrseq_file): + logger.info("Migrating {0} to {1}.", old_ext_mrseq_file, new_ext_dir) shutil.copy2(old_ext_mrseq_file, new_ext_dir) + else: + logger.info("{0} does not exist, no migration is needed.", old_ext_mrseq_file) old_ext_status_dir = old_ext_handler_i.get_status_dir() new_ext_status_dir = self.get_status_dir() From 7cf7c724f9453a8024701f00dba2f60f4acb62a8 Mon Sep 17 00:00:00 2001 From: maddieford <93676569+maddieford@users.noreply.github.com> Date: Fri, 30 Sep 2022 14:48:23 -0700 Subject: [PATCH 02/63] Update CODEOWNERS (#2680) --- CODEOWNERS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CODEOWNERS b/CODEOWNERS index 32cd27f227..8707e60a58 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -21,4 +21,4 @@ # # Linux Agent team # -* @narrieta @ZhidongPeng @nagworld9 +* @narrieta @ZhidongPeng @nagworld9 @maddieford From 243ce393dcfd7f4457fbdaee101f4318ebbf6a80 Mon Sep 17 00:00:00 2001 From: Norberto Arrieta Date: Wed, 5 Oct 2022 13:26:18 -0700 Subject: [PATCH 03/63] Additional telemetry for goal state (#2675) (#2677) * Additional telemetry for goal state * add success message Co-authored-by: narrieta (cherry picked from commit e7641bd3321ed15a41114238fd348f85d6e7ced2) --- azurelinuxagent/common/protocol/goal_state.py | 39 +++++++++++++------ 1 file changed, 28 insertions(+), 11 deletions(-) diff --git a/azurelinuxagent/common/protocol/goal_state.py b/azurelinuxagent/common/protocol/goal_state.py index c2b95cfb25..ef47305037 100644 --- a/azurelinuxagent/common/protocol/goal_state.py +++ b/azurelinuxagent/common/protocol/goal_state.py @@ -192,7 +192,9 @@ def _update(self, force_update): incarnation, xml_text, xml_doc = GoalState._fetch_goal_state(self._wire_client) goal_state_updated = force_update or incarnation != self._incarnation if goal_state_updated: - self.logger.info('Fetched a new incarnation for the WireServer goal state [incarnation {0}]', incarnation) + message = 'Fetched a new incarnation for the WireServer goal state [incarnation {0}]'.format(incarnation) + self.logger.info(message) + add_event(op=WALAEventOperation.GoalState, message=message) vm_settings, vm_settings_updated = None, False try: @@ -203,11 +205,15 @@ def _update(self, force_update): if vm_settings_updated: self.logger.info('') - self.logger.info("Fetched new vmSettings [HostGAPlugin correlation ID: {0} eTag: {1} source: {2}]", vm_settings.hostga_plugin_correlation_id, vm_settings.etag, vm_settings.source) + message = "Fetched new vmSettings [HostGAPlugin correlation ID: {0} eTag: {1} source: {2}]".format(vm_settings.hostga_plugin_correlation_id, vm_settings.etag, vm_settings.source) + self.logger.info(message) + add_event(op=WALAEventOperation.GoalState, message=message) # Ignore the vmSettings if their source is Fabric (processing a Fabric goal state may require the tenant certificate and the vmSettings don't include it.) if vm_settings is not None and vm_settings.source == GoalStateSource.Fabric: if vm_settings_updated: - self.logger.info("The vmSettings originated via Fabric; will ignore them.") + message = "The vmSettings originated via Fabric; will ignore them." + self.logger.info(message) + add_event(op=WALAEventOperation.GoalState, message=message) vm_settings, vm_settings_updated = None, False # If neither goal state has changed we are done with the update @@ -265,7 +271,9 @@ def _check_certificates(self): raise GoalStateInconsistentError(message) def _restore_wire_server_goal_state(self, incarnation, xml_text, xml_doc, vm_settings_support_stopped_error): - self.logger.info('The HGAP stopped supporting vmSettings; will fetched the goal state from the WireServer.') + msg = 'The HGAP stopped supporting vmSettings; will fetched the goal state from the WireServer.' + self.logger.info(msg) + add_event(op=WALAEventOperation.VmSettings, message=msg) self._history = GoalStateHistory(datetime.datetime.utcnow(), incarnation) self._history.save_goal_state(xml_text) self._extensions_goal_state = self._fetch_full_wire_server_goal_state(incarnation, xml_doc) @@ -274,7 +282,7 @@ def _restore_wire_server_goal_state(self, incarnation, xml_text, xml_doc, vm_set msg = "Fetched a Fabric goal state older than the most recent FastTrack goal state; will skip it.\nFabric: {0}\nFastTrack: {1}".format( self._extensions_goal_state.created_on_timestamp, vm_settings_support_stopped_error.timestamp) self.logger.info(msg) - add_event(op=WALAEventOperation.VmSettings, message=msg, is_success=True) + add_event(op=WALAEventOperation.VmSettings, message=msg) def save_to_history(self, data, file_name): self._history.save(data, file_name) @@ -351,7 +359,9 @@ def _fetch_full_wire_server_goal_state(self, incarnation, xml_doc): """ try: self.logger.info('') - self.logger.info('Fetching full goal state from the WireServer [incarnation {0}]', incarnation) + message = 'Fetching full goal state from the WireServer [incarnation {0}]'.format(incarnation) + self.logger.info(message) + add_event(op=WALAEventOperation.GoalState, message=message) role_instance = find(xml_doc, "RoleInstance") role_instance_id = findtext(role_instance, "InstanceId") @@ -391,9 +401,12 @@ def _fetch_full_wire_server_goal_state(self, incarnation, xml_doc): certs = Certificates(xml_text, self.logger) # Log and save the certificates summary (i.e. the thumbprint but not the certificate itself) to the goal state history for c in certs.summary: - self.logger.info("Downloaded certificate {0}".format(c)) + message = "Downloaded certificate {0}".format(c) + self.logger.info(message) + add_event(op=WALAEventOperation.GoalState, message=message) if len(certs.warnings) > 0: self.logger.warn(certs.warnings) + add_event(op=WALAEventOperation.GoalState, message=certs.warnings) self._history.save_certificates(json.dumps(certs.summary)) remote_access = None @@ -418,7 +431,9 @@ def _fetch_full_wire_server_goal_state(self, incarnation, xml_doc): self.logger.warn("Fetching the goal state failed: {0}", ustr(exception)) raise ProtocolError(msg="Error fetching goal state", inner=exception) finally: - self.logger.info('Fetch goal state completed') + message = 'Fetch goal state completed' + self.logger.info(message) + add_event(op=WALAEventOperation.GoalState, message=message) class HostingEnv(object): @@ -455,9 +470,11 @@ def __init__(self, xml_text, my_logger): return # if the certificates format is not Pkcs7BlobWithPfxContents do not parse it - certificateFormat = findtext(xml_doc, "Format") - if certificateFormat and certificateFormat != "Pkcs7BlobWithPfxContents": - my_logger.warn("The Format is not Pkcs7BlobWithPfxContents. Format is " + certificateFormat) + certificate_format = findtext(xml_doc, "Format") + if certificate_format and certificate_format != "Pkcs7BlobWithPfxContents": + message = "The Format is not Pkcs7BlobWithPfxContents. Format is {0}".format(certificate_format) + my_logger.warn(message) + add_event(op=WALAEventOperation.GoalState, message=message) return cryptutil = CryptUtil(conf.get_openssl_cmd()) From aeda2c1ce95269d474850d7972a73a9b9e2a15ba Mon Sep 17 00:00:00 2001 From: Norberto Arrieta Date: Wed, 5 Oct 2022 13:31:08 -0700 Subject: [PATCH 04/63] Require HostGAPlugin >= 133 for Fast Track (#2673) (#2676) Co-authored-by: narrieta (cherry picked from commit a65135f7577faa605cb31ccae7d1f50766b46dd8) --- azurelinuxagent/common/protocol/hostplugin.py | 6 +++--- .../vm_settings-difference_in_required_features.json | 2 +- tests/data/hostgaplugin/vm_settings-empty_depends_on.json | 2 +- .../hostgaplugin/vm_settings-fabric-no_thumbprints.json | 2 +- tests/data/hostgaplugin/vm_settings-invalid_blob_type.json | 2 +- tests/data/hostgaplugin/vm_settings-missing_cert.json | 2 +- tests/data/hostgaplugin/vm_settings-no_manifests.json | 2 +- .../hostgaplugin/vm_settings-no_status_upload_blob.json | 2 +- tests/data/hostgaplugin/vm_settings-out-of-sync.json | 2 +- tests/data/hostgaplugin/vm_settings-parse_error.json | 2 +- tests/data/hostgaplugin/vm_settings-requested_version.json | 2 +- tests/data/hostgaplugin/vm_settings.json | 2 +- 12 files changed, 14 insertions(+), 14 deletions(-) diff --git a/azurelinuxagent/common/protocol/hostplugin.py b/azurelinuxagent/common/protocol/hostplugin.py index 55997d6490..0aaff2184d 100644 --- a/azurelinuxagent/common/protocol/hostplugin.py +++ b/azurelinuxagent/common/protocol/hostplugin.py @@ -491,7 +491,7 @@ def format_message(msg): try: # Raise if VmSettings are not supported, but check again periodically since the HostGAPlugin could have been updated since the last check # Note that self._host_plugin_supports_vm_settings can be None, so we need to compare against False - if self._supports_vm_settings == False and self._supports_vm_settings_next_check > datetime.datetime.now(): + if not self._supports_vm_settings and self._supports_vm_settings_next_check > datetime.datetime.now(): # Raise VmSettingsNotSupported directly instead of using raise_not_supported() to avoid resetting the timestamp for the next check raise VmSettingsNotSupported() @@ -551,8 +551,8 @@ def format_message(msg): logger.info(message) add_event(op=WALAEventOperation.HostPlugin, message=message, is_success=True) - # Don't support HostGAPlugin versions older than 124 - if vm_settings.host_ga_plugin_version < FlexibleVersion("1.0.8.124"): + # Don't support HostGAPlugin versions older than 133 + if vm_settings.host_ga_plugin_version < FlexibleVersion("1.0.8.133"): raise_not_supported() self._supports_vm_settings = True diff --git a/tests/data/hostgaplugin/vm_settings-difference_in_required_features.json b/tests/data/hostgaplugin/vm_settings-difference_in_required_features.json index a17776828e..71cdbf5c55 100644 --- a/tests/data/hostgaplugin/vm_settings-difference_in_required_features.json +++ b/tests/data/hostgaplugin/vm_settings-difference_in_required_features.json @@ -1,5 +1,5 @@ { - "hostGAPluginVersion": "1.0.8.124", + "hostGAPluginVersion": "1.0.8.133", "vmSettingsSchemaVersion": "0.0", "activityId": "a33f6f53-43d6-4625-b322-1a39651a00c9", "correlationId": "9a47a2a2-e740-4bfc-b11b-4f2f7cfe7d2e", diff --git a/tests/data/hostgaplugin/vm_settings-empty_depends_on.json b/tests/data/hostgaplugin/vm_settings-empty_depends_on.json index 94d9f0eb1f..2295ae85c0 100644 --- a/tests/data/hostgaplugin/vm_settings-empty_depends_on.json +++ b/tests/data/hostgaplugin/vm_settings-empty_depends_on.json @@ -1,5 +1,5 @@ { - "hostGAPluginVersion": "1.0.8.124", + "hostGAPluginVersion": "1.0.8.133", "vmSettingsSchemaVersion": "0.0", "activityId": "2e7f8b5d-f637-4721-b757-cb190d49b4e9", "correlationId": "1bef4c48-044e-4225-8f42-1d1eac1eb158", diff --git a/tests/data/hostgaplugin/vm_settings-fabric-no_thumbprints.json b/tests/data/hostgaplugin/vm_settings-fabric-no_thumbprints.json index bbd9459336..a98f0affe5 100644 --- a/tests/data/hostgaplugin/vm_settings-fabric-no_thumbprints.json +++ b/tests/data/hostgaplugin/vm_settings-fabric-no_thumbprints.json @@ -1,5 +1,5 @@ { - "hostGAPluginVersion": "1.0.8.124", + "hostGAPluginVersion": "1.0.8.133", "vmSettingsSchemaVersion": "0.0", "activityId": "a33f6f53-43d6-4625-b322-1a39651a00c9", "correlationId": "9a47a2a2-e740-4bfc-b11b-4f2f7cfe7d2e", diff --git a/tests/data/hostgaplugin/vm_settings-invalid_blob_type.json b/tests/data/hostgaplugin/vm_settings-invalid_blob_type.json index e7945845ac..ef63166dfb 100644 --- a/tests/data/hostgaplugin/vm_settings-invalid_blob_type.json +++ b/tests/data/hostgaplugin/vm_settings-invalid_blob_type.json @@ -1,5 +1,5 @@ { - "hostGAPluginVersion": "1.0.8.124", + "hostGAPluginVersion": "1.0.8.133", "vmSettingsSchemaVersion": "0.0", "activityId": "2e7f8b5d-f637-4721-b757-cb190d49b4e9", "correlationId": "1bef4c48-044e-4225-8f42-1d1eac1eb158", diff --git a/tests/data/hostgaplugin/vm_settings-missing_cert.json b/tests/data/hostgaplugin/vm_settings-missing_cert.json index a7192e942d..4ce8a20cf8 100644 --- a/tests/data/hostgaplugin/vm_settings-missing_cert.json +++ b/tests/data/hostgaplugin/vm_settings-missing_cert.json @@ -1,5 +1,5 @@ { - "hostGAPluginVersion": "1.0.8.124", + "hostGAPluginVersion": "1.0.8.133", "vmSettingsSchemaVersion": "0.0", "activityId": "a33f6f53-43d6-4625-b322-1a39651a00c9", "correlationId": "9a47a2a2-e740-4bfc-b11b-4f2f7cfe7d2e", diff --git a/tests/data/hostgaplugin/vm_settings-no_manifests.json b/tests/data/hostgaplugin/vm_settings-no_manifests.json index 7ec3a5c3d1..c8d2bf138f 100644 --- a/tests/data/hostgaplugin/vm_settings-no_manifests.json +++ b/tests/data/hostgaplugin/vm_settings-no_manifests.json @@ -1,5 +1,5 @@ { - "hostGAPluginVersion": "1.0.8.124", + "hostGAPluginVersion": "1.0.8.133", "vmSettingsSchemaVersion": "0.0", "activityId": "89d50bf1-fa55-4257-8af3-3db0c9f81ab4", "correlationId": "c143f8f0-a66b-4881-8c06-1efd278b0b02", diff --git a/tests/data/hostgaplugin/vm_settings-no_status_upload_blob.json b/tests/data/hostgaplugin/vm_settings-no_status_upload_blob.json index 2f70b55762..502d9a99c2 100644 --- a/tests/data/hostgaplugin/vm_settings-no_status_upload_blob.json +++ b/tests/data/hostgaplugin/vm_settings-no_status_upload_blob.json @@ -1,5 +1,5 @@ { - "hostGAPluginVersion": "1.0.8.124", + "hostGAPluginVersion": "1.0.8.133", "vmSettingsSchemaVersion": "0.0", "activityId": "a33f6f53-43d6-4625-b322-1a39651a00c9", "correlationId": "9a47a2a2-e740-4bfc-b11b-4f2f7cfe7d2e", diff --git a/tests/data/hostgaplugin/vm_settings-out-of-sync.json b/tests/data/hostgaplugin/vm_settings-out-of-sync.json index c35fdb5a33..0d4806af9d 100644 --- a/tests/data/hostgaplugin/vm_settings-out-of-sync.json +++ b/tests/data/hostgaplugin/vm_settings-out-of-sync.json @@ -1,5 +1,5 @@ { - "hostGAPluginVersion": "1.0.8.124", + "hostGAPluginVersion": "1.0.8.133", "vmSettingsSchemaVersion": "0.0", "activityId": "AAAAAAA-BBBB-CCCC-DDDD-EEEEEEEEEEEE", "correlationId": "EEEEEEEE-DDDD-CCCC-BBBB-AAAAAAAAAAAA", diff --git a/tests/data/hostgaplugin/vm_settings-parse_error.json b/tests/data/hostgaplugin/vm_settings-parse_error.json index bae5de4cb8..da82fda78e 100644 --- a/tests/data/hostgaplugin/vm_settings-parse_error.json +++ b/tests/data/hostgaplugin/vm_settings-parse_error.json @@ -1,5 +1,5 @@ { - "hostGAPluginVersion": "1.0.8.124", + "hostGAPluginVersion": "1.0.8.133", "vmSettingsSchemaVersion": THIS_IS_A_SYNTAX_ERROR, "activityId": "a33f6f53-43d6-4625-b322-1a39651a00c9", "correlationId": "9a47a2a2-e740-4bfc-b11b-4f2f7cfe7d2e", diff --git a/tests/data/hostgaplugin/vm_settings-requested_version.json b/tests/data/hostgaplugin/vm_settings-requested_version.json index e7e5135f90..0f73cb255e 100644 --- a/tests/data/hostgaplugin/vm_settings-requested_version.json +++ b/tests/data/hostgaplugin/vm_settings-requested_version.json @@ -1,5 +1,5 @@ { - "hostGAPluginVersion": "1.0.8.124", + "hostGAPluginVersion": "1.0.8.133", "vmSettingsSchemaVersion": "0.0", "activityId": "a33f6f53-43d6-4625-b322-1a39651a00c9", "correlationId": "9a47a2a2-e740-4bfc-b11b-4f2f7cfe7d2e", diff --git a/tests/data/hostgaplugin/vm_settings.json b/tests/data/hostgaplugin/vm_settings.json index 3018616ab4..1f6d44debc 100644 --- a/tests/data/hostgaplugin/vm_settings.json +++ b/tests/data/hostgaplugin/vm_settings.json @@ -1,5 +1,5 @@ { - "hostGAPluginVersion": "1.0.8.124", + "hostGAPluginVersion": "1.0.8.133", "vmSettingsSchemaVersion": "0.0", "activityId": "a33f6f53-43d6-4625-b322-1a39651a00c9", "correlationId": "9a47a2a2-e740-4bfc-b11b-4f2f7cfe7d2e", From faa8b14c165e7f57f43cf8e26a2453c86f15512d Mon Sep 17 00:00:00 2001 From: Nageswara Nandigam <84482346+nagworld9@users.noreply.github.com> Date: Thu, 6 Oct 2022 16:01:02 -0700 Subject: [PATCH 05/63] enforce memory usage for agent (#2671) * enforce memory usage for agent * address comments * fix pylint warning --- azurelinuxagent/common/cgroupconfigurator.py | 19 +++++- azurelinuxagent/common/conf.py | 20 +++++++ azurelinuxagent/common/event.py | 1 + azurelinuxagent/common/exception.py | 8 +++ azurelinuxagent/ga/update.py | 29 ++++++++- tests/common/test_cgroupconfigurator.py | 15 ++++- tests/ga/test_update.py | 62 ++++++++++++++++---- tests/test_agent.py | 2 + 8 files changed, 142 insertions(+), 14 deletions(-) diff --git a/azurelinuxagent/common/cgroupconfigurator.py b/azurelinuxagent/common/cgroupconfigurator.py index 47f1da35ab..0abee2201a 100644 --- a/azurelinuxagent/common/cgroupconfigurator.py +++ b/azurelinuxagent/common/cgroupconfigurator.py @@ -26,7 +26,7 @@ from azurelinuxagent.common.cgroup import CpuCgroup, AGENT_NAME_TELEMETRY, MetricsCounter, MemoryCgroup from azurelinuxagent.common.cgroupapi import CGroupsApi, SystemdCgroupsApi, SystemdRunError, EXTENSION_SLICE_PREFIX from azurelinuxagent.common.cgroupstelemetry import CGroupsTelemetry -from azurelinuxagent.common.exception import ExtensionErrorCodes, CGroupsException +from azurelinuxagent.common.exception import ExtensionErrorCodes, CGroupsException, AgentMemoryExceededException from azurelinuxagent.common.future import ustr from azurelinuxagent.common.osutil import get_osutil, systemd from azurelinuxagent.common.version import get_distro @@ -143,6 +143,7 @@ def __init__(self): self._cgroups_api = None self._agent_cpu_cgroup_path = None self._agent_memory_cgroup_path = None + self._agent_memory_cgroup = None self._check_cgroups_lock = threading.RLock() # Protect the check_cgroups which is called from Monitor thread and main loop. def initialize(self): @@ -194,7 +195,8 @@ def initialize(self): if self._agent_memory_cgroup_path is not None: _log_cgroup_info("Agent Memory cgroup: {0}", self._agent_memory_cgroup_path) - CGroupsTelemetry.track_cgroup(MemoryCgroup(AGENT_NAME_TELEMETRY, self._agent_memory_cgroup_path)) + self._agent_memory_cgroup = MemoryCgroup(AGENT_NAME_TELEMETRY, self._agent_memory_cgroup_path) + CGroupsTelemetry.track_cgroup(self._agent_memory_cgroup) _log_cgroup_info('Agent cgroups enabled: {0}', self._agent_cgroups_enabled) @@ -729,6 +731,19 @@ def _check_agent_throttled_time(cgroup_metrics): if metric.value > conf.get_agent_cpu_throttled_time_threshold(): raise CGroupsException("The agent has been throttled for {0} seconds".format(metric.value)) + def check_agent_memory_usage(self): + if self.enabled() and self._agent_memory_cgroup: + metrics = self._agent_memory_cgroup.get_tracked_metrics() + current_usage = 0 + for metric in metrics: + if metric.counter == MetricsCounter.TOTAL_MEM_USAGE: + current_usage += metric.value + elif metric.counter == MetricsCounter.SWAP_MEM_USAGE: + current_usage += metric.value + + if current_usage > conf.get_agent_memory_quota(): + raise AgentMemoryExceededException("The agent memory limit {0} bytes exceeded. The current reported usage is {1} bytes.".format(conf.get_agent_memory_quota(), current_usage)) + @staticmethod def _get_parent(pid): """ diff --git a/azurelinuxagent/common/conf.py b/azurelinuxagent/common/conf.py index 3c6e960fd0..46765ea989 100644 --- a/azurelinuxagent/common/conf.py +++ b/azurelinuxagent/common/conf.py @@ -136,6 +136,7 @@ def load_conf_from_file(conf_file_path, conf=__conf__): "Debug.CgroupLogMetrics": False, "Debug.CgroupDisableOnProcessCheckFailure": True, "Debug.CgroupDisableOnQuotaCheckFailure": True, + "Debug.EnableAgentMemoryUsageCheck": False, "Debug.EnableFastTrack": True, "Debug.EnableGAVersioning": False } @@ -186,6 +187,7 @@ def load_conf_from_file(conf_file_path, conf=__conf__): "Debug.CgroupCheckPeriod": 300, "Debug.AgentCpuQuota": 50, "Debug.AgentCpuThrottledTimeThreshold": 120, + "Debug.AgentMemoryQuota": 30 * 1024 ** 2, "Debug.EtpCollectionPeriod": 300, "Debug.AutoUpdateHotfixFrequency": 14400, "Debug.AutoUpdateNormalFrequency": 86400, @@ -555,6 +557,24 @@ def get_agent_cpu_throttled_time_threshold(conf=__conf__): return conf.get_int("Debug.AgentCpuThrottledTimeThreshold", 120) +def get_agent_memory_quota(conf=__conf__): + """ + Memory quota for the agent in bytes. + + NOTE: This option is experimental and may be removed in later versions of the Agent. + """ + return conf.get_int("Debug.AgentMemoryQuota", 30 * 1024 ** 2) + + +def get_enable_agent_memory_usage_check(conf=__conf__): + """ + If True, Agent checks it's Memory usage. + + NOTE: This option is experimental and may be removed in later versions of the Agent. + """ + return conf.get_switch("Debug.EnableAgentMemoryUsageCheck", False) + + def get_cgroup_monitor_expiry_time(conf=__conf__): """ cgroups monitoring for pilot extensions disabled after expiry time diff --git a/azurelinuxagent/common/event.py b/azurelinuxagent/common/event.py index b7aba5e415..1f903a9faa 100644 --- a/azurelinuxagent/common/event.py +++ b/azurelinuxagent/common/event.py @@ -69,6 +69,7 @@ class WALAEventOperation: ActivateResourceDisk = "ActivateResourceDisk" AgentBlacklisted = "AgentBlacklisted" AgentEnabled = "AgentEnabled" + AgentMemory = "AgentMemory" AgentUpgrade = "AgentUpgrade" ArtifactsProfileBlob = "ArtifactsProfileBlob" CGroupsCleanUp = "CGroupsCleanUp" diff --git a/azurelinuxagent/common/exception.py b/azurelinuxagent/common/exception.py index 9b16c42678..0484662327 100644 --- a/azurelinuxagent/common/exception.py +++ b/azurelinuxagent/common/exception.py @@ -58,6 +58,14 @@ def __init__(self, msg=None, inner=None): super(AgentConfigError, self).__init__(msg, inner) +class AgentMemoryExceededException(AgentError): + """ + When Agent memory limit reached. + """ + def __init__(self, msg=None, inner=None): + super(AgentMemoryExceededException, self).__init__(msg, inner) + + class AgentNetworkError(AgentError): """ When network is not available. diff --git a/azurelinuxagent/ga/update.py b/azurelinuxagent/ga/update.py index 0fac66f259..58766847e1 100644 --- a/azurelinuxagent/ga/update.py +++ b/azurelinuxagent/ga/update.py @@ -39,7 +39,8 @@ from azurelinuxagent.common.cgroupconfigurator import CGroupConfigurator from azurelinuxagent.common.event import add_event, initialize_event_logger_vminfo_common_parameters, \ WALAEventOperation, EVENTS_DIRECTORY -from azurelinuxagent.common.exception import ResourceGoneError, UpdateError, ExitException, AgentUpgradeExitException +from azurelinuxagent.common.exception import ResourceGoneError, UpdateError, ExitException, AgentUpgradeExitException, \ + AgentMemoryExceededException from azurelinuxagent.common.future import ustr from azurelinuxagent.common.osutil import get_osutil, systemd from azurelinuxagent.common.persist_firewall_rules import PersistFirewallRulesHandler @@ -137,6 +138,7 @@ def get_update_handler(): class UpdateHandler(object): TELEMETRY_HEARTBEAT_PERIOD = timedelta(minutes=30) + CHECK_MEMORY_USAGE_PERIOD = timedelta(seconds=conf.get_cgroup_check_period()) def __init__(self): self.osutil = get_osutil() @@ -162,6 +164,9 @@ def __init__(self): self._heartbeat_id = str(uuid.uuid4()).upper() self._heartbeat_counter = 0 + self._last_check_memory_usage = datetime.min + self._check_memory_usage_last_error_report = datetime.min + # VM Size is reported via the heartbeat, default it here. self._vm_size = None @@ -401,6 +406,7 @@ def run(self, debug=False): self._check_threads_running(all_thread_handlers) self._process_goal_state(exthandlers_handler, remote_access_handler) self._send_heartbeat_telemetry(protocol) + self._check_agent_memory_usage() time.sleep(self._goal_state_period) except AgentUpgradeExitException as exitException: @@ -1288,6 +1294,27 @@ def _send_heartbeat_telemetry(self, protocol): self._heartbeat_update_goal_state_error_count = 0 self._last_telemetry_heartbeat = datetime.utcnow() + def _check_agent_memory_usage(self): + """ + This checks the agent current memory usage and safely exit the process if agent reaches the memory limit + """ + try: + if conf.get_enable_agent_memory_usage_check() and self._extensions_summary.converged: + if self._last_check_memory_usage == datetime.min or datetime.utcnow() >= (self._last_check_memory_usage + UpdateHandler.CHECK_MEMORY_USAGE_PERIOD): + self._last_check_memory_usage = datetime.utcnow() + CGroupConfigurator.get_instance().check_agent_memory_usage() + except AgentMemoryExceededException as exception: + msg = "Check on agent memory usage:\n{0}".format(ustr(exception)) + logger.info(msg) + add_event(AGENT_NAME, op=WALAEventOperation.AgentMemory, is_success=True, message=msg) + raise ExitException("Agent {0} is reached memory limit -- exiting".format(CURRENT_AGENT)) + except Exception as exception: + if self._check_memory_usage_last_error_report == datetime.min or (self._check_memory_usage_last_error_report + timedelta(hours=6)) > datetime.now(): + self._check_memory_usage_last_error_report = datetime.now() + msg = "Error checking the agent's memory usage: {0} --- [NOTE: Will not log the same error for the 6 hours]".format(ustr(exception)) + logger.warn(msg) + add_event(AGENT_NAME, op=WALAEventOperation.AgentMemory, is_success=False, message=msg) + @staticmethod def _ensure_extension_telemetry_state_configured_properly(protocol): etp_enabled = get_supported_feature_by_name(SupportedFeatureNames.ExtensionTelemetryPipeline).is_supported diff --git a/tests/common/test_cgroupconfigurator.py b/tests/common/test_cgroupconfigurator.py index 62a7211a70..60a7cfde1c 100644 --- a/tests/common/test_cgroupconfigurator.py +++ b/tests/common/test_cgroupconfigurator.py @@ -33,7 +33,8 @@ from azurelinuxagent.common.cgroupconfigurator import CGroupConfigurator, DisableCgroups from azurelinuxagent.common.cgroupstelemetry import CGroupsTelemetry from azurelinuxagent.common.event import WALAEventOperation -from azurelinuxagent.common.exception import CGroupsException, ExtensionError, ExtensionErrorCodes +from azurelinuxagent.common.exception import CGroupsException, ExtensionError, ExtensionErrorCodes, \ + AgentMemoryExceededException from azurelinuxagent.common.future import ustr from azurelinuxagent.common.utils import shellutil, fileutil from tests.common.mock_environment import MockCommand @@ -986,3 +987,15 @@ def test_check_cgroups_should_disable_cgroups_when_a_check_fails(self): finally: for p in patchers: p.stop() + + def test_check_agent_memory_usage_should_raise_a_cgroups_exception_when_the_limit_is_exceeded(self): + metrics = [MetricValue(MetricsCategory.MEMORY_CATEGORY, MetricsCounter.TOTAL_MEM_USAGE, AGENT_NAME_TELEMETRY, conf.get_agent_memory_quota() + 1), + MetricValue(MetricsCategory.MEMORY_CATEGORY, MetricsCounter.SWAP_MEM_USAGE, AGENT_NAME_TELEMETRY, conf.get_agent_memory_quota() + 1)] + + with self.assertRaises(AgentMemoryExceededException) as context_manager: + with self._get_cgroup_configurator() as configurator: + with patch("azurelinuxagent.common.cgroup.MemoryCgroup.get_tracked_metrics") as tracked_metrics: + tracked_metrics.return_value = metrics + configurator.check_agent_memory_usage() + + self.assertIn("The agent memory limit {0} bytes exceeded".format(conf.get_agent_memory_quota()), ustr(context_manager.exception), "An incorrect exception was raised") \ No newline at end of file diff --git a/tests/ga/test_update.py b/tests/ga/test_update.py index 695b6d578d..0bbd22791d 100644 --- a/tests/ga/test_update.py +++ b/tests/ga/test_update.py @@ -28,7 +28,8 @@ from azurelinuxagent.common import conf from azurelinuxagent.common.event import EVENTS_DIRECTORY, WALAEventOperation -from azurelinuxagent.common.exception import ProtocolError, UpdateError, ResourceGoneError, HttpError +from azurelinuxagent.common.exception import ProtocolError, UpdateError, ResourceGoneError, HttpError, \ + ExitException, AgentMemoryExceededException from azurelinuxagent.common.future import ustr, httpclient from azurelinuxagent.common.persist_firewall_rules import PersistFirewallRulesHandler from azurelinuxagent.common.protocol.hostplugin import URI_FORMAT_GET_API_VERSIONS, HOST_PLUGIN_PORT, \ @@ -1461,7 +1462,7 @@ def _get_test_ext_handler_instance(protocol, name="OSTCExtensions.ExampleHandler eh = Extension(name=name) eh.version = version return ExtHandlerInstance(eh, protocol) - + def test_update_handler_recovers_from_error_with_no_certs(self): data = DATA_FILE.copy() data['goal_state'] = 'wire/goal_state_no_certs.xml' @@ -1489,7 +1490,7 @@ def match_unexpected_errors(): for (args, _) in filter(lambda a: len(a) > 0, patched_error.call_args_list): if unexpected_msg_fragment in args[0]: matching_errors.append(args[0]) - + if len(matching_errors) > 1: self.fail("Guest Agent did not recover, with new error(s): {}"\ .format(matching_errors[1:])) @@ -2894,7 +2895,7 @@ def vm_settings_not_supported(url, *_, **__): if HttpRequestPredicates.is_host_plugin_vm_settings_request(url): return MockHttpResponse(404) return None - + with mock_wire_protocol(data) as protocol: def mock_live_migration(iteration): @@ -2904,7 +2905,7 @@ def mock_live_migration(iteration): elif iteration == 2: protocol.mock_wire_data.set_incarnation(3) protocol.set_http_handlers(http_get_handler=vm_settings_not_supported) - + with mock_update_handler(protocol, 3, on_new_iteration=mock_live_migration) as update_handler: with patch("azurelinuxagent.ga.update.logger.error") as patched_error: def check_for_errors(): @@ -2916,7 +2917,7 @@ def check_for_errors(): update_handler.run(debug=True) check_for_errors() - + timestamp = protocol.client.get_host_plugin()._fast_track_timestamp self.assertEqual(timestamp, timeutil.create_timestamp(datetime.min), "Expected fast track time stamp to be set to {0}, got {1}".format(datetime.min, timestamp)) @@ -2926,16 +2927,16 @@ class HeartbeatTestCase(AgentTestCase): @patch("azurelinuxagent.common.logger.info") @patch("azurelinuxagent.ga.update.add_event") def test_telemetry_heartbeat_creates_event(self, patch_add_event, patch_info, *_): - + with mock_wire_protocol(mockwiredata.DATA_FILE) as mock_protocol: update_handler = get_update_handler() - + update_handler.last_telemetry_heartbeat = datetime.utcnow() - timedelta(hours=1) update_handler._send_heartbeat_telemetry(mock_protocol) self.assertEqual(1, patch_add_event.call_count) self.assertTrue(any(call_args[0] == "[HEARTBEAT] Agent {0} is running as the goal state agent {1}" for call_args in patch_info.call_args), "The heartbeat was not written to the agent's log") - + @patch("azurelinuxagent.ga.update.add_event") @patch("azurelinuxagent.common.protocol.imds.ImdsClient") def test_telemetry_heartbeat_retries_failed_vm_size_fetch(self, mock_imds_factory, patch_add_event, *_): @@ -2953,7 +2954,7 @@ def validate_single_heartbeat_event_matches_vm_size(vm_size): self.assertTrue(telemetry_message.endswith(vm_size), "Expected HeartBeat message ('{0}') to end with the test vmSize value, {1}."\ .format(telemetry_message, vm_size)) - + with mock_wire_protocol(mockwiredata.DATA_FILE) as mock_protocol: update_handler = get_update_handler() update_handler.protocol_util.get_protocol = Mock(return_value=mock_protocol) @@ -2979,6 +2980,47 @@ def validate_single_heartbeat_event_matches_vm_size(vm_size): validate_single_heartbeat_event_matches_vm_size("TestVmSizeValue") +class AgentMemoryCheckTestCase(AgentTestCase): + + @patch("azurelinuxagent.common.logger.info") + @patch("azurelinuxagent.ga.update.add_event") + def test_check_agent_memory_usage_raises_exit_exception(self, patch_add_event, patch_info, *_): + with patch("azurelinuxagent.common.cgroupconfigurator.CGroupConfigurator._Impl.check_agent_memory_usage", side_effect=AgentMemoryExceededException()): + with patch('azurelinuxagent.common.conf.get_enable_agent_memory_usage_check', return_value=True): + with self.assertRaises(ExitException) as context_manager: + update_handler = get_update_handler() + + update_handler._check_agent_memory_usage() + self.assertEqual(1, patch_add_event.call_count) + self.assertTrue(any("Check on agent memory usage" in call_args[0] + for call_args in patch_info.call_args), + "The memory check was not written to the agent's log") + self.assertIn("Agent {0} is reached memory limit -- exiting".format(CURRENT_AGENT), + ustr(context_manager.exception), "An incorrect exception was raised") + + @patch("azurelinuxagent.common.logger.warn") + @patch("azurelinuxagent.ga.update.add_event") + def test_check_agent_memory_usage_fails(self, patch_add_event, patch_warn, *_): + with patch("azurelinuxagent.common.cgroupconfigurator.CGroupConfigurator._Impl.check_agent_memory_usage", side_effect=Exception()): + with patch('azurelinuxagent.common.conf.get_enable_agent_memory_usage_check', return_value=True): + update_handler = get_update_handler() + + update_handler._check_agent_memory_usage() + self.assertTrue(any("Error checking the agent's memory usage" in call_args[0] + for call_args in patch_warn.call_args), + "The memory check was not written to the agent's log") + self.assertEqual(1, patch_add_event.call_count) + add_events = [kwargs for _, kwargs in patch_add_event.call_args_list if + kwargs["op"] == WALAEventOperation.AgentMemory] + self.assertTrue( + len(add_events) == 1, + "Exactly 1 event should have been emitted when memory usage check fails. Got: {0}".format(add_events)) + self.assertIn( + "Error checking the agent's memory usage", + add_events[0]["message"], + "The error message is not correct when memory usage check failed") + + class GoalStateIntervalTestCase(AgentTestCase): def test_initial_goal_state_period_should_default_to_goal_state_period(self): configuration_provider = conf.ConfigurationProvider() diff --git a/tests/test_agent.py b/tests/test_agent.py index 1b14c9d169..c585e845ee 100644 --- a/tests/test_agent.py +++ b/tests/test_agent.py @@ -31,6 +31,7 @@ DVD.MountPoint = /mnt/cdrom/secure Debug.AgentCpuQuota = 50 Debug.AgentCpuThrottledTimeThreshold = 120 +Debug.AgentMemoryQuota = 31457280 Debug.AutoUpdateHotfixFrequency = 14400 Debug.AutoUpdateNormalFrequency = 86400 Debug.CgroupCheckPeriod = 300 @@ -39,6 +40,7 @@ Debug.CgroupLogMetrics = False Debug.CgroupMonitorExpiryTime = 2022-03-31 Debug.CgroupMonitorExtensionName = Microsoft.Azure.Monitor.AzureMonitorLinuxAgent +Debug.EnableAgentMemoryUsageCheck = False Debug.EnableFastTrack = True Debug.EnableGAVersioning = False Debug.EtpCollectionPeriod = 300 From 527443c105fd9bbbad99f03905f2bf9efdb4a8b5 Mon Sep 17 00:00:00 2001 From: Norberto Arrieta Date: Tue, 18 Oct 2022 09:08:44 -0700 Subject: [PATCH 06/63] Use common download logic for agent downloads (#2682) * Use common download logic for agent downloads * rename method * add unit test Co-authored-by: narrieta --- azurelinuxagent/common/protocol/goal_state.py | 7 - azurelinuxagent/common/protocol/wire.py | 54 ++- azurelinuxagent/ga/exthandlers.py | 15 +- azurelinuxagent/ga/update.py | 152 ++------ .../ga/WALinuxAgent-9.9.9.9-no_manifest.zip | Bin 0 -> 637559 bytes tests/data/ga/fake_extension.zip | Bin 0 -> 169 bytes tests/ga/test_extension.py | 125 +------ .../ga/test_exthandlers_download_extension.py | 13 +- tests/ga/test_update.py | 348 +++++------------- tests/protocol/test_hostplugin.py | 2 +- tests/protocol/test_imds.py | 24 +- tests/protocol/test_wire.py | 69 +++- tests/test_agent.py | 6 +- 13 files changed, 248 insertions(+), 567 deletions(-) create mode 100644 tests/data/ga/WALinuxAgent-9.9.9.9-no_manifest.zip create mode 100644 tests/data/ga/fake_extension.zip diff --git a/azurelinuxagent/common/protocol/goal_state.py b/azurelinuxagent/common/protocol/goal_state.py index ef47305037..664b70ef1b 100644 --- a/azurelinuxagent/common/protocol/goal_state.py +++ b/azurelinuxagent/common/protocol/goal_state.py @@ -152,13 +152,6 @@ def _fetch_manifest(self, manifest_type, name, uris): except Exception as e: raise ProtocolError("Failed to retrieve {0} manifest. Error: {1}".format(manifest_type, ustr(e))) - def download_extension(self, uris, destination, on_downloaded=lambda: True): - """ - This is a convenience method that wraps WireClient.download_extension(), but adds the required 'use_verify_header' parameter. - """ - is_fast_track = self.extensions_goal_state.source == GoalStateSource.FastTrack - self._wire_client.download_extension(uris, destination, use_verify_header=is_fast_track, on_downloaded=on_downloaded) - @staticmethod def update_host_plugin_headers(wire_client): """ diff --git a/azurelinuxagent/common/protocol/wire.py b/azurelinuxagent/common/protocol/wire.py index 167d4820a5..d5aeda71c7 100644 --- a/azurelinuxagent/common/protocol/wire.py +++ b/azurelinuxagent/common/protocol/wire.py @@ -19,7 +19,9 @@ import json import os import random +import shutil import time +import zipfile from collections import defaultdict from datetime import datetime, timedelta @@ -604,25 +606,29 @@ def hgap_download(uri): return self._download_with_fallback_channel(download_type, uris, direct_download=direct_download, hgap_download=hgap_download) - def download_extension(self, uris, destination, use_verify_header, on_downloaded=lambda: True): + def download_zip_package(self, package_type, uris, target_file, target_directory, use_verify_header): """ - Walks the given list of 'uris' issuing HTTP GET requests and saves the content of the first successful request to 'destination'. + Downloads the ZIP package specified in 'uris' (which is a list of alternate locations for the ZIP), saving it to 'target_file' and then expanding + its contents to 'target_directory'. Deletes the target file after it has been expanded. - When the download is successful, this method invokes the 'on_downloaded' callback function, which can be used to process the results of the download. - on_downloaded() should return True on success and False on failure (it should not raise any exceptions); ff the return value is False, the download - is considered a failure and the next URI is tried. + The 'package_type' is only used in log messages and has no other semantics. It should specify the contents of the ZIP, e.g. "extension package" + or "agent package" + + The 'use_verify_header' parameter indicates whether the verify header should be added when using the extensionArtifact API of the HostGAPlugin. """ host_ga_plugin = self.get_host_plugin() - direct_download = lambda uri: self.stream(uri, destination, headers=None, use_proxy=True) + direct_download = lambda uri: self.stream(uri, target_file, headers=None, use_proxy=True) def hgap_download(uri): request_uri, request_headers = host_ga_plugin.get_artifact_request(uri, use_verify_header=use_verify_header, artifact_manifest_url=host_ga_plugin.manifest_uri) - return self.stream(request_uri, destination, headers=request_headers, use_proxy=False) + return self.stream(request_uri, target_file, headers=request_headers, use_proxy=False) + + on_downloaded = lambda: WireClient._try_expand_zip_package(package_type, target_file, target_directory) - self._download_with_fallback_channel("extension package", uris, direct_download=direct_download, hgap_download=hgap_download, on_downloaded=on_downloaded) + self._download_with_fallback_channel(package_type, uris, direct_download=direct_download, hgap_download=hgap_download, on_downloaded=on_downloaded) - def _download_with_fallback_channel(self, download_type, uris, direct_download, hgap_download, on_downloaded=lambda: True): + def _download_with_fallback_channel(self, download_type, uris, direct_download, hgap_download, on_downloaded=None): """ Walks the given list of 'uris' issuing HTTP GET requests, attempting to download the content of each URI. The download is done using both the default and the fallback channels, until one of them succeeds. The 'direct_download' and 'hgap_download' functions define the logic to do direct calls to the URI or @@ -630,9 +636,9 @@ def _download_with_fallback_channel(self, download_type, uris, direct_download, but the default can be depending on the success/failure of each channel (see _download_using_appropriate_channel() for the logic to do this). The 'download_type' is added to any log messages produced by this method; it should describe the type of content of the given URIs - (e.g. "manifest", "extension package", etc). + (e.g. "manifest", "extension package, "agent package", etc). - When the download is successful download_extension() invokes the 'on_downloaded' function, which can be used to process the results of the download. This + When the download is successful, _download_with_fallback_channel invokes the 'on_downloaded' function, which can be used to process the results of the download. This function should return True on success, and False on failure (it should not raise any exceptions). If the return value is False, the download is considered a failure and the next URI is tried. @@ -641,7 +647,7 @@ def _download_with_fallback_channel(self, download_type, uris, direct_download, This method enforces a timeout (_DOWNLOAD_TIMEOUT) on the download and raises an exception if the limit is exceeded. """ - logger.verbose("Downloading {0}", download_type) + logger.info("Downloading {0}", download_type) start_time = datetime.now() uris_shuffled = uris @@ -658,14 +664,34 @@ def _download_with_fallback_channel(self, download_type, uris, direct_download, # Disable W0640: OK to use uri in a lambda within the loop's body response = self._download_using_appropriate_channel(lambda: direct_download(uri), lambda: hgap_download(uri)) # pylint: disable=W0640 - if on_downloaded(): - return uri, response + if on_downloaded is not None: + on_downloaded() + return uri, response except Exception as exception: most_recent_error = exception raise ExtensionDownloadError("Failed to download {0} from all URIs. Last error: {1}".format(download_type, ustr(most_recent_error)), code=ExtensionErrorCodes.PluginManifestDownloadError) + @staticmethod + def _try_expand_zip_package(package_type, target_file, target_directory): + logger.info("Unzipping {0}: {1}", package_type, target_file) + try: + zipfile.ZipFile(target_file).extractall(target_directory) + except Exception as exception: + logger.error("Error while unzipping {0}: {1}", package_type, ustr(exception)) + if os.path.exists(target_directory): + try: + shutil.rmtree(target_directory) + except Exception as exception: + logger.warn("Cannot delete {0}: {1}", target_directory, ustr(exception)) + raise + finally: + try: + os.remove(target_file) + except Exception as exception: + logger.warn("Cannot delete {0}: {1}", target_file, ustr(exception)) + def stream(self, uri, destination, headers=None, use_proxy=None): """ Downloads the content of the given 'uri' and saves it to the 'destination' file. diff --git a/azurelinuxagent/ga/exthandlers.py b/azurelinuxagent/ga/exthandlers.py index c01fc15bca..974ab19f9b 100644 --- a/azurelinuxagent/ga/exthandlers.py +++ b/azurelinuxagent/ga/exthandlers.py @@ -46,6 +46,7 @@ ExtensionOperationError, ExtensionUpdateError, ProtocolError, ProtocolNotFoundError, ExtensionsGoalStateError, \ GoalStateAggregateStatusCodes, MultiConfigExtensionEnableError from azurelinuxagent.common.future import ustr, is_file_not_found_error +from azurelinuxagent.common.protocol.extensions_goal_state import GoalStateSource from azurelinuxagent.common.protocol.restapi import ExtensionStatus, ExtensionSubStatus, Extension, ExtHandlerStatus, \ VMStatus, GoalStateAggregateStatus, ExtensionState, ExtensionRequestedState, ExtensionSettings from azurelinuxagent.common.utils import textutil @@ -1252,21 +1253,23 @@ def download(self): if self.pkg is None or self.pkg.uris is None or len(self.pkg.uris) == 0: raise ExtensionDownloadError("No package uri found") - destination = os.path.join(conf.get_lib_dir(), self.get_extension_package_zipfile_name()) + package_file = os.path.join(conf.get_lib_dir(), self.get_extension_package_zipfile_name()) package_exists = False - if os.path.exists(destination): - self.logger.info("Using existing extension package: {0}", destination) - if self._unzip_extension_package(destination, self.get_base_dir()): + if os.path.exists(package_file): + self.logger.info("Using existing extension package: {0}", package_file) + if self._unzip_extension_package(package_file, self.get_base_dir()): package_exists = True else: self.logger.info("The existing extension package is invalid, will ignore it.") if not package_exists: - self.protocol.get_goal_state().download_extension(self.pkg.uris, destination, on_downloaded=lambda: self._unzip_extension_package(destination, self.get_base_dir())) + is_fast_track_goal_state = self.protocol.get_goal_state().extensions_goal_state.source == GoalStateSource.FastTrack + self.protocol.client.download_zip_package("extension package", self.pkg.uris, package_file, self.get_base_dir(), use_verify_header=is_fast_track_goal_state) self.report_event(message="Download succeeded", duration=elapsed_milliseconds(begin_utc)) - self.pkg_file = destination + self.pkg_file = package_file + def ensure_consistent_data_for_mc(self): # If CRP expects Handler to support MC, ensure the HandlerManifest also reflects that. diff --git a/azurelinuxagent/ga/update.py b/azurelinuxagent/ga/update.py index 58766847e1..8d2c97df20 100644 --- a/azurelinuxagent/ga/update.py +++ b/azurelinuxagent/ga/update.py @@ -19,7 +19,6 @@ import glob import json import os -import random import re import shutil import signal @@ -28,19 +27,17 @@ import sys import time import uuid -import zipfile from datetime import datetime, timedelta from azurelinuxagent.common import conf from azurelinuxagent.common import logger from azurelinuxagent.common.protocol.imds import get_imds_client -from azurelinuxagent.common.utils import fileutil, restutil, textutil +from azurelinuxagent.common.utils import fileutil, textutil from azurelinuxagent.common.agent_supported_feature import get_supported_feature_by_name, SupportedFeatureNames from azurelinuxagent.common.cgroupconfigurator import CGroupConfigurator from azurelinuxagent.common.event import add_event, initialize_event_logger_vminfo_common_parameters, \ WALAEventOperation, EVENTS_DIRECTORY -from azurelinuxagent.common.exception import ResourceGoneError, UpdateError, ExitException, AgentUpgradeExitException, \ - AgentMemoryExceededException +from azurelinuxagent.common.exception import UpdateError, ExitException, AgentUpgradeExitException, AgentMemoryExceededException from azurelinuxagent.common.future import ustr from azurelinuxagent.common.osutil import get_osutil, systemd from azurelinuxagent.common.persist_firewall_rules import PersistFirewallRulesHandler @@ -950,9 +947,6 @@ def _find_agents(self): logger.warn(u"Exception occurred loading available agents: {0}", ustr(e)) return - def _get_host_plugin(self, protocol): - return protocol.client.get_host_plugin() if protocol and protocol.client else None - def _get_pid_parts(self): pid_file = conf.get_agent_pid_file_path() pid_dir = os.path.dirname(pid_file) @@ -991,7 +985,7 @@ def _is_orphaned(self): def _load_agents(self): path = os.path.join(conf.get_lib_dir(), "{0}-*".format(AGENT_NAME)) - return [GuestAgent(path=agent_dir) + return [GuestAgent.from_installed_agent(agent_dir) for agent_dir in glob.iglob(path) if os.path.isdir(agent_dir)] def _partition(self): @@ -1206,8 +1200,8 @@ def agent_upgrade_time_elapsed(now_): # Set the agents to those available for download at least as current as the existing agent # or to the requested version (if specified) - host = self._get_host_plugin(protocol=protocol) - agents_to_download = [GuestAgent(is_fast_track_goal_state=self._goal_state.extensions_goal_state.source == GoalStateSource.FastTrack, pkg=pkg, host=host) for pkg in packages_to_download] + is_fast_track_goal_state = self._goal_state.extensions_goal_state.source == GoalStateSource.FastTrack + agents_to_download = [GuestAgent.from_agent_package(pkg, protocol, is_fast_track_goal_state) for pkg in packages_to_download] # Filter out the agents that were downloaded/extracted successfully. If the agent was not installed properly, # we delete the directory and the zip package from the filesystem @@ -1494,18 +1488,20 @@ def _reset_legacy_blacklisted_agents(self): class GuestAgent(object): - def __init__(self, path=None, pkg=None, is_fast_track_goal_state=False, host=None): + def __init__(self, path, pkg, protocol, is_fast_track_goal_state): """ If 'path' is given, the object is initialized to the version installed under that path. If 'pkg' is given, the version specified in the package information is downloaded and the object is initialized to that version. - 'is_fast_track_goal_state' and 'host' are using only when a package is downloaded. + 'is_fast_track_goal_state' and 'protocol' are used only when a package is downloaded. + + NOTE: Prefer using the from_installed_agent and from_agent_package methods instead of calling __init__ directly """ self._is_fast_track_goal_state = is_fast_track_goal_state self.pkg = pkg - self.host = host + self._protocol = protocol version = None if path is not None: m = AGENT_DIR_PATTERN.match(path) @@ -1529,26 +1525,12 @@ def __init__(self, path=None, pkg=None, is_fast_track_goal_state=False, host=Non self._ensure_downloaded() self._ensure_loaded() except Exception as e: - if isinstance(e, ResourceGoneError): - raise - - # The agent was improperly blacklisting versions due to a timeout - # encountered while downloading a later version. Errors of type - # socket.error are IOError, so this should provide sufficient - # protection against a large class of I/O operation failures. - if isinstance(e, IOError): - raise - - # If we're unable to download/unpack the agent, delete the Agent directory and the zip file (if exists) to - # ensure we try downloading again in the next round. + # If we're unable to download/unpack the agent, delete the Agent directory try: if os.path.isdir(self.get_agent_dir()): shutil.rmtree(self.get_agent_dir(), ignore_errors=True) - if os.path.isfile(self.get_agent_pkg_path()): - os.remove(self.get_agent_pkg_path()) except Exception as err: logger.warn("Unable to delete Agent files: {0}".format(err)) - msg = u"Agent {0} install failed with exception:".format( self.name) detailed_msg = '{0} {1}'.format(msg, textutil.format_exception(e)) @@ -1559,6 +1541,20 @@ def __init__(self, path=None, pkg=None, is_fast_track_goal_state=False, host=Non is_success=False, message=detailed_msg) + @staticmethod + def from_installed_agent(path): + """ + Creates an instance of GuestAgent using the agent installed in the given 'path'. + """ + return GuestAgent(path, None, None, False) + + @staticmethod + def from_agent_package(package, protocol, is_fast_track_goal_state): + """ + Creates an instance of GuestAgent using the information provided in the 'package'; if that version of the agent is not installed it, it installs it. + """ + return GuestAgent(None, package, protocol, is_fast_track_goal_state) + @property def name(self): return "{0}-{1}".format(AGENT_NAME, self.version) @@ -1621,7 +1617,6 @@ def _ensure_downloaded(self): self.name)) self._download() - self._unpack() msg = u"Agent {0} downloaded successfully".format(self.name) logger.verbose(msg) @@ -1637,39 +1632,10 @@ def _ensure_loaded(self): self._load_error() def _download(self): - uris_shuffled = self.pkg.uris - random.shuffle(uris_shuffled) - for uri in uris_shuffled: - if not HostPluginProtocol.is_default_channel and self._fetch(uri): - break - - elif self.host is not None and self.host.ensure_initialized(): - if not HostPluginProtocol.is_default_channel: - logger.warn("Download failed, switching to host plugin") - else: - logger.verbose("Using host plugin as default channel") - - uri, headers = self.host.get_artifact_request(uri, use_verify_header=self._is_fast_track_goal_state, artifact_manifest_url=self.host.manifest_uri) - try: - if self._fetch(uri, headers=headers, use_proxy=False, retry_codes=restutil.HGAP_GET_EXTENSION_ARTIFACT_RETRY_CODES): - if not HostPluginProtocol.is_default_channel: - logger.verbose("Setting host plugin as default channel") - HostPluginProtocol.is_default_channel = True - break - else: - logger.warn("Host plugin download failed") - - # If the HostPlugin rejects the request, - # let the error continue, but set to use the HostPlugin - except ResourceGoneError: - HostPluginProtocol.is_default_channel = True - raise - - else: - logger.error("No download channels available") - - if not os.path.isfile(self.get_agent_pkg_path()): - msg = u"Unable to download Agent {0} from any URI".format(self.name) + try: + self._protocol.client.download_zip_package("agent package", self.pkg.uris, self.get_agent_pkg_path(), self.get_agent_dir(), use_verify_header=self._is_fast_track_goal_state) + except Exception as exception: + msg = "Unable to download Agent {0}: {1}".format(self.name, ustr(exception)) add_event( AGENT_NAME, op=WALAEventOperation.Download, @@ -1678,37 +1644,6 @@ def _download(self): message=msg) raise UpdateError(msg) - def _fetch(self, uri, headers=None, use_proxy=True, retry_codes=None): - package = None - try: - is_healthy = True - error_response = '' - resp = restutil.http_get(uri, use_proxy=use_proxy, headers=headers, max_retry=3, retry_codes=retry_codes) # Use only 3 retries, since there are usually 5 or 6 URIs and we try all of them - if restutil.request_succeeded(resp): - package = resp.read() - fileutil.write_file(self.get_agent_pkg_path(), - bytearray(package), - asbin=True) - logger.verbose(u"Agent {0} downloaded from {1}", self.name, uri) - else: - error_response = restutil.read_response_error(resp) - logger.verbose("Fetch was unsuccessful [{0}]", error_response) - is_healthy = not restutil.request_failed_at_hostplugin(resp) - - if self.host is not None: - self.host.report_fetch_health(uri, is_healthy, source='GuestAgent', response=error_response) - - except restutil.HttpError as http_error: - if isinstance(http_error, ResourceGoneError): - raise - - logger.verbose(u"Agent {0} download from {1} failed [{2}]", - self.name, - uri, - http_error) - - return package is not None - def _load_error(self): try: self.error = GuestAgentError(self.get_agent_error_file()) @@ -1758,35 +1693,6 @@ def _load_manifest(self): ustr(self.manifest.data)) return - def _unpack(self): - try: - if os.path.isdir(self.get_agent_dir()): - shutil.rmtree(self.get_agent_dir()) - - zipfile.ZipFile(self.get_agent_pkg_path()).extractall(self.get_agent_dir()) - - except Exception as e: - fileutil.clean_ioerror(e, - paths=[self.get_agent_dir(), self.get_agent_pkg_path()]) - - msg = u"Exception unpacking Agent {0} from {1}: {2}".format( - self.name, - self.get_agent_pkg_path(), - ustr(e)) - raise UpdateError(msg) - - if not os.path.isdir(self.get_agent_dir()): - msg = u"Unpacking Agent {0} failed to create directory {1}".format( - self.name, - self.get_agent_dir()) - raise UpdateError(msg) - - logger.verbose( - u"Agent {0} unpacked successfully to {1}", - self.name, - self.get_agent_dir()) - return - class GuestAgentError(object): def __init__(self, path): diff --git a/tests/data/ga/WALinuxAgent-9.9.9.9-no_manifest.zip b/tests/data/ga/WALinuxAgent-9.9.9.9-no_manifest.zip new file mode 100644 index 0000000000000000000000000000000000000000..8d84af378d620e2e31ccef3d53efbe9c1c539f28 GIT binary patch literal 637559 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

k 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

D^->B>We(=ec)0oj4_{7U0q=CyJ^aObf;%!pw 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�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 literal 0 HcmV?d00001 diff --git a/tests/ga/test_extension.py b/tests/ga/test_extension.py index 1f261ee26f..5947e9e669 100644 --- a/tests/ga/test_extension.py +++ b/tests/ga/test_extension.py @@ -79,6 +79,7 @@ def raise_ioerror(*args): # pylint: disable=unused-argument e.errno = EIO raise e + class TestExtensionCleanup(AgentTestCase): def setUp(self): @@ -140,8 +141,6 @@ def test_cleanup_leaves_installed_extensions(self): exthandlers_handler.run() exthandlers_handler.report_ext_handlers_status() - self.assertEqual(no_of_exts, TestExtensionCleanup._count_packages(), - "No of extensions in config doesn't match the packages") self.assertEqual(no_of_exts, TestExtensionCleanup._count_extension_directories(), "No of extension directories doesnt match the no of extensions in GS") self._assert_ext_handler_status(protocol.aggregate_status, "Ready", expected_ext_handler_count=no_of_exts, @@ -151,8 +150,6 @@ def test_cleanup_removes_uninstalled_extensions(self): with self._setup_test_env(mockwiredata.DATA_FILE_MULTIPLE_EXT) as (exthandlers_handler, protocol, no_of_exts): exthandlers_handler.run() exthandlers_handler.report_ext_handlers_status() - self.assertEqual(no_of_exts, TestExtensionCleanup._count_packages(), - "No of extensions in config doesn't match the packages") self._assert_ext_handler_status(protocol.aggregate_status, "Ready", expected_ext_handler_count=no_of_exts, version="1.0.0") @@ -242,8 +239,6 @@ def assert_extension_seq_no(expected_seq_no): # Run 1 - GS has no required features and contains 5 extensions exthandlers_handler.run() exthandlers_handler.report_ext_handlers_status() - self.assertEqual(orig_no_of_exts, TestExtensionCleanup._count_packages(), - "No of extensions in config doesn't match the packages") self.assertEqual(orig_no_of_exts, TestExtensionCleanup._count_extension_directories(), "No of extension directories doesnt match the no of extensions in GS") self._assert_ext_handler_status(protocol.aggregate_status, "Ready", expected_ext_handler_count=orig_no_of_exts, @@ -261,8 +256,6 @@ def assert_extension_seq_no(expected_seq_no): exthandlers_handler.run() exthandlers_handler.report_ext_handlers_status() self.assertGreater(orig_no_of_exts, 1, "No of extensions to check should be > 1") - self.assertEqual(orig_no_of_exts, TestExtensionCleanup._count_packages(), - "No of extensions should not be changed") self.assertEqual(orig_no_of_exts, TestExtensionCleanup._count_extension_directories(), "No of extension directories should not be changed") self._assert_ext_handler_status(protocol.aggregate_status, "Ready", expected_ext_handler_count=orig_no_of_exts, @@ -286,8 +279,6 @@ def assert_extension_seq_no(expected_seq_no): protocol.client.update_goal_state() exthandlers_handler.run() exthandlers_handler.report_ext_handlers_status() - self.assertEqual(1, TestExtensionCleanup._count_packages(), - "No of extensions should not be changed") self.assertEqual(1, TestExtensionCleanup._count_extension_directories(), "No of extension directories should not be changed") self._assert_ext_handler_status(protocol.aggregate_status, "Ready", expected_ext_handler_count=1, @@ -682,120 +673,6 @@ def test_it_should_process_valid_extensions_if_present(self, mock_get, mock_cryp expected_handlers.remove(handler.name) self.assertEqual(0, len(expected_handlers), "All handlers not reported status") - def test_ext_zip_file_packages_removed_in_update_case(self, *args): - # Test enable scenario. - test_data = mockwiredata.WireProtocolData(mockwiredata.DATA_FILE) - exthandlers_handler, protocol = self._create_mock(test_data, *args) # pylint: disable=no-value-for-parameter - - exthandlers_handler.run() - exthandlers_handler.report_ext_handlers_status() - - self._assert_handler_status(protocol.report_vm_status, "Ready", 1, "1.0.0") - self._assert_ext_status(protocol.report_vm_status, "success", 0) - self._assert_ext_pkg_file_status(expected_to_be_present=True, extension_version="1.0.0") - - # Update the package - test_data.set_incarnation(2) - test_data.set_extensions_config_sequence_number(1) - test_data.set_extensions_config_version("1.1.0") - protocol.update_goal_state() - - exthandlers_handler.run() - exthandlers_handler.report_ext_handlers_status() - - self._assert_handler_status(protocol.report_vm_status, "Ready", 1, "1.1.0") - self._assert_ext_status(protocol.report_vm_status, "success", 1) - self._assert_ext_pkg_file_status(expected_to_be_present=False, extension_version="1.0.0") - self._assert_ext_pkg_file_status(expected_to_be_present=True, extension_version="1.1.0") - - # Update the package second time - test_data.set_incarnation(3) - test_data.set_extensions_config_sequence_number(2) - test_data.set_extensions_config_version("1.2.0") - protocol.update_goal_state() - - exthandlers_handler.run() - exthandlers_handler.report_ext_handlers_status() - - self._assert_handler_status(protocol.report_vm_status, "Ready", 1, "1.2.0") - self._assert_ext_status(protocol.report_vm_status, "success", 2) - self._assert_ext_pkg_file_status(expected_to_be_present=False, extension_version="1.1.0") - self._assert_ext_pkg_file_status(expected_to_be_present=True, extension_version="1.2.0") - - def test_ext_zip_file_packages_removed_in_uninstall_case(self, *args): - # Test enable scenario. - test_data = mockwiredata.WireProtocolData(mockwiredata.DATA_FILE) - exthandlers_handler, protocol = self._create_mock(test_data, *args) # pylint: disable=no-value-for-parameter - extension_version = "1.0.0" - - exthandlers_handler.run() - exthandlers_handler.report_ext_handlers_status() - - self._assert_handler_status(protocol.report_vm_status, "Ready", 1, extension_version) - self._assert_ext_status(protocol.report_vm_status, "success", 0) - self._assert_ext_pkg_file_status(expected_to_be_present=True, extension_version=extension_version) - - # Test uninstall - test_data.set_incarnation(2) - test_data.set_extensions_config_state(ExtensionRequestedState.Uninstall) - protocol.update_goal_state() - - exthandlers_handler.run() - exthandlers_handler.report_ext_handlers_status() - - self._assert_no_handler_status(protocol.report_vm_status) - self._assert_ext_pkg_file_status(expected_to_be_present=False, extension_version=extension_version) - - def test_ext_zip_file_packages_removed_in_update_and_uninstall_case(self, *args): - # Test enable scenario. - test_data = mockwiredata.WireProtocolData(mockwiredata.DATA_FILE) - exthandlers_handler, protocol = self._create_mock(test_data, *args) # pylint: disable=no-value-for-parameter - - exthandlers_handler.run() - exthandlers_handler.report_ext_handlers_status() - - self._assert_handler_status(protocol.report_vm_status, "Ready", 1, "1.0.0") - self._assert_ext_status(protocol.report_vm_status, "success", 0) - self._assert_ext_pkg_file_status(expected_to_be_present=True, extension_version="1.0.0") - - # Update the package - test_data.set_incarnation(2) - test_data.set_extensions_config_sequence_number(1) - test_data.set_extensions_config_version("1.1.0") - protocol.update_goal_state() - - exthandlers_handler.run() - exthandlers_handler.report_ext_handlers_status() - - self._assert_handler_status(protocol.report_vm_status, "Ready", 1, "1.1.0") - self._assert_ext_status(protocol.report_vm_status, "success", 1) - self._assert_ext_pkg_file_status(expected_to_be_present=False, extension_version="1.0.0") - self._assert_ext_pkg_file_status(expected_to_be_present=True, extension_version="1.1.0") - - # Update the package second time - test_data.set_incarnation(3) - test_data.set_extensions_config_sequence_number(2) - test_data.set_extensions_config_version("1.2.0") - protocol.update_goal_state() - - exthandlers_handler.run() - exthandlers_handler.report_ext_handlers_status() - - self._assert_handler_status(protocol.report_vm_status, "Ready", 1, "1.2.0") - self._assert_ext_status(protocol.report_vm_status, "success", 2) - self._assert_ext_pkg_file_status(expected_to_be_present=False, extension_version="1.1.0") - self._assert_ext_pkg_file_status(expected_to_be_present=True, extension_version="1.2.0") - - # Test uninstall - test_data.set_incarnation(4) - test_data.set_extensions_config_state(ExtensionRequestedState.Uninstall) - protocol.update_goal_state() - - exthandlers_handler.run() - exthandlers_handler.report_ext_handlers_status() - - self._assert_no_handler_status(protocol.report_vm_status) - self._assert_ext_pkg_file_status(expected_to_be_present=False, extension_version="1.2.0") def test_it_should_ignore_case_when_parsing_plugin_settings(self, mock_get, mock_crypt_util, *args): test_data = mockwiredata.WireProtocolData(mockwiredata.DATA_FILE_CASE_MISMATCH_EXT) diff --git a/tests/ga/test_exthandlers_download_extension.py b/tests/ga/test_exthandlers_download_extension.py index 3a9683889f..556254fa3b 100644 --- a/tests/ga/test_exthandlers_download_extension.py +++ b/tests/ga/test_exthandlers_download_extension.py @@ -96,6 +96,9 @@ def _create_invalid_zip_file(filename): with open(filename, "w") as file: # pylint: disable=redefined-builtin file.write("An invalid ZIP file\n") + def _get_extension_base_dir(self): + return self.extension_dir + def _get_extension_package_file(self): return os.path.join(self.agent_dir, self.ext_handler_instance.get_extension_package_zipfile_name()) @@ -103,7 +106,7 @@ def _get_extension_command_file(self): return os.path.join(self.extension_dir, DownloadExtensionTestCase._extension_command) def _assert_download_and_expand_succeeded(self): - self.assertTrue(os.path.exists(self._get_extension_package_file()), "The extension package was not downloaded to the expected location") + self.assertTrue(os.path.exists(self._get_extension_base_dir()), "The extension package was not downloaded to the expected location") self.assertTrue(os.path.exists(self._get_extension_command_file()), "The extension package was not expanded to the expected location") @staticmethod @@ -246,9 +249,11 @@ def stream(_, destination, **__): self._assert_download_and_expand_succeeded() def test_it_should_raise_an_exception_when_all_downloads_fail(self): - def stream(_, __, **___): - DownloadExtensionTestCase._create_invalid_zip_file(self._get_extension_package_file()) + def stream(_, target_file, **___): + stream.target_file = target_file + DownloadExtensionTestCase._create_invalid_zip_file(target_file) return True + stream.target_file = None with DownloadExtensionTestCase.create_mock_stream(stream) as mock_stream: with self.assertRaises(ExtensionDownloadError) as context_manager: @@ -260,5 +265,5 @@ def stream(_, __, **___): self.assertEqual(context_manager.exception.code, ExtensionErrorCodes.PluginManifestDownloadError) self.assertFalse(os.path.exists(self.extension_dir), "The extension directory was not removed") - self.assertFalse(os.path.exists(self._get_extension_package_file()), "The extension package was not removed") + self.assertFalse(os.path.exists(stream.target_file), "The extension package was not removed") diff --git a/tests/ga/test_update.py b/tests/ga/test_update.py index 0bbd22791d..31fd4a425a 100644 --- a/tests/ga/test_update.py +++ b/tests/ga/test_update.py @@ -19,7 +19,7 @@ import zipfile from datetime import datetime, timedelta -from threading import currentThread +from threading import current_thread from azurelinuxagent.common.protocol.imds import ComputeInfo from tests.common.osutil.test_default import TestOSUtil import azurelinuxagent.common.osutil.default as osutil @@ -28,18 +28,16 @@ from azurelinuxagent.common import conf from azurelinuxagent.common.event import EVENTS_DIRECTORY, WALAEventOperation -from azurelinuxagent.common.exception import ProtocolError, UpdateError, ResourceGoneError, HttpError, \ +from azurelinuxagent.common.exception import ProtocolError, UpdateError, HttpError, \ ExitException, AgentMemoryExceededException from azurelinuxagent.common.future import ustr, httpclient from azurelinuxagent.common.persist_firewall_rules import PersistFirewallRulesHandler -from azurelinuxagent.common.protocol.hostplugin import URI_FORMAT_GET_API_VERSIONS, HOST_PLUGIN_PORT, \ - URI_FORMAT_GET_EXTENSION_ARTIFACT, HostPluginProtocol +from azurelinuxagent.common.protocol.hostplugin import HostPluginProtocol from azurelinuxagent.common.protocol.restapi import VMAgentFamily, \ ExtHandlerPackage, ExtHandlerPackageList, Extension, VMStatus, ExtHandlerStatus, ExtensionStatus, \ VMAgentUpdateStatuses from azurelinuxagent.common.protocol.util import ProtocolUtil -from azurelinuxagent.common.protocol.wire import WireProtocol -from azurelinuxagent.common.utils import fileutil, restutil, textutil, timeutil +from azurelinuxagent.common.utils import fileutil, textutil, timeutil from azurelinuxagent.common.utils.archive import ARCHIVE_DIRECTORY_NAME, AGENT_STATUS_FILE from azurelinuxagent.common.utils.flexible_version import FlexibleVersion from azurelinuxagent.common.utils.networkutil import FirewallCmdDirectCommands, AddFirewallRules @@ -195,7 +193,7 @@ def rename_agent_bin(self, path, dst_v): shutil.move(src_bin, dst_bin) def agents(self): - return [GuestAgent(is_fast_track_goal_state=False, path=path) for path in self.agent_dirs()] + return [GuestAgent.from_installed_agent(path) for path in self.agent_dirs()] def agent_count(self): return len(self.agent_dirs()) @@ -314,7 +312,7 @@ def replicate_agents(self, shutil.copytree(from_path, to_path) self.rename_agent_bin(to_path, dst_v) if not is_available: - GuestAgent(is_fast_track_goal_state=False, path=to_path).mark_failure(is_fatal=True) + GuestAgent.from_installed_agent(to_path).mark_failure(is_fatal=True) return dst_v @@ -406,13 +404,15 @@ def setUp(self): self.agent_path = os.path.join(self.tmp_dir, self._get_agent_name()) def test_creation(self): - self.assertRaises(UpdateError, GuestAgent, "A very bad file name") - n = "{0}-a.bad.version".format(AGENT_NAME) - self.assertRaises(UpdateError, GuestAgent, n) + with self.assertRaises(UpdateError): + GuestAgent.from_installed_agent("A very bad file name") + + with self.assertRaises(UpdateError): + GuestAgent.from_installed_agent("{0}-a.bad.version".format(AGENT_NAME)) self.expand_agents() - agent = GuestAgent(is_fast_track_goal_state=False, path=self.agent_path) + agent = GuestAgent.from_installed_agent(self.agent_path) self.assertNotEqual(None, agent) self.assertEqual(self._get_agent_name(), agent.name) self.assertEqual(self._get_agent_version(), agent.version) @@ -433,11 +433,10 @@ def test_creation(self): self.assertFalse(agent.is_blacklisted) self.assertTrue(agent.is_available) - @patch("azurelinuxagent.ga.update.GuestAgent._ensure_downloaded") - def test_clear_error(self, mock_downloaded): # pylint: disable=unused-argument + def test_clear_error(self): self.expand_agents() - agent = GuestAgent(is_fast_track_goal_state=False, path=self.agent_path) + agent = GuestAgent.from_installed_agent(self.agent_path) agent.mark_failure(is_fatal=True) self.assertTrue(agent.error.last_failure > 0.0) @@ -451,25 +450,19 @@ def test_clear_error(self, mock_downloaded): # pylint: disable=unused-argument self.assertFalse(agent.is_blacklisted) self.assertEqual(agent.is_blacklisted, agent.error.is_blacklisted) - @patch("azurelinuxagent.ga.update.GuestAgent._ensure_downloaded") - @patch("azurelinuxagent.ga.update.GuestAgent._ensure_loaded") - def test_is_available(self, mock_loaded, mock_downloaded): # pylint: disable=unused-argument - agent = GuestAgent(is_fast_track_goal_state=False, path=self.agent_path) + def test_is_available(self): + self.expand_agents() - self.assertFalse(agent.is_available) - agent._unpack() - self.assertTrue(agent.is_available) + agent = GuestAgent.from_installed_agent(self.agent_path) + self.assertTrue(agent.is_available) agent.mark_failure(is_fatal=True) self.assertFalse(agent.is_available) - @patch("azurelinuxagent.ga.update.GuestAgent._ensure_downloaded") - @patch("azurelinuxagent.ga.update.GuestAgent._ensure_loaded") - def test_is_blacklisted(self, mock_loaded, mock_downloaded): # pylint: disable=unused-argument - agent = GuestAgent(is_fast_track_goal_state=False, path=self.agent_path) - self.assertFalse(agent.is_blacklisted) + def test_is_blacklisted(self): + self.expand_agents() - agent._unpack() + agent = GuestAgent.from_installed_agent(self.agent_path) self.assertFalse(agent.is_blacklisted) self.assertEqual(agent.is_blacklisted, agent.error.is_blacklisted) @@ -477,42 +470,13 @@ def test_is_blacklisted(self, mock_loaded, mock_downloaded): # pylint: disable= self.assertTrue(agent.is_blacklisted) self.assertEqual(agent.is_blacklisted, agent.error.is_blacklisted) - @patch("azurelinuxagent.ga.update.GuestAgent._ensure_downloaded") - @patch("azurelinuxagent.ga.update.GuestAgent._ensure_loaded") - def test_resource_gone_error_not_blacklisted(self, mock_loaded, mock_downloaded): # pylint: disable=unused-argument - try: - mock_downloaded.side_effect = ResourceGoneError() - agent = GuestAgent(is_fast_track_goal_state=False, path=self.agent_path) - self.assertFalse(agent.is_blacklisted) - except ResourceGoneError: - pass - except: # pylint: disable=bare-except - self.fail("Exception was not expected!") - - @patch("azurelinuxagent.ga.update.GuestAgent._ensure_downloaded") - @patch("azurelinuxagent.ga.update.GuestAgent._ensure_loaded") - def test_ioerror_not_blacklisted(self, mock_loaded, mock_downloaded): # pylint: disable=unused-argument - try: - mock_downloaded.side_effect = IOError() - agent = GuestAgent(is_fast_track_goal_state=False, path=self.agent_path) - self.assertFalse(agent.is_blacklisted) - except IOError: - pass - except: # pylint: disable=bare-except - self.fail("Exception was not expected!") - - @patch("azurelinuxagent.ga.update.GuestAgent._ensure_downloaded") - @patch("azurelinuxagent.ga.update.GuestAgent._ensure_loaded") - def test_is_downloaded(self, mock_loaded, mock_downloaded): # pylint: disable=unused-argument - agent = GuestAgent(is_fast_track_goal_state=False, path=self.agent_path) - self.assertFalse(agent.is_downloaded) - agent._unpack() + def test_is_downloaded(self): + self.expand_agents() + agent = GuestAgent.from_installed_agent(self.agent_path) self.assertTrue(agent.is_downloaded) - @patch("azurelinuxagent.ga.update.GuestAgent._ensure_downloaded") - @patch("azurelinuxagent.ga.update.GuestAgent._ensure_loaded") - def test_mark_failure(self, mock_loaded, mock_downloaded): # pylint: disable=unused-argument - agent = GuestAgent(is_fast_track_goal_state=False, path=self.agent_path) + def test_mark_failure(self): + agent = GuestAgent.from_installed_agent(self.agent_path) agent.mark_failure() self.assertEqual(1, agent.error.failure_count) @@ -521,59 +485,31 @@ def test_mark_failure(self, mock_loaded, mock_downloaded): # pylint: disable=un self.assertEqual(2, agent.error.failure_count) self.assertTrue(agent.is_blacklisted) - @patch("azurelinuxagent.ga.update.GuestAgent._ensure_downloaded") - @patch("azurelinuxagent.ga.update.GuestAgent._ensure_loaded") - def test_unpack(self, mock_loaded, mock_downloaded): # pylint: disable=unused-argument - agent = GuestAgent(is_fast_track_goal_state=False, path=self.agent_path) - self.assertFalse(os.path.isdir(agent.get_agent_dir())) - agent._unpack() - self.assertTrue(os.path.isdir(agent.get_agent_dir())) - self.assertTrue(os.path.isfile(agent.get_agent_manifest_path())) - - @patch("azurelinuxagent.ga.update.GuestAgent._ensure_downloaded") - @patch("azurelinuxagent.ga.update.GuestAgent._ensure_loaded") - def test_unpack_fail(self, mock_loaded, mock_downloaded): # pylint: disable=unused-argument - agent = GuestAgent(is_fast_track_goal_state=False, path=self.agent_path) - self.assertFalse(os.path.isdir(agent.get_agent_dir())) - os.remove(agent.get_agent_pkg_path()) - self.assertRaises(UpdateError, agent._unpack) - - @patch("azurelinuxagent.ga.update.GuestAgent._ensure_downloaded") - @patch("azurelinuxagent.ga.update.GuestAgent._ensure_loaded") - def test_load_manifest(self, mock_loaded, mock_downloaded): # pylint: disable=unused-argument - agent = GuestAgent(is_fast_track_goal_state=False, path=self.agent_path) - agent._unpack() + def test_load_manifest(self): + self.expand_agents() + agent = GuestAgent.from_installed_agent(self.agent_path) agent._load_manifest() self.assertEqual(agent.manifest.get_enable_command(), agent.get_agent_cmd()) - @patch("azurelinuxagent.ga.update.GuestAgent._ensure_downloaded") - @patch("azurelinuxagent.ga.update.GuestAgent._ensure_loaded") - def test_load_manifest_missing(self, mock_loaded, mock_downloaded): # pylint: disable=unused-argument - agent = GuestAgent(is_fast_track_goal_state=False, path=self.agent_path) - self.assertFalse(os.path.isdir(agent.get_agent_dir())) - agent._unpack() + def test_load_manifest_missing(self): + self.expand_agents() + agent = GuestAgent.from_installed_agent(self.agent_path) os.remove(agent.get_agent_manifest_path()) self.assertRaises(UpdateError, agent._load_manifest) - @patch("azurelinuxagent.ga.update.GuestAgent._ensure_downloaded") - @patch("azurelinuxagent.ga.update.GuestAgent._ensure_loaded") - def test_load_manifest_is_empty(self, mock_loaded, mock_downloaded): # pylint: disable=unused-argument - agent = GuestAgent(is_fast_track_goal_state=False, path=self.agent_path) - self.assertFalse(os.path.isdir(agent.get_agent_dir())) - agent._unpack() + def test_load_manifest_is_empty(self): + self.expand_agents() + agent = GuestAgent.from_installed_agent(self.agent_path) self.assertTrue(os.path.isfile(agent.get_agent_manifest_path())) with open(agent.get_agent_manifest_path(), "w") as file: # pylint: disable=redefined-builtin json.dump(EMPTY_MANIFEST, file) self.assertRaises(UpdateError, agent._load_manifest) - @patch("azurelinuxagent.ga.update.GuestAgent._ensure_downloaded") - @patch("azurelinuxagent.ga.update.GuestAgent._ensure_loaded") - def test_load_manifest_is_malformed(self, mock_loaded, mock_downloaded): # pylint: disable=unused-argument - agent = GuestAgent(is_fast_track_goal_state=False, path=self.agent_path) - self.assertFalse(os.path.isdir(agent.get_agent_dir())) - agent._unpack() + def test_load_manifest_is_malformed(self): + self.expand_agents() + agent = GuestAgent.from_installed_agent(self.agent_path) self.assertTrue(os.path.isfile(agent.get_agent_manifest_path())) with open(agent.get_agent_manifest_path(), "w") as file: # pylint: disable=redefined-builtin @@ -581,165 +517,83 @@ def test_load_manifest_is_malformed(self, mock_loaded, mock_downloaded): # pyli self.assertRaises(UpdateError, agent._load_manifest) def test_load_error(self): - agent = GuestAgent(is_fast_track_goal_state=False, path=self.agent_path) + agent = GuestAgent.from_installed_agent(self.agent_path) agent.error = None agent._load_error() self.assertTrue(agent.error is not None) - @patch("azurelinuxagent.ga.update.GuestAgent._ensure_downloaded") - @patch("azurelinuxagent.ga.update.GuestAgent._ensure_loaded") - @patch("azurelinuxagent.ga.update.restutil.http_get") - def test_download(self, mock_http_get, mock_loaded, mock_downloaded): # pylint: disable=unused-argument + def test_download(self): self.remove_agents() self.assertFalse(os.path.isdir(self.agent_path)) - agent_pkg = load_bin_data(self._get_agent_file_name(), self._agent_zip_dir) - mock_http_get.return_value = ResponseMock(response=agent_pkg) - - pkg = ExtHandlerPackage(version=str(self._get_agent_version())) - pkg.uris.append(None) - agent = GuestAgent(is_fast_track_goal_state=False, pkg=pkg) - agent._download() - - self.assertTrue(os.path.isfile(agent.get_agent_pkg_path())) - - @patch("azurelinuxagent.ga.update.GuestAgent._ensure_downloaded") - @patch("azurelinuxagent.ga.update.GuestAgent._ensure_loaded") - @patch("azurelinuxagent.ga.update.restutil.http_get") - def test_download_fail(self, mock_http_get, mock_loaded, mock_downloaded): # pylint: disable=unused-argument - self.remove_agents() - self.assertFalse(os.path.isdir(self.agent_path)) + agent_uri = 'https://foo.blob.core.windows.net/bar/OSTCExtensions.WALinuxAgent__1.0.0' - mock_http_get.return_value = ResponseMock(status=restutil.httpclient.SERVICE_UNAVAILABLE) + def http_get_handler(uri, *_, **__): + if uri == agent_uri: + response = load_bin_data(self._get_agent_file_name(), self._agent_zip_dir) + return MockHttpResponse(status=httpclient.OK, body=response) + return None pkg = ExtHandlerPackage(version=str(self._get_agent_version())) - pkg.uris.append(None) - agent = GuestAgent(is_fast_track_goal_state=False, pkg=pkg) + pkg.uris.append(agent_uri) - self.assertRaises(UpdateError, agent._download) - self.assertFalse(os.path.isfile(agent.get_agent_pkg_path())) - self.assertFalse(agent.is_downloaded) - - @patch("azurelinuxagent.ga.update.GuestAgent._ensure_downloaded") - @patch("azurelinuxagent.ga.update.GuestAgent._ensure_loaded") - @patch("azurelinuxagent.ga.update.restutil.http_get") - @patch("azurelinuxagent.ga.update.restutil.http_post") - def test_download_fallback(self, mock_http_post, mock_http_get, mock_loaded, mock_downloaded): # pylint: disable=unused-argument - self.remove_agents() - self.assertFalse(os.path.isdir(self.agent_path)) - - mock_http_get.return_value = ResponseMock( - status=restutil.httpclient.SERVICE_UNAVAILABLE, - response="") + with mock_wire_protocol(mockwiredata.DATA_FILE) as protocol: + protocol.set_http_handlers(http_get_handler=http_get_handler) + agent = GuestAgent.from_agent_package(pkg, protocol, False) - ext_uri = 'ext_uri' - host_uri = 'host_uri' - api_uri = URI_FORMAT_GET_API_VERSIONS.format(host_uri, HOST_PLUGIN_PORT) - art_uri = URI_FORMAT_GET_EXTENSION_ARTIFACT.format(host_uri, HOST_PLUGIN_PORT) - mock_host = HostPluginProtocol(host_uri) + self.assertTrue(os.path.isdir(agent.get_agent_dir())) + self.assertTrue(agent.is_downloaded) - pkg = ExtHandlerPackage(version=str(self._get_agent_version())) - pkg.uris.append(ext_uri) - agent = GuestAgent(is_fast_track_goal_state=False, pkg=pkg) - agent.host = mock_host - - # ensure fallback fails gracefully, no http - self.assertRaises(UpdateError, agent._download) - self.assertEqual(mock_http_get.call_count, 2) - self.assertEqual(mock_http_get.call_args_list[0][0][0], ext_uri) - self.assertEqual(mock_http_get.call_args_list[1][0][0], api_uri) - - # ensure fallback fails gracefully, artifact api failure - with patch.object(HostPluginProtocol, - "ensure_initialized", - return_value=True): - self.assertRaises(UpdateError, agent._download) - self.assertEqual(mock_http_get.call_count, 4) - - self.assertEqual(mock_http_get.call_args_list[2][0][0], ext_uri) - - self.assertEqual(mock_http_get.call_args_list[3][0][0], art_uri) - a, k = mock_http_get.call_args_list[3] # pylint: disable=unused-variable - self.assertEqual(False, k['use_proxy']) - - # ensure fallback works as expected - with patch.object(HostPluginProtocol, - "get_artifact_request", - return_value=[art_uri, {}]): - self.assertRaises(UpdateError, agent._download) - self.assertEqual(mock_http_get.call_count, 6) - - a, k = mock_http_get.call_args_list[3] - self.assertEqual(False, k['use_proxy']) - - self.assertEqual(mock_http_get.call_args_list[4][0][0], ext_uri) - a, k = mock_http_get.call_args_list[4] - - self.assertEqual(mock_http_get.call_args_list[5][0][0], art_uri) - a, k = mock_http_get.call_args_list[5] - self.assertEqual(False, k['use_proxy']) - - @patch("azurelinuxagent.ga.update.restutil.http_get") - def test_ensure_downloaded(self, mock_http_get): + def test_download_fail(self): self.remove_agents() self.assertFalse(os.path.isdir(self.agent_path)) - agent_pkg = load_bin_data(self._get_agent_file_name(), self._agent_zip_dir) - mock_http_get.return_value = ResponseMock(response=agent_pkg) + agent_uri = 'https://foo.blob.core.windows.net/bar/OSTCExtensions.WALinuxAgent__1.0.0' + + def http_get_handler(uri, *_, **__): + if uri in (agent_uri, 'http://168.63.129.16:32526/extensionArtifact'): + return MockHttpResponse(status=httpclient.SERVICE_UNAVAILABLE) + return None pkg = ExtHandlerPackage(version=str(self._get_agent_version())) - pkg.uris.append(None) - agent = GuestAgent(is_fast_track_goal_state=False, pkg=pkg) + pkg.uris.append(agent_uri) - self.assertTrue(os.path.isfile(agent.get_agent_manifest_path())) - self.assertTrue(agent.is_downloaded) + with mock_wire_protocol(mockwiredata.DATA_FILE) as protocol: + protocol.set_http_handlers(http_get_handler=http_get_handler) + with patch("azurelinuxagent.ga.update.add_event") as add_event: + agent = GuestAgent.from_agent_package(pkg, protocol, False) - @patch("azurelinuxagent.ga.update.GuestAgent._download", side_effect=UpdateError) - def test_ensure_failure_in_download_cleans_up_filesystem(self, _): - self.remove_agents() - self.assertFalse(os.path.isdir(self.agent_path)) + self.assertFalse(os.path.isfile(self.agent_path)) - pkg = ExtHandlerPackage(version=str(self._get_agent_version())) - pkg.uris.append(None) - agent = GuestAgent(is_fast_track_goal_state=False, pkg=pkg) + messages = [kwargs['message'] for _, kwargs in add_event.call_args_list if kwargs['op'] == 'Install' and kwargs['is_success'] == False] + self.assertEqual(1, len(messages), "Expected exactly 1 install error/ Got: {0}".format(add_event.call_args_list)) + self.assertIn('[UpdateError] Unable to download Agent WALinuxAgent-9.9.9.9', messages[0], "The install error does not include the expected message") - self.assertFalse(agent.is_blacklisted, "The agent should not be blacklisted if unable to unpack/download") - self.assertFalse(os.path.exists(agent.get_agent_dir()), "Agent directory should be cleaned up") - self.assertFalse(os.path.exists(agent.get_agent_pkg_path()), "Agent package should be cleaned up") + self.assertFalse(agent.is_blacklisted, "Download failures should not blacklist the Agent") - @patch("azurelinuxagent.ga.update.GuestAgent._download") - @patch("azurelinuxagent.ga.update.GuestAgent._unpack", side_effect=UpdateError) - def test_ensure_downloaded_unpack_failure_cleans_file_system(self, *_): - self.assertFalse(os.path.isdir(self.agent_path)) + def test_invalid_agent_package_does_not_blacklist_the_agent(self): + agent_uri = 'https://foo.blob.core.windows.net/bar/OSTCExtensions.WALinuxAgent__9.9.9.9' - pkg = ExtHandlerPackage(version=str(self._get_agent_version())) - pkg.uris.append(None) - agent = GuestAgent(is_fast_track_goal_state=False, pkg=pkg) - - self.assertFalse(agent.is_blacklisted, "The agent should not be blacklisted if unable to unpack/download") - self.assertFalse(os.path.exists(agent.get_agent_dir()), "Agent directory should be cleaned up") - self.assertFalse(os.path.exists(agent.get_agent_pkg_path()), "Agent package should be cleaned up") + def http_get_handler(uri, *_, **__): + if uri in (agent_uri, 'http://168.63.129.16:32526/extensionArtifact'): + response = load_bin_data("ga/WALinuxAgent-9.9.9.9-no_manifest.zip") + return MockHttpResponse(status=httpclient.OK, body=response) + return None - @patch("azurelinuxagent.ga.update.GuestAgent._download") - @patch("azurelinuxagent.ga.update.GuestAgent._unpack") - @patch("azurelinuxagent.ga.update.GuestAgent._load_manifest", side_effect=UpdateError) - def test_ensure_downloaded_load_manifest_cleans_up_agent_directories(self, *_): - self.assertFalse(os.path.isdir(self.agent_path)) + pkg = ExtHandlerPackage(version="9.9.9.9") + pkg.uris.append(agent_uri) - pkg = ExtHandlerPackage(version=str(self._get_agent_version())) - pkg.uris.append(None) - agent = GuestAgent(is_fast_track_goal_state=False, pkg=pkg) + with mock_wire_protocol(mockwiredata.DATA_FILE) as protocol: + protocol.set_http_handlers(http_get_handler=http_get_handler) + agent = GuestAgent.from_agent_package(pkg, protocol, False) self.assertFalse(agent.is_blacklisted, "The agent should not be blacklisted if unable to unpack/download") self.assertFalse(os.path.exists(agent.get_agent_dir()), "Agent directory should be cleaned up") - self.assertFalse(os.path.exists(agent.get_agent_pkg_path()), "Agent package should be cleaned up") @patch("azurelinuxagent.ga.update.GuestAgent._download") - @patch("azurelinuxagent.ga.update.GuestAgent._unpack") - @patch("azurelinuxagent.ga.update.GuestAgent._load_manifest") - def test_ensure_download_skips_blacklisted(self, mock_manifest, mock_unpack, mock_download): # pylint: disable=unused-argument - agent = GuestAgent(is_fast_track_goal_state=False, path=self.agent_path) + def test_ensure_download_skips_blacklisted(self, mock_download): + agent = GuestAgent.from_installed_agent(self.agent_path) self.assertEqual(0, mock_download.call_count) agent.clear_error() @@ -748,13 +602,13 @@ def test_ensure_download_skips_blacklisted(self, mock_manifest, mock_unpack, moc pkg = ExtHandlerPackage(version=str(self._get_agent_version())) pkg.uris.append(None) - agent = GuestAgent(is_fast_track_goal_state=False, pkg=pkg) + # _download is mocked so there will be no http request; passing a None protocol + agent = GuestAgent.from_agent_package(pkg, None, False) self.assertEqual(1, agent.error.failure_count) self.assertTrue(agent.error.was_fatal) self.assertTrue(agent.is_blacklisted) self.assertEqual(0, mock_download.call_count) - self.assertEqual(0, mock_unpack.call_count) class TestUpdate(UpdateTestCase): @@ -955,7 +809,7 @@ def test_evaluate_agent_health_resets_with_new_agent(self): def test_filter_blacklisted_agents(self): self.prepare_agents() - self.update_handler._set_and_sort_agents([GuestAgent(is_fast_track_goal_state=False, path=path) for path in self.agent_dirs()]) + self.update_handler._set_and_sort_agents([GuestAgent.from_installed_agent(path) for path in self.agent_dirs()]) self.assertEqual(len(self.agent_dirs()), len(self.update_handler.agents)) kept_agents = self.update_handler.agents[::2] @@ -990,15 +844,6 @@ def test_find_agents_sorts(self): self.assertTrue(v > a.version) v = a.version - @patch('azurelinuxagent.common.protocol.wire.WireClient.get_host_plugin') - def test_get_host_plugin_returns_host_for_wireserver(self, mock_get_host): - protocol = WireProtocol('12.34.56.78') - mock_get_host.return_value = "faux host" - host = self.update_handler._get_host_plugin(protocol=protocol) - print("mock_get_host call cound={0}".format(mock_get_host.call_count)) - self.assertEqual(1, mock_get_host.call_count) - self.assertEqual("faux host", host) - def test_get_latest_agent(self): latest_version = self.prepare_agents() @@ -1026,7 +871,7 @@ def test_get_latest_agent_skips_unavailable(self): latest_version = self.prepare_agents(count=self.agent_count() + 1, is_available=False) latest_path = os.path.join(self.tmp_dir, "{0}-{1}".format(AGENT_NAME, latest_version)) - self.assertFalse(GuestAgent(is_fast_track_goal_state=False, path=latest_path).is_available) + self.assertFalse(GuestAgent.from_installed_agent(latest_path).is_available) latest_agent = self.update_handler.get_latest_agent_greater_than_daemon() self.assertTrue(latest_agent.version < latest_version) @@ -1301,14 +1146,14 @@ def test_get_latest_agent_should_return_latest_agent_even_on_bad_error_json(self def test_set_agents_sets_agents(self): self.prepare_agents() - self.update_handler._set_and_sort_agents([GuestAgent(is_fast_track_goal_state=False, path=path) for path in self.agent_dirs()]) + self.update_handler._set_and_sort_agents([GuestAgent.from_installed_agent(path) for path in self.agent_dirs()]) self.assertTrue(len(self.update_handler.agents) > 0) self.assertEqual(len(self.agent_dirs()), len(self.update_handler.agents)) def test_set_agents_sorts_agents(self): self.prepare_agents() - self.update_handler._set_and_sort_agents([GuestAgent(is_fast_track_goal_state=False, path=path) for path in self.agent_dirs()]) + self.update_handler._set_and_sort_agents([GuestAgent.from_installed_agent(path) for path in self.agent_dirs()]) v = FlexibleVersion("100000") for a in self.update_handler.agents: @@ -1985,7 +1830,7 @@ def get_handler(url, **kwargs): if HttpRequestPredicates.is_agent_package_request(url): agent_pkg = load_bin_data(self._get_agent_file_name(), self._agent_zip_dir) protocol.mock_wire_data.call_counts['agentArtifact'] += 1 - return ResponseMock(response=agent_pkg) + return MockHttpResponse(status=httpclient.OK, body=agent_pkg) return protocol.mock_wire_data.mock_http_get(url, **kwargs) def put_handler(url, *args, **_): @@ -2419,7 +2264,7 @@ class MonitorThreadTest(AgentTestCaseWithGetVmSizeMock): def setUp(self): super(MonitorThreadTest, self).setUp() self.event_patch = patch('azurelinuxagent.common.event.add_event') - currentThread().setName("ExtHandler") + current_thread().name = "ExtHandler" protocol = Mock() self.update_handler = get_update_handler() self.update_handler.protocol_util = Mock() @@ -2589,17 +2434,6 @@ def update_goal_state(self): self.call_counts["update_goal_state"] += 1 -class ResponseMock(Mock): - def __init__(self, status=restutil.httpclient.OK, response=None, reason=None): - Mock.__init__(self) - self.status = status - self.reason = reason - self.response = response - - def read(self): - return self.response - - class TimeMock(Mock): def __init__(self, time_increment=1): Mock.__init__(self) diff --git a/tests/protocol/test_hostplugin.py b/tests/protocol/test_hostplugin.py index 42d8579d52..c980433b5a 100644 --- a/tests/protocol/test_hostplugin.py +++ b/tests/protocol/test_hostplugin.py @@ -163,7 +163,7 @@ def create_mock_protocol(): yield protocol @patch("azurelinuxagent.common.protocol.healthservice.HealthService.report_host_plugin_versions") - @patch("azurelinuxagent.ga.update.restutil.http_get") + @patch("azurelinuxagent.common.protocol.hostplugin.restutil.http_get") @patch("azurelinuxagent.common.protocol.hostplugin.add_event") def assert_ensure_initialized(self, patch_event, patch_http_get, patch_report_health, response_body, diff --git a/tests/protocol/test_imds.py b/tests/protocol/test_imds.py index 167fe2bfb1..1f8e428c1f 100644 --- a/tests/protocol/test_imds.py +++ b/tests/protocol/test_imds.py @@ -20,18 +20,18 @@ import os import unittest -import azurelinuxagent.common.protocol.imds as imds +from azurelinuxagent.common.protocol import imds from azurelinuxagent.common.datacontract import set_properties from azurelinuxagent.common.exception import HttpError, ResourceGoneError from azurelinuxagent.common.future import ustr, httpclient from azurelinuxagent.common.utils import restutil -from tests.ga.test_update import ResponseMock +from tests.protocol.mocks import MockHttpResponse from tests.tools import AgentTestCase, data_dir, MagicMock, Mock, patch def get_mock_compute_response(): - return ResponseMock(response='''{ + return MockHttpResponse(status=httpclient.OK, body='''{ "location": "westcentralus", "name": "unit_test", "offer": "UnitOffer", @@ -52,7 +52,7 @@ def get_mock_compute_response(): class TestImds(AgentTestCase): - @patch("azurelinuxagent.ga.update.restutil.http_get") + @patch("azurelinuxagent.common.protocol.imds.restutil.http_get") def test_get(self, mock_http_get): mock_http_get.return_value = get_mock_compute_response() @@ -67,23 +67,23 @@ def test_get(self, mock_http_get): self.assertTrue('Metadata' in kw_args['headers']) self.assertEqual(True, kw_args['headers']['Metadata']) - @patch("azurelinuxagent.ga.update.restutil.http_get") + @patch("azurelinuxagent.common.protocol.imds.restutil.http_get") def test_get_bad_request(self, mock_http_get): - mock_http_get.return_value = ResponseMock(status=restutil.httpclient.BAD_REQUEST) + mock_http_get.return_value = MockHttpResponse(status=restutil.httpclient.BAD_REQUEST) test_subject = imds.ImdsClient(restutil.KNOWN_WIRESERVER_IP) self.assertRaises(HttpError, test_subject.get_compute) - @patch("azurelinuxagent.ga.update.restutil.http_get") + @patch("azurelinuxagent.common.protocol.imds.restutil.http_get") def test_get_internal_service_error(self, mock_http_get): - mock_http_get.return_value = ResponseMock(status=restutil.httpclient.INTERNAL_SERVER_ERROR) + mock_http_get.return_value = MockHttpResponse(status=restutil.httpclient.INTERNAL_SERVER_ERROR) test_subject = imds.ImdsClient(restutil.KNOWN_WIRESERVER_IP) self.assertRaises(HttpError, test_subject.get_compute) - @patch("azurelinuxagent.ga.update.restutil.http_get") + @patch("azurelinuxagent.common.protocol.imds.restutil.http_get") def test_get_empty_response(self, mock_http_get): - mock_http_get.return_value = ResponseMock(response=''.encode('utf-8')) + mock_http_get.return_value = MockHttpResponse(status=httpclient.OK, body=''.encode('utf-8')) test_subject = imds.ImdsClient(restutil.KNOWN_WIRESERVER_IP) self.assertRaises(ValueError, test_subject.get_compute) @@ -361,9 +361,9 @@ def _imds_response(f): def _assert_validation(self, http_status_code, http_response, expected_valid, expected_response): test_subject = imds.ImdsClient(restutil.KNOWN_WIRESERVER_IP) with patch("azurelinuxagent.common.utils.restutil.http_get") as mock_http_get: - mock_http_get.return_value = ResponseMock(status=http_status_code, + mock_http_get.return_value = MockHttpResponse(status=http_status_code, reason='reason', - response=http_response) + body=http_response) validate_response = test_subject.validate() self.assertEqual(1, mock_http_get.call_count) diff --git a/tests/protocol/test_wire.py b/tests/protocol/test_wire.py index b9fe23e413..7c121f4481 100644 --- a/tests/protocol/test_wire.py +++ b/tests/protocol/test_wire.py @@ -44,7 +44,7 @@ from tests.protocol.HttpRequestPredicates import HttpRequestPredicates from tests.protocol.mockwiredata import DATA_FILE_NO_EXT, DATA_FILE from tests.protocol.mockwiredata import WireProtocolData -from tests.tools import patch, AgentTestCase +from tests.tools import patch, AgentTestCase, load_bin_data data_with_bom = b'\xef\xbb\xbfhehe' testurl = 'http://foo' @@ -497,13 +497,30 @@ def test_get_ext_conf_with_extensions_should_retrieve_ext_handlers_and_vmagent_m self.assertFalse(extensions_goal_state.on_hold, "Extensions On Hold is expected to be False") - def test_download_ext_handler_pkg_should_not_invoke_host_channel_when_direct_channel_succeeds(self): + def test_download_zip_package_should_expand_and_delete_the_package(self): extension_url = 'https://fake_host/fake_extension.zip' target_file = os.path.join(self.tmp_dir, 'fake_extension.zip') + target_directory = os.path.join(self.tmp_dir, "fake_extension") + + def http_get_handler(url, *_, **__): + if url == extension_url or self.is_host_plugin_extension_artifact_request(url): + return MockHttpResponse(200, body=load_bin_data("ga/fake_extension.zip")) + return None + + with mock_wire_protocol(mockwiredata.DATA_FILE, http_get_handler=http_get_handler) as protocol: + protocol.client.download_zip_package("extension package", [extension_url], target_file, target_directory, use_verify_header=False) + + self.assertTrue(os.path.exists(target_directory), "The extension package was not downloaded") + self.assertFalse(os.path.exists(target_file), "The extension package was not deleted") + + def test_download_zip_package_should_not_invoke_host_channel_when_direct_channel_succeeds(self): + extension_url = 'https://fake_host/fake_extension.zip' + target_file = os.path.join(self.tmp_dir, 'fake_extension.zip') + target_directory = os.path.join(self.tmp_dir, "fake_extension") def http_get_handler(url, *_, **__): if url == extension_url: - return MockHttpResponse(200) + return MockHttpResponse(200, body=load_bin_data("ga/fake_extension.zip")) if self.is_host_plugin_extension_artifact_request(url): self.fail('The host channel should not have been used') return None @@ -511,40 +528,42 @@ def http_get_handler(url, *_, **__): with mock_wire_protocol(mockwiredata.DATA_FILE, http_get_handler=http_get_handler) as protocol: HostPluginProtocol.is_default_channel = False - protocol.client.download_extension([extension_url], target_file, use_verify_header=False) + protocol.client.download_zip_package("extension package", [extension_url], target_file, target_directory, use_verify_header=False) urls = protocol.get_tracked_urls() self.assertEqual(len(urls), 1, "Unexpected number of HTTP requests: [{0}]".format(urls)) self.assertEqual(urls[0], extension_url, "The extension should have been downloaded over the direct channel") - self.assertTrue(os.path.exists(target_file), "The extension package was not downloaded") + self.assertTrue(os.path.exists(target_directory), "The extension package was not downloaded") self.assertFalse(HostPluginProtocol.is_default_channel, "The host channel should not have been set as the default") - def test_download_ext_handler_pkg_should_use_host_channel_when_direct_channel_fails_and_set_host_as_default(self): + def test_download_zip_package_should_use_host_channel_when_direct_channel_fails_and_set_host_as_default(self): extension_url = 'https://fake_host/fake_extension.zip' target_file = os.path.join(self.tmp_dir, 'fake_extension.zip') + target_directory = os.path.join(self.tmp_dir, "fake_extension") def http_get_handler(url, *_, **kwargs): if url == extension_url: return HttpError("Exception to fake an error on the direct channel") if self.is_host_plugin_extension_request(url, kwargs, extension_url): - return MockHttpResponse(200) + return MockHttpResponse(200, body=load_bin_data("ga/fake_extension.zip")) return None with mock_wire_protocol(mockwiredata.DATA_FILE, http_get_handler=http_get_handler) as protocol: HostPluginProtocol.is_default_channel = False - protocol.client.download_extension([extension_url], target_file, use_verify_header=False) + protocol.client.download_zip_package("extension package", [extension_url], target_file, target_directory, use_verify_header=False) urls = protocol.get_tracked_urls() self.assertEqual(len(urls), 2, "Unexpected number of HTTP requests: [{0}]".format(urls)) self.assertEqual(urls[0], extension_url, "The first attempt should have been over the direct channel") self.assertTrue(self.is_host_plugin_extension_artifact_request(urls[1]), "The retry attempt should have been over the host channel") - self.assertTrue(os.path.exists(target_file), 'The extension package was not downloaded') + self.assertTrue(os.path.exists(target_directory), 'The extension package was not downloaded') self.assertTrue(HostPluginProtocol.is_default_channel, "The host channel should have been set as the default") - def test_download_ext_handler_pkg_should_retry_the_host_channel_after_refreshing_host_plugin(self): + def test_download_zip_package_should_retry_the_host_channel_after_refreshing_host_plugin(self): extension_url = 'https://fake_host/fake_extension.zip' target_file = os.path.join(self.tmp_dir, 'fake_extension.zip') + target_directory = os.path.join(self.tmp_dir, "fake_extension") def http_get_handler(url, *_, **kwargs): if url == extension_url: @@ -554,7 +573,7 @@ def http_get_handler(url, *_, **kwargs): if http_get_handler.goal_state_requests == 0: http_get_handler.goal_state_requests += 1 return ResourceGoneError("Exception to fake a stale goal") - return MockHttpResponse(200) + return MockHttpResponse(200, body=load_bin_data("ga/fake_extension.zip")) if self.is_goal_state_request(url): protocol.track_url(url) # track requests for the goal state return None @@ -569,7 +588,7 @@ def http_get_handler(url, *_, **kwargs): protocol.set_http_handlers(http_get_handler=http_get_handler) - protocol.client.download_extension([extension_url], target_file, use_verify_header=False) + protocol.client.download_zip_package("extension package", [extension_url], target_file, target_directory, use_verify_header=False) urls = protocol.get_tracked_urls() self.assertEqual(len(urls), 4, "Unexpected number of HTTP requests: [{0}]".format(urls)) @@ -577,14 +596,15 @@ def http_get_handler(url, *_, **kwargs): self.assertTrue(self.is_host_plugin_extension_artifact_request(urls[1]), "The second attempt should have been over the host channel") self.assertTrue(self.is_goal_state_request(urls[2]), "The host channel should have been refreshed the goal state") self.assertTrue(self.is_host_plugin_extension_artifact_request(urls[3]), "The third attempt should have been over the host channel") - self.assertTrue(os.path.exists(target_file), 'The extension package was not downloaded') + self.assertTrue(os.path.exists(target_directory), 'The extension package was not downloaded') self.assertTrue(HostPluginProtocol.is_default_channel, "The host channel should have been set as the default") finally: HostPluginProtocol.is_default_channel = False - def test_download_ext_handler_pkg_should_not_change_default_channel_when_all_channels_fail(self): + def test_download_zip_package_should_not_change_default_channel_when_all_channels_fail(self): extension_url = 'https://fake_host/fake_extension.zip' target_file = os.path.join(self.tmp_dir, "fake_extension.zip") + target_directory = os.path.join(self.tmp_dir, "fake_extension") def http_get_handler(url, *_, **kwargs): if url == extension_url or self.is_host_plugin_extension_request(url, kwargs, extension_url): @@ -602,7 +622,7 @@ def http_get_handler(url, *_, **kwargs): protocol.set_http_handlers(http_get_handler=http_get_handler) with self.assertRaises(ExtensionDownloadError): - protocol.client.download_extension([extension_url], target_file, use_verify_header=False) + protocol.client.download_zip_package("extension package", [extension_url], target_file, target_directory, use_verify_header=False) urls = protocol.get_tracked_urls() self.assertEqual(len(urls), 2, "Unexpected number of HTTP requests: [{0}]".format(urls)) @@ -611,6 +631,25 @@ def http_get_handler(url, *_, **kwargs): self.assertFalse(os.path.exists(target_file), "The extension package was downloaded and it shouldn't have") self.assertFalse(HostPluginProtocol.is_default_channel, "The host channel should not have been set as the default") + def test_invalid_zip_should_raise_an_error(self): + extension_url = 'https://fake_host/fake_extension.zip' + target_file = os.path.join(self.tmp_dir, "fake_extension.zip") + target_directory = os.path.join(self.tmp_dir, "fake_extension") + + def http_get_handler(url, *_, **kwargs): + if url == extension_url or self.is_host_plugin_extension_request(url, kwargs, extension_url): + return MockHttpResponse(status=200, body=b"NOT A ZIP") + return None + + with mock_wire_protocol(mockwiredata.DATA_FILE) as protocol: + protocol.set_http_handlers(http_get_handler=http_get_handler) + + with self.assertRaises(ExtensionDownloadError): + protocol.client.download_zip_package("extension package", [extension_url], target_file, target_directory, use_verify_header=False) + + self.assertFalse(os.path.exists(target_file), "The extension package should have been deleted") + self.assertFalse(os.path.exists(target_directory), "The extension directory should not have been created") + def test_fetch_manifest_should_not_invoke_host_channel_when_direct_channel_succeeds(self): manifest_url = 'https://fake_host/fake_manifest.xml' manifest_xml = '' diff --git a/tests/test_agent.py b/tests/test_agent.py index c585e845ee..2f80d695e0 100644 --- a/tests/test_agent.py +++ b/tests/test_agent.py @@ -232,14 +232,13 @@ def test_calls_collect_logs_on_valid_cgroups(self, mock_log_collector): try: CollectLogsHandler.enable_cgroups_validation() - @staticmethod def mock_cgroup_paths(*args, **kwargs): if args and args[0] == "self": relative_path = "{0}/{1}".format(cgroupconfigurator.LOGCOLLECTOR_SLICE, logcollector.CGROUPS_UNIT) return (cgroupconfigurator.LOGCOLLECTOR_SLICE, relative_path) return SystemdCgroupsApi.get_process_cgroup_relative_paths(*args, **kwargs) - with patch.object(SystemdCgroupsApi, "get_process_cgroup_relative_paths", mock_cgroup_paths): + with patch("azurelinuxagent.agent.SystemdCgroupsApi.get_process_cgroup_paths", side_effect=mock_cgroup_paths): agent = Agent(False, conf_file_path=os.path.join(data_dir, "test_waagent.conf")) agent.collect_logs(is_full_mode=True) @@ -251,13 +250,12 @@ def test_doesnt_call_collect_logs_on_invalid_cgroups(self): try: CollectLogsHandler.enable_cgroups_validation() - @staticmethod def mock_cgroup_paths(*args, **kwargs): if args and args[0] == "self": return ("NOT_THE_CORRECT_PATH", "NOT_THE_CORRECT_PATH") return SystemdCgroupsApi.get_process_cgroup_relative_paths(*args, **kwargs) - with patch.object(SystemdCgroupsApi, "get_process_cgroup_relative_paths", mock_cgroup_paths): + with patch("azurelinuxagent.agent.SystemdCgroupsApi.get_process_cgroup_paths", side_effect=mock_cgroup_paths): agent = Agent(False, conf_file_path=os.path.join(data_dir, "test_waagent.conf")) exit_error = RuntimeError("Exiting") From a1f30494da793ceb1b81a5b8fed8572addc78f8e Mon Sep 17 00:00:00 2001 From: Norberto Arrieta Date: Wed, 26 Oct 2022 09:25:08 -0700 Subject: [PATCH 07/63] Initial (bare-bones) implementation of the infrastructure for end-to-end tests (#2691) * Prototype for end-to-end tests * Add agent BVT * Cleanup SSH key; delete cleanup pipeline * Delete unused references * Delete unused references - Part 2 * Remove local configuration * Remove unused references, Part 3 * Disable proxy Co-authored-by: narrieta --- tests_e2e/__init__.py | 0 tests_e2e/azure-pipelines.yml | 21 ++ tests_e2e/lisa/runbook/azure.yml | 82 ++++++ tests_e2e/lisa/tests/__init__.py | 0 tests_e2e/lisa/tests/agent_bvt/__init__.py | 0 .../tests/agent_bvt/check_agent_version.py | 23 ++ .../lisa/tests/agent_bvt/custom_script.py | 45 ++++ tests_e2e/lisa/testsuites/agent-bvt.py | 41 +++ tests_e2e/requirements.txt | 13 + tests_e2e/scenario_utils/__init__.py | 0 tests_e2e/scenario_utils/azure_models.py | 239 ++++++++++++++++++ .../extensions/BaseExtensionTestClass.py | 113 +++++++++ .../extensions/CustomScriptExtension.py | 29 +++ .../scenario_utils/extensions/__init__.py | 0 tests_e2e/scenario_utils/models.py | 137 ++++++++++ tests_e2e/scripts/__init__.py | 0 tests_e2e/scripts/execute_tests.sh | 18 ++ tests_e2e/scripts/install_dependencies.sh | 43 ++++ tests_e2e/templates/execute-tests.yml | 36 +++ 19 files changed, 840 insertions(+) create mode 100644 tests_e2e/__init__.py create mode 100644 tests_e2e/azure-pipelines.yml create mode 100644 tests_e2e/lisa/runbook/azure.yml create mode 100644 tests_e2e/lisa/tests/__init__.py create mode 100644 tests_e2e/lisa/tests/agent_bvt/__init__.py create mode 100755 tests_e2e/lisa/tests/agent_bvt/check_agent_version.py create mode 100644 tests_e2e/lisa/tests/agent_bvt/custom_script.py create mode 100644 tests_e2e/lisa/testsuites/agent-bvt.py create mode 100644 tests_e2e/requirements.txt create mode 100644 tests_e2e/scenario_utils/__init__.py create mode 100644 tests_e2e/scenario_utils/azure_models.py create mode 100644 tests_e2e/scenario_utils/extensions/BaseExtensionTestClass.py create mode 100644 tests_e2e/scenario_utils/extensions/CustomScriptExtension.py create mode 100644 tests_e2e/scenario_utils/extensions/__init__.py create mode 100644 tests_e2e/scenario_utils/models.py create mode 100644 tests_e2e/scripts/__init__.py create mode 100755 tests_e2e/scripts/execute_tests.sh create mode 100755 tests_e2e/scripts/install_dependencies.sh create mode 100644 tests_e2e/templates/execute-tests.yml diff --git a/tests_e2e/__init__.py b/tests_e2e/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests_e2e/azure-pipelines.yml b/tests_e2e/azure-pipelines.yml new file mode 100644 index 0000000000..689ad868be --- /dev/null +++ b/tests_e2e/azure-pipelines.yml @@ -0,0 +1,21 @@ +variables: + - name: azureConnection + value: 'AzLinux DCR Public (8e037ad4-618f-4466-8bc8-5099d41ac15b)' + - name: subId + value: '8e037ad4-618f-4466-8bc8-5099d41ac15b' + - name: testsSourcesDirectory + value: "$(Build.SourcesDirectory)/tests_e2e" + +trigger: + - develop + +pr: none + +pool: + vmImage: ubuntu-latest + +stages: + - stage: "Execute" + jobs: + - template: 'templates/execute-tests.yml' + diff --git a/tests_e2e/lisa/runbook/azure.yml b/tests_e2e/lisa/runbook/azure.yml new file mode 100644 index 0000000000..8f0ef40133 --- /dev/null +++ b/tests_e2e/lisa/runbook/azure.yml @@ -0,0 +1,82 @@ +name: azure +extension: + - "../testsuites" +variable: + - name: location + value: "westus2" + - name: subscription_id + value: "" + - name: resource_group_name + value: "" + # + # Set the vm_name to run on an existing VM + # + - name: vm_name + value: "" + - name: marketplace_image + value: "Canonical UbuntuServer 18.04-LTS latest" + - name: vhd + value: "" + - name: vm_size + value: "" + # + # Turn off deploy to run on an existing VM + # + - name: deploy + value: true + - name: keep_environment + value: "no" + - name: wait_delete + value: false + - name: user + value: "waagent" + - name: identity_file + value: "" + is_secret: true + - name: admin_password + value: "" + is_secret: true + - name: proxy_host + value: "" + - name: proxy_user + value: "" + - name: proxy_identity_file + value: "" + is_secret: true +notifier: + - type: html + - type: env_stats +platform: + - type: azure + admin_username: $(user) + admin_private_key_file: $(identity_file) + admin_password: $(admin_password) + keep_environment: $(keep_environment) + azure: + resource_group_name: $(resource_group_name) + deploy: $(deploy) + subscription_id: $(subscription_id) + wait_delete: $(wait_delete) + requirement: + core_count: + min: 2 + azure: + marketplace: "$(marketplace_image)" + vhd: $(vhd) + location: $(location) + name: $(vm_name) + vm_size: $(vm_size) + +testcase: + - criteria: + area: bvt + +# +# Set to do SSH proxy jumps +# +#dev: +# mock_tcp_ping: True +# jump_boxes: +# - private_key_file: $(proxy_identity_file) +# address: $(proxy_host) +# username: $(proxy_user) diff --git a/tests_e2e/lisa/tests/__init__.py b/tests_e2e/lisa/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests_e2e/lisa/tests/agent_bvt/__init__.py b/tests_e2e/lisa/tests/agent_bvt/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests_e2e/lisa/tests/agent_bvt/check_agent_version.py b/tests_e2e/lisa/tests/agent_bvt/check_agent_version.py new file mode 100755 index 0000000000..c63402dfae --- /dev/null +++ b/tests_e2e/lisa/tests/agent_bvt/check_agent_version.py @@ -0,0 +1,23 @@ +#!/usr/bin/env python + +from __future__ import print_function + +import subprocess +import sys + + +def main(): + print("Executing waagent --version") + + pipe = subprocess.Popen(['waagent', '-version'], stdout=subprocess.PIPE, stderr=subprocess.PIPE) + stdout_lines = list(map(lambda s: s.decode('utf-8'), pipe.stdout.readlines())) + exit_code = pipe.wait() + + for line in stdout_lines: + print(line) + + return exit_code + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tests_e2e/lisa/tests/agent_bvt/custom_script.py b/tests_e2e/lisa/tests/agent_bvt/custom_script.py new file mode 100644 index 0000000000..f7bd6de4b0 --- /dev/null +++ b/tests_e2e/lisa/tests/agent_bvt/custom_script.py @@ -0,0 +1,45 @@ +import argparse +import os +import uuid +import sys + +from tests_e2e.scenario_utils.extensions.CustomScriptExtension import CustomScriptExtension + + +def main(subscription_id, resource_group_name, vm_name): + os.environ["VMNAME"] = vm_name + os.environ['RGNAME'] = resource_group_name + os.environ["SUBID"] = subscription_id + os.environ["SCENARIONAME"] = "BVT" + os.environ["LOCATION"] = "westus2" + os.environ["ADMINUSERNAME"] = "somebody" + os.environ["BUILD_SOURCESDIRECTORY"] = "/somewhere" + + cse = CustomScriptExtension(extension_name="testCSE") + + ext_props = [ + cse.get_ext_props(settings={'commandToExecute': f"echo \'Hello World! {uuid.uuid4()} \'"}), + cse.get_ext_props(settings={'commandToExecute': "echo \'Hello again\'"}) + ] + + cse.run(ext_props=ext_props) + + +if __name__ == "__main__": + try: + parser = argparse.ArgumentParser() + parser.add_argument('--subscription') + parser.add_argument('--group') + parser.add_argument('--vm') + + args = parser.parse_args() + + main(args.subscription, args.group, args.vm) + + except Exception as exception: + print(str(exception)) + sys.exit(1) + + sys.exit(0) + + diff --git a/tests_e2e/lisa/testsuites/agent-bvt.py b/tests_e2e/lisa/testsuites/agent-bvt.py new file mode 100644 index 0000000000..7b3fa53d1a --- /dev/null +++ b/tests_e2e/lisa/testsuites/agent-bvt.py @@ -0,0 +1,41 @@ +from assertpy import assert_that +from pathlib import Path +from tests_e2e.lisa.tests.agent_bvt import custom_script + +from lisa import ( + CustomScriptBuilder, + Logger, + Node, + simple_requirement, + TestCaseMetadata, + TestSuite, + TestSuiteMetadata, +) +from lisa.sut_orchestrator.azure.common import get_node_context + + +@TestSuiteMetadata( + area="bvt", + category="functional", + description=""" + A POC test suite for the waagent BVTs. + """, + requirement=simple_requirement(unsupported_os=[]), +) +class AgentBvt(TestSuite): + @TestCaseMetadata(description="", priority=0) + def check_agent_version(self, node: Node, log: Logger) -> None: + script_path = CustomScriptBuilder(Path(__file__).parent.parent.joinpath("tests", "agent_bvt"), ["check_agent_version.py"]) + script = node.tools[script_path] + result = script.run() + log.info(result.stdout) + log.error(result.stderr) + assert_that(result.exit_code).is_equal_to(0) + + @TestCaseMetadata(description="", priority=0) + def custom_script(self, node: Node) -> None: + node_context = get_node_context(node) + subscription_id = node.features._platform.subscription_id + resource_group_name = node_context.resource_group_name + vm_name = node_context.vm_name + custom_script.main(subscription_id, resource_group_name, vm_name) diff --git a/tests_e2e/requirements.txt b/tests_e2e/requirements.txt new file mode 100644 index 0000000000..63bd6d2c73 --- /dev/null +++ b/tests_e2e/requirements.txt @@ -0,0 +1,13 @@ +# This is a list of pip packages that will be installed on both the orchestrator and the test VM +# Only add the common packages here, for more specific modules, add them to the scenario itself +azure-identity +azure-keyvault-keys +azure-mgmt-compute>=22.1.0 +azure-mgmt-keyvault>=7.0.0 +azure-mgmt-network>=16.0.0 +azure-mgmt-resource>=15.0.0 +cryptography +distro +junitparser +msrestazure +python-dotenv \ No newline at end of file diff --git a/tests_e2e/scenario_utils/__init__.py b/tests_e2e/scenario_utils/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests_e2e/scenario_utils/azure_models.py b/tests_e2e/scenario_utils/azure_models.py new file mode 100644 index 0000000000..4422e3d608 --- /dev/null +++ b/tests_e2e/scenario_utils/azure_models.py @@ -0,0 +1,239 @@ +import time +from abc import ABC, abstractmethod +from builtins import TimeoutError +from typing import List + +from azure.core.exceptions import HttpResponseError +from azure.core.polling import LROPoller +from azure.identity import DefaultAzureCredential +from azure.mgmt.compute import ComputeManagementClient +from azure.mgmt.compute.models import VirtualMachineExtension, VirtualMachineScaleSetExtension, \ + VirtualMachineInstanceView, VirtualMachineScaleSetInstanceView, VirtualMachineExtensionInstanceView +from azure.mgmt.resource import ResourceManagementClient +from msrestazure.azure_exceptions import CloudError + +from dcr.scenario_utils.logging_utils import LoggingHandler +from dcr.scenario_utils.models import get_vm_data_from_env, VMModelType, VMMetaData + + +class AzureComputeBaseClass(ABC, LoggingHandler): + + def __init__(self): + super().__init__() + self.__vm_data = get_vm_data_from_env() + self.__compute_client = None + self.__resource_client = None + + @property + def vm_data(self) -> VMMetaData: + return self.__vm_data + + @property + def compute_client(self) -> ComputeManagementClient: + if self.__compute_client is None: + self.__compute_client = ComputeManagementClient( + credential=DefaultAzureCredential(), + subscription_id=self.vm_data.sub_id + ) + return self.__compute_client + + @property + def resource_client(self) -> ResourceManagementClient: + if self.__resource_client is None: + self.__resource_client = ResourceManagementClient( + credential=DefaultAzureCredential(), + subscription_id=self.vm_data.sub_id + ) + return self.__resource_client + + @property + @abstractmethod + def vm_func(self): + pass + + @property + @abstractmethod + def extension_func(self): + pass + + @abstractmethod + def get_vm_instance_view(self): + pass + + @abstractmethod + def get_extensions(self): + pass + + @abstractmethod + def get_extension_instance_view(self, extension_name): + pass + + @abstractmethod + def get_ext_props(self, extension_data, settings=None, protected_settings=None, auto_upgrade_minor_version=True, + force_update_tag=None): + pass + + @abstractmethod + def restart(self, timeout=5): + pass + + def _run_azure_op_with_retry(self, get_func): + max_retries = 3 + retries = max_retries + while retries > 0: + try: + ext = get_func() + return ext + except (CloudError, HttpResponseError) as ce: + if retries > 0: + self.log.exception(f"Got Azure error: {ce}") + self.log.warning("...retrying [{0} attempts remaining]".format(retries)) + retries -= 1 + time.sleep(30 * (max_retries - retries)) + else: + raise + + +class VirtualMachineHelper(AzureComputeBaseClass): + + def __init__(self): + super().__init__() + + @property + def vm_func(self): + return self.compute_client.virtual_machines + + @property + def extension_func(self): + return self.compute_client.virtual_machine_extensions + + def get_vm_instance_view(self) -> VirtualMachineInstanceView: + return self._run_azure_op_with_retry(lambda: self.vm_func.get( + resource_group_name=self.vm_data.rg_name, + vm_name=self.vm_data.name, + expand="instanceView" + )) + + def get_extensions(self) -> List[VirtualMachineExtension]: + return self._run_azure_op_with_retry(lambda: self.extension_func.list( + resource_group_name=self.vm_data.rg_name, + vm_name=self.vm_data.name + )) + + def get_extension_instance_view(self, extension_name) -> VirtualMachineExtensionInstanceView: + return self._run_azure_op_with_retry(lambda: self.extension_func.get( + resource_group_name=self.vm_data.rg_name, + vm_name=self.vm_data.name, + vm_extension_name=extension_name, + expand="instanceView" + )) + + def get_ext_props(self, extension_data, settings=None, protected_settings=None, auto_upgrade_minor_version=True, + force_update_tag=None) -> VirtualMachineExtension: + return VirtualMachineExtension( + location=self.vm_data.location, + publisher=extension_data.publisher, + type_properties_type=extension_data.ext_type, + type_handler_version=extension_data.version, + auto_upgrade_minor_version=auto_upgrade_minor_version, + settings=settings, + protected_settings=protected_settings, + force_update_tag=force_update_tag + ) + + def restart(self, timeout=5): + self.log.info(f"Initiating restart of machine: {self.vm_data.name}") + poller : LROPoller = self._run_azure_op_with_retry(lambda: self.vm_func.begin_restart( + resource_group_name=self.vm_data.rg_name, + vm_name=self.vm_data.name + )) + poller.wait(timeout=timeout * 60) + if not poller.done(): + raise TimeoutError(f"Machine {self.vm_data.name} failed to restart after {timeout} mins") + self.log.info(f"Restarted machine: {self.vm_data.name}") + + +class VirtualMachineScaleSetHelper(AzureComputeBaseClass): + + def restart(self, timeout=5): + poller: LROPoller = self._run_azure_op_with_retry(lambda: self.vm_func.begin_restart( + resource_group_name=self.vm_data.rg_name, + vm_scale_set_name=self.vm_data.name + )) + poller.wait(timeout=timeout * 60) + if not poller.done(): + raise TimeoutError(f"ScaleSet {self.vm_data.name} failed to restart after {timeout} mins") + + def __init__(self): + super().__init__() + + @property + def vm_func(self): + return self.compute_client.virtual_machine_scale_set_vms + + @property + def extension_func(self): + return self.compute_client.virtual_machine_scale_set_extensions + + def get_vm_instance_view(self) -> VirtualMachineScaleSetInstanceView: + # Since this is a VMSS, return the instance view of the first VMSS VM. For the instance view of the complete VMSS, + # use the compute_client.virtual_machine_scale_sets function - + # https://docs.microsoft.com/en-us/python/api/azure-mgmt-compute/azure.mgmt.compute.v2019_12_01.operations.virtualmachinescalesetsoperations?view=azure-python + + for vm in self._run_azure_op_with_retry(lambda: self.vm_func.list(self.vm_data.rg_name, self.vm_data.name)): + try: + return self._run_azure_op_with_retry(lambda: self.vm_func.get_instance_view( + resource_group_name=self.vm_data.rg_name, + vm_scale_set_name=self.vm_data.name, + instance_id=vm.instance_id + )) + except Exception as err: + self.log.warning( + f"Unable to fetch instance view of VMSS VM: {vm}. Trying out other instances.\nError: {err}") + continue + + raise Exception(f"Unable to fetch instance view of any VMSS instances for {self.vm_data.name}") + + def get_extensions(self) -> List[VirtualMachineScaleSetExtension]: + return self._run_azure_op_with_retry(lambda: self.extension_func.list( + resource_group_name=self.vm_data.rg_name, + vm_scale_set_name=self.vm_data.name + )) + + def get_extension_instance_view(self, extension_name) -> VirtualMachineExtensionInstanceView: + return self._run_azure_op_with_retry(lambda: self.extension_func.get( + resource_group_name=self.vm_data.rg_name, + vm_scale_set_name=self.vm_data.name, + vmss_extension_name=extension_name, + expand="instanceView" + )) + + def get_ext_props(self, extension_data, settings=None, protected_settings=None, auto_upgrade_minor_version=True, + force_update_tag=None) -> VirtualMachineScaleSetExtension: + return VirtualMachineScaleSetExtension( + publisher=extension_data.publisher, + type_properties_type=extension_data.ext_type, + type_handler_version=extension_data.version, + auto_upgrade_minor_version=auto_upgrade_minor_version, + settings=settings, + protected_settings=protected_settings + ) + + +class ComputeManager: + """ + The factory class for setting the Helper class based on the setting. + """ + def __init__(self): + self.__vm_data = get_vm_data_from_env() + self.__compute_manager = None + + @property + def is_vm(self) -> bool: + return self.__vm_data.model_type == VMModelType.VM + + @property + def compute_manager(self): + if self.__compute_manager is None: + self.__compute_manager = VirtualMachineHelper() if self.is_vm else VirtualMachineScaleSetHelper() + return self.__compute_manager diff --git a/tests_e2e/scenario_utils/extensions/BaseExtensionTestClass.py b/tests_e2e/scenario_utils/extensions/BaseExtensionTestClass.py new file mode 100644 index 0000000000..8c23e1e712 --- /dev/null +++ b/tests_e2e/scenario_utils/extensions/BaseExtensionTestClass.py @@ -0,0 +1,113 @@ +import time +from typing import List + +from azure.core.polling import LROPoller + +from dcr.scenario_utils.azure_models import ComputeManager +from dcr.scenario_utils.logging_utils import LoggingHandler +from dcr.scenario_utils.models import ExtensionMetaData, get_vm_data_from_env + + +class BaseExtensionTestClass(LoggingHandler): + + def __init__(self, extension_data: ExtensionMetaData): + super().__init__() + self.__extension_data = extension_data + self.__vm_data = get_vm_data_from_env() + self.__compute_manager = ComputeManager().compute_manager + + def get_ext_props(self, settings=None, protected_settings=None, auto_upgrade_minor_version=True, + force_update_tag=None): + + return self.__compute_manager.get_ext_props( + extension_data=self.__extension_data, + settings=settings, + protected_settings=protected_settings, + auto_upgrade_minor_version=auto_upgrade_minor_version, + force_update_tag=force_update_tag + ) + + def run(self, ext_props: List, remove: bool = True, continue_on_error: bool = False): + + def __add_extension(): + extension: LROPoller = self.__compute_manager.extension_func.begin_create_or_update( + self.__vm_data.rg_name, + self.__vm_data.name, + self.__extension_data.name, + ext_prop + ) + self.log.info("Add extension: {0}".format(extension.result(timeout=5 * 60))) + + def __remove_extension(): + self.__compute_manager.extension_func.begin_delete( + self.__vm_data.rg_name, + self.__vm_data.name, + self.__extension_data.name + ).result() + self.log.info(f"Delete vm extension {self.__extension_data.name} successful") + + def _retry_on_retryable_error(func): + retry = 1 + while retry < 5: + try: + func() + break + except Exception as err_: + if "RetryableError" in str(err_) and retry < 5: + self.log.warning(f"({retry}/5) Ran into RetryableError, retrying in 30 secs: {err_}") + time.sleep(30) + retry += 1 + continue + raise + + try: + for ext_prop in ext_props: + try: + _retry_on_retryable_error(__add_extension) + # Validate success from instance view + _retry_on_retryable_error(self.validate_ext) + except Exception as err: + if continue_on_error: + self.log.exception("Ran into error but ignoring it as asked: {0}".format(err)) + continue + else: + raise + finally: + # Always try to delete extensions if asked to remove even on errors + if remove: + _retry_on_retryable_error(__remove_extension) + + def validate_ext(self): + """ + Validate if the extension operation was successful from the Instance View + :raises: Exception if either unable to fetch instance view or if extension not successful + """ + retry = 0 + max_retry = 3 + ext_instance_view = None + status = None + + while retry < max_retry: + try: + ext_instance_view = self.__compute_manager.get_extension_instance_view(self.__extension_data.name) + if ext_instance_view is None: + raise Exception("Extension not found") + elif not ext_instance_view.instance_view: + raise Exception("Instance view not present") + elif not ext_instance_view.instance_view.statuses or len(ext_instance_view.instance_view.statuses) < 1: + raise Exception("Instance view status not present") + else: + status = ext_instance_view.instance_view.statuses[0].code + status_message = ext_instance_view.instance_view.statuses[0].message + self.log.info('Extension Status: \n\tCode: [{0}]\n\tMessage: {1}'.format(status, status_message)) + break + except Exception as err: + self.log.exception(f"Ran into error: {err}") + retry += 1 + if retry < max_retry: + self.log.info("Retrying in 30 secs") + time.sleep(30) + raise + + if 'succeeded' not in status: + raise Exception(f"Extension did not succeed. Last Instance view: {ext_instance_view}") diff --git a/tests_e2e/scenario_utils/extensions/CustomScriptExtension.py b/tests_e2e/scenario_utils/extensions/CustomScriptExtension.py new file mode 100644 index 0000000000..29df351134 --- /dev/null +++ b/tests_e2e/scenario_utils/extensions/CustomScriptExtension.py @@ -0,0 +1,29 @@ +import uuid + +from dcr.scenario_utils.extensions.BaseExtensionTestClass import BaseExtensionTestClass +from dcr.scenario_utils.models import ExtensionMetaData + + +class CustomScriptExtension(BaseExtensionTestClass): + META_DATA = ExtensionMetaData( + publisher='Microsoft.Azure.Extensions', + ext_type='CustomScript', + version="2.1" + ) + + def __init__(self, extension_name: str): + extension_data = self.META_DATA + extension_data.name = extension_name + super().__init__(extension_data) + + +def add_cse(): + # Install and remove CSE + cse = CustomScriptExtension(extension_name="testCSE") + + ext_props = [ + cse.get_ext_props(settings={'commandToExecute': f"echo \'Hello World! {uuid.uuid4()} \'"}), + cse.get_ext_props(settings={'commandToExecute': "echo \'Hello again\'"}) + ] + + cse.run(ext_props=ext_props) \ No newline at end of file diff --git a/tests_e2e/scenario_utils/extensions/__init__.py b/tests_e2e/scenario_utils/extensions/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests_e2e/scenario_utils/models.py b/tests_e2e/scenario_utils/models.py new file mode 100644 index 0000000000..806c830c12 --- /dev/null +++ b/tests_e2e/scenario_utils/models.py @@ -0,0 +1,137 @@ +import os +from enum import Enum, auto +from typing import List + +from dotenv import load_dotenv + + +class VMModelType(Enum): + VM = auto() + VMSS = auto() + + +class ExtensionMetaData: + def __init__(self, publisher: str, ext_type: str, version: str, ext_name: str = ""): + self.__publisher = publisher + self.__ext_type = ext_type + self.__version = version + self.__ext_name = ext_name + + @property + def publisher(self) -> str: + return self.__publisher + + @property + def ext_type(self) -> str: + return self.__ext_type + + @property + def version(self) -> str: + return self.__version + + @property + def name(self): + return self.__ext_name + + @name.setter + def name(self, ext_name): + self.__ext_name = ext_name + + @property + def handler_name(self): + return f"{self.publisher}.{self.ext_type}" + + +class VMMetaData: + + def __init__(self, vm_name: str, rg_name: str, sub_id: str, location: str, admin_username: str, + ips: List[str] = None): + self.__vm_name = vm_name + self.__rg_name = rg_name + self.__sub_id = sub_id + self.__location = location + self.__admin_username = admin_username + + vm_ips, vmss_ips = _get_ips(admin_username) + # By default assume the test is running on a VM + self.__type = VMModelType.VM + self.__ips = vm_ips + if any(vmss_ips): + self.__type = VMModelType.VMSS + self.__ips = vmss_ips + + if ips is not None: + self.__ips = ips + + print(f"IPs: {self.__ips}") + + @property + def name(self) -> str: + return self.__vm_name + + @property + def rg_name(self) -> str: + return self.__rg_name + + @property + def location(self) -> str: + return self.__location + + @property + def sub_id(self) -> str: + return self.__sub_id + + @property + def admin_username(self): + return self.__admin_username + + @property + def ips(self) -> List[str]: + return self.__ips + + @property + def model_type(self): + return self.__type + + +def _get_ips(username) -> (list, list): + """ + Try fetching Ips from the files that we create via az-cli. + We do a best effort to fetch this from both orchestrator or the test VM. Its located in different locations on both + scenarios. + Returns: Tuple of (VmIps, VMSSIps). + """ + + vms, vmss = [], [] + orchestrator_path = os.path.join(os.environ['BUILD_SOURCESDIRECTORY'], "dcr") + test_vm_path = os.path.join("/home", username, "dcr") + + for ip_path in [orchestrator_path, test_vm_path]: + + vm_ip_path = os.path.join(ip_path, ".vm_ips") + if os.path.exists(vm_ip_path): + with open(vm_ip_path, 'r') as vm_ips: + vms.extend(ip.strip() for ip in vm_ips.readlines()) + + vmss_ip_path = os.path.join(ip_path, ".vmss_ips") + if os.path.exists(vmss_ip_path): + with open(vmss_ip_path, 'r') as vmss_ips: + vmss.extend(ip.strip() for ip in vmss_ips.readlines()) + + return vms, vmss + + +def get_vm_data_from_env() -> VMMetaData: + if get_vm_data_from_env.__instance is None: + load_dotenv() + get_vm_data_from_env.__instance = VMMetaData(vm_name=os.environ["VMNAME"], + rg_name=os.environ['RGNAME'], + sub_id=os.environ["SUBID"], + location=os.environ['LOCATION'], + admin_username=os.environ['ADMINUSERNAME']) + + return get_vm_data_from_env.__instance + + +get_vm_data_from_env.__instance = None + diff --git a/tests_e2e/scripts/__init__.py b/tests_e2e/scripts/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests_e2e/scripts/execute_tests.sh b/tests_e2e/scripts/execute_tests.sh new file mode 100755 index 0000000000..47e846ec9a --- /dev/null +++ b/tests_e2e/scripts/execute_tests.sh @@ -0,0 +1,18 @@ +#!/usr/bin/env bash + +set -euxo pipefail + +export PYTHONPATH=$BUILD_SOURCESDIRECTORY + +cd $BUILD_SOURCESDIRECTORY/lisa + +# LISA needs both the public and private keys; generate the former +chmod 700 $SSHKEY_SECUREFILEPATH +ssh-keygen -y -f $SSHKEY_SECUREFILEPATH > "$SSHKEY_SECUREFILEPATH".pub + +./lisa.sh --runbook $BUILD_SOURCESDIRECTORY/tests_e2e/lisa/runbook/azure.yml \ + --log_path $HOME/tmp \ + --working_path $HOME/tmp \ + -v subscription_id:$SUBID \ + -v identity_file:$SSHKEY_SECUREFILEPATH + diff --git a/tests_e2e/scripts/install_dependencies.sh b/tests_e2e/scripts/install_dependencies.sh new file mode 100755 index 0000000000..43cbf68e1c --- /dev/null +++ b/tests_e2e/scripts/install_dependencies.sh @@ -0,0 +1,43 @@ +#!/usr/bin/env bash + +set -euxo pipefail + +# +# Install LISA (see https://mslisa.readthedocs.io/en/main/installation_linux.html) +# + +# Install LISA dependencies +sudo apt install -y git gcc libgirepository1.0-dev libcairo2-dev qemu-utils libvirt-dev python3-venv + +# Install Poetry +curl -sSL https://raw.githubusercontent.com/python-poetry/poetry/master/install-poetry.py | python3 - +export PATH="$HOME/.local/bin:$PATH" +echo "##vso[task.prependpath]$HOME/.local/bin" + +# Install LISA +cd $BUILD_SOURCESDIRECTORY +git clone https://github.com/microsoft/lisa.git +cd lisa +make setup + +# Verify LISA installation +./lisa.sh + +# +# Install test dependencies +# +# NOTE: Need to review the test dependencies, they require module versions greater than the same modules used by LISA +# and the version update adds a significant build delay. For the moment, just add the modules not included +# already by LISA +# +# ===== DISABLED ===== +## (make a copy of the requirements file removing comments since poetry-add-requirements does not support them) +## +#pip install poetry-add-requirements.txt +#sed '/^#/d' $BUILD_SOURCESDIRECTORY/tests_e2e/requirements.txt > WALinuxAgent-requirements.txt +#poetry-add-requirements.txt WALinuxAgent-requirements.txt +# ===== END DISABLED ===== + +poetry add msrestazure +poetry add python-dotenv + diff --git a/tests_e2e/templates/execute-tests.yml b/tests_e2e/templates/execute-tests.yml new file mode 100644 index 0000000000..e4abdc8b0d --- /dev/null +++ b/tests_e2e/templates/execute-tests.yml @@ -0,0 +1,36 @@ +jobs: + - job: "" + + steps: + + - task: DownloadSecureFile@1 + name: sshKey + displayName: "Download SSH key" + inputs: + secureFile: 'id_rsa' + + - task: AzureKeyVault@2 + displayName: "Fetch secrets from KV" + inputs: + azureSubscription: '$(azureConnection)' + KeyVaultName: 'dcrV2SPs' + SecretsFilter: '*' + RunAsPreJob: true + + - task: UsePythonVersion@0 + displayName: "Set Python Version" + inputs: + versionSpec: '3.10' + addToPath: true + architecture: 'x64' + + - bash: $(testsSourcesDirectory)/scripts/install_dependencies.sh + displayName: "Install Dependencies" + + - bash: $(testsSourcesDirectory)/scripts/execute_tests.sh + displayName: "Execute tests" + env: + # Add all KeyVault secrets explicitly as they're not added by default to the environment vars + AZURE_CLIENT_ID: $(AZURE-CLIENT-ID) + AZURE_CLIENT_SECRET: $(AZURE-CLIENT-SECRET) + AZURE_TENANT_ID: $(AZURE-TENANT-ID) \ No newline at end of file From a906320c63cb111672b4b813b6dca81f470d9060 Mon Sep 17 00:00:00 2001 From: Vitaly Kuznetsov Date: Mon, 31 Oct 2022 14:50:05 +0100 Subject: [PATCH 08/63] Minimal Fedora OS implementation (#2642) Fedora distribution is not covered by Redhat*OS* classes, create a separate one for it. Note: this patch was previosly carried out-of-tree, fedora packages already have it included. Signed-off-by: Vitaly Kuznetsov Signed-off-by: Vitaly Kuznetsov Co-authored-by: Norberto Arrieta --- azurelinuxagent/common/osutil/factory.py | 4 ++ azurelinuxagent/common/osutil/fedora.py | 77 ++++++++++++++++++++++++ setup.py | 6 ++ 3 files changed, 87 insertions(+) create mode 100644 azurelinuxagent/common/osutil/fedora.py diff --git a/azurelinuxagent/common/osutil/factory.py b/azurelinuxagent/common/osutil/factory.py index d48c493477..83123e3f53 100644 --- a/azurelinuxagent/common/osutil/factory.py +++ b/azurelinuxagent/common/osutil/factory.py @@ -40,6 +40,7 @@ from .photonos import PhotonOSUtil from .ubuntu import UbuntuOSUtil, Ubuntu12OSUtil, Ubuntu14OSUtil, \ UbuntuSnappyOSUtil, Ubuntu16OSUtil, Ubuntu18OSUtil +from .fedora import FedoraOSUtil def get_osutil(distro_name=DISTRO_NAME, @@ -153,5 +154,8 @@ def _get_osutil(distro_name, distro_code_name, distro_version, distro_full_name) if distro_name == "openwrt": return OpenWRTOSUtil() + if distro_name == "fedora": + return FedoraOSUtil() + logger.warn("Unable to load distro implementation for {0}. Using default distro implementation instead.", distro_name) return DefaultOSUtil() diff --git a/azurelinuxagent/common/osutil/fedora.py b/azurelinuxagent/common/osutil/fedora.py new file mode 100644 index 0000000000..164b55ebf7 --- /dev/null +++ b/azurelinuxagent/common/osutil/fedora.py @@ -0,0 +1,77 @@ +# +# Copyright 2022 Red Hat Inc. +# +# 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. +# +# Requires Python 2.6+ and Openssl 1.0+ +# + +import time +import azurelinuxagent.common.logger as logger +import azurelinuxagent.common.utils.shellutil as shellutil +from azurelinuxagent.common.osutil.default import DefaultOSUtil + + +class FedoraOSUtil(DefaultOSUtil): + + def __init__(self): + super(FedoraOSUtil, self).__init__() + self.agent_conf_file_path = '/etc/waagent.conf' + + @staticmethod + def get_systemd_unit_file_install_path(): + return '/usr/lib/systemd/system' + + @staticmethod + def get_agent_bin_path(): + return '/usr/sbin' + + def is_dhcp_enabled(self): + return True + + def start_network(self): + pass + + def restart_if(self, ifname=None, retries=None, wait=None): + retry_limit = retries+1 + for attempt in range(1, retry_limit): + return_code = shellutil.run("ip link set {0} down && ip link set {0} up".format(ifname)) + if return_code == 0: + return + logger.warn("failed to restart {0}: return code {1}".format(ifname, return_code)) + if attempt < retry_limit: + logger.info("retrying in {0} seconds".format(wait)) + time.sleep(wait) + else: + logger.warn("exceeded restart retries") + + def restart_ssh_service(self): + shellutil.run('systemctl restart sshd') + + def stop_dhcp_service(self): + pass + + def start_dhcp_service(self): + pass + + def start_agent_service(self): + return shellutil.run('systemctl start waagent', chk_err=False) + + def stop_agent_service(self): + return shellutil.run('systemctl stop waagent', chk_err=False) + + def get_dhcp_pid(self): + return self._get_dhcp_pid(["pidof", "dhclient"]) + + def conf_sshd(self, disable_password): + pass diff --git a/setup.py b/setup.py index 17d130867e..a4ce296c65 100755 --- a/setup.py +++ b/setup.py @@ -248,6 +248,12 @@ def get_data_files(name, version, fullname): # pylint: disable=R0912 set_conf_files(data_files, src=["config/photonos/waagent.conf"]) set_systemd_files(data_files, dest=systemd_dir_path, src=["init/photonos/waagent.service"]) + elif name == 'fedora': + set_bin_files(data_files, dest=agent_bin_path) + set_conf_files(data_files) + set_logrotate_files(data_files) + set_udev_files(data_files) + set_systemd_files(data_files, dest=systemd_dir_path) else: # Use default setting set_bin_files(data_files, dest=agent_bin_path) From a82427fc7012b133e5a799984d00e43f53ea2a9f Mon Sep 17 00:00:00 2001 From: maddieford <93676569+maddieford@users.noreply.github.com> Date: Mon, 31 Oct 2022 16:44:19 -0700 Subject: [PATCH 09/63] Report message in handler heartbeat (#2688) * Report message in handler heartbeat * Check heartbeat message exists * Add unit test for heartbeat reporting * Fix unit test syntax error * Speed up test by removing call to sleep * Remove http_put_handler --- azurelinuxagent/ga/exthandlers.py | 2 ++ tests/ga/test_exthandlers.py | 28 +++++++++++++++++++++++++--- 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/azurelinuxagent/ga/exthandlers.py b/azurelinuxagent/ga/exthandlers.py index 974ab19f9b..0aa4ed93d4 100644 --- a/azurelinuxagent/ga/exthandlers.py +++ b/azurelinuxagent/ga/exthandlers.py @@ -1001,6 +1001,8 @@ def report_ext_handler_status(self, vm_status, ext_handler, goal_state_changed): heartbeat = ext_handler_i.collect_heartbeat() if heartbeat is not None: handler_status.status = heartbeat.get('status') + if 'formattedMessage' in heartbeat: + handler_status.message = parse_formatted_message(heartbeat.get('formattedMessage')) except ExtensionError as e: ext_handler_i.set_handler_status(message=ustr(e), code=e.code) diff --git a/tests/ga/test_exthandlers.py b/tests/ga/test_exthandlers.py index 69079cf7f4..67b0771779 100644 --- a/tests/ga/test_exthandlers.py +++ b/tests/ga/test_exthandlers.py @@ -31,9 +31,8 @@ from azurelinuxagent.common.utils.extensionprocessutil import TELEMETRY_MESSAGE_MAX_LEN, format_stdout_stderr, \ read_output from azurelinuxagent.ga.exthandlers import parse_ext_status, ExtHandlerInstance, ExtCommandEnvVariable, \ - ExtensionStatusError, _DEFAULT_SEQ_NO -from tests.protocol import mockwiredata -from tests.protocol.mocks import mock_wire_protocol + ExtensionStatusError, _DEFAULT_SEQ_NO, get_exthandlers_handler, ExtHandlerState +from tests.protocol.mocks import mock_wire_protocol, mockwiredata from tests.tools import AgentTestCase, patch, mock_sleep, clear_singleton_instances @@ -288,6 +287,29 @@ def test_command_extension_log_truncates_correctly(self, mock_log_dir): with open(log_file_path) as truncated_log_file: self.assertEqual(truncated_log_file.read(), "{second_line}\n".format(second_line=second_line)) + def test_it_should_report_the_message_in_the_hearbeat(self): + def heartbeat_with_message(): + return {'code': 0, 'formattedMessage': {'lang': 'en-US', 'message': 'This is a heartbeat message'}, + 'status': 'ready'} + + with mock_wire_protocol(mockwiredata.DATA_FILE) as protocol: + with patch("azurelinuxagent.common.protocol.wire.WireProtocol.report_vm_status", return_value=None): + with patch("azurelinuxagent.ga.exthandlers.ExtHandlerInstance.collect_heartbeat", + side_effect=heartbeat_with_message): + with patch("azurelinuxagent.ga.exthandlers.ExtHandlerInstance.get_handler_state", + return_value=ExtHandlerState.Enabled): + with patch("azurelinuxagent.ga.exthandlers.ExtHandlerInstance.collect_ext_status", + return_value=None): + exthandlers_handler = get_exthandlers_handler(protocol) + exthandlers_handler.run() + vm_status = exthandlers_handler.report_ext_handlers_status() + ext_handler = vm_status.vmAgent.extensionHandlers[0] + self.assertEqual(ext_handler.message, + heartbeat_with_message().get('formattedMessage').get('message'), + "Extension handler messages don't match") + self.assertEqual(ext_handler.status, heartbeat_with_message().get('status'), + "Extension handler statuses don't match") + class LaunchCommandTestCase(AgentTestCase): """ Test cases for launch_command From 3b6c304d7c86da402a3fef27a4319c6ccdb0b1b4 Mon Sep 17 00:00:00 2001 From: Nageswara Nandigam <84482346+nagworld9@users.noreply.github.com> Date: Thu, 3 Nov 2022 13:53:36 -0700 Subject: [PATCH 10/63] drop cgroup support for rhel (#2685) (#2696) (cherry picked from commit 60f5b4dd465b3921ca50ec2e5c9fc4f1987d90b8) --- azurelinuxagent/common/cgroupapi.py | 2 +- tests/common/test_cgroupapi.py | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/azurelinuxagent/common/cgroupapi.py b/azurelinuxagent/common/cgroupapi.py index 2935e2516a..d7040747a0 100644 --- a/azurelinuxagent/common/cgroupapi.py +++ b/azurelinuxagent/common/cgroupapi.py @@ -60,7 +60,7 @@ def cgroups_supported(): except ValueError: return False return ((distro_name.lower() == 'ubuntu' and distro_version.major >= 16) or - (distro_name.lower() in ("centos", "redhat") and + (distro_name.lower() == "centos" and ((distro_version.major == 7 and distro_version.minor >= 4) or distro_version.major >= 8))) @staticmethod diff --git a/tests/common/test_cgroupapi.py b/tests/common/test_cgroupapi.py index 3b70214d39..ca380ed2cb 100644 --- a/tests/common/test_cgroupapi.py +++ b/tests/common/test_cgroupapi.py @@ -57,13 +57,13 @@ def test_cgroups_should_be_supported_only_on_ubuntu16_centos7dot4_redhat7dot4_an (['ubuntu', '20.04', 'focal'], True), (['ubuntu', '20.10', 'groovy'], True), (['centos', '7.8', 'Source'], True), - (['redhat', '7.8', 'Maipo'], True), - (['redhat', '7.9.1908', 'Core'], True), + (['redhat', '7.8', 'Maipo'], False), + (['redhat', '7.9.1908', 'Core'], False), (['centos', '8.1', 'Source'], True), - (['redhat', '8.2', 'Maipo'], True), - (['redhat', '8.2.2111', 'Core'], True), + (['redhat', '8.2', 'Maipo'], False), + (['redhat', '8.2.2111', 'Core'], False), (['centos', '7.4', 'Source'], True), - (['redhat', '7.4', 'Maipo'], True), + (['redhat', '7.4', 'Maipo'], False), (['centos', '7.5', 'Source'], True), (['centos', '7.3', 'Maipo'], False), (['redhat', '7.2', 'Maipo'], False), From b652939cdbea8a123a9b476fe1ec2ef8f81da8ce Mon Sep 17 00:00:00 2001 From: Nageswara Nandigam <84482346+nagworld9@users.noreply.github.com> Date: Thu, 3 Nov 2022 14:09:20 -0700 Subject: [PATCH 11/63] drop cgroup support for centos (#2689) (#2697) (cherry picked from commit 2fa01e53c5de85c831e2cf01ae39113ac643f164) --- azurelinuxagent/common/cgroupapi.py | 4 +--- tests/common/test_cgroupapi.py | 8 ++++---- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/azurelinuxagent/common/cgroupapi.py b/azurelinuxagent/common/cgroupapi.py index d7040747a0..66e893ef6b 100644 --- a/azurelinuxagent/common/cgroupapi.py +++ b/azurelinuxagent/common/cgroupapi.py @@ -59,9 +59,7 @@ def cgroups_supported(): distro_version = FlexibleVersion(distro_info[1]) except ValueError: return False - return ((distro_name.lower() == 'ubuntu' and distro_version.major >= 16) or - (distro_name.lower() == "centos" and - ((distro_version.major == 7 and distro_version.minor >= 4) or distro_version.major >= 8))) + return distro_name.lower() == 'ubuntu' and distro_version.major >= 16 @staticmethod def track_cgroups(extension_cgroups): diff --git a/tests/common/test_cgroupapi.py b/tests/common/test_cgroupapi.py index ca380ed2cb..a31d57d722 100644 --- a/tests/common/test_cgroupapi.py +++ b/tests/common/test_cgroupapi.py @@ -56,15 +56,15 @@ def test_cgroups_should_be_supported_only_on_ubuntu16_centos7dot4_redhat7dot4_an (['ubuntu', '18.10', 'cosmic'], True), (['ubuntu', '20.04', 'focal'], True), (['ubuntu', '20.10', 'groovy'], True), - (['centos', '7.8', 'Source'], True), + (['centos', '7.8', 'Source'], False), (['redhat', '7.8', 'Maipo'], False), (['redhat', '7.9.1908', 'Core'], False), - (['centos', '8.1', 'Source'], True), + (['centos', '8.1', 'Source'], False), (['redhat', '8.2', 'Maipo'], False), (['redhat', '8.2.2111', 'Core'], False), - (['centos', '7.4', 'Source'], True), + (['centos', '7.4', 'Source'], False), (['redhat', '7.4', 'Maipo'], False), - (['centos', '7.5', 'Source'], True), + (['centos', '7.5', 'Source'], False), (['centos', '7.3', 'Maipo'], False), (['redhat', '7.2', 'Maipo'], False), (['bigip', '15.0.1', 'Final'], False), From dd8d0a230be2224e86ce00e37aaab04cab398d44 Mon Sep 17 00:00:00 2001 From: Nageswara Nandigam <84482346+nagworld9@users.noreply.github.com> Date: Thu, 3 Nov 2022 15:11:26 -0700 Subject: [PATCH 12/63] reset the quotas when agent drop the cgroup support (#2693) (#2698) * reset the quotas when agent drop the cgroup support * address comments * log removed file names * remove only agent created files * fix pylint error * fix agent drop in path str (cherry picked from commit 49d4f7cbb38c4ef9e1e6b7450c62cf2b7235b157) --- azurelinuxagent/common/cgroupconfigurator.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/azurelinuxagent/common/cgroupconfigurator.py b/azurelinuxagent/common/cgroupconfigurator.py index 0abee2201a..5d7f2372e9 100644 --- a/azurelinuxagent/common/cgroupconfigurator.py +++ b/azurelinuxagent/common/cgroupconfigurator.py @@ -150,6 +150,25 @@ def initialize(self): try: if self._initialized: return + # This check is to reset the quotas if agent goes from cgroup supported to unsupported distros later in time. + if not CGroupsApi.cgroups_supported(): + agent_drop_in_path = systemd.get_agent_drop_in_path() + try: + if os.path.exists(agent_drop_in_path) and os.path.isdir(agent_drop_in_path): + files_to_cleanup = [] + agent_drop_in_file_slice = os.path.join(agent_drop_in_path, _AGENT_DROP_IN_FILE_SLICE) + agent_drop_in_file_cpu_accounting = os.path.join(agent_drop_in_path, + _DROP_IN_FILE_CPU_ACCOUNTING) + agent_drop_in_file_memory_accounting = os.path.join(agent_drop_in_path, + _DROP_IN_FILE_MEMORY_ACCOUNTING) + agent_drop_in_file_cpu_quota = os.path.join(agent_drop_in_path, _DROP_IN_FILE_CPU_QUOTA) + files_to_cleanup.extend([agent_drop_in_file_slice, agent_drop_in_file_cpu_accounting, + agent_drop_in_file_memory_accounting, agent_drop_in_file_cpu_quota]) + self.__cleanup_all_files(files_to_cleanup) + self.__reload_systemd_config() + logger.info("Agent reset the quotas if distro: {0} goes from supported to unsupported list", get_distro()) + except Exception as err: + logger.warn("Unable to delete Agent drop-in files while resetting the quotas: {0}".format(err)) # check whether cgroup monitoring is supported on the current distro self._cgroups_supported = CGroupsApi.cgroups_supported() From 97e4d39e145f4d9721f99d4f636c5bbed0493d1c Mon Sep 17 00:00:00 2001 From: Norberto Arrieta Date: Mon, 7 Nov 2022 15:52:38 -0800 Subject: [PATCH 13/63] update connection name (#2701) Co-authored-by: narrieta --- dcr/templates/vars.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dcr/templates/vars.yml b/dcr/templates/vars.yml index 6b41ac6a1e..4efb0c9a58 100644 --- a/dcr/templates/vars.yml +++ b/dcr/templates/vars.yml @@ -7,7 +7,7 @@ variables: adminUsername: 'dcr' # Public Cloud Data - azureConnection: 'AzLinux DCR Public (8e037ad4-618f-4466-8bc8-5099d41ac15b)' + azureConnection: 'azuremanagement' subId: '8e037ad4-618f-4466-8bc8-5099d41ac15b' location: 'East US 2' From a07e48af88964331340cf7be52a61c2cbfec1660 Mon Sep 17 00:00:00 2001 From: Norberto Arrieta Date: Wed, 23 Nov 2022 11:35:21 -0800 Subject: [PATCH 14/63] Run tests using a container (#2704) * Run tests within a container * cleanup Co-authored-by: narrieta --- tests_e2e/azure-pipelines.yml | 9 +-- tests_e2e/docker/Dockerfile | 73 +++++++++++++++++++ tests_e2e/requirements.txt | 1 - tests_e2e/scenario_utils/azure_models.py | 4 +- .../extensions/BaseExtensionTestClass.py | 6 +- .../extensions/CustomScriptExtension.py | 4 +- tests_e2e/scenario_utils/logging_utils.py | 33 +++++++++ tests_e2e/scenario_utils/models.py | 4 +- tests_e2e/scripts/__init__.py | 0 tests_e2e/scripts/execute_tests.sh | 26 ++++--- tests_e2e/scripts/execute_tests_container.sh | 27 +++++++ tests_e2e/scripts/install_dependencies.sh | 43 ----------- tests_e2e/templates/execute-tests.yml | 12 ++- 13 files changed, 163 insertions(+), 79 deletions(-) create mode 100644 tests_e2e/docker/Dockerfile create mode 100644 tests_e2e/scenario_utils/logging_utils.py delete mode 100644 tests_e2e/scripts/__init__.py create mode 100755 tests_e2e/scripts/execute_tests_container.sh delete mode 100755 tests_e2e/scripts/install_dependencies.sh diff --git a/tests_e2e/azure-pipelines.yml b/tests_e2e/azure-pipelines.yml index 689ad868be..982477d82c 100644 --- a/tests_e2e/azure-pipelines.yml +++ b/tests_e2e/azure-pipelines.yml @@ -1,10 +1,6 @@ variables: - name: azureConnection - value: 'AzLinux DCR Public (8e037ad4-618f-4466-8bc8-5099d41ac15b)' - - name: subId - value: '8e037ad4-618f-4466-8bc8-5099d41ac15b' - - name: testsSourcesDirectory - value: "$(Build.SourcesDirectory)/tests_e2e" + value: 'azuremanagement' trigger: - develop @@ -15,7 +11,8 @@ pool: vmImage: ubuntu-latest stages: - - stage: "Execute" + - stage: "ExecuteTests" + jobs: - template: 'templates/execute-tests.yml' diff --git a/tests_e2e/docker/Dockerfile b/tests_e2e/docker/Dockerfile new file mode 100644 index 0000000000..5105b0df1a --- /dev/null +++ b/tests_e2e/docker/Dockerfile @@ -0,0 +1,73 @@ +# +# * Sample command to build the image: +# +# docker build -t waagenttests . +# +# * Sample command to execute a container interactively: +# +# docker run --rm -it -v /home/nam/src/WALinuxAgent:/home/waagent/WALinuxAgent waagenttests bash --login +# +FROM ubuntu:latest +LABEL description="Test environment for WALinuxAgent" + +SHELL ["/bin/bash", "-c"] + +# +# Install the required packages as root +# +USER root + +RUN \ + apt-get update && \ + \ + # \ + # Install basic dependencies \ + # \ + apt-get install -y git python3.10 python3.10-dev curl && \ + ln /usr/bin/python3.10 /usr/bin/python3 && \ + \ + # \ + # Install LISA dependencies \ + # \ + apt-get install -y git gcc libgirepository1.0-dev libcairo2-dev qemu-utils libvirt-dev python3-venv && \ + \ + # \ + # Create user waagent, which is used to execute the tests \ + # \ + groupadd waagent && \ + useradd --shell /bin/bash --create-home -g waagent waagent && \ + : + +# +# Do the Poetry and LISA setup as waagent +# +USER waagent + +RUN \ + # \ + # Install Poetry \ + # \ + curl -sSL https://raw.githubusercontent.com/python-poetry/poetry/master/install-poetry.py | python3 - && \ + export PATH="$HOME/.local/bin:$PATH" && \ + \ + # \ + # Install LISA \ + # \ + cd $HOME && \ + git clone https://github.com/microsoft/lisa.git && \ + cd lisa && \ + poetry config virtualenvs.in-project true && \ + make setup && \ + \ + # \ + # Install additional test dependencies \ + # \ + poetry add msrestazure && \ + \ + # \ + # The setup for the tests depends on a couple of paths; add those to the profile \ + # \ + echo 'export PYTHONPATH="$HOME/WALinuxAgent"' >> $HOME/.bash_profile && \ + echo 'export PATH="$HOME/.local/bin:$PATH"' >> $HOME/.bash_profile && \ + : + diff --git a/tests_e2e/requirements.txt b/tests_e2e/requirements.txt index 63bd6d2c73..fa74f1bfeb 100644 --- a/tests_e2e/requirements.txt +++ b/tests_e2e/requirements.txt @@ -10,4 +10,3 @@ cryptography distro junitparser msrestazure -python-dotenv \ No newline at end of file diff --git a/tests_e2e/scenario_utils/azure_models.py b/tests_e2e/scenario_utils/azure_models.py index 4422e3d608..3fd237ea88 100644 --- a/tests_e2e/scenario_utils/azure_models.py +++ b/tests_e2e/scenario_utils/azure_models.py @@ -12,8 +12,8 @@ from azure.mgmt.resource import ResourceManagementClient from msrestazure.azure_exceptions import CloudError -from dcr.scenario_utils.logging_utils import LoggingHandler -from dcr.scenario_utils.models import get_vm_data_from_env, VMModelType, VMMetaData +from tests_e2e.scenario_utils.logging_utils import LoggingHandler +from tests_e2e.scenario_utils.models import get_vm_data_from_env, VMModelType, VMMetaData class AzureComputeBaseClass(ABC, LoggingHandler): diff --git a/tests_e2e/scenario_utils/extensions/BaseExtensionTestClass.py b/tests_e2e/scenario_utils/extensions/BaseExtensionTestClass.py index 8c23e1e712..1e44e8a1c9 100644 --- a/tests_e2e/scenario_utils/extensions/BaseExtensionTestClass.py +++ b/tests_e2e/scenario_utils/extensions/BaseExtensionTestClass.py @@ -3,9 +3,9 @@ from azure.core.polling import LROPoller -from dcr.scenario_utils.azure_models import ComputeManager -from dcr.scenario_utils.logging_utils import LoggingHandler -from dcr.scenario_utils.models import ExtensionMetaData, get_vm_data_from_env +from tests_e2e.scenario_utils.azure_models import ComputeManager +from tests_e2e.scenario_utils.logging_utils import LoggingHandler +from tests_e2e.scenario_utils.models import ExtensionMetaData, get_vm_data_from_env class BaseExtensionTestClass(LoggingHandler): diff --git a/tests_e2e/scenario_utils/extensions/CustomScriptExtension.py b/tests_e2e/scenario_utils/extensions/CustomScriptExtension.py index 29df351134..0e339093d9 100644 --- a/tests_e2e/scenario_utils/extensions/CustomScriptExtension.py +++ b/tests_e2e/scenario_utils/extensions/CustomScriptExtension.py @@ -1,7 +1,7 @@ import uuid -from dcr.scenario_utils.extensions.BaseExtensionTestClass import BaseExtensionTestClass -from dcr.scenario_utils.models import ExtensionMetaData +from tests_e2e.scenario_utils.extensions.BaseExtensionTestClass import BaseExtensionTestClass +from tests_e2e.scenario_utils.models import ExtensionMetaData class CustomScriptExtension(BaseExtensionTestClass): diff --git a/tests_e2e/scenario_utils/logging_utils.py b/tests_e2e/scenario_utils/logging_utils.py new file mode 100644 index 0000000000..462f6a957a --- /dev/null +++ b/tests_e2e/scenario_utils/logging_utils.py @@ -0,0 +1,33 @@ +# Create a base class +import logging + + +def get_logger(name): + return LoggingHandler(name).log + + +class LoggingHandler: + """ + Base class for Logging + """ + def __init__(self, name=None): + self.log = self.__setup_and_get_logger(name) + + def __setup_and_get_logger(self, name): + logger = logging.getLogger(name if name is not None else self.__class__.__name__) + if logger.hasHandlers(): + # Logging module inherits from base loggers if already setup, if a base logger found, reuse that + return logger + + # No handlers found for logger, set it up + # This logging format is easier to read on the DevOps UI - + # https://docs.microsoft.com/en-us/azure/devops/pipelines/scripts/logging-commands?view=azure-devops&tabs=bash#formatting-commands + log_formatter = logging.Formatter("##[%(levelname)s] [%(asctime)s] [%(module)s] {%(pathname)s:%(lineno)d} %(message)s", + datefmt="%Y-%m-%dT%H:%M:%S%z") + console_handler = logging.StreamHandler() + console_handler.setFormatter(log_formatter) + logger.addHandler(console_handler) + logger.setLevel(logging.INFO) + + return logger + diff --git a/tests_e2e/scenario_utils/models.py b/tests_e2e/scenario_utils/models.py index 806c830c12..a9e3e8cf01 100644 --- a/tests_e2e/scenario_utils/models.py +++ b/tests_e2e/scenario_utils/models.py @@ -2,8 +2,6 @@ from enum import Enum, auto from typing import List -from dotenv import load_dotenv - class VMModelType(Enum): VM = auto() @@ -123,13 +121,13 @@ def _get_ips(username) -> (list, list): def get_vm_data_from_env() -> VMMetaData: if get_vm_data_from_env.__instance is None: - load_dotenv() get_vm_data_from_env.__instance = VMMetaData(vm_name=os.environ["VMNAME"], rg_name=os.environ['RGNAME'], sub_id=os.environ["SUBID"], location=os.environ['LOCATION'], admin_username=os.environ['ADMINUSERNAME']) + return get_vm_data_from_env.__instance diff --git a/tests_e2e/scripts/__init__.py b/tests_e2e/scripts/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests_e2e/scripts/execute_tests.sh b/tests_e2e/scripts/execute_tests.sh index 47e846ec9a..ec2ffcc578 100755 --- a/tests_e2e/scripts/execute_tests.sh +++ b/tests_e2e/scripts/execute_tests.sh @@ -2,17 +2,19 @@ set -euxo pipefail -export PYTHONPATH=$BUILD_SOURCESDIRECTORY +# The private ssh key is shared from the container host as $HOME/id_rsa; copy it to +# HOME/.ssh, set the correct mode and generate the public key. +mkdir "$HOME/.ssh" +cp "$HOME/id_rsa" "$HOME/.ssh" +chmod 700 "$HOME/.ssh/id_rsa" +ssh-keygen -y -f "$HOME/.ssh/id_rsa" > "$HOME/.ssh/id_rsa.pub" -cd $BUILD_SOURCESDIRECTORY/lisa - -# LISA needs both the public and private keys; generate the former -chmod 700 $SSHKEY_SECUREFILEPATH -ssh-keygen -y -f $SSHKEY_SECUREFILEPATH > "$SSHKEY_SECUREFILEPATH".pub - -./lisa.sh --runbook $BUILD_SOURCESDIRECTORY/tests_e2e/lisa/runbook/azure.yml \ - --log_path $HOME/tmp \ - --working_path $HOME/tmp \ - -v subscription_id:$SUBID \ - -v identity_file:$SSHKEY_SECUREFILEPATH +# Execute the tests, this needs to be done from the LISA root directory +cd "$HOME/lisa" +./lisa.sh \ + --runbook "$HOME/WALinuxAgent/tests_e2e/lisa/runbook/azure.yml" \ + --log_path "$HOME/logs" \ + --working_path "$HOME/logs" \ + -v subscription_id:"$SUBSCRIPTION_ID" \ + -v identity_file:"$HOME/.ssh/id_rsa.pub" diff --git a/tests_e2e/scripts/execute_tests_container.sh b/tests_e2e/scripts/execute_tests_container.sh new file mode 100755 index 0000000000..0b0b1c2e9c --- /dev/null +++ b/tests_e2e/scripts/execute_tests_container.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env bash + +set -euxo pipefail + +az login --service-principal --username "$AZURE_CLIENT_ID" --password "$AZURE_CLIENT_SECRET" --tenant "$AZURE_TENANT_ID" > /dev/null + +az acr login --name waagenttests + +docker pull waagenttests.azurecr.io/waagenttests:latest + +# Logs will be placed in this location. Make waagent (UID 1000 in the container) the owner. +mkdir "$HOME/logs" +sudo chown 1000 "$HOME/logs" + +docker run --rm \ + --volume "$BUILD_SOURCESDIRECTORY:/home/waagent/WALinuxAgent" \ + --volume "$DOWNLOADSSHKEY_SECUREFILEPATH:/home/waagent/id_rsa" \ + --volume "$HOME/logs:/home/waagent/logs" \ + --env SUBSCRIPTION_ID \ + --env AZURE_CLIENT_ID \ + --env AZURE_CLIENT_SECRET \ + --env AZURE_TENANT_ID \ + waagenttests.azurecr.io/waagenttests \ + bash --login -c '~/WALinuxAgent/tests_e2e/scripts/execute_tests.sh' + +ls -lR "$HOME/logs" + diff --git a/tests_e2e/scripts/install_dependencies.sh b/tests_e2e/scripts/install_dependencies.sh deleted file mode 100755 index 43cbf68e1c..0000000000 --- a/tests_e2e/scripts/install_dependencies.sh +++ /dev/null @@ -1,43 +0,0 @@ -#!/usr/bin/env bash - -set -euxo pipefail - -# -# Install LISA (see https://mslisa.readthedocs.io/en/main/installation_linux.html) -# - -# Install LISA dependencies -sudo apt install -y git gcc libgirepository1.0-dev libcairo2-dev qemu-utils libvirt-dev python3-venv - -# Install Poetry -curl -sSL https://raw.githubusercontent.com/python-poetry/poetry/master/install-poetry.py | python3 - -export PATH="$HOME/.local/bin:$PATH" -echo "##vso[task.prependpath]$HOME/.local/bin" - -# Install LISA -cd $BUILD_SOURCESDIRECTORY -git clone https://github.com/microsoft/lisa.git -cd lisa -make setup - -# Verify LISA installation -./lisa.sh - -# -# Install test dependencies -# -# NOTE: Need to review the test dependencies, they require module versions greater than the same modules used by LISA -# and the version update adds a significant build delay. For the moment, just add the modules not included -# already by LISA -# -# ===== DISABLED ===== -## (make a copy of the requirements file removing comments since poetry-add-requirements does not support them) -## -#pip install poetry-add-requirements.txt -#sed '/^#/d' $BUILD_SOURCESDIRECTORY/tests_e2e/requirements.txt > WALinuxAgent-requirements.txt -#poetry-add-requirements.txt WALinuxAgent-requirements.txt -# ===== END DISABLED ===== - -poetry add msrestazure -poetry add python-dotenv - diff --git a/tests_e2e/templates/execute-tests.yml b/tests_e2e/templates/execute-tests.yml index e4abdc8b0d..0e9a4cb1fd 100644 --- a/tests_e2e/templates/execute-tests.yml +++ b/tests_e2e/templates/execute-tests.yml @@ -1,10 +1,10 @@ jobs: - - job: "" + - job: "ExecuteTests" steps: - task: DownloadSecureFile@1 - name: sshKey + name: downloadSshKey displayName: "Download SSH key" inputs: secureFile: 'id_rsa' @@ -24,13 +24,11 @@ jobs: addToPath: true architecture: 'x64' - - bash: $(testsSourcesDirectory)/scripts/install_dependencies.sh - displayName: "Install Dependencies" - - - bash: $(testsSourcesDirectory)/scripts/execute_tests.sh + - bash: $(Build.SourcesDirectory)/tests_e2e/scripts/execute_tests_container.sh displayName: "Execute tests" env: # Add all KeyVault secrets explicitly as they're not added by default to the environment vars AZURE_CLIENT_ID: $(AZURE-CLIENT-ID) AZURE_CLIENT_SECRET: $(AZURE-CLIENT-SECRET) - AZURE_TENANT_ID: $(AZURE-TENANT-ID) \ No newline at end of file + AZURE_TENANT_ID: $(AZURE-TENANT-ID) + SUBSCRIPTION_ID: $(SUBSCRIPTION-ID) From 6872beff7e9fe8272a78a379cbc29e6705796d80 Mon Sep 17 00:00:00 2001 From: Norberto Arrieta Date: Thu, 1 Dec 2022 13:06:50 -0800 Subject: [PATCH 15/63] Publish test results and logs (#2707) Co-authored-by: narrieta --- tests_e2e/lisa/runbook/azure.yml | 1 + tests_e2e/lisa/testsuites/agent-bvt.py | 35 +++++------- tests_e2e/lisa/testsuites/agent_test_suite.py | 53 +++++++++++++++++++ tests_e2e/scripts/collect_logs.sh | 17 ++++++ tests_e2e/scripts/execute_tests_container.sh | 30 ++++++++--- tests_e2e/templates/execute-tests.yml | 11 ++++ 6 files changed, 118 insertions(+), 29 deletions(-) create mode 100644 tests_e2e/lisa/testsuites/agent_test_suite.py create mode 100755 tests_e2e/scripts/collect_logs.sh diff --git a/tests_e2e/lisa/runbook/azure.yml b/tests_e2e/lisa/runbook/azure.yml index 8f0ef40133..27d9d5bb1c 100644 --- a/tests_e2e/lisa/runbook/azure.yml +++ b/tests_e2e/lisa/runbook/azure.yml @@ -46,6 +46,7 @@ variable: notifier: - type: html - type: env_stats + - type: junit platform: - type: azure admin_username: $(user) diff --git a/tests_e2e/lisa/testsuites/agent-bvt.py b/tests_e2e/lisa/testsuites/agent-bvt.py index 7b3fa53d1a..b7151bd08b 100644 --- a/tests_e2e/lisa/testsuites/agent-bvt.py +++ b/tests_e2e/lisa/testsuites/agent-bvt.py @@ -1,17 +1,13 @@ from assertpy import assert_that -from pathlib import Path + +from tests_e2e.lisa.testsuites.agent_test_suite import AgentTestSuite from tests_e2e.lisa.tests.agent_bvt import custom_script from lisa import ( - CustomScriptBuilder, - Logger, - Node, simple_requirement, TestCaseMetadata, - TestSuite, TestSuiteMetadata, ) -from lisa.sut_orchestrator.azure.common import get_node_context @TestSuiteMetadata( @@ -22,20 +18,17 @@ """, requirement=simple_requirement(unsupported_os=[]), ) -class AgentBvt(TestSuite): +class AgentBvt(AgentTestSuite): @TestCaseMetadata(description="", priority=0) - def check_agent_version(self, node: Node, log: Logger) -> None: - script_path = CustomScriptBuilder(Path(__file__).parent.parent.joinpath("tests", "agent_bvt"), ["check_agent_version.py"]) - script = node.tools[script_path] - result = script.run() - log.info(result.stdout) - log.error(result.stderr) - assert_that(result.exit_code).is_equal_to(0) + def main(self, *_, **__) -> None: + self.check_agent_version() + self.custom_script() + + def check_agent_version(self) -> None: + exit_code = self._execute_remote_script(self._test_root.joinpath("lisa", "tests", "agent_bvt"), "check_agent_version.py") + assert_that(exit_code).is_equal_to(0) + + def custom_script(self) -> None: + custom_script.main(self._subscription_id, self._resource_group_name, self._vm_name) + - @TestCaseMetadata(description="", priority=0) - def custom_script(self, node: Node) -> None: - node_context = get_node_context(node) - subscription_id = node.features._platform.subscription_id - resource_group_name = node_context.resource_group_name - vm_name = node_context.vm_name - custom_script.main(subscription_id, resource_group_name, vm_name) diff --git a/tests_e2e/lisa/testsuites/agent_test_suite.py b/tests_e2e/lisa/testsuites/agent_test_suite.py new file mode 100644 index 0000000000..5644473831 --- /dev/null +++ b/tests_e2e/lisa/testsuites/agent_test_suite.py @@ -0,0 +1,53 @@ +from pathlib import Path, PurePath + +from lisa import ( + CustomScriptBuilder, + TestSuite, + TestSuiteMetadata, +) +from lisa.sut_orchestrator.azure.common import get_node_context + + +class AgentTestSuite(TestSuite): + def __init__(self, metadata: TestSuiteMetadata): + super().__init__(metadata) + self._log = None + self._node = None + self._test_root = None + self._subscription_id = None + self._resource_group_name = None + self._vm_name = None + + def before_case(self, *_, **kwargs) -> None: + node = kwargs['node'] + log = kwargs['log'] + node_context = get_node_context(node) + + self._log = log + self._node = node + self._test_root = Path(__file__).parent.parent.parent + self._subscription_id = node.features._platform.subscription_id + self._resource_group_name = node_context.resource_group_name + self._vm_name = node_context.vm_name + + def after_case(self, *_, **__) -> None: + # Collect the logs on the test machine into a compressed tarball + self._log.info("Collecting logs on test machine [%s]...", self._node.name) + self._execute_remote_script(self._test_root.joinpath("scripts"), "collect_logs.sh") + + # Copy the tarball to the local logs directory + remote_path = PurePath('/home') / self._node.connection_info['username'] / 'logs.tgz' + local_path = Path.home() / 'logs' / 'vm-logs-{0}.tgz'.format(self._node.name) + self._log.info("Copying %s:%s to %s...", self._node.name, remote_path, local_path) + self._node.shell.copy_back(remote_path, local_path) + + def _execute_remote_script(self, path: Path, script: str) -> int: + custom_script_builder = CustomScriptBuilder(path, [script]) + custom_script = self._node.tools[custom_script_builder] + self._log.info('Executing %s/%s...', path, script) + result = custom_script.run() + if result.stdout: + self._log.info('%s', result.stdout) + if result.stderr: + self._log.error('%s', result.stderr) + return result.exit_code diff --git a/tests_e2e/scripts/collect_logs.sh b/tests_e2e/scripts/collect_logs.sh new file mode 100755 index 0000000000..b557215d1c --- /dev/null +++ b/tests_e2e/scripts/collect_logs.sh @@ -0,0 +1,17 @@ +#!/usr/bin/env bash + +set -euxo pipefail + +logs_file_name="$HOME/logs.tgz" + +echo "Collecting logs to $logs_file_name ..." + +sudo tar --exclude='journal/*' --exclude='omsbundle' --exclude='omsagent' --exclude='mdsd' --exclude='scx*' \ + --exclude='*.so' --exclude='*__LinuxDiagnostic__*' --exclude='*.zip' --exclude='*.deb' --exclude='*.rpm' \ + -czf "$logs_file_name" \ + /var/log \ + /var/lib/waagent/ \ + /etc/waagent.conf + +sudo chmod +r "$logs_file_name" + diff --git a/tests_e2e/scripts/execute_tests_container.sh b/tests_e2e/scripts/execute_tests_container.sh index 0b0b1c2e9c..39424272a8 100755 --- a/tests_e2e/scripts/execute_tests_container.sh +++ b/tests_e2e/scripts/execute_tests_container.sh @@ -8,20 +8,34 @@ az acr login --name waagenttests docker pull waagenttests.azurecr.io/waagenttests:latest -# Logs will be placed in this location. Make waagent (UID 1000 in the container) the owner. -mkdir "$HOME/logs" -sudo chown 1000 "$HOME/logs" +# Logs will be placed in the staging directory. Make waagent (UID 1000 in the container) the owner so that it can write to that location +sudo chown 1000 "$BUILD_ARTIFACTSTAGINGDIRECTORY" docker run --rm \ --volume "$BUILD_SOURCESDIRECTORY:/home/waagent/WALinuxAgent" \ --volume "$DOWNLOADSSHKEY_SECUREFILEPATH:/home/waagent/id_rsa" \ - --volume "$HOME/logs:/home/waagent/logs" \ + --volume "$BUILD_ARTIFACTSTAGINGDIRECTORY:/home/waagent/logs" \ --env SUBSCRIPTION_ID \ --env AZURE_CLIENT_ID \ --env AZURE_CLIENT_SECRET \ --env AZURE_TENANT_ID \ waagenttests.azurecr.io/waagenttests \ - bash --login -c '~/WALinuxAgent/tests_e2e/scripts/execute_tests.sh' - -ls -lR "$HOME/logs" - + bash --login -c '$HOME/WALinuxAgent/tests_e2e/scripts/execute_tests.sh' + +# Retake ownership of the staging directory +sudo find "$BUILD_ARTIFACTSTAGINGDIRECTORY" -exec chown "$USER" {} \; + +# LISA organizes its logs in a tree similar to +# +# .../20221130 +# .../20221130/20221130-160013-749 +# .../20221130/20221130-160013-749/environments +# .../20221130/20221130-160013-749/lisa-20221130-160013-749.log +# .../20221130/20221130-160013-749/lisa.junit.xml +# etc +# +# Remove the first 2 levels of the tree (which indicate the time of the test run) to make navigation +# in the Azure Pipelines UI easier. +# +mv "$BUILD_ARTIFACTSTAGINGDIRECTORY"/[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]/*/* "$BUILD_ARTIFACTSTAGINGDIRECTORY" +rm -r "$BUILD_ARTIFACTSTAGINGDIRECTORY"/[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9] diff --git a/tests_e2e/templates/execute-tests.yml b/tests_e2e/templates/execute-tests.yml index 0e9a4cb1fd..71dd6c50c1 100644 --- a/tests_e2e/templates/execute-tests.yml +++ b/tests_e2e/templates/execute-tests.yml @@ -32,3 +32,14 @@ jobs: AZURE_CLIENT_SECRET: $(AZURE-CLIENT-SECRET) AZURE_TENANT_ID: $(AZURE-TENANT-ID) SUBSCRIPTION_ID: $(SUBSCRIPTION-ID) + + - task: PublishTestResults@2 + inputs: + testResultsFormat: 'JUnit' + testResultsFiles: '**/*junit.xml' + searchFolder: $(Build.ArtifactStagingDirectory) + testRunTitle: 'Publish test results' + + - publish: $(Build.ArtifactStagingDirectory) + artifact: 'test-logs' + displayName: 'Publish test logs' From efc39df80b2ef0be8f7e9e99896a4bb642467638 Mon Sep 17 00:00:00 2001 From: Norberto Arrieta Date: Wed, 7 Dec 2022 06:46:39 -0800 Subject: [PATCH 16/63] Reorganize directory structure of end-to-end tests (#2708) * Reorganize directory structure of end-to-end tests * change path Co-authored-by: narrieta --- tests_e2e/{azure-pipelines.yml => pipeline/pipeline.yml} | 0 .../scripts/execute_tests.sh} | 2 +- tests_e2e/{ => pipeline}/templates/execute-tests.yml | 2 +- tests_e2e/scenario_utils/__init__.py | 0 tests_e2e/scenario_utils/extensions/__init__.py | 0 .../modules}/BaseExtensionTestClass.py | 6 +++--- .../modules}/CustomScriptExtension.py | 4 ++-- tests_e2e/{ => scenarios/modules}/__init__.py | 0 .../{scenario_utils => scenarios/modules}/azure_models.py | 4 ++-- .../{scenario_utils => scenarios/modules}/logging_utils.py | 0 tests_e2e/{scenario_utils => scenarios/modules}/models.py | 0 .../runbook/azure.yml => scenarios/runbooks/daily.yml} | 0 tests_e2e/{ => scenarios}/scripts/collect_logs.sh | 0 .../execute_tests.sh => scenarios/scripts/run_scenarios.sh} | 2 +- tests_e2e/{lisa => scenarios}/tests/__init__.py | 0 .../tests/agent_bvt => scenarios/tests/bvts}/__init__.py | 0 .../agent_bvt => scenarios/tests/bvts}/custom_script.py | 2 +- .../agent_bvt => scenarios/tests}/check_agent_version.py | 0 .../agent-bvt.py => scenarios/testsuites/agent_bvt.py} | 6 +++--- .../{lisa => scenarios}/testsuites/agent_test_suite.py | 2 +- 20 files changed, 15 insertions(+), 15 deletions(-) rename tests_e2e/{azure-pipelines.yml => pipeline/pipeline.yml} (100%) rename tests_e2e/{scripts/execute_tests_container.sh => pipeline/scripts/execute_tests.sh} (94%) rename tests_e2e/{ => pipeline}/templates/execute-tests.yml (93%) delete mode 100644 tests_e2e/scenario_utils/__init__.py delete mode 100644 tests_e2e/scenario_utils/extensions/__init__.py rename tests_e2e/{scenario_utils/extensions => scenarios/modules}/BaseExtensionTestClass.py (95%) rename tests_e2e/{scenario_utils/extensions => scenarios/modules}/CustomScriptExtension.py (82%) rename tests_e2e/{ => scenarios/modules}/__init__.py (100%) rename tests_e2e/{scenario_utils => scenarios/modules}/azure_models.py (98%) rename tests_e2e/{scenario_utils => scenarios/modules}/logging_utils.py (100%) rename tests_e2e/{scenario_utils => scenarios/modules}/models.py (100%) rename tests_e2e/{lisa/runbook/azure.yml => scenarios/runbooks/daily.yml} (100%) rename tests_e2e/{ => scenarios}/scripts/collect_logs.sh (100%) rename tests_e2e/{scripts/execute_tests.sh => scenarios/scripts/run_scenarios.sh} (88%) rename tests_e2e/{lisa => scenarios}/tests/__init__.py (100%) rename tests_e2e/{lisa/tests/agent_bvt => scenarios/tests/bvts}/__init__.py (100%) rename tests_e2e/{lisa/tests/agent_bvt => scenarios/tests/bvts}/custom_script.py (92%) rename tests_e2e/{lisa/tests/agent_bvt => scenarios/tests}/check_agent_version.py (100%) rename tests_e2e/{lisa/testsuites/agent-bvt.py => scenarios/testsuites/agent_bvt.py} (80%) rename tests_e2e/{lisa => scenarios}/testsuites/agent_test_suite.py (98%) diff --git a/tests_e2e/azure-pipelines.yml b/tests_e2e/pipeline/pipeline.yml similarity index 100% rename from tests_e2e/azure-pipelines.yml rename to tests_e2e/pipeline/pipeline.yml diff --git a/tests_e2e/scripts/execute_tests_container.sh b/tests_e2e/pipeline/scripts/execute_tests.sh similarity index 94% rename from tests_e2e/scripts/execute_tests_container.sh rename to tests_e2e/pipeline/scripts/execute_tests.sh index 39424272a8..2f177faa73 100755 --- a/tests_e2e/scripts/execute_tests_container.sh +++ b/tests_e2e/pipeline/scripts/execute_tests.sh @@ -20,7 +20,7 @@ docker run --rm \ --env AZURE_CLIENT_SECRET \ --env AZURE_TENANT_ID \ waagenttests.azurecr.io/waagenttests \ - bash --login -c '$HOME/WALinuxAgent/tests_e2e/scripts/execute_tests.sh' + bash --login -c '$HOME/WALinuxAgent/tests_e2e/scenarios/scripts/run_scenarios.sh' # Retake ownership of the staging directory sudo find "$BUILD_ARTIFACTSTAGINGDIRECTORY" -exec chown "$USER" {} \; diff --git a/tests_e2e/templates/execute-tests.yml b/tests_e2e/pipeline/templates/execute-tests.yml similarity index 93% rename from tests_e2e/templates/execute-tests.yml rename to tests_e2e/pipeline/templates/execute-tests.yml index 71dd6c50c1..cb7acea244 100644 --- a/tests_e2e/templates/execute-tests.yml +++ b/tests_e2e/pipeline/templates/execute-tests.yml @@ -24,7 +24,7 @@ jobs: addToPath: true architecture: 'x64' - - bash: $(Build.SourcesDirectory)/tests_e2e/scripts/execute_tests_container.sh + - bash: $(Build.SourcesDirectory)/tests_e2e/pipeline/scripts/execute_tests.sh displayName: "Execute tests" env: # Add all KeyVault secrets explicitly as they're not added by default to the environment vars diff --git a/tests_e2e/scenario_utils/__init__.py b/tests_e2e/scenario_utils/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests_e2e/scenario_utils/extensions/__init__.py b/tests_e2e/scenario_utils/extensions/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests_e2e/scenario_utils/extensions/BaseExtensionTestClass.py b/tests_e2e/scenarios/modules/BaseExtensionTestClass.py similarity index 95% rename from tests_e2e/scenario_utils/extensions/BaseExtensionTestClass.py rename to tests_e2e/scenarios/modules/BaseExtensionTestClass.py index 1e44e8a1c9..bc00f15c1d 100644 --- a/tests_e2e/scenario_utils/extensions/BaseExtensionTestClass.py +++ b/tests_e2e/scenarios/modules/BaseExtensionTestClass.py @@ -3,9 +3,9 @@ from azure.core.polling import LROPoller -from tests_e2e.scenario_utils.azure_models import ComputeManager -from tests_e2e.scenario_utils.logging_utils import LoggingHandler -from tests_e2e.scenario_utils.models import ExtensionMetaData, get_vm_data_from_env +from tests_e2e.scenarios.modules.azure_models import ComputeManager +from tests_e2e.scenarios.modules.logging_utils import LoggingHandler +from tests_e2e.scenarios.modules.models import ExtensionMetaData, get_vm_data_from_env class BaseExtensionTestClass(LoggingHandler): diff --git a/tests_e2e/scenario_utils/extensions/CustomScriptExtension.py b/tests_e2e/scenarios/modules/CustomScriptExtension.py similarity index 82% rename from tests_e2e/scenario_utils/extensions/CustomScriptExtension.py rename to tests_e2e/scenarios/modules/CustomScriptExtension.py index 0e339093d9..7c67052ef6 100644 --- a/tests_e2e/scenario_utils/extensions/CustomScriptExtension.py +++ b/tests_e2e/scenarios/modules/CustomScriptExtension.py @@ -1,7 +1,7 @@ import uuid -from tests_e2e.scenario_utils.extensions.BaseExtensionTestClass import BaseExtensionTestClass -from tests_e2e.scenario_utils.models import ExtensionMetaData +from tests_e2e.scenarios.modules.BaseExtensionTestClass import BaseExtensionTestClass +from tests_e2e.scenarios.modules.models import ExtensionMetaData class CustomScriptExtension(BaseExtensionTestClass): diff --git a/tests_e2e/__init__.py b/tests_e2e/scenarios/modules/__init__.py similarity index 100% rename from tests_e2e/__init__.py rename to tests_e2e/scenarios/modules/__init__.py diff --git a/tests_e2e/scenario_utils/azure_models.py b/tests_e2e/scenarios/modules/azure_models.py similarity index 98% rename from tests_e2e/scenario_utils/azure_models.py rename to tests_e2e/scenarios/modules/azure_models.py index 3fd237ea88..99875d39c4 100644 --- a/tests_e2e/scenario_utils/azure_models.py +++ b/tests_e2e/scenarios/modules/azure_models.py @@ -12,8 +12,8 @@ from azure.mgmt.resource import ResourceManagementClient from msrestazure.azure_exceptions import CloudError -from tests_e2e.scenario_utils.logging_utils import LoggingHandler -from tests_e2e.scenario_utils.models import get_vm_data_from_env, VMModelType, VMMetaData +from tests_e2e.scenarios.modules.logging_utils import LoggingHandler +from tests_e2e.scenarios.modules.models import get_vm_data_from_env, VMModelType, VMMetaData class AzureComputeBaseClass(ABC, LoggingHandler): diff --git a/tests_e2e/scenario_utils/logging_utils.py b/tests_e2e/scenarios/modules/logging_utils.py similarity index 100% rename from tests_e2e/scenario_utils/logging_utils.py rename to tests_e2e/scenarios/modules/logging_utils.py diff --git a/tests_e2e/scenario_utils/models.py b/tests_e2e/scenarios/modules/models.py similarity index 100% rename from tests_e2e/scenario_utils/models.py rename to tests_e2e/scenarios/modules/models.py diff --git a/tests_e2e/lisa/runbook/azure.yml b/tests_e2e/scenarios/runbooks/daily.yml similarity index 100% rename from tests_e2e/lisa/runbook/azure.yml rename to tests_e2e/scenarios/runbooks/daily.yml diff --git a/tests_e2e/scripts/collect_logs.sh b/tests_e2e/scenarios/scripts/collect_logs.sh similarity index 100% rename from tests_e2e/scripts/collect_logs.sh rename to tests_e2e/scenarios/scripts/collect_logs.sh diff --git a/tests_e2e/scripts/execute_tests.sh b/tests_e2e/scenarios/scripts/run_scenarios.sh similarity index 88% rename from tests_e2e/scripts/execute_tests.sh rename to tests_e2e/scenarios/scripts/run_scenarios.sh index ec2ffcc578..8aa62531d5 100755 --- a/tests_e2e/scripts/execute_tests.sh +++ b/tests_e2e/scenarios/scripts/run_scenarios.sh @@ -13,7 +13,7 @@ ssh-keygen -y -f "$HOME/.ssh/id_rsa" > "$HOME/.ssh/id_rsa.pub" cd "$HOME/lisa" ./lisa.sh \ - --runbook "$HOME/WALinuxAgent/tests_e2e/lisa/runbook/azure.yml" \ + --runbook "$HOME/WALinuxAgent/tests_e2e/scenarios/runbooks/daily.yml" \ --log_path "$HOME/logs" \ --working_path "$HOME/logs" \ -v subscription_id:"$SUBSCRIPTION_ID" \ diff --git a/tests_e2e/lisa/tests/__init__.py b/tests_e2e/scenarios/tests/__init__.py similarity index 100% rename from tests_e2e/lisa/tests/__init__.py rename to tests_e2e/scenarios/tests/__init__.py diff --git a/tests_e2e/lisa/tests/agent_bvt/__init__.py b/tests_e2e/scenarios/tests/bvts/__init__.py similarity index 100% rename from tests_e2e/lisa/tests/agent_bvt/__init__.py rename to tests_e2e/scenarios/tests/bvts/__init__.py diff --git a/tests_e2e/lisa/tests/agent_bvt/custom_script.py b/tests_e2e/scenarios/tests/bvts/custom_script.py similarity index 92% rename from tests_e2e/lisa/tests/agent_bvt/custom_script.py rename to tests_e2e/scenarios/tests/bvts/custom_script.py index f7bd6de4b0..a65c4fd9a8 100644 --- a/tests_e2e/lisa/tests/agent_bvt/custom_script.py +++ b/tests_e2e/scenarios/tests/bvts/custom_script.py @@ -3,7 +3,7 @@ import uuid import sys -from tests_e2e.scenario_utils.extensions.CustomScriptExtension import CustomScriptExtension +from tests_e2e.scenarios.modules.CustomScriptExtension import CustomScriptExtension def main(subscription_id, resource_group_name, vm_name): diff --git a/tests_e2e/lisa/tests/agent_bvt/check_agent_version.py b/tests_e2e/scenarios/tests/check_agent_version.py similarity index 100% rename from tests_e2e/lisa/tests/agent_bvt/check_agent_version.py rename to tests_e2e/scenarios/tests/check_agent_version.py diff --git a/tests_e2e/lisa/testsuites/agent-bvt.py b/tests_e2e/scenarios/testsuites/agent_bvt.py similarity index 80% rename from tests_e2e/lisa/testsuites/agent-bvt.py rename to tests_e2e/scenarios/testsuites/agent_bvt.py index b7151bd08b..34a228d68a 100644 --- a/tests_e2e/lisa/testsuites/agent-bvt.py +++ b/tests_e2e/scenarios/testsuites/agent_bvt.py @@ -1,7 +1,7 @@ from assertpy import assert_that -from tests_e2e.lisa.testsuites.agent_test_suite import AgentTestSuite -from tests_e2e.lisa.tests.agent_bvt import custom_script +from tests_e2e.scenarios.testsuites.agent_test_suite import AgentTestSuite +from tests_e2e.scenarios.tests.bvts import custom_script from lisa import ( simple_requirement, @@ -25,7 +25,7 @@ def main(self, *_, **__) -> None: self.custom_script() def check_agent_version(self) -> None: - exit_code = self._execute_remote_script(self._test_root.joinpath("lisa", "tests", "agent_bvt"), "check_agent_version.py") + exit_code = self._execute_remote_script(self._test_root.joinpath("scenarios", "tests"), "check_agent_version.py") assert_that(exit_code).is_equal_to(0) def custom_script(self) -> None: diff --git a/tests_e2e/lisa/testsuites/agent_test_suite.py b/tests_e2e/scenarios/testsuites/agent_test_suite.py similarity index 98% rename from tests_e2e/lisa/testsuites/agent_test_suite.py rename to tests_e2e/scenarios/testsuites/agent_test_suite.py index 5644473831..e5f995f3d4 100644 --- a/tests_e2e/lisa/testsuites/agent_test_suite.py +++ b/tests_e2e/scenarios/testsuites/agent_test_suite.py @@ -33,7 +33,7 @@ def before_case(self, *_, **kwargs) -> None: def after_case(self, *_, **__) -> None: # Collect the logs on the test machine into a compressed tarball self._log.info("Collecting logs on test machine [%s]...", self._node.name) - self._execute_remote_script(self._test_root.joinpath("scripts"), "collect_logs.sh") + self._execute_remote_script(self._test_root.joinpath("scenarios", "scripts"), "collect_logs.sh") # Copy the tarball to the local logs directory remote_path = PurePath('/home') / self._node.connection_info['username'] / 'logs.tgz' From cdb25bfdb4287192d4d19ad559a548cb01f7eff1 Mon Sep 17 00:00:00 2001 From: Norberto Arrieta Date: Wed, 7 Dec 2022 15:31:42 -0800 Subject: [PATCH 17/63] Update LISA setup in container image (#2710) Co-authored-by: narrieta --- tests_e2e/docker/Dockerfile | 15 ++++++--------- tests_e2e/scenarios/scripts/run_scenarios.sh | 7 +++---- 2 files changed, 9 insertions(+), 13 deletions(-) diff --git a/tests_e2e/docker/Dockerfile b/tests_e2e/docker/Dockerfile index 5105b0df1a..ee98f0f700 100644 --- a/tests_e2e/docker/Dockerfile +++ b/tests_e2e/docker/Dockerfile @@ -29,8 +29,8 @@ RUN \ # \ # Install LISA dependencies \ # \ - apt-get install -y git gcc libgirepository1.0-dev libcairo2-dev qemu-utils libvirt-dev python3-venv && \ - \ + apt-get install -y git gcc libgirepository1.0-dev libcairo2-dev qemu-utils libvirt-dev \ + python3-pip python3-venv && \ # \ # Create user waagent, which is used to execute the tests \ # \ @@ -44,10 +44,6 @@ RUN \ USER waagent RUN \ - # \ - # Install Poetry \ - # \ - curl -sSL https://raw.githubusercontent.com/python-poetry/poetry/master/install-poetry.py | python3 - && \ export PATH="$HOME/.local/bin:$PATH" && \ \ # \ @@ -56,13 +52,14 @@ RUN \ cd $HOME && \ git clone https://github.com/microsoft/lisa.git && \ cd lisa && \ - poetry config virtualenvs.in-project true && \ - make setup && \ + \ + python3 -m pip install --upgrade pip && \ + python3 -m pip install --editable .[azure,libvirt] --config-settings editable_mode=compat && \ \ # \ # Install additional test dependencies \ # \ - poetry add msrestazure && \ + python3 -m pip install msrestazure && \ \ # \ # The setup for the tests depends on a couple of paths; add those to the profile \ diff --git a/tests_e2e/scenarios/scripts/run_scenarios.sh b/tests_e2e/scenarios/scripts/run_scenarios.sh index 8aa62531d5..75fd96ba3f 100755 --- a/tests_e2e/scenarios/scripts/run_scenarios.sh +++ b/tests_e2e/scenarios/scripts/run_scenarios.sh @@ -2,6 +2,8 @@ set -euxo pipefail +cd "$HOME" + # The private ssh key is shared from the container host as $HOME/id_rsa; copy it to # HOME/.ssh, set the correct mode and generate the public key. mkdir "$HOME/.ssh" @@ -9,10 +11,7 @@ cp "$HOME/id_rsa" "$HOME/.ssh" chmod 700 "$HOME/.ssh/id_rsa" ssh-keygen -y -f "$HOME/.ssh/id_rsa" > "$HOME/.ssh/id_rsa.pub" -# Execute the tests, this needs to be done from the LISA root directory -cd "$HOME/lisa" - -./lisa.sh \ +lisa \ --runbook "$HOME/WALinuxAgent/tests_e2e/scenarios/runbooks/daily.yml" \ --log_path "$HOME/logs" \ --working_path "$HOME/logs" \ From cc91dc67c3a70deec52b603a1491ea46c8e13d88 Mon Sep 17 00:00:00 2001 From: Norberto Arrieta Date: Thu, 15 Dec 2022 15:18:43 -0800 Subject: [PATCH 18/63] Install agent on test VMs (#2714) --- .github/workflows/ci_pr.yml | 14 +- .gitignore | 2 - ci/nosetests_only.sh | 18 -- ci/pylint_and_nosetests.sh | 32 --- makepkg.py | 118 ++++++----- setup.py | 5 +- tests_e2e/docker/Dockerfile | 2 +- .../orchestrator/lib/agent_test_suite.py | 190 ++++++++++++++++++ tests_e2e/orchestrator/scripts/collect-logs | 19 ++ tests_e2e/orchestrator/scripts/install-agent | 105 ++++++++++ .../scripts/run-scenarios} | 9 +- tests_e2e/pipeline/scripts/execute_tests.sh | 10 +- tests_e2e/requirements.txt | 12 -- .../BaseExtensionTestClass.py | 6 +- .../{modules => lib}/CustomScriptExtension.py | 4 +- .../scenarios/{modules => lib}/__init__.py | 0 .../{modules => lib}/azure_models.py | 4 +- .../{modules => lib}/logging_utils.py | 0 .../scenarios/{modules => lib}/models.py | 0 tests_e2e/scenarios/runbooks/daily.yml | 1 - tests_e2e/scenarios/scripts/collect_logs.sh | 17 -- .../scenarios/tests/bvts/custom_script.py | 2 +- .../scenarios/tests/check_agent_version.py | 23 --- tests_e2e/scenarios/testsuites/__init__.py | 0 tests_e2e/scenarios/testsuites/agent_bvt.py | 14 +- .../scenarios/testsuites/agent_test_suite.py | 53 ----- 26 files changed, 419 insertions(+), 241 deletions(-) delete mode 100755 ci/nosetests_only.sh delete mode 100755 ci/pylint_and_nosetests.sh create mode 100644 tests_e2e/orchestrator/lib/agent_test_suite.py create mode 100755 tests_e2e/orchestrator/scripts/collect-logs create mode 100755 tests_e2e/orchestrator/scripts/install-agent rename tests_e2e/{scenarios/scripts/run_scenarios.sh => orchestrator/scripts/run-scenarios} (63%) delete mode 100644 tests_e2e/requirements.txt rename tests_e2e/scenarios/{modules => lib}/BaseExtensionTestClass.py (95%) rename tests_e2e/scenarios/{modules => lib}/CustomScriptExtension.py (82%) rename tests_e2e/scenarios/{modules => lib}/__init__.py (100%) rename tests_e2e/scenarios/{modules => lib}/azure_models.py (98%) rename tests_e2e/scenarios/{modules => lib}/logging_utils.py (100%) rename tests_e2e/scenarios/{modules => lib}/models.py (100%) delete mode 100755 tests_e2e/scenarios/scripts/collect_logs.sh delete mode 100755 tests_e2e/scenarios/tests/check_agent_version.py create mode 100644 tests_e2e/scenarios/testsuites/__init__.py delete mode 100644 tests_e2e/scenarios/testsuites/agent_test_suite.py diff --git a/.github/workflows/ci_pr.yml b/.github/workflows/ci_pr.yml index a4565198d8..279b7e76c4 100644 --- a/.github/workflows/ci_pr.yml +++ b/.github/workflows/ci_pr.yml @@ -41,22 +41,22 @@ jobs: include: - python-version: 2.7 - PYLINTOPTS: "--rcfile=ci/2.7.pylintrc" + PYLINTOPTS: "--rcfile=ci/2.7.pylintrc --ignore=tests_e2e,makepkg.py" - python-version: 3.4 - PYLINTOPTS: "--rcfile=ci/2.7.pylintrc" + PYLINTOPTS: "--rcfile=ci/2.7.pylintrc --ignore=tests_e2e,makepkg.py" - python-version: 3.6 - PYLINTOPTS: "--rcfile=ci/3.6.pylintrc" + PYLINTOPTS: "--rcfile=ci/3.6.pylintrc --ignore=tests_e2e" - python-version: 3.7 - PYLINTOPTS: "--rcfile=ci/3.6.pylintrc" + PYLINTOPTS: "--rcfile=ci/3.6.pylintrc --ignore=tests_e2e" - python-version: 3.8 - PYLINTOPTS: "--rcfile=ci/3.6.pylintrc" + PYLINTOPTS: "--rcfile=ci/3.6.pylintrc --ignore=tests_e2e" - python-version: 3.9 - PYLINTOPTS: "--rcfile=ci/3.6.pylintrc" + PYLINTOPTS: "--rcfile=ci/3.6.pylintrc --ignore=azure_models.py,BaseExtensionTestClass.py" additional-nose-opts: "--with-coverage --cover-erase --cover-inclusive --cover-branches --cover-package=azurelinuxagent" name: "Python ${{ matrix.python-version }} Unit Tests" @@ -64,7 +64,7 @@ jobs: env: PYLINTOPTS: ${{ matrix.PYLINTOPTS }} - PYLINTFILES: "azurelinuxagent setup.py makepkg.py tests" + PYLINTFILES: "azurelinuxagent setup.py makepkg.py tests tests_e2e" NOSEOPTS: "--with-timer ${{ matrix.additional-nose-opts }}" PYTHON_VERSION: ${{ matrix.python-version }} diff --git a/.gitignore b/.gitignore index d4c7873f2b..fd64d3314e 100644 --- a/.gitignore +++ b/.gitignore @@ -17,8 +17,6 @@ develop-eggs/ dist/ downloads/ eggs/ -lib/ -lib64/ parts/ sdist/ var/ diff --git a/ci/nosetests_only.sh b/ci/nosetests_only.sh deleted file mode 100755 index 8f87ea2488..0000000000 --- a/ci/nosetests_only.sh +++ /dev/null @@ -1,18 +0,0 @@ -#!/usr/bin/env bash - -set -u - -EXIT_CODE=0 - -echo -echo "=========================================" -echo "nosetests -a '!requires_sudo' output" -echo "=========================================" -nosetests -a '!requires_sudo' tests || EXIT_CODE=$(($EXIT_CODE || $?)) - -echo "=========================================" -echo "nosetests -a 'requires_sudo' output" -echo "=========================================" -sudo env "PATH=$PATH" nosetests -a 'requires_sudo' tests || EXIT_CODE=$(($EXIT_CODE || $?)) - -exit "$EXIT_CODE" diff --git a/ci/pylint_and_nosetests.sh b/ci/pylint_and_nosetests.sh deleted file mode 100755 index e3e6b93556..0000000000 --- a/ci/pylint_and_nosetests.sh +++ /dev/null @@ -1,32 +0,0 @@ -#!/usr/bin/env bash - -set -u - -pylint $PYLINTOPTS --jobs=0 $PYLINTFILES &> pylint.output & PYLINT_PID=$! -nosetests -a '!requires_sudo' tests &> nosetests_no_sudo.output & NOSETESTS_PID=$! -sudo env "PATH=$PATH" nosetests -a 'requires_sudo' tests &> nosetests_sudo.output & NOSETESTS_SUDO_PID=$! - -EXIT_CODE=0 -wait $PYLINT_PID || EXIT_CODE=$(($EXIT_CODE || $?)) -wait $NOSETESTS_PID || EXIT_CODE=$(($EXIT_CODE || $?)) -wait $NOSETESTS_SUDO_PID || EXIT_CODE=$(($EXIT_CODE || $?)) - -echo "=========================================" -echo "pylint output:" -echo "=========================================" - -cat pylint.output - -echo -echo "=========================================" -echo "nosetests -a '!requires_sudo' output:" -echo "=========================================" -cat nosetests_no_sudo.output - -echo -echo "=========================================" -echo "nosetests -a 'requires_sudo' output:" -echo "=========================================" -cat nosetests_sudo.output - -exit "$EXIT_CODE" \ No newline at end of file diff --git a/makepkg.py b/makepkg.py index 11e90b95a7..e35b16e488 100755 --- a/makepkg.py +++ b/makepkg.py @@ -1,14 +1,15 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 +import argparse import glob -import os +import logging import os.path +import pathlib import shutil import subprocess import sys -from azurelinuxagent.common.version import AGENT_NAME, AGENT_VERSION, \ - AGENT_LONG_VERSION +from azurelinuxagent.common.version import AGENT_NAME, AGENT_VERSION, AGENT_LONG_VERSION from azurelinuxagent.ga.update import AGENT_MANIFEST_FILE MANIFEST = '''[{{ @@ -48,62 +49,77 @@ PUBLISH_MANIFEST_FILE = 'manifest.xml' -output_path = os.path.join(os.getcwd(), "eggs") # pylint: disable=invalid-name -target_path = os.path.join(output_path, AGENT_LONG_VERSION) # pylint: disable=invalid-name -bin_path = os.path.join(target_path, "bin") # pylint: disable=invalid-name -egg_path = os.path.join(bin_path, AGENT_LONG_VERSION + ".egg") # pylint: disable=invalid-name -manifest_path = os.path.join(target_path, AGENT_MANIFEST_FILE) # pylint: disable=invalid-name -publish_manifest_path = os.path.join(target_path, PUBLISH_MANIFEST_FILE) # pylint: disable=invalid-name -pkg_name = os.path.join(output_path, AGENT_LONG_VERSION + ".zip") # pylint: disable=invalid-name -family = 'Test' # pylint: disable=C0103 -if len(sys.argv) > 1: - family = sys.argv[1] # pylint: disable=invalid-name - -def do(*args): # pylint: disable=C0103,W0621 +def do(*args): try: - subprocess.check_output(args, stderr=subprocess.STDOUT) + return subprocess.check_output(args, stderr=subprocess.STDOUT) except subprocess.CalledProcessError as e: # pylint: disable=C0103 - print("ERROR: {0}".format(str(e))) - print("\t{0}".format(" ".join(args))) - print(e.output) - sys.exit(1) - + raise Exception("[{0}] failed:\n{1}\n{2}".format(" ".join(args), str(e), e.stdout)) + + +def run(agent_family, output_directory, log): + output_path = os.path.join(output_directory, "eggs") + target_path = os.path.join(output_path, AGENT_LONG_VERSION) + bin_path = os.path.join(target_path, "bin") + egg_path = os.path.join(bin_path, AGENT_LONG_VERSION + ".egg") + manifest_path = os.path.join(target_path, AGENT_MANIFEST_FILE) + publish_manifest_path = os.path.join(target_path, PUBLISH_MANIFEST_FILE) + pkg_name = os.path.join(output_path, AGENT_LONG_VERSION + ".zip") + + if os.path.isdir(target_path): + shutil.rmtree(target_path) + elif os.path.isfile(target_path): + os.remove(target_path) + if os.path.isfile(pkg_name): + os.remove(pkg_name) + os.makedirs(bin_path) + log.info("Created {0} directory".format(target_path)) + + setup_script = str(pathlib.Path(__file__).parent.joinpath("setup.py")) + args = ["python3", setup_script, "bdist_egg", "--dist-dir={0}".format(bin_path)] + + log.info("Creating egg {0}".format(egg_path)) + do(*args) + + egg_name = os.path.join("bin", os.path.basename( + glob.glob(os.path.join(bin_path, "*"))[0])) + + log.info("Writing {0}".format(manifest_path)) + with open(manifest_path, mode='w') as manifest: + manifest.write(MANIFEST.format(AGENT_NAME, egg_name)) + + log.info("Writing {0}".format(publish_manifest_path)) + with open(publish_manifest_path, mode='w') as publish_manifest: + publish_manifest.write(PUBLISH_MANIFEST.format(AGENT_VERSION, agent_family)) + + cwd = os.getcwd() + os.chdir(target_path) + try: + log.info("Creating package {0}".format(pkg_name)) + do("zip", "-r", pkg_name, egg_name) + do("zip", "-j", pkg_name, AGENT_MANIFEST_FILE) + do("zip", "-j", pkg_name, PUBLISH_MANIFEST_FILE) + finally: + os.chdir(cwd) -if os.path.isdir(target_path): - shutil.rmtree(target_path) -elif os.path.isfile(target_path): - os.remove(target_path) -if os.path.isfile(pkg_name): - os.remove(pkg_name) -os.makedirs(bin_path) -print("Created {0} directory".format(target_path)) + log.info("Package {0} successfully created".format(pkg_name)) -args = ["python", "setup.py", "bdist_egg", "--dist-dir={0}".format(bin_path)] # pylint: disable=invalid-name -print("Creating egg {0}".format(egg_path)) -do(*args) +if __name__ == "__main__": + logging.basicConfig(format='%(message)s', level=logging.INFO) -egg_name = os.path.join("bin", os.path.basename( # pylint: disable=invalid-name - glob.glob(os.path.join(bin_path, "*"))[0])) + parser = argparse.ArgumentParser() + parser.add_argument('family', metavar='family', nargs='?', default='Test', help='Agent family') + parser.add_argument('-o', '--output', default=os.getcwd(), help='Output directory') -print("Writing {0}".format(manifest_path)) -with open(manifest_path, mode='w') as manifest: - manifest.write(MANIFEST.format(AGENT_NAME, egg_name)) + arguments = parser.parse_args() -print("Writing {0}".format(publish_manifest_path)) -with open(publish_manifest_path, mode='w') as publish_manifest: - publish_manifest.write(PUBLISH_MANIFEST.format(AGENT_VERSION, - family)) + try: + run(arguments.family, arguments.output, logging) -cwd = os.getcwd() # pylint: disable=invalid-name -os.chdir(target_path) -print("Creating package {0}".format(pkg_name)) -do("zip", "-r", pkg_name, egg_name) -do("zip", "-j", pkg_name, AGENT_MANIFEST_FILE) -do("zip", "-j", pkg_name, PUBLISH_MANIFEST_FILE) -os.chdir(cwd) + except Exception as exception: + logging.error(str(exception)) + sys.exit(1) -print("Package {0} successfully created".format(pkg_name)) -sys.exit(0) + sys.exit(0) diff --git a/setup.py b/setup.py index a4ce296c65..8f5d92b42e 100755 --- a/setup.py +++ b/setup.py @@ -288,7 +288,10 @@ def initialize_options(self): self.lnx_distro_version = DISTRO_VERSION self.lnx_distro_fullname = DISTRO_FULL_NAME self.register_service = False - self.skip_data_files = False + # All our data files are system-wide files that are not included in the egg; skip them when + # creating an egg. + self.skip_data_files = "bdist_egg" in sys.argv + # pylint: enable=attribute-defined-outside-init def finalize_options(self): diff --git a/tests_e2e/docker/Dockerfile b/tests_e2e/docker/Dockerfile index ee98f0f700..0489d3907f 100644 --- a/tests_e2e/docker/Dockerfile +++ b/tests_e2e/docker/Dockerfile @@ -59,7 +59,7 @@ RUN \ # \ # Install additional test dependencies \ # \ - python3 -m pip install msrestazure && \ + python3 -m pip install distro msrestazure && \ \ # \ # The setup for the tests depends on a couple of paths; add those to the profile \ diff --git a/tests_e2e/orchestrator/lib/agent_test_suite.py b/tests_e2e/orchestrator/lib/agent_test_suite.py new file mode 100644 index 0000000000..4ed68507cb --- /dev/null +++ b/tests_e2e/orchestrator/lib/agent_test_suite.py @@ -0,0 +1,190 @@ +# 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. +# + +from pathlib import Path +import shutil + +import makepkg + +# E0401: Unable to import 'lisa' (import-error) +from lisa import ( # pylint: disable=E0401 + CustomScriptBuilder, + Logger, + Node, + TestSuite, + TestSuiteMetadata, +) +# E0401: Unable to import 'lisa.sut_orchestrator.azure.common' (import-error) +from lisa.sut_orchestrator.azure.common import get_node_context # pylint: disable=E0401 + +from azurelinuxagent.common.version import AGENT_VERSION + + +class AgentTestSuite(TestSuite): + """ + Base class for VM Agent tests. It provides initialization, cleanup, and utilities common to all VM Agent test suites. + """ + def __init__(self, metadata: TestSuiteMetadata): + super().__init__(metadata) + # The actual initialization happens in _initialize() + self._log = None + self._node = None + self._subscription_id = None + self._resource_group_name = None + self._vm_name = None + self._test_source_directory = None + self._working_directory = None + self._node_home_directory = None + + def before_case(self, *_, **kwargs) -> None: + self._initialize(kwargs['node'], kwargs['log']) + self._setup_node() + + def after_case(self, *_, **__) -> None: + try: + self._collect_node_logs() + finally: + self._clean_up() + + def _initialize(self, node: Node, log: Logger) -> None: + self._node = node + self._log = log + + node_context = get_node_context(node) + self._subscription_id = node.features._platform.subscription_id + self._resource_group_name = node_context.resource_group_name + self._vm_name = node_context.vm_name + + self._test_source_directory = AgentTestSuite._get_test_source_directory() + self._working_directory = Path().home()/"waagent-tmp" + self._node_home_directory = Path('/home')/self._node.connection_info['username'] + + self._log.info(f"Test Node: {self._vm_name}") + self._log.info(f"Resource Group: {self._resource_group_name}") + self._log.info(f"Working directory: {self._working_directory}...") + + if self._working_directory.exists(): + self._log.info(f"Removing existing working directory: {self._working_directory}...") + try: + shutil.rmtree(self._working_directory.as_posix()) + except Exception as exception: + self._log.warning(f"Failed to remove the working directory: {exception}") + self._working_directory.mkdir() + + def _clean_up(self) -> None: + self._log.info(f"Removing {self._working_directory}...") + shutil.rmtree(self._working_directory.as_posix(), ignore_errors=True) + + @staticmethod + def _get_test_source_directory() -> Path: + """ + Returns the root directory of the source code for the end-to-end tests (".../WALinuxAgent/tests_e2e") + """ + path = Path(__file__) + while path.name != '': + if path.name == "tests_e2e": + return path + path = path.parent + raise Exception("Can't find the test root directory (tests_e2e)") + + def _setup_node(self) -> None: + """ + Prepares the remote node for executing the test suite. + """ + agent_package_path = self._build_agent_package() + self._install_agent_on_node(agent_package_path) + + def _build_agent_package(self) -> Path: + """ + Builds the agent package and returns the path to the package. + """ + build_path = self._working_directory/"build" + + # The same orchestrator machine may be executing multiple suites on the same test VM, or + # the same suite on one or more test VMs; we use this file to mark the build is already done + build_done_path = self._working_directory/"build.done" + if build_done_path.exists(): + self._log.info("The agent build is already completed, will use existing package.") + else: + self._log.info(f"Building agent package to {build_path}") + makepkg.run(agent_family="Test", output_directory=str(build_path), log=self._log) + build_done_path.touch() + + package_path = build_path/"eggs"/f"WALinuxAgent-{AGENT_VERSION}.zip" + if not package_path.exists(): + raise Exception(f"Can't find the agent package at {package_path}") + + self._log.info(f"Agent package: {package_path}") + + return package_path + + def _install_agent_on_node(self, agent_package: Path) -> None: + """ + Installs the given agent package on the test node. + """ + # The same orchestrator machine may be executing multiple suites on the same test VM, + # we use this file to mark the agent is already installed on the test VM. + install_done_path = self._working_directory/f"agent-install.{self._vm_name}.done" + if install_done_path.exists(): + self._log.info(f"Package {agent_package} is already installed on {self._vm_name}...") + return + + # The install script needs to unzip the agent package; ensure unzip is installed on the test node + self._log.info(f"Installing unzip on {self._vm_name}...") + self._node.os.install_packages("unzip") + + self._log.info(f"Installing {agent_package} on {self._vm_name}...") + agent_package_remote_path = self._node_home_directory/agent_package.name + self._log.info(f"Copying {agent_package} to {self._vm_name}:{agent_package_remote_path}") + self._node.shell.copy(agent_package, agent_package_remote_path) + self._execute_script_on_node( + self._test_source_directory/"orchestrator"/"scripts"/"install-agent", + parameters=f"--package {agent_package_remote_path} --version {AGENT_VERSION}", + sudo=True) + + self._log.info("The agent was installed successfully.") + install_done_path.touch() + + def _collect_node_logs(self) -> None: + """ + Collects the test logs from the remote machine and copied them to the local machine + """ + # Collect the logs on the test machine into a compressed tarball + self._log.info("Collecting logs on test machine [%s]...", self._node.name) + self._execute_script_on_node(self._test_source_directory/"orchestrator"/"scripts"/"collect-logs", sudo=True) + + # Copy the tarball to the local logs directory + remote_path = self._node_home_directory/"logs.tgz" + local_path = Path.home()/'logs'/'vm-logs-{0}.tgz'.format(self._node.name) + self._log.info(f"Copying {self._node.name}:{remote_path} to {local_path}") + self._node.shell.copy_back(remote_path, local_path) + + def _execute_script_on_node(self, script_path: Path, parameters: str = "", sudo: bool = False) -> int: + custom_script_builder = CustomScriptBuilder(script_path.parent, [script_path.name]) + custom_script = self._node.tools[custom_script_builder] + + command_line = f"{script_path} {parameters}" + self._log.info(f"Executing {command_line}") + result = custom_script.run(parameters=parameters, sudo=sudo) + + # LISA appends stderr to stdout so no need to check for stderr + if result.exit_code != 0: + raise Exception(f"[{command_line}] failed.\n{result.stdout}") + + return result.exit_code + + diff --git a/tests_e2e/orchestrator/scripts/collect-logs b/tests_e2e/orchestrator/scripts/collect-logs new file mode 100755 index 0000000000..9872878a7f --- /dev/null +++ b/tests_e2e/orchestrator/scripts/collect-logs @@ -0,0 +1,19 @@ +#!/usr/bin/env bash +# +# Collects the logs needed to debug agent issues into a compressed tarball. +# +set -euxo pipefail + +logs_file_name="$HOME/logs.tgz" + +echo "Collecting logs to $logs_file_name ..." + +tar --exclude='journal/*' --exclude='omsbundle' --exclude='omsagent' --exclude='mdsd' --exclude='scx*' \ + --exclude='*.so' --exclude='*__LinuxDiagnostic__*' --exclude='*.zip' --exclude='*.deb' --exclude='*.rpm' \ + -czf "$logs_file_name" \ + /var/log \ + /var/lib/waagent/ \ + /etc/waagent.conf + +chmod +r "$logs_file_name" + diff --git a/tests_e2e/orchestrator/scripts/install-agent b/tests_e2e/orchestrator/scripts/install-agent new file mode 100755 index 0000000000..08122a2780 --- /dev/null +++ b/tests_e2e/orchestrator/scripts/install-agent @@ -0,0 +1,105 @@ +#!/usr/bin/env bash +# +# +# +set -euo pipefail + +usage() ( + echo "Usage: install-agent -p|--package -v|--version " + exit 1 +) + +while [[ $# -gt 0 ]]; do + case $1 in + -p|--package) + shift + if [ "$#" -lt 1 ]; then + usage + fi + package=$1 + shift + ;; + -v|--version) + shift + if [ "$#" -lt 1 ]; then + usage + fi + version=$1 + shift + ;; + *) + usage + esac +done +if [ "$#" -ne 0 ] || [ -z ${package+x} ] || [ -z ${version+x} ]; then + usage +fi + +# +# The service name is walinuxagent in Ubuntu and waagent elsewhere +# +if service walinuxagent status > /dev/null;then + service_name="walinuxagent" +else + service_name="waagent" +fi +echo "Service name: $service_name" + +# +# Install the package +# +echo "Installing $package..." +unzip -d "/var/lib/waagent/WALinuxAgent-$version" -o "$package" +sed -i 's/AutoUpdate.Enabled=n/AutoUpdate.Enabled=y/g' /etc/waagent.conf + +# +# Restart the service +# +echo "Restarting service..." +service $service_name stop + +# Rename the previous log to ensure the new log starts with the agent we just installed +mv /var/log/waagent.log /var/log/waagent.pre-install.log + +if command -v systemctl &> /dev/null; then + systemctl daemon-reload +fi + +service $service_name start + +# +# Verify that the new agent is running and output its status. Note that the extension handler +# may take some time to start so give 1 minute. +# +echo "Verifying agent installation..." +check-version() { + for i in {0..5} + do + if waagent --version | grep -E "Goal state agent:\s+$1" > /dev/null; then + return 0 + fi + sleep 10 + done + + return 1 +} + +if check-version "$version"; then + printf "\nThe agent was installed successfully\n" + exit_code=0 +else + printf "\nThe agent was not installed correctly; expected version %s\n" "$version" + exit_code=1 +fi + +waagent --version + +printf "\n" + +if command -v systemctl &> /dev/null; then + systemctl --no-pager -l status $service_name +else + service $service_name status +fi + +exit $exit_code diff --git a/tests_e2e/scenarios/scripts/run_scenarios.sh b/tests_e2e/orchestrator/scripts/run-scenarios similarity index 63% rename from tests_e2e/scenarios/scripts/run_scenarios.sh rename to tests_e2e/orchestrator/scripts/run-scenarios index 75fd96ba3f..e5216e5b92 100755 --- a/tests_e2e/scenarios/scripts/run_scenarios.sh +++ b/tests_e2e/orchestrator/scripts/run-scenarios @@ -1,5 +1,11 @@ #!/usr/bin/env bash - +# +# This script runs on the container executing the tests. It creates the SSH keys (private and public) used +# to manage the test VMs taking the initial key value from the file shared by the container host, then it +# executes the daily test runbook. +# +# TODO: The runbook should be parameterized. +# set -euxo pipefail cd "$HOME" @@ -11,6 +17,7 @@ cp "$HOME/id_rsa" "$HOME/.ssh" chmod 700 "$HOME/.ssh/id_rsa" ssh-keygen -y -f "$HOME/.ssh/id_rsa" > "$HOME/.ssh/id_rsa.pub" +# Now start the runbook lisa \ --runbook "$HOME/WALinuxAgent/tests_e2e/scenarios/runbooks/daily.yml" \ --log_path "$HOME/logs" \ diff --git a/tests_e2e/pipeline/scripts/execute_tests.sh b/tests_e2e/pipeline/scripts/execute_tests.sh index 2f177faa73..1da857c3cb 100755 --- a/tests_e2e/pipeline/scripts/execute_tests.sh +++ b/tests_e2e/pipeline/scripts/execute_tests.sh @@ -2,13 +2,16 @@ set -euxo pipefail +# Pull the container image used to execute the tests az login --service-principal --username "$AZURE_CLIENT_ID" --password "$AZURE_CLIENT_SECRET" --tenant "$AZURE_TENANT_ID" > /dev/null az acr login --name waagenttests docker pull waagenttests.azurecr.io/waagenttests:latest -# Logs will be placed in the staging directory. Make waagent (UID 1000 in the container) the owner so that it can write to that location +# Building the agent package writes the egg info to the source code directory, and test write their logs to the staging directory. +# Make waagent (UID 1000 in the container) the owner of both locations, so that it can write to them. +sudo chown 1000 "$BUILD_SOURCESDIRECTORY" sudo chown 1000 "$BUILD_ARTIFACTSTAGINGDIRECTORY" docker run --rm \ @@ -20,9 +23,10 @@ docker run --rm \ --env AZURE_CLIENT_SECRET \ --env AZURE_TENANT_ID \ waagenttests.azurecr.io/waagenttests \ - bash --login -c '$HOME/WALinuxAgent/tests_e2e/scenarios/scripts/run_scenarios.sh' + bash --login -c '$HOME/WALinuxAgent/tests_e2e/orchestrator/scripts/run-scenarios' -# Retake ownership of the staging directory +# Retake ownership of the source and staging directory (note that the former does not need to be done recursively) +sudo chown "$USER" "$BUILD_SOURCESDIRECTORY" sudo find "$BUILD_ARTIFACTSTAGINGDIRECTORY" -exec chown "$USER" {} \; # LISA organizes its logs in a tree similar to diff --git a/tests_e2e/requirements.txt b/tests_e2e/requirements.txt deleted file mode 100644 index fa74f1bfeb..0000000000 --- a/tests_e2e/requirements.txt +++ /dev/null @@ -1,12 +0,0 @@ -# This is a list of pip packages that will be installed on both the orchestrator and the test VM -# Only add the common packages here, for more specific modules, add them to the scenario itself -azure-identity -azure-keyvault-keys -azure-mgmt-compute>=22.1.0 -azure-mgmt-keyvault>=7.0.0 -azure-mgmt-network>=16.0.0 -azure-mgmt-resource>=15.0.0 -cryptography -distro -junitparser -msrestazure diff --git a/tests_e2e/scenarios/modules/BaseExtensionTestClass.py b/tests_e2e/scenarios/lib/BaseExtensionTestClass.py similarity index 95% rename from tests_e2e/scenarios/modules/BaseExtensionTestClass.py rename to tests_e2e/scenarios/lib/BaseExtensionTestClass.py index bc00f15c1d..0a7cf71b0c 100644 --- a/tests_e2e/scenarios/modules/BaseExtensionTestClass.py +++ b/tests_e2e/scenarios/lib/BaseExtensionTestClass.py @@ -3,9 +3,9 @@ from azure.core.polling import LROPoller -from tests_e2e.scenarios.modules.azure_models import ComputeManager -from tests_e2e.scenarios.modules.logging_utils import LoggingHandler -from tests_e2e.scenarios.modules.models import ExtensionMetaData, get_vm_data_from_env +from tests_e2e.scenarios.lib.azure_models import ComputeManager +from tests_e2e.scenarios.lib.logging_utils import LoggingHandler +from tests_e2e.scenarios.lib.models import ExtensionMetaData, get_vm_data_from_env class BaseExtensionTestClass(LoggingHandler): diff --git a/tests_e2e/scenarios/modules/CustomScriptExtension.py b/tests_e2e/scenarios/lib/CustomScriptExtension.py similarity index 82% rename from tests_e2e/scenarios/modules/CustomScriptExtension.py rename to tests_e2e/scenarios/lib/CustomScriptExtension.py index 7c67052ef6..369c710124 100644 --- a/tests_e2e/scenarios/modules/CustomScriptExtension.py +++ b/tests_e2e/scenarios/lib/CustomScriptExtension.py @@ -1,7 +1,7 @@ import uuid -from tests_e2e.scenarios.modules.BaseExtensionTestClass import BaseExtensionTestClass -from tests_e2e.scenarios.modules.models import ExtensionMetaData +from tests_e2e.scenarios.lib.BaseExtensionTestClass import BaseExtensionTestClass +from tests_e2e.scenarios.lib.models import ExtensionMetaData class CustomScriptExtension(BaseExtensionTestClass): diff --git a/tests_e2e/scenarios/modules/__init__.py b/tests_e2e/scenarios/lib/__init__.py similarity index 100% rename from tests_e2e/scenarios/modules/__init__.py rename to tests_e2e/scenarios/lib/__init__.py diff --git a/tests_e2e/scenarios/modules/azure_models.py b/tests_e2e/scenarios/lib/azure_models.py similarity index 98% rename from tests_e2e/scenarios/modules/azure_models.py rename to tests_e2e/scenarios/lib/azure_models.py index 99875d39c4..9a9c7a15bf 100644 --- a/tests_e2e/scenarios/modules/azure_models.py +++ b/tests_e2e/scenarios/lib/azure_models.py @@ -12,8 +12,8 @@ from azure.mgmt.resource import ResourceManagementClient from msrestazure.azure_exceptions import CloudError -from tests_e2e.scenarios.modules.logging_utils import LoggingHandler -from tests_e2e.scenarios.modules.models import get_vm_data_from_env, VMModelType, VMMetaData +from tests_e2e.scenarios.lib.logging_utils import LoggingHandler +from tests_e2e.scenarios.lib.models import get_vm_data_from_env, VMModelType, VMMetaData class AzureComputeBaseClass(ABC, LoggingHandler): diff --git a/tests_e2e/scenarios/modules/logging_utils.py b/tests_e2e/scenarios/lib/logging_utils.py similarity index 100% rename from tests_e2e/scenarios/modules/logging_utils.py rename to tests_e2e/scenarios/lib/logging_utils.py diff --git a/tests_e2e/scenarios/modules/models.py b/tests_e2e/scenarios/lib/models.py similarity index 100% rename from tests_e2e/scenarios/modules/models.py rename to tests_e2e/scenarios/lib/models.py diff --git a/tests_e2e/scenarios/runbooks/daily.yml b/tests_e2e/scenarios/runbooks/daily.yml index 27d9d5bb1c..fe723b0f9f 100644 --- a/tests_e2e/scenarios/runbooks/daily.yml +++ b/tests_e2e/scenarios/runbooks/daily.yml @@ -44,7 +44,6 @@ variable: value: "" is_secret: true notifier: - - type: html - type: env_stats - type: junit platform: diff --git a/tests_e2e/scenarios/scripts/collect_logs.sh b/tests_e2e/scenarios/scripts/collect_logs.sh deleted file mode 100755 index b557215d1c..0000000000 --- a/tests_e2e/scenarios/scripts/collect_logs.sh +++ /dev/null @@ -1,17 +0,0 @@ -#!/usr/bin/env bash - -set -euxo pipefail - -logs_file_name="$HOME/logs.tgz" - -echo "Collecting logs to $logs_file_name ..." - -sudo tar --exclude='journal/*' --exclude='omsbundle' --exclude='omsagent' --exclude='mdsd' --exclude='scx*' \ - --exclude='*.so' --exclude='*__LinuxDiagnostic__*' --exclude='*.zip' --exclude='*.deb' --exclude='*.rpm' \ - -czf "$logs_file_name" \ - /var/log \ - /var/lib/waagent/ \ - /etc/waagent.conf - -sudo chmod +r "$logs_file_name" - diff --git a/tests_e2e/scenarios/tests/bvts/custom_script.py b/tests_e2e/scenarios/tests/bvts/custom_script.py index a65c4fd9a8..2d716223bb 100644 --- a/tests_e2e/scenarios/tests/bvts/custom_script.py +++ b/tests_e2e/scenarios/tests/bvts/custom_script.py @@ -3,7 +3,7 @@ import uuid import sys -from tests_e2e.scenarios.modules.CustomScriptExtension import CustomScriptExtension +from tests_e2e.scenarios.lib.CustomScriptExtension import CustomScriptExtension def main(subscription_id, resource_group_name, vm_name): diff --git a/tests_e2e/scenarios/tests/check_agent_version.py b/tests_e2e/scenarios/tests/check_agent_version.py deleted file mode 100755 index c63402dfae..0000000000 --- a/tests_e2e/scenarios/tests/check_agent_version.py +++ /dev/null @@ -1,23 +0,0 @@ -#!/usr/bin/env python - -from __future__ import print_function - -import subprocess -import sys - - -def main(): - print("Executing waagent --version") - - pipe = subprocess.Popen(['waagent', '-version'], stdout=subprocess.PIPE, stderr=subprocess.PIPE) - stdout_lines = list(map(lambda s: s.decode('utf-8'), pipe.stdout.readlines())) - exit_code = pipe.wait() - - for line in stdout_lines: - print(line) - - return exit_code - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/tests_e2e/scenarios/testsuites/__init__.py b/tests_e2e/scenarios/testsuites/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests_e2e/scenarios/testsuites/agent_bvt.py b/tests_e2e/scenarios/testsuites/agent_bvt.py index 34a228d68a..da39539a9c 100644 --- a/tests_e2e/scenarios/testsuites/agent_bvt.py +++ b/tests_e2e/scenarios/testsuites/agent_bvt.py @@ -1,10 +1,8 @@ -from assertpy import assert_that - -from tests_e2e.scenarios.testsuites.agent_test_suite import AgentTestSuite +from tests_e2e.orchestrator.lib.agent_test_suite import AgentTestSuite from tests_e2e.scenarios.tests.bvts import custom_script -from lisa import ( - simple_requirement, +# E0401: Unable to import 'lisa' (import-error) +from lisa import ( # pylint: disable=E0401 TestCaseMetadata, TestSuiteMetadata, ) @@ -16,18 +14,12 @@ description=""" A POC test suite for the waagent BVTs. """, - requirement=simple_requirement(unsupported_os=[]), ) class AgentBvt(AgentTestSuite): @TestCaseMetadata(description="", priority=0) def main(self, *_, **__) -> None: - self.check_agent_version() self.custom_script() - def check_agent_version(self) -> None: - exit_code = self._execute_remote_script(self._test_root.joinpath("scenarios", "tests"), "check_agent_version.py") - assert_that(exit_code).is_equal_to(0) - def custom_script(self) -> None: custom_script.main(self._subscription_id, self._resource_group_name, self._vm_name) diff --git a/tests_e2e/scenarios/testsuites/agent_test_suite.py b/tests_e2e/scenarios/testsuites/agent_test_suite.py deleted file mode 100644 index e5f995f3d4..0000000000 --- a/tests_e2e/scenarios/testsuites/agent_test_suite.py +++ /dev/null @@ -1,53 +0,0 @@ -from pathlib import Path, PurePath - -from lisa import ( - CustomScriptBuilder, - TestSuite, - TestSuiteMetadata, -) -from lisa.sut_orchestrator.azure.common import get_node_context - - -class AgentTestSuite(TestSuite): - def __init__(self, metadata: TestSuiteMetadata): - super().__init__(metadata) - self._log = None - self._node = None - self._test_root = None - self._subscription_id = None - self._resource_group_name = None - self._vm_name = None - - def before_case(self, *_, **kwargs) -> None: - node = kwargs['node'] - log = kwargs['log'] - node_context = get_node_context(node) - - self._log = log - self._node = node - self._test_root = Path(__file__).parent.parent.parent - self._subscription_id = node.features._platform.subscription_id - self._resource_group_name = node_context.resource_group_name - self._vm_name = node_context.vm_name - - def after_case(self, *_, **__) -> None: - # Collect the logs on the test machine into a compressed tarball - self._log.info("Collecting logs on test machine [%s]...", self._node.name) - self._execute_remote_script(self._test_root.joinpath("scenarios", "scripts"), "collect_logs.sh") - - # Copy the tarball to the local logs directory - remote_path = PurePath('/home') / self._node.connection_info['username'] / 'logs.tgz' - local_path = Path.home() / 'logs' / 'vm-logs-{0}.tgz'.format(self._node.name) - self._log.info("Copying %s:%s to %s...", self._node.name, remote_path, local_path) - self._node.shell.copy_back(remote_path, local_path) - - def _execute_remote_script(self, path: Path, script: str) -> int: - custom_script_builder = CustomScriptBuilder(path, [script]) - custom_script = self._node.tools[custom_script_builder] - self._log.info('Executing %s/%s...', path, script) - result = custom_script.run() - if result.stdout: - self._log.info('%s', result.stdout) - if result.stderr: - self._log.error('%s', result.stderr) - return result.exit_code From d56a3c782d6976f6c8086ee7d809a9b111fb81c5 Mon Sep 17 00:00:00 2001 From: Norberto Arrieta Date: Fri, 16 Dec 2022 12:26:04 -0800 Subject: [PATCH 19/63] Remove dependency on pathlib from makepkg.py (#2717) Co-authored-by: narrieta --- makepkg.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/makepkg.py b/makepkg.py index e35b16e488..cef24e3513 100755 --- a/makepkg.py +++ b/makepkg.py @@ -4,7 +4,6 @@ import glob import logging import os.path -import pathlib import shutil import subprocess import sys @@ -75,8 +74,7 @@ def run(agent_family, output_directory, log): os.makedirs(bin_path) log.info("Created {0} directory".format(target_path)) - setup_script = str(pathlib.Path(__file__).parent.joinpath("setup.py")) - args = ["python3", setup_script, "bdist_egg", "--dist-dir={0}".format(bin_path)] + args = ["python3", "setup.py", "bdist_egg", "--dist-dir={0}".format(bin_path)] log.info("Creating egg {0}".format(egg_path)) do(*args) From a3a41bd1e565b9dcf298584181e36f101ac97d78 Mon Sep 17 00:00:00 2001 From: Norberto Arrieta Date: Tue, 20 Dec 2022 13:49:16 -0800 Subject: [PATCH 20/63] Improvements in error handling on end-to-end tests (#2716) * Improvements in error handling on end-to-end tests * Undo changes to daily.yml * pylint Co-authored-by: narrieta --- .../orchestrator/lib/agent_test_suite.py | 186 +++++++++--------- tests_e2e/orchestrator/scripts/collect-logs | 6 + tests_e2e/orchestrator/scripts/install-agent | 17 +- tests_e2e/orchestrator/scripts/run-scenarios | 18 ++ .../scenarios/runbooks/samples/hello_world.py | 32 +++ .../scenarios/runbooks/samples/local.yml | 28 +++ tests_e2e/scenarios/testsuites/agent_bvt.py | 44 +++-- 7 files changed, 226 insertions(+), 105 deletions(-) create mode 100644 tests_e2e/scenarios/runbooks/samples/hello_world.py create mode 100644 tests_e2e/scenarios/runbooks/samples/local.yml diff --git a/tests_e2e/orchestrator/lib/agent_test_suite.py b/tests_e2e/orchestrator/lib/agent_test_suite.py index 4ed68507cb..e5ebf3dba1 100644 --- a/tests_e2e/orchestrator/lib/agent_test_suite.py +++ b/tests_e2e/orchestrator/lib/agent_test_suite.py @@ -14,9 +14,9 @@ # See the License for the specific language governing permissions and # limitations under the License. # - +from collections.abc import Callable from pathlib import Path -import shutil +from shutil import rmtree import makepkg @@ -25,8 +25,6 @@ CustomScriptBuilder, Logger, Node, - TestSuite, - TestSuiteMetadata, ) # E0401: Unable to import 'lisa.sut_orchestrator.azure.common' (import-error) from lisa.sut_orchestrator.azure.common import get_node_context # pylint: disable=E0401 @@ -34,60 +32,35 @@ from azurelinuxagent.common.version import AGENT_VERSION -class AgentTestSuite(TestSuite): +class AgentTestScenario(object): """ - Base class for VM Agent tests. It provides initialization, cleanup, and utilities common to all VM Agent test suites. + Instances of this class are used to execute Agent test scenarios. It also provides facilities to execute commands over SSH. """ - def __init__(self, metadata: TestSuiteMetadata): - super().__init__(metadata) - # The actual initialization happens in _initialize() - self._log = None - self._node = None - self._subscription_id = None - self._resource_group_name = None - self._vm_name = None - self._test_source_directory = None - self._working_directory = None - self._node_home_directory = None - - def before_case(self, *_, **kwargs) -> None: - self._initialize(kwargs['node'], kwargs['log']) - self._setup_node() - - def after_case(self, *_, **__) -> None: - try: - self._collect_node_logs() - finally: - self._clean_up() - - def _initialize(self, node: Node, log: Logger) -> None: - self._node = node - self._log = log - node_context = get_node_context(node) - self._subscription_id = node.features._platform.subscription_id - self._resource_group_name = node_context.resource_group_name - self._vm_name = node_context.vm_name - - self._test_source_directory = AgentTestSuite._get_test_source_directory() - self._working_directory = Path().home()/"waagent-tmp" - self._node_home_directory = Path('/home')/self._node.connection_info['username'] + class Context: + """ + Execution context for test scenarios, this information is passed to test scenarios by AgentTestScenario.execute() + """ + subscription_id: str + resource_group_name: str + vm_name: str + test_source_directory: Path + working_directory: Path + node_home_directory: Path - self._log.info(f"Test Node: {self._vm_name}") - self._log.info(f"Resource Group: {self._resource_group_name}") - self._log.info(f"Working directory: {self._working_directory}...") + def __init__(self, node: Node, log: Logger) -> None: + self._node: Node = node + self._log: Logger = log - if self._working_directory.exists(): - self._log.info(f"Removing existing working directory: {self._working_directory}...") - try: - shutil.rmtree(self._working_directory.as_posix()) - except Exception as exception: - self._log.warning(f"Failed to remove the working directory: {exception}") - self._working_directory.mkdir() + node_context = get_node_context(node) + self._context: AgentTestScenario.Context = AgentTestScenario.Context() + self._context.subscription_id = node.features._platform.subscription_id + self._context.resource_group_name = node_context.resource_group_name + self._context.vm_name = node_context.vm_name - def _clean_up(self) -> None: - self._log.info(f"Removing {self._working_directory}...") - shutil.rmtree(self._working_directory.as_posix(), ignore_errors=True) + self._context.test_source_directory = AgentTestScenario._get_test_source_directory() + self._context.working_directory = Path().home()/"waagent-tmp" + self._context.node_home_directory = Path('/home')/node.connection_info['username'] @staticmethod def _get_test_source_directory() -> Path: @@ -101,6 +74,29 @@ def _get_test_source_directory() -> Path: path = path.parent raise Exception("Can't find the test root directory (tests_e2e)") + def _setup(self) -> None: + """ + Prepares the test scenario for execution + """ + self._log.info(f"Test Node: {self._context.vm_name}") + self._log.info(f"Resource Group: {self._context.resource_group_name}") + self._log.info(f"Working directory: {self._context.working_directory}...") + + if self._context.working_directory.exists(): + self._log.info(f"Removing existing working directory: {self._context.working_directory}...") + try: + rmtree(self._context.working_directory.as_posix()) + except Exception as exception: + self._log.warning(f"Failed to remove the working directory: {exception}") + self._context.working_directory.mkdir() + + def _clean_up(self) -> None: + """ + Cleans up any leftovers from the test scenario run. + """ + self._log.info(f"Removing {self._context.working_directory}...") + rmtree(self._context.working_directory.as_posix(), ignore_errors=True) + def _setup_node(self) -> None: """ Prepares the remote node for executing the test suite. @@ -112,18 +108,10 @@ def _build_agent_package(self) -> Path: """ Builds the agent package and returns the path to the package. """ - build_path = self._working_directory/"build" - - # The same orchestrator machine may be executing multiple suites on the same test VM, or - # the same suite on one or more test VMs; we use this file to mark the build is already done - build_done_path = self._working_directory/"build.done" - if build_done_path.exists(): - self._log.info("The agent build is already completed, will use existing package.") - else: - self._log.info(f"Building agent package to {build_path}") - makepkg.run(agent_family="Test", output_directory=str(build_path), log=self._log) - build_done_path.touch() + build_path = self._context.working_directory/"build" + self._log.info(f"Building agent package to {build_path}") + makepkg.run(agent_family="Test", output_directory=str(build_path), log=self._log) package_path = build_path/"eggs"/f"WALinuxAgent-{AGENT_VERSION}.zip" if not package_path.exists(): raise Exception(f"Can't find the agent package at {package_path}") @@ -136,54 +124,70 @@ def _install_agent_on_node(self, agent_package: Path) -> None: """ Installs the given agent package on the test node. """ - # The same orchestrator machine may be executing multiple suites on the same test VM, - # we use this file to mark the agent is already installed on the test VM. - install_done_path = self._working_directory/f"agent-install.{self._vm_name}.done" - if install_done_path.exists(): - self._log.info(f"Package {agent_package} is already installed on {self._vm_name}...") - return - # The install script needs to unzip the agent package; ensure unzip is installed on the test node - self._log.info(f"Installing unzip on {self._vm_name}...") + self._log.info(f"Installing unzip on {self._context.vm_name}...") self._node.os.install_packages("unzip") - self._log.info(f"Installing {agent_package} on {self._vm_name}...") - agent_package_remote_path = self._node_home_directory/agent_package.name - self._log.info(f"Copying {agent_package} to {self._vm_name}:{agent_package_remote_path}") + self._log.info(f"Installing {agent_package} on {self._context.vm_name}...") + agent_package_remote_path = self._context.node_home_directory/agent_package.name + self._log.info(f"Copying {agent_package} to {self._context.vm_name}:{agent_package_remote_path}") self._node.shell.copy(agent_package, agent_package_remote_path) - self._execute_script_on_node( - self._test_source_directory/"orchestrator"/"scripts"/"install-agent", + self.execute_script_on_node( + self._context.test_source_directory/"orchestrator"/"scripts"/"install-agent", parameters=f"--package {agent_package_remote_path} --version {AGENT_VERSION}", sudo=True) self._log.info("The agent was installed successfully.") - install_done_path.touch() def _collect_node_logs(self) -> None: """ - Collects the test logs from the remote machine and copied them to the local machine + Collects the test logs from the remote machine and copies them to the local machine """ - # Collect the logs on the test machine into a compressed tarball - self._log.info("Collecting logs on test machine [%s]...", self._node.name) - self._execute_script_on_node(self._test_source_directory/"orchestrator"/"scripts"/"collect-logs", sudo=True) - - # Copy the tarball to the local logs directory - remote_path = self._node_home_directory/"logs.tgz" - local_path = Path.home()/'logs'/'vm-logs-{0}.tgz'.format(self._node.name) - self._log.info(f"Copying {self._node.name}:{remote_path} to {local_path}") - self._node.shell.copy_back(remote_path, local_path) + try: + # Collect the logs on the test machine into a compressed tarball + self._log.info("Collecting logs on test machine [%s]...", self._node.name) + self.execute_script_on_node(self._context.test_source_directory/"orchestrator"/"scripts"/"collect-logs", sudo=True) + + # Copy the tarball to the local logs directory + remote_path = self._context.node_home_directory/"logs.tgz" + local_path = Path.home()/'logs'/'vm-logs-{0}.tgz'.format(self._node.name) + self._log.info(f"Copying {self._node.name}:{remote_path} to {local_path}") + self._node.shell.copy_back(remote_path, local_path) + except Exception as e: + self._log.warning(f"Failed to collect logs from the test machine: {e}") + + def execute(self, scenario: Callable[[Context], None]) -> None: + """ + Executes the given scenario + """ + try: + self._setup() + try: + self._setup_node() + scenario(self._context) + finally: + self._collect_node_logs() + finally: + self._clean_up() - def _execute_script_on_node(self, script_path: Path, parameters: str = "", sudo: bool = False) -> int: + def execute_script_on_node(self, script_path: Path, parameters: str = "", sudo: bool = False) -> int: + """ + Executes the given script on the test node; if 'sudo' is True, the script is executed using the sudo command. + """ custom_script_builder = CustomScriptBuilder(script_path.parent, [script_path.name]) custom_script = self._node.tools[custom_script_builder] - command_line = f"{script_path} {parameters}" - self._log.info(f"Executing {command_line}") + if parameters == '': + command_line = f"{script_path}" + else: + command_line = f"{script_path} {parameters}" + + self._log.info(f"Executing [{command_line}]") result = custom_script.run(parameters=parameters, sudo=sudo) # LISA appends stderr to stdout so no need to check for stderr if result.exit_code != 0: - raise Exception(f"[{command_line}] failed.\n{result.stdout}") + raise Exception(f"Command [{command_line}] failed.\n{result.stdout}") return result.exit_code diff --git a/tests_e2e/orchestrator/scripts/collect-logs b/tests_e2e/orchestrator/scripts/collect-logs index 9872878a7f..46a23aff18 100755 --- a/tests_e2e/orchestrator/scripts/collect-logs +++ b/tests_e2e/orchestrator/scripts/collect-logs @@ -15,5 +15,11 @@ tar --exclude='journal/*' --exclude='omsbundle' --exclude='omsagent' --exclude=' /var/lib/waagent/ \ /etc/waagent.conf +# tar exits with 1 on warnings; ignore those +exit_code=$? +if [ "$exit_code" != "1" ] && [ "$exit_code" != "0" ]; then + exit $exit_code +fi + chmod +r "$logs_file_name" diff --git a/tests_e2e/orchestrator/scripts/install-agent b/tests_e2e/orchestrator/scripts/install-agent index 08122a2780..439bdcec65 100755 --- a/tests_e2e/orchestrator/scripts/install-agent +++ b/tests_e2e/orchestrator/scripts/install-agent @@ -1,7 +1,22 @@ #!/usr/bin/env bash + +# 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. +# + set -euo pipefail usage() ( @@ -59,7 +74,7 @@ echo "Restarting service..." service $service_name stop # Rename the previous log to ensure the new log starts with the agent we just installed -mv /var/log/waagent.log /var/log/waagent.pre-install.log +mv /var/log/waagent.log /var/log/waagent."$(date --iso-8601=seconds)".log if command -v systemctl &> /dev/null; then systemctl daemon-reload diff --git a/tests_e2e/orchestrator/scripts/run-scenarios b/tests_e2e/orchestrator/scripts/run-scenarios index e5216e5b92..43bc43a856 100755 --- a/tests_e2e/orchestrator/scripts/run-scenarios +++ b/tests_e2e/orchestrator/scripts/run-scenarios @@ -1,4 +1,22 @@ #!/usr/bin/env bash + +# 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 script runs on the container executing the tests. It creates the SSH keys (private and public) used # to manage the test VMs taking the initial key value from the file shared by the container host, then it diff --git a/tests_e2e/scenarios/runbooks/samples/hello_world.py b/tests_e2e/scenarios/runbooks/samples/hello_world.py new file mode 100644 index 0000000000..bf1a44a5c5 --- /dev/null +++ b/tests_e2e/scenarios/runbooks/samples/hello_world.py @@ -0,0 +1,32 @@ +# 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. +# + +# E0401: Unable to import 'lisa' (import-error) +from lisa import ( # pylint: disable=E0401 + Logger, + Node, + TestCaseMetadata, + TestSuite, + TestSuiteMetadata, +) + + +@TestSuiteMetadata(area="sample", category="", description="") +class HelloWorld(TestSuite): + @TestCaseMetadata(description="") + def main(self, node: Node, log: Logger) -> None: + log.info(f"Hello world from {node.os.name}!") diff --git a/tests_e2e/scenarios/runbooks/samples/local.yml b/tests_e2e/scenarios/runbooks/samples/local.yml new file mode 100644 index 0000000000..f5edec65b2 --- /dev/null +++ b/tests_e2e/scenarios/runbooks/samples/local.yml @@ -0,0 +1,28 @@ +# 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. +# + +extension: + - "." +environment: + environments: + - nodes: + - type: local +notifier: + - type: console +testcase: + - criteria: + area: sample diff --git a/tests_e2e/scenarios/testsuites/agent_bvt.py b/tests_e2e/scenarios/testsuites/agent_bvt.py index da39539a9c..a63c3e34df 100644 --- a/tests_e2e/scenarios/testsuites/agent_bvt.py +++ b/tests_e2e/scenarios/testsuites/agent_bvt.py @@ -1,26 +1,44 @@ -from tests_e2e.orchestrator.lib.agent_test_suite import AgentTestSuite +# 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. +# + +from tests_e2e.orchestrator.lib.agent_test_suite import AgentTestScenario from tests_e2e.scenarios.tests.bvts import custom_script # E0401: Unable to import 'lisa' (import-error) from lisa import ( # pylint: disable=E0401 + Logger, + Node, TestCaseMetadata, + TestSuite, TestSuiteMetadata, ) -@TestSuiteMetadata( - area="bvt", - category="functional", - description=""" - A POC test suite for the waagent BVTs. - """, -) -class AgentBvt(AgentTestSuite): +@TestSuiteMetadata(area="bvt", category="", description="Test suite for Agent BVTs") +class AgentBvt(TestSuite): + """ + Test suite for Agent BVTs + """ @TestCaseMetadata(description="", priority=0) - def main(self, *_, **__) -> None: - self.custom_script() + def main(self, log: Logger, node: Node) -> None: + def tests(ctx: AgentTestScenario.Context) -> None: + custom_script.main(ctx.subscription_id, ctx.resource_group_name, ctx.vm_name) + + AgentTestScenario(node, log).execute(tests) - def custom_script(self) -> None: - custom_script.main(self._subscription_id, self._resource_group_name, self._vm_name) From 2bd03c9abd6b8ddd94dfefad04ca957fff456537 Mon Sep 17 00:00:00 2001 From: Norberto Arrieta Date: Thu, 5 Jan 2023 17:00:47 -0800 Subject: [PATCH 21/63] Add BVT for the agent (#2719) * Add BVT for extension workflow * logging * update scenario * test context * AgentTest * vmaccess bvt * arguments; protected settings * fix username * RunCommand * Test dependencies * ssh key mode * ssh key file * key mode * log type * PR review * Unused import Co-authored-by: narrieta --- .github/workflows/ci_pr.yml | 2 +- makepkg.py | 3 +- test-requirements.txt | 6 + .../orchestrator/lib/agent_test_suite.py | 239 ++++++++++++------ tests_e2e/orchestrator/scripts/run-scenarios | 2 +- .../scenarios/lib/BaseExtensionTestClass.py | 113 --------- .../scenarios/lib/CustomScriptExtension.py | 29 --- tests_e2e/scenarios/lib/agent_test.py | 54 ++++ tests_e2e/scenarios/lib/agent_test_context.py | 162 ++++++++++++ tests_e2e/scenarios/lib/azure_models.py | 239 ------------------ tests_e2e/scenarios/lib/identifiers.py | 63 +++++ tests_e2e/scenarios/lib/logging.py | 37 +++ tests_e2e/scenarios/lib/logging_utils.py | 33 --- tests_e2e/scenarios/lib/models.py | 135 ---------- tests_e2e/scenarios/lib/retry.py | 41 +++ tests_e2e/scenarios/lib/shell.py | 53 ++++ tests_e2e/scenarios/lib/ssh_client.py | 46 ++++ tests_e2e/scenarios/lib/virtual_machine.py | 143 +++++++++++ tests_e2e/scenarios/lib/vm_extension.py | 239 ++++++++++++++++++ .../scenarios/tests/bvts/custom_script.py | 45 ---- .../tests/bvts/extension_operations.py | 79 ++++++ tests_e2e/scenarios/tests/bvts/run_command.py | 89 +++++++ tests_e2e/scenarios/tests/bvts/vm_access.py | 75 ++++++ tests_e2e/scenarios/testsuites/agent_bvt.py | 24 +- 24 files changed, 1265 insertions(+), 686 deletions(-) delete mode 100644 tests_e2e/scenarios/lib/BaseExtensionTestClass.py delete mode 100644 tests_e2e/scenarios/lib/CustomScriptExtension.py create mode 100644 tests_e2e/scenarios/lib/agent_test.py create mode 100644 tests_e2e/scenarios/lib/agent_test_context.py delete mode 100644 tests_e2e/scenarios/lib/azure_models.py create mode 100644 tests_e2e/scenarios/lib/identifiers.py create mode 100644 tests_e2e/scenarios/lib/logging.py delete mode 100644 tests_e2e/scenarios/lib/logging_utils.py delete mode 100644 tests_e2e/scenarios/lib/models.py create mode 100644 tests_e2e/scenarios/lib/retry.py create mode 100644 tests_e2e/scenarios/lib/shell.py create mode 100644 tests_e2e/scenarios/lib/ssh_client.py create mode 100644 tests_e2e/scenarios/lib/virtual_machine.py create mode 100644 tests_e2e/scenarios/lib/vm_extension.py delete mode 100644 tests_e2e/scenarios/tests/bvts/custom_script.py create mode 100755 tests_e2e/scenarios/tests/bvts/extension_operations.py create mode 100755 tests_e2e/scenarios/tests/bvts/run_command.py create mode 100755 tests_e2e/scenarios/tests/bvts/vm_access.py diff --git a/.github/workflows/ci_pr.yml b/.github/workflows/ci_pr.yml index 279b7e76c4..589f0f5e7b 100644 --- a/.github/workflows/ci_pr.yml +++ b/.github/workflows/ci_pr.yml @@ -56,7 +56,7 @@ jobs: PYLINTOPTS: "--rcfile=ci/3.6.pylintrc --ignore=tests_e2e" - python-version: 3.9 - PYLINTOPTS: "--rcfile=ci/3.6.pylintrc --ignore=azure_models.py,BaseExtensionTestClass.py" + PYLINTOPTS: "--rcfile=ci/3.6.pylintrc" additional-nose-opts: "--with-coverage --cover-erase --cover-inclusive --cover-branches --cover-package=azurelinuxagent" name: "Python ${{ matrix.python-version }} Unit Tests" diff --git a/makepkg.py b/makepkg.py index cef24e3513..25b209229b 100755 --- a/makepkg.py +++ b/makepkg.py @@ -74,7 +74,8 @@ def run(agent_family, output_directory, log): os.makedirs(bin_path) log.info("Created {0} directory".format(target_path)) - args = ["python3", "setup.py", "bdist_egg", "--dist-dir={0}".format(bin_path)] + setup_path = os.path.join(os.path.dirname(__file__), "setup.py") + args = ["python3", setup_path, "bdist_egg", "--dist-dir={0}".format(bin_path)] log.info("Creating egg {0}".format(egg_path)) do(*args) diff --git a/test-requirements.txt b/test-requirements.txt index f335db2826..3c54ab9974 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -13,3 +13,9 @@ wrapt==1.12.0; python_version > '2.6' and python_version < '3.6' pylint; python_version > '2.6' and python_version < '3.6' pylint==2.8.3; python_version >= '3.6' +# Requirements to run pylint on the end-to-end tests source code +assertpy +azure-core +azure-identity +azure-mgmt-compute>=22.1.0 +azure-mgmt-resource>=15.0.0 diff --git a/tests_e2e/orchestrator/lib/agent_test_suite.py b/tests_e2e/orchestrator/lib/agent_test_suite.py index e5ebf3dba1..c4d94a2421 100644 --- a/tests_e2e/orchestrator/lib/agent_test_suite.py +++ b/tests_e2e/orchestrator/lib/agent_test_suite.py @@ -14,88 +14,109 @@ # See the License for the specific language governing permissions and # limitations under the License. # -from collections.abc import Callable +from assertpy import assert_that from pathlib import Path from shutil import rmtree +from typing import List, Type -import makepkg - -# E0401: Unable to import 'lisa' (import-error) +# Disable those warnings, since 'lisa' is an external, non-standard, dependency +# E0401: Unable to import 'lisa' (import-error) +# E0401: Unable to import 'lisa.sut_orchestrator' (import-error) +# E0401: Unable to import 'lisa.sut_orchestrator.azure.common' (import-error) from lisa import ( # pylint: disable=E0401 CustomScriptBuilder, - Logger, Node, + TestSuite, + TestSuiteMetadata ) -# E0401: Unable to import 'lisa.sut_orchestrator.azure.common' (import-error) -from lisa.sut_orchestrator.azure.common import get_node_context # pylint: disable=E0401 +from lisa.sut_orchestrator import AZURE # pylint: disable=E0401 +from lisa.sut_orchestrator.azure.common import get_node_context, AzureNodeSchema # pylint: disable=E0401 +import makepkg from azurelinuxagent.common.version import AGENT_VERSION +from tests_e2e.scenarios.lib.agent_test import AgentTest +from tests_e2e.scenarios.lib.agent_test_context import AgentTestContext +from tests_e2e.scenarios.lib.identifiers import VmIdentifier +from tests_e2e.scenarios.lib.logging import log -class AgentTestScenario(object): +class AgentLisaTestContext(AgentTestContext): """ - Instances of this class are used to execute Agent test scenarios. It also provides facilities to execute commands over SSH. + Execution context for LISA tests. """ + def __init__(self, vm: VmIdentifier, node: Node): + super().__init__( + vm=vm, + paths=AgentTestContext.Paths(remote_working_directory=Path('/home')/node.connection_info['username']), + connection=AgentTestContext.Connection( + ip_address=node.connection_info['address'], + username=node.connection_info['username'], + private_key_file=node.connection_info['private_key_file'], + ssh_port=node.connection_info['port']) + ) + self._node = node - class Context: - """ - Execution context for test scenarios, this information is passed to test scenarios by AgentTestScenario.execute() - """ - subscription_id: str - resource_group_name: str - vm_name: str - test_source_directory: Path - working_directory: Path - node_home_directory: Path + @property + def node(self) -> Node: + return self._node + + +class AgentTestSuite(TestSuite): + """ + Base class for Agent test suites. It provides facilities for setup, execution of tests and reporting results. Derived + classes use the execute() method to run the tests in their corresponding suites. + """ + def __init__(self, metadata: TestSuiteMetadata) -> None: + super().__init__(metadata) + # The context is initialized by execute() + self.__context: AgentLisaTestContext = None - def __init__(self, node: Node, log: Logger) -> None: - self._node: Node = node - self._log: Logger = log + @property + def context(self) -> AgentLisaTestContext: + if self.__context is None: + raise Exception("The context for the AgentTestSuite has not been initialized") + return self.__context + def _set_context(self, node: Node): node_context = get_node_context(node) - self._context: AgentTestScenario.Context = AgentTestScenario.Context() - self._context.subscription_id = node.features._platform.subscription_id - self._context.resource_group_name = node_context.resource_group_name - self._context.vm_name = node_context.vm_name - self._context.test_source_directory = AgentTestScenario._get_test_source_directory() - self._context.working_directory = Path().home()/"waagent-tmp" - self._context.node_home_directory = Path('/home')/node.connection_info['username'] + runbook = node.capability.get_extended_runbook(AzureNodeSchema, AZURE) - @staticmethod - def _get_test_source_directory() -> Path: - """ - Returns the root directory of the source code for the end-to-end tests (".../WALinuxAgent/tests_e2e") - """ - path = Path(__file__) - while path.name != '': - if path.name == "tests_e2e": - return path - path = path.parent - raise Exception("Can't find the test root directory (tests_e2e)") + self.__context = AgentLisaTestContext( + VmIdentifier( + location=runbook.location, + subscription=node.features._platform.subscription_id, + resource_group=node_context.resource_group_name, + name=node_context.vm_name + ), + node + ) def _setup(self) -> None: """ - Prepares the test scenario for execution + Prepares the test suite for execution """ - self._log.info(f"Test Node: {self._context.vm_name}") - self._log.info(f"Resource Group: {self._context.resource_group_name}") - self._log.info(f"Working directory: {self._context.working_directory}...") + log.info("Test Node: %s", self.context.vm.name) + log.info("Resource Group: %s", self.context.vm.resource_group) + log.info("Working directory: %s", self.context.working_directory) - if self._context.working_directory.exists(): - self._log.info(f"Removing existing working directory: {self._context.working_directory}...") + if self.context.working_directory.exists(): + log.info("Removing existing working directory: %s", self.context.working_directory) try: - rmtree(self._context.working_directory.as_posix()) + rmtree(self.context.working_directory.as_posix()) except Exception as exception: - self._log.warning(f"Failed to remove the working directory: {exception}") - self._context.working_directory.mkdir() + log.warning("Failed to remove the working directory: %s", exception) + self.context.working_directory.mkdir() def _clean_up(self) -> None: """ - Cleans up any leftovers from the test scenario run. + Cleans up any leftovers from the test suite run. """ - self._log.info(f"Removing {self._context.working_directory}...") - rmtree(self._context.working_directory.as_posix(), ignore_errors=True) + try: + log.info("Removing %s", self.context.working_directory) + rmtree(self.context.working_directory.as_posix(), ignore_errors=True) + except: # pylint: disable=bare-except + log.exception("Failed to cleanup the test run") def _setup_node(self) -> None: """ @@ -108,15 +129,17 @@ def _build_agent_package(self) -> Path: """ Builds the agent package and returns the path to the package. """ - build_path = self._context.working_directory/"build" + build_path = self.context.working_directory/"build" + + log.info("Building agent package to %s", build_path) + + makepkg.run(agent_family="Test", output_directory=str(build_path), log=log) - self._log.info(f"Building agent package to {build_path}") - makepkg.run(agent_family="Test", output_directory=str(build_path), log=self._log) package_path = build_path/"eggs"/f"WALinuxAgent-{AGENT_VERSION}.zip" if not package_path.exists(): raise Exception(f"Can't find the agent package at {package_path}") - self._log.info(f"Agent package: {package_path}") + log.info("Agent package: %s", package_path) return package_path @@ -125,19 +148,19 @@ def _install_agent_on_node(self, agent_package: Path) -> None: Installs the given agent package on the test node. """ # The install script needs to unzip the agent package; ensure unzip is installed on the test node - self._log.info(f"Installing unzip on {self._context.vm_name}...") - self._node.os.install_packages("unzip") + log.info("Installing unzip tool on %s", self.context.node.name) + self.context.node.os.install_packages("unzip") - self._log.info(f"Installing {agent_package} on {self._context.vm_name}...") - agent_package_remote_path = self._context.node_home_directory/agent_package.name - self._log.info(f"Copying {agent_package} to {self._context.vm_name}:{agent_package_remote_path}") - self._node.shell.copy(agent_package, agent_package_remote_path) + log.info("Installing %s on %s", agent_package, self.context.node.name) + agent_package_remote_path = self.context.remote_working_directory / agent_package.name + log.info("Copying %s to %s:%s", agent_package, self.context.node.name, agent_package_remote_path) + self.context.node.shell.copy(agent_package, agent_package_remote_path) self.execute_script_on_node( - self._context.test_source_directory/"orchestrator"/"scripts"/"install-agent", + self.context.test_source_directory/"orchestrator"/"scripts"/"install-agent", parameters=f"--package {agent_package_remote_path} --version {AGENT_VERSION}", sudo=True) - self._log.info("The agent was installed successfully.") + log.info("The agent was installed successfully.") def _collect_node_logs(self) -> None: """ @@ -145,49 +168,107 @@ def _collect_node_logs(self) -> None: """ try: # Collect the logs on the test machine into a compressed tarball - self._log.info("Collecting logs on test machine [%s]...", self._node.name) - self.execute_script_on_node(self._context.test_source_directory/"orchestrator"/"scripts"/"collect-logs", sudo=True) + log.info("Collecting logs on test machine [%s]...", self.context.node.name) + self.execute_script_on_node(self.context.test_source_directory/"orchestrator"/"scripts"/"collect-logs", sudo=True) # Copy the tarball to the local logs directory - remote_path = self._context.node_home_directory/"logs.tgz" - local_path = Path.home()/'logs'/'vm-logs-{0}.tgz'.format(self._node.name) - self._log.info(f"Copying {self._node.name}:{remote_path} to {local_path}") - self._node.shell.copy_back(remote_path, local_path) - except Exception as e: - self._log.warning(f"Failed to collect logs from the test machine: {e}") + remote_path = self.context.remote_working_directory / "logs.tgz" + local_path = Path.home()/'logs'/'vm-logs-{0}.tgz'.format(self.context.node.name) + log.info("Copying %s:%s to %s", self.context.node.name, remote_path, local_path) + self.context.node.shell.copy_back(remote_path, local_path) + except: # pylint: disable=bare-except + log.exception("Failed to collect logs from the test machine") - def execute(self, scenario: Callable[[Context], None]) -> None: + def execute(self, node: Node, test_suite: List[Type[AgentTest]]) -> None: """ - Executes the given scenario + Executes each of the AgentTests in the given List. Note that 'test_suite' is a list of test classes, rather than + instances of the test class (this method will instantiate each of these test classes). """ + self._set_context(node) + + log.info("") + log.info("**************************************** [Setup] ****************************************") + log.info("") + + failed: List[str] = [] + try: self._setup() + try: self._setup_node() - scenario(self._context) + + log.info("") + log.info("**************************************** [%s] ****************************************", self._metadata.full_name) + log.info("") + + results: List[str] = [] + + for test in test_suite: + try: + log.info("******************** [%s]", test.__name__) + log.info("") + + test(self.context).run() + + result = f"[Passed] {test.__name__}" + + log.info("") + log.info("******************** %s", result) + log.info("") + + results.append(result) + except: # pylint: disable=bare-except + result = f"[Failed] {test.__name__}" + + log.info("") + log.exception("******************** %s\n", result) + log.info("") + + results.append(result) + failed.append(test.__name__) + + log.info("**************************************** [Test Results] ****************************************") + log.info("") + for r in results: + log.info("\t%s", r) + log.info("") + finally: self._collect_node_logs() + finally: self._clean_up() + # Fail the entire test suite if any test failed + assert_that(failed).described_as("One or more tests failed").is_length(0) + def execute_script_on_node(self, script_path: Path, parameters: str = "", sudo: bool = False) -> int: """ Executes the given script on the test node; if 'sudo' is True, the script is executed using the sudo command. """ custom_script_builder = CustomScriptBuilder(script_path.parent, [script_path.name]) - custom_script = self._node.tools[custom_script_builder] + custom_script = self.context.node.tools[custom_script_builder] if parameters == '': command_line = f"{script_path}" else: command_line = f"{script_path} {parameters}" - self._log.info(f"Executing [{command_line}]") + log.info("Executing [%s]", command_line) + result = custom_script.run(parameters=parameters, sudo=sudo) - # LISA appends stderr to stdout so no need to check for stderr if result.exit_code != 0: - raise Exception(f"Command [{command_line}] failed.\n{result.stdout}") + output = result.stdout if result.stderr == "" else f"{result.stdout}\n{result.stderr}" + raise Exception(f"[{command_line}] failed:\n{output}") + + if result.stdout != "": + separator = "\n" if "\n" in result.stdout else " " + log.info("stdout:%s%s", separator, result.stdout) + if result.stderr != "": + separator = "\n" if "\n" in result.stderr else " " + log.error("stderr:%s%s", separator, result.stderr) return result.exit_code diff --git a/tests_e2e/orchestrator/scripts/run-scenarios b/tests_e2e/orchestrator/scripts/run-scenarios index 43bc43a856..8eecec40d9 100755 --- a/tests_e2e/orchestrator/scripts/run-scenarios +++ b/tests_e2e/orchestrator/scripts/run-scenarios @@ -41,4 +41,4 @@ lisa \ --log_path "$HOME/logs" \ --working_path "$HOME/logs" \ -v subscription_id:"$SUBSCRIPTION_ID" \ - -v identity_file:"$HOME/.ssh/id_rsa.pub" + -v identity_file:"$HOME/.ssh/id_rsa" diff --git a/tests_e2e/scenarios/lib/BaseExtensionTestClass.py b/tests_e2e/scenarios/lib/BaseExtensionTestClass.py deleted file mode 100644 index 0a7cf71b0c..0000000000 --- a/tests_e2e/scenarios/lib/BaseExtensionTestClass.py +++ /dev/null @@ -1,113 +0,0 @@ -import time -from typing import List - -from azure.core.polling import LROPoller - -from tests_e2e.scenarios.lib.azure_models import ComputeManager -from tests_e2e.scenarios.lib.logging_utils import LoggingHandler -from tests_e2e.scenarios.lib.models import ExtensionMetaData, get_vm_data_from_env - - -class BaseExtensionTestClass(LoggingHandler): - - def __init__(self, extension_data: ExtensionMetaData): - super().__init__() - self.__extension_data = extension_data - self.__vm_data = get_vm_data_from_env() - self.__compute_manager = ComputeManager().compute_manager - - def get_ext_props(self, settings=None, protected_settings=None, auto_upgrade_minor_version=True, - force_update_tag=None): - - return self.__compute_manager.get_ext_props( - extension_data=self.__extension_data, - settings=settings, - protected_settings=protected_settings, - auto_upgrade_minor_version=auto_upgrade_minor_version, - force_update_tag=force_update_tag - ) - - def run(self, ext_props: List, remove: bool = True, continue_on_error: bool = False): - - def __add_extension(): - extension: LROPoller = self.__compute_manager.extension_func.begin_create_or_update( - self.__vm_data.rg_name, - self.__vm_data.name, - self.__extension_data.name, - ext_prop - ) - self.log.info("Add extension: {0}".format(extension.result(timeout=5 * 60))) - - def __remove_extension(): - self.__compute_manager.extension_func.begin_delete( - self.__vm_data.rg_name, - self.__vm_data.name, - self.__extension_data.name - ).result() - self.log.info(f"Delete vm extension {self.__extension_data.name} successful") - - def _retry_on_retryable_error(func): - retry = 1 - while retry < 5: - try: - func() - break - except Exception as err_: - if "RetryableError" in str(err_) and retry < 5: - self.log.warning(f"({retry}/5) Ran into RetryableError, retrying in 30 secs: {err_}") - time.sleep(30) - retry += 1 - continue - raise - - try: - for ext_prop in ext_props: - try: - _retry_on_retryable_error(__add_extension) - # Validate success from instance view - _retry_on_retryable_error(self.validate_ext) - except Exception as err: - if continue_on_error: - self.log.exception("Ran into error but ignoring it as asked: {0}".format(err)) - continue - else: - raise - finally: - # Always try to delete extensions if asked to remove even on errors - if remove: - _retry_on_retryable_error(__remove_extension) - - def validate_ext(self): - """ - Validate if the extension operation was successful from the Instance View - :raises: Exception if either unable to fetch instance view or if extension not successful - """ - retry = 0 - max_retry = 3 - ext_instance_view = None - status = None - - while retry < max_retry: - try: - ext_instance_view = self.__compute_manager.get_extension_instance_view(self.__extension_data.name) - if ext_instance_view is None: - raise Exception("Extension not found") - elif not ext_instance_view.instance_view: - raise Exception("Instance view not present") - elif not ext_instance_view.instance_view.statuses or len(ext_instance_view.instance_view.statuses) < 1: - raise Exception("Instance view status not present") - else: - status = ext_instance_view.instance_view.statuses[0].code - status_message = ext_instance_view.instance_view.statuses[0].message - self.log.info('Extension Status: \n\tCode: [{0}]\n\tMessage: {1}'.format(status, status_message)) - break - except Exception as err: - self.log.exception(f"Ran into error: {err}") - retry += 1 - if retry < max_retry: - self.log.info("Retrying in 30 secs") - time.sleep(30) - raise - - if 'succeeded' not in status: - raise Exception(f"Extension did not succeed. Last Instance view: {ext_instance_view}") diff --git a/tests_e2e/scenarios/lib/CustomScriptExtension.py b/tests_e2e/scenarios/lib/CustomScriptExtension.py deleted file mode 100644 index 369c710124..0000000000 --- a/tests_e2e/scenarios/lib/CustomScriptExtension.py +++ /dev/null @@ -1,29 +0,0 @@ -import uuid - -from tests_e2e.scenarios.lib.BaseExtensionTestClass import BaseExtensionTestClass -from tests_e2e.scenarios.lib.models import ExtensionMetaData - - -class CustomScriptExtension(BaseExtensionTestClass): - META_DATA = ExtensionMetaData( - publisher='Microsoft.Azure.Extensions', - ext_type='CustomScript', - version="2.1" - ) - - def __init__(self, extension_name: str): - extension_data = self.META_DATA - extension_data.name = extension_name - super().__init__(extension_data) - - -def add_cse(): - # Install and remove CSE - cse = CustomScriptExtension(extension_name="testCSE") - - ext_props = [ - cse.get_ext_props(settings={'commandToExecute': f"echo \'Hello World! {uuid.uuid4()} \'"}), - cse.get_ext_props(settings={'commandToExecute': "echo \'Hello again\'"}) - ] - - cse.run(ext_props=ext_props) \ No newline at end of file diff --git a/tests_e2e/scenarios/lib/agent_test.py b/tests_e2e/scenarios/lib/agent_test.py new file mode 100644 index 0000000000..6bbb8eaede --- /dev/null +++ b/tests_e2e/scenarios/lib/agent_test.py @@ -0,0 +1,54 @@ +#!/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. +# + +import sys + +from abc import ABC, abstractmethod + +from tests_e2e.scenarios.lib.agent_test_context import AgentTestContext +from tests_e2e.scenarios.lib.logging import log + + +class AgentTest(ABC): + """ + Defines the interface for agent tests, which are simply constructed from an AgentTestContext and expose a single method, + run(), to execute the test. + """ + def __init__(self, context: AgentTestContext): + self._context = context + + @abstractmethod + def run(self): + pass + + @classmethod + def run_from_command_line(cls): + """ + Convenience method to execute the test when it is being invoked directly from the command line (as opposed as + being invoked from a test framework or library. + """ + try: + cls(AgentTestContext.from_args()).run() + except SystemExit: # Bad arguments + pass + except: # pylint: disable=bare-except + log.exception("Test failed") + sys.exit(1) + + sys.exit(0) diff --git a/tests_e2e/scenarios/lib/agent_test_context.py b/tests_e2e/scenarios/lib/agent_test_context.py new file mode 100644 index 0000000000..b35e93a80d --- /dev/null +++ b/tests_e2e/scenarios/lib/agent_test_context.py @@ -0,0 +1,162 @@ +# 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. +# +import argparse +import os + +from pathlib import Path + +import tests_e2e +from tests_e2e.scenarios.lib.identifiers import VmIdentifier + + +class AgentTestContext: + """ + Execution context for agent tests. Defines the test VM, working directories and connection info for the tests. + """ + + class Paths: + # E1101: Instance of 'list' has no '_path' member (no-member) + DEFAULT_TEST_SOURCE_DIRECTORY = Path(tests_e2e.__path__._path[0]) # pylint: disable=E1101 + DEFAULT_WORKING_DIRECTORY = Path().home() / "waagent-tmp" + + def __init__( + self, + remote_working_directory: Path, + test_source_directory: Path = DEFAULT_TEST_SOURCE_DIRECTORY, + working_directory: Path = DEFAULT_WORKING_DIRECTORY + ): + self._test_source_directory: Path = test_source_directory + self._working_directory: Path = working_directory + self._remote_working_directory: Path = remote_working_directory + + class Connection: + DEFAULT_SSH_PORT = 22 + + def __init__( + self, + ip_address: str, + username: str, + private_key_file: Path, + ssh_port: int = DEFAULT_SSH_PORT + ): + self._ip_address: str = ip_address + self._username: str = username + self._private_key_file: Path = private_key_file + self._ssh_port: int = ssh_port + + def __init__(self, vm: VmIdentifier, paths: Paths, connection: Connection): + self._vm: VmIdentifier = vm + self._paths = paths + self._connection = connection + + @property + def vm(self) -> VmIdentifier: + """ + The test VM (the VM on which the tested Agent is running) + """ + return self._vm + + @property + def vm_ip_address(self) -> str: + """ + The IP address of the test VM + """ + return self._connection._ip_address + + @property + def test_source_directory(self) -> Path: + """ + Root directory for the source code of the tests. Used to build paths to specific scripts. + """ + return self._paths._test_source_directory + + @property + def working_directory(self) -> Path: + """ + Tests create temporary files under this directory + """ + return self._paths._working_directory + + @property + def remote_working_directory(self) -> Path: + """ + Tests create temporary files under this directory on the test VM + """ + return self._paths._remote_working_directory + + @property + def username(self) -> str: + """ + The username to use for SSH connections + """ + return self._connection._username + + @property + def private_key_file(self) -> Path: + """ + The file containing the private SSH key for the username + """ + return self._connection._private_key_file + + @property + def ssh_port(self) -> int: + """ + Port for SSH connections + """ + return self._connection._ssh_port + + @staticmethod + def from_args(): + """ + Creates an AgentTestContext from the command line arguments. + """ + parser = argparse.ArgumentParser() + parser.add_argument('-g', '--group', required=True) + parser.add_argument('-l', '--location', required=True) + parser.add_argument('-s', '--subscription', required=True) + parser.add_argument('-vm', '--vm', required=True) + + parser.add_argument('-rw', '--remote-working-directory', dest="remote_working_directory", required=False, default=str(Path('/home')/os.getenv("USER"))) + parser.add_argument('-t', '--test-source-directory', dest="test_source_directory", required=False, default=str(AgentTestContext.Paths.DEFAULT_TEST_SOURCE_DIRECTORY)) + parser.add_argument('-w', '--working-directory', dest="working_directory", required=False, default=str(AgentTestContext.Paths.DEFAULT_WORKING_DIRECTORY)) + + parser.add_argument('-a', '--ip-address', dest="ip_address", required=False) # Use the vm name as default + parser.add_argument('-u', '--username', required=False, default=os.getenv("USER")) + parser.add_argument('-k', '--private-key-file', dest="private_key_file", required=False, default=Path.home()/".ssh"/"id_rsa") + parser.add_argument('-p', '--ssh-port', dest="ssh_port", required=False, default=AgentTestContext.Connection.DEFAULT_SSH_PORT) + + args = parser.parse_args() + + working_directory = Path(args.working_directory) + if not working_directory.exists(): + working_directory.mkdir(exist_ok=True) + + return AgentTestContext( + vm=VmIdentifier( + location=args.location, + subscription=args.subscription, + resource_group=args.group, + name=args.vm), + paths=AgentTestContext.Paths( + remote_working_directory=Path(args.remote_working_directory), + test_source_directory=Path(args.test_source_directory), + working_directory=working_directory), + connection=AgentTestContext.Connection( + ip_address=args.ip_address if args.ip_address is not None else args.vm, + username=args.username, + private_key_file=Path(args.private_key_file), + ssh_port=args.ssh_port)) diff --git a/tests_e2e/scenarios/lib/azure_models.py b/tests_e2e/scenarios/lib/azure_models.py deleted file mode 100644 index 9a9c7a15bf..0000000000 --- a/tests_e2e/scenarios/lib/azure_models.py +++ /dev/null @@ -1,239 +0,0 @@ -import time -from abc import ABC, abstractmethod -from builtins import TimeoutError -from typing import List - -from azure.core.exceptions import HttpResponseError -from azure.core.polling import LROPoller -from azure.identity import DefaultAzureCredential -from azure.mgmt.compute import ComputeManagementClient -from azure.mgmt.compute.models import VirtualMachineExtension, VirtualMachineScaleSetExtension, \ - VirtualMachineInstanceView, VirtualMachineScaleSetInstanceView, VirtualMachineExtensionInstanceView -from azure.mgmt.resource import ResourceManagementClient -from msrestazure.azure_exceptions import CloudError - -from tests_e2e.scenarios.lib.logging_utils import LoggingHandler -from tests_e2e.scenarios.lib.models import get_vm_data_from_env, VMModelType, VMMetaData - - -class AzureComputeBaseClass(ABC, LoggingHandler): - - def __init__(self): - super().__init__() - self.__vm_data = get_vm_data_from_env() - self.__compute_client = None - self.__resource_client = None - - @property - def vm_data(self) -> VMMetaData: - return self.__vm_data - - @property - def compute_client(self) -> ComputeManagementClient: - if self.__compute_client is None: - self.__compute_client = ComputeManagementClient( - credential=DefaultAzureCredential(), - subscription_id=self.vm_data.sub_id - ) - return self.__compute_client - - @property - def resource_client(self) -> ResourceManagementClient: - if self.__resource_client is None: - self.__resource_client = ResourceManagementClient( - credential=DefaultAzureCredential(), - subscription_id=self.vm_data.sub_id - ) - return self.__resource_client - - @property - @abstractmethod - def vm_func(self): - pass - - @property - @abstractmethod - def extension_func(self): - pass - - @abstractmethod - def get_vm_instance_view(self): - pass - - @abstractmethod - def get_extensions(self): - pass - - @abstractmethod - def get_extension_instance_view(self, extension_name): - pass - - @abstractmethod - def get_ext_props(self, extension_data, settings=None, protected_settings=None, auto_upgrade_minor_version=True, - force_update_tag=None): - pass - - @abstractmethod - def restart(self, timeout=5): - pass - - def _run_azure_op_with_retry(self, get_func): - max_retries = 3 - retries = max_retries - while retries > 0: - try: - ext = get_func() - return ext - except (CloudError, HttpResponseError) as ce: - if retries > 0: - self.log.exception(f"Got Azure error: {ce}") - self.log.warning("...retrying [{0} attempts remaining]".format(retries)) - retries -= 1 - time.sleep(30 * (max_retries - retries)) - else: - raise - - -class VirtualMachineHelper(AzureComputeBaseClass): - - def __init__(self): - super().__init__() - - @property - def vm_func(self): - return self.compute_client.virtual_machines - - @property - def extension_func(self): - return self.compute_client.virtual_machine_extensions - - def get_vm_instance_view(self) -> VirtualMachineInstanceView: - return self._run_azure_op_with_retry(lambda: self.vm_func.get( - resource_group_name=self.vm_data.rg_name, - vm_name=self.vm_data.name, - expand="instanceView" - )) - - def get_extensions(self) -> List[VirtualMachineExtension]: - return self._run_azure_op_with_retry(lambda: self.extension_func.list( - resource_group_name=self.vm_data.rg_name, - vm_name=self.vm_data.name - )) - - def get_extension_instance_view(self, extension_name) -> VirtualMachineExtensionInstanceView: - return self._run_azure_op_with_retry(lambda: self.extension_func.get( - resource_group_name=self.vm_data.rg_name, - vm_name=self.vm_data.name, - vm_extension_name=extension_name, - expand="instanceView" - )) - - def get_ext_props(self, extension_data, settings=None, protected_settings=None, auto_upgrade_minor_version=True, - force_update_tag=None) -> VirtualMachineExtension: - return VirtualMachineExtension( - location=self.vm_data.location, - publisher=extension_data.publisher, - type_properties_type=extension_data.ext_type, - type_handler_version=extension_data.version, - auto_upgrade_minor_version=auto_upgrade_minor_version, - settings=settings, - protected_settings=protected_settings, - force_update_tag=force_update_tag - ) - - def restart(self, timeout=5): - self.log.info(f"Initiating restart of machine: {self.vm_data.name}") - poller : LROPoller = self._run_azure_op_with_retry(lambda: self.vm_func.begin_restart( - resource_group_name=self.vm_data.rg_name, - vm_name=self.vm_data.name - )) - poller.wait(timeout=timeout * 60) - if not poller.done(): - raise TimeoutError(f"Machine {self.vm_data.name} failed to restart after {timeout} mins") - self.log.info(f"Restarted machine: {self.vm_data.name}") - - -class VirtualMachineScaleSetHelper(AzureComputeBaseClass): - - def restart(self, timeout=5): - poller: LROPoller = self._run_azure_op_with_retry(lambda: self.vm_func.begin_restart( - resource_group_name=self.vm_data.rg_name, - vm_scale_set_name=self.vm_data.name - )) - poller.wait(timeout=timeout * 60) - if not poller.done(): - raise TimeoutError(f"ScaleSet {self.vm_data.name} failed to restart after {timeout} mins") - - def __init__(self): - super().__init__() - - @property - def vm_func(self): - return self.compute_client.virtual_machine_scale_set_vms - - @property - def extension_func(self): - return self.compute_client.virtual_machine_scale_set_extensions - - def get_vm_instance_view(self) -> VirtualMachineScaleSetInstanceView: - # Since this is a VMSS, return the instance view of the first VMSS VM. For the instance view of the complete VMSS, - # use the compute_client.virtual_machine_scale_sets function - - # https://docs.microsoft.com/en-us/python/api/azure-mgmt-compute/azure.mgmt.compute.v2019_12_01.operations.virtualmachinescalesetsoperations?view=azure-python - - for vm in self._run_azure_op_with_retry(lambda: self.vm_func.list(self.vm_data.rg_name, self.vm_data.name)): - try: - return self._run_azure_op_with_retry(lambda: self.vm_func.get_instance_view( - resource_group_name=self.vm_data.rg_name, - vm_scale_set_name=self.vm_data.name, - instance_id=vm.instance_id - )) - except Exception as err: - self.log.warning( - f"Unable to fetch instance view of VMSS VM: {vm}. Trying out other instances.\nError: {err}") - continue - - raise Exception(f"Unable to fetch instance view of any VMSS instances for {self.vm_data.name}") - - def get_extensions(self) -> List[VirtualMachineScaleSetExtension]: - return self._run_azure_op_with_retry(lambda: self.extension_func.list( - resource_group_name=self.vm_data.rg_name, - vm_scale_set_name=self.vm_data.name - )) - - def get_extension_instance_view(self, extension_name) -> VirtualMachineExtensionInstanceView: - return self._run_azure_op_with_retry(lambda: self.extension_func.get( - resource_group_name=self.vm_data.rg_name, - vm_scale_set_name=self.vm_data.name, - vmss_extension_name=extension_name, - expand="instanceView" - )) - - def get_ext_props(self, extension_data, settings=None, protected_settings=None, auto_upgrade_minor_version=True, - force_update_tag=None) -> VirtualMachineScaleSetExtension: - return VirtualMachineScaleSetExtension( - publisher=extension_data.publisher, - type_properties_type=extension_data.ext_type, - type_handler_version=extension_data.version, - auto_upgrade_minor_version=auto_upgrade_minor_version, - settings=settings, - protected_settings=protected_settings - ) - - -class ComputeManager: - """ - The factory class for setting the Helper class based on the setting. - """ - def __init__(self): - self.__vm_data = get_vm_data_from_env() - self.__compute_manager = None - - @property - def is_vm(self) -> bool: - return self.__vm_data.model_type == VMModelType.VM - - @property - def compute_manager(self): - if self.__compute_manager is None: - self.__compute_manager = VirtualMachineHelper() if self.is_vm else VirtualMachineScaleSetHelper() - return self.__compute_manager diff --git a/tests_e2e/scenarios/lib/identifiers.py b/tests_e2e/scenarios/lib/identifiers.py new file mode 100644 index 0000000000..48794140b3 --- /dev/null +++ b/tests_e2e/scenarios/lib/identifiers.py @@ -0,0 +1,63 @@ +# 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. +# + + +class VmIdentifier(object): + def __init__(self, location, subscription, resource_group, name): + """ + Represents the information that identifies a VM to the ARM APIs + """ + self.location = location + self.subscription: str = subscription + self.resource_group: str = resource_group + self.name: str = name + + def __str__(self): + return f"{self.resource_group}:{self.name}" + + +class VmExtensionIdentifier(object): + def __init__(self, publisher, ext_type, version): + """ + Represents the information that identifies an extension to the ARM APIs + + publisher - e.g. Microsoft.Azure.Extensions + type - e.g. CustomScript + version - e.g. 2.1, 2.* + name - arbitrary name for the extension ARM resource + """ + self.publisher: str = publisher + self.type: str = ext_type + self.version: str = version + + def __str__(self): + return f"{self.publisher}.{self.type}" + + +class VmExtensionIds(object): + """ + A set of extensions used by the tests, listed here for convenience (easy to reference them by name). + + Only the major version is specified, and the minor version is set to 0 (set autoUpgradeMinorVersion to True in the call to enable + to use the latest version) + """ + CustomScript: VmExtensionIdentifier = VmExtensionIdentifier(publisher='Microsoft.Azure.Extensions', ext_type='CustomScript', version="2.0") + # Older run command extension, still used by the Portal as of Dec 2022 + RunCommand: VmExtensionIdentifier = VmExtensionIdentifier(publisher='Microsoft.CPlat.Core', ext_type='RunCommandLinux', version="1.0") + # New run command extension, with support for multi-config + RunCommandHandler: VmExtensionIdentifier = VmExtensionIdentifier(publisher='Microsoft.CPlat.Core', ext_type='RunCommandHandlerLinux', version="1.0") + VmAccess: VmExtensionIdentifier = VmExtensionIdentifier(publisher='Microsoft.OSTCExtensions', ext_type='VMAccessForLinux', version="1.0") diff --git a/tests_e2e/scenarios/lib/logging.py b/tests_e2e/scenarios/lib/logging.py new file mode 100644 index 0000000000..2cb523d6bc --- /dev/null +++ b/tests_e2e/scenarios/lib/logging.py @@ -0,0 +1,37 @@ +# 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. +# +import logging + +# +# This module defines a single object, 'log', which test use for logging. +# +# When the test is invoked as part of a LISA test suite, 'log' references the LISA root logger. +# Otherwise, it references a new Logger named 'waagent'. +# + +log: logging.Logger = logging.getLogger("lisa") + +if not log.hasHandlers(): + log = logging.getLogger("waagent") + console_handler = logging.StreamHandler() + log.addHandler(console_handler) + +log.setLevel(logging.INFO) + +formatter = logging.Formatter('%(asctime)s.%(msecs)03d [%(levelname)s] %(message)s', datefmt="%Y-%m-%dT%H:%M:%SZ") +for handler in log.handlers: + handler.setFormatter(formatter) diff --git a/tests_e2e/scenarios/lib/logging_utils.py b/tests_e2e/scenarios/lib/logging_utils.py deleted file mode 100644 index 462f6a957a..0000000000 --- a/tests_e2e/scenarios/lib/logging_utils.py +++ /dev/null @@ -1,33 +0,0 @@ -# Create a base class -import logging - - -def get_logger(name): - return LoggingHandler(name).log - - -class LoggingHandler: - """ - Base class for Logging - """ - def __init__(self, name=None): - self.log = self.__setup_and_get_logger(name) - - def __setup_and_get_logger(self, name): - logger = logging.getLogger(name if name is not None else self.__class__.__name__) - if logger.hasHandlers(): - # Logging module inherits from base loggers if already setup, if a base logger found, reuse that - return logger - - # No handlers found for logger, set it up - # This logging format is easier to read on the DevOps UI - - # https://docs.microsoft.com/en-us/azure/devops/pipelines/scripts/logging-commands?view=azure-devops&tabs=bash#formatting-commands - log_formatter = logging.Formatter("##[%(levelname)s] [%(asctime)s] [%(module)s] {%(pathname)s:%(lineno)d} %(message)s", - datefmt="%Y-%m-%dT%H:%M:%S%z") - console_handler = logging.StreamHandler() - console_handler.setFormatter(log_formatter) - logger.addHandler(console_handler) - logger.setLevel(logging.INFO) - - return logger - diff --git a/tests_e2e/scenarios/lib/models.py b/tests_e2e/scenarios/lib/models.py deleted file mode 100644 index a9e3e8cf01..0000000000 --- a/tests_e2e/scenarios/lib/models.py +++ /dev/null @@ -1,135 +0,0 @@ -import os -from enum import Enum, auto -from typing import List - - -class VMModelType(Enum): - VM = auto() - VMSS = auto() - - -class ExtensionMetaData: - def __init__(self, publisher: str, ext_type: str, version: str, ext_name: str = ""): - self.__publisher = publisher - self.__ext_type = ext_type - self.__version = version - self.__ext_name = ext_name - - @property - def publisher(self) -> str: - return self.__publisher - - @property - def ext_type(self) -> str: - return self.__ext_type - - @property - def version(self) -> str: - return self.__version - - @property - def name(self): - return self.__ext_name - - @name.setter - def name(self, ext_name): - self.__ext_name = ext_name - - @property - def handler_name(self): - return f"{self.publisher}.{self.ext_type}" - - -class VMMetaData: - - def __init__(self, vm_name: str, rg_name: str, sub_id: str, location: str, admin_username: str, - ips: List[str] = None): - self.__vm_name = vm_name - self.__rg_name = rg_name - self.__sub_id = sub_id - self.__location = location - self.__admin_username = admin_username - - vm_ips, vmss_ips = _get_ips(admin_username) - # By default assume the test is running on a VM - self.__type = VMModelType.VM - self.__ips = vm_ips - if any(vmss_ips): - self.__type = VMModelType.VMSS - self.__ips = vmss_ips - - if ips is not None: - self.__ips = ips - - print(f"IPs: {self.__ips}") - - @property - def name(self) -> str: - return self.__vm_name - - @property - def rg_name(self) -> str: - return self.__rg_name - - @property - def location(self) -> str: - return self.__location - - @property - def sub_id(self) -> str: - return self.__sub_id - - @property - def admin_username(self): - return self.__admin_username - - @property - def ips(self) -> List[str]: - return self.__ips - - @property - def model_type(self): - return self.__type - - -def _get_ips(username) -> (list, list): - """ - Try fetching Ips from the files that we create via az-cli. - We do a best effort to fetch this from both orchestrator or the test VM. Its located in different locations on both - scenarios. - Returns: Tuple of (VmIps, VMSSIps). - """ - - vms, vmss = [], [] - orchestrator_path = os.path.join(os.environ['BUILD_SOURCESDIRECTORY'], "dcr") - test_vm_path = os.path.join("/home", username, "dcr") - - for ip_path in [orchestrator_path, test_vm_path]: - - vm_ip_path = os.path.join(ip_path, ".vm_ips") - if os.path.exists(vm_ip_path): - with open(vm_ip_path, 'r') as vm_ips: - vms.extend(ip.strip() for ip in vm_ips.readlines()) - - vmss_ip_path = os.path.join(ip_path, ".vmss_ips") - if os.path.exists(vmss_ip_path): - with open(vmss_ip_path, 'r') as vmss_ips: - vmss.extend(ip.strip() for ip in vmss_ips.readlines()) - - return vms, vmss - - -def get_vm_data_from_env() -> VMMetaData: - if get_vm_data_from_env.__instance is None: - get_vm_data_from_env.__instance = VMMetaData(vm_name=os.environ["VMNAME"], - rg_name=os.environ['RGNAME'], - sub_id=os.environ["SUBID"], - location=os.environ['LOCATION'], - admin_username=os.environ['ADMINUSERNAME']) - - - return get_vm_data_from_env.__instance - - -get_vm_data_from_env.__instance = None - diff --git a/tests_e2e/scenarios/lib/retry.py b/tests_e2e/scenarios/lib/retry.py new file mode 100644 index 0000000000..1b78f0a13b --- /dev/null +++ b/tests_e2e/scenarios/lib/retry.py @@ -0,0 +1,41 @@ +# 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. +# +import time + +from typing import Callable, Any + +from tests_e2e.scenarios.lib.logging import log + + +def execute_with_retry(operation: Callable[[], Any]) -> Any: + """ + Some Azure errors (e.g. throttling) are retryable; this method attempts the given operation retrying a few times + (after a short delay) if the error includes the string "RetryableError" + """ + attempts = 3 + while attempts > 0: + attempts -= 1 + try: + return operation() + except Exception as e: + # TODO: Do we need to retry on msrestazure.azure_exceptions.CloudError? + if "RetryableError" not in str(e) or attempts == 0: + raise + log.warning("The operation failed with a RetryableError, retrying in 30 secs. Error: %s", e) + time.sleep(30) + + diff --git a/tests_e2e/scenarios/lib/shell.py b/tests_e2e/scenarios/lib/shell.py new file mode 100644 index 0000000000..894ba90ca2 --- /dev/null +++ b/tests_e2e/scenarios/lib/shell.py @@ -0,0 +1,53 @@ +#!/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. +# +from subprocess import Popen, PIPE +from typing import Any + + +class CommandError(Exception): + """ + Exception raised by run_command when the command returns an error + """ + def __init__(self, command: Any, exit_code: int, stdout: str, stderr: str): + super().__init__(f"'{command}' failed (exit code: {exit_code}): {stderr}") + self.command: Any = command + self.exit_code: int = exit_code + self.stdout: str = stdout + self.stderr: str = stderr + + +def run_command(command: Any, shell=False) -> str: + """ + This function is a thin wrapper around Popen/communicate in the subprocess module. It executes the given command + and returns its stdout. If the command returns a non-zero exit code, the function raises a RunCommandException. + + Similarly to Popen, the 'command' can be a string or a list of strings, and 'shell' indicates whether to execute + the command through the shell. + + NOTE: The command's stdout and stderr are read as text streams. + """ + process = Popen(command, stdout=PIPE, stderr=PIPE, shell=shell, text=True) + + stdout, stderr = process.communicate() + + if process.returncode != 0: + raise CommandError(command, process.returncode, stdout, stderr) + + return stdout + diff --git a/tests_e2e/scenarios/lib/ssh_client.py b/tests_e2e/scenarios/lib/ssh_client.py new file mode 100644 index 0000000000..5e0afbd41a --- /dev/null +++ b/tests_e2e/scenarios/lib/ssh_client.py @@ -0,0 +1,46 @@ +#!/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. +# +from pathlib import Path + +from tests_e2e.scenarios.lib import shell + + +class SshClient(object): + def __init__(self, ip_address: str, username: str, private_key_file: Path, port: int = 22): + self._ip_address: str = ip_address + self._username:str = username + self._private_key_file: Path = private_key_file + self._port: int = port + + def run_command(self, command: str) -> str: + """ + Executes the given command over SSH and returns its stdout. If the command returns a non-zero exit code, + the function raises a RunCommandException. + """ + destination = f"ssh://{self._username}@{self._ip_address}:{self._port}" + + return shell.run_command(["ssh", "-o", "StrictHostKeyChecking=no", "-i", self._private_key_file, destination, command]) + + @staticmethod + def generate_ssh_key(private_key_file: Path): + """ + Generates an SSH key on the given Path + """ + shell.run_command(["ssh-keygen", "-m", "PEM", "-t", "rsa", "-b", "4096", "-q", "-N", "", "-f", str(private_key_file)]) + diff --git a/tests_e2e/scenarios/lib/virtual_machine.py b/tests_e2e/scenarios/lib/virtual_machine.py new file mode 100644 index 0000000000..6a1f76f961 --- /dev/null +++ b/tests_e2e/scenarios/lib/virtual_machine.py @@ -0,0 +1,143 @@ +# 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 some operations on virtual machines and scale sets (list extensions, restart, etc). +# + +from abc import ABC, abstractmethod +from builtins import TimeoutError +from typing import Any, List + +from azure.core.polling import LROPoller +from azure.identity import DefaultAzureCredential +from azure.mgmt.compute import ComputeManagementClient +from azure.mgmt.compute.models import VirtualMachineExtension, VirtualMachineScaleSetExtension, VirtualMachineInstanceView, VirtualMachineScaleSetInstanceView +from azure.mgmt.resource import ResourceManagementClient + +from tests_e2e.scenarios.lib.identifiers import VmIdentifier +from tests_e2e.scenarios.lib.logging import log +from tests_e2e.scenarios.lib.retry import execute_with_retry + + +class VirtualMachineBaseClass(ABC): + """ + Abstract base class for VirtualMachine and VmScaleSet. + + Defines the interface common to both classes and provides the implementation of some methods in that interface. + """ + def __init__(self, vm: VmIdentifier): + super().__init__() + self._identifier: VmIdentifier = vm + self._compute_client = ComputeManagementClient(credential=DefaultAzureCredential(), subscription_id=vm.subscription) + self._resource_client = ResourceManagementClient(credential=DefaultAzureCredential(), subscription_id=vm.subscription) + + @abstractmethod + def get_instance_view(self) -> Any: # Returns VirtualMachineInstanceView or VirtualMachineScaleSetInstanceView + """ + Retrieves the instance view of the virtual machine or scale set + """ + + @abstractmethod + def get_extensions(self) -> Any: # Returns List[VirtualMachineExtension] or List[VirtualMachineScaleSetExtension] + """ + Retrieves the extensions installed on the virtual machine or scale set + """ + + def restart(self, timeout=5 * 60) -> None: + """ + Restarts the virtual machine or scale set + """ + log.info("Initiating restart of %s", self._identifier) + + poller: LROPoller = execute_with_retry(self._begin_restart) + + poller.wait(timeout=timeout) + + if not poller.done(): + raise TimeoutError(f"Failed to restart {self._identifier.name} after {timeout} seconds") + + log.info("Restarted %s", self._identifier.name) + + @abstractmethod + def _begin_restart(self) -> LROPoller: + """ + Derived classes must provide the implementation for this method using their corresponding begin_restart() implementation + """ + + def __str__(self): + return f"{self._identifier}" + + +class VirtualMachine(VirtualMachineBaseClass): + def get_instance_view(self) -> VirtualMachineInstanceView: + log.info("Retrieving instance view for %s", self._identifier) + return execute_with_retry(self._compute_client.virtual_machines.get( + resource_group_name=self._identifier.resource_group, + vm_name=self._identifier.name, + expand="instanceView" + ).instance_view) + + def get_extensions(self) -> List[VirtualMachineExtension]: + log.info("Retrieving extensions for %s", self._identifier) + return execute_with_retry(self._compute_client.virtual_machine_extensions.list( + resource_group_name=self._identifier.resource_group, + vm_name=self._identifier.name)) + + def _begin_restart(self) -> LROPoller: + return self._compute_client.virtual_machines.begin_restart( + resource_group_name=self._identifier.resource_group, + vm_name=self._identifier.name) + + +class VmScaleSet(VirtualMachineBaseClass): + def get_instance_view(self) -> VirtualMachineScaleSetInstanceView: + log.info("Retrieving instance view for %s", self._identifier) + + # TODO: Revisit this implementation. Currently this method returns the instance view of the first VM instance available. + # For the instance view of the complete VMSS, use the compute_client.virtual_machine_scale_sets function + # https://docs.microsoft.com/en-us/python/api/azure-mgmt-compute/azure.mgmt.compute.v2019_12_01.operations.virtualmachinescalesetsoperations?view=azure-python + for vm in execute_with_retry(lambda: self._compute_client.virtual_machine_scale_set_vms.list(self._identifier.resource_group, self._identifier.name)): + try: + return execute_with_retry(lambda: self._compute_client.virtual_machine_scale_set_vms.get_instance_view( + resource_group_name=self._identifier.resource_group, + vm_scale_set_name=self._identifier.name, + instance_id=vm.instance_id)) + except Exception as e: + log.warning("Unable to retrieve instance view for scale set instance %s. Trying out other instances.\nError: %s", vm, e) + + raise Exception(f"Unable to retrieve instance view of any instances for scale set {self._identifier}") + + + @property + def vm_func(self): + return self._compute_client.virtual_machine_scale_set_vms + + @property + def extension_func(self): + return self._compute_client.virtual_machine_scale_set_extensions + + def get_extensions(self) -> List[VirtualMachineScaleSetExtension]: + log.info("Retrieving extensions for %s", self._identifier) + return execute_with_retry(self._compute_client.virtual_machine_scale_set_extensions.list( + resource_group_name=self._identifier.resource_group, + vm_scale_set_name=self._identifier.name)) + + def _begin_restart(self) -> LROPoller: + return self._compute_client.virtual_machine_scale_sets.begin_restart( + resource_group_name=self._identifier.resource_group, + vm_scale_set_name=self._identifier.name) diff --git a/tests_e2e/scenarios/lib/vm_extension.py b/tests_e2e/scenarios/lib/vm_extension.py new file mode 100644 index 0000000000..1a30ce8b5b --- /dev/null +++ b/tests_e2e/scenarios/lib/vm_extension.py @@ -0,0 +1,239 @@ +# 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 operations (enable, remove, etc) on single virtual machines (using +# class VmExtension) or virtual machine scale sets (using class VmssExtension). +# + +import uuid + +from abc import ABC, abstractmethod +from assertpy import assert_that, soft_assertions +from typing import Any, Callable, Dict, Type + +from azure.core.polling import LROPoller +from azure.mgmt.compute import ComputeManagementClient +from azure.mgmt.compute.models import VirtualMachineExtension, VirtualMachineScaleSetExtension, VirtualMachineExtensionInstanceView +from azure.identity import DefaultAzureCredential + +from tests_e2e.scenarios.lib.identifiers import VmIdentifier, VmExtensionIdentifier +from tests_e2e.scenarios.lib.logging import log +from tests_e2e.scenarios.lib.retry import execute_with_retry + + +_TIMEOUT = 5 * 60 # Timeout for extension operations (in seconds) + + +class _VmExtensionBaseClass(ABC): + """ + Abstract base class for VmExtension and VmssExtension. + + Implements the operations that are common to virtual machines and scale sets. Derived classes must provide the specific types and methods for the + virtual machine or scale set. + """ + def __init__(self, vm: VmIdentifier, extension: VmExtensionIdentifier, resource_name: str): + super().__init__() + self._vm: VmIdentifier = vm + self._identifier = extension + self._resource_name = resource_name + self._compute_client: ComputeManagementClient = ComputeManagementClient(credential=DefaultAzureCredential(), subscription_id=vm.subscription) + + def enable( + self, + settings: Dict[str, Any] = None, + protected_settings: Dict[str, Any] = None, + auto_upgrade_minor_version: bool = True, + force_update: bool = False, + force_update_tag: str = None + ) -> None: + """ + Performs an enable operation on the extension. + + NOTE: 'force_update' is not a parameter of the actual ARM API. It is provided for convenience: If set to True, + the 'force_update_tag' can be left unspecified and this method will generate a random tag. + """ + if force_update_tag is not None and not force_update: + raise ValueError("If force_update_tag is provided then force_update must be set to true") + + if force_update and force_update_tag is None: + force_update_tag = str(uuid.uuid4()) + + extension_parameters = self._ExtensionType( + publisher=self._identifier.publisher, + location=self._vm.location, + type_properties_type=self._identifier.type, + type_handler_version=self._identifier.version, + auto_upgrade_minor_version=auto_upgrade_minor_version, + settings=settings, + protected_settings=protected_settings, + force_update_tag=force_update_tag) + + # Hide the protected settings from logging + if protected_settings is not None: + extension_parameters.protected_settings = "*****[REDACTED]*****" + log.info("Enabling %s", self._identifier) + log.info("%s", extension_parameters) + # Now set the actual protected settings before invoking the extension + extension_parameters.protected_settings = protected_settings + + result: VirtualMachineExtension = execute_with_retry( + lambda: self._begin_create_or_update( + self._vm.resource_group, + self._vm.name, + self._resource_name, + extension_parameters + ).result(timeout=_TIMEOUT)) + + if result.provisioning_state != 'Succeeded': + raise Exception(f"Enable {self._identifier} failed. Provisioning state: {result.provisioning_state}") + log.info("Enable succeeded.") + + def get_instance_view(self) -> VirtualMachineExtensionInstanceView: # TODO: Check type for scale sets + """ + Retrieves the instance view of the extension + """ + log.info("Retrieving instance view for %s...", self._identifier) + + return execute_with_retry(lambda: self._get( + resource_group_name=self._vm.resource_group, + vm_name=self._vm.name, + vm_extension_name=self._resource_name, + expand="instanceView" + ).instance_view) + + def assert_instance_view( + self, + expected_status_code: str = "ProvisioningState/succeeded", + expected_version: str = None, + expected_message: str = None, + assert_function: Callable[[VirtualMachineExtensionInstanceView], None] = None + ) -> None: + """ + Asserts that the extension's instance view matches the given expected values. If 'expected_version' and/or 'expected_message' + are omitted, they are 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() + + with soft_assertions(): + if expected_version is not None: + # Compare only the major and minor versions (i.e. the first 2 items in the result of split()) + installed_version = instance_view.type_handler_version + assert_that(expected_version.split(".")[0:2]).described_as("Unexpected extension version").is_equal_to(installed_version.split(".")[0:2]) + + assert_that(instance_view.statuses).described_as(f"Expected 1 status, got: {instance_view.statuses}").is_length(1) + status = instance_view.statuses[0] + + if expected_message is not None: + assert_that(expected_message in status.message).described_as(f"{expected_message} should be in the InstanceView message ({status.message})").is_true() + + assert_that(status.code).described_as("InstanceView status code").is_equal_to(expected_status_code) + + if assert_function is not None: + assert_function(instance_view) + + log.info("The instance view matches the expected values") + + @abstractmethod + def delete(self) -> None: + """ + Performs a delete operation on the extension + """ + + @property + @abstractmethod + def _ExtensionType(self) -> Type: + """ + Type of the extension object for the virtual machine or scale set (i.e. VirtualMachineExtension or VirtualMachineScaleSetExtension) + """ + + @property + @abstractmethod + def _begin_create_or_update(self) -> Callable[[str, str, str, Any], LROPoller[Any]]: # "Any" can be VirtualMachineExtension or VirtualMachineScaleSetExtension + """ + The begin_create_or_update method for the virtual machine or scale set extension + """ + + @property + @abstractmethod + def _get(self) -> Any: # VirtualMachineExtension or VirtualMachineScaleSetExtension + """ + The get method for the virtual machine or scale set extension + """ + + def __str__(self): + return f"{self._identifier}" + + +class VmExtension(_VmExtensionBaseClass): + """ + Extension operations on a single virtual machine. + """ + @property + def _ExtensionType(self) -> Type: + return VirtualMachineExtension + + @property + def _begin_create_or_update(self) -> Callable[[str, str, str, VirtualMachineExtension], LROPoller[VirtualMachineExtension]]: + return self._compute_client.virtual_machine_extensions.begin_create_or_update + + @property + def _get(self) -> VirtualMachineExtension: + return self._compute_client.virtual_machine_extensions.get + + def delete(self) -> None: + log.info("Deleting %s", self._identifier) + + execute_with_retry(lambda: self._compute_client.virtual_machine_extensions.begin_delete( + self._vm.resource_group, + self._vm.name, + self._resource_name + ).wait(timeout=_TIMEOUT)) + + +class VmssExtension(_VmExtensionBaseClass): + """ + Extension operations on virtual machine scale sets. + """ + @property + def _ExtensionType(self) -> Type: + return VirtualMachineScaleSetExtension + + @property + def _begin_create_or_update(self) -> Callable[[str, str, str, VirtualMachineScaleSetExtension], LROPoller[VirtualMachineScaleSetExtension]]: + return self._compute_client.virtual_machine_scale_set_extensions.begin_create_or_update + + @property + def _get(self) -> VirtualMachineScaleSetExtension: + return self._compute_client.virtual_machine_scale_set_extensions.get + + def delete(self) -> None: # TODO: Implement this method + raise NotImplementedError() + + def delete_from_instance(self, instance_id: str) -> None: + log.info("Deleting %s from scale set instance %s", self._identifier, instance_id) + + execute_with_retry(lambda: self._compute_client.virtual_machine_scale_set_vm_extensions.begin_delete( + resource_group_name=self._vm.resource_group, + vm_scale_set_name=self._vm.name, + vm_extension_name=self._resource_name, + instance_id=instance_id + ).wait(timeout=_TIMEOUT)) + diff --git a/tests_e2e/scenarios/tests/bvts/custom_script.py b/tests_e2e/scenarios/tests/bvts/custom_script.py deleted file mode 100644 index 2d716223bb..0000000000 --- a/tests_e2e/scenarios/tests/bvts/custom_script.py +++ /dev/null @@ -1,45 +0,0 @@ -import argparse -import os -import uuid -import sys - -from tests_e2e.scenarios.lib.CustomScriptExtension import CustomScriptExtension - - -def main(subscription_id, resource_group_name, vm_name): - os.environ["VMNAME"] = vm_name - os.environ['RGNAME'] = resource_group_name - os.environ["SUBID"] = subscription_id - os.environ["SCENARIONAME"] = "BVT" - os.environ["LOCATION"] = "westus2" - os.environ["ADMINUSERNAME"] = "somebody" - os.environ["BUILD_SOURCESDIRECTORY"] = "/somewhere" - - cse = CustomScriptExtension(extension_name="testCSE") - - ext_props = [ - cse.get_ext_props(settings={'commandToExecute': f"echo \'Hello World! {uuid.uuid4()} \'"}), - cse.get_ext_props(settings={'commandToExecute': "echo \'Hello again\'"}) - ] - - cse.run(ext_props=ext_props) - - -if __name__ == "__main__": - try: - parser = argparse.ArgumentParser() - parser.add_argument('--subscription') - parser.add_argument('--group') - parser.add_argument('--vm') - - args = parser.parse_args() - - main(args.subscription, args.group, args.vm) - - except Exception as exception: - print(str(exception)) - sys.exit(1) - - sys.exit(0) - - diff --git a/tests_e2e/scenarios/tests/bvts/extension_operations.py b/tests_e2e/scenarios/tests/bvts/extension_operations.py new file mode 100755 index 0000000000..ae5e0c13b1 --- /dev/null +++ b/tests_e2e/scenarios/tests/bvts/extension_operations.py @@ -0,0 +1,79 @@ +#!/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. +# + +# +# BVT for extension operations (Install/Enable/Update/Uninstall). +# +# The test executes an older version of an extension, then updates it to a newer version, and lastly +# it removes it. The actual extension is irrelevant, but the test uses CustomScript for simplicity, +# since it's invocation is trivial and the entire extension workflow can be tested end-to-end by +# checking the message in the status produced by the extension. +# +import uuid + +from assertpy import assert_that + +from azure.core.exceptions import ResourceNotFoundError + +from tests_e2e.scenarios.lib.agent_test import AgentTest +from tests_e2e.scenarios.lib.identifiers import VmExtensionIds, VmExtensionIdentifier +from tests_e2e.scenarios.lib.logging import log +from tests_e2e.scenarios.lib.vm_extension import VmExtension + + +class ExtensionOperationsBvt(AgentTest): + def run(self): + custom_script_2_0 = VmExtension( + self._context.vm, + VmExtensionIds.CustomScript, + resource_name="CustomScript") + + custom_script_2_1 = VmExtension( + self._context.vm, + VmExtensionIdentifier(VmExtensionIds.CustomScript.publisher, VmExtensionIds.CustomScript.type, "2.1"), + resource_name="CustomScript") + + log.info("Installing %s", custom_script_2_0) + message = f"Hello {uuid.uuid4()}!" + custom_script_2_0.enable( + settings={ + 'commandToExecute': f"echo \'{message}\'" + }, + auto_upgrade_minor_version=False + ) + custom_script_2_0.assert_instance_view(expected_version="2.0", expected_message=message) + + log.info("Updating %s to %s", custom_script_2_0, custom_script_2_1) + message = f"Hello {uuid.uuid4()}!" + custom_script_2_1.enable( + settings={ + 'commandToExecute': f"echo \'{message}\'" + } + ) + custom_script_2_1.assert_instance_view(expected_version="2.1", expected_message=message) + + custom_script_2_1.delete() + + assert_that(custom_script_2_1.get_instance_view).\ + described_as("Fetching the instance view should fail after removing the extension").\ + raises(ResourceNotFoundError) + + +if __name__ == "__main__": + ExtensionOperationsBvt.run_from_command_line() diff --git a/tests_e2e/scenarios/tests/bvts/run_command.py b/tests_e2e/scenarios/tests/bvts/run_command.py new file mode 100755 index 0000000000..25258cef39 --- /dev/null +++ b/tests_e2e/scenarios/tests/bvts/run_command.py @@ -0,0 +1,89 @@ +#!/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. +# + +# +# BVT for RunCommand. +# +# Note that there are two incarnations of RunCommand (which are actually two different extensions): +# Microsoft.CPlat.Core.RunCommandHandlerLinux and Microsoft.CPlat.Core.RunCommandLinux. This test +# exercises both using the same strategy: execute the extension to create a file on the test VM, +# then fetch the contents of the file over SSH and compare against the known value. +# +import base64 +import uuid + +from assertpy import assert_that, soft_assertions +from typing import Callable, Dict + +from tests_e2e.scenarios.lib.agent_test import AgentTest +from tests_e2e.scenarios.lib.identifiers import VmExtensionIds +from tests_e2e.scenarios.lib.logging import log +from tests_e2e.scenarios.lib.ssh_client import SshClient +from tests_e2e.scenarios.lib.vm_extension import VmExtension + + +class RunCommandBvt(AgentTest): + class TestCase: + def __init__(self, extension: VmExtension, get_settings: Callable[[str], Dict[str, str]]): + self.extension = extension + self.get_settings = get_settings + + def run(self): + test_cases = [ + RunCommandBvt.TestCase( + VmExtension(self._context.vm, VmExtensionIds.RunCommand, resource_name="RunCommand"), + lambda s: { + "script": base64.standard_b64encode(bytearray(s, 'utf-8')).decode('utf-8') + }), + RunCommandBvt.TestCase( + VmExtension(self._context.vm, VmExtensionIds.RunCommandHandler, resource_name="RunCommandHandler"), + lambda s: { + "source": { + "script": s + } + }) + ] + + ssh_client = SshClient( + ip_address=self._context.vm_ip_address, + username=self._context.username, + private_key_file=self._context.private_key_file) + + with soft_assertions(): + for t in test_cases: + log.info("Test case: %s", t.extension) + + unique = str(uuid.uuid4()) + test_file = f"/tmp/waagent-test.{unique}" + script = f"echo '{unique}' > {test_file}" + log.info("Script to execute: %s", script) + + t.extension.enable(settings=t.get_settings(script)) + t.extension.assert_instance_view() + + log.info("Verifying contents of the file created by the extension") + contents = ssh_client.run_command(f"cat {test_file}").rstrip() # remove the \n + assert_that(contents).\ + described_as("Contents of the file created by the extension").\ + is_equal_to(unique) + log.info("The contents match") + + +if __name__ == "__main__": + RunCommandBvt.run_from_command_line() diff --git a/tests_e2e/scenarios/tests/bvts/vm_access.py b/tests_e2e/scenarios/tests/bvts/vm_access.py new file mode 100755 index 0000000000..36919e3f30 --- /dev/null +++ b/tests_e2e/scenarios/tests/bvts/vm_access.py @@ -0,0 +1,75 @@ +#!/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. +# + +# +# BVT for the VmAccess extension +# +# The test executes VmAccess to add a user and then verifies that an SSH connection to the VM can +# be established with that user's identity. +# +import uuid + +from assertpy import assert_that +from pathlib import Path + +from tests_e2e.scenarios.lib.agent_test import AgentTest +from tests_e2e.scenarios.lib.identifiers import VmExtensionIds +from tests_e2e.scenarios.lib.logging import log +from tests_e2e.scenarios.lib.ssh_client import SshClient + +from tests_e2e.scenarios.lib.vm_extension import VmExtension + + +class VmAccessBvt(AgentTest): + def run(self): + # Try to use a unique username for each test run (note that we truncate to 32 chars to + # comply with the rules for usernames) + log.info("Generating a new username and SSH key") + username: str = f"test-{uuid.uuid4()}"[0:32] + log.info("Username: %s", username) + + # Create an SSH key for the user and fetch the public key + private_key_file: Path = self._context.working_directory/f"{username}_rsa" + public_key_file: Path = self._context.working_directory/f"{username}_rsa.pub" + log.info("Generating SSH key as %s", private_key_file) + ssh: SshClient = SshClient(ip_address=self._context.vm_ip_address, username=username, private_key_file=private_key_file) + ssh.generate_ssh_key(private_key_file) + with public_key_file.open() as f: + public_key = f.read() + + # Invoke the extension + vm_access = VmExtension(self._context.vm, VmExtensionIds.VmAccess, resource_name="VmAccess") + vm_access.enable( + protected_settings={ + 'username': username, + 'ssh_key': public_key, + 'reset_ssh': 'false' + } + ) + vm_access.assert_instance_view() + + # Verify the user was added correctly by starting an SSH session to the VM + log.info("Verifying SSH connection to the test VM") + stdout = ssh.run_command("echo -n $USER") + assert_that(stdout).described_as("Output from SSH command").is_equal_to(username) + log.info("SSH command output ($USER): %s", stdout) + + +if __name__ == "__main__": + VmAccessBvt.run_from_command_line() diff --git a/tests_e2e/scenarios/testsuites/agent_bvt.py b/tests_e2e/scenarios/testsuites/agent_bvt.py index a63c3e34df..0b0383e99a 100644 --- a/tests_e2e/scenarios/testsuites/agent_bvt.py +++ b/tests_e2e/scenarios/testsuites/agent_bvt.py @@ -15,30 +15,34 @@ # limitations under the License. # -from tests_e2e.orchestrator.lib.agent_test_suite import AgentTestScenario -from tests_e2e.scenarios.tests.bvts import custom_script +from tests_e2e.orchestrator.lib.agent_test_suite import AgentTestSuite +from tests_e2e.scenarios.tests.bvts.extension_operations import ExtensionOperationsBvt +from tests_e2e.scenarios.tests.bvts.vm_access import VmAccessBvt +from tests_e2e.scenarios.tests.bvts.run_command import RunCommandBvt # E0401: Unable to import 'lisa' (import-error) from lisa import ( # pylint: disable=E0401 - Logger, Node, TestCaseMetadata, - TestSuite, TestSuiteMetadata, ) @TestSuiteMetadata(area="bvt", category="", description="Test suite for Agent BVTs") -class AgentBvt(TestSuite): +class AgentBvt(AgentTestSuite): """ Test suite for Agent BVTs """ @TestCaseMetadata(description="", priority=0) - def main(self, log: Logger, node: Node) -> None: - def tests(ctx: AgentTestScenario.Context) -> None: - custom_script.main(ctx.subscription_id, ctx.resource_group_name, ctx.vm_name) - - AgentTestScenario(node, log).execute(tests) + def main(self, node: Node) -> None: + self.execute( + node, + [ + ExtensionOperationsBvt, # Tests the basic operations (install, enable, update, uninstall) using CustomScript + RunCommandBvt, + VmAccessBvt + ] + ) From 1fe3de80ba4a1c5477400d0212bf61d3f3f32004 Mon Sep 17 00:00:00 2001 From: Norberto Arrieta Date: Fri, 6 Jan 2023 12:49:04 -0800 Subject: [PATCH 22/63] Disable DCR v2 Pipeline (#2722) Co-authored-by: narrieta --- dcr/azure-pipelines.yml | 8 -------- 1 file changed, 8 deletions(-) diff --git a/dcr/azure-pipelines.yml b/dcr/azure-pipelines.yml index 3fcffc6aef..c2bbe9d191 100644 --- a/dcr/azure-pipelines.yml +++ b/dcr/azure-pipelines.yml @@ -59,14 +59,6 @@ trigger: # no PR triggers pr: none -schedules: - - cron: "0 */8 * * *" # Run every 8 hours - displayName: Daily validation builds - branches: - include: - - develop - always: true - variables: - template: templates/vars.yml From e9f495d0c00bd553c71b9ecf89dea8d4d5f58765 Mon Sep 17 00:00:00 2001 From: maddieford <93676569+maddieford@users.noreply.github.com> Date: Mon, 9 Jan 2023 15:56:33 -0800 Subject: [PATCH 23/63] Log collector should not fetch full goal state with extensions (#2713) * Update version to dummy 1.0.0.0' * Revert version change * Update goalstate to take list of properties to process * Update protocol to not process extensions in goal state update * Update logcollector to not process extensions when updating goal state * Remove comments * Remove import enum * Update parameter name to goalstate_properties * Add default value for goalstate_properties * Use integers and bitwise operations for goal state properties * Initialize protocol for logcollector with only necessary goal state properties * Separate update into reset and made goal state properties a private member * Remove goal_state_properties param from _detect_protocol * Update tests to remove force_update * Remove unused import * Remove update_goal_State from wire protocol * Remove update_goal_State from wire protocol * Separate extensionsconfig and certificates property * Address PR comments * Add flag to determine if goal state should be init * Add test case for goal_state_properties * Correct pylint errors * Add reset_goal_state test with goal_state_properties * Add reset test cases * Remove Certificates property in GoalState * Revert certs change --- azurelinuxagent/common/logcollector.py | 5 +- azurelinuxagent/common/protocol/goal_state.py | 131 ++++++++++++------ azurelinuxagent/common/protocol/util.py | 8 +- azurelinuxagent/common/protocol/wire.py | 39 ++++-- azurelinuxagent/daemon/main.py | 5 +- azurelinuxagent/ga/update.py | 2 +- tests/common/test_event.py | 4 +- tests/ga/test_extension.py | 72 +++++----- tests/ga/test_monitor.py | 2 +- tests/ga/test_multi_config_extension.py | 20 +-- tests/protocol/test_goal_state.py | 74 +++++++++- tests/protocol/test_hostplugin.py | 8 +- tests/protocol/test_wire.py | 45 ++++-- 13 files changed, 285 insertions(+), 130 deletions(-) diff --git a/azurelinuxagent/common/logcollector.py b/azurelinuxagent/common/logcollector.py index b0da848fc5..393333c962 100644 --- a/azurelinuxagent/common/logcollector.py +++ b/azurelinuxagent/common/logcollector.py @@ -34,6 +34,7 @@ # Please note: be careful when adding agent dependencies in this module. # This module uses its own logger and logs to its own file, not to the agent log. +from azurelinuxagent.common.protocol.goal_state import GoalStateProperties from azurelinuxagent.common.protocol.util import get_protocol_util _EXTENSION_LOG_DIR = get_ext_log_dir() @@ -117,8 +118,8 @@ def _set_resource_usage_cgroups(cpu_cgroup_path, memory_cgroup_path): @staticmethod def _initialize_telemetry(): - protocol = get_protocol_util().get_protocol() - protocol.client.update_goal_state(force_update=True) + protocol = get_protocol_util().get_protocol(init_goal_state=False) + protocol.client.reset_goal_state(goalstate_properties=GoalStateProperties.RoleConfig | GoalStateProperties.HostingEnv) # Initialize the common parameters for telemetry events initialize_event_logger_vminfo_common_parameters(protocol) diff --git a/azurelinuxagent/common/protocol/goal_state.py b/azurelinuxagent/common/protocol/goal_state.py index 664b70ef1b..ed96159c8a 100644 --- a/azurelinuxagent/common/protocol/goal_state.py +++ b/azurelinuxagent/common/protocol/goal_state.py @@ -48,6 +48,18 @@ _GET_GOAL_STATE_MAX_ATTEMPTS = 6 +class GoalStateProperties(object): + """ + Enum for defining the properties that we fetch in the goal state + """ + RoleConfig = 0x1 + HostingEnv = 0x2 + SharedConfig = 0x4 + ExtensionsGoalState = 0x8 + RemoteAccessInfo = 0x10 + All = RoleConfig | HostingEnv | SharedConfig | ExtensionsGoalState | RemoteAccessInfo + + class GoalStateInconsistentError(ProtocolError): """ Indicates an inconsistency in the goal state (e.g. missing tenant certificate) @@ -57,7 +69,7 @@ def __init__(self, msg, inner=None): class GoalState(object): - def __init__(self, wire_client, silent=False): + def __init__(self, wire_client, goal_state_properties=GoalStateProperties.All, silent=False): """ Fetches the goal state using the given wire client. @@ -72,6 +84,7 @@ def __init__(self, wire_client, silent=False): self._wire_client = wire_client self._history = None self._extensions_goal_state = None # populated from vmSettings or extensionsConfig + self._goal_state_properties = goal_state_properties self.logger = logger.Logger(logger.DEFAULT_LOGGER) self.logger.silent = silent @@ -99,35 +112,59 @@ def incarnation(self): @property def container_id(self): - return self._container_id + if not self._goal_state_properties & GoalStateProperties.RoleConfig: + raise ProtocolError("ContainerId is not in goal state properties") + else: + return self._container_id @property def role_instance_id(self): - return self._role_instance_id + if not self._goal_state_properties & GoalStateProperties.RoleConfig: + raise ProtocolError("RoleInstanceId is not in goal state properties") + else: + return self._role_instance_id @property def role_config_name(self): - return self._role_config_name + if not self._goal_state_properties & GoalStateProperties.RoleConfig: + raise ProtocolError("RoleConfig is not in goal state properties") + else: + return self._role_config_name @property def extensions_goal_state(self): - return self._extensions_goal_state + if not self._goal_state_properties & GoalStateProperties.ExtensionsGoalState: + raise ProtocolError("ExtensionsGoalState is not in goal state properties") + else: + return self._extensions_goal_state @property def certs(self): - return self._certs + if not self._goal_state_properties & GoalStateProperties.ExtensionsGoalState: + raise ProtocolError("Certificates is not in goal state properties") + else: + return self._certs @property def hosting_env(self): - return self._hosting_env + if not self._goal_state_properties & GoalStateProperties.HostingEnv: + raise ProtocolError("HostingEnvironment is not in goal state properties") + else: + return self._hosting_env @property def shared_conf(self): - return self._shared_conf + if not self._goal_state_properties & GoalStateProperties.SharedConfig: + raise ProtocolError("SharedConfig is not in goal state properties") + else: + return self._shared_conf @property def remote_access(self): - return self._remote_access + if not self._goal_state_properties & GoalStateProperties.RemoteAccessInfo: + raise ProtocolError("RemoteAccessInfo is not in goal state properties") + else: + return self._remote_access def fetch_agent_manifest(self, family_name, uris): """ @@ -190,11 +227,12 @@ def _update(self, force_update): add_event(op=WALAEventOperation.GoalState, message=message) vm_settings, vm_settings_updated = None, False - try: - vm_settings, vm_settings_updated = GoalState._fetch_vm_settings(self._wire_client, force_update=force_update) - except VmSettingsSupportStopped as exception: # If the HGAP stopped supporting vmSettings, we need to use the goal state from the WireServer - self._restore_wire_server_goal_state(incarnation, xml_text, xml_doc, exception) - return + if self._goal_state_properties & GoalStateProperties.ExtensionsGoalState: + try: + vm_settings, vm_settings_updated = GoalState._fetch_vm_settings(self._wire_client, force_update=force_update) + except VmSettingsSupportStopped as exception: # If the HGAP stopped supporting vmSettings, we need to use the goal state from the WireServer + self._restore_wire_server_goal_state(incarnation, xml_text, xml_doc, exception) + return if vm_settings_updated: self.logger.info('') @@ -356,40 +394,48 @@ def _fetch_full_wire_server_goal_state(self, incarnation, xml_doc): self.logger.info(message) add_event(op=WALAEventOperation.GoalState, message=message) - role_instance = find(xml_doc, "RoleInstance") - role_instance_id = findtext(role_instance, "InstanceId") - role_config = find(role_instance, "Configuration") - role_config_name = findtext(role_config, "ConfigName") - container = find(xml_doc, "Container") - container_id = findtext(container, "ContainerId") + role_instance_id = None + role_config_name = None + container_id = None + if GoalStateProperties.RoleConfig & self._goal_state_properties: + role_instance = find(xml_doc, "RoleInstance") + role_instance_id = findtext(role_instance, "InstanceId") + role_config = find(role_instance, "Configuration") + role_config_name = findtext(role_config, "ConfigName") + container = find(xml_doc, "Container") + container_id = findtext(container, "ContainerId") extensions_config_uri = findtext(xml_doc, "ExtensionsConfig") - if extensions_config_uri is None: + if not (GoalStateProperties.ExtensionsGoalState & self._goal_state_properties) or extensions_config_uri is None: extensions_config = ExtensionsGoalStateFactory.create_empty(incarnation) else: xml_text = self._wire_client.fetch_config(extensions_config_uri, self._wire_client.get_header()) extensions_config = ExtensionsGoalStateFactory.create_from_extensions_config(incarnation, xml_text, self._wire_client) self._history.save_extensions_config(extensions_config.get_redacted_text()) - hosting_env_uri = findtext(xml_doc, "HostingEnvironmentConfig") - xml_text = self._wire_client.fetch_config(hosting_env_uri, self._wire_client.get_header()) - hosting_env = HostingEnv(xml_text) - self._history.save_hosting_env(xml_text) - - shared_conf_uri = findtext(xml_doc, "SharedConfig") - xml_text = self._wire_client.fetch_config(shared_conf_uri, self._wire_client.get_header()) - shared_config = SharedConfig(xml_text) - self._history.save_shared_conf(xml_text) - # SharedConfig.xml is used by other components (Azsec and Singularity/HPC Infiniband), so save it to the agent's root directory as well - shared_config_file = os.path.join(conf.get_lib_dir(), SHARED_CONF_FILE_NAME) - try: - fileutil.write_file(shared_config_file, xml_text) - except Exception as e: - logger.warn("Failed to save {0}: {1}".format(shared_config, e)) + hosting_env = None + if GoalStateProperties.HostingEnv & self._goal_state_properties: + hosting_env_uri = findtext(xml_doc, "HostingEnvironmentConfig") + xml_text = self._wire_client.fetch_config(hosting_env_uri, self._wire_client.get_header()) + hosting_env = HostingEnv(xml_text) + self._history.save_hosting_env(xml_text) + + shared_config = None + if GoalStateProperties.SharedConfig & self._goal_state_properties: + shared_conf_uri = findtext(xml_doc, "SharedConfig") + xml_text = self._wire_client.fetch_config(shared_conf_uri, self._wire_client.get_header()) + shared_config = SharedConfig(xml_text) + self._history.save_shared_conf(xml_text) + # SharedConfig.xml is used by other components (Azsec and Singularity/HPC Infiniband), so save it to the agent's root directory as well + shared_config_file = os.path.join(conf.get_lib_dir(), SHARED_CONF_FILE_NAME) + try: + fileutil.write_file(shared_config_file, xml_text) + except Exception as e: + logger.warn("Failed to save {0}: {1}".format(shared_config, e)) certs = EmptyCertificates() certs_uri = findtext(xml_doc, "Certificates") - if certs_uri is not None: + if (GoalStateProperties.ExtensionsGoalState & self._goal_state_properties) and certs_uri is not None: xml_text = self._wire_client.fetch_config(certs_uri, self._wire_client.get_header_for_cert()) certs = Certificates(xml_text, self.logger) # Log and save the certificates summary (i.e. the thumbprint but not the certificate itself) to the goal state history @@ -403,11 +449,12 @@ def _fetch_full_wire_server_goal_state(self, incarnation, xml_doc): self._history.save_certificates(json.dumps(certs.summary)) remote_access = None - remote_access_uri = findtext(container, "RemoteAccessInfo") - if remote_access_uri is not None: - xml_text = self._wire_client.fetch_config(remote_access_uri, self._wire_client.get_header_for_cert()) - remote_access = RemoteAccess(xml_text) - self._history.save_remote_access(xml_text) + if GoalStateProperties.RemoteAccessInfo & self._goal_state_properties: + remote_access_uri = findtext(container, "RemoteAccessInfo") + if remote_access_uri is not None: + xml_text = self._wire_client.fetch_config(remote_access_uri, self._wire_client.get_header_for_cert()) + remote_access = RemoteAccess(xml_text) + self._history.save_remote_access(xml_text) self._incarnation = incarnation self._role_instance_id = role_instance_id diff --git a/azurelinuxagent/common/protocol/util.py b/azurelinuxagent/common/protocol/util.py index 92b691e92b..7d7f901681 100644 --- a/azurelinuxagent/common/protocol/util.py +++ b/azurelinuxagent/common/protocol/util.py @@ -188,7 +188,7 @@ def _clear_wireserver_endpoint(self): return logger.error("Failed to clear wiresever endpoint: {0}", e) - def _detect_protocol(self): + def _detect_protocol(self, init_goal_state=True): """ Probe protocol endpoints in turn. """ @@ -217,7 +217,7 @@ def _detect_protocol(self): try: protocol = WireProtocol(endpoint) - protocol.detect() + protocol.detect(init_goal_state=init_goal_state) self._set_wireserver_endpoint(endpoint) return protocol @@ -268,7 +268,7 @@ def clear_protocol(self): finally: self._lock.release() - def get_protocol(self): + def get_protocol(self, init_goal_state=True): """ Detect protocol by endpoint. :returns: protocol instance @@ -296,7 +296,7 @@ def get_protocol(self): logger.info("Detect protocol endpoint") - protocol = self._detect_protocol() + protocol = self._detect_protocol(init_goal_state=init_goal_state) IOErrorCounter.set_protocol_endpoint(endpoint=protocol.get_endpoint()) self._save_protocol(WIRE_PROTOCOL_NAME) diff --git a/azurelinuxagent/common/protocol/wire.py b/azurelinuxagent/common/protocol/wire.py index d5aeda71c7..38a3e0621d 100644 --- a/azurelinuxagent/common/protocol/wire.py +++ b/azurelinuxagent/common/protocol/wire.py @@ -37,7 +37,8 @@ from azurelinuxagent.common.exception import ProtocolNotFoundError, \ ResourceGoneError, ExtensionDownloadError, InvalidContainerError, ProtocolError, HttpError, ExtensionErrorCodes from azurelinuxagent.common.future import httpclient, bytebuffer, ustr -from azurelinuxagent.common.protocol.goal_state import GoalState, TRANSPORT_CERT_FILE_NAME, TRANSPORT_PRV_FILE_NAME +from azurelinuxagent.common.protocol.goal_state import GoalState, TRANSPORT_CERT_FILE_NAME, TRANSPORT_PRV_FILE_NAME, \ + GoalStateProperties from azurelinuxagent.common.protocol.hostplugin import HostPluginProtocol from azurelinuxagent.common.protocol.restapi import DataContract, ProvisionStatus, VMInfo, VMStatus from azurelinuxagent.common.telemetryevent import GuestAgentExtensionEventsSchema @@ -72,7 +73,7 @@ def __init__(self, endpoint): raise ProtocolError("WireProtocol endpoint is None") self.client = WireClient(endpoint) - def detect(self): + def detect(self, init_goal_state=True): self.client.check_wire_protocol_version() trans_prv_file = os.path.join(conf.get_lib_dir(), @@ -83,11 +84,9 @@ def detect(self): cryptutil.gen_transport_cert(trans_prv_file, trans_cert_file) # Initialize the goal state, including all the inner properties - logger.info('Initializing goal state during protocol detection') - self.client.update_goal_state(force_update=True) - - def update_goal_state(self, silent=False): - self.client.update_goal_state(silent=silent) + if init_goal_state: + logger.info('Initializing goal state during protocol detection') + self.client.reset_goal_state() def update_host_plugin_from_goal_state(self): self.client.update_host_plugin_from_goal_state() @@ -778,15 +777,12 @@ def update_host_plugin(self, container_id, role_config_name): self._host_plugin.update_container_id(container_id) self._host_plugin.update_role_config_name(role_config_name) - def update_goal_state(self, force_update=False, silent=False): + def update_goal_state(self, silent=False): """ - Updates the goal state if the incarnation or etag changed or if 'force_update' is True + Updates the goal state if the incarnation or etag changed """ try: - if force_update and not silent: - logger.info("Forcing an update of the goal state.") - - if self._goal_state is None or force_update: + if self._goal_state is None: self._goal_state = GoalState(self, silent=silent) else: self._goal_state.update(silent=silent) @@ -796,6 +792,21 @@ def update_goal_state(self, force_update=False, silent=False): except Exception as exception: raise ProtocolError("Error fetching goal state: {0}".format(ustr(exception))) + def reset_goal_state(self, goal_state_properties=GoalStateProperties.All, silent=False): + """ + Resets the goal state + """ + try: + if not silent: + logger.info("Forcing an update of the goal state.") + + self._goal_state = GoalState(self, goal_state_properties=goal_state_properties, silent=silent) + + except ProtocolError: + raise + except Exception as exception: + raise ProtocolError("Error fetching goal state: {0}".format(ustr(exception))) + def get_goal_state(self): if self._goal_state is None: raise ProtocolError("Trying to fetch goal state before initialization!") @@ -925,7 +936,7 @@ def upload_status_blob(self): if extensions_goal_state.status_upload_blob is None: # the status upload blob is in ExtensionsConfig so force a full goal state refresh - self.update_goal_state(force_update=True, silent=True) + self.reset_goal_state(silent=True) extensions_goal_state = self.get_goal_state().extensions_goal_state if extensions_goal_state.status_upload_blob is None: diff --git a/azurelinuxagent/daemon/main.py b/azurelinuxagent/daemon/main.py index c608768a67..1eb58ec99b 100644 --- a/azurelinuxagent/daemon/main.py +++ b/azurelinuxagent/daemon/main.py @@ -28,6 +28,7 @@ from azurelinuxagent.common.event import add_event, WALAEventOperation, initialize_event_logger_vminfo_common_parameters from azurelinuxagent.common.future import ustr from azurelinuxagent.common.osutil import get_osutil +from azurelinuxagent.common.protocol.goal_state import GoalState, GoalStateProperties from azurelinuxagent.common.protocol.util import get_protocol_util from azurelinuxagent.common.rdma import setup_rdma_device from azurelinuxagent.common.utils import textutil @@ -160,9 +161,9 @@ def daemon(self, child_args=None): # current values. protocol = self.protocol_util.get_protocol() - protocol.client.update_goal_state(force_update=True) + goal_state = GoalState(protocol, goal_state_properties=GoalStateProperties.SharedConfig) - setup_rdma_device(nd_version, protocol.client.get_shared_conf()) + setup_rdma_device(nd_version, goal_state.shared_conf) except Exception as e: logger.error("Error setting up rdma device: %s" % e) else: diff --git a/azurelinuxagent/ga/update.py b/azurelinuxagent/ga/update.py index 8d2c97df20..cd758b972a 100644 --- a/azurelinuxagent/ga/update.py +++ b/azurelinuxagent/ga/update.py @@ -485,7 +485,7 @@ def _try_update_goal_state(self, protocol): try: max_errors_to_log = 3 - protocol.update_goal_state(silent=self._update_goal_state_error_count >= max_errors_to_log) + protocol.client.update_goal_state(silent=self._update_goal_state_error_count >= max_errors_to_log) self._goal_state = protocol.get_goal_state() diff --git a/tests/common/test_event.py b/tests/common/test_event.py index 7191f3c30a..fad21155f2 100644 --- a/tests/common/test_event.py +++ b/tests/common/test_event.py @@ -161,12 +161,12 @@ def create_event_and_return_container_id(): # pylint: disable=inconsistent-retu self.assertEqual(contained_id, 'c6d5526c-5ac2-4200-b6e2-56f2b70c5ab2', "Incorrect container ID") protocol.mock_wire_data.set_container_id('AAAAAAAA-BBBB-CCCC-DDDD-EEEEEEEEEEEE') - protocol.update_goal_state() + protocol.client.update_goal_state() contained_id = create_event_and_return_container_id() self.assertEqual(contained_id, 'AAAAAAAA-BBBB-CCCC-DDDD-EEEEEEEEEEEE', "Incorrect container ID") protocol.mock_wire_data.set_container_id('11111111-2222-3333-4444-555555555555') - protocol.update_goal_state() + protocol.client.update_goal_state() contained_id = create_event_and_return_container_id() self.assertEqual(contained_id, '11111111-2222-3333-4444-555555555555', "Incorrect container ID") diff --git a/tests/ga/test_extension.py b/tests/ga/test_extension.py index 5947e9e669..2272a1907b 100644 --- a/tests/ga/test_extension.py +++ b/tests/ga/test_extension.py @@ -510,7 +510,7 @@ def _set_up_update_test_and_update_gs(self, patch_command, *args): test_data.set_incarnation(2) test_data.set_extensions_config_version("1.0.1") test_data.set_manifest_version('1.0.1') - protocol.update_goal_state() + protocol.client.update_goal_state() # Ensure the patched command fails patch_command.return_value = "exit 1" @@ -542,7 +542,7 @@ def test_ext_handler(self, *args): # Test goal state changed test_data.set_incarnation(2) test_data.set_extensions_config_sequence_number(1) - protocol.update_goal_state() + protocol.client.update_goal_state() exthandlers_handler.run() exthandlers_handler.report_ext_handlers_status() @@ -555,7 +555,7 @@ def test_ext_handler(self, *args): test_data.set_incarnation(3) test_data.set_extensions_config_version("1.1.1") test_data.set_extensions_config_sequence_number(2) - protocol.update_goal_state() + protocol.client.update_goal_state() exthandlers_handler.run() exthandlers_handler.report_ext_handlers_status() @@ -567,7 +567,7 @@ def test_ext_handler(self, *args): test_data.set_incarnation(4) test_data.set_extensions_config_version("1.2.0") test_data.set_extensions_config_sequence_number(3) - protocol.update_goal_state() + protocol.client.update_goal_state() exthandlers_handler.run() exthandlers_handler.report_ext_handlers_status() @@ -578,7 +578,7 @@ def test_ext_handler(self, *args): # Test disable test_data.set_incarnation(5) test_data.set_extensions_config_state(ExtensionRequestedState.Disabled) - protocol.update_goal_state() + protocol.client.update_goal_state() exthandlers_handler.run() exthandlers_handler.report_ext_handlers_status() @@ -588,7 +588,7 @@ def test_ext_handler(self, *args): # Test uninstall test_data.set_incarnation(6) test_data.set_extensions_config_state(ExtensionRequestedState.Uninstall) - protocol.update_goal_state() + protocol.client.update_goal_state() exthandlers_handler.run() exthandlers_handler.report_ext_handlers_status() @@ -597,7 +597,7 @@ def test_ext_handler(self, *args): # Test uninstall again! test_data.set_incarnation(7) - protocol.update_goal_state() + protocol.client.update_goal_state() exthandlers_handler.run() exthandlers_handler.report_ext_handlers_status() @@ -620,7 +620,7 @@ def _assert_handler_status_and_manifest_download_count(protocol, test_data, mani # Update Incarnation test_data.set_incarnation(2) - protocol.update_goal_state() + protocol.client.update_goal_state() exthandlers_handler.run() exthandlers_handler.report_ext_handlers_status() @@ -720,7 +720,7 @@ def test_ext_handler_no_settings(self, *args): # Uninstall the Plugin and make sure Disable called test_data.set_incarnation(2) test_data.set_extensions_config_state(ExtensionRequestedState.Uninstall) - protocol.update_goal_state() + protocol.client.update_goal_state() with enable_invocations(test_ext) as invocation_record: exthandlers_handler.run() @@ -799,7 +799,7 @@ def test_ext_handler_sequencing(self, *args): dep_ext_level_4 = extension_emulator(name="OSTCExtensions.OtherExampleHandlerLinux") test_data.ext_conf = test_data.ext_conf.replace("dependencyLevel=\"2\"", "dependencyLevel=\"3\"") test_data.ext_conf = test_data.ext_conf.replace("dependencyLevel=\"1\"", "dependencyLevel=\"4\"") - protocol.update_goal_state() + protocol.client.update_goal_state() with enable_invocations(dep_ext_level_3, dep_ext_level_4) as invocation_record: exthandlers_handler.run() @@ -826,7 +826,7 @@ def test_ext_handler_sequencing(self, *args): # the last one disabled. test_data.set_incarnation(3) test_data.set_extensions_config_state(ExtensionRequestedState.Disabled) - protocol.update_goal_state() + protocol.client.update_goal_state() with enable_invocations(dep_ext_level_3, dep_ext_level_4) as invocation_record: exthandlers_handler.run() @@ -858,7 +858,7 @@ def test_ext_handler_sequencing(self, *args): dep_ext_level_6 = extension_emulator(name="OSTCExtensions.ExampleHandlerLinux") test_data.ext_conf = test_data.ext_conf.replace("dependencyLevel=\"3\"", "dependencyLevel=\"6\"") test_data.ext_conf = test_data.ext_conf.replace("dependencyLevel=\"4\"", "dependencyLevel=\"5\"") - protocol.update_goal_state() + protocol.client.update_goal_state() with enable_invocations(dep_ext_level_5, dep_ext_level_6) as invocation_record: exthandlers_handler.run() @@ -938,7 +938,7 @@ def mock_fail_extension_commands(args, **kwargs): _assert_event_reported_only_on_incarnation_change(expected_count=1) test_data.set_incarnation(2) - protocol.update_goal_state() + protocol.client.update_goal_state() exthandlers_handler.run() exthandlers_handler.report_ext_handlers_status() @@ -949,7 +949,7 @@ def mock_fail_extension_commands(args, **kwargs): # Test it recovers on a new goal state if Handler succeeds test_data.set_incarnation(3) test_data.set_extensions_config_sequence_number(1) - protocol.update_goal_state() + protocol.client.update_goal_state() exthandlers_handler.run() exthandlers_handler.report_ext_handlers_status() @@ -961,7 +961,7 @@ def mock_fail_extension_commands(args, **kwargs): # Update incarnation to confirm extension invocation order test_data.set_incarnation(4) - protocol.update_goal_state() + protocol.client.update_goal_state() dep_ext_level_2 = extension_emulator(name="OSTCExtensions.ExampleHandlerLinux") dep_ext_level_1 = extension_emulator(name="OSTCExtensions.OtherExampleHandlerLinux") @@ -1023,7 +1023,7 @@ def test_ext_handler_rollingupgrade(self, *args): # Test goal state changed test_data.set_incarnation(2) - protocol.update_goal_state() + protocol.client.update_goal_state() exthandlers_handler.run() exthandlers_handler.report_ext_handlers_status() @@ -1034,7 +1034,7 @@ def test_ext_handler_rollingupgrade(self, *args): # Test minor version bump test_data.set_incarnation(3) test_data.set_extensions_config_version("1.1.0") - protocol.update_goal_state() + protocol.client.update_goal_state() exthandlers_handler.run() exthandlers_handler.report_ext_handlers_status() @@ -1045,7 +1045,7 @@ def test_ext_handler_rollingupgrade(self, *args): # Test hotfix version bump test_data.set_incarnation(4) test_data.set_extensions_config_version("1.1.1") - protocol.update_goal_state() + protocol.client.update_goal_state() exthandlers_handler.run() exthandlers_handler.report_ext_handlers_status() @@ -1056,7 +1056,7 @@ def test_ext_handler_rollingupgrade(self, *args): # Test disable test_data.set_incarnation(5) test_data.set_extensions_config_state(ExtensionRequestedState.Disabled) - protocol.update_goal_state() + protocol.client.update_goal_state() exthandlers_handler.run() exthandlers_handler.report_ext_handlers_status() @@ -1066,7 +1066,7 @@ def test_ext_handler_rollingupgrade(self, *args): # Test uninstall test_data.set_incarnation(6) test_data.set_extensions_config_state(ExtensionRequestedState.Uninstall) - protocol.update_goal_state() + protocol.client.update_goal_state() exthandlers_handler.run() exthandlers_handler.report_ext_handlers_status() @@ -1075,7 +1075,7 @@ def test_ext_handler_rollingupgrade(self, *args): # Test uninstall again! test_data.set_incarnation(7) - protocol.update_goal_state() + protocol.client.update_goal_state() exthandlers_handler.run() exthandlers_handler.report_ext_handlers_status() @@ -1085,7 +1085,7 @@ def test_ext_handler_rollingupgrade(self, *args): # Test re-install test_data.set_incarnation(8) test_data.set_extensions_config_state(ExtensionRequestedState.Enabled) - protocol.update_goal_state() + protocol.client.update_goal_state() exthandlers_handler.run() exthandlers_handler.report_ext_handlers_status() @@ -1096,7 +1096,7 @@ def test_ext_handler_rollingupgrade(self, *args): # Test version bump post-re-install test_data.set_incarnation(9) test_data.set_extensions_config_version("1.2.0") - protocol.update_goal_state() + protocol.client.update_goal_state() exthandlers_handler.run() exthandlers_handler.report_ext_handlers_status() @@ -1107,7 +1107,7 @@ def test_ext_handler_rollingupgrade(self, *args): # Test rollback test_data.set_incarnation(10) test_data.set_extensions_config_version("1.1.0") - protocol.update_goal_state() + protocol.client.update_goal_state() exthandlers_handler.run() exthandlers_handler.report_ext_handlers_status() @@ -1170,7 +1170,7 @@ def test_it_should_not_delete_extension_events_directory_on_extension_uninstall( # Uninstall extensions now test_data.set_extensions_config_state(ExtensionRequestedState.Uninstall) test_data.set_incarnation(2) - protocol.update_goal_state() + protocol.client.update_goal_state() exthandlers_handler.run() exthandlers_handler.report_ext_handlers_status() @@ -1192,7 +1192,7 @@ def test_it_should_uninstall_unregistered_extensions_properly(self, *args): # Since the installed version is not in PIR anymore, we need to also remove it from manifest file test_data.manifest = test_data.manifest.replace("1.0.0", "9.9.9") test_data.set_incarnation(2) - protocol.update_goal_state() + protocol.client.update_goal_state() exthandlers_handler.run() exthandlers_handler.report_ext_handlers_status() @@ -1666,7 +1666,7 @@ def test_extensions_deleted(self, *args): test_data.set_incarnation(2) test_data.set_extensions_config_version("1.0.1") test_data.set_manifest_version('1.0.1') - protocol.update_goal_state() + protocol.client.update_goal_state() # Ensure new extension can be enabled exthandlers_handler.run() @@ -1753,7 +1753,7 @@ def test_disable_failure_with_exception_handling(self, patch_get_disable_command # Next incarnation, disable extension test_data.set_incarnation(2) test_data.set_extensions_config_state(ExtensionRequestedState.Disabled) - protocol.update_goal_state() + protocol.client.update_goal_state() exthandlers_handler.run() exthandlers_handler.report_ext_handlers_status() @@ -1784,7 +1784,7 @@ def test_uninstall_failure(self, patch_get_uninstall_command, *args): # Next incarnation, disable extension test_data.set_incarnation(2) test_data.set_extensions_config_state(ExtensionRequestedState.Uninstall) - protocol.update_goal_state() + protocol.client.update_goal_state() exthandlers_handler.run() exthandlers_handler.report_ext_handlers_status() @@ -1841,7 +1841,7 @@ def mock_popen(*args, **kwargs): # If the incarnation number changes (there's a new goal state), ensure we go through the entire upgrade # process again. test_data.set_incarnation(3) - protocol.update_goal_state() + protocol.client.update_goal_state() exthandlers_handler.run() exthandlers_handler.report_ext_handlers_status() @@ -1888,7 +1888,7 @@ def test_extension_upgrade_failure_when_prev_version_disable_fails_and_recovers_ # Force a new goal state incarnation, only then will we attempt the upgrade again test_data.set_incarnation(3) - protocol.update_goal_state() + protocol.client.update_goal_state() # Ensure disable won't fail by making launch_command a no-op with patch('azurelinuxagent.ga.exthandlers.ExtHandlerInstance.launch_command') as patch_launch_command: # pylint: disable=unused-variable @@ -2111,7 +2111,7 @@ def test_uninstall_rc_env_var_should_report_not_run_for_non_update_calls_to_exth # Initiating another run which shouldn't have any failed env variables in it if no failures # Updating Incarnation test_data.set_incarnation(3) - protocol.update_goal_state() + protocol.client.update_goal_state() exthandlers_handler.run() exthandlers_handler.report_ext_handlers_status() @@ -3355,7 +3355,7 @@ def http_get_handler(url, *_, **kwargs): # Update GoalState protocol.mock_wire_data.set_incarnation(2) - protocol.update_goal_state() + protocol.client.update_goal_state() with patch.object(conf, 'get_extensions_enabled', return_value=False): assert_extensions_called(exthandlers_handler, expected_call_count=0) @@ -3369,7 +3369,7 @@ def http_get_handler(url, *_, **kwargs): # Enabled on_hold property in artifact_blob mock_in_vm_artifacts_profile_response = MockHttpResponse(200, body='{ "onHold": true }'.encode('utf-8')) - protocol.client.update_goal_state(force_update=True) + protocol.client.reset_goal_state() with patch.object(conf, 'get_extensions_enabled', return_value=True): with patch.object(conf, "get_enable_overprovisioning", return_value=True): @@ -3377,7 +3377,7 @@ def http_get_handler(url, *_, **kwargs): # Disabled on_hold property in artifact_blob mock_in_vm_artifacts_profile_response = MockHttpResponse(200, body='{ "onHold": false }'.encode('utf-8')) - protocol.client.update_goal_state(force_update=True) + protocol.client.reset_goal_state() with patch.object(conf, 'get_extensions_enabled', return_value=True): with patch.object(conf, "get_enable_overprovisioning", return_value=True): @@ -3410,7 +3410,7 @@ def http_get_handler(url, *_, **kwargs): return None protocol.set_http_handlers(http_get_handler=http_get_handler) - protocol.client.update_goal_state(force_update=True) + protocol.client.reset_goal_state() exthandlers_handler.run() exthandlers_handler.report_ext_handlers_status() diff --git a/tests/ga/test_monitor.py b/tests/ga/test_monitor.py index d5700bc91b..5853b23eff 100644 --- a/tests/ga/test_monitor.py +++ b/tests/ga/test_monitor.py @@ -188,7 +188,7 @@ def setUp(self): CGroupsTelemetry.reset() clear_singleton_instances(ProtocolUtil) protocol = WireProtocol('endpoint') - protocol.update_goal_state = MagicMock() + protocol.client.update_goal_state = MagicMock() self.get_protocol = patch('azurelinuxagent.common.protocol.util.ProtocolUtil.get_protocol', return_value=protocol) self.get_protocol.start() diff --git a/tests/ga/test_multi_config_extension.py b/tests/ga/test_multi_config_extension.py index a9a07bd67e..365052f5d4 100644 --- a/tests/ga/test_multi_config_extension.py +++ b/tests/ga/test_multi_config_extension.py @@ -253,7 +253,7 @@ def __setup_and_assert_disable_scenario(self, exthandlers_handler, protocol): self.test_data['ext_conf'] = os.path.join(self._MULTI_CONFIG_TEST_DATA, 'ext_conf_mc_disabled_extensions.xml') protocol.mock_wire_data = WireProtocolData(self.test_data) protocol.mock_wire_data.set_incarnation(2) - protocol.update_goal_state() + protocol.client.update_goal_state() exthandlers_handler.run() exthandlers_handler.report_ext_handlers_status() @@ -319,7 +319,7 @@ def test_it_should_execute_and_report_multi_config_extensions_properly(self): # Case 3: Uninstall Multi-config handler (with enabled extensions) and single config extension protocol.mock_wire_data.set_incarnation(3) protocol.mock_wire_data.set_extensions_config_state(ExtensionRequestedState.Uninstall) - protocol.update_goal_state() + protocol.client.update_goal_state() exthandlers_handler.run() exthandlers_handler.report_ext_handlers_status() self.assertEqual(0, len(protocol.aggregate_status['aggregateStatus']['handlerAggregateStatus']), @@ -333,7 +333,7 @@ def test_it_should_report_unregistered_version_error_per_extension(self): failing_version = "19.12.1221" protocol.mock_wire_data.set_extensions_config_version(failing_version) protocol.mock_wire_data.set_incarnation(2) - protocol.update_goal_state() + protocol.client.update_goal_state() exthandlers_handler.run() exthandlers_handler.report_ext_handlers_status() self.assertEqual(no_of_extensions, @@ -411,7 +411,7 @@ def test_it_should_only_disable_enabled_extensions_on_update(self): self.test_data['ext_conf'] = os.path.join(self._MULTI_CONFIG_TEST_DATA, 'ext_conf_mc_update_extensions.xml') protocol.mock_wire_data = WireProtocolData(self.test_data) protocol.mock_wire_data.set_incarnation(2) - protocol.update_goal_state() + protocol.client.update_goal_state() new_version = "1.1.0" new_first_ext = extension_emulator(name="OSTCExtensions.ExampleHandlerLinux.firstExtension", @@ -460,7 +460,7 @@ def test_it_should_retry_update_sequence_per_extension_if_previous_failed(self): self.test_data['ext_conf'] = os.path.join(self._MULTI_CONFIG_TEST_DATA, 'ext_conf_mc_update_extensions.xml') protocol.mock_wire_data = WireProtocolData(self.test_data) protocol.mock_wire_data.set_incarnation(2) - protocol.update_goal_state() + protocol.client.update_goal_state() new_version = "1.1.0" _, fail_action = Actions.generate_unique_fail() @@ -529,7 +529,7 @@ def test_it_should_report_disabled_extension_errors_if_update_failed(self): self.test_data['ext_conf'] = os.path.join(self._MULTI_CONFIG_TEST_DATA, 'ext_conf_mc_update_extensions.xml') protocol.mock_wire_data = WireProtocolData(self.test_data) protocol.mock_wire_data.set_incarnation(2) - protocol.update_goal_state() + protocol.client.update_goal_state() new_version = "1.1.0" fail_code, fail_action = Actions.generate_unique_fail() @@ -655,7 +655,7 @@ def __assert_state_file(handler_name, handler_version, extensions, state, not_pr self.test_data['ext_conf'] = os.path.join(self._MULTI_CONFIG_TEST_DATA, 'ext_conf_mc_disabled_extensions.xml') protocol.mock_wire_data = WireProtocolData(self.test_data) protocol.mock_wire_data.set_incarnation(2) - protocol.update_goal_state() + protocol.client.update_goal_state() ext_handler.run() ext_handler.report_ext_handlers_status() @@ -781,7 +781,7 @@ def mock_popen(cmd, *_, **kwargs): 'ext_conf_mc_update_extensions.xml') protocol.mock_wire_data = WireProtocolData(self.test_data) protocol.mock_wire_data.set_incarnation(2) - protocol.update_goal_state() + protocol.client.update_goal_state() exthandlers_handler.run() exthandlers_handler.report_ext_handlers_status() @@ -961,7 +961,7 @@ def test_it_should_report_status_correctly_for_unsupported_goal_state(self): self.test_data['ext_conf'] = "wire/ext_conf_required_features.xml" protocol.mock_wire_data = WireProtocolData(self.test_data) protocol.mock_wire_data.set_incarnation(2) - protocol.update_goal_state() + protocol.client.update_goal_state() # Assert the extension status is the same as we reported for Incarnation 1. self.__run_and_assert_generic_case(exthandlers_handler, protocol, no_of_extensions=4, with_message=False) @@ -1021,7 +1021,7 @@ def test_it_should_check_every_time_if_handler_supports_mc(self): with self.__setup_generic_test_env() as (exthandlers_handler, protocol, old_exts): protocol.mock_wire_data.set_incarnation(2) - protocol.update_goal_state() + protocol.client.update_goal_state() # Mock manifest to not support multiple extensions with patch('azurelinuxagent.ga.exthandlers.HandlerManifest.supports_multiple_extensions', return_value=False): diff --git a/tests/protocol/test_goal_state.py b/tests/protocol/test_goal_state.py index d853363c73..869da68c8c 100644 --- a/tests/protocol/test_goal_state.py +++ b/tests/protocol/test_goal_state.py @@ -14,7 +14,8 @@ from azurelinuxagent.common.protocol.extensions_goal_state_from_extensions_config import ExtensionsGoalStateFromExtensionsConfig from azurelinuxagent.common.protocol.extensions_goal_state_from_vm_settings import ExtensionsGoalStateFromVmSettings from azurelinuxagent.common.protocol import hostplugin -from azurelinuxagent.common.protocol.goal_state import GoalState, GoalStateInconsistentError, _GET_GOAL_STATE_MAX_ATTEMPTS +from azurelinuxagent.common.protocol.goal_state import GoalState, GoalStateInconsistentError, \ + _GET_GOAL_STATE_MAX_ATTEMPTS, GoalStateProperties from azurelinuxagent.common.exception import ProtocolError from azurelinuxagent.common.utils import fileutil from azurelinuxagent.common.utils.archive import ARCHIVE_DIRECTORY_NAME @@ -419,3 +420,74 @@ def http_get_handler(url, *_, **__): for settings in extension.settings: if settings.protectedSettings is not None: self.assertIn(settings.certificateThumbprint, thumbprints, "Certificate is missing from the goal state.") + + def test_it_should_raise_when_goal_state_properties_not_initialized(self): + with GoalStateTestCase._create_protocol_ws_and_hgap_in_sync() as protocol: + goal_state = GoalState( + protocol.client, + goal_state_properties=~GoalStateProperties.All) + + goal_state.update() + + with self.assertRaises(ProtocolError) as context: + _ = goal_state.container_id + + expected_message = "ContainerId is not in goal state properties" + self.assertIn(expected_message, str(context.exception)) + + with self.assertRaises(ProtocolError) as context: + _ = goal_state.role_config_name + + expected_message = "RoleConfig is not in goal state properties" + self.assertIn(expected_message, str(context.exception)) + + with self.assertRaises(ProtocolError) as context: + _ = goal_state.role_instance_id + + expected_message = "RoleInstanceId is not in goal state properties" + self.assertIn(expected_message, str(context.exception)) + + with self.assertRaises(ProtocolError) as context: + _ = goal_state.extensions_goal_state + + expected_message = "ExtensionsGoalState is not in goal state properties" + self.assertIn(expected_message, str(context.exception)) + + with self.assertRaises(ProtocolError) as context: + _ = goal_state.hosting_env + + expected_message = "HostingEnvironment is not in goal state properties" + self.assertIn(expected_message, str(context.exception)) + + with self.assertRaises(ProtocolError) as context: + _ = goal_state.certs + + expected_message = "Certificates is not in goal state properties" + self.assertIn(expected_message, str(context.exception)) + + with self.assertRaises(ProtocolError) as context: + _ = goal_state.shared_conf + + expected_message = "SharedConfig is not in goal state properties" + self.assertIn(expected_message, str(context.exception)) + + with self.assertRaises(ProtocolError) as context: + _ = goal_state.remote_access + + expected_message = "RemoteAccessInfo is not in goal state properties" + self.assertIn(expected_message, str(context.exception)) + + goal_state = GoalState( + protocol.client, + goal_state_properties=GoalStateProperties.All & ~GoalStateProperties.HostingEnv) + + goal_state.update() + + _ = goal_state.container_id, goal_state.role_instance_id, goal_state.role_config_name, \ + goal_state.extensions_goal_state, goal_state.certs, goal_state.shared_conf, goal_state.remote_access + + with self.assertRaises(ProtocolError) as context: + _ = goal_state.hosting_env + + expected_message = "HostingEnvironment is not in goal state properties" + self.assertIn(expected_message, str(context.exception)) diff --git a/tests/protocol/test_hostplugin.py b/tests/protocol/test_hostplugin.py index c980433b5a..b85ed7574f 100644 --- a/tests/protocol/test_hostplugin.py +++ b/tests/protocol/test_hostplugin.py @@ -244,7 +244,7 @@ def test_default_channel(self, patch_put, patch_upload, _): with self.create_mock_protocol() as wire_protocol: wire.HostPluginProtocol.is_default_channel = False - wire_protocol.update_goal_state() + wire_protocol.client.update_goal_state() # act wire_protocol.client.upload_status_blob() @@ -277,7 +277,7 @@ def test_fallback_channel_503(self, patch_put, patch_upload, _): with self.create_mock_protocol() as wire_protocol: wire.HostPluginProtocol.is_default_channel = False - wire_protocol.update_goal_state() + wire_protocol.client.update_goal_state() # act wire_protocol.client.upload_status_blob() @@ -311,7 +311,7 @@ def test_fallback_channel_410(self, patch_refresh_host_plugin, patch_put, patch_ with self.create_mock_protocol() as wire_protocol: wire.HostPluginProtocol.is_default_channel = False - wire_protocol.update_goal_state() + wire_protocol.client.update_goal_state() # act wire_protocol.client.upload_status_blob() @@ -345,7 +345,7 @@ def test_fallback_channel_failure(self, patch_put, patch_upload, _): with self.create_mock_protocol() as wire_protocol: wire.HostPluginProtocol.is_default_channel = False - wire_protocol.update_goal_state() + wire_protocol.client.update_goal_state() # act self.assertRaises(wire.ProtocolError, wire_protocol.client.upload_status_blob) diff --git a/tests/protocol/test_wire.py b/tests/protocol/test_wire.py index 7c121f4481..bbf018fc30 100644 --- a/tests/protocol/test_wire.py +++ b/tests/protocol/test_wire.py @@ -30,6 +30,7 @@ from azurelinuxagent.common.exception import ResourceGoneError, ProtocolError, \ ExtensionDownloadError, HttpError from azurelinuxagent.common.protocol.extensions_goal_state_from_extensions_config import ExtensionsGoalStateFromExtensionsConfig +from azurelinuxagent.common.protocol.goal_state import GoalStateProperties from azurelinuxagent.common.protocol.hostplugin import HostPluginProtocol from azurelinuxagent.common.protocol.wire import WireProtocol, WireClient, \ StatusBlob, VMStatus @@ -271,23 +272,23 @@ def http_get_handler(url, *_, **kwargs): protocol.set_http_handlers(http_get_handler=http_get_handler) mock_response = MockHttpResponse(200, body=None) - protocol.client.update_goal_state(force_update=True) + protocol.client.reset_goal_state() extensions_on_hold = protocol.get_goal_state().extensions_goal_state.on_hold self.assertFalse(extensions_on_hold, "Extensions should not be on hold when the in-vm artifacts profile response body is None") mock_response = MockHttpResponse(200, ' '.encode('utf-8')) - protocol.client.update_goal_state(force_update=True) + protocol.client.reset_goal_state() extensions_on_hold = protocol.get_goal_state().extensions_goal_state.on_hold self.assertFalse(extensions_on_hold, "Extensions should not be on hold when the in-vm artifacts profile response is an empty string") mock_response = MockHttpResponse(200, '{ }'.encode('utf-8')) - protocol.client.update_goal_state(force_update=True) + protocol.client.reset_goal_state() extensions_on_hold = protocol.get_goal_state().extensions_goal_state.on_hold self.assertFalse(extensions_on_hold, "Extensions should not be on hold when the in-vm artifacts profile response is an empty json object") with patch("azurelinuxagent.common.protocol.extensions_goal_state_from_extensions_config.add_event") as add_event: mock_response = MockHttpResponse(200, 'invalid json'.encode('utf-8')) - protocol.client.update_goal_state(force_update=True) + protocol.client.reset_goal_state() extensions_on_hold = protocol.get_goal_state().extensions_goal_state.on_hold self.assertFalse(extensions_on_hold, "Extensions should not be on hold when the in-vm artifacts profile response is not valid json") @@ -780,7 +781,7 @@ def http_get_handler(url, *_, **__): protocol.set_http_handlers(http_get_handler=http_get_handler) HostPluginProtocol.is_default_channel = False - protocol.client.update_goal_state(force_update=True) + protocol.client.reset_goal_state() urls = protocol.get_tracked_urls() self.assertEqual(len(urls), 1, "Unexpected HTTP requests: [{0}]".format(urls)) @@ -799,7 +800,7 @@ def http_get_handler(url, *_, **kwargs): HostPluginProtocol.is_default_channel = False try: - protocol.client.update_goal_state(force_update=True) + protocol.client.reset_goal_state() urls = protocol.get_tracked_urls() self.assertEqual(len(urls), 2, "Invalid number of requests: [{0}]".format(urls)) @@ -832,7 +833,7 @@ def http_get_handler(url, *_, **kwargs): protocol.set_http_handlers(http_get_handler=http_get_handler) - protocol.client.update_goal_state(force_update=True) + protocol.client.reset_goal_state() urls = protocol.get_tracked_urls() self.assertEqual(len(urls), 4, "Invalid number of requests: [{0}]".format(urls)) @@ -864,7 +865,7 @@ def http_get_handler(url, *_, **kwargs): protocol.set_http_handlers(http_get_handler=http_get_handler) - protocol.client.update_goal_state(force_update=True) + protocol.client.reset_goal_state() urls = protocol.get_tracked_urls() self.assertEqual(len(urls), 4, "Invalid number of requests: [{0}]".format(urls)) @@ -991,7 +992,7 @@ def test_download_using_appropriate_channel_should_change_default_channel_when_s class UpdateGoalStateTestCase(HttpRequestPredicates, AgentTestCase): """ - Tests for WireClient.update_goal_state() + Tests for WireClient.update_goal_state() and WireClient.reset_goal_state() """ def test_it_should_update_the_goal_state_and_the_host_plugin_when_the_incarnation_changes(self): @@ -1036,7 +1037,7 @@ def test_it_should_update_the_goal_state_and_the_host_plugin_when_the_incarnatio ''' if forced: - protocol.client.update_goal_state(force_update=True) + protocol.client.reset_goal_state() else: protocol.client.update_goal_state() @@ -1090,7 +1091,7 @@ def test_forced_update_should_update_the_goal_state_and_the_host_plugin_when_the protocol.mock_wire_data.set_role_config_name(new_role_config_name) protocol.mock_wire_data.shared_config = new_shared_conf - protocol.client.update_goal_state(force_update=True) + protocol.client.reset_goal_state() self.assertEqual(protocol.client.get_goal_state().incarnation, incarnation) self.assertEqual(protocol.client.get_shared_conf().xml_text, new_shared_conf) @@ -1098,6 +1099,28 @@ def test_forced_update_should_update_the_goal_state_and_the_host_plugin_when_the self.assertEqual(protocol.client.get_host_plugin().container_id, new_container_id) self.assertEqual(protocol.client.get_host_plugin().role_config_name, new_role_config_name) + def test_reset_should_init_provided_goal_state_properties(self): + with mock_wire_protocol(mockwiredata.DATA_FILE) as protocol: + protocol.client.reset_goal_state(goal_state_properties=GoalStateProperties.All & ~GoalStateProperties.ExtensionsGoalState) + + with self.assertRaises(ProtocolError) as context: + _ = protocol.client.get_certs() + + expected_message = "Certificates is not in goal state properties" + self.assertIn(expected_message, str(context.exception)) + + def test_reset_should_init_the_goal_state(self): + with mock_wire_protocol(mockwiredata.DATA_FILE) as protocol: + new_container_id = str(uuid.uuid4()) + new_role_config_name = str(uuid.uuid4()) + protocol.mock_wire_data.set_container_id(new_container_id) + protocol.mock_wire_data.set_role_config_name(new_role_config_name) + + protocol.client.reset_goal_state() + + self.assertEqual(protocol.client.get_goal_state().container_id, new_container_id) + self.assertEqual(protocol.client.get_goal_state().role_config_name, new_role_config_name) + class UpdateHostPluginFromGoalStateTestCase(AgentTestCase): """ From 0dc0071744cdf0452dbd3f5c98c2b99329d91496 Mon Sep 17 00:00:00 2001 From: Norberto Arrieta Date: Tue, 10 Jan 2023 12:23:42 -0800 Subject: [PATCH 24/63] Add note on systemd and environment variables (#2724) * Add note on systemd and environment variables * typo Co-authored-by: narrieta --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 996fafd5e9..ae6a851064 100644 --- a/README.md +++ b/README.md @@ -62,7 +62,9 @@ The agent will use an HTTP proxy if provided via the `http_proxy` (for `http` re `https_proxy` (for `https` requests) environment variables. The `HttpProxy.Host` and `HttpProxy.Port` configuration variables (see below), if used, will override the environment settings. Due to limitations of Python, the agent *does not* support HTTP proxies requiring -authentication. +authentication. Note that when the agent service is managed by systemd, environment variables +such as `http_proxy` and `https_proxy` should be defined using one the mechanisms provided by +systemd (e.g. by using Environment or EnvironmentFile in the service file). ## Requirements From 4fe49858aba3aa083f21650824b917c5c528f352 Mon Sep 17 00:00:00 2001 From: Nageswara Nandigam <84482346+nagworld9@users.noreply.github.com> Date: Tue, 10 Jan 2023 15:22:10 -0800 Subject: [PATCH 25/63] cgroup check on systemd-run process (#2721) * cgroup check on systemd-run process * address CR comments * update log msg --- azurelinuxagent/common/cgroupconfigurator.py | 34 +++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/azurelinuxagent/common/cgroupconfigurator.py b/azurelinuxagent/common/cgroupconfigurator.py index 5d7f2372e9..840961e7df 100644 --- a/azurelinuxagent/common/cgroupconfigurator.py +++ b/azurelinuxagent/common/cgroupconfigurator.py @@ -646,7 +646,7 @@ def _check_processes_in_agent_cgroup(self): Raises a CGroupsException if the check fails """ unexpected = [] - + agent_cgroup_proc_names = [] try: daemon = os.getppid() extension_handler = os.getpid() @@ -660,9 +660,13 @@ def _check_processes_in_agent_cgroup(self): systemd_run_commands.update(self._cgroups_api.get_systemd_run_commands()) for process in agent_cgroup: + agent_cgroup_proc_names.append(self.__format_process(process)) # Note that the agent uses systemd-run to start extensions; systemd-run belongs to the agent cgroup, though the extensions don't. if process in (daemon, extension_handler) or process in systemd_run_commands: continue + # check shell systemd_run process if above process check didn't catch it + if self._check_systemd_run_process(process): + continue # systemd_run_commands contains the shell that started systemd-run, so we also need to check for the parent if self._get_parent(process) in systemd_run_commands and self._get_command( process) == 'systemd-run': @@ -681,6 +685,7 @@ def _check_processes_in_agent_cgroup(self): _log_cgroup_warning("Error checking the processes in the agent's cgroup: {0}".format(ustr(exception))) if len(unexpected) > 0: + self._report_agent_cgroups_procs(agent_cgroup_proc_names, unexpected) raise CGroupsException("The agent's cgroup includes unexpected processes: {0}".format(unexpected)) @staticmethod @@ -743,6 +748,33 @@ def __is_zombie_process(pid): pass return False + @staticmethod + def _check_systemd_run_process(process): + """ + Returns True if process is shell systemd-run process started by agent otherwise False. + + Ex: sh,7345 -c systemd-run --unit=enable_7c5cab19-eb79-4661-95d9-9e5091bd5ae0 --scope --slice=azure-vmextensions-Microsoft.OSTCExtensions.VMAccessForLinux_1.5.11.slice /var/lib/waagent/Microsoft.OSTCExtensions.VMAccessForLinux-1.5.11/processes.sh + """ + try: + process_name = "UNKNOWN" + cmdline = '/proc/{0}/cmdline'.format(process) + if os.path.exists(cmdline): + with open(cmdline, "r") as cmdline_file: + process_name = "{0}".format(cmdline_file.read()) + match = re.search(r'systemd-run.*--unit=.*--scope.*--slice=azure-vmextensions.*', process_name) + if match is not None: + return True + except Exception: + pass + return False + + @staticmethod + def _report_agent_cgroups_procs(agent_cgroup_proc_names, unexpected): + for proc_name in unexpected: + if 'UNKNOWN' in proc_name: + msg = "Agent includes following processes when UNKNOWN process found: {0}".format("\n".join([ustr(proc) for proc in agent_cgroup_proc_names])) + add_event(op=WALAEventOperation.CGroupsInfo, message=msg) + @staticmethod def _check_agent_throttled_time(cgroup_metrics): for metric in cgroup_metrics: From 1778742424575f3a46cee536de815cb54259edce Mon Sep 17 00:00:00 2001 From: Norberto Arrieta Date: Tue, 10 Jan 2023 16:32:57 -0800 Subject: [PATCH 26/63] Use CalledProcessError.output instead of CalledProcessError.stdout (#2727) Co-authored-by: narrieta --- makepkg.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/makepkg.py b/makepkg.py index 25b209229b..5ec04d5d89 100755 --- a/makepkg.py +++ b/makepkg.py @@ -53,7 +53,7 @@ def do(*args): try: return subprocess.check_output(args, stderr=subprocess.STDOUT) except subprocess.CalledProcessError as e: # pylint: disable=C0103 - raise Exception("[{0}] failed:\n{1}\n{2}".format(" ".join(args), str(e), e.stdout)) + raise Exception("[{0}] failed:\n{1}\n{2}".format(" ".join(args), str(e), e.output)) def run(agent_family, output_directory, log): From abf4161d89384f264eebe0ac172472d27616329b Mon Sep 17 00:00:00 2001 From: Norberto Arrieta Date: Thu, 12 Jan 2023 12:29:50 -0800 Subject: [PATCH 27/63] Add support to execute tests on multiple distros (#2725) * Execute BVT on multiple distros * concurrency; distro differences * add all distros * remove comment * Update Dockerfile Co-authored-by: narrieta --- tests_e2e/docker/Dockerfile | 8 +- .../orchestrator/lib/agent_test_suite.py | 208 ++++++++++-------- tests_e2e/orchestrator/scripts/collect-logs | 19 +- tests_e2e/orchestrator/scripts/install-agent | 33 +-- tests_e2e/scenarios/lib/agent_test_context.py | 21 +- tests_e2e/scenarios/lib/logging.py | 2 +- tests_e2e/scenarios/runbooks/daily.yml | 70 +++--- .../scenarios/runbooks/include/ssh_proxy.yml | 19 ++ .../runbooks/samples/existing_vm.yml | 65 ++++++ .../{ => local_machine}/hello_world.py | 0 .../samples/{ => local_machine}/local.yml | 4 + 11 files changed, 290 insertions(+), 159 deletions(-) create mode 100644 tests_e2e/scenarios/runbooks/include/ssh_proxy.yml create mode 100644 tests_e2e/scenarios/runbooks/samples/existing_vm.yml rename tests_e2e/scenarios/runbooks/samples/{ => local_machine}/hello_world.py (100%) rename tests_e2e/scenarios/runbooks/samples/{ => local_machine}/local.yml (93%) diff --git a/tests_e2e/docker/Dockerfile b/tests_e2e/docker/Dockerfile index 0489d3907f..752aa4ff28 100644 --- a/tests_e2e/docker/Dockerfile +++ b/tests_e2e/docker/Dockerfile @@ -23,7 +23,7 @@ RUN \ # \ # Install basic dependencies \ # \ - apt-get install -y git python3.10 python3.10-dev curl && \ + apt-get install -y git python3.10 python3.10-dev && \ ln /usr/bin/python3.10 /usr/bin/python3 && \ \ # \ @@ -31,6 +31,12 @@ RUN \ # \ apt-get install -y git gcc libgirepository1.0-dev libcairo2-dev qemu-utils libvirt-dev \ python3-pip python3-venv && \ + \ + # \ + # Install test dependencies \ + # \ + apt-get install -y zip && \ + \ # \ # Create user waagent, which is used to execute the tests \ # \ diff --git a/tests_e2e/orchestrator/lib/agent_test_suite.py b/tests_e2e/orchestrator/lib/agent_test_suite.py index c4d94a2421..ee2f511a60 100644 --- a/tests_e2e/orchestrator/lib/agent_test_suite.py +++ b/tests_e2e/orchestrator/lib/agent_test_suite.py @@ -14,9 +14,11 @@ # See the License for the specific language governing permissions and # limitations under the License. # -from assertpy import assert_that +import re + +from assertpy import fail from pathlib import Path -from shutil import rmtree +from threading import current_thread, RLock from typing import List, Type # Disable those warnings, since 'lisa' is an external, non-standard, dependency @@ -40,121 +42,145 @@ from tests_e2e.scenarios.lib.logging import log -class AgentLisaTestContext(AgentTestContext): - """ - Execution context for LISA tests. - """ - def __init__(self, vm: VmIdentifier, node: Node): - super().__init__( - vm=vm, - paths=AgentTestContext.Paths(remote_working_directory=Path('/home')/node.connection_info['username']), - connection=AgentTestContext.Connection( - ip_address=node.connection_info['address'], - username=node.connection_info['username'], - private_key_file=node.connection_info['private_key_file'], - ssh_port=node.connection_info['port']) - ) - self._node = node - - @property - def node(self) -> Node: - return self._node - - class AgentTestSuite(TestSuite): """ Base class for Agent test suites. It provides facilities for setup, execution of tests and reporting results. Derived classes use the execute() method to run the tests in their corresponding suites. """ + + class _Context(AgentTestContext): + def __init__(self, vm: VmIdentifier, paths: AgentTestContext.Paths, connection: AgentTestContext.Connection): + super().__init__(vm=vm, paths=paths, connection=connection) + # These are initialized by AgentTestSuite._set_context(). + self.node: Node = None + self.runbook_name: str = None + self.suite_name: str = None + def __init__(self, metadata: TestSuiteMetadata) -> None: super().__init__(metadata) - # The context is initialized by execute() - self.__context: AgentLisaTestContext = None - - @property - def context(self) -> AgentLisaTestContext: - if self.__context is None: - raise Exception("The context for the AgentTestSuite has not been initialized") - return self.__context + # The context is initialized by _set_context() via the call to execute() + self.__context: AgentTestSuite._Context = None def _set_context(self, node: Node): + connection_info = node.connection_info node_context = get_node_context(node) - runbook = node.capability.get_extended_runbook(AzureNodeSchema, AZURE) + # Remove the resource group and node suffix, e.g. "e1-n0" in "lisa-20230110-162242-963-e1-n0" + runbook_name = re.sub(r"-\w+-\w+$", "", runbook.name) - self.__context = AgentLisaTestContext( - VmIdentifier( + self.__context = AgentTestSuite._Context( + vm=VmIdentifier( location=runbook.location, subscription=node.features._platform.subscription_id, resource_group=node_context.resource_group_name, - name=node_context.vm_name - ), - node - ) + name=node_context.vm_name), + paths=AgentTestContext.Paths( + # The runbook name is unique on each run, so we will use different working directory every time + working_directory=Path().home()/"tmp"/runbook_name, + remote_working_directory=Path('/home')/connection_info['username']), + connection=AgentTestContext.Connection( + ip_address=connection_info['address'], + username=connection_info['username'], + private_key_file=connection_info['private_key_file'], + ssh_port=connection_info['port'])) - def _setup(self) -> None: - """ - Prepares the test suite for execution - """ - log.info("Test Node: %s", self.context.vm.name) - log.info("Resource Group: %s", self.context.vm.resource_group) - log.info("Working directory: %s", self.context.working_directory) + self.__context.node = node + self.__context.suite_name = f"{self._metadata.full_name}:{runbook.marketplace.offer}-{runbook.marketplace.sku}" - if self.context.working_directory.exists(): - log.info("Removing existing working directory: %s", self.context.working_directory) - try: - rmtree(self.context.working_directory.as_posix()) - except Exception as exception: - log.warning("Failed to remove the working directory: %s", exception) - self.context.working_directory.mkdir() + @property + def context(self): + if self.__context is None: + raise Exception("The context for the AgentTestSuite has not been initialized") + return self.__context - def _clean_up(self) -> None: + # + # Test suites within the same runbook may be executed concurrently, and setup needs to be done only once. + # We use this lock to allow only 1 thread to do the setup. Setup completion is marked using the 'completed' + # file: the thread doing the setup creates the file and threads that find that the file already exists + # simply skip setup. + # + _setup_lock = RLock() + + def _setup(self) -> None: """ - Cleans up any leftovers from the test suite run. + Prepares the test suite for execution (currently, it just builds the agent package) + + Returns the path to the agent package. """ + AgentTestSuite._setup_lock.acquire() + try: - log.info("Removing %s", self.context.working_directory) - rmtree(self.context.working_directory.as_posix(), ignore_errors=True) - except: # pylint: disable=bare-except - log.exception("Failed to cleanup the test run") + log.info("") + log.info("**************************************** [Build] ****************************************") + log.info("") + completed: Path = self.context.working_directory/"completed" - def _setup_node(self) -> None: - """ - Prepares the remote node for executing the test suite. - """ - agent_package_path = self._build_agent_package() - self._install_agent_on_node(agent_package_path) + if completed.exists(): + log.info("Found %s. Build has already been done, skipping", completed) + return + + log.info("Creating working directory: %s", self.context.working_directory) + self.context.working_directory.mkdir(parents=True) + self._build_agent_package() - def _build_agent_package(self) -> Path: + log.info("Completed setup, creating %s", completed) + completed.touch() + + finally: + AgentTestSuite._setup_lock.release() + + def _build_agent_package(self) -> None: """ Builds the agent package and returns the path to the package. """ - build_path = self.context.working_directory/"build" - - log.info("Building agent package to %s", build_path) + log.info("Building agent package to %s", self.context.working_directory) - makepkg.run(agent_family="Test", output_directory=str(build_path), log=log) + makepkg.run(agent_family="Test", output_directory=str(self.context.working_directory), log=log) - package_path = build_path/"eggs"/f"WALinuxAgent-{AGENT_VERSION}.zip" + package_path: Path = self._get_agent_package_path() if not package_path.exists(): raise Exception(f"Can't find the agent package at {package_path}") - log.info("Agent package: %s", package_path) + log.info("Built agent package as %s", package_path) + + def _get_agent_package_path(self) -> Path: + """ + Returns the path to the agent package. + """ + return self.context.working_directory/"eggs"/f"WALinuxAgent-{AGENT_VERSION}.zip" - return package_path + def _clean_up(self) -> None: + """ + Cleans up any leftovers from the test suite run. Currently just an empty placeholder for future use. + """ - def _install_agent_on_node(self, agent_package: Path) -> None: + def _setup_node(self) -> None: + """ + Prepares the remote node for executing the test suite. + """ + log.info("") + log.info("************************************** [Node Setup] **************************************") + log.info("") + log.info("Test Node: %s", self.context.vm.name) + log.info("Resource Group: %s", self.context.vm.resource_group) + log.info("") + + self._install_agent_on_node() + + def _install_agent_on_node(self) -> None: """ Installs the given agent package on the test node. """ + agent_package_path: Path = self._get_agent_package_path() + # The install script needs to unzip the agent package; ensure unzip is installed on the test node log.info("Installing unzip tool on %s", self.context.node.name) self.context.node.os.install_packages("unzip") - log.info("Installing %s on %s", agent_package, self.context.node.name) - agent_package_remote_path = self.context.remote_working_directory / agent_package.name - log.info("Copying %s to %s:%s", agent_package, self.context.node.name, agent_package_remote_path) - self.context.node.shell.copy(agent_package, agent_package_remote_path) + log.info("Installing %s on %s", agent_package_path, self.context.node.name) + agent_package_remote_path = self.context.remote_working_directory/agent_package_path.name + log.info("Copying %s to %s:%s", agent_package_path, self.context.node.name, agent_package_remote_path) + self.context.node.shell.copy(agent_package_path, agent_package_remote_path) self.execute_script_on_node( self.context.test_source_directory/"orchestrator"/"scripts"/"install-agent", parameters=f"--package {agent_package_remote_path} --version {AGENT_VERSION}", @@ -172,8 +198,8 @@ def _collect_node_logs(self) -> None: self.execute_script_on_node(self.context.test_source_directory/"orchestrator"/"scripts"/"collect-logs", sudo=True) # Copy the tarball to the local logs directory - remote_path = self.context.remote_working_directory / "logs.tgz" - local_path = Path.home()/'logs'/'vm-logs-{0}.tgz'.format(self.context.node.name) + remote_path = "/tmp/waagent-logs.tgz" + local_path = Path.home()/'logs'/'vm-logs-{0}.tgz'.format(self.context.suite_name) log.info("Copying %s:%s to %s", self.context.node.name, remote_path, local_path) self.context.node.shell.copy_back(remote_path, local_path) except: # pylint: disable=bare-except @@ -186,12 +212,11 @@ def execute(self, node: Node, test_suite: List[Type[AgentTest]]) -> None: """ self._set_context(node) - log.info("") - log.info("**************************************** [Setup] ****************************************") - log.info("") - failed: List[str] = [] + thread_name = current_thread().name + current_thread().name = self.context.suite_name + try: self._setup() @@ -233,15 +258,21 @@ def execute(self, node: Node, test_suite: List[Type[AgentTest]]) -> None: for r in results: log.info("\t%s", r) log.info("") - finally: self._collect_node_logs() + except: # pylint: disable=bare-except + # Log the error here so the it is decorated with the thread name, then re-raise + log.exception("Test suite failed") + raise + finally: self._clean_up() + current_thread().name = thread_name # Fail the entire test suite if any test failed - assert_that(failed).described_as("One or more tests failed").is_length(0) + if len(failed) > 0: + fail(f"{[self.context.suite_name]} One or more tests failed: {failed}") def execute_script_on_node(self, script_path: Path, parameters: str = "", sudo: bool = False) -> int: """ @@ -259,10 +290,6 @@ def execute_script_on_node(self, script_path: Path, parameters: str = "", sudo: result = custom_script.run(parameters=parameters, sudo=sudo) - if result.exit_code != 0: - output = result.stdout if result.stderr == "" else f"{result.stdout}\n{result.stderr}" - raise Exception(f"[{command_line}] failed:\n{output}") - if result.stdout != "": separator = "\n" if "\n" in result.stdout else " " log.info("stdout:%s%s", separator, result.stdout) @@ -270,6 +297,9 @@ def execute_script_on_node(self, script_path: Path, parameters: str = "", sudo: separator = "\n" if "\n" in result.stderr else " " log.error("stderr:%s%s", separator, result.stderr) + if result.exit_code != 0: + raise Exception(f"[{command_line}] failed. Exit code: {result.exit_code}") + return result.exit_code diff --git a/tests_e2e/orchestrator/scripts/collect-logs b/tests_e2e/orchestrator/scripts/collect-logs index 46a23aff18..eadf0483ae 100755 --- a/tests_e2e/orchestrator/scripts/collect-logs +++ b/tests_e2e/orchestrator/scripts/collect-logs @@ -2,24 +2,33 @@ # # Collects the logs needed to debug agent issues into a compressed tarball. # -set -euxo pipefail -logs_file_name="$HOME/logs.tgz" +# Note that we do "set -euxo pipefail" only after executing "tar". That command exits with code 1 on warnings +# and we do not want to consider those as failures. + +logs_file_name="/tmp/waagent-logs.tgz" echo "Collecting logs to $logs_file_name ..." tar --exclude='journal/*' --exclude='omsbundle' --exclude='omsagent' --exclude='mdsd' --exclude='scx*' \ --exclude='*.so' --exclude='*__LinuxDiagnostic__*' --exclude='*.zip' --exclude='*.deb' --exclude='*.rpm' \ + --warning=no-file-changed \ -czf "$logs_file_name" \ /var/log \ /var/lib/waagent/ \ /etc/waagent.conf -# tar exits with 1 on warnings; ignore those +set -euxo pipefail + +# Ignore warnings (exit code 1) exit_code=$? -if [ "$exit_code" != "1" ] && [ "$exit_code" != "0" ]; then + +if [ "$exit_code" == "1" ]; then + echo "WARNING: tar exit code is 1" +elif [ "$exit_code" != "0" ]; then exit $exit_code fi -chmod +r "$logs_file_name" +chmod a+r "$logs_file_name" +ls -l "$logs_file_name" diff --git a/tests_e2e/orchestrator/scripts/install-agent b/tests_e2e/orchestrator/scripts/install-agent index 439bdcec65..0b513569f9 100755 --- a/tests_e2e/orchestrator/scripts/install-agent +++ b/tests_e2e/orchestrator/scripts/install-agent @@ -51,9 +51,22 @@ if [ "$#" -ne 0 ] || [ -z ${package+x} ] || [ -z ${version+x} ]; then fi # -# The service name is walinuxagent in Ubuntu and waagent elsewhere +# Find the command to manage services # -if service walinuxagent status > /dev/null;then +if command -v systemctl &> /dev/null; then + service-status() { systemctl --no-pager -l status $1; } + service-stop() { systemctl stop $1; } + service-start() { systemctl start $1; } +else + service-status() { service $1 status; } + service-stop() { service $1 stop; } + service-start() { service $1 start; } +fi + +# +# Find the service name (walinuxagent in Ubuntu and waagent elsewhere) +# +if service-status walinuxagent > /dev/null 2>&1;then service_name="walinuxagent" else service_name="waagent" @@ -71,16 +84,12 @@ sed -i 's/AutoUpdate.Enabled=n/AutoUpdate.Enabled=y/g' /etc/waagent.conf # Restart the service # echo "Restarting service..." -service $service_name stop +service-stop $service_name # Rename the previous log to ensure the new log starts with the agent we just installed mv /var/log/waagent.log /var/log/waagent."$(date --iso-8601=seconds)".log -if command -v systemctl &> /dev/null; then - systemctl daemon-reload -fi - -service $service_name start +service-start $service_name # # Verify that the new agent is running and output its status. Note that the extension handler @@ -108,13 +117,7 @@ else fi waagent --version - printf "\n" - -if command -v systemctl &> /dev/null; then - systemctl --no-pager -l status $service_name -else - service $service_name status -fi +service-status $service_name exit $exit_code diff --git a/tests_e2e/scenarios/lib/agent_test_context.py b/tests_e2e/scenarios/lib/agent_test_context.py index b35e93a80d..6c008fe480 100644 --- a/tests_e2e/scenarios/lib/agent_test_context.py +++ b/tests_e2e/scenarios/lib/agent_test_context.py @@ -26,18 +26,20 @@ class AgentTestContext: """ Execution context for agent tests. Defines the test VM, working directories and connection info for the tests. - """ + NOTE: The context is shared by all tests in the same runbook execution. Tests within the same test suite + are executed sequentially, but multiple test suites may be executed concurrently depending on the + concurrency level of the runbook. + """ class Paths: # E1101: Instance of 'list' has no '_path' member (no-member) DEFAULT_TEST_SOURCE_DIRECTORY = Path(tests_e2e.__path__._path[0]) # pylint: disable=E1101 - DEFAULT_WORKING_DIRECTORY = Path().home() / "waagent-tmp" def __init__( self, + working_directory: Path, remote_working_directory: Path, - test_source_directory: Path = DEFAULT_TEST_SOURCE_DIRECTORY, - working_directory: Path = DEFAULT_WORKING_DIRECTORY + test_source_directory: Path = DEFAULT_TEST_SOURCE_DIRECTORY ): self._test_source_directory: Path = test_source_directory self._working_directory: Path = working_directory @@ -87,14 +89,15 @@ def test_source_directory(self) -> Path: @property def working_directory(self) -> Path: """ - Tests create temporary files under this directory + Tests can create temporary files under this directory. + """ return self._paths._working_directory @property def remote_working_directory(self) -> Path: """ - Tests create temporary files under this directory on the test VM + Tests can create temporary files under this directory on the test VM. """ return self._paths._remote_working_directory @@ -132,7 +135,7 @@ def from_args(): parser.add_argument('-rw', '--remote-working-directory', dest="remote_working_directory", required=False, default=str(Path('/home')/os.getenv("USER"))) parser.add_argument('-t', '--test-source-directory', dest="test_source_directory", required=False, default=str(AgentTestContext.Paths.DEFAULT_TEST_SOURCE_DIRECTORY)) - parser.add_argument('-w', '--working-directory', dest="working_directory", required=False, default=str(AgentTestContext.Paths.DEFAULT_WORKING_DIRECTORY)) + parser.add_argument('-w', '--working-directory', dest="working_directory", required=False, default=str(Path().home()/"tmp")) parser.add_argument('-a', '--ip-address', dest="ip_address", required=False) # Use the vm name as default parser.add_argument('-u', '--username', required=False, default=os.getenv("USER")) @@ -152,9 +155,9 @@ def from_args(): resource_group=args.group, name=args.vm), paths=AgentTestContext.Paths( + working_directory=working_directory, remote_working_directory=Path(args.remote_working_directory), - test_source_directory=Path(args.test_source_directory), - working_directory=working_directory), + test_source_directory=Path(args.test_source_directory)), connection=AgentTestContext.Connection( ip_address=args.ip_address if args.ip_address is not None else args.vm, username=args.username, diff --git a/tests_e2e/scenarios/lib/logging.py b/tests_e2e/scenarios/lib/logging.py index 2cb523d6bc..d0629a21d9 100644 --- a/tests_e2e/scenarios/lib/logging.py +++ b/tests_e2e/scenarios/lib/logging.py @@ -32,6 +32,6 @@ log.setLevel(logging.INFO) -formatter = logging.Formatter('%(asctime)s.%(msecs)03d [%(levelname)s] %(message)s', datefmt="%Y-%m-%dT%H:%M:%SZ") +formatter = logging.Formatter('%(asctime)s.%(msecs)03d [%(levelname)s] [%(threadName)s] %(message)s', datefmt="%Y-%m-%dT%H:%M:%SZ") for handler in log.handlers: handler.setFormatter(formatter) diff --git a/tests_e2e/scenarios/runbooks/daily.yml b/tests_e2e/scenarios/runbooks/daily.yml index fe723b0f9f..016e3695a7 100644 --- a/tests_e2e/scenarios/runbooks/daily.yml +++ b/tests_e2e/scenarios/runbooks/daily.yml @@ -1,29 +1,23 @@ -name: azure +name: Daily + +testcase: + - criteria: + area: bvt + extension: - "../testsuites" + variable: - name: location value: "westus2" - name: subscription_id value: "" - - name: resource_group_name - value: "" - # - # Set the vm_name to run on an existing VM - # - - name: vm_name - value: "" - name: marketplace_image - value: "Canonical UbuntuServer 18.04-LTS latest" + value: "" - name: vhd value: "" - name: vm_size value: "" - # - # Turn off deploy to run on an existing VM - # - - name: deploy - value: true - name: keep_environment value: "no" - name: wait_delete @@ -36,16 +30,7 @@ variable: - name: admin_password value: "" is_secret: true - - name: proxy_host - value: "" - - name: proxy_user - value: "" - - name: proxy_identity_file - value: "" - is_secret: true -notifier: - - type: env_stats - - type: junit + platform: - type: azure admin_username: $(user) @@ -53,8 +38,7 @@ platform: admin_password: $(admin_password) keep_environment: $(keep_environment) azure: - resource_group_name: $(resource_group_name) - deploy: $(deploy) + deploy: True subscription_id: $(subscription_id) wait_delete: $(wait_delete) requirement: @@ -64,19 +48,27 @@ platform: marketplace: "$(marketplace_image)" vhd: $(vhd) location: $(location) - name: $(vm_name) vm_size: $(vm_size) -testcase: - - criteria: - area: bvt +combinator: + type: grid + items: + - name: marketplace_image + value: + - "Canonical UbuntuServer 18.04-LTS latest" + - "Debian debian-10 10 latest" + - "OpenLogic CentOS 7_9 latest" + - "SUSE sles-15-sp2-basic gen2 latest" + - "RedHat RHEL 7-RAW latest" + - "microsoftcblmariner cbl-mariner cbl-mariner-1 latest" + - "microsoftcblmariner cbl-mariner cbl-mariner-2 latest" + +concurrency: 10 + +notifier: + - type: env_stats + - type: junit + +include: + - path: ./include/ssh_proxy.yml -# -# Set to do SSH proxy jumps -# -#dev: -# mock_tcp_ping: True -# jump_boxes: -# - private_key_file: $(proxy_identity_file) -# address: $(proxy_host) -# username: $(proxy_user) diff --git a/tests_e2e/scenarios/runbooks/include/ssh_proxy.yml b/tests_e2e/scenarios/runbooks/include/ssh_proxy.yml new file mode 100644 index 0000000000..84704cb448 --- /dev/null +++ b/tests_e2e/scenarios/runbooks/include/ssh_proxy.yml @@ -0,0 +1,19 @@ +variable: + - name: proxy + value: False + - name: proxy_host + value: "" + - name: proxy_user + value: "foo" + - name: proxy_identity_file + value: "" + is_secret: true + +dev: + enabled: $(proxy) + mock_tcp_ping: $(proxy) + jump_boxes: + - private_key_file: $(proxy_identity_file) + address: $(proxy_host) + username: $(proxy_user) + password: "dummy" diff --git a/tests_e2e/scenarios/runbooks/samples/existing_vm.yml b/tests_e2e/scenarios/runbooks/samples/existing_vm.yml new file mode 100644 index 0000000000..2d8057a864 --- /dev/null +++ b/tests_e2e/scenarios/runbooks/samples/existing_vm.yml @@ -0,0 +1,65 @@ +# 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. +# + +# +# Executes the test suites on an existing VM +# +name: ExistingVM + +testcase: + - criteria: + area: bvt + +extension: + - "../../testsuites" + +variable: + - name: subscription_id + value: "" + + - name: resource_group_name + value: "" + - name: vm_name + value: "" + - name: location + value: "" + + - name: user + value: "" + - name: identity_file + value: "" + is_secret: true + +platform: + - type: azure + admin_username: $(user) + admin_private_key_file: $(identity_file) + azure: + resource_group_name: $(resource_group_name) + deploy: false + subscription_id: $(subscription_id) + requirement: + azure: + location: $(location) + name: $(vm_name) + +notifier: + - type: env_stats + - type: junit + +include: + - path: ../include/ssh_proxy.yml diff --git a/tests_e2e/scenarios/runbooks/samples/hello_world.py b/tests_e2e/scenarios/runbooks/samples/local_machine/hello_world.py similarity index 100% rename from tests_e2e/scenarios/runbooks/samples/hello_world.py rename to tests_e2e/scenarios/runbooks/samples/local_machine/hello_world.py diff --git a/tests_e2e/scenarios/runbooks/samples/local.yml b/tests_e2e/scenarios/runbooks/samples/local_machine/local.yml similarity index 93% rename from tests_e2e/scenarios/runbooks/samples/local.yml rename to tests_e2e/scenarios/runbooks/samples/local_machine/local.yml index f5edec65b2..c397159f88 100644 --- a/tests_e2e/scenarios/runbooks/samples/local.yml +++ b/tests_e2e/scenarios/runbooks/samples/local_machine/local.yml @@ -15,6 +15,10 @@ # limitations under the License. # +# +# Executes the test suites on the local machine +# + extension: - "." environment: From 4b8fe5ebda583dbea94a94e5018a3f06c3c1ab15 Mon Sep 17 00:00:00 2001 From: Norberto Arrieta Date: Wed, 18 Jan 2023 15:37:34 -0800 Subject: [PATCH 28/63] Create individual log files for each (distro, test suite) pair (#2731) * Create individual log files for each test * cleanup * typo * remove duplicate Co-authored-by: narrieta --- .../orchestrator/lib/agent_test_suite.py | 171 +++++++++++------- tests_e2e/orchestrator/scripts/run-scenarios | 11 +- tests_e2e/pipeline/scripts/execute_tests.sh | 18 +- tests_e2e/scenarios/lib/agent_test_context.py | 3 +- tests_e2e/scenarios/lib/logging.py | 114 ++++++++++-- tests_e2e/scenarios/runbooks/daily.yml | 5 + tests_e2e/scenarios/tests/error_test.py | 32 ++++ tests_e2e/scenarios/tests/fail_test.py | 33 ++++ tests_e2e/scenarios/tests/pass_test.py | 33 ++++ tests_e2e/scenarios/testsuites/agent_bvt.py | 7 +- 10 files changed, 334 insertions(+), 93 deletions(-) create mode 100755 tests_e2e/scenarios/tests/error_test.py create mode 100755 tests_e2e/scenarios/tests/fail_test.py create mode 100755 tests_e2e/scenarios/tests/pass_test.py diff --git a/tests_e2e/orchestrator/lib/agent_test_suite.py b/tests_e2e/orchestrator/lib/agent_test_suite.py index ee2f511a60..4f54949ce7 100644 --- a/tests_e2e/orchestrator/lib/agent_test_suite.py +++ b/tests_e2e/orchestrator/lib/agent_test_suite.py @@ -14,6 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. # +import logging import re from assertpy import fail @@ -27,6 +28,7 @@ # E0401: Unable to import 'lisa.sut_orchestrator.azure.common' (import-error) from lisa import ( # pylint: disable=E0401 CustomScriptBuilder, + Logger, Node, TestSuite, TestSuiteMetadata @@ -39,7 +41,31 @@ from tests_e2e.scenarios.lib.agent_test import AgentTest from tests_e2e.scenarios.lib.agent_test_context import AgentTestContext from tests_e2e.scenarios.lib.identifiers import VmIdentifier -from tests_e2e.scenarios.lib.logging import log +from tests_e2e.scenarios.lib.logging import log as agent_test_logger # Logger used by the tests + + +def _initialize_lisa_logger(): + """ + Customizes the LISA logger. + + The default behavior of this logger is too verbose, which makes reading the logs difficult. We set up a more succinct + formatter and decrease the log level to INFO (the default is VERBOSE). In the future we may consider making this + customization settable at runtime in case we need to debug LISA issues. + """ + logger: Logger = logging.getLogger("lisa") + + logger.setLevel(logging.INFO) + + formatter = logging.Formatter('%(asctime)s.%(msecs)03d [%(levelname)s] [%(threadName)s] %(message)s', datefmt="%Y-%m-%dT%H:%M:%SZ") + for handler in logger.handlers: + handler.setFormatter(formatter) + + +# +# We want to customize the LISA logger as early as possible, so we do it when this module is first imported. That will +# happen early in the LISA workflow, when it loads the test suites to execute. +# +_initialize_lisa_logger() class AgentTestSuite(TestSuite): @@ -52,6 +78,7 @@ class _Context(AgentTestContext): def __init__(self, vm: VmIdentifier, paths: AgentTestContext.Paths, connection: AgentTestContext.Connection): super().__init__(vm=vm, paths=paths, connection=connection) # These are initialized by AgentTestSuite._set_context(). + self.log: Logger = None self.node: Node = None self.runbook_name: str = None self.suite_name: str = None @@ -61,7 +88,7 @@ def __init__(self, metadata: TestSuiteMetadata) -> None: # The context is initialized by _set_context() via the call to execute() self.__context: AgentTestSuite._Context = None - def _set_context(self, node: Node): + def _set_context(self, node: Node, log: Logger): connection_info = node.connection_info node_context = get_node_context(node) runbook = node.capability.get_extended_runbook(AzureNodeSchema, AZURE) @@ -84,8 +111,9 @@ def _set_context(self, node: Node): private_key_file=connection_info['private_key_file'], ssh_port=connection_info['port'])) + self.__context.log = log self.__context.node = node - self.__context.suite_name = f"{self._metadata.full_name}:{runbook.marketplace.offer}-{runbook.marketplace.sku}" + self.__context.suite_name = f"{self._metadata.full_name}_{runbook.marketplace.offer}-{runbook.marketplace.sku}" @property def context(self): @@ -93,6 +121,13 @@ def context(self): raise Exception("The context for the AgentTestSuite has not been initialized") return self.__context + @property + def _log(self) -> Logger: + """ + Returns a reference to the LISA Logger. + """ + return self.context.log + # # Test suites within the same runbook may be executed concurrently, and setup needs to be done only once. # We use this lock to allow only 1 thread to do the setup. Setup completion is marked using the 'completed' @@ -110,20 +145,20 @@ def _setup(self) -> None: AgentTestSuite._setup_lock.acquire() try: - log.info("") - log.info("**************************************** [Build] ****************************************") - log.info("") + self._log.info("") + self._log.info("**************************************** [Build] ****************************************") + self._log.info("") completed: Path = self.context.working_directory/"completed" if completed.exists(): - log.info("Found %s. Build has already been done, skipping", completed) + self._log.info("Found %s. Build has already been done, skipping", completed) return - log.info("Creating working directory: %s", self.context.working_directory) + self._log.info("Creating working directory: %s", self.context.working_directory) self.context.working_directory.mkdir(parents=True) self._build_agent_package() - log.info("Completed setup, creating %s", completed) + self._log.info("Completed setup, creating %s", completed) completed.touch() finally: @@ -133,15 +168,15 @@ def _build_agent_package(self) -> None: """ Builds the agent package and returns the path to the package. """ - log.info("Building agent package to %s", self.context.working_directory) + self._log.info("Building agent package to %s", self.context.working_directory) - makepkg.run(agent_family="Test", output_directory=str(self.context.working_directory), log=log) + makepkg.run(agent_family="Test", output_directory=str(self.context.working_directory), log=self._log) package_path: Path = self._get_agent_package_path() if not package_path.exists(): raise Exception(f"Can't find the agent package at {package_path}") - log.info("Built agent package as %s", package_path) + self._log.info("Built agent package as %s", package_path) def _get_agent_package_path(self) -> Path: """ @@ -158,12 +193,12 @@ def _setup_node(self) -> None: """ Prepares the remote node for executing the test suite. """ - log.info("") - log.info("************************************** [Node Setup] **************************************") - log.info("") - log.info("Test Node: %s", self.context.vm.name) - log.info("Resource Group: %s", self.context.vm.resource_group) - log.info("") + self._log.info("") + self._log.info("************************************** [Node Setup] **************************************") + self._log.info("") + self._log.info("Test Node: %s", self.context.vm.name) + self._log.info("Resource Group: %s", self.context.vm.resource_group) + self._log.info("") self._install_agent_on_node() @@ -174,19 +209,19 @@ def _install_agent_on_node(self) -> None: agent_package_path: Path = self._get_agent_package_path() # The install script needs to unzip the agent package; ensure unzip is installed on the test node - log.info("Installing unzip tool on %s", self.context.node.name) + self._log.info("Installing unzip tool on %s", self.context.node.name) self.context.node.os.install_packages("unzip") - log.info("Installing %s on %s", agent_package_path, self.context.node.name) + self._log.info("Installing %s on %s", agent_package_path, self.context.node.name) agent_package_remote_path = self.context.remote_working_directory/agent_package_path.name - log.info("Copying %s to %s:%s", agent_package_path, self.context.node.name, agent_package_remote_path) + self._log.info("Copying %s to %s:%s", agent_package_path, self.context.node.name, agent_package_remote_path) self.context.node.shell.copy(agent_package_path, agent_package_remote_path) self.execute_script_on_node( self.context.test_source_directory/"orchestrator"/"scripts"/"install-agent", parameters=f"--package {agent_package_remote_path} --version {AGENT_VERSION}", sudo=True) - log.info("The agent was installed successfully.") + self._log.info("The agent was installed successfully.") def _collect_node_logs(self) -> None: """ @@ -194,83 +229,97 @@ def _collect_node_logs(self) -> None: """ try: # Collect the logs on the test machine into a compressed tarball - log.info("Collecting logs on test machine [%s]...", self.context.node.name) + self._log.info("Collecting logs on test machine [%s]...", self.context.node.name) self.execute_script_on_node(self.context.test_source_directory/"orchestrator"/"scripts"/"collect-logs", sudo=True) # Copy the tarball to the local logs directory remote_path = "/tmp/waagent-logs.tgz" - local_path = Path.home()/'logs'/'vm-logs-{0}.tgz'.format(self.context.suite_name) - log.info("Copying %s:%s to %s", self.context.node.name, remote_path, local_path) + local_path = Path.home()/'logs'/'{0}.tgz'.format(self.context.suite_name) + self._log.info("Copying %s:%s to %s", self.context.node.name, remote_path, local_path) self.context.node.shell.copy_back(remote_path, local_path) except: # pylint: disable=bare-except - log.exception("Failed to collect logs from the test machine") + self._log.exception("Failed to collect logs from the test machine") - def execute(self, node: Node, test_suite: List[Type[AgentTest]]) -> None: + def execute(self, node: Node, log: Logger, test_suite: List[Type[AgentTest]]) -> None: """ Executes each of the AgentTests in the given List. Note that 'test_suite' is a list of test classes, rather than instances of the test class (this method will instantiate each of these test classes). """ - self._set_context(node) + self._set_context(node, log) - failed: List[str] = [] + failed: List[str] = [] # List of failed tests (names only) + # The thread name is added to self._log, set it to the current test suite while we execute it thread_name = current_thread().name current_thread().name = self.context.suite_name + # We create a separate log file for the test suite. + suite_log_file: Path = Path.home()/'logs'/f"{self.context.suite_name}.log" + agent_test_logger.set_current_thread_log(suite_log_file) + try: self._setup() try: self._setup_node() - log.info("") - log.info("**************************************** [%s] ****************************************", self._metadata.full_name) - log.info("") + agent_test_logger.info("") + agent_test_logger.info("**************************************** %s ****************************************", self.context.suite_name) + agent_test_logger.info("") results: List[str] = [] for test in test_suite: - try: - log.info("******************** [%s]", test.__name__) - log.info("") + result: str = "[UNKNOWN]" + test_full_name = f"{self.context.suite_name} {test.__name__}" + agent_test_logger.info("******** Executing %s", test_full_name) + self._log.info("******** Executing %s", test_full_name) + agent_test_logger.info("") + try: test(self.context).run() - - result = f"[Passed] {test.__name__}" - - log.info("") - log.info("******************** %s", result) - log.info("") - - results.append(result) + result = f"[Passed] {test_full_name}" + except AssertionError as e: + failed.append(test.__name__) + result = f"[Failed] {test_full_name}" + agent_test_logger.error("%s", e) + self._log.error("%s", e) except: # pylint: disable=bare-except - result = f"[Failed] {test.__name__}" - - log.info("") - log.exception("******************** %s\n", result) - log.info("") - - results.append(result) failed.append(test.__name__) - - log.info("**************************************** [Test Results] ****************************************") - log.info("") + result = f"[Error] {test_full_name}" + agent_test_logger.exception("UNHANDLED EXCEPTION IN %s", test_full_name) + self._log.exception("UNHANDLED EXCEPTION IN %s", test_full_name) + + agent_test_logger.info("******** %s", result) + agent_test_logger.info("") + self._log.info("******** %s", result) + results.append(result) + + agent_test_logger.info("") + agent_test_logger.info("********* [Test Results]") + agent_test_logger.info("") for r in results: - log.info("\t%s", r) - log.info("") + agent_test_logger.info("\t%s", r) + agent_test_logger.info("") + finally: self._collect_node_logs() except: # pylint: disable=bare-except - # Log the error here so the it is decorated with the thread name, then re-raise - log.exception("Test suite failed") + agent_test_logger.exception("UNHANDLED EXCEPTION IN %s", self.context.suite_name) + # Note that we report the error to the LISA log and then re-raise it. We log it here + # so that the message is decorated with the thread name in the LISA log; we re-raise + # to let LISA know the test errored out (LISA will report that error one more time + # in its log) + self._log.exception("UNHANDLED EXCEPTION IN %s", self.context.suite_name) raise finally: self._clean_up() + agent_test_logger.close_current_thread_log() current_thread().name = thread_name - # Fail the entire test suite if any test failed + # Fail the entire test suite if any test failed; this exception is handled by LISA if len(failed) > 0: fail(f"{[self.context.suite_name]} One or more tests failed: {failed}") @@ -286,16 +335,16 @@ def execute_script_on_node(self, script_path: Path, parameters: str = "", sudo: else: command_line = f"{script_path} {parameters}" - log.info("Executing [%s]", command_line) + self._log.info("Executing [%s]", command_line) result = custom_script.run(parameters=parameters, sudo=sudo) if result.stdout != "": separator = "\n" if "\n" in result.stdout else " " - log.info("stdout:%s%s", separator, result.stdout) + self._log.info("stdout:%s%s", separator, result.stdout) if result.stderr != "": separator = "\n" if "\n" in result.stderr else " " - log.error("stderr:%s%s", separator, result.stderr) + self._log.error("stderr:%s%s", separator, result.stderr) if result.exit_code != 0: raise Exception(f"[{command_line}] failed. Exit code: {result.exit_code}") diff --git a/tests_e2e/orchestrator/scripts/run-scenarios b/tests_e2e/orchestrator/scripts/run-scenarios index 8eecec40d9..e0d3896d35 100755 --- a/tests_e2e/orchestrator/scripts/run-scenarios +++ b/tests_e2e/orchestrator/scripts/run-scenarios @@ -35,10 +35,15 @@ cp "$HOME/id_rsa" "$HOME/.ssh" chmod 700 "$HOME/.ssh/id_rsa" ssh-keygen -y -f "$HOME/.ssh/id_rsa" > "$HOME/.ssh/id_rsa.pub" +# # Now start the runbook +# +lisa_logs="$HOME/logs/lisa" + lisa \ --runbook "$HOME/WALinuxAgent/tests_e2e/scenarios/runbooks/daily.yml" \ - --log_path "$HOME/logs" \ - --working_path "$HOME/logs" \ + --log_path "$lisa_logs" \ + --working_path "$lisa_logs" \ -v subscription_id:"$SUBSCRIPTION_ID" \ - -v identity_file:"$HOME/.ssh/id_rsa" + -v identity_file:"$HOME/.ssh/id_rsa" \ + || true # force a success exit code to allow execution to continue when a test fails diff --git a/tests_e2e/pipeline/scripts/execute_tests.sh b/tests_e2e/pipeline/scripts/execute_tests.sh index 1da857c3cb..0b2cdcd7eb 100755 --- a/tests_e2e/pipeline/scripts/execute_tests.sh +++ b/tests_e2e/pipeline/scripts/execute_tests.sh @@ -29,17 +29,17 @@ docker run --rm \ sudo chown "$USER" "$BUILD_SOURCESDIRECTORY" sudo find "$BUILD_ARTIFACTSTAGINGDIRECTORY" -exec chown "$USER" {} \; -# LISA organizes its logs in a tree similar to +# The LISA run will produce a tree similar to # -# .../20221130 -# .../20221130/20221130-160013-749 -# .../20221130/20221130-160013-749/environments -# .../20221130/20221130-160013-749/lisa-20221130-160013-749.log -# .../20221130/20221130-160013-749/lisa.junit.xml +# $BUILD_ARTIFACTSTAGINGDIRECTORY/lisa/20221130 +# $BUILD_ARTIFACTSTAGINGDIRECTORY/lisa/20221130/20221130-160013-749 +# $BUILD_ARTIFACTSTAGINGDIRECTORY/lisa/20221130/20221130-160013-749/environments +# $BUILD_ARTIFACTSTAGINGDIRECTORY/lisa/20221130/20221130-160013-749/lisa-20221130-160013-749.log +# $BUILD_ARTIFACTSTAGINGDIRECTORY/lisa/20221130/20221130-160013-749/lisa.junit.xml # etc # -# Remove the first 2 levels of the tree (which indicate the time of the test run) to make navigation +# Remove the 2 levels of the tree that indicate the time of the test run to make navigation # in the Azure Pipelines UI easier. # -mv "$BUILD_ARTIFACTSTAGINGDIRECTORY"/[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]/*/* "$BUILD_ARTIFACTSTAGINGDIRECTORY" -rm -r "$BUILD_ARTIFACTSTAGINGDIRECTORY"/[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9] +mv "$BUILD_ARTIFACTSTAGINGDIRECTORY"/lisa/[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]/*/* "$BUILD_ARTIFACTSTAGINGDIRECTORY"/lisa +rm -r "$BUILD_ARTIFACTSTAGINGDIRECTORY"/lisa/[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9] diff --git a/tests_e2e/scenarios/lib/agent_test_context.py b/tests_e2e/scenarios/lib/agent_test_context.py index 6c008fe480..9225177a09 100644 --- a/tests_e2e/scenarios/lib/agent_test_context.py +++ b/tests_e2e/scenarios/lib/agent_test_context.py @@ -32,8 +32,7 @@ class AgentTestContext: concurrency level of the runbook. """ class Paths: - # E1101: Instance of 'list' has no '_path' member (no-member) - DEFAULT_TEST_SOURCE_DIRECTORY = Path(tests_e2e.__path__._path[0]) # pylint: disable=E1101 + DEFAULT_TEST_SOURCE_DIRECTORY = Path(tests_e2e.__path__[0]) def __init__( self, diff --git a/tests_e2e/scenarios/lib/logging.py b/tests_e2e/scenarios/lib/logging.py index d0629a21d9..860d30b662 100644 --- a/tests_e2e/scenarios/lib/logging.py +++ b/tests_e2e/scenarios/lib/logging.py @@ -14,24 +14,110 @@ # See the License for the specific language governing permissions and # limitations under the License. # -import logging # -# This module defines a single object, 'log', which test use for logging. -# -# When the test is invoked as part of a LISA test suite, 'log' references the LISA root logger. -# Otherwise, it references a new Logger named 'waagent'. +# This module defines a single object, 'log', of type AgentLogger, which the end-to-end tests and libraries use +# for logging. # -log: logging.Logger = logging.getLogger("lisa") +from logging import FileHandler, Formatter, Handler, Logger, StreamHandler, INFO +from pathlib import Path +from threading import current_thread +from typing import Dict, Callable + + +class _AgentLoggingHandler(Handler): + """ + AgentLoggingHandler is a helper class for AgentLogger. + + This handler simply redirects logging to other handlers. It maintains a set of FileHandlers associated to specific + threads. When a thread emits a log record, the AgentLoggingHandler passes through the call to the FileHandlers + associated with that thread, or to a StreamHandler that outputs to stdout if there is not a FileHandler for that + thread. + + Thread can set a FileHandler for themselves using _AgentLoggingHandler.set_current_thread_log() and remove that + handler using _AgentLoggingHandler.close_current_thread_log(). + + The _AgentLoggingHandler simply passes through calls to setLevel, setFormatter, flush, and close to the handlers + it maintains. + + AgentLoggingHandler is meant to be primarily used in multithreaded scenarios and is thread-safe. + """ + def __init__(self): + super().__init__() + self.formatter: Formatter = Formatter('%(asctime)s.%(msecs)03d [%(levelname)s] %(message)s', datefmt="%Y-%m-%dT%H:%M:%SZ") + self.default_handler = StreamHandler() + self.default_handler.setFormatter(self.formatter) + self.per_thread_handlers: Dict[int, FileHandler] = {} + + def set_thread_log(self, thread_ident: int, log_file: Path) -> None: + self.close_current_thread_log() + handler: FileHandler = FileHandler(str(log_file)) + handler.setFormatter(self.formatter) + self.per_thread_handlers[thread_ident] = handler + + def close_thread_log(self, thread_ident: int) -> None: + handler = self.per_thread_handlers.pop(thread_ident, None) + if handler is not None: + handler.close() + + def set_current_thread_log(self, log_file: Path) -> None: + self.set_thread_log(current_thread().ident, log_file) + + def close_current_thread_log(self) -> None: + self.close_thread_log(current_thread().ident) + + def emit(self, record) -> None: + handler = self.per_thread_handlers.get(current_thread().ident) + if handler is None: + handler = self.default_handler + handler.emit(record) + + def setLevel(self, level) -> None: + self._for_each_handler(lambda h: h.setLevel(level)) + + def setFormatter(self, fmt) -> None: + self._for_each_handler(lambda h: h.setFormatter(fmt)) + + def flush(self) -> None: + self._for_each_handler(lambda h: h.flush()) + + def close(self) -> None: + self._for_each_handler(lambda h: h.close()) + + def _for_each_handler(self, op: Callable[[Handler], None]) -> None: + op(self.default_handler) + # copy of the values into a new list in case the dictionary changes while we are iterating + for handler in list(self.per_thread_handlers.values()): + op(handler) + + +class AgentLogger(Logger): + """ + AgentLogger is a Logger customized for agent test scenarios. When tests are executed from the command line + (for example, during development) the AgentLogger can be used with its default configuration, which simply + outputs to stdout. When tests are executed from the test framework, typically there are multiple test suites + executed concurrently on different threads, and each test suite must have its own log file; in that case, + each thread can call AgentLogger.set_current_thread_log() to send all the logging from that thread to a + particular file. + """ + def __init__(self): + super().__init__(name="waagent", level=INFO) + self._handler: _AgentLoggingHandler = _AgentLoggingHandler() + self.addHandler(self._handler) + + def set_thread_log(self, thread_ident: int, log_file: Path) -> None: + self._handler.set_thread_log(thread_ident, log_file) + + def close_thread_log(self, thread_ident: int) -> None: + self._handler.close_thread_log(thread_ident) + + def set_current_thread_log(self, log_file: Path) -> None: + self._handler.set_current_thread_log(log_file) + + def close_current_thread_log(self) -> None: + self._handler.close_current_thread_log() -if not log.hasHandlers(): - log = logging.getLogger("waagent") - console_handler = logging.StreamHandler() - log.addHandler(console_handler) -log.setLevel(logging.INFO) +log: AgentLogger = AgentLogger() -formatter = logging.Formatter('%(asctime)s.%(msecs)03d [%(levelname)s] [%(threadName)s] %(message)s', datefmt="%Y-%m-%dT%H:%M:%SZ") -for handler in log.handlers: - handler.setFormatter(formatter) diff --git a/tests_e2e/scenarios/runbooks/daily.yml b/tests_e2e/scenarios/runbooks/daily.yml index 016e3695a7..5124d32ba0 100644 --- a/tests_e2e/scenarios/runbooks/daily.yml +++ b/tests_e2e/scenarios/runbooks/daily.yml @@ -62,6 +62,11 @@ combinator: - "RedHat RHEL 7-RAW latest" - "microsoftcblmariner cbl-mariner cbl-mariner-1 latest" - "microsoftcblmariner cbl-mariner cbl-mariner-2 latest" + # + # TODO: Add this distro, currently available in eastus + # + # - "microsoftcblmariner cbl-mariner cbl-mariner-2-arm64 latest" + # concurrency: 10 diff --git a/tests_e2e/scenarios/tests/error_test.py b/tests_e2e/scenarios/tests/error_test.py new file mode 100755 index 0000000000..b8a0e7eea3 --- /dev/null +++ b/tests_e2e/scenarios/tests/error_test.py @@ -0,0 +1,32 @@ +#!/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. +# + +from tests_e2e.scenarios.lib.agent_test import AgentTest + + +class ErrorTest(AgentTest): + """ + A trivial test that errors out + """ + def run(self): + raise Exception("* ERROR *") + + +if __name__ == "__main__": + ErrorTest.run_from_command_line() diff --git a/tests_e2e/scenarios/tests/fail_test.py b/tests_e2e/scenarios/tests/fail_test.py new file mode 100755 index 0000000000..4b6fd5d60d --- /dev/null +++ b/tests_e2e/scenarios/tests/fail_test.py @@ -0,0 +1,33 @@ +#!/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. +# + +from assertpy import fail +from tests_e2e.scenarios.lib.agent_test import AgentTest + + +class FailTest(AgentTest): + """ + A trivial test that fails + """ + def run(self): + fail("* FAILED *") + + +if __name__ == "__main__": + FailTest.run_from_command_line() diff --git a/tests_e2e/scenarios/tests/pass_test.py b/tests_e2e/scenarios/tests/pass_test.py new file mode 100755 index 0000000000..f1b9c3aad1 --- /dev/null +++ b/tests_e2e/scenarios/tests/pass_test.py @@ -0,0 +1,33 @@ +#!/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. +# + +from tests_e2e.scenarios.lib.agent_test import AgentTest +from tests_e2e.scenarios.lib.logging import log + + +class PassTest(AgentTest): + """ + A trivial test that passes. + """ + def run(self): + log.info("* PASSED *") + + +if __name__ == "__main__": + PassTest.run_from_command_line() diff --git a/tests_e2e/scenarios/testsuites/agent_bvt.py b/tests_e2e/scenarios/testsuites/agent_bvt.py index 0b0383e99a..7bb5647528 100644 --- a/tests_e2e/scenarios/testsuites/agent_bvt.py +++ b/tests_e2e/scenarios/testsuites/agent_bvt.py @@ -22,6 +22,7 @@ # E0401: Unable to import 'lisa' (import-error) from lisa import ( # pylint: disable=E0401 + Logger, Node, TestCaseMetadata, TestSuiteMetadata, @@ -34,15 +35,13 @@ class AgentBvt(AgentTestSuite): Test suite for Agent BVTs """ @TestCaseMetadata(description="", priority=0) - def main(self, node: Node) -> None: + def main(self, node: Node, log: Logger) -> None: self.execute( node, + log, [ ExtensionOperationsBvt, # Tests the basic operations (install, enable, update, uninstall) using CustomScript RunCommandBvt, VmAccessBvt ] ) - - - From 43b7c3c6712a5b9a7b2e2c634db407b852d735e5 Mon Sep 17 00:00:00 2001 From: Norberto Arrieta Date: Thu, 19 Jan 2023 17:03:25 -0800 Subject: [PATCH 29/63] Add lambda in calls to execute_with_retry (#2733) Co-authored-by: narrieta --- tests_e2e/scenarios/lib/virtual_machine.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests_e2e/scenarios/lib/virtual_machine.py b/tests_e2e/scenarios/lib/virtual_machine.py index 6a1f76f961..61eaecdcb7 100644 --- a/tests_e2e/scenarios/lib/virtual_machine.py +++ b/tests_e2e/scenarios/lib/virtual_machine.py @@ -86,7 +86,7 @@ def __str__(self): class VirtualMachine(VirtualMachineBaseClass): def get_instance_view(self) -> VirtualMachineInstanceView: log.info("Retrieving instance view for %s", self._identifier) - return execute_with_retry(self._compute_client.virtual_machines.get( + return execute_with_retry(lambda: self._compute_client.virtual_machines.get( resource_group_name=self._identifier.resource_group, vm_name=self._identifier.name, expand="instanceView" @@ -94,7 +94,7 @@ def get_instance_view(self) -> VirtualMachineInstanceView: def get_extensions(self) -> List[VirtualMachineExtension]: log.info("Retrieving extensions for %s", self._identifier) - return execute_with_retry(self._compute_client.virtual_machine_extensions.list( + return execute_with_retry(lambda: self._compute_client.virtual_machine_extensions.list( resource_group_name=self._identifier.resource_group, vm_name=self._identifier.name)) @@ -133,7 +133,7 @@ def extension_func(self): def get_extensions(self) -> List[VirtualMachineScaleSetExtension]: log.info("Retrieving extensions for %s", self._identifier) - return execute_with_retry(self._compute_client.virtual_machine_scale_set_extensions.list( + return execute_with_retry(lambda: self._compute_client.virtual_machine_scale_set_extensions.list( resource_group_name=self._identifier.resource_group, vm_scale_set_name=self._identifier.name)) From 106979e2a829b9986ee1c3b096b45d629276f33f Mon Sep 17 00:00:00 2001 From: Norberto Arrieta Date: Fri, 20 Jan 2023 14:11:44 -0800 Subject: [PATCH 30/63] Bug fixes for test results (#2732) * Bug fixes for test results * pylint warnings Co-authored-by: narrieta --- tests_e2e/orchestrator/scripts/run-scenarios | 4 +- tests_e2e/pipeline/scripts/execute_tests.sh | 10 +++- .../pipeline/templates/execute-tests.yml | 13 +++-- tests_e2e/scenarios/runbooks/daily.yml | 2 +- .../runbooks/samples/existing_vm.yml | 2 +- tests_e2e/scenarios/testsuites/agent_junit.py | 55 +++++++++++++++++++ 6 files changed, 76 insertions(+), 10 deletions(-) create mode 100644 tests_e2e/scenarios/testsuites/agent_junit.py diff --git a/tests_e2e/orchestrator/scripts/run-scenarios b/tests_e2e/orchestrator/scripts/run-scenarios index e0d3896d35..338c5e550f 100755 --- a/tests_e2e/orchestrator/scripts/run-scenarios +++ b/tests_e2e/orchestrator/scripts/run-scenarios @@ -45,5 +45,5 @@ lisa \ --log_path "$lisa_logs" \ --working_path "$lisa_logs" \ -v subscription_id:"$SUBSCRIPTION_ID" \ - -v identity_file:"$HOME/.ssh/id_rsa" \ - || true # force a success exit code to allow execution to continue when a test fails + -v identity_file:"$HOME/.ssh/id_rsa" + diff --git a/tests_e2e/pipeline/scripts/execute_tests.sh b/tests_e2e/pipeline/scripts/execute_tests.sh index 0b2cdcd7eb..63dea5d4d8 100755 --- a/tests_e2e/pipeline/scripts/execute_tests.sh +++ b/tests_e2e/pipeline/scripts/execute_tests.sh @@ -14,6 +14,10 @@ docker pull waagenttests.azurecr.io/waagenttests:latest sudo chown 1000 "$BUILD_SOURCESDIRECTORY" sudo chown 1000 "$BUILD_ARTIFACTSTAGINGDIRECTORY" +# A test failure will cause automation to exit with an error code and we don't want this script to stop so we force the command +# to succeed and capture the exit code to return it at the end of the script. +echo "exit 0" > /tmp/exit.sh + docker run --rm \ --volume "$BUILD_SOURCESDIRECTORY:/home/waagent/WALinuxAgent" \ --volume "$DOWNLOADSSHKEY_SECUREFILEPATH:/home/waagent/id_rsa" \ @@ -23,7 +27,8 @@ docker run --rm \ --env AZURE_CLIENT_SECRET \ --env AZURE_TENANT_ID \ waagenttests.azurecr.io/waagenttests \ - bash --login -c '$HOME/WALinuxAgent/tests_e2e/orchestrator/scripts/run-scenarios' + bash --login -c '$HOME/WALinuxAgent/tests_e2e/orchestrator/scripts/run-scenarios' \ +|| echo "exit $?" > /tmp/exit.sh # Retake ownership of the source and staging directory (note that the former does not need to be done recursively) sudo chown "$USER" "$BUILD_SOURCESDIRECTORY" @@ -43,3 +48,6 @@ sudo find "$BUILD_ARTIFACTSTAGINGDIRECTORY" -exec chown "$USER" {} \; # mv "$BUILD_ARTIFACTSTAGINGDIRECTORY"/lisa/[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]/*/* "$BUILD_ARTIFACTSTAGINGDIRECTORY"/lisa rm -r "$BUILD_ARTIFACTSTAGINGDIRECTORY"/lisa/[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9] + +cat /tmp/exit.sh +bash /tmp/exit.sh \ No newline at end of file diff --git a/tests_e2e/pipeline/templates/execute-tests.yml b/tests_e2e/pipeline/templates/execute-tests.yml index cb7acea244..82b134cef7 100644 --- a/tests_e2e/pipeline/templates/execute-tests.yml +++ b/tests_e2e/pipeline/templates/execute-tests.yml @@ -26,6 +26,7 @@ jobs: - bash: $(Build.SourcesDirectory)/tests_e2e/pipeline/scripts/execute_tests.sh displayName: "Execute tests" + continueOnError: true env: # Add all KeyVault secrets explicitly as they're not added by default to the environment vars AZURE_CLIENT_ID: $(AZURE-CLIENT-ID) @@ -33,13 +34,15 @@ jobs: AZURE_TENANT_ID: $(AZURE-TENANT-ID) SUBSCRIPTION_ID: $(SUBSCRIPTION-ID) + - publish: $(Build.ArtifactStagingDirectory) + artifact: 'artifacts' + displayName: 'Publish test artifacts' + - task: PublishTestResults@2 + displayName: 'Publish test results' inputs: testResultsFormat: 'JUnit' - testResultsFiles: '**/*junit.xml' + testResultsFiles: 'lisa/agent.junit.xml' searchFolder: $(Build.ArtifactStagingDirectory) - testRunTitle: 'Publish test results' + failTaskOnFailedTests: true - - publish: $(Build.ArtifactStagingDirectory) - artifact: 'test-logs' - displayName: 'Publish test logs' diff --git a/tests_e2e/scenarios/runbooks/daily.yml b/tests_e2e/scenarios/runbooks/daily.yml index 5124d32ba0..d0969699f6 100644 --- a/tests_e2e/scenarios/runbooks/daily.yml +++ b/tests_e2e/scenarios/runbooks/daily.yml @@ -72,7 +72,7 @@ concurrency: 10 notifier: - type: env_stats - - type: junit + - type: agent.junit include: - path: ./include/ssh_proxy.yml diff --git a/tests_e2e/scenarios/runbooks/samples/existing_vm.yml b/tests_e2e/scenarios/runbooks/samples/existing_vm.yml index 2d8057a864..2ae358f609 100644 --- a/tests_e2e/scenarios/runbooks/samples/existing_vm.yml +++ b/tests_e2e/scenarios/runbooks/samples/existing_vm.yml @@ -59,7 +59,7 @@ platform: notifier: - type: env_stats - - type: junit + - type: agent.junit include: - path: ../include/ssh_proxy.yml diff --git a/tests_e2e/scenarios/testsuites/agent_junit.py b/tests_e2e/scenarios/testsuites/agent_junit.py new file mode 100644 index 0000000000..b918f17a3d --- /dev/null +++ b/tests_e2e/scenarios/testsuites/agent_junit.py @@ -0,0 +1,55 @@ +# 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. + +from typing import Type + +# +# Disable those warnings, since 'lisa' is an external, non-standard, dependency +# E0401: Unable to import 'dataclasses_json' (import-error) +# E0401: Unable to import 'lisa.notifiers.junit' (import-error) +# E0401: Unable to import 'lisa' (import-error) +# E0401: Unable to import 'lisa.messages' (import-error) +from dataclasses import dataclass # pylint: disable=E0401 +from dataclasses_json import dataclass_json # pylint: disable=E0401 +from lisa.notifiers.junit import JUnit # pylint: disable=E0401 +from lisa import schema # pylint: disable=E0401 +from lisa.messages import ( # pylint: disable=E0401 + MessageBase, + TestResultMessage, +) + + +@dataclass_json() +@dataclass +class AgentJUnitSchema(schema.Notifier): + path: str = "agent.junit.xml" + + +class AgentJUnit(JUnit): + @classmethod + def type_name(cls) -> str: + return "agent.junit" + + @classmethod + def type_schema(cls) -> Type[schema.TypedSchema]: + return AgentJUnitSchema + + def _received_message(self, message: MessageBase) -> None: + if isinstance(message, TestResultMessage): + distro = message.information.get('distro_version') + if distro is not None: + message.name = distro + super()._received_message(message) From 32dd7a841f2041f850fcccc7f311c03f6b4df253 Mon Sep 17 00:00:00 2001 From: Norberto Arrieta Date: Mon, 23 Jan 2023 15:07:51 -0800 Subject: [PATCH 31/63] Add ARM64 to the daily test run (#2734) * Add ARM64 to the daily test run * remove mariner 2 arm64 Co-authored-by: narrieta --- tests_e2e/scenarios/lib/ssh_client.py | 2 + tests_e2e/scenarios/runbooks/daily.yml | 53 +++++++++++-------- .../tests/bvts/extension_operations.py | 35 ++++++++---- tests_e2e/scenarios/tests/bvts/run_command.py | 27 ++++++---- tests_e2e/scenarios/testsuites/agent_junit.py | 10 ++-- 5 files changed, 78 insertions(+), 49 deletions(-) diff --git a/tests_e2e/scenarios/lib/ssh_client.py b/tests_e2e/scenarios/lib/ssh_client.py index 5e0afbd41a..e4e650e3be 100644 --- a/tests_e2e/scenarios/lib/ssh_client.py +++ b/tests_e2e/scenarios/lib/ssh_client.py @@ -44,3 +44,5 @@ def generate_ssh_key(private_key_file: Path): """ shell.run_command(["ssh-keygen", "-m", "PEM", "-t", "rsa", "-b", "4096", "-q", "-N", "", "-f", str(private_key_file)]) + def get_architecture(self): + return self.run_command("uname -m").rstrip() diff --git a/tests_e2e/scenarios/runbooks/daily.yml b/tests_e2e/scenarios/runbooks/daily.yml index d0969699f6..3ed7c223c3 100644 --- a/tests_e2e/scenarios/runbooks/daily.yml +++ b/tests_e2e/scenarios/runbooks/daily.yml @@ -8,14 +8,8 @@ extension: - "../testsuites" variable: - - name: location - value: "westus2" - name: subscription_id value: "" - - name: marketplace_image - value: "" - - name: vhd - value: "" - name: vm_size value: "" - name: keep_environment @@ -31,6 +25,14 @@ variable: value: "" is_secret: true + # The image and location are set by the combinator + - name: marketplace_image + value: "" + - name: location + value: "" + - name: default_location + value: "westus2" + platform: - type: azure admin_username: $(user) @@ -46,32 +48,37 @@ platform: min: 2 azure: marketplace: "$(marketplace_image)" - vhd: $(vhd) + vhd: "" location: $(location) vm_size: $(vm_size) combinator: - type: grid + type: batch items: - - name: marketplace_image - value: - - "Canonical UbuntuServer 18.04-LTS latest" - - "Debian debian-10 10 latest" - - "OpenLogic CentOS 7_9 latest" - - "SUSE sles-15-sp2-basic gen2 latest" - - "RedHat RHEL 7-RAW latest" - - "microsoftcblmariner cbl-mariner cbl-mariner-1 latest" - - "microsoftcblmariner cbl-mariner cbl-mariner-2 latest" - # - # TODO: Add this distro, currently available in eastus - # - # - "microsoftcblmariner cbl-mariner cbl-mariner-2-arm64 latest" - # + - marketplace_image: "Canonical UbuntuServer 18.04-LTS latest" + location: $(default_location) + - marketplace_image: "Debian debian-10 10 latest" + location: $(default_location) + - marketplace_image: "OpenLogic CentOS 7_9 latest" + location: $(default_location) + - marketplace_image: "SUSE sles-15-sp2-basic gen2 latest" + location: $(default_location) + - marketplace_image: "RedHat RHEL 7-RAW latest" + location: $(default_location) + - marketplace_image: "microsoftcblmariner cbl-mariner cbl-mariner-1 latest" + location: $(default_location) + - marketplace_image: "microsoftcblmariner cbl-mariner cbl-mariner-2 latest" + location: $(default_location) +# +# TODO: Add this image (currently there are allocation issues trying to use it) +# +# # Mariner 2 ARM64 is not available in westus2 currently; use eastus. +# - marketplace_image: "microsoftcblmariner cbl-mariner cbl-mariner-2-arm64 latest" +# location: "eastus" concurrency: 10 notifier: - - type: env_stats - type: agent.junit include: diff --git a/tests_e2e/scenarios/tests/bvts/extension_operations.py b/tests_e2e/scenarios/tests/bvts/extension_operations.py index ae5e0c13b1..e037465bff 100755 --- a/tests_e2e/scenarios/tests/bvts/extension_operations.py +++ b/tests_e2e/scenarios/tests/bvts/extension_operations.py @@ -34,32 +34,47 @@ from tests_e2e.scenarios.lib.agent_test import AgentTest from tests_e2e.scenarios.lib.identifiers import VmExtensionIds, VmExtensionIdentifier from tests_e2e.scenarios.lib.logging import log +from tests_e2e.scenarios.lib.ssh_client import SshClient from tests_e2e.scenarios.lib.vm_extension import VmExtension class ExtensionOperationsBvt(AgentTest): def run(self): + ssh_client: SshClient = SshClient( + ip_address=self._context.vm_ip_address, + username=self._context.username, + private_key_file=self._context.private_key_file) + + is_arm64: bool = ssh_client.get_architecture() == "aarch64" + custom_script_2_0 = VmExtension( self._context.vm, VmExtensionIds.CustomScript, resource_name="CustomScript") + if is_arm64: + log.info("Will skip the update scenario, since currently there is only 1 version of CSE on ARM64") + else: + log.info("Installing %s", custom_script_2_0) + message = f"Hello {uuid.uuid4()}!" + custom_script_2_0.enable( + settings={ + 'commandToExecute': f"echo \'{message}\'" + }, + auto_upgrade_minor_version=False + ) + custom_script_2_0.assert_instance_view(expected_version="2.0", expected_message=message) + custom_script_2_1 = VmExtension( self._context.vm, VmExtensionIdentifier(VmExtensionIds.CustomScript.publisher, VmExtensionIds.CustomScript.type, "2.1"), resource_name="CustomScript") - log.info("Installing %s", custom_script_2_0) - message = f"Hello {uuid.uuid4()}!" - custom_script_2_0.enable( - settings={ - 'commandToExecute': f"echo \'{message}\'" - }, - auto_upgrade_minor_version=False - ) - custom_script_2_0.assert_instance_view(expected_version="2.0", expected_message=message) + if is_arm64: + log.info("Installing %s", custom_script_2_1) + else: + log.info("Updating %s to %s", custom_script_2_0, custom_script_2_1) - log.info("Updating %s to %s", custom_script_2_0, custom_script_2_1) message = f"Hello {uuid.uuid4()}!" custom_script_2_1.enable( settings={ diff --git a/tests_e2e/scenarios/tests/bvts/run_command.py b/tests_e2e/scenarios/tests/bvts/run_command.py index 25258cef39..624cd10ea5 100755 --- a/tests_e2e/scenarios/tests/bvts/run_command.py +++ b/tests_e2e/scenarios/tests/bvts/run_command.py @@ -45,25 +45,30 @@ def __init__(self, extension: VmExtension, get_settings: Callable[[str], Dict[st self.get_settings = get_settings def run(self): + ssh_client = SshClient( + ip_address=self._context.vm_ip_address, + username=self._context.username, + private_key_file=self._context.private_key_file) + test_cases = [ RunCommandBvt.TestCase( VmExtension(self._context.vm, VmExtensionIds.RunCommand, resource_name="RunCommand"), lambda s: { "script": base64.standard_b64encode(bytearray(s, 'utf-8')).decode('utf-8') - }), - RunCommandBvt.TestCase( - VmExtension(self._context.vm, VmExtensionIds.RunCommandHandler, resource_name="RunCommandHandler"), - lambda s: { - "source": { - "script": s - } }) ] - ssh_client = SshClient( - ip_address=self._context.vm_ip_address, - username=self._context.username, - private_key_file=self._context.private_key_file) + if ssh_client.get_architecture() == "aarch64": + log.info("Skipping test case for %s, since it has not been published on ARM64", VmExtensionIds.RunCommandHandler) + else: + test_cases.append( + RunCommandBvt.TestCase( + VmExtension(self._context.vm, VmExtensionIds.RunCommandHandler, resource_name="RunCommandHandler"), + lambda s: { + "source": { + "script": s + } + })) with soft_assertions(): for t in test_cases: diff --git a/tests_e2e/scenarios/testsuites/agent_junit.py b/tests_e2e/scenarios/testsuites/agent_junit.py index b918f17a3d..bcf6a71a70 100644 --- a/tests_e2e/scenarios/testsuites/agent_junit.py +++ b/tests_e2e/scenarios/testsuites/agent_junit.py @@ -28,7 +28,7 @@ from lisa import schema # pylint: disable=E0401 from lisa.messages import ( # pylint: disable=E0401 MessageBase, - TestResultMessage, + TestResultMessageBase, ) @@ -48,8 +48,8 @@ def type_schema(cls) -> Type[schema.TypedSchema]: return AgentJUnitSchema def _received_message(self, message: MessageBase) -> None: - if isinstance(message, TestResultMessage): - distro = message.information.get('distro_version') - if distro is not None: - message.name = distro + if isinstance(message, TestResultMessageBase): + image = message.information.get('image') + if image is not None: + message.name = image super()._received_message(message) From 0f0759660b5b10a566c3dfa98bb11d361addb192 Mon Sep 17 00:00:00 2001 From: Norberto Arrieta Date: Tue, 24 Jan 2023 12:57:08 -0800 Subject: [PATCH 32/63] Update azure-mgmt-compute in container image (#2739) Co-authored-by: narrieta --- tests_e2e/docker/Dockerfile | 1 + 1 file changed, 1 insertion(+) diff --git a/tests_e2e/docker/Dockerfile b/tests_e2e/docker/Dockerfile index 752aa4ff28..6699ff4bba 100644 --- a/tests_e2e/docker/Dockerfile +++ b/tests_e2e/docker/Dockerfile @@ -66,6 +66,7 @@ RUN \ # Install additional test dependencies \ # \ python3 -m pip install distro msrestazure && \ + python3 -m pip install azure-mgmt-compute --upgrade && \ \ # \ # The setup for the tests depends on a couple of paths; add those to the profile \ From 83282868726ad28ec1c2083149ee6139afddb428 Mon Sep 17 00:00:00 2001 From: Norberto Arrieta Date: Wed, 25 Jan 2023 11:03:34 -0800 Subject: [PATCH 33/63] Add Mariner 2 ARM64 to test run (#2740) Co-authored-by: narrieta --- tests_e2e/scenarios/runbooks/daily.yml | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/tests_e2e/scenarios/runbooks/daily.yml b/tests_e2e/scenarios/runbooks/daily.yml index 3ed7c223c3..e0cae83a1a 100644 --- a/tests_e2e/scenarios/runbooks/daily.yml +++ b/tests_e2e/scenarios/runbooks/daily.yml @@ -10,8 +10,6 @@ extension: variable: - name: subscription_id value: "" - - name: vm_size - value: "" - name: keep_environment value: "no" - name: wait_delete @@ -25,9 +23,11 @@ variable: value: "" is_secret: true - # The image and location are set by the combinator + # The image, vm_size, and location are set by the combinator - name: marketplace_image value: "" + - name: vm_size + value: "" - name: location value: "" - name: default_location @@ -57,24 +57,28 @@ combinator: items: - marketplace_image: "Canonical UbuntuServer 18.04-LTS latest" location: $(default_location) + vm_size: "" - marketplace_image: "Debian debian-10 10 latest" location: $(default_location) + vm_size: "" - marketplace_image: "OpenLogic CentOS 7_9 latest" location: $(default_location) + vm_size: "" - marketplace_image: "SUSE sles-15-sp2-basic gen2 latest" location: $(default_location) + vm_size: "" - marketplace_image: "RedHat RHEL 7-RAW latest" location: $(default_location) + vm_size: "" - marketplace_image: "microsoftcblmariner cbl-mariner cbl-mariner-1 latest" location: $(default_location) + vm_size: "" - marketplace_image: "microsoftcblmariner cbl-mariner cbl-mariner-2 latest" location: $(default_location) -# -# TODO: Add this image (currently there are allocation issues trying to use it) -# -# # Mariner 2 ARM64 is not available in westus2 currently; use eastus. -# - marketplace_image: "microsoftcblmariner cbl-mariner cbl-mariner-2-arm64 latest" -# location: "eastus" + vm_size: "" + - marketplace_image: "microsoftcblmariner cbl-mariner cbl-mariner-2-arm64 latest" + location: "eastus" + vm_size: "Standard_D2pls_v5" concurrency: 10 From 629a0eced71926aee935c59a6439026f685f8faf Mon Sep 17 00:00:00 2001 From: Norberto Arrieta Date: Mon, 30 Jan 2023 14:24:04 -0800 Subject: [PATCH 34/63] Load tests dynamically + Parameterize the test pipeline (#2745) * Load tests dynamically + Parameterize the test pipeline --- .../lib}/agent_junit.py | 0 .../orchestrator/lib/agent_test_loader.py | 107 +++++++++++ .../orchestrator/lib/agent_test_suite.py | 173 ++++++++++++------ .../daily.yml => orchestrator/runbook.yml} | 50 ++++- .../sample_runbooks}/existing_vm.yml | 38 +++- .../local_machine/hello_world.py | 0 .../sample_runbooks}/local_machine/local.yml | 0 tests_e2e/orchestrator/scripts/run-scenarios | 56 ++++-- tests_e2e/pipeline/pipeline.yml | 76 +++++++- tests_e2e/pipeline/scripts/execute_tests.sh | 3 +- .../pipeline/templates/execute-tests.yml | 48 ----- tests_e2e/scenarios/lib/logging.py | 13 +- .../scenarios/runbooks/include/ssh_proxy.yml | 19 -- tests_e2e/scenarios/testsuites/__init__.py | 0 tests_e2e/scenarios/testsuites/agent_bvt.json | 9 + tests_e2e/scenarios/testsuites/agent_bvt.py | 47 ----- tests_e2e/scenarios/testsuites/fail.json | 4 + tests_e2e/scenarios/testsuites/pass.json | 4 + 18 files changed, 448 insertions(+), 199 deletions(-) rename tests_e2e/{scenarios/testsuites => orchestrator/lib}/agent_junit.py (100%) create mode 100644 tests_e2e/orchestrator/lib/agent_test_loader.py rename tests_e2e/{scenarios/runbooks/daily.yml => orchestrator/runbook.yml} (69%) rename tests_e2e/{scenarios/runbooks/samples => orchestrator/sample_runbooks}/existing_vm.yml (65%) rename tests_e2e/{scenarios/runbooks/samples => orchestrator/sample_runbooks}/local_machine/hello_world.py (100%) rename tests_e2e/{scenarios/runbooks/samples => orchestrator/sample_runbooks}/local_machine/local.yml (100%) delete mode 100644 tests_e2e/pipeline/templates/execute-tests.yml delete mode 100644 tests_e2e/scenarios/runbooks/include/ssh_proxy.yml delete mode 100644 tests_e2e/scenarios/testsuites/__init__.py create mode 100644 tests_e2e/scenarios/testsuites/agent_bvt.json delete mode 100644 tests_e2e/scenarios/testsuites/agent_bvt.py create mode 100644 tests_e2e/scenarios/testsuites/fail.json create mode 100644 tests_e2e/scenarios/testsuites/pass.json diff --git a/tests_e2e/scenarios/testsuites/agent_junit.py b/tests_e2e/orchestrator/lib/agent_junit.py similarity index 100% rename from tests_e2e/scenarios/testsuites/agent_junit.py rename to tests_e2e/orchestrator/lib/agent_junit.py diff --git a/tests_e2e/orchestrator/lib/agent_test_loader.py b/tests_e2e/orchestrator/lib/agent_test_loader.py new file mode 100644 index 0000000000..a258b1ca44 --- /dev/null +++ b/tests_e2e/orchestrator/lib/agent_test_loader.py @@ -0,0 +1,107 @@ +# 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. +# +import importlib.util +import json + +from pathlib import Path +from typing import Any, Dict, List, Type + +from tests_e2e.scenarios.lib.agent_test import AgentTest + + +class TestSuiteDescription(object): + """ + Description of the test suite loaded from its JSON file. + """ + name: str + tests: List[Type[AgentTest]] + + +class AgentTestLoader(object): + """ + Loads the description of a set of test suites + """ + def __init__(self, test_source_directory: Path): + """ + The test_source_directory parameter must be the root directory of the end-to-end tests (".../WALinuxAgent/tests_e2e") + """ + self._root: Path = test_source_directory/"scenarios" + + def load(self, test_suites: str) -> List[TestSuiteDescription]: + """ + Loads the specified 'test_suites', which are given as a string of comma-separated suite names or a JSON description + of a single test_suite. + + When given as a comma-separated list, each item must correspond to the name of the JSON files describing s suite (those + files are located under the .../WALinuxAgent/tests_e2e/scenarios/testsuites directory). For example, + if test_suites == "agent_bvt, fast-track" then this method will load files agent_bvt.json and fast-track.json. + + When given as a JSON string, the value must correspond to the description a single test suite, for example + + { + "name": "AgentBvt", + + "tests": [ + "bvts/extension_operations.py", + "bvts/run_command.py", + "bvts/vm_access.py" + ] + } + """ + # Attempt to parse 'test_suites' as the JSON description for a single suite + try: + return [self._load_test_suite(json.loads(test_suites))] + except json.decoder.JSONDecodeError: + pass + + # Else, it should be a comma-separated list of description files + description_files: List[Path] = [self._root/"testsuites"/f"{t.strip()}.json" for t in test_suites.split(',')] + return [self._load_test_suite(AgentTestLoader._load_file(s)) for s in description_files] + + def _load_test_suite(self, test_suite: Dict[str, Any]) -> TestSuiteDescription: + """ + Creates a TestSuiteDescription from its JSON representation, which has been loaded by JSON.loads and is passed + to this method as a dictionary + """ + suite = TestSuiteDescription() + suite.name = test_suite["name"] + suite.tests = [] + for source_file in [self._root/"tests"/t for t in test_suite["tests"]]: + suite.tests.extend(AgentTestLoader._load_tests(source_file)) + return suite + + @staticmethod + def _load_tests(source_file: Path) -> List[Type[AgentTest]]: + """ + Takes a 'source_file', which must be a Python module, and returns a list of all the classes derived from AgentTest. + """ + spec = importlib.util.spec_from_file_location(f"tests_e2e.scenarios.{source_file.name}", str(source_file)) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + # return all the classes in the module that are subclasses of AgentTest but are not AgentTest itself. + return [v for v in module.__dict__.values() if isinstance(v, type) and issubclass(v, AgentTest) and v != AgentTest] + + @staticmethod + def _load_file(file: Path): + """Helper to load a JSON file""" + try: + with file.open() as f: + return json.load(f) + except Exception as e: + raise Exception(f"Can't load {file}: {e}") + + diff --git a/tests_e2e/orchestrator/lib/agent_test_suite.py b/tests_e2e/orchestrator/lib/agent_test_suite.py index 4f54949ce7..d5aaa13e76 100644 --- a/tests_e2e/orchestrator/lib/agent_test_suite.py +++ b/tests_e2e/orchestrator/lib/agent_test_suite.py @@ -14,13 +14,15 @@ # See the License for the specific language governing permissions and # limitations under the License. # +import contextlib import logging import re from assertpy import fail +from enum import Enum from pathlib import Path from threading import current_thread, RLock -from typing import List, Type +from typing import Any, Dict, List # Disable those warnings, since 'lisa' is an external, non-standard, dependency # E0401: Unable to import 'lisa' (import-error) @@ -31,17 +33,19 @@ Logger, Node, TestSuite, - TestSuiteMetadata + TestSuiteMetadata, + TestCaseMetadata, ) from lisa.sut_orchestrator import AZURE # pylint: disable=E0401 from lisa.sut_orchestrator.azure.common import get_node_context, AzureNodeSchema # pylint: disable=E0401 import makepkg from azurelinuxagent.common.version import AGENT_VERSION -from tests_e2e.scenarios.lib.agent_test import AgentTest +from tests_e2e.orchestrator.lib.agent_test_loader import AgentTestLoader, TestSuiteDescription from tests_e2e.scenarios.lib.agent_test_context import AgentTestContext from tests_e2e.scenarios.lib.identifiers import VmIdentifier from tests_e2e.scenarios.lib.logging import log as agent_test_logger # Logger used by the tests +from tests_e2e.scenarios.lib.logging import set_current_thread_log def _initialize_lisa_logger(): @@ -68,6 +72,29 @@ def _initialize_lisa_logger(): _initialize_lisa_logger() +# +# Helper to change the current thread name temporarily +# +@contextlib.contextmanager +def _set_thread_name(name: str): + initial_name = current_thread().name + current_thread().name = name + try: + yield + finally: + current_thread().name = initial_name + + +# +# Possible values for the collect_logs parameter +# +class CollectLogs(Enum): + Always = 'always' # Always collect logs + Failed = 'failed' # Collect logs only on test failures + No = 'no' # Never collect logs + + +@TestSuiteMetadata(area="waagent", category="", description="") class AgentTestSuite(TestSuite): """ Base class for Agent test suites. It provides facilities for setup, execution of tests and reporting results. Derived @@ -81,14 +108,17 @@ def __init__(self, vm: VmIdentifier, paths: AgentTestContext.Paths, connection: self.log: Logger = None self.node: Node = None self.runbook_name: str = None - self.suite_name: str = None + self.image_name: str = None + self.test_suites: List[str] = None + self.collect_logs: str = None + self.skip_setup: bool = None def __init__(self, metadata: TestSuiteMetadata) -> None: super().__init__(metadata) # The context is initialized by _set_context() via the call to execute() self.__context: AgentTestSuite._Context = None - def _set_context(self, node: Node, log: Logger): + def _set_context(self, node: Node, variables: Dict[str, Any], log: Logger): connection_info = node.connection_info node_context = get_node_context(node) runbook = node.capability.get_extended_runbook(AzureNodeSchema, AZURE) @@ -113,7 +143,23 @@ def _set_context(self, node: Node, log: Logger): self.__context.log = log self.__context.node = node - self.__context.suite_name = f"{self._metadata.full_name}_{runbook.marketplace.offer}-{runbook.marketplace.sku}" + self.__context.image_name = f"{runbook.marketplace.offer}-{runbook.marketplace.sku}" + self.__context.test_suites = AgentTestSuite._get_required_parameter(variables, "test_suites") + self.__context.collect_logs = AgentTestSuite._get_required_parameter(variables, "collect_logs") + self.__context.skip_setup = AgentTestSuite._get_required_parameter(variables, "skip_setup") + + self._log.info( + "Test suite parameters: [skip_setup: %s] [collect_logs: %s] [test_suites: %s]", + self.context.skip_setup, + self.context.collect_logs, + self.context.test_suites) + + @staticmethod + def _get_required_parameter(variables: Dict[str, Any], name: str) -> Any: + value = variables.get(name) + if value is None: + raise Exception(f"The runbook is missing required parameter '{name}'") + return value @property def context(self): @@ -234,94 +280,103 @@ def _collect_node_logs(self) -> None: # Copy the tarball to the local logs directory remote_path = "/tmp/waagent-logs.tgz" - local_path = Path.home()/'logs'/'{0}.tgz'.format(self.context.suite_name) + local_path = Path.home()/'logs'/'{0}.tgz'.format(self.context.image_name) self._log.info("Copying %s:%s to %s", self.context.node.name, remote_path, local_path) self.context.node.shell.copy_back(remote_path, local_path) except: # pylint: disable=bare-except self._log.exception("Failed to collect logs from the test machine") - def execute(self, node: Node, log: Logger, test_suite: List[Type[AgentTest]]) -> None: + @TestCaseMetadata(description="", priority=0) + def execute(self, node: Node, variables: Dict[str, Any], log: Logger) -> None: """ Executes each of the AgentTests in the given List. Note that 'test_suite' is a list of test classes, rather than instances of the test class (this method will instantiate each of these test classes). """ - self._set_context(node, log) + self._set_context(node, variables, log) failed: List[str] = [] # List of failed tests (names only) - # The thread name is added to self._log, set it to the current test suite while we execute it - thread_name = current_thread().name - current_thread().name = self.context.suite_name + with _set_thread_name(self.context.image_name): # The thread name is added to self._log + try: + if not self.context.skip_setup: + self._setup() - # We create a separate log file for the test suite. - suite_log_file: Path = Path.home()/'logs'/f"{self.context.suite_name}.log" - agent_test_logger.set_current_thread_log(suite_log_file) + try: + if not self.context.skip_setup: + self._setup_node() - try: - self._setup() + test_suites: List[TestSuiteDescription] = AgentTestLoader(self.context.test_source_directory).load(self.context.test_suites) - try: - self._setup_node() + for suite in test_suites: + failed.extend(self._execute_test_suite(suite)) + + finally: + collect = self.context.collect_logs + if collect == CollectLogs.Always or collect == CollectLogs.Failed and len(failed) > 0: + self._collect_node_logs() + except: # pylint: disable=bare-except + # Note that we report the error to the LISA log and then re-raise it. We log it here + # so that the message is decorated with the thread name in the LISA log; we re-raise + # to let LISA know the test errored out (LISA will report that error one more time + # in its log) + self._log.exception("UNHANDLED EXCEPTION") + raise + + finally: + self._clean_up() + + # Fail the entire test suite if any test failed; this exception is handled by LISA + if len(failed) > 0: + fail(f"{[self.context.image_name]} One or more tests failed: {failed}") + + def _execute_test_suite(self, suite: TestSuiteDescription) -> List[str]: + suite_name = suite.name + suite_full_name = f"{suite_name}-{self.context.image_name}" + + with _set_thread_name(suite_full_name): # The thread name is added to self._log + with set_current_thread_log(Path.home()/'logs'/f"{suite_full_name}.log"): agent_test_logger.info("") - agent_test_logger.info("**************************************** %s ****************************************", self.context.suite_name) + agent_test_logger.info("**************************************** %s ****************************************", suite_name) agent_test_logger.info("") - results: List[str] = [] + failed: List[str] = [] + summary: List[str] = [] + + for test in suite.tests: + test_name = test.__name__ + test_full_name = f"{suite_name}-{test_name}" - for test in test_suite: - result: str = "[UNKNOWN]" - test_full_name = f"{self.context.suite_name} {test.__name__}" - agent_test_logger.info("******** Executing %s", test_full_name) + agent_test_logger.info("******** Executing %s", test_name) self._log.info("******** Executing %s", test_full_name) - agent_test_logger.info("") try: + test(self.context).run() - result = f"[Passed] {test_full_name}" + + summary.append(f"[Passed] {test_name}") + agent_test_logger.info("******** [Passed] %s", test_name) + self._log.info("******** [Passed] %s", test_full_name) except AssertionError as e: - failed.append(test.__name__) - result = f"[Failed] {test_full_name}" - agent_test_logger.error("%s", e) - self._log.error("%s", e) + summary.append(f"[Failed] {test_name}") + failed.append(test_full_name) + agent_test_logger.error("******** [Failed] %s: %s", test_name, e) + self._log.error("******** [Failed] %s", test_full_name) except: # pylint: disable=bare-except - failed.append(test.__name__) - result = f"[Error] {test_full_name}" - agent_test_logger.exception("UNHANDLED EXCEPTION IN %s", test_full_name) + summary.append(f"[Error] {test_name}") + failed.append(test_full_name) + agent_test_logger.exception("UNHANDLED EXCEPTION IN %s", test_name) self._log.exception("UNHANDLED EXCEPTION IN %s", test_full_name) - agent_test_logger.info("******** %s", result) agent_test_logger.info("") - self._log.info("******** %s", result) - results.append(result) - agent_test_logger.info("") agent_test_logger.info("********* [Test Results]") agent_test_logger.info("") - for r in results: + for r in summary: agent_test_logger.info("\t%s", r) agent_test_logger.info("") - finally: - self._collect_node_logs() - - except: # pylint: disable=bare-except - agent_test_logger.exception("UNHANDLED EXCEPTION IN %s", self.context.suite_name) - # Note that we report the error to the LISA log and then re-raise it. We log it here - # so that the message is decorated with the thread name in the LISA log; we re-raise - # to let LISA know the test errored out (LISA will report that error one more time - # in its log) - self._log.exception("UNHANDLED EXCEPTION IN %s", self.context.suite_name) - raise - - finally: - self._clean_up() - agent_test_logger.close_current_thread_log() - current_thread().name = thread_name - - # Fail the entire test suite if any test failed; this exception is handled by LISA - if len(failed) > 0: - fail(f"{[self.context.suite_name]} One or more tests failed: {failed}") + return failed def execute_script_on_node(self, script_path: Path, parameters: str = "", sudo: bool = False) -> int: """ diff --git a/tests_e2e/scenarios/runbooks/daily.yml b/tests_e2e/orchestrator/runbook.yml similarity index 69% rename from tests_e2e/scenarios/runbooks/daily.yml rename to tests_e2e/orchestrator/runbook.yml index e0cae83a1a..0d91b376f3 100644 --- a/tests_e2e/scenarios/runbooks/daily.yml +++ b/tests_e2e/orchestrator/runbook.yml @@ -1,11 +1,11 @@ -name: Daily +name: WALinuxAgent testcase: - criteria: - area: bvt + area: waagent extension: - - "../testsuites" + - "./lib" variable: - name: subscription_id @@ -23,7 +23,22 @@ variable: value: "" is_secret: true + # + # Set these to use an SSH proxy + # + - name: proxy + value: False + - name: proxy_host + value: "" + - name: proxy_user + value: "foo" + - name: proxy_identity_file + value: "" + is_secret: true + + # # The image, vm_size, and location are set by the combinator + # - name: marketplace_image value: "" - name: vm_size @@ -33,6 +48,25 @@ variable: - name: default_location value: "westus2" + # + # These variables define parameters for the AgentTestSuite; see the test wiki for details + # + # The test suites to execute + - name: test_suites + value: "agent_bvt" + is_case_visible: true + + # Whether to collect logs from the test VM + - name: collect_logs + value: "failed" + is_case_visible: true + + # Whether to skip setup of the test VM + - name: skip_setup + value: false + is_case_visible: true + + platform: - type: azure admin_username: $(user) @@ -85,6 +119,12 @@ concurrency: 10 notifier: - type: agent.junit -include: - - path: ./include/ssh_proxy.yml +dev: + enabled: $(proxy) + mock_tcp_ping: $(proxy) + jump_boxes: + - private_key_file: $(proxy_identity_file) + address: $(proxy_host) + username: $(proxy_user) + password: "dummy" diff --git a/tests_e2e/scenarios/runbooks/samples/existing_vm.yml b/tests_e2e/orchestrator/sample_runbooks/existing_vm.yml similarity index 65% rename from tests_e2e/scenarios/runbooks/samples/existing_vm.yml rename to tests_e2e/orchestrator/sample_runbooks/existing_vm.yml index 2ae358f609..2dbdf1d215 100644 --- a/tests_e2e/scenarios/runbooks/samples/existing_vm.yml +++ b/tests_e2e/orchestrator/sample_runbooks/existing_vm.yml @@ -22,10 +22,10 @@ name: ExistingVM testcase: - criteria: - area: bvt + area: waagent extension: - - "../../testsuites" + - "../lib" variable: - name: subscription_id @@ -44,6 +44,30 @@ variable: value: "" is_secret: true + # Set these to use an SSH proxy + - name: proxy + value: False + - name: proxy_host + value: "" + - name: proxy_user + value: "foo" + - name: proxy_identity_file + value: "" + is_secret: true + + # These variables define parameters for the AgentTestSuite + - name: test_suites + value: "agent_bvt" + is_case_visible: true + + - name: collect_logs + value: "failed" + is_case_visible: true + + - name: skip_setup + value: true + is_case_visible: true + platform: - type: azure admin_username: $(user) @@ -61,5 +85,11 @@ notifier: - type: env_stats - type: agent.junit -include: - - path: ../include/ssh_proxy.yml +dev: + enabled: $(proxy) + mock_tcp_ping: $(proxy) + jump_boxes: + - private_key_file: $(proxy_identity_file) + address: $(proxy_host) + username: $(proxy_user) + password: "dummy" diff --git a/tests_e2e/scenarios/runbooks/samples/local_machine/hello_world.py b/tests_e2e/orchestrator/sample_runbooks/local_machine/hello_world.py similarity index 100% rename from tests_e2e/scenarios/runbooks/samples/local_machine/hello_world.py rename to tests_e2e/orchestrator/sample_runbooks/local_machine/hello_world.py diff --git a/tests_e2e/scenarios/runbooks/samples/local_machine/local.yml b/tests_e2e/orchestrator/sample_runbooks/local_machine/local.yml similarity index 100% rename from tests_e2e/scenarios/runbooks/samples/local_machine/local.yml rename to tests_e2e/orchestrator/sample_runbooks/local_machine/local.yml diff --git a/tests_e2e/orchestrator/scripts/run-scenarios b/tests_e2e/orchestrator/scripts/run-scenarios index 338c5e550f..09306ecc95 100755 --- a/tests_e2e/orchestrator/scripts/run-scenarios +++ b/tests_e2e/orchestrator/scripts/run-scenarios @@ -22,18 +22,52 @@ # to manage the test VMs taking the initial key value from the file shared by the container host, then it # executes the daily test runbook. # -# TODO: The runbook should be parameterized. -# set -euxo pipefail -cd "$HOME" +usage() ( + echo "Usage: run-scenarios [-t|--test-suites ] [-l|--collect-logs ] [-k|--skip-setup ]" + exit 1 +) + +test_suite_parameters="" + +while [[ $# -gt 0 ]] +do + case $1 in + -t|--test-suites) + shift + if [ "$#" -lt 1 ]; then + usage + fi + test_suite_parameters="$test_suite_parameters -v test_suites:$1" + ;; + -l|--collect-logs) + shift + if [ "$#" -lt 1 ]; then + usage + fi + test_suite_parameters="$test_suite_parameters -v collect_logs:$1" + ;; + -k|--skip-setup) + shift + if [ "$#" -lt 1 ]; then + usage + fi + test_suite_parameters="$test_suite_parameters -v skip_setup:$1" + ;; + *) + usage + esac + shift +done # The private ssh key is shared from the container host as $HOME/id_rsa; copy it to -# HOME/.ssh, set the correct mode and generate the public key. -mkdir "$HOME/.ssh" -cp "$HOME/id_rsa" "$HOME/.ssh" -chmod 700 "$HOME/.ssh/id_rsa" -ssh-keygen -y -f "$HOME/.ssh/id_rsa" > "$HOME/.ssh/id_rsa.pub" +# $HOME/.ssh, set the correct mode and generate the public key. +cd "$HOME" +mkdir .ssh +cp id_rsa .ssh +chmod 700 .ssh/id_rsa +ssh-keygen -y -f .ssh/id_rsa > .ssh/id_rsa.pub # # Now start the runbook @@ -41,9 +75,9 @@ ssh-keygen -y -f "$HOME/.ssh/id_rsa" > "$HOME/.ssh/id_rsa.pub" lisa_logs="$HOME/logs/lisa" lisa \ - --runbook "$HOME/WALinuxAgent/tests_e2e/scenarios/runbooks/daily.yml" \ + --runbook "$HOME/WALinuxAgent/tests_e2e/orchestrator/runbook.yml" \ --log_path "$lisa_logs" \ --working_path "$lisa_logs" \ -v subscription_id:"$SUBSCRIPTION_ID" \ - -v identity_file:"$HOME/.ssh/id_rsa" - + -v identity_file:"$HOME/.ssh/id_rsa" \ + $test_suite_parameters diff --git a/tests_e2e/pipeline/pipeline.yml b/tests_e2e/pipeline/pipeline.yml index 982477d82c..d0c570e662 100644 --- a/tests_e2e/pipeline/pipeline.yml +++ b/tests_e2e/pipeline/pipeline.yml @@ -1,6 +1,33 @@ +parameters: + # see the test wiki for a description of the parameters + - name: test_suites + displayName: Test Suites + type: string + default: agent_bvt + + - name: collect_logs + displayName: Collect logs from test VMs + type: string + default: failed + values: + - always + - failed + - no + + - name: skip_setup + displayName: Skip setup of the test VMs + type: boolean + default: false + variables: - name: azureConnection value: 'azuremanagement' + - name: test_suites + value: ${{ parameters.test_suites }} + - name: collect_logs + value: ${{ parameters.collect_logs }} + - name: skip_setup + value: ${{ parameters.skip_setup }} trigger: - develop @@ -10,9 +37,50 @@ pr: none pool: vmImage: ubuntu-latest -stages: - - stage: "ExecuteTests" +jobs: + - job: "ExecuteTests" + + steps: + - task: DownloadSecureFile@1 + name: downloadSshKey + displayName: "Download SSH key" + inputs: + secureFile: 'id_rsa' + + - task: AzureKeyVault@2 + displayName: "Fetch secrets from KV" + inputs: + azureSubscription: '$(azureConnection)' + KeyVaultName: 'dcrV2SPs' + SecretsFilter: '*' + RunAsPreJob: true + + - task: UsePythonVersion@0 + displayName: "Set Python Version" + inputs: + versionSpec: '3.10' + addToPath: true + architecture: 'x64' + + - bash: $(Build.SourcesDirectory)/tests_e2e/pipeline/scripts/execute_tests.sh + displayName: "Execute tests" + continueOnError: true + env: + # Add all KeyVault secrets explicitly as they're not added by default to the environment vars + AZURE_CLIENT_ID: $(AZURE-CLIENT-ID) + AZURE_CLIENT_SECRET: $(AZURE-CLIENT-SECRET) + AZURE_TENANT_ID: $(AZURE-TENANT-ID) + SUBSCRIPTION_ID: $(SUBSCRIPTION-ID) + + - publish: $(Build.ArtifactStagingDirectory) + artifact: 'artifacts' + displayName: 'Publish test artifacts' - jobs: - - template: 'templates/execute-tests.yml' + - task: PublishTestResults@2 + displayName: 'Publish test results' + inputs: + testResultsFormat: 'JUnit' + testResultsFiles: 'lisa/agent.junit.xml' + searchFolder: $(Build.ArtifactStagingDirectory) + failTaskOnFailedTests: true diff --git a/tests_e2e/pipeline/scripts/execute_tests.sh b/tests_e2e/pipeline/scripts/execute_tests.sh index 63dea5d4d8..06ed8c1ef6 100755 --- a/tests_e2e/pipeline/scripts/execute_tests.sh +++ b/tests_e2e/pipeline/scripts/execute_tests.sh @@ -27,7 +27,8 @@ docker run --rm \ --env AZURE_CLIENT_SECRET \ --env AZURE_TENANT_ID \ waagenttests.azurecr.io/waagenttests \ - bash --login -c '$HOME/WALinuxAgent/tests_e2e/orchestrator/scripts/run-scenarios' \ + bash --login -c \ + "\$HOME/WALinuxAgent/tests_e2e/orchestrator/scripts/run-scenarios -t $TEST_SUITES -l $COLLECT_LOGS -k $SKIP_SETUP" \ || echo "exit $?" > /tmp/exit.sh # Retake ownership of the source and staging directory (note that the former does not need to be done recursively) diff --git a/tests_e2e/pipeline/templates/execute-tests.yml b/tests_e2e/pipeline/templates/execute-tests.yml deleted file mode 100644 index 82b134cef7..0000000000 --- a/tests_e2e/pipeline/templates/execute-tests.yml +++ /dev/null @@ -1,48 +0,0 @@ -jobs: - - job: "ExecuteTests" - - steps: - - - task: DownloadSecureFile@1 - name: downloadSshKey - displayName: "Download SSH key" - inputs: - secureFile: 'id_rsa' - - - task: AzureKeyVault@2 - displayName: "Fetch secrets from KV" - inputs: - azureSubscription: '$(azureConnection)' - KeyVaultName: 'dcrV2SPs' - SecretsFilter: '*' - RunAsPreJob: true - - - task: UsePythonVersion@0 - displayName: "Set Python Version" - inputs: - versionSpec: '3.10' - addToPath: true - architecture: 'x64' - - - bash: $(Build.SourcesDirectory)/tests_e2e/pipeline/scripts/execute_tests.sh - displayName: "Execute tests" - continueOnError: true - env: - # Add all KeyVault secrets explicitly as they're not added by default to the environment vars - AZURE_CLIENT_ID: $(AZURE-CLIENT-ID) - AZURE_CLIENT_SECRET: $(AZURE-CLIENT-SECRET) - AZURE_TENANT_ID: $(AZURE-TENANT-ID) - SUBSCRIPTION_ID: $(SUBSCRIPTION-ID) - - - publish: $(Build.ArtifactStagingDirectory) - artifact: 'artifacts' - displayName: 'Publish test artifacts' - - - task: PublishTestResults@2 - displayName: 'Publish test results' - inputs: - testResultsFormat: 'JUnit' - testResultsFiles: 'lisa/agent.junit.xml' - searchFolder: $(Build.ArtifactStagingDirectory) - failTaskOnFailedTests: true - diff --git a/tests_e2e/scenarios/lib/logging.py b/tests_e2e/scenarios/lib/logging.py index 860d30b662..95e6e0cafc 100644 --- a/tests_e2e/scenarios/lib/logging.py +++ b/tests_e2e/scenarios/lib/logging.py @@ -19,7 +19,7 @@ # This module defines a single object, 'log', of type AgentLogger, which the end-to-end tests and libraries use # for logging. # - +import contextlib from logging import FileHandler, Formatter, Handler, Logger, StreamHandler, INFO from pathlib import Path from threading import current_thread @@ -121,3 +121,14 @@ def close_current_thread_log(self) -> None: log: AgentLogger = AgentLogger() + +@contextlib.contextmanager +def set_current_thread_log(log_file: Path): + """ + Context Manager to set the log file for the current thread temporarily + """ + log.set_current_thread_log(log_file) + try: + yield + finally: + log.close_current_thread_log() diff --git a/tests_e2e/scenarios/runbooks/include/ssh_proxy.yml b/tests_e2e/scenarios/runbooks/include/ssh_proxy.yml deleted file mode 100644 index 84704cb448..0000000000 --- a/tests_e2e/scenarios/runbooks/include/ssh_proxy.yml +++ /dev/null @@ -1,19 +0,0 @@ -variable: - - name: proxy - value: False - - name: proxy_host - value: "" - - name: proxy_user - value: "foo" - - name: proxy_identity_file - value: "" - is_secret: true - -dev: - enabled: $(proxy) - mock_tcp_ping: $(proxy) - jump_boxes: - - private_key_file: $(proxy_identity_file) - address: $(proxy_host) - username: $(proxy_user) - password: "dummy" diff --git a/tests_e2e/scenarios/testsuites/__init__.py b/tests_e2e/scenarios/testsuites/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests_e2e/scenarios/testsuites/agent_bvt.json b/tests_e2e/scenarios/testsuites/agent_bvt.json new file mode 100644 index 0000000000..76896791ee --- /dev/null +++ b/tests_e2e/scenarios/testsuites/agent_bvt.json @@ -0,0 +1,9 @@ +{ + "name": "AgentBvt", + + "tests": [ + "bvts/extension_operations.py", + "bvts/run_command.py", + "bvts/vm_access.py" + ] +} diff --git a/tests_e2e/scenarios/testsuites/agent_bvt.py b/tests_e2e/scenarios/testsuites/agent_bvt.py deleted file mode 100644 index 7bb5647528..0000000000 --- a/tests_e2e/scenarios/testsuites/agent_bvt.py +++ /dev/null @@ -1,47 +0,0 @@ -# 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. -# - -from tests_e2e.orchestrator.lib.agent_test_suite import AgentTestSuite -from tests_e2e.scenarios.tests.bvts.extension_operations import ExtensionOperationsBvt -from tests_e2e.scenarios.tests.bvts.vm_access import VmAccessBvt -from tests_e2e.scenarios.tests.bvts.run_command import RunCommandBvt - -# E0401: Unable to import 'lisa' (import-error) -from lisa import ( # pylint: disable=E0401 - Logger, - Node, - TestCaseMetadata, - TestSuiteMetadata, -) - - -@TestSuiteMetadata(area="bvt", category="", description="Test suite for Agent BVTs") -class AgentBvt(AgentTestSuite): - """ - Test suite for Agent BVTs - """ - @TestCaseMetadata(description="", priority=0) - def main(self, node: Node, log: Logger) -> None: - self.execute( - node, - log, - [ - ExtensionOperationsBvt, # Tests the basic operations (install, enable, update, uninstall) using CustomScript - RunCommandBvt, - VmAccessBvt - ] - ) diff --git a/tests_e2e/scenarios/testsuites/fail.json b/tests_e2e/scenarios/testsuites/fail.json new file mode 100644 index 0000000000..b33ac8ac18 --- /dev/null +++ b/tests_e2e/scenarios/testsuites/fail.json @@ -0,0 +1,4 @@ +{ + "name": "FailingTests", + "tests": ["fail_test.py", "error_test.py"] +} diff --git a/tests_e2e/scenarios/testsuites/pass.json b/tests_e2e/scenarios/testsuites/pass.json new file mode 100644 index 0000000000..018dfcd9b2 --- /dev/null +++ b/tests_e2e/scenarios/testsuites/pass.json @@ -0,0 +1,4 @@ +{ + "name": "PassingTest", + "tests": ["pass_test.py"] +} From becea90267fb029fb695bcbf5ed6de202c17322c Mon Sep 17 00:00:00 2001 From: Norberto Arrieta Date: Tue, 31 Jan 2023 18:59:28 -0800 Subject: [PATCH 35/63] Report test results for multiple suites (#2747) Co-authored-by: narrieta --- .../{ => orchestrator}/docker/Dockerfile | 0 tests_e2e/orchestrator/lib/agent_junit.py | 9 +- .../orchestrator/lib/agent_test_loader.py | 14 +- .../orchestrator/lib/agent_test_suite.py | 140 +++++++++++------- .../testsuites => test_suites}/agent_bvt.json | 0 .../testsuites => test_suites}/fail.json | 2 +- .../testsuites => test_suites}/pass.json | 2 +- .../{scenarios/lib => tests}/__init__.py | 0 .../tests => tests/bvts}/__init__.py | 0 .../tests/bvts/extension_operations.py | 10 +- .../{scenarios => }/tests/bvts/run_command.py | 10 +- .../{scenarios => }/tests/bvts/vm_access.py | 10 +- tests_e2e/{scenarios => }/tests/error_test.py | 2 +- tests_e2e/{scenarios => }/tests/fail_test.py | 2 +- .../tests/bvts => tests/lib}/__init__.py | 0 .../{scenarios => tests}/lib/agent_test.py | 4 +- .../lib/agent_test_context.py | 2 +- .../{scenarios => tests}/lib/identifiers.py | 0 tests_e2e/{scenarios => tests}/lib/logging.py | 0 tests_e2e/{scenarios => tests}/lib/retry.py | 2 +- tests_e2e/{scenarios => tests}/lib/shell.py | 0 .../{scenarios => tests}/lib/ssh_client.py | 2 +- .../lib/virtual_machine.py | 6 +- .../{scenarios => tests}/lib/vm_extension.py | 6 +- tests_e2e/{scenarios => }/tests/pass_test.py | 4 +- 25 files changed, 130 insertions(+), 97 deletions(-) rename tests_e2e/{ => orchestrator}/docker/Dockerfile (100%) rename tests_e2e/{scenarios/testsuites => test_suites}/agent_bvt.json (100%) rename tests_e2e/{scenarios/testsuites => test_suites}/fail.json (65%) rename tests_e2e/{scenarios/testsuites => test_suites}/pass.json (56%) rename tests_e2e/{scenarios/lib => tests}/__init__.py (100%) rename tests_e2e/{scenarios/tests => tests/bvts}/__init__.py (100%) rename tests_e2e/{scenarios => }/tests/bvts/extension_operations.py (91%) rename tests_e2e/{scenarios => }/tests/bvts/run_command.py (92%) rename tests_e2e/{scenarios => }/tests/bvts/vm_access.py (90%) rename tests_e2e/{scenarios => }/tests/error_test.py (93%) rename tests_e2e/{scenarios => }/tests/fail_test.py (93%) rename tests_e2e/{scenarios/tests/bvts => tests/lib}/__init__.py (100%) rename tests_e2e/{scenarios => tests}/lib/agent_test.py (92%) rename tests_e2e/{scenarios => tests}/lib/agent_test_context.py (98%) rename tests_e2e/{scenarios => tests}/lib/identifiers.py (100%) rename tests_e2e/{scenarios => tests}/lib/logging.py (100%) rename tests_e2e/{scenarios => tests}/lib/retry.py (96%) rename tests_e2e/{scenarios => tests}/lib/shell.py (100%) rename tests_e2e/{scenarios => tests}/lib/ssh_client.py (97%) rename tests_e2e/{scenarios => tests}/lib/virtual_machine.py (97%) rename tests_e2e/{scenarios => tests}/lib/vm_extension.py (98%) rename tests_e2e/{scenarios => }/tests/pass_test.py (88%) diff --git a/tests_e2e/docker/Dockerfile b/tests_e2e/orchestrator/docker/Dockerfile similarity index 100% rename from tests_e2e/docker/Dockerfile rename to tests_e2e/orchestrator/docker/Dockerfile diff --git a/tests_e2e/orchestrator/lib/agent_junit.py b/tests_e2e/orchestrator/lib/agent_junit.py index bcf6a71a70..04dd234b73 100644 --- a/tests_e2e/orchestrator/lib/agent_junit.py +++ b/tests_e2e/orchestrator/lib/agent_junit.py @@ -28,7 +28,7 @@ from lisa import schema # pylint: disable=E0401 from lisa.messages import ( # pylint: disable=E0401 MessageBase, - TestResultMessageBase, + TestResultMessage, ) @@ -48,8 +48,11 @@ def type_schema(cls) -> Type[schema.TypedSchema]: return AgentJUnitSchema def _received_message(self, message: MessageBase) -> None: - if isinstance(message, TestResultMessageBase): + if isinstance(message, TestResultMessage) and message.type != "AgentTestResultMessage": + message.suite_full_name = "_Setup_" + message.suite_name = message.suite_full_name image = message.information.get('image') if image is not None: - message.name = image + message.full_name = image + message.name = message.full_name super()._received_message(message) diff --git a/tests_e2e/orchestrator/lib/agent_test_loader.py b/tests_e2e/orchestrator/lib/agent_test_loader.py index a258b1ca44..f295da9c15 100644 --- a/tests_e2e/orchestrator/lib/agent_test_loader.py +++ b/tests_e2e/orchestrator/lib/agent_test_loader.py @@ -20,7 +20,7 @@ from pathlib import Path from typing import Any, Dict, List, Type -from tests_e2e.scenarios.lib.agent_test import AgentTest +from tests_e2e.tests.lib.agent_test import AgentTest class TestSuiteDescription(object): @@ -39,7 +39,7 @@ def __init__(self, test_source_directory: Path): """ The test_source_directory parameter must be the root directory of the end-to-end tests (".../WALinuxAgent/tests_e2e") """ - self._root: Path = test_source_directory/"scenarios" + self._root: Path = test_source_directory def load(self, test_suites: str) -> List[TestSuiteDescription]: """ @@ -47,10 +47,10 @@ def load(self, test_suites: str) -> List[TestSuiteDescription]: of a single test_suite. When given as a comma-separated list, each item must correspond to the name of the JSON files describing s suite (those - files are located under the .../WALinuxAgent/tests_e2e/scenarios/testsuites directory). For example, - if test_suites == "agent_bvt, fast-track" then this method will load files agent_bvt.json and fast-track.json. + files are located under the .../WALinuxAgent/tests_e2e/test_suites directory). For example, if test_suites == "agent_bvt, fast-track" + then this method will load files agent_bvt.json and fast-track.json. - When given as a JSON string, the value must correspond to the description a single test suite, for example + When given as a JSON string, the value must correspond to the description a single test suite, for example { "name": "AgentBvt", @@ -69,7 +69,7 @@ def load(self, test_suites: str) -> List[TestSuiteDescription]: pass # Else, it should be a comma-separated list of description files - description_files: List[Path] = [self._root/"testsuites"/f"{t.strip()}.json" for t in test_suites.split(',')] + description_files: List[Path] = [self._root/"test_suites"/f"{t.strip()}.json" for t in test_suites.split(',')] return [self._load_test_suite(AgentTestLoader._load_file(s)) for s in description_files] def _load_test_suite(self, test_suite: Dict[str, Any]) -> TestSuiteDescription: @@ -89,7 +89,7 @@ def _load_tests(source_file: Path) -> List[Type[AgentTest]]: """ Takes a 'source_file', which must be a Python module, and returns a list of all the classes derived from AgentTest. """ - spec = importlib.util.spec_from_file_location(f"tests_e2e.scenarios.{source_file.name}", str(source_file)) + spec = importlib.util.spec_from_file_location(f"tests_e2e.tests.{source_file.name}", str(source_file)) module = importlib.util.module_from_spec(spec) spec.loader.exec_module(module) # return all the classes in the module that are subclasses of AgentTest but are not AgentTest itself. diff --git a/tests_e2e/orchestrator/lib/agent_test_suite.py b/tests_e2e/orchestrator/lib/agent_test_suite.py index d5aaa13e76..dd4570af3a 100644 --- a/tests_e2e/orchestrator/lib/agent_test_suite.py +++ b/tests_e2e/orchestrator/lib/agent_test_suite.py @@ -15,10 +15,12 @@ # limitations under the License. # import contextlib +import datetime import logging import re +import traceback +import uuid -from assertpy import fail from enum import Enum from pathlib import Path from threading import current_thread, RLock @@ -26,26 +28,27 @@ # Disable those warnings, since 'lisa' is an external, non-standard, dependency # E0401: Unable to import 'lisa' (import-error) -# E0401: Unable to import 'lisa.sut_orchestrator' (import-error) -# E0401: Unable to import 'lisa.sut_orchestrator.azure.common' (import-error) +# etc from lisa import ( # pylint: disable=E0401 CustomScriptBuilder, Logger, Node, + notifier, + TestCaseMetadata, TestSuite, TestSuiteMetadata, - TestCaseMetadata, ) +from lisa.messages import TestStatus, TestResultMessage # pylint: disable=E0401 from lisa.sut_orchestrator import AZURE # pylint: disable=E0401 from lisa.sut_orchestrator.azure.common import get_node_context, AzureNodeSchema # pylint: disable=E0401 import makepkg from azurelinuxagent.common.version import AGENT_VERSION from tests_e2e.orchestrator.lib.agent_test_loader import AgentTestLoader, TestSuiteDescription -from tests_e2e.scenarios.lib.agent_test_context import AgentTestContext -from tests_e2e.scenarios.lib.identifiers import VmIdentifier -from tests_e2e.scenarios.lib.logging import log as agent_test_logger # Logger used by the tests -from tests_e2e.scenarios.lib.logging import set_current_thread_log +from tests_e2e.tests.lib.agent_test_context import AgentTestContext +from tests_e2e.tests.lib.identifiers import VmIdentifier +from tests_e2e.tests.lib.logging import log as agent_test_logger # Logger used by the tests +from tests_e2e.tests.lib.logging import set_current_thread_log def _initialize_lisa_logger(): @@ -97,8 +100,8 @@ class CollectLogs(Enum): @TestSuiteMetadata(area="waagent", category="", description="") class AgentTestSuite(TestSuite): """ - Base class for Agent test suites. It provides facilities for setup, execution of tests and reporting results. Derived - classes use the execute() method to run the tests in their corresponding suites. + Manages the setup of test VMs and execution of Agent test suites. This class acts as the interface with the LISA framework, which + will invoke the execute() method when a runbook is executed. """ class _Context(AgentTestContext): @@ -294,7 +297,7 @@ def execute(self, node: Node, variables: Dict[str, Any], log: Logger) -> None: """ self._set_context(node, variables, log) - failed: List[str] = [] # List of failed tests (names only) + test_suite_success = True with _set_thread_name(self.context.image_name): # The thread name is added to self._log try: @@ -308,11 +311,11 @@ def execute(self, node: Node, variables: Dict[str, Any], log: Logger) -> None: test_suites: List[TestSuiteDescription] = AgentTestLoader(self.context.test_source_directory).load(self.context.test_suites) for suite in test_suites: - failed.extend(self._execute_test_suite(suite)) + test_suite_success = self._execute_test_suite(suite) and test_suite_success finally: collect = self.context.collect_logs - if collect == CollectLogs.Always or collect == CollectLogs.Failed and len(failed) > 0: + if collect == CollectLogs.Always or collect == CollectLogs.Failed and not test_suite_success: self._collect_node_logs() except: # pylint: disable=bare-except @@ -326,57 +329,84 @@ def execute(self, node: Node, variables: Dict[str, Any], log: Logger) -> None: finally: self._clean_up() - # Fail the entire test suite if any test failed; this exception is handled by LISA - if len(failed) > 0: - fail(f"{[self.context.image_name]} One or more tests failed: {failed}") - - def _execute_test_suite(self, suite: TestSuiteDescription) -> List[str]: + def _execute_test_suite(self, suite: TestSuiteDescription) -> bool: + """ + Executes the given test suite and returns True if all the tests in the suite succeeded. + """ suite_name = suite.name suite_full_name = f"{suite_name}-{self.context.image_name}" with _set_thread_name(suite_full_name): # The thread name is added to self._log with set_current_thread_log(Path.home()/'logs'/f"{suite_full_name}.log"): - agent_test_logger.info("") - agent_test_logger.info("**************************************** %s ****************************************", suite_name) - agent_test_logger.info("") - - failed: List[str] = [] - summary: List[str] = [] - - for test in suite.tests: - test_name = test.__name__ - test_full_name = f"{suite_name}-{test_name}" - - agent_test_logger.info("******** Executing %s", test_name) - self._log.info("******** Executing %s", test_full_name) - - try: - - test(self.context).run() - - summary.append(f"[Passed] {test_name}") - agent_test_logger.info("******** [Passed] %s", test_name) - self._log.info("******** [Passed] %s", test_full_name) - except AssertionError as e: - summary.append(f"[Failed] {test_name}") - failed.append(test_full_name) - agent_test_logger.error("******** [Failed] %s: %s", test_name, e) - self._log.error("******** [Failed] %s", test_full_name) - except: # pylint: disable=bare-except - summary.append(f"[Error] {test_name}") - failed.append(test_full_name) - agent_test_logger.exception("UNHANDLED EXCEPTION IN %s", test_name) - self._log.exception("UNHANDLED EXCEPTION IN %s", test_full_name) + start_time: datetime.datetime = datetime.datetime.now() + + message: TestResultMessage = TestResultMessage() + message.type = "AgentTestResultMessage" + message.id_ = str(uuid.uuid4()) + message.status = TestStatus.RUNNING + message.suite_full_name = suite_name + message.suite_name = message.suite_full_name + message.full_name = f"{suite_name}-{self.context.image_name}" + message.name = message.full_name + message.elapsed = 0 + notifier.notify(message) + try: + agent_test_logger.info("") + agent_test_logger.info("**************************************** %s ****************************************", suite_name) agent_test_logger.info("") - agent_test_logger.info("********* [Test Results]") - agent_test_logger.info("") - for r in summary: - agent_test_logger.info("\t%s", r) - agent_test_logger.info("") + failed: List[str] = [] + summary: List[str] = [] + + for test in suite.tests: + test_name = test.__name__ + test_full_name = f"{suite_name}-{test_name}" + + agent_test_logger.info("******** Executing %s", test_name) + self._log.info("******** Executing %s", test_full_name) + + try: + + test(self.context).run() + + summary.append(f"[Passed] {test_name}") + agent_test_logger.info("******** [Passed] %s", test_name) + self._log.info("******** [Passed] %s", test_full_name) + except AssertionError as e: + summary.append(f"[Failed] {test_name}") + failed.append(test_name) + agent_test_logger.error("******** [Failed] %s: %s", test_name, e) + self._log.error("******** [Failed] %s", test_full_name) + except: # pylint: disable=bare-except + summary.append(f"[Error] {test_name}") + failed.append(test_name) + agent_test_logger.exception("UNHANDLED EXCEPTION IN %s", test_name) + self._log.exception("UNHANDLED EXCEPTION IN %s", test_full_name) + + agent_test_logger.info("") + + agent_test_logger.info("********* [Test Results]") + agent_test_logger.info("") + for r in summary: + agent_test_logger.info("\t%s", r) + agent_test_logger.info("") + + if len(failed) == 0: + message.status = TestStatus.PASSED + else: + message.status = TestStatus.FAILED + message.message = f"Tests failed: {failed}" + + except: # pylint: disable=bare-except + message.status = TestStatus.FAILED + message.message = "Unhandled exception while executing test suite." + message.stacktrace = traceback.format_exc() + finally: + message.elapsed = (datetime.datetime.now() - start_time).total_seconds() + notifier.notify(message) - return failed + return len(failed) == 0 def execute_script_on_node(self, script_path: Path, parameters: str = "", sudo: bool = False) -> int: """ diff --git a/tests_e2e/scenarios/testsuites/agent_bvt.json b/tests_e2e/test_suites/agent_bvt.json similarity index 100% rename from tests_e2e/scenarios/testsuites/agent_bvt.json rename to tests_e2e/test_suites/agent_bvt.json diff --git a/tests_e2e/scenarios/testsuites/fail.json b/tests_e2e/test_suites/fail.json similarity index 65% rename from tests_e2e/scenarios/testsuites/fail.json rename to tests_e2e/test_suites/fail.json index b33ac8ac18..8f3ebcdce0 100644 --- a/tests_e2e/scenarios/testsuites/fail.json +++ b/tests_e2e/test_suites/fail.json @@ -1,4 +1,4 @@ { - "name": "FailingTests", + "name": "Fail", "tests": ["fail_test.py", "error_test.py"] } diff --git a/tests_e2e/scenarios/testsuites/pass.json b/tests_e2e/test_suites/pass.json similarity index 56% rename from tests_e2e/scenarios/testsuites/pass.json rename to tests_e2e/test_suites/pass.json index 018dfcd9b2..66b521b888 100644 --- a/tests_e2e/scenarios/testsuites/pass.json +++ b/tests_e2e/test_suites/pass.json @@ -1,4 +1,4 @@ { - "name": "PassingTest", + "name": "Pass", "tests": ["pass_test.py"] } diff --git a/tests_e2e/scenarios/lib/__init__.py b/tests_e2e/tests/__init__.py similarity index 100% rename from tests_e2e/scenarios/lib/__init__.py rename to tests_e2e/tests/__init__.py diff --git a/tests_e2e/scenarios/tests/__init__.py b/tests_e2e/tests/bvts/__init__.py similarity index 100% rename from tests_e2e/scenarios/tests/__init__.py rename to tests_e2e/tests/bvts/__init__.py diff --git a/tests_e2e/scenarios/tests/bvts/extension_operations.py b/tests_e2e/tests/bvts/extension_operations.py similarity index 91% rename from tests_e2e/scenarios/tests/bvts/extension_operations.py rename to tests_e2e/tests/bvts/extension_operations.py index e037465bff..e8a45ee449 100755 --- a/tests_e2e/scenarios/tests/bvts/extension_operations.py +++ b/tests_e2e/tests/bvts/extension_operations.py @@ -31,11 +31,11 @@ from azure.core.exceptions import ResourceNotFoundError -from tests_e2e.scenarios.lib.agent_test import AgentTest -from tests_e2e.scenarios.lib.identifiers import VmExtensionIds, VmExtensionIdentifier -from tests_e2e.scenarios.lib.logging import log -from tests_e2e.scenarios.lib.ssh_client import SshClient -from tests_e2e.scenarios.lib.vm_extension import VmExtension +from tests_e2e.tests.lib.agent_test import AgentTest +from tests_e2e.tests.lib.identifiers import VmExtensionIds, VmExtensionIdentifier +from tests_e2e.tests.lib.logging import log +from tests_e2e.tests.lib.ssh_client import SshClient +from tests_e2e.tests.lib.vm_extension import VmExtension class ExtensionOperationsBvt(AgentTest): diff --git a/tests_e2e/scenarios/tests/bvts/run_command.py b/tests_e2e/tests/bvts/run_command.py similarity index 92% rename from tests_e2e/scenarios/tests/bvts/run_command.py rename to tests_e2e/tests/bvts/run_command.py index 624cd10ea5..188c12d3fa 100755 --- a/tests_e2e/scenarios/tests/bvts/run_command.py +++ b/tests_e2e/tests/bvts/run_command.py @@ -31,11 +31,11 @@ from assertpy import assert_that, soft_assertions from typing import Callable, Dict -from tests_e2e.scenarios.lib.agent_test import AgentTest -from tests_e2e.scenarios.lib.identifiers import VmExtensionIds -from tests_e2e.scenarios.lib.logging import log -from tests_e2e.scenarios.lib.ssh_client import SshClient -from tests_e2e.scenarios.lib.vm_extension import VmExtension +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.ssh_client import SshClient +from tests_e2e.tests.lib.vm_extension import VmExtension class RunCommandBvt(AgentTest): diff --git a/tests_e2e/scenarios/tests/bvts/vm_access.py b/tests_e2e/tests/bvts/vm_access.py similarity index 90% rename from tests_e2e/scenarios/tests/bvts/vm_access.py rename to tests_e2e/tests/bvts/vm_access.py index 36919e3f30..9e4c345ab9 100755 --- a/tests_e2e/scenarios/tests/bvts/vm_access.py +++ b/tests_e2e/tests/bvts/vm_access.py @@ -28,12 +28,12 @@ from assertpy import assert_that from pathlib import Path -from tests_e2e.scenarios.lib.agent_test import AgentTest -from tests_e2e.scenarios.lib.identifiers import VmExtensionIds -from tests_e2e.scenarios.lib.logging import log -from tests_e2e.scenarios.lib.ssh_client import SshClient +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.ssh_client import SshClient -from tests_e2e.scenarios.lib.vm_extension import VmExtension +from tests_e2e.tests.lib.vm_extension import VmExtension class VmAccessBvt(AgentTest): diff --git a/tests_e2e/scenarios/tests/error_test.py b/tests_e2e/tests/error_test.py similarity index 93% rename from tests_e2e/scenarios/tests/error_test.py rename to tests_e2e/tests/error_test.py index b8a0e7eea3..cf369f7d39 100755 --- a/tests_e2e/scenarios/tests/error_test.py +++ b/tests_e2e/tests/error_test.py @@ -17,7 +17,7 @@ # limitations under the License. # -from tests_e2e.scenarios.lib.agent_test import AgentTest +from tests_e2e.tests.lib.agent_test import AgentTest class ErrorTest(AgentTest): diff --git a/tests_e2e/scenarios/tests/fail_test.py b/tests_e2e/tests/fail_test.py similarity index 93% rename from tests_e2e/scenarios/tests/fail_test.py rename to tests_e2e/tests/fail_test.py index 4b6fd5d60d..e96b5bcf7e 100755 --- a/tests_e2e/scenarios/tests/fail_test.py +++ b/tests_e2e/tests/fail_test.py @@ -18,7 +18,7 @@ # from assertpy import fail -from tests_e2e.scenarios.lib.agent_test import AgentTest +from tests_e2e.tests.lib.agent_test import AgentTest class FailTest(AgentTest): diff --git a/tests_e2e/scenarios/tests/bvts/__init__.py b/tests_e2e/tests/lib/__init__.py similarity index 100% rename from tests_e2e/scenarios/tests/bvts/__init__.py rename to tests_e2e/tests/lib/__init__.py diff --git a/tests_e2e/scenarios/lib/agent_test.py b/tests_e2e/tests/lib/agent_test.py similarity index 92% rename from tests_e2e/scenarios/lib/agent_test.py rename to tests_e2e/tests/lib/agent_test.py index 6bbb8eaede..e72c5f0ee9 100644 --- a/tests_e2e/scenarios/lib/agent_test.py +++ b/tests_e2e/tests/lib/agent_test.py @@ -21,8 +21,8 @@ from abc import ABC, abstractmethod -from tests_e2e.scenarios.lib.agent_test_context import AgentTestContext -from tests_e2e.scenarios.lib.logging import log +from tests_e2e.tests.lib.agent_test_context import AgentTestContext +from tests_e2e.tests.lib.logging import log class AgentTest(ABC): diff --git a/tests_e2e/scenarios/lib/agent_test_context.py b/tests_e2e/tests/lib/agent_test_context.py similarity index 98% rename from tests_e2e/scenarios/lib/agent_test_context.py rename to tests_e2e/tests/lib/agent_test_context.py index 9225177a09..ca9fc64ad3 100644 --- a/tests_e2e/scenarios/lib/agent_test_context.py +++ b/tests_e2e/tests/lib/agent_test_context.py @@ -20,7 +20,7 @@ from pathlib import Path import tests_e2e -from tests_e2e.scenarios.lib.identifiers import VmIdentifier +from tests_e2e.tests.lib.identifiers import VmIdentifier class AgentTestContext: diff --git a/tests_e2e/scenarios/lib/identifiers.py b/tests_e2e/tests/lib/identifiers.py similarity index 100% rename from tests_e2e/scenarios/lib/identifiers.py rename to tests_e2e/tests/lib/identifiers.py diff --git a/tests_e2e/scenarios/lib/logging.py b/tests_e2e/tests/lib/logging.py similarity index 100% rename from tests_e2e/scenarios/lib/logging.py rename to tests_e2e/tests/lib/logging.py diff --git a/tests_e2e/scenarios/lib/retry.py b/tests_e2e/tests/lib/retry.py similarity index 96% rename from tests_e2e/scenarios/lib/retry.py rename to tests_e2e/tests/lib/retry.py index 1b78f0a13b..a86227bc6c 100644 --- a/tests_e2e/scenarios/lib/retry.py +++ b/tests_e2e/tests/lib/retry.py @@ -18,7 +18,7 @@ from typing import Callable, Any -from tests_e2e.scenarios.lib.logging import log +from tests_e2e.tests.lib.logging import log def execute_with_retry(operation: Callable[[], Any]) -> Any: diff --git a/tests_e2e/scenarios/lib/shell.py b/tests_e2e/tests/lib/shell.py similarity index 100% rename from tests_e2e/scenarios/lib/shell.py rename to tests_e2e/tests/lib/shell.py diff --git a/tests_e2e/scenarios/lib/ssh_client.py b/tests_e2e/tests/lib/ssh_client.py similarity index 97% rename from tests_e2e/scenarios/lib/ssh_client.py rename to tests_e2e/tests/lib/ssh_client.py index e4e650e3be..917afd049b 100644 --- a/tests_e2e/scenarios/lib/ssh_client.py +++ b/tests_e2e/tests/lib/ssh_client.py @@ -18,7 +18,7 @@ # from pathlib import Path -from tests_e2e.scenarios.lib import shell +from tests_e2e.tests.lib import shell class SshClient(object): diff --git a/tests_e2e/scenarios/lib/virtual_machine.py b/tests_e2e/tests/lib/virtual_machine.py similarity index 97% rename from tests_e2e/scenarios/lib/virtual_machine.py rename to tests_e2e/tests/lib/virtual_machine.py index 61eaecdcb7..032a7e0f54 100644 --- a/tests_e2e/scenarios/lib/virtual_machine.py +++ b/tests_e2e/tests/lib/virtual_machine.py @@ -29,9 +29,9 @@ from azure.mgmt.compute.models import VirtualMachineExtension, VirtualMachineScaleSetExtension, VirtualMachineInstanceView, VirtualMachineScaleSetInstanceView from azure.mgmt.resource import ResourceManagementClient -from tests_e2e.scenarios.lib.identifiers import VmIdentifier -from tests_e2e.scenarios.lib.logging import log -from tests_e2e.scenarios.lib.retry import execute_with_retry +from tests_e2e.tests.lib.identifiers import VmIdentifier +from tests_e2e.tests.lib.logging import log +from tests_e2e.tests.lib.retry import execute_with_retry class VirtualMachineBaseClass(ABC): diff --git a/tests_e2e/scenarios/lib/vm_extension.py b/tests_e2e/tests/lib/vm_extension.py similarity index 98% rename from tests_e2e/scenarios/lib/vm_extension.py rename to tests_e2e/tests/lib/vm_extension.py index 1a30ce8b5b..505a695a07 100644 --- a/tests_e2e/scenarios/lib/vm_extension.py +++ b/tests_e2e/tests/lib/vm_extension.py @@ -31,9 +31,9 @@ from azure.mgmt.compute.models import VirtualMachineExtension, VirtualMachineScaleSetExtension, VirtualMachineExtensionInstanceView from azure.identity import DefaultAzureCredential -from tests_e2e.scenarios.lib.identifiers import VmIdentifier, VmExtensionIdentifier -from tests_e2e.scenarios.lib.logging import log -from tests_e2e.scenarios.lib.retry import execute_with_retry +from tests_e2e.tests.lib.identifiers import VmIdentifier, VmExtensionIdentifier +from tests_e2e.tests.lib.logging import log +from tests_e2e.tests.lib.retry import execute_with_retry _TIMEOUT = 5 * 60 # Timeout for extension operations (in seconds) diff --git a/tests_e2e/scenarios/tests/pass_test.py b/tests_e2e/tests/pass_test.py similarity index 88% rename from tests_e2e/scenarios/tests/pass_test.py rename to tests_e2e/tests/pass_test.py index f1b9c3aad1..580db2dc08 100755 --- a/tests_e2e/scenarios/tests/pass_test.py +++ b/tests_e2e/tests/pass_test.py @@ -17,8 +17,8 @@ # limitations under the License. # -from tests_e2e.scenarios.lib.agent_test import AgentTest -from tests_e2e.scenarios.lib.logging import log +from tests_e2e.tests.lib.agent_test import AgentTest +from tests_e2e.tests.lib.logging import log class PassTest(AgentTest): From ce369acb20eca1697204ecabe41a5d7c0ab53a87 Mon Sep 17 00:00:00 2001 From: Norberto Arrieta Date: Fri, 3 Feb 2023 15:42:51 -0800 Subject: [PATCH 36/63] Report results for individual tests (#2751) * Report results for individual tests * mark failure --------- Co-authored-by: narrieta --- tests_e2e/orchestrator/lib/agent_junit.py | 3 + .../orchestrator/lib/agent_test_suite.py | 96 +++++++++++++------ 2 files changed, 70 insertions(+), 29 deletions(-) diff --git a/tests_e2e/orchestrator/lib/agent_junit.py b/tests_e2e/orchestrator/lib/agent_junit.py index 04dd234b73..7fbb069379 100644 --- a/tests_e2e/orchestrator/lib/agent_junit.py +++ b/tests_e2e/orchestrator/lib/agent_junit.py @@ -48,6 +48,9 @@ def type_schema(cls) -> Type[schema.TypedSchema]: return AgentJUnitSchema def _received_message(self, message: MessageBase) -> None: + # The Agent sends its own TestResultMessage and marks them as "AgentTestResultMessage"; for the + # test results sent by LISA itself, we change the suite name to "_Setup_" in order to separate them + # from actual test results. if isinstance(message, TestResultMessage) and message.type != "AgentTestResultMessage": message.suite_full_name = "_Setup_" message.suite_name = message.suite_full_name diff --git a/tests_e2e/orchestrator/lib/agent_test_suite.py b/tests_e2e/orchestrator/lib/agent_test_suite.py index dd4570af3a..18f7fd35e3 100644 --- a/tests_e2e/orchestrator/lib/agent_test_suite.py +++ b/tests_e2e/orchestrator/lib/agent_test_suite.py @@ -335,33 +335,22 @@ def _execute_test_suite(self, suite: TestSuiteDescription) -> bool: """ suite_name = suite.name suite_full_name = f"{suite_name}-{self.context.image_name}" + suite_start_time: datetime.datetime = datetime.datetime.now() with _set_thread_name(suite_full_name): # The thread name is added to self._log with set_current_thread_log(Path.home()/'logs'/f"{suite_full_name}.log"): - start_time: datetime.datetime = datetime.datetime.now() - - message: TestResultMessage = TestResultMessage() - message.type = "AgentTestResultMessage" - message.id_ = str(uuid.uuid4()) - message.status = TestStatus.RUNNING - message.suite_full_name = suite_name - message.suite_name = message.suite_full_name - message.full_name = f"{suite_name}-{self.context.image_name}" - message.name = message.full_name - message.elapsed = 0 - notifier.notify(message) - try: agent_test_logger.info("") agent_test_logger.info("**************************************** %s ****************************************", suite_name) agent_test_logger.info("") - failed: List[str] = [] + failed: bool = False # True if any test fails summary: List[str] = [] for test in suite.tests: test_name = test.__name__ test_full_name = f"{suite_name}-{test_name}" + test_start_time: datetime.datetime = datetime.datetime.now() agent_test_logger.info("******** Executing %s", test_name) self._log.info("******** Executing %s", test_full_name) @@ -373,16 +362,34 @@ def _execute_test_suite(self, suite: TestSuiteDescription) -> bool: summary.append(f"[Passed] {test_name}") agent_test_logger.info("******** [Passed] %s", test_name) self._log.info("******** [Passed] %s", test_full_name) + self._report_test_result( + suite_full_name, + test_name, + TestStatus.PASSED, + test_start_time) except AssertionError as e: + failed = True summary.append(f"[Failed] {test_name}") - failed.append(test_name) agent_test_logger.error("******** [Failed] %s: %s", test_name, e) self._log.error("******** [Failed] %s", test_full_name) + self._report_test_result( + suite_full_name, + test_name, + TestStatus.FAILED, + test_start_time, + message=str(e)) except: # pylint: disable=bare-except + failed = True summary.append(f"[Error] {test_name}") - failed.append(test_name) agent_test_logger.exception("UNHANDLED EXCEPTION IN %s", test_name) self._log.exception("UNHANDLED EXCEPTION IN %s", test_full_name) + self._report_test_result( + suite_full_name, + test_name, + TestStatus.FAILED, + test_start_time, + message="Unhandled exception.", + add_exception_stack_trace=True) agent_test_logger.info("") @@ -392,21 +399,52 @@ def _execute_test_suite(self, suite: TestSuiteDescription) -> bool: agent_test_logger.info("\t%s", r) agent_test_logger.info("") - if len(failed) == 0: - message.status = TestStatus.PASSED - else: - message.status = TestStatus.FAILED - message.message = f"Tests failed: {failed}" - except: # pylint: disable=bare-except - message.status = TestStatus.FAILED - message.message = "Unhandled exception while executing test suite." - message.stacktrace = traceback.format_exc() - finally: - message.elapsed = (datetime.datetime.now() - start_time).total_seconds() - notifier.notify(message) + failed = True + self._report_test_result( + suite_full_name, + suite_name, + TestStatus.FAILED, + suite_start_time, + message=f"Unhandled exception while executing test suite {suite_name}.", + add_exception_stack_trace=True) + + return failed - return len(failed) == 0 + @staticmethod + def _report_test_result( + suite_name: str, + test_name: str, + status: TestStatus, + start_time: datetime.datetime, + message: str = "", + add_exception_stack_trace: bool = False + ) -> None: + """ + Reports a test result to the junit notifier + """ + # The junit notifier requires an initial RUNNING message in order to register the test in its internal cache. + msg: TestResultMessage = TestResultMessage() + msg.type = "AgentTestResultMessage" + msg.id_ = str(uuid.uuid4()) + msg.status = TestStatus.RUNNING + msg.suite_full_name = suite_name + msg.suite_name = msg.suite_full_name + msg.full_name = test_name + msg.name = msg.full_name + msg.elapsed = 0 + + notifier.notify(msg) + + # Now send the actual result. The notifier pipeline makes a deep copy of the message so it is OK to re-use the + # same object and just update a few fields. If using a different object, be sure that the "id_" is the same. + msg.status = status + msg.message = message + if add_exception_stack_trace: + msg.stacktrace = traceback.format_exc() + msg.elapsed = (datetime.datetime.now() - start_time).total_seconds() + + notifier.notify(msg) def execute_script_on_node(self, script_path: Path, parameters: str = "", sudo: bool = False) -> int: """ From 36a5ec1db1b6711898beb06fca43590745b1bea4 Mon Sep 17 00:00:00 2001 From: Norberto Arrieta Date: Mon, 6 Feb 2023 11:43:54 -0800 Subject: [PATCH 37/63] Improvements in Pipeline parameters (#2748) * Improvements in Pipeline parameters * remove extra space * fix jq --------- Co-authored-by: narrieta --- tests_e2e/orchestrator/docker/Dockerfile | 1 + tests_e2e/orchestrator/runbook.yml | 49 ++++++------ tests_e2e/orchestrator/scripts/run-scenarios | 83 -------------------- tests_e2e/pipeline/pipeline-cleanup.yml | 56 +++++++++++++ tests_e2e/pipeline/pipeline.yml | 16 ++-- tests_e2e/pipeline/scripts/execute_tests.sh | 60 ++++++++++---- 6 files changed, 134 insertions(+), 131 deletions(-) delete mode 100755 tests_e2e/orchestrator/scripts/run-scenarios create mode 100644 tests_e2e/pipeline/pipeline-cleanup.yml diff --git a/tests_e2e/orchestrator/docker/Dockerfile b/tests_e2e/orchestrator/docker/Dockerfile index 6699ff4bba..08fd4ca312 100644 --- a/tests_e2e/orchestrator/docker/Dockerfile +++ b/tests_e2e/orchestrator/docker/Dockerfile @@ -73,5 +73,6 @@ RUN \ # \ echo 'export PYTHONPATH="$HOME/WALinuxAgent"' >> $HOME/.bash_profile && \ echo 'export PATH="$HOME/.local/bin:$PATH"' >> $HOME/.bash_profile && \ + echo 'cd $HOME' >> $HOME/.bash_profile && \ : diff --git a/tests_e2e/orchestrator/runbook.yml b/tests_e2e/orchestrator/runbook.yml index 0d91b376f3..74bbec46e4 100644 --- a/tests_e2e/orchestrator/runbook.yml +++ b/tests_e2e/orchestrator/runbook.yml @@ -8,12 +8,11 @@ extension: - "./lib" variable: + # + # These variables define runbook parameters; they are handled by LISA. + # - name: subscription_id value: "" - - name: keep_environment - value: "no" - - name: wait_delete - value: false - name: user value: "waagent" - name: identity_file @@ -22,9 +21,28 @@ variable: - name: admin_password value: "" is_secret: true + - name: keep_environment + value: "no" + # + # These variables define parameters for the AgentTestSuite; see the test wiki for details. + # + # The test suites to execute + - name: test_suites + value: "agent_bvt" + is_case_visible: true + + # Whether to collect logs from the test VM + - name: collect_logs + value: "failed" + is_case_visible: true + + # Whether to skip setup of the test VM + - name: skip_setup + value: false + is_case_visible: true # - # Set these to use an SSH proxy + # Set these to use an SSH proxy when executing the runbook # - name: proxy value: False @@ -48,25 +66,6 @@ variable: - name: default_location value: "westus2" - # - # These variables define parameters for the AgentTestSuite; see the test wiki for details - # - # The test suites to execute - - name: test_suites - value: "agent_bvt" - is_case_visible: true - - # Whether to collect logs from the test VM - - name: collect_logs - value: "failed" - is_case_visible: true - - # Whether to skip setup of the test VM - - name: skip_setup - value: false - is_case_visible: true - - platform: - type: azure admin_username: $(user) @@ -76,7 +75,7 @@ platform: azure: deploy: True subscription_id: $(subscription_id) - wait_delete: $(wait_delete) + wait_delete: false requirement: core_count: min: 2 diff --git a/tests_e2e/orchestrator/scripts/run-scenarios b/tests_e2e/orchestrator/scripts/run-scenarios deleted file mode 100755 index 09306ecc95..0000000000 --- a/tests_e2e/orchestrator/scripts/run-scenarios +++ /dev/null @@ -1,83 +0,0 @@ -#!/usr/bin/env bash - -# 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 script runs on the container executing the tests. It creates the SSH keys (private and public) used -# to manage the test VMs taking the initial key value from the file shared by the container host, then it -# executes the daily test runbook. -# -set -euxo pipefail - -usage() ( - echo "Usage: run-scenarios [-t|--test-suites ] [-l|--collect-logs ] [-k|--skip-setup ]" - exit 1 -) - -test_suite_parameters="" - -while [[ $# -gt 0 ]] -do - case $1 in - -t|--test-suites) - shift - if [ "$#" -lt 1 ]; then - usage - fi - test_suite_parameters="$test_suite_parameters -v test_suites:$1" - ;; - -l|--collect-logs) - shift - if [ "$#" -lt 1 ]; then - usage - fi - test_suite_parameters="$test_suite_parameters -v collect_logs:$1" - ;; - -k|--skip-setup) - shift - if [ "$#" -lt 1 ]; then - usage - fi - test_suite_parameters="$test_suite_parameters -v skip_setup:$1" - ;; - *) - usage - esac - shift -done - -# The private ssh key is shared from the container host as $HOME/id_rsa; copy it to -# $HOME/.ssh, set the correct mode and generate the public key. -cd "$HOME" -mkdir .ssh -cp id_rsa .ssh -chmod 700 .ssh/id_rsa -ssh-keygen -y -f .ssh/id_rsa > .ssh/id_rsa.pub - -# -# Now start the runbook -# -lisa_logs="$HOME/logs/lisa" - -lisa \ - --runbook "$HOME/WALinuxAgent/tests_e2e/orchestrator/runbook.yml" \ - --log_path "$lisa_logs" \ - --working_path "$lisa_logs" \ - -v subscription_id:"$SUBSCRIPTION_ID" \ - -v identity_file:"$HOME/.ssh/id_rsa" \ - $test_suite_parameters diff --git a/tests_e2e/pipeline/pipeline-cleanup.yml b/tests_e2e/pipeline/pipeline-cleanup.yml new file mode 100644 index 0000000000..fd01212131 --- /dev/null +++ b/tests_e2e/pipeline/pipeline-cleanup.yml @@ -0,0 +1,56 @@ +# +# Pipeline for cleaning up any remaining Resource Groups generated by the Azure.WALinuxAgent pipeline. +# +# Runs every 3 hours and deletes any resource groups that are more than a day old and contain string "lisa-WALinuxAgent-" +# +schedules: + - cron: "0 */3 * * *" # Run every 3 hours + displayName: cleanup build + branches: + include: + - develop + always: true + +# no PR triggers +pr: none + +pool: + vmImage: ubuntu-latest + +variables: + - name: azureConnection + value: 'azuremanagement' + - name: rgPrefix + value: 'lisa-WALinuxAgent-' + +steps: + + - task: AzureKeyVault@2 + displayName: "Fetch secrets from KV" + inputs: + azureSubscription: '$(azureConnection)' + KeyVaultName: 'dcrV2SPs' + SecretsFilter: '*' + RunAsPreJob: true + + - task: AzureCLI@2 + inputs: + azureSubscription: '$(azureConnection)' + scriptType: 'bash' + scriptLocation: 'inlineScript' + inlineScript: | + set -euxo pipefail + date=`date --utc +%Y-%m-%d'T'%H:%M:%S.%N'Z' -d "1 day ago"` + + # Using the Azure REST GET resourceGroups API call as we can add the createdTime to the results. + # This feature is not available via the az-cli commands directly so we have to use the Azure REST APIs + + az rest --method GET \ + --url "https://management.azure.com/subscriptions/$(SUBSCRIPTION-ID)/resourcegroups" \ + --url-parameters api-version=2021-04-01 \$expand=createdTime \ + --output json \ + --query value \ + | jq --arg date "$date" '.[] | select (.createdTime < $date).name' \ + | grep "$(rgPrefix)" \ + | xargs -l -t -r az group delete --no-wait -y -n \ + || echo "No resource groups found to delete" diff --git a/tests_e2e/pipeline/pipeline.yml b/tests_e2e/pipeline/pipeline.yml index d0c570e662..69154d08db 100644 --- a/tests_e2e/pipeline/pipeline.yml +++ b/tests_e2e/pipeline/pipeline.yml @@ -14,10 +14,14 @@ parameters: - failed - no - - name: skip_setup - displayName: Skip setup of the test VMs - type: boolean - default: false + - name: keep_environment + displayName: Keep the test VMs (do not delete them) + type: string + default: no + values: + - always + - failed + - no variables: - name: azureConnection @@ -26,8 +30,8 @@ variables: value: ${{ parameters.test_suites }} - name: collect_logs value: ${{ parameters.collect_logs }} - - name: skip_setup - value: ${{ parameters.skip_setup }} + - name: keep_environment + value: ${{ parameters.keep_environment }} trigger: - develop diff --git a/tests_e2e/pipeline/scripts/execute_tests.sh b/tests_e2e/pipeline/scripts/execute_tests.sh index 06ed8c1ef6..b02df16bcc 100755 --- a/tests_e2e/pipeline/scripts/execute_tests.sh +++ b/tests_e2e/pipeline/scripts/execute_tests.sh @@ -2,36 +2,62 @@ set -euxo pipefail +# +# Set the correct mode for the private SSH key and generate the public key. +# +cd "$HOME" +mkdir ssh +cp "$DOWNLOADSSHKEY_SECUREFILEPATH" ssh +chmod 700 ssh/id_rsa +ssh-keygen -y -f ssh/id_rsa > ssh/id_rsa.pub + +# +# Change the ownership of the "ssh" directory we just created, as well as the sources and staging directories. +# Make waagent (UID 1000 in the container) the owner of both locations, so that it can write to them. +# This is needed because building the agent package writes the egg info to the source code directory, and +# tests write their logs to the staging directory. +# +sudo find ssh -exec chown 1000 {} \; +sudo chown 1000 "$BUILD_SOURCESDIRECTORY" +sudo chown 1000 "$BUILD_ARTIFACTSTAGINGDIRECTORY" + +# # Pull the container image used to execute the tests +# az login --service-principal --username "$AZURE_CLIENT_ID" --password "$AZURE_CLIENT_SECRET" --tenant "$AZURE_TENANT_ID" > /dev/null az acr login --name waagenttests docker pull waagenttests.azurecr.io/waagenttests:latest -# Building the agent package writes the egg info to the source code directory, and test write their logs to the staging directory. -# Make waagent (UID 1000 in the container) the owner of both locations, so that it can write to them. -sudo chown 1000 "$BUILD_SOURCESDIRECTORY" -sudo chown 1000 "$BUILD_ARTIFACTSTAGINGDIRECTORY" - # A test failure will cause automation to exit with an error code and we don't want this script to stop so we force the command # to succeed and capture the exit code to return it at the end of the script. echo "exit 0" > /tmp/exit.sh docker run --rm \ - --volume "$BUILD_SOURCESDIRECTORY:/home/waagent/WALinuxAgent" \ - --volume "$DOWNLOADSSHKEY_SECUREFILEPATH:/home/waagent/id_rsa" \ - --volume "$BUILD_ARTIFACTSTAGINGDIRECTORY:/home/waagent/logs" \ - --env SUBSCRIPTION_ID \ - --env AZURE_CLIENT_ID \ - --env AZURE_CLIENT_SECRET \ - --env AZURE_TENANT_ID \ - waagenttests.azurecr.io/waagenttests \ - bash --login -c \ - "\$HOME/WALinuxAgent/tests_e2e/orchestrator/scripts/run-scenarios -t $TEST_SUITES -l $COLLECT_LOGS -k $SKIP_SETUP" \ + --volume "$BUILD_SOURCESDIRECTORY:/home/waagent/WALinuxAgent" \ + --volume "$HOME"/ssh:/home/waagent/.ssh \ + --volume "$BUILD_ARTIFACTSTAGINGDIRECTORY":/home/waagent/logs \ + --env AZURE_CLIENT_ID \ + --env AZURE_CLIENT_SECRET \ + --env AZURE_TENANT_ID \ + waagenttests.azurecr.io/waagenttests \ + bash --login -c \ + "lisa \ + --runbook \$HOME/WALinuxAgent/tests_e2e/orchestrator/runbook.yml \ + --log_path \$HOME/logs/lisa \ + --working_path \$HOME/logs/lisa \ + -v subscription_id:$SUBSCRIPTION_ID \ + -v identity_file:\$HOME/.ssh/id_rsa \ + -v test_suites:\"$TEST_SUITES\" \ + -v collect_logs:\"$COLLECT_LOGS\" \ + -v keep_environment:\"$KEEP_ENVIRONMENT\"" \ || echo "exit $?" > /tmp/exit.sh -# Retake ownership of the source and staging directory (note that the former does not need to be done recursively) +# +# Retake ownership of the source and staging directories (note that the former does not need to be done recursively; also, we don't need to +# retake ownership of the ssh directory) +# sudo chown "$USER" "$BUILD_SOURCESDIRECTORY" sudo find "$BUILD_ARTIFACTSTAGINGDIRECTORY" -exec chown "$USER" {} \; @@ -51,4 +77,4 @@ mv "$BUILD_ARTIFACTSTAGINGDIRECTORY"/lisa/[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0- rm -r "$BUILD_ARTIFACTSTAGINGDIRECTORY"/lisa/[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9] cat /tmp/exit.sh -bash /tmp/exit.sh \ No newline at end of file +bash /tmp/exit.sh From dd4e72da46df1df2c37f9fd44f9177bd60c82044 Mon Sep 17 00:00:00 2001 From: maddieford <93676569+maddieford@users.noreply.github.com> Date: Fri, 10 Feb 2023 10:15:26 -0800 Subject: [PATCH 38/63] Update log collector unit file to remove memorylimit (#2757) * Update version to dummy 1.0.0.0' * Revert version change * Update log collector slice unit file without memory limit * Update comment * Update setup errors as exceptions * Update comment for accuracy --- azurelinuxagent/common/cgroupconfigurator.py | 7 +++---- azurelinuxagent/common/logcollector.py | 2 +- tests/common/mock_cgroup_environment.py | 1 + tests/common/test_cgroupconfigurator.py | 21 +++++++++++++++++++ .../azure-walinuxagent-logcollector.slice | 9 ++++++++ 5 files changed, 35 insertions(+), 5 deletions(-) create mode 100644 tests/data/init/azure-walinuxagent-logcollector.slice diff --git a/azurelinuxagent/common/cgroupconfigurator.py b/azurelinuxagent/common/cgroupconfigurator.py index 840961e7df..627567b038 100644 --- a/azurelinuxagent/common/cgroupconfigurator.py +++ b/azurelinuxagent/common/cgroupconfigurator.py @@ -368,10 +368,9 @@ def __setup_azure_slice(): if not os.path.exists(vmextensions_slice): files_to_create.append((vmextensions_slice, _VMEXTENSIONS_SLICE_CONTENTS)) - if not os.path.exists(logcollector_slice): - slice_contents = _LOGCOLLECTOR_SLICE_CONTENTS_FMT.format(cpu_quota=_LOGCOLLECTOR_CPU_QUOTA) - - files_to_create.append((logcollector_slice, slice_contents)) + # Update log collector slice contents + slice_contents = _LOGCOLLECTOR_SLICE_CONTENTS_FMT.format(cpu_quota=_LOGCOLLECTOR_CPU_QUOTA) + files_to_create.append((logcollector_slice, slice_contents)) if fileutil.findre_in_file(agent_unit_file, r"Slice=") is not None: CGroupConfigurator._Impl.__cleanup_unit_file(agent_drop_in_file_slice) diff --git a/azurelinuxagent/common/logcollector.py b/azurelinuxagent/common/logcollector.py index 393333c962..fe62a7db6a 100644 --- a/azurelinuxagent/common/logcollector.py +++ b/azurelinuxagent/common/logcollector.py @@ -119,7 +119,7 @@ def _set_resource_usage_cgroups(cpu_cgroup_path, memory_cgroup_path): @staticmethod def _initialize_telemetry(): protocol = get_protocol_util().get_protocol(init_goal_state=False) - protocol.client.reset_goal_state(goalstate_properties=GoalStateProperties.RoleConfig | GoalStateProperties.HostingEnv) + protocol.client.reset_goal_state(goal_state_properties=GoalStateProperties.RoleConfig | GoalStateProperties.HostingEnv) # Initialize the common parameters for telemetry events initialize_event_logger_vminfo_common_parameters(protocol) diff --git a/tests/common/mock_cgroup_environment.py b/tests/common/mock_cgroup_environment.py index 10d499077e..e38471060e 100644 --- a/tests/common/mock_cgroup_environment.py +++ b/tests/common/mock_cgroup_environment.py @@ -91,6 +91,7 @@ class UnitFilePaths: walinuxagent = "/lib/systemd/system/walinuxagent.service" + logcollector = "/lib/systemd/system/azure-walinuxagent-logcollector.slice" azure = "/lib/systemd/system/azure.slice" vmextensions = "/lib/systemd/system/azure-vmextensions.slice" extensionslice = "/lib/systemd/system/azure-vmextensions-Microsoft.CPlat.Extension.slice" diff --git a/tests/common/test_cgroupconfigurator.py b/tests/common/test_cgroupconfigurator.py index 60a7cfde1c..d3410cf54a 100644 --- a/tests/common/test_cgroupconfigurator.py +++ b/tests/common/test_cgroupconfigurator.py @@ -188,6 +188,27 @@ def test_initialize_should_create_unit_files_when_the_agent_service_file_is_not_ self.assertTrue(os.path.exists(agent_drop_in_file_cpu_accounting), "{0} was not created".format(agent_drop_in_file_cpu_accounting)) self.assertTrue(os.path.exists(agent_drop_in_file_memory_accounting), "{0} was not created".format(agent_drop_in_file_memory_accounting)) + def test_initialize_should_update_logcollector_memorylimit(self): + with self._get_cgroup_configurator(initialize=False) as configurator: + log_collector_unit_file = configurator.mocks.get_mapped_path(UnitFilePaths.logcollector) + original_memory_limit = "MemoryLimit=30M" + + # The mock creates the slice unit file with memory limit + configurator.mocks.add_data_file(os.path.join(data_dir, 'init', "azure-walinuxagent-logcollector.slice"), + UnitFilePaths.logcollector) + if not os.path.exists(log_collector_unit_file): + raise Exception("{0} should have been created during test setup".format(log_collector_unit_file)) + if not fileutil.findre_in_file(log_collector_unit_file, original_memory_limit): + raise Exception("MemoryLimit was not set correctly. Expected: {0}. Got:\n{1}".format( + original_memory_limit, fileutil.read_file(log_collector_unit_file))) + + configurator.initialize() + + # initialize() should update the unit file to remove the memory limit + self.assertFalse(fileutil.findre_in_file(log_collector_unit_file, original_memory_limit), + "Log collector slice unit file was not updated correctly. Expected no memory limit. Got:\n{0}".format( + fileutil.read_file(log_collector_unit_file))) + def test_setup_extension_slice_should_create_unit_files(self): with self._get_cgroup_configurator() as configurator: # get the paths to the mocked files diff --git a/tests/data/init/azure-walinuxagent-logcollector.slice b/tests/data/init/azure-walinuxagent-logcollector.slice new file mode 100644 index 0000000000..63c09d431d --- /dev/null +++ b/tests/data/init/azure-walinuxagent-logcollector.slice @@ -0,0 +1,9 @@ +[Unit] +Description=Slice for Azure VM Agent Periodic Log Collector +DefaultDependencies=no +Before=slices.target +[Slice] +CPUAccounting=yes +CPUQuota=5% +MemoryAccounting=yes +MemoryLimit=30M \ No newline at end of file From ca471d2b1058debc60894363605b5a2603d835f4 Mon Sep 17 00:00:00 2001 From: Nageswara Nandigam <84482346+nagworld9@users.noreply.github.com> Date: Mon, 13 Feb 2023 14:27:14 -0800 Subject: [PATCH 39/63] update github hosted build runner for Unit tests (#2755) * update image for PR builds * added py3.4 * fix log collector hang test --- .github/workflows/ci_pr.yml | 57 ++++++++++++++++--------- tests/common/osutil/test_default.py | 3 +- tests/common/test_cgroupconfigurator.py | 4 +- tests/common/test_event.py | 3 +- tests/ga/test_update.py | 3 +- tests/test_agent.py | 6 ++- tests/tools.py | 8 ++++ 7 files changed, 59 insertions(+), 25 deletions(-) diff --git a/.github/workflows/ci_pr.yml b/.github/workflows/ci_pr.yml index 589f0f5e7b..e5592688c6 100644 --- a/.github/workflows/ci_pr.yml +++ b/.github/workflows/ci_pr.yml @@ -10,31 +10,48 @@ on: jobs: test-legacy-python-versions: - name: "Python 2.6 Unit Tests" - runs-on: ubuntu-18.04 - strategy: fail-fast: false + matrix: + include: + - python-version: 2.6 + - python-version: 3.4 + + name: "Python ${{ matrix.python-version }} Unit Tests" + runs-on: ubuntu-20.04 + container: + image: ubuntu:16.04 + volumes: + - /home/waagent:/home/waagent + defaults: + run: + shell: bash -l {0} + env: NOSEOPTS: "--verbose" steps: + - uses: actions/checkout@v3 - - name: Install Python 2.6 + - name: Install Python ${{ matrix.python-version }} run: | - curl https://dcrdata.blob.core.windows.net/python/python-2.6.tar.bz2 -o python-2.6.tar.bz2 - sudo tar xjvf python-2.6.tar.bz2 --directory / - - - uses: actions/checkout@v2 + apt-get update + apt-get install -y curl bzip2 sudo python3 + curl https://dcrdata.blob.core.windows.net/python/python-${{ matrix.python-version }}.tar.bz2 -o python-${{ matrix.python-version }}.tar.bz2 + sudo tar xjvf python-${{ matrix.python-version }}.tar.bz2 --directory / - name: Test with nosetests run: | - source /home/waagent/virtualenv/python2.6.9/bin/activate + if [[ ${{ matrix.python-version }} == 2.6 ]]; then + source /home/waagent/virtualenv/python2.6.9/bin/activate + else + source /home/waagent/virtualenv/python3.4.8/bin/activate + fi ./ci/nosetests.sh exit $? test-current-python-versions: - + strategy: fail-fast: false matrix: @@ -43,24 +60,24 @@ jobs: - python-version: 2.7 PYLINTOPTS: "--rcfile=ci/2.7.pylintrc --ignore=tests_e2e,makepkg.py" - - python-version: 3.4 - PYLINTOPTS: "--rcfile=ci/2.7.pylintrc --ignore=tests_e2e,makepkg.py" + - python-version: 3.5 + PYLINTOPTS: "--rcfile=ci/3.6.pylintrc --ignore=tests_e2e,makepkg.py" - python-version: 3.6 PYLINTOPTS: "--rcfile=ci/3.6.pylintrc --ignore=tests_e2e" - python-version: 3.7 PYLINTOPTS: "--rcfile=ci/3.6.pylintrc --ignore=tests_e2e" - + - python-version: 3.8 PYLINTOPTS: "--rcfile=ci/3.6.pylintrc --ignore=tests_e2e" - python-version: 3.9 PYLINTOPTS: "--rcfile=ci/3.6.pylintrc" additional-nose-opts: "--with-coverage --cover-erase --cover-inclusive --cover-branches --cover-package=azurelinuxagent" - + name: "Python ${{ matrix.python-version }} Unit Tests" - runs-on: ubuntu-18.04 + runs-on: ubuntu-20.04 env: PYLINTOPTS: ${{ matrix.PYLINTOPTS }} @@ -69,15 +86,15 @@ jobs: PYTHON_VERSION: ${{ matrix.python-version }} steps: - + - name: Checkout WALinuxAgent repo - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: Setup Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} - + - name: Install dependencies id: install-dependencies run: | @@ -106,6 +123,6 @@ jobs: - name: Upload Coverage if: matrix.python-version == 3.9 - uses: codecov/codecov-action@v1 + uses: codecov/codecov-action@v2 with: file: ./coverage.xml \ No newline at end of file diff --git a/tests/common/osutil/test_default.py b/tests/common/osutil/test_default.py index ac55102430..ab4fa5c999 100644 --- a/tests/common/osutil/test_default.py +++ b/tests/common/osutil/test_default.py @@ -35,7 +35,7 @@ from azurelinuxagent.common.utils.flexible_version import FlexibleVersion from azurelinuxagent.common.utils.networkutil import AddFirewallRules from tests.common.mock_environment import MockEnvironment -from tests.tools import AgentTestCase, patch, open_patch, load_data, data_dir +from tests.tools import AgentTestCase, patch, open_patch, load_data, data_dir, is_python_version_26_or_34, skip_if_predicate_true actual_get_proc_net_route = 'azurelinuxagent.common.osutil.default.DefaultOSUtil._get_proc_net_route' @@ -950,6 +950,7 @@ def test_remove_firewall_should_not_retry_invalid_rule(self): self.assertFalse(osutil._enable_firewall) + @skip_if_predicate_true(is_python_version_26_or_34, "Disabled on Python 2.6 and 3.4 for now. Need to revisit to fix it") def test_get_nic_state(self): state = osutil.DefaultOSUtil().get_nic_state() self.assertNotEqual(state, {}) diff --git a/tests/common/test_cgroupconfigurator.py b/tests/common/test_cgroupconfigurator.py index d3410cf54a..7e2dc45b44 100644 --- a/tests/common/test_cgroupconfigurator.py +++ b/tests/common/test_cgroupconfigurator.py @@ -39,7 +39,7 @@ from azurelinuxagent.common.utils import shellutil, fileutil from tests.common.mock_environment import MockCommand from tests.common.mock_cgroup_environment import mock_cgroup_environment, UnitFilePaths -from tests.tools import AgentTestCase, patch, mock_sleep, i_am_root, data_dir +from tests.tools import AgentTestCase, patch, mock_sleep, i_am_root, data_dir, is_python_version_26_or_34, skip_if_predicate_true from tests.utils.miscellaneous_tools import format_processes, wait_for @@ -526,6 +526,7 @@ def test_start_extension_command_should_disable_cgroups_and_invoke_the_command_d self.assertEqual(len(CGroupsTelemetry._tracked), 0, "No cgroups should have been created") + @skip_if_predicate_true(is_python_version_26_or_34, "Disabled on Python 2.6 and 3.4 for now. Need to revisit to fix it") @attr('requires_sudo') @patch('time.sleep', side_effect=lambda _: mock_sleep()) def test_start_extension_command_should_not_use_fallback_option_if_extension_fails(self, *args): @@ -563,6 +564,7 @@ def test_start_extension_command_should_not_use_fallback_option_if_extension_fai # wasn't truncated. self.assertIn("Running scope as unit", ustr(context_manager.exception)) + @skip_if_predicate_true(is_python_version_26_or_34, "Disabled on Python 2.6 and 3.4 for now. Need to revisit to fix it") @attr('requires_sudo') @patch('time.sleep', side_effect=lambda _: mock_sleep()) @patch("azurelinuxagent.common.utils.extensionprocessutil.TELEMETRY_MESSAGE_MAX_LEN", 5) diff --git a/tests/common/test_event.py b/tests/common/test_event.py index fad21155f2..de5ad7353a 100644 --- a/tests/common/test_event.py +++ b/tests/common/test_event.py @@ -44,7 +44,7 @@ from tests.protocol import mockwiredata from tests.protocol.mocks import mock_wire_protocol, MockHttpResponse from tests.protocol.HttpRequestPredicates import HttpRequestPredicates -from tests.tools import AgentTestCase, data_dir, load_data, patch, skip_if_predicate_true +from tests.tools import AgentTestCase, data_dir, load_data, patch, skip_if_predicate_true, is_python_version_26_or_34 from tests.utils.event_logger_tools import EventLoggerTools @@ -414,6 +414,7 @@ def test_collect_events_should_be_able_to_process_events_with_non_ascii_characte self.assertEqual(len(event_list), 1) self.assertEqual(TestEvent._get_event_message(event_list[0]), u'World\u05e2\u05d9\u05d5\u05ea \u05d0\u05d7\u05e8\u05d5\u05ea\u0906\u091c') + @skip_if_predicate_true(is_python_version_26_or_34, "Disabled on Python 2.6 and 3.4 for now. Need to revisit to fix it") def test_collect_events_should_ignore_invalid_event_files(self): self._create_test_event_file("custom_script_1.tld") # a valid event self._create_test_event_file("custom_script_utf-16.tld") diff --git a/tests/ga/test_update.py b/tests/ga/test_update.py index 31fd4a425a..b329bbaa05 100644 --- a/tests/ga/test_update.py +++ b/tests/ga/test_update.py @@ -53,7 +53,7 @@ from tests.protocol.mocks import mock_wire_protocol, MockHttpResponse from tests.protocol.mockwiredata import DATA_FILE, DATA_FILE_MULTIPLE_EXT, DATA_FILE_VM_SETTINGS from tests.tools import AgentTestCase, AgentTestCaseWithGetVmSizeMock, data_dir, DEFAULT, patch, load_bin_data, Mock, MagicMock, \ - clear_singleton_instances + clear_singleton_instances, is_python_version_26_or_34, skip_if_predicate_true from tests.protocol import mockwiredata from tests.protocol.HttpRequestPredicates import HttpRequestPredicates @@ -1410,6 +1410,7 @@ def _mock_popen(cmd, *args, **kwargs): "Not setting up persistent firewall rules as OS.EnableFirewall=False" == args[0] for (args, _) in patch_info.call_args_list), "Info not logged properly, got: {0}".format(patch_info.call_args_list)) + @skip_if_predicate_true(is_python_version_26_or_34, "Disabled on Python 2.6 and 3.4 for now. Need to revisit to fix it") def test_it_should_setup_persistent_firewall_rules_on_startup(self): iterations = 1 executed_commands = [] diff --git a/tests/test_agent.py b/tests/test_agent.py index 2f80d695e0..f0f773f059 100644 --- a/tests/test_agent.py +++ b/tests/test_agent.py @@ -218,6 +218,7 @@ def test_rejects_invalid_log_collector_mode(self, mock_exit, mock_stderr): # py @patch("azurelinuxagent.agent.LogCollector") def test_calls_collect_logs_with_proper_mode(self, mock_log_collector, *args): # pylint: disable=unused-argument agent = Agent(False, conf_file_path=os.path.join(data_dir, "test_waagent.conf")) + mock_log_collector.run = Mock() agent.collect_logs(is_full_mode=True) full_mode = mock_log_collector.call_args_list[0][0][0] @@ -231,6 +232,7 @@ def test_calls_collect_logs_with_proper_mode(self, mock_log_collector, *args): def test_calls_collect_logs_on_valid_cgroups(self, mock_log_collector): try: CollectLogsHandler.enable_cgroups_validation() + mock_log_collector.run = Mock() def mock_cgroup_paths(*args, **kwargs): if args and args[0] == "self": @@ -246,9 +248,11 @@ def mock_cgroup_paths(*args, **kwargs): finally: CollectLogsHandler.disable_cgroups_validation() - def test_doesnt_call_collect_logs_on_invalid_cgroups(self): + @patch("azurelinuxagent.agent.LogCollector") + def test_doesnt_call_collect_logs_on_invalid_cgroups(self, mock_log_collector): try: CollectLogsHandler.enable_cgroups_validation() + mock_log_collector.run = Mock() def mock_cgroup_paths(*args, **kwargs): if args and args[0] == "self": diff --git a/tests/tools.py b/tests/tools.py index b22a856377..85d460d374 100644 --- a/tests/tools.py +++ b/tests/tools.py @@ -121,6 +121,14 @@ def is_python_version_26(): return sys.version_info[0] == 2 and sys.version_info[1] == 6 +def is_python_version_34(): + return sys.version_info[0] == 3 and sys.version_info[1] == 4 + + +def is_python_version_26_or_34(): + return is_python_version_26() or is_python_version_34() + + class AgentTestCase(unittest.TestCase): @classmethod def setUpClass(cls): From 41a275f6e32c7f282929a1d33b4af365e7f68393 Mon Sep 17 00:00:00 2001 From: Norberto Arrieta Date: Mon, 13 Feb 2023 15:45:02 -0800 Subject: [PATCH 40/63] Add distros and location to the test suites definition files (#2756) * Add distro, location, and VM size configuration to test suites --------- Co-authored-by: narrieta --- .../orchestrator/lib/agent_test_loader.py | 234 ++++++++++++++---- .../orchestrator/lib/agent_test_suite.py | 35 ++- .../lib/agent_test_suite_combinator.py | 119 +++++++++ tests_e2e/orchestrator/runbook.yml | 74 +++--- tests_e2e/pipeline/pipeline-cleanup.yml | 2 +- tests_e2e/test_suites/agent_bvt.json | 9 - tests_e2e/test_suites/agent_bvt.yml | 6 + tests_e2e/test_suites/fail.json | 4 - tests_e2e/test_suites/fail.yml | 5 + tests_e2e/test_suites/images.yml | 70 ++++++ tests_e2e/test_suites/pass.json | 4 - tests_e2e/test_suites/pass.yml | 4 + 12 files changed, 438 insertions(+), 128 deletions(-) create mode 100644 tests_e2e/orchestrator/lib/agent_test_suite_combinator.py delete mode 100644 tests_e2e/test_suites/agent_bvt.json create mode 100644 tests_e2e/test_suites/agent_bvt.yml delete mode 100644 tests_e2e/test_suites/fail.json create mode 100644 tests_e2e/test_suites/fail.yml create mode 100644 tests_e2e/test_suites/images.yml delete mode 100644 tests_e2e/test_suites/pass.json create mode 100644 tests_e2e/test_suites/pass.yml diff --git a/tests_e2e/orchestrator/lib/agent_test_loader.py b/tests_e2e/orchestrator/lib/agent_test_loader.py index f295da9c15..bd3f320545 100644 --- a/tests_e2e/orchestrator/lib/agent_test_loader.py +++ b/tests_e2e/orchestrator/lib/agent_test_loader.py @@ -15,77 +15,178 @@ # limitations under the License. # import importlib.util -import json +# E0401: Unable to import 'yaml' (import-error) +import yaml # pylint: disable=E0401 from pathlib import Path from typing import Any, Dict, List, Type +import tests_e2e from tests_e2e.tests.lib.agent_test import AgentTest -class TestSuiteDescription(object): +class TestSuiteInfo(object): """ - Description of the test suite loaded from its JSON file. + Description of a test suite """ + # The name of the test suite name: str + # The tests that comprise the suite tests: List[Type[AgentTest]] + # An image or image set (as defined in images.yml) specifying the images the suite must run on. + images: str + # The location (region) on which the suite must run; if empty, the suite can run on any location + location: str + # Whether this suite must run on its own test VM + owns_vm: bool + + def __str__(self): + return self.name + + +class VmImageInfo(object): + # The URN of the image (publisher, offer, version separated by spaces) + urn: str + # Indicates that the image is available only on those locations. If empty, the image should be available in all locations + locations: List[str] + # Indicates that the image is available only for those VM sizes. If empty, the image should be available for all VM sizes + vm_sizes: List[str] + + def __str__(self): + return self.urn class AgentTestLoader(object): """ - Loads the description of a set of test suites + Loads a given set of test suites from the YAML configuration files. """ - def __init__(self, test_source_directory: Path): + def __init__(self, test_suites: str): """ - The test_source_directory parameter must be the root directory of the end-to-end tests (".../WALinuxAgent/tests_e2e") - """ - self._root: Path = test_source_directory + Loads the specified 'test_suites', which are given as a string of comma-separated suite names or a YAML description + of a single test_suite. + + When given as a comma-separated list, each item must correspond to the name of the YAML files describing s suite (those + files are located under the .../WALinuxAgent/tests_e2e/test_suites directory). For example, if test_suites == "agent_bvt, fast_track" + then this method will load files agent_bvt.yml and fast_track.yml. + + When given as a YAML string, the value must correspond to the description a single test suite, for example - def load(self, test_suites: str) -> List[TestSuiteDescription]: + name: "AgentBvt" + tests: + - "bvts/extension_operations.py" + - "bvts/run_command.py" + - "bvts/vm_access.py" """ - Loads the specified 'test_suites', which are given as a string of comma-separated suite names or a JSON description - of a single test_suite. + self.__test_suites: List[TestSuiteInfo] = self._load_test_suites(test_suites) + self.__images: Dict[str, List[VmImageInfo]] = self._load_images() + self._validate() - When given as a comma-separated list, each item must correspond to the name of the JSON files describing s suite (those - files are located under the .../WALinuxAgent/tests_e2e/test_suites directory). For example, if test_suites == "agent_bvt, fast-track" - then this method will load files agent_bvt.json and fast-track.json. + _SOURCE_CODE_ROOT: Path = Path(tests_e2e.__path__[0]) - When given as a JSON string, the value must correspond to the description a single test suite, for example + @property + def test_suites(self) -> List[TestSuiteInfo]: + return self.__test_suites - { - "name": "AgentBvt", + @property + def images(self) -> Dict[str, List[VmImageInfo]]: + """ + A dictionary where, for each item, the key is the name of an image or image set and the value is a list of VmImageInfos for + the corresponding images. + """ + return self.__images - "tests": [ - "bvts/extension_operations.py", - "bvts/run_command.py", - "bvts/vm_access.py" - ] - } + def _validate(self): """ - # Attempt to parse 'test_suites' as the JSON description for a single suite - try: - return [self._load_test_suite(json.loads(test_suites))] - except json.decoder.JSONDecodeError: - pass + Performs some basic validations on the data loaded from the YAML description files + """ + for suite in self.test_suites: + # Validate that the images the suite must run on are in images.yml + if suite.images not in self.images: + raise Exception(f"Invalid image reference in test suite {suite.name}: Can't find {suite.images} in images.yml") + + # If the suite specifies a location, validate that the images are available in that location + if suite.location != '': + if not any(suite.location in i.locations for i in self.images[suite.images]): + raise Exception(f"Test suite {suite.name} must be executed in {suite.location}, but no images in {suite.images} are available in that location") - # Else, it should be a comma-separated list of description files - description_files: List[Path] = [self._root/"test_suites"/f"{t.strip()}.json" for t in test_suites.split(',')] - return [self._load_test_suite(AgentTestLoader._load_file(s)) for s in description_files] + @staticmethod + def _load_test_suites(test_suites: str) -> List[TestSuiteInfo]: + # + # Attempt to parse 'test_suites' as the YML description of a single suite + # + parsed = yaml.safe_load(test_suites) + + # + # A comma-separated list (e.g. "foo", "foo, bar", etc.) is valid YAML, but it is parsed as a string. An actual test suite would + # be parsed as a dictionary. If it is a dict, take is as the YML description of a single test suite + # + if isinstance(parsed, dict): + return [AgentTestLoader._load_test_suite(parsed)] + + # + # If test_suites is not YML, then it should be a comma-separated list of description files + # + description_files: List[Path] = [AgentTestLoader._SOURCE_CODE_ROOT/"test_suites"/f"{t.strip()}.yml" for t in test_suites.split(',')] + return [AgentTestLoader._load_test_suite(f) for f in description_files] - def _load_test_suite(self, test_suite: Dict[str, Any]) -> TestSuiteDescription: + @staticmethod + def _load_test_suite(description_file: Path) -> TestSuiteInfo: """ - Creates a TestSuiteDescription from its JSON representation, which has been loaded by JSON.loads and is passed - to this method as a dictionary + Loads the description of a TestSuite from its YAML file. + + A test suite has 5 properties: name, tests, images, location, and owns-vm. For example: + + name: "AgentBvt" + tests: + - "bvts/extension_operations.py" + - "bvts/run_command.py" + - "bvts/vm_access.py" + images: "endorsed" + location: "eastuseaup" + owns-vm: true + + * name - A string used to identify the test suite + * tests - A list of the tests in the suite. Each test is specified by the path for its source code relative to + WALinuxAgent/tests_e2e/tests. + * images - A string specifying the images on which the test suite must be executed. The value can be the name + of a single image (e.g."ubuntu_2004"), or the name of an image set (e.g. "endorsed"). The names for + images and image sets are defined in WALinuxAgent/tests_e2e/tests_suites/images.yml. + * location - [Optional; string] If given, the test suite must be executed on that location. If not specified, + or set to an empty string, the test suite will be executed in the default location. This is useful + for test suites that exercise a feature that is enabled only in certain regions. + * owns-vm - [Optional; boolean] By default all suites in a test run are executed on the same test VMs; if this + value is set to True, new test VMs will be created and will be used exclusively for this test suite. + This is useful for suites that modify the test VMs in such a way that the setup may cause problems + in other test suites (for example, some tests targeted to the HGAP block internet access in order to + force the agent to use the HGAP). + """ - suite = TestSuiteDescription() - suite.name = test_suite["name"] - suite.tests = [] - for source_file in [self._root/"tests"/t for t in test_suite["tests"]]: - suite.tests.extend(AgentTestLoader._load_tests(source_file)) - return suite + test_suite: Dict[str, Any] = AgentTestLoader._load_file(description_file) + + if any([test_suite.get(p) is None for p in ["name", "tests", "images"]]): + raise Exception(f"Invalid test suite: {description_file}. 'name', 'tests', and 'images' are required properties") + + test_suite_info = TestSuiteInfo() + + test_suite_info.name = test_suite["name"] + + test_suite_info.tests = [] + source_files = [AgentTestLoader._SOURCE_CODE_ROOT/"tests"/t for t in test_suite["tests"]] + for f in source_files: + test_suite_info.tests.extend(AgentTestLoader._load_test_classes(f)) + + test_suite_info.images = test_suite["images"] + + test_suite_info.location = test_suite.get("location") + if test_suite_info.location is None: + test_suite_info.location = "" + + test_suite_info.owns_vm = "owns-vm" in test_suite and test_suite["owns-vm"] + + return test_suite_info @staticmethod - def _load_tests(source_file: Path) -> List[Type[AgentTest]]: + def _load_test_classes(source_file: Path) -> List[Type[AgentTest]]: """ Takes a 'source_file', which must be a Python module, and returns a list of all the classes derived from AgentTest. """ @@ -96,12 +197,53 @@ def _load_tests(source_file: Path) -> List[Type[AgentTest]]: return [v for v in module.__dict__.values() if isinstance(v, type) and issubclass(v, AgentTest) and v != AgentTest] @staticmethod - def _load_file(file: Path): - """Helper to load a JSON file""" + def _load_images() -> Dict[str, List[VmImageInfo]]: + """ + Loads images.yml into a dictionary where, for each item, the key is an image or image set and the value is a list of VmImageInfos + for the corresponding images. + + See the comments in image.yml for a description of the structure of each item. + """ + image_descriptions = AgentTestLoader._load_file(AgentTestLoader._SOURCE_CODE_ROOT/"test_suites"/"images.yml") + if "images" not in image_descriptions: + raise Exception("images.yml is missing the 'images' item") + + images = {} + + # first load the images as 1-item lists + for name, description in image_descriptions["images"].items(): + i = VmImageInfo() + if isinstance(description, str): + i.urn = description + i.locations = [] + i.vm_sizes = [] + else: + if "urn" not in description: + raise Exception(f"Image {name} is missing the 'urn' property: {description}") + i.urn = description["urn"] + i.locations = description["locations"] if "locations" in description else [] + i.vm_sizes = description["vm_sizes"] if "vm_sizes" in description else [] + images[name] = [i] + + # now load the image-sets, mapping them to the images that we just computed + for image_set_name, image_list in image_descriptions["image-sets"].items(): + # the same name cannot denote an image and an image-set + if image_set_name in images: + raise Exception(f"Invalid image-set in images.yml: {image_set_name}. The name is used by an existing image") + images_in_set = [] + for i in image_list: + if i not in images: + raise Exception(f"Can't find image {i} (referenced by image-set {image_set_name}) in images.yml") + images_in_set.extend(images[i]) + images[image_set_name] = images_in_set + + return images + + @staticmethod + def _load_file(file: Path) -> Dict[str, Any]: + """Helper to load a YML file""" try: with file.open() as f: - return json.load(f) + return yaml.safe_load(f) except Exception as e: raise Exception(f"Can't load {file}: {e}") - - diff --git a/tests_e2e/orchestrator/lib/agent_test_suite.py b/tests_e2e/orchestrator/lib/agent_test_suite.py index 18f7fd35e3..54551541b4 100644 --- a/tests_e2e/orchestrator/lib/agent_test_suite.py +++ b/tests_e2e/orchestrator/lib/agent_test_suite.py @@ -35,7 +35,7 @@ Node, notifier, TestCaseMetadata, - TestSuite, + TestSuite as LisaTestSuite, TestSuiteMetadata, ) from lisa.messages import TestStatus, TestResultMessage # pylint: disable=E0401 @@ -44,7 +44,7 @@ import makepkg from azurelinuxagent.common.version import AGENT_VERSION -from tests_e2e.orchestrator.lib.agent_test_loader import AgentTestLoader, TestSuiteDescription +from tests_e2e.orchestrator.lib.agent_test_loader import TestSuiteInfo from tests_e2e.tests.lib.agent_test_context import AgentTestContext from tests_e2e.tests.lib.identifiers import VmIdentifier from tests_e2e.tests.lib.logging import log as agent_test_logger # Logger used by the tests @@ -98,7 +98,7 @@ class CollectLogs(Enum): @TestSuiteMetadata(area="waagent", category="", description="") -class AgentTestSuite(TestSuite): +class AgentTestSuite(LisaTestSuite): """ Manages the setup of test VMs and execution of Agent test suites. This class acts as the interface with the LISA framework, which will invoke the execute() method when a runbook is executed. @@ -112,7 +112,7 @@ def __init__(self, vm: VmIdentifier, paths: AgentTestContext.Paths, connection: self.node: Node = None self.runbook_name: str = None self.image_name: str = None - self.test_suites: List[str] = None + self.test_suites: List[AgentTestSuite] = None self.collect_logs: str = None self.skip_setup: bool = None @@ -128,7 +128,7 @@ def _set_context(self, node: Node, variables: Dict[str, Any], log: Logger): # Remove the resource group and node suffix, e.g. "e1-n0" in "lisa-20230110-162242-963-e1-n0" runbook_name = re.sub(r"-\w+-\w+$", "", runbook.name) - self.__context = AgentTestSuite._Context( + self.__context = self._Context( vm=VmIdentifier( location=runbook.location, subscription=node.features._platform.subscription_id, @@ -147,15 +147,15 @@ def _set_context(self, node: Node, variables: Dict[str, Any], log: Logger): self.__context.log = log self.__context.node = node self.__context.image_name = f"{runbook.marketplace.offer}-{runbook.marketplace.sku}" - self.__context.test_suites = AgentTestSuite._get_required_parameter(variables, "test_suites") - self.__context.collect_logs = AgentTestSuite._get_required_parameter(variables, "collect_logs") - self.__context.skip_setup = AgentTestSuite._get_required_parameter(variables, "skip_setup") + self.__context.test_suites = self._get_required_parameter(variables, "test_suites_info") + self.__context.collect_logs = self._get_required_parameter(variables, "collect_logs") + self.__context.skip_setup = self._get_required_parameter(variables, "skip_setup") self._log.info( "Test suite parameters: [skip_setup: %s] [collect_logs: %s] [test_suites: %s]", self.context.skip_setup, self.context.collect_logs, - self.context.test_suites) + [t.name for t in self.context.test_suites]) @staticmethod def _get_required_parameter(variables: Dict[str, Any], name: str) -> Any: @@ -191,7 +191,7 @@ def _setup(self) -> None: Returns the path to the agent package. """ - AgentTestSuite._setup_lock.acquire() + self._setup_lock.acquire() try: self._log.info("") @@ -211,7 +211,7 @@ def _setup(self) -> None: completed.touch() finally: - AgentTestSuite._setup_lock.release() + self._setup_lock.release() def _build_agent_package(self) -> None: """ @@ -290,10 +290,9 @@ def _collect_node_logs(self) -> None: self._log.exception("Failed to collect logs from the test machine") @TestCaseMetadata(description="", priority=0) - def execute(self, node: Node, variables: Dict[str, Any], log: Logger) -> None: + def agent_test_suite(self, node: Node, variables: Dict[str, Any], log: Logger) -> None: """ - Executes each of the AgentTests in the given List. Note that 'test_suite' is a list of test classes, rather than - instances of the test class (this method will instantiate each of these test classes). + Executes each of the AgentTests included in "test_suites_info" variable (which is generated by the AgentTestSuitesCombinator). """ self._set_context(node, variables, log) @@ -308,9 +307,9 @@ def execute(self, node: Node, variables: Dict[str, Any], log: Logger) -> None: if not self.context.skip_setup: self._setup_node() - test_suites: List[TestSuiteDescription] = AgentTestLoader(self.context.test_source_directory).load(self.context.test_suites) - - for suite in test_suites: + # pylint seems to think self.context.test_suites is not iterable. Suppressing warning, since its type is List[AgentTestSuite] + # E1133: Non-iterable value self.context.test_suites is used in an iterating context (not-an-iterable) + for suite in self.context.test_suites: # pylint: disable=E1133 test_suite_success = self._execute_test_suite(suite) and test_suite_success finally: @@ -329,7 +328,7 @@ def execute(self, node: Node, variables: Dict[str, Any], log: Logger) -> None: finally: self._clean_up() - def _execute_test_suite(self, suite: TestSuiteDescription) -> bool: + def _execute_test_suite(self, suite: TestSuiteInfo) -> bool: """ Executes the given test suite and returns True if all the tests in the suite succeeded. """ diff --git a/tests_e2e/orchestrator/lib/agent_test_suite_combinator.py b/tests_e2e/orchestrator/lib/agent_test_suite_combinator.py new file mode 100644 index 0000000000..8cf91dc5a2 --- /dev/null +++ b/tests_e2e/orchestrator/lib/agent_test_suite_combinator.py @@ -0,0 +1,119 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. +import logging + +from dataclasses import dataclass, field +from typing import Any, Dict, List, Optional, Type + +# E0401: Unable to import 'dataclasses_json' (import-error) +from dataclasses_json import dataclass_json # pylint: disable=E0401 + +# Disable those warnings, since 'lisa' is an external, non-standard, dependency +# E0401: Unable to import 'lisa' (import-error) +# etc +from lisa import schema # pylint: disable=E0401 +from lisa.combinator import Combinator # pylint: disable=E0401 +from lisa.util import field_metadata # pylint: disable=E0401 + +from tests_e2e.orchestrator.lib.agent_test_loader import AgentTestLoader + + +@dataclass_json() +@dataclass +class AgentTestSuitesCombinatorSchema(schema.Combinator): + test_suites: str = field( + default_factory=str, metadata=field_metadata(required=True) + ) + + +class AgentTestSuitesCombinator(Combinator): + """ + The "agent_test_suites" combinator returns a list of items containing five variables that specify the environments + that the agent test suites must be executed on: + + * marketplace_image: e.g. "Canonical UbuntuServer 18.04-LTS latest", + * location: e.g. "westus2", + * vm_size: e.g. "Standard_D2pls_v5" + * vhd: e.g "https://rhel.blob.core.windows.net/images/RHEL_8_Standard-8.3.202006170423.vhd?se=..." + * test_suites_info: e.g. [AgentBvt, FastTrack] + + (marketplace_image, location, vm_size) and vhd are mutually exclusive and define the environment (i.e. the test VM) + in which the test will be executed. test_suites_info defines the test suites that should be executed in that + environment. + """ + def __init__(self, runbook: AgentTestSuitesCombinatorSchema) -> None: + super().__init__(runbook) + self._environments = self.create_environment_list(self.runbook.test_suites) + self._index = 0 + + @classmethod + def type_name(cls) -> str: + return "agent_test_suites" + + @classmethod + def type_schema(cls) -> Type[schema.TypedSchema]: + return AgentTestSuitesCombinatorSchema + + def _next(self) -> Optional[Dict[str, Any]]: + result: Optional[Dict[str, Any]] = None + if self._index < len(self._environments): + result = self._environments[self._index] + self._index += 1 + return result + + _DEFAULT_LOCATION = "westus2" + + @staticmethod + def create_environment_list(test_suites: str) -> List[Dict[str, Any]]: + environment_list: List[Dict[str, Any]] = [] + shared_environments: Dict[str, Dict[str, Any]] = {} + + loader = AgentTestLoader(test_suites) + + for suite_info in loader.test_suites: + images_info = loader.images[suite_info.images] + for image in images_info: + # If the suite specifies a location, use it. Else, if the image specifies a list of locations, use + # any of them. Otherwise, use the default location. + if suite_info.location != '': + location = suite_info.location + elif len(image.locations) > 0: + location = image.locations[0] + else: + location = AgentTestSuitesCombinator._DEFAULT_LOCATION + + # If the image specifies a list of VM sizes, use any of them. Otherwise, set the size to empty and let LISA choose it. + vm_size = image.vm_sizes[0] if len(image.vm_sizes) > 0 else "" + + if suite_info.owns_vm: + environment_list.append({ + "marketplace_image": image.urn, + "location": location, + "vm_size": vm_size, + "vhd": "", + "test_suites_info": [suite_info] + }) + else: + key: str = f"{image.urn}:{location}" + if key in shared_environments: + shared_environments[key]["test_suites_info"].append(suite_info) + else: + shared_environments[key] = { + "marketplace_image": image.urn, + "location": location, + "vm_size": vm_size, + "vhd": "", + "test_suites_info": [suite_info] + } + + environment_list.extend(shared_environments.values()) + + log: logging.Logger = logging.getLogger("lisa") + log.info("******** Environments *****") + for e in environment_list: + log.info( + "{ marketplace_image: '%s', location: '%s', vm_size: '%s', vhd: '%s', test_suites_info: '%s' }", + e['marketplace_image'], e['location'], e['vm_size'], e['vhd'], [s.name for s in e['test_suites_info']]) + log.info("***************************") + + return environment_list diff --git a/tests_e2e/orchestrator/runbook.yml b/tests_e2e/orchestrator/runbook.yml index 74bbec46e4..2f67cf7ff8 100644 --- a/tests_e2e/orchestrator/runbook.yml +++ b/tests_e2e/orchestrator/runbook.yml @@ -26,11 +26,6 @@ variable: # # These variables define parameters for the AgentTestSuite; see the test wiki for details. # - # The test suites to execute - - name: test_suites - value: "agent_bvt" - is_case_visible: true - # Whether to collect logs from the test VM - name: collect_logs value: "failed" @@ -42,20 +37,15 @@ variable: is_case_visible: true # - # Set these to use an SSH proxy when executing the runbook + # These variables parameters for the AgentTestSuitesCombinator combinator # - - name: proxy - value: False - - name: proxy_host - value: "" - - name: proxy_user - value: "foo" - - name: proxy_identity_file - value: "" - is_secret: true + # The test suites to execute + - name: test_suites + value: "agent_bvt" + is_case_visible: true # - # The image, vm_size, and location are set by the combinator + # These variables are set by the AgentTestSuitesCombinator combinator # - name: marketplace_image value: "" @@ -63,8 +53,24 @@ variable: value: "" - name: location value: "" - - name: default_location - value: "westus2" + - name: vhd + value: "" + - name: test_suites_info + value: [] + is_case_visible: true + + # + # Set these variables to use an SSH proxy when executing the runbook + # + - name: proxy + value: False + - name: proxy_host + value: "" + - name: proxy_user + value: "foo" + - name: proxy_identity_file + value: "" + is_secret: true platform: - type: azure @@ -80,38 +86,14 @@ platform: core_count: min: 2 azure: - marketplace: "$(marketplace_image)" - vhd: "" + marketplace: $(marketplace_image) + vhd: $(vhd) location: $(location) vm_size: $(vm_size) combinator: - type: batch - items: - - marketplace_image: "Canonical UbuntuServer 18.04-LTS latest" - location: $(default_location) - vm_size: "" - - marketplace_image: "Debian debian-10 10 latest" - location: $(default_location) - vm_size: "" - - marketplace_image: "OpenLogic CentOS 7_9 latest" - location: $(default_location) - vm_size: "" - - marketplace_image: "SUSE sles-15-sp2-basic gen2 latest" - location: $(default_location) - vm_size: "" - - marketplace_image: "RedHat RHEL 7-RAW latest" - location: $(default_location) - vm_size: "" - - marketplace_image: "microsoftcblmariner cbl-mariner cbl-mariner-1 latest" - location: $(default_location) - vm_size: "" - - marketplace_image: "microsoftcblmariner cbl-mariner cbl-mariner-2 latest" - location: $(default_location) - vm_size: "" - - marketplace_image: "microsoftcblmariner cbl-mariner cbl-mariner-2-arm64 latest" - location: "eastus" - vm_size: "Standard_D2pls_v5" + type: agent_test_suites + test_suites: $(test_suites) concurrency: 10 diff --git a/tests_e2e/pipeline/pipeline-cleanup.yml b/tests_e2e/pipeline/pipeline-cleanup.yml index fd01212131..109b9492e7 100644 --- a/tests_e2e/pipeline/pipeline-cleanup.yml +++ b/tests_e2e/pipeline/pipeline-cleanup.yml @@ -4,7 +4,7 @@ # Runs every 3 hours and deletes any resource groups that are more than a day old and contain string "lisa-WALinuxAgent-" # schedules: - - cron: "0 */3 * * *" # Run every 3 hours + - cron: "0 */12 * * *" # Run twice a day (every 12 hours) displayName: cleanup build branches: include: diff --git a/tests_e2e/test_suites/agent_bvt.json b/tests_e2e/test_suites/agent_bvt.json deleted file mode 100644 index 76896791ee..0000000000 --- a/tests_e2e/test_suites/agent_bvt.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "name": "AgentBvt", - - "tests": [ - "bvts/extension_operations.py", - "bvts/run_command.py", - "bvts/vm_access.py" - ] -} diff --git a/tests_e2e/test_suites/agent_bvt.yml b/tests_e2e/test_suites/agent_bvt.yml new file mode 100644 index 0000000000..d5551837ee --- /dev/null +++ b/tests_e2e/test_suites/agent_bvt.yml @@ -0,0 +1,6 @@ +name: "AgentBvt" +tests: + - "bvts/extension_operations.py" + - "bvts/run_command.py" + - "bvts/vm_access.py" +images: "endorsed" \ No newline at end of file diff --git a/tests_e2e/test_suites/fail.json b/tests_e2e/test_suites/fail.json deleted file mode 100644 index 8f3ebcdce0..0000000000 --- a/tests_e2e/test_suites/fail.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "name": "Fail", - "tests": ["fail_test.py", "error_test.py"] -} diff --git a/tests_e2e/test_suites/fail.yml b/tests_e2e/test_suites/fail.yml new file mode 100644 index 0000000000..6cd3b01aff --- /dev/null +++ b/tests_e2e/test_suites/fail.yml @@ -0,0 +1,5 @@ +name: "Fail" +tests: + - "fail_test.py" + - "error_test.py" +images: "ubuntu_1804" diff --git a/tests_e2e/test_suites/images.yml b/tests_e2e/test_suites/images.yml new file mode 100644 index 0000000000..4b04373f7a --- /dev/null +++ b/tests_e2e/test_suites/images.yml @@ -0,0 +1,70 @@ +# +# Image sets are used to group images +# +image-sets: + # Endorsed distros that are tested on the daily runs + endorsed: +# +# TODO: Add CentOS 6.10 and Debian 8 +# +# - "centos_610" + - "centos_79" +# - "debian_8" + - "debian_10" + - "debian_9" + - "suse_12" + - "mariner_1" + - "mariner_2" + - "mariner_2_arm64" + - "suse_15" + - "rhel_78" + - "rhel_82" + - "ubuntu_1604" + - "ubuntu_1804" + - "ubuntu_2004" + +# +# An image can be specified by a string giving its urn, as in +# +# ubuntu_2004: "Canonical 0001-com-ubuntu-server-focal 20_04-lts latest" +# +# or by an object with 3 properties: urn, locations and vm_sizes, as in +# +# mariner_2_arm64: +# urn: "microsoftcblmariner cbl-mariner cbl-mariner-2-arm64 latest" +# locations: +# - "eastus" +# vm_sizes: +# - "Standard_D2pls_v5" +# +# 'urn' is required, while 'locations' and 'vm_sizes' are optional. The latter +# two properties can be used to specify that the image is available only in +# some locations, or that it can be used only on some VM sizes. +# +# URNs follow the format ' ' or +# ':::' +# +images: +# +# TODO: Add CentOS 6.10 and Debian 8 +# +# centos_610: "OpenLogic CentOS 6.10 latest" + centos_79: "OpenLogic CentOS 7_9 latest" +# debian_8: "credativ Debian 8 latest" + debian_9: "credativ Debian 9 latest" + debian_10: "Debian debian-10 10 latest" + mariner_1: "microsoftcblmariner cbl-mariner cbl-mariner-1 latest" + mariner_2: "microsoftcblmariner cbl-mariner cbl-mariner-2 latest" + mariner_2_arm64: + urn: "microsoftcblmariner cbl-mariner cbl-mariner-2-arm64 latest" + locations: + - "eastus" + vm_sizes: + - "Standard_D2pls_v5" + suse_12: "SUSE sles-12-sp5-basic gen1 latest" + suse_15: "SUSE sles-15-sp2-basic gen2 latest" + rhel_78: "RedHat RHEL 7.8 latest" + rhel_82: "RedHat RHEL 8.2 latest" + ubuntu_1604: "Canonical UbuntuServer 16.04-LTS latest" + ubuntu_1804: "Canonical UbuntuServer 18.04-LTS latest" + ubuntu_2004: "Canonical 0001-com-ubuntu-server-focal 20_04-lts latest" diff --git a/tests_e2e/test_suites/pass.json b/tests_e2e/test_suites/pass.json deleted file mode 100644 index 66b521b888..0000000000 --- a/tests_e2e/test_suites/pass.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "name": "Pass", - "tests": ["pass_test.py"] -} diff --git a/tests_e2e/test_suites/pass.yml b/tests_e2e/test_suites/pass.yml new file mode 100644 index 0000000000..40b0e60b46 --- /dev/null +++ b/tests_e2e/test_suites/pass.yml @@ -0,0 +1,4 @@ +name: "Pass" +tests: + - "pass_test.py" +images: "ubuntu_2004" From 31212a34038c76e077cc72b011942268abae7310 Mon Sep 17 00:00:00 2001 From: Norberto Arrieta Date: Wed, 15 Feb 2023 14:12:37 -0800 Subject: [PATCH 41/63] Remove DCR v2 prototype (#2762) Co-authored-by: narrieta --- dcr/README.md | 129 -------- dcr/__init__.py | 0 dcr/azure-cleanup-pipeline.yml | 35 -- dcr/azure-pipelines.yml | 105 ------ dcr/docs/DCR-workflow.jpg | Bin 277162 -> 0 bytes dcr/docs/orchestrator-vm-flow.jpg | Bin 265783 -> 0 bytes dcr/requirements.txt | 14 - dcr/scenario_utils/__init__.py | 0 dcr/scenario_utils/agent_log_parser.py | 99 ------ dcr/scenario_utils/azure_models.py | 239 -------------- dcr/scenario_utils/cgroups_helpers.py | 301 ----------------- dcr/scenario_utils/check_waagent_log.py | 207 ------------ dcr/scenario_utils/common_utils.py | 154 --------- dcr/scenario_utils/crypto.py | 60 ---- dcr/scenario_utils/distro.py | 40 --- .../extensions/BaseExtensionTestClass.py | 113 ------- .../extensions/CustomScriptExtension.py | 29 -- .../extensions/GATestExtGoExtension.py | 26 -- .../extensions/RunCommandExtension.py | 27 -- .../extensions/VMAccessExtension.py | 38 --- dcr/scenario_utils/extensions/__init__.py | 0 dcr/scenario_utils/logging_utils.py | 33 -- dcr/scenario_utils/models.py | 137 -------- dcr/scenario_utils/test_orchestrator.py | 97 ------ dcr/scenarios/__init__.py | 0 dcr/scenarios/agent-bvt/__init__.py | 0 .../agent-bvt/check_extension_timing.py | 55 ---- dcr/scenarios/agent-bvt/check_firewall.py | 64 ---- dcr/scenarios/agent-bvt/run.host.py | 23 -- dcr/scenarios/agent-bvt/run1.py | 16 - dcr/scenarios/agent-bvt/run2.py | 21 -- dcr/scenarios/agent-bvt/test_agent_basics.py | 103 ------ .../agent-persist-firewall/access_wire_ip.sh | 66 ---- .../persist_firewall_helpers.py | 294 ----------------- .../agent-persist-firewall/run.host.py | 41 --- dcr/scenarios/agent-persist-firewall/run1.py | 13 - dcr/scenarios/agent-persist-firewall/run2.py | 58 ---- dcr/scenarios/agent-persist-firewall/run3.py | 36 --- dcr/scenarios/agent-persist-firewall/setup.sh | 29 -- .../ext-seq-multiple-dependencies/config.json | 3 - .../ext-seq-multiple-dependencies/ext_seq.py | 171 ---------- .../ext_seq_tests.py | 196 ----------- .../ext-seq-multiple-dependencies/run.host.py | 27 -- .../ext-seq-multiple-dependencies/run.py | 15 - .../template.json | 304 ------------------ .../etp_helpers.py | 180 ----------- .../extension-telemetry-pipeline/run.host.py | 19 -- .../extension-telemetry-pipeline/run.py | 102 ------ .../extension-telemetry-pipeline/setup.sh | 20 -- dcr/scripts/__init__.py | 0 dcr/scripts/build_agent_zip.sh | 16 - dcr/scripts/get_pypy.sh | 21 -- dcr/scripts/install_pip_packages.sh | 15 - dcr/scripts/move_scenario.sh | 20 -- dcr/scripts/orchestrator/__init__.py | 0 dcr/scripts/orchestrator/execute_ssh_on_vm.py | 61 ---- .../orchestrator/generate_test_files.py | 36 --- dcr/scripts/orchestrator/set_environment.py | 64 ---- dcr/scripts/setup_agent.sh | 34 -- dcr/scripts/test-vm/harvest.sh | 20 -- dcr/templates/arm-delete.yml | 33 -- dcr/templates/deploy-linux-vm-params.json | 9 - dcr/templates/deploy-linux-vm.json | 280 ---------------- dcr/templates/setup-vm-and-execute-tests.yml | 207 ------------ dcr/templates/vars.yml | 21 -- 65 files changed, 4576 deletions(-) delete mode 100644 dcr/README.md delete mode 100644 dcr/__init__.py delete mode 100644 dcr/azure-cleanup-pipeline.yml delete mode 100644 dcr/azure-pipelines.yml delete mode 100644 dcr/docs/DCR-workflow.jpg delete mode 100644 dcr/docs/orchestrator-vm-flow.jpg delete mode 100644 dcr/requirements.txt delete mode 100644 dcr/scenario_utils/__init__.py delete mode 100644 dcr/scenario_utils/agent_log_parser.py delete mode 100644 dcr/scenario_utils/azure_models.py delete mode 100644 dcr/scenario_utils/cgroups_helpers.py delete mode 100644 dcr/scenario_utils/check_waagent_log.py delete mode 100644 dcr/scenario_utils/common_utils.py delete mode 100644 dcr/scenario_utils/crypto.py delete mode 100644 dcr/scenario_utils/distro.py delete mode 100644 dcr/scenario_utils/extensions/BaseExtensionTestClass.py delete mode 100644 dcr/scenario_utils/extensions/CustomScriptExtension.py delete mode 100644 dcr/scenario_utils/extensions/GATestExtGoExtension.py delete mode 100644 dcr/scenario_utils/extensions/RunCommandExtension.py delete mode 100644 dcr/scenario_utils/extensions/VMAccessExtension.py delete mode 100644 dcr/scenario_utils/extensions/__init__.py delete mode 100644 dcr/scenario_utils/logging_utils.py delete mode 100644 dcr/scenario_utils/models.py delete mode 100644 dcr/scenario_utils/test_orchestrator.py delete mode 100644 dcr/scenarios/__init__.py delete mode 100644 dcr/scenarios/agent-bvt/__init__.py delete mode 100644 dcr/scenarios/agent-bvt/check_extension_timing.py delete mode 100644 dcr/scenarios/agent-bvt/check_firewall.py delete mode 100644 dcr/scenarios/agent-bvt/run.host.py delete mode 100644 dcr/scenarios/agent-bvt/run1.py delete mode 100644 dcr/scenarios/agent-bvt/run2.py delete mode 100644 dcr/scenarios/agent-bvt/test_agent_basics.py delete mode 100644 dcr/scenarios/agent-persist-firewall/access_wire_ip.sh delete mode 100644 dcr/scenarios/agent-persist-firewall/persist_firewall_helpers.py delete mode 100644 dcr/scenarios/agent-persist-firewall/run.host.py delete mode 100644 dcr/scenarios/agent-persist-firewall/run1.py delete mode 100644 dcr/scenarios/agent-persist-firewall/run2.py delete mode 100644 dcr/scenarios/agent-persist-firewall/run3.py delete mode 100644 dcr/scenarios/agent-persist-firewall/setup.sh delete mode 100644 dcr/scenarios/ext-seq-multiple-dependencies/config.json delete mode 100644 dcr/scenarios/ext-seq-multiple-dependencies/ext_seq.py delete mode 100644 dcr/scenarios/ext-seq-multiple-dependencies/ext_seq_tests.py delete mode 100644 dcr/scenarios/ext-seq-multiple-dependencies/run.host.py delete mode 100644 dcr/scenarios/ext-seq-multiple-dependencies/run.py delete mode 100644 dcr/scenarios/ext-seq-multiple-dependencies/template.json delete mode 100644 dcr/scenarios/extension-telemetry-pipeline/etp_helpers.py delete mode 100644 dcr/scenarios/extension-telemetry-pipeline/run.host.py delete mode 100644 dcr/scenarios/extension-telemetry-pipeline/run.py delete mode 100644 dcr/scenarios/extension-telemetry-pipeline/setup.sh delete mode 100644 dcr/scripts/__init__.py delete mode 100755 dcr/scripts/build_agent_zip.sh delete mode 100755 dcr/scripts/get_pypy.sh delete mode 100644 dcr/scripts/install_pip_packages.sh delete mode 100755 dcr/scripts/move_scenario.sh delete mode 100644 dcr/scripts/orchestrator/__init__.py delete mode 100644 dcr/scripts/orchestrator/execute_ssh_on_vm.py delete mode 100644 dcr/scripts/orchestrator/generate_test_files.py delete mode 100644 dcr/scripts/orchestrator/set_environment.py delete mode 100644 dcr/scripts/setup_agent.sh delete mode 100644 dcr/scripts/test-vm/harvest.sh delete mode 100644 dcr/templates/arm-delete.yml delete mode 100644 dcr/templates/deploy-linux-vm-params.json delete mode 100644 dcr/templates/deploy-linux-vm.json delete mode 100644 dcr/templates/setup-vm-and-execute-tests.yml delete mode 100644 dcr/templates/vars.yml diff --git a/dcr/README.md b/dcr/README.md deleted file mode 100644 index 7f8b4da7ec..0000000000 --- a/dcr/README.md +++ /dev/null @@ -1,129 +0,0 @@ -# DCR v2 - Azure Pipelines - -## Introduction - -This is the testing pipeline for the Linux Guest Agent. It uses [Azure Pipelines](https://docs.microsoft.com/en-us/azure/devops/pipelines/get-started/what-is-azure-pipelines?view=azure-devops) for its orchestration. Here's the link -of the pipeline - https://dev.azure.com/cplatruntime/WALinuxAgent/_build?definitionId=1 -
// To-Do: Update link with the final pipeline - -## Architecture - -A rough outline of the workflow is defined below. -- The entry way into the pipeline is - `dcr/azure-pipelines.yml` -- When a run is initiated, a DevOps agent on a fresh VM is assigned to the run from the Azure Pipelines hosted pool (image: `ubuntu-latest`). This is the orchestrator VM. - -### Orchestrator Setup -- We set up the orchestrator VM for the test runs - - Setup SSH keys on the orchestrator. We use the same keys to deploy the test VMs later so that the orchestrator always has access to the test VMs. - - Pin to Python 3.7 and install pip dependencies - - Download pypy3.7 for the test VM - - Overwrite the default settings of the run if a `config.json` file exists in the executing scenario. - - Downloads all secrets from Key-Vault that are needed to deploy ARM template and use `az-cli` to initiate extension/VM related APIs (These secrets are only available scoped till the orchestrator and not passed to the test VM to avoid any cred leaks) - -### Test-VM/VMSS Setup -- After setup, we deploy the Test-VM/VMSS as per the requested Distro. -- Once the VM is deployed, we set up the test VM and prepare it for the tests - - - Copy over all files (agent + DCR related) to the test VM. - - The agent that is copied over is placed in the `/var/lib/waagent` directory and the agent is restarted to force it to pick up the testing agent. - > We don't install agent from source. - - (We copy the whole `dcr` directory over to the test VM. To make the abstractions easier, a new directory `dcr/scenario/` is created on the test VM and the contents of the executing scenario are copied over there. This negates the need to maintain a name-specific path if needed in the tests). - - In the case of a VMSS, we set up each instance separately. - - Using `asyncio`, we execute the setup commands simultaneously on each instance of the VMSS using SSH and wait for them to complete. - - If even a single instance fails to set up properly, we fail the whole step. - - The recommended way for setting up a Scale Set is to either use a custom image or CSE extension. Since we didn't want to rely on either of those methods, we chose this approach of setting up the instances separately. - - Install test dependencies on the Test VM (pip requirements, etc) - - Run scenario specific `setup.sh` (if applicable) - - Run distro specific setup scripts (if applicable) - -### Execute tests - -- Finally, after the setup is complete, we execute the tests (`run.host.py` or `run.py`) - - If both files are present in the scenario directory, we execute `run.host.py` first and then `run.py` in that order. - - If none of these files are present, then **no scripts would be executed for that scenario**. - > Note: run.py is executed on the VM using pypy3.7 and not the system python interpreter. - -### Fetch and Publish test results and artifacts - -- Once the tests are executed, we capture the `harvest` logs from the test VMs -- We collect the results from either `os.environ['BUILD_ARTIFACTSTAGINGDIRECTORY’]` directory in case of orchestrator VM or the `/home/{admin_username}` (or `~`) directory in case of the test VM. -- After collecting both data, we publish them separately per run to be visible on the UI. - -![Basic Workflow](docs/DCR-workflow.jpg) - -![Orchestrator-TestVM Flow](docs/orchestrator-vm-flow.jpg) - -## Key-points of DCR v2 - - -- Uses PyPy3.7 for executing python scripts on the Test VMs, alleviating the need to write cross-version compatible code -- For more ease of authoring scripts, pinned the orchestrator python version to py3.7 for parity -- Supports Mooncake and Fairfax -- Sets up the test VM by setting up the new agent as auto-update rather than installing from source (as that’s distro - dependent), making this less susceptible to set up failures -- Parameterized inputs, makes it easier to test specific scenarios and distros if needed. Easier to onboard new distros - too -- Easy to author as its simple python scripts -- (M * N) test VMs are created per run, where M being the number of scenarios and N being the number of Distros -- There's a 1:1 relation between the Azure Pipeline agent VM and the test VM. This is to reduce the setup scripts we need to maintain on our end and to utilize Azure Pipelines to the fullest. -- This framework supports both VMs and VMSS deployments and can handle concurrently executing scripts for their setups. - - -## Author a Test - -### Add a test -- To add a new scenario to the daily runs, simply add a new directory to the `dcr/scenarios` directory and add the scenario name to `scenarios` parameter in `dcr/azure-pipelines.yml` file -- Each file inside the scenario directory is confined to that scenario. To share code between scenarios, add the code in `dcr/scenario_utils` directory. -- Can specify `setup.sh` to run any scenario specific setup on the test VM –
-Eg: set up a cron job for firewall testing -- Can specify a `config.json` file to override default parameters of the test –
-Eg: set VM location to a specific region - -### Executing the test scripts -There are 2 entry points into the test pipeline – **_run.host.py_** and _**run.py**_ - - #### run.host.py - - Run the script on the Orchestrator VM - - Useful for running scenarios that require controlling the test VM externally - - Eg: Using Az-cli for adding extensions, restarting VMs, etc - - Drop off the result Junit XML file to `os.environ['BUILD_ARTIFACTSTAGINGDIRECTORY’]` directory - > This is the only script that has access to the KeyVault secrets. The other method does not have that. - - - #### run.py - - Executed via SSH on the test VM - - Can run anything, the only requirement is to drop off test result Junit XML file with to the home directory - `~ - /test-result-pf-run.xml` - -### Publishing test results -- The test framework expects the test results to be in a JUnit XML file. If no file is produced by the test script, then no test results would be published in the pipeline. -- Depending on the type of test script (orchestrator VM vs the test VM), the JUnit file needs to be dropped off in a specific location. -- In addition to the directory location, the result file must conform to this naming convention - `test-result*.xml`
-Eg: `test-results-bvt-host.xml` or `test-results-ext-seq-run1.xml` -- The framework will automatically fetch all the test files from both the locations and aggregate them into one single file for better readability in the UI. -- The test results would be visible from the `Tests` tab under the summary UI. - -## Troubleshooting failures -- The current implementation provides multiple helper utility tools to ease the process of authoring tests with enough retries and logging. -- In case of a test failure, the best place would be to start at the `Test UI` page as that would give you the exact failures with a stack trace of the failure. -- If that's not enough, you can go into the `summary` page of the run and check the console output at the task level.
-> Tip: The logger implemented in the code logs to the console too in the format it expects to make it more readable from the console output itself. -- Additionally, the `harvest logs` captures all relevant data from the TestVM before deleting it. That can be referred to too if the failure is coming from within the test VM itself. -- Currently, the logs are not written to a file, but can be added later if needed. - -## Nomenclature - -Here's a list of certain terminologies used widely in the repo - -| Name | Meaning | -| ------------- |:-------------:| -| Scenarios | The test directories used to add new test cases to the pipeline. Each directory under dcr/scenarios represents a test scenario that would be tested on the test VM | -| Test Orchestrator | The VM created by Azure Pipelines that hosts the tests for a specific scenario and a distro | -| Test VM | The VM created by the pipeline to run the tests. This is where we actually test out the scripts. | -| Scenario Utils | Directory where all common code is placed | -| Harvest logs | All the logs from the test VM. Useful for debugging VM related issues | -| [YML/YAML](https://docs.microsoft.com/en-us/azure/devops/pipelines/get-started/yaml-pipeline-editor?view=azure-devops) | The file format in which the azure pipeline is defined | -| [Azure Pipelines](https://docs.microsoft.com/en-us/azure/devops/pipelines/get-started/what-is-azure-pipelines?view=azure-devops) | CI/CD tool that we utilize for our DCR testing | -| JUnit XML | Standard for the test results file that we use to publish our test results | -| [Jobs](https://docs.microsoft.com/en-us/azure/devops/pipelines/process/phases?view=azure-devops&tabs=yaml) | A job is a series of steps that run sequentially as a unit. In other words, a job is the smallest unit of work that can be scheduled to run. | -| [Tasks](https://docs.microsoft.com/en-us/azure/devops/pipelines/get-started/key-pipelines-concepts?view=azure-devops#task) | A task is the building block for defining automation in a pipeline. A task is packaged script or procedure that has been abstracted with a set of inputs. | -| [Steps](https://docs.microsoft.com/en-us/azure/devops/pipelines/get-started/key-pipelines-concepts?view=azure-devops#step) | A step is the smallest building block of a pipeline. | - - - diff --git a/dcr/__init__.py b/dcr/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/dcr/azure-cleanup-pipeline.yml b/dcr/azure-cleanup-pipeline.yml deleted file mode 100644 index 12f8993370..0000000000 --- a/dcr/azure-cleanup-pipeline.yml +++ /dev/null @@ -1,35 +0,0 @@ -# Pipeline for cleaning up any remaining Resource Groups generated by the Azure.WALinuxAgent pipeline -# Runs every 3 hours and deletes any resource groups that are more than a day old and contain string dcr-v2-test - -schedules: - - cron: "0 */3 * * *" # Run every 3 hours - displayName: cleanup build - branches: - include: - - develop - always: true - -# no PR triggers -pr: none - -pool: - vmImage: ubuntu-latest - -variables: - - template: templates/vars.yml - -steps: - - - task: AzureCLI@2 - inputs: - azureSubscription: '$(azureConnection)' - scriptType: 'bash' - scriptLocation: 'inlineScript' - inlineScript: | - set -euxo pipefail - date=`date --utc +%Y-%m-%d'T'%H:%M:%S.%N'Z' -d "1 day ago"` - - # Using the Azure REST GET resourceGroups API call as we can add the createdTime to the results. - # This feature is not available via the az-cli commands directly so we have to use the Azure REST APIs - - az rest --method GET --url "https://management.azure.com/subscriptions/$(subId)/resourcegroups" --url-parameters api-version=2021-04-01 \$expand=createdTime --output json --query value | jq --arg date "$date" '.[] | select (.createdTime < $date).name' | grep "$(rgPrefix)" | xargs -l -t -r az group delete --no-wait -y -n || echo "No resource groups found to delete" diff --git a/dcr/azure-pipelines.yml b/dcr/azure-pipelines.yml deleted file mode 100644 index c2bbe9d191..0000000000 --- a/dcr/azure-pipelines.yml +++ /dev/null @@ -1,105 +0,0 @@ -parameters: - - name: scenarios - type: object - default: - - agent-bvt - - extension-telemetry-pipeline - - - name: distros - type: object - default: - - publisher: "Canonical" - offer: "UbuntuServer" - version: "latest" - sku: "18.04-LTS" - name: "ubuntu18" - # ToDo: Figure out a better way to incorporate distro setup scripts -# setupPath: "dcr/distros/install_pip_packages.sh" - - - publisher: "Debian" - offer: "debian-10" - sku: "10" - version: "latest" - name: "deb10" -## setupPath: "dcr/distros/install_pip_packages.sh" -# - - publisher: "OpenLogic" - offer: "CentOS" - sku: "7_9" - version: "latest" - name: "cent79" -## - - publisher: "SUSE" - offer: "sles-15-sp2-basic" - sku: "gen1" - version: "latest" - name: "suse15" -## - - publisher: "RedHat" - offer: "RHEL" - sku: "7-RAW" - version: "latest" - name: "rhel7Raw" -## - - publisher: "microsoftcblmariner" - offer: "cbl-mariner" - sku: "cbl-mariner-1" - version: "latest" - name: "mariner1" -## - - publisher: "microsoftcblmariner" - offer: "cbl-mariner" - sku: "cbl-mariner-2" - version: "latest" - name: "mariner2" - -trigger: - - develop - -# no PR triggers -pr: none - -variables: - - template: templates/vars.yml - - - name: SSH_PUBLIC - value: "$(sshPublicKey)" # set in GUI variables - - name: rgNamePrefix - value: "$(rgPrefix)$(Build.BuildId)" - - -pool: #larohra-dcrvmsspool - vmImage: ubuntu-latest - -stages: - - stage: "Execute" - jobs: - - template: 'templates/setup-vm-and-execute-tests.yml' - parameters: - scenarios: - - ${{ parameters.scenarios }} - distros: - - ${{ parameters.distros }} - rgPrefix: $(rgNamePrefix) - - - stage: "Cleanup" - condition: succeededOrFailed() - jobs: - - job: "Wait" - pool: server - # ToDo: Add a parameter to force wait before deleting the Test VMs - condition: in(stageDependencies.Execute.CreateVM.result, 'Failed', 'SucceededWithIssues') - steps: - - task: ManualValidation@0 - timeoutInMinutes: 50 - inputs: - notifyUsers: 'larohra' - onTimeout: 'resume' - - - template: templates/arm-delete.yml - parameters: - scenarios: - - ${{ parameters.scenarios }} - distros: - - ${{ parameters.distros }} - rgPrefix: $(rgNamePrefix) \ No newline at end of file diff --git a/dcr/docs/DCR-workflow.jpg b/dcr/docs/DCR-workflow.jpg deleted file mode 100644 index 0e517f1fa33cda370c29989b914522502c226296..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 277162 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
T$PRe0YT&b{!?v3=V!{6uHH84|^J*S@U3X4(1AD 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 TrLRf^Kj44x$9x9(SKj{xDQsoc diff --git a/dcr/docs/orchestrator-vm-flow.jpg b/dcr/docs/orchestrator-vm-flow.jpg deleted file mode 100644 index 76c8926369eaed304425ce1baee47f709287ddfa..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 265783 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 b02lxUfB|3t7yt%<0bl?aSZEAzu-^P99@}^L diff --git a/dcr/requirements.txt b/dcr/requirements.txt deleted file mode 100644 index 91f91625b5..0000000000 --- a/dcr/requirements.txt +++ /dev/null @@ -1,14 +0,0 @@ -# This is a list of pip packages that will be installed on both the orchestrator and the test VM -# Only add the common packages here, for more specific modules, add them to the scenario itself -azure-identity -azure-keyvault-keys -azure-mgmt-compute>=22.1.0 -azure-mgmt-keyvault>=7.0.0 -azure-mgmt-network>=16.0.0 -azure-mgmt-resource>=15.0.0 -cryptography -distro -junitparser -msrestazure -pudb -python-dotenv \ No newline at end of file diff --git a/dcr/scenario_utils/__init__.py b/dcr/scenario_utils/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/dcr/scenario_utils/agent_log_parser.py b/dcr/scenario_utils/agent_log_parser.py deleted file mode 100644 index 5c67c3a806..0000000000 --- a/dcr/scenario_utils/agent_log_parser.py +++ /dev/null @@ -1,99 +0,0 @@ -from __future__ import print_function - -import os -import re -from datetime import datetime - -AGENT_LOG_FILE = '/var/log/waagent.log' - -# Example: -# ProcessExtensionsGoalState completed [etag_2824367392948713696 4073 ms] -GOAL_STATE_COMPLETED = r"ProcessExtensionsGoalState completed\s\[(?P[a-z_\d]+)\s(?P\d+)\sms\]" - -# The format of the log has changed over time and the current log may include records from different sources. Most records are single-line, but some of them -# can span across multiple lines. We will assume records always start with a line similar to the examples below; any other lines will be assumed to be part -# of the record that is being currently parsed. -# -# Newer Agent: 2019-11-27T22:22:48.123985Z VERBOSE ExtHandler ExtHandler Report vm agent status -# 2021-03-30T19:45:33.793213Z INFO ExtHandler [Microsoft.Azure.Security.Monitoring.AzureSecurityLinuxAgent-2.14.64] Target handler state: enabled [incarnation 3] -# -# Older Agent: 2021/03/30 19:35:35.971742 INFO Daemon Azure Linux Agent Version:2.2.45 -# -# Extension: 2021/03/30 19:45:31 Azure Monitoring Agent for Linux started to handle. -# 2021/03/30 19:45:31 [Microsoft.Azure.Monitor.AzureMonitorLinuxAgent-1.7.0] cwd is /var/lib/waagent/Microsoft.Azure.Monitor.AzureMonitorLinuxAgent-1.7.0 -# -_NEW_AGENT_RECORD = re.compile(r'(?P[0-9-]+T[0-9:.]+Z)\s(?PVERBOSE|INFO|WARNING|ERROR)\s(?P\S+)\s(?P(Daemon)|(ExtHandler)|(\[\S+\]))\s(?P.*)') -_OLD_AGENT_RECORD = re.compile(r'(?P[0-9/]+\s[0-9:.]+)\s(?PVERBOSE|INFO|WARNING|ERROR)\s(?P)(?P\S*)\s(?P.*)') -_EXTENSION_RECORD = re.compile(r'(?P[0-9/]+\s[0-9:.]+)\s(?P)(?P)((?P\[[^\]]+\])\s)?(?P.*)') - -# In 2.2.46, the date time was changed to ISO-8601 format but thread name was not added. This regex takes care of that -# Sample: 2021-05-28T01:17:40.683072Z INFO ExtHandler Wire server endpoint:168.63.129.16 -# 2021-05-28T01:17:40.683823Z WARNING ExtHandler Move rules file 70-persistent-net.rules to /var/lib/waagent/70-persistent-net.rules -# 2021-05-28T01:17:40.767600Z INFO ExtHandler Successfully added Azure fabric firewall rules -_46_AGENT_RECORD = re.compile(r'(?P[0-9-]+T[0-9:.]+Z)\s(?PVERBOSE|INFO|WARNING|ERROR)\s(?P)(?PDaemon|ExtHandler|\[\S+\])\s(?P.*)') - - -class AgentLogRecord: - __ERROR_TAGS = ['Exception', 'Traceback', '[CGW]'] - - def __init__(self, match): - self.text = match.string - self.when = match.group("when") - self.level = match.group("level") - self.thread = match.group("thread") - self.who = match.group("who") - self.message = match.group("message") - - def get_timestamp(self): - return datetime.strptime(self.when, u'%Y-%m-%dT%H:%M:%S.%fZ') - - @property - def is_error(self): - is_error = self.level in ('ERROR', 'WARNING') or any(err in self.text for err in self.__ERROR_TAGS) - # - # Don't report errors in the telemetry data. Sample log line: - # - # 2022-03-27T06:40:46.011455Z VERBOSE SendTelemetryHandler ExtHandler HTTP connection [POST] [/machine?comp=telemetrydata] [ VMMetaData: - return self.__vm_data - - @property - def compute_client(self) -> ComputeManagementClient: - if self.__compute_client is None: - self.__compute_client = ComputeManagementClient( - credential=DefaultAzureCredential(), - subscription_id=self.vm_data.sub_id - ) - return self.__compute_client - - @property - def resource_client(self) -> ResourceManagementClient: - if self.__resource_client is None: - self.__resource_client = ResourceManagementClient( - credential=DefaultAzureCredential(), - subscription_id=self.vm_data.sub_id - ) - return self.__resource_client - - @property - @abstractmethod - def vm_func(self): - pass - - @property - @abstractmethod - def extension_func(self): - pass - - @abstractmethod - def get_vm_instance_view(self): - pass - - @abstractmethod - def get_extensions(self): - pass - - @abstractmethod - def get_extension_instance_view(self, extension_name): - pass - - @abstractmethod - def get_ext_props(self, extension_data, settings=None, protected_settings=None, auto_upgrade_minor_version=True, - force_update_tag=None): - pass - - @abstractmethod - def restart(self, timeout=5): - pass - - def _run_azure_op_with_retry(self, get_func): - max_retries = 3 - retries = max_retries - while retries > 0: - try: - ext = get_func() - return ext - except (CloudError, HttpResponseError) as ce: - if retries > 0: - self.log.exception(f"Got Azure error: {ce}") - self.log.warning("...retrying [{0} attempts remaining]".format(retries)) - retries -= 1 - time.sleep(30 * (max_retries - retries)) - else: - raise - - -class VirtualMachineHelper(AzureComputeBaseClass): - - def __init__(self): - super().__init__() - - @property - def vm_func(self): - return self.compute_client.virtual_machines - - @property - def extension_func(self): - return self.compute_client.virtual_machine_extensions - - def get_vm_instance_view(self) -> VirtualMachineInstanceView: - return self._run_azure_op_with_retry(lambda: self.vm_func.get( - resource_group_name=self.vm_data.rg_name, - vm_name=self.vm_data.name, - expand="instanceView" - )) - - def get_extensions(self) -> List[VirtualMachineExtension]: - return self._run_azure_op_with_retry(lambda: self.extension_func.list( - resource_group_name=self.vm_data.rg_name, - vm_name=self.vm_data.name - )) - - def get_extension_instance_view(self, extension_name) -> VirtualMachineExtensionInstanceView: - return self._run_azure_op_with_retry(lambda: self.extension_func.get( - resource_group_name=self.vm_data.rg_name, - vm_name=self.vm_data.name, - vm_extension_name=extension_name, - expand="instanceView" - )) - - def get_ext_props(self, extension_data, settings=None, protected_settings=None, auto_upgrade_minor_version=True, - force_update_tag=None) -> VirtualMachineExtension: - return VirtualMachineExtension( - location=self.vm_data.location, - publisher=extension_data.publisher, - type_properties_type=extension_data.ext_type, - type_handler_version=extension_data.version, - auto_upgrade_minor_version=auto_upgrade_minor_version, - settings=settings, - protected_settings=protected_settings, - force_update_tag=force_update_tag - ) - - def restart(self, timeout=5): - self.log.info(f"Initiating restart of machine: {self.vm_data.name}") - poller : LROPoller = self._run_azure_op_with_retry(lambda: self.vm_func.begin_restart( - resource_group_name=self.vm_data.rg_name, - vm_name=self.vm_data.name - )) - poller.wait(timeout=timeout * 60) - if not poller.done(): - raise TimeoutError(f"Machine {self.vm_data.name} failed to restart after {timeout} mins") - self.log.info(f"Restarted machine: {self.vm_data.name}") - - -class VirtualMachineScaleSetHelper(AzureComputeBaseClass): - - def restart(self, timeout=5): - poller: LROPoller = self._run_azure_op_with_retry(lambda: self.vm_func.begin_restart( - resource_group_name=self.vm_data.rg_name, - vm_scale_set_name=self.vm_data.name - )) - poller.wait(timeout=timeout * 60) - if not poller.done(): - raise TimeoutError(f"ScaleSet {self.vm_data.name} failed to restart after {timeout} mins") - - def __init__(self): - super().__init__() - - @property - def vm_func(self): - return self.compute_client.virtual_machine_scale_set_vms - - @property - def extension_func(self): - return self.compute_client.virtual_machine_scale_set_extensions - - def get_vm_instance_view(self) -> VirtualMachineScaleSetInstanceView: - # Since this is a VMSS, return the instance view of the first VMSS VM. For the instance view of the complete VMSS, - # use the compute_client.virtual_machine_scale_sets function - - # https://docs.microsoft.com/en-us/python/api/azure-mgmt-compute/azure.mgmt.compute.v2019_12_01.operations.virtualmachinescalesetsoperations?view=azure-python - - for vm in self._run_azure_op_with_retry(lambda: self.vm_func.list(self.vm_data.rg_name, self.vm_data.name)): - try: - return self._run_azure_op_with_retry(lambda: self.vm_func.get_instance_view( - resource_group_name=self.vm_data.rg_name, - vm_scale_set_name=self.vm_data.name, - instance_id=vm.instance_id - )) - except Exception as err: - self.log.warning( - f"Unable to fetch instance view of VMSS VM: {vm}. Trying out other instances.\nError: {err}") - continue - - raise Exception(f"Unable to fetch instance view of any VMSS instances for {self.vm_data.name}") - - def get_extensions(self) -> List[VirtualMachineScaleSetExtension]: - return self._run_azure_op_with_retry(lambda: self.extension_func.list( - resource_group_name=self.vm_data.rg_name, - vm_scale_set_name=self.vm_data.name - )) - - def get_extension_instance_view(self, extension_name) -> VirtualMachineExtensionInstanceView: - return self._run_azure_op_with_retry(lambda: self.extension_func.get( - resource_group_name=self.vm_data.rg_name, - vm_scale_set_name=self.vm_data.name, - vmss_extension_name=extension_name, - expand="instanceView" - )) - - def get_ext_props(self, extension_data, settings=None, protected_settings=None, auto_upgrade_minor_version=True, - force_update_tag=None) -> VirtualMachineScaleSetExtension: - return VirtualMachineScaleSetExtension( - publisher=extension_data.publisher, - type_properties_type=extension_data.ext_type, - type_handler_version=extension_data.version, - auto_upgrade_minor_version=auto_upgrade_minor_version, - settings=settings, - protected_settings=protected_settings - ) - - -class ComputeManager: - """ - The factory class for setting the Helper class based on the setting. - """ - def __init__(self): - self.__vm_data = get_vm_data_from_env() - self.__compute_manager = None - - @property - def is_vm(self) -> bool: - return self.__vm_data.model_type == VMModelType.VM - - @property - def compute_manager(self): - if self.__compute_manager is None: - self.__compute_manager = VirtualMachineHelper() if self.is_vm else VirtualMachineScaleSetHelper() - return self.__compute_manager diff --git a/dcr/scenario_utils/cgroups_helpers.py b/dcr/scenario_utils/cgroups_helpers.py deleted file mode 100644 index f38827c111..0000000000 --- a/dcr/scenario_utils/cgroups_helpers.py +++ /dev/null @@ -1,301 +0,0 @@ -import os -import re -import subprocess -import sys - -from dcr.scenario_utils.distro import get_distro - -BASE_CGROUP = '/sys/fs/cgroup' -AGENT_CGROUP_NAME = 'WALinuxAgent' -AGENT_SERVICE_NAME = "walinuxagent.service" -CONTROLLERS = ['cpu'] # Only verify the CPU controller since memory accounting is not enabled yet. - -DAEMON_CMDLINE_PATTERN = re.compile(r".*python.*waagent.*-daemon") -AGENT_CMDLINE_PATTERN = re.compile(r".*python.*-run-exthandlers") - -CREATED_CGROUP_PATTERN = r"..*Created cgroup (/sys/fs/cgroup/.+)" -EXTENSION_PID_ADDED_PATTERN = re.compile(r".*Added PID (\d+) to cgroup[s]* (/sys/fs/cgroup/.+)") -CGROUP_TRACKED_PATTERN = re.compile(r'Started tracking cgroup ([^\s]+)\s+\[(?P[^\s]+)\]') - -# -# It is OK for these processes to show up in the Agent's cgroup -# -WHITELISTED_AGENT_REGEXES = [ - # - # The monitor thread uses these periodically: - # - re.compile(r"/sbin/dhclient\s.+/run/dhclient.*/var/lib/dhcp/dhclient.*/var/lib/dhcp/dhclient.*"), - re.compile(r".*iptables --version.*"), - re.compile(r".*iptables (-w)? -t security.*"), - # - # The agent starts extensions using systemd-run; the actual extension command will be on a different process. - # - re.compile(r".*systemd-run --unit=Microsoft.Azure.Diagnostics.LinuxDiagnostic_3.* " - r"--scope --slice=azure-vmextensions.slice /var/lib/waagent/Microsoft.Azure.Diagnostics.LinuxDiagnostic-3.*/diagnostic.py " - r"-enable.*"), - # - # The agent can start a new shell process. - # - re.compile(r"^\[sh\]$") -] - - -def exit_if_cgroups_not_supported(): - print("===== Checking if distro supports cgroups =====") - - __distro__ = get_distro() - base_fs_exists = os.path.exists(BASE_CGROUP) - - if not base_fs_exists: - print("\tDistro {0} does not support cgroups -- exiting".format(__distro__)) - sys.exit(1) - else: - print('\tDistro {0} supports cgroups\n'.format(__distro__)) - - -def run_get_output(cmd, print_std_out=False): - # Returns a list of stdout lines without \n at the end of the line. - output = subprocess.check_output(cmd, - stderr=subprocess.STDOUT, - shell=True) - output = str(output, - encoding='utf-8', - errors="backslashreplace") - - if print_std_out: - print(output) - - return output.split("\n") - - -def is_systemd_distro(): - try: - return run_get_output('cat /proc/1/comm')[0].strip() == 'systemd' - except Exception: - return False - - -def print_cgroups(): - print("====== Currently mounted cgroups ======") - for m in run_get_output('mount'): - if 'type cgroup' in m: - print('\t{0}'.format(m)) - print("") - - -def print_processes(): - print("====== Currently running processes ======") - processes = run_get_output("ps aux --forest") - for process in processes: - print("\t{0}".format(process)) - print("") - - -def print_service_status(service_status): - # Make sure to replace non-ascii characters since DCR logs anything that goes to stdout and will fail if - # there are non-ascii characters such as the ones showing up in `systemctl status {service_name}`. - for line in service_status: - print("\t" + line.encode("ascii", "replace").decode().replace("\n", "")) - print("") - - -def get_parent_pid(pid): - try: - with open("/proc/{0}/stat".format(pid), "r") as fh: - raw = fh.readline() - ppid = raw.split(" ")[3] - return ppid - except Exception: - return None - - -def get_pid_by_cmdline(pattern): - agent_pid = -1 - - for dirname in os.listdir('/proc'): - if dirname == 'curproc': - continue - - try: - with open('/proc/{0}/cmdline'.format(dirname), mode='r') as fd: - ps_cmd = fd.read() - if re.match(pattern, ps_cmd): - agent_pid = dirname - break - except Exception: - pass - - return agent_pid - - -def get_cmdline_by_pid(pid): - try: - with open('/proc/{0}/cmdline'.format(pid), mode='r') as process_fd: - return process_fd.read() - except Exception: - return None - - -def get_process_cgroups(pid): - with open('/proc/{0}/cgroup'.format(pid), mode='r') as fd: - return fd.read().split('\n')[:-1] - - -def get_agent_cgroup_mount_path(): - # TODO: change the service name based on distro (SUSE is waagent, for example) - if is_systemd_distro(): - return os.path.join('/', 'azure.slice', AGENT_SERVICE_NAME) - else: - return os.path.join('/', AGENT_SERVICE_NAME) - - -def check_cgroup_for_agent_process(name, pid): - process_cgroups = get_process_cgroups(pid) - expected_cgroup_path = get_agent_cgroup_mount_path() - - print('\tretrieved cgroups for {0}:'.format(name)) - for cgroup in process_cgroups: - print("\t\t{0}".format(cgroup)) - print("") - - for controller in CONTROLLERS: - for cgroup in process_cgroups: - # This is what the lines in /proc/PID/cgroup look like: - # 4:memory:/system.slice/walinuxagent.service - # 7:memory:/WALinuxAgent/Microsoft.EnterpriseCloud.Monitoring.OmsAgentForLinux - # We are interested in extracting the controller and mount path - mounted_controller = cgroup.split(':')[1].split(',') - mounted_path = cgroup.split(':')[2] - if controller in mounted_controller: - if mounted_path != expected_cgroup_path: - raise Exception("Expected {0} cgroup to be mounted under {1}, " - "but it's mounted under {2}".format(name, expected_cgroup_path, mounted_path)) - - print("\t{0}'s PID is {1}, cgroup mount path is {2}".format(name, pid, expected_cgroup_path)) - print("\tverified {0}'s /proc/cgroup is expected!\n".format(name)) - - -def check_pids_in_agent_cgroup(agent_cgroup_procs, daemon_pid, agent_pid): - with open(agent_cgroup_procs, "r") as agent_fd: - content = agent_fd.read() - print("\tcontent of {0}:\n{1}".format(agent_cgroup_procs, content)) - - pids = content.split('\n')[:-1] - - if daemon_pid not in pids: - raise Exception("Daemon PID {0} not found in expected cgroup {1}!".format(daemon_pid, agent_cgroup_procs)) - - if agent_pid not in pids: - raise Exception("Agent PID {0} not found in expected cgroup {1}!".format(agent_pid, agent_cgroup_procs)) - - for pid in pids: - if pid == daemon_pid or pid == agent_pid: - continue - else: - # There is an unexpected PID in the cgroup, check what process it is - cmd = get_cmdline_by_pid(pid) - ppid = get_parent_pid(pid) - whitelisted = is_whitelisted(cmd) - - # If the process is whitelisted and a child of the agent, allow it. The process could have terminated - # in the meantime, but we allow it if it's whitelisted. - if whitelisted and (ppid is None or ppid == agent_pid or ppid == daemon_pid): - print("\tFound whitelisted process in agent cgroup:\n\t{0} {1}\n" - "\tparent process {2}".format(pid, cmd, ppid)) - continue - - raise Exception("Found unexpected process in the agent cgroup:\n\t{0} {1}\n" - "\tparent process {2}".format(pid, cmd, ppid)) - - return True - - -def is_whitelisted(cmd): - matches = [re.match(r, cmd) is not None for r in WHITELISTED_AGENT_REGEXES] - return any(matches) - - -def parse_processes_from_systemctl_status(service_status): - processes_start_pattern = re.compile(r".*CGroup:\s+.*") - processes_end_pattern = re.compile(r"^$") - - processes_start_index = -1 - processes_end_index = -1 - - for line in service_status: - if re.match(processes_start_pattern, line): - processes_start_index = service_status.index(line) - if re.match(processes_end_pattern, line): - processes_end_index = service_status.index(line) - break - - processes_raw = service_status[processes_start_index+1:processes_end_index] - - # Remove non-ascii characters and extra whitespace - cleaned = list(map(lambda x: ''.join([i if ord(i) < 128 else '' for i in x]).strip(), processes_raw)) - - # Return a list of tuples [(PID1, cmdline1), (PID2, cmdline2)] - processes = list(map(lambda x: (x.split(" ")[0], ' '.join(x.split(" ")[1:])), cleaned)) - - return processes - - -def verify_agent_cgroup_assigned_correctly_systemd(service_status): - print_service_status(service_status) - - is_active = False - is_active_pattern = re.compile(r".*Active:\s+active.*") - - for line in service_status: - if re.match(is_active_pattern, line): - is_active = True - - if not is_active: - raise Exception('walinuxagent service was not active') - - print("\tVerified the agent service status is correct!\n") - - -def verify_agent_cgroup_assigned_correctly_filesystem(): - print("===== Verifying the daemon and the agent are assigned to the same correct cgroup using filesystem =====") - - # Find out daemon and agent PIDs by looking at currently running processes - daemon_pid = get_pid_by_cmdline(DAEMON_CMDLINE_PATTERN) - agent_pid = get_pid_by_cmdline(AGENT_CMDLINE_PATTERN) - - if daemon_pid == -1: - raise Exception('daemon PID not found!') - - if agent_pid == -1: - raise Exception('agent PID not found!') - - # Ensure both the daemon and the agent are assigned to the (same) expected cgroup - check_cgroup_for_agent_process("daemon", daemon_pid) - check_cgroup_for_agent_process("agent", agent_pid) - - # Ensure the daemon/agent cgroup doesn't have any other processes there - for controller in CONTROLLERS: - # Mount path is /system.slice/walinuxagent.service or - # /WALinuxAgent/WALinuxAgent, so remove the first "/" to correctly build path - agent_cgroup_mount_path = get_agent_cgroup_mount_path()[1:] - agent_cgroup_path = os.path.join(BASE_CGROUP, controller, agent_cgroup_mount_path) - agent_cgroup_procs = os.path.join(agent_cgroup_path, 'cgroup.procs') - - # Check if the processes in the agent cgroup are expected. We expect to see the daemon and extension handler - # processes. Sometimes, we might observe more than one extension handler process. This is short-lived and - # happens because, in Linux, the process doubles before forking. Therefore, check twice with a bit of delay - # in between to see if it goes away. Still raise an exception if this happens so we can keep track of it. - check_pids_in_agent_cgroup(agent_cgroup_procs, daemon_pid, agent_pid) - - print('\tVerified the daemon and agent are assigned to the same correct cgroup {0}'.format(agent_cgroup_path)) - print("") - - -def verify_agent_cgroup_assigned_correctly(): - if is_systemd_distro(): - print("===== Verifying the daemon and the agent are assigned to the same correct cgroup using systemd =====") - output = run_get_output("systemctl status walinuxagent") - verify_agent_cgroup_assigned_correctly_systemd(output) - else: - verify_agent_cgroup_assigned_correctly_filesystem() - diff --git a/dcr/scenario_utils/check_waagent_log.py b/dcr/scenario_utils/check_waagent_log.py deleted file mode 100644 index cfc6668bcd..0000000000 --- a/dcr/scenario_utils/check_waagent_log.py +++ /dev/null @@ -1,207 +0,0 @@ -import re - -from dcr.scenario_utils.agent_log_parser import AGENT_LOG_FILE, parse_agent_log_file -from dcr.scenario_utils.cgroups_helpers import is_systemd_distro -from dcr.scenario_utils.distro import get_distro - - -def check_waagent_log_for_errors(waagent_log=AGENT_LOG_FILE, ignore=None): - # Returns any ERROR messages from the log except transient ones. - # Currently, the only transient one is /proc/net/route not being set up if it's being reported before - # provisioning was completed. In that case, we ignore that error message. - - no_routes_error = None - provisioning_complete = False - - distro = "".join(get_distro()) - systemd_enabled = is_systemd_distro() - - # - # NOTES: - # * 'message' is matched using re.search; be sure to escape any regex metacharacters - # * 'if' receives as parameter an AgentLogRecord - # - ignore_list = [ - # This warning is expected on CentOS/RedHat 7.8 and Redhat 7.6 - { - 'message': r"Move rules file 70-persistent-net.rules to /var/lib/waagent/70-persistent-net.rules", - 'if': lambda log_line: re.match(r"((centos7\.8)|(redhat7\.8)|(redhat7\.6)|(redhat8\.2))\D*", distro, - flags=re.IGNORECASE) is not None and log_line.level == "WARNING" and - log_line.who == "ExtHandler" and log_line.thread in ("", "EnvHandler") - }, - # This warning is expected on SUSE 12 - { - 'message': r"WARNING EnvHandler ExtHandler Move rules file 75-persistent-net-generator.rules to /var/lib/waagent/75-persistent-net-generator.rules", - 'if': lambda _: re.match(r"((sles15\.2)|suse12)\D*", distro, flags=re.IGNORECASE) is not None - }, - # This warning is expected on when WireServer gives us the incomplete goalstate without roleinstance data - { - 'message': r"\[ProtocolError\] Fetched goal state without a RoleInstance", - }, - # The following message is expected to log an error if systemd is not enabled on it - { - 'message': r"Did not detect Systemd, unable to set wa(|linux)agent-network-setup.service", - 'if': lambda _: not systemd_enabled - }, - # Download warnings (manifest and zips). - # - # Examples: - # 2021-03-31T03:48:35.216494Z WARNING ExtHandler ExtHandler Fetch failed: [HttpError] [HTTP Failed] GET https://zrdfepirv2cbn04prdstr01a.blob.core.windows.net/f72653efd9e349ed9842c8b99e4c1712/Microsoft.CPlat.Core_NullSeqA_useast2euap_manifest.xml -- IOError ('The read operation timed out',) -- 1 attempts made - # 2021-03-31T06:54:29.655861Z WARNING ExtHandler ExtHandler Fetch failed: [HttpError] [HTTP Retry] GET http://168.63.129.16:32526/extensionArtifact -- Status Code 502 -- 1 attempts made - # 2021-03-31T06:43:17.806663Z WARNING ExtHandler ExtHandler Download failed, switching to host plugin - { - 'message': r"(Fetch failed: \[HttpError\] .+ GET .+ -- [0-9]+ attempts made)|(Download failed, switching to host plugin)", - 'if': lambda log_line: log_line.level == "WARNING" and log_line.who == "ExtHandler" and log_line.thread == "ExtHandler" - }, - # Sometimes it takes the Daemon some time to identify primary interface and the route to Wireserver, - # ignoring those errors if they come from the Daemon. - { - 'message': r"(No route exists to \d+\.\d+\.\d+\.\d+|" - r"Could not determine primary interface, please ensure \/proc\/net\/route is correct|" - r"Contents of \/proc\/net\/route:|Primary interface examination will retry silently|" - r"\/proc\/net\/route contains no routes)", - 'if': lambda log_line: log_line.who == "Daemon" - }, - # Journalctl in Debian 8.11 does not have the --utc option by default. - # Ignoring this error for Deb 8 as its not a blocker and since Deb 8 is old and not widely used - { - 'message': r"journalctl: unrecognized option '--utc'", - 'if': lambda log_line: re.match(r"(debian8\.11)\D*", distro, - flags=re.IGNORECASE) is not None and log_line.level == "WARNING" - }, - # 2021-07-09T01:46:53.307959Z INFO MonitorHandler ExtHandler [CGW] Disabling resource usage monitoring. Reason: Check on cgroups failed: - # [CGroupsException] The agent's cgroup includes unexpected processes: ['[PID: 2367] UNKNOWN'] - { - 'message': r"The agent's cgroup includes unexpected processes: \[('\[PID:\s?\d+\]\s*UNKNOWN'(,\s*)?)+\]" - }, - # Probably the agent should log this as INFO, but for now it is a warning - # e.g. - # 2021-07-29T04:40:17.190879Z WARNING EnvHandler ExtHandler Dhcp client is not running. - { - 'message': r"WARNING EnvHandler ExtHandler Dhcp client is not running." - }, - # 2021-12-20T07:46:23.020197Z INFO ExtHandler ExtHandler [CGW] The agent's process is not within a memory cgroup - { - 'message': r"The agent's process is not within a memory cgroup", - 'if': lambda log_line: re.match(r"((centos7\.8)|(centos7\.9)|(redhat7\.8)|(redhat8\.2))\D*", distro, - flags=re.IGNORECASE) - }, - # - # 2022-01-20T06:52:21.515447Z WARNING Daemon Daemon Fetch failed: [HttpError] [HTTP Failed] GET https://dcrgajhx62.blob.core.windows.net/$system/edprpwqbj6.5c2ddb5b-d6c3-4d73-9468-54419ca87a97.vmSettings -- IOError timed out -- 6 attempts made - # - # The daemon does not need the artifacts profile blob, but the request is done as part of protocol initialization. This timeout can be ignored, if the issue persist the log would include additional instances. - # - { - 'message': r"\[HTTP Failed\] GET https://.*\.vmSettings -- IOError timed out", - 'if': lambda log_line: log_line.level == "WARNING" and log_line.who == "Daemon" - }, - # - # 2022-02-09T04:50:37.384810Z ERROR ExtHandler ExtHandler Error fetching the goal state: [ProtocolError] GET vmSettings [correlation ID: 2bed9b62-188e-4668-b1a8-87c35cfa4927 eTag: 7031887032544600793]: [Internal error in HostGAPlugin] [HTTP Failed] [502: Bad Gateway] b'{ "errorCode": "VMArtifactsProfileBlobContentNotFound", "message": "VM artifacts profile blob has no content in it.", "details": ""}' - # - # Fetching the goal state may catch the HostGAPlugin in the process of computing the vmSettings. This can be ignored, if the issue persist the log would include additional instances. - # - { - 'message': r"\[ProtocolError\] GET vmSettings.*VMArtifactsProfileBlobContentNotFound", - 'if': lambda log_line: log_line.level == "ERROR" - }, - # - # 2021-12-29T06:50:49.904601Z ERROR ExtHandler ExtHandler Error fetching the goal state: [ProtocolError] Error fetching goal state Inner error: [ResourceGoneError] [HTTP Failed] [410: Gone] The page you requested was removed. - # 2022-03-21T02:44:03.770017Z ERROR ExtHandler ExtHandler Error fetching the goal state: [ProtocolError] Error fetching goal state Inner error: [ResourceGoneError] Resource is gone - # 2022-02-16T04:46:50.477315Z WARNING Daemon Daemon Fetching the goal state failed: [ResourceGoneError] [HTTP Failed] [410: Gone] b'\n\n ResourceNotAvailable\n The resource requested is no longer available. Please refresh your cache.\n
\n
' - # - # ResourceGone can happen if we are fetching one of the URIs in the goal state and a new goal state arrives - { - 'message': r"(?s)(Fetching the goal state failed|Error fetching goal state|Error fetching the goal state).*(\[ResourceGoneError\]|\[410: Gone\]|Resource is gone)", - 'if': lambda log_line: log_line.level in ("WARNING", "ERROR") - }, - # - # 2022-03-08T03:03:23.036161Z WARNING ExtHandler ExtHandler Fetch failed from [http://168.63.129.16:32526/extensionArtifact]: [HTTP Failed] [400: Bad Request] b'' - # 2022-03-08T03:03:23.042008Z WARNING ExtHandler ExtHandler Fetch failed: [ProtocolError] Fetch failed from [http://168.63.129.16:32526/extensionArtifact]: [HTTP Failed] [400: Bad Request] b'' - # - # Warning downloading extension manifest. If the issue persists, this would cause errors elsewhere so safe to ignore - { - 'message': r"\[http://168.63.129.16:32526/extensionArtifact\]: \[HTTP Failed\] \[400: Bad Request\]", - 'if': lambda log_line: log_line.level == "WARNING" - }, - # - # 2022-03-08T03:03:23.036161Z WARNING ExtHandler ExtHandler Fetch failed from [http://168.63.129.16:32526/extensionArtifact]: [HTTP Failed] [400: Bad Request] b'' - # 2022-03-08T03:03:23.042008Z WARNING ExtHandler ExtHandler Fetch failed: [ProtocolError] Fetch failed from [http://168.63.129.16:32526/extensionArtifact]: [HTTP Failed] [400: Bad Request] b'' - # - # Warning downloading extension manifest. If the issue persists, this would cause errors elsewhere so safe to ignore - { - 'message': r"\[http://168.63.129.16:32526/extensionArtifact\]: \[HTTP Failed\] \[400: Bad Request\]", - 'if': lambda log_line: log_line.level == "WARNING" - }, - # - # 2022-03-29T05:52:10.089958Z WARNING ExtHandler ExtHandler An error occurred while retrieving the goal state: [ProtocolError] GET vmSettings [correlation ID: da106cf5-83a0-44ec-9484-d0e9223847ab eTag: 9856274988128027586]: Timeout - # - # Ignore warnings about timeouts in vmSettings; if the condition persists, an error will occur elsewhere. - # - { - 'message': r"GET vmSettings \[[^]]+\]: Timeout", - 'if': lambda log_line: log_line.level == "WARNING" - }, - # 2022-03-09T20:04:33.745721Z ERROR ExtHandler ExtHandler Event: name=Microsoft.Azure.Monitor.AzureMonitorLinuxAgent, op=Install, message=[ExtensionOperationError] \ - # Non-zero exit code: 51, /var/lib/waagent/Microsoft.Azure.Monitor.AzureMonitorLinuxAgent-1.15.3/./shim.sh -install - # - # This is a known issue where AMA does not support Mariner 2.0. Please remove when support is - # added in the next AMA release (1.16.x). - { - 'message': r"Event: name=Microsoft.Azure.Monitor.AzureMonitorLinuxAgent, op=Install, message=\[ExtensionOperationError\] Non-zero exit code: 51", - 'if': lambda log_line: "Mariner2.0" in distro and log_line.level == "ERROR" and log_line.who == "ExtHandler" - }, - # 2022-03-18T00:13:37.063540Z INFO ExtHandler ExtHandler [CGW] The daemon's PID was added to a legacy cgroup; will not monitor resource usage. - # - # Agent disables cgroups in older versions of the daemon (2.2.31-2.2.40).This is known issue and ignoring. - { - 'message': r"The daemon's PID was added to a legacy cgroup; will not monitor resource usage" - } - ] - - if ignore is not None: - ignore_list.extend(ignore) - - def can_be_ignored(log_line): - return any(re.search(msg['message'], log_line.text) is not None and ('if' not in msg or msg['if'](log_line)) for msg in ignore_list) - - errors = [] - - for agent_log_line in parse_agent_log_file(waagent_log): - if agent_log_line.is_error and not can_be_ignored(agent_log_line): - # Handle "/proc/net/route contains no routes" as a special case since it can take time for the - # primary interface to come up and we don't want to report transient errors as actual errors - if "/proc/net/route contains no routes" in agent_log_line.text: - no_routes_error = agent_log_line.text - provisioning_complete = False - else: - errors.append(agent_log_line.text) - - if "Provisioning complete" in agent_log_line.text: - provisioning_complete = True - - # Keep the "no routes found" as a genuine error message if it was never corrected - if no_routes_error is not None and not provisioning_complete: - errors.append(no_routes_error) - - if len(errors) > 0: - # print('waagent.log contains the following ERROR(s):') - # for item in errors: - # print(item.rstrip()) - raise Exception("waagent.log contains the following ERROR(s): {0}".format('\n '.join(errors))) - - print(f"No errors/warnings found in {waagent_log}") - - -def is_data_in_waagent_log(data): - """ - This function looks for the specified test data string in the WALinuxAgent logs and returns if found or not. - :param data: The string to look for in the agent logs - :raises: Exception if data string not found - """ - for agent_log_line in parse_agent_log_file(): - if data in agent_log_line.text: - print("Found data: {0} in line: {1}".format(data, agent_log_line.text)) - return - - raise AssertionError("waagent.log file did not have the data string: {0}".format(data)) - diff --git a/dcr/scenario_utils/common_utils.py b/dcr/scenario_utils/common_utils.py deleted file mode 100644 index b1d58ab730..0000000000 --- a/dcr/scenario_utils/common_utils.py +++ /dev/null @@ -1,154 +0,0 @@ -import asyncio -import math -import os -import secrets -import subprocess -import time -from datetime import datetime -from typing import List - -from dcr.scenario_utils.distro import get_distro -from dcr.scenario_utils.logging_utils import get_logger -from dcr.scenario_utils.models import get_vm_data_from_env - -logger = get_logger("dcr.scenario_utils.common_utils") - - -def get_current_agent_name(distro_name=None): - """ - Only Ubuntu and Debian used walinuxagent, everyone else uses waagent. - Note: If distro_name is not specified, we will search the distro in the VM itself - :return: walinuxagent or waagent - """ - - if distro_name is None: - distro_name = get_distro()[0] - - walinuxagent_distros = ["ubuntu", "debian"] - if any(dist.lower() in distro_name.lower() for dist in walinuxagent_distros): - return "walinuxagent" - - return "waagent" - - -def execute_command_and_raise_on_error(command, shell=False, timeout=None, stdout=subprocess.PIPE, - stderr=subprocess.PIPE): - pipe = subprocess.Popen(command, shell=shell, stdout=stdout, stderr=stderr) - stdout, stderr = pipe.communicate(timeout=timeout) - - logger.info("STDOUT:\n{0}".format(stdout.decode())) - logger.info("STDERR:\n{0}".format(stderr.decode())) - if pipe.returncode != 0: - raise Exception("non-0 exit code: {0} for command: {1}".format(pipe.returncode, command)) - - return stdout.decode().strip(), stderr.decode().strip() - - -def execute_py_script_over_ssh_on_test_vms(command: str): - """ - Execute a python script over SSH on test VMs. If there are multiple VMs, this will execute the script on all VMs concurrently. - The script should be relative to the dcr/ directory. It uses the PyPy interpreter to execute the script and - logs the stdout/stderr of the script - raises: Exception if any script exits with non-0 exit code. - """ - ssh_cmd = f"ssh -o StrictHostKeyChecking=no {{username}}@{{ip}} sudo PYTHONPATH=. {os.environ['PYPYPATH']} /home/{{username}}/{command}" - asyncio.run(execute_commands_concurrently_on_test_vms([ssh_cmd])) - logger.info(f"Finished executing SSH command: {ssh_cmd}") - - -def random_alphanum(length: int) -> str: - if length == 0: - return '' - elif length < 0: - raise ValueError('negative argument not allowed') - else: - text = secrets.token_hex(nbytes=math.ceil(length / 2)) - is_length_even = length % 2 == 0 - return text if is_length_even else text[1:] - - -async def execute_commands_concurrently_on_test_vms(commands: List[str], timeout: int = 5): - vm_data = get_vm_data_from_env() - tasks = [ - asyncio.create_task(_execute_commands_on_vm_async(commands=commands, username=vm_data.admin_username, ip=ip_)) - for ip_ in vm_data.ips] - return await asyncio.wait_for(asyncio.gather(*tasks, return_exceptions=False), timeout=timeout * 60) - - -async def _execute_commands_on_vm_async(commands: List[str], username: str, ip: str, max_retry: int = 5): - """ - Execute the list of commands synchronously on the VM. This runs as an async operation. - The code also replaces the {username} and {ip} in the command string with their actual values before executing the command. - """ - attempt = 0 - - for command in commands: - cmd = command.format(ip=ip, username=username) - stdout, stderr = "", "" - # ToDo: Separate out retries due to network error vs retries due to test failures. - # The latter should be only once (or as specified by the test author). - # https://msazure.visualstudio.com/One/_workitems/edit/12377120 - while attempt < max_retry: - try: - proc = await asyncio.create_subprocess_shell(cmd, stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.PIPE) - stdout, stderr = await proc.communicate() - stdout = stdout.decode('utf-8') - stderr = stderr.decode('utf-8') - if proc.returncode != 0: - raise Exception(f"Failed command: {cmd}. Exit Code: {proc.returncode}") - break - - except asyncio.CancelledError as err: - logger.warning(f"Task was cancelled: {cmd}; {err}") - try: - proc.terminate() - except: - # Eat all exceptions when trying to terminate a process that has been Cancelled - pass - finally: - return - - except Exception as err: - attempt += 1 - if attempt < max_retry: - logger.warning(f"[{username}/{ip}] ({attempt}/{max_retry}) Failed to execute command {cmd}: {err}. Retrying in 3 secs", - exc_info=True) - await asyncio.sleep(3) - else: - raise - - finally: - print(f"##[group][{username}/{ip}] - Attempts ({attempt}/{max_retry})") - print(f"##[command]{cmd}") - if stdout: - logger.info(f"Stdout: {stdout}") - if stderr: - logger.warning(f"Stderr: {stderr}") - print("##[endgroup]") - - -def execute_with_retry(func, max_retry=3, sleep=5): - retry = 0 - while retry < max_retry: - try: - func() - return - except Exception as error: - print("{0} Op failed with error: {1}. Retry: {2}, total attempts: {3}".format(datetime.utcnow().isoformat(), - error, retry + 1, max_retry)) - retry += 1 - if retry < max_retry: - time.sleep(sleep) - continue - raise - - -def read_file(log_file): - if not os.path.exists(log_file): - raise Exception("{0} file not found!".format(log_file)) - - with open(log_file) as f: - lines = list(map(lambda _: _.strip(), f.readlines())) - - return lines diff --git a/dcr/scenario_utils/crypto.py b/dcr/scenario_utils/crypto.py deleted file mode 100644 index f7555be0c4..0000000000 --- a/dcr/scenario_utils/crypto.py +++ /dev/null @@ -1,60 +0,0 @@ -import os - -from cryptography.hazmat.primitives import serialization -from cryptography.hazmat.primitives.asymmetric import rsa -from cryptography.hazmat.backends import default_backend - -from dcr.scenario_utils.common_utils import random_alphanum - - -class OpenSshKey(object): - """ - Represents an OpenSSH key pair. - """ - - def __init__(self, public_key: bytes, private_key: bytes): - self._private_key = private_key - self._public_key = public_key - - @property - def private_key(self) -> bytes: - return self._private_key - - @property - def public_key(self) -> bytes: - return self._public_key - - -class OpenSshKeyFactory(object): - @staticmethod - def create() -> OpenSshKey: - key = rsa.generate_private_key( - public_exponent=65537, - key_size=2048, - backend=default_backend() - ) - - public_key = key.public_key().public_bytes( - serialization.Encoding.OpenSSH, - serialization.PublicFormat.OpenSSH - ) - - private_key = key.private_bytes( - serialization.Encoding.PEM, - serialization.PrivateFormat.PKCS8, - serialization.NoEncryption() - ) - - return OpenSshKey(public_key, private_key) - - -def generate_ssh_key_pair(key_prefix='dcr_id_rsa'): - # New SSH public/private keys - ssh_keys = OpenSshKeyFactory().create() - - private_key_file_name = '{0}_{1}'.format(key_prefix, random_alphanum(10)) - with open(private_key_file_name, 'wb') as fh: - fh.write(ssh_keys.private_key) - private_key_file = os.path.abspath(private_key_file_name) - - return ssh_keys.public_key.decode('utf-8'), private_key_file \ No newline at end of file diff --git a/dcr/scenario_utils/distro.py b/dcr/scenario_utils/distro.py deleted file mode 100644 index eaa687b589..0000000000 --- a/dcr/scenario_utils/distro.py +++ /dev/null @@ -1,40 +0,0 @@ -import platform -import sys - -import distro - - -def get_distro(): - """ - In some distros, e.g. SUSE 15, platform.linux_distribution is present, - but returns an empty value - so we also try distro.linux_distribution in those cases - """ - osinfo = [] - if hasattr(platform, 'linux_distribution'): - osinfo = list(platform.linux_distribution( - full_distribution_name=0, - supported_dists=platform._supported_dists + ('alpine',))) - - # Remove trailing whitespace and quote in distro name - - osinfo[0] = osinfo[0].strip('"').strip(' ').lower() - if not osinfo or not len(osinfo[0]): - # platform.linux_distribution() is deprecated, the suggested option is to use distro module - osinfo = distro.linux_distribution() - - return osinfo - - -def print_distro_info(): - print('\n--== distro ==--') - distro_name = get_distro() - - print('DISTRO_NAME = {0}'.format(distro_name[0])) - print('DISTRO_VERSION = {0}'.format(distro_name[1])) - print('DISTRO_CODE_NAME = {0}'.format(distro_name[2])) - - print('PY_VERSION = {0}'.format(sys.version_info)) - print('PY_VERSION_MAJOR = {0}'.format(sys.version_info[0])) - print('PY_VERSION_MINOR = {0}'.format(sys.version_info[1])) - print('PY_VERSION_MICRO = {0}'.format(sys.version_info[2])) diff --git a/dcr/scenario_utils/extensions/BaseExtensionTestClass.py b/dcr/scenario_utils/extensions/BaseExtensionTestClass.py deleted file mode 100644 index 8c23e1e712..0000000000 --- a/dcr/scenario_utils/extensions/BaseExtensionTestClass.py +++ /dev/null @@ -1,113 +0,0 @@ -import time -from typing import List - -from azure.core.polling import LROPoller - -from dcr.scenario_utils.azure_models import ComputeManager -from dcr.scenario_utils.logging_utils import LoggingHandler -from dcr.scenario_utils.models import ExtensionMetaData, get_vm_data_from_env - - -class BaseExtensionTestClass(LoggingHandler): - - def __init__(self, extension_data: ExtensionMetaData): - super().__init__() - self.__extension_data = extension_data - self.__vm_data = get_vm_data_from_env() - self.__compute_manager = ComputeManager().compute_manager - - def get_ext_props(self, settings=None, protected_settings=None, auto_upgrade_minor_version=True, - force_update_tag=None): - - return self.__compute_manager.get_ext_props( - extension_data=self.__extension_data, - settings=settings, - protected_settings=protected_settings, - auto_upgrade_minor_version=auto_upgrade_minor_version, - force_update_tag=force_update_tag - ) - - def run(self, ext_props: List, remove: bool = True, continue_on_error: bool = False): - - def __add_extension(): - extension: LROPoller = self.__compute_manager.extension_func.begin_create_or_update( - self.__vm_data.rg_name, - self.__vm_data.name, - self.__extension_data.name, - ext_prop - ) - self.log.info("Add extension: {0}".format(extension.result(timeout=5 * 60))) - - def __remove_extension(): - self.__compute_manager.extension_func.begin_delete( - self.__vm_data.rg_name, - self.__vm_data.name, - self.__extension_data.name - ).result() - self.log.info(f"Delete vm extension {self.__extension_data.name} successful") - - def _retry_on_retryable_error(func): - retry = 1 - while retry < 5: - try: - func() - break - except Exception as err_: - if "RetryableError" in str(err_) and retry < 5: - self.log.warning(f"({retry}/5) Ran into RetryableError, retrying in 30 secs: {err_}") - time.sleep(30) - retry += 1 - continue - raise - - try: - for ext_prop in ext_props: - try: - _retry_on_retryable_error(__add_extension) - # Validate success from instance view - _retry_on_retryable_error(self.validate_ext) - except Exception as err: - if continue_on_error: - self.log.exception("Ran into error but ignoring it as asked: {0}".format(err)) - continue - else: - raise - finally: - # Always try to delete extensions if asked to remove even on errors - if remove: - _retry_on_retryable_error(__remove_extension) - - def validate_ext(self): - """ - Validate if the extension operation was successful from the Instance View - :raises: Exception if either unable to fetch instance view or if extension not successful - """ - retry = 0 - max_retry = 3 - ext_instance_view = None - status = None - - while retry < max_retry: - try: - ext_instance_view = self.__compute_manager.get_extension_instance_view(self.__extension_data.name) - if ext_instance_view is None: - raise Exception("Extension not found") - elif not ext_instance_view.instance_view: - raise Exception("Instance view not present") - elif not ext_instance_view.instance_view.statuses or len(ext_instance_view.instance_view.statuses) < 1: - raise Exception("Instance view status not present") - else: - status = ext_instance_view.instance_view.statuses[0].code - status_message = ext_instance_view.instance_view.statuses[0].message - self.log.info('Extension Status: \n\tCode: [{0}]\n\tMessage: {1}'.format(status, status_message)) - break - except Exception as err: - self.log.exception(f"Ran into error: {err}") - retry += 1 - if retry < max_retry: - self.log.info("Retrying in 30 secs") - time.sleep(30) - raise - - if 'succeeded' not in status: - raise Exception(f"Extension did not succeed. Last Instance view: {ext_instance_view}") diff --git a/dcr/scenario_utils/extensions/CustomScriptExtension.py b/dcr/scenario_utils/extensions/CustomScriptExtension.py deleted file mode 100644 index 29df351134..0000000000 --- a/dcr/scenario_utils/extensions/CustomScriptExtension.py +++ /dev/null @@ -1,29 +0,0 @@ -import uuid - -from dcr.scenario_utils.extensions.BaseExtensionTestClass import BaseExtensionTestClass -from dcr.scenario_utils.models import ExtensionMetaData - - -class CustomScriptExtension(BaseExtensionTestClass): - META_DATA = ExtensionMetaData( - publisher='Microsoft.Azure.Extensions', - ext_type='CustomScript', - version="2.1" - ) - - def __init__(self, extension_name: str): - extension_data = self.META_DATA - extension_data.name = extension_name - super().__init__(extension_data) - - -def add_cse(): - # Install and remove CSE - cse = CustomScriptExtension(extension_name="testCSE") - - ext_props = [ - cse.get_ext_props(settings={'commandToExecute': f"echo \'Hello World! {uuid.uuid4()} \'"}), - cse.get_ext_props(settings={'commandToExecute': "echo \'Hello again\'"}) - ] - - cse.run(ext_props=ext_props) \ No newline at end of file diff --git a/dcr/scenario_utils/extensions/GATestExtGoExtension.py b/dcr/scenario_utils/extensions/GATestExtGoExtension.py deleted file mode 100644 index 39eb144fd0..0000000000 --- a/dcr/scenario_utils/extensions/GATestExtGoExtension.py +++ /dev/null @@ -1,26 +0,0 @@ -from typing import List - -from azure.mgmt.compute.models import VirtualMachineExtension - -from dcr.scenario_utils.extensions.BaseExtensionTestClass import BaseExtensionTestClass -from dcr.scenario_utils.models import ExtensionMetaData - - -class GATestExtGoExtension(BaseExtensionTestClass): - def __init__(self, extension_name: str): - extension_data = ExtensionMetaData( - publisher='Microsoft.Azure.Extensions.Edp', - ext_type='GATestExtGo', - version="1.0", - ext_name=extension_name - ) - super().__init__(extension_data) - - def run(self, ext_props: List[VirtualMachineExtension], remove: bool = True, continue_on_error: bool = False): - for ext_prop in ext_props: - if 'name' not in ext_prop.settings: - # GATestExtGo expects name to always be there, making sure we send it always - ext_prop.settings['name'] = "Enabling GA Test Extension" - - super().run(ext_props, remove, continue_on_error) - diff --git a/dcr/scenario_utils/extensions/RunCommandExtension.py b/dcr/scenario_utils/extensions/RunCommandExtension.py deleted file mode 100644 index 0a059bd391..0000000000 --- a/dcr/scenario_utils/extensions/RunCommandExtension.py +++ /dev/null @@ -1,27 +0,0 @@ -import uuid - -from dcr.scenario_utils.extensions.BaseExtensionTestClass import BaseExtensionTestClass -from dcr.scenario_utils.models import ExtensionMetaData - - -class RunCommandExtension(BaseExtensionTestClass): - def __init__(self, extension_name: str): - extension_data = ExtensionMetaData( - publisher='Microsoft.CPlat.Core', - ext_type='RunCommandLinux', - version="1.0", - ext_name=extension_name - ) - super().__init__(extension_data) - - -def add_rc(): - # Install and remove RC - rc = RunCommandExtension(extension_name="testRC") - - ext_props = [ - rc.get_ext_props(settings={'commandToExecute': f"echo \'Hello World! {uuid.uuid4()} \'"}), - rc.get_ext_props(settings={'commandToExecute': "echo \'Hello again\'"}) - ] - - rc.run(ext_props=ext_props) diff --git a/dcr/scenario_utils/extensions/VMAccessExtension.py b/dcr/scenario_utils/extensions/VMAccessExtension.py deleted file mode 100644 index c84ae12053..0000000000 --- a/dcr/scenario_utils/extensions/VMAccessExtension.py +++ /dev/null @@ -1,38 +0,0 @@ -import asyncio -import os - -from dcr.scenario_utils.common_utils import random_alphanum, execute_commands_concurrently_on_test_vms -from dcr.scenario_utils.crypto import generate_ssh_key_pair -from dcr.scenario_utils.extensions.BaseExtensionTestClass import BaseExtensionTestClass -from dcr.scenario_utils.models import ExtensionMetaData - - -class VMAccessExtension(BaseExtensionTestClass): - META_DATA = ExtensionMetaData( - publisher='Microsoft.OSTCExtensions', - ext_type='VMAccessForLinux', - version="1.5" - ) - - def __init__(self, extension_name: str): - extension_data = self.META_DATA - extension_data.name = extension_name - super().__init__(extension_data) - self.public_key, self.private_key_file = generate_ssh_key_pair('dcr_py') - self.user_name = f'dcr{random_alphanum(length=8)}' - - def verify(self): - os.chmod(self.private_key_file, 0o600) - ssh_cmd = f'ssh -o StrictHostKeyChecking=no -i {self.private_key_file} {self.user_name}@{{ip}} ' \ - f'"echo script was executed successfully on remote vm"' - print(asyncio.run(execute_commands_concurrently_on_test_vms([ssh_cmd]))) - - -def add_and_verify_vmaccess(): - vmaccess = VMAccessExtension(extension_name="testVmAccessExt") - ext_props = [ - vmaccess.get_ext_props(protected_settings={'username': vmaccess.user_name, 'ssh_key': vmaccess.public_key, - 'reset_ssh': 'false'}) - ] - vmaccess.run(ext_props=ext_props) - vmaccess.verify() diff --git a/dcr/scenario_utils/extensions/__init__.py b/dcr/scenario_utils/extensions/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/dcr/scenario_utils/logging_utils.py b/dcr/scenario_utils/logging_utils.py deleted file mode 100644 index 462f6a957a..0000000000 --- a/dcr/scenario_utils/logging_utils.py +++ /dev/null @@ -1,33 +0,0 @@ -# Create a base class -import logging - - -def get_logger(name): - return LoggingHandler(name).log - - -class LoggingHandler: - """ - Base class for Logging - """ - def __init__(self, name=None): - self.log = self.__setup_and_get_logger(name) - - def __setup_and_get_logger(self, name): - logger = logging.getLogger(name if name is not None else self.__class__.__name__) - if logger.hasHandlers(): - # Logging module inherits from base loggers if already setup, if a base logger found, reuse that - return logger - - # No handlers found for logger, set it up - # This logging format is easier to read on the DevOps UI - - # https://docs.microsoft.com/en-us/azure/devops/pipelines/scripts/logging-commands?view=azure-devops&tabs=bash#formatting-commands - log_formatter = logging.Formatter("##[%(levelname)s] [%(asctime)s] [%(module)s] {%(pathname)s:%(lineno)d} %(message)s", - datefmt="%Y-%m-%dT%H:%M:%S%z") - console_handler = logging.StreamHandler() - console_handler.setFormatter(log_formatter) - logger.addHandler(console_handler) - logger.setLevel(logging.INFO) - - return logger - diff --git a/dcr/scenario_utils/models.py b/dcr/scenario_utils/models.py deleted file mode 100644 index 806c830c12..0000000000 --- a/dcr/scenario_utils/models.py +++ /dev/null @@ -1,137 +0,0 @@ -import os -from enum import Enum, auto -from typing import List - -from dotenv import load_dotenv - - -class VMModelType(Enum): - VM = auto() - VMSS = auto() - - -class ExtensionMetaData: - def __init__(self, publisher: str, ext_type: str, version: str, ext_name: str = ""): - self.__publisher = publisher - self.__ext_type = ext_type - self.__version = version - self.__ext_name = ext_name - - @property - def publisher(self) -> str: - return self.__publisher - - @property - def ext_type(self) -> str: - return self.__ext_type - - @property - def version(self) -> str: - return self.__version - - @property - def name(self): - return self.__ext_name - - @name.setter - def name(self, ext_name): - self.__ext_name = ext_name - - @property - def handler_name(self): - return f"{self.publisher}.{self.ext_type}" - - -class VMMetaData: - - def __init__(self, vm_name: str, rg_name: str, sub_id: str, location: str, admin_username: str, - ips: List[str] = None): - self.__vm_name = vm_name - self.__rg_name = rg_name - self.__sub_id = sub_id - self.__location = location - self.__admin_username = admin_username - - vm_ips, vmss_ips = _get_ips(admin_username) - # By default assume the test is running on a VM - self.__type = VMModelType.VM - self.__ips = vm_ips - if any(vmss_ips): - self.__type = VMModelType.VMSS - self.__ips = vmss_ips - - if ips is not None: - self.__ips = ips - - print(f"IPs: {self.__ips}") - - @property - def name(self) -> str: - return self.__vm_name - - @property - def rg_name(self) -> str: - return self.__rg_name - - @property - def location(self) -> str: - return self.__location - - @property - def sub_id(self) -> str: - return self.__sub_id - - @property - def admin_username(self): - return self.__admin_username - - @property - def ips(self) -> List[str]: - return self.__ips - - @property - def model_type(self): - return self.__type - - -def _get_ips(username) -> (list, list): - """ - Try fetching Ips from the files that we create via az-cli. - We do a best effort to fetch this from both orchestrator or the test VM. Its located in different locations on both - scenarios. - Returns: Tuple of (VmIps, VMSSIps). - """ - - vms, vmss = [], [] - orchestrator_path = os.path.join(os.environ['BUILD_SOURCESDIRECTORY'], "dcr") - test_vm_path = os.path.join("/home", username, "dcr") - - for ip_path in [orchestrator_path, test_vm_path]: - - vm_ip_path = os.path.join(ip_path, ".vm_ips") - if os.path.exists(vm_ip_path): - with open(vm_ip_path, 'r') as vm_ips: - vms.extend(ip.strip() for ip in vm_ips.readlines()) - - vmss_ip_path = os.path.join(ip_path, ".vmss_ips") - if os.path.exists(vmss_ip_path): - with open(vmss_ip_path, 'r') as vmss_ips: - vmss.extend(ip.strip() for ip in vmss_ips.readlines()) - - return vms, vmss - - -def get_vm_data_from_env() -> VMMetaData: - if get_vm_data_from_env.__instance is None: - load_dotenv() - get_vm_data_from_env.__instance = VMMetaData(vm_name=os.environ["VMNAME"], - rg_name=os.environ['RGNAME'], - sub_id=os.environ["SUBID"], - location=os.environ['LOCATION'], - admin_username=os.environ['ADMINUSERNAME']) - - return get_vm_data_from_env.__instance - - -get_vm_data_from_env.__instance = None - diff --git a/dcr/scenario_utils/test_orchestrator.py b/dcr/scenario_utils/test_orchestrator.py deleted file mode 100644 index 014531164c..0000000000 --- a/dcr/scenario_utils/test_orchestrator.py +++ /dev/null @@ -1,97 +0,0 @@ -import os -import time -import traceback -from typing import List - -from dotenv import load_dotenv -from junitparser import TestCase, Skipped, Failure, TestSuite, JUnitXml - -from dcr.scenario_utils.logging_utils import LoggingHandler -from dcr.scenario_utils.models import get_vm_data_from_env - - -class TestFuncObj: - def __init__(self, test_name, test_func, raise_on_error=False, retry=1): - self.name = test_name - self.func = test_func - self.raise_on_error = raise_on_error - self.retry = retry - - -class TestOrchestrator(LoggingHandler): - def __init__(self, name: str, tests: List[TestFuncObj]): - super().__init__() - self.name = name - self.__tests: List[TestFuncObj] = tests - self.__test_suite = TestSuite(name) - - def run_tests(self): - load_dotenv() - skip_due_to = None - for test in self.__tests: - tc = TestCase(test.name, classname=os.environ['SCENARIONAME']) - if skip_due_to is not None: - tc.result = [Skipped(message=f"Skipped due to failing test: {skip_due_to}")] - else: - attempt = 1 - while attempt <= test.retry: - print(f"##[group][{test.name}] - Attempts ({attempt}/{test.retry})") - tc = self.run_test_and_get_tc(test.name, test.func) - if isinstance(tc.result, Failure): - attempt += 1 - if attempt > test.retry and test.raise_on_error: - self.log.warning(f"Breaking test case failed: {test.name}; Skipping remaining tests") - skip_due_to = test.name - else: - self.log.warning(f"(Attempt {attempt-1}/Total {test.retry}) Test {test.name} failed") - if attempt <= test.retry: - self.log.warning("retrying in 10 secs") - time.sleep(10) - print("##[endgroup]") - else: - print("##[endgroup]") - break - self.__test_suite.add_testcase(tc) - - def __generate_report(self, test_file_path): - xml_junit = JUnitXml() - xml_junit.add_testsuite(self.__test_suite) - xml_junit.write(filepath=test_file_path, pretty=True) - - def generate_report_on_orchestrator(self, file_name: str): - """ - Use this function to generate Junit XML report on the orchestrator. - The report is dropped in `$(Build.ArtifactStagingDirectory)/harvest` directory - """ - assert file_name.startswith("test-result"), "File name is invalid, it should start with test-result*" - self.__generate_report(os.path.join(os.environ['BUILD_ARTIFACTSTAGINGDIRECTORY'], file_name)) - - def generate_report_on_vm(self, file_name): - """ - Use this function to generate Junit XML report on the Test VM. - The report is dropped in `/home/$(adminUsername)/` directory - """ - assert file_name.startswith("test-result"), "File name is invalid, it should start with test-result*" - admin_username = get_vm_data_from_env().admin_username - self.__generate_report(os.path.join("/home", admin_username, file_name)) - - @property - def failed(self) -> bool: - return (self.__test_suite.failures + self.__test_suite.errors) > 0 - - def run_test_and_get_tc(self, test_name, test_func) -> TestCase: - tc = TestCase(test_name, classname=os.environ['SCENARIONAME']) - start_time = time.time() - self.log.info("Execute Test: {0}".format(test_name)) - try: - stdout = test_func() - self.log.debug("[{0}] Debug Output: {1}".format(test_name, stdout)) - except Exception as err: - self.log.exception("Error: {1}".format(test_name, err)) - stdout = str(err) - tc.result = [Failure(f"Failure: {err}", type_=f"Stack: {traceback.format_exc()}")] - - tc.system_out = stdout - tc.time = (time.time() - start_time) - return tc - diff --git a/dcr/scenarios/__init__.py b/dcr/scenarios/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/dcr/scenarios/agent-bvt/__init__.py b/dcr/scenarios/agent-bvt/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/dcr/scenarios/agent-bvt/check_extension_timing.py b/dcr/scenarios/agent-bvt/check_extension_timing.py deleted file mode 100644 index 97c6b61af2..0000000000 --- a/dcr/scenarios/agent-bvt/check_extension_timing.py +++ /dev/null @@ -1,55 +0,0 @@ -from __future__ import print_function - -import re - -from dcr.scenario_utils.agent_log_parser import parse_agent_log_file, GOAL_STATE_COMPLETED - -extension_name_pattern = r'\[(\S*)\]' - -# 2018/05/22 21:23:32.888949 INFO [Microsoft.EnterpriseCloud.Monitoring.OmsAgentForLinux-1.6.42.0] Target handler state: enabled -handle_extensions_starting_pattern = r'Target handler state:\s(\S*)' - -extension_cycle = {0: '', 1: ''} - -cycle_completed = False - - -def __update_cycle(pos, name, when, info): - global extension_cycle - global cycle_completed - - extension_cycle[pos] = '@trace {0} {1} [{2}]'.format(when, name, info) - - for i in range(pos+1, 2): - extension_cycle[i] = '' - - if all(i != '' for i in extension_cycle.values()): - for key in extension_cycle.keys(): - print(extension_cycle[key]) - extension_cycle = {} - cycle_completed = True - - -def verify_extension_timing(): - for agent_log_line in parse_agent_log_file(): - match = re.match(handle_extensions_starting_pattern, agent_log_line.message) - if match: - op = match.groups()[0] - match = re.match(extension_name_pattern, agent_log_line.who) - ext_name = match.groups()[0] if match else "invalid.extension.name.syntax" - trans_op = "add/update" if op == "enabled" else "remove" - info = "{0}: {1}".format(ext_name, trans_op) - __update_cycle(0, 'handle_extension_started', agent_log_line.when, info) - continue - - match = re.match(GOAL_STATE_COMPLETED, agent_log_line.message) - if match: - duration = match.group('duration') - __update_cycle(1, 'handle_extension_duration', agent_log_line.when, duration) - - if not cycle_completed: - raise Exception('full cycle not completed') - - return "Extension cycle complete" - - diff --git a/dcr/scenarios/agent-bvt/check_firewall.py b/dcr/scenarios/agent-bvt/check_firewall.py deleted file mode 100644 index df7261a7f6..0000000000 --- a/dcr/scenarios/agent-bvt/check_firewall.py +++ /dev/null @@ -1,64 +0,0 @@ -import os -import pwd -import re -import subprocess -import sys - -if sys.version_info[0] == 3: - import http.client as httpclient -elif sys.version_info[0] == 2: - import httplib as httpclient - -WIRESERVER_ENDPOINT_FILE = '/var/lib/waagent/WireServerEndpoint' -VERSIONS_PATH = '/?comp=versions' - -AGENT_CONFIG_FILE = '/etc/waagent.conf' -OS_ENABLE_FIREWALL_RX = r'OS.EnableFirewall\s*=\s*(\S+)' - - -def __is_firewall_enabled(): - with open(AGENT_CONFIG_FILE, 'r') as config_fh: - for line in config_fh.readlines(): - if not line.startswith('#'): - update_match = re.match(OS_ENABLE_FIREWALL_RX, line, re.IGNORECASE) - if update_match: - return update_match.groups()[0].lower() == 'y' - - # The firewall is disabled by default. - return False - - -def run(*args): - p = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE) - rc = p.wait() - if rc != 0: - return False, None - else: - o = list(map(lambda s: s.decode('utf-8').strip(), p.stdout.read())) - return True, o - - -def check_firewall(username): - if not __is_firewall_enabled(): - return "The firewall is not enabled, skipping checks" - - with open(WIRESERVER_ENDPOINT_FILE, 'r') as f: - wireserver_ip = f.read() - - uid = pwd.getpwnam(username)[2] - os.seteuid(uid) - - client = httpclient.HTTPConnection(wireserver_ip, timeout=1) - - try: - client.request('GET', VERSIONS_PATH) - success = True - except Exception as err: - print(err) - success = False - - if success: - raise Exception("Error -- user could connect to wireserver") - - return "Success -- user access to wireserver is blocked" - diff --git a/dcr/scenarios/agent-bvt/run.host.py b/dcr/scenarios/agent-bvt/run.host.py deleted file mode 100644 index c5ce45883d..0000000000 --- a/dcr/scenarios/agent-bvt/run.host.py +++ /dev/null @@ -1,23 +0,0 @@ -from dcr.scenario_utils.common_utils import execute_py_script_over_ssh_on_test_vms -from dcr.scenario_utils.extensions.CustomScriptExtension import add_cse -from dcr.scenario_utils.extensions.VMAccessExtension import add_and_verify_vmaccess -from dcr.scenario_utils.test_orchestrator import TestOrchestrator, TestFuncObj - -if __name__ == '__main__': - # Execute run1.py first - execute_py_script_over_ssh_on_test_vms(command="dcr/scenarios/agent-bvt/run1.py") - - # Add extensions from the Host - tests = [ - TestFuncObj("Add Cse", add_cse, raise_on_error=True), - TestFuncObj("Add VMAccess", add_and_verify_vmaccess, raise_on_error=True) - ] - - test_orchestrator = TestOrchestrator("AgentBVT-Host", tests=tests) - test_orchestrator.run_tests() - test_orchestrator.generate_report_on_orchestrator("test-results-bvt-host.xml") - assert not test_orchestrator.failed, f"Test Suite: {test_orchestrator.name} failed" - - # Execute run2.py finally - execute_py_script_over_ssh_on_test_vms(command="dcr/scenarios/agent-bvt/run2.py") - diff --git a/dcr/scenarios/agent-bvt/run1.py b/dcr/scenarios/agent-bvt/run1.py deleted file mode 100644 index 7c17b0ce9f..0000000000 --- a/dcr/scenarios/agent-bvt/run1.py +++ /dev/null @@ -1,16 +0,0 @@ -from dcr.scenario_utils.test_orchestrator import TestFuncObj, TestOrchestrator -from test_agent_basics import test_agent_version, check_hostname, check_ns_lookup, check_root_login - -if __name__ == '__main__': - tests = [ - TestFuncObj("check_agent_version", test_agent_version), - TestFuncObj("Check hostname", check_hostname), - TestFuncObj("Check NSLookup", check_ns_lookup), - TestFuncObj("Check Root Login", check_root_login) - ] - - test_orchestrator = TestOrchestrator("AgentBVTs-VM", tests=tests) - test_orchestrator.run_tests() - test_orchestrator.generate_report_on_vm("test-result-bvt-run1.xml") - assert not test_orchestrator.failed, f"Test Suite: {test_orchestrator.name} failed" - diff --git a/dcr/scenarios/agent-bvt/run2.py b/dcr/scenarios/agent-bvt/run2.py deleted file mode 100644 index a563b62c3a..0000000000 --- a/dcr/scenarios/agent-bvt/run2.py +++ /dev/null @@ -1,21 +0,0 @@ -from check_extension_timing import verify_extension_timing -from check_firewall import check_firewall -from dcr.scenario_utils.check_waagent_log import check_waagent_log_for_errors -from dcr.scenario_utils.models import get_vm_data_from_env -from dcr.scenario_utils.test_orchestrator import TestFuncObj, TestOrchestrator -from test_agent_basics import check_agent_processes, check_sudoers - -if __name__ == '__main__': - admin_username = get_vm_data_from_env().admin_username - tests = [ - TestFuncObj("check agent processes", check_agent_processes), - TestFuncObj("check agent log", check_waagent_log_for_errors), - TestFuncObj("verify extension timing", verify_extension_timing), - TestFuncObj("Check Firewall", lambda: check_firewall(admin_username)), - TestFuncObj("Check Sudoers", lambda: check_sudoers(admin_username)) - ] - - test_orchestrator = TestOrchestrator("AgentBVTs-VM", tests=tests) - test_orchestrator.run_tests() - test_orchestrator.generate_report_on_vm("test-result-bvt-run2.xml") - assert not test_orchestrator.failed, f"Test Suite: {test_orchestrator.name} failed" diff --git a/dcr/scenarios/agent-bvt/test_agent_basics.py b/dcr/scenarios/agent-bvt/test_agent_basics.py deleted file mode 100644 index b8c9483c89..0000000000 --- a/dcr/scenarios/agent-bvt/test_agent_basics.py +++ /dev/null @@ -1,103 +0,0 @@ -import os -import re -import socket - -from dotenv import load_dotenv - -from dcr.scenario_utils.common_utils import execute_command_and_raise_on_error -from dcr.scenario_utils.models import get_vm_data_from_env - - -def test_agent_version(): - stdout, _ = execute_command_and_raise_on_error(['waagent', '-version'], timeout=30) - - # release_file contains: - # AGENT_VERSION = 'x.y.z' - load_dotenv() - expected_version = os.environ.get("AGENTVERSION") - - if "Goal state agent: {0}".format(expected_version) not in stdout: - raise Exception("expected version {0} not found".format(expected_version)) - - return stdout - - -def check_hostname(): - vm_name = get_vm_data_from_env().name - stdout, _ = execute_command_and_raise_on_error(['hostname'], timeout=30) - - if vm_name.lower() != stdout.lower(): - raise Exception("Hostname does not match! Expected: {0}, found: {1}".format(vm_name, stdout.strip())) - - return stdout - - -def check_ns_lookup(): - hostname, _ = execute_command_and_raise_on_error(['hostname'], timeout=30) - - ip = socket.gethostbyname(hostname) - msg = "Resolved IP: {0}".format(ip) - print(msg) - - return msg - - -def check_root_login(): - stdout, _ = execute_command_and_raise_on_error(['cat', '/etc/shadow'], timeout=30) - root_passwd_line = next(line for line in stdout.splitlines() if 'root' in line) - print(root_passwd_line) - root_passwd = root_passwd_line.split(":")[1] - - if any(val in root_passwd for val in ("!", "*", "x")): - return 'root login disabled' - else: - raise Exception('root login appears to be enabled: {0}'.format(root_passwd)) - - -def check_agent_processes(): - daemon_pattern = r'.*python.*waagent -daemon$' - handler_pattern = r'.*python.*-run-exthandlers' - status_pattern = r'^(\S+)\s+' - - std_out, _ = execute_command_and_raise_on_error(['ps', 'axo', 'stat,args'], timeout=30) - - daemon = False - ext_handler = False - agent_processes = [line for line in std_out.splitlines() if 'python' in line] - - for process in agent_processes: - if re.match(daemon_pattern, process): - daemon = True - elif re.match(handler_pattern, process): - ext_handler = True - else: - continue - - status = re.match(status_pattern, process).groups(1)[0] - if not(status.startswith('S') or status.startswith('R')): - raise Exception('process is not running: {0}'.format(process)) - - if not daemon: - raise Exception('daemon process not found:\n\n{0}'.format(std_out)) - if not ext_handler: - raise Exception('extension handler process not found:\n\n{0}'.format(std_out)) - - return 'expected processes found running' - - -def check_sudoers(user): - found = False - root = '/etc/sudoers.d/' - - for f in os.listdir(root): - sudoers = os.path.join(root, f) - with open(sudoers) as fh: - for entry in fh.readlines(): - if entry.startswith(user) and 'ALL=(ALL)' in entry: - print('entry found: {0}'.format(entry)) - found = True - - if not found: - raise Exception('user {0} not found'.format(user)) - - return "Found user {0} in list of sudoers".format(user) diff --git a/dcr/scenarios/agent-persist-firewall/access_wire_ip.sh b/dcr/scenarios/agent-persist-firewall/access_wire_ip.sh deleted file mode 100644 index 40d99ee9f2..0000000000 --- a/dcr/scenarios/agent-persist-firewall/access_wire_ip.sh +++ /dev/null @@ -1,66 +0,0 @@ -#!/bin/bash - -# Helper file which tries to access Wireserver on system reboot. Also prints out iptable rules if non-root and still -# able to access Wireserver - -# Args: 0 1 -# Usage ./access_wire_ip.sh - -USER=$(whoami) -echo "$(date --utc +%FT%T.%3NZ): Running as user: $USER" - -function check_online -{ - ping 8.8.8.8 -c 1 -i .2 -t 30 > /dev/null 2>&1 && echo 0 || echo 1 -} - -# Check more, sleep less -MAX_CHECKS=10 -# Initial starting value for checks -CHECKS=0 -IS_ONLINE=$(check_online) - -# Loop while we're not online. -while [ "$IS_ONLINE" -eq 1 ]; do - - CHECKS=$((CHECKS + 1)) - if [ $CHECKS -gt $MAX_CHECKS ]; then - break - fi - - echo "$(date --utc +%FT%T.%3NZ): Network still not accessible" - # We're offline. Sleep for a bit, then check again - sleep 1; - IS_ONLINE=$(check_online) - -done - -if [ "$IS_ONLINE" -eq 1 ]; then - # We will never be able to get online. Kill script. - echo "Unable to connect to network, exiting now" - echo "ExitCode: 1" - exit 1 -fi - -echo "Finally online, Time: $(date --utc +%FT%T.%3NZ)" -echo "Trying to contact Wireserver as $USER to see if accessible" - -echo "" -echo "IPTables before accessing Wireserver" -sudo "$1" -t security -L -nxv -echo "" - -file_name="/var/tmp/wire-versions-root.xml" -if [[ "$USER" != "root" ]]; then - file_name="/var/tmp/wire-versions-non-root.xml" -fi - -WIRE_IP=$(cat /var/lib/waagent/WireServerEndpoint 2>/dev/null || echo '168.63.129.16' | tr -d '[:space:]') -wget --tries=3 "http://$WIRE_IP/?comp=versions" --timeout=5 -O "$file_name" -WIRE_EC=$? -echo "ExitCode: $WIRE_EC" - -if [[ "$USER" != "root" && "$WIRE_EC" == 0 ]]; then - echo "Wireserver should not be accessible for non-root user ($USER), IPTable rules -" - sudo "$1" -t security -L -nxv -fi \ No newline at end of file diff --git a/dcr/scenarios/agent-persist-firewall/persist_firewall_helpers.py b/dcr/scenarios/agent-persist-firewall/persist_firewall_helpers.py deleted file mode 100644 index ff7df4a44d..0000000000 --- a/dcr/scenarios/agent-persist-firewall/persist_firewall_helpers.py +++ /dev/null @@ -1,294 +0,0 @@ -import os -import re -import shutil -import subprocess -import time -from datetime import datetime - -from dcr.scenario_utils.common_utils import execute_with_retry, read_file, get_current_agent_name - -__ROOT_CRON_LOG = "/var/tmp/reboot-cron-root.log" -__NON_ROOT_CRON_LOG = "/var/tmp/reboot-cron-non-root.log" -__NON_ROOT_WIRE_XML = "/var/tmp/wire-versions-non-root.xml" -__ROOT_WIRE_XML = "/var/tmp/wire-versions-root.xml" - -SVG_DIR = os.path.join("/var", "log", "svgs") - - -def get_wire_ip(): - wireserver_endpoint_file = '/var/lib/waagent/WireServerEndpoint' - try: - with open(wireserver_endpoint_file, 'r') as f: - wireserver_ip = f.read() - except Exception as e: - print("unable to read wireserver ip: {0}".format(e)) - wireserver_ip = '168.63.129.16' - print("In the meantime -- Using the well-known WireServer address.") - - return wireserver_ip - - -def get_iptables_rules(): - pipe = subprocess.Popen(["iptables", "-t", "security", "-L", "-nxv"], - stdout=subprocess.PIPE, - stderr=subprocess.PIPE) - stdout, stderr = pipe.communicate() - exit_code = pipe.returncode - - return exit_code, stdout.strip().decode(), stderr.strip().decode() - - -def __move_file_with_date_suffix(file_name): - # Copy it over to /var/log/ for future debugging - try: - shutil.move(src=file_name, dst=os.path.join("/var", "log", - "{0}.{1}".format(os.path.basename(file_name), - datetime.utcnow().isoformat()))) - except: - pass - - -def __read_and_get_wire_versions_file(wire_version_file): - print("\nCheck Output of wire-versions file") - if not os.path.exists(wire_version_file): - print("\tFile: {0} not found".format(wire_version_file)) - return None - - lines = None - if os.stat(wire_version_file).st_size > 0: - print("\n\t{0} not empty".format(wire_version_file)) - with open(wire_version_file) as f: - lines = f.readlines() - else: - print("\n\t{0} is empty".format(wire_version_file)) - - return lines - - -def __verify_data_in_cron_logs(cron_log, verify, err_msg): - print("\nVerify Cron logs - ") - - def op(): - cron_logs_lines = read_file(cron_log) - if not cron_logs_lines: - raise Exception("Empty cron file, looks like cronjob didnt run") - - if not any("ExitCode" in line for line in cron_logs_lines): - raise Exception("Cron logs still incomplete, will try again in a minute") - - if not any(verify(line) for line in cron_logs_lines): - raise Exception("Verification failed! (UNEXPECTED): {0}".format(err_msg)) - - print("Verification succeeded. Cron logs as expected") - - execute_with_retry(op, sleep=60, max_retry=7) - - -def verify_wire_ip_reachable_for_root(): - # For root logs - - # Ensure the /var/log/wire-versions-root.xml is not-empty (generated by the cron job) - # Ensure the exit code in the /var/log/reboot-cron-root.log file is 0 - print("\nVerifying WireIP is reachable from root user - ") - - def check_exit_code(line): - pattern = "ExitCode:\\s(\\d+)" - return re.match(pattern, line) is not None and int(re.match(pattern, line).groups()[0]) == 0 - - __verify_data_in_cron_logs(cron_log=__ROOT_CRON_LOG, verify=check_exit_code, - err_msg="Exit Code should be 0 for root based cron job!") - - if __read_and_get_wire_versions_file(__ROOT_WIRE_XML) is None: - raise Exception("Wire version file should not be empty for root!") - - -def verify_wire_ip_unreachable_for_non_root(): - # For non-root - - # Ensure the /var/log/wire-versions-non-root.xml is empty (generated by the cron job) - # Ensure the exit code in the /var/log/reboot-cron-non-root.log file is non-0 - print("\nVerifying WireIP is unreachable from non-root users - ") - - def check_exit_code(line): - match = re.match("ExitCode:\\s(\\d+)", line) - return match is not None and int(match.groups()[0]) != 0 - - __verify_data_in_cron_logs(cron_log=__NON_ROOT_CRON_LOG, verify=check_exit_code, - err_msg="Exit Code should be non-0 for non-root cron job!") - - if __read_and_get_wire_versions_file(__NON_ROOT_WIRE_XML) is not None: - raise Exception("Wire version file should be empty for non-root!") - - -def verify_wire_ip_in_iptables(max_retry=5): - expected_wire_ip = get_wire_ip() - stdout, stderr = "", "" - expected_regexes = [ - r"DROP.*{0}\s+ctstate\sINVALID,NEW.*".format(expected_wire_ip), - r"ACCEPT.*{0}\s+owner UID match 0.*".format(expected_wire_ip) - ] - retry = 0 - found = False - while retry < max_retry and not found: - ec, stdout, stderr = get_iptables_rules() - if not all(re.search(regex, stdout, re.MULTILINE) is not None for regex in expected_regexes): - # Some distros take some time for iptables to setup, sleeping a bit to give it enough time - time.sleep(30) - retry += 1 - continue - found = True - - print("\nIPTABLES RULES:\n\tSTDOUT: {0}".format(stdout)) - if stderr: - print("\tSTDERR: {0}".format(stderr)) - - if not found: - raise Exception("IPTables NOT set properly - WireIP not found in IPTables") - else: - print("IPTables set properly") - - -def verify_system_rebooted(): - - # This is primarily a fail safe mechanism to ensure tests don't run if the VM didnt reboot properly - signal_file = "/var/log/reboot_time.txt" - if not os.path.exists(signal_file): - print("Signal file not found, checking uptime") - __execute_and_print_cmd(["uptime", "-s"]) - raise Exception("Signal file {0} not found! Reboot didnt work as expected!".format(signal_file)) - - try: - with open(signal_file) as sig: - reboot_time_str = sig.read().strip() - - reboot_time = datetime.strptime(reboot_time_str, "%Y-%m-%dT%H:%M:%S.%fZ") - now = datetime.utcnow() - print("\nCron file Reboot time: {0}; Current Time: {1}\n".format(reboot_time_str, now.isoformat())) - if now <= reboot_time: - raise Exception( - "The reboot time {0} is somehow greater than current time {1}".format(reboot_time_str, now.isoformat())) - finally: - # Finally delete file to keep state clean - os.rename(signal_file, "{0}-{1}".format(signal_file, datetime.utcnow().isoformat())) - - -def __execute_and_print_cmd(cmd): - pipe = subprocess.Popen(cmd, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - shell=False) - stdout, stderr = pipe.communicate() - exit_code = pipe.returncode - - print( - "\n\tCommand: {0}, ExitCode: {1}\n\tStdout: {2}\n\tStderr: {3}".format(' '.join(cmd), exit_code, stdout.strip(), - stderr.strip())) - return exit_code, stdout, stderr - - -def run_systemctl_command(service_name, command="is-enabled"): - cmd = ["systemctl", command, service_name] - return __execute_and_print_cmd(cmd) - - -def get_firewalld_rules(): - cmd = ["firewall-cmd", "--permanent", "--direct", "--get-all-passthroughs"] - return __execute_and_print_cmd(cmd) - - -def get_firewalld_running_state(): - cmd = ["firewall-cmd", "--state"] - return __execute_and_print_cmd(cmd) - - -def get_logs_from_journalctl(unit_name): - cmd = ["journalctl", "-u", unit_name, "-b", "-o", "short-precise"] - return __execute_and_print_cmd(cmd) - - -def generate_svg(svg_name): - # This is a good to have, but not must have. Not failing tests if we're unable to generate a SVG - print("Running systemd-analyze plot command to get the svg for boot execution order") - dest_dir = SVG_DIR - if not os.path.exists(dest_dir): - os.makedirs(dest_dir) - retry = 0 - ec = 1 - while ec > 0 and retry < 3: - cmd = "systemd-analyze plot > {0}".format(os.path.join(dest_dir, svg_name)) - print("\tCommand for Svg: {0}".format(cmd)) - pipe = subprocess.Popen(cmd, - shell=True, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE) - stdout, stderr = pipe.communicate() - ec = pipe.returncode - if stdout or stderr: - print("\n\tSTDOUT: {0}\n\tSTDERR: {1}".format(stdout.strip(), stderr.strip())) - - if ec > 0: - retry += 1 - print("Failed with exit-code: {0}, retrying again in 60secs. Retry Attempt: {1}".format(ec, retry)) - time.sleep(60) - - -def firewalld_service_enabled(): - try: - exit_code, _, __ = get_firewalld_running_state() - return exit_code == 0 - except Exception as error: - print("\nFirewall service not running: {0}".format(error)) - - return False - - -def print_stateful_debug_data(): - """ - This function is used to print all debug data that we can capture to debug the scenario (which might not be - available on the log file). It would print the following if available (else just print the error) - - - The agent.service status - - Agent-network-setup.service status - - Agent-network-setup.service logs - - Firewall rules set using firewalld.service - - Output of Cron-logs for the current boot - - The state of iptables currently - """ - print("\n\n\nAll possible stateful Debug data (capturing before reboot) : ") - - agent_name = get_current_agent_name() - # - The agent.service status - run_systemctl_command("{0}.service".format(agent_name), "status") - - if firewalld_service_enabled(): - # - Firewall rules set using firewalld.service - get_firewalld_rules() - - # - Firewalld.service status - run_systemctl_command("firewalld.service", "status") - - else: - # - Agent-network-setup.service status - run_systemctl_command("{0}-network-setup.service".format(agent_name), "status") - - # - Agent-network-setup.service logs - # Sometimes the service status does not return logs, calling journalctl explicitly for fetching service logs - get_logs_from_journalctl(unit_name="{0}-network-setup.service".format(agent_name)) - - # - Print both Cron-logs contents (root and non-root) and if file is empty or not for Wire-version file - def _print_log_data(log_file): - try: - log_lines = read_file(log_file) - print("\nLogs for {0}: \n".format(log_file)) - for line in log_lines: - print("\t{0}".format(line)) - except Exception as error: - print("\nUnable to print logs for: {0}; Error: {1}".format(log_file, error)) - - for test_file in [__NON_ROOT_CRON_LOG, __NON_ROOT_WIRE_XML, __ROOT_CRON_LOG, __ROOT_WIRE_XML]: - # Move files over to the /var/log/ directory for bookkeeping - _print_log_data(test_file) - __move_file_with_date_suffix(test_file) - - # - The state of iptables currently - ec, stdout, stderr = get_iptables_rules() - print("\nIPTABLES RULES:\n\tSTDOUT: {0}".format(stdout)) - if stderr: - print("\tSTDERR: {0}".format(stderr)) diff --git a/dcr/scenarios/agent-persist-firewall/run.host.py b/dcr/scenarios/agent-persist-firewall/run.host.py deleted file mode 100644 index 2b2ef9073f..0000000000 --- a/dcr/scenarios/agent-persist-firewall/run.host.py +++ /dev/null @@ -1,41 +0,0 @@ -import asyncio -import os -import time - -from dcr.scenario_utils.azure_models import ComputeManager -from dcr.scenario_utils.common_utils import execute_py_script_over_ssh_on_test_vms, \ - execute_commands_concurrently_on_test_vms - -from persist_firewall_helpers import SVG_DIR - - -def get_svg_files(): - harvest_dir = os.path.join(os.environ['BUILD_ARTIFACTSTAGINGDIRECTORY'], "harvest") - scp_cmd = f"scp -o StrictHostKeyChecking=no {{username}}@{{ip}}:{SVG_DIR} {harvest_dir}" - asyncio.run(execute_commands_concurrently_on_test_vms([scp_cmd])) - - -if __name__ == '__main__': - # Execute run1.py first - try: - execute_py_script_over_ssh_on_test_vms(command="dcr/scenarios/agent-persist-firewall/run1.py") - - compute_manager = ComputeManager().compute_manager - # Restart VM and wait for it to come back up - compute_manager.restart() - - # Execute suite 2 - # Since the VM just restarted, wait for 10 secs before executing the script - time.sleep(10) - execute_py_script_over_ssh_on_test_vms(command="dcr/scenarios/agent-persist-firewall/run2.py") - - compute_manager.restart() - - # Execute suite 3 - # Since the VM just restarted, wait for 10 secs before executing the script - time.sleep(10) - execute_py_script_over_ssh_on_test_vms(command="dcr/scenarios/agent-persist-firewall/run3.py") - finally: - # Always try to fetch svg files off of the VM - get_svg_files() - diff --git a/dcr/scenarios/agent-persist-firewall/run1.py b/dcr/scenarios/agent-persist-firewall/run1.py deleted file mode 100644 index cac9b43aa0..0000000000 --- a/dcr/scenarios/agent-persist-firewall/run1.py +++ /dev/null @@ -1,13 +0,0 @@ -from dcr.scenario_utils.test_orchestrator import TestFuncObj, TestOrchestrator -from persist_firewall_helpers import verify_wire_ip_in_iptables - -if __name__ == '__main__': - tests = [ - TestFuncObj("Verify_Wire_IP_IPTables", verify_wire_ip_in_iptables) - ] - - test_orchestrator = TestOrchestrator("PersistFirewall-VM1", tests=tests) - test_orchestrator.run_tests() - test_orchestrator.generate_report_on_vm("test-result-pf-run1.xml") - assert not test_orchestrator.failed, f"Test Suite: {test_orchestrator.name} failed" - diff --git a/dcr/scenarios/agent-persist-firewall/run2.py b/dcr/scenarios/agent-persist-firewall/run2.py deleted file mode 100644 index 42a420f762..0000000000 --- a/dcr/scenarios/agent-persist-firewall/run2.py +++ /dev/null @@ -1,58 +0,0 @@ -from dcr.scenario_utils.common_utils import get_current_agent_name -from dcr.scenario_utils.test_orchestrator import TestFuncObj, TestOrchestrator -from persist_firewall_helpers import verify_wire_ip_in_iptables, verify_system_rebooted, generate_svg, \ - verify_wire_ip_unreachable_for_non_root, verify_wire_ip_reachable_for_root, run_systemctl_command, \ - firewalld_service_enabled, print_stateful_debug_data - - -def check_external_service_status(): - agent_name = get_current_agent_name() - # Check if firewall active on the Vm - if firewalld_service_enabled(): - # If yes, then print its status - ec, _, __ = run_systemctl_command("firewalld.service", command="status") - if ec != 0: - raise Exception("Something wrong with firewalld.service!") - - # Else print status of our custom service - else: - service_name = "{0}-network-setup.service".format(agent_name) - - # Check if enabled, if not then raise Error - ec, stdout, stderr = run_systemctl_command(service_name, command="is-enabled") - if ec != 0: - raise Exception("Service should be enabled!") - - # Check if failed, if so then raise Error - ec, stdout, stderr = run_systemctl_command(service_name, command="is-failed") - if ec == 0: - raise Exception("The service should not be in a failed state!") - - # Finally print the status of the service - run_systemctl_command(service_name, command="status") - - print("\nDisable Guest Agent service for more verbose testing") - ec, _, __ = run_systemctl_command(service_name="{0}.service".format(agent_name), command="disable") - if ec != 0: - raise Exception("Agent not disabled properly!") - - -if __name__ == '__main__': - tests = [ - TestFuncObj("Verify system rebooted", verify_system_rebooted, raise_on_error=True), - TestFuncObj("Generate SVG", lambda: generate_svg(svg_name="agent_running.svg")), - TestFuncObj("Verify wireIP unreachable for non-root", verify_wire_ip_unreachable_for_non_root), - TestFuncObj("Verify wireIP reachable for root", verify_wire_ip_reachable_for_root), - TestFuncObj("Verify_Wire_IP_IPTables", lambda: verify_wire_ip_in_iptables(max_retry=1)), - TestFuncObj("Verify External services", check_external_service_status) - ] - - test_orchestrator = TestOrchestrator("PersistFirewall-VM2", tests=tests) - test_orchestrator.run_tests() - - # Print stateful debug data before reboot because the state might be lost after - print_stateful_debug_data() - - test_orchestrator.generate_report_on_vm("test-result-pf-run2.xml") - assert not test_orchestrator.failed, f"Test Suite: {test_orchestrator.name} failed" - diff --git a/dcr/scenarios/agent-persist-firewall/run3.py b/dcr/scenarios/agent-persist-firewall/run3.py deleted file mode 100644 index 6a7963ebc4..0000000000 --- a/dcr/scenarios/agent-persist-firewall/run3.py +++ /dev/null @@ -1,36 +0,0 @@ -from dcr.scenario_utils.check_waagent_log import check_waagent_log_for_errors -from dcr.scenario_utils.common_utils import get_current_agent_name -from dcr.scenario_utils.test_orchestrator import TestFuncObj, TestOrchestrator -from persist_firewall_helpers import verify_wire_ip_in_iptables, run_systemctl_command, verify_system_rebooted, \ - generate_svg, verify_wire_ip_unreachable_for_non_root, verify_wire_ip_reachable_for_root - - -def ensure_agent_not_running(): - print("Verifying agent not running") - agent_service_name = "{0}.service".format(get_current_agent_name()) - ec, _, __ = run_systemctl_command(agent_service_name, "is-enabled") - if ec == 0: - raise Exception("{0} is enabled!".format(agent_service_name)) - - ec, _, __ = run_systemctl_command(agent_service_name, "is-active") - if ec == 0: - raise Exception("{0} should not be active!".format(agent_service_name)) - - -if __name__ == '__main__': - tests = [ - TestFuncObj("Verify system rebooted", verify_system_rebooted, raise_on_error=True), - TestFuncObj("Ensure agent not running", ensure_agent_not_running), - TestFuncObj("Generate SVG", lambda: generate_svg(svg_name="agent_not_running.svg")), - TestFuncObj("Verify wire IP unreachable for non-root", verify_wire_ip_unreachable_for_non_root), - TestFuncObj("Verify wire IP reachable for root", verify_wire_ip_reachable_for_root), - # Considering the rules should be set on reboot, not adding a retry check - TestFuncObj("Verify wire IP in IPTables", lambda: verify_wire_ip_in_iptables(max_retry=1)), - TestFuncObj("Check agent log", check_waagent_log_for_errors) - ] - - test_orchestrator = TestOrchestrator("PersistFirewall-VM3", tests=tests) - test_orchestrator.run_tests() - test_orchestrator.generate_report_on_vm("test-result-pf-run3.xml") - assert not test_orchestrator.failed, f"Test Suite: {test_orchestrator.name} failed" - diff --git a/dcr/scenarios/agent-persist-firewall/setup.sh b/dcr/scenarios/agent-persist-firewall/setup.sh deleted file mode 100644 index 73ef46419a..0000000000 --- a/dcr/scenarios/agent-persist-firewall/setup.sh +++ /dev/null @@ -1,29 +0,0 @@ -#!/usr/bin/env bash - -# 1 2 3 -# Usage: -set -euxo pipefail - -d=$(which date) -ipt=$(which iptables) -username="dcr" -script_dir=$(dirname "$0") -cp "$script_dir/access_wire_ip.sh" "/usr/bin/" -chmod 700 "/usr/bin/access_wire_ip.sh" -mkdir -p /home/$username || echo "this is only needed for Suse VMs for running cron jobs as non-root" -# Setup Cron jobs -echo "@reboot ($d --utc +\\%FT\\%T.\\%3NZ && /usr/bin/access_wire_ip.sh $ipt) > /var/tmp/reboot-cron-root.log 2>&1" | crontab -u root - -echo "@reboot ($d --utc +\\%FT\\%T.\\%3NZ && /usr/bin/access_wire_ip.sh $ipt) > /var/tmp/reboot-cron-non-root.log 2>&1" | crontab -u $username - -(crontab -l 2>/dev/null; echo "@reboot ($d --utc +\%FT\%T.\%3NZ) > /var/log/reboot_time.txt 2>&1") | crontab -u root - -s=$(which systemctl) -(crontab -l 2>/dev/null; echo "@reboot ($s status walinuxagent-network-setup.service || $s status waagent-network-setup.service) > /var/log/reboot_network_setup.txt 2>&1)") | crontab -u root - - -# Enable Firewall for all distros -sed -i 's/OS.EnableFirewall=n/OS.EnableFirewall=y/g' /etc/waagent.conf - -# Restart agent to pick up the new conf -systemctl restart waagent || systemctl restart walinuxagent - -# Ensure that the setup file exists -file="wa*-network-setup.service" -[ "$(ls /usr/lib/systemd/system/$file /lib/systemd/system/$file 2>/dev/null | wc -w)" -gt 0 ] && echo "agent-network-setup file exists" || echo "agent-network-setup file does not exists" \ No newline at end of file diff --git a/dcr/scenarios/ext-seq-multiple-dependencies/config.json b/dcr/scenarios/ext-seq-multiple-dependencies/config.json deleted file mode 100644 index ad89d13979..0000000000 --- a/dcr/scenarios/ext-seq-multiple-dependencies/config.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "location": "West Central US" -} diff --git a/dcr/scenarios/ext-seq-multiple-dependencies/ext_seq.py b/dcr/scenarios/ext-seq-multiple-dependencies/ext_seq.py deleted file mode 100644 index 61c648afc5..0000000000 --- a/dcr/scenarios/ext-seq-multiple-dependencies/ext_seq.py +++ /dev/null @@ -1,171 +0,0 @@ -import re -import uuid -from datetime import datetime -from time import sleep - -from azure.mgmt.resource.resources.models import DeploymentMode, DeploymentProperties, Deployment -from msrestazure.azure_exceptions import CloudError - -from dcr.scenario_utils.azure_models import ComputeManager -from dcr.scenario_utils.logging_utils import LoggingHandler -from dcr.scenario_utils.models import get_vm_data_from_env - - -class ExtensionSequencingTestClass(LoggingHandler): - - # This is the base ARM template that's used for deploying extensions for this scenario. These templates build on - # top of each other. i.e., 01_test runs first, then 02_test builds on top of it and so on and so forth. - extension_template = { - "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json", - "contentVersion": "1.0.0.0", - "resources": [ - { - "type": "Microsoft.Compute/virtualMachineScaleSets", - "name": "", - "location": "[resourceGroup().location]", - "apiVersion": "2018-06-01", - "properties": { - "virtualMachineProfile": { - "extensionProfile": { - "extensions": [] - } - } - } - } - ] - } - - def __init__(self): - super().__init__() - self.__vm_data = get_vm_data_from_env() - self.__compute_manager = ComputeManager().compute_manager - - # Update the VMSS name - ExtensionSequencingTestClass.extension_template['resources'][0]['name'] = self.__vm_data.name - - def deploy_extensions(self, ext_json): - self.log.info(f"Deploying extension template: {ext_json}") - - retry = 0 - max_retry = 5 - while retry < max_retry: - try: - props = DeploymentProperties(template=ext_json, - mode=DeploymentMode.incremental) - poller = self.__compute_manager.resource_client.deployments.begin_create_or_update( - self.__vm_data.rg_name, 'TestDeployment', Deployment(properties=props)) - # Wait a max of 10 mins - poller.wait(timeout=10 * 60) - if poller.done(): - break - else: - raise TimeoutError("Extension deployment timed out after 10 mins") - except CloudError as ce: - self.log.warning(f"Cloud Error: {ce}", exc_info=True) - retry += 1 - err_msg = str(ce) - if "'code': 'Conflict'" in err_msg and retry < max_retry: - self.log.warning( - "({0}/{1}) Conflict Error when deploying extension in VMSS, trying again in 1 sec (Error: {2})".format( - retry, max_retry, ce)) - # Since this was a conflicting operation, sleeping for a second before retrying - sleep(1) - else: - raise - - self.log.info("Successfully deployed extensions") - - @staticmethod - def get_dependency_map(ext_json) -> dict: - dependency_map = dict() - - vmss = ext_json['resources'][0] - extensions = vmss['properties']['virtualMachineProfile']['extensionProfile']['extensions'] - - for ext in extensions: - ext_name = ext['name'] - provisioned_after = ext['properties'].get('provisionAfterExtensions') - dependency_map[ext_name] = provisioned_after - - return dependency_map - - @staticmethod - def __get_time(ext, test_guid): - if ext.statuses[0].time is not None: - # This is populated if `configurationAppliedTime` is provided in the status file of extension - return ext.statuses[0].time - - if ext.statuses[0].message is not None: - # In our tests, for CSE and RunCommand, we would execute this command to get the time when it was enabled - - # echo 'GUID: $(date +%Y-%m-%dT%H:%M:%S.%3NZ)' - match = re.search(r"{0}: ([\d-]+T[\d:.]+Z)".format(test_guid), ext.statuses[0].message) - if match is not None: - return datetime.strptime(match.group(1), "%Y-%m-%dT%H:%M:%S.%fZ") - - # If nothing else works, just return the minimum datetime - return datetime.min - - def get_sorted_extension_names(self, test_guid): - # Retrieve the VMSS extension instances - vmss_vm_extensions = self.__compute_manager.get_vm_instance_view().extensions - - # Log the extension enabled datetime - for ext in vmss_vm_extensions: - ext.time = self.__get_time(ext, test_guid) - self.log.info("Extension {0} Status: {1}".format(ext.name, ext.statuses[0])) - - # sort the extensions based on their enabled datetime - sorted_extensions = sorted(vmss_vm_extensions, key=lambda ext_: ext_.time) - self.log.info("Sorted extension names with time: {0}".format( - ', '.join(["{0}: {1}".format(ext.name, ext.time) for ext in sorted_extensions]))) - return [ext.name for ext in sorted_extensions] - - def validate_extension_sequencing(self, dependency_map, sorted_extension_names): - installed_ext = dict() - - # Iterate through the extensions in the enabled order and validate if their depending - # extensions are already enabled prior to that. - for ext in sorted_extension_names: - # Check if the depending extension are already installed - if ext not in dependency_map: - # Some extensions might be installed by policy, continue in this case - self.log.info("Unwanted extension found in Instance view: {0}".format(ext)) - continue - if dependency_map[ext] is not None: - for dep in dependency_map[ext]: - if installed_ext.get(dep) is None: - # The depending extension is not installed prior to the current extension - raise Exception("{0} is not installed prior to {1}".format(dep, ext)) - - # Mark the current extension as installed - installed_ext[ext] = ext - - self.log.info("Validated extension sequencing") - - def run(self, extension_template): - - # Update the settings for each extension to make sure they're always unique to force CRP to generate a new - # sequence number each time - ext_json = ExtensionSequencingTestClass.extension_template.copy() - test_guid = str(uuid.uuid4()) - for ext in extension_template: - ext["properties"]["settings"].update({ - "commandToExecute": "echo \"{0}: $(date +%Y-%m-%dT%H:%M:%S.%3NZ)\"".format(test_guid) - }) - - # We update the extensions here, they are specific to the scenario that we want to test out (01_test, 02_test..) - ext_json['resources'][0]['properties']['virtualMachineProfile']['extensionProfile'][ - 'extensions'] = extension_template - - # Deploy VMSS extensions with sequence - self.deploy_extensions(ext_json) - - # Build the dependency map from the list of extensions in the extension profile - dependency_map = self.get_dependency_map(ext_json) - self.log.info("Dependency map: {0}".format(dependency_map)) - - # Get the extensions sorted based on their enabled datetime - sorted_extension_names = self.get_sorted_extension_names(test_guid) - self.log.info("Sorted extensions: {0}".format(sorted_extension_names)) - - self.validate_extension_sequencing(dependency_map, sorted_extension_names) diff --git a/dcr/scenarios/ext-seq-multiple-dependencies/ext_seq_tests.py b/dcr/scenarios/ext-seq-multiple-dependencies/ext_seq_tests.py deleted file mode 100644 index b1be968da9..0000000000 --- a/dcr/scenarios/ext-seq-multiple-dependencies/ext_seq_tests.py +++ /dev/null @@ -1,196 +0,0 @@ -def add_extensions_with_dependency_template(): - return [ - { - "name": "GATestExt", - "properties": { - "publisher": "Microsoft.Azure.Extensions.Edp", - "type": "GATestExtGo", - "typeHandlerVersion": "1.0", - "autoUpgradeMinorVersion": True, - "settings": { - "name": "Enabling GA Test Extension" - } - } - }, - { - "name": "RunCommand", - "properties": { - "provisionAfterExtensions": ["GATestExt"], - "publisher": "Microsoft.CPlat.Core", - "type": "RunCommandLinux", - "typeHandlerVersion": "1.0", - "autoUpgradeMinorVersion": True, - "settings": {} - } - }, - { - "name": "CSE", - "properties": { - "provisionAfterExtensions": ["RunCommand", "GATestExt"], - "publisher": "Microsoft.Azure.Extensions", - "type": "CustomScript", - "typeHandlerVersion": "2.1", - "autoUpgradeMinorVersion": True, - "settings": {} - } - } - ] - - -def remove_dependent_extension_template(): - return [ - { - "name": "GATestExt", - "properties": { - "publisher": "Microsoft.Azure.Extensions.Edp", - "type": "GATestExtGo", - "typeHandlerVersion": "1.0", - "autoUpgradeMinorVersion": True, - "settings": { - "name": "Enabling GA Test Extension" - } - } - }, - { - "name": "CSE", - "properties": { - "provisionAfterExtensions": ["GATestExt"], - "publisher": "Microsoft.Azure.Extensions", - "type": "CustomScript", - "typeHandlerVersion": "2.1", - "autoUpgradeMinorVersion": True, - "settings": {} - } - } - ] - - -def remove_all_dependencies_template(): - return [ - { - "name": "GATestExt", - "properties": { - "publisher": "Microsoft.Azure.Extensions.Edp", - "type": "GATestExtGo", - "typeHandlerVersion": "1.0", - "autoUpgradeMinorVersion": True, - "settings": { - "name": "Enabling GA Test Extension" - } - } - }, - { - "name": "RunCommand", - "properties": { - "publisher": "Microsoft.CPlat.Core", - "type": "RunCommandLinux", - "typeHandlerVersion": "1.0", - "autoUpgradeMinorVersion": True, - "settings": {} - } - }, - { - "name": "CSE", - "properties": { - "publisher": "Microsoft.Azure.Extensions", - "type": "CustomScript", - "typeHandlerVersion": "2.1", - "autoUpgradeMinorVersion": True, - "settings": {} - } - } - ] - - -def add_more_dependencies_template(): - return [ - { - "name": "GATestExt", - "properties": { - "publisher": "Microsoft.Azure.Extensions.Edp", - "type": "GATestExtGo", - "typeHandlerVersion": "1.0", - "autoUpgradeMinorVersion": True, - "settings": { - "name": "Enabling GA Test Extension" - } - } - }, - { - "name": "RunCommand", - "properties": { - "publisher": "Microsoft.CPlat.Core", - "type": "RunCommandLinux", - "typeHandlerVersion": "1.0", - "autoUpgradeMinorVersion": True, - "settings": {} - } - }, - { - "name": "CSE", - "properties": { - "provisionAfterExtensions": ["RunCommand", "GATestExt"], - "publisher": "Microsoft.Azure.Extensions", - "type": "CustomScript", - "typeHandlerVersion": "2.1", - "autoUpgradeMinorVersion": True, - "settings": {} - } - } - ] - - -def single_dependencies_template(): - return [ - { - "name": "GATestExt", - "properties": { - "publisher": "Microsoft.Azure.Extensions.Edp", - "type": "GATestExtGo", - "typeHandlerVersion": "1.0", - "autoUpgradeMinorVersion": True, - "settings": { - "name": "Enabling GA Test Extension" - } - } - }, - { - "name": "RunCommand", - "properties": { - "provisionAfterExtensions": ["CSE"], - "publisher": "Microsoft.CPlat.Core", - "type": "RunCommandLinux", - "typeHandlerVersion": "1.0", - "autoUpgradeMinorVersion": True, - "settings": {} - } - }, - { - "name": "CSE", - "properties": { - "provisionAfterExtensions": ["GATestExt"], - "publisher": "Microsoft.Azure.Extensions", - "type": "CustomScript", - "typeHandlerVersion": "2.1", - "autoUpgradeMinorVersion": True, - "settings": {} - } - } - ] - - -def delete_extensions_template(): - return [ - { - "name": "GATestExt", - "properties": { - "publisher": "Microsoft.Azure.Extensions.Edp", - "type": "GATestExtGo", - "typeHandlerVersion": "1.0", - "autoUpgradeMinorVersion": True, - "settings": { - "name": "Enabling GA Test Extension" - } - } - } - ] diff --git a/dcr/scenarios/ext-seq-multiple-dependencies/run.host.py b/dcr/scenarios/ext-seq-multiple-dependencies/run.host.py deleted file mode 100644 index d75acb8054..0000000000 --- a/dcr/scenarios/ext-seq-multiple-dependencies/run.host.py +++ /dev/null @@ -1,27 +0,0 @@ -from dcr.scenario_utils.test_orchestrator import TestFuncObj, TestOrchestrator -from ext_seq import ExtensionSequencingTestClass -from ext_seq_tests import add_extensions_with_dependency_template, remove_dependent_extension_template, \ - remove_all_dependencies_template, add_more_dependencies_template, single_dependencies_template, \ - delete_extensions_template - - -def main(): - ext_seq = ExtensionSequencingTestClass() - - tests = [ - TestFuncObj("Add Extensions with dependencies", lambda: ext_seq.run(add_extensions_with_dependency_template()), raise_on_error=True), - TestFuncObj("Remove dependent extension", lambda: ext_seq.run(remove_dependent_extension_template())), - TestFuncObj("Remove all dependencies", lambda: ext_seq.run(remove_all_dependencies_template())), - TestFuncObj("Add more dependencies", lambda: ext_seq.run(add_more_dependencies_template())), - TestFuncObj("single dependencies", lambda: ext_seq.run(single_dependencies_template())), - TestFuncObj("Delete extensions", lambda: ext_seq.run(delete_extensions_template())) - ] - - test_orchestrator = TestOrchestrator("ExtSeqDependency-Host", tests=tests) - test_orchestrator.run_tests() - test_orchestrator.generate_report_on_orchestrator("test-results-ext-seq-host.xml") - assert not test_orchestrator.failed, f"Test Suite: {test_orchestrator.name} failed" - - -if __name__ == '__main__': - main() diff --git a/dcr/scenarios/ext-seq-multiple-dependencies/run.py b/dcr/scenarios/ext-seq-multiple-dependencies/run.py deleted file mode 100644 index bd28188cea..0000000000 --- a/dcr/scenarios/ext-seq-multiple-dependencies/run.py +++ /dev/null @@ -1,15 +0,0 @@ -import socket - -from dcr.scenario_utils.check_waagent_log import check_waagent_log_for_errors -from dcr.scenario_utils.test_orchestrator import TestFuncObj, TestOrchestrator - - -if __name__ == '__main__': - tests = [ - TestFuncObj("check agent log", check_waagent_log_for_errors) - ] - - test_orchestrator = TestOrchestrator("ExtSeqDependency-VM", tests=tests) - test_orchestrator.run_tests() - test_orchestrator.generate_report_on_vm(f"test-result-ext-seq-vm-{socket.gethostname()}.xml") - assert not test_orchestrator.failed, f"Test Suite: {test_orchestrator.name} failed" diff --git a/dcr/scenarios/ext-seq-multiple-dependencies/template.json b/dcr/scenarios/ext-seq-multiple-dependencies/template.json deleted file mode 100644 index 358b264a3c..0000000000 --- a/dcr/scenarios/ext-seq-multiple-dependencies/template.json +++ /dev/null @@ -1,304 +0,0 @@ -{ - "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", - "contentVersion": "1.0.0.0", - "parameters": { - "adminUsername": { - "type": "string" - }, - "adminPasswordOrKey": { - "type": "string" - }, - "vmSize": { - "type": "string", - "defaultValue": "Standard_B2s" - }, - "vmName": { - "type": "string" - }, - "scenarioPrefix": { - "type": "string", - "defaultValue": "dcr" - }, - "imagePublisher": { - "type": "string", - "defaultValue": "Canonical" - }, - "imageOffer": { - "type": "string", - "defaultValue": "UbuntuServer" - }, - "imageVersion": { - "type": "string", - "defaultValue": "latest" - }, - "imageSku": { - "type": "string", - "defaultValue": "18.04-LTS" - }, - "location": { - "type": "string", - "defaultValue": "[resourceGroup().location]", - "metadata": { - "description": "Location for all resources." - } - }, - "dnsLabelPrefix": { - "type": "string", - "defaultValue": "[toLower(format('simplelinuxvm-{0}', uniqueString(resourceGroup().id)))]", - "metadata": { - "description": "Unique DNS Name for the Public IP used to access the Virtual Machine." - } - } - }, - "variables": { - "nicName": "[concat(parameters('scenarioPrefix'),'Nic')]", - "vnetAddressPrefix": "10.130.0.0/16", - "subnetName": "[concat(parameters('scenarioPrefix'),'Subnet')]", - "subnetPrefix": "10.130.0.0/24", - "publicIPAddressName": "[concat(parameters('scenarioPrefix'),'PublicIp')]", - "lbIpName": "[concat(parameters('scenarioPrefix'),'PublicLbIp')]", - "lbIpId": "[resourceId('Microsoft.Network/publicIPAddresses', variables('lbIpName'))]", - "virtualNetworkName": "[concat(parameters('scenarioPrefix'),'Vnet')]", - "lbName": "[concat(parameters('scenarioPrefix'),'lb')]", - "bepoolName": "[concat(variables('lbName'), 'bepool')]", - "natpoolName": "[concat(variables('lbName'), 'natpool')]", - "feIpConfigName": "[concat(variables('lbName'), 'fepool', 'IpConfig')]", - "sshProbeName": "[concat(variables('lbName'), 'probe')]", - "vnetID": "[resourceId('Microsoft.Network/virtualNetworks',variables('virtualNetworkName'))]", - "subnetRef": "[concat(variables('vnetID'),'/subnets/',variables('subnetName'))]", - "lbId": "[resourceId('Microsoft.Network/loadBalancers', variables('lbName'))]", - "bepoolID": "[concat(variables('lbId'), '/backendAddressPools/', variables('bepoolName'))]", - "natpoolID": "[concat(variables('lbId'), '/inboundNatPools/', variables('natpoolName'))]", - "feIpConfigId": "[concat(variables('lbId'), '/frontendIPConfigurations/', variables('feIpConfigName'))]", - "sshProbeId": "[concat(variables('lbId'), '/probes/', variables('sshProbeName'))]", - "sshKeyPath": "[concat('/home/', parameters('adminUsername'), '/.ssh/authorized_keys')]", - "networkSecurityGroupName": "networkSecurityGroup1" - }, - "resources": [ - { - "apiVersion": "2017-06-01", - "type": "Microsoft.Network/networkSecurityGroups", - "name": "[variables('networkSecurityGroupName')]", - "location": "[parameters('location')]", - "properties": { - "securityRules": [ - { - "name": "SSH", - "properties": { - "description": "Locks inbound down to jenkins ip range.", - "protocol": "Tcp", - "sourcePortRange": "*", - "destinationPortRange": "22", - "sourceAddressPrefix": "*", - "destinationAddressPrefix": "*", - "access": "Allow", - "priority": 100, - "direction": "Inbound" - } - } - ] - } - }, - { - "apiVersion": "2016-12-01", - "type": "Microsoft.Network/virtualNetworks", - "name": "[variables('virtualNetworkName')]", - "location": "[parameters('location')]", - "dependsOn": [ - "[concat('Microsoft.Network/networkSecurityGroups/', variables('networkSecurityGroupName'))]" - ], - "properties": { - "addressSpace": { - "addressPrefixes": [ - "[variables('vnetAddressPrefix')]" - ] - }, - "subnets": [ - { - "name": "[variables('subnetName')]", - "properties": { - "addressPrefix": "[variables('subnetPrefix')]", - "networkSecurityGroup": { - "id": "[resourceId('Microsoft.Network/networkSecurityGroups', variables('networkSecurityGroupName'))]" - } - } - } - ] - } - }, - { - "type": "Microsoft.Network/publicIPAddresses", - "name": "[variables('lbIpName')]", - "location": "[parameters('location')]", - "apiVersion": "2017-04-01", - "properties": { - "publicIPAllocationMethod": "Dynamic", - "dnsSettings": { - "domainNameLabel": "[parameters('dnsLabelPrefix')]" - } - } - }, - { - "type": "Microsoft.Network/loadBalancers", - "name": "[variables('lbName')]", - "location": "[parameters('location')]", - "apiVersion": "2016-03-30", - "dependsOn": [ - "[concat('Microsoft.Network/virtualNetworks/', variables('virtualNetworkName'))]", - "[concat('Microsoft.Network/publicIPAddresses/', variables('lbIpName'))]" - ], - "properties": { - "frontendIPConfigurations": [ - { - "name": "[variables('feIpConfigName')]", - "properties": { - "PublicIpAddress": { - "id": "[variables('lbIpId')]" - } - } - } - ], - "backendAddressPools": [ - { - "name": "[variables('bepoolName')]" - } - ], - "inboundNatPools": [ - { - "name": "[variables('natpoolName')]", - "properties": { - "FrontendIPConfiguration": { - "Id": "[variables('feIpConfigId')]" - }, - "BackendPort": 22, - "Protocol": "tcp", - "FrontendPortRangeStart": 3500, - "FrontendPortRangeEnd": 4500 - } - } - ], - "loadBalancingRules": [ - { - "name": "ProbeRule", - "properties": { - "frontendIPConfiguration": { - "id": "[variables('feIpConfigId')]" - }, - "backendAddressPool": { - "id": "[variables('bepoolID')]" - }, - "protocol": "Tcp", - "frontendPort": 80, - "backendPort": 80, - "idleTimeoutInMinutes": 5, - "probe": { - "id": "[variables('sshProbeId')]" - } - } - } - ], - "probes": [ - { - "name": "[variables('sshProbeName')]", - "properties": { - "protocol": "tcp", - "port": 22, - "intervalInSeconds": 5, - "numberOfProbes": 2 - } - } - ] - } - }, - { - "apiVersion": "2018-06-01", - "type": "Microsoft.Compute/virtualMachineScaleSets", - "name": "[parameters('vmName')]", - "location": "[parameters('location')]", - "dependsOn": [ - "[concat('Microsoft.Network/virtualNetworks/', variables('virtualNetworkName'))]", - "[concat('Microsoft.Network/loadBalancers/', variables('lbName'))]" - ], - "sku": { - "name": "[parameters('vmSize')]", - "tier": "Standard", - "capacity": 3 - }, - "properties": { - "virtualMachineProfile": { - "extensionProfile": {}, - "osProfile": { - "computerNamePrefix": "[parameters('vmName')]", - "adminUsername": "[parameters('adminUsername')]", - "linuxConfiguration": { - "disablePasswordAuthentication": true, - "ssh": { - "publicKeys": [ - { - "path": "[variables('sshKeyPath')]", - "keyData": "[parameters('adminPasswordOrKey')]" - } - ] - } - } - }, - "storageProfile": { - "osDisk": { - "osType": "Linux", - "createOption": "FromImage" - }, - "imageReference": { - "publisher": "[parameters('imagePublisher')]", - "offer": "[parameters('imageOffer')]", - "sku": "[parameters('imageSku')]", - "version": "[parameters('imageVersion')]" - } - }, - "networkProfile": { - "healthProbe": { - "id": "[variables('sshProbeId')]" - }, - "networkInterfaceConfigurations": [ - { - "name": "[variables('nicName')]", - "properties": { - "primary": true, - "ipConfigurations": [ - { - "name": "ipconfig1", - "properties": { - "primary": true, - "publicIPAddressConfiguration": { - "name": "[variables('publicIPAddressName')]", - "properties": { - "idleTimeoutInMinutes": 15 - } - }, - "subnet": { - "id": "[variables('subnetRef')]" - }, - "loadBalancerBackendAddressPools": [ - { - "id": "[variables('bepoolID')]" - } - ], - "loadBalancerInboundNatPools": [ - { - "id": "[variables('natpoolID')]" - } - ] - } - } - ] - } - } - ] - } - }, - "upgradePolicy": { - "mode": "Automatic" - } - } - } - ] -} \ No newline at end of file diff --git a/dcr/scenarios/extension-telemetry-pipeline/etp_helpers.py b/dcr/scenarios/extension-telemetry-pipeline/etp_helpers.py deleted file mode 100644 index 9fd06587ea..0000000000 --- a/dcr/scenarios/extension-telemetry-pipeline/etp_helpers.py +++ /dev/null @@ -1,180 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import print_function - -import glob -import json -import os -import time -import uuid -from datetime import datetime, timedelta -from random import choice - - -def get_collect_telemetry_thread_name(): - return "TelemetryEventsCollector" - - -def wait_for_extension_events_dir_empty(timeout=timedelta(minutes=2)): - # By ensuring events dir to be empty, we verify that the telemetry events collector has completed its run - event_dirs = glob.glob(os.path.join("/var/log/azure/", "*", "events")) - start_time = datetime.now() - - assert event_dirs, "No extension event directories exist!" - - while (start_time + timeout) >= datetime.now(): - all_dir_empty = True - for event_dir in event_dirs: - if not os.path.exists(event_dir) or len(os.listdir(event_dir)) != 0: - print("Dir: {0} not empty".format(event_dir)) - all_dir_empty = False - break - - if all_dir_empty: - return - - time.sleep(5) - - raise AssertionError("Extension events dir not empty!") - - -def add_extension_events_and_get_count(bad_event_count=0, no_of_events_per_extension=50, extension_names=None): - print("Creating random extension events now. No of Good Events: {0}, No of Bad Events: {1}".format( - no_of_events_per_extension - bad_event_count, bad_event_count)) - - def missing_key(make_bad_event): - key = choice(list(make_bad_event.keys())) - del make_bad_event[key] - return "MissingKeyError: {0}".format(key) - - def oversize_error(make_bad_event): - make_bad_event["EventLevel"] = "ThisIsAnOversizeErrorOnSteroids\n" * 300 - return "OversizeEventError" - - def empty_message(make_bad_event): - make_bad_event["Message"] = "" - return "EmptyMessageError" - - def oversize_file_limit(make_bad_event): - make_bad_event["EventLevel"] = "MakeThisFileGreatAgain\n" * 30000 - return "OversizeEventFileSize" - - sample_ext_event = { - "EventLevel": "INFO", - "Message": "Starting IaaS ScriptHandler Extension v1", - "Version": "1.0", - "TaskName": "Extension Info", - "EventPid": "3228", - "EventTid": "1", - "OperationId": "519e4beb-018a-4bd9-8d8e-c5226cf7f56e", - "TimeStamp": "2019-12-12T01:20:05.0950244Z" - } - - sample_messages = [ - "Starting IaaS ScriptHandler Extension v1", - "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.", - "The quick brown fox jumps over the lazy dog", - "Cursus risus at ultrices mi.", - "Doing Something", - "Iaculis eu non diam phasellus.", - "Doing other thing", - "Look ma, lemons", - "Pretium quam vulputate dignissim suspendisse.", - "Man this is insane", - "I wish it worked as it should and not as it ain't", - "Ut faucibus pulvinar elementum integer enim neque volutpat ac tincidunt." - "Did you get any of that?", - ] - - # Currently the GA cant send special chars in telemetry as the unicode changes were reverted. - # Once its enabled again, we would add these messages back to our tests. - # Should be enabled when this task is completed - https://msazure.visualstudio.com/One/_workitems/edit/8733946 - non_english_messages = [ - "Non-English message - 此文字不是英文的" - "κόσμε", - "�", - "Quizdeltagerne spiste jordbær med fløde, mens cirkusklovnen Wolther spillede på xylofon.", - "Falsches Üben von Xylophonmusik quält jeden größeren Zwerg", - "Zwölf Boxkämpfer jagten Eva quer über den Sylter Deich", - "Heizölrückstoßabdämpfung", - "Γαζέες καὶ μυρτιὲς δὲν θὰ βρῶ πιὰ στὸ χρυσαφὶ ξέφωτο", - "Ξεσκεπάζω τὴν ψυχοφθόρα βδελυγμία", - "El pingüino Wenceslao hizo kilómetros bajo exhaustiva lluvia y frío, añoraba a su querido cachorro.", - "Portez ce vieux whisky au juge blond qui fume sur son île intérieure, à côté de l'alcôve ovoïde, où les bûches", - "se consument dans l'âtre, ce qui lui permet de penser à la cænogenèse de l'être dont il est question", - "dans la cause ambiguë entendue à Moÿ, dans un capharnaüm qui, pense-t-il, diminue çà et là la qualité de son œuvre.", - "D'fhuascail Íosa, Úrmhac na hÓighe Beannaithe, pór Éava agus Ádhaimh", - "Árvíztűrő tükörfúrógép", - "Kæmi ný öxi hér ykist þjófum nú bæði víl og ádrepa", - "Sævör grét áðan því úlpan var ónýt", - "いろはにほへとちりぬるを わかよたれそつねならむ うゐのおくやまけふこえて あさきゆめみしゑひもせす", - "イロハニホヘト チリヌルヲ ワカヨタレソ ツネナラム ウヰノオクヤマ ケフコエテ アサキユメミシ ヱヒモセスン", - "? דג סקרן שט בים מאוכזב ולפתע מצא לו חברה איך הקליטה" - "Pchnąć w tę łódź jeża lub ośm skrzyń fig", - "В чащах юга жил бы цитрус? Да, но фальшивый экземпляр!", - "๏ เป็นมนุษย์สุดประเสริฐเลิศคุณค่า กว่าบรรดาฝูงสัตว์เดรัจฉาน", - "Pijamalı hasta, yağız şoföre çabucak güvendi." - ] - - last_err = -1 - error_map = { - 0: missing_key, - 1: oversize_error, - 2: empty_message - } - - ext_log_dir = "/var/log/azure/" - - total_counts = {} - - for ext_dir in os.listdir(ext_log_dir): - events_dir = os.path.join(ext_log_dir, ext_dir, "events") - # If specific extensions are provided, only add the events for them - if not os.path.isdir(events_dir) or (extension_names is not None and ext_dir not in extension_names): - continue - - new_opr_id = str(uuid.uuid4()) - event_list = [] - good_count = 0 - bad_count = 0 - - for _ in range(no_of_events_per_extension): - event = sample_ext_event.copy() - event["OperationId"] = new_opr_id - event["TimeStamp"] = datetime.utcnow().strftime(u'%Y-%m-%dT%H:%M:%S.%fZ') - event["Message"] = choice(sample_messages) - - if bad_count < bad_event_count: - # Make this event a bad event by cycling through the possible errors - last_err += 1 - reason = error_map[last_err % len(error_map)](event) - bad_count += 1 - - # Missing key error might delete the TaskName key from the event - if "TaskName" in event: - event["TaskName"] = "{0}. BTW a bad event: {1}".format(event["TaskName"], reason) - else: - event["EventLevel"] = "{0}. BTW a bad event: {1}".format(event["EventLevel"], reason) - else: - good_count += 1 - event_list.append(event) - - file_name = os.path.join(events_dir, '{0}.json'.format(int(time.time() * 1000000))) - with open("{0}.tmp".format(file_name), 'w+') as f: - json.dump(event_list, f) - - os.rename("{0}.tmp".format(file_name), file_name) - - counts = { - "good": good_count, - "bad": bad_count - } - - print("OperationId: {0}; Extension: {1}; Count: {2}".format(new_opr_id, ext_dir, counts)) - - if ext_dir in total_counts: - total_counts[ext_dir]['good'] += good_count - total_counts[ext_dir]['bad'] += bad_count - else: - total_counts[ext_dir] = counts - - return total_counts diff --git a/dcr/scenarios/extension-telemetry-pipeline/run.host.py b/dcr/scenarios/extension-telemetry-pipeline/run.host.py deleted file mode 100644 index e7f0d48002..0000000000 --- a/dcr/scenarios/extension-telemetry-pipeline/run.host.py +++ /dev/null @@ -1,19 +0,0 @@ -from dcr.scenario_utils.extensions.CustomScriptExtension import add_cse -from dcr.scenario_utils.extensions.VMAccessExtension import add_and_verify_vmaccess -from dcr.scenario_utils.test_orchestrator import TestFuncObj, TestOrchestrator - - -def main(): - tests = [ - TestFuncObj("Add Cse", lambda: add_cse(), raise_on_error=True), - TestFuncObj("Add VMAccess", lambda: add_and_verify_vmaccess(), raise_on_error=True) - ] - - test_orchestrator = TestOrchestrator("ETPTests-Host", tests=tests) - test_orchestrator.run_tests() - test_orchestrator.generate_report_on_orchestrator("test-results-etp-host.xml") - assert not test_orchestrator.failed, f"Test Suite: {test_orchestrator.name} failed" - - -if __name__ == '__main__': - main() diff --git a/dcr/scenarios/extension-telemetry-pipeline/run.py b/dcr/scenarios/extension-telemetry-pipeline/run.py deleted file mode 100644 index 3bff11e111..0000000000 --- a/dcr/scenarios/extension-telemetry-pipeline/run.py +++ /dev/null @@ -1,102 +0,0 @@ -import glob -import os -import random -import time - -from dcr.scenario_utils.agent_log_parser import parse_agent_log_file -from dcr.scenario_utils.check_waagent_log import is_data_in_waagent_log, check_waagent_log_for_errors -from dcr.scenario_utils.extensions.CustomScriptExtension import CustomScriptExtension -from dcr.scenario_utils.extensions.VMAccessExtension import VMAccessExtension -from dcr.scenario_utils.test_orchestrator import TestFuncObj -from dcr.scenario_utils.test_orchestrator import TestOrchestrator -from etp_helpers import add_extension_events_and_get_count, wait_for_extension_events_dir_empty, \ - get_collect_telemetry_thread_name - - -def add_good_extension_events_and_verify(extension_names): - max_events = random.randint(10, 50) - print("Creating a total of {0} events".format(max_events)) - ext_event_count = add_extension_events_and_get_count(no_of_events_per_extension=max_events, - extension_names=extension_names) - - # Ensure that the event collector ran after adding the events - wait_for_extension_events_dir_empty() - - # Sleep for a min to ensure that the TelemetryService has enough time to send events and report errors if any - time.sleep(60) - telemetry_event_collector_name = get_collect_telemetry_thread_name() - errors_reported = False - for agent_log_line in parse_agent_log_file(): - if agent_log_line.thread == telemetry_event_collector_name and agent_log_line.is_error: - if not errors_reported: - print( - f"waagent.log contains the following errors emitted by the {telemetry_event_collector_name} thread (none expected):") - errors_reported = True - print(agent_log_line.text.rstrip()) - - for ext_name in ext_event_count: - good_count = ext_event_count[ext_name]['good'] - is_data_in_waagent_log("Collected {0} events for extension: {1}".format(good_count, ext_name)) - - -def add_bad_events_and_verify_count(extension_names): - max_events = random.randint(15, 50) - print("Creating a total of {0} events".format(max_events)) - extension_event_count = add_extension_events_and_get_count(bad_event_count=random.randint(5, max_events - 5), - no_of_events_per_extension=max_events, - extension_names=extension_names) - - # Ensure that the event collector ran after adding the events - wait_for_extension_events_dir_empty() - - # Sleep for a min to ensure that the TelemetryService has enough time to send events and report errors if any - time.sleep(60) - - for ext_name in extension_event_count: - good_count = extension_event_count[ext_name]['good'] - is_data_in_waagent_log("Dropped events for Extension: {0}".format(ext_name)) - is_data_in_waagent_log("Collected {0} events for extension: {1}".format(good_count, ext_name)) - - -def verify_etp_enabled(): - # Assert from log if ETP is enabled - is_data_in_waagent_log('Extension Telemetry pipeline enabled: True') - - # Since ETP is enabled, events dir should have been created for all extensions - event_dirs = glob.glob(os.path.join("/var/log/azure/", "*", "events")) - assert event_dirs, "No extension event directories exist!" - - if not all(os.path.exists(event_dir) for event_dir in event_dirs): - raise AssertionError("Event directory not found for all extensions!") - - -def check_agent_log(): - # Since we're injecting bad events in the add_bad_events_and_verify_count() function test, - # we expect some warnings to be emitted by the agent. - # We're already verifying if these warnings are being emitted properly in the specified test, so ignoring those here. - ignore = [ - { - 'message': r"Dropped events for Extension: Microsoft\.(OSTCExtensions.VMAccessForLinux|Azure.Extensions.CustomScript); Details:", - 'if': lambda log_line: log_line.level == "WARNING" and log_line.thread == get_collect_telemetry_thread_name() - } - ] - check_waagent_log_for_errors(ignore=ignore) - - -if __name__ == '__main__': - - extensions_to_verify = [CustomScriptExtension.META_DATA.handler_name, VMAccessExtension.META_DATA.handler_name] - tests = [ - TestFuncObj("Verify ETP enabled", verify_etp_enabled, raise_on_error=True, retry=3), - TestFuncObj("Add Good extension events and verify", - lambda: add_good_extension_events_and_verify(extensions_to_verify)), - TestFuncObj("Add Bad extension events and verify", - lambda: add_bad_events_and_verify_count(extensions_to_verify)), - TestFuncObj("Verify all events processed", wait_for_extension_events_dir_empty), - TestFuncObj("Check Agent log", check_agent_log), - ] - - test_orchestrator = TestOrchestrator("ETPTests-VM", tests=tests) - test_orchestrator.run_tests() - test_orchestrator.generate_report_on_vm("test-result-etp-vm.xml") - assert not test_orchestrator.failed, f"Test Suite: {test_orchestrator.name} failed" diff --git a/dcr/scenarios/extension-telemetry-pipeline/setup.sh b/dcr/scenarios/extension-telemetry-pipeline/setup.sh deleted file mode 100644 index f9aa671182..0000000000 --- a/dcr/scenarios/extension-telemetry-pipeline/setup.sh +++ /dev/null @@ -1,20 +0,0 @@ -#!/usr/bin/env bash - -# 1 2 3 -# Usage: -set -euxo pipefail - -if systemctl status walinuxagent;then - agent="walinuxagent" -else - agent="waagent" -fi - -systemctl stop $agent -# Change ETP collection period for faster testing and turn on verbose -echo 'Debug.EtpCollectionPeriod=30' >> /etc/waagent.conf -sed -i 's/Logs.Verbose=n/Logs.Verbose=y/g' /etc/waagent.conf -# Moving the log to create a new fresh log for testing -mv /var/log/waagent.log /var/log/waagent.old.log -systemctl start $agent -systemctl status $agent diff --git a/dcr/scripts/__init__.py b/dcr/scripts/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/dcr/scripts/build_agent_zip.sh b/dcr/scripts/build_agent_zip.sh deleted file mode 100755 index a9747049bd..0000000000 --- a/dcr/scripts/build_agent_zip.sh +++ /dev/null @@ -1,16 +0,0 @@ -#!/usr/bin/env bash - -# https://linuxcommand.org/lc3_man_pages/seth.html -# -e Exit immediately if a command exits with a non-zero status. -# -u Treat unset variables as an error when substituting. -# -x Print commands and their arguments as they are executed. -# -o pipefail the return value of a pipeline is the status of the last command to exit with a non-zero status, -# or zero if no command exited with a non-zero status -set -euxo pipefail - -version=$(grep '^AGENT_VERSION' "$BUILD_SOURCESDIRECTORY/azurelinuxagent/common/version.py" | sed "s/.*'\([^']\+\)'.*/\1/") -# Azure Pipelines adds an extra quote at the end of the variable if we enable bash debugging as it prints an extra line - https://developercommunity.visualstudio.com/t/pipeline-variable-incorrectly-inserts-single-quote/375679 -set +x; echo "##vso[task.setvariable variable=agentVersion]$version"; set -x -sudo ./makepkg.py -sudo cp ./eggs/WALinuxAgent-$version.zip "$BUILD_SOURCESDIRECTORY/dcr" -sudo cp -r ./eggs/WALinuxAgent-$version "$BUILD_SOURCESDIRECTORY/dcr" \ No newline at end of file diff --git a/dcr/scripts/get_pypy.sh b/dcr/scripts/get_pypy.sh deleted file mode 100755 index f61dbe89dc..0000000000 --- a/dcr/scripts/get_pypy.sh +++ /dev/null @@ -1,21 +0,0 @@ -#!/usr/bin/env bash - -# https://linuxcommand.org/lc3_man_pages/seth.html -# -e Exit immediately if a command exits with a non-zero status. -# -u Treat unset variables as an error when substituting. -# -x Print commands and their arguments as they are executed. -# -o pipefail the return value of a pipeline is the status of the last command to exit with a non-zero status, -# or zero if no command exited with a non-zero status -set -euxo pipefail - -pushd "$BUILD_SOURCESDIRECTORY/dcr" -curl "https://downloads.python.org/pypy/pypy3.7-v7.3.5-linux64.tar.bz2" -o "pypy.tar.bz2" -mkdir "pypy" -tar xf "$BUILD_SOURCESDIRECTORY/dcr/pypy.tar.bz2" -C "pypy" -pypy_path=$(ls -d pypy/*/bin/pypy3) -rm -rf "pypy.tar.bz2" -popd - -# Azure Pipelines adds an extra quote at the end of the variable if we enable bash debugging as it prints an extra line - https://developercommunity.visualstudio.com/t/pipeline-variable-incorrectly-inserts-single-quote/375679 -set +x -echo "##vso[task.setvariable variable=pypyPath]/home/$ADMINUSERNAME/dcr/$pypy_path" \ No newline at end of file diff --git a/dcr/scripts/install_pip_packages.sh b/dcr/scripts/install_pip_packages.sh deleted file mode 100644 index 5fd563bbf5..0000000000 --- a/dcr/scripts/install_pip_packages.sh +++ /dev/null @@ -1,15 +0,0 @@ -#!/usr/bin/env bash - -# 1 2 3 -# Usage: - -# https://linuxcommand.org/lc3_man_pages/seth.html -# -e Exit immediately if a command exits with a non-zero status. -# -u Treat unset variables as an error when substituting. -# -x Print commands and their arguments as they are executed. -# -o pipefail the return value of a pipeline is the status of the last command to exit with a non-zero status, -# or zero if no command exited with a non-zero status -set -euxo pipefail - -$PYPYPATH -m ensurepip -$PYPYPATH -m pip install -r "$1" diff --git a/dcr/scripts/move_scenario.sh b/dcr/scripts/move_scenario.sh deleted file mode 100755 index 0ec6c73b53..0000000000 --- a/dcr/scripts/move_scenario.sh +++ /dev/null @@ -1,20 +0,0 @@ -#!/usr/bin/env bash - -# https://linuxcommand.org/lc3_man_pages/seth.html -# -e Exit immediately if a command exits with a non-zero status. -# -u Treat unset variables as an error when substituting. -# -x Print commands and their arguments as they are executed. -# -o pipefail the return value of a pipeline is the status of the last command to exit with a non-zero status, -# or zero if no command exited with a non-zero status -set -euxo pipefail - -# Delete all scenarios except for the one we're running in this VM -shopt -s extglob -pushd "$BUILD_SOURCESDIRECTORY/dcr/scenarios" -rm -rf !("$SCENARIONAME") -popd - -# Move contents of the remaining scenario to a directory called scenario -# This is done to be able to import the yml easily as importing a yml template can only be static, it cant be dynamic -mkdir "$BUILD_SOURCESDIRECTORY/dcr/scenario" -cp -r "$BUILD_SOURCESDIRECTORY/dcr/scenarios/$SCENARIONAME"/* "$BUILD_SOURCESDIRECTORY/dcr/scenario/" diff --git a/dcr/scripts/orchestrator/__init__.py b/dcr/scripts/orchestrator/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/dcr/scripts/orchestrator/execute_ssh_on_vm.py b/dcr/scripts/orchestrator/execute_ssh_on_vm.py deleted file mode 100644 index f10f5e4df5..0000000000 --- a/dcr/scripts/orchestrator/execute_ssh_on_vm.py +++ /dev/null @@ -1,61 +0,0 @@ -import asyncio -import os -import sys -import time - -from enum import Enum - -from dcr.scenario_utils.common_utils import execute_commands_concurrently_on_test_vms -from dcr.scenario_utils.logging_utils import get_logger - -logger = get_logger("dcr.scripts.orchestrator.execute_ssh_on_vm") - - -class SetupCommands: - setup_vm = "setup_vm" - fetch_results = "fetch_results" - harvest = "harvest" - - -async def run_tasks(command: str): - ssh_cmd = f'ssh -o StrictHostKeyChecking=no {{username}}@{{ip}}' - sources_dir = os.environ.get('BUILD_SOURCESDIRECTORY') - artifact_dir = os.environ.get('BUILD_ARTIFACTSTAGINGDIRECTORY') - - if command == SetupCommands.setup_vm: - dcr_root_dir = f"/home/{{username}}/dcr" - pypy_path = os.environ.get("PYPYPATH") - agent_version = os.environ.get("AGENTVERSION") - - setup_commands = [ - f"scp -o StrictHostKeyChecking=no -r {sources_dir}/dcr/ {{username}}@{{ip}}:~/", - f'{ssh_cmd} "sudo PYPYPATH={pypy_path} bash {dcr_root_dir}/scripts/install_pip_packages.sh {dcr_root_dir}/requirements.txt"', - f'{ssh_cmd} "sudo bash {dcr_root_dir}/scripts/setup_agent.sh {agent_version}"' - ] - return await execute_commands_concurrently_on_test_vms(commands=setup_commands, timeout=15) - elif command == SetupCommands.fetch_results: - commands = [ - f"scp -o StrictHostKeyChecking=no {{username}}@{{ip}}:~/test-result*.xml {artifact_dir}" - ] - try: - # Try fetching test results in a best effort scenario, if unable to fetch, dont throw an error - return await execute_commands_concurrently_on_test_vms(commands=commands, timeout=15) - except Exception as err: - logger.warning(f"Unable to fetch test results; Error: {err}", exc_info=True) - elif command == SetupCommands.harvest: - commands = [ - f"bash {sources_dir}/dcr/scripts/test-vm/harvest.sh {{username}} {{ip}} {artifact_dir}/harvest" - ] - return await execute_commands_concurrently_on_test_vms(commands=commands, timeout=15) - else: - cmd = f'{ssh_cmd} "{command}"' - return await execute_commands_concurrently_on_test_vms(commands=[cmd], timeout=15) - - -if __name__ == '__main__': - start_time = time.time() - print(f"Start Time: {start_time}") - try: - print(asyncio.run(run_tasks(command=sys.argv[1]))) - finally: - print(f"End time: {time.time()}; Duration: {time.time() - start_time} secs") diff --git a/dcr/scripts/orchestrator/generate_test_files.py b/dcr/scripts/orchestrator/generate_test_files.py deleted file mode 100644 index 527f7d8b97..0000000000 --- a/dcr/scripts/orchestrator/generate_test_files.py +++ /dev/null @@ -1,36 +0,0 @@ -import glob -import os -import shutil -import sys - -from junitparser import JUnitXml - -from dcr.scenario_utils.logging_utils import get_logger - -logger = get_logger("dcr.scripts.orchestrator.generate_test_files") - - -def merge_xml_files(test_file_pattern): - xml_data = JUnitXml() - staging_dir = os.environ['BUILD_ARTIFACTSTAGINGDIRECTORY'] - - for test_file in glob.glob(test_file_pattern): - xml_data += JUnitXml.fromfile(test_file) - # Move file to harvest dir to save state and not publish the same test twice - shutil.move(test_file, os.path.join(staging_dir, "harvest", os.path.basename(test_file))) - - if xml_data.tests > 0: - # Merge all files into a single file for cleaner output - output_file_name = f"test-results-{os.environ['SCENARIONAME']}-{os.environ['DISTRONAME']}.xml" - xml_data.write(os.path.join(staging_dir, output_file_name)) - else: - logger.info(f"No test files found for pattern: {test_file_pattern}") - - -if __name__ == "__main__": - try: - merge_xml_files(test_file_pattern=sys.argv[1]) - except Exception as err: - logger.exception( - f"Ran into error when trying to merge test cases. Ignoring the rest: {err}") - diff --git a/dcr/scripts/orchestrator/set_environment.py b/dcr/scripts/orchestrator/set_environment.py deleted file mode 100644 index ff511f1688..0000000000 --- a/dcr/scripts/orchestrator/set_environment.py +++ /dev/null @@ -1,64 +0,0 @@ -import json -import os.path - -from dcr.scenario_utils.logging_utils import get_logger - -logger = get_logger("dcr.script.orchestrator.set_environment") -add_variable_to_pipeline = '##vso[task.setvariable variable={name};]{value}' - - -def _check_if_file_in_scenario_and_set_variable(file_name: str, name: str, true_value: str, false_val: str = None): - """ - We have certain scenarios in the tests where we determine what type of test to run based on the availability of the file. - Check if file is present in the current scenario, and if so, set the variable name. - Syntax for setting the variable : https://docs.microsoft.com/en-us/azure/devops/pipelines/scripts/logging-commands?view=azure-devops&tabs=bash#setvariable-initialize-or-modify-the-value-of-a-variable - Eg: echo "##vso[task.setvariable variable=;]" - """ - file_path = os.path.join(scenario_path, file_name) - if os.path.exists(file_path): - logger.info(f"Found file: {file_path}, setting variable: {name}") - print(add_variable_to_pipeline.format(name=name, value=true_value)) - elif false_val is not None: - print(add_variable_to_pipeline.format(name=name, value=false_val)) - - -def _override_config(): - """ - This function reads the config.json file present in the scenario and makes all the variables available to the whole - job as environment variables. - It also overrides existing variables with the same name if available. - Note: This function expects config.json to be a flat JSON - """ - config_path = os.path.join(scenario_path, "config.json") - if not os.path.exists(config_path): - logger.info(f"Config file: {config_path} not available") - return - - with open(config_path, encoding="utf-8") as config_fh: - config_data = json.load(config_fh) - for key, val in config_data.items(): - print(add_variable_to_pipeline.format(name=key, value=val)) - - -if __name__ == '__main__': - """ - This script sets the environment for the current job. - It determines what files to run and what not. - Eg: If we're supposed to run run.host.py or run.py - """ - __dcr_dir = os.path.join(os.environ.get("BUILD_SOURCESDIRECTORY"), "dcr") - scenario_path = os.path.join(__dcr_dir, "scenario") - template_dir = os.path.join(__dcr_dir, "templates") - - _check_if_file_in_scenario_and_set_variable(file_name="run.py", name="runPy", true_value="true") - _check_if_file_in_scenario_and_set_variable(file_name="run.host.py", name="runHost", true_value="true") - _check_if_file_in_scenario_and_set_variable(file_name="setup.sh", name="runScenarioSetup", true_value="true") - _check_if_file_in_scenario_and_set_variable(file_name="template.json", name="templateFile", - true_value=os.path.join(scenario_path, "template.json"), - false_val=os.path.join(template_dir, "deploy-linux-vm.json")) - _check_if_file_in_scenario_and_set_variable(file_name="parameters.json", name="parametersFile", - true_value=os.path.join(scenario_path, "parameters.json"), - false_val=os.path.join(template_dir, "deploy-linux-vm-params.json")) - - # Check if config.json exists and add to environment - _override_config() diff --git a/dcr/scripts/setup_agent.sh b/dcr/scripts/setup_agent.sh deleted file mode 100644 index 1e57694727..0000000000 --- a/dcr/scripts/setup_agent.sh +++ /dev/null @@ -1,34 +0,0 @@ -#!/usr/bin/env bash - -# https://linuxcommand.org/lc3_man_pages/seth.html -# -e Exit immediately if a command exits with a non-zero status. -# -u Treat unset variables as an error when substituting. -# -x Print commands and their arguments as they are executed. -# -o pipefail the return value of a pipeline is the status of the last command to exit with a non-zero status, -# or zero if no command exited with a non-zero status -set -euxo pipefail - -# $1 $2 $3 $4 $5 $6 $7 -# Usage: AgentVersion - -# Copy agent zip file to /var/lib/waagent to force it to auto-update -[ -z "$1" ] && version="9.9.9.9" || version=$1 - -if systemctl status walinuxagent;then - agent="walinuxagent" -else - agent="waagent" -fi - -sudo systemctl stop $agent - -# We need to force the agent to AutoUpdate to enable our testing -sed -i 's/AutoUpdate.Enabled=n/AutoUpdate.Enabled=y/g' /etc/waagent.conf -# Move the older agent log file to ensure we have a clean slate when testing agent logs -mv /var/log/waagent.log /var/log/waagent.old.log - -sudo cp -r ./dcr/*-$version /var/lib/waagent -sudo systemctl daemon-reload && sudo systemctl start $agent - -sudo systemctl status $agent --no-pager -waagent --version \ No newline at end of file diff --git a/dcr/scripts/test-vm/harvest.sh b/dcr/scripts/test-vm/harvest.sh deleted file mode 100644 index d1715ee59d..0000000000 --- a/dcr/scripts/test-vm/harvest.sh +++ /dev/null @@ -1,20 +0,0 @@ -#!/usr/bin/env bash -# 1 2 3 -# Usage: - -# https://linuxcommand.org/lc3_man_pages/seth.html -# -e Exit immediately if a command exits with a non-zero status. -# -u Treat unset variables as an error when substituting. -# -x Print commands and their arguments as they are executed. -# -o pipefail the return value of a pipeline is the status of the last command to exit with a non-zero status, -# or zero if no command exited with a non-zero status -set -euxo pipefail - -ssh -o "StrictHostKeyChecking no" "$1"@"$2" "sudo tar --exclude='journal/*' --exclude='omsbundle' --exclude='omsagent' --exclude='mdsd' --exclude='scx*' --exclude='*.so' --exclude='*__LinuxDiagnostic__*' --exclude='*.zip' --exclude='*.deb' --exclude='*.rpm' -czf logs-$2.tgz /var/log /var/lib/waagent/ /etc/waagent.conf" -# Some distros do not have "other" permissions (e.g., mariner1.0), so change the -# owning user so we can grab them below (during the scp command). -ssh -o "StrictHostKeyChecking no" "$1"@"$2" "sudo chown $1 logs-$2.tgz" - -# Create directory if doesn't exist -mkdir -p "$3" -scp -o "StrictHostKeyChecking no" "$1@$2:logs-$2.tgz" "$3/logs-$2.tgz" \ No newline at end of file diff --git a/dcr/templates/arm-delete.yml b/dcr/templates/arm-delete.yml deleted file mode 100644 index 047b345485..0000000000 --- a/dcr/templates/arm-delete.yml +++ /dev/null @@ -1,33 +0,0 @@ -parameters: - - name: scenarios - type: object - - - name: distros - type: object - - - name: rgPrefix - type: string - -jobs: - - job: "DeleteRG" - dependsOn: "Wait" - condition: always() - strategy: - matrix: - ${{ each distro in parameters.distros }}: - ${{ each scenario in parameters.scenarios }}: - ${{ format('{0}-{1}', distro.name, scenario) }}: - scenarioName: ${{ scenario }} - distroName: ${{ distro.name }} - rgName: ${{ format('{0}-{1}-{2}', parameters.rgPrefix, scenario, distro.name) }} - maxParallel: 50 - - steps: - - task: AzureResourceManagerTemplateDeployment@3 - displayName: "Delete test RG" - inputs: - deploymentScope: 'Resource Group' - azureResourceManagerConnection: '$(azureConnection)' - subscriptionId: '$(subId)' - action: 'DeleteRG' - resourceGroupName: '$(rgName)' \ No newline at end of file diff --git a/dcr/templates/deploy-linux-vm-params.json b/dcr/templates/deploy-linux-vm-params.json deleted file mode 100644 index aa29215e32..0000000000 --- a/dcr/templates/deploy-linux-vm-params.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", - "contentVersion": "1.0.0.0", - "parameters": { - "vmName": { - "value": "simpleLinuxVM" - } - } -} \ No newline at end of file diff --git a/dcr/templates/deploy-linux-vm.json b/dcr/templates/deploy-linux-vm.json deleted file mode 100644 index ecff1d62a0..0000000000 --- a/dcr/templates/deploy-linux-vm.json +++ /dev/null @@ -1,280 +0,0 @@ -{ - "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", - "contentVersion": "1.0.0.0", - "metadata": { - "_generator": { - "name": "bicep", - "version": "0.4.1.14562", - "templateHash": "16607361201936431976" - } - }, - "parameters": { - "vmName": { - "type": "string", - "defaultValue": "simpleLinuxVM", - "metadata": { - "description": "The name of your Virtual Machine." - } - }, - "adminUsername": { - "type": "string", - "metadata": { - "description": "Username for the Virtual Machine." - } - }, - "authenticationType": { - "type": "string", - "defaultValue": "sshPublicKey", - "allowedValues": [ - "sshPublicKey", - "password" - ], - "metadata": { - "description": "Type of authentication to use on the Virtual Machine. SSH key is recommended." - } - }, - "adminPasswordOrKey": { - "type": "secureString", - "metadata": { - "description": "SSH Key or password for the Virtual Machine. SSH key is recommended." - } - }, - "dnsLabelPrefix": { - "type": "string", - "defaultValue": "[toLower(format('simplelinuxvm-{0}', uniqueString(resourceGroup().id)))]", - "metadata": { - "description": "Unique DNS Name for the Public IP used to access the Virtual Machine." - } - }, - "imagePublisher": { - "type": "string", - "defaultValue": "Canonical" - }, - "imageOffer": { - "type": "string", - "defaultValue": "UbuntuServer" - }, - "imageVersion": { - "type": "string", - "defaultValue": "latest" - }, - "imageSku": { - "type": "string", - "defaultValue": "18.04-LTS" - }, - "location": { - "type": "string", - "defaultValue": "[resourceGroup().location]", - "metadata": { - "description": "Location for all resources." - } - }, - "vmSize": { - "type": "string", - "defaultValue": "Standard_B2s", - "metadata": { - "description": "The size of the VM" - } - }, - "virtualNetworkName": { - "type": "string", - "defaultValue": "vNet", - "metadata": { - "description": "Name of the VNET" - } - }, - "subnetName": { - "type": "string", - "defaultValue": "Subnet", - "metadata": { - "description": "Name of the subnet in the virtual network" - } - }, - "networkSecurityGroupName": { - "type": "string", - "defaultValue": "SecGroupNet", - "metadata": { - "description": "Name of the Network Security Group" - } - } - }, - "functions": [], - "variables": { - "publicIPAddressName": "[format('{0}PublicIP', parameters('vmName'))]", - "networkInterfaceName": "[format('{0}NetInt', parameters('vmName'))]", - "osDiskType": "Standard_LRS", - "subnetAddressPrefix": "10.1.0.0/24", - "addressPrefix": "10.1.0.0/16", - "linuxConfiguration": { - "disablePasswordAuthentication": true, - "ssh": { - "publicKeys": [ - { - "path": "[format('/home/{0}/.ssh/authorized_keys', parameters('adminUsername'))]", - "keyData": "[parameters('adminPasswordOrKey')]" - } - ] - } - } - }, - "resources": [ - { - "type": "Microsoft.Network/networkInterfaces", - "apiVersion": "2020-06-01", - "name": "[variables('networkInterfaceName')]", - "location": "[parameters('location')]", - "properties": { - "ipConfigurations": [ - { - "name": "ipconfig1", - "properties": { - "subnet": { - "id": "[resourceId('Microsoft.Network/virtualNetworks/subnets', parameters('virtualNetworkName'), parameters('subnetName'))]" - }, - "privateIPAllocationMethod": "Dynamic", - "publicIPAddress": { - "id": "[resourceId('Microsoft.Network/publicIPAddresses', variables('publicIPAddressName'))]" - } - } - } - ], - "networkSecurityGroup": { - "id": "[resourceId('Microsoft.Network/networkSecurityGroups', parameters('networkSecurityGroupName'))]" - } - }, - "dependsOn": [ - "[resourceId('Microsoft.Network/networkSecurityGroups', parameters('networkSecurityGroupName'))]", - "[resourceId('Microsoft.Network/publicIPAddresses', variables('publicIPAddressName'))]", - "[resourceId('Microsoft.Network/virtualNetworks/subnets', parameters('virtualNetworkName'), parameters('subnetName'))]" - ] - }, - { - "type": "Microsoft.Network/networkSecurityGroups", - "apiVersion": "2020-06-01", - "name": "[parameters('networkSecurityGroupName')]", - "location": "[parameters('location')]", - "properties": { - "securityRules": [ - { - "name": "SSH_service_tag", - "properties": { - "priority": 100, - "protocol": "Tcp", - "access": "Allow", - "direction": "Inbound", - "sourceAddressPrefix": "*", - "sourcePortRange": "*", - "destinationAddressPrefix": "*", - "destinationPortRange": "22" - } - } - ] - } - }, - { - "type": "Microsoft.Network/virtualNetworks", - "apiVersion": "2020-06-01", - "name": "[parameters('virtualNetworkName')]", - "location": "[parameters('location')]", - "dependsOn": [ - "[concat('Microsoft.Network/networkSecurityGroups/', parameters('networkSecurityGroupName'))]" - ], - "properties": { - "addressSpace": { - "addressPrefixes": [ - "[variables('addressPrefix')]" - ] - } - } - }, - { - "type": "Microsoft.Network/virtualNetworks/subnets", - "apiVersion": "2020-06-01", - "name": "[format('{0}/{1}', parameters('virtualNetworkName'), parameters('subnetName'))]", - "properties": { - "addressPrefix": "[variables('subnetAddressPrefix')]", - "privateEndpointNetworkPolicies": "Enabled", - "privateLinkServiceNetworkPolicies": "Enabled", - "networkSecurityGroup": { - "id": "[resourceId('Microsoft.Network/networkSecurityGroups', parameters('networkSecurityGroupName'))]" - } - }, - "dependsOn": [ - "[resourceId('Microsoft.Network/virtualNetworks', parameters('virtualNetworkName'))]" - ] - }, - { - "type": "Microsoft.Network/publicIPAddresses", - "apiVersion": "2020-06-01", - "name": "[variables('publicIPAddressName')]", - "location": "[parameters('location')]", - "sku": { - "name": "Basic" - }, - "properties": { - "publicIPAllocationMethod": "Dynamic", - "publicIPAddressVersion": "IPv4", - "dnsSettings": { - "domainNameLabel": "[parameters('dnsLabelPrefix')]" - }, - "idleTimeoutInMinutes": 4 - } - }, - { - "type": "Microsoft.Compute/virtualMachines", - "apiVersion": "2020-06-01", - "name": "[parameters('vmName')]", - "location": "[parameters('location')]", - "properties": { - "hardwareProfile": { - "vmSize": "[parameters('vmSize')]" - }, - "storageProfile": { - "osDisk": { - "createOption": "FromImage", - "managedDisk": { - "storageAccountType": "[variables('osDiskType')]" - }, - "diskSizeGB": 32 - }, - "imageReference": { - "publisher": "[parameters('imagePublisher')]", - "offer": "[parameters('imageOffer')]", - "sku": "[parameters('imageSku')]", - "version": "[parameters('imageVersion')]" - } - }, - "networkProfile": { - "networkInterfaces": [ - { - "id": "[resourceId('Microsoft.Network/networkInterfaces', variables('networkInterfaceName'))]" - } - ] - }, - "osProfile": { - "computerName": "[parameters('vmName')]", - "adminUsername": "[parameters('adminUsername')]", - "adminPassword": "[parameters('adminPasswordOrKey')]", - "linuxConfiguration": "[if(equals(parameters('authenticationType'), 'password'), null(), variables('linuxConfiguration'))]" - } - }, - "dependsOn": [ - "[resourceId('Microsoft.Network/networkInterfaces', variables('networkInterfaceName'))]" - ] - } - ], - "outputs": { - "adminUsername": { - "type": "string", - "value": "[parameters('adminUsername')]" - }, - "hostname": { - "type": "string", - "value": "[reference(resourceId('Microsoft.Network/publicIPAddresses', variables('publicIPAddressName'))).dnsSettings.fqdn]" - }, - "sshCommand": { - "type": "string", - "value": "[format('ssh {0}@{1}', parameters('adminUsername'), reference(resourceId('Microsoft.Network/publicIPAddresses', variables('publicIPAddressName'))).dnsSettings.fqdn)]" - } - } -} \ No newline at end of file diff --git a/dcr/templates/setup-vm-and-execute-tests.yml b/dcr/templates/setup-vm-and-execute-tests.yml deleted file mode 100644 index b5779d7dbe..0000000000 --- a/dcr/templates/setup-vm-and-execute-tests.yml +++ /dev/null @@ -1,207 +0,0 @@ -parameters: - - name: scenarios - type: object - - - name: distros - type: object - - - name: rgPrefix - type: string - -jobs: - - job: "CreateVM" - displayName: "Setup VM and Run Test" - - strategy: - matrix: - ${{ each distro in parameters.distros }}: - ${{ each scenario in parameters.scenarios }}: - ${{ format('{0}-{1}', distro.name, scenario) }}: - scenarioName: ${{ scenario }} - imagePublisher: ${{ distro.publisher }} - imageOffer: ${{ distro.offer }} - imageSku: ${{ distro.sku }} - imageVersion: ${{ distro.version }} - distroName: ${{ distro.name }} - distroSetupPath: ${{ distro.setupPath }} - rgName: ${{ format('{0}-{1}-{2}', parameters.rgPrefix, scenario, distro.name) }} - maxParallel: 50 - - steps: - - task: InstallSSHKey@0 - displayName: 'Install SSH Key to agent' - name: "InstallKey" - inputs: - knownHostsEntry: 'github.com $(SSH_PUBLIC)' # Adding a dummy known host for github.com as leaving it empty is not allowed by this task - sshPublicKey: '$(SSH_PUBLIC)' - sshKeySecureFile: 'id_rsa' - - - task: AzureKeyVault@2 - displayName: "Fetch secrets from KV" - inputs: - azureSubscription: '$(azureConnection)' - KeyVaultName: 'dcrV2SPs' - SecretsFilter: '*' - RunAsPreJob: true - - - task: UsePythonVersion@0 - displayName: "Set host python version" - inputs: - versionSpec: '3.7' - addToPath: true - architecture: 'x64' - - - script: | - mkdir -p "$(Build.ArtifactStagingDirectory)/harvest" - displayName: "Create harvest directories" - - - bash: $(Build.SourcesDirectory)/dcr/scripts/build_agent_zip.sh - displayName: "Build Agent Zip" - - - bash: $(Build.SourcesDirectory)/dcr/scripts/get_pypy.sh - displayName: "Get PyPy" - - - bash: $(Build.SourcesDirectory)/dcr/scripts/move_scenario.sh - displayName: "Move scenarios" - - - script: pip install -r $(Build.SourcesDirectory)/dcr/requirements.txt - displayName: "Install pip modules on orchestrator" - - - task: PythonScript@0 - inputs: - scriptSource: 'filePath' - scriptPath: '$(Build.SourcesDirectory)/dcr/scripts/orchestrator/set_environment.py' - env: - PYTHONPATH: $(Build.SourcesDirectory) - displayName: "Set Environment" - - - task: AzureResourceManagerTemplateDeployment@3 - name: "deployVM" - timeoutInMinutes: 10 - inputs: - deploymentScope: 'Resource Group' - azureResourceManagerConnection: '$(azureConnection)' - subscriptionId: '$(subId)' - action: 'Create Or Update Resource Group' - resourceGroupName: '$(rgName)' - location: '$(location)' - templateLocation: 'Linked artifact' - csmFile: '$(templateFile)' - csmParametersFile: '$(parametersFile)' - overrideParameters: '-vmName "$(vmName)" -adminUsername "$(adminUsername)" -adminPasswordOrKey "$(SSH_PUBLIC)" -imagePublisher "$(imagePublisher)" -imageOffer "$(imageOffer)" -imageSku $(imageSku) -imageVersion $(imageVersion)' - deploymentMode: 'Complete' - deploymentOutputs: 'armDeploymentOutput' - - - task: AzureCLI@2 - displayName: "Get VMIp" - inputs: - azureSubscription: '$(azureConnection)' - scriptType: 'bash' - scriptLocation: 'inlineScript' - inlineScript: | - az vm list-ip-addresses --resource-group $(rgName) --name $(vmName) --query "[].virtualMachine.network.publicIpAddresses[0].ipAddress" --output tsv > $(Build.SourcesDirectory)/dcr/.vm_ips || echo "No VM Ips" - az vmss list-instance-public-ips --name $(vmName) --resource-group $(rgName) --query "[].ipAddress" --output tsv > $(Build.SourcesDirectory)/dcr/.vmss_ips || echo "No VMSS IPs" - - - script: | - printenv > $(Build.SourcesDirectory)/dcr/.env - displayName: 'Get all environment variables' - name: 'setOutputVars' - - - task: PythonScript@0 - inputs: - scriptSource: 'filePath' - scriptPath: '$(Build.SourcesDirectory)/dcr/scripts/orchestrator/execute_ssh_on_vm.py' - arguments: 'setup_vm' - env: - PYTHONPATH: $(Build.SourcesDirectory) - displayName: "Setup test VM" - - - task: PythonScript@0 - inputs: - scriptSource: 'filePath' - scriptPath: '$(Build.SourcesDirectory)/dcr/scripts/orchestrator/execute_ssh_on_vm.py' - arguments: '"sudo bash /home/$(adminUsername)/$(distroSetupPath)"' - env: - PYTHONPATH: $(Build.SourcesDirectory) - condition: and(succeeded(), not(eq(variables.distroSetupPath, ''))) - displayName: 'Execute Distro Setup on test VM' - - - task: PythonScript@0 - name: "runScenarioSetup" - inputs: - scriptSource: 'filePath' - scriptPath: '$(Build.SourcesDirectory)/dcr/scripts/orchestrator/execute_ssh_on_vm.py' - arguments: '"sudo bash /home/$(adminUsername)/dcr/scenario/setup.sh"' - env: - PYTHONPATH: $(Build.SourcesDirectory) - condition: and(succeeded(), eq(variables.runScenarioSetup, 'true')) - displayName: "Execute Scenario Setup on test VM" - - # This task is needed to ensure we execute the following tasks even if a single one of them fails - - bash: echo "##vso[task.setvariable variable=executeTests]true" - displayName: "Start executing tests" - - - task: PythonScript@0 - inputs: - scriptSource: 'filePath' - scriptPath: '$(Build.SourcesDirectory)/dcr/scenario/run.host.py' - env: - PYTHONPATH: $(Build.SourcesDirectory) - # Add all KeyVault secrets explicitly as they're not added by default to the environment vars - AZURE_CLIENT_ID: $(AZURE-CLIENT-ID) - AZURE_CLIENT_SECRET: $(AZURE-CLIENT-SECRET) - AZURE_TENANT_ID: $(AZURE-TENANT-ID) - displayName: "Run the test file on the Orchestrator" - condition: and(eq(variables.executeTests, 'true'), eq(variables.runHost, 'true')) - - - task: PythonScript@0 - inputs: - scriptSource: 'filePath' - scriptPath: '$(Build.SourcesDirectory)/dcr/scripts/orchestrator/execute_ssh_on_vm.py' - arguments: '"sudo PYTHONPATH=. $(pypyPath) dcr/scenario/run.py"' - env: - PYTHONPATH: $(Build.SourcesDirectory) - condition: and(eq(variables.executeTests, 'true'), eq(variables.runPy, 'true')) - displayName: "Execute test suite on VM" - - - task: PythonScript@0 - inputs: - scriptSource: 'filePath' - scriptPath: '$(Build.SourcesDirectory)/dcr/scripts/orchestrator/execute_ssh_on_vm.py' - arguments: 'fetch_results' - env: - PYTHONPATH: $(Build.SourcesDirectory) - condition: eq(variables.executeTests, 'true') - displayName: 'Fetch test results' - - - task: PythonScript@0 - inputs: - scriptSource: 'filePath' - scriptPath: '$(Build.SourcesDirectory)/dcr/scripts/orchestrator/generate_test_files.py' - arguments: '"$(Build.ArtifactStagingDirectory)/test-result*.xml"' - env: - PYTHONPATH: $(Build.SourcesDirectory) - condition: eq(variables.executeTests, 'true') - displayName: 'Merge test results' - - - task: PublishTestResults@2 - condition: eq(variables.executeTests, 'true') - inputs: - testResultsFormat: 'JUnit' - testResultsFiles: '$(Build.ArtifactStagingDirectory)/test-result*.xml' - testRunTitle: 'Publish test results for $(scenarioName)-$(distroName)' - - - task: PythonScript@0 - inputs: - scriptSource: 'filePath' - scriptPath: '$(Build.SourcesDirectory)/dcr/scripts/orchestrator/execute_ssh_on_vm.py' - arguments: 'harvest' - env: - PYTHONPATH: $(Build.SourcesDirectory) - condition: and(failed(), eq(variables.executeTests, 'true')) - displayName: 'Fetch Harvest results' - - - publish: $(Build.ArtifactStagingDirectory)/harvest - artifact: $(rgName)-harvest - condition: and(failed(), eq(variables.executeTests, 'true')) - displayName: 'Publish Harvest logs' diff --git a/dcr/templates/vars.yml b/dcr/templates/vars.yml deleted file mode 100644 index 4efb0c9a58..0000000000 --- a/dcr/templates/vars.yml +++ /dev/null @@ -1,21 +0,0 @@ -# Template file for the common variables between the 2 pipelines - -variables: - rgPrefix: 'dcr-v2-test' - - vmName: 'dcrLinuxVM' - adminUsername: 'dcr' - - # Public Cloud Data - azureConnection: 'azuremanagement' - subId: '8e037ad4-618f-4466-8bc8-5099d41ac15b' - location: 'East US 2' - - # ToDo: Create new pipelines for Fairfax and Mooncake - fairfaxConn: 'VMGuestAgentAndExtensionsFairfax (8e5abcac-74f0-4955-9dfb-fe3fe36f8d19)' - fairfaxSub: '8e5abcac-74f0-4955-9dfb-fe3fe36f8d19' - fairfaxLocation: 'usgovarizona' - - mooncakeConn: 'Guest Agent Mooncake ( 557a8daa-8ac8-4caa-88e4-3b6f939978b9 )' - mooncakeSub: '557a8daa-8ac8-4caa-88e4-3b6f939978b9' - mooncakeLocation: 'china north 2' \ No newline at end of file From 9e6f64e298b60d779c9e3333d37dae5490a078af Mon Sep 17 00:00:00 2001 From: Norberto Arrieta Date: Thu, 16 Feb 2023 09:35:45 -0800 Subject: [PATCH 42/63] Add support for VHDs; add image, location and vm_size as parameters to the pipeline (#2758) * Add support for VHDs; add image, location and vm_size as parameters to the pipeline --------- Co-authored-by: narrieta --- .../orchestrator/lib/agent_test_suite.py | 13 +- .../lib/agent_test_suite_combinator.py | 126 +++++++++++++----- tests_e2e/orchestrator/runbook.yml | 45 +++++-- tests_e2e/pipeline/pipeline.yml | 30 ++++- tests_e2e/pipeline/scripts/execute_tests.sh | 17 ++- 5 files changed, 180 insertions(+), 51 deletions(-) diff --git a/tests_e2e/orchestrator/lib/agent_test_suite.py b/tests_e2e/orchestrator/lib/agent_test_suite.py index 54551541b4..0f663a8bac 100644 --- a/tests_e2e/orchestrator/lib/agent_test_suite.py +++ b/tests_e2e/orchestrator/lib/agent_test_suite.py @@ -112,6 +112,7 @@ def __init__(self, vm: VmIdentifier, paths: AgentTestContext.Paths, connection: self.node: Node = None self.runbook_name: str = None self.image_name: str = None + self.is_vhd: bool = None self.test_suites: List[AgentTestSuite] = None self.collect_logs: str = None self.skip_setup: bool = None @@ -146,8 +147,9 @@ def _set_context(self, node: Node, variables: Dict[str, Any], log: Logger): self.__context.log = log self.__context.node = node - self.__context.image_name = f"{runbook.marketplace.offer}-{runbook.marketplace.sku}" - self.__context.test_suites = self._get_required_parameter(variables, "test_suites_info") + self.__context.is_vhd = self._get_required_parameter(variables, "c_vhd") != "" + self.__context.image_name = f"{node.os.name}-vhd" if self.__context.is_vhd else f"{runbook.marketplace.offer}-{runbook.marketplace.sku}" + self.__context.test_suites = self._get_required_parameter(variables, "c_test_suites") self.__context.collect_logs = self._get_required_parameter(variables, "collect_logs") self.__context.skip_setup = self._get_required_parameter(variables, "skip_setup") @@ -249,7 +251,10 @@ def _setup_node(self) -> None: self._log.info("Resource Group: %s", self.context.vm.resource_group) self._log.info("") - self._install_agent_on_node() + if self.context.is_vhd: + self._log.info("Using a VHD; will not install the test Agent.") + else: + self._install_agent_on_node() def _install_agent_on_node(self) -> None: """ @@ -292,7 +297,7 @@ def _collect_node_logs(self) -> None: @TestCaseMetadata(description="", priority=0) def agent_test_suite(self, node: Node, variables: Dict[str, Any], log: Logger) -> None: """ - Executes each of the AgentTests included in "test_suites_info" variable (which is generated by the AgentTestSuitesCombinator). + Executes each of the AgentTests included in the "c_test_suites" variable (which is generated by the AgentTestSuitesCombinator). """ self._set_context(node, variables, log) diff --git a/tests_e2e/orchestrator/lib/agent_test_suite_combinator.py b/tests_e2e/orchestrator/lib/agent_test_suite_combinator.py index 8cf91dc5a2..39fb104587 100644 --- a/tests_e2e/orchestrator/lib/agent_test_suite_combinator.py +++ b/tests_e2e/orchestrator/lib/agent_test_suite_combinator.py @@ -1,6 +1,8 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT license. import logging +import re +import urllib.parse from dataclasses import dataclass, field from typing import Any, Dict, List, Optional, Type @@ -15,7 +17,7 @@ from lisa.combinator import Combinator # pylint: disable=E0401 from lisa.util import field_metadata # pylint: disable=E0401 -from tests_e2e.orchestrator.lib.agent_test_loader import AgentTestLoader +from tests_e2e.orchestrator.lib.agent_test_loader import AgentTestLoader, VmImageInfo @dataclass_json() @@ -24,6 +26,15 @@ class AgentTestSuitesCombinatorSchema(schema.Combinator): test_suites: str = field( default_factory=str, metadata=field_metadata(required=True) ) + image: str = field( + default_factory=str, metadata=field_metadata(required=True) + ) + location: str = field( + default_factory=str, metadata=field_metadata(required=True) + ) + vm_size: str = field( + default_factory=str, metadata=field_metadata(required=True) + ) class AgentTestSuitesCombinator(Combinator): @@ -31,19 +42,19 @@ class AgentTestSuitesCombinator(Combinator): The "agent_test_suites" combinator returns a list of items containing five variables that specify the environments that the agent test suites must be executed on: - * marketplace_image: e.g. "Canonical UbuntuServer 18.04-LTS latest", - * location: e.g. "westus2", - * vm_size: e.g. "Standard_D2pls_v5" - * vhd: e.g "https://rhel.blob.core.windows.net/images/RHEL_8_Standard-8.3.202006170423.vhd?se=..." - * test_suites_info: e.g. [AgentBvt, FastTrack] + * c_marketplace_image: e.g. "Canonical UbuntuServer 18.04-LTS latest", + * c_location: e.g. "westus2", + * c_vm_size: e.g. "Standard_D2pls_v5" + * c_vhd: e.g "https://rhel.blob.core.windows.net/images/RHEL_8_Standard-8.3.202006170423.vhd?se=..." + * c_test_suites: e.g. [AgentBvt, FastTrack] - (marketplace_image, location, vm_size) and vhd are mutually exclusive and define the environment (i.e. the test VM) - in which the test will be executed. test_suites_info defines the test suites that should be executed in that + (c_marketplace_image, c_location, c_vm_size) and vhd are mutually exclusive and define the environment (i.e. the test VM) + in which the test will be executed. c_test_suites defines the test suites that should be executed in that environment. """ def __init__(self, runbook: AgentTestSuitesCombinatorSchema) -> None: super().__init__(runbook) - self._environments = self.create_environment_list(self.runbook.test_suites) + self._environments = self.create_environment_list() self._index = 0 @classmethod @@ -63,47 +74,87 @@ def _next(self) -> Optional[Dict[str, Any]]: _DEFAULT_LOCATION = "westus2" - @staticmethod - def create_environment_list(test_suites: str) -> List[Dict[str, Any]]: + def create_environment_list(self) -> List[Dict[str, Any]]: + loader = AgentTestLoader(self.runbook.test_suites) + + # + # If the runbook provides any of 'image', 'location', or 'vm_size', those values + # override any configuration values on the test suite. + # + # Check 'images' first and add them to 'runbook_images', if any + # + if self.runbook.image == "": + runbook_images = [] + else: + runbook_images = loader.images.get(self.runbook.image) + if runbook_images is None: + if not self._is_urn(self.runbook.image) and not self._is_vhd(self.runbook.image): + raise Exception(f"The 'image' parameter must be an image or image set name, a urn, or a vhd: {self.runbook.image}") + i = VmImageInfo() + i.urn = self.runbook.image # Note that this could be a URN or the URI for a VHD + i.locations = [] + i.vm_sizes = [] + runbook_images = [i] + + # + # Now walk through all the test_suites and create a list of the environments (test VMs) that need to be created. + # environment_list: List[Dict[str, Any]] = [] shared_environments: Dict[str, Dict[str, Any]] = {} - loader = AgentTestLoader(test_suites) - for suite_info in loader.test_suites: - images_info = loader.images[suite_info.images] + images_info = runbook_images if len(runbook_images) > 0 else loader.images[suite_info.images] + for image in images_info: - # If the suite specifies a location, use it. Else, if the image specifies a list of locations, use - # any of them. Otherwise, use the default location. - if suite_info.location != '': + # The URN can be a VHD if the runbook provided a VHD in the 'images' parameter + if self._is_vhd(image.urn): + marketplace_image = "" + vhd = image.urn + else: + marketplace_image = image.urn + vhd = "" + + # If the runbook specified a location, use it. Then try the suite location, if any. Otherwise, check if the image specifies + # a list of locations and use any of them. If no location is specified so far, use the default. + if self.runbook.location != "": + location = self.runbook.location + elif suite_info.location != '': location = suite_info.location elif len(image.locations) > 0: location = image.locations[0] else: location = AgentTestSuitesCombinator._DEFAULT_LOCATION - # If the image specifies a list of VM sizes, use any of them. Otherwise, set the size to empty and let LISA choose it. - vm_size = image.vm_sizes[0] if len(image.vm_sizes) > 0 else "" + # If the runbook specified a VM size, use it. Else if the image specifies a list of VM sizes, use any of them. Otherwise, + # set the size to empty and let LISA choose it. + if self.runbook.vm_size != '': + vm_size = self.runbook.vm_size + elif len(image.vm_sizes) > 0: + vm_size = image.vm_sizes[0] + else: + vm_size = "" if suite_info.owns_vm: + # create an environment for exclusive use by this suite environment_list.append({ - "marketplace_image": image.urn, - "location": location, - "vm_size": vm_size, - "vhd": "", - "test_suites_info": [suite_info] + "c_marketplace_image": marketplace_image, + "c_location": location, + "c_vm_size": vm_size, + "c_vhd": vhd, + "c_test_suites": [suite_info] }) else: + # add this suite to the shared environments key: str = f"{image.urn}:{location}" if key in shared_environments: - shared_environments[key]["test_suites_info"].append(suite_info) + shared_environments[key]["c_test_suites"].append(suite_info) else: shared_environments[key] = { - "marketplace_image": image.urn, - "location": location, - "vm_size": vm_size, - "vhd": "", - "test_suites_info": [suite_info] + "c_marketplace_image": marketplace_image, + "c_location": location, + "c_vm_size": vm_size, + "c_vhd": vhd, + "c_test_suites": [suite_info] } environment_list.extend(shared_environments.values()) @@ -112,8 +163,19 @@ def create_environment_list(test_suites: str) -> List[Dict[str, Any]]: log.info("******** Environments *****") for e in environment_list: log.info( - "{ marketplace_image: '%s', location: '%s', vm_size: '%s', vhd: '%s', test_suites_info: '%s' }", - e['marketplace_image'], e['location'], e['vm_size'], e['vhd'], [s.name for s in e['test_suites_info']]) + "{ c_marketplace_image: '%s', c_location: '%s', c_vm_size: '%s', c_vhd: '%s', c_test_suites: '%s' }", + e['c_marketplace_image'], e['c_location'], e['c_vm_size'], e['c_vhd'], [s.name for s in e['c_test_suites']]) log.info("***************************") return environment_list + + @staticmethod + def _is_urn(urn: str) -> bool: + # URNs can be given as ' ' or ':::' + return re.match(r"(\S+\s\S+\s\S+\s\S+)|([^:]+:[^:]+:[^:]+:[^:]+)", urn) is not None + + @staticmethod + def _is_vhd(vhd: str) -> bool: + # VHDs are given as URIs to storage; do some basic validation, not intending to be exhaustive. + parsed = urllib.parse.urlparse(vhd) + return parsed.scheme == 'https' and parsed.netloc != "" and parsed.path != "" diff --git a/tests_e2e/orchestrator/runbook.yml b/tests_e2e/orchestrator/runbook.yml index 2f67cf7ff8..542f4acfeb 100644 --- a/tests_e2e/orchestrator/runbook.yml +++ b/tests_e2e/orchestrator/runbook.yml @@ -9,7 +9,7 @@ extension: variable: # - # These variables define runbook parameters; they are handled by LISA. + # These variables define parameters handled by LISA. # - name: subscription_id value: "" @@ -26,6 +26,9 @@ variable: # # These variables define parameters for the AgentTestSuite; see the test wiki for details. # + # NOTE: c_test_suites, generated by the AgentTestSuitesCombinator, is also a parameter + # for the AgentTestSuite + # # Whether to collect logs from the test VM - name: collect_logs value: "failed" @@ -37,25 +40,38 @@ variable: is_case_visible: true # - # These variables parameters for the AgentTestSuitesCombinator combinator + # These variables are parameters for the AgentTestSuitesCombinator # # The test suites to execute - name: test_suites value: "agent_bvt" - is_case_visible: true + - name: image + value: "" + - name: location + value: "" + - name: vm_size + value: "" # - # These variables are set by the AgentTestSuitesCombinator combinator + # The values for these variables are generated by the AgentTestSuitesCombinator combinator. They are + # prefixed with "c_" to distinguish them from the rest of the variables, whose value can be set from + # the command line. + # + # c_marketplace_image, c_vm_size, c_location, and c_vhd are handled by LISA and define + # the set of test VMs that need to be created, while c_test_suites is a parameter + # for the AgentTestSuite and defines the test suites that must be executed on each + # of those test VMs (the AgentTestSuite also uses c_vhd) # - - name: marketplace_image + - name: c_marketplace_image value: "" - - name: vm_size + - name: c_vm_size value: "" - - name: location + - name: c_location value: "" - - name: vhd + - name: c_vhd value: "" - - name: test_suites_info + is_case_visible: true + - name: c_test_suites value: [] is_case_visible: true @@ -86,14 +102,17 @@ platform: core_count: min: 2 azure: - marketplace: $(marketplace_image) - vhd: $(vhd) - location: $(location) - vm_size: $(vm_size) + marketplace: $(c_marketplace_image) + vhd: $(c_vhd) + location: $(c_location) + vm_size: $(c_vm_size) combinator: type: agent_test_suites test_suites: $(test_suites) + image: $(image) + location: $(location) + vm_size: $(vm_size) concurrency: 10 diff --git a/tests_e2e/pipeline/pipeline.yml b/tests_e2e/pipeline/pipeline.yml index 69154d08db..3251ff24e5 100644 --- a/tests_e2e/pipeline/pipeline.yml +++ b/tests_e2e/pipeline/pipeline.yml @@ -1,10 +1,31 @@ parameters: - # see the test wiki for a description of the parameters + # See the test wiki for a description of the parameters - name: test_suites displayName: Test Suites type: string default: agent_bvt + # NOTES: + # * 'image', 'location' and 'vm_size' override any values in the test suites/images definition + # files. Those parameters are useful for 1-off tests, like testing a VHD or checking if + # an image is supported in a particular location. + # * Azure Pipelines do not allow empty string for the parameter value, using "-" instead. + # + - name: image + displayName: Image (image/image set name, URN, or VHD) + type: string + default: "-" + + - name: location + displayName: Location (region) + type: string + default: "-" + + - name: vm_size + displayName: VM size + type: string + default: "-" + - name: collect_logs displayName: Collect logs from test VMs type: string @@ -26,8 +47,15 @@ parameters: variables: - name: azureConnection value: 'azuremanagement' + # These variables exposed the above parameters as environment variables - name: test_suites value: ${{ parameters.test_suites }} + - name: image + value: ${{ parameters.image }} + - name: location + value: ${{ parameters.location }} + - name: vm_size + value: ${{ parameters.vm_size }} - name: collect_logs value: ${{ parameters.collect_logs }} - name: keep_environment diff --git a/tests_e2e/pipeline/scripts/execute_tests.sh b/tests_e2e/pipeline/scripts/execute_tests.sh index b02df16bcc..cece7cd848 100755 --- a/tests_e2e/pipeline/scripts/execute_tests.sh +++ b/tests_e2e/pipeline/scripts/execute_tests.sh @@ -30,6 +30,18 @@ az acr login --name waagenttests docker pull waagenttests.azurecr.io/waagenttests:latest +# Azure Pipelines does not allow an empty string as the value for a pipeline parameter; instead we use "-" to indicate +# an empty value. Change "-" to "" for the variables that capture the parameter values. +if [[ $IMAGE == "-" ]]; then + IMAGE="" +fi +if [[ $LOCATION == "-" ]]; then + LOCATION="" +fi +if [[ $VM_SIZE == "-" ]]; then + VM_SIZE="" +fi + # A test failure will cause automation to exit with an error code and we don't want this script to stop so we force the command # to succeed and capture the exit code to return it at the end of the script. echo "exit 0" > /tmp/exit.sh @@ -51,7 +63,10 @@ docker run --rm \ -v identity_file:\$HOME/.ssh/id_rsa \ -v test_suites:\"$TEST_SUITES\" \ -v collect_logs:\"$COLLECT_LOGS\" \ - -v keep_environment:\"$KEEP_ENVIRONMENT\"" \ + -v keep_environment:\"$KEEP_ENVIRONMENT\" \ + -v image:\"$IMAGE\" \ + -v location:\"$LOCATION\" \ + -v vm_size:\"$VM_SIZE\"" \ || echo "exit $?" > /tmp/exit.sh # From 2c0cacd7efc6a558221789a719c2cdc56bd81a69 Mon Sep 17 00:00:00 2001 From: Norberto Arrieta Date: Tue, 21 Feb 2023 10:54:05 -0800 Subject: [PATCH 43/63] Bug fixes for test pipeline (#2764) * Bug fixes for test pipeline * Add support to execute test suites in multiple locations. Separate ARM64 images into their own image set --------- Co-authored-by: narrieta --- .../orchestrator/lib/agent_test_loader.py | 30 ++++--- .../orchestrator/lib/agent_test_suite.py | 14 ++-- .../lib/agent_test_suite_combinator.py | 11 ++- .../sample_runbooks/existing_vm.yml | 80 +++++++++++++++---- tests_e2e/test_suites/agent_bvt.yml | 4 +- tests_e2e/test_suites/images.yml | 5 +- 6 files changed, 108 insertions(+), 36 deletions(-) diff --git a/tests_e2e/orchestrator/lib/agent_test_loader.py b/tests_e2e/orchestrator/lib/agent_test_loader.py index bd3f320545..a0f0bfaaf1 100644 --- a/tests_e2e/orchestrator/lib/agent_test_loader.py +++ b/tests_e2e/orchestrator/lib/agent_test_loader.py @@ -33,8 +33,8 @@ class TestSuiteInfo(object): name: str # The tests that comprise the suite tests: List[Type[AgentTest]] - # An image or image set (as defined in images.yml) specifying the images the suite must run on. - images: str + # Images or image sets (as defined in images.yml) on which the suite must run. + images: List[str] # The location (region) on which the suite must run; if empty, the suite can run on any location location: str # Whether this suite must run on its own test VM @@ -101,13 +101,17 @@ def _validate(self): """ for suite in self.test_suites: # Validate that the images the suite must run on are in images.yml - if suite.images not in self.images: - raise Exception(f"Invalid image reference in test suite {suite.name}: Can't find {suite.images} in images.yml") + for image in suite.images: + if image not in self.images: + raise Exception(f"Invalid image reference in test suite {suite.name}: Can't find {image} in images.yml") - # If the suite specifies a location, validate that the images are available in that location + # If the suite specifies a location, validate that the images it uses are available in that location if suite.location != '': - if not any(suite.location in i.locations for i in self.images[suite.images]): - raise Exception(f"Test suite {suite.name} must be executed in {suite.location}, but no images in {suite.images} are available in that location") + for suite_image in suite.images: + for image in self.images[suite_image]: + if len(image.locations) > 0: + if suite.location not in image.locations: + raise Exception(f"Test suite {suite.name} must be executed in {suite.location}, but <{image.urn}> is not available in that location") @staticmethod def _load_test_suites(test_suites: str) -> List[TestSuiteInfo]: @@ -148,9 +152,9 @@ def _load_test_suite(description_file: Path) -> TestSuiteInfo: * name - A string used to identify the test suite * tests - A list of the tests in the suite. Each test is specified by the path for its source code relative to WALinuxAgent/tests_e2e/tests. - * images - A string specifying the images on which the test suite must be executed. The value can be the name - of a single image (e.g."ubuntu_2004"), or the name of an image set (e.g. "endorsed"). The names for - images and image sets are defined in WALinuxAgent/tests_e2e/tests_suites/images.yml. + * images - A string, or a list of strings, specifying the images on which the test suite must be executed. Each value + can be the name of a single image (e.g."ubuntu_2004"), or the name of an image set (e.g. "endorsed"). The + names for images and image sets are defined in WALinuxAgent/tests_e2e/tests_suites/images.yml. * location - [Optional; string] If given, the test suite must be executed on that location. If not specified, or set to an empty string, the test suite will be executed in the default location. This is useful for test suites that exercise a feature that is enabled only in certain regions. @@ -175,7 +179,11 @@ def _load_test_suite(description_file: Path) -> TestSuiteInfo: for f in source_files: test_suite_info.tests.extend(AgentTestLoader._load_test_classes(f)) - test_suite_info.images = test_suite["images"] + images = test_suite["images"] + if isinstance(images, str): + test_suite_info.images = [images] + else: + test_suite_info.images = images test_suite_info.location = test_suite.get("location") if test_suite_info.location is None: diff --git a/tests_e2e/orchestrator/lib/agent_test_suite.py b/tests_e2e/orchestrator/lib/agent_test_suite.py index 0f663a8bac..003ae50e74 100644 --- a/tests_e2e/orchestrator/lib/agent_test_suite.py +++ b/tests_e2e/orchestrator/lib/agent_test_suite.py @@ -21,7 +21,6 @@ import traceback import uuid -from enum import Enum from pathlib import Path from threading import current_thread, RLock from typing import Any, Dict, List @@ -91,7 +90,7 @@ def _set_thread_name(name: str): # # Possible values for the collect_logs parameter # -class CollectLogs(Enum): +class CollectLogs(object): Always = 'always' # Always collect logs Failed = 'failed' # Collect logs only on test failures No = 'no' # Never collect logs @@ -341,6 +340,8 @@ def _execute_test_suite(self, suite: TestSuiteInfo) -> bool: suite_full_name = f"{suite_name}-{self.context.image_name}" suite_start_time: datetime.datetime = datetime.datetime.now() + success: bool = True # True if all the tests succeed + with _set_thread_name(suite_full_name): # The thread name is added to self._log with set_current_thread_log(Path.home()/'logs'/f"{suite_full_name}.log"): try: @@ -348,7 +349,6 @@ def _execute_test_suite(self, suite: TestSuiteInfo) -> bool: agent_test_logger.info("**************************************** %s ****************************************", suite_name) agent_test_logger.info("") - failed: bool = False # True if any test fails summary: List[str] = [] for test in suite.tests: @@ -372,7 +372,7 @@ def _execute_test_suite(self, suite: TestSuiteInfo) -> bool: TestStatus.PASSED, test_start_time) except AssertionError as e: - failed = True + success = False summary.append(f"[Failed] {test_name}") agent_test_logger.error("******** [Failed] %s: %s", test_name, e) self._log.error("******** [Failed] %s", test_full_name) @@ -383,7 +383,7 @@ def _execute_test_suite(self, suite: TestSuiteInfo) -> bool: test_start_time, message=str(e)) except: # pylint: disable=bare-except - failed = True + success = False summary.append(f"[Error] {test_name}") agent_test_logger.exception("UNHANDLED EXCEPTION IN %s", test_name) self._log.exception("UNHANDLED EXCEPTION IN %s", test_full_name) @@ -404,7 +404,7 @@ def _execute_test_suite(self, suite: TestSuiteInfo) -> bool: agent_test_logger.info("") except: # pylint: disable=bare-except - failed = True + success = False self._report_test_result( suite_full_name, suite_name, @@ -413,7 +413,7 @@ def _execute_test_suite(self, suite: TestSuiteInfo) -> bool: message=f"Unhandled exception while executing test suite {suite_name}.", add_exception_stack_trace=True) - return failed + return success @staticmethod def _report_test_result( diff --git a/tests_e2e/orchestrator/lib/agent_test_suite_combinator.py b/tests_e2e/orchestrator/lib/agent_test_suite_combinator.py index 39fb104587..a3beac6338 100644 --- a/tests_e2e/orchestrator/lib/agent_test_suite_combinator.py +++ b/tests_e2e/orchestrator/lib/agent_test_suite_combinator.py @@ -103,7 +103,16 @@ def create_environment_list(self) -> List[Dict[str, Any]]: shared_environments: Dict[str, Dict[str, Any]] = {} for suite_info in loader.test_suites: - images_info = runbook_images if len(runbook_images) > 0 else loader.images[suite_info.images] + if len(runbook_images) > 0: + images_info = runbook_images + else: + # The test suite may be referencing multiple image sets, and sets can intersect, so we need to ensure + # we eliminate any duplicates. + unique_images: Dict[str, str] = {} + for image in suite_info.images: + for i in loader.images[image]: + unique_images[i] = i + images_info = unique_images.values() for image in images_info: # The URN can be a VHD if the runbook provided a VHD in the 'images' parameter diff --git a/tests_e2e/orchestrator/sample_runbooks/existing_vm.yml b/tests_e2e/orchestrator/sample_runbooks/existing_vm.yml index 2dbdf1d215..2730a5acca 100644 --- a/tests_e2e/orchestrator/sample_runbooks/existing_vm.yml +++ b/tests_e2e/orchestrator/sample_runbooks/existing_vm.yml @@ -28,9 +28,11 @@ extension: - "../lib" variable: + # + # These variables identify the existing VM, and the user for SSH connections + # - name: subscription_id value: "" - - name: resource_group_name value: "" - name: vm_name @@ -44,7 +46,61 @@ variable: value: "" is_secret: true - # Set these to use an SSH proxy + # + # These variables define parameters for the AgentTestSuite; see the test wiki for details. + # + # NOTE: c_test_suites, generated by the AgentTestSuitesCombinator, is also a parameter + # for the AgentTestSuite + # + # Whether to collect logs from the test VM + - name: collect_logs + value: "failed" + is_case_visible: true + + # Whether to skip setup of the test VM + - name: skip_setup + value: false + is_case_visible: true + + # + # These variables are parameters for the AgentTestSuitesCombinator + # + # The test suites to execute + - name: test_suites + value: "agent_bvt" + - name: image + value: "" + - name: location + value: "" + - name: vm_size + value: "" + + # + # The values for these variables are generated by the AgentTestSuitesCombinator combinator. They are + # prefixed with "c_" to distinguish them from the rest of the variables, whose value can be set from + # the command line. + # + # c_marketplace_image, c_vm_size, c_location, and c_vhd are handled by LISA and define + # the set of test VMs that need to be created, while c_test_suites is a parameter + # for the AgentTestSuite and defines the test suites that must be executed on each + # of those test VMs (the AgentTestSuite also uses c_vhd) + # + - name: c_marketplace_image + value: "" + - name: c_vm_size + value: "" + - name: c_location + value: "" + - name: c_vhd + value: "" + is_case_visible: true + - name: c_test_suites + value: [] + is_case_visible: true + + # + # Set these variables to use an SSH proxy when executing the runbook + # - name: proxy value: False - name: proxy_host @@ -55,19 +111,6 @@ variable: value: "" is_secret: true - # These variables define parameters for the AgentTestSuite - - name: test_suites - value: "agent_bvt" - is_case_visible: true - - - name: collect_logs - value: "failed" - is_case_visible: true - - - name: skip_setup - value: true - is_case_visible: true - platform: - type: azure admin_username: $(user) @@ -81,6 +124,13 @@ platform: location: $(location) name: $(vm_name) +combinator: + type: agent_test_suites + test_suites: $(test_suites) + image: $(image) + location: $(location) + vm_size: $(vm_size) + notifier: - type: env_stats - type: agent.junit diff --git a/tests_e2e/test_suites/agent_bvt.yml b/tests_e2e/test_suites/agent_bvt.yml index d5551837ee..1f0f91405f 100644 --- a/tests_e2e/test_suites/agent_bvt.yml +++ b/tests_e2e/test_suites/agent_bvt.yml @@ -3,4 +3,6 @@ tests: - "bvts/extension_operations.py" - "bvts/run_command.py" - "bvts/vm_access.py" -images: "endorsed" \ No newline at end of file +images: + - "endorsed" + - "endorsed-arm64" diff --git a/tests_e2e/test_suites/images.yml b/tests_e2e/test_suites/images.yml index 4b04373f7a..0e66f44140 100644 --- a/tests_e2e/test_suites/images.yml +++ b/tests_e2e/test_suites/images.yml @@ -15,7 +15,6 @@ image-sets: - "suse_12" - "mariner_1" - "mariner_2" - - "mariner_2_arm64" - "suse_15" - "rhel_78" - "rhel_82" @@ -23,6 +22,10 @@ image-sets: - "ubuntu_1804" - "ubuntu_2004" + # Endorsed distros (ARM64) that are tested on the daily runs + endorsed-arm64: + - "mariner_2_arm64" + # # An image can be specified by a string giving its urn, as in # From 7c44c2f6b744dc4226788b916bc2a0f75f4c334b Mon Sep 17 00:00:00 2001 From: Norberto Arrieta Date: Thu, 23 Feb 2023 06:47:44 -0800 Subject: [PATCH 44/63] Use unique name for test VMs. Remove hardcoded log path (#2767) * Use unique name for test VMs. Remove hardcoded log path --------- Co-authored-by: narrieta --- tests_e2e/orchestrator/lib/agent_junit.py | 4 ++- .../orchestrator/lib/agent_test_suite.py | 22 +++++++++++----- .../lib/agent_test_suite_combinator.py | 26 +++++++++++++------ tests_e2e/orchestrator/runbook.yml | 15 ++++++++--- .../sample_runbooks/existing_vm.yml | 3 +++ tests_e2e/pipeline/scripts/execute_tests.sh | 6 ++++- tests_e2e/tests/lib/vm_extension.py | 4 +-- 7 files changed, 59 insertions(+), 21 deletions(-) diff --git a/tests_e2e/orchestrator/lib/agent_junit.py b/tests_e2e/orchestrator/lib/agent_junit.py index 7fbb069379..049bbb161c 100644 --- a/tests_e2e/orchestrator/lib/agent_junit.py +++ b/tests_e2e/orchestrator/lib/agent_junit.py @@ -56,6 +56,8 @@ def _received_message(self, message: MessageBase) -> None: message.suite_name = message.suite_full_name image = message.information.get('image') if image is not None: - message.full_name = image + # NOTE: message.information['environment'] is similar to "[generated_2]" and can be correlated + # with the main LISA log to find the specific VM for the message. + message.full_name = f"{image} [{message.information['environment']}]" message.name = message.full_name super()._received_message(message) diff --git a/tests_e2e/orchestrator/lib/agent_test_suite.py b/tests_e2e/orchestrator/lib/agent_test_suite.py index 003ae50e74..3109f2ba4d 100644 --- a/tests_e2e/orchestrator/lib/agent_test_suite.py +++ b/tests_e2e/orchestrator/lib/agent_test_suite.py @@ -107,6 +107,7 @@ class _Context(AgentTestContext): def __init__(self, vm: VmIdentifier, paths: AgentTestContext.Paths, connection: AgentTestContext.Connection): super().__init__(vm=vm, paths=paths, connection=connection) # These are initialized by AgentTestSuite._set_context(). + self.log_path: Path = None self.log: Logger = None self.node: Node = None self.runbook_name: str = None @@ -121,7 +122,7 @@ def __init__(self, metadata: TestSuiteMetadata) -> None: # The context is initialized by _set_context() via the call to execute() self.__context: AgentTestSuite._Context = None - def _set_context(self, node: Node, variables: Dict[str, Any], log: Logger): + def _set_context(self, node: Node, variables: Dict[str, Any], lisa_log_path: str, log: Logger): connection_info = node.connection_info node_context = get_node_context(node) runbook = node.capability.get_extended_runbook(AzureNodeSchema, AZURE) @@ -144,10 +145,11 @@ def _set_context(self, node: Node, variables: Dict[str, Any], log: Logger): private_key_file=connection_info['private_key_file'], ssh_port=connection_info['port'])) + self.__context.log_path = self._get_log_path(variables, lisa_log_path) self.__context.log = log self.__context.node = node self.__context.is_vhd = self._get_required_parameter(variables, "c_vhd") != "" - self.__context.image_name = f"{node.os.name}-vhd" if self.__context.is_vhd else f"{runbook.marketplace.offer}-{runbook.marketplace.sku}" + self.__context.image_name = f"{node.os.name}-vhd" if self.__context.is_vhd else self._get_required_parameter(variables, "c_name") self.__context.test_suites = self._get_required_parameter(variables, "c_test_suites") self.__context.collect_logs = self._get_required_parameter(variables, "collect_logs") self.__context.skip_setup = self._get_required_parameter(variables, "skip_setup") @@ -165,6 +167,14 @@ def _get_required_parameter(variables: Dict[str, Any], name: str) -> Any: raise Exception(f"The runbook is missing required parameter '{name}'") return value + @staticmethod + def _get_log_path(variables: Dict[str, Any], lisa_log_path: str): + # NOTE: If "log_path" is not given as argument to the runbook, use a path derived from LISA's log for the test suite. + # That path is derived from LISA's "--log_path" command line argument and has a value similar to + # "<--log_path>/20230217/20230217-040022-342/tests/20230217-040119-288-agent_test_suite"; use the directory + # 2 levels up. + return Path(variables["log_path"]) if "log_path" in variables else Path(lisa_log_path).parent.parent + @property def context(self): if self.__context is None: @@ -287,18 +297,18 @@ def _collect_node_logs(self) -> None: # Copy the tarball to the local logs directory remote_path = "/tmp/waagent-logs.tgz" - local_path = Path.home()/'logs'/'{0}.tgz'.format(self.context.image_name) + local_path = self.context.log_path/'{0}.tgz'.format(self.context.image_name) self._log.info("Copying %s:%s to %s", self.context.node.name, remote_path, local_path) self.context.node.shell.copy_back(remote_path, local_path) except: # pylint: disable=bare-except self._log.exception("Failed to collect logs from the test machine") @TestCaseMetadata(description="", priority=0) - def agent_test_suite(self, node: Node, variables: Dict[str, Any], log: Logger) -> None: + def agent_test_suite(self, node: Node, variables: Dict[str, Any], log_path: str, log: Logger) -> None: """ Executes each of the AgentTests included in the "c_test_suites" variable (which is generated by the AgentTestSuitesCombinator). """ - self._set_context(node, variables, log) + self._set_context(node, variables, log_path, log) test_suite_success = True @@ -343,7 +353,7 @@ def _execute_test_suite(self, suite: TestSuiteInfo) -> bool: success: bool = True # True if all the tests succeed with _set_thread_name(suite_full_name): # The thread name is added to self._log - with set_current_thread_log(Path.home()/'logs'/f"{suite_full_name}.log"): + with set_current_thread_log(self.context.log_path/f"{suite_full_name}.log"): try: agent_test_logger.info("") agent_test_logger.info("**************************************** %s ****************************************", suite_name) diff --git a/tests_e2e/orchestrator/lib/agent_test_suite_combinator.py b/tests_e2e/orchestrator/lib/agent_test_suite_combinator.py index a3beac6338..2561b3f4b1 100644 --- a/tests_e2e/orchestrator/lib/agent_test_suite_combinator.py +++ b/tests_e2e/orchestrator/lib/agent_test_suite_combinator.py @@ -39,7 +39,7 @@ class AgentTestSuitesCombinatorSchema(schema.Combinator): class AgentTestSuitesCombinator(Combinator): """ - The "agent_test_suites" combinator returns a list of items containing five variables that specify the environments + The "agent_test_suites" combinator returns a list of items containing six variables that specify the environments that the agent test suites must be executed on: * c_marketplace_image: e.g. "Canonical UbuntuServer 18.04-LTS latest", @@ -47,6 +47,7 @@ class AgentTestSuitesCombinator(Combinator): * c_vm_size: e.g. "Standard_D2pls_v5" * c_vhd: e.g "https://rhel.blob.core.windows.net/images/RHEL_8_Standard-8.3.202006170423.vhd?se=..." * c_test_suites: e.g. [AgentBvt, FastTrack] + * c_name: Unique name for the image, e.g. "0001-com-ubuntu-server-focal-20_04-lts-westus2" (c_marketplace_image, c_location, c_vm_size) and vhd are mutually exclusive and define the environment (i.e. the test VM) in which the test will be executed. c_test_suites defines the test suites that should be executed in that @@ -115,13 +116,18 @@ def create_environment_list(self) -> List[Dict[str, Any]]: images_info = unique_images.values() for image in images_info: - # The URN can be a VHD if the runbook provided a VHD in the 'images' parameter + # The URN can actually point to a VHD if the runbook provided a VHD in the 'images' parameter if self._is_vhd(image.urn): marketplace_image = "" vhd = image.urn + name = "vhd" else: marketplace_image = image.urn vhd = "" + match = AgentTestSuitesCombinator._URN.match(image.urn) + if match is None: + raise Exception(f"Invalid URN: {image.urn}") + name = f"{match.group('offer')}-{match.group('sku')}" # If the runbook specified a location, use it. Then try the suite location, if any. Otherwise, check if the image specifies # a list of locations and use any of them. If no location is specified so far, use the default. @@ -150,11 +156,12 @@ def create_environment_list(self) -> List[Dict[str, Any]]: "c_location": location, "c_vm_size": vm_size, "c_vhd": vhd, - "c_test_suites": [suite_info] + "c_test_suites": [suite_info], + "c_name": f"{name}-{suite_info.name}" }) else: # add this suite to the shared environments - key: str = f"{image.urn}:{location}" + key: str = f"{name}-{location}" if key in shared_environments: shared_environments[key]["c_test_suites"].append(suite_info) else: @@ -163,7 +170,8 @@ def create_environment_list(self) -> List[Dict[str, Any]]: "c_location": location, "c_vm_size": vm_size, "c_vhd": vhd, - "c_test_suites": [suite_info] + "c_test_suites": [suite_info], + "c_name": key } environment_list.extend(shared_environments.values()) @@ -172,16 +180,18 @@ def create_environment_list(self) -> List[Dict[str, Any]]: log.info("******** Environments *****") for e in environment_list: log.info( - "{ c_marketplace_image: '%s', c_location: '%s', c_vm_size: '%s', c_vhd: '%s', c_test_suites: '%s' }", - e['c_marketplace_image'], e['c_location'], e['c_vm_size'], e['c_vhd'], [s.name for s in e['c_test_suites']]) + "{ c_marketplace_image: '%s', c_location: '%s', c_vm_size: '%s', c_vhd: '%s', c_test_suites: '%s', c_name: '%s' }", + e['c_marketplace_image'], e['c_location'], e['c_vm_size'], e['c_vhd'], [s.name for s in e['c_test_suites']], e['c_name']) log.info("***************************") return environment_list + _URN = re.compile(r"(?P[^\s:]+)[\s:](?P[^\s:]+)[\s:](?P[^\s:]+)[\s:](?P[^\s:]+)") + @staticmethod def _is_urn(urn: str) -> bool: # URNs can be given as ' ' or ':::' - return re.match(r"(\S+\s\S+\s\S+\s\S+)|([^:]+:[^:]+:[^:]+:[^:]+)", urn) is not None + return AgentTestSuitesCombinator._URN.match(urn) is not None @staticmethod def _is_vhd(vhd: str) -> bool: diff --git a/tests_e2e/orchestrator/runbook.yml b/tests_e2e/orchestrator/runbook.yml index 542f4acfeb..2fdc6dcc99 100644 --- a/tests_e2e/orchestrator/runbook.yml +++ b/tests_e2e/orchestrator/runbook.yml @@ -29,6 +29,11 @@ variable: # NOTE: c_test_suites, generated by the AgentTestSuitesCombinator, is also a parameter # for the AgentTestSuite # + # Root directory for log files (optional) + - name: log_path + value: "" + is_case_visible: true + # Whether to collect logs from the test VM - name: collect_logs value: "failed" @@ -58,9 +63,10 @@ variable: # the command line. # # c_marketplace_image, c_vm_size, c_location, and c_vhd are handled by LISA and define - # the set of test VMs that need to be created, while c_test_suites is a parameter - # for the AgentTestSuite and defines the test suites that must be executed on each - # of those test VMs (the AgentTestSuite also uses c_vhd) + # the set of test VMs that need to be created, while c_test_suites and c_name are parameters + # for the AgentTestSuite; the former defines the test suites that must be executed on each + # of those test VMs and the latter is the name of the environment, which is used for logging + # purposes (NOTE: the AgentTestSuite also uses c_vhd). # - name: c_marketplace_image value: "" @@ -74,6 +80,9 @@ variable: - name: c_test_suites value: [] is_case_visible: true + - name: c_name + value: "" + is_case_visible: true # # Set these variables to use an SSH proxy when executing the runbook diff --git a/tests_e2e/orchestrator/sample_runbooks/existing_vm.yml b/tests_e2e/orchestrator/sample_runbooks/existing_vm.yml index 2730a5acca..31f8c98343 100644 --- a/tests_e2e/orchestrator/sample_runbooks/existing_vm.yml +++ b/tests_e2e/orchestrator/sample_runbooks/existing_vm.yml @@ -97,6 +97,9 @@ variable: - name: c_test_suites value: [] is_case_visible: true + - name: c_name + value: "" + is_case_visible: true # # Set these variables to use an SSH proxy when executing the runbook diff --git a/tests_e2e/pipeline/scripts/execute_tests.sh b/tests_e2e/pipeline/scripts/execute_tests.sh index cece7cd848..1eaae0e453 100755 --- a/tests_e2e/pipeline/scripts/execute_tests.sh +++ b/tests_e2e/pipeline/scripts/execute_tests.sh @@ -62,6 +62,7 @@ docker run --rm \ -v subscription_id:$SUBSCRIPTION_ID \ -v identity_file:\$HOME/.ssh/id_rsa \ -v test_suites:\"$TEST_SUITES\" \ + -v log_path:\$HOME/logs \ -v collect_logs:\"$COLLECT_LOGS\" \ -v keep_environment:\"$KEEP_ENVIRONMENT\" \ -v image:\"$IMAGE\" \ @@ -86,10 +87,13 @@ sudo find "$BUILD_ARTIFACTSTAGINGDIRECTORY" -exec chown "$USER" {} \; # etc # # Remove the 2 levels of the tree that indicate the time of the test run to make navigation -# in the Azure Pipelines UI easier. +# in the Azure Pipelines UI easier. Also, move the lisa log one level up and remove some of +# the logs that are not needed # mv "$BUILD_ARTIFACTSTAGINGDIRECTORY"/lisa/[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]/*/* "$BUILD_ARTIFACTSTAGINGDIRECTORY"/lisa rm -r "$BUILD_ARTIFACTSTAGINGDIRECTORY"/lisa/[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9] +mv "$BUILD_ARTIFACTSTAGINGDIRECTORY"/lisa/lisa-*.log "$BUILD_ARTIFACTSTAGINGDIRECTORY" +rm "$BUILD_ARTIFACTSTAGINGDIRECTORY"/lisa/messages.log cat /tmp/exit.sh bash /tmp/exit.sh diff --git a/tests_e2e/tests/lib/vm_extension.py b/tests_e2e/tests/lib/vm_extension.py index 505a695a07..eab676e75a 100644 --- a/tests_e2e/tests/lib/vm_extension.py +++ b/tests_e2e/tests/lib/vm_extension.py @@ -99,9 +99,9 @@ def enable( extension_parameters ).result(timeout=_TIMEOUT)) - if result.provisioning_state != 'Succeeded': + if result.provisioning_state not in ('Succeeded', 'Updating'): raise Exception(f"Enable {self._identifier} failed. Provisioning state: {result.provisioning_state}") - log.info("Enable succeeded.") + log.info("Enable completed (provisioning state: %s).", result.provisioning_state) def get_instance_view(self) -> VirtualMachineExtensionInstanceView: # TODO: Check type for scale sets """ From 5aa1df626806451f5b72f412a083c78109dca41a Mon Sep 17 00:00:00 2001 From: Norberto Arrieta Date: Thu, 23 Feb 2023 20:31:25 -0800 Subject: [PATCH 45/63] Add distros to end-to-end automation (#2769) * Add distros to end-to-end automation --------- Co-authored-by: narrieta --- tests_e2e/orchestrator/runbook.yml | 2 +- tests_e2e/pipeline/pipeline-cleanup.yml | 6 ++++-- tests_e2e/test_suites/images.yml | 28 ++++++++++++++++++++----- 3 files changed, 28 insertions(+), 8 deletions(-) diff --git a/tests_e2e/orchestrator/runbook.yml b/tests_e2e/orchestrator/runbook.yml index 2fdc6dcc99..eb6b33ed50 100644 --- a/tests_e2e/orchestrator/runbook.yml +++ b/tests_e2e/orchestrator/runbook.yml @@ -123,7 +123,7 @@ combinator: location: $(location) vm_size: $(vm_size) -concurrency: 10 +concurrency: 32 notifier: - type: agent.junit diff --git a/tests_e2e/pipeline/pipeline-cleanup.yml b/tests_e2e/pipeline/pipeline-cleanup.yml index 109b9492e7..ba880a4f4f 100644 --- a/tests_e2e/pipeline/pipeline-cleanup.yml +++ b/tests_e2e/pipeline/pipeline-cleanup.yml @@ -1,7 +1,7 @@ # # Pipeline for cleaning up any remaining Resource Groups generated by the Azure.WALinuxAgent pipeline. # -# Runs every 3 hours and deletes any resource groups that are more than a day old and contain string "lisa-WALinuxAgent-" +# Deletes any resource groups that are more than a day old and contain string "lisa-WALinuxAgent-" # schedules: - cron: "0 */12 * * *" # Run twice a day (every 12 hours) @@ -11,7 +11,9 @@ schedules: - develop always: true -# no PR triggers +trigger: + - develop + pr: none pool: diff --git a/tests_e2e/test_suites/images.yml b/tests_e2e/test_suites/images.yml index 0e66f44140..9c726d0a87 100644 --- a/tests_e2e/test_suites/images.yml +++ b/tests_e2e/test_suites/images.yml @@ -8,23 +8,32 @@ image-sets: # TODO: Add CentOS 6.10 and Debian 8 # # - "centos_610" - - "centos_79" # - "debian_8" - - "debian_10" +# + - "alma_9" + - "centos_79" - "debian_9" + - "debian_10" + - "debian_11" - "suse_12" - "mariner_1" - "mariner_2" - "suse_15" - - "rhel_78" + - "rhel_79" - "rhel_82" + - "rhel_90" + - "rocky_9" - "ubuntu_1604" - "ubuntu_1804" - "ubuntu_2004" + - "ubuntu_2204" # Endorsed distros (ARM64) that are tested on the daily runs endorsed-arm64: + - "debian_11_arm64" - "mariner_2_arm64" + - "rhel_90_arm64" + - "ubuntu_2204_arm64" # # An image can be specified by a string giving its urn, as in @@ -52,10 +61,14 @@ images: # TODO: Add CentOS 6.10 and Debian 8 # # centos_610: "OpenLogic CentOS 6.10 latest" - centos_79: "OpenLogic CentOS 7_9 latest" # debian_8: "credativ Debian 8 latest" +# + alma_9: "almalinux almalinux 9-gen2 latest" + centos_79: "OpenLogic CentOS 7_9 latest" debian_9: "credativ Debian 9 latest" debian_10: "Debian debian-10 10 latest" + debian_11: "Debian debian-11 11 latest" + debian_11_arm64: "Debian debian-11 11-backports-arm64 latest" mariner_1: "microsoftcblmariner cbl-mariner cbl-mariner-1 latest" mariner_2: "microsoftcblmariner cbl-mariner cbl-mariner-2 latest" mariner_2_arm64: @@ -64,10 +77,15 @@ images: - "eastus" vm_sizes: - "Standard_D2pls_v5" + rocky_9: "erockyenterprisesoftwarefoundationinc1653071250513 rockylinux-9 rockylinux-9 latest" suse_12: "SUSE sles-12-sp5-basic gen1 latest" suse_15: "SUSE sles-15-sp2-basic gen2 latest" - rhel_78: "RedHat RHEL 7.8 latest" + rhel_79: "RedHat RHEL 7_9 latest" rhel_82: "RedHat RHEL 8.2 latest" + rhel_90: "RedHat RHEL 9_0 latest" + rhel_90_arm64: "RedHat rhel-arm64 9_0-arm64 latest" ubuntu_1604: "Canonical UbuntuServer 16.04-LTS latest" ubuntu_1804: "Canonical UbuntuServer 18.04-LTS latest" ubuntu_2004: "Canonical 0001-com-ubuntu-server-focal 20_04-lts latest" + ubuntu_2204: "Canonical 0001-com-ubuntu-server-jammy 22_04-lts latest" + ubuntu_2204_arm64: "Canonical 0001-com-ubuntu-server-jammy 22_04-lts-arm64 latest" From 66a6258536c4d108268c518a54d2f14b9990c9a7 Mon Sep 17 00:00:00 2001 From: Norberto Arrieta Date: Wed, 1 Mar 2023 07:05:38 -0800 Subject: [PATCH 46/63] Add support for Azure clouds (#2771) Co-authored-by: narrieta --- .../lib/agent_test_suite_combinator.py | 14 ++++- tests_e2e/orchestrator/runbook.yml | 3 + tests_e2e/pipeline/pipeline.yml | 61 ++++++++++--------- tests_e2e/pipeline/scripts/execute_tests.sh | 5 +- 4 files changed, 48 insertions(+), 35 deletions(-) diff --git a/tests_e2e/orchestrator/lib/agent_test_suite_combinator.py b/tests_e2e/orchestrator/lib/agent_test_suite_combinator.py index 2561b3f4b1..51bca96e24 100644 --- a/tests_e2e/orchestrator/lib/agent_test_suite_combinator.py +++ b/tests_e2e/orchestrator/lib/agent_test_suite_combinator.py @@ -26,6 +26,9 @@ class AgentTestSuitesCombinatorSchema(schema.Combinator): test_suites: str = field( default_factory=str, metadata=field_metadata(required=True) ) + cloud: str = field( + default_factory=str, metadata=field_metadata(required=True) + ) image: str = field( default_factory=str, metadata=field_metadata(required=True) ) @@ -58,6 +61,9 @@ def __init__(self, runbook: AgentTestSuitesCombinatorSchema) -> None: self._environments = self.create_environment_list() self._index = 0 + if self.runbook.cloud not in self._DEFAULT_LOCATIONS: + raise Exception(f"Invalid cloud: {self.runbook.cloud}") + @classmethod def type_name(cls) -> str: return "agent_test_suites" @@ -73,7 +79,11 @@ def _next(self) -> Optional[Dict[str, Any]]: self._index += 1 return result - _DEFAULT_LOCATION = "westus2" + _DEFAULT_LOCATIONS = { + "china": "china north 2", + "government": "usgovarizona", + "public": "westus2" + } def create_environment_list(self) -> List[Dict[str, Any]]: loader = AgentTestLoader(self.runbook.test_suites) @@ -138,7 +148,7 @@ def create_environment_list(self) -> List[Dict[str, Any]]: elif len(image.locations) > 0: location = image.locations[0] else: - location = AgentTestSuitesCombinator._DEFAULT_LOCATION + location = AgentTestSuitesCombinator._DEFAULT_LOCATIONS[self.runbook.cloud] # If the runbook specified a VM size, use it. Else if the image specifies a list of VM sizes, use any of them. Otherwise, # set the size to empty and let LISA choose it. diff --git a/tests_e2e/orchestrator/runbook.yml b/tests_e2e/orchestrator/runbook.yml index eb6b33ed50..3191233e95 100644 --- a/tests_e2e/orchestrator/runbook.yml +++ b/tests_e2e/orchestrator/runbook.yml @@ -50,6 +50,8 @@ variable: # The test suites to execute - name: test_suites value: "agent_bvt" + - name: cloud + value: "public" - name: image value: "" - name: location @@ -119,6 +121,7 @@ platform: combinator: type: agent_test_suites test_suites: $(test_suites) + cloud: $(cloud) image: $(image) location: $(location) vm_size: $(vm_size) diff --git a/tests_e2e/pipeline/pipeline.yml b/tests_e2e/pipeline/pipeline.yml index 3251ff24e5..fe532d6d74 100644 --- a/tests_e2e/pipeline/pipeline.yml +++ b/tests_e2e/pipeline/pipeline.yml @@ -1,3 +1,9 @@ +# variables: + # + # NOTE: When creating the pipeline, "connection_info" must be added as a variable pointing to the + # corresponding key vault; see wiki for details. + # + parameters: # See the test wiki for a description of the parameters - name: test_suites @@ -44,23 +50,6 @@ parameters: - failed - no -variables: - - name: azureConnection - value: 'azuremanagement' - # These variables exposed the above parameters as environment variables - - name: test_suites - value: ${{ parameters.test_suites }} - - name: image - value: ${{ parameters.image }} - - name: location - value: ${{ parameters.location }} - - name: vm_size - value: ${{ parameters.vm_size }} - - name: collect_logs - value: ${{ parameters.collect_logs }} - - name: keep_environment - value: ${{ parameters.keep_environment }} - trigger: - develop @@ -73,6 +62,18 @@ jobs: - job: "ExecuteTests" steps: + - task: UsePythonVersion@0 + displayName: "Set Python Version" + inputs: + versionSpec: '3.10' + addToPath: true + architecture: 'x64' + + # Extract the Azure cloud from the "connection_info" variable and store it in the "cloud" variable. + # The cloud name is used as a suffix of the value for "connection_info" and comes after the last '-'. + - bash: echo "##vso[task.setvariable variable=cloud]$(echo $CONNECTION_INFO | sed 's/^.*-//')" + displayName: "Set Cloud type" + - task: DownloadSecureFile@1 name: downloadSshKey displayName: "Download SSH key" @@ -80,29 +81,29 @@ jobs: secureFile: 'id_rsa' - task: AzureKeyVault@2 - displayName: "Fetch secrets from KV" + displayName: "Fetch connection info" inputs: - azureSubscription: '$(azureConnection)' - KeyVaultName: 'dcrV2SPs' + azureSubscription: 'azuremanagement' + KeyVaultName: '$(connection_info)' SecretsFilter: '*' - RunAsPreJob: true - - - task: UsePythonVersion@0 - displayName: "Set Python Version" - inputs: - versionSpec: '3.10' - addToPath: true - architecture: 'x64' - bash: $(Build.SourcesDirectory)/tests_e2e/pipeline/scripts/execute_tests.sh displayName: "Execute tests" continueOnError: true env: - # Add all KeyVault secrets explicitly as they're not added by default to the environment vars + SUBSCRIPTION_ID: $(SUBSCRIPTION-ID) AZURE_CLIENT_ID: $(AZURE-CLIENT-ID) AZURE_CLIENT_SECRET: $(AZURE-CLIENT-SECRET) AZURE_TENANT_ID: $(AZURE-TENANT-ID) - SUBSCRIPTION_ID: $(SUBSCRIPTION-ID) + CR_USER: $(CR-USER) + CR_SECRET: $(CR-SECRET) + CLOUD: ${{ variables.cloud }} + COLLECT_LOGS: ${{ parameters.collect_logs }} + IMAGE: ${{ parameters.image }} + KEEP_ENVIRONMENT: ${{ parameters.keep_environment }} + LOCATION: ${{ parameters.location }} + TEST_SUITES: ${{ parameters.test_suites }} + VM_SIZE: ${{ parameters.vm_size }} - publish: $(Build.ArtifactStagingDirectory) artifact: 'artifacts' diff --git a/tests_e2e/pipeline/scripts/execute_tests.sh b/tests_e2e/pipeline/scripts/execute_tests.sh index 1eaae0e453..e1d26d6e25 100755 --- a/tests_e2e/pipeline/scripts/execute_tests.sh +++ b/tests_e2e/pipeline/scripts/execute_tests.sh @@ -24,9 +24,7 @@ sudo chown 1000 "$BUILD_ARTIFACTSTAGINGDIRECTORY" # # Pull the container image used to execute the tests # -az login --service-principal --username "$AZURE_CLIENT_ID" --password "$AZURE_CLIENT_SECRET" --tenant "$AZURE_TENANT_ID" > /dev/null - -az acr login --name waagenttests +az acr login --name waagenttests --username "$CR_USER" --password "$CR_SECRET" docker pull waagenttests.azurecr.io/waagenttests:latest @@ -59,6 +57,7 @@ docker run --rm \ --runbook \$HOME/WALinuxAgent/tests_e2e/orchestrator/runbook.yml \ --log_path \$HOME/logs/lisa \ --working_path \$HOME/logs/lisa \ + -v cloud:$CLOUD \ -v subscription_id:$SUBSCRIPTION_ID \ -v identity_file:\$HOME/.ssh/id_rsa \ -v test_suites:\"$TEST_SUITES\" \ From 0903727fbe20490d123cad8ec6c3c2705e7548ec Mon Sep 17 00:00:00 2001 From: maddieford <93676569+maddieford@users.noreply.github.com> Date: Tue, 7 Mar 2023 12:20:40 -0800 Subject: [PATCH 47/63] Download certificates when goal state source is Fast Track (#2761) * Update version to dummy 1.0.0.0' * Revert version change * Download certificate in case of ft gs source * Update unit test after separating extensionsconfig and certificates * Update unit tests so that certificates are downloaded in all cases * Update unit test message * Add unit tests for downloading certs * Update goal state before checking for cert * Update unit test names * Update mock to check for etag before updating * Update protected settings test * Update failing unit tests after mock updatE * Update failing unit test * Make cert re-download more readable * Make certs_uri a data member * Update bitwise check for consistency --- azurelinuxagent/common/protocol/goal_state.py | 41 ++++++++----- tests/protocol/mockwiredata.py | 4 ++ ..._extensions_goal_state_from_vm_settings.py | 1 + tests/protocol/test_goal_state.py | 60 +++++++++++++++++-- tests/protocol/test_hostplugin.py | 2 +- tests/protocol/test_wire.py | 2 +- 6 files changed, 89 insertions(+), 21 deletions(-) diff --git a/azurelinuxagent/common/protocol/goal_state.py b/azurelinuxagent/common/protocol/goal_state.py index ed96159c8a..6b2a0c2cf8 100644 --- a/azurelinuxagent/common/protocol/goal_state.py +++ b/azurelinuxagent/common/protocol/goal_state.py @@ -56,8 +56,9 @@ class GoalStateProperties(object): HostingEnv = 0x2 SharedConfig = 0x4 ExtensionsGoalState = 0x8 - RemoteAccessInfo = 0x10 - All = RoleConfig | HostingEnv | SharedConfig | ExtensionsGoalState | RemoteAccessInfo + Certificates = 0x10 + RemoteAccessInfo = 0x20 + All = RoleConfig | HostingEnv | SharedConfig | ExtensionsGoalState | Certificates | RemoteAccessInfo class GoalStateInconsistentError(ProtocolError): @@ -96,6 +97,7 @@ def __init__(self, wire_client, goal_state_properties=GoalStateProperties.All, s self._hosting_env = None self._shared_conf = None self._certs = EmptyCertificates() + self._certs_uri = None self._remote_access = None self.update(silent=silent) @@ -140,7 +142,7 @@ def extensions_goal_state(self): @property def certs(self): - if not self._goal_state_properties & GoalStateProperties.ExtensionsGoalState: + if not self._goal_state_properties & GoalStateProperties.Certificates: raise ProtocolError("Certificates is not in goal state properties") else: return self._certs @@ -292,6 +294,10 @@ def _update(self, force_update): self._check_certificates() def _check_certificates(self): + # Re-download certificates in case they have been removed from disk since last download + if self._goal_state_properties & GoalStateProperties.Certificates and self._certs_uri is not None: + self._download_certificates(self._certs_uri) + # Check that certificates needed by extensions are in goal state certs.summary for extension in self.extensions_goal_state.extensions: for settings in extension.settings: if settings.protectedSettings is None: @@ -301,6 +307,20 @@ def _check_certificates(self): message = "Certificate {0} needed by {1} is missing from the goal state".format(settings.certificateThumbprint, extension.name) raise GoalStateInconsistentError(message) + def _download_certificates(self, certs_uri): + xml_text = self._wire_client.fetch_config(certs_uri, self._wire_client.get_header_for_cert()) + certs = Certificates(xml_text, self.logger) + # Log and save the certificates summary (i.e. the thumbprint but not the certificate itself) to the goal state history + for c in certs.summary: + message = "Downloaded certificate {0}".format(c) + self.logger.info(message) + add_event(op=WALAEventOperation.GoalState, message=message) + if len(certs.warnings) > 0: + self.logger.warn(certs.warnings) + add_event(op=WALAEventOperation.GoalState, message=certs.warnings) + self._history.save_certificates(json.dumps(certs.summary)) + return certs + def _restore_wire_server_goal_state(self, incarnation, xml_text, xml_doc, vm_settings_support_stopped_error): msg = 'The HGAP stopped supporting vmSettings; will fetched the goal state from the WireServer.' self.logger.info(msg) @@ -435,18 +455,8 @@ def _fetch_full_wire_server_goal_state(self, incarnation, xml_doc): certs = EmptyCertificates() certs_uri = findtext(xml_doc, "Certificates") - if (GoalStateProperties.ExtensionsGoalState & self._goal_state_properties) and certs_uri is not None: - xml_text = self._wire_client.fetch_config(certs_uri, self._wire_client.get_header_for_cert()) - certs = Certificates(xml_text, self.logger) - # Log and save the certificates summary (i.e. the thumbprint but not the certificate itself) to the goal state history - for c in certs.summary: - message = "Downloaded certificate {0}".format(c) - self.logger.info(message) - add_event(op=WALAEventOperation.GoalState, message=message) - if len(certs.warnings) > 0: - self.logger.warn(certs.warnings) - add_event(op=WALAEventOperation.GoalState, message=certs.warnings) - self._history.save_certificates(json.dumps(certs.summary)) + if (GoalStateProperties.Certificates & self._goal_state_properties) and certs_uri is not None: + certs = self._download_certificates(certs_uri) remote_access = None if GoalStateProperties.RemoteAccessInfo & self._goal_state_properties: @@ -463,6 +473,7 @@ def _fetch_full_wire_server_goal_state(self, incarnation, xml_doc): self._hosting_env = hosting_env self._shared_conf = shared_config self._certs = certs + self._certs_uri = certs_uri self._remote_access = remote_access return extensions_config diff --git a/tests/protocol/mockwiredata.py b/tests/protocol/mockwiredata.py index 7ec311af46..196ed32db8 100644 --- a/tests/protocol/mockwiredata.py +++ b/tests/protocol/mockwiredata.py @@ -165,6 +165,7 @@ def __init__(self, data_files=None): self.in_vm_artifacts_profile = None self.vm_settings = None self.etag = None + self.prev_etag = None self.imds_info = None self.reload() @@ -242,9 +243,12 @@ def mock_http_get(self, url, *_, **kwargs): elif "/vmSettings" in url: if self.vm_settings is None: resp.status = httpclient.NOT_FOUND + elif self.call_counts["vm_settings"] > 0 and self.prev_etag == self.etag: + resp.status = httpclient.NOT_MODIFIED else: content = self.vm_settings response_headers = [('ETag', self.etag)] + self.prev_etag = self.etag self.call_counts["vm_settings"] += 1 elif '{0}/metadata/compute'.format(IMDS_ENDPOINT) in url: content = json.dumps(self.imds_info.get("compute", "{}")) diff --git a/tests/protocol/test_extensions_goal_state_from_vm_settings.py b/tests/protocol/test_extensions_goal_state_from_vm_settings.py index fb97a075f6..1100b05bf9 100644 --- a/tests/protocol/test_extensions_goal_state_from_vm_settings.py +++ b/tests/protocol/test_extensions_goal_state_from_vm_settings.py @@ -58,6 +58,7 @@ def test_it_should_parse_requested_version_properly(self): data_file = mockwiredata.DATA_FILE_VM_SETTINGS.copy() data_file["vm_settings"] = "hostgaplugin/vm_settings-requested_version.json" with mock_wire_protocol(data_file) as protocol: + protocol.mock_wire_data.set_etag(888) goal_state = GoalState(protocol.client) families = goal_state.extensions_goal_state.agent_families for family in families: diff --git a/tests/protocol/test_goal_state.py b/tests/protocol/test_goal_state.py index 869da68c8c..61653b2af6 100644 --- a/tests/protocol/test_goal_state.py +++ b/tests/protocol/test_goal_state.py @@ -28,6 +28,7 @@ class GoalStateTestCase(AgentTestCase, HttpRequestPredicates): def test_it_should_use_vm_settings_by_default(self): with mock_wire_protocol(mockwiredata.DATA_FILE_VM_SETTINGS) as protocol: + protocol.mock_wire_data.set_etag(888) extensions_goal_state = GoalState(protocol.client).extensions_goal_state self.assertTrue( isinstance(extensions_goal_state, ExtensionsGoalStateFromVmSettings), @@ -156,11 +157,12 @@ def http_get_handler(url, *_, **__): protocol.set_http_handlers(http_get_handler=None) goal_state.update() self._assert_directory_contents( - self._find_history_subdirectory("234-987"), ["VmSettings.json"]) + self._find_history_subdirectory("234-987"), ["VmSettings.json", "Certificates.json"]) def test_it_should_redact_the_protected_settings_when_saving_to_the_history_directory(self): with mock_wire_protocol(mockwiredata.DATA_FILE_VM_SETTINGS) as protocol: protocol.mock_wire_data.set_incarnation(888) + protocol.mock_wire_data.set_etag(888) goal_state = GoalState(protocol.client) @@ -173,7 +175,7 @@ def test_it_should_redact_the_protected_settings_when_saving_to_the_history_dire if len(protected_settings) == 0: raise Exception("The test goal state does not include any protected settings") - history_directory = self._find_history_subdirectory("888-1") + history_directory = self._find_history_subdirectory("888-888") extensions_config_file = os.path.join(history_directory, "ExtensionsConfig.xml") vm_settings_file = os.path.join(history_directory, "VmSettings.json") for file_name in extensions_config_file, vm_settings_file: @@ -198,7 +200,6 @@ def test_it_should_save_vm_settings_on_parse_errors(self): data_file = mockwiredata.DATA_FILE_VM_SETTINGS.copy() data_file["vm_settings"] = invalid_vm_settings_file protocol.mock_wire_data = mockwiredata.WireProtocolData(data_file) - protocol.mock_wire_data.set_etag(888) with self.assertRaises(ProtocolError): # the parsing error will cause an exception _ = GoalState(protocol.client) @@ -206,6 +207,7 @@ def test_it_should_save_vm_settings_on_parse_errors(self): # Do an extra call to update the goal state; this should save the vmsettings to the history directory # only once (self._find_history_subdirectory asserts 1 single match) time.sleep(0.1) # add a short delay to ensure that a new timestamp would be saved in the history folder + protocol.mock_wire_data.set_etag(888) with self.assertRaises(ProtocolError): _ = GoalState(protocol.client) @@ -375,6 +377,7 @@ def test_it_should_raise_when_the_tenant_certificate_is_missing(self): with mock_wire_protocol(data_file) as protocol: data_file["vm_settings"] = "hostgaplugin/vm_settings-missing_cert.json" protocol.mock_wire_data.reload() + protocol.mock_wire_data.set_etag(888) with self.assertRaises(GoalStateInconsistentError) as context: _ = GoalState(protocol.client) @@ -382,6 +385,55 @@ def test_it_should_raise_when_the_tenant_certificate_is_missing(self): expected_message = "Certificate 59A10F50FFE2A0408D3F03FE336C8FD5716CF25C needed by Microsoft.OSTCExtensions.VMAccessForLinux is missing from the goal state" self.assertIn(expected_message, str(context.exception)) + def test_it_should_download_certs_on_a_new_fast_track_goal_state(self): + data_file = mockwiredata.DATA_FILE_VM_SETTINGS.copy() + + with mock_wire_protocol(data_file) as protocol: + goal_state = GoalState(protocol.client) + + cert = "BD447EF71C3ADDF7C837E84D630F3FAC22CCD22F" + crt_path = os.path.join(self.tmp_dir, cert + ".crt") + prv_path = os.path.join(self.tmp_dir, cert + ".prv") + + # Check that crt and prv files are downloaded after processing goal state + self.assertTrue(os.path.isfile(crt_path)) + self.assertTrue(os.path.isfile(prv_path)) + + # Remove .crt file + os.remove(crt_path) + if os.path.isfile(crt_path): + raise Exception("{0}.crt was not removed.".format(cert)) + + # Update goal state and check that .crt was downloaded + protocol.mock_wire_data.set_etag(888) + goal_state.update() + self.assertTrue(os.path.isfile(crt_path)) + + def test_it_should_download_certs_on_a_new_fabric_goal_state(self): + data_file = mockwiredata.DATA_FILE_VM_SETTINGS.copy() + + with mock_wire_protocol(data_file) as protocol: + protocol.mock_wire_data.set_vm_settings_source(GoalStateSource.Fabric) + goal_state = GoalState(protocol.client) + + cert = "BD447EF71C3ADDF7C837E84D630F3FAC22CCD22F" + crt_path = os.path.join(self.tmp_dir, cert + ".crt") + prv_path = os.path.join(self.tmp_dir, cert + ".prv") + + # Check that crt and prv files are downloaded after processing goal state + self.assertTrue(os.path.isfile(crt_path)) + self.assertTrue(os.path.isfile(prv_path)) + + # Remove .crt file + os.remove(crt_path) + if os.path.isfile(crt_path): + raise Exception("{0}.crt was not removed.".format(cert)) + + # Update goal state and check that .crt was downloaded + protocol.mock_wire_data.set_incarnation(999) + goal_state.update() + self.assertTrue(os.path.isfile(crt_path)) + def test_it_should_refresh_the_goal_state_when_it_is_inconsistent(self): # # Some scenarios can produce inconsistent goal states. For example, during hibernation/resume, the Fabric goal state changes (the @@ -412,7 +464,7 @@ def http_get_handler(url, *_, **__): goal_state = GoalState(protocol.client) self.assertEqual(2, protocol.mock_wire_data.call_counts['goalstate'], "There should have been exactly 2 requests for the goal state (original + refresh)") - self.assertEqual(2, http_get_handler.certificate_requests, "There should have been exactly 2 requests for the goal state certificates (original + refresh)") + self.assertEqual(4, http_get_handler.certificate_requests, "There should have been exactly 4 requests for the goal state certificates (2x original + 2x refresh)") thumbprints = [c.thumbprint for c in goal_state.certs.cert_list.certificates] diff --git a/tests/protocol/test_hostplugin.py b/tests/protocol/test_hostplugin.py index b85ed7574f..47e6871bea 100644 --- a/tests/protocol/test_hostplugin.py +++ b/tests/protocol/test_hostplugin.py @@ -998,7 +998,7 @@ def test_it_should_save_the_timestamp_of_the_most_recent_fast_track_goal_state(s # A fabric goal state should remove the state file protocol.mock_wire_data.set_vm_settings_source(GoalStateSource.Fabric) - + protocol.mock_wire_data.set_etag(888) _ = host_ga_plugin.fetch_vm_settings() self.assertFalse(os.path.exists(state_file), "{0} was not removed by a Fabric goal state".format(state_file)) diff --git a/tests/protocol/test_wire.py b/tests/protocol/test_wire.py index bbf018fc30..2a36fc2913 100644 --- a/tests/protocol/test_wire.py +++ b/tests/protocol/test_wire.py @@ -1101,7 +1101,7 @@ def test_forced_update_should_update_the_goal_state_and_the_host_plugin_when_the def test_reset_should_init_provided_goal_state_properties(self): with mock_wire_protocol(mockwiredata.DATA_FILE) as protocol: - protocol.client.reset_goal_state(goal_state_properties=GoalStateProperties.All & ~GoalStateProperties.ExtensionsGoalState) + protocol.client.reset_goal_state(goal_state_properties=GoalStateProperties.All & ~GoalStateProperties.Certificates) with self.assertRaises(ProtocolError) as context: _ = protocol.client.get_certs() From 0583078833d4fd7b8c3c13de74df2853239d6ab2 Mon Sep 17 00:00:00 2001 From: Norberto Arrieta Date: Thu, 9 Mar 2023 11:45:33 -0800 Subject: [PATCH 48/63] Add Flatcar to end-to-end tests, install Pypy on test VMs, etc (#2779) Add Flatcar, install Pypy, etc --------- Co-authored-by: narrieta --- tests_e2e/orchestrator/docker/Dockerfile | 11 +- .../orchestrator/lib/agent_test_suite.py | 142 ++++++++++-------- .../lib/agent_test_suite_combinator.py | 67 +++++++-- tests_e2e/orchestrator/runbook.yml | 8 +- .../sample_runbooks/existing_vm.yml | 40 ++--- tests_e2e/orchestrator/scripts/find-python | 51 +++++++ .../orchestrator/scripts/get-agent-pythonpath | 74 +++++++++ .../orchestrator/scripts/get-waagent-path | 57 +++++++ tests_e2e/orchestrator/scripts/install-agent | 31 ++-- tests_e2e/orchestrator/scripts/install-tools | 71 +++++++++ tests_e2e/orchestrator/scripts/uncompress.py | 35 +++++ tests_e2e/orchestrator/scripts/unzip.py | 36 +++++ tests_e2e/test_suites/images.yml | 15 +- tests_e2e/tests/bvts/vm_access.py | 5 +- tests_e2e/tests/lib/agent_test.py | 7 + tests_e2e/tests/lib/shell.py | 3 + tests_e2e/tests/lib/ssh_client.py | 25 ++- 17 files changed, 555 insertions(+), 123 deletions(-) create mode 100755 tests_e2e/orchestrator/scripts/find-python create mode 100755 tests_e2e/orchestrator/scripts/get-agent-pythonpath create mode 100755 tests_e2e/orchestrator/scripts/get-waagent-path create mode 100755 tests_e2e/orchestrator/scripts/install-tools create mode 100755 tests_e2e/orchestrator/scripts/uncompress.py create mode 100755 tests_e2e/orchestrator/scripts/unzip.py diff --git a/tests_e2e/orchestrator/docker/Dockerfile b/tests_e2e/orchestrator/docker/Dockerfile index 08fd4ca312..a748ff0b83 100644 --- a/tests_e2e/orchestrator/docker/Dockerfile +++ b/tests_e2e/orchestrator/docker/Dockerfile @@ -23,7 +23,7 @@ RUN \ # \ # Install basic dependencies \ # \ - apt-get install -y git python3.10 python3.10-dev && \ + apt-get install -y git python3.10 python3.10-dev wget bzip2 && \ ln /usr/bin/python3.10 /usr/bin/python3 && \ \ # \ @@ -69,7 +69,14 @@ RUN \ python3 -m pip install azure-mgmt-compute --upgrade && \ \ # \ - # The setup for the tests depends on a couple of paths; add those to the profile \ + # Download Pypy to a known location, from which it will be installed to the test VMs. \ + # \ + mkdir $HOME/bin && \ + wget https://downloads.python.org/pypy/pypy3.7-v7.3.5-linux64.tar.bz2 -O /tmp/pypy3.7-x64.tar.bz2 && \ + wget https://downloads.python.org/pypy/pypy3.7-v7.3.5-aarch64.tar.bz2 -O /tmp/pypy3.7-arm64.tar.bz2 && \ + \ + # \ + # The setup for the tests depends on a few paths; add those to the profile \ # \ echo 'export PYTHONPATH="$HOME/WALinuxAgent"' >> $HOME/.bash_profile && \ echo 'export PATH="$HOME/.local/bin:$PATH"' >> $HOME/.bash_profile && \ diff --git a/tests_e2e/orchestrator/lib/agent_test_suite.py b/tests_e2e/orchestrator/lib/agent_test_suite.py index 3109f2ba4d..204546b6e7 100644 --- a/tests_e2e/orchestrator/lib/agent_test_suite.py +++ b/tests_e2e/orchestrator/lib/agent_test_suite.py @@ -29,7 +29,6 @@ # E0401: Unable to import 'lisa' (import-error) # etc from lisa import ( # pylint: disable=E0401 - CustomScriptBuilder, Logger, Node, notifier, @@ -44,10 +43,13 @@ import makepkg from azurelinuxagent.common.version import AGENT_VERSION from tests_e2e.orchestrator.lib.agent_test_loader import TestSuiteInfo +from tests_e2e.tests.lib.agent_test import TestSkipped from tests_e2e.tests.lib.agent_test_context import AgentTestContext from tests_e2e.tests.lib.identifiers import VmIdentifier from tests_e2e.tests.lib.logging import log as agent_test_logger # Logger used by the tests from tests_e2e.tests.lib.logging import set_current_thread_log +from tests_e2e.tests.lib.shell import run_command +from tests_e2e.tests.lib.ssh_client import SshClient def _initialize_lisa_logger(): @@ -116,6 +118,7 @@ def __init__(self, vm: VmIdentifier, paths: AgentTestContext.Paths, connection: self.test_suites: List[AgentTestSuite] = None self.collect_logs: str = None self.skip_setup: bool = None + self.ssh_client: SshClient = None def __init__(self, metadata: TestSuiteMetadata) -> None: super().__init__(metadata) @@ -148,17 +151,12 @@ def _set_context(self, node: Node, variables: Dict[str, Any], lisa_log_path: str self.__context.log_path = self._get_log_path(variables, lisa_log_path) self.__context.log = log self.__context.node = node - self.__context.is_vhd = self._get_required_parameter(variables, "c_vhd") != "" - self.__context.image_name = f"{node.os.name}-vhd" if self.__context.is_vhd else self._get_required_parameter(variables, "c_name") + self.__context.is_vhd = self._get_optional_parameter(variables, "c_vhd") != "" + self.__context.image_name = f"{node.os.name}-vhd" if self.__context.is_vhd else self._get_required_parameter(variables, "c_env_name") self.__context.test_suites = self._get_required_parameter(variables, "c_test_suites") self.__context.collect_logs = self._get_required_parameter(variables, "collect_logs") self.__context.skip_setup = self._get_required_parameter(variables, "skip_setup") - - self._log.info( - "Test suite parameters: [skip_setup: %s] [collect_logs: %s] [test_suites: %s]", - self.context.skip_setup, - self.context.collect_logs, - [t.name for t in self.context.test_suites]) + self.__context.ssh_client = SshClient(ip_address=self.__context.vm_ip_address, username=self.__context.username, private_key_file=self.__context.private_key_file) @staticmethod def _get_required_parameter(variables: Dict[str, Any], name: str) -> Any: @@ -167,6 +165,13 @@ def _get_required_parameter(variables: Dict[str, Any], name: str) -> Any: raise Exception(f"The runbook is missing required parameter '{name}'") return value + @staticmethod + def _get_optional_parameter(variables: Dict[str, Any], name: str, default_value: Any = "") -> Any: + value = variables.get(name) + if value is None: + return default_value + return value + @staticmethod def _get_log_path(variables: Dict[str, Any], lisa_log_path: str): # NOTE: If "log_path" is not given as argument to the runbook, use a path derived from LISA's log for the test suite. @@ -211,7 +216,7 @@ def _setup(self) -> None: completed: Path = self.context.working_directory/"completed" if completed.exists(): - self._log.info("Found %s. Build has already been done, skipping", completed) + self._log.info("Found %s. Build has already been done, skipping.", completed) return self._log.info("Creating working directory: %s", self.context.working_directory) @@ -260,29 +265,51 @@ def _setup_node(self) -> None: self._log.info("Resource Group: %s", self.context.vm.resource_group) self._log.info("") + self._install_tools_on_node() + if self.context.is_vhd: self._log.info("Using a VHD; will not install the test Agent.") else: self._install_agent_on_node() + def _install_tools_on_node(self) -> None: + """ + Installs the test tools on the test node + """ + self.context.ssh_client.run_command("mkdir -p ~/bin") + + tools_path = self.context.test_source_directory/"orchestrator"/"scripts" + self._log.info(f"Copying {tools_path} to the test node") + self.context.ssh_client.copy(tools_path, Path("~/bin"), remote_target=True, recursive=True) + + if self.context.ssh_client.get_architecture() == "aarch64": + pypy_path = Path("/tmp/pypy3.7-arm64.tar.bz2") + pypy_download = "https://downloads.python.org/pypy/pypy3.7-v7.3.5-aarch64.tar.bz2" + else: + pypy_path = Path("/tmp/pypy3.7-x64.tar.bz2") + pypy_download = "https://downloads.python.org/pypy/pypy3.7-v7.3.5-linux64.tar.bz2" + + if not pypy_path.exists(): + self._log.info(f"Downloading {pypy_download} to {pypy_path}") + run_command(["wget", pypy_download, "-O", pypy_path]) + self._log.info(f"Copying {pypy_path} to the test node") + self.context.ssh_client.copy(pypy_path, Path("~/bin/pypy3.7.tar.bz2"), remote_target=True) + + self._log.info(f'Installing tools on the test node\n{self.context.ssh_client.run_command("~/bin/scripts/install-tools")}') + self._log.info(f'Remote commands will use {self.context.ssh_client.run_command("which python3")}') + def _install_agent_on_node(self) -> None: """ Installs the given agent package on the test node. """ agent_package_path: Path = self._get_agent_package_path() - # The install script needs to unzip the agent package; ensure unzip is installed on the test node - self._log.info("Installing unzip tool on %s", self.context.node.name) - self.context.node.os.install_packages("unzip") - self._log.info("Installing %s on %s", agent_package_path, self.context.node.name) agent_package_remote_path = self.context.remote_working_directory/agent_package_path.name self._log.info("Copying %s to %s:%s", agent_package_path, self.context.node.name, agent_package_remote_path) - self.context.node.shell.copy(agent_package_path, agent_package_remote_path) - self.execute_script_on_node( - self.context.test_source_directory/"orchestrator"/"scripts"/"install-agent", - parameters=f"--package {agent_package_remote_path} --version {AGENT_VERSION}", - sudo=True) + self.context.ssh_client.copy(agent_package_path, agent_package_remote_path, remote_target=True) + stdout = self.context.ssh_client.run_command(f"install-agent --package {agent_package_remote_path} --version {AGENT_VERSION}", use_sudo=True) + self._log.info(stdout) self._log.info("The agent was installed successfully.") @@ -293,13 +320,14 @@ def _collect_node_logs(self) -> None: try: # Collect the logs on the test machine into a compressed tarball self._log.info("Collecting logs on test machine [%s]...", self.context.node.name) - self.execute_script_on_node(self.context.test_source_directory/"orchestrator"/"scripts"/"collect-logs", sudo=True) + stdout = self.context.ssh_client.run_command("collect-logs", use_sudo=True) + self._log.info(stdout) # Copy the tarball to the local logs directory remote_path = "/tmp/waagent-logs.tgz" local_path = self.context.log_path/'{0}.tgz'.format(self.context.image_name) self._log.info("Copying %s:%s to %s", self.context.node.name, remote_path, local_path) - self.context.node.shell.copy_back(remote_path, local_path) + self.context.ssh_client.copy(remote_path, local_path, remote_source=True) except: # pylint: disable=bare-except self._log.exception("Failed to collect logs from the test machine") @@ -310,9 +338,16 @@ def agent_test_suite(self, node: Node, variables: Dict[str, Any], log_path: str, """ self._set_context(node, variables, log_path, log) - test_suite_success = True - with _set_thread_name(self.context.image_name): # The thread name is added to self._log + # E1133: Non-iterable value self.context.test_suites is used in an iterating context (not-an-iterable) + # (OK to iterate, test_suite is a List) + self._log.info( + "Test suite parameters: [test_suites: %s] [skip_setup: %s] [collect_logs: %s]", + [t.name for t in self.context.test_suites], self.context.skip_setup, self.context.collect_logs) # pylint: disable=E1133 + + start_time: datetime.datetime = datetime.datetime.now() + test_suite_success = True + try: if not self.context.skip_setup: self._setup() @@ -332,12 +367,17 @@ def agent_test_suite(self, node: Node, variables: Dict[str, Any], log_path: str, self._collect_node_logs() except: # pylint: disable=bare-except - # Note that we report the error to the LISA log and then re-raise it. We log it here - # so that the message is decorated with the thread name in the LISA log; we re-raise - # to let LISA know the test errored out (LISA will report that error one more time - # in its log) - self._log.exception("UNHANDLED EXCEPTION") - raise + # Report the error and raise and exception to let LISA know that the test errored out. + self._log.exception("TEST FAILURE DUE TO AN UNEXPECTED ERROR.") + self._report_test_result( + self.context.image_name, + "Setup", + TestStatus.FAILED, + start_time, + message="TEST FAILURE DUE TO AN UNEXPECTED ERROR.", + add_exception_stack_trace=True) + + raise Exception("STOPPING TEST EXECUTION DUE TO AN UNEXPECTED ERROR.") finally: self._clean_up() @@ -373,7 +413,7 @@ def _execute_test_suite(self, suite: TestSuiteInfo) -> bool: test(self.context).run() - summary.append(f"[Passed] {test_name}") + summary.append(f"[Passed] {test_name}") agent_test_logger.info("******** [Passed] %s", test_name) self._log.info("******** [Passed] %s", test_full_name) self._report_test_result( @@ -381,9 +421,19 @@ def _execute_test_suite(self, suite: TestSuiteInfo) -> bool: test_name, TestStatus.PASSED, test_start_time) + except TestSkipped as e: + summary.append(f"[Skipped] {test_name}") + agent_test_logger.info("******** [Skipped] %s: %s", test_name, e) + self._log.info("******** [Skipped] %s", test_full_name) + self._report_test_result( + suite_full_name, + test_name, + TestStatus.SKIPPED, + test_start_time, + message=str(e)) except AssertionError as e: success = False - summary.append(f"[Failed] {test_name}") + summary.append(f"[Failed] {test_name}") agent_test_logger.error("******** [Failed] %s: %s", test_name, e) self._log.error("******** [Failed] %s", test_full_name) self._report_test_result( @@ -394,7 +444,7 @@ def _execute_test_suite(self, suite: TestSuiteInfo) -> bool: message=str(e)) except: # pylint: disable=bare-except success = False - summary.append(f"[Error] {test_name}") + summary.append(f"[Error] {test_name}") agent_test_logger.exception("UNHANDLED EXCEPTION IN %s", test_name) self._log.exception("UNHANDLED EXCEPTION IN %s", test_full_name) self._report_test_result( @@ -460,32 +510,4 @@ def _report_test_result( notifier.notify(msg) - def execute_script_on_node(self, script_path: Path, parameters: str = "", sudo: bool = False) -> int: - """ - Executes the given script on the test node; if 'sudo' is True, the script is executed using the sudo command. - """ - custom_script_builder = CustomScriptBuilder(script_path.parent, [script_path.name]) - custom_script = self.context.node.tools[custom_script_builder] - - if parameters == '': - command_line = f"{script_path}" - else: - command_line = f"{script_path} {parameters}" - - self._log.info("Executing [%s]", command_line) - - result = custom_script.run(parameters=parameters, sudo=sudo) - - if result.stdout != "": - separator = "\n" if "\n" in result.stdout else " " - self._log.info("stdout:%s%s", separator, result.stdout) - if result.stderr != "": - separator = "\n" if "\n" in result.stderr else " " - self._log.error("stderr:%s%s", separator, result.stderr) - - if result.exit_code != 0: - raise Exception(f"[{command_line}] failed. Exit code: {result.exit_code}") - - return result.exit_code - diff --git a/tests_e2e/orchestrator/lib/agent_test_suite_combinator.py b/tests_e2e/orchestrator/lib/agent_test_suite_combinator.py index 51bca96e24..28fca0fad6 100644 --- a/tests_e2e/orchestrator/lib/agent_test_suite_combinator.py +++ b/tests_e2e/orchestrator/lib/agent_test_suite_combinator.py @@ -29,41 +29,59 @@ class AgentTestSuitesCombinatorSchema(schema.Combinator): cloud: str = field( default_factory=str, metadata=field_metadata(required=True) ) - image: str = field( - default_factory=str, metadata=field_metadata(required=True) - ) location: str = field( default_factory=str, metadata=field_metadata(required=True) ) + image: str = field( + default_factory=str, metadata=field_metadata(required=False) + ) vm_size: str = field( - default_factory=str, metadata=field_metadata(required=True) + default_factory=str, metadata=field_metadata(required=False) + ) + vm_name: str = field( + default_factory=str, metadata=field_metadata(required=False) ) class AgentTestSuitesCombinator(Combinator): """ - The "agent_test_suites" combinator returns a list of items containing six variables that specify the environments - that the agent test suites must be executed on: + The "agent_test_suites" combinator returns a list of variables that specify the environments (i.e. test VMs) that the agent + test suites must be executed on: + * c_env_name: Unique name for the environment, e.g. "0001-com-ubuntu-server-focal-20_04-lts-westus2" * c_marketplace_image: e.g. "Canonical UbuntuServer 18.04-LTS latest", * c_location: e.g. "westus2", * c_vm_size: e.g. "Standard_D2pls_v5" * c_vhd: e.g "https://rhel.blob.core.windows.net/images/RHEL_8_Standard-8.3.202006170423.vhd?se=..." * c_test_suites: e.g. [AgentBvt, FastTrack] - * c_name: Unique name for the image, e.g. "0001-com-ubuntu-server-focal-20_04-lts-westus2" (c_marketplace_image, c_location, c_vm_size) and vhd are mutually exclusive and define the environment (i.e. the test VM) in which the test will be executed. c_test_suites defines the test suites that should be executed in that environment. + + The 'vm_name' runbook parameter can be used to execute the test suites on an existing VM. In that case, the combinator + generates a single item with these variables: + + * c_env_name: Name for the environment, same as vm_name + * c_vm_name: Name of the test VM + * c_location: Location of the test VM e.g. "westus2", + * c_test_suites: e.g. [AgentBvt, FastTrack] """ def __init__(self, runbook: AgentTestSuitesCombinatorSchema) -> None: super().__init__(runbook) - self._environments = self.create_environment_list() - self._index = 0 - if self.runbook.cloud not in self._DEFAULT_LOCATIONS: raise Exception(f"Invalid cloud: {self.runbook.cloud}") + if self.runbook.vm_name != '' and (self.runbook.image != '' or self.runbook.vm_size != ''): + raise Exception("Invalid runbook parameters: When 'vm_name' is specified, 'image' and 'vm_size' should not be specified.") + + if self.runbook.vm_name != '': + self._environments = self.create_environment_for_existing_vm() + else: + self._environments = self.create_environment_list() + self._index = 0 + + @classmethod def type_name(cls) -> str: return "agent_test_suites" @@ -85,6 +103,27 @@ def _next(self) -> Optional[Dict[str, Any]]: "public": "westus2" } + def create_environment_for_existing_vm(self) -> List[Dict[str, Any]]: + loader = AgentTestLoader(self.runbook.test_suites) + + environment: List[Dict[str, Any]] = [ + { + "c_env_name": self.runbook.vm_name, + "c_vm_name": self.runbook.vm_name, + "c_location": self.runbook.location, + "c_test_suites": loader.test_suites, + } + ] + + log: logging.Logger = logging.getLogger("lisa") + log.info("******** Environment for existing VMs *****") + log.info( + "{ c_env_name: '%s', c_vm_name: '%s', c_location: '%s', c_test_suites: '%s' }", + environment[0]['c_env_name'], environment[0]['c_vm_name'], environment[0]['c_location'], [s.name for s in environment[0]['c_test_suites']]) + log.info("***************************") + + return environment + def create_environment_list(self) -> List[Dict[str, Any]]: loader = AgentTestLoader(self.runbook.test_suites) @@ -167,7 +206,7 @@ def create_environment_list(self) -> List[Dict[str, Any]]: "c_vm_size": vm_size, "c_vhd": vhd, "c_test_suites": [suite_info], - "c_name": f"{name}-{suite_info.name}" + "c_env_name": f"{name}-{suite_info.name}" }) else: # add this suite to the shared environments @@ -181,7 +220,7 @@ def create_environment_list(self) -> List[Dict[str, Any]]: "c_vm_size": vm_size, "c_vhd": vhd, "c_test_suites": [suite_info], - "c_name": key + "c_env_name": key } environment_list.extend(shared_environments.values()) @@ -190,8 +229,8 @@ def create_environment_list(self) -> List[Dict[str, Any]]: log.info("******** Environments *****") for e in environment_list: log.info( - "{ c_marketplace_image: '%s', c_location: '%s', c_vm_size: '%s', c_vhd: '%s', c_test_suites: '%s', c_name: '%s' }", - e['c_marketplace_image'], e['c_location'], e['c_vm_size'], e['c_vhd'], [s.name for s in e['c_test_suites']], e['c_name']) + "{ c_marketplace_image: '%s', c_location: '%s', c_vm_size: '%s', c_vhd: '%s', c_test_suites: '%s', c_env_name: '%s' }", + e['c_marketplace_image'], e['c_location'], e['c_vm_size'], e['c_vhd'], [s.name for s in e['c_test_suites']], e['c_env_name']) log.info("***************************") return environment_list diff --git a/tests_e2e/orchestrator/runbook.yml b/tests_e2e/orchestrator/runbook.yml index 3191233e95..bb968bad86 100644 --- a/tests_e2e/orchestrator/runbook.yml +++ b/tests_e2e/orchestrator/runbook.yml @@ -65,11 +65,14 @@ variable: # the command line. # # c_marketplace_image, c_vm_size, c_location, and c_vhd are handled by LISA and define - # the set of test VMs that need to be created, while c_test_suites and c_name are parameters + # the set of test VMs that need to be created, while c_test_suites and c_env_name are parameters # for the AgentTestSuite; the former defines the test suites that must be executed on each # of those test VMs and the latter is the name of the environment, which is used for logging # purposes (NOTE: the AgentTestSuite also uses c_vhd). # + - name: c_env_name + value: "" + is_case_visible: true - name: c_marketplace_image value: "" - name: c_vm_size @@ -82,9 +85,6 @@ variable: - name: c_test_suites value: [] is_case_visible: true - - name: c_name - value: "" - is_case_visible: true # # Set these variables to use an SSH proxy when executing the runbook diff --git a/tests_e2e/orchestrator/sample_runbooks/existing_vm.yml b/tests_e2e/orchestrator/sample_runbooks/existing_vm.yml index 31f8c98343..2e312e5e84 100644 --- a/tests_e2e/orchestrator/sample_runbooks/existing_vm.yml +++ b/tests_e2e/orchestrator/sample_runbooks/existing_vm.yml @@ -31,6 +31,8 @@ variable: # # These variables identify the existing VM, and the user for SSH connections # + - name: cloud + value: "public" - name: subscription_id value: "" - name: resource_group_name @@ -46,6 +48,12 @@ variable: value: "" is_secret: true + # + # The test suites to execute + # + - name: test_suites + value: "agent_bvt" + # # These variables define parameters for the AgentTestSuite; see the test wiki for details. # @@ -62,19 +70,6 @@ variable: value: false is_case_visible: true - # - # These variables are parameters for the AgentTestSuitesCombinator - # - # The test suites to execute - - name: test_suites - value: "agent_bvt" - - name: image - value: "" - - name: location - value: "" - - name: vm_size - value: "" - # # The values for these variables are generated by the AgentTestSuitesCombinator combinator. They are # prefixed with "c_" to distinguish them from the rest of the variables, whose value can be set from @@ -85,21 +80,16 @@ variable: # for the AgentTestSuite and defines the test suites that must be executed on each # of those test VMs (the AgentTestSuite also uses c_vhd) # - - name: c_marketplace_image + - name: c_env_name value: "" - - name: c_vm_size + is_case_visible: true + - name: c_vm_name value: "" - name: c_location value: "" - - name: c_vhd - value: "" - is_case_visible: true - name: c_test_suites value: [] is_case_visible: true - - name: c_name - value: "" - is_case_visible: true # # Set these variables to use an SSH proxy when executing the runbook @@ -124,15 +114,15 @@ platform: subscription_id: $(subscription_id) requirement: azure: - location: $(location) - name: $(vm_name) + name: $(c_vm_name) + location: $(c_location) combinator: type: agent_test_suites test_suites: $(test_suites) - image: $(image) + cloud: $(cloud) location: $(location) - vm_size: $(vm_size) + vm_name: $(vm_name) notifier: - type: env_stats diff --git a/tests_e2e/orchestrator/scripts/find-python b/tests_e2e/orchestrator/scripts/find-python new file mode 100755 index 0000000000..b36178e361 --- /dev/null +++ b/tests_e2e/orchestrator/scripts/find-python @@ -0,0 +1,51 @@ +#!/usr/bin/env bash + +# 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. +# + +# +# Returns the path to the Python executable. +# +set -euo pipefail + +# python3 is available on most distros +if which python3 2> /dev/null; then + exit 0 +fi + +# try python +if which python 2> /dev/null; then + exit 0 +fi + +# try some well-known locations +declare -a known_locations=( + "/usr/share/oem/python/bin/python" + "/usr/share/oem/python/bin/python3" +) + +for python in "${known_locations[@]}" +do + if [[ -e $python ]]; then + echo "$python" + exit 0 + fi +done + +echo "Can't find the python executable" >&2 + +exit 1 diff --git a/tests_e2e/orchestrator/scripts/get-agent-pythonpath b/tests_e2e/orchestrator/scripts/get-agent-pythonpath new file mode 100755 index 0000000000..bc9f0764e4 --- /dev/null +++ b/tests_e2e/orchestrator/scripts/get-agent-pythonpath @@ -0,0 +1,74 @@ +#!/usr/bin/env bash + +# 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. +# + +# +# Returns the PYTHONPATH on which the azurelinuxagent and associated modules are located. +# +# To do this, the script tries to find the python command used to start the agent and then +# returns the value of site.getsitepackages(). +# +set -euo pipefail + +find-agent-python() { + # if the agent is running, get the python command from 'exe' in the /proc file system + if test -e /run/waagent.pid; then + exe="/proc/$(cat /run/waagent.pid)/exe" + if test -e "$exe"; then + # exe is a symbolic link; return its target + readlink -f "$exe" + return 0 + fi + fi + + # try all the instances of 'python' and 'python3' in $PATH + for path in $(echo "$PATH" | tr ':' '\n'); do + if [[ -e $path ]]; then + for python in $(find "$path" -maxdepth 1 -name python3 -or -name python); do + if $python -c 'import azurelinuxagent' 2> /dev/null; then + echo "$python" + return 0 + fi + done + fi + done + + # try some well-known locations + declare -a known_locations=( + "/usr/share/oem/python/bin/python" + "/usr/share/oem/python/bin/python3" + ) + + for python in "${known_locations[@]}" + do + if $python -c 'import azurelinuxagent' 2> /dev/null; then + echo "$python" + return 0 + fi + done + + + return 1 +} + +if ! python=$(find-agent-python); then + echo "Can't find the python command used to start the agent" >&2 + exit 1 +fi + +$python -c 'import site; print(":".join(site.getsitepackages()))' diff --git a/tests_e2e/orchestrator/scripts/get-waagent-path b/tests_e2e/orchestrator/scripts/get-waagent-path new file mode 100755 index 0000000000..c177585695 --- /dev/null +++ b/tests_e2e/orchestrator/scripts/get-waagent-path @@ -0,0 +1,57 @@ +#!/usr/bin/env bash + +# 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. +# + +# +# Returns the path for the 'waagent' command. +# +set -euo pipefail + +# + +# On most distros, 'waagent' is in PATH +if which waagent 2> /dev/null; then + exit 0 +fi + +# if the agent is running, get the path from 'cmdline' in the /proc file system +if test -e /run/waagent.pid; then + cmdline="/proc/$(cat /run/waagent.pid)/cmdline" + if test -e "$cmdline"; then + # cmdline is a sequence of null-terminated strings; break into lines and look for waagent + if tr '\0' '\n' < "$cmdline" | grep waagent; then + exit 0 + fi + fi +fi + +# try some well-known locations +declare -a known_locations=( + "/usr/share/oem/bin/waagent" +) + +for path in "${known_locations[@]}" +do + if [[ -e $path ]]; then + echo "$path" + exit 0 + fi +done + +echo "Can't find the path for the 'waagent' command" >&2 +exit 1 diff --git a/tests_e2e/orchestrator/scripts/install-agent b/tests_e2e/orchestrator/scripts/install-agent index 0b513569f9..64d96307a3 100755 --- a/tests_e2e/orchestrator/scripts/install-agent +++ b/tests_e2e/orchestrator/scripts/install-agent @@ -16,7 +16,6 @@ # See the License for the specific language governing permissions and # limitations under the License. # - set -euo pipefail usage() ( @@ -76,9 +75,14 @@ echo "Service name: $service_name" # # Install the package # -echo "Installing $package..." -unzip -d "/var/lib/waagent/WALinuxAgent-$version" -o "$package" -sed -i 's/AutoUpdate.Enabled=n/AutoUpdate.Enabled=y/g' /etc/waagent.conf +echo "Installing $package as version $version..." +unzip.py "$package" "/var/lib/waagent/WALinuxAgent-$version" + +# Ensure that AutoUpdate is enabled. some distros, e.g. Flatcar, don't have a waagent.conf +# but AutoUpdate defaults to True so there is no need to anything in that case. +if [[ -e /etc/waagent.conf ]]; then + sed -i 's/AutoUpdate.Enabled=n/AutoUpdate.Enabled=y/g' /etc/waagent.conf +fi # # Restart the service @@ -92,14 +96,22 @@ mv /var/log/waagent.log /var/log/waagent."$(date --iso-8601=seconds)".log service-start $service_name # -# Verify that the new agent is running and output its status. Note that the extension handler -# may take some time to start so give 1 minute. +# Verify that the new agent is running and output its status. +# Note that the extension handler may take some time to start so give 1 minute. +# Also, note that the default Python is set to Pypy, so before executing 'waagent' we need to set the +# Python path to the location of the azurelinuxagent module. # echo "Verifying agent installation..." + +PYTHONPATH=$(get-agent-pythonpath) +export PYTHONPATH + check-version() { + waagent=$(get-waagent-path) + for i in {0..5} do - if waagent --version | grep -E "Goal state agent:\s+$1" > /dev/null; then + if $waagent --version | grep -E "Goal state agent:\s+$1" > /dev/null; then return 0 fi sleep 10 @@ -112,11 +124,12 @@ if check-version "$version"; then printf "\nThe agent was installed successfully\n" exit_code=0 else - printf "\nThe agent was not installed correctly; expected version %s\n" "$version" + printf "\nFailed to install agent.\n" exit_code=1 fi -waagent --version +waagent=$(get-waagent-path) +$waagent --version printf "\n" service-status $service_name diff --git a/tests_e2e/orchestrator/scripts/install-tools b/tests_e2e/orchestrator/scripts/install-tools new file mode 100755 index 0000000000..04dbb4f653 --- /dev/null +++ b/tests_e2e/orchestrator/scripts/install-tools @@ -0,0 +1,71 @@ +#!/usr/bin/env bash + +# 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. +# + +# +# Installs the tools in ~/bin/scripts/* to ~/bin, as well as Pypy. +# +# It also makes Pypy the default python for the current user. +# + +set -euo pipefail + +echo "Installing scripts to ~/bin" +mv ~/bin/scripts/* ~/bin +rm -r ~/bin/scripts + +echo "Installing Pypy to ~/bin" +# bzip2/lbzip2 (used by tar to uncompress *.bz2 files) are not available by default in some distros; +# use Python to uncompress the Pypy tarball. +python=$(~/bin/find-python) +$python ~/bin/uncompress.py ~/bin/pypy3.7.tar.bz2 ~/bin/pypy3.7.tar +tar xf ~/bin/pypy3.7.tar -C ~/bin +rm ~/bin/pypy3.7.tar ~/bin/pypy3.7.tar.bz2 + +if [[ -e ~/bin/python ]]; then + rm ~/bin/python +fi +ln -s ~/bin/pypy*/bin/pypy3.7 ~/bin/python + +if [[ -e ~/bin/python3 ]]; then + rm ~/bin/python3 +fi +ln -s ~/bin/pypy*/bin/pypy3.7 ~/bin/python3 + +# Note that we place $HOME/bin at the front of PATH, so the tools in ~/bin (including python) will +# take precedence over system tools with the same name. +# In some distros, e.g. Flatcar, .bash_profile is a symbolic link to a read-only file. Make a copy in +# that case. +echo "Adding ~/bin to PATH in .bash_profile" +if test -e ~/.bash_profile && ls -l .bash_profile | grep '\->'; then + cp ~/.bash_profile ~/.bash_profile-bk + rm ~/.bash_profile + mv ~/.bash_profile-bk ~/.bash_profile +fi +if ! test -e ~/.bash_profile || ! grep '# Add $HOME/bin to $PATH' ~/.bash_profile > /dev/null; then + echo ' +# Add $HOME/bin to $PATH +if [[ $PATH != *"$HOME/bin"* ]]; then + PATH="$HOME/bin:$PATH:" +fi +' >> ~/.bash_profile +fi + +echo "python3 has been set to $(which python3)" +python3 --version + diff --git a/tests_e2e/orchestrator/scripts/uncompress.py b/tests_e2e/orchestrator/scripts/uncompress.py new file mode 100755 index 0000000000..796ea98007 --- /dev/null +++ b/tests_e2e/orchestrator/scripts/uncompress.py @@ -0,0 +1,35 @@ +#!/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. +# + +# +# Un-compresses a bz2 file +# +import argparse +import bz2 +import shutil + +parser = argparse.ArgumentParser() +parser.add_argument('source', help='File to uncompress') +parser.add_argument('target', help='Output file') + +args = parser.parse_args() + +with bz2.BZ2File(args.source, 'rb') as f_in: + with open(args.target, 'wb') as f_out: + shutil.copyfileobj(f_in, f_out) diff --git a/tests_e2e/orchestrator/scripts/unzip.py b/tests_e2e/orchestrator/scripts/unzip.py new file mode 100755 index 0000000000..e25da1981f --- /dev/null +++ b/tests_e2e/orchestrator/scripts/unzip.py @@ -0,0 +1,36 @@ +#!/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. +# +import argparse +import sys +import zipfile + +try: + parser = argparse.ArgumentParser() + parser.add_argument('source', help='ZIP package to expand') + parser.add_argument('target', help='Destination directory') + + args = parser.parse_args() + + zipfile.ZipFile(args.source).extractall(args.target) + +except Exception as e: + print(f"{e}", file=sys.stderr) + sys.exit(1) + +sys.exit(0) diff --git a/tests_e2e/test_suites/images.yml b/tests_e2e/test_suites/images.yml index 9c726d0a87..253f8a1389 100644 --- a/tests_e2e/test_suites/images.yml +++ b/tests_e2e/test_suites/images.yml @@ -15,6 +15,7 @@ image-sets: - "debian_9" - "debian_10" - "debian_11" + - "flatcar" - "suse_12" - "mariner_1" - "mariner_2" @@ -31,6 +32,7 @@ image-sets: # Endorsed distros (ARM64) that are tested on the daily runs endorsed-arm64: - "debian_11_arm64" + - "flatcar_arm64" - "mariner_2_arm64" - "rhel_90_arm64" - "ubuntu_2204_arm64" @@ -57,18 +59,19 @@ image-sets: # ':::' # images: -# -# TODO: Add CentOS 6.10 and Debian 8 -# -# centos_610: "OpenLogic CentOS 6.10 latest" -# debian_8: "credativ Debian 8 latest" -# alma_9: "almalinux almalinux 9-gen2 latest" + centos_610: "OpenLogic CentOS 6.10 latest" centos_79: "OpenLogic CentOS 7_9 latest" + debian_8: "credativ Debian 8 latest" debian_9: "credativ Debian 9 latest" debian_10: "Debian debian-10 10 latest" debian_11: "Debian debian-11 11 latest" debian_11_arm64: "Debian debian-11 11-backports-arm64 latest" + flatcar: "kinvolk flatcar-container-linux-free stable latest" + flatcar_arm64: + urn: "kinvolk flatcar-container-linux-corevm stable latest" + vm_sizes: + - "Standard_D2pls_v5" mariner_1: "microsoftcblmariner cbl-mariner cbl-mariner-1 latest" mariner_2: "microsoftcblmariner cbl-mariner cbl-mariner-2 latest" mariner_2_arm64: diff --git a/tests_e2e/tests/bvts/vm_access.py b/tests_e2e/tests/bvts/vm_access.py index 9e4c345ab9..c354bbd9da 100755 --- a/tests_e2e/tests/bvts/vm_access.py +++ b/tests_e2e/tests/bvts/vm_access.py @@ -28,7 +28,7 @@ from assertpy import assert_that from pathlib import Path -from tests_e2e.tests.lib.agent_test import AgentTest +from tests_e2e.tests.lib.agent_test import AgentTest, TestSkipped from tests_e2e.tests.lib.identifiers import VmExtensionIds from tests_e2e.tests.lib.logging import log from tests_e2e.tests.lib.ssh_client import SshClient @@ -38,6 +38,9 @@ class VmAccessBvt(AgentTest): def run(self): + if type(self._context.node.os).__name__ == 'CoreOs' and self._context.node.os.information.full_version.startswith('Flatcar'): + raise TestSkipped("Currently VMAccess is not supported on Flatcar") + # Try to use a unique username for each test run (note that we truncate to 32 chars to # comply with the rules for usernames) log.info("Generating a new username and SSH key") diff --git a/tests_e2e/tests/lib/agent_test.py b/tests_e2e/tests/lib/agent_test.py index e72c5f0ee9..6e79ad4f2f 100644 --- a/tests_e2e/tests/lib/agent_test.py +++ b/tests_e2e/tests/lib/agent_test.py @@ -25,6 +25,13 @@ from tests_e2e.tests.lib.logging import log +class TestSkipped(Exception): + """ + Tests can raise this exception to indicate they should not be executed (for example, if trying to execute them on + an unsupported distro + """ + + class AgentTest(ABC): """ Defines the interface for agent tests, which are simply constructed from an AgentTestContext and expose a single method, diff --git a/tests_e2e/tests/lib/shell.py b/tests_e2e/tests/lib/shell.py index 894ba90ca2..a5288439a6 100644 --- a/tests_e2e/tests/lib/shell.py +++ b/tests_e2e/tests/lib/shell.py @@ -31,6 +31,9 @@ def __init__(self, command: Any, exit_code: int, stdout: str, stderr: str): self.stdout: str = stdout self.stderr: str = stderr + def __str__(self): + return f"'{self.command}' failed (exit code: {self.exit_code})\nstdout:\n{self.stdout}\nstderr:\n{self.stderr}\n" + def run_command(command: Any, shell=False) -> str: """ diff --git a/tests_e2e/tests/lib/ssh_client.py b/tests_e2e/tests/lib/ssh_client.py index 917afd049b..e0c07420e6 100644 --- a/tests_e2e/tests/lib/ssh_client.py +++ b/tests_e2e/tests/lib/ssh_client.py @@ -28,14 +28,19 @@ def __init__(self, ip_address: str, username: str, private_key_file: Path, port: self._private_key_file: Path = private_key_file self._port: int = port - def run_command(self, command: str) -> str: + def run_command(self, command: str, use_sudo: bool = False) -> str: """ Executes the given command over SSH and returns its stdout. If the command returns a non-zero exit code, the function raises a RunCommandException. """ destination = f"ssh://{self._username}@{self._ip_address}:{self._port}" - return shell.run_command(["ssh", "-o", "StrictHostKeyChecking=no", "-i", self._private_key_file, destination, command]) + # Note that we add ~/bin to the remote PATH, since Python (Pypy) and other test tools are installed there. + # Note, too, that when using sudo we need to carry over the value of PATH to the sudo session + sudo = "sudo env PATH=$PATH" if use_sudo else '' + return shell.run_command([ + "ssh", "-o", "StrictHostKeyChecking=no", "-i", self._private_key_file, destination, + f"PATH=~/bin:$PATH;{sudo} {command}"]) @staticmethod def generate_ssh_key(private_key_file: Path): @@ -46,3 +51,19 @@ def generate_ssh_key(private_key_file: Path): def get_architecture(self): return self.run_command("uname -m").rstrip() + + def copy(self, source: Path, target: Path, remote_source: bool = False, remote_target: bool = False, recursive: bool = False): + """ + Copy file from local to remote machine + """ + if remote_source: + source = f"{self._username}@{self._ip_address}:{source}" + if remote_target: + target = f"{self._username}@{self._ip_address}:{target}" + + command = ["scp", "-o", "StrictHostKeyChecking=no", "-i", self._private_key_file] + if recursive: + command.append("-r") + command.extend([str(source), str(target)]) + + shell.run_command(command) From 5e538870c89bcf17ad902bb80a363eef31014f02 Mon Sep 17 00:00:00 2001 From: Nageswara Nandigam <84482346+nagworld9@users.noreply.github.com> Date: Thu, 9 Mar 2023 13:04:51 -0800 Subject: [PATCH 49/63] fetch full distro version for mariner (#2773) * fetch full distro version for mariner * address comment --- azurelinuxagent/common/future.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/azurelinuxagent/common/future.py b/azurelinuxagent/common/future.py index 0c0e016eea..be28ba9d88 100644 --- a/azurelinuxagent/common/future.py +++ b/azurelinuxagent/common/future.py @@ -109,6 +109,12 @@ def get_linux_distribution_from_distro(get_full_name): ) full_name = distro.linux_distribution()[0].strip() osinfo.append(full_name) + + # Fixing is the problem https://github.com/Azure/WALinuxAgent/issues/2715. Distro.linux_distribution method not retuning full version + # If best is true, the most precise version number out of all examined sources is returned. + if "mariner" in osinfo[0].lower(): + osinfo[1] = distro.version(best=True) + return osinfo From b49b11a3286b92d1b2218264ca13fcfeb2d48868 Mon Sep 17 00:00:00 2001 From: Norberto Arrieta Date: Fri, 10 Mar 2023 12:01:13 -0800 Subject: [PATCH 50/63] Increase the max number of extension events by 20% (#2785) Co-authored-by: narrieta --- azurelinuxagent/ga/collect_telemetry_events.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/azurelinuxagent/ga/collect_telemetry_events.py b/azurelinuxagent/ga/collect_telemetry_events.py index 792ae0de67..689803f8ff 100644 --- a/azurelinuxagent/ga/collect_telemetry_events.py +++ b/azurelinuxagent/ga/collect_telemetry_events.py @@ -78,7 +78,7 @@ class _ProcessExtensionEvents(PeriodicOperation): _EXTENSION_EVENT_FILE_NAME_REGEX = re.compile(r"^(\d+)\.json$", re.IGNORECASE) # Limits - _MAX_NUMBER_OF_EVENTS_PER_EXTENSION_PER_PERIOD = 300 + _MAX_NUMBER_OF_EVENTS_PER_EXTENSION_PER_PERIOD = 360 _EXTENSION_EVENT_FILE_MAX_SIZE = 4 * 1024 * 1024 # 4 MB = 4 * 1,048,576 Bytes _EXTENSION_EVENT_MAX_SIZE = 1024 * 6 # 6Kb or 6144 characters. Limit for the whole event. Prevent oversized events. _EXTENSION_EVENT_MAX_MSG_LEN = 1024 * 3 # 3Kb or 3072 chars. From 355dd0981af835e6b2201912896e476961bf5baf Mon Sep 17 00:00:00 2001 From: Norberto Arrieta Date: Fri, 10 Mar 2023 12:07:05 -0800 Subject: [PATCH 51/63] Fix bug in get_dhcp_pid/CoreOS (#2784) Co-authored-by: narrieta --- azurelinuxagent/common/osutil/coreos.py | 6 ++++-- azurelinuxagent/common/osutil/default.py | 17 ++++++++++------- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/azurelinuxagent/common/osutil/coreos.py b/azurelinuxagent/common/osutil/coreos.py index fc0a66043f..373727e200 100644 --- a/azurelinuxagent/common/osutil/coreos.py +++ b/azurelinuxagent/common/osutil/coreos.py @@ -17,7 +17,7 @@ # import os -import azurelinuxagent.common.utils.shellutil as shellutil +from azurelinuxagent.common.utils import shellutil from azurelinuxagent.common.osutil.default import DefaultOSUtil @@ -78,7 +78,9 @@ def stop_agent_service(self): return shellutil.run("systemctl stop {0}".format(self.service_name), chk_err=False) def get_dhcp_pid(self): - return self._get_dhcp_pid(["systemctl", "show", "-p", "MainPID", "systemd-networkd"]) + return self._get_dhcp_pid( + ["systemctl", "show", "-p", "MainPID", "systemd-networkd"], + transform_command_output=lambda o: o.replace("MainPID=", "")) def conf_sshd(self, disable_password): # In CoreOS, /etc/sshd_config is mount readonly. Skip the setting. diff --git a/azurelinuxagent/common/osutil/default.py b/azurelinuxagent/common/osutil/default.py index 056c50e07e..9fb97f157f 100644 --- a/azurelinuxagent/common/osutil/default.py +++ b/azurelinuxagent/common/osutil/default.py @@ -36,11 +36,11 @@ import array -import azurelinuxagent.common.conf as conf -import azurelinuxagent.common.logger as logger -import azurelinuxagent.common.utils.fileutil as fileutil -import azurelinuxagent.common.utils.shellutil as shellutil -import azurelinuxagent.common.utils.textutil as textutil +from azurelinuxagent.common import conf +from azurelinuxagent.common import logger +from azurelinuxagent.common.utils import fileutil +from azurelinuxagent.common.utils import shellutil +from azurelinuxagent.common.utils import textutil from azurelinuxagent.common.exception import OSUtilError from azurelinuxagent.common.future import ustr, array_to_bytes @@ -1137,9 +1137,12 @@ def _text_to_pid_list(text): return [int(n) for n in text.split()] @staticmethod - def _get_dhcp_pid(command): + def _get_dhcp_pid(command, transform_command_output=None): try: - return DefaultOSUtil._text_to_pid_list(shellutil.run_command(command)) + output = shellutil.run_command(command) + if transform_command_output is not None: + output = transform_command_output(output) + return DefaultOSUtil._text_to_pid_list(output) except CommandError as exception: # pylint: disable=W0612 return [] From b5840577c17b5e09932249c53b4aa79bde2658d8 Mon Sep 17 00:00:00 2001 From: Nageswara Nandigam <84482346+nagworld9@users.noreply.github.com> Date: Mon, 13 Mar 2023 11:52:19 -0700 Subject: [PATCH 52/63] remove version suffix from extension slice (#2782) * remove version suffix from extension slice * address comments --- azurelinuxagent/common/cgroupapi.py | 7 ++++++- azurelinuxagent/common/cgroupconfigurator.py | 10 +++++++++- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/azurelinuxagent/common/cgroupapi.py b/azurelinuxagent/common/cgroupapi.py index 66e893ef6b..ca0ef3bb5b 100644 --- a/azurelinuxagent/common/cgroupapi.py +++ b/azurelinuxagent/common/cgroupapi.py @@ -253,7 +253,12 @@ def _is_systemd_failure(scope_name, stderr): return unit_not_found in stderr or scope_name not in stderr @staticmethod - def get_extension_slice_name(extension_name): + def get_extension_slice_name(extension_name, old_slice=False): + # The old slice makes it difficult for user to override the limits because they need to place drop-in files on every upgrade if extension slice is different for each version. + # old slice includes .- + # new slice without version . + if not old_slice: + extension_name = extension_name.rsplit("-", 1)[0] # Since '-' is used as a separator in systemd unit names, we replace it with '_' to prevent side-effects. return EXTENSION_SLICE_PREFIX + "-" + extension_name.replace('-', '_') + ".slice" diff --git a/azurelinuxagent/common/cgroupconfigurator.py b/azurelinuxagent/common/cgroupconfigurator.py index 627567b038..767786f014 100644 --- a/azurelinuxagent/common/cgroupconfigurator.py +++ b/azurelinuxagent/common/cgroupconfigurator.py @@ -455,6 +455,11 @@ def __create_all_files(files_to_create): def is_extension_resource_limits_setup_completed(self, extension_name, cpu_quota=None): unit_file_install_path = systemd.get_unit_file_install_path() + old_extension_slice_path = os.path.join(unit_file_install_path, SystemdCgroupsApi.get_extension_slice_name(extension_name, old_slice=True)) + # clean up the old slice from the disk + if os.path.exists(old_extension_slice_path): + CGroupConfigurator._Impl.__cleanup_unit_file(old_extension_slice_path) + extension_slice_path = os.path.join(unit_file_install_path, SystemdCgroupsApi.get_extension_slice_name(extension_name)) cpu_quota = str( @@ -921,7 +926,10 @@ def setup_extension_slice(self, extension_name, cpu_quota): SystemdCgroupsApi.get_extension_slice_name(extension_name)) try: cpu_quota = str(cpu_quota) + "%" if cpu_quota is not None else "" # setting an empty value resets to the default (infinity) - _log_cgroup_info("Ensuring the {0}'s CPUQuota is {1}", extension_name, cpu_quota) + if cpu_quota == "": + _log_cgroup_info("CPUQuota not set for {0}", extension_name) + else: + _log_cgroup_info("Ensuring the {0}'s CPUQuota is {1}", extension_name, cpu_quota) slice_contents = _EXTENSION_SLICE_CONTENTS.format(extension_name=extension_name, cpu_quota=cpu_quota) CGroupConfigurator._Impl.__create_unit_file(extension_slice_path, slice_contents) From 8ebaf41f1b76e9d9aa04470466645053abf3f1fc Mon Sep 17 00:00:00 2001 From: Nageswara Nandigam <84482346+nagworld9@users.noreply.github.com> Date: Tue, 14 Mar 2023 14:48:12 -0700 Subject: [PATCH 53/63] accept int type for event fields (#2786) --- azurelinuxagent/ga/collect_telemetry_events.py | 13 ++++++++++--- .../extension_events/int_type/1519934744.json | 10 ++++++++++ tests/ga/test_collect_telemetry_events.py | 10 ++++++++++ 3 files changed, 30 insertions(+), 3 deletions(-) create mode 100644 tests/data/events/extension_events/int_type/1519934744.json diff --git a/azurelinuxagent/ga/collect_telemetry_events.py b/azurelinuxagent/ga/collect_telemetry_events.py index 689803f8ff..01049ee875 100644 --- a/azurelinuxagent/ga/collect_telemetry_events.py +++ b/azurelinuxagent/ga/collect_telemetry_events.py @@ -58,6 +58,8 @@ class ExtensionEventSchema(object): "EventTid":"2", "OperationId":"Guid (str)" } + + From next version(2.10+) we accept integer values for EventPid and EventTid fields. But we still support string type for backward compatability """ Version = "Version" Timestamp = "Timestamp" @@ -323,15 +325,20 @@ def _parse_event_and_ensure_it_is_valid(self, extension_event): :param extension_event: The json event from file :return: Verified Json event that qualifies the contract. """ - - clean_string = lambda x: x.strip() if x is not None else x + def _clean_value(k, v): + if v is not None: + if isinstance(v, int): + if k.lower() in [ExtensionEventSchema.EventPid.lower(), ExtensionEventSchema.EventTid.lower()]: + return str(v) + return v.strip() + return v event_size = 0 key_err_msg = "{0}: {1} not found" # Convert the dict to all lower keys to avoid schema confusion. # Only pick the params that we care about and skip the rest. - event = dict((k.lower(), clean_string(v)) for k, v in extension_event.items() if + event = dict((k.lower(), _clean_value(k, v)) for k, v in extension_event.items() if k.lower() in self._EXTENSION_EVENT_REQUIRED_FIELDS) # Trim message and only pick the first 3k chars diff --git a/tests/data/events/extension_events/int_type/1519934744.json b/tests/data/events/extension_events/int_type/1519934744.json new file mode 100644 index 0000000000..01773a9ad4 --- /dev/null +++ b/tests/data/events/extension_events/int_type/1519934744.json @@ -0,0 +1,10 @@ +{ + "EventLevel": "INFO", + "Message": "Accept int value for eventpid and eventtid", + "Version": "1", + "TaskName": "Downloading files", + "EventPid": 3228, + "EventTid": 1, + "OpErAtiOnID": "519e4beb-018a-4bd9-8d8e-c5226cf7f56e", + "TimeStamp": "2023-03-13T01:21:05.1960563Z" +} \ No newline at end of file diff --git a/tests/ga/test_collect_telemetry_events.py b/tests/ga/test_collect_telemetry_events.py index f429bd52aa..bdd763effb 100644 --- a/tests/ga/test_collect_telemetry_events.py +++ b/tests/ga/test_collect_telemetry_events.py @@ -309,6 +309,16 @@ def test_it_should_parse_special_chars_properly(self): self._assert_handler_data_in_event_list(telemetry_events, extensions_with_count) + def test_it_should_parse_int_type_for_eventpid_or_eventtid_properly(self): + with self._create_extension_telemetry_processor() as extension_telemetry_processor: + extensions_with_count = self._create_random_extension_events_dir_with_events(2, os.path.join( + self._TEST_DATA_DIR, "int_type")) + + extension_telemetry_processor.run() + telemetry_events = self._get_handlers_with_version(extension_telemetry_processor.event_list) + + self._assert_handler_data_in_event_list(telemetry_events, extensions_with_count) + def _setup_and_assert_tests_for_max_sizes(self, no_of_extensions=2, expected_count=None): with self._create_extension_telemetry_processor() as extension_telemetry_processor: extensions_with_count = self._create_random_extension_events_dir_with_events(no_of_extensions, From e9b51d7d867f97db1af4b8c0b28e87fd1c142bbc Mon Sep 17 00:00:00 2001 From: Nageswara Nandigam <84482346+nagworld9@users.noreply.github.com> Date: Tue, 21 Mar 2023 14:23:42 -0700 Subject: [PATCH 54/63] update swap counter not found error log (#2789) --- azurelinuxagent/common/cgroup.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/azurelinuxagent/common/cgroup.py b/azurelinuxagent/common/cgroup.py index b22ea2994e..b2bf32fbc1 100644 --- a/azurelinuxagent/common/cgroup.py +++ b/azurelinuxagent/common/cgroup.py @@ -360,8 +360,7 @@ def try_swap_memory_usage(self): except CounterNotFound as e: if self._counter_not_found_error_count < 1: logger.periodic_info(logger.EVERY_HALF_HOUR, - 'Could not find swap counter from "memory.stat" file in the cgroup: {0}.' - ' Internal error: {1}'.format(self.path, ustr(e))) + '{0} from "memory.stat" file in the cgroup: {1}---[Note: This log for informational purpose only and can be ignored]'.format(ustr(e), self.path)) self._counter_not_found_error_count += 1 return 0 From 1640510271bed665b239d3e3a5b332f935fb3fcf Mon Sep 17 00:00:00 2001 From: Norberto Arrieta Date: Wed, 22 Mar 2023 10:13:04 -0700 Subject: [PATCH 55/63] Check agent log for errors; install test libraries (#2787) * Check agent log for errors; install test libraries --------- Co-authored-by: narrieta --- .../orchestrator/lib/agent_test_suite.py | 132 ++++-- .../orchestrator/scripts/check-agent-log.py | 49 ++ .../{get-waagent-path => get-agent-path} | 3 +- tests_e2e/orchestrator/scripts/install-agent | 18 +- tests_e2e/orchestrator/scripts/install-tools | 111 ++++- tests_e2e/tests/lib/agent_log.py | 446 ++++++++++++++++++ tests_e2e/tests/lib/agent_test.py | 5 + tests_e2e/tests/lib/ssh_client.py | 22 +- 8 files changed, 709 insertions(+), 77 deletions(-) create mode 100755 tests_e2e/orchestrator/scripts/check-agent-log.py rename tests_e2e/orchestrator/scripts/{get-waagent-path => get-agent-path} (98%) create mode 100644 tests_e2e/tests/lib/agent_log.py diff --git a/tests_e2e/orchestrator/lib/agent_test_suite.py b/tests_e2e/orchestrator/lib/agent_test_suite.py index 204546b6e7..7abd714343 100644 --- a/tests_e2e/orchestrator/lib/agent_test_suite.py +++ b/tests_e2e/orchestrator/lib/agent_test_suite.py @@ -16,6 +16,7 @@ # import contextlib import datetime +import json import logging import re import traceback @@ -29,6 +30,7 @@ # E0401: Unable to import 'lisa' (import-error) # etc from lisa import ( # pylint: disable=E0401 + Environment, Logger, Node, notifier, @@ -43,11 +45,13 @@ import makepkg from azurelinuxagent.common.version import AGENT_VERSION from tests_e2e.orchestrator.lib.agent_test_loader import TestSuiteInfo +from tests_e2e.tests.lib.agent_log import AgentLog from tests_e2e.tests.lib.agent_test import TestSkipped from tests_e2e.tests.lib.agent_test_context import AgentTestContext from tests_e2e.tests.lib.identifiers import VmIdentifier from tests_e2e.tests.lib.logging import log as agent_test_logger # Logger used by the tests from tests_e2e.tests.lib.logging import set_current_thread_log +from tests_e2e.tests.lib.agent_log import AgentLogRecord from tests_e2e.tests.lib.shell import run_command from tests_e2e.tests.lib.ssh_client import SshClient @@ -256,7 +260,7 @@ def _clean_up(self) -> None: def _setup_node(self) -> None: """ - Prepares the remote node for executing the test suite. + Prepares the remote node for executing the test suite (installs tools and the test agent, etc) """ self._log.info("") self._log.info("************************************** [Node Setup] **************************************") @@ -265,23 +269,27 @@ def _setup_node(self) -> None: self._log.info("Resource Group: %s", self.context.vm.resource_group) self._log.info("") - self._install_tools_on_node() + self.context.ssh_client.run_command("mkdir -p ~/bin/tests_e2e/tests; touch ~/bin/agent-env") - if self.context.is_vhd: - self._log.info("Using a VHD; will not install the test Agent.") - else: - self._install_agent_on_node() + # Copy the test tools + tools_path = self.context.test_source_directory/"orchestrator"/"scripts" + tools_target_path = Path("~/bin") + self._log.info("Copying %s to %s:%s", tools_path, self.context.node.name, tools_target_path) + self.context.ssh_client.copy_to_node(tools_path, tools_target_path, recursive=True) - def _install_tools_on_node(self) -> None: - """ - Installs the test tools on the test node - """ - self.context.ssh_client.run_command("mkdir -p ~/bin") + # Copy the test libraries + lib_path = self.context.test_source_directory/"tests"/"lib" + lib_target_path = Path("~/bin/tests_e2e/tests") + self._log.info("Copying %s to %s:%s", lib_path, self.context.node.name, lib_target_path) + self.context.ssh_client.copy_to_node(lib_path, lib_target_path, recursive=True) - tools_path = self.context.test_source_directory/"orchestrator"/"scripts" - self._log.info(f"Copying {tools_path} to the test node") - self.context.ssh_client.copy(tools_path, Path("~/bin"), remote_target=True, recursive=True) + # Copy the test agent + agent_package_path: Path = self._get_agent_package_path() + agent_package_target_path = Path("~/bin")/agent_package_path.name + self._log.info("Copying %s to %s:%s", agent_package_path, self.context.node.name, agent_package_target_path) + self.context.ssh_client.copy_to_node(agent_package_path, agent_package_target_path) + # Copy Pypy if self.context.ssh_client.get_architecture() == "aarch64": pypy_path = Path("/tmp/pypy3.7-arm64.tar.bz2") pypy_download = "https://downloads.python.org/pypy/pypy3.7-v7.3.5-aarch64.tar.bz2" @@ -292,26 +300,21 @@ def _install_tools_on_node(self) -> None: if not pypy_path.exists(): self._log.info(f"Downloading {pypy_download} to {pypy_path}") run_command(["wget", pypy_download, "-O", pypy_path]) - self._log.info(f"Copying {pypy_path} to the test node") - self.context.ssh_client.copy(pypy_path, Path("~/bin/pypy3.7.tar.bz2"), remote_target=True) + pypy_target_path = Path("~/bin/pypy3.7.tar.bz2") + self._log.info("Copying %s to %s:%s", pypy_path, self.context.node.name, pypy_target_path) + self.context.ssh_client.copy_to_node(pypy_path, pypy_target_path) - self._log.info(f'Installing tools on the test node\n{self.context.ssh_client.run_command("~/bin/scripts/install-tools")}') - self._log.info(f'Remote commands will use {self.context.ssh_client.run_command("which python3")}') + # Install the tools and libraries + install_command = lambda: self.context.ssh_client.run_command(f"~/bin/scripts/install-tools --agent-package {agent_package_target_path}") + self._log.info('Installing tools on the test node\n%s', install_command()) + self._log.info('Remote commands will use %s', self.context.ssh_client.run_command("which python3")) - def _install_agent_on_node(self) -> None: - """ - Installs the given agent package on the test node. - """ - agent_package_path: Path = self._get_agent_package_path() - - self._log.info("Installing %s on %s", agent_package_path, self.context.node.name) - agent_package_remote_path = self.context.remote_working_directory/agent_package_path.name - self._log.info("Copying %s to %s:%s", agent_package_path, self.context.node.name, agent_package_remote_path) - self.context.ssh_client.copy(agent_package_path, agent_package_remote_path, remote_target=True) - stdout = self.context.ssh_client.run_command(f"install-agent --package {agent_package_remote_path} --version {AGENT_VERSION}", use_sudo=True) - self._log.info(stdout) - - self._log.info("The agent was installed successfully.") + # Install the agent + if self.context.is_vhd: + self._log.info("Using a VHD; will not install the Test Agent.") + else: + install_command = lambda: self.context.ssh_client.run_command(f"install-agent --package {agent_package_target_path} --version {AGENT_VERSION}", use_sudo=True) + self._log.info("Installing the Test Agent on %s\n%s", self.context.node.name, install_command()) def _collect_node_logs(self) -> None: """ @@ -327,23 +330,25 @@ def _collect_node_logs(self) -> None: remote_path = "/tmp/waagent-logs.tgz" local_path = self.context.log_path/'{0}.tgz'.format(self.context.image_name) self._log.info("Copying %s:%s to %s", self.context.node.name, remote_path, local_path) - self.context.ssh_client.copy(remote_path, local_path, remote_source=True) + self.context.ssh_client.copy_from_node(remote_path, local_path) + except: # pylint: disable=bare-except self._log.exception("Failed to collect logs from the test machine") @TestCaseMetadata(description="", priority=0) - def agent_test_suite(self, node: Node, variables: Dict[str, Any], log_path: str, log: Logger) -> None: + def agent_test_suite(self, node: Node, environment: Environment, variables: Dict[str, Any], log_path: str, log: Logger) -> None: """ Executes each of the AgentTests included in the "c_test_suites" variable (which is generated by the AgentTestSuitesCombinator). """ self._set_context(node, variables, log_path, log) - with _set_thread_name(self.context.image_name): # The thread name is added to self._log - # E1133: Non-iterable value self.context.test_suites is used in an iterating context (not-an-iterable) - # (OK to iterate, test_suite is a List) + # Set the thread name to the image; this name is added to self._log + with _set_thread_name(self.context.image_name): + # Log the environment's name and the variables received from the runbook (note that we need to expand the names of the test suites) + self._log.info("LISA Environment: %s", environment.name) self._log.info( - "Test suite parameters: [test_suites: %s] [skip_setup: %s] [collect_logs: %s]", - [t.name for t in self.context.test_suites], self.context.skip_setup, self.context.collect_logs) # pylint: disable=E1133 + "Runbook variables:\n%s", + '\n'.join([f"\t{name}: {value if name != 'c_test_suites' else [t.name for t in value] }" for name, value in variables.items()])) start_time: datetime.datetime = datetime.datetime.now() test_suite_success = True @@ -361,6 +366,8 @@ def agent_test_suite(self, node: Node, variables: Dict[str, Any], log_path: str, for suite in self.context.test_suites: # pylint: disable=E1133 test_suite_success = self._execute_test_suite(suite) and test_suite_success + test_suite_success = self._check_agent_log() and test_suite_success + finally: collect = self.context.collect_logs if collect == CollectLogs.Always or collect == CollectLogs.Failed and not test_suite_success: @@ -475,6 +482,53 @@ def _execute_test_suite(self, suite: TestSuiteInfo) -> bool: return success + def _check_agent_log(self) -> bool: + """ + Checks the agent log for errors; returns true on success (no errors int the log) + """ + start_time: datetime.datetime = datetime.datetime.now() + + self._log.info("Checking agent log on the test node") + output = self.context.ssh_client.run_command("check-agent-log.py -j") + errors = json.loads(output, object_hook=AgentLogRecord.from_dictionary) + + # Individual tests may have rules to ignore known errors; filter those out + ignore_error_rules = [] + # pylint seems to think self.context.test_suites is not iterable. Suppressing warning, since its type is List[AgentTestSuite] + # E1133: Non-iterable value self.context.test_suites is used in an iterating context (not-an-iterable) + for suite in self.context.test_suites: # pylint: disable=E1133 + for test in suite.tests: + ignore_error_rules.extend(test(self.context).get_ignore_error_rules()) + + if len(ignore_error_rules) > 0: + new = [] + for e in errors: + if not AgentLog.matches_ignore_rule(e, ignore_error_rules): + new.append(e) + errors = new + + if len(errors) == 0: + # If no errors, we are done; don't create a log or test result. + self._log.info("There are no errors in the agent log") + return True + + log_path: Path = self.context.log_path/f"CheckAgentLog-{self.context.image_name}.log" + message = f"Detected {len(errors)} error(s) in the agent log. See {log_path} for a full report." + self._log.info(message) + + with set_current_thread_log(log_path): + agent_test_logger.info("Detected %s error(s) in the agent log:\n\n%s", len(errors), '\n'.join(['\t' + e.text for e in errors])) + + self._report_test_result( + self.context.image_name, + "CheckAgentLog", + TestStatus.FAILED, + start_time, + message=message + '\n' + '\n'.join([e.text for e in errors[0:3]]), + add_exception_stack_trace=True) + + return False + @staticmethod def _report_test_result( suite_name: str, diff --git a/tests_e2e/orchestrator/scripts/check-agent-log.py b/tests_e2e/orchestrator/scripts/check-agent-log.py new file mode 100755 index 0000000000..231e7bcd05 --- /dev/null +++ b/tests_e2e/orchestrator/scripts/check-agent-log.py @@ -0,0 +1,49 @@ +#!/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. +# + +import argparse +import json +import sys + +from pathlib import Path +from tests_e2e.tests.lib.agent_log import AgentLog + +try: + parser = argparse.ArgumentParser() + parser.add_argument('path', nargs='?', help='Path of the log file', default='/var/log/waagent.log') + parser.add_argument('-j', '--json', action='store_true', help='Produce a JSON report') + parser.set_defaults(json=False) + args = parser.parse_args() + + error_list = AgentLog(Path(args.path)).get_errors() + + if args.json: + print(json.dumps(error_list, default=lambda o: o.__dict__)) + else: + if len(error_list) == 0: + print("No errors were found.") + else: + for e in error_list: + print(e.text) + +except Exception as e: + print(f"{e}", file=sys.stderr) + sys.exit(1) + +sys.exit(0) diff --git a/tests_e2e/orchestrator/scripts/get-waagent-path b/tests_e2e/orchestrator/scripts/get-agent-path similarity index 98% rename from tests_e2e/orchestrator/scripts/get-waagent-path rename to tests_e2e/orchestrator/scripts/get-agent-path index c177585695..e2e44f453f 100755 --- a/tests_e2e/orchestrator/scripts/get-waagent-path +++ b/tests_e2e/orchestrator/scripts/get-agent-path @@ -22,8 +22,6 @@ # set -euo pipefail -# - # On most distros, 'waagent' is in PATH if which waagent 2> /dev/null; then exit 0 @@ -42,6 +40,7 @@ fi # try some well-known locations declare -a known_locations=( + "/usr/sbin/waagent" "/usr/share/oem/bin/waagent" ) diff --git a/tests_e2e/orchestrator/scripts/install-agent b/tests_e2e/orchestrator/scripts/install-agent index 64d96307a3..868b9fa788 100755 --- a/tests_e2e/orchestrator/scripts/install-agent +++ b/tests_e2e/orchestrator/scripts/install-agent @@ -72,6 +72,14 @@ else fi echo "Service name: $service_name" +# +# Find the path to the Agent's executable file +# +waagent=$(get-agent-path) +echo "Agent's path: $waagent" +$waagent --version +echo "" + # # Install the package # @@ -98,20 +106,13 @@ service-start $service_name # # Verify that the new agent is running and output its status. # Note that the extension handler may take some time to start so give 1 minute. -# Also, note that the default Python is set to Pypy, so before executing 'waagent' we need to set the -# Python path to the location of the azurelinuxagent module. # echo "Verifying agent installation..." -PYTHONPATH=$(get-agent-pythonpath) -export PYTHONPATH - check-version() { - waagent=$(get-waagent-path) - for i in {0..5} do - if $waagent --version | grep -E "Goal state agent:\s+$1" > /dev/null; then + if $waagent --version | grep -E "Goal state agent:\s+$version" > /dev/null; then return 0 fi sleep 10 @@ -128,7 +129,6 @@ else exit_code=1 fi -waagent=$(get-waagent-path) $waagent --version printf "\n" service-status $service_name diff --git a/tests_e2e/orchestrator/scripts/install-tools b/tests_e2e/orchestrator/scripts/install-tools index 04dbb4f653..6feb71e530 100755 --- a/tests_e2e/orchestrator/scripts/install-tools +++ b/tests_e2e/orchestrator/scripts/install-tools @@ -25,47 +25,112 @@ set -euo pipefail +usage() ( + echo "Usage: install-tools -p|--agent-package " + exit 1 +) + +while [[ $# -gt 0 ]]; do + case $1 in + -p|--agent-package) + shift + if [ "$#" -lt 1 ]; then + usage + fi + agent_package=$1 + shift + ;; + *) + usage + esac +done +if [ "$#" -ne 0 ] || [ -z ${agent_package+x} ]; then + usage +fi + echo "Installing scripts to ~/bin" mv ~/bin/scripts/* ~/bin rm -r ~/bin/scripts +PATH="$HOME/bin:$PATH" -echo "Installing Pypy to ~/bin" -# bzip2/lbzip2 (used by tar to uncompress *.bz2 files) are not available by default in some distros; -# use Python to uncompress the Pypy tarball. +# If the system's Python is <= 3.7, install Pypy and make it the default Python for the user executing the tests python=$(~/bin/find-python) -$python ~/bin/uncompress.py ~/bin/pypy3.7.tar.bz2 ~/bin/pypy3.7.tar -tar xf ~/bin/pypy3.7.tar -C ~/bin -rm ~/bin/pypy3.7.tar ~/bin/pypy3.7.tar.bz2 +python_version=$($python -c 'import sys; print("{0:02}.{1:02}".format(sys.version_info[0], sys.version_info[1]))') +echo "Python: $python ($python_version)" +if [[ $python_version < "03.07" ]]; then + echo "Installing Pypy 3.7 to ~/bin and making it the default Python for user $USER" + # bzip2/lbzip2 (used by tar to uncompress *.bz2 files) are not available by default in some distros; + # use Python to uncompress the Pypy tarball. + $python ~/bin/uncompress.py ~/bin/pypy3.7.tar.bz2 ~/bin/pypy3.7.tar + tar xf ~/bin/pypy3.7.tar -C ~/bin + rm ~/bin/pypy3.7.tar ~/bin/pypy3.7.tar.bz2 -if [[ -e ~/bin/python ]]; then - rm ~/bin/python -fi -ln -s ~/bin/pypy*/bin/pypy3.7 ~/bin/python + if [[ -e ~/bin/python ]]; then + rm ~/bin/python + fi + ln -s ~/bin/pypy*/bin/pypy3.7 ~/bin/python + + if [[ -e ~/bin/python3 ]]; then + rm ~/bin/python3 + fi + ln -s ~/bin/pypy*/bin/pypy3.7 ~/bin/python3 -if [[ -e ~/bin/python3 ]]; then - rm ~/bin/python3 + echo "Installing the 'distro' module" + python3 -m ensurepip + python3 -mpip install -U pip wheel + python3 -mpip install distro +else + # In some distros (e.g. Flatcar), Python is not in PATH; in that case create a symlink under ~/bin + if ! which python3 > /dev/null 2>&1; then + if [[ ! $python_version < "03.00" ]]; then + echo "Python ($python) is not in PATH; creating symbolic links as ~/bin/python and ~/bin/python3" + ln -s "$python" ~/bin/python + ln -s "$python" ~/bin/python3 + fi + fi fi -ln -s ~/bin/pypy*/bin/pypy3.7 ~/bin/python3 -# Note that we place $HOME/bin at the front of PATH, so the tools in ~/bin (including python) will +echo "Installing Agent modules to ~/bin" +unzip.py "$agent_package" ~/bin/WALinuxAgent +unzip.py ~/bin/WALinuxAgent/bin/WALinuxAgent-*.egg ~/bin/WALinuxAgent/bin/WALinuxAgent.egg +mv ~/bin/WALinuxAgent/bin/WALinuxAgent.egg/azurelinuxagent ~/bin +rm -rf ~/bin/WALinuxAgent + +# +# Create ~/bin/agent-env to set PATH and PYTHONPATH. +# +# We add $HOME/bin to the front of PATH, so tools in ~/bin (including python) will # take precedence over system tools with the same name. +# +# We set PYTHONPATH to include the location of the agent modules installed on the VM image and also +# the test modules we copied to ~/bin. +# +# +echo "Creating ~/bin/agent-env to set PATH and PYTHONPATH" +echo ' +if [[ $PATH != *"$HOME/bin"* ]]; then + PATH="$HOME/bin:$PATH:" +fi + +export PYTHONPATH="$HOME/bin" +' > ~/bin/agent-env +chmod u+x ~/bin/agent-env + +echo "Adding ~/bin/agent-env to ~/.bash_profile" # In some distros, e.g. Flatcar, .bash_profile is a symbolic link to a read-only file. Make a copy in # that case. -echo "Adding ~/bin to PATH in .bash_profile" if test -e ~/.bash_profile && ls -l .bash_profile | grep '\->'; then cp ~/.bash_profile ~/.bash_profile-bk rm ~/.bash_profile mv ~/.bash_profile-bk ~/.bash_profile fi -if ! test -e ~/.bash_profile || ! grep '# Add $HOME/bin to $PATH' ~/.bash_profile > /dev/null; then - echo ' -# Add $HOME/bin to $PATH -if [[ $PATH != *"$HOME/bin"* ]]; then - PATH="$HOME/bin:$PATH:" -fi +if ! test -e ~/.bash_profile || ! grep '~/bin/agent-env' ~/.bash_profile > /dev/null; then + echo 'source ~/bin/agent-env ' >> ~/.bash_profile fi -echo "python3 has been set to $(which python3)" +source ~/bin/agent-env +echo "PATH=$PATH" +echo "PYTHONPATH=$PYTHONPATH" +echo "python3 -> $(which python3)" python3 --version - diff --git a/tests_e2e/tests/lib/agent_log.py b/tests_e2e/tests/lib/agent_log.py new file mode 100644 index 0000000000..61b4ca85cd --- /dev/null +++ b/tests_e2e/tests/lib/agent_log.py @@ -0,0 +1,446 @@ +# +# 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. +# + +import os +import re + +from datetime import datetime +from pathlib import Path +from typing import Any, AnyStr, Dict, Iterable, List, Match + +from azurelinuxagent.common.version import DISTRO_NAME, DISTRO_VERSION + + +class AgentLogRecord: + """ + Represents an entry in the Agent's log (note that entries can span multiple lines in the log) + + Sample message: + 2023-03-13T15:44:04.906673Z INFO ExtHandler ExtHandler Azure Linux Agent (Goal State Agent version 9.9.9.9) + """ + text: str # Full text of the record + when: str # Timestamp (as text) + level: str # Level (INFO, ERROR, etc) + thread: str # Thread name (e.g. 'Daemon', 'ExtHandler') + prefix: str # Prefix (e.g. 'Daemon', 'ExtHandler', ) + message: str # Message + + @staticmethod + def from_match(match: Match[AnyStr]): + """Builds a record from a regex match""" + record = AgentLogRecord() + record.text = match.string + record.when = match.group("when") + record.level = match.group("level") + record.thread = match.group("thread") + record.prefix = match.group("prefix") + record.message = match.group("message") + return record + + @staticmethod + def from_dictionary(dictionary: Dict[str, str]): + """Deserializes from a dict""" + record = AgentLogRecord() + record.text = dictionary["text"] + record.when = dictionary["when"] + record.level = dictionary["level"] + record.thread = dictionary["thread"] + record.prefix = dictionary["prefix"] + record.message = dictionary["message"] + return record + + @property + def timestamp(self) -> datetime: + return datetime.strptime(self.when, u'%Y-%m-%dT%H:%M:%S.%fZ') + + +class AgentLog(object): + """ + Provides facilities to parse and/or extract errors from the agent's log. + """ + def __init__(self, path: Path = Path('/var/log/waagent.log')): + self._path: Path = path + self._counter_table: Dict[str, int] = {} + + def get_errors(self) -> List[AgentLogRecord]: + """ + Returns any ERRORs or WARNINGs in the agent log. + + The function filters out known/uninteresting errors, which are kept in the 'ignore_list' variable. + """ + # + # Items in this list are known errors and they are ignored. + # + # * 'message' - A regular expression matched using re.search; be sure to escape any regex metacharacters. A positive match indicates + # that the error should be ignored + # * 'if' - A lambda that takes as parameter an AgentLogRecord representing an error and returns true if the error should be ignored + # + ignore_rules = [ + # + # NOTE: This list was taken from the older agent tests and needs to be cleaned up. Feel free to un-comment rules as new tests are added. + # + # # This warning is expected on CentOS/RedHat 7.4, 7.8 and Redhat 7.6 + # { + # 'message': r"Move rules file 70-persistent-net.rules to /var/lib/waagent/70-persistent-net.rules", + # 'if': lambda r: + # re.match(r"(((centos|redhat)7\.[48])|(redhat7\.6)|(redhat8\.2))\D*", DISTRO_NAME, flags=re.IGNORECASE) is not None + # and r.level == "WARNING" + # and r.prefix == "ExtHandler" and r.thread in ("", "EnvHandler") + # }, + # # This warning is expected on SUSE 12 + # { + # 'message': r"WARNING EnvHandler ExtHandler Move rules file 75-persistent-net-generator.rules to /var/lib/waagent/75-persistent-net-generator.rules", + # 'if': lambda _: re.match(r"((sles15\.2)|suse12)\D*", DISTRO_NAME, flags=re.IGNORECASE) is not None + # }, + # # The following message is expected to log an error if systemd is not enabled on it + # { + # 'message': r"Did not detect Systemd, unable to set wa(|linux)agent-network-setup.service", + # 'if': lambda _: not self._is_systemd() + # }, + # # + # # Journalctl in Debian 8.11 does not have the --utc option by default. + # # Ignoring this error for Deb 8 as its not a blocker and since Deb 8 is old and not widely used + # { + # 'message': r"journalctl: unrecognized option '--utc'", + # 'if': lambda r: re.match(r"(debian8\.11)\D*", DISTRO_NAME, flags=re.IGNORECASE) is not None and r.level == "WARNING" + # }, + # # Sometimes it takes the Daemon some time to identify primary interface and the route to Wireserver, + # # ignoring those errors if they come from the Daemon. + # { + # 'message': r"(No route exists to \d+\.\d+\.\d+\.\d+|" + # r"Could not determine primary interface, please ensure \/proc\/net\/route is correct|" + # r"Contents of \/proc\/net\/route:|Primary interface examination will retry silently)", + # 'if': lambda r: r.prefix == "Daemon" + # }, + # + # # This happens in CENTOS and RHEL when waagent attempt to format and mount the error while cloud init is already doing it + # # 2021-09-20T06:45:57.253801Z WARNING Daemon Daemon Could not mount resource disk: mount: /dev/sdb1 is already mounted or /mnt/resource busy + # # /dev/sdb1 is already mounted on /mnt/resource + # { + # 'message': r"Could not mount resource disk: mount: \/dev\/sdb1 is already mounted or \/mnt\/resource busy", + # 'if': lambda r: + # re.match(r"((centos7\.8)|(redhat7\.8)|(redhat7\.6)|(redhat8\.2))\D*", DISTRO_NAME, flags=re.IGNORECASE) + # and r.level == "WARNING" + # and r.prefix == "Daemon" + # }, + # # + # # 2021-09-20T06:45:57.246593Z ERROR Daemon Daemon Command: [mkfs.ext4 -F /dev/sdb1], return code: [1], result: [mke2fs 1.42.9 (28-Dec-2013) + # # /dev/sdb1 is mounted; will not make a filesystem here! + # { + # 'message': r"Command: \[mkfs.ext4 -F \/dev\/sdb1\], return code: \[1\]", + # 'if': lambda r: + # re.match(r"((centos7\.8)|(redhat7\.8)|(redhat7\.6)|(redhat8\.2))\D*", DISTRO_NAME, flags=re.IGNORECASE) + # and r.level == "ERROR" + # and r.prefix == "Daemon" + # }, + # # + # # 2022-01-20T06:52:21.515447Z WARNING Daemon Daemon Fetch failed: [HttpError] [HTTP Failed] GET https://dcrgajhx62.blob.core.windows.net/$system/edprpwqbj6.5c2ddb5b-d6c3-4d73-9468-54419ca87a97.vmSettings -- IOError timed out -- 6 attempts made + # # + # # The daemon does not need the artifacts profile blob, but the request is done as part of protocol initialization. This timeout can be ignored, if the issue persist the log would include additional instances. + # # + # { + # 'message': r"\[HTTP Failed\] GET https://.*\.vmSettings -- IOError timed out", + # 'if': lambda r: r.level == "WARNING" and r.prefix == "Daemon" + # }, + # + # Probably the agent should log this as INFO, but for now it is a warning + # e.g. + # 2021-07-29T04:40:17.190879Z WARNING EnvHandler ExtHandler Dhcp client is not running. + # old agents logs don't have a prefix of thread and/or logger names. + { + 'message': r"Dhcp client is not running.", + 'if': lambda r: r.level == "WARNING" + }, + # Known bug fixed in the current agent, but still present in older daemons + # + { + 'message': r"\[CGroupsException\].*Error: join\(\) argument must be str, bytes, or os.PathLike object, not 'NoneType'", + 'if': lambda r: r.level == "WARNING" and r.prefix == "Daemon" + }, + # This warning is expected on when WireServer gives us the incomplete goalstate without roleinstance data + { + 'message': r"\[ProtocolError\] Fetched goal state without a RoleInstance", + }, + # + # Download warnings (manifest and zips). + # + # Examples: + # 2021-03-31T03:48:35.216494Z WARNING ExtHandler ExtHandler Fetch failed: [HttpError] [HTTP Failed] GET https://zrdfepirv2cbn04prdstr01a.blob.core.windows.net/f72653efd9e349ed9842c8b99e4c1712/Microsoft.CPlat.Core_NullSeqA_useast2euap_manifest.xml -- IOError ('The read operation timed out',) -- 1 attempts made + # 2021-03-31T06:54:29.655861Z WARNING ExtHandler ExtHandler Fetch failed: [HttpError] [HTTP Retry] GET http://168.63.129.16:32526/extensionArtifact -- Status Code 502 -- 1 attempts made + # 2021-03-31T06:43:17.806663Z WARNING ExtHandler ExtHandler Download failed, switching to host plugin + { + 'message': r"(Fetch failed: \[HttpError\] .+ GET .+ -- [0-9]+ attempts made)|(Download failed, switching to host plugin)", + 'if': lambda r: r.level == "WARNING" and r.prefix == "ExtHandler" and r.thread == "ExtHandler" + }, + # 2021-07-09T01:46:53.307959Z INFO MonitorHandler ExtHandler [CGW] Disabling resource usage monitoring. Reason: Check on cgroups failed: + # [CGroupsException] The agent's cgroup includes unexpected processes: ['[PID: 2367] UNKNOWN'] + { + 'message': r"The agent's cgroup includes unexpected processes: \[('\[PID:\s?\d+\]\s*UNKNOWN'(,\s*)?)+\]" + }, + # 2021-12-20T07:46:23.020197Z INFO ExtHandler ExtHandler [CGW] The agent's process is not within a memory cgroup + # Ignoring this since memory cgroup(MemoryAccounting) not enabled. + { + 'message': r"The agent's process is not within a memory cgroup", + 'if': lambda r: re.match(r"(((centos|redhat)7\.[48])|(redhat7\.6)|(redhat8\.2))\D*", DISTRO_NAME, flags=re.IGNORECASE) + }, + # + # Ubuntu 22 uses cgroups v2, so we need to ignore these: + # + # 2023-03-15T20:47:56.684849Z INFO ExtHandler ExtHandler [CGW] The CPU cgroup controller is not mounted + # 2023-03-15T20:47:56.685392Z INFO ExtHandler ExtHandler [CGW] The memory cgroup controller is not mounted + # 2023-03-15T20:47:56.688576Z INFO ExtHandler ExtHandler [CGW] The agent's process is not within a CPU cgroup + # 2023-03-15T20:47:56.688981Z INFO ExtHandler ExtHandler [CGW] The agent's process is not within a memory cgroup + # + { + 'message': r"\[CGW\]\s*(The (CPU|memory) cgroup controller is not mounted)|(The agent's process is not within a (CPU|memory) cgroup)", + 'if': lambda r: DISTRO_NAME == 'ubuntu' and DISTRO_VERSION >= '22.00' + }, + # + # 2022-02-09T04:50:37.384810Z ERROR ExtHandler ExtHandler Error fetching the goal state: [ProtocolError] GET vmSettings [correlation ID: 2bed9b62-188e-4668-b1a8-87c35cfa4927 eTag: 7031887032544600793]: [Internal error in HostGAPlugin] [HTTP Failed] [502: Bad Gateway] b'{ "errorCode": "VMArtifactsProfileBlobContentNotFound", "message": "VM artifacts profile blob has no content in it.", "details": ""}' + # + # Fetching the goal state may catch the HostGAPlugin in the process of computing the vmSettings. This can be ignored, if the issue persist the log would include other errors as well. + # + { + 'message': r"\[ProtocolError\] GET vmSettings.*VMArtifactsProfileBlobContentNotFound", + 'if': lambda r: r.level == "ERROR" + }, + # + # 2022-11-01T02:45:55.513692Z ERROR ExtHandler ExtHandler Error fetching the goal state: [ProtocolError] GET vmSettings [correlation ID: 616873cc-be87-41b6-83b7-ef3a76370628 eTag: 3693655388249891516]: [Internal error in HostGAPlugin] [HTTP Failed] [502: Bad Gateway] { "errorCode": "InternalError", "message": "The server encountered an internal error. Please retry the request.", "details": ""} + # + # Fetching the goal state may catch the HostGAPlugin in the process of computing the vmSettings. This can be ignored, if the issue persist the log would include other errors as well. + # + { + 'message': r"\[ProtocolError\] GET vmSettings.*Please retry the request", + 'if': lambda r: r.level == "ERROR" + }, + # + # 2022-08-16T01:50:10.759502Z ERROR ExtHandler ExtHandler Error fetching the goal state: [ProtocolError] GET vmSettings [correlation ID: e162f7c3-8d0c-4a9b-a987-8f9ec0699dae eTag: 9757461589808963322]: Timeout + # + # Fetching the goal state may hit timeouts in the HostGAPlugin's vmSettings. This can be ignored, if the issue persist the log would include other errors as well. + # + { + 'message': r"\[ProtocolError\] GET vmSettings.*Timeout", + 'if': lambda r: r.level == "ERROR" + }, + # + # 2021-12-29T06:50:49.904601Z ERROR ExtHandler ExtHandler Error fetching the goal state: [ProtocolError] Error fetching goal state Inner error: [ResourceGoneError] [HTTP Failed] [410: Gone] The page you requested was removed. + # 2022-03-21T02:44:03.770017Z ERROR ExtHandler ExtHandler Error fetching the goal state: [ProtocolError] Error fetching goal state Inner error: [ResourceGoneError] Resource is gone + # 2022-02-16T04:46:50.477315Z WARNING Daemon Daemon Fetching the goal state failed: [ResourceGoneError] [HTTP Failed] [410: Gone] b'\n\n ResourceNotAvailable\n The resource requested is no longer available. Please refresh your cache.\n
\n
' + # + # ResourceGone can happen if we are fetching one of the URIs in the goal state and a new goal state arrives + { + 'message': r"(?s)(Fetching the goal state failed|Error fetching goal state|Error fetching the goal state).*(\[ResourceGoneError\]|\[410: Gone\]|Resource is gone)", + 'if': lambda r: r.level in ("WARNING", "ERROR") + }, + # + # 2022-12-02T05:45:51.771876Z ERROR ExtHandler ExtHandler Error fetching the goal state: [ProtocolError] [Wireserver Exception] [HttpError] [HTTP Failed] GET http://168.63.129.16/machine/ -- IOError [Errno 104] Connection reset by peer -- 6 attempts made + # + { + 'message': r"\[HttpError\] \[HTTP Failed\] GET http://168.63.129.16/machine/ -- IOError \[Errno 104\] Connection reset by peer", + 'if': lambda r: r.level in ("WARNING", "ERROR") + }, + # + # 2022-03-08T03:03:23.036161Z WARNING ExtHandler ExtHandler Fetch failed from [http://168.63.129.16:32526/extensionArtifact]: [HTTP Failed] [400: Bad Request] b'' + # 2022-03-08T03:03:23.042008Z WARNING ExtHandler ExtHandler Fetch failed: [ProtocolError] Fetch failed from [http://168.63.129.16:32526/extensionArtifact]: [HTTP Failed] [400: Bad Request] b'' + # + # Warning downloading extension manifest. If the issue persists, this would cause errors elsewhere so safe to ignore + { + 'message': r"\[http://168.63.129.16:32526/extensionArtifact\]: \[HTTP Failed\] \[400: Bad Request\]", + 'if': lambda r: r.level == "WARNING" + }, + # + # 2022-03-29T05:52:10.089958Z WARNING ExtHandler ExtHandler An error occurred while retrieving the goal state: [ProtocolError] GET vmSettings [correlation ID: da106cf5-83a0-44ec-9484-d0e9223847ab eTag: 9856274988128027586]: Timeout + # + # Ignore warnings about timeouts in vmSettings; if the condition persists, an error will occur elsewhere. + # + { + 'message': r"GET vmSettings \[[^]]+\]: Timeout", + 'if': lambda r: r.level == "WARNING" + }, + # + # 2022-09-30T02:48:33.134649Z WARNING MonitorHandler ExtHandler Error in SendHostPluginHeartbeat: [HttpError] [HTTP Failed] GET http://168.63.129.16:32526/health -- IOError timed out -- 1 attempts made --- [NOTE: Will not log the same error for the next hour] + # + # Ignore timeouts in the HGAP's health API... those are tracked in the HGAP dashboard so no need to worry about them on test runs + # + { + 'message': r"SendHostPluginHeartbeat:.*GET http://168.63.129.16:32526/health.*timed out", + 'if': lambda r: r.level == "WARNING" + }, + # + # 2022-09-30T03:09:25.013398Z WARNING MonitorHandler ExtHandler Error in SendHostPluginHeartbeat: [ResourceGoneError] [HTTP Failed] [410: Gone] + # + # ResourceGone should not happen very often, since the monitor thread already refreshes the goal state before sending the HostGAPlugin heartbeat. Errors can still happen, though, since the goal state + # can change in-between the time at which the monitor thread refreshes and the time at which it sends the heartbeat. Ignore these warnings unless there are 2 or more of them. + # + { + 'message': r"SendHostPluginHeartbeat:.*ResourceGoneError.*410", + 'if': lambda r: r.level == "WARNING" and self._increment_counter("SendHostPluginHeartbeat-ResourceGoneError-410") < 2 # ignore unless there are 2 or more instances + }, + # 2023-01-18T02:58:25.589492Z ERROR SendTelemetryHandler ExtHandler Event: name=WALinuxAgent, op=ReportEventErrors, message=DroppedEventsCount: 1 + # Reasons (first 5 errors): [ProtocolError] [Wireserver Exception] [ProtocolError] [Wireserver Failed] URI http://168.63.129.16/machine?comp=telemetrydata [HTTP Failed] Status Code 400: Traceback (most recent call last): + # + { + 'message': r"(?s)SendTelemetryHandler.*http://168.63.129.16/machine\?comp=telemetrydata.*Status Code 400", + 'if': lambda _: self._increment_counter("SendTelemetryHandler-telemetrydata-Status Code 400") < 2 # ignore unless there are 2 or more instances + }, + # + # Ignore these errors in flatcar: + # + # 1) 2023-03-16T14:30:33.091427Z ERROR Daemon Daemon Failed to mount resource disk [ResourceDiskError] unable to detect disk topology + # 2) 2023-03-16T14:30:33.091708Z ERROR Daemon Daemon Event: name=WALinuxAgent, op=ActivateResourceDisk, message=[ResourceDiskError] unable to detect disk topology, duration=0 + # 3) 2023-03-16T14:30:34.660976Z WARNING ExtHandler ExtHandler Fetch failed: [HttpError] HTTPS is unavailable and required + # 4) 2023-03-16T14:30:34.800112Z ERROR ExtHandler ExtHandler Unable to setup the persistent firewall rules: [Errno 30] Read-only file system: '/lib/systemd/system/waagent-network-setup.service' + # + # 1, 2) under investigation + # 3) There seems to be a configuration issue in flatcar that prevents python from using HTTPS when trying to reach storage. This does not produce any actual errors, since the agent fallbacks to the HGAP. + # 4) Remove this when bug 17523033 is fixed. + # + { + 'message': r"(Failed to mount resource disk)|(unable to detect disk topology)", + 'if': lambda r: r.prefix == 'Daemon' and DISTRO_NAME == 'flatcar' + }, + { + 'message': r"(HTTPS is unavailable and required)|(Unable to setup the persistent firewall rules.*Read-only file system)", + 'if': lambda r: DISTRO_NAME == 'flatcar' + }, + # + # AzureSecurityLinuxAgent fails to install on a few distros (e.g. Debian 11) + # + # 2023-03-16T14:29:48.798415Z ERROR ExtHandler ExtHandler Event: name=Microsoft.Azure.Security.Monitoring.AzureSecurityLinuxAgent, op=Install, message=[ExtensionOperationError] Non-zero exit code: 56, /var/lib/waagent/Microsoft.Azure.Security.Monitoring.AzureSecurityLinuxAgent-2.21.115/handler.sh install + # + { + 'message': r"Microsoft.Azure.Security.Monitoring.AzureSecurityLinuxAgent.*op=Install.*Non-zero exit code: 56,", + }, + + ] + + def is_error(r: AgentLogRecord) -> bool: + return r.level in ('ERROR', 'WARNING') or any(err in r.text for err in ['Exception', 'Traceback', '[CGW]']) + + errors = [] + primary_interface_error = None + provisioning_complete = False + + for record in self.read(): + if is_error(record) and not self.matches_ignore_rule(record, ignore_rules): + # Handle "/proc/net/route contains no routes" and "/proc/net/route is missing headers" as a special case + # since it can take time for the primary interface to come up, and we don't want to report transient + # errors as actual errors. The last of these errors in the log will be reported + if "/proc/net/route contains no routes" in record.text or "/proc/net/route is missing headers" in record.text and record.prefix == "Daemon": + primary_interface_error = record + provisioning_complete = False + else: + errors.append(record) + + if "Provisioning complete" in record.text and record.prefix == "Daemon": + provisioning_complete = True + + # Keep the "no routes found" as a genuine error message if it was never corrected + if primary_interface_error is not None and not provisioning_complete: + errors.append(primary_interface_error) + + return errors + + @staticmethod + def _is_systemd(): + # Taken from azurelinuxagent/common/osutil/systemd.py; repeated here because it is available only on agents >= 2.3 + return os.path.exists("/run/systemd/system/") + + def _increment_counter(self, counter_name) -> int: + """ + Keeps a table of counters indexed by the given 'counter_name'. Each call to the function + increments the value of that counter and returns the new value. + """ + count = self._counter_table.get(counter_name) + count = 1 if count is None else count + 1 + self._counter_table[counter_name] = count + return count + + @staticmethod + def matches_ignore_rule(record: AgentLogRecord, ignore_rules: List[Dict[str, Any]]) -> bool: + """ + Returns True if the given 'record' matches any of the 'ignore_rules' + """ + return any(re.search(rule['message'], record.message) is not None and ('if' not in rule or rule['if'](record)) for rule in ignore_rules) + + # The format of the log has changed over time and the current log may include records from different sources. Most records are single-line, but some of them + # can span across multiple lines. We will assume records always start with a line similar to the examples below; any other lines will be assumed to be part + # of the record that is being currently parsed. + # + # Newer Agent: 2019-11-27T22:22:48.123985Z VERBOSE ExtHandler ExtHandler Report vm agent status + # 2021-03-30T19:45:33.793213Z INFO ExtHandler [Microsoft.Azure.Security.Monitoring.AzureSecurityLinuxAgent-2.14.64] Target handler state: enabled [incarnation 3] + # + # 2.2.46: the date time was changed to ISO-8601 format but the thread name was not added. + # 2021-05-28T01:17:40.683072Z INFO ExtHandler Wire server endpoint:168.63.129.16 + # 2021-05-28T01:17:40.683823Z WARNING ExtHandler Move rules file 70-persistent-net.rules to /var/lib/waagent/70-persistent-net.rules + # 2021-05-28T01:17:40.767600Z INFO ExtHandler Successfully added Azure fabric firewall rules + # + # Older Agent: 2021/03/30 19:35:35.971742 INFO Daemon Azure Linux Agent Version:2.2.45 + # + # Extension: 2021/03/30 19:45:31 Azure Monitoring Agent for Linux started to handle. + # 2021/03/30 19:45:31 [Microsoft.Azure.Monitor.AzureMonitorLinuxAgent-1.7.0] cwd is /var/lib/waagent/Microsoft.Azure.Monitor.AzureMonitorLinuxAgent-1.7.0 + # + _NEWER_AGENT_RECORD = re.compile(r'(?P[\d-]+T[\d:.]+Z)\s(?PVERBOSE|INFO|WARNING|ERROR)\s(?P\S+)\s(?P(Daemon)|(ExtHandler)|(\[\S+\]))\s(?P.*)') + _2_2_46_AGENT_RECORD = re.compile(r'(?P[\d-]+T[\d:.]+Z)\s(?PVERBOSE|INFO|WARNING|ERROR)\s(?P)(?PDaemon|ExtHandler|\[\S+\])\s(?P.*)') + _OLDER_AGENT_RECORD = re.compile(r'(?P[\d/]+\s[\d:.]+)\s(?PVERBOSE|INFO|WARNING|ERROR)\s(?P)(?P\S*)\s(?P.*)') + _EXTENSION_RECORD = re.compile(r'(?P[\d/]+\s[\d:.]+)\s(?P)(?P)((?P\[[^\]]+\])\s)?(?P.*)') + + def read(self) -> Iterable[AgentLogRecord]: + """ + Generator function that returns each of the entries in the agent log parsed as AgentLogRecords. + + The function can be used following this pattern: + + for record in read_agent_log(): + ... do something... + + """ + if not self._path.exists(): + raise IOError('{0} is not found'.format(self._path)) + + def match_record(): + for regex in [self._NEWER_AGENT_RECORD, self._2_2_46_AGENT_RECORD, self._OLDER_AGENT_RECORD]: + m = regex.match(line) + if m is not None: + return m + # The extension regex also matches the old agent records, so it needs to be last + return self._EXTENSION_RECORD.match(line) + + def complete_record(): + record.text = record.text.rstrip() # the text includes \n + if extra_lines != "": + record.text = record.text + "\n" + extra_lines.rstrip() + record.message = record.message + "\n" + extra_lines.rstrip() + return record + + with self._path.open() as file_: + record = None + extra_lines = "" + + line = file_.readline() + while line != "": # while not EOF + match = match_record() + if match is not None: + if record is not None: + yield complete_record() + record = AgentLogRecord.from_match(match) + extra_lines = "" + else: + extra_lines = extra_lines + line + line = file_.readline() + + if record is not None: + yield complete_record() diff --git a/tests_e2e/tests/lib/agent_test.py b/tests_e2e/tests/lib/agent_test.py index 6e79ad4f2f..22f865a6f3 100644 --- a/tests_e2e/tests/lib/agent_test.py +++ b/tests_e2e/tests/lib/agent_test.py @@ -20,6 +20,7 @@ import sys from abc import ABC, abstractmethod +from typing import Any, Dict, List from tests_e2e.tests.lib.agent_test_context import AgentTestContext from tests_e2e.tests.lib.logging import log @@ -44,6 +45,10 @@ def __init__(self, context: AgentTestContext): def run(self): pass + def get_ignore_error_rules(self) -> List[Dict[str, Any]]: + # Tests can override this method to return a list with rules to ignore errors in the agent log (see agent_log.py for sample rules). + return [] + @classmethod def run_from_command_line(cls): """ diff --git a/tests_e2e/tests/lib/ssh_client.py b/tests_e2e/tests/lib/ssh_client.py index e0c07420e6..50ff9f8086 100644 --- a/tests_e2e/tests/lib/ssh_client.py +++ b/tests_e2e/tests/lib/ssh_client.py @@ -16,6 +16,8 @@ # See the License for the specific language governing permissions and # limitations under the License. # +import re + from pathlib import Path from tests_e2e.tests.lib import shell @@ -33,14 +35,17 @@ def run_command(self, command: str, use_sudo: bool = False) -> str: Executes the given command over SSH and returns its stdout. If the command returns a non-zero exit code, the function raises a RunCommandException. """ + if re.match(r"^\s*sudo\s*", command): + raise Exception("Do not include 'sudo' in the 'command' argument, use the 'use_sudo' parameter instead") + destination = f"ssh://{self._username}@{self._ip_address}:{self._port}" # Note that we add ~/bin to the remote PATH, since Python (Pypy) and other test tools are installed there. # Note, too, that when using sudo we need to carry over the value of PATH to the sudo session - sudo = "sudo env PATH=$PATH" if use_sudo else '' + sudo = "sudo env PATH=$PATH PYTHONPATH=$PYTHONPATH" if use_sudo else '' return shell.run_command([ "ssh", "-o", "StrictHostKeyChecking=no", "-i", self._private_key_file, destination, - f"PATH=~/bin:$PATH;{sudo} {command}"]) + f"source ~/bin/agent-env;{sudo} {command}"]) @staticmethod def generate_ssh_key(private_key_file: Path): @@ -52,10 +57,19 @@ def generate_ssh_key(private_key_file: Path): def get_architecture(self): return self.run_command("uname -m").rstrip() - def copy(self, source: Path, target: Path, remote_source: bool = False, remote_target: bool = False, recursive: bool = False): + def copy_to_node(self, local_path: Path, remote_path: Path, recursive: bool = False) -> None: + """ + File copy to a remote node + """ + self._copy(local_path, remote_path, remote_source=False, remote_target=True, recursive=recursive) + + def copy_from_node(self, remote_path: Path, local_path: Path, recursive: bool = False) -> None: """ - Copy file from local to remote machine + File copy from a remote node """ + self._copy(remote_path, local_path, remote_source=True, remote_target=False, recursive=recursive) + + def _copy(self, source: Path, target: Path, remote_source: bool, remote_target: bool, recursive: bool) -> None: if remote_source: source = f"{self._username}@{self._ip_address}:{source}" if remote_target: From a78787bb010724fb0b55cdc754bda1c69c7c856e Mon Sep 17 00:00:00 2001 From: Nageswara Nandigam <84482346+nagworld9@users.noreply.github.com> Date: Wed, 22 Mar 2023 11:10:11 -0700 Subject: [PATCH 56/63] retry ssh run in e2e tests (#2788) * ssh retry * update comment * addressed comment --- tests_e2e/orchestrator/runbook.yml | 2 +- tests_e2e/tests/lib/retry.py | 18 ++++++++++++++++++ tests_e2e/tests/lib/ssh_client.py | 10 ++++++---- 3 files changed, 25 insertions(+), 5 deletions(-) diff --git a/tests_e2e/orchestrator/runbook.yml b/tests_e2e/orchestrator/runbook.yml index bb968bad86..8075725eb0 100644 --- a/tests_e2e/orchestrator/runbook.yml +++ b/tests_e2e/orchestrator/runbook.yml @@ -126,7 +126,7 @@ combinator: location: $(location) vm_size: $(vm_size) -concurrency: 32 +concurrency: 16 notifier: - type: agent.junit diff --git a/tests_e2e/tests/lib/retry.py b/tests_e2e/tests/lib/retry.py index a86227bc6c..bbd327cda3 100644 --- a/tests_e2e/tests/lib/retry.py +++ b/tests_e2e/tests/lib/retry.py @@ -19,6 +19,7 @@ from typing import Callable, Any from tests_e2e.tests.lib.logging import log +from tests_e2e.tests.lib.shell import CommandError def execute_with_retry(operation: Callable[[], Any]) -> Any: @@ -39,3 +40,20 @@ def execute_with_retry(operation: Callable[[], Any]) -> Any: time.sleep(30) +def retry_ssh_run(operation: Callable[[], Any]) -> Any: + """ + This method attempts to retry ssh run command a few times if operation failed with connection time out + """ + attempts = 3 + while attempts > 0: + attempts -= 1 + try: + return operation() + except Exception as e: + # We raise CommandError on !=0 exit codes in the called method + if isinstance(e, CommandError): + # Instance of 'Exception' has no 'exit_code' member (no-member) - Disabled: e is actually an CommandError + if e.exit_code != 255 or attempts == 0: # pylint: disable=no-member + raise + log.warning("The operation failed with %s, retrying in 30 secs.", e) + time.sleep(30) diff --git a/tests_e2e/tests/lib/ssh_client.py b/tests_e2e/tests/lib/ssh_client.py index 50ff9f8086..c10d763a47 100644 --- a/tests_e2e/tests/lib/ssh_client.py +++ b/tests_e2e/tests/lib/ssh_client.py @@ -21,12 +21,13 @@ from pathlib import Path from tests_e2e.tests.lib import shell +from tests_e2e.tests.lib.retry import retry_ssh_run class SshClient(object): def __init__(self, ip_address: str, username: str, private_key_file: Path, port: int = 22): self._ip_address: str = ip_address - self._username:str = username + self._username: str = username self._private_key_file: Path = private_key_file self._port: int = port @@ -43,16 +44,17 @@ def run_command(self, command: str, use_sudo: bool = False) -> str: # Note that we add ~/bin to the remote PATH, since Python (Pypy) and other test tools are installed there. # Note, too, that when using sudo we need to carry over the value of PATH to the sudo session sudo = "sudo env PATH=$PATH PYTHONPATH=$PYTHONPATH" if use_sudo else '' - return shell.run_command([ + return retry_ssh_run(lambda: shell.run_command([ "ssh", "-o", "StrictHostKeyChecking=no", "-i", self._private_key_file, destination, - f"source ~/bin/agent-env;{sudo} {command}"]) + f"source ~/bin/agent-env;{sudo} {command}"])) @staticmethod def generate_ssh_key(private_key_file: Path): """ Generates an SSH key on the given Path """ - shell.run_command(["ssh-keygen", "-m", "PEM", "-t", "rsa", "-b", "4096", "-q", "-N", "", "-f", str(private_key_file)]) + shell.run_command( + ["ssh-keygen", "-m", "PEM", "-t", "rsa", "-b", "4096", "-q", "-N", "", "-f", str(private_key_file)]) def get_architecture(self): return self.run_command("uname -m").rstrip() From de7debd80b264655840b5200bc1e1c72936b23a9 Mon Sep 17 00:00:00 2001 From: maddieford <93676569+maddieford@users.noreply.github.com> Date: Fri, 24 Mar 2023 13:47:05 -0700 Subject: [PATCH 57/63] Remove cgroup files during deprovisioning (#2790) * Update version to dummy 1.0.0.0' * Revert version change * Remove cgroup config during deprovision * Remove CPUQuota in deprovision * Remove azure slice * Do not remove service file in deprovision * Update comment * Do not remvoe azure.slice in deprovision * Update comment * Remove unused import --- azurelinuxagent/pa/deprovision/default.py | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/azurelinuxagent/pa/deprovision/default.py b/azurelinuxagent/pa/deprovision/default.py index 105b61825f..89492b75e2 100644 --- a/azurelinuxagent/pa/deprovision/default.py +++ b/azurelinuxagent/pa/deprovision/default.py @@ -26,8 +26,10 @@ import azurelinuxagent.common.conf as conf import azurelinuxagent.common.utils.fileutil as fileutil from azurelinuxagent.common import version +from azurelinuxagent.common.cgroupconfigurator import _AGENT_DROP_IN_FILE_SLICE, _DROP_IN_FILE_CPU_ACCOUNTING, \ + _DROP_IN_FILE_CPU_QUOTA, _DROP_IN_FILE_MEMORY_ACCOUNTING, LOGCOLLECTOR_SLICE from azurelinuxagent.common.exception import ProtocolError -from azurelinuxagent.common.osutil import get_osutil +from azurelinuxagent.common.osutil import get_osutil, systemd from azurelinuxagent.common.persist_firewall_rules import PersistFirewallRulesHandler from azurelinuxagent.common.protocol.util import get_protocol_util from azurelinuxagent.ga.exthandlers import HANDLER_COMPLETE_NAME_PATTERN @@ -199,6 +201,7 @@ def setup(self, deluser): self.del_user(warnings, actions) self.del_persist_firewall_rules(actions) + self.remove_agent_cgroup_config(actions) return warnings, actions @@ -210,6 +213,7 @@ def setup_changed_unique_id(self): self.del_lib_dir_files(warnings, actions) self.del_ext_handler_files(warnings, actions) self.del_persist_firewall_rules(actions) + self.remove_agent_cgroup_config(actions) return warnings, actions @@ -266,3 +270,20 @@ def del_persist_firewall_rules(actions): actions.append(DeprovisionAction(fileutil.rm_files, [agent_network_service_path, os.path.join(conf.get_lib_dir(), PersistFirewallRulesHandler.BINARY_FILE_NAME)])) + + @staticmethod + def remove_agent_cgroup_config(actions): + # Get all service drop in file paths + agent_drop_in_path = systemd.get_agent_drop_in_path() + slice_path = os.path.join(agent_drop_in_path, _AGENT_DROP_IN_FILE_SLICE) + cpu_accounting_path = os.path.join(agent_drop_in_path, _DROP_IN_FILE_CPU_ACCOUNTING) + cpu_quota_path = os.path.join(agent_drop_in_path, _DROP_IN_FILE_CPU_QUOTA) + mem_accounting_path = os.path.join(agent_drop_in_path, _DROP_IN_FILE_MEMORY_ACCOUNTING) + + # Get log collector slice + unit_file_install_path = systemd.get_unit_file_install_path() + log_collector_slice_path = os.path.join(unit_file_install_path, LOGCOLLECTOR_SLICE) + + actions.append(DeprovisionAction(fileutil.rm_files, + [slice_path, cpu_accounting_path, cpu_quota_path, mem_accounting_path, + log_collector_slice_path])) From 08f86dbfd48eee7005864bf23f677cb364bb9ab5 Mon Sep 17 00:00:00 2001 From: Norberto Arrieta Date: Wed, 29 Mar 2023 15:24:16 -0700 Subject: [PATCH 58/63] Improvements in test logs (#2792) Co-authored-by: narrieta --- tests_e2e/orchestrator/lib/agent_junit.py | 7 +- .../orchestrator/lib/agent_test_suite.py | 356 ++++++++++-------- .../sample_runbooks/existing_vm.yml | 5 + tests_e2e/orchestrator/scripts/install-agent | 2 +- tests_e2e/pipeline/pipeline.yml | 2 +- tests_e2e/pipeline/scripts/execute_tests.sh | 71 ++-- tests_e2e/tests/bvts/vm_access.py | 5 +- tests_e2e/tests/lib/agent_log.py | 2 +- tests_e2e/tests/lib/logging.py | 21 ++ 9 files changed, 285 insertions(+), 186 deletions(-) diff --git a/tests_e2e/orchestrator/lib/agent_junit.py b/tests_e2e/orchestrator/lib/agent_junit.py index 049bbb161c..a8ff8eb6c5 100644 --- a/tests_e2e/orchestrator/lib/agent_junit.py +++ b/tests_e2e/orchestrator/lib/agent_junit.py @@ -49,10 +49,13 @@ def type_schema(cls) -> Type[schema.TypedSchema]: def _received_message(self, message: MessageBase) -> None: # The Agent sends its own TestResultMessage and marks them as "AgentTestResultMessage"; for the - # test results sent by LISA itself, we change the suite name to "_Setup_" in order to separate them + # test results sent by LISA itself, we change the suite name to "_Runbook_" in order to separate them # from actual test results. if isinstance(message, TestResultMessage) and message.type != "AgentTestResultMessage": - message.suite_full_name = "_Setup_" + if "Unexpected error in AgentTestSuite" in message.message: + # Ignore these errors, they are already reported as AgentTestResultMessages + return + message.suite_full_name = "_Runbook_" message.suite_name = message.suite_full_name image = message.information.get('image') if image is not None: diff --git a/tests_e2e/orchestrator/lib/agent_test_suite.py b/tests_e2e/orchestrator/lib/agent_test_suite.py index 7abd714343..38274db743 100644 --- a/tests_e2e/orchestrator/lib/agent_test_suite.py +++ b/tests_e2e/orchestrator/lib/agent_test_suite.py @@ -18,7 +18,6 @@ import datetime import json import logging -import re import traceback import uuid @@ -34,10 +33,12 @@ Logger, Node, notifier, + simple_requirement, TestCaseMetadata, TestSuite as LisaTestSuite, TestSuiteMetadata, ) +from lisa.environment import EnvironmentStatus # pylint: disable=E0401 from lisa.messages import TestStatus, TestResultMessage # pylint: disable=E0401 from lisa.sut_orchestrator import AZURE # pylint: disable=E0401 from lisa.sut_orchestrator.azure.common import get_node_context, AzureNodeSchema # pylint: disable=E0401 @@ -49,7 +50,7 @@ from tests_e2e.tests.lib.agent_test import TestSkipped from tests_e2e.tests.lib.agent_test_context import AgentTestContext from tests_e2e.tests.lib.identifiers import VmIdentifier -from tests_e2e.tests.lib.logging import log as agent_test_logger # Logger used by the tests +from tests_e2e.tests.lib.logging import log from tests_e2e.tests.lib.logging import set_current_thread_log from tests_e2e.tests.lib.agent_log import AgentLogRecord from tests_e2e.tests.lib.shell import run_command @@ -114,10 +115,10 @@ def __init__(self, vm: VmIdentifier, paths: AgentTestContext.Paths, connection: super().__init__(vm=vm, paths=paths, connection=connection) # These are initialized by AgentTestSuite._set_context(). self.log_path: Path = None - self.log: Logger = None + self.lisa_log: Logger = None self.node: Node = None self.runbook_name: str = None - self.image_name: str = None + self.environment_name: str = None self.is_vhd: bool = None self.test_suites: List[AgentTestSuite] = None self.collect_logs: str = None @@ -129,12 +130,10 @@ def __init__(self, metadata: TestSuiteMetadata) -> None: # The context is initialized by _set_context() via the call to execute() self.__context: AgentTestSuite._Context = None - def _set_context(self, node: Node, variables: Dict[str, Any], lisa_log_path: str, log: Logger): + def _initialize(self, node: Node, variables: Dict[str, Any], lisa_working_path: str, lisa_log_path: str, lisa_log: Logger): connection_info = node.connection_info node_context = get_node_context(node) runbook = node.capability.get_extended_runbook(AzureNodeSchema, AZURE) - # Remove the resource group and node suffix, e.g. "e1-n0" in "lisa-20230110-162242-963-e1-n0" - runbook_name = re.sub(r"-\w+-\w+$", "", runbook.name) self.__context = self._Context( vm=VmIdentifier( @@ -143,8 +142,7 @@ def _set_context(self, node: Node, variables: Dict[str, Any], lisa_log_path: str resource_group=node_context.resource_group_name, name=node_context.vm_name), paths=AgentTestContext.Paths( - # The runbook name is unique on each run, so we will use different working directory every time - working_directory=Path().home()/"tmp"/runbook_name, + working_directory=self._get_working_directory(lisa_working_path), remote_working_directory=Path('/home')/connection_info['username']), connection=AgentTestContext.Connection( ip_address=connection_info['address'], @@ -153,10 +151,10 @@ def _set_context(self, node: Node, variables: Dict[str, Any], lisa_log_path: str ssh_port=connection_info['port'])) self.__context.log_path = self._get_log_path(variables, lisa_log_path) - self.__context.log = log + self.__context.lisa_log = lisa_log self.__context.node = node self.__context.is_vhd = self._get_optional_parameter(variables, "c_vhd") != "" - self.__context.image_name = f"{node.os.name}-vhd" if self.__context.is_vhd else self._get_required_parameter(variables, "c_env_name") + self.__context.environment_name = f"{node.os.name}-vhd" if self.__context.is_vhd else self._get_required_parameter(variables, "c_env_name") self.__context.test_suites = self._get_required_parameter(variables, "c_test_suites") self.__context.collect_logs = self._get_required_parameter(variables, "collect_logs") self.__context.skip_setup = self._get_required_parameter(variables, "skip_setup") @@ -177,12 +175,26 @@ def _get_optional_parameter(variables: Dict[str, Any], name: str, default_value: return value @staticmethod - def _get_log_path(variables: Dict[str, Any], lisa_log_path: str): + def _get_log_path(variables: Dict[str, Any], lisa_log_path: str) -> Path: # NOTE: If "log_path" is not given as argument to the runbook, use a path derived from LISA's log for the test suite. # That path is derived from LISA's "--log_path" command line argument and has a value similar to # "<--log_path>/20230217/20230217-040022-342/tests/20230217-040119-288-agent_test_suite"; use the directory # 2 levels up. - return Path(variables["log_path"]) if "log_path" in variables else Path(lisa_log_path).parent.parent + log_path = variables.get("log_path") + if log_path is not None and len(log_path) > 0: + return Path(log_path) + return Path(lisa_log_path).parent.parent + + @staticmethod + def _get_working_directory(lisa_working_path: str) -> Path: + # LISA's "working_path" has a value similar to + # "<--working_path>/20230322/20230322-194430-287/tests/20230322-194451-333-agent_test_suite + # where "<--working_path>" is the value given to the --working_path command line argument. Create the working for + # the AgentTestSuite as + # "<--working_path>/20230322/20230322-194430-287/waagent + # This directory will be unique for each execution of the runbook ("20230322-194430" is the timestamp and "287" is a + # unique ID per execution) + return Path(lisa_working_path).parent.parent / "waagent" @property def context(self): @@ -190,13 +202,6 @@ def context(self): raise Exception("The context for the AgentTestSuite has not been initialized") return self.__context - @property - def _log(self) -> Logger: - """ - Returns a reference to the LISA Logger. - """ - return self.context.log - # # Test suites within the same runbook may be executed concurrently, and setup needs to be done only once. # We use this lock to allow only 1 thread to do the setup. Setup completion is marked using the 'completed' @@ -214,20 +219,22 @@ def _setup(self) -> None: self._setup_lock.acquire() try: - self._log.info("") - self._log.info("**************************************** [Build] ****************************************") - self._log.info("") + log.info("") + log.info("**************************************** [Build] ****************************************") + log.info("") completed: Path = self.context.working_directory/"completed" if completed.exists(): - self._log.info("Found %s. Build has already been done, skipping.", completed) + log.info("Found %s. Build has already been done, skipping.", completed) return - self._log.info("Creating working directory: %s", self.context.working_directory) + self.context.lisa_log.info("Building test agent") + log.info("Creating working directory: %s", self.context.working_directory) self.context.working_directory.mkdir(parents=True) + self._build_agent_package() - self._log.info("Completed setup, creating %s", completed) + log.info("Completed setup, creating %s", completed) completed.touch() finally: @@ -237,15 +244,15 @@ def _build_agent_package(self) -> None: """ Builds the agent package and returns the path to the package. """ - self._log.info("Building agent package to %s", self.context.working_directory) + log.info("Building agent package to %s", self.context.working_directory) - makepkg.run(agent_family="Test", output_directory=str(self.context.working_directory), log=self._log) + makepkg.run(agent_family="Test", output_directory=str(self.context.working_directory), log=log) package_path: Path = self._get_agent_package_path() if not package_path.exists(): raise Exception(f"Can't find the agent package at {package_path}") - self._log.info("Built agent package as %s", package_path) + log.info("Built agent package as %s", package_path) def _get_agent_package_path(self) -> Path: """ @@ -262,34 +269,37 @@ def _setup_node(self) -> None: """ Prepares the remote node for executing the test suite (installs tools and the test agent, etc) """ - self._log.info("") - self._log.info("************************************** [Node Setup] **************************************") - self._log.info("") - self._log.info("Test Node: %s", self.context.vm.name) - self._log.info("Resource Group: %s", self.context.vm.resource_group) - self._log.info("") + self.context.lisa_log.info("Setting up test node") + log.info("") + log.info("************************************** [Node Setup] **************************************") + log.info("") + log.info("Test Node: %s", self.context.vm.name) + log.info("Resource Group: %s", self.context.vm.resource_group) + log.info("") self.context.ssh_client.run_command("mkdir -p ~/bin/tests_e2e/tests; touch ~/bin/agent-env") # Copy the test tools tools_path = self.context.test_source_directory/"orchestrator"/"scripts" tools_target_path = Path("~/bin") - self._log.info("Copying %s to %s:%s", tools_path, self.context.node.name, tools_target_path) + log.info("Copying %s to %s:%s", tools_path, self.context.node.name, tools_target_path) self.context.ssh_client.copy_to_node(tools_path, tools_target_path, recursive=True) # Copy the test libraries lib_path = self.context.test_source_directory/"tests"/"lib" lib_target_path = Path("~/bin/tests_e2e/tests") - self._log.info("Copying %s to %s:%s", lib_path, self.context.node.name, lib_target_path) + log.info("Copying %s to %s:%s", lib_path, self.context.node.name, lib_target_path) self.context.ssh_client.copy_to_node(lib_path, lib_target_path, recursive=True) # Copy the test agent agent_package_path: Path = self._get_agent_package_path() agent_package_target_path = Path("~/bin")/agent_package_path.name - self._log.info("Copying %s to %s:%s", agent_package_path, self.context.node.name, agent_package_target_path) + log.info("Copying %s to %s:%s", agent_package_path, self.context.node.name, agent_package_target_path) self.context.ssh_client.copy_to_node(agent_package_path, agent_package_target_path) # Copy Pypy + # NOTE: Pypy is pre-downloaded to /tmp on the container image used for Azure Pipelines runs. For dev runs, + # if we don't find Pypy under /tmp, then we download it a few lines below. if self.context.ssh_client.get_architecture() == "aarch64": pypy_path = Path("/tmp/pypy3.7-arm64.tar.bz2") pypy_download = "https://downloads.python.org/pypy/pypy3.7-v7.3.5-aarch64.tar.bz2" @@ -298,23 +308,23 @@ def _setup_node(self) -> None: pypy_download = "https://downloads.python.org/pypy/pypy3.7-v7.3.5-linux64.tar.bz2" if not pypy_path.exists(): - self._log.info(f"Downloading {pypy_download} to {pypy_path}") + log.info("Downloading %s to %s", pypy_download, pypy_path) run_command(["wget", pypy_download, "-O", pypy_path]) pypy_target_path = Path("~/bin/pypy3.7.tar.bz2") - self._log.info("Copying %s to %s:%s", pypy_path, self.context.node.name, pypy_target_path) + log.info("Copying %s to %s:%s", pypy_path, self.context.node.name, pypy_target_path) self.context.ssh_client.copy_to_node(pypy_path, pypy_target_path) # Install the tools and libraries install_command = lambda: self.context.ssh_client.run_command(f"~/bin/scripts/install-tools --agent-package {agent_package_target_path}") - self._log.info('Installing tools on the test node\n%s', install_command()) - self._log.info('Remote commands will use %s', self.context.ssh_client.run_command("which python3")) + log.info('Installing tools on the test node\n%s', install_command()) + log.info('Remote commands will use %s', self.context.ssh_client.run_command("which python3")) # Install the agent if self.context.is_vhd: - self._log.info("Using a VHD; will not install the Test Agent.") + log.info("Using a VHD; will not install the Test Agent.") else: install_command = lambda: self.context.ssh_client.run_command(f"install-agent --package {agent_package_target_path} --version {AGENT_VERSION}", use_sudo=True) - self._log.info("Installing the Test Agent on %s\n%s", self.context.node.name, install_command()) + log.info("Installing the Test Agent on %s\n%s", self.context.node.name, install_command()) def _collect_node_logs(self) -> None: """ @@ -322,89 +332,112 @@ def _collect_node_logs(self) -> None: """ try: # Collect the logs on the test machine into a compressed tarball - self._log.info("Collecting logs on test machine [%s]...", self.context.node.name) + self.context.lisa_log.info("Collecting logs on test node") + log.info("Collecting logs on test node") stdout = self.context.ssh_client.run_command("collect-logs", use_sudo=True) - self._log.info(stdout) + log.info(stdout) # Copy the tarball to the local logs directory remote_path = "/tmp/waagent-logs.tgz" - local_path = self.context.log_path/'{0}.tgz'.format(self.context.image_name) - self._log.info("Copying %s:%s to %s", self.context.node.name, remote_path, local_path) + local_path = self.context.log_path/'{0}.tgz'.format(self.context.environment_name) + log.info("Copying %s:%s to %s", self.context.node.name, remote_path, local_path) self.context.ssh_client.copy_from_node(remote_path, local_path) except: # pylint: disable=bare-except - self._log.exception("Failed to collect logs from the test machine") + log.exception("Failed to collect logs from the test machine") - @TestCaseMetadata(description="", priority=0) - def agent_test_suite(self, node: Node, environment: Environment, variables: Dict[str, Any], log_path: str, log: Logger) -> None: + # NOTES: + # + # * environment_status=EnvironmentStatus.Deployed skips most of LISA's initialization of the test node, which is not needed + # for agent tests. + # + # * We need to take the LISA Logger using a parameter named 'log'; this parameter hides tests_e2e.tests.lib.logging.log. + # Be aware then, that within this method 'log' refers to the LISA log, and elsewhere it refers to tests_e2e.tests.lib.logging.log. + # + # W0621: Redefining name 'log' from outer scope (line 53) (redefined-outer-name) + @TestCaseMetadata(description="", priority=0, requirement=simple_requirement(environment_status=EnvironmentStatus.Deployed)) + def main(self, node: Node, environment: Environment, variables: Dict[str, Any], working_path: str, log_path: str, log: Logger): # pylint: disable=redefined-outer-name + """ + Entry point from LISA + """ + self._initialize(node, variables, working_path, log_path, log) + self._execute(environment, variables) + + def _execute(self, environment: Environment, variables: Dict[str, Any]): """ Executes each of the AgentTests included in the "c_test_suites" variable (which is generated by the AgentTestSuitesCombinator). """ - self._set_context(node, variables, log_path, log) + # Set the thread name to the name of the environment. The thread name is added to each item in LISA's log. + with _set_thread_name(self.context.environment_name): + log_path: Path = self.context.log_path/f"env-{self.context.environment_name}.log" + with set_current_thread_log(log_path): + start_time: datetime.datetime = datetime.datetime.now() + success = True + + try: + # Log the environment's name and the variables received from the runbook (note that we need to expand the names of the test suites) + log.info("LISA Environment (for correlation with the LISA log): %s", environment.name) + log.info("Runbook variables:") + for name, value in variables.items(): + log.info(" %s: %s", name, value if name != 'c_test_suites' else [t.name for t in value]) - # Set the thread name to the image; this name is added to self._log - with _set_thread_name(self.context.image_name): - # Log the environment's name and the variables received from the runbook (note that we need to expand the names of the test suites) - self._log.info("LISA Environment: %s", environment.name) - self._log.info( - "Runbook variables:\n%s", - '\n'.join([f"\t{name}: {value if name != 'c_test_suites' else [t.name for t in value] }" for name, value in variables.items()])) + test_suite_success = True - start_time: datetime.datetime = datetime.datetime.now() - test_suite_success = True + try: + if not self.context.skip_setup: + self._setup() - try: - if not self.context.skip_setup: - self._setup() + if not self.context.skip_setup: + self._setup_node() - try: - if not self.context.skip_setup: - self._setup_node() + # pylint seems to think self.context.test_suites is not iterable. Suppressing warning, since its type is List[AgentTestSuite] + # E1133: Non-iterable value self.context.test_suites is used in an iterating context (not-an-iterable) + for suite in self.context.test_suites: # pylint: disable=E1133 + test_suite_success = self._execute_test_suite(suite) and test_suite_success + + test_suite_success = self._check_agent_log() and test_suite_success + + finally: + collect = self.context.collect_logs + if collect == CollectLogs.Always or collect == CollectLogs.Failed and not test_suite_success: + self._collect_node_logs() - # pylint seems to think self.context.test_suites is not iterable. Suppressing warning, since its type is List[AgentTestSuite] - # E1133: Non-iterable value self.context.test_suites is used in an iterating context (not-an-iterable) - for suite in self.context.test_suites: # pylint: disable=E1133 - test_suite_success = self._execute_test_suite(suite) and test_suite_success + except Exception as e: # pylint: disable=bare-except + # Report the error and raise an exception to let LISA know that the test errored out. + success = False + log.exception("UNEXPECTED ERROR.") + self._report_test_result( + self.context.environment_name, + "Unexpected Error", + TestStatus.FAILED, + start_time, + message="UNEXPECTED ERROR.", + add_exception_stack_trace=True) - test_suite_success = self._check_agent_log() and test_suite_success + raise Exception(f"[{self.context.environment_name}] Unexpected error in AgentTestSuite: {e}") finally: - collect = self.context.collect_logs - if collect == CollectLogs.Always or collect == CollectLogs.Failed and not test_suite_success: - self._collect_node_logs() - - except: # pylint: disable=bare-except - # Report the error and raise and exception to let LISA know that the test errored out. - self._log.exception("TEST FAILURE DUE TO AN UNEXPECTED ERROR.") - self._report_test_result( - self.context.image_name, - "Setup", - TestStatus.FAILED, - start_time, - message="TEST FAILURE DUE TO AN UNEXPECTED ERROR.", - add_exception_stack_trace=True) - - raise Exception("STOPPING TEST EXECUTION DUE TO AN UNEXPECTED ERROR.") - - finally: - self._clean_up() + self._clean_up() + if not success: + self._mark_log_as_failed(log_path) def _execute_test_suite(self, suite: TestSuiteInfo) -> bool: """ Executes the given test suite and returns True if all the tests in the suite succeeded. """ suite_name = suite.name - suite_full_name = f"{suite_name}-{self.context.image_name}" + suite_full_name = f"{suite_name}-{self.context.environment_name}" suite_start_time: datetime.datetime = datetime.datetime.now() success: bool = True # True if all the tests succeed - with _set_thread_name(suite_full_name): # The thread name is added to self._log - with set_current_thread_log(self.context.log_path/f"{suite_full_name}.log"): + with _set_thread_name(suite_full_name): # The thread name is added to the LISA log + log_path:Path = self.context.log_path/f"{suite_full_name}.log" + with set_current_thread_log(log_path): try: - agent_test_logger.info("") - agent_test_logger.info("**************************************** %s ****************************************", suite_name) - agent_test_logger.info("") + log.info("") + log.info("**************************************** %s ****************************************", suite_name) + log.info("") summary: List[str] = [] @@ -413,16 +446,16 @@ def _execute_test_suite(self, suite: TestSuiteInfo) -> bool: test_full_name = f"{suite_name}-{test_name}" test_start_time: datetime.datetime = datetime.datetime.now() - agent_test_logger.info("******** Executing %s", test_name) - self._log.info("******** Executing %s", test_full_name) + log.info("******** Executing %s", test_name) + self.context.lisa_log.info("******** Executing %s", test_full_name) try: test(self.context).run() summary.append(f"[Passed] {test_name}") - agent_test_logger.info("******** [Passed] %s", test_name) - self._log.info("******** [Passed] %s", test_full_name) + log.info("******** [Passed] %s", test_name) + self.context.lisa_log.info("******** [Passed] %s", test_full_name) self._report_test_result( suite_full_name, test_name, @@ -430,8 +463,8 @@ def _execute_test_suite(self, suite: TestSuiteInfo) -> bool: test_start_time) except TestSkipped as e: summary.append(f"[Skipped] {test_name}") - agent_test_logger.info("******** [Skipped] %s: %s", test_name, e) - self._log.info("******** [Skipped] %s", test_full_name) + log.info("******** [Skipped] %s: %s", test_name, e) + self.context.lisa_log.info("******** [Skipped] %s", test_full_name) self._report_test_result( suite_full_name, test_name, @@ -441,8 +474,8 @@ def _execute_test_suite(self, suite: TestSuiteInfo) -> bool: except AssertionError as e: success = False summary.append(f"[Failed] {test_name}") - agent_test_logger.error("******** [Failed] %s: %s", test_name, e) - self._log.error("******** [Failed] %s", test_full_name) + log.error("******** [Failed] %s: %s", test_name, e) + self.context.lisa_log.error("******** [Failed] %s", test_full_name) self._report_test_result( suite_full_name, test_name, @@ -452,8 +485,8 @@ def _execute_test_suite(self, suite: TestSuiteInfo) -> bool: except: # pylint: disable=bare-except success = False summary.append(f"[Error] {test_name}") - agent_test_logger.exception("UNHANDLED EXCEPTION IN %s", test_name) - self._log.exception("UNHANDLED EXCEPTION IN %s", test_full_name) + log.exception("UNHANDLED EXCEPTION IN %s", test_name) + self.context.lisa_log.exception("UNHANDLED EXCEPTION IN %s", test_full_name) self._report_test_result( suite_full_name, test_name, @@ -462,13 +495,13 @@ def _execute_test_suite(self, suite: TestSuiteInfo) -> bool: message="Unhandled exception.", add_exception_stack_trace=True) - agent_test_logger.info("") + log.info("") - agent_test_logger.info("********* [Test Results]") - agent_test_logger.info("") + log.info("********* [Test Results]") + log.info("") for r in summary: - agent_test_logger.info("\t%s", r) - agent_test_logger.info("") + log.info("\t%s", r) + log.info("") except: # pylint: disable=bare-except success = False @@ -479,6 +512,9 @@ def _execute_test_suite(self, suite: TestSuiteInfo) -> bool: suite_start_time, message=f"Unhandled exception while executing test suite {suite_name}.", add_exception_stack_trace=True) + finally: + if not success: + self._mark_log_as_failed(log_path) return success @@ -488,47 +524,67 @@ def _check_agent_log(self) -> bool: """ start_time: datetime.datetime = datetime.datetime.now() - self._log.info("Checking agent log on the test node") - output = self.context.ssh_client.run_command("check-agent-log.py -j") - errors = json.loads(output, object_hook=AgentLogRecord.from_dictionary) - - # Individual tests may have rules to ignore known errors; filter those out - ignore_error_rules = [] - # pylint seems to think self.context.test_suites is not iterable. Suppressing warning, since its type is List[AgentTestSuite] - # E1133: Non-iterable value self.context.test_suites is used in an iterating context (not-an-iterable) - for suite in self.context.test_suites: # pylint: disable=E1133 - for test in suite.tests: - ignore_error_rules.extend(test(self.context).get_ignore_error_rules()) - - if len(ignore_error_rules) > 0: - new = [] - for e in errors: - if not AgentLog.matches_ignore_rule(e, ignore_error_rules): - new.append(e) - errors = new - - if len(errors) == 0: - # If no errors, we are done; don't create a log or test result. - self._log.info("There are no errors in the agent log") - return True - - log_path: Path = self.context.log_path/f"CheckAgentLog-{self.context.image_name}.log" - message = f"Detected {len(errors)} error(s) in the agent log. See {log_path} for a full report." - self._log.info(message) - - with set_current_thread_log(log_path): - agent_test_logger.info("Detected %s error(s) in the agent log:\n\n%s", len(errors), '\n'.join(['\t' + e.text for e in errors])) - - self._report_test_result( - self.context.image_name, - "CheckAgentLog", - TestStatus.FAILED, - start_time, - message=message + '\n' + '\n'.join([e.text for e in errors[0:3]]), - add_exception_stack_trace=True) + try: + self.context.lisa_log.info("Checking agent log on the test node") + log.info("Checking agent log on the test node") + + output = self.context.ssh_client.run_command("check-agent-log.py -j") + errors = json.loads(output, object_hook=AgentLogRecord.from_dictionary) + + # Individual tests may have rules to ignore known errors; filter those out + ignore_error_rules = [] + # pylint seems to think self.context.test_suites is not iterable. Suppressing warning, since its type is List[AgentTestSuite] + # E1133: Non-iterable value self.context.test_suites is used in an iterating context (not-an-iterable) + for suite in self.context.test_suites: # pylint: disable=E1133 + for test in suite.tests: + ignore_error_rules.extend(test(self.context).get_ignore_error_rules()) + + if len(ignore_error_rules) > 0: + new = [] + for e in errors: + if not AgentLog.matches_ignore_rule(e, ignore_error_rules): + new.append(e) + errors = new + + if len(errors) == 0: + # If no errors, we are done; don't create a log or test result. + log.info("There are no errors in the agent log") + return True + + log_path: Path = self.context.log_path/f"CheckAgentLog-{self.context.environment_name}.log" + message = f"Detected {len(errors)} error(s) in the agent log. See {log_path} for a full report." + self.context.lisa_log.info(message) + log.info(message) + + with set_current_thread_log(log_path): + log.info("Detected %s error(s) in the agent log:\n\n%s", len(errors), '\n'.join(['\t' + e.text for e in errors])) + self._mark_log_as_failed(log_path) + + self._report_test_result( + self.context.environment_name, + "CheckAgentLog", + TestStatus.FAILED, + start_time, + message=message + '\n' + '\n'.join([e.text for e in errors[0:3]])) + except: # pylint: disable=bare-except + log.exception("Error checking agent log") + self._report_test_result( + self.context.environment_name, + "CheckAgentLog", + TestStatus.FAILED, + start_time, + "Error checking agent log", + add_exception_stack_trace=True) return False + @staticmethod + def _mark_log_as_failed(log_path: Path): + """ + Renames the given log to prefix it with "_". + """ + log_path.rename(log_path.parent / ("_" + log_path.name)) + @staticmethod def _report_test_result( suite_name: str, diff --git a/tests_e2e/orchestrator/sample_runbooks/existing_vm.yml b/tests_e2e/orchestrator/sample_runbooks/existing_vm.yml index 2e312e5e84..2a5109f41f 100644 --- a/tests_e2e/orchestrator/sample_runbooks/existing_vm.yml +++ b/tests_e2e/orchestrator/sample_runbooks/existing_vm.yml @@ -60,6 +60,11 @@ variable: # NOTE: c_test_suites, generated by the AgentTestSuitesCombinator, is also a parameter # for the AgentTestSuite # + # Root directory for log files (optional) + - name: log_path + value: "" + is_case_visible: true + # Whether to collect logs from the test VM - name: collect_logs value: "failed" diff --git a/tests_e2e/orchestrator/scripts/install-agent b/tests_e2e/orchestrator/scripts/install-agent index 868b9fa788..197f026540 100755 --- a/tests_e2e/orchestrator/scripts/install-agent +++ b/tests_e2e/orchestrator/scripts/install-agent @@ -87,7 +87,7 @@ echo "Installing $package as version $version..." unzip.py "$package" "/var/lib/waagent/WALinuxAgent-$version" # Ensure that AutoUpdate is enabled. some distros, e.g. Flatcar, don't have a waagent.conf -# but AutoUpdate defaults to True so there is no need to anything in that case. +# but AutoUpdate defaults to True so there is no need to do anything in that case. if [[ -e /etc/waagent.conf ]]; then sed -i 's/AutoUpdate.Enabled=n/AutoUpdate.Enabled=y/g' /etc/waagent.conf fi diff --git a/tests_e2e/pipeline/pipeline.yml b/tests_e2e/pipeline/pipeline.yml index fe532d6d74..1de5416340 100644 --- a/tests_e2e/pipeline/pipeline.yml +++ b/tests_e2e/pipeline/pipeline.yml @@ -113,7 +113,7 @@ jobs: displayName: 'Publish test results' inputs: testResultsFormat: 'JUnit' - testResultsFiles: 'lisa/agent.junit.xml' + testResultsFiles: 'runbook_logs/agent.junit.xml' searchFolder: $(Build.ArtifactStagingDirectory) failTaskOnFailedTests: true diff --git a/tests_e2e/pipeline/scripts/execute_tests.sh b/tests_e2e/pipeline/scripts/execute_tests.sh index e1d26d6e25..e5054bf2ea 100755 --- a/tests_e2e/pipeline/scripts/execute_tests.sh +++ b/tests_e2e/pipeline/scripts/execute_tests.sh @@ -3,23 +3,32 @@ set -euxo pipefail # -# Set the correct mode for the private SSH key and generate the public key. +# UID of 'waagent' in the Docker container +# +WAAGENT_UID=1000 + +# +# Set the correct mode and owner for the private SSH key and generate the public key. # cd "$HOME" mkdir ssh cp "$DOWNLOADSSHKEY_SECUREFILEPATH" ssh chmod 700 ssh/id_rsa ssh-keygen -y -f ssh/id_rsa > ssh/id_rsa.pub +sudo find ssh -exec chown "$WAAGENT_UID" {} \; + +# +# Allow write access to the sources directory. This is needed because building the agent package (within the Docker +# container) writes the egg info to that directory. +# +chmod a+w "$BUILD_SOURCESDIRECTORY" # -# Change the ownership of the "ssh" directory we just created, as well as the sources and staging directories. -# Make waagent (UID 1000 in the container) the owner of both locations, so that it can write to them. -# This is needed because building the agent package writes the egg info to the source code directory, and -# tests write their logs to the staging directory. +# Create the directory where the Docker container will create the test logs and give ownership to 'waagent' # -sudo find ssh -exec chown 1000 {} \; -sudo chown 1000 "$BUILD_SOURCESDIRECTORY" -sudo chown 1000 "$BUILD_ARTIFACTSTAGINGDIRECTORY" +LOGS_DIRECTORY="$HOME/logs" +mkdir "$LOGS_DIRECTORY" +sudo chown "$WAAGENT_UID" "$LOGS_DIRECTORY" # # Pull the container image used to execute the tests @@ -47,7 +56,7 @@ echo "exit 0" > /tmp/exit.sh docker run --rm \ --volume "$BUILD_SOURCESDIRECTORY:/home/waagent/WALinuxAgent" \ --volume "$HOME"/ssh:/home/waagent/.ssh \ - --volume "$BUILD_ARTIFACTSTAGINGDIRECTORY":/home/waagent/logs \ + --volume "$LOGS_DIRECTORY":/home/waagent/logs \ --env AZURE_CLIENT_ID \ --env AZURE_CLIENT_SECRET \ --env AZURE_TENANT_ID \ @@ -56,7 +65,7 @@ docker run --rm \ "lisa \ --runbook \$HOME/WALinuxAgent/tests_e2e/orchestrator/runbook.yml \ --log_path \$HOME/logs/lisa \ - --working_path \$HOME/logs/lisa \ + --working_path \$HOME/tmp \ -v cloud:$CLOUD \ -v subscription_id:$SUBSCRIPTION_ID \ -v identity_file:\$HOME/.ssh/id_rsa \ @@ -70,29 +79,33 @@ docker run --rm \ || echo "exit $?" > /tmp/exit.sh # -# Retake ownership of the source and staging directories (note that the former does not need to be done recursively; also, we don't need to -# retake ownership of the ssh directory) +# Re-take ownership of the logs directory # -sudo chown "$USER" "$BUILD_SOURCESDIRECTORY" -sudo find "$BUILD_ARTIFACTSTAGINGDIRECTORY" -exec chown "$USER" {} \; +sudo find "$LOGS_DIRECTORY" -exec chown "$USER" {} \; -# The LISA run will produce a tree similar to # -# $BUILD_ARTIFACTSTAGINGDIRECTORY/lisa/20221130 -# $BUILD_ARTIFACTSTAGINGDIRECTORY/lisa/20221130/20221130-160013-749 -# $BUILD_ARTIFACTSTAGINGDIRECTORY/lisa/20221130/20221130-160013-749/environments -# $BUILD_ARTIFACTSTAGINGDIRECTORY/lisa/20221130/20221130-160013-749/lisa-20221130-160013-749.log -# $BUILD_ARTIFACTSTAGINGDIRECTORY/lisa/20221130/20221130-160013-749/lisa.junit.xml -# etc +# Move the relevant logs to the staging directory # -# Remove the 2 levels of the tree that indicate the time of the test run to make navigation -# in the Azure Pipelines UI easier. Also, move the lisa log one level up and remove some of -# the logs that are not needed -# -mv "$BUILD_ARTIFACTSTAGINGDIRECTORY"/lisa/[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]/*/* "$BUILD_ARTIFACTSTAGINGDIRECTORY"/lisa -rm -r "$BUILD_ARTIFACTSTAGINGDIRECTORY"/lisa/[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9] -mv "$BUILD_ARTIFACTSTAGINGDIRECTORY"/lisa/lisa-*.log "$BUILD_ARTIFACTSTAGINGDIRECTORY" -rm "$BUILD_ARTIFACTSTAGINGDIRECTORY"/lisa/messages.log +if ls "$LOGS_DIRECTORY"/env-*.log > /dev/null 2>&1; then + mkdir "$BUILD_ARTIFACTSTAGINGDIRECTORY"/environment_logs + mv "$LOGS_DIRECTORY"/env-*.log "$BUILD_ARTIFACTSTAGINGDIRECTORY"/environment_logs +fi +if ls "$LOGS_DIRECTORY"/*.log > /dev/null 2>&1; then + mkdir "$BUILD_ARTIFACTSTAGINGDIRECTORY"/test_logs + mv "$LOGS_DIRECTORY"/*.log "$BUILD_ARTIFACTSTAGINGDIRECTORY"/test_logs +fi +# Move the logs for failed tests to the main directory +if ls "$BUILD_ARTIFACTSTAGINGDIRECTORY"/test_logs/_*.log > /dev/null 2>&1; then + mv "$BUILD_ARTIFACTSTAGINGDIRECTORY"/test_logs/_*.log "$BUILD_ARTIFACTSTAGINGDIRECTORY" +fi +if ls "$LOGS_DIRECTORY"/*.tgz > /dev/null 2>&1; then + mkdir "$BUILD_ARTIFACTSTAGINGDIRECTORY"/vm_logs + mv "$LOGS_DIRECTORY"/*.tgz "$BUILD_ARTIFACTSTAGINGDIRECTORY"/vm_logs +fi +# Files created by LISA are under .../lisa//" +mkdir "$BUILD_ARTIFACTSTAGINGDIRECTORY"/runbook_logs +mv "$LOGS_DIRECTORY"/lisa/*/*/lisa-*.log "$BUILD_ARTIFACTSTAGINGDIRECTORY"/runbook_logs +mv "$LOGS_DIRECTORY"/lisa/*/*/agent.junit.xml "$BUILD_ARTIFACTSTAGINGDIRECTORY"/runbook_logs cat /tmp/exit.sh bash /tmp/exit.sh diff --git a/tests_e2e/tests/bvts/vm_access.py b/tests_e2e/tests/bvts/vm_access.py index c354bbd9da..1af0f99e16 100755 --- a/tests_e2e/tests/bvts/vm_access.py +++ b/tests_e2e/tests/bvts/vm_access.py @@ -38,7 +38,8 @@ class VmAccessBvt(AgentTest): def run(self): - if type(self._context.node.os).__name__ == 'CoreOs' and self._context.node.os.information.full_version.startswith('Flatcar'): + ssh: SshClient = SshClient(ip_address=self._context.vm_ip_address, username=self._context.username, private_key_file=self._context.private_key_file) + if "-flatcar" in ssh.run_command("uname -a"): raise TestSkipped("Currently VMAccess is not supported on Flatcar") # Try to use a unique username for each test run (note that we truncate to 32 chars to @@ -51,7 +52,7 @@ def run(self): private_key_file: Path = self._context.working_directory/f"{username}_rsa" public_key_file: Path = self._context.working_directory/f"{username}_rsa.pub" log.info("Generating SSH key as %s", private_key_file) - ssh: SshClient = SshClient(ip_address=self._context.vm_ip_address, username=username, private_key_file=private_key_file) + ssh = SshClient(ip_address=self._context.vm_ip_address, username=username, private_key_file=private_key_file) ssh.generate_ssh_key(private_key_file) with public_key_file.open() as f: public_key = f.read() diff --git a/tests_e2e/tests/lib/agent_log.py b/tests_e2e/tests/lib/agent_log.py index 61b4ca85cd..657b729282 100644 --- a/tests_e2e/tests/lib/agent_log.py +++ b/tests_e2e/tests/lib/agent_log.py @@ -409,7 +409,7 @@ def read(self) -> Iterable[AgentLogRecord]: """ if not self._path.exists(): - raise IOError('{0} is not found'.format(self._path)) + raise IOError('{0} does not exist'.format(self._path)) def match_record(): for regex in [self._NEWER_AGENT_RECORD, self._2_2_46_AGENT_RECORD, self._OLDER_AGENT_RECORD]: diff --git a/tests_e2e/tests/lib/logging.py b/tests_e2e/tests/lib/logging.py index 95e6e0cafc..ff636b63de 100644 --- a/tests_e2e/tests/lib/logging.py +++ b/tests_e2e/tests/lib/logging.py @@ -56,6 +56,12 @@ def set_thread_log(self, thread_ident: int, log_file: Path) -> None: handler.setFormatter(self.formatter) self.per_thread_handlers[thread_ident] = handler + def get_thread_log(self, thread_ident: int) -> Path: + handler = self.per_thread_handlers.get(thread_ident) + if handler is None: + return None + return Path(handler.baseFilename) + def close_thread_log(self, thread_ident: int) -> None: handler = self.per_thread_handlers.pop(thread_ident, None) if handler is not None: @@ -64,6 +70,9 @@ def close_thread_log(self, thread_ident: int) -> None: def set_current_thread_log(self, log_file: Path) -> None: self.set_thread_log(current_thread().ident, log_file) + def get_current_thread_log(self) -> Path: + return self.get_thread_log(current_thread().ident) + def close_current_thread_log(self) -> None: self.close_thread_log(current_thread().ident) @@ -109,12 +118,21 @@ def __init__(self): def set_thread_log(self, thread_ident: int, log_file: Path) -> None: self._handler.set_thread_log(thread_ident, log_file) + def get_thread_log_file(self, thread_ident: int) -> Path: + """ + Returns the Path of the log file for the current thread, or None if a log has not been set + """ + return self._handler.get_thread_log(thread_ident) + def close_thread_log(self, thread_ident: int) -> None: self._handler.close_thread_log(thread_ident) def set_current_thread_log(self, log_file: Path) -> None: self._handler.set_current_thread_log(log_file) + def get_current_thread_log(self) -> Path: + return self._handler.get_current_thread_log() + def close_current_thread_log(self) -> None: self._handler.close_current_thread_log() @@ -127,8 +145,11 @@ def set_current_thread_log(log_file: Path): """ Context Manager to set the log file for the current thread temporarily """ + initial_value = log.get_current_thread_log() log.set_current_thread_log(log_file) try: yield finally: log.close_current_thread_log() + if initial_value is not None: + log.set_current_thread_log(initial_value) From 1601e88cd109383c76d5ddea87522ca0f13f6ec7 Mon Sep 17 00:00:00 2001 From: Norberto Arrieta Date: Tue, 4 Apr 2023 12:58:43 -0700 Subject: [PATCH 59/63] Python configuration for tests (#2793) * Python configuration for tests --------- Co-authored-by: narrieta --- .../orchestrator/lib/agent_test_suite.py | 126 +++++++------ .../orchestrator/scripts/check-agent-log.py | 2 +- .../{get-agent-path => get-agent-bin-path} | 0 .../{find-python => get-agent-modules-path} | 40 ++--- .../orchestrator/scripts/get-agent-python | 59 +++++++ .../orchestrator/scripts/get-agent-pythonpath | 74 -------- tests_e2e/orchestrator/scripts/install-agent | 13 +- tests_e2e/orchestrator/scripts/install-tools | 167 +++++++++--------- tests_e2e/orchestrator/scripts/uncompress.py | 2 - tests_e2e/orchestrator/scripts/unzip.py | 2 +- tests_e2e/pipeline/scripts/execute_tests.sh | 13 +- tests_e2e/tests/lib/ssh_client.py | 2 +- 12 files changed, 250 insertions(+), 250 deletions(-) rename tests_e2e/orchestrator/scripts/{get-agent-path => get-agent-bin-path} (100%) rename tests_e2e/orchestrator/scripts/{find-python => get-agent-modules-path} (56%) create mode 100755 tests_e2e/orchestrator/scripts/get-agent-python delete mode 100755 tests_e2e/orchestrator/scripts/get-agent-pythonpath diff --git a/tests_e2e/orchestrator/lib/agent_test_suite.py b/tests_e2e/orchestrator/lib/agent_test_suite.py index 38274db743..0c95daf60f 100644 --- a/tests_e2e/orchestrator/lib/agent_test_suite.py +++ b/tests_e2e/orchestrator/lib/agent_test_suite.py @@ -274,57 +274,81 @@ def _setup_node(self) -> None: log.info("************************************** [Node Setup] **************************************") log.info("") log.info("Test Node: %s", self.context.vm.name) + log.info("IP Address: %s", self.context.vm_ip_address) log.info("Resource Group: %s", self.context.vm.resource_group) log.info("") - self.context.ssh_client.run_command("mkdir -p ~/bin/tests_e2e/tests; touch ~/bin/agent-env") - - # Copy the test tools - tools_path = self.context.test_source_directory/"orchestrator"/"scripts" - tools_target_path = Path("~/bin") - log.info("Copying %s to %s:%s", tools_path, self.context.node.name, tools_target_path) - self.context.ssh_client.copy_to_node(tools_path, tools_target_path, recursive=True) - - # Copy the test libraries - lib_path = self.context.test_source_directory/"tests"/"lib" - lib_target_path = Path("~/bin/tests_e2e/tests") - log.info("Copying %s to %s:%s", lib_path, self.context.node.name, lib_target_path) - self.context.ssh_client.copy_to_node(lib_path, lib_target_path, recursive=True) - - # Copy the test agent - agent_package_path: Path = self._get_agent_package_path() - agent_package_target_path = Path("~/bin")/agent_package_path.name - log.info("Copying %s to %s:%s", agent_package_path, self.context.node.name, agent_package_target_path) - self.context.ssh_client.copy_to_node(agent_package_path, agent_package_target_path) - - # Copy Pypy - # NOTE: Pypy is pre-downloaded to /tmp on the container image used for Azure Pipelines runs. For dev runs, - # if we don't find Pypy under /tmp, then we download it a few lines below. + # + # Ensure that the correct version (x84 vs ARM64) Pypy has been downloaded; it is pre-downloaded to /tmp on the container image + # used for Azure Pipelines runs, but for developer runs it may need to be downloaded. + # if self.context.ssh_client.get_architecture() == "aarch64": pypy_path = Path("/tmp/pypy3.7-arm64.tar.bz2") pypy_download = "https://downloads.python.org/pypy/pypy3.7-v7.3.5-aarch64.tar.bz2" else: pypy_path = Path("/tmp/pypy3.7-x64.tar.bz2") pypy_download = "https://downloads.python.org/pypy/pypy3.7-v7.3.5-linux64.tar.bz2" - - if not pypy_path.exists(): + if pypy_path.exists(): + log.info("Found Pypy at %s", pypy_path) + else: log.info("Downloading %s to %s", pypy_download, pypy_path) run_command(["wget", pypy_download, "-O", pypy_path]) - pypy_target_path = Path("~/bin/pypy3.7.tar.bz2") - log.info("Copying %s to %s:%s", pypy_path, self.context.node.name, pypy_target_path) - self.context.ssh_client.copy_to_node(pypy_path, pypy_target_path) - # Install the tools and libraries - install_command = lambda: self.context.ssh_client.run_command(f"~/bin/scripts/install-tools --agent-package {agent_package_target_path}") - log.info('Installing tools on the test node\n%s', install_command()) - log.info('Remote commands will use %s', self.context.ssh_client.run_command("which python3")) + # + # Create a tarball with the files we need to copy to the test node. The tarball includes two directories: + # + # * bin - Executables file (Bash and Python scripts) + # * lib - Library files (Python modules) + # + # After extracting the tarball on the test node, 'bin' will be added to PATH and PYTHONPATH will be set to 'lib'. + # + # Note that executables are placed directly under 'bin', while the path for Python modules is preserved under 'lib. + # + tarball_path: Path = Path("/tmp/waagent.tar") + log.info("Creating %s with the files need on the test node", tarball_path) + log.info("Adding orchestrator/scripts") + run_command(['tar', 'cvf', str(tarball_path), '--transform=s,.*/,bin/,', '-C', str(self.context.test_source_directory/"orchestrator"/"scripts"), '.']) + # log.info("Adding tests/scripts") + # run_command(['tar', 'rvf', str(tarball_path), '--transform=s,.*/,bin/,', '-C', str(self.context.test_source_directory/"tests"/"scripts"), '.']) + log.info("Adding tests/lib") + run_command(['tar', 'rvf', str(tarball_path), '--transform=s,^,lib/,', '-C', str(self.context.test_source_directory.parent), '--exclude=__pycache__', 'tests_e2e/tests/lib']) + log.info("Contents of %s:\n\n%s", tarball_path, run_command(['tar', 'tvf', str(tarball_path)])) + + # + # Cleanup the test node (useful for developer runs) + # + log.info('Preparing the test node for setup') + # Note that removing lib requires sudo, since a Python cache may have been created by tests using sudo + self.context.ssh_client.run_command("rm -rvf ~/{bin,lib,tmp}", use_sudo=True) + + # + # Copy the tarball, Pypy and the test Agent to the test node + # + target_path = Path("~")/"tmp" + self.context.ssh_client.run_command(f"mkdir {target_path}") + log.info("Copying %s to %s:%s", tarball_path, self.context.node.name, target_path) + self.context.ssh_client.copy_to_node(tarball_path, target_path) + log.info("Copying %s to %s:%s", pypy_path, self.context.node.name, target_path) + self.context.ssh_client.copy_to_node(pypy_path, target_path) + agent_package_path: Path = self._get_agent_package_path() + log.info("Copying %s to %s:%s", agent_package_path, self.context.node.name, target_path) + self.context.ssh_client.copy_to_node(agent_package_path, target_path) + + # + # Extract the tarball and execute the install scripts + # + log.info('Installing tools on the test node') + command = f"tar xf {target_path/tarball_path.name} && ~/bin/install-tools" + log.info("%s\n%s", command, self.context.ssh_client.run_command(command)) - # Install the agent if self.context.is_vhd: log.info("Using a VHD; will not install the Test Agent.") else: - install_command = lambda: self.context.ssh_client.run_command(f"install-agent --package {agent_package_target_path} --version {AGENT_VERSION}", use_sudo=True) - log.info("Installing the Test Agent on %s\n%s", self.context.node.name, install_command()) + log.info("Installing the Test Agent on the test node") + command = f"install-agent --package ~/tmp/{agent_package_path.name} --version {AGENT_VERSION}" + log.info("%s\n%s", command, self.context.ssh_client.run_command(command, use_sudo=True)) + + log.info("Completed test node setup") def _collect_node_logs(self) -> None: """ @@ -393,6 +417,8 @@ def _execute(self, environment: Environment, variables: Dict[str, Any]): # pylint seems to think self.context.test_suites is not iterable. Suppressing warning, since its type is List[AgentTestSuite] # E1133: Non-iterable value self.context.test_suites is used in an iterating context (not-an-iterable) for suite in self.context.test_suites: # pylint: disable=E1133 + log.info("Executing test suite %s", suite.name) + self.context.lisa_log.info("Executing Test Suite %s", suite.name) test_suite_success = self._execute_test_suite(suite) and test_suite_success test_suite_success = self._check_agent_log() and test_suite_success @@ -419,7 +445,7 @@ def _execute(self, environment: Environment, variables: Dict[str, Any]): finally: self._clean_up() if not success: - self._mark_log_as_failed(log_path) + self._mark_log_as_failed() def _execute_test_suite(self, suite: TestSuiteInfo) -> bool: """ @@ -432,7 +458,7 @@ def _execute_test_suite(self, suite: TestSuiteInfo) -> bool: success: bool = True # True if all the tests succeed with _set_thread_name(suite_full_name): # The thread name is added to the LISA log - log_path:Path = self.context.log_path/f"{suite_full_name}.log" + log_path: Path = self.context.log_path/f"{suite_full_name}.log" with set_current_thread_log(log_path): try: log.info("") @@ -447,7 +473,7 @@ def _execute_test_suite(self, suite: TestSuiteInfo) -> bool: test_start_time: datetime.datetime = datetime.datetime.now() log.info("******** Executing %s", test_name) - self.context.lisa_log.info("******** Executing %s", test_full_name) + self.context.lisa_log.info("Executing test %s", test_full_name) try: @@ -455,7 +481,7 @@ def _execute_test_suite(self, suite: TestSuiteInfo) -> bool: summary.append(f"[Passed] {test_name}") log.info("******** [Passed] %s", test_name) - self.context.lisa_log.info("******** [Passed] %s", test_full_name) + self.context.lisa_log.info("[Passed] %s", test_full_name) self._report_test_result( suite_full_name, test_name, @@ -514,7 +540,7 @@ def _execute_test_suite(self, suite: TestSuiteInfo) -> bool: add_exception_stack_trace=True) finally: if not success: - self._mark_log_as_failed(log_path) + self._mark_log_as_failed() return success @@ -551,21 +577,17 @@ def _check_agent_log(self) -> bool: log.info("There are no errors in the agent log") return True - log_path: Path = self.context.log_path/f"CheckAgentLog-{self.context.environment_name}.log" - message = f"Detected {len(errors)} error(s) in the agent log. See {log_path} for a full report." - self.context.lisa_log.info(message) - log.info(message) - - with set_current_thread_log(log_path): - log.info("Detected %s error(s) in the agent log:\n\n%s", len(errors), '\n'.join(['\t' + e.text for e in errors])) - self._mark_log_as_failed(log_path) + message = f"Detected {len(errors)} error(s) in the agent log" + self.context.lisa_log.error(message) + log.error("%s:\n\n%s\n", message, '\n'.join(['\t\t' + e.text.replace('\n', '\n\t\t') for e in errors])) + self._mark_log_as_failed() self._report_test_result( self.context.environment_name, "CheckAgentLog", TestStatus.FAILED, start_time, - message=message + '\n' + '\n'.join([e.text for e in errors[0:3]])) + message=message + ' - First few errors:\n' + '\n'.join([e.text for e in errors[0:3]])) except: # pylint: disable=bare-except log.exception("Error checking agent log") self._report_test_result( @@ -579,11 +601,11 @@ def _check_agent_log(self) -> bool: return False @staticmethod - def _mark_log_as_failed(log_path: Path): + def _mark_log_as_failed(): """ - Renames the given log to prefix it with "_". + Adds a message to indicate the log contains errors. """ - log_path.rename(log_path.parent / ("_" + log_path.name)) + log.info("MARKER-LOG-WITH-ERRORS") @staticmethod def _report_test_result( diff --git a/tests_e2e/orchestrator/scripts/check-agent-log.py b/tests_e2e/orchestrator/scripts/check-agent-log.py index 231e7bcd05..8807f8046a 100755 --- a/tests_e2e/orchestrator/scripts/check-agent-log.py +++ b/tests_e2e/orchestrator/scripts/check-agent-log.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python3 +#!/usr/bin/env pypy3 # Microsoft Azure Linux Agent # diff --git a/tests_e2e/orchestrator/scripts/get-agent-path b/tests_e2e/orchestrator/scripts/get-agent-bin-path similarity index 100% rename from tests_e2e/orchestrator/scripts/get-agent-path rename to tests_e2e/orchestrator/scripts/get-agent-bin-path diff --git a/tests_e2e/orchestrator/scripts/find-python b/tests_e2e/orchestrator/scripts/get-agent-modules-path similarity index 56% rename from tests_e2e/orchestrator/scripts/find-python rename to tests_e2e/orchestrator/scripts/get-agent-modules-path index b36178e361..5493b96d9a 100755 --- a/tests_e2e/orchestrator/scripts/find-python +++ b/tests_e2e/orchestrator/scripts/get-agent-modules-path @@ -18,34 +18,20 @@ # # -# Returns the path to the Python executable. +# Returns the PYTHONPATH on which the azurelinuxagent and associated modules are located. +# +# To do this, the script walks the site packages for the Python used to execute the agent, +# looking for the directory that contains "azurelinuxagent". # set -euo pipefail -# python3 is available on most distros -if which python3 2> /dev/null; then - exit 0 -fi - -# try python -if which python 2> /dev/null; then - exit 0 -fi - -# try some well-known locations -declare -a known_locations=( - "/usr/share/oem/python/bin/python" - "/usr/share/oem/python/bin/python3" -) - -for python in "${known_locations[@]}" -do - if [[ -e $python ]]; then - echo "$python" - exit 0 - fi -done - -echo "Can't find the python executable" >&2 +$(get-agent-python) -c ' +import site +import os -exit 1 +for dir in site.getsitepackages(): + if os.path.isdir(dir + "/azurelinuxagent"): + print(dir) + exit(0) +exit(1) +' diff --git a/tests_e2e/orchestrator/scripts/get-agent-python b/tests_e2e/orchestrator/scripts/get-agent-python new file mode 100755 index 0000000000..049732d454 --- /dev/null +++ b/tests_e2e/orchestrator/scripts/get-agent-python @@ -0,0 +1,59 @@ +#!/usr/bin/env bash + +# 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. +# + +# +# Returns the path of the Python executable used to start the Agent. +# +set -euo pipefail + +# if the agent is running, get the python command from 'exe' in the /proc file system +if test -e /run/waagent.pid; then + exe="/proc/$(cat /run/waagent.pid)/exe" + if test -e "$exe"; then + # exe is a symbolic link; return its target + readlink -f "$exe" + exit 0 + fi +fi + +# try all the instances of 'python' and 'python3' in $PATH +for path in $(echo "$PATH" | tr ':' '\n'); do + if [[ -e $path ]]; then + for python in $(find "$path" -maxdepth 1 -name python3 -or -name python); do + if $python -c 'import azurelinuxagent' 2> /dev/null; then + echo "$python" + exit 0 + fi + done + fi +done + +# try some well-known locations +declare -a known_locations=( + "/usr/share/oem/python/bin/python" +) +for python in "${known_locations[@]}" +do + if $python -c 'import azurelinuxagent' 2> /dev/null; then + echo "$python" + exit 0 + fi +done + +exit 1 diff --git a/tests_e2e/orchestrator/scripts/get-agent-pythonpath b/tests_e2e/orchestrator/scripts/get-agent-pythonpath deleted file mode 100755 index bc9f0764e4..0000000000 --- a/tests_e2e/orchestrator/scripts/get-agent-pythonpath +++ /dev/null @@ -1,74 +0,0 @@ -#!/usr/bin/env bash - -# 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. -# - -# -# Returns the PYTHONPATH on which the azurelinuxagent and associated modules are located. -# -# To do this, the script tries to find the python command used to start the agent and then -# returns the value of site.getsitepackages(). -# -set -euo pipefail - -find-agent-python() { - # if the agent is running, get the python command from 'exe' in the /proc file system - if test -e /run/waagent.pid; then - exe="/proc/$(cat /run/waagent.pid)/exe" - if test -e "$exe"; then - # exe is a symbolic link; return its target - readlink -f "$exe" - return 0 - fi - fi - - # try all the instances of 'python' and 'python3' in $PATH - for path in $(echo "$PATH" | tr ':' '\n'); do - if [[ -e $path ]]; then - for python in $(find "$path" -maxdepth 1 -name python3 -or -name python); do - if $python -c 'import azurelinuxagent' 2> /dev/null; then - echo "$python" - return 0 - fi - done - fi - done - - # try some well-known locations - declare -a known_locations=( - "/usr/share/oem/python/bin/python" - "/usr/share/oem/python/bin/python3" - ) - - for python in "${known_locations[@]}" - do - if $python -c 'import azurelinuxagent' 2> /dev/null; then - echo "$python" - return 0 - fi - done - - - return 1 -} - -if ! python=$(find-agent-python); then - echo "Can't find the python command used to start the agent" >&2 - exit 1 -fi - -$python -c 'import site; print(":".join(site.getsitepackages()))' diff --git a/tests_e2e/orchestrator/scripts/install-agent b/tests_e2e/orchestrator/scripts/install-agent index 197f026540..14663d0b8d 100755 --- a/tests_e2e/orchestrator/scripts/install-agent +++ b/tests_e2e/orchestrator/scripts/install-agent @@ -73,12 +73,13 @@ fi echo "Service name: $service_name" # -# Find the path to the Agent's executable file +# Output the initial version of the agent # -waagent=$(get-agent-path) +python=$(get-agent-python) +waagent=$(get-agent-bin-path) echo "Agent's path: $waagent" -$waagent --version -echo "" +$python "$waagent" --version +printf "\n" # # Install the package @@ -112,7 +113,7 @@ echo "Verifying agent installation..." check-version() { for i in {0..5} do - if $waagent --version | grep -E "Goal state agent:\s+$version" > /dev/null; then + if $python "$waagent" --version | grep -E "Goal state agent:\s+$version" > /dev/null; then return 0 fi sleep 10 @@ -129,7 +130,7 @@ else exit_code=1 fi -$waagent --version +$python "$waagent" --version printf "\n" service-status $service_name diff --git a/tests_e2e/orchestrator/scripts/install-tools b/tests_e2e/orchestrator/scripts/install-tools index 6feb71e530..2e2dd53fb7 100755 --- a/tests_e2e/orchestrator/scripts/install-tools +++ b/tests_e2e/orchestrator/scripts/install-tools @@ -25,112 +25,111 @@ set -euo pipefail -usage() ( - echo "Usage: install-tools -p|--agent-package " - exit 1 -) - -while [[ $# -gt 0 ]]; do - case $1 in - -p|--agent-package) - shift - if [ "$#" -lt 1 ]; then - usage - fi - agent_package=$1 - shift - ;; - *) - usage - esac -done -if [ "$#" -ne 0 ] || [ -z ${agent_package+x} ]; then - usage -fi - -echo "Installing scripts to ~/bin" -mv ~/bin/scripts/* ~/bin -rm -r ~/bin/scripts PATH="$HOME/bin:$PATH" -# If the system's Python is <= 3.7, install Pypy and make it the default Python for the user executing the tests -python=$(~/bin/find-python) -python_version=$($python -c 'import sys; print("{0:02}.{1:02}".format(sys.version_info[0], sys.version_info[1]))') -echo "Python: $python ($python_version)" -if [[ $python_version < "03.07" ]]; then - echo "Installing Pypy 3.7 to ~/bin and making it the default Python for user $USER" - # bzip2/lbzip2 (used by tar to uncompress *.bz2 files) are not available by default in some distros; - # use Python to uncompress the Pypy tarball. - $python ~/bin/uncompress.py ~/bin/pypy3.7.tar.bz2 ~/bin/pypy3.7.tar - tar xf ~/bin/pypy3.7.tar -C ~/bin - rm ~/bin/pypy3.7.tar ~/bin/pypy3.7.tar.bz2 +python=$(get-agent-python) +echo "Python executable: $python" +echo "Python version: $($python --version)" - if [[ -e ~/bin/python ]]; then - rm ~/bin/python - fi - ln -s ~/bin/pypy*/bin/pypy3.7 ~/bin/python +# +# Install Pypy as ~/bin/pypy3 +# +# Note that bzip2/lbzip2 (used by tar to uncompress *.bz2 files) are not available by default in some distros; +# use Python to uncompress the Pypy tarball. +# +echo "Installing Pypy 3.7" +$python ~/bin/uncompress.py ~/tmp/pypy3.7-*.tar.bz2 ~/tmp/pypy3.7.tar +tar xf ~/tmp/pypy3.7.tar -C ~/bin +echo "Pypy was installed in $(ls -d ~/bin/pypy*)" +ln -s ~/bin/pypy*/bin/pypy3.7 ~/bin/pypy3 +echo "Creating symbolic link to Pypy: ~/bin/pypy3" - if [[ -e ~/bin/python3 ]]; then - rm ~/bin/python3 - fi - ln -s ~/bin/pypy*/bin/pypy3.7 ~/bin/python3 +# +# The 'distro' and 'platform' modules in Pypy have small differences with the ones in the system's Python. +# This can create problems in tests that use the get_distro() method in the Agent's 'version.py' module. +# To work around this, we copy the system's 'distro' module to Pypy. +# +# In the case of 'platform', the 'linux_distribution' method was removed on Python 3.7 so we check the +# system's module and, if the method does not exist, we also remove it from Pypy. Ubuntu 16 and 18 are +# special cases in that the 'platform' module in Pypy identifies the distro as 'debian'; in this case we +# copy the system's 'platform' module to Pypy. +# +distro_path=$($python -c ' +try: + import distro +except: + exit(0) +print(distro.__file__.replace("__init__.py", "distro.py")) +exit(0) +') +if [[ "$distro_path" != "" ]]; then + echo "Copying the system's distro module to Pypy" + cp -v "$distro_path" ~/bin/pypy*/site-packages +else + echo "The distro module is not is not installing on the system; skipping." +fi - echo "Installing the 'distro' module" - python3 -m ensurepip - python3 -mpip install -U pip wheel - python3 -mpip install distro +has_linux_distribution=$($python -c 'import platform; print(hasattr(platform, "linux_distribution"))') +if [[ "$has_linux_distribution" == "False" ]]; then + echo "Python does not have platform.linux_distribution; removing it from Pypy" + sed -i 's/def linux_distribution(/def __linux_distribution__(/' ~/bin/pypy*/lib-python/3/platform.py else - # In some distros (e.g. Flatcar), Python is not in PATH; in that case create a symlink under ~/bin - if ! which python3 > /dev/null 2>&1; then - if [[ ! $python_version < "03.00" ]]; then - echo "Python ($python) is not in PATH; creating symbolic links as ~/bin/python and ~/bin/python3" - ln -s "$python" ~/bin/python - ln -s "$python" ~/bin/python3 - fi + echo "Python has platform.linux_distribution" + uname=$(uname -v) + if [[ "$uname" == *~18*-Ubuntu* || "$uname" == *~16*-Ubuntu* ]]; then + echo "Copying the system's platform module to Pypy" + pypy_platform=$(pypy3 -c 'import platform; print(platform.__file__)') + python_platform=$($python -c 'import platform; print(platform.__file__)') + cp -v "$python_platform" "$pypy_platform" fi fi -echo "Installing Agent modules to ~/bin" -unzip.py "$agent_package" ~/bin/WALinuxAgent -unzip.py ~/bin/WALinuxAgent/bin/WALinuxAgent-*.egg ~/bin/WALinuxAgent/bin/WALinuxAgent.egg -mv ~/bin/WALinuxAgent/bin/WALinuxAgent.egg/azurelinuxagent ~/bin -rm -rf ~/bin/WALinuxAgent +# +# Now install the test Agent as a module package in Pypy. +# +echo "Installing Agent modules to Pypy" +unzip.py ~/tmp/WALinuxAgent-*.zip ~/tmp/WALinuxAgent +unzip.py ~/tmp/WALinuxAgent/bin/WALinuxAgent-*.egg ~/tmp/WALinuxAgent/bin/WALinuxAgent.egg +mv ~/tmp/WALinuxAgent/bin/WALinuxAgent.egg/azurelinuxagent ~/bin/pypy*/site-packages # -# Create ~/bin/agent-env to set PATH and PYTHONPATH. +# Log the results of get_distro() in Pypy and Python. # -# We add $HOME/bin to the front of PATH, so tools in ~/bin (including python) will -# take precedence over system tools with the same name. +pypy_get_distro=$(pypy3 -c 'from azurelinuxagent.common.version import get_distro; print(get_distro())') +python_get_distro=$($python -c 'from azurelinuxagent.common.version import get_distro; print(get_distro())') +echo "Pypy get_distro(): $pypy_get_distro" +echo "Python get_distro(): $python_get_distro" + # -# We set PYTHONPATH to include the location of the agent modules installed on the VM image and also -# the test modules we copied to ~/bin. +# Create ~/bin/set-agent-env to set PATH and PYTHONPATH. # +# We append $HOME/bin to PATH and set PYTHONPATH to $HOME/lib (bin contains the scripts used by tests, while +# lib contains the Python libraries used by tests). # -echo "Creating ~/bin/agent-env to set PATH and PYTHONPATH" -echo ' -if [[ $PATH != *"$HOME/bin"* ]]; then - PATH="$HOME/bin:$PATH:" +echo "Creating ~/bin/set-agent-env to set PATH and PYTHONPATH" + +echo " +if [[ \$PATH != *\"$HOME/bin\"* ]]; then + PATH=\"$HOME/bin:\$PATH:\" fi -export PYTHONPATH="$HOME/bin" -' > ~/bin/agent-env -chmod u+x ~/bin/agent-env +export PYTHONPATH=\"$HOME/lib\" +" > ~/bin/set-agent-env -echo "Adding ~/bin/agent-env to ~/.bash_profile" -# In some distros, e.g. Flatcar, .bash_profile is a symbolic link to a read-only file. Make a copy in -# that case. +chmod u+x ~/bin/set-agent-env + +# +# Add ~/bin/set-agent-env to .bash_profile to simplify interactive debugging sessions +# +# Note that in some distros .bash_profile is a symbolic link to a read-only file. Make a copy in that case. +# +echo "Adding ~/bin/set-agent-env to ~/.bash_profile" if test -e ~/.bash_profile && ls -l .bash_profile | grep '\->'; then cp ~/.bash_profile ~/.bash_profile-bk rm ~/.bash_profile mv ~/.bash_profile-bk ~/.bash_profile fi -if ! test -e ~/.bash_profile || ! grep '~/bin/agent-env' ~/.bash_profile > /dev/null; then - echo 'source ~/bin/agent-env +if ! test -e ~/.bash_profile || ! grep '~/bin/set-agent-env' ~/.bash_profile > /dev/null; then + echo 'source ~/bin/set-agent-env ' >> ~/.bash_profile fi - -source ~/bin/agent-env -echo "PATH=$PATH" -echo "PYTHONPATH=$PYTHONPATH" -echo "python3 -> $(which python3)" -python3 --version diff --git a/tests_e2e/orchestrator/scripts/uncompress.py b/tests_e2e/orchestrator/scripts/uncompress.py index 796ea98007..755397cf3e 100755 --- a/tests_e2e/orchestrator/scripts/uncompress.py +++ b/tests_e2e/orchestrator/scripts/uncompress.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - # Microsoft Azure Linux Agent # # Copyright 2018 Microsoft Corporation diff --git a/tests_e2e/orchestrator/scripts/unzip.py b/tests_e2e/orchestrator/scripts/unzip.py index e25da1981f..b909d6ae78 100755 --- a/tests_e2e/orchestrator/scripts/unzip.py +++ b/tests_e2e/orchestrator/scripts/unzip.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python3 +#!/usr/bin/env pypy3 # Microsoft Azure Linux Agent # diff --git a/tests_e2e/pipeline/scripts/execute_tests.sh b/tests_e2e/pipeline/scripts/execute_tests.sh index e5054bf2ea..15c9f0b5f6 100755 --- a/tests_e2e/pipeline/scripts/execute_tests.sh +++ b/tests_e2e/pipeline/scripts/execute_tests.sh @@ -86,18 +86,27 @@ sudo find "$LOGS_DIRECTORY" -exec chown "$USER" {} \; # # Move the relevant logs to the staging directory # +# Move the logs for failed tests to a temporary location +mkdir "$BUILD_ARTIFACTSTAGINGDIRECTORY"/tmp +for log in $(grep -l MARKER-LOG-WITH-ERRORS "$LOGS_DIRECTORY"/*.log); do + mv "$log" "$BUILD_ARTIFACTSTAGINGDIRECTORY"/tmp +done +# Move the environment logs to "environment_logs" if ls "$LOGS_DIRECTORY"/env-*.log > /dev/null 2>&1; then mkdir "$BUILD_ARTIFACTSTAGINGDIRECTORY"/environment_logs mv "$LOGS_DIRECTORY"/env-*.log "$BUILD_ARTIFACTSTAGINGDIRECTORY"/environment_logs fi +# Move the rest of the logs to "test_logs" if ls "$LOGS_DIRECTORY"/*.log > /dev/null 2>&1; then mkdir "$BUILD_ARTIFACTSTAGINGDIRECTORY"/test_logs mv "$LOGS_DIRECTORY"/*.log "$BUILD_ARTIFACTSTAGINGDIRECTORY"/test_logs fi # Move the logs for failed tests to the main directory -if ls "$BUILD_ARTIFACTSTAGINGDIRECTORY"/test_logs/_*.log > /dev/null 2>&1; then - mv "$BUILD_ARTIFACTSTAGINGDIRECTORY"/test_logs/_*.log "$BUILD_ARTIFACTSTAGINGDIRECTORY" +if ls "$BUILD_ARTIFACTSTAGINGDIRECTORY"/tmp/*.log > /dev/null 2>&1; then + mv "$BUILD_ARTIFACTSTAGINGDIRECTORY"/tmp/*.log "$BUILD_ARTIFACTSTAGINGDIRECTORY" fi +rmdir "$BUILD_ARTIFACTSTAGINGDIRECTORY"/tmp +# Move the logs collected from the test VMs to vm_logs if ls "$LOGS_DIRECTORY"/*.tgz > /dev/null 2>&1; then mkdir "$BUILD_ARTIFACTSTAGINGDIRECTORY"/vm_logs mv "$LOGS_DIRECTORY"/*.tgz "$BUILD_ARTIFACTSTAGINGDIRECTORY"/vm_logs diff --git a/tests_e2e/tests/lib/ssh_client.py b/tests_e2e/tests/lib/ssh_client.py index c10d763a47..a6e1ab9fd3 100644 --- a/tests_e2e/tests/lib/ssh_client.py +++ b/tests_e2e/tests/lib/ssh_client.py @@ -46,7 +46,7 @@ def run_command(self, command: str, use_sudo: bool = False) -> str: sudo = "sudo env PATH=$PATH PYTHONPATH=$PYTHONPATH" if use_sudo else '' return retry_ssh_run(lambda: shell.run_command([ "ssh", "-o", "StrictHostKeyChecking=no", "-i", self._private_key_file, destination, - f"source ~/bin/agent-env;{sudo} {command}"])) + f"if [[ -e ~/bin/set-agent-env ]]; then source ~/bin/set-agent-env; fi; {sudo} {command}"])) @staticmethod def generate_ssh_key(private_key_file: Path): From c89647cf17946f92b5b6142abf4c6d59cee840b1 Mon Sep 17 00:00:00 2001 From: maddieford <93676569+maddieford@users.noreply.github.com> Date: Fri, 7 Apr 2023 13:12:11 -0700 Subject: [PATCH 60/63] Update assert (#2799) * Update version to dummy 1.0.0.0' * Revert version change * Update test_download_fail assertion --- tests/ga/test_update.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/ga/test_update.py b/tests/ga/test_update.py index b329bbaa05..1b84d6f1c4 100644 --- a/tests/ga/test_update.py +++ b/tests/ga/test_update.py @@ -556,7 +556,8 @@ def http_get_handler(uri, *_, **__): return MockHttpResponse(status=httpclient.SERVICE_UNAVAILABLE) return None - pkg = ExtHandlerPackage(version=str(self._get_agent_version())) + agent_version = self._get_agent_version() + pkg = ExtHandlerPackage(version=str(agent_version)) pkg.uris.append(agent_uri) with mock_wire_protocol(mockwiredata.DATA_FILE) as protocol: @@ -568,7 +569,7 @@ def http_get_handler(uri, *_, **__): messages = [kwargs['message'] for _, kwargs in add_event.call_args_list if kwargs['op'] == 'Install' and kwargs['is_success'] == False] self.assertEqual(1, len(messages), "Expected exactly 1 install error/ Got: {0}".format(add_event.call_args_list)) - self.assertIn('[UpdateError] Unable to download Agent WALinuxAgent-9.9.9.9', messages[0], "The install error does not include the expected message") + self.assertIn(str.format('[UpdateError] Unable to download Agent WALinuxAgent-{0}', agent_version), messages[0], "The install error does not include the expected message") self.assertFalse(agent.is_blacklisted, "Download failures should not blacklist the Agent") From 8d0ca20fe040bc16b004dec96161f69daf21b530 Mon Sep 17 00:00:00 2001 From: maddieford <93676569+maddieford@users.noreply.github.com> Date: Fri, 7 Apr 2023 13:46:45 -0700 Subject: [PATCH 61/63] Update version to 2.9.1.0 (#2800) * Update version to dummy 1.0.0.0' * Revert version change * Update version to 2.9.1.0 * Trigger unit tests --- azurelinuxagent/common/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/azurelinuxagent/common/version.py b/azurelinuxagent/common/version.py index ff9c903b93..a77f35e257 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 = '9.9.9.9' +AGENT_VERSION = '2.9.1.0' AGENT_LONG_VERSION = "{0}-{1}".format(AGENT_NAME, AGENT_VERSION) AGENT_DESCRIPTION = """ The Azure Linux Agent supports the provisioning and running of Linux From 66e8b3d782fdf2ebc443212bbb731a89599201f6 Mon Sep 17 00:00:00 2001 From: maddieford <93676569+maddieford@users.noreply.github.com> Date: Fri, 12 May 2023 16:32:31 -0700 Subject: [PATCH 62/63] Add vm arch to heartbeat telemetry (#2818) * Add VM Arch to heartbeat telemetry * Remove outdated vmsize heartbeat tesT * Remove unused import * Use platform to get vmarch --- azurelinuxagent/ga/update.py | 12 ++++++---- tests/ga/test_update.py | 43 ------------------------------------ 2 files changed, 8 insertions(+), 47 deletions(-) diff --git a/azurelinuxagent/ga/update.py b/azurelinuxagent/ga/update.py index cd758b972a..2b0975b05b 100644 --- a/azurelinuxagent/ga/update.py +++ b/azurelinuxagent/ga/update.py @@ -19,6 +19,7 @@ import glob import json import os +import platform import re import shutil import signal @@ -462,6 +463,9 @@ def _get_vm_size(self, protocol): return self._vm_size + def _get_vm_arch(self): + return platform.machine() + def _check_daemon_running(self, debug): # Check that the parent process (the agent's daemon) is still running if not debug and self._is_orphaned: @@ -1265,13 +1269,13 @@ def _send_heartbeat_telemetry(self, protocol): if datetime.utcnow() >= (self._last_telemetry_heartbeat + UpdateHandler.TELEMETRY_HEARTBEAT_PERIOD): dropped_packets = self.osutil.get_firewall_dropped_packets(protocol.get_endpoint()) auto_update_enabled = 1 if conf.get_autoupdate_enabled() else 0 - # Include VMSize in the heartbeat message because the kusto table does not have - # a separate column for it (or architecture). - vmsize = self._get_vm_size(protocol) + # Include vm architecture in the heartbeat message because the kusto table does not have + # a separate column for it. + vmarch = self._get_vm_arch() telemetry_msg = "{0};{1};{2};{3};{4};{5}".format(self._heartbeat_counter, self._heartbeat_id, dropped_packets, self._heartbeat_update_goal_state_error_count, - auto_update_enabled, vmsize) + auto_update_enabled, vmarch) debug_log_msg = "[DEBUG HeartbeatCounter: {0};HeartbeatId: {1};DroppedPackets: {2};" \ "UpdateGSErrors: {3};AutoUpdate: {4}]".format(self._heartbeat_counter, self._heartbeat_id, dropped_packets, diff --git a/tests/ga/test_update.py b/tests/ga/test_update.py index 1b84d6f1c4..e5f15fbd07 100644 --- a/tests/ga/test_update.py +++ b/tests/ga/test_update.py @@ -20,7 +20,6 @@ from datetime import datetime, timedelta from threading import current_thread -from azurelinuxagent.common.protocol.imds import ComputeInfo from tests.common.osutil.test_default import TestOSUtil import azurelinuxagent.common.osutil.default as osutil @@ -2773,48 +2772,6 @@ def test_telemetry_heartbeat_creates_event(self, patch_add_event, patch_info, *_ self.assertTrue(any(call_args[0] == "[HEARTBEAT] Agent {0} is running as the goal state agent {1}" for call_args in patch_info.call_args), "The heartbeat was not written to the agent's log") - @patch("azurelinuxagent.ga.update.add_event") - @patch("azurelinuxagent.common.protocol.imds.ImdsClient") - def test_telemetry_heartbeat_retries_failed_vm_size_fetch(self, mock_imds_factory, patch_add_event, *_): - - def validate_single_heartbeat_event_matches_vm_size(vm_size): - heartbeat_event_kwargs = [ - kwargs for _, kwargs in patch_add_event.call_args_list - if kwargs.get('op', None) == WALAEventOperation.HeartBeat - ] - - self.assertEqual(1, len(heartbeat_event_kwargs), "Expected exactly one HeartBeat event, got {0}"\ - .format(heartbeat_event_kwargs)) - - telemetry_message = heartbeat_event_kwargs[0].get("message", "") - self.assertTrue(telemetry_message.endswith(vm_size), - "Expected HeartBeat message ('{0}') to end with the test vmSize value, {1}."\ - .format(telemetry_message, vm_size)) - - with mock_wire_protocol(mockwiredata.DATA_FILE) as mock_protocol: - update_handler = get_update_handler() - update_handler.protocol_util.get_protocol = Mock(return_value=mock_protocol) - - # Zero out the _vm_size parameter for test resiliency - update_handler._vm_size = None - - mock_imds_client = mock_imds_factory.return_value = Mock() - - # First force a vmSize retrieval failure - mock_imds_client.get_compute.side_effect = HttpError(msg="HTTP Test Failure") - update_handler._last_telemetry_heartbeat = datetime.utcnow() - timedelta(hours=1) - update_handler._send_heartbeat_telemetry(mock_protocol) - - validate_single_heartbeat_event_matches_vm_size("unknown") - patch_add_event.reset_mock() - - # Now provide a vmSize - mock_imds_client.get_compute = lambda: ComputeInfo(vmSize="TestVmSizeValue") - update_handler._last_telemetry_heartbeat = datetime.utcnow() - timedelta(hours=1) - update_handler._send_heartbeat_telemetry(mock_protocol) - - validate_single_heartbeat_event_matches_vm_size("TestVmSizeValue") - class AgentMemoryCheckTestCase(AgentTestCase): From 362f6859662598347ddf4c7066e3e9c3136539d7 Mon Sep 17 00:00:00 2001 From: maddieford <93676569+maddieford@users.noreply.github.com> Date: Fri, 12 May 2023 16:44:05 -0700 Subject: [PATCH 63/63] Increment version to 2.9.1.1 for retake (#2819) --- azurelinuxagent/common/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/azurelinuxagent/common/version.py b/azurelinuxagent/common/version.py index a77f35e257..8e12eff5f1 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.9.1.0' +AGENT_VERSION = '2.9.1.1' AGENT_LONG_VERSION = "{0}-{1}".format(AGENT_NAME, AGENT_VERSION) AGENT_DESCRIPTION = """ The Azure Linux Agent supports the provisioning and running of Linux

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