Move source to packer, add board spec.

This commit is contained in:
2023-04-12 05:15:34 +00:00
parent fa9f06cb9a
commit 262f82aa64
12 changed files with 72 additions and 17 deletions

18
packer/BUILD.bazel Normal file
View File

@@ -0,0 +1,18 @@
load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")
go_library(
name = "packer_lib",
srcs = ["ubuntu-board-gen.go"],
importpath = "git.wntrmute.dev/kyle/bladerunner/packer",
visibility = ["//visibility:private"],
deps = [
"//packer/packerlib",
"@ht_sr_git_kisom_goutils//die",
],
)
go_binary(
name = "ubuntu-board-gen",
embed = [":packer_lib"],
visibility = ["//visibility:public"],
)

View File

@@ -0,0 +1,54 @@
{
"variables": {},
"builders": [
{
"type": "arm",
"file_urls": [
"build/ubuntu-22.04.2-preinstalled-server-arm64+raspi.img.xz",
"https://cdimage.ubuntu.com/releases/22.04.2/release/ubuntu-22.04.2-preinstalled-server-arm64+raspi.img.xz"
],
"file_checksum_url": "http://cdimage.ubuntu.com/releases/22.04.2/release/SHA256SUMS",
"file_checksum_type": "sha256",
"file_target_extension": "xz",
"file_unarchive_cmd": [
"xz",
"--decompress",
"$ARCHIVE_PATH"
],
"image_build_method": "reuse",
"image_path": "build/cm4-cluster-ubuntu-22.04.2.img",
"image_size": "32G",
"image_type": "dos",
"image_partitions": [
{
"name": "boot",
"type": "c",
"start_sector": 2048,
"size": "256M",
"mountpoint": "/boot/firmware"
},
{
"name": "root",
"type": "83",
"start_sector": 526336,
"size": "31.7G",
"mountpoint": "/"
}
],
"image_chroot_env": [
"PATH=/usr/local/bin:/usr/local/sbin:/usr/bin:/usr/sbin:/bin:/sbin"
],
"qemu_binary_source_path": "/usr/bin/qemu-aarch64-static",
"qemu_binary_destination_path": "/usr/bin/qemu-aarch64-static"
}
],
"provisioners": [
{
"scripts": [
"scripts/install-base.sh"
],
"type": "shell"
}
],
"post-processors": null
}

View File

@@ -11,7 +11,7 @@ errmsg () {
preflight () {
case "${IMAGE_TYPE}" in
ubuntu) PACKER_BUILD_FILE="boards/pi-cm4-ubuntu-22.04.2.json" ;;
ubuntu) PACKER_BUILD_FILE="boards/cm4-cluster-ubuntu-22.04.2.json" ;;
raspbian) PACKER_BUILD_FILE="boards/raspberry-pi/raspios-lite-arm.json" ;;
custom)
if [ -z "${PACKER_BUILD_FILE}" ]

View File

@@ -0,0 +1,15 @@
load("@io_bazel_rules_go//go:def.bzl", "go_library")
go_library(
name = "packerlib",
srcs = [
"board.go",
"builder.go",
"packer.go",
"provisioner.go",
"ubuntu.go",
],
importpath = "git.wntrmute.dev/kyle/bladerunner/packer/packerlib",
visibility = ["//visibility:public"],
deps = ["@in_gopkg_yaml_v2//:yaml_v2"],
)

View File

@@ -0,0 +1,8 @@
package packerlib
type Board struct {
Variables map[string]string `json:"variables"`
Builders []Builder `json:"builders"`
Provisioners []Provisioner `json:"provisioners"`
PostProcessors []string `json:"post-processors"`
}

192
packer/packerlib/builder.go Normal file
View File

@@ -0,0 +1,192 @@
package packerlib
// RootPartitionSizes describes how big a single root partition should
// be given a standard 256M boot partition. This should cover standard
// install media sizes.
var RootPartitionSizes = map[string]string{
"4G": "3.7G",
"8G": "7.7G",
"16G": "15.7G",
"32G": "31.7G",
}
// Safe defaults that can be used when a custom option isn't needed.
const (
QemuDefaultPath = "/usr/bin/qemu-aarch64-static"
RootPartitionSafeDefault = "2.8G"
StandardPath = "PATH=/usr/local/bin:/usr/local/sbin:/usr/bin:/usr/sbin:/bin:/sbin"
)
// Partition describes a partition in the generated image.
type Partition struct {
Name string `json:"name"`
Type string `json:"type"`
StartSector int `json:"start_sector"` // should be a uint64, except JSON
Size string `json:"size"`
Mountpoint string `json:"mountpoint"`
}
// Builder specifically refers to an Arm builder as used for the images
// generated for a computeblade.
type Builder struct {
Type string `json:"type"`
FileURLs []string `json:"file_urls"`
FileChecksumURL string `json:"file_checksum_url"`
FileChecksumType string `json:"file_checksum_type"`
FileTargetExtension string `json:"file_target_extension"`
FileUnarchiveCommand []string `json:"file_unarchive_cmd"`
ImageBuildMethod string `json:"image_build_method"`
ImagePath string `json:"image_path"`
ImageSize string `json:"image_size"`
ImageType string `json:"image_type"`
ImagePartitions []Partition `json:"image_partitions"`
ImageChrootEnviron []string `json:"image_chroot_env"`
QemuBinarySourcePath string `json:"qemu_binary_source_path"`
QemuBinaryDestinationPath string `json:"qemu_binary_destination_path"`
}
func NewBuilder(fileURLs []string) Builder {
return Builder{
Type: "arm",
FileURLs: fileURLs,
}
}
// With256MBootPartition returns a standard 256M boot partition.
func (b Builder) With256MBootPartition() Builder {
p := Partition{
Name: "boot",
Type: "c",
StartSector: 2048,
Size: "256M",
Mountpoint: "/boot/firmware",
}
b.ImagePartitions = append(b.ImagePartitions, p)
return b
}
// WithSingleRootPartition should be called after With256MBootPartition,
// as it assumes a root partition immediately following the boot.
func (b Builder) WithSingleRootPartition(size string) Builder {
p := Partition{
Name: "root",
Type: "83",
StartSector: 526336,
Size: size,
Mountpoint: "/",
}
b.ImagePartitions = append(b.ImagePartitions, p)
return b
}
// WithStandardImage generates a standard image with a boot and root partition.
func (b Builder) WithStandardImage(size string, outputPath string) Builder {
b.ImageBuildMethod = "reuse"
b.ImagePath = outputPath
b.ImageSize = size
b.ImageType = "dos"
rootPartitionSize := RootPartitionSizes[size]
if rootPartitionSize == "" {
rootPartitionSize = RootPartitionSafeDefault
}
b = b.With256MBootPartition()
b = b.WithSingleRootPartition(rootPartitionSize)
// Append a standard PATH environment.
b.ImageChrootEnviron = append(b.ImageChrootEnviron, StandardPath)
return b
}
// WithStandard32GImage adds a standard 32G image spec to the Builder.
func (b Builder) WithStandard32GImage(outputPath string) Builder {
return b.WithStandardImage("32G", outputPath)
}
// WithQemuDefaults sets the standard qemu path in the builder.
func (b Builder) WithQemuDefaults() Builder {
b.QemuBinarySourcePath = QemuDefaultPath
b.QemuBinaryDestinationPath = QemuDefaultPath
return b
}
// WithSHA256Checksum sets a SHA256 checksum URL.
func (b Builder) WithSHA256Checksum(url string) Builder {
b.FileChecksumURL = url
b.FileChecksumType = "sha256"
return b
}
func (b Builder) withXZ() Builder {
b.FileTargetExtension = "xz"
b.FileUnarchiveCommand = []string{
"xz",
"--decompress",
}
return b
}
func (b Builder) withUnzip() Builder {
b.FileTargetExtension = "zip"
b.FileUnarchiveCommand = []string{
"unzip",
}
return b
}
func (b Builder) withTGZ() Builder {
b.FileTargetExtension = "tgz"
b.FileUnarchiveCommand = []string{
"tar",
"xzf",
}
return b
}
func (b Builder) withTarGZ() Builder {
b.FileTargetExtension = "tar.gz"
b.FileUnarchiveCommand = []string{
"tar",
"xzf",
}
return b
}
func (b Builder) withGZ() Builder {
b.FileTargetExtension = "gz"
b.FileUnarchiveCommand = []string{
"gunzip",
}
return b
}
// WithUnarchiver fills in the appropriate unarchiver command given the
// archive extension.
func (b Builder) WithUnarchiver(extension string) Builder {
switch extension {
case "xz":
b = b.withXZ()
case "zip":
b = b.withUnzip()
case "tgz":
b = b.withTGZ()
case "tar.gz":
b = b.withTarGZ()
case "gz":
b = b.withGZ()
}
b.FileUnarchiveCommand = append(b.FileUnarchiveCommand, "$ARCHIVE_PATH")
return b
}

View File

@@ -0,0 +1,3 @@
// Package packerlib contains utilities for interacting with Packer
// in the context of building images for the computeblade cluster.
package packerlib

View File

@@ -0,0 +1,78 @@
package packerlib
type Provisioner map[string]interface{}
// ShellProvisioner provisions machines built by Packer using shell scripts.
func ShellProvisioner(scripts ...string) Provisioner {
return map[string]interface{}{
"type": "shell",
"scripts": scripts,
}
}
// ShellLocalProvisioner will run a shell script of your choosing on the
// machine where Packer is being run - in other words, shell-local will run
// the shell script on your build server, or your desktop, etc., rather than
// the remote/guest machine being provisioned by Packer.
func ShellLocalProvisioner(scripts ...string) Provisioner {
return map[string]interface{}{
"type": "shell-local",
"scripts": scripts,
}
}
// uploads files to machines built by Packer. The recommended usage of the file
// provisioner is to use it to upload files, and then use shell provisioner to
// move them to the proper place, set permissions, etc.
func FileProvisioner(src, dest string) Provisioner {
return map[string]interface{}{
"type": "file",
"source": src,
"destination": dest,
}
}
// BreakpointProvisioner pauses until the user presses "enter" to resume the build.
func BreakpointProvisioner(note string) Provisioner {
return map[string]interface{}{
"type": "breakpoint",
"disable": false,
"note": note,
}
}
func (b Board) WithShellProvisioner(scripts ...string) Board {
b.Provisioners = append(b.Provisioners, ShellProvisioner(scripts...))
return b
}
func (b Board) WithShellLocalProvisioner(scripts ...string) Board {
b.Provisioners = append(b.Provisioners, ShellLocalProvisioner(scripts...))
return b
}
func (b Board) WithFileProvisioner(src, dest string) Board {
b.Provisioners = append(b.Provisioners, FileProvisioner(src, dest))
return b
}
func (b Board) WithBreakpointProvisioner(note string) Board {
b.Provisioners = append(b.Provisioners, BreakpointProvisioner(note))
return b
}
type FileSet struct {
Source string `yaml:"source"`
Destination string `yaml:"destination"`
}
func (fs FileSet) Provisioner() Provisioner {
return FileProvisioner(fs.Source, fs.Destination)
}
func (b Board) WithFileSetsProvisioner(fileSets []FileSet) Board {
for _, fs := range fileSets {
b.Provisioners = append(b.Provisioners, fs.Provisioner())
}
return b
}

146
packer/packerlib/ubuntu.go Normal file
View File

@@ -0,0 +1,146 @@
package packerlib
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"log"
"path/filepath"
"strings"
"gopkg.in/yaml.v2"
)
type urlSet struct {
FileURLs []string
ChecksumURL string
}
// ubuntuImage returns a local cache path and a remote fetch path for an
// Ubuntu image for a given version.
func ubuntuImage(version string) urlSet {
imageBase := fmt.Sprintf("ubuntu-%s-preinstalled-server-arm64+raspi.img.xz", version)
imageDir := fmt.Sprintf("https://cdimage.ubuntu.com/releases/%s/release/", version)
checksumURL := fmt.Sprintf("http://cdimage.ubuntu.com/releases/%s/release/SHA256SUMS", version)
localImage := "build/" + imageBase
remoteImage := imageDir + imageBase
return urlSet{
FileURLs: []string{
localImage,
remoteImage,
},
ChecksumURL: checksumURL,
}
}
func UbuntuBuilder(version, size, outputPath string) Builder {
urlSet := ubuntuImage(version)
outputPath = "build/" + outputPath
b := NewBuilder(urlSet.FileURLs)
return b.
WithSHA256Checksum(urlSet.ChecksumURL).
WithUnarchiver("xz").
WithStandardImage(size, outputPath).
WithQemuDefaults()
}
type UbuntuBoardSpec struct {
Version string `yaml:"version"`
Size string `yaml:"size"`
ImageName string `yaml:"name"`
LocalScripts []string `yaml:"local-scripts"`
Files []FileSet `yaml:"files"`
Scripts []string `yaml:"scripts"`
}
func (spec UbuntuBoardSpec) JSONPath(base string) string {
dest := spec.ImageName
ext := filepath.Ext(dest)
if ext != "" {
ext = "." + ext
dest = strings.TrimSuffix(dest, ext)
}
dest += ".json"
if base != "" {
dest = filepath.Join(base, dest)
}
return dest
}
func (spec UbuntuBoardSpec) Board() Board {
board := Board{
Variables: map[string]string{},
Builders: []Builder{
UbuntuBuilder(spec.Version, spec.Size, spec.ImageName),
},
}
if len(spec.LocalScripts) != 0 {
board = board.WithShellLocalProvisioner(spec.LocalScripts...)
}
if len(spec.Files) != 0 {
board = board.WithFileSetsProvisioner(spec.Files)
}
if len(spec.Scripts) != 0 {
board = board.WithShellProvisioner(spec.Scripts...)
}
return board
}
func LoadUbuntuSpecs(path string) (specFile UbuntuBoardSpecFile, err error) {
log.Println("loading from", path)
data, err := ioutil.ReadFile(path)
if err != nil {
return
}
log.Println("parsing specs")
err = yaml.Unmarshal(data, &specFile)
if err != nil {
return
}
return specFile, nil
}
// UbuntuBoardSpecFile describes a set of specifications for Ubuntu boards.
//
// In the future, this may include additional data.
type UbuntuBoardSpecFile struct {
Boards []UbuntuBoardSpec `yaml:"boards"`
}
func (specFile UbuntuBoardSpecFile) WriteBoards(outputDir string) error {
for _, spec := range specFile.Boards {
board := spec.Board()
dest := spec.JSONPath(outputDir)
contents, err := json.Marshal(board)
if err != nil {
return err
}
buf := &bytes.Buffer{}
err = json.Indent(buf, contents, "", " ")
if err != nil {
return err
}
contents = buf.Bytes()
log.Println("writing spec to", dest)
err = ioutil.WriteFile(dest, contents, 0644)
if err != nil {
return err
}
}
return nil
}

View File

@@ -3,6 +3,7 @@
set -euxo pipefail
echo "==> Setting nameserver"
rm /etc/resolv.conf
echo 'nameserver 8.8.8.8' > /etc/resolv.conf
echo "==> installing base updates"

View File

@@ -0,0 +1,22 @@
package main
import (
"flag"
"git.sr.ht/~kisom/goutils/die"
packer "git.wntrmute.dev/kyle/bladerunner/packer/packerlib"
)
func main() {
var specFile, outputDir string
flag.StringVar(&specFile, "f", "ubuntu-boards.yml", "`path` to Ubuntu board spec file")
flag.StringVar(&outputDir, "o", "", "`directory` to store Ubuntu boards")
flag.Parse()
boardSpecs, err := packer.LoadUbuntuSpecs(specFile)
die.If(err)
err = boardSpecs.WriteBoards(outputDir)
die.If(err)
}