push sheeet
Some checks failed
Periodic Merges (6h) / master → staging-nixos (push) Failing after 12m50s
Periodic Merges (6h) / master → staging-next (push) Failing after 12m54s
Periodic Merges (24h) / merge-base(master,staging) → haskell-updates (push) Failing after 11m54s
Periodic Merges (6h) / staging-next → staging (push) Failing after 12m13s
Periodic Merges (24h) / staging-next-25.05 → staging-25.05 (push) Failing after 13m24s
Periodic Merges (24h) / release-25.05 → staging-next-25.05 (push) Failing after 14m28s

This commit is contained in:
Dark Steveneq
2025-10-09 14:15:47 +02:00
commit 646b892680
49168 changed files with 5897842 additions and 0 deletions

View File

@@ -0,0 +1,5 @@
throw ''
This file is not the source for amazon AMIs anymore since 24.05.
The canonical source for NixOS AMIs is the AWS API. Please see
https://nixos.org/download/#nixos-amazon or https://nixos.github.io/amis/ for instructions
''

View File

@@ -0,0 +1,125 @@
# Configuration for Amazon EC2 instances. (Note that this file is a
# misnomer - it should be "amazon-config.nix" or so, not
# "amazon-image.nix", since it's used not only to build images but
# also to reconfigure instances. However, we can't rename it because
# existing "configuration.nix" files on EC2 instances refer to it.)
{
config,
lib,
pkgs,
...
}:
let
inherit (lib) mkDefault mkIf;
cfg = config.ec2;
in
{
imports = [
../profiles/headless.nix
# Note: While we do use the headless profile, we also explicitly
# turn on the serial console on ttyS0 below. This is because
# AWS does support accessing the serial console:
# https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/configure-access-to-serial-console.html
./ec2-data.nix
./amazon-init.nix
];
config = {
assertions = [ ];
boot.growPartition = true;
fileSystems."/" = mkIf (!cfg.zfs.enable) (
lib.mkDefault {
device = "/dev/disk/by-label/nixos";
fsType = "ext4";
autoResize = true;
}
);
fileSystems."/boot" = mkIf (cfg.efi || cfg.zfs.enable) (
lib.mkDefault {
# The ZFS image uses a partition labeled ESP whether or not we're
# booting with EFI.
device = "/dev/disk/by-label/ESP";
fsType = "vfat";
}
);
services.zfs.expandOnBoot = mkIf cfg.zfs.enable "all";
boot.zfs.devNodes = mkIf cfg.zfs.enable "/dev/";
boot.extraModulePackages = [
config.boot.kernelPackages.ena
];
boot.initrd.kernelModules = [ "xen-blkfront" ];
boot.initrd.availableKernelModules = [ "nvme" ];
boot.kernelParams = [
"console=ttyS0,115200n8"
"random.trust_cpu=on"
];
# Prevent the nouveau kernel module from being loaded, as it
# interferes with the nvidia/nvidia-uvm modules needed for CUDA.
# Also blacklist xen_fbfront to prevent a 30 second delay during
# boot.
boot.blacklistedKernelModules = [
"nouveau"
"xen_fbfront"
];
boot.loader.grub.device = if cfg.efi then "nodev" else "/dev/xvda";
boot.loader.grub.efiSupport = cfg.efi;
boot.loader.grub.efiInstallAsRemovable = cfg.efi;
boot.loader.timeout = 1;
boot.loader.grub.extraConfig = ''
serial --unit=0 --speed=115200 --word=8 --parity=no --stop=1
terminal_output console serial
terminal_input console serial
'';
systemd.services.fetch-ec2-metadata = {
wantedBy = [ "multi-user.target" ];
wants = [ "network-online.target" ];
after = [ "network-online.target" ];
path = [ pkgs.curl ];
script = builtins.readFile ./ec2-metadata-fetcher.sh;
serviceConfig.Type = "oneshot";
serviceConfig.StandardOutput = "journal+console";
};
# Amazon-issued AMIs include the SSM Agent by default, so we do the same.
# https://docs.aws.amazon.com/systems-manager/latest/userguide/ami-preinstalled-agent.html
services.amazon-ssm-agent.enable = true;
# Allow root logins only using the SSH key that the user specified
# at instance creation time.
services.openssh.enable = true;
services.openssh.settings.PermitRootLogin = "prohibit-password";
# Enable the serial console on ttyS0
systemd.services."serial-getty@ttyS0".enable = true;
# Creates symlinks for block device names.
services.udev.packages = [ pkgs.amazon-ec2-utils ];
# Force getting the hostname from EC2.
networking.hostName = mkDefault "";
# Always include cryptsetup so that Charon can use it.
environment.systemPackages = [ pkgs.cryptsetup ];
# EC2 has its own NTP server provided by the hypervisor
networking.timeServers = [ "169.254.169.123" ];
# udisks has become too bloated to have in a headless system
# (e.g. it depends on GTK).
services.udisks2.enable = false;
};
meta.maintainers = with lib.maintainers; [ arianvp ];
}

View File

@@ -0,0 +1,111 @@
{
config,
lib,
pkgs,
...
}:
with lib;
let
cfg = config.virtualisation.amazon-init;
script = ''
#!${pkgs.runtimeShell} -eu
echo "attempting to fetch configuration from EC2 user data..."
export HOME=/root
export PATH=${
pkgs.lib.makeBinPath [
config.nix.package
config.systemd.package
pkgs.gnugrep
pkgs.git
pkgs.gnutar
pkgs.gzip
pkgs.gnused
pkgs.xz
config.system.build.nixos-rebuild
]
}:$PATH
export NIX_PATH=nixpkgs=/nix/var/nix/profiles/per-user/root/channels/nixos:nixos-config=/etc/nixos/configuration.nix:/nix/var/nix/profiles/per-user/root/channels
userData=/etc/ec2-metadata/user-data
# Check if user-data looks like a shell script and execute it with the
# runtime shell if it does. Otherwise treat it as a nixos configuration
# expression
if IFS= LC_ALL=C read -rN2 shebang < $userData && [ "$shebang" = '#!' ]; then
# NB: we cannot chmod the $userData file, this is why we execute it via
# `pkgs.runtimeShell`. This means we have only limited support for shell
# scripts compatible with the `pkgs.runtimeShell`.
exec ${pkgs.runtimeShell} $userData
fi
if [ -s "$userData" ]; then
# If the user-data looks like it could be a nix expression,
# copy it over. Also, look for a magic three-hash comment and set
# that as the channel.
if sed '/^\(#\|SSH_HOST_.*\)/d' < "$userData" | grep -q '\S'; then
channels="$(grep '^###' "$userData" | sed 's|###\s*||')"
while IFS= read -r channel; do
echo "writing channel: $channel"
done < <(printf "%s\n" "$channels")
if [[ -n "$channels" ]]; then
printf "%s" "$channels" > /root/.nix-channels
nix-channel --update
fi
echo "setting configuration from EC2 user data"
cp "$userData" /etc/nixos/configuration.nix
else
echo "user data does not appear to be a Nix expression; ignoring"
exit
fi
else
echo "no user data is available"
exit
fi
nixos-rebuild switch
'';
in
{
options.virtualisation.amazon-init = {
enable = mkOption {
default = true;
type = types.bool;
description = ''
Enable or disable the amazon-init service.
'';
};
};
config = mkIf cfg.enable {
systemd.services.amazon-init = {
inherit script;
description = "Reconfigure the system from EC2 userdata on startup";
wantedBy = [ "multi-user.target" ];
after = [ "multi-user.target" ];
requires = [ "network-online.target" ];
path = [
"/run/wrappers"
"/run/current-system/sw"
];
restartIfChanged = false;
unitConfig.X-StopOnRemoval = false;
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
};
};
};
meta.maintainers = with maintainers; [ arianvp ];
}

View File

@@ -0,0 +1,84 @@
{
config,
lib,
pkgs,
...
}:
let
inherit (lib) literalExpression types;
in
{
options = {
ec2 = {
zfs = {
enable = lib.mkOption {
default = false;
internal = true;
description = ''
Whether the EC2 instance uses a ZFS root.
'';
};
datasets = lib.mkOption {
description = ''
Datasets to create under the `tank` and `boot` zpools.
**NOTE:** This option is used only at image creation time, and
does not attempt to declaratively create or manage datasets
on an existing system.
'';
default = { };
type = types.attrsOf (
types.submodule {
options = {
mount = lib.mkOption {
description = "Where to mount this dataset.";
type = types.nullOr types.str;
default = null;
};
properties = lib.mkOption {
description = "Properties to set on this dataset.";
type = types.attrsOf types.str;
default = { };
};
};
}
);
};
};
efi = lib.mkOption {
default = pkgs.stdenv.hostPlatform.isAarch64;
defaultText = literalExpression "pkgs.stdenv.hostPlatform.isAarch64";
internal = true;
description = ''
Whether the EC2 instance is using EFI.
'';
};
hvm = lib.mkOption {
description = "Unused legacy option. While support for non-hvm has been dropped, we keep this option around so that NixOps remains compatible with a somewhat recent `nixpkgs` and machines with an old `stateVersion`.";
internal = true;
default = true;
readOnly = true;
};
};
};
config = lib.mkIf config.ec2.zfs.enable {
networking.hostId = lib.mkDefault "00000000";
fileSystems =
let
mountable = lib.filterAttrs (_: value: ((value.mount or null) != null)) config.ec2.zfs.datasets;
in
lib.mapAttrs' (
dataset: opts:
lib.nameValuePair opts.mount {
device = dataset;
fsType = "zfs";
}
) mountable;
};
}

View File

@@ -0,0 +1,54 @@
{
config,
lib,
pkgs,
...
}:
with lib;
let
cfg = config.virtualisation.appvm;
in
{
options = {
virtualisation.appvm = {
enable = mkOption {
type = types.bool;
default = false;
description = ''
This enables AppVMs and related virtualisation settings.
'';
};
user = mkOption {
type = types.str;
description = ''
AppVM user login. Currently only AppVMs are supported for a single user only.
'';
};
};
};
config = mkIf cfg.enable {
virtualisation.libvirtd = {
enable = true;
qemu.verbatimConfig = ''
namespaces = []
user = "${cfg.user}"
group = "users"
remember_owner = 0
'';
};
users.users."${cfg.user}" = {
packages = [ pkgs.appvm ];
extraGroups = [ "libvirtd" ];
};
};
}

View File

@@ -0,0 +1,56 @@
{ lib, ... }:
with lib;
warn
''
`virtualisation.azure.agent` provided by `azure-agent.nix` module has been replaced
by `services.waagent` options, and will be removed in a future release.
''
{
imports = [
(mkRenamedOptionModule
[
"virtualisation"
"azure"
"agent"
"enable"
]
[
"services"
"waagent"
"enable"
]
)
(mkRenamedOptionModule
[
"virtualisation"
"azure"
"agent"
"verboseLogging"
]
[
"services"
"waagent"
"settings"
"Logs"
"Verbose"
]
)
(mkRenamedOptionModule
[
"virtualisation"
"azure"
"agent"
"mountResourceDisk"
]
[
"services"
"waagent"
"settings"
"ResourceDisk"
"Format"
]
)
];
}

View File

@@ -0,0 +1,4 @@
{
"16.03" =
"https://nixos.blob.core.windows.net/images/nixos-image-16.03.847.8688c17-x86_64-linux.vhd";
}

View File

@@ -0,0 +1,88 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.virtualisation.azure;
mlxDrivers = [
"mlx4_en"
"mlx4_core"
"mlx5_core"
];
in
{
options.virtualisation.azure = {
acceleratedNetworking = lib.mkOption {
default = false;
description = "Whether the machine's network interface has enabled accelerated networking.";
};
};
config = {
services.waagent.enable = true;
# Enable cloud-init by default for waagent.
# Otherwise waagent would try manage networking using ifupdown,
# which is currently not available in nixpkgs.
services.cloud-init.enable = true;
services.cloud-init.network.enable = true;
systemd.services.cloud-config.serviceConfig.Restart = "on-failure";
# cloud-init.network.enable also enables systemd-networkd
networking.useNetworkd = true;
# Ensure kernel outputs to ttyS0 (Azure Serial Console),
# and reboot machine upon fatal boot issues
boot.kernelParams = [
"console=ttyS0"
"earlyprintk=ttyS0"
"rootdelay=300"
"panic=1"
"boot.panic_on_fail"
];
# Load Hyper-V kernel modules
boot.initrd.kernelModules = [
"hv_vmbus"
"hv_netvsc"
"hv_utils"
"hv_storvsc"
];
# Accelerated networking, configured following:
# https://learn.microsoft.com/en-us/azure/virtual-network/accelerated-networking-overview
boot.initrd.availableKernelModules = lib.optionals cfg.acceleratedNetworking mlxDrivers;
systemd.network.networks."99-azure-unmanaged-devices.network" = lib.mkIf cfg.acceleratedNetworking {
matchConfig.Driver = mlxDrivers;
linkConfig.Unmanaged = "yes";
};
networking.networkmanager.unmanaged = lib.mkIf cfg.acceleratedNetworking (
builtins.map (drv: "driver:${drv}") mlxDrivers
);
# Allow root logins only using the SSH key that the user specified
# at instance creation time, ping client connections to avoid timeouts
services.openssh.enable = true;
services.openssh.settings.PermitRootLogin = "prohibit-password";
services.openssh.settings.ClientAliveInterval = 180;
# Force getting the hostname from Azure
networking.hostName = lib.mkDefault "";
# Always include cryptsetup so that NixOps can use it.
# sg_scan is needed to finalize disk removal on older kernels
environment.systemPackages = [
pkgs.cryptsetup
pkgs.sg3_utils
];
networking.usePredictableInterfaceNames = false;
services.udev.extraRules = lib.concatMapStrings (i: ''
ENV{DEVTYPE}=="disk", KERNEL!="sda" SUBSYSTEM=="block", SUBSYSTEMS=="scsi", KERNELS=="?:0:0:${toString i}", ATTR{removable}=="0", SYMLINK+="disk/by-lun/${toString i}"
'') (lib.range 1 15);
};
}

View File

@@ -0,0 +1,18 @@
{ modulesPath, ... }:
{
# To build the configuration or use nix-env, you need to run
# either nixos-rebuild --upgrade or nix-channel --update
# to fetch the nixos channel.
# This configures everything but bootstrap services,
# which only need to be run once and have already finished
# if you are able to see this comment.
imports = [
"${modulesPath}/virtualisation/azure-common.nix"
"${modulesPath}/virtualisation/azure-image.nix"
];
# Please set the VM Generation to the actual value
# virtualisation.azureImage.vmGeneration = "v1";
}

View File

@@ -0,0 +1,5 @@
{ modulesPath, ... }:
{
imports = [ "${modulesPath}/virtualisation/azure-image.nix" ];
}

View File

@@ -0,0 +1,130 @@
{
config,
lib,
pkgs,
...
}:
with lib;
let
cfg = config.virtualisation.azureImage;
in
{
imports = [
./azure-common.nix
./disk-size-option.nix
../image/file-options.nix
(lib.mkRenamedOptionModuleWith {
sinceRelease = 2411;
from = [
"virtualisation"
"azureImage"
"diskSize"
];
to = [
"virtualisation"
"diskSize"
];
})
];
options.virtualisation.azureImage = {
bootSize = mkOption {
type = types.int;
default = 256;
description = ''
ESP partition size. Unit is MB.
Only effective when vmGeneration is `v2`.
'';
};
contents = mkOption {
type = with types; listOf attrs;
default = [ ];
description = ''
Extra contents to add to the image.
'';
};
label = mkOption {
type = types.str;
default = "nixos";
description = ''
NixOS partition label.
'';
};
vmGeneration = mkOption {
type =
with types;
enum [
"v1"
"v2"
];
default = "v1";
description = ''
VM Generation to use.
For v2, secure boot needs to be turned off during creation.
'';
};
};
config = {
image.extension = "vhd";
system.nixos.tags = [ "azure" ];
system.build.image = config.system.build.azureImage;
system.build.azureImage = import ../../lib/make-disk-image.nix {
name = "azure-image";
inherit (config.image) baseName;
# Azure expects vhd format with fixed size,
# generating raw format and convert with subformat args afterwards
format = "raw";
postVM = ''
${pkgs.vmTools.qemu}/bin/qemu-img convert -f raw -o subformat=fixed,force_size -O vpc $diskImage $out/${config.image.fileName}
rm $diskImage
'';
configFile = ./azure-config-user.nix;
bootSize = "${toString cfg.bootSize}M";
partitionTableType = if (cfg.vmGeneration == "v2") then "efi" else "legacy";
inherit (cfg) contents label;
inherit (config.virtualisation) diskSize;
inherit config lib pkgs;
};
boot.growPartition = true;
boot.loader.grub = rec {
efiSupport = (cfg.vmGeneration == "v2");
device = if efiSupport then "nodev" else "/dev/sda";
efiInstallAsRemovable = efiSupport;
# Force grub to run in text mode and output to console
# by disabling font and splash image
font = null;
splashImage = null;
# For Gen 1 VM, configurate grub output to serial_com0.
# Not needed for Gen 2 VM wbere serial_com0 does not exist,
# and outputting to console is enough to make Azure Serial Console working
extraConfig = lib.mkIf (!efiSupport) ''
serial --unit=0 --speed=115200 --word=8 --parity=no --stop=1
terminal_input --append serial
terminal_output --append serial
'';
};
fileSystems = {
"/" = {
device = "/dev/disk/by-label/${cfg.label}";
inherit (cfg) label;
fsType = "ext4";
autoResize = true;
};
"/boot" = lib.mkIf (cfg.vmGeneration == "v2") {
device = "/dev/disk/by-label/ESP";
fsType = "vfat";
};
};
};
}

View File

@@ -0,0 +1,77 @@
{
config,
extendModules,
lib,
...
}:
let
inherit (lib)
mkOption
;
vmVariant = extendModules {
modules = [ ./qemu-vm.nix ];
};
vmVariantWithBootLoader = vmVariant.extendModules {
modules = [
(
{ config, ... }:
{
_file = "nixos/default.nix##vmWithBootLoader";
virtualisation.useBootLoader = true;
virtualisation.useEFIBoot =
config.boot.loader.systemd-boot.enable || config.boot.loader.efi.canTouchEfiVariables;
}
)
];
};
in
{
options = {
virtualisation.vmVariant = mkOption {
description = ''
Machine configuration to be added for the vm script produced by `nixos-rebuild build-vm`.
'';
inherit (vmVariant) type;
default = { };
visible = "shallow";
};
virtualisation.vmVariantWithBootLoader = mkOption {
description = ''
Machine configuration to be added for the vm script produced by `nixos-rebuild build-vm-with-bootloader`.
'';
inherit (vmVariantWithBootLoader) type;
default = { };
visible = "shallow";
};
};
config = {
system.build = {
vm = lib.mkDefault config.virtualisation.vmVariant.system.build.vm;
vmWithBootLoader = lib.mkDefault config.virtualisation.vmVariantWithBootLoader.system.build.vm;
};
virtualisation.vmVariant = {
options = {
virtualisation.vmVariant = lib.mkOption {
apply = _: throw "virtualisation.vmVariant*.virtualisation.vmVariant is not supported";
};
virtualisation.vmVariantWithBootLoader = lib.mkOption {
apply =
_: throw "virtualisation.vmVariant*.virtualisation.vmVariantWithBootloader is not supported";
};
};
};
};
# uses extendModules
meta.buildDocsInSandbox = false;
}

View File

@@ -0,0 +1,39 @@
{ lib, pkgs, ... }:
{
imports = [
../profiles/qemu-guest.nix
];
config = {
fileSystems."/" = lib.mkImageMediaOverride {
device = "/dev/disk/by-label/nixos";
autoResize = true;
fsType = "ext4";
};
boot.growPartition = true;
boot.kernelParams = [ "console=tty0" ];
boot.loader.grub.device = "/dev/vda";
boot.loader.timeout = 0;
# Allow root logins
services.openssh = {
enable = true;
settings.PermitRootLogin = "prohibit-password";
};
# Cloud-init configuration.
services.cloud-init.enable = true;
# Wget is needed for setting password. This is of little use as
# root password login is disabled above.
environment.systemPackages = [ pkgs.wget ];
# Only enable CloudStack datasource for faster boot speed.
environment.etc."cloud/cloud.cfg.d/99_cloudstack.cfg".text = ''
datasource:
CloudStack: {}
None: {}
datasource_list: ["CloudStack"]
'';
};
}

View File

@@ -0,0 +1,48 @@
{
config,
pkgs,
lib,
...
}:
with lib;
{
config = mkIf config.boot.isContainer {
# Disable some features that are not useful in a container.
# containers don't have a kernel
boot.kernel.enable = false;
boot.modprobeConfig.enable = false;
console.enable = mkDefault false;
nix.optimise.automatic = mkDefault false; # the store is host managed
powerManagement.enable = mkDefault false;
documentation.nixos.enable = mkDefault false;
networking.useHostResolvConf = mkDefault true;
# Containers should be light-weight, so start sshd on demand.
services.openssh.startWhenNeeded = mkDefault true;
# containers do not need to setup devices
services.udev.enable = false;
# containers normally do not need to manage logical volumes
services.lvm.enable = lib.mkDefault false;
# Shut up warnings about not having a boot loader.
system.build.installBootLoader = lib.mkDefault "${pkgs.coreutils}/bin/true";
# Not supported in systemd-nspawn containers.
security.audit.enable = false;
# Use the host's nix-daemon.
environment.variables.NIX_REMOTE = "daemon";
};
}

View File

@@ -0,0 +1,117 @@
{
pkgs,
lib,
config,
...
}:
let
cfg = config.virtualisation.containerd;
configFile =
if cfg.configFile == null then
settingsFormat.generate "containerd.toml" cfg.settings
else
cfg.configFile;
containerdConfigChecked =
pkgs.runCommand "containerd-config-checked.toml"
{
nativeBuildInputs = [ pkgs.containerd ];
}
''
containerd -c ${configFile} config dump >/dev/null
ln -s ${configFile} $out
'';
settingsFormat = pkgs.formats.toml { };
in
{
options.virtualisation.containerd = with lib.types; {
enable = lib.mkEnableOption "containerd container runtime";
configFile = lib.mkOption {
default = null;
description = ''
Path to containerd config file.
Setting this option will override any configuration applied by the settings option.
'';
type = nullOr path;
};
settings = lib.mkOption {
type = settingsFormat.type;
default = { };
description = ''
Verbatim lines to add to containerd.toml
'';
};
args = lib.mkOption {
default = { };
description = "extra args to append to the containerd cmdline";
type = attrsOf str;
};
};
config = lib.mkIf cfg.enable {
warnings = lib.optional (cfg.configFile != null) ''
`virtualisation.containerd.configFile` is deprecated. use `virtualisation.containerd.settings` instead.
'';
virtualisation.containerd = {
args.config = toString containerdConfigChecked;
settings = {
version = 2;
plugins."io.containerd.grpc.v1.cri" = {
containerd.snapshotter = lib.mkIf config.boot.zfs.enabled (lib.mkOptionDefault "zfs");
cni.bin_dir = lib.mkOptionDefault "${pkgs.cni-plugins}/bin";
};
};
};
environment.systemPackages = [ pkgs.containerd ];
systemd.services.containerd = {
description = "containerd - container runtime";
wantedBy = [ "multi-user.target" ];
after = [
"network.target"
"local-fs.target"
"dbus.service"
];
path =
with pkgs;
[
containerd
runc
iptables
]
++ lib.optional config.boot.zfs.enabled config.boot.zfs.package;
serviceConfig = {
ExecStart = ''${pkgs.containerd}/bin/containerd ${
lib.concatStringsSep " " (lib.cli.toGNUCommandLine { } cfg.args)
}'';
Delegate = "yes";
KillMode = "process";
Type = "notify";
Restart = "always";
RestartSec = "10";
# "limits" defined below are adopted from upstream: https://github.com/containerd/containerd/blob/master/containerd.service
LimitNPROC = "infinity";
LimitCORE = "infinity";
TasksMax = "infinity";
OOMScoreAdjust = "-999";
StateDirectory = "containerd";
RuntimeDirectory = "containerd";
RuntimeDirectoryPreserve = "yes";
};
unitConfig = {
StartLimitBurst = "16";
StartLimitIntervalSec = "120s";
};
};
};
}

View File

@@ -0,0 +1,152 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.virtualisation.containers;
inherit (lib) literalExpression mkOption types;
toml = pkgs.formats.toml { };
in
{
meta = {
maintainers = [ ] ++ lib.teams.podman.members;
};
options.virtualisation.containers = {
enable = mkOption {
type = types.bool;
default = false;
description = ''
This option enables the common /etc/containers configuration module.
'';
};
ociSeccompBpfHook.enable = mkOption {
type = types.bool;
default = false;
description = "Enable the OCI seccomp BPF hook";
};
containersConf.settings = mkOption {
type = toml.type;
default = { };
description = "containers.conf configuration";
};
containersConf.cniPlugins = mkOption {
type = types.listOf types.package;
defaultText = literalExpression ''
[
pkgs.cni-plugins
]
'';
example = literalExpression ''
[
pkgs.cniPlugins.dnsname
]
'';
description = ''
CNI plugins to install on the system.
'';
};
storage.settings = mkOption {
type = toml.type;
description = "storage.conf configuration";
};
registries = {
search = mkOption {
type = types.listOf types.str;
default = [
"docker.io"
"quay.io"
];
description = ''
List of repositories to search.
'';
};
insecure = mkOption {
default = [ ];
type = types.listOf types.str;
description = ''
List of insecure repositories.
'';
};
block = mkOption {
default = [ ];
type = types.listOf types.str;
description = ''
List of blocked repositories.
'';
};
};
policy = mkOption {
default = { };
type = types.attrs;
example = literalExpression ''
{
default = [ { type = "insecureAcceptAnything"; } ];
transports = {
docker-daemon = {
"" = [ { type = "insecureAcceptAnything"; } ];
};
};
}
'';
description = ''
Signature verification policy file.
If this option is empty the default policy file from
`skopeo` will be used.
'';
};
};
config = lib.mkIf cfg.enable {
virtualisation.containers.containersConf.cniPlugins = [ pkgs.cni-plugins ];
virtualisation.containers.containersConf.settings = {
network.cni_plugin_dirs = map (p: "${lib.getBin p}/bin") cfg.containersConf.cniPlugins;
engine = {
init_path = "${pkgs.catatonit}/bin/catatonit";
}
// lib.optionalAttrs cfg.ociSeccompBpfHook.enable {
hooks_dir = [ config.boot.kernelPackages.oci-seccomp-bpf-hook ];
};
};
virtualisation.containers.storage.settings.storage = {
driver = lib.mkDefault "overlay";
graphroot = lib.mkDefault "/var/lib/containers/storage";
runroot = lib.mkDefault "/run/containers/storage";
};
environment.etc = {
"containers/containers.conf".source = toml.generate "containers.conf" cfg.containersConf.settings;
"containers/storage.conf".source = toml.generate "storage.conf" cfg.storage.settings;
"containers/registries.conf".source = toml.generate "registries.conf" {
registries = lib.mapAttrs (n: v: { registries = v; }) cfg.registries;
};
"containers/policy.json".source =
if cfg.policy != { } then
pkgs.writeText "policy.json" (builtins.toJSON cfg.policy)
else
"${pkgs.skopeo.policy}/default-policy.json";
};
};
}

View File

@@ -0,0 +1,181 @@
{
config,
lib,
pkgs,
...
}:
with lib;
let
cfg = config.virtualisation.cri-o;
crioPackage = pkgs.cri-o.override {
extraPackages =
cfg.extraPackages
++ lib.optional (config.boot.supportedFilesystems.zfs or false) config.boot.zfs.package;
};
format = pkgs.formats.toml { };
cfgFile = format.generate "00-default.conf" cfg.settings;
in
{
meta = {
maintainers = teams.podman.members;
};
options.virtualisation.cri-o = {
enable = mkEnableOption "Container Runtime Interface for OCI (CRI-O)";
storageDriver = mkOption {
type = types.enum [
"aufs"
"btrfs"
"devmapper"
"overlay"
"vfs"
"zfs"
];
default = "overlay";
description = "Storage driver to be used";
};
logLevel = mkOption {
type = types.enum [
"trace"
"debug"
"info"
"warn"
"error"
"fatal"
];
default = "info";
description = "Log level to be used";
};
pauseImage = mkOption {
type = types.nullOr types.str;
default = null;
description = "Override the default pause image for pod sandboxes";
example = "k8s.gcr.io/pause:3.2";
};
pauseCommand = mkOption {
type = types.nullOr types.str;
default = null;
description = "Override the default pause command";
example = "/pause";
};
runtime = mkOption {
type = types.nullOr types.str;
default = null;
description = "Override the default runtime";
example = "crun";
};
extraPackages = mkOption {
type = with types; listOf package;
default = [ ];
example = literalExpression ''
[
pkgs.gvisor
]
'';
description = ''
Extra packages to be installed in the CRI-O wrapper.
'';
};
package = mkOption {
type = types.package;
default = crioPackage;
internal = true;
description = ''
The final CRI-O package (including extra packages).
'';
};
networkDir = mkOption {
type = types.nullOr types.path;
default = null;
description = "Override the network_dir option.";
internal = true;
};
settings = mkOption {
type = format.type;
default = { };
description = ''
Configuration for cri-o, see
<https://github.com/cri-o/cri-o/blob/master/docs/crio.conf.5.md>.
'';
};
};
config = mkIf cfg.enable {
environment.systemPackages = [
cfg.package
pkgs.cri-tools
];
environment.etc."crictl.yaml".source = "${cfg.package}/etc/crictl.yaml";
virtualisation.cri-o.settings.crio = {
storage_driver = cfg.storageDriver;
image = {
pause_image = mkIf (cfg.pauseImage != null) cfg.pauseImage;
pause_command = mkIf (cfg.pauseCommand != null) cfg.pauseCommand;
};
network = {
plugin_dirs = [ "${pkgs.cni-plugins}/bin" ];
network_dir = mkIf (cfg.networkDir != null) cfg.networkDir;
};
runtime = {
cgroup_manager = "systemd";
log_level = cfg.logLevel;
manage_ns_lifecycle = true;
pinns_path = "${cfg.package}/bin/pinns";
hooks_dir = optional (config.virtualisation.containers.ociSeccompBpfHook.enable) config.boot.kernelPackages.oci-seccomp-bpf-hook;
default_runtime = mkIf (cfg.runtime != null) cfg.runtime;
runtimes = mkIf (cfg.runtime != null) {
"${cfg.runtime}" = { };
};
};
};
environment.etc."cni/net.d/10-crio-bridge.conflist".source =
"${cfg.package}/etc/cni/net.d/10-crio-bridge.conflist";
environment.etc."cni/net.d/99-loopback.conflist".source =
"${cfg.package}/etc/cni/net.d/99-loopback.conflist";
environment.etc."crio/crio.conf.d/00-default.conf".source = cfgFile;
# Enable common /etc/containers configuration
virtualisation.containers.enable = true;
systemd.services.crio = {
description = "Container Runtime Interface for OCI (CRI-O)";
documentation = [ "https://github.com/cri-o/cri-o" ];
wantedBy = [ "multi-user.target" ];
after = [ "network.target" ];
path = [ cfg.package ];
serviceConfig = {
Type = "notify";
ExecStart = "${cfg.package}/bin/crio";
ExecReload = "/bin/kill -s HUP $MAINPID";
TasksMax = "infinity";
LimitNOFILE = "1048576";
LimitNPROC = "1048576";
LimitCORE = "infinity";
OOMScoreAdjust = "-999";
TimeoutStartSec = "0";
Restart = "on-abnormal";
};
restartTriggers = [ cfgFile ];
};
};
}

View File

@@ -0,0 +1,228 @@
{
config,
pkgs,
lib,
modulesPath,
...
}:
with lib;
{
imports = [
(modulesPath + "/profiles/qemu-guest.nix")
(modulesPath + "/virtualisation/digital-ocean-init.nix")
];
options.virtualisation.digitalOcean = with types; {
setRootPassword = mkOption {
type = bool;
default = false;
example = true;
description = "Whether to set the root password from the Digital Ocean metadata";
};
setSshKeys = mkOption {
type = bool;
default = true;
example = true;
description = "Whether to fetch ssh keys from Digital Ocean";
};
seedEntropy = mkOption {
type = bool;
default = true;
example = true;
description = "Whether to run the kernel RNG entropy seeding script from the Digital Ocean vendor data";
};
};
config =
let
cfg = config.virtualisation.digitalOcean;
hostName = config.networking.hostName;
doMetadataFile = "/run/do-metadata/v1.json";
in
mkMerge [
{
fileSystems."/" = lib.mkDefault {
device = "/dev/disk/by-label/nixos";
autoResize = true;
fsType = "ext4";
};
boot = {
growPartition = true;
kernelParams = [
"console=ttyS0"
"panic=1"
"boot.panic_on_fail"
];
initrd.kernelModules = [ "virtio_scsi" ];
kernelModules = [
"virtio_pci"
"virtio_net"
];
loader.grub.devices = [ "/dev/vda" ];
};
services.openssh = {
enable = mkDefault true;
settings.PasswordAuthentication = mkDefault false;
};
services.do-agent.enable = mkDefault true;
networking = {
hostName = mkDefault ""; # use Digital Ocean metadata server
};
/*
Check for and wait for the metadata server to become reachable.
This serves as a dependency for all the other metadata services.
*/
systemd.services.digitalocean-metadata = {
path = [ pkgs.curl ];
description = "Get host metadata provided by Digitalocean";
script = ''
set -eu
DO_DELAY_ATTEMPTS=0
while ! curl -fsSL -o $RUNTIME_DIRECTORY/v1.json http://169.254.169.254/metadata/v1.json; do
DO_DELAY_ATTEMPTS=$((DO_DELAY_ATTEMPTS + 1))
if (( $DO_DELAY_ATTEMPTS >= $DO_DELAY_ATTEMPTS_MAX )); then
echo "giving up"
exit 1
fi
echo "metadata unavailable, trying again in 1s..."
sleep 1
done
chmod 600 $RUNTIME_DIRECTORY/v1.json
'';
environment = {
DO_DELAY_ATTEMPTS_MAX = "10";
};
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
RuntimeDirectory = "do-metadata";
RuntimeDirectoryPreserve = "yes";
};
unitConfig = {
ConditionPathExists = "!${doMetadataFile}";
After = [
"network-pre.target"
]
++ optional config.networking.dhcpcd.enable "dhcpcd.service"
++ optional config.systemd.network.enable "systemd-networkd.service";
};
};
/*
Fetch the root password from the digital ocean metadata.
There is no specific route for this, so we use jq to get
it from the One Big JSON metadata blob
*/
systemd.services.digitalocean-set-root-password = mkIf cfg.setRootPassword {
path = [
pkgs.shadow
pkgs.jq
];
description = "Set root password provided by Digitalocean";
wantedBy = [ "multi-user.target" ];
script = ''
set -eo pipefail
ROOT_PASSWORD=$(jq -er '.auth_key' ${doMetadataFile})
echo "root:$ROOT_PASSWORD" | chpasswd
mkdir -p /etc/do-metadata/set-root-password
'';
unitConfig = {
ConditionPathExists = "!/etc/do-metadata/set-root-password";
Before = optional config.services.openssh.enable "sshd.service";
After = [ "digitalocean-metadata.service" ];
Requires = [ "digitalocean-metadata.service" ];
};
serviceConfig = {
Type = "oneshot";
};
};
/*
Set the hostname from Digital Ocean, unless the user configured it in
the NixOS configuration. The cached metadata file isn't used here
because the hostname is a mutable part of the droplet.
*/
systemd.services.digitalocean-set-hostname = mkIf (hostName == "") {
path = [
pkgs.curl
pkgs.net-tools
];
description = "Set hostname provided by Digitalocean";
wantedBy = [ "network.target" ];
script = ''
set -e
DIGITALOCEAN_HOSTNAME=$(curl -fsSL http://169.254.169.254/metadata/v1/hostname)
hostname "$DIGITALOCEAN_HOSTNAME"
if [[ ! -e /etc/hostname || -w /etc/hostname ]]; then
printf "%s\n" "$DIGITALOCEAN_HOSTNAME" > /etc/hostname
fi
'';
unitConfig = {
Before = [ "network.target" ];
After = [ "digitalocean-metadata.service" ];
Wants = [ "digitalocean-metadata.service" ];
};
serviceConfig = {
Type = "oneshot";
};
};
# Fetch the ssh keys for root from Digital Ocean
systemd.services.digitalocean-ssh-keys = mkIf cfg.setSshKeys {
description = "Set root ssh keys provided by Digital Ocean";
wantedBy = [ "multi-user.target" ];
path = [ pkgs.jq ];
script = ''
set -e
mkdir -m 0700 -p /root/.ssh
jq -er '.public_keys[]' ${doMetadataFile} > /root/.ssh/authorized_keys
chmod 600 /root/.ssh/authorized_keys
'';
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
};
unitConfig = {
ConditionPathExists = "!/root/.ssh/authorized_keys";
Before = optional config.services.openssh.enable "sshd.service";
After = [ "digitalocean-metadata.service" ];
Requires = [ "digitalocean-metadata.service" ];
};
};
/*
Initialize the RNG by running the entropy-seed script from the
Digital Ocean metadata
*/
systemd.services.digitalocean-entropy-seed = mkIf cfg.seedEntropy {
description = "Run the kernel RNG entropy seeding script from the Digital Ocean vendor data";
wantedBy = [ "network.target" ];
path = [
pkgs.jq
pkgs.mpack
];
script = ''
set -eo pipefail
TEMPDIR=$(mktemp -d)
jq -er '.vendor_data' ${doMetadataFile} | munpack -tC $TEMPDIR
ENTROPY_SEED=$(grep -rl "DigitalOcean Entropy Seed script" $TEMPDIR)
${pkgs.runtimeShell} $ENTROPY_SEED
rm -rf $TEMPDIR
'';
unitConfig = {
Before = [ "network.target" ];
After = [ "digitalocean-metadata.service" ];
Requires = [ "digitalocean-metadata.service" ];
};
serviceConfig = {
Type = "oneshot";
};
};
}
];
meta.maintainers = with maintainers; [
arianvp
eamsden
];
}

View File

@@ -0,0 +1,114 @@
{
config,
lib,
pkgs,
...
}:
with lib;
let
cfg = config.virtualisation.digitalOceanImage;
in
{
imports = [
./digital-ocean-config.nix
./disk-size-option.nix
../image/file-options.nix
(lib.mkRenamedOptionModuleWith {
sinceRelease = 2411;
from = [
"virtualisation"
"digitalOceanImage"
"diskSize"
];
to = [
"virtualisation"
"diskSize"
];
})
];
options = {
virtualisation.digitalOceanImage.configFile = mkOption {
type = with types; nullOr path;
default = null;
description = ''
A path to a configuration file which will be placed at
`/etc/nixos/configuration.nix` and be used when switching
to a new configuration. If set to `null`, a default
configuration is used that imports
`(modulesPath + "/virtualisation/digital-ocean-config.nix")`.
'';
};
virtualisation.digitalOceanImage.compressionMethod = mkOption {
type = types.enum [
"gzip"
"bzip2"
];
default = "gzip";
example = "bzip2";
description = ''
Disk image compression method. Choose bzip2 to generate smaller images that
take longer to generate but will consume less metered storage space on your
Digital Ocean account.
'';
};
};
#### implementation
config =
let
format = "qcow2";
in
{
image.extension = lib.concatStringsSep "." [
format
(
{
"gzip" = "gz";
"bzip2" = "bz2";
}
.${cfg.compressionMethod}
)
];
system.nixos.tags = [ "digital-ocean" ];
system.build.image = config.system.build.digitalOceanImage;
system.build.digitalOceanImage = import ../../lib/make-disk-image.nix {
name = "digital-ocean-image";
inherit (config.image) baseName;
inherit (config.virtualisation) diskSize;
inherit
config
lib
pkgs
format
;
postVM =
let
compress =
{
"gzip" = "${pkgs.gzip}/bin/gzip";
"bzip2" = "${pkgs.bzip2}/bin/bzip2";
}
.${cfg.compressionMethod};
in
''
${compress} $diskImage
'';
configFile =
if cfg.configFile == null then
config.virtualisation.digitalOcean.defaultConfigFile
else
cfg.configFile;
};
};
meta.maintainers = with maintainers; [
arianvp
eamsden
];
}

View File

@@ -0,0 +1,114 @@
{
config,
pkgs,
lib,
...
}:
with lib;
let
cfg = config.virtualisation.digitalOcean;
defaultConfigFile = pkgs.writeText "digitalocean-configuration.nix" ''
{ modulesPath, lib, ... }:
{
imports = lib.optional (builtins.pathExists ./do-userdata.nix) ./do-userdata.nix ++ [
(modulesPath + "/virtualisation/digital-ocean-config.nix")
];
}
'';
in
{
options.virtualisation.digitalOcean.rebuildFromUserData = mkOption {
type = types.bool;
default = true;
example = true;
description = "Whether to reconfigure the system from Digital Ocean user data";
};
options.virtualisation.digitalOcean.defaultConfigFile = mkOption {
type = types.path;
default = defaultConfigFile;
defaultText = literalMD ''
The default configuration imports user-data if applicable and
`(modulesPath + "/virtualisation/digital-ocean-config.nix")`.
'';
description = ''
A path to a configuration file which will be placed at
`/etc/nixos/configuration.nix` and be used when switching to
a new configuration.
'';
};
config = {
systemd.services.digitalocean-init = mkIf cfg.rebuildFromUserData {
description = "Reconfigure the system from Digital Ocean userdata on startup";
wantedBy = [ "network-online.target" ];
unitConfig = {
ConditionPathExists = "!/etc/nixos/do-userdata.nix";
After = [
"digitalocean-metadata.service"
"network-online.target"
];
Requires = [ "digitalocean-metadata.service" ];
X-StopOnRemoval = false;
};
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
};
restartIfChanged = false;
path = [
pkgs.jq
pkgs.gnused
pkgs.gnugrep
config.systemd.package
config.nix.package
config.system.build.nixos-rebuild
];
environment = {
HOME = "/root";
NIX_PATH = concatStringsSep ":" [
"/nix/var/nix/profiles/per-user/root/channels/nixos"
"nixos-config=/etc/nixos/configuration.nix"
"/nix/var/nix/profiles/per-user/root/channels"
];
};
script = ''
set -e
echo "attempting to fetch configuration from Digital Ocean user data..."
userData=$(mktemp)
if jq -er '.user_data' /run/do-metadata/v1.json > $userData; then
# If the user-data looks like it could be a nix expression,
# copy it over. Also, look for a magic three-hash comment and set
# that as the channel.
if nix-instantiate --parse $userData > /dev/null; then
channels="$(grep '^###' "$userData" | sed 's|###\s*||')"
printf "%s" "$channels" | while read channel; do
echo "writing channel: $channel"
done
if [[ -n "$channels" ]]; then
printf "%s" "$channels" > /root/.nix-channels
nix-channel --update
fi
echo "setting configuration from Digital Ocean user data"
cp "$userData" /etc/nixos/do-userdata.nix
if [[ ! -e /etc/nixos/configuration.nix ]]; then
install -m0644 ${cfg.defaultConfigFile} /etc/nixos/configuration.nix
fi
else
echo "user data does not appear to be a Nix expression; ignoring"
exit
fi
nixos-rebuild switch
else
echo "no user data is available"
fi
'';
};
};
meta.maintainers = with maintainers; [
arianvp
eamsden
];
}

View File

@@ -0,0 +1,61 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.image;
in
{
imports = [
./disk-size-option.nix
../image/file-options.nix
];
options.image = {
format = lib.mkOption {
description = "Format of the disk image to generate: raw or qcow2";
type = lib.types.enum [
"raw"
"qcow2"
];
default = "qcow2";
};
efiSupport = lib.mkOption {
description = "Whether the disk image should support EFI boot or legacy boot";
type = lib.types.bool;
default = true;
};
};
config = {
boot.loader.grub = lib.mkIf (!cfg.efiSupport) {
enable = lib.mkOptionDefault true;
devices = lib.mkDefault [ "/dev/vda" ];
};
boot.loader.systemd-boot.enable = lib.mkDefault cfg.efiSupport;
boot.growPartition = lib.mkDefault true;
fileSystems = {
"/" = {
device = "/dev/disk/by-label/nixos";
autoResize = true;
fsType = "ext4";
};
"/boot" = lib.mkIf (cfg.efiSupport) {
device = "/dev/disk/by-label/ESP";
fsType = "vfat";
};
};
system.nixos.tags = [ cfg.format ] ++ lib.optionals cfg.efiSupport [ "efi" ];
image.extension = if cfg.format == "raw" then "img" else cfg.format;
system.build.image = import ../../lib/make-disk-image.nix {
inherit lib config pkgs;
inherit (config.virtualisation) diskSize;
inherit (cfg) baseName format;
partitionTableType = if cfg.efiSupport then "efi" else "legacy";
};
};
}

View File

@@ -0,0 +1,38 @@
{ lib, config, ... }:
let
t = lib.types;
in
{
options = {
virtualisation.diskSizeAutoSupported = lib.mkOption {
type = t.bool;
default = true;
description = ''
Whether the current image builder or vm runner supports `virtualisation.diskSize = "auto".`
'';
internal = true;
};
virtualisation.diskSize = lib.mkOption {
type = t.either (t.enum [ "auto" ]) t.ints.positive;
default = if config.virtualisation.diskSizeAutoSupported then "auto" else 1024;
defaultText = lib.literalExpression "if virtualisation.diskSizeAutoSupported then \"auto\" else 1024";
description = ''
The disk size in MiB (1024×1024 bytes) of the virtual machine.
'';
};
};
config =
let
inherit (config.virtualisation) diskSize diskSizeAutoSupported;
in
{
assertions = [
{
assertion = diskSize != "auto" || diskSizeAutoSupported;
message = "Setting virtualisation.diskSize to `auto` is not supported by the current image build or vm runner; use an explicit size.";
}
];
};
}

View File

@@ -0,0 +1,56 @@
{ ... }:
{
imports = [
../profiles/docker-container.nix # FIXME, shouldn't include something from profiles/
];
boot.postBootCommands = ''
# Set virtualisation to docker
echo "docker" > /run/systemd/container
'';
# Iptables do not work in Docker.
networking.firewall.enable = false;
# Socket activated ssh presents problem in Docker.
services.openssh.startWhenNeeded = false;
}
# Example usage:
#
## default.nix
# let
# nixos = import <nixpkgs/nixos> {
# configuration = ./configuration.nix;
# system = "x86_64-linux";
# };
# in
# nixos.config.system.build.tarball
#
## configuration.nix
# { pkgs, config, lib, ... }:
# {
# imports = [
# <nixpkgs/nixos/modules/virtualisation/docker-image.nix>
# <nixpkgs/nixos/modules/installer/cd-dvd/channel.nix>
# ];
#
# documentation.doc.enable = false;
#
# environment.systemPackages = with pkgs; [
# bashInteractive
# cacert
# nix
# ];
# }
#
## Run
# Build the tarball:
# $ nix-build default.nix
# Load into docker:
# $ docker import result/tarball/nixos-system-*.tar.xz nixos-docker
# Boots into systemd
# $ docker run --privileged -it nixos-docker /init
# Log into the container
# $ docker exec -it <container-name> /run/current-system/sw/bin/bash

View File

@@ -0,0 +1,106 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.virtualisation.docker.rootless;
proxy_env = config.networking.proxy.envVars;
settingsFormat = pkgs.formats.json { };
daemonSettingsFile = settingsFormat.generate "daemon.json" cfg.daemon.settings;
in
{
###### interface
options.virtualisation.docker.rootless = {
enable = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
This option enables docker in a rootless mode, a daemon that manages
linux containers. To interact with the daemon, one needs to set
{command}`DOCKER_HOST=unix://$XDG_RUNTIME_DIR/docker.sock`.
'';
};
setSocketVariable = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
Point {command}`DOCKER_HOST` to rootless Docker instance for
normal users by default.
'';
};
daemon.settings = lib.mkOption {
type = settingsFormat.type;
default = { };
example = {
ipv6 = true;
"fixed-cidr-v6" = "fd00::/80";
};
description = ''
Configuration for docker daemon. The attributes are serialized to JSON used as daemon.conf.
See <https://docs.docker.com/engine/reference/commandline/dockerd/#daemon-configuration-file>
'';
};
package = lib.mkPackageOption pkgs "docker" { };
extraPackages = lib.mkOption {
type = lib.types.listOf lib.types.package;
default = [ ];
description = ''
Extra packages to add to PATH for the docker daemon process.
'';
};
};
###### implementation
config = lib.mkIf cfg.enable {
environment.systemPackages = [ cfg.package ];
environment.extraInit = lib.optionalString cfg.setSocketVariable ''
if [ -z "$DOCKER_HOST" -a -n "$XDG_RUNTIME_DIR" ]; then
export DOCKER_HOST="unix://$XDG_RUNTIME_DIR/docker.sock"
fi
'';
# Taken from https://github.com/moby/moby/blob/master/contrib/dockerd-rootless-setuptool.sh
systemd.user.services.docker = {
wantedBy = [ "default.target" ];
description = "Docker Application Container Engine (Rootless)";
# needs newuidmap from pkgs.shadow
path = [ "/run/wrappers" ] ++ cfg.extraPackages;
environment = proxy_env;
unitConfig = {
# docker-rootless doesn't support running as root.
ConditionUser = "!root";
StartLimitInterval = "60s";
};
serviceConfig = {
Type = "notify";
ExecStart = "${cfg.package}/bin/dockerd-rootless --config-file=${daemonSettingsFile}";
ExecReload = "${pkgs.procps}/bin/kill -s HUP $MAINPID";
TimeoutSec = 0;
RestartSec = 2;
Restart = "always";
LimitNOFILE = "infinity";
LimitNPROC = "infinity";
LimitCORE = "infinity";
Delegate = true;
NotifyAccess = "all";
KillMode = "mixed";
};
unitConfig = {
StartLimitBurst = 3;
};
};
};
}

View File

@@ -0,0 +1,364 @@
# Systemd services for docker.
{
config,
lib,
utils,
pkgs,
...
}:
with lib;
let
cfg = config.virtualisation.docker;
proxy_env = config.networking.proxy.envVars;
settingsFormat = pkgs.formats.json { };
daemonSettingsFile = settingsFormat.generate "daemon.json" cfg.daemon.settings;
in
{
###### interface
options.virtualisation.docker = {
enable = mkOption {
type = types.bool;
default = false;
description = ''
This option enables docker, a daemon that manages
linux containers. Users in the "docker" group can interact with
the daemon (e.g. to start or stop containers) using the
{command}`docker` command line tool.
'';
};
listenOptions = mkOption {
type = types.listOf types.str;
default = [ "/run/docker.sock" ];
description = ''
A list of unix and tcp docker should listen to. The format follows
ListenStream as described in {manpage}`systemd.socket(5)`.
'';
};
enableOnBoot = mkOption {
type = types.bool;
default = true;
description = ''
When enabled dockerd is started on boot. This is required for
containers which are created with the
`--restart=always` flag to work. If this option is
disabled, docker might be started on demand by socket activation.
'';
};
daemon.settings = mkOption {
type = types.submodule {
freeformType = settingsFormat.type;
options = {
live-restore = mkOption {
type = types.bool;
# Prior to NixOS 24.11, this was set to true by default, while upstream defaulted to false.
# Keep the option unset to follow upstream defaults
default = versionOlder config.system.stateVersion "24.11";
defaultText = literalExpression "lib.versionOlder config.system.stateVersion \"24.11\"";
description = ''
Allow dockerd to be restarted without affecting running container.
This option is incompatible with docker swarm.
'';
};
};
};
default = { };
example = {
ipv6 = true;
"live-restore" = true;
"fixed-cidr-v6" = "fd00::/80";
};
description = ''
Configuration for docker daemon. The attributes are serialized to JSON used as daemon.conf.
See <https://docs.docker.com/engine/reference/commandline/dockerd/#daemon-configuration-file>
'';
};
enableNvidia = mkOption {
type = types.bool;
default = false;
description = ''
**Deprecated**, please use hardware.nvidia-container-toolkit.enable instead.
Enable Nvidia GPU support inside docker containers.
'';
};
storageDriver = mkOption {
type = types.nullOr (
types.enum [
"aufs"
"btrfs"
"devicemapper"
"overlay"
"overlay2"
"zfs"
]
);
default = null;
description = ''
This option determines which Docker
[storage driver](https://docs.docker.com/storage/storagedriver/select-storage-driver/)
to use.
By default it lets docker automatically choose the preferred storage
driver.
However, it is recommended to specify a storage driver explicitly, as
docker's default varies over versions.
::: {.warning}
Changing the storage driver will cause any existing containers
and images to become inaccessible.
:::
'';
};
logDriver = mkOption {
type = types.enum [
"none"
"json-file"
"syslog"
"journald"
"gelf"
"fluentd"
"awslogs"
"splunk"
"etwlogs"
"gcplogs"
"local"
];
default = "journald";
description = ''
This option determines which Docker log driver to use.
'';
};
extraOptions = mkOption {
type = types.separatedString " ";
default = "";
description = ''
The extra command-line options to pass to
{command}`docker` daemon.
'';
};
autoPrune = {
enable = mkOption {
type = types.bool;
default = false;
description = ''
Whether to periodically prune Docker resources. If enabled, a
systemd timer will run `docker system prune -f`
as specified by the `dates` option.
'';
};
flags = mkOption {
type = types.listOf types.str;
default = [ ];
example = [ "--all" ];
description = ''
Any additional flags passed to {command}`docker system prune`.
'';
};
dates = mkOption {
default = "weekly";
type = types.str;
description = ''
Specification (in the format described by
{manpage}`systemd.time(7)`) of the time at
which the prune will occur.
'';
};
randomizedDelaySec = mkOption {
default = "0";
type = types.singleLineStr;
example = "45min";
description = ''
Add a randomized delay before each auto prune.
The delay will be chosen between zero and this value.
This value must be a time span in the format specified by
{manpage}`systemd.time(7)`
'';
};
persistent = mkOption {
default = true;
type = types.bool;
example = false;
description = ''
Takes a boolean argument. If true, the time when the service
unit was last triggered is stored on disk. When the timer is
activated, the service unit is triggered immediately if it
would have been triggered at least once during the time when
the timer was inactive. Such triggering is nonetheless
subject to the delay imposed by RandomizedDelaySec=. This is
useful to catch up on missed runs of the service when the
system was powered down.
'';
};
};
package = mkPackageOption pkgs "docker" { };
extraPackages = mkOption {
type = types.listOf types.package;
default = [ ];
example = literalExpression "with pkgs; [ criu ]";
description = ''
Extra packages to add to PATH for the docker daemon process.
'';
};
};
imports = [
(mkRemovedOptionModule [
"virtualisation"
"docker"
"socketActivation"
] "This option was removed and socket activation is now always active")
(mkAliasOptionModule
[ "virtualisation" "docker" "liveRestore" ]
[ "virtualisation" "docker" "daemon" "settings" "live-restore" ]
)
];
###### implementation
config = mkIf cfg.enable (mkMerge [
{
boot.kernelModules = [
"bridge"
"veth"
"br_netfilter"
"xt_nat"
];
boot.kernel.sysctl = {
"net.ipv4.conf.all.forwarding" = mkOverride 98 true;
"net.ipv4.conf.default.forwarding" = mkOverride 98 true;
};
environment.systemPackages = [ cfg.package ];
users.groups.docker.gid = config.ids.gids.docker;
systemd.packages = [ cfg.package ];
# Docker 25.0.0 supports CDI by default
# (https://docs.docker.com/engine/release-notes/25.0/#new). Encourage
# moving to CDI as opposed to having deprecated runtime
# wrappers.
warnings =
lib.optionals (cfg.enableNvidia && (lib.strings.versionAtLeast cfg.package.version "25"))
[
''
You have set virtualisation.docker.enableNvidia. This option is deprecated, please set hardware.nvidia-container-toolkit.enable instead.
''
];
systemd.services.docker = {
wantedBy = optional cfg.enableOnBoot "multi-user.target";
after = [
"network.target"
"docker.socket"
];
requires = [ "docker.socket" ];
environment = proxy_env;
serviceConfig = {
Type = "notify";
ExecStart = [
""
''
${cfg.package}/bin/dockerd \
--config-file=${daemonSettingsFile} \
${cfg.extraOptions}
''
];
ExecReload = [
""
"${pkgs.procps}/bin/kill -s HUP $MAINPID"
];
};
path = [
pkgs.kmod
]
++ optional (cfg.storageDriver == "zfs") config.boot.zfs.package
++ cfg.extraPackages;
};
systemd.sockets.docker = {
description = "Docker Socket for the API";
wantedBy = [ "sockets.target" ];
socketConfig = {
ListenStream = cfg.listenOptions;
SocketMode = "0660";
SocketUser = "root";
SocketGroup = "docker";
};
};
systemd.services.docker-prune = {
description = "Prune docker resources";
restartIfChanged = false;
unitConfig.X-StopOnRemoval = false;
serviceConfig = {
Type = "oneshot";
ExecStart = utils.escapeSystemdExecArgs (
[
(lib.getExe cfg.package)
"system"
"prune"
"-f"
]
++ cfg.autoPrune.flags
);
};
startAt = optional cfg.autoPrune.enable cfg.autoPrune.dates;
after = [ "docker.service" ];
requires = [ "docker.service" ];
};
systemd.timers.docker-prune = mkIf cfg.autoPrune.enable {
timerConfig = {
RandomizedDelaySec = cfg.autoPrune.randomizedDelaySec;
Persistent = cfg.autoPrune.persistent;
};
};
assertions = [
{
assertion =
cfg.enableNvidia && pkgs.stdenv.hostPlatform.isx86_64
-> config.hardware.graphics.enable32Bit or false;
message = "Option enableNvidia on x86_64 requires 32-bit support libraries";
}
];
virtualisation.docker.daemon.settings = {
group = "docker";
hosts = [ "fd://" ];
log-driver = mkDefault cfg.logDriver;
storage-driver = mkIf (cfg.storageDriver != null) (mkDefault cfg.storageDriver);
runtimes = mkIf cfg.enableNvidia {
nvidia = {
# Use the legacy nvidia-container-runtime wrapper to allow
# the `--runtime=nvidia` approach to expose
# GPU's. Starting with Docker > 25, CDI can be used
# instead, removing the need for runtime wrappers.
path = lib.getExe' (lib.getOutput "tools" config.hardware.nvidia-container-toolkit.package) "nvidia-container-runtime";
};
};
};
}
]);
}

View File

@@ -0,0 +1,5 @@
throw ''
This file is not the source for amazon AMIs anymore since 24.05.
The canonical source for NixOS AMIs is the AWS API. Please see
https://nixos.org/download/#nixos-amazon or https://nixos.github.io/amis/ for instructions
''

View File

@@ -0,0 +1,101 @@
# This module defines a systemd service that sets the SSH host key and
# authorized client key and host name of virtual machines running on
# Amazon EC2, Eucalyptus and OpenStack Compute (Nova).
{
config,
lib,
pkgs,
...
}:
with lib;
{
imports = [
(mkRemovedOptionModule [ "ec2" "metadata" ] "")
];
config = {
systemd.services.apply-ec2-data = {
description = "Apply EC2 Data";
wantedBy = [
"multi-user.target"
"sshd-keygen.service"
];
before = [ "sshd-keygen.service" ];
after = [ "fetch-ec2-metadata.service" ];
path = [ pkgs.iproute2 ];
script = ''
${optionalString (config.networking.hostName == "") ''
echo "setting host name..."
if [ -s /etc/ec2-metadata/hostname ]; then
${lib.getExe pkgs.hostname-debian} -F /etc/ec2-metadata/hostname
fi
''}
if ! [ -e /root/.ssh/authorized_keys ]; then
echo "obtaining SSH key..."
mkdir -p /root/.ssh
chmod 0700 /root/.ssh
if [ -s /etc/ec2-metadata/public-keys-0-openssh-key ]; then
(umask 177; cat /etc/ec2-metadata/public-keys-0-openssh-key >> /root/.ssh/authorized_keys)
echo "new key added to authorized_keys"
fi
fi
# Extract the intended SSH host key for this machine from
# the supplied user data, if available. Otherwise sshd will
# generate one normally.
userData=/etc/ec2-metadata/user-data
mkdir -p /etc/ssh
chmod 0755 /etc/ssh
if [ -s "$userData" ]; then
key="$(sed 's/|/\n/g; s/SSH_HOST_DSA_KEY://; t; d' $userData)"
key_pub="$(sed 's/SSH_HOST_DSA_KEY_PUB://; t; d' $userData)"
if [ -n "$key" ] && [ -n "$key_pub" ] && [ ! -e /etc/ssh/ssh_host_dsa_key ]; then
(umask 077; echo "$key" > /etc/ssh/ssh_host_dsa_key)
echo "$key_pub" > /etc/ssh/ssh_host_dsa_key.pub
fi
key="$(sed 's/|/\n/g; s/SSH_HOST_ED25519_KEY://; t; d' $userData)"
key_pub="$(sed 's/SSH_HOST_ED25519_KEY_PUB://; t; d' $userData)"
if [ -n "$key" ] && [ -n "$key_pub" ] && [ ! -e /etc/ssh/ssh_host_ed25519_key ]; then
(umask 077; echo "$key" > /etc/ssh/ssh_host_ed25519_key)
echo "$key_pub" > /etc/ssh/ssh_host_ed25519_key.pub
fi
fi
'';
serviceConfig.Type = "oneshot";
serviceConfig.RemainAfterExit = true;
};
systemd.services.print-host-key = {
description = "Print SSH Host Key";
wantedBy = [ "multi-user.target" ];
after = [ "sshd-keygen.service" ];
script = ''
# Print the host public key on the console so that the user
# can obtain it securely by parsing the output of
# ec2-get-console-output.
echo "-----BEGIN SSH HOST KEY FINGERPRINTS-----" > /dev/console
for i in /etc/ssh/ssh_host_*_key.pub; do
${config.programs.ssh.package}/bin/ssh-keygen -l -f "$i" > /dev/console || true
done
echo "-----END SSH HOST KEY FINGERPRINTS-----" > /dev/console
'';
serviceConfig.Type = "oneshot";
serviceConfig.RemainAfterExit = true;
};
};
meta.maintainers = with maintainers; [ arianvp ];
}

View File

@@ -0,0 +1,67 @@
metaDir=/etc/ec2-metadata
mkdir -p "$metaDir"
chmod 0755 "$metaDir"
rm -f "$metaDir/*"
get_imds_token() {
# retry-delay of 1 selected to give the system a second to get going,
# but not add a lot to the bootup time
curl \
--silent \
--show-error \
--retry 3 \
--retry-delay 1 \
--fail \
-X PUT \
--connect-timeout 1 \
-H "X-aws-ec2-metadata-token-ttl-seconds: 600" \
http://169.254.169.254/latest/api/token
}
preflight_imds_token() {
# retry-delay of 1 selected to give the system a second to get going,
# but not add a lot to the bootup time
curl \
--silent \
--show-error \
--retry 3 \
--retry-delay 1 \
--fail \
--connect-timeout 1 \
-H "X-aws-ec2-metadata-token: $IMDS_TOKEN" \
-o /dev/null \
http://169.254.169.254/1.0/meta-data/instance-id
}
try=1
while [ $try -le 3 ]; do
echo "(attempt $try/3) getting an EC2 instance metadata service v2 token..."
IMDS_TOKEN=$(get_imds_token) && break
try=$((try + 1))
sleep 1
done
if [ "$IMDS_TOKEN" == "" ]; then
echo "failed to fetch an IMDS2v token."
fi
try=1
while [ $try -le 10 ]; do
echo "(attempt $try/10) validating the EC2 instance metadata service v2 token..."
preflight_imds_token && break
try=$((try + 1))
sleep 1
done
echo "getting EC2 instance metadata..."
get_imds() {
# --fail to avoid populating missing files with 404 HTML response body
# || true to allow the script to continue even when encountering a 404
curl --silent --show-error --fail --header "X-aws-ec2-metadata-token: $IMDS_TOKEN" "$@" || true
}
get_imds -o "$metaDir/ami-manifest-path" http://169.254.169.254/1.0/meta-data/ami-manifest-path
(umask 077 && get_imds -o "$metaDir/user-data" http://169.254.169.254/1.0/user-data)
get_imds -o "$metaDir/hostname" http://169.254.169.254/1.0/meta-data/hostname
get_imds -o "$metaDir/public-keys-0-openssh-key" http://169.254.169.254/1.0/meta-data/public-keys/0/openssh-key

View File

@@ -0,0 +1,46 @@
{
config,
pkgs,
lib,
...
}:
with lib;
let
cfg = config.services.ecs-agent;
in
{
options.services.ecs-agent = {
enable = mkEnableOption "Amazon ECS agent";
package = mkPackageOption pkgs "ecs-agent" { };
extra-environment = mkOption {
type = types.attrsOf types.str;
description = "The environment the ECS agent should run with. See the ECS agent documentation for keys that work here.";
default = { };
};
};
config = lib.mkIf cfg.enable {
# This service doesn't run if docker isn't running, and unlike potentially remote services like e.g., postgresql, docker has
# to be running locally so `docker.enable` will always be set if the ECS agent is enabled.
virtualisation.docker.enable = true;
systemd.services.ecs-agent = {
inherit (cfg.package.meta) description;
after = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
environment = cfg.extra-environment;
script = ''
if [ ! -z "$ECS_DATADIR" ]; then
mkdir -p "$ECS_DATADIR"
fi
${cfg.package}/bin/agent
'';
};
};
}

View File

@@ -0,0 +1,144 @@
{
config,
lib,
pkgs,
...
}:
let
inherit (lib)
boolToString
mkDefault
mkIf
optional
readFile
;
in
{
imports = [
../profiles/headless.nix
../profiles/qemu-guest.nix
];
fileSystems."/" = {
fsType = "ext4";
device = "/dev/disk/by-label/nixos";
autoResize = true;
};
boot.growPartition = true;
boot.kernelParams = [
"console=ttyS0"
"panic=1"
"boot.panic_on_fail"
];
boot.initrd.kernelModules = [ "virtio_scsi" ];
boot.kernelModules = [
"virtio_pci"
"virtio_net"
];
# Generate a GRUB menu.
boot.loader.grub.device = "/dev/sda";
boot.loader.timeout = 0;
# Don't put old configurations in the GRUB menu. The user has no
# way to select them anyway.
boot.loader.grub.configurationLimit = 0;
# Allow root logins only using SSH keys
# and disable password authentication in general
services.openssh.enable = true;
services.openssh.settings.PermitRootLogin = mkDefault "prohibit-password";
services.openssh.settings.PasswordAuthentication = mkDefault false;
# enable OS Login. This also requires setting enable-oslogin=TRUE metadata on
# instance or project level
security.googleOsLogin.enable = true;
# Use GCE udev rules for dynamic disk volumes
services.udev.packages = [ pkgs.google-guest-configs ];
services.udev.path = [ pkgs.google-guest-configs ];
# Force getting the hostname from Google Compute.
networking.hostName = mkDefault "";
# Always include cryptsetup so that NixOps can use it.
environment.systemPackages = [ pkgs.cryptsetup ];
# Rely on GCP's firewall instead
networking.firewall.enable = mkDefault false;
# Configure default metadata hostnames
networking.extraHosts = ''
169.254.169.254 metadata.google.internal metadata
'';
networking.timeServers = [ "metadata.google.internal" ];
networking.usePredictableInterfaceNames = false;
# GC has 1460 MTU
networking.interfaces.eth0.mtu = 1460;
systemd.packages = [ pkgs.google-guest-agent ];
systemd.services.google-guest-agent = {
wantedBy = [ "multi-user.target" ];
restartTriggers = [ config.environment.etc."default/instance_configs.cfg".source ];
path = optional config.users.mutableUsers pkgs.shadow;
};
systemd.services.google-startup-scripts.wantedBy = [ "multi-user.target" ];
systemd.services.google-shutdown-scripts.wantedBy = [ "multi-user.target" ];
security.sudo.extraRules = mkIf config.users.mutableUsers [
{
groups = [ "google-sudoers" ];
commands = [
{
command = "ALL";
options = [ "NOPASSWD" ];
}
];
}
];
security.sudo-rs.extraRules = mkIf config.users.mutableUsers [
{
groups = [ "google-sudoers" ];
commands = [
{
command = "ALL";
options = [ "NOPASSWD" ];
}
];
}
];
users.groups.google-sudoers = mkIf config.users.mutableUsers { };
boot.extraModprobeConfig = readFile "${pkgs.google-guest-configs}/etc/modprobe.d/gce-blacklist.conf";
environment.etc."sysctl.d/60-gce-network-security.conf".source =
"${pkgs.google-guest-configs}/etc/sysctl.d/60-gce-network-security.conf";
environment.etc."default/instance_configs.cfg".text = ''
[Accounts]
useradd_cmd = useradd -m -s /run/current-system/sw/bin/bash -p * {user}
[Daemons]
accounts_daemon = ${boolToString config.users.mutableUsers}
[InstanceSetup]
# Make sure GCE image does not replace host key that NixOps sets.
set_host_keys = false
[MetadataScripts]
default_shell = ${pkgs.stdenv.shell}
[NetworkInterfaces]
dhclient_script = ${pkgs.google-guest-configs}/bin/google-dhclient-script
# We set up network interfaces declaratively.
setup = false
'';
}

View File

@@ -0,0 +1,137 @@
{
config,
lib,
pkgs,
...
}:
with lib;
let
cfg = config.virtualisation.googleComputeImage;
defaultConfigFile = pkgs.writeText "configuration.nix" ''
{ ... }:
{
imports = [
<nixpkgs/nixos/modules/virtualisation/google-compute-image.nix>
];
}
'';
in
{
imports = [
./google-compute-config.nix
./disk-size-option.nix
../image/file-options.nix
(lib.mkRenamedOptionModuleWith {
sinceRelease = 2411;
from = [
"virtualisation"
"googleComputeImage"
"diskSize"
];
to = [
"virtualisation"
"diskSize"
];
})
];
options = {
virtualisation.googleComputeImage.configFile = mkOption {
type = with types; nullOr str;
default = null;
description = ''
A path to a configuration file which will be placed at `/etc/nixos/configuration.nix`
and be used when switching to a new configuration.
If set to `null`, a default configuration is used, where the only import is
`<nixpkgs/nixos/modules/virtualisation/google-compute-image.nix>`.
'';
};
virtualisation.googleComputeImage.compressionLevel = mkOption {
type = types.int;
default = 6;
description = ''
GZIP compression level of the resulting disk image (1-9).
'';
};
virtualisation.googleComputeImage.contents = mkOption {
type = with types; listOf attrs;
default = [ ];
description = ''
The files and directories to be placed in the image.
This is a list of attribute sets {source, target, mode, user, group} where
`source' is the file system object (regular file or directory) to be
grafted in the file system at path `target', `mode' is a string containing
the permissions that will be set (ex. "755"), `user' and `group' are the
user and group name that will be set as owner of the files.
`mode', `user', and `group' are optional.
When setting one of `user' or `group', the other needs to be set too.
'';
example = literalExpression ''
[
{
source = ./default.nix;
target = "/etc/nixos/default.nix";
mode = "0644";
user = "root";
group = "root";
}
];
'';
};
virtualisation.googleComputeImage.efi = mkEnableOption "EFI booting";
};
#### implementation
config = {
boot.initrd.availableKernelModules = [ "nvme" ];
boot.loader.grub = mkIf cfg.efi {
device = mkForce "nodev";
efiSupport = true;
efiInstallAsRemovable = true;
};
fileSystems."/boot" = mkIf cfg.efi {
device = "/dev/disk/by-label/ESP";
fsType = "vfat";
};
system.nixos.tags = [ "google-compute" ];
image.extension = "raw.tar.gz";
system.build.image = config.system.build.googleComputeImage;
system.build.googleComputeImage = import ../../lib/make-disk-image.nix {
name = "google-compute-image";
inherit (config.image) baseName;
postVM = ''
PATH=$PATH:${
with pkgs;
lib.makeBinPath [
gnutar
gzip
]
}
pushd $out
# RTFM:
# https://cloud.google.com/compute/docs/images/create-custom
# https://cloud.google.com/compute/docs/import/import-existing-image
mv $diskImage disk.raw
tar -Sc disk.raw | gzip -${toString cfg.compressionLevel} > \
${config.image.fileName}
rm disk.raw
popd
'';
format = "raw";
configFile = if cfg.configFile == null then defaultConfigFile else cfg.configFile;
inherit (cfg) contents;
partitionTableType = if cfg.efi then "efi" else "legacy";
inherit (config.virtualisation) diskSize;
inherit config lib pkgs;
};
};
}

View File

@@ -0,0 +1,4 @@
# This profile is deprecated, use boot.growPartition directly.
builtins.trace
"the profile <nixos/modules/virtualisation/grow-partition.nix> is deprecated, use boot.growPartition instead"
{ }

View File

@@ -0,0 +1,71 @@
{
config,
lib,
pkgs,
...
}:
with lib;
let
cfg = config.virtualisation.hypervGuest;
in
{
imports = [
(mkRemovedOptionModule [
"virtualisation"
"hypervGuest"
"videoMode"
] "The video mode can now be configured via standard tools, or in Hyper-V VM settings.")
];
options = {
virtualisation.hypervGuest = {
enable = mkEnableOption "Hyper-V Guest Support";
};
};
config = mkIf cfg.enable {
boot = {
initrd.kernelModules = [
"hv_balloon"
"hv_netvsc"
"hv_storvsc"
"hv_utils"
"hv_vmbus"
];
initrd.availableKernelModules = [ "hyperv_keyboard" ];
kernelParams = [
"elevator=noop"
];
};
environment.systemPackages = [ config.boot.kernelPackages.hyperv-daemons.bin ];
# enable hotadding cpu/memory
services.udev.packages = lib.singleton (
pkgs.writeTextFile {
name = "hyperv-cpu-and-memory-hotadd-udev-rules";
destination = "/etc/udev/rules.d/99-hyperv-cpu-and-memory-hotadd.rules";
text = ''
# Memory hotadd
SUBSYSTEM=="memory", ACTION=="add", DEVPATH=="/devices/system/memory/memory[0-9]*", TEST=="state", ATTR{state}="online"
# CPU hotadd
SUBSYSTEM=="cpu", ACTION=="add", DEVPATH=="/devices/system/cpu/cpu[0-9]*", TEST=="online", ATTR{online}="1"
'';
}
);
systemd = {
packages = [ config.boot.kernelPackages.hyperv-daemons.lib ];
targets.hyperv-daemons = {
wantedBy = [ "multi-user.target" ];
};
};
};
}

View File

@@ -0,0 +1,97 @@
{
config,
pkgs,
lib,
...
}:
with lib;
let
cfg = config.hyperv;
in
{
imports = [
./disk-size-option.nix
../image/file-options.nix
(lib.mkRenamedOptionModuleWith {
sinceRelease = 2411;
from = [
"hyperv"
"baseImageSize"
];
to = [
"virtualisation"
"diskSize"
];
})
(lib.mkRenamedOptionModuleWith {
sinceRelease = 2505;
from = [
"virtualisation"
"hyperv"
"vmFileName"
];
to = [
"image"
"fileName"
];
})
];
options = {
hyperv = {
vmDerivationName = mkOption {
type = types.str;
default = "nixos-hyperv-${config.system.nixos.label}-${pkgs.stdenv.hostPlatform.system}";
description = ''
The name of the derivation for the hyper-v appliance.
'';
};
};
};
config = {
# Use a priority just below mkOptionDefault (1500) instead of lib.mkDefault
# to avoid breaking existing configs using that.
virtualisation.diskSize = lib.mkOverride 1490 (4 * 1024);
system.nixos.tags = [ "hyperv" ];
image.extension = "vhdx";
system.build.image = config.system.build.hypervImage;
system.build.hypervImage = import ../../lib/make-disk-image.nix {
name = cfg.vmDerivationName;
baseName = config.image.baseName;
postVM = ''
${pkgs.vmTools.qemu}/bin/qemu-img convert -f raw -o subformat=dynamic -O vhdx $diskImage $out/${config.image.fileName}
rm $diskImage
'';
format = "raw";
inherit (config.virtualisation) diskSize;
partitionTableType = "efi";
inherit config lib pkgs;
};
fileSystems."/" = {
device = "/dev/disk/by-label/nixos";
autoResize = true;
fsType = "ext4";
};
fileSystems."/boot" = {
device = "/dev/disk/by-label/ESP";
fsType = "vfat";
};
boot.growPartition = true;
boot.loader.grub = {
device = "nodev";
efiSupport = true;
efiInstallAsRemovable = true;
};
virtualisation.hypervGuest.enable = true;
};
}

View File

@@ -0,0 +1,41 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.virtualisation.incus.agent;
in
{
meta = {
maintainers = lib.teams.lxc.members;
};
options = {
virtualisation.incus.agent.enable = lib.mkEnableOption "Incus agent";
};
config = lib.mkIf cfg.enable {
services.udev.packages = [ config.virtualisation.incus.package.agent_loader ];
systemd.packages = [ config.virtualisation.incus.package.agent_loader ];
systemd.services.incus-agent = {
enable = true;
wantedBy = [ "multi-user.target" ];
path = [
pkgs.kmod
pkgs.util-linux
# allow `incus exec` to find system binaries
"/run/current-system/sw"
];
# avoid killing nixos-rebuild switch when executed through incus exec
restartIfChanged = false;
stopIfChanged = false;
};
};
}

View File

@@ -0,0 +1,61 @@
{
config,
lib,
pkgs,
...
}:
let
serialDevice = if pkgs.stdenv.hostPlatform.isx86 then "ttyS0" else "ttyAMA0";
in
{
meta = {
maintainers = lib.teams.lxc.members;
};
imports = [
./lxc-instance-common.nix
../profiles/qemu-guest.nix
];
config = {
system.build.qemuImage = import ../../lib/make-disk-image.nix {
inherit pkgs lib config;
partitionTableType = "efi";
format = "qcow2-compressed";
copyChannel = true;
};
fileSystems = {
"/" = {
device = "/dev/disk/by-label/nixos";
autoResize = true;
fsType = "ext4";
};
"/boot" = {
device = "/dev/disk/by-label/ESP";
fsType = "vfat";
};
};
boot.growPartition = true;
boot.loader.systemd-boot.enable = true;
# image building needs to know what device to install bootloader on
boot.loader.grub.device = "/dev/vda";
boot.kernelParams = [
"console=tty1"
"console=${serialDevice}"
];
# CPU hotplug
services.udev.extraRules = ''
SUBSYSTEM=="cpu", CONST{arch}=="x86-64", TEST=="online", ATTR{online}=="0", ATTR{online}="1"
'';
virtualisation.incus.agent.enable = lib.mkDefault true;
};
}

View File

@@ -0,0 +1,527 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.virtualisation.incus;
preseedFormat = pkgs.formats.yaml { };
nvidiaEnabled = (lib.elem "nvidia" config.services.xserver.videoDrivers);
serverBinPath = ''/run/wrappers/bin:${pkgs.qemu_kvm}/libexec:${
lib.makeBinPath (
with pkgs;
[
cfg.package
acl
attr
bash
btrfs-progs
cdrkit
coreutils
criu
dnsmasq
e2fsprogs
findutils
getent
gawk
gnugrep
gnused
gnutar
gptfdisk
gzip
iproute2
iptables
iw
kmod
libxfs
lvm2
lxcfs
minio
minio-client
nftables
qemu-utils
qemu_kvm
rsync
squashfs-tools-ng
squashfsTools
sshfs
swtpm
systemd
thin-provisioning-tools
util-linux
virtiofsd
xdelta
xz
]
++ lib.optionals (lib.versionAtLeast cfg.package.version "6.3.0") [
skopeo
umoci
]
++ lib.optionals (lib.versionAtLeast cfg.package.version "6.11.0") [
lego
]
++ lib.optionals config.security.apparmor.enable [
apparmor-bin-utils
(writeShellScriptBin "apparmor_parser" ''
exec '${apparmor-parser}/bin/apparmor_parser' -I '${apparmor-profiles}/etc/apparmor.d' "$@"
'')
]
++ lib.optionals config.services.ceph.client.enable [ ceph-client ]
++ lib.optionals config.virtualisation.vswitch.enable [ config.virtualisation.vswitch.package ]
++ lib.optionals config.boot.zfs.enabled [
config.boot.zfs.package
"${config.boot.zfs.package}/lib/udev"
]
++ lib.optionals nvidiaEnabled [
libnvidia-container
]
)
}'';
# https://github.com/lxc/incus/blob/cff35a29ee3d7a2af1f937cbb6cf23776941854b/internal/server/instance/drivers/driver_qemu.go#L123
OVMF2MB = pkgs.OVMF.override {
secureBoot = true;
fdSize2MB = true;
};
ovmf-prefix = if pkgs.stdenv.hostPlatform.isAarch64 then "AAVMF" else "OVMF";
ovmf = pkgs.linkFarm "incus-ovmf" (
[
# 2MB must remain the default or existing VMs will fail to boot. New VMs will prefer 4MB
{
name = "OVMF_CODE.fd";
path = "${OVMF2MB.fd}/FV/${ovmf-prefix}_CODE.fd";
}
{
name = "OVMF_VARS.fd";
path = "${OVMF2MB.fd}/FV/${ovmf-prefix}_VARS.fd";
}
{
name = "OVMF_VARS.ms.fd";
path = "${OVMF2MB.fd}/FV/${ovmf-prefix}_VARS.fd";
}
{
name = "OVMF_CODE.4MB.fd";
path = "${pkgs.OVMFFull.fd}/FV/${ovmf-prefix}_CODE.fd";
}
{
name = "OVMF_VARS.4MB.fd";
path = "${pkgs.OVMFFull.fd}/FV/${ovmf-prefix}_VARS.fd";
}
{
name = "OVMF_VARS.4MB.ms.fd";
path = "${pkgs.OVMFFull.fd}/FV/${ovmf-prefix}_VARS.fd";
}
]
++ lib.optionals pkgs.stdenv.hostPlatform.isx86_64 [
{
name = "seabios.bin";
path = "${pkgs.seabios-qemu}/share/seabios/bios.bin";
}
]
);
environment = lib.mkMerge [
{
INCUS_DOCUMENTATION = "${cfg.package.doc}/html";
INCUS_EDK2_PATH = ovmf;
INCUS_LXC_HOOK = "${cfg.lxcPackage}/share/lxc/hooks";
INCUS_LXC_TEMPLATE_CONFIG = "${pkgs.lxcfs}/share/lxc/config";
INCUS_USBIDS_PATH = "${pkgs.hwdata}/share/hwdata/usb.ids";
INCUS_AGENT_PATH = "${cfg.package}/share/agent";
PATH = lib.mkForce serverBinPath;
}
(lib.mkIf (cfg.ui.enable) { "INCUS_UI" = cfg.ui.package; })
];
incus-startup = pkgs.writeShellScript "incus-startup" ''
case "$1" in
start)
systemctl is-active incus.service -q && exit 0
exec incusd activateifneeded
;;
stop)
systemctl is-active incus.service -q || exit 0
exec incusd shutdown
;;
*)
echo "unknown argument \`$1'" >&2
exit 1
;;
esac
exit 0
'';
in
{
meta = {
maintainers = lib.teams.lxc.members;
};
options = {
virtualisation.incus = {
enable = lib.mkEnableOption ''
incusd, a daemon that manages containers and virtual machines.
Users in the "incus-admin" group can interact with
the daemon (e.g. to start or stop containers) using the
{command}`incus` command line tool, among others.
Users in the "incus" group can also interact with
the daemon, but with lower permissions
(i.e. administrative operations are forbidden).
'';
package = lib.mkPackageOption pkgs "incus-lts" { };
lxcPackage = lib.mkOption {
type = lib.types.package;
default = config.virtualisation.lxc.package;
defaultText = lib.literalExpression "config.virtualisation.lxc.package";
description = "The lxc package to use.";
};
clientPackage = lib.mkOption {
type = lib.types.package;
default = cfg.package.client;
defaultText = lib.literalExpression "config.virtualisation.incus.package.client";
description = "The incus client package to use. This package is added to PATH.";
};
softDaemonRestart = lib.mkOption {
type = lib.types.bool;
default = true;
description = ''
Allow for incus.service to be stopped without affecting running instances.
'';
};
preseed = lib.mkOption {
type = lib.types.nullOr (lib.types.submodule { freeformType = preseedFormat.type; });
default = null;
description = ''
Configuration for Incus preseed, see
<https://linuxcontainers.org/incus/docs/main/howto/initialize/#non-interactive-configuration>
for supported values.
Changes to this will be re-applied to Incus which will overwrite existing entities or create missing ones,
but entities will *not* be removed by preseed.
'';
example = {
networks = [
{
name = "incusbr0";
type = "bridge";
config = {
"ipv4.address" = "10.0.100.1/24";
"ipv4.nat" = "true";
};
}
];
profiles = [
{
name = "default";
devices = {
eth0 = {
name = "eth0";
network = "incusbr0";
type = "nic";
};
root = {
path = "/";
pool = "default";
size = "35GiB";
type = "disk";
};
};
}
];
storage_pools = [
{
name = "default";
driver = "dir";
config = {
source = "/var/lib/incus/storage-pools/default";
};
}
];
};
};
socketActivation = lib.mkEnableOption ''
socket-activation for starting incus.service. Enabling this option
will stop incus.service from starting automatically on boot.
'';
startTimeout = lib.mkOption {
type = lib.types.ints.unsigned;
default = 600;
apply = toString;
description = ''
Time to wait (in seconds) for incusd to become ready to process requests.
If incusd does not reply within the configured time, `incus.service` will be
considered failed and systemd will attempt to restart it.
'';
};
ui = {
enable = lib.mkEnableOption "Incus Web UI";
package = lib.mkPackageOption pkgs [ "incus-ui-canonical" ] { };
};
};
};
config = lib.mkIf cfg.enable {
assertions = [
{
assertion =
!(
config.networking.firewall.enable
&& !config.networking.nftables.enable
&& config.virtualisation.incus.enable
);
message = "Incus on NixOS is unsupported using iptables. Set `networking.nftables.enable = true;`";
}
];
# https://github.com/lxc/incus/blob/f145309929f849b9951658ad2ba3b8f10cbe69d1/doc/reference/server_settings.md
boot.kernel.sysctl = {
"fs.aio-max-nr" = lib.mkDefault 524288;
"fs.inotify.max_queued_events" = lib.mkDefault 1048576;
"fs.inotify.max_user_instances" = lib.mkOverride 1050 1048576; # override in case conflict nixos/modules/services/x11/xserver.nix
"fs.inotify.max_user_watches" = lib.mkOverride 1050 1048576; # override in case conflict nixos/modules/services/x11/xserver.nix
"kernel.dmesg_restrict" = lib.mkDefault 1;
"kernel.keys.maxbytes" = lib.mkDefault 2000000;
"kernel.keys.maxkeys" = lib.mkDefault 2000;
"net.core.bpf_jit_limit" = lib.mkDefault 1000000000;
"net.ipv4.neigh.default.gc_thresh3" = lib.mkDefault 8192;
"net.ipv6.neigh.default.gc_thresh3" = lib.mkDefault 8192;
# vm.max_map_count is set higher in nixos/modules/config/sysctl.nix
};
boot.kernelModules = [
"br_netfilter"
"veth"
"xt_comment"
"xt_CHECKSUM"
"xt_MASQUERADE"
"vhost_vsock"
]
++ lib.optionals nvidiaEnabled [ "nvidia_uvm" ];
environment.systemPackages = [
cfg.clientPackage
# gui console support
pkgs.spice-gtk
];
# Note: the following options are also declared in virtualisation.lxc, but
# the latter can't be simply enabled to reuse the formers, because it
# does a bunch of unrelated things.
systemd.tmpfiles.rules = [ "d /var/lib/lxc/rootfs 0755 root root -" ];
security.apparmor = {
packages = [ cfg.lxcPackage ];
policies = {
"bin.lxc-start".profile = ''
include ${cfg.lxcPackage}/etc/apparmor.d/usr.bin.lxc-start
'';
"lxc-containers".profile = ''
include ${cfg.lxcPackage}/etc/apparmor.d/lxc-containers
'';
"incusd".profile = ''
# This profile allows everything and only exists to give the
# application a name instead of having the label "unconfined"
abi <abi/4.0>,
include <tunables/global>
profile incusd ${lib.getExe' config.virtualisation.incus.package "incusd"} flags=(unconfined) {
userns,
include "/var/lib/incus/security/apparmor/cache"
# Site-specific additions and overrides. See local/README for details.
include if exists <local/incusd>
}
include "/var/lib/incus/security/apparmor/profiles"
'';
};
includes."abstractions/base" = ''
# Allow incusd's various AA profiles to load dynamic libraries from Nix store
# https://discuss.linuxcontainers.org/t/creating-new-containers-vms-blocked-by-apparmor-on-nixos/21908/6
mr /nix/store/*/lib/*.so*,
r ${pkgs.stdenv.cc.libc}/lib/gconv/gconv-modules,
r ${pkgs.stdenv.cc.libc}/lib/gconv/gconv-modules.d/,
r ${pkgs.stdenv.cc.libc}/lib/gconv/gconv-modules.d/gconv-modules-extra.conf,
# Support use of VM instance
mrix ${pkgs.qemu_kvm}/bin/*,
k ${OVMF2MB.fd}/FV/*.fd,
k ${pkgs.OVMFFull.fd}/FV/*.fd,
''
+ lib.optionalString pkgs.stdenv.hostPlatform.isx86_64 ''
k ${pkgs.seabios-qemu}/share/seabios/bios.bin,
'';
};
systemd.services.incus = {
description = "Incus Container and Virtual Machine Management Daemon";
inherit environment;
wantedBy = lib.mkIf (!cfg.socketActivation) [ "multi-user.target" ];
after = [
"network-online.target"
"lxcfs.service"
"incus.socket"
]
++ lib.optionals config.virtualisation.vswitch.enable [ "ovs-vswitchd.service" ];
requires = [
"lxcfs.service"
"incus.socket"
]
++ lib.optionals config.virtualisation.vswitch.enable [ "ovs-vswitchd.service" ];
wants = [ "network-online.target" ];
serviceConfig = {
ExecStart = "${cfg.package}/bin/incusd --group incus-admin";
ExecStartPost = "${cfg.package}/bin/incusd waitready --timeout=${cfg.startTimeout}";
ExecStop = lib.optionalString (!cfg.softDaemonRestart) "${cfg.package}/bin/incus admin shutdown";
KillMode = "process"; # when stopping, leave the containers alone
Delegate = "yes";
LimitMEMLOCK = "infinity";
LimitNOFILE = "1048576";
LimitNPROC = "infinity";
TasksMax = "infinity";
Restart = "on-failure";
TimeoutStartSec = "${cfg.startTimeout}s";
TimeoutStopSec = "30s";
};
};
systemd.services.incus-user = {
description = "Incus Container and Virtual Machine Management User Daemon";
inherit environment;
after = [
"incus.service"
"incus-user.socket"
];
requires = [
"incus-user.socket"
];
serviceConfig = {
ExecStart = "${cfg.package}/bin/incus-user --group incus";
Restart = "on-failure";
};
};
systemd.services.incus-startup = lib.mkIf cfg.softDaemonRestart {
description = "Incus Instances Startup/Shutdown";
inherit environment;
after = [
"incus.service"
"incus.socket"
];
requires = [ "incus.socket" ];
wantedBy = config.systemd.services.incus.wantedBy;
# restarting this service will affect instances
restartIfChanged = false;
serviceConfig = {
ExecStart = "${incus-startup} start";
ExecStop = "${incus-startup} stop";
RemainAfterExit = true;
TimeoutStartSec = "600s";
TimeoutStopSec = "600s";
Type = "oneshot";
};
};
systemd.sockets.incus = {
description = "Incus UNIX socket";
wantedBy = [ "sockets.target" ];
socketConfig = {
ListenStream = "/var/lib/incus/unix.socket";
SocketMode = "0660";
SocketGroup = "incus-admin";
};
};
systemd.sockets.incus-user = {
description = "Incus user UNIX socket";
wantedBy = [ "sockets.target" ];
socketConfig = {
ListenStream = "/var/lib/incus/unix.socket.user";
SocketMode = "0660";
SocketGroup = "incus";
};
};
systemd.services.incus-preseed = lib.mkIf (cfg.preseed != null) {
description = "Incus initialization with preseed file";
wantedBy = [ "incus.service" ];
after = [ "incus.service" ];
bindsTo = [ "incus.service" ];
partOf = [ "incus.service" ];
script = ''
${cfg.package}/bin/incus admin init --preseed <${preseedFormat.generate "incus-preseed.yaml" cfg.preseed}
'';
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
};
};
users.groups.incus = { };
users.groups.incus-admin = { };
users.users.root = {
# match documented default ranges https://linuxcontainers.org/incus/docs/main/userns-idmap/#allowed-ranges
subUidRanges = [
{
startUid = 1000000;
count = 1000000000;
}
];
subGidRanges = [
{
startGid = 1000000;
count = 1000000000;
}
];
};
virtualisation.lxc.lxcfs.enable = true;
};
}

View File

@@ -0,0 +1,40 @@
{
config,
lib,
pkgs,
...
}:
{
imports = [
../profiles/qemu-guest.nix
../image/file-options.nix
];
config = {
fileSystems."/" = {
device = "/dev/disk/by-label/nixos";
fsType = "ext4";
autoResize = true;
};
boot.growPartition = true;
boot.kernelParams = [ "console=ttyS0" ];
boot.loader.grub.device = "/dev/vda";
boot.loader.timeout = 0;
services.qemuGuest.enable = true;
services.openssh.enable = true;
services.cloud-init.enable = true;
systemd.services."serial-getty@ttyS0".enable = true;
system.nixos.tags = [ "kubevirt" ];
image.extension = "qcow2";
system.build.image = config.system.build.kubevirtImage;
system.build.kubevirtImage = import ../../lib/make-disk-image.nix {
inherit lib config pkgs;
inherit (config.image) baseName;
format = "qcow2";
};
};
}

View File

@@ -0,0 +1,103 @@
{
config,
lib,
pkgs,
...
}:
with lib;
let
cfg = config.virtualisation.kvmgt;
kernelPackages = config.boot.kernelPackages;
vgpuOptions = {
uuid = mkOption {
type = with types; listOf str;
description = "UUID(s) of VGPU device. You can generate one with `libossp_uuid`.";
};
};
in
{
options = {
virtualisation.kvmgt = {
enable = mkEnableOption ''
KVMGT (iGVT-g) VGPU support. Allows Qemu/KVM guests to share host's Intel integrated graphics card.
Currently only one graphical device can be shared. To allow users to access the device without root add them
to the kvm group: `users.extraUsers.<yourusername>.extraGroups = [ "kvm" ];`
'';
# multi GPU support is under the question
device = mkOption {
type = types.str;
default = "0000:00:02.0";
description = "PCI ID of graphics card. You can figure it with {command}`ls /sys/class/mdev_bus`.";
};
vgpus = mkOption {
default = { };
type = with types; attrsOf (submodule [ { options = vgpuOptions; } ]);
description = ''
Virtual GPUs to be used in Qemu. You can find devices via {command}`ls /sys/bus/pci/devices/*/mdev_supported_types`
and find info about device via {command}`cat /sys/bus/pci/devices/*/mdev_supported_types/i915-GVTg_V5_4/description`
'';
example = {
i915-GVTg_V5_8.uuid = [ "a297db4a-f4c2-11e6-90f6-d3b88d6c9525" ];
};
};
};
};
config = mkIf cfg.enable {
assertions = singleton {
assertion = versionAtLeast kernelPackages.kernel.version "4.16";
message = "KVMGT is not properly supported for kernels older than 4.16";
};
boot.kernelModules = [ "kvmgt" ];
boot.kernelParams = [ "i915.enable_gvt=1" ];
services.udev.extraRules = ''
SUBSYSTEM=="vfio", OWNER="root", GROUP="kvm"
'';
systemd =
let
vgpus = listToAttrs (
flatten (
mapAttrsToList (
mdev: opt:
map (
id:
nameValuePair "kvmgt-${id}" {
inherit mdev;
uuid = id;
}
) opt.uuid
) cfg.vgpus
)
);
in
{
paths = mapAttrs (_: opt: {
description = "KVMGT VGPU ${opt.uuid} path";
wantedBy = [ "multi-user.target" ];
pathConfig = {
PathExists = "/sys/bus/pci/devices/${cfg.device}/mdev_supported_types/${opt.mdev}/create";
};
}) vgpus;
services = mapAttrs (_: opt: {
description = "KVMGT VGPU ${opt.uuid}";
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
ExecStart = "${pkgs.runtimeShell} -c 'echo ${opt.uuid} > /sys/bus/pci/devices/${cfg.device}/mdev_supported_types/${opt.mdev}/create'";
ExecStop = "${pkgs.runtimeShell} -c 'echo 1 > /sys/bus/pci/devices/${cfg.device}/${opt.uuid}/remove'";
};
}) vgpus;
};
};
meta.maintainers = with maintainers; [ patryk27 ];
}

View File

@@ -0,0 +1,628 @@
{
config,
lib,
pkgs,
...
}:
with lib;
let
cfg = config.virtualisation.libvirtd;
vswitch = config.virtualisation.vswitch;
configFile = pkgs.writeText "libvirtd.conf" ''
auth_unix_ro = "polkit"
auth_unix_rw = "polkit"
${cfg.extraConfig}
'';
qemuConfigFile = pkgs.writeText "qemu.conf" ''
${optionalString (!cfg.qemu.runAsRoot) ''
user = "qemu-libvirtd"
group = "qemu-libvirtd"
''}
${cfg.qemu.verbatimConfig}
'';
networkConfigFile = pkgs.writeText "network.conf" ''
firewall_backend = "${cfg.firewallBackend}"
'';
dirName = "libvirt";
subDirs = list: [ dirName ] ++ map (e: "${dirName}/${e}") list;
swtpmModule = types.submodule {
options = {
enable = mkOption {
type = types.bool;
default = false;
description = ''
Allows libvirtd to use swtpm to create an emulated TPM.
'';
};
package = mkPackageOption pkgs "swtpm" { };
};
};
qemuModule = types.submodule {
options = {
package = mkPackageOption pkgs "qemu" {
extraDescription = ''
`pkgs.qemu` can emulate alien architectures (e.g. aarch64 on x86)
`pkgs.qemu_kvm` saves disk space allowing to emulate only host architectures.
'';
};
runAsRoot = mkOption {
type = types.bool;
default = true;
description = ''
If true, libvirtd runs qemu as root.
If false, libvirtd runs qemu as unprivileged user qemu-libvirtd.
Changing this option to false may cause file permission issues
for existing guests. To fix these, manually change ownership
of affected files in /var/lib/libvirt/qemu to qemu-libvirtd.
'';
};
verbatimConfig = mkOption {
type = types.lines;
default = ''
namespaces = []
'';
description = ''
Contents written to the qemu configuration file, qemu.conf.
Make sure to include a proper namespace configuration when
supplying custom configuration.
'';
};
ovmf = mkOption {
type = types.submodule {
options = {
enable = mkOption {
type = types.nullOr types.bool;
default = null;
internal = true;
};
package = mkOption {
type = types.nullOr types.package;
default = null;
internal = true;
};
packages = mkOption {
type = types.nullOr (types.listOf types.package);
default = null;
internal = true;
};
};
};
default = { };
internal = true;
description = "This submodule is deprecated and has been removed";
};
swtpm = mkOption {
type = swtpmModule;
default = { };
description = ''
QEMU's swtpm options.
'';
};
vhostUserPackages = mkOption {
type = types.listOf types.package;
default = [ ];
example = lib.literalExpression "[ pkgs.virtiofsd ]";
description = ''
Packages containing out-of-tree vhost-user drivers.
'';
};
};
};
hooksModule = types.submodule {
options = {
daemon = mkOption {
type = types.attrsOf types.path;
default = { };
description = ''
Hooks that will be placed under /var/lib/libvirt/hooks/daemon.d/
and called for daemon start/shutdown/SIGHUP events.
Please see <https://libvirt.org/hooks.html> for documentation.
'';
};
qemu = mkOption {
type = types.attrsOf types.path;
default = { };
description = ''
Hooks that will be placed under /var/lib/libvirt/hooks/qemu.d/
and called for qemu domains begin/end/migrate events.
Please see <https://libvirt.org/hooks.html> for documentation.
'';
};
lxc = mkOption {
type = types.attrsOf types.path;
default = { };
description = ''
Hooks that will be placed under /var/lib/libvirt/hooks/lxc.d/
and called for lxc domains begin/end events.
Please see <https://libvirt.org/hooks.html> for documentation.
'';
};
libxl = mkOption {
type = types.attrsOf types.path;
default = { };
description = ''
Hooks that will be placed under /var/lib/libvirt/hooks/libxl.d/
and called for libxl-handled xen domains begin/end events.
Please see <https://libvirt.org/hooks.html> for documentation.
'';
};
network = mkOption {
type = types.attrsOf types.path;
default = { };
description = ''
Hooks that will be placed under /var/lib/libvirt/hooks/network.d/
and called for networks begin/end events.
Please see <https://libvirt.org/hooks.html> for documentation.
'';
};
};
};
nssModule = types.submodule {
options = {
enable = mkOption {
type = types.bool;
default = false;
description = ''
This option enables the older libvirt NSS module. This method uses
DHCP server records, therefore is dependent on the hostname provided
by the guest.
Please see <https://libvirt.org/nss.html> for more information.
'';
};
enableGuest = mkOption {
type = types.bool;
default = false;
description = ''
This option enables the newer libvirt_guest NSS module. This module
uses the libvirt guest name instead of the hostname of the guest.
Please see <https://libvirt.org/nss.html> for more information.
'';
};
};
};
qemuOvmfMetadata = pkgs.stdenv.mkDerivation {
name = "qemu-ovmf-metadata";
version = cfg.qemu.package.version;
nativeBuildInputs = [ cfg.qemu.package ];
dontBuild = true;
dontUnpack = true;
installPhase = ''
mkdir -p $out
cp ${cfg.qemu.package}/share/qemu/firmware/*.json $out
substituteInPlace $out/*.json \
--replace-fail "${cfg.qemu.package}/share/qemu/" "/run/${dirName}/nix-ovmf/"
'';
};
in
{
imports = [
(mkRemovedOptionModule [
"virtualisation"
"libvirtd"
"enableKVM"
] "Set the option `virtualisation.libvirtd.qemu.package' instead.")
(mkRenamedOptionModule
[ "virtualisation" "libvirtd" "qemuPackage" ]
[ "virtualisation" "libvirtd" "qemu" "package" ]
)
(mkRenamedOptionModule
[ "virtualisation" "libvirtd" "qemuRunAsRoot" ]
[ "virtualisation" "libvirtd" "qemu" "runAsRoot" ]
)
(mkRenamedOptionModule
[ "virtualisation" "libvirtd" "qemuVerbatimConfig" ]
[ "virtualisation" "libvirtd" "qemu" "verbatimConfig" ]
)
(mkRenamedOptionModule
[ "virtualisation" "libvirtd" "qemuSwtpm" ]
[ "virtualisation" "libvirtd" "qemu" "swtpm" "enable" ]
)
(mkRemovedOptionModule [ "virtualisation" "libvirtd" "qemuOvmf" ]
"The 'virtualisation.libvirtd.qemuOvmf' option has been removed. All OVMF images distributed with QEMU are now available by default."
)
(mkRemovedOptionModule [ "virtualisation" "libvirtd" "qemuOvmfPackage" ]
"The 'virtualisation.libvirtd.qemuOvmfPackage' option has been removed. All OVMF images distributed with QEMU are now available by default."
)
];
###### interface
options.virtualisation.libvirtd = {
enable = mkOption {
type = types.bool;
default = false;
description = ''
This option enables libvirtd, a daemon that manages
virtual machines. Users in the "libvirtd" group can interact with
the daemon (e.g. to start or stop VMs) using the
{command}`virsh` command line tool, among others.
'';
};
package = mkPackageOption pkgs "libvirt" { };
extraConfig = mkOption {
type = types.lines;
default = "";
description = ''
Extra contents appended to the libvirtd configuration file,
libvirtd.conf.
'';
};
extraOptions = mkOption {
type = types.listOf types.str;
default = [ ];
example = [ "--verbose" ];
description = ''
Extra command line arguments passed to libvirtd on startup.
'';
};
onBoot = mkOption {
type = types.enum [
"start"
"ignore"
];
default = "start";
description = ''
Specifies the action to be done to / on the guests when the host boots.
The "start" option starts all guests that were running prior to shutdown
regardless of their autostart settings. The "ignore" option will not
start the formerly running guest on boot. However, any guest marked as
autostart will still be automatically started by libvirtd.
'';
};
onShutdown = mkOption {
type = types.enum [
"shutdown"
"suspend"
];
default = "suspend";
description = ''
When shutting down / restarting the host what method should
be used to gracefully halt the guests. Setting to "shutdown"
will cause an ACPI shutdown of each guest. "suspend" will
attempt to save the state of the guests ready to restore on boot.
'';
};
parallelShutdown = mkOption {
type = types.ints.unsigned;
default = 0;
description = ''
Number of guests that will be shutdown concurrently, taking effect when onShutdown
is set to "shutdown". If set to 0, guests will be shutdown one after another.
Number of guests on shutdown at any time will not exceed number set in this
variable.
'';
};
shutdownTimeout = mkOption {
type = types.ints.unsigned;
default = 300;
description = ''
Number of seconds we're willing to wait for a guest to shut down.
If parallel shutdown is enabled, this timeout applies as a timeout
for shutting down all guests on a single URI defined in the variable URIS.
If this is 0, then there is no time out (use with caution, as guests might not
respond to a shutdown request).
'';
};
startDelay = mkOption {
type = types.ints.unsigned;
default = 0;
description = ''
Number of seconds to wait between each guest start.
If set to 0, all guests will start up in parallel.
'';
};
allowedBridges = mkOption {
type = types.listOf types.str;
default = [ "virbr0" ];
description = ''
List of bridge devices that can be used by qemu:///session
'';
};
qemu = mkOption {
type = qemuModule;
default = { };
description = ''
QEMU related options.
'';
};
hooks = mkOption {
type = hooksModule;
default = { };
description = ''
Hooks related options.
'';
};
nss = mkOption {
type = nssModule;
default = { };
description = ''
libvirt NSS module options.
'';
};
sshProxy = mkOption {
type = types.bool;
default = true;
description = ''
Whether to configure OpenSSH to use the [SSH Proxy](https://libvirt.org/ssh-proxy.html).
'';
};
firewallBackend = mkOption {
type = types.enum [
"iptables"
"nftables"
];
default = if config.networking.nftables.enable then "nftables" else "iptables";
defaultText = lib.literalExpression "if config.networking.nftables.enable then \"nftables\" else \"iptables\"";
description = ''
The backend used to setup virtual network firewall rules.
'';
};
};
###### implementation
config = mkIf cfg.enable {
assertions = [
{
assertion = config.security.polkit.enable;
message = "The libvirtd module currently requires Polkit to be enabled ('security.polkit.enable = true').";
}
{
assertion = ((lib.filterAttrs (n: v: v != null) cfg.qemu.ovmf) == { });
message = "The 'virtualisation.libvirtd.qemu.ovmf' submodule has been removed. All OVMF images distributed with QEMU are now available by default.";
}
];
environment = {
# this file is expected in /etc/qemu and not sysconfdir (/var/lib)
etc."qemu/bridge.conf".text = lib.concatMapStringsSep "\n" (e: "allow ${e}") cfg.allowedBridges;
systemPackages = with pkgs; [
libressl.nc
config.networking.firewall.package
cfg.package
cfg.qemu.package
];
etc.ethertypes.source = "${pkgs.iptables}/etc/ethertypes";
};
boot.kernelModules = [ "tun" ];
users.groups.libvirtd.gid = config.ids.gids.libvirtd;
# libvirtd runs qemu as this user and group by default
users.extraGroups.qemu-libvirtd.gid = config.ids.gids.qemu-libvirtd;
users.extraUsers.qemu-libvirtd = {
uid = config.ids.uids.qemu-libvirtd;
isNormalUser = false;
group = "qemu-libvirtd";
};
security.wrappers.qemu-bridge-helper = {
setuid = true;
owner = "root";
group = "root";
source = "${cfg.qemu.package}/libexec/qemu-bridge-helper";
};
programs.ssh.extraConfig = mkIf cfg.sshProxy ''
Include ${cfg.package}/etc/ssh/ssh_config.d/30-libvirt-ssh-proxy.conf
'';
systemd.packages = [ cfg.package ];
systemd.services.libvirtd-config = {
description = "Libvirt Virtual Machine Management Daemon - configuration";
script = ''
# Copy default libvirt network config .xml files to /var/lib
# Files modified by the user will not be overwritten
for i in $(cd ${cfg.package}/var/lib && echo \
libvirt/qemu/networks/*.xml \
libvirt/nwfilter/*.xml );
do
# Intended behavior
# shellcheck disable=SC2174
mkdir -p "/var/lib/$(dirname "$i")" -m 755
if [ ! -e "/var/lib/$i" ]; then
cp -pd "${cfg.package}/var/lib/$i" "/var/lib/$i"
fi
done
# Copy generated qemu config to libvirt directory
cp -f ${qemuConfigFile} /var/lib/${dirName}/qemu.conf
# Copy generated network config to libvirt directory
cp -f ${networkConfigFile} /var/lib/${dirName}/network.conf
# stable (not GC'able as in /nix/store) paths for using in <emulator> section of xml configs
for emulator in ${cfg.package}/libexec/libvirt_lxc ${cfg.qemu.package}/bin/qemu-kvm ${cfg.qemu.package}/bin/qemu-system-*; do
ln -s --force "$emulator" /run/${dirName}/nix-emulators/
done
ln -s --force ${cfg.qemu.package}/bin/qemu-pr-helper /run/${dirName}/nix-helpers/
# Symlink to OVMF firmware code and variable template images distributed with QEMU
cp -sfv $(
${pkgs.jq}/bin/jq -rs \
'[.[] | .mapping.executable.filename, .mapping."nvram-template".filename] | unique | .[]' \
${cfg.qemu.package}/share/qemu/firmware/* \
) /run/${dirName}/nix-ovmf
# Symlink hooks to /var/lib/libvirt
${concatStringsSep "\n" (
map (driver: ''
mkdir -p /var/lib/${dirName}/hooks/${driver}.d
rm -rf /var/lib/${dirName}/hooks/${driver}.d/*
${concatStringsSep "\n" (
mapAttrsToList (
name: value: "ln -s --force ${value} /var/lib/${dirName}/hooks/${driver}.d/${name}"
) cfg.hooks.${driver}
)}
'') (attrNames cfg.hooks)
)}
'';
serviceConfig = {
Type = "oneshot";
RuntimeDirectoryPreserve = "yes";
LogsDirectory = subDirs [ "qemu" ];
RuntimeDirectory = subDirs [
"nix-emulators"
"nix-helpers"
"nix-ovmf"
];
StateDirectory = subDirs [ "dnsmasq" ];
};
};
systemd.services.libvirtd = {
wantedBy = [ "multi-user.target" ];
requires = [ "libvirtd-config.service" ];
after = [ "libvirtd-config.service" ] ++ optional vswitch.enable "ovs-vswitchd.service";
environment.LIBVIRTD_ARGS = escapeShellArgs (
[
"--config"
configFile
"--timeout"
"120" # from ${libvirt}/var/lib/sysconfig/libvirtd
]
++ cfg.extraOptions
);
path = [
cfg.qemu.package
pkgs.netcat
] # libvirtd requires qemu-img to manage disk images
++ optional vswitch.enable vswitch.package
++ optional cfg.qemu.swtpm.enable cfg.qemu.swtpm.package;
serviceConfig = {
Type = "notify";
KillMode = "process"; # when stopping, leave the VMs alone
Restart = "no";
OOMScoreAdjust = "-999";
};
restartIfChanged = false;
};
systemd.services.virtchd = {
path = [ pkgs.cloud-hypervisor ];
};
systemd.services.libvirt-guests = {
wantedBy = [ "multi-user.target" ];
requires = [ "libvirtd.service" ];
after = [ "libvirtd.service" ];
path = with pkgs; [
coreutils
gawk
cfg.package
];
restartIfChanged = false;
environment.ON_BOOT = "${cfg.onBoot}";
environment.ON_SHUTDOWN = "${cfg.onShutdown}";
environment.PARALLEL_SHUTDOWN = "${toString cfg.parallelShutdown}";
environment.SHUTDOWN_TIMEOUT = "${toString cfg.shutdownTimeout}";
environment.START_DELAY = "${toString cfg.startDelay}";
};
systemd.sockets.virtlogd = {
description = "Virtual machine log manager socket";
wantedBy = [ "sockets.target" ];
listenStreams = [ "/run/${dirName}/virtlogd-sock" ];
};
systemd.services.virtlogd = {
description = "Virtual machine log manager";
serviceConfig.ExecStart = "@${cfg.package}/sbin/virtlogd virtlogd";
restartIfChanged = false;
};
systemd.sockets.virtlockd = {
description = "Virtual machine lock manager socket";
wantedBy = [ "sockets.target" ];
listenStreams = [ "/run/${dirName}/virtlockd-sock" ];
};
systemd.services.virtlockd = {
description = "Virtual machine lock manager";
serviceConfig.ExecStart = "@${cfg.package}/sbin/virtlockd virtlockd";
restartIfChanged = false;
};
# https://libvirt.org/daemons.html#monolithic-systemd-integration
systemd.sockets.libvirtd.wantedBy = [ "sockets.target" ];
systemd.tmpfiles.rules =
let
vhostUserCollection = pkgs.buildEnv {
name = "vhost-user";
paths = cfg.qemu.vhostUserPackages;
pathsToLink = [ "/share/qemu/vhost-user" ];
};
in
[
"L+ /var/lib/qemu/vhost-user - - - - ${vhostUserCollection}/share/qemu/vhost-user"
"L+ /var/lib/qemu/firmware - - - - ${qemuOvmfMetadata}"
];
security.polkit = {
enable = true;
extraConfig = ''
polkit.addRule(function(action, subject) {
if (action.id == "org.libvirt.unix.manage" &&
subject.isInGroup("libvirtd")) {
return polkit.Result.YES;
}
});
'';
};
system.nssModules = optional (cfg.nss.enable or cfg.nss.enableGuest) cfg.package;
system.nssDatabases.hosts = mkMerge [
# ensure that the NSS modules come between mymachines (which is 400) and resolve (which is 501)
(mkIf cfg.nss.enable (mkOrder 430 [ "libvirt" ]))
(mkIf cfg.nss.enableGuest (mkOrder 432 [ "libvirt_guest" ]))
];
};
}

View File

@@ -0,0 +1,79 @@
{
config,
lib,
pkgs,
...
}:
with lib;
{
imports = [ ../profiles/qemu-guest.nix ];
services.openssh = {
enable = true;
settings.PermitRootLogin = "prohibit-password";
settings.PasswordAuthentication = mkDefault false;
};
networking = {
usePredictableInterfaceNames = false;
useDHCP = false;
interfaces.eth0 = {
useDHCP = true;
# Linode expects IPv6 privacy extensions to be disabled, so disable them
# See: https://www.linode.com/docs/guides/manual-network-configuration/#static-vs-dynamic-addressing
tempAddress = "disabled";
};
};
# Install diagnostic tools for Linode support
environment.systemPackages = with pkgs; [
inetutils
mtr
sysstat
];
fileSystems."/" = {
fsType = "ext4";
device = "/dev/sda";
autoResize = true;
};
swapDevices = mkDefault [ { device = "/dev/sdb"; } ];
# Enable LISH and Linode Booting w/ GRUB
boot = {
# Add Required Kernel Modules
# NOTE: These are not documented in the install guide
initrd.availableKernelModules = [
"virtio_pci"
"virtio_scsi"
"ahci"
"sd_mod"
];
# Set Up LISH Serial Connection
kernelParams = [ "console=ttyS0,19200n8" ];
kernelModules = [ "virtio_net" ];
loader = {
# Increase Timeout to Allow LISH Connection
# NOTE: The image generator tries to set a timeout of 0, so we must force
timeout = lib.mkForce 10;
grub = {
enable = true;
forceInstall = true;
device = "nodev";
# Allow serial connection for GRUB to be able to use LISH
extraConfig = ''
serial --speed=19200 --unit=0 --word=8 --parity=no --stop=1;
terminal_input serial;
terminal_output serial
'';
};
};
};
}

View File

@@ -0,0 +1,83 @@
{
config,
lib,
pkgs,
...
}:
with lib;
let
cfg = config.virtualisation.linodeImage;
defaultConfigFile = pkgs.writeText "configuration.nix" ''
_: {
imports = [
<nixpkgs/nixos/modules/virtualisation/linode-image.nix>
];
}
'';
in
{
imports = [
./linode-config.nix
./disk-size-option.nix
../image/file-options.nix
(lib.mkRenamedOptionModuleWith {
sinceRelease = 2411;
from = [
"virtualisation"
"linodeImage"
"diskSize"
];
to = [
"virtualisation"
"diskSize"
];
})
];
options = {
virtualisation.linodeImage.configFile = mkOption {
type = with types; nullOr str;
default = null;
description = ''
A path to a configuration file which will be placed at `/etc/nixos/configuration.nix`
and be used when switching to a new configuration.
If set to `null`, a default configuration is used, where the only import is
`<nixpkgs/nixos/modules/virtualisation/linode-image.nix>`
'';
};
virtualisation.linodeImage.compressionLevel = mkOption {
type = types.ints.between 1 9;
default = 6;
description = ''
GZIP compression level of the resulting disk image (1-9).
'';
};
};
config = {
system.nixos.tags = [ "linode" ];
image.extension = "img.gz";
system.build.image = config.system.build.linodeImage;
system.build.linodeImage = import ../../lib/make-disk-image.nix {
name = "linode-image";
baseName = config.image.baseName;
# NOTE: Linode specifically requires images to be `gzip`-ed prior to upload
# See: https://www.linode.com/docs/products/tools/images/guides/upload-an-image/#requirements-and-considerations
postVM = ''
${pkgs.gzip}/bin/gzip -${toString cfg.compressionLevel} -c -- $diskImage > \
$out/${config.image.fileName}
rm $diskImage
'';
format = "raw";
partitionTableType = "none";
configFile = if cfg.configFile == null then defaultConfigFile else cfg.configFile;
inherit (config.virtualisation) diskSize;
inherit config lib pkgs;
};
};
meta.maintainers = with maintainers; [ cyntheticfox ];
}

View File

@@ -0,0 +1,126 @@
{
lib,
config,
pkgs,
...
}:
{
meta = {
maintainers = lib.teams.lxc.members;
};
imports = [
./lxc-instance-common.nix
(lib.mkRemovedOptionModule [
"virtualisation"
"lxc"
"nestedContainer"
] "")
(lib.mkRemovedOptionModule [
"virtualisation"
"lxc"
"privilegedContainer"
] "")
];
options = { };
config =
{
boot.isContainer = true;
boot.postBootCommands = ''
# After booting, register the contents of the Nix store in the Nix
# database.
if [ -f /nix-path-registration ]; then
${config.nix.package.out}/bin/nix-store --load-db < /nix-path-registration &&
rm /nix-path-registration
fi
# nixos-rebuild also requires a "system" profile
${config.nix.package.out}/bin/nix-env -p /nix/var/nix/profiles/system --set /run/current-system
'';
# supplement 99-ethernet-default-dhcp which excludes veth
systemd.network = lib.mkIf config.networking.useDHCP {
networks."99-lxc-veth-default-dhcp" = {
matchConfig = {
Type = "ether";
Kind = "veth";
Name = [
"en*"
"eth*"
];
};
DHCP = "yes";
networkConfig.IPv6PrivacyExtensions = "kernel";
};
};
system.nixos.tags = lib.mkOverride 99 [ "lxc" ];
image.extension = "tar.xz";
image.filePath = "tarball/${config.image.fileName}";
system.build.image = lib.mkOverride 99 config.system.build.tarball;
system.build.tarball = pkgs.callPackage ../../lib/make-system-tarball.nix {
fileName = config.image.baseName;
extraArgs = "--owner=0";
storeContents = [
{
object = config.system.build.toplevel;
symlink = "none";
}
];
contents = [
{
source = config.system.build.toplevel + "/init";
target = "/sbin/init";
}
# Technically this is not required for lxc, but having also make this configuration work with systemd-nspawn.
# Nixos will setup the same symlink after start.
{
source = config.system.build.toplevel + "/etc/os-release";
target = "/etc/os-release";
}
];
extraCommands = "mkdir -p proc sys dev";
};
system.build.squashfs = pkgs.callPackage ../../lib/make-squashfs.nix {
fileName = "nixos-lxc-image-${pkgs.stdenv.hostPlatform.system}";
hydraBuildProduct = true;
noStrip = true; # keep directory structure
comp = "zstd -Xcompression-level 6";
storeContents = [ config.system.build.toplevel ];
pseudoFiles = [
"/sbin d 0755 0 0"
"/sbin/init s 0555 0 0 ${config.system.build.toplevel}/init"
"/dev d 0755 0 0"
"/proc d 0555 0 0"
"/sys d 0555 0 0"
];
};
system.build.installBootLoader = pkgs.writeScript "install-lxc-sbin-init.sh" ''
#!${pkgs.runtimeShell}
${pkgs.coreutils}/bin/ln -fs "$1/init" /sbin/init
'';
# networkd depends on this, but systemd module disables this for containers
systemd.additionalUpstreamSystemUnits = [ "systemd-udev-trigger.service" ];
systemd.packages = [ pkgs.distrobuilder.generator ];
system.activationScripts.installInitScript = lib.mkForce ''
ln -fs $systemConfig/init /sbin/init
'';
};
}

View File

@@ -0,0 +1,141 @@
{
lib,
config,
pkgs,
...
}:
let
templateSubmodule =
{ ... }:
{
options = {
enable = lib.mkEnableOption "this template";
target = lib.mkOption {
description = "Path in the container";
type = lib.types.path;
};
template = lib.mkOption {
description = ".tpl file for rendering the target";
type = lib.types.path;
};
when = lib.mkOption {
description = "Events which trigger a rewrite (create, copy)";
type = lib.types.listOf (lib.types.str);
};
properties = lib.mkOption {
description = "Additional properties";
type = lib.types.attrs;
default = { };
};
};
};
toYAML = name: data: pkgs.writeText name (lib.generators.toYAML { } data);
cfg = config.virtualisation.lxc;
templates =
if cfg.templates != { } then
let
list = lib.mapAttrsToList (name: value: { inherit name; } // value) (
lib.filterAttrs (name: value: value.enable) cfg.templates
);
in
{
files = map (tpl: {
source = tpl.template;
target = "/templates/${tpl.name}.tpl";
}) list;
properties = lib.listToAttrs (
map (
tpl:
lib.nameValuePair tpl.target {
when = tpl.when;
template = "${tpl.name}.tpl";
properties = tpl.properties;
}
) list
);
}
else
{
files = [ ];
properties = { };
};
in
{
imports = [
../image/file-options.nix
];
meta = {
maintainers = lib.teams.lxc.members;
};
options = {
virtualisation.lxc = {
templates = lib.mkOption {
description = "Templates for LXC images";
type = lib.types.attrsOf (lib.types.submodule templateSubmodule);
default = { };
example = lib.literalExpression ''
{
# create /etc/hostname on container creation
"hostname" = {
enable = true;
target = "/etc/hostname";
template = builtins.writeFile "hostname.tpl" "{{ container.name }}";
when = [ "create" ];
};
# create /etc/nixos/hostname.nix with a configuration for keeping the hostname applied
"hostname-nix" = {
enable = true;
target = "/etc/nixos/hostname.nix";
template = builtins.writeFile "hostname-nix.tpl" "{ ... }: { networking.hostName = "{{ container.name }}"; }";
# copy keeps the file updated when the container is changed
when = [ "create" "copy" ];
};
# copy allow the user to specify a custom configuration.nix
"configuration-nix" = {
enable = true;
target = "/etc/nixos/configuration.nix";
template = builtins.writeFile "configuration-nix" "{{ config_get(\"user.user-data\", properties.default) }}";
when = [ "create" ];
};
};
'';
};
};
};
config = {
system.nixos.tags = [
"lxc"
"metadata"
];
image.extension = "tar.xz";
image.filePath = "tarball/${config.image.fileName}";
system.build.image = config.system.build.metadata;
system.build.metadata = pkgs.callPackage ../../lib/make-system-tarball.nix {
fileName = config.image.baseName;
contents = [
{
source = toYAML "metadata.yaml" {
architecture = builtins.elemAt (builtins.match "^([a-z0-9_]+).+" (toString pkgs.stdenv.hostPlatform.system)) 0;
creation_date = 1;
properties = {
description = "${config.system.nixos.distroName} ${config.system.nixos.codeName} ${config.system.nixos.label} ${pkgs.stdenv.hostPlatform.system}";
os = "${config.system.nixos.distroId}";
release = "${config.system.nixos.codeName}";
};
templates = templates.properties;
};
target = "/metadata.yaml";
}
]
++ templates.files;
};
};
}

View File

@@ -0,0 +1,33 @@
{ lib, ... }:
{
meta = {
maintainers = lib.teams.lxc.members;
};
imports = [
./lxc-image-metadata.nix
../installer/cd-dvd/channel.nix
../profiles/clone-config.nix
../profiles/minimal.nix
];
# Allow the user to login as root without password.
users.users.root.initialHashedPassword = lib.mkOverride 150 "";
# Some more help text.
services.getty.helpLine = ''
Log in as "root" with an empty password.
'';
# Containers should be light-weight, so start sshd on demand.
services.openssh.enable = lib.mkDefault true;
services.openssh.startWhenNeeded = lib.mkDefault true;
# As this is intended as a standalone image, undo some of the minimal profile stuff
documentation.enable = true;
documentation.nixos.enable = true;
services.logrotate.enable = true;
}

View File

@@ -0,0 +1,120 @@
# LXC Configuration
{
config,
lib,
pkgs,
...
}:
let
cfg = config.virtualisation.lxc;
in
{
meta = {
maintainers = lib.teams.lxc.members;
};
options.virtualisation.lxc = {
enable = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
This enables Linux Containers (LXC), which provides tools
for creating and managing system or application containers
on Linux.
'';
};
unprivilegedContainers = lib.mkEnableOption "support for unprivileged users to launch containers";
systemConfig = lib.mkOption {
type = lib.types.lines;
default = "";
description = ''
This is the system-wide LXC config. See
{manpage}`lxc.system.conf(5)`.
'';
};
package = lib.mkPackageOption pkgs "lxc" { };
defaultConfig = lib.mkOption {
type = lib.types.lines;
default = "";
description = ''
Default config (default.conf) for new containers, i.e. for
network config. See {manpage}`lxc.container.conf(5)`.
'';
};
usernetConfig = lib.mkOption {
type = lib.types.lines;
default = "";
description = ''
This is the config file for managing unprivileged user network
administration access in LXC. See {manpage}`lxc-usernet(5)`.
'';
};
bridgeConfig = lib.mkOption {
type = lib.types.lines;
default = "";
description = ''
This is the config file for override lxc-net bridge default settings.
'';
};
};
###### implementation
config = lib.mkIf cfg.enable {
environment.systemPackages = [ cfg.package ];
environment.etc."lxc/lxc.conf".text = cfg.systemConfig;
environment.etc."lxc/lxc-usernet".text = cfg.usernetConfig;
environment.etc."lxc/default.conf".text = cfg.defaultConfig;
environment.etc."lxc/lxc-net".text = cfg.bridgeConfig;
environment.pathsToLink = [ "/share/lxc" ];
systemd.tmpfiles.rules = [ "d /var/lib/lxc/rootfs 0755 root root -" ];
security.apparmor.packages = [ cfg.package ];
security.apparmor.policies = {
"bin.lxc-start".profile = ''
include ${cfg.package}/etc/apparmor.d/usr.bin.lxc-start
'';
"lxc-containers".profile = ''
include ${cfg.package}/etc/apparmor.d/lxc-containers
'';
};
# We don't need the `lxc-user` group, unless the unprivileged containers are enabled.
users.groups = lib.mkIf cfg.unprivilegedContainers { lxc-user = { }; };
# `lxc-user-nic` needs suid to attach to bridge for unpriv containers.
security.wrappers = lib.mkIf cfg.unprivilegedContainers {
lxcUserNet = {
source = "${pkgs.lxc}/libexec/lxc/lxc-user-nic";
setuid = true;
owner = "root";
group = "lxc-user";
program = "lxc-user-nic";
permissions = "u+rx,g+x,o-rx";
};
};
# Add lxc-net service if unpriv mode is enabled.
systemd.packages = lib.mkIf cfg.unprivilegedContainers [ pkgs.lxc ];
systemd.services = lib.mkIf cfg.unprivilegedContainers {
lxc-net = {
enable = true;
wantedBy = [ "multi-user.target" ];
path = [
pkgs.iproute2
pkgs.iptables
pkgs.getent
pkgs.dnsmasq
];
};
};
};
}

View File

@@ -0,0 +1,50 @@
# LXC Configuration
{
config,
lib,
pkgs,
...
}:
let
cfg = config.virtualisation.lxc.lxcfs;
in
{
meta = {
maintainers = lib.teams.lxc.members;
};
###### interface
options.virtualisation.lxc.lxcfs = {
enable = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
This enables LXCFS, a FUSE filesystem for LXC.
To use lxcfs in include the following configuration in your
container configuration:
```
virtualisation.lxc.defaultConfig = "lxc.include = ''${pkgs.lxcfs}/share/lxc/config/common.conf.d/00-lxcfs.conf";
```
'';
};
};
###### implementation
config = lib.mkIf cfg.enable {
systemd.services.lxcfs = {
description = "FUSE filesystem for LXC";
wantedBy = [ "multi-user.target" ];
before = [ "lxc.service" ];
restartIfChanged = false;
serviceConfig = {
ExecStartPre = "${pkgs.coreutils}/bin/mkdir -p /var/lib/lxcfs";
ExecStart = "${pkgs.lxcfs}/bin/lxcfs /var/lib/lxcfs";
ExecStopPost = "-${pkgs.fuse}/bin/fusermount -u /var/lib/lxcfs";
KillMode = "process";
Restart = "on-failure";
};
};
};
}

View File

@@ -0,0 +1,66 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.virtualisation.multipass;
in
{
options = {
virtualisation.multipass = {
enable = lib.mkEnableOption "Multipass, a simple manager for virtualised Ubuntu instances";
logLevel = lib.mkOption {
type = lib.types.enum [
"error"
"warning"
"info"
"debug"
"trace"
];
default = "debug";
description = ''
The logging verbosity of the multipassd binary.
'';
};
package = lib.mkPackageOption pkgs "multipass" { };
};
};
config = lib.mkIf cfg.enable {
environment.systemPackages = [ cfg.package ];
systemd.services.multipass = {
description = "Multipass orchestrates virtual Ubuntu instances.";
wantedBy = [ "multi-user.target" ];
wants = [ "network-online.target" ];
after = [ "network-online.target" ];
environment = {
"XDG_DATA_HOME" = "/var/lib/multipass/data";
"XDG_CACHE_HOME" = "/var/lib/multipass/cache";
"XDG_CONFIG_HOME" = "/var/lib/multipass/config";
};
serviceConfig = {
ExecStart = "${cfg.package}/bin/multipassd --logger platform --verbosity ${cfg.logLevel}";
SyslogIdentifier = "multipassd";
Restart = "on-failure";
TimeoutStopSec = 300;
Type = "simple";
WorkingDirectory = "/var/lib/multipass";
StateDirectory = "multipass";
StateDirectoryMode = "0750";
CacheDirectory = "multipass";
CacheDirectoryMode = "0750";
};
};
};
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,65 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.oci;
in
{
imports = [ ../profiles/qemu-guest.nix ];
# Taken from /proc/cmdline of Ubuntu 20.04.2 LTS on OCI
boot.kernelParams = [
"nvme.shutdown_timeout=10"
"nvme_core.shutdown_timeout=10"
"libiscsi.debug_libiscsi_eh=1"
"crash_kexec_post_notifiers"
# VNC console
"console=tty1"
# x86_64-linux
"console=ttyS0"
# aarch64-linux
"console=ttyAMA0,115200"
];
boot.growPartition = true;
fileSystems."/" = {
device = "/dev/disk/by-label/nixos";
fsType = "ext4";
autoResize = true;
};
fileSystems."/boot" = lib.mkIf cfg.efi {
device = "/dev/disk/by-label/ESP";
fsType = "vfat";
};
boot.loader.efi.canTouchEfiVariables = false;
boot.loader.grub = {
device = if cfg.efi then "nodev" else "/dev/sda";
splashImage = null;
extraConfig = ''
serial --unit=0 --speed=115200 --word=8 --parity=no --stop=1
terminal_input --append serial
terminal_output --append serial
'';
efiInstallAsRemovable = cfg.efi;
efiSupport = cfg.efi;
};
# https://docs.oracle.com/en-us/iaas/Content/Compute/Tasks/configuringntpservice.htm#Configuring_the_Oracle_Cloud_Infrastructure_NTP_Service_for_an_Instance
networking.timeServers = [ "169.254.169.254" ];
services.openssh.enable = true;
# Otherwise the instance may not have a working network-online.target,
# making the fetch-ssh-keys.service fail
networking.useNetworkd = lib.mkDefault true;
}

View File

@@ -0,0 +1,12 @@
{ modulesPath, ... }:
{
# To build the configuration or use nix-env, you need to run
# either nixos-rebuild --upgrade or nix-channel --update
# to fetch the nixos channel.
# This configures everything but bootstrap services,
# which only need to be run once and have already finished
# if you are able to see this comment.
imports = [ "${modulesPath}/virtualisation/oci-common.nix" ];
}

View File

@@ -0,0 +1,652 @@
{
config,
options,
lib,
pkgs,
...
}:
with lib;
let
cfg = config.virtualisation.oci-containers;
proxy_env = config.networking.proxy.envVars;
defaultBackend = options.virtualisation.oci-containers.backend.default;
containerOptions =
{ name, ... }:
{
config = {
podman = mkIf (cfg.backend == "podman") { };
};
options = {
image = mkOption {
type = with types; str;
description = "OCI image to run.";
example = "library/hello-world";
};
imageFile = mkOption {
type = with types; nullOr package;
default = null;
description = ''
Path to an image file to load before running the image. This can
be used to bypass pulling the image from the registry.
The `image` attribute must match the name and
tag of the image contained in this file, as they will be used to
run the container with that image. If they do not match, the
image will be pulled from the registry as usual.
'';
example = literalExpression "pkgs.dockerTools.buildImage {...};";
};
imageStream = mkOption {
type = with types; nullOr package;
default = null;
description = ''
Path to a script that streams the desired image on standard output.
This option is mainly intended for use with
`pkgs.dockerTools.streamLayeredImage` so that the intermediate
image archive does not need to be stored in the Nix store. For
larger images this optimization can significantly reduce Nix store
churn compared to using the `imageFile` option, because you don't
have to store a new copy of the image archive in the Nix store
every time you change the image. Instead, if you stream the image
then you only need to build and store the layers that differ from
the previous image.
'';
example = literalExpression "pkgs.dockerTools.streamLayeredImage {...};";
};
serviceName = mkOption {
type = types.str;
default = "${cfg.backend}-${name}";
defaultText = "<backend>-<name>";
description = "Systemd service name that manages the container";
};
login = {
username = mkOption {
type = with types; nullOr str;
default = null;
description = "Username for login.";
};
passwordFile = mkOption {
type = with types; nullOr str;
default = null;
description = "Path to file containing password.";
example = "/etc/nixos/dockerhub-password.txt";
};
registry = mkOption {
type = with types; nullOr str;
default = null;
description = "Registry where to login to.";
example = "https://docker.pkg.github.com";
};
};
cmd = mkOption {
type = with types; listOf str;
default = [ ];
description = "Commandline arguments to pass to the image's entrypoint.";
example = [ "--port=9000" ];
};
labels = mkOption {
type = with types; attrsOf str;
default = { };
description = "Labels to attach to the container at runtime.";
example = {
"traefik.https.routers.example.rule" = "Host(`example.container`)";
};
};
entrypoint = mkOption {
type = with types; nullOr str;
description = "Override the default entrypoint of the image.";
default = null;
example = "/bin/my-app";
};
environment = mkOption {
type = with types; attrsOf str;
default = { };
description = "Environment variables to set for this container.";
example = {
DATABASE_HOST = "db.example.com";
DATABASE_PORT = "3306";
};
};
environmentFiles = mkOption {
type = with types; listOf path;
default = [ ];
description = "Environment files for this container.";
example = [
/path/to/.env
/path/to/.env.secret
];
};
log-driver = mkOption {
type = types.str;
default = "journald";
description = ''
Logging driver for the container. The default of
`"journald"` means that the container's logs will be
handled as part of the systemd unit.
For more details and a full list of logging drivers, refer to respective backends documentation.
For Docker:
[Docker engine documentation](https://docs.docker.com/engine/logging/configure/)
For Podman:
Refer to the {manpage}`docker-run(1)` man page.
'';
};
ports = mkOption {
type = with types; listOf str;
default = [ ];
description = ''
Network ports to publish from the container to the outer host.
Valid formats:
- `<ip>:<hostPort>:<containerPort>`
- `<ip>::<containerPort>`
- `<hostPort>:<containerPort>`
- `<containerPort>`
Both `hostPort` and `containerPort` can be specified as a range of
ports. When specifying ranges for both, the number of container
ports in the range must match the number of host ports in the
range. Example: `1234-1236:1234-1236/tcp`
When specifying a range for `hostPort` only, the `containerPort`
must *not* be a range. In this case, the container port is published
somewhere within the specified `hostPort` range.
Example: `1234-1236:1234/tcp`
Publishing a port bypasses the NixOS firewall. If the port is not
supposed to be shared on the network, make sure to publish the
port to localhost.
Example: `127.0.0.1:1234:1234`
Refer to the
[Docker engine documentation](https://docs.docker.com/engine/network/#published-ports) for full details.
'';
example = [
"127.0.0.1:8080:9000"
];
};
user = mkOption {
type = with types; nullOr str;
default = null;
description = ''
Override the username or UID (and optionally groupname or GID) used
in the container.
'';
example = "nobody:nogroup";
};
volumes = mkOption {
type = with types; listOf str;
default = [ ];
description = ''
List of volumes to attach to this container.
Note that this is a list of `"src:dst"` strings to
allow for `src` to refer to `/nix/store` paths, which
would be difficult with an attribute set. There are
also a variety of mount options available as a third
field; please refer to the
[docker engine documentation](https://docs.docker.com/engine/storage/volumes/) for details.
'';
example = [
"volume_name:/path/inside/container"
"/path/on/host:/path/inside/container"
];
};
workdir = mkOption {
type = with types; nullOr str;
default = null;
description = "Override the default working directory for the container.";
example = "/var/lib/hello_world";
};
dependsOn = mkOption {
type = with types; listOf str;
default = [ ];
description = ''
Define which other containers this one depends on. They will be added to both After and Requires for the unit.
Use the same name as the attribute under `virtualisation.oci-containers.containers`.
'';
example = literalExpression ''
virtualisation.oci-containers.containers = {
node1 = {};
node2.dependsOn = [ "node1" ];
};
'';
};
hostname = mkOption {
type = with types; nullOr str;
default = null;
description = "The hostname of the container.";
example = "hello-world";
};
preRunExtraOptions = mkOption {
type = with types; listOf str;
default = [ ];
description = "Extra options for {command}`${defaultBackend}` that go before the `run` argument.";
example = [
"--runtime"
"runsc"
];
};
extraOptions = mkOption {
type = with types; listOf str;
default = [ ];
description = "Extra options for {command}`${defaultBackend} run`.";
example = [ "--network=host" ];
};
autoStart = mkOption {
type = with types; bool;
default = true;
description = ''
When enabled, the container is automatically started on boot.
If this option is set to false, the container has to be started on-demand via its service.
'';
};
podman = mkOption {
type = types.nullOr (
types.submodule {
options = {
sdnotify = mkOption {
default = "conmon";
type = types.enum [
"conmon"
"healthy"
"container"
];
description = ''
Determines how `podman` should notify systemd that the unit is ready. There are
[three options](https://docs.podman.io/en/latest/markdown/podman-run.1.html#sdnotify-container-conmon-healthy-ignore):
* `conmon`: marks the unit as ready when the container has started.
* `healthy`: marks the unit as ready when the [container's healthcheck](https://docs.podman.io/en/stable/markdown/podman-healthcheck-run.1.html) passes.
* `container`: `NOTIFY_SOCKET` is passed into the container and the process inside the container needs to indicate on its own that it's ready.
'';
};
user = mkOption {
default = "root";
type = types.str;
description = ''
The user under which the container should run.
'';
};
};
}
);
default = null;
description = ''
Podman-specific settings in OCI containers. These must be null when using
the `docker` backend.
'';
};
pull = mkOption {
type =
with types;
enum [
"always"
"missing"
"never"
"newer"
];
default = "missing";
description = ''
Image pull policy for the container. Must be one of: always, missing, never, newer
'';
};
capabilities = mkOption {
type = with types; lazyAttrsOf (nullOr bool);
default = { };
description = ''
Capabilities to configure for the container.
When set to true, capability is added to the container.
When set to false, capability is dropped from the container.
When null, default runtime settings apply.
'';
example = {
SYS_ADMIN = true;
SYS_WRITE = false;
};
};
devices = mkOption {
type = with types; listOf str;
default = [ ];
description = ''
List of devices to attach to this container.
'';
example = [
"/dev/dri:/dev/dri"
];
};
privileged = mkOption {
type = with types; bool;
default = false;
description = ''
Give extended privileges to the container
'';
};
autoRemoveOnStop = mkOption {
type = types.bool;
default = true;
description = ''
Automatically remove the container when it is stopped or killed
'';
};
networks = mkOption {
type = with types; listOf str;
default = [ ];
description = ''
Networks to attach the container to
'';
};
};
};
isValidLogin =
login: login.username != null && login.passwordFile != null && login.registry != null;
mkService =
name: container:
let
dependsOn = lib.attrsets.mapAttrsToList (k: v: "${v.serviceName}.service") (
lib.attrsets.getAttrs container.dependsOn cfg.containers
);
escapedName = escapeShellArg name;
preStartScript = pkgs.writeShellApplication {
name = "pre-start";
runtimeInputs = [ ];
text = ''
${cfg.backend} rm -f ${name} || true
${optionalString (isValidLogin container.login) ''
# try logging in, if it fails, check if image exists locally
${cfg.backend} login \
${container.login.registry} \
--username ${escapeShellArg container.login.username} \
--password-stdin < ${container.login.passwordFile} \
|| ${cfg.backend} image inspect ${container.image} >/dev/null \
|| { echo "image doesn't exist locally and login failed" >&2 ; exit 1; }
''}
${optionalString (container.imageFile != null) ''
${cfg.backend} load -i ${container.imageFile}
''}
${optionalString (container.imageStream != null) ''
${container.imageStream} | ${cfg.backend} load
''}
${optionalString (cfg.backend == "podman") ''
rm -f /run/${escapedName}/ctr-id
''}
'';
};
effectiveUser = container.podman.user or "root";
inherit (config.users.users.${effectiveUser}) uid;
dependOnLingerService =
cfg.backend == "podman" && effectiveUser != "root" && config.users.users.${effectiveUser}.linger;
in
{
wantedBy = [ ] ++ optional (container.autoStart) "multi-user.target";
wants =
lib.optional (container.imageFile == null && container.imageStream == null) "network-online.target"
++ lib.optionals dependOnLingerService [ "linger-users.service" ];
after =
lib.optionals (cfg.backend == "docker") [
"docker.service"
"docker.socket"
]
# if imageFile or imageStream is not set, the service needs the network to download the image from the registry
++ lib.optionals (container.imageFile == null && container.imageStream == null) [
"network-online.target"
]
++ dependsOn
++ lib.optionals dependOnLingerService [ "linger-users.service" ]
++ lib.optionals (effectiveUser != "root" && container.podman.sdnotify == "healthy") [
"user@${toString uid}.service"
];
requires =
dependsOn
++ lib.optionals (effectiveUser != "root" && container.podman.sdnotify == "healthy") [
"user@${toString uid}.service"
];
environment = lib.mkMerge [
proxy_env
(mkIf (cfg.backend == "podman" && container.podman.user != "root") {
HOME = config.users.users.${container.podman.user}.home;
})
];
path =
if cfg.backend == "docker" then
[ config.virtualisation.docker.package ]
else if cfg.backend == "podman" then
[ config.virtualisation.podman.package ]
else
throw "Unhandled backend: ${cfg.backend}";
script = concatStringsSep " \\\n " (
[
"exec ${cfg.backend} "
]
++ map escapeShellArg container.preRunExtraOptions
++ [
"run"
"--name=${escapedName}"
"--log-driver=${container.log-driver}"
]
++ optional (container.entrypoint != null) "--entrypoint=${escapeShellArg container.entrypoint}"
++ optional (container.hostname != null) "--hostname=${escapeShellArg container.hostname}"
++ lib.optionals (cfg.backend == "podman") [
"--cidfile=/run/${escapedName}/ctr-id"
"--cgroups=enabled"
"--sdnotify=${container.podman.sdnotify}"
"-d"
"--replace"
]
++ (mapAttrsToList (k: v: "-e ${escapeShellArg k}=${escapeShellArg v}") container.environment)
++ map (f: "--env-file ${escapeShellArg f}") container.environmentFiles
++ map (p: "-p ${escapeShellArg p}") container.ports
++ optional (container.user != null) "-u ${escapeShellArg container.user}"
++ map (v: "-v ${escapeShellArg v}") container.volumes
++ (mapAttrsToList (k: v: "-l ${escapeShellArg k}=${escapeShellArg v}") container.labels)
++ optional (container.workdir != null) "-w ${escapeShellArg container.workdir}"
++ optional (container.privileged) "--privileged"
++ optional (container.autoRemoveOnStop) "--rm"
++ mapAttrsToList (k: _: "--cap-add=${escapeShellArg k}") (
filterAttrs (_: v: v == true) container.capabilities
)
++ mapAttrsToList (k: _: "--cap-drop=${escapeShellArg k}") (
filterAttrs (_: v: v == false) container.capabilities
)
++ map (d: "--device=${escapeShellArg d}") container.devices
++ map (n: "--network=${escapeShellArg n}") (lib.lists.unique container.networks)
++ [ "--pull ${escapeShellArg container.pull}" ]
++ map escapeShellArg container.extraOptions
++ [ container.image ]
++ map escapeShellArg container.cmd
);
preStop =
if cfg.backend == "podman" then
"podman stop --ignore --cidfile=/run/${escapedName}/ctr-id"
else
"${cfg.backend} stop ${name} || true";
postStop =
if cfg.backend == "podman" then
"podman rm -f --ignore --cidfile=/run/${escapedName}/ctr-id"
else
"${cfg.backend} rm -f ${name} || true";
unitConfig = mkIf (effectiveUser != "root") {
RequiresMountsFor = "/run/user/${toString uid}/containers";
};
serviceConfig = {
### There is no generalized way of supporting `reload` for docker
### containers. Some containers may respond well to SIGHUP sent to their
### init process, but it is not guaranteed; some apps have other reload
### mechanisms, some don't have a reload signal at all, and some docker
### images just have broken signal handling. The best compromise in this
### case is probably to leave ExecReload undefined, so `systemctl reload`
### will at least result in an error instead of potentially undefined
### behaviour.
###
### Advanced users can still override this part of the unit to implement
### a custom reload handler, since the result of all this is a normal
### systemd service from the perspective of the NixOS module system.
###
# ExecReload = ...;
###
ExecStartPre = [ "${preStartScript}/bin/pre-start" ];
TimeoutStartSec = 0;
TimeoutStopSec = 120;
Restart = "always";
}
// optionalAttrs (cfg.backend == "podman") {
Environment = "PODMAN_SYSTEMD_UNIT=%n";
Type = "notify";
NotifyAccess = "all";
Delegate = mkIf (container.podman.sdnotify == "healthy") true;
User = effectiveUser;
RuntimeDirectory = escapedName;
};
};
in
{
imports = [
(lib.mkChangedOptionModule [ "docker-containers" ] [ "virtualisation" "oci-containers" ] (oldcfg: {
backend = "docker";
containers = lib.mapAttrs (
n: v:
builtins.removeAttrs (
v
// {
extraOptions = v.extraDockerOptions or [ ];
}
) [ "extraDockerOptions" ]
) oldcfg.docker-containers;
}))
];
options.virtualisation.oci-containers = {
backend = mkOption {
type = types.enum [
"podman"
"docker"
];
default = if versionAtLeast config.system.stateVersion "22.05" then "podman" else "docker";
description = "The underlying Docker implementation to use.";
};
containers = mkOption {
default = { };
type = types.attrsOf (types.submodule containerOptions);
description = "OCI (Docker) containers to run as systemd services.";
};
};
config = lib.mkIf (cfg.containers != { }) (
lib.mkMerge [
{
systemd.services = mapAttrs' (n: v: nameValuePair v.serviceName (mkService n v)) cfg.containers;
assertions =
let
toAssertions =
name:
{
imageFile,
imageStream,
podman,
...
}:
[
{
assertion = imageFile == null || imageStream == null;
message = "virtualisation.oci-containers.containers.${name}: You can only define one of imageFile and imageStream";
}
{
assertion = cfg.backend == "docker" -> podman == null;
message = "virtualisation.oci-containers.containers.${name}: Cannot set `podman` option if backend is `docker`.";
}
{
assertion =
cfg.backend == "podman" && podman.sdnotify == "healthy" && podman.user != "root"
-> config.users.users.${podman.user}.uid != null;
message = ''
Rootless container ${name} (with podman and sdnotify=healthy)
requires that its running user ${podman.user} has a statically specified uid.
'';
}
];
in
concatMap (name: toAssertions name cfg.containers.${name}) (lib.attrNames cfg.containers);
warnings = mkIf (cfg.backend == "podman") (
lib.foldlAttrs (
warnings: name:
{ podman, ... }:
let
inherit (config.users.users.${podman.user}) linger;
in
warnings
++ lib.optional (podman.user != "root" && linger && podman.sdnotify == "conmon") ''
Podman container ${name} is configured as rootless (user ${podman.user})
with `--sdnotify=conmon`, but lingering for this user is turned on.
''
++ lib.optional (podman.user != "root" && !linger && podman.sdnotify == "healthy") ''
Podman container ${name} is configured as rootless (user ${podman.user})
with `--sdnotify=healthy`, but lingering for this user is turned off.
''
) [ ] cfg.containers
);
}
(lib.mkIf (cfg.backend == "podman") {
virtualisation.podman.enable = true;
})
(lib.mkIf (cfg.backend == "docker") {
virtualisation.docker.enable = true;
})
]
);
}

View File

@@ -0,0 +1,70 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.oci;
in
{
imports = [
./oci-common.nix
../image/file-options.nix
];
config = {
# Use a priority just below mkOptionDefault (1500) instead of lib.mkDefault
# to avoid breaking existing configs using that.
virtualisation.diskSize = lib.mkOverride 1490 (8 * 1024);
virtualisation.diskSizeAutoSupported = false;
system.nixos.tags = [ "oci" ];
image.extension = "qcow2";
system.build.image = config.system.build.OCIImage;
system.build.OCIImage = import ../../lib/make-disk-image.nix {
inherit config lib pkgs;
inherit (config.virtualisation) diskSize;
name = "oci-image";
baseName = config.image.baseName;
configFile = ./oci-config-user.nix;
format = "qcow2";
partitionTableType = if cfg.efi then "efi" else "legacy";
};
systemd.services.fetch-ssh-keys = {
description = "Fetch authorized_keys for root user";
wantedBy = [ "sshd.service" ];
before = [ "sshd.service" ];
after = [ "network-online.target" ];
wants = [ "network-online.target" ];
path = [
pkgs.coreutils
pkgs.curl
];
script = ''
mkdir -m 0700 -p /root/.ssh
if [ -f /root/.ssh/authorized_keys ]; then
echo "Authorized keys have already been downloaded"
else
echo "Downloading authorized keys from Instance Metadata Service v2"
curl -s -S -L \
-H "Authorization: Bearer Oracle" \
-o /root/.ssh/authorized_keys \
http://169.254.169.254/opc/v2/instance/metadata/ssh_authorized_keys
chmod 600 /root/.ssh/authorized_keys
fi
'';
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
StandardError = "journal+console";
StandardOutput = "journal+console";
};
};
};
}

View File

@@ -0,0 +1,32 @@
{
lib,
...
}:
{
imports = [
./disk-size-option.nix
(lib.mkRenamedOptionModuleWith {
sinceRelease = 2411;
from = [
"oci"
"diskSize"
];
to = [
"virtualisation"
"diskSize"
];
})
];
options = {
oci = {
efi = lib.mkOption {
default = true;
internal = true;
description = ''
Whether the OCI instance is using EFI.
'';
};
};
};
}

View File

@@ -0,0 +1,96 @@
{
config,
pkgs,
lib,
...
}:
# image metadata:
# hw_firmware_type=uefi
let
inherit (lib) mkIf mkDefault;
cfg = config.openstack;
metadataFetcher = import ./openstack-metadata-fetcher.nix {
targetRoot = "/";
wgetExtraOptions = "--retry-connrefused";
};
in
{
imports = [
../profiles/qemu-guest.nix
# Note: While we do use the headless profile, we also explicitly
# turn on the serial console on tty1 below.
# Note that I could not find any documentation indicating tty1 was
# the correct choice. I picked tty1 because that is what one
# particular host was using.
../profiles/headless.nix
# The Openstack Metadata service exposes data on an EC2 API also.
./ec2-data.nix
./amazon-init.nix
];
config = {
fileSystems."/" = mkIf (!cfg.zfs.enable) {
device = "/dev/disk/by-label/nixos";
fsType = "ext4";
autoResize = true;
};
fileSystems."/boot" = mkIf (cfg.efi || cfg.zfs.enable) {
# The ZFS image uses a partition labeled ESP whether or not we're
# booting with EFI.
device = "/dev/disk/by-label/ESP";
fsType = "vfat";
};
boot.growPartition = true;
boot.kernelParams = [ "console=tty1" ];
boot.loader.grub.device = if (!cfg.efi) then "/dev/vda" else "nodev";
boot.loader.grub.efiSupport = cfg.efi;
boot.loader.grub.efiInstallAsRemovable = cfg.efi;
boot.loader.timeout = 1;
boot.loader.grub.extraConfig = ''
serial --unit=1 --speed=115200 --word=8 --parity=no --stop=1
terminal_output console serial
terminal_input console serial
'';
services.zfs.expandOnBoot = mkIf cfg.zfs.enable (lib.mkDefault "all");
boot.zfs.devNodes = mkIf cfg.zfs.enable "/dev/";
# Allow root logins
services.openssh = {
enable = true;
settings.PermitRootLogin = "prohibit-password";
settings.PasswordAuthentication = mkDefault false;
};
# Enable the serial console on tty1
systemd.services."serial-getty@tty1".enable = true;
# Force getting the hostname from Openstack metadata.
networking.hostName = mkDefault "";
systemd.services.openstack-init = {
path = [ pkgs.wget ];
description = "Fetch Metadata on startup";
wantedBy = [ "multi-user.target" ];
before = [
"apply-ec2-data.service"
"amazon-init.service"
];
wants = [ "network-online.target" ];
after = [ "network-online.target" ];
script = metadataFetcher;
restartIfChanged = false;
unitConfig.X-StopOnRemoval = false;
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
};
};
};
}

View File

@@ -0,0 +1,22 @@
{ targetRoot, wgetExtraOptions }:
# OpenStack's metadata service aims to be EC2-compatible. Where
# possible, try to keep the set of fetched metadata in sync with
# ./ec2-metadata-fetcher.nix .
''
metaDir=${targetRoot}etc/ec2-metadata
mkdir -m 0755 -p "$metaDir"
rm -f "$metaDir/*"
echo "getting instance metadata..."
wget_imds() {
wget ${wgetExtraOptions} "$@"
}
wget_imds -O "$metaDir/ami-manifest-path" http://169.254.169.254/1.0/meta-data/ami-manifest-path || true
# When no user-data is provided, the OpenStack metadata server doesn't expose the user-data route.
(umask 077 && wget_imds -O "$metaDir/user-data" http://169.254.169.254/1.0/user-data || rm -f "$metaDir/user-data")
wget_imds -O "$metaDir/hostname" http://169.254.169.254/1.0/meta-data/hostname || true
wget_imds -O "$metaDir/public-keys-0-openssh-key" http://169.254.169.254/1.0/meta-data/public-keys/0/openssh-key || true
''

View File

@@ -0,0 +1,81 @@
{
config,
lib,
pkgs,
...
}:
let
inherit (lib) literalExpression types;
in
{
options = {
openstack = {
zfs = {
enable = lib.mkOption {
default = false;
internal = true;
description = ''
Whether the OpenStack instance uses a ZFS root.
'';
};
datasets = lib.mkOption {
description = ''
Datasets to create under the `tank` and `boot` zpools.
**NOTE:** This option is used only at image creation time, and
does not attempt to declaratively create or manage datasets
on an existing system.
'';
default = { };
type = types.attrsOf (
types.submodule {
options = {
mount = lib.mkOption {
description = "Where to mount this dataset.";
type = types.nullOr types.str;
default = null;
};
properties = lib.mkOption {
description = "Properties to set on this dataset.";
type = types.attrsOf types.str;
default = { };
};
};
}
);
};
};
efi = lib.mkOption {
default = pkgs.stdenv.hostPlatform.isAarch64;
defaultText = literalExpression "pkgs.stdenv.hostPlatform.isAarch64";
internal = true;
description = ''
Whether the instance is using EFI.
'';
};
};
};
config = lib.mkIf config.openstack.zfs.enable {
networking.hostId = lib.mkDefault "00000000";
fileSystems =
let
mountable = lib.filterAttrs (
_: value: ((value.mount or null) != null)
) config.openstack.zfs.datasets;
in
lib.mapAttrs' (
dataset: opts:
lib.nameValuePair opts.mount {
device = dataset;
fsType = "zfs";
}
) mountable;
};
}

View File

@@ -0,0 +1,151 @@
# Systemd services for openvswitch
{
config,
lib,
pkgs,
...
}:
with lib;
let
cfg = config.virtualisation.vswitch;
in
{
options.virtualisation.vswitch = {
enable = mkOption {
type = types.bool;
default = false;
description = ''
Whether to enable Open vSwitch. A configuration daemon (ovs-server)
will be started.
'';
};
resetOnStart = mkOption {
type = types.bool;
default = false;
description = ''
Whether to reset the Open vSwitch configuration database to a default
configuration on every start of the systemd `ovsdb.service`.
'';
};
package = mkPackageOption pkgs "openvswitch" { };
};
config = mkIf cfg.enable (
let
# Where the communication sockets live
runDir = "/run/openvswitch";
# The path to the an initialized version of the database
db = pkgs.stdenv.mkDerivation {
name = "vswitch.db";
dontUnpack = true;
buildPhase = "true";
buildInputs = [
cfg.package
];
installPhase = "mkdir -p $out";
};
in
{
environment.systemPackages = [ cfg.package ];
boot.kernelModules = [
"tun"
"openvswitch"
];
boot.extraModulePackages = [ cfg.package ];
systemd.services.ovsdb = {
description = "Open_vSwitch Database Server";
wantedBy = [ "multi-user.target" ];
after = [ "systemd-udev-settle.service" ];
path = [ cfg.package ];
restartTriggers = [
db
cfg.package
];
# Create the config database
preStart = ''
mkdir -p ${runDir}
mkdir -p /var/db/openvswitch
chmod +w /var/db/openvswitch
${optionalString cfg.resetOnStart "rm -f /var/db/openvswitch/conf.db"}
if [[ ! -e /var/db/openvswitch/conf.db ]]; then
${cfg.package}/bin/ovsdb-tool create \
"/var/db/openvswitch/conf.db" \
"${cfg.package}/share/openvswitch/vswitch.ovsschema"
fi
chmod -R +w /var/db/openvswitch
if ${cfg.package}/bin/ovsdb-tool needs-conversion /var/db/openvswitch/conf.db | grep -q "yes"
then
echo "Performing database upgrade"
${cfg.package}/bin/ovsdb-tool convert /var/db/openvswitch/conf.db
else
echo "Database already up to date"
fi
'';
serviceConfig = {
ExecStart = ''
${cfg.package}/bin/ovsdb-server \
--remote=punix:${runDir}/db.sock \
--private-key=db:Open_vSwitch,SSL,private_key \
--certificate=db:Open_vSwitch,SSL,certificate \
--bootstrap-ca-cert=db:Open_vSwitch,SSL,ca_cert \
--unixctl=ovsdb.ctl.sock \
--pidfile=/run/openvswitch/ovsdb.pid \
--detach \
/var/db/openvswitch/conf.db
'';
Restart = "always";
RestartSec = 3;
PIDFile = "/run/openvswitch/ovsdb.pid";
# Use service type 'forking' to correctly determine when ovsdb-server is ready.
Type = "forking";
};
postStart = ''
${cfg.package}/bin/ovs-vsctl --timeout 3 --retry --no-wait init
'';
};
systemd.services.ovs-vswitchd = {
description = "Open_vSwitch Daemon";
wantedBy = [ "multi-user.target" ];
bindsTo = [ "ovsdb.service" ];
after = [ "ovsdb.service" ];
path = [ cfg.package ];
serviceConfig = {
ExecStart = ''
${cfg.package}/bin/ovs-vswitchd \
--pidfile=/run/openvswitch/ovs-vswitchd.pid \
--detach
'';
PIDFile = "/run/openvswitch/ovs-vswitchd.pid";
# Use service type 'forking' to correctly determine when vswitchd is ready.
Type = "forking";
Restart = "always";
RestartSec = 3;
};
};
}
);
imports = [
(mkRemovedOptionModule [ "virtualisation" "vswitch" "ipsec" ] ''
OpenVSwitch IPSec functionality has been removed, because it depended on racoon,
which was removed from nixpkgs, because it was abanoded upstream.
'')
];
meta.maintainers = with maintainers; [ netixx ];
}

View File

@@ -0,0 +1,84 @@
{
config,
lib,
pkgs,
...
}:
with lib;
let
prl-tools = config.hardware.parallels.package;
in
{
imports = [
(mkRemovedOptionModule [
"hardware"
"parallels"
"autoMountShares"
] "Shares are always automatically mounted since Parallels Desktop 20.")
];
options = {
hardware.parallels = {
enable = mkOption {
type = types.bool;
default = false;
description = ''
This enables Parallels Tools for Linux guests.
'';
};
package = lib.mkPackageOption pkgs "prl-tools" { };
};
};
config = mkIf config.hardware.parallels.enable {
services.udev.packages = [ prl-tools ];
environment.systemPackages = [ prl-tools ];
boot.extraModulePackages = [ prl-tools ];
services.timesyncd.enable = false;
systemd.services.prltoolsd = {
description = "Parallels Tools Service";
wantedBy = [ "multi-user.target" ];
path = [ prl-tools ];
serviceConfig = {
ExecStart = "${prl-tools}/bin/prltoolsd -f";
PIDFile = "/var/run/prltoolsd.pid";
WorkingDirectory = "${prl-tools}/bin";
};
};
systemd.services.prlshprint = {
description = "Parallels Printing Tool";
wantedBy = [ "multi-user.target" ];
bindsTo = [ "cups.service" ];
path = [ prl-tools ];
serviceConfig = {
ExecStart = "${prl-tools}/bin/prlshprint";
WorkingDirectory = "${prl-tools}/bin";
};
};
systemd.user.services.prlcc = {
description = "Parallels Control Center";
wantedBy = [ "graphical-session.target" ];
path = [ prl-tools ];
serviceConfig = {
ExecStart = "${prl-tools}/bin/prlcc";
WorkingDirectory = "${prl-tools}/bin";
};
};
};
meta.maintainers = with maintainers; [ codgician ];
}

View File

@@ -0,0 +1,331 @@
{
config,
lib,
utils,
pkgs,
...
}:
let
cfg = config.virtualisation.podman;
json = pkgs.formats.json { };
inherit (lib) mkOption types;
# Provides a fake "docker" binary mapping to podman
dockerCompat =
pkgs.runCommand "${cfg.package.pname}-docker-compat-${cfg.package.version}"
{
outputs = [
"out"
"man"
];
inherit (cfg.package) meta;
preferLocalBuild = true;
}
''
mkdir -p $out/bin
ln -s ${cfg.package}/bin/podman $out/bin/docker
mkdir -p $man/share/man/man1
for f in ${cfg.package.man}/share/man/man1/*; do
basename=$(basename $f | sed s/podman/docker/g)
ln -s $f $man/share/man/man1/$basename
done
'';
in
{
imports = [
(lib.mkRemovedOptionModule [
"virtualisation"
"podman"
"defaultNetwork"
"dnsname"
] "Use virtualisation.podman.defaultNetwork.settings.dns_enabled instead.")
(lib.mkRemovedOptionModule [
"virtualisation"
"podman"
"defaultNetwork"
"extraPlugins"
] "Netavark isn't compatible with CNI plugins.")
./network-socket.nix
];
meta = {
maintainers = lib.teams.podman.members;
};
options.virtualisation.podman = {
enable = mkOption {
type = types.bool;
default = false;
description = ''
This option enables Podman, a daemonless container engine for
developing, managing, and running OCI Containers on your Linux System.
It is a drop-in replacement for the {command}`docker` command.
'';
};
dockerSocket.enable = mkOption {
type = types.bool;
default = false;
description = ''
Make the Podman socket available in place of the Docker socket, so
Docker tools can find the Podman socket.
Podman implements the Docker API.
Users must be in the `podman` group in order to connect. As
with Docker, members of this group can gain root access.
'';
};
dockerCompat = mkOption {
type = types.bool;
default = false;
description = ''
Create an alias mapping {command}`docker` to {command}`podman`.
'';
};
enableNvidia = mkOption {
type = types.bool;
default = false;
description = ''
**Deprecated**, please use hardware.nvidia-container-toolkit.enable instead.
Enable use of Nvidia GPUs from within podman containers.
'';
};
extraPackages = mkOption {
type = with types; listOf package;
default = [ ];
description = ''
Extra dependencies for podman to be placed on $PATH in the wrapper.
'';
};
extraRuntimes = mkOption {
type = with types; listOf package;
# keep the default in sync with the podman package
default = lib.optionals pkgs.stdenv.hostPlatform.isLinux [ pkgs.runc ];
defaultText = lib.literalExpression ''lib.optionals pkgs.stdenv.hostPlatform.isLinux [ pkgs.runc ]'';
example = lib.literalExpression ''
[
pkgs.gvisor
]
'';
description = ''
Extra runtime packages to be installed in the Podman wrapper.
Those are then placed in libexec/podman, i.e. are seen as podman internal commands.
'';
};
autoPrune = {
enable = mkOption {
type = types.bool;
default = false;
description = ''
Whether to periodically prune Podman resources. If enabled, a
systemd timer will run `podman system prune -f`
as specified by the `dates` option.
'';
};
flags = mkOption {
type = types.listOf types.str;
default = [ ];
example = [ "--all" ];
description = ''
Any additional flags passed to {command}`podman system prune`.
'';
};
dates = mkOption {
default = "weekly";
type = types.str;
description = ''
Specification (in the format described by
{manpage}`systemd.time(7)`) of the time at
which the prune will occur.
'';
};
};
package =
(lib.mkPackageOption pkgs "podman" {
extraDescription = ''
This package will automatically include extra packages and runtimes.
'';
})
// {
apply =
pkg:
pkg.override {
extraPackages =
cfg.extraPackages
++ [
"/run/wrappers" # setuid shadow
config.systemd.package # To allow systemd-based container healthchecks
]
++ lib.optional (config.boot.supportedFilesystems.zfs or false) config.boot.zfs.package;
extraRuntimes =
cfg.extraRuntimes
++
lib.optionals
(
config.virtualisation.containers.containersConf.settings.network.default_rootless_network_cmd or ""
== "slirp4netns"
)
(
with pkgs;
[
slirp4netns
]
);
};
};
defaultNetwork.settings = lib.mkOption {
type = json.type;
default = { };
example = lib.literalExpression "{ dns_enabled = true; }";
description = ''
Settings for podman's default network.
'';
};
};
config =
let
networkConfig = (
{
dns_enabled = false;
driver = "bridge";
id = "0000000000000000000000000000000000000000000000000000000000000000";
internal = false;
ipam_options = {
driver = "host-local";
};
ipv6_enabled = false;
name = "podman";
network_interface = "podman0";
subnets = [
{
gateway = "10.88.0.1";
subnet = "10.88.0.0/16";
}
];
}
// cfg.defaultNetwork.settings
);
inherit (networkConfig) dns_enabled network_interface;
in
lib.mkIf cfg.enable {
warnings = lib.optionals cfg.enableNvidia [
''
You have set virtualisation.podman.enableNvidia. This option is deprecated, please set hardware.nvidia-container-toolkit.enable instead.
''
];
environment.systemPackages = [ cfg.package ] ++ lib.optional cfg.dockerCompat dockerCompat;
# https://github.com/containers/podman/blob/097cc6eb6dd8e598c0e8676d21267b4edb11e144/docs/tutorials/basic_networking.md#default-network
environment.etc."containers/networks/podman.json" = lib.mkIf (cfg.defaultNetwork.settings != { }) {
source = json.generate "podman.json" networkConfig;
};
# containers cannot reach aardvark-dns otherwise
networking.firewall.interfaces.${network_interface}.allowedUDPPorts = lib.mkIf dns_enabled [ 53 ];
virtualisation.containers = {
enable = true; # Enable common /etc/containers configuration
containersConf.settings = {
network = {
network_backend = "netavark";
firewall_driver = lib.mkIf config.networking.nftables.enable "nftables";
};
};
};
systemd.packages = [ cfg.package ];
systemd.services.podman-prune = {
description = "Prune podman resources";
restartIfChanged = false;
unitConfig.X-StopOnRemoval = false;
serviceConfig = {
Type = "oneshot";
ExecStart = utils.escapeSystemdExecArgs (
[
(lib.getExe cfg.package)
"system"
"prune"
"-f"
]
++ cfg.autoPrune.flags
);
};
startAt = lib.optional cfg.autoPrune.enable cfg.autoPrune.dates;
after = [ "podman.service" ];
requires = [ "podman.service" ];
};
systemd.services.podman.environment = config.networking.proxy.envVars;
systemd.sockets.podman.wantedBy = [ "sockets.target" ];
systemd.sockets.podman.socketConfig.SocketGroup = "podman";
# Podman does not support multiple sockets, as of podman 5.0.2, so we use
# a symlink. Unfortunately this does not let us use an alternate group,
# such as `docker`.
systemd.sockets.podman.socketConfig.Symlinks = lib.mkIf cfg.dockerSocket.enable [
"/run/docker.sock"
];
systemd.user.services.podman.environment = config.networking.proxy.envVars;
systemd.user.sockets.podman.wantedBy = [ "sockets.target" ];
systemd.timers.podman-prune.timerConfig = lib.mkIf cfg.autoPrune.enable {
Persistent = true;
RandomizedDelaySec = 1800;
};
systemd.tmpfiles.packages = [
# The /run/podman rule interferes with our podman group, so we remove
# it and let the systemd socket logic take care of it.
(pkgs.runCommand "podman-tmpfiles-nixos"
{
package = cfg.package;
preferLocalBuild = true;
}
''
mkdir -p $out/lib/tmpfiles.d/
grep -v 'D! /run/podman 0700 root root' \
<$package/lib/tmpfiles.d/podman.conf \
>$out/lib/tmpfiles.d/podman.conf
''
)
];
users.groups.podman = { };
assertions = [
{
assertion = cfg.dockerCompat -> !config.virtualisation.docker.enable;
message = "Option dockerCompat conflicts with docker";
}
{
assertion = cfg.dockerSocket.enable -> !config.virtualisation.docker.enable;
message = ''
The options virtualisation.podman.dockerSocket.enable and virtualisation.docker.enable conflict, because only one can serve the socket.
'';
}
];
};
}

View File

@@ -0,0 +1,39 @@
{
config,
lib,
pkg,
...
}:
let
inherit (lib)
mkOption
types
;
cfg = config.virtualisation.podman.networkSocket;
in
{
options.virtualisation.podman.networkSocket = {
server = mkOption {
type = types.enum [ "ghostunnel" ];
};
};
config = lib.mkIf (cfg.enable && cfg.server == "ghostunnel") {
services.ghostunnel = {
enable = true;
servers."podman-socket" = {
inherit (cfg.tls) cert key cacert;
listen = "${cfg.listenAddress}:${toString cfg.port}";
target = "unix:/run/podman/podman.sock";
allowAll = lib.mkDefault true;
};
};
systemd.services.ghostunnel-server-podman-socket.serviceConfig.SupplementaryGroups = [ "podman" ];
};
meta.maintainers = lib.teams.podman.members ++ [ lib.maintainers.roberth ];
}

View File

@@ -0,0 +1,99 @@
{
config,
lib,
pkg,
...
}:
let
inherit (lib)
mkOption
types
;
cfg = config.virtualisation.podman.networkSocket;
in
{
imports = [
./network-socket-ghostunnel.nix
];
options.virtualisation.podman.networkSocket = {
enable = mkOption {
type = types.bool;
default = false;
description = ''
Make the Podman and Docker compatibility API available over the network
with TLS client certificate authentication.
This allows Docker clients to connect with the equivalents of the Docker
CLI `-H` and `--tls*` family of options.
For certificate setup, see <https://docs.docker.com/engine/security/protect-access/>
This option is independent of [](#opt-virtualisation.podman.dockerSocket.enable).
'';
};
server = mkOption {
type = types.enum [ ];
description = ''
Choice of TLS proxy server.
'';
example = "ghostunnel";
};
openFirewall = mkOption {
type = types.bool;
default = false;
description = ''
Whether to open the port in the firewall.
'';
};
tls.cacert = mkOption {
type = types.path;
description = ''
Path to CA certificate to use for client authentication.
'';
};
tls.cert = mkOption {
type = types.path;
description = ''
Path to certificate describing the server.
'';
};
tls.key = mkOption {
type = types.path;
description = ''
Path to the private key corresponding to the server certificate.
Use a string for this setting. Otherwise it will be copied to the Nix
store first, where it is readable by any system process.
'';
};
port = mkOption {
type = types.port;
default = 2376;
description = ''
TCP port number for receiving TLS connections.
'';
};
listenAddress = mkOption {
type = types.str;
default = "0.0.0.0";
description = ''
Interface address for receiving TLS connections.
'';
};
};
config = {
networking.firewall.allowedTCPPorts = lib.optional (cfg.enable && cfg.openFirewall) cfg.port;
};
meta.maintainers = lib.teams.podman.members ++ [ lib.maintainers.roberth ];
}

View File

@@ -0,0 +1,376 @@
{
config,
pkgs,
lib,
...
}:
with lib;
{
imports = [
./disk-size-option.nix
../image/file-options.nix
(lib.mkRenamedOptionModuleWith {
sinceRelease = 2411;
from = [
"proxmox"
"qemuConf"
"diskSize"
];
to = [
"virtualisation"
"diskSize"
];
})
];
options.proxmox = {
qemuConf = {
# essential configs
boot = mkOption {
type = types.str;
default = "";
example = "order=scsi0;net0";
description = ''
Default boot device. PVE will try all devices in its default order if this value is empty.
'';
};
scsihw = mkOption {
type = types.str;
default = "virtio-scsi-single";
example = "lsi";
description = ''
SCSI controller type. Must be one of the supported values given in
<https://pve.proxmox.com/wiki/Qemu/KVM_Virtual_Machines>
'';
};
virtio0 = mkOption {
type = types.str;
default = "local-lvm:vm-9999-disk-0";
example = "ceph:vm-123-disk-0";
description = ''
Configuration for the default virtio disk. It can be used as a cue for PVE to autodetect the target storage.
This parameter is required by PVE even if it isn't used.
'';
};
ostype = mkOption {
type = types.str;
default = "l26";
description = ''
Guest OS type
'';
};
cores = mkOption {
type = types.ints.positive;
default = 1;
description = ''
Guest core count
'';
};
memory = mkOption {
type = types.ints.positive;
default = 1024;
description = ''
Guest memory in MiB (1024×1024 bytes)
'';
};
bios = mkOption {
type = types.enum [
"seabios"
"ovmf"
];
default = "seabios";
description = ''
Select BIOS implementation (seabios = Legacy BIOS, ovmf = UEFI).
'';
};
# optional configs
name = mkOption {
type = types.str;
default = "nixos-${config.system.nixos.label}";
description = ''
VM name
'';
};
additionalSpace = mkOption {
type = types.str;
default = "512M";
example = "2048M";
description = ''
additional disk space to be added to the image if diskSize "auto"
is used.
'';
};
bootSize = mkOption {
type = types.str;
default = "256M";
example = "512M";
description = ''
Size of the boot partition. Is only used if partitionTableType is
either "efi" or "hybrid".
'';
};
net0 = mkOption {
type = types.commas;
default = "virtio=00:00:00:00:00:00,bridge=vmbr0,firewall=1";
description = ''
Configuration for the default interface. When restoring from VMA, check the
"unique" box to ensure device mac is randomized.
'';
};
serial0 = mkOption {
type = types.str;
default = "socket";
example = "/dev/ttyS0";
description = ''
Create a serial device inside the VM (n is 0 to 3), and pass through a host serial device (i.e. /dev/ttyS0),
or create a unix socket on the host side (use qm terminal to open a terminal connection).
'';
};
agent = mkOption {
type = types.bool;
apply = x: if x then "1" else "0";
default = true;
description = ''
Expect guest to have qemu agent running
'';
};
};
qemuExtraConf = mkOption {
type =
with types;
attrsOf (oneOf [
str
int
]);
default = { };
example = literalExpression ''
{
cpu = "host";
onboot = 1;
}
'';
description = ''
Additional options appended to qemu-server.conf
'';
};
partitionTableType = mkOption {
type = types.enum [
"efi"
"hybrid"
"legacy"
"legacy+gpt"
];
description = ''
Partition table type to use. See make-disk-image.nix partitionTableType for details.
Defaults to 'legacy' for 'proxmox.qemuConf.bios="seabios"' (default), other bios values defaults to 'efi'.
Use 'hybrid' to build grub-based hybrid bios+efi images.
'';
default = if config.proxmox.qemuConf.bios == "seabios" then "legacy" else "efi";
defaultText = lib.literalExpression ''if config.proxmox.qemuConf.bios == "seabios" then "legacy" else "efi"'';
example = "hybrid";
};
filenameSuffix = mkOption {
type = types.str;
default = config.proxmox.qemuConf.name;
example = "999-nixos_template";
description = ''
Filename of the image will be vzdump-qemu-''${filenameSuffix}.vma.zstd.
This will also determine the default name of the VM on restoring the VMA.
Start this value with a number if you want the VMA to be detected as a backup of
any specific VMID.
'';
};
cloudInit = {
enable = mkOption {
type = types.bool;
default = true;
description = ''
Whether the VM should accept cloud init configurations from PVE.
'';
};
defaultStorage = mkOption {
default = "local-lvm";
example = "tank";
type = types.str;
description = ''
Default storage name for cloud init drive.
'';
};
device = mkOption {
default = "ide2";
example = "scsi0";
type = types.str;
description = ''
Bus/device to which the cloud init drive is attached.
'';
};
};
};
config =
let
cfg = config.proxmox;
cfgLine = name: value: ''
${name}: ${builtins.toString value}
'';
virtio0Storage = builtins.head (builtins.split ":" cfg.qemuConf.virtio0);
cfgFile =
fileName: properties:
pkgs.writeTextDir fileName ''
# generated by NixOS
${lib.concatStrings (lib.mapAttrsToList cfgLine properties)}
#qmdump#map:virtio0:drive-virtio0:${virtio0Storage}:raw:
'';
inherit (cfg) partitionTableType;
supportEfi = partitionTableType == "efi" || partitionTableType == "hybrid";
supportBios =
partitionTableType == "legacy"
|| partitionTableType == "hybrid"
|| partitionTableType == "legacy+gpt";
hasBootPartition = partitionTableType == "efi" || partitionTableType == "hybrid";
hasNoFsPartition = partitionTableType == "hybrid" || partitionTableType == "legacy+gpt";
in
{
assertions = [
{
assertion = config.boot.loader.systemd-boot.enable -> config.proxmox.qemuConf.bios == "ovmf";
message = "systemd-boot requires 'ovmf' bios";
}
{
assertion = partitionTableType == "efi" -> config.proxmox.qemuConf.bios == "ovmf";
message = "'efi' disk partitioning requires 'ovmf' bios";
}
{
assertion = partitionTableType == "legacy" -> config.proxmox.qemuConf.bios == "seabios";
message = "'legacy' disk partitioning requires 'seabios' bios";
}
{
assertion = partitionTableType == "legacy+gpt" -> config.proxmox.qemuConf.bios == "seabios";
message = "'legacy+gpt' disk partitioning requires 'seabios' bios";
}
];
image.baseName = lib.mkDefault "vzdump-qemu-${cfg.filenameSuffix}";
image.extension = "vma.zst";
system.build.image = config.system.build.VMA;
system.build.VMA = import ../../lib/make-disk-image.nix {
name = "proxmox-${cfg.filenameSuffix}";
baseName = config.image.baseName;
inherit (cfg) partitionTableType;
postVM =
let
# Build qemu with PVE's patch that adds support for the VMA format
vma =
(pkgs.qemu_kvm.override {
alsaSupport = false;
pulseSupport = false;
sdlSupport = false;
jackSupport = false;
gtkSupport = false;
vncSupport = false;
smartcardSupport = false;
spiceSupport = false;
ncursesSupport = false;
libiscsiSupport = false;
tpmSupport = false;
numaSupport = false;
seccompSupport = false;
guestAgentSupport = false;
}).overrideAttrs
(super: rec {
# Check https://github.com/proxmox/pve-qemu/tree/master for the version
# of qemu and patch to use
version = "9.0.0";
src = pkgs.fetchurl {
url = "https://download.qemu.org/qemu-${version}.tar.xz";
hash = "sha256-MnCKxmww2MiSYz6paMdxwcdtWX1w3erSGg0izPOG2mk=";
};
patches = [
# Proxmox' VMA tool is published as a particular patch upon QEMU
"${
pkgs.fetchFromGitHub {
owner = "proxmox";
repo = "pve-qemu";
rev = "14afbdd55f04d250bd679ca1ad55d3f47cd9d4c8";
hash = "sha256-lSJQA5SHIHfxJvMLIID2drv2H43crTPMNIlIT37w9Nc=";
}
}/debian/patches/pve/0027-PVE-Backup-add-vma-backup-format-code.patch"
];
buildInputs = super.buildInputs ++ [ pkgs.libuuid ];
nativeBuildInputs = super.nativeBuildInputs ++ [ pkgs.perl ];
});
in
''
${vma}/bin/vma create "${config.image.baseName}.vma" \
-c ${
cfgFile "qemu-server.conf" (cfg.qemuConf // cfg.qemuExtraConf)
}/qemu-server.conf drive-virtio0=$diskImage
rm $diskImage
${pkgs.zstd}/bin/zstd "${config.image.baseName}.vma"
mv "${config.image.fileName}" $out/
mkdir -p $out/nix-support
echo "file vma $out/${config.image.fileName}" > $out/nix-support/hydra-build-products
'';
inherit (cfg.qemuConf) additionalSpace bootSize;
inherit (config.virtualisation) diskSize;
format = "raw";
inherit config lib pkgs;
};
boot = {
growPartition = true;
kernelParams = [ "console=ttyS0" ];
loader.grub = {
device = lib.mkDefault (
if (hasNoFsPartition || supportBios) then
# Even if there is a separate no-fs partition ("/dev/disk/by-partlabel/no-fs" i.e. "/dev/vda2"),
# which will be used the bootloader, do not set it as loader.grub.device.
# GRUB installation fails, unless the whole disk is selected.
"/dev/vda"
else
"nodev"
);
efiSupport = lib.mkDefault supportEfi;
efiInstallAsRemovable = lib.mkDefault supportEfi;
};
loader.timeout = 0;
initrd.availableKernelModules = [
"uas"
"virtio_blk"
"virtio_pci"
];
};
fileSystems."/" = {
device = "/dev/disk/by-label/nixos";
autoResize = true;
fsType = "ext4";
};
fileSystems."/boot" = lib.mkIf hasBootPartition {
device = "/dev/disk/by-label/ESP";
fsType = "vfat";
};
networking = mkIf cfg.cloudInit.enable {
hostName = mkForce "";
useDHCP = false;
};
services = {
cloud-init = mkIf cfg.cloudInit.enable {
enable = true;
network.enable = true;
};
sshd.enable = mkDefault true;
qemuGuest.enable = true;
};
proxmox.qemuExtraConf.${cfg.cloudInit.device} =
"${cfg.cloudInit.defaultStorage}:vm-9999-cloudinit,media=cdrom";
};
}

View File

@@ -0,0 +1,139 @@
{
config,
pkgs,
lib,
...
}:
with lib;
{
imports = [
../image/file-options.nix
];
options.proxmoxLXC = {
enable = mkOption {
default = true;
type = types.bool;
description = "Whether to enable the Proxmox VE LXC module.";
};
privileged = mkOption {
type = types.bool;
default = false;
description = ''
Whether to enable privileged mounts
'';
};
manageNetwork = mkOption {
type = types.bool;
default = false;
description = ''
Whether to manage network interfaces through nix options
When false, systemd-networkd is enabled to accept network
configuration from proxmox.
'';
};
manageHostName = mkOption {
type = types.bool;
default = false;
description = ''
Whether to manage hostname through nix options
When false, the hostname is picked up from /etc/hostname
populated by proxmox.
'';
};
};
config =
let
cfg = config.proxmoxLXC;
in
mkIf cfg.enable {
system.nixos.tags = [
"proxmox"
"lxc"
];
image.extension = "tar.xz";
image.filePath = "tarball/${config.image.fileName}";
system.build.image = config.system.build.tarball;
system.build.tarball = pkgs.callPackage ../../lib/make-system-tarball.nix {
fileName = config.image.baseName;
storeContents = [
{
object = config.system.build.toplevel;
symlink = "none";
}
];
contents = [
{
source = config.system.build.toplevel + "/init";
target = "/sbin/init";
}
];
extraCommands = "mkdir -p root etc/systemd/network";
};
boot.postBootCommands = ''
# After booting, register the contents of the Nix store in the Nix
# database.
if [ -f /nix-path-registration ]; then
${config.nix.package.out}/bin/nix-store --load-db < /nix-path-registration &&
rm /nix-path-registration
fi
# nixos-rebuild also requires a "system" profile
${config.nix.package.out}/bin/nix-env -p /nix/var/nix/profiles/system --set /run/current-system
'';
boot = {
isContainer = true;
loader.initScript.enable = true;
};
console.enable = true;
networking = mkIf (!cfg.manageNetwork) {
useDHCP = false;
useHostResolvConf = false;
useNetworkd = true;
# pick up hostname from /etc/hostname generated by proxmox
hostName = mkIf (!cfg.manageHostName) (mkForce "");
};
# unprivileged LXCs can't set net.ipv4.ping_group_range
security.wrappers.ping = mkIf (!cfg.privileged) {
owner = "root";
group = "root";
capabilities = "cap_net_raw+p";
source = "${pkgs.iputils.out}/bin/ping";
};
services.openssh = {
enable = mkDefault true;
startWhenNeeded = mkDefault true;
};
systemd = {
mounts = mkIf (!cfg.privileged) [
{
enable = false;
where = "/sys/kernel/debug";
}
];
# By default only starts getty on tty0 but first on LXC is tty1
services."autovt@".unitConfig.ConditionPathExists = [
""
"/dev/%I"
];
# These are disabled by `console.enable` but console via tty is the default in Proxmox
services."getty@tty1".enable = lib.mkForce true;
services."autovt@".enable = lib.mkForce true;
};
};
}

View File

@@ -0,0 +1,44 @@
{
config,
lib,
pkgs,
...
}:
with lib;
let
cfg = config.services.qemuGuest;
in
{
options.services.qemuGuest = {
enable = mkOption {
type = types.bool;
default = false;
description = "Whether to enable the qemu guest agent.";
};
package = mkPackageOption pkgs [ "qemu_kvm" "ga" ] { };
};
config = mkIf cfg.enable (mkMerge [
{
services.udev.extraRules = ''
SUBSYSTEM=="virtio-ports", ATTR{name}=="org.qemu.guest_agent.0", TAG+="systemd", ENV{SYSTEMD_WANTS}="qemu-guest-agent.service"
'';
systemd.services.qemu-guest-agent = {
description = "Run the QEMU Guest Agent";
serviceConfig = {
ExecStart = "${cfg.package}/bin/qemu-ga --statedir /run/qemu-ga";
Restart = "always";
RestartSec = 0;
# Runtime directory and mode
RuntimeDirectory = "qemu-ga";
RuntimeDirectoryMode = "0755";
};
};
}
]);
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,85 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.virtualisation.rosetta;
inherit (lib) types;
in
{
options = {
virtualisation.rosetta.enable = lib.mkOption {
type = types.bool;
default = false;
description = ''
Whether to enable [Rosetta](https://developer.apple.com/documentation/apple-silicon/about-the-rosetta-translation-environment) support.
This feature requires the system to be a virtualised guest on an Apple silicon host.
The default settings are suitable for the [UTM](https://docs.getutm.app/) virtualisation [package](https://search.nixos.org/packages?channel=unstable&show=utm&from=0&size=1&sort=relevance&type=packages&query=utm).
Make sure to select 'Apple Virtualization' as the virtualisation engine and then tick the 'Enable Rosetta' option.
'';
};
virtualisation.rosetta.mountPoint = lib.mkOption {
type = types.str;
default = "/run/rosetta";
internal = true;
description = ''
The mount point for the Rosetta runtime inside the guest system.
The proprietary runtime is exposed through a VirtioFS directory share and then mounted at this directory.
'';
};
virtualisation.rosetta.mountTag = lib.mkOption {
type = types.str;
default = "rosetta";
description = ''
The VirtioFS mount tag for the Rosetta runtime, exposed by the host's virtualisation software.
If supported, your virtualisation software should provide instructions on how register the Rosetta runtime inside Linux guests.
These instructions should mention the name of the mount tag used for the VirtioFS directory share that contains the Rosetta runtime.
'';
};
};
config = lib.mkIf cfg.enable {
assertions = [
{
assertion = pkgs.stdenv.hostPlatform.isAarch64;
message = "Rosetta is only supported on aarch64 systems";
}
];
fileSystems."${cfg.mountPoint}" = {
device = cfg.mountTag;
fsType = "virtiofs";
};
nix.settings = {
extra-platforms = [ "x86_64-linux" ];
extra-sandbox-paths = [
"/run/binfmt"
cfg.mountPoint
];
};
boot.binfmt.registrations.rosetta = {
interpreter = "${cfg.mountPoint}/rosetta";
# The required flags for binfmt are documented by Apple:
# https://developer.apple.com/documentation/virtualization/running_intel_binaries_in_linux_vms_with_rosetta
magicOrExtension = ''\x7fELF\x02\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x3e\x00'';
mask = ''\xff\xff\xff\xff\xff\xfe\xfe\x00\xff\xff\xff\xff\xff\xff\xff\xff\xfe\xff\xff\xff'';
fixBinary = true;
matchCredentials = true;
preserveArgvZero = false;
# Remove the shell wrapper and call the runtime directly
wrapInterpreterInShell = false;
};
};
}

View File

@@ -0,0 +1,31 @@
{
config,
pkgs,
lib,
...
}:
{
options.virtualisation.spiceUSBRedirection.enable = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
Install the SPICE USB redirection helper with setuid
privileges. This allows unprivileged users to pass USB devices
connected to this machine to libvirt VMs, both local and
remote. Note that this allows users arbitrary access to USB
devices.
'';
};
config = lib.mkIf config.virtualisation.spiceUSBRedirection.enable {
environment.systemPackages = [ pkgs.spice-gtk ]; # For polkit actions
security.wrappers.spice-client-glib-usb-acl-helper = {
owner = "root";
group = "root";
capabilities = "cap_fowner+ep";
source = "${pkgs.spice-gtk}/bin/spice-client-glib-usb-acl-helper";
};
};
meta.maintainers = [ ];
}

View File

@@ -0,0 +1,61 @@
# Minimal configuration that vagrant depends on
{ config, pkgs, ... }:
let
# Vagrant uses an insecure shared private key by default, but we
# don't use the authorizedKeys attribute under users because it should be
# removed on first boot and replaced with a random one. This script sets
# the correct permissions and installs the temporary key if no
# ~/.ssh/authorized_keys exists.
install-vagrant-ssh-key = pkgs.writeScriptBin "install-vagrant-ssh-key" ''
#!${pkgs.runtimeShell}
if [ ! -e ~/.ssh/authorized_keys ]; then
mkdir -m 0700 -p ~/.ssh
install -m 0600 <(echo "ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEA6NF8iallvQVp22WDkTkyrtvp9eWW6A8YVr+kz4TjGYe7gHzIw+niNltGEFHzD8+v1I2YJ6oXevct1YeS0o9HZyN1Q9qgCgzUFtdOKLv6IedplqoPkcmF0aYet2PkEDo3MlTBckFXPITAMzF8dJSIFo9D8HfdOV0IAdx4O7PtixWKn5y2hMNG0zQPyUecp4pzC6kivAIhyfHilFR61RGL+GPXQ2MWZWFYbAGjyiYJnAmCP3NOTd0jMZEnDkbUvxhMmBYSdETk1rRgm+R4LOzFUGaHqHDLKLX+FIPKcF96hrucXzcWyLbIbEgE98OHlnVYCzRdK8jlqm8tehUc9c9WhQ== vagrant insecure public key") ~/.ssh/authorized_keys
fi
'';
in
{
# Enable the OpenSSH daemon.
services.openssh.enable = true;
# Packages used by Vagrant
environment.systemPackages = with pkgs; [
findutils
iputils
net-tools
netcat
nfs-utils
rsync
];
users.extraUsers.vagrant = {
isNormalUser = true;
createHome = true;
description = "Vagrant user account";
extraGroups = [
"users"
"wheel"
];
home = "/home/vagrant";
password = "vagrant";
useDefaultShell = true;
uid = 1000;
};
systemd.services.install-vagrant-ssh-key = {
description = "Vagrant SSH key install (if needed)";
after = [ "fs.target" ];
wants = [ "fs.target" ];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
ExecStart = "${install-vagrant-ssh-key}/bin/install-vagrant-ssh-key";
User = "vagrant";
# So it won't be (needlessly) restarted:
RemainAfterExit = true;
};
};
security.sudo.wheelNeedsPassword = false;
security.sudo-rs.wheelNeedsPassword = false;
}

View File

@@ -0,0 +1,64 @@
# Vagrant + VirtualBox
{
config,
pkgs,
lib,
...
}:
{
imports = [
./vagrant-guest.nix
./virtualbox-image.nix
];
virtualbox.params = {
audio = "none";
audioin = "off";
audioout = "off";
usb = "off";
usbehci = "off";
};
documentation.man.enable = false;
documentation.nixos.enable = false;
users.extraUsers.vagrant.extraGroups = [ "vboxsf" ];
# generate the box v1 format which is much easier to generate
# https://www.vagrantup.com/docs/boxes/format.html
image.extension = lib.mkOverride 999 "${config.image.baseName}.box";
system.nixos.tags = [ "vagrant" ];
system.build.image = lib.mkOverride 999 config.system.build.vagrantVirtualbox;
system.build.vagrantVirtualbox = pkgs.runCommand config.image.fileName { } ''
mkdir workdir
cd workdir
# 1. create that metadata.json file
echo '{"provider":"virtualbox"}' > metadata.json
# 2. create a default Vagrantfile config
cat <<VAGRANTFILE > Vagrantfile
Vagrant.configure("2") do |config|
config.vm.base_mac = "0800275F0936"
end
VAGRANTFILE
# 3. add the exported VM files
tar xvf ${config.system.build.virtualBoxOVA}/*.ova
# 4. move the ovf to the fixed location
mv *.ovf box.ovf
# 5. generate OVF manifest file
rm *.mf
touch box.mf
for fname in *; do
checksum=$(sha256sum $fname | cut -d' ' -f 1)
echo "SHA256($fname)= $checksum" >> box.mf
done
# 6. compress everything back together
tar --owner=0 --group=0 --sort=name --numeric-owner -czf $out .
'';
}

View File

@@ -0,0 +1,156 @@
# Module for VirtualBox guests.
{
config,
lib,
pkgs,
...
}:
let
cfg = config.virtualisation.virtualbox.guest;
kernel = config.boot.kernelPackages;
mkVirtualBoxUserService = serviceArgs: verbose: {
description = "VirtualBox Guest User Services ${serviceArgs}";
wantedBy = [ "graphical-session.target" ];
partOf = [ "graphical-session.target" ];
# The graphical session may not be ready when starting the service
# Hence, check if the DISPLAY env var is set, otherwise fail, wait and retry again
startLimitBurst = 20;
unitConfig.ConditionVirtualization = "oracle";
# Check if the display environment is ready, otherwise fail
preStart = "${pkgs.bash}/bin/bash -c \"if [ -z $DISPLAY ]; then exit 1; fi\"";
serviceConfig = {
ExecStart =
"@${kernel.virtualboxGuestAdditions}/bin/VBoxClient"
+ (lib.strings.optionalString verbose " --verbose")
+ " --foreground ${serviceArgs}";
# Wait after a failure, hoping that the display environment is ready after waiting
RestartSec = 2;
Restart = "always";
};
};
mkVirtualBoxUserX11OnlyService =
serviceArgs: verbose:
(mkVirtualBoxUserService serviceArgs verbose)
// {
unitConfig.ConditionEnvironment = "XDG_SESSION_TYPE=x11";
};
in
{
imports = [
(lib.mkRenamedOptionModule
[
"virtualisation"
"virtualbox"
"guest"
"draganddrop"
]
[
"virtualisation"
"virtualbox"
"guest"
"dragAndDrop"
]
)
];
options.virtualisation.virtualbox.guest = {
enable = lib.mkOption {
default = false;
type = lib.types.bool;
description = "Whether to enable the VirtualBox service and other guest additions.";
};
clipboard = lib.mkOption {
default = true;
type = lib.types.bool;
description = "Whether to enable clipboard support.";
};
seamless = lib.mkOption {
default = true;
type = lib.types.bool;
description = "Whether to enable seamless mode. When activated windows from the guest appear next to the windows of the host.";
};
dragAndDrop = lib.mkOption {
default = true;
type = lib.types.bool;
description = "Whether to enable drag and drop support.";
};
verbose = lib.mkOption {
default = false;
type = lib.types.bool;
description = "Whether to verbose logging for guest services.";
};
vboxsf = lib.mkOption {
default = true;
type = lib.types.bool;
description = "Whether to load vboxsf";
};
};
###### implementation
config = lib.mkIf cfg.enable (
lib.mkMerge [
{
assertions = [
{
assertion = pkgs.stdenv.hostPlatform.isx86 || pkgs.stdenv.hostPlatform.isAarch64;
message = "Virtualbox not currently supported on ${pkgs.stdenv.hostPlatform.system}";
}
];
environment.systemPackages = [ kernel.virtualboxGuestAdditions ];
boot.extraModulePackages = [ kernel.virtualboxGuestAdditions ];
systemd.services.virtualbox = {
description = "VirtualBox Guest Services";
wantedBy = [ "multi-user.target" ];
requires = [ "dev-vboxguest.device" ];
after = [ "dev-vboxguest.device" ];
unitConfig.ConditionVirtualization = "oracle";
serviceConfig.ExecStart = "@${kernel.virtualboxGuestAdditions}/bin/VBoxService VBoxService --foreground";
};
services.udev.extraRules = ''
# /dev/vboxuser is necessary for VBoxClient to work. Maybe we
# should restrict this to logged-in users.
KERNEL=="vboxuser", OWNER="root", GROUP="root", MODE="0666"
# Allow systemd dependencies on vboxguest.
SUBSYSTEM=="misc", KERNEL=="vboxguest", TAG+="systemd"
'';
systemd.user.services.virtualboxClientVmsvga = mkVirtualBoxUserService "--vmsvga-session" cfg.verbose;
}
(lib.mkIf cfg.vboxsf {
boot.supportedFilesystems = [ "vboxsf" ];
boot.initrd.supportedFilesystems = [ "vboxsf" ];
users.groups.vboxsf.gid = config.ids.gids.vboxsf;
})
(lib.mkIf cfg.clipboard {
systemd.user.services.virtualboxClientClipboard = mkVirtualBoxUserService "--clipboard" cfg.verbose;
})
(lib.mkIf cfg.seamless {
systemd.user.services.virtualboxClientSeamless = mkVirtualBoxUserX11OnlyService "--seamless" cfg.verbose;
})
(lib.mkIf cfg.dragAndDrop {
systemd.user.services.virtualboxClientDragAndDrop = mkVirtualBoxUserService "--draganddrop" cfg.verbose;
})
]
);
}

View File

@@ -0,0 +1,230 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.virtualisation.virtualbox.host;
virtualbox = cfg.package.override {
inherit (cfg)
enableHardening
headless
enableWebService
enableKvm
;
extensionPack = if cfg.enableExtensionPack then pkgs.virtualboxExtpack else null;
};
kernelModules = config.boot.kernelPackages.virtualbox.override {
inherit virtualbox;
};
in
{
options.virtualisation.virtualbox.host = {
enable = lib.mkEnableOption "VirtualBox" // {
description = ''
Whether to enable VirtualBox.
::: {.note}
In order to pass USB devices from the host to the guests, the user
needs to be in the `vboxusers` group.
:::
'';
};
enableExtensionPack = lib.mkEnableOption "VirtualBox extension pack" // {
description = ''
Whether to install the Oracle Extension Pack for VirtualBox.
::: {.important}
You must set `nixpkgs.config.allowUnfree = true` in
order to use this. This requires you accept the VirtualBox PUEL.
:::
'';
};
package = lib.mkPackageOption pkgs "virtualbox" { };
addNetworkInterface = lib.mkOption {
type = lib.types.bool;
default = true;
description = ''
Automatically set up a vboxnet0 host-only network interface.
'';
};
enableHardening = lib.mkOption {
type = lib.types.bool;
default = true;
description = ''
Enable hardened VirtualBox, which ensures that only the binaries in the
system path get access to the devices exposed by the kernel modules
instead of all users in the vboxusers group.
::: {.important}
Disabling this can put your system's security at risk, as local users
in the vboxusers group can tamper with the VirtualBox device files.
:::
'';
};
headless = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
Use VirtualBox installation without GUI and Qt dependency. Useful to enable on servers
and when virtual machines are controlled only via SSH.
'';
};
enableWebService = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
Build VirtualBox web service tool (vboxwebsrv) to allow managing VMs via other webpage frontend tools. Useful for headless servers.
'';
};
enableKvm = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
Enable KVM support for VirtualBox. This increases compatibility with Linux kernel versions, because the VirtualBox kernel modules
are not required.
This option is incompatible with `addNetworkInterface`.
Note: This is experimental. Please check <https://github.com/cyberus-technology/virtualbox-kvm/issues>.
'';
};
};
config = lib.mkIf cfg.enable (
lib.mkMerge [
{
warnings = lib.mkIf (pkgs.config.virtualbox.enableExtensionPack or false) [
"'nixpkgs.virtualbox.enableExtensionPack' has no effect, please use 'virtualisation.virtualbox.host.enableExtensionPack'"
];
environment.systemPackages = [ virtualbox ];
security.wrappers =
let
mkSuid = program: {
source = "${virtualbox}/libexec/virtualbox/${program}";
owner = "root";
group = "vboxusers";
setuid = true;
};
executables = [
"VBoxHeadless"
"VBoxNetAdpCtl"
"VBoxNetDHCP"
"VBoxNetNAT"
"VBoxVolInfo"
]
++ (lib.optionals (!cfg.headless) [
"VBoxSDL"
"VirtualBoxVM"
]);
in
lib.mkIf cfg.enableHardening (
builtins.listToAttrs (
map (x: {
name = x;
value = mkSuid x;
}) executables
)
);
users.groups.vboxusers.gid = config.ids.gids.vboxusers;
services.udev.extraRules = ''
SUBSYSTEM=="usb_device", ACTION=="add", RUN+="${virtualbox}/libexec/virtualbox/VBoxCreateUSBNode.sh $major $minor $attr{bDeviceClass}"
SUBSYSTEM=="usb", ACTION=="add", ENV{DEVTYPE}=="usb_device", RUN+="${virtualbox}/libexec/virtualbox/VBoxCreateUSBNode.sh $major $minor $attr{bDeviceClass}"
SUBSYSTEM=="usb_device", ACTION=="remove", RUN+="${virtualbox}/libexec/virtualbox/VBoxCreateUSBNode.sh --remove $major $minor"
SUBSYSTEM=="usb", ACTION=="remove", ENV{DEVTYPE}=="usb_device", RUN+="${virtualbox}/libexec/virtualbox/VBoxCreateUSBNode.sh --remove $major $minor"
'';
}
(lib.mkIf cfg.enableKvm {
assertions = [
{
assertion = !cfg.addNetworkInterface;
message = "VirtualBox KVM only supports standard NAT networking for VMs. Please turn off virtualisation.virtualbox.host.addNetworkInterface.";
}
];
})
(lib.mkIf (!cfg.enableKvm) {
boot.kernelModules = [
"vboxdrv"
"vboxnetadp"
"vboxnetflt"
];
boot.extraModulePackages = [ kernelModules ];
# See https://github.com/VirtualBox/virtualbox/issues/188
boot.kernelParams =
lib.mkIf
(
lib.versionAtLeast config.boot.kernelPackages.kernel.version "6.12"
&& lib.versionOlder config.boot.kernelPackages.kernel.version "6.16"
)
[
"kvm.enable_virt_at_load=0"
];
services.udev.extraRules = ''
KERNEL=="vboxdrv", OWNER="root", GROUP="vboxusers", MODE="0660", TAG+="systemd"
KERNEL=="vboxdrvu", OWNER="root", GROUP="root", MODE="0666", TAG+="systemd"
KERNEL=="vboxnetctl", OWNER="root", GROUP="vboxusers", MODE="0660", TAG+="systemd"
'';
# Since we lack the right setuid/setcap binaries, set up a host-only network by default.
})
(lib.mkIf cfg.addNetworkInterface {
systemd.services.vboxnet0 = {
description = "VirtualBox vboxnet0 Interface";
requires = [ "dev-vboxnetctl.device" ];
after = [ "dev-vboxnetctl.device" ];
wantedBy = [
"network.target"
"sys-subsystem-net-devices-vboxnet0.device"
];
path = [ virtualbox ];
serviceConfig.RemainAfterExit = true;
serviceConfig.Type = "oneshot";
serviceConfig.PrivateTmp = true;
environment.VBOX_USER_HOME = "/tmp";
script = ''
if ! [ -e /sys/class/net/vboxnet0 ]; then
VBoxManage hostonlyif create
cat /tmp/VBoxSVC.log >&2
fi
'';
postStop = ''
VBoxManage hostonlyif remove vboxnet0
'';
};
networking.interfaces.vboxnet0.ipv4.addresses = [
{
address = "192.168.56.1";
prefixLength = 24;
}
];
# Make sure NetworkManager won't assume this interface being up
# means we have internet access.
networking.networkmanager.unmanaged = [ "vboxnet0" ];
})
(lib.mkIf config.networking.useNetworkd {
systemd.network.networks."40-vboxnet0".extraConfig = ''
[Link]
RequiredForOnline=no
'';
})
]
);
}

View File

@@ -0,0 +1,305 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.virtualbox;
in
{
imports = [
./disk-size-option.nix
../image/file-options.nix
(lib.mkRenamedOptionModuleWith {
sinceRelease = 2411;
from = [
"virtualbox"
"baseImageSize"
];
to = [
"virtualisation"
"diskSize"
];
})
(lib.mkRenamedOptionModuleWith {
sinceRelease = 2505;
from = [
"virtualisation"
"virtualbox"
"vmFileName"
];
to = [
"image"
"fileName"
];
})
];
options = {
virtualbox = {
baseImageFreeSpace = lib.mkOption {
type = lib.types.int;
default = 30 * 1024;
description = ''
Free space in the VirtualBox base image in MiB.
'';
};
memorySize = lib.mkOption {
type = lib.types.int;
default = 1536;
description = ''
The amount of RAM the VirtualBox appliance can use in MiB.
'';
};
vmDerivationName = lib.mkOption {
type = lib.types.str;
default = "nixos-ova-${config.system.nixos.label}-${pkgs.stdenv.hostPlatform.system}";
description = ''
The name of the derivation for the VirtualBox appliance.
'';
};
vmName = lib.mkOption {
type = lib.types.str;
default = "${config.system.nixos.distroName} ${config.system.nixos.label} (${pkgs.stdenv.hostPlatform.system})";
description = ''
The name of the VirtualBox appliance.
'';
};
params = lib.mkOption {
type =
with lib.types;
attrsOf (oneOf [
str
int
bool
(listOf str)
]);
example = {
audio = "alsa";
rtcuseutc = "on";
usb = "off";
};
description = ''
Parameters passed to the Virtualbox appliance.
Run `VBoxManage modifyvm --help` to see more options.
'';
};
exportParams = lib.mkOption {
type =
with lib.types;
listOf (oneOf [
str
int
bool
(listOf str)
]);
example = [
"--vsys"
"0"
"--vendor"
"ACME Inc."
];
default = [ ];
description = ''
Parameters passed to the Virtualbox export command.
Run `VBoxManage export --help` to see more options.
'';
};
extraDisk = lib.mkOption {
description = ''
Optional extra disk/hdd configuration.
The disk will be an 'ext4' partition on a separate file.
'';
default = null;
example = {
label = "storage";
mountPoint = "/home/demo/storage";
size = 100 * 1024;
};
type = lib.types.nullOr (
lib.types.submodule {
options = {
size = lib.mkOption {
type = lib.types.int;
description = "Size in MiB";
};
label = lib.mkOption {
type = lib.types.str;
default = "vm-extra-storage";
description = "Label for the disk partition";
};
mountPoint = lib.mkOption {
type = lib.types.str;
description = "Path where to mount this disk.";
};
};
}
);
};
postExportCommands = lib.mkOption {
type = lib.types.lines;
default = "";
example = ''
${pkgs.cot}/bin/cot edit-hardware "$fn" \
-v vmx-14 \
--nics 2 \
--nic-types VMXNET3 \
--nic-names 'Nic name' \
--nic-networks 'Nic match' \
--network-descriptions 'Nic description' \
--scsi-subtypes VirtualSCSI
'';
description = ''
Extra commands to run after exporting the OVA to `$fn`.
'';
};
storageController = lib.mkOption {
type =
with lib.types;
attrsOf (oneOf [
str
int
bool
(listOf str)
]);
example = {
name = "SCSI";
add = "scsi";
portcount = 16;
bootable = "on";
hostiocache = "on";
};
default = {
name = "SATA";
add = "sata";
portcount = 4;
bootable = "on";
hostiocache = "on";
};
description = ''
Parameters passed to the VirtualBox appliance. Must have at least
`name`.
Run `VBoxManage storagectl --help` to see more options.
'';
};
};
};
config = {
# Use a priority just below mkOptionDefault (1500) instead of lib.mkDefault
# to avoid breaking existing configs using that.
virtualisation.diskSize = lib.mkOverride 1490 (50 * 1024);
virtualbox.params = lib.mkMerge [
(lib.mapAttrs (name: lib.mkDefault) {
acpi = "on";
vram = 32;
nictype1 = "virtio";
nic1 = "nat";
audiocontroller = "ac97";
audio = "alsa";
audioout = "on";
graphicscontroller = "vmsvga";
rtcuseutc = "on";
usb = "on";
usbehci = "on";
mouse = "usbtablet";
})
(lib.mkIf (pkgs.stdenv.hostPlatform.system == "i686-linux") { pae = "on"; })
];
system.nixos.tags = [ "virtualbox" ];
image.extension = "ova";
system.build.image = lib.mkDefault config.system.build.virtualBoxOVA;
system.build.virtualBoxOVA = import ../../lib/make-disk-image.nix {
name = cfg.vmDerivationName;
baseName = config.image.baseName;
inherit pkgs lib config;
partitionTableType = "legacy";
inherit (config.virtualisation) diskSize;
additionalSpace = "${toString cfg.baseImageFreeSpace}M";
postVM = ''
export HOME=$PWD
export PATH=${pkgs.virtualbox}/bin:$PATH
echo "converting image to VirtualBox format..."
VBoxManage convertfromraw $diskImage disk.vdi
${lib.optionalString (cfg.extraDisk != null) ''
echo "creating extra disk: data-disk.raw"
dataDiskImage=data-disk.raw
truncate -s ${toString cfg.extraDisk.size}M $dataDiskImage
parted --script $dataDiskImage -- \
mklabel msdos \
mkpart primary ext4 1MiB -1
eval $(partx $dataDiskImage -o START,SECTORS --nr 1 --pairs)
mkfs.ext4 -F -L ${cfg.extraDisk.label} $dataDiskImage -E offset=$(sectorsToBytes $START) $(sectorsToKilobytes $SECTORS)K
echo "creating extra disk: data-disk.vdi"
VBoxManage convertfromraw $dataDiskImage data-disk.vdi
''}
echo "creating VirtualBox VM..."
vmName="${cfg.vmName}";
VBoxManage createvm --name "$vmName" --register \
--ostype ${if pkgs.stdenv.hostPlatform.system == "x86_64-linux" then "Linux26_64" else "Linux26"}
VBoxManage modifyvm "$vmName" \
--memory ${toString cfg.memorySize} \
${lib.cli.toGNUCommandLineShell { } cfg.params}
VBoxManage storagectl "$vmName" ${lib.cli.toGNUCommandLineShell { } cfg.storageController}
VBoxManage storageattach "$vmName" --storagectl ${cfg.storageController.name} --port 0 --device 0 --type hdd \
--medium disk.vdi
${lib.optionalString (cfg.extraDisk != null) ''
VBoxManage storageattach "$vmName" --storagectl ${cfg.storageController.name} --port 1 --device 0 --type hdd \
--medium data-disk.vdi
''}
echo "exporting VirtualBox VM..."
mkdir -p $out
fn="$out/${config.image.fileName}"
VBoxManage export "$vmName" --output "$fn" --options manifest ${lib.escapeShellArgs cfg.exportParams}
${cfg.postExportCommands}
rm -v $diskImage
mkdir -p $out/nix-support
echo "file ova $fn" >> $out/nix-support/hydra-build-products
'';
};
fileSystems = {
"/" = {
device = "/dev/disk/by-label/nixos";
autoResize = true;
fsType = "ext4";
};
}
// (lib.optionalAttrs (cfg.extraDisk != null) {
${cfg.extraDisk.mountPoint} = {
device = "/dev/disk/by-label/" + cfg.extraDisk.label;
autoResize = true;
fsType = "ext4";
};
});
boot.growPartition = true;
boot.loader.grub.device = "/dev/sda";
swapDevices = [
{
device = "/var/swap";
size = 2048;
}
];
virtualisation.virtualbox.guest.enable = true;
};
}

View File

@@ -0,0 +1,115 @@
{
config,
lib,
pkgs,
...
}:
let
inherit (lib)
getExe'
literalExpression
maintainers
mkEnableOption
mkIf
mkOption
mkRenamedOptionModule
optionals
optionalString
types
;
cfg = config.virtualisation.vmware.guest;
xf86inputvmmouse = pkgs.xorg.xf86inputvmmouse;
in
{
imports = [
(mkRenamedOptionModule [ "services" "vmwareGuest" ] [ "virtualisation" "vmware" "guest" ])
];
meta = {
maintainers = [ maintainers.kjeremy ];
};
options.virtualisation.vmware.guest = {
enable = mkEnableOption "VMWare Guest Support";
headless = mkOption {
type = types.bool;
default = !config.services.xserver.enable;
defaultText = literalExpression "!config.services.xserver.enable";
description = "Whether to disable X11-related features.";
};
package = mkOption {
type = types.package;
default = if cfg.headless then pkgs.open-vm-tools-headless else pkgs.open-vm-tools;
defaultText = literalExpression "if config.virtualisation.vmware.headless then pkgs.open-vm-tools-headless else pkgs.open-vm-tools;";
example = literalExpression "pkgs.open-vm-tools";
description = "Package providing open-vm-tools.";
};
};
config = mkIf cfg.enable {
assertions = [
{
assertion = pkgs.stdenv.hostPlatform.isx86 || pkgs.stdenv.hostPlatform.isAarch64;
message = "VMWare guest is not currently supported on ${pkgs.stdenv.hostPlatform.system}";
}
];
boot.initrd.availableKernelModules = [ "mptspi" ];
boot.initrd.kernelModules = optionals pkgs.stdenv.hostPlatform.isx86 [ "vmw_pvscsi" ];
environment.systemPackages = [ cfg.package ];
systemd.services.vmware = {
description = "VMWare Guest Service";
wantedBy = [ "multi-user.target" ];
after = [ "display-manager.service" ];
unitConfig.ConditionVirtualization = "vmware";
serviceConfig.ExecStart = getExe' cfg.package "vmtoolsd";
};
# Mount the vmblock for drag-and-drop and copy-and-paste.
systemd.mounts = mkIf (!cfg.headless) [
{
description = "VMware vmblock fuse mount";
documentation = [
"https://github.com/vmware/open-vm-tools/blob/master/open-vm-tools/vmblock-fuse/design.txt"
];
unitConfig.ConditionVirtualization = "vmware";
what = getExe' cfg.package "vmware-vmblock-fuse";
where = "/run/vmblock-fuse";
type = "fuse";
options = "subtype=vmware-vmblock,default_permissions,allow_other";
wantedBy = [ "multi-user.target" ];
}
];
security.wrappers.vmware-user-suid-wrapper = mkIf (!cfg.headless) {
setuid = true;
owner = "root";
group = "root";
source = getExe' cfg.package "vmware-user-suid-wrapper";
};
environment.etc.vmware-tools.source = "${cfg.package}/etc/vmware-tools/*";
services.xserver = mkIf (!cfg.headless) {
modules = optionals pkgs.stdenv.hostPlatform.isx86 [ xf86inputvmmouse ];
config = optionalString (pkgs.stdenv.hostPlatform.isx86) ''
Section "InputClass"
Identifier "VMMouse"
MatchDevicePath "/dev/input/event*"
MatchProduct "ImPS/2 Generic Wheel Mouse"
Driver "vmmouse"
EndSection
'';
displayManager.sessionCommands = ''
${getExe' cfg.package "vmware-user-suid-wrapper"}
'';
};
services.udev.packages = [ cfg.package ];
};
}

View File

@@ -0,0 +1,195 @@
{
config,
pkgs,
lib,
...
}:
let
cfg = config.virtualisation.vmware.host;
wrapperDir = "/run/vmware/bin"; # Perfectly fits as /usr/local/bin
parentWrapperDir = dirOf wrapperDir;
vmwareWrappers = # Needed as hardcoded paths workaround
let
mkVmwareSymlink = program: ''
ln -s "${config.security.wrapperDir}/${program}" $wrapperDir/${program}
'';
in
[
(mkVmwareSymlink "pkexec")
(mkVmwareSymlink "mount")
(mkVmwareSymlink "umount")
];
in
{
options = with lib; {
virtualisation.vmware.host = {
enable = mkEnableOption "VMware" // {
description = ''
This enables VMware host virtualisation for running VMs.
::: {.important}
`vmware-vmx` will cause kcompactd0 due to
`Transparent Hugepages` feature in kernel.
Apply `[ "transparent_hugepage=never" ]` in
option {option}`boot.kernelParams` to disable them.
:::
::: {.note}
If that didn't work disable `TRANSPARENT_HUGEPAGE`,
`COMPACTION` configs and recompile kernel.
:::
'';
};
package = mkPackageOption pkgs "vmware-workstation" { };
extraPackages = mkOption {
type = with types; listOf package;
default = [ ];
description = "Extra packages to be used with VMware host.";
example = "with pkgs; [ ntfs3g ]";
};
extraConfig = mkOption {
type = types.lines;
default = "";
description = "Add extra config to /etc/vmware/config";
example = ''
# Allow unsupported device's OpenGL and Vulkan acceleration for guest vGPU
mks.gl.allowUnsupportedDrivers = "TRUE"
mks.vk.allowUnsupportedDevices = "TRUE"
'';
};
};
};
config = lib.mkIf cfg.enable {
boot.extraModulePackages = [ config.boot.kernelPackages.vmware ];
boot.extraModprobeConfig = "alias char-major-10-229 fuse";
boot.kernelModules = [
"vmw_pvscsi"
"vmw_vmci"
"vmmon"
"vmnet"
"fuse"
];
environment.systemPackages = [ cfg.package ] ++ cfg.extraPackages;
services.printing.drivers = [ cfg.package ];
environment.etc."vmware/config".source =
let
packageConfig = "${cfg.package}/etc/vmware/config";
in
if cfg.extraConfig == "" then
packageConfig
else
pkgs.runCommandLocal "etc-vmware-config"
{
inherit packageConfig;
inherit (cfg) extraConfig;
}
''
(
cat "$packageConfig"
printf "\n"
echo "$extraConfig"
) >"$out"
'';
environment.etc."vmware/bootstrap".source = "${cfg.package}/etc/vmware/bootstrap";
environment.etc."vmware/icu".source = "${cfg.package}/etc/vmware/icu";
environment.etc."vmware-installer".source = "${cfg.package}/etc/vmware-installer";
# SUID wrappers
security.wrappers = {
vmware-vmx = {
setuid = true;
owner = "root";
group = "root";
source = "${cfg.package}/lib/vmware/bin/.vmware-vmx-wrapped";
};
};
# Services
systemd.services."vmware-wrappers" = {
description = "Create VMVare Wrappers";
wantedBy = [ "multi-user.target" ];
before = [
"vmware-authdlauncher.service"
"vmware-networks-configuration.service"
"vmware-networks.service"
"vmware-usbarbitrator.service"
];
after = [ "systemd-sysusers.service" ];
serviceConfig.Type = "oneshot";
serviceConfig.RemainAfterExit = true;
script = ''
mkdir -p "${parentWrapperDir}"
chmod 755 "${parentWrapperDir}"
# We want to place the tmpdirs for the wrappers to the parent dir.
wrapperDir=$(mktemp --directory --tmpdir="${parentWrapperDir}" wrappers.XXXXXXXXXX)
chmod a+rx "$wrapperDir"
${lib.concatStringsSep "\n" vmwareWrappers}
if [ -L ${wrapperDir} ]; then
# Atomically replace the symlink
# See https://axialcorps.com/2013/07/03/atomically-replacing-files-and-directories/
old=$(readlink -f ${wrapperDir})
if [ -e "${wrapperDir}-tmp" ]; then
rm --force --recursive "${wrapperDir}-tmp"
fi
ln --symbolic --force --no-dereference "$wrapperDir" "${wrapperDir}-tmp"
mv --no-target-directory "${wrapperDir}-tmp" "${wrapperDir}"
rm --force --recursive "$old"
else
# For initial setup
ln --symbolic "$wrapperDir" "${wrapperDir}"
fi
'';
};
systemd.services."vmware-authdlauncher" = {
description = "VMware Authentication Daemon";
serviceConfig = {
Type = "forking";
ExecStart = [ "${cfg.package}/bin/vmware-authdlauncher" ];
};
wantedBy = [ "multi-user.target" ];
};
systemd.services."vmware-networks-configuration" = {
description = "VMware Networks Configuration Generation";
unitConfig.ConditionPathExists = "!/etc/vmware/networking";
serviceConfig = {
UMask = "0077";
ExecStart = [
"${cfg.package}/bin/vmware-networks --postinstall vmware-player,0,1"
];
Type = "oneshot";
RemainAfterExit = "yes";
};
wantedBy = [ "multi-user.target" ];
};
systemd.services."vmware-networks" = {
description = "VMware Networks";
after = [ "vmware-networks-configuration.service" ];
requires = [ "vmware-networks-configuration.service" ];
serviceConfig = {
Type = "forking";
ExecCondition = [ "${pkgs.kmod}/bin/modprobe vmnet" ];
ExecStart = [ "${cfg.package}/bin/vmware-networks --start" ];
ExecStop = [ "${cfg.package}/bin/vmware-networks --stop" ];
};
wantedBy = [ "multi-user.target" ];
};
systemd.services."vmware-usbarbitrator" = {
description = "VMware USB Arbitrator";
serviceConfig = {
ExecStart = [ "${cfg.package}/bin/vmware-usbarbitrator -f" ];
};
wantedBy = [ "multi-user.target" ];
};
};
}

View File

@@ -0,0 +1,107 @@
{
config,
pkgs,
lib,
...
}:
let
boolToStr = value: if value then "on" else "off";
cfg = config.vmware;
subformats = [
"monolithicSparse"
"monolithicFlat"
"twoGbMaxExtentSparse"
"twoGbMaxExtentFlat"
"streamOptimized"
];
in
{
imports = [
../image/file-options.nix
(lib.mkRenamedOptionModuleWith {
sinceRelease = 2505;
from = [
"virtualisation"
"vmware"
"vmFileName"
];
to = [
"image"
"fileName"
];
})
];
options = {
vmware = {
baseImageSize = lib.mkOption {
type = with lib.types; either (enum [ "auto" ]) int;
default = "auto";
example = 2048;
description = ''
The size of the VMWare base image in MiB.
'';
};
vmDerivationName = lib.mkOption {
type = lib.types.str;
default = "nixos-vmware-${config.system.nixos.label}-${pkgs.stdenv.hostPlatform.system}";
description = ''
The name of the derivation for the VMWare appliance.
'';
};
vmSubformat = lib.mkOption {
type = lib.types.enum subformats;
default = "monolithicSparse";
description = "Specifies which VMDK subformat to use.";
};
vmCompat6 = lib.mkOption {
type = lib.types.bool;
default = false;
example = true;
description = "Create a VMDK version 6 image (instead of version 4).";
};
};
};
config = {
system.nixos.tags = [ "vmware" ];
image.extension = "vmdk";
system.build.image = config.system.build.vmwareImage;
system.build.vmwareImage = import ../../lib/make-disk-image.nix {
name = cfg.vmDerivationName;
baseName = config.image.baseName;
postVM = ''
${pkgs.vmTools.qemu}/bin/qemu-img convert -f raw -o compat6=${boolToStr cfg.vmCompat6},subformat=${cfg.vmSubformat} -O vmdk $diskImage $out/${config.image.fileName}
rm $diskImage
'';
format = "raw";
diskSize = cfg.baseImageSize;
partitionTableType = "efi";
inherit config lib pkgs;
};
fileSystems."/" = {
device = "/dev/disk/by-label/nixos";
autoResize = true;
fsType = "ext4";
};
fileSystems."/boot" = {
device = "/dev/disk/by-label/ESP";
fsType = "vfat";
};
boot.growPartition = true;
boot.loader.grub = {
device = "nodev";
efiSupport = true;
efiInstallAsRemovable = true;
};
virtualisation.vmware.guest.enable = true;
};
}

View File

@@ -0,0 +1,383 @@
{
config,
lib,
pkgs,
...
}:
with lib;
let
cfg = config.services.waagent;
# Format for waagent.conf
settingsFormat = {
type =
with types;
let
singleAtom =
(nullOr (oneOf [
bool
str
int
float
]))
// {
description = "atom (bool, string, int or float) or null";
};
atom = either singleAtom (listOf singleAtom) // {
description = singleAtom.description + " or a list of them";
};
in
attrsOf (
either atom (attrsOf atom)
// {
description = atom.description + " or an attribute set of them";
}
);
generate =
name: value:
let
# Transform non-attribute values
transform =
x:
# Transform bool to "y" or "n"
if (isBool x) then
(if x then "y" else "n")
# Concatenate list items with comma
else if (isList x) then
concatStringsSep "," (map transform x)
else
toString x;
# Convert to format of waagent.conf
recurse =
path: value:
if builtins.isAttrs value then
pipe value [
(mapAttrsToList (k: v: recurse (path ++ [ k ]) v))
concatLists
]
else
[
{
name = concatStringsSep "." path;
inherit value;
}
];
convert =
attrs:
pipe (recurse [ ] attrs) [
# Filter out null values and empty lists
(filter (kv: kv.value != null && kv.value != [ ]))
# Transform to Key=Value form, then concatenate
(map (kv: "${kv.name}=${transform kv.value}"))
(concatStringsSep "\n")
];
in
pkgs.writeText name (convert value);
};
settingsType = types.submodule {
freeformType = settingsFormat.type;
options = {
Provisioning = {
Enable = mkOption {
type = types.bool;
default = !config.services.cloud-init.enable;
defaultText = literalExpression "!config.services.cloud-init.enable";
description = ''
Whether to enable provisioning functionality in the agent.
If provisioning is disabled, SSH host and user keys in the image are preserved
and configuration in the Azure provisioning API is ignored.
Set to `false` if cloud-init is used for provisioning tasks.
'';
};
Agent = mkOption {
type = types.enum [
"auto"
"waagent"
"cloud-init"
"disabled"
];
default = "auto";
description = ''
Which provisioning agent to use.
'';
};
};
ResourceDisk = {
Format = mkOption {
type = types.bool;
default = false;
description = ''
If set to `true`, waagent formats and mounts the resource disk that the platform provides,
unless the file system type in `ResourceDisk.FileSystem` is set to `ntfs`.
The agent makes a single Linux partition (ID 83) available on the disk.
This partition isn't formatted if it can be successfully mounted.
This configuration has no effect if resource disk is managed by cloud-init.
'';
};
FileSystem = mkOption {
type = types.str;
default = "ext4";
description = ''
The file system type for the resource disk.
If the string is `X`, then `mkfs.X` should be present in the environment.
You can add additional filesystem packages using `services.waagent.extraPackages`.
This configuration has no effect if resource disk is managed by cloud-init.
'';
};
MountPoint = mkOption {
type = types.str;
default = "/mnt/resource";
description = ''
This option specifies the path at which the resource disk is mounted.
The resource disk is a temporary disk and might be emptied when the VM is deprovisioned.
This configuration has no effect if resource disk is managed by cloud-init.
'';
};
MountOptions = mkOption {
type = with types; listOf str;
default = [ ];
example = [
"nodev"
"nosuid"
];
description = ''
This option specifies disk mount options to be passed to the `mount -o` command.
For more information, see the {manpage}`mount(8)` manual page.
'';
};
EnableSwap = mkOption {
type = types.bool;
default = false;
description = ''
If enabled, the agent creates a swap file (`/swapfile`) on the resource disk
and adds it to the system swap space.
This configuration has no effect if resource disk is managed by cloud-init.
'';
};
SwapSizeMB = mkOption {
type = types.int;
default = 0;
description = ''
Specifies the size of the swap file in MiB (1024×1024 bytes).
This configuration has no effect if resource disk is managed by cloud-init.
'';
};
};
Logs.Verbose = lib.mkOption {
type = types.bool;
default = false;
description = ''
If you set this option, log verbosity is boosted.
Waagent logs to `/var/log/waagent.log` and uses the system logrotate functionality to rotate logs.
'';
};
OS = {
EnableRDMA = lib.mkOption {
type = types.bool;
default = false;
description = ''
If enabled, the agent attempts to install and then load an RDMA kernel driver
that matches the version of the firmware on the underlying hardware.
'';
};
RootDeviceScsiTimeout = lib.mkOption {
type = types.nullOr types.int;
default = 300;
description = ''
Configures the SCSI timeout in seconds on the OS disk and data drives.
If set to `null`, the system defaults are used.
'';
};
};
HttpProxy = {
Host = lib.mkOption {
type = types.nullOr types.str;
default = null;
description = ''
If you set http proxy, waagent will use is proxy to access the Internet.
'';
};
Port = lib.mkOption {
type = types.nullOr types.port;
default = null;
description = ''
If you set http proxy, waagent will use this proxy to access the Internet.
'';
};
};
AutoUpdate.UpdateToLatestVersion = lib.mkOption {
type = types.bool;
default = false;
description = ''
Whether or not to enable auto-update of the Extension Handler.
'';
};
};
};
in
{
options.services.waagent = {
enable = lib.mkEnableOption "Windows Azure Linux Agent";
package = lib.mkPackageOption pkgs "waagent" { };
extraPackages = lib.mkOption {
default = [ ];
description = ''
Additional packages to add to the waagent {env}`PATH`.
'';
example = lib.literalExpression "[ pkgs.powershell ]";
type = lib.types.listOf lib.types.package;
};
settings = lib.mkOption {
type = settingsType;
default = { };
description = ''
The waagent.conf configuration, see <https://learn.microsoft.com/en-us/azure/virtual-machines/extensions/agent-linux> for documentation.
'';
};
};
config = lib.mkIf cfg.enable {
assertions = [
{
assertion = (cfg.settings.HttpProxy.Host != null) -> (cfg.settings.HttpProxy.Port != null);
message = "Option services.waagent.settings.HttpProxy.Port must be set if services.waagent.settings.HttpProxy.Host is set.";
}
];
boot.initrd.kernelModules = [ "ata_piix" ];
networking.firewall.allowedUDPPorts = [ 68 ];
services.udev.packages = with pkgs; [ waagent ];
boot.initrd.services.udev = with pkgs; {
# Provide waagent-shipped udev rules in initrd too.
packages = [ waagent ];
# udev rules shell out to chmod, cut and readlink, which are all
# provided by pkgs.coreutils, which is in services.udev.path, but not
# boot.initrd.services.udev.binPackages.
binPackages = [ coreutils ];
};
networking.dhcpcd.persistent = true;
services.logrotate = {
enable = true;
settings."/var/log/waagent.log" = {
compress = true;
frequency = "monthly";
rotate = 6;
};
};
# Write settings to /etc/waagent.conf
environment.etc."waagent.conf".source = settingsFormat.generate "waagent.conf" cfg.settings;
systemd.targets.provisioned = {
description = "Services Requiring Azure VM provisioning to have finished";
};
systemd.services.consume-hypervisor-entropy = {
description = "Consume entropy in ACPI table provided by Hyper-V";
wantedBy = [
"sshd.service"
"waagent.service"
];
before = [
"sshd.service"
"waagent.service"
];
path = [ pkgs.coreutils ];
script = ''
echo "Fetching entropy..."
cat /sys/firmware/acpi/tables/OEM0 > /dev/random
'';
serviceConfig.Type = "oneshot";
serviceConfig.RemainAfterExit = true;
serviceConfig.StandardError = "journal+console";
serviceConfig.StandardOutput = "journal+console";
};
systemd.services.waagent = {
wantedBy = [ "multi-user.target" ];
after = [
"network-online.target"
]
++ lib.optionals config.services.cloud-init.enable [ "cloud-init.service" ];
wants = [
"network-online.target"
"sshd.service"
"sshd-keygen.service"
];
path =
with pkgs;
[
e2fsprogs
bash
findutils
gnugrep
gnused
iproute2
iptables
openssh
openssl
parted
# for hostname
net-tools
# for pidof
procps
# for useradd, usermod
shadow
util-linux # for (u)mount, fdisk, sfdisk, mkswap
# waagent's Microsoft.CPlat.Core.RunCommandLinux needs lsof
lsof
]
++ cfg.extraPackages;
description = "Windows Azure Agent Service";
unitConfig.ConditionPathExists = "/etc/waagent.conf";
serviceConfig = {
ExecStart = "${lib.getExe cfg.package} -daemon";
Type = "simple";
Restart = "always";
Slice = "azure.slice";
CPUAccounting = true;
MemoryAccounting = true;
};
};
# waagent will generate files under /etc/sudoers.d during provisioning
security.sudo.extraConfig = ''
#includedir /etc/sudoers.d
'';
};
}

View File

@@ -0,0 +1,78 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.virtualisation.waydroid;
kCfg = config.lib.kernelConfig;
waydroidGbinderConf = pkgs.writeText "waydroid.conf" ''
[Protocol]
/dev/binder = aidl2
/dev/vndbinder = aidl2
/dev/hwbinder = hidl
[ServiceManager]
/dev/binder = aidl2
/dev/vndbinder = aidl2
/dev/hwbinder = hidl
'';
in
{
options.virtualisation.waydroid = {
enable = lib.mkEnableOption "Waydroid";
package = lib.mkPackageOption pkgs "waydroid" { };
};
config = lib.mkIf cfg.enable {
assertions = lib.singleton {
assertion = lib.versionAtLeast (lib.getVersion config.boot.kernelPackages.kernel) "4.18";
message = "Waydroid needs user namespace support to work properly";
};
system.requiredKernelConfig = [
(kCfg.isEnabled "ANDROID_BINDER_IPC")
(kCfg.isEnabled "ANDROID_BINDERFS")
(kCfg.isEnabled "MEMFD_CREATE")
];
/*
NOTE: we always enable this flag even if CONFIG_PSI_DEFAULT_DISABLED is not on
as reading the kernel config is not always possible and on kernels where it's
already on it will be no-op
*/
boot.kernelParams = [ "psi=1" ];
environment.etc."gbinder.d/waydroid.conf".source = waydroidGbinderConf;
environment.systemPackages = [ cfg.package ];
networking.firewall.trustedInterfaces = [ "waydroid0" ];
virtualisation.lxc.enable = true;
systemd.services.waydroid-container = {
description = "Waydroid Container";
wantedBy = [ "multi-user.target" ];
serviceConfig = {
Type = "dbus";
UMask = "0022";
ExecStart = "${cfg.package}/bin/waydroid -w container start";
BusName = "id.waydro.Container";
};
};
systemd.tmpfiles.rules = [
"d /var/lib/misc 0755 root root -" # for dnsmasq.leases
];
services.dbus.packages = [ cfg.package ];
};
}

View File

@@ -0,0 +1,66 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.xe-guest-utilities;
in
{
options = {
services.xe-guest-utilities = {
enable = lib.mkEnableOption "the XenServer guest utilities daemon";
};
};
config = lib.mkIf cfg.enable {
services.udev.packages = [ pkgs.xe-guest-utilities ];
systemd.tmpfiles.rules = [ "d /run/xenstored 0755 - - -" ];
systemd.services.xe-daemon = {
description = "xen daemon file";
wantedBy = [ "multi-user.target" ];
after = [ "xe-linux-distribution.service" ];
requires = [ "proc-xen.mount" ];
path = [
pkgs.coreutils
pkgs.iproute2
];
serviceConfig = {
PIDFile = "/run/xe-daemon.pid";
ExecStart = "${pkgs.xe-guest-utilities}/bin/xe-daemon -p /run/xe-daemon.pid";
ExecStop = "${pkgs.procps}/bin/pkill -TERM -F /run/xe-daemon.pid";
};
};
systemd.services.xe-linux-distribution = {
description = "xen linux distribution service";
wantedBy = [ "multi-user.target" ];
before = [ "xend.service" ];
path = [
pkgs.xe-guest-utilities
pkgs.coreutils
pkgs.gawk
pkgs.gnused
];
serviceConfig = {
Type = "simple";
RemainAfterExit = "yes";
ExecStart = "${pkgs.xe-guest-utilities}/bin/xe-linux-distribution /var/cache/xe-linux-distribution";
};
};
systemd.mounts = [
{
description = "Mount /proc/xen files";
what = "xenfs";
where = "/proc/xen";
type = "xenfs";
unitConfig = {
ConditionPathExists = "/proc/xen";
RefuseManualStop = "true";
};
}
];
};
}

View File

@@ -0,0 +1,187 @@
# This script is called by ./xen-dom0.nix to create the Xen boot entries.
# shellcheck shell=bash
export LC_ALL=C
# Handle input argument and exit if the flag is invalid. See virtualisation.xen.boot.builderVerbosity below.
[[ $# -ne 1 ]] && echo -e "\e[1;31merror:\e[0m xenBootBuilder must be called with exactly one verbosity argument. See the \e[1;34mvirtualisation.xen.boot.builderVerbosity\e[0m option." && exit 1
case "$1" in
"quiet") true ;;
"default" | "info") echo -n "Installing Xen Project Hypervisor boot entries..." ;;
"debug") echo -e "\e[1;34mxenBootBuilder:\e[0m called with the '$1' flag" ;;
*)
echo -e "\e[1;31merror:\e[0m xenBootBuilder was called with an invalid argument. See the \e[1;34mvirtualisation.xen.boot.builderVerbosity\e[0m option."
exit 2
;;
esac
# Get the current Xen generations and store them in an array. This will be used
# for displaying the diff later, if xenBootBuilder was called with `info`.
# We also delete the current Xen entries here, as they'll be rebuilt later if
# the corresponding NixOS generation still exists.
mapfile -t preGenerations < <(find "$efiMountPoint"/loader/entries -type f -name 'xen-*.conf' | sort -V | sed 's_/loader/entries/nixos_/loader/entries/xen_g;s_^.*/xen_xen_g;s_.conf$__g')
if [ "$1" = "debug" ]; then
if ((${#preGenerations[@]} == 0)); then
echo -e "\e[1;34mxenBootBuilder:\e[0m no previous Xen entries."
else
echo -e "\e[1;34mxenBootBuilder:\e[0m deleting the following stale xen entries:" && for debugGen in "${preGenerations[@]}"; do echo " - $debugGen"; done
fi
fi
# Cleanup all Xen entries.
rm -f "$efiMountPoint"/{loader/entries/xen-*.conf,efi/nixos/xen-*.efi}
# Main array for storing which generations exist in $efiMountPoint after
# systemd-boot-builder.py builds the main entries.
mapfile -t gens < <(find "$efiMountPoint"/loader/entries -type f -name 'nixos-*.conf' | sort -V)
[ "$1" = "debug" ] && echo -e "\e[1;34mxenBootBuilder:\e[0m found the following NixOS boot entries:" && for debugGen in "${gens[@]}"; do echo " - $debugGen"; done
# This is the main loop that installs the Xen entries.
for gen in "${gens[@]}"; do
# We discover the path to Bootspec through the init attribute in the entries,
# as it is equivalent to $toplevel/init.
bootspecFile="$(sed -nr 's/^options init=(.*)\/init.*$/\1/p' "$gen")/boot.json"
[ "$1" = "debug" ] && echo -e "\e[1;34mxenBootBuilder:\e[0m processing bootspec file $bootspecFile"
# We do nothing if the Bootspec for the current $gen does not contain the Xen
# extension, which is added as a configuration attribute below.
# We determine this by checking for the v1 or v2 bootspec extension,
# and setting the appropriate attributes based on version
xenSpecVer=""
xenParamVar=""
xenEfiPath=""
if grep -sq '"org.xenproject.bootspec.v1"' "$bootspecFile"; then
xenSpecVer="v1"
xenParamVar="xenParams"
xenEfiPath="xen"
fi
# We prefer the v2 extension, so if both are present somehow,
# we will use the v2 attributes
if grep -sq '"org.xenproject.bootspec.v2"' "$bootspecFile"; then
xenSpecVer="v2"
xenParamVar="params"
xenEfiPath="efiPath"
fi
# Check for a valid Xen spec being detected
if [ -n "$xenSpecVer" ]; then
[ "$1" = "debug" ] && echo -e " \e[1;32msuccess:\e[0m found $xenSpecVer Xen entries in $gen."
# TODO: Support DeviceTree booting. Xen has some special handling for DeviceTree
# attributes, which will need to be translated in a boot script similar to this
# one. Having a DeviceTree entry is rare, and it is not always required for a
# successful boot, so we don't fail here, only warn with `debug`.
if grep -sq '"devicetree"' "$bootspecFile"; then
echo -e "\n\e[1;33mwarning:\e[0m $gen has a \e[1;34morg.nixos.systemd-boot.devicetree\e[0m Bootspec entry. Xen currently does not support DeviceTree, so this value will be ignored in the Xen boot entries, which may cause them to \e[1;31mfail to boot\e[0m."
else
[ "$1" = "debug" ] && echo -e "\e[1;34mxenBootBuilder:\e[0m no DeviceTree entries found in $gen."
fi
# Prepare required attributes for `xen.cfg/xen.conf`. It inherits the name of
# the corresponding nixos generation, substituting `nixos` with `xen`:
# `xen-$profile-generation-$number-specialisation-$specialisation.{cfg,conf}`
xenGen=$(echo "$gen" | sed 's_/loader/entries/nixos_/loader/entries/xen_g;s_^.*/xen_xen_g;s_.conf$__g')
bootParams=$(jq -re ".\"org.xenproject.bootspec.$xenSpecVer\".$xenParamVar | join(\" \")" "$bootspecFile")
kernel=$(jq -re '."org.nixos.bootspec.v1".kernel | sub("^/nix/store/"; "") | sub("/bzImage"; "-bzImage.efi")' "$bootspecFile")
kernelParams=$(jq -re '."org.nixos.bootspec.v1".kernelParams | join(" ")' "$bootspecFile")
initrd=$(jq -re '."org.nixos.bootspec.v1".initrd | sub("^/nix/store/"; "") | sub("/initrd"; "-initrd.efi")' "$bootspecFile")
init=$(jq -re '."org.nixos.bootspec.v1".init' "$bootspecFile")
title=$(sed -nr 's/^title (.*)$/\1/p' "$gen")
version=$(sed -nr 's/^version (.*)$/\1/p' "$gen")
machineID=$(sed -nr 's/^machine-id (.*)$/\1/p' "$gen")
sortKey=$(sed -nr 's/^sort-key (.*)$/\1/p' "$gen")
# Write `xen.cfg` to a temporary location prior to UKI creation.
tmpCfg=$(mktemp)
[ "$1" = "debug" ] && echo -ne "\e[1;34mxenBootBuilder:\e[0m writing $xenGen.cfg to temporary file..."
cat > "$tmpCfg" << EOF
[global]
default=xen
[xen]
options=$bootParams
kernel=$kernel init=$init $kernelParams
ramdisk=$initrd
EOF
[ "$1" = "debug" ] && echo -e "done."
# Create Xen UKI for $generation. Most of this is lifted from
# https://xenbits.xenproject.org/docs/unstable/misc/efi.html.
[ "$1" = "debug" ] && echo -e "\e[1;34mxenBootBuilder:\e[0m making Xen UKI..."
xenEfi=$(jq -re ".\"org.xenproject.bootspec.$xenSpecVer\".$xenEfiPath" "$bootspecFile")
finalSection=$(objdump --header --wide "$xenEfi" | tail -n +6 | sort --key="4,4" | tail -n 1 | grep -Eo '\.[a-z]*')
padding=$(objdump --header --section="$finalSection" "$xenEfi" | awk -v section="$finalSection" '$0 ~ section { printf("0x%016x\n", and(strtonum("0x"$3) + strtonum("0x"$4) + 0xfff, compl(0xfff)))};')
[ "$1" = "debug" ] && echo " - padding: $padding"
objcopy \
--add-section .config="$tmpCfg" \
--change-section-vma .config="$padding" \
"$xenEfi" \
"$efiMountPoint"/EFI/nixos/"$xenGen".efi
[ "$1" = "debug" ] && echo -e " - \e[1;32msuccessfully built\e[0m $xenGen.efi"
rm -f "$tmpCfg"
# Write `xen.conf`.
[ "$1" = "debug" ] && echo -ne "\e[1;34mxenBootBuilder:\e[0m writing $xenGen.conf to EFI System Partition..."
cat > "$efiMountPoint"/loader/entries/"$xenGen".conf << EOF
title $title (with Xen Hypervisor)
version $version
efi /EFI/nixos/$xenGen.efi
machine-id $machineID
sort-key $sortKey
EOF
[ "$1" = "debug" ] && echo -e "done."
# Sometimes, garbage collection weirdness causes a generation to still exist in
# the loader entries, but its Bootspec file was deleted. We consider such a
# generation to be invalid, but we don't write extra code to handle this
# situation, as supressing grep's error messages above is quite enough, and the
# error message below is still technically correct, as no Xen can be found in
# something that does not exist.
else
[ "$1" = "debug" ] && echo -e " \e[1;33mwarning:\e[0m \e[1;31mno Xen found\e[0m in $gen."
fi
done
# Counterpart to the preGenerations array above. We use it to diff the
# generations created/deleted when callled with the `info` argument.
mapfile -t postGenerations < <(find "$efiMountPoint"/loader/entries -type f -name 'xen-*.conf' | sort -V | sed 's_/loader/entries/nixos_/loader/entries/xen_g;s_^.*/xen_xen_g;s_.conf$__g')
# In the event the script does nothing, guide the user to debug, as it'll only
# ever run when Xen is enabled, and it makes no sense to enable Xen and not have
# any hypervisor boot entries.
if ((${#postGenerations[@]} == 0)); then
case "$1" in
"default" | "info") echo "none found." && echo -e "If you believe this is an error, set the \e[1;34mvirtualisation.xen.boot.builderVerbosity\e[0m option to \e[1;34m\"debug\"\e[0m and rebuild to print debug logs." ;;
"debug") echo -e "\e[1;34mxenBootBuilder:\e[0m wrote \e[1;31mno generations\e[0m. Most likely, there were no generations with a valid \e[1;34morg.xenproject.bootspec.v1\e[0m or \e[1;34morg.xenproject.bootspec.v2\e[0m entry." ;;
esac
# If the script is successful, change the default boot, say "done.", write a
# diff, or print the total files written, depending on the argument this script
# was called with. We use some dumb dependencies here, like `diff` or `bat` for
# colourisation, but they're only included with the `info` argument.
#
# It's also fine to change the default here, as this runs after the
# `systemd-boot-builder.py` script, which overwrites the file, and this script
# does not run after an user disables the Xen module.
else
sed --in-place 's/^default nixos-/default xen-/g' "$efiMountPoint"/loader/loader.conf
case "$1" in
"default" | "info") echo "done." ;;
"debug") echo -e "\e[1;34mxenBootBuilder:\e[0m \e[1;32msuccessfully wrote\e[0m the following generations:" && for debugGen in "${postGenerations[@]}"; do echo " - $debugGen"; done ;;
esac
if [ "$1" = "info" ]; then
if [[ ${#preGenerations[@]} == "${#postGenerations[@]}" ]]; then
echo -e "\e[1;33mNo Change:\e[0m Xen Project Hypervisor boot entries were refreshed, but their contents are identical."
else
echo -e "\e[1;32mSuccess:\e[0m Changed the following boot entries:"
# We briefly unset errexit and pipefail here, as GNU diff has no option to not fail when files differ.
set +o errexit
set +o pipefail
diff <(echo "${preGenerations[*]}" | tr ' ' '\n') <(echo "${postGenerations[*]}" | tr ' ' '\n') -U 0 | grep --invert-match --extended-regexp '^(@@|---|\+\+\+).*' | sed '1{/^-$/d}' | bat --language diff --theme ansi --paging=never --plain
true
set -o errexit
set -o pipefail
fi
fi
fi

View File

@@ -0,0 +1,943 @@
# Xen Project Hypervisor (Dom0) support.
{
config,
lib,
pkgs,
...
}:
let
inherit (builtins) readFile;
inherit (lib.meta) hiPrio;
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.boot.builderVerbosity == "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"
]
)
(mkRenamedOptionModule
[
"virtualisation"
"xen"
"efi"
"bootBuilderVerbosity"
]
[
"virtualisation"
"xen"
"boot"
"builderVerbosity"
]
)
(mkRenamedOptionModule
[
"virtualisation"
"xen"
"bootParams"
]
[
"virtualisation"
"xen"
"boot"
"params"
]
)
(mkRenamedOptionModule
[
"virtualisation"
"xen"
"efi"
"path"
]
[
"virtualisation"
"xen"
"boot"
"efi"
"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.";
};
};
boot = {
params = 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.
'';
};
builderVerbosity = mkOption {
type = enum [
"default"
"info"
"debug"
"quiet"
];
default = "default";
example = "info";
description = ''
The 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.
'';
};
bios = {
path = mkOption {
type = path;
default = "${cfg.package.boot}/${cfg.package.multiboot}";
defaultText = literalExpression "\${config.virtualisation.xen.package.boot}/\${config.virtualisation.xen.package.multiboot}";
example = literalExpression "\${config.virtualisation.xen.package}/boot/xen-\${config.virtualisation.xen.package.version}";
description = ''
Path to the Xen `multiboot` binary used for BIOS booting.
Unless you're building your own Xen derivation, you should leave this
option as the default value.
'';
};
};
efi = {
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
|| config.boot.loader.limine.enable;
message = "Xen only supports booting on systemd-boot, Lanzaboote or Limine.";
}
{
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.boot.params =
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 dom0 kernel paths and access the `cfg.boot.params` option.
bootspec.extensions = {
# Bootspec extension v1 is deprecated, and will be removed in 26.05
# It is present for backwards compatibility
"org.xenproject.bootspec.v1" = {
xen = cfg.boot.efi.path;
xenParams = cfg.boot.params;
};
# Bootspec extension v2 includes more detail,
# including supporting multiboot, and is the current supported
# bootspec extension
"org.xenproject.bootspec.v2" = {
efiPath = cfg.boot.efi.path;
multibootPath = cfg.boot.bios.path;
version = cfg.package.version;
params = cfg.boot.params;
};
};
# See the `xenBootBuilder` script in the main `let...in` statement of this file.
loader.systemd-boot.extraInstallCommands = ''
${getExe xenBootBuilder} ${cfg.boot.builderVerbosity}
'';
};
# 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
(hiPrio 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;
}

View File

@@ -0,0 +1,23 @@
# Common configuration for Xen DomU NixOS virtual machines.
{ ... }:
{
boot.loader.grub.device = "nodev";
boot.initrd.kernelModules = [
"xen-blkfront"
"xen-tpmfront"
"xen-kbdfront"
"xen-fbfront"
"xen-netfront"
"xen-pcifront"
"xen-scsifront"
];
# Send syslog messages to the Xen console.
services.syslogd.tty = "hvc0";
# Don't run ntpd, since we should get the correct time from Dom0.
services.timesyncd.enable = false;
}