1# Microsoft Azure Linux Agent 2# 3# Copyright Microsoft Corporation 4# 5# Licensed under the Apache License, Version 2.0 (the "License"); 6# you may not use this file except in compliance with the License. 7# You may obtain a copy of the License at 8# 9# http://www.apache.org/licenses/LICENSE-2.0 10# 11# Unless required by applicable law or agreed to in writing, software 12# distributed under the License is distributed on an "AS IS" BASIS, 13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14# See the License for the specific language governing permissions and 15# limitations under the License. 16# 17# Requires Python 2.6+ and Openssl 1.0+ 18# 19 20import datetime 21import glob 22import json 23import operator 24import os 25import random 26import re 27import shutil 28import stat 29import sys 30import tempfile 31import time 32import traceback 33import zipfile 34 35import azurelinuxagent.common.conf as conf 36import azurelinuxagent.common.logger as logger 37import azurelinuxagent.common.utils.fileutil as fileutil 38import azurelinuxagent.common.version as version 39from azurelinuxagent.common.agent_supported_feature import get_agent_supported_features_list_for_extensions, \ 40 SupportedFeatureNames, get_supported_feature_by_name 41from azurelinuxagent.common.cgroupconfigurator import CGroupConfigurator 42from azurelinuxagent.common.datacontract import get_properties, set_properties 43from azurelinuxagent.common.errorstate import ErrorState 44from azurelinuxagent.common.event import add_event, elapsed_milliseconds, report_event, WALAEventOperation, \ 45 add_periodic, EVENTS_DIRECTORY 46from azurelinuxagent.common.exception import ExtensionDownloadError, ExtensionError, ExtensionErrorCodes, \ 47 ExtensionOperationError, ExtensionUpdateError, ProtocolError, ProtocolNotFoundError, ExtensionConfigError 48from azurelinuxagent.common.future import ustr, is_file_not_found_error 49from azurelinuxagent.common.protocol.restapi import ExtensionStatus, ExtensionSubStatus, ExtHandler, ExtHandlerStatus, \ 50 VMStatus 51from azurelinuxagent.common.utils.flexible_version import FlexibleVersion 52from azurelinuxagent.common.version import AGENT_NAME, CURRENT_VERSION, DISTRO_NAME, DISTRO_VERSION, \ 53 GOAL_STATE_AGENT_VERSION, PY_VERSION_MAJOR, PY_VERSION_MICRO, PY_VERSION_MINOR 54 55_HANDLER_NAME_PATTERN = r'^([^-]+)' 56_HANDLER_VERSION_PATTERN = r'(\d+(?:\.\d+)*)' 57_HANDLER_PATTERN = _HANDLER_NAME_PATTERN + r"-" + _HANDLER_VERSION_PATTERN 58_HANDLER_PKG_PATTERN = re.compile(_HANDLER_PATTERN + r'\.zip$', re.IGNORECASE) 59_DEFAULT_EXT_TIMEOUT_MINUTES = 90 60 61_VALID_HANDLER_STATUS = ['Ready', 'NotReady', "Installing", "Unresponsive"] 62 63HANDLER_NAME_PATTERN = re.compile(_HANDLER_NAME_PATTERN, re.IGNORECASE) 64HANDLER_COMPLETE_NAME_PATTERN = re.compile(_HANDLER_PATTERN + r'$', re.IGNORECASE) 65HANDLER_PKG_EXT = ".zip" 66 67AGENT_STATUS_FILE = "waagent_status.json" 68NUMBER_OF_DOWNLOAD_RETRIES = 5 69 70# This is the default value for the env variables, whenever we call a command which is not an update scenario, we 71# set the env variable value to NOT_RUN to reduce ambiguity for the extension publishers 72NOT_RUN = "NOT_RUN" 73 74# Max size of individual status file 75_MAX_STATUS_FILE_SIZE_IN_BYTES = 128 * 1024 # 128K 76 77# Truncating length of fields. 78_MAX_STATUS_MESSAGE_LENGTH = 1024 # 1k message allowed to be shown in the portal. 79_MAX_SUBSTATUS_FIELD_LENGTH = 10 * 1024 # Making 10K; allowing fields to have enough debugging information.. 80_TRUNCATED_SUFFIX = u" ... [TRUNCATED]" 81 82# Status file specific retries and delays. 83_NUM_OF_STATUS_FILE_RETRIES = 5 84_STATUS_FILE_RETRY_DELAY = 2 # seconds 85 86 87class ValidHandlerStatus(object): 88 transitioning = "transitioning" 89 warning = "warning" 90 error = "error" 91 success = "success" 92 STRINGS = ['transitioning', 'warning', 'error', 'success'] 93 94 95_EXTENSION_TERMINAL_STATUSES = [ValidHandlerStatus.error, ValidHandlerStatus.success] 96 97 98class ExtCommandEnvVariable(object): 99 Prefix = "AZURE_GUEST_AGENT" 100 DisableReturnCode = "{0}_DISABLE_CMD_EXIT_CODE".format(Prefix) 101 UninstallReturnCode = "{0}_UNINSTALL_CMD_EXIT_CODE".format(Prefix) 102 ExtensionPath = "{0}_EXTENSION_PATH".format(Prefix) 103 ExtensionVersion = "{0}_EXTENSION_VERSION".format(Prefix) 104 ExtensionSeqNumber = "ConfigSequenceNumber" # At par with Windows Guest Agent 105 UpdatingFromVersion = "{0}_UPDATING_FROM_VERSION".format(Prefix) 106 WireProtocolAddress = "{0}_WIRE_PROTOCOL_ADDRESS".format(Prefix) 107 ExtensionSupportedFeatures = "{0}_EXTENSION_SUPPORTED_FEATURES".format(Prefix) 108 109 110def get_traceback(e): # pylint: disable=R1710 111 if sys.version_info[0] == 3: # pylint: disable=R1705 112 return e.__traceback__ 113 elif sys.version_info[0] == 2: 114 ex_type, ex, tb = sys.exc_info() # pylint: disable=W0612 115 return tb 116 117 118def validate_has_key(obj, key, full_key_path): 119 if key not in obj: 120 raise ExtensionStatusError(msg="Invalid status format by extension: Missing {0} key".format(full_key_path), 121 code=ExtensionStatusError.StatusFileMalformed) 122 123 124def validate_in_range(val, valid_range, name): 125 if val not in valid_range: 126 raise ExtensionStatusError(msg="Invalid value {0} in range {1} at the node {2}".format(val, valid_range, name), 127 code=ExtensionStatusError.StatusFileMalformed) 128 129 130def parse_formatted_message(formatted_message): 131 if formatted_message is None: 132 return None 133 validate_has_key(formatted_message, 'lang', 'formattedMessage/lang') 134 validate_has_key(formatted_message, 'message', 'formattedMessage/message') 135 return formatted_message.get('message') 136 137 138def parse_ext_substatus(substatus): 139 # Check extension sub status format 140 validate_has_key(substatus, 'status', 'substatus/status') 141 validate_in_range(substatus['status'], ValidHandlerStatus.STRINGS, 'substatus/status') 142 status = ExtensionSubStatus() 143 status.name = substatus.get('name') 144 status.status = substatus.get('status') 145 status.code = substatus.get('code', 0) 146 formatted_message = substatus.get('formattedMessage') 147 status.message = parse_formatted_message(formatted_message) 148 return status 149 150 151def parse_ext_status(ext_status, data): 152 if data is None: 153 return 154 if not isinstance(data, list): 155 data_string = ustr(data)[:4096] 156 raise ExtensionStatusError(msg="The extension status must be an array: {0}".format(data_string), code=ExtensionStatusError.StatusFileMalformed) 157 if not data: 158 return 159 160 # Currently, only the first status will be reported 161 data = data[0] 162 # Check extension status format 163 validate_has_key(data, 'status', 'status') 164 status_data = data['status'] 165 validate_has_key(status_data, 'status', 'status/status') 166 167 status = status_data['status'] 168 if status not in ValidHandlerStatus.STRINGS: 169 status = ValidHandlerStatus.error 170 171 applied_time = status_data.get('configurationAppliedTime') 172 ext_status.configurationAppliedTime = applied_time 173 ext_status.operation = status_data.get('operation') 174 ext_status.status = status 175 ext_status.code = status_data.get('code', 0) 176 formatted_message = status_data.get('formattedMessage') 177 ext_status.message = parse_formatted_message(formatted_message) 178 substatus_list = status_data.get('substatus', []) 179 # some extensions incorrectly report an empty substatus with a null value 180 if substatus_list is None: 181 substatus_list = [] 182 for substatus in substatus_list: 183 if substatus is not None: 184 ext_status.substatusList.append(parse_ext_substatus(substatus)) 185 186 187def migrate_handler_state(): 188 """ 189 Migrate handler state and status (if they exist) from an agent-owned directory into the 190 handler-owned config directory 191 192 Notes: 193 - The v2.0.x branch wrote all handler-related state into the handler-owned config 194 directory (e.g., /var/lib/waagent/Microsoft.Azure.Extensions.LinuxAsm-2.0.1/config). 195 - The v2.1.x branch original moved that state into an agent-owned handler 196 state directory (e.g., /var/lib/waagent/handler_state). 197 - This move can cause v2.1.x agents to multiply invoke a handler's install command. It also makes 198 clean-up more difficult since the agent must remove the state as well as the handler directory. 199 """ 200 handler_state_path = os.path.join(conf.get_lib_dir(), "handler_state") 201 if not os.path.isdir(handler_state_path): 202 return 203 204 for handler_path in glob.iglob(os.path.join(handler_state_path, "*")): 205 handler = os.path.basename(handler_path) 206 handler_config_path = os.path.join(conf.get_lib_dir(), handler, "config") 207 if os.path.isdir(handler_config_path): 208 for file in ("State", "Status"): # pylint: disable=redefined-builtin 209 from_path = os.path.join(handler_state_path, handler, file.lower()) 210 to_path = os.path.join(handler_config_path, "Handler" + file) 211 if os.path.isfile(from_path) and not os.path.isfile(to_path): 212 try: 213 shutil.move(from_path, to_path) 214 except Exception as e: 215 logger.warn( 216 "Exception occurred migrating {0} {1} file: {2}", 217 handler, 218 file, 219 str(e)) 220 221 try: 222 shutil.rmtree(handler_state_path) 223 except Exception as e: 224 logger.warn("Exception occurred removing {0}: {1}", handler_state_path, str(e)) 225 return 226 227 228class ExtHandlerState(object): 229 NotInstalled = "NotInstalled" 230 Installed = "Installed" 231 Enabled = "Enabled" 232 FailedUpgrade = "FailedUpgrade" 233 234 235class ExtensionRequestedState(object): 236 """ 237 This is the state of the Extension as requested by the Goal State. 238 CRP only supports 2 states as of now - Enabled and Uninstall 239 Disabled was used for older XML extensions and we keep it to support backward compatibility. 240 """ 241 Enabled = u"enabled" 242 Disabled = u"disabled" 243 Uninstall = u"uninstall" 244 245 246def get_exthandlers_handler(protocol): 247 return ExtHandlersHandler(protocol) 248 249 250def list_agent_lib_directory(skip_agent_package=True): 251 lib_dir = conf.get_lib_dir() 252 for name in os.listdir(lib_dir): 253 path = os.path.join(lib_dir, name) 254 255 if skip_agent_package and (version.is_agent_package(path) or version.is_agent_path(path)): 256 continue 257 258 yield name, path 259 260 261class ExtHandlersHandler(object): 262 def __init__(self, protocol): 263 self.protocol = protocol 264 self.ext_handlers = None 265 self.last_etag = None 266 self.log_report = False 267 self.log_process = False 268 269 self.report_status_error_state = ErrorState() 270 271 def _incarnation_changed(self, etag): 272 # Skip processing if GoalState incarnation did not change 273 return self.last_etag != etag 274 275 def get_goal_state_debug_metadata(self): 276 """ 277 This function fetches metadata fetched from the GoalState for better debuggability 278 :return: Tuple (activity_id, correlation_id, gs_created_timestamp) or "NA" for any property that's not available 279 """ 280 281 def format_value(parse_fn, value): 282 283 try: 284 if value not in (None, ""): 285 return parse_fn(value) 286 except Exception as e: 287 # A failure here isn't a fatal error, because the info we're 288 # trying to retrieve is debug only on linux. 289 error_msg = u"Couldn't parse debug metadata value: {0}".format(e) 290 logger.verbose(error_msg) 291 292 return "NA" 293 294 to_utc = lambda time: time.strftime(logger.Logger.LogTimeFormatInUTC) 295 identity = lambda value: value 296 297 in_vm_gs_metadata = self.protocol.get_in_vm_gs_metadata() 298 299 gs_creation_time = format_value(to_utc, in_vm_gs_metadata.created_on_ticks) 300 activity_id = format_value(identity, in_vm_gs_metadata.activity_id) 301 correlation_id = format_value(identity, in_vm_gs_metadata.correlation_id) 302 303 return activity_id, correlation_id, gs_creation_time 304 305 def run(self): 306 307 try: 308 self.ext_handlers, etag = self.protocol.get_ext_handlers() 309 msg = u"Handle extensions updates for incarnation {0}".format(etag) 310 logger.verbose(msg) 311 # Log status report success on new config 312 self.log_report = True 313 314 if self._extension_processing_allowed() and self._incarnation_changed(etag): 315 activity_id, correlation_id, gs_creation_time = self.get_goal_state_debug_metadata() 316 317 logger.info( 318 "ProcessGoalState started [Incarnation: {0}; Activity Id: {1}; Correlation Id: {2}; GS Creation Time: {3}]".format( 319 etag, activity_id, correlation_id, gs_creation_time)) 320 self.handle_ext_handlers(etag) 321 self.last_etag = etag 322 323 self.report_ext_handlers_status() 324 self._cleanup_outdated_handlers() 325 except Exception as error: 326 msg = u"Exception processing extension handlers: {0}".format(ustr(error)) 327 detailed_msg = '{0} {1}'.format(msg, traceback.extract_tb(get_traceback(error))) 328 logger.warn(msg) 329 add_event(AGENT_NAME, 330 version=CURRENT_VERSION, 331 op=WALAEventOperation.ExtensionProcessing, 332 is_success=False, 333 message=detailed_msg) 334 return 335 336 @staticmethod 337 def get_ext_handler_instance_from_path(name, path, protocol, skip_handlers=None): 338 if not os.path.isdir(path) or re.match(HANDLER_NAME_PATTERN, name) is None: 339 return None 340 separator = name.rfind('-') 341 handler_name = name[0:separator] 342 if skip_handlers is not None and handler_name in skip_handlers: 343 # Handler in skip_handlers list, not parsing it 344 return None 345 346 eh = ExtHandler(name=handler_name) 347 eh.properties.version = str(FlexibleVersion(name[separator + 1:])) 348 349 return ExtHandlerInstance(eh, protocol) 350 351 def _cleanup_outdated_handlers(self): 352 handlers = [] 353 pkgs = [] 354 ext_handlers_in_gs = [ext_handler.name for ext_handler in self.ext_handlers.extHandlers] 355 356 # Build a collection of uninstalled handlers and orphaned packages 357 # Note: 358 # -- An orphaned package is one without a corresponding handler 359 # directory 360 361 for item, path in list_agent_lib_directory(skip_agent_package=True): 362 try: 363 handler_instance = ExtHandlersHandler.get_ext_handler_instance_from_path(name=item, 364 path=path, 365 protocol=self.protocol, 366 skip_handlers=ext_handlers_in_gs) 367 if handler_instance is not None: 368 # Since this handler name doesn't exist in the GS, marking it for deletion 369 handlers.append(handler_instance) 370 continue 371 except Exception: 372 continue 373 374 if os.path.isfile(path) and \ 375 not os.path.isdir(path[0:-len(HANDLER_PKG_EXT)]): 376 if not re.match(_HANDLER_PKG_PATTERN, item): 377 continue 378 pkgs.append(path) 379 380 # Then, remove the orphaned packages 381 for pkg in pkgs: 382 try: 383 os.remove(pkg) 384 logger.verbose("Removed orphaned extension package {0}".format(pkg)) 385 except OSError as e: 386 logger.warn("Failed to remove orphaned package {0}: {1}".format(pkg, e.strerror)) 387 388 # Finally, remove the directories and packages of the orphaned handlers, i.e. Any extension directory that 389 # is still in the FileSystem but not in the GoalState 390 for handler in handlers: 391 handler.remove_ext_handler() 392 pkg = os.path.join(conf.get_lib_dir(), handler.get_full_name() + HANDLER_PKG_EXT) 393 if os.path.isfile(pkg): 394 try: 395 os.remove(pkg) 396 logger.verbose("Removed extension package {0}".format(pkg)) 397 except OSError as e: 398 logger.warn("Failed to remove extension package {0}: {1}".format(pkg, e.strerror)) 399 400 def _extension_processing_allowed(self): 401 if not conf.get_extensions_enabled(): 402 logger.verbose("Extension handling is disabled") 403 return False 404 405 if conf.get_enable_overprovisioning(): 406 artifacts_profile = self.protocol.get_artifacts_profile() 407 if artifacts_profile and artifacts_profile.is_on_hold(): 408 logger.info("Extension handling is on hold") 409 return False 410 411 return True 412 413 def handle_ext_handlers(self, etag=None): 414 if not self.ext_handlers.extHandlers: 415 logger.info("No extension handlers found, not processing anything.") 416 return 417 418 wait_until = datetime.datetime.utcnow() + datetime.timedelta(minutes=_DEFAULT_EXT_TIMEOUT_MINUTES) 419 max_dep_level = max([handler.sort_key() for handler in self.ext_handlers.extHandlers]) 420 421 self.ext_handlers.extHandlers.sort(key=operator.methodcaller('sort_key')) 422 for ext_handler in self.ext_handlers.extHandlers: 423 handler_success = self.handle_ext_handler(ext_handler, etag) 424 425 # Wait for the extension installation until it is handled. 426 # This is done for the install and enable. Not for the uninstallation. 427 # If handled successfully, proceed with the current handler. 428 # Otherwise, skip the rest of the extension installation. 429 dep_level = ext_handler.sort_key() 430 if 0 <= dep_level < max_dep_level: 431 432 # Do no wait for extension status if the handler failed 433 if not handler_success: 434 msg = "Handler: {0} processing failed, will skip processing the rest of the extensions".format( 435 ext_handler.name) 436 add_event(AGENT_NAME, 437 version=CURRENT_VERSION, 438 op=WALAEventOperation.ExtensionProcessing, 439 is_success=False, 440 message=msg, 441 log_event=True) 442 break 443 444 if not self.wait_for_handler_completion(ext_handler, wait_until): 445 logger.warn("An extension failed or timed out, will skip processing the rest of the extensions") 446 break 447 448 def wait_for_handler_completion(self, ext_handler, wait_until): 449 """ 450 Check the status of the extension being handled. 451 Wait until it has a terminal state or times out. 452 Return True if it is handled successfully. False if not. 453 """ 454 455 def _report_error_event_and_return_false(error_message): 456 # In case of error, return False so that the processing of dependent extensions can be skipped (fail fast) 457 add_event(AGENT_NAME, 458 version=CURRENT_VERSION, 459 op=WALAEventOperation.ExtensionProcessing, 460 is_success=False, 461 message=error_message, 462 log_event=True) 463 return False 464 465 try: 466 handler_i = ExtHandlerInstance(ext_handler, self.protocol) 467 468 # Loop through all settings of the Handler and verify all extensions reported success status in status file 469 # Currently, we only support 1 extension (runtime-settings) per handler 470 ext_completed, status = False, None 471 for ext in ext_handler.properties.extensions: 472 473 # Keep polling for the extension status until it succeeds or times out 474 while datetime.datetime.utcnow() <= wait_until: 475 ext_completed, status = handler_i.is_ext_handling_complete(ext) 476 if ext_completed: 477 break 478 time.sleep(5) 479 480 # In case of timeout or terminal error state, we log it and return false 481 # Incase extension reported status at the last sec, we should prioritize reporting status over timeout 482 if not ext_completed and datetime.datetime.utcnow() > wait_until: 483 msg = "Extension {0} did not reach a terminal state within the allowed timeout. Last status was {1}".format( 484 ext.name, status) 485 return _report_error_event_and_return_false(msg) 486 487 if status != ValidHandlerStatus.success: 488 msg = "Extension {0} did not succeed. Status was {1}".format(ext.name, status) 489 return _report_error_event_and_return_false(msg) 490 491 except Exception as error: 492 msg = "Failed to wait for Handler completion due to unknown error. Marking the extension as failed: {0}, {1}".format( 493 ustr(error), traceback.format_exc()) 494 return _report_error_event_and_return_false(msg) 495 496 return True 497 498 def handle_ext_handler(self, ext_handler, etag): 499 """ 500 Execute the requested command for the handler and return if success 501 :param ext_handler: The ExtHandler to execute the command on 502 :param etag: Current incarnation of the GoalState 503 :return: True if the operation was successful, False if not 504 """ 505 ext_handler_i = ExtHandlerInstance(ext_handler, self.protocol) 506 try: 507 # Ensure the extension config was valid 508 if ext_handler.is_invalid_setting: 509 raise ExtensionConfigError(ext_handler.invalid_setting_reason) 510 511 state = ext_handler.properties.state 512 513 # The Guest Agent currently only supports 1 installed version per extension on the VM. 514 # If the extension version is unregistered and the customers wants to uninstall the extension, 515 # we should let it go through even if the installed version doesnt exist in Handler manifest (PIR) anymore. 516 # If target state is enabled and version not found in manifest, do not process the extension. 517 if ext_handler_i.decide_version(target_state=state) is None and state == ExtensionRequestedState.Enabled: 518 handler_version = ext_handler_i.ext_handler.properties.version 519 name = ext_handler_i.ext_handler.name 520 err_msg = "Unable to find version {0} in manifest for extension {1}".format(handler_version, name) 521 ext_handler_i.set_operation(WALAEventOperation.Download) 522 ext_handler_i.set_handler_status(message=ustr(err_msg), code=-1) 523 ext_handler_i.report_event(message=ustr(err_msg), is_success=False) 524 return False 525 526 ext_handler_i.logger.info("Target handler state: {0} [incarnation {1}]", state, etag) 527 if state == ExtensionRequestedState.Enabled: 528 self.handle_enable(ext_handler_i) 529 elif state == ExtensionRequestedState.Disabled: 530 self.handle_disable(ext_handler_i) 531 elif state == ExtensionRequestedState.Uninstall: 532 self.handle_uninstall(ext_handler_i) 533 else: 534 message = u"Unknown ext handler state:{0}".format(state) 535 raise ExtensionError(message) 536 537 return True 538 539 except ExtensionConfigError as error: 540 # Catch and report Invalid ExtensionConfig errors here to fail fast rather than timing out after 90 min 541 err_msg = "Ran into config errors: {0}. \nPlease retry again as another operation with updated settings".format( 542 ustr(error)) 543 self.__handle_and_report_ext_handler_errors(ext_handler_i, error, 544 report_op=WALAEventOperation.InvalidExtensionConfig, 545 message=err_msg) 546 except ExtensionUpdateError as error: 547 # Not reporting the error as it has already been reported from the old version 548 self.handle_ext_handler_error(ext_handler_i, error, error.code, report_telemetry_event=False) 549 except ExtensionDownloadError as error: 550 msg = "Failed to download artifacts: {0}".format(ustr(error)) 551 self.__handle_and_report_ext_handler_errors(ext_handler_i, error, report_op=WALAEventOperation.Download, 552 message=msg) 553 except ExtensionError as error: 554 self.handle_ext_handler_error(ext_handler_i, error, error.code) 555 except Exception as error: 556 self.handle_ext_handler_error(ext_handler_i, error) 557 558 return False 559 560 @staticmethod 561 def handle_ext_handler_error(ext_handler_i, error, code=-1, report_telemetry_event=True): 562 msg = ustr(error) 563 ext_handler_i.set_handler_status(message=msg, code=code) 564 565 if report_telemetry_event: 566 ext_handler_i.report_event(message=msg, is_success=False, log_event=True) 567 568 @staticmethod 569 def __handle_and_report_ext_handler_errors(ext_handler_i, error, report_op, message): 570 ext_handler_i.set_handler_status(message=message, code=error.code) 571 report_event(op=report_op, is_success=False, log_event=True, message=message) 572 573 def handle_enable(self, ext_handler_i): 574 self.log_process = True 575 uninstall_exit_code = None 576 old_ext_handler_i = ext_handler_i.get_installed_ext_handler() 577 578 handler_state = ext_handler_i.get_handler_state() 579 ext_handler_i.logger.info("[Enable] current handler state is: {0}", 580 handler_state.lower()) 581 # We go through the entire process of downloading and initializing the extension if it's either a fresh 582 # extension or if it's a retry of a previously failed upgrade. 583 if handler_state == ExtHandlerState.NotInstalled or handler_state == ExtHandlerState.FailedUpgrade: 584 ext_handler_i.set_handler_state(ExtHandlerState.NotInstalled) 585 ext_handler_i.download() 586 ext_handler_i.initialize() 587 ext_handler_i.update_settings() 588 if old_ext_handler_i is None: 589 ext_handler_i.install() 590 elif ext_handler_i.version_ne(old_ext_handler_i): 591 uninstall_exit_code = ExtHandlersHandler._update_extension_handler_and_return_if_failed( 592 old_ext_handler_i, ext_handler_i) 593 else: 594 ext_handler_i.update_settings() 595 596 ext_handler_i.enable(uninstall_exit_code=uninstall_exit_code) 597 598 @staticmethod 599 def _update_extension_handler_and_return_if_failed(old_ext_handler_i, ext_handler_i): 600 601 def execute_old_handler_command_and_return_if_succeeds(func): 602 """ 603 Created a common wrapper to execute all commands that need to be executed from the old handler 604 so that it can have a common exception handling mechanism 605 :param func: The command to be executed on the old handler 606 :return: True if command execution succeeds and False if it fails 607 """ 608 continue_on_update_failure = False 609 exit_code = 0 610 try: 611 continue_on_update_failure = ext_handler_i.load_manifest().is_continue_on_update_failure() 612 func() 613 except ExtensionError as e: 614 # Reporting the event with the old handler and raising a new ExtensionUpdateError to set the 615 # handler status on the new version 616 msg = "%s; ContinueOnUpdate: %s" % (ustr(e), continue_on_update_failure) 617 old_ext_handler_i.report_event(message=msg, is_success=False) 618 if not continue_on_update_failure: 619 raise ExtensionUpdateError(msg) 620 621 exit_code = e.code 622 if isinstance(e, ExtensionOperationError): 623 exit_code = e.exit_code # pylint: disable=E1101 624 625 logger.info("Continue on Update failure flag is set, proceeding with update") 626 return exit_code 627 628 disable_exit_code = NOT_RUN 629 # We only want to disable the old handler if it is currently enabled; no 630 # other state makes sense. 631 if old_ext_handler_i.get_handler_state() == ExtHandlerState.Enabled: 632 disable_exit_code = execute_old_handler_command_and_return_if_succeeds( 633 func=lambda: old_ext_handler_i.disable()) # pylint: disable=W0108 634 635 ext_handler_i.copy_status_files(old_ext_handler_i) 636 if ext_handler_i.version_gt(old_ext_handler_i): 637 ext_handler_i.update(disable_exit_code=disable_exit_code, 638 updating_from_version=old_ext_handler_i.ext_handler.properties.version) 639 else: 640 updating_from_version = ext_handler_i.ext_handler.properties.version 641 old_ext_handler_i.update(version=updating_from_version, 642 disable_exit_code=disable_exit_code, updating_from_version=updating_from_version) 643 uninstall_exit_code = execute_old_handler_command_and_return_if_succeeds( 644 func=lambda: old_ext_handler_i.uninstall()) # pylint: disable=W0108 645 old_ext_handler_i.remove_ext_handler() 646 ext_handler_i.update_with_install(uninstall_exit_code=uninstall_exit_code) 647 return uninstall_exit_code 648 649 def handle_disable(self, ext_handler_i): 650 self.log_process = True 651 handler_state = ext_handler_i.get_handler_state() 652 ext_handler_i.logger.info("[Disable] current handler state is: {0}", 653 handler_state.lower()) 654 if handler_state == ExtHandlerState.Enabled: 655 ext_handler_i.disable() 656 657 def handle_uninstall(self, ext_handler_i): 658 self.log_process = True 659 handler_state = ext_handler_i.get_handler_state() 660 ext_handler_i.logger.info("[Uninstall] current handler state is: {0}", 661 handler_state.lower()) 662 if handler_state != ExtHandlerState.NotInstalled: 663 if handler_state == ExtHandlerState.Enabled: 664 ext_handler_i.disable() 665 666 # Try uninstalling the extension and swallow any exceptions in case of failures after logging them 667 try: 668 ext_handler_i.uninstall() 669 except ExtensionError as e: 670 ext_handler_i.report_event(message=ustr(e), is_success=False) 671 672 ext_handler_i.remove_ext_handler() 673 674 def report_ext_handlers_status(self): 675 """ 676 Go through handler_state dir, collect and report status 677 """ 678 vm_status = VMStatus(status="Ready", message="Guest Agent is running") 679 if self.ext_handlers is not None: 680 for ext_handler in self.ext_handlers.extHandlers: 681 try: 682 self.report_ext_handler_status(vm_status, ext_handler) 683 except ExtensionError as e: 684 add_event( 685 AGENT_NAME, 686 version=CURRENT_VERSION, 687 op=WALAEventOperation.ExtensionProcessing, 688 is_success=False, 689 message=ustr(e)) 690 691 logger.verbose("Report vm agent status") 692 try: 693 self.protocol.report_vm_status(vm_status) 694 if self.log_report: 695 logger.verbose("Completed vm agent status report") 696 self.report_status_error_state.reset() 697 except ProtocolNotFoundError as e: 698 self.report_status_error_state.incr() 699 message = "Failed to report vm agent status: {0}".format(e) 700 logger.verbose(message) 701 except ProtocolError as e: 702 self.report_status_error_state.incr() 703 message = "Failed to report vm agent status: {0}".format(e) 704 add_event(AGENT_NAME, 705 version=CURRENT_VERSION, 706 op=WALAEventOperation.ExtensionProcessing, 707 is_success=False, 708 message=message) 709 710 if self.report_status_error_state.is_triggered(): 711 message = "Failed to report vm agent status for more than {0}" \ 712 .format(self.report_status_error_state.min_timedelta) 713 714 add_event(AGENT_NAME, 715 version=CURRENT_VERSION, 716 op=WALAEventOperation.ReportStatusExtended, 717 is_success=False, 718 message=message) 719 720 self.report_status_error_state.reset() 721 722 self.write_ext_handlers_status_to_info_file(vm_status) 723 724 @staticmethod 725 def write_ext_handlers_status_to_info_file(vm_status): 726 status_path = os.path.join(conf.get_lib_dir(), AGENT_STATUS_FILE) 727 728 agent_details = { 729 "agent_name": AGENT_NAME, 730 "current_version": str(CURRENT_VERSION), 731 "goal_state_version": str(GOAL_STATE_AGENT_VERSION), 732 "distro_details": "{0}:{1}".format(DISTRO_NAME, DISTRO_VERSION), 733 "last_successful_status_upload_time": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()), 734 "python_version": "Python: {0}.{1}.{2}".format(PY_VERSION_MAJOR, PY_VERSION_MINOR, PY_VERSION_MICRO) 735 } 736 737 # Convert VMStatus class to Dict. 738 data = get_properties(vm_status) 739 740 # The above class contains vmAgent.extensionHandlers 741 # (more info: azurelinuxagent.common.protocol.restapi.VMAgentStatus) 742 handler_statuses = data['vmAgent']['extensionHandlers'] 743 for handler_status in handler_statuses: 744 try: 745 handler_status.pop('code', None) 746 handler_status.pop('message', None) 747 handler_status.pop('extensions', None) 748 except KeyError: 749 pass 750 751 agent_details['extensions_status'] = handler_statuses 752 agent_details_json = json.dumps(agent_details) 753 754 fileutil.write_file(status_path, agent_details_json) 755 756 def report_ext_handler_status(self, vm_status, ext_handler): 757 ext_handler_i = ExtHandlerInstance(ext_handler, self.protocol) 758 759 handler_status = ext_handler_i.get_handler_status() 760 if handler_status is None: 761 return 762 763 handler_state = ext_handler_i.get_handler_state() 764 if handler_state != ExtHandlerState.NotInstalled: 765 try: 766 active_exts = ext_handler_i.report_ext_status() 767 handler_status.extensions.extend(active_exts) 768 except ExtensionError as e: 769 ext_handler_i.set_handler_status(message=ustr(e), code=e.code) 770 771 try: 772 heartbeat = ext_handler_i.collect_heartbeat() 773 if heartbeat is not None: 774 handler_status.status = heartbeat.get('status') 775 except ExtensionError as e: 776 ext_handler_i.set_handler_status(message=ustr(e), code=e.code) 777 778 vm_status.vmAgent.extensionHandlers.append(handler_status) 779 780 781class ExtHandlerInstance(object): 782 783 def __truncate_file_head(self, filename, max_size): 784 try: 785 if os.stat(filename).st_size <= max_size: 786 return 787 788 with open(filename, "rb") as existing_file: 789 existing_file.seek(-1 * max_size, 2) 790 _ = existing_file.readline() 791 792 with open(filename + ".tmp", "wb") as tmp_file: 793 shutil.copyfileobj(existing_file, tmp_file) 794 795 os.rename(filename + ".tmp", filename) 796 797 except (IOError, OSError) as e: 798 if is_file_not_found_error(e): 799 # If CommandExecution.log does not exist, it's not noteworthy; 800 # this just means that no extension with self.ext_handler.name is 801 # installed. 802 return 803 804 logger.error("Exception occurred while attempting to truncate CommandExecution.log " 805 "for extension {0}. Exception is: {1}", self.ext_handler.name, e) 806 807 for f in (filename, filename + ".tmp"): 808 try: 809 os.remove(f) 810 except (IOError, OSError) as cleanup_exception: 811 if is_file_not_found_error(cleanup_exception): 812 logger.info("File '{0}' does not exist.", f) 813 else: 814 logger.warn("Exception occurred while attempting " 815 "to remove file '{0}': {1}", f, cleanup_exception) 816 817 818 def __init__(self, ext_handler, protocol, execution_log_max_size=(10 * 1024 * 1024)): 819 self.ext_handler = ext_handler 820 self.protocol = protocol 821 self.operation = None 822 self.pkg = None 823 self.pkg_file = None 824 self.logger = None 825 self.set_logger() 826 827 try: 828 fileutil.mkdir(self.get_log_dir(), mode=0o755) 829 except IOError as e: 830 self.logger.error(u"Failed to create extension log dir: {0}", e) 831 else: 832 log_file = os.path.join(self.get_log_dir(), "CommandExecution.log") 833 834 self.__truncate_file_head(log_file, execution_log_max_size) 835 836 self.logger.add_appender(logger.AppenderType.FILE, logger.LogLevel.INFO, log_file) 837 838 def decide_version(self, target_state=None): 839 self.logger.verbose("Decide which version to use") 840 try: 841 pkg_list = self.protocol.get_ext_handler_pkgs(self.ext_handler) 842 except ProtocolError as e: 843 raise ExtensionError("Failed to get ext handler pkgs", e) 844 except ExtensionDownloadError: 845 self.set_operation(WALAEventOperation.Download) 846 raise 847 848 # Determine the desired and installed versions 849 requested_version = FlexibleVersion(str(self.ext_handler.properties.version)) 850 installed_version_string = self.get_installed_version() 851 installed_version = requested_version \ 852 if installed_version_string is None \ 853 else FlexibleVersion(installed_version_string) 854 855 # Divide packages 856 # - Find the installed package (its version must exactly match) 857 # - Find the internal candidate (its version must exactly match) 858 # - Separate the public packages 859 selected_pkg = None 860 installed_pkg = None 861 pkg_list.versions.sort(key=lambda p: FlexibleVersion(p.version)) 862 for pkg in pkg_list.versions: 863 pkg_version = FlexibleVersion(pkg.version) 864 if pkg_version == installed_version: 865 installed_pkg = pkg 866 if requested_version.matches(pkg_version): 867 selected_pkg = pkg 868 869 # Finally, update the version only if not downgrading 870 # Note: 871 # - A downgrade, which will be bound to the same major version, 872 # is allowed if the installed version is no longer available 873 if target_state in (ExtensionRequestedState.Uninstall, ExtensionRequestedState.Disabled): 874 if installed_pkg is None: 875 msg = "Failed to find installed version: {0} of Handler: {1} in handler manifest to uninstall.".format( 876 installed_version, self.ext_handler.name) 877 self.logger.warn(msg) 878 self.pkg = installed_pkg 879 self.ext_handler.properties.version = str(installed_version) \ 880 if installed_version is not None else None 881 else: 882 self.pkg = selected_pkg 883 if self.pkg is not None: 884 self.ext_handler.properties.version = str(selected_pkg.version) 885 886 if self.pkg is not None: 887 self.logger.verbose("Use version: {0}", self.pkg.version) 888 self.set_logger() 889 return self.pkg 890 891 def set_logger(self): 892 prefix = "[{0}]".format(self.get_full_name()) 893 self.logger = logger.Logger(logger.DEFAULT_LOGGER, prefix) 894 895 def version_gt(self, other): 896 self_version = self.ext_handler.properties.version 897 other_version = other.ext_handler.properties.version 898 return FlexibleVersion(self_version) > FlexibleVersion(other_version) 899 900 def version_ne(self, other): 901 self_version = self.ext_handler.properties.version 902 other_version = other.ext_handler.properties.version 903 return FlexibleVersion(self_version) != FlexibleVersion(other_version) 904 905 def get_installed_ext_handler(self): 906 latest_version = self.get_installed_version() 907 if latest_version is None: 908 return None 909 910 installed_handler = ExtHandler() 911 set_properties("ExtHandler", installed_handler, get_properties(self.ext_handler)) 912 installed_handler.properties.version = latest_version 913 return ExtHandlerInstance(installed_handler, self.protocol) 914 915 def get_installed_version(self): 916 latest_version = None 917 918 for path in glob.iglob(os.path.join(conf.get_lib_dir(), self.ext_handler.name + "-*")): 919 if not os.path.isdir(path): 920 continue 921 922 separator = path.rfind('-') 923 version_from_path = FlexibleVersion(path[separator + 1:]) 924 state_path = os.path.join(path, 'config', 'HandlerState') 925 926 if not os.path.exists(state_path) or fileutil.read_file(state_path) == ExtHandlerState.NotInstalled \ 927 or fileutil.read_file(state_path) == ExtHandlerState.FailedUpgrade: 928 logger.verbose("Ignoring version of uninstalled or failed extension: " 929 "{0}".format(path)) 930 continue 931 932 if latest_version is None or latest_version < version_from_path: 933 latest_version = version_from_path 934 935 return str(latest_version) if latest_version is not None else None 936 937 def copy_status_files(self, old_ext_handler_i): 938 self.logger.info("Copy status files from old plugin to new") 939 old_ext_dir = old_ext_handler_i.get_base_dir() 940 new_ext_dir = self.get_base_dir() 941 942 old_ext_mrseq_file = os.path.join(old_ext_dir, "mrseq") 943 if os.path.isfile(old_ext_mrseq_file): 944 shutil.copy2(old_ext_mrseq_file, new_ext_dir) 945 946 old_ext_status_dir = old_ext_handler_i.get_status_dir() 947 new_ext_status_dir = self.get_status_dir() 948 949 if os.path.isdir(old_ext_status_dir): 950 for status_file in os.listdir(old_ext_status_dir): 951 status_file = os.path.join(old_ext_status_dir, status_file) 952 if os.path.isfile(status_file): 953 shutil.copy2(status_file, new_ext_status_dir) 954 955 def set_operation(self, op): 956 self.operation = op 957 958 def report_event(self, message="", is_success=True, duration=0, log_event=True): 959 ext_handler_version = self.ext_handler.properties.version 960 add_event(name=self.ext_handler.name, version=ext_handler_version, message=message, 961 op=self.operation, is_success=is_success, duration=duration, log_event=log_event) 962 963 def _download_extension_package(self, source_uri, target_file): 964 self.logger.info("Downloading extension package: {0}", source_uri) 965 try: 966 if not self.protocol.download_ext_handler_pkg(source_uri, target_file): 967 raise Exception("Failed to download extension package from {0}".format(source_uri)) 968 except Exception as exception: 969 self.logger.info("Error downloading extension package: {0}", ustr(exception)) 970 if os.path.exists(target_file): 971 os.remove(target_file) 972 return False 973 return True 974 975 def _unzip_extension_package(self, source_file, target_directory): 976 self.logger.info("Unzipping extension package: {0}", source_file) 977 try: 978 zipfile.ZipFile(source_file).extractall(target_directory) 979 except Exception as exception: 980 logger.info("Error while unzipping extension package: {0}", ustr(exception)) 981 os.remove(source_file) 982 if os.path.exists(target_directory): 983 shutil.rmtree(target_directory) 984 return False 985 return True 986 987 def download(self): 988 begin_utc = datetime.datetime.utcnow() 989 self.set_operation(WALAEventOperation.Download) 990 991 if self.pkg is None or self.pkg.uris is None or len(self.pkg.uris) == 0: 992 raise ExtensionDownloadError("No package uri found") 993 994 destination = os.path.join(conf.get_lib_dir(), self.get_extension_package_zipfile_name()) 995 996 package_exists = False 997 if os.path.exists(destination): 998 self.logger.info("Using existing extension package: {0}", destination) 999 if self._unzip_extension_package(destination, self.get_base_dir()): 1000 package_exists = True 1001 else: 1002 self.logger.info("The existing extension package is invalid, will ignore it.") 1003 1004 if not package_exists: 1005 downloaded = False 1006 i = 0 1007 while i < NUMBER_OF_DOWNLOAD_RETRIES: 1008 uris_shuffled = self.pkg.uris 1009 random.shuffle(uris_shuffled) 1010 1011 for uri in uris_shuffled: 1012 if not self._download_extension_package(uri.uri, destination): 1013 continue 1014 1015 if self._unzip_extension_package(destination, self.get_base_dir()): 1016 downloaded = True 1017 break 1018 1019 if downloaded: 1020 break 1021 1022 self.logger.info("Failed to download the extension package from all uris, will retry after a minute") 1023 time.sleep(60) 1024 i += 1 1025 1026 if not downloaded: 1027 raise ExtensionDownloadError("Failed to download extension", 1028 code=ExtensionErrorCodes.PluginManifestDownloadError) 1029 1030 duration = elapsed_milliseconds(begin_utc) 1031 self.report_event(message="Download succeeded", duration=duration) 1032 1033 self.pkg_file = destination 1034 1035 def initialize(self): 1036 self.logger.info("Initializing extension {0}".format(self.get_full_name())) 1037 1038 # Add user execute permission to all files under the base dir 1039 for file in fileutil.get_all_files(self.get_base_dir()): # pylint: disable=redefined-builtin 1040 fileutil.chmod(file, os.stat(file).st_mode | stat.S_IXUSR) 1041 1042 # Save HandlerManifest.json 1043 man_file = fileutil.search_file(self.get_base_dir(), 'HandlerManifest.json') 1044 1045 if man_file is None: 1046 raise ExtensionDownloadError("HandlerManifest.json not found") 1047 1048 try: 1049 man = fileutil.read_file(man_file, remove_bom=True) 1050 fileutil.write_file(self.get_manifest_file(), man) 1051 except IOError as e: 1052 fileutil.clean_ioerror(e, paths=[self.get_base_dir(), self.pkg_file]) 1053 raise ExtensionDownloadError(u"Failed to save HandlerManifest.json", e) 1054 1055 # Create status and config dir 1056 try: 1057 status_dir = self.get_status_dir() 1058 fileutil.mkdir(status_dir, mode=0o700) 1059 1060 conf_dir = self.get_conf_dir() 1061 fileutil.mkdir(conf_dir, mode=0o700) 1062 1063 if get_supported_feature_by_name(SupportedFeatureNames.ExtensionTelemetryPipeline).is_supported: 1064 fileutil.mkdir(self.get_extension_events_dir(), mode=0o700) 1065 1066 seq_no, status_path = self.get_status_file_path() # pylint: disable=W0612 1067 if status_path is not None: 1068 now = datetime.datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%SZ") 1069 status = [ 1070 { 1071 "version": 1.0, 1072 "timestampUTC": now, 1073 "status": { 1074 "name": self.ext_handler.name, 1075 "operation": "Enabling Handler", 1076 "status": "transitioning", 1077 "code": 0, 1078 "formattedMessage": { 1079 "lang": "en-US", 1080 "message": "Install/Enable is in progress." 1081 } 1082 } 1083 } 1084 ] 1085 fileutil.write_file(status_path, json.dumps(status)) 1086 1087 except IOError as e: 1088 fileutil.clean_ioerror(e, paths=[self.get_base_dir(), self.pkg_file]) 1089 raise ExtensionDownloadError(u"Failed to initialize extension '{0}'".format(self.get_full_name()), e) 1090 1091 # Save HandlerEnvironment.json 1092 self.create_handler_env() 1093 1094 def enable(self, uninstall_exit_code=None): 1095 uninstall_exit_code = str(uninstall_exit_code) if uninstall_exit_code is not None else NOT_RUN 1096 env = {ExtCommandEnvVariable.UninstallReturnCode: uninstall_exit_code} 1097 1098 self.set_operation(WALAEventOperation.Enable) 1099 man = self.load_manifest() 1100 enable_cmd = man.get_enable_command() 1101 self.logger.info("Enable extension [{0}]".format(enable_cmd)) 1102 self.launch_command(enable_cmd, timeout=300, 1103 extension_error_code=ExtensionErrorCodes.PluginEnableProcessingFailed, env=env) 1104 self.set_handler_state(ExtHandlerState.Enabled) 1105 self.set_handler_status(status="Ready", message="Plugin enabled") 1106 1107 def disable(self): 1108 self.set_operation(WALAEventOperation.Disable) 1109 man = self.load_manifest() 1110 disable_cmd = man.get_disable_command() 1111 self.logger.info("Disable extension [{0}]".format(disable_cmd)) 1112 self.launch_command(disable_cmd, timeout=900, 1113 extension_error_code=ExtensionErrorCodes.PluginDisableProcessingFailed) 1114 self.set_handler_state(ExtHandlerState.Installed) 1115 self.set_handler_status(status="NotReady", message="Plugin disabled") 1116 1117 def install(self, uninstall_exit_code=None): 1118 uninstall_exit_code = str(uninstall_exit_code) if uninstall_exit_code is not None else NOT_RUN 1119 env = {ExtCommandEnvVariable.UninstallReturnCode: uninstall_exit_code} 1120 1121 man = self.load_manifest() 1122 install_cmd = man.get_install_command() 1123 self.logger.info("Install extension [{0}]".format(install_cmd)) 1124 self.set_operation(WALAEventOperation.Install) 1125 self.launch_command(install_cmd, timeout=900, 1126 extension_error_code=ExtensionErrorCodes.PluginInstallProcessingFailed, env=env) 1127 self.set_handler_state(ExtHandlerState.Installed) 1128 1129 def uninstall(self): 1130 self.set_operation(WALAEventOperation.UnInstall) 1131 man = self.load_manifest() 1132 uninstall_cmd = man.get_uninstall_command() 1133 self.logger.info("Uninstall extension [{0}]".format(uninstall_cmd)) 1134 self.launch_command(uninstall_cmd) 1135 1136 def remove_ext_handler(self): 1137 try: 1138 zip_filename = os.path.join(conf.get_lib_dir(), self.get_extension_package_zipfile_name()) 1139 if os.path.exists(zip_filename): 1140 os.remove(zip_filename) 1141 self.logger.verbose("Deleted the extension zip at path {0}", zip_filename) 1142 1143 base_dir = self.get_base_dir() 1144 if os.path.isdir(base_dir): 1145 self.logger.info("Remove extension handler directory: {0}", base_dir) 1146 1147 # some extensions uninstall asynchronously so ignore error 2 while removing them 1148 def on_rmtree_error(_, __, exc_info): 1149 _, exception, _ = exc_info 1150 if not isinstance(exception, OSError) or exception.errno != 2: # [Errno 2] No such file or directory 1151 raise exception 1152 1153 shutil.rmtree(base_dir, onerror=on_rmtree_error) 1154 except IOError as e: 1155 message = "Failed to remove extension handler directory: {0}".format(e) 1156 self.report_event(message=message, is_success=False) 1157 self.logger.warn(message) 1158 1159 def update(self, version=None, disable_exit_code=None, updating_from_version=None): # pylint: disable=W0621 1160 if version is None: 1161 version = self.ext_handler.properties.version 1162 1163 disable_exit_code = str(disable_exit_code) if disable_exit_code is not None else NOT_RUN 1164 env = {'VERSION': version, ExtCommandEnvVariable.DisableReturnCode: disable_exit_code, 1165 ExtCommandEnvVariable.UpdatingFromVersion: updating_from_version} 1166 1167 try: 1168 self.set_operation(WALAEventOperation.Update) 1169 man = self.load_manifest() 1170 update_cmd = man.get_update_command() 1171 self.logger.info("Update extension [{0}]".format(update_cmd)) 1172 self.launch_command(update_cmd, 1173 timeout=900, 1174 extension_error_code=ExtensionErrorCodes.PluginUpdateProcessingFailed, 1175 env=env) 1176 except ExtensionError: 1177 # Mark the handler as Failed so we don't clean it up and can keep reporting its status 1178 self.set_handler_state(ExtHandlerState.FailedUpgrade) 1179 raise 1180 1181 def update_with_install(self, uninstall_exit_code=None): 1182 man = self.load_manifest() 1183 if man.is_update_with_install(): 1184 self.install(uninstall_exit_code=uninstall_exit_code) 1185 else: 1186 self.logger.info("UpdateWithInstall not set. " 1187 "Skip install during upgrade.") 1188 self.set_handler_state(ExtHandlerState.Installed) 1189 1190 def _get_last_modified_seq_no_from_config_files(self): 1191 """ 1192 The sequence number is not guaranteed to always be strictly increasing. To ensure we always get the latest one, 1193 fetching the sequence number from config file that was last modified (and not necessarily the largest). 1194 :return: Last modified Sequence number or -1 on errors 1195 1196 Note: This function is going to be deprecated soon. We should only rely on seqNo from GoalState rather than file system. 1197 """ 1198 seq_no = -1 1199 1200 try: 1201 largest_modified_time = 0 1202 conf_dir = self.get_conf_dir() 1203 for item in os.listdir(conf_dir): 1204 item_path = os.path.join(conf_dir, item) 1205 if not os.path.isfile(item_path): 1206 continue 1207 try: 1208 separator = item.rfind(".") 1209 if separator > 0 and item[separator + 1:] == 'settings': 1210 curr_seq_no = int(item.split('.')[0]) 1211 curr_modified_time = os.path.getmtime(item_path) 1212 if curr_modified_time > largest_modified_time: 1213 seq_no = curr_seq_no 1214 largest_modified_time = curr_modified_time 1215 except (ValueError, IndexError, TypeError): 1216 self.logger.verbose("Failed to parse file name: {0}", item) 1217 continue 1218 except Exception as error: 1219 logger.verbose("Error fetching sequence number from config files: {0}".format(ustr(error))) 1220 seq_no = -1 1221 1222 return seq_no 1223 1224 def get_status_file_path(self, extension=None): 1225 path = None 1226 # Todo: Remove check on filesystem for fetching sequence number (legacy behaviour). 1227 # We should technically only fetch the sequence number from GoalState and not rely on the filesystem at all, 1228 # But since we still have Kusto data from the operation below (~0.000065% VMs are still reporting 1229 # WALAEventOperation.SequenceNumberMismatch), keeping this as is with modified logic for fetching 1230 # sequence number from filesystem. Based on the new data we will eventually phase this out. 1231 seq_no = self._get_last_modified_seq_no_from_config_files() 1232 1233 # Issue 1116: use the sequence number from goal state where possible 1234 if extension is not None and extension.sequenceNumber is not None: 1235 try: 1236 gs_seq_no = int(extension.sequenceNumber) 1237 1238 if gs_seq_no != seq_no: 1239 add_event(AGENT_NAME, version=CURRENT_VERSION, op=WALAEventOperation.SequenceNumberMismatch, 1240 is_success=False, message="Goal state: {0}, disk: {1}".format(gs_seq_no, seq_no), 1241 log_event=False) 1242 1243 seq_no = gs_seq_no 1244 except ValueError: 1245 logger.error('Sequence number [{0}] does not appear to be valid'.format(extension.sequenceNumber)) 1246 1247 if seq_no > -1: 1248 path = os.path.join( 1249 self.get_status_dir(), 1250 "{0}.status".format(seq_no)) 1251 1252 return seq_no, path 1253 1254 def collect_ext_status(self, ext): 1255 self.logger.verbose("Collect extension status for {0}".format(ext.name)) 1256 seq_no, ext_status_file = self.get_status_file_path(ext) 1257 if seq_no == -1: 1258 return None 1259 1260 data = None 1261 data_str = None 1262 ext_status = ExtensionStatus(seq_no=seq_no) 1263 1264 try: 1265 data_str, data = self._read_and_parse_json_status_file(ext_status_file) 1266 except ExtensionStatusError as e: 1267 msg = "" 1268 if e.code == ExtensionStatusError.CouldNotReadStatusFile: 1269 ext_status.code = ExtensionErrorCodes.PluginUnknownFailure 1270 msg = u"We couldn't read any status for {0}-{1} extension, for the sequence number {2}. It failed due" \ 1271 u" to {3}".format(ext.name, self.ext_handler.properties.version, seq_no, e) 1272 elif ExtensionStatusError.InvalidJsonFile: 1273 ext_status.code = ExtensionErrorCodes.PluginSettingsStatusInvalid 1274 msg = u"The status reported by the extension {0}-{1}(Sequence number {2}), was in an " \ 1275 u"incorrect format and the agent could not parse it correctly. Failed due to {3}" \ 1276 .format(ext.name, self.ext_handler.properties.version, seq_no, e) 1277 1278 # This log is periodic due to the verbose nature of the status check. Please make sure that the message 1279 # constructed above does not change very frequently and includes important info such as sequence number, 1280 # extension name to make sure that the log reflects changes in the extension sequence for which the 1281 # status is being sent. 1282 logger.periodic_warn(logger.EVERY_HALF_HOUR, u"[PERIODIC] " + msg) 1283 add_periodic(delta=logger.EVERY_HALF_HOUR, name=ext.name, version=self.ext_handler.properties.version, 1284 op=WALAEventOperation.StatusProcessing, is_success=False, message=msg, 1285 log_event=False) 1286 1287 ext_status.message = msg 1288 ext_status.status = ValidHandlerStatus.error 1289 1290 return ext_status 1291 1292 # We did not encounter InvalidJsonFile/CouldNotReadStatusFile and thus the status file was correctly written 1293 # and has valid json. 1294 try: 1295 parse_ext_status(ext_status, data) 1296 if len(data_str) > _MAX_STATUS_FILE_SIZE_IN_BYTES: 1297 raise ExtensionStatusError(msg="For Extension Handler {0}-{1} for the sequence number {2}, the status " 1298 "file {3} of size {4} bytes is too big. Max Limit allowed is {5} bytes" 1299 .format(ext.name, self.ext_handler.properties.version, seq_no, 1300 ext_status_file, len(data_str), _MAX_STATUS_FILE_SIZE_IN_BYTES), 1301 code=ExtensionStatusError.MaxSizeExceeded) 1302 except ExtensionStatusError as e: 1303 msg = u"For Extension Handler {0}-{1} for the sequence number {2}, the status file {3}. " \ 1304 u"Encountered the following error: {4}".format(ext.name, self.ext_handler.properties.version, seq_no, 1305 ext_status_file, ustr(e)) 1306 logger.periodic_warn(logger.EVERY_DAY, u"[PERIODIC] " + msg) 1307 add_periodic(delta=logger.EVERY_HALF_HOUR, name=ext.name, version=self.ext_handler.properties.version, 1308 op=WALAEventOperation.StatusProcessing, is_success=False, message=msg, log_event=False) 1309 1310 if e.code == ExtensionStatusError.MaxSizeExceeded: 1311 ext_status.message, field_size = self._truncate_message(ext_status.message, _MAX_STATUS_MESSAGE_LENGTH) 1312 ext_status.substatusList = self._process_substatus_list(ext_status.substatusList, field_size) 1313 1314 elif e.code == ExtensionStatusError.StatusFileMalformed: 1315 ext_status.message = "Could not get a valid status from the extension {0}-{1}. Encountered the " \ 1316 "following error: {2}".format(ext.name, self.ext_handler.properties.version, 1317 ustr(e)) 1318 ext_status.code = ExtensionErrorCodes.PluginSettingsStatusInvalid 1319 ext_status.status = ValidHandlerStatus.error 1320 1321 return ext_status 1322 1323 def get_ext_handling_status(self, ext): 1324 seq_no, ext_status_file = self.get_status_file_path(ext) 1325 1326 # This is legacy scenario for cases when no extension settings is available 1327 if seq_no < 0 or ext_status_file is None: 1328 return None 1329 1330 # Missing status file is considered a non-terminal state here 1331 # so that extension sequencing can wait until it becomes existing 1332 if not os.path.exists(ext_status_file): 1333 status = ValidHandlerStatus.warning 1334 else: 1335 ext_status = self.collect_ext_status(ext) 1336 status = ext_status.status if ext_status is not None else None 1337 1338 return status 1339 1340 def is_ext_handling_complete(self, ext): 1341 status = self.get_ext_handling_status(ext) 1342 1343 # when seq < 0 (i.e. no new user settings), the handling is complete and return None status 1344 if status is None: 1345 return True, None 1346 1347 # If not in terminal state, it is incomplete 1348 if status not in _EXTENSION_TERMINAL_STATUSES: 1349 return False, status 1350 1351 # Extension completed, return its status 1352 return True, status 1353 1354 def report_ext_status(self): 1355 active_exts = [] 1356 # TODO Refactor or remove this common code pattern (for each extension subordinate to an ext_handler, do X). 1357 for ext in self.ext_handler.properties.extensions: 1358 ext_status = self.collect_ext_status(ext) 1359 if ext_status is None: 1360 continue 1361 try: 1362 self.protocol.report_ext_status(self.ext_handler.name, ext.name, 1363 ext_status) 1364 active_exts.append(ext.name) 1365 except ProtocolError as e: 1366 self.logger.error(u"Failed to report extension status: {0}", e) 1367 return active_exts 1368 1369 def collect_heartbeat(self): # pylint: disable=R1710 1370 man = self.load_manifest() 1371 if not man.is_report_heartbeat(): 1372 return 1373 heartbeat_file = os.path.join(conf.get_lib_dir(), 1374 self.get_heartbeat_file()) 1375 1376 if not os.path.isfile(heartbeat_file): 1377 raise ExtensionError("Failed to get heart beat file") 1378 if not self.is_responsive(heartbeat_file): 1379 return { 1380 "status": "Unresponsive", 1381 "code": -1, 1382 "message": "Extension heartbeat is not responsive" 1383 } 1384 try: 1385 heartbeat_json = fileutil.read_file(heartbeat_file) 1386 heartbeat = json.loads(heartbeat_json)[0]['heartbeat'] 1387 except IOError as e: 1388 raise ExtensionError("Failed to get heartbeat file:{0}".format(e)) 1389 except (ValueError, KeyError) as e: 1390 raise ExtensionError("Malformed heartbeat file: {0}".format(e)) 1391 return heartbeat 1392 1393 @staticmethod 1394 def is_responsive(heartbeat_file): 1395 """ 1396 Was heartbeat_file updated within the last ten (10) minutes? 1397 1398 :param heartbeat_file: str 1399 :return: bool 1400 """ 1401 last_update = int(time.time() - os.stat(heartbeat_file).st_mtime) 1402 return last_update <= 600 1403 1404 def launch_command(self, cmd, timeout=300, extension_error_code=ExtensionErrorCodes.PluginProcessingError, 1405 env=None): 1406 begin_utc = datetime.datetime.utcnow() 1407 self.logger.verbose("Launch command: [{0}]", cmd) 1408 1409 base_dir = self.get_base_dir() 1410 1411 with tempfile.TemporaryFile(dir=base_dir, mode="w+b") as stdout: 1412 with tempfile.TemporaryFile(dir=base_dir, mode="w+b") as stderr: 1413 if env is None: 1414 env = {} 1415 env.update(os.environ) 1416 # Always add Extension Path and version to the current launch_command (Ask from publishers) 1417 env.update({ 1418 ExtCommandEnvVariable.ExtensionPath: base_dir, 1419 ExtCommandEnvVariable.ExtensionVersion: str(self.ext_handler.properties.version), 1420 ExtCommandEnvVariable.ExtensionSeqNumber: str(self.get_seq_no()), 1421 ExtCommandEnvVariable.WireProtocolAddress: self.protocol.get_endpoint() 1422 }) 1423 1424 supported_features = [] 1425 for _, feature in get_agent_supported_features_list_for_extensions().items(): 1426 if feature.is_supported: 1427 supported_features.append( 1428 { 1429 "Key": feature.name, 1430 "Value": feature.version 1431 } 1432 ) 1433 if supported_features: 1434 env.update({ 1435 ExtCommandEnvVariable.ExtensionSupportedFeatures: json.dumps(supported_features) 1436 }) 1437 1438 try: 1439 # Some extensions erroneously begin cmd with a slash; don't interpret those 1440 # as root-relative. (Issue #1170) 1441 full_path = os.path.join(base_dir, cmd.lstrip(os.path.sep)) 1442 1443 process_output = CGroupConfigurator.get_instance().start_extension_command( 1444 extension_name=self.get_full_name(), 1445 command=full_path, 1446 timeout=timeout, 1447 shell=True, 1448 cwd=base_dir, 1449 env=env, 1450 stdout=stdout, 1451 stderr=stderr, 1452 error_code=extension_error_code) 1453 1454 except OSError as e: 1455 raise ExtensionError("Failed to launch '{0}': {1}".format(full_path, e.strerror), 1456 code=extension_error_code) 1457 1458 duration = elapsed_milliseconds(begin_utc) 1459 log_msg = "{0}\n{1}".format(cmd, "\n".join([line for line in process_output.split('\n') if line != ""])) 1460 1461 self.logger.verbose(log_msg) 1462 self.report_event(message=log_msg, duration=duration, log_event=False) 1463 1464 return process_output 1465 1466 def load_manifest(self): 1467 man_file = self.get_manifest_file() 1468 try: 1469 data = json.loads(fileutil.read_file(man_file)) 1470 except (IOError, OSError) as e: 1471 raise ExtensionError('Failed to load manifest file ({0}): {1}'.format(man_file, e.strerror), 1472 code=ExtensionErrorCodes.PluginHandlerManifestNotFound) 1473 except ValueError: 1474 raise ExtensionError('Malformed manifest file ({0}).'.format(man_file), 1475 code=ExtensionErrorCodes.PluginHandlerManifestDeserializationError) 1476 1477 return HandlerManifest(data[0]) 1478 1479 def update_settings_file(self, settings_file, settings): 1480 settings_file = os.path.join(self.get_conf_dir(), settings_file) 1481 try: 1482 fileutil.write_file(settings_file, settings) 1483 except IOError as e: 1484 fileutil.clean_ioerror(e, 1485 paths=[settings_file]) 1486 raise ExtensionError(u"Failed to update settings file", e) 1487 1488 def update_settings(self): 1489 if self.ext_handler.properties.extensions is None or \ 1490 len(self.ext_handler.properties.extensions) == 0: 1491 # This is the behavior of waagent 2.0.x 1492 # The new agent has to be consistent with the old one. 1493 self.logger.info("Extension has no settings, write empty 0.settings") 1494 self.update_settings_file("0.settings", "") 1495 return 1496 1497 for ext in self.ext_handler.properties.extensions: 1498 settings = { 1499 'publicSettings': ext.publicSettings, 1500 'protectedSettings': ext.protectedSettings, 1501 'protectedSettingsCertThumbprint': ext.certificateThumbprint 1502 } 1503 ext_settings = { 1504 "runtimeSettings": [{ 1505 "handlerSettings": settings 1506 }] 1507 } 1508 settings_file = "{0}.settings".format(ext.sequenceNumber) 1509 self.logger.info("Update settings file: {0}", settings_file) 1510 self.update_settings_file(settings_file, json.dumps(ext_settings)) 1511 1512 def create_handler_env(self): 1513 handler_env = { 1514 HandlerEnvironment.logFolder: self.get_log_dir(), 1515 HandlerEnvironment.configFolder: self.get_conf_dir(), 1516 HandlerEnvironment.statusFolder: self.get_status_dir(), 1517 HandlerEnvironment.heartbeatFile: self.get_heartbeat_file() 1518 } 1519 1520 if get_supported_feature_by_name(SupportedFeatureNames.ExtensionTelemetryPipeline).is_supported: 1521 handler_env[HandlerEnvironment.eventsFolder] = self.get_extension_events_dir() 1522 1523 env = [{ 1524 HandlerEnvironment.name: self.ext_handler.name, 1525 HandlerEnvironment.version: HandlerEnvironment.schemaVersion, 1526 HandlerEnvironment.handlerEnvironment: handler_env 1527 }] 1528 try: 1529 fileutil.write_file(self.get_env_file(), json.dumps(env)) 1530 except IOError as e: 1531 fileutil.clean_ioerror(e, 1532 paths=[self.get_base_dir(), self.pkg_file]) 1533 raise ExtensionDownloadError(u"Failed to save handler environment", e) 1534 1535 def set_handler_state(self, handler_state): 1536 state_dir = self.get_conf_dir() 1537 state_file = os.path.join(state_dir, "HandlerState") 1538 try: 1539 if not os.path.exists(state_dir): 1540 fileutil.mkdir(state_dir, mode=0o700) 1541 fileutil.write_file(state_file, handler_state) 1542 except IOError as e: 1543 fileutil.clean_ioerror(e, paths=[state_file]) 1544 self.logger.error("Failed to set state: {0}", e) 1545 1546 def get_handler_state(self): 1547 state_dir = self.get_conf_dir() 1548 state_file = os.path.join(state_dir, "HandlerState") 1549 if not os.path.isfile(state_file): 1550 return ExtHandlerState.NotInstalled 1551 1552 try: 1553 return fileutil.read_file(state_file) 1554 except IOError as e: 1555 self.logger.error("Failed to get state: {0}", e) 1556 return ExtHandlerState.NotInstalled 1557 1558 def set_handler_status(self, status="NotReady", message="", code=0): 1559 state_dir = self.get_conf_dir() 1560 1561 handler_status = ExtHandlerStatus() 1562 handler_status.name = self.ext_handler.name 1563 handler_status.version = str(self.ext_handler.properties.version) 1564 handler_status.message = message 1565 handler_status.code = code 1566 handler_status.status = status 1567 status_file = os.path.join(state_dir, "HandlerStatus") 1568 1569 try: 1570 handler_status_json = json.dumps(get_properties(handler_status)) 1571 if handler_status_json is not None: 1572 if not os.path.exists(state_dir): 1573 fileutil.mkdir(state_dir, mode=0o700) 1574 fileutil.write_file(status_file, handler_status_json) 1575 else: 1576 self.logger.error("Failed to create JSON document of handler status for {0} version {1}".format( 1577 self.ext_handler.name, 1578 self.ext_handler.properties.version)) 1579 except (IOError, ValueError, ProtocolError) as error: 1580 fileutil.clean_ioerror(error, paths=[status_file]) 1581 self.logger.error("Failed to save handler status: {0}, {1}", ustr(error), traceback.format_exc()) 1582 1583 def get_handler_status(self): 1584 state_dir = self.get_conf_dir() 1585 status_file = os.path.join(state_dir, "HandlerStatus") 1586 if not os.path.isfile(status_file): 1587 return None 1588 1589 handler_status_contents = "" 1590 try: 1591 handler_status_contents = fileutil.read_file(status_file) 1592 data = json.loads(handler_status_contents) 1593 handler_status = ExtHandlerStatus() 1594 set_properties("ExtHandlerStatus", handler_status, data) 1595 return handler_status 1596 except (IOError, ValueError) as error: 1597 self.logger.error("Failed to get handler status: {0}", error) 1598 except Exception as error: 1599 error_msg = "Failed to get handler status message: {0}.\n Contents of file: {1}".format( 1600 ustr(error), handler_status_contents).replace('"', '\'') 1601 add_periodic( 1602 delta=logger.EVERY_HOUR, 1603 name=AGENT_NAME, 1604 version=CURRENT_VERSION, 1605 op=WALAEventOperation.ExtensionProcessing, 1606 is_success=False, 1607 message=error_msg) 1608 raise 1609 1610 return None 1611 1612 def get_extension_package_zipfile_name(self): 1613 return "{0}__{1}{2}".format(self.ext_handler.name, 1614 self.ext_handler.properties.version, 1615 HANDLER_PKG_EXT) 1616 1617 def get_full_name(self): 1618 return "{0}-{1}".format(self.ext_handler.name, 1619 self.ext_handler.properties.version) 1620 1621 def get_base_dir(self): 1622 return os.path.join(conf.get_lib_dir(), self.get_full_name()) 1623 1624 def get_status_dir(self): 1625 return os.path.join(self.get_base_dir(), "status") 1626 1627 def get_conf_dir(self): 1628 return os.path.join(self.get_base_dir(), 'config') 1629 1630 def get_extension_events_dir(self): 1631 return os.path.join(self.get_log_dir(), EVENTS_DIRECTORY) 1632 1633 def get_heartbeat_file(self): 1634 return os.path.join(self.get_base_dir(), 'heartbeat.log') 1635 1636 def get_manifest_file(self): 1637 return os.path.join(self.get_base_dir(), 'HandlerManifest.json') 1638 1639 def get_env_file(self): 1640 return os.path.join(self.get_base_dir(), HandlerEnvironment.fileName) 1641 1642 def get_log_dir(self): 1643 return os.path.join(conf.get_ext_log_dir(), self.ext_handler.name) 1644 1645 def get_seq_no(self): 1646 runtime_settings = self.ext_handler.properties.extensions 1647 # If no runtime_settings available for this ext_handler, then return 0 (this is the behavior we follow 1648 # for update_settings) 1649 if not runtime_settings or len(runtime_settings) == 0: 1650 return "0" 1651 # Currently for every runtime settings we use the same sequence number 1652 # (Check : def parse_plugin_settings(self, ext_handler, plugin_settings) in wire.py) 1653 # Will have to revisit once the feature to enable multiple runtime settings is rolled out by CRP 1654 return self.ext_handler.properties.extensions[0].sequenceNumber 1655 1656 @staticmethod 1657 def _read_and_parse_json_status_file(ext_status_file): 1658 failed_to_read = False 1659 failed_to_parse_json = False 1660 raised_exception = None 1661 data_str = None 1662 data = None 1663 1664 for attempt in range(_NUM_OF_STATUS_FILE_RETRIES): # pylint: disable=W0612 1665 try: 1666 data_str = fileutil.read_file(ext_status_file) 1667 data = json.loads(data_str) 1668 break 1669 except IOError as e: 1670 failed_to_read = True 1671 raised_exception = e 1672 except (ValueError, TypeError) as e: 1673 failed_to_parse_json = True 1674 raised_exception = e 1675 time.sleep(_STATUS_FILE_RETRY_DELAY) 1676 1677 if failed_to_read: 1678 raise ExtensionStatusError(msg=ustr(raised_exception), inner=raised_exception, 1679 code=ExtensionStatusError.CouldNotReadStatusFile) 1680 elif failed_to_parse_json: 1681 raise ExtensionStatusError(msg=ustr(raised_exception), inner=raised_exception, 1682 code=ExtensionStatusError.InvalidJsonFile) 1683 else: 1684 return data_str, data 1685 1686 def _process_substatus_list(self, substatus_list, current_status_size=0): 1687 processed_substatus = [] 1688 1689 # Truncating the substatus to reduce the size, and preserve other fields of the text 1690 for substatus in substatus_list: 1691 substatus.name, field_size = self._truncate_message(substatus.name, _MAX_SUBSTATUS_FIELD_LENGTH) 1692 current_status_size += field_size 1693 1694 substatus.message, field_size = self._truncate_message(substatus.message, _MAX_SUBSTATUS_FIELD_LENGTH) 1695 current_status_size += field_size 1696 1697 if current_status_size <= _MAX_STATUS_FILE_SIZE_IN_BYTES: 1698 processed_substatus.append(substatus) 1699 else: 1700 break 1701 1702 return processed_substatus 1703 1704 @staticmethod 1705 def _truncate_message(field, truncate_size=_MAX_SUBSTATUS_FIELD_LENGTH): # pylint: disable=R1710 1706 if field is None: # pylint: disable=R1705 1707 return 1708 else: 1709 truncated_field = field if len(field) < truncate_size else field[:truncate_size] + _TRUNCATED_SUFFIX 1710 return truncated_field, len(truncated_field) 1711 1712 1713class HandlerEnvironment(object): 1714 # HandlerEnvironment.json schema version 1715 schemaVersion = 1.0 1716 fileName = "HandlerEnvironment.json" 1717 handlerEnvironment = "handlerEnvironment" 1718 logFolder = "logFolder" 1719 configFolder = "configFolder" 1720 statusFolder = "statusFolder" 1721 heartbeatFile = "heartbeatFile" 1722 eventsFolder = "eventsFolder_preview" 1723 name = "name" 1724 version = "version" 1725 1726 1727class HandlerManifest(object): 1728 def __init__(self, data): 1729 if data is None or data['handlerManifest'] is None: 1730 raise ExtensionError('Malformed manifest file.') 1731 self.data = data 1732 1733 def get_name(self): 1734 return self.data["name"] 1735 1736 def get_version(self): 1737 return self.data["version"] 1738 1739 def get_install_command(self): 1740 return self.data['handlerManifest']["installCommand"] 1741 1742 def get_uninstall_command(self): 1743 return self.data['handlerManifest']["uninstallCommand"] 1744 1745 def get_update_command(self): 1746 return self.data['handlerManifest']["updateCommand"] 1747 1748 def get_enable_command(self): 1749 return self.data['handlerManifest']["enableCommand"] 1750 1751 def get_disable_command(self): 1752 return self.data['handlerManifest']["disableCommand"] 1753 1754 def is_report_heartbeat(self): 1755 return self.data['handlerManifest'].get('reportHeartbeat', False) 1756 1757 def is_update_with_install(self): 1758 update_mode = self.data['handlerManifest'].get('updateMode') 1759 if update_mode is None: 1760 return True 1761 return update_mode.lower() == "updatewithinstall" 1762 1763 def is_continue_on_update_failure(self): 1764 return self.data['handlerManifest'].get('continueOnUpdateFailure', False) 1765 1766 1767class ExtensionStatusError(ExtensionError): 1768 """ 1769 When extension failed to provide a valid status file 1770 """ 1771 CouldNotReadStatusFile = 1 1772 InvalidJsonFile = 2 1773 StatusFileMalformed = 3 1774 MaxSizeExceeded = 4 1775 1776 def __init__(self, msg=None, inner=None, code=-1): # pylint: disable=W0235 1777 super(ExtensionStatusError, self).__init__(msg, inner, code) 1778