19
"github.com/alexflint/go-filemutex"
20
"github.com/containernetworking/cni/libcni"
21
"github.com/containernetworking/cni/pkg/types"
22
types100 "github.com/containernetworking/cni/pkg/types/100"
23
"github.com/containernetworking/plugins/pkg/ns"
24
"github.com/containernetworking/plugins/pkg/testutils"
25
"github.com/containernetworking/plugins/pkg/utils"
26
"github.com/coreos/go-iptables/iptables"
27
"github.com/google/uuid"
28
"github.com/siderolabs/gen/xslices"
29
"github.com/siderolabs/go-blockdevice/blockdevice/partition/gpt"
30
sideronet "github.com/siderolabs/net"
32
"github.com/siderolabs/talos/pkg/provision"
33
"github.com/siderolabs/talos/pkg/provision/internal/cniutils"
34
"github.com/siderolabs/talos/pkg/provision/providers/vm"
38
type LaunchConfig struct {
47
KernelImagePath string
54
DefaultBootOrder string
56
BootloaderEnabled bool
66
NetworkConfig *libcni.NetworkConfigList
67
CNI provision.CNIConfig
70
NoMasqueradeCIDRs []netip.Prefix
72
GatewayAddrs []netip.Addr
74
Nameservers []netip.Addr
79
IPXEBootFileName string
93
controller *Controller
96
type tpm2Config struct {
104
func withCNIOperationLocked[T any](config *LaunchConfig, f func() (T, error)) (T, error) {
107
lock, err := filemutex.New(filepath.Join(config.StatePath, "cni.lock"))
109
return zeroT, fmt.Errorf("failed to create CNI lock: %w", err)
112
if err = lock.Lock(); err != nil {
113
return zeroT, fmt.Errorf("failed to acquire CNI lock: %w", err)
117
if err := lock.Close(); err != nil {
118
log.Printf("failed to release CNI lock: %s", err)
126
func withCNIOperationLockedNoResult(config *LaunchConfig, f func() error) error {
127
_, err := withCNIOperationLocked(config, func() (struct{}, error) {
128
return struct{}{}, f()
138
func withCNI(ctx context.Context, config *LaunchConfig, f func(config *LaunchConfig) error) error {
140
containerID := uuid.New().String()
142
cniConfig := libcni.NewCNIConfigWithCacheDir(config.CNI.BinPath, config.CNI.CacheDir, nil)
145
ns, err := testutils.NewNS()
152
testutils.UnmountNS(ns)
155
ips := make([]string, len(config.IPs))
157
ips[j] = sideronet.FormatCIDR(config.IPs[j], config.CIDRs[j])
160
gatewayAddrs := xslices.Map(config.GatewayAddrs, netip.Addr.String)
162
runtimeConf := libcni.RuntimeConf{
163
ContainerID: containerID,
167
{"IP", strings.Join(ips, ",")},
168
{"GATEWAY", strings.Join(gatewayAddrs, ",")},
169
{"IgnoreUnknown", "1"},
174
err = withCNIOperationLockedNoResult(
177
return cniConfig.DelNetworkList(ctx, config.NetworkConfig, &runtimeConf)
181
return fmt.Errorf("error deleting CNI network: %w", err)
184
res, err := withCNIOperationLocked(
186
func() (types.Result, error) {
187
return cniConfig.AddNetworkList(ctx, config.NetworkConfig, &runtimeConf)
191
return fmt.Errorf("error provisioning CNI network: %w", err)
195
if e := withCNIOperationLockedNoResult(
198
return cniConfig.DelNetworkList(ctx, config.NetworkConfig, &runtimeConf)
201
log.Printf("error cleaning up CNI: %s", e)
205
currentResult, err := types100.NewResultFromResult(res)
207
return fmt.Errorf("failed to parse cni result: %w", err)
210
vmIface, tapIface, err := cniutils.VMTapPair(currentResult, containerID)
213
"failed to parse VM network configuration from CNI output, ensure CNI is configured with a plugin " +
214
"that supports automatic VM network configuration such as tc-redirect-tap")
217
cniChain := utils.FormatChainName(config.NetworkConfig.Name, containerID)
219
ipt, err := iptables.New()
221
return fmt.Errorf("failed to initialize iptables: %w", err)
227
if err = ipt.InsertUnique("nat", cniChain, 1, "--destination", "255.255.255.255/32", "-j", "ACCEPT"); err != nil {
228
return fmt.Errorf("failed to insert iptables rule to allow broadcast traffic: %w", err)
231
for _, cidr := range config.NoMasqueradeCIDRs {
232
if err = ipt.InsertUnique("nat", cniChain, 1, "--destination", cidr.String(), "-j", "ACCEPT"); err != nil {
233
return fmt.Errorf("failed to insert iptables rule to allow non-masquerade traffic to cidr %q: %w", cidr.String(), err)
237
config.tapName = tapIface.Name
238
config.vmMAC = vmIface.Mac
241
for j := range config.CIDRs {
242
nameservers := make([]netip.Addr, 0, len(config.Nameservers))
245
for i := range config.Nameservers {
246
if config.IPs[j].Is6() {
247
if config.Nameservers[i].Is6() {
248
nameservers = append(nameservers, config.Nameservers[i])
251
if config.Nameservers[i].Is4() {
252
nameservers = append(nameservers, config.Nameservers[i])
258
if err = vm.DumpIPAMRecord(config.StatePath, vm.IPAMRecord{
260
Netmask: byte(config.CIDRs[j].Bits()),
262
Hostname: config.Hostname,
263
Gateway: config.GatewayAddrs[j],
265
Nameservers: nameservers,
266
TFTPServer: config.TFTPServer,
267
IPXEBootFilename: config.IPXEBootFileName,
276
func checkPartitions(config *LaunchConfig) (bool, error) {
277
disk, err := os.Open(config.DiskPaths[0])
279
return false, fmt.Errorf("failed to open disk file %w", err)
284
diskTable, err := gpt.Open(disk)
286
if errors.Is(err, gpt.ErrPartitionTableDoesNotExist) {
290
return false, fmt.Errorf("error creating GPT object: %w", err)
293
if err = diskTable.Read(); err != nil {
297
return len(diskTable.Partitions().Items()) > 0, nil
303
func launchVM(config *LaunchConfig) error {
304
bootOrder := config.DefaultBootOrder
306
if config.controller.ForcePXEBoot() {
313
cpuArg += ",-kvmclock"
317
"-m", strconv.FormatInt(config.MemSize, 10),
318
"-smp", fmt.Sprintf("cpus=%d", config.VCPUCount),
321
"-netdev", fmt.Sprintf("tap,id=net0,ifname=%s,script=no,downscript=no", config.tapName),
322
"-device", fmt.Sprintf("virtio-net-pci,netdev=net0,mac=%s", config.vmMAC),
325
"-device", "virtio-rng-pci",
326
"-device", "virtio-balloon,deflate-on-oom=on",
327
"-monitor", fmt.Sprintf("unix:%s,server,nowait", config.MonitorPath),
329
"-boot", fmt.Sprintf("order=%s,reboot-timeout=5000", bootOrder),
330
"-smbios", fmt.Sprintf("type=1,uuid=%s", config.NodeUUID),
331
"-chardev", fmt.Sprintf("socket,path=%s/%s.sock,server=on,wait=off,id=qga0", config.StatePath, config.Hostname),
332
"-device", "virtio-serial",
333
"-device", "virtserialport,chardev=qga0,name=org.qemu.guest_agent.0",
334
"-device", "i6300esb,id=watchdog0",
340
scsiAttached, ahciAttached, nvmeAttached bool
344
for i, disk := range config.DiskPaths {
345
driver := config.DiskDrivers[i]
349
args = append(args, "-drive", fmt.Sprintf("format=raw,if=virtio,file=%s,cache=none,", disk))
351
args = append(args, "-drive", fmt.Sprintf("format=raw,if=ide,file=%s,cache=none,", disk))
354
args = append(args, "-device", "ahci,id=ahci0")
359
"-drive", fmt.Sprintf("id=ide%d,format=raw,if=none,file=%s", i, disk),
360
"-device", fmt.Sprintf("ide-hd,drive=ide%d,bus=ahci0.%d", i, ahciBus),
366
args = append(args, "-device", "virtio-scsi-pci,id=scsi0")
371
"-drive", fmt.Sprintf("id=scsi%d,format=raw,if=none,file=%s,discard=unmap,aio=native,cache=none", i, disk),
372
"-device", fmt.Sprintf("scsi-hd,drive=scsi%d,bus=scsi0.0", i),
378
"-device", "nvme,id=nvme-ctrl-0,serial=deadbeef",
384
"-drive", fmt.Sprintf("id=nvme%d,format=raw,if=none,file=%s,discard=unmap,aio=native,cache=none", i, disk),
385
"-device", fmt.Sprintf("nvme-ns,drive=nvme%d", i),
388
return fmt.Errorf("unsupported disk driver %q", driver)
392
machineArg := config.MachineType
394
if config.EnableKVM {
395
machineArg += ",accel=kvm,smm=on"
398
args = append(args, "-machine", machineArg)
400
pflashArgs := make([]string, 2*len(config.PFlashImages))
401
for i := range config.PFlashImages {
402
pflashArgs[2*i] = "-drive"
403
pflashArgs[2*i+1] = fmt.Sprintf("file=%s,format=raw,if=pflash", config.PFlashImages[i])
406
args = append(args, pflashArgs...)
409
diskBootable, err := checkPartitions(config)
414
if config.TPM2Config.NodeName != "" {
415
tpm2SocketPath := filepath.Join(config.TPM2Config.StateDir, "swtpm.sock")
417
cmd := exec.Command("swtpm", []string{
420
fmt.Sprintf("dir=%s,mode=0644", config.TPM2Config.StateDir),
422
fmt.Sprintf("type=unixio,path=%s", tpm2SocketPath),
425
fmt.Sprintf("file=%s", filepath.Join(config.TPM2Config.StateDir, "swtpm.pid")),
427
fmt.Sprintf("file=%s,level=20", filepath.Join(config.TPM2Config.StateDir, "swtpm.log")),
430
log.Printf("starting swtpm: %s", cmd.String())
432
if err := cmd.Start(); err != nil {
438
fmt.Sprintf("socket,id=chrtpm,path=%s", tpm2SocketPath),
440
"emulator,id=tpm0,chardev=chrtpm",
442
"tpm-tis,tpmdev=tpm0",
446
if !diskBootable || !config.BootloaderEnabled {
447
if config.ISOPath != "" {
449
"-cdrom", config.ISOPath,
451
} else if config.KernelImagePath != "" {
453
"-kernel", config.KernelImagePath,
454
"-initrd", config.InitrdPath,
455
"-append", config.KernelArgs,
463
"base=2011-11-11T11:11:00,clock=rt",
467
fmt.Fprintf(os.Stderr, "starting %s with args:\n%s\n", config.QemuExecutable, strings.Join(args, " "))
469
config.QemuExecutable,
473
cmd.Stdout = os.Stdout
474
cmd.Stderr = os.Stderr
476
if err := ns.WithNetNSPath(config.ns.Path(), func(_ ns.NetNS) error {
482
done := make(chan error)
490
case sig := <-config.c:
491
fmt.Fprintf(os.Stderr, "exiting VM as signal %s was received\n", sig)
493
if err := cmd.Process.Kill(); err != nil {
494
return fmt.Errorf("failed to kill process %w", err)
499
return errors.New("process stopped")
502
return fmt.Errorf("process exited with error %s", err)
507
case command := <-config.controller.CommandsCh():
508
if command == VMCommandStop {
509
fmt.Fprintf(os.Stderr, "exiting VM as stop command via API was received\n")
511
if err := cmd.Process.Kill(); err != nil {
512
return fmt.Errorf("failed to kill process %w", err)
536
var config LaunchConfig
538
ctx := context.Background()
540
if err := vm.ReadConfig(&config); err != nil {
544
config.c = vm.ConfigureSignals()
545
config.controller = NewController()
547
httpServer, err := vm.NewHTTPServer(config.GatewayAddrs[0], config.APIPort, []byte(config.Config), config.controller)
553
defer httpServer.Shutdown(ctx)
556
config.KernelArgs = strings.ReplaceAll(config.KernelArgs, "{TALOS_CONFIG_URL}", fmt.Sprintf("http://%s/config.yaml", httpServer.GetAddr()))
558
return withCNI(ctx, &config, func(config *LaunchConfig) error {
560
for config.controller.PowerState() != PoweredOn {
562
case <-config.controller.CommandsCh():
564
case sig := <-config.c:
565
fmt.Fprintf(os.Stderr, "exiting stopped launcher as signal %s was received\n", sig)
567
return errors.New("process stopped")
571
if err := launchVM(config); err != nil {