mirror of
https://github.com/linuxkit/linuxkit.git
synced 2025-07-20 09:39:08 +00:00
Merge pull request #2408 from rn/p3
Add a HTTP server to packet.net to serve iPXE files
This commit is contained in:
commit
afef11d4f5
@ -24,47 +24,25 @@ packet --help` for the options and environment variables.
|
|||||||
|
|
||||||
## Boot
|
## Boot
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
LinuxKit on Packet boots the `kernel+initrd` output from moby
|
LinuxKit on Packet boots the `kernel+initrd` output from moby
|
||||||
via
|
via
|
||||||
[iPXE](https://help.packet.net/technical/infrastructure/custom-ipxe). iPXE
|
[iPXE](https://help.packet.net/technical/infrastructure/custom-ipxe). iPXE
|
||||||
booting requires a HTTP server on which you can store your images. At
|
booting requires a HTTP server on which you can store your images. The
|
||||||
the moment there is no builtin support for this, although we are
|
`-base-url` option specifies the URL to the HTTP server.
|
||||||
working on this too.
|
|
||||||
|
|
||||||
A simple way to host files is via a simple local http server written in Go, e.g.:
|
If you don't have a public HTTP server at hand, you can use the
|
||||||
|
`-serve` option. This will create a local HTTP server which can either
|
||||||
|
be run on another Packet machine or be made accessible with tools
|
||||||
|
like [ngrok](https://ngrok.com/).
|
||||||
|
|
||||||
```Go
|
For example, to boot the toplevel [linuxkit.yml](../linuxkit.yml)
|
||||||
package main
|
example with a local HTTP server:
|
||||||
|
|
||||||
import (
|
|
||||||
"log"
|
|
||||||
"net/http"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
// Simple static webserver:
|
|
||||||
log.Fatal(http.ListenAndServe(":8080", http.FileServer(http.Dir("."))))
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
and then `go run` this in the directory with the `kernel+initrd`.
|
|
||||||
|
|
||||||
The web server must be accessible from Packet. You can either run the
|
|
||||||
server on another Packet machine, or use tools
|
|
||||||
like [ngrok](https://ngrok.com/) to make it accessible via a reverse
|
|
||||||
proxy.
|
|
||||||
|
|
||||||
You then specify the location of your http server using the
|
|
||||||
`-base-url` command line option. For example, to boot the
|
|
||||||
toplevel [linuxkit.yml](../linuxkit.yml) example:
|
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
moby build linuxkit.yml
|
moby build linuxkit.yml
|
||||||
# run the web server
|
# run the web server
|
||||||
# run 'ngrok http 8080' in another window
|
# run 'ngrok http 8080' in another window
|
||||||
PACKET_API_KEY=<API key> linuxkit run packet -base-url http://9b828514.ngrok.io -project-id <Project ID> linuxkit
|
PACKET_API_KEY=<API key> linuxkit run packet -serve :8080 -base-url http://9b828514.ngrok.io -project-id <Project ID> linuxkit
|
||||||
```
|
```
|
||||||
|
|
||||||
**Note**: It may take several minutes to deploy a new server. If you
|
**Note**: It may take several minutes to deploy a new server. If you
|
||||||
|
@ -2,6 +2,7 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
@ -9,11 +10,14 @@ import (
|
|||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
"os/signal"
|
||||||
"os/user"
|
"os/user"
|
||||||
|
"path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/bzub/packngo" // TODO(rn): Update to official once iPXE is merged
|
"github.com/packethost/packngo"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
"golang.org/x/crypto/ssh"
|
"golang.org/x/crypto/ssh"
|
||||||
"golang.org/x/crypto/ssh/agent"
|
"golang.org/x/crypto/ssh/agent"
|
||||||
@ -60,7 +64,9 @@ func runPacket(args []string) {
|
|||||||
hostNameFlag := flags.String("hostname", packetDefaultHostname, "Hostname of new instance (or "+packetHostnameVar+")")
|
hostNameFlag := flags.String("hostname", packetDefaultHostname, "Hostname of new instance (or "+packetHostnameVar+")")
|
||||||
nameFlag := flags.String("img-name", "", "Overrides the prefix used to identify the files. Defaults to [name] (or "+packetNameVar+")")
|
nameFlag := flags.String("img-name", "", "Overrides the prefix used to identify the files. Defaults to [name] (or "+packetNameVar+")")
|
||||||
alwaysPXE := flags.Bool("always-pxe", true, "Reboot from PXE every time.")
|
alwaysPXE := flags.Bool("always-pxe", true, "Reboot from PXE every time.")
|
||||||
|
serveFlag := flags.String("serve", "", "Serve local files via the http port specified, e.g. ':8080'.")
|
||||||
consoleFlag := flags.Bool("console", true, "Provide interactive access on the console.")
|
consoleFlag := flags.Bool("console", true, "Provide interactive access on the console.")
|
||||||
|
keepFlag := flags.Bool("keep", false, "Keep the machine after exiting/poweroff.")
|
||||||
if err := flags.Parse(args); err != nil {
|
if err := flags.Parse(args); err != nil {
|
||||||
log.Fatal("Unable to parse args")
|
log.Fatal("Unable to parse args")
|
||||||
}
|
}
|
||||||
@ -90,6 +96,10 @@ func runPacket(args []string) {
|
|||||||
osType := "custom_ipxe"
|
osType := "custom_ipxe"
|
||||||
billing := "hourly"
|
billing := "hourly"
|
||||||
|
|
||||||
|
if !*keepFlag && !*consoleFlag {
|
||||||
|
log.Fatalf("Combination of keep=%t and console=%t makes little sense", *keepFlag, *consoleFlag)
|
||||||
|
}
|
||||||
|
|
||||||
// Read kernel command line
|
// Read kernel command line
|
||||||
var cmdline string
|
var cmdline string
|
||||||
if c, err := ioutil.ReadFile(prefix + "-cmdline"); err != nil {
|
if c, err := ioutil.ReadFile(prefix + "-cmdline"); err != nil {
|
||||||
@ -98,6 +108,19 @@ func runPacket(args []string) {
|
|||||||
cmdline = string(c)
|
cmdline = string(c)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Serve files with a local http server
|
||||||
|
var httpServer *http.Server
|
||||||
|
if *serveFlag != "" {
|
||||||
|
fs := serveFiles{[]string{fmt.Sprintf("%s-kernel", name), fmt.Sprintf("%s-initrd.img", name)}}
|
||||||
|
httpServer = &http.Server{Addr: ":8080", Handler: http.FileServer(fs)}
|
||||||
|
go func() {
|
||||||
|
log.Debugf("Listening on http://%s\n", *serveFlag)
|
||||||
|
if err := httpServer.ListenAndServe(); err != nil {
|
||||||
|
log.Infof("http server exited with: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
// Build the iPXE script
|
// Build the iPXE script
|
||||||
// Note, we *append* the <prefix>-cmdline. iXPE booting will
|
// Note, we *append* the <prefix>-cmdline. iXPE booting will
|
||||||
// need the first set of "kernel-params" and we don't want to
|
// need the first set of "kernel-params" and we don't want to
|
||||||
@ -130,32 +153,56 @@ func runPacket(args []string) {
|
|||||||
Tags: tags,
|
Tags: tags,
|
||||||
AlwaysPXE: *alwaysPXE,
|
AlwaysPXE: *alwaysPXE,
|
||||||
}
|
}
|
||||||
d, _, err := client.Devices.Create(&req)
|
dev, _, err := client.Devices.Create(&req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
b, err := json.MarshalIndent(d, "", " ")
|
b, err := json.MarshalIndent(dev, "", " ")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
// log response json if in verbose mode
|
|
||||||
log.Debugf("%s\n", string(b))
|
log.Debugf("%s\n", string(b))
|
||||||
|
|
||||||
sshHost := "sos." + d.Facility.Code + ".packet.net"
|
log.Printf("Machine booting...")
|
||||||
|
|
||||||
|
sshHost := "sos." + dev.Facility.Code + ".packet.net"
|
||||||
if *consoleFlag {
|
if *consoleFlag {
|
||||||
// Connect to the serial console
|
// Connect to the serial console
|
||||||
if err := sshSOS(d.ID, sshHost); err != nil {
|
if err := sshSOS(dev.ID, sshHost); err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
log.Printf("Machine booting")
|
log.Printf("Access the console with: ssh %s@%s", dev.ID, sshHost)
|
||||||
log.Printf("Access the console with: ssh %s@%s", d.ID, sshHost)
|
|
||||||
|
// if the serve option is present, wait till 'ctrl-c' is hit.
|
||||||
|
// Otherwise we wouldn't serve the files
|
||||||
|
if *serveFlag != "" {
|
||||||
|
stop := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(stop, os.Interrupt)
|
||||||
|
log.Printf("Hit ctrl-c to stop http server")
|
||||||
|
<-stop
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop the http server before exiting
|
||||||
|
if *serveFlag != "" {
|
||||||
|
log.Debugf("Shutting down http server...")
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
httpServer.Shutdown(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !*keepFlag {
|
||||||
|
if _, err := client.Devices.Delete(dev.ID); err != nil {
|
||||||
|
log.Fatalf("Unable to delete device: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// validateHTTPURL does a sanity check that a URL returns a 200 or 300 response
|
// validateHTTPURL does a sanity check that a URL returns a 200 or 300 response
|
||||||
func validateHTTPURL(url string) {
|
func validateHTTPURL(url string) {
|
||||||
log.Printf("Validating URL: %s", url)
|
log.Infof("Validating URL: %s", url)
|
||||||
resp, err := http.Head(url)
|
resp, err := http.Head(url)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
@ -163,11 +210,11 @@ func validateHTTPURL(url string) {
|
|||||||
if resp.StatusCode >= 400 {
|
if resp.StatusCode >= 400 {
|
||||||
log.Fatal("Got a non 200- or 300- HTTP response code: %s", resp)
|
log.Fatal("Got a non 200- or 300- HTTP response code: %s", resp)
|
||||||
}
|
}
|
||||||
log.Printf("OK: %d response code", resp.StatusCode)
|
log.Debugf("OK: %d response code", resp.StatusCode)
|
||||||
}
|
}
|
||||||
|
|
||||||
func sshSOS(user, host string) error {
|
func sshSOS(user, host string) error {
|
||||||
log.Printf("console: ssh %s@%s", user, host)
|
log.Debugf("console: ssh %s@%s", user, host)
|
||||||
|
|
||||||
hostKey, err := sshHostKey(host)
|
hostKey, err := sshHostKey(host)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -267,3 +314,35 @@ func sshHostKey(host string) (ssh.PublicKey, error) {
|
|||||||
}
|
}
|
||||||
return hostKey, nil
|
return hostKey, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// This implements a http.FileSystem which only responds to specific files.
|
||||||
|
type serveFiles struct {
|
||||||
|
files []string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open implements the Open method for the serveFiles FileSystem
|
||||||
|
// implementation.
|
||||||
|
// It converts both the name from the URL and the files provided in
|
||||||
|
// the serveFiles structure into cleaned, absolute filesystem path and
|
||||||
|
// only returns the file if the requested name matches one of the
|
||||||
|
// files in the list.
|
||||||
|
func (fs serveFiles) Open(name string) (http.File, error) {
|
||||||
|
cwd, err := os.Getwd()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
name = filepath.Join(cwd, filepath.FromSlash(path.Clean("/"+name)))
|
||||||
|
for _, fn := range fs.files {
|
||||||
|
fn = filepath.Join(cwd, filepath.FromSlash(path.Clean("/"+fn)))
|
||||||
|
if name == fn {
|
||||||
|
f, err := os.Open(fn)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
log.Debugf("Serving: %s", fn)
|
||||||
|
return f, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("File %s not found", name)
|
||||||
|
}
|
||||||
|
@ -13,6 +13,7 @@ github.com/gophercloud/gophercloud 2804b72cf099b41d2e25c8afcca786f9f962ddee
|
|||||||
github.com/jmespath/go-jmespath bd40a432e4c76585ef6b72d3fd96fb9b6dc7b68d
|
github.com/jmespath/go-jmespath bd40a432e4c76585ef6b72d3fd96fb9b6dc7b68d
|
||||||
github.com/mitchellh/go-ps 4fdf99ab29366514c69ccccddab5dc58b8d84062
|
github.com/mitchellh/go-ps 4fdf99ab29366514c69ccccddab5dc58b8d84062
|
||||||
github.com/moby/hyperkit a82b409a87f12fa3306813410c37f4eed270efac
|
github.com/moby/hyperkit a82b409a87f12fa3306813410c37f4eed270efac
|
||||||
|
github.com/packethost/packngo 9d9409c8c09de7695281e900a776cca03676026a
|
||||||
github.com/radu-matei/azure-sdk-for-go 3b12823551999669c9a325a32472508e0af7978e
|
github.com/radu-matei/azure-sdk-for-go 3b12823551999669c9a325a32472508e0af7978e
|
||||||
github.com/radu-matei/azure-vhd-utils e52754d5569d2a643a7775f72ff2a6cf524f4c25
|
github.com/radu-matei/azure-vhd-utils e52754d5569d2a643a7775f72ff2a6cf524f4c25
|
||||||
github.com/rn/iso9660wrap 4606f848a055435cdef85305960b0e1bb788d506
|
github.com/rn/iso9660wrap 4606f848a055435cdef85305960b0e1bb788d506
|
||||||
|
@ -57,6 +57,7 @@ type DeviceCreateRequest struct {
|
|||||||
UserData string `json:"userdata"`
|
UserData string `json:"userdata"`
|
||||||
Tags []string `json:"tags"`
|
Tags []string `json:"tags"`
|
||||||
IPXEScriptUrl string `json:"ipxe_script_url,omitempty"`
|
IPXEScriptUrl string `json:"ipxe_script_url,omitempty"`
|
||||||
|
PublicIPv4SubnetSize int `json:"public_ipv4_subnet_size,omitempty"`
|
||||||
AlwaysPXE bool `json:"always_pxe,omitempty"`
|
AlwaysPXE bool `json:"always_pxe,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user