Skip to content

Commit b78697c

Browse files
dylanbunDylan Bunarto
andauthored
[AMA] Improve the uninstall of AMA by removing files for clean uninstall situation (#2087)
* cleanup of files for a clean uninstall situation. uninstall command is unaffected * wrong commit * refactoring to support current operations and have the update operation cache files for install later on * cleanup * preserving log files into cache. may need to look into rpm vs dpkg due to difference in uninstall command * Removed events folder for extension logs. Removes /opt/microsoft/azuremonitoragent for rpm. Caches logs in /var/opt/microsoft/azuremonitoragent/log * cleanup * fixed path * added caching of tenants * more robust file and directory collection for uninstall * Config files such as tenants are cached, removing changes related to this. * Changed patternt o be use context from a marker file * add comments and update docstring * fixed logging for debug and edited comments * updated doc on update() * quick change to bundlefilename * added logging and tested for python 2 and 3 compat * spacing * code cleanup and changes based on feedback * minor cleanup and moved call to get_uninstall_context() * spacing * updated comments and logging * remove extra period * cleanup code, style changes * cleanup * added constant for AMA data path --------- Co-authored-by: Dylan Bunarto <[email protected]>
1 parent 4d110c8 commit b78697c

File tree

1 file changed

+177
-8
lines changed

1 file changed

+177
-8
lines changed

AzureMonitorAgent/agent.py

Lines changed: 177 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,7 @@
4242
import ama_tst.modules.install.supported_distros as supported_distros
4343
from collections import OrderedDict
4444
from hashlib import sha256
45-
from shutil import copyfile
46-
from shutil import copytree
47-
from shutil import rmtree
45+
from shutil import copyfile, rmtree, copytree, copy2
4846

4947
from threading import Thread
5048
import telegraf_utils.telegraf_config_handler as telhandler
@@ -134,6 +132,8 @@ def __str__(self):
134132
AMAAstTransformConfigMarkerPath = '/etc/opt/microsoft/azuremonitoragent/config-cache/agenttransform.marker'
135133
AMAExtensionLogRotateFilePath = '/etc/logrotate.d/azuremonitoragentextension'
136134
WAGuestAgentLogRotateFilePath = '/etc/logrotate.d/waagent-extn.logrotate'
135+
AmaUninstallContextFile = '/var/opt/microsoft/uninstall-context'
136+
AmaDataPath = '/var/opt/microsoft/azuremonitoragent/'
137137
SupportedArch = set(['x86_64', 'aarch64'])
138138

139139
# Error codes
@@ -538,7 +538,7 @@ def install():
538538
def uninstall():
539539
"""
540540
Uninstall the Azure Monitor Linux Agent.
541-
This is a somewhat soft uninstall. It is not a purge.
541+
Whether it is a purge of all files or preserve of log files depends on the uninstall context file.
542542
Note: uninstall operation times out from WAAgent at 5 minutes
543543
"""
544544

@@ -554,6 +554,13 @@ def uninstall():
554554
if PackageManager != "dpkg" and PackageManager != "rpm":
555555
log_and_exit("Uninstall", UnsupportedOperatingSystem, "The OS has neither rpm nor dpkg." )
556556

557+
# For clean uninstall, gather the file list BEFORE running the uninstall command
558+
# This ensures we have the complete list even after the package manager removes its database
559+
package_files_for_cleanup = []
560+
561+
hutil_log_info("Gathering package file list for clean uninstall before removing package")
562+
package_files_for_cleanup = _get_package_files_for_cleanup()
563+
557564
# Attempt to uninstall each specific
558565

559566
# Try a specific package uninstall for rpm
@@ -604,6 +611,13 @@ def uninstall():
604611
# Retry, since uninstall can fail due to concurrent package operations
605612
try:
606613
exit_code, output = force_uninstall_azure_monitor_agent()
614+
615+
# Remove all files installed by the package that were listed
616+
_remove_package_files_from_list(package_files_for_cleanup)
617+
618+
# Clean up context marker (always do this)
619+
_cleanup_uninstall_context()
620+
607621
except Exception as ex:
608622
exit_code = GenericErrorCode
609623
output = 'Uninstall failed with error: {0}\n' \
@@ -683,6 +697,110 @@ def force_uninstall_azure_monitor_agent():
683697
else:
684698
hutil_log_info("Azure Monitor Agent has been uninstalled.")
685699
return 0, "Azure Monitor Agent has been uninstalled."
700+
701+
def _get_package_files_for_cleanup():
702+
"""
703+
Get the list of files and directories installed by the provided
704+
azuremonitoragent spec that should be removed during uninstall.
705+
This must be called BEFORE the package is uninstalled to ensure the package
706+
manager still has the file list available.
707+
708+
Returns:
709+
tuple: (files_list, directories_to_add) where files_list contains package files
710+
and directories_to_add contains directories that need explicit cleanup
711+
"""
712+
try:
713+
# Get list of files installed by the package
714+
if PackageManager == "dpkg":
715+
# For Debian-based systems
716+
cmd = "dpkg -L azuremonitoragent"
717+
elif PackageManager == "rpm":
718+
# For RPM-based systems
719+
cmd = "rpm -ql azuremonitoragent"
720+
else:
721+
hutil_log_info("Unknown package manager, cannot list package files")
722+
return []
723+
724+
exit_code, output = run_command_and_log(cmd, check_error=False)
725+
726+
if exit_code != 0 or not output:
727+
hutil_log_info("Could not get package file list for cleanup")
728+
return []
729+
730+
# Parse the file list
731+
files = [line.strip() for line in output.strip().split('\n') if line.strip()]
732+
733+
# Collect all azuremonitor-related paths
734+
azuremonitoragent_files = []
735+
736+
for file_path in files:
737+
# Only include files/directories that have "azuremonitor" in their path
738+
# This covers both "azuremonitoragent" and "azuremonitor-*" service files
739+
if "azuremonitor" in file_path:
740+
azuremonitoragent_files.append(file_path)
741+
else:
742+
hutil_log_info("Skipping non-azuremonitor path: {0}".format(file_path))
743+
744+
return azuremonitoragent_files
745+
746+
except Exception as ex:
747+
hutil_log_error("Error gathering package files for cleanup: {0}\n Is Azure Monitor Agent Installed?".format(ex))
748+
return []
749+
750+
def _remove_package_files_from_list(package_files):
751+
"""
752+
Remove all files and directories from the provided list that were installed
753+
by the provided azuremonitoragent spec. This function works with a pre-gathered
754+
list of files from _get_package_files_for_cleanup(), allowing it to work even
755+
after the package has been uninstalled.
756+
757+
Args:
758+
package_files (list): List of file/directory paths to remove
759+
"""
760+
try:
761+
if not package_files:
762+
hutil_log_info("No package files provided for removal")
763+
return
764+
765+
# Build consolidated list of paths to clean up
766+
cleanup_paths = set(package_files) if package_files else set()
767+
768+
# Add directories that need explicit cleanup since on rpm systems
769+
# the initial list for this path does not remove the directories and files
770+
cleanup_paths.add("/opt/microsoft/azuremonitoragent/")
771+
772+
# Determine uninstall context based on if the context file exists
773+
uninstall_context = _get_uninstall_context()
774+
hutil_log_info("Uninstall context: {0}".format(uninstall_context))
775+
776+
if uninstall_context == 'complete':
777+
hutil_log_info("Complete uninstall context - removing everything")
778+
cleanup_paths.add(AmaDataPath)
779+
780+
# Sort paths by depth (deepest first) to avoid removing parent before children
781+
sorted_paths = sorted(cleanup_paths, key=lambda x: x.count('/'), reverse=True)
782+
783+
hutil_log_info("Removing {0} azuremonitor paths".format(len(sorted_paths)))
784+
785+
items_removed = 0
786+
for item_path in sorted_paths:
787+
try:
788+
if os.path.exists(item_path):
789+
if os.path.isdir(item_path):
790+
rmtree(item_path)
791+
hutil_log_info("Removed directory: {0}".format(item_path))
792+
else:
793+
os.remove(item_path)
794+
hutil_log_info("Removed file: {0}".format(item_path))
795+
items_removed += 1
796+
except Exception as ex:
797+
hutil_log_info("Failed to remove {0}: {1}".format(item_path, ex))
798+
799+
hutil_log_info("Removed {0} items total".format(items_removed))
800+
801+
except Exception as ex:
802+
hutil_log_error("Error during file removal from list: {0}\n Were these files removed already?".format(ex))
803+
686804
def enable():
687805
"""
688806
Start the Azure Monitor Linux Agent Service
@@ -1116,12 +1234,63 @@ def disable():
11161234

11171235
def update():
11181236
"""
1119-
Update the current installation of AzureMonitorLinuxAgent
1120-
No logic to install the agent as agent -> install() will be called
1121-
with update because upgradeMode = "UpgradeWithInstall" set in HandlerManifest
1237+
This function is called when the extension is updated.
1238+
It marks the uninstall context to indicate that the next run should be treated as an update rather than a clean install.
1239+
1240+
Always returns 0
11221241
"""
11231242

1124-
return 0, ""
1243+
hutil_log_info("Update operation called for Azure Monitor Agent")
1244+
1245+
try:
1246+
state_dir = os.path.dirname(AmaUninstallContextFile)
1247+
if not os.path.exists(state_dir):
1248+
os.makedirs(state_dir)
1249+
with open(AmaUninstallContextFile, 'w') as f:
1250+
f.write('update\n')
1251+
f.write(datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')) # Timestamp for debugging
1252+
hutil_log_info("Marked uninstall context as 'update'")
1253+
except Exception as ex:
1254+
hutil_log_error("Failed to set uninstall context: {0}\n The uninstall operation will not behave as expected with the uninstall context file missing, defaulting to an uninstall that removes {1}.".format(ex, AmaDataPath))
1255+
1256+
return 0, "Update succeeded"
1257+
1258+
def _get_uninstall_context():
1259+
"""
1260+
Determine the context of this uninstall operation
1261+
1262+
Returns the context as a string:
1263+
'complete' - if this is a clean uninstall
1264+
'update' - if this is an update operation
1265+
Also returns as 'complete' if it fails to read the context file.
1266+
"""
1267+
1268+
try:
1269+
if os.path.exists(AmaUninstallContextFile):
1270+
with open(AmaUninstallContextFile, 'r') as f:
1271+
context = f.read().strip().split('\n')[0]
1272+
hutil_log_info("Found uninstall context: {0}".format(context))
1273+
return context
1274+
else:
1275+
hutil_log_info("Uninstall context file does not exist, defaulting to 'complete'")
1276+
except Exception as ex:
1277+
hutil_log_error("Failed to read uninstall context file: {0}\n The uninstall operation will not behave as expected with the uninstall context file missing, defaulting to an uninstall that removes {1}.".format(ex, AmaDataPath))
1278+
1279+
return 'complete'
1280+
1281+
def _cleanup_uninstall_context():
1282+
"""
1283+
Clean up uninstall context marker
1284+
"""
1285+
1286+
try:
1287+
if os.path.exists(AmaUninstallContextFile):
1288+
os.remove(AmaUninstallContextFile)
1289+
hutil_log_info("Removed uninstall context file")
1290+
else:
1291+
hutil_log_info("Uninstall context file does not exist, nothing to remove")
1292+
except Exception as ex:
1293+
hutil_log_error("Failed to cleanup uninstall context: {0}\n This may result in unintended behavior as described.\nIf the marker file exists and cannot be removed, uninstall will continue to keep the {1} path, leading users to have to remove it manually.".format(ex, AmaDataPath))
11251294

11261295
def restart_launcher():
11271296
# start agent launcher

0 commit comments

Comments
 (0)