From b5429f4becbacda03427ea282a26d3f134670bc5 Mon Sep 17 00:00:00 2001 From: Davide Principi Date: Tue, 9 Jul 2024 16:57:11 +0200 Subject: [PATCH 1/7] Enforce app and core minimum version check Added support for the following image labels: - org.nethserver.min-core - org.nethserver.min-from --- .../usr/local/agent/pypkg/cluster/modules.py | 37 +++++++++++++++---- docs/modules/images.md | 10 +++++ 2 files changed, 40 insertions(+), 7 deletions(-) diff --git a/core/imageroot/usr/local/agent/pypkg/cluster/modules.py b/core/imageroot/usr/local/agent/pypkg/cluster/modules.py index 018e6c70a..d9929dc2c 100644 --- a/core/imageroot/usr/local/agent/pypkg/cluster/modules.py +++ b/core/imageroot/usr/local/agent/pypkg/cluster/modules.py @@ -327,11 +327,13 @@ def list_installed(rdb, skip_core_modules = False): return installed -def list_installed_core(rdb): - installed = {'ghcr.io/nethserver/core': []} +def _get_core_tag(): core_env = agent.read_envfile('/etc/nethserver/core.env') - (url, tag) = core_env['CORE_IMAGE'].split(":") - installed['ghcr.io/nethserver/core'].append({ 'id': 'core', 'version': tag, 'module': 'core'}) + _, tag = core_env['CORE_IMAGE'].rsplit(":", 1) + return tag + +def _list_installed_core(rdb): + installed = {'ghcr.io/nethserver/core': [{'id': 'core', 'version': _get_core_tag(), 'module': 'core'}]} # Search for installed modules for m in rdb.scan_iter('module/*/environment'): vars = rdb.hgetall(m) @@ -349,6 +351,12 @@ def list_updates(rdb, skip_core_modules=False, with_testing_update=False): updates = [] installed_modules = list_installed(rdb, skip_core_modules) available_modules = _get_available_modules(rdb) + try: + current_core = semver.parse_version_info(_get_core_tag()) + except: + # Set an arbitrary high number for comparision with a development + # image of core: + current_core = semver.Version(999, 999, 999) flat_instance_list = list(mi for module_instances in installed_modules.values() for mi in module_instances) for instance in flat_instance_list: @@ -364,13 +372,28 @@ def list_updates(rdb, skip_core_modules=False, with_testing_update=False): testing_update_candidate = None available_module = available_modules[instance['source']] repository_name = available_module['repository'] - for atag in list(aver['tag'] for aver in available_module['versions']): + for aver in available_module['versions']: try: - available_version = semver.parse_version_info(atag) + available_version = semver.parse_version_info(aver['tag']) except: continue # skip non-semver available tag if available_version <= current_version: continue # ignore tags that do not update the current one + try: + minimum_version = semver.parse_version_info(aver['labels']['org.nethserver.min-from']) + except: + # Arbitrary low version to satisfy any tag: + minimum_version = (0,0,0) + if current_version < minimum_version: + print(agent.SD_NOTICE + f"Ignoring update of {instance['id']} with {instance['source']}:{aver['tag']}: org.nethserver.min-from", minimum_version, file=sys.stderr) + continue # Skip versions incompatible with instance version. + try: + minimum_core = semver.parse_version_info(aver['labels']['org.nethserver.min-core']) + except: + minimum_core = (0,0,0) + if current_core < minimum_core: + print(agent.SD_NOTICE + f"Ignoring update of {instance['id']} with {instance['source']}:{aver['tag']}: org.nethserver.min-core:", minimum_core, file=sys.stderr) + continue # Skip versions incompatible with core. if update_candidate is None and ( _repo_has_testing_flag(rdb, repository_name) or not available_version.prerelease @@ -411,7 +434,7 @@ def _calc_update(image_name, cur): pass return "" - for module_source, instances in list_installed_core(rdb).items(): + for module_source, instances in _list_installed_core(rdb).items(): _, image_name = module_source.rsplit("/", 1) core_modules.setdefault(image_name, {"name": image_name, "instances": []}) for instance in instances: diff --git a/docs/modules/images.md b/docs/modules/images.md index 01a0a7fb3..389a6cbd8 100644 --- a/docs/modules/images.md +++ b/docs/modules/images.md @@ -56,6 +56,16 @@ Module images can use a list of well-known labels to configure the system: - `rootless`: if present, the module is rootless (calculated from `org.nethserver.rootfull` label) - `rootfull`: if present, the module is rootfull (calculated from `org.nethserver.rootfull` label) - `org.nethserver.max-per-node`: maximum number of module instances installed on the same node +- `org.nethserver.min-from`: the image can be used to install a new + application instance, or to update an existing instance provided it has + a version greater than or equal to the label value. E.g. if the image + label `org.nethserver.min-from` has value `2.0.0`, an existing instance + with version `1.3.0` cannot be updated with it. +- `org.nethserver.min-core`: the image can be used to install a new + application instance, or update an existing one, if the core version is + greater than or equal to the label value. E.g. if the image label + `org.nethserver.min-core` has value `2.7.0` it cannot be installed if + the leader node running core has version `2.6.2`. Labels are set by `build-images.sh`, when the images are built. From 4a8530af87face90049104a584dbe585b3252a5d Mon Sep 17 00:00:00 2001 From: Davide Principi Date: Wed, 10 Jul 2024 13:09:56 +0200 Subject: [PATCH 2/7] update-core. Use list_modules to enforce min-core Get the list of available core module updates from list_modules() to enforce org.nethserver.min-core label requirement. --- .../actions/update-core/70update_modules | 23 ++++--------------- 1 file changed, 5 insertions(+), 18 deletions(-) diff --git a/core/imageroot/var/lib/nethserver/cluster/actions/update-core/70update_modules b/core/imageroot/var/lib/nethserver/cluster/actions/update-core/70update_modules index 35a1071c7..7681f333a 100755 --- a/core/imageroot/var/lib/nethserver/cluster/actions/update-core/70update_modules +++ b/core/imageroot/var/lib/nethserver/cluster/actions/update-core/70update_modules @@ -20,29 +20,16 @@ agent.run_helper("run-scriptdir", f"{agent_install_dir}/update-core-pre-modules. rdb = agent.redis_connect(privileged=True) # Update all core modules -instances = dict() -for oimage in cluster.modules.list_core_modules(rdb): - image_id = oimage['name'] - if image_id == 'core': - continue # skip core: it is handled by another action step - - # Prepare a list of module instances that need the update - module_instances = [oinstance['id'] for oinstance in oimage['instances'] if oinstance['update']] - - if len(module_instances) > 0: # must be not empty - instances[image_id] = { - 'url': cluster.modules.get_latest_module(image_id, rdb), - 'instances': module_instances, - } - update_module_tasks = [] -for mid in instances.keys(): +for uinstance in cluster.modules.list_updates(rdb, skip_core_modules=False): + if not 'core_module' in uinstance.get('flags', []): + continue # skip module without core_module flag update_module_tasks.append({ 'agent_id': 'cluster', "action": "update-module", "data": { - "module_url": instances[mid]['url'], - "instances": instances[mid]['instances'], + "module_url": uinstance['source'] + ':' + uinstance['update'], + "instances": [uinstance['id']], "force": force_update, } }) From 035628410fddc0771737ce14511cd70f7228fd99 Mon Sep 17 00:00:00 2001 From: Davide Principi Date: Wed, 10 Jul 2024 17:14:06 +0200 Subject: [PATCH 3/7] update-module. Mandatory module_url parameter The image_url must be a good update for all instances passed as parameter. As it is difficult to check if it is appropriate or not, or to calculate an image_url that fits all instances (if not given), we drop the automatic latest version lookup and we expect a good one is passed. --- .../cluster/actions/update-module/50update | 15 +-------------- .../actions/update-module/validate-input.json | 11 +++-------- 2 files changed, 4 insertions(+), 22 deletions(-) diff --git a/core/imageroot/var/lib/nethserver/cluster/actions/update-module/50update b/core/imageroot/var/lib/nethserver/cluster/actions/update-module/50update index 505f4b6c6..798c46a1f 100755 --- a/core/imageroot/var/lib/nethserver/cluster/actions/update-module/50update +++ b/core/imageroot/var/lib/nethserver/cluster/actions/update-module/50update @@ -29,26 +29,13 @@ import subprocess import cluster.grants request = json.load(sys.stdin) -image_url= request.get('module_url') +image_url= request['module_url'] instances = request['instances'] image_id = '' force = "force" in request and request["force"] is True rdb = agent.redis_connect(privileged=True) -# Explicit image_url always wins -if not image_url: - # resolve image_id from first instance - image_id = agent.get_image_name_from_url(rdb.hget(f'module/{instances[0]}/environment', 'IMAGE_URL')) - - override = rdb.hget('cluster/override/modules', image_id) - # use override from redis - if override: - image_url = override - else: - # search for the latest package inside the repository metadata - image_url = cluster.modules.get_latest_module(image_id, rdb) - # Modules sanity check: send a "list-actions" ping task and wait the result. # If any module fails abort the whole action. ping_errors = agent.tasks.runp_brief([{"agent_id": f"module/{mid}", "action": "list-actions"} for mid in instances], diff --git a/core/imageroot/var/lib/nethserver/cluster/actions/update-module/validate-input.json b/core/imageroot/var/lib/nethserver/cluster/actions/update-module/validate-input.json index b552ffd78..f4288a87c 100644 --- a/core/imageroot/var/lib/nethserver/cluster/actions/update-module/validate-input.json +++ b/core/imageroot/var/lib/nethserver/cluster/actions/update-module/validate-input.json @@ -10,25 +10,20 @@ "mymodule2", "mymodule3" ] - }, - { - "instances": [ - "mymodule2", - "mymodule3" - ] } ], "type": "object", "required": [ + "module_url", "instances" ], "properties": { "module_url": { - "description": "Module image URL to download and install. If empty, search for the latest available image from repositories.", + "description": "Module image URL to download and use as update.", "type": "string" }, "instances": { - "description": "Instance identifiers where the selected image is installed", + "description": "Instance identifiers where the selected image is installed as update.", "type": "array", "items": { "type": "string", From fcc9bb645760fe9fbaad3cbad350f87c78b8dfed Mon Sep 17 00:00:00 2001 From: Davide Principi Date: Wed, 10 Jul 2024 17:47:04 +0200 Subject: [PATCH 4/7] update-modules. Add filter parameters - ui. Update a list of instances with update-modules. - api. add filter parameters to update-modules, to restrict the update to certain instances and module types. This action can now be used to replace update-module if an automatic version selection is wanted. --- .../cluster/actions/update-modules/50update | 12 +++++- .../update-modules/validate-input.json | 38 +++++++++++++++++++ .../src/views/SoftwareCenterAppInstances.vue | 2 +- 3 files changed, 50 insertions(+), 2 deletions(-) create mode 100644 core/imageroot/var/lib/nethserver/cluster/actions/update-modules/validate-input.json diff --git a/core/imageroot/var/lib/nethserver/cluster/actions/update-modules/50update b/core/imageroot/var/lib/nethserver/cluster/actions/update-modules/50update index c7c9699cd..83c78eed8 100755 --- a/core/imageroot/var/lib/nethserver/cluster/actions/update-modules/50update +++ b/core/imageroot/var/lib/nethserver/cluster/actions/update-modules/50update @@ -25,12 +25,22 @@ import sys import os import agent.tasks import cluster.modules +import json +request = json.load(sys.stdin) rdb = agent.redis_connect(privileged=True) updates = cluster.modules.list_updates(rdb, skip_core_modules = True) +if 'modules' in request: + # Filter out non-matching modules: + updates = list(filter(lambda uo: uo['module'] in request['modules'], updates)) + +if 'instances' in request: + # Filter out non-matching instances: + updates = list(filter(lambda uo: uo['id'] in request['instances'], updates)) + if len(updates) == 0: - print(agent.SD_INFO, "No updates available for the installed modules", file=sys.stderr) + print(agent.SD_INFO, "update-modules: no update found. Filters:", request, file=sys.stderr) sys.exit(0) errors = 0 diff --git a/core/imageroot/var/lib/nethserver/cluster/actions/update-modules/validate-input.json b/core/imageroot/var/lib/nethserver/cluster/actions/update-modules/validate-input.json new file mode 100644 index 000000000..232c088e8 --- /dev/null +++ b/core/imageroot/var/lib/nethserver/cluster/actions/update-modules/validate-input.json @@ -0,0 +1,38 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "update-modules input", + "$id": "http://schema.nethserver.org/cluster/update-modules-input.json", + "description": "Input schema of the update-modules action. All matching modules are updated to their own valid latest version. The limits min-core and min-from are considered. If no option is given, all cluster app instances are updated.", + "examples": [ + { + "instances": [ + "mymodule2", + "mymodule3" + ] + }, + { + "modules": [ + "mymodule" + ] + } + ], + "type": "object", + "properties": { + "instances": { + "description": "Limit the update to this list of instances. Only matching instances are updated.", + "type": "array", + "items": { + "type": "string", + "minLength": 2, + "pattern": "^.+[0-9]+$" + } + }, + "modules": { + "description": "Limit the update to this list of modules. Only instances of this module are updated.", + "type": "array", + "items": { + "type": "string" + } + } + } +} diff --git a/core/ui/src/views/SoftwareCenterAppInstances.vue b/core/ui/src/views/SoftwareCenterAppInstances.vue index 22d6776e0..d95136f14 100644 --- a/core/ui/src/views/SoftwareCenterAppInstances.vue +++ b/core/ui/src/views/SoftwareCenterAppInstances.vue @@ -795,7 +795,7 @@ export default { async updateAllInstances() { this.error.updateModule = ""; this.setUpdateInProgressInStore(true); - const taskAction = "update-module"; + const taskAction = "update-modules"; const eventId = this.getUuid(); // register to task error From 155401fb96cb14fe83c5bc6be71408e2cac1e189 Mon Sep 17 00:00:00 2001 From: Davide Principi Date: Wed, 10 Jul 2024 18:05:29 +0200 Subject: [PATCH 5/7] get_latest_module() refactored with list_available The logic that defines the "latest" tag of a given module is implemented inside the list_available() function. A new assumption simplifies the function code. --- .../usr/local/agent/pypkg/cluster/modules.py | 27 +++---------------- 1 file changed, 4 insertions(+), 23 deletions(-) diff --git a/core/imageroot/usr/local/agent/pypkg/cluster/modules.py b/core/imageroot/usr/local/agent/pypkg/cluster/modules.py index d9929dc2c..709098c15 100644 --- a/core/imageroot/usr/local/agent/pypkg/cluster/modules.py +++ b/core/imageroot/usr/local/agent/pypkg/cluster/modules.py @@ -146,31 +146,12 @@ class LatestModuleLookupError(Exception): def get_latest_module(module, rdb): """Find most recent version of the given module """ - version = "" - source = "" - available = list_available(rdb) - for m in available: + for m in list_available(rdb, skip_core_modules=False): if m["id"] == module: - source = m["source"] - repo_testing = rdb.hget(f'cluster/repository/{m["repository"]}', 'testing') == "1" - for v in m["versions"]: - if repo_testing: - version = v["tag"] - elif not v["testing"]: - version = v["tag"] + # We assume at index 0 we find the latest tag: + return m["source"] + ':' + m['versions'][0]['tag'] - if version: - break - - if version: - break - - - # Fail if package has not been found inside the repository metadata - if not source or not version: - raise LatestModuleLookupError(module) - - return f'{source}:{version}' + raise LatestModuleLookupError(module) def _parse_version_object(v): try: From e4ca806a741909d5e431c6288954b2aad8177929 Mon Sep 17 00:00:00 2001 From: Davide Principi Date: Thu, 11 Jul 2024 16:55:25 +0200 Subject: [PATCH 6/7] list_core_modules() return all core instances - calculate core versions update for every cluster node, not only leader - fix install-core.sh to initialize IMAGE_URL and related variables in node environment --- .../usr/local/agent/pypkg/cluster/modules.py | 117 ++++++++++-------- .../var/lib/nethserver/node/install-core.sh | 4 + 2 files changed, 71 insertions(+), 50 deletions(-) diff --git a/core/imageroot/usr/local/agent/pypkg/cluster/modules.py b/core/imageroot/usr/local/agent/pypkg/cluster/modules.py index 709098c15..04872e567 100644 --- a/core/imageroot/usr/local/agent/pypkg/cluster/modules.py +++ b/core/imageroot/usr/local/agent/pypkg/cluster/modules.py @@ -286,26 +286,37 @@ def _get_available_modules(rdb): def list_installed(rdb, skip_core_modules = False): installed = {} logos = _get_downloaded_logos() - # Search for installed modules - for m in rdb.scan_iter('module/*/environment'): - vars = rdb.hgetall(m) - module_ui_name = rdb.get(m.removesuffix('/environment') + '/ui_name') or "" - url, sep, tag = vars['IMAGE_URL'].partition(":") - image = url[url.rindex('/')+1:] - logo = logos.get(vars["MODULE_ID"]) or '' - flags = list(rdb.smembers(f'module/{vars["MODULE_ID"]}/flags')) or [] - if skip_core_modules and 'core_module' in flags: + hmodules = rdb.hgetall("cluster/module_node") or {} + hnode_names = {} + for node_id in set(hmodules.values()): + hnode_names[node_id] = rdb.get(f'node/{node_id}/ui_name') or "" + for module_id in hmodules.keys(): + try: + mflags = list(rdb.smembers(f'module/{module_id}/flags')) + if skip_core_modules and 'core_module' in mflags: + continue # ignore core modules as requested + menv = rdb.hgetall(f'module/{module_id}/environment') or {} + msource, mtag = menv['IMAGE_URL'].rsplit(":", 1) + mnode_id = hmodules[module_id] + hinstance = { + 'id': module_id, + 'source': msource, + 'version': mtag, + 'module': msource.rsplit("/", 1)[1], + 'ui_name': rdb.get(f'module/{module_id}/ui_name') or "", + 'node': mnode_id, + 'node_ui_name': hnode_names[mnode_id], + 'logo': logos.get(module_id, ""), + 'digest': menv['IMAGE_DIGEST'], + 'flags': mflags, + } + installed.setdefault(msource, []) + installed[msource].append(hinstance) + except Exception as ex: + print(agent.SD_ERR+f"Cannot fetch {module_id} attributes:", ex, file=sys.stderr) continue - if url not in installed.keys(): - installed[url] = [] - # Retrieve node ui_name - node_id = vars['NODE_ID'] - node_ui_name = rdb.get(f"node/{node_id}/ui_name") or "" - installed[url].append({ 'id': vars["MODULE_ID"], 'ui_name': module_ui_name, 'node': node_id, 'node_ui_name': node_ui_name, 'digest': vars["IMAGE_DIGEST"], 'source': url, 'version': tag, 'logo': logo, 'module': image, 'flags': flags}) - for instances in installed.values(): instances.sort(key=lambda v: _parse_version_object(v["version"]), reverse=True) - return installed def _get_core_tag(): @@ -313,21 +324,6 @@ def _get_core_tag(): _, tag = core_env['CORE_IMAGE'].rsplit(":", 1) return tag -def _list_installed_core(rdb): - installed = {'ghcr.io/nethserver/core': [{'id': 'core', 'version': _get_core_tag(), 'module': 'core'}]} - # Search for installed modules - for m in rdb.scan_iter('module/*/environment'): - vars = rdb.hgetall(m) - if 'core_module' in rdb.smembers(f'module/{vars["MODULE_ID"]}/flags'): - url, sep, tag = vars['IMAGE_URL'].partition(":") - image = url[url.rindex('/')+1:] - - if url not in installed.keys(): - installed[url] = [] - installed[url].append({'id': vars["MODULE_ID"], 'version': tag, 'module': image}) - - return installed - def list_updates(rdb, skip_core_modules=False, with_testing_update=False): updates = [] installed_modules = list_installed(rdb, skip_core_modules) @@ -396,33 +392,54 @@ def list_updates(rdb, skip_core_modules=False, with_testing_update=False): def list_core_modules(rdb): """List core modules and if they can be updated.""" + updates = list_updates(rdb, skip_core_modules=False) + def _get_module_update(module_id): + for oupdate in updates: + if oupdate['id'] == module_id: + return oupdate['update'] + return "" core_modules = {} - available = list_available(rdb, skip_core_modules = False) - - def _calc_update(image_name, cur): - # Lookup module information from repositories - for module in available: - if module["id"] == image_name: - break + _, latest_core = get_latest_module('core', rdb).rsplit(":", 1) + def _calc_core_update(current, latest): + try: + vcur = semver.parse_version_info(current) + except: + vcur = semver.Version(999) + try: + vlatest = semver.parse_version_info(latest) + except: + vlatest = semver.Version(0) + if vlatest > vcur: + return latest else: return "" + for node_id, ntag in _get_node_core_versions(rdb).items(): try: - vupdate = module["versions"][0]['tag'] - vinfo = semver.VersionInfo.parse(vupdate) - if vupdate > cur: - return vupdate - except: - pass - return "" - - for module_source, instances in _list_installed_core(rdb).items(): + nenv = rdb.hgetall(f'node/{node_id}/environment') or {} + _, ntag = nenv['IMAGE_URL'].rsplit(":", 1) + core_instance = { + 'id': 'core' + node_id, + 'version': ntag, + 'update': _calc_core_update(ntag, latest_core), + } + except Exception as ex: + print(agent.SD_ERR+f"Cannot fetch node {node_id} attributes:", ex, file=sys.stderr) + continue + core_modules.setdefault('core', {"name": 'core', "instances": []}) + core_modules['core']['instances'].append(core_instance) + for module_source, instances in list_installed(rdb, skip_core_modules=False).items(): _, image_name = module_source.rsplit("/", 1) + try: + has_core_module = 'core_module' in instances[0]['flags'] + except: + has_core_module = False + if not has_core_module: + continue core_modules.setdefault(image_name, {"name": image_name, "instances": []}) for instance in instances: core_modules[image_name]['instances'].append({ "id": instance["id"], "version": instance["version"], - "update": _calc_update(image_name, instance["version"]), + "update": _get_module_update(instance['id']), }) - return list(core_modules.values()) diff --git a/core/imageroot/var/lib/nethserver/node/install-core.sh b/core/imageroot/var/lib/nethserver/node/install-core.sh index 806d092e8..c298c1c85 100755 --- a/core/imageroot/var/lib/nethserver/node/install-core.sh +++ b/core/imageroot/var/lib/nethserver/node/install-core.sh @@ -68,10 +68,14 @@ echo "Write initial cluster environment state" echo "Write initial node environment state" (exec > /var/lib/nethserver/node/state/environment printf "NODE_ID=1\n" + printf "IMAGE_URL=%s\n" "${CORE_IMAGE}" printf "IMAGE_ID=%s\n" $(podman image inspect -f '{{.Id}}' "${CORE_IMAGE}") printf "IMAGE_DIGEST=%s\n" $(podman image inspect -f '{{.Digest}}' "${CORE_IMAGE}") + printf "IMAGE_REPODIGEST=%s\n" $(podman image inspect -f '{{index .RepoDigests 0}}' "${CORE_IMAGE}") + printf "PREV_IMAGE_URL=%s\n" "${CORE_IMAGE}" printf "PREV_IMAGE_ID=%s\n" $(podman image inspect -f '{{.Id}}' "${CORE_IMAGE}") printf "PREV_IMAGE_DIGEST=%s\n" $(podman image inspect -f '{{.Digest}}' "${CORE_IMAGE}") + printf "PREV_IMAGE_REPODIGEST=%s\n" $(podman image inspect -f '{{index .RepoDigests 0}}' "${CORE_IMAGE}") ) if [[ -z "${NS8_TWO_STEPS_INSTALL}" ]]; then From 617fd8dd0aa14a09a37ce23f82514c9c1e2d5294 Mon Sep 17 00:00:00 2001 From: Davide Principi Date: Thu, 11 Jul 2024 18:50:04 +0200 Subject: [PATCH 7/7] min-core. Use node core version to compare Compare the app requirement min-core with the core version installed on the app's node. Previously the leader version was used. --- .../usr/local/agent/pypkg/cluster/modules.py | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/core/imageroot/usr/local/agent/pypkg/cluster/modules.py b/core/imageroot/usr/local/agent/pypkg/cluster/modules.py index 04872e567..33606ef3a 100644 --- a/core/imageroot/usr/local/agent/pypkg/cluster/modules.py +++ b/core/imageroot/usr/local/agent/pypkg/cluster/modules.py @@ -329,14 +329,17 @@ def list_updates(rdb, skip_core_modules=False, with_testing_update=False): installed_modules = list_installed(rdb, skip_core_modules) available_modules = _get_available_modules(rdb) try: - current_core = semver.parse_version_info(_get_core_tag()) + leader_core_version = semver.parse_version_info(_get_core_tag()) except: - # Set an arbitrary high number for comparision with a development - # image of core: - current_core = semver.Version(999, 999, 999) + leader_core_version = semver.Version(999) + node_core_versions = _get_node_core_versions(rdb) flat_instance_list = list(mi for module_instances in installed_modules.values() for mi in module_instances) for instance in flat_instance_list: + try: + current_core = semver.parse_version_info(node_core_versions.get(instance["node"])) + except: + current_core = leader_core_version if not instance['source'] in available_modules: continue # skip instance if is not available from any repository try: @@ -390,6 +393,13 @@ def list_updates(rdb, skip_core_modules=False, with_testing_update=False): return updates +def _get_node_core_versions(rdb): + hversions = {} + for node_id in set(rdb.hvals("cluster/module_node")): + _, vtag = rdb.hget(f'node/{node_id}/environment', 'IMAGE_URL').rsplit(":", 1) + hversions[node_id] = vtag + return hversions + def list_core_modules(rdb): """List core modules and if they can be updated.""" updates = list_updates(rdb, skip_core_modules=False) @@ -415,8 +425,6 @@ def _calc_core_update(current, latest): return "" for node_id, ntag in _get_node_core_versions(rdb).items(): try: - nenv = rdb.hgetall(f'node/{node_id}/environment') or {} - _, ntag = nenv['IMAGE_URL'].rsplit(":", 1) core_instance = { 'id': 'core' + node_id, 'version': ntag,