Merge branch 'main' into FR-1652
Resolve conflicts in PR 1151. Signed-off-by: Olivier Gayot <olivier.gayot@canonical.com>
This commit is contained in:
commit
f8704aa246
|
@ -260,6 +260,18 @@
|
|||
}
|
||||
]
|
||||
},
|
||||
"ubuntu-advantage": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"token": {
|
||||
"type": "string",
|
||||
"minLength": 24,
|
||||
"maxLength": 30,
|
||||
"pattern": "^C[1-9A-HJ-NP-Za-km-z]+$",
|
||||
"description": "A valid token starts with a C and is followed by 23 to 29 Base58 characters.\nSee https://pkg.go.dev/github.com/btcsuite/btcutil/base58#CheckEncode"
|
||||
}
|
||||
}
|
||||
},
|
||||
"proxy": {
|
||||
"type": [
|
||||
"string",
|
||||
|
|
|
@ -42,6 +42,9 @@ snaps:
|
|||
channel: 3.2/stable
|
||||
updates: all
|
||||
timezone: Pacific/Guam
|
||||
ubuntu-advantage:
|
||||
# Token that passes the basic format checking but is invalid (i.e. contains more than 16 bytes of random data)
|
||||
token: C1NWcZTHLteJXGVMM6YhvHDpGrhyy7
|
||||
storage:
|
||||
config:
|
||||
- {type: disk, ptable: gpt, path: /dev/vdb, wipe: superblock, preserve: false, grub_device: true, id: disk-1}
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
'''kvm-test - boot a kvm with a test iso, possibly building that test iso first
|
||||
|
||||
kvm-test --build -q --install -o --boot
|
||||
kvm-test -q --install -o --boot
|
||||
slimy build, install, overwrite existing image if it exists,
|
||||
and boot the result after install
|
||||
|
||||
|
@ -15,20 +15,23 @@ import copy
|
|||
import crypt
|
||||
import os
|
||||
import random
|
||||
import shlex
|
||||
import socket
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import yaml
|
||||
|
||||
|
||||
cfg = '''
|
||||
default_mem: '8G'
|
||||
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
|
||||
desktop: impish/jammy-desktop-amd64.iso
|
||||
desktop: jammy/jammy-desktop-amd64.iso
|
||||
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
|
||||
|
@ -37,9 +40,6 @@ iso:
|
|||
default: edge
|
||||
'''
|
||||
|
||||
sys_memory = '8G'
|
||||
|
||||
|
||||
def salted_crypt(plaintext_password):
|
||||
# match subiquity documentation
|
||||
salt = '$6$exDY1mhS4KUYCE/2'
|
||||
|
@ -51,6 +51,7 @@ class Context:
|
|||
self.config = self.load_config()
|
||||
self.args = args
|
||||
self.release = args.release
|
||||
self.default_mem = self.config.get('default_mem', '8G')
|
||||
if not self.release:
|
||||
self.release = self.config["iso"]["default"]
|
||||
iso = self.config["iso"]
|
||||
|
@ -60,10 +61,8 @@ class Context:
|
|||
except KeyError:
|
||||
pass
|
||||
self.curdir = os.getcwd()
|
||||
# self.iso = f'{self.curdir}/{self.release}-test.iso'
|
||||
self.iso = f'/tmp/kvm-test/{self.release}-test.iso'
|
||||
self.hostname = f'{self.release}-test'
|
||||
# self.target = f'{self.curdir}/{self.hostname}.img'
|
||||
self.target = f'/tmp/kvm-test/{self.hostname}.img'
|
||||
self.password = salted_crypt('ubuntu')
|
||||
self.cloudconfig = f'''\
|
||||
|
@ -80,9 +79,6 @@ autoinstall:
|
|||
password: "{self.password}"
|
||||
username: ubuntu
|
||||
'''
|
||||
# refresh-installer:
|
||||
# update: yes
|
||||
# channel: candidate
|
||||
|
||||
def merge(self, a, b):
|
||||
'''Take a pair of dictionaries, and provide the merged result.
|
||||
|
@ -137,8 +133,8 @@ parser.add_argument('--basesnap', default=None, action='store',
|
|||
parser.add_argument('--snap', default=None, action='store',
|
||||
help='inject this snap into the ISO')
|
||||
parser.add_argument('-B', '--bios', action='store_true', default=False,
|
||||
help='boot in BIOS mode')
|
||||
parser.add_argument('-c', '--channel', default=False, action='store',
|
||||
help='boot in BIOS mode (default mode is UEFI)')
|
||||
parser.add_argument('-c', '--channel', action='store',
|
||||
help='build iso with snap from channel')
|
||||
parser.add_argument('-d', '--disksize', default='12G', action='store',
|
||||
help='size of disk to create (12G default)')
|
||||
|
@ -157,14 +153,15 @@ 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')
|
||||
parser.add_argument('-t', '--this', action='store',
|
||||
help='use this iso')
|
||||
parser.add_argument('--iso', action='store', help='use this iso')
|
||||
parser.add_argument('-u', '--update', action='store',
|
||||
help='subiquity-channel argument')
|
||||
parser.add_argument('-m', '--memory', action='store',
|
||||
help='memory for VM')
|
||||
parser.add_argument('--save', action='store_true',
|
||||
help='preserve built snap')
|
||||
parser.add_argument('--reuse', action='store_true',
|
||||
help='reuse previously saved snap')
|
||||
help='reuse previously saved snap. Implies --save')
|
||||
parser.add_argument('--build', default=False, action='store_true',
|
||||
help='build test iso')
|
||||
parser.add_argument('--install', default=False, action='store_true',
|
||||
|
@ -174,44 +171,36 @@ parser.add_argument('--boot', default=False, action='store_true',
|
|||
help='boot test image')
|
||||
|
||||
|
||||
def waitstatus_to_exitcode(waitstatus):
|
||||
'''If the process exited normally (if WIFEXITED(status) is true), return
|
||||
the process exit status (return WEXITSTATUS(status)): result greater
|
||||
than or equal to 0.
|
||||
|
||||
If the process was terminated by a signal (if WIFSIGNALED(status) is
|
||||
true), return -signum where signum is the number of the signal that
|
||||
caused the process to terminate (return -WTERMSIG(status)): result less
|
||||
than 0.
|
||||
|
||||
Otherwise, raise a ValueError.'''
|
||||
|
||||
# This function is for python 3.9 compat
|
||||
|
||||
if 'waitstatus_to_exitcode' in dir(os):
|
||||
return os.waitstatus_to_exitcode(waitstatus)
|
||||
if os.WIFEXITED(waitstatus):
|
||||
return os.WEXITSTATUS(waitstatus)
|
||||
if os.WIFSIGNALED(waitstatus):
|
||||
return -os.WTERMSIG(waitstatus)
|
||||
|
||||
raise ValueError
|
||||
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
|
||||
return ctx
|
||||
|
||||
|
||||
class SubProcessFailure(Exception):
|
||||
pass
|
||||
def run(cmd):
|
||||
if isinstance(cmd, str):
|
||||
cmd_str = cmd
|
||||
cmd_array = shlex.split(cmd)
|
||||
else:
|
||||
cmd_str = shlex.join(cmd)
|
||||
cmd_array = cmd
|
||||
# semi-simulate "bash -x"
|
||||
print(f'+ {cmd_str}', file=sys.stderr)
|
||||
subprocess.run(cmd_array, check=True)
|
||||
|
||||
|
||||
def run(cmds):
|
||||
for cmd in [line.strip() for line in cmds.splitlines()]:
|
||||
if len(cmd) < 1:
|
||||
continue
|
||||
# semi-simulate "bash -x"
|
||||
print(f'+ {cmd}')
|
||||
# FIXME subprocess check
|
||||
ec = waitstatus_to_exitcode(os.system(cmd))
|
||||
if ec != 0:
|
||||
raise SubProcessFailure(f'command [{cmd}] returned [{ec}]')
|
||||
def assert_exists(path):
|
||||
if not os.path.exists(path):
|
||||
raise Exception(f'Expected file {path} not found')
|
||||
|
||||
|
||||
def remove_if_exists(path):
|
||||
if os.path.exists(path):
|
||||
os.remove(path)
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
|
@ -219,7 +208,7 @@ def delete_later(path):
|
|||
try:
|
||||
yield path
|
||||
finally:
|
||||
os.remove(path)
|
||||
remove_if_exists(path)
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
|
@ -236,16 +225,17 @@ def mounter(src, dest):
|
|||
run(f'sudo umount {dest}')
|
||||
|
||||
|
||||
def livefs_edit(ctx, *args):
|
||||
run(['sudo', 'PYTHONPATH=$LIVEFS_EDITOR', 'python3', '-m', 'livefs_edit',
|
||||
ctx.baseiso, ctx.iso, *args])
|
||||
|
||||
|
||||
def build(ctx):
|
||||
run('sudo -v')
|
||||
run(f'rm -f {ctx.iso}')
|
||||
remove_if_exists(ctx.iso)
|
||||
project = os.path.basename(os.getcwd())
|
||||
|
||||
snap_manager = noop if ctx.args.save else delete_later
|
||||
# FIXME generalize to
|
||||
# PYTHONPATH=$LIVEFS_EDITOR python3 -m livefs_edit $OLD_ISO $NEW_ISO
|
||||
# --inject-snap $SUBIQUITY_SNAP_PATH
|
||||
if project.startswith('subiquity'):
|
||||
if project == 'subiquity':
|
||||
if ctx.args.quick:
|
||||
run(f'sudo ./scripts/quick-test-this-branch.sh {ctx.baseiso} \
|
||||
{ctx.iso}')
|
||||
|
@ -259,38 +249,27 @@ def build(ctx):
|
|||
run(f'sudo ./scripts/inject-subiquity-snap.sh {ctx.baseiso} \
|
||||
{ctx.args.snap} {ctx.iso}')
|
||||
elif ctx.args.channel:
|
||||
run(f'sudo PYTHONPATH=$LIVEFS_EDITOR python3 -m livefs_edit \
|
||||
{ctx.baseiso} {ctx.iso} \
|
||||
--add-snap-from-store subiquity {ctx.args.channel}')
|
||||
livefs_edit(ctx, '--add-snap-from-store', 'core20', 'stable',
|
||||
'--add-snap-from-store', 'subiquity',
|
||||
ctx.args.channel)
|
||||
else:
|
||||
with snap_manager('subiquity_test.snap') as snap:
|
||||
if not ctx.args.reuse:
|
||||
run(f'''
|
||||
snapcraft clean --use-lxd
|
||||
snapcraft snap --use-lxd --output {snap}
|
||||
''')
|
||||
run(f'''
|
||||
test -f {snap}
|
||||
sudo PYTHONPATH=$LIVEFS_EDITOR python3 -m livefs_edit \
|
||||
{ctx.baseiso} {ctx.iso} \
|
||||
--add-snap-from-store core20 stable \
|
||||
--inject-snap {snap}
|
||||
''')
|
||||
# sudo ./scripts/inject-subiquity-snap.sh {ctx.baseiso} \
|
||||
# {snap} {ctx.iso}
|
||||
run('snapcraft clean --use-lxd')
|
||||
run(f'snapcraft snap --use-lxd --output {snap}')
|
||||
assert_exists(snap)
|
||||
livefs_edit(ctx, '--add-snap-from-store', 'core20', 'stable',
|
||||
'--inject-snap', snap)
|
||||
elif project == 'ubuntu-desktop-installer':
|
||||
with snap_manager('udi_test.snap') as snap:
|
||||
run(f'''
|
||||
snapcraft clean --use-lxd
|
||||
snapcraft snap --use-lxd --output {snap}
|
||||
test -f {snap}
|
||||
sudo ./scripts/inject-snap {ctx.baseiso} \
|
||||
{ctx.iso} {snap}
|
||||
''')
|
||||
run('snapcraft clean --use-lxd')
|
||||
run(f'snapcraft snap --use-lxd --output {snap}')
|
||||
assert_exists(snap)
|
||||
run(f'sudo ./scripts/inject-snap {ctx.baseiso} {ctx.iso} {snap}')
|
||||
else:
|
||||
raise Exception(f'do not know how to build {project}')
|
||||
|
||||
run(f'test -f {ctx.iso}')
|
||||
assert_exists(ctx.iso)
|
||||
|
||||
|
||||
def write(dest, data):
|
||||
|
@ -315,15 +294,12 @@ def drive(path, format='qcow2'):
|
|||
kwargs = []
|
||||
serial = None
|
||||
cparam = 'writethrough'
|
||||
# if cache == False: cparam = 'none'
|
||||
# serial doesn't work..
|
||||
# serial = str(int(random.random() * 100000000)).zfill(8)
|
||||
kwargs += [f'file={path}']
|
||||
kwargs += [f'format={format}']
|
||||
kwargs += [f'cache={cparam}']
|
||||
kwargs += ['if=virtio']
|
||||
kwargs.append(f'file={path}')
|
||||
kwargs.append(f'format={format}')
|
||||
kwargs.append(f'cache={cparam}')
|
||||
kwargs.append('if=virtio')
|
||||
if serial:
|
||||
kwargs += [f'serial={serial}']
|
||||
kwargs.append(f'serial={serial}')
|
||||
|
||||
return '-drive ' + ','.join(kwargs)
|
||||
|
||||
|
@ -346,15 +322,15 @@ class PortFinder:
|
|||
def nets(ctx):
|
||||
ports = PortFinder()
|
||||
|
||||
ret = []
|
||||
if ctx.args.nets > 0:
|
||||
ret = []
|
||||
for _ in range(ctx.args.nets):
|
||||
port = ports.get()
|
||||
ret += ['-nic',
|
||||
'user,model=virtio-net-pci,' +
|
||||
f'hostfwd=tcp::{port}-:22'] # ,restrict=on']
|
||||
ret.extend(('-nic',
|
||||
'user,model=virtio-net-pci,' +
|
||||
f'hostfwd=tcp::{port}-:22'))
|
||||
else:
|
||||
ret += ['-nic', 'none']
|
||||
ret = ['-nic', 'none']
|
||||
return ret
|
||||
|
||||
|
||||
|
@ -362,12 +338,12 @@ def bios(ctx):
|
|||
ret = []
|
||||
# https://help.ubuntu.com/community/UEFI
|
||||
if not ctx.args.bios:
|
||||
ret += ['-bios', '/usr/share/qemu/OVMF.fd']
|
||||
ret = ['-bios', '/usr/share/qemu/OVMF.fd']
|
||||
return ret
|
||||
|
||||
|
||||
def memory(ctx):
|
||||
return ['-m', str(sys_memory)]
|
||||
return ['-m', ctx.args.memory or ctx.default_mem]
|
||||
|
||||
|
||||
def kvm_common(ctx):
|
||||
|
@ -381,35 +357,6 @@ def kvm_common(ctx):
|
|||
return ret
|
||||
|
||||
|
||||
def grub_get_extra_args(mntdir):
|
||||
# The inject-snap process of livefs-edit adds a new layer squash
|
||||
# that must be in place, or the injected snap isn't there.
|
||||
# We don't want to hardcode the current value, because that layer
|
||||
# isn't there if a base iso is being used.
|
||||
# Parse the args out of grub.cfg and include them with ours.
|
||||
cfgpath = f'{mntdir}/boot/grub/grub.cfg'
|
||||
ret = []
|
||||
try:
|
||||
with open(cfgpath, 'r') as fp:
|
||||
for line in fp.readlines():
|
||||
chunks = line.strip().split('\t')
|
||||
# ['linux', '/casper/vmlinuz ---']
|
||||
# layerfs-path=a.b.c.d.e.squashfs
|
||||
if not chunks or not chunks[0] == 'linux':
|
||||
continue
|
||||
subchunks = chunks[1].split(' ')
|
||||
if not subchunks or not subchunks[0] == '/casper/vmlinuz':
|
||||
continue
|
||||
for sc in subchunks[1:]:
|
||||
if sc != '---':
|
||||
ret.append(sc)
|
||||
break
|
||||
# breakpoint()
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
return ret
|
||||
|
||||
|
||||
def get_initrd(mntdir):
|
||||
for initrd in ('initrd', 'initrd.lz', 'initrd.lz4'):
|
||||
path = f'{mntdir}/casper/{initrd}'
|
||||
|
@ -422,8 +369,8 @@ def install(ctx):
|
|||
if os.path.exists(ctx.target):
|
||||
if ctx.args.overwrite:
|
||||
os.remove(ctx.target)
|
||||
|
||||
run('sudo -v')
|
||||
else:
|
||||
raise Exception('refusing to overwrite existing image')
|
||||
|
||||
with tempfile.TemporaryDirectory() as tempdir:
|
||||
mntdir = f'{tempdir}/mnt'
|
||||
|
@ -432,47 +379,43 @@ def install(ctx):
|
|||
|
||||
kvm = kvm_common(ctx)
|
||||
|
||||
if ctx.args.this:
|
||||
iso = ctx.args.this
|
||||
if ctx.args.iso:
|
||||
iso = ctx.args.iso
|
||||
elif ctx.args.base:
|
||||
iso = ctx.baseiso
|
||||
else:
|
||||
iso = ctx.iso
|
||||
|
||||
kvm += ['-cdrom', iso]
|
||||
kvm.extend(('-cdrom', iso))
|
||||
|
||||
if ctx.args.serial:
|
||||
kvm += ['-nographic']
|
||||
appends += ['console=ttyS0']
|
||||
kvm.append('-nographic')
|
||||
appends.append('console=ttyS0')
|
||||
|
||||
if ctx.args.autoinstall or ctx.args.autoinstall_file:
|
||||
if ctx.args.autoinstall_file:
|
||||
ctx.cloudconfig = ctx.args.autoinstall_file.read()
|
||||
kvm += [drive(create_seed(ctx.cloudconfig, tempdir), 'raw')]
|
||||
appends += ['autoinstall']
|
||||
kvm.append(drive(create_seed(ctx.cloudconfig, tempdir), 'raw'))
|
||||
appends.append('autoinstall')
|
||||
|
||||
if ctx.args.update:
|
||||
appends += ['subiquity-channel=candidate']
|
||||
appends.append('subiquity-channel=' + ctx.args.update)
|
||||
|
||||
kvm += [drive(ctx.target)]
|
||||
kvm.append(drive(ctx.target))
|
||||
if not os.path.exists(ctx.target) or ctx.args.overwrite:
|
||||
run(f'qemu-img create -f qcow2 {ctx.target} {ctx.args.disksize}')
|
||||
|
||||
# drive2 = f'{ctx.curdir}/drive2.img'
|
||||
|
||||
# appends += ['subiquity-channel=edge']
|
||||
|
||||
with mounter(iso, mntdir):
|
||||
if len(appends) > 0:
|
||||
appends += grub_get_extra_args(mntdir)
|
||||
if len(appends) > 0:
|
||||
with mounter(iso, mntdir):
|
||||
# if we're passing kernel args, we need to manually specify
|
||||
# kernel / initrd
|
||||
kvm += ['-kernel', f'{mntdir}/casper/vmlinuz']
|
||||
kvm += ['-initrd', get_initrd(mntdir)]
|
||||
kvm.extend(('-kernel', f'{mntdir}/casper/vmlinuz'))
|
||||
kvm.extend(('-initrd', get_initrd(mntdir)))
|
||||
toappend = ' '.join(appends)
|
||||
kvm += ['-append', f'"{toappend}"']
|
||||
|
||||
run(' '.join(kvm))
|
||||
kvm.extend(('-append', f'"{toappend}"'))
|
||||
run(kvm)
|
||||
else:
|
||||
run(kvm)
|
||||
|
||||
|
||||
def boot(ctx):
|
||||
|
@ -481,21 +424,17 @@ def boot(ctx):
|
|||
target = ctx.args.img
|
||||
|
||||
kvm = kvm_common(ctx)
|
||||
kvm += [drive(target)]
|
||||
run(' '.join(kvm))
|
||||
kvm.append(target)
|
||||
run(kvm)
|
||||
|
||||
|
||||
def help(ctx):
|
||||
def help():
|
||||
parser.print_usage()
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def cloud(ctx):
|
||||
print(ctx.cloudconfig)
|
||||
|
||||
|
||||
try:
|
||||
ctx = Context(parser.parse_args())
|
||||
ctx = parse_args()
|
||||
except TypeError:
|
||||
help()
|
||||
|
||||
|
|
|
@ -28,19 +28,30 @@ validate () {
|
|||
netplan generate --root .subiquity
|
||||
elif [ "${mode}" = "system_setup" ]; then
|
||||
setup_mode="$2"
|
||||
launcher_cmds=".subiquity/run/launcher-command"
|
||||
echo "system setup validation for $setup_mode"
|
||||
echo "checking launcher-status"
|
||||
[ -d ".subiquity/run/subiquity/" ] || (echo "run/subiquity/ dir not created for status"; exit 1)
|
||||
[ -e ".subiquity/run/subiquity/launcher-status" ] || (echo "run/subiquity/launcher-status not created"; exit 1)
|
||||
echo "checking ${launcher_cmds}"
|
||||
if [ ! -f ${launcher_cmds} ]; then
|
||||
echo "Expected launcher commands to be written to the file."
|
||||
exit 1
|
||||
elif [ -z "$(grep action ${launcher_cmds})" ] && [ "${setup_mode}" != "autoinstall-no-shutdown" ]; then
|
||||
echo "Expected action to be set in launcher commands."
|
||||
exit 1
|
||||
elif [ -z "$(grep defaultUid ${launcher_cmds})" ] && [ "${setup_mode}" != "answers-reconf" ]; then
|
||||
echo "Expected defaultUid to be set in launcher commands."
|
||||
exit 1
|
||||
else
|
||||
cat ${launcher_cmds}
|
||||
fi
|
||||
expected_status="reboot"
|
||||
if [ "${setup_mode}" = "autoinstall-full" ]; then
|
||||
expected_status="shutdown"
|
||||
elif [ "${setup_mode}" = "autoinstall-no-shutdown" ]; then
|
||||
expected_status="complete"
|
||||
expected_status=""
|
||||
fi
|
||||
result_status="$(cat .subiquity/run/subiquity/launcher-status)"
|
||||
result_status="$(cat ${launcher_cmds} | grep action | cut -d = -f 2)"
|
||||
if [ "${result_status}" != "${expected_status}" ]; then
|
||||
echo "incorrect run/subiquity/launcher-status: expect ${expected_status}, got ${result_status}"
|
||||
echo "incorrect ${launcher_cmds}: expect ${expected_status}, got ${result_status}"
|
||||
exit 1
|
||||
fi
|
||||
echo "checking generated config"
|
||||
|
@ -52,7 +63,7 @@ validate () {
|
|||
for file in system_setup/tests/golden/${setup_mode}/*.conf; do
|
||||
filename=$(basename ${file})
|
||||
conf_filepath=".subiquity/etc/${filename}"
|
||||
diff -Nup "${file}" "${conf_filepath}" || exit 1
|
||||
diff -NBup "${file}" "${conf_filepath}" || exit 1
|
||||
done
|
||||
if [ "${setup_mode}" != "answers-reconf" ]; then
|
||||
echo "checking user created"
|
||||
|
@ -78,8 +89,9 @@ validate () {
|
|||
echo "user not assigned with the expected group sudo"
|
||||
exit 1
|
||||
fi
|
||||
lang="$(grep -Eo 'LANG="([^.@ _]+)' .subiquity/etc/default/locale | cut -d \" -f 2)"
|
||||
if [ -z "$( ls .subiquity/var/cache/apt/archives/) | grep $lang" ] ; then
|
||||
# Extract value of the LANG variable from etc/default/locale (with or without quotes)
|
||||
lang="$(grep -Eo 'LANG=([^.@ _]+)' .subiquity/etc/default/locale | cut -d= -f 2- | cut -d\" -f 2-)"
|
||||
if ! ls .subiquity/var/cache/apt/archives/ | grep --fixed-strings --quiet -- "$lang"; then
|
||||
echo "expected $lang language packs in directory var/cache/apt/archives/"
|
||||
exit 1
|
||||
fi
|
||||
|
@ -161,6 +173,7 @@ python3 scripts/check-yaml-fields.py .subiquity/var/log/installer/subiquity-curt
|
|||
python3 scripts/check-yaml-fields.py <(python3 scripts/check-yaml-fields.py .subiquity/etc/cloud/cloud.cfg.d/99-installer.cfg datasource.None.userdata_raw) \
|
||||
locale='"en_GB.UTF-8"' \
|
||||
timezone='"Pacific/Guam"' \
|
||||
ubuntu_advantage.token='"C1NWcZTHLteJXGVMM6YhvHDpGrhyy7"' \
|
||||
'snap.commands=[snap install --channel=3.2/stable etcd]'
|
||||
grep -q 'finish: subiquity/Install/install/postinstall/install_package1: SUCCESS: installing package1' \
|
||||
.subiquity/subiquity-server-debug.log
|
||||
|
|
|
@ -8,7 +8,8 @@ sudo apt install -y zsync xorriso isolinux
|
|||
|
||||
snapcraft snap --output subiquity_test.snap
|
||||
urlbase=http://cdimage.ubuntu.com/ubuntu-server/daily-live/current
|
||||
isoname=$(distro-info -d)-live-server-$(dpkg --print-architecture).iso
|
||||
distroname=$(distro-info -d)
|
||||
isoname="${distroname}"-live-server-$(dpkg --print-architecture).iso
|
||||
zsync ${urlbase}/${isoname}.zsync
|
||||
sudo ./scripts/inject-subiquity-snap.sh ${isoname} subiquity_test.snap custom.iso
|
||||
|
||||
|
|
|
@ -217,7 +217,7 @@ class FilesystemController(SubiquityTuiController, FilesystemManipulator):
|
|||
if self.model.bootloader == Bootloader.PREP:
|
||||
self.supports_resilient_boot = False
|
||||
else:
|
||||
release = lsb_release()['release']
|
||||
release = lsb_release(dry_run=self.app.opts.dry_run)['release']
|
||||
self.supports_resilient_boot = release >= '20.04'
|
||||
self.ui.set_body(FilesystemView(self.model, self))
|
||||
|
||||
|
|
|
@ -16,7 +16,6 @@
|
|||
"""
|
||||
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
from subiquitycore.async_helpers import schedule_task
|
||||
|
||||
|
@ -56,11 +55,9 @@ class UbuntuAdvantageController(SubiquityTuiController):
|
|||
|
||||
async def make_ui(self) -> UbuntuAdvantageView:
|
||||
""" Generate the UI, based on the data provided by the model. """
|
||||
path_lsb_release: Optional[str] = None
|
||||
if self.app.opts.dry_run:
|
||||
# In dry-run mode, always pretend to be on LTS
|
||||
path_lsb_release = "examples/lsb-release-focal"
|
||||
if "LTS" not in lsb_release(path_lsb_release)["description"]:
|
||||
|
||||
dry_run: bool = self.app.opts.dry_run
|
||||
if "LTS" not in lsb_release(dry_run=dry_run)["description"]:
|
||||
await self.endpoint.skip.POST()
|
||||
raise Skip("Not running LTS version")
|
||||
|
||||
|
|
|
@ -207,7 +207,7 @@ class AptConfigurer:
|
|||
if os.path.exists(proxy_path):
|
||||
os.unlink(proxy_path)
|
||||
|
||||
codename = lsb_release()['codename']
|
||||
codename = lsb_release(dry_run=self.app.opts.dry_run)['codename']
|
||||
|
||||
write_file(
|
||||
self.install_tree.p('etc/apt/sources.list'),
|
||||
|
|
|
@ -448,7 +448,7 @@ class FilesystemController(SubiquityController, FilesystemManipulator):
|
|||
if self.model.bootloader == Bootloader.PREP:
|
||||
self.supports_resilient_boot = False
|
||||
else:
|
||||
release = lsb_release()['release']
|
||||
release = lsb_release(dry_run=self.app.opts.dry_run)['release']
|
||||
self.supports_resilient_boot = release >= '20.04'
|
||||
self._start_task = schedule_task(self._start())
|
||||
|
||||
|
|
|
@ -70,8 +70,10 @@ class KernelController(NonInteractiveController):
|
|||
# Should check this package exists really but
|
||||
# that's a bit tricky until we get cleverer about
|
||||
# the apt config in general.
|
||||
dry_run: bool = self.app.opts.dry_run
|
||||
package = 'linux-{flavor}-{release}'.format(
|
||||
flavor=flavor, release=lsb_release()['release'])
|
||||
flavor=flavor,
|
||||
release=lsb_release(dry_run=dry_run)['release'])
|
||||
self.model.metapkg_name = package
|
||||
|
||||
def make_autoinstall(self):
|
||||
|
|
|
@ -22,6 +22,10 @@ from subiquity.server.controller import SubiquityController
|
|||
|
||||
log = logging.getLogger("subiquity.server.controllers.ubuntu_advantage")
|
||||
|
||||
TOKEN_DESC = """\
|
||||
A valid token starts with a C and is followed by 23 to 29 Base58 characters.
|
||||
See https://pkg.go.dev/github.com/btcsuite/btcutil/base58#CheckEncode"""
|
||||
|
||||
|
||||
class UbuntuAdvantageController(SubiquityController):
|
||||
""" Represent the server-side Ubuntu Advantage controller. """
|
||||
|
@ -29,6 +33,33 @@ class UbuntuAdvantageController(SubiquityController):
|
|||
endpoint = API.ubuntu_advantage
|
||||
|
||||
model_name = "ubuntu_advantage"
|
||||
autoinstall_key = "ubuntu-advantage"
|
||||
autoinstall_schema = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"token": {
|
||||
"type": "string",
|
||||
"minLength": 24,
|
||||
"maxLength": 30,
|
||||
"pattern": "^C[1-9A-HJ-NP-Za-km-z]+$",
|
||||
"description": TOKEN_DESC,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
def load_autoinstall_data(self, data: dict) -> None:
|
||||
""" Load autoinstall data and update the model. """
|
||||
if data is None:
|
||||
return
|
||||
self.model.token = data.get("token", "")
|
||||
|
||||
def make_autoinstall(self) -> dict:
|
||||
""" Return a dictionary that can be used as an autoinstall snippet for
|
||||
Ubuntu Advantage.
|
||||
"""
|
||||
return {
|
||||
"token": self.model.token
|
||||
}
|
||||
|
||||
def serialize(self) -> str:
|
||||
""" Save the current state of the model so it can be loaded later.
|
||||
|
|
|
@ -441,7 +441,7 @@ class HelpMenu(PopUpLauncher):
|
|||
self.app.add_global_overlay(stretchy)
|
||||
|
||||
def about(self, sender=None):
|
||||
info = lsb_release()
|
||||
info = lsb_release(dry_run=self.app.opts.dry_run)
|
||||
if 'LTS' in info['description']:
|
||||
template = _(ABOUT_INSTALLER_LTS)
|
||||
else:
|
||||
|
|
|
@ -2,13 +2,17 @@
|
|||
import shlex
|
||||
|
||||
LSB_RELEASE_FILE = "/etc/lsb-release"
|
||||
LSB_RELEASE_EXAMPLE = "examples/lsb-release-focal"
|
||||
|
||||
|
||||
def lsb_release(path=None):
|
||||
def lsb_release(path=None, dry_run: bool = False):
|
||||
"""return a dictionary of values from /etc/lsb-release.
|
||||
keys are lower case with DISTRIB_ prefix removed."""
|
||||
if dry_run and path is not None:
|
||||
raise ValueError("Both dry_run and path are specified.")
|
||||
|
||||
if path is None:
|
||||
path = LSB_RELEASE_FILE
|
||||
path = LSB_RELEASE_EXAMPLE if dry_run else LSB_RELEASE_FILE
|
||||
|
||||
ret = {}
|
||||
try:
|
||||
|
|
|
@ -0,0 +1,58 @@
|
|||
# Copyright 2021 Canonical, Ltd.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as
|
||||
# published by the Free Software Foundation, either version 3 of the
|
||||
# License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import unittest
|
||||
from unittest.mock import patch, mock_open
|
||||
|
||||
from subiquitycore.lsb_release import lsb_release
|
||||
|
||||
|
||||
class TestLSBRelease(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.target = "subiquitycore.lsb_release.open"
|
||||
|
||||
def test_lsb_release(self):
|
||||
lsb_str = """
|
||||
DISTRIB_ID=Ubuntu
|
||||
DISTRIB_RELEASE=21.10
|
||||
DISTRIB_CODENAME=impish
|
||||
DISTRIB_DESCRIPTION="Ubuntu 21.10"
|
||||
"""
|
||||
|
||||
with patch(self.target, mock_open(read_data=lsb_str)) as patched:
|
||||
distro = lsb_release(path="dummy")
|
||||
patched.assert_called_once_with("dummy", "r")
|
||||
self.assertEqual(distro["id"], "Ubuntu")
|
||||
self.assertEqual(distro["release"], "21.10")
|
||||
self.assertEqual(distro["codename"], "impish")
|
||||
self.assertEqual(distro["description"], "Ubuntu 21.10")
|
||||
|
||||
def test_lsb_release_inexistent(self):
|
||||
with patch(self.target, side_effect=FileNotFoundError):
|
||||
self.assertEqual(lsb_release("/inexistent"), {})
|
||||
|
||||
def test_lsb_release_default(self):
|
||||
with patch(self.target, side_effect=FileNotFoundError) as patched:
|
||||
lsb_release(path=None)
|
||||
patched.assert_called_once_with("/etc/lsb-release", "r")
|
||||
|
||||
def test_lsb_release_dry_run(self):
|
||||
with patch(self.target, side_effect=FileNotFoundError) as patched:
|
||||
lsb_release(dry_run=True)
|
||||
patched.assert_called_once_with("examples/lsb-release-focal", "r")
|
||||
|
||||
def test_lsb_release_mutually_exclusive(self):
|
||||
with self.assertRaises(ValueError):
|
||||
lsb_release(path="dummy", dry_run=True)
|
|
@ -111,7 +111,7 @@ def default_loader(is_advanced=False):
|
|||
return data
|
||||
|
||||
|
||||
def wsl_config_update(config_class, root_dir, default_user=None):
|
||||
def wsl_config_update(config_class, root_dir):
|
||||
"""
|
||||
This update the configuration file for the given class.
|
||||
|
||||
|
@ -163,11 +163,6 @@ def wsl_config_update(config_class, root_dir, default_user=None):
|
|||
config.add_section(config_section)
|
||||
config[config_section][config_setting] = config_value
|
||||
|
||||
if config_type == "wsl" and default_user is not None:
|
||||
if "user" not in config:
|
||||
config.add_section("user")
|
||||
config["user"]["default"] = default_user
|
||||
|
||||
# sort config in ascii order
|
||||
for section in config._sections:
|
||||
config._sections[section] = \
|
||||
|
|
|
@ -32,6 +32,7 @@ log = logging.getLogger("system_setup.server.controllers.configure")
|
|||
|
||||
|
||||
class ConfigureController(SubiquityController):
|
||||
default_uid = 0
|
||||
|
||||
def __init__(self, app):
|
||||
super().__init__(app)
|
||||
|
@ -265,6 +266,81 @@ class ConfigureController(SubiquityController):
|
|||
log.error("Failed to run locale activation commands.")
|
||||
return
|
||||
|
||||
def __query_uid(self, etc_dir, username):
|
||||
""" Finds the UID of username in etc_dir/passwd file. """
|
||||
uid = None
|
||||
with open(os.path.join(etc_dir, "passwd")) as f:
|
||||
for line in f:
|
||||
tokens = line.split(":")
|
||||
if username == tokens[0]:
|
||||
if len(tokens) != 7:
|
||||
raise Exception("Invalid passwd entry")
|
||||
|
||||
uid = int(tokens[2])
|
||||
break
|
||||
|
||||
return uid
|
||||
|
||||
async def _create_user(self, root_dir):
|
||||
""" Helper method to create the user from the identity model
|
||||
and store it's UID. """
|
||||
wsl_id = self.model.identity.user
|
||||
username = wsl_id.username
|
||||
create_user_base = []
|
||||
assign_grp_base = []
|
||||
usergroups_list = get_users_and_groups()
|
||||
if self.app.opts.dry_run:
|
||||
log.debug("creating a mock-up env for user %s", username)
|
||||
# creating folders and files for dryrun
|
||||
etc_dir = os.path.join(root_dir, "etc")
|
||||
os.makedirs(etc_dir, exist_ok=True)
|
||||
home_dir = os.path.join(root_dir, "home")
|
||||
os.makedirs(home_dir, exist_ok=True)
|
||||
pseudo_files = ["passwd", "shadow", "gshadow", "group",
|
||||
"subgid", "subuid"]
|
||||
for file in pseudo_files:
|
||||
filepath = os.path.join(etc_dir, file)
|
||||
open(filepath, "a").close()
|
||||
|
||||
# mimic groupadd
|
||||
group_id = 1000
|
||||
for group in usergroups_list:
|
||||
group_filepath = os.path.join(etc_dir, "group")
|
||||
gshadow_filepath = os.path.join(etc_dir, "gshadow")
|
||||
shutil.copy(group_filepath,
|
||||
"{}-".format(group_filepath))
|
||||
with open(group_filepath, "a") as group_file:
|
||||
group_file.write("{}:x:{}:\n".format(group, group_id))
|
||||
group_id += 1
|
||||
shutil.copy(gshadow_filepath,
|
||||
"{}-".format(gshadow_filepath))
|
||||
with open(gshadow_filepath, "a") as gshadow_file:
|
||||
gshadow_file.write("{}:!::\n".format(group))
|
||||
|
||||
create_user_base = ["-P", root_dir]
|
||||
assign_grp_base = ["-P", root_dir]
|
||||
|
||||
create_user_cmd = ["useradd"] + create_user_base + \
|
||||
["-m", "-s", "/bin/bash", "-c", wsl_id.realname,
|
||||
"-p", wsl_id.password, username]
|
||||
assign_grp_cmd = ["usermod"] + assign_grp_base + \
|
||||
["-a", "-G", ",".join(usergroups_list), username]
|
||||
|
||||
create_user_proc = await arun_command(create_user_cmd)
|
||||
if create_user_proc.returncode != 0:
|
||||
raise Exception("Failed to create user %s: %s"
|
||||
% (username, create_user_proc.stderr))
|
||||
log.debug("created user %s", username)
|
||||
|
||||
self.default_uid = self.__query_uid(etc_dir, username)
|
||||
if self.default_uid is None:
|
||||
log.error("Could not retrieve %s UID", username)
|
||||
|
||||
assign_grp_proc = await arun_command(assign_grp_cmd)
|
||||
if assign_grp_proc.returncode != 0:
|
||||
raise Exception(("Failed to assign group to user %s: %s")
|
||||
% (username, assign_grp_proc.stderr))
|
||||
|
||||
@with_context(
|
||||
description="final system configuration", level="INFO",
|
||||
childlevel="DEBUG")
|
||||
|
@ -287,68 +363,12 @@ class ConfigureController(SubiquityController):
|
|||
|
||||
self.app.update_state(ApplicationState.POST_RUNNING)
|
||||
|
||||
dryrun = self.app.opts.dry_run
|
||||
variant = self.app.variant
|
||||
root_dir = self.model.root
|
||||
username = None
|
||||
|
||||
if variant == "wsl_setup":
|
||||
wsl_id = self.model.identity.user
|
||||
username = wsl_id.username
|
||||
create_user_base = []
|
||||
assign_grp_base = []
|
||||
usergroups_list = get_users_and_groups()
|
||||
await self._create_user(root_dir)
|
||||
lang = self.model.locale.selected_language
|
||||
if dryrun:
|
||||
log.debug("creating a mock-up env for user %s", username)
|
||||
# creating folders and files for dryrun
|
||||
etc_dir = os.path.join(root_dir, "etc")
|
||||
os.makedirs(etc_dir, exist_ok=True)
|
||||
home_dir = os.path.join(root_dir, "home")
|
||||
os.makedirs(home_dir, exist_ok=True)
|
||||
pseudo_files = ["passwd", "shadow", "gshadow", "group",
|
||||
"subgid", "subuid"]
|
||||
for file in pseudo_files:
|
||||
filepath = os.path.join(etc_dir, file)
|
||||
open(filepath, "a").close()
|
||||
|
||||
# mimic groupadd
|
||||
group_id = 1000
|
||||
for group in usergroups_list:
|
||||
group_filepath = os.path.join(etc_dir, "group")
|
||||
gshadow_filepath = os.path.join(etc_dir, "gshadow")
|
||||
shutil.copy(group_filepath,
|
||||
"{}-".format(group_filepath))
|
||||
with open(group_filepath, "a") as group_file:
|
||||
group_file.write("{}:x:{}:\n".
|
||||
format(group, group_id))
|
||||
group_id += 1
|
||||
shutil.copy(gshadow_filepath,
|
||||
"{}-".format(gshadow_filepath))
|
||||
with open(gshadow_filepath, "a") as gshadow_file:
|
||||
gshadow_file.write("{}:!::\n".format(group))
|
||||
|
||||
create_user_base = ["-P", root_dir]
|
||||
assign_grp_base = ["-P", root_dir]
|
||||
|
||||
create_user_cmd = ["useradd"] + create_user_base + \
|
||||
["-m", "-s", "/bin/bash",
|
||||
"-c", wsl_id.realname,
|
||||
"-p", wsl_id.password, username]
|
||||
assign_grp_cmd = ["usermod"] + assign_grp_base + \
|
||||
["-a", "-G", ",".join(usergroups_list),
|
||||
username]
|
||||
|
||||
create_user_proc = await arun_command(create_user_cmd)
|
||||
if create_user_proc.returncode != 0:
|
||||
raise Exception("Failed to create user %s: %s"
|
||||
% (username, create_user_proc.stderr))
|
||||
log.debug("created user %s", username)
|
||||
|
||||
assign_grp_proc = await arun_command(assign_grp_cmd)
|
||||
if assign_grp_proc.returncode != 0:
|
||||
raise Exception(("Failed to assign group to user %s: %s")
|
||||
% (username, assign_grp_proc.stderr))
|
||||
|
||||
await self.apply_locale(lang)
|
||||
|
||||
else:
|
||||
|
@ -361,8 +381,7 @@ class ConfigureController(SubiquityController):
|
|||
wsl_config_update(self.model.wslconfadvanced.wslconfadvanced,
|
||||
root_dir)
|
||||
|
||||
wsl_config_update(self.model.wslconfbase.wslconfbase, root_dir,
|
||||
default_user=username)
|
||||
wsl_config_update(self.model.wslconfbase.wslconfbase, root_dir)
|
||||
|
||||
self.app.update_state(ApplicationState.DONE)
|
||||
|
||||
|
@ -375,3 +394,7 @@ class ConfigureController(SubiquityController):
|
|||
def stop_uu(self):
|
||||
# This is a no-op to allow Shutdown controller to depend on this one
|
||||
pass
|
||||
|
||||
# Allows passing the recently created user UID to the Shutdown controller.
|
||||
def get_default_uid(self):
|
||||
return self.default_uid
|
||||
|
|
|
@ -50,18 +50,24 @@ class SetupShutdownController(ShutdownController):
|
|||
@with_context(description='mode={self.mode.name}')
|
||||
def shutdown(self, context):
|
||||
self.shuttingdown_event.set()
|
||||
launcher_status = "complete"
|
||||
comments = ["# This file was auto generated by system-setup.",
|
||||
"# Don't edit it. It will be overwritten at next run."]
|
||||
launcher_status = []
|
||||
|
||||
if self.mode == ShutdownMode.REBOOT:
|
||||
log.debug("rebooting")
|
||||
launcher_status = "reboot"
|
||||
log.debug("Setting launcher for reboot")
|
||||
launcher_status += ["action=reboot"]
|
||||
elif self.mode == ShutdownMode.POWEROFF:
|
||||
log.debug("Shutting down")
|
||||
launcher_status = "shutdown"
|
||||
log.debug("Setting launcher for shut down")
|
||||
launcher_status += ["action=shutdown"]
|
||||
|
||||
default_uid = self.app.controllers.Install.get_default_uid()
|
||||
if default_uid is not None and default_uid != 0:
|
||||
launcher_status += ["defaultUid={}".format(default_uid)]
|
||||
|
||||
if len(launcher_status) > 0:
|
||||
status_file = os.path.join(self.root_dir, "run/launcher-command")
|
||||
with open(status_file, "w+") as f:
|
||||
f.write("\n".join(comments + launcher_status))
|
||||
|
||||
subiquity_rundir = os.path.join(self.root_dir, "run", "subiquity")
|
||||
os.makedirs(subiquity_rundir, exist_ok=True)
|
||||
lau_status_file = os.path.join(subiquity_rundir, "launcher-status")
|
||||
with open(lau_status_file, "w+") as f:
|
||||
f.write(launcher_status)
|
||||
self.app.exit()
|
||||
|
|
|
@ -5,7 +5,3 @@ root = /custom_mnt_path
|
|||
[network]
|
||||
generatehosts = false
|
||||
generateresolvconf = false
|
||||
|
||||
[user]
|
||||
default = ubuntu
|
||||
|
||||
|
|
|
@ -11,7 +11,3 @@ enabled = false
|
|||
[network]
|
||||
generatehosts = false
|
||||
generateresolvconf = false
|
||||
|
||||
[user]
|
||||
default = ubuntu
|
||||
|
||||
|
|
|
@ -5,7 +5,3 @@ root = /custom_mnt_path
|
|||
[network]
|
||||
generatehosts = false
|
||||
generateresolvconf = false
|
||||
|
||||
[user]
|
||||
default = ubuntu
|
||||
|
||||
|
|
Loading…
Reference in New Issue