diff --git a/.github/workflows/build-and-test-deb.yml b/.github/workflows/build-and-test-deb.yml index 0230ff1..4b9a502 100644 --- a/.github/workflows/build-and-test-deb.yml +++ b/.github/workflows/build-and-test-deb.yml @@ -167,6 +167,9 @@ jobs: - name: "Run test: check updates" run: python3 /opt/linupdate/linupdate.py --check-updates + - name: "Run text: update specific packages" + run: python3 /opt/linupdate/linupdate.py --update curl,wget,apache2 --assume-yes + - name: "Run test: list available modules" run: python3 /opt/linupdate/linupdate.py --mod-list @@ -255,6 +258,9 @@ jobs: - name: "Run test: check updates" run: python3 /opt/linupdate/linupdate.py --check-updates + - name: "Run text: update specific packages" + run: python3 /opt/linupdate/linupdate.py --update curl,wget,apache2 --assume-yes + - name: "Run test: list available modules" run: python3 /opt/linupdate/linupdate.py --mod-list @@ -343,6 +349,9 @@ jobs: - name: "Run test: check updates" run: python3 /opt/linupdate/linupdate.py --check-updates + - name: "Run text: update specific packages" + run: python3 /opt/linupdate/linupdate.py --update curl,wget,apache2 --assume-yes + - name: "Run test: list available modules" run: python3 /opt/linupdate/linupdate.py --mod-list @@ -428,6 +437,9 @@ jobs: - name: "Run test: check updates" run: sudo python3 /opt/linupdate/linupdate.py --check-updates + - name: "Run text: update specific packages" + run: sudo python3 /opt/linupdate/linupdate.py --update curl,wget,apache2 --assume-yes + - name: "Run test: list available modules" run: sudo python3 /opt/linupdate/linupdate.py --mod-list @@ -535,6 +547,9 @@ jobs: - name: "Run test: check updates" run: python3 /opt/linupdate/linupdate.py --check-updates + - name: "Run text: update specific packages" + run: python3 /opt/linupdate/linupdate.py --update curl,wget,apache2 --assume-yes + - name: "Run test: print raw configuration" run: python3 /opt/linupdate/linupdate.py --show-config diff --git a/.github/workflows/build-and-test-rpm.yml b/.github/workflows/build-and-test-rpm.yml index ae9ba83..7c79784 100644 --- a/.github/workflows/build-and-test-rpm.yml +++ b/.github/workflows/build-and-test-rpm.yml @@ -152,8 +152,8 @@ jobs: - name: Install package run: dnf --nogpgcheck localinstall -y ./linupdate-test-build-${{ env.VERSION }}.noarch.rpm - - name: Launch linupdate - run: python3 /opt/linupdate/linupdate.py --check-updates + # - name: Launch linupdate + # run: python3 /opt/linupdate/linupdate.py --check-updates # Tests some params - name: "Run test: print help" @@ -184,13 +184,13 @@ jobs: run: python3 /opt/linupdate/linupdate.py --get-exclude - name: "Run test: set package exclusions on major update" - run: python3 /opt/linupdate/linupdate.py --exclude-major "apache2,mysql.*" + run: python3 /opt/linupdate/linupdate.py --exclude-major "httpd,mysql.*" - name: "Run test: get package exclusions on major update" run: python3 /opt/linupdate/linupdate.py --get-exclude-major - name: "Run test: set services to restart after update" - run: python3 /opt/linupdate/linupdate.py --service-restart "apache2,mysql" + run: python3 /opt/linupdate/linupdate.py --service-restart "httpd,mysql" - name: "Run test: get services to restart after update" run: python3 /opt/linupdate/linupdate.py --get-service-restart @@ -198,6 +198,9 @@ jobs: - name: "Run test: check updates" run: python3 /opt/linupdate/linupdate.py --check-updates + - name: "Run text: update specific packages" + run: python3 /opt/linupdate/linupdate.py --update curl,wget,httpd --assume-yes + - name: "Run test: list available modules" run: python3 /opt/linupdate/linupdate.py --mod-list @@ -247,8 +250,8 @@ jobs: - name: Install package run: dnf --nogpgcheck localinstall -y ./linupdate-test-build-${{ env.VERSION }}.noarch.rpm - - name: Launch linupdate - run: python3 /opt/linupdate/linupdate.py --check-updates + # - name: Launch linupdate + # run: python3 /opt/linupdate/linupdate.py --check-updates # Tests some params - name: "Run test: print help" @@ -279,13 +282,13 @@ jobs: run: python3 /opt/linupdate/linupdate.py --get-exclude - name: "Run test: set package exclusions on major update" - run: python3 /opt/linupdate/linupdate.py --exclude-major "apache2,mysql.*" + run: python3 /opt/linupdate/linupdate.py --exclude-major "httpd,mysql.*" - name: "Run test: get package exclusions on major update" run: python3 /opt/linupdate/linupdate.py --get-exclude-major - name: "Run test: set services to restart after update" - run: python3 /opt/linupdate/linupdate.py --service-restart "apache2,mysql" + run: python3 /opt/linupdate/linupdate.py --service-restart "httpd,mysql" - name: "Run test: get services to restart after update" run: python3 /opt/linupdate/linupdate.py --get-service-restart @@ -293,6 +296,9 @@ jobs: - name: "Run test: check updates" run: python3 /opt/linupdate/linupdate.py --check-updates + - name: "Run text: update specific packages" + run: python3 /opt/linupdate/linupdate.py --update curl,wget,httpd --assume-yes + - name: "Run test: list available modules" run: python3 /opt/linupdate/linupdate.py --mod-list diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5fcfd7f..b4aa92d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -185,7 +185,9 @@ jobs: body: | **Changes:** - - Reposerver module: sending more logs to the server when updating packages + - 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 draft: false prerelease: false diff --git a/linupdate.py b/linupdate.py index 32cfd25..aa2b1ad 100755 --- a/linupdate.py +++ b/linupdate.py @@ -99,7 +99,8 @@ def main(): my_module.pre() # Execute packages update - my_package.update(my_args.assume_yes, + my_package.update(my_args.packages_to_update, + my_args.assume_yes, my_args.ignore_exclude, my_args.check_updates, my_args.dist_upgrade, diff --git a/src/controllers/App/Service.py b/src/controllers/App/Service.py index 971957c..9fa7990 100644 --- a/src/controllers/App/Service.py +++ b/src/controllers/App/Service.py @@ -95,7 +95,7 @@ def main(self): # If the process has terminated with an error (exit 1), print a message if retcode != 0: print('[' + child['agent'] + '-agent] Terminated with return code ' + str(retcode) + ' :(') - print("[" + child['agent'] + "-agent] I'm dead for now but I will be resurrected soon, please wait or restart main service") + print("[" + child['agent'] + "-agent] I'm dead for now but I will be resurrected soon, please wait or restart linupdate service") # Remove child process from list self.child_processes.remove(child) diff --git a/src/controllers/Args.py b/src/controllers/Args.py index a6ffe01..b24b0fa 100644 --- a/src/controllers/Args.py +++ b/src/controllers/Args.py @@ -41,6 +41,7 @@ def parse(self): Args.assume_yes = False Args.check_updates = False Args.ignore_exclude = False + Args.packages_to_update = [] Args.dist_upgrade = False Args.keep_oldconf = True @@ -72,6 +73,8 @@ def parse(self): # Set mail recipient parser.add_argument("--set-mail-recipient", action="store", nargs='?', default="null") + # Packages to update list + parser.add_argument("--update", "-u", action="store", nargs='?', default="null") # Dist upgrade parser.add_argument("--dist-upgrade", "-du", action="store_true", default="null") # Keep oldconf @@ -298,8 +301,19 @@ def parse(self): except Exception as e: raise ArgsException('Could not set mail recipient(s): ' + str(e)) + # + # If --update param has been set + # + if args.update != "null": + try: + for package in args.update.split(','): + Args.packages_to_update.append({'name': package.strip()}) + except Exception as e: + raise ArgsException('Could not parse update list: ' + str(e)) + # # If --ignore-exclude param has been set + # if args.ignore_exclude != "null": Args.ignore_exclude = True @@ -629,6 +643,14 @@ def help(self): { 'title': 'Update options' }, + { + 'args': [ + '--update', + '-u' + ], + 'option': 'PACKAGE', + 'description': 'Update only the specified packages (separated by commas)' + }, { 'args': [ '--dist-upgrade', diff --git a/src/controllers/Module/Reposerver/Agent.py b/src/controllers/Module/Reposerver/Agent.py index a2380a5..56cb7bb 100644 --- a/src/controllers/Module/Reposerver/Agent.py +++ b/src/controllers/Module/Reposerver/Agent.py @@ -122,6 +122,23 @@ def run_inotify_package_event(self): self.inotify_exception = str(e) + #----------------------------------------------------------------------------------------------- + # + # Set request status + # + #----------------------------------------------------------------------------------------------- + def set_request_status(self, request_id, status): + json_data = { + 'response-to-request': { + 'request-id': request_id, + 'status': status + } + } + + # Send the response + self.websocket.send(json.dumps(json_data)) + + #----------------------------------------------------------------------------------------------- # # On message received from the websocket @@ -175,6 +192,11 @@ def websocket_on_message(self, ws, message): # 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() @@ -182,12 +204,33 @@ def websocket_on_message(self, ws, message): elif message['request'] == 'update-all-packages': print('[reposerver-agent] Reposerver requested all packages update') + # 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.packageController.update(True) + self.packageController.update([], True) # 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 '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') + + # 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.packageController.update(message['data']['packages'], True) + + # Send a summary to the reposerver, with the summary of the installation (number of packages installed or failed) + summary = self.packageController.summary + else: raise Exception('unknown request sent by reposerver: ' + message['request']) diff --git a/src/controllers/Package/Apt.py b/src/controllers/Package/Apt.py index 836f73c..04eedf7 100644 --- a/src/controllers/Package/Apt.py +++ b/src/controllers/Package/Apt.py @@ -19,25 +19,59 @@ def __init__(self): # Create an instance of the apt cache self.aptcache = apt.Cache() - # Total count of success and failed package updates - self.summary = { - 'update': { - 'success': { - 'count': 0, - 'packages': {} - }, - 'failed': { - 'count': 0, - 'packages': {} - } - } - } - # Define some default options self.dist_upgrade = False self.keep_oldconf = True + #----------------------------------------------------------------------------------------------- + # + # Return the current version of a package + # + #----------------------------------------------------------------------------------------------- + def get_current_version(self, package): + try: + # Open apt cache + self.aptcache.open(None) + + # Get the package from the cache + pkg = self.aptcache[package] + + # If the package is not installed, return an empty string + if not pkg.is_installed: + return '' + + # Return the installed version of the package + return pkg.installed.version + + except Exception as e: + raise Exception('could not get current version of package ' + package + ': ' + str(e)) + + + #----------------------------------------------------------------------------------------------- + # + # Return the available version of a package + # + #----------------------------------------------------------------------------------------------- + def get_available_version(self, package): + try: + # Open apt cache + self.aptcache.open(None) + + # Get the package from the cache + pkg = self.aptcache[package] + + # If the package is not installed, return an empty string + if not pkg.is_installed: + return '' + + # Return the available version of the package + return pkg.candidate.version + + except Exception as e: + raise Exception('could not get available version of package ' + package + ': ' + str(e)) + + #----------------------------------------------------------------------------------------------- # # Return list of installed apt packages, sorted by name @@ -208,6 +242,20 @@ def update(self, packagesList, update_method: str = 'one_by_one', exit_on_packag # Log file to store each package update output (when 'one_by_one' method is used) log = '/tmp/linupdate-update-package.log' + # Package update summary + self.summary = { + 'update': { + 'success': { + 'count': 0, + 'packages': {} + }, + 'failed': { + 'count': 0, + 'packages': {} + } + } + } + # 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 @@ -252,14 +300,14 @@ def update(self, packagesList, update_method: str = 'one_by_one', exit_on_packag # If --keep-oldconf is True, then keep the old configuration file if self.keep_oldconf: cmd = [ - 'apt-get', 'install', pkg['name'], '-y', + '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'], '-y'] + cmd = ['apt-get', 'install', pkg['name'] + '=' + pkg['available_version'], '-y'] popen = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, universal_newlines=True) diff --git a/src/controllers/Package/Dnf.py b/src/controllers/Package/Dnf.py index 0a285ce..b12e4b0 100644 --- a/src/controllers/Package/Dnf.py +++ b/src/controllers/Package/Dnf.py @@ -10,21 +10,53 @@ from pathlib import Path import configparser +# Import classes +from src.controllers.Log import Log +from src.controllers.App.Utils import Utils + class Dnf: - def __init__(self): - # Total count of success and failed package updates - self.summary = { - 'update': { - 'success': { - 'count': 0, - 'packages': {} - }, - 'failed': { - 'count': 0, - 'packages': {} - } - } - } + #----------------------------------------------------------------------------------------------- + # + # Return the current version of a package + # + #----------------------------------------------------------------------------------------------- + def get_current_version(self, package): + # Get the current version of the package + # e.g. dnf repoquery --installed --qf="%{version}-%{release}.%{arch}" wget + result = subprocess.run( + ["dnf", "repoquery", "--installed", "--qf=%{version}-%{release}.%{arch}", package], + 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 ' + package + ': ' + result.stderr) + + return result.stdout.strip() + + + #----------------------------------------------------------------------------------------------- + # + # Return the available version of a package + # + #----------------------------------------------------------------------------------------------- + def get_available_version(self, package): + # Get the available version of the package + # e.g. dnf repoquery --upgrades --latest-limit 1 --qf="%{version}-%{release}.%{arch}" wget + result = subprocess.run( + ["dnf", "repoquery", "--upgrades", "--latest-limit", "1", "--qf=%{version}-%{release}.%{arch}", package], + 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 available version of package ' + package + ': ' + result.stderr) + + return result.stdout.strip() #----------------------------------------------------------------------------------------------- @@ -249,6 +281,20 @@ def update(self, packagesList, update_method: str = 'one_by_one', exit_on_packag # Log file to store each package update output (when 'one_by_one' method is used) log = '/tmp/linupdate-update-package.log' + # Package update summary + self.summary = { + 'update': { + 'success': { + 'count': 0, + 'packages': {} + }, + 'failed': { + 'count': 0, + 'packages': {} + } + } + } + # 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 @@ -261,99 +307,100 @@ def update(self, packagesList, update_method: str = 'one_by_one', exit_on_packag if Path(log).is_file(): Path(log).unlink() - 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() + 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 - # 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).') + # Define the command to update the package + cmd = ['dnf', 'update', pkg['name'] + '-' + pkg['available_version'], '-y'] - # Mark the package as already updated - self.summary['update']['success']['count'] += 1 + popen = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, bufsize=1, universal_newlines=True) - # 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).' - } + # Print lines as they are read + for line in popen.stdout: + line = line.replace('\r', '') + print(' | ' + line, end='') - # Continue to the next package - continue + # Wait for the command to finish + popen.wait() + + # 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 + + # Also add the package to the list of failed packages - # Define the command to update the package - cmd = ['dnf', 'update', pkg['name'], '-y'] + # First get log content + with open(log, 'r') as file: + log_content = file.read() - popen = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, bufsize=1, universal_newlines=True) + self.summary['update']['failed']['packages'][pkg['name']] = { + 'version': pkg['available_version'], + 'log': log_content + } - # Print lines as they are read - for line in popen.stdout: - line = line.replace('\r', '') - print(' | ' + line, end='') + # If error is critical, raise an exception to quit + if (exit_on_package_update_error == True): + raise Exception('Error while updating ' + pkg['name'] + '.') - # Wait for the command to finish - popen.wait() + # Else continue to the next package + else: + print(Fore.RED + ' ✕ ' + Style.RESET_ALL + 'Error while updating ' + pkg['name'] + '.') + continue - # 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 + # Close the pipe + popen.stdout.close() - # Also add the package to the list of failed packages + # If command succeeded, increment the success counter + self.summary['update']['success']['count'] += 1 + + # Also add the package to the list of successful packages # First get log content with open(log, 'r') as file: log_content = file.read() - self.summary['update']['failed']['packages'][pkg['name']] = { + self.summary['update']['success']['packages'][pkg['name']] = { 'version': pkg['available_version'], 'log': log_content } - # If error is critical, raise an exception to quit - if (exit_on_package_update_error == True): - raise Exception('Error while updating ' + pkg['name'] + '.') - - # Else 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 - - # Also add the package to the list of successful packages - - # First get log content - with open(log, 'r') as file: - log_content = file.read() - - 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.') + # 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': diff --git a/src/controllers/Package/Package.py b/src/controllers/Package/Package.py index 014cc8a..888815e 100644 --- a/src/controllers/Package/Package.py +++ b/src/controllers/Package/Package.py @@ -137,9 +137,10 @@ def get_available_packages(self, dist_upgrade: bool = False): #----------------------------------------------------------------------------------------------- # # Update packages + # This can be a list of specific packages or all packages # #----------------------------------------------------------------------------------------------- - def update(self, 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): restart_file = '/tmp/linupdate.restart-needed' # Package update summary @@ -170,9 +171,39 @@ def update(self, assume_yes: bool = False, ignore_exclude: bool = False, check_u # Remove all exclusions before starting (could be some left from a previous run that failed) self.remove_all_exclusions() - # Retrieve available packages, - # passing the dist_upgrade parameter (which will, with apt, update the list of available packages including packages such as the kernel) - self.packagesToUpdateList = self.get_available_packages(dist_upgrade) + # If a list of packages to update has been provided, use it + if len(packages_list) > 0: + packages_list_temp = [] + + # For each package in the list, if no current version or available version is provided, retrieve it + # This is the case when the user uses the --update parameter + for package in packages_list: + if 'current_version' not in package or 'available_version' not in package: + package['current_version'] = self.myPackageManagerController.get_current_version(package['name']) + package['available_version'] = self.myPackageManagerController.get_available_version(package['name']) + + # If current version or available version have not been found, skip the package + if package['current_version'] == '' or package['available_version'] == '': + continue + + # If current version and available version are the same, skip the package + if package['current_version'] == package['available_version']: + continue + + # Add the package to the list + packages_list_temp.append({ + 'name': package['name'], + 'current_version': package['current_version'], + 'available_version': package['available_version'] + }) + + self.packagesToUpdateList = packages_list_temp + + # Otherwise, retrieve the list of all available packages + else: + # Retrieve available packages passing the dist_upgrade parameter + # (which will, with apt, update the list of available packages including packages such as the kernel) + self.packagesToUpdateList = self.get_available_packages(dist_upgrade) # Check for package exclusions self.exclude(ignore_exclude) @@ -203,7 +234,7 @@ def update(self, assume_yes: bool = False, ignore_exclude: bool = False, check_u # Print the table list of packages to update # Check prettytable for table with width control https://pypi.org/project/prettytable/ - print(tabulate(table, headers=["", "Package", "Current version", "Available version", "Install decision"], tablefmt="simple"), end='\n\n') + print(tabulate(table, headers=["", "Package", "Current version", "Available version", "Update decision"], tablefmt="simple"), end='\n\n') # If there are no packages to update if self.packagesToUpdateCount == 0: @@ -223,6 +254,10 @@ def update(self, assume_yes: bool = False, ignore_exclude: bool = False, check_u if self.packagesToUpdateCount == 0: return + # 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 --assume-yes param has not been specified, then ask for confirmation before installing the printed packages update list if not assume_yes: # Ask for confirmation diff --git a/src/controllers/Service/Service.py b/src/controllers/Service/Service.py index 9ef9190..6ec01c5 100644 --- a/src/controllers/Service/Service.py +++ b/src/controllers/Service/Service.py @@ -4,6 +4,7 @@ import subprocess import re from colorama import Fore, Style +from pathlib import Path # Import classes from src.controllers.App.Config import Config @@ -25,6 +26,11 @@ def restart(self, update_summary: list): if updated_packages == 0: return + # Quit if systemctl is not installed (e.g. in docker container of linupdate's CI) + if not Path('/usr/bin/systemctl').is_file(): + print('\n systemctl is not installed, skipping service restart') + return + print('\n Restarting services') # If no services to restart, skip diff --git a/version b/version index a4f52a5..0fa4ae4 100644 --- a/version +++ b/version @@ -1 +1 @@ -3.2.0 \ No newline at end of file +3.3.0 \ No newline at end of file