mirror of
https://github.com/bpg/terraform-provider-proxmox.git
synced 2025-07-12 00:35:03 +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_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).
|
||||
- `unprivileged` - (Optional) Whether the container runs as unprivileged on
|
||||
the host (defaults to `false`).
|
||||
- `unprivileged` - (Optional) Whether the container runs as unprivileged on the host (defaults to `false`).
|
||||
- `vm_id` - (Optional) The container identifier
|
||||
- `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
|
||||
to `false`)
|
||||
- `fuse` - (Optional) Whether the container supports FUSE mounts (defaults
|
||||
to `false`)
|
||||
- `keyctl` - (Optional) Whether the container supports `keyctl()` system
|
||||
call (defaults to `false`)
|
||||
- `nesting` - (Optional) Whether the container is nested (defaults to `false`)
|
||||
- `fuse` - (Optional) Whether the container supports FUSE mounts (defaults to `false`)
|
||||
- `keyctl` - (Optional) Whether the container supports `keyctl()` system call (defaults to `false`)
|
||||
- `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).
|
||||
|
||||
## 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
|
||||
|
||||
|
@ -128,7 +128,7 @@ resource "proxmox_virtual_environment_file" "ubuntu_container_template" {
|
||||
node_name = "first-node"
|
||||
|
||||
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",
|
||||
"initialization.0.dns.#": "0",
|
||||
}),
|
||||
ResourceAttributesSet(accTestContainerName, []string{
|
||||
"ipv4.vmbr0",
|
||||
}),
|
||||
func(*terraform.State) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
@ -102,6 +102,78 @@ func (c *Client) GetContainerStatus(ctx context.Context) (*GetStatusResponseData
|
||||
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.
|
||||
func (c *Client) RebootContainer(ctx context.Context, d *RebootRequestBody) error {
|
||||
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"`
|
||||
}
|
||||
|
||||
// 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.
|
||||
type StartResponseBody struct {
|
||||
Data *string `json:"data,omitempty"`
|
||||
|
@ -16,6 +16,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/terraform-plugin-log/tflog"
|
||||
"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/schema"
|
||||
@ -180,6 +181,9 @@ const (
|
||||
mkTimeoutDelete = "timeout_delete"
|
||||
mkUnprivileged = "unprivileged"
|
||||
mkVMID = "vm_id"
|
||||
|
||||
mkIPv4 = "ipv4"
|
||||
mkIPv6 = "ipv6"
|
||||
)
|
||||
|
||||
// Container returns a resource that manages a container.
|
||||
@ -565,6 +569,27 @@ func Container() *schema.Resource {
|
||||
MaxItems: 1,
|
||||
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: {
|
||||
Type: schema.TypeList,
|
||||
Description: "The memory allocation",
|
||||
@ -2094,6 +2119,7 @@ func containerRead(ctx context.Context, d *schema.ResourceData, m interface{}) d
|
||||
return diag.FromErr(e)
|
||||
}
|
||||
|
||||
template := d.Get(mkTemplate).(bool)
|
||||
nodeName := d.Get(mkNodeName).(string)
|
||||
|
||||
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)...)
|
||||
}
|
||||
|
||||
currentTemplate := d.Get(mkTemplate).(bool)
|
||||
|
||||
if len(clone) == 0 || currentTemplate {
|
||||
if len(clone) == 0 || template {
|
||||
if containerConfig.Template != nil {
|
||||
e = d.Set(
|
||||
mkTemplate,
|
||||
@ -2754,7 +2778,47 @@ func containerRead(ctx context.Context, d *schema.ResourceData, m interface{}) d
|
||||
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)...)
|
||||
|
||||
return diags
|
||||
|
Loading…
Reference in New Issue
Block a user