diff --git a/builder/hyperone/artifact.go b/builder/hyperone/artifact.go new file mode 100644 index 000000000..1436637c1 --- /dev/null +++ b/builder/hyperone/artifact.go @@ -0,0 +1,48 @@ +package hyperone + +import ( + "context" + "fmt" + + "github.com/hyperonecom/h1-client-go" +) + +type Artifact struct { + imageName string + imageID string + client *openapi.APIClient +} + +func (a *Artifact) BuilderId() string { + return BuilderID +} + +func (a *Artifact) Files() []string { + return nil +} + +func (a *Artifact) Id() string { + return a.imageID +} + +func (a *Artifact) String() string { + return fmt.Sprintf("Image '%s' created, ID: %s", a.imageName, a.imageID) +} + +func (a *Artifact) State(name string) interface{} { + return nil +} + +func (a *Artifact) Destroy() error { + if a.imageID == "" { + // No image to destroy + return nil + } + + _, err := a.client.ImageApi.ImageDelete(context.TODO(), a.imageID) + if err != nil { + return err + } + + return nil +} diff --git a/builder/hyperone/builder.go b/builder/hyperone/builder.go new file mode 100644 index 000000000..8858d01eb --- /dev/null +++ b/builder/hyperone/builder.go @@ -0,0 +1,85 @@ +package hyperone + +import ( + "fmt" + "log" + + "github.com/hashicorp/packer/common" + "github.com/hashicorp/packer/helper/communicator" + "github.com/hashicorp/packer/helper/multistep" + "github.com/hashicorp/packer/packer" + "github.com/hyperonecom/h1-client-go" +) + +const BuilderID = "hyperone.builder" + +type Builder struct { + config Config + runner multistep.Runner + client *openapi.APIClient +} + +func (b *Builder) Prepare(raws ...interface{}) ([]string, error) { + config, warnings, errs := NewConfig(raws...) + if errs != nil { + return warnings, errs + } + + b.config = *config + + cfg := openapi.NewConfiguration() + cfg.AddDefaultHeader("x-auth-token", b.config.Token) + if b.config.Project != "" { + cfg.AddDefaultHeader("x-project", b.config.Project) + } + + prefer := fmt.Sprintf("respond-async,wait=%d", int(b.config.StateTimeout.Seconds())) + cfg.AddDefaultHeader("Prefer", prefer) + + b.client = openapi.NewAPIClient(cfg) + + return nil, nil +} + +func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packer.Artifact, error) { + state := &multistep.BasicStateBag{} + state.Put("config", &b.config) + state.Put("client", b.client) + state.Put("hook", hook) + state.Put("ui", ui) + + steps := []multistep.Step{ + &stepCreateSSHKey{}, + &stepCreateVM{}, + &communicator.StepConnect{ + Config: &b.config.Comm, + Host: getPublicIP, + SSHConfig: b.config.Comm.SSHConfigFunc(), + }, + &common.StepProvision{}, + &stepStopVM{}, + &stepCreateImage{}, + } + + b.runner = common.NewRunner(steps, b.config.PackerConfig, ui) + b.runner.Run(state) + + if rawErr, ok := state.GetOk("error"); ok { + return nil, rawErr.(error) + } + + artifact := &Artifact{ + imageID: state.Get("image_id").(string), + imageName: state.Get("image_name").(string), + client: b.client, + } + + return artifact, nil +} + +func (b *Builder) Cancel() { + if b.runner != nil { + log.Println("Cancelling the runner...") + b.runner.Cancel() + } +} diff --git a/builder/hyperone/config.go b/builder/hyperone/config.go new file mode 100644 index 000000000..2a943e490 --- /dev/null +++ b/builder/hyperone/config.go @@ -0,0 +1,208 @@ +package hyperone + +import ( + "errors" + "fmt" + "io/ioutil" + "os" + "time" + + "github.com/hashicorp/packer/common" + "github.com/hashicorp/packer/common/json" + "github.com/hashicorp/packer/common/uuid" + "github.com/hashicorp/packer/helper/communicator" + "github.com/hashicorp/packer/helper/config" + "github.com/hashicorp/packer/helper/multistep" + "github.com/hashicorp/packer/packer" + "github.com/hashicorp/packer/template/interpolate" + "github.com/mitchellh/go-homedir" + "github.com/mitchellh/mapstructure" +) + +const ( + configPath = "~/.h1-cli/conf.json" + tokenEnv = "HYPERONE_TOKEN" + + defaultDiskType = "ssd" + defaultImageService = "564639bc052c084e2f2e3266" + defaultStateTimeout = 5 * time.Minute + defaultUserName = "guru" +) + +type Config struct { + common.PackerConfig `mapstructure:",squash"` + Comm communicator.Config `mapstructure:",squash"` + + Token string `mapstructure:"token"` + Project string `mapstructure:"project"` + TokenLogin string `mapstructure:"token_login"` + + StateTimeout time.Duration `mapstructure:"state_timeout"` + + SourceImage string `mapstructure:"source_image"` + ImageName string `mapstructure:"image_name"` + ImageDescription string `mapstructure:"image_description"` + ImageTags map[string]interface{} `mapstructure:"image_tags"` + ImageService string `mapstructure:"image_service"` + + VmFlavour string `mapstructure:"vm_flavour"` + VmName string `mapstructure:"vm_name"` + VmTags map[string]interface{} `mapstructure:"vm_tags"` + + DiskName string `mapstructure:"disk_name"` + DiskType string `mapstructure:"disk_type"` + DiskSize float32 `mapstructure:"disk_size"` + + Network string `mapstructure:"network"` + PrivateIP string `mapstructure:"private_ip"` + PublicIP string `mapstructure:"public_ip"` + + SSHKeys []string `mapstructure:"ssh_keys"` + UserData string `mapstructure:"user_data"` + + ctx interpolate.Context +} + +func NewConfig(raws ...interface{}) (*Config, []string, error) { + c := &Config{} + + var md mapstructure.Metadata + err := config.Decode(c, &config.DecodeOpts{ + Metadata: &md, + Interpolate: true, + InterpolateContext: &c.ctx, + InterpolateFilter: &interpolate.RenderFilter{ + Exclude: []string{ + "run_command", + }, + }, + }, raws...) + if err != nil { + return nil, nil, err + } + + cliConfig, err := loadCLIConfig() + if err != nil { + return nil, nil, err + } + + // Defaults + if c.Comm.SSHUsername == "" { + c.Comm.SSHUsername = defaultUserName + } + + if c.Comm.SSHTimeout == 0 { + c.Comm.SSHTimeout = 10 * time.Minute + } + + if c.Token == "" { + c.Token = os.Getenv(tokenEnv) + + if c.Token == "" { + c.Token = cliConfig.Profile.APIKey + } + + if c.TokenLogin != "" { + c.Token, err = fetchTokenBySSH(c.TokenLogin) + if err != nil { + return nil, nil, err + } + } + } + + if c.Project == "" { + c.Project = cliConfig.Profile.Project.ID + } + + if c.StateTimeout == 0 { + c.StateTimeout = defaultStateTimeout + } + + if c.ImageName == "" { + name, err := interpolate.Render("packer-{{timestamp}}", nil) + if err != nil { + return nil, nil, err + } + c.ImageName = name + } + + if c.ImageService == "" { + c.ImageService = defaultImageService + } + + if c.VmName == "" { + c.VmName = fmt.Sprintf("packer-%s", uuid.TimeOrderedUUID()) + } + + if c.DiskType == "" { + c.DiskType = defaultDiskType + } + + var errs *packer.MultiError + if es := c.Comm.Prepare(&c.ctx); len(es) > 0 { + errs = packer.MultiErrorAppend(errs, es...) + } + + if c.Token == "" { + errs = packer.MultiErrorAppend(errs, errors.New("token is required")) + } + + if c.VmFlavour == "" { + errs = packer.MultiErrorAppend(errs, errors.New("vm flavour is required")) + } + + if c.DiskSize == 0 { + errs = packer.MultiErrorAppend(errs, errors.New("disk size is required")) + } + + if c.SourceImage == "" { + errs = packer.MultiErrorAppend(errs, errors.New("source image is required")) + } + + if errs != nil && len(errs.Errors) > 0 { + return nil, nil, errs + } + + packer.LogSecretFilter.Set(c.Token) + + return c, nil, nil +} + +type cliConfig struct { + Profile struct { + APIKey string `json:"apiKey"` + Project struct { + ID string `json:"_id"` + } `json:"project"` + } `json:"profile"` +} + +func loadCLIConfig() (cliConfig, error) { + path, err := homedir.Expand(configPath) + if err != nil { + return cliConfig{}, err + } + + _, err = os.Stat(path) + if err != nil { + // Config not found + return cliConfig{}, nil + } + + content, err := ioutil.ReadFile(path) + if err != nil { + return cliConfig{}, err + } + + var c cliConfig + err = json.Unmarshal(content, &c) + if err != nil { + return cliConfig{}, err + } + + return c, nil +} + +func getPublicIP(state multistep.StateBag) (string, error) { + return state.Get("public_ip").(string), nil +} diff --git a/builder/hyperone/step_create_image.go b/builder/hyperone/step_create_image.go new file mode 100644 index 000000000..3bb4c414d --- /dev/null +++ b/builder/hyperone/step_create_image.go @@ -0,0 +1,42 @@ +package hyperone + +import ( + "context" + "fmt" + + "github.com/hashicorp/packer/helper/multistep" + "github.com/hashicorp/packer/packer" + "github.com/hyperonecom/h1-client-go" +) + +type stepCreateImage struct{} + +func (s *stepCreateImage) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction { + client := state.Get("client").(*openapi.APIClient) + ui := state.Get("ui").(packer.Ui) + config := state.Get("config").(*Config) + vmID := state.Get("vm_id").(string) + + ui.Say("Creating image...") + + image, _, err := client.ImageApi.ImageCreate(ctx, openapi.ImageCreate{ + Name: config.ImageName, + Vm: vmID, + Service: config.ImageService, + Description: config.ImageDescription, + Tag: config.ImageTags, + }) + if err != nil { + err := fmt.Errorf("error creating image: %s", formatOpenAPIError(err)) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + + state.Put("image_id", image.Id) + state.Put("image_name", image.Name) + + return multistep.ActionContinue +} + +func (s *stepCreateImage) Cleanup(state multistep.StateBag) {} diff --git a/builder/hyperone/step_create_ssh_key.go b/builder/hyperone/step_create_ssh_key.go new file mode 100644 index 000000000..15dd34f2a --- /dev/null +++ b/builder/hyperone/step_create_ssh_key.go @@ -0,0 +1,82 @@ +package hyperone + +import ( + "context" + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "encoding/pem" + "fmt" + "os" + "runtime" + + "github.com/hashicorp/packer/helper/multistep" + "github.com/hashicorp/packer/packer" + "golang.org/x/crypto/ssh" +) + +type stepCreateSSHKey struct { + Debug bool + DebugKeyPath string +} + +func (s *stepCreateSSHKey) Run(_ context.Context, state multistep.StateBag) multistep.StepAction { + ui := state.Get("ui").(packer.Ui) + c := state.Get("config").(*Config) + ui.Say("Creating a temporary ssh key for the VM...") + + priv, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + state.Put("error", fmt.Errorf("error generating ssh key: %s", err)) + return multistep.ActionHalt + } + + privDER := x509.MarshalPKCS1PrivateKey(priv) + privBLK := pem.Block{ + Type: "RSA PRIVATE KEY", + Headers: nil, + Bytes: privDER, + } + + c.Comm.SSHPrivateKey = pem.EncodeToMemory(&privBLK) + + pub, err := ssh.NewPublicKey(&priv.PublicKey) + if err != nil { + state.Put("error", fmt.Errorf("error getting public key: %s", err)) + return multistep.ActionHalt + } + + pubSSHFormat := string(ssh.MarshalAuthorizedKey(pub)) + + // Remember public SSH key for future connections + state.Put("ssh_public_key", pubSSHFormat) + + // If we're in debug mode, output the private key to the working directory. + if s.Debug { + ui.Message(fmt.Sprintf("Saving key for debug purposes: %s", s.DebugKeyPath)) + f, err := os.Create(s.DebugKeyPath) + if err != nil { + state.Put("error", fmt.Errorf("error saving debug key: %s", err)) + return multistep.ActionHalt + } + defer f.Close() + + // Write the key out + if _, err := f.Write(pem.EncodeToMemory(&privBLK)); err != nil { + state.Put("error", fmt.Errorf("error saving debug key: %s", err)) + return multistep.ActionHalt + } + + // Chmod it so that it is SSH ready + if runtime.GOOS != "windows" { + if err := f.Chmod(0600); err != nil { + state.Put("error", fmt.Errorf("error setting permissions of debug key: %s", err)) + return multistep.ActionHalt + } + } + } + + return multistep.ActionContinue +} + +func (s *stepCreateSSHKey) Cleanup(state multistep.StateBag) {} diff --git a/builder/hyperone/step_create_vm.go b/builder/hyperone/step_create_vm.go new file mode 100644 index 000000000..eafcf8a98 --- /dev/null +++ b/builder/hyperone/step_create_vm.go @@ -0,0 +1,169 @@ +package hyperone + +import ( + "context" + "fmt" + + "github.com/hashicorp/packer/helper/multistep" + "github.com/hashicorp/packer/packer" + "github.com/hyperonecom/h1-client-go" +) + +type stepCreateVM struct { + vmID string +} + +func (s *stepCreateVM) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction { + client := state.Get("client").(*openapi.APIClient) + ui := state.Get("ui").(packer.Ui) + config := state.Get("config").(*Config) + sshKey := state.Get("ssh_public_key").(string) + + ui.Say("Creating VM...") + + netAdapter := pickNetAdapter(config) + + var sshKeys = []string{sshKey} + sshKeys = append(sshKeys, config.SSHKeys...) + + options := openapi.VmCreate{ + Name: config.VmName, + Image: config.SourceImage, + Service: config.VmFlavour, + SshKeys: sshKeys, + Disk: []openapi.VmCreateDisk{ + { + Service: config.DiskType, + Size: config.DiskSize, + }, + }, + Netadp: []openapi.VmCreateNetadp{netAdapter}, + UserMetadata: config.UserData, + Tag: config.VmTags, + } + + vm, _, err := client.VmApi.VmCreate(ctx, options) + if err != nil { + err := fmt.Errorf("error creating VM: %s", formatOpenAPIError(err)) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + + s.vmID = vm.Id + state.Put("vm_id", vm.Id) + + hdds, _, err := client.VmApi.VmListHdd(ctx, vm.Id) + if err != nil { + err := fmt.Errorf("error listing hdd: %s", formatOpenAPIError(err)) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + + var diskIDs []string + for _, hdd := range hdds { + diskIDs = append(diskIDs, hdd.Disk.Id) + } + + state.Put("disk_ids", diskIDs) + + netadp, _, err := client.VmApi.VmListNetadp(ctx, vm.Id) + if err != nil { + err := fmt.Errorf("error listing netadp: %s", formatOpenAPIError(err)) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + + if len(netadp) < 1 { + err := fmt.Errorf("no network adapters found") + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + + publicIP, err := associatePublicIP(ctx, config, client, netadp[0]) + if err != nil { + err := fmt.Errorf("error associating IP: %s", formatOpenAPIError(err)) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + + state.Put("public_ip", publicIP) + + return multistep.ActionContinue +} + +func pickNetAdapter(config *Config) openapi.VmCreateNetadp { + if config.Network == "" { + if config.PublicIP != "" { + return openapi.VmCreateNetadp{ + Service: "public", + Ip: []string{config.PublicIP}, + } + } + } else { + var privateIPs []string + + if config.PrivateIP == "" { + privateIPs = nil + } else { + privateIPs = []string{config.PrivateIP} + } + + return openapi.VmCreateNetadp{ + Service: "private", + Network: config.Network, + Ip: privateIPs, + } + } + + return openapi.VmCreateNetadp{ + Service: "public", + } +} + +func associatePublicIP(ctx context.Context, config *Config, client *openapi.APIClient, netadp openapi.Netadp) (string, error) { + if config.Network == "" || config.PublicIP == "" { + // Public IP belongs to attached net adapter + return netadp.Ip[0].Address, nil + } + + var privateIP string + if config.PrivateIP == "" { + privateIP = netadp.Ip[0].Id + } else { + privateIP = config.PrivateIP + } + + ip, _, err := client.IpApi.IpActionAssociate(ctx, config.PublicIP, openapi.IpActionAssociate{Ip: privateIP}) + if err != nil { + return "", err + } + + return ip.Address, nil +} + +func (s *stepCreateVM) Cleanup(state multistep.StateBag) { + if s.vmID == "" { + return + } + + client := state.Get("client").(*openapi.APIClient) + ui := state.Get("ui").(packer.Ui) + + ui.Say("Deleting VM...") + + deleteOptions := openapi.VmDelete{} + diskIDs, ok := state.Get("disk_ids").([]string) + if ok && len(diskIDs) > 0 { + deleteOptions.RemoveDisks = diskIDs + } + + _, err := client.VmApi.VmDelete(context.TODO(), s.vmID, deleteOptions) + if err != nil { + ui.Error(fmt.Sprintf("Error deleting server '%s' - please delete it manually: %s", s.vmID, formatOpenAPIError(err))) + } +} diff --git a/builder/hyperone/step_create_vm_test.go b/builder/hyperone/step_create_vm_test.go new file mode 100644 index 000000000..e2d97fc1f --- /dev/null +++ b/builder/hyperone/step_create_vm_test.go @@ -0,0 +1,107 @@ +package hyperone + +import ( + "testing" + + "github.com/hyperonecom/h1-client-go" + "github.com/stretchr/testify/assert" +) + +func TestPickNetAdapter(t *testing.T) { + cases := []struct { + Name string + Config Config + Expected openapi.VmCreateNetadp + }{ + { + Name: "no_network", + Config: Config{}, + Expected: openapi.VmCreateNetadp{ + Service: "public", + }, + }, + { + Name: "no_network_public_ip", + Config: Config{ + PublicIP: "some-public-ip", + }, + Expected: openapi.VmCreateNetadp{ + Service: "public", + Ip: []string{"some-public-ip"}, + }, + }, + { + Name: "no_network_private_ip", + Config: Config{ + PrivateIP: "some-private-ip", + }, + Expected: openapi.VmCreateNetadp{ + Service: "public", + }, + }, + { + Name: "no_network_both_ip", + Config: Config{ + PublicIP: "some-public-ip", + PrivateIP: "some-private-ip", + }, + Expected: openapi.VmCreateNetadp{ + Service: "public", + Ip: []string{"some-public-ip"}, + }, + }, + { + Name: "network_no_ip", + Config: Config{ + Network: "some-network", + }, + Expected: openapi.VmCreateNetadp{ + Service: "private", + Network: "some-network", + }, + }, + { + Name: "network_public_ip", + Config: Config{ + Network: "some-network", + PublicIP: "some-public-ip", + }, + Expected: openapi.VmCreateNetadp{ + Service: "private", + Network: "some-network", + }, + }, + { + Name: "network_private_ip", + Config: Config{ + Network: "some-network", + PrivateIP: "some-private-ip", + }, + Expected: openapi.VmCreateNetadp{ + Service: "private", + Network: "some-network", + Ip: []string{"some-private-ip"}, + }, + }, + { + Name: "network_both_ip", + Config: Config{ + Network: "some-network", + PublicIP: "some-public-ip", + PrivateIP: "some-private-ip", + }, + Expected: openapi.VmCreateNetadp{ + Service: "private", + Network: "some-network", + Ip: []string{"some-private-ip"}, + }, + }, + } + + for _, c := range cases { + t.Run(c.Name, func(t *testing.T) { + result := pickNetAdapter(&c.Config) + assert.Equal(t, c.Expected, result) + }) + } +} diff --git a/builder/hyperone/step_stop_vm.go b/builder/hyperone/step_stop_vm.go new file mode 100644 index 000000000..b43befd60 --- /dev/null +++ b/builder/hyperone/step_stop_vm.go @@ -0,0 +1,32 @@ +package hyperone + +import ( + "context" + "fmt" + + "github.com/hashicorp/packer/helper/multistep" + "github.com/hashicorp/packer/packer" + "github.com/hyperonecom/h1-client-go" +) + +type stepStopVM struct{} + +func (s *stepStopVM) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction { + client := state.Get("client").(*openapi.APIClient) + ui := state.Get("ui").(packer.Ui) + vmID := state.Get("vm_id").(string) + + ui.Say("Stopping VM...") + + _, _, err := client.VmApi.VmActionStop(ctx, vmID) + if err != nil { + err := fmt.Errorf("error stopping VM: %s", formatOpenAPIError(err)) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + + return multistep.ActionContinue +} + +func (s *stepStopVM) Cleanup(multistep.StateBag) {} diff --git a/builder/hyperone/token_by_ssh.go b/builder/hyperone/token_by_ssh.go new file mode 100644 index 000000000..158572cbd --- /dev/null +++ b/builder/hyperone/token_by_ssh.go @@ -0,0 +1,73 @@ +package hyperone + +import ( + "io/ioutil" + "net" + "os" + + "golang.org/x/crypto/ssh" + "golang.org/x/crypto/ssh/agent" + + "github.com/hashicorp/packer/common/json" +) + +const ( + sshAddress = "api.hyperone.com:22" + sshSubsystem = "rbx-auth" +) + +type sshData struct { + ID string `json:"_id"` +} + +func sshAgent() ssh.AuthMethod { + if sshAgent, err := net.Dial("unix", os.Getenv("SSH_AUTH_SOCK")); err == nil { + return ssh.PublicKeysCallback(agent.NewClient(sshAgent).Signers) + } + return nil +} + +func fetchTokenBySSH(user string) (string, error) { + sshConfig := &ssh.ClientConfig{ + User: user, + Auth: []ssh.AuthMethod{ + sshAgent(), + }, + HostKeyCallback: ssh.InsecureIgnoreHostKey(), + } + + client, err := ssh.Dial("tcp", sshAddress, sshConfig) + if err != nil { + return "", err + } + defer client.Close() + + session, err := client.NewSession() + if err != nil { + return "", err + } + defer session.Close() + + stdout, err := session.StdoutPipe() + if err != nil { + return "", err + } + + err = session.RequestSubsystem(sshSubsystem) + if err != nil { + return "", err + } + + out, err := ioutil.ReadAll(stdout) + if err != nil { + return "", err + } + + var data sshData + err = json.Unmarshal(out, &data) + if err != nil { + return "", err + } + + return data.ID, nil +} diff --git a/builder/hyperone/utils.go b/builder/hyperone/utils.go new file mode 100644 index 000000000..3105693de --- /dev/null +++ b/builder/hyperone/utils.go @@ -0,0 +1,16 @@ +package hyperone + +import ( + "fmt" + + "github.com/hyperonecom/h1-client-go" +) + +func formatOpenAPIError(err error) string { + openAPIError, ok := err.(openapi.GenericOpenAPIError) + if !ok { + return err.Error() + } + + return fmt.Sprintf("%s (body: %s)", openAPIError.Error(), openAPIError.Body()) +} diff --git a/command/plugin.go b/command/plugin.go index 825f06683..9b65f2b8d 100644 --- a/command/plugin.go +++ b/command/plugin.go @@ -26,6 +26,7 @@ import ( filebuilder "github.com/hashicorp/packer/builder/file" googlecomputebuilder "github.com/hashicorp/packer/builder/googlecompute" hcloudbuilder "github.com/hashicorp/packer/builder/hcloud" + hyperonebuilder "github.com/hashicorp/packer/builder/hyperone" hypervisobuilder "github.com/hashicorp/packer/builder/hyperv/iso" hypervvmcxbuilder "github.com/hashicorp/packer/builder/hyperv/vmcx" lxcbuilder "github.com/hashicorp/packer/builder/lxc" @@ -99,6 +100,7 @@ var Builders = map[string]packer.Builder{ "file": new(filebuilder.Builder), "googlecompute": new(googlecomputebuilder.Builder), "hcloud": new(hcloudbuilder.Builder), + "hyperone": new(hyperonebuilder.Builder), "hyperv-iso": new(hypervisobuilder.Builder), "hyperv-vmcx": new(hypervvmcxbuilder.Builder), "lxc": new(lxcbuilder.Builder), diff --git a/go.mod b/go.mod index c5aa3571c..2591d61af 100644 --- a/go.mod +++ b/go.mod @@ -90,6 +90,7 @@ require ( github.com/hashicorp/vault-plugin-secrets-kv v0.0.0-20181106190520-2236f141171e // indirect github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb github.com/hetznercloud/hcloud-go v1.12.0 + github.com/hyperonecom/h1-client-go v0.0.0-20190122232013-cf38e8387775 github.com/jefferai/jsonx v0.0.0-20160721235117-9cc31c3135ee // indirect github.com/joyent/triton-go v0.0.0-20180116165742-545edbe0d564 github.com/jtolds/gls v4.2.1+incompatible // indirect diff --git a/go.sum b/go.sum index 89b849f17..f41baf7e9 100644 --- a/go.sum +++ b/go.sum @@ -197,6 +197,8 @@ github.com/hetznercloud/hcloud-go v1.12.0 h1:ugZO8a8ADekqSWi7xWlcs6pxr4QE0tw5Vny github.com/hetznercloud/hcloud-go v1.12.0/go.mod h1:g5pff0YNAZywQaivY/CmhUYFVp7oP0nu3MiODC2W4Hw= github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/hyperonecom/h1-client-go v0.0.0-20190122232013-cf38e8387775 h1:MIteIoIQ5nFoOmwEHPDsqng8d0dtKj3lCnQCwGvtxXc= +github.com/hyperonecom/h1-client-go v0.0.0-20190122232013-cf38e8387775/go.mod h1:R9rU87RxxmcD3DkspW9JqGBXiJyg5MA+WNCtJrBtnXs= github.com/jefferai/jsonx v0.0.0-20160721235117-9cc31c3135ee h1:AQ/QmCk6x8ECPpf2pkPtA4lyncEEBbs8VFnVXPYKhIs= github.com/jefferai/jsonx v0.0.0-20160721235117-9cc31c3135ee/go.mod h1:N0t2vlmpe8nyZB5ouIbJQPDSR+mH6oe7xHB9VZHSUzM= github.com/jmespath/go-jmespath v0.0.0-20160202185014-0b12d6b521d8 h1:12VvqtR6Aowv3l/EQUlocDHW2Cp4G9WJVH7uyH8QFJE= diff --git a/website/source/docs/builders/hyperone.html.md b/website/source/docs/builders/hyperone.html.md new file mode 100644 index 000000000..850e7f454 --- /dev/null +++ b/website/source/docs/builders/hyperone.html.md @@ -0,0 +1,110 @@ +--- +description: | + HyperOne Packer builder creates new images on the HyperOne platform. + The builder takes a source image, runs any provisioning necessary on + the image after launching it, then creates a reusable image. +layout: docs +page_title: 'HyperOne - Builders' +sidebar_current: 'docs-builders-hyperone' +--- + +# HyperOne Builder + +Type: `hyperone` + +The `hyperone` Packer builder is able to create new images on the [HyperOne +platform](http://www.hyperone.com/). The builder takes a source image, runs +any provisioning necessary on the image after launching it, then creates a +reusable image. + +The builder does *not* manage images. Once it creates an image, it is up to you +to use it or delete it. + +## Configuration Reference + +There are many configuration options available for the builder. They are +segmented below into two categories: required and optional parameters. Within +each category, the available configuration keys are alphabetized. + +In addition to the options listed here, a +[communicator](/docs/templates/communicator.html) can be configured for this +builder. + +### Required: + +- `disk_size` (float) - Size of the created disk, in GiB. + +- `project` (string) - The id or name of the project. This field is required + only if using session tokens. It should be skipped when using service + account authentication. + +- `source_image` (string) - ID or name of the image to launch server from. + +- `token` (string) - The authentication token used to access your account. + This can be either a session token or a service account token. + If not defined, the builder will attempt to find it in the following order: + + - In `HYPERONE_TOKEN` environment variable. + - In `~/.h1-cli/conf.json` config file used by [h1-cli](https://github.com/hyperonecom/h1-cli). + - By using SSH authentication if `token_login` variable has been set. + +- `vm_flavour` (string) - ID or name of the type this server should be created with. + +### Optional: + +- `disk_name` (string) - The name of the created disk. + +- `disk_type` (string) - The type of the created disk. + +- `image_description` (string) - The description of the resulting image. + +- `image_name` (string) - The name of the resulting image. Defaults to + "packer-{{timestamp}}" + (see [configuration templates](/docs/templates/engine.html) for more info). + +- `image_service` (string) - The service of the resulting image. + +- `image_tags` (map of key/value strings) - Key/value pair tags to + add to the created image. + +- `network` (string) - The ID of the network to attach to the created server. + +- `private_ip` (string) - The ID of the private IP within chosen `network` + that should be assigned to the created server. + +- `public_ip` (string) - The ID of the public IP that should be assigned to + the created server. If `network` is chosen, the public IP will be associated + with server's private IP. + +- `ssh_keys` (array of strings) - List of SSH keys by name or id to be added + to the server on launch. + +- `state_timeout` (string) - Timeout for waiting on the API to complete + a request. Defaults to 5m. + +- `token_login` (string) - Login (an e-mail) on HyperOne platform. Set this + if you want to fetch the token by SSH authentication. + +- `user_data` (string) - User data to launch with the server. Packer will not + automatically wait for a user script to finish before shutting down the + instance, this must be handled in a provisioner. + +- `vm_name` (string) - The name of the created server. + +- `vm_tags` (map of key/value strings) - Key/value pair tags to + add to the created server. + +## Basic Example + +Here is a basic example. It is completely valid as soon as you enter your own +token. + +``` json +{ + "type": "hyperone", + "token": "YOUR_AUTH_TOKEN", + "source_image": "ubuntu-18.04", + "vm_flavour": "a1.nano", + "disk_size": 10 +} +``` diff --git a/website/source/layouts/docs.erb b/website/source/layouts/docs.erb index 5a9f4436d..0268795c3 100644 --- a/website/source/layouts/docs.erb +++ b/website/source/layouts/docs.erb @@ -102,6 +102,9 @@