From 7fac31eaaa70a0c6beea8e47bd7c8d0a5ba4c35b Mon Sep 17 00:00:00 2001 From: kkumar Date: Fri, 3 Jan 2025 15:52:08 +0530 Subject: [PATCH 1/5] Implement new configuration strategy for HTTPBOOT plugin --- example/httpboot_config.yaml | 1 + internal/api/httpboot_config.go | 8 ++++ plugins/httpboot/custom_httpboot_config.yaml | 1 + plugins/httpboot/httpboot_config.yaml | 1 + .../httpboot/malformed_httpboot_config.yaml | 1 + plugins/httpboot/plugin.go | 39 +++++++++++++++--- plugins/httpboot/plugin_test.go | 41 ++++++++++--------- 7 files changed, 68 insertions(+), 24 deletions(-) create mode 100644 example/httpboot_config.yaml create mode 100644 internal/api/httpboot_config.go create mode 100644 plugins/httpboot/custom_httpboot_config.yaml create mode 100644 plugins/httpboot/httpboot_config.yaml create mode 100644 plugins/httpboot/malformed_httpboot_config.yaml diff --git a/example/httpboot_config.yaml b/example/httpboot_config.yaml new file mode 100644 index 0000000..cc00eab --- /dev/null +++ b/example/httpboot_config.yaml @@ -0,0 +1 @@ +bootServer: http://[2001:db8::1]/image.uki \ No newline at end of file diff --git a/internal/api/httpboot_config.go b/internal/api/httpboot_config.go new file mode 100644 index 0000000..df61e29 --- /dev/null +++ b/internal/api/httpboot_config.go @@ -0,0 +1,8 @@ +// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and IronCore contributors +// SPDX-License-Identifier: MIT + +package api + +type HttpBootConfig struct { + BootServer string `yaml:"bootServer"` +} diff --git a/plugins/httpboot/custom_httpboot_config.yaml b/plugins/httpboot/custom_httpboot_config.yaml new file mode 100644 index 0000000..3f14053 --- /dev/null +++ b/plugins/httpboot/custom_httpboot_config.yaml @@ -0,0 +1 @@ +bootServer: "bootservice:http://[::1]:8888/httpboot" \ No newline at end of file diff --git a/plugins/httpboot/httpboot_config.yaml b/plugins/httpboot/httpboot_config.yaml new file mode 100644 index 0000000..cce8035 --- /dev/null +++ b/plugins/httpboot/httpboot_config.yaml @@ -0,0 +1 @@ +bootServer: "https://[2001:db8::1]/boot.uki" \ No newline at end of file diff --git a/plugins/httpboot/malformed_httpboot_config.yaml b/plugins/httpboot/malformed_httpboot_config.yaml new file mode 100644 index 0000000..69e8440 --- /dev/null +++ b/plugins/httpboot/malformed_httpboot_config.yaml @@ -0,0 +1 @@ +bootServer: ftp://www.example.com/boot.uki \ No newline at end of file diff --git a/plugins/httpboot/plugin.go b/plugins/httpboot/plugin.go index bf621b1..264ed48 100644 --- a/plugins/httpboot/plugin.go +++ b/plugins/httpboot/plugin.go @@ -10,6 +10,7 @@ import ( "io" "net/http" "net/url" + "os" "strings" "github.com/coredhcp/coredhcp/handler" @@ -17,6 +18,8 @@ import ( "github.com/coredhcp/coredhcp/plugins" "github.com/insomniacslk/dhcp/dhcpv4" "github.com/insomniacslk/dhcp/dhcpv6" + "github.com/ironcore-dev/fedhcp/internal/api" + "gopkg.in/yaml.v2" ) var bootFile4 string @@ -33,11 +36,19 @@ var Plugin = plugins.Plugin{ const httpClient = "HTTPClient" -func parseArgs(args ...string) (*url.URL, bool, error) { +func parseArgs(args ...string) (string, error) { if len(args) != 1 { - return nil, false, fmt.Errorf("exactly one argument must be passed to the httpboot plugin, got %d", len(args)) + return "", fmt.Errorf("exactly one argument must be passed to the httpboot plugin, got %d", len(args)) } - arg := args[0] + return args[0], nil +} + +func parseConfig(args ...string) (*url.URL, bool, error) { + httpbootConfig, err := loadConfig(args...) + if err != nil { + return nil, false, fmt.Errorf("erorr loading httpboot plugin config: %v", err) + } + arg := httpbootConfig.BootServer useBootService := strings.HasPrefix(arg, "bootservice:") if useBootService { arg = strings.TrimPrefix(arg, "bootservice:") @@ -52,8 +63,26 @@ func parseArgs(args ...string) (*url.URL, bool, error) { return parsedURL, useBootService, nil } +func loadConfig(args ...string) (*api.HttpBootConfig, error) { + path, err := parseArgs(args...) + if err != nil { + return nil, fmt.Errorf("invalid configuration: %v", err) + } + + log.Debugf("Reading httpboot config file %s", path) + configData, err := os.ReadFile(path) + if err != nil { + return nil, fmt.Errorf("failed to read config file: %v", err) + } + config := &api.HttpBootConfig{} + if err = yaml.Unmarshal(configData, config); err != nil { + return nil, fmt.Errorf("failed to parse config file: %v", err) + } + return config, nil +} + func setup6(args ...string) (handler.Handler6, error) { - u, ubs, err := parseArgs(args...) + u, ubs, err := parseConfig(args...) if err != nil { return nil, fmt.Errorf("invalid configuration: %v", err) } @@ -64,7 +93,7 @@ func setup6(args ...string) (handler.Handler6, error) { } func setup4(args ...string) (handler.Handler4, error) { - u, ubs, err := parseArgs(args...) + u, ubs, err := parseConfig(args...) if err != nil { return nil, fmt.Errorf("invalid configuration: %v", err) } diff --git a/plugins/httpboot/plugin_test.go b/plugins/httpboot/plugin_test.go index 53cee9b..7e14aa7 100644 --- a/plugins/httpboot/plugin_test.go +++ b/plugins/httpboot/plugin_test.go @@ -31,18 +31,20 @@ const ( ) var ( - expectedHTTPClient = []byte("HTTPClient") + expectedHTTPClient = []byte("HTTPClient") + httpbootFilePath = []string{"httpboot_config.yaml"} + customHttpbootFilePath = []string{"custom_httpboot_config.yaml"} ) -func Init4(bootURL string) { - _, err := setup4(bootURL) +func Init4(bootFileUrl ...string) { + _, err := setup4(bootFileUrl...) if err != nil { log.Fatal(err) } } -func Init6(bootURL string) { - _, err := setup6(bootURL) +func Init6(bootFileUrl ...string) { + _, err := setup6(bootFileUrl...) if err != nil { log.Fatal(err) } @@ -51,12 +53,12 @@ func Init6(bootURL string) { /* parametrization */ func TestWrongNumberArgs(t *testing.T) { - _, _, err := parseArgs("foo", "bar") + _, err := parseArgs("foo", "bar") if err == nil { t.Fatal("no error occurred when providing wrong number of args (2), but it should have") } - _, _, err = parseArgs() + _, err = parseArgs() if err == nil { t.Fatal("no error occurred when providing wrong number of args (0), but it should have") } @@ -69,12 +71,13 @@ func TestWrongArgs(t *testing.T) { "bootfail:https://www.example.com/boot.uki", "bootservice:tftp://www.example.com/boot.uki"} + malformedHttpbootFilePath := []string{"malformed_httpboot_config.yaml"} for _, wrongURL := range malformedBootURL { - _, err := setup4(wrongURL) + _, err := setup4(malformedHttpbootFilePath...) if err == nil { t.Fatalf("no error occurred when parsing wrong boot param %s, but it should have", wrongURL) } - _, err = setup6(wrongURL) + _, err = setup6(malformedHttpbootFilePath...) if err == nil { t.Fatalf("no error occurred when parsing wrong boot param %s, but it should have", wrongURL) } @@ -83,7 +86,7 @@ func TestWrongArgs(t *testing.T) { /* IPv6 */ func TestGenericHTTPBootRequested6(t *testing.T) { - Init6(expectedGenericBootURL) + Init6(httpbootFilePath...) req, err := dhcpv6.NewMessage() if err != nil { @@ -141,7 +144,7 @@ func TestGenericHTTPBootRequested6(t *testing.T) { } func TestMalformedHTTPBootRequested6(t *testing.T) { - Init6(expectedGenericBootURL) + Init6(httpbootFilePath...) req, err := dhcpv6.NewMessage() if err != nil { @@ -210,7 +213,7 @@ func TestMalformedHTTPBootRequested6(t *testing.T) { } func TestHTTPBootNotRequested6(t *testing.T) { - Init6(expectedGenericBootURL) + Init6(httpbootFilePath...) req, err := dhcpv6.NewMessage() if err != nil { @@ -251,7 +254,7 @@ func TestHTTPBootNotRequested6(t *testing.T) { } func TestHTTPBootNotRelayedMsg6(t *testing.T) { - Init6(expectedGenericBootURL) + Init6(httpbootFilePath...) req, err := dhcpv6.NewMessage() if err != nil { @@ -287,7 +290,7 @@ func TestHTTPBootNotRelayedMsg6(t *testing.T) { /* IPv4 */ func TestGenericHTTPBootRequested4(t *testing.T) { - Init4(expectedGenericBootURL) + Init4(httpbootFilePath...) req, err := dhcpv4.NewDiscovery(net.HardwareAddr{ 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff}, @@ -325,7 +328,7 @@ func TestGenericHTTPBootRequested4(t *testing.T) { } func TestMalformedHTTPBootRequested4(t *testing.T) { - Init4(expectedGenericBootURL) + Init4(httpbootFilePath...) req, err := dhcpv4.NewDiscovery(net.HardwareAddr{ 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff}, @@ -393,7 +396,7 @@ func TestMalformedHTTPBootRequested4(t *testing.T) { } func TestHTTPBootNotRequested4(t *testing.T) { - Init4(expectedGenericBootURL) + Init4(httpbootFilePath...) req, err := dhcpv4.NewDiscovery(net.HardwareAddr{ 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff}, @@ -434,7 +437,7 @@ func TestCustomHTTPBootRequestedKnownIP(t *testing.T) { go startBootServiceMock() time.Sleep(time.Second * 1) - Init6(fmt.Sprintf(bootServiceEndpoint, bootServicePort)) + Init6(customHttpbootFilePath...) req, err := dhcpv6.NewMessage() if err != nil { t.Fatal(err) @@ -570,7 +573,7 @@ func TestCustomHTTPBootRequestedUnknownClient(t *testing.T) { } func createHTTPBootRequest(t *testing.T) (error, *dhcpv6.RelayMessage) { - Init6(fmt.Sprintf(bootServiceEndpoint, bootServicePort)) + Init6(customHttpbootFilePath...) req, err := dhcpv6.NewMessage() if err != nil { t.Fatal(err) @@ -596,7 +599,7 @@ func createHTTPBootRequest(t *testing.T) (error, *dhcpv6.RelayMessage) { } func TestNoRelayCustomHTTPBootRequested(t *testing.T) { - Init6(fmt.Sprintf(bootServiceEndpoint, bootServicePort)) + Init6(customHttpbootFilePath...) req, err := dhcpv6.NewMessage() if err != nil { From 8f09a55d3e4f704658c433d30397cd224a7d3d55 Mon Sep 17 00:00:00 2001 From: kkumar Date: Fri, 3 Jan 2025 15:59:51 +0530 Subject: [PATCH 2/5] Fix complaince check --- .reuse/dep5 | 1 + README.md | 5 ++++- config/default/config.yaml | 2 +- example/config.yaml | 2 +- 4 files changed, 7 insertions(+), 3 deletions(-) diff --git a/.reuse/dep5 b/.reuse/dep5 index 81c5ca6..180f5b0 100644 --- a/.reuse/dep5 +++ b/.reuse/dep5 @@ -17,6 +17,7 @@ Files: go.mod go.sum hack/* + plugins/httpboot/*.yaml Copyright: 2024 SAP SE or an SAP affiliate company and IronCore contributors License: MIT diff --git a/README.md b/README.md index ff176f4..70064c2 100644 --- a/README.md +++ b/README.md @@ -30,9 +30,12 @@ Implements HTTP boot from [Unifed Kernel Image](https://uapi-group.org/specifica Delivers the HTTP boot image as a [BootFileURL](https://www.rfc-editor.org/rfc/rfc5970.html#section-3.2). Based on configuration it delivers either a client-specific UKIs dynamically or a default UKI for all clients. When client-specific UKIs are configured, IPv6 relays *must* be used, so the client can be identified based on its link-local address (which the relay always provides). ### Configuration -A single HTTP(s) URL shall be passed as a string. It must be either +A single HTTP(s) URL shall be passed as a string in `httpboot_config.yaml` as given below. It must be either - a direct URL to an UKI (default UKI for all clients) - magic identifier `bootservice:`+ a URL to a boot service delivering dynamically client-specific UKIs based on client identification +````yaml +bootServer: http://[2001:db8::1]/image.uki +```` ### Notes - not tested on IPv4 - IPv6 relays are supported diff --git a/config/default/config.yaml b/config/default/config.yaml index 93b3564..3a25b2f 100644 --- a/config/default/config.yaml +++ b/config/default/config.yaml @@ -14,4 +14,4 @@ data: - onmetal: - dns: 2001:4860:4860::6464 2001:4860:4860::64 - pxeboot: pxeboot_config.yaml - - httpboot: bootservice:http://example.com/boot-operator-endpoint:8083 + - httpboot: httpboot_config.yaml diff --git a/example/config.yaml b/example/config.yaml index 742bcad..23aa90f 100644 --- a/example/config.yaml +++ b/example/config.yaml @@ -8,7 +8,7 @@ server6: # always provide the same IP address, no matter who's asking: # - bluefield: bluefield_config.yaml # implement HTTPBoot - - httpboot: http://[2001:db8::1]/image.uki + - httpboot: httpboot_config.yaml # add leased IPs to ironcore's IPAM - ipam: ipam_config.yaml # lease IPs based on /127 subnets coming from relays running on the switches From 8ab95544a5309f8fe30c693087f94604ec2b7809 Mon Sep 17 00:00:00 2001 From: kkumar Date: Wed, 8 Jan 2025 20:30:41 +0530 Subject: [PATCH 3/5] Add test configs dynamically --- example/httpboot_config.yaml | 3 +- internal/api/httpboot_config.go | 3 +- plugins/httpboot/custom_httpboot_config.yaml | 1 - plugins/httpboot/httpboot_config.yaml | 1 - .../httpboot/malformed_httpboot_config.yaml | 1 - plugins/httpboot/plugin.go | 2 +- plugins/httpboot/plugin_test.go | 108 ++++++++++++++---- 7 files changed, 91 insertions(+), 28 deletions(-) delete mode 100644 plugins/httpboot/custom_httpboot_config.yaml delete mode 100644 plugins/httpboot/httpboot_config.yaml delete mode 100644 plugins/httpboot/malformed_httpboot_config.yaml diff --git a/example/httpboot_config.yaml b/example/httpboot_config.yaml index cc00eab..86ec062 100644 --- a/example/httpboot_config.yaml +++ b/example/httpboot_config.yaml @@ -1 +1,2 @@ -bootServer: http://[2001:db8::1]/image.uki \ No newline at end of file +bootServer: http://[2001:db8::1]/image.uki +clientSpecific: false \ No newline at end of file diff --git a/internal/api/httpboot_config.go b/internal/api/httpboot_config.go index df61e29..e458221 100644 --- a/internal/api/httpboot_config.go +++ b/internal/api/httpboot_config.go @@ -4,5 +4,6 @@ package api type HttpBootConfig struct { - BootServer string `yaml:"bootServer"` + BootServer string `yaml:"bootServer"` + ClientSpecific bool `yaml:"clientSpecific"` } diff --git a/plugins/httpboot/custom_httpboot_config.yaml b/plugins/httpboot/custom_httpboot_config.yaml deleted file mode 100644 index 3f14053..0000000 --- a/plugins/httpboot/custom_httpboot_config.yaml +++ /dev/null @@ -1 +0,0 @@ -bootServer: "bootservice:http://[::1]:8888/httpboot" \ No newline at end of file diff --git a/plugins/httpboot/httpboot_config.yaml b/plugins/httpboot/httpboot_config.yaml deleted file mode 100644 index cce8035..0000000 --- a/plugins/httpboot/httpboot_config.yaml +++ /dev/null @@ -1 +0,0 @@ -bootServer: "https://[2001:db8::1]/boot.uki" \ No newline at end of file diff --git a/plugins/httpboot/malformed_httpboot_config.yaml b/plugins/httpboot/malformed_httpboot_config.yaml deleted file mode 100644 index 69e8440..0000000 --- a/plugins/httpboot/malformed_httpboot_config.yaml +++ /dev/null @@ -1 +0,0 @@ -bootServer: ftp://www.example.com/boot.uki \ No newline at end of file diff --git a/plugins/httpboot/plugin.go b/plugins/httpboot/plugin.go index 264ed48..35e99dc 100644 --- a/plugins/httpboot/plugin.go +++ b/plugins/httpboot/plugin.go @@ -49,7 +49,7 @@ func parseConfig(args ...string) (*url.URL, bool, error) { return nil, false, fmt.Errorf("erorr loading httpboot plugin config: %v", err) } arg := httpbootConfig.BootServer - useBootService := strings.HasPrefix(arg, "bootservice:") + useBootService := httpbootConfig.ClientSpecific if useBootService { arg = strings.TrimPrefix(arg, "bootservice:") } diff --git a/plugins/httpboot/plugin_test.go b/plugins/httpboot/plugin_test.go index 7e14aa7..b9ff985 100644 --- a/plugins/httpboot/plugin_test.go +++ b/plugins/httpboot/plugin_test.go @@ -9,6 +9,7 @@ import ( "fmt" "net" "net/http" + "os" "strings" "testing" "time" @@ -16,6 +17,8 @@ import ( "github.com/insomniacslk/dhcp/dhcpv4" "github.com/insomniacslk/dhcp/dhcpv6" "github.com/insomniacslk/dhcp/iana" + "github.com/ironcore-dev/fedhcp/internal/api" + "gopkg.in/yaml.v2" ) const ( @@ -31,23 +34,69 @@ const ( ) var ( - expectedHTTPClient = []byte("HTTPClient") - httpbootFilePath = []string{"httpboot_config.yaml"} - customHttpbootFilePath = []string{"custom_httpboot_config.yaml"} + expectedHTTPClient = []byte("HTTPClient") + tempConfigFilePattern = "*-httpboot_config.yaml" + genericConfig = &api.HttpBootConfig{ + BootServer: "https://[2001:db8::1]/boot.uki", + ClientSpecific: false, + } + + customConfig = &api.HttpBootConfig{ + BootServer: "http://[::1]:8888/httpboot", + ClientSpecific: true, + } ) -func Init4(bootFileUrl ...string) { - _, err := setup4(bootFileUrl...) +func Init4(config api.HttpBootConfig, tempDir string) error { + configFile, err := createTempConfig(config, tempDir) if err != nil { - log.Fatal(err) + return err } + + _, err = setup4(configFile) + if err != nil { + return err + } + + return nil } -func Init6(bootFileUrl ...string) { - _, err := setup6(bootFileUrl...) +func Init6(config api.HttpBootConfig, tempDir string) error { + configFile, err := createTempConfig(config, tempDir) if err != nil { - log.Fatal(err) + return err } + + _, err = setup6(configFile) + if err != nil { + return err + } + + return nil +} + +func createTempConfig(config api.HttpBootConfig, tempDir string) (string, error) { + configData, err := yaml.Marshal(config) + if err != nil { + return "", err + } + + file, err := os.CreateTemp(tempDir, tempConfigFilePattern) + if err != nil { + return "", err + } + defer func() { + _ = file.Close() + }() + + configFile := file.Name() + + err = os.WriteFile(configFile, configData, 0644) + if err != nil { + return "", err + } + + return configFile, nil } /* parametrization */ @@ -71,13 +120,17 @@ func TestWrongArgs(t *testing.T) { "bootfail:https://www.example.com/boot.uki", "bootservice:tftp://www.example.com/boot.uki"} - malformedHttpbootFilePath := []string{"malformed_httpboot_config.yaml"} + malformedConfig := &api.HttpBootConfig{ + BootServer: "ftp://www.example.com/boot.uki", + ClientSpecific: false, + } + tempDir := t.TempDir() for _, wrongURL := range malformedBootURL { - _, err := setup4(malformedHttpbootFilePath...) + err := Init4(*malformedConfig, tempDir) if err == nil { t.Fatalf("no error occurred when parsing wrong boot param %s, but it should have", wrongURL) } - _, err = setup6(malformedHttpbootFilePath...) + err = Init6(*malformedConfig, tempDir) if err == nil { t.Fatalf("no error occurred when parsing wrong boot param %s, but it should have", wrongURL) } @@ -86,7 +139,8 @@ func TestWrongArgs(t *testing.T) { /* IPv6 */ func TestGenericHTTPBootRequested6(t *testing.T) { - Init6(httpbootFilePath...) + tempDir := t.TempDir() + _ = Init6(*genericConfig, tempDir) req, err := dhcpv6.NewMessage() if err != nil { @@ -144,7 +198,8 @@ func TestGenericHTTPBootRequested6(t *testing.T) { } func TestMalformedHTTPBootRequested6(t *testing.T) { - Init6(httpbootFilePath...) + tempDir := t.TempDir() + _ = Init6(*genericConfig, tempDir) req, err := dhcpv6.NewMessage() if err != nil { @@ -213,7 +268,8 @@ func TestMalformedHTTPBootRequested6(t *testing.T) { } func TestHTTPBootNotRequested6(t *testing.T) { - Init6(httpbootFilePath...) + tempDir := t.TempDir() + _ = Init6(*genericConfig, tempDir) req, err := dhcpv6.NewMessage() if err != nil { @@ -254,7 +310,8 @@ func TestHTTPBootNotRequested6(t *testing.T) { } func TestHTTPBootNotRelayedMsg6(t *testing.T) { - Init6(httpbootFilePath...) + tempDir := t.TempDir() + _ = Init6(*genericConfig, tempDir) req, err := dhcpv6.NewMessage() if err != nil { @@ -290,7 +347,8 @@ func TestHTTPBootNotRelayedMsg6(t *testing.T) { /* IPv4 */ func TestGenericHTTPBootRequested4(t *testing.T) { - Init4(httpbootFilePath...) + tempDir := t.TempDir() + _ = Init4(*genericConfig, tempDir) req, err := dhcpv4.NewDiscovery(net.HardwareAddr{ 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff}, @@ -328,7 +386,8 @@ func TestGenericHTTPBootRequested4(t *testing.T) { } func TestMalformedHTTPBootRequested4(t *testing.T) { - Init4(httpbootFilePath...) + tempDir := t.TempDir() + _ = Init4(*genericConfig, tempDir) req, err := dhcpv4.NewDiscovery(net.HardwareAddr{ 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff}, @@ -396,7 +455,8 @@ func TestMalformedHTTPBootRequested4(t *testing.T) { } func TestHTTPBootNotRequested4(t *testing.T) { - Init4(httpbootFilePath...) + tempDir := t.TempDir() + _ = Init4(*genericConfig, tempDir) req, err := dhcpv4.NewDiscovery(net.HardwareAddr{ 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff}, @@ -437,7 +497,8 @@ func TestCustomHTTPBootRequestedKnownIP(t *testing.T) { go startBootServiceMock() time.Sleep(time.Second * 1) - Init6(customHttpbootFilePath...) + tempDir := t.TempDir() + _ = Init6(*customConfig, tempDir) req, err := dhcpv6.NewMessage() if err != nil { t.Fatal(err) @@ -573,7 +634,9 @@ func TestCustomHTTPBootRequestedUnknownClient(t *testing.T) { } func createHTTPBootRequest(t *testing.T) (error, *dhcpv6.RelayMessage) { - Init6(customHttpbootFilePath...) + tempDir := t.TempDir() + _ = Init6(*customConfig, tempDir) + req, err := dhcpv6.NewMessage() if err != nil { t.Fatal(err) @@ -599,7 +662,8 @@ func createHTTPBootRequest(t *testing.T) (error, *dhcpv6.RelayMessage) { } func TestNoRelayCustomHTTPBootRequested(t *testing.T) { - Init6(customHttpbootFilePath...) + tempDir := t.TempDir() + _ = Init6(*customConfig, tempDir) req, err := dhcpv6.NewMessage() if err != nil { From 3f9c047e626e58b7b75923589c3dd3b154a79162 Mon Sep 17 00:00:00 2001 From: kkumar Date: Wed, 8 Jan 2025 21:09:08 +0530 Subject: [PATCH 4/5] Update Readme.md --- README.md | 6 ++++-- example/httpboot_config.yaml | 2 +- internal/api/httpboot_config.go | 2 +- plugins/httpboot/plugin.go | 8 ++------ plugins/httpboot/plugin_test.go | 6 +++--- 5 files changed, 11 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 70064c2..c381d12 100644 --- a/README.md +++ b/README.md @@ -32,9 +32,11 @@ Delivers the HTTP boot image as a [BootFileURL](https://www.rfc-editor.org/rfc/r ### Configuration A single HTTP(s) URL shall be passed as a string in `httpboot_config.yaml` as given below. It must be either - a direct URL to an UKI (default UKI for all clients) -- magic identifier `bootservice:`+ a URL to a boot service delivering dynamically client-specific UKIs based on client identification +- `clientSpecific: false` (for all clients). +- `clientSpecific: true` to use a boot service delivering dynamically client-specific UKIs based on client identification ````yaml -bootServer: http://[2001:db8::1]/image.uki +bootFile: http://[2001:db8::1]/image.uki +clientSpecific: false ```` ### Notes - not tested on IPv4 diff --git a/example/httpboot_config.yaml b/example/httpboot_config.yaml index 86ec062..3ca5af1 100644 --- a/example/httpboot_config.yaml +++ b/example/httpboot_config.yaml @@ -1,2 +1,2 @@ -bootServer: http://[2001:db8::1]/image.uki +bootFile: http://[2001:db8::1]/image.uki clientSpecific: false \ No newline at end of file diff --git a/internal/api/httpboot_config.go b/internal/api/httpboot_config.go index e458221..c5cd0cf 100644 --- a/internal/api/httpboot_config.go +++ b/internal/api/httpboot_config.go @@ -4,6 +4,6 @@ package api type HttpBootConfig struct { - BootServer string `yaml:"bootServer"` + BootFile string `yaml:"bootFile"` ClientSpecific bool `yaml:"clientSpecific"` } diff --git a/plugins/httpboot/plugin.go b/plugins/httpboot/plugin.go index 35e99dc..932625f 100644 --- a/plugins/httpboot/plugin.go +++ b/plugins/httpboot/plugin.go @@ -48,11 +48,7 @@ func parseConfig(args ...string) (*url.URL, bool, error) { if err != nil { return nil, false, fmt.Errorf("erorr loading httpboot plugin config: %v", err) } - arg := httpbootConfig.BootServer - useBootService := httpbootConfig.ClientSpecific - if useBootService { - arg = strings.TrimPrefix(arg, "bootservice:") - } + arg := httpbootConfig.BootFile parsedURL, err := url.Parse(arg) if err != nil { return nil, false, fmt.Errorf("invalid URL: %v", err) @@ -60,7 +56,7 @@ func parseConfig(args ...string) (*url.URL, bool, error) { if (parsedURL.Scheme != "http" && parsedURL.Scheme != "https") || parsedURL.Host == "" || parsedURL.Path == "" { return nil, false, fmt.Errorf("malformed httpboot parameter, should be a valid HTTP(s) URL") } - return parsedURL, useBootService, nil + return parsedURL, httpbootConfig.ClientSpecific, nil } func loadConfig(args ...string) (*api.HttpBootConfig, error) { diff --git a/plugins/httpboot/plugin_test.go b/plugins/httpboot/plugin_test.go index b9ff985..94aa921 100644 --- a/plugins/httpboot/plugin_test.go +++ b/plugins/httpboot/plugin_test.go @@ -37,12 +37,12 @@ var ( expectedHTTPClient = []byte("HTTPClient") tempConfigFilePattern = "*-httpboot_config.yaml" genericConfig = &api.HttpBootConfig{ - BootServer: "https://[2001:db8::1]/boot.uki", + BootFile: "https://[2001:db8::1]/boot.uki", ClientSpecific: false, } customConfig = &api.HttpBootConfig{ - BootServer: "http://[::1]:8888/httpboot", + BootFile: "http://[::1]:8888/httpboot", ClientSpecific: true, } ) @@ -121,7 +121,7 @@ func TestWrongArgs(t *testing.T) { "bootservice:tftp://www.example.com/boot.uki"} malformedConfig := &api.HttpBootConfig{ - BootServer: "ftp://www.example.com/boot.uki", + BootFile: "ftp://www.example.com/boot.uki", ClientSpecific: false, } tempDir := t.TempDir() From b1fa4846cd39b8b35660628b2447c9bd539f995d Mon Sep 17 00:00:00 2001 From: Damyan Yordanov Date: Thu, 16 Jan 2025 13:38:09 +0100 Subject: [PATCH 5/5] Refactor tests Fix some debugging output --- .reuse/dep5 | 1 - README.md | 6 +- example/httpboot_config.yaml | 2 +- plugins/httpboot/plugin.go | 37 ++++---- plugins/httpboot/plugin_test.go | 156 ++++++++++---------------------- 5 files changed, 70 insertions(+), 132 deletions(-) diff --git a/.reuse/dep5 b/.reuse/dep5 index 180f5b0..81c5ca6 100644 --- a/.reuse/dep5 +++ b/.reuse/dep5 @@ -17,7 +17,6 @@ Files: go.mod go.sum hack/* - plugins/httpboot/*.yaml Copyright: 2024 SAP SE or an SAP affiliate company and IronCore contributors License: MIT diff --git a/README.md b/README.md index c381d12..9dbc003 100644 --- a/README.md +++ b/README.md @@ -32,10 +32,10 @@ Delivers the HTTP boot image as a [BootFileURL](https://www.rfc-editor.org/rfc/r ### Configuration A single HTTP(s) URL shall be passed as a string in `httpboot_config.yaml` as given below. It must be either - a direct URL to an UKI (default UKI for all clients) -- `clientSpecific: false` (for all clients). -- `clientSpecific: true` to use a boot service delivering dynamically client-specific UKIs based on client identification +- `clientSpecific: false` deliver default UKI for all clients via a static address +- `clientSpecific: true` use a boot service delivering dynamically client-specific UKIs, based on client identification ````yaml -bootFile: http://[2001:db8::1]/image.uki +bootFile: http://[2001:db8::1]/default-image.uki clientSpecific: false ```` ### Notes diff --git a/example/httpboot_config.yaml b/example/httpboot_config.yaml index 3ca5af1..98b4e6b 100644 --- a/example/httpboot_config.yaml +++ b/example/httpboot_config.yaml @@ -1,2 +1,2 @@ -bootFile: http://[2001:db8::1]/image.uki +bootFile: http://[2001:db8::1]/default-image.uki clientSpecific: false \ No newline at end of file diff --git a/plugins/httpboot/plugin.go b/plugins/httpboot/plugin.go index 932625f..940b3f2 100644 --- a/plugins/httpboot/plugin.go +++ b/plugins/httpboot/plugin.go @@ -38,38 +38,23 @@ const httpClient = "HTTPClient" func parseArgs(args ...string) (string, error) { if len(args) != 1 { - return "", fmt.Errorf("exactly one argument must be passed to the httpboot plugin, got %d", len(args)) + return "", fmt.Errorf("exactly one argument must be passed to the plugin, got %d", len(args)) } return args[0], nil } -func parseConfig(args ...string) (*url.URL, bool, error) { - httpbootConfig, err := loadConfig(args...) - if err != nil { - return nil, false, fmt.Errorf("erorr loading httpboot plugin config: %v", err) - } - arg := httpbootConfig.BootFile - parsedURL, err := url.Parse(arg) - if err != nil { - return nil, false, fmt.Errorf("invalid URL: %v", err) - } - if (parsedURL.Scheme != "http" && parsedURL.Scheme != "https") || parsedURL.Host == "" || parsedURL.Path == "" { - return nil, false, fmt.Errorf("malformed httpboot parameter, should be a valid HTTP(s) URL") - } - return parsedURL, httpbootConfig.ClientSpecific, nil -} - func loadConfig(args ...string) (*api.HttpBootConfig, error) { path, err := parseArgs(args...) if err != nil { return nil, fmt.Errorf("invalid configuration: %v", err) } - log.Debugf("Reading httpboot config file %s", path) + log.Debugf("Reading config file %s", path) configData, err := os.ReadFile(path) if err != nil { return nil, fmt.Errorf("failed to read config file: %v", err) } + config := &api.HttpBootConfig{} if err = yaml.Unmarshal(configData, config); err != nil { return nil, fmt.Errorf("failed to parse config file: %v", err) @@ -77,6 +62,22 @@ func loadConfig(args ...string) (*api.HttpBootConfig, error) { return config, nil } +func parseConfig(args ...string) (*url.URL, bool, error) { + httpbootConfig, err := loadConfig(args...) + if err != nil { + return nil, false, fmt.Errorf("erorr loading plugin configuration: %v", err) + } + arg := httpbootConfig.BootFile + parsedURL, err := url.Parse(arg) + if err != nil { + return nil, false, fmt.Errorf("invalid URL: %v", err) + } + if (parsedURL.Scheme != "http" && parsedURL.Scheme != "https") || parsedURL.Host == "" || parsedURL.Path == "" { + return nil, false, fmt.Errorf("malformed httpboot parameter, should be a valid HTTP(s) URL") + } + return parsedURL, httpbootConfig.ClientSpecific, nil +} + func setup6(args ...string) (handler.Handler6, error) { u, ubs, err := parseConfig(args...) if err != nil { diff --git a/plugins/httpboot/plugin_test.go b/plugins/httpboot/plugin_test.go index 94aa921..649ed79 100644 --- a/plugins/httpboot/plugin_test.go +++ b/plugins/httpboot/plugin_test.go @@ -28,9 +28,12 @@ const ( expectedGenericBootURL = "https://[2001:db8::1]/boot.uki" expectedCustomBootURL = "https://[2001:db8::1]/client-specific/boot.uki" expectedDefaultCustomBootURL = "https://[2001:db8::1]/default.uki" - bootServiceEndpoint = "bootservice:http://[::1]:%d/httpboot" expectedEnterpriseNumber = 0 bootServicePort = 8888 + notKnownClientIP = "::2" + knownClientIP = "::1" + notKnownClientMAC = "11:22:33:44:55:66" + knownClientMAC = "aa:bb:cc:dd:ee:ff" ) var ( @@ -116,16 +119,13 @@ func TestWrongNumberArgs(t *testing.T) { func TestWrongArgs(t *testing.T) { malformedBootURL := []string{"ftp://www.example.com/boot.uki", "tftp:/www.example.com/boot.uki", - "foobar:/www.example.com/boot.uki", - "bootfail:https://www.example.com/boot.uki", - "bootservice:tftp://www.example.com/boot.uki"} - - malformedConfig := &api.HttpBootConfig{ - BootFile: "ftp://www.example.com/boot.uki", - ClientSpecific: false, - } - tempDir := t.TempDir() + "foobar:/www.example.com/boot.uki"} for _, wrongURL := range malformedBootURL { + malformedConfig := &api.HttpBootConfig{ + BootFile: wrongURL, + ClientSpecific: false, + } + tempDir := t.TempDir() err := Init4(*malformedConfig, tempDir) if err == nil { t.Fatalf("no error occurred when parsing wrong boot param %s, but it should have", wrongURL) @@ -497,84 +497,62 @@ func TestCustomHTTPBootRequestedKnownIP(t *testing.T) { go startBootServiceMock() time.Sleep(time.Second * 1) - tempDir := t.TempDir() - _ = Init6(*customConfig, tempDir) - req, err := dhcpv6.NewMessage() + ip := net.ParseIP(knownClientIP) + err, relayedRequest := createHTTPBootRequest(t, ip) if err != nil { t.Fatal(err) } - req.MessageType = dhcpv6.MessageTypeRequest - req.AddOption(dhcpv6.OptRequestedOption(dhcpv6.OptionBootfileURL)) - optVendorClass := dhcpv6.OptVendorClass{} - buf := []byte{ - 0, 0, 5, 57, // nice "random" enterprise number, can be ignored - 0, 10, // length ot vendor class - 'H', 'T', 'T', 'P', 'C', 'l', 'i', 'e', 'n', 't', // vendor class - } - _ = optVendorClass.FromBytes(buf) - req.UpdateOption(&optVendorClass) - // known LinkAddr - relayedRequest, err := dhcpv6.EncapsulateRelay(req, dhcpv6.MessageTypeRelayForward, net.IPv6loopback, net.IPv6loopback) + macAddress, _ := net.ParseMAC(notKnownClientMAC) + ensureBootURL(t, macAddress, relayedRequest, expectedCustomBootURL) +} + +func TestCustomHTTPBootRequestedKnownMAC(t *testing.T) { + ip := net.ParseIP(notKnownClientIP) + err, relayedRequest := createHTTPBootRequest(t, ip) if err != nil { t.Fatal(err) } - // not known LinkLayerAddress - macAddress, _ := net.ParseMAC("11:22:33:44:55:66") - opt := dhcpv6.OptClientLinkLayerAddress(iana.HWTypeEthernet, macAddress) - relayedRequest.AddOption(opt) + macAddress, _ := net.ParseMAC(knownClientMAC) + ensureBootURL(t, macAddress, relayedRequest, expectedCustomBootURL) +} - stub, err := dhcpv6.NewMessage() +func TestCustomHTTPBootRequestedUnknownClient(t *testing.T) { + ip := net.ParseIP(notKnownClientIP) + err, relayedRequest := createHTTPBootRequest(t, ip) if err != nil { t.Fatal(err) } - stub.MessageType = dhcpv6.MessageTypeReply - - resp, stop := handler6(relayedRequest, stub) - if resp == nil { - t.Fatal("plugin did not return a message") - } - if stop { - t.Error("plugin interrupted processing, but it shouldn't have") - } - - opts := resp.GetOption(dhcpv6.OptionBootfileURL) - if len(opts) != optionEnabled { - t.Fatalf("Expected %d BootFileUrl option, got %d: %v", optionEnabled, len(opts), opts) - } - bootFileURL := resp.(*dhcpv6.Message).Options.BootFileURL() - if bootFileURL != expectedCustomBootURL { - t.Errorf("Found BootFileURL %s, expected %s", bootFileURL, expectedCustomBootURL) - } + macAddress, _ := net.ParseMAC(notKnownClientMAC) + ensureBootURL(t, macAddress, relayedRequest, expectedDefaultCustomBootURL) +} - opts = resp.GetOption(dhcpv6.OptionVendorClass) - if len(opts) != optionEnabled { - t.Fatalf("Expected %d VendorClass option, got %d: %v", optionEnabled, len(opts), opts) - } +func createHTTPBootRequest(t *testing.T, clientIP net.IP) (error, *dhcpv6.RelayMessage) { + tempDir := t.TempDir() + _ = Init6(*customConfig, tempDir) - vc := resp.(*dhcpv6.Message).Options.VendorClasses()[0] - if vc.EnterpriseNumber != expectedEnterpriseNumber { - t.Errorf("Found EnterpriseNumber %d, expected %d", vc.EnterpriseNumber, expectedEnterpriseNumber) + req, err := dhcpv6.NewMessage() + if err != nil { + t.Fatal(err) } - - vcData := resp.(*dhcpv6.Message).Options.VendorClass(vc.EnterpriseNumber) - if !bytes.Equal(vcData[0], expectedHTTPClient) { - t.Errorf("Found VendorClass %x, expected %x", vcData[0], expectedHTTPClient) + req.MessageType = dhcpv6.MessageTypeRequest + req.AddOption(dhcpv6.OptRequestedOption(dhcpv6.OptionBootfileURL)) + optVendorClass := dhcpv6.OptVendorClass{} + buf := []byte{ + 0, 0, 5, 57, // nice "random" enterprise number, can be ignored + 0, 10, // length ot vendor class + 'H', 'T', 'T', 'P', 'C', 'l', 'i', 'e', 'n', 't', // vendor class } -} - -func TestCustomHTTPBootRequestedKnownMAC(t *testing.T) { - // known LinkLayerAddress - macAddress, _ := net.ParseMAC("aa:bb:cc:dd:ee:ff") + _ = optVendorClass.FromBytes(buf) + req.UpdateOption(&optVendorClass) - err, relayedRequest := createHTTPBootRequest(t) + relayedRequest, err := dhcpv6.EncapsulateRelay(req, dhcpv6.MessageTypeRelayForward, clientIP, net.IPv6loopback) if err != nil { t.Fatal(err) } - - ensureBootURL(t, macAddress, relayedRequest, expectedCustomBootURL) + return err, relayedRequest } func ensureBootURL(t *testing.T, macAddress net.HardwareAddr, relayedRequest *dhcpv6.RelayMessage, expectedBootURL string) { @@ -621,46 +599,6 @@ func ensureBootURL(t *testing.T, macAddress net.HardwareAddr, relayedRequest *dh } } -func TestCustomHTTPBootRequestedUnknownClient(t *testing.T) { - // not known LinkLayerAddress - macAddress, _ := net.ParseMAC("11:22:33:44:55:66") - - err, relayedRequest := createHTTPBootRequest(t) - if err != nil { - t.Fatal(err) - } - - ensureBootURL(t, macAddress, relayedRequest, expectedDefaultCustomBootURL) -} - -func createHTTPBootRequest(t *testing.T) (error, *dhcpv6.RelayMessage) { - tempDir := t.TempDir() - _ = Init6(*customConfig, tempDir) - - req, err := dhcpv6.NewMessage() - if err != nil { - t.Fatal(err) - } - req.MessageType = dhcpv6.MessageTypeRequest - req.AddOption(dhcpv6.OptRequestedOption(dhcpv6.OptionBootfileURL)) - optVendorClass := dhcpv6.OptVendorClass{} - buf := []byte{ - 0, 0, 5, 57, // nice "random" enterprise number, can be ignored - 0, 10, // length ot vendor class - 'H', 'T', 'T', 'P', 'C', 'l', 'i', 'e', 'n', 't', // vendor class - } - _ = optVendorClass.FromBytes(buf) - req.UpdateOption(&optVendorClass) - - // not known LinkAddr - relayedRequest, err := dhcpv6.EncapsulateRelay(req, dhcpv6.MessageTypeRelayForward, - net.IP{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2}, net.IPv6loopback) - if err != nil { - t.Fatal(err) - } - return err, relayedRequest -} - func TestNoRelayCustomHTTPBootRequested(t *testing.T) { tempDir := t.TempDir() _ = Init6(*customConfig, tempDir) @@ -729,12 +667,12 @@ func httpHandler(w http.ResponseWriter, r *http.Request) { } switch clientIP { - case "::1": + case knownClientIP: log.Printf("Match for client IP '%s' found", clientIP) httpBootResponseData["ClientIPs"] = strings.Join(clientIPs, ", ") httpBootResponseData["UKIURL"] = expectedCustomBootURL goon = false - case "aa:bb:cc:dd:ee:ff": + case knownClientMAC: log.Printf("Match for client MAC '%s' found", clientIP) httpBootResponseData["ClientIPs"] = strings.Join(clientIPs, ", ") httpBootResponseData["UKIURL"] = expectedCustomBootURL