Skip to content
Draft
Show file tree
Hide file tree
Changes from 41 commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
0eb2df7
Use os-installer comming with metal-hammer instead from metal-images
majst01 Mar 3, 2026
0bb4b00
Try with os-installer which includes metal-networker
majst01 Mar 4, 2026
4507b40
Fix build
majst01 Mar 4, 2026
012a679
os-installer v0.2.0
majst01 Mar 4, 2026
23a5bd9
Try calling installer in chroot
majst01 Mar 11, 2026
f3348cc
Do not install the os-installer binary
majst01 Mar 11, 2026
cfab799
Remove commented code
majst01 Mar 11, 2026
d8ba527
no fprintf
majst01 Mar 13, 2026
299bbff
Try apiv2 os-installer
majst01 Mar 13, 2026
ee2ca10
Fix test
majst01 Mar 13, 2026
8b387c7
Use persistconfig from os-installer
majst01 Mar 14, 2026
70f4392
Go mod tidy
majst01 Mar 14, 2026
a8b4913
Write configuration also in chroot
majst01 Mar 16, 2026
6c91bf7
write configs in chroot
majst01 Mar 16, 2026
7a0db64
Debug log
majst01 Mar 16, 2026
ec2a06e
Fix
majst01 Mar 16, 2026
a7c835d
More debug log
majst01 Mar 16, 2026
cda612e
Recover
majst01 Mar 16, 2026
6eec5d4
More debug logs
majst01 Mar 16, 2026
5e8fcd6
More logs
majst01 Mar 16, 2026
2371b04
More logs
majst01 Mar 16, 2026
47efb82
Dump stack
majst01 Mar 16, 2026
78236d5
Progress
majst01 Mar 16, 2026
e2f3ee6
Progress
majst01 Mar 16, 2026
9d1a772
More debug
majst01 Mar 16, 2026
aaa66a7
Welcome back RootUUID
majst01 Mar 16, 2026
8f1ef44
More debug
majst01 Mar 16, 2026
c70efa0
Fix frr version detection
majst01 Mar 17, 2026
1f8dd56
Fix frr version detection
majst01 Mar 17, 2026
0f48ad7
Fix frr version detection
majst01 Mar 17, 2026
3a15378
Fix frr version detection
majst01 Mar 17, 2026
323d118
Fix nft validation
majst01 Mar 17, 2026
cc619d4
Fix nft validation
majst01 Mar 17, 2026
909bf80
Fix debug systemd reload
majst01 Mar 17, 2026
207a98c
Fix debug systemd enable
majst01 Mar 17, 2026
f81838d
Fix systemd enable
majst01 Mar 17, 2026
3cce52b
disable chrony temporarily
majst01 Mar 17, 2026
2e27a76
Fix systemd enable
majst01 Mar 17, 2026
0ab8940
Fix systemd enable
majst01 Mar 17, 2026
1c25a18
Fix systemd enable
majst01 Mar 17, 2026
1dac24c
Fix systemd enable
majst01 Mar 17, 2026
284046b
Write complete legacy install.yaml
majst01 Mar 18, 2026
593ae9f
Call Ignition
majst01 Mar 18, 2026
5daa8c9
Execute ignition in directory
majst01 Mar 18, 2026
69f4697
Next ignition try
majst01 Mar 18, 2026
113e52b
Fix suricata
majst01 Mar 18, 2026
c21850f
Cancel install after 10min
majst01 Mar 19, 2026
7e99e13
Less info logging
majst01 Mar 19, 2026
4c8de99
Pin
majst01 Mar 20, 2026
138e12c
write allocation.yaml during install
majst01 Mar 23, 2026
b6836dc
Remove duplicate chrony rendering
majst01 Mar 23, 2026
f1c9540
Pin
majst01 Apr 1, 2026
4cc57ac
Update deps
majst01 Apr 10, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
303 changes: 224 additions & 79 deletions cmd/install.go
Original file line number Diff line number Diff line change
@@ -1,28 +1,31 @@
package cmd

import (
"context"
"encoding/base64"
"fmt"
"os"
"os/exec"
"path"
"path/filepath"
"strings"
"syscall"
"time"

apiv2 "github.com/metal-stack/api/go/metalstack/api/v2"
"github.com/metal-stack/metal-hammer/cmd/utils"
"github.com/metal-stack/metal-hammer/pkg/api"
"github.com/metal-stack/metal-lib/pkg/pointer"

installerv1 "github.com/metal-stack/os-installer/api/v1"
"github.com/metal-stack/os-installer/pkg/installer"

"github.com/metal-stack/metal-go/api/models"
img "github.com/metal-stack/metal-hammer/cmd/image"
"github.com/metal-stack/metal-hammer/cmd/storage"
"github.com/metal-stack/metal-hammer/pkg/chroot"
"github.com/metal-stack/metal-hammer/pkg/kernel"
"gopkg.in/yaml.v3"
)

// Install a given image to the disk by using genuinetools/img
func (h *hammer) Install(machine *models.V1MachineResponse) (*api.Bootinfo, error) {
func (h *hammer) Install(machine *models.V1MachineResponse) (*installerv1.Bootinfo, error) {
s := storage.New(h.log, h.chrootPrefix, *h.filesystemLayout)
err := s.Run()
if err != nil {
Expand Down Expand Up @@ -58,14 +61,14 @@ func (h *hammer) Install(machine *models.V1MachineResponse) (*api.Bootinfo, erro
return info, nil
}

// install will execute /install.sh in the pulled docker image which was extracted onto disk
// install will execute Install from os-installer in the chroot where the os-image was extracted
// to finish installation e.g. install mbr, grub, write network and filesystem config
func (h *hammer) install(prefix string, machine *models.V1MachineResponse, rootUUID string) (*api.Bootinfo, error) {
func (h *hammer) install(prefix string, machine *models.V1MachineResponse, rootUUID string) (*installerv1.Bootinfo, error) {
h.log.Info("install", "image", machine.Allocation.Image.URL)

err := h.writeInstallerConfig(machine, rootUUID)
lldpdConfig, machineDetails, machineAllocation, err := h.convertConfigs(machine, rootUUID)
if err != nil {
return nil, fmt.Errorf("writing configuration install.yaml failed %w", err)
return nil, fmt.Errorf("error converting configuration: %w", err)
}

err = h.writeUserData(machine)
Expand All @@ -78,47 +81,35 @@ func (h *hammer) install(prefix string, machine *models.V1MachineResponse, rootU
return nil, err
}

installBinary := "/install.sh"
if fileExists(path.Join(prefix, "install-go")) {
installBinary = "/install-go"
}

h.log.Info("running install", "binary", installBinary, "prefix", prefix)
err = os.Chdir(prefix)
if err != nil {
return nil, fmt.Errorf("unable to chdir to: %s error %w", prefix, err)
}
cmd := exec.Command(installBinary)
cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
// these syscalls are required to execute the command in a chroot env.
cmd.SysProcAttr = &syscall.SysProcAttr{
Credential: &syscall.Credential{
Uid: uint32(0),
Gid: uint32(0),
Groups: []uint32{0},
},
Chroot: prefix,
}
if err := cmd.Run(); err != nil {
return nil, fmt.Errorf("running %q in chroot failed %w", installBinary, err)
}
// Write configuration to /etc/metal and execute the installer in chroot
if err := chroot.RunInChroot(h.log, prefix, func() error {
h.log.Debug("write configs in chroot")
err = h.writeConfigs(lldpdConfig, machineDetails, machineAllocation)
if err != nil {
return fmt.Errorf("error writing configuration: %w", err)
}

err = os.Chdir("/")
if err != nil {
return nil, fmt.Errorf("unable to chdir to: / error %w", err)
h.log.Debug("start install in chroot", "details", machineDetails, "allocation", machineAllocation)
i := installer.New(h.log, machineDetails, machineAllocation)
err = i.Install(context.TODO())
Comment thread
vknabel marked this conversation as resolved.
Outdated
if err != nil {
h.log.Error("error during install", "error", err)
return fmt.Errorf("error during install: %w", err)
}
return nil
}); err != nil {
return nil, fmt.Errorf("unable to run the installer %w", err)
}
h.log.Info("finish running", "binary", installBinary)

err = os.Remove(path.Join(prefix, installBinary))
if err != nil {
h.log.Warn("unable to remove, ignoring", "binary", installBinary, "error", err)
}
h.log.Info("finish running the installer")

info, err := kernel.ReadBootinfo(path.Join(prefix, "etc", "metal", "boot-info.yaml"))
if err != nil {
return info, fmt.Errorf("unable to read boot-info.yaml %w", err)
}

h.log.Info("bootinfo", "info", info)

err = h.EnsureBootOrder(info.BootloaderID)
if err != nil {
return info, fmt.Errorf("unable to ensure boot order %w", err)
Expand Down Expand Up @@ -192,21 +183,40 @@ func (h *hammer) writeUserData(machine *models.V1MachineResponse) error {
return nil
}

func (h *hammer) writeInstallerConfig(machine *models.V1MachineResponse, rootUUiD string) error {
func (h *hammer) writeConfigs(lldpconfig *installerv1.LLDPDConfig, details *installerv1.MachineDetails, allocation *apiv2.MachineAllocation) error {
h.log.Info("write installation configuration")
configdir := path.Join(h.chrootPrefix, "etc", "metal")
configdir := path.Join("etc", "metal")
Comment thread
vknabel marked this conversation as resolved.
err := os.MkdirAll(configdir, 0755)
if err != nil {
return fmt.Errorf("mkdir of %s target os failed %w", configdir, err)
}
destination := path.Join(configdir, "install.yaml")

i := installer.New(h.log, details, allocation)

err = i.PersistConfigurations()
if err != nil {
return fmt.Errorf("unable to persist configuration: %w", err)
}

yamlContent, err := yaml.Marshal(lldpconfig)
if err != nil {
return err
}

err = os.WriteFile(installerv1.LLDPDConfigPath, yamlContent, os.ModePerm)
if err != nil {
return fmt.Errorf("unable to write lldpd config %w", err)
}

return nil
}

func (h *hammer) convertConfigs(machine *models.V1MachineResponse, rootUUiD string) (*installerv1.LLDPDConfig, *installerv1.MachineDetails, *apiv2.MachineAllocation, error) {
Comment thread
vknabel marked this conversation as resolved.
Outdated
alloc := machine.Allocation

sshPubkeys := strings.Join(alloc.SSHPubKeys, "\n")
cmdline, err := kernel.ParseCmdline()
if err != nil {
return fmt.Errorf("unable to get kernel cmdline map %w", err)
return nil, nil, nil, fmt.Errorf("unable to get kernel cmdline map %w", err)
}

console, ok := cmdline["console"]
Expand All @@ -219,32 +229,167 @@ func (h *hammer) writeInstallerConfig(machine *models.V1MachineResponse, rootUUi
raidEnabled = true
}

y := &api.InstallerConfig{
Hostname: *alloc.Hostname,
SSHPublicKey: sshPubkeys,
Networks: alloc.Networks,
MachineUUID: h.spec.MachineUUID,
Password: h.spec.ConsolePassword,
Console: console,
Timestamp: time.Now().Format(time.RFC3339),
Nics: h.onlyNicsWithNeighbors(machine.Hardware.Nics),
VPN: alloc.Vpn,
Role: *alloc.Role,
RaidEnabled: raidEnabled,
RootUUID: rootUUiD,
FirewallRules: alloc.FirewallRules,
DNSServers: alloc.DNSServers,
NTPServers: alloc.NtpServers,
}

yamlContent, err := yaml.Marshal(y)
if err != nil {
return err
lldpdConfig := &installerv1.LLDPDConfig{
MachineUUID: h.spec.MachineUUID,
Timestamp: time.Now().Format(time.RFC3339),
}

machineDetails := &installerv1.MachineDetails{
ID: h.spec.MachineUUID,
Password: h.spec.ConsolePassword,
Console: console,
RaidEnabled: raidEnabled,
RootUUID: rootUUiD,
Nics: h.onlyNicsWithNeighbors(machine.Hardware.Nics),
}

h.log.Info("generated apiv2 machinedetails", "details", machineDetails)

var vpn *apiv2.MachineVPN
if alloc.Vpn != nil {
vpn = &apiv2.MachineVPN{
ControlPlaneAddress: pointer.SafeDeref(alloc.Vpn.Address),
AuthKey: pointer.SafeDeref(alloc.Vpn.AuthKey),
Connected: pointer.SafeDeref(alloc.Vpn.Connected),
}
}

allocationType := apiv2.MachineAllocationType_MACHINE_ALLOCATION_TYPE_MACHINE
if alloc.Role != nil && *alloc.Role == "firewall" {
allocationType = apiv2.MachineAllocationType_MACHINE_ALLOCATION_TYPE_FIREWALL
}

var dnsservers []*apiv2.DNSServer
for _, dns := range alloc.DNSServers {
dnsservers = append(dnsservers, &apiv2.DNSServer{
Ip: pointer.SafeDeref(dns.IP),
})
}
var ntpservers []*apiv2.NTPServer
for _, ntp := range alloc.NtpServers {
ntpservers = append(ntpservers, &apiv2.NTPServer{
Address: pointer.SafeDeref(ntp.Address),
})
}

var firewallRules *apiv2.FirewallRules
if alloc.FirewallRules != nil {
var egressrules []*apiv2.FirewallEgressRule

for _, egress := range alloc.FirewallRules.Egress {
var proto apiv2.IPProtocol
if egress.Protocol == "tcp" {
proto = apiv2.IPProtocol_IP_PROTOCOL_TCP
}
if egress.Protocol == "udp" {
proto = apiv2.IPProtocol_IP_PROTOCOL_UDP
}
var ports []uint32
for _, port := range egress.Ports {
ports = append(ports, uint32(port))
}

egressrules = append(egressrules, &apiv2.FirewallEgressRule{
Comment: egress.Comment,
Protocol: proto,
Ports: ports,
To: egress.To,
})
}

var ingressrules []*apiv2.FirewallIngressRule
for _, ingress := range alloc.FirewallRules.Ingress {
var proto apiv2.IPProtocol
if ingress.Protocol == "tcp" {
proto = apiv2.IPProtocol_IP_PROTOCOL_TCP
}
if ingress.Protocol == "udp" {
proto = apiv2.IPProtocol_IP_PROTOCOL_UDP
}
var ports []uint32
for _, port := range ingress.Ports {
ports = append(ports, uint32(port))
}

ingressrules = append(ingressrules, &apiv2.FirewallIngressRule{
Comment: ingress.Comment,
Protocol: proto,
Ports: ports,
To: ingress.To,
From: ingress.From,
})
}

firewallRules = &apiv2.FirewallRules{
Egress: egressrules,
Ingress: ingressrules,
}
}

return os.WriteFile(destination, yamlContent, 0600)
var networks []*apiv2.MachineNetwork
for _, nw := range alloc.Networks {

natType := apiv2.NATType_NAT_TYPE_NONE
if nw.Nat != nil && *nw.Nat {
natType = apiv2.NATType_NAT_TYPE_IPV4_MASQUERADE
}

var networkType apiv2.NetworkType
switch pointer.SafeDeref(nw.Networktypev2) {
case "external":
networkType = apiv2.NetworkType_NETWORK_TYPE_EXTERNAL
case "underlay":
networkType = apiv2.NetworkType_NETWORK_TYPE_UNDERLAY
case "super":
networkType = apiv2.NetworkType_NETWORK_TYPE_SUPER
case "super-namespaced":
networkType = apiv2.NetworkType_NETWORK_TYPE_SUPER_NAMESPACED
case "child":
networkType = apiv2.NetworkType_NETWORK_TYPE_CHILD
case "child-shared":
networkType = apiv2.NetworkType_NETWORK_TYPE_CHILD_SHARED
}

networks = append(networks, &apiv2.MachineNetwork{
Network: pointer.SafeDeref(nw.Networkid),
Prefixes: nw.Prefixes,
DestinationPrefixes: nw.Destinationprefixes,
Ips: nw.Ips,
Vrf: uint64(pointer.SafeDeref(nw.Vrf)),
Asn: uint32(pointer.SafeDeref(nw.Asn)),
Project: nw.Projectid,
NatType: natType,
NetworkType: networkType,
})
}

machineAllocation := &apiv2.MachineAllocation{
Uuid: pointer.SafeDeref(alloc.Allocationuuid),
Name: pointer.SafeDeref(alloc.Name),
Description: alloc.Description,
CreatedBy: pointer.SafeDeref(alloc.Creator),
Project: pointer.SafeDeref(alloc.Project),
Image: &apiv2.Image{
Id: pointer.SafeDeref(alloc.Image.ID),
Url: alloc.Image.URL,
},
Hostname: pointer.SafeDeref(alloc.Hostname),
SshPublicKeys: alloc.SSHPubKeys,
Userdata: alloc.UserData,
AllocationType: allocationType,
FirewallRules: firewallRules,
Networks: networks,
DnsServers: dnsservers,
NtpServers: ntpservers,
Vpn: vpn,
}

h.log.Info("generated apiv2 allocation", "allocation", machineAllocation)

return lldpdConfig, machineDetails, machineAllocation, nil
}
func (h *hammer) onlyNicsWithNeighbors(nics []*models.V1MachineNic) []*models.V1MachineNic {

func (h *hammer) onlyNicsWithNeighbors(nics []*models.V1MachineNic) []*apiv2.MachineNic {
noNeighbors := func(neighbors []*models.V1MachineNic) bool {
if len(neighbors) == 0 {
return true
Expand All @@ -257,22 +402,22 @@ func (h *hammer) onlyNicsWithNeighbors(nics []*models.V1MachineNic) []*models.V1
return false
}

result := []*models.V1MachineNic{}
result := []*apiv2.MachineNic{}
for i := range nics {
nic := nics[i]
if noNeighbors(nic.Neighbors) {
continue
}
result = append(result, nic)
n := &apiv2.MachineNic{
Mac: pointer.SafeDeref(nic.Mac),
Name: pointer.SafeDeref(nic.Name),
Identifier: pointer.SafeDeref(nic.Identifier),
Neighbors: []*apiv2.MachineNic{
{Mac: pointer.SafeDeref(nic.Neighbors[0].Mac), Name: pointer.SafeDeref(nic.Neighbors[0].Name)},
},
}
result = append(result, n)
}
h.log.Info("onlyNicWithNeighbors add", "result", result)
return result
}

func fileExists(filename string) bool {
info, err := os.Stat(filename)
if os.IsNotExist(err) {
return false
}
return !info.IsDir()
}
Loading
Loading