Move source to packer, add board spec.
This commit is contained in:
18
packer/BUILD.bazel
Normal file
18
packer/BUILD.bazel
Normal 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"],
|
||||
)
|
||||
54
packer/boards/cm4-cluster-ubuntu-22.04.2.json
Normal file
54
packer/boards/cm4-cluster-ubuntu-22.04.2.json
Normal 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
|
||||
}
|
||||
@@ -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}" ]
|
||||
|
||||
15
packer/packerlib/BUILD.bazel
Normal file
15
packer/packerlib/BUILD.bazel
Normal 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"],
|
||||
)
|
||||
8
packer/packerlib/board.go
Normal file
8
packer/packerlib/board.go
Normal 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
192
packer/packerlib/builder.go
Normal 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
|
||||
}
|
||||
3
packer/packerlib/packer.go
Normal file
3
packer/packerlib/packer.go
Normal 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
|
||||
78
packer/packerlib/provisioner.go
Normal file
78
packer/packerlib/provisioner.go
Normal 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
146
packer/packerlib/ubuntu.go
Normal 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
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
22
packer/ubuntu-board-gen.go
Normal file
22
packer/ubuntu-board-gen.go
Normal 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)
|
||||
}
|
||||
Reference in New Issue
Block a user