Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Reorganize the history directory #2520

Merged
merged 6 commits into from
Mar 1, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,11 @@ def extensions(self):
return self._extensions

def get_redacted_text(self):
return re.sub(r'("protectedSettings"\s*:\s*)"[^"]+"', r'\1"*** REDACTED ***"', self._text)
return ExtensionsGoalStateFromVmSettings.redact(self._text)

@staticmethod
def redact(text):
return re.sub(r'("protectedSettings"\s*:\s*)"[^"]+"', r'\1"*** REDACTED ***"', text)

def _parse_vm_settings(self, json_text):
vm_settings = _CaseFoldedDict.from_dict(json.loads(json_text))
Expand Down
75 changes: 34 additions & 41 deletions azurelinuxagent/common/protocol/goal_state.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
# limitations under the License.
#
# Requires Python 2.6+ and Openssl 1.0+
import datetime
import os
import re
import time
Expand All @@ -27,12 +26,14 @@
from azurelinuxagent.common.exception import ProtocolError, ResourceGoneError, VmSettingsError
from azurelinuxagent.common.future import ustr
from azurelinuxagent.common.protocol.extensions_goal_state_factory import ExtensionsGoalStateFactory
from azurelinuxagent.common.protocol.extensions_goal_state_from_vm_settings import ExtensionsGoalStateFromVmSettings
from azurelinuxagent.common.protocol.hostplugin import VmSettingsNotSupported
from azurelinuxagent.common.protocol.restapi import Cert, CertList, RemoteAccessUser, RemoteAccessUsersList
from azurelinuxagent.common.utils import fileutil
from azurelinuxagent.common.utils.archive import GoalStateHistory
from azurelinuxagent.common.utils.cryptutil import CryptUtil
from azurelinuxagent.common.utils.textutil import parse_doc, findall, find, findtext, getattrib
from azurelinuxagent.common.utils.timeutil import create_timestamp

GOAL_STATE_URI = "http://{0}/machine/?comp=goalstate"
CERTS_FILE_NAME = "Certificates.xml"
Expand Down Expand Up @@ -60,37 +61,29 @@ def __init__(self, wire_client):
self._wire_client = wire_client

# These "basic" properties come from the initial request to WireServer's goalstate API
self._timestamp = None
self._incarnation = None
self._role_instance_id = None
self._role_config_name = None
self._container_id = None

xml_text, xml_doc = GoalState._fetch_goal_state(self._wire_client)

self._initialize_basic_properties(xml_doc)

# The goal state for extensions can come from vmSettings when using FastTrack or from extensionsConfig otherwise, self._fetch_extended_goal_state
# populates the '_extensions_goal_state' property.
# These "extended" properties come from additional HTTP requests to the URIs included in the basic goal state, or to the HostGAPlugin
self._extensions_goal_state = None
vm_settings = self._fetch_vm_settings()

# These "extended" properties come from additional HTTP requests to the URIs included in the basic goal state
self._hosting_env = None
self._shared_conf = None
self._certs = None
self._remote_access = None

self._fetch_extended_goal_state(xml_text, xml_doc, vm_settings)
timestamp = create_timestamp()
xml_text, xml_doc, incarnation = GoalState._fetch_goal_state(self._wire_client)
self._history = GoalStateHistory(timestamp, incarnation)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the main change here is intantiating the history during init; the rest of the changes are just moving the code around to ensure all properties are defined before invoking any method

self._initialize_basic_properties(xml_doc)
self._fetch_extended_goal_state(xml_text, xml_doc)

except Exception as exception:
# We don't log the error here since fetching the goal state is done every few seconds
raise ProtocolError(msg="Error fetching goal state", inner=exception)

@property
def timestamp(self):
return self._timestamp

@property
def incarnation(self):
return self._incarnation
Expand Down Expand Up @@ -139,26 +132,28 @@ def update(self, force_update=False):
"""
Updates the current GoalState instance fetching values from the WireServer/HostGAPlugin as needed
"""
xml_text, xml_doc = GoalState._fetch_goal_state(self._wire_client)

vm_settings = self._fetch_vm_settings(force_update=force_update)
timestamp = create_timestamp()
xml_text, xml_doc, incarnation = GoalState._fetch_goal_state(self._wire_client)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

where/when is this full goal state saved to history folder?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's be saved as part of _fetch_extended_goal_state (called below @line 142) or after retrieving the vmsettings (below @line 150)


if force_update or self._incarnation != findtext(xml_doc, "Incarnation"):
# update the extended goal state, using vm_settings for the extensions (unless they are None, then use extensionsConfig)
if force_update or self._incarnation != incarnation:
# If we are fetching a new goal state
self._history = GoalStateHistory(timestamp, incarnation)
self._initialize_basic_properties(xml_doc)
self._fetch_extended_goal_state(xml_text, xml_doc, vm_settings)
self._fetch_extended_goal_state(xml_text, xml_doc, force_vm_settings_update=force_update)
else:
# else just ensure the extensions are using the latest vm_settings
if vm_settings is not None:
# else ensure the extensions are using the latest vm_settings
timestamp = create_timestamp()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why do we need different timestamp? can't we use L#135

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

L#136 calls _fetch_goal_state, which does a network call, which can have delays. we need a new timestamp here

vm_settings, vm_settings_updated = self._fetch_vm_settings(force_update=force_update)
if vm_settings_updated:
self._history = GoalStateHistory(timestamp, vm_settings.etag)
self._extensions_goal_state = vm_settings
self._history.save_vm_settings(vm_settings.get_redacted_text())

def save_to_history(self, data, file_name):
self._history.save(data, file_name)

def _initialize_basic_properties(self, xml_doc):
self._timestamp = datetime.datetime.utcnow().isoformat()
self._incarnation = findtext(xml_doc, "Incarnation")
self._history = GoalStateHistory(self._timestamp, self._incarnation) # history for the WireServer goal state; vmSettings are separate
role_instance = find(xml_doc, "RoleInstance")
self._role_instance_id = findtext(role_instance, "InstanceId")
role_config = find(role_instance, "Configuration")
Expand All @@ -175,16 +170,17 @@ def _fetch_goal_state(wire_client):

# In some environments a few goal state requests return a missing RoleInstance; these retries are used to work around that issue
# TODO: Consider retrying on 410 (ResourceGone) as well
incarnation = "unknown"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will this ever be "unknown"? It should always get updated right?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the linter believes it can be used before initialization at line #184. that would happen, i believe, only if _GET_GOAL_STATE_MAX_ATTEMPTS is 0, but I decided to go ahead an initialize it anyways

for _ in range(0, _GET_GOAL_STATE_MAX_ATTEMPTS):
xml_text = wire_client.fetch_config(uri, wire_client.get_header())
xml_doc = parse_doc(xml_text)
incarnation = findtext(xml_doc, "Incarnation")

role_instance = find(xml_doc, "RoleInstance")
if role_instance:
break
time.sleep(0.5)
else:
incarnation = findtext(xml_doc, "Incarnation")
raise ProtocolError("Fetched goal state without a RoleInstance [incarnation {inc}]".format(inc=incarnation))

# Telemetry and the HostGAPlugin depend on the container id/role config; keep them up-to-date each time we fetch the goal state
Expand All @@ -198,7 +194,7 @@ def _fetch_goal_state(wire_client):

wire_client.update_host_plugin(container_id, role_config_name)

return xml_text, xml_doc
return xml_text, xml_doc, incarnation

def _fetch_vm_settings(self, force_update=False):
"""
Expand All @@ -207,30 +203,23 @@ def _fetch_vm_settings(self, force_update=False):
vm_settings, vm_settings_updated = (None, False)

if conf.get_enable_fast_track():
def save_to_history(etag, text):
# The vmSettings are updated independently of the WireServer goal state and they are saved to a separate directory
history = GoalStateHistory(datetime.datetime.utcnow().isoformat(), etag)
history.save_vm_settings(text)

try:
vm_settings, vm_settings_updated = self._wire_client.get_host_plugin().fetch_vm_settings(force_update=force_update)

except VmSettingsNotSupported:
pass
except VmSettingsError as exception:
save_to_history(exception.etag, exception.vm_settings_text)
# ensure we save the vmSettings if there were parsing errors
self._history.save_vm_settings(ExtensionsGoalStateFromVmSettings.redact(exception.vm_settings_text))
raise
except ResourceGoneError:
# retry after refreshing the HostGAPlugin
GoalState.update_host_plugin_headers(self._wire_client)
vm_settings, vm_settings_updated = self._wire_client.get_host_plugin().fetch_vm_settings(force_update=force_update)

if vm_settings_updated:
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

now the vmSettings are added to the history by the caller

save_to_history(vm_settings.etag, vm_settings.get_redacted_text())

return vm_settings
return vm_settings, vm_settings_updated

def _fetch_extended_goal_state(self, xml_text, xml_doc, vm_settings):
def _fetch_extended_goal_state(self, xml_text, xml_doc, force_vm_settings_update=False):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NIT: maybe rename to fetch_complete_goal_state? Extended seemed a bit confusing to me personally

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

in init, I split the member variables in 2 blocks, which I called "basic" and "extended", then initialize_basic_properties initializes the former and fetch_extended_goal_state the latter (I did not name fetch_extended_goal_state "initialize_extended_properties" because there is a fetch operation in it and I want that to be explicit)

"""
Issues HTTP requests (WireServer) for each of the URIs in the goal state (ExtensionsConfig, Certificate, Remote Access users, etc)
and populates the corresponding properties. If the given 'vm_settings' are not None they are used for the extensions goal state,
Expand All @@ -241,8 +230,8 @@ def _fetch_extended_goal_state(self, xml_text, xml_doc, vm_settings):

self._history.save_goal_state(xml_text)

# TODO: at this point we always fetch the extensionsConfig, even if it is not needed, and save it for debugging purposes. Once
# FastTrack is stable this code can be updated to fetch it only when actually needed.
# Always fetch the ExtensionsConfig, even if it is not needed, and save it for debugging purposes. Once FastTrack is stable this code could be updated to
# fetch it only when actually needed.
extensions_config_uri = findtext(xml_doc, "ExtensionsConfig")

if extensions_config_uri is None:
Expand All @@ -252,8 +241,12 @@ def _fetch_extended_goal_state(self, xml_text, xml_doc, vm_settings):
extensions_config = ExtensionsGoalStateFactory.create_from_extensions_config(self._incarnation, xml_text, self._wire_client)
self._history.save_extensions_config(extensions_config.get_redacted_text())

vm_settings, vm_settings_updated = self._fetch_vm_settings(force_update=force_vm_settings_update)

if vm_settings is not None:
self._extensions_goal_state = vm_settings
if vm_settings_updated:
self._history.save_vm_settings(vm_settings.get_redacted_text())
else:
self._extensions_goal_state = extensions_config

Expand Down
21 changes: 4 additions & 17 deletions azurelinuxagent/common/utils/archive.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@

ARCHIVE_DIRECTORY_NAME = 'history'

_MAX_ARCHIVED_STATES = 100
_MAX_ARCHIVED_STATES = 50
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Previously I increased this value from 50 to 100 because I was saving waagent_status.json to a separate directory. Since now I am saving it to the same directory as the goal state, I am reverting the value to the original 50


_CACHE_PATTERNS = [
re.compile(r"^VmSettings.\d+\.json$"),
Expand Down Expand Up @@ -208,25 +208,15 @@ def __init__(self, timestamp, tag=None):
self._root = os.path.join(conf.get_lib_dir(), ARCHIVE_DIRECTORY_NAME, "{0}_{1}".format(timestamp, tag) if tag is not None else timestamp)

def save(self, data, file_name):
def write_to_file(d, f):
with open(f, "w") as h:
h.write(d)

self._save(write_to_file, data, file_name)

def _save_file(self, source_file, target_name):
self._save(shutil.move, source_file, target_name)

def _save(self, function, source, target_name):
try:
if not os.path.exists(self._root):
fileutil.mkdir(self._root, mode=0o700)
target = os.path.join(self._root, target_name)
function(source, target)
with open(os.path.join(self._root, file_name), "w") as handle:
handle.write(data)
except Exception as e:
if not self._errors: # report only 1 error per directory
self._errors = True
logger.warn("Failed to save goal state file {0}: {1} [no additional errors saving the goal state will be reported]".format(target_name, e))
logger.warn("Failed to save {0} to the goal state history: {1} [no additional errors saving the goal state will be reported]".format(file_name, e))

def save_goal_state(self, text):
self.save(text, _GOAL_STATE_FILE_NAME)
Expand All @@ -245,6 +235,3 @@ def save_hosting_env(self, text):

def save_shared_conf(self, text):
self.save(text, _SHARED_CONF_FILE_NAME)

def save_status_file(self, status_file):
self._save_file(status_file, AGENT_STATUS_FILE)
10 changes: 10 additions & 0 deletions azurelinuxagent/common/utils/timeutil.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the Apache License.
import datetime


def create_timestamp():
"""
Returns a string with current UTC time in iso format
"""
return datetime.datetime.utcnow().isoformat()
52 changes: 15 additions & 37 deletions azurelinuxagent/ga/exthandlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@
from azurelinuxagent.common.protocol.restapi import ExtensionStatus, ExtensionSubStatus, Extension, ExtHandlerStatus, \
VMStatus, GoalStateAggregateStatus, ExtensionState, ExtensionRequestedState, ExtensionSettings
from azurelinuxagent.common.utils import textutil
from azurelinuxagent.common.utils.archive import ARCHIVE_DIRECTORY_NAME, AGENT_STATUS_FILE, GoalStateHistory
from azurelinuxagent.common.utils.archive import ARCHIVE_DIRECTORY_NAME
from azurelinuxagent.common.utils.flexible_version import FlexibleVersion
from azurelinuxagent.common.version import AGENT_NAME, CURRENT_VERSION, \
PY_VERSION_MAJOR, PY_VERSION_MICRO, PY_VERSION_MINOR
Expand Down Expand Up @@ -945,8 +945,6 @@ def report_ext_handlers_status(self, incarnation_changed=False, vm_agent_update_

self.report_status_error_state.reset()

self.write_ext_handlers_status_to_info_file(vm_status, incarnation_changed)

return vm_status

except Exception as error:
Expand All @@ -959,52 +957,32 @@ def report_ext_handlers_status(self, incarnation_changed=False, vm_agent_update_
message=msg)
return None

def write_ext_handlers_status_to_info_file(self, vm_status, incarnation_changed):
status_file = os.path.join(conf.get_lib_dir(), AGENT_STATUS_FILE)

if os.path.exists(status_file) and incarnation_changed:
# On new goal state, move the last status report for the previous goal state to the history folder
last_modified = os.path.getmtime(status_file)
timestamp = datetime.datetime.utcfromtimestamp(last_modified).isoformat()
GoalStateHistory(timestamp, "status").save_status_file(status_file)

# Now create/overwrite the status file; this file is kept for debugging purposes only
def get_ext_handlers_status_debug_info(self, vm_status):
status_blob_text = self.protocol.get_status_blob_data()
if status_blob_text is None:
status_blob_text = ""

debug_info = ExtHandlersHandler._get_status_debug_info(vm_status)

status_file_text = \
'''{{
"__comment__": "The __status__ property is the actual status reported to CRP",
"__status__": {0},
"__debug__": {1}
}}
'''.format(status_blob_text, debug_info)

fileutil.write_file(status_file, status_file_text)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

now the file is created by the caller, and this code is exposed as get_ext_handlers_status_debug_info


@staticmethod
def _get_status_debug_info(vm_status):
support_multi_config = dict()
vm_status_data = get_properties(vm_status)
vm_handler_statuses = vm_status_data.get('vmAgent', dict()).get('extensionHandlers')
for handler_status in vm_handler_statuses:
if handler_status.get('name') is not None:
support_multi_config[handler_status.get('name')] = handler_status.get('supports_multi_config')

if vm_status is not None:
vm_status_data = get_properties(vm_status)
vm_handler_statuses = vm_status_data.get('vmAgent', dict()).get('extensionHandlers')
for handler_status in vm_handler_statuses:
if handler_status.get('name') is not None:
support_multi_config[handler_status.get('name')] = handler_status.get('supports_multi_config')

debug_info = {
debug_text = json.dumps({
"agentName": AGENT_NAME,
"daemonVersion": str(version.get_daemon_version()),
"pythonVersion": "Python: {0}.{1}.{2}".format(PY_VERSION_MAJOR, PY_VERSION_MINOR, PY_VERSION_MICRO),
"extensionSupportedFeatures": [name for name, _ in get_agent_supported_features_list_for_extensions().items()],
"supportsMultiConfig": support_multi_config
}
})

return json.dumps(debug_info)
return '''{{
"__comment__": "The __status__ property is the actual status reported to CRP",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NIT: Indent missing for better visuals

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did not want extra indentation in the actual file, so I added only 4 leading spaces

"__status__": {0},
"__debug__": {1}
}}
'''.format(status_blob_text, debug_text)

def report_ext_handler_status(self, vm_status, ext_handler, incarnation_changed):
ext_handler_i = ExtHandlerInstance(ext_handler, self.protocol)
Expand Down
Loading