Merge pull request #1956 from Chris-Peterson444/top-level-autoinstall
Allow top-level autoinstall in all delivery methods
This commit is contained in:
commit
16bcfcf7ae
|
@ -73,6 +73,7 @@ pw
|
|||
realname
|
||||
rootfs
|
||||
rsyslog
|
||||
runtime
|
||||
subvolume
|
||||
subvolumes
|
||||
superset
|
||||
|
|
|
@ -8,6 +8,8 @@ EBS
|
|||
EKS
|
||||
Grafana
|
||||
IAM
|
||||
ISO
|
||||
ISOs
|
||||
JSON
|
||||
Jira
|
||||
Juju
|
||||
|
|
|
@ -94,18 +94,6 @@ path is relative to the rootfs of the installation system. For example:
|
|||
|
||||
* :code:`subiquity.autoinstallpath=path/to/autoinstall.yaml`
|
||||
|
||||
.. note::
|
||||
|
||||
Directly specifying autoinstall as a :code:`autoinstall.yaml` file does not
|
||||
require a :code:`#cloud-config` header, and does not use a top level
|
||||
``autoinstall:`` key. The autoinstall directives are placed at the top
|
||||
level. For example:
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
version: 1
|
||||
....
|
||||
|
||||
|
||||
Order precedence of the autoinstall locations
|
||||
=============================================
|
||||
|
|
|
@ -3,10 +3,40 @@
|
|||
Autoinstall configuration reference manual
|
||||
==========================================
|
||||
|
||||
The autoinstall file uses the YAML format. At the top level, it must be a
|
||||
mapping containing the keys described in this document. Unrecognised keys
|
||||
are ignored in version 1, but will cause a fatal validation error in future
|
||||
versions.
|
||||
The autoinstall file uses the YAML format. At the top level is a
|
||||
single key ``autoinstall`` which contains a mapping of the keys described in
|
||||
this document. Unrecognised keys are ignored in version 1, but will cause a
|
||||
fatal validation error in future versions.
|
||||
|
||||
Here is an example of a minimal autoinstall configuration:
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
autoinstall:
|
||||
version: 1
|
||||
identity:
|
||||
...
|
||||
|
||||
|
||||
At the top level is the ``autoinstall`` keyword, which contains a version section
|
||||
and an (incomplete) identity section which are explained in more detail below.
|
||||
Any other key at the level of ``autoinstall``, will result in an autoinstall
|
||||
validation error at runtime.
|
||||
|
||||
.. warning::
|
||||
|
||||
This behaviour was first introduced during 24.04 (Noble). On any ISOs built
|
||||
before this, you will need to refresh the installer to see this behaviour.
|
||||
Please the note below about the old format.
|
||||
|
||||
.. note::
|
||||
|
||||
Technically, in all but one case the top level ``autoinstall`` keyword is
|
||||
strictly unnecessary. This keyword is only necessary when serving autoinstall
|
||||
via cloud-config. For backwards compatibility this format is still supported
|
||||
for non-cloud-config based delivery methods; however, it is
|
||||
**highly recommended** to use the format with a top-level ``autoinstall``
|
||||
keyword as mistakes in this formatting are a common source of confusion.
|
||||
|
||||
|
||||
.. _ai-schema:
|
||||
|
@ -29,6 +59,10 @@ Several configuration keys are lists of commands to be executed. Each command ca
|
|||
Top-level keys
|
||||
--------------
|
||||
|
||||
The following keys can be used to configure various aspects of the installation.
|
||||
If the global ``autoinstall`` key is provided, then all "top-level keys" must
|
||||
be provided underneath it and "top-level" refers to this sub-level. The
|
||||
examples below demonstrate this structure.
|
||||
|
||||
.. warning::
|
||||
In version 1, Subiquity will emit warnings when encountering unrecognised
|
||||
|
@ -57,6 +91,7 @@ A list of configuration keys to still show in the user interface (UI). For examp
|
|||
|
||||
.. code-block:: yaml
|
||||
|
||||
autoinstall:
|
||||
version: 1
|
||||
interactive-sections:
|
||||
- network
|
||||
|
@ -221,6 +256,7 @@ For example, to run DHCP version 6 on a specific network interface:
|
|||
|
||||
.. code-block:: yaml
|
||||
|
||||
autoinstall:
|
||||
network:
|
||||
version: 2
|
||||
ethernets:
|
||||
|
@ -231,6 +267,7 @@ Note that in the 20.04 GA release of Subiquity, the behaviour is slightly differ
|
|||
|
||||
.. code-block:: yaml
|
||||
|
||||
autoinstall:
|
||||
network:
|
||||
network:
|
||||
version: 2
|
||||
|
@ -274,6 +311,7 @@ The default is:
|
|||
|
||||
.. code-block:: yaml
|
||||
|
||||
autoinstall:
|
||||
apt:
|
||||
preserve_sources_list: false
|
||||
mirror-selection:
|
||||
|
@ -330,6 +368,7 @@ To specify a mirror, use a configuration like this:
|
|||
|
||||
.. code-block:: yaml
|
||||
|
||||
autoinstall:
|
||||
apt:
|
||||
mirror-selection:
|
||||
primary:
|
||||
|
@ -341,6 +380,7 @@ To add a PPA:
|
|||
|
||||
.. code-block:: yaml
|
||||
|
||||
autoinstall:
|
||||
apt:
|
||||
sources:
|
||||
curtin-ppa:
|
||||
|
@ -364,6 +404,7 @@ The three supported layouts at the time of writing are ``lvm``, ``direct`` and `
|
|||
|
||||
.. code-block:: yaml
|
||||
|
||||
autoinstall:
|
||||
storage:
|
||||
layout:
|
||||
name: lvm
|
||||
|
@ -379,6 +420,7 @@ By default, these layouts install to the largest disk in a system, but you can s
|
|||
|
||||
.. code-block:: yaml
|
||||
|
||||
autoinstall:
|
||||
storage:
|
||||
layout:
|
||||
name: lvm
|
||||
|
@ -396,6 +438,7 @@ When using the ``lvm`` layout, LUKS encryption can be enabled by supplying a pas
|
|||
|
||||
.. code-block:: yaml
|
||||
|
||||
autoinstall:
|
||||
storage:
|
||||
layout:
|
||||
name: lvm
|
||||
|
@ -427,6 +470,7 @@ Example with no size scaling and a passphrase:
|
|||
|
||||
.. code-block:: yaml
|
||||
|
||||
autoinstall:
|
||||
storage:
|
||||
layout:
|
||||
name: lvm
|
||||
|
@ -444,6 +488,7 @@ An example to enable Reset Partition:
|
|||
|
||||
.. code-block:: yaml
|
||||
|
||||
autoinstall:
|
||||
storage:
|
||||
layout:
|
||||
name: direct
|
||||
|
@ -453,6 +498,7 @@ The size of the reset partition can also be fixed to a specified size. This is
|
|||
|
||||
.. code-block:: yaml
|
||||
|
||||
autoinstall:
|
||||
storage:
|
||||
layout:
|
||||
name: direct
|
||||
|
@ -462,6 +508,7 @@ The installer can also install Reset Partition without installing the system. T
|
|||
|
||||
.. code-block:: yaml
|
||||
|
||||
autoinstall:
|
||||
storage:
|
||||
layout:
|
||||
name: direct
|
||||
|
@ -482,6 +529,7 @@ An example storage section:
|
|||
|
||||
.. code-block:: yaml
|
||||
|
||||
autoinstall:
|
||||
storage:
|
||||
swap:
|
||||
size: 0
|
||||
|
@ -613,6 +661,7 @@ Example:
|
|||
|
||||
.. code-block:: yaml
|
||||
|
||||
autoinstall:
|
||||
identity:
|
||||
realname: 'Ubuntu User'
|
||||
username: ubuntu
|
||||
|
@ -758,6 +807,7 @@ A list of snaps to install. Each snap is represented as a mapping with a require
|
|||
|
||||
.. code-block:: yaml
|
||||
|
||||
autoinstall:
|
||||
snaps:
|
||||
- name: etcd
|
||||
channel: edge
|
||||
|
@ -898,6 +948,7 @@ The default configuration is:
|
|||
|
||||
.. code-block:: yaml
|
||||
|
||||
autoinstall:
|
||||
reporting:
|
||||
builtin:
|
||||
type: print
|
||||
|
@ -906,6 +957,7 @@ Report to rsyslog:
|
|||
|
||||
.. code-block:: yaml
|
||||
|
||||
autoinstall:
|
||||
reporting:
|
||||
central:
|
||||
type: rsyslog
|
||||
|
@ -916,6 +968,7 @@ Suppress the default output:
|
|||
|
||||
.. code-block:: yaml
|
||||
|
||||
autoinstall:
|
||||
reporting:
|
||||
builtin:
|
||||
type: none
|
||||
|
@ -924,6 +977,7 @@ Report to a curtin-style webhook:
|
|||
|
||||
.. code-block:: yaml
|
||||
|
||||
autoinstall:
|
||||
reporting:
|
||||
hook:
|
||||
type: webhook
|
||||
|
|
|
@ -60,7 +60,13 @@ def main() -> None:
|
|||
assert user_data.readline() == "#cloud-config\n"
|
||||
def get_autoinstall_data(data): return data["autoinstall"]
|
||||
else:
|
||||
def get_autoinstall_data(data): return data
|
||||
def get_autoinstall_data(data):
|
||||
try:
|
||||
cfg = data["autoinstall"]
|
||||
except KeyError:
|
||||
cfg = data
|
||||
return cfg
|
||||
|
||||
|
||||
# Verify autoinstall doc link is in the file
|
||||
|
||||
|
|
|
@ -668,7 +668,38 @@ class SubiquityServer(Application):
|
|||
context=ctx,
|
||||
)
|
||||
|
||||
def load_autoinstall_config(self, *, only_early):
|
||||
@with_context(name="read_config")
|
||||
def _read_config(self, *, cfg_path: str, context: Context) -> dict[str, Any]:
|
||||
with open(cfg_path) as fp:
|
||||
config: dict[str, Any] = yaml.safe_load(fp)
|
||||
|
||||
autoinstall_config: dict[str, Any]
|
||||
|
||||
# Support "autoinstall" as a top-level key
|
||||
if "autoinstall" in config:
|
||||
autoinstall_config = config.pop("autoinstall")
|
||||
|
||||
# but the only top level key
|
||||
if len(config) != 0:
|
||||
self.interactive = bool(autoinstall_config.get("interactive-sections"))
|
||||
msg: str = (
|
||||
"autoinstall.yaml is not a valid cloud config datasource.\n"
|
||||
"No other keys may be present alongside 'autoinstall' at "
|
||||
"the top level."
|
||||
)
|
||||
context.error(msg)
|
||||
raise AutoinstallValidationError(
|
||||
owner="top-level keys",
|
||||
details="autoinstall.yaml is not a valid cloud config datasource",
|
||||
)
|
||||
|
||||
else:
|
||||
autoinstall_config = config
|
||||
|
||||
return autoinstall_config
|
||||
|
||||
@with_context()
|
||||
def load_autoinstall_config(self, *, only_early, context):
|
||||
log.debug(
|
||||
"load_autoinstall_config only_early %s file %s",
|
||||
only_early,
|
||||
|
@ -689,8 +720,9 @@ class SubiquityServer(Application):
|
|||
self.interactive = True
|
||||
return
|
||||
|
||||
with open(self.autoinstall) as fp:
|
||||
self.autoinstall_config = yaml.safe_load(fp)
|
||||
self.autoinstall_config = self._read_config(
|
||||
cfg_path=self.autoinstall, context=context
|
||||
)
|
||||
|
||||
# Check every time
|
||||
self.interactive = bool(self.autoinstall_config.get("interactive-sections"))
|
||||
|
|
|
@ -20,6 +20,7 @@ from typing import Any
|
|||
from unittest.mock import AsyncMock, Mock, patch
|
||||
|
||||
import jsonschema
|
||||
import yaml
|
||||
from jsonschema.validators import validator_for
|
||||
|
||||
from subiquity.cloudinit import CloudInitSchemaValidationError
|
||||
|
@ -156,6 +157,7 @@ early-commands: ["{cmd}"]
|
|||
|
||||
class TestAutoinstallValidation(SubiTestCase):
|
||||
async def asyncSetUp(self):
|
||||
self.tempdir = self.tmp_dir()
|
||||
opts = Mock()
|
||||
opts.dry_run = True
|
||||
opts.output_base = self.tmp_dir()
|
||||
|
@ -171,6 +173,15 @@ class TestAutoinstallValidation(SubiTestCase):
|
|||
}
|
||||
self.server.make_apport_report = Mock()
|
||||
|
||||
def path(self, relative_path):
|
||||
return self.tmp_path(relative_path, dir=self.tempdir)
|
||||
|
||||
def create(self, path, contents):
|
||||
path = self.path(path)
|
||||
with open(path, "w") as fp:
|
||||
fp.write(contents)
|
||||
return path
|
||||
|
||||
# Pseudo Load Controllers to avoid patching the loading logic for each
|
||||
# controller when we still want access to class attributes
|
||||
def pseudo_load_controllers(self):
|
||||
|
@ -445,6 +456,44 @@ class TestAutoinstallValidation(SubiTestCase):
|
|||
|
||||
self.assertEqual(cfg, expected)
|
||||
|
||||
async def test_autoinstall_validation__top_level_autoinstall(self):
|
||||
"""Test allow autoinstall as top-level key"""
|
||||
|
||||
new_style = {
|
||||
"autoinstall": {
|
||||
"version": 1,
|
||||
"interactive-sections": ["identity"],
|
||||
"apt": "...",
|
||||
}
|
||||
}
|
||||
old_style = new_style["autoinstall"]
|
||||
|
||||
# Read new style correctly
|
||||
path = self.create("autoinstall.yaml", yaml.dump(new_style))
|
||||
self.assertEqual(self.server._read_config(cfg_path=path), old_style)
|
||||
|
||||
# No changes to old style
|
||||
path = self.create("autoinstall.yaml", yaml.dump(old_style))
|
||||
self.assertEqual(self.server._read_config(cfg_path=path), old_style)
|
||||
|
||||
async def test_autoinstall_validation__not_cloudinit_datasource(self):
|
||||
"""Test no cloud init datasources in new style autoinstall"""
|
||||
|
||||
new_style = {
|
||||
"autoinstall": {
|
||||
"version": 1,
|
||||
"interactive-sections": ["identity"],
|
||||
"apt": "...",
|
||||
},
|
||||
"cloudinit-data": "I am data",
|
||||
}
|
||||
|
||||
with self.assertRaises(AutoinstallValidationError) as ctx:
|
||||
path = self.create("autoinstall.yaml", yaml.dump(new_style))
|
||||
self.server._read_config(cfg_path=path)
|
||||
|
||||
self.assertEqual("top-level keys", ctx.exception.owner)
|
||||
|
||||
|
||||
class TestMetaController(SubiTestCase):
|
||||
async def test_interactive_sections_not_present(self):
|
||||
|
|
Loading…
Reference in New Issue