Skip to content

Commit

Permalink
allow custom_ipxe to handle both paths for booting an ipxe script
Browse files Browse the repository at this point in the history
  • Loading branch information
mikemrm committed Aug 10, 2021
1 parent 5119be6 commit b895f56
Show file tree
Hide file tree
Showing 7 changed files with 313 additions and 344 deletions.
1 change: 0 additions & 1 deletion cmd/boots/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ 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
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package ipxe
package custom_ipxe

import "errors"

Expand Down
2 changes: 1 addition & 1 deletion installers/custom_ipxe/ipxe_script_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ func TestScript(t *testing.T) {
s.Set("tinkerbell", "http://127.0.0.1")
s.Set("ipxe_cloud_config", "packet")

bootScript(m.Job(), &s)
ipxeScript(m.Job(), &s)
got := string(s.Bytes())
if script != got {
t.Fatalf("%s bad iPXE script:\n%v", typ, diff.LineDiff(script, got))
Expand Down
81 changes: 76 additions & 5 deletions installers/custom_ipxe/main.go
Original file line number Diff line number Diff line change
@@ -1,24 +1,95 @@
package custom_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.RegisterSlug("custom_ipxe", bootScript)
job.RegisterInstaller("ipxe", ipxeScript)
}

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

var cfg *Config
var err error

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

return
}
} else {
cfg = &Config{}

if strings.HasPrefix(j.UserData(), "#!ipxe") {
cfg.Script = j.UserData()
} else {
cfg.Chain = j.IPXEScriptURL()
}
}

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 strings.HasPrefix(j.UserData(), "#!ipxe") {
s.AppendString(strings.TrimPrefix(j.UserData(), "#!ipxe"))
if cfg.Chain != "" {
s.Chain(cfg.Chain)
} else if cfg.Script != "" {
s.AppendString(strings.TrimPrefix(cfg.Script, "#!ipxe"))
} else {
s.Chain(j.IPXEScriptURL())
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
}
235 changes: 235 additions & 0 deletions installers/custom_ipxe/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,20 @@ package custom_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")
Expand All @@ -16,5 +24,232 @@ func TestMain(m *testing.M) {

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.SetOSInstaller("ipxe")
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, "")
}
Loading

0 comments on commit b895f56

Please sign in to comment.