Skip to content

Commit

Permalink
add os support for specifying an ipxe configuration for its installer
Browse files Browse the repository at this point in the history
Signed-off-by: Mike Mason <mason@packet.com>
  • Loading branch information
mikemrm committed Aug 10, 2021
1 parent 94f4394 commit 5119be6
Show file tree
Hide file tree
Showing 7 changed files with 373 additions and 5 deletions.
1 change: 1 addition & 0 deletions cmd/boots/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (

_ "github.com/tinkerbell/boots/installers/coreos"
_ "github.com/tinkerbell/boots/installers/custom_ipxe"
_ "github.com/tinkerbell/boots/installers/ipxe"
_ "github.com/tinkerbell/boots/installers/nixos"
_ "github.com/tinkerbell/boots/installers/osie"
_ "github.com/tinkerbell/boots/installers/rancher"
Expand Down
7 changes: 7 additions & 0 deletions installers/ipxe/errors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package ipxe

import "errors"

var (
ErrEmptyIpxeConfig = errors.New("ipxe config URL or Script must be defined")
)
82 changes: 82 additions & 0 deletions installers/ipxe/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package ipxe

import (
"encoding/json"
"strings"

"github.com/packethost/pkg/log"
"github.com/tinkerbell/boots/installers"
"github.com/tinkerbell/boots/ipxe"
"github.com/tinkerbell/boots/job"
)

func init() {
job.RegisterInstaller("ipxe", ipxeScript)
}

func ipxeScript(j job.Job, s *ipxe.Script) {
logger := installers.Logger("ipxe")
if j.InstanceID() != "" {
logger = logger.With("instance.id", j.InstanceID())
}

cfg, err := ipxeConfigFromJob(j)
if err != nil {
s.Echo("Failed to decode installer data")
s.Shell()
logger.Error(err, "decoding installer data")

return
}

IpxeScriptFromConfig(logger, cfg, j, s)
}

func IpxeScriptFromConfig(logger log.Logger, cfg *Config, j job.Job, s *ipxe.Script) {
if err := cfg.validate(); err != nil {
s.Echo("Invalid ipxe configuration")
s.Shell()
logger.Error(err, "validating ipxe config")

return
}

s.PhoneHome("provisioning.104.01")
s.Set("packet_facility", j.FacilityCode())
s.Set("packet_plan", j.PlanSlug())

if cfg.Chain != "" {
s.Chain(cfg.Chain)
} else if cfg.Script != "" {
s.AppendString(strings.TrimPrefix(cfg.Script, "#!ipxe"))
} else {
s.Echo("Unknown ipxe config path")
s.Shell()
}
}

func ipxeConfigFromJob(j job.Job) (*Config, error) {
data := j.OperatingSystem().InstallerData

cfg := &Config{}

err := json.NewDecoder(strings.NewReader(data)).Decode(&cfg)
if err != nil {
return nil, err
}

return cfg, nil
}

type Config struct {
Chain string `json:"chain,omitempty"`
Script string `json:"script,omitempty"`
}

func (c *Config) validate() error {
if c.Chain == "" && c.Script == "" {
return ErrEmptyIpxeConfig
}

return nil
}
254 changes: 254 additions & 0 deletions installers/ipxe/main_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,254 @@
package ipxe

import (
"os"
"regexp"
"testing"

l "github.com/packethost/pkg/log"
"github.com/stretchr/testify/require"
"github.com/tinkerbell/boots/installers"
"github.com/tinkerbell/boots/ipxe"
"github.com/tinkerbell/boots/job"
)

var (
testLogger l.Logger
)

func TestMain(m *testing.M) {
os.Setenv("PACKET_ENV", "test")
os.Setenv("PACKET_VERSION", "0")
os.Setenv("ROLLBAR_DISABLE", "1")
os.Setenv("ROLLBAR_TOKEN", "1")

logger, _ := l.Init("github.com/tinkerbell/boots")
job.Init(logger)
installers.Init(logger)
testLogger = logger
os.Exit(m.Run())
}

func TestIpxeScript(t *testing.T) {
var testCases = []struct {
name string
installerData string
want string
}{
{
"invalid config",
"",
`#!ipxe
echo Failed to decode installer data
shell
`,
},
{
"valid config",
`{"chain": "http://url/path.ipxe"}`,
`#!ipxe
params
param body Device connected to DHCP system
param type provisioning.104.01
imgfetch ${tinkerbell}/phone-home##params
imgfree
set packet_facility test.facility
set packet_plan test.slug
chain --autofree http://url/path.ipxe
`,
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
assert := require.New(t)
mockJob := job.NewMock(t, "test.slug", "test.facility")
script := ipxe.NewScript()

mockJob.SetOSInstallerData(tc.installerData)

ipxeScript(mockJob.Job(), script)

assert.Equal(dedent(tc.want), string(script.Bytes()))
})
}
}

func TestIpxeScriptFromConfig(t *testing.T) {
var testCases = []struct {
name string
config *Config
want string
}{
{
"invalid config",
&Config{},
`#!ipxe
echo Invalid ipxe configuration
shell
`,
},
{
"valid chain",
&Config{Chain: "http://url/path.ipxe"},
`#!ipxe
params
param body Device connected to DHCP system
param type provisioning.104.01
imgfetch ${tinkerbell}/phone-home##params
imgfree
set packet_facility test.facility
set packet_plan test.slug
chain --autofree http://url/path.ipxe
`,
},
{
"valid script",
&Config{Script: "echo my test script"},
`#!ipxe
params
param body Device connected to DHCP system
param type provisioning.104.01
imgfetch ${tinkerbell}/phone-home##params
imgfree
set packet_facility test.facility
set packet_plan test.slug
echo my test script
`,
},
{
"valid script with header",
&Config{Script: "#!ipxe\necho my test script"},
`#!ipxe
params
param body Device connected to DHCP system
param type provisioning.104.01
imgfetch ${tinkerbell}/phone-home##params
imgfree
set packet_facility test.facility
set packet_plan test.slug
echo my test script
`,
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
assert := require.New(t)
mockJob := job.NewMock(t, "test.slug", "test.facility")
script := ipxe.NewScript()

IpxeScriptFromConfig(testLogger, tc.config, mockJob.Job(), script)

assert.Equal(dedent(tc.want), string(script.Bytes()))
})
}
}

func TestIpxeConfigFromJob(t *testing.T) {
var testCases = []struct {
name string
installerData string
want *Config
expectError string
}{
{
"valid chain",
`{"chain": "http://url/path.ipxe"}`,
&Config{Chain: "http://url/path.ipxe"},
"",
},
{
"valid script",
`{"script": "echo script"}`,
&Config{Script: "echo script"},
"",
},
{
"empty json error",
``,
nil,
"EOF",
},
{
"invalid json error",
`{"error"`,
nil,
"unexpected EOF",
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
assert := require.New(t)

mockJob := job.NewMock(t, "test.slug", "test.facility")

mockJob.SetOSInstallerData(tc.installerData)

cfg, err := ipxeConfigFromJob(mockJob.Job())

if tc.expectError == "" {
assert.Nil(err)
} else {
assert.EqualError(err, tc.expectError)
}

assert.Equal(tc.want, cfg)
})
}
}

func TestConfigValidate(t *testing.T) {
var testCases = []struct {
name string
chain string
script string
want string
}{
{"error when empty", "", "", "ipxe config URL or Script must be defined"},
{"using chain", "http://chain.url/script.ipxe", "", ""},
{"using script", "", "#!ipxe\necho ipxe script", ""},
{"using both", "http://path", "ipxe script", ""},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
assert := require.New(t)

cfg := &Config{
Chain: tc.chain,
Script: tc.script,
}

got := cfg.validate()

if tc.want == "" {
assert.Nil(got)
} else {
assert.EqualError(got, tc.want)
}
})
}
}

var dedentRegexp = regexp.MustCompile(`(?m)^[^\S\n]+`)

func dedent(s string) string {
return dedentRegexp.ReplaceAllString(s, "")
}
14 changes: 14 additions & 0 deletions job/ipxe.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (

var (
byDistro = make(map[string]BootScript)
byInstaller = make(map[string]BootScript)
bySlug = make(map[string]BootScript)
defaultInstaller BootScript
scripts = map[string]BootScript{
Expand All @@ -36,6 +37,14 @@ func RegisterDistro(name string, builder BootScript) {
byDistro[name] = builder
}

func RegisterInstaller(name string, builder BootScript) {
if _, ok := byInstaller[name]; ok {
err := errors.Errorf("installer %q already registered!", name)
joblog.Fatal(err, "installer", name)
}
byInstaller[name] = builder
}

func RegisterSlug(name string, builder BootScript) {
if _, ok := bySlug[name]; ok {
err := errors.Errorf("slug %q already registered!", name)
Expand Down Expand Up @@ -77,6 +86,11 @@ func auto(j Job, s *ipxe.Script) {

return
}
if f, ok := byInstaller[j.hardware.OperatingSystem().Installer]; ok {
f(j, s)

return
}
if f, ok := bySlug[j.hardware.OperatingSystem().Slug]; ok {
f(j, s)

Expand Down
Loading

0 comments on commit 5119be6

Please sign in to comment.