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