diff --git a/api/swagger.yaml b/api/swagger.yaml index ff518dd..66cb43e 100644 --- a/api/swagger.yaml +++ b/api/swagger.yaml @@ -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 diff --git a/cmd/boot-script-service/default_api.go b/cmd/boot-script-service/default_api.go index 7e6812d..dc779aa 100644 --- a/cmd/boot-script-service/default_api.go +++ b/cmd/boot-script-service/default_api.go @@ -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 @@ -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 { @@ -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 @@ -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) @@ -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 diff --git a/cmd/boot-script-service/default_api_test.go b/cmd/boot-script-service/default_api_test.go index bbdd97d..ba320fe 100644 --- a/cmd/boot-script-service/default_api_test.go +++ b/cmd/boot-script-service/default_api_test.go @@ -28,7 +28,9 @@ import ( "fmt" "net/http" "net/http/httptest" + "reflect" "regexp" + "strings" "testing" "github.com/OpenCHAMI/bss/pkg/bssTypes" @@ -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{ diff --git a/docs/examples.adoc b/docs/examples.adoc index 0e81eb9..384545c 100644 --- a/docs/examples.adoc +++ b/docs/examples.adoc @@ -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.