Public/New-PVEContainer.ps1

function New-PVEContainer {
  <#
  .SYNOPSIS
  Creates an LXC container from an OS template.
  .DESCRIPTION
  Creates the container with POST /nodes/{node}/lxc, waits for the create task, and writes the new
  container with its live status. Options not given are left to PVE's own defaults (for example 512 MiB
  of memory and 512 MiB of swap). Containers are created unprivileged unless -Privileged is given; this
  module sends unprivileged=1 explicitly, because the API schema lists a default of 0 while its own
  description says creation defaults to 1.

  What the parameters add to the configuration:
  - -Storage and -DiskGB: the root file system (rootfs), a new volume of -DiskGB GiB on -Storage.
  - -NetworkBridge: one network device, eth0, on that bridge, with -IPAddress (DHCP by default),
    -Gateway and -IPv6Address.
  - -Nesting: the nesting feature. The API documentation says systemd needs it to isolate services, and
    that it exposes procfs and sysfs contents of the host to the guest.
  - -Parameter: any other create options, by their API names. Applied last, so they override the above.
  .PARAMETER Name
  Host name of the container. PVE requires a valid DNS name.
  .PARAMETER OSTemplate
  OS template volume, for example local:vztmpl/debian-13-standard_13.1-2_amd64.tar.zst. Get-PVEStorage
  -Content vztmpl lists the storages that hold templates.
  .PARAMETER VMId
  ID for the container. Defaults to the next free ID. PVE checks a given ID is free before the container
  is created.
  .PARAMETER Node
  Node to create the container on. Can be omitted when the server has a single node.
  .PARAMETER Storage
  Storage for the root file system.
  .PARAMETER DiskGB
  Size of the root file system in GiB. Defaults to 8.
  .PARAMETER Cores
  CPU cores the container may use. PVE's default is all of the host's cores.
  .PARAMETER MemoryGB
  Memory in GiB, sent to PVE in whole MiB.
  .PARAMETER SwapGB
  Swap in GiB, sent to PVE in whole MiB. 0 disables swap.
  .PARAMETER Password
  Root password inside the container. Accepts a string or a SecureString.
  .PARAMETER SshPublicKey
  Public SSH keys (OpenSSH format) for root inside the container. One key per value.
  .PARAMETER Privileged
  Create a privileged container instead of an unprivileged one.
  .PARAMETER NetworkBridge
  Bridge for the container's eth0 network device, for example vmbr0.
  .PARAMETER IPAddress
  IPv4 address for eth0 in CIDR notation (192.0.2.10/24), dhcp, or manual. Defaults to dhcp. Needs
  -NetworkBridge.
  .PARAMETER Gateway
  IPv4 gateway for eth0. Needs -NetworkBridge.
  .PARAMETER IPv6Address
  IPv6 address for eth0 in CIDR notation, auto, dhcp, or manual. Needs -NetworkBridge.
  .PARAMETER Nesting
  Enable the nesting feature (see the description).
  .PARAMETER Pool
  Resource pool to add the container to.
  .PARAMETER Tags
  Tags for the container.
  .PARAMETER Description
  Description (notes) shown in the container's summary.
  .PARAMETER StartOnBoot
  Start the container when the node boots.
  .PARAMETER PowerOn
  Start the container once it has been created.
  .PARAMETER Parameter
  Further create options by their API names, for example @{ timezone = 'host'; nameserver = '192.0.2.53' }.
  .PARAMETER RunAsync
  Return the create task at once instead of waiting.
  .PARAMETER Server
  Connection to use. Defaults to the default connection.
  .EXAMPLE
  New-PVEContainer -Name dns01 -OSTemplate local:vztmpl/debian-13-standard_13.1-2_amd64.tar.zst -Storage local-lvm -NetworkBridge vmbr0 -SshPublicKey (Get-Content ~/.ssh/id_ed25519.pub)
  .EXAMPLE
  New-PVEContainer proxy01 -OSTemplate $template -Storage local-lvm -DiskGB 16 -MemoryGB 1 -NetworkBridge vmbr0 -IPAddress 192.0.2.20/24 -Gateway 192.0.2.1 -Nesting -PowerOn
  #>

  [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingPlainTextForPassword', 'Password', Justification = 'Deliberately accepts a string or a SecureString, as Connect-PVE does.')]
  [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'Medium')]
  [OutputType('PVEPS.Container', 'PVEPS.Task')]
  param(
    [Parameter(Mandatory, Position = 0)]
    [ValidateNotNullOrEmpty()]
    [string]$Name,

    [Parameter(Mandatory)]
    [ValidateNotNullOrEmpty()]
    [string]$OSTemplate,

    [ValidateRange(100, 999999999)]
    [int]$VMId,

    [ValidateNotNullOrEmpty()]
    [string]$Node,

    [Parameter(Mandatory)]
    [ValidateNotNullOrEmpty()]
    [string]$Storage,

    [ValidateRange(1, [int]::MaxValue)]
    [int]$DiskGB = 8,

    [ValidateRange(1, 8192)]
    [int]$Cores,

    [ValidateRange(0.015625, [double]::MaxValue)]
    [double]$MemoryGB,

    [ValidateRange(0, [double]::MaxValue)]
    [double]$SwapGB,

    [ValidateScript({ $_ -is [string] -or $_ -is [securestring] }, ErrorMessage = 'Password must be a string or a SecureString.')]
    [object]$Password,

    [ValidateNotNullOrEmpty()]
    [string[]]$SshPublicKey,

    [switch]$Privileged,

    [ValidateNotNullOrEmpty()]
    [string]$NetworkBridge,

    [ValidateNotNullOrEmpty()]
    [string]$IPAddress = 'dhcp',

    [ValidateNotNullOrEmpty()]
    [string]$Gateway,

    [ValidateNotNullOrEmpty()]
    [string]$IPv6Address,

    [switch]$Nesting,

    [ValidateNotNullOrEmpty()]
    [string]$Pool,

    [ValidateNotNullOrEmpty()]
    [string[]]$Tags,

    [string]$Description,

    [switch]$StartOnBoot,

    [switch]$PowerOn,

    [hashtable]$Parameter,

    [switch]$RunAsync,

    [Alias('Connection')]
    [object]$Server
  )

  foreach ($option in 'IPAddress', 'Gateway', 'IPv6Address') {
    if ($PSBoundParameters.ContainsKey($option) -and -not $NetworkBridge) {
      $PSCmdlet.ThrowTerminatingError((New-PVEArgumentError -Message "-$option needs -NetworkBridge." -Target $PSBoundParameters[$option]))
    }
  }
  foreach ($key in 'vmid', 'node') {
    if ($Parameter -and $Parameter.ContainsKey($key)) {
      $PSCmdlet.ThrowTerminatingError((New-PVEArgumentError -Message "-Parameter cannot set '$key'; use -VMId or -Node." -Target $Parameter))
    }
  }

  try {
    $connection = Resolve-PVESingleConnection -Server $Server -Reason 'a container is created on exactly one'
    $nodeName = Resolve-PVENodeName -Connection $connection -Node $Node
    $id = Get-PVENewVMId -Connection $connection -VMId $VMId
  }
  catch {
    $PSCmdlet.WriteError($_)
    return
  }

  $body = @{
    vmid       = $id
    hostname   = $Name
    ostemplate = $OSTemplate
    rootfs     = '{0}:{1}' -f $Storage, $DiskGB
  }
  if ($PSBoundParameters.ContainsKey('Cores')) { $body.cores = $Cores }
  if ($PSBoundParameters.ContainsKey('MemoryGB')) { $body.memory = ConvertTo-PVEMiB $MemoryGB }
  if ($PSBoundParameters.ContainsKey('SwapGB')) { $body.swap = ConvertTo-PVEMiB $SwapGB }
  if ($null -ne $Password) {
    $body.password = if ($Password -is [securestring]) { ConvertFrom-PVESecureString -SecureString $Password } else { $Password }
  }
  if ($SshPublicKey) { $body['ssh-public-keys'] = $SshPublicKey -join "`n" }
  $body.unprivileged = if ($Privileged) { 0 } else { 1 }
  if ($NetworkBridge) {
    $net = 'name=eth0,bridge={0},ip={1}' -f $NetworkBridge, $IPAddress
    if ($Gateway) { $net += ',gw={0}' -f $Gateway }
    if ($IPv6Address) { $net += ',ip6={0}' -f $IPv6Address }
    $body.net0 = $net
  }
  if ($Nesting) { $body.features = 'nesting=1' }
  if ($Pool) { $body.pool = $Pool }
  if ($Tags) { $body.tags = $Tags -join ';' }
  if ($PSBoundParameters.ContainsKey('Description')) { $body.description = $Description }
  if ($StartOnBoot) { $body.onboot = 1 }
  if ($PowerOn) { $body.start = 1 }
  if ($Parameter) {
    foreach ($key in $Parameter.Keys) {
      $body[$key] = $Parameter[$key]
    }
  }

  $label = 'Container {0} ({1}) on {2}' -f $id, $Name, $nodeName
  if (-not $PSCmdlet.ShouldProcess($label, 'Create')) {
    return
  }

  $operations = [System.Collections.Generic.List[psobject]]::new()
  try {
    $path = Format-PVEPath '/nodes/{node}/lxc' @{ node = $nodeName }
    $upid = Invoke-PVERequest -Connection $connection -Method POST -Path $path -Parameters $body
  }
  catch {
    $PSCmdlet.WriteError($_)
    return
  }
  Write-Verbose "Create $label`: task $upid"
  $operations.Add([pscustomobject]@{ Upid = [string]$upid; Connection = $connection; Guest = $null; Type = 'lxc'; Node = $nodeName; VMId = $id; Pool = $Pool })
  Complete-PVEOperation -Cmdlet $PSCmdlet -Operation $operations -RunAsync:$RunAsync
}