From dac1e1403f18147cb737d3b5a32943edc8bcbf6c Mon Sep 17 00:00:00 2001 From: Michael Aldridge Date: Wed, 25 Jan 2023 13:25:17 -0600 Subject: [PATCH] main: Support generating the shoelaces mapping file. --- Containerfile | 1 + README.md | 28 +++++++++++++++++-- main.go | 52 +++++++++++++++++++++++++++++++++++ netbox/type.go | 9 ++++++ runit/netbox-dnsmasq-dhcp/run | 13 ++++++++- 5 files changed, 100 insertions(+), 3 deletions(-) diff --git a/Containerfile b/Containerfile index e5beab1..2fe15b4 100644 --- a/Containerfile +++ b/Containerfile @@ -33,6 +33,7 @@ RUN apk add git && \ FROM base as shoelaces WORKDIR / +ENV SHOELACES_MAPFILE=/var/lib/shoelaces/mappings.yaml COPY --from=shoelaces_build /shoelaces/shoelaces /usr/local/bin/shoelaces COPY --from=shoelaces_build /shoelaces/web /usr/share/shoelaces/web COPY runit/shoelaces /etc/service/shoelaces diff --git a/README.md b/README.md index 02d7f03..9769749 100644 --- a/README.md +++ b/README.md @@ -19,9 +19,15 @@ in the environment: * `DNSMASQ_TEMPLATE` - A go template expression for the dhcp-hosts file. Defaults to a suitable configuration for IPv4. The default template is `{{JoinStrings .HWAddr ","}},{{.Addr}}`. + * `SHOELACES_MAPFILE` - A file to write out shoelaces mappings too. + Must be named `mappings.yaml` and at the path expected by + shoelaces. Only relevant in images that contain shoelaces. + * `SHOELACES_TAG_PREFIX` - A prefix that if found on a tag will be + used to form the mapping for shoelaces to supply the correct boot + files to the machine without human interaction. The search through netbox by default pulls hosts that have the -`pxe-enable` tag set. Hosts are then filtered to ensure they hae a +$NETBOX_TAG tag set. Hosts are then filtered to ensure they have a primary IPv4 address and at least one interface that has a MAC address set. Hosts that have the correct tag but do not have at least one MAC address associated with a non-management-only interface and a primary @@ -37,7 +43,6 @@ install time, it is more likely that only one interface will be brought up, but it is still prefered that the machine get its final address. - ## Configuring dnsmasq You will need to supply a suitable dnsmasq config file that points to @@ -71,3 +76,22 @@ In this example the server hosting the PXE services is located at dnsmasq not to provide services to hosts that do not have a pre-existing reservation, which can be useful if your network does not use DHCP except for machine installation. + +## Using with Shoelaces + +Shoelaces is a powerful ipxe mapping tool that can mux different boot +images to different machines by hand or by address. Each build of the +containers from this repo comes in a non-shoelaces flavor and a +shoelaces flavor. Using shoelaces will provide a web interface on +port 8081 that allows you to manually select ipxe scripts. Shoelaces +expects to find your template scripts in `/var/lib/shoelaces/ipxe` and +can serve static assets for you from `/var/lib/shoelaces/static`. + +Shoelaces is also capable of directly mapping a machine to its boot +scripts based on tags in netbox. To use this behavior configure the +value of `SHOELACES_TAG_PREFIX` to the prefix used by the tags you +have set in Netbox. For example, if you wanted machines that posess +the tags `pxe-zerotouch-deb11` to boot with the ipxe script +`deb11.ipxe`, you would configure the tag to be `pxe-zerotouch-`. Only +the first tag found will be mapped, so its a good idea to carefully +plan the tag structure in use for these tags to be unique. diff --git a/main.go b/main.go index 95d415a..68fcb11 100644 --- a/main.go +++ b/main.go @@ -1,6 +1,7 @@ package main import ( + "encoding/json" "io" "log" "os" @@ -19,6 +20,25 @@ type DHCPHost struct { Addr string } +// ShoelacesHost is a host that can be mapped without user input in +// shoelaces. +type ShoelacesHost struct { + Network string `json:"network"` + Script ShoeScript `json:"script"` +} + +// ShoeScript is the nested script type for shoelaces. It is only +// supported to specify by name. +type ShoeScript struct { + Name string `json:"name"` +} + +// ShoelacesNetworkMap matches the structure that shoelaces expects to +// be able to read in. +type ShoelacesNetworkMap struct { + NetworkMaps []ShoelacesHost `json:"networkMaps"` +} + func main() { if _, verbose := os.LookupEnv("VERBOSE"); !verbose { log.SetOutput(io.Discard) @@ -56,6 +76,7 @@ func main() { site := os.Getenv("NETBOX_SITE") tag := os.Getenv("NETBOX_TAG") + shoetag := os.Getenv("SHOELACES_TAG_PREFIX") devices, err := nb.ListDevices(site, tag) if err != nil { log.Println("Error listing devices:", err) @@ -63,6 +84,7 @@ func main() { } hosts := make(map[int64]*DHCPHost, len(devices)) + shoenets := []ShoelacesHost{} for _, dev := range devices { ipaddr := strings.SplitN(dev.PrimaryIPv4.Address, "/", 2)[0] @@ -91,6 +113,23 @@ func main() { Addr: ipaddr, HWAddr: hwaddrs, } + + // Construct the shoelaces mapping if enabled. + if shoetag != "" { + for _, tag := range dev.Tags { + if strings.HasPrefix(tag.Slug, shoetag) { + // Map this script for this host's IPs + shoehost := ShoelacesHost{ + Network: ipaddr + "/32", + Script: ShoeScript{ + Name: strings.TrimPrefix(tag.Slug, shoetag) + ".ipxe", + }, + } + shoenets = append(shoenets, shoehost) + break + } + } + } } for _, host := range hosts { @@ -98,4 +137,17 @@ func main() { log.Println("Error executing template", err) } } + + if os.Getenv("SHOELACES_MAPFILE") != "" { + bytes, err := json.Marshal(ShoelacesNetworkMap{NetworkMaps: shoenets}) + if err != nil { + log.Println("Error marshalling shoelaces mappings", err) + os.Exit(1) + } + + if err := os.WriteFile(os.Getenv("SHOELACES_MAPFILE"), bytes, 0644); err != nil { + log.Println("Error writing shoelaces mappings", err) + os.Exit(1) + } + } } diff --git a/netbox/type.go b/netbox/type.go index 444086c..49ea3c0 100644 --- a/netbox/type.go +++ b/netbox/type.go @@ -6,6 +6,14 @@ type Address struct { Address string } +// Tag is a small associated label that is used to group machines. +// This is used by the shoelaces mapper to determine what target +// should be mapped for the particular machine. +type Tag struct { + Name string + Slug string +} + // Device represents the minimum information we wish to retreive from // the netbox devices API as opposed to using the full fat OpenAPI // client. @@ -13,6 +21,7 @@ type Device struct { ID int64 Name string `json:"name"` PrimaryIPv4 Address `json:"primary_ip4"` + Tags []Tag `json:"tags"` } // Interface represents the minimum informatino we wish to retreive diff --git a/runit/netbox-dnsmasq-dhcp/run b/runit/netbox-dnsmasq-dhcp/run index c03c263..8a6b023 100755 --- a/runit/netbox-dnsmasq-dhcp/run +++ b/runit/netbox-dnsmasq-dhcp/run @@ -1,6 +1,17 @@ #!/bin/sh while true ; do - /usr/local/bin/netbox-dhcp-hosts > /run/dhcp-hosts && pkill -SIGHUP dnsmasq + if /usr/local/bin/netbox-dhcp-hosts | sort > /run/dhcp-hosts.next ; then + if ! diff /run/dhcp-hosts /run/dhcp-hosts.next ; then + echo "Updated host mappings, reloading services" + mv /run/dhcp-hosts.next /run/dhcp-hosts + + pkill -SIGHUP dnsmasq + + if [ -n "$SHOELACES_TAG_PREFIX" ] ; then + sv restart shoelaces + fi + fi + fi sleep 600 done