mirror of
https://github.com/siderolabs/talos.git
synced 2025-08-21 14:41:12 +02:00
Firecracker launches tries to open VM disk image before every boot, parses partition table, finds boot partition, tries to read it as FAT32 filesystem, extracts uncompressed kernel from `bzImage` (firecracker doesn't support `bzImage` yet), extracts initramfs and passes it to firecracker binary. This flow allows for extended tests, e.g. testing installer, upgrade and downgrade tests, etc. Bootloader emulation is disabled by default for now, can be enabled via `--with-bootloader-emulation` flag to `osctl cluster create`. Signed-off-by: Andrey Smirnov <smirnov.andrey@gmail.com>
241 lines
6.4 KiB
Go
241 lines
6.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 firecracker
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"io/ioutil"
|
|
"math"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strconv"
|
|
"syscall"
|
|
|
|
"github.com/firecracker-microvm/firecracker-go-sdk"
|
|
models "github.com/firecracker-microvm/firecracker-go-sdk/client/models"
|
|
"github.com/hashicorp/go-multierror"
|
|
"k8s.io/apimachinery/pkg/util/json"
|
|
|
|
"github.com/talos-systems/talos/internal/pkg/kernel"
|
|
"github.com/talos-systems/talos/internal/pkg/provision"
|
|
)
|
|
|
|
func (p *provisioner) createDisk(state *state, nodeReq provision.NodeRequest) (diskPath string, err error) {
|
|
diskPath = filepath.Join(state.statePath, fmt.Sprintf("%s.disk", nodeReq.Name))
|
|
|
|
var diskF *os.File
|
|
|
|
diskF, err = os.Create(diskPath)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
defer diskF.Close() //nolint: errcheck
|
|
|
|
err = diskF.Truncate(nodeReq.DiskSize)
|
|
|
|
return
|
|
}
|
|
|
|
func (p *provisioner) createNodes(state *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)
|
|
errCh <- err
|
|
|
|
if err == nil {
|
|
nodeCh <- nodeInfo
|
|
}
|
|
}(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()
|
|
}
|
|
|
|
//nolint: gocyclo
|
|
func (p *provisioner) createNode(state *state, clusterReq provision.ClusterRequest, nodeReq provision.NodeRequest, opts *provision.Options) (provision.NodeInfo, error) {
|
|
socketPath := filepath.Join(state.statePath, fmt.Sprintf("%s.sock", nodeReq.Name))
|
|
pidPath := filepath.Join(state.statePath, fmt.Sprintf("%s.pid", nodeReq.Name))
|
|
|
|
vcpuCount := int64(math.RoundToEven(float64(nodeReq.NanoCPUs) / 1000 / 1000 / 1000))
|
|
if vcpuCount < 2 {
|
|
vcpuCount = 1
|
|
}
|
|
|
|
memSize := nodeReq.Memory / 1024 / 1024
|
|
|
|
diskPath, err := p.createDisk(state, nodeReq)
|
|
if err != nil {
|
|
return provision.NodeInfo{}, err
|
|
}
|
|
|
|
cmdline := kernel.NewDefaultCmdline()
|
|
|
|
// required to get kernel console
|
|
cmdline.Append("console", "ttyS0")
|
|
|
|
// reboot configuration
|
|
cmdline.Append("reboot", "k")
|
|
cmdline.Append("panic", "1")
|
|
|
|
// disable stuff we don't need
|
|
cmdline.Append("pci", "off")
|
|
cmdline.Append("acpi", "off")
|
|
cmdline.Append("i8042.noaux", "")
|
|
|
|
// Talos config
|
|
cmdline.Append("talos.platform", "metal")
|
|
cmdline.Append("talos.config", "{TALOS_CONFIG_URL}") // to be patched by launcher
|
|
cmdline.Append("talos.hostname", nodeReq.Name)
|
|
|
|
ones, _ := clusterReq.Network.CIDR.Mask.Size()
|
|
|
|
cfg := firecracker.Config{
|
|
SocketPath: socketPath,
|
|
KernelImagePath: clusterReq.KernelPath,
|
|
KernelArgs: cmdline.String(),
|
|
InitrdPath: clusterReq.InitramfsPath,
|
|
ForwardSignals: []os.Signal{}, // don't forward any signals
|
|
MachineCfg: models.MachineConfiguration{
|
|
HtEnabled: firecracker.Bool(false),
|
|
VcpuCount: firecracker.Int64(vcpuCount),
|
|
MemSizeMib: firecracker.Int64(memSize),
|
|
},
|
|
NetworkInterfaces: firecracker.NetworkInterfaces{
|
|
firecracker.NetworkInterface{
|
|
CNIConfiguration: &firecracker.CNIConfiguration{
|
|
BinPath: clusterReq.Network.CNI.BinPath,
|
|
ConfDir: clusterReq.Network.CNI.ConfDir,
|
|
CacheDir: clusterReq.Network.CNI.CacheDir,
|
|
NetworkConfig: state.vmCNIConfig,
|
|
Args: [][2]string{
|
|
{"IP", fmt.Sprintf("%s/%d", nodeReq.IP, ones)},
|
|
{"GATEWAY", clusterReq.Network.GatewayAddr.String()},
|
|
},
|
|
IfName: "veth0",
|
|
VMIfName: "eth0",
|
|
},
|
|
},
|
|
},
|
|
Drives: []models.Drive{
|
|
{
|
|
DriveID: firecracker.String("disk"),
|
|
IsRootDevice: firecracker.Bool(false),
|
|
IsReadOnly: firecracker.Bool(false),
|
|
PathOnHost: firecracker.String(diskPath),
|
|
},
|
|
},
|
|
}
|
|
|
|
logFile, err := os.OpenFile(filepath.Join(state.statePath, fmt.Sprintf("%s.log", nodeReq.Name)), os.O_APPEND|os.O_CREATE|os.O_RDWR, 0666)
|
|
if err != nil {
|
|
return provision.NodeInfo{}, err
|
|
}
|
|
|
|
defer logFile.Close() //nolint: errcheck
|
|
|
|
nodeConfig, err := nodeReq.Config.String()
|
|
if err != nil {
|
|
return provision.NodeInfo{}, err
|
|
}
|
|
|
|
launchConfig := LaunchConfig{
|
|
FirecrackerConfig: cfg,
|
|
Config: nodeConfig,
|
|
GatewayAddr: clusterReq.Network.GatewayAddr.String(),
|
|
BootloaderEmulation: opts.BootloaderEmulation,
|
|
}
|
|
|
|
launchConfigFile, err := os.Create(filepath.Join(state.statePath, 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, "firecracker-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 = ioutil.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
|
|
|
|
nodeInfo := provision.NodeInfo{
|
|
ID: pidPath,
|
|
Name: nodeReq.Name,
|
|
Type: nodeReq.Config.Machine().Type(),
|
|
|
|
NanoCPUs: nodeReq.NanoCPUs,
|
|
Memory: nodeReq.Memory,
|
|
DiskSize: nodeReq.DiskSize,
|
|
|
|
PrivateIP: nodeReq.IP,
|
|
}
|
|
|
|
return nodeInfo, nil
|
|
}
|
|
|
|
func (p *provisioner) destroyNodes(cluster provision.ClusterInfo, options *provision.Options) error {
|
|
errCh := make(chan error)
|
|
|
|
for _, node := range cluster.Nodes {
|
|
go func(node provision.NodeInfo) {
|
|
fmt.Fprintln(options.LogWriter, "stopping VM", node.Name)
|
|
|
|
errCh <- p.destroyNode(node)
|
|
}(node)
|
|
}
|
|
|
|
var multiErr *multierror.Error
|
|
|
|
for range cluster.Nodes {
|
|
multiErr = multierror.Append(multiErr, <-errCh)
|
|
}
|
|
|
|
return multiErr.ErrorOrNil()
|
|
}
|
|
|
|
func (p *provisioner) destroyNode(node provision.NodeInfo) error {
|
|
return stopProcessByPidfile(node.ID) // node.ID stores PID path for control process
|
|
}
|