Skip to content

Commit 4434d2a

Browse files
authored
Log changes (#166)
* Log changes * Addressing feedback for: Log changes #1 * Adding some missing UTs for: Log changes * Addressing feedback for: Log changes #2 * Addressing more UTs for: Log changes * Addressing feedback for: Log changes #2 * Minor changes/cleanup for: Log changes
1 parent e239674 commit 4434d2a

21 files changed

+612
-38
lines changed

src/core/src/CoreMain.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
# limitations under the License.
1414
#
1515
# Requires Python 2.7+
16+
import os
1617

1718
from core.src.bootstrap.Bootstrapper import Bootstrapper
1819
from core.src.bootstrap.Constants import Constants
@@ -27,7 +28,7 @@ def __init__(self, argv):
2728
composite_logger = bootstrapper.composite_logger
2829
stdout_file_mirror = bootstrapper.stdout_file_mirror
2930
telemetry_writer = bootstrapper.telemetry_writer
30-
lifecycle_manager = status_handler = None
31+
lifecycle_manager = status_handler = execution_config = None
3132

3233
# Init operation statuses
3334
patch_operation_requested = Constants.UNKNOWN
@@ -59,6 +60,12 @@ def __init__(self, argv):
5960
telemetry_writer.set_task_name(Constants.TelemetryTaskName.AUTO_ASSESSMENT if execution_config.exec_auto_assess_only else Constants.TelemetryTaskName.EXEC)
6061
patch_operation_requested = execution_config.operation.lower()
6162

63+
# clean up temp folder before any operation execution begins from Core
64+
if os.path.exists(execution_config.temp_folder):
65+
composite_logger.log_debug("Deleting all files of certain format from temp folder [FileFormat={0}][TempFolderLocation={1}]"
66+
.format(Constants.TEMP_FOLDER_CLEANUP_ARTIFACT_LIST, str(execution_config.temp_folder)))
67+
bootstrapper.env_layer.file_system.delete_files_from_dir(execution_config.temp_folder, Constants.TEMP_FOLDER_CLEANUP_ARTIFACT_LIST)
68+
6269
patch_assessor = container.get('patch_assessor')
6370
package_manager = container.get('package_manager')
6471
configure_patching_processor = container.get('configure_patching_processor')
@@ -115,6 +122,12 @@ def __init__(self, argv):
115122
composite_logger.log_debug("Completed exception handling.\n")
116123

117124
finally:
125+
# clean up temp folder of files created by Core after execution completes
126+
if self.is_temp_folder_available(bootstrapper.env_layer, execution_config):
127+
composite_logger.log_debug("Deleting all files of certain format from temp folder [FileFormat={0}][TempFolderLocation={1}]"
128+
.format(Constants.TEMP_FOLDER_CLEANUP_ARTIFACT_LIST, str(execution_config.temp_folder)))
129+
bootstrapper.env_layer.file_system.delete_files_from_dir(execution_config.temp_folder, Constants.TEMP_FOLDER_CLEANUP_ARTIFACT_LIST)
130+
118131
if lifecycle_manager is not None:
119132
lifecycle_manager.update_core_sequence(completed=True)
120133

@@ -139,3 +152,10 @@ def update_patch_substatus_if_pending(patch_operation_requested, overall_patch_i
139152
status_handler.set_configure_patching_substatus_json(status=Constants.STATUS_ERROR)
140153
composite_logger.log_debug(' -- Persisted failed configure patching substatus.')
141154

155+
@staticmethod
156+
def is_temp_folder_available(env_layer, execution_config):
157+
return env_layer is not None \
158+
and execution_config is not None \
159+
and execution_config.temp_folder is not None \
160+
and os.path.exists(execution_config.temp_folder)
161+

src/core/src/bootstrap/Constants.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ class EnvSettings(EnumBackport):
5959
CONFIG_FOLDER = "configFolder"
6060
STATUS_FOLDER = "statusFolder"
6161
EVENTS_FOLDER = "eventsFolder"
62+
TEMP_FOLDER = "tempFolder"
6263
TELEMETRY_SUPPORTED = "telemetrySupported"
6364

6465
class ConfigSettings(EnumBackport):
@@ -76,6 +77,9 @@ class ConfigSettings(EnumBackport):
7677
ASSESSMENT_MODE = 'assessmentMode'
7778
MAXIMUM_ASSESSMENT_INTERVAL = 'maximumAssessmentInterval'
7879

80+
TEMP_FOLDER_DIR_NAME = "tmp"
81+
TEMP_FOLDER_CLEANUP_ARTIFACT_LIST = ["*.list"]
82+
7983
# File to save default settings for auto OS updates
8084
IMAGE_DEFAULT_PATCH_CONFIGURATION_BACKUP_PATH = "ImageDefaultPatchConfiguration.bak"
8185

src/core/src/bootstrap/EnvLayer.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
from __future__ import print_function
1818
import base64
1919
import datetime
20+
import glob
2021
import json
2122
import os
2223
import re
@@ -428,6 +429,27 @@ def write_with_retry_using_temp_file(file_path, data, mode='w'):
428429
else:
429430
raise Exception("Unable to write to {0} (retries exhausted). Error: {1}.".format(str(file_path), repr(error)))
430431

432+
@staticmethod
433+
def delete_files_from_dir(dir_name, file_identifier_list, raise_if_delete_failed=False):
434+
""" Clears all files from given dir. NOTE: Uses file_identifier_list to determine the content to delete """
435+
for file_identifier in file_identifier_list:
436+
files_to_delete = glob.glob(str(dir_name) + "/" + str(file_identifier))
437+
438+
for file_to_delete in files_to_delete:
439+
try:
440+
os.remove(file_to_delete)
441+
except Exception as error:
442+
error_message = "Unable to delete files from directory [Dir={0}][File={1}][Error={2}][RaiseIfDeleteFailed={3}].".format(
443+
str(dir_name),
444+
str(file_to_delete),
445+
repr(error),
446+
str(raise_if_delete_failed))
447+
448+
if raise_if_delete_failed:
449+
raise Exception(error_message)
450+
else:
451+
print(error_message)
452+
return None
431453
# endregion - File system emulation and extensions
432454

433455
# region - DateTime emulation and extensions

src/core/src/core_logic/ExecutionConfig.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,9 @@ def __init__(self, env_layer, composite_logger, execution_parameters):
4444
self.config_folder = self.environment_settings[Constants.EnvSettings.CONFIG_FOLDER]
4545
self.status_folder = self.environment_settings[Constants.EnvSettings.STATUS_FOLDER]
4646
self.events_folder = self.environment_settings[Constants.EnvSettings.EVENTS_FOLDER]
47+
self.temp_folder = self.environment_settings[Constants.EnvSettings.TEMP_FOLDER]
48+
self.__check_and_create_temp_folder_if_not_exists()
49+
4750
self.telemetry_supported = self.environment_settings[Constants.EnvSettings.TELEMETRY_SUPPORTED]
4851

4952
# Config Settings
@@ -162,3 +165,16 @@ def __extract_most_significant_unit_from_duration(duration_portion, unit_delimit
162165
else: # bad data
163166
raise Exception("Invalid duration portion: {0}".format(str(duration_portion)))
164167
return most_significant_unit, remaining_duration_portion
168+
169+
def __check_and_create_temp_folder_if_not_exists(self):
170+
"""Verifies temp folder exists, creates new one if not found"""
171+
if self.temp_folder is None:
172+
par_dir = os.path.dirname(self.config_folder)
173+
if not os.path.exists(par_dir):
174+
raise Exception("Parent directory for all extension artifacts such as config folder, status folder, etc. not found at [{0}].".format(repr(par_dir)))
175+
self.temp_folder = os.path.join(par_dir, Constants.TEMP_FOLDER_DIR_NAME)
176+
177+
if not os.path.exists(self.temp_folder):
178+
self.composite_logger.log_debug("Temp folder does not exist, creating one from extension core. [Path={0}]".format(str(self.temp_folder)))
179+
os.mkdir(self.temp_folder)
180+

src/core/src/package_managers/AptitudePackageManager.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
import json
1919
import os
2020
import re
21+
import uuid
22+
2123
from core.src.package_managers.PackageManager import PackageManager
2224
from core.src.bootstrap.Constants import Constants
2325

@@ -28,12 +30,14 @@ class AptitudePackageManager(PackageManager):
2830
# For more details, try `man apt-get` on any Debian/Ubuntu based box.
2931
def __init__(self, env_layer, execution_config, composite_logger, telemetry_writer, status_handler):
3032
super(AptitudePackageManager, self).__init__(env_layer, execution_config, composite_logger, telemetry_writer, status_handler)
33+
34+
security_list_guid = str(uuid.uuid4())
3135
# Repo refresh
3236
self.repo_refresh = 'sudo apt-get -q update'
3337

3438
# Support to get updates and their dependencies
35-
self.security_sources_list = '/tmp/az-update-security.list'
36-
self.prep_security_sources_list_cmd = 'sudo grep security /etc/apt/sources.list > ' + self.security_sources_list
39+
self.security_sources_list = os.path.join(execution_config.temp_folder, 'msft-patch-security-{0}.list'.format(security_list_guid))
40+
self.prep_security_sources_list_cmd = 'sudo grep security /etc/apt/sources.list > ' + os.path.normpath(self.security_sources_list)
3741
self.dist_upgrade_simulation_cmd_template = 'LANG=en_US.UTF8 sudo apt-get -s dist-upgrade <SOURCES> ' # Dist-upgrade simulation template - <SOURCES> needs to be replaced before use; sudo is used as sometimes the sources list needs sudo to be readable
3842
self.single_package_check_versions = 'apt-cache madison <PACKAGE-NAME>'
3943
self.single_package_find_installed_dpkg = 'sudo dpkg -s <PACKAGE-NAME>'

src/core/tests/Test_CoreMain.py

Lines changed: 101 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,11 @@
1414
#
1515
# Requires Python 2.7+
1616
import datetime
17+
import glob
1718
import json
1819
import os
1920
import re
21+
import shutil
2022
import time
2123
import unittest
2224
import uuid
@@ -46,6 +48,12 @@ def mock_linux_distribution_to_return_centos(self):
4648
def mock_linux_distribution_to_return_redhat(self):
4749
return ['Red Hat Enterprise Linux Server', '7.5', 'Maipo']
4850

51+
def mock_os_remove(self, file_to_remove):
52+
raise Exception("File could not be deleted")
53+
54+
def mock_os_path_exists(self, patch_to_validate):
55+
return False
56+
4957
def test_operation_fail_for_non_autopatching_request(self):
5058
# Test for non auto patching request
5159
argument_composer = ArgumentComposer()
@@ -880,7 +888,7 @@ def test_assessment_superseded(self):
880888
scratch_path = os.path.join(os.path.curdir, "scratch")
881889

882890
# Step 2: Set 1.status to Transitioning
883-
with open(os.path.join(scratch_path, "1.status"), 'r+') as f:
891+
with open(os.path.join(scratch_path, "status", "1.status"), 'r+') as f:
884892
status = json.load(f)
885893
status[0]["status"]["status"] = "transitioning"
886894
status[0]["status"]["substatus"][0]["status"] = "transitioning"
@@ -919,6 +927,98 @@ def test_assessment_superseded(self):
919927

920928
runtime.stop()
921929

930+
def test_temp_folder_created_during_execution_config_init(self):
931+
# temp_folder is set with a path in environment settings but the dir does not exist
932+
argument_composer = ArgumentComposer()
933+
shutil.rmtree(argument_composer.temp_folder)
934+
argument_composer.operation = Constants.ASSESSMENT
935+
runtime = RuntimeCompositor(argument_composer.get_composed_arguments(), True, Constants.APT)
936+
# validate temp_folder is created
937+
self.assertTrue(runtime.execution_config.temp_folder is not None)
938+
self.assertTrue(os.path.exists(runtime.execution_config.temp_folder))
939+
runtime.stop()
940+
941+
# temp_folder is set to None in ExecutionConfig with a valid config_folder location
942+
argument_composer = ArgumentComposer()
943+
shutil.rmtree(argument_composer.temp_folder)
944+
argument_composer.temp_folder = None
945+
argument_composer.operation = Constants.ASSESSMENT
946+
runtime = RuntimeCompositor(argument_composer.get_composed_arguments(), True, Constants.APT)
947+
# validate temp_folder is created
948+
self.assertTrue(runtime.execution_config.temp_folder is not None)
949+
self.assertTrue(os.path.exists(runtime.execution_config.temp_folder))
950+
runtime.stop()
951+
952+
# temp_folder is set to None in ExecutionConfig with an invalid config_folder location, throws exception
953+
argument_composer = ArgumentComposer()
954+
shutil.rmtree(argument_composer.temp_folder)
955+
argument_composer.temp_folder = None
956+
argument_composer.operation = Constants.ASSESSMENT
957+
# mock path exists check to return False on config_folder exists check
958+
backup_os_path_exists = os.path.exists
959+
os.path.exists = self.mock_os_path_exists
960+
self.assertRaises(Exception, lambda: RuntimeCompositor(argument_composer.get_composed_arguments(), True, Constants.APT))
961+
# validate temp_folder is not created
962+
self.assertFalse(os.path.exists(os.path.join(os.path.curdir, "scratch", "tmp")))
963+
os.path.exists = backup_os_path_exists
964+
runtime.stop()
965+
966+
def test_delete_temp_folder_contents_success(self):
967+
argument_composer = ArgumentComposer()
968+
self.assertTrue(argument_composer.temp_folder is not None)
969+
self.assertEqual(argument_composer.temp_folder, os.path.abspath(os.path.join(os.path.curdir, "scratch", "tmp")))
970+
971+
# delete temp content
972+
argument_composer.operation = Constants.ASSESSMENT
973+
runtime = RuntimeCompositor(argument_composer.get_composed_arguments(), True, Constants.APT)
974+
runtime.set_legacy_test_type('HappyPath')
975+
CoreMain(argument_composer.get_composed_arguments())
976+
977+
# validate files are deleted
978+
self.assertTrue(argument_composer.temp_folder is not None)
979+
files_matched = glob.glob(str(argument_composer.temp_folder) + "/" + str(Constants.TEMP_FOLDER_CLEANUP_ARTIFACT_LIST))
980+
self.assertTrue(len(files_matched) == 0)
981+
runtime.stop()
982+
983+
def test_delete_temp_folder_contents_when_none_exists(self):
984+
argument_composer = ArgumentComposer()
985+
argument_composer.operation = Constants.ASSESSMENT
986+
runtime = RuntimeCompositor(argument_composer.get_composed_arguments(), True, Constants.APT)
987+
shutil.rmtree(runtime.execution_config.temp_folder)
988+
989+
# attempt to delete temp content
990+
runtime.env_layer.file_system.delete_files_from_dir(runtime.execution_config.temp_folder, Constants.TEMP_FOLDER_CLEANUP_ARTIFACT_LIST)
991+
992+
# validate files are deleted
993+
self.assertTrue(runtime.execution_config.temp_folder is not None)
994+
files_matched = glob.glob(str(runtime.execution_config.temp_folder) + "/" + str(Constants.TEMP_FOLDER_CLEANUP_ARTIFACT_LIST))
995+
self.assertTrue(len(files_matched) == 0)
996+
runtime.stop()
997+
998+
def test_delete_temp_folder_contents_failure(self):
999+
argument_composer = ArgumentComposer()
1000+
self.assertTrue(argument_composer.temp_folder is not None)
1001+
self.assertEqual(argument_composer.temp_folder, os.path.abspath(os.path.join(os.path.curdir, "scratch", "tmp")))
1002+
1003+
# mock os.remove()
1004+
self.backup_os_remove = os.remove
1005+
os.remove = self.mock_os_remove
1006+
1007+
argument_composer.operation = Constants.ASSESSMENT
1008+
runtime = RuntimeCompositor(argument_composer.get_composed_arguments(), True, Constants.APT)
1009+
1010+
# delete temp content attempt #1, throws exception
1011+
self.assertRaises(Exception, lambda: runtime.env_layer.file_system.delete_files_from_dir(runtime.execution_config.temp_folder, Constants.TEMP_FOLDER_CLEANUP_ARTIFACT_LIST, raise_if_delete_failed=True))
1012+
self.assertTrue(os.path.isfile(os.path.join(runtime.execution_config.temp_folder, "temp1.list")))
1013+
1014+
# delete temp content attempt #2, does not throws exception
1015+
runtime.env_layer.file_system.delete_files_from_dir(runtime.execution_config.temp_folder, Constants.TEMP_FOLDER_CLEANUP_ARTIFACT_LIST)
1016+
self.assertTrue(os.path.isfile(os.path.join(runtime.execution_config.temp_folder, "temp1.list")))
1017+
1018+
# reset os.remove() mock
1019+
os.remove = self.backup_os_remove
1020+
runtime.stop()
1021+
9221022
def __check_telemetry_events(self, runtime):
9231023
all_events = os.listdir(runtime.telemetry_writer.events_folder_path)
9241024
self.assertTrue(len(all_events) > 0)

src/core/tests/library/ArgumentComposer.py

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -31,14 +31,22 @@ def __init__(self):
3131
self.__TESTS_FOLDER = "tests"
3232
self.__SCRATCH_FOLDER = "scratch"
3333
self.__ARG_TEMPLATE = "{0} {1} {2} {3} \'{4}\' {5} \'{6}\' {7} {8}"
34+
self.__CONFIG_FOLDER = "config"
35+
self.__STATUS_FOLDER = "status"
36+
self.__LOG_FOLDER = "log"
3437
self.__EVENTS_FOLDER = "events"
38+
self.__TEMP_FOLDER = "tmp"
3539

3640
# sequence number
3741
self.sequence_number = 1
3842

3943
# environment settings
40-
self.__log_folder = self.__config_folder = self.__status_folder = self.__get_scratch_folder()
41-
self.events_folder = self.__get_events_folder(self.__log_folder)
44+
scratch_folder = self.__get_scratch_folder()
45+
self.__log_folder = self.__get_custom_folder(scratch_folder, self.__LOG_FOLDER)
46+
self.__config_folder = self.__get_custom_folder(scratch_folder, self.__CONFIG_FOLDER)
47+
self.__status_folder = self.__get_custom_folder(scratch_folder, self.__STATUS_FOLDER)
48+
self.events_folder = self.__get_custom_folder(self.__log_folder, self.__EVENTS_FOLDER)
49+
self.temp_folder = self.__get_custom_folder(scratch_folder, self.__TEMP_FOLDER)
4250

4351
# config settings
4452
self.operation = Constants.INSTALLATION
@@ -67,6 +75,7 @@ def get_composed_arguments(self, env_settings={}):
6775
"configFolder": self.__config_folder,
6876
"statusFolder": self.__status_folder,
6977
"eventsFolder": self.events_folder,
78+
"tempFolder": self.temp_folder,
7079
"telemetrySupported": True
7180
}
7281

@@ -111,14 +120,15 @@ def __get_scratch_folder(self):
111120
os.mkdir(scratch_folder)
112121
return scratch_folder
113122

114-
def __get_events_folder(self, scratch_folder):
115-
""" Returns a predetermined events folder and guarantees it exists and is empty. """
116-
events_folder = os.path.join(scratch_folder, self.__EVENTS_FOLDER)
117-
if os.path.exists(events_folder):
118-
shutil.rmtree(events_folder, ignore_errors=True)
119-
if not os.path.exists(events_folder):
120-
os.mkdir(events_folder)
121-
return events_folder
123+
@staticmethod
124+
def __get_custom_folder(par_dir, custom_folder_name):
125+
""" Returns a predetermined custom folder, and guarantees it exists and is empty. """
126+
custom_folder = os.path.join(par_dir, custom_folder_name)
127+
if os.path.exists(custom_folder):
128+
shutil.rmtree(custom_folder, ignore_errors=True)
129+
if not os.path.exists(custom_folder):
130+
os.mkdir(custom_folder)
131+
return custom_folder
122132

123133
def __try_get_tests_folder(self, path=os.getcwd()):
124134
""" Returns the current working directory if there's no folder with tests in its name in the absolute path

0 commit comments

Comments
 (0)