2021-11-04 23:20:41 +00:00
|
|
|
#!/usr/bin/env python3
|
|
|
|
|
|
|
|
'''kvm-test - boot a kvm with a test iso, possibly building that test iso first
|
|
|
|
|
2021-12-01 23:48:12 +00:00
|
|
|
kvm-test -q --install -o --boot
|
2021-11-04 23:20:41 +00:00
|
|
|
slimy build, install, overwrite existing image if it exists,
|
|
|
|
and boot the result after install
|
|
|
|
|
|
|
|
See kvm-test -h for options and more examples.
|
|
|
|
'''
|
|
|
|
|
|
|
|
import argparse
|
|
|
|
import contextlib
|
|
|
|
import copy
|
|
|
|
import crypt
|
2023-08-02 10:46:36 +00:00
|
|
|
import dataclasses
|
2021-11-04 23:20:41 +00:00
|
|
|
import os
|
2023-08-02 10:46:36 +00:00
|
|
|
import pathlib
|
2022-01-04 17:58:09 +00:00
|
|
|
import shlex
|
2021-11-04 23:20:41 +00:00
|
|
|
import socket
|
2021-12-01 23:48:12 +00:00
|
|
|
import subprocess
|
2021-11-04 23:20:41 +00:00
|
|
|
import sys
|
|
|
|
import tempfile
|
2023-08-02 10:46:36 +00:00
|
|
|
from typing import List, Optional, Tuple
|
2021-11-04 23:20:41 +00:00
|
|
|
import yaml
|
|
|
|
|
|
|
|
|
|
|
|
cfg = '''
|
|
|
|
iso:
|
|
|
|
basedir: /srv/iso
|
|
|
|
release:
|
|
|
|
edge: jammy/subiquity-edge/jammy-live-server-subiquity-edge-amd64.iso
|
|
|
|
canary: jammy/jammy-desktop-canary-amd64.iso
|
|
|
|
jammy: jammy/jammy-live-server-amd64.iso
|
2021-12-01 23:48:12 +00:00
|
|
|
desktop: jammy/jammy-desktop-amd64.iso
|
2021-11-04 23:20:41 +00:00
|
|
|
impish: impish/ubuntu-21.10-live-server-amd64.iso
|
|
|
|
hirsute: hirsute/ubuntu-21.04-live-server-amd64.iso
|
|
|
|
groovy: groovy/ubuntu-20.10-live-server-amd64.iso
|
|
|
|
focal: focal/ubuntu-20.04.3-live-server-amd64.iso
|
|
|
|
bionic: bionic/bionic-live-server-amd64.iso
|
|
|
|
default: edge
|
2023-11-12 21:02:20 +00:00
|
|
|
profiles:
|
|
|
|
server:
|
|
|
|
memory: 2G
|
|
|
|
disk-size: 12G
|
|
|
|
extra-qemu-options: []
|
|
|
|
desktop:
|
|
|
|
memory: 8G
|
|
|
|
disk-size: 20G
|
|
|
|
extra-qemu-options: [-device, qxl, -smp, "2"]
|
2021-11-04 23:20:41 +00:00
|
|
|
'''
|
|
|
|
|
2022-01-12 21:56:40 +00:00
|
|
|
|
2023-11-07 13:36:07 +00:00
|
|
|
@dataclasses.dataclass
|
|
|
|
class Profile:
|
|
|
|
name: str
|
|
|
|
memory: str
|
|
|
|
disk_size: str
|
|
|
|
extra_qemu_options: list[str]
|
|
|
|
|
2023-11-12 21:02:20 +00:00
|
|
|
@classmethod
|
|
|
|
def from_config(cls, name, props) -> 'Profile':
|
|
|
|
return Profile(name=name, memory=props['memory'],
|
|
|
|
disk_size=props['disk-size'],
|
|
|
|
extra_qemu_options=props['extra-qemu-options'])
|
2023-11-07 13:36:07 +00:00
|
|
|
|
|
|
|
|
2021-11-04 23:20:41 +00:00
|
|
|
def salted_crypt(plaintext_password):
|
|
|
|
# match subiquity documentation
|
|
|
|
salt = '$6$exDY1mhS4KUYCE/2'
|
|
|
|
return crypt.crypt(plaintext_password, salt)
|
|
|
|
|
|
|
|
|
kvm-test: introduce ability to pass -nic options to QEMU
The only way we had to pass -nic options to QEMU was to specify the
number of networks using the --nets option. Depending on its value, the
option would instruct QEMU to:
* instantiate *n* user host networks if --nets >= 1
* instantiate no network if --nets is 0
* instantiate a "dead" network if --nets < 0
To simulate more advanced networks (such as networks where HTTP proxy is
needed), we want to add the possibility to use TAP network interfaces.
This patch adds the ability to pass custom -nic options to QEMU.
Three new options are available:
* The --nic option passes the argument as-is to QEMU -nic option
* The --nic-user option passes a user host network -nic option to QEMU
- with SSH forwarding enabled. This is just like we do when using the
--nets >= 1 option)
* The --net-tap takes the name of an existing tap interface and
generates the following -nic argument:
tap,id={ifname},ifname={ifname},script=no,downscript=no,model=e1000
In the example below:
* the first network uses the tap0 interface.
* the second network uses the tap1 interface (it is syntactically
equivalent to the first one)
* the third network uses a user host network (with SSH forwarding)
$ kvm-test.py \
--nic tap,id=tap0,ifname=tap0,script=no,downscript=no,model=e1000 \
--nic-tap tap1 \
--nic-user
Signed-off-by: Olivier Gayot <olivier.gayot@canonical.com>
2022-04-28 10:28:03 +00:00
|
|
|
class Tap:
|
|
|
|
def __init__(self, ifname: str) -> None:
|
|
|
|
self.ifname = ifname
|
|
|
|
|
|
|
|
|
2021-11-04 23:20:41 +00:00
|
|
|
class Context:
|
|
|
|
def __init__(self, args):
|
|
|
|
self.config = self.load_config()
|
|
|
|
self.args = args
|
|
|
|
self.release = args.release
|
2023-11-12 21:02:20 +00:00
|
|
|
profiles: dict[str, Profile] = {}
|
|
|
|
for profile_name, profile_props in self.config["profiles"].items():
|
|
|
|
profiles[profile_name] = Profile.from_config(profile_name, profile_props)
|
2023-11-07 13:36:07 +00:00
|
|
|
self.default_mem = profiles[self.args.profile].memory
|
|
|
|
self.default_disk_size = profiles[self.args.profile].disk_size
|
|
|
|
self.qemu_extra_options = profiles[self.args.profile].extra_qemu_options
|
2021-11-04 23:20:41 +00:00
|
|
|
if not self.release:
|
|
|
|
self.release = self.config["iso"]["default"]
|
|
|
|
iso = self.config["iso"]
|
|
|
|
try:
|
|
|
|
self.baseiso = os.path.join(iso["basedir"],
|
|
|
|
iso["release"][self.release])
|
|
|
|
except KeyError:
|
|
|
|
pass
|
|
|
|
self.curdir = os.getcwd()
|
|
|
|
self.iso = f'/tmp/kvm-test/{self.release}-test.iso'
|
|
|
|
self.hostname = f'{self.release}-test'
|
|
|
|
self.target = f'/tmp/kvm-test/{self.hostname}.img'
|
|
|
|
self.password = salted_crypt('ubuntu')
|
|
|
|
self.cloudconfig = f'''\
|
|
|
|
#cloud-config
|
|
|
|
autoinstall:
|
|
|
|
version: 1
|
|
|
|
locale:
|
|
|
|
en_US.UTF-8
|
|
|
|
ssh:
|
|
|
|
install-server: true
|
|
|
|
allow-pw: true
|
|
|
|
identity:
|
|
|
|
hostname: {self.hostname}
|
|
|
|
password: "{self.password}"
|
|
|
|
username: ubuntu
|
|
|
|
'''
|
|
|
|
|
|
|
|
def merge(self, a, b):
|
|
|
|
'''Take a pair of dictionaries, and provide the merged result.
|
|
|
|
Assumes that any key conflicts have values that are themselves
|
|
|
|
dictionaries and raises TypeError if found otherwise.'''
|
|
|
|
result = copy.deepcopy(a)
|
|
|
|
|
|
|
|
for key in b:
|
|
|
|
if key in result:
|
|
|
|
left = result[key]
|
|
|
|
right = b[key]
|
|
|
|
if type(left) is not dict or type(right) is not dict:
|
|
|
|
result[key] = right
|
|
|
|
else:
|
|
|
|
result[key] = self.merge(left, right)
|
|
|
|
else:
|
|
|
|
result[key] = b[key]
|
|
|
|
|
|
|
|
return result
|
|
|
|
|
|
|
|
def load_config(self):
|
|
|
|
result = yaml.safe_load(cfg)
|
|
|
|
homecfg = f'{os.environ["HOME"]}/.kvm-test.yaml'
|
|
|
|
if os.path.exists(homecfg):
|
|
|
|
with open(homecfg, 'r') as f:
|
|
|
|
result = self.merge(result, yaml.safe_load(f))
|
|
|
|
|
|
|
|
return result
|
|
|
|
|
|
|
|
|
|
|
|
parser = argparse.ArgumentParser(
|
|
|
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
|
|
description='''\
|
|
|
|
Test isos and images written to /tmp/kvm-test
|
|
|
|
|
|
|
|
Sample usage:
|
|
|
|
kvm-test --build -q --install -o -a --boot
|
|
|
|
slimy build, run install, overwrite existing image, use autoinstall,
|
|
|
|
boot final resulting image
|
|
|
|
|
|
|
|
kvm-test --install -bo -rfocal
|
|
|
|
boot the focal base iso unmodified and run install manually
|
|
|
|
|
2022-01-12 20:36:41 +00:00
|
|
|
If DEBOOTSTRAP_PROXY is set, that will be passed to snapcraft to pick up
|
|
|
|
packages from a cache.
|
|
|
|
|
2021-11-04 23:20:41 +00:00
|
|
|
See 'cfg' in script for expected layout of iso files,
|
|
|
|
which can be managed with ~/.kvm-test.yaml''')
|
|
|
|
parser.add_argument('-b', '--base', default=False, action='store_true',
|
|
|
|
help='use base iso')
|
2021-11-05 03:03:35 +00:00
|
|
|
parser.add_argument('--basesnap', default=None, action='store',
|
|
|
|
help='use slimy-update-snap on this snap')
|
2021-11-05 03:04:18 +00:00
|
|
|
parser.add_argument('--snap', default=None, action='store',
|
|
|
|
help='inject this snap into the ISO')
|
2021-11-04 23:20:41 +00:00
|
|
|
parser.add_argument('-B', '--bios', action='store_true', default=False,
|
2021-12-01 23:48:12 +00:00
|
|
|
help='boot in BIOS mode (default mode is UEFI)')
|
|
|
|
parser.add_argument('-c', '--channel', action='store',
|
2021-11-04 23:20:41 +00:00
|
|
|
help='build iso with snap from channel')
|
2023-11-07 13:36:07 +00:00
|
|
|
parser.add_argument('-d', '--disksize', help='size of disk to create')
|
2021-11-04 23:20:41 +00:00
|
|
|
parser.add_argument('-i', '--img', action='store', help='use this img')
|
|
|
|
parser.add_argument('-n', '--nets', action='store', default=1, type=int,
|
2022-01-25 00:03:00 +00:00
|
|
|
help='''number of network interfaces.
|
|
|
|
0=no network, -1=deadnet''')
|
kvm-test: introduce ability to pass -nic options to QEMU
The only way we had to pass -nic options to QEMU was to specify the
number of networks using the --nets option. Depending on its value, the
option would instruct QEMU to:
* instantiate *n* user host networks if --nets >= 1
* instantiate no network if --nets is 0
* instantiate a "dead" network if --nets < 0
To simulate more advanced networks (such as networks where HTTP proxy is
needed), we want to add the possibility to use TAP network interfaces.
This patch adds the ability to pass custom -nic options to QEMU.
Three new options are available:
* The --nic option passes the argument as-is to QEMU -nic option
* The --nic-user option passes a user host network -nic option to QEMU
- with SSH forwarding enabled. This is just like we do when using the
--nets >= 1 option)
* The --net-tap takes the name of an existing tap interface and
generates the following -nic argument:
tap,id={ifname},ifname={ifname},script=no,downscript=no,model=e1000
In the example below:
* the first network uses the tap0 interface.
* the second network uses the tap1 interface (it is syntactically
equivalent to the first one)
* the third network uses a user host network (with SSH forwarding)
$ kvm-test.py \
--nic tap,id=tap0,ifname=tap0,script=no,downscript=no,model=e1000 \
--nic-tap tap1 \
--nic-user
Signed-off-by: Olivier Gayot <olivier.gayot@canonical.com>
2022-04-28 10:28:03 +00:00
|
|
|
parser.add_argument('--nic-user', action="append_const", dest="nics",
|
|
|
|
const=None,
|
|
|
|
help='pass user host -nic to QEMU'
|
|
|
|
' - overrides --nets')
|
|
|
|
parser.add_argument('--nic-tap', action="append", dest="nics", type=Tap,
|
|
|
|
metavar="ifname",
|
|
|
|
help='TAP interface to be passed as -nic to QEMU'
|
|
|
|
' - overrides --nets')
|
|
|
|
parser.add_argument('--nic', action="append", dest="nics",
|
|
|
|
metavar="argument",
|
|
|
|
help='pass custom -nic argument to QEMU'
|
|
|
|
' - overrides --nets')
|
2021-11-04 23:20:41 +00:00
|
|
|
parser.add_argument('-o', '--overwrite', default=False, action='store_true',
|
|
|
|
help='allow overwrite of the target image')
|
|
|
|
parser.add_argument('-q', '--quick', default=False, action='store_true',
|
|
|
|
help='build iso with quick-test-this-branch')
|
|
|
|
parser.add_argument('-r', '--release', action='store', help='target release')
|
|
|
|
parser.add_argument('-s', '--serial', default=False, action='store_true',
|
|
|
|
help='attach to serial console')
|
|
|
|
parser.add_argument('-S', '--sound', default=False, action='store_true',
|
|
|
|
help='enable sound')
|
2021-12-01 23:48:12 +00:00
|
|
|
parser.add_argument('--iso', action='store', help='use this iso')
|
2021-11-04 23:20:41 +00:00
|
|
|
parser.add_argument('-u', '--update', action='store',
|
|
|
|
help='subiquity-channel argument')
|
2021-12-01 23:48:12 +00:00
|
|
|
parser.add_argument('-m', '--memory', action='store',
|
|
|
|
help='memory for VM')
|
2021-11-04 23:20:41 +00:00
|
|
|
parser.add_argument('--save', action='store_true',
|
|
|
|
help='preserve built snap')
|
|
|
|
parser.add_argument('--reuse', action='store_true',
|
2021-12-01 23:48:12 +00:00
|
|
|
help='reuse previously saved snap. Implies --save')
|
2021-11-04 23:20:41 +00:00
|
|
|
parser.add_argument('--build', default=False, action='store_true',
|
|
|
|
help='build test iso')
|
|
|
|
parser.add_argument('--install', default=False, action='store_true',
|
|
|
|
help='''install from iso - one must either build a test
|
|
|
|
iso, use a base iso, or reuse previous test iso''')
|
|
|
|
parser.add_argument('--boot', default=False, action='store_true',
|
|
|
|
help='boot test image')
|
2022-09-15 11:22:12 +00:00
|
|
|
parser.add_argument('--force-autoinstall', default=None,
|
|
|
|
action='store_true', dest="autoinstall",
|
|
|
|
help='pass autoinstall on the kernel command line')
|
|
|
|
parser.add_argument('--force-no-autoinstall', default=None,
|
|
|
|
action='store_false', dest="autoinstall",
|
|
|
|
help='do not pass autoinstall on the kernel command line')
|
2023-08-02 10:46:36 +00:00
|
|
|
parser.add_argument('--with-tpm2', action='store_true',
|
|
|
|
help='''emulate a TPM 2.0 interface (requires swtpm
|
|
|
|
package)''')
|
2023-11-07 13:36:07 +00:00
|
|
|
parser.add_argument('--profile', default="server",
|
|
|
|
help='load predefined memory, disk size and qemu options')
|
2022-09-15 11:22:12 +00:00
|
|
|
|
|
|
|
|
|
|
|
cc_group = parser.add_mutually_exclusive_group()
|
|
|
|
cc_group.add_argument('--cloud-config', action='store',
|
|
|
|
type=argparse.FileType(),
|
|
|
|
help='specify the cloud-config file to use (it may'
|
|
|
|
' contain an autoinstall section or not)')
|
|
|
|
cc_group.add_argument('--cloud-config-default',
|
|
|
|
action="store_true",
|
|
|
|
help='use hardcoded cloud-config template')
|
2021-11-04 23:20:41 +00:00
|
|
|
|
2021-12-01 23:48:12 +00:00
|
|
|
def parse_args():
|
|
|
|
ctx = Context(parser.parse_args())
|
|
|
|
if ctx.args.quick or ctx.args.basesnap or ctx.args.snap \
|
|
|
|
or ctx.args.channel or ctx.args.reuse:
|
|
|
|
ctx.args.build = True
|
|
|
|
if ctx.args.reuse:
|
|
|
|
ctx.args.save = True
|
2022-01-12 20:36:41 +00:00
|
|
|
|
|
|
|
ctx.livefs_editor = os.environ.get('LIVEFS_EDITOR')
|
2022-04-27 17:59:17 +00:00
|
|
|
if not ctx.livefs_editor and ctx.args.build:
|
2022-01-12 20:36:41 +00:00
|
|
|
raise Exception('Obtain a copy of livefs-editor and point ' +
|
|
|
|
'LIVEFS_EDITOR to it\n'
|
|
|
|
'https://github.com/mwhudson/livefs-editor')
|
|
|
|
|
2021-12-01 23:48:12 +00:00
|
|
|
return ctx
|
|
|
|
|
|
|
|
|
|
|
|
def run(cmd):
|
|
|
|
if isinstance(cmd, str):
|
|
|
|
cmd_str = cmd
|
2022-01-04 17:58:09 +00:00
|
|
|
cmd_array = shlex.split(cmd)
|
2021-12-01 23:48:12 +00:00
|
|
|
else:
|
2022-01-04 17:58:09 +00:00
|
|
|
cmd_str = shlex.join(cmd)
|
2021-12-01 23:48:12 +00:00
|
|
|
cmd_array = cmd
|
|
|
|
# semi-simulate "bash -x"
|
2022-01-04 17:58:09 +00:00
|
|
|
print(f'+ {cmd_str}', file=sys.stderr)
|
2021-12-01 23:48:12 +00:00
|
|
|
subprocess.run(cmd_array, check=True)
|
2021-11-04 23:20:41 +00:00
|
|
|
|
|
|
|
|
2021-12-01 23:48:12 +00:00
|
|
|
def assert_exists(path):
|
|
|
|
if not os.path.exists(path):
|
2022-01-04 17:58:09 +00:00
|
|
|
raise Exception(f'Expected file {path} not found')
|
|
|
|
|
|
|
|
|
|
|
|
def remove_if_exists(path):
|
|
|
|
if os.path.exists(path):
|
|
|
|
os.remove(path)
|
2021-11-04 23:20:41 +00:00
|
|
|
|
|
|
|
|
|
|
|
@contextlib.contextmanager
|
|
|
|
def delete_later(path):
|
|
|
|
try:
|
|
|
|
yield path
|
|
|
|
finally:
|
2022-01-04 17:58:09 +00:00
|
|
|
remove_if_exists(path)
|
2021-11-04 23:20:41 +00:00
|
|
|
|
|
|
|
|
|
|
|
@contextlib.contextmanager
|
|
|
|
def noop(path):
|
|
|
|
yield path
|
|
|
|
|
|
|
|
|
|
|
|
@contextlib.contextmanager
|
2022-01-20 10:59:48 +00:00
|
|
|
def mounter(src, dest):
|
|
|
|
run(["fuseiso", src, dest])
|
2021-11-04 23:20:41 +00:00
|
|
|
try:
|
|
|
|
yield
|
|
|
|
finally:
|
2022-01-20 10:59:48 +00:00
|
|
|
run(["fusermount", "-u", dest])
|
2021-11-04 23:20:41 +00:00
|
|
|
|
|
|
|
|
2021-12-01 23:48:12 +00:00
|
|
|
def livefs_edit(ctx, *args):
|
2022-01-12 20:36:41 +00:00
|
|
|
livefs_editor = os.environ['LIVEFS_EDITOR']
|
|
|
|
run(['sudo', f'PYTHONPATH={livefs_editor}', 'python3', '-m', 'livefs_edit',
|
2022-01-12 21:56:40 +00:00
|
|
|
ctx.baseiso, ctx.iso, *args])
|
2021-12-01 23:48:12 +00:00
|
|
|
|
|
|
|
|
2021-11-04 23:20:41 +00:00
|
|
|
def build(ctx):
|
2022-01-04 17:58:09 +00:00
|
|
|
remove_if_exists(ctx.iso)
|
2021-11-04 23:20:41 +00:00
|
|
|
project = os.path.basename(os.getcwd())
|
|
|
|
|
2022-01-12 20:36:41 +00:00
|
|
|
snapargs = '--debug'
|
2022-01-13 19:25:12 +00:00
|
|
|
http_proxy = os.environ.get('DEBOOTSTRAP_PROXY')
|
2022-01-12 20:36:41 +00:00
|
|
|
if http_proxy:
|
|
|
|
snapargs += f' --http-proxy={http_proxy}'
|
|
|
|
|
2021-11-04 23:20:41 +00:00
|
|
|
snap_manager = noop if ctx.args.save else delete_later
|
2021-12-01 23:48:12 +00:00
|
|
|
if project == 'subiquity':
|
2021-11-04 23:20:41 +00:00
|
|
|
if ctx.args.quick:
|
2021-11-08 23:44:35 +00:00
|
|
|
run(f'sudo ./scripts/quick-test-this-branch.sh {ctx.baseiso} \
|
|
|
|
{ctx.iso}')
|
2021-11-05 03:03:35 +00:00
|
|
|
elif ctx.args.basesnap:
|
|
|
|
with snap_manager('subiquity_test.snap') as snap:
|
|
|
|
run(f'sudo ./scripts/slimy-update-snap.sh {ctx.args.basesnap} \
|
|
|
|
{snap}')
|
|
|
|
run(f'sudo ./scripts/inject-subiquity-snap.sh {ctx.baseiso} \
|
|
|
|
{snap} {ctx.iso}')
|
2021-11-05 03:04:18 +00:00
|
|
|
elif ctx.args.snap:
|
|
|
|
run(f'sudo ./scripts/inject-subiquity-snap.sh {ctx.baseiso} \
|
|
|
|
{ctx.args.snap} {ctx.iso}')
|
2021-11-04 23:20:41 +00:00
|
|
|
elif ctx.args.channel:
|
2021-12-01 23:48:12 +00:00
|
|
|
livefs_edit(ctx, '--add-snap-from-store', 'core20', 'stable',
|
|
|
|
'--add-snap-from-store', 'subiquity',
|
|
|
|
ctx.args.channel)
|
2021-11-04 23:20:41 +00:00
|
|
|
else:
|
|
|
|
with snap_manager('subiquity_test.snap') as snap:
|
|
|
|
if not ctx.args.reuse:
|
2021-12-01 23:48:12 +00:00
|
|
|
run('snapcraft clean --use-lxd')
|
2023-10-20 13:41:24 +00:00
|
|
|
run(f'snapcraft pack --use-lxd --output {snap} {snapargs}')
|
2021-12-01 23:48:12 +00:00
|
|
|
assert_exists(snap)
|
|
|
|
livefs_edit(ctx, '--add-snap-from-store', 'core20', 'stable',
|
2022-01-12 21:56:40 +00:00
|
|
|
'--inject-snap', snap)
|
2021-11-04 23:20:41 +00:00
|
|
|
elif project == 'ubuntu-desktop-installer':
|
|
|
|
with snap_manager('udi_test.snap') as snap:
|
2021-12-01 23:48:12 +00:00
|
|
|
run('snapcraft clean --use-lxd')
|
2023-10-20 13:41:24 +00:00
|
|
|
run(f'snapcraft pack --use-lxd --output {snap} {snapargs}')
|
2021-12-01 23:48:12 +00:00
|
|
|
assert_exists(snap)
|
|
|
|
run(f'sudo ./scripts/inject-snap {ctx.baseiso} {ctx.iso} {snap}')
|
2021-11-04 23:20:41 +00:00
|
|
|
else:
|
2021-11-05 03:05:00 +00:00
|
|
|
raise Exception(f'do not know how to build {project}')
|
2021-11-04 23:20:41 +00:00
|
|
|
|
2021-12-01 23:48:12 +00:00
|
|
|
assert_exists(ctx.iso)
|
2021-11-04 23:20:41 +00:00
|
|
|
|
|
|
|
|
|
|
|
def write(dest, data):
|
|
|
|
with open(dest, 'w') as destfile:
|
|
|
|
destfile.write(data)
|
|
|
|
|
|
|
|
|
|
|
|
def touch(dest):
|
|
|
|
with open(dest, 'w'):
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
def create_seed(cloudconfig, tempdir):
|
|
|
|
write(f'{tempdir}/user-data', cloudconfig)
|
|
|
|
touch(f'{tempdir}/meta-data')
|
|
|
|
seed = f'{tempdir}/seed.iso'
|
|
|
|
run(f'cloud-localds {seed} {tempdir}/user-data {tempdir}/meta-data')
|
|
|
|
return seed
|
|
|
|
|
|
|
|
|
Fix -drive option from kvm-test.sh
The function drive() used to return a string in the following format:
"-drive file=/path/to/iso,..."
However, qemu/kvm expects "-drive" to be an argument and
"file=/path/to/iso,..." to be another argument.
The command was constructed as below since the beginning:
kvm = [
"kvm",
"-cdrom", "custom.iso", # <- OK
"-drive file=/path/to/iso,...", # <- NOK
]
Before 06ac3f92, we would join all the arguments using spaces before
executing the kvm command. Therefore we would luckily end up with a
correct command:
" ".join(kvm) -> "kvm -cdrom custom.iso -drive file=/path/to/iso,..."
However, now that we supply the command to subprocess.run directly, the
problem shows up.
Fixed by returning a tuple("-drive", "file=/path/to/iso,...") from
the drive() function.
Signed-off-by: Olivier Gayot <olivier.gayot@canonical.com>
2022-01-13 09:25:06 +00:00
|
|
|
def drive(path, format='qcow2') -> Tuple[str, str]:
|
|
|
|
""" Return a tuple (-drive, <options>) that can be passed to kvm """
|
2021-11-04 23:20:41 +00:00
|
|
|
kwargs = []
|
|
|
|
serial = None
|
|
|
|
cparam = 'writethrough'
|
2021-12-01 23:48:12 +00:00
|
|
|
kwargs.append(f'file={path}')
|
|
|
|
kwargs.append(f'format={format}')
|
|
|
|
kwargs.append(f'cache={cparam}')
|
|
|
|
kwargs.append('if=virtio')
|
2021-11-04 23:20:41 +00:00
|
|
|
if serial:
|
2021-12-01 23:48:12 +00:00
|
|
|
kwargs.append(f'serial={serial}')
|
2021-11-04 23:20:41 +00:00
|
|
|
|
Fix -drive option from kvm-test.sh
The function drive() used to return a string in the following format:
"-drive file=/path/to/iso,..."
However, qemu/kvm expects "-drive" to be an argument and
"file=/path/to/iso,..." to be another argument.
The command was constructed as below since the beginning:
kvm = [
"kvm",
"-cdrom", "custom.iso", # <- OK
"-drive file=/path/to/iso,...", # <- NOK
]
Before 06ac3f92, we would join all the arguments using spaces before
executing the kvm command. Therefore we would luckily end up with a
correct command:
" ".join(kvm) -> "kvm -cdrom custom.iso -drive file=/path/to/iso,..."
However, now that we supply the command to subprocess.run directly, the
problem shows up.
Fixed by returning a tuple("-drive", "file=/path/to/iso,...") from
the drive() function.
Signed-off-by: Olivier Gayot <olivier.gayot@canonical.com>
2022-01-13 09:25:06 +00:00
|
|
|
return ('-drive', ','.join(kwargs))
|
2021-11-04 23:20:41 +00:00
|
|
|
|
|
|
|
|
|
|
|
class PortFinder:
|
|
|
|
def __init__(self):
|
|
|
|
self.finder = self.port_generator()
|
|
|
|
|
|
|
|
def port_generator(self):
|
|
|
|
for port in range(2222, 8000):
|
|
|
|
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
|
|
|
|
res = sock.connect_ex(('localhost', port))
|
|
|
|
if res != 0:
|
|
|
|
yield port
|
|
|
|
|
|
|
|
def get(self):
|
|
|
|
return next(self.finder)
|
|
|
|
|
|
|
|
|
kvm-test: introduce ability to pass -nic options to QEMU
The only way we had to pass -nic options to QEMU was to specify the
number of networks using the --nets option. Depending on its value, the
option would instruct QEMU to:
* instantiate *n* user host networks if --nets >= 1
* instantiate no network if --nets is 0
* instantiate a "dead" network if --nets < 0
To simulate more advanced networks (such as networks where HTTP proxy is
needed), we want to add the possibility to use TAP network interfaces.
This patch adds the ability to pass custom -nic options to QEMU.
Three new options are available:
* The --nic option passes the argument as-is to QEMU -nic option
* The --nic-user option passes a user host network -nic option to QEMU
- with SSH forwarding enabled. This is just like we do when using the
--nets >= 1 option)
* The --net-tap takes the name of an existing tap interface and
generates the following -nic argument:
tap,id={ifname},ifname={ifname},script=no,downscript=no,model=e1000
In the example below:
* the first network uses the tap0 interface.
* the second network uses the tap1 interface (it is syntactically
equivalent to the first one)
* the third network uses a user host network (with SSH forwarding)
$ kvm-test.py \
--nic tap,id=tap0,ifname=tap0,script=no,downscript=no,model=e1000 \
--nic-tap tap1 \
--nic-user
Signed-off-by: Olivier Gayot <olivier.gayot@canonical.com>
2022-04-28 10:28:03 +00:00
|
|
|
class NetFactory:
|
|
|
|
""" Generate -nic options for QEMU. """
|
|
|
|
ports_finder = PortFinder()
|
|
|
|
|
|
|
|
def user(self) -> Tuple[str, ...]:
|
|
|
|
""" User host network with SSH forwarding """
|
|
|
|
port = self.ports_finder.get()
|
|
|
|
return ('-nic', f'user,model=virtio-net-pci,hostfwd=tcp::{port}-:22')
|
|
|
|
|
|
|
|
def tap(self, ifname: str) -> Tuple[str, ...]:
|
|
|
|
""" Network using an existing TAP interface. """
|
|
|
|
tap_props = {
|
|
|
|
"id": ifname,
|
|
|
|
"ifname": ifname,
|
|
|
|
"script": "no",
|
|
|
|
"downscript": "no",
|
|
|
|
"model": "e1000",
|
|
|
|
}
|
|
|
|
|
|
|
|
nic = ",".join(["tap"] + [f"{k}={v}" for k, v in tap_props.items()])
|
|
|
|
|
|
|
|
return ('-nic', nic)
|
|
|
|
|
|
|
|
def deadnet(self) -> Tuple[str, ...]:
|
|
|
|
""" NIC present but restricted - simulate deadnet environment """
|
|
|
|
return ('-nic', 'user,model=virtio-net-pci,restrict=on')
|
|
|
|
|
|
|
|
def nonet(self) -> Tuple[str, ...]:
|
|
|
|
""" No network """
|
|
|
|
return ('-nic', 'none')
|
|
|
|
|
|
|
|
|
|
|
|
def nets(ctx) -> List[str]:
|
|
|
|
nics: List[str] = []
|
|
|
|
factory = NetFactory()
|
|
|
|
|
|
|
|
if ctx.args.nics:
|
|
|
|
for nic in ctx.args.nics:
|
|
|
|
if nic is None:
|
|
|
|
nics.extend(factory.user())
|
|
|
|
elif isinstance(nic, Tap):
|
|
|
|
nics.extend(factory.tap(nic.ifname))
|
|
|
|
else:
|
|
|
|
nics.extend(('-nic', nic))
|
|
|
|
elif ctx.args.nets > 0:
|
2021-11-04 23:20:41 +00:00
|
|
|
for _ in range(ctx.args.nets):
|
kvm-test: introduce ability to pass -nic options to QEMU
The only way we had to pass -nic options to QEMU was to specify the
number of networks using the --nets option. Depending on its value, the
option would instruct QEMU to:
* instantiate *n* user host networks if --nets >= 1
* instantiate no network if --nets is 0
* instantiate a "dead" network if --nets < 0
To simulate more advanced networks (such as networks where HTTP proxy is
needed), we want to add the possibility to use TAP network interfaces.
This patch adds the ability to pass custom -nic options to QEMU.
Three new options are available:
* The --nic option passes the argument as-is to QEMU -nic option
* The --nic-user option passes a user host network -nic option to QEMU
- with SSH forwarding enabled. This is just like we do when using the
--nets >= 1 option)
* The --net-tap takes the name of an existing tap interface and
generates the following -nic argument:
tap,id={ifname},ifname={ifname},script=no,downscript=no,model=e1000
In the example below:
* the first network uses the tap0 interface.
* the second network uses the tap1 interface (it is syntactically
equivalent to the first one)
* the third network uses a user host network (with SSH forwarding)
$ kvm-test.py \
--nic tap,id=tap0,ifname=tap0,script=no,downscript=no,model=e1000 \
--nic-tap tap1 \
--nic-user
Signed-off-by: Olivier Gayot <olivier.gayot@canonical.com>
2022-04-28 10:28:03 +00:00
|
|
|
nics.extend(factory.user())
|
2022-01-25 00:03:00 +00:00
|
|
|
elif ctx.args.nets == 0:
|
kvm-test: introduce ability to pass -nic options to QEMU
The only way we had to pass -nic options to QEMU was to specify the
number of networks using the --nets option. Depending on its value, the
option would instruct QEMU to:
* instantiate *n* user host networks if --nets >= 1
* instantiate no network if --nets is 0
* instantiate a "dead" network if --nets < 0
To simulate more advanced networks (such as networks where HTTP proxy is
needed), we want to add the possibility to use TAP network interfaces.
This patch adds the ability to pass custom -nic options to QEMU.
Three new options are available:
* The --nic option passes the argument as-is to QEMU -nic option
* The --nic-user option passes a user host network -nic option to QEMU
- with SSH forwarding enabled. This is just like we do when using the
--nets >= 1 option)
* The --net-tap takes the name of an existing tap interface and
generates the following -nic argument:
tap,id={ifname},ifname={ifname},script=no,downscript=no,model=e1000
In the example below:
* the first network uses the tap0 interface.
* the second network uses the tap1 interface (it is syntactically
equivalent to the first one)
* the third network uses a user host network (with SSH forwarding)
$ kvm-test.py \
--nic tap,id=tap0,ifname=tap0,script=no,downscript=no,model=e1000 \
--nic-tap tap1 \
--nic-user
Signed-off-by: Olivier Gayot <olivier.gayot@canonical.com>
2022-04-28 10:28:03 +00:00
|
|
|
nics.extend(factory.nonet())
|
2021-11-04 23:20:41 +00:00
|
|
|
else:
|
kvm-test: introduce ability to pass -nic options to QEMU
The only way we had to pass -nic options to QEMU was to specify the
number of networks using the --nets option. Depending on its value, the
option would instruct QEMU to:
* instantiate *n* user host networks if --nets >= 1
* instantiate no network if --nets is 0
* instantiate a "dead" network if --nets < 0
To simulate more advanced networks (such as networks where HTTP proxy is
needed), we want to add the possibility to use TAP network interfaces.
This patch adds the ability to pass custom -nic options to QEMU.
Three new options are available:
* The --nic option passes the argument as-is to QEMU -nic option
* The --nic-user option passes a user host network -nic option to QEMU
- with SSH forwarding enabled. This is just like we do when using the
--nets >= 1 option)
* The --net-tap takes the name of an existing tap interface and
generates the following -nic argument:
tap,id={ifname},ifname={ifname},script=no,downscript=no,model=e1000
In the example below:
* the first network uses the tap0 interface.
* the second network uses the tap1 interface (it is syntactically
equivalent to the first one)
* the third network uses a user host network (with SSH forwarding)
$ kvm-test.py \
--nic tap,id=tap0,ifname=tap0,script=no,downscript=no,model=e1000 \
--nic-tap tap1 \
--nic-user
Signed-off-by: Olivier Gayot <olivier.gayot@canonical.com>
2022-04-28 10:28:03 +00:00
|
|
|
nics.extend(factory.deadnet())
|
|
|
|
return nics
|
2021-11-04 23:20:41 +00:00
|
|
|
|
|
|
|
|
2023-08-02 10:46:36 +00:00
|
|
|
@dataclasses.dataclass(frozen=True)
|
|
|
|
class TPMEmulator:
|
|
|
|
socket: pathlib.Path
|
|
|
|
logfile: pathlib.Path
|
|
|
|
tpmstate: pathlib.Path
|
|
|
|
|
|
|
|
|
|
|
|
def tpm(emulator: Optional[TPMEmulator]) -> List[str]:
|
|
|
|
if emulator is None:
|
|
|
|
return []
|
|
|
|
|
|
|
|
return ['-chardev', f'socket,id=chrtpm,path={emulator.socket}',
|
|
|
|
'-tpmdev', 'emulator,id=tpm0,chardev=chrtpm',
|
|
|
|
'-device', 'tpm-tis,tpmdev=tpm0']
|
|
|
|
|
|
|
|
|
2021-11-04 23:20:41 +00:00
|
|
|
def bios(ctx):
|
|
|
|
ret = []
|
|
|
|
# https://help.ubuntu.com/community/UEFI
|
|
|
|
if not ctx.args.bios:
|
2021-12-01 23:48:12 +00:00
|
|
|
ret = ['-bios', '/usr/share/qemu/OVMF.fd']
|
2021-11-04 23:20:41 +00:00
|
|
|
return ret
|
|
|
|
|
|
|
|
|
|
|
|
def memory(ctx):
|
2021-12-01 23:48:12 +00:00
|
|
|
return ['-m', ctx.args.memory or ctx.default_mem]
|
2021-11-04 23:20:41 +00:00
|
|
|
|
|
|
|
|
2023-08-02 10:46:36 +00:00
|
|
|
@contextlib.contextmanager
|
|
|
|
def kvm_prepare_common(ctx):
|
|
|
|
'''Spawn needed background processes and return the CLI options for QEMU'''
|
2021-11-04 23:20:41 +00:00
|
|
|
ret = ['kvm', '-no-reboot']
|
|
|
|
ret.extend(('-vga', 'virtio'))
|
|
|
|
ret.extend(memory(ctx))
|
|
|
|
ret.extend(bios(ctx))
|
|
|
|
ret.extend(nets(ctx))
|
|
|
|
if ctx.args.sound:
|
|
|
|
ret.extend(('-device', 'AC97', '-device', 'usb-ehci'))
|
2023-08-02 10:46:36 +00:00
|
|
|
|
2023-11-07 13:36:07 +00:00
|
|
|
ret.extend(ctx.qemu_extra_options)
|
|
|
|
|
2023-08-02 10:46:36 +00:00
|
|
|
if ctx.args.with_tpm2:
|
|
|
|
tpm_emulator_context = tpm_emulator()
|
|
|
|
else:
|
|
|
|
tpm_emulator_context = contextlib.nullcontext()
|
|
|
|
|
|
|
|
with tpm_emulator_context as tpm_emulator_cm:
|
|
|
|
ret.extend(tpm(tpm_emulator_cm))
|
|
|
|
yield ret
|
2021-11-04 23:20:41 +00:00
|
|
|
|
|
|
|
|
|
|
|
def get_initrd(mntdir):
|
|
|
|
for initrd in ('initrd', 'initrd.lz', 'initrd.lz4'):
|
|
|
|
path = f'{mntdir}/casper/{initrd}'
|
|
|
|
if os.path.exists(path):
|
|
|
|
return path
|
|
|
|
raise Exception('initrd not found')
|
|
|
|
|
|
|
|
|
|
|
|
def install(ctx):
|
|
|
|
if os.path.exists(ctx.target):
|
|
|
|
if ctx.args.overwrite:
|
|
|
|
os.remove(ctx.target)
|
2021-12-01 23:48:12 +00:00
|
|
|
else:
|
2022-01-12 20:36:41 +00:00
|
|
|
raise Exception('refusing to overwrite existing image, use the ' +
|
|
|
|
'-o option to allow overwriting')
|
2021-11-04 23:20:41 +00:00
|
|
|
|
|
|
|
with tempfile.TemporaryDirectory() as tempdir:
|
|
|
|
mntdir = f'{tempdir}/mnt'
|
|
|
|
os.mkdir(mntdir)
|
|
|
|
appends = []
|
|
|
|
|
2023-08-02 10:46:36 +00:00
|
|
|
with kvm_prepare_common(ctx) as kvm:
|
2021-11-04 23:20:41 +00:00
|
|
|
|
2023-08-02 10:46:36 +00:00
|
|
|
if ctx.args.iso:
|
|
|
|
iso = ctx.args.iso
|
|
|
|
elif ctx.args.base:
|
|
|
|
iso = ctx.baseiso
|
2022-09-15 11:22:12 +00:00
|
|
|
else:
|
2023-08-02 10:46:36 +00:00
|
|
|
iso = ctx.iso
|
|
|
|
|
|
|
|
kvm.extend(('-cdrom', iso))
|
|
|
|
|
|
|
|
if ctx.args.serial:
|
|
|
|
kvm.append('-nographic')
|
|
|
|
appends.append('console=ttyS0')
|
|
|
|
|
|
|
|
if ctx.args.cloud_config is not None or ctx.args.cloud_config_default:
|
|
|
|
if ctx.args.cloud_config is not None:
|
|
|
|
ctx.cloudconfig = ctx.args.cloud_config.read()
|
|
|
|
kvm.extend(drive(create_seed(ctx.cloudconfig, tempdir), 'raw'))
|
|
|
|
if ctx.args.autoinstall is None:
|
|
|
|
# Let's inspect the yaml and check if there is an autoinstall
|
|
|
|
# section.
|
|
|
|
autoinstall = "autoinstall" in yaml.safe_load(ctx.cloudconfig)
|
|
|
|
else:
|
|
|
|
autoinstall = ctx.args.autoinstall
|
2022-09-15 11:22:12 +00:00
|
|
|
|
2023-08-02 10:46:36 +00:00
|
|
|
if autoinstall:
|
|
|
|
appends.append('autoinstall')
|
2022-09-15 11:22:12 +00:00
|
|
|
|
2021-11-04 23:20:41 +00:00
|
|
|
|
2023-08-02 10:46:36 +00:00
|
|
|
if ctx.args.update:
|
|
|
|
appends.append('subiquity-channel=' + ctx.args.update)
|
2021-11-04 23:20:41 +00:00
|
|
|
|
2023-08-02 10:46:36 +00:00
|
|
|
kvm.extend(drive(ctx.target))
|
|
|
|
if not os.path.exists(ctx.target) or ctx.args.overwrite:
|
2023-11-07 13:36:07 +00:00
|
|
|
disksize = ctx.args.disksize or ctx.default_disk_size
|
|
|
|
run(f'qemu-img create -f qcow2 {ctx.target} {disksize}')
|
2021-11-04 23:20:41 +00:00
|
|
|
|
2023-08-02 10:46:36 +00:00
|
|
|
if len(appends) > 0:
|
|
|
|
with mounter(iso, mntdir):
|
|
|
|
# if we're passing kernel args, we need to manually specify
|
|
|
|
# kernel / initrd
|
|
|
|
kvm.extend(('-kernel', f'{mntdir}/casper/vmlinuz'))
|
|
|
|
kvm.extend(('-initrd', get_initrd(mntdir)))
|
|
|
|
kvm.extend(('-append', ' '.join(appends)))
|
|
|
|
run(kvm)
|
|
|
|
else:
|
2021-12-01 23:48:12 +00:00
|
|
|
run(kvm)
|
2023-08-02 10:46:36 +00:00
|
|
|
|
|
|
|
|
|
|
|
@contextlib.contextmanager
|
|
|
|
def tpm_emulator(directory=None):
|
|
|
|
if directory is None:
|
|
|
|
directory_context = tempfile.TemporaryDirectory()
|
|
|
|
else:
|
|
|
|
directory_context = contextlib.nullcontext(enter_result=directory)
|
|
|
|
|
|
|
|
with directory_context as tempdir:
|
|
|
|
socket = os.path.join(tempdir, 'swtpm-sock')
|
|
|
|
logfile = os.path.join(tempdir, 'log')
|
|
|
|
tpmstate = tempdir
|
|
|
|
|
|
|
|
ps = subprocess.Popen(['swtpm', 'socket',
|
|
|
|
'--tpmstate', f'dir={tpmstate}',
|
|
|
|
'--ctrl', f'type=unixio,path={socket}',
|
|
|
|
'--tpm2',
|
|
|
|
'--log', f'file={logfile},level=20'],
|
|
|
|
)
|
|
|
|
try:
|
|
|
|
yield TPMEmulator(socket=pathlib.Path(socket),
|
|
|
|
logfile=pathlib.Path(logfile),
|
|
|
|
tpmstate=pathlib.Path(tpmstate))
|
|
|
|
finally:
|
|
|
|
ps.communicate()
|
2021-11-04 23:20:41 +00:00
|
|
|
|
|
|
|
|
|
|
|
def boot(ctx):
|
|
|
|
target = ctx.target
|
|
|
|
if ctx.args.img:
|
|
|
|
target = ctx.args.img
|
|
|
|
|
2023-08-02 10:46:36 +00:00
|
|
|
with kvm_prepare_common(ctx) as kvm:
|
|
|
|
kvm.extend(drive(target))
|
|
|
|
run(kvm)
|
2021-11-04 23:20:41 +00:00
|
|
|
|
|
|
|
|
2022-01-04 17:58:09 +00:00
|
|
|
def help():
|
2021-11-04 23:20:41 +00:00
|
|
|
parser.print_usage()
|
|
|
|
sys.exit(1)
|
|
|
|
|
|
|
|
|
2022-04-27 17:55:55 +00:00
|
|
|
def main() -> None:
|
|
|
|
""" Entry point. """
|
|
|
|
try:
|
|
|
|
ctx = parse_args()
|
|
|
|
except TypeError:
|
|
|
|
help()
|
|
|
|
|
|
|
|
if ctx.args.base and ctx.args.build:
|
|
|
|
raise Exception('cannot use base iso and build')
|
|
|
|
|
|
|
|
os.makedirs('/tmp/kvm-test', exist_ok=True)
|
|
|
|
|
|
|
|
if ctx.args.build:
|
|
|
|
build(ctx)
|
|
|
|
if ctx.args.install:
|
|
|
|
install(ctx)
|
|
|
|
if ctx.args.boot:
|
|
|
|
boot(ctx)
|
|
|
|
if True not in (ctx.args.build, ctx.args.install, ctx.args.boot):
|
|
|
|
parser.print_help()
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
main()
|