depot/third_party/nixpkgs/nixos/modules/virtualisation/xen-dom0.nix
Default email 555cd8a8f9 Project import generated by Copybara.
GitOrigin-RevId: 5633bcff0c6162b9e4b5f1264264611e950c8ec7
2024-10-09 18:51:18 +02:00

875 lines
30 KiB
Nix

# Xen Project Hypervisor (Dom0) support.
{
config,
lib,
pkgs,
...
}:
let
inherit (builtins) readFile;
inherit (lib.modules) mkRemovedOptionModule mkRenamedOptionModule mkIf;
inherit (lib.options)
mkOption
mkEnableOption
literalExpression
mkPackageOption
;
inherit (lib.types)
listOf
str
ints
lines
enum
path
submodule
addCheck
float
bool
int
nullOr
;
inherit (lib.lists) optional optionals;
inherit (lib.strings) hasSuffix optionalString;
inherit (lib.meta) getExe;
inherit (lib.attrsets) optionalAttrs;
inherit (lib.trivial) boolToString;
inherit (lib.teams.xen) members;
cfg = config.virtualisation.xen;
xenBootBuilder = pkgs.writeShellApplication {
name = "xenBootBuilder";
runtimeInputs =
(with pkgs; [
binutils
coreutils
findutils
gawk
gnugrep
gnused
jq
])
++ optionals (cfg.efi.bootBuilderVerbosity == "info") (
with pkgs;
[
bat
diffutils
]
);
runtimeEnv = {
efiMountPoint = config.boot.loader.efi.efiSysMountPoint;
};
# We disable SC2016 because we don't want to expand the regexes in the sed commands.
excludeShellChecks = [ "SC2016" ];
text = readFile ./xen-boot-builder.sh;
};
in
{
imports = [
(mkRemovedOptionModule
[
"virtualisation"
"xen"
"bridge"
"name"
]
"The Xen Network Bridge options are currently unavailable. Please set up your own bridge manually."
)
(mkRemovedOptionModule
[
"virtualisation"
"xen"
"bridge"
"address"
]
"The Xen Network Bridge options are currently unavailable. Please set up your own bridge manually."
)
(mkRemovedOptionModule
[
"virtualisation"
"xen"
"bridge"
"prefixLength"
]
"The Xen Network Bridge options are currently unavailable. Please set up your own bridge manually."
)
(mkRemovedOptionModule
[
"virtualisation"
"xen"
"bridge"
"forwardDns"
]
"The Xen Network Bridge options are currently unavailable. Please set up your own bridge manually."
)
(mkRenamedOptionModule
[
"virtualisation"
"xen"
"qemu-package"
]
[
"virtualisation"
"xen"
"qemu"
"package"
]
)
(mkRenamedOptionModule
[
"virtualisation"
"xen"
"package-qemu"
]
[
"virtualisation"
"xen"
"qemu"
"package"
]
)
(mkRenamedOptionModule
[
"virtualisation"
"xen"
"stored"
]
[
"virtualisation"
"xen"
"store"
"path"
]
)
];
## Interface ##
options.virtualisation.xen = {
enable = mkEnableOption "the Xen Project Hypervisor, a virtualisation technology defined as a *type-1 hypervisor*, which allows multiple virtual machines, known as *domains*, to run concurrently on the physical machine. NixOS runs as the privileged *Domain 0*. This option requires a reboot into a Xen kernel to take effect";
debug = mkEnableOption "Xen debug features for Domain 0. This option enables some hidden debugging tests and features, and should not be used in production";
trace = mkOption {
type = bool;
default = cfg.debug;
defaultText = literalExpression "false";
example = true;
description = "Whether to enable Xen debug tracing and logging for Domain 0.";
};
package = mkPackageOption pkgs "Xen Hypervisor" { default = [ "xen" ]; };
qemu = {
package = mkPackageOption pkgs "QEMU (with Xen Hypervisor support)" {
default = [ "qemu_xen" ];
};
pidFile = mkOption {
type = path;
default = "/run/xen/qemu-dom0.pid";
example = "/var/run/xen/qemu-dom0.pid";
description = "Path to the QEMU PID file.";
};
};
bootParams = mkOption {
default = [ ];
example = ''
[
"iommu=force:true,qinval:true,debug:true"
"noreboot=true"
"vga=ask"
]
'';
type = listOf str;
description = ''
Xen Command Line parameters passed to Domain 0 at boot time.
Note: these are different from `boot.kernelParams`. See
the [Xen documentation](https://xenbits.xenproject.org/docs/unstable/misc/xen-command-line.html) for more information.
'';
};
efi = {
bootBuilderVerbosity = mkOption {
type = enum [
"default"
"info"
"debug"
"quiet"
];
default = "default";
example = "info";
description = ''
The EFI boot entry builder script should be called with exactly one of the following arguments in order to specify its verbosity:
- `quiet` supresses all messages.
- `default` adds a simple "Installing Xen Project Hypervisor boot entries...done." message to the script.
- `info` is the same as `default`, but it also prints a diff with information on which generations were altered.
- This option adds two extra dependencies to the script: `diffutils` and `bat`.
- `debug` prints information messages for every single step of the script.
This option does not alter the actual functionality of the script, just the number of messages printed when rebuilding the system.
'';
};
path = mkOption {
type = path;
default = "${cfg.package.boot}/${cfg.package.efi}";
defaultText = literalExpression "\${config.virtualisation.xen.package.boot}/\${config.virtualisation.xen.package.efi}";
example = literalExpression "\${config.virtualisation.xen.package}/boot/efi/efi/nixos/xen-\${config.virtualisation.xen.package.version}.efi";
description = ''
Path to xen.efi. `pkgs.xen` is patched to install the xen.efi file
on `$boot/boot/xen.efi`, but an unpatched Xen build may install it
somewhere else, such as `$out/boot/efi/efi/nixos/xen.efi`. Unless
you're building your own Xen derivation, you should leave this
option as the default value.
'';
};
};
dom0Resources = {
maxVCPUs = mkOption {
default = 0;
example = 4;
type = ints.unsigned;
description = ''
Amount of virtual CPU cores allocated to Domain 0 on boot.
If set to 0, all cores are assigned to Domain 0, and
unprivileged domains will compete with Domain 0 for CPU time.
'';
};
memory = mkOption {
default = 0;
example = 512;
type = ints.unsigned;
description = ''
Amount of memory (in MiB) allocated to Domain 0 on boot.
If set to 0, all memory is assigned to Domain 0, and
unprivileged domains will compete with Domain 0 for free RAM.
'';
};
maxMemory = mkOption {
default = cfg.dom0Resources.memory;
defaultText = literalExpression "config.virtualisation.xen.dom0Resources.memory";
example = 1024;
type = ints.unsigned;
description = ''
Maximum amount of memory (in MiB) that Domain 0 can
dynamically allocate to itself. Does nothing if set
to the same amount as virtualisation.xen.memory, or
if that option is set to 0.
'';
};
};
domains = {
extraConfig = mkOption {
type = lines;
default = "";
example = ''
XENDOMAINS_SAVE=/persist/xen/save
XENDOMAINS_RESTORE=false
XENDOMAINS_CREATE_USLEEP=10000000
'';
description = ''
Options defined here will override the defaults for xendomains.
The default options can be seen in the file included from
/etc/default/xendomains.
'';
};
};
store = {
path = mkOption {
type = path;
default = "${cfg.package}/bin/oxenstored";
defaultText = literalExpression "\${config.virtualisation.xen.package}/bin/oxenstored";
example = literalExpression "\${config.virtualisation.xen.package}/bin/xenstored";
description = ''
Path to the Xen Store Daemon. This option is useful to
switch between the legacy C-based Xen Store Daemon, and
the newer OCaml-based Xen Store Daemon, `oxenstored`.
'';
};
type = mkOption {
type = enum [
"c"
"ocaml"
];
default = if (hasSuffix "oxenstored" cfg.store.path) then "ocaml" else "c";
internal = true;
readOnly = true;
description = "Helper internal option that determines the type of the Xen Store Daemon based on cfg.store.path.";
};
settings = mkOption {
default = { };
example = {
enableMerge = false;
quota.maxWatchEvents = 2048;
quota.enable = true;
conflict.maxHistorySeconds = 0.12;
conflict.burstLimit = 15.0;
xenstored.log.file = "/dev/null";
xenstored.log.level = "info";
};
description = ''
The OCaml-based Xen Store Daemon configuration. This
option does nothing with the C-based `xenstored`.
'';
type = submodule {
options = {
pidFile = mkOption {
default = "/run/xen/xenstored.pid";
example = "/var/run/xen/xenstored.pid";
type = path;
description = "Path to the Xen Store Daemon PID file.";
};
testEAGAIN = mkOption {
default = cfg.debug;
defaultText = literalExpression "config.virtualisation.xen.debug";
example = true;
type = bool;
visible = false;
description = "Randomly fail a transaction with EAGAIN. This option is used for debugging purposes only.";
};
enableMerge = mkOption {
default = true;
example = false;
type = bool;
description = "Whether to enable transaction merge support.";
};
conflict = {
burstLimit = mkOption {
default = 5.0;
example = 15.0;
type = addCheck (
float
// {
name = "nonnegativeFloat";
description = "nonnegative floating point number, meaning >=0";
descriptionClass = "nonRestrictiveClause";
}
) (n: n >= 0);
description = ''
Limits applied to domains whose writes cause other domains' transaction
commits to fail. Must include decimal point.
The burst limit is the number of conflicts a domain can cause to
fail in a short period; this value is used for both the initial and
the maximum value of each domain's conflict-credit, which falls by
one point for each conflict caused, and when it reaches zero the
domain's requests are ignored.
'';
};
maxHistorySeconds = mkOption {
default = 5.0e-2;
example = 1.0;
type = addCheck (float // { description = "nonnegative floating point number, meaning >=0"; }) (
n: n >= 0
);
description = ''
Limits applied to domains whose writes cause other domains' transaction
commits to fail. Must include decimal point.
The conflict-credit is replenished over time:
one point is issued after each conflict.maxHistorySeconds, so this
is the minimum pause-time during which a domain will be ignored.
'';
};
rateLimitIsAggregate = mkOption {
default = true;
example = false;
type = bool;
description = ''
If the conflict.rateLimitIsAggregate option is `true`, then after each
tick one point of conflict-credit is given to just one domain: the
one at the front of the queue. If `false`, then after each tick each
domain gets a point of conflict-credit.
In environments where it is known that every transaction will
involve a set of nodes that is writable by at most one other domain,
then it is safe to set this aggregate limit flag to `false` for better
performance. (This can be determined by considering the layout of
the xenstore tree and permissions, together with the content of the
transactions that require protection.)
A transaction which involves a set of nodes which can be modified by
multiple other domains can suffer conflicts caused by any of those
domains, so the flag must be set to `true`.
'';
};
};
perms = {
enable = mkOption {
default = true;
example = false;
type = bool;
description = "Whether to enable the node permission system.";
};
enableWatch = mkOption {
default = true;
example = false;
type = bool;
description = ''
Whether to enable the watch permission system.
When this is set to `true`, unprivileged guests can only get watch events
for xenstore entries that they would've been able to read.
When this is set to `false`, unprivileged guests may get watch events
for xenstore entries that they cannot read. The watch event contains
only the entry name, not the value.
This restores behaviour prior to [XSA-115](https://xenbits.xenproject.org/xsa/advisory-115.html).
'';
};
};
quota = {
enable = mkOption {
default = true;
example = false;
type = bool;
description = "Whether to enable the quota system.";
};
maxEntity = mkOption {
default = 1000;
example = 1024;
type = ints.positive;
description = "Entity limit for transactions.";
};
maxSize = mkOption {
default = 2048;
example = 4096;
type = ints.positive;
description = "Size limit for transactions.";
};
maxWatch = mkOption {
default = 100;
example = 256;
type = ints.positive;
description = "Maximum number of watches by the Xenstore Watchdog.";
};
transaction = mkOption {
default = 10;
example = 50;
type = ints.positive;
description = "Maximum number of transactions.";
};
maxRequests = mkOption {
default = 1024;
example = 1024;
type = ints.positive;
description = "Maximum number of requests per transaction.";
};
maxPath = mkOption {
default = 1024;
example = 1024;
type = ints.positive;
description = "Path limit for the quota system.";
};
maxOutstanding = mkOption {
default = 1024;
example = 1024;
type = ints.positive;
description = "Maximum outstanding requests, i.e. in-flight requests / domain.";
};
maxWatchEvents = mkOption {
default = 1024;
example = 2048;
type = ints.positive;
description = "Maximum number of outstanding watch events per watch.";
};
};
persistent = mkOption {
default = false;
example = true;
type = bool;
description = "Whether to activate the filed base backend.";
};
xenstored = {
log = {
file = mkOption {
default = "/var/log/xen/xenstored.log";
example = "/dev/null";
type = path;
description = "Path to the Xen Store log file.";
};
level = mkOption {
default = if cfg.trace then "debug" else null;
defaultText = literalExpression "if (config.virtualisation.xen.trace == true) then \"debug\" else null";
example = "error";
type = nullOr (enum [
"debug"
"info"
"warn"
"error"
]);
description = "Logging level for the Xen Store.";
};
# The hidden options below have no upstream documentation whatsoever.
# The nb* options appear to alter the log rotation behaviour, and
# the specialOps option appears to affect the Xenbus logging logic.
nbFiles = mkOption {
default = 10;
example = 16;
type = int;
visible = false;
description = "Set `xenstored-log-nb-files`.";
};
};
accessLog = {
file = mkOption {
default = "/var/log/xen/xenstored-access.log";
example = "/var/log/security/xenstored-access.log";
type = path;
description = "Path to the Xen Store access log file.";
};
nbLines = mkOption {
default = 13215;
example = 16384;
type = int;
visible = false;
description = "Set `access-log-nb-lines`.";
};
nbChars = mkOption {
default = 180;
example = 256;
type = int;
visible = false;
description = "Set `acesss-log-nb-chars`.";
};
specialOps = mkOption {
default = false;
example = true;
type = bool;
visible = false;
description = "Set `access-log-special-ops`.";
};
};
xenfs = {
kva = mkOption {
default = "/proc/xen/xsd_kva";
example = cfg.store.settings.xenstored.xenfs.kva;
type = path;
visible = false;
description = ''
Path to the Xen Store Daemon KVA location inside the XenFS pseudo-filesystem.
While it is possible to alter this value, some drivers may be hardcoded to follow the default paths.
'';
};
port = mkOption {
default = "/proc/xen/xsd_port";
example = cfg.store.settings.xenstored.xenfs.port;
type = path;
visible = false;
description = ''
Path to the Xen Store Daemon userspace port inside the XenFS pseudo-filesystem.
While it is possible to alter this value, some drivers may be hardcoded to follow the default paths.
'';
};
};
};
ringScanInterval = mkOption {
default = 20;
example = 30;
type = addCheck (
int
// {
name = "nonzeroInt";
description = "nonzero signed integer, meaning !=0";
descriptionClass = "nonRestrictiveClause";
}
) (n: n != 0);
description = ''
Perodic scanning for all the rings as a safenet for lazy clients.
Define the interval in seconds; set to a negative integer to disable.
'';
};
};
};
};
};
};
## Implementation ##
config = mkIf cfg.enable {
assertions = [
{
assertion = pkgs.stdenv.hostPlatform.isx86_64;
message = "Xen is currently not supported on ${pkgs.stdenv.hostPlatform.system}.";
}
{
assertion =
config.boot.loader.systemd-boot.enable
|| (config.boot ? lanzaboote) && config.boot.lanzaboote.enable;
message = "Xen only supports booting on systemd-boot or Lanzaboote.";
}
{
assertion = config.boot.initrd.systemd.enable;
message = "Xen does not support the legacy script-based Stage 1 initrd.";
}
{
assertion = cfg.dom0Resources.maxMemory >= cfg.dom0Resources.memory;
message = ''
You have allocated more memory to dom0 than virtualisation.xen.dom0Resources.maxMemory
allows for. Please increase the maximum memory limit, or decrease the default memory allocation.
'';
}
{
assertion = cfg.debug -> cfg.trace;
message = "Xen's debugging features are enabled, but logging is disabled. This is most likely not what you want.";
}
{
assertion = cfg.store.settings.quota.maxWatchEvents >= cfg.store.settings.quota.maxOutstanding;
message = ''
Upstream Xen recommends that maxWatchEvents be equal to or greater than maxOutstanding,
in order to mitigate denial of service attacks from malicious frontends.
'';
}
];
virtualisation.xen.bootParams =
optionals cfg.trace [
"loglvl=all"
"guest_loglvl=all"
]
++
optional (cfg.dom0Resources.memory != 0)
"dom0_mem=${toString cfg.dom0Resources.memory}M${
optionalString (
cfg.dom0Resources.memory != cfg.dom0Resources.maxMemory
) ",max:${toString cfg.dom0Resources.maxMemory}M"
}"
++ optional (
cfg.dom0Resources.maxVCPUs != 0
) "dom0_max_vcpus=${toString cfg.dom0Resources.maxVCPUs}";
boot = {
kernelModules = [
"xen-evtchn"
"xen-gntdev"
"xen-gntalloc"
"xen-blkback"
"xen-netback"
"xen-pciback"
"evtchn"
"gntdev"
"netbk"
"blkbk"
"xen-scsibk"
"usbbk"
"pciback"
"xen-acpi-processor"
"blktap2"
"tun"
"netxen_nic"
"xen_wdt"
"xen-acpi-processor"
"xen-privcmd"
"xen-scsiback"
"xenfs"
];
# The xenfs module is needed to mount /proc/xen.
initrd.kernelModules = [ "xenfs" ];
# Increase the number of loopback devices from the default (8),
# which is way too small because every VM virtual disk requires a
# loopback device.
extraModprobeConfig = ''
options loop max_loop=64
'';
# Xen Bootspec extension. This extension allows NixOS bootloaders to
# fetch the `xen.efi` path and access the `cfg.bootParams` option.
bootspec.extensions = {
"org.xenproject.bootspec.v1" = {
xen = cfg.efi.path;
xenParams = cfg.bootParams;
};
};
# See the `xenBootBuilder` script in the main `let...in` statement of this file.
loader.systemd-boot.extraInstallCommands = ''
${getExe xenBootBuilder} ${cfg.efi.bootBuilderVerbosity}
'';
};
# Domain 0 requires a pvops-enabled kernel.
# All NixOS kernels come with this enabled by default; this is merely a sanity check.
system.requiredKernelConfig = with config.lib.kernelConfig; [
(isYes "XEN")
(isYes "X86_IO_APIC")
(isYes "ACPI")
(isYes "XEN_DOM0")
(isYes "PCI_XEN")
(isYes "XEN_DEV_EVTCHN")
(isYes "XENFS")
(isYes "XEN_COMPAT_XENFS")
(isYes "XEN_SYS_HYPERVISOR")
(isYes "XEN_GNTDEV")
(isYes "XEN_BACKEND")
(isModule "XEN_NETDEV_BACKEND")
(isModule "XEN_BLKDEV_BACKEND")
(isModule "XEN_PCIDEV_BACKEND")
(isYes "XEN_BALLOON")
(isYes "XEN_SCRUB_PAGES")
];
environment = {
systemPackages = [
cfg.package
cfg.qemu.package
];
etc =
# Set up Xen Domain 0 configuration files.
{
"xen/xl.conf".source = "${cfg.package}/etc/xen/xl.conf"; # TODO: Add options to configure xl.conf declaratively. It's worth considering making a new "xl value" type, as it could be reused to produce xl.cfg (domain definition) files.
"xen/scripts-xen" = {
source = "${cfg.package}/etc/xen/scripts/*";
target = "xen/scripts";
};
"default/xencommons".text = ''
source ${cfg.package}/etc/default/xencommons
XENSTORED="${cfg.store.path}"
QEMU_XEN="${cfg.qemu.package}/${cfg.qemu.package.qemu-system-i386}"
${optionalString cfg.trace ''
XENSTORED_TRACE=yes
XENCONSOLED_TRACE=all
''}
'';
"default/xendomains".text = ''
source ${cfg.package}/etc/default/xendomains
${cfg.domains.extraConfig}
'';
}
# The OCaml-based Xen Store Daemon requires /etc/xen/oxenstored.conf to start.
// optionalAttrs (cfg.store.type == "ocaml") {
"xen/oxenstored.conf".text = ''
pid-file = ${cfg.store.settings.pidFile}
test-eagain = ${boolToString cfg.store.settings.testEAGAIN}
merge-activate = ${toString cfg.store.settings.enableMerge}
conflict-burst-limit = ${toString cfg.store.settings.conflict.burstLimit}
conflict-max-history-seconds = ${toString cfg.store.settings.conflict.maxHistorySeconds}
conflict-rate-limit-is-aggregate = ${toString cfg.store.settings.conflict.rateLimitIsAggregate}
perms-activate = ${toString cfg.store.settings.perms.enable}
perms-watch-activate = ${toString cfg.store.settings.perms.enableWatch}
quota-activate = ${toString cfg.store.settings.quota.enable}
quota-maxentity = ${toString cfg.store.settings.quota.maxEntity}
quota-maxsize = ${toString cfg.store.settings.quota.maxSize}
quota-maxwatch = ${toString cfg.store.settings.quota.maxWatch}
quota-transaction = ${toString cfg.store.settings.quota.transaction}
quota-maxrequests = ${toString cfg.store.settings.quota.maxRequests}
quota-path-max = ${toString cfg.store.settings.quota.maxPath}
quota-maxoutstanding = ${toString cfg.store.settings.quota.maxOutstanding}
quota-maxwatchevents = ${toString cfg.store.settings.quota.maxWatchEvents}
persistent = ${boolToString cfg.store.settings.persistent}
xenstored-log-file = ${cfg.store.settings.xenstored.log.file}
xenstored-log-level = ${
if isNull cfg.store.settings.xenstored.log.level then
"null"
else
cfg.store.settings.xenstored.log.level
}
xenstored-log-nb-files = ${toString cfg.store.settings.xenstored.log.nbFiles}
access-log-file = ${cfg.store.settings.xenstored.accessLog.file}
access-log-nb-lines = ${toString cfg.store.settings.xenstored.accessLog.nbLines}
acesss-log-nb-chars = ${toString cfg.store.settings.xenstored.accessLog.nbChars}
access-log-special-ops = ${boolToString cfg.store.settings.xenstored.accessLog.specialOps}
ring-scan-interval = ${toString cfg.store.settings.ringScanInterval}
xenstored-kva = ${cfg.store.settings.xenstored.xenfs.kva}
xenstored-port = ${cfg.store.settings.xenstored.xenfs.port}
'';
};
};
# Xen provides udev rules.
services.udev.packages = [ cfg.package ];
systemd = {
# Xen provides systemd units.
packages = [ cfg.package ];
mounts = [
{
description = "Mount /proc/xen files";
what = "xenfs";
where = "/proc/xen";
type = "xenfs";
unitConfig = {
ConditionPathExists = "/proc/xen";
RefuseManualStop = "true";
};
}
];
services = {
# While this service is installed by the `xen` package, it shouldn't be used in dom0.
xendriverdomain.enable = false;
xenstored = {
wantedBy = [ "multi-user.target" ];
preStart = ''
export XENSTORED_ROOTDIR="/var/lib/xenstored"
rm -f "$XENSTORED_ROOTDIR"/tdb* &>/dev/null
mkdir -p /var/{run,log,lib}/xen
'';
};
xen-init-dom0 = {
restartIfChanged = false;
wantedBy = [ "multi-user.target" ];
};
xen-qemu-dom0-disk-backend = {
wantedBy = [ "multi-user.target" ];
serviceConfig = {
PIDFile = cfg.qemu.pidFile;
ExecStart = ''
${cfg.qemu.package}/${cfg.qemu.package.qemu-system-i386} \
-xen-domid 0 -xen-attach -name dom0 -nographic -M xenpv \
-daemonize -monitor /dev/null -serial /dev/null -parallel \
/dev/null -nodefaults -no-user-config -pidfile \
${cfg.qemu.pidFile}
'';
};
};
xenconsoled.wantedBy = [ "multi-user.target" ];
xen-watchdog = {
wantedBy = [ "multi-user.target" ];
serviceConfig = {
RestartSec = "1";
Restart = "on-failure";
};
};
xendomains = {
restartIfChanged = false;
path = [
cfg.package
cfg.qemu.package
];
preStart = "mkdir -p /var/lock/subsys -m 755";
wantedBy = [ "multi-user.target" ];
};
};
};
};
meta.maintainers = members;
}