Skip to content
9 changes: 9 additions & 0 deletions api/swagger.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,15 @@ paths:
in: query
type: integer
description: Node ID (NID) of host requesting boot script
- name: json
in: query
type: integer
default: 0
# TODO: it would make sense to give a schema for this object, but
# Swagger doesn't expect a query parameter to change the output
# schema because this is a weird way to implement it.
description: >-
If nonzero, a JSON object will be returned instead of an iPXE boot script.
- name: retry
in: query
type: integer
Expand Down
64 changes: 44 additions & 20 deletions cmd/boot-script-service/default_api.go
Original file line number Diff line number Diff line change
Expand Up @@ -643,6 +643,38 @@ func buildBootScript(bd BootData, sp scriptParams, chain, role, subRole, descr s
return "", fmt.Errorf("%s: this host not configured for booting.", descr)
}

script := "#!ipxe\n"
params, err := buildParams(bd, sp, role, subRole)
if err != nil {
return "", err
}

u := bd.Kernel.Path
u, err = checkURL(u)
if err == nil {
script += "kernel --name kernel " + u + " " + strings.Trim(params, " ")
script += " || goto boot_retry\n"
if bd.Initrd.Path != "" {
u, err = checkURL(bd.Initrd.Path)
if err == nil {
script += "initrd --name initrd " + u + " || goto boot_retry\n"
}
}
script += "boot || goto boot_retry\n:boot_retry\n"
// We could vary the length of the sleep based on retry count or some
// other criteria.
// For now, just sleep a bit
script += fmt.Sprintf("sleep %d\n", retryDelay) + chain + "\n"
}
return script, err
}

// buildParams constructs the full parameter list based on the
// BootData and additional parameters provided, accounting for special
// parameters. The params are returned as a string. If an error occurs, an
// empty string is returned along with the error.
func buildParams(bd BootData, sp scriptParams, role, subRole string) (string, error) {
debugf("buildParams(%v, %v, %v, %v)", bd, sp, role, subRole)
params := bd.Params
if bd.Kernel.Params != "" {
params += " " + bd.Kernel.Params
Expand Down Expand Up @@ -677,7 +709,7 @@ func buildBootScript(bd BootData, sp scriptParams, chain, role, subRole, descr s
err = nil
}

script := "#!ipxe\n"
// if bootdata specifies an initrd, add "initrd=initrd" to params (deleting any existing occurrence)
if bd.Initrd.Path != "" {
start := strings.Index(params, "initrd")
if start != -1 {
Expand All @@ -689,24 +721,8 @@ func buildBootScript(bd BootData, sp scriptParams, chain, role, subRole, descr s
}
params = "initrd=initrd " + params
}
u := bd.Kernel.Path
u, err = checkURL(u)
if err == nil {
script += "kernel --name kernel " + u + " " + strings.Trim(params, " ")
script += " || goto boot_retry\n"
if bd.Initrd.Path != "" {
u, err = checkURL(bd.Initrd.Path)
if err == nil {
script += "initrd --name initrd " + u + " || goto boot_retry\n"
}
}
script += "boot || goto boot_retry\n:boot_retry\n"
// We could vary the length of the sleep based on retry count or some
// other criteria.
// For now, just sleep a bit
script += fmt.Sprintf("sleep %d\n", retryDelay) + chain + "\n"
}
return script, err

return params, nil
}

// Function unknownBootScript() constructs the boot script for an unknown host
Expand Down Expand Up @@ -825,12 +841,21 @@ func BootscriptGet(w http.ResponseWriter, r *http.Request) {
log.Printf("BSS request failed: bootscript request without mac=, name=, or nid= parameter")
return
}
sp := scriptParams{comp.ID, comp.NID.String(), bd.ReferralToken}

debugf("bd: %v\n", bd)
debugf("comp: %v\n", comp)

is_json, _ := getIntParam(r, "json", 0)
if is_json != 0 {
params, err := buildParams(bd, sp, comp.Role, comp.SubRole)
if err != nil {
message := fmt.Sprintf("Failed to build params: %v", err)
base.SendProblemDetailsGeneric(w, http.StatusInternalServerError, message)
return
}

bd.Params = "kernel " + params
b, err := json.Marshal(bd)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
Expand Down Expand Up @@ -891,7 +916,6 @@ func BootscriptGet(w http.ResponseWriter, r *http.Request) {
if mac == "" && comp.Mac != nil {
mac = comp.Mac[0]
}
sp := scriptParams{comp.ID, comp.NID.String(), bd.ReferralToken}
chain := "chain " + chainProto + "://" + ipxeServer + gwURI + r.URL.Path
if mac != "" {
chain += "?mac=" + mac
Expand Down
144 changes: 144 additions & 0 deletions cmd/boot-script-service/default_api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,9 @@ import (
"fmt"
"net/http"
"net/http/httptest"
"reflect"
"regexp"
"strings"
"testing"

"github.com/OpenCHAMI/bss/pkg/bssTypes"
Expand Down Expand Up @@ -186,6 +188,148 @@ func TestReplaceS3Params_error(t *testing.T) {
}
}

func TestBuildParams_Success(t *testing.T) {
test_cases := []struct {
bd BootData
sp scriptParams
expectedParams []string
}{
{
// basic usage
BootData{
Params: "root=nfs:example/path/to/rootfs:ro",
Kernel: ImageData{Path: "http://example/path/to/vmlinuz"},
Initrd: ImageData{Path: "http://example/path/to/initramfs.img"},
},
scriptParams{
xname: "x0000c0s0b0n0",
nid: "0",
},
[]string{
"initrd=initrd",
"root=nfs:example/path/to/rootfs:ro",
"xname=x0000c0s0b0n0",
"nid=0",
fmt.Sprintf("ds=nocloud-net;s=%s/", advertiseAddress),
},
},
{
// bss referral token
BootData{
Params: "root=nfs:example/path/to/rootfs:ro",
Kernel: ImageData{Path: "http://example/path/to/vmlinuz"},
Initrd: ImageData{Path: "http://example/path/to/initramfs.img"},
},
scriptParams{
xname: "x0000c0s0b0n0",
nid: "0",
referralToken: "meaningless_but_nonempty_string",
},
[]string{
"initrd=initrd",
"root=nfs:example/path/to/rootfs:ro",
"xname=x0000c0s0b0n0",
"nid=0",
"bss_referral_token=meaningless_but_nonempty_string",
fmt.Sprintf("ds=nocloud-net;s=%s/", advertiseAddress),
},
},
{
// params set under kernel
BootData{
Params: "root=nfs:example/path/to/rootfs:ro",
Kernel: ImageData{Path: "http://example/path/to/vmlinuz", Params: "kernel_param=set"},
Initrd: ImageData{Path: "http://example/path/to/initramfs.img"},
},
scriptParams{
xname: "x0000c0s0b0n0",
nid: "0",
},
[]string{
"initrd=initrd",
"root=nfs:example/path/to/rootfs:ro",
"kernel_param=set",
"xname=x0000c0s0b0n0",
"nid=0",
fmt.Sprintf("ds=nocloud-net;s=%s/", advertiseAddress),
},
},
{
// params set under initrd
BootData{
Params: "root=nfs:example/path/to/rootfs:ro",
Kernel: ImageData{Path: "http://example/path/to/vmlinuz"},
Initrd: ImageData{Path: "http://example/path/to/initramfs.img", Params: "init_param=set"},
},
scriptParams{
xname: "x0000c0s0b0n0",
nid: "0",
},
[]string{
"initrd=initrd",
"root=nfs:example/path/to/rootfs:ro",
"init_param=set",
"xname=x0000c0s0b0n0",
"nid=0",
fmt.Sprintf("ds=nocloud-net;s=%s/", advertiseAddress),
},
},
{
// initrd already in bootdata params
BootData{
Params: "initrd=should_get_deleted root=nfs:example/path/to/rootfs:ro",
Kernel: ImageData{Path: "http://example/path/to/vmlinuz"},
Initrd: ImageData{Path: "http://example/path/to/initramfs.img"},
},
scriptParams{
xname: "x0000c0s0b0n0",
nid: "0",
},
[]string{
"initrd=initrd",
"root=nfs:example/path/to/rootfs:ro",
"xname=x0000c0s0b0n0",
"nid=0",
fmt.Sprintf("ds=nocloud-net;s=%s/", advertiseAddress),
},
},
{
// initrd already in bootdata params (at the end)
BootData{
Params: "root=nfs:example/path/to/rootfs:ro initrd=should_get_deleted",
Kernel: ImageData{Path: "http://example/path/to/vmlinuz"},
Initrd: ImageData{Path: "http://example/path/to/initramfs.img"},
},
scriptParams{
xname: "x0000c0s0b0n0",
nid: "0",
},
[]string{
"initrd=initrd",
"root=nfs:example/path/to/rootfs:ro",
"xname=x0000c0s0b0n0",
"nid=0",
fmt.Sprintf("ds=nocloud-net;s=%s/", advertiseAddress),
}},
}

for _, tc := range test_cases {
// role and subRole are only used when adding spire jopin tokens, which we can't test for this way.
output, err := buildParams(tc.bd, tc.sp, "dummy role", "dummy subRole")
if err != nil {
t.Errorf("Failed to build params: %v\n", err)
}

outputParams := strings.Fields(output)
if !reflect.DeepEqual(tc.expectedParams, outputParams) {
t.Log("Built params did not match.")
t.Logf(" expected: %v", tc.expectedParams)
t.Logf(" got: %v", outputParams)
t.Fail()
}
}
}

func TestBootparametersPost_Success(t *testing.T) {

args := bssTypes.BootParams{
Expand Down
22 changes: 22 additions & 0 deletions docs/examples.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,28 @@ Identify the node which expects to receive an iPXE boot script.

----

[source, bash]
.Use curl to request boot script components in JSON
----

curl 'https://sms-1/apis/bss/boot/v1/bootscript?mac=44:A8:42:21:A8:AD&json=1'
----

[source, json]
.A JSON object similar to the following will be returned:
----

{
"params": "kernel bootname=x0c0s7b0n0 console=ttyS0,115200 console=tty0 unregistered=1 heartbeat_url=http://sms-1/apis/hbtd/heartbeat bootmac=44:A8:42:21:A8:AD",
"kernel": {
"path": "http://sms-1/apis/ars/assets/artifacts/generic/vmlinuz_801"
},
"initrd": {
"path": "http://sms-1/apis/ars/assets/artifacts/generic/initramfs-cray_1058.img"
}
}
----

=== Retrieve current boot parameters for one or more nodes
Make a GET request to /bootparameters endpoint.
Specifiy a list of nodes to retrieve parameters for. If no nodes are given, will return all nodes which currently have parameter settings.
Expand Down
Loading