diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b4aa92d..7233aef 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -183,11 +183,10 @@ jobs: tag_name: ${{ env.VERSION }} release_name: ${{ env.VERSION }} body: | - **Changes:** - - - Fixed update process on RHEL based systems - - Added a new parameter to update only a list of packages - - Reposerver module: added the feature to receive a list of packages to update + **Changes**: + - Added `--dry-run` option to simulate an update + - Removed the `--set-update-method` and `--get-update-method` options, now the update method is always one package at a time (easier to log) + - Reposerver module: improved logs output cleaning before sending it to the reposerver draft: false prerelease: false diff --git a/README.md b/README.md index d954d92..e53d7ae 100644 --- a/README.md +++ b/README.md @@ -59,9 +59,7 @@ It should help you **installing** and starting using linupdate. --assume-yes, -y Answer yes to all questions --check-updates, -cu Only check for updates and exit --ignore-exclude, -ie Ignore all package exclusions - --get-update-method Get current update method - --set-update-method [one_by_one|global] Set update method: one_by_one (update packages one by one, one apt command executed for each package) or global (update all packages at once, one single apt command executed for all packages) - --exit-on-package-update-error [true|false] When update method is one_by_one, immediately exit if an error occurs during package update and do not update the remaining packages + --exit-on-package-update-error [true|false] Immediately exit if an error occurs during package update and do not update the remaining packages Packages exclusion and services restart diff --git a/linupdate.py b/linupdate.py index aa2b1ad..d0073de 100755 --- a/linupdate.py +++ b/linupdate.py @@ -104,13 +104,14 @@ def main(): my_args.ignore_exclude, my_args.check_updates, my_args.dist_upgrade, - my_args.keep_oldconf) + my_args.keep_oldconf, + my_args.dry_run) # Execute post-update modules functions my_module.post(my_package.summary) # Restart services - my_service.restart(my_package.summary) + my_service.restart(my_package.summary, my_args.dry_run) # Check if reboot is required if my_system.reboot_required() is True: diff --git a/src/controllers/App/Config.py b/src/controllers/App/Config.py index 7e90c96..0941aa5 100644 --- a/src/controllers/App/Config.py +++ b/src/controllers/App/Config.py @@ -109,14 +109,6 @@ def check_conf(self): if 'update' not in configuration: raise Exception('update key is missing in ' + self.config_file) - # Check if update.method is set - if 'method' not in configuration['update']: - raise Exception('update.method key is missing in ' + self.config_file) - - # Check if update.method is not empty - if configuration['update']['method'] == None: - raise Exception('update.method key is empty in ' + self.config_file) - # Check if update.exit_on_package_update_error is set if 'exit_on_package_update_error' not in configuration['update']: raise Exception('update.exit_on_package_update_error key is missing in ' + self.config_file) @@ -409,37 +401,6 @@ def get_mail_recipient(self): return configuration['main']['mail']['recipient'] - #----------------------------------------------------------------------------------------------- - # - # Get update method - # - #----------------------------------------------------------------------------------------------- - def get_update_method(self): - # Get current configuration - configuration = self.get_conf() - - return configuration['update']['method'] - - - #----------------------------------------------------------------------------------------------- - # - # Set update method - # - #----------------------------------------------------------------------------------------------- - def set_update_method(self, method: str): - if method not in ['one_by_one', 'global']: - raise Exception('Invalid update method: ' + method) - - # Get current configuration - configuration = self.get_conf() - - # Set method - configuration['update']['method'] = method - - # Write config file - self.write_conf(configuration) - - #----------------------------------------------------------------------------------------------- # # Set exit on package update error @@ -449,7 +410,7 @@ def set_exit_on_package_update_error(self, exit_on_package_update_error: bool): # Get current configuration configuration = self.get_conf() - # Set method + # Set exit on package update error configuration['update']['exit_on_package_update_error'] = exit_on_package_update_error # Write config file diff --git a/src/controllers/App/Utils.py b/src/controllers/App/Utils.py index edcbed2..14feafb 100644 --- a/src/controllers/App/Utils.py +++ b/src/controllers/App/Utils.py @@ -32,3 +32,20 @@ def remove_ansi(self, text): # If an exception occurs, simply return the original text as it is except Exception as e: return text + + #----------------------------------------------------------------------------------------------- + # + # Clean log text + # + #----------------------------------------------------------------------------------------------- + def clean_log(self, text): + # First, remove ANSI escape codes + text = self.remove_ansi(text) + + # Replace double line breaks with single line breaks before '|' character (occurs mainly with apt logs) + text = re.sub(r'\n\n \|', '\n |', text) + + # Remove leading and trailing whitespaces + text = text.strip() + + return text diff --git a/src/controllers/Args.py b/src/controllers/Args.py index b24b0fa..d891375 100644 --- a/src/controllers/Args.py +++ b/src/controllers/Args.py @@ -44,6 +44,7 @@ def parse(self): Args.packages_to_update = [] Args.dist_upgrade = False Args.keep_oldconf = True + Args.dry_run = False myApp = App() myAppConfig = Config() @@ -77,6 +78,8 @@ def parse(self): parser.add_argument("--update", "-u", action="store", nargs='?', default="null") # Dist upgrade parser.add_argument("--dist-upgrade", "-du", action="store_true", default="null") + # Dry run + parser.add_argument("--dry-run", action="store_true", default="null") # Keep oldconf parser.add_argument("--keep-oldconf", action="store_true", default="null") # Force / assume-yes @@ -85,10 +88,6 @@ def parse(self): parser.add_argument("--check-updates", "-cu", action="store_true", default="null") # Ignore exclude parser.add_argument("--ignore-exclude", "-ie", action="store_true", default="null") - # Get update method - parser.add_argument("--get-update-method", action="store", nargs='?', default="null") - # Set Update method - parser.add_argument("--set-update-method", action="store", nargs='?', default="null") # Exit on package update error parser.add_argument("--exit-on-package-update-error", action="store", nargs='?', default="null") @@ -310,6 +309,12 @@ def parse(self): Args.packages_to_update.append({'name': package.strip()}) except Exception as e: raise ArgsException('Could not parse update list: ' + str(e)) + + # + # If --dry-run param has been set + # + if args.dry_run != "null": + Args.dry_run = True # # If --ignore-exclude param has been set @@ -341,28 +346,6 @@ def parse(self): if args.assume_yes != "null": Args.assume_yes = True - # - # If --get-update-method param has been set - # - if args.get_update_method != "null": - try: - update_method = myAppConfig.get_update_method() - print(' Current update method: ' + Fore.GREEN + update_method + Style.RESET_ALL, end='\n\n') - myExit.clean_exit(0, False) - except Exception as e: - raise ArgsException('Could not get update method: ' + str(e)) - - # - # If --set-update-method param has been set - # - if args.set_update_method != "null": - try: - myAppConfig.set_update_method(args.set_update_method) - print(' Update method set to ' + Fore.GREEN + args.set_update_method + Style.RESET_ALL, end='\n\n') - myExit.clean_exit(0, False) - except Exception as e: - raise ArgsException('Could not set update method: ' + str(e)) - # # If --exit-on-package-update-error param has been set # @@ -510,9 +493,6 @@ def parse(self): print(' Setting services to restart after package update: ' + Fore.GREEN) - for service in services: - print(' ▪ ' + service) - # If no service is set to restart if not services: print(' ▪ None') @@ -658,6 +638,12 @@ def help(self): ], 'description': 'Enable distribution upgrade when updating packages (Debian based OS only)' }, + { + 'args': [ + '--dry-run' + ], + 'description': 'Simulate the update process (do not update packages)' + }, { 'args': [ '--keep-oldconf' @@ -685,25 +671,12 @@ def help(self): ], 'description': 'Ignore all package exclusions' }, - { - 'args': [ - '--get-update-method', - ], - 'description': 'Get current update method' - }, - { - 'args': [ - '--set-update-method', - ], - 'option': 'one_by_one|global', - 'description': 'Set update method: one_by_one (update packages one by one, one apt command executed for each package) or global (update all packages at once, one single apt command executed for all packages)' - }, { 'args': [ '--exit-on-package-update-error', ], 'option': 'true|false', - 'description': 'When update method is one_by_one, immediately exit if an error occurs during package update and do not update the remaining packages' + 'description': 'Immediately exit if an error occurs during package update and do not update the remaining packages' }, { 'title': 'Packages exclusion and services restart' diff --git a/src/controllers/Module/Reposerver/Agent.py b/src/controllers/Module/Reposerver/Agent.py index 56cb7bb..75683ec 100644 --- a/src/controllers/Module/Reposerver/Agent.py +++ b/src/controllers/Module/Reposerver/Agent.py @@ -8,6 +8,8 @@ import json import sys from pathlib import Path +from shutil import rmtree +import os # Import classes from src.controllers.Log import Log @@ -24,6 +26,17 @@ def __init__(self): self.reposerverStatusController = Status() self.packageController = Package() + # Set default values + self.authenticated = False + + # Root directory for the requests logs + self.request_dir = '/opt/linupdate/tmp/reposerver/requests' + + # Create the directories to store the logs if they do not exist + if not Path(self.request_dir).is_dir(): + Path(self.request_dir).mkdir(parents=True, exist_ok=True) + + #----------------------------------------------------------------------------------------------- # # General checks for running the agent @@ -139,20 +152,123 @@ def set_request_status(self, request_id, status): self.websocket.send(json.dumps(json_data)) + #----------------------------------------------------------------------------------------------- + # + # Send remaining requests logs to the reposerver + # + #----------------------------------------------------------------------------------------------- + def send_remaining_requests_logs(self): + try: + # Quit if not authenticated to the reposerver + if not self.authenticated: + return + + # Get all requests logs directories + requests_dirs = Path(self.request_dir).iterdir() + + # Then order them by name + requests_dirs = sorted(requests_dirs, key=lambda x: x.name) + + # For each directory, get its name (request id) + for request_dir in requests_dirs: + try: + status = 'unknown' + summary = None + error = None + logcontent = None + + # Check if it is a directory + if not request_dir.is_dir(): + continue + + # Only process directories with a creation time older than 5 minutes + if time.time() - os.path.getmtime(request_dir) < 300: + continue + + # If the directory is empty, then delete it + if not any(request_dir.iterdir()): + rmtree(request_dir) + continue + + # Initialize json + json_response = { + 'response-to-request': {} + } + + # Get request id and set it in the json + request_id = request_dir.name + json_response['response-to-request']['request-id'] = request_id + + # Get general log, if log file exists, and set it in the json + if Path(self.request_dir + '/' + request_id + '/log').is_file(): + with open(self.request_dir + '/' + request_id + '/log', 'r') as file: + # Get log content and remove ANSI escape codes + logcontent = Utils().clean_log(file.read()) + + # Get status, if log file exists, and set it in the json + if Path(self.request_dir + '/' + request_id + '/status').is_file(): + with open(self.request_dir + '/' + request_id + '/status', 'r') as file: + status = file.read().strip() + + # Get summary, if log file exists, and set it in the json + if Path(self.request_dir + '/' + request_id + '/summary').is_file(): + with open(self.request_dir + '/' + request_id + '/summary', 'r') as file: + summary = file.read().strip() + # Convert the string to a json + summary = json.loads(summary) + + # Get error, if log file exists, and set it in the json + if Path(self.request_dir + '/' + request_id + '/error').is_file(): + with open(self.request_dir + '/' + request_id + '/error', 'r') as file: + error = file.read().strip() + + if status: + json_response['response-to-request']['status'] = status + + if summary: + json_response['response-to-request']['summary'] = summary + + if error: + json_response['response-to-request']['error'] = error + + if logcontent: + json_response['response-to-request']['log'] = logcontent + + # Send the response + print('[reposerver-agent] Sending remaining requests logs for request id #' + request_id + ' with status: ' + status) + self.websocket.send(json.dumps(json_response)) + + # Delete the directory and all its content + if Path(self.request_dir + '/' + request_id).is_dir(): + rmtree(self.request_dir + '/' + request_id) + except Exception as e: + raise Exception('could not send remaining requests logs for request id #' + request_id + ': ' + str(e)) + except Exception as e: + print('[reposerver-agent] Error: ' + str(e)) + + #----------------------------------------------------------------------------------------------- # # On message received from the websocket # #----------------------------------------------------------------------------------------------- def websocket_on_message(self, ws, message): - # Decode JSON message - message = json.loads(message) + # Default values request_id = None summary = None - log = '/tmp/linupdate.reposerver.request.log' + status = None + error = None + + # Decode JSON message + message = json.loads(message) + + # Default log file path, could be overwritten if request id is present + log = '/opt/linupdate/tmp/reposerver/requests/log' + # Lock to prevent service restart while processing the request lock = '/tmp/linupdate.reposerver.request.lock' - error = None + + # Default json response json_response = { 'response-to-request': { 'request-id': '', @@ -162,134 +278,182 @@ def websocket_on_message(self, ws, message): } } - # Create a lock file to prevent service restart while processing the request - if not Path(lock).is_file(): - Path(lock).touch() - - # If the message contains 'request' - if 'request' in message: - try: - # Retrieve request Id if any (authenticate request does not have an id) - if 'request-id' in message: - request_id = message['request-id'] - - # Case the request is 'authenticate', then authenticate to the reposerver - if message['request'] == 'authenticate': - print('[reposerver-agent] Authenticating to the reposerver') - - id = self.configuration['client']['auth']['id'] - token = self.configuration['client']['auth']['token'] - - # Send a response to authenticate to the reposerver, with id and token - self.websocket.send(json.dumps({'response-to-request': {'request': 'authenticate', 'auth-id': id, 'token': token}})) - - # Case the request is 'request-general-infos', then send general informations to the reposerver - elif message['request'] == 'request-general-infos': - print('[reposerver-agent] Reposerver requested general informations') - with Log(log): - self.reposerverStatusController.send_general_info() - - # Case the request is 'request-packages-infos', then send packages informations to the reposerver - elif message['request'] == 'request-packages-infos': - print('[reposerver-agent] Reposerver requested packages informations') - - # Send a response to the reposerver to make the request as running - self.set_request_status(request_id, 'running') - - # Log everything to the log file - with Log(log): - self.reposerverStatusController.send_packages_info() + try: + # Create a lock file to prevent service restart while processing the request + if not Path(lock).is_file(): + Path(lock).touch() - # Case the request is 'update-all-packages', then update all packages - elif message['request'] == 'update-all-packages': - print('[reposerver-agent] Reposerver requested all packages update') + # If the message contains 'request' + if 'request' in message: + try: + # Retrieve request Id if any (authenticate request does not have an id) + if 'request-id' in message: + request_id = str(message['request-id']) - # Send a response to the reposerver to make the request as running - self.set_request_status(request_id, 'running') + # Create a new directory for the request id + if not Path(self.request_dir + '/' + request_id).is_dir(): + Path(self.request_dir + '/' + request_id).mkdir(parents=True, exist_ok=True) - # Log everything to the log file - with Log(log): - self.packageController.update([], True) + # Set new log files path for the request id + log = self.request_dir + '/' + request_id + '/log' + log_status = self.request_dir + '/' + request_id + '/status' + log_error = self.request_dir + '/' + request_id + '/error' + log_summary = self.request_dir + '/' + request_id + '/summary' - # Send a summary to the reposerver, with the summary of the update (number of packages updated or failed) - summary = self.packageController.summary + # Case the request is 'authenticate', then authenticate to the reposerver + if message['request'] == 'authenticate': + print('[reposerver-agent] Authenticating to the reposerver') - # Case the request is 'request-specific-packages-installation', then update a list of packages - # A list of packages must be provided in the message - elif message['request'] == 'request-specific-packages-installation': - if 'data' in message: - if 'packages' in message['data'] and len(message['data']['packages']) > 0: - print('[reposerver-agent] Reposerver requested to update a list of packages') + id = self.configuration['client']['auth']['id'] + token = self.configuration['client']['auth']['token'] - # Send a response to the reposerver to make the request as running - self.set_request_status(request_id, 'running') + # Send a response to authenticate to the reposerver, with id and token + self.websocket.send(json.dumps({'response-to-request': {'request': 'authenticate', 'auth-id': id, 'token': token}})) - # Log everything to the log file - with Log(log): - self.packageController.update(message['data']['packages'], True) + # Case the request is 'request-general-infos', then send general informations to the reposerver + elif message['request'] == 'request-general-infos': + print('[reposerver-agent] Reposerver requested general informations') + with Log(log): + self.reposerverStatusController.send_general_info() - # Send a summary to the reposerver, with the summary of the installation (number of packages installed or failed) - summary = self.packageController.summary + # Case the request is 'request-packages-infos', then send packages informations to the reposerver + elif message['request'] == 'request-packages-infos': + print('[reposerver-agent] Reposerver requested packages informations') - else: - raise Exception('unknown request sent by reposerver: ' + message['request']) + # Send a response to the reposerver to make the request as running + self.set_request_status(request_id, 'running') - # If request was successful - status = 'completed' + # Log everything to the log file + with Log(log): + self.reposerverStatusController.send_packages_info() - # If request failed - except Exception as e: - print('[reposerver-agent] Error: ' + str(e)) - status = 'failed' - error = str(e) + # Case the request is 'update-all-packages', then update all packages + elif message['request'] == 'update-all-packages': + print('[reposerver-agent] Reposerver requested all packages update') - finally: - # If there was a request id, then send a response to reposerver to make the request as completed - if request_id: - # Set request id - json_response['response-to-request']['request-id'] = request_id + # Send a response to the reposerver to make the request as running + self.set_request_status(request_id, 'running') - # Set status - json_response['response-to-request']['status'] = status + # Log everything to the log file + with Log(log): + self.packageController.update([], True) - # If there was an error - if error: - json_response['response-to-request']['error'] = error + # TODO + # Take into account the --dry-run option + # Restart/reload services if the user said so + # Execute reposever post-update actions - # If there is a summary - if summary: - json_response['response-to-request']['summary'] = summary + # Send a summary to the reposerver, with the summary of the update (number of packages updated or failed) + summary = self.packageController.summary - # If there is a log file - if log and Path(log).is_file(): - # Get log file content - try: - with open(log, 'r') as file: - # Get log content and remove ANSI escape codes - logcontent = Utils().remove_ansi(file.read()) + # Case the request is 'request-specific-packages-installation', then update a list of packages + # A list of packages must be provided in the message + elif message['request'] == 'request-specific-packages-installation': + if 'data' in message: + if 'packages' in message['data'] and len(message['data']['packages']) > 0: + print('[reposerver-agent] Reposerver requested to update a list of packages') - # Delete the log file - Path(log).unlink() - except Exception as e: - # If content could not be read, then generate an error message - logcontent = 'Error: could not read log file: ' + str(e) + # Send a response to the reposerver to make the request as running + self.set_request_status(request_id, 'running') - json_response['response-to-request']['log'] = logcontent + # Log everything to the log file + with Log(log): + self.packageController.update(message['data']['packages'], True) - # Send the response - self.websocket.send(json.dumps(json_response)) + # Send a summary to the reposerver, with the summary of the installation (number of packages installed or failed) + summary = self.packageController.summary - # If the message contains 'info' - if 'info' in message: - print('[reposerver-agent] Received info message from reposerver: ' + message['info']) + else: + raise Exception('unknown request sent by reposerver: ' + message['request']) - # If the message contains 'error' - if 'error' in message: - print('[reposerver-agent] Received error message from reposerver: ' + message['error']) + # If request was successful + status = 'completed' - # Delete the lock file - if Path(lock).is_file(): - Path(lock).unlink() + # If request failed + except Exception as e: + print('[reposerver-agent] Error: ' + str(e)) + status = 'failed' + error = str(e) + + finally: + # If there was a request id, then send a response to reposerver to make the request as completed + if request_id: + # First, save the log, status, summary and error to a file, in case the message cannot be sent + # It will be sent later when the agent is running again + if status: + with open(log_status, 'w') as file: + file.write(status) + if summary: + with open(log_summary, 'w') as file: + # Convert summary to string to write it to the file + json.dump(summary, file) + if error: + with open(log_error, 'w') as file: + file.write(error) + + # Then try to send the response + + # Set request id + json_response['response-to-request']['request-id'] = request_id + + # Set status + json_response['response-to-request']['status'] = status + + # If there was an error + if error: + json_response['response-to-request']['error'] = error + + # If there is a summary + if summary: + json_response['response-to-request']['summary'] = summary + + # If there is a log file + if log and Path(log).is_file(): + # Get log file content + try: + with open(log, 'r') as file: + # Get log content and remove ANSI escape codes + logcontent = Utils().clean_log(file.read()) + except Exception as e: + # If content could not be read, then generate an error message + logcontent = 'Error: could not read log file: ' + str(e) + + json_response['response-to-request']['log'] = logcontent + + # Try to send the response to the reposerver + # Note: impossible to use try/except here, because no exception is raised directly, + # if there is an error then it is the on_error function that is called + self.websocket.send(json.dumps(json_response)) + + # If the message contains 'info' + if 'info' in message: + print('[reposerver-agent] Received info message from reposerver: ' + message['info']) + + # If the message is 'Authentication successful', then set authenticated to True + if message['info'] == 'Authentication successful': + self.authenticated = True + + # If the message is 'Request response received', then delete the remaining logs + if message['info'] == 'Request response received': + # First retrieve the request id + if 'request-id' in message: + request_id = message['request-id'] + + # If the server has tell what kinf of data it has received, then delete the corresponding files if they exist + if 'data' in message: + for data in message['data']: + if Path(self.request_dir + '/' + request_id + '/' + data).is_file(): + Path(self.request_dir + '/' + request_id + '/' + data).unlink() + + # If the message contains 'error' + if 'error' in message: + print('[reposerver-agent] Received error message from reposerver: ' + message['error']) + + # If all goes well or if an exception is raised, then delete the lock file + finally: + # Delete the lock file + if Path(lock).is_file(): + Path(lock).unlink() #----------------------------------------------------------------------------------------------- @@ -308,6 +472,7 @@ def websocket_on_error(self, ws, error): #----------------------------------------------------------------------------------------------- def websocket_on_close(self, ws, close_status_code, close_msg): print('[reposerver-agent] Reposerver websocket connection closed with status code: ' + str(close_status_code) + ' and message: ' + close_msg) + self.authenticated = False raise Exception('reposerver websocket connection closed') @@ -356,12 +521,14 @@ def websocket_client(self): # self.websocket.on_open = self.websocket_on_open self.websocket.run_forever() - except KeyboardInterrupt: + except KeyboardInterrupt as e: self.websocket_is_running = False self.websocket_exception = str(e) + self.authenticated = False except Exception as e: self.websocket_is_running = False self.websocket_exception = str(e) + self.authenticated = False #----------------------------------------------------------------------------------------------- @@ -378,9 +545,6 @@ def main(self): self.websocket_is_running = False self.websocket_exception = None - # Checking that all the necessary elements are present for the agent execution - self.run_general_checks() - # Executing regular tasks while True: # Checking that all the necessary elements are present for the agent execution. @@ -424,6 +588,9 @@ def main(self): thread.start() except Exception as e: raise Exception('reposerver websocket connection failed: ' + str(e)) + + # If some requests logs were not sent (because the program crashed or the reposerver was unavailable for eg), then send them now + self.send_remaining_requests_logs() time.sleep(5) diff --git a/src/controllers/Package/Apt.py b/src/controllers/Package/Apt.py index 04eedf7..b412c3a 100644 --- a/src/controllers/Package/Apt.py +++ b/src/controllers/Package/Apt.py @@ -139,6 +139,29 @@ def get_available_packages(self, dist_upgrade: bool = False): return list + #----------------------------------------------------------------------------------------------- + # + # Wait for dpkg lock to be released + # Default timeout is 30 seconds + # + #----------------------------------------------------------------------------------------------- + def wait_for_dpkg_lock(self, timeout: int = 30): + import fcntl + from time import sleep + + while timeout > 0: + with open('/var/lib/dpkg/lock', 'w') as handle: + try: + fcntl.lockf(handle, fcntl.LOCK_EX | fcntl.LOCK_NB) + return + except IOError: + pass + + timeout -= 1 + sleep(1) + + raise Exception('could not acquire dpkg lock (timeout ' + str(timeout) + 's)') + #----------------------------------------------------------------------------------------------- # # Clear apt cache @@ -146,6 +169,9 @@ def get_available_packages(self, dist_upgrade: bool = False): #----------------------------------------------------------------------------------------------- def clear_cache(self): try: + # Wait for the lock to be released + self.wait_for_dpkg_lock() + self.aptcache.clear() except Exception as e: raise Exception('could not clear apt cache: ' + str(e)) @@ -159,12 +185,15 @@ def clear_cache(self): def update_cache(self): try: # Clear cache - self.aptcache.clear() + self.wait_for_dpkg_lock() + self.clear_cache() # Update cache + self.wait_for_dpkg_lock() self.aptcache.update() # Reopen cache + self.wait_for_dpkg_lock() self.aptcache.open(None) except Exception as e: @@ -238,8 +267,8 @@ def remove_all_exclusions(self): # Update packages # #----------------------------------------------------------------------------------------------- - def update(self, packagesList, update_method: str = 'one_by_one', exit_on_package_update_error: bool = True): - # Log file to store each package update output (when 'one_by_one' method is used) + def update(self, packagesList, exit_on_package_update_error: bool = True, dry_run: bool = False): + # Log file to store each package update output log = '/tmp/linupdate-update-package.log' # Package update summary @@ -256,164 +285,115 @@ def update(self, packagesList, update_method: str = 'one_by_one', exit_on_packag } } - # If update_method is 'one_by_one', update packages one by one (one command per package) - if update_method == 'one_by_one': - # Loop through the list of packages to update - for pkg in packagesList: - # If the package is excluded, ignore it - if pkg['excluded']: - continue + # Loop through the list of packages to update + for pkg in packagesList: + # If the package is excluded, ignore it + if pkg['excluded']: + continue - # If log file exists, remove it - if Path(log).is_file(): - Path(log).unlink() - - with Log(log): - print('\n ▪ Updating ' + Fore.GREEN + pkg['name'] + Style.RESET_ALL + ' (' + pkg['current_version'] + ' → ' + pkg['available_version'] + '):') - - # Before updating, check If package is already in the latest version, if so, skip it - # It means that it has been updated previously by another package, probably because it was a dependency - # Get the current version of the package from apt cache. Use a new temporary apt cache to be sure it is up to date - try: - temp_apt_cache = apt.Cache() - temp_apt_cache.open(None) - - # If version in cache is the same the target version, skip the update - if temp_apt_cache[pkg['name']].installed.version == pkg['available_version']: - print(Fore.GREEN + ' ✔ ' + Style.RESET_ALL + pkg['name'] + ' is already up to date (updated with another package).') - - # Mark the package as already updated - self.summary['update']['success']['count'] += 1 - - # Also add the package to the list of successful packages - self.summary['update']['success']['packages'][pkg['name']] = { - 'version': pkg['available_version'], - 'log': 'Already up to date (updated with another package).' - } - - # Continue to the next package - continue - - except Exception as e: - raise Exception('Could not retrieve current version of package ' + pkg['name'] + ': ' + str(e)) - - # If --keep-oldconf is True, then keep the old configuration file - if self.keep_oldconf: - cmd = [ - 'apt-get', 'install', pkg['name'] + '=' + pkg['available_version'], '-y', - # Debug only - # '--dry-run', - '-o', 'Dpkg::Options::=--force-confdef', - '-o', 'Dpkg::Options::=--force-confold' - ] - else: - cmd = ['apt-get', 'install', pkg['name'] + '=' + pkg['available_version'], '-y'] - - popen = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, universal_newlines=True) - - # Print lines as they are read - for line in popen.stdout: - # Deal with carriage return - parts = line.split('\r') - # for part in parts[:-1]: - # sys.stdout.write('\r' + ' | ' + part.strip() + '\n') - # sys.stdout.flush() - buffer = parts[-1] - sys.stdout.write('\r' + ' | ' + buffer.strip() + '\n') - sys.stdout.flush() - - # Wait for the command to finish - popen.wait() + # If log file exists, remove it + if Path(log).is_file(): + Path(log).unlink() + + with Log(log): + print('\n ▪ Updating ' + Fore.GREEN + pkg['name'] + Style.RESET_ALL + ' (' + pkg['current_version'] + ' → ' + pkg['available_version'] + '):') - # If command failed, either raise an exception or print a warning - if popen.returncode != 0: - # Add the package to the list of failed packages - self.summary['update']['failed']['count'] += 1 + # Before updating, check If package is already in the latest version, if so, skip it + # It means that it has been updated previously by another package, probably because it was a dependency + # Get the current version of the package from apt cache. Use a new temporary apt cache to be sure it is up to date + try: + temp_apt_cache = apt.Cache() + temp_apt_cache.open(None) - # Also add the package to the list of failed packages + # If version in cache is the same the target version, skip the update + if temp_apt_cache[pkg['name']].installed.version == pkg['available_version']: + print(Fore.GREEN + ' ✔ ' + Style.RESET_ALL + pkg['name'] + ' is already up to date (updated with another package).') - # First get log content - with open(log, 'r') as file: - log_content = Utils().remove_ansi(file.read()) + # Mark the package as already updated + self.summary['update']['success']['count'] += 1 - self.summary['update']['failed']['packages'][pkg['name']] = { + # Also add the package to the list of successful packages + self.summary['update']['success']['packages'][pkg['name']] = { 'version': pkg['available_version'], - 'log': log_content + 'log': 'Already up to date (updated with another package).' } - # If error is critical, raise an exception - if (exit_on_package_update_error == True): - raise Exception('Error while updating ' + pkg['name'] + '.') + # Continue to the next package + continue + + except Exception as e: + raise Exception('Could not retrieve current version of package ' + pkg['name'] + ': ' + str(e)) + + # If --keep-oldconf is True, then keep the old configuration file + if self.keep_oldconf: + cmd = [ + 'apt-get', 'install', pkg['name'] + '=' + pkg['available_version'], '-y', + # Debug only + # '--dry-run', + '-o', 'Dpkg::Options::=--force-confdef', + '-o', 'Dpkg::Options::=--force-confold' + ] + else: + cmd = ['apt-get', 'install', pkg['name'] + '=' + pkg['available_version'], '-y'] - # Else print an error message and continue to the next package - else: - print(Fore.RED + ' ✕ ' + Style.RESET_ALL + 'Error while updating ' + pkg['name'] + '.') - continue + # If --dry-run is True, then simulate the update + if dry_run: + cmd.append('--dry-run') - # Close the pipe - popen.stdout.close() + popen = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, universal_newlines=True) - # If command succeeded, increment the success counter - self.summary['update']['success']['count'] += 1 + # Print lines as they are read + for line in popen.stdout: + # Deal with carriage return + parts = line.split('\r') + for part in parts[:-1]: + sys.stdout.write('\r' + ' | ' + part.strip() + '\n') + sys.stdout.flush() + buffer = parts[-1] + sys.stdout.write('\r' + ' | ' + buffer.strip() + '\n') + sys.stdout.flush() + + # Wait for the command to finish + popen.wait() - # Also add the package to the list of successful packages + # Get log content + with open(log, 'r') as file: + log_content = Utils().clean_log(file.read()) - # First get log content - with open(log, 'r') as file: - log_content = Utils().remove_ansi(file.read()) + # If command failed, either raise an exception or print a warning + if popen.returncode != 0: + # Add the package to the list of failed packages + self.summary['update']['failed']['count'] += 1 - self.summary['update']['success']['packages'][pkg['name']] = { + # Add the package to the list of failed packages + self.summary['update']['failed']['packages'][pkg['name']] = { 'version': pkg['available_version'], 'log': log_content } - # Print a success message - print(Fore.GREEN + ' ✔ ' + Style.RESET_ALL + pkg['name'] + ' updated successfully.') - - # If update_method is 'global', update all packages at once (one command) - if update_method == 'global': - # If --keep-oldconf is True, then keep the old configuration file - if self.keep_oldconf: - cmd = [ - 'apt-get', 'upgrade', '-y', - '-o', 'Dpkg::Options::=--force-confdef', - '-o', 'Dpkg::Options::=--force-confold' - ] - else: - cmd = ['apt-get', 'upgrade', '-y'] - - popen = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, bufsize=1, universal_newlines=True) - - # Print lines as they are read - for line in popen.stdout: - # Deal with carriage return - parts = line.split('\r') - # for part in parts[:-1]: - # sys.stdout.write('\r' + ' | ' + part.strip() + '\n') - # sys.stdout.flush() - buffer = parts[-1] - sys.stdout.write('\r' + ' | ' + buffer.strip() + '\n') - sys.stdout.flush() - - # Wait for the command to finish - popen.wait() - - # If command failed, either raise an exception or print a warning - if popen.returncode != 0: - # If error is critical, raise an exception - if (exit_on_package_update_error == True): - raise Exception('Error while updating packages.') - - # Else print an error message - else: - print(Fore.RED + ' ✕ ' + Style.RESET_ALL + 'Error.') + # If error is critical, raise an exception + if (exit_on_package_update_error == True): + raise Exception('Error while updating ' + pkg['name'] + '.') - else: - # Print a success message - print(Fore.GREEN + ' ✔ ' + Style.RESET_ALL + 'Done.') + # Else print an error message and continue to the next package + else: + print(Fore.RED + ' ✕ ' + Style.RESET_ALL + 'Error while updating ' + pkg['name'] + '.') + continue + + # Close the pipe + popen.stdout.close() + + # If command succeeded, increment the success counter + self.summary['update']['success']['count'] += 1 - # Close the pipe - popen.stdout.close() + # Add the package to the list of successful packages + self.summary['update']['success']['packages'][pkg['name']] = { + 'version': pkg['available_version'], + 'log': log_content + } + + # Print a success message + print(Fore.GREEN + ' ✔ ' + Style.RESET_ALL + pkg['name'] + ' updated successfully.') #----------------------------------------------------------------------------------------------- diff --git a/src/controllers/Package/Dnf.py b/src/controllers/Package/Dnf.py index b12e4b0..3e9e69a 100644 --- a/src/controllers/Package/Dnf.py +++ b/src/controllers/Package/Dnf.py @@ -277,8 +277,8 @@ def remove_all_exclusions(self): # Update packages # #----------------------------------------------------------------------------------------------- - def update(self, packagesList, update_method: str = 'one_by_one', exit_on_package_update_error: bool = True): - # Log file to store each package update output (when 'one_by_one' method is used) + def update(self, packagesList, exit_on_package_update_error: bool = True, dry_run: bool = False): + # Log file to store each package update output log = '/tmp/linupdate-update-package.log' # Package update summary @@ -295,143 +295,109 @@ def update(self, packagesList, update_method: str = 'one_by_one', exit_on_packag } } - # If update_method is 'one_by_one', update packages one by one (one command per package) - if update_method == 'one_by_one': - # Loop through the list of packages to update - for pkg in packagesList: - # If the package is excluded, ignore it - if pkg['excluded']: - continue - - # If log file exists, remove it - if Path(log).is_file(): - Path(log).unlink() - - with Log(log): - print('\n ▪ Updating ' + Fore.GREEN + pkg['name'] + Style.RESET_ALL + ' (' + pkg['current_version'] + ' → ' + pkg['available_version'] + '):') - - # Before updating, check if package is already in the latest version, if so, skip it - # It means that it has been updated previously by another package, probably because it was a dependency - # Get the current version of the package with dnf - # e.g. dnf repoquery --installed --qf="%{version}-%{release}.%{arch}" wget - result = subprocess.run( - ["dnf", "repoquery", "--installed", "--qf=%{version}-%{release}.%{arch}", pkg['name']], - stdout = subprocess.PIPE, # subprocess.PIPE & subprocess.PIPE are alias of 'capture_output = True' - stderr = subprocess.PIPE, - universal_newlines = True # Alias of 'text = True' - ) - - # Quit if an error occurred - if result.returncode != 0: - raise Exception('Could not retrieve current version of package ' + pkg['name'] + ': ' + result.stderr) - - # Retrieve current version - current_version = result.stdout.strip() - - # If current version is the same the target version, skip the update - if current_version == pkg['available_version']: - print(Fore.GREEN + ' ✔ ' + Style.RESET_ALL + pkg['name'] + ' is already up to date (updated with another package).') - - # Mark the package as already updated - self.summary['update']['success']['count'] += 1 - - # Also add the package to the list of successful packages - self.summary['update']['success']['packages'][pkg['name']] = { - 'version': pkg['available_version'], - 'log': 'Already up to date (updated with another package).' - } - - # Continue to the next package - continue - - # Define the command to update the package - cmd = ['dnf', 'update', pkg['name'] + '-' + pkg['available_version'], '-y'] - - popen = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, bufsize=1, universal_newlines=True) + # Loop through the list of packages to update + for pkg in packagesList: + # If the package is excluded, ignore it + if pkg['excluded']: + continue - # Print lines as they are read - for line in popen.stdout: - line = line.replace('\r', '') - print(' | ' + line, end='') + # If log file exists, remove it + if Path(log).is_file(): + Path(log).unlink() + + with Log(log): + print('\n ▪ Updating ' + Fore.GREEN + pkg['name'] + Style.RESET_ALL + ' (' + pkg['current_version'] + ' → ' + pkg['available_version'] + '):') + + # Before updating, check if package is already in the latest version, if so, skip it + # It means that it has been updated previously by another package, probably because it was a dependency + # Get the current version of the package with dnf + # e.g. dnf repoquery --installed --qf="%{version}-%{release}.%{arch}" wget + result = subprocess.run( + ["dnf", "repoquery", "--installed", "--qf=%{version}-%{release}.%{arch}", pkg['name']], + stdout = subprocess.PIPE, # subprocess.PIPE & subprocess.PIPE are alias of 'capture_output = True' + stderr = subprocess.PIPE, + universal_newlines = True # Alias of 'text = True' + ) + + # Quit if an error occurred + if result.returncode != 0: + raise Exception('Could not retrieve current version of package ' + pkg['name'] + ': ' + result.stderr) + + # Retrieve current version + current_version = result.stdout.strip() - # Wait for the command to finish - popen.wait() + # If current version is the same the target version, skip the update + if current_version == pkg['available_version']: + print(Fore.GREEN + ' ✔ ' + Style.RESET_ALL + pkg['name'] + ' is already up to date (updated with another package).') - # If command failed, either raise an exception or print a warning - if popen.returncode != 0: - # Add the package to the list of failed packages - self.summary['update']['failed']['count'] += 1 + # Mark the package as already updated + self.summary['update']['success']['count'] += 1 - # Also add the package to the list of failed packages + # Also add the package to the list of successful packages + self.summary['update']['success']['packages'][pkg['name']] = { + 'version': pkg['available_version'], + 'log': 'Already up to date (updated with another package).' + } - # First get log content - with open(log, 'r') as file: - log_content = file.read() + # Continue to the next package + continue - self.summary['update']['failed']['packages'][pkg['name']] = { - 'version': pkg['available_version'], - 'log': log_content - } + # Define the command to update the package + cmd = ['dnf', 'update', pkg['name'] + '-' + pkg['available_version'], '-y'] - # If error is critical, raise an exception to quit - if (exit_on_package_update_error == True): - raise Exception('Error while updating ' + pkg['name'] + '.') + # If dry_run is True, add the --setopt tsflags=test option to simulate the update + if dry_run: + cmd.append('--setopt') + cmd.append('tsflags=test') - # Else continue to the next package - else: - print(Fore.RED + ' ✕ ' + Style.RESET_ALL + 'Error while updating ' + pkg['name'] + '.') - continue + popen = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, bufsize=1, universal_newlines=True) - # Close the pipe - popen.stdout.close() + # Print lines as they are read + for line in popen.stdout: + line = line.replace('\r', '') + print(' | ' + line, end='') - # If command succeeded, increment the success counter - self.summary['update']['success']['count'] += 1 + # Wait for the command to finish + popen.wait() - # Also add the package to the list of successful packages + # Get log content + with open(log, 'r') as file: + log_content = Utils().clean_log(file.read()) - # First get log content - with open(log, 'r') as file: - log_content = file.read() + # If command failed, either raise an exception or print a warning + if popen.returncode != 0: + # Add the package to the list of failed packages + self.summary['update']['failed']['count'] += 1 - self.summary['update']['success']['packages'][pkg['name']] = { + # Add the package to the list of failed packages + self.summary['update']['failed']['packages'][pkg['name']] = { 'version': pkg['available_version'], 'log': log_content } - # Print a success message - print(Fore.GREEN + ' ✔ ' + Style.RESET_ALL + pkg['name'] + ' updated successfully.') + # If error is critical, raise an exception to quit + if (exit_on_package_update_error == True): + raise Exception('Error while updating ' + pkg['name'] + '.') - # If update_method is 'global', update all packages at once (one command) - if update_method == 'global': - # Define the command to update all packages - cmd = ['dnf', 'update', '-y'] + # Else continue to the next package + else: + print(Fore.RED + ' ✕ ' + Style.RESET_ALL + 'Error while updating ' + pkg['name'] + '.') + continue - popen = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, bufsize=1, universal_newlines=True) + # Close the pipe + popen.stdout.close() - # Print lines as they are read - for line in popen.stdout: - line = line.replace('\r', '') - print(' | ' + line, end='') + # If command succeeded, increment the success counter + self.summary['update']['success']['count'] += 1 - # Wait for the command to finish - popen.wait() + # Add the package to the list of successful packages + self.summary['update']['success']['packages'][pkg['name']] = { + 'version': pkg['available_version'], + 'log': log_content + } - # If command failed, either raise an exception or print a warning - if popen.returncode != 0: - # If error is critical, raise an exception to quit - if (exit_on_package_update_error == True): - raise Exception('Error while updating packages.') - - # Else print an error message - else: - print(Fore.RED + ' ✕ ' + Style.RESET_ALL + 'Error.') - else: # Print a success message - print(Fore.GREEN + ' ✔ ' + Style.RESET_ALL + 'Done.') - - # Close the pipe - popen.stdout.close() + print(Fore.GREEN + ' ✔ ' + Style.RESET_ALL + pkg['name'] + ' updated successfully.') #----------------------------------------------------------------------------------------------- diff --git a/src/controllers/Package/Package.py b/src/controllers/Package/Package.py index 888815e..c0dd519 100644 --- a/src/controllers/Package/Package.py +++ b/src/controllers/Package/Package.py @@ -140,13 +140,16 @@ def get_available_packages(self, dist_upgrade: bool = False): # This can be a list of specific packages or all packages # #----------------------------------------------------------------------------------------------- - def update(self, packages_list: list = [], assume_yes: bool = False, ignore_exclude: bool = False, check_updates: bool = False, dist_upgrade: bool = False, keep_oldconf: bool = True): + def update(self, packages_list: list = [], assume_yes: bool = False, ignore_exclude: bool = False, check_updates: bool = False, dist_upgrade: bool = False, keep_oldconf: bool = True, dry_run: bool = False): restart_file = '/tmp/linupdate.restart-needed' # Package update summary self.summary = { 'update': { 'status': 'running', + 'options': { + 'dry_run': dry_run, + }, 'success': { 'count': 0, 'packages': {} @@ -162,9 +165,6 @@ def update(self, packages_list: list = [], assume_yes: bool = False, ignore_excl # Retrieve configuration configuration = self.appConfigController.get_conf() - # Retrieve the update method - update_method = configuration['update']['method'] - # Retrieve the exit_on_package_update_error option exit_on_package_update_error = configuration['update']['exit_on_package_update_error'] @@ -219,7 +219,10 @@ def update(self, packages_list: list = [], assume_yes: bool = False, ignore_excl self.packagesToUpdateCount += 1 # Print the number of packages to update - print('\n ' + Fore.GREEN + str(self.packagesToUpdateCount) + Style.RESET_ALL + ' packages will be updated, ' + Fore.YELLOW + str(self.packagesExcludedCount) + Style.RESET_ALL + ' will be excluded \n') + if dry_run: + print('\n ' + Fore.YELLOW + '(dry run) ' + Fore.GREEN + str(self.packagesToUpdateCount) + Style.RESET_ALL + ' packages would be updated, ' + Fore.YELLOW + str(self.packagesExcludedCount) + Style.RESET_ALL + ' would be excluded \n') + else: + print('\n ' + Fore.GREEN + str(self.packagesToUpdateCount) + Style.RESET_ALL + ' packages will be updated, ' + Fore.YELLOW + str(self.packagesExcludedCount) + Style.RESET_ALL + ' will be excluded \n') # Convert the list of packages to a table table = [] @@ -256,7 +259,10 @@ def update(self, packages_list: list = [], assume_yes: bool = False, ignore_excl # Print again the number of packages if total count is > 50 to avoid the user to scroll up to see it if self.packagesToUpdateCount + self.packagesExcludedCount > 50: - print('\n ' + Fore.GREEN + str(self.packagesToUpdateCount) + Style.RESET_ALL + ' packages will be updated, ' + Fore.YELLOW + str(self.packagesExcludedCount) + Style.RESET_ALL + ' will be excluded \n') + if dry_run: + print('\n ' + Fore.YELLOW + '(dry run) ' + Fore.GREEN + str(self.packagesToUpdateCount) + Style.RESET_ALL + ' packages would be updated, ' + Fore.YELLOW + str(self.packagesExcludedCount) + Style.RESET_ALL + ' would be excluded \n') + else: + print('\n ' + Fore.GREEN + str(self.packagesToUpdateCount) + Style.RESET_ALL + ' packages will be updated, ' + Fore.YELLOW + str(self.packagesExcludedCount) + Style.RESET_ALL + ' will be excluded \n') # If --assume-yes param has not been specified, then ask for confirmation before installing the printed packages update list if not assume_yes: @@ -285,7 +291,7 @@ def update(self, packages_list: list = [], assume_yes: bool = False, ignore_excl # Execute the packages update self.myPackageManagerController.dist_upgrade = dist_upgrade self.myPackageManagerController.keep_oldconf = keep_oldconf - self.myPackageManagerController.update(self.packagesToUpdateList, update_method, exit_on_package_update_error) + self.myPackageManagerController.update(self.packagesToUpdateList, exit_on_package_update_error, dry_run) # Update the summary status self.summary['update']['status'] = 'done' @@ -298,30 +304,20 @@ def update(self, packages_list: list = [], assume_yes: bool = False, ignore_excl # Remove all exclusions self.remove_all_exclusions() - # If update method is 'one_by_one', it will be possible to print precise information about the number of packages updated and failed - if update_method == 'one_by_one': - # Update the summary with the number of packages updated and failed - self.summary['update']['success']['count'] = self.myPackageManagerController.summary['update']['success']['count'] - self.summary['update']['failed']['count'] = self.myPackageManagerController.summary['update']['failed']['count'] - - # Also retrieve the list of packages updated and failed, with their version and log - self.summary['update']['success']['packages'] = self.myPackageManagerController.summary['update']['success']['packages'] - self.summary['update']['failed']['packages'] = self.myPackageManagerController.summary['update']['failed']['packages'] + # Update the summary with the number of packages updated and failed + self.summary['update']['success']['count'] = self.myPackageManagerController.summary['update']['success']['count'] + self.summary['update']['failed']['count'] = self.myPackageManagerController.summary['update']['failed']['count'] - # Print the number of packages updated and failed - # If there was a failed package, print the number in red - if self.summary['update']['failed']['count'] > 0: - print('\n ' + Fore.GREEN + str(self.summary['update']['success']['count']) + Style.RESET_ALL + ' packages updated, ' + Fore.RED + str(self.summary['update']['failed']['count']) + Style.RESET_ALL + ' packages failed' + Style.RESET_ALL) - else: - print('\n ' + Fore.GREEN + str(self.summary['update']['success']['count']) + Style.RESET_ALL + ' packages updated, ' + str(self.summary['update']['failed']['count']) + ' packages failed' + Style.RESET_ALL) + # Also retrieve the list of packages updated and failed, with their version and log + self.summary['update']['success']['packages'] = self.myPackageManagerController.summary['update']['success']['packages'] + self.summary['update']['failed']['packages'] = self.myPackageManagerController.summary['update']['failed']['packages'] - # If update method is 'global', just print success or failure - if update_method == 'global': - # If there was a failed package, print the number in red - if self.summary['update']['status'] == 'done': - print('\n ' + Fore.GREEN + 'All packages updated' + Style.RESET_ALL) - else: - print('\n ' + Fore.RED + 'Some packages failed to update' + Style.RESET_ALL) + # Print the number of packages updated and failed + # If there was a failed package, print the number in red + if self.summary['update']['failed']['count'] > 0: + print('\n ' + Fore.GREEN + str(self.summary['update']['success']['count']) + Style.RESET_ALL + ' packages updated, ' + Fore.RED + str(self.summary['update']['failed']['count']) + Style.RESET_ALL + ' packages failed' + Style.RESET_ALL) + else: + print('\n ' + Fore.GREEN + str(self.summary['update']['success']['count']) + Style.RESET_ALL + ' packages updated, ' + str(self.summary['update']['failed']['count']) + ' packages failed' + Style.RESET_ALL) # If there was a failed package update and the package update error is critical (set to true), then raise an exception to exit if exit_on_package_update_error == True and self.summary['update']['failed']['count'] > 0: diff --git a/src/controllers/Service/Service.py b/src/controllers/Service/Service.py index 6ec01c5..1b1c924 100644 --- a/src/controllers/Service/Service.py +++ b/src/controllers/Service/Service.py @@ -15,7 +15,7 @@ class Service: # Restart services # #----------------------------------------------------------------------------------------------- - def restart(self, update_summary: list): + def restart(self, update_summary: list, dry_run: bool = False): # Retrieve services to restart services = Config().get_service_to_restart() @@ -52,6 +52,11 @@ def restart(self, update_summary: list): if not re.match(regex, package): continue + # If dry-run is enabled, just print the service that would be restarted + if dry_run: + print(' ▪ Would restart ' + Fore.YELLOW + service + Style.RESET_ALL) + continue + print(' ▪ Restarting ' + Fore.YELLOW + service + Style.RESET_ALL + ':', end=' ') # Check if service is active diff --git a/templates/update.template.yml b/templates/update.template.yml index 426abcd..d06f67b 100644 --- a/templates/update.template.yml +++ b/templates/update.template.yml @@ -1,6 +1,5 @@ update: exit_on_package_update_error: true - method: one_by_one packages: exclude: always: [] diff --git a/version b/version index 0fa4ae4..fbcbf73 100644 --- a/version +++ b/version @@ -1 +1 @@ -3.3.0 \ No newline at end of file +3.4.0 \ No newline at end of file