Merge pull request #2852 from rn/pktpxe

Improve packet.net support
This commit is contained in:
Rolf Neugebauer 2018-01-11 12:12:47 +00:00 committed by GitHub
commit cd1a472678
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 775 additions and 699 deletions

View File

@ -40,11 +40,17 @@ retry the boot typically fixes this.
## Boot
LinuxKit on Packet boots the `kernel+initrd` output from moby
via
[iPXE](https://help.packet.net/technical/infrastructure/custom-ipxe). iPXE
booting requires a HTTP server on which you can store your images. The
`-base-url` option specifies the URL to the HTTP server.
LinuxKit on Packet boots the `kernel+initrd` output from moby via
[iPXE](https://help.packet.net/technical/infrastructure/custom-ipxe)
which also requires a iPXE script. iPXE booting requires a HTTP server
on which you can store your images. The `-base-url` option specifies
the URL to a HTTP server from which `<name>-kernel`,
`<name>-initrd.img`, and `<name>-packet.ipxe` can be downloaded during
boot.
If you have your own HTTP server, you can use `linuxkit push packet`
to create the files (including the iPXE script) you need to make
available.
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
@ -62,9 +68,10 @@ PACKET_API_KEY=<API key> PACKET_PROJECT_ID=<Project ID> \
linuxkit run packet -serve :8080 -base-url <ngrok url> packet
```
To boot a `arm64` image for Type 2a machine (`-machine
baremetal_2a`) you currently need build using `linuxkit build packet.yml packet.arm64.yml` and then un-compress both the kernel and
the initrd before booting, e.g:
To boot a `arm64` image for Type 2a machine (`-machine baremetal_2a`)
you currently need to build using `linuxkit build packet.yml
packet.arm64.yml` and then un-compress both the kernel and the initrd
before booting, e.g:
```sh
mv packet-initrd.img packet-initrd.img.gz && gzip -d packet-initrd.img.gz
@ -78,12 +85,16 @@ PACKET_API_KEY=<API key> PACKET_PROJECT_ID=<Project ID> \
linuxkit run packet -machine baremetal_2a -serve :8080 -base-url -base-url <ngrok url> packet
```
Alternatively, `linuxkit push packet` will uncompress the kernel and
initrd images on arm machines (or explicitly via the `-decompress`
flag. There is also a `linuxkit serve` command which will start a
local HTTP server serving the specified directory.
**Note**: It may take several minutes to deploy a new server. If you
are attached to the console, you should see the BIOS and the boot
messages.
## Console
By default, `linuxkit run packet ...` will connect to the

View File

@ -76,6 +76,7 @@ func main() {
fmt.Printf(" pkg Package building\n")
fmt.Printf(" push Push a VM image to a cloud or image store\n")
fmt.Printf(" run Run a VM image on a local hypervisor or remote cloud\n")
fmt.Printf(" serve Run a local http server (for iPXE booting)\n")
fmt.Printf(" version Print version information\n")
fmt.Printf(" help Print this message\n")
fmt.Printf("\n")
@ -124,6 +125,8 @@ func main() {
push(args[1:])
case "run":
run(args[1:])
case "serve":
serve(args[1:])
case "version":
printVersion()
case "help":

View File

@ -18,6 +18,7 @@ func pushUsage() {
fmt.Printf(" azure\n")
fmt.Printf(" gcp\n")
fmt.Printf(" openstack\n")
fmt.Printf(" packet\n")
fmt.Printf(" vcenter\n")
fmt.Printf("\n")
fmt.Printf("'options' are the backend specific options.\n")
@ -41,6 +42,8 @@ func push(args []string) {
pushGcp(args[1:])
case "openstack":
pushOpenstack(args[1:])
case "packet":
pushPacket(args[1:])
case "vcenter":
pushVCenter(args[1:])
case "help", "-h", "-help", "--help":

View File

@ -0,0 +1,147 @@
package main
import (
"compress/gzip"
"flag"
"fmt"
"io"
"io/ioutil"
"net/url"
"os"
"path/filepath"
"runtime"
log "github.com/sirupsen/logrus"
)
var (
packetDefaultArch = "x86_64"
packetDefaultDecompress = false
)
func init() {
if runtime.GOARCH == "arm64" {
packetDefaultArch = "aarch64"
// decompress on arm64. iPXE/kernel does not
// seem to grok compressed kernels/initrds.
packetDefaultDecompress = true
}
}
// Process the run arguments and execute run
func pushPacket(args []string) {
flags := flag.NewFlagSet("packet", flag.ExitOnError)
invoked := filepath.Base(os.Args[0])
flags.Usage = func() {
fmt.Printf("USAGE: %s push packet [options] [name]\n\n", invoked)
fmt.Printf("Options:\n\n")
flags.PrintDefaults()
}
baseURLFlag := flags.String("base-url", "", "Base URL that the kernel, initrd and iPXE script are served from (or "+packetBaseURL+")")
nameFlag := flags.String("img-name", "", "Overrides the prefix used to identify the files. Defaults to [name] (or "+packetNameVar+")")
archFlag := flags.String("arch", packetDefaultArch, "Image architecture (x86_64 or aarch64)")
decompressFlag := flags.Bool("decompress", packetDefaultDecompress, "Decompress kernel/initrd before pushing")
dstFlag := flags.String("destination", "", "URL where to push the image to. Currently only 'file' is supported as a scheme (which is also the default if omitted)")
if err := flags.Parse(args); err != nil {
log.Fatal("Unable to parse args")
}
remArgs := flags.Args()
prefix := "packet"
if len(remArgs) > 0 {
prefix = remArgs[0]
}
baseURL := getStringValue(packetBaseURL, *baseURLFlag, "")
if baseURL == "" {
log.Fatal("Need to specify a value for --base-url from where the kernel, initrd and iPXE script will be loaded from.")
}
if *dstFlag == "" {
log.Fatal("Need to specify the destination where to push to.")
}
name := getStringValue(packetNameVar, *nameFlag, prefix)
if _, err := os.Stat(fmt.Sprintf("%s-kernel", name)); os.IsNotExist(err) {
log.Fatalf("kernel file does not exist: %v", err)
}
if _, err := os.Stat(fmt.Sprintf("%s-initrd.img", name)); os.IsNotExist(err) {
log.Fatalf("initrd file does not exist: %v", err)
}
// Read kernel command line
var cmdline string
if c, err := ioutil.ReadFile(prefix + "-cmdline"); err != nil {
log.Fatalf("Cannot open cmdline file: %v", err)
} else {
cmdline = string(c)
}
ipxeScript := packetIPXEScript(name, baseURL, cmdline, *archFlag)
// Parse the destination
dst, err := url.Parse(*dstFlag)
if err != nil {
log.Fatalf("Cannot parse destination: %v", err)
}
switch dst.Scheme {
case "", "file":
packetPushFile(dst, *decompressFlag, name, cmdline, ipxeScript)
default:
log.Fatalf("Unknown destination format: %s", dst.Scheme)
}
}
func packetPushFile(dst *url.URL, decompress bool, name, cmdline, ipxeScript string) {
// Make sure the destination exists
dstPath := filepath.Clean(dst.Path)
if err := os.MkdirAll(dstPath, 0755); err != nil {
log.Fatalf("Could not create destination directory: %v", err)
}
kernelName := fmt.Sprintf("%s-kernel", name)
if err := packetCopy(filepath.Join(dstPath, kernelName), kernelName, decompress); err != nil {
log.Fatalf("Error copying kernel: %v", err)
}
initrdName := fmt.Sprintf("%s-initrd.img", name)
if err := packetCopy(filepath.Join(dstPath, initrdName), initrdName, decompress); err != nil {
log.Fatalf("Error copying initrd: %v", err)
}
ipxeScriptName := fmt.Sprintf("%s-packet.ipxe", name)
if err := ioutil.WriteFile(filepath.Join(dstPath, ipxeScriptName), []byte(ipxeScript), 0644); err != nil {
log.Fatalf("Error writing iPXE script: %v", err)
}
}
func packetCopy(dst, src string, decompress bool) error {
in, err := os.Open(src)
if err != nil {
return err
}
defer in.Close()
var r io.Reader = in
if decompress {
if rd, err := gzip.NewReader(in); err != nil {
log.Warnf("%s does not seem to be gzip'ed (%v). Ignore decompress.", src, err)
} else {
r = rd
}
}
out, err := os.Create(dst)
if err != nil {
return err
}
defer out.Close()
_, err = io.Copy(out, r)
if err != nil {
return err
}
return out.Close()
}

View File

@ -56,7 +56,7 @@ func runPacket(args []string) {
fmt.Printf("Options:\n\n")
flags.PrintDefaults()
}
baseURLFlag := flags.String("base-url", "", "Base URL that the kernel and initrd are served from (or "+packetBaseURL+")")
baseURLFlag := flags.String("base-url", "", "Base URL that the kernel, initrd and iPXE script are served from (or "+packetBaseURL+")")
zoneFlag := flags.String("zone", packetDefaultZone, "Packet Zone (or "+packetZoneVar+")")
machineFlag := flags.String("machine", packetDefaultMachine, "Packet Machine Type (or "+packetMachineVar+")")
apiKeyFlag := flags.String("api-key", "", "Packet API key (or "+packetAPIKeyVar+")")
@ -80,7 +80,7 @@ func runPacket(args []string) {
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 <url>/%s-kernel and <url>/%s-initrd.img")
log.Fatal("Need to specify a value for --base-url where the images are hosted. This URL should contain <url>/%s-kernel, <url>/%s-initrd.img and <url>/%s-packet.ipxe")
}
facility := getStringValue(packetZoneVar, *zoneFlag, "")
plan := getStringValue(packetMachineVar, *machineFlag, defaultMachine)
@ -101,19 +101,31 @@ func runPacket(args []string) {
log.Fatalf("Combination of keep=%t and console=%t makes little sense", *keepFlag, *consoleFlag)
}
// Read kernel command line
var cmdline string
if c, err := ioutil.ReadFile(prefix + "-cmdline"); err != nil {
log.Fatalf("Cannot open cmdline file: %v", err)
} else {
cmdline = string(c)
}
ipxeScriptName := fmt.Sprintf("%s-packet.ipxe", name)
// Serve files with a local http server
var httpServer *http.Server
if *serveFlag != "" {
// Read kernel command line
var cmdline string
if c, err := ioutil.ReadFile(prefix + "-cmdline"); err != nil {
log.Fatalf("Cannot open cmdline file: %v", err)
} else {
cmdline = string(c)
}
ipxeScript := packetIPXEScript(name, url, cmdline, packetMachineToArch(*machineFlag))
log.Debugf("Using iPXE script:\n%s\n", ipxeScript)
// Two handlers, one for the iPXE script and one for the kernel/initrd files
mux := http.NewServeMux()
mux.HandleFunc(fmt.Sprintf("/%s", ipxeScriptName),
func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, ipxeScript)
})
fs := serveFiles{[]string{fmt.Sprintf("%s-kernel", name), fmt.Sprintf("%s-initrd.img", name)}}
httpServer = &http.Server{Addr: ":8080", Handler: http.FileServer(fs)}
mux.Handle("/", http.FileServer(fs))
httpServer = &http.Server{Addr: *serveFlag, Handler: mux}
go func() {
log.Debugf("Listening on http://%s\n", *serveFlag)
if err := httpServer.ListenAndServe(); err != nil {
@ -122,38 +134,22 @@ func runPacket(args []string) {
}()
}
// Build the iPXE script
// Note, we *append* the <prefix>-cmdline. iXPE booting will
// need the first set of "kernel-params" and we don't want to
// require these to be added to every YAML file.
userData := "#!ipxe\n\n"
userData += "dhcp\n"
userData += fmt.Sprintf("set base-url %s\n", url)
if *machineFlag != "baremetal_2a" {
var tty string
// x86_64 Packet machines have console on non standard ttyS1 which is not in most examples
if !strings.Contains(cmdline, "console=ttyS1") {
tty = "console=ttyS1,115200"
}
userData += fmt.Sprintf("set kernel-params ip=dhcp nomodeset ro serial %s %s\n", tty, cmdline)
userData += fmt.Sprintf("kernel ${base-url}/%s-kernel ${kernel-params}\n", name)
userData += fmt.Sprintf("initrd ${base-url}/%s-initrd.img\n", name)
} else {
// With EFI boot need to specify the initrd and root dev explicitly. See:
// http://ipxe.org/appnote/debian_preseed
// http://forum.ipxe.org/showthread.php?tid=7589
userData += fmt.Sprintf("initrd --name initrd ${base-url}/%s-initrd.img\n", name)
userData += fmt.Sprintf("set kernel-params ip=dhcp nomodeset ro %s\n", cmdline)
userData += fmt.Sprintf("kernel ${base-url}/%s-kernel initrd=initrd root=/dev/ram0 ${kernel-params}\n", name)
}
userData += "boot"
log.Debugf("Using userData of:\n%s\n", userData)
// Make sure the URL works
// Make sure the URLs work
ipxeURL := fmt.Sprintf("%s/%s", url, ipxeScriptName)
initrdURL := fmt.Sprintf("%s/%s-initrd.img", url, name)
kernelURL := fmt.Sprintf("%s/%s-kernel", url, name)
validateHTTPURL(kernelURL)
validateHTTPURL(initrdURL)
log.Infof("Validating URL: %s", ipxeURL)
if err := validateHTTPURL(ipxeURL); err != nil {
log.Fatalf("Invalid iPXE URL %s: %v", ipxeURL, err)
}
log.Infof("Validating URL: %s", kernelURL)
if err := validateHTTPURL(kernelURL); err != nil {
log.Fatalf("Invalid kernel URL %s: %v", kernelURL, err)
}
log.Infof("Validating URL: %s", initrdURL)
if err := validateHTTPURL(initrdURL); err != nil {
log.Fatalf("Invalid initrd URL %s: %v", initrdURL, err)
}
client := packngo.NewClient("", apiKey, nil)
tags := []string{}
@ -172,11 +168,11 @@ func runPacket(args []string) {
log.Debugf("%s\n", string(b))
req := packngo.DeviceUpdateRequest{
HostName: hostname,
UserData: userData,
Locked: dev.Locked,
Tags: dev.Tags,
AlwaysPXE: *alwaysPXE,
Hostname: hostname,
Locked: dev.Locked,
Tags: dev.Tags,
IPXEScriptURL: ipxeURL,
AlwaysPXE: *alwaysPXE,
}
dev, _, err = client.Devices.Update(*deviceFlag, &req)
if err != nil {
@ -188,15 +184,15 @@ func runPacket(args []string) {
} else {
// Create a new device
req := packngo.DeviceCreateRequest{
HostName: hostname,
Plan: plan,
Facility: facility,
OS: osType,
BillingCycle: billing,
ProjectID: projectID,
UserData: userData,
Tags: tags,
AlwaysPXE: *alwaysPXE,
Hostname: hostname,
Plan: plan,
Facility: facility,
OS: osType,
BillingCycle: billing,
ProjectID: projectID,
Tags: tags,
IPXEScriptURL: ipxeURL,
AlwaysPXE: *alwaysPXE,
}
dev, _, err = client.Devices.Create(&req)
if err != nil {
@ -214,7 +210,7 @@ func runPacket(args []string) {
sshHost := "sos." + dev.Facility.Code + ".packet.net"
if *consoleFlag {
// Connect to the serial console
if err := sshSOS(dev.ID, sshHost); err != nil {
if err := packetSOS(dev.ID, sshHost); err != nil {
log.Fatal(err)
}
} else {
@ -249,20 +245,58 @@ func runPacket(args []string) {
}
}
// validateHTTPURL does a sanity check that a URL returns a 200 or 300 response
func validateHTTPURL(url string) {
log.Infof("Validating URL: %s", url)
resp, err := http.Head(url)
if err != nil {
log.Fatal(err)
// Convert machine type to architecture
func packetMachineToArch(machine string) string {
switch machine {
case "baremetal_2a", "baremetal_2a2":
return "aarch64"
default:
return "x86_64"
}
if resp.StatusCode >= 400 {
log.Fatal("Got a non 200- or 300- HTTP response code: %s", resp)
}
log.Debugf("OK: %d response code", resp.StatusCode)
}
func sshSOS(user, host string) error {
// Build the iPXE script for packet machines
func packetIPXEScript(name, baseURL, cmdline, arch string) string {
// Note, we *append* the <prefix>-cmdline. iXPE booting will
// need the first set of "kernel-params" and we don't want to
// require these to be added to every YAML file.
script := "#!ipxe\n\n"
script += "dhcp\n"
script += fmt.Sprintf("set base-url %s\n", baseURL)
if arch != "aarch64" {
var tty string
// x86_64 Packet machines have console on non standard ttyS1 which is not in most examples
if !strings.Contains(cmdline, "console=ttyS1") {
tty = "console=ttyS1,115200"
}
script += fmt.Sprintf("set kernel-params ip=dhcp nomodeset ro serial %s %s\n", tty, cmdline)
script += fmt.Sprintf("kernel ${base-url}/%s-kernel ${kernel-params}\n", name)
script += fmt.Sprintf("initrd ${base-url}/%s-initrd.img\n", name)
} else {
// With EFI boot need to specify the initrd and root dev explicitly. See:
// http://ipxe.org/appnote/debian_preseed
// http://forum.ipxe.org/showthread.php?tid=7589
script += fmt.Sprintf("initrd --name initrd ${base-url}/%s-initrd.img\n", name)
script += fmt.Sprintf("set kernel-params ip=dhcp nomodeset ro %s\n", cmdline)
script += fmt.Sprintf("kernel ${base-url}/%s-kernel initrd=initrd root=/dev/ram0 ${kernel-params}\n", name)
}
script += "boot"
return script
}
// validateHTTPURL does a sanity check that a URL returns a 200 or 300 response
func validateHTTPURL(url string) error {
resp, err := http.Head(url)
if err != nil {
return err
}
if resp.StatusCode >= 400 {
return fmt.Errorf("Got a non 200- or 300- HTTP response code: %s", resp)
}
return nil
}
func packetSOS(user, host string) error {
log.Debugf("console: ssh %s@%s", user, host)
hostKey, err := sshHostKey(host)

34
src/cmd/linuxkit/serve.go Normal file
View File

@ -0,0 +1,34 @@
package main
import (
"flag"
"fmt"
"net/http"
"os"
"path/filepath"
log "github.com/sirupsen/logrus"
)
func logRequest(handler http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
log.Infof("%s %s", r.Method, r.URL)
handler.ServeHTTP(w, r)
})
}
// serve starts a local web server
func serve(args []string) {
flags := flag.NewFlagSet("serve", flag.ExitOnError)
invoked := filepath.Base(os.Args[0])
flags.Usage = func() {
fmt.Printf("USAGE: %s serve [options]\n\n", invoked)
fmt.Printf("Options:\n\n")
flags.PrintDefaults()
}
portFlag := flags.String("port", ":8080", "Local port to serve on")
dirFlag := flags.String("directory", ".", "Directory to serve")
http.Handle("/", http.FileServer(http.Dir(*dirFlag)))
log.Fatal(http.ListenAndServe(*portFlag, logRequest(http.DefaultServeMux)))
}

View File

@ -31,7 +31,7 @@ github.com/moby/vpnkit 0e4293bb1058598c4b0a406ed171f52573ef414c
github.com/opencontainers/go-digest 21dfd564fd89c944783d00d069f33e3e7123c448
github.com/opencontainers/image-spec v1.0.0
github.com/opencontainers/runtime-spec v1.0.0
github.com/packethost/packngo 131798f2804a1b3e895ca98047d56f0d7e094e2a
github.com/packethost/packngo f1be085ecd6fca1b0a0e25eda71f208dcfcee5ab
github.com/pkg/errors v0.8.0
github.com/pmezard/go-difflib v1.0.0
github.com/radu-matei/azure-sdk-for-go 3b12823551999669c9a325a32472508e0af7978e

View File

@ -1,60 +0,0 @@
## About
This directory contains a collection of scripts used to build and manage this
repository. If there are any issues regarding the intention of a particular
script (or even part of a certain script), please reach out to us.
It may help us either refine our current scripts, or add on new ones
that are appropriate for a given use case.
## DinD (dind.sh)
DinD is a wrapper script which allows Docker to be run inside a Docker
container. DinD requires the container to
be run with privileged mode enabled.
## Generate Authors (generate-authors.sh)
Generates AUTHORS; a file with all the names and corresponding emails of
individual contributors. AUTHORS can be found in the home directory of
this repository.
## Make
There are two make files, each with different extensions. Neither are supposed
to be called directly; only invoke `make`. Both scripts run inside a Docker
container.
### make.ps1
- The Windows native build script that uses PowerShell semantics; it is limited
unlike `hack\make.sh` since it does not provide support for the full set of
operations provided by the Linux counterpart, `make.sh`. However, `make.ps1`
does provide support for local Windows development and Windows to Windows CI.
More information is found within `make.ps1` by the author, @jhowardmsft
### make.sh
- Referenced via `make test` when running tests on a local machine,
or directly referenced when running tests inside a Docker development container.
- When running on a local machine, `make test` to run all tests found in
`test`, `test-unit`, `test-integration`, and `test-docker-py` on
your local machine. The default timeout is set in `make.sh` to 60 minutes
(`${TIMEOUT:=60m}`), since it currently takes up to an hour to run
all of the tests.
- When running inside a Docker development container, `hack/make.sh` does
not have a single target that runs all the tests. You need to provide a
single command line with multiple targets that performs the same thing.
An example referenced from [Run targets inside a development container](https://docs.docker.com/opensource/project/test-and-docs/#run-targets-inside-a-development-container): `root@5f8630b873fe:/go/src/github.com/moby/moby# hack/make.sh dynbinary binary cross test-unit test-integration test-docker-py`
- For more information related to testing outside the scope of this README,
refer to
[Run tests and test documentation](https://docs.docker.com/opensource/project/test-and-docs/)
## Release (release.sh)
Releases any bundles built by `make` on a public AWS S3 bucket.
For information regarding configuration, please view `release.sh`.
## Vendor (vendor.sh)
A shell script that is a wrapper around Vndr. For information on how to use
this, please refer to [vndr's README](https://github.com/LK4D4/vndr/blob/master/README.md)

View File

@ -1,69 +0,0 @@
# Integration Testing on Swarm
IT on Swarm allows you to execute integration test in parallel across a Docker Swarm cluster
## Architecture
### Master service
- Works as a funker caller
- Calls a worker funker (`-worker-service`) with a chunk of `-check.f` filter strings (passed as a file via `-input` flag, typically `/mnt/input`)
### Worker service
- Works as a funker callee
- Executes an equivalent of `TESTFLAGS=-check.f TestFoo|TestBar|TestBaz ... make test-integration-cli` using the bind-mounted API socket (`docker.sock`)
### Client
- Controls master and workers via `docker stack`
- No need to have a local daemon
Typically, the master and workers are supposed to be running on a cloud environment,
while the client is supposed to be running on a laptop, e.g. Docker for Mac/Windows.
## Requirement
- Docker daemon 1.13 or later
- Private registry for distributed execution with multiple nodes
## Usage
### Step 1: Prepare images
$ make build-integration-cli-on-swarm
Following environment variables are known to work in this step:
- `BUILDFLAGS`
- `DOCKER_INCREMENTAL_BINARY`
Note: during the transition into Moby Project, you might need to create a symbolic link `$GOPATH/src/github.com/docker/docker` to `$GOPATH/src/github.com/moby/moby`.
### Step 2: Execute tests
$ ./hack/integration-cli-on-swarm/integration-cli-on-swarm -replicas 40 -push-worker-image YOUR_REGISTRY.EXAMPLE.COM/integration-cli-worker:latest
Following environment variables are known to work in this step:
- `DOCKER_GRAPHDRIVER`
- `DOCKER_EXPERIMENTAL`
#### Flags
Basic flags:
- `-replicas N`: the number of worker service replicas. i.e. degree of parallelism.
- `-chunks N`: the number of chunks. By default, `chunks` == `replicas`.
- `-push-worker-image REGISTRY/IMAGE:TAG`: push the worker image to the registry. Note that if you have only single node and hence you do not need a private registry, you do not need to specify `-push-worker-image`.
Experimental flags for mitigating makespan nonuniformity:
- `-shuffle`: Shuffle the test filter strings
Flags for debugging IT on Swarm itself:
- `-rand-seed N`: the random seed. This flag is useful for deterministic replaying. By default(0), the timestamp is used.
- `-filters-file FILE`: the file contains `-check.f` strings. By default, the file is automatically generated.
- `-dry-run`: skip the actual workload
- `keep-executor`: do not auto-remove executor containers, which is used for running privileged programs on Swarm

View File

@ -1,2 +0,0 @@
# dependencies specific to worker (i.e. github.com/docker/docker/...) are not vendored here
github.com/bfirsh/funker-go eaa0a2e06f30e72c9a0b7f858951e581e26ef773

View File

@ -7,3 +7,20 @@ Committing
----------
Before committing, it's a good idea to run `gofmt -w *.go`. ([gofmt](https://golang.org/cmd/gofmt/))
Acceptance Tests
----------------
If you want to run tests against the actual Packet API, you must set envvar `PACKET_TEST_ACTUAL_API` to non-empty string for the `go test`. The device tests wait for the device creation, so it's best to run a few in parallel.
To run all the tests, you can do
```
$ PACKNGO_TEST_ACTUAL_API=1 go test -v -parallel 8
```
It's also useful to run only single acceptance test at a time:
```
$ PACKNGO_TEST_ACTUAL_API=1 go test -v -run=TestAccDeviceBasic
```

View File

@ -24,24 +24,43 @@ type devicesRoot struct {
// Device represents a Packet device
type Device struct {
ID string `json:"id"`
Href string `json:"href,omitempty"`
Hostname string `json:"hostname,omitempty"`
State string `json:"state,omitempty"`
Created string `json:"created_at,omitempty"`
Updated string `json:"updated_at,omitempty"`
Locked bool `json:"locked,omitempty"`
BillingCycle string `json:"billing_cycle,omitempty"`
Tags []string `json:"tags,omitempty"`
Network []*IPAddress `json:"ip_addresses"`
OS *OS `json:"operating_system,omitempty"`
Plan *Plan `json:"plan,omitempty"`
Facility *Facility `json:"facility,omitempty"`
Project *Project `json:"project,omitempty"`
ProvisionPer float32 `json:"provisioning_percentage,omitempty"`
UserData string `json:"userdata",omitempty`
IPXEScriptUrl string `json:"ipxe_script_url,omitempty"`
AlwaysPXE bool `json:"always_pxe,omitempty"`
ID string `json:"id"`
Href string `json:"href,omitempty"`
Hostname string `json:"hostname,omitempty"`
State string `json:"state,omitempty"`
Created string `json:"created_at,omitempty"`
Updated string `json:"updated_at,omitempty"`
Locked bool `json:"locked,omitempty"`
BillingCycle string `json:"billing_cycle,omitempty"`
Storage map[string]interface{} `json:"storage,omitempty"`
Tags []string `json:"tags,omitempty"`
Network []*IPAddressAssignment `json:"ip_addresses"`
Volumes []*Volume `json:"volumes"`
OS *OS `json:"operating_system,omitempty"`
Plan *Plan `json:"plan,omitempty"`
Facility *Facility `json:"facility,omitempty"`
Project *Project `json:"project,omitempty"`
ProvisionEvents []*ProvisionEvent `json:"provisioning_events,omitempty"`
ProvisionPer float32 `json:"provisioning_percentage,omitempty"`
UserData string `json:"userdata,omitempty"`
RootPassword string `json:"root_password,omitempty"`
IPXEScriptURL string `json:"ipxe_script_url,omitempty"`
AlwaysPXE bool `json:"always_pxe,omitempty"`
HardwareReservation Href `json:"hardware_reservation,omitempty"`
SpotInstance bool `json:"spot_instance,omitempty"`
SpotPriceMax float64 `json:"spot_price_max,omitempty"`
TerminationTime *Timestamp `json:"termination_time,omitempty"`
}
type ProvisionEvent struct {
ID string `json:"id"`
Body string `json:"body"`
CreatedAt *Timestamp `json:"created_at,omitempty"`
Href string `json:"href"`
Interpolated string `json:"interpolated"`
Relationships []Href `json:"relationships"`
State string `json:"state"`
Type string `json:"type"`
}
func (d Device) String() string {
@ -50,28 +69,33 @@ func (d Device) String() string {
// DeviceCreateRequest type used to create a Packet device
type DeviceCreateRequest struct {
HostName string `json:"hostname"`
Plan string `json:"plan"`
Facility string `json:"facility"`
OS string `json:"operating_system"`
BillingCycle string `json:"billing_cycle"`
ProjectID string `json:"project_id"`
UserData string `json:"userdata"`
Tags []string `json:"tags"`
IPXEScriptUrl string `json:"ipxe_script_url,omitempty"`
PublicIPv4SubnetSize int `json:"public_ipv4_subnet_size,omitempty"`
AlwaysPXE bool `json:"always_pxe,omitempty"`
Hostname string `json:"hostname"`
Plan string `json:"plan"`
Facility string `json:"facility"`
OS string `json:"operating_system"`
BillingCycle string `json:"billing_cycle"`
ProjectID string `json:"project_id"`
UserData string `json:"userdata"`
Storage string `json:"storage,omitempty"`
Tags []string `json:"tags"`
IPXEScriptURL string `json:"ipxe_script_url,omitempty"`
PublicIPv4SubnetSize int `json:"public_ipv4_subnet_size,omitempty"`
AlwaysPXE bool `json:"always_pxe,omitempty"`
HardwareReservationID string `json:"hardware_reservation_id,omitempty"`
SpotInstance bool `json:"spot_instance,omitempty"`
SpotPriceMax float64 `json:"spot_price_max,omitempty,string"`
TerminationTime *Timestamp `json:"termination_time,omitempty"`
}
// DeviceUpdateRequest type used to update a Packet device
type DeviceUpdateRequest struct {
HostName string `json:"hostname"`
Hostname string `json:"hostname"`
Description string `json:"description"`
UserData string `json:"userdata"`
Locked bool `json:"locked"`
Tags []string `json:"tags"`
AlwaysPXE bool `json:"always_pxe,omitempty"`
IPXEScriptUrl string `json:"ipxe_script_url,omitempty"`
IPXEScriptURL string `json:"ipxe_script_url,omitempty"`
}
func (d DeviceCreateRequest) String() string {
@ -95,14 +119,9 @@ type DeviceServiceOp struct {
// List returns devices on a project
func (s *DeviceServiceOp) List(projectID string) ([]Device, *Response, error) {
path := fmt.Sprintf("%s/%s/devices?include=facility", projectBasePath, projectID)
req, err := s.client.NewRequest("GET", path, nil)
if err != nil {
return nil, nil, err
}
root := new(devicesRoot)
resp, err := s.client.Do(req, root)
resp, err := s.client.DoRequest("GET", path, nil, root)
if err != nil {
return nil, resp, err
}
@ -113,14 +132,9 @@ func (s *DeviceServiceOp) List(projectID string) ([]Device, *Response, error) {
// Get returns a device by id
func (s *DeviceServiceOp) Get(deviceID string) (*Device, *Response, error) {
path := fmt.Sprintf("%s/%s?include=facility", deviceBasePath, deviceID)
req, err := s.client.NewRequest("GET", path, nil)
if err != nil {
return nil, nil, err
}
device := new(Device)
resp, err := s.client.Do(req, device)
resp, err := s.client.DoRequest("GET", path, nil, device)
if err != nil {
return nil, resp, err
}
@ -131,14 +145,9 @@ func (s *DeviceServiceOp) Get(deviceID string) (*Device, *Response, error) {
// Create creates a new device
func (s *DeviceServiceOp) Create(createRequest *DeviceCreateRequest) (*Device, *Response, error) {
path := fmt.Sprintf("%s/%s/devices", projectBasePath, createRequest.ProjectID)
req, err := s.client.NewRequest("POST", path, createRequest)
if err != nil {
return nil, nil, err
}
device := new(Device)
resp, err := s.client.Do(req, device)
resp, err := s.client.DoRequest("POST", path, createRequest, device)
if err != nil {
return nil, resp, err
}
@ -149,14 +158,9 @@ func (s *DeviceServiceOp) Create(createRequest *DeviceCreateRequest) (*Device, *
// Update updates an existing device
func (s *DeviceServiceOp) Update(deviceID string, updateRequest *DeviceUpdateRequest) (*Device, *Response, error) {
path := fmt.Sprintf("%s/%s?include=facility", deviceBasePath, deviceID)
req, err := s.client.NewRequest("PUT", path, updateRequest)
if err != nil {
return nil, nil, err
}
device := new(Device)
resp, err := s.client.Do(req, device)
resp, err := s.client.DoRequest("PUT", path, updateRequest, device)
if err != nil {
return nil, resp, err
}
@ -168,59 +172,31 @@ func (s *DeviceServiceOp) Update(deviceID string, updateRequest *DeviceUpdateReq
func (s *DeviceServiceOp) Delete(deviceID string) (*Response, error) {
path := fmt.Sprintf("%s/%s", deviceBasePath, deviceID)
req, err := s.client.NewRequest("DELETE", path, nil)
if err != nil {
return nil, err
}
resp, err := s.client.Do(req, nil)
return resp, err
return s.client.DoRequest("DELETE", path, nil, nil)
}
// Reboot reboots on a device
func (s *DeviceServiceOp) Reboot(deviceID string) (*Response, error) {
path := fmt.Sprintf("%s/%s/actions", deviceBasePath, deviceID)
action := &DeviceActionRequest{Type: "reboot"}
req, err := s.client.NewRequest("POST", path, action)
if err != nil {
return nil, err
}
resp, err := s.client.Do(req, nil)
return resp, err
return s.client.DoRequest("POST", path, action, nil)
}
// PowerOff powers on a device
func (s *DeviceServiceOp) PowerOff(deviceID string) (*Response, error) {
path := fmt.Sprintf("%s/%s/actions", deviceBasePath, deviceID)
action := &DeviceActionRequest{Type: "power_off"}
req, err := s.client.NewRequest("POST", path, action)
if err != nil {
return nil, err
}
resp, err := s.client.Do(req, nil)
return resp, err
return s.client.DoRequest("POST", path, action, nil)
}
// PowerOn powers on a device
func (s *DeviceServiceOp) PowerOn(deviceID string) (*Response, error) {
path := fmt.Sprintf("%s/%s/actions", deviceBasePath, deviceID)
action := &DeviceActionRequest{Type: "power_on"}
req, err := s.client.NewRequest("POST", path, action)
if err != nil {
return nil, err
}
resp, err := s.client.Do(req, nil)
return resp, err
return s.client.DoRequest("POST", path, action, nil)
}
type lockDeviceType struct {
@ -230,30 +206,15 @@ type lockDeviceType struct {
// Lock sets a device to "locked"
func (s *DeviceServiceOp) Lock(deviceID string) (*Response, error) {
path := fmt.Sprintf("%s/%s", deviceBasePath, deviceID)
action := lockDeviceType{Locked: true}
req, err := s.client.NewRequest("PATCH", path, action)
if err != nil {
return nil, err
}
resp, err := s.client.Do(req, nil)
return resp, err
return s.client.DoRequest("PATCH", path, action, nil)
}
// Unlock sets a device to "locked"
func (s *DeviceServiceOp) Unlock(deviceID string) (*Response, error) {
path := fmt.Sprintf("%s/%s", deviceBasePath, deviceID)
action := lockDeviceType{Locked: false}
req, err := s.client.NewRequest("PATCH", path, action)
if err != nil {
return nil, err
}
resp, err := s.client.Do(req, nil)
return resp, err
return s.client.DoRequest("PATCH", path, action, nil)
}

View File

@ -26,13 +26,9 @@ type EmailServiceOp struct {
// Get retrieves an email by id
func (s *EmailServiceOp) Get(emailID string) (*Email, *Response, error) {
req, err := s.client.NewRequest("GET", emailBasePath, nil)
if err != nil {
return nil, nil, err
}
email := new(Email)
resp, err := s.client.Do(req, email)
resp, err := s.client.DoRequest("GET", emailBasePath, nil, email)
if err != nil {
return nil, resp, err
}

View File

@ -41,13 +41,9 @@ type FacilityServiceOp struct {
// List returns all available Packet facilities
func (s *FacilityServiceOp) List() ([]Facility, *Response, error) {
req, err := s.client.NewRequest("GET", facilityBasePath, nil)
if err != nil {
return nil, nil, err
}
root := new(facilityRoot)
resp, err := s.client.Do(req, root)
resp, err := s.client.DoRequest("GET", facilityBasePath, nil, root)
if err != nil {
return nil, resp, err
}

View File

@ -1,110 +1,71 @@
package packngo
import "fmt"
import (
"fmt"
)
const ipBasePath = "/ips"
// IPService interface defines available IP methods
type IPService interface {
Assign(deviceID string, assignRequest *IPAddressAssignRequest) (*IPAddress, *Response, error)
Unassign(ipAddressID string) (*Response, error)
Get(ipAddressID string) (*IPAddress, *Response, error)
// DeviceIPService handles assignment of addresses from reserved blocks to instances in a project.
type DeviceIPService interface {
Assign(deviceID string, assignRequest *AddressStruct) (*IPAddressAssignment, *Response, error)
Unassign(assignmentID string) (*Response, error)
Get(assignmentID string) (*IPAddressAssignment, *Response, error)
}
// IPAddress represents a ip address
type IPAddress struct {
ID string `json:"id"`
Address string `json:"address"`
Gateway string `json:"gateway"`
Network string `json:"network"`
AddressFamily int `json:"address_family"`
Netmask string `json:"netmask"`
Public bool `json:"public"`
Cidr int `json:"cidr"`
AssignedTo map[string]string `json:"assigned_to"`
Created string `json:"created_at,omitempty"`
Updated string `json:"updated_at,omitempty"`
Href string `json:"href"`
Facility Facility `json:"facility,omitempty"`
}
// IPAddressAssignRequest represents the body if a ip assign request
type IPAddressAssignRequest struct {
Address string `json:"address"`
}
func (i IPAddress) String() string {
return Stringify(i)
}
// IPServiceOp implements IPService
type IPServiceOp struct {
client *Client
}
// Get returns IpAddress by ID
func (i *IPServiceOp) Get(ipAddressID string) (*IPAddress, *Response, error) {
path := fmt.Sprintf("%s/%s", ipBasePath, ipAddressID)
req, err := i.client.NewRequest("GET", path, nil)
if err != nil {
return nil, nil, err
}
ip := new(IPAddress)
resp, err := i.client.Do(req, ip)
if err != nil {
return nil, resp, err
}
return ip, resp, err
}
// Unassign unassigns an IP address record. This will remove the relationship between an IP
// and the device and will make the IP address available to be assigned to another device.
func (i *IPServiceOp) Unassign(ipAddressID string) (*Response, error) {
path := fmt.Sprintf("%s/%s", ipBasePath, ipAddressID)
req, err := i.client.NewRequest("DELETE", path, nil)
if err != nil {
return nil, err
}
resp, err := i.client.Do(req, nil)
return resp, err
}
// Assign assigns an IP address to a device. The IP address must be in one of the IP ranges assigned to the devices project.
func (i *IPServiceOp) Assign(deviceID string, assignRequest *IPAddressAssignRequest) (*IPAddress, *Response, error) {
path := fmt.Sprintf("%s/%s%s", deviceBasePath, deviceID, ipBasePath)
req, err := i.client.NewRequest("POST", path, assignRequest)
ip := new(IPAddress)
resp, err := i.client.Do(req, ip)
if err != nil {
return nil, resp, err
}
return ip, resp, err
}
// IP RESERVATIONS API
// IPReservationService interface defines available IPReservation methods
type IPReservationService interface {
List(projectID string) ([]IPReservation, *Response, error)
RequestMore(projectID string, ipReservationReq *IPReservationRequest) (*IPReservation, *Response, error)
Get(ipReservationID string) (*IPReservation, *Response, error)
// ProjectIPService handles reservation of IP address blocks for a project.
type ProjectIPService interface {
Get(reservationID string) (*IPAddressReservation, *Response, error)
List(projectID string) ([]IPAddressReservation, *Response, error)
Request(projectID string, ipReservationReq *IPReservationRequest) (*IPAddressReservation, *Response, error)
Remove(ipReservationID string) (*Response, error)
AvailableAddresses(ipReservationID string, r *AvailableRequest) ([]string, *Response, error)
}
// IPReservationServiceOp implements the IPReservationService interface
type IPReservationServiceOp struct {
client *Client
type ipAddressCommon struct {
ID string `json:"id"`
Address string `json:"address"`
Gateway string `json:"gateway"`
Network string `json:"network"`
AddressFamily int `json:"address_family"`
Netmask string `json:"netmask"`
Public bool `json:"public"`
CIDR int `json:"cidr"`
Created string `json:"created_at,omitempty"`
Updated string `json:"updated_at,omitempty"`
Href string `json:"href"`
Management bool `json:"management"`
Manageable bool `json:"manageable"`
Project Href `json:"project"`
}
// IPReservationRequest represents the body of a reservation request
// IPAddressReservation is created when user sends IP reservation request for a project (considering it's within quota).
type IPAddressReservation struct {
ipAddressCommon
Assignments []Href `json:"assignments"`
Facility Facility `json:"facility,omitempty"`
Available string `json:"available"`
Addon bool `json:"addon"`
Bill bool `json:"bill"`
}
// AvailableResponse is a type for listing of available addresses from a reserved block.
type AvailableResponse struct {
Available []string `json:"available"`
}
// AvailableRequest is a type for listing available addresses from a reserved block.
type AvailableRequest struct {
CIDR int `json:"cidr"`
}
// IPAddressAssignment is created when an IP address from reservation block is assigned to a device.
type IPAddressAssignment struct {
ipAddressCommon
AssignedTo Href `json:"assigned_to"`
}
// IPReservationRequest represents the body of a reservation request.
type IPReservationRequest struct {
Type string `json:"type"`
Quantity int `json:"quantity"`
@ -112,95 +73,122 @@ type IPReservationRequest struct {
Facility string `json:"facility"`
}
// IPReservation represent an IP reservation for a single project
type IPReservation struct {
ID string `json:"id"`
Network string `json:"network"`
Address string `json:"address"`
AddressFamily int `json:"address_family"`
Netmask string `json:"netmask"`
Public bool `json:"public"`
Cidr int `json:"cidr"`
Management bool `json:"management"`
Manageable bool `json:"manageable"`
Addon bool `json:"addon"`
Bill bool `json:"bill"`
Assignments []map[string]string `json:"assignments"`
Created string `json:"created_at,omitempty"`
Updated string `json:"updated_at,omitempty"`
Href string `json:"href"`
Facility Facility `json:"facility,omitempty"`
// AddressStruct is a helper type for request/response with dict like {"address": ... }
type AddressStruct struct {
Address string `json:"address"`
}
type ipReservationRoot struct {
IPReservations []IPReservation `json:"ip_addresses"`
func deleteFromIP(client *Client, resourceID string) (*Response, error) {
path := fmt.Sprintf("%s/%s", ipBasePath, resourceID)
return client.DoRequest("DELETE", path, nil, nil)
}
func (i IPAddressReservation) String() string {
return Stringify(i)
}
func (i IPAddressAssignment) String() string {
return Stringify(i)
}
// DeviceIPServiceOp is interface for IP-address assignment methods.
type DeviceIPServiceOp struct {
client *Client
}
// Unassign unassigns an IP address from the device to which it is currently assigned.
// This will remove the relationship between an IP and the device and will make the IP
// address available to be assigned to another device.
func (i *DeviceIPServiceOp) Unassign(assignmentID string) (*Response, error) {
return deleteFromIP(i.client, assignmentID)
}
// Assign assigns an IP address to a device.
// The IP address must be in one of the IP ranges assigned to the devices project.
func (i *DeviceIPServiceOp) Assign(deviceID string, assignRequest *AddressStruct) (*IPAddressAssignment, *Response, error) {
path := fmt.Sprintf("%s/%s%s", deviceBasePath, deviceID, ipBasePath)
ipa := new(IPAddressAssignment)
resp, err := i.client.DoRequest("POST", path, assignRequest, ipa)
if err != nil {
return nil, resp, err
}
return ipa, resp, err
}
// Get returns assignment by ID.
func (i *DeviceIPServiceOp) Get(assignmentID string) (*IPAddressAssignment, *Response, error) {
path := fmt.Sprintf("%s/%s", ipBasePath, assignmentID)
ipa := new(IPAddressAssignment)
resp, err := i.client.DoRequest("GET", path, nil, ipa)
if err != nil {
return nil, resp, err
}
return ipa, resp, err
}
// ProjectIPServiceOp is interface for IP assignment methods.
type ProjectIPServiceOp struct {
client *Client
}
// Get returns reservation by ID.
func (i *ProjectIPServiceOp) Get(reservationID string) (*IPAddressReservation, *Response, error) {
path := fmt.Sprintf("%s/%s", ipBasePath, reservationID)
ipr := new(IPAddressReservation)
resp, err := i.client.DoRequest("GET", path, nil, ipr)
if err != nil {
return nil, resp, err
}
return ipr, resp, err
}
// List provides a list of IP resevations for a single project.
func (i *IPReservationServiceOp) List(projectID string) ([]IPReservation, *Response, error) {
func (i *ProjectIPServiceOp) List(projectID string) ([]IPAddressReservation, *Response, error) {
path := fmt.Sprintf("%s/%s%s", projectBasePath, projectID, ipBasePath)
reservations := new(struct {
Reservations []IPAddressReservation `json:"ip_addresses"`
})
req, err := i.client.NewRequest("GET", path, nil)
if err != nil {
return nil, nil, err
}
reservations := new(ipReservationRoot)
resp, err := i.client.Do(req, reservations)
resp, err := i.client.DoRequest("GET", path, nil, reservations)
if err != nil {
return nil, resp, err
}
return reservations.IPReservations, resp, err
return reservations.Reservations, resp, nil
}
// RequestMore requests more IP space for a project in order to have additional IP addresses to assign to devices
func (i *IPReservationServiceOp) RequestMore(projectID string, ipReservationReq *IPReservationRequest) (*IPReservation, *Response, error) {
// Request requests more IP space for a project in order to have additional IP addresses to assign to devices.
func (i *ProjectIPServiceOp) Request(projectID string, ipReservationReq *IPReservationRequest) (*IPAddressReservation, *Response, error) {
path := fmt.Sprintf("%s/%s%s", projectBasePath, projectID, ipBasePath)
ipr := new(IPAddressReservation)
req, err := i.client.NewRequest("POST", path, &ipReservationReq)
if err != nil {
return nil, nil, err
}
ip := new(IPReservation)
resp, err := i.client.Do(req, ip)
resp, err := i.client.DoRequest("POST", path, ipReservationReq, ipr)
if err != nil {
return nil, resp, err
}
return ip, resp, err
}
// Get returns a single IP reservation object
func (i *IPReservationServiceOp) Get(ipReservationID string) (*IPReservation, *Response, error) {
path := fmt.Sprintf("%s/%s", ipBasePath, ipReservationID)
req, err := i.client.NewRequest("GET", path, nil)
if err != nil {
return nil, nil, err
}
reservation := new(IPReservation)
resp, err := i.client.Do(req, reservation)
if err != nil {
return nil, nil, err
}
return reservation, resp, err
return ipr, resp, err
}
// Remove removes an IP reservation from the project.
func (i *IPReservationServiceOp) Remove(ipReservationID string) (*Response, error) {
path := fmt.Sprintf("%s/%s", ipBasePath, ipReservationID)
req, err := i.client.NewRequest("DELETE", path, nil)
if err != nil {
return nil, err
}
resp, err := i.client.Do(req, nil)
if err != nil {
return nil, err
}
return resp, err
func (i *ProjectIPServiceOp) Remove(ipReservationID string) (*Response, error) {
return deleteFromIP(i.client, ipReservationID)
}
// AvailableAddresses lists addresses available from a reserved block
func (i *ProjectIPServiceOp) AvailableAddresses(ipReservationID string, r *AvailableRequest) ([]string, *Response, error) {
path := fmt.Sprintf("%s/%s/available", ipBasePath, ipReservationID)
ar := new(AvailableResponse)
resp, err := i.client.DoRequest("GET", path, r, ar)
if err != nil {
return nil, resp, err
}
return ar.Available, resp, nil
}

View File

@ -30,13 +30,9 @@ type OSServiceOp struct {
// List returns all available operating systems
func (s *OSServiceOp) List() ([]OS, *Response, error) {
req, err := s.client.NewRequest("GET", osBasePath, nil)
if err != nil {
return nil, nil, err
}
root := new(osRoot)
resp, err := s.client.Do(req, root)
resp, err := s.client.DoRequest("GET", osBasePath, nil, root)
if err != nil {
return nil, resp, err
}

View File

@ -6,8 +6,11 @@ import (
"fmt"
"io"
"io/ioutil"
"log"
"net/http"
"net/http/httputil"
"net/url"
"os"
"strconv"
"strings"
"time"
@ -18,6 +21,7 @@ const (
baseURL = "https://api.packet.net/"
userAgent = "packngo/" + libraryVersion
mediaType = "application/json"
debugEnvVar = "PACKNGO_DEBUG"
headerRateLimit = "X-RateLimit-Limit"
headerRateRemaining = "X-RateLimit-Remaining"
@ -42,6 +46,11 @@ type Response struct {
Rate
}
// Href is an API link
type Href struct {
Href string `json:"href"`
}
func (r *Response) populateRate() {
// parse the rate limit headers and populate Response.Rate
if limit := r.Header.Get(headerRateLimit); limit != "" {
@ -59,18 +68,20 @@ func (r *Response) populateRate() {
// ErrorResponse is the http response used on errrors
type ErrorResponse struct {
Response *http.Response
Errors []string `json:"errors"`
Response *http.Response
Errors []string `json:"errors"`
SingleError string `json:"error"`
}
func (r *ErrorResponse) Error() string {
return fmt.Sprintf("%v %v: %d %v",
r.Response.Request.Method, r.Response.Request.URL, r.Response.StatusCode, strings.Join(r.Errors, ", "))
return fmt.Sprintf("%v %v: %d %v %v",
r.Response.Request.Method, r.Response.Request.URL, r.Response.StatusCode, strings.Join(r.Errors, ", "), r.SingleError)
}
// Client is the base API Client
type Client struct {
client *http.Client
debug bool
BaseURL *url.URL
@ -81,17 +92,19 @@ type Client struct {
RateLimit Rate
// Packet Api Objects
Plans PlanService
Users UserService
Emails EmailService
SSHKeys SSHKeyService
Devices DeviceService
Projects ProjectService
Facilities FacilityService
OperatingSystems OSService
Ips IPService
IpReservations IPReservationService
Volumes VolumeService
Plans PlanService
Users UserService
Emails EmailService
SSHKeys SSHKeyService
Devices DeviceService
Projects ProjectService
Facilities FacilityService
OperatingSystems OSService
DeviceIPs DeviceIPService
ProjectIPs ProjectIPService
Volumes VolumeService
VolumeAttachments VolumeAttachmentService
SpotMarket SpotMarketService
}
// NewRequest inits a new http request with the proper headers
@ -140,6 +153,10 @@ func (c *Client) Do(req *http.Request, v interface{}) (*Response, error) {
response := Response{Response: resp}
response.populateRate()
if c.debug {
o, _ := httputil.DumpResponse(response.Response, true)
log.Printf("%s\n", string(o))
}
c.RateLimit = response.Rate
err = checkResponse(resp)
@ -163,6 +180,19 @@ func (c *Client) Do(req *http.Request, v interface{}) (*Response, error) {
return &response, err
}
// DoRequest is a convenience method, it calls NewRequest follwed by Do
func (c *Client) DoRequest(method, path string, body, v interface{}) (*Response, error) {
req, err := c.NewRequest(method, path, body)
if c.debug {
o, _ := httputil.DumpRequestOut(req, true)
log.Printf("%s\n", string(o))
}
if err != nil {
return nil, err
}
return c.Do(req, v)
}
// NewClient initializes and returns a Client, use this to get an API Client to operate on
// N.B.: Packet's API certificate requires Go 1.5+ to successfully parse. If you are using
// an older version of Go, pass in a custom http.Client with a custom TLS configuration
@ -171,6 +201,9 @@ func NewClient(consumerToken string, apiKey string, httpClient *http.Client) *Cl
client, _ := NewClientWithBaseURL(consumerToken, apiKey, httpClient, baseURL)
return client
}
// NewClientWithBaseURL returns a Client pointing to nonstandard API URL, e.g.
// for mocking the remote API
func NewClientWithBaseURL(consumerToken string, apiKey string, httpClient *http.Client, apiBaseURL string) (*Client, error) {
if httpClient == nil {
// Don't fall back on http.DefaultClient as it's not nice to adjust state
@ -185,6 +218,7 @@ func NewClientWithBaseURL(consumerToken string, apiKey string, httpClient *http.
}
c := &Client{client: httpClient, BaseURL: u, UserAgent: userAgent, ConsumerToken: consumerToken, APIKey: apiKey}
c.debug = os.Getenv(debugEnvVar) != ""
c.Plans = &PlanServiceOp{client: c}
c.Users = &UserServiceOp{client: c}
c.Emails = &EmailServiceOp{client: c}
@ -193,9 +227,11 @@ func NewClientWithBaseURL(consumerToken string, apiKey string, httpClient *http.
c.Projects = &ProjectServiceOp{client: c}
c.Facilities = &FacilityServiceOp{client: c}
c.OperatingSystems = &OSServiceOp{client: c}
c.Ips = &IPServiceOp{client: c}
c.IpReservations = &IPReservationServiceOp{client: c}
c.DeviceIPs = &DeviceIPServiceOp{client: c}
c.ProjectIPs = &ProjectIPServiceOp{client: c}
c.Volumes = &VolumeServiceOp{client: c}
c.VolumeAttachments = &VolumeAttachmentServiceOp{client: c}
c.SpotMarket = &SpotMarketServiceOp{client: c}
return c, nil
}

View File

@ -106,14 +106,9 @@ type PlanServiceOp struct {
// List method returns all available plans
func (s *PlanServiceOp) List() ([]Plan, *Response, error) {
req, err := s.client.NewRequest("GET", planBasePath, nil)
if err != nil {
return nil, nil, err
}
root := new(planRoot)
resp, err := s.client.Do(req, root)
resp, err := s.client.DoRequest("GET", planBasePath, nil, root)
if err != nil {
return nil, resp, err
}

View File

@ -11,14 +11,9 @@ type ProjectService interface {
Create(*ProjectCreateRequest) (*Project, *Response, error)
Update(*ProjectUpdateRequest) (*Project, *Response, error)
Delete(string) (*Response, error)
ListIPAddresses(string) ([]IPAddress, *Response, error)
ListVolumes(string) ([]Volume, *Response, error)
}
type ipsRoot struct {
IPAddresses []IPAddress `json:"ip_addresses"`
}
type volumesRoot struct {
Volumes []Volume `json:"volumes"`
}
@ -69,31 +64,11 @@ type ProjectServiceOp struct {
client *Client
}
func (s *ProjectServiceOp) ListIPAddresses(projectID string) ([]IPAddress, *Response, error) {
url := fmt.Sprintf("%s/%s/ips", projectBasePath, projectID)
req, err := s.client.NewRequest("GET", url, nil)
if err != nil {
return nil, nil, err
}
root := new(ipsRoot)
resp, err := s.client.Do(req, root)
if err != nil {
return nil, resp, err
}
return root.IPAddresses, resp, err
}
// List returns the user's projects
func (s *ProjectServiceOp) List() ([]Project, *Response, error) {
req, err := s.client.NewRequest("GET", projectBasePath, nil)
if err != nil {
return nil, nil, err
}
root := new(projectsRoot)
resp, err := s.client.Do(req, root)
resp, err := s.client.DoRequest("GET", projectBasePath, nil, root)
if err != nil {
return nil, resp, err
}
@ -104,13 +79,9 @@ func (s *ProjectServiceOp) List() ([]Project, *Response, error) {
// Get returns a project by id
func (s *ProjectServiceOp) Get(projectID string) (*Project, *Response, error) {
path := fmt.Sprintf("%s/%s", projectBasePath, projectID)
req, err := s.client.NewRequest("GET", path, nil)
if err != nil {
return nil, nil, err
}
project := new(Project)
resp, err := s.client.Do(req, project)
resp, err := s.client.DoRequest("GET", path, nil, project)
if err != nil {
return nil, resp, err
}
@ -120,13 +91,9 @@ func (s *ProjectServiceOp) Get(projectID string) (*Project, *Response, error) {
// Create creates a new project
func (s *ProjectServiceOp) Create(createRequest *ProjectCreateRequest) (*Project, *Response, error) {
req, err := s.client.NewRequest("POST", projectBasePath, createRequest)
if err != nil {
return nil, nil, err
}
project := new(Project)
resp, err := s.client.Do(req, project)
resp, err := s.client.DoRequest("POST", projectBasePath, createRequest, project)
if err != nil {
return nil, resp, err
}
@ -137,13 +104,9 @@ func (s *ProjectServiceOp) Create(createRequest *ProjectCreateRequest) (*Project
// Update updates a project
func (s *ProjectServiceOp) Update(updateRequest *ProjectUpdateRequest) (*Project, *Response, error) {
path := fmt.Sprintf("%s/%s", projectBasePath, updateRequest.ID)
req, err := s.client.NewRequest("PATCH", path, updateRequest)
if err != nil {
return nil, nil, err
}
project := new(Project)
resp, err := s.client.Do(req, project)
resp, err := s.client.DoRequest("PATCH", path, updateRequest, project)
if err != nil {
return nil, resp, err
}
@ -155,26 +118,15 @@ func (s *ProjectServiceOp) Update(updateRequest *ProjectUpdateRequest) (*Project
func (s *ProjectServiceOp) Delete(projectID string) (*Response, error) {
path := fmt.Sprintf("%s/%s", projectBasePath, projectID)
req, err := s.client.NewRequest("DELETE", path, nil)
if err != nil {
return nil, err
}
resp, err := s.client.Do(req, nil)
return resp, err
return s.client.DoRequest("DELETE", path, nil, nil)
}
// List returns Volumes for a project
// ListVolumes returns Volumes for a project
func (s *ProjectServiceOp) ListVolumes(projectID string) ([]Volume, *Response, error) {
url := fmt.Sprintf("%s/%s%s", projectBasePath, projectID, volumeBasePath)
req, err := s.client.NewRequest("GET", url, nil)
if err != nil {
return nil, nil, err
}
root := new(volumesRoot)
resp, err := s.client.Do(req, root)
resp, err := s.client.DoRequest("GET", url, nil, root)
if err != nil {
return nil, resp, err
}

View File

@ -0,0 +1,39 @@
package packngo
const spotMarketBasePath = "/market/spot/prices"
// SpotMarketService expooses Spot Market methods
type SpotMarketService interface {
Prices() (PriceMap, *Response, error)
}
// SpotMarketServiceOp implements SpotMarketService
type SpotMarketServiceOp struct {
client *Client
}
// PriceMap is a map of [facility][plan]-> float Price
type PriceMap map[string]map[string]float64
// Prices gets current PriceMap from the API
func (s *SpotMarketServiceOp) Prices() (PriceMap, *Response, error) {
root := new(struct {
SMPs map[string]map[string]struct {
Price float64 `json:"price"`
} `json:"spot_market_prices"`
})
resp, err := s.client.DoRequest("GET", spotMarketBasePath, nil, root)
if err != nil {
return nil, resp, err
}
prices := make(PriceMap)
for facility, planMap := range root.SMPs {
prices[facility] = map[string]float64{}
for plan, v := range planMap {
prices[facility][plan] = v.Price
}
}
return prices, resp, err
}

View File

@ -2,11 +2,14 @@ package packngo
import "fmt"
const sshKeyBasePath = "/ssh-keys"
const (
sshKeyBasePath = "/ssh-keys"
)
// SSHKeyService interface defines available device methods
type SSHKeyService interface {
List() ([]SSHKey, *Response, error)
ProjectList(string) ([]SSHKey, *Response, error)
Get(string) (*SSHKey, *Response, error)
Create(*SSHKeyCreateRequest) (*SSHKey, *Response, error)
Update(*SSHKeyUpdateRequest) (*SSHKey, *Response, error)
@ -60,15 +63,10 @@ type SSHKeyServiceOp struct {
client *Client
}
// List returns a user's ssh keys
func (s *SSHKeyServiceOp) List() ([]SSHKey, *Response, error) {
req, err := s.client.NewRequest("GET", sshKeyBasePath, nil)
if err != nil {
return nil, nil, err
}
func (s *SSHKeyServiceOp) list(url string) ([]SSHKey, *Response, error) {
root := new(sshKeyRoot)
resp, err := s.client.Do(req, root)
resp, err := s.client.DoRequest("GET", url, nil, root)
if err != nil {
return nil, resp, err
}
@ -76,17 +74,23 @@ func (s *SSHKeyServiceOp) List() ([]SSHKey, *Response, error) {
return root.SSHKeys, resp, err
}
// ProjectList lists ssh keys of a project
func (s *SSHKeyServiceOp) ProjectList(projectID string) ([]SSHKey, *Response, error) {
return s.list(fmt.Sprintf("%s/%s%s", projectBasePath, projectID, sshKeyBasePath))
}
// List returns a user's ssh keys
func (s *SSHKeyServiceOp) List() ([]SSHKey, *Response, error) {
return s.list(sshKeyBasePath)
}
// Get returns an ssh key by id
func (s *SSHKeyServiceOp) Get(sshKeyID string) (*SSHKey, *Response, error) {
path := fmt.Sprintf("%s/%s", sshKeyBasePath, sshKeyID)
req, err := s.client.NewRequest("GET", path, nil)
if err != nil {
return nil, nil, err
}
sshKey := new(SSHKey)
resp, err := s.client.Do(req, sshKey)
resp, err := s.client.DoRequest("GET", path, nil, sshKey)
if err != nil {
return nil, resp, err
}
@ -98,15 +102,11 @@ func (s *SSHKeyServiceOp) Get(sshKeyID string) (*SSHKey, *Response, error) {
func (s *SSHKeyServiceOp) Create(createRequest *SSHKeyCreateRequest) (*SSHKey, *Response, error) {
path := sshKeyBasePath
if createRequest.ProjectID != "" {
path = "/projects/" + createRequest.ProjectID + sshKeyBasePath
path = fmt.Sprintf("%s/%s%s", projectBasePath, createRequest.ProjectID, sshKeyBasePath)
}
req, err := s.client.NewRequest("POST", path, createRequest)
if err != nil {
return nil, nil, err
}
sshKey := new(SSHKey)
resp, err := s.client.Do(req, sshKey)
resp, err := s.client.DoRequest("POST", path, createRequest, sshKey)
if err != nil {
return nil, resp, err
}
@ -117,13 +117,9 @@ func (s *SSHKeyServiceOp) Create(createRequest *SSHKeyCreateRequest) (*SSHKey, *
// Update updates an ssh key
func (s *SSHKeyServiceOp) Update(updateRequest *SSHKeyUpdateRequest) (*SSHKey, *Response, error) {
path := fmt.Sprintf("%s/%s", sshKeyBasePath, updateRequest.ID)
req, err := s.client.NewRequest("PATCH", path, updateRequest)
if err != nil {
return nil, nil, err
}
sshKey := new(SSHKey)
resp, err := s.client.Do(req, sshKey)
resp, err := s.client.DoRequest("PATCH", path, updateRequest, sshKey)
if err != nil {
return nil, resp, err
}
@ -135,12 +131,5 @@ func (s *SSHKeyServiceOp) Update(updateRequest *SSHKeyUpdateRequest) (*SSHKey, *
func (s *SSHKeyServiceOp) Delete(sshKeyID string) (*Response, error) {
path := fmt.Sprintf("%s/%s", sshKeyBasePath, sshKeyID)
req, err := s.client.NewRequest("DELETE", path, nil)
if err != nil {
return nil, err
}
resp, err := s.client.Do(req, nil)
return resp, err
return s.client.DoRequest("DELETE", path, nil, nil)
}

View File

@ -22,7 +22,7 @@ type User struct {
Created string `json:"created_at,omitempty"`
Updated string `json:"updated_at,omitempty"`
TimeZone string `json:"timezone,omitempty"`
Emails []Email `json:"email,omitempty"`
Emails []Email `json:"emails,omitempty"`
PhoneNumber string `json:"phone_number,omitempty"`
URL string `json:"href,omitempty"`
}
@ -38,13 +38,9 @@ type UserServiceOp struct {
// Get method gets a user by userID
func (s *UserServiceOp) Get(userID string) (*User, *Response, error) {
req, err := s.client.NewRequest("GET", userBasePath, nil)
if err != nil {
return nil, nil, err
}
user := new(User)
resp, err := s.client.Do(req, user)
resp, err := s.client.DoRequest("GET", userBasePath, nil, user)
if err != nil {
return nil, resp, err
}

View File

@ -17,27 +17,6 @@ func Stringify(message interface{}) string {
return buf.String()
}
// String allocates a new string value to store v and returns a pointer to it
func String(v string) *string {
p := new(string)
*p = v
return p
}
// Int allocates a new int32 value to store v and returns a pointer to it, but unlike Int32 its argument value is an int.
func Int(v int) *int {
p := new(int)
*p = v
return p
}
// Bool allocates a new bool value to store v and returns a pointer to it.
func Bool(v bool) *bool {
p := new(bool)
*p = v
return p
}
// StreamToString converts a reader to a string
func StreamToString(stream io.Reader) string {
buf := new(bytes.Buffer)

View File

@ -2,33 +2,43 @@ package packngo
import "fmt"
const volumeBasePath = "/storage"
const (
volumeBasePath = "/storage"
attachmentsBasePath = "/attachments"
)
// VolumeService interface defines available Volume methods
type VolumeService interface {
Get(string) (*Volume, *Response, error)
Update(*VolumeUpdateRequest) (*Volume, *Response, error)
Delete(string) (*Response, error)
Create(*VolumeCreateRequest) (*Volume, *Response, error)
Create(*VolumeCreateRequest, string) (*Volume, *Response, error)
}
// VolumeAttachmentService defines attachment methdods
type VolumeAttachmentService interface {
Get(string) (*VolumeAttachment, *Response, error)
Create(string, string) (*VolumeAttachment, *Response, error)
Delete(string) (*Response, error)
}
// Volume represents a volume
type Volume struct {
ID string `json:"id"`
Name string `json:"name,omitempty"`
Description string `json:"description,omitempty"`
Size int `json:"size,omitempty"`
State string `json:"state,omitempty"`
Locked bool `json:"locked,omitempty"`
BillingCycle string `json:"billing_cycle,omitempty"`
Created string `json:"created_at,omitempty"`
Updated string `json:"updated_at,omitempty"`
Href string `json:"href,omitempty"`
SnapshotPolicies []*SnapshotPolicy `json:"snapshot_policies,omitempty"`
Attachments []*Attachment `json:"attachments,omitempty"`
Plan *Plan `json:"plan,omitempty"`
Facility *Facility `json:"facility,omitempty"`
Project *Project `json:"project,omitempty"`
ID string `json:"id"`
Name string `json:"name,omitempty"`
Description string `json:"description,omitempty"`
Size int `json:"size,omitempty"`
State string `json:"state,omitempty"`
Locked bool `json:"locked,omitempty"`
BillingCycle string `json:"billing_cycle,omitempty"`
Created string `json:"created_at,omitempty"`
Updated string `json:"updated_at,omitempty"`
Href string `json:"href,omitempty"`
SnapshotPolicies []*SnapshotPolicy `json:"snapshot_policies,omitempty"`
Attachments []*VolumeAttachment `json:"attachments,omitempty"`
Plan *Plan `json:"plan,omitempty"`
Facility *Facility `json:"facility,omitempty"`
Project *Project `json:"project,omitempty"`
}
// SnapshotPolicy used to execute actions on volume
@ -39,12 +49,6 @@ type SnapshotPolicy struct {
SnapshotCount int `json:"snapshot_count,omitempty"`
}
// Attachment used to execute actions on volume
type Attachment struct {
ID string `json:"id"`
Href string `json:"href"`
}
func (v Volume) String() string {
return Stringify(v)
}
@ -71,10 +75,23 @@ type VolumeUpdateRequest struct {
Plan string `json:"plan,omitempty"`
}
// VolumeAttachment is a type from Packet API
type VolumeAttachment struct {
Href string `json:"href"`
ID string `json:"id"`
Volume Volume `json:"volume"`
Device Device `json:"device"`
}
func (v VolumeUpdateRequest) String() string {
return Stringify(v)
}
// VolumeAttachmentServiceOp implements VolumeService
type VolumeAttachmentServiceOp struct {
client *Client
}
// VolumeServiceOp implements VolumeService
type VolumeServiceOp struct {
client *Client
@ -83,13 +100,9 @@ type VolumeServiceOp struct {
// Get returns a volume by id
func (v *VolumeServiceOp) Get(volumeID string) (*Volume, *Response, error) {
path := fmt.Sprintf("%s/%s?include=facility,snapshot_policies,attachments.device", volumeBasePath, volumeID)
req, err := v.client.NewRequest("GET", path, nil)
if err != nil {
return nil, nil, err
}
volume := new(Volume)
resp, err := v.client.Do(req, volume)
resp, err := v.client.DoRequest("GET", path, nil, volume)
if err != nil {
return nil, resp, err
}
@ -100,13 +113,9 @@ func (v *VolumeServiceOp) Get(volumeID string) (*Volume, *Response, error) {
// Update updates a volume
func (v *VolumeServiceOp) Update(updateRequest *VolumeUpdateRequest) (*Volume, *Response, error) {
path := fmt.Sprintf("%s/%s", volumeBasePath, updateRequest.ID)
req, err := v.client.NewRequest("PATCH", path, updateRequest)
if err != nil {
return nil, nil, err
}
volume := new(Volume)
resp, err := v.client.Do(req, volume)
resp, err := v.client.DoRequest("PATCH", path, updateRequest, volume)
if err != nil {
return nil, resp, err
}
@ -118,29 +127,55 @@ func (v *VolumeServiceOp) Update(updateRequest *VolumeUpdateRequest) (*Volume, *
func (v *VolumeServiceOp) Delete(volumeID string) (*Response, error) {
path := fmt.Sprintf("%s/%s", volumeBasePath, volumeID)
req, err := v.client.NewRequest("DELETE", path, nil)
if err != nil {
return nil, err
}
resp, err := v.client.Do(req, nil)
return resp, err
return v.client.DoRequest("DELETE", path, nil, nil)
}
// Create creates a new volume for a project
func (v *VolumeServiceOp) Create(createRequest *VolumeCreateRequest) (*Volume, *Response, error) {
url := fmt.Sprintf("%s/%s%s", projectBasePath, createRequest.ProjectID, volumeBasePath)
req, err := v.client.NewRequest("POST", url, createRequest)
if err != nil {
return nil, nil, err
}
func (v *VolumeServiceOp) Create(createRequest *VolumeCreateRequest, projectID string) (*Volume, *Response, error) {
url := fmt.Sprintf("%s/%s%s", projectBasePath, projectID, volumeBasePath)
volume := new(Volume)
resp, err := v.client.Do(req, volume)
resp, err := v.client.DoRequest("POST", url, createRequest, volume)
if err != nil {
return nil, resp, err
}
return volume, resp, err
}
// Attachments
// Create Attachment, i.e. attach volume to a device
func (v *VolumeAttachmentServiceOp) Create(volumeID, deviceID string) (*VolumeAttachment, *Response, error) {
url := fmt.Sprintf("%s/%s%s", volumeBasePath, volumeID, attachmentsBasePath)
volAttachParam := map[string]string{
"device_id": deviceID,
}
volumeAttachment := new(VolumeAttachment)
resp, err := v.client.DoRequest("POST", url, volAttachParam, volumeAttachment)
if err != nil {
return nil, resp, err
}
return volumeAttachment, resp, nil
}
// Get gets attachment by id
func (v *VolumeAttachmentServiceOp) Get(attachmentID string) (*VolumeAttachment, *Response, error) {
path := fmt.Sprintf("%s%s/%s", volumeBasePath, attachmentsBasePath, attachmentID)
volumeAttachment := new(VolumeAttachment)
resp, err := v.client.DoRequest("GET", path, nil, volumeAttachment)
if err != nil {
return nil, resp, err
}
return volumeAttachment, resp, nil
}
// Delete deletes attachment by id
func (v *VolumeAttachmentServiceOp) Delete(attachmentID string) (*Response, error) {
path := fmt.Sprintf("%s%s/%s", volumeBasePath, attachmentsBasePath, attachmentID)
return v.client.DoRequest("DELETE", path, nil, nil)
}