From 87d5df00589bdcd899df4d093a6160a9b0e0e62c Mon Sep 17 00:00:00 2001 From: Castor Sky Date: Wed, 16 Oct 2024 01:52:19 +0300 Subject: [PATCH 1/4] add: scaffolded directory and some files Basic configuration verification copied from builder. Template in directory test-fixtures works. --- datasource/proxmox/data.go | 112 ++++++++++++++++++ datasource/proxmox/data.hcl2spec.go | 64 ++++++++++ datasource/proxmox/data_acc_test.go | 65 ++++++++++ .../proxmox/test-fixtures/template.pkr.hcl | 28 +++++ main.go | 2 + 5 files changed, 271 insertions(+) create mode 100644 datasource/proxmox/data.go create mode 100644 datasource/proxmox/data.hcl2spec.go create mode 100644 datasource/proxmox/data_acc_test.go create mode 100644 datasource/proxmox/test-fixtures/template.pkr.hcl diff --git a/datasource/proxmox/data.go b/datasource/proxmox/data.go new file mode 100644 index 00000000..7e4e1a7a --- /dev/null +++ b/datasource/proxmox/data.go @@ -0,0 +1,112 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +//go:generate packer-sdc mapstructure-to-hcl2 -type Config,DatasourceOutput +package proxmoxtemplate + +import ( + "errors" + "fmt" + "github.com/hashicorp/hcl/v2/hcldec" + "github.com/hashicorp/packer-plugin-sdk/hcl2helper" + packersdk "github.com/hashicorp/packer-plugin-sdk/packer" + "github.com/hashicorp/packer-plugin-sdk/template/config" + "github.com/zclconf/go-cty/cty" + "net/url" + "os" +) + +type Config struct { + // URL to the Proxmox API, including the full path, + // so `https://:/api2/json` for example. + // Can also be set via the `PROXMOX_URL` environment variable. + ProxmoxURLRaw string `mapstructure:"proxmox_url"` + proxmoxURL *url.URL + // Skip validating the certificate. + SkipCertValidation bool `mapstructure:"insecure_skip_tls_verify"` + // Username when authenticating to Proxmox, including + // the realm. For example `user@pve` to use the local Proxmox realm. When using + // token authentication, the username must include the token id after an exclamation + // mark. For example, `user@pve!tokenid`. + // Can also be set via the `PROXMOX_USERNAME` environment variable. + Username string `mapstructure:"username"` + // Password for the user. + // For API tokens please use `token`. + // Can also be set via the `PROXMOX_PASSWORD` environment variable. + // Either `password` or `token` must be specifed. If both are set, + // `token` takes precedence. + Password string `mapstructure:"password"` + // Token for authenticating API calls. + // This allows the API client to work with API tokens instead of user passwords. + // Can also be set via the `PROXMOX_TOKEN` environment variable. + // Either `password` or `token` must be specifed. If both are set, + // `token` takes precedence. + Token string `mapstructure:"token"` +} + +type Datasource struct { + config Config +} + +type DatasourceOutput struct { + Foo string `mapstructure:"foo"` + Bar string `mapstructure:"bar"` +} + +func (d *Datasource) ConfigSpec() hcldec.ObjectSpec { + return d.config.FlatMapstructure().HCL2Spec() +} + +func (d *Datasource) Configure(raws ...interface{}) error { + err := config.Decode(&d.config, nil, raws...) + if err != nil { + return err + } + + var errs *packersdk.MultiError + + // Defaults + if d.config.ProxmoxURLRaw == "" { + d.config.ProxmoxURLRaw = os.Getenv("PROXMOX_URL") + } + if d.config.Username == "" { + d.config.Username = os.Getenv("PROXMOX_USERNAME") + } + if d.config.Password == "" { + d.config.Password = os.Getenv("PROXMOX_PASSWORD") + } + if d.config.Token == "" { + d.config.Token = os.Getenv("PROXMOX_TOKEN") + } + + // Required configurations that will display errors if not set + if d.config.Username == "" { + errs = packersdk.MultiErrorAppend(errs, errors.New("username must be specified")) + } + if d.config.Password == "" && d.config.Token == "" { + errs = packersdk.MultiErrorAppend(errs, errors.New("password or token must be specified")) + } + if d.config.ProxmoxURLRaw == "" { + errs = packersdk.MultiErrorAppend(errs, errors.New("proxmox_url must be specified")) + } + if d.config.proxmoxURL, err = url.Parse(d.config.ProxmoxURLRaw); err != nil { + errs = packersdk.MultiErrorAppend(errs, fmt.Errorf("could not parse proxmox_url: %s", err)) + } + + if errs != nil && len(errs.Errors) > 0 { + return errs + } + return nil +} + +func (d *Datasource) OutputSpec() hcldec.ObjectSpec { + return (&DatasourceOutput{}).FlatMapstructure().HCL2Spec() +} + +func (d *Datasource) Execute() (cty.Value, error) { + output := DatasourceOutput{ + Foo: "foo-value", + Bar: "bar-value", + } + return hcl2helper.HCL2ValueFromConfig(output, d.OutputSpec()), nil +} diff --git a/datasource/proxmox/data.hcl2spec.go b/datasource/proxmox/data.hcl2spec.go new file mode 100644 index 00000000..5fd3ba90 --- /dev/null +++ b/datasource/proxmox/data.hcl2spec.go @@ -0,0 +1,64 @@ +// Code generated by "packer-sdc mapstructure-to-hcl2"; DO NOT EDIT. + +package proxmoxtemplate + +import ( + "github.com/hashicorp/hcl/v2/hcldec" + "github.com/zclconf/go-cty/cty" +) + +// FlatConfig is an auto-generated flat version of Config. +// Where the contents of a field with a `mapstructure:,squash` tag are bubbled up. +type FlatConfig struct { + ProxmoxURLRaw *string `mapstructure:"proxmox_url" cty:"proxmox_url" hcl:"proxmox_url"` + SkipCertValidation *bool `mapstructure:"insecure_skip_tls_verify" cty:"insecure_skip_tls_verify" hcl:"insecure_skip_tls_verify"` + Username *string `mapstructure:"username" cty:"username" hcl:"username"` + Password *string `mapstructure:"password" cty:"password" hcl:"password"` + Token *string `mapstructure:"token" cty:"token" hcl:"token"` +} + +// FlatMapstructure returns a new FlatConfig. +// FlatConfig is an auto-generated flat version of Config. +// Where the contents a fields with a `mapstructure:,squash` tag are bubbled up. +func (*Config) FlatMapstructure() interface{ HCL2Spec() map[string]hcldec.Spec } { + return new(FlatConfig) +} + +// HCL2Spec returns the hcl spec of a Config. +// This spec is used by HCL to read the fields of Config. +// The decoded values from this spec will then be applied to a FlatConfig. +func (*FlatConfig) HCL2Spec() map[string]hcldec.Spec { + s := map[string]hcldec.Spec{ + "proxmox_url": &hcldec.AttrSpec{Name: "proxmox_url", Type: cty.String, Required: false}, + "insecure_skip_tls_verify": &hcldec.AttrSpec{Name: "insecure_skip_tls_verify", Type: cty.Bool, Required: false}, + "username": &hcldec.AttrSpec{Name: "username", Type: cty.String, Required: false}, + "password": &hcldec.AttrSpec{Name: "password", Type: cty.String, Required: false}, + "token": &hcldec.AttrSpec{Name: "token", Type: cty.String, Required: false}, + } + return s +} + +// FlatDatasourceOutput is an auto-generated flat version of DatasourceOutput. +// Where the contents of a field with a `mapstructure:,squash` tag are bubbled up. +type FlatDatasourceOutput struct { + Foo *string `mapstructure:"foo" cty:"foo" hcl:"foo"` + Bar *string `mapstructure:"bar" cty:"bar" hcl:"bar"` +} + +// FlatMapstructure returns a new FlatDatasourceOutput. +// FlatDatasourceOutput is an auto-generated flat version of DatasourceOutput. +// Where the contents a fields with a `mapstructure:,squash` tag are bubbled up. +func (*DatasourceOutput) FlatMapstructure() interface{ HCL2Spec() map[string]hcldec.Spec } { + return new(FlatDatasourceOutput) +} + +// HCL2Spec returns the hcl spec of a DatasourceOutput. +// This spec is used by HCL to read the fields of DatasourceOutput. +// The decoded values from this spec will then be applied to a FlatDatasourceOutput. +func (*FlatDatasourceOutput) HCL2Spec() map[string]hcldec.Spec { + s := map[string]hcldec.Spec{ + "foo": &hcldec.AttrSpec{Name: "foo", Type: cty.String, Required: false}, + "bar": &hcldec.AttrSpec{Name: "bar", Type: cty.String, Required: false}, + } + return s +} diff --git a/datasource/proxmox/data_acc_test.go b/datasource/proxmox/data_acc_test.go new file mode 100644 index 00000000..7c241dec --- /dev/null +++ b/datasource/proxmox/data_acc_test.go @@ -0,0 +1,65 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package proxmoxtemplate + +import ( + _ "embed" + "fmt" + "io/ioutil" + "os" + "os/exec" + "regexp" + "testing" + + "github.com/hashicorp/packer-plugin-sdk/acctest" +) + +//go:embed test-fixtures/template.pkr.hcl +var testDatasourceHCL2Basic string + +// Run with: PACKER_ACC=1 go test -count 1 -v ./datasource/scaffolding/data_acc_test.go -timeout=120m +func TestAccScaffoldingDatasource(t *testing.T) { + testCase := &acctest.PluginTestCase{ + Name: "scaffolding_datasource_basic_test", + Setup: func() error { + return nil + }, + Teardown: func() error { + return nil + }, + Template: testDatasourceHCL2Basic, + Type: "scaffolding-my-datasource", + Check: func(buildCommand *exec.Cmd, logfile string) error { + if buildCommand.ProcessState != nil { + if buildCommand.ProcessState.ExitCode() != 0 { + return fmt.Errorf("Bad exit code. Logfile: %s", logfile) + } + } + + logs, err := os.Open(logfile) + if err != nil { + return fmt.Errorf("Unable find %s", logfile) + } + defer logs.Close() + + logsBytes, err := ioutil.ReadAll(logs) + if err != nil { + return fmt.Errorf("Unable to read %s", logfile) + } + logsString := string(logsBytes) + + fooLog := "null.basic-example: foo: foo-value" + barLog := "null.basic-example: bar: bar-value" + + if matched, _ := regexp.MatchString(fooLog+".*", logsString); !matched { + t.Fatalf("logs doesn't contain expected foo value %q", logsString) + } + if matched, _ := regexp.MatchString(barLog+".*", logsString); !matched { + t.Fatalf("logs doesn't contain expected bar value %q", logsString) + } + return nil + }, + } + acctest.TestPlugin(t, testCase) +} diff --git a/datasource/proxmox/test-fixtures/template.pkr.hcl b/datasource/proxmox/test-fixtures/template.pkr.hcl new file mode 100644 index 00000000..6a974a2e --- /dev/null +++ b/datasource/proxmox/test-fixtures/template.pkr.hcl @@ -0,0 +1,28 @@ +data "proxmox-template" "default" { + proxmox_url = "https://localhost:8006/api2/json" + insecure_skip_tls_verify = true + username = "root@pam" + password = "password" +} + +locals { + foo = data.proxmox-template.default.foo + bar = data.proxmox-template.default.bar +} + +source "null" "basic-example" { + communicator = "none" +} + +build { + sources = [ + "source.null.basic-example" + ] + + provisioner "shell-local" { + inline = [ + "echo foo: ${local.foo}", + "echo bar: ${local.bar}", + ] + } +} diff --git a/main.go b/main.go index 7452cff9..91ce4609 100644 --- a/main.go +++ b/main.go @@ -11,6 +11,7 @@ import ( proxmoxclone "github.com/hashicorp/packer-plugin-proxmox/builder/proxmox/clone" proxmoxiso "github.com/hashicorp/packer-plugin-proxmox/builder/proxmox/iso" + proxmoxtemplate "github.com/hashicorp/packer-plugin-proxmox/datasource/proxmox" "github.com/hashicorp/packer-plugin-proxmox/version" ) @@ -21,6 +22,7 @@ func main() { pps.RegisterBuilder(plugin.DEFAULT_NAME, new(proxmoxiso.Builder)) pps.RegisterBuilder("iso", new(proxmoxiso.Builder)) pps.RegisterBuilder("clone", new(proxmoxclone.Builder)) + pps.RegisterDatasource("template", new(proxmoxtemplate.Datasource)) pps.SetVersion(version.PluginVersion) err := pps.Run() if err != nil { From 5740ec7b67b3b8bf3f058ce83daecc7344288532 Mon Sep 17 00:00:00 2001 From: Castor Sky Date: Fri, 18 Oct 2024 05:01:30 +0300 Subject: [PATCH 2/4] add: new datasource and documentation --- .web-docs/README.md | 5 + .../components/data-source/template/README.md | 124 +++++++ .web-docs/metadata.hcl | 5 + datasource/proxmox/data.go | 312 +++++++++++++++++- datasource/proxmox/data.hcl2spec.go | 60 +++- datasource/proxmox/data_acc_test.go | 65 ---- datasource/proxmox/data_test.go | 189 +++++++++++ .../proxmox/test-fixtures/template.pkr.hcl | 28 -- .../proxmox/Config-not-required.mdx | 49 +++ docs-partials/datasource/proxmox/Config.mdx | 9 + .../datasource/proxmox/DatasourceOutput.mdx | 9 + docs/README.md | 5 + docs/datasources/template.mdx | 70 ++++ 13 files changed, 817 insertions(+), 113 deletions(-) create mode 100644 .web-docs/components/data-source/template/README.md delete mode 100644 datasource/proxmox/data_acc_test.go create mode 100644 datasource/proxmox/data_test.go delete mode 100644 datasource/proxmox/test-fixtures/template.pkr.hcl create mode 100644 docs-partials/datasource/proxmox/Config-not-required.mdx create mode 100644 docs-partials/datasource/proxmox/Config.mdx create mode 100644 docs-partials/datasource/proxmox/DatasourceOutput.mdx create mode 100644 docs/datasources/template.mdx diff --git a/.web-docs/README.md b/.web-docs/README.md index 3d02cf72..f15b45cc 100644 --- a/.web-docs/README.md +++ b/.web-docs/README.md @@ -35,3 +35,8 @@ Packer is able to target both ISO and existing Cloud-Init images. takes an ISO source, runs any provisioning necessary on the image after launching it, then creates a virtual machine template. +#### Data Sources + +- [data source](/packer/integrations/hashicorp/proxmox/latest/components/datasource/template) - The proxmox template + datasource is able to get info about existing guests from Proxmox cluster and return VM ID of a single guest that + matches all specified filters. This ID can later be used in the 'clone' builder to select template. diff --git a/.web-docs/components/data-source/template/README.md b/.web-docs/components/data-source/template/README.md new file mode 100644 index 00000000..c1d2bd9f --- /dev/null +++ b/.web-docs/components/data-source/template/README.md @@ -0,0 +1,124 @@ +Type: `proxmox-template` +Artifact BuilderId: `proxmox.template` + +The `proxmox-template` datasource is able to get info about existing guests +from [Proxmox](https://www.proxmox.com/en/proxmox-ve) cluster and return VM +ID of a single guest that matches all specified filters. This ID can later +be used in the `proxmox-clone` builder to select template. + +## Configuration Reference + + + +Datasource has a bunch of filters which you can use, for example, to find the latest available +template in the cluster that matches defined filters. + +You can combine any number of filters but all of them will be conjuncted with AND. +When datasource cannot return only one (zero or >1) guest identifiers it will return error. + + + + +## Optional: + + + +- `proxmox_url` (string) - URL to the Proxmox API, including the full path, + so `https://:/api2/json` for example. + Can also be set via the `PROXMOX_URL` environment variable. + +- `insecure_skip_tls_verify` (bool) - Skip validating the certificate. + +- `username` (string) - Username when authenticating to Proxmox, including + the realm. For example `user@pve` to use the local Proxmox realm. When using + token authentication, the username must include the token id after an exclamation + mark. For example, `user@pve!tokenid`. + Can also be set via the `PROXMOX_USERNAME` environment variable. + +- `password` (string) - Password for the user. + For API tokens please use `token`. + Can also be set via the `PROXMOX_PASSWORD` environment variable. + Either `password` or `token` must be specifed. If both are set, + `token` takes precedence. + +- `token` (string) - Token for authenticating API calls. + This allows the API client to work with API tokens instead of user passwords. + Can also be set via the `PROXMOX_TOKEN` environment variable. + Either `password` or `token` must be specifed. If both are set, + `token` takes precedence. + +- `task_timeout` (duration string | ex: "1h5m2s") - `task_timeout` (duration string | ex: "10m") - The timeout for + Promox API operations, e.g. clones. Defaults to 1 minute. + +- `name` (string) - Filter that returns `vm_id` for guest which name exactly matches this value. + Options `name` and `name_regex` are mutually exclusive. + +- `name_regex` (string) - Filter that returns `vm_id` for guest which name matches the regular expression. + Expression must use [Go Regex Syntax](https://pkg.go.dev/regexp/syntax). + Options `name` and `name_regex` are mutually exclusive. + +- `template` (bool) - Filter that returns guest `vm_id` only when guest is template. + +- `node` (string) - Filter that returns `vm_id` only when guest is located on the specified PVE node. + +- `vm_tags` (string) - Filter that returns `vm_id` for guest which has all these tags. When you need to + specify more than one tag, use semicolon as separator (`"tag1;tag2"`). + Every specified tag must exist in guest. + +- `latest` (bool) - This filter determines how to handle multiple guests that were matched with all + previous filters. Guest creation time is being used to find latest. + By default, multiple matching guests results in an error. + + + + +## Output: + + + +- `vm_id` (uint) - Identifier of the found guest. + +- `vm_name` (string) - Name of the found guest. + +- `vm_tags` (string) - Tags of the found guest separated with semicolon. + + + + +## Example Usage + +This is a very basic example which connects to local PVE host, finds the latest +guest which name matches the regex `image-.*` and which type is `template`. The +ID is then printed to console as output variable. + +```hcl +data "proxmox-template" "default" { + proxmox_url = "https://localhost:8006/api2/json" + insecure_skip_tls_verify = true + username = "root@pam" + password = "password" + name_regex = "image-.*" + template = true + latest = true +} + +locals { + vm_id = data.proxmox-template.default.vm_id +} + +source "null" "basic-example" { + communicator = "none" +} + +build { + sources = [ + "source.null.basic-example" + ] + + provisioner "shell-local" { + inline = [ + "echo vm_id: ${local.vm_id}", + ] + } +} +``` diff --git a/.web-docs/metadata.hcl b/.web-docs/metadata.hcl index c819c494..ab7afc53 100644 --- a/.web-docs/metadata.hcl +++ b/.web-docs/metadata.hcl @@ -17,4 +17,9 @@ integration { name = "Proxmox ISO" slug = "iso" } + component { + type = "data-source" + name = "Proxmox Template" + slug = "template" + } } diff --git a/datasource/proxmox/data.go b/datasource/proxmox/data.go index 7e4e1a7a..d9c51e3d 100644 --- a/datasource/proxmox/data.go +++ b/datasource/proxmox/data.go @@ -1,22 +1,40 @@ // Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 +//go:generate packer-sdc struct-markdown //go:generate packer-sdc mapstructure-to-hcl2 -type Config,DatasourceOutput + package proxmoxtemplate import ( + "crypto/tls" "errors" "fmt" + "log" + "net/url" + "os" + "regexp" + "strconv" + "strings" + "time" + + "github.com/Telmate/proxmox-api-go/proxmox" "github.com/hashicorp/hcl/v2/hcldec" + "github.com/hashicorp/packer-plugin-sdk/common" "github.com/hashicorp/packer-plugin-sdk/hcl2helper" packersdk "github.com/hashicorp/packer-plugin-sdk/packer" "github.com/hashicorp/packer-plugin-sdk/template/config" "github.com/zclconf/go-cty/cty" - "net/url" - "os" ) +// Datasource has a bunch of filters which you can use, for example, to find the latest available +// template in the cluster that matches defined filters. +// +// You can combine any number of filters but all of them will be conjuncted with AND. +// When datasource cannot return only one (zero or >1) guest identifiers it will return error. type Config struct { + common.PackerConfig `mapstructure:",squash"` + // URL to the Proxmox API, including the full path, // so `https://:/api2/json` for example. // Can also be set via the `PROXMOX_URL` environment variable. @@ -42,6 +60,28 @@ type Config struct { // Either `password` or `token` must be specifed. If both are set, // `token` takes precedence. Token string `mapstructure:"token"` + // `task_timeout` (duration string | ex: "10m") - The timeout for + // Promox API operations, e.g. clones. Defaults to 1 minute. + TaskTimeout time.Duration `mapstructure:"task_timeout"` + // Filter that returns `vm_id` for guest which name exactly matches this value. + // Options `name` and `name_regex` are mutually exclusive. + Name string `mapstructure:"name"` + // Filter that returns `vm_id` for guest which name matches the regular expression. + // Expression must use [Go Regex Syntax](https://pkg.go.dev/regexp/syntax). + // Options `name` and `name_regex` are mutually exclusive. + NameRegex string `mapstructure:"name_regex"` + // Filter that returns guest `vm_id` only when guest is template. + Template bool `mapstructure:"template"` + // Filter that returns `vm_id` only when guest is located on the specified PVE node. + Node string `mapstructure:"node"` + // Filter that returns `vm_id` for guest which has all these tags. When you need to + // specify more than one tag, use semicolon as separator (`"tag1;tag2"`). + // Every specified tag must exist in guest. + VmTags string `mapstructure:"vm_tags"` + // This filter determines how to handle multiple guests that were matched with all + // previous filters. Guest creation time is being used to find latest. + // By default, multiple matching guests results in an error. + Latest bool `mapstructure:"latest"` } type Datasource struct { @@ -49,10 +89,16 @@ type Datasource struct { } type DatasourceOutput struct { - Foo string `mapstructure:"foo"` - Bar string `mapstructure:"bar"` + // Identifier of the found guest. + VmId uint `mapstructure:"vm_id"` + // Name of the found guest. + VmName string `mapstructure:"vm_name"` + // Tags of the found guest separated with semicolon. + VmTags string `mapstructure:"vm_tags"` } +type vmConfig map[string]interface{} + func (d *Datasource) ConfigSpec() hcldec.ObjectSpec { return d.config.FlatMapstructure().HCL2Spec() } @@ -93,6 +139,20 @@ func (d *Datasource) Configure(raws ...interface{}) error { errs = packersdk.MultiErrorAppend(errs, fmt.Errorf("could not parse proxmox_url: %s", err)) } + if d.config.Name != "" && d.config.NameRegex != "" { + errs = packersdk.MultiErrorAppend(errs, errors.New("name and name_regex are mutually exclusive")) + } + + if d.config.NameRegex != "" { + if _, err := regexp.Compile(d.config.NameRegex); err != nil { + errs = packersdk.MultiErrorAppend(errs, errors.New("cannot compile regex string")) + } + } + + if d.config.TaskTimeout == 0 { + d.config.TaskTimeout = 60 * time.Second + } + if errs != nil && len(errs.Errors) > 0 { return errs } @@ -104,9 +164,249 @@ func (d *Datasource) OutputSpec() hcldec.ObjectSpec { } func (d *Datasource) Execute() (cty.Value, error) { + // This value of VM ID the function should return + var vmId uint + var vmName, vmTags string + + client, err := newProxmoxClient(d.config) + if err != nil { + return cty.NullVal(cty.EmptyObject), err + } + + vmList, err := proxmox.ListGuests(client) + if err != nil { + return cty.NullVal(cty.EmptyObject), err + } + + filteredVms := filterGuests(d.config, vmList) + if len(filteredVms) == 0 { + return cty.NullVal(cty.EmptyObject), errors.New("not a single vm matches the configured filters") + } + + if d.config.Latest { + vmConfigList, err := getVmConfigs(client, filteredVms) + if err != nil { + return cty.NullVal(cty.EmptyObject), err + } + + latestConfig, err := findLatestConfig(vmConfigList) + if err != nil { + return cty.NullVal(cty.EmptyObject), err + } + + vmId = latestConfig["vmid"].(uint) + vmName = latestConfig["name"].(string) + vmTags = latestConfig["tags"].(string) + } else { + if len(filteredVms) > 1 { + return cty.NullVal(cty.EmptyObject), errors.New("more than one guest passed filters, cannot return vm_id") + } + vmId = filteredVms[0].Id + vmName = filteredVms[0].Name + vmTags = joinTags(filteredVms[0].Tags, ";") + } + output := DatasourceOutput{ - Foo: "foo-value", - Bar: "bar-value", + VmId: vmId, + VmName: vmName, + VmTags: vmTags, } return hcl2helper.HCL2ValueFromConfig(output, d.OutputSpec()), nil } + +// Find the latest VM among filtered. +// The `meta` field contains info about creation time (but it is not described in API docs). +func findLatestConfig(configs []vmConfig) (vmConfig, error) { + var result vmConfig + var maxCtime int + for i := range configs { + if metaField, ok := configs[i]["meta"]; ok { + vmCtime, err := parseMetaField(metaField.(string)) + if err != nil { + return nil, err + } + if vmCtime > maxCtime { + maxCtime = vmCtime + result = configs[i] + } + } else { + return nil, errors.New("no meta field in the guest config") + } + } + return result, nil +} + +// Get configs from PVE in 'map[string]interface{}' format for all VMs in the list. +// Also add value of VM ID to every config (useful for further steps). +func getVmConfigs(client *proxmox.Client, vmList []proxmox.GuestResource) ([]vmConfig, error) { + var result []vmConfig + for _, vm := range vmList { + var thisConfig vmConfig + vmr := proxmox.NewVmRef(int(vm.Id)) + thisConfig, err := client.GetVmConfig(vmr) + if err != nil { + return nil, err + } + thisConfig["vmid"] = vm.Id + result = append(result, thisConfig) + } + return result, nil +} + +// Drop guests from list that are not match some filters in the datasource config. +func filterGuests(config Config, guests []proxmox.GuestResource) []proxmox.GuestResource { + var result []proxmox.GuestResource + + if config.Name != "" { + result = filterByName(guests, config.Name) + } else { + result = guests + } + + if config.NameRegex != "" { + result = filterByNameRegex(guests, config.NameRegex) + } else { + if config.Name == "" { + result = guests + } + } + + if config.Template { + result = filterByTemplate(result) + } + if config.Node != "" { + result = filterByNode(result, config.Node) + } + if config.VmTags != "" { + result = filterByTags(result, config.VmTags) + } + + return result +} + +func filterByName(guests []proxmox.GuestResource, name string) []proxmox.GuestResource { + result := make([]proxmox.GuestResource, 0) + for _, i := range guests { + if i.Name == name { + result = append(result, i) + } + } + return result +} + +func filterByNameRegex(guests []proxmox.GuestResource, nameRegex string) []proxmox.GuestResource { + re, _ := regexp.Compile(nameRegex) + result := make([]proxmox.GuestResource, 0) + for _, i := range guests { + if re.MatchString(i.Name) { + result = append(result, i) + } + } + return result +} + +func filterByTemplate(guests []proxmox.GuestResource) []proxmox.GuestResource { + result := make([]proxmox.GuestResource, 0) + for _, i := range guests { + if i.Template { + result = append(result, i) + } + } + return result +} + +func filterByNode(guests []proxmox.GuestResource, node string) []proxmox.GuestResource { + result := make([]proxmox.GuestResource, 0) + for _, i := range guests { + if i.Node == node { + result = append(result, i) + } + } + return result +} + +func filterByTags(guests []proxmox.GuestResource, tags string) []proxmox.GuestResource { + result := make([]proxmox.GuestResource, 0) + // Split tags string because it can contain several tags separated with ";" + tagsSplitted := strings.Split(tags, ";") + for _, guest := range guests { + if len(guest.Tags) > 0 && configTagsMatchNodeTags(tagsSplitted, guest.Tags) { + result = append(result, guest) + } + } + return result +} + +func configTagsMatchNodeTags(configTags []string, nodeTags []proxmox.Tag) bool { + var countOfMatchedTags int + for _, configTag := range configTags { + var matched bool + for _, nodeTag := range nodeTags { + if configTag == string(nodeTag) { + matched = true + break + } + } + if matched { + countOfMatchedTags += 1 + } + } + if countOfMatchedTags != len(configTags) { + return false + } + return true +} + +func newProxmoxClient(config Config) (*proxmox.Client, error) { + tlsConfig := &tls.Config{ + InsecureSkipVerify: config.SkipCertValidation, + } + + client, err := proxmox.NewClient(strings.TrimSuffix(config.proxmoxURL.String(), "/"), nil, "", tlsConfig, "", int(config.TaskTimeout.Seconds())) + if err != nil { + return nil, err + } + + *proxmox.Debug = config.PackerDebug + + if config.Token != "" { + // configure token auth + log.Print("using token auth") + client.SetAPIToken(config.Username, config.Token) + } else { + // fallback to login if not using tokens + log.Print("using password auth") + err = client.Login(config.Username, config.Password, "") + if err != nil { + return nil, err + } + } + + return client, nil +} + +func parseMetaField(field string) (int, error) { + re, err := regexp.Compile(`.*ctime=(?P[0-9]+).*`) + if err != nil { + return 0, err + } + + matched := re.MatchString(field) + if !matched { + return 0, nil + } + valueStr := re.ReplaceAllString(field, "${ctime}") + value, err := strconv.Atoi(valueStr) + if err != nil { + return 0, err + } + return value, nil +} + +func joinTags(tags []proxmox.Tag, separator string) string { + tagsAsStrings := make([]string, len(tags)) + for i, tag := range tags { + tagsAsStrings[i] = string(tag) + } + return strings.Join(tagsAsStrings, separator) +} diff --git a/datasource/proxmox/data.hcl2spec.go b/datasource/proxmox/data.hcl2spec.go index 5fd3ba90..a8badc9e 100644 --- a/datasource/proxmox/data.hcl2spec.go +++ b/datasource/proxmox/data.hcl2spec.go @@ -10,11 +10,26 @@ import ( // FlatConfig is an auto-generated flat version of Config. // Where the contents of a field with a `mapstructure:,squash` tag are bubbled up. type FlatConfig struct { - ProxmoxURLRaw *string `mapstructure:"proxmox_url" cty:"proxmox_url" hcl:"proxmox_url"` - SkipCertValidation *bool `mapstructure:"insecure_skip_tls_verify" cty:"insecure_skip_tls_verify" hcl:"insecure_skip_tls_verify"` - Username *string `mapstructure:"username" cty:"username" hcl:"username"` - Password *string `mapstructure:"password" cty:"password" hcl:"password"` - Token *string `mapstructure:"token" cty:"token" hcl:"token"` + PackerBuildName *string `mapstructure:"packer_build_name" cty:"packer_build_name" hcl:"packer_build_name"` + PackerBuilderType *string `mapstructure:"packer_builder_type" cty:"packer_builder_type" hcl:"packer_builder_type"` + PackerCoreVersion *string `mapstructure:"packer_core_version" cty:"packer_core_version" hcl:"packer_core_version"` + PackerDebug *bool `mapstructure:"packer_debug" cty:"packer_debug" hcl:"packer_debug"` + PackerForce *bool `mapstructure:"packer_force" cty:"packer_force" hcl:"packer_force"` + PackerOnError *string `mapstructure:"packer_on_error" cty:"packer_on_error" hcl:"packer_on_error"` + PackerUserVars map[string]string `mapstructure:"packer_user_variables" cty:"packer_user_variables" hcl:"packer_user_variables"` + PackerSensitiveVars []string `mapstructure:"packer_sensitive_variables" cty:"packer_sensitive_variables" hcl:"packer_sensitive_variables"` + ProxmoxURLRaw *string `mapstructure:"proxmox_url" cty:"proxmox_url" hcl:"proxmox_url"` + SkipCertValidation *bool `mapstructure:"insecure_skip_tls_verify" cty:"insecure_skip_tls_verify" hcl:"insecure_skip_tls_verify"` + Username *string `mapstructure:"username" cty:"username" hcl:"username"` + Password *string `mapstructure:"password" cty:"password" hcl:"password"` + Token *string `mapstructure:"token" cty:"token" hcl:"token"` + TaskTimeout *string `mapstructure:"task_timeout" cty:"task_timeout" hcl:"task_timeout"` + Name *string `mapstructure:"name" cty:"name" hcl:"name"` + NameRegex *string `mapstructure:"name_regex" cty:"name_regex" hcl:"name_regex"` + Template *bool `mapstructure:"template" cty:"template" hcl:"template"` + Node *string `mapstructure:"node" cty:"node" hcl:"node"` + VmTags *string `mapstructure:"vm_tags" cty:"vm_tags" hcl:"vm_tags"` + Latest *bool `mapstructure:"latest" cty:"latest" hcl:"latest"` } // FlatMapstructure returns a new FlatConfig. @@ -29,11 +44,26 @@ func (*Config) FlatMapstructure() interface{ HCL2Spec() map[string]hcldec.Spec } // The decoded values from this spec will then be applied to a FlatConfig. func (*FlatConfig) HCL2Spec() map[string]hcldec.Spec { s := map[string]hcldec.Spec{ - "proxmox_url": &hcldec.AttrSpec{Name: "proxmox_url", Type: cty.String, Required: false}, - "insecure_skip_tls_verify": &hcldec.AttrSpec{Name: "insecure_skip_tls_verify", Type: cty.Bool, Required: false}, - "username": &hcldec.AttrSpec{Name: "username", Type: cty.String, Required: false}, - "password": &hcldec.AttrSpec{Name: "password", Type: cty.String, Required: false}, - "token": &hcldec.AttrSpec{Name: "token", Type: cty.String, Required: false}, + "packer_build_name": &hcldec.AttrSpec{Name: "packer_build_name", Type: cty.String, Required: false}, + "packer_builder_type": &hcldec.AttrSpec{Name: "packer_builder_type", Type: cty.String, Required: false}, + "packer_core_version": &hcldec.AttrSpec{Name: "packer_core_version", Type: cty.String, Required: false}, + "packer_debug": &hcldec.AttrSpec{Name: "packer_debug", Type: cty.Bool, Required: false}, + "packer_force": &hcldec.AttrSpec{Name: "packer_force", Type: cty.Bool, Required: false}, + "packer_on_error": &hcldec.AttrSpec{Name: "packer_on_error", Type: cty.String, Required: false}, + "packer_user_variables": &hcldec.AttrSpec{Name: "packer_user_variables", Type: cty.Map(cty.String), Required: false}, + "packer_sensitive_variables": &hcldec.AttrSpec{Name: "packer_sensitive_variables", Type: cty.List(cty.String), Required: false}, + "proxmox_url": &hcldec.AttrSpec{Name: "proxmox_url", Type: cty.String, Required: false}, + "insecure_skip_tls_verify": &hcldec.AttrSpec{Name: "insecure_skip_tls_verify", Type: cty.Bool, Required: false}, + "username": &hcldec.AttrSpec{Name: "username", Type: cty.String, Required: false}, + "password": &hcldec.AttrSpec{Name: "password", Type: cty.String, Required: false}, + "token": &hcldec.AttrSpec{Name: "token", Type: cty.String, Required: false}, + "task_timeout": &hcldec.AttrSpec{Name: "task_timeout", Type: cty.String, Required: false}, + "name": &hcldec.AttrSpec{Name: "name", Type: cty.String, Required: false}, + "name_regex": &hcldec.AttrSpec{Name: "name_regex", Type: cty.String, Required: false}, + "template": &hcldec.AttrSpec{Name: "template", Type: cty.Bool, Required: false}, + "node": &hcldec.AttrSpec{Name: "node", Type: cty.String, Required: false}, + "vm_tags": &hcldec.AttrSpec{Name: "vm_tags", Type: cty.String, Required: false}, + "latest": &hcldec.AttrSpec{Name: "latest", Type: cty.Bool, Required: false}, } return s } @@ -41,8 +71,9 @@ func (*FlatConfig) HCL2Spec() map[string]hcldec.Spec { // FlatDatasourceOutput is an auto-generated flat version of DatasourceOutput. // Where the contents of a field with a `mapstructure:,squash` tag are bubbled up. type FlatDatasourceOutput struct { - Foo *string `mapstructure:"foo" cty:"foo" hcl:"foo"` - Bar *string `mapstructure:"bar" cty:"bar" hcl:"bar"` + VmId *uint `mapstructure:"vm_id" cty:"vm_id" hcl:"vm_id"` + VmName *string `mapstructure:"vm_name" cty:"vm_name" hcl:"vm_name"` + VmTags *string `mapstructure:"vm_tags" cty:"vm_tags" hcl:"vm_tags"` } // FlatMapstructure returns a new FlatDatasourceOutput. @@ -57,8 +88,9 @@ func (*DatasourceOutput) FlatMapstructure() interface{ HCL2Spec() map[string]hcl // The decoded values from this spec will then be applied to a FlatDatasourceOutput. func (*FlatDatasourceOutput) HCL2Spec() map[string]hcldec.Spec { s := map[string]hcldec.Spec{ - "foo": &hcldec.AttrSpec{Name: "foo", Type: cty.String, Required: false}, - "bar": &hcldec.AttrSpec{Name: "bar", Type: cty.String, Required: false}, + "vm_id": &hcldec.AttrSpec{Name: "vm_id", Type: cty.Number, Required: false}, + "vm_name": &hcldec.AttrSpec{Name: "vm_name", Type: cty.String, Required: false}, + "vm_tags": &hcldec.AttrSpec{Name: "vm_tags", Type: cty.String, Required: false}, } return s } diff --git a/datasource/proxmox/data_acc_test.go b/datasource/proxmox/data_acc_test.go deleted file mode 100644 index 7c241dec..00000000 --- a/datasource/proxmox/data_acc_test.go +++ /dev/null @@ -1,65 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package proxmoxtemplate - -import ( - _ "embed" - "fmt" - "io/ioutil" - "os" - "os/exec" - "regexp" - "testing" - - "github.com/hashicorp/packer-plugin-sdk/acctest" -) - -//go:embed test-fixtures/template.pkr.hcl -var testDatasourceHCL2Basic string - -// Run with: PACKER_ACC=1 go test -count 1 -v ./datasource/scaffolding/data_acc_test.go -timeout=120m -func TestAccScaffoldingDatasource(t *testing.T) { - testCase := &acctest.PluginTestCase{ - Name: "scaffolding_datasource_basic_test", - Setup: func() error { - return nil - }, - Teardown: func() error { - return nil - }, - Template: testDatasourceHCL2Basic, - Type: "scaffolding-my-datasource", - Check: func(buildCommand *exec.Cmd, logfile string) error { - if buildCommand.ProcessState != nil { - if buildCommand.ProcessState.ExitCode() != 0 { - return fmt.Errorf("Bad exit code. Logfile: %s", logfile) - } - } - - logs, err := os.Open(logfile) - if err != nil { - return fmt.Errorf("Unable find %s", logfile) - } - defer logs.Close() - - logsBytes, err := ioutil.ReadAll(logs) - if err != nil { - return fmt.Errorf("Unable to read %s", logfile) - } - logsString := string(logsBytes) - - fooLog := "null.basic-example: foo: foo-value" - barLog := "null.basic-example: bar: bar-value" - - if matched, _ := regexp.MatchString(fooLog+".*", logsString); !matched { - t.Fatalf("logs doesn't contain expected foo value %q", logsString) - } - if matched, _ := regexp.MatchString(barLog+".*", logsString); !matched { - t.Fatalf("logs doesn't contain expected bar value %q", logsString) - } - return nil - }, - } - acctest.TestPlugin(t, testCase) -} diff --git a/datasource/proxmox/data_test.go b/datasource/proxmox/data_test.go new file mode 100644 index 00000000..95861096 --- /dev/null +++ b/datasource/proxmox/data_test.go @@ -0,0 +1,189 @@ +package proxmoxtemplate + +import ( + "fmt" + "net/http" + "net/http/httptest" + "net/url" + "testing" + + "github.com/Telmate/proxmox-api-go/proxmox" + "github.com/stretchr/testify/require" +) + +// For the sake of saving code clean have left only test-related fields in these JSONs. +const mockResourcesResponse = `{"data":[ + {"type":"qemu","name":"first-vm","vmid":100,"id":"qemu/100","template":0,"node":"pve"}, + {"id":"qemu/101","template":0,"type":"qemu","name":"second-vm","tags":"blue;red","vmid":101,"node":"pve"}, + {"node":"pve","template":1,"id":"qemu/102","vmid":102,"tags":"blue","type":"qemu","name":"template-three"} +]}` +const mockVmConfig100Response = `{"data": + {"meta":"creation-qemu=8.1.5,ctime=1729285344","name":"first-vm"} +}` +const mockVmConfig101Response = `{"data": + {"meta":"creation-qemu=8.1.5,ctime=1729285359","name":"second-vm","tags":"blue;red"} +}` +const mockVmConfig102Response = `{"data": + {"template":1,"meta":"creation-qemu=8.1.5,ctime=1729285377","tags":"blue","name":"template-three"} +}` + +// All configs have to have all default fields so add them from `configDefault` (in place). +func (c *Config) saturateWithDefault(configDefault Config) { + c.proxmoxURL = configDefault.proxmoxURL + c.SkipCertValidation = configDefault.SkipCertValidation + c.Username = configDefault.Username + c.Token = configDefault.Token +} + +func TestExecute(t *testing.T) { + mockAPI := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method == http.MethodGet { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + switch path := r.URL.Path; path { + case "/cluster/resources": + _, _ = fmt.Fprintln(w, mockResourcesResponse) + case "/nodes/pve/qemu/100/config": + _, _ = fmt.Fprintln(w, mockVmConfig100Response) + case "/nodes/pve/qemu/101/config": + _, _ = fmt.Fprintln(w, mockVmConfig101Response) + case "/nodes/pve/qemu/102/config": + _, _ = fmt.Fprintln(w, mockVmConfig102Response) + default: + return + } + } + })) + defer mockAPI.Close() + + pxmxURL, _ := url.Parse(mockAPI.URL) + defaultConfig := Config{ + proxmoxURL: pxmxURL, + SkipCertValidation: true, + Username: "dummy@vmhost", + Token: "dummy", + } + + dsTestConfigs := []struct { + name string + expectFailure bool + expectedVmId int64 + configDiff Config + }{ + { + name: "guest with name first-vm found, no error", + expectFailure: false, + expectedVmId: 100, + configDiff: Config{ + Name: "first-vm", + }, + }, + { + name: "no existent guest matches filter, error", + expectFailure: true, + configDiff: Config{ + Name: "firstest-vm", + }, + }, + { + name: "found guest by regex filter, no error", + expectFailure: false, + expectedVmId: 101, + configDiff: Config{ + NameRegex: "sec.*", + }, + }, + { + name: "multiple guests match the regex, but latest not used, error", + expectFailure: true, + configDiff: Config{ + NameRegex: ".*-vm", + }, + }, + { + name: "multiple guests match the regex and latest used, error", + expectFailure: false, + expectedVmId: 101, + configDiff: Config{ + NameRegex: ".*-vm", + Latest: true, + }, + }, + { + name: "found guest that is template, no error", + expectFailure: false, + expectedVmId: 102, + configDiff: Config{ + Template: true, + }, + }, + { + name: "found latest guest at node, no error", + expectFailure: false, + expectedVmId: 102, + configDiff: Config{ + Node: "pve", + Latest: true, + }, + }, + { + name: "proxmox host not found, error", + expectFailure: true, + configDiff: Config{ + Node: "proxmox-host", + }, + }, + { + name: "found guest with set of tags, no error", + expectFailure: false, + expectedVmId: 101, + configDiff: Config{ + VmTags: "blue;red", + }, + }, + { + name: "found multiple guests with tag, error", + expectFailure: true, + configDiff: Config{ + VmTags: "blue", + }, + }, + } + + for _, dsTestConfig := range dsTestConfigs { + t.Run(dsTestConfig.name, func(t *testing.T) { + dsTestConfig.configDiff.saturateWithDefault(defaultConfig) + ds := Datasource{ + config: dsTestConfig.configDiff, + } + + result, err := ds.Execute() + if err != nil && !dsTestConfig.expectFailure { + t.Fatalf("unexpected failure: %s", err) + } + if err == nil && dsTestConfig.expectFailure { + t.Errorf("expected failure, but execution succeeded") + } + if err == nil { + vmIdInt64, _ := result.GetAttr("vm_id").AsBigFloat().Int64() + vmName := result.GetAttr("vm_name").AsString() + vmTags := result.GetAttr("vm_tags").AsString() + t.Logf("Returned: vmId=%d, vmName=%s, vmTags=%s", vmIdInt64, vmName, vmTags) + require.Equal(t, dsTestConfig.expectedVmId, vmIdInt64) + } + }) + } +} + +func TestParseMetaField(t *testing.T) { + const metaField = `creation-qemu=8.1.5,ctime=1729285377` + result, err := parseMetaField(metaField) + require.NoError(t, err) + require.Equal(t, 1729285377, result) +} + +func TestCompareTags(t *testing.T) { + configTags := []string{"blue", "green"} + nodeTags := []proxmox.Tag{"blue", "red"} + require.Equal(t, false, configTagsMatchNodeTags(configTags, nodeTags)) +} diff --git a/datasource/proxmox/test-fixtures/template.pkr.hcl b/datasource/proxmox/test-fixtures/template.pkr.hcl deleted file mode 100644 index 6a974a2e..00000000 --- a/datasource/proxmox/test-fixtures/template.pkr.hcl +++ /dev/null @@ -1,28 +0,0 @@ -data "proxmox-template" "default" { - proxmox_url = "https://localhost:8006/api2/json" - insecure_skip_tls_verify = true - username = "root@pam" - password = "password" -} - -locals { - foo = data.proxmox-template.default.foo - bar = data.proxmox-template.default.bar -} - -source "null" "basic-example" { - communicator = "none" -} - -build { - sources = [ - "source.null.basic-example" - ] - - provisioner "shell-local" { - inline = [ - "echo foo: ${local.foo}", - "echo bar: ${local.bar}", - ] - } -} diff --git a/docs-partials/datasource/proxmox/Config-not-required.mdx b/docs-partials/datasource/proxmox/Config-not-required.mdx new file mode 100644 index 00000000..64a89210 --- /dev/null +++ b/docs-partials/datasource/proxmox/Config-not-required.mdx @@ -0,0 +1,49 @@ + + +- `proxmox_url` (string) - URL to the Proxmox API, including the full path, + so `https://:/api2/json` for example. + Can also be set via the `PROXMOX_URL` environment variable. + +- `insecure_skip_tls_verify` (bool) - Skip validating the certificate. + +- `username` (string) - Username when authenticating to Proxmox, including + the realm. For example `user@pve` to use the local Proxmox realm. When using + token authentication, the username must include the token id after an exclamation + mark. For example, `user@pve!tokenid`. + Can also be set via the `PROXMOX_USERNAME` environment variable. + +- `password` (string) - Password for the user. + For API tokens please use `token`. + Can also be set via the `PROXMOX_PASSWORD` environment variable. + Either `password` or `token` must be specifed. If both are set, + `token` takes precedence. + +- `token` (string) - Token for authenticating API calls. + This allows the API client to work with API tokens instead of user passwords. + Can also be set via the `PROXMOX_TOKEN` environment variable. + Either `password` or `token` must be specifed. If both are set, + `token` takes precedence. + +- `task_timeout` (duration string | ex: "1h5m2s") - `task_timeout` (duration string | ex: "10m") - The timeout for + Promox API operations, e.g. clones. Defaults to 1 minute. + +- `name` (string) - Filter that returns `vm_id` for guest which name exactly matches this value. + Options `name` and `name_regex` are mutually exclusive. + +- `name_regex` (string) - Filter that returns `vm_id` for guest which name matches the regular expression. + Expression must use [Go Regex Syntax](https://pkg.go.dev/regexp/syntax). + Options `name` and `name_regex` are mutually exclusive. + +- `template` (bool) - Filter that returns guest `vm_id` only when guest is template. + +- `node` (string) - Filter that returns `vm_id` only when guest is located on the specified PVE node. + +- `vm_tags` (string) - Filter that returns `vm_id` for guest which has all these tags. When you need to + specify more than one tag, use semicolon as separator (`"tag1;tag2"`). + Every specified tag must exist in guest. + +- `latest` (bool) - This filter determines how to handle multiple guests that were matched with all + previous filters. Guest creation time is being used to find latest. + By default, multiple matching guests results in an error. + + diff --git a/docs-partials/datasource/proxmox/Config.mdx b/docs-partials/datasource/proxmox/Config.mdx new file mode 100644 index 00000000..c681f1ab --- /dev/null +++ b/docs-partials/datasource/proxmox/Config.mdx @@ -0,0 +1,9 @@ + + +Datasource has a bunch of filters which you can use, for example, to find the latest available +template in the cluster that matches defined filters. + +You can combine any number of filters but all of them will be conjuncted with AND. +When datasource cannot return only one (zero or >1) guest identifiers it will return error. + + diff --git a/docs-partials/datasource/proxmox/DatasourceOutput.mdx b/docs-partials/datasource/proxmox/DatasourceOutput.mdx new file mode 100644 index 00000000..88fce6a8 --- /dev/null +++ b/docs-partials/datasource/proxmox/DatasourceOutput.mdx @@ -0,0 +1,9 @@ + + +- `vm_id` (uint) - Identifier of the found guest. + +- `vm_name` (string) - Name of the found guest. + +- `vm_tags` (string) - Tags of the found guest separated with semicolon. + + diff --git a/docs/README.md b/docs/README.md index 3d02cf72..f15b45cc 100644 --- a/docs/README.md +++ b/docs/README.md @@ -35,3 +35,8 @@ Packer is able to target both ISO and existing Cloud-Init images. takes an ISO source, runs any provisioning necessary on the image after launching it, then creates a virtual machine template. +#### Data Sources + +- [data source](/packer/integrations/hashicorp/proxmox/latest/components/datasource/template) - The proxmox template + datasource is able to get info about existing guests from Proxmox cluster and return VM ID of a single guest that + matches all specified filters. This ID can later be used in the 'clone' builder to select template. diff --git a/docs/datasources/template.mdx b/docs/datasources/template.mdx new file mode 100644 index 00000000..ce7d3648 --- /dev/null +++ b/docs/datasources/template.mdx @@ -0,0 +1,70 @@ +--- +description: | + The proxmox template datasource is able to get info about existing guests + from Proxmox cluster and return VM ID of a single guest that matches all + specified filters. This ID can later be used in the 'clone' builder to + select template. +page_title: Proxmox Clone - Datasources +sidebar_title: proxmox-template +nav_title: Template +--- + +# Proxmox Template Datasource + +Type: `proxmox-template` +Artifact BuilderId: `proxmox.template` + +The `proxmox-template` datasource is able to get info about existing guests +from [Proxmox](https://www.proxmox.com/en/proxmox-ve) cluster and return VM +ID of a single guest that matches all specified filters. This ID can later +be used in the `proxmox-clone` builder to select template. + +## Configuration Reference + +@include 'datasource/proxmox/Config.mdx' + +## Optional: + +@include 'datasource/proxmox/Config-not-required.mdx' + +## Output: + +@include 'datasource/proxmox/DatasourceOutput.mdx' + +## Example Usage + +This is a very basic example which connects to local PVE host, finds the latest +guest which name matches the regex `image-.*` and which type is `template`. The +ID is then printed to console as output variable. + +```hcl +data "proxmox-template" "default" { + proxmox_url = "https://localhost:8006/api2/json" + insecure_skip_tls_verify = true + username = "root@pam" + password = "password" + name_regex = "image-.*" + template = true + latest = true +} + +locals { + vm_id = data.proxmox-template.default.vm_id +} + +source "null" "basic-example" { + communicator = "none" +} + +build { + sources = [ + "source.null.basic-example" + ] + + provisioner "shell-local" { + inline = [ + "echo vm_id: ${local.vm_id}", + ] + } +} +``` From 9adc728c19fff76269a2fa8158a787e75b057cfd Mon Sep 17 00:00:00 2001 From: Castor Sky Date: Tue, 10 Dec 2024 00:28:37 +0300 Subject: [PATCH 3/4] review: refactor filterGuests Used anonymous functions instead of a bunch of named ones. --- datasource/proxmox/data.go | 92 +++++++++++++------------------------- 1 file changed, 30 insertions(+), 62 deletions(-) diff --git a/datasource/proxmox/data.go b/datasource/proxmox/data.go index d9c51e3d..4695b18e 100644 --- a/datasource/proxmox/data.go +++ b/datasource/proxmox/data.go @@ -214,7 +214,7 @@ func (d *Datasource) Execute() (cty.Value, error) { return hcl2helper.HCL2ValueFromConfig(output, d.OutputSpec()), nil } -// Find the latest VM among filtered. +// findLatestConfig finds the latest VM among those passed using `configs`. // The `meta` field contains info about creation time (but it is not described in API docs). func findLatestConfig(configs []vmConfig) (vmConfig, error) { var result vmConfig @@ -253,87 +253,55 @@ func getVmConfigs(client *proxmox.Client, vmList []proxmox.GuestResource) ([]vmC return result, nil } -// Drop guests from list that are not match some filters in the datasource config. +// filterGuests removes guests from the `guests` list that do not match some filters in the datasource config. func filterGuests(config Config, guests []proxmox.GuestResource) []proxmox.GuestResource { - var result []proxmox.GuestResource + filterFuncs := make([]func(proxmox.GuestResource) bool, 0) if config.Name != "" { - result = filterByName(guests, config.Name) - } else { - result = guests + filterFuncs = append(filterFuncs, func(vm proxmox.GuestResource) bool { + return vm.Name == config.Name + }) } if config.NameRegex != "" { - result = filterByNameRegex(guests, config.NameRegex) - } else { - if config.Name == "" { - result = guests - } + filterFuncs = append(filterFuncs, func(vm proxmox.GuestResource) bool { + return regexp.MustCompile(config.NameRegex).MatchString(vm.Name) + }) } if config.Template { - result = filterByTemplate(result) - } - if config.Node != "" { - result = filterByNode(result, config.Node) - } - if config.VmTags != "" { - result = filterByTags(result, config.VmTags) - } - - return result -} - -func filterByName(guests []proxmox.GuestResource, name string) []proxmox.GuestResource { - result := make([]proxmox.GuestResource, 0) - for _, i := range guests { - if i.Name == name { - result = append(result, i) - } - } - return result -} - -func filterByNameRegex(guests []proxmox.GuestResource, nameRegex string) []proxmox.GuestResource { - re, _ := regexp.Compile(nameRegex) - result := make([]proxmox.GuestResource, 0) - for _, i := range guests { - if re.MatchString(i.Name) { - result = append(result, i) - } + filterFuncs = append(filterFuncs, func(vm proxmox.GuestResource) bool { + return vm.Template + }) } - return result -} -func filterByTemplate(guests []proxmox.GuestResource) []proxmox.GuestResource { - result := make([]proxmox.GuestResource, 0) - for _, i := range guests { - if i.Template { - result = append(result, i) - } + if config.Node != "" { + filterFuncs = append(filterFuncs, func(vm proxmox.GuestResource) bool { + return vm.Node == config.Node + }) } - return result -} -func filterByNode(guests []proxmox.GuestResource, node string) []proxmox.GuestResource { - result := make([]proxmox.GuestResource, 0) - for _, i := range guests { - if i.Node == node { - result = append(result, i) - } + if config.VmTags != "" { + // Split tags string because it can contain several tags separated with ";" + tagsSplitted := strings.Split(config.VmTags, ";") + filterFuncs = append(filterFuncs, func(vm proxmox.GuestResource) bool { + return len(vm.Tags) > 0 && configTagsMatchNodeTags(tagsSplitted, vm.Tags) + }) } - return result -} -func filterByTags(guests []proxmox.GuestResource, tags string) []proxmox.GuestResource { result := make([]proxmox.GuestResource, 0) - // Split tags string because it can contain several tags separated with ";" - tagsSplitted := strings.Split(tags, ";") for _, guest := range guests { - if len(guest.Tags) > 0 && configTagsMatchNodeTags(tagsSplitted, guest.Tags) { + var ok bool + for _, guestPassedFilter := range filterFuncs { + if ok = guestPassedFilter(guest); !ok { + break + } + } + if ok { result = append(result, guest) } } + return result } From 4a177f72bfc08f632379c7c273aaeb7ac60bdd2e Mon Sep 17 00:00:00 2001 From: Castor Sky Date: Tue, 10 Dec 2024 23:55:33 +0300 Subject: [PATCH 4/4] edit: rename package from `template` to `virtualmachine` Renamed package and corrected documentation accordingly. --- .web-docs/README.md | 7 +- .../{template => virtualmachine}/README.md | 69 +++++++++------- .web-docs/metadata.hcl | 4 +- .../{proxmox => virtualmachine}/data.go | 75 ++++++++++++------ .../data.hcl2spec.go | 2 +- .../{proxmox => virtualmachine}/data_test.go | 10 ++- .../datasource/proxmox/DatasourceOutput.mdx | 9 --- .../Config-not-required.mdx | 22 +++--- .../{proxmox => virtualmachine}/Config.mdx | 4 +- .../virtualmachine/DatasourceOutput.mdx | 9 +++ docs/README.md | 7 +- docs/datasources/template.mdx | 70 ----------------- docs/datasources/virtualmachine.mdx | 78 +++++++++++++++++++ main.go | 4 +- 14 files changed, 212 insertions(+), 158 deletions(-) rename .web-docs/components/data-source/{template => virtualmachine}/README.md (59%) rename datasource/{proxmox => virtualmachine}/data.go (78%) rename datasource/{proxmox => virtualmachine}/data.hcl2spec.go (99%) rename datasource/{proxmox => virtualmachine}/data_test.go (96%) delete mode 100644 docs-partials/datasource/proxmox/DatasourceOutput.mdx rename docs-partials/datasource/{proxmox => virtualmachine}/Config-not-required.mdx (66%) rename docs-partials/datasource/{proxmox => virtualmachine}/Config.mdx (81%) create mode 100644 docs-partials/datasource/virtualmachine/DatasourceOutput.mdx delete mode 100644 docs/datasources/template.mdx create mode 100644 docs/datasources/virtualmachine.mdx diff --git a/.web-docs/README.md b/.web-docs/README.md index f15b45cc..c83156f3 100644 --- a/.web-docs/README.md +++ b/.web-docs/README.md @@ -37,6 +37,7 @@ Packer is able to target both ISO and existing Cloud-Init images. #### Data Sources -- [data source](/packer/integrations/hashicorp/proxmox/latest/components/datasource/template) - The proxmox template - datasource is able to get info about existing guests from Proxmox cluster and return VM ID of a single guest that - matches all specified filters. This ID can later be used in the 'clone' builder to select template. +- [virtualmachine](/packer/integrations/hashicorp/proxmox/latest/components/datasource/virtualmachine) - The proxmox + virtual machine datasource retrieves information about existing virtual machines + from Proxmox cluster and returns VM ID of one virtual machine that matches all + specified filters. This ID can be used in the clone builder to select a template. diff --git a/.web-docs/components/data-source/template/README.md b/.web-docs/components/data-source/virtualmachine/README.md similarity index 59% rename from .web-docs/components/data-source/template/README.md rename to .web-docs/components/data-source/virtualmachine/README.md index c1d2bd9f..75bb8118 100644 --- a/.web-docs/components/data-source/template/README.md +++ b/.web-docs/components/data-source/virtualmachine/README.md @@ -1,14 +1,13 @@ -Type: `proxmox-template` -Artifact BuilderId: `proxmox.template` +Type: `proxmox-virtualmachine` +Artifact BuilderId: `proxmox.virtualmachine` -The `proxmox-template` datasource is able to get info about existing guests -from [Proxmox](https://www.proxmox.com/en/proxmox-ve) cluster and return VM -ID of a single guest that matches all specified filters. This ID can later -be used in the `proxmox-clone` builder to select template. +The `proxmox-virtualmachine` datasource retrieves information about existing virtual machines +from [Proxmox](https://www.proxmox.com/en/proxmox-ve) cluster and returns VM ID of one virtual machine +that matches all specified filters. This ID can be used in the `proxmox-clone` builder to select a template. ## Configuration Reference - + Datasource has a bunch of filters which you can use, for example, to find the latest available template in the cluster that matches defined filters. @@ -16,12 +15,12 @@ template in the cluster that matches defined filters. You can combine any number of filters but all of them will be conjuncted with AND. When datasource cannot return only one (zero or >1) guest identifiers it will return error. - + ## Optional: - + - `proxmox_url` (string) - URL to the Proxmox API, including the full path, so `https://:/api2/json` for example. @@ -50,60 +49,70 @@ When datasource cannot return only one (zero or >1) guest identifiers it will re - `task_timeout` (duration string | ex: "1h5m2s") - `task_timeout` (duration string | ex: "10m") - The timeout for Promox API operations, e.g. clones. Defaults to 1 minute. -- `name` (string) - Filter that returns `vm_id` for guest which name exactly matches this value. +- `name` (string) - Filter that returns `vm_id` for virtual machine which name exactly matches this value. Options `name` and `name_regex` are mutually exclusive. -- `name_regex` (string) - Filter that returns `vm_id` for guest which name matches the regular expression. +- `name_regex` (string) - Filter that returns `vm_id` for virtual machine which name matches the regular expression. Expression must use [Go Regex Syntax](https://pkg.go.dev/regexp/syntax). Options `name` and `name_regex` are mutually exclusive. -- `template` (bool) - Filter that returns guest `vm_id` only when guest is template. +- `template` (bool) - Filter that returns virtual machine `vm_id` only when virtual machine is template. -- `node` (string) - Filter that returns `vm_id` only when guest is located on the specified PVE node. +- `node` (string) - Filter that returns `vm_id` only when virtual machine is located on the specified PVE node. -- `vm_tags` (string) - Filter that returns `vm_id` for guest which has all these tags. When you need to +- `vm_tags` (string) - Filter that returns `vm_id` for virtual machine which has all these tags. When you need to specify more than one tag, use semicolon as separator (`"tag1;tag2"`). - Every specified tag must exist in guest. + Every specified tag must exist in virtual machine. -- `latest` (bool) - This filter determines how to handle multiple guests that were matched with all - previous filters. Guest creation time is being used to find latest. - By default, multiple matching guests results in an error. +- `latest` (bool) - This filter determines how to handle multiple virtual machines that were matched with all + previous filters. Virtual machine creation time is being used to find latest. + By default, multiple matching virtual machines results in an error. - + ## Output: - + -- `vm_id` (uint) - Identifier of the found guest. +- `vm_id` (uint) - Identifier of the found virtual machine. -- `vm_name` (string) - Name of the found guest. +- `vm_name` (string) - Name of the found virtual machine. -- `vm_tags` (string) - Tags of the found guest separated with semicolon. +- `vm_tags` (string) - Tags of the found virtual machine separated with semicolon. - + ## Example Usage This is a very basic example which connects to local PVE host, finds the latest guest which name matches the regex `image-.*` and which type is `template`. The -ID is then printed to console as output variable. +ID of the virtual machine is printed to console as output variable. ```hcl -data "proxmox-template" "default" { - proxmox_url = "https://localhost:8006/api2/json" +variable "password" { + type = string + default = "supersecret" +} + +variable "username" { + type = string + default = "apiuser@pve" +} + +data "proxmox-virtualmachine" "default" { + proxmox_url = "https://my-proxmox.my-domain:8006/api2/json" insecure_skip_tls_verify = true - username = "root@pam" - password = "password" + username = "${var.username}" + password = "${var.password}" name_regex = "image-.*" template = true latest = true } locals { - vm_id = data.proxmox-template.default.vm_id + vm_id = data.proxmox-virtualmachine.default.vm_id } source "null" "basic-example" { diff --git a/.web-docs/metadata.hcl b/.web-docs/metadata.hcl index ab7afc53..5c4b6d97 100644 --- a/.web-docs/metadata.hcl +++ b/.web-docs/metadata.hcl @@ -19,7 +19,7 @@ integration { } component { type = "data-source" - name = "Proxmox Template" - slug = "template" + name = "Proxmox Virtual Machine" + slug = "virtualmachine" } } diff --git a/datasource/proxmox/data.go b/datasource/virtualmachine/data.go similarity index 78% rename from datasource/proxmox/data.go rename to datasource/virtualmachine/data.go index 4695b18e..d3ff538d 100644 --- a/datasource/proxmox/data.go +++ b/datasource/virtualmachine/data.go @@ -4,7 +4,7 @@ //go:generate packer-sdc struct-markdown //go:generate packer-sdc mapstructure-to-hcl2 -type Config,DatasourceOutput -package proxmoxtemplate +package virtualmachine import ( "crypto/tls" @@ -63,24 +63,24 @@ type Config struct { // `task_timeout` (duration string | ex: "10m") - The timeout for // Promox API operations, e.g. clones. Defaults to 1 minute. TaskTimeout time.Duration `mapstructure:"task_timeout"` - // Filter that returns `vm_id` for guest which name exactly matches this value. + // Filter that returns `vm_id` for virtual machine which name exactly matches this value. // Options `name` and `name_regex` are mutually exclusive. Name string `mapstructure:"name"` - // Filter that returns `vm_id` for guest which name matches the regular expression. + // Filter that returns `vm_id` for virtual machine which name matches the regular expression. // Expression must use [Go Regex Syntax](https://pkg.go.dev/regexp/syntax). // Options `name` and `name_regex` are mutually exclusive. NameRegex string `mapstructure:"name_regex"` - // Filter that returns guest `vm_id` only when guest is template. + // Filter that returns virtual machine `vm_id` only when virtual machine is template. Template bool `mapstructure:"template"` - // Filter that returns `vm_id` only when guest is located on the specified PVE node. + // Filter that returns `vm_id` only when virtual machine is located on the specified PVE node. Node string `mapstructure:"node"` - // Filter that returns `vm_id` for guest which has all these tags. When you need to + // Filter that returns `vm_id` for virtual machine which has all these tags. When you need to // specify more than one tag, use semicolon as separator (`"tag1;tag2"`). - // Every specified tag must exist in guest. + // Every specified tag must exist in virtual machine. VmTags string `mapstructure:"vm_tags"` - // This filter determines how to handle multiple guests that were matched with all - // previous filters. Guest creation time is being used to find latest. - // By default, multiple matching guests results in an error. + // This filter determines how to handle multiple virtual machines that were matched with all + // previous filters. Virtual machine creation time is being used to find latest. + // By default, multiple matching virtual machines results in an error. Latest bool `mapstructure:"latest"` } @@ -89,11 +89,11 @@ type Datasource struct { } type DatasourceOutput struct { - // Identifier of the found guest. + // Identifier of the found virtual machine. VmId uint `mapstructure:"vm_id"` - // Name of the found guest. + // Name of the found virtual machine. VmName string `mapstructure:"vm_name"` - // Tags of the found guest separated with semicolon. + // Tags of the found virtual machine separated with semicolon. VmTags string `mapstructure:"vm_tags"` } @@ -180,7 +180,7 @@ func (d *Datasource) Execute() (cty.Value, error) { filteredVms := filterGuests(d.config, vmList) if len(filteredVms) == 0 { - return cty.NullVal(cty.EmptyObject), errors.New("not a single vm matches the configured filters") + return cty.NullVal(cty.EmptyObject), errors.New("no virtual machine matches the filters") } if d.config.Latest { @@ -195,11 +195,11 @@ func (d *Datasource) Execute() (cty.Value, error) { } vmId = latestConfig["vmid"].(uint) - vmName = latestConfig["name"].(string) - vmTags = latestConfig["tags"].(string) + vmName = configValueOrEmpty(&latestConfig, "name") + vmTags = configValueOrEmpty(&latestConfig, "tags") } else { if len(filteredVms) > 1 { - return cty.NullVal(cty.EmptyObject), errors.New("more than one guest passed filters, cannot return vm_id") + return cty.NullVal(cty.EmptyObject), errors.New("more than one virtual machine matched the filters") } vmId = filteredVms[0].Id vmName = filteredVms[0].Name @@ -230,13 +230,13 @@ func findLatestConfig(configs []vmConfig) (vmConfig, error) { result = configs[i] } } else { - return nil, errors.New("no meta field in the guest config") + return nil, errors.New("no meta field in the virtual machine config") } } return result, nil } -// Get configs from PVE in 'map[string]interface{}' format for all VMs in the list. +// getVmConfigs retrieves configs from PVE in 'map[string]interface{}' format for all VMs in the list. // Also add value of VM ID to every config (useful for further steps). func getVmConfigs(client *proxmox.Client, vmList []proxmox.GuestResource) ([]vmConfig, error) { var result []vmConfig @@ -253,7 +253,7 @@ func getVmConfigs(client *proxmox.Client, vmList []proxmox.GuestResource) ([]vmC return result, nil } -// filterGuests removes guests from the `guests` list that do not match some filters in the datasource config. +// filterGuests removes virtual machines from the `guests` list that do not match some filters in the datasource config. func filterGuests(config Config, guests []proxmox.GuestResource) []proxmox.GuestResource { filterFuncs := make([]func(proxmox.GuestResource) bool, 0) @@ -292,6 +292,9 @@ func filterGuests(config Config, guests []proxmox.GuestResource) []proxmox.Guest result := make([]proxmox.GuestResource, 0) for _, guest := range guests { var ok bool + if len(filterFuncs) == 0 { + ok = true + } for _, guestPassedFilter := range filterFuncs { if ok = guestPassedFilter(guest); !ok { break @@ -305,6 +308,8 @@ func filterGuests(config Config, guests []proxmox.GuestResource) []proxmox.Guest return result } +// configTagsMatchNodeTags compares two lists of strings and returns true only when all +// elements from the first list are present in the second list. func configTagsMatchNodeTags(configTags []string, nodeTags []proxmox.Tag) bool { var countOfMatchedTags int for _, configTag := range configTags { @@ -325,6 +330,7 @@ func configTagsMatchNodeTags(configTags []string, nodeTags []proxmox.Tag) bool { return true } +// newProxmoxClient creates new client and tries to connect and log in to Proxmox instance. func newProxmoxClient(config Config) (*proxmox.Client, error) { tlsConfig := &tls.Config{ InsecureSkipVerify: config.SkipCertValidation, @@ -332,7 +338,7 @@ func newProxmoxClient(config Config) (*proxmox.Client, error) { client, err := proxmox.NewClient(strings.TrimSuffix(config.proxmoxURL.String(), "/"), nil, "", tlsConfig, "", int(config.TaskTimeout.Seconds())) if err != nil { - return nil, err + return nil, fmt.Errorf("could not connect to Proxmox: %w", err) } *proxmox.Debug = config.PackerDebug @@ -346,17 +352,19 @@ func newProxmoxClient(config Config) (*proxmox.Client, error) { log.Print("using password auth") err = client.Login(config.Username, config.Password, "") if err != nil { - return nil, err + return nil, fmt.Errorf("could not log in to Proxmox: %w", err) } } return client, nil } +// parseMetaField parses the string from the `meta` field and returns integer value +// representing the creation date of the virtual machine in epoch seconds format. func parseMetaField(field string) (int, error) { re, err := regexp.Compile(`.*ctime=(?P[0-9]+).*`) if err != nil { - return 0, err + return 0, fmt.Errorf("could not compile regex to parse meta field: %w", err) } matched := re.MatchString(field) @@ -366,11 +374,12 @@ func parseMetaField(field string) (int, error) { valueStr := re.ReplaceAllString(field, "${ctime}") value, err := strconv.Atoi(valueStr) if err != nil { - return 0, err + return 0, fmt.Errorf("could not convert date field to int: %w", err) } return value, nil } +// joinTags used to combine list of strings into one string with defined separator. func joinTags(tags []proxmox.Tag, separator string) string { tagsAsStrings := make([]string, len(tags)) for i, tag := range tags { @@ -378,3 +387,21 @@ func joinTags(tags []proxmox.Tag, separator string) string { } return strings.Join(tagsAsStrings, separator) } + +// configValueOrEmpty tries to retrieve string by key from dynamic map of interfaces. +// In case when key not found or there was an error, this function returns empty string. +func configValueOrEmpty(values *vmConfig, key string) string { + result := "" + if values != nil { + value, exists := (*values)[key] + if !exists { + return result + } + strValue, ok := value.(string) + if !ok { + return result + } + result = strValue + } + return result +} diff --git a/datasource/proxmox/data.hcl2spec.go b/datasource/virtualmachine/data.hcl2spec.go similarity index 99% rename from datasource/proxmox/data.hcl2spec.go rename to datasource/virtualmachine/data.hcl2spec.go index a8badc9e..bf89735f 100644 --- a/datasource/proxmox/data.hcl2spec.go +++ b/datasource/virtualmachine/data.hcl2spec.go @@ -1,6 +1,6 @@ // Code generated by "packer-sdc mapstructure-to-hcl2"; DO NOT EDIT. -package proxmoxtemplate +package virtualmachine import ( "github.com/hashicorp/hcl/v2/hcldec" diff --git a/datasource/proxmox/data_test.go b/datasource/virtualmachine/data_test.go similarity index 96% rename from datasource/proxmox/data_test.go rename to datasource/virtualmachine/data_test.go index 95861096..254ad38b 100644 --- a/datasource/proxmox/data_test.go +++ b/datasource/virtualmachine/data_test.go @@ -1,4 +1,4 @@ -package proxmoxtemplate +package virtualmachine import ( "fmt" @@ -126,6 +126,14 @@ func TestExecute(t *testing.T) { Latest: true, }, }, + { + name: "found latest guest at cluster, no error", + expectFailure: false, + expectedVmId: 102, + configDiff: Config{ + Latest: true, + }, + }, { name: "proxmox host not found, error", expectFailure: true, diff --git a/docs-partials/datasource/proxmox/DatasourceOutput.mdx b/docs-partials/datasource/proxmox/DatasourceOutput.mdx deleted file mode 100644 index 88fce6a8..00000000 --- a/docs-partials/datasource/proxmox/DatasourceOutput.mdx +++ /dev/null @@ -1,9 +0,0 @@ - - -- `vm_id` (uint) - Identifier of the found guest. - -- `vm_name` (string) - Name of the found guest. - -- `vm_tags` (string) - Tags of the found guest separated with semicolon. - - diff --git a/docs-partials/datasource/proxmox/Config-not-required.mdx b/docs-partials/datasource/virtualmachine/Config-not-required.mdx similarity index 66% rename from docs-partials/datasource/proxmox/Config-not-required.mdx rename to docs-partials/datasource/virtualmachine/Config-not-required.mdx index 64a89210..36591a38 100644 --- a/docs-partials/datasource/proxmox/Config-not-required.mdx +++ b/docs-partials/datasource/virtualmachine/Config-not-required.mdx @@ -1,4 +1,4 @@ - + - `proxmox_url` (string) - URL to the Proxmox API, including the full path, so `https://:/api2/json` for example. @@ -27,23 +27,23 @@ - `task_timeout` (duration string | ex: "1h5m2s") - `task_timeout` (duration string | ex: "10m") - The timeout for Promox API operations, e.g. clones. Defaults to 1 minute. -- `name` (string) - Filter that returns `vm_id` for guest which name exactly matches this value. +- `name` (string) - Filter that returns `vm_id` for virtual machine which name exactly matches this value. Options `name` and `name_regex` are mutually exclusive. -- `name_regex` (string) - Filter that returns `vm_id` for guest which name matches the regular expression. +- `name_regex` (string) - Filter that returns `vm_id` for virtual machine which name matches the regular expression. Expression must use [Go Regex Syntax](https://pkg.go.dev/regexp/syntax). Options `name` and `name_regex` are mutually exclusive. -- `template` (bool) - Filter that returns guest `vm_id` only when guest is template. +- `template` (bool) - Filter that returns virtual machine `vm_id` only when virtual machine is template. -- `node` (string) - Filter that returns `vm_id` only when guest is located on the specified PVE node. +- `node` (string) - Filter that returns `vm_id` only when virtual machine is located on the specified PVE node. -- `vm_tags` (string) - Filter that returns `vm_id` for guest which has all these tags. When you need to +- `vm_tags` (string) - Filter that returns `vm_id` for virtual machine which has all these tags. When you need to specify more than one tag, use semicolon as separator (`"tag1;tag2"`). - Every specified tag must exist in guest. + Every specified tag must exist in virtual machine. -- `latest` (bool) - This filter determines how to handle multiple guests that were matched with all - previous filters. Guest creation time is being used to find latest. - By default, multiple matching guests results in an error. +- `latest` (bool) - This filter determines how to handle multiple virtual machines that were matched with all + previous filters. Virtual machine creation time is being used to find latest. + By default, multiple matching virtual machines results in an error. - + diff --git a/docs-partials/datasource/proxmox/Config.mdx b/docs-partials/datasource/virtualmachine/Config.mdx similarity index 81% rename from docs-partials/datasource/proxmox/Config.mdx rename to docs-partials/datasource/virtualmachine/Config.mdx index c681f1ab..0cb9a598 100644 --- a/docs-partials/datasource/proxmox/Config.mdx +++ b/docs-partials/datasource/virtualmachine/Config.mdx @@ -1,4 +1,4 @@ - + Datasource has a bunch of filters which you can use, for example, to find the latest available template in the cluster that matches defined filters. @@ -6,4 +6,4 @@ template in the cluster that matches defined filters. You can combine any number of filters but all of them will be conjuncted with AND. When datasource cannot return only one (zero or >1) guest identifiers it will return error. - + diff --git a/docs-partials/datasource/virtualmachine/DatasourceOutput.mdx b/docs-partials/datasource/virtualmachine/DatasourceOutput.mdx new file mode 100644 index 00000000..becc453c --- /dev/null +++ b/docs-partials/datasource/virtualmachine/DatasourceOutput.mdx @@ -0,0 +1,9 @@ + + +- `vm_id` (uint) - Identifier of the found virtual machine. + +- `vm_name` (string) - Name of the found virtual machine. + +- `vm_tags` (string) - Tags of the found virtual machine separated with semicolon. + + diff --git a/docs/README.md b/docs/README.md index f15b45cc..c83156f3 100644 --- a/docs/README.md +++ b/docs/README.md @@ -37,6 +37,7 @@ Packer is able to target both ISO and existing Cloud-Init images. #### Data Sources -- [data source](/packer/integrations/hashicorp/proxmox/latest/components/datasource/template) - The proxmox template - datasource is able to get info about existing guests from Proxmox cluster and return VM ID of a single guest that - matches all specified filters. This ID can later be used in the 'clone' builder to select template. +- [virtualmachine](/packer/integrations/hashicorp/proxmox/latest/components/datasource/virtualmachine) - The proxmox + virtual machine datasource retrieves information about existing virtual machines + from Proxmox cluster and returns VM ID of one virtual machine that matches all + specified filters. This ID can be used in the clone builder to select a template. diff --git a/docs/datasources/template.mdx b/docs/datasources/template.mdx deleted file mode 100644 index ce7d3648..00000000 --- a/docs/datasources/template.mdx +++ /dev/null @@ -1,70 +0,0 @@ ---- -description: | - The proxmox template datasource is able to get info about existing guests - from Proxmox cluster and return VM ID of a single guest that matches all - specified filters. This ID can later be used in the 'clone' builder to - select template. -page_title: Proxmox Clone - Datasources -sidebar_title: proxmox-template -nav_title: Template ---- - -# Proxmox Template Datasource - -Type: `proxmox-template` -Artifact BuilderId: `proxmox.template` - -The `proxmox-template` datasource is able to get info about existing guests -from [Proxmox](https://www.proxmox.com/en/proxmox-ve) cluster and return VM -ID of a single guest that matches all specified filters. This ID can later -be used in the `proxmox-clone` builder to select template. - -## Configuration Reference - -@include 'datasource/proxmox/Config.mdx' - -## Optional: - -@include 'datasource/proxmox/Config-not-required.mdx' - -## Output: - -@include 'datasource/proxmox/DatasourceOutput.mdx' - -## Example Usage - -This is a very basic example which connects to local PVE host, finds the latest -guest which name matches the regex `image-.*` and which type is `template`. The -ID is then printed to console as output variable. - -```hcl -data "proxmox-template" "default" { - proxmox_url = "https://localhost:8006/api2/json" - insecure_skip_tls_verify = true - username = "root@pam" - password = "password" - name_regex = "image-.*" - template = true - latest = true -} - -locals { - vm_id = data.proxmox-template.default.vm_id -} - -source "null" "basic-example" { - communicator = "none" -} - -build { - sources = [ - "source.null.basic-example" - ] - - provisioner "shell-local" { - inline = [ - "echo vm_id: ${local.vm_id}", - ] - } -} -``` diff --git a/docs/datasources/virtualmachine.mdx b/docs/datasources/virtualmachine.mdx new file mode 100644 index 00000000..439de377 --- /dev/null +++ b/docs/datasources/virtualmachine.mdx @@ -0,0 +1,78 @@ +--- +description: | + The proxmox virtual machine datasource retrieves information about existing virtual machines + from Proxmox cluster and returns VM ID of one virtual machine that matches all + specified filters. This ID can be used in the clone builder to select a template. +page_title: Proxmox Clone - Datasources +sidebar_title: proxmox-template +nav_title: Template +--- + +# Proxmox Template Datasource + +Type: `proxmox-virtualmachine` +Artifact BuilderId: `proxmox.virtualmachine` + +The `proxmox-virtualmachine` datasource retrieves information about existing virtual machines +from [Proxmox](https://www.proxmox.com/en/proxmox-ve) cluster and returns VM ID of one virtual machine +that matches all specified filters. This ID can be used in the `proxmox-clone` builder to select a template. + +## Configuration Reference + +@include 'datasource/virtualmachine/Config.mdx' + +## Optional: + +@include 'datasource/virtualmachine/Config-not-required.mdx' + +## Output: + +@include 'datasource/virtualmachine/DatasourceOutput.mdx' + +## Example Usage + +This is a very basic example which connects to local PVE host, finds the latest +guest which name matches the regex `image-.*` and which type is `template`. The +ID of the virtual machine is printed to console as output variable. + +```hcl +variable "password" { + type = string + default = "supersecret" +} + +variable "username" { + type = string + default = "apiuser@pve" +} + +data "proxmox-virtualmachine" "default" { + proxmox_url = "https://my-proxmox.my-domain:8006/api2/json" + insecure_skip_tls_verify = true + username = "${var.username}" + password = "${var.password}" + name_regex = "image-.*" + template = true + latest = true +} + +locals { + vm_id = data.proxmox-virtualmachine.default.vm_id +} + +source "null" "basic-example" { + communicator = "none" +} + +build { + sources = [ + "source.null.basic-example" + ] + + provisioner "shell-local" { + inline = [ + "echo vm_id: ${local.vm_id}", + ] + } +} +``` diff --git a/main.go b/main.go index 91ce4609..c1d51a4b 100644 --- a/main.go +++ b/main.go @@ -11,7 +11,7 @@ import ( proxmoxclone "github.com/hashicorp/packer-plugin-proxmox/builder/proxmox/clone" proxmoxiso "github.com/hashicorp/packer-plugin-proxmox/builder/proxmox/iso" - proxmoxtemplate "github.com/hashicorp/packer-plugin-proxmox/datasource/proxmox" + "github.com/hashicorp/packer-plugin-proxmox/datasource/virtualmachine" "github.com/hashicorp/packer-plugin-proxmox/version" ) @@ -22,7 +22,7 @@ func main() { pps.RegisterBuilder(plugin.DEFAULT_NAME, new(proxmoxiso.Builder)) pps.RegisterBuilder("iso", new(proxmoxiso.Builder)) pps.RegisterBuilder("clone", new(proxmoxclone.Builder)) - pps.RegisterDatasource("template", new(proxmoxtemplate.Datasource)) + pps.RegisterDatasource("virtualmachine", new(virtualmachine.Datasource)) pps.SetVersion(version.PluginVersion) err := pps.Run() if err != nil {