Merge "Improvements to sneaky patch utility"
This commit is contained in:
commit
661fabdbb3
|
@ -3,12 +3,33 @@ Copyright (c) 2023 Wind River Systems, Inc.
|
||||||
|
|
||||||
SPDX-License-Identifier: Apache-2.0
|
SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
This utility creates an ostree patch using .deb files
|
||||||
|
This utility is meant to be run on the controller
|
||||||
|
It writes to /opt/backups because it needs lots of disk space
|
||||||
|
|
||||||
|
Future Improvements:
|
||||||
|
1) support wildcards for .debs
|
||||||
|
2) Verify debs are newer than what is installed (otherwise the install fails)
|
||||||
|
3) Figure out how to run before bootstrap (not enough disk space)
|
||||||
|
4) Figure out how to avoid these GPG workarounds
|
||||||
|
sudo sed -i '$a gpg-verify=false' /var/www/pages/feed/rel-23.09/ostree_repo/config
|
||||||
|
sudo sed -i '$a gpg-verify=false' /sysroot/ostree/repo/config
|
||||||
|
|
||||||
|
The following is a sample patch.yaml that shows how a series of 2 patches can be made:
|
||||||
|
|
||||||
|
---
|
||||||
|
SNEAKY_1:
|
||||||
|
debs:
|
||||||
|
- sysinv-1.deb
|
||||||
|
- software-1.deb
|
||||||
|
sneaky_script: restart.sh
|
||||||
|
|
||||||
|
SNEAKY_2:
|
||||||
|
debs:
|
||||||
|
- sysinv-2.deb
|
||||||
|
sneaky_script: restart2.sh
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# This utility creates an ostree patch using .deb files
|
|
||||||
# This utility is meant to be run on the controller
|
|
||||||
# It writes to /opt/backups because it needs lots of disk space
|
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
from cgcs_patch import ostree_utils
|
from cgcs_patch import ostree_utils
|
||||||
from cgcs_patch import patch_functions
|
from cgcs_patch import patch_functions
|
||||||
|
@ -26,14 +47,100 @@ import time
|
||||||
from tsconfig.tsconfig import SW_VERSION
|
from tsconfig.tsconfig import SW_VERSION
|
||||||
import xml.etree.ElementTree as ET
|
import xml.etree.ElementTree as ET
|
||||||
from xml.dom import minidom
|
from xml.dom import minidom
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
|
||||||
|
class PatchInfo(object):
|
||||||
|
|
||||||
|
def __init__(self,
|
||||||
|
patch_id,
|
||||||
|
debs,
|
||||||
|
install_instructions=None,
|
||||||
|
pem_file=None,
|
||||||
|
req_patch=None,
|
||||||
|
sneaky_script=None,
|
||||||
|
description=None,
|
||||||
|
summary=None,
|
||||||
|
sw_version=None,
|
||||||
|
warnings=None):
|
||||||
|
# debs must be a string and not a list
|
||||||
|
if not isinstance(debs, list):
|
||||||
|
raise ValueError("debs for %s must be a list and not %s" % (patch_id, type(debs)))
|
||||||
|
self.debs = debs
|
||||||
|
self.patch_id = patch_id
|
||||||
|
self.install_instructions = install_instructions
|
||||||
|
self.pem_file = pem_file
|
||||||
|
self.req_patch = req_patch
|
||||||
|
self.sneaky_script = sneaky_script
|
||||||
|
self.description = description
|
||||||
|
self.summary = summary
|
||||||
|
self.sw_version = sw_version
|
||||||
|
self.warnings = warnings
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_args(cls, args):
|
||||||
|
"""Construct a list of a single PatchInfo based on args"""
|
||||||
|
return [cls(args.patch_id,
|
||||||
|
args.debs,
|
||||||
|
install_instructions=args.install_instructions,
|
||||||
|
pem_file=args.pem_file,
|
||||||
|
req_patch=args.req_patch,
|
||||||
|
sneaky_script=args.sneaky_script,
|
||||||
|
description=args.description,
|
||||||
|
summary=args.summary,
|
||||||
|
sw_version=args.sw_version,
|
||||||
|
warnings=args.warnings), ]
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_val(some_key, patch_dict, args):
|
||||||
|
return patch_dict.get(some_key, getattr(args, some_key))
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_yaml(cls, some_yaml, args):
|
||||||
|
"""Construct a list of a PatchInfo based on parsing yaml"""
|
||||||
|
|
||||||
|
patch_info_list = []
|
||||||
|
with open(some_yaml) as f:
|
||||||
|
yaml_data = yaml.safe_load(f)
|
||||||
|
invalid_yaml = set()
|
||||||
|
req_patch = None
|
||||||
|
for patch_id, patch_contents in yaml_data.items():
|
||||||
|
# validate the patch_contents
|
||||||
|
for patch_key in patch_contents.keys():
|
||||||
|
if not hasattr(args, patch_key):
|
||||||
|
print("invalid patch attribute: %s" % patch_key)
|
||||||
|
invalid_yaml.add(patch_key)
|
||||||
|
if invalid_yaml:
|
||||||
|
raise ValueError("yaml contains invalid entries %s" % invalid_yaml)
|
||||||
|
|
||||||
|
# When creating a chain of patches, they need to 'require' the previous one
|
||||||
|
# if the req_patch was passed in the yaml or args, use it.
|
||||||
|
req_patch_cur = cls.get_val('req_patch', patch_contents, args)
|
||||||
|
if req_patch_cur is None:
|
||||||
|
req_patch_cur = req_patch
|
||||||
|
|
||||||
|
patch_info_list.append(cls(patch_id,
|
||||||
|
patch_contents.get('debs'),
|
||||||
|
install_instructions=cls.get_val('install_instructions', patch_contents, args),
|
||||||
|
pem_file=cls.get_val('pem_file', patch_contents, args),
|
||||||
|
req_patch=req_patch_cur,
|
||||||
|
sneaky_script=cls.get_val('sneaky_script', patch_contents, args),
|
||||||
|
description=cls.get_val('description', patch_contents, args),
|
||||||
|
summary=cls.get_val('summary', patch_contents, args),
|
||||||
|
sw_version=cls.get_val('sw_version', patch_contents, args),
|
||||||
|
warnings=cls.get_val('warnings', patch_contents, args)))
|
||||||
|
|
||||||
|
# set the 'next' req_patch to be this patch_id
|
||||||
|
req_patch = patch_id
|
||||||
|
return patch_info_list
|
||||||
|
|
||||||
|
|
||||||
def setup_argparse():
|
def setup_argparse():
|
||||||
parser = argparse.ArgumentParser(prog="sneaky_patch",
|
parser = argparse.ArgumentParser(prog="sneaky_patch",
|
||||||
description="Creates a patch from a deb file")
|
description="Creates a patch from a deb file")
|
||||||
parser.add_argument('deb',
|
parser.add_argument('debs',
|
||||||
nargs="+", # accepts a list
|
nargs="+", # accepts a list
|
||||||
help='List of deb files to install to a patch')
|
help='List of deb files to install to a patch or a yaml file')
|
||||||
parser.add_argument('--verbose',
|
parser.add_argument('--verbose',
|
||||||
action='store_true',
|
action='store_true',
|
||||||
help="Display verbose output")
|
help="Display verbose output")
|
||||||
|
@ -81,8 +188,22 @@ def print_debug(output, debug):
|
||||||
print("%s" % output)
|
print("%s" % output)
|
||||||
|
|
||||||
|
|
||||||
def get_repo_src(args):
|
def get_major_release_version(sw_release_version):
|
||||||
return "/var/www/pages/feed/rel-%s/ostree_repo" % args.sw_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 get_repo_src(sw_version):
|
||||||
|
return "/var/www/pages/feed/rel-%s/ostree_repo" % get_major_release_version(sw_version)
|
||||||
|
|
||||||
|
|
||||||
def add_text_tag_to_xml(parent, name, text):
|
def add_text_tag_to_xml(parent, name, text):
|
||||||
|
@ -91,26 +212,26 @@ def add_text_tag_to_xml(parent, name, text):
|
||||||
return tag
|
return tag
|
||||||
|
|
||||||
|
|
||||||
def gen_xml(file_name, base_commit_id, base_checksum, commit_id, commit_checksum, args):
|
def gen_xml(file_name, base_commit_id, base_checksum, commit_id, commit_checksum, patch_info):
|
||||||
top = ET.Element("patch")
|
top = ET.Element("patch")
|
||||||
add_text_tag_to_xml(top, "id", args.patch_id)
|
add_text_tag_to_xml(top, "id", patch_info.patch_id)
|
||||||
add_text_tag_to_xml(top, "sw_version", args.sw_version)
|
add_text_tag_to_xml(top, "sw_version", patch_info.sw_version)
|
||||||
add_text_tag_to_xml(top, "summary", args.summary)
|
add_text_tag_to_xml(top, "summary", patch_info.summary)
|
||||||
desc = args.description
|
desc = patch_info.description
|
||||||
if desc is None:
|
if desc is None:
|
||||||
desc = "Deb Files: %s" % " ".join(args.deb)
|
desc = "Deb Files: %s" % " ".join(patch_info.debs)
|
||||||
add_text_tag_to_xml(top, "description", desc)
|
add_text_tag_to_xml(top, "description", desc)
|
||||||
add_text_tag_to_xml(top, "install_instructions", args.install_instructions)
|
add_text_tag_to_xml(top, "install_instructions", patch_info.install_instructions)
|
||||||
add_text_tag_to_xml(top, "warnings", args.warnings)
|
add_text_tag_to_xml(top, "warnings", patch_info.warnings)
|
||||||
add_text_tag_to_xml(top, "status", 'DEV')
|
add_text_tag_to_xml(top, "status", 'DEV')
|
||||||
add_text_tag_to_xml(top, "unremovable", "N")
|
add_text_tag_to_xml(top, "unremovable", "N")
|
||||||
if args.sneaky_script is None:
|
if patch_info.sneaky_script is None:
|
||||||
add_text_tag_to_xml(top, "reboot_required", "Y")
|
add_text_tag_to_xml(top, "reboot_required", "Y")
|
||||||
else:
|
else:
|
||||||
add_text_tag_to_xml(top, "reboot_required", "N")
|
add_text_tag_to_xml(top, "reboot_required", "N")
|
||||||
add_text_tag_to_xml(top,
|
add_text_tag_to_xml(top,
|
||||||
"restart_script",
|
"restart_script",
|
||||||
os.path.basename(args.sneaky_script))
|
os.path.basename(patch_info.sneaky_script))
|
||||||
|
|
||||||
content = ET.SubElement(top, "contents")
|
content = ET.SubElement(top, "contents")
|
||||||
ostree = ET.SubElement(content, "ostree")
|
ostree = ET.SubElement(content, "ostree")
|
||||||
|
@ -126,8 +247,8 @@ def gen_xml(file_name, base_commit_id, base_checksum, commit_id, commit_checksum
|
||||||
add_text_tag_to_xml(commit, "checksum", commit_checksum)
|
add_text_tag_to_xml(commit, "checksum", commit_checksum)
|
||||||
|
|
||||||
req = ET.SubElement(top, 'requires')
|
req = ET.SubElement(top, 'requires')
|
||||||
if args.req_patch is not None:
|
if patch_info.req_patch is not None:
|
||||||
add_text_tag_to_xml(req, 'req_patch_id', args.req_patch)
|
add_text_tag_to_xml(req, 'req_patch_id', patch_info.req_patch)
|
||||||
|
|
||||||
add_text_tag_to_xml(top, "semantics", "")
|
add_text_tag_to_xml(top, "semantics", "")
|
||||||
|
|
||||||
|
@ -164,13 +285,42 @@ def sign_and_pack(patch_file, tar_dir, pem_file):
|
||||||
print(" !!! Patch file is located at: %s" % patch_file)
|
print(" !!! Patch file is located at: %s" % patch_file)
|
||||||
|
|
||||||
|
|
||||||
def make_patch(args, tempdir, rootfs):
|
def setup_patch(feed_dir, patch_bare_dir, debug):
|
||||||
# This algorthithm is based on make_patch.py
|
|
||||||
# Phase 1: make an ostree that contains the new commit based on the new rootfs
|
# Phase 1: make an ostree that contains the new commit based on the new rootfs
|
||||||
# - required because a bare repo can create a commit from a rootfs, but an archive repo cannot
|
# - required because a bare repo can create a commit from a rootfs, but an archive repo cannot
|
||||||
# ostree --repo=/opt/backups/sneaky/patch_bare init --mode=bare
|
# ostree --repo=/opt/backups/sneaky/patch_bare init --mode=bare
|
||||||
# ostree --repo=/opt/backups/sneaky/patch_bare pull-local \
|
# ostree --repo=/opt/backups/sneaky/patch_bare pull-local \
|
||||||
# /var/www/pages/feed/rel-22.12/ostree_repo
|
# /var/www/pages/feed/rel-22.12/ostree_repo
|
||||||
|
# Phase 1: Step 1: create a bare patch repo
|
||||||
|
try:
|
||||||
|
print(" - Creating bare patch repo ...")
|
||||||
|
output = subprocess.check_output(["ostree",
|
||||||
|
"--repo=%s" % patch_bare_dir,
|
||||||
|
"init",
|
||||||
|
"--mode=bare"],
|
||||||
|
stderr=subprocess.STDOUT)
|
||||||
|
print_debug(output, debug)
|
||||||
|
except CalledProcessError as ex:
|
||||||
|
print("Failed ostree init bare. %s" % ex.output)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
# Phase 1: Step 2: Pull history from ostree clone_dir (ie: the feed_dir)
|
||||||
|
try:
|
||||||
|
print(" - Updating bare patch repo ...")
|
||||||
|
output = subprocess.check_output(["ostree",
|
||||||
|
"--repo=%s" % patch_bare_dir,
|
||||||
|
"pull-local",
|
||||||
|
feed_dir],
|
||||||
|
stderr=subprocess.STDOUT)
|
||||||
|
print_debug(output, debug)
|
||||||
|
except CalledProcessError as ex:
|
||||||
|
print("Failed ostree pull-local. %s" % ex.output)
|
||||||
|
return 1
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def make_patch(patch_info, tempdir, rootfs, feed_dir, patch_archive_dir, debug, verbose):
|
||||||
|
# This algorthithm is based on make_patch.py
|
||||||
# ostree --repo=/opt/backups/sneaky/patch_bare commit --tree=dir=/opt/backups/sneaky/rootfs \
|
# ostree --repo=/opt/backups/sneaky/patch_bare commit --tree=dir=/opt/backups/sneaky/rootfs \
|
||||||
# --skip-if-unchanged --branch=starlingx --subject=sneaky --timestamp=timestamp
|
# --skip-if-unchanged --branch=starlingx --subject=sneaky --timestamp=timestamp
|
||||||
# TODO(abailey): Determine if these can also be added
|
# TODO(abailey): Determine if these can also be added
|
||||||
|
@ -187,37 +337,9 @@ def make_patch(args, tempdir, rootfs):
|
||||||
# rsync from feed_dir and patch_archive with the difference stored in delta_dir
|
# rsync from feed_dir and patch_archive with the difference stored in delta_dir
|
||||||
|
|
||||||
prev = datetime.now()
|
prev = datetime.now()
|
||||||
feed_dir = get_repo_src(args)
|
|
||||||
patch_bare_dir = "%s/patch_bare" % tempdir # bare
|
patch_bare_dir = "%s/patch_bare" % tempdir # bare
|
||||||
patch_archive_dir = "%s/patch_archive" % tempdir # archive
|
|
||||||
|
|
||||||
# Phase 1: Step 1: create a bare patch repo
|
# Phase 1: Step 3: Create a new commit. Needs a commit-id
|
||||||
try:
|
|
||||||
print(" - Creating bare patch repo ...")
|
|
||||||
output = subprocess.check_output(["ostree",
|
|
||||||
"--repo=%s" % patch_bare_dir,
|
|
||||||
"init",
|
|
||||||
"--mode=bare"],
|
|
||||||
stderr=subprocess.STDOUT)
|
|
||||||
print_debug(output, args.debug)
|
|
||||||
except CalledProcessError as ex:
|
|
||||||
print("Failed ostree init bare. %s" % ex.output)
|
|
||||||
return 1
|
|
||||||
|
|
||||||
# Phase 1: Step 2: Pull history from ostree clone_dir (ie: the feed_dir)
|
|
||||||
try:
|
|
||||||
print(" - Updating bare patch repo ...")
|
|
||||||
output = subprocess.check_output(["ostree",
|
|
||||||
"--repo=%s" % patch_bare_dir,
|
|
||||||
"pull-local",
|
|
||||||
feed_dir],
|
|
||||||
stderr=subprocess.STDOUT)
|
|
||||||
print_debug(output, args.debug)
|
|
||||||
except CalledProcessError as ex:
|
|
||||||
print("Failed ostree pull-local. %s" % ex.output)
|
|
||||||
return 1
|
|
||||||
|
|
||||||
# Phase 1: Step 3: Create a new commit Needs a commit
|
|
||||||
timestamp = time.asctime()
|
timestamp = time.asctime()
|
||||||
subject = "Commit-id: SNEAKY-" + time.strftime("%Y%m%d%H%M%S", time.localtime())
|
subject = "Commit-id: SNEAKY-" + time.strftime("%Y%m%d%H%M%S", time.localtime())
|
||||||
try:
|
try:
|
||||||
|
@ -231,11 +353,11 @@ def make_patch(args, tempdir, rootfs):
|
||||||
"'--timestamp=%s'" % timestamp,
|
"'--timestamp=%s'" % timestamp,
|
||||||
"'--subject=%s'" % subject],
|
"'--subject=%s'" % subject],
|
||||||
stderr=subprocess.STDOUT)
|
stderr=subprocess.STDOUT)
|
||||||
print_debug(output, args.debug)
|
print_debug(output, debug)
|
||||||
except CalledProcessError as ex:
|
except CalledProcessError as ex:
|
||||||
print("Failed ostree commit. %s" % ex.output)
|
print("Failed ostree commit. %s" % ex.output)
|
||||||
return 1
|
return 1
|
||||||
prev = print_duration("commit creation", prev, args.verbose)
|
prev = print_duration("commit creation", prev, verbose)
|
||||||
|
|
||||||
# Phase 2: Step 1: Make the archive repo containing the patch contents
|
# Phase 2: Step 1: Make the archive repo containing the patch contents
|
||||||
try:
|
try:
|
||||||
|
@ -245,7 +367,7 @@ def make_patch(args, tempdir, rootfs):
|
||||||
"init",
|
"init",
|
||||||
"--mode=archive-z2"],
|
"--mode=archive-z2"],
|
||||||
stderr=subprocess.STDOUT)
|
stderr=subprocess.STDOUT)
|
||||||
print_debug(output, args.debug)
|
print_debug(output, debug)
|
||||||
except CalledProcessError as ex:
|
except CalledProcessError as ex:
|
||||||
print("Failed ostree init archive. %s" % ex.output)
|
print("Failed ostree init archive. %s" % ex.output)
|
||||||
return 1
|
return 1
|
||||||
|
@ -259,7 +381,7 @@ def make_patch(args, tempdir, rootfs):
|
||||||
"--depth=1",
|
"--depth=1",
|
||||||
patch_bare_dir],
|
patch_bare_dir],
|
||||||
stderr=subprocess.STDOUT)
|
stderr=subprocess.STDOUT)
|
||||||
print_debug(output, args.debug)
|
print_debug(output, debug)
|
||||||
except CalledProcessError as ex:
|
except CalledProcessError as ex:
|
||||||
print("Failed ostree archive pull-local. %s" % ex.output)
|
print("Failed ostree archive pull-local. %s" % ex.output)
|
||||||
return 1
|
return 1
|
||||||
|
@ -272,13 +394,14 @@ def make_patch(args, tempdir, rootfs):
|
||||||
"summary",
|
"summary",
|
||||||
"-u"],
|
"-u"],
|
||||||
stderr=subprocess.STDOUT)
|
stderr=subprocess.STDOUT)
|
||||||
print_debug(output, args.debug)
|
print_debug(output, debug)
|
||||||
except CalledProcessError as ex:
|
except CalledProcessError as ex:
|
||||||
print("Failed ostree summary update. %s" % ex.output)
|
print("Failed ostree summary update. %s" % ex.output)
|
||||||
return 1
|
return 1
|
||||||
prev = print_duration("creating archive", prev, args.verbose)
|
prev = print_duration("creating archive", prev, verbose)
|
||||||
|
|
||||||
# this is the difference between the feed_dir and the archive
|
# this is the difference between the feed_dir and the archive
|
||||||
|
# Note that the feed_dir will be the last patch
|
||||||
try:
|
try:
|
||||||
# automatically creates "delta_dir"
|
# automatically creates "delta_dir"
|
||||||
print(" - rsyncing to determine patch delta...")
|
print(" - rsyncing to determine patch delta...")
|
||||||
|
@ -297,30 +420,30 @@ def make_patch(args, tempdir, rootfs):
|
||||||
patch_archive_dir + "/", # SRC
|
patch_archive_dir + "/", # SRC
|
||||||
"delta_dir" + "/"], # DEST
|
"delta_dir" + "/"], # DEST
|
||||||
stderr=subprocess.STDOUT)
|
stderr=subprocess.STDOUT)
|
||||||
print_debug(output, args.debug)
|
print_debug(output, debug)
|
||||||
except CalledProcessError as ex:
|
except CalledProcessError as ex:
|
||||||
print("Failed rsync. %s" % ex.output)
|
print("Failed rsync. %s" % ex.output)
|
||||||
return 1
|
return 1
|
||||||
prev = print_duration("rsync", prev, args.verbose)
|
prev = print_duration("rsync", prev, verbose)
|
||||||
|
|
||||||
# base_commit comes from feed
|
# base_commit comes from feed
|
||||||
# commit comes from archive
|
# commit comes from archive
|
||||||
# checksum values do not appear to be used by patching
|
# checksum values do not appear to be used by patching
|
||||||
base_commit_id = ostree_utils.get_feed_latest_commit(args.sw_version)
|
base_commit_id = ostree_utils.get_ostree_latest_commit("starlingx", feed_dir)
|
||||||
base_checksum = "UNUSED"
|
base_checksum = "UNUSED"
|
||||||
commit_id = ostree_utils.get_ostree_latest_commit("starlingx", patch_archive_dir)
|
commit_id = ostree_utils.get_ostree_latest_commit("starlingx", patch_archive_dir)
|
||||||
commit_checksum = "UNUSED"
|
commit_checksum = "UNUSED"
|
||||||
|
|
||||||
# Writing the final patch file
|
# Writing the final patch file
|
||||||
final_patch_file = "/tmp/%s.patch" % args.patch_id
|
final_patch_file = "/tmp/%s.patch" % patch_info.patch_id
|
||||||
|
|
||||||
pem_url = "https://raw.githubusercontent.com/starlingx/root/master/build-tools/signing/dev-private-key.pem"
|
pem_url = "https://raw.githubusercontent.com/starlingx/root/master/build-tools/signing/dev-private-key.pem"
|
||||||
pem_file = "%s/dev-private-key.pem" % tempdir
|
pem_file = "%s/dev-private-key.pem" % tempdir
|
||||||
if args.pem_file is None:
|
if patch_info.pem_file is None:
|
||||||
urllib.request.urlretrieve(pem_url, pem_file)
|
urllib.request.urlretrieve(pem_url, pem_file)
|
||||||
else:
|
else:
|
||||||
# use the already downloaded pem_file passed as an argument
|
# use the already downloaded pem_file passed as an argument
|
||||||
pem_file = args.pem_file
|
pem_file = patch_info.pem_file
|
||||||
|
|
||||||
with tempfile.TemporaryDirectory(prefix="sneaky_patch", dir="/tmp") as sneaky_tar:
|
with tempfile.TemporaryDirectory(prefix="sneaky_patch", dir="/tmp") as sneaky_tar:
|
||||||
print(" - Generating software.tar...") # Make tarball of delta_dir
|
print(" - Generating software.tar...") # Make tarball of delta_dir
|
||||||
|
@ -335,24 +458,27 @@ def make_patch(args, tempdir, rootfs):
|
||||||
gen_xml("metadata.xml",
|
gen_xml("metadata.xml",
|
||||||
base_commit_id, base_checksum,
|
base_commit_id, base_checksum,
|
||||||
commit_id, commit_checksum,
|
commit_id, commit_checksum,
|
||||||
args)
|
patch_info)
|
||||||
with tarfile.open("%s/metadata.tar" % sneaky_tar, "w") as tar:
|
with tarfile.open("%s/metadata.tar" % sneaky_tar, "w") as tar:
|
||||||
tar.add("metadata.xml")
|
tar.add("metadata.xml")
|
||||||
os.remove("metadata.xml")
|
os.remove("metadata.xml")
|
||||||
|
|
||||||
# Copy the restart script to the temporary tar directory
|
# Copy the restart script to the temporary tar directory
|
||||||
if args.sneaky_script is not None:
|
if patch_info.sneaky_script is not None:
|
||||||
shutil.copy(args.sneaky_script, sneaky_tar)
|
shutil.copy(patch_info.sneaky_script, sneaky_tar)
|
||||||
|
|
||||||
# patch_functions.write_patch looks like it skips restart scripts
|
# patch_functions.write_patch looks like it skips restart scripts
|
||||||
# using the logic from make_patch.py sign_and_pack
|
# using the logic from make_patch.py sign_and_pack
|
||||||
sign_and_pack(final_patch_file, sneaky_tar, pem_file)
|
sign_and_pack(final_patch_file, sneaky_tar, pem_file)
|
||||||
|
|
||||||
prev = print_duration("Writing patch", prev, args.verbose)
|
prev = print_duration("Writing patch", prev, verbose)
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|
||||||
def sneaky_patch(args):
|
def sneaky_patch(patch_info_list, debug, verbose):
|
||||||
|
# hold onto the cwd where we are when we initiate patching
|
||||||
|
cwd = os.getcwd()
|
||||||
|
|
||||||
# Hold onto a directory handle outside of chroot.
|
# Hold onto a directory handle outside of chroot.
|
||||||
real_root = os.open("/", os.O_RDONLY)
|
real_root = os.open("/", os.O_RDONLY)
|
||||||
in_jail = False
|
in_jail = False
|
||||||
|
@ -360,12 +486,14 @@ def sneaky_patch(args):
|
||||||
prev = datetime.now()
|
prev = datetime.now()
|
||||||
start_time = prev
|
start_time = prev
|
||||||
|
|
||||||
|
# all patches must be based on the same sw_version
|
||||||
|
repo_src = get_repo_src(patch_info_list[0].sw_version)
|
||||||
|
|
||||||
# Step 1: make a temporary directory under /opt/backups
|
# Step 1: make a temporary directory under /opt/backups
|
||||||
with tempfile.TemporaryDirectory(prefix="sneaky", dir="/opt/backups") as sneaky_temp:
|
with tempfile.TemporaryDirectory(prefix="sneaky", dir="/opt/backups") as sneaky_temp:
|
||||||
|
|
||||||
# Checkout the ostree feed
|
# Checkout the ostree feed
|
||||||
rootfs = "%s/rootfs" % sneaky_temp
|
rootfs = "%s/rootfs" % sneaky_temp
|
||||||
repo_src = get_repo_src(args)
|
|
||||||
try:
|
try:
|
||||||
print(" - Checking out ostree...")
|
print(" - Checking out ostree...")
|
||||||
output = subprocess.check_output(["ostree",
|
output = subprocess.check_output(["ostree",
|
||||||
|
@ -375,93 +503,131 @@ def sneaky_patch(args):
|
||||||
"starlingx",
|
"starlingx",
|
||||||
rootfs],
|
rootfs],
|
||||||
stderr=subprocess.STDOUT)
|
stderr=subprocess.STDOUT)
|
||||||
print_debug(output, args.debug)
|
print_debug(output, debug)
|
||||||
except CalledProcessError as ex:
|
except CalledProcessError as ex:
|
||||||
print("Failed ostree checkout. %s" % ex.output)
|
print("Failed ostree checkout. %s" % ex.output)
|
||||||
return 1
|
return 1
|
||||||
prev = print_duration("Ostree checkout", prev, args.verbose)
|
prev = print_duration("Ostree checkout", prev, verbose)
|
||||||
|
|
||||||
# Stage the deb files under rootfs/var/tmp/
|
|
||||||
rootfs_tmp = "%s/var/tmp" % rootfs
|
rootfs_tmp = "%s/var/tmp" % rootfs
|
||||||
for deb_file in args.deb:
|
patch_bare_dir = "%s/patch_bare" % sneaky_temp # bare
|
||||||
|
feed_dir = repo_src
|
||||||
|
rc = setup_patch(repo_src, patch_bare_dir, debug)
|
||||||
|
if rc != 0:
|
||||||
|
print("setup patch failed")
|
||||||
|
return rc
|
||||||
|
prev = print_duration("Patch Setup", prev, verbose)
|
||||||
|
|
||||||
|
# loop over the patches...
|
||||||
|
for patch_info in patch_info_list:
|
||||||
|
patch_desc = "Preparing Patch %s" % patch_info.patch_id
|
||||||
|
prev = print_duration(patch_desc, prev, verbose)
|
||||||
|
patch_archive_dir = "%s/patch_archive_%s" % (sneaky_temp, patch_info.patch_id) # archive
|
||||||
|
|
||||||
|
# We MUST be located at the starting directory
|
||||||
|
os.chdir(cwd)
|
||||||
|
|
||||||
|
# Stage the deb files under rootfs/var/tmp/
|
||||||
|
for deb_file in patch_info.debs:
|
||||||
|
try:
|
||||||
|
shutil.copy(deb_file, rootfs_tmp)
|
||||||
|
except Exception as ex:
|
||||||
|
print("Failed debian file copy. %s" % ex)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
# enter chroot jail and install those packages
|
||||||
|
# enter chroot jail
|
||||||
|
os.chroot(rootfs)
|
||||||
|
os.chdir('/')
|
||||||
|
in_jail = True
|
||||||
|
|
||||||
|
# Note: We need to leave chroot jail before calling 'return'
|
||||||
|
# otherwise the tmp dir will not be cleaned up
|
||||||
|
|
||||||
|
# symlink /etc
|
||||||
try:
|
try:
|
||||||
shutil.copy(deb_file, rootfs_tmp)
|
print(" - Setting up symlinks...")
|
||||||
except Exception as ex:
|
output = subprocess.check_output(["ln", "-sfn", "usr/etc", "etc"],
|
||||||
print("Failed debian file copy. %s" % ex)
|
stderr=subprocess.STDOUT)
|
||||||
|
print_debug(output, debug)
|
||||||
|
except CalledProcessError as ex:
|
||||||
|
print("Failed chroot symlink step. %s" % ex.output)
|
||||||
|
os.fchdir(real_root) # leave jail
|
||||||
|
os.chroot(".")
|
||||||
|
in_jail = False
|
||||||
return 1
|
return 1
|
||||||
|
# change into the /var/tmp in the chroot where the .deb files are located
|
||||||
|
os.chdir("/var/tmp")
|
||||||
|
deb_list = " ".join(patch_info.debs)
|
||||||
|
# install the deb files
|
||||||
|
try:
|
||||||
|
print(" - Installing %s ..." % deb_list)
|
||||||
|
install_args = ["dpkg", "-i"]
|
||||||
|
install_args.extend(patch_info.debs)
|
||||||
|
output = subprocess.check_output(install_args, stderr=subprocess.STDOUT)
|
||||||
|
print_debug(output, debug)
|
||||||
|
except CalledProcessError as ex:
|
||||||
|
print("Failed debian package installation. %s" % ex.output)
|
||||||
|
os.fchdir(real_root) # leave jail
|
||||||
|
os.chroot(".")
|
||||||
|
in_jail = False
|
||||||
|
return 1
|
||||||
|
prev = print_duration("Installing packages", prev, verbose)
|
||||||
|
# remove the etc symlink from within chroot
|
||||||
|
os.chdir('/')
|
||||||
|
if os.path.isdir("/etc"):
|
||||||
|
os.remove("etc")
|
||||||
|
|
||||||
# Step 4: enter chroot jail and install those packages
|
# leave chroot jail
|
||||||
# enter chroot jail
|
os.fchdir(real_root)
|
||||||
os.chroot(rootfs)
|
|
||||||
os.chdir('/')
|
|
||||||
in_jail = True
|
|
||||||
|
|
||||||
# Note: We need to leave chroot jail before calling 'return'
|
|
||||||
# otherwise the tmp dir will not be cleaned up
|
|
||||||
|
|
||||||
# symlink /etc
|
|
||||||
try:
|
|
||||||
print(" - Setting up symlinks...")
|
|
||||||
output = subprocess.check_output(["ln", "-sfn", "usr/etc", "etc"],
|
|
||||||
stderr=subprocess.STDOUT)
|
|
||||||
print_debug(output, args.debug)
|
|
||||||
except CalledProcessError as ex:
|
|
||||||
print("Failed chroot symlink step. %s" % ex.output)
|
|
||||||
os.fchdir(real_root) # leave jail
|
|
||||||
os.chroot(".")
|
os.chroot(".")
|
||||||
in_jail = False
|
in_jail = False
|
||||||
return 1
|
|
||||||
# change into the /var/tmp in the chroot where the .deb files are located
|
|
||||||
os.chdir("/var/tmp")
|
|
||||||
deb_list = " ".join(args.deb)
|
|
||||||
# install the deb files
|
|
||||||
try:
|
|
||||||
print(" - Installing %s ..." % deb_list)
|
|
||||||
install_args = ["dpkg", "-i"]
|
|
||||||
install_args.extend(args.deb)
|
|
||||||
output = subprocess.check_output(install_args, stderr=subprocess.STDOUT)
|
|
||||||
print_debug(output, args.debug)
|
|
||||||
except CalledProcessError as ex:
|
|
||||||
print("Failed debian package installation. %s" % ex.output)
|
|
||||||
os.fchdir(real_root) # leave jail
|
|
||||||
os.chroot(".")
|
|
||||||
in_jail = False
|
|
||||||
return 1
|
|
||||||
prev = print_duration("Installing packages", prev, args.verbose)
|
|
||||||
# remove the etc symlink from within chroot
|
|
||||||
os.chdir('/')
|
|
||||||
if os.path.isdir("/etc"):
|
|
||||||
os.remove("etc")
|
|
||||||
|
|
||||||
# leave chroot jail
|
# make the commit, etc..
|
||||||
os.fchdir(real_root)
|
make_patch(patch_info, sneaky_temp, rootfs, feed_dir, patch_archive_dir, debug, verbose)
|
||||||
os.chroot(".")
|
# for the next patch, the feed will be the archive_dir of the last patch
|
||||||
in_jail = False
|
feed_dir = patch_archive_dir
|
||||||
|
prev = print_duration("Committing changes", prev, verbose)
|
||||||
# make the commit, etc..
|
|
||||||
make_patch(args, sneaky_temp, rootfs)
|
|
||||||
prev = print_duration("Committing changes", prev, args.verbose)
|
|
||||||
|
|
||||||
# escape back from chroot jail
|
# escape back from chroot jail
|
||||||
if in_jail:
|
if in_jail:
|
||||||
|
# Should never get here...
|
||||||
os.fchdir(real_root)
|
os.fchdir(real_root)
|
||||||
os.chroot(".")
|
os.chroot(".")
|
||||||
# now we can safely close fd for real_root
|
# now we can safely close fd for real_root
|
||||||
os.close(real_root)
|
os.close(real_root)
|
||||||
|
|
||||||
print_duration("Entire activity", start_time, args.verbose)
|
print_duration("Entire activity", start_time, verbose)
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
|
|
||||||
def extra_validation(args):
|
def validate_file(some_file):
|
||||||
|
file_location = os.path.abspath(some_file)
|
||||||
|
if not os.path.isfile(file_location):
|
||||||
|
raise FileNotFoundError(file_location)
|
||||||
|
|
||||||
|
|
||||||
|
def extra_validation(patch_info_list):
|
||||||
# Add in any additional validators
|
# Add in any additional validators
|
||||||
# that argparse does not handle
|
# that argparse does not handle
|
||||||
if args.sneaky_script is not None:
|
unique_scripts = set()
|
||||||
script_location = os.path.abspath(args.sneaky_script)
|
for patch_info in patch_info_list:
|
||||||
if os.path.isfile(script_location):
|
# make sure all deb files exist
|
||||||
args.sneaky_script = script_location
|
for deb in patch_info.debs:
|
||||||
else:
|
validate_file(deb)
|
||||||
raise FileNotFoundError(script_location)
|
# if the script exists, determine its actual path
|
||||||
|
if patch_info.sneaky_script is not None:
|
||||||
|
script_location = os.path.abspath(patch_info.sneaky_script)
|
||||||
|
if os.path.isfile(script_location):
|
||||||
|
patch_info.sneaky_script = script_location
|
||||||
|
else:
|
||||||
|
raise FileNotFoundError(script_location)
|
||||||
|
# also check that the script is executable
|
||||||
|
if not os.access(script_location, os.X_OK):
|
||||||
|
raise PermissionError("%s needs executable permissions" % script_location)
|
||||||
|
if script_location in unique_scripts:
|
||||||
|
raise PermissionError("%s must be unique. It is already used by another patch" % script_location)
|
||||||
|
unique_scripts.add(script_location)
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
|
@ -470,8 +636,14 @@ def main():
|
||||||
if os.geteuid() != 0:
|
if os.geteuid() != 0:
|
||||||
print("MUST BE RUN AS ROOT (or sudo)")
|
print("MUST BE RUN AS ROOT (or sudo)")
|
||||||
return 1
|
return 1
|
||||||
extra_validation(args)
|
# If the args.debs is a yaml we parse that
|
||||||
return sneaky_patch(args)
|
# otherwise its the args that populate the PatchInfo
|
||||||
|
if args.debs[0].endswith(".yaml"):
|
||||||
|
patch_info_list = PatchInfo.from_yaml(args.debs[0], args)
|
||||||
|
else:
|
||||||
|
patch_info_list = PatchInfo.from_args(args)
|
||||||
|
extra_validation(patch_info_list)
|
||||||
|
return sneaky_patch(patch_info_list, args.debug, args.verbose)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
|
Loading…
Reference in New Issue