Chris Friesen 5435387623 Add support for symlinks instead of bindmounts for version control
We have been using bind mounts to select K8s versions, but they are not
well supported by Puppet and suffer from fragility since you cannot
remove a bind mount while an executable is still running from it.  They
also need to be re-created when creating an OSTree hotfix or when
applying software patches.

Symlinks suffer from no such issues, they just need to be created in
a filesystem that is not managed by OSTree.

Accordingly, make the current bindmount-related code conditional on
the bindmount directory actually being present.  That way the code
will not complain when we switch to using symlinks.

Story: 2011047
Task: 49917

TEST PLAN:

PASS: Run the modified code snippet standalone on system where
/usr/local/kubernetes/current/ exists, ensure it attempts to
run the two mount commands.

PASS: Run the modified code snippet standalone on system where
/usr/local/kubernetes/current/ does not exist, ensure it does not
attempt to run the two mount commands.

Change-Id: I1dfea974ae9532cf316bb1fac701ae93f5507681
2024-04-22 12:20:46 -06:00

385 lines
15 KiB
Python

"""
Copyright (c) 2022 Wind River Systems, Inc.
SPDX-License-Identifier: Apache-2.0
"""
import filecmp
import glob
import logging
import os
import sh
import shutil
import subprocess
import time
from cgcs_patch import constants
from cgcs_patch.exceptions import OSTreeCommandFail
from tsconfig.tsconfig import subfunctions
LOG = logging.getLogger('main_logger')
def get_ostree_latest_commit(ostree_ref, repo_path):
"""
Query ostree using ostree log <ref> --repo=<path>
:param ostree_ref: the ostree ref.
example: starlingx
:param repo_path: the path to the ostree repo:
example: /var/www/pages/feed/rel-22.06/ostree_repo
:return: The most recent commit of the repo
"""
# Sample command and output that is parsed to get the commit
#
# Command: ostree log starlingx --repo=/var/www/pages/feed/rel-22.02/ostree_repo
#
# Output:
#
# commit 478bc21c1702b9b667b5a75fac62a3ef9203cc1767cbe95e89dface6dc7f205e
# ContentChecksum: 61fc5bb4398d73027595a4d839daeb404200d0899f6e7cdb24bb8fb6549912ba
# Date: 2022-04-28 18:58:57 +0000
#
# Commit-id: starlingx-intel-x86-64-20220428185802
#
# commit ad7057a94a1d06e38eaedee2ce3fe56826ae817497469bce5d5ac05bc506aaa7
# ContentChecksum: dc42a42427a4f9e4de1210327c12b12ea3ad6a5d232497a903cc6478ca381e8b
# Date: 2022-04-28 18:05:43 +0000
#
# Commit-id: starlingx-intel-x86-64-20220428180512
cmd = "ostree log %s --repo=%s" % (ostree_ref, repo_path)
try:
output = subprocess.run(cmd, shell=True, check=True, capture_output=True)
except subprocess.CalledProcessError as e:
info_msg = "OSTree log Error: return code: %s , Output: %s" \
% (e.returncode, e.stderr.decode("utf-8"))
LOG.info(info_msg)
msg = "Failed to fetch ostree log for %s." % repo_path
raise OSTreeCommandFail(msg)
# Store the output of the above command in a string
output_string = output.stdout.decode('utf-8')
# Parse the string to get the latest commit for the ostree
split_output_string = output_string.split()
latest_commit = split_output_string[1]
return latest_commit
def get_feed_latest_commit(patch_sw_version):
"""
Query ostree feed using ostree log <ref> --repo=<path>
:param patch_sw_version: software version for the feed
example: 22.06
:return: The latest commit for the feed repo
"""
repo_path = "%s/rel-%s/ostree_repo" % (constants.FEED_OSTREE_BASE_DIR,
patch_sw_version)
return get_ostree_latest_commit(constants.OSTREE_REF, repo_path)
def get_sysroot_latest_commit():
"""
Query ostree sysroot to determine the currently active commit
:return: The latest commit for sysroot repo
"""
return get_ostree_latest_commit(constants.OSTREE_REF, constants.SYSROOT_OSTREE)
def get_latest_deployment_commit():
"""
Get the active deployment commit ID
:return: The commit ID associated with the active commit
"""
# Sample command and output that is parsed to get the active commit
# associated with the deployment
#
# Command: ostree admin status
#
# Output:
#
# debian 0658a62854647b89caf5c0e9ed6ff62a6c98363ada13701d0395991569248d7e.0 (pending)
# origin refspec: starlingx
# * debian a5d8f8ca9bbafa85161083e9ca2259ff21e5392b7595a67f3bc7e7ab8cb583d9.0
# Unlocked: hotfix
# origin refspec: starlingx
cmd = "ostree admin status"
try:
output = subprocess.run(cmd, shell=True, check=True, capture_output=True)
except subprocess.CalledProcessError as e:
msg = "Failed to fetch ostree admin status."
info_msg = "OSTree Admin Status Error: return code: %s , Output: %s" \
% (e.returncode, e.stderr.decode("utf-8"))
LOG.info(info_msg)
raise OSTreeCommandFail(msg)
# Store the output of the above command in a string
output_string = output.stdout.decode('utf-8')
# Parse the string to get the active commit on this deployment
# Trim everything before * as * represents the active deployment commit
trimmed_output_string = output_string[output_string.index("*"):]
split_output_string = trimmed_output_string.split()
active_deployment_commit = split_output_string[2]
return active_deployment_commit
def update_repo_summary_file(repo_path):
"""
Updates the summary file for the specified ostree repo
:param repo_path: the path to the ostree repo:
example: /var/www/pages/feed/rel-22.06/ostree_repo
"""
cmd = "ostree summary --update --repo=%s" % repo_path
try:
subprocess.run(cmd, shell=True, check=True, capture_output=True)
except subprocess.CalledProcessError as e:
msg = "Failed to update summary file for ostree repo %s." % (repo_path)
info_msg = "OSTree Summary Update Error: return code: %s , Output: %s" \
% (e.returncode, e.stderr.decode("utf-8"))
LOG.info(info_msg)
raise OSTreeCommandFail(msg)
def reset_ostree_repo_head(commit, repo_path):
"""
Resets the ostree repo HEAD to the commit that is specified
:param commit: an existing commit on the ostree repo which we need the HEAD to point to
example: 478bc21c1702b9b667b5a75fac62a3ef9203cc1767cbe95e89dface6dc7f205e
:param repo_path: the path to the ostree repo:
example: /var/www/pages/feed/rel-22.06/ostree_repo
"""
cmd = "ostree reset %s %s --repo=%s" % (constants.OSTREE_REF, commit, repo_path)
try:
subprocess.run(cmd, shell=True, check=True, capture_output=True)
except subprocess.CalledProcessError as e:
msg = "Failed to reset head of ostree repo: %s to commit: %s" % (repo_path, commit)
info_msg = "OSTree Reset Error: return code: %s , Output: %s" \
% (e.returncode, e.stderr.decode("utf-8"))
LOG.info(info_msg)
raise OSTreeCommandFail(msg)
def pull_ostree_from_remote():
"""
Pull from remote ostree to sysroot ostree
"""
cmd = "ostree pull %s --depth=-1" % constants.OSTREE_REMOTE
try:
subprocess.run(cmd, shell=True, check=True, capture_output=True)
except subprocess.CalledProcessError as e:
msg = "Failed to pull from %s remote into sysroot ostree" % constants.OSTREE_REMOTE
info_msg = "OSTree Pull Error: return code: %s , Output: %s" \
% (e.returncode, e.stderr.decode("utf-8"))
LOG.info(info_msg)
raise OSTreeCommandFail(msg)
def delete_ostree_repo_commit(commit, repo_path):
"""
Delete the specified commit from the ostree repo
:param commit: an existing commit on the ostree repo which we need to delete
example: 478bc21c1702b9b667b5a75fac62a3ef9203cc1767cbe95e89dface6dc7f205e
:param repo_path: the path to the ostree repo:
example: /var/www/pages/feed/rel-22.06/ostree_repo
"""
cmd = "ostree prune --delete-commit %s --repo=%s" % (commit, repo_path)
try:
subprocess.run(cmd, shell=True, check=True, capture_output=True)
except subprocess.CalledProcessError as e:
msg = "Failed to delete commit %s from ostree repo %s" % (commit, repo_path)
info_msg = "OSTree Delete Commit Error: return code: %s , Output: %s" \
% (e.returncode, e.stderr.decode("utf-8"))
LOG.info(info_msg)
raise OSTreeCommandFail(msg)
def create_deployment():
"""
Create a new deployment while retaining the previous ones
"""
cmd = "ostree admin deploy %s --no-prune --retain" % constants.OSTREE_REF
try:
subprocess.run(cmd, shell=True, check=True, capture_output=True)
except subprocess.CalledProcessError as e:
msg = "Failed to create an ostree deployment for sysroot ref %s." % constants.OSTREE_REF
info_msg = "OSTree Deployment Error: return code: %s , Output: %s" \
% (e.returncode, e.stderr.decode("utf-8"))
LOG.info(info_msg)
raise OSTreeCommandFail(msg)
def update_deployment_kernel_env():
"""
Update the /boot/1/kernel.env after creating a deployment
"""
try:
# copy /boot/2/kernel.env to /boot/1/kernel.env
# this is to preserve args (ie: apparmor)
# if the files are identical, do nothing
if not filecmp.cmp("/boot/1/kernel.env",
"/boot/2/kernel.env",
shallow=False):
shutil.copy2("/boot/2/kernel.env",
"/boot/1/kernel.env")
# Determine the appropriate kernel for this env
desired_kernel = None
for kernel in glob.glob(os.path.join("/boot/1", "vmlinuz*-amd64")):
kernel_entry = os.path.basename(kernel)
# If we are running in lowlatency mode, we want the rt-amd64 kernel
if 'lowlatency' in subfunctions and 'rt-amd64' in kernel_entry:
desired_kernel = kernel_entry
break
# If we are not running lowlatency we want the entry that does NOT contain rt-amd64
if 'lowlatency' not in subfunctions and 'rt-amd64' not in kernel_entry:
desired_kernel = kernel_entry
break
if desired_kernel is None: # This should never happen
LOG.warning("Unable to find a valid kernel under /boot/1")
else:
# Explicitly update /boot/1/kernel.env using the
# /usr/local/bin/puppet-update-grub-env.py utility
LOG.info("Updating /boot/1/kernel.env to:%s", desired_kernel)
cmd = "python /usr/local/bin/puppet-update-grub-env.py --set-kernel %s" % desired_kernel
subprocess.run(cmd, shell=True, check=True, capture_output=True)
except subprocess.CalledProcessError as e:
msg = "Failed to run puppet-update-grub-env.py"
info_msg = "OSTree Post-Deployment Error: return code: %s , Output: %s" \
% (e.returncode, e.stderr.decode("utf-8"))
LOG.info(info_msg)
raise OSTreeCommandFail(msg)
except Exception as e:
msg = "Failed to manually update /boot/1/kernel.env. Err=%s" % str(e)
LOG.info(msg)
raise OSTreeCommandFail(msg)
def fetch_pending_deployment():
"""
Fetch the deployment ID of the pending deployment
:return: The deployment ID of the pending deployment
"""
cmd = "ostree admin status | grep pending |awk '{printf $2}'"
try:
output = subprocess.run(cmd, shell=True, check=True, capture_output=True)
except subprocess.CalledProcessError as e:
msg = "Failed to fetch ostree admin status."
info_msg = "OSTree Admin Status Error: return code: %s , Output: %s" \
% (e.returncode, e.stderr.decode("utf-8"))
LOG.info(info_msg)
raise OSTreeCommandFail(msg)
# Store the output of the above command in a string
pending_deployment = output.stdout.decode('utf-8')
return pending_deployment
def mount_new_deployment(deployment_dir):
"""
Unmount /usr and /etc from the file system and remount it to directory
<depoyment_dir>/usr and <depoyment_dir>/etc respectively
:param deployment_dir: a path on the filesystem which points to the pending
deployment
example: /ostree/deploy/debian/deploy/<deployment_id>
"""
try:
new_usr_mount_dir = "%s/usr" % (deployment_dir)
new_etc_mount_dir = "%s/etc" % (deployment_dir)
sh.mount("--bind", "-o", "ro,noatime", new_usr_mount_dir, "/usr")
sh.mount("--bind", "-o", "rw,noatime", new_etc_mount_dir, "/etc")
except sh.ErrorReturnCode:
LOG.warning("Mount failed. Retrying to mount /usr and /etc again after 5 secs.")
time.sleep(5)
try:
sh.mount("--bind", "-o", "ro,noatime", new_usr_mount_dir, "/usr")
sh.mount("--bind", "-o", "rw,noatime", new_etc_mount_dir, "/etc")
except sh.ErrorReturnCode as e:
msg = "Failed to re-mount /usr and /etc."
info_msg = "OSTree Deployment Mount Error: Output: %s" \
% (e.stderr.decode("utf-8"))
LOG.warning(info_msg)
raise OSTreeCommandFail(msg)
finally:
# Handle the switch from bind mounts to symlinks for K8s versions.
# Can be removed once the switch is complete.
if os.path.isdir('/usr/local/kubernetes/current'):
try:
sh.mount("/usr/local/kubernetes/current/stage1")
sh.mount("/usr/local/kubernetes/current/stage2")
except sh.ErrorReturnCode:
msg = "Failed to mount kubernetes. Please manually run these commands:\n" \
"sudo mount /usr/local/kubernetes/current/stage1\n" \
"sudo mount /usr/local/kubernetes/current/stage2\n"
LOG.info(msg)
def delete_older_deployments():
"""
Delete all older deployments after a reboot to save space
"""
# Sample command and output that is parsed to get the list of
# deployment IDs
#
# Command: ostree admin status | grep debian
#
# Output:
#
# * debian 3334dc80691a38c0ba6c519ec4b4b449f8420e98ac4d8bded3436ade56bb229d.2
# debian 3334dc80691a38c0ba6c519ec4b4b449f8420e98ac4d8bded3436ade56bb229d.1 (rollback)
# debian 3334dc80691a38c0ba6c519ec4b4b449f8420e98ac4d8bded3436ade56bb229d.0
LOG.info("Inside delete_older_deployments of ostree_utils")
cmd = "ostree admin status | grep debian"
try:
output = subprocess.run(cmd, shell=True, check=True, capture_output=True)
except subprocess.CalledProcessError as e:
msg = "Failed to fetch ostree admin status."
info_msg = "OSTree Admin Status Error: return code: %s , Output: %s" \
% (e.returncode, e.stderr.decode("utf-8"))
LOG.info(info_msg)
raise OSTreeCommandFail(msg)
# Store the output of the above command in a string
output_string = output.stdout.decode('utf-8')
# Parse the string to get the latest commit for the ostree
split_output_string = output_string.split()
deployment_id_list = []
for index, deployment_id in enumerate(split_output_string):
if deployment_id == "debian":
deployment_id_list.append(split_output_string[index + 1])
# After a reboot, the deployment ID at the 0th index of the list
# is always the active deployment and the deployment ID at the
# 1st index of the list is always the fallback deployment.
# We want to delete all deployments except the two mentioned above.
# This means we will undeploy all deployments starting from the
# 2nd index of deployment_id_list
for index in reversed(range(2, len(deployment_id_list))):
try:
cmd = "ostree admin undeploy %s" % index
output = subprocess.run(cmd, shell=True, check=True, capture_output=True)
info_log = "Deleted ostree deployment %s" % deployment_id_list[index]
LOG.info(info_log)
except subprocess.CalledProcessError as e:
msg = "Failed to undeploy ostree deployment %s." % deployment_id_list[index]
info_msg = "OSTree Undeploy Error: return code: %s , Output: %s" \
% (e.returncode, e.stderr.decode("utf-8"))
LOG.info(info_msg)
raise OSTreeCommandFail(msg)