-
Notifications
You must be signed in to change notification settings - Fork 372
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -15,7 +15,6 @@ | |
# limitations under the License. | ||
# | ||
# Requires Python 2.6+ and Openssl 1.0+ | ||
import datetime | ||
import os | ||
import re | ||
import time | ||
|
@@ -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" | ||
|
@@ -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) | ||
|
||
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 | ||
|
@@ -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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. where/when is this full goal state saved to history folder? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
|
||
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() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why do we need different timestamp? can't we use L#135 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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") | ||
|
@@ -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" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Will this ever be "unknown"? It should always get updated right? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
|
@@ -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): | ||
""" | ||
|
@@ -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: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. NIT: maybe rename to There was a problem hiding this comment. Choose a reason for hiding this commentThe 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, | ||
|
@@ -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: | ||
|
@@ -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 | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -38,7 +38,7 @@ | |
|
||
ARCHIVE_DIRECTORY_NAME = 'history' | ||
|
||
_MAX_ARCHIVED_STATES = 100 | ||
_MAX_ARCHIVED_STATES = 50 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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$"), | ||
|
@@ -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) | ||
|
@@ -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) |
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() |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 | ||
|
@@ -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: | ||
|
@@ -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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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", | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. NIT: Indent missing for better visuals There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) | ||
|
There was a problem hiding this comment.
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