The PlanVMS format provides granular control over individual VM migration settings, allowing customization of target names, disk configuration, networking, and migration behavior on a per-VM basis. This chapter covers the complete VM customization capabilities.
PlanVMS format is a structured YAML/JSON format that defines individual VM configurations within a migration plan. It enables:
- Per-VM Customization: Different settings for each VM in the same plan
- Resource Templates: Custom naming templates for generated resources
- Migration Behavior: Individual migration settings and cleanup policies
- Target Configuration: Specific target cluster settings per VM
- Complex Migrations: When VMs require different target configurations
- Name Normalization: When source VM names need target-specific adjustments
- Resource Management: When custom resource naming is required
- Hook Integration: When different VMs need different automation hooks
- Security Requirements: When VMs have different encryption or security needs
The PlanVMS format is based on the Forklift API VM specification, verified from the vendor code:
Basic VM Structure
1
2
3
4
| # Basic VM entry
- name: source-vm-name # Required: Source VM name
targetName: target-vm-name # Optional: Custom target name
rootDisk: /dev/sda # Optional: Boot disk selection
|
Complete VM Structure
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| - name: web-server-01
targetName: web-prod-01
rootDisk: /dev/sda
instanceType: web-server
targetPowerState: on
deleteVmOnFailMigration: false
nbdeClevis: false
luks:
name: encryption-keys
namespace: security
hooks:
- step: PreHook
hook:
name: backup-hook
namespace: migration-hooks
- step: PostHook
hook:
name: validation-hook
namespace: migration-hooks
pvcNameTemplate: "{{.TargetVmName}}-{{.DiskIndex}}"
volumeNameTemplate: "vol-{{.VolumeIndex}}"
networkNameTemplate: "net-{{.NetworkIndex}}"
|
Editable Fields for Customization
All fields are verified from the Forklift API VM struct definition:
Core VM Configuration
Name and Identity
1
2
3
4
5
| # Required source VM identifier
- name: source-vm-name
# Optional custom target name
targetName: custom-target-name
|
Name Field Requirements:
name: Must match exactly the VM name in the source provider
targetName: Must be valid Kubernetes resource name (DNS-1123 compliant)
Target Power State
1
2
| # Control VM power state after migration
targetPowerState: on # Options: on, off, auto (default)
|
Power State Options:
on: Start VM after migration completes
off: Keep VM powered off after migration
auto: Match source VM power state (default behavior)
Instance Type Override
1
2
| # Override VM resource specifications
instanceType: high-performance
|
Selects a predefined InstanceType resource that overrides CPU, memory, and other VM properties.
Disk and Storage Configuration
Root Disk Selection
1
2
| # Specify the primary boot disk
rootDisk: /dev/sda
|
Critical for multi-disk VMs to ensure proper boot configuration.
LUKS Disk Encryption
1
2
3
4
5
6
7
| # Reference to LUKS encryption keys
luks:
name: vm-encryption-keys
namespace: security-namespace
# Enable automatic TANG/Clevis unlocking
nbdeClevis: true
|
LUKS Configuration:
luks: References a Kubernetes Secret containing LUKS passphrases
nbdeClevis: Enables network-based automatic unlocking using TANG servers
Migration Behavior Configuration
Failure Cleanup Policy
1
2
| # Control VM deletion on migration failure
deleteVmOnFailMigration: true
|
Cleanup Behavior:
true: Delete target VM and resources if migration fails
false: Preserve target VM for troubleshooting (default)
Note: Plan-level setting overrides VM-level setting when enabled.
Hook Integration
1
2
3
4
5
6
7
8
9
10
| # Attach migration hooks to specific VMs
hooks:
- step: PreHook
hook:
name: database-quiesce
namespace: migration-hooks
- step: PostHook
hook:
name: health-check
namespace: migration-hooks
|
Hook Configuration:
step: Hook execution phase (PreHook, PostHook)
hook.name: Name of the Hook resource
hook.namespace: Namespace containing the Hook resource
Go Template Variables Reference
kubectl-mtv provides rich template variables for resource naming, verified from the API documentation:
PVC Name Template Variables
Available in pvcNameTemplate field:
| Variable |
Description |
Example Value |
{{.VmName}} |
Original source VM name |
web-server-01 |
{{.TargetVmName}} |
Final target VM name |
web-prod-01 |
{{.PlanName}} |
Migration plan name |
production-migration |
{{.DiskIndex}} |
Disk index (0-based) |
0, 1, 2 |
{{.WinDriveLetter}} |
Windows drive letter |
c, d, e |
{{.RootDiskIndex}} |
Index of root/boot disk |
0 |
{{.Shared}} |
True if disk is shared |
true, false |
{{.FileName}} |
VMware disk filename |
web-server-01.vmdk |
PVC Template Examples
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| # Basic PVC naming
pvcNameTemplate: "{{.TargetVmName}}-disk-{{.DiskIndex}}"
# Result: web-prod-01-disk-0, web-prod-01-disk-1
# Root vs data disk differentiation
pvcNameTemplate: "{{if eq .DiskIndex .RootDiskIndex}}{{.TargetVmName}}-root{{else}}{{.TargetVmName}}-data-{{.DiskIndex}}{{end}}"
# Result: web-prod-01-root, web-prod-01-data-1
# Shared disk identification
pvcNameTemplate: "{{if .Shared}}shared-{{end}}{{.TargetVmName}}-{{.DiskIndex}}"
# Result: web-prod-01-0, shared-web-prod-01-1
# Windows drive letter naming
pvcNameTemplate: "{{.TargetVmName}}-{{.WinDriveLetter}}-drive"
# Result: windows-server-c-drive, windows-server-d-drive
# Plan-scoped naming
pvcNameTemplate: "{{.PlanName}}-{{.TargetVmName}}-disk{{.DiskIndex}}"
# Result: prod-migration-web-prod-01-disk0
|
Volume Name Template Variables
Available in volumeNameTemplate field:
| Variable |
Description |
Example Value |
{{.PVCName}} |
Generated PVC name |
web-prod-01-disk-0 |
{{.VolumeIndex}} |
Volume interface index |
0, 1, 2 |
Volume Template Examples
1
2
3
4
5
6
7
8
9
10
11
| # Simple volume naming
volumeNameTemplate: "disk-{{.VolumeIndex}}"
# Result: disk-0, disk-1, disk-2
# PVC-based naming
volumeNameTemplate: "vol-{{.PVCName}}"
# Result: vol-web-prod-01-disk-0
# Combined indexing
volumeNameTemplate: "{{.VolumeIndex}}-{{.PVCName}}"
# Result: 0-web-prod-01-disk-0
|
Network Name Template Variables
Available in networkNameTemplate field:
| Variable |
Description |
Example Value |
{{.NetworkName}} |
Multus network name |
production-net |
{{.NetworkNamespace}} |
Network namespace |
multus-system |
{{.NetworkType}} |
Network type |
Multus, Pod |
{{.NetworkIndex}} |
Interface index |
0, 1, 2 |
Network Template Examples
1
2
3
4
5
6
7
8
9
10
11
| # Simple interface naming
networkNameTemplate: "net-{{.NetworkIndex}}"
# Result: net-0, net-1, net-2
# Type-based naming
networkNameTemplate: "{{if eq .NetworkType \"Pod\"}}pod-net{{else}}multus-{{.NetworkIndex}}{{end}}"
# Result: pod-net, multus-1, multus-2
# Network-specific naming
networkNameTemplate: "{{.NetworkType}}-{{.NetworkName}}-{{.NetworkIndex}}"
# Result: Multus-production-net-0
|
How-To: Editing the List
Method 1: Create PlanVMS File from Scratch
Basic VM List Creation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
| # Save as vm-customizations.yaml
- name: web-server-01
targetName: web-prod-01
targetPowerState: on
pvcNameTemplate: "{{.TargetVmName}}-{{.DiskIndex}}"
- name: database-01
targetName: db-prod-01
rootDisk: /dev/sda
targetPowerState: on
instanceType: database-server
deleteVmOnFailMigration: false
- name: app-server-01
targetName: app-prod-01
hooks:
- step: PreHook
hook:
name: app-shutdown
namespace: migration-hooks
- step: PostHook
hook:
name: app-startup
namespace: migration-hooks
|
Use Custom VM List in Plan
1
2
3
4
5
| kubectl mtv create plan custom-vm-migration \
--source vsphere-prod \
--vms @vm-customizations.yaml \
--network-mapping prod-network-map \
--storage-mapping prod-storage-map
|
Method 2: Export and Modify Existing Inventory
1
2
3
4
5
6
7
| # Export all VMs from provider
kubectl mtv get inventory vms vsphere-prod -o planvms > all-vms.yaml
# Export filtered VMs
kubectl mtv get inventory vms vsphere-prod \
-q "where powerState = 'poweredOn' and memoryMB >= 4096" \
-o planvms > production-vms.yaml
|
Modify Exported VMs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| # Original exported format
- name: web-server-01
targetName: ""
rootDisk: ""
# Modified with customizations
- name: web-server-01
targetName: web-prod-01
rootDisk: /dev/sda
targetPowerState: on
pvcNameTemplate: "prod-{{.TargetVmName}}-disk-{{.DiskIndex}}"
hooks:
- step: PostHook
hook:
name: web-validation
namespace: migration-hooks
|
Method 3: Template-Based Mass Customization
Create Template for Similar VMs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| # Template for web servers
- name: web-server-01
targetName: web-prod-01
targetPowerState: on
pvcNameTemplate: "web-{{.TargetVmName}}-{{.DiskIndex}}"
volumeNameTemplate: "vol-{{.VolumeIndex}}"
hooks:
- step: PostHook
hook:
name: web-health-check
namespace: migration-hooks
- name: web-server-02
targetName: web-prod-02
targetPowerState: on
pvcNameTemplate: "web-{{.TargetVmName}}-{{.DiskIndex}}"
volumeNameTemplate: "vol-{{.VolumeIndex}}"
hooks:
- step: PostHook
hook:
name: web-health-check
namespace: migration-hooks
|
Advanced Customization Scenarios
Scenario 1: Database Cluster Migration
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
| # Database cluster with shared storage
- name: db-primary-01
targetName: postgres-primary
rootDisk: /dev/sda
instanceType: database-primary
targetPowerState: on
pvcNameTemplate: "{{.TargetVmName}}-{{if .Shared}}shared-{{end}}{{.DiskIndex}}"
luks:
name: db-encryption-keys
namespace: database-security
hooks:
- step: PreHook
hook:
name: database-backup
namespace: db-hooks
- step: PostHook
hook:
name: database-validate
namespace: db-hooks
- name: db-replica-01
targetName: postgres-replica-01
rootDisk: /dev/sda
instanceType: database-replica
targetPowerState: on
pvcNameTemplate: "{{.TargetVmName}}-{{if .Shared}}shared-{{end}}{{.DiskIndex}}"
luks:
name: db-encryption-keys
namespace: database-security
- name: db-replica-02
targetName: postgres-replica-02
rootDisk: /dev/sda
instanceType: database-replica
targetPowerState: on
pvcNameTemplate: "{{.TargetVmName}}-{{if .Shared}}shared-{{end}}{{.DiskIndex}}"
luks:
name: db-encryption-keys
namespace: database-security
|
Scenario 2: Windows Domain Migration
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
| # Windows domain controller
- name: dc01
targetName: domain-controller-01
rootDisk: /dev/sda
targetPowerState: on
instanceType: windows-server
pvcNameTemplate: "{{.TargetVmName}}-{{.WinDriveLetter}}"
volumeNameTemplate: "{{.WinDriveLetter}}-drive"
hooks:
- step: PreHook
hook:
name: ad-replication-pause
namespace: windows-hooks
- step: PostHook
hook:
name: ad-health-check
namespace: windows-hooks
# Windows file server
- name: fileserver01
targetName: file-server-01
rootDisk: /dev/sda
targetPowerState: on
instanceType: file-server
pvcNameTemplate: "{{.TargetVmName}}-{{.WinDriveLetter}}-{{if .Shared}}shared{{else}}local{{end}}"
deleteVmOnFailMigration: false
|
Scenario 3: Multi-Tier Application
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
| # Web tier
- name: web-lb-01
targetName: web-loadbalancer
targetPowerState: on
instanceType: load-balancer
pvcNameTemplate: "web-{{.TargetVmName}}-{{.DiskIndex}}"
hooks:
- step: PreHook
hook:
name: drain-connections
namespace: web-hooks
# Application tier
- name: app-server-01
targetName: app-primary
targetPowerState: on
instanceType: application-server
pvcNameTemplate: "app-{{.TargetVmName}}-{{.DiskIndex}}"
hooks:
- step: PreHook
hook:
name: app-graceful-shutdown
namespace: app-hooks
- step: PostHook
hook:
name: app-health-check
namespace: app-hooks
# Data tier
- name: cache-redis-01
targetName: redis-cache
targetPowerState: on
instanceType: cache-server
pvcNameTemplate: "cache-{{.TargetVmName}}-{{.DiskIndex}}"
luks:
name: cache-encryption
namespace: security
|
Scenario 4: Development Environment Normalization
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| # Normalize development VM names
- name: "Dev Web Server 01" # Source name with spaces
targetName: dev-web-01 # Kubernetes-compliant name
targetPowerState: on
pvcNameTemplate: "dev-{{.TargetVmName | lower}}-disk{{.DiskIndex}}"
- name: "Test Database (MySQL)"
targetName: test-mysql-db
rootDisk: /dev/sda
targetPowerState: on
pvcNameTemplate: "test-{{.TargetVmName}}-{{if eq .DiskIndex .RootDiskIndex}}os{{else}}data{{end}}"
- name: "QA_Environment_App"
targetName: qa-app-server
targetPowerState: off # Keep powered off initially
deleteVmOnFailMigration: true # Clean up failures in test env
|
Template Functions and Advanced Usage
Built-in Template Functions
kubectl-mtv supports Go template functions for advanced string manipulation:
String Functions
1
2
3
4
5
6
7
8
| # Lowercase conversion
pvcNameTemplate: "{{.TargetVmName | lower}}-{{.DiskIndex}}"
# Replace characters
pvcNameTemplate: "{{.VmName | replace \" \" \"-\" | lower}}-disk{{.DiskIndex}}"
# Conditional logic
pvcNameTemplate: "{{if .Shared}}shared-{{else}}local-{{end}}{{.TargetVmName}}"
|
Complex Conditional Templates
1
2
3
4
5
6
7
8
| # Multi-condition PVC naming
pvcNameTemplate: "{{if eq .DiskIndex .RootDiskIndex}}root{{else if .Shared}}shared-data{{else}}data{{end}}-{{.DiskIndex}}"
# Windows vs Linux differentiation
volumeNameTemplate: "{{if .WinDriveLetter}}{{.WinDriveLetter}}-drive{{else}}disk-{{.VolumeIndex}}{{end}}"
# Network type-based naming
networkNameTemplate: "{{if eq .NetworkType \"Pod\"}}pod{{else}}{{.NetworkName | lower}}{{end}}-{{.NetworkIndex}}"
|
Validation and Testing
Syntax Validation
1
2
3
4
5
6
7
8
| # Validate YAML syntax
yamllint vm-customizations.yaml
# Test with kubectl dry-run
kubectl mtv create plan test-validation \
--source vsphere-prod \
--vms @vm-customizations.yaml \
--dry-run=client
|
Template Testing
1
2
3
4
5
6
7
8
9
| # Test template rendering (requires actual plan creation)
kubectl mtv create plan template-test \
--source vsphere-prod \
--vms @template-test.yaml \
-n test-namespace
# Check generated resource names
kubectl get pvc -n test-namespace
kubectl describe vm template-test-vm -n test-namespace
|
Field Validation
Required Field Check
1
2
3
4
5
| # Minimal valid VM entry
- name: source-vm-name # Required
# Invalid: missing name
- targetName: target-only # Error: name is required
|
Target Name Validation
1
2
3
4
5
6
7
| # Valid target names (DNS-1123 compliant)
- name: source-vm
targetName: valid-vm-name-01
# Invalid target names
- name: source-vm
targetName: "Invalid Name With Spaces" # Error: invalid format
|
Integration with Plan Creation
Using PlanVMS in Migration Plans
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| # Create plan with custom VM configurations
kubectl mtv create plan customized-migration \
--source vsphere-prod \
--target openshift-prod \
--network-mapping prod-network-map \
--storage-mapping prod-storage-map \
--migration-type warm \
--vms @customized-vms.yaml
# Combine with plan-level settings
kubectl mtv create plan comprehensive-migration \
--source vsphere-prod \
--target-namespace production \
--migration-type warm \
--preserve-static-ips \
--vms @enterprise-vms.yaml
|
Template Override Behavior
Plan-level templates are overridden by VM-level templates:
1
2
3
4
5
6
7
8
| # Plan-level template
kubectl mtv create plan plan-template \
--pvc-name-template "{{.PlanName}}-{{.VmName}}-{{.DiskIndex}}" \
--vms @vms-with-templates.yaml
# VM-level template overrides plan-level
# VMs with pvcNameTemplate: use VM template
# VMs without pvcNameTemplate: use plan template
|
Troubleshooting PlanVMS Issues
Common PlanVMS Errors
1
2
3
4
5
| # Check YAML syntax
python -c "import yaml; yaml.safe_load(open('vm-list.yaml'))"
# Validate with yq
yq eval . vm-list.yaml
|
Template Rendering Errors
1
2
3
4
5
| # Check template variables
kubectl logs -n konveyor-forklift deployment/forklift-controller | grep template
# Validate generated names
kubectl get pvc,vm -n target-namespace --show-labels
|
Name Conflicts
1
2
3
4
5
| # Check for duplicate target names
grep -n "targetName:" vm-list.yaml | sort -k2
# Verify uniqueness in target namespace
kubectl get vm -n target-namespace -o name
|
Debug PlanVMS Processing
1
2
3
4
5
6
7
8
9
10
| # Monitor plan creation with verbosity
kubectl mtv create plan debug-planvms \
--vms @debug-vms.yaml \
-v=2
# Check plan status
kubectl describe plan debug-planvms
# Monitor VM processing
kubectl get vmstatus -n migration-namespace --watch
|
Best Practices for PlanVMS Usage
Design Principles
- Consistency: Use consistent naming patterns across similar VMs
- Clarity: Make target names self-documenting
- Scalability: Design templates that work for large VM sets
- Security: Properly configure LUKS and encryption settings
Operational Guidelines
- Version Control: Store PlanVMS files in version control systems
- Documentation: Document custom template logic and naming conventions
- Testing: Validate PlanVMS configurations in test environments first
- Monitoring: Track resource usage of generated PVCs and volumes
Template Design Best Practices
- Readable Names: Generate human-readable resource names
- Unique Identifiers: Ensure generated names are unique
- Length Limits: Keep names under Kubernetes limits (63 characters)
- Special Characters: Avoid special characters in generated names
Next Steps
After mastering PlanVMS customization:
- Advanced Placement: Learn VM placement strategies in Chapter 12: Target VM Placement
- Performance Optimization: Apply customization insights in Chapter 13: Migration Process Optimization
- Hook Development: Create custom hooks in Chapter 14: Migration Hooks
- Plan Patching: Modify plans dynamically in Chapter 15: Advanced Plan Patching
Previous: Chapter 10: Migration Plan Creation
Next: Chapter 12: Target VM Placement