mirror of
https://github.com/siderolabs/talos.git
synced 2025-12-12 21:11:31 +01:00
Part of: #9127 Label executables and processes, build, load and manage SELinux policy, enable audit support. Labeling filesystems, devices and runtime files will be done in further changes, see the full PR. Signed-off-by: Dmitry Sharshakov <dmitry.sharshakov@siderolabs.com>
353 lines
9.4 KiB
Go
353 lines
9.4 KiB
Go
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
|
|
package qemu
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"math"
|
|
"net"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strconv"
|
|
"strings"
|
|
"syscall"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/hashicorp/go-multierror"
|
|
"github.com/siderolabs/gen/xslices"
|
|
"github.com/siderolabs/go-cmd/pkg/cmd"
|
|
"github.com/siderolabs/go-procfs/procfs"
|
|
|
|
"github.com/siderolabs/talos/pkg/machinery/constants"
|
|
"github.com/siderolabs/talos/pkg/machinery/kernel"
|
|
"github.com/siderolabs/talos/pkg/provision"
|
|
"github.com/siderolabs/talos/pkg/provision/providers/vm"
|
|
)
|
|
|
|
//nolint:gocyclo,cyclop
|
|
func (p *provisioner) createNode(state *vm.State, clusterReq provision.ClusterRequest, nodeReq provision.NodeRequest, opts *provision.Options) (provision.NodeInfo, error) {
|
|
arch := Arch(opts.TargetArch)
|
|
pidPath := state.GetRelativePath(fmt.Sprintf("%s.pid", nodeReq.Name))
|
|
|
|
var pflashImages []string
|
|
|
|
if pflashSpec := arch.PFlash(opts.UEFIEnabled, opts.ExtraUEFISearchPaths); pflashSpec != nil {
|
|
var err error
|
|
|
|
if pflashImages, err = p.createPFlashImages(state, nodeReq.Name, pflashSpec); err != nil {
|
|
return provision.NodeInfo{}, fmt.Errorf("error creating flash images: %w", err)
|
|
}
|
|
}
|
|
|
|
vcpuCount := int64(math.RoundToEven(float64(nodeReq.NanoCPUs) / 1000 / 1000 / 1000))
|
|
if vcpuCount < 2 {
|
|
vcpuCount = 1
|
|
}
|
|
|
|
memSize := nodeReq.Memory / 1024 / 1024
|
|
|
|
diskPaths, err := p.CreateDisks(state, nodeReq)
|
|
if err != nil {
|
|
return provision.NodeInfo{}, err
|
|
}
|
|
|
|
err = p.populateSystemDisk(diskPaths, clusterReq)
|
|
if err != nil {
|
|
return provision.NodeInfo{}, err
|
|
}
|
|
|
|
logFile, err := os.OpenFile(state.GetRelativePath(fmt.Sprintf("%s.log", nodeReq.Name)), os.O_APPEND|os.O_CREATE|os.O_RDWR, 0o666)
|
|
if err != nil {
|
|
return provision.NodeInfo{}, err
|
|
}
|
|
|
|
defer logFile.Close() //nolint:errcheck
|
|
|
|
cmdline := procfs.NewCmdline("")
|
|
|
|
cmdline.SetAll(kernel.DefaultArgs)
|
|
|
|
// required to get kernel console
|
|
cmdline.Append("console", arch.Console())
|
|
|
|
// reboot configuration
|
|
cmdline.Append("reboot", "k")
|
|
cmdline.Append("panic", "1")
|
|
cmdline.Append("talos.shutdown", "halt")
|
|
|
|
// Talos config
|
|
cmdline.Append("talos.platform", constants.PlatformMetal)
|
|
|
|
// SELinux
|
|
cmdline.Append("selinux", "1")
|
|
|
|
// add overrides
|
|
if nodeReq.ExtraKernelArgs != nil {
|
|
if err = cmdline.AppendAll(nodeReq.ExtraKernelArgs.Strings()); err != nil {
|
|
return provision.NodeInfo{}, err
|
|
}
|
|
}
|
|
|
|
if opts.WithDebugShell {
|
|
cmdline.Append("talos.debugshell", "")
|
|
}
|
|
|
|
var (
|
|
nodeConfig string
|
|
extraISOPath string
|
|
)
|
|
|
|
if !nodeReq.SkipInjectingConfig {
|
|
nodeConfig, err = nodeReq.Config.EncodeString()
|
|
if err != nil {
|
|
return provision.NodeInfo{}, err
|
|
}
|
|
|
|
switch nodeReq.ConfigInjectionMethod {
|
|
case provision.ConfigInjectionMethodHTTP:
|
|
cmdline.Append("talos.config", "{TALOS_CONFIG_URL}") // to be patched by launcher
|
|
case provision.ConfigInjectionMethodMetalISO:
|
|
cmdline.Append("talos.config", "metal-iso")
|
|
|
|
extraISOPath, err = p.createMetalConfigISO(state, nodeReq.Name, nodeConfig)
|
|
if err != nil {
|
|
return provision.NodeInfo{}, fmt.Errorf("error creating metal-iso: %w", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
nodeUUID := uuid.New()
|
|
if nodeReq.UUID != nil {
|
|
nodeUUID = *nodeReq.UUID
|
|
}
|
|
|
|
apiPort, err := p.findBridgeListenPort(clusterReq)
|
|
if err != nil {
|
|
return provision.NodeInfo{}, fmt.Errorf("error finding listen address for the API: %w", err)
|
|
}
|
|
|
|
defaultBootOrder := "cn"
|
|
if nodeReq.DefaultBootOrder != "" {
|
|
defaultBootOrder = nodeReq.DefaultBootOrder
|
|
}
|
|
|
|
// backwards compatibility, set Driver if not set
|
|
for i := range nodeReq.Disks {
|
|
if nodeReq.Disks[i].Driver != "" {
|
|
continue
|
|
}
|
|
|
|
if i == 0 {
|
|
nodeReq.Disks[i].Driver = "virtio"
|
|
} else {
|
|
nodeReq.Disks[i].Driver = "ide"
|
|
}
|
|
}
|
|
|
|
launchConfig := LaunchConfig{
|
|
ArchitectureData: arch,
|
|
DiskPaths: diskPaths,
|
|
DiskDrivers: xslices.Map(nodeReq.Disks, func(disk *provision.Disk) string {
|
|
return disk.Driver
|
|
}),
|
|
VCPUCount: vcpuCount,
|
|
MemSize: memSize,
|
|
KernelArgs: cmdline.String(),
|
|
ExtraISOPath: extraISOPath,
|
|
PFlashImages: pflashImages,
|
|
MonitorPath: state.GetRelativePath(fmt.Sprintf("%s.monitor", nodeReq.Name)),
|
|
EnableKVM: opts.TargetArch == runtime.GOARCH,
|
|
BadRTC: nodeReq.BadRTC,
|
|
DefaultBootOrder: defaultBootOrder,
|
|
BootloaderEnabled: opts.BootloaderEnabled,
|
|
NodeUUID: nodeUUID,
|
|
Config: nodeConfig,
|
|
BridgeName: state.BridgeName,
|
|
NetworkConfig: state.VMCNIConfig,
|
|
CNI: clusterReq.Network.CNI,
|
|
CIDRs: clusterReq.Network.CIDRs,
|
|
NoMasqueradeCIDRs: clusterReq.Network.NoMasqueradeCIDRs,
|
|
IPs: nodeReq.IPs,
|
|
GatewayAddrs: clusterReq.Network.GatewayAddrs,
|
|
MTU: clusterReq.Network.MTU,
|
|
Nameservers: clusterReq.Network.Nameservers,
|
|
TFTPServer: nodeReq.TFTPServer,
|
|
IPXEBootFileName: nodeReq.IPXEBootFilename,
|
|
APIPort: apiPort,
|
|
WithDebugShell: opts.WithDebugShell,
|
|
}
|
|
|
|
if clusterReq.IPXEBootScript != "" {
|
|
launchConfig.TFTPServer = clusterReq.Network.GatewayAddrs[0].String()
|
|
launchConfig.IPXEBootFileName = fmt.Sprintf("ipxe/%s/snp.efi", string(arch))
|
|
}
|
|
|
|
nodeInfo := provision.NodeInfo{
|
|
ID: pidPath,
|
|
UUID: nodeUUID,
|
|
Name: nodeReq.Name,
|
|
Type: nodeReq.Type,
|
|
|
|
NanoCPUs: nodeReq.NanoCPUs,
|
|
Memory: nodeReq.Memory,
|
|
DiskSize: nodeReq.Disks[0].Size,
|
|
|
|
IPs: nodeReq.IPs,
|
|
|
|
APIPort: apiPort,
|
|
}
|
|
|
|
if opts.TPM2Enabled {
|
|
tpm2, tpm2Err := p.createVirtualTPM2State(state, nodeReq.Name)
|
|
if tpm2Err != nil {
|
|
return provision.NodeInfo{}, tpm2Err
|
|
}
|
|
|
|
launchConfig.TPM2Config = tpm2
|
|
nodeInfo.TPM2StateDir = tpm2.StateDir
|
|
}
|
|
|
|
if !clusterReq.Network.DHCPSkipHostname {
|
|
launchConfig.Hostname = nodeReq.Name
|
|
}
|
|
|
|
if !(nodeReq.PXEBooted || launchConfig.IPXEBootFileName != "") {
|
|
launchConfig.KernelImagePath = strings.ReplaceAll(clusterReq.KernelPath, constants.ArchVariable, opts.TargetArch)
|
|
launchConfig.InitrdPath = strings.ReplaceAll(clusterReq.InitramfsPath, constants.ArchVariable, opts.TargetArch)
|
|
launchConfig.ISOPath = strings.ReplaceAll(clusterReq.ISOPath, constants.ArchVariable, opts.TargetArch)
|
|
}
|
|
|
|
launchConfig.StatePath, err = state.StatePath()
|
|
if err != nil {
|
|
return provision.NodeInfo{}, err
|
|
}
|
|
|
|
launchConfigFile, err := os.Create(state.GetRelativePath(fmt.Sprintf("%s.config", nodeReq.Name)))
|
|
if err != nil {
|
|
return provision.NodeInfo{}, err
|
|
}
|
|
|
|
if err = json.NewEncoder(launchConfigFile).Encode(&launchConfig); err != nil {
|
|
return provision.NodeInfo{}, err
|
|
}
|
|
|
|
if _, err = launchConfigFile.Seek(0, io.SeekStart); err != nil {
|
|
return provision.NodeInfo{}, err
|
|
}
|
|
|
|
defer launchConfigFile.Close() //nolint:errcheck
|
|
|
|
cmd := exec.Command(clusterReq.SelfExecutable, "qemu-launch")
|
|
cmd.Stdout = logFile
|
|
cmd.Stderr = logFile
|
|
cmd.Stdin = launchConfigFile
|
|
cmd.SysProcAttr = &syscall.SysProcAttr{
|
|
Setsid: true, // daemonize
|
|
}
|
|
|
|
if err = cmd.Start(); err != nil {
|
|
return provision.NodeInfo{}, err
|
|
}
|
|
|
|
if err = os.WriteFile(pidPath, []byte(strconv.Itoa(cmd.Process.Pid)), os.ModePerm); err != nil {
|
|
return provision.NodeInfo{}, fmt.Errorf("error writing PID file: %w", err)
|
|
}
|
|
|
|
// no need to wait here, as cmd has all the Stdin/out/err via *os.File
|
|
|
|
return nodeInfo, nil
|
|
}
|
|
|
|
func (p *provisioner) createNodes(state *vm.State, clusterReq provision.ClusterRequest, nodeReqs []provision.NodeRequest, opts *provision.Options) ([]provision.NodeInfo, error) {
|
|
errCh := make(chan error)
|
|
nodeCh := make(chan provision.NodeInfo, len(nodeReqs))
|
|
|
|
for _, nodeReq := range nodeReqs {
|
|
go func(nodeReq provision.NodeRequest) {
|
|
nodeInfo, err := p.createNode(state, clusterReq, nodeReq, opts)
|
|
if err == nil {
|
|
nodeCh <- nodeInfo
|
|
}
|
|
|
|
errCh <- err
|
|
}(nodeReq)
|
|
}
|
|
|
|
var multiErr *multierror.Error
|
|
|
|
for range nodeReqs {
|
|
multiErr = multierror.Append(multiErr, <-errCh)
|
|
}
|
|
|
|
close(nodeCh)
|
|
|
|
nodesInfo := make([]provision.NodeInfo, 0, len(nodeReqs))
|
|
|
|
for nodeInfo := range nodeCh {
|
|
nodesInfo = append(nodesInfo, nodeInfo)
|
|
}
|
|
|
|
return nodesInfo, multiErr.ErrorOrNil()
|
|
}
|
|
|
|
func (p *provisioner) findBridgeListenPort(clusterReq provision.ClusterRequest) (int, error) {
|
|
l, err := net.Listen("tcp", net.JoinHostPort(clusterReq.Network.GatewayAddrs[0].String(), "0"))
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
port := l.Addr().(*net.TCPAddr).Port
|
|
|
|
return port, l.Close()
|
|
}
|
|
|
|
func (p *provisioner) populateSystemDisk(disks []string, clusterReq provision.ClusterRequest) error {
|
|
if len(disks) > 0 && clusterReq.DiskImagePath != "" {
|
|
disk, err := os.OpenFile(disks[0], os.O_RDWR, 0o755)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer disk.Close() //nolint:errcheck
|
|
|
|
image, err := os.Open(clusterReq.DiskImagePath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer image.Close() //nolint:errcheck
|
|
|
|
_, err = io.Copy(disk, image)
|
|
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (p *provisioner) createMetalConfigISO(state *vm.State, nodeName, config string) (string, error) {
|
|
isoPath := state.GetRelativePath(nodeName + "-metal-config.iso")
|
|
|
|
tmpDir, err := os.MkdirTemp("", "talos-metal-config-iso")
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
defer os.RemoveAll(tmpDir) //nolint:errcheck
|
|
|
|
if err = os.WriteFile(filepath.Join(tmpDir, "config.yaml"), []byte(config), 0o644); err != nil {
|
|
return "", err
|
|
}
|
|
|
|
_, err = cmd.Run("mkisofs", "-joliet", "-rock", "-volid", "metal-iso", "-output", isoPath, tmpDir)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
return isoPath, nil
|
|
}
|