From 535a92dad40923d469100a522b3e5666d2f02a09 Mon Sep 17 00:00:00 2001 From: Olivier Gayot Date: Fri, 21 Jan 2022 12:11:38 +0100 Subject: [PATCH 1/5] Add script to validate autoinstall data against JSON schema The script can be used to validate autoinstall user data against the schema. By default, it expects a #cloud-config header and the user-data to be under the autoinstall: key. By passing the --no-expect-cloudconfig, it validates the data directly. We can use this option to validate the YAML files under examples/autoinstall-*.yaml Signed-off-by: Olivier Gayot --- scripts/validate-autoinstall-user-data.py | 69 +++++++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100755 scripts/validate-autoinstall-user-data.py diff --git a/scripts/validate-autoinstall-user-data.py b/scripts/validate-autoinstall-user-data.py new file mode 100755 index 00000000..0f82e299 --- /dev/null +++ b/scripts/validate-autoinstall-user-data.py @@ -0,0 +1,69 @@ +#!/usr/bin/env python3 + +# Copyright 2022 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 . + +""" Entry-point to validate autoinstall-user-data against schema. +By default, we are expecting the autoinstall user-data to be wrapped in a cloud +config format: + +#cloud-config +autoinstall: + + +To validate the user-data directly, you can pass the --no-expect-cloudconfig +switch. +""" + +import argparse +import json + +import jsonschema +import yaml + + +def main() -> None: + """ Entry point. """ + parser = argparse.ArgumentParser() + + parser.add_argument("--json-schema", + help="Path to the JSON schema", + type=argparse.FileType("r"), + default="autoinstall-schema.json") + parser.add_argument("input", nargs="?", + help="Path to the user data instead of stdin", + type=argparse.FileType("r"), + default="-") + parser.add_argument("--no-expect-cloudconfig", dest="expect-cloudconfig", + action="store_false", + help="Assume the data is not wrapped in cloud-config.", + default=True) + + args = vars(parser.parse_args()) + + if args["expect-cloudconfig"]: + assert args["input"].readline() == "#cloud-config\n" + get_autoinstall_data = lambda data: data["autoinstall"] + else: + get_autoinstall_data = lambda data: data + + data = yaml.safe_load(args["input"]) + + jsonschema.validate(get_autoinstall_data(data), + json.load(args["json_schema"])) + + +if __name__ == "__main__": + main() From 85958b9404bf54745f7813d3586a6d463e104b7b Mon Sep 17 00:00:00 2001 From: Olivier Gayot Date: Wed, 19 Jan 2022 14:04:04 +0100 Subject: [PATCH 2/5] Don't include UA token in autoinstall if empty If no UA token is provided, the UbuntuAdvantageController will generate autoinstall data that contains an empty string: ubuntu-advantage: token: "" Unfortunately, this is not valid according to the JSON schema. Fixed by not returning a token: key if the token is an empty string. Signed-off-by: Olivier Gayot --- subiquity/server/controllers/ubuntu_advantage.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/subiquity/server/controllers/ubuntu_advantage.py b/subiquity/server/controllers/ubuntu_advantage.py index c91cec47..9db63907 100644 --- a/subiquity/server/controllers/ubuntu_advantage.py +++ b/subiquity/server/controllers/ubuntu_advantage.py @@ -57,6 +57,8 @@ class UbuntuAdvantageController(SubiquityController): """ Return a dictionary that can be used as an autoinstall snippet for Ubuntu Advantage. """ + if not self.model.token: + return {} return { "token": self.model.token } From 7664258aa4b254fbd494848d62944a43ec76cbba Mon Sep 17 00:00:00 2001 From: Olivier Gayot Date: Fri, 21 Jan 2022 13:05:47 +0100 Subject: [PATCH 3/5] Make sure SSHController always yields valid autoinstall data When running an autoinstallation with no ssh: field (which is valid), the output autoinstall-user-data file would end up with None for the key ssh:authorized-keys, as shown below: ssh: authorized-keys: null Unfortunately, null is not a valid value according to the schema (which expects a string). Fixed by initializing the authorized_keys variable to [] instead of None so that it yields an empty list instead of null when no autoinstall data is loaded. Signed-off-by: Olivier Gayot --- subiquity/models/ssh.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/subiquity/models/ssh.py b/subiquity/models/ssh.py index 34e64dbb..c8ebae5b 100644 --- a/subiquity/models/ssh.py +++ b/subiquity/models/ssh.py @@ -14,6 +14,7 @@ # along with this program. If not, see . import logging +from typing import List log = logging.getLogger("subiquity.models.ssh") @@ -22,7 +23,7 @@ class SSHModel: def __init__(self): self.install_server = False - self.authorized_keys = None + self.authorized_keys: List[str] = [] self.pwauth = True # Although the generated config just contains the key above, # we store the imported id so that we can re-fill the form if From 2bbd4fffe65a76baf5dcff640bf4bd1dafaa8b44 Mon Sep 17 00:00:00 2001 From: Olivier Gayot Date: Fri, 21 Jan 2022 13:05:08 +0100 Subject: [PATCH 4/5] Update example states for Subiquity to cover valid token values Signed-off-by: Olivier Gayot --- examples/answers-bond.yaml | 2 +- examples/answers-guided-lvm.yaml | 2 +- examples/answers-imsm.yaml | 2 +- examples/answers-lvm-dmcrypt.yaml | 2 +- examples/answers-lvm.yaml | 2 +- examples/answers-preserve.yaml | 2 +- examples/answers-raid-lvm.yaml | 2 +- examples/answers-raid.yaml | 2 +- examples/answers-serial.yaml | 2 +- examples/answers-swap.yaml | 2 +- examples/answers.yaml | 2 +- 11 files changed, 11 insertions(+), 11 deletions(-) diff --git a/examples/answers-bond.yaml b/examples/answers-bond.yaml index f38de8cd..6eb24261 100644 --- a/examples/answers-bond.yaml +++ b/examples/answers-bond.yaml @@ -46,7 +46,7 @@ Identity: # ubuntu password: '$6$wdAcoXrU039hKYPd$508Qvbe7ObUnxoj15DRCkzC3qO7edjH0VV7BPNRDYK4QR8ofJaEEF2heacn0QgD.f8pO8SNp83XNdWG6tocBM1' UbuntuAdvantage: - token: "a1b2c3d4" + token: "" SSH: install_server: false SnapList: diff --git a/examples/answers-guided-lvm.yaml b/examples/answers-guided-lvm.yaml index 35f500c4..f2652896 100644 --- a/examples/answers-guided-lvm.yaml +++ b/examples/answers-guided-lvm.yaml @@ -21,7 +21,7 @@ Identity: # ubuntu password: '$6$wdAcoXrU039hKYPd$508Qvbe7ObUnxoj15DRCkzC3qO7edjH0VV7BPNRDYK4QR8ofJaEEF2heacn0QgD.f8pO8SNp83XNdWG6tocBM1' UbuntuAdvantage: - token: "a1b2c3d4" + token: C1NWcZTHLteJXGVMM6YhvHDpGrhyy7 SSH: install_server: false SnapList: diff --git a/examples/answers-imsm.yaml b/examples/answers-imsm.yaml index 0ec69af3..3bf409f0 100644 --- a/examples/answers-imsm.yaml +++ b/examples/answers-imsm.yaml @@ -21,7 +21,7 @@ Identity: # ubuntu password: '$6$wdAcoXrU039hKYPd$508Qvbe7ObUnxoj15DRCkzC3qO7edjH0VV7BPNRDYK4QR8ofJaEEF2heacn0QgD.f8pO8SNp83XNdWG6tocBM1' UbuntuAdvantage: - token: "a1b2c3d4" + token: C1NWcZTHLteJXGVMM6YhvHDpGrhyy7 SSH: install_server: true pwauth: false diff --git a/examples/answers-lvm-dmcrypt.yaml b/examples/answers-lvm-dmcrypt.yaml index 251fd298..d3983973 100644 --- a/examples/answers-lvm-dmcrypt.yaml +++ b/examples/answers-lvm-dmcrypt.yaml @@ -65,7 +65,7 @@ Identity: # ubuntu password: '$6$wdAcoXrU039hKYPd$508Qvbe7ObUnxoj15DRCkzC3qO7edjH0VV7BPNRDYK4QR8ofJaEEF2heacn0QgD.f8pO8SNp83XNdWG6tocBM1' UbuntuAdvantage: - token: "a1b2c3d4" + token: C1NWcZTHLteJXGVMM6YhvHDpGrhyy7 SSH: install_server: true pwauth: false diff --git a/examples/answers-lvm.yaml b/examples/answers-lvm.yaml index d81c8f5d..36675156 100644 --- a/examples/answers-lvm.yaml +++ b/examples/answers-lvm.yaml @@ -46,7 +46,7 @@ Filesystem: mount: / - action: done UbuntuAdvantage: - token: "a1b2c3d4" + token: C1NWcZTHLteJXGVMM6YhvHDpGrhyy7 Identity: realname: Ubuntu username: ubuntu diff --git a/examples/answers-preserve.yaml b/examples/answers-preserve.yaml index 142adf68..82ff240f 100644 --- a/examples/answers-preserve.yaml +++ b/examples/answers-preserve.yaml @@ -26,7 +26,7 @@ Identity: # ubuntu password: '$6$wdAcoXrU039hKYPd$508Qvbe7ObUnxoj15DRCkzC3qO7edjH0VV7BPNRDYK4QR8ofJaEEF2heacn0QgD.f8pO8SNp83XNdWG6tocBM1' UbuntuAdvantage: - token: "a1b2c3d4" + token: "" SSH: install_server: true pwauth: false diff --git a/examples/answers-raid-lvm.yaml b/examples/answers-raid-lvm.yaml index b3d71e07..1e8eda17 100644 --- a/examples/answers-raid-lvm.yaml +++ b/examples/answers-raid-lvm.yaml @@ -97,7 +97,7 @@ Identity: # ubuntu password: '$6$wdAcoXrU039hKYPd$508Qvbe7ObUnxoj15DRCkzC3qO7edjH0VV7BPNRDYK4QR8ofJaEEF2heacn0QgD.f8pO8SNp83XNdWG6tocBM1' UbuntuAdvantage: - token: "a1b2c3d4" + token: C1NWcZTHLteJXGVMM6YhvHDpGrhyy7 SSH: install_server: false SnapList: diff --git a/examples/answers-raid.yaml b/examples/answers-raid.yaml index 16434788..56033fd3 100644 --- a/examples/answers-raid.yaml +++ b/examples/answers-raid.yaml @@ -56,7 +56,7 @@ Identity: # ubuntu password: '$6$wdAcoXrU039hKYPd$508Qvbe7ObUnxoj15DRCkzC3qO7edjH0VV7BPNRDYK4QR8ofJaEEF2heacn0QgD.f8pO8SNp83XNdWG6tocBM1' UbuntuAdvantage: - token: "a1b2c3d4" + token: C1NWcZTHLteJXGVMM6YhvHDpGrhyy7 SSH: install_server: false SnapList: diff --git a/examples/answers-serial.yaml b/examples/answers-serial.yaml index 0ccfa3af..98c4d5c5 100644 --- a/examples/answers-serial.yaml +++ b/examples/answers-serial.yaml @@ -25,7 +25,7 @@ Identity: # ubuntu password: '$6$wdAcoXrU039hKYPd$508Qvbe7ObUnxoj15DRCkzC3qO7edjH0VV7BPNRDYK4QR8ofJaEEF2heacn0QgD.f8pO8SNp83XNdWG6tocBM1' UbuntuAdvantage: - token: "a1b2c3d4" + token: "" SSH: install_server: true pwauth: false diff --git a/examples/answers-swap.yaml b/examples/answers-swap.yaml index 095b8037..6a7f5816 100644 --- a/examples/answers-swap.yaml +++ b/examples/answers-swap.yaml @@ -29,7 +29,7 @@ Identity: # ubuntu password: '$6$wdAcoXrU039hKYPd$508Qvbe7ObUnxoj15DRCkzC3qO7edjH0VV7BPNRDYK4QR8ofJaEEF2heacn0QgD.f8pO8SNp83XNdWG6tocBM1' UbuntuAdvantage: - token: "a1b2c3d4" + token: C1NWcZTHLteJXGVMM6YhvHDpGrhyy7 SSH: install_server: false SnapList: diff --git a/examples/answers.yaml b/examples/answers.yaml index 96c17d12..2727f25f 100644 --- a/examples/answers.yaml +++ b/examples/answers.yaml @@ -23,7 +23,7 @@ Identity: password: '$6$wdAcoXrU039hKYPd$508Qvbe7ObUnxoj15DRCkzC3qO7edjH0VV7BPNRDYK4QR8ofJaEEF2heacn0QgD.f8pO8SNp83XNdWG6tocBM1' ssh-import-id: gh:mwhudson UbuntuAdvantage: - token: "a1b2c3d4" + token: "" SnapList: snaps: hello: From e2216c1d53b3ce5d0bd2c764f031e161206e169b Mon Sep 17 00:00:00 2001 From: Olivier Gayot Date: Fri, 21 Jan 2022 13:25:26 +0100 Subject: [PATCH 5/5] Run script to validate autoinstall-user-data in integration tests Signed-off-by: Olivier Gayot --- scripts/runtests.sh | 2 ++ 1 file changed, 2 insertions(+) diff --git a/scripts/runtests.sh b/scripts/runtests.sh index c60df759..ed075d69 100755 --- a/scripts/runtests.sh +++ b/scripts/runtests.sh @@ -21,6 +21,7 @@ validate () { echo "log file not created" exit 1 fi + python3 scripts/validate-autoinstall-user-data.py < .subiquity/var/log/installer/autoinstall-user-data if grep passw0rd .subiquity/subiquity-client-debug.log .subiquity/subiquity-server-debug.log | grep -v "Loaded answers" | grep -v "answers_action"; then echo "password leaked into log file" exit 1 @@ -118,6 +119,7 @@ clean () { rm -rf .subiquity/etc/cloud/cloud.cfg.d/99-installer.cfg rm -rf .subiquity/var/crash rm -rf .subiquity/var/cache + rm -rf .subiquity/run/subiquity/states } error () {