Skip to content

Commit

Permalink
main: Support generating the shoelaces mapping file.
Browse files Browse the repository at this point in the history
  • Loading branch information
the-maldridge committed Jan 25, 2023
1 parent 751b815 commit dac1e14
Show file tree
Hide file tree
Showing 5 changed files with 100 additions and 3 deletions.
1 change: 1 addition & 0 deletions Containerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
28 changes: 26 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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.
52 changes: 52 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package main

import (
"encoding/json"
"io"
"log"
"os"
Expand All @@ -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)
Expand Down Expand Up @@ -56,13 +76,15 @@ 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)
os.Exit(1)
}

hosts := make(map[int64]*DHCPHost, len(devices))
shoenets := []ShoelacesHost{}
for _, dev := range devices {
ipaddr := strings.SplitN(dev.PrimaryIPv4.Address, "/", 2)[0]

Expand Down Expand Up @@ -91,11 +113,41 @@ 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 {
if err := hostTmpl.Execute(os.Stdout, host); err != nil {
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)
}
}
}
9 changes: 9 additions & 0 deletions netbox/type.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,22 @@ 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.
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
Expand Down
13 changes: 12 additions & 1 deletion runit/netbox-dnsmasq-dhcp/run
Original file line number Diff line number Diff line change
@@ -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

0 comments on commit dac1e14

Please sign in to comment.