azurelinuxagent/common/protocol/healthservice.py (109 lines of code) (raw):
# Microsoft Azure Linux Agent
#
# Copyright 2018 Microsoft Corporation
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# Requires Python 2.6+ and Openssl 1.0+
#
import json
from azurelinuxagent.common import logger
from azurelinuxagent.common.exception import HttpError
from azurelinuxagent.common.future import ustr
from azurelinuxagent.common.utils import restutil
from azurelinuxagent.common.version import AGENT_NAME, CURRENT_VERSION
class Observation(object):
def __init__(self, name, is_healthy, description='', value=''):
if name is None:
raise ValueError("Observation name must be provided")
if is_healthy is None:
raise ValueError("Observation health must be provided")
if value is None:
value = ''
if description is None:
description = ''
self.name = name
self.is_healthy = is_healthy
self.description = description
self.value = value
@property
def as_obj(self):
return {
"ObservationName": self.name[:64],
"IsHealthy": self.is_healthy,
"Description": self.description[:128],
"Value": self.value[:128]
}
class HealthService(object):
ENDPOINT = 'http://{0}:80/HealthService'
API = 'reporttargethealth'
VERSION = "1.0"
OBSERVER_NAME = 'WALinuxAgent'
HOST_PLUGIN_HEARTBEAT_OBSERVATION_NAME = 'GuestAgentPluginHeartbeat'
HOST_PLUGIN_STATUS_OBSERVATION_NAME = 'GuestAgentPluginStatus'
HOST_PLUGIN_VERSIONS_OBSERVATION_NAME = 'GuestAgentPluginVersions'
HOST_PLUGIN_ARTIFACT_OBSERVATION_NAME = 'GuestAgentPluginArtifact'
IMDS_OBSERVATION_NAME = 'InstanceMetadataHeartbeat'
MAX_OBSERVATIONS = 10
def __init__(self, endpoint):
self.endpoint = HealthService.ENDPOINT.format(endpoint)
self.api = HealthService.API
self.version = HealthService.VERSION
self.source = HealthService.OBSERVER_NAME
self.observations = []
@property
def as_json(self):
data = {
"Api": self.api,
"Version": self.version,
"Source": self.source,
"Observations": [o.as_obj for o in self.observations]
}
return json.dumps(data)
def report_host_plugin_heartbeat(self, is_healthy):
"""
Reports a signal for /health
:param is_healthy: whether the call succeeded
"""
self._observe(name=HealthService.HOST_PLUGIN_HEARTBEAT_OBSERVATION_NAME,
is_healthy=is_healthy)
self._report()
def report_host_plugin_versions(self, is_healthy, response):
"""
Reports a signal for /versions
:param is_healthy: whether the api call succeeded
:param response: debugging information for failures
"""
self._observe(name=HealthService.HOST_PLUGIN_VERSIONS_OBSERVATION_NAME,
is_healthy=is_healthy,
value=response)
self._report()
def report_host_plugin_extension_artifact(self, is_healthy, source, response):
"""
Reports a signal for /extensionArtifact
:param is_healthy: whether the api call succeeded
:param source: specifies the api caller for debugging failures
:param response: debugging information for failures
"""
self._observe(name=HealthService.HOST_PLUGIN_ARTIFACT_OBSERVATION_NAME,
is_healthy=is_healthy,
description=source,
value=response)
self._report()
def report_host_plugin_status(self, is_healthy, response):
"""
Reports a signal for /status
:param is_healthy: whether the api call succeeded
:param response: debugging information for failures
"""
self._observe(name=HealthService.HOST_PLUGIN_STATUS_OBSERVATION_NAME,
is_healthy=is_healthy,
value=response)
self._report()
def report_imds_status(self, is_healthy, response):
"""
Reports a signal for /metadata/instance
:param is_healthy: whether the api call succeeded and returned valid data
:param response: debugging information for failures
"""
self._observe(name=HealthService.IMDS_OBSERVATION_NAME,
is_healthy=is_healthy,
value=response)
self._report()
def _observe(self, name, is_healthy, value='', description=''):
# ensure we keep the list size within bounds
if len(self.observations) >= HealthService.MAX_OBSERVATIONS:
del self.observations[:HealthService.MAX_OBSERVATIONS-1]
self.observations.append(Observation(name=name,
is_healthy=is_healthy,
value=value,
description=description))
def _report(self):
logger.verbose('HealthService: report observations')
try:
restutil.http_post(self.endpoint, self.as_json, headers={'Content-Type': 'application/json'})
logger.verbose('HealthService: Reported observations to {0}: {1}', self.endpoint, self.as_json)
except HttpError as e:
logger.warn("HealthService: could not report observations: {0}", ustr(e))
finally:
# report any failures via telemetry
self._report_failures()
# these signals are not timestamped, so there is no value in persisting data
del self.observations[:]
def _report_failures(self):
try:
logger.verbose("HealthService: report failures as telemetry")
from azurelinuxagent.common.event import add_event, WALAEventOperation
for o in self.observations:
if not o.is_healthy:
add_event(AGENT_NAME,
version=CURRENT_VERSION,
op=WALAEventOperation.HealthObservation,
is_success=False,
message=json.dumps(o.as_obj))
except Exception as e:
logger.verbose("HealthService: could not report failures: {0}".format(ustr(e)))