Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

codecs: skip installation when running an offline install #1812

Merged
merged 1 commit into from
Oct 2, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions examples/autoinstall/fallback-offline.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
version: 1
locale: en_GB.UTF-8
network:
version: 2
ethernets:
all-eth:
match:
name: "en*"
dhcp6: yes
apt:
mirror-selection:
primary:
- uri: http://localhost/failed
fallback: offline-install
codecs:
install: true
identity:
realname: ''
username: ubuntu
password: '$6$wdAcoXrU039hKYPd$508Qvbe7ObUnxoj15DRCkzC3qO7edjH0VV7BPNRDYK4QR8ofJaEEF2heacn0QgD.f8pO8SNp83XNdWG6tocBM1'
hostname: ubuntu
15 changes: 15 additions & 0 deletions scripts/runtests.sh
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,9 @@ validate () {
apt.security[1].uri='"http://ports.ubuntu.com/ubuntu-ports"'
;;
esac
if [ "$testname" == autoinstall-fallback-offline ]; then
grep -F -- 'skipping installation of package ubuntu-restricted-addons' "$tmpdir"/subiquity-server-debug.log
fi
netplan generate --root $tmpdir
elif [ "${mode}" = "system_setup" ]; then
setup_mode="$2"
Expand Down Expand Up @@ -319,6 +322,18 @@ LANG=C.UTF-8 timeout --foreground 60 \
--source-catalog examples/sources/install.yaml
validate install

clean
testname=autoinstall-fallback-offline
LANG=C.UTF-8 timeout --foreground 60 \
python3 -m subiquity.cmd.tui \
--dry-run \
--output-base "$tmpdir" \
--machine-config examples/machines/simple.json \
--autoinstall examples/autoinstall/fallback-offline.yaml \
--kernel-cmdline autoinstall \
--source-catalog examples/sources/install.yaml
validate install

# The OOBE doesn't exist in WSL < 20.04
if [ "${RELEASE%.*}" -ge 20 ]; then
# Test TCP connectivity (system_setup only)
Expand Down
25 changes: 25 additions & 0 deletions subiquity/common/pkg.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Copyright 2023 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 attr


@attr.s(auto_attribs=True)
class TargetPkg:
name: str
# Some packages are not present in the pool and require a working network
# connection to be downloaded. By marking them with "skip_when_offline", we
# can skip them when running an offline install.
skip_when_offline: bool
11 changes: 8 additions & 3 deletions subiquity/models/ad.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,9 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.

import logging
from typing import Optional
from typing import List, Optional

from subiquity.common.pkg import TargetPkg
from subiquity.common.types import AdConnectionInfo

log = logging.getLogger("subiquity.models.ad")
Expand All @@ -42,10 +43,14 @@ def set_domain(self, domain: str):
else:
self.conn_info = AdConnectionInfo(domain_name=domain)

async def target_packages(self):
async def target_packages(self) -> List[TargetPkg]:
# NOTE Those packages must be present in the target system to allow
# joining to a domain.
if self.do_join:
return ["adcli", "realmd", "sssd"]
return [
TargetPkg(name="adcli", skip_when_offline=False),
TargetPkg(name="realmd", skip_when_offline=False),
TargetPkg(name="sssd", skip_when_offline=False),
]

return []
10 changes: 8 additions & 2 deletions subiquity/models/codecs.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,22 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.

import logging
from typing import List

from subiquity.common.pkg import TargetPkg

log = logging.getLogger("subiquity.models.codecs")


class CodecsModel:
do_install = False

async def target_packages(self):
async def target_packages(self) -> List[TargetPkg]:
# NOTE currently, ubuntu-restricted-addons is an empty package that
# pulls relevant packages through Recommends: Ideally, we should make
# sure to run the APT command for this package with the
# --install-recommends option.
return ["ubuntu-restricted-addons"] if self.do_install else []
if not self.do_install:
return []

return [TargetPkg(name="ubuntu-restricted-addons", skip_when_offline=True)]
7 changes: 5 additions & 2 deletions subiquity/models/locale.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@
import locale
import logging
import subprocess
from typing import List

from subiquity.common.pkg import TargetPkg
from subiquitycore.utils import arun_command, split_cmd_output

log = logging.getLogger("subiquity.models.locale")
Expand Down Expand Up @@ -60,7 +62,7 @@ def make_cloudconfig(self):
locale += ".UTF-8"
return {"locale": locale}

async def target_packages(self):
async def target_packages(self) -> List[TargetPkg]:
if self.selected_language is None:
return []
if self.locale_support != "langpack":
Expand All @@ -69,6 +71,7 @@ async def target_packages(self):
if lang == "C":
return []

return await split_cmd_output(
pkgs = await split_cmd_output(
self.chroot_prefix + ["check-language-support", "-l", lang], None
)
return [TargetPkg(name=pkg, skip_when_offline=False) for pkg in pkgs]
10 changes: 6 additions & 4 deletions subiquity/models/network.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,10 @@

import logging
import subprocess
from typing import List

from subiquity import cloudinit
from subiquity.common.pkg import TargetPkg
from subiquitycore.models.network import NetworkModel as CoreNetworkModel
from subiquitycore.utils import arun_command

Expand Down Expand Up @@ -93,12 +95,12 @@ def render(self):
}
return r

async def target_packages(self):
if self.needs_wpasupplicant:
return ["wpasupplicant"]
else:
async def target_packages(self) -> List[TargetPkg]:
if not self.needs_wpasupplicant:
return []

return [TargetPkg(name="wpasupplicant", skip_when_offline=False)]

async def is_nm_enabled(self):
try:
cp = await arun_command(("nmcli", "networking"), check=True)
Expand Down
10 changes: 6 additions & 4 deletions subiquity/models/ssh.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
import logging
from typing import List

from subiquity.common.pkg import TargetPkg

log = logging.getLogger("subiquity.models.ssh")


Expand All @@ -29,8 +31,8 @@ def __init__(self):
# we go back to it.
self.ssh_import_id = ""

async def target_packages(self):
if self.install_server:
return ["openssh-server"]
else:
async def target_packages(self) -> List[TargetPkg]:
if not self.install_server:
return []

return [TargetPkg(name="openssh-server", skip_when_offline=False)]
12 changes: 9 additions & 3 deletions subiquity/models/subiquity.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
import os
import uuid
from collections import OrderedDict
from typing import Any, Dict, Set, Tuple
from typing import Any, Dict, List, Set, Tuple

import yaml
from cloudinit.config.schema import (
Expand All @@ -39,6 +39,7 @@ def SchemaProblem(x, y):

from curtin.config import merge_config

from subiquity.common.pkg import TargetPkg
from subiquity.common.resources import get_users_and_groups
from subiquity.server.types import InstallerChannels
from subiquitycore.file_util import generate_timestamped_header, write_file
Expand Down Expand Up @@ -177,6 +178,9 @@ def __init__(self, root, hub, install_model_names, postinstall_model_names):
self.chroot_prefix = []

self.active_directory = AdModel()
# List of packages that will be installed using cloud-init on first
# boot.
self.cloud_init_packages: List[str] = []
self.codecs = CodecsModel()
self.debconf_selections = DebconfSelectionsModel()
self.drivers = DriversModel()
Expand All @@ -189,7 +193,7 @@ def __init__(self, root, hub, install_model_names, postinstall_model_names):
self.mirror = MirrorModel()
self.network = NetworkModel()
self.oem = OEMModel()
self.packages = []
self.packages: List[TargetPkg] = []
self.proxy = ProxyModel()
self.snaplist = SnapListModel()
self.ssh = SSHModel()
Expand Down Expand Up @@ -376,13 +380,15 @@ def _cloud_init_config(self):
model = getattr(self, model_name)
if getattr(model, "make_cloudconfig", None):
merge_config(config, model.make_cloudconfig())
for package in self.cloud_init_packages:
merge_config(config, {"packages": list(self.cloud_init_packages)})
merge_cloud_init_config(config, self.userdata)
if lsb_release()["release"] not in ("20.04", "22.04"):
config.setdefault("write_files", []).append(CLOUDINIT_DISABLE_AFTER_INSTALL)
self.validate_cloudconfig_schema(data=config, data_source="system install")
return config

async def target_packages(self):
async def target_packages(self) -> List[TargetPkg]:
packages = list(self.packages)
for model_name in self._postinstall_model_names.all():
meth = getattr(getattr(self, model_name), "target_packages", None)
Expand Down
22 changes: 17 additions & 5 deletions subiquity/server/controllers/install.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
from curtin.util import get_efibootmgr, is_uefi_bootable

from subiquity.common.errorreport import ErrorReportKind
from subiquity.common.pkg import TargetPkg
from subiquity.common.types import ApplicationState, PackageInstallState
from subiquity.journald import journald_listen
from subiquity.models.filesystem import ActionRenderMode, Partition
Expand Down Expand Up @@ -689,11 +690,22 @@ async def postinstall(self, *, context):
{"autoinstall": self.app.make_autoinstall()}
)
write_file(autoinstall_path, autoinstall_config)
await self.configure_cloud_init(context=context)
try:
if self.supports_apt():
packages = await self.get_target_packages(context=context)
for package in packages:
if package.skip_when_offline and not self.model.network.has_network:
log.warning(
"skipping installation of package %s when"
" performing an offline install.",
package.name,
)
continue
await self.install_package(context=context, package=package.name)
finally:
await self.configure_cloud_init(context=context)

if self.supports_apt():
packages = await self.get_target_packages(context=context)
for package in packages:
await self.install_package(context=context, package=package)
if self.model.drivers.do_install:
with context.child(
"ubuntu-drivers-install", "installing third-party drivers"
Expand All @@ -720,7 +732,7 @@ async def configure_cloud_init(self, context):
await run_in_thread(self.model.configure_cloud_init)

@with_context(description="calculating extra packages to install")
async def get_target_packages(self, context):
async def get_target_packages(self, context) -> List[TargetPkg]:
return await self.app.base_model.target_packages()

@with_context(name="install_{package}", description="installing {package}")
Expand Down
5 changes: 3 additions & 2 deletions subiquity/server/controllers/package.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
# 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/>.

from subiquity.common.pkg import TargetPkg
from subiquity.server.controller import NonInteractiveController


Expand All @@ -25,7 +26,7 @@ class PackageController(NonInteractiveController):
}

def load_autoinstall_data(self, data):
self.model[:] = data
self.model[:] = [TargetPkg(name=pkg, skip_when_offline=False) for pkg in data]

def make_autoinstall(self):
return self.model
return [pkg.name for pkg in self.model]
2 changes: 1 addition & 1 deletion subiquity/tests/api/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -2133,7 +2133,7 @@ async def packages_lookup(self, log_dir: str) -> Dict[str, bool]:
to be installed in the target system and whether they were
referred to or not in the server log."""
expected_packages = await self.target_packages()
packages_lookup = {p: False for p in expected_packages}
packages_lookup = {p.name: False for p in expected_packages}
log_path = os.path.join(log_dir, "subiquity-server-debug.log")
find_start = "finish: subiquity/Install/install/postinstall/install_{}:"
log_status = " SUCCESS: installing {}"
Expand Down
Loading