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