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

Add static ipam to kernel parameters for ISO patching: #558

Merged
merged 6 commits into from
Nov 23, 2024
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
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -149,9 +149,9 @@ FLAGS
-tink-server [http] IP:Port for the Tink server
-tink-server-tls [http] use TLS for Tink server (default "false")
-trusted-proxies [http] comma separated list of trusted proxies in CIDR notation
-iso-enabled [iso] enable serving Hook as an iso (default "false")
-iso-magic-string [iso] the string pattern to match for in the source iso, if not set the default from HookOS is used
-iso-url [iso] the url for source iso before binary patching
-iso-enabled [iso] enable patching an OSIE ISO (default "false")
-iso-magic-string [iso] the string pattern to match for in the source ISO, defaults to the one defined in HookOS
-iso-static-ipam-enabled [iso] enable static IPAM for HookOS (default "false")
-otel-endpoint [otel] OpenTelemetry collector endpoint
-otel-insecure [otel] OpenTelemetry collector insecure (default "true")
-syslog-addr [syslog] local IP to listen on for Syslog messages (default "172.17.0.3")
Expand Down
7 changes: 4 additions & 3 deletions cmd/smee/flag.go
Original file line number Diff line number Diff line change
Expand Up @@ -154,9 +154,10 @@ func otelFlags(c *config, fs *flag.FlagSet) {
}

func isoFlags(c *config, fs *flag.FlagSet) {
fs.BoolVar(&c.iso.enabled, "iso-enabled", false, "[iso] enable serving Hook as an iso")
fs.StringVar(&c.iso.url, "iso-url", "", "[iso] the url for source iso before binary patching")
fs.StringVar(&c.iso.magicString, "iso-magic-string", "", "[iso] the string pattern to match for in the source iso, if not set the default from HookOS is used")
fs.BoolVar(&c.iso.enabled, "iso-enabled", false, "[iso] enable patching an OSIE ISO")
fs.StringVar(&c.iso.url, "iso-url", "", "[iso] an ISO source URL target for patching")
fs.StringVar(&c.iso.magicString, "iso-magic-string", "", "[iso] the string pattern to match for in the source ISO, defaults to the one defined in HookOS")
fs.BoolVar(&c.iso.staticIPAMEnabled, "iso-static-ipam-enabled", false, "[iso] enable static IPAM for HookOS")
}

func setFlags(c *config, fs *flag.FlagSet) {
Expand Down
7 changes: 4 additions & 3 deletions cmd/smee/flag_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -153,9 +153,10 @@ FLAGS
-tink-server-insecure-tls [http] use insecure TLS for Tink server (default "false")
-tink-server-tls [http] use TLS for Tink server (default "false")
-trusted-proxies [http] comma separated list of trusted proxies in CIDR notation
-iso-enabled [iso] enable serving Hook as an iso (default "false")
-iso-magic-string [iso] the string pattern to match for in the source iso, if not set the default from HookOS is used
-iso-url [iso] the url for source iso before binary patching
-iso-enabled [iso] enable patching an OSIE ISO (default "false")
-iso-magic-string [iso] the string pattern to match for in the source ISO, defaults to the one defined in HookOS
-iso-static-ipam-enabled [iso] enable static IPAM for HookOS (default "false")
-iso-url [iso] an ISO source URL target for patching
-otel-endpoint [otel] OpenTelemetry collector endpoint
-otel-insecure [otel] OpenTelemetry collector insecure (default "true")
-syslog-addr [syslog] local IP to listen on for Syslog messages (default "%[1]v")
Expand Down
8 changes: 5 additions & 3 deletions cmd/smee/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -142,9 +142,10 @@
}

type isoConfig struct {
enabled bool
url string
magicString string
enabled bool
url string
magicString string
staticIPAMEnabled bool
}

func main() {
Expand Down Expand Up @@ -262,6 +263,7 @@
Syslog: cfg.dhcp.syslogIP,
TinkServerTLS: cfg.ipxeHTTPScript.tinkServerUseTLS,
TinkServerGRPCAddr: cfg.ipxeHTTPScript.tinkServer,
StaticIPAMEnabled: cfg.iso.staticIPAMEnabled,

Check warning on line 266 in cmd/smee/main.go

View check run for this annotation

Codecov / codecov/patch

cmd/smee/main.go#L266

Added line #L266 was not covered by tests
MagicString: func() string {
if cfg.iso.magicString == "" {
return magicString
Expand Down
139 changes: 110 additions & 29 deletions internal/iso/iso.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"net"
"net/http"
"net/http/httputil"
"net/netip"
"net/url"
"path"
"path/filepath"
Expand All @@ -34,21 +35,6 @@
GetByMac(context.Context, net.HardwareAddr) (*data.DHCP, *data.Netboot, error)
}

// HandlerFunc returns a reverse proxy HTTP handler function that performs ISO patching.
func (h *Handler) HandlerFunc() (http.HandlerFunc, error) {
target, err := url.Parse(h.SourceISO)
if err != nil {
return nil, err
}
h.parsedURL = target
proxy := httputil.NewSingleHostReverseProxy(target)

proxy.Transport = h
proxy.FlushInterval = -1

return proxy.ServeHTTP, nil
}

// Handler is a struct that contains the necessary fields to patch an ISO file with
// relevant information for the Tink worker.
type Handler struct {
Expand All @@ -66,11 +52,27 @@
Syslog string
TinkServerTLS bool
TinkServerGRPCAddr string
StaticIPAMEnabled bool
// parsedURL derives a url.URL from the SourceISO field.
// It needed for validation of SourceISO and easier modification.
parsedURL *url.URL
}

// HandlerFunc returns a reverse proxy HTTP handler function that performs ISO patching.
func (h *Handler) HandlerFunc() (http.HandlerFunc, error) {
target, err := url.Parse(h.SourceISO)
if err != nil {
return nil, err
}
h.parsedURL = target
proxy := httputil.NewSingleHostReverseProxy(target)

proxy.Transport = h
proxy.FlushInterval = -1

return proxy.ServeHTTP, nil

Check warning on line 73 in internal/iso/iso.go

View check run for this annotation

Codecov / codecov/patch

internal/iso/iso.go#L62-L73

Added lines #L62 - L73 were not covered by tests
}

// RoundTrip is a method on the Handler struct that implements the http.RoundTripper interface.
// This method is called by the httputil.NewSingleHostReverseProxy to handle the incoming request.
// The method is responsible for validating the incoming request, reading the source ISO, patching the ISO.
Expand Down Expand Up @@ -112,7 +114,7 @@
}, nil
}

f, err := getFacility(req.Context(), ha, h.Backend)
fac, dhcpData, err := h.getFacility(req.Context(), ha, h.Backend)
if err != nil {
log.Info("unable to get the hardware object", "error", err, "mac", ha)
if apierrors.IsNotFound(err) {
Expand Down Expand Up @@ -222,10 +224,10 @@
// historically the facility is used as a way to define consoles on a per Hardware basis.
var consoles string
switch {
case f != "" && strings.Contains(f, "console="):
consoles = fmt.Sprintf("facility=%s", f)
case f != "":
consoles = fmt.Sprintf("facility=%s %s", f, defaultConsoles)
case fac != "" && strings.Contains(fac, "console="):
consoles = fmt.Sprintf("facility=%s", fac)

Check warning on line 228 in internal/iso/iso.go

View check run for this annotation

Codecov / codecov/patch

internal/iso/iso.go#L227-L228

Added lines #L227 - L228 were not covered by tests
case fac != "":
consoles = fmt.Sprintf("facility=%s %s", fac, defaultConsoles)
default:
consoles = defaultConsoles
}
Expand All @@ -240,7 +242,7 @@
dup := make([]byte, len(b))
copy(dup, b)
copy(dup[i:], magicStringPadding)
copy(dup[i:], []byte(h.constructPatch(consoles, ha.String())))
copy(dup[i:], []byte(h.constructPatch(consoles, ha.String(), dhcpData)))
b = dup
}

Expand All @@ -250,13 +252,24 @@
return resp, nil
}

func (h *Handler) constructPatch(console, mac string) string {
func (h *Handler) constructPatch(console, mac string, d *data.DHCP) string {
syslogHost := fmt.Sprintf("syslog_host=%s", h.Syslog)
grpcAuthority := fmt.Sprintf("grpc_authority=%s", h.TinkServerGRPCAddr)
tinkerbellTLS := fmt.Sprintf("tinkerbell_tls=%v", h.TinkServerTLS)
workerID := fmt.Sprintf("worker_id=%s", mac)
vlanID := func() string {
if d != nil && d.VLANID != "" {
return fmt.Sprintf("vlan_id=%s", d.VLANID)
}

Check warning on line 263 in internal/iso/iso.go

View check run for this annotation

Codecov / codecov/patch

internal/iso/iso.go#L262-L263

Added lines #L262 - L263 were not covered by tests
return ""
}()
hwAddr := fmt.Sprintf("hw_addr=%s", mac)
all := []string{strings.Join(h.ExtraKernelParams, " "), console, vlanID, hwAddr, syslogHost, grpcAuthority, tinkerbellTLS, workerID}
if h.StaticIPAMEnabled {
all = append(all, parseIPAM(d))
}

Check warning on line 270 in internal/iso/iso.go

View check run for this annotation

Codecov / codecov/patch

internal/iso/iso.go#L269-L270

Added lines #L269 - L270 were not covered by tests

return strings.Join([]string{strings.Join(h.ExtraKernelParams, " "), console, syslogHost, grpcAuthority, tinkerbellTLS, workerID}, " ")
return strings.Join(all, " ")
}

func getMAC(urlPath string) (net.HardwareAddr, error) {
Expand All @@ -269,18 +282,17 @@
return hw, nil
}

func getFacility(ctx context.Context, mac net.HardwareAddr, br BackendReader) (string, error) {
func (h *Handler) getFacility(ctx context.Context, mac net.HardwareAddr, br BackendReader) (string, *data.DHCP, error) {
if br == nil {
return "", errors.New("backend is nil")
return "", nil, errors.New("backend is nil")
}

// TODO(jacobweinstock): Pass DHCP info to kernel cmdline parameters for static IP assignment.
_, n, err := br.GetByMac(ctx, mac)
d, n, err := br.GetByMac(ctx, mac)
if err != nil {
return "", err
return "", nil, err

Check warning on line 292 in internal/iso/iso.go

View check run for this annotation

Codecov / codecov/patch

internal/iso/iso.go#L292

Added line #L292 was not covered by tests
}

return n.Facility, nil
return n.Facility, d, nil
}

func randomPercentage(precision int64) float64 {
Expand All @@ -291,3 +303,72 @@

return float64(random.Int64()) / float64(precision)
}

func parseIPAM(d *data.DHCP) string {
if d == nil {
return ""
}
// return format is ipam=<mac-address>:<vlan-id>:<ip-address>:<netmask>:<gateway>:<hostname>:<dns>:<search-domains>:<ntp>
ipam := make([]string, 9)
ipam[0] = func() string {
m := d.MACAddress.String()

return strings.ReplaceAll(m, ":", "-")
}()
ipam[1] = func() string {
if d.VLANID != "" {
return d.VLANID
}
return ""
}()
ipam[2] = func() string {
if d.IPAddress.Compare(netip.Addr{}) != 0 {
return d.IPAddress.String()
}
return ""
}()
ipam[3] = func() string {
if d.SubnetMask != nil {
return net.IP(d.SubnetMask).String()
}
return ""
}()
ipam[4] = func() string {
if d.DefaultGateway.Compare(netip.Addr{}) != 0 {
return d.DefaultGateway.String()
}
return ""
}()
ipam[5] = d.Hostname
ipam[6] = func() string {
var nameservers []string
for _, e := range d.NameServers {
nameservers = append(nameservers, e.String())
}
if len(nameservers) > 0 {
return strings.Join(nameservers, ",")
}

return ""
}()
ipam[7] = func() string {
if len(d.DomainSearch) > 0 {
return strings.Join(d.DomainSearch, ",")
}

return ""
}()
ipam[8] = func() string {
var ntp []string
for _, e := range d.NTPServers {
ntp = append(ntp, e.String())
}
if len(ntp) > 0 {
return strings.Join(ntp, ",")
}

return ""
}()

return fmt.Sprintf("ipam=%s", strings.Join(ipam, ":"))
}
51 changes: 45 additions & 6 deletions internal/iso/iso_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"net"
"net/http"
"net/http/httptest"
"net/netip"
"net/url"
"os"
"testing"
Expand Down Expand Up @@ -110,13 +111,13 @@ func TestPatching(t *testing.T) {
// patch the ISO file
// mount the ISO file and check if the magic string was patched

// If anything changes here the space padding will be different. Be sure to update it accordingly.
wantGrubCfg := `set timeout=0
set gfxpayload=text
menuentry 'LinuxKit ISO Image' {
linuxefi /kernel facility=test console=ttyAMA0 console=ttyS0 console=tty0 console=tty1 console=ttyS1 syslog_host=127.0.0.1:514 grpc_authority=127.0.0.1:42113 tinkerbell_tls=false worker_id=de:ed:be:ef:fe:ed text
linuxefi /kernel facility=test console=ttyAMA0 console=ttyS0 console=tty0 console=tty1 console=ttyS1 hw_addr=de:ed:be:ef:fe:ed syslog_host=127.0.0.1:514 grpc_authority=127.0.0.1:42113 tinkerbell_tls=false worker_id=de:ed:be:ef:fe:ed text
initrdefi /initrd.img
}
`
}`
// This expects that testdata/output.iso exists. Run the TestCreateISO test to create it.

// serve it with a http server
Expand All @@ -141,6 +142,8 @@ menuentry 'LinuxKit ISO Image' {
parsedURL: parsedURL,
MagicString: magicString,
}
// for debugging enable a logger
// h.Logger = logr.FromSlogHandler(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{AddSource: true}))

rurl := hs.URL + "/iso/de:ed:be:ef:fe:ed/output.iso"
purl, _ := url.Parse(rurl)
Expand All @@ -164,14 +167,14 @@ menuentry 'LinuxKit ISO Image' {
t.Fatal(err)
}

idx := bytes.Index(isoContents, []byte(wantGrubCfg))
idx := bytes.Index(isoContents, []byte(`set timeout=0`))
if idx == -1 {
t.Fatalf("could not find grub.cfg in the ISO")
t.Fatalf("could not find the expected grub.cfg contents in the ISO")
}
contents := isoContents[idx : idx+len(wantGrubCfg)]

if diff := cmp.Diff(wantGrubCfg, string(contents)); diff != "" {
t.Fatalf("unexpected grub.cfg file: %s", diff)
t.Fatalf("patched grub.cfg contents don't match expected: %v", diff)
}
}

Expand All @@ -192,3 +195,39 @@ func (m *mockBackend) GetByIP(context.Context, net.IP) (*data.DHCP, *data.Netboo
}
return d, n, nil
}

func TestParseIPAM(t *testing.T) {
tests := map[string]struct {
input *data.DHCP
want string
}{
"empty": {},
"only MAC": {
input: &data.DHCP{MACAddress: net.HardwareAddr{0xde, 0xed, 0xbe, 0xef, 0xfe, 0xed}},
want: "ipam=de-ed-be-ef-fe-ed::::::::",
},
"everything": {
input: &data.DHCP{
MACAddress: net.HardwareAddr{0xde, 0xed, 0xbe, 0xef, 0xfe, 0xed},
IPAddress: netip.AddrFrom4([4]byte{127, 0, 0, 1}),
SubnetMask: net.IPv4Mask(255, 255, 255, 0),
DefaultGateway: netip.AddrFrom4([4]byte{127, 0, 0, 2}),
NameServers: []net.IP{{1, 1, 1, 1}, {4, 4, 4, 4}},
Hostname: "myhost",
NTPServers: []net.IP{{129, 6, 15, 28}, {129, 6, 15, 29}},
DomainSearch: []string{"example.com", "example.org"},
VLANID: "400",
},
want: "ipam=de-ed-be-ef-fe-ed:400:127.0.0.1:255.255.255.0:127.0.0.2:myhost:1.1.1.1,4.4.4.4:example.com,example.org:129.6.15.28,129.6.15.29",
},
}

for name, tt := range tests {
t.Run(name, func(t *testing.T) {
got := parseIPAM(tt.input)
if diff := cmp.Diff(tt.want, got); diff != "" {
t.Fatalf("diff: %v", diff)
}
})
}
}
Loading