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:
Olivier Gayot 2022-01-10 10:53:51 +01:00
commit f8704aa246
20 changed files with 338 additions and 266 deletions

View File

@ -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",

View File

@ -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}

View File

@ -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()

View File

@ -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

View File

@ -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

View File

@ -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))

View File

@ -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")

View File

@ -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'),

View File

@ -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())

View File

@ -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):

View File

@ -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.

View File

@ -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:

View File

@ -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:

View File

@ -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)

View File

@ -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] = \

View File

@ -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

View File

@ -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()

View File

@ -5,7 +5,3 @@ root = /custom_mnt_path
[network]
generatehosts = false
generateresolvconf = false
[user]
default = ubuntu

View File

@ -11,7 +11,3 @@ enabled = false
[network]
generatehosts = false
generateresolvconf = false
[user]
default = ubuntu

View File

@ -5,7 +5,3 @@ root = /custom_mnt_path
[network]
generatehosts = false
generateresolvconf = false
[user]
default = ubuntu