Skip to content

Commit d812d4a

Browse files
authored
Auto-assessment (#75)
1 parent 76ef8c3 commit d812d4a

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

60 files changed

+940
-227
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,3 +135,4 @@ dmypy.json
135135

136136
# Linux Patch Extension specific
137137
/src/core/tests/scratch/
138+
/src/extension/tests/MsftLinuxPatchAutoAssess.sh

CONTRIBUTING.md

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,5 +76,3 @@ Ubuntu Server | 16.04-LTS, 18.04-LTS, 20.04-LTS
7676
Red Hat Enterprise Linux | 6 (x86/x64), 7 (x64), 8 (x64)
7777
CentOS | 6 (x86/x64), 7 (x64), 8 (x64)
7878
SUSE Linux Enterprise Server | 11 (x86/x64), 12 (x64), 15 (x64)
79-
80-
NOTE: Code is pending validation on Ubuntu Server 20.04-LTS, Red Hat Enterprise Linux 8 (x64), CentOS 8 (x64), SUSE Linux Enterprise Server 15 (x64), but will be supported when done.

README.md

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ and resiliency standards are met.
1313

1414
Configurations expected in the request:
1515

16-
* `operation`: the operation expected to occur (Assessment/Installation/NoOperation).
16+
* `operation`: the operation expected to occur (Assessment/Installation/ConfigurePatching/NoOperation).
1717
NoOperation can be used to cancel an ongoing assess or patch operation. (**required for all 3 operations**)
1818
* `activityId`: GUID used to track the operation end to end (**required for all 3 operations**)
1919
* `startTime`: the expected start time of the operation in UTC (**required for all 3 operations**)
@@ -25,20 +25,27 @@ NoOperation can be used to cancel an ongoing assess or patch operation. (**requi
2525
(**optional for all operations**)
2626
* `patchesToExclude`: packages to exclude during the operation. Package names and versions are supported (both with wildcards)
2727
(**optional for all operations**)
28-
* `patchMode`: is a setting that will be used to configure the automatic update by OS. Acceptable values: "ImageDefault" or "AutomaticByPlatform"
28+
* `patchMode`: is the setting used to configure the automatic update by OS. Acceptable values: "ImageDefault" or "AutomaticByPlatform"
29+
* `assessmentMode`: is the setting used to configure automatic assessment if desired. Acceptable values: "ImageDefault" or "AutomaticByPlatform"
30+
* `maximumAssessmentInterval`: the expected maximum time interval expected between automatic assessments, if configured.
2931

3032
> Example:
3133
>
3234
> ```json
3335
> {
3436
> "operation": "Assessment",
35-
> "activityId": "1612-2327-1334-23245-32112",
36-
> "startTime": "2019-09-26T23:37:14Z",
37+
> "activityId": "def820db-ec3c-4ecd-9d6c-cb95e6fd5231",
38+
> "startTime": "2021-08-10T23:37:14Z",
3739
> "maximumDuration": "PT2H",
3840
> "rebootSetting": "IfRequired",
3941
> "classificationsToInclude":["Critical","Security"],
4042
> "patchesToInclude": ["mysql-server", "snapd"],
41-
> "patchesToExclude": ["kernel*"]
43+
> "patchesToExclude": ["kernel*"],
44+
> "internalSettings": "test",
45+
> "maintenanceRunId": "2021-08-10T12:12:14Z",
46+
> "patchMode": "AutomaticByPlatform",
47+
> "assessmentMode": "AutomaticByPlatform",
48+
> "maximumAssessmentInterval": "PT3H"
4249
> }
4350
> ```
4451
@@ -56,11 +63,11 @@ and add this file into the configFolder path from HandlerEnvironment.json
5663
5764
## 3. Troubleshooting
5865
59-
Within Azure VM, you can find logs/config files at these locations:
66+
Within your Azure VM, you can find logs/config files at these locations:
6067
6168
* Agent log: `/var/log/waagent.log`
62-
* Extension logs: `under /var/log/azure/Microsoft.CPlat.Core.Edp.LinuxPatchExtension/`
63-
* Other Extension files (such as status blob, config file, etc): `/var/lib/waagent/Microsoft.CPlat.Core.Edp.LinuxPatchExtension-<version>/`
69+
* Extension logs: `under /var/log/azure/Microsoft.CPlat.Core.LinuxPatchExtension/`
70+
* Other Extension files (such as status blob, config file, etc): `/var/lib/waagent/Microsoft.CPlat.Core.LinuxPatchExtension-<version>/`
6471
6572
Please open an issue on this GitHub repository if you encounter problems that
6673
you could not debug with these log files.

src/core/src/CoreMain.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,6 @@ def __init__(self, argv):
3333
patch_operation_requested = Constants.UNKNOWN
3434
configure_patching_successful = False
3535
patch_assessment_successful = False
36-
patch_installation_successful = False
3736
overall_patch_installation_operation_successful = False
3837

3938
try:
@@ -65,9 +64,11 @@ def __init__(self, argv):
6564
package_manager = container.get('package_manager')
6665
configure_patching_processor = container.get('configure_patching_processor')
6766

68-
configure_patching_successful = configure_patching_processor.start_configure_patching()
67+
# Configure patching always runs first, except if it's AUTO_ASSESSMENT
68+
if patch_operation_requested != Constants.AUTO_ASSESSMENT.lower():
69+
configure_patching_successful = configure_patching_processor.start_configure_patching()
6970

70-
# Assessment happens if operation requested is not Configure Patching
71+
# Assessment happens if operation requested is not Configure Patching [i.e. AUTO_ASSESSMENT, ASSESSMENT, INSTALLATION]
7172
if patch_operation_requested != Constants.CONFIGURE_PATCHING.lower():
7273
patch_assessment_successful = patch_assessor.start_assessment()
7374

src/core/src/bootstrap/Bootstrapper.py

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ def __init__(self, argv, capture_stdout=True):
4343
self.env_layer = self.container.get('env_layer')
4444

4545
# Logging initializations
46+
self.reset_auto_assessment_log_file_if_needed()
4647
self.file_logger = self.container.get('file_logger')
4748
if capture_stdout:
4849
self.stdout_file_mirror = StdOutFileMirror(self.env_layer, self.file_logger)
@@ -67,11 +68,18 @@ def get_path_to_log_files_and_telemetry_dir(self, argv):
6768
sequence_number = self.get_value_from_argv(argv, Constants.ARG_SEQUENCE_NUMBER)
6869
environment_settings = json.loads(base64.b64decode(self.get_value_from_argv(argv, Constants.ARG_ENVIRONMENT_SETTINGS).replace("b\'", "")))
6970
log_folder = environment_settings[Constants.EnvSettings.LOG_FOLDER] # can throw exception and that's okay (since we can't recover from this)
70-
log_file_path = os.path.join(log_folder, str(sequence_number) + ".core.log")
71-
real_rec_path = os.path.join(log_folder, str(sequence_number) + ".core.rec")
71+
exec_demarcator = ".aa" if bool(self.get_value_from_argv(argv, Constants.ARG_AUTO_ASSESS_ONLY, False)) else ""
72+
log_file_path = os.path.join(log_folder, str(sequence_number) + exec_demarcator + ".core.log")
73+
real_rec_path = os.path.join(log_folder, str(sequence_number) + exec_demarcator + ".core.rec")
7274
events_folder = environment_settings[Constants.EnvSettings.EVENTS_FOLDER] # can throw exception and that's okay (since we can't recover from this)
7375
return log_file_path, real_rec_path, events_folder
7476

77+
def reset_auto_assessment_log_file_if_needed(self):
78+
""" Deletes the auto assessment log file when needed to prevent excessive growth """
79+
if bool(self.get_value_from_argv(self.argv, Constants.ARG_AUTO_ASSESS_ONLY, False)) and os.path.exists(self.log_file_path) \
80+
and os.path.getsize(self.log_file_path) > Constants.MAX_AUTO_ASSESSMENT_LOGFILE_SIZE_IN_BYTES:
81+
os.remove(self.log_file_path)
82+
7583
def get_recorder_emulator_flags(self, argv):
7684
""" Determines if the recorder or emulator flags need to be changed from the defaults """
7785
recorder_enabled = False
@@ -84,13 +92,17 @@ def get_recorder_emulator_flags(self, argv):
8492
return recorder_enabled, emulator_enabled
8593

8694
@staticmethod
87-
def get_value_from_argv(argv, key):
95+
def get_value_from_argv(argv, key, default_value=Constants.DEFAULT_UNSPECIFIED_VALUE):
8896
""" Discovers the value assigned to a given key based on the core contract on arguments """
8997
for x in range(1, len(argv)):
9098
if x % 2 == 1: # key checker
9199
if str(argv[x]).lower() == key.lower() and x < len(argv):
92100
return str(argv[x+1])
93-
raise Exception("Unable to find key {0} in core arguments: {1}.".format(key, str(argv)))
101+
102+
if default_value == Constants.DEFAULT_UNSPECIFIED_VALUE:
103+
raise Exception("Unable to find key {0} in core arguments: {1}.".format(key, str(argv)))
104+
else:
105+
return default_value
94106

95107
def build_out_container(self):
96108
# First output in a positive bootstrap
@@ -152,7 +164,7 @@ def check_sudo_status(self, raise_if_not_sudo=True):
152164
raise Exception("Unexpected sudo check result. Output: " + " ".join(output.split("\n")))
153165
except Exception as exception:
154166
self.composite_logger.log_error("Sudo status check failed. Please ensure the computer is configured correctly for sudo invocation. " +
155-
"Exception details: " + str(exception))
167+
"Exception details: " + str(exception))
156168
if raise_if_not_sudo:
157169
raise
158170

src/core/src/bootstrap/ConfigurationFactory.py

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@
2828
from core.src.core_logic.PatchAssessor import PatchAssessor
2929
from core.src.core_logic.PatchInstaller import PatchInstaller
3030

31+
from core.src.core_logic.ServiceManager import ServiceManager
32+
from core.src.core_logic.ServiceManager import ServiceInfo
33+
from core.src.core_logic.TimerManager import TimerManager
34+
3135
from core.src.local_loggers.FileLogger import FileLogger
3236
from core.src.local_loggers.CompositeLogger import CompositeLogger
3337

@@ -191,9 +195,28 @@ def new_prod_configuration(self, package_manager_name, package_manager_component
191195
'component_args': ['env_layer', 'execution_config', 'composite_logger', 'telemetry_writer', 'status_handler', 'lifecycle_manager', 'package_manager', 'package_filter', 'maintenance_window', 'reboot_manager'],
192196
'component_kwargs': {}
193197
},
198+
'service_info': {
199+
'component': ServiceInfo,
200+
'component_args': [],
201+
'component_kwargs': {
202+
'service_name': Constants.AUTO_ASSESSMENT_SERVICE_NAME,
203+
'service_desc': Constants.AUTO_ASSESSMENT_SERVICE_DESC,
204+
'service_exec_path': os.path.join(os.path.dirname(os.path.realpath(__file__)), Constants.CORE_AUTO_ASSESS_SH_FILE_NAME)
205+
}
206+
},
207+
'auto_assess_service_manager': {
208+
'component': ServiceManager,
209+
'component_args': ['env_layer', 'execution_config', 'composite_logger', 'telemetry_writer', 'service_info'],
210+
'component_kwargs': {}
211+
},
212+
'auto_assess_timer_manager': {
213+
'component': TimerManager,
214+
'component_args': ['env_layer', 'execution_config', 'composite_logger', 'telemetry_writer', 'service_info'],
215+
'component_kwargs': {}
216+
},
194217
'configure_patching_processor': {
195218
'component': ConfigurePatchingProcessor,
196-
'component_args': ['env_layer', 'execution_config', 'composite_logger', 'telemetry_writer', 'status_handler', 'package_manager'],
219+
'component_args': ['env_layer', 'execution_config', 'composite_logger', 'telemetry_writer', 'status_handler', 'package_manager', 'auto_assess_service_manager', 'auto_assess_timer_manager'],
197220
'component_kwargs': {}
198221
},
199222
'maintenance_window': {

src/core/src/bootstrap/Constants.py

Lines changed: 35 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -42,10 +42,17 @@ def __iter__(self):
4242
ARG_SEQUENCE_NUMBER = '-sequenceNumber'
4343
ARG_ENVIRONMENT_SETTINGS = "-environmentSettings"
4444
ARG_CONFIG_SETTINGS = "-configSettings"
45+
ARG_AUTO_ASSESS_ONLY = "-autoAssessOnly"
4546
ARG_PROTECTED_CONFIG_SETTINGS = "-protectedConfigSettings"
4647
ARG_INTERNAL_RECORDER_ENABLED = "-recorderEnabled"
4748
ARG_INTERNAL_EMULATOR_ENABLED = "-emulatorEnabled"
4849

50+
# Max values
51+
MAX_AUTO_ASSESSMENT_LOGFILE_SIZE_IN_BYTES = 5*1024*1024
52+
53+
class Paths(EnumBackport):
54+
SYSTEMD_ROOT = "etc/systemd/system/"
55+
4956
class EnvSettings(EnumBackport):
5057
LOG_FOLDER = "logFolder"
5158
CONFIG_FOLDER = "configFolder"
@@ -63,14 +70,23 @@ class ConfigSettings(EnumBackport):
6370
PATCHES_TO_EXCLUDE = 'patchesToExclude'
6471
MAINTENANCE_RUN_ID = 'maintenanceRunId'
6572
PATCH_MODE = 'patchMode'
73+
ASSESSMENT_MODE = 'assessmentMode'
74+
MAXIMUM_ASSESSMENT_INTERVAL = 'maximumAssessmentInterval'
6675

6776
# File to save default settings for auto OS updates
6877
IMAGE_DEFAULT_PATCH_CONFIGURATION_BACKUP_PATH = "ImageDefaultPatchConfiguration.bak"
6978

79+
# Auto assessment shell script name
80+
CORE_AUTO_ASSESS_SH_FILE_NAME = "MsftLinuxPatchAutoAssess.sh"
81+
AUTO_ASSESSMENT_SERVICE_NAME = "MsftLinuxPatchAutoAssess"
82+
AUTO_ASSESSMENT_SERVICE_DESC = "Microsoft Azure Linux Patch Extension - Auto Assessment"
83+
7084
# Operations
85+
AUTO_ASSESSMENT = 'AutoAssessment'
7186
ASSESSMENT = "Assessment"
7287
INSTALLATION = "Installation"
7388
CONFIGURE_PATCHING = "ConfigurePatching"
89+
CONFIGURE_PATCHING_AUTO_ASSESSMENT = "ConfigurePatching_AutoAssessment" # only used internally
7490
PATCH_ASSESSMENT_SUMMARY = "PatchAssessmentSummary"
7591
PATCH_INSTALLATION_SUMMARY = "PatchInstallationSummary"
7692
PATCH_METADATA_FOR_HEALTHSTORE = "PatchMetadataForHealthStore"
@@ -80,16 +96,30 @@ class ConfigSettings(EnumBackport):
8096
PATCH_VERSION_UNKNOWN = "UNKNOWN"
8197

8298
# Patch Modes for Configure Patching
83-
IMAGE_DEFAULT = "ImageDefault"
84-
AUTOMATIC_BY_PLATFORM = "AutomaticByPlatform"
99+
class PatchModes(EnumBackport):
100+
IMAGE_DEFAULT = "ImageDefault"
101+
AUTOMATIC_BY_PLATFORM = "AutomaticByPlatform"
102+
103+
class AssessmentModes(EnumBackport):
104+
IMAGE_DEFAULT = "ImageDefault"
105+
AUTOMATIC_BY_PLATFORM = "AutomaticByPlatform"
85106

86107
# automatic OS patch states for configure patching
87-
PATCH_STATE_UNKNOWN = "Unknown"
88-
PATCH_STATE_DISABLED = "Disabled"
89-
PATCH_STATE_ENABLED = "Enabled"
108+
class AutomaticOsPatchStates(EnumBackport):
109+
UNKNOWN = "Unknown"
110+
DISABLED = "Disabled"
111+
ENABLED = "Enabled"
112+
113+
# auto assessment states
114+
class AutoAssessmentStates(EnumBackport):
115+
UNKNOWN = "Unknown"
116+
ERROR = "Error"
117+
DISABLED = "Disabled"
118+
ENABLED = "Enabled"
90119

91120
# wait time after status updates
92121
WAIT_TIME_AFTER_HEALTHSTORE_STATUS_UPDATE_IN_SECS = 20
122+
AUTO_ASSESSMENT_MAXIMUM_DURATION = "PT1H"
93123

94124
# Status file states
95125
STATUS_TRANSITIONING = "Transitioning"
@@ -212,4 +242,3 @@ class EnvLayer(EnumBackport):
212242
PRIVILEGED_OP_MARKER = "Privileged_Op_e6df678d-d09b-436a-a08a-65f2f70a6798"
213243
PRIVILEGED_OP_REBOOT = PRIVILEGED_OP_MARKER + "Reboot_Exception"
214244
PRIVILEGED_OP_EXIT = PRIVILEGED_OP_MARKER + "Exit_"
215-

src/core/src/bootstrap/Container.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ def register(self, component_id, component, *component_args, **component_kwargs)
6565
self.components[component_id] = component, component_args, component_kwargs
6666

6767
def get(self, component_id):
68-
"""Lookups the given property name in context.
68+
"""Looks up the given property name in context.
6969
Raises KeyError when no such property is found.
7070
"""
7171
if component_id not in self.components:

src/core/src/bootstrap/EnvLayer.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -384,7 +384,7 @@ def datetime_utcnow(self):
384384
def timestamp(self):
385385
operation = "DATETIME_TIMESTAMP"
386386
if not self.__emulator_enabled:
387-
value = datetime.datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S")
387+
value = datetime.datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%SZ")
388388
self.__write_record(operation, code=0, output=value, delay=0)
389389
return value
390390
else:
@@ -402,6 +402,11 @@ def total_minutes_from_time_delta(time_delta):
402402
def utc_to_standard_datetime(utc_datetime):
403403
""" Converts string of format '"%Y-%m-%dT%H:%M:%SZ"' to datetime object """
404404
return datetime.datetime.strptime(utc_datetime.split(".")[0], "%Y-%m-%dT%H:%M:%S")
405+
406+
@staticmethod
407+
def standard_datetime_to_utc(std_datetime):
408+
""" Converts datetime object to string of format '"%Y-%m-%dT%H:%M:%SZ"' """
409+
return std_datetime.strftime("%Y-%m-%dT%H:%M:%SZ")
405410
# endregion - DateTime emulator and extensions
406411

407412
# region - Core Emulator support functions

0 commit comments

Comments
 (0)