Incus手册

什么是Incus

2023年7月5日,Canonical宣布全面接管LXD。原先的LXD工作组对此相当不满,因此Fork并重新制作了移除掉所有Canonical相关内容的Incus。

抛开这些历史问题不谈,Incus的本质是:

一套为容器和虚拟机提供统一的集群化管理接口的系统

架构

Incus由三个部分组成:

  • 服务端:即incusd,提供基于UNIX套接字或REST API的接口供客户端访问。
  • 客户端:即incus,提供访问服务端进行集群管理的功能。
  • 镜像仓库:提供容器镜像或虚拟机磁盘的下载仓库,即https://images.linuxcontainers.org

Incus管理的对象包括虚拟机和系统容器(相对于应用容器而言,即LXC),它们的区别如下表:

虚拟机 系统容器 应用容器
使用独立内核 使用宿主机内核 使用宿主机内核
使用了硬件虚拟化功能(Intel VT-X或AMD-V) 只使用内核功能 只使用内核功能
使用资源最多 使用资源较少 使用资源较少
有完整的引导过程,因此启动较慢 只有用户空间的init过程,启动较快 完全没有引导和init过程,启动最快
典型代表是Qemu-KVM 典型代表是LXC 典型代表是Docker

vm-sc-ac.jpeg

安装

Incus仅支持使用systemd的发行版

目前Incus服务端在Debian 12+的官方源中进行提供:

1
apt install incus

Alpine 3.19+:

1
2
3
4
5
6
apk add incus incus-client

# 你还需要这些可选依赖
apk add nftables sgdisk incus-agent aavmf qemu-system-$(arch) qemu-modules incus-vm
# Alpine需要手动加载TUN模块
echo 'tun' >> /etc/modules

Fedora(尚未进入官方源):

1
2
3
dnf install 'dnf-command(copr)'
dnf copr enable ganto/lxc4
dnf install incus

客户端不限平台,二进制程序可以在Github页面直接下载。

安装Incus后,用户的权限通过两个组管理:

  • 被添加到incus附加组的用户有权使用/var/lib/incus/unix.sock套接字以对Incus进行管理。
  • 被添加到incus-admin附加组的用户有权使用incus-user.socket套接字以创建自己的Project使用。

安装Incus后,存在5个systemd单元:

  • incus.serviceincusd服务,它不应该被直接启用。
  • incus.socket/var/lib/incus/unix.sock套接字,它会激活incusd服务。
  • incus-user.service:Incus用户管理服务,它不应该被直接启用。
  • incus-user.socket:Incus用户管理服务的套接字。
  • incus-startup.service:执行incusd activateifneeded,它的功能是,在系统启动时检查是否存在boot.autostart=true的实例,如果存在,则对incus.socket发送一个请求,使得incus.service启动。

初始化

执行:

1
incus admin init

命令进行初始化,初始化过程包括以下几个问题:

  1. 是否启用集群模式?默认为否。
    • 如果选择是,那么会询问使用的IP、以及是否加入已存在的集群。
    • 如果选择加入已存在的集群,那么会要求提供集群Token。
    • 如果选择不加入,那么会询问新建的集群名称。
  2. 是否创建一个存储池?默认为是。
    • 存储池默认名称为default(可以修改),使用dir(或可以选择btrfs)存储引擎。
  3. 是否自动创建网桥?默认为是。
    • 自动创建的网桥是一个独立网桥。用户需要手动设置网桥使用的IPv4/IPv6子网(也可以让Incus自动选择)。
    • 如果选择是,还会要求提供网桥的设备名称,默认为incusbr0
    • 如果选择否,那么会要求用户选择一个已存在的网桥或网络接口。可以创建一个二层网桥,或是使用一个物理接口,不过如果是物理接口的话,会被Incus占用,不能再被宿主机使用。
  4. 是否在网络上暴露该服务器(即启用REST API)?默认为否。
  5. 是否定期更新镜像?默认为是。
  6. 是否打印一个Preseed自动化YAML?默认为否。

除了手动初始化,还有三种自动初始化方式:

  • --minimal:全部使用默认选项,自动化初始化。
  • --auto:从命令行选项获取配置,自动化初始化。可用配置有:
    • --network-address:绑定的IP。
    • --network-port:绑定的端口号,默认为8443。
    • --storage-backend=dir|lvm|btrfs|zfs|ceph:使用的存储引擎。
    • --storage-create-device=/dev/设备:使用device引擎并指定设备存储。
    • --storage-create-loop=大小:使用loop引擎并指定大小。
    • --storage-pool='名称':指定存储池名称。
  • --preseed:从标准输入获取Preseed YAML配置文件,自动化初始化。

网络

检查网络

列出可用网络:

1
incus network list
  • 非Incus管理的网络也会显示,但是MANAGED列会显示为NO

检查网络:

1
incus network show|info 网络名

创建网络

Incus可以创建一个用于虚拟机/容器的网络:

1
incus network create 网络名称 [参数=值] --type=bridge|macvlan|sriov|physical|ovn [--target=节点]
  • bridge:使用一个Independent Bridge进行交换的网络。
    • 在可以使用nftables的情况下,Incus使用nftables创建防火墙转发规则,否则使用xtables。具体的防火墙后端可以通过incus info | grep -F 'firewall_driver'进行检查。
  • macvlan:在网卡上创建MACVLan网络,使得实例可以共享该网卡。
  • sriov:使用SR-IOV协议,使得实例可以共享该网卡。
  • physical:直接使用物理网卡,注意这会导致该网卡在宿主机上不可用。
  • ovn:使用OVN网络。

bridgeovn常用参数有:

  • ipv4.address:子网的CIDR,默认为auto
  • ipv4.dhcp:是否启用(Dnsmasq实现的)DHCP,默认为true
  • ipv4.nat:是否启用IPv4的NAT规则,默认为false(当地址为auto时,是true)。
  • ipv4.firewall:是否添加IPv4的防火墙放行规则,默认为true
  • ipv4.routing:是否尝试设置net.ipv4.ip_forward = 1,并放行进出网桥的所有IPv4流量,默认为true
  • ipv6.address:子网的CIDR,默认为auto
  • ipv6.dhcp:是否启用(Dnsmasq实现的)DHCP(以发送额外信息),默认为true
  • ipv6.dhcp.stateful:是否使用DHCP分配IPv6地址,默认为true
  • ipv6.nat:是否启用IPv6的NAT规则,默认为false(当地址为auto时,是true)。
  • ipv6.firewall:是否添加IPv6的防火墙放行规则,默认为true
  • ipv6.routing:是否尝试设置net.ipv6.conf.all.accept_ra = 2(如果它的值是0,则不设置),并尝试设置net.ipv6.conf.all.autoconf = 1,并放行进出网桥的所有IPv6流量,默认为true
  • dns.domain:实例的DNS域名,默认为incus

bridge还有以下参数可用:

  • dns.mode:DNS模式,none不添加DNS记录,managed由Incus负责添加记录,dynamic由实例负责添加记录。
  • bridge.external_interfaces:网桥附加的物理网络,这可以用于创建Host-shared bridge。
    • 注意,在进行该操作前你需要配置:ipv4.address=none ipv6.address=none ipv4.dhcp=false ipv6.dhcp=false ipv4.nat=false ipv6.nat=false ipv4.firewall=false ipv6.firewall=false dns.mode=none

macvlan参数有:

  • parent:使用的父网卡。
  • mode:MACVLan的工作模式,可选值有bridgevepaprivatepassthru,默认为bridge

sriovphysical参数有:

  • parent:使用的父网卡。

在集群中创建网络时,需要在每个节点上都执行一次创建命令,最后再不带--target选项执行一次创建命令。

MACVLan问题

众所周知MACVLan创建的网络接口无法直接与宿主机通信,一个解决方案是手动创建一个专门与宿主机通信的Host-only网桥并为其启用DHCP。

执行:

1
incus network create incusbr1 ipv4.nat=false ipv6.nat=false ipv4.firewall=false ipv6.firewall=false ipv4.routing=false ipv6.routing=false --type=bridge

然后在Profile中添加设备:

1
2
3
4
5
devices:
eth1:
name: eth1
network: incusbr1
type: nic

即可。

防止伪造

默认情况下Incus网桥设备接受所有二层流量。为了提高安全性,可以进行以下设置:

  • incus network set bridge网络名称 security.mac_filtering=true:禁止实例伪造MAC地址。
  • incus network set bridge网络名称 security.ipv4_filtering=true:阻止任何包含伪造源地址的数据包,丢弃任何不是ARP或IPv4的以太网帧。
  • incus network set bridge网络名称 security.ipv6_filtering=true:阻止路由器通告、阻止任何包含伪造源地址的数据包,丢弃任何不是ARP或IPv6的以太网帧。

自定义防火墙

如果你安装了其他的防火墙前端,那么需要手动为防火墙配置放行规则(转发规则不必手动设置),首先禁用bridge网络内置的防火墙:

1
incus network set bridge网络名称 ipv4.firewall=false ipv6.firewall=false

如果是firewalld

1
2
firewall-cmd --zone=trusted --change-interface=bridge网络设备 --permanent
firewall-cmd --reload

如果是UFW:

1
2
3
ufw allow in on bridge网络设备
ufw route allow in on bridge网络设备
ufw route allow out on bridge网络设备

如果不配置,那么由于Incus网桥的入站和出站数据包都被拦截,实例将无法通过DHCP协议获取IP。

接入网络

执行以下命令将虚拟机/容器实例接入网络:

1
incus network attach 网络名 [服务器名:]实例名 实例中的接口名

实际上这是这条命令的别名:

1
incus config device add [服务器名:]实例名 实例中的接口名 nic network=网络名

实际上,Incus也支持为实例直接创建接口,这样的网络接口种类稍微多一些:

1
incus config device add [服务器名:]实例名 实例中的接口名 nic nictype=bridged|macvlan|sriov|physical|ipvlan|p2p|routed|ovn parent=接口名
  • bridged:接入一个网桥。
    • 要么有network参数,要么有parent参数。
  • macvlan:使用MACVLan接入宿主机的一个网卡。
    • 要么有network参数,要么有parent参数。
  • sriov:使用SR-IOV接入宿主机的一个网卡。
    • 要么有network参数,要么有parent参数。
  • ovn:接入一个OVN网络。
    • 必须有network参数。
  • physical:直接使用物理网卡,注意这会导致该网卡在宿主机上不可用。
  • ipvlan:在网卡上创建IPVLan网络,使得实例可以共享该网卡。需要parent=父网卡参数。
  • p2p:创建一个Veth对(或者对于虚拟机来说是TAP对),一头连接实例,另一头连接宿主机。宿主机和实例上的IP和路由需要手动配置。
    • host_name:宿主机Veth设备的名称。
    • name:实例内Veth设备的名称。
  • routed:创建一个Veth对(或者对于虚拟机来说是TAP对),一头连接实例,另一头连接宿主机。同时在宿主机中配置静态路由,并代理ARP/NDP条目,以允许实例连接到外部网络。注意,该设备必须手动配置IP,也就是说,必须存在ipv4.address参数。而且还必须把该设备设为实例内的网关。
    • host_name:宿主机Veth设备的名称。
    • name:实例内Veth设备的名称。
    • ipv4|6.address:Veth设备IP。
    • ipv4|6.gateway:是否自动添加默认IPv4网关,可选值为autonone
    • ipv4|6.host_address:主机Veth接口的IP。

这种方式不是推荐方式,唯一实用的场景是手动创建一个二层网桥并使用bridged模式让实例接入,也就是说:

  1. 在宿主机上创建一个Host-shared网桥,如br0
  2. 执行incus config device add|set [服务器名:]实例名 eth0 nic nictype=bridged parent=br0 name=eth0让实例接入。
  • 这种场景现在应该被MACVLan或SR-IOV取代了。

或者,创建一个Profile,其中devices段如下:

1
2
3
4
5
6
devices:
eth0:
name: eth0
nictype: bridged
parent: br0
type: nic

编辑网络

要在创建后修改网络配置,执行:

1
incus network set 网络名 参数=值

或是执行:

1
incus network edit 网络名

直接编辑YAML配置。

ACL

Incus支持对网络创建ACL规则。首先创建一个ACL配置:

1
incus network acl create ACL配置名 [参数=值]

可用参数有:

  • description:描述。
  • ingress:入站规则列表。
  • egress:出站规则列表。

然后在ACL配置中添加规则:

1
incus network acl rule add ACL配置名 ingress|egress [参数=值]

可用参数有:

  • action=allow|reject|drop:必须参数,针对特定数据包的操作。
  • state=enabled|disabled|logged:规则状态。logged表示登记模式。
  • description:描述。
  • source:匹配源地址,可以是CIDR,也可以是IP范围。特殊的值@internal表示内网通信,@external表示外网通信。
  • destination:匹配目标地址,可以是CIDR,也可以是IP范围。
  • protocol=icmp4|icmp6|tcp|udp:网络协议。
  • source_port:仅支持tcp|udp协议,匹配源端口号。
  • destination_port:仅支持tcp|udp协议,匹配目标端口号。
  • icmp_type:仅支持icmp4|icmp6协议,匹配ICMP类型号。
  • icmp_code:仅支持icmp4|icmp6协议,匹配ICMP状态码。

要编辑一个ACL配置,执行:

1
incus network acl edit ACL配置名

要查看ACL日志,执行:

1
incus network acl show-log ACL配置名

要分配ACL,执行:

1
incus network set 网络名 security.acls=ACL配置名

或是仅对实例生效:

1
incus config device set [服务器名:]实例名 实例中的网络接口 security.acls=ACL配置名

配置了ACL的网络/接口的默认行为是阻止所有的出入站数据包,要修改这一行为,执行:

1
incus network set 网络名 security.acks.default.ingress|egress.action=allow

1
incus config device set [服务器名:]实例名 实例中的网络接口 security.acls.default.ingress|egress.action=allow

OVN

首先安装OVN:

1
2
3
4
5
# Debian
apt install ovn-central ovn-host

# Fedora
dnf install ovn-central ovn-host

配置OVN网桥

执行以下命令:

1
2
3
4
5
# 设置OVS的整体配置
ovs-vsctl set open_vswitch . \
external_ids:ovn-remote=unix:/run/ovn/ovnsb_db.sock \
external_ids:ovn-encap-type=geneve \
external_ids:ovn-encap-ip=127.0.0.1

配置OVN网络

OVN网络必须连接到一个Incus Bridge类型的父网络上。

执行以下命令:

1
incus set 父网络名 ipv4.ovn.ranges=范围
1
incus network create 网络名 --type=ovn network=父网络名

端口映射

Proxy设备

Incus可以通过为实例添加Proxy设备的方式配置流量转发:

1
incus config device add [服务器名:]实例名 设备名 proxy listen=协议:主机IP[:端口号] connect=协议:实例内部IP[:端口号] [nat=true]
  • 默认情况下使用非NAT(直接代理)模式,启用nat=true后使用NAT模式。
  • 在使用直接代理模式的情况下,容器内的应用必须支持HAProxy的PROXY协议才能获取客户端源IP。而NAT模式不需要。
  • 对于直接代理模式来说,支持的协议有tcpudpunix
  • 对于虚拟机来说,只能使用NAT模式,而且仅支持tcpudp协议。
    • 实例还必须有被特别分配的静态IP才能使用NAT模式。具体方法为incus config device override 实例名 网络设备名称 ipv4.address=IPv4 ipv6.address=IPv6。对于IPv6地址来说,实例接入的网络必须启用ipv6.dhcp.stateful: true才能设置静态IP。
  • 主机IP可以为0.0.0.0
    • 对于NAT模式来说不能使用通配地址,必须得有确切的IP。
  • 实例内部IP可以为127.0.0.1,如果是0.0.0.0的话,会自动转换为对应的实例的IP。

其他可用的选项如下:

  • mode:UNIX套接字的权限位,默认为0644
  • gid:UNIX套接字的所有组,默认为0
  • proxy_protocol:在使用非NAT模式时,尝试使用HAProxy Proxy协议传递请求者的信息。

网络转发

Incus的网络转发类似于Proxy设备的NAT模式,但是它工作于宿主机的网络配置。

  • 网络转发用于实现浮动IP(Floating IP)以及高可用策略。
  • 网络转发只可用于桥接网络和OVN网络。

先为网络添加一个监听的IP,这个IP应当是主机的外网IP:

1
incus network forward create 网络名 监听IP [参数=值]

可用参数有:

  • description:描述。
  • ports:允许使用的端口列表。
  • 在集群中创建网络转发时,需要在每个节点上都执行一次创建命令,最后再不带--target选项执行一次创建命令。

然后即可添加映射端口:

1
incus network forward port add 网络名 监听IP tcp|udp 监听端口 目标IP 目标端口 [参数=值]

其他参数如下:

  • target_port:目标端口,默认和监听端口一致。
  • description:描述。
  • 在OVN网络上,监听IP必须在ipv4|ipv6.routes中存在。
  • 在集群中创建网络转发时,需要在每个节点上都执行一次创建命令,最后再不带--target选项执行一次创建命令。

删除映射端口:

1
incus network forward port remove 网络名 监听IP [tcp|udp 监听端口 目标IP]

删除网络转发:

1
incus network forward delete 网络名 监听IP

编辑网络转发:

1
incus network forward edit 网络名 监听IP

存储

Incus的存储使用池化管理。它支持以下几类存储引擎:

存储特性 目录 BTRFS LVM ZFS CEPH RBD CEPHFS CEPH OBJ
宿主机共享
独立磁盘
回环设备
远程存储
写入优化
克隆
快照
配额 是(Ext4或Xfs)

它们分别需要以下组件:

  • Btrfs:btrfs-progs(RHEL不支持)。
  • Ceph:ceph(RHEL官方源中不提供)。
  • ZFS:zfsutils-linux(RHEL不支持)。
  • LVM:lvm2

检查存储池

列出可用存储池:

1
incus storage list

检查存储池:

1
incus storage show|info 池名

创建存储池

执行:

1
incus storage create 池名 存储引擎 [参数=值]

dir引擎可用参数有:

  • source=目录位置:指定目录位置。

btrfs引擎可用参数有:

  • size=:大小。
  • source=/dev/sdX:指定建立了Btrfs文件系统的块设备。

lvm(或用于网络存储设备的lvmcluster)引擎可用参数有:

  • size=:大小。
  • source=/dev/sdX lvm.vg_name=VG名:在指定块设备上创建VG并使用。
  • source=VG名:指定使用的VG。
  • lvm.thinpool_name=:(lvmcluster引擎不支持)创建一个指定名称的Thinpool。

zfs引擎可用参数有:

  • size=:大小。
  • source=/dev/sdX zfs.pool_name=ZPool名:在指定块设备上创建ZPool并使用。
  • source=ZPool名:指定使用的ZPool。
  • source=ZPool名/Dataset名 volume.zfs.block.mode=yes:指定使用的Dataset。

ceph(Ceph RBD)可用的参数有:

  • ceph.cluster_name=:使用的Ceph集群名。
  • ceph.osd.pool_name=:使用的Ceph池名。
  • source=OSD名:使用现有的Ceph OSD池。

cephfs引擎可用参数有:

  • source=文件系统名[/目录]:指定使用的CephFS文件系统。
  • cephfs.create_missing=true:自动创建不存在的目录。

cephobject引擎可用参数有:

  • cephobject.radosgw.endpoint=:指定Ceph Object的HTTP入点。

在集群中创建存储池时,需要在每个节点上都执行一次创建命令,最后再不带--target选项执行一次创建命令。

编辑存储池

要在创建后修改存储池配置,执行:

1
incus storage set 池名 参数=值

例如:

1
incus storage set 池名 size=大小

或是执行:

1
incus storage edit 池名

直接编辑YAML配置。

使用存储池

很简单,在启动实例时使用--storage=池名选项指定即可。如果没有指定,那么使用当前的Profile的配置。

要移动实例,执行:

1
incus move [服务器名:]实例名 --storage 池名

创建存储卷

Incus在创建实例时会自动创建所需的卷,在移除实例时会自动删除,因此一般情况下,存储卷不需要手动创建,创建的命令为:

1
incus storage volume create 池名 卷名 [--type=filesystem|block] [参数=值]

可用类型有两种:

  • filesystem:存储容器和容器镜像。不能挂载在虚拟机中。
  • block:存储虚拟机的虚拟磁盘。不能挂载在容器中。

可用参数有:

  • size:大小。

此外,还有一种特殊的类型iso,也就是可挂载的ISO镜像卷,不过这种卷的创建方式是:

1
incus storage volume import 池名 ISO路径 卷名 --type=iso

这种卷只能是只读的。

要设置卷的用途(备份或镜像),执行:

1
incus config set storage.backups_volme|storage.images_volume 池名/卷名

查看池内的所有存储卷:

1
incus storage volume list 池名

设置存储卷

要在创建后修改存储卷配置,执行:

1
incus storage volume set 池名 卷名 参数=值

例如:

1
incus storage volume set 池名 卷名 size=大小

或是执行:

1
incus storage volume edit 池名 卷名

直接编辑YAML配置。

挂载存储卷

对于filesystem卷:

1
incus storage volume attach [服务器名:]池名 卷名 容器实例名 挂载路径

对于block卷:

1
incus storage volume attach [服务器名:]池名 卷名 虚拟机实例名

此外,也可以把卷作为一个设备挂载:

1
incus storage volume attach [服务器名:]池名 卷名 实例名 设备名 [挂载路径] [参数=值]

这条命令实际上是个别名:

1
incus config device add [服务器名:]实例名 设备名 disk pool=池名 source=卷名 [path=路径] [参数=值]

设置IO限制

在把卷作为设备挂载时,可以通过参数设置IO限制,在本质上,这是通过CGroup实现的:

  • boot.priority:设备的引导优先级,用于虚拟机。
  • io.bus=nvme|virtio-blk|virtio-scsi:设备的接口。
  • io.cache=none|writeback|unsafe:设备的写入缓存。
  • limits.max:最大读写速度限制。
  • limits.read:最大读速度限制。
  • limits.write:最大写入速度限制。

拷贝/移动存储卷

也就是:

1
incus storage volume copy|move 池名/卷名 池名/卷名

不过,要在集群节点之间移动的话,还要加上:

1
incus storage volume copy|move 池名/卷名 池名/卷名 --target 节点 --destination-target 节点

还可以在Incus集群间移动:

1
incus storage volume copy|move 主机名:池名/卷名 主机名:池名/卷名

备份卷

创建一个卷快照:

1
incus storage volume snapshot 池名 卷名 [快照名]

列出卷快照:

1
incus storage volume snapshot list 池名 卷名

检查卷快照:

1
incus storage volume snapshot show 池名 卷名/快照名

删除卷快照:

1
incus storage volume snapshot delete 池名 卷名/快照名

还可以设置定时卷快照:

1
incus storage volume set 池名 卷名 snapshots.schedule '分 时 日 月 年'|@daily

恢复卷快照:

1
incus storage volume snapshot restore 池名 卷名 快照名

还可以从卷快照构建一个新的卷:

1
incus storage volume copy 池名/卷名/快照名 池名/卷名/快照名

导出备份:

1
incus storage volume export 池名 卷名 [文件路径]

可用选项如下:

  • --compression=gzip|bzip2|none:压缩方式。
  • --optimized-storage:仅用于zfsbtrfs,启用优化存储方式。
  • --volume-only:仅备份卷,不备份快照。

恢复备份:

1
incus storage volume import 池名 文件路径 [卷名]

REST API

Incus支持使用REST API的方式进行远程使用。在服务端上执行:

1
incus config set core.https_address :8443|IP|URL

以设置REST API监听地址。

  • 如果还希望监听所有的IPv6地址,使用[::]:8443

默认情况下,对外开放的Incus服务器被视为公共服务器,这类服务器本身并不能运行实例,它只能为其他的Incus服务器提供镜像存储与共享服务。要使Incus服务器转换为常规服务器,即可以通过客户端远程操作以运行实例的服务器,必须为其配置一种认证方式,并添加信任的客户端才能使用。

要添加Incus服务器,在客户端执行:

1
incus remote add 名称 IP|URL

以添加,再执行:

1
incus remote switch 名称

表明切换到默认使用该服务器。在初始情况下,默认使用local:即本地服务器。

如果希望提高连接安全性,可以对客户端密钥添加密码,具体操作为:

1
ssh-keygen -p -f ~/.config/incus/client.key

公有镜像源实际上就是一种特殊的,只读的远端服务器。因此它使用和远程服务器一样的管理接口,但是公有镜像源是只读的,而且只能进行镜像下载操作。

TLS证书认证

Incus客户端和远程服务器之间默认使用TLS证书认证。具体的步骤是:

  1. 客户端在执行incus remote add命令时,会使用HTTPS协议连接服务端,并下载服务端的证书。
  2. 客户端会为用户展示证书的指纹,并要求用户确认。
  3. 服务端对客户端进行认证,具体方式有两种:
    • 添加信任证书:在客户端执行incus remote generate-certificate生成证书,然后在服务端执行incus config add-certificate 文件路径将客户端的证书添加到服务端。在认证时,服务端会在本地查找是否存在客户端的证书。
    • 使用Token:预先在服务端本地执行incus config trust add 客户端名为客户端生成独一无二的Token。在认证时,服务端会要求用户提供Token。
      • Token是由有效期的,这通过core.remote_token_expiry配置项设置。

修改默认证书

如果希望使用中央PKI,那么Incus也是支持PKI模式的。具体步骤为:

  1. 把CA证书上传到服务端的/var/lib/incus目录。
  2. 把CA证书上传到客户端的~/.config/incus目录。
  3. 把经过CA签名的证书上传到客户端和服务端,替换自动生成的证书。
  4. 重启服务端。

然后,按照前面提到的步骤配置证书认证即可。

ACME

Incus支持使用ACME协议为服务器配置证书。只需要在服务器上配置以下几个配置项:

  • acme.domain:发布证书的服务器的域名。
  • acme.email:ACME的账户邮箱。
  • acme.ca_url:ACME服务的URL,默认使用Let's Encrypt
  • acme.agree_tos:同意ACME协议。必须设为true才能启用。

OpenID认证

Incus可以使用OpenID提供商进行认证。首先在服务器上配置以下几个配置项:

  • oidc.claim:OpenID Connect的用户名。
  • oid.client.id:OpenID Connect的客户端ID。
  • odic.issuer:OpenID Connect的提供商URL。
  • oidc.audience:OpenID Connect的Audience,部分提供商需要该配置。

Web UI

Incus的WebUI包可以在Zabbly找到(名为incus-ui-canonical)。

Incus服务端在启动时,会读取INCUS_UI环境变量以获取Incus WebUI组件的资源目录,该目录下应当存在index.html主页文件。

用户可以下载incus-ui-canonical包后,手动解包并在incus.service单元内设置Environment=INCUS_UI=/xxx来启用WebUI。

启用WebUI后,设置incus config set core.https_address :8443即可从服务端的8443端口直接访问WebUI,首次访问时,WebUI会引导用户交互式配置浏览器客户端证书:

incus-webui.png

另一种访问方式是,在已添加过该服务端(配置过证书或Token)的Incus客户端上,执行incus webui [服务端名称],Incus客户端会自动转发请求并在本地打开浏览器展示WebUI。

HTTP协议

Incus WebUI本身不支持通过未经验证和加密的HTTP协议访问,但是,Incus会监听/var/lib/incus/unix.socketUNIX域套接字,因此可以通过代理的方式强行达到目的。

配置incus-webui.socket

1
2
3
4
5
[Socket]
ListenStream=8080

[Install]
WantedBy=sockets.target

配置incus-webui.service

1
2
3
4
5
6
7
8
9
[Unit]
Requires=incus-webui.socket incus.socket
After=incus-webui.socket incus.socket

[Service]
Type=notify
ExecStart=/usr/lib/systemd/systemd-socket-proxyd /var/lib/incus/unix.socket
PrivateTmp=yes
PrivateNetwork=yes

其他

重命名远端服务器:

1
incus remote rename 名称 新名称

获取当前使用的远端服务器:

1
incus remote get-default

列出信任的客户端:

1
incus config trust list

列出已生成的Token:

1
incus config trust list-tokens

移除信任的客户端:

1
incus config trust remove 客户端名

集群

Incus支持以集群方式运作,集群内部使用CowSQL分布式数据库存储元信息,并且使用木筏算法选举。集群最少需要三个节点以实现高可用。

查看集群信息:

1
incus query /1.0/cluster

原理

Incus集群中的每个节点要么是Voter节点,要么是Stand-by节点。Voter节点会参与分布式数据库选主,默认情况下,一个集群中只有三个节点是Voter,其他都是Standby节点。当有一个Voter节点离线时,一个Standby节点会立刻升级为Voter节点并顶替。这些相关配置项分别有:

  • cluster.max_voters:最大Voter节点数。
  • cluster.max_standby:最大Standby节点数。
  • cluster.offline_threshold:认为离线的响应延迟。

建立集群

在第一台Incus服务器上执行:

1
incus admin init

然后在:

1
Would you like to use Incus clustering?

选择yes,设置该服务器的IP即可。这个IP之后可以执行:

1
incus config set cluster.https_adddress=IP|URL

进行重设。

完成后执行:

1
incus cluster list

即可检查集群情况。

加入集群

在集群服务器上执行:

1
incus cluster add 成员名

然后在新的服务器上执行:

1
incus admin init

然后在:

1
Would you like to use Incus clustering?

选择yes,在:

1
Are you joining an existing cluster?

也选择yes,加入集群并输入Token即可。

执行:

1
incus cluster show|info 节点名

以查看信息。

节点证书位于/var/lib/incus/cluster.crt

节点组

一部分节点可以组成节点组方便管理,执行:

1
incus cluster group create 组名

以创建节点组。

设置节点所在组:

1
incus cluster group assign 节点名 default,组名

该命令会覆盖默认组,所以可以带上default防止覆盖。

将节点添加组:

1
incus cluster group add 节点名 组名

节点组可以在--target选项中使用@组名调用。

镜像存储

默认情况下Incus会在每个节点上保存一份镜像的拷贝,这一行为可以通过:

  • cluster.image_minimal_replica:镜像最小副本数,设为-1表示节点数。

修改。

实例存储

新建实例时,Incus默认会尝试保存在当前实例数量最少的那个节点上。不过,也可以使用--target选项指定节点。scheduler.instance可以修改默认操作。

  • 如果scheduler.instance=all,那么使用默认逻辑。
  • 如果scheduler.instance=manual,那么该节点必须被手动指定使用。
  • 如果scheduler.instance=group,那么该节点在用户选择同组节点时可能会被使用。

执行:

1
incus cluster set 节点名 scheduler.instance all|manual|group

以设置。

要移动实例,执行:

1
incus move [服务器名:]实例名 --target 节点名

排空/隔离

有时候我们会希望排空/隔离一个节点(比如升级),此时执行:

1
incus cluster evacuate 节点名

即可,要恢复,执行:

1
incus cluster restore 节点名

此外,对于离线节点还可以自动排空,只需要设置:

1
incus cluster set 节点名 cluster.healing_threshold=秒数

删除节点

1
incus cluster remove 节点名 [--force]
  • --force用于移除离线节点。

灾难恢复

有时候你会非常不幸的同时损失多个节点,此时Incus集群会处于不可用状态,要恢复,执行:

1
incus admin cluster list-database

查看可用的节点。连接一个节点,执行:

1
systemctl stop incus.service incus.socket

停止服务,然后执行:

1
incus admin cluster recover-from-quorum-loss

恢复数据。最后再启动服务。

重设IP

当集群中有节点IP发生变化时,必须执行:

1
incus admin cluster edit

重新设置。

镜像

配置镜像源

Incus内置的镜像源是images:,这是一个Simple Stream类型的镜像源,会从https://images.linuxcontainers.org拉取镜像,这一配置可以进行修改:

1
incus remote set-url images https://xxx

查看远程源中的镜像:

1
incus image list 源名称:[镜像名] [版本号] [architecture=架构] [type=container|virtual-machine]

镜像默认会自动更新,要禁用这一行为,执行:

1
incus config set images.auto_update_interval=0

下载镜像

在Incus中,本地镜像需要保存在目标服务器上(默认为local:),因此要“下载镜像”实际上就是把其他服务器的镜像拷贝到目标服务器:

1
incus image copy 服务器名:镜像名/版本/类型/架构 local:|服务器名: [--public] [--vm] [--alias=名称] [--copy-aliases] [--auto-update] [--target=节点]
  • --public:使镜像公开。
  • --copy-aliases:拷贝源镜像的别名。
  • 如果没有使用--alias或是--copy-alias选项,那么镜像将没有名称,只能使用ID(指纹)指定,很麻烦。
  • --mode pull|push|relay:传输模式,pull表示从源服务器拉取,源服务器必须对网络开放;push表示源服务器向目标服务器推送,目标服务器必须对网络开放;relay表示客户端负责从源服务器获取数据并转发给目标服务器,两个服务器都必须对网络开放。

检查镜像信息:

1
incus image show|info 镜像名|ID

给镜像设置别名:

1
incus image alias list|create|delete 别名 镜像ID

重设别名:

1
incus image alias rename 别名 新别名

导出与导入镜像

Incus可以方便快捷地制作与发布镜像。

要从头制作镜像,先使用mkosi或其他类似工具下载一个基本根文件系统,并将其命名为rootfs/。注意,打包时位置(-C选项)必须为根文件系统。

然后,制作元数据。创建一个文件metadata.yaml,内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
# 架构
architecture: x86_64
# 创建时间,执行date +%s命令获取
creation_date: xxx
# 属性
properties:
# 描述
description: 起个系统名称吧
# 发行版
os: 使用的发行版
# 版本号
release: "版本号"
# 变体
variant: "名称"

# 使用的Jinja2模板文件,可选,按需要配置即可
# 为了实现Incus默认行为的兼容,建议至少配置一下/etc/hostname和/etc/hosts,将默认主机名设为容器名
templates:
/etc/hosts:
# 使用该模板的条件,即仅在创建和拷贝时使用
when:
- create
- copy
# 是否仅在不存在时应用模板
create_only: false
# 使用的模板文件
template: hosts.tpl
# 填充的变量字典
properties: {}
/etc/hostname:
# 使用该模板的条件,即仅在创建和拷贝时使用
when:
- create
- copy
# 是否仅在不存在时应用模板
create_only: false
# 使用的模板文件
template: hostname.tpl
# 填充的变量字典
properties: {}

在相同的工作目录下,创建templates/目录,在其中保存对应的模板文件。以我们这里的例子来说:

1
2
3
4
# hosts.tpl
127.0.1.1 {{ container.name }}
127.0.0.1 localhost localhost.localdomain localhost4 localhost4.localdomain4
::1 localhost localhost.localdomain localhost6 localhost6.localdomain6
1
2
# hostname.tpl
{{ container.name }}

rootfs/metadata.yamltemplate/目录共同打包为image.tar.gz。注意,打包时位置(-C选项)必须为工作目录。

从文件中导入镜像:

1
2
# VM
incus image import QCOW2镜像文件路径 [--public]
1
2
# 容器
incus image import 元数据Tar包路径 根文件系统Tar包路径 [--public]
  • --public:使镜像公开。

导出镜像,即把镜像到出到文件中:

1
incus image export 源名:镜像名 [目录] [--vm]

从实例或快照中发布镜像:

1
incus publish [服务器名:]实例名[/快照名] [服务器名:] [--public] [--alias=别名] [--compression=压缩方式]
  • --public:使镜像公开。

镜像Profile

从镜像创建实例时,会自动按顺序应用一系列的Profile,要修改这些应用的Profile及其顺序,可以执行:

1
incus image edit 镜像名

创建镜像服务器

如之前所说过的,可为Incus提供镜像存储与共享服务的服务器包括:

  • Incus公共服务器:只通过core.https_address配置项对外部网络暴露了Incus,但是并没有配置认证的服务器。
  • Incus常规服务器:对外部网络暴露了Incus,并且配置了认证的服务器。

对于这类服务器来说,只要是在导入、发布、或是拷贝时使用了--public选项的镜像,都可以被其他的Incus服务器访问和下载。在客户端处执行:

1
2
# 如果是公共服务器的话,需要添加--public选项
incus remote add 名称 服务器IP|URL [--public]

此外还有一类:

  • Simple Stream服务器:使用Simple Stream对外提供纯镜像服务的简单文件服务器。

配置Incus服务器的方式之前已经提到过了。要配置Simple Stream服务器,首先创建合适的镜像:

  • 容器:squashfs格式的镜像文件。
  • 虚拟机:qcow2格式的虚拟磁盘文件。

然后,使用incus-tools包中提供的incus-simplestreams工具配置Simple Stream。

首先,执行:

1
incus-simplestreams generate-metadata 元数据.tar.xz

生成元数据,生成前会交互式要求输入一些信息。

然后,执行:

1
incus-simplestreams add 元数据.tar.xz 镜像.squashfs|镜像.qcow2

导入镜像,这会将当前工作目录转换为Simple Stream的服务目录树,具体来说,会在当前目录下创建imagesstreams两个目录:

  • images:保存镜像。
  • streams:保存索引。

执行:

1
incus-simplestreams remove 指纹

删除镜像。

执行:

1
incus-simplestreams list

列出当前工作目录下的镜像。

然后,使用任意一种Web服务器在该目录上提供服务即可,需要注意的是,必须使用HTTPS协议。

在客户端处,执行:

1
incus remote add 名称 https://服务器地址 --protocol=simplestreams

以添加Simple Stream服务器。

实例

实例,即一个VM或容器,是Incus的中心。Incus使用一致的接口创建VM和容器,但是底层技术是不一样的:

  • 容器使用liblxc实现。
  • 虚拟机使用qemu-kvm实现,附带一个incus-agent虚拟机代理辅助工作。
    • Qemu和qemu-img需要另外安装,安装Qemu后需要重启Incus守护进程以使其读取Qemu位置。
    • 除了Qemu,还需要OVMF固件(用于X86_64,位于/usr/share/OVMF/OVMF_CODE.fd)或AAVMF固件(用于AARCH64,也被称为qemu-efi-arm[aarch64],位于/usr/share/AAVMF/OVMF_CODE.fd),如果发行版没有打包,你可以在这里找到预构建的二进制文件:EDK2-Nightly

Incus Agent的工作原理:

  • Incus启动的Qemu虚拟机具备一个Attribute为org.linuxcontainers.incus的VSock,并通过Udev规则(准确地说,是50-udev-default.rules这条默认规则)挂载到/dev/virtio-ports/目录下。
  • (Incus自动构建的镜像已包含,不用手动安装)用户挂载Incus通过9pvirtiofs暴露的config虚拟光盘,并执行其中的install.sh,安装之后要用到的incus-agent.rules规则,incus-agent.service服务和incus-agent-setup脚本。这需要宿主机安装incus-agent,并且确保该命令存在于$PATH目录下。
  • Incus提供的incus-agent.rules(内容为SYMLINK=="virtio-ports/org.linuxcontainers.incus", TAG+="systemd", ENV{SYSTEMD_WANTS}+="incus-agent.service")规则生效,使得该VSock设备触发incus-agent.service服务启动。
  • incus-agent.service服务中包含一个StartPre=字段,启动的是incus-agent-setup脚本。该脚本的功能是,创建一个Tmpfs于/run/incus_agent,自动挂载Incus通过9pvirtiofs暴露的config虚拟光盘,将其所有内容拷贝到/run/incus_agent目录下,然后再弹出虚拟光盘。
  • 准备完毕,incus-agent二进制文件确定位于/run/incus_agent/incus-agentincus-agent.service服务正常启动。

创建实例

创建一个实例的命令为:

1
incus create|launch|init [服务器名:]镜像名 [服务器名:]实例名 [--vm] [其他选项]

实例名中仅允许包含字母、数字和短横线-

可用的其他选项包括:

  • -e|--ephemeral:临时模式,停止容器时抹除任何操作。
  • -p|--profile 配置名:指定使用的Profile。
    • --no-profile:不使用任何Profile。这意味着必须手动配置容器。
  • -c|--config 配置=值:手动设置配置项。
  • -n|--network 网络名:指定使用的Incus网络。
  • -s|--storage 池名:指定使用的Incus存储池。
  • --target 节点:指定使用的集群节点。
  • -d|--device 设备,配置=值:指定一些虚拟设备配置,这可以用于覆盖实例从Profile中继承的设备配置。例如设置根文件系统大小--device root,size=
  • --empty:什么也不做,新建一个空实例。
    • --vm:新建一个虚拟机实例。

要从ISO引导VM,首先需要创建一个ISO镜像的存储卷:

1
incus storage volume import 池名 ISO文件路径 卷名 --type=iso

然后在虚拟机中添加设备:

1
incus config device add [服务器名:]实例名 设备名 disk pool=池名 source=卷名 boot.priority=0

安装完成后移除卷:

1
incus storage volume detach [服务器名:]池名 卷名 实例名

控制与检查实例

所有的Incus实例接口都支持命令行和REST两种方式。

列出实例:

1
2
3
incus list [匹配词=值]

incus query /1.0/instances?filter=[匹配词+eq+值]

可用匹配词包括:

  • type=container|vm:类型。
  • status=running|stopped:状态。
  • location=节点名:所在节点。
  • 系统类型.*:系统类型。

查看实例信息:

1
2
3
incus info [服务器名:]实例名 [--show-log]

incus query /1.0/instances/实例名

启动实例:

1
2
3
incus start [服务器名:]实例名 [--console|--console=vga]

incus query --request PUT /1.0/instances/实例名/state --data '{"action": "start"}'
  • --stateless:无视实例状态。

冻结实例:

1
2
3
incus pause|freeze [服务器名:]实例名

incus query --request PUT /1.0/instances/实例名/state --data '{"action": "pause"}'

恢复实例:

1
2
3
incus resume|unfreeze [服务器名:]实例名

incus query --request PUT /1.0/instances/实例名/state --data '{"action": "resume"}'

停止实例:

1
2
3
incus stop [服务器名:]实例名

incus query --request PUT /1.0/instances/实例名/state --data '{"action": "stop"}'

重启实例:

1
incus restart [服务器名:]实例名

删除实例:

1
2
3
incus delete [服务器名:]实例名

incus query --request DELETE /1.0/instances/实例名

重建实例(保留配置,删除并重新创建):

1
2
3
4
5
incus rebuild [服务器名:]镜像名 [服务器名:]实例名 [--empty]

incus query --request POST /1.0/instances/实例名/rebuild --data '{"source": {"alias":"镜像名","server":"服务器URL", protocol:"simplestreams"}}'

incus query --request POST /1.0/instances/实例名/rebuild --data '{"source": {"type":"none"}}'

无特权容器

Incus默认以无特权方式运行容器,因此必须确保Root用户有合适的SubUID和SubGID。SubUID和SubGID的范围中至少要有65536个值,否则启动容器时会立刻失败

默认情况下,在安装Incus时,Debian会为Root用户创建1000000-1000000000的SubUID和SubGID,这在一般情况下足够了。

  • Arch不会这么做,用户需要手动执行usermod -v 1000000-1000000000 -w 1000000-1000000000 root

在创建容器时,Incus会随机抽取一段UID(以下文字中省略GID)并对容器中的UID进行映射,因为抽取的UID是随机的,所以不同的容器映射的目标UID有可能重合,这在一般情况下没什么问题,不过如果用户对安全性很敏感,可以给容器设置配置项security.idmap.isolated=true,这可以确保容器映射的目标UID是唯一的。

此外,用户也可以手动设置UID映射,只需要给容器设置以下两个配置项:

  • security.idmap.base:映射的目标UID起点。
  • security.idmap.size:映射的UID数量。

如果用户不希望使用无特权容器(例如嵌套容器环境下),那么可以通过给容器或Profile中设置该配置项关闭:

  • security.privileged=true

配置实例

查看实例配置,执行:

1
2
3
incus config show [服务器名:]实例名 [--expanded]

incus query /1.0/instances/实例名

配置实例的命令为:

1
2
3
incus config set [服务器名:]实例名 配置项=值

incus query --request PATCH /1.0/instances/实例名 --data '{"config": {"配置项":"值"}}'

可用的配置项包括:

杂项:

  • boot.autostart:是否随incusd守护进程启动。
  • security.protection.delete:禁止实例被删除。默认禁用。
  • security.guestapi:是否在实例内提供/dev/incus接口。默认启用。

CGroup限制:

  • limits.cpu:可用的CPU。
  • limits.cpu.allowance:可用的CPU占用率,以百分比形式提供。
  • limits.cpu.priority:0-10的CPU占用优先级。
  • limits.cpu.nodes:可用的CPU节点。
  • limits.disk.priority:0-10的IO优先级。
  • limits.memory:内存最大使用量,容器默认无限制,VM默认为1GiB。
  • limits.memory.enforce=soft|hard:仅用于容器,内存限制性质。
  • limits.memory.swap:仅用于容器,交换空间最大使用量。设为false彻底禁用换页。
  • limits.memory.swap.priority:仅用于容器,0-10的交换空间优先级。
  • limits.memory.hugepages:仅用于VM,是否允许使用HugePage。
  • limits.hugepages.大小:允许使用的HugePage量。
  • limits.processes:仅用于容器,最大进程数量。

用于容器的Rlimit限制:

  • limits.kernel.as:虚拟内存用量。
  • limits.kernel.core:核心转储文件大小。
  • limits.kernel.cpu:秒数计算的CPU时间。
  • limits.kernel.data:数据段长度。
  • limits.kernel.fsize:文件最大大小。
  • limits.kernel.locks:文件锁数量。
  • limits.kernel.memlock:内存锁定大小。
  • limits.kernel.nice:NICE级别。
  • limits.kernel.nofile:打开的文件数量。
  • limits.kernel.nproc:用户进程数量。
  • limits.kernel.rtprio:实时调度优先级。
  • limits.kernel.sigpending:延迟信号数量。

快照配置:

  • snapshots.expiry:快照过期时间。
  • snapshots.schedule:定时设置快照。
  • snapshots.schedule.stopped:是否即使停止,也定时设置快照。默认禁用。
  • snapshots.pattern:定时设置的快照的名称格式,默认为snap%d

用于容器的增强功能:

  • security.guestapi:是否启用/dev/incus接口。
  • security.privileged:是否运行特权容器,默认禁用。
  • security.idmap.base:用于非特权容器,ID映射的起点。默认为0,表示随机选择。
  • security.idmap.size:用于非特权容器,ID映射的数量。默认为65536。
  • security.idmap.isolated:用于非特权容器,ID映射是否严格独立。默认禁用。
  • security.nesting:是否允许容器内嵌套运行Incus,默认禁用。
    • 这实际上会做两件事,在容器的/dev/.lxc/目录下挂载procfssysfs,以及切换使用嵌套模式的Apparmor配置。
  • security.protection.shift:禁止容器ID映射被调整,默认禁用。
  • security.syscalls.deny_default:是否启用默认的系统调用禁用策略,默认启用。
  • security.syscalls.deny_compat:是否禁止在x86-64系统上运行compat_*系统调用,即禁止运行32位应用程序,默认禁用。
  • security.syscalls.deny:禁用的系统调用列表。必须和security.syscalls.allow互斥。
  • security.syscalls.allow:允许的系统调用列表。必须和security.syscalls.deny互斥。

Incus可以对容器中的某些系统调用进行拦截审查,并根据用户的需要判断其是否允许执行,Intercept选项仅对启用了SECCOMP的非特权容器有意义

  • security.syscalls.intercept.bpf:是否允许使用bpf()系统调用,默认禁用。
  • security.syscalls.intercept.bpf.devices:是否允许读取CGroup树中的.device节点的BPF程序进行加载,默认禁用。
  • security.syscalls.intercept.mknod:是否允许使用mknod()系统调用,默认禁用。
  • security.syscalls.intercept.mount:是否允许使用mount()系统调用,默认禁用。
    • security.syscalls.intercept.mount.allowed:允许挂载的文件系统,仅在security.syscalls.intercept.mount=true时有效,例如ext4btrfs
    • security.syscalls.intercept.mount.fuse:在挂载指定的文件系统时转换为指定的FUSE,必须和security.syscalls.intercept.mount.allowed互斥,格式为ext4=fuse2fs
    • security.syscalls.intercept.mount.shift:是否在挂载时尝试使用ID映射,默认禁用。
  • security.syscalls.intercept.sched_setcheduler:是否允许使用sche_setsheculer()系统调用,默认禁用。
  • security.syscalls.intercept.setxattr:是否允许使用setxattr()系统调用,默认禁用。
  • security.syscalls.intercept.sysinfo:是否允许使用sysinfo()系统调用,默认禁用。

用于虚拟机的增强功能:

  • security.agent.metrics:是否启用指标采集。默认启用。
  • security.csm:是否启用Legacy BIOS支持,默认禁用(新版ovmf固件的软件包中,可能没有提供OVMF_CODE.csm.fd文件,这会导致虚拟机启动失败,需要特别注意)。
  • security.secureboot:是否启用安全引导,默认启用。
  • security.sev:是否启用AMD安全虚拟化技术。默认禁用。
  • security.sev.policy.es:是否启用AMD安全虚拟化加密。默认禁用。
  • raw.qemu:Qemu原生启动选项。空格分隔。
  • raw.lxc:LXC原生启动选项。空格分隔。
    • 例如,禁用AppArmor:raw.lxc: lxc.apparmor.profile = unconfined,设置Init进程raw.lxc: lxc.init.cmd = /sbin/init

用于NVIDIA显卡直通的特殊配置:

  • nvidia.runtime:在非特权容器启动时执行/usr/share/lxc/hooks/nvidia钩子脚本,加载GPU设备节点和NVIDIA用户空间驱动,使得非特权命名空间中的用户可以使用GPU设备节点,默认禁用。
    • nvidia.driver.capabilities:允许调用的NVIDIA驱动功能,默认为compute,utilityall表示所有,video表示音视频转码。
    • nvidia.require.cuda:需要的最小CUDA版本。如果容器内的CUDA小于该版本,容器会被禁止使用GPU。
    • nvidia.require.driver:需要的最小驱动版本。

要给实例添加设备,执行:

1
2
3
incus config device add [服务器名:]实例名 设备名 设备类型 [设备选项=值]

incus query --request PATCH /1.0/instances/实例名 --data '{"devices": {"设备名": {"type":"设备类型","设备选项":"值"}}}'

这实际上就是修改了实例的Config YAML中的device字典。

要覆写实例从Profile继承得到的设备的属性,执行:

1
incus config device override [服务器名:]实例名 设备名 设备类型 [设备选项=值]

可用的设备类型及其选项有:

  • nic:网络接口。
    • network=:实际使用的Incus网络。
    • nictype=:直接接入的网络接口类型,不太常用。
  • disk:磁盘。
    • pool=:使用的存储池。如果使用了存储池,则使用source=指定卷名。
    • source=:宿主机上的目录或块设备。
      • 如果是块设备:
      • io.bus=nvme|virtio-blk|virtio-scsi:设备的接口。默认使用virtio-scsi
      • io.cache=none|writeback|unsafe:设备的写入缓存。默认使用none
      • 如果是目录:
      • io.bus=auto|9p|virtiofs:设备的接口。默认使用auto
      • io.cache=none|metadata|unsafe:设备的写入缓存。默认使用none
    • source=ceph:池名/卷名 ceph.user_name=用户名 ceph.cluster_name=集群名:使用Ceph RBD。
    • source=cephfs:FS名/路径 ceph.user_name=用户名 ceph.cluster_name=集群名:使用Ceph FS。
    • source=ISO文件路径:接入ISO镜像。
    • boot.priority:设备的引导优先级,用于虚拟机。
    • limits.max:最大读写速度限制。
    • limits.read:最大读速度限制。
    • limits.write:最大写入速度限制。
  • unix-char:字符设备。
    • source:设备路径。
    • path:容器内的位置。
    • uid:所属的用户。
    • gid:所属的用户组。
    • mode:权限位,默认为0660
    • required:是否必要。
  • unix-block:块设备。
    • source:设备路径。
    • path:容器内的位置。
    • uid:所属的用户。
    • gid:所属的用户组。
    • mode:权限位,默认为0660
    • required:是否必要。
  • unix-hotplug:热插拔设备。
    • uid:所属的用户。
    • gid:所属的用户组。
    • mode:权限位,默认为0660
    • required:是否必要。
    • productid:设备ID。
    • vendorid:设备制造商ID。
  • usb:USB设备。
    • uid:所属的用户。
    • gid:所属的用户组。
    • mode:权限位,默认为0660
    • required:是否必要。
    • productid:USB设备ID。
    • vendorid:USB制造商ID。
  • gpu:GPU设备。
    • gputype=physical:物理显卡直通,可用于容器。
      • pci:设备PCI地址。仅该选项是必须的。
      • uid:所属的用户。
      • gid:所属的用户组。
      • mode:权限位,默认为0660
      • vendorid:对虚拟机重写设备制造商ID。
      • productid:对虚拟机重写设备ID。
    • gputype=mdev:虚拟机的虚拟显卡。
      • pci:设备PCI地址。
      • mdev:使用的虚拟显卡类型。
      • vendorid:对虚拟机重写设备制造商ID。
      • productid:对虚拟机重写设备ID。
    • gputype=mig:用于容器的NVIDIA MIG单元。
      • pci:设备PCI地址。
      • mig.ci:MIG的计算实例ID。
      • mig.gi:MIG的GPU实例ID。
      • mig.uuid:MIG的设备UUID。
      • vendorid:对虚拟机重写设备制造商ID。
      • productid:对虚拟机重写设备ID。
    • gputype=sriov:用于虚拟机的SRIOV。
      • pci:设备PCI地址。仅该选项是必须的。
      • vendorid:对虚拟机重写设备制造商ID。
      • productid:对虚拟机重写设备ID。
  • infiniband:IB网卡。
    • nictype=physical:物理的IB网卡。
      • parent:父网卡。
      • mtu:MTU。
      • name:名称。
      • hwaddr:对虚拟机重写MAC地址。
    • nictype=sriov:SRIOV分配的网卡。
      • parent:父网卡。
      • mtu:MTU。
      • name:名称。
      • hwaddr:对虚拟机重写MAC地址。
  • tpm:TPM。
    • path:路径,一般为/dev/tpmX
    • pathrm:资源管理器路径,一般为/dev/tpmrmX
  • pci:PCI设备。不推荐存在其他选项的情况下使用该选项(例如显卡)。
    • address:PCI地址。

还有一个特殊的agent设备,由于RHEL系发行版没有9p(Plan 9文件系统)内核模块,因此必须通过伪CD-ROM设备的方式加载Agent,也就是:

1
incus config device add [服务器名:]实例名 agent disk source=agent:config

然后在虚拟机启动时,即会自动加载并启动Agent。这个功能需要mkisofsgenisoimage工具。

编辑实例配置:

1
2
3
incus config edit [服务器名:]实例名

incus query --request PUT /1.0/instances/实例名 --data '配置'

在容器中使用KVM

为容器启用security.nesting后,添加以下设备:

1
2
3
4
5
6
7
incus config device add 容器名 kvm unix-char source=/dev/kvm

# 如果存在的话
incus config device add 容器名 vhost-vsock unix-char source=/dev/vsock
incus config device add 容器名 vhost-vsock unix-char source=/dev/vhost-vsock
incus config device add 容器名 vhost-net unix-char source=/dev/vhost-net
incus config device add 容器名 loop-control unix-char source=/dev/loop-control

针对容器的GPU直通

参考:在 LXC/Incus 容器中复用宿主机的 Nvidia 显卡进行视频转码

要获取宿主机上所有设备的PCI编号,执行:

1
incus info --resources

要直通GPU,执行:

1
incus config device add 实例名 设备名 gpu pci="PCI编号"

对于NVIDIA显卡来说,非特权容器可以通过libnvidia-container调度宿主机上的GPU和CUDA运行环境。宿主机上需要安装libnvidia-container、显卡驱动和CUDA Toolkit,具体安装方法见nvidia-container-toolkit。然后在容器中配置nvidia.runtime: true即可调用显卡和CUDA。需要配置nvidia.driver.capabilities: all才能使用所有显卡功能。

该功能对特权容器不可用,特权容器应当直接直通GPU后,在容器中配置驱动和CUDA环境,参见

针对虚拟机的GPU直通

针对Qemu虚拟机的GPU直通需要一些额外配置。

  1. 修改/etc/default/grub,将GRUB_CMDLINE_LINUX_DEFAULT一项改为以下内容(要使用iommu=pt的原因参考:Why do we need iommu=pt?):
    1
    2
    3
    4
    5
    # Intel
    GRUB_CMDLINE_LINUX_DEFAULT="quiet intel_iommu=on iommu=pt"

    # AMD实际上并不需要进行配置,默认都是启用的
    #GRUB_CMDLINE_LINUX_DEFAULT="quiet amd_iommu=on iommu=pt"
  2. 参考Explaining CSM, efifb=off, and Setting the Boot GPU Manually,具备显示功能的显卡在宿主机引导时会被固件初始化一次,但是如果设备已经被初始化,那么被直通到虚拟机中后,虚拟机中的固件再次尝试初始化时就会失败,虚拟机固件会在此时Hang住。修改/etc/default/grub,在GRUB_CMDLINE_LINUX_DEFAULT后添加以下内容:
    1
    2
    3
    4
    5
    # 这可以避免宿主机系统使用显卡
    initcall_blacklist=sysfb_init

    # 备用,已经过时,5.14以后的内核不再需要
    #video=vesafb:off video=efifb:off video=simplefb:off
  3. 执行update-grub
  4. 修改/etc/modules,添加以下模块:
    1
    2
    3
    4
    5
    6
    vfio
    vfio_iommu_type1
    vfio_pci

    # 在6.2以后的内核版本中,该模块被包含在vfio内,因此不再需要
    #vfio_virqfd
  5. 创建/etc/modprobe.d/kvm-iommu.conf,写入以下内容(参考Windows 10 1803 as guest with QEMU/KVM, BSOD under installs):
    1
    2
    3
    # 允许一些不安全的设备中断,这在直通时会避免一些问题
    options vfio_iommu_type1 allow_unsafe_interrupts=1
    options kvm ignore_msrs=1 report_ignored_msrs=0
  6. 创建/etc/modprobe.d/vfio-blacklist.conf,写入在主机上禁用指定的PCI设备的参数,有两种方法:
    1
    2
    3
    4
    5
    6
    # 传递给vfio_pci,使用lspci -nn查看PCI设备号,就在硬件信息的末尾,最后一个方括号的内容,类似于[10de:2489]这样的十六进制数字
    # 如果只使用OVMF-UEFI,那么VGA模拟是没有必要的,可以禁用
    options vfio_pci ids=PCI设备号1,PCI设备号2... [disable_vga=1]

    # 如果不起作用,尝试为lspci -k看到的驱动添加vfio_pci依赖项
    #softdep nouveau pre: vfio_pci
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    # 或者直接禁用驱动,使用lspci -k查看驱动

    # 屏蔽AMD显卡
    blacklist amdgpu
    blacklist radeon

    # 屏蔽英伟达显卡
    blacklist nouveau
    blacklist nvidia
    blacklist nvidiafb

    # 屏蔽英特尔显卡
    blacklist snd_hda_intel
    blacklist snd_hda_codec_hdmi
    blacklist i915
  7. 执行update-initramfs -u -k all,然后重启。启动后执行lspci -k,查看对应的设备正在使用的驱动(Kernel driver in use:)是否为vfio-pci或者,如果是则可以用于直通。
  8. 在Incus中使用GPU。

快照与备份

快照的实际实现原理随着存储引擎而变化,有些支持CoW的存储引擎支持优化的快照实现方式:

1
incus snapshot create [服务器名:]实例名 [快照名] [--reuse] [--stateful]
  • --reuse:覆盖原先已存在的快照。
  • --stateful:尝试保存VM当前的运行状态,包括CPU状态、内存状态等等。

列出快照:

1
incus snapshot list [服务器名:]实例名

查看快照信息:

1
incus snapshot show [服务器名:]实例名 快照名

重命名快照:

1
incus snapshot show [服务器名:]实例名 快照名 新快照名

删除快照:

1
incus snapshot delete [服务器名:]实例名 快照名

查看快照配置:

1
incus config show [服务器名:]实例名 快照名

编辑快照配置:

1
incus config edit [服务器名:]实例名/快照名

还可以设置定时快照:

1
incus config set [服务器名:]实例名 snapshots.schedule 'Cron日程'|@daily

恢复快照:

1
incus snapshot restore [服务器名:]实例名 快照名

导出实例:

1
incus export [服务器名:]实例名 [文件路径]
  • --compression=gzip|bzip2|none:压缩方式。
  • --optimized-storage:仅用于zfsbtrfs,启用优化存储方式。
  • --instance-only:仅备份实例,不备份快照。

从文件恢复实例:

1
incus import [服务器名:] 文件路径 [实例名]

Profile

Profile保存了一系列的实例配置,它可以用于创建实例时复用。不过在创建实例时,命令行选项总是会覆盖配置组中的选项。Incus初始化时,会创建一个默认Profile,名为default

列出可用Profile:

1
incus profile list

查看Profile:

1
incus profile show Profile名

创建一个Profile:

1
incus profile create Profile名

编辑Profile可以使用配置实例一样的方式:

1
2
3
incus profile set Profile名 配置=值

incus profile device add Profile名 设备名 配置=名

例如添加Agent:

1
incus profile device add Profile名 agent disk source=agent:config

不过,更合理的方法是直接编辑:

1
incus profile edit Profile名

删除Profile的前提是,该Profile没有被任何实例引用(具体被哪些实例引用可以查看Profile中的used_by列表):

1
incus profile delete Profile名

在创建实例时可以使用一个Profile:

1
incus launch [服务器名:]镜像名 [服务器名:]实例名 --profile Profile名

也可以给已创建的实例添加Profile:

1
incus profile add|remove [服务器名:]实例名 Profile名

或者直接给已创建的实例重新设置Profile:

1
incus profile assign [服务器名:]实例名 Profile名

默认Profile示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
config:
security.secureboot: "false"
security.csm: "true"
security.privileged: "true"
boot.autostart: "false"
security.syscalls.deny_default: "false"
description: Default Incus profile
devices:
eth0:
name: eth0
network: incusbr0
type: nic
root:
path: /
pool: default
type: disk
name: default

执行命令

在实例中执行一条命令:

1
incus exec|shell [服务器名:]实例名 -- 命令
  • 和Docker不一样,Incus不需要手动分配伪终端,它有能力在需要的情况下(启动Shell)自动分配(默认启用--mode auto选项)。不过,如果希望强制使用非交互模式,也是可以的,只需要添加-T|--force-noninteractive|--mode non-interactive选项。如果希望强制分配伪终端,添加-t|--force-interactive|--mode interactive选项。
  • 使用-n|--disable-stdin禁用标准输入,这会将标准输入连接到/dev/null
  • 默认情况下,Incus使用root用户和组运行命令,工作目录为/root,这些行为可以通过--user--group--cwd选项修改。
  • 使用--env ENV=VALUE选项设置环境变量。

要持久化设置环境变量,执行:

1
incus config set [服务器名:]实例名 environment.ENV=VALUE

进入终端

Incus可以通过一个伪终端对,一头在宿主机的命名空间,一头在实例的命名空间并连接到实例的/dev/console设备上以获取交互式终端:

1
incus console [服务器名:]实例名 [--show-log]
  • 要断开连接,按下Ctrl + A,然后再按Q
  • 因为容器空间中的伪终端总是pts/0,所以在宿主机上显示的容器中的进程的控制终端总是pts/0
  • 在宿主机视角下,/dev/pts/目录中会按顺序创建两个新的伪终端。
  • --show-log:打印日志,不进行交互。

对于虚拟机,尤其是没有安装incus-agent的虚拟机,可以使用对Agent没有要求的SPICE协议进行连接,不过,这需要系统上安装virt-viewer或是spicy,如果两个都没有安装,那么Incus会仅仅只是打开一个SPICE套接字,而不会自动调用客户端进行连接:

1
incus console [服务器名:]实例名 --type=vga

创建实例时可以自动进入终端:

1
incus init [服务器名:]镜像名 [服务器名:]实例名 --console|--console=vga

获取实例中文件

Incus通过一些手段,实现了直接编辑实例中的文件:

1
incus file edit [服务器名:]实例名/文件路径

注意,你不能用这种方式创建文件。

也可以删除实例中的文件:

1
incus file delete [服务器名:]实例名/文件路径

还可以获取实例中的文件到宿主机:

1
incus file pull [-r] [-p] [服务器名:]实例名/文件路径 宿主机文件路径
  • 如果宿主机文件路径是-,那么会直接打印文件内容(拷贝到标准输出)。
  • -r:递归拷贝。
  • -p:必要时创建目录。

上传宿主机文件:

1
incus file push [-r] [-p] 宿主机文件路径 [服务器名:]实例名/文件路径

映射宿主机目录:

1
incus file mount [服务器名:]实例名/路径 宿主机路径

配置SFTP服务器:

1
incus file mount [服务器名:]实例名 [--listen 地址:端口]
  • 默认情况下,地址为127.0.0.1,端口号随机生成。

这样就可以使用sshfs连接了:

1
sshfs 用户名@地址:/路径 本地路径 -p 端口号

迁移

实例可以在服务器间迁移。执行:

1
incus copy [服务器名:]实例名 服务器名:实例名
  • --allow-inconsistent:无视临时文件的拷贝失败。
  • -e|--ephemeral:目标容器是一个临时容器。
  • --instance-only:仅拷贝实例,不拷贝快照。
  • --refresh:对目标实例进行更新,即增量拷贝。
  • --stateless:拷贝时停止实例。
  • --mode pull|push|relay:传输模式,pull表示从源服务器拉取,源服务器必须对网络开放;push表示源服务器向目标服务器推送,目标服务器必须对网络开放;relay表示客户端负责从源服务器获取数据并转发给目标服务器,两个服务器都必须对网络开放。

项目(Project)

项目是一组相互关联的实例的合集。项目可以包含一系列的镜像、网络和存储。

Incus初始化时会附带一个default项目。

创建项目

1
incus project create 项目名 --config 配置项=值

查看项目:

1
incus project list

配置项目

1
incus project set 项目名 配置项=值

可用配置项有:

  • features.images:是否使用独立的镜像空间。
  • features.networks:是否使用独立的网络空间。
  • features.storage.buckets:是否使用独立的Bucket存储空间。
  • features.storage.volumes:是否使用独立的存储卷空间。
  • features.profiles:是否使用独立的Profile空间。
  • limits.containers:最大容器数量。
  • limits.virtual-machines:最大VM数量。
  • limits.instances:最大实例数量。
  • limits.cpu:最大总CPU资源。
  • limits.memory:最大总内存。
  • limits.disk:最大总存储。
  • limits.networks:最大总网络。
  • limits.processes:最大总进程。
  • restricted:是否启用限制。
  • restricted.backups:限制使用备份。
  • restricted.cluster.groups:限制可用的集群组。
  • restricted.cluster.target:限制是否可指定集群节点。
  • restricted.containers.interception:限制是否可用Interception。
  • restricted.containers.lowlevel:限制是否可用底层配置项如raw.*
  • restricted.containers.privilege:限制使用特权容器。
  • restricted.containers.nesting:限制容器嵌套。
  • restricted.virtual-machines.lowlevel:限制是否可用底层配置项如raw.*
  • restricted.device.*:限制指定类型设备的使用。

编辑项目配置:

1
incus project edit 项目名

切换项目

配置完成后,可以切换项目:

1
incus project switch 项目名

或者,可以在Project间移动实例:

1
incus move [服务器名:]实例名 [服务器名:]新实例名 --project 项目名 --target-project 项目名

用户特定项目

要将项目绑定到一个用户,执行:

1
incus config trust add --projects 项目名 --restricted
1
incus config trust add-certificate 证书路径 --projects 项目名 --restricted

自动绑定

incus-user是一个辅助服务,它的功能是在指定组(通过--group选项指定)的用户使用incus客户端时,自动为其创建受限项目。

杂记与BUG

Docker兼容性

Incus在默认情况下与Docker的兼容性不佳,这主要是因为Docker在安装时会把FORWARD链的默认行为修改为DROP,阻止了所有包的转发。解决方法有两种:

  1. 启用net.ipv4.ip_forward=1,这会使Docker不再把FORWARD链的默认行为修改为DROP
  2. 创建一条独立规则:iptables -I DOCKER-USER -i Incus网桥 -o 物理接口 -j ACCEPT
  3. 卸载Docker,在Incus虚拟机中使用。

Podman没这个问题。

声音

为了安全性,Incus的虚拟机预设中不会模拟除了Virtio组件以外的任何设备。虽然现在有一个正在开发的virtio-sound,但是仍然尚不完善。

如果希望手动添加声卡模拟,添加以下Config:

1
raw.qemu="-device intel-hda -device hda-duplex -audio spice"

嵌套虚拟化

要使用嵌套虚拟化需要把主机CPU的功能传递给虚拟机,具体方法为设置Config:

1
raw.qemu="-cpu host"

共享文件

在Windows虚拟机实例上使用:

1
incus config device add ...

添加主机目录映射后并不能直接访问,对此需要在虚拟机中安装两个工具:

需要TPM

添加设备:

1
incus config device add [服务器名:]实例名 vtpm tpm path=/dev/tpm0

挂载回环设备

回环设备不存在命名空间机制,要在容器中挂载回环设备,强烈建议在宿主机上挂载后映射目录。或者这样做:

1
2
3
4
5
incus config device add [服务器名:]实例名 loop-control unix-char source=/dev/loop-control

incus config device add [服务器名:]实例名 loopX unix-block source=/dev/loopX

incus config set [服务器名:]实例名 security.syscalls.intercept.mount=true security.syscalls.intercept.mount.allowed=文件系统

手动安装Agent

手动创建虚拟机并安装系统时需要手动安装Agent。执行:

1
mount -t 9p config /挂载点

然后以Root身份进入挂载点,执行install.sh即可。

Agent只支持systemd作为init系统的发行版。

登录时报错

在某些使用SysV init的发行版容器上,登录时可能会报错:

1
cannot set terminal process group (-1): Inappropriate ioctl for device

这是因为SysV init会占用/dev/console(参考:sysvinit/src/init.c)。在启动过程中,ioctl调用会失败,因为无特权容器中的进程无权再设置终端模式。解决办法有三个:

  1. 在容器中执行su -l "$USER" -P使用另一个伪终端进行操作。
  2. 使用别的init系统例如OpenRC。
  3. 将容器启用特权模式。

临时配置

Incus生成的临时LXC/Qemu配置文件位于/run/incus/目录下。

OCI容器

6.3版本以后,Incus提供了对OCI容器的原生支持,但是仅仅实现了基本的功能(下载镜像与运行),Incus没有计划实现更复杂的功能。

要使用Docker镜像站,执行:

1
2
incus remote add --protocol oci docker.io https://docker.io
incus remote add --protocol oci ghcr.io https://ghcr.io

安装依赖

要使用OCI容器,需要依赖以下可执行程序:

  • skopeo:用于检索镜像。
  • umoci:用于转换镜像。

添加OCI镜像源

在添加镜像源时,将协议设为oci即可:

1
incus remote add 名称 https://docker.io --protocol=oci
  • OCI镜像源不能被列出。

拉取镜像

和拉取Incus镜像一样:

1
incus image copy 源名:镜像名 local:|服务器名: --alias=镜像名

运行

和Incus系统容器一样运行即可:

1
incus create 镜像名 容器名
  • 强烈建议对于无状态容器添加--ephemeral选项。
  • 使用-c environment.变量名=变量值选项设置环境变量。