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:
parent
6f5d24cc3d
commit
c6eb18eed5
@ -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(®istryMirrors, "registry-mirror", []string{}, "list of registry mirrors to use in format: <registry host>=<mirror URL>")
|
||||
|
38
cmd/talosctl/cmd/mgmt/dhcpd_launch_linux.go
Normal file
38
cmd/talosctl/cmd/mgmt/dhcpd_launch_linux.go
Normal 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)
|
||||
}
|
29
cmd/talosctl/cmd/mgmt/qemu_launch_linux.go
Normal file
29
cmd/talosctl/cmd/mgmt/qemu_launch_linux.go
Normal 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)
|
||||
}
|
@ -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
59
hack/test/e2e-qemu.sh
Executable 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
|
87
internal/pkg/cniutils/cniutils.go
Normal file
87
internal/pkg/cniutils/cniutils.go
Normal 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
|
||||
}
|
@ -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)
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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{
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
96
internal/pkg/provision/providers/qemu/create.go
Normal file
96
internal/pkg/provision/providers/qemu/create.go
Normal 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
|
||||
}
|
63
internal/pkg/provision/providers/qemu/destroy.go
Normal file
63
internal/pkg/provision/providers/qemu/destroy.go
Normal 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)
|
||||
}
|
263
internal/pkg/provision/providers/qemu/launch.go
Normal file
263
internal/pkg/provision/providers/qemu/launch.go
Normal 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
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
169
internal/pkg/provision/providers/qemu/node.go
Normal file
169
internal/pkg/provision/providers/qemu/node.go
Normal 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()
|
||||
}
|
59
internal/pkg/provision/providers/qemu/qemu.go
Normal file
59
internal/pkg/provision/providers/qemu/qemu.go
Normal 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()
|
||||
}
|
18
internal/pkg/provision/providers/qemu_linux.go
Normal file
18
internal/pkg/provision/providers/qemu_linux.go
Normal 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)
|
||||
}
|
18
internal/pkg/provision/providers/qemu_other.go
Normal file
18
internal/pkg/provision/providers/qemu_other.go
Normal 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")
|
||||
}
|
141
internal/pkg/provision/providers/vm/dhcpd.go
Normal file
141
internal/pkg/provision/providers/vm/dhcpd.go
Normal 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)
|
||||
}
|
30
internal/pkg/provision/providers/vm/disk.go
Normal file
30
internal/pkg/provision/providers/vm/disk.go
Normal 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
|
||||
}
|
79
internal/pkg/provision/providers/vm/ipam.go
Normal file
79
internal/pkg/provision/providers/vm/ipam.go
Normal 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()
|
||||
}
|
58
internal/pkg/provision/providers/vm/launch.go
Normal file
58
internal/pkg/provision/providers/vm/launch.go
Normal 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
|
||||
}
|
@ -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)
|
||||
}
|
||||
|
39
internal/pkg/provision/providers/vm/node.go
Normal file
39
internal/pkg/provision/providers/vm/node.go
Normal 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
|
||||
}
|
@ -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) {
|
||||
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user