From 6e844d8d2ba4f90087f0f780ab8278b55dfeaad7 Mon Sep 17 00:00:00 2001 From: Ludovic <54670129+lbr38@users.noreply.github.com> Date: Mon, 23 Sep 2024 09:44:48 +0200 Subject: [PATCH] 3.3.0 --- .github/workflows/build-and-test-deb.yml | 3 ++ linupdate.py | 3 +- src/controllers/Args.py | 22 +++++++++ src/controllers/Module/Reposerver/Agent.py | 15 +++++- src/controllers/Package/Apt.py | 54 ++++++++++++++++++++-- src/controllers/Package/Dnf.py | 43 +++++++++++++++++ src/controllers/Package/Package.py | 38 +++++++++++++-- version | 2 +- 8 files changed, 169 insertions(+), 11 deletions(-) diff --git a/.github/workflows/build-and-test-deb.yml b/.github/workflows/build-and-test-deb.yml index 0230ff1..4a6613b 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" + - name: "Run test: list available modules" run: python3 /opt/linupdate/linupdate.py --mod-list 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/Args.py b/src/controllers/Args.py index a6ffe01..866ae55 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-list 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..5f2c19f 100644 --- a/src/controllers/Module/Reposerver/Agent.py +++ b/src/controllers/Module/Reposerver/Agent.py @@ -184,10 +184,23 @@ def websocket_on_message(self, ws, message): # 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' and 'packages' in message and len(message['packages']) > 0: + print('[reposerver-agent] Reposerver requested to update a list of packages') + + # Log everything to the log file + with Log(log): + self.packageController.update(message['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..5d121f7 100644 --- a/src/controllers/Package/Apt.py +++ b/src/controllers/Package/Apt.py @@ -38,6 +38,54 @@ def __init__(self): 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 @@ -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', + '--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..92aa100 100644 --- a/src/controllers/Package/Dnf.py +++ b/src/controllers/Package/Dnf.py @@ -26,6 +26,49 @@ def __init__(self): } } + #----------------------------------------------------------------------------------------------- + # + # 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() + #----------------------------------------------------------------------------------------------- # diff --git a/src/controllers/Package/Package.py b/src/controllers/Package/Package.py index 014cc8a..c15b35a 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,32 @@ 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-list 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 and available version have been found, then add the package to the final list + if package['current_version'] != '' or package['available_version'] != '': + 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 +227,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 +247,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/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