Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

support a more generic ipxe installer #192

Merged
merged 7 commits into from
Aug 12, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions installers/custom_ipxe/errors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package custom_ipxe

import "errors"

var (
ErrEmptyIpxeConfig = errors.New("ipxe config URL or Script must be defined")
)
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
60 changes: 54 additions & 6 deletions installers/custom_ipxe/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,70 @@ package custom_ipxe
import (
"strings"

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

func init() {
job.RegisterSlug("custom_ipxe", bootScript)
job.RegisterInstaller("custom_ipxe", ipxeScript)
job.RegisterSlug("custom_ipxe", ipxeScript)
}

func bootScript(j job.Job, s *ipxe.Script) {
func ipxeScript(j job.Job, s *ipxe.Script) {
logger := j.Logger.With("installer", "custom_ipxe")

var cfg *packet.InstallerData

if j.OperatingSystem().Installer == "custom_ipxe" {
cfg = j.OperatingSystem().InstallerData
if cfg == nil {
s.Echo("Installer data not provided")
s.Shell()
logger.Error(ErrEmptyIpxeConfig, "installer data not provided")

return
}
} else if strings.HasPrefix(j.UserData(), "#!ipxe") {
cfg = &packet.InstallerData{Script: j.UserData()}
} else if j.IPXEScriptURL() != "" {
cfg = &packet.InstallerData{Chain: j.IPXEScriptURL()}
Comment on lines +33 to +34
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To illustrate how we could benefit from Data URL handling (using https://pkg.go.dev/github.com/vincent-petithory/dataurl#example-DecodeString):

} else if dataURL, err := dataurl.DecodeString(j.IPXEScriptURL()); err == nil {
    // I assume err is returned unless there is a real data URL, but we might need different logic to ensure the URL was actually a data URL and not just empty
    cfg = &packet.InstallerData{Script: dataURL.Data}
} else if j.IPXEScriptURL() != "" {
...

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@displague lets give it a go in a different PR.

} else {
s.Echo("Unknown ipxe configuration")
s.Shell()
logger.Error(ErrEmptyIpxeConfig, "unknown ipxe configuration")

return
}

ipxeScriptFromConfig(logger, cfg, j, s)
}

func ipxeScriptFromConfig(logger log.Logger, cfg *packet.InstallerData, j job.Job, s *ipxe.Script) {
if err := validateConfig(cfg); err != nil {
s.Echo(err.Error())
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())

jacobweinstock marked this conversation as resolved.
Show resolved Hide resolved
if strings.HasPrefix(j.UserData(), "#!ipxe") {
s.AppendString(strings.TrimPrefix(j.UserData(), "#!ipxe"))
} else {
s.Chain(j.IPXEScriptURL())
if cfg.Chain != "" {
s.Chain(cfg.Chain)
} else if cfg.Script != "" {
s.AppendString(strings.TrimPrefix(cfg.Script, "#!ipxe"))
jacobweinstock marked this conversation as resolved.
Show resolved Hide resolved
}
}

func validateConfig(c *packet.InstallerData) error {
if c.Chain == "" && c.Script == "" {
return ErrEmptyIpxeConfig
}

return nil
}
255 changes: 255 additions & 0 deletions installers/custom_ipxe/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,19 @@ 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"
"github.com/tinkerbell/boots/packet"
)

var (
testLogger l.Logger
)

func TestMain(m *testing.M) {
Expand All @@ -16,5 +25,251 @@ 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
installer string
installerData *packet.InstallerData
want string
}{
{
"installer: invalid config",
"custom_ipxe",
nil,
`#!ipxe

echo Installer data not provided
shell
`,
},
{
"valid config",
"custom_ipxe",
&packet.InstallerData{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
`,
},
{
"installer: valid config",
"",
&packet.InstallerData{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
`,
},
{
"instance: no config",
"",
nil,
`#!ipxe

echo Unknown ipxe configuration
shell
`,
},
{
"instance: ipxe script url",
"",
&packet.InstallerData{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
`,
},
{
"instance: userdata script",
"",
&packet.InstallerData{Script: "#!ipxe\necho userdata 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 userdata 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()

if tc.installer == "custom_ipxe" {
mockJob.SetOSInstaller("custom_ipxe")
mockJob.SetOSInstallerData(tc.installerData)
} else if tc.installerData != nil {
mockJob.SetIPXEScriptURL(tc.installerData.Chain)
mockJob.SetUserData(tc.installerData.Script)
}

ipxeScript(mockJob.Job(), script)

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

func TestIpxeScriptFromConfig(t *testing.T) {
var testCases = []struct {
name string
config *packet.InstallerData
want string
}{
{
"invalid config",
&packet.InstallerData{},
`#!ipxe

echo ipxe config URL or Script must be defined
shell
`,
},
{
"valid chain",
&packet.InstallerData{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",
&packet.InstallerData{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",
&packet.InstallerData{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 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 := &packet.InstallerData{
Chain: tc.chain,
Script: tc.script,
}

got := validateConfig(cfg)

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