add setting policy for post-install updates via autoinstall (#920)
* Add element updates (non-UI) This can be controlled with autoinstall updates: security or all Also an API for controlling this: curl --silent --unix-socket .subiquity/socket a/updates -> "security" curl -d '"all"' --unix-socket .subiquity/socket a/updates * Automated tests - log grep for default/none/all states * Enforce possible values on Updates controller Route all the various get/set thru 2 common functions. Validate incoming data.
This commit is contained in:
parent
7111efbd61
commit
a0e2e244bd
|
@ -323,6 +323,13 @@
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"updates": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"security",
|
||||||
|
"all"
|
||||||
|
]
|
||||||
|
},
|
||||||
"late-commands": {
|
"late-commands": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"items": {
|
"items": {
|
||||||
|
|
|
@ -13,6 +13,7 @@ late-commands:
|
||||||
- echo a
|
- echo a
|
||||||
keyboard:
|
keyboard:
|
||||||
layout: gb
|
layout: gb
|
||||||
|
updates: security
|
||||||
user-data:
|
user-data:
|
||||||
users:
|
users:
|
||||||
- username: ubuntu
|
- username: ubuntu
|
||||||
|
|
|
@ -37,6 +37,7 @@ identity:
|
||||||
username: ubuntu
|
username: ubuntu
|
||||||
password: '$6$wdAcoXrU039hKYPd$508Qvbe7ObUnxoj15DRCkzC3qO7edjH0VV7BPNRDYK4QR8ofJaEEF2heacn0QgD.f8pO8SNp83XNdWG6tocBM1'
|
password: '$6$wdAcoXrU039hKYPd$508Qvbe7ObUnxoj15DRCkzC3qO7edjH0VV7BPNRDYK4QR8ofJaEEF2heacn0QgD.f8pO8SNp83XNdWG6tocBM1'
|
||||||
hostname: ubuntu
|
hostname: ubuntu
|
||||||
|
updates: all
|
||||||
storage:
|
storage:
|
||||||
config:
|
config:
|
||||||
- {type: disk, ptable: gpt, path: /dev/vdb, wipe: superblock, preserve: false, grub_device: true, id: disk-1}
|
- {type: disk, ptable: gpt, path: /dev/vdb, wipe: superblock, preserve: false, grub_device: true, id: disk-1}
|
||||||
|
|
|
@ -36,6 +36,7 @@ for answers in examples/answers*.yaml; do
|
||||||
# The --foreground is important to avoid subiquity getting SIGTTOU-ed.
|
# 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
|
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
|
validate
|
||||||
|
grep -q 'finish: subiquity/Install/install/run_unattended_upgrades: SUCCESS: downloading and installing security updates' .subiquity/subiquity-server-debug.log
|
||||||
done
|
done
|
||||||
|
|
||||||
clean
|
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' \
|
grep -q 'finish: subiquity/Install/install/postinstall/install_package2: SUCCESS: installing package2' \
|
||||||
.subiquity/subiquity-server-debug.log
|
.subiquity/subiquity-server-debug.log
|
||||||
grep -q 'switching subiquity to edge' .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
|
clean
|
||||||
timeout --foreground 60 sh -c "LANG=C.UTF-8 python3 -m subiquity.cmd.tui --autoinstall examples/autoinstall-user-data.yaml \
|
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'"
|
--dry-run --machine-config examples/simple.json --kernel-cmdline 'autoinstall'"
|
||||||
validate
|
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"
|
python3 -m subiquity.cmd.schema > "$testschema"
|
||||||
diff -u "autoinstall-schema.json" "$testschema"
|
diff -u "autoinstall-schema.json" "$testschema"
|
||||||
|
|
|
@ -47,11 +47,12 @@ from subiquity.common.types import (
|
||||||
@api
|
@api
|
||||||
class API:
|
class API:
|
||||||
"""The API offered by the subiquity installer process."""
|
"""The API offered by the subiquity installer process."""
|
||||||
locale = simple_endpoint(str)
|
|
||||||
proxy = simple_endpoint(str)
|
|
||||||
mirror = simple_endpoint(str)
|
|
||||||
identity = simple_endpoint(IdentityData)
|
identity = simple_endpoint(IdentityData)
|
||||||
|
locale = simple_endpoint(str)
|
||||||
|
mirror = simple_endpoint(str)
|
||||||
|
proxy = simple_endpoint(str)
|
||||||
ssh = simple_endpoint(SSHData)
|
ssh = simple_endpoint(SSHData)
|
||||||
|
updates = simple_endpoint(str)
|
||||||
|
|
||||||
class meta:
|
class meta:
|
||||||
class status:
|
class status:
|
||||||
|
|
|
@ -29,14 +29,15 @@ from subiquitycore.file_util import write_file
|
||||||
from subiquitycore.utils import run_command
|
from subiquitycore.utils import run_command
|
||||||
|
|
||||||
from .filesystem import FilesystemModel
|
from .filesystem import FilesystemModel
|
||||||
|
from .identity import IdentityModel
|
||||||
from .keyboard import KeyboardModel
|
from .keyboard import KeyboardModel
|
||||||
from .locale import LocaleModel
|
from .locale import LocaleModel
|
||||||
from .proxy import ProxyModel
|
|
||||||
from .mirror import MirrorModel
|
from .mirror import MirrorModel
|
||||||
from .network import NetworkModel
|
from .network import NetworkModel
|
||||||
|
from .proxy import ProxyModel
|
||||||
from .snaplist import SnapListModel
|
from .snaplist import SnapListModel
|
||||||
from .ssh import SSHModel
|
from .ssh import SSHModel
|
||||||
from .identity import IdentityModel
|
from .updates import UpdatesModel
|
||||||
|
|
||||||
|
|
||||||
log = logging.getLogger('subiquity.models.subiquity')
|
log = logging.getLogger('subiquity.models.subiquity')
|
||||||
|
@ -117,6 +118,7 @@ class SubiquityModel:
|
||||||
self.proxy = ProxyModel()
|
self.proxy = ProxyModel()
|
||||||
self.snaplist = SnapListModel()
|
self.snaplist = SnapListModel()
|
||||||
self.ssh = SSHModel()
|
self.ssh = SSHModel()
|
||||||
|
self.updates = UpdatesModel()
|
||||||
self.userdata = {}
|
self.userdata = {}
|
||||||
|
|
||||||
self.confirmation = asyncio.Event()
|
self.confirmation = asyncio.Event()
|
||||||
|
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
log = logging.getLogger('subiquity.models.updates')
|
||||||
|
|
||||||
|
|
||||||
|
class UpdatesModel(object):
|
||||||
|
""" Model representing updates selection"""
|
||||||
|
|
||||||
|
updates = 'security'
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return "<Updates: {}>".format(self.updates)
|
|
@ -29,6 +29,7 @@ from .refresh import RefreshController
|
||||||
from .reporting import ReportingController
|
from .reporting import ReportingController
|
||||||
from .snaplist import SnapListController
|
from .snaplist import SnapListController
|
||||||
from .ssh import SSHController
|
from .ssh import SSHController
|
||||||
|
from .updates import UpdatesController
|
||||||
from .userdata import UserdataController
|
from .userdata import UserdataController
|
||||||
from .zdev import ZdevController
|
from .zdev import ZdevController
|
||||||
|
|
||||||
|
@ -51,6 +52,7 @@ __all__ = [
|
||||||
'ReportingController',
|
'ReportingController',
|
||||||
'SnapListController',
|
'SnapListController',
|
||||||
'SSHController',
|
'SSHController',
|
||||||
|
'UpdatesController',
|
||||||
'UserdataController',
|
'UserdataController',
|
||||||
'ZdevController',
|
'ZdevController',
|
||||||
]
|
]
|
||||||
|
|
|
@ -14,13 +14,13 @@
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
|
import contextlib
|
||||||
import datetime
|
import datetime
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import shutil
|
import shutil
|
||||||
import sys
|
import sys
|
||||||
import tempfile
|
|
||||||
|
|
||||||
from curtin.commands.install import (
|
from curtin.commands.install import (
|
||||||
ERROR_TARFILE,
|
ERROR_TARFILE,
|
||||||
|
@ -135,7 +135,7 @@ class InstallController(SubiquityController):
|
||||||
|
|
||||||
def _write_config(self, path, config):
|
def _write_config(self, path, config):
|
||||||
with open(path, 'w') as conf:
|
with open(path, 'w') as conf:
|
||||||
datestr = '# Autogenerated by SUbiquity: {} UTC\n'.format(
|
datestr = '# Autogenerated by Subiquity: {} UTC\n'.format(
|
||||||
str(datetime.datetime.utcnow()))
|
str(datetime.datetime.utcnow()))
|
||||||
conf.write(datestr)
|
conf.write(datestr)
|
||||||
conf.write(yaml.dump(config))
|
conf.write(yaml.dump(config))
|
||||||
|
@ -234,7 +234,9 @@ class InstallController(SubiquityController):
|
||||||
|
|
||||||
if self.model.network.has_network:
|
if self.model.network.has_network:
|
||||||
self.app.update_state(ApplicationState.UU_RUNNING)
|
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)
|
self.app.update_state(ApplicationState.DONE)
|
||||||
except Exception:
|
except Exception:
|
||||||
|
@ -307,31 +309,31 @@ class InstallController(SubiquityController):
|
||||||
for cmd in cmds:
|
for cmd in cmds:
|
||||||
await arun_command(self.logged_command(cmd), check=True)
|
await arun_command(self.logged_command(cmd), check=True)
|
||||||
|
|
||||||
@with_context(description="downloading and installing security updates")
|
@with_context(description="downloading and installing {policy} updates")
|
||||||
async def run_unattended_upgrades(self, context):
|
async def run_unattended_upgrades(self, context, policy):
|
||||||
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
|
|
||||||
if self.app.opts.dry_run:
|
if self.app.opts.dry_run:
|
||||||
self.unattended_upgrades_proc = await astart_command(
|
command = ["sleep", str(5/self.app.scale_factor)]
|
||||||
self.logged_command(
|
aptdir = os.path.join(self.model.target, "tmp")
|
||||||
["sleep", str(5/self.app.scale_factor)]), env=env)
|
|
||||||
else:
|
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.unattended_upgrades_proc = await astart_command(
|
||||||
self.logged_command([
|
self.logged_command(command))
|
||||||
sys.executable, "-m", "curtin", "in-target", "-t",
|
await self.unattended_upgrades_proc.communicate()
|
||||||
"/target", "--", "unattended-upgrades", "-v",
|
self.unattended_upgrades_proc = None
|
||||||
]), env=env)
|
self.unattended_upgrades_ctx = None
|
||||||
await self.unattended_upgrades_proc.communicate()
|
|
||||||
self.unattended_upgrades_proc = None
|
|
||||||
self.unattended_upgrades_ctx = None
|
|
||||||
os.remove(apt_conf.name)
|
|
||||||
|
|
||||||
async def stop_unattended_upgrades(self):
|
async def stop_unattended_upgrades(self):
|
||||||
with self.unattended_upgrades_ctx.parent.child(
|
with self.unattended_upgrades_ctx.parent.child(
|
||||||
|
@ -349,9 +351,43 @@ class InstallController(SubiquityController):
|
||||||
]), check=True)
|
]), 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
|
# Config for the unattended-upgrades run to avoid failing on battery power or
|
||||||
# a metered connection.
|
# a metered connection.
|
||||||
Unattended-Upgrade::OnlyOnACPower "false";
|
Unattended-Upgrade::OnlyOnACPower "false";
|
||||||
Unattended-Upgrade::Skip-Updates-On-Metered-Connections "true";
|
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";
|
||||||
|
};
|
||||||
|
"""
|
||||||
|
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
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)
|
|
@ -188,6 +188,7 @@ class SubiquityServer(Application):
|
||||||
"SSH",
|
"SSH",
|
||||||
"SnapList",
|
"SnapList",
|
||||||
"Install",
|
"Install",
|
||||||
|
"Updates",
|
||||||
"Late",
|
"Late",
|
||||||
"Reboot",
|
"Reboot",
|
||||||
]
|
]
|
||||||
|
|
Loading…
Reference in New Issue