From 92451cf2e432ca66eadd052d498ca389ea6b6c59 Mon Sep 17 00:00:00 2001 From: Justin Cormack Date: Fri, 18 Aug 2017 20:53:59 +0100 Subject: [PATCH] Add packet metadata support, with bonding Use the packet metadata to set up bonding, also get ssh keys and hostname. This does not yet do anything with disk metadata. Userdata is not used if it has been used for ipxe, but is otherwise available. Signed-off-by: Justin Cormack --- pkg/metadata/Dockerfile | 6 +- pkg/metadata/main.go | 4 +- pkg/metadata/provider_packet.go | 184 ++++++++++++++++++++++++++++++++ 3 files changed, 190 insertions(+), 4 deletions(-) create mode 100644 pkg/metadata/provider_packet.go diff --git a/pkg/metadata/Dockerfile b/pkg/metadata/Dockerfile index 216ee9aca..ce6839c95 100644 --- a/pkg/metadata/Dockerfile +++ b/pkg/metadata/Dockerfile @@ -1,12 +1,12 @@ FROM linuxkit/alpine:87a0cd10449d72f374f950004467737dbf440630 AS mirror -RUN apk add --no-cache go musl-dev +RUN apk add --no-cache go musl-dev linux-headers ENV GOPATH=/go PATH=$PATH:/go/bin COPY . /go/src/metadata/ RUN go-compile.sh /go/src/metadata -RUN mkdir -p out/tmp out/var out/dev out/etc +RUN mkdir -p out/tmp out/var out/dev out/etc out/etc/ssl/certs FROM scratch ENTRYPOINT [] @@ -15,4 +15,4 @@ WORKDIR / COPY --from=mirror /go/bin/metadata /usr/bin/metadata COPY --from=mirror /out/ / CMD ["/usr/bin/metadata"] -LABEL org.mobyproject.config='{"binds": ["/dev:/dev", "/var:/var", "/etc/resolv.conf:/etc/resolv.conf"], "tmpfs": ["/tmp"], "readonly": true, "capabilities": ["CAP_SYS_ADMIN"]}' +LABEL org.mobyproject.config='{"binds": ["/dev:/dev", "/var:/var", "/sys:/sys", "/etc/resolv.conf:/etc/resolv.conf", "/etc/ssl/certs:/etc/ssl/certs"], "tmpfs": ["/tmp"], "readonly": true, "capabilities": ["CAP_SYS_ADMIN", "CAP_NET_ADMIN"]}' diff --git a/pkg/metadata/main.go b/pkg/metadata/main.go index ccf5e09a1..1e1689392 100644 --- a/pkg/metadata/main.go +++ b/pkg/metadata/main.go @@ -41,7 +41,7 @@ var netProviders []Provider var cdromProviders []Provider func main() { - providers := []string{"aws", "gcp", "vultr", "cdrom"} + providers := []string{"aws", "gcp", "vultr", "packet", "cdrom"} if len(os.Args) > 1 { providers = os.Args[1:] } @@ -51,6 +51,8 @@ func main() { netProviders = append(netProviders, NewAWS()) case "gcp": netProviders = append(netProviders, NewGCP()) + case "packet": + netProviders = append(netProviders, NewPacket()) case "vultr": netProviders = append(netProviders, NewVultr()) case "cdrom": diff --git a/pkg/metadata/provider_packet.go b/pkg/metadata/provider_packet.go new file mode 100644 index 000000000..a52a0bc1a --- /dev/null +++ b/pkg/metadata/provider_packet.go @@ -0,0 +1,184 @@ +package main + +import ( + "fmt" + "io/ioutil" + "net" + "os" + "path" + "strconv" + "strings" + + "github.com/packethost/packngo/metadata" + "github.com/vishvananda/netlink" +) + +// ProviderPacket is the type implementing the Provider interface for Packet.net +type ProviderPacket struct { + metadata *metadata.CurrentDevice + err error +} + +// NewPacket returns a new ProviderPacket +func NewPacket() *ProviderPacket { + return &ProviderPacket{} +} + +func (p *ProviderPacket) String() string { + return "Packet" +} + +// Probe checks if we are running on Packet +func (p *ProviderPacket) Probe() bool { + // Unfortunately the host is resolveable globally, so no easy test + p.metadata, p.err = metadata.GetMetadata() + return p.err == nil +} + +// Extract gets both the Packet specific and generic userdata +func (p *ProviderPacket) Extract() ([]byte, error) { + // do not retrieve if we Probed + if p.metadata == nil && p.err == nil { + p.metadata, p.err = metadata.GetMetadata() + if p.err != nil { + return nil, p.err + } + } else if p.err != nil { + return nil, p.err + } + + if err := ioutil.WriteFile(path.Join(ConfigPath, Hostname), []byte(p.metadata.Hostname), 0644); err != nil { + return nil, fmt.Errorf("Packet: Failed to write hostname: %s", err) + } + + if err := os.MkdirAll(path.Join(ConfigPath, SSH), 0755); err != nil { + return nil, fmt.Errorf("Failed to create %s: %s", SSH, err) + } + + sshKeys := strings.Join(p.metadata.SSHKeys, "\n") + + if err := ioutil.WriteFile(path.Join(ConfigPath, SSH, "authorized_keys"), []byte(sshKeys), 0600); err != nil { + return nil, fmt.Errorf("Failed to write ssh keys: %s", err) + } + + if err := networkConfig(p.metadata.Network); err != nil { + return nil, err + } + + userData, err := metadata.GetUserData() + if err != nil { + return nil, fmt.Errorf("Packet: failed to get userdata: %s", err) + } + + if len(userData) == 0 { + return nil, nil + } + + if len(userData) > 6 && string(userData[0:6]) == "#!ipxe" { + // if you use the userdata for ipxe boot, no use as userdata + return nil, nil + } + + return userData, nil +} + +// networkConfig handles Packet network configuration, primarily bonding +func networkConfig(ni metadata.NetworkInfo) error { + // rename interfaces to match what the metadata calls them + links, err := netlink.LinkList() + if err != nil { + return fmt.Errorf("Failed to list links: %v", err) + } + for _, link := range links { + attrs := link.Attrs() + mac := attrs.HardwareAddr.String() + for _, iface := range ni.Interfaces { + if iface.MAC == mac { + // remove existing addresses from link + addresses, err := netlink.AddrList(link, netlink.FAMILY_ALL) + if err != nil { + return fmt.Errorf("Cannot list addresses on interface: %v", err) + } + for _, addr := range addresses { + if err := netlink.AddrDel(link, &addr); err != nil { + return fmt.Errorf("Cannot remove address from interface: %v", err) + } + } + // links need to be down to be bonded + if err := netlink.LinkSetDown(link); err != nil { + return fmt.Errorf("Cannot down link: %v", err) + } + // see if we need to rename interface + if iface.Name != attrs.Name { + if err := netlink.LinkSetName(link, iface.Name); err != nil { + return fmt.Errorf("Interface rename failed: %v", err) + } + } + } + } + } + + // set up bonding + la := netlink.LinkAttrs{Name: "bond0"} + bond := &netlink.GenericLink{la, "bond"} + if err := netlink.LinkAdd(bond); err != nil { + // weirdly creating a bind always seems to return EEXIST + fmt.Fprintf(os.Stderr, "Error adding bond0: %v (ignoring)", err) + } + if err := ioutil.WriteFile("/sys/class/net/bond0/bonding/mode", []byte(strconv.Itoa(int(ni.Bonding.Mode))), 0); err != nil { + return fmt.Errorf("Cannot write to /sys/class/net/bond0/bonding/mode: %v", err) + } + if err := netlink.LinkSetUp(bond); err != nil { + return fmt.Errorf("Failed to bring bond0 up: %v", err) + } + for _, iface := range ni.Interfaces { + link, err := netlink.LinkByName(iface.Name) + if err != nil { + return fmt.Errorf("Cannot find interface %s: %v", iface.Name, err) + } + if err := netlink.LinkSetMasterByIndex(link, bond.Attrs().Index); err != nil { + return fmt.Errorf("Cannot join %s to bond0: %v", iface.Name, err) + } + } + + // set up addresses and routes + for _, address := range ni.Addresses { + cidr := "/" + strconv.Itoa(address.NetworkBits) + addr, err := netlink.ParseAddr(address.Address.String() + cidr) + if err != nil { + return err + } + if err := netlink.AddrAdd(bond, addr); err != nil { + return fmt.Errorf("Failed to add address to bonded interface: %v", err) + } + var additionalNet string + switch { + case address.Public && address.Family == metadata.IPv4: + additionalNet = "0.0.0.0/0" + case address.Public && address.Family == metadata.IPv6: + additionalNet = "::/0" + case !address.Public && address.Family == metadata.IPv4: + // add a gateway for eg 10.0.0.0/8 + mask := address.Address.DefaultMask() + masked := address.Address.Mask(mask) + network := &net.IPNet{IP: masked, Mask: mask} + additionalNet = network.String() + } + if additionalNet != "" { + _, network, err := net.ParseCIDR(additionalNet) + if err != nil { + return err + } + route := netlink.Route{ + LinkIndex: bond.Attrs().Index, + Gw: address.Gateway, + Dst: network, + } + if err := netlink.RouteAdd(&route); err != nil { + return fmt.Errorf("Failed to add route: %v", err) + } + } + } + + return nil +}