From 0175778e8338921e933916cf4ca335adffa91ecf Mon Sep 17 00:00:00 2001 From: Anil Madhavapeddy Date: Wed, 12 Apr 2017 12:48:09 +0100 Subject: [PATCH] Add `moby run packet` to boot on baremetal Packet.net hosts This uses the Packet.net API and iPXE to boot a Moby host. There are several enhancements coming soon, such as SSH key customisation, but this PR is sufficient to boot a host and then use the web interface to get console access. The user must currently upload the built artefacts to a public URL and specify it via --base-url, e.g.: moby run packet --api-key --project-id \ --base-url http://recoil.org/~avsm/ipxe --hostname test-moby packet See #1424 #1245 for related issues. Signed-off-by: Anil Madhavapeddy --- src/cmd/moby/run.go | 3 + src/cmd/moby/run_packet.go | 113 +++++++++++++++++++++++++++++++++++++ 2 files changed, 116 insertions(+) create mode 100644 src/cmd/moby/run_packet.go diff --git a/src/cmd/moby/run.go b/src/cmd/moby/run.go index dc5a48882..d0564b81a 100644 --- a/src/cmd/moby/run.go +++ b/src/cmd/moby/run.go @@ -18,6 +18,7 @@ func runUsage() { fmt.Printf(" hyperkit [macOS]\n") fmt.Printf(" qemu [linux]\n") fmt.Printf(" vmware\n") + fmt.Printf(" packet\n") fmt.Printf("\n") fmt.Printf("'options' are the backend specific options.\n") fmt.Printf("See 'moby run [backend] --help' for details.\n\n") @@ -43,6 +44,8 @@ func run(args []string) { runGcp(args[1:]) case "qemu": runQemu(args[1:]) + case "packet": + runPacket(args[1:]) default: switch runtime.GOOS { case "darwin": diff --git a/src/cmd/moby/run_packet.go b/src/cmd/moby/run_packet.go new file mode 100644 index 000000000..080b4821d --- /dev/null +++ b/src/cmd/moby/run_packet.go @@ -0,0 +1,113 @@ +package main + +import ( + "encoding/json" + "flag" + "fmt" + log "github.com/Sirupsen/logrus" + "github.com/packethost/packngo" + "net/http" + "os" +) + +const ( + packetDefaultZone = "ams1" + packetDefaultMachine = "baremetal_0" + packetDefaultHostname = "moby" + packetBaseURL = "PACKET_BASE_URL" + packetZoneVar = "PACKET_ZONE" + packetMachineVar = "PACKET_MACHINE" + packetAPIKeyVar = "PACKET_API_KEY" + packetProjectIDVar = "PACKET_PROJECT_ID" + packetHostnameVar = "PACKET_HOSTNAME" + packetNameVar = "PACKET_NAME" +) + +// ValidateHTTPURL does a sanity check that a URL returns a 200 or 300 response +func ValidateHTTPURL(url string) { + log.Printf("Validating URL: %s", url) + resp, err := http.Head(url) + if err != nil { + log.Fatal(err) + } + if resp.StatusCode >= 400 { + log.Fatal("Got a non 200- or 300- HTTP response code: %s", resp) + } + log.Printf("OK: %d response code", resp.StatusCode) +} + +// Process the run arguments and execute run +func runPacket(args []string) { + packetCmd := flag.NewFlagSet("packet", flag.ExitOnError) + packetCmd.Usage = func() { + fmt.Printf("USAGE: %s run packet [options] [name]\n\n", os.Args[0]) + fmt.Printf("Options:\n\n") + packetCmd.PrintDefaults() + } + baseURLFlag := packetCmd.String("base-url", "", "Base URL that the kernel and initrd are served from.") + zoneFlag := packetCmd.String("zone", packetDefaultZone, "Packet Zone") + machineFlag := packetCmd.String("machine", packetDefaultMachine, "Packet Machine Type") + apiKeyFlag := packetCmd.String("api-key", "", "Packet API key") + projectFlag := packetCmd.String("project-id", "", "Packet Project ID") + hostNameFlag := packetCmd.String("hostname", packetDefaultHostname, "Hostname of new instance") + nameFlag := packetCmd.String("img-name", "", "Overrides the prefix used to identify the files. Defaults to [name]") + if err := packetCmd.Parse(args); err != nil { + log.Fatal("Unable to parse args") + } + + remArgs := packetCmd.Args() + prefix := "packet" + if len(remArgs) > 0 { + prefix = remArgs[0] + } + + url := getStringValue(packetBaseURL, *baseURLFlag, "") + if url == "" { + log.Fatal("Need to specify a value for --base-url where the images are hosted. This URL should contain /%s-bzImage and /%s-initrd.img") + } + facility := getStringValue(packetZoneVar, *zoneFlag, "") + plan := getStringValue(packetMachineVar, *machineFlag, defaultMachine) + apiKey := getStringValue(packetAPIKeyVar, *apiKeyFlag, "") + if apiKey == "" { + log.Fatal("Must specify a Packet.net API key with --api-key") + } + projectID := getStringValue(packetProjectIDVar, *projectFlag, "") + if projectID == "" { + log.Fatal("Must specify a Packet.net Project ID with --project-id") + } + hostname := getStringValue(packetHostnameVar, *hostNameFlag, "") + name := getStringValue(packetNameVar, *nameFlag, prefix) + osType := "custom_ipxe" + billing := "hourly" + userData := fmt.Sprintf("#!ipxe\n\ndhcp\nset base-url %s\nset kernel-params ip=dhcp nomodeset ro serial console=ttyS1,115200\nkernel ${base-url}/%s-bzImage ${kernel-params}\ninitrd ${base-url}/%s-initrd.img\nboot", url, name, name) + log.Debugf("Using userData of:\n%s\n", userData) + initrdURL := fmt.Sprintf("%s/%s-initrd.img", url, name) + kernelURL := fmt.Sprintf("%s/%s-bzImage", url, name) + ValidateHTTPURL(kernelURL) + ValidateHTTPURL(initrdURL) + client := packngo.NewClient("", apiKey, nil) + tags := []string{} + req := packngo.DeviceCreateRequest{ + HostName: hostname, + Plan: plan, + Facility: facility, + OS: osType, + BillingCycle: billing, + ProjectID: projectID, + UserData: userData, + Tags: tags, + } + d, _, err := client.Devices.Create(&req) + if err != nil { + log.Fatal(err) + } + b, err := json.MarshalIndent(d, "", " ") + if err != nil { + log.Fatal(err) + } + // log response json if in verbose mode + log.Debugf("%s\n", string(b)) + // TODO: poll events api for bringup (requires extpacknogo) + // TODO: connect to serial console (requires API extension to get SSH URI) + // TODO: add ssh keys via API registered keys +}