Files
infix/test/case/containers/upgrade/test.py
T
Joachim Wiberg 1bbd80d8c7 test: drop leading ietf/infix prefix from directories
Let's drop the leading IETF or Infix prefixes from tests.  Initially the
idea was to mimnic the YANG models, but it's difficult to navigate and
does not provide any real benefit to developers or end-users.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2025-11-20 20:23:23 +01:00

185 lines
7.4 KiB
Python
Executable File

#!/usr/bin/env python3
"""Container Upgrade
Verify container upgrade functionality by testing the optimization
that skips recreation when the container configuration hasn't changed.
This test uses two versions of curios-httpd (24.05.0 and 24.11.0) served
over HTTP with :latest tag. The test verifies that:
1. Container starts successfully from the :latest image
2. When the :latest tag points to a new version, upgrade is triggered
3. The container runs the new version after upgrade
"""
import os
import time
import infamy
import infamy.file_server as srv
from infamy.util import until
SRVPORT = 8008
SRVDIR = "/srv"
with infamy.Test() as test:
NAME = "web"
with test.step("Set up topology and attach to target DUT"):
env = infamy.Env()
target = env.attach("target", "mgmt")
tgtssh = env.attach("target", "mgmt", "ssh")
if not target.has_model("infix-containers"):
test.skip()
_, hport = env.ltop.xlate("host", "data")
_, tport = env.ltop.xlate("target", "data")
with test.step("Detect target architecture"):
# Query operational datastore for machine architecture
system_state = target.get_data("/ietf-system:system-state")
arch = system_state["system-state"]["platform"]["machine"]
# Map kernel arch to our image naming
arch_map = {
"amd64": "amd64",
"x86_64": "amd64",
"arm64": "arm64",
"aarch64": "arm64",
"armv7l": "arm64",
}
image_arch = arch_map.get(arch, "amd64")
print(f"Detected architecture: {arch} -> using {image_arch} images")
with test.step("Set up isolated network and file server"):
netns = infamy.IsolatedMacVlan(hport).start()
netns.addip("192.168.0.1")
target.put_config_dicts({
"ietf-interfaces": {
"interfaces": {
"interface": [
{
"name": tport,
"ipv4": {
"address": [
{
"ip": "192.168.0.2",
"prefix-length": 24
}
]
}
}
]
}
}
})
netns.must_reach("192.168.0.2")
with srv.FileServer(netns, "192.168.0.1", SRVPORT, SRVDIR):
with test.step("Create symlink for curios-httpd:latest -> 24.05.0"):
# Create symlink in the file server directory
old_img = f"{SRVDIR}/curios-httpd-24.05.0-{image_arch}.tar.gz"
new_img = f"{SRVDIR}/curios-httpd-24.11.0-{image_arch}.tar.gz"
latest_link = f"{SRVDIR}/curios-httpd-latest.tar.gz"
# Remove any existing symlink
if os.path.exists(latest_link):
os.unlink(latest_link)
# Point to old version
os.symlink(old_img, latest_link)
print(f"Created symlink: {latest_link} -> {old_img}")
with test.step("Create container 'web' from curios-httpd:latest (24.05.0)"):
target.put_config_dict("infix-containers", {
"containers": {
"container": [
{
"name": f"{NAME}",
"image": f"http://192.168.0.1:{SRVPORT}/curios-httpd-latest.tar.gz",
"command": "/usr/sbin/httpd -f -v -p 91",
"network": {
"host": True
}
}
]
}
})
with test.step("Verify container 'web' has started with version 24.05.0"):
c = infamy.Container(target)
until(lambda: c.running(NAME), attempts=60)
# Get initial operational data to capture container ID and image ID
containers_data = target.get_data("/infix-containers:containers")
container = containers_data["containers"]["container"][NAME]
initial_container_id = container["id"]
initial_image_id = container["image-id"]
print(f"Container started with container-id: {initial_container_id[:12]}...")
print(f" and image-id: {initial_image_id[:12]}...")
# Get baseline disk usage for /var/lib/containers
result = tgtssh.runsh("doas du -s /var/lib/containers")
initial_disk_usage = int(result.stdout.split()[0])
print(f"Disk usage after initial creation: {initial_disk_usage} KiB")
with test.step("Update symlink to point to curios-httpd:24.11.0"):
# Remove old symlink and point to new version
os.unlink(latest_link)
os.symlink(new_img, latest_link)
print(f"Updated symlink: {latest_link} -> {new_img}")
with test.step("Trigger container upgrade by calling upgrade action"):
c = infamy.Container(target)
c.action(NAME, "upgrade")
with test.step("Wait for container 'web' to complete uprgade"):
time.sleep(3)
until(lambda: c.running(NAME), attempts=30)
with test.step("Verify container 'web' is running new version 24.11.0"):
c = infamy.Container(target)
# Wait for upgrade to complete and container to restart
until(lambda: c.running(NAME), attempts=60)
# Get operational data after upgrade
containers_data = target.get_data("/infix-containers:containers")
container = containers_data["containers"]["container"][NAME]
new_container_id = container["id"]
new_image_id = container["image-id"]
print(f"After upgrade container-id: {new_container_id[:12]}...")
print(f" image-id: {new_image_id[:12]}...")
# Verify that both IDs have changed
if new_container_id == initial_container_id:
test.fail("Container ID did not change after upgrade!")
if new_image_id == initial_image_id:
test.fail("Image ID did not change after upgrade!")
print("✓ Both container ID and image ID changed after upgrade")
with test.step("Verify old image was pruned and disk usage is reasonable"):
# We expect minimal growth — the new image replaces old
# image, and they are each around 500-600 KiB. We allow for
# some overhead, but should be < 200 KiB. If the old image
# is not pruned properly, we'll see ~600 KiB extra growth.
MAX_ACCEPTABLE_GROWTH = 1000
# Get disk usage after upgrade
result = tgtssh.runsh("doas du -s /var/lib/containers")
final_disk_usage = int(result.stdout.split()[0])
print(f"Disk usage after upgrade: {final_disk_usage} KiB")
# Calculate the difference
disk_growth = final_disk_usage - initial_disk_usage
print(f"Disk usage growth: {disk_growth} KiB")
if disk_growth > MAX_ACCEPTABLE_GROWTH:
test.fail(f"Disk usage grew by {disk_growth} KiB (expected < {MAX_ACCEPTABLE_GROWTH} KiB). "
f"Old image may not have been pruned!")
print(f"✓ Disk usage growth is acceptable ({disk_growth} KiB < {MAX_ACCEPTABLE_GROWTH} KiB)")
test.succeed()