Skip to content
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 cmd/smee/flag.go
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,12 @@ func otelFlags(c *config, fs *flag.FlagSet) {
fs.BoolVar(&c.otel.insecure, "otel-insecure", true, "[otel] OpenTelemetry collector insecure")
}

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")
}

func setFlags(c *config, fs *flag.FlagSet) {
fs.StringVar(&c.logLevel, "log-level", "info", "log level (debug, info)")
dhcpFlags(c, fs)
Expand All @@ -162,6 +168,7 @@ func setFlags(c *config, fs *flag.FlagSet) {
syslogFlags(c, fs)
backendFlags(c, fs)
otelFlags(c, fs)
isoFlags(c, fs)
}

func newCLI(cfg *config, fs *flag.FlagSet) *ffcli.Command {
Expand Down
13 changes: 13 additions & 0 deletions cmd/smee/flag_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,11 @@ func TestParser(t *testing.T) {
injectMacAddress: true,
},
},
iso: isoConfig{
enabled: true,
url: "http://10.10.10.10:8787/hook.iso",
magicString: magicString,
},
logLevel: "info",
backends: dhcpBackends{
file: File{},
Expand All @@ -77,6 +82,9 @@ func TestParser(t *testing.T) {
"-dhcp-tftp-ip", "192.168.2.4",
"-dhcp-http-ipxe-binary-host", "192.168.2.4",
"-dhcp-http-ipxe-script-host", "192.168.2.4",
"-iso-enabled=true",
"-iso-magic-string", magicString,
"-iso-url", "http://10.10.10.10:8787/hook.iso",
}
cli := newCLI(&got, fs)
cli.Parse(args)
Expand All @@ -89,9 +97,11 @@ func TestParser(t *testing.T) {
cmp.AllowUnexported(dhcpConfig{}),
cmp.AllowUnexported(dhcpBackends{}),
cmp.AllowUnexported(httpIpxeScript{}),
cmp.AllowUnexported(isoConfig{}),
cmp.AllowUnexported(otelConfig{}),
cmp.AllowUnexported(urlBuilder{}),
}

if diff := cmp.Diff(want, got, opts); diff != "" {
t.Fatal(diff)
}
Expand Down Expand Up @@ -143,6 +153,9 @@ 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
-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
38 changes: 38 additions & 0 deletions cmd/smee/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
"github.com/tinkerbell/smee/internal/dhcp/server"
"github.com/tinkerbell/smee/internal/ipxe/http"
"github.com/tinkerbell/smee/internal/ipxe/script"
"github.com/tinkerbell/smee/internal/iso"
"github.com/tinkerbell/smee/internal/metric"
"github.com/tinkerbell/smee/internal/otel"
"github.com/tinkerbell/smee/internal/syslog"
Expand All @@ -47,6 +48,9 @@
dhcpModeProxy dhcpMode = "proxy"
dhcpModeReservation dhcpMode = "reservation"
dhcpModeAutoProxy dhcpMode = "auto-proxy"
// magicString comes from the HookOS repo
// ref: https://github.com/tinkerbell/hook/blob/main/linuxkit-templates/hook.template.yaml
magicString = `464vn90e7rbj08xbwdjejmdf4it17c5zfzjyfhthbh19eij201hjgit021bmpdb9ctrc87x2ymc8e7icu4ffi15x1hah9iyaiz38ckyap8hwx2vt5rm44ixv4hau8iw718q5yd019um5dt2xpqqa2rjtdypzr5v1gun8un110hhwp8cex7pqrh2ivh0ynpm4zkkwc8wcn367zyethzy7q8hzudyeyzx3cgmxqbkh825gcak7kxzjbgjajwizryv7ec1xm2h0hh7pz29qmvtgfjj1vphpgq1zcbiiehv52wrjy9yq473d9t1rvryy6929nk435hfx55du3ih05kn5tju3vijreru1p6knc988d4gfdz28eragvryq5x8aibe5trxd0t6t7jwxkde34v6pj1khmp50k6qqj3nzgcfzabtgqkmeqhdedbvwf3byfdma4nkv3rcxugaj2d0ru30pa2fqadjqrtjnv8bu52xzxv7irbhyvygygxu1nt5z4fh9w1vwbdcmagep26d298zknykf2e88kumt59ab7nq79d8amnhhvbexgh48e8qc61vq2e9qkihzt1twk1ijfgw70nwizai15iqyted2dt9gfmf2gg7amzufre79hwqkddc1cd935ywacnkrnak6r7xzcz7zbmq3kt04u2hg1iuupid8rt4nyrju51e6uejb2ruu36g9aibmz3hnmvazptu8x5tyxk820g2cdpxjdij766bt2n3djur7v623a2v44juyfgz80ekgfb9hkibpxh3zgknw8a34t4jifhf116x15cei9hwch0fye3xyq0acuym8uhitu5evc4rag3ui0fny3qg4kju7zkfyy8hwh537urd5uixkzwu5bdvafz4jmv7imypj543xg5em8jk8cgk7c4504xdd5e4e71ihaumt6u5u2t1w7um92fepzae8p0vq93wdrd1756npu1pziiur1payc7kmdwyxg3hj5n4phxbc29x0tcddamjrwt260b0w`
)

type config struct {
Expand All @@ -55,6 +59,7 @@
ipxeHTTPBinary ipxeHTTPBinary
ipxeHTTPScript ipxeHTTPScript
dhcp dhcpConfig
iso isoConfig

// loglevel is the log level for smee.
logLevel string
Expand Down Expand Up @@ -137,6 +142,12 @@
insecure bool
}

type isoConfig struct {
enabled bool
url string
magicString string
}

func main() {
cfg := &config{}
cli := newCLI(cfg, flag.NewFlagSet(name, flag.ExitOnError))
Expand Down Expand Up @@ -239,6 +250,33 @@
handlers["/"] = jh.HandlerFunc()
}

if cfg.iso.enabled {
br, err := cfg.backend(ctx, log)
if err != nil {
panic(fmt.Errorf("failed to create backend: %w", err))

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

View check run for this annotation

Codecov / codecov/patch

cmd/smee/main.go#L253-L256

Added lines #L253 - L256 were not covered by tests
}
ih := iso.Handler{
Logger: log,
Backend: br,
SourceISO: cfg.iso.url,
ExtraKernelParams: strings.Split(cfg.ipxeHTTPScript.extraKernelArgs, " "),
Syslog: cfg.dhcp.syslogIP,
TinkServerTLS: cfg.ipxeHTTPScript.tinkServerUseTLS,
TinkServerGRPCAddr: cfg.ipxeHTTPScript.tinkServer,
MagicString: func() string {
if cfg.iso.magicString == "" {
return magicString
}
return cfg.iso.magicString

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

View check run for this annotation

Codecov / codecov/patch

cmd/smee/main.go#L258-L270

Added lines #L258 - L270 were not covered by tests
}(),
}
isoHandler, err := ih.Reverse()
if err != nil {
panic(fmt.Errorf("failed to create iso handler: %w", err))

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

View check run for this annotation

Codecov / codecov/patch

cmd/smee/main.go#L273-L275

Added lines #L273 - L275 were not covered by tests
}
handlers["/iso/"] = isoHandler

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

View check run for this annotation

Codecov / codecov/patch

cmd/smee/main.go#L277

Added line #L277 was not covered by tests
}

if len(handlers) > 0 {
// start the http server for ipxe binaries and scripts
tp := parseTrustedProxies(cfg.ipxeHTTPScript.trustedProxies)
Expand Down
15 changes: 15 additions & 0 deletions internal/backend/kube/error.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,22 @@
package kube

import (
"net/http"

metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

type hardwareNotFoundError struct{}

func (hardwareNotFoundError) NotFound() bool { return true }

func (hardwareNotFoundError) Error() string { return "hardware not found" }

// Status() implements the APIStatus interface from apimachinery/pkg/api/errors
// so that IsNotFound function could be used against this error type.
func (hardwareNotFoundError) Status() metav1.Status {
return metav1.Status{
Reason: metav1.StatusReasonNotFound,
Code: http.StatusNotFound,
}

Check warning on line 21 in internal/backend/kube/error.go

View check run for this annotation

Codecov / codecov/patch

internal/backend/kube/error.go#L17-L21

Added lines #L17 - L21 were not covered by tests
}
226 changes: 226 additions & 0 deletions internal/iso/iso.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
package iso

import (
"bytes"
"context"
"errors"
"fmt"
"io"
"net"
"net/http"
"net/http/httputil"
"net/url"
"path"
"path/filepath"
"strings"

apierrors "k8s.io/apimachinery/pkg/api/errors"

"github.com/go-logr/logr"
"github.com/tinkerbell/smee/internal/dhcp/handler"
)

const (
defaultConsoles = "console=ttyS1 console=ttyS1 console=ttyS0 console=ttyAMA0 console=ttyS1 console=tty0"
)

type Handler struct {
Logger logr.Logger
Backend handler.BackendReader
// SourceISO is the source url where the unmodified iso lives
// patch this at runtime, should be a HTTP(S) url.
SourceISO string
ExtraKernelParams []string
Syslog string
TinkServerTLS bool
TinkServerGRPCAddr string
// parsedURL derives a url.URL from the SourceISO
// It helps accessing different parts of URL
parsedURL *url.URL
// MagicString is the string pattern that will be matched
// in the source iso before patching. The field can be set
// during build time by setting this field.
// Ref: https://github.com/tinkerbell/hook/blob/main/linuxkit-templates/hook.template.yaml
MagicString string
}

func (h *Handler) RoundTrip(req *http.Request) (*http.Response, error) {
h.Logger.V(1).Info("entered the roundtrip func")
if req.Method != http.MethodHead && req.Method != http.MethodGet {
return &http.Response{
Status: fmt.Sprintf("%d %s", http.StatusNotImplemented, http.StatusText(http.StatusNotImplemented)),
StatusCode: http.StatusNotImplemented,
Body: http.NoBody,
Request: req,
}, nil
}

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

View check run for this annotation

Codecov / codecov/patch

internal/iso/iso.go#L50-L56

Added lines #L50 - L56 were not covered by tests

if filepath.Ext(req.URL.Path) != ".iso" {
h.Logger.Info("Extension not supported, only supported type is '.iso'", "path", req.URL.Path)
return &http.Response{
Status: fmt.Sprintf("%d %s", http.StatusNotFound, http.StatusText(http.StatusNotFound)),
StatusCode: http.StatusNotFound,
Body: http.NoBody,
Request: req,
}, nil
}

ctx := req.Context()
// The incoming request url is expected to have the mac address present.
// Fetch the mac and validate if there's a hardware object
// associated with the mac.
//
// We serve the iso only if this validation passes.
ha, err := getMAC(req.URL.Path)
if err != nil {
h.Logger.Info("unable to get the mac address", "error", err)
return &http.Response{
Status: "400 BAD REQUEST",
StatusCode: http.StatusBadRequest,
Body: http.NoBody,
Request: req,
}, nil
}

f, err := getFacility(ctx, ha, h.Backend)
if err != nil {
h.Logger.V(1).Info("unable to get facility", "mac", ha, "error", err)
if apierrors.IsNotFound(err) {
return &http.Response{
Status: fmt.Sprintf("%d %s", http.StatusNotFound, http.StatusText(http.StatusNotFound)),
StatusCode: http.StatusNotFound,
Body: http.NoBody,
Request: req,
}, nil
}

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

View check run for this annotation

Codecov / codecov/patch

internal/iso/iso.go#L89-L95

Added lines #L89 - L95 were not covered by tests
return &http.Response{
Status: fmt.Sprintf("%d %s", http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError)),
StatusCode: http.StatusInternalServerError,
Body: http.NoBody,
Request: req,
}, nil
}

// The hardware object doesn't contain a specific field for consoles
// right now facility is used instead.
var consoles string
switch {
case f != "" && strings.Contains(f, "console="):
consoles = f
case f != "":
consoles = fmt.Sprintf("%s %s", f, defaultConsoles)
default:
consoles = defaultConsoles

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

View check run for this annotation

Codecov / codecov/patch

internal/iso/iso.go#L106-L113

Added lines #L106 - L113 were not covered by tests
}

// Reverse Proxy modifies the request url to
// the same path it received the incoming request.
// mac-id is added to the url path to do hardware lookups using the backend reader
// and is not used when making http calls to the source url.
req.URL.Path = h.parsedURL.Path

// RoundTripper needs a Transport to execute a HTTP transaction
// For our use case the default transport will suffice.
resp, err := http.DefaultTransport.RoundTrip(req)
// resp, err := h.RoundTripper.RoundTrip(req)
if err != nil {
h.Logger.Info("HTTP request didn't receive a response", "sourceIso", h.SourceISO, "error", err)
return nil, err
}

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

View check run for this annotation

Codecov / codecov/patch

internal/iso/iso.go#L120-L129

Added lines #L120 - L129 were not covered by tests

if req.Method == http.MethodHead {
// Fuse client typically make a HEAD request before they start requesting content.
h.Logger.V(1).Info("HTTP HEAD request received, patching only occurs on 206 requests")
return resp, nil
}

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

View check run for this annotation

Codecov / codecov/patch

internal/iso/iso.go#L131-L135

Added lines #L131 - L135 were not covered by tests

// roundtripper should only return error when no response from the server
// for any other case just log the error and return 404 response
if resp.StatusCode == http.StatusPartialContent {
b, err := io.ReadAll(resp.Body)
if err != nil {
h.Logger.Error(err, "reading response bytes", "response", resp.Body)
return &http.Response{
Status: fmt.Sprintf("%d %s", http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError)),
StatusCode: http.StatusInternalServerError,
Body: http.NoBody,
Request: req,
}, nil
}
if err := resp.Body.Close(); err != nil {
h.Logger.Error(err, "closing response body", "response", resp.Body)
return &http.Response{
Status: fmt.Sprintf("%d %s", http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError)),
StatusCode: http.StatusInternalServerError,
Body: http.NoBody,
Request: req,
}, nil
}

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

View check run for this annotation

Codecov / codecov/patch

internal/iso/iso.go#L139-L158

Added lines #L139 - L158 were not covered by tests

magicStringPadding := bytes.Repeat([]byte{' '}, len(h.MagicString))

// TODO: revisit later to handle the magic string potentially being spread across two chunks.
// In current implementation we will never patch the above case. Add logic to patch the case of
// magic string spread across multiple response bodies in the future.
i := bytes.Index(b, []byte(h.MagicString))
if i != -1 {
h.Logger.Info("Magic string found, patching the iso at runtime")
dup := make([]byte, len(b))
copy(dup, b)
copy(dup[i:], magicStringPadding)
copy(dup[i:], []byte(h.constructPatch(fmt.Sprintf("facility=%s", consoles), ha.String())))
b = dup
}

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

View check run for this annotation

Codecov / codecov/patch

internal/iso/iso.go#L160-L173

Added lines #L160 - L173 were not covered by tests

resp.Body = io.NopCloser(bytes.NewReader(b))

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

View check run for this annotation

Codecov / codecov/patch

internal/iso/iso.go#L175

Added line #L175 was not covered by tests
}

h.Logger.Info("roundtrip complete")
return resp, nil

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

View check run for this annotation

Codecov / codecov/patch

internal/iso/iso.go#L178-L179

Added lines #L178 - L179 were not covered by tests
}

func (h *Handler) Reverse() (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 193 in internal/iso/iso.go

View check run for this annotation

Codecov / codecov/patch

internal/iso/iso.go#L182-L193

Added lines #L182 - L193 were not covered by tests
}

func (h *Handler) constructPatch(console, mac string) 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)

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

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

View check run for this annotation

Codecov / codecov/patch

internal/iso/iso.go#L196-L202

Added lines #L196 - L202 were not covered by tests
}

func getMAC(urlPath string) (net.HardwareAddr, error) {
mac := path.Base(path.Dir(urlPath))
hw, err := net.ParseMAC(mac)
if err != nil {
return nil, fmt.Errorf("failed to parse URL path: %s , the second to last element in the URL path must be a valid mac address, err: %w", urlPath, err)
}

return hw, nil
}

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

_, n, err := br.GetByMac(ctx, mac)
if err != nil {
return "", err
}

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

View check run for this annotation

Codecov / codecov/patch

internal/iso/iso.go#L220-L223

Added lines #L220 - L223 were not covered by tests

return n.Facility, nil

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

View check run for this annotation

Codecov / codecov/patch

internal/iso/iso.go#L225

Added line #L225 was not covered by tests
}
Loading
Loading