update/software/software/release_data.py

353 lines
10 KiB
Python

#
# SPDX-License-Identifier: Apache-2.0
#
# Copyright (c) 2024 Wind River Systems, Inc.
#
import os
from packaging import version
import shutil
import threading
from software import states
from software.exceptions import FileSystemError
from software.exceptions import ReleaseNotFound
from software.software_functions import LOG
from software import utils
from software.software_functions import ReleaseData
class SWRelease(object):
'''wrapper class to group matching metadata and contents'''
def __init__(self, rel_id, metadata, contents):
self._id = rel_id
self._metadata = metadata
self._contents = contents
self._sw_version = None
self._release = None
@property
def metadata(self):
return self._metadata
@property
def contents(self):
return self._contents
@property
def id(self):
return self._id
@property
def state(self):
return self.metadata['state']
@staticmethod
def _ensure_state_transition(to_state):
to_dir = states.RELEASE_STATE_TO_DIR_MAP[to_state]
if not os.path.isdir(to_dir):
try:
os.makedirs(to_dir, mode=0o755, exist_ok=True)
except FileExistsError:
error = "Cannot create directory %s" % to_dir
raise FileSystemError(error)
def update_state(self, state):
LOG.info("%s state from %s to %s" % (self.id, self.state, state))
SWRelease._ensure_state_transition(state)
to_dir = states.RELEASE_STATE_TO_DIR_MAP[state]
from_dir = states.RELEASE_STATE_TO_DIR_MAP[self.state]
try:
shutil.move("%s/%s-metadata.xml" % (from_dir, self.id),
"%s/%s-metadata.xml" % (to_dir, self.id))
except shutil.Error:
msg = "Failed to move the metadata for %s" % self.id
LOG.exception(msg)
raise FileSystemError(msg)
self.metadata['state'] = state
@property
def version_obj(self):
'''returns packaging.version object'''
if self._release is None:
self._release = version.parse(self.sw_release)
return self._release
@property
def sw_release(self):
'''3 sections MM.mm.pp release version'''
return self.metadata['sw_version']
@property
def sw_version(self):
'''2 sections MM.mm software version'''
if self._sw_version is None:
self._sw_version = utils.get_major_release_version(self.sw_release)
return self._sw_version
@property
def component(self):
return self._get_by_key('component')
def _get_latest_commit(self):
if 'number_of_commits' not in self.contents:
return None
num_commits = self.contents['number_of_commits']
if int(num_commits) > 0:
commit_tag = "commit%s" % num_commits
return self.contents[commit_tag]
else:
# may consider raise InvalidRelease exception in this case after
# iso metadata comes with commit id
LOG.warning("Commit data not found in metadata. Release %s" %
self.id)
return None
@property
def commit_id(self):
commit = self._get_latest_commit()
if commit is not None:
return commit['commit']
else:
# may consider raise InvalidRelease exception when iso comes with
# latest commit
return None
@property
def base_commit_id(self):
commit = None
base = self.contents.get('base')
if base:
commit = base.get('commit')
return commit
def _get_by_key(self, key, default=None):
if key in self._metadata:
return self._metadata[key]
else:
return default
@property
def summary(self):
return self._get_by_key('summary')
@property
def description(self):
return self._get_by_key('description')
@property
def install_instructions(self):
return self._get_by_key('install_instructions')
@property
def warnings(self):
return self._get_by_key('warnings')
@property
def status(self):
return self._get_by_key('status')
@property
def unremovable(self):
return self._get_by_key('unremovable') == "Y"
@property
def reboot_required(self):
return self._get_by_key('reboot_required') == "Y"
@property
def requires_release_ids(self):
return self._get_by_key('requires') or []
@property
def packages(self):
return self._get_by_key('packages')
@property
def restart_script(self):
return self._get_by_key('restart_script')
@property
def apply_active_release_only(self):
return self._get_by_key('apply_active_release_only')
@property
def commit_checksum(self):
commit = self._get_latest_commit()
if commit is not None:
return commit['checksum']
else:
# may consider raise InvalidRelease exception when iso comes with
# latest commit
return None
def get_all_dependencies(self, filter_states=None):
"""
:return: sorted list of all direct and indirect required releases
raise ReleaseNotFound if one of the release is not uploaded.
"""
def _get_all_deps(release_id, release_collection, deps):
release = release_collection[release_id]
if release is None:
raise ReleaseNotFound([release_id])
if filter_states and release.state not in filter_states:
return
for id in release.requires_release_ids:
if id not in deps:
deps.append(id)
_get_all_deps(id, release_collection, deps)
all_deps = []
release_collection = get_SWReleaseCollection()
_get_all_deps(self.id, release_collection, all_deps)
releases = sorted([release_collection[id] for id in all_deps])
return releases
def __lt__(self, other):
return self.version_obj < other.version_obj
def __le__(self, other):
return self.version_obj <= other.version_obj
def __eq__(self, other):
return self.version_obj == other.version_obj
def __ge__(self, other):
return self.version_obj >= other.version_obj
def __gt__(self, other):
return self.version_obj > other.version_obj
def __ne__(self, other):
return self.version_obj != other.version_obj
@property
def is_ga_release(self):
ver = version.parse(self.sw_release)
if len(ver.release) == 2:
pp = 0
else:
_, _, pp = ver.release
return pp == 0
@property
def is_deletable(self):
return self.state in states.DELETABLE_STATE
def to_query_dict(self):
data = {"release_id": self.id,
"state": self.state,
"sw_version": self.sw_release,
"component": self.component,
"status": self.status,
"unremovable": self.unremovable,
"summary": self.summary,
"description": self.description,
"install_instructions": self.install_instructions,
"warnings": self.warnings,
"reboot_required": self.reboot_required,
"requires": self.requires_release_ids[:],
"packages": self.packages[:]}
return data
class SWReleaseCollection(object):
'''SWReleaseCollection encapsulates aggregated software release collection
managed by USM.
'''
def __init__(self, release_data):
self._sw_releases = {}
for rel_id in release_data.metadata:
rel_data = release_data.metadata[rel_id]
contents = release_data.contents[rel_id]
sw_release = SWRelease(rel_id, rel_data, contents)
self._sw_releases[rel_id] = sw_release
@property
def running_release(self):
latest = None
for rel in self.iterate_releases_by_state(states.DEPLOYED):
if latest is None or rel.version_obj > latest.version_obj:
latest = rel
return latest
def get_release_by_id(self, rel_id):
if rel_id in self._sw_releases:
return self._sw_releases[rel_id]
return None
def __getitem__(self, rel_id):
return self.get_release_by_id(rel_id)
def get_release_by_commit_id(self, commit_id):
for _, sw_release in self._sw_releases:
if sw_release.commit_id == commit_id:
return sw_release
return None
def iterate_releases_by_state(self, state):
'''return iteration of releases matching specified state.
sorted by id in ascending order
'''
sorted_list = sorted(self._sw_releases)
for rel_id in sorted_list:
rel_data = self._sw_releases[rel_id]
if rel_data.metadata['state'] == state:
yield rel_data
def iterate_releases(self):
'''return iteration of all releases sorted by id in ascending order'''
sorted_list = sorted(self._sw_releases)
for rel_id in sorted_list:
yield self._sw_releases[rel_id]
def update_state(self, list_of_releases, state):
for release_id in list_of_releases:
release = self.get_release_by_id(release_id)
if release is not None:
release.update_state(state)
class LocalStorage(object):
def __init__(self):
self._storage = threading.local()
def get_value(self, key):
if hasattr(self._storage, key):
return getattr(self._storage, key)
else:
return None
def set_value(self, key, value):
setattr(self._storage, key, value)
def void_value(self, key):
if hasattr(self._storage, key):
delattr(self._storage, key)
_local_storage = LocalStorage()
def get_SWReleaseCollection():
release_data = _local_storage.get_value('release_data')
if release_data is None:
LOG.info("Load release_data")
release_data = ReleaseData()
release_data.load_all()
LOG.info("release_data loaded")
_local_storage.set_value('release_data', release_data)
return SWReleaseCollection(release_data)
def reload_release_data():
_local_storage.void_value('release_data')