feat: qemu provisioner

Starts and stops qemu VMs, has some initial configuration subset.
Sets up networking through CNI tools, sets up DHCP server which gives IP
addresses to nodes.

Signed-off-by: Artem Chernyshev <artem.0xD2@gmail.com>
This commit is contained in:
Artem Chernyshev 2020-07-21 15:12:01 +03:00 committed by talos-bot
parent 6f5d24cc3d
commit c6eb18eed5
26 changed files with 1272 additions and 80 deletions

View File

@ -41,6 +41,7 @@ var (
registryMirrors []string
kubernetesVersion string
nodeVmlinuxPath string
nodeVmlinuzPath string
nodeInitramfsPath string
bootloaderEmulation bool
configDebug bool
@ -153,9 +154,10 @@ func create(ctx context.Context) (err error) {
},
},
Image: nodeImage,
KernelPath: nodeVmlinuxPath,
InitramfsPath: nodeInitramfsPath,
Image: nodeImage,
UncompressedKernelPath: nodeVmlinuxPath,
CompressedKernelPath: nodeVmlinuzPath,
InitramfsPath: nodeInitramfsPath,
SelfExecutable: os.Args[0],
StateDirectory: stateDir,
@ -412,6 +414,7 @@ func init() {
createCmd.Flags().StringVar(&nodeImage, "image", helpers.DefaultImage(constants.DefaultTalosImageRepository), "the image to use")
createCmd.Flags().StringVar(&nodeInstallImage, "install-image", helpers.DefaultImage(constants.DefaultInstallerImageRepository), "the installer image to use")
createCmd.Flags().StringVar(&nodeVmlinuxPath, "vmlinux-path", helpers.ArtifactPath(constants.KernelUncompressedAsset), "the uncompressed kernel image to use")
createCmd.Flags().StringVar(&nodeVmlinuzPath, "vmlinuz-path", helpers.ArtifactPath(constants.KernelAsset), "the compressed kernel image to use")
createCmd.Flags().StringVar(&nodeInitramfsPath, "initrd-path", helpers.ArtifactPath(constants.InitramfsAsset), "the uncompressed kernel image to use")
createCmd.Flags().BoolVar(&bootloaderEmulation, "with-bootloader-emulation", false, "enable bootloader emulation to load kernel and initramfs from disk image")
createCmd.Flags().StringSliceVar(&registryMirrors, "registry-mirror", []string{}, "list of registry mirrors to use in format: <registry host>=<mirror URL>")

View File

@ -0,0 +1,38 @@
// 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 mgmt
import (
"net"
"github.com/spf13/cobra"
"github.com/talos-systems/talos/internal/pkg/provision/providers/vm"
)
var dhcpdLaunchCmdFlags struct {
addr string
ifName string
statePath string
}
// dhcpdLaunchCmd represents the loadbalancer-launch command.
var dhcpdLaunchCmd = &cobra.Command{
Use: "dhcpd-launch",
Short: "Internal command used by VM provisioners",
Long: ``,
Args: cobra.NoArgs,
Hidden: true,
RunE: func(cmd *cobra.Command, args []string) error {
return vm.DHCPd(dhcpdLaunchCmdFlags.ifName, net.ParseIP(dhcpdLaunchCmdFlags.addr), dhcpdLaunchCmdFlags.statePath)
},
}
func init() {
dhcpdLaunchCmd.Flags().StringVar(&dhcpdLaunchCmdFlags.addr, "addr", "localhost", "IP address to listen on")
dhcpdLaunchCmd.Flags().StringVar(&dhcpdLaunchCmdFlags.ifName, "interface", "", "interface to listen on")
dhcpdLaunchCmd.Flags().StringVar(&dhcpdLaunchCmdFlags.statePath, "state-path", "", "path to state directory")
addCommand(dhcpdLaunchCmd)
}

View File

@ -0,0 +1,29 @@
// 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/.
// +build linux
package mgmt
import (
"github.com/spf13/cobra"
"github.com/talos-systems/talos/internal/pkg/provision/providers/qemu"
)
// qemuLaunchCmd represents the qemu-launch command.
var qemuLaunchCmd = &cobra.Command{
Use: "qemu-launch",
Short: "Internal command used by Firecracker provisioner",
Long: ``,
Args: cobra.NoArgs,
Hidden: true,
RunE: func(cmd *cobra.Command, args []string) error {
return qemu.Launch()
},
}
func init() {
addCommand(qemuLaunchCmd)
}

View File

@ -39,6 +39,7 @@ talosctl cluster create [flags]
--nameservers strings list of nameservers to use (default [8.8.8.8,1.1.1.1])
--registry-mirror strings list of registry mirrors to use in format: <registry host>=<mirror URL>
--vmlinux-path string the uncompressed kernel image to use (default "_out/vmlinux")
--vmlinuz-path string the compressed kernel image to use (default "_out/vmlinuz")
--wait wait for the cluster to be ready before returning (default true)
--wait-timeout duration timeout to wait for the cluster to be ready (default 20m0s)
--with-bootloader-emulation enable bootloader emulation to load kernel and initramfs from disk image

59
hack/test/e2e-qemu.sh Executable file
View File

@ -0,0 +1,59 @@
#!/bin/bash
set -eou pipefail
source ./hack/test/e2e.sh
PROVISIONER=qemu
CLUSTER_NAME=e2e-${PROVISIONER}
case "${CI:-false}" in
true)
REGISTRY="127.0.0.1:5000"
REGISTRY_ADDR=`docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' registry`
QEMU_FLAGS="--registry-mirror ${REGISTRY}=http://${REGISTRY_ADDR}:5000"
INSTALLER_TAG="${TAG}"
docker tag ${INSTALLER_IMAGE} 127.0.0.1:5000/autonomy/installer:"${TAG}"
docker push 127.0.0.1:5000/autonomy/installer:"${TAG}"
;;
*)
QEMU_FLAGS=
INSTALLER_TAG="latest"
;;
esac
case "${CUSTOM_CNI_URL:-false}" in
false)
CUSTOM_CNI_FLAG=
;;
*)
CUSTOM_CNI_FLAG="--custom-cni-url=${CUSTOM_CNI_URL}"
;;
esac
function create_cluster {
"${TALOSCTL}" cluster create \
--provisioner "${PROVISIONER}" \
--name "${CLUSTER_NAME}" \
--masters=3 \
--mtu 1500 \
--memory 2048 \
--cpus 2.0 \
--cidr 172.20.1.0/24 \
--install-image ${REGISTRY:-docker.io}/autonomy/installer:${INSTALLER_TAG} \
--with-init-node=false \
--crashdump \
${QEMU_FLAGS} \
${CUSTOM_CNI_FLAG}
"${TALOSCTL}" config node 172.20.1.2
}
function destroy_cluster() {
"${TALOSCTL}" cluster destroy --name "${CLUSTER_NAME}"
}
create_cluster
get_kubeconfig
run_talos_integration_test
run_kubernetes_integration_test

View File

@ -0,0 +1,87 @@
// 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 cniutils provides helper functions to parse CNI results.
package cniutils
import (
"fmt"
"github.com/containernetworking/cni/pkg/types/current"
)
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License"). You may
// not use this file except in compliance with the License. A copy of the
// License is located at
//
// http://aws.amazon.com/apache2.0/
//
// or in the "license" file accompanying this file. This file is distributed
// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
// express or implied. See the License for the specific language governing
// permissions and limitations under the License.
// FilterBySandbox returns scans the provided list of interfaces and returns two lists: the first
// are a list of interfaces with the provided sandboxID, the second are the other interfaces not
// in that sandboxID.
func FilterBySandbox(sandbox string, ifaces ...*current.Interface) (in, out []*current.Interface) {
for _, iface := range ifaces {
if iface.Sandbox == sandbox {
in = append(in, iface)
} else {
out = append(out, iface)
}
}
return
}
// IfacesWithName scans the provided list of ifaces and returns the ones with the provided name.
func IfacesWithName(name string, ifaces ...*current.Interface) []*current.Interface {
var foundIfaces []*current.Interface
for _, iface := range ifaces {
if iface.Name == name {
foundIfaces = append(foundIfaces, iface)
}
}
return foundIfaces
}
// VMTapPair takes a CNI result and returns the vm iface and the tap iface corresponding
// to the provided vmID. See the vmconf package docs for details on the expected
// vm and tap iface configurations.
func VMTapPair(result *current.Result, vmID string) (vmIface, tapIface *current.Interface, err error) {
vmIfaces, otherIfaces := FilterBySandbox(vmID, result.Interfaces...)
if len(vmIfaces) > 1 {
return nil, nil, fmt.Errorf(
"expected to find at most 1 interface in sandbox %q, but instead found %d",
vmID, len(vmIfaces))
} else if len(vmIfaces) == 0 {
return nil, nil, fmt.Errorf("no pseudo-device for %s", vmID)
}
vmIface = vmIfaces[0]
// As specified in the package docstring, the vm interface is given the same name as the
// corresponding tap device outside the VM. The tap device, however, will be in a sandbox
// corresponding to a network namespace path.
tapName := vmIface.Name
tapIfaces := IfacesWithName(tapName, otherIfaces...)
if len(tapIfaces) > 1 {
return nil, nil, fmt.Errorf(
"expected to find at most 1 interface with name %q, but instead found %d",
tapName, len(tapIfaces))
} else if len(tapIfaces) == 0 {
return nil, nil, fmt.Errorf("device not found: %s", tapName)
}
tapIface = tapIfaces[0]
return vmIface, tapIface, nil
}

View File

@ -19,6 +19,8 @@ func Factory(ctx context.Context, name string) (provision.Provisioner, error) {
return docker.NewProvisioner(ctx)
case "firecracker":
return newFirecracker(ctx)
case "qemu":
return newQemu(ctx)
default:
return nil, fmt.Errorf("unsupported provisioner %q", name)
}

View File

@ -25,7 +25,7 @@ func (p *provisioner) Destroy(ctx context.Context, cluster provision.Cluster, op
fmt.Fprintln(options.LogWriter, "stopping VMs")
if err := p.destroyNodes(cluster.Info(), &options); err != nil {
if err := p.DestroyNodes(cluster.Info(), &options); err != nil {
return err
}

View File

@ -19,7 +19,7 @@ type provisioner struct {
vm.Provisioner
}
// NewProvisioner initializes docker provisioner.
// NewProvisioner initializes firecracker provisioner.
func NewProvisioner(ctx context.Context) (provision.Provisioner, error) {
p := &provisioner{
vm.Provisioner{

View File

@ -6,22 +6,20 @@ package firecracker
import (
"context"
"encoding/json"
"fmt"
"net"
"os"
"os/signal"
"strings"
"syscall"
"time"
firecracker "github.com/firecracker-microvm/firecracker-go-sdk"
"github.com/talos-systems/talos/internal/pkg/inmemhttp"
"github.com/talos-systems/talos/internal/pkg/provision/providers/vm"
)
// LaunchConfig is passed in to the Launch function over stdin.
type LaunchConfig struct {
GatewayAddr string
GatewayAddr net.IP
Config string
BootloaderEmulation bool
FirecrackerConfig firecracker.Config
@ -44,33 +42,16 @@ type LaunchConfig struct {
func Launch() error {
var config LaunchConfig
d := json.NewDecoder(os.Stdin)
if err := d.Decode(&config); err != nil {
return fmt.Errorf("error decoding config from stdin: %w", err)
}
if d.More() {
return fmt.Errorf("extra unexpected input on stdin")
}
if err := os.Stdin.Close(); err != nil {
if err := vm.ReadConfig(&config); err != nil {
return err
}
signal.Ignore(syscall.SIGHUP)
c := make(chan os.Signal, 1)
signal.Notify(c, syscall.SIGTERM, syscall.SIGINT)
c := vm.ConfigureSignals()
ctx := context.Background()
httpServer, err := inmemhttp.NewServer(fmt.Sprintf("%s:0", config.GatewayAddr))
httpServer, err := vm.NewConfigServer(config.GatewayAddr, []byte(config.Config))
if err != nil {
return fmt.Errorf("error launching in-memory HTTP server: %w", err)
}
if err = httpServer.AddFile("config.yaml", []byte(config.Config)); err != nil {
return err
}

View File

@ -25,23 +25,6 @@ import (
"github.com/talos-systems/talos/internal/pkg/provision/providers/vm"
)
func (p *provisioner) createDisk(state *vm.State, nodeReq provision.NodeRequest) (diskPath string, err error) {
diskPath = state.GetRelativePath(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 *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))
@ -85,7 +68,7 @@ func (p *provisioner) createNode(state *vm.State, clusterReq provision.ClusterRe
memSize := nodeReq.Memory / 1024 / 1024
diskPath, err := p.createDisk(state, nodeReq)
diskPath, err := p.CreateDisk(state, nodeReq)
if err != nil {
return provision.NodeInfo{}, err
}
@ -113,7 +96,7 @@ func (p *provisioner) createNode(state *vm.State, clusterReq provision.ClusterRe
cfg := firecracker.Config{
SocketPath: socketPath,
KernelImagePath: clusterReq.KernelPath,
KernelImagePath: clusterReq.UncompressedKernelPath,
KernelArgs: cmdline.String(),
InitrdPath: clusterReq.InitramfsPath,
ForwardSignals: []os.Signal{}, // don't forward any signals
@ -163,7 +146,7 @@ func (p *provisioner) createNode(state *vm.State, clusterReq provision.ClusterRe
launchConfig := LaunchConfig{
FirecrackerConfig: cfg,
Config: nodeConfig,
GatewayAddr: clusterReq.Network.GatewayAddr.String(),
GatewayAddr: clusterReq.Network.GatewayAddr,
BootloaderEmulation: opts.BootloaderEmulation,
}
@ -214,27 +197,3 @@ func (p *provisioner) createNode(state *vm.State, clusterReq provision.ClusterRe
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 vm.StopProcessByPidfile(node.ID) // node.ID stores PID path for control process
}

View File

@ -0,0 +1,96 @@
// 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 (
"context"
"fmt"
"path/filepath"
"github.com/talos-systems/talos/internal/pkg/provision"
"github.com/talos-systems/talos/internal/pkg/provision/providers/vm"
)
// Create Talos cluster as a set of qemu VMs.
//
//nolint: gocyclo
func (p *provisioner) Create(ctx context.Context, request provision.ClusterRequest, opts ...provision.Option) (provision.Cluster, error) {
options := provision.DefaultOptions()
for _, opt := range opts {
if err := opt(&options); err != nil {
return nil, err
}
}
statePath := filepath.Join(request.StateDirectory, request.Name)
fmt.Fprintf(options.LogWriter, "creating state directory in %q\n", statePath)
state, err := vm.NewState(
statePath,
p.Name,
request.Name,
)
if err != nil {
return nil, err
}
fmt.Fprintln(options.LogWriter, "creating network", request.Network.Name)
if err = p.CreateNetwork(ctx, state, request.Network); err != nil {
return nil, fmt.Errorf("unable to provision CNI network: %w", err)
}
fmt.Fprintln(options.LogWriter, "creating load balancer")
if err = p.CreateLoadBalancer(state, request); err != nil {
return nil, fmt.Errorf("error creating loadbalancer: %w", err)
}
fmt.Fprintln(options.LogWriter, "creating dhcpd")
if err = p.CreateDHCPd(state, request); err != nil {
return nil, fmt.Errorf("error creating dhcpd: %w", err)
}
var nodeInfo []provision.NodeInfo
fmt.Fprintln(options.LogWriter, "creating master nodes")
if nodeInfo, err = p.createNodes(state, request, request.Nodes.MasterNodes()); err != nil {
return nil, err
}
fmt.Fprintln(options.LogWriter, "creating worker nodes")
var workerNodeInfo []provision.NodeInfo
if workerNodeInfo, err = p.createNodes(state, request, request.Nodes.WorkerNodes()); err != nil {
return nil, err
}
nodeInfo = append(nodeInfo, workerNodeInfo...)
fmt.Fprintln(options.LogWriter, "creating master nodes")
state.ClusterInfo = provision.ClusterInfo{
ClusterName: request.Name,
Network: provision.NetworkInfo{
Name: request.Network.Name,
CIDR: request.Network.CIDR,
GatewayAddr: request.Network.GatewayAddr,
MTU: request.Network.MTU,
},
Nodes: nodeInfo,
}
err = state.Save()
if err != nil {
return nil, err
}
return state, nil
}

View File

@ -0,0 +1,63 @@
// 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 (
"context"
"fmt"
"os"
"github.com/talos-systems/talos/internal/pkg/provision"
"github.com/talos-systems/talos/internal/pkg/provision/providers/vm"
)
// Destroy Talos cluster as set of qemu VMs.
func (p *provisioner) Destroy(ctx context.Context, cluster provision.Cluster, opts ...provision.Option) error {
options := provision.DefaultOptions()
for _, opt := range opts {
if err := opt(&options); err != nil {
return err
}
}
fmt.Fprintln(options.LogWriter, "stopping VMs")
if err := p.DestroyNodes(cluster.Info(), &options); err != nil {
return err
}
state, ok := cluster.(*vm.State)
if !ok {
return fmt.Errorf("error inspecting firecracker state, %#+v", cluster)
}
fmt.Fprintln(options.LogWriter, "removing dhcpd")
if err := p.DestroyDHCPd(state); err != nil {
return fmt.Errorf("error stopping dhcpd: %w", err)
}
fmt.Fprintln(options.LogWriter, "removing load balancer")
if err := p.DestroyLoadBalancer(state); err != nil {
return fmt.Errorf("error stopping loadbalancer: %w", err)
}
fmt.Fprintln(options.LogWriter, "removing network")
if err := p.DestroyNetwork(state); err != nil {
return err
}
fmt.Fprintln(options.LogWriter, "removing state directory")
stateDirectoryPath, err := cluster.StatePath()
if err != nil {
return err
}
return os.RemoveAll(stateDirectoryPath)
}

View File

@ -0,0 +1,263 @@
// 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 (
"bytes"
"context"
"fmt"
"log"
"net"
"os"
"os/exec"
"strconv"
"strings"
"github.com/containernetworking/cni/libcni"
"github.com/containernetworking/cni/pkg/types/current"
"github.com/containernetworking/plugins/pkg/ns"
"github.com/containernetworking/plugins/pkg/testutils"
"github.com/google/uuid"
"github.com/talos-systems/talos/internal/pkg/cniutils"
"github.com/talos-systems/talos/internal/pkg/provision"
"github.com/talos-systems/talos/internal/pkg/provision/providers/vm"
)
// LaunchConfig is passed in to the Launch function over stdin.
type LaunchConfig struct {
StatePath string
// VM options
DiskPath string
VCPUCount int64
MemSize int64
QemuExecutable string
KernelImagePath string
InitrdPath string
KernelArgs string
// Talos config
Config string
// Network
NetworkConfig *libcni.NetworkConfigList
CNI provision.CNIConfig
IP net.IP
CIDR net.IPNet
Hostname string
GatewayAddr net.IP
MTU int
Nameservers []net.IP
// filled by CNI invocation
tapName string
vmMAC string
ns ns.NetNS
// signals
c chan os.Signal
}
// withCNI creates network namespace, launches CNI and passes control to the next function
// filling config with netNS and interface details.
func withCNI(ctx context.Context, config *LaunchConfig, f func(config *LaunchConfig) error) error {
// random ID for the CNI, maps to single VM
containerID := uuid.New().String()
cniConfig := libcni.NewCNIConfigWithCacheDir(config.CNI.BinPath, config.CNI.CacheDir, nil)
// create a network namespace
ns, err := testutils.NewNS()
if err != nil {
return err
}
defer func() {
ns.Close() //nolint: errcheck
testutils.UnmountNS(ns) //nolint: errcheck
}()
ones, _ := config.CIDR.Mask.Size()
runtimeConf := libcni.RuntimeConf{
ContainerID: containerID,
NetNS: ns.Path(),
IfName: "veth0",
Args: [][2]string{
{"IP", fmt.Sprintf("%s/%d", config.IP, ones)},
{"GATEWAY", config.GatewayAddr.String()},
},
}
// attempt to clean up network in case it was deployed previously
err = cniConfig.DelNetworkList(ctx, config.NetworkConfig, &runtimeConf)
if err != nil {
return fmt.Errorf("error deleting CNI network: %w", err)
}
res, err := cniConfig.AddNetworkList(ctx, config.NetworkConfig, &runtimeConf)
if err != nil {
return fmt.Errorf("error provisioning CNI network: %w", err)
}
defer func() {
if e := cniConfig.DelNetworkList(ctx, config.NetworkConfig, &runtimeConf); e != nil {
log.Printf("error cleaning up CNI: %s", e)
}
}()
currentResult, err := current.NewResultFromResult(res)
if err != nil {
return fmt.Errorf("failed to parse cni result: %w", err)
}
vmIface, tapIface, err := cniutils.VMTapPair(currentResult, containerID)
if err != nil {
return fmt.Errorf(
"failed to parse VM network configuration from CNI output, ensure CNI is configured with a plugin " +
"that supports automatic VM network configuration such as tc-redirect-tap",
)
}
config.tapName = tapIface.Name
config.vmMAC = vmIface.Mac
config.ns = ns
// dump node IP/mac/hostname for dhcp
if err = vm.DumpIPAMRecord(config.StatePath, vm.IPAMRecord{
IP: config.IP,
Netmask: config.CIDR.Mask,
MAC: vmIface.Mac,
Hostname: config.Hostname,
Gateway: config.GatewayAddr,
MTU: config.MTU,
Nameservers: config.Nameservers,
}); err != nil {
return err
}
return f(config)
}
// launchVM runs qemu with args built based on config.
func launchVM(config *LaunchConfig) error {
args := []string{
"-m", strconv.FormatInt(config.MemSize, 10),
"-drive", fmt.Sprintf("format=raw,if=virtio,file=%s", config.DiskPath),
"-smp", fmt.Sprintf("cpus=%d", config.VCPUCount),
"-accel",
"kvm",
"-nographic",
"-netdev", fmt.Sprintf("tap,id=net0,ifname=%s,script=no,downscript=no", config.tapName),
"-device", fmt.Sprintf("virtio-net-pci,netdev=net0,mac=%s", config.vmMAC),
}
disk, err := os.Open(config.DiskPath)
if err != nil {
return fmt.Errorf("failed to open disk file %w", err)
}
// check if disk is empty
checkSize := 512
buf := make([]byte, checkSize)
_, err = disk.Read(buf)
if err != nil {
return fmt.Errorf("failed to read disk file %w", err)
}
if bytes.Equal(buf, make([]byte, checkSize)) {
args = append(args,
"-kernel", config.KernelImagePath,
"-initrd", config.InitrdPath,
"-append", config.KernelArgs,
"-no-reboot",
)
}
fmt.Fprintf(os.Stderr, "starting qemu with args:\n%s\n", strings.Join(args, " "))
cmd := exec.Command(
config.QemuExecutable,
args...,
)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := ns.WithNetNSPath(config.ns.Path(), func(_ ns.NetNS) error {
return cmd.Start()
}); err != nil {
return err
}
done := make(chan error)
go func() {
done <- cmd.Wait()
}()
select {
case sig := <-config.c:
fmt.Fprintf(os.Stderr, "exiting VM as signal %s was received\n", sig)
if err := cmd.Process.Kill(); err != nil {
return fmt.Errorf("failed to kill process %w", err)
}
return fmt.Errorf("process stopped")
case err := <-done:
if err != nil {
return fmt.Errorf("process exited with error %s", err)
}
// graceful exit
return nil
}
}
// Launch a control process around qemu VM manager.
//
// This function is invoked from 'talosctl qemu-launch' hidden command
// and wraps starting, controlling 'qemu' VM process.
//
// Launch restarts VM forever until control process is stopped itself with a signal.
//
// Process is expected to receive configuration on stdin. Current working directory
// should be cluster state directory, process output should be redirected to the
// logfile in state directory.
//
// When signals SIGINT, SIGTERM are received, control process stops qemu and exits.
//
//nolint: gocyclo
func Launch() error {
var config LaunchConfig
ctx := context.Background()
if err := vm.ReadConfig(&config); err != nil {
return err
}
config.c = vm.ConfigureSignals()
httpServer, err := vm.NewConfigServer(config.GatewayAddr, []byte(config.Config))
if err != nil {
return err
}
httpServer.Serve()
defer httpServer.Shutdown(ctx) //nolint: errcheck
// patch kernel args
config.KernelArgs = strings.ReplaceAll(config.KernelArgs, "{TALOS_CONFIG_URL}", fmt.Sprintf("http://%s/config.yaml", httpServer.GetAddr()))
return withCNI(ctx, &config, func(config *LaunchConfig) error {
for {
if err := launchVM(config); err != nil {
return err
}
}
})
}

View File

@ -0,0 +1,169 @@
// 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"
"io/ioutil"
"math"
"os"
"os/exec"
"strconv"
"syscall"
multierror "github.com/hashicorp/go-multierror"
"github.com/talos-systems/talos/internal/pkg/provision"
"github.com/talos-systems/talos/internal/pkg/provision/providers/vm"
"github.com/talos-systems/go-procfs/procfs"
)
//nolint: gocyclo
func (p *provisioner) createNode(state *vm.State, clusterReq provision.ClusterRequest, nodeReq provision.NodeRequest) (provision.NodeInfo, error) {
pidPath := state.GetRelativePath(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
}
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.NewDefaultCmdline()
// required to get kernel console
cmdline.Append("console", "ttyS0")
// reboot configuration
cmdline.Append("reboot", "k")
cmdline.Append("panic", "1")
// Talos config
cmdline.Append("talos.platform", "metal")
cmdline.Append("talos.config", "{TALOS_CONFIG_URL}") // to be patched by launcher
nodeConfig, err := nodeReq.Config.String()
if err != nil {
return provision.NodeInfo{}, err
}
launchConfig := LaunchConfig{
QemuExecutable: "qemu-system-x86_64",
DiskPath: diskPath,
VCPUCount: vcpuCount,
MemSize: memSize,
KernelImagePath: clusterReq.CompressedKernelPath,
KernelArgs: cmdline.String(),
InitrdPath: clusterReq.InitramfsPath,
Config: nodeConfig,
NetworkConfig: state.VMCNIConfig,
CNI: clusterReq.Network.CNI,
CIDR: clusterReq.Network.CIDR,
IP: nodeReq.IP,
Hostname: nodeReq.Name,
GatewayAddr: clusterReq.Network.GatewayAddr,
MTU: clusterReq.Network.MTU,
Nameservers: clusterReq.Network.Nameservers,
}
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 = 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) createNodes(state *vm.State, clusterReq provision.ClusterRequest, nodeReqs []provision.NodeRequest) ([]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)
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()
}

View File

@ -0,0 +1,59 @@
// 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 (
"context"
"github.com/talos-systems/talos/internal/pkg/provision"
"github.com/talos-systems/talos/internal/pkg/provision/providers/vm"
"github.com/talos-systems/talos/pkg/config/types/v1alpha1/generate"
)
type provisioner struct {
vm.Provisioner
}
// NewProvisioner initializes qemu provisioner.
func NewProvisioner(ctx context.Context) (provision.Provisioner, error) {
p := &provisioner{
vm.Provisioner{
Name: "qemu",
},
}
return p, nil
}
// Close and release resources.
func (p *provisioner) Close() error {
return nil
}
// GenOptions provides a list of additional config generate options.
func (p *provisioner) GenOptions(networkReq provision.NetworkRequest) []generate.GenOption {
nameservers := make([]string, len(networkReq.Nameservers))
for i := range nameservers {
nameservers[i] = networkReq.Nameservers[i].String()
}
return []generate.GenOption{
generate.WithInstallDisk("/dev/vda"),
generate.WithInstallExtraKernelArgs([]string{
"console=ttyS0",
// reboot configuration
"reboot=k",
"panic=1",
// Talos-specific
"talos.platform=metal",
}),
}
}
// GetLoadBalancers returns internal/external loadbalancer endpoints.
func (p *provisioner) GetLoadBalancers(networkReq provision.NetworkRequest) (internalEndpoint, externalEndpoint string) {
// firecracker runs loadbalancer on the bridge, which is good for both internal & external access
return networkReq.GatewayAddr.String(), networkReq.GatewayAddr.String()
}

View File

@ -0,0 +1,18 @@
// 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/.
// +build linux
package providers
import (
"context"
"github.com/talos-systems/talos/internal/pkg/provision"
"github.com/talos-systems/talos/internal/pkg/provision/providers/qemu"
)
func newQemu(ctx context.Context) (provision.Provisioner, error) {
return qemu.NewProvisioner(ctx)
}

View File

@ -0,0 +1,18 @@
// 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/.
// +build !linux
package providers
import (
"context"
"fmt"
"github.com/talos-systems/talos/internal/pkg/provision"
)
func newQemu(ctx context.Context) (provision.Provisioner, error) {
return nil, fmt.Errorf("qemu provisioner is not supported on this platform")
}

View File

@ -0,0 +1,141 @@
// 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 vm
import (
"fmt"
"io/ioutil"
"log"
"net"
"os"
"os/exec"
"strconv"
"syscall"
"time"
"github.com/insomniacslk/dhcp/dhcpv4"
"github.com/insomniacslk/dhcp/dhcpv4/server4"
"github.com/talos-systems/talos/internal/pkg/provision"
)
func handler(serverIP net.IP, statePath string) server4.Handler {
return func(conn net.PacketConn, peer net.Addr, m *dhcpv4.DHCPv4) {
if m.OpCode != dhcpv4.OpcodeBootRequest {
return
}
db, err := LoadIPAMRecords(statePath)
if err != nil {
log.Printf("failed loading the IPAM db: %s", err)
return
}
if db == nil {
return
}
match, ok := db[m.ClientHWAddr.String()]
if !ok {
log.Printf("no match for MAC: %s", m.ClientHWAddr.String())
return
}
resp, err := dhcpv4.NewReplyFromRequest(m,
dhcpv4.WithNetmask(match.Netmask),
dhcpv4.WithServerIP(serverIP),
dhcpv4.WithYourIP(match.IP),
dhcpv4.WithOption(dhcpv4.OptHostName(match.Hostname)),
dhcpv4.WithOption(dhcpv4.OptDNS(match.Nameservers...)),
dhcpv4.WithOption(dhcpv4.OptRouter(match.Gateway)),
dhcpv4.WithOption(dhcpv4.OptIPAddressLeaseTime(time.Hour)),
dhcpv4.WithOption(dhcpv4.OptServerIdentifier(serverIP)),
)
if err != nil {
log.Printf("failure building response: %s", err)
return
}
resp.UpdateOption(dhcpv4.OptGeneric(dhcpv4.OptionInterfaceMTU, dhcpv4.Uint16(match.MTU).ToBytes()))
switch mt := m.MessageType(); mt { //nolint: exhaustive
case dhcpv4.MessageTypeDiscover:
resp.UpdateOption(dhcpv4.OptMessageType(dhcpv4.MessageTypeOffer))
case dhcpv4.MessageTypeRequest:
resp.UpdateOption(dhcpv4.OptMessageType(dhcpv4.MessageTypeAck))
default:
log.Printf("unhandled message type: %v", mt)
return
}
_, err = conn.WriteTo(resp.ToBytes(), peer)
if err != nil {
log.Printf("failure sending response: %s", err)
}
}
}
// DHCPd entrypoint.
func DHCPd(ifName string, ip net.IP, statePath string) error {
server, err := server4.NewServer(ifName, nil, handler(ip, statePath), server4.WithDebugLogger())
if err != nil {
return err
}
return server.Serve()
}
const (
dhcpPid = "dhcpd.pid"
dhcpLog = "dhcpd.log"
)
// CreateDHCPd creates DHCPd.
func (p *Provisioner) CreateDHCPd(state *State, clusterReq provision.ClusterRequest) error {
pidPath := state.GetRelativePath(dhcpPid)
logFile, err := os.OpenFile(state.GetRelativePath(dhcpLog), os.O_APPEND|os.O_CREATE|os.O_RDWR, 0o666)
if err != nil {
return err
}
defer logFile.Close() //nolint: errcheck
statePath, err := state.StatePath()
if err != nil {
return err
}
args := []string{
"dhcpd-launch",
"--state-path", statePath,
"--addr", clusterReq.Network.GatewayAddr.String(),
"--interface", state.BridgeName,
}
cmd := exec.Command(clusterReq.SelfExecutable, args...)
cmd.Stdout = logFile
cmd.Stderr = logFile
cmd.SysProcAttr = &syscall.SysProcAttr{
Setsid: true, // daemonize
}
if err = cmd.Start(); err != nil {
return err
}
if err = ioutil.WriteFile(pidPath, []byte(strconv.Itoa(cmd.Process.Pid)), os.ModePerm); err != nil {
return fmt.Errorf("error writing dhcp PID file: %w", err)
}
return nil
}
// DestroyDHCPd destoys load balancer.
func (p *Provisioner) DestroyDHCPd(state *State) error {
pidPath := state.GetRelativePath(dhcpPid)
return stopProcessByPidfile(pidPath)
}

View File

@ -0,0 +1,30 @@
// 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 vm
import (
"fmt"
"os"
"github.com/talos-systems/talos/internal/pkg/provision"
)
// CreateDisk creates an empty disk file.
func (p *Provisioner) CreateDisk(state *State, nodeReq provision.NodeRequest) (diskPath string, err error) {
diskPath = state.GetRelativePath(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
}

View File

@ -0,0 +1,79 @@
// 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 vm
import (
"bufio"
"encoding/json"
"fmt"
"net"
"os"
"path/filepath"
)
// IPAMRecord describes a single record about a node.
type IPAMRecord struct {
IP net.IP
Netmask net.IPMask
MAC string
Hostname string
Gateway net.IP
MTU int
Nameservers []net.IP
}
// IPAMDatabase is a mapping from MAC address to records.
type IPAMDatabase map[string]IPAMRecord
const dbFile = "ipam.db"
// DumpIPAMRecord appends IPAM record to the database.
func DumpIPAMRecord(statePath string, record IPAMRecord) error {
f, err := os.OpenFile(filepath.Join(statePath, dbFile), os.O_APPEND|os.O_WRONLY|os.O_CREATE, os.ModePerm)
if err != nil {
return err
}
defer f.Close() //nolint: errcheck
// need atomic write, buffering json
b, err := json.Marshal(record)
if err != nil {
return fmt.Errorf("error marshaling IPAM record: %w", err)
}
_, err = f.Write(append(b, '\n'))
return err
}
// LoadIPAMRecords loads all the IPAM records indexed by the MAC address.
func LoadIPAMRecords(statePath string) (IPAMDatabase, error) {
f, err := os.Open(filepath.Join(statePath, dbFile))
if err != nil {
if os.IsNotExist(err) {
return nil, nil
}
return nil, err
}
defer f.Close() //nolint: errcheck
result := make(IPAMDatabase)
scanner := bufio.NewScanner(f)
for scanner.Scan() {
var record IPAMRecord
if err := json.Unmarshal(scanner.Bytes(), &record); err != nil {
return nil, err
}
result[record.MAC] = record
}
return result, scanner.Err()
}

View File

@ -0,0 +1,58 @@
// 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 vm
import (
"encoding/json"
"fmt"
"net"
"os"
"os/signal"
"syscall"
"github.com/talos-systems/talos/internal/pkg/inmemhttp"
)
// ReadConfig loads configuration from stdin.
func ReadConfig(config interface{}) error {
d := json.NewDecoder(os.Stdin)
if err := d.Decode(config); err != nil {
return fmt.Errorf("error decoding config from stdin: %w", err)
}
if d.More() {
return fmt.Errorf("extra unexpected input on stdin")
}
if err := os.Stdin.Close(); err != nil {
return err
}
return nil
}
// ConfigureSignals configures signal handling for the process.
func ConfigureSignals() chan os.Signal {
signal.Ignore(syscall.SIGHUP)
c := make(chan os.Signal, 1)
signal.Notify(c, syscall.SIGTERM, syscall.SIGINT)
return c
}
// NewConfigServer creates new inmemhttp.Server and mounts config file into it.
func NewConfigServer(gatewayAddr net.IP, config []byte) (inmemhttp.Server, error) {
httpServer, err := inmemhttp.NewServer(fmt.Sprintf("%s:0", gatewayAddr))
if err != nil {
return nil, fmt.Errorf("error launching in-memory HTTP server: %w", err)
}
if err = httpServer.AddFile("config.yaml", config); err != nil {
return nil, err
}
return httpServer, nil
}

View File

@ -67,5 +67,5 @@ func (p *Provisioner) CreateLoadBalancer(state *State, clusterReq provision.Clus
func (p *Provisioner) DestroyLoadBalancer(state *State) error {
pidPath := state.GetRelativePath(lbPid)
return StopProcessByPidfile(pidPath)
return stopProcessByPidfile(pidPath)
}

View File

@ -0,0 +1,39 @@
// 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 vm
import (
"fmt"
multierror "github.com/hashicorp/go-multierror"
"github.com/talos-systems/talos/internal/pkg/provision"
)
// DestroyNodes destroys all VMs.
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()
}
// DestroyNode destroys VM.
func (p *Provisioner) DestroyNode(node provision.NodeInfo) error {
return stopProcessByPidfile(node.ID) // node.ID stores PID path for control process
}

View File

@ -11,8 +11,7 @@ import (
"syscall"
)
// StopProcessByPidfile send SIGTERM to pid from pidfile.
func StopProcessByPidfile(pidPath string) error {
func stopProcessByPidfile(pidPath string) error {
pidFile, err := os.Open(pidPath)
if err != nil {
if os.IsNotExist(err) {

View File

@ -18,9 +18,10 @@ type ClusterRequest struct {
Network NetworkRequest
Nodes NodeRequests
Image string
KernelPath string
InitramfsPath string
Image string
UncompressedKernelPath string
CompressedKernelPath string
InitramfsPath string
// Path to talosctl executable to re-execute itself as needed.
SelfExecutable string