|
Packit Service |
a04d08 |
# This file is part of cloud-init. See LICENSE file for license information.
|
|
Packit Service |
9bfd13 |
import base64
|
|
Packit Service |
a04d08 |
import json
|
|
Packit Service |
a04d08 |
import logging
|
|
Packit Service |
a04d08 |
import os
|
|
Packit Service |
a04d08 |
import re
|
|
Packit Service |
a04d08 |
import socket
|
|
Packit Service |
a04d08 |
import struct
|
|
Packit Service |
a04d08 |
import time
|
|
Packit Service |
a04d08 |
import textwrap
|
|
Packit Service |
9bfd13 |
import zlib
|
|
Packit Service |
a04d08 |
|
|
Packit Service |
9bfd13 |
from cloudinit.settings import CFG_BUILTIN
|
|
Packit Service |
a04d08 |
from cloudinit.net import dhcp
|
|
Packit Service |
a04d08 |
from cloudinit import stages
|
|
Packit Service |
a04d08 |
from cloudinit import temp_utils
|
|
Packit Service |
a04d08 |
from contextlib import contextmanager
|
|
Packit Service |
a04d08 |
from xml.etree import ElementTree
|
|
Packit Service |
a04d08 |
|
|
Packit Service |
9bfd13 |
from cloudinit import subp
|
|
Packit Service |
a04d08 |
from cloudinit import url_helper
|
|
Packit Service |
a04d08 |
from cloudinit import util
|
|
Packit Service |
a04d08 |
from cloudinit import version
|
|
Packit Service |
a04d08 |
from cloudinit import distros
|
|
Packit Service |
a04d08 |
from cloudinit.reporting import events
|
|
Packit Service |
a04d08 |
from cloudinit.net.dhcp import EphemeralDHCPv4
|
|
Packit Service |
a04d08 |
from datetime import datetime
|
|
Packit Service |
a04d08 |
|
|
Packit Service |
a04d08 |
LOG = logging.getLogger(__name__)
|
|
Packit Service |
a04d08 |
|
|
Packit Service |
a04d08 |
# This endpoint matches the format as found in dhcp lease files, since this
|
|
Packit Service |
a04d08 |
# value is applied if the endpoint can't be found within a lease file
|
|
Packit Service |
a04d08 |
DEFAULT_WIRESERVER_ENDPOINT = "a8:3f:81:10"
|
|
Packit Service |
a04d08 |
|
|
Packit Service |
a04d08 |
BOOT_EVENT_TYPE = 'boot-telemetry'
|
|
Packit Service |
a04d08 |
SYSTEMINFO_EVENT_TYPE = 'system-info'
|
|
Packit Service |
a04d08 |
DIAGNOSTIC_EVENT_TYPE = 'diagnostic'
|
|
Packit Service |
9bfd13 |
COMPRESSED_EVENT_TYPE = 'compressed'
|
|
Packit Service |
9bfd13 |
# Maximum number of bytes of the cloud-init.log file that can be dumped to KVP
|
|
Packit Service |
9bfd13 |
# at once. This number is based on the analysis done on a large sample of
|
|
Packit Service |
9bfd13 |
# cloud-init.log files where the P95 of the file sizes was 537KB and the time
|
|
Packit Service |
9bfd13 |
# consumed to dump 500KB file was (P95:76, P99:233, P99.9:1170) in ms
|
|
Packit Service |
9bfd13 |
MAX_LOG_TO_KVP_LENGTH = 512000
|
|
Packit Service |
9bfd13 |
# Marker file to indicate whether cloud-init.log is pushed to KVP
|
|
Packit Service |
9bfd13 |
LOG_PUSHED_TO_KVP_MARKER_FILE = '/var/lib/cloud/data/log_pushed_to_kvp'
|
|
Packit Service |
a04d08 |
azure_ds_reporter = events.ReportEventStack(
|
|
Packit Service |
a04d08 |
name="azure-ds",
|
|
Packit Service |
a04d08 |
description="initialize reporter for azure ds",
|
|
Packit Service |
a04d08 |
reporting_enabled=True)
|
|
Packit Service |
a04d08 |
|
|
Packit Service |
a04d08 |
|
|
Packit Service |
a04d08 |
def azure_ds_telemetry_reporter(func):
|
|
Packit Service |
a04d08 |
def impl(*args, **kwargs):
|
|
Packit Service |
a04d08 |
with events.ReportEventStack(
|
|
Packit Service |
a04d08 |
name=func.__name__,
|
|
Packit Service |
a04d08 |
description=func.__name__,
|
|
Packit Service |
a04d08 |
parent=azure_ds_reporter):
|
|
Packit Service |
a04d08 |
return func(*args, **kwargs)
|
|
Packit Service |
a04d08 |
return impl
|
|
Packit Service |
a04d08 |
|
|
Packit Service |
a04d08 |
|
|
Packit Service |
a04d08 |
def is_byte_swapped(previous_id, current_id):
|
|
Packit Service |
a04d08 |
"""
|
|
Packit Service |
a04d08 |
Azure stores the instance ID with an incorrect byte ordering for the
|
|
Packit Service |
a04d08 |
first parts. This corrects the byte order such that it is consistent with
|
|
Packit Service |
a04d08 |
that returned by the metadata service.
|
|
Packit Service |
a04d08 |
"""
|
|
Packit Service |
a04d08 |
if previous_id == current_id:
|
|
Packit Service |
a04d08 |
return False
|
|
Packit Service |
a04d08 |
|
|
Packit Service |
a04d08 |
def swap_bytestring(s, width=2):
|
|
Packit Service |
a04d08 |
dd = [byte for byte in textwrap.wrap(s, 2)]
|
|
Packit Service |
a04d08 |
dd.reverse()
|
|
Packit Service |
a04d08 |
return ''.join(dd)
|
|
Packit Service |
a04d08 |
|
|
Packit Service |
a04d08 |
parts = current_id.split('-')
|
|
Packit Service |
9bfd13 |
swapped_id = '-'.join(
|
|
Packit Service |
9bfd13 |
[
|
|
Packit Service |
a04d08 |
swap_bytestring(parts[0]),
|
|
Packit Service |
a04d08 |
swap_bytestring(parts[1]),
|
|
Packit Service |
a04d08 |
swap_bytestring(parts[2]),
|
|
Packit Service |
a04d08 |
parts[3],
|
|
Packit Service |
a04d08 |
parts[4]
|
|
Packit Service |
9bfd13 |
]
|
|
Packit Service |
9bfd13 |
)
|
|
Packit Service |
a04d08 |
|
|
Packit Service |
a04d08 |
return previous_id == swapped_id
|
|
Packit Service |
a04d08 |
|
|
Packit Service |
a04d08 |
|
|
Packit Service |
a04d08 |
@azure_ds_telemetry_reporter
|
|
Packit Service |
a04d08 |
def get_boot_telemetry():
|
|
Packit Service |
a04d08 |
"""Report timestamps related to kernel initialization and systemd
|
|
Packit Service |
a04d08 |
activation of cloud-init"""
|
|
Packit Service |
a04d08 |
if not distros.uses_systemd():
|
|
Packit Service |
a04d08 |
raise RuntimeError(
|
|
Packit Service |
a04d08 |
"distro not using systemd, skipping boot telemetry")
|
|
Packit Service |
a04d08 |
|
|
Packit Service |
a04d08 |
LOG.debug("Collecting boot telemetry")
|
|
Packit Service |
a04d08 |
try:
|
|
Packit Service |
a04d08 |
kernel_start = float(time.time()) - float(util.uptime())
|
|
Packit Service |
9bfd13 |
except ValueError as e:
|
|
Packit Service |
9bfd13 |
raise RuntimeError(
|
|
Packit Service |
9bfd13 |
"Failed to determine kernel start timestamp"
|
|
Packit Service |
9bfd13 |
) from e
|
|
Packit Service |
a04d08 |
|
|
Packit Service |
a04d08 |
try:
|
|
Packit Service |
9bfd13 |
out, _ = subp.subp(['/bin/systemctl',
|
|
Packit Service |
a04d08 |
'show', '-p',
|
|
Packit Service |
a04d08 |
'UserspaceTimestampMonotonic'],
|
|
Packit Service |
a04d08 |
capture=True)
|
|
Packit Service |
a04d08 |
tsm = None
|
|
Packit Service |
a04d08 |
if out and '=' in out:
|
|
Packit Service |
a04d08 |
tsm = out.split("=")[1]
|
|
Packit Service |
a04d08 |
|
|
Packit Service |
a04d08 |
if not tsm:
|
|
Packit Service |
a04d08 |
raise RuntimeError("Failed to parse "
|
|
Packit Service |
a04d08 |
"UserspaceTimestampMonotonic from systemd")
|
|
Packit Service |
a04d08 |
|
|
Packit Service |
a04d08 |
user_start = kernel_start + (float(tsm) / 1000000)
|
|
Packit Service |
9bfd13 |
except subp.ProcessExecutionError as e:
|
|
Packit Service |
9bfd13 |
raise RuntimeError(
|
|
Packit Service |
9bfd13 |
"Failed to get UserspaceTimestampMonotonic: %s" % e
|
|
Packit Service |
9bfd13 |
) from e
|
|
Packit Service |
a04d08 |
except ValueError as e:
|
|
Packit Service |
9bfd13 |
raise RuntimeError(
|
|
Packit Service |
9bfd13 |
"Failed to parse UserspaceTimestampMonotonic from systemd: %s" % e
|
|
Packit Service |
9bfd13 |
) from e
|
|
Packit Service |
a04d08 |
|
|
Packit Service |
a04d08 |
try:
|
|
Packit Service |
9bfd13 |
out, _ = subp.subp(['/bin/systemctl', 'show',
|
|
Packit Service |
a04d08 |
'cloud-init-local', '-p',
|
|
Packit Service |
a04d08 |
'InactiveExitTimestampMonotonic'],
|
|
Packit Service |
a04d08 |
capture=True)
|
|
Packit Service |
a04d08 |
tsm = None
|
|
Packit Service |
a04d08 |
if out and '=' in out:
|
|
Packit Service |
a04d08 |
tsm = out.split("=")[1]
|
|
Packit Service |
a04d08 |
if not tsm:
|
|
Packit Service |
a04d08 |
raise RuntimeError("Failed to parse "
|
|
Packit Service |
a04d08 |
"InactiveExitTimestampMonotonic from systemd")
|
|
Packit Service |
a04d08 |
|
|
Packit Service |
a04d08 |
cloudinit_activation = kernel_start + (float(tsm) / 1000000)
|
|
Packit Service |
9bfd13 |
except subp.ProcessExecutionError as e:
|
|
Packit Service |
9bfd13 |
raise RuntimeError(
|
|
Packit Service |
9bfd13 |
"Failed to get InactiveExitTimestampMonotonic: %s" % e
|
|
Packit Service |
9bfd13 |
) from e
|
|
Packit Service |
a04d08 |
except ValueError as e:
|
|
Packit Service |
9bfd13 |
raise RuntimeError(
|
|
Packit Service |
9bfd13 |
"Failed to parse InactiveExitTimestampMonotonic from systemd: %s"
|
|
Packit Service |
9bfd13 |
% e
|
|
Packit Service |
9bfd13 |
) from e
|
|
Packit Service |
a04d08 |
|
|
Packit Service |
a04d08 |
evt = events.ReportingEvent(
|
|
Packit Service |
a04d08 |
BOOT_EVENT_TYPE, 'boot-telemetry',
|
|
Packit Service |
a04d08 |
"kernel_start=%s user_start=%s cloudinit_activation=%s" %
|
|
Packit Service |
a04d08 |
(datetime.utcfromtimestamp(kernel_start).isoformat() + 'Z',
|
|
Packit Service |
a04d08 |
datetime.utcfromtimestamp(user_start).isoformat() + 'Z',
|
|
Packit Service |
a04d08 |
datetime.utcfromtimestamp(cloudinit_activation).isoformat() + 'Z'),
|
|
Packit Service |
a04d08 |
events.DEFAULT_EVENT_ORIGIN)
|
|
Packit Service |
a04d08 |
events.report_event(evt)
|
|
Packit Service |
a04d08 |
|
|
Packit Service |
a04d08 |
# return the event for unit testing purpose
|
|
Packit Service |
a04d08 |
return evt
|
|
Packit Service |
a04d08 |
|
|
Packit Service |
a04d08 |
|
|
Packit Service |
a04d08 |
@azure_ds_telemetry_reporter
|
|
Packit Service |
a04d08 |
def get_system_info():
|
|
Packit Service |
a04d08 |
"""Collect and report system information"""
|
|
Packit Service |
a04d08 |
info = util.system_info()
|
|
Packit Service |
a04d08 |
evt = events.ReportingEvent(
|
|
Packit Service |
a04d08 |
SYSTEMINFO_EVENT_TYPE, 'system information',
|
|
Packit Service |
a04d08 |
"cloudinit_version=%s, kernel_version=%s, variant=%s, "
|
|
Packit Service |
a04d08 |
"distro_name=%s, distro_version=%s, flavor=%s, "
|
|
Packit Service |
a04d08 |
"python_version=%s" %
|
|
Packit Service |
a04d08 |
(version.version_string(), info['release'], info['variant'],
|
|
Packit Service |
a04d08 |
info['dist'][0], info['dist'][1], info['dist'][2],
|
|
Packit Service |
a04d08 |
info['python']), events.DEFAULT_EVENT_ORIGIN)
|
|
Packit Service |
a04d08 |
events.report_event(evt)
|
|
Packit Service |
a04d08 |
|
|
Packit Service |
a04d08 |
# return the event for unit testing purpose
|
|
Packit Service |
a04d08 |
return evt
|
|
Packit Service |
a04d08 |
|
|
Packit Service |
a04d08 |
|
|
Packit Service |
a04d08 |
def report_diagnostic_event(str):
|
|
Packit Service |
a04d08 |
"""Report a diagnostic event"""
|
|
Packit Service |
a04d08 |
evt = events.ReportingEvent(
|
|
Packit Service |
a04d08 |
DIAGNOSTIC_EVENT_TYPE, 'diagnostic message',
|
|
Packit Service |
a04d08 |
str, events.DEFAULT_EVENT_ORIGIN)
|
|
Packit Service |
a04d08 |
events.report_event(evt)
|
|
Packit Service |
a04d08 |
|
|
Packit Service |
a04d08 |
# return the event for unit testing purpose
|
|
Packit Service |
a04d08 |
return evt
|
|
Packit Service |
a04d08 |
|
|
Packit Service |
a04d08 |
|
|
Packit Service |
9bfd13 |
def report_compressed_event(event_name, event_content):
|
|
Packit Service |
9bfd13 |
"""Report a compressed event"""
|
|
Packit Service |
9bfd13 |
compressed_data = base64.encodebytes(zlib.compress(event_content))
|
|
Packit Service |
9bfd13 |
event_data = {"encoding": "gz+b64",
|
|
Packit Service |
9bfd13 |
"data": compressed_data.decode('ascii')}
|
|
Packit Service |
9bfd13 |
evt = events.ReportingEvent(
|
|
Packit Service |
9bfd13 |
COMPRESSED_EVENT_TYPE, event_name,
|
|
Packit Service |
9bfd13 |
json.dumps(event_data),
|
|
Packit Service |
9bfd13 |
events.DEFAULT_EVENT_ORIGIN)
|
|
Packit Service |
9bfd13 |
events.report_event(evt,
|
|
Packit Service |
9bfd13 |
excluded_handler_types={"log", "print", "webhook"})
|
|
Packit Service |
9bfd13 |
|
|
Packit Service |
9bfd13 |
# return the event for unit testing purpose
|
|
Packit Service |
9bfd13 |
return evt
|
|
Packit Service |
9bfd13 |
|
|
Packit Service |
9bfd13 |
|
|
Packit Service |
9bfd13 |
@azure_ds_telemetry_reporter
|
|
Packit Service |
9bfd13 |
def push_log_to_kvp(file_name=CFG_BUILTIN['def_log_file']):
|
|
Packit Service |
9bfd13 |
"""Push a portion of cloud-init.log file or the whole file to KVP
|
|
Packit Service |
9bfd13 |
based on the file size.
|
|
Packit Service |
9bfd13 |
If called more than once, it skips pushing the log file to KVP again."""
|
|
Packit Service |
9bfd13 |
|
|
Packit Service |
9bfd13 |
log_pushed_to_kvp = bool(os.path.isfile(LOG_PUSHED_TO_KVP_MARKER_FILE))
|
|
Packit Service |
9bfd13 |
if log_pushed_to_kvp:
|
|
Packit Service |
9bfd13 |
report_diagnostic_event("cloud-init.log is already pushed to KVP")
|
|
Packit Service |
9bfd13 |
return
|
|
Packit Service |
9bfd13 |
|
|
Packit Service |
9bfd13 |
LOG.debug("Dumping cloud-init.log file to KVP")
|
|
Packit Service |
9bfd13 |
try:
|
|
Packit Service |
9bfd13 |
with open(file_name, "rb") as f:
|
|
Packit Service |
9bfd13 |
f.seek(0, os.SEEK_END)
|
|
Packit Service |
9bfd13 |
seek_index = max(f.tell() - MAX_LOG_TO_KVP_LENGTH, 0)
|
|
Packit Service |
9bfd13 |
report_diagnostic_event(
|
|
Packit Service |
9bfd13 |
"Dumping last {} bytes of cloud-init.log file to KVP".format(
|
|
Packit Service |
9bfd13 |
f.tell() - seek_index))
|
|
Packit Service |
9bfd13 |
f.seek(seek_index, os.SEEK_SET)
|
|
Packit Service |
9bfd13 |
report_compressed_event("cloud-init.log", f.read())
|
|
Packit Service |
9bfd13 |
util.write_file(LOG_PUSHED_TO_KVP_MARKER_FILE, '')
|
|
Packit Service |
9bfd13 |
except Exception as ex:
|
|
Packit Service |
9bfd13 |
report_diagnostic_event("Exception when dumping log file: %s" %
|
|
Packit Service |
9bfd13 |
repr(ex))
|
|
Packit Service |
9bfd13 |
|
|
Packit Service |
9bfd13 |
|
|
Packit Service |
a04d08 |
@contextmanager
|
|
Packit Service |
a04d08 |
def cd(newdir):
|
|
Packit Service |
a04d08 |
prevdir = os.getcwd()
|
|
Packit Service |
a04d08 |
os.chdir(os.path.expanduser(newdir))
|
|
Packit Service |
a04d08 |
try:
|
|
Packit Service |
a04d08 |
yield
|
|
Packit Service |
a04d08 |
finally:
|
|
Packit Service |
a04d08 |
os.chdir(prevdir)
|
|
Packit Service |
a04d08 |
|
|
Packit Service |
a04d08 |
|
|
Packit Service |
a04d08 |
def _get_dhcp_endpoint_option_name():
|
|
Packit Service |
a04d08 |
if util.is_FreeBSD():
|
|
Packit Service |
a04d08 |
azure_endpoint = "option-245"
|
|
Packit Service |
a04d08 |
else:
|
|
Packit Service |
a04d08 |
azure_endpoint = "unknown-245"
|
|
Packit Service |
a04d08 |
return azure_endpoint
|
|
Packit Service |
a04d08 |
|
|
Packit Service |
a04d08 |
|
|
Packit Service |
9bfd13 |
class AzureEndpointHttpClient:
|
|
Packit Service |
a04d08 |
|
|
Packit Service |
a04d08 |
headers = {
|
|
Packit Service |
a04d08 |
'x-ms-agent-name': 'WALinuxAgent',
|
|
Packit Service |
a04d08 |
'x-ms-version': '2012-11-30',
|
|
Packit Service |
a04d08 |
}
|
|
Packit Service |
a04d08 |
|
|
Packit Service |
a04d08 |
def __init__(self, certificate):
|
|
Packit Service |
a04d08 |
self.extra_secure_headers = {
|
|
Packit Service |
a04d08 |
"x-ms-cipher-name": "DES_EDE3_CBC",
|
|
Packit Service |
a04d08 |
"x-ms-guest-agent-public-x509-cert": certificate,
|
|
Packit Service |
a04d08 |
}
|
|
Packit Service |
a04d08 |
|
|
Packit Service |
a04d08 |
def get(self, url, secure=False):
|
|
Packit Service |
a04d08 |
headers = self.headers
|
|
Packit Service |
a04d08 |
if secure:
|
|
Packit Service |
a04d08 |
headers = self.headers.copy()
|
|
Packit Service |
a04d08 |
headers.update(self.extra_secure_headers)
|
|
Packit Service |
9bfd13 |
return url_helper.readurl(url, headers=headers,
|
|
Packit Service |
9bfd13 |
timeout=5, retries=10, sec_between=5)
|
|
Packit Service |
a04d08 |
|
|
Packit Service |
a04d08 |
def post(self, url, data=None, extra_headers=None):
|
|
Packit Service |
a04d08 |
headers = self.headers
|
|
Packit Service |
a04d08 |
if extra_headers is not None:
|
|
Packit Service |
a04d08 |
headers = self.headers.copy()
|
|
Packit Service |
a04d08 |
headers.update(extra_headers)
|
|
Packit Service |
9bfd13 |
return url_helper.readurl(url, data=data, headers=headers,
|
|
Packit Service |
9bfd13 |
timeout=5, retries=10, sec_between=5)
|
|
Packit Service |
11b429 |
|
|
Packit Service |
11b429 |
|
|
Packit Service |
9bfd13 |
class InvalidGoalStateXMLException(Exception):
|
|
Packit Service |
9bfd13 |
"""Raised when GoalState XML is invalid or has missing data."""
|
|
Packit Service |
751c4a |
|
|
Packit Service |
a04d08 |
|
|
Packit Service |
9bfd13 |
class GoalState:
|
|
Packit Service |
11b429 |
|
|
Packit Service |
9bfd13 |
def __init__(self, unparsed_xml, azure_endpoint_client):
|
|
Packit Service |
9bfd13 |
"""Parses a GoalState XML string and returns a GoalState object.
|
|
Packit Service |
b1601c |
|
|
Packit Service |
9bfd13 |
@param unparsed_xml: string representing a GoalState XML.
|
|
Packit Service |
9bfd13 |
@param azure_endpoint_client: instance of AzureEndpointHttpClient
|
|
Packit Service |
9bfd13 |
@return: GoalState object representing the GoalState XML string.
|
|
Packit Service |
9bfd13 |
"""
|
|
Packit Service |
9bfd13 |
self.azure_endpoint_client = azure_endpoint_client
|
|
Packit Service |
b1601c |
|
|
Packit Service |
9bfd13 |
try:
|
|
Packit Service |
9bfd13 |
self.root = ElementTree.fromstring(unparsed_xml)
|
|
Packit Service |
9bfd13 |
except ElementTree.ParseError as e:
|
|
Packit Service |
9bfd13 |
msg = 'Failed to parse GoalState XML: %s'
|
|
Packit Service |
9bfd13 |
LOG.warning(msg, e)
|
|
Packit Service |
9bfd13 |
report_diagnostic_event(msg % (e,))
|
|
Packit Service |
9bfd13 |
raise
|
|
Packit Service |
9bfd13 |
|
|
Packit Service |
9bfd13 |
self.container_id = self._text_from_xpath('./Container/ContainerId')
|
|
Packit Service |
9bfd13 |
self.instance_id = self._text_from_xpath(
|
|
Packit Service |
b1601c |
'./Container/RoleInstanceList/RoleInstance/InstanceId')
|
|
Packit Service |
9bfd13 |
self.incarnation = self._text_from_xpath('./Incarnation')
|
|
Packit Service |
9bfd13 |
|
|
Packit Service |
9bfd13 |
for attr in ("container_id", "instance_id", "incarnation"):
|
|
Packit Service |
9bfd13 |
if getattr(self, attr) is None:
|
|
Packit Service |
9bfd13 |
msg = 'Missing %s in GoalState XML'
|
|
Packit Service |
9bfd13 |
LOG.warning(msg, attr)
|
|
Packit Service |
9bfd13 |
report_diagnostic_event(msg % (attr,))
|
|
Packit Service |
9bfd13 |
raise InvalidGoalStateXMLException(msg)
|
|
Packit Service |
9bfd13 |
|
|
Packit Service |
9bfd13 |
self.certificates_xml = None
|
|
Packit Service |
9bfd13 |
url = self._text_from_xpath(
|
|
Packit Service |
9bfd13 |
'./Container/RoleInstanceList/RoleInstance'
|
|
Packit Service |
9bfd13 |
'/Configuration/Certificates')
|
|
Packit Service |
9bfd13 |
if url is not None:
|
|
Packit Service |
9bfd13 |
with events.ReportEventStack(
|
|
Packit Service |
9bfd13 |
name="get-certificates-xml",
|
|
Packit Service |
9bfd13 |
description="get certificates xml",
|
|
Packit Service |
9bfd13 |
parent=azure_ds_reporter):
|
|
Packit Service |
9bfd13 |
self.certificates_xml = \
|
|
Packit Service |
9bfd13 |
self.azure_endpoint_client.get(
|
|
Packit Service |
9bfd13 |
url, secure=True).contents
|
|
Packit Service |
9bfd13 |
if self.certificates_xml is None:
|
|
Packit Service |
9bfd13 |
raise InvalidGoalStateXMLException(
|
|
Packit Service |
9bfd13 |
'Azure endpoint returned empty certificates xml.')
|
|
Packit Service |
11b429 |
|
|
Packit Service |
9bfd13 |
def _text_from_xpath(self, xpath):
|
|
Packit Service |
9bfd13 |
element = self.root.find(xpath)
|
|
Packit Service |
9bfd13 |
if element is not None:
|
|
Packit Service |
9bfd13 |
return element.text
|
|
Packit Service |
9bfd13 |
return None
|
|
Packit Service |
b1601c |
|
|
Packit Service |
b1601c |
|
|
Packit Service |
9bfd13 |
class OpenSSLManager:
|
|
Packit Service |
a04d08 |
|
|
Packit Service |
a04d08 |
certificate_names = {
|
|
Packit Service |
a04d08 |
'private_key': 'TransportPrivate.pem',
|
|
Packit Service |
a04d08 |
'certificate': 'TransportCert.pem',
|
|
Packit Service |
a04d08 |
}
|
|
Packit Service |
a04d08 |
|
|
Packit Service |
a04d08 |
def __init__(self):
|
|
Packit Service |
a04d08 |
self.tmpdir = temp_utils.mkdtemp()
|
|
Packit Service |
a04d08 |
self.certificate = None
|
|
Packit Service |
a04d08 |
self.generate_certificate()
|
|
Packit Service |
a04d08 |
|
|
Packit Service |
a04d08 |
def clean_up(self):
|
|
Packit Service |
a04d08 |
util.del_dir(self.tmpdir)
|
|
Packit Service |
a04d08 |
|
|
Packit Service |
a04d08 |
@azure_ds_telemetry_reporter
|
|
Packit Service |
a04d08 |
def generate_certificate(self):
|
|
Packit Service |
a04d08 |
LOG.debug('Generating certificate for communication with fabric...')
|
|
Packit Service |
a04d08 |
if self.certificate is not None:
|
|
Packit Service |
a04d08 |
LOG.debug('Certificate already generated.')
|
|
Packit Service |
a04d08 |
return
|
|
Packit Service |
a04d08 |
with cd(self.tmpdir):
|
|
Packit Service |
9bfd13 |
subp.subp([
|
|
Packit Service |
a04d08 |
'openssl', 'req', '-x509', '-nodes', '-subj',
|
|
Packit Service |
a04d08 |
'/CN=LinuxTransport', '-days', '32768', '-newkey', 'rsa:2048',
|
|
Packit Service |
a04d08 |
'-keyout', self.certificate_names['private_key'],
|
|
Packit Service |
a04d08 |
'-out', self.certificate_names['certificate'],
|
|
Packit Service |
a04d08 |
])
|
|
Packit Service |
a04d08 |
certificate = ''
|
|
Packit Service |
a04d08 |
for line in open(self.certificate_names['certificate']):
|
|
Packit Service |
a04d08 |
if "CERTIFICATE" not in line:
|
|
Packit Service |
a04d08 |
certificate += line.rstrip()
|
|
Packit Service |
a04d08 |
self.certificate = certificate
|
|
Packit Service |
a04d08 |
LOG.debug('New certificate generated.')
|
|
Packit Service |
a04d08 |
|
|
Packit Service |
a04d08 |
@staticmethod
|
|
Packit Service |
a04d08 |
@azure_ds_telemetry_reporter
|
|
Packit Service |
a04d08 |
def _run_x509_action(action, cert):
|
|
Packit Service |
a04d08 |
cmd = ['openssl', 'x509', '-noout', action]
|
|
Packit Service |
9bfd13 |
result, _ = subp.subp(cmd, data=cert)
|
|
Packit Service |
a04d08 |
return result
|
|
Packit Service |
a04d08 |
|
|
Packit Service |
a04d08 |
@azure_ds_telemetry_reporter
|
|
Packit Service |
a04d08 |
def _get_ssh_key_from_cert(self, certificate):
|
|
Packit Service |
a04d08 |
pub_key = self._run_x509_action('-pubkey', certificate)
|
|
Packit Service |
a04d08 |
keygen_cmd = ['ssh-keygen', '-i', '-m', 'PKCS8', '-f', '/dev/stdin']
|
|
Packit Service |
9bfd13 |
ssh_key, _ = subp.subp(keygen_cmd, data=pub_key)
|
|
Packit Service |
a04d08 |
return ssh_key
|
|
Packit Service |
a04d08 |
|
|
Packit Service |
a04d08 |
@azure_ds_telemetry_reporter
|
|
Packit Service |
a04d08 |
def _get_fingerprint_from_cert(self, certificate):
|
|
Packit Service |
a04d08 |
"""openssl x509 formats fingerprints as so:
|
|
Packit Service |
a04d08 |
'SHA1 Fingerprint=07:3E:19:D1:4D:1C:79:92:24:C6:A0:FD:8D:DA:\
|
|
Packit Service |
a04d08 |
B6:A8:BF:27:D4:73\n'
|
|
Packit Service |
a04d08 |
|
|
Packit Service |
a04d08 |
Azure control plane passes that fingerprint as so:
|
|
Packit Service |
a04d08 |
'073E19D14D1C799224C6A0FD8DDAB6A8BF27D473'
|
|
Packit Service |
a04d08 |
"""
|
|
Packit Service |
a04d08 |
raw_fp = self._run_x509_action('-fingerprint', certificate)
|
|
Packit Service |
a04d08 |
eq = raw_fp.find('=')
|
|
Packit Service |
a04d08 |
octets = raw_fp[eq+1:-1].split(':')
|
|
Packit Service |
a04d08 |
return ''.join(octets)
|
|
Packit Service |
a04d08 |
|
|
Packit Service |
a04d08 |
@azure_ds_telemetry_reporter
|
|
Packit Service |
a04d08 |
def _decrypt_certs_from_xml(self, certificates_xml):
|
|
Packit Service |
a04d08 |
"""Decrypt the certificates XML document using the our private key;
|
|
Packit Service |
a04d08 |
return the list of certs and private keys contained in the doc.
|
|
Packit Service |
a04d08 |
"""
|
|
Packit Service |
a04d08 |
tag = ElementTree.fromstring(certificates_xml).find('.//Data')
|
|
Packit Service |
a04d08 |
certificates_content = tag.text
|
|
Packit Service |
a04d08 |
lines = [
|
|
Packit Service |
a04d08 |
b'MIME-Version: 1.0',
|
|
Packit Service |
a04d08 |
b'Content-Disposition: attachment; filename="Certificates.p7m"',
|
|
Packit Service |
a04d08 |
b'Content-Type: application/x-pkcs7-mime; name="Certificates.p7m"',
|
|
Packit Service |
a04d08 |
b'Content-Transfer-Encoding: base64',
|
|
Packit Service |
a04d08 |
b'',
|
|
Packit Service |
a04d08 |
certificates_content.encode('utf-8'),
|
|
Packit Service |
a04d08 |
]
|
|
Packit Service |
a04d08 |
with cd(self.tmpdir):
|
|
Packit Service |
9bfd13 |
out, _ = subp.subp(
|
|
Packit Service |
a04d08 |
'openssl cms -decrypt -in /dev/stdin -inkey'
|
|
Packit Service |
a04d08 |
' {private_key} -recip {certificate} | openssl pkcs12 -nodes'
|
|
Packit Service |
a04d08 |
' -password pass:'.format(**self.certificate_names),
|
|
Packit Service |
a04d08 |
shell=True, data=b'\n'.join(lines))
|
|
Packit Service |
a04d08 |
return out
|
|
Packit Service |
a04d08 |
|
|
Packit Service |
a04d08 |
@azure_ds_telemetry_reporter
|
|
Packit Service |
a04d08 |
def parse_certificates(self, certificates_xml):
|
|
Packit Service |
a04d08 |
"""Given the Certificates XML document, return a dictionary of
|
|
Packit Service |
a04d08 |
fingerprints and associated SSH keys derived from the certs."""
|
|
Packit Service |
a04d08 |
out = self._decrypt_certs_from_xml(certificates_xml)
|
|
Packit Service |
a04d08 |
current = []
|
|
Packit Service |
a04d08 |
keys = {}
|
|
Packit Service |
a04d08 |
for line in out.splitlines():
|
|
Packit Service |
a04d08 |
current.append(line)
|
|
Packit Service |
a04d08 |
if re.match(r'[-]+END .*?KEY[-]+$', line):
|
|
Packit Service |
a04d08 |
# ignore private_keys
|
|
Packit Service |
a04d08 |
current = []
|
|
Packit Service |
a04d08 |
elif re.match(r'[-]+END .*?CERTIFICATE[-]+$', line):
|
|
Packit Service |
a04d08 |
certificate = '\n'.join(current)
|
|
Packit Service |
a04d08 |
ssh_key = self._get_ssh_key_from_cert(certificate)
|
|
Packit Service |
a04d08 |
fingerprint = self._get_fingerprint_from_cert(certificate)
|
|
Packit Service |
a04d08 |
keys[fingerprint] = ssh_key
|
|
Packit Service |
a04d08 |
current = []
|
|
Packit Service |
a04d08 |
return keys
|
|
Packit Service |
a04d08 |
|
|
Packit Service |
a04d08 |
|
|
Packit Service |
9bfd13 |
class GoalStateHealthReporter:
|
|
Packit Service |
9bfd13 |
|
|
Packit Service |
9bfd13 |
HEALTH_REPORT_XML_TEMPLATE = textwrap.dedent('''\
|
|
Packit Service |
9bfd13 |
|
|
Packit Service |
9bfd13 |
|
|
Packit Service |
9bfd13 |
xmlns:xsd="http://www.w3.org/2001/XMLSchema">
|
|
Packit Service |
9bfd13 |
<GoalStateIncarnation>{incarnation}</GoalStateIncarnation>
|
|
Packit Service |
9bfd13 |
<Container>
|
|
Packit Service |
9bfd13 |
<ContainerId>{container_id}</ContainerId>
|
|
Packit Service |
9bfd13 |
<RoleInstanceList>
|
|
Packit Service |
9bfd13 |
<Role>
|
|
Packit Service |
9bfd13 |
<InstanceId>{instance_id}</InstanceId>
|
|
Packit Service |
9bfd13 |
<Health>
|
|
Packit Service |
9bfd13 |
<State>{health_status}</State>
|
|
Packit Service |
9bfd13 |
{health_detail_subsection}
|
|
Packit Service |
9bfd13 |
</Health>
|
|
Packit Service |
9bfd13 |
</Role>
|
|
Packit Service |
9bfd13 |
</RoleInstanceList>
|
|
Packit Service |
9bfd13 |
</Container>
|
|
Packit Service |
9bfd13 |
</Health>
|
|
Packit Service |
9bfd13 |
''')
|
|
Packit Service |
9bfd13 |
|
|
Packit Service |
9bfd13 |
HEALTH_DETAIL_SUBSECTION_XML_TEMPLATE = textwrap.dedent('''\
|
|
Packit Service |
9bfd13 |
<Details>
|
|
Packit Service |
9bfd13 |
<SubStatus>{health_substatus}</SubStatus>
|
|
Packit Service |
9bfd13 |
<Description>{health_description}</Description>
|
|
Packit Service |
9bfd13 |
</Details>
|
|
Packit Service |
9bfd13 |
''')
|
|
Packit Service |
9bfd13 |
|
|
Packit Service |
9bfd13 |
PROVISIONING_SUCCESS_STATUS = 'Ready'
|
|
Packit Service |
9bfd13 |
|
|
Packit Service |
9bfd13 |
def __init__(self, goal_state, azure_endpoint_client, endpoint):
|
|
Packit Service |
9bfd13 |
"""Creates instance that will report provisioning status to an endpoint
|
|
Packit Service |
9bfd13 |
|
|
Packit Service |
9bfd13 |
@param goal_state: An instance of class GoalState that contains
|
|
Packit Service |
9bfd13 |
goal state info such as incarnation, container id, and instance id.
|
|
Packit Service |
9bfd13 |
These 3 values are needed when reporting the provisioning status
|
|
Packit Service |
9bfd13 |
to Azure
|
|
Packit Service |
9bfd13 |
@param azure_endpoint_client: Instance of class AzureEndpointHttpClient
|
|
Packit Service |
9bfd13 |
@param endpoint: Endpoint (string) where the provisioning status report
|
|
Packit Service |
9bfd13 |
will be sent to
|
|
Packit Service |
9bfd13 |
@return: Instance of class GoalStateHealthReporter
|
|
Packit Service |
9bfd13 |
"""
|
|
Packit Service |
9bfd13 |
self._goal_state = goal_state
|
|
Packit Service |
9bfd13 |
self._azure_endpoint_client = azure_endpoint_client
|
|
Packit Service |
9bfd13 |
self._endpoint = endpoint
|
|
Packit Service |
9bfd13 |
|
|
Packit Service |
9bfd13 |
@azure_ds_telemetry_reporter
|
|
Packit Service |
9bfd13 |
def send_ready_signal(self):
|
|
Packit Service |
9bfd13 |
document = self.build_report(
|
|
Packit Service |
9bfd13 |
incarnation=self._goal_state.incarnation,
|
|
Packit Service |
9bfd13 |
container_id=self._goal_state.container_id,
|
|
Packit Service |
9bfd13 |
instance_id=self._goal_state.instance_id,
|
|
Packit Service |
9bfd13 |
status=self.PROVISIONING_SUCCESS_STATUS)
|
|
Packit Service |
9bfd13 |
LOG.debug('Reporting ready to Azure fabric.')
|
|
Packit Service |
9bfd13 |
try:
|
|
Packit Service |
9bfd13 |
self._post_health_report(document=document)
|
|
Packit Service |
9bfd13 |
except Exception as e:
|
|
Packit Service |
9bfd13 |
msg = "exception while reporting ready: %s" % e
|
|
Packit Service |
9bfd13 |
LOG.error(msg)
|
|
Packit Service |
9bfd13 |
report_diagnostic_event(msg)
|
|
Packit Service |
9bfd13 |
raise
|
|
Packit Service |
9bfd13 |
|
|
Packit Service |
9bfd13 |
LOG.info('Reported ready to Azure fabric.')
|
|
Packit Service |
9bfd13 |
|
|
Packit Service |
9bfd13 |
def build_report(
|
|
Packit Service |
9bfd13 |
self, incarnation, container_id, instance_id,
|
|
Packit Service |
9bfd13 |
status, substatus=None, description=None):
|
|
Packit Service |
9bfd13 |
health_detail = ''
|
|
Packit Service |
9bfd13 |
if substatus is not None:
|
|
Packit Service |
9bfd13 |
health_detail = self.HEALTH_DETAIL_SUBSECTION_XML_TEMPLATE.format(
|
|
Packit Service |
9bfd13 |
health_substatus=substatus, health_description=description)
|
|
Packit Service |
9bfd13 |
|
|
Packit Service |
9bfd13 |
health_report = self.HEALTH_REPORT_XML_TEMPLATE.format(
|
|
Packit Service |
9bfd13 |
incarnation=incarnation,
|
|
Packit Service |
9bfd13 |
container_id=container_id,
|
|
Packit Service |
9bfd13 |
instance_id=instance_id,
|
|
Packit Service |
9bfd13 |
health_status=status,
|
|
Packit Service |
9bfd13 |
health_detail_subsection=health_detail)
|
|
Packit Service |
9bfd13 |
|
|
Packit Service |
9bfd13 |
return health_report
|
|
Packit Service |
9bfd13 |
|
|
Packit Service |
9bfd13 |
@azure_ds_telemetry_reporter
|
|
Packit Service |
9bfd13 |
def _post_health_report(self, document):
|
|
Packit Service |
9bfd13 |
push_log_to_kvp()
|
|
Packit Service |
9bfd13 |
|
|
Packit Service |
9bfd13 |
# Whenever report_diagnostic_event(diagnostic_msg) is invoked in code,
|
|
Packit Service |
9bfd13 |
# the diagnostic messages are written to special files
|
|
Packit Service |
9bfd13 |
# (/var/opt/hyperv/.kvp_pool_*) as Hyper-V KVP messages.
|
|
Packit Service |
9bfd13 |
# Hyper-V KVP message communication is done through these files,
|
|
Packit Service |
9bfd13 |
# and KVP functionality is used to communicate and share diagnostic
|
|
Packit Service |
9bfd13 |
# info with the Azure Host.
|
|
Packit Service |
9bfd13 |
# The Azure Host will collect the VM's Hyper-V KVP diagnostic messages
|
|
Packit Service |
9bfd13 |
# when cloud-init reports to fabric.
|
|
Packit Service |
9bfd13 |
# When the Azure Host receives the health report signal, it will only
|
|
Packit Service |
9bfd13 |
# collect and process whatever KVP diagnostic messages have been
|
|
Packit Service |
9bfd13 |
# written to the KVP files.
|
|
Packit Service |
9bfd13 |
# KVP messages that are published after the Azure Host receives the
|
|
Packit Service |
9bfd13 |
# signal are ignored and unprocessed, so yield this thread to the
|
|
Packit Service |
9bfd13 |
# Hyper-V KVP Reporting thread so that they are written.
|
|
Packit Service |
9bfd13 |
# time.sleep(0) is a low-cost and proven method to yield the scheduler
|
|
Packit Service |
9bfd13 |
# and ensure that events are flushed.
|
|
Packit Service |
9bfd13 |
# See HyperVKvpReportingHandler class, which is a multi-threaded
|
|
Packit Service |
9bfd13 |
# reporting handler that writes to the special KVP files.
|
|
Packit Service |
9bfd13 |
time.sleep(0)
|
|
Packit Service |
9bfd13 |
|
|
Packit Service |
9bfd13 |
LOG.debug('Sending health report to Azure fabric.')
|
|
Packit Service |
9bfd13 |
url = "http://{}/machine?comp=health".format(self._endpoint)
|
|
Packit Service |
9bfd13 |
self._azure_endpoint_client.post(
|
|
Packit Service |
9bfd13 |
url,
|
|
Packit Service |
9bfd13 |
data=document,
|
|
Packit Service |
9bfd13 |
extra_headers={'Content-Type': 'text/xml; charset=utf-8'})
|
|
Packit Service |
9bfd13 |
LOG.debug('Successfully sent health report to Azure fabric')
|
|
Packit Service |
9bfd13 |
|
|
Packit Service |
9bfd13 |
|
|
Packit Service |
9bfd13 |
class WALinuxAgentShim:
|
|
Packit Service |
a04d08 |
|
|
Packit Service |
a04d08 |
def __init__(self, fallback_lease_file=None, dhcp_options=None):
|
|
Packit Service |
a04d08 |
LOG.debug('WALinuxAgentShim instantiated, fallback_lease_file=%s',
|
|
Packit Service |
a04d08 |
fallback_lease_file)
|
|
Packit Service |
a04d08 |
self.dhcpoptions = dhcp_options
|
|
Packit Service |
a04d08 |
self._endpoint = None
|
|
Packit Service |
a04d08 |
self.openssl_manager = None
|
|
Packit Service |
9bfd13 |
self.azure_endpoint_client = None
|
|
Packit Service |
a04d08 |
self.lease_file = fallback_lease_file
|
|
Packit Service |
a04d08 |
|
|
Packit Service |
a04d08 |
def clean_up(self):
|
|
Packit Service |
a04d08 |
if self.openssl_manager is not None:
|
|
Packit Service |
a04d08 |
self.openssl_manager.clean_up()
|
|
Packit Service |
a04d08 |
|
|
Packit Service |
a04d08 |
@staticmethod
|
|
Packit Service |
a04d08 |
def _get_hooks_dir():
|
|
Packit Service |
a04d08 |
_paths = stages.Init()
|
|
Packit Service |
a04d08 |
return os.path.join(_paths.paths.get_runpath(), "dhclient.hooks")
|
|
Packit Service |
a04d08 |
|
|
Packit Service |
a04d08 |
@property
|
|
Packit Service |
a04d08 |
def endpoint(self):
|
|
Packit Service |
a04d08 |
if self._endpoint is None:
|
|
Packit Service |
a04d08 |
self._endpoint = self.find_endpoint(self.lease_file,
|
|
Packit Service |
a04d08 |
self.dhcpoptions)
|
|
Packit Service |
a04d08 |
return self._endpoint
|
|
Packit Service |
a04d08 |
|
|
Packit Service |
a04d08 |
@staticmethod
|
|
Packit Service |
a04d08 |
def get_ip_from_lease_value(fallback_lease_value):
|
|
Packit Service |
a04d08 |
unescaped_value = fallback_lease_value.replace('\\', '')
|
|
Packit Service |
a04d08 |
if len(unescaped_value) > 4:
|
|
Packit Service |
a04d08 |
hex_string = ''
|
|
Packit Service |
a04d08 |
for hex_pair in unescaped_value.split(':'):
|
|
Packit Service |
a04d08 |
if len(hex_pair) == 1:
|
|
Packit Service |
a04d08 |
hex_pair = '0' + hex_pair
|
|
Packit Service |
a04d08 |
hex_string += hex_pair
|
|
Packit Service |
a04d08 |
packed_bytes = struct.pack(
|
|
Packit Service |
a04d08 |
'>L', int(hex_string.replace(':', ''), 16))
|
|
Packit Service |
a04d08 |
else:
|
|
Packit Service |
a04d08 |
packed_bytes = unescaped_value.encode('utf-8')
|
|
Packit Service |
a04d08 |
return socket.inet_ntoa(packed_bytes)
|
|
Packit Service |
a04d08 |
|
|
Packit Service |
a04d08 |
@staticmethod
|
|
Packit Service |
a04d08 |
@azure_ds_telemetry_reporter
|
|
Packit Service |
a04d08 |
def _networkd_get_value_from_leases(leases_d=None):
|
|
Packit Service |
a04d08 |
return dhcp.networkd_get_option_from_leases(
|
|
Packit Service |
a04d08 |
'OPTION_245', leases_d=leases_d)
|
|
Packit Service |
a04d08 |
|
|
Packit Service |
a04d08 |
@staticmethod
|
|
Packit Service |
a04d08 |
@azure_ds_telemetry_reporter
|
|
Packit Service |
a04d08 |
def _get_value_from_leases_file(fallback_lease_file):
|
|
Packit Service |
a04d08 |
leases = []
|
|
Packit Service |
a04d08 |
try:
|
|
Packit Service |
a04d08 |
content = util.load_file(fallback_lease_file)
|
|
Packit Service |
a04d08 |
except IOError as ex:
|
|
Packit Service |
a04d08 |
LOG.error("Failed to read %s: %s", fallback_lease_file, ex)
|
|
Packit Service |
a04d08 |
return None
|
|
Packit Service |
a04d08 |
|
|
Packit Service |
a04d08 |
LOG.debug("content is %s", content)
|
|
Packit Service |
a04d08 |
option_name = _get_dhcp_endpoint_option_name()
|
|
Packit Service |
a04d08 |
for line in content.splitlines():
|
|
Packit Service |
a04d08 |
if option_name in line:
|
|
Packit Service |
a04d08 |
# Example line from Ubuntu
|
|
Packit Service |
a04d08 |
# option unknown-245 a8:3f:81:10;
|
|
Packit Service |
a04d08 |
leases.append(line.strip(' ').split(' ', 2)[-1].strip(';\n"'))
|
|
Packit Service |
a04d08 |
# Return the "most recent" one in the list
|
|
Packit Service |
a04d08 |
if len(leases) < 1:
|
|
Packit Service |
a04d08 |
return None
|
|
Packit Service |
a04d08 |
else:
|
|
Packit Service |
a04d08 |
return leases[-1]
|
|
Packit Service |
a04d08 |
|
|
Packit Service |
a04d08 |
@staticmethod
|
|
Packit Service |
a04d08 |
@azure_ds_telemetry_reporter
|
|
Packit Service |
a04d08 |
def _load_dhclient_json():
|
|
Packit Service |
a04d08 |
dhcp_options = {}
|
|
Packit Service |
a04d08 |
hooks_dir = WALinuxAgentShim._get_hooks_dir()
|
|
Packit Service |
a04d08 |
if not os.path.exists(hooks_dir):
|
|
Packit Service |
a04d08 |
LOG.debug("%s not found.", hooks_dir)
|
|
Packit Service |
a04d08 |
return None
|
|
Packit Service |
a04d08 |
hook_files = [os.path.join(hooks_dir, x)
|
|
Packit Service |
a04d08 |
for x in os.listdir(hooks_dir)]
|
|
Packit Service |
a04d08 |
for hook_file in hook_files:
|
|
Packit Service |
a04d08 |
try:
|
|
Packit Service |
a04d08 |
name = os.path.basename(hook_file).replace('.json', '')
|
|
Packit Service |
a04d08 |
dhcp_options[name] = json.loads(util.load_file((hook_file)))
|
|
Packit Service |
9bfd13 |
except ValueError as e:
|
|
Packit Service |
a04d08 |
raise ValueError(
|
|
Packit Service |
9bfd13 |
'{_file} is not valid JSON data'.format(_file=hook_file)
|
|
Packit Service |
9bfd13 |
) from e
|
|
Packit Service |
a04d08 |
return dhcp_options
|
|
Packit Service |
a04d08 |
|
|
Packit Service |
a04d08 |
@staticmethod
|
|
Packit Service |
a04d08 |
@azure_ds_telemetry_reporter
|
|
Packit Service |
a04d08 |
def _get_value_from_dhcpoptions(dhcp_options):
|
|
Packit Service |
a04d08 |
if dhcp_options is None:
|
|
Packit Service |
a04d08 |
return None
|
|
Packit Service |
a04d08 |
# the MS endpoint server is given to us as DHPC option 245
|
|
Packit Service |
a04d08 |
_value = None
|
|
Packit Service |
a04d08 |
for interface in dhcp_options:
|
|
Packit Service |
a04d08 |
_value = dhcp_options[interface].get('unknown_245', None)
|
|
Packit Service |
a04d08 |
if _value is not None:
|
|
Packit Service |
a04d08 |
LOG.debug("Endpoint server found in dhclient options")
|
|
Packit Service |
a04d08 |
break
|
|
Packit Service |
a04d08 |
return _value
|
|
Packit Service |
a04d08 |
|
|
Packit Service |
a04d08 |
@staticmethod
|
|
Packit Service |
a04d08 |
@azure_ds_telemetry_reporter
|
|
Packit Service |
a04d08 |
def find_endpoint(fallback_lease_file=None, dhcp245=None):
|
|
Packit Service |
9bfd13 |
"""Finds and returns the Azure endpoint using various methods.
|
|
Packit Service |
9bfd13 |
|
|
Packit Service |
9bfd13 |
The Azure endpoint is searched in the following order:
|
|
Packit Service |
9bfd13 |
1. Endpoint from dhcp options (dhcp option 245).
|
|
Packit Service |
9bfd13 |
2. Endpoint from networkd.
|
|
Packit Service |
9bfd13 |
3. Endpoint from dhclient hook json.
|
|
Packit Service |
9bfd13 |
4. Endpoint from fallback lease file.
|
|
Packit Service |
9bfd13 |
5. The default Azure endpoint.
|
|
Packit Service |
9bfd13 |
|
|
Packit Service |
9bfd13 |
@param fallback_lease_file: Fallback lease file that will be used
|
|
Packit Service |
9bfd13 |
during endpoint search.
|
|
Packit Service |
9bfd13 |
@param dhcp245: dhcp options that will be used during endpoint search.
|
|
Packit Service |
9bfd13 |
@return: Azure endpoint IP address.
|
|
Packit Service |
9bfd13 |
"""
|
|
Packit Service |
a04d08 |
value = None
|
|
Packit Service |
9bfd13 |
|
|
Packit Service |
a04d08 |
if dhcp245 is not None:
|
|
Packit Service |
a04d08 |
value = dhcp245
|
|
Packit Service |
a04d08 |
LOG.debug("Using Azure Endpoint from dhcp options")
|
|
Packit Service |
a04d08 |
if value is None:
|
|
Packit Service |
a04d08 |
report_diagnostic_event("No Azure endpoint from dhcp options")
|
|
Packit Service |
a04d08 |
LOG.debug('Finding Azure endpoint from networkd...')
|
|
Packit Service |
a04d08 |
value = WALinuxAgentShim._networkd_get_value_from_leases()
|
|
Packit Service |
a04d08 |
if value is None:
|
|
Packit Service |
a04d08 |
# Option-245 stored in /run/cloud-init/dhclient.hooks/<ifc>.json
|
|
Packit Service |
a04d08 |
# a dhclient exit hook that calls cloud-init-dhclient-hook
|
|
Packit Service |
a04d08 |
report_diagnostic_event("No Azure endpoint from networkd")
|
|
Packit Service |
a04d08 |
LOG.debug('Finding Azure endpoint from hook json...')
|
|
Packit Service |
a04d08 |
dhcp_options = WALinuxAgentShim._load_dhclient_json()
|
|
Packit Service |
a04d08 |
value = WALinuxAgentShim._get_value_from_dhcpoptions(dhcp_options)
|
|
Packit Service |
a04d08 |
if value is None:
|
|
Packit Service |
a04d08 |
# Fallback and check the leases file if unsuccessful
|
|
Packit Service |
a04d08 |
report_diagnostic_event("No Azure endpoint from dhclient logs")
|
|
Packit Service |
a04d08 |
LOG.debug("Unable to find endpoint in dhclient logs. "
|
|
Packit Service |
a04d08 |
" Falling back to check lease files")
|
|
Packit Service |
a04d08 |
if fallback_lease_file is None:
|
|
Packit Service |
a04d08 |
LOG.warning("No fallback lease file was specified.")
|
|
Packit Service |
a04d08 |
value = None
|
|
Packit Service |
a04d08 |
else:
|
|
Packit Service |
a04d08 |
LOG.debug("Looking for endpoint in lease file %s",
|
|
Packit Service |
a04d08 |
fallback_lease_file)
|
|
Packit Service |
a04d08 |
value = WALinuxAgentShim._get_value_from_leases_file(
|
|
Packit Service |
a04d08 |
fallback_lease_file)
|
|
Packit Service |
a04d08 |
if value is None:
|
|
Packit Service |
a04d08 |
msg = "No lease found; using default endpoint"
|
|
Packit Service |
a04d08 |
report_diagnostic_event(msg)
|
|
Packit Service |
a04d08 |
LOG.warning(msg)
|
|
Packit Service |
a04d08 |
value = DEFAULT_WIRESERVER_ENDPOINT
|
|
Packit Service |
a04d08 |
|
|
Packit Service |
a04d08 |
endpoint_ip_address = WALinuxAgentShim.get_ip_from_lease_value(value)
|
|
Packit Service |
a04d08 |
msg = 'Azure endpoint found at %s' % endpoint_ip_address
|
|
Packit Service |
a04d08 |
report_diagnostic_event(msg)
|
|
Packit Service |
a04d08 |
LOG.debug(msg)
|
|
Packit Service |
a04d08 |
return endpoint_ip_address
|
|
Packit Service |
a04d08 |
|
|
Packit Service |
a04d08 |
@azure_ds_telemetry_reporter
|
|
Packit Service |
a04d08 |
def register_with_azure_and_fetch_data(self, pubkey_info=None):
|
|
Packit Service |
9bfd13 |
"""Gets the VM's GoalState from Azure, uses the GoalState information
|
|
Packit Service |
9bfd13 |
to report ready/send the ready signal/provisioning complete signal to
|
|
Packit Service |
9bfd13 |
Azure, and then uses pubkey_info to filter and obtain the user's
|
|
Packit Service |
9bfd13 |
pubkeys from the GoalState.
|
|
Packit Service |
9bfd13 |
|
|
Packit Service |
9bfd13 |
@param pubkey_info: List of pubkey values and fingerprints which are
|
|
Packit Service |
9bfd13 |
used to filter and obtain the user's pubkey values from the
|
|
Packit Service |
9bfd13 |
GoalState.
|
|
Packit Service |
9bfd13 |
@return: The list of user's authorized pubkey values.
|
|
Packit Service |
9bfd13 |
"""
|
|
Packit Service |
a04d08 |
if self.openssl_manager is None:
|
|
Packit Service |
a04d08 |
self.openssl_manager = OpenSSLManager()
|
|
Packit Service |
9bfd13 |
if self.azure_endpoint_client is None:
|
|
Packit Service |
9bfd13 |
self.azure_endpoint_client = AzureEndpointHttpClient(
|
|
Packit Service |
9bfd13 |
self.openssl_manager.certificate)
|
|
Packit Service |
9bfd13 |
goal_state = self._fetch_goal_state_from_azure()
|
|
Packit Service |
9bfd13 |
ssh_keys = self._get_user_pubkeys(goal_state, pubkey_info)
|
|
Packit Service |
9bfd13 |
health_reporter = GoalStateHealthReporter(
|
|
Packit Service |
9bfd13 |
goal_state, self.azure_endpoint_client, self.endpoint)
|
|
Packit Service |
9bfd13 |
health_reporter.send_ready_signal()
|
|
Packit Service |
9bfd13 |
return {'public-keys': ssh_keys}
|
|
Packit Service |
9bfd13 |
|
|
Packit Service |
9bfd13 |
@azure_ds_telemetry_reporter
|
|
Packit Service |
9bfd13 |
def _fetch_goal_state_from_azure(self):
|
|
Packit Service |
9bfd13 |
"""Fetches the GoalState XML from the Azure endpoint, parses the XML,
|
|
Packit Service |
9bfd13 |
and returns a GoalState object.
|
|
Packit Service |
9bfd13 |
|
|
Packit Service |
9bfd13 |
@return: GoalState object representing the GoalState XML
|
|
Packit Service |
9bfd13 |
"""
|
|
Packit Service |
9bfd13 |
unparsed_goal_state_xml = self._get_raw_goal_state_xml_from_azure()
|
|
Packit Service |
9bfd13 |
return self._parse_raw_goal_state_xml(unparsed_goal_state_xml)
|
|
Packit Service |
9bfd13 |
|
|
Packit Service |
9bfd13 |
@azure_ds_telemetry_reporter
|
|
Packit Service |
9bfd13 |
def _get_raw_goal_state_xml_from_azure(self):
|
|
Packit Service |
9bfd13 |
"""Fetches the GoalState XML from the Azure endpoint and returns
|
|
Packit Service |
9bfd13 |
the XML as a string.
|
|
Packit Service |
9bfd13 |
|
|
Packit Service |
9bfd13 |
@return: GoalState XML string
|
|
Packit Service |
9bfd13 |
"""
|
|
Packit Service |
9bfd13 |
|
|
Packit Service |
a04d08 |
LOG.info('Registering with Azure...')
|
|
Packit Service |
9bfd13 |
url = 'http://{}/machine/?comp=goalstate'.format(self.endpoint)
|
|
Packit Service |
9bfd13 |
try:
|
|
Packit Service |
9bfd13 |
response = self.azure_endpoint_client.get(url)
|
|
Packit Service |
9bfd13 |
except Exception as e:
|
|
Packit Service |
9bfd13 |
msg = 'failed to register with Azure: %s' % e
|
|
Packit Service |
9bfd13 |
LOG.warning(msg)
|
|
Packit Service |
9bfd13 |
report_diagnostic_event(msg)
|
|
Packit Service |
9bfd13 |
raise
|
|
Packit Service |
a04d08 |
LOG.debug('Successfully fetched GoalState XML.')
|
|
Packit Service |
9bfd13 |
return response.contents
|
|
Packit Service |
9bfd13 |
|
|
Packit Service |
9bfd13 |
@azure_ds_telemetry_reporter
|
|
Packit Service |
9bfd13 |
def _parse_raw_goal_state_xml(self, unparsed_goal_state_xml):
|
|
Packit Service |
9bfd13 |
"""Parses a GoalState XML string and returns a GoalState object.
|
|
Packit Service |
9bfd13 |
|
|
Packit Service |
9bfd13 |
@param unparsed_goal_state_xml: GoalState XML string
|
|
Packit Service |
9bfd13 |
@return: GoalState object representing the GoalState XML
|
|
Packit Service |
9bfd13 |
"""
|
|
Packit Service |
9bfd13 |
try:
|
|
Packit Service |
9bfd13 |
goal_state = GoalState(
|
|
Packit Service |
9bfd13 |
unparsed_goal_state_xml, self.azure_endpoint_client)
|
|
Packit Service |
9bfd13 |
except Exception as e:
|
|
Packit Service |
9bfd13 |
msg = 'Error processing GoalState XML: %s' % e
|
|
Packit Service |
9bfd13 |
LOG.warning(msg)
|
|
Packit Service |
9bfd13 |
report_diagnostic_event(msg)
|
|
Packit Service |
9bfd13 |
raise
|
|
Packit Service |
9bfd13 |
msg = ', '.join([
|
|
Packit Service |
9bfd13 |
'GoalState XML container id: %s' % goal_state.container_id,
|
|
Packit Service |
9bfd13 |
'GoalState XML instance id: %s' % goal_state.instance_id,
|
|
Packit Service |
9bfd13 |
'GoalState XML incarnation: %s' % goal_state.incarnation])
|
|
Packit Service |
9bfd13 |
LOG.debug(msg)
|
|
Packit Service |
9bfd13 |
report_diagnostic_event(msg)
|
|
Packit Service |
9bfd13 |
return goal_state
|
|
Packit Service |
9bfd13 |
|
|
Packit Service |
9bfd13 |
@azure_ds_telemetry_reporter
|
|
Packit Service |
9bfd13 |
def _get_user_pubkeys(self, goal_state, pubkey_info):
|
|
Packit Service |
9bfd13 |
"""Gets and filters the VM admin user's authorized pubkeys.
|
|
Packit Service |
9bfd13 |
|
|
Packit Service |
9bfd13 |
The admin user in this case is the username specified as "admin"
|
|
Packit Service |
9bfd13 |
when deploying VMs on Azure.
|
|
Packit Service |
9bfd13 |
See https://docs.microsoft.com/en-us/cli/azure/vm#az-vm-create.
|
|
Packit Service |
9bfd13 |
cloud-init expects a straightforward array of keys to be dropped
|
|
Packit Service |
9bfd13 |
into the admin user's authorized_keys file. Azure control plane exposes
|
|
Packit Service |
9bfd13 |
multiple public keys to the VM via wireserver. Select just the
|
|
Packit Service |
9bfd13 |
admin user's key(s) and return them, ignoring any other certs.
|
|
Packit Service |
9bfd13 |
|
|
Packit Service |
9bfd13 |
@param goal_state: GoalState object. The GoalState object contains
|
|
Packit Service |
9bfd13 |
a certificate XML, which contains both the VM user's authorized
|
|
Packit Service |
9bfd13 |
pubkeys and other non-user pubkeys, which are used for
|
|
Packit Service |
9bfd13 |
MSI and protected extension handling.
|
|
Packit Service |
9bfd13 |
@param pubkey_info: List of VM user pubkey dicts that were previously
|
|
Packit Service |
9bfd13 |
obtained from provisioning data.
|
|
Packit Service |
9bfd13 |
Each pubkey dict in this list can either have the format
|
|
Packit Service |
9bfd13 |
pubkey['value'] or pubkey['fingerprint'].
|
|
Packit Service |
9bfd13 |
Each pubkey['fingerprint'] in the list is used to filter
|
|
Packit Service |
9bfd13 |
and obtain the actual pubkey value from the GoalState
|
|
Packit Service |
9bfd13 |
certificates XML.
|
|
Packit Service |
9bfd13 |
Each pubkey['value'] requires no further processing and is
|
|
Packit Service |
9bfd13 |
immediately added to the return list.
|
|
Packit Service |
9bfd13 |
@return: A list of the VM user's authorized pubkey values.
|
|
Packit Service |
9bfd13 |
"""
|
|
Packit Service |
a04d08 |
ssh_keys = []
|
|
Packit Service |
a04d08 |
if goal_state.certificates_xml is not None and pubkey_info is not None:
|
|
Packit Service |
a04d08 |
LOG.debug('Certificate XML found; parsing out public keys.')
|
|
Packit Service |
a04d08 |
keys_by_fingerprint = self.openssl_manager.parse_certificates(
|
|
Packit Service |
a04d08 |
goal_state.certificates_xml)
|
|
Packit Service |
a04d08 |
ssh_keys = self._filter_pubkeys(keys_by_fingerprint, pubkey_info)
|
|
Packit Service |
9bfd13 |
return ssh_keys
|
|
Packit Service |
a04d08 |
|
|
Packit Service |
9bfd13 |
@staticmethod
|
|
Packit Service |
9bfd13 |
def _filter_pubkeys(keys_by_fingerprint, pubkey_info):
|
|
Packit Service |
9bfd13 |
""" Filter and return only the user's actual pubkeys.
|
|
Packit Service |
9bfd13 |
|
|
Packit Service |
9bfd13 |
@param keys_by_fingerprint: pubkey fingerprint -> pubkey value dict
|
|
Packit Service |
9bfd13 |
that was obtained from GoalState Certificates XML. May contain
|
|
Packit Service |
9bfd13 |
non-user pubkeys.
|
|
Packit Service |
9bfd13 |
@param pubkey_info: List of VM user pubkeys. Pubkey values are added
|
|
Packit Service |
9bfd13 |
to the return list without further processing. Pubkey fingerprints
|
|
Packit Service |
9bfd13 |
are used to filter and obtain the actual pubkey values from
|
|
Packit Service |
9bfd13 |
keys_by_fingerprint.
|
|
Packit Service |
9bfd13 |
@return: A list of the VM user's authorized pubkey values.
|
|
Packit Service |
a04d08 |
"""
|
|
Packit Service |
a04d08 |
keys = []
|
|
Packit Service |
a04d08 |
for pubkey in pubkey_info:
|
|
Packit Service |
a04d08 |
if 'value' in pubkey and pubkey['value']:
|
|
Packit Service |
a04d08 |
keys.append(pubkey['value'])
|
|
Packit Service |
a04d08 |
elif 'fingerprint' in pubkey and pubkey['fingerprint']:
|
|
Packit Service |
a04d08 |
fingerprint = pubkey['fingerprint']
|
|
Packit Service |
a04d08 |
if fingerprint in keys_by_fingerprint:
|
|
Packit Service |
a04d08 |
keys.append(keys_by_fingerprint[fingerprint])
|
|
Packit Service |
a04d08 |
else:
|
|
Packit Service |
a04d08 |
LOG.warning("ovf-env.xml specified PublicKey fingerprint "
|
|
Packit Service |
a04d08 |
"%s not found in goalstate XML", fingerprint)
|
|
Packit Service |
a04d08 |
else:
|
|
Packit Service |
a04d08 |
LOG.warning("ovf-env.xml specified PublicKey with neither "
|
|
Packit Service |
a04d08 |
"value nor fingerprint: %s", pubkey)
|
|
Packit Service |
a04d08 |
|
|
Packit Service |
a04d08 |
return keys
|
|
Packit Service |
a04d08 |
|
|
Packit Service |
a04d08 |
|
|
Packit Service |
a04d08 |
@azure_ds_telemetry_reporter
|
|
Packit Service |
a04d08 |
def get_metadata_from_fabric(fallback_lease_file=None, dhcp_opts=None,
|
|
Packit Service |
a04d08 |
pubkey_info=None):
|
|
Packit Service |
a04d08 |
shim = WALinuxAgentShim(fallback_lease_file=fallback_lease_file,
|
|
Packit Service |
a04d08 |
dhcp_options=dhcp_opts)
|
|
Packit Service |
a04d08 |
try:
|
|
Packit Service |
a04d08 |
return shim.register_with_azure_and_fetch_data(pubkey_info=pubkey_info)
|
|
Packit Service |
a04d08 |
finally:
|
|
Packit Service |
a04d08 |
shim.clean_up()
|
|
Packit Service |
a04d08 |
|
|
Packit Service |
a04d08 |
|
|
Packit Service |
9bfd13 |
def dhcp_log_cb(out, err):
|
|
Packit Service |
9bfd13 |
report_diagnostic_event("dhclient output stream: %s" % out)
|
|
Packit Service |
9bfd13 |
report_diagnostic_event("dhclient error stream: %s" % err)
|
|
Packit Service |
9bfd13 |
|
|
Packit Service |
9bfd13 |
|
|
Packit Service |
9bfd13 |
class EphemeralDHCPv4WithReporting:
|
|
Packit Service |
a04d08 |
def __init__(self, reporter, nic=None):
|
|
Packit Service |
a04d08 |
self.reporter = reporter
|
|
Packit Service |
9bfd13 |
self.ephemeralDHCPv4 = EphemeralDHCPv4(
|
|
Packit Service |
9bfd13 |
iface=nic, dhcp_log_func=dhcp_log_cb)
|
|
Packit Service |
a04d08 |
|
|
Packit Service |
a04d08 |
def __enter__(self):
|
|
Packit Service |
a04d08 |
with events.ReportEventStack(
|
|
Packit Service |
a04d08 |
name="obtain-dhcp-lease",
|
|
Packit Service |
a04d08 |
description="obtain dhcp lease",
|
|
Packit Service |
a04d08 |
parent=self.reporter):
|
|
Packit Service |
a04d08 |
return self.ephemeralDHCPv4.__enter__()
|
|
Packit Service |
a04d08 |
|
|
Packit Service |
a04d08 |
def __exit__(self, excp_type, excp_value, excp_traceback):
|
|
Packit Service |
a04d08 |
self.ephemeralDHCPv4.__exit__(
|
|
Packit Service |
a04d08 |
excp_type, excp_value, excp_traceback)
|
|
Packit Service |
a04d08 |
|
|
Packit Service |
a04d08 |
|
|
Packit Service |
a04d08 |
# vi: ts=4 expandtab
|