Build a Proxmox Self-Service Portal
Turn a PowerShell VM provisioner into a local Proxmox self-service portal.
Introduction
30 Second Summary
Building a lab machine from a long command leaves plenty of room for a mistyped ID or size. A clear request screen helps operators repeat approved choices.
In this project, you will turn the existing PowerShell provisioner into a local self-service portal using PowerShell Universal. The portal safely previews each request before sending an approved one to Proxmox VE.
What You'll Build
You'll submit an approved lab VM request through a browser portal that reports the machine's running state on the same page.
By the end of this project, you'll have:
- A guided request form that converts the existing typed request into required browser fields with approved size choices.
- A safe preview that shows the exact request while leaving the Proxmox VE inventory unchanged.
- A secret-backed provisioning path that keeps the API token out of source code while showing progress through to a running result.
- Secret Mission: Add a second browser form that previews the guarded VM cleanup command while leaving the selected machine unchanged.
Are there any prerequisites?
You need the completed linked project with PveApi.psm1, Request-LabVm.ps1, Remove-LabVm.ps1, and catalog.json.
A working Linux Cloud-Init template plus the API token permissions from that project are also required before the final provisioning run.
Step 1 covers the local portal runtime plus its free Developer License.
Before We Start
A self-service portal is only safe when every field maps to an approved lab request. A handwritten command can hide an incorrect VM ID or an unapproved size until infrastructure changes.
This checkpoint defines the operator for one governed VM request. The result is a typed request contract for every field in the browser form.
You will confirm the existing provisioner files. You will also verify the working Proxmox VE template.
In this step, get ready to:
- Define an approved VM request for one operator.
- Confirm the prerequisite provisioner assets.
- Verify the template readiness for the final provisioning run.
Define the approved lab request
The portal form reflects the parameter names used by Request-LabVm.ps1. Each value should describe a specific lab outcome before automation receives it.
- Identify the person or role operating the portal: operator name or role.
- Record the Proxmox host for PveHost: proxmox host name or IP address.
- Record the Proxmox node for Node: proxmox node name.
- Record the working template VM ID for TemplateVmid: working template VM ID.
- Reserve an unused destination VM ID for NewVmid: unused destination VM ID.
- Choose the approved VM name for VmName: approved VM name.
- Select small or medium for Size: small or medium.
- Record why this browser form is safer for the operator than a handwritten command: why the form is safer.
You have locked in one concrete request with governed values. The future form now has a clear outcome to validate.
Confirm the provisioner assets
The portal wrapper reuses the scripts from the original project. It also references an API token ID while the secret value stays out of this checklist.
- Open the original proxmox-mini-platform folder in File Explorer.
- Confirm that PveApi.psm1 appears in the folder.
- Confirm that Request-LabVm.ps1 appears in the folder.
- Confirm that Remove-LabVm.ps1 appears in the folder.
- Confirm that catalog.json appears in the folder.
- Record only the full Proxmox API token ID: full Proxmox API token ID.
- Keep the one-time API token secret in your existing secure storage.
- Revisit the linked Build a Proxmox VM Provisioner project if any required file is missing.
The folder should show all four prerequisite files together. This confirms that the portal can reuse the existing provisioning workflow.
Verify the template and destination ID
A working Linux Cloud-Init template provides the source for the cloned VM. The destination ID needs an unused place in the Proxmox VE inventory.
Before you check the inventory, make a quick prediction about which ID should already exist.
- Locate the entry matching your TemplateVmid in the Proxmox VE inventory.
- Confirm that the entry is the working Linux Cloud-Init template required by the provisioner.
- Search the Proxmox VE inventory for your reserved NewVmid.
- Confirm that no existing VM uses the reserved destination ID.
- Complete the separate template prerequisite project before provisioning if the working template is unavailable.
You should see TemplateVmid attached to the existing template. You should find no VM using NewVmid.
That is the safety gate complete. Your source exists while your destination remains available.
Your request now has a named operator plus governed values. Next, you will install PowerShell Universal as the local browser host.
Install the Local Portal Runtime
Your approved Proxmox VE lab request is ready. The existing provisioner still lives in a terminal.
Your prerequisite checkpoint already confirms the source files plus the Cloud-Init template needed later. This runtime installation leaves the approved request unchanged.
PowerShell Universal adds the local browser host that turns scripts into forms. PowerShell 7 installs the pinned module plus the Windows service that runs that host.
Why PowerShell Universal?
PowerShell Universal can generate browser forms from typed PowerShell parameters. It also surfaces job progress without a custom web server.
A Windows Forms interface would require manual layout plus background job handling. PowerShell Universal keeps this project focused on the self-service platform pattern.
In this step, get ready to:
- Install PowerShell Universal 2026.2.5.
- Start the local PowerShell Universal Windows service.
- Activate the Developer License.
Check the pinned module
The PowerShell Universal module provides the server installer used in this step. A version check tells you whether the pinned release needs to be installed.
- Press the Windows key to open Windows Search.
- Type PowerShell 7 into the search field.
- Select the administrator launch option.
- Approve the Windows permission request if it appears.
- Check the installed PowerShell Universal module by running this command:
Get-InstalledModule -Name Devolutions.PowerShellUniversal
What does this check show?
- The Get-InstalledModule command checks modules installed through PowerShellGet.
- The -Name parameter limits the result to the PowerShell Universal module.
Your output now falls into one of three states. Choose the tab that matches what you see.
✔️ I see version 2026.2.5
Good, version 2026.2.5 is already installed. The server installer is ready in this elevated session.
ⓧ I see an older version
An older module can expose commands that differ from this project. Install the pinned release before starting the local server.
- Approve the module download if PowerShell requests confirmation.
- Install the required release by running this command:
Install-Module -Name Devolutions.PowerShellUniversal -RequiredVersion 2026.2.5
What does this installation do?
The command downloads the Devolutions.PowerShellUniversal module from the PowerShell Gallery. The required version pins the installation to 2026.2.5.
Module installation failed?
Confirm that this is an elevated PowerShell 7 window. Check that Windows has an internet connection before trying the installation again.
Ask for help with the failed installation. You can also compare your setup with the official PowerShell Universal getting-started guide.
ⓧ No module is listed
This is the expected path when PowerShell Universal has not been installed. The pinned module supplies the server installation command.
- Approve the module download if PowerShell requests confirmation.
- Install the pinned module by running this command:
Install-Module -Name Devolutions.PowerShellUniversal -RequiredVersion 2026.2.5
What does this installation do?
The command downloads the Devolutions.PowerShellUniversal module from the PowerShell Gallery. The required version installs release 2026.2.5 for this project.
Module installation failed?
Confirm that this is an elevated PowerShell 7 window. Check that Windows has an internet connection before trying the installation again.
Ask for help with the failed installation. You can also compare your setup with the official PowerShell Universal getting-started guide.
- Confirm the pinned module is available by running the version check again:
Get-InstalledModule -Name Devolutions.PowerShellUniversal
What should the result confirm?
The result identifies the Devolutions.PowerShellUniversal module. Its displayed version should be 2026.2.5.
Still missing the pinned version?
Keep the same elevated PowerShell 7 window open. Run the pinned installation command from the tab that matched your original result.
Help me understand why the pinned module is missing after installation.
Install the local Windows service
The module is now available in your elevated session. Its server installer creates the local PowerShell Universal Windows service that hosts the browser console.
- Install the server plus start its Windows service by running this command:
Install-PSUServer
What does this command do?
The Install-PSUServer command installs the PowerShell Universal server on Windows. The installation creates plus starts its local Windows service.
- Wait for the installer to finish.
PowerShell returns control after the local server installation completes.
Server installation did not finish?
Confirm that the PowerShell window still has administrator privileges. Retry the command after closing any earlier installer that is waiting for input.
Help me troubleshoot the local PowerShell Universal server installation.
Activate the Developer License
The free Developer License enables PowerShell Universal features for local non-production development. Activation uses a Devolutions account in the local onboarding flow.
Account sign-in is the only credential step here. Keep your password inside the Devolutions sign-in flow.
- Enter http://localhost:5000 in your browser address bar.
- Create a Devolutions account from the onboarding page if you do not have one.
- Sign in with your Devolutions account.
- Activate the Developer License for this local environment.
Before you reload the page, what do you expect the local service plus activated license to display?
- Reload http://localhost:5000 in your browser.
You'll see the signed-in PowerShell Universal admin console under the Developer License. This confirms the local Windows service is accepting browser requests.
What did the browser check prove?
The admin console confirms that the browser can reach the local PowerShell Universal service. The activated license keeps this environment ready for local non-production portal development.
- Press the Windows key to open Windows Search.
- Type File Explorer into the search field.
- Press Enter.
- Paste %ProgramData%\UniversalAutomation\Repository into the File Explorer address bar.
- Press Enter.
You'll see File Explorer open the PowerShell Universal repository directory. This is where the portal files are placed in the next steps.
Runtime checkpoint
- %ProgramData%\UniversalAutomation\Repository: PowerShell Universal repository directory is available.
- Developer License: Activated for local, non-production PowerShell Universal use.
- PowerShell Universal Windows service: Installed and running locally after Install-PSUServer.
Your local portal host is running under the Developer License. Next up, you will turn the governed VM request into a preview-only browser form.
Build a Preview-Only Request Portal
Your local PowerShell Universal runtime is ready. You can now give the existing command-line workflow a browser interface.
A form can capture a governed request without becoming a provisioning platform. This first version lets you test the request boundary safely before you inspect the missing connection to Proxmox VE.
In this step, get ready to:
- Create a typed Automation script that represents the approved VM request.
- Build a browser app that generates its form from the script parameters.
- Submit a request that displays a safe preview without changing the VM inventory.
Create the preview Automation script
A typed parameter contract controls which values an operator can submit. PowerShell Universal reads this contract to generate required fields plus governed choices.
- Return to the PowerShell Universal admin console from the previous step.
- Select Automation in the navigation.
- Select Scripts.
- Select Create New Script.
- Name the script Request-LabVmPortal.ps1.
- Define the request contract by replacing the starter content with this code:
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[string]$PveHost,
[Parameter(Mandatory)]
[string]$Node,
[Parameter(Mandatory)]
[ValidateRange(100, 999999999)]
[int]$TemplateVmid,
[Parameter(Mandatory)]
[ValidateRange(100, 999999999)]
[int]$NewVmid,
[Parameter(Mandatory)]
[string]$VmName,
[Parameter(Mandatory)]
[ValidateSet('small', 'medium')]
[string]$Size,
[switch]$FullClone,
[switch]$SkipCertificateCheck
)
What does this code do?
- Mandatory parameters become required inputs in the generated form.
- ValidateRange limits each VM ID to the accepted numeric range.
- ValidateSet turns the approved sizes into a drop-down containing small or medium.
- The two switch parameters become optional choices for clone behavior or certificate handling.
- Save Request-LabVmPortal.ps1 with the editor's save control.
- Select Request-LabVmPortal.ps1 from the Automation script list.
- Use the script page's run control to display its generated parameter form.
You should see required inputs for the host details plus the VM request values. The size input should offer two governed choices.
Missing fields from the parameter form?
Check that every mandatory attribute sits directly above its matching parameter. Confirm that the closing parenthesis remains below SkipCertificateCheck.
Ask for help checking the parameter contract.
The parameter block governs input. A request object now needs to preserve those submitted values so the operator can inspect the exact request.
- Return to the code editor for Request-LabVmPortal.ps1.
- Add the preview logic directly below the parameter block using this code:
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
$request = [pscustomobject]@{
Action = 'Preview only'
PveHost = $PveHost
Node = $Node
TemplateVmid = $TemplateVmid
NewVmid = $NewVmid
VmName = $VmName
Size = $Size
FullClone = [bool]$FullClone
SkipCertificateCheck = [bool]$SkipCertificateCheck
}
$request
How does the preview stay safe?
- Strict mode surfaces references to values that do not exist.
- The error preference stops the script when PowerShell encounters an error.
- The request object records the submitted values in one structured result.
- The fixed Preview only action describes the script's current behavior.
- The final request line sends the preview object to the output stream.
- Save Request-LabVmPortal.ps1.
- Confirm that $request is the final line in the script editor.
Seeing a script syntax problem?
Check that the request object starts with @{ and closes with }. Confirm that each property remains on its own line.
Ask for help finding the syntax issue.
- Double-check the completed Automation script by choosing the matching tab below.
✔️ Awesome, I've got everything!
Your typed preview wrapper is complete. Keep the saved script open for the app you build next.
ⓧ I'd like to double check the full code
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[string]$PveHost,
[Parameter(Mandatory)]
[string]$Node,
[Parameter(Mandatory)]
[ValidateRange(100, 999999999)]
[int]$TemplateVmid,
[Parameter(Mandatory)]
[ValidateRange(100, 999999999)]
[int]$NewVmid,
[Parameter(Mandatory)]
[string]$VmName,
[Parameter(Mandatory)]
[ValidateSet('small', 'medium')]
[string]$Size,
[switch]$FullClone,
[switch]$SkipCertificateCheck
)
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
$request = [pscustomobject]@{
Action = 'Preview only'
PveHost = $PveHost
Node = $Node
TemplateVmid = $TemplateVmid
NewVmid = $NewVmid
VmName = $VmName
Size = $Size
FullClone = [bool]$FullClone
SkipCertificateCheck = [bool]$SkipCertificateCheck
}
$request
What should match?
Your full file should contain the typed parameter contract followed by the preview object. It should finish by returning $request.
Create the browser app
A Script Form reads an Automation script's parameter block to generate browser controls. The portal inherits required fields plus the governed size choices from the contract you just saved.
- Return to the PowerShell Universal admin console.
- Select User Interfaces in the navigation.
- Select Apps.
- Select Create New App.
- Name the app Proxmox Self-Service Portal.
- Set the app URL to /proxmox.
- Replace the starter app code with this code:
New-UDApp -Title 'Proxmox Self-Service Portal' -Content {
New-UDForm `
-Id 'vmRequest' `
-Script 'Request-LabVmPortal.ps1' `
-OutputType 'Text' `
-ShowBackButton
}
How does the app connect to the script?
- New-UDApp defines the page title plus the content displayed by the portal.
- New-UDForm connects the browser form to Request-LabVmPortal.ps1.
- The vmRequest ID gives the form a stable identifier inside the app.
- The Text output type displays the returned request as text.
- ShowBackButton gives the operator a way to return after viewing a submission.
- Save ProxmoxPortal.ps1.
- Open Details for Proxmox Self-Service Portal.
- Select Restart.
- Select View.
You should see a browser form with required fields for the host details plus the VM request. The size control should offer small or medium.
Portal form not loading?
Confirm that the app references Request-LabVmPortal.ps1 with the same spelling as the Automation script. Restart the app again after saving the latest code.
Ask for help connecting the app to the script.
- Double-check the completed app file by choosing the matching tab below.
✔️ Awesome, I've got everything!
Your app now points to the preview script. Keep it running for the first browser submission.
ⓧ I'd like to double check the full code
New-UDApp -Title 'Proxmox Self-Service Portal' -Content {
New-UDForm `
-Id 'vmRequest' `
-Script 'Request-LabVmPortal.ps1' `
-OutputType 'Text' `
-ShowBackButton
}
What should match?
The complete app contains one form named vmRequest. That form runs Request-LabVmPortal.ps1 and displays its result as text.
Test the preview boundary
The browser now has enough structure to capture an approved request. This test reveals whether a validated form can create infrastructure by itself.
Before you submit the request, do you think the typed browser form is enough to create a new VM?
- Enter the recorded Proxmox host in PveHost.
- Enter the recorded Proxmox node in Node.
- Enter the working template VM ID in TemplateVmid.
- Enter the reserved destination VM ID in NewVmid.
- Enter the approved VM name in VmName.
- Select small in Size.
What do the switches control?
FullClone records whether the approved request asks for a full clone. SkipCertificateCheck records the lab-only certificate choice.
This preview stores both choices as Boolean values. Neither switch calls the Proxmox API in this version.
- Leave FullClone off unless the approved request requires it.
- Leave SkipCertificateCheck off unless the known lab host uses a self-signed certificate.
- Use the form's submission button to send the request.
You should see Action with the value Preview only. The output should also echo the host details plus every VM request value you submitted.
- Return to the Proxmox VE inventory you use for the lab.
- Refresh the inventory.
- Confirm that no new VM uses the submitted NewVmid.
The form captured and validated the request while the VM inventory remained unchanged. That shortfall is intentional because the portal has no execution path yet.
Preview missing from the page?
Confirm that every required field contains a value from the approved request. Check that Size contains one of its available choices.
Ask for help tracing the form submission.
You have a working browser portal that validates and previews an approved VM request without changing infrastructure. Next, you will give the portal a secret-backed Proxmox identity while keeping the token out of source code.
Add a Secret-Backed Proxmox Identity
Your portal now captures a governed VM request in the browser. It keeps Proxmox VE unchanged.
The browser job cannot answer the provisioner's terminal prompt. PowerShell Universal will supply the API token through a stored credential so the secret never enters either script.
In this step, get ready to:
- Place the existing provisioner files beside the portal wrapper.
- Store the Proxmox API token in the database vault.
- Extend the browser form with credential and provisioning controls.
Place the provisioner beside the wrapper
The existing provisioner expects its module and catalog beside it. The PowerShell Universal repository gives these files one shared location.
- Press the Windows key to open Windows search.
- Type File Explorer and press Enter.
- Use the address bar to navigate to the existing proxmox-mini-platform folder from the prerequisite project.
- Select these three files: PveApi.psm1, Request-LabVm.ps1, and catalog.json.
- Copy the selected files.
- Use the address bar to navigate to %ProgramData%\UniversalAutomation\Repository.
- Paste the three copied files into the repository.
- Confirm that PveApi.psm1, Request-LabVm.ps1, and catalog.json appear beside Request-LabVmPortal.ps1.
That file handoff is complete. The wrapper now sits beside its unchanged provisioner dependencies.
Store the API token as a secret credential
A PSCredential stores a username and password as one credential. The full token ID becomes the username.
The one-time token secret becomes the password. The database vault keeps that value outside both repositories.
- Switch back to the PowerShell Universal admin console from earlier.
- Select Platform in the navigation.
- Select Variables.
- Select Create Secret Variable.
- Select the Secret tab.
- Enter ProxmoxApiToken as the variable name.
- Set the secret type to PSCredential.
- Choose the database vault.
This is the sensitive part, so keep the token secret in the password field and out of every source file.
- Enter the full API token ID you recorded earlier as the username. A token ID can look like root@pam!nextwork.
- Enter the one-time API token secret as the password.
- Submit the secret-variable form.
- Return to the variables list to confirm that ProxmoxApiToken is listed without its password value.
Why Use a PSCredential?
The username carries the complete token ID. The protected password carries the token secret.
The portal receives a reference to ProxmoxApiToken. Your source files stay free of the live credential.
Extend the preview contract
Script Forms read an Automation script's typed parameters. Adding ApiToken creates a credential selector.
Adding Provision creates an explicit switch. Its unchecked state preserves the safe preview path.
- Return to Automation in the admin console.
- Select Scripts.
- Open Request-LabVmPortal.ps1.
- Find the parameter section that starts above ValidateSet('small', 'medium').
- Replace that section through $SkipCertificateCheck with the code below:
[Parameter(Mandatory)]
[ValidateSet('small', 'medium')]
[string]$Size,
[PSCredential]$ApiToken,
[switch]$FullClone,
[switch]$SkipCertificateCheck,
[switch]$Provision
What Do These Parameters Add?
- ApiToken accepts a stored credential from the portal's credential selector.
- Provision records whether the operator requested preview or execution.
- Both parameters stay optional so the default form submission remains safe.
- Save Request-LabVmPortal.ps1.
- Return to the /proxmox page from earlier.
- Refresh the browser page.
You should see an ApiToken credential selector and a Provision switch in the form.
Missing the New Controls?
Confirm that you saved Request-LabVmPortal.ps1 in the PowerShell Universal repository. Check that both new parameters remain inside the script's param block.
Refresh the portal after correcting the file.
Help me troubleshoot missing Script Form controls.
The new switch also changes the request's stated intent. The preview object needs to report the selected path accurately.
- Return to Request-LabVmPortal.ps1 in the script editor.
- Replace the complete $request object with the code below:
$request = [pscustomobject]@{
Action = if ($Provision) { 'Provision' } else { 'Preview only' }
PveHost = $PveHost
Node = $Node
TemplateVmid = $TemplateVmid
NewVmid = $NewVmid
VmName = $VmName
Size = $Size
FullClone = [bool]$FullClone
SkipCertificateCheck = [bool]$SkipCertificateCheck
}
What Does This Object Do?
Action now reflects the Provision switch. Every other property continues to echo the governed request.
The object contains no credential property. The token secret cannot enter the preview output through this request object.
- Save Request-LabVmPortal.ps1.
- Return to the /proxmox page.
- Refresh the browser page.
- Leave Provision switched off.
- Submit the same safe request values you used in the previous step.
The result should still show Action as Preview only.
Seeing the Wrong Action?
Confirm that Provision is switched off before submitting. Check that the conditional Action line matches the code above.
Help me fix the request action.
A selected Provision switch must stop at a clear boundary until execution exists. The wrapper will add that execution in the next step.
- Find the final $request line at the bottom of Request-LabVmPortal.ps1.
- Replace that line with the guarded preview branch below:
if (-not $Provision) {
$request
return
}
throw 'Provisioning execution will be added in the next step.'
How Does the Guard Work?
When Provision is off, the script returns the preview object and exits. This remains the normal path.
When Provision is on, the temporary error prevents a false provisioning result. The next step replaces this boundary with the proven execution workflow.
- Save Request-LabVmPortal.ps1.
✔️ Awesome, I've got everything!
Your incremental edits now match the secret-backed preview contract.
ⓧ I'd like to double check the full code
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[string]$PveHost,
[Parameter(Mandatory)]
[string]$Node,
[Parameter(Mandatory)]
[ValidateRange(100, 999999999)]
[int]$TemplateVmid,
[Parameter(Mandatory)]
[ValidateRange(100, 999999999)]
[int]$NewVmid,
[Parameter(Mandatory)]
[string]$VmName,
[Parameter(Mandatory)]
[ValidateSet('small', 'medium')]
[string]$Size,
[PSCredential]$ApiToken,
[switch]$FullClone,
[switch]$SkipCertificateCheck,
[switch]$Provision
)
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
$request = [pscustomobject]@{
Action = if ($Provision) { 'Provision' } else { 'Preview only' }
PveHost = $PveHost
Node = $Node
TemplateVmid = $TemplateVmid
NewVmid = $NewVmid
VmName = $VmName
Size = $Size
FullClone = [bool]$FullClone
SkipCertificateCheck = [bool]$SkipCertificateCheck
}
if (-not $Provision) {
$request
return
}
throw 'Provisioning execution will be added in the next step.'
How to Use This Reference
This file keeps preview as the default path. It exposes the credential and provisioning controls while reserving execution for the next step.
ProxmoxPortal.ps1 remains unchanged. Its Script Form reads the updated parameter contract automatically.
Before you submit, do you expect the browser to reveal any part of the stored token secret?
- Return to http://localhost:5000/proxmox.
- Refresh the browser page.
- Enter the same safe preview request values from the previous step.
- Select ProxmoxApiToken in the ApiToken credential selector.
- Leave Provision switched off.
- Submit the form.
You should see Action with the value Preview only. The token secret should be absent from the page output.
You have crossed the sensitive part cleanly. The portal can now select a Proxmox identity without exposing its secret.
Preview Submission Failing?
Confirm that ProxmoxApiToken appears in the credential selector. Keep Provision switched off for this check.
Help me troubleshoot the secret-backed preview.
Your portal now has a secret-backed identity while preserving its preview-first behavior. Next up, you will connect the guarded Provision path to the existing VM workflow.
Turn the Preview into Provisioning
The previous step gave your PowerShell Universal portal a secret-backed credential selector. The preview path still cannot create infrastructure.
A Proxmox VE request needs an authorization header built from the selected API token. The wrapper can then pass the approved fields into the existing provisioner.
In this step, get ready to:
- Protect the execution path with the selected credential.
- Send the approved request to the existing provisioning workflow.
- Verify that the new VM reaches a running state.
Add the execution branch
The wrapper needs to reject provisioning when no credential is selected. It also needs to translate the stored [PSCredential] into the header expected by the existing provisioner.
- In the PowerShell Universal console, return to Automation.
- Select Scripts.
- Select Request-LabVmPortal.ps1.
- Replace the final placeholder error with the credential branch below:
if ($null -eq $ApiToken) {
throw 'Select the ProxmoxApiToken credential before enabling Provision.'
}
$plainSecret = ConvertFrom-SecureString -SecureString $ApiToken.Password -AsPlainText
$apiHeaders = @{
Authorization = "PVEAPIToken=$($ApiToken.UserName)=$plainSecret"
}
What does this code do?
- The $ApiToken guard stops provisioning when the form has no selected credential.
- ConvertFrom-SecureString obtains the secret inside the job process.
- $apiHeaders stores the authorization value required by the existing provisioner.
- The wrapper does not send $plainSecret to the browser output.
- Save Request-LabVmPortal.ps1.
- Return to the portal at http://localhost:5000/proxmox.
- Submit the same safe request with Provision disabled.
You should see Action as Preview only.
Good. The safe default still returns the governed request without contacting Proxmox VE.
Preview stopped working?
- Confirm that you replaced only the final placeholder error.
- Check that the closing brace for $apiHeaders remains in place.
- Confirm that Provision is disabled for this preview check.
Help me diagnose the preview branch in my portal wrapper.
Surface progress in the browser
A Script Form can display progress records from its Automation job. The wrapper also uses a finally block so its local secret references are cleared after success or failure.
- Add the execution block below the $apiHeaders block in Request-LabVmPortal.ps1 by pasting this code:
try {
Write-Progress -Activity 'Provisioning lab VM' -Status 'Calling Request-LabVm.ps1' -PercentComplete 10
& "$PSScriptRoot\Request-LabVm.ps1" `
-PveHost $PveHost `
-Node $Node `
-TemplateVmid $TemplateVmid `
-NewVmid $NewVmid `
-VmName $VmName `
-Size $Size `
-TokenId $ApiToken.UserName `
-ApiHeaders $apiHeaders `
-FullClone:$FullClone `
-SkipCertificateCheck:$SkipCertificateCheck
}
finally {
$plainSecret = $null
$apiHeaders = $null
Write-Progress -Activity 'Provisioning lab VM' -Completed
}
How does the execution path work?
- Write-Progress starts the progress record before the provisioner runs.
- & invokes the existing script beside the wrapper.
- $PSScriptRoot resolves the PowerShell Universal repository path at runtime.
- The named arguments pass the validated request into Request-LabVm.ps1.
- finally clears the local references before completing the progress record.
- Save Request-LabVmPortal.ps1.
- Refresh the portal.
- Submit one more request with Provision disabled.
You should still see Preview only in the output. This confirms that the complete wrapper parses while preserving its safe default.
Preview missing after the update?
- Confirm that the try block starts after the closing brace for $apiHeaders.
- Check that each continued provisioner argument ends with the PowerShell backtick shown in the reference.
- Confirm that the final closing brace belongs to the finally block.
Help me find the syntax problem in my provisioning branch.
✔️ Awesome, I've got everything!
Your saved wrapper now matches the complete provisioning version. The preview path remains the default.
ⓧ I'd like to double check the full code
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[string]$PveHost,
[Parameter(Mandatory)]
[string]$Node,
[Parameter(Mandatory)]
[ValidateRange(100, 999999999)]
[int]$TemplateVmid,
[Parameter(Mandatory)]
[ValidateRange(100, 999999999)]
[int]$NewVmid,
[Parameter(Mandatory)]
[string]$VmName,
[Parameter(Mandatory)]
[ValidateSet('small', 'medium')]
[string]$Size,
[PSCredential]$ApiToken,
[switch]$FullClone,
[switch]$SkipCertificateCheck,
[switch]$Provision
)
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
$request = [pscustomobject]@{
Action = if ($Provision) { 'Provision' } else { 'Preview only' }
PveHost = $PveHost
Node = $Node
TemplateVmid = $TemplateVmid
NewVmid = $NewVmid
VmName = $VmName
Size = $Size
FullClone = [bool]$FullClone
SkipCertificateCheck = [bool]$SkipCertificateCheck
}
if (-not $Provision) {
$request
return
}
if ($null -eq $ApiToken) {
throw 'Select the ProxmoxApiToken credential before enabling Provision.'
}
$plainSecret = ConvertFrom-SecureString -SecureString $ApiToken.Password -AsPlainText
$apiHeaders = @{
Authorization = "PVEAPIToken=$($ApiToken.UserName)=$plainSecret"
}
try {
Write-Progress -Activity 'Provisioning lab VM' -Status 'Calling Request-LabVm.ps1' -PercentComplete 10
& "$PSScriptRoot\Request-LabVm.ps1" `
-PveHost $PveHost `
-Node $Node `
-TemplateVmid $TemplateVmid `
-NewVmid $NewVmid `
-VmName $VmName `
-Size $Size `
-TokenId $ApiToken.UserName `
-ApiHeaders $apiHeaders `
-FullClone:$FullClone `
-SkipCertificateCheck:$SkipCertificateCheck
}
finally {
$plainSecret = $null
$apiHeaders = $null
Write-Progress -Activity 'Provisioning lab VM' -Completed
}
This reference shows the complete wrapper after this step. It preserves preview behavior while adding the protected provisioning path.
Submit the governed request
The execution path is ready to create the approved lab VM. The form passes the selected credential into the job without placing its password in source code.
- Return to the portal at http://localhost:5000/proxmox.
- Enter the approved Proxmox host in the PveHost field.
- Enter the recorded node in the Node field.
- Enter the working template VM ID in the TemplateVmid field.
- Enter the reserved unused VM ID in the NewVmid field.
- Enter the approved VM name in the VmName field.
- Select small or medium in the Size field.
Choose lab-only options carefully
FullClone passes your approved clone choice into the existing workflow. Keep it aligned with the request you previewed.
SkipCertificateCheck bypasses certificate validation. Enable it only for the known lab host that uses a self-signed certificate.
This submission creates a real VM from your approved preview.
- Set FullClone to match the approved request.
- Enable SkipCertificateCheck only if the known lab host uses its self-signed certificate.
- Select ProxmoxApiToken in the ApiToken credential selector.
- Enable Provision.
Before you submit, do you expect the browser to stop at the preview or call the existing provisioner?
- Submit the form.
- Keep the browser page open while the job reports progress.
The browser shows progress while the wrapper calls Request-LabVm.ps1. When the workflow finishes, the output includes Status with the value running.
- Confirm that the browser output shows Status as running.
- Confirm that the reserved VM ID appears in the Proxmox VE inventory.
- Confirm that the displayed VM name matches the submitted VmName.
- Confirm that the VM state is running.
Provisioning did not reach running?
- Confirm that the template VM ID points to the working Cloud-Init template.
- Confirm that the destination VM ID is still unused.
- Confirm that ProxmoxApiToken remains selected in the credential field.
Help me diagnose why my governed VM request did not reach a running state.
You have connected a governed browser request to the proven provisioning workflow. The portal now reports a running VM without exposing the token secret.
Secret mission
Add a Guarded Cleanup Preview
Your provisioning portal can create a lab VM. In this mission, you will add a second browser form that previews the guarded cleanup workflow while leaving the selected VM unchanged.
Clean Up Your Resources
Clean Up Your Resources
Choose whether to keep your resources running, pause them for another session, or delete the project-specific resources. A running Proxmox VE VM continues consuming local CPU, memory, and storage under the free local PowerShell Universal Developer License.
Resources you used:
- The demonstration Proxmox VE VM with its local compute, memory, and storage allocation.
- The ProxmoxApiToken database-vault PSCredential used by portal provisioning.
- The /proxmox app backed by ProxmoxPortal.ps1 in %ProgramData%\UniversalAutomation\Repository.
- The portal wrappers Request-LabVmPortal.ps1 and Preview-LabVmRemoval.ps1.
- The copied prerequisite files PveApi.psm1, Request-LabVm.ps1, Remove-LabVm.ps1, and catalog.json.
Keep everything running
No action is needed. Choose this if you are still using the portal or plan to extend it.
- Keep the PowerShell Universal service running so the /proxmox app stays available.
- Keep ProxmoxApiToken in the database vault so provisioning remains available.
- Leave the demonstration VM running only while you have enough local Proxmox VE capacity.
- Retain the project files in %ProgramData%\UniversalAutomation\Repository for future portal work.
Pause - I'll come back to this later
Pause the running processes to free local resources while preserving the portal files and secret for your next session.
- Press the Windows key to open search.
- Type PowerShell 7 and press Enter.
- Stop the demonstration VM without deleting it by running this command:
& "$env:ProgramData\UniversalAutomation\Repository\Remove-LabVm.ps1" `
-PveHost '[[PVE_HOST="your Proxmox host"]]' `
-Node '[[PVE_NODE="your Proxmox node"]]' `
-VmId [[VM_ID="your demonstration VM ID"]] `
-TokenId '[[TOKEN_ID="your full API token ID"]]' `
-ShutdownOnly
What does this command do?
- The Remove-LabVm.ps1 workflow targets the demonstration VM through the existing guarded cleanup path.
- The -ShutdownOnly switch stops the VM while preserving its configuration and storage.
- Return to Proxmox VE.
- Confirm the demonstration VM is stopped while its VM ID and name remain listed.
VM still running?
- Confirm the VM ID in the command matches the demonstration VM.
- Rerun the command with -SkipCertificateCheck only if the known lab host uses the expected self-signed certificate.
- Help me troubleshoot why the guarded cleanup workflow did not stop my Proxmox VE VM.
- Press the Windows key to open search.
- Type Services and press Enter.
- Select the PowerShell Universal service entry.
- Choose Stop.
- Confirm the PowerShell Universal service is no longer running.
Delete - I don't want to use this again
Remove the project-specific resources when you no longer need the portal or its demonstration VM.
Deleting the demonstration VM is irreversible. The guarded workflow targets only the VM ID you supply, so compare that value with Proxmox VE before approving the cleanup.
- Press the Windows key to open search.
- Type PowerShell 7 and press Enter.
- Remove the demonstration VM through the existing guarded cleanup workflow by running this command:
& "$env:ProgramData\UniversalAutomation\Repository\Remove-LabVm.ps1" `
-PveHost '[[PVE_HOST="your Proxmox host"]]' `
-Node '[[PVE_NODE="your Proxmox node"]]' `
-VmId [[VM_ID="your demonstration VM ID"]] `
-TokenId '[[TOKEN_ID="your full API token ID"]]'
What does this command do?
- The command sends the selected VM through the original Remove-LabVm.ps1 lifecycle workflow.
- The -WhatIf preview switch is absent, so the workflow can perform the shutdown and deletion after its safeguards pass.
- Return to the Proxmox VE inventory.
- Confirm the demonstration VM ID and name are no longer listed.
VM still present?
- Confirm the host and node match the values used during provisioning.
- Confirm the API token still has the permissions established in the prerequisite project.
- Help me troubleshoot why my guarded Proxmox VE cleanup did not remove the selected VM.
Removing the stored credential disables portal provisioning until you create it again. Its secret stays hidden during this cleanup.
- Return to the local PowerShell Universal admin console from earlier.
- Select Platform.
- Select Variables.
- Select ProxmoxApiToken.
- Remove the selected secret variable.
- Confirm ProxmoxApiToken is no longer listed.
The final file cleanup targets only the seven copies used by this portal. Your original proxmox-mini-platform folder remains untouched.
- Delete the portal files from %ProgramData%\UniversalAutomation\Repository by running these commands in the same PowerShell 7 window:
cmd /c del /q "%ProgramData%\UniversalAutomation\Repository\PveApi.psm1"
cmd /c del /q "%ProgramData%\UniversalAutomation\Repository\Request-LabVm.ps1"
cmd /c del /q "%ProgramData%\UniversalAutomation\Repository\Remove-LabVm.ps1"
cmd /c del /q "%ProgramData%\UniversalAutomation\Repository\catalog.json"
cmd /c del /q "%ProgramData%\UniversalAutomation\Repository\Request-LabVmPortal.ps1"
cmd /c del /q "%ProgramData%\UniversalAutomation\Repository\Preview-LabVmRemoval.ps1"
cmd /c del /q "%ProgramData%\UniversalAutomation\Repository\ProxmoxPortal.ps1"
What do these commands remove?
- Each command deletes one named portal or provisioner file from the PowerShell Universal repository.
- The commands leave the surrounding repository folder and the original prerequisite project in place.
- Return to %ProgramData%\UniversalAutomation\Repository in File Explorer.
- Confirm the seven project files no longer appear.
Files still listed?
- Run PowerShell 7 as Administrator if Windows blocks access to the repository.
- Stop the PowerShell Universal service in Windows Services if a file is in use. Rerun the file command afterward.
- Help me troubleshoot why the portal files cannot be deleted from the PowerShell Universal repository.
- Press the Windows key to open search.
- Type Services and press Enter.
- Select the PowerShell Universal service entry.
- Choose Stop.
- Confirm the PowerShell Universal service is no longer running.
What stays on Windows?
These steps remove the VM, secret, and project-specific repository files. The PowerShell Universal runtime and Developer License configuration remain installed for another local portal.
Nice Work!
Nice Work!
You did it! You built a local PowerShell Universal portal that provisions a governed Proxmox VE lab VM from the browser.
You've learned how to:
- Build a browser-based self-service form from typed PowerShell parameters. Its safe preview shows the exact request without changing infrastructure.
- Protect the Proxmox API token with a secret-backed credential stored as ProxmoxApiToken. The token secret stays out of source code and portal output.
- Reuse the existing Request-LabVm.ps1 provisioning workflow to create a governed lab VM. The browser reports progress before confirming the final running state.
- Secret Mission: Add a second browser form that runs Preview-LabVmRemoval.ps1 with the original -WhatIf safety gate. The cleanup preview leaves the selected VM unchanged.
Ready to quiz yourself?