diff --git a/autoinstall-schema.json b/autoinstall-schema.json
index 05e2fd7b..c24fbacb 100644
--- a/autoinstall-schema.json
+++ b/autoinstall-schema.json
@@ -323,6 +323,13 @@
"additionalProperties": false
}
},
+ "updates": {
+ "type": "string",
+ "enum": [
+ "security",
+ "all"
+ ]
+ },
"late-commands": {
"type": "array",
"items": {
diff --git a/examples/autoinstall-user-data.yaml b/examples/autoinstall-user-data.yaml
index 961aad37..e7de1003 100644
--- a/examples/autoinstall-user-data.yaml
+++ b/examples/autoinstall-user-data.yaml
@@ -13,6 +13,7 @@ late-commands:
- echo a
keyboard:
layout: gb
+updates: security
user-data:
users:
- username: ubuntu
diff --git a/examples/autoinstall.yaml b/examples/autoinstall.yaml
index 70d19d18..300c30ea 100644
--- a/examples/autoinstall.yaml
+++ b/examples/autoinstall.yaml
@@ -37,6 +37,7 @@ identity:
username: ubuntu
password: '$6$wdAcoXrU039hKYPd$508Qvbe7ObUnxoj15DRCkzC3qO7edjH0VV7BPNRDYK4QR8ofJaEEF2heacn0QgD.f8pO8SNp83XNdWG6tocBM1'
hostname: ubuntu
+updates: all
storage:
config:
- {type: disk, ptable: gpt, path: /dev/vdb, wipe: superblock, preserve: false, grub_device: true, id: disk-1}
diff --git a/scripts/runtests.sh b/scripts/runtests.sh
index e3fd93ab..e1c740bd 100755
--- a/scripts/runtests.sh
+++ b/scripts/runtests.sh
@@ -36,6 +36,7 @@ for answers in examples/answers*.yaml; do
# The --foreground is important to avoid subiquity getting SIGTTOU-ed.
timeout --foreground 60 sh -c "LANG=C.UTF-8 python3 -m subiquity.cmd.tui --answers $answers --dry-run --snaps-from-examples --machine-config $config" < $tty
validate
+ grep -q 'finish: subiquity/Install/install/run_unattended_upgrades: SUCCESS: downloading and installing security updates' .subiquity/subiquity-server-debug.log
done
clean
@@ -52,11 +53,14 @@ grep -q 'finish: subiquity/Install/install/postinstall/install_package1: SUCCESS
grep -q 'finish: subiquity/Install/install/postinstall/install_package2: SUCCESS: installing package2' \
.subiquity/subiquity-server-debug.log
grep -q 'switching subiquity to edge' .subiquity/subiquity-server-debug.log
+grep -q 'finish: subiquity/Install/install/run_unattended_upgrades: SUCCESS: downloading and installing all updates' \
+ .subiquity/subiquity-server-debug.log
clean
timeout --foreground 60 sh -c "LANG=C.UTF-8 python3 -m subiquity.cmd.tui --autoinstall examples/autoinstall-user-data.yaml \
--dry-run --machine-config examples/simple.json --kernel-cmdline 'autoinstall'"
validate
+grep -q 'finish: subiquity/Install/install/run_unattended_upgrades: SUCCESS: downloading and installing security updates' .subiquity/subiquity-server-debug.log
python3 -m subiquity.cmd.schema > "$testschema"
diff -u "autoinstall-schema.json" "$testschema"
diff --git a/subiquity/common/apidef.py b/subiquity/common/apidef.py
index 8aaf548c..8fcf8af7 100644
--- a/subiquity/common/apidef.py
+++ b/subiquity/common/apidef.py
@@ -47,11 +47,12 @@ from subiquity.common.types import (
@api
class API:
"""The API offered by the subiquity installer process."""
- locale = simple_endpoint(str)
- proxy = simple_endpoint(str)
- mirror = simple_endpoint(str)
identity = simple_endpoint(IdentityData)
+ locale = simple_endpoint(str)
+ mirror = simple_endpoint(str)
+ proxy = simple_endpoint(str)
ssh = simple_endpoint(SSHData)
+ updates = simple_endpoint(str)
class meta:
class status:
diff --git a/subiquity/models/subiquity.py b/subiquity/models/subiquity.py
index 17bfa258..cd06b438 100644
--- a/subiquity/models/subiquity.py
+++ b/subiquity/models/subiquity.py
@@ -29,14 +29,15 @@ from subiquitycore.file_util import write_file
from subiquitycore.utils import run_command
from .filesystem import FilesystemModel
+from .identity import IdentityModel
from .keyboard import KeyboardModel
from .locale import LocaleModel
-from .proxy import ProxyModel
from .mirror import MirrorModel
from .network import NetworkModel
+from .proxy import ProxyModel
from .snaplist import SnapListModel
from .ssh import SSHModel
-from .identity import IdentityModel
+from .updates import UpdatesModel
log = logging.getLogger('subiquity.models.subiquity')
@@ -117,6 +118,7 @@ class SubiquityModel:
self.proxy = ProxyModel()
self.snaplist = SnapListModel()
self.ssh = SSHModel()
+ self.updates = UpdatesModel()
self.userdata = {}
self.confirmation = asyncio.Event()
diff --git a/subiquity/models/updates.py b/subiquity/models/updates.py
new file mode 100644
index 00000000..40a5929b
--- /dev/null
+++ b/subiquity/models/updates.py
@@ -0,0 +1,27 @@
+# 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 .
+
+import logging
+
+log = logging.getLogger('subiquity.models.updates')
+
+
+class UpdatesModel(object):
+ """ Model representing updates selection"""
+
+ updates = 'security'
+
+ def __repr__(self):
+ return "".format(self.updates)
diff --git a/subiquity/server/controllers/__init__.py b/subiquity/server/controllers/__init__.py
index fde735e4..8a3985d9 100644
--- a/subiquity/server/controllers/__init__.py
+++ b/subiquity/server/controllers/__init__.py
@@ -29,6 +29,7 @@ from .refresh import RefreshController
from .reporting import ReportingController
from .snaplist import SnapListController
from .ssh import SSHController
+from .updates import UpdatesController
from .userdata import UserdataController
from .zdev import ZdevController
@@ -51,6 +52,7 @@ __all__ = [
'ReportingController',
'SnapListController',
'SSHController',
+ 'UpdatesController',
'UserdataController',
'ZdevController',
]
diff --git a/subiquity/server/controllers/install.py b/subiquity/server/controllers/install.py
index c8a028ec..4d322f15 100644
--- a/subiquity/server/controllers/install.py
+++ b/subiquity/server/controllers/install.py
@@ -14,13 +14,13 @@
# along with this program. If not, see .
import asyncio
+import contextlib
import datetime
import logging
import os
import re
import shutil
import sys
-import tempfile
from curtin.commands.install import (
ERROR_TARFILE,
@@ -135,7 +135,7 @@ class InstallController(SubiquityController):
def _write_config(self, path, config):
with open(path, 'w') as conf:
- datestr = '# Autogenerated by SUbiquity: {} UTC\n'.format(
+ datestr = '# Autogenerated by Subiquity: {} UTC\n'.format(
str(datetime.datetime.utcnow()))
conf.write(datestr)
conf.write(yaml.dump(config))
@@ -234,7 +234,9 @@ class InstallController(SubiquityController):
if self.model.network.has_network:
self.app.update_state(ApplicationState.UU_RUNNING)
- await self.run_unattended_upgrades(context=context)
+ policy = self.model.updates.updates
+ await self.run_unattended_upgrades(context=context,
+ policy=policy)
self.app.update_state(ApplicationState.DONE)
except Exception:
@@ -307,31 +309,31 @@ class InstallController(SubiquityController):
for cmd in cmds:
await arun_command(self.logged_command(cmd), check=True)
- @with_context(description="downloading and installing security updates")
- async def run_unattended_upgrades(self, context):
- target_tmp = os.path.join(self.model.target, "tmp")
- os.makedirs(target_tmp, exist_ok=True)
- apt_conf = tempfile.NamedTemporaryFile(
- dir=target_tmp, delete=False, mode='w')
- apt_conf.write(uu_apt_conf)
- apt_conf.close()
- env = os.environ.copy()
- env["APT_CONFIG"] = apt_conf.name[len(self.model.target):]
- self.unattended_upgrades_ctx = context
+ @with_context(description="downloading and installing {policy} updates")
+ async def run_unattended_upgrades(self, context, policy):
if self.app.opts.dry_run:
- self.unattended_upgrades_proc = await astart_command(
- self.logged_command(
- ["sleep", str(5/self.app.scale_factor)]), env=env)
+ command = ["sleep", str(5/self.app.scale_factor)]
+ aptdir = os.path.join(self.model.target, "tmp")
else:
+ command = [sys.executable, "-m", "curtin", "in-target",
+ "-t", "/target", "--", "unattended-upgrades", "-v"]
+ aptdir = os.path.join(self.model.target, "etc/apt/apt.conf.d")
+ os.makedirs(aptdir, exist_ok=True)
+ apt_conf_contents = uu_apt_conf
+ if policy == 'all':
+ apt_conf_contents += uu_apt_conf_update_all
+ else:
+ apt_conf_contents += uu_apt_conf_update_security
+ fname = 'zzzz-temp-installer-unattended-upgrade'
+ with external_temp_file(aptdir, fname) as apt_conf:
+ apt_conf.write(apt_conf_contents)
+ apt_conf.close()
+ self.unattended_upgrades_ctx = context
self.unattended_upgrades_proc = await astart_command(
- self.logged_command([
- sys.executable, "-m", "curtin", "in-target", "-t",
- "/target", "--", "unattended-upgrades", "-v",
- ]), env=env)
- await self.unattended_upgrades_proc.communicate()
- self.unattended_upgrades_proc = None
- self.unattended_upgrades_ctx = None
- os.remove(apt_conf.name)
+ self.logged_command(command))
+ await self.unattended_upgrades_proc.communicate()
+ self.unattended_upgrades_proc = None
+ self.unattended_upgrades_ctx = None
async def stop_unattended_upgrades(self):
with self.unattended_upgrades_ctx.parent.child(
@@ -349,9 +351,43 @@ class InstallController(SubiquityController):
]), check=True)
-uu_apt_conf = """\
+@contextlib.contextmanager
+def external_temp_file(dir, fname):
+ path = os.path.join(dir, fname)
+ fid = open(path, 'wb')
+ try:
+ yield fid
+ finally:
+ fid.close()
+ os.remove(path)
+
+
+uu_apt_conf = b"""\
# Config for the unattended-upgrades run to avoid failing on battery power or
# a metered connection.
Unattended-Upgrade::OnlyOnACPower "false";
Unattended-Upgrade::Skip-Updates-On-Metered-Connections "true";
"""
+
+uu_apt_conf_update_security = b"""\
+# A copy of the current default unattended-upgrades config to grab
+# security.
+Unattended-Upgrade::Allowed-Origins {
+ "${distro_id}:${distro_codename}";
+ "${distro_id}:${distro_codename}-security";
+ "${distro_id}ESMApps:${distro_codename}-apps-security";
+ "${distro_id}ESM:${distro_codename}-infra-security";
+};
+"""
+
+uu_apt_conf_update_all = b"""\
+# A modified version of the unattended-upgrades default Allowed-Origins
+# to include updates in the permitted origins.
+Unattended-Upgrade::Allowed-Origins {
+ "${distro_id}:${distro_codename}";
+ "${distro_id}:${distro_codename}-updates";
+ "${distro_id}:${distro_codename}-security";
+ "${distro_id}ESMApps:${distro_codename}-apps-security";
+ "${distro_id}ESM:${distro_codename}-infra-security";
+};
+"""
diff --git a/subiquity/server/controllers/updates.py b/subiquity/server/controllers/updates.py
new file mode 100644
index 00000000..140c899c
--- /dev/null
+++ b/subiquity/server/controllers/updates.py
@@ -0,0 +1,56 @@
+# Copyright 2018 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 .
+
+import logging
+
+from subiquity.common.apidef import API
+from subiquity.server.controller import SubiquityController
+
+
+log = logging.getLogger('subiquity.server.controllers.updates')
+
+
+class UpdatesController(SubiquityController):
+
+ endpoint = API.updates
+
+ possible = ['security', 'all']
+
+ autoinstall_key = model_name = "updates"
+ autoinstall_schema = {
+ 'type': 'string',
+ 'enum': possible,
+ }
+ autoinstall_default = 'security'
+
+ def load_autoinstall_data(self, data):
+ self.deserialize(data)
+
+ def make_autoinstall(self):
+ return self.serialize()
+
+ def serialize(self):
+ return self.model.updates
+
+ def deserialize(self, data):
+ if data not in self.possible:
+ raise ValueError
+ self.model.updates = data
+
+ async def GET(self) -> str:
+ return self.serialize()
+
+ async def POST(self, data: str):
+ self.deserialize(data)
diff --git a/subiquity/server/server.py b/subiquity/server/server.py
index f14789f6..a7e5e684 100644
--- a/subiquity/server/server.py
+++ b/subiquity/server/server.py
@@ -188,6 +188,7 @@ class SubiquityServer(Application):
"SSH",
"SnapList",
"Install",
+ "Updates",
"Late",
"Reboot",
]