update/software/scripts/usm_load_import
junfeng-li 9bd770217c Display legacy patch info after upload
When using USM 'software upload' to upload prepatched iso, all
the patch metadata files are copied to /opt/software/metadata/committed
folder. In the system where legacy patch commands are still supported,
'sw-patch query' doesn't return these uploaded patch info as the command
scans /opt/patching/metadata/ directory.

This commit is to add extra step to copy patch metadata files to
/opt/patching/metedata/committed and restart legacy patching
service daemon so 'sw-patch query' can return the uploaded
patch info.

Test Plan:

PASS: build and deploy iso on system controller
PASS: upload 22.12 patch 6 iso
PASS: upload 25.03 iso

Task: 51066
Story: 2010676
Change-Id: I851203244895936567ce54f7c9d1780e7bf1fcc0
Signed-off-by: junfeng-li <junfeng.li@windriver.com>
2024-10-01 14:29:21 +00:00

385 lines
15 KiB
Python

#!/usr/bin/python3
# -*- encoding: utf-8 -*-
#
# vim: tabstop=4 shiftwidth=4 softtabstop=4
#
# Copyright (c) 2023-2024 Wind River Systems, Inc.
#
# SPDX-License-Identifier: Apache-2.0
#
"""
This script is run during 'software upload' command.
It is used to copy the required files from uploaded iso image
to the controller.
"""
import argparse
import glob
import logging as LOG
import os
import shutil
import subprocess
import sys
import upgrade_utils
AVAILABLE_DIR = "/opt/software/metadata/available"
UNAVAILABLE_DIR = "/opt/software/metadata/unavailable"
COMMITTED_DIR = "/opt/software/metadata/committed"
PATCHING_COMMITTED_DIR = "/opt/patching/metadata/committed"
FEED_OSTREE_BASE_DIR = "/var/www/pages/feed"
RELEASE_GA_NAME = "starlingx-%s"
SOFTWARE_STORAGE_DIR = "/opt/software"
TMP_DIR = "/tmp"
VAR_PXEBOOT_DIR = "/var/pxeboot"
FEED_REMOTE = "starlingx"
FEED_BRANCH = "starlingx"
# TODO(bqian) move the function to shareable utils.
def get_major_release_version(sw_release_version):
"""Gets the major release for a given software version """
if not sw_release_version:
return None
else:
try:
separator = '.'
separated_string = sw_release_version.split(separator)
major_version = separated_string[0] + separator + separated_string[1]
return major_version
except Exception:
return None
def setup_from_release_load(from_release, to_feed_dir):
"""
Setup from release load
:param from_release: from release version
:param to_feed_dir: to release feed directory
"""
# 'None' is passed from this script argument
if from_release == 'None':
LOG.info("From release is not specified. Skipping from release load")
return
try:
from_major_rel = get_major_release_version(from_release)
# Copy install_uuid to /var/www/pages/feed/rel-<release>
from_feed_dir = os.path.join(FEED_OSTREE_BASE_DIR, ("rel-%s" % from_major_rel))
shutil.copyfile(os.path.join(from_feed_dir, "install_uuid"),
os.path.join(to_feed_dir, "install_uuid"))
LOG.info("Copied install_uuid to %s", to_feed_dir)
# Copy pxeboot-update-${from_major_release}.sh to from-release feed /upgrades
from_iso_upgrades_dir = os.path.join(from_feed_dir, "upgrades")
os.makedirs(from_iso_upgrades_dir, exist_ok=True)
shutil.copyfile(os.path.join("/etc", "pxeboot-update-%s.sh" % from_major_rel),
os.path.join(from_iso_upgrades_dir, "pxeboot-update-%s.sh" % from_major_rel))
LOG.info("Copied pxeboot-update-%s.sh to %s", from_major_rel, from_iso_upgrades_dir)
# Copy pxelinux.cfg.files to from-release feed /pxeboot
from_feed_pxeboot_dir = os.path.join(from_feed_dir, "pxeboot")
os.makedirs(from_feed_pxeboot_dir, exist_ok=True)
# Find from-release pxelinux.cfg.files
pxe_dir = os.path.join(VAR_PXEBOOT_DIR, "pxelinux.cfg.files")
from_pxe_files = glob.glob(os.path.join(pxe_dir, '*' + from_major_rel))
for from_pxe_file in from_pxe_files:
if os.path.isfile(from_pxe_file):
shutil.copyfile(from_pxe_file, os.path.join(from_feed_pxeboot_dir,
os.path.basename(from_pxe_file)))
LOG.info("Copied %s to %s", from_pxe_file, from_feed_pxeboot_dir)
except Exception:
raise
def load_import(from_release, to_major_rel, iso_mount_dir):
"""
Import the iso files to the feed and pxeboot directories
:param from_release: from release version (MM.mm/MM.mm.p)
:param to_release: to release version (MM.mm.p)
:param iso_mount_dir: iso mount dir
"""
# for now the from_release is the same as from_major_rel. until
# the sw_version is redefied to major release version, there is
# chance that from_release could be major.minor.patch.
try:
# Copy the iso file to /var/www/pages/feed/rel-<release>
os.makedirs(FEED_OSTREE_BASE_DIR, exist_ok=True)
to_feed_dir = os.path.join(FEED_OSTREE_BASE_DIR, ("rel-%s" % to_major_rel))
if os.path.exists(to_feed_dir):
shutil.rmtree(to_feed_dir)
LOG.info("Removed existing %s", to_feed_dir)
os.makedirs(to_feed_dir, exist_ok=True)
feed_contents = ["install_uuid", "efi.img", "kickstart",
"ostree_repo", "pxeboot", "upgrades"]
for content in feed_contents:
src_abs_path = os.path.join(iso_mount_dir, content)
if os.path.isfile(src_abs_path):
shutil.copyfile(src_abs_path, os.path.join(to_feed_dir, content))
LOG.info("Copied %s to %s", src_abs_path, to_feed_dir)
elif os.path.isdir(src_abs_path):
shutil.copytree(src_abs_path, os.path.join(to_feed_dir, content))
LOG.info("Copied %s to %s", src_abs_path, to_feed_dir)
# Add min-free-space-percent to feed ostree config file
config_path = "%s/ostree_repo/config" % to_feed_dir
if os.path.exists(config_path):
with open(config_path, 'r') as file:
content = file.read()
if "min-free-space-percent" not in content:
with open(config_path, 'a') as file:
file.write("min-free-space-percent=0\n")
# Create 'starlingx' remote on the feed ostree_repo
cmd = ["ostree", "remote", "add", "--repo=%s/ostree_repo/" % to_feed_dir,
FEED_REMOTE, "http://controller:8080/feed/rel-%s/ostree_repo/" % to_major_rel,
FEED_BRANCH]
try:
subprocess.check_call(cmd)
LOG.info("Created feed remote '%s'" % FEED_REMOTE)
except subprocess.CalledProcessError as e:
LOG.exception("Feed remote '%s' creation failed. Error: %s" % (FEED_REMOTE, str(e)))
raise
# Converted from upgrade package extraction script
shutil.copyfile(os.path.join(to_feed_dir, "kickstart", "kickstart.cfg"),
os.path.join(to_feed_dir, "kickstart.cfg"))
# Copy bzImage and initrd
bzimage_files = glob.glob(os.path.join(to_feed_dir, 'pxeboot', 'bzImage*'))
for bzimage_file in bzimage_files:
if os.path.isfile(bzimage_file):
shutil.copyfile(bzimage_file, os.path.join(VAR_PXEBOOT_DIR,
os.path.basename(bzimage_file)))
LOG.info("Copied %s to %s", bzimage_file, VAR_PXEBOOT_DIR)
initrd_files = glob.glob(os.path.join(to_feed_dir, 'pxeboot', 'initrd*'))
for initrd_file in initrd_files:
if os.path.isfile(initrd_file):
shutil.copyfile(initrd_file, os.path.join(VAR_PXEBOOT_DIR,
os.path.basename(initrd_file)))
LOG.info("Copied %s to %s", initrd_file, VAR_PXEBOOT_DIR)
# Copy to_release_feed/pxelinux.cfg.files to /var/pxeboot/pxelinux.cfg.files
pxeboot_cfg_files = glob.glob(os.path.join(to_feed_dir, 'pxeboot', 'pxelinux.cfg.files',
'*' + to_major_rel))
for pxeboot_cfg_file in pxeboot_cfg_files:
if os.path.isfile(pxeboot_cfg_file):
shutil.copyfile(pxeboot_cfg_file, os.path.join(VAR_PXEBOOT_DIR,
'pxelinux.cfg.files',
os.path.basename(pxeboot_cfg_file)))
LOG.info("Copied %s to %s", pxeboot_cfg_file, VAR_PXEBOOT_DIR)
# Copy pxeboot-update.sh to /etc
pxeboot_update_filename = "pxeboot-update-%s.sh" % to_major_rel
shutil.copyfile(os.path.join(to_feed_dir, "upgrades", pxeboot_update_filename),
os.path.join("/etc", pxeboot_update_filename))
os.chmod(os.path.join("/etc", pxeboot_update_filename), mode=0o755)
LOG.info("Copied pxeboot-update-%s.sh to %s", to_major_rel, "/etc")
# Setup from release load
setup_from_release_load(from_release, to_feed_dir)
except Exception as e:
LOG.exception("Load import failed. Error: %s" % str(e))
shutil.rmtree(to_feed_dir)
LOG.info("Removed %s", to_feed_dir)
raise
def move_metadata_file_to_available_dir(to_release, iso_mount_dir):
"""
Move release metadata file to /opt/software/metadata/available
:param to_release: release version
:param iso_mount_dir: iso mount dir
"""
try:
# Copy metadata.xml to /opt/software/metadata/available
os.makedirs(AVAILABLE_DIR, exist_ok=True)
metadata_name = f"{RELEASE_GA_NAME % to_release}-metadata.xml"
LOG.info("metadata name: %s", metadata_name)
abs_stx_release_metadata_file = os.path.join(iso_mount_dir,
'patches',
metadata_name)
# Copy stx release metadata.xml to available metadata dir
# TODO(jli14): prepatched iso will have more than one metadata file.
shutil.copyfile(abs_stx_release_metadata_file,
os.path.join(AVAILABLE_DIR, metadata_name))
LOG.info("Copied %s to %s", abs_stx_release_metadata_file, AVAILABLE_DIR)
except shutil.Error:
LOG.exception("Failed to copy the release %s metadata file to %s" %
(to_release, AVAILABLE_DIR))
raise
def generate_metadata_file_in_unavailable_dir(to_release):
"""
Generate release metadata file in /opt/software/metadata/unavailable
This is only for 22.12 pre USM iso load import
:param to_release: release version
"""
try:
# Copy metadata.xml to /opt/software/metadata/unavailable
os.makedirs(UNAVAILABLE_DIR, exist_ok=True)
# TODO(jli14): release name should be dynamically generated based on the branch.
metadata_name = f"{RELEASE_GA_NAME % to_release}-metadata.xml"
LOG.info("metadata name: %s", metadata_name)
# Generate metadata.xml
import xml.etree.ElementTree as ET
from xml.dom import minidom
root = ET.Element('patch')
ET.SubElement(root, "id").text = RELEASE_GA_NAME % to_release
ET.SubElement(root, "sw_version").text = to_release
ET.SubElement(root, "component").text = RELEASE_GA_NAME.split('-')[0]
ET.SubElement(root, "summary").text = 'This file is generated by usm_load_import'
xml_str = ET.tostring(root, encoding='unicode')
pretty_xml = minidom.parseString(xml_str).toprettyxml(indent=" ")
pretty_xml = '\n'.join([line for line in pretty_xml.split('\n') if line.strip()])
# Write to file
abs_path_metadata_filename = os.path.join(UNAVAILABLE_DIR, metadata_name)
with open(abs_path_metadata_filename, "w") as file:
file.write(pretty_xml)
except Exception:
LOG.exception("Failed to copy the release %s metadata file to %s" %
(to_release, UNAVAILABLE_DIR))
raise
def copy_patch_metadata_files_to_committed(iso_mount_dir):
"""
Copy patch metadata files to /opt/software/metadata/committed
:param iso_mount_dir: iso mount dir
"""
committed_patch_dir = os.path.join(iso_mount_dir, 'patches')
try:
shutil.copytree(committed_patch_dir, COMMITTED_DIR, dirs_exist_ok=True)
LOG.info("Copied patch metadata file to %s", COMMITTED_DIR)
except shutil.Error:
LOG.exception("Failed to copy patch metadata file(s) to %s" %
COMMITTED_DIR)
raise
def copy_patch_metadata_files_to_patching_committed(iso_mount_dir):
# TODO(jli14): remove this function when 'sw-patch query' is deprecated
"""
Copy patch metadata files to /opt/patching/metadata/committed
:param iso_mount_dir: iso mount dir
"""
deployed_patch_dir = os.path.join(iso_mount_dir, 'patches')
try:
shutil.copytree(deployed_patch_dir, PATCHING_COMMITTED_DIR, dirs_exist_ok=True)
LOG.info("Copied patch metadata file to %s", PATCHING_COMMITTED_DIR)
except shutil.Error:
LOG.exception("Failed to copy patch metadata file(s) to %s" %
PATCHING_COMMITTED_DIR)
raise
def restart_legacy_patching_service():
"""
Restart legacy patching service daemon
"""
# TODO(jli14): remove this function when 'sw-patch query' is deprecated
try:
restart_cmd = ['pmon-restart', 'sw-patch-controller-daemon']
LOG.info("Restarting legacy patching service daemon: %s", " ".join(restart_cmd))
subprocess.run(restart_cmd, stdout=subprocess.PIPE,
stderr=subprocess.STDOUT, check=True, text=True)
LOG.info("Restarted legacy patching service daemon successfully")
except Exception as e:
LOG.exception("Failed to restart legacy patching service daemon with error: %s", str(e))
raise
def sync_inactive_load_to_controller1(to_major_rel):
"""
Sync inactive load to controller-1
Upload is only allowed in controller-0 so sync to controller-1 is needed
:param to_major_rel: release version
"""
feed_dir = os.path.join(FEED_OSTREE_BASE_DIR, ("rel-%s" % to_major_rel))
sync_cmd = [
"rsync",
"-ac",
"--delete",
"--exclude", "tmp",
feed_dir,
"rsync://controller-1/feed"]
LOG.info("Syncing inactive load to controllers %s", ' '.join(sync_cmd))
subprocess.run(sync_cmd, stdout=subprocess.PIPE,
stderr=subprocess.STDOUT, check=True, text=True)
LOG.info("Sync controllers completed")
def main():
parser = argparse.ArgumentParser(
description="Import files from uploaded iso image to controller.",
epilog="Use %(prog)s -h for help.",
)
parser.add_argument(
"--from-release",
required=True,
help="The from-release version.",
)
parser.add_argument(
"--to-release",
required=True,
help="The to-release version, MM.mm.p",
)
parser.add_argument(
"--iso-dir",
required=True,
help="The mounted iso image directory.",
)
parser.add_argument(
"--is-usm-iso",
required=False,
help="True if the iso supports USM upgrade.",
default=True
)
args = parser.parse_args()
try:
to_major_rel = get_major_release_version(args.to_release)
LOG.info("Load import from %s to %s started", args.from_release, args.to_release)
load_import(args.from_release, to_major_rel, args.iso_dir)
if args.is_usm_iso in ["True", True]:
move_metadata_file_to_available_dir(args.to_release, args.iso_dir)
else:
# pre USM iso needs to generate metadata file
generate_metadata_file_in_unavailable_dir(args.to_release)
copy_patch_metadata_files_to_committed(args.iso_dir)
# Currently imported load is N-1
if args.from_release in ['None', None]:
copy_patch_metadata_files_to_patching_committed(args.iso_dir)
restart_legacy_patching_service()
sync_inactive_load_to_controller1(to_major_rel)
except Exception as e:
LOG.exception(e)
return 1
if __name__ == "__main__":
upgrade_utils.configure_logging('/var/log/software.log', log_level=LOG.INFO)
sys.exit(main())