1# Microsoft Azure Linux Agent 2# 3# Copyright 2018 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 json 21 22from azurelinuxagent.common import logger 23from azurelinuxagent.common.exception import HttpError 24from azurelinuxagent.common.future import ustr 25from azurelinuxagent.common.utils import restutil 26from azurelinuxagent.common.version import AGENT_NAME, CURRENT_VERSION 27 28 29class Observation(object): 30 def __init__(self, name, is_healthy, description='', value=''): 31 if name is None: 32 raise ValueError("Observation name must be provided") 33 34 if is_healthy is None: 35 raise ValueError("Observation health must be provided") 36 37 if value is None: 38 value = '' 39 40 if description is None: 41 description = '' 42 43 self.name = name 44 self.is_healthy = is_healthy 45 self.description = description 46 self.value = value 47 48 @property 49 def as_obj(self): 50 return { 51 "ObservationName": self.name[:64], 52 "IsHealthy": self.is_healthy, 53 "Description": self.description[:128], 54 "Value": self.value[:128] 55 } 56 57 58class HealthService(object): 59 60 ENDPOINT = 'http://{0}:80/HealthService' 61 API = 'reporttargethealth' 62 VERSION = "1.0" 63 OBSERVER_NAME = 'WALinuxAgent' 64 HOST_PLUGIN_HEARTBEAT_OBSERVATION_NAME = 'GuestAgentPluginHeartbeat' 65 HOST_PLUGIN_STATUS_OBSERVATION_NAME = 'GuestAgentPluginStatus' 66 HOST_PLUGIN_VERSIONS_OBSERVATION_NAME = 'GuestAgentPluginVersions' 67 HOST_PLUGIN_ARTIFACT_OBSERVATION_NAME = 'GuestAgentPluginArtifact' 68 IMDS_OBSERVATION_NAME = 'InstanceMetadataHeartbeat' 69 MAX_OBSERVATIONS = 10 70 71 def __init__(self, endpoint): 72 self.endpoint = HealthService.ENDPOINT.format(endpoint) 73 self.api = HealthService.API 74 self.version = HealthService.VERSION 75 self.source = HealthService.OBSERVER_NAME 76 self.observations = list() 77 78 @property 79 def as_json(self): 80 data = { 81 "Api": self.api, 82 "Version": self.version, 83 "Source": self.source, 84 "Observations": [o.as_obj for o in self.observations] 85 } 86 return json.dumps(data) 87 88 def report_host_plugin_heartbeat(self, is_healthy): 89 """ 90 Reports a signal for /health 91 :param is_healthy: whether the call succeeded 92 """ 93 self._observe(name=HealthService.HOST_PLUGIN_HEARTBEAT_OBSERVATION_NAME, 94 is_healthy=is_healthy) 95 self._report() 96 97 def report_host_plugin_versions(self, is_healthy, response): 98 """ 99 Reports a signal for /versions 100 :param is_healthy: whether the api call succeeded 101 :param response: debugging information for failures 102 """ 103 self._observe(name=HealthService.HOST_PLUGIN_VERSIONS_OBSERVATION_NAME, 104 is_healthy=is_healthy, 105 value=response) 106 self._report() 107 108 def report_host_plugin_extension_artifact(self, is_healthy, source, response): 109 """ 110 Reports a signal for /extensionArtifact 111 :param is_healthy: whether the api call succeeded 112 :param source: specifies the api caller for debugging failures 113 :param response: debugging information for failures 114 """ 115 self._observe(name=HealthService.HOST_PLUGIN_ARTIFACT_OBSERVATION_NAME, 116 is_healthy=is_healthy, 117 description=source, 118 value=response) 119 self._report() 120 121 def report_host_plugin_status(self, is_healthy, response): 122 """ 123 Reports a signal for /status 124 :param is_healthy: whether the api call succeeded 125 :param response: debugging information for failures 126 """ 127 self._observe(name=HealthService.HOST_PLUGIN_STATUS_OBSERVATION_NAME, 128 is_healthy=is_healthy, 129 value=response) 130 self._report() 131 132 def report_imds_status(self, is_healthy, response): 133 """ 134 Reports a signal for /metadata/instance 135 :param is_healthy: whether the api call succeeded and returned valid data 136 :param response: debugging information for failures 137 """ 138 self._observe(name=HealthService.IMDS_OBSERVATION_NAME, 139 is_healthy=is_healthy, 140 value=response) 141 self._report() 142 143 def _observe(self, name, is_healthy, value='', description=''): 144 # ensure we keep the list size within bounds 145 if len(self.observations) >= HealthService.MAX_OBSERVATIONS: 146 del self.observations[:HealthService.MAX_OBSERVATIONS-1] 147 self.observations.append(Observation(name=name, 148 is_healthy=is_healthy, 149 value=value, 150 description=description)) 151 152 def _report(self): 153 logger.verbose('HealthService: report observations') 154 try: 155 restutil.http_post(self.endpoint, self.as_json, headers={'Content-Type': 'application/json'}) 156 logger.verbose('HealthService: Reported observations to {0}: {1}', self.endpoint, self.as_json) 157 except HttpError as e: 158 logger.warn("HealthService: could not report observations: {0}", ustr(e)) 159 finally: 160 # report any failures via telemetry 161 self._report_failures() 162 # these signals are not timestamped, so there is no value in persisting data 163 del self.observations[:] 164 165 def _report_failures(self): 166 try: 167 logger.verbose("HealthService: report failures as telemetry") 168 from azurelinuxagent.common.event import add_event, WALAEventOperation 169 for o in self.observations: 170 if not o.is_healthy: 171 add_event(AGENT_NAME, 172 version=CURRENT_VERSION, 173 op=WALAEventOperation.HealthObservation, 174 is_success=False, 175 message=json.dumps(o.as_obj)) 176 except Exception as e: 177 logger.verbose("HealthService: could not report failures: {0}".format(ustr(e))) 178