mirror of
https://github.com/bpg/terraform-provider-proxmox.git
synced 2025-08-22 19:38:35 +00:00
feat(lxc): retrieve container IP addresses (#2030)
Signed-off-by: Pavel Boldyrev <627562+bpg@users.noreply.github.com>
This commit is contained in:
parent
a2c40c7c79
commit
20572d95e0
@ -236,22 +236,19 @@ output "ubuntu_container_public_key" {
|
|||||||
- `timeout_clone` - (Optional) Timeout for cloning a container in seconds (defaults to 1800).
|
- `timeout_clone` - (Optional) Timeout for cloning a container in seconds (defaults to 1800).
|
||||||
- `timeout_delete` - (Optional) Timeout for deleting a container in seconds (defaults to 60).
|
- `timeout_delete` - (Optional) Timeout for deleting a container in seconds (defaults to 60).
|
||||||
- `timeout_update` - (Optional) Timeout for updating a container in seconds (defaults to 1800).
|
- `timeout_update` - (Optional) Timeout for updating a container in seconds (defaults to 1800).
|
||||||
- `unprivileged` - (Optional) Whether the container runs as unprivileged on
|
- `unprivileged` - (Optional) Whether the container runs as unprivileged on the host (defaults to `false`).
|
||||||
the host (defaults to `false`).
|
|
||||||
- `vm_id` - (Optional) The container identifier
|
- `vm_id` - (Optional) The container identifier
|
||||||
- `features` - (Optional) The container feature flags. Changing flags (except nesting) is only allowed for `root@pam` authenticated user.
|
- `features` - (Optional) The container feature flags. Changing flags (except nesting) is only allowed for `root@pam` authenticated user.
|
||||||
- `nesting` - (Optional) Whether the container is nested (defaults
|
- `nesting` - (Optional) Whether the container is nested (defaults to `false`)
|
||||||
to `false`)
|
- `fuse` - (Optional) Whether the container supports FUSE mounts (defaults to `false`)
|
||||||
- `fuse` - (Optional) Whether the container supports FUSE mounts (defaults
|
- `keyctl` - (Optional) Whether the container supports `keyctl()` system call (defaults to `false`)
|
||||||
to `false`)
|
|
||||||
- `keyctl` - (Optional) Whether the container supports `keyctl()` system
|
|
||||||
call (defaults to `false`)
|
|
||||||
- `mount` - (Optional) List of allowed mount types (`cifs` or `nfs`)
|
- `mount` - (Optional) List of allowed mount types (`cifs` or `nfs`)
|
||||||
- `hook_script_file_id` - (Optional) The identifier for a file containing a hook script (needs to be executable, e.g. by using the `proxmox_virtual_environment_file.file_mode` attribute).
|
- `hook_script_file_id` - (Optional) The identifier for a file containing a hook script (needs to be executable, e.g. by using the `proxmox_virtual_environment_file.file_mode` attribute).
|
||||||
|
|
||||||
## Attribute Reference
|
## Attribute Reference
|
||||||
|
|
||||||
There are no additional attributes available for this resource.
|
- `ipv4` - The map of IPv4 addresses per network devices. Returns the first address for each network device, if multiple addresses are assigned.
|
||||||
|
- `ipv6` - The map of IPv6 addresses per network device. Returns the first address for each network device, if multiple addresses are assigned.
|
||||||
|
|
||||||
## Import
|
## Import
|
||||||
|
|
||||||
|
@ -128,7 +128,7 @@ resource "proxmox_virtual_environment_file" "ubuntu_container_template" {
|
|||||||
node_name = "first-node"
|
node_name = "first-node"
|
||||||
|
|
||||||
source_file {
|
source_file {
|
||||||
path = "https://download.proxmox.com/images/system/ubuntu-20.04-standard_20.04-1_amd64.tar.gz"
|
path = "http://download.proxmox.com/images/system/ubuntu-20.04-standard_20.04-1_amd64.tar.gz"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
@ -110,6 +110,9 @@ func TestAccResourceContainer(t *testing.T) {
|
|||||||
"device_passthrough.0.mode": "0660",
|
"device_passthrough.0.mode": "0660",
|
||||||
"initialization.0.dns.#": "0",
|
"initialization.0.dns.#": "0",
|
||||||
}),
|
}),
|
||||||
|
ResourceAttributesSet(accTestContainerName, []string{
|
||||||
|
"ipv4.vmbr0",
|
||||||
|
}),
|
||||||
func(*terraform.State) error {
|
func(*terraform.State) error {
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
@ -102,6 +102,78 @@ func (c *Client) GetContainerStatus(ctx context.Context) (*GetStatusResponseData
|
|||||||
return resBody.Data, nil
|
return resBody.Data, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetContainerNetworkInterfaces retrieves details about the container network interfaces.
|
||||||
|
func (c *Client) GetContainerNetworkInterfaces(ctx context.Context) ([]GetNetworkInterfacesData, error) {
|
||||||
|
resBody := &GetNetworkInterfaceResponseBody{}
|
||||||
|
|
||||||
|
err := c.DoRequest(ctx, http.MethodGet, c.ExpandPath("interfaces"), nil, resBody)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error retrieving container network interfaces: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if resBody.Data == nil {
|
||||||
|
return nil, api.ErrNoDataObjectInResponse
|
||||||
|
}
|
||||||
|
|
||||||
|
return resBody.Data, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// WaitForContainerNetworkInterfaces waits for a container to publish its network interfaces.
|
||||||
|
func (c *Client) WaitForContainerNetworkInterfaces(
|
||||||
|
ctx context.Context,
|
||||||
|
timeout time.Duration,
|
||||||
|
) ([]GetNetworkInterfacesData, error) {
|
||||||
|
errNoIPsYet := errors.New("no ips yet")
|
||||||
|
|
||||||
|
ctxWithTimeout, cancel := context.WithTimeout(ctx, timeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
ifaces, err := retry.DoWithData(
|
||||||
|
func() ([]GetNetworkInterfacesData, error) {
|
||||||
|
ifaces, err := c.GetContainerNetworkInterfaces(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, iface := range ifaces {
|
||||||
|
if iface.Name != "lo" && iface.IPAddresses != nil && len(*iface.IPAddresses) > 0 {
|
||||||
|
// we have at least one non-loopback interface with an IP address
|
||||||
|
return ifaces, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, errNoIPsYet
|
||||||
|
},
|
||||||
|
retry.Context(ctxWithTimeout),
|
||||||
|
retry.RetryIf(func(err error) bool {
|
||||||
|
var target *api.HTTPError
|
||||||
|
if errors.As(err, &target) {
|
||||||
|
if target.Code == http.StatusBadRequest {
|
||||||
|
// this is a special case to account for eventual consistency
|
||||||
|
// when creating a task -- the task may not be available via status API
|
||||||
|
// immediately after creation
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors.Is(err, api.ErrNoDataObjectInResponse) || errors.Is(err, errNoIPsYet)
|
||||||
|
}),
|
||||||
|
retry.LastErrorOnly(true),
|
||||||
|
retry.UntilSucceeded(),
|
||||||
|
retry.DelayType(retry.FixedDelay),
|
||||||
|
retry.Delay(time.Second),
|
||||||
|
)
|
||||||
|
if errors.Is(err, context.DeadlineExceeded) {
|
||||||
|
return nil, errors.New("timeout while waiting for container IP addresses")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error while waiting for container IP addresses: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ifaces, nil
|
||||||
|
}
|
||||||
|
|
||||||
// RebootContainer reboots a container.
|
// RebootContainer reboots a container.
|
||||||
func (c *Client) RebootContainer(ctx context.Context, d *RebootRequestBody) error {
|
func (c *Client) RebootContainer(ctx context.Context, d *RebootRequestBody) error {
|
||||||
err := c.DoRequest(ctx, http.MethodPost, c.ExpandPath("status/reboot"), d, nil)
|
err := c.DoRequest(ctx, http.MethodPost, c.ExpandPath("status/reboot"), d, nil)
|
||||||
|
@ -228,6 +228,22 @@ type GetStatusResponseData struct {
|
|||||||
VMID *types.CustomInt `json:"vmid,omitempty"`
|
VMID *types.CustomInt `json:"vmid,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetNetworkInterfaceResponseBody contains the body from a container get network interface response.
|
||||||
|
type GetNetworkInterfaceResponseBody struct {
|
||||||
|
Data []GetNetworkInterfacesData `json:"data,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetNetworkInterfacesData contains the data from a container get network interfaces response.
|
||||||
|
type GetNetworkInterfacesData struct {
|
||||||
|
MACAddress string `json:"hardware-address"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
IPAddresses *[]struct {
|
||||||
|
Address string `json:"ip-address"`
|
||||||
|
Prefix types.CustomInt `json:"prefix"`
|
||||||
|
Type string `json:"ip-address-type"`
|
||||||
|
} `json:"ip-addresses,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
// StartResponseBody contains the body from a container start response.
|
// StartResponseBody contains the body from a container start response.
|
||||||
type StartResponseBody struct {
|
type StartResponseBody struct {
|
||||||
Data *string `json:"data,omitempty"`
|
Data *string `json:"data,omitempty"`
|
||||||
|
@ -16,6 +16,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/hashicorp/terraform-plugin-log/tflog"
|
||||||
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
|
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
|
||||||
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/customdiff"
|
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/customdiff"
|
||||||
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
|
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
|
||||||
@ -180,6 +181,9 @@ const (
|
|||||||
mkTimeoutDelete = "timeout_delete"
|
mkTimeoutDelete = "timeout_delete"
|
||||||
mkUnprivileged = "unprivileged"
|
mkUnprivileged = "unprivileged"
|
||||||
mkVMID = "vm_id"
|
mkVMID = "vm_id"
|
||||||
|
|
||||||
|
mkIPv4 = "ipv4"
|
||||||
|
mkIPv6 = "ipv6"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Container returns a resource that manages a container.
|
// Container returns a resource that manages a container.
|
||||||
@ -565,6 +569,27 @@ func Container() *schema.Resource {
|
|||||||
MaxItems: 1,
|
MaxItems: 1,
|
||||||
MinItems: 0,
|
MinItems: 0,
|
||||||
},
|
},
|
||||||
|
mkIPv4: {
|
||||||
|
Type: schema.TypeMap,
|
||||||
|
Description: "The container's IPv4 addresses per network device",
|
||||||
|
Computed: true,
|
||||||
|
Elem: &schema.Schema{
|
||||||
|
Type: schema.TypeString,
|
||||||
|
},
|
||||||
|
// this does not work with map datatype in SDK :(
|
||||||
|
// Elem: &schema.Schema{
|
||||||
|
// Type: schema.TypeList,
|
||||||
|
// Elem: &schema.Schema{Type: schema.TypeString},
|
||||||
|
// },
|
||||||
|
},
|
||||||
|
mkIPv6: {
|
||||||
|
Type: schema.TypeMap,
|
||||||
|
Description: "The container's IPv6 addresses per network device",
|
||||||
|
Computed: true,
|
||||||
|
Elem: &schema.Schema{
|
||||||
|
Type: schema.TypeString,
|
||||||
|
},
|
||||||
|
},
|
||||||
mkMemory: {
|
mkMemory: {
|
||||||
Type: schema.TypeList,
|
Type: schema.TypeList,
|
||||||
Description: "The memory allocation",
|
Description: "The memory allocation",
|
||||||
@ -2094,6 +2119,7 @@ func containerRead(ctx context.Context, d *schema.ResourceData, m interface{}) d
|
|||||||
return diag.FromErr(e)
|
return diag.FromErr(e)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
template := d.Get(mkTemplate).(bool)
|
||||||
nodeName := d.Get(mkNodeName).(string)
|
nodeName := d.Get(mkNodeName).(string)
|
||||||
|
|
||||||
vmID, e := strconv.Atoi(d.Id())
|
vmID, e := strconv.Atoi(d.Id())
|
||||||
@ -2733,9 +2759,7 @@ func containerRead(ctx context.Context, d *schema.ResourceData, m interface{}) d
|
|||||||
diags = append(diags, diag.FromErr(e)...)
|
diags = append(diags, diag.FromErr(e)...)
|
||||||
}
|
}
|
||||||
|
|
||||||
currentTemplate := d.Get(mkTemplate).(bool)
|
if len(clone) == 0 || template {
|
||||||
|
|
||||||
if len(clone) == 0 || currentTemplate {
|
|
||||||
if containerConfig.Template != nil {
|
if containerConfig.Template != nil {
|
||||||
e = d.Set(
|
e = d.Set(
|
||||||
mkTemplate,
|
mkTemplate,
|
||||||
@ -2754,7 +2778,47 @@ func containerRead(ctx context.Context, d *schema.ResourceData, m interface{}) d
|
|||||||
return diag.FromErr(e)
|
return diag.FromErr(e)
|
||||||
}
|
}
|
||||||
|
|
||||||
e = d.Set(mkStarted, status.Status == "running")
|
started := status.Status == "running"
|
||||||
|
|
||||||
|
if started && len(networkInterfaces) > 0 {
|
||||||
|
ifaces, err := containerAPI.WaitForContainerNetworkInterfaces(ctx, 10*time.Second)
|
||||||
|
if err == nil {
|
||||||
|
ipv4Map := make(map[string]interface{})
|
||||||
|
ipv6Map := make(map[string]interface{})
|
||||||
|
|
||||||
|
for _, iface := range ifaces {
|
||||||
|
if iface.IPAddresses != nil && iface.Name != "lo" {
|
||||||
|
for _, ip := range *iface.IPAddresses {
|
||||||
|
switch ip.Type {
|
||||||
|
case "inet":
|
||||||
|
// store only the first IPv4 address per interface
|
||||||
|
if _, exists := ipv4Map[iface.Name]; !exists {
|
||||||
|
ipv4Map[iface.Name] = ip.Address
|
||||||
|
}
|
||||||
|
case "inet6":
|
||||||
|
// store only the first IPV6 address per interface
|
||||||
|
if _, exists := ipv6Map[iface.Name]; !exists {
|
||||||
|
ipv6Map[iface.Name] = ip.Address
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return diag.FromErr(fmt.Errorf("unexpected IP address type %q for interface %q", ip.Type, iface.Name))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
e = d.Set(mkIPv4, ipv4Map)
|
||||||
|
diags = append(diags, diag.FromErr(e)...)
|
||||||
|
e = d.Set(mkIPv6, ipv6Map)
|
||||||
|
diags = append(diags, diag.FromErr(e)...)
|
||||||
|
} else {
|
||||||
|
tflog.Warn(ctx, "error waiting for container network interfaces", map[string]interface{}{
|
||||||
|
"error": err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
e = d.Set(mkStarted, started)
|
||||||
diags = append(diags, diag.FromErr(e)...)
|
diags = append(diags, diag.FromErr(e)...)
|
||||||
|
|
||||||
return diags
|
return diags
|
||||||
|
Loading…
Reference in New Issue
Block a user