Skip to content
Closed
124 changes: 124 additions & 0 deletions providers/base/bin/remodel_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
#!/usr/bin/env python3
# Copyright 2025 Canonical Ltd.
# All rights reserved.
#
# Written by:
# Authors: Philip Meulengracht <[email protected]>

import argparse
import os
import platform
import subprocess
from urllib.request import urlretrieve


def get_platform():
plt = platform.platform()
if "raspi-aarch64" in plt:
return "pi-arm64"
elif "raspi" in plt:
return "pi-armhf"
elif "x86_64" in plt:
return "amd64"
raise SystemExit(f"platform not supported for remodeling test: {plt}")


# Currently images used in certifications are sourced from cdimage,
# those images builds using the models supplied in canonical/models.
# Make sure we use the same models that come from the same authority,
# otherwise remodeling will fail.
def download_model(uc_ver):
base_uri = "https://raw.githubusercontent.com/canonical/models/"
branch = "refs/heads/master/"
model = f"ubuntu-core-{uc_ver}-{get_platform()}-dangerous.model"
print(f"downloading model for remodeling: {base_uri + branch + model}")
path, _ = urlretrieve(base_uri + branch + model)
return path


# downloads a snap to the tmp folder
def download_snap(name, out, channel):
dir = os.getcwd()
os.chdir("/tmp")
subprocess.run(
[
"snap",
"download",
name,
f"--channel=latest/{channel}",
f"--basename={out}",
]
)
os.chdir(dir)


def download_snaps(uc_ver):
# use stable for remodel, we are not testing the snaps we are
# remodeling to, but rather the process works.
channel = "stable"
download_snap(f"core{uc_ver}", "base", channel)
if "pi" in get_platform():
download_snap("pi", "gadget", f"--channel={uc_ver}/{channel}")
download_snap("pi-kernel", "kernel", f"--channel={uc_ver}/{channel}")
else:
download_snap("pc", "gadget", f"--channel={uc_ver}/{channel}")
download_snap("pc-kernel", "kernel", f"--channel={uc_ver}/{channel}")


def main():
"""Run remodel of an Ubuntu Core host."""

parser = argparse.ArgumentParser()
parser.add_argument(
"target",
help="which verison of ubuntu-core that should be remodeled to",
choices=["22", "24"],
)

# resolve the snaps for the remodel if offline has been requested
# (currently offline was used for testing in certain scenarios during
# test development) - for normal testing offline should not be needed
parser.add_argument(
"--offline",
help="whether the remodel should be offline",
action="store_true",
)
args = parser.parse_args()

# resolve the model for the current platform
model_path = download_model(args.target)

if args.offline:
download_snaps(args.target)

# instantiate the offline remodel
print("initiating offline device remodel")
subprocess.run(
[
"sudo",
"snap",
"remodel",
"--offline",
"--snap",
"/tmp/base.snap",
"--assertion",
"/tmp/base.assert",
"--snap",
"/tmp/gadget.snap",
"--assertion",
"/tmp/gadget.assert",
"--snap",
"/tmp/kernel.snap",
"--assertion",
"/tmp/kernel.assert",
model_path,
]
)
else:
# instantiate the remodel
print("initiating device remodel")
subprocess.run(["sudo", "snap", "remodel", model_path])


if __name__ == "__main__":
exit(main())
122 changes: 122 additions & 0 deletions providers/base/units/ubuntucore/jobs.pxu
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,128 @@ _verification:
plugin: manual
category_id: ubuntucore

id: ubuntucore/remodels
_summary: Create a list of remodel tests
_description:
Remodeling is supported for the below versions, and variants of the remodeling
test must be created for each
category_id: ubuntucore
plugin: resource
command:
echo source: 22
echo destination: 24
echo
echo source: 20
echo destination: 22

id: ubuntucore/remodel-{source}-to-{destination}
category_id: ubuntucore
_summary: Remodel Ubuntu Core {source} to {destination}
_purpose:
Verify the system can remodel from ubuntu core {source} to {destination} under normal
circumstances. The requirement is that the official models from canonical
are used for the running Ubuntu Core host. Multiple reboots will happen
as a part of this test.
unit: template
template-id: ubuntucore/remodel
template-resource: ubuntucore/remodels
requires: os.distributor_id == 'Ubuntu Core'
template-unit: job
plugin: shell
user: root
estimated_duration: 60.0
command:
set -e
snap model
snap model | grep "ubuntu-core-{source}"
remodel_test.py {destination}

id: ubuntucore/remodel-reboot-{source}-first
category_id: ubuntucore
_summary: First remodel checkpoint to reboot system during remodel.
_purpose:
A system reboot will be requested as a part of requesting a remodel, this serves
to be a checkpoint for that, and waits for the reboot to be requested.
unit: template
template-id: ubuntucore/remodel-reboot-first
template-resource: ubuntucore/remodels
requires: os.distributor_id == 'Ubuntu Core'
template-unit: job
plugin: shell
user: root
flags: noreturn
estimated_duration: 30.0
command:
set -e
max_attempts=30
attempt_num=1
success=false
while [ $success = false ] && [ $attempt_num -le $max_attempts ]; do
journalctl -b -u snapd | grep "Waiting for system reboot"
if [ $? -eq 0 ]; then
success=true
else
echo "Attemp $attempt_num: waiting for reboot request"
attempt_num=$(( attempt_num + 1 ))
sleep 1
fi
done
reboot

id: ubuntucore/remodel-reboot-{source}-second
category_id: ubuntucore
_summary: Second remodel checkpoint to reboot system during remodel.
_purpose:
At this point, the remodel will start, the first reboot was to create a
recovery system and the next will be to start the remodel itself.
This will take a while and do several reboots but not be observed by
checkbox
unit: template
template-id: ubuntucore/remodel-reboot-second
template-resource: ubuntucore/remodels
requires: os.distributor_id == 'Ubuntu Core'
template-unit: job
plugin: shell
user: root
flags: noreturn
estimated_duration: 300.0
command:
set -e
max_attempts=60
attempt_num=1
success=false
while [ $success = false ] && [ $attempt_num -le $max_attempts ]; do
journalctl -b -u snapd | grep "Waiting for system reboot"
if [ $? -eq 0 ]; then
success=true
else
echo "Attemp $attempt_num: waiting for reboot request"
attempt_num=$(( attempt_num + 1 ))
sleep 5
fi
done
reboot

id: ubuntucore/remodel-{source}-to-{destination}-verify
category_id: ubuntucore
_summary: Verify remodel of Ubuntu Core {source} to {destination}
unit: template
template-id: ubuntucore/remodel-verify
template-resource: ubuntucore/remodels
requires: os.distributor_id == 'Ubuntu Core'
template-unit: job
plugin: shell
user: root
estimated_duration: 0.1
command:
set -e
cat /proc/cmdline
snap model
snap changes
systemctl is-active ssh
cat /proc/cmdline | grep snapd_recovery_mode=run
snap model | grep "ubuntu-core-{destination}"

unit: template
template-resource: lsb
template-filter: lsb.distributor_id == 'Ubuntu Core'
Expand Down
26 changes: 26 additions & 0 deletions providers/base/units/ubuntucore/test-plan.pxu
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,29 @@ include:
ubuntucore/os-recovery-mode
ubuntucore/os-fail-boot-(?!with-refresh-control).*
ubuntucore/sshd

id: ubuntucore-20-team
unit: test plan
_name: Automated tests from the Ubuntu Core Team for Ubuntu Core 20
_description: Runs automated OS feature tests for Ubuntu Core 20 devices.
bootstrap_include:
os
ubuntucore/remodels
include:
ubuntucore/remodel-20-to-22
ubuntucore/remodel-reboot-20-first
ubuntucore/remodel-reboot-20-second
ubuntucore/remodel-20-to-22-verify

id: ubuntucore-22-team
unit: test plan
_name: Automated tests from the Ubuntu Core Team for Ubuntu Core 20
_description: Runs automated OS feature tests for Ubuntu Core 20 devices.
bootstrap_include:
os
ubuntucore/remodels
include:
ubuntucore/remodel-22-to-24
ubuntucore/remodel-reboot-22-first
ubuntucore/remodel-reboot-22-second
ubuntucore/remodel-22-to-24-verify
Loading