blob: 17ec098e5a0a93ba5010e6df2746bf957f5ef9a5 [file] [log] [blame]
package main
import (
"archive/zip"
"bytes"
"errors"
"fmt"
"net/http"
"path/filepath"
"github.com/cenkalti/backoff/v4"
"google.golang.org/protobuf/proto"
bpb "source.monogon.dev/cloud/bmaas/server/api"
"source.monogon.dev/metropolis/node/build/mkimage/osimage"
"source.monogon.dev/metropolis/pkg/efivarfs"
"source.monogon.dev/metropolis/pkg/logtree"
)
// install dispatches OSInstallationRequests to the appropriate installer
// method
func install(req *bpb.OSInstallationRequest, l logtree.LeveledLogger, isEFIBoot bool) error {
switch reqT := req.Type.(type) {
case *bpb.OSInstallationRequest_Metropolis:
return installMetropolis(reqT.Metropolis, l, isEFIBoot)
default:
return errors.New("unknown installation request type")
}
}
func installMetropolis(req *bpb.MetropolisInstallationRequest, l logtree.LeveledLogger, isEFIBoot bool) error {
if !isEFIBoot {
return errors.New("Monogon OS can only be installed on EFI-booted machines, this one is not")
}
// Download into a buffer as ZIP files cannot efficiently be read from
// HTTP in Go as the ReaderAt has no way of indicating continuous sections,
// thus a ton of small range requests would need to be used, causing
// a huge latency penalty as well as costing a lot of money on typical
// object storages. This should go away when we switch to a better bundle
// format which can be streamed.
var bundleRaw bytes.Buffer
b := backoff.NewExponentialBackOff()
err := backoff.Retry(func() error {
bundleRes, err := http.Get(req.BundleUrl)
if err != nil {
l.Warningf("Metropolis bundle request failed: %v", err)
return fmt.Errorf("HTTP request failed: %v", err)
}
defer bundleRes.Body.Close()
switch bundleRes.StatusCode {
case http.StatusTooEarly, http.StatusTooManyRequests,
http.StatusInternalServerError, http.StatusBadGateway,
http.StatusServiceUnavailable, http.StatusGatewayTimeout:
l.Warningf("Metropolis bundle request HTTP %d error, retrying", bundleRes.StatusCode)
return fmt.Errorf("HTTP error %d", bundleRes.StatusCode)
default:
// Non-standard code range used for proxy-related issue by various
// vendors. Treat as non-permanent error.
if bundleRes.StatusCode >= 520 && bundleRes.StatusCode < 599 {
l.Warningf("Metropolis bundle request HTTP %d error, retrying", bundleRes.StatusCode)
return fmt.Errorf("HTTP error %d", bundleRes.StatusCode)
}
if bundleRes.StatusCode != 200 {
l.Errorf("Metropolis bundle request permanent HTTP %d error, aborting", bundleRes.StatusCode)
return backoff.Permanent(fmt.Errorf("HTTP error %d", bundleRes.StatusCode))
}
}
if _, err := bundleRaw.ReadFrom(bundleRes.Body); err != nil {
l.Warningf("Metropolis bundle download failed, retrying: %v", err)
bundleRaw.Reset()
return err
}
return nil
}, b)
if err != nil {
return fmt.Errorf("error downloading Metropolis bundle: %v", err)
}
l.Info("Metropolis Bundle downloaded")
bundle, err := zip.NewReader(bytes.NewReader(bundleRaw.Bytes()), int64(bundleRaw.Len()))
if err != nil {
return fmt.Errorf("failed to open node bundle: %w", err)
}
efiPayload, err := bundle.Open("kernel_efi.efi")
if err != nil {
return fmt.Errorf("invalid bundle: %w", err)
}
defer efiPayload.Close()
systemImage, err := bundle.Open("verity_rootfs.img")
if err != nil {
return fmt.Errorf("invalid bundle: %w", err)
}
defer systemImage.Close()
nodeParamsRaw, err := proto.Marshal(req.NodeParameters)
if err != nil {
return fmt.Errorf("failed marshaling: %w", err)
}
installParams := osimage.Params{
PartitionSize: osimage.PartitionSizeInfo{
ESP: 128,
System: 4096,
Data: 128,
},
SystemImage: systemImage,
EFIPayload: efiPayload,
NodeParameters: bytes.NewReader(nodeParamsRaw),
OutputPath: filepath.Join("/dev", req.RootDevice),
}
be, err := osimage.Create(&installParams)
if err != nil {
return err
}
bootEntryIdx, err := efivarfs.CreateBootEntry(be)
if err != nil {
return fmt.Errorf("error creating EFI boot entry: %w", err)
}
if err := efivarfs.SetBootOrder(&efivarfs.BootOrder{uint16(bootEntryIdx)}); err != nil {
return fmt.Errorf("error setting EFI boot order: %w", err)
}
l.Info("Metropolis installation completed")
return nil
}