
Adds validation to whether the .deb files exist. Validates if the restart script is executable. This adds in yaml support as the input. This adds multiple patch support (through the yaml). Adds support for a sem-ver sw-version (major.minor.micro) Validates if a sequence of patches use the same restart script (prohibited). Test Plan: Make a sneaky patch using command line args Make a sneaky patch sequence of 2 patches using yaml and verify they can both be applied and installed. Story: 2010547 Task: 48323 Signed-off-by: Al Bailey <al.bailey@windriver.com> Change-Id: If8e7745b2054b21a1476b9685629559a376a3841
651 lines
26 KiB
Python
651 lines
26 KiB
Python
"""
|
|
Copyright (c) 2023 Wind River Systems, Inc.
|
|
|
|
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
|
|
|
|
"""
|
|
import argparse
|
|
from cgcs_patch import ostree_utils
|
|
from cgcs_patch import patch_functions
|
|
from cgcs_patch import patch_verify
|
|
from cgcs_patch.patch_signing import sign_files
|
|
from datetime import datetime
|
|
import urllib.request
|
|
import os
|
|
import shutil
|
|
import subprocess
|
|
from subprocess import CalledProcessError
|
|
import tarfile
|
|
import tempfile
|
|
import time
|
|
from tsconfig.tsconfig import SW_VERSION
|
|
import xml.etree.ElementTree as ET
|
|
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():
|
|
parser = argparse.ArgumentParser(prog="sneaky_patch",
|
|
description="Creates a patch from a deb file")
|
|
parser.add_argument('debs',
|
|
nargs="+", # accepts a list
|
|
help='List of deb files to install to a patch or a yaml file')
|
|
parser.add_argument('--verbose',
|
|
action='store_true',
|
|
help="Display verbose output")
|
|
parser.add_argument('--debug',
|
|
action='store_true',
|
|
help="Display debugging output")
|
|
parser.add_argument('--sw-version',
|
|
default=SW_VERSION,
|
|
help="Version being patched. Usually same as what is running")
|
|
parser.add_argument('--patch-id',
|
|
default="SNEAKY",
|
|
help="Patch ID")
|
|
parser.add_argument('--summary',
|
|
default="SNEAKY Summary",
|
|
help="Summary for this patch.")
|
|
parser.add_argument('--description',
|
|
# this defaults to the list of deb files
|
|
help="Description for this patch")
|
|
parser.add_argument('--install-instructions',
|
|
default="SNEAKY Install Instructions",
|
|
help="Install instructions for this patch")
|
|
parser.add_argument('--warnings',
|
|
default="SNEAKY Warnings",
|
|
help="Warnings for this patch")
|
|
parser.add_argument('--sneaky-script',
|
|
help="A script, making this a no-reboot patch")
|
|
parser.add_argument('--req-patch',
|
|
help="ID of any required patch")
|
|
parser.add_argument('--pem-file',
|
|
help="An already downloaded patch signing key. Default is to download the dev key")
|
|
return parser
|
|
|
|
|
|
def print_duration(action, prev, verbose):
|
|
now = datetime.now()
|
|
duration = now - prev
|
|
if verbose:
|
|
print("%s took %.2f seconds" % (action, duration.total_seconds()))
|
|
return now
|
|
|
|
|
|
def print_debug(output, debug):
|
|
"""Print the output if we are in debug mode"""
|
|
if debug:
|
|
print("%s" % output)
|
|
|
|
|
|
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 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):
|
|
tag = ET.SubElement(parent, name)
|
|
tag.text = text
|
|
return tag
|
|
|
|
|
|
def gen_xml(file_name, base_commit_id, base_checksum, commit_id, commit_checksum, patch_info):
|
|
top = ET.Element("patch")
|
|
add_text_tag_to_xml(top, "id", patch_info.patch_id)
|
|
add_text_tag_to_xml(top, "sw_version", patch_info.sw_version)
|
|
add_text_tag_to_xml(top, "summary", patch_info.summary)
|
|
desc = patch_info.description
|
|
if desc is None:
|
|
desc = "Deb Files: %s" % " ".join(patch_info.debs)
|
|
add_text_tag_to_xml(top, "description", desc)
|
|
add_text_tag_to_xml(top, "install_instructions", patch_info.install_instructions)
|
|
add_text_tag_to_xml(top, "warnings", patch_info.warnings)
|
|
add_text_tag_to_xml(top, "status", 'DEV')
|
|
add_text_tag_to_xml(top, "unremovable", "N")
|
|
if patch_info.sneaky_script is None:
|
|
add_text_tag_to_xml(top, "reboot_required", "Y")
|
|
else:
|
|
add_text_tag_to_xml(top, "reboot_required", "N")
|
|
add_text_tag_to_xml(top,
|
|
"restart_script",
|
|
os.path.basename(patch_info.sneaky_script))
|
|
|
|
content = ET.SubElement(top, "contents")
|
|
ostree = ET.SubElement(content, "ostree")
|
|
|
|
# sneaky patches are just one commit
|
|
add_text_tag_to_xml(ostree, "number_of_commits", "1")
|
|
base_commit = ET.SubElement(ostree, "base")
|
|
add_text_tag_to_xml(base_commit, "commit", base_commit_id)
|
|
add_text_tag_to_xml(base_commit, "checksum", base_checksum)
|
|
|
|
commit = ET.SubElement(ostree, "commit1")
|
|
add_text_tag_to_xml(commit, "commit", commit_id)
|
|
add_text_tag_to_xml(commit, "checksum", commit_checksum)
|
|
|
|
req = ET.SubElement(top, 'requires')
|
|
if patch_info.req_patch is not None:
|
|
add_text_tag_to_xml(req, 'req_patch_id', patch_info.req_patch)
|
|
|
|
add_text_tag_to_xml(top, "semantics", "")
|
|
|
|
with open(file_name, "w") as outfile:
|
|
tree = ET.tostring(top)
|
|
outfile.write(minidom.parseString(tree).toprettyxml(indent=" "))
|
|
|
|
|
|
def sign_and_pack(patch_file, tar_dir, pem_file):
|
|
os.chdir(tar_dir)
|
|
filelist = ["metadata.tar", "software.tar"]
|
|
# Generate the local signature file
|
|
sig = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
|
|
for f in filelist:
|
|
sig ^= patch_functions.get_md5(f)
|
|
|
|
with open("signature", "w") as sigfile:
|
|
sigfile.write("%x" % sig)
|
|
|
|
detached_signature_file = "signature.v2"
|
|
private_key = patch_verify.read_RSA_key(open(pem_file, 'rb').read())
|
|
|
|
sign_files(filelist,
|
|
detached_signature_file,
|
|
private_key=private_key,
|
|
cert_type=None)
|
|
|
|
# Save files into .patch
|
|
files = [f for f in os.listdir('.') if os.path.isfile(f)]
|
|
with tarfile.open(patch_file, "w:gz") as tar:
|
|
for afile in files:
|
|
print(" -=- Adding to patch %s" % afile)
|
|
tar.add(afile)
|
|
print(" !!! Patch file is located at: %s" % patch_file)
|
|
|
|
|
|
def setup_patch(feed_dir, patch_bare_dir, debug):
|
|
# 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
|
|
# ostree --repo=/opt/backups/sneaky/patch_bare init --mode=bare
|
|
# ostree --repo=/opt/backups/sneaky/patch_bare pull-local \
|
|
# /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 \
|
|
# --skip-if-unchanged --branch=starlingx --subject=sneaky --timestamp=timestamp
|
|
# TODO(abailey): Determine if these can also be added
|
|
# --gpg-sign=gpg_id --gpg-homedir=gpg_homedir --parent=commit_id_base
|
|
#
|
|
# Phase 2: make an ostree archive from Phase 1
|
|
# - required because the binary patch contents are based on archive repo format
|
|
# ostree --repo=/opt/backups/sneaky/patch_archive init --mode=archive-z2
|
|
# ostree --repo=/opt/backups/sneaky/patch_archive pull-local --depth=1 \
|
|
# /opt/backups/sneaky/patch_bare
|
|
# ostree --repo=/opt/backups/sneaky/patch_archive summary -u
|
|
#
|
|
# Phase 3:
|
|
# rsync from feed_dir and patch_archive with the difference stored in delta_dir
|
|
|
|
prev = datetime.now()
|
|
patch_bare_dir = "%s/patch_bare" % tempdir # bare
|
|
|
|
# Phase 1: Step 3: Create a new commit. Needs a commit-id
|
|
timestamp = time.asctime()
|
|
subject = "Commit-id: SNEAKY-" + time.strftime("%Y%m%d%H%M%S", time.localtime())
|
|
try:
|
|
print(" - Commiting new change to bare patch repo ...")
|
|
output = subprocess.check_output(["ostree",
|
|
"--repo=%s" % patch_bare_dir,
|
|
"commit",
|
|
"--tree=dir=%s" % rootfs,
|
|
"--skip-if-unchanged",
|
|
"--branch=starlingx",
|
|
"'--timestamp=%s'" % timestamp,
|
|
"'--subject=%s'" % subject],
|
|
stderr=subprocess.STDOUT)
|
|
print_debug(output, debug)
|
|
except CalledProcessError as ex:
|
|
print("Failed ostree commit. %s" % ex.output)
|
|
return 1
|
|
prev = print_duration("commit creation", prev, verbose)
|
|
|
|
# Phase 2: Step 1: Make the archive repo containing the patch contents
|
|
try:
|
|
print(" - Creating archive patch repo ...")
|
|
output = subprocess.check_output(["ostree",
|
|
"--repo=%s" % patch_archive_dir,
|
|
"init",
|
|
"--mode=archive-z2"],
|
|
stderr=subprocess.STDOUT)
|
|
print_debug(output, debug)
|
|
except CalledProcessError as ex:
|
|
print("Failed ostree init archive. %s" % ex.output)
|
|
return 1
|
|
|
|
# Phase 2: Step 2: Pull history from temporary patch repo (depth=1)
|
|
try:
|
|
print(" - Populating archive patch repo ...")
|
|
output = subprocess.check_output(["ostree",
|
|
"--repo=%s" % patch_archive_dir,
|
|
"pull-local",
|
|
"--depth=1",
|
|
patch_bare_dir],
|
|
stderr=subprocess.STDOUT)
|
|
print_debug(output, debug)
|
|
except CalledProcessError as ex:
|
|
print("Failed ostree archive pull-local. %s" % ex.output)
|
|
return 1
|
|
|
|
# Phase 2: Step 3: Update the summary file in the archive repo
|
|
try:
|
|
print(" - Updating summary for archive patch repo ...")
|
|
output = subprocess.check_output(["ostree",
|
|
"--repo=%s" % patch_archive_dir,
|
|
"summary",
|
|
"-u"],
|
|
stderr=subprocess.STDOUT)
|
|
print_debug(output, debug)
|
|
except CalledProcessError as ex:
|
|
print("Failed ostree summary update. %s" % ex.output)
|
|
return 1
|
|
prev = print_duration("creating archive", prev, verbose)
|
|
|
|
# this is the difference between the feed_dir and the archive
|
|
# Note that the feed_dir will be the last patch
|
|
try:
|
|
# automatically creates "delta_dir"
|
|
print(" - rsyncing to determine patch delta...")
|
|
os.chdir(tempdir)
|
|
|
|
# Getting 2 GB delta folder instead of 2MB
|
|
# Removed -p which was specified in make_patch.py to fix the issue
|
|
options = "-rcgo" # recursive, checksum, perms, group, owner
|
|
|
|
output = subprocess.check_output(["rsync",
|
|
options,
|
|
"--exclude=/.lock",
|
|
"--exclude=/config",
|
|
"--no-owner",
|
|
"--compare-dest", feed_dir, # compare received files relative to feed_dir
|
|
patch_archive_dir + "/", # SRC
|
|
"delta_dir" + "/"], # DEST
|
|
stderr=subprocess.STDOUT)
|
|
print_debug(output, debug)
|
|
except CalledProcessError as ex:
|
|
print("Failed rsync. %s" % ex.output)
|
|
return 1
|
|
prev = print_duration("rsync", prev, verbose)
|
|
|
|
# base_commit comes from feed
|
|
# commit comes from archive
|
|
# checksum values do not appear to be used by patching
|
|
base_commit_id = ostree_utils.get_ostree_latest_commit("starlingx", feed_dir)
|
|
base_checksum = "UNUSED"
|
|
commit_id = ostree_utils.get_ostree_latest_commit("starlingx", patch_archive_dir)
|
|
commit_checksum = "UNUSED"
|
|
|
|
# Writing the final patch file
|
|
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_file = "%s/dev-private-key.pem" % tempdir
|
|
if patch_info.pem_file is None:
|
|
urllib.request.urlretrieve(pem_url, pem_file)
|
|
else:
|
|
# use the already downloaded pem_file passed as an argument
|
|
pem_file = patch_info.pem_file
|
|
|
|
with tempfile.TemporaryDirectory(prefix="sneaky_patch", dir="/tmp") as sneaky_tar:
|
|
print(" - Generating software.tar...") # Make tarball of delta_dir
|
|
with tarfile.open("%s/software.tar" % sneaky_tar, "w") as tar:
|
|
tar.add(os.path.join(tempdir, "delta_dir"), arcname="")
|
|
|
|
# now we can change into the tar location and do the rest of the patch generation
|
|
os.chdir(sneaky_tar)
|
|
|
|
# generate the metadata.xml
|
|
print(" - Generating metadata.tar...")
|
|
gen_xml("metadata.xml",
|
|
base_commit_id, base_checksum,
|
|
commit_id, commit_checksum,
|
|
patch_info)
|
|
with tarfile.open("%s/metadata.tar" % sneaky_tar, "w") as tar:
|
|
tar.add("metadata.xml")
|
|
os.remove("metadata.xml")
|
|
|
|
# Copy the restart script to the temporary tar directory
|
|
if patch_info.sneaky_script is not None:
|
|
shutil.copy(patch_info.sneaky_script, sneaky_tar)
|
|
|
|
# patch_functions.write_patch looks like it skips restart scripts
|
|
# using the logic from make_patch.py sign_and_pack
|
|
sign_and_pack(final_patch_file, sneaky_tar, pem_file)
|
|
|
|
prev = print_duration("Writing patch", prev, verbose)
|
|
return 0
|
|
|
|
|
|
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.
|
|
real_root = os.open("/", os.O_RDONLY)
|
|
in_jail = False
|
|
|
|
prev = datetime.now()
|
|
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
|
|
with tempfile.TemporaryDirectory(prefix="sneaky", dir="/opt/backups") as sneaky_temp:
|
|
|
|
# Checkout the ostree feed
|
|
rootfs = "%s/rootfs" % sneaky_temp
|
|
try:
|
|
print(" - Checking out ostree...")
|
|
output = subprocess.check_output(["ostree",
|
|
"-v",
|
|
"--repo=%s" % repo_src,
|
|
"checkout",
|
|
"starlingx",
|
|
rootfs],
|
|
stderr=subprocess.STDOUT)
|
|
print_debug(output, debug)
|
|
except CalledProcessError as ex:
|
|
print("Failed ostree checkout. %s" % ex.output)
|
|
return 1
|
|
prev = print_duration("Ostree checkout", prev, verbose)
|
|
|
|
rootfs_tmp = "%s/var/tmp" % rootfs
|
|
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:
|
|
print(" - Setting up symlinks...")
|
|
output = subprocess.check_output(["ln", "-sfn", "usr/etc", "etc"],
|
|
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
|
|
# 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")
|
|
|
|
# leave chroot jail
|
|
os.fchdir(real_root)
|
|
os.chroot(".")
|
|
in_jail = False
|
|
|
|
# make the commit, etc..
|
|
make_patch(patch_info, sneaky_temp, rootfs, feed_dir, patch_archive_dir, debug, verbose)
|
|
# for the next patch, the feed will be the archive_dir of the last patch
|
|
feed_dir = patch_archive_dir
|
|
prev = print_duration("Committing changes", prev, verbose)
|
|
|
|
# escape back from chroot jail
|
|
if in_jail:
|
|
# Should never get here...
|
|
os.fchdir(real_root)
|
|
os.chroot(".")
|
|
# now we can safely close fd for real_root
|
|
os.close(real_root)
|
|
|
|
print_duration("Entire activity", start_time, verbose)
|
|
return 1
|
|
|
|
|
|
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
|
|
# that argparse does not handle
|
|
unique_scripts = set()
|
|
for patch_info in patch_info_list:
|
|
# make sure all deb files exist
|
|
for deb in patch_info.debs:
|
|
validate_file(deb)
|
|
# 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():
|
|
parser = setup_argparse()
|
|
args = parser.parse_args()
|
|
if os.geteuid() != 0:
|
|
print("MUST BE RUN AS ROOT (or sudo)")
|
|
return 1
|
|
# If the args.debs is a yaml we parse that
|
|
# 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__':
|
|
exit(main())
|