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 |

安装
Incus仅支持使用systemd的发行版。
目前Incus服务端在Debian 12+的官方源中进行提供:
1 | apt install incus |
Alpine 3.19+:
1 | apk add incus incus-client |
Fedora(尚未进入官方源):
1 | dnf install 'dnf-command(copr)' |
客户端不限平台,二进制程序可以在Github页面直接下载。
安装Incus后,用户的权限通过两个组管理:
- 被添加到
incus附加组的用户有权使用/var/lib/incus/unix.sock套接字以对Incus进行管理。 - 被添加到
incus-admin附加组的用户有权使用incus-user.socket套接字以创建自己的Project使用。
安装Incus后,存在5个systemd单元:
incus.service:incusd服务,它不应该被直接启用。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 |
命令进行初始化,初始化过程包括以下几个问题:
- 是否启用集群模式?默认为否。
- 如果选择是,那么会询问使用的IP、以及是否加入已存在的集群。
- 如果选择加入已存在的集群,那么会要求提供集群Token。
- 如果选择不加入,那么会询问新建的集群名称。
- 是否创建一个存储池?默认为是。
- 存储池默认名称为
default(可以修改),使用dir(或可以选择btrfs)存储引擎。
- 存储池默认名称为
- 是否自动创建网桥?默认为是。
- 自动创建的网桥是一个独立网桥。用户需要手动设置网桥使用的IPv4/IPv6子网(也可以让Incus自动选择)。
- 如果选择是,还会要求提供网桥的设备名称,默认为
incusbr0。 - 如果选择否,那么会要求用户选择一个已存在的网桥或网络接口。可以创建一个二层网桥,或是使用一个物理接口,不过如果是物理接口的话,会被Incus占用,不能再被宿主机使用。
- 是否在网络上暴露该服务器(即启用REST API)?默认为否。
- 是否定期更新镜像?默认为是。
- 是否打印一个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网络。
bridge和ovn常用参数有:
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的工作模式,可选值有bridge、vepa、private、passthru,默认为bridge。
sriov和physical参数有:
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 | devices: |
即可。
防止伪造
默认情况下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 | firewall-cmd --zone=trusted --change-interface=bridge网络设备 --permanent |
如果是UFW:
1 | ufw allow in 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网关,可选值为auto或none。ipv4|6.host_address:主机Veth接口的IP。
这种方式不是推荐方式,唯一实用的场景是手动创建一个二层网桥并使用bridged模式让实例接入,也就是说:
- 在宿主机上创建一个Host-shared网桥,如
br0。 - 执行
incus config device add|set [服务器名:]实例名 eth0 nic nictype=bridged parent=br0 name=eth0让实例接入。
- 这种场景现在应该被MACVLan或SR-IOV取代了。
或者,创建一个Profile,其中devices段如下:
1 | devices: |
编辑网络
要在创建后修改网络配置,执行:
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 | # Debian |
配置OVN网桥
执行以下命令:
1 | # 设置OVS的整体配置 |
配置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模式不需要。
- 对于直接代理模式来说,支持的协议有
tcp、udp、unix。 - 对于虚拟机来说,只能使用NAT模式,而且仅支持
tcp和udp协议。- 实例还必须有被特别分配的静态IP才能使用NAT模式。具体方法为
incus config device override 实例名 网络设备名称 ipv4.address=IPv4 ipv6.address=IPv6。对于IPv6地址来说,实例接入的网络必须启用ipv6.dhcp.stateful: true才能设置静态IP。
- 实例还必须有被特别分配的静态IP才能使用NAT模式。具体方法为
- 主机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:仅用于zfs和btrfs,启用优化存储方式。--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证书认证。具体的步骤是:
- 客户端在执行
incus remote add命令时,会使用HTTPS协议连接服务端,并下载服务端的证书。 - 客户端会为用户展示证书的指纹,并要求用户确认。
- 服务端对客户端进行认证,具体方式有两种:
- 添加信任证书:在客户端执行
incus remote generate-certificate生成证书,然后在服务端执行incus config add-certificate 文件路径将客户端的证书添加到服务端。在认证时,服务端会在本地查找是否存在客户端的证书。 - 使用Token:预先在服务端本地执行
incus config trust add 客户端名为客户端生成独一无二的Token。在认证时,服务端会要求用户提供Token。- Token是由有效期的,这通过
core.remote_token_expiry配置项设置。
- Token是由有效期的,这通过
- 添加信任证书:在客户端执行
修改默认证书
如果希望使用中央PKI,那么Incus也是支持PKI模式的。具体步骤为:
- 把CA证书上传到服务端的
/var/lib/incus目录。 - 把CA证书上传到客户端的
~/.config/incus目录。 - 把经过CA签名的证书上传到客户端和服务端,替换自动生成的证书。
- 重启服务端。
然后,按照前面提到的步骤配置证书认证即可。
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会引导用户交互式配置浏览器客户端证书:

另一种访问方式是,在已添加过该服务端(配置过证书或Token)的Incus客户端上,执行incus webui [服务端名称],Incus客户端会自动转发请求并在本地打开浏览器展示WebUI。
HTTP协议
Incus WebUI本身不支持通过未经验证和加密的HTTP协议访问,但是,Incus会监听/var/lib/incus/unix.socketUNIX域套接字,因此可以通过代理的方式强行达到目的。
配置incus-webui.socket:
1 | [Socket] |
配置incus-webui.service:
1 | [Unit] |
其他
重命名远端服务器:
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 | # 架构 |
在相同的工作目录下,创建templates/目录,在其中保存对应的模板文件。以我们这里的例子来说:
1 | # hosts.tpl |
1 | # hostname.tpl |
将rootfs/、metadata.yaml和template/目录共同打包为image.tar.gz。注意,打包时位置(-C选项)必须为工作目录。
从文件中导入镜像:
1 | # VM |
1 | # 容器 |
--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 | # 如果是公共服务器的话,需要添加--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的服务目录树,具体来说,会在当前目录下创建images和streams两个目录:
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。
- Qemu和
Incus Agent的工作原理:
- Incus启动的Qemu虚拟机具备一个Attribute为
org.linuxcontainers.incus的VSock,并通过Udev规则(准确地说,是50-udev-default.rules这条默认规则)挂载到/dev/virtio-ports/目录下。 - (Incus自动构建的镜像已包含,不用手动安装)用户挂载Incus通过
9p或virtiofs暴露的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通过9p或virtiofs暴露的config虚拟光盘,将其所有内容拷贝到/run/incus_agent目录下,然后再弹出虚拟光盘。- 准备完毕,
incus-agent二进制文件确定位于/run/incus_agent/incus-agent,incus-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 | incus list [匹配词=值] |
可用匹配词包括:
type=container|vm:类型。status=running|stopped:状态。location=节点名:所在节点。系统类型.*:系统类型。
查看实例信息:
1 | incus info [服务器名:]实例名 [--show-log] |
启动实例:
1 | incus start [服务器名:]实例名 [--console|--console=vga] |
--stateless:无视实例状态。
冻结实例:
1 | incus pause|freeze [服务器名:]实例名 |
恢复实例:
1 | incus resume|unfreeze [服务器名:]实例名 |
停止实例:
1 | incus stop [服务器名:]实例名 |
重启实例:
1 | incus restart [服务器名:]实例名 |
删除实例:
1 | incus delete [服务器名:]实例名 |
重建实例(保留配置,删除并重新创建):
1 | incus rebuild [服务器名:]镜像名 [服务器名:]实例名 [--empty] |
无特权容器
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 | incus config show [服务器名:]实例名 [--expanded] |
配置实例的命令为:
1 | incus config set [服务器名:]实例名 配置项=值 |
可用的配置项包括:
杂项:
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/目录下挂载procfs和sysfs,以及切换使用嵌套模式的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时有效,例如ext4、btrfs。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。
- 例如,禁用AppArmor:
用于NVIDIA显卡直通的特殊配置:
nvidia.runtime:在非特权容器启动时执行/usr/share/lxc/hooks/nvidia钩子脚本,加载GPU设备节点和NVIDIA用户空间驱动,使得非特权命名空间中的用户可以使用GPU设备节点,默认禁用。nvidia.driver.capabilities:允许调用的NVIDIA驱动功能,默认为compute,utility,all表示所有,video表示音视频转码。nvidia.require.cuda:需要的最小CUDA版本。如果容器内的CUDA小于该版本,容器会被禁止使用GPU。nvidia.require.driver:需要的最小驱动版本。
要给实例添加设备,执行:
1 | incus config device add [服务器名:]实例名 设备名 设备类型 [设备选项=值] |
这实际上就是修改了实例的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。这个功能需要mkisofs或genisoimage工具。
编辑实例配置:
1 | incus config edit [服务器名:]实例名 |
在容器中使用KVM
为容器启用security.nesting后,添加以下设备:
1 | incus config device add 容器名 kvm unix-char source=/dev/kvm |
针对容器的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直通需要一些额外配置。
- 修改
/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" - 参考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 - 执行
update-grub。 - 修改
/etc/modules,添加以下模块:1
2
3
4
5
6vfio
vfio_iommu_type1
vfio_pci
# 在6.2以后的内核版本中,该模块被包含在vfio内,因此不再需要
#vfio_virqfd - 创建
/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 - 创建
/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_pci1
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 - 执行
update-initramfs -u -k all,然后重启。启动后执行lspci -k,查看对应的设备正在使用的驱动(Kernel driver in use:)是否为vfio-pci或者空,如果是则可以用于直通。 - 在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:仅用于zfs和btrfs,启用优化存储方式。--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 | incus profile set 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 | config: |
执行命令
在实例中执行一条命令:
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,阻止了所有包的转发。解决方法有两种:
- 启用
net.ipv4.ip_forward=1,这会使Docker不再把FORWARD链的默认行为修改为DROP。 - 创建一条独立规则:
iptables -I DOCKER-USER -i Incus网桥 -o 物理接口 -j ACCEPT。 - 卸载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 ... |
添加主机目录映射后并不能直接访问,对此需要在虚拟机中安装两个工具:
- WinFSP:提供自定义文件系统功能。
- Virtio-guest-tools:提供自动映射服务。
需要TPM
添加设备:
1 | incus config device add [服务器名:]实例名 vtpm tpm path=/dev/tpm0 |
挂载回环设备
回环设备不存在命名空间机制,要在容器中挂载回环设备,强烈建议在宿主机上挂载后映射目录。或者这样做:
1 | incus config device add [服务器名:]实例名 loop-control unix-char source=/dev/loop-control |
手动安装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调用会失败,因为无特权容器中的进程无权再设置终端模式。解决办法有三个:
- 在容器中执行
su -l "$USER" -P使用另一个伪终端进行操作。 - 使用别的init系统例如OpenRC。
- 将容器启用特权模式。
临时配置
Incus生成的临时LXC/Qemu配置文件位于/run/incus/目录下。
OCI容器
6.3版本以后,Incus提供了对OCI容器的原生支持,但是仅仅实现了基本的功能(下载镜像与运行),Incus没有计划实现更复杂的功能。
要使用Docker镜像站,执行:
1 | incus remote add --protocol oci docker.io https://docker.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.变量名=变量值选项设置环境变量。