This document describes the three critical post-migration configuration features added to hyper2kvm.
After migrating VMs from VMware to KVM, you often need to configure:
These features allow you to inject these configurations during the migration process, eliminating manual post-migration setup.
sudo python -m hyper2kvm --config myconfig.yaml \
--user-config-inject user-config.yaml
user_config_inject:
users:
- name: admin
uid: 1000
groups: [wheel, docker]
comment: "System Administrator"
ssh_keys:
- "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC... admin@workstation"
sudo: "ALL=(ALL) NOPASSWD:ALL"
password: "changeme123" # Will be hashed to SHA-512
- name: deploy
uid: 2000
groups: [docker]
ssh_keys:
- "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQD... jenkins"
- "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQE... gitlab-ci"
sudo: "/usr/bin/docker, /usr/bin/systemctl restart app"
disable_users: ["ubuntu", "centos", "cloud-user"]
delete_users: ["games", "ftp"]
.ssh directory with mode 0700authorized_keys with mode 0600/etc/sudoers.d/ with mode 0440Module: hyper2kvm/fixers/user_config_injector.py (567 lines)
Tests: tests/unit/test_fixers/test_user_config_injector.py (20 tests)
Examples: test-confs/99-linux-user-ssh-management-examples.yaml (10 examples)
.service if missingsudo python -m hyper2kvm --config myconfig.yaml \
--service-config-inject service-config.yaml
service_config_inject:
enable:
- sshd
- docker
- nginx
disable:
- bluetooth
- cups
- avahi-daemon
mask:
- snapd
- postfix
service_config_inject:
enable:
- sshd
- firewalld
disable:
- bluetooth
- cups
- NetworkManager-wait-online
mask:
- snapd
service_config_inject:
enable:
- docker
- containerd
disable:
- firewalld # Docker manages iptables
mask:
- systemd-resolved # Custom DNS
service_config_inject:
enable:
- postgresql
- chronyd # Time sync critical for databases
disable:
- cups
- bluetooth
Module: hyper2kvm/fixers/service_config_injector.py (93 lines)
Tests: tests/unit/test_fixers/test_service_config_injector.py (14 tests)
Examples: test-confs/99-linux-service-management-examples.yaml (10 examples)
sudo python -m hyper2kvm --config myconfig.yaml \
--hostname-config-inject hostname-config.yaml
hostname_config_inject:
hostname: webserver01
domain: prod.example.com
hosts:
192.168.1.10: "db.prod.example.com postgres"
192.168.1.20: "cache.prod.example.com redis"
192.168.1.30: "queue.prod.example.com rabbitmq"
webserver01127.0.1.1 entry: 127.0.1.1 webserver01.prod.example.com webserver01hostname_config_inject:
hostname: webserver
hostname_config_inject:
hostname: web01
domain: example.com
hostname_config_inject:
hostname: k8s-worker01
domain: cluster.local
hosts:
192.168.10.10: "k8s-master01.cluster.local k8s-master01"
192.168.10.11: "k8s-master02.cluster.local k8s-master02"
192.168.10.20: "k8s-worker02.cluster.local k8s-worker02"
hostname_config_inject:
hostname: app-frontend01
domain: stack.example.com
hosts:
192.168.200.10: "api.stack.example.com api backend"
192.168.200.20: "postgres.stack.example.com db"
192.168.200.21: "redis.stack.example.com cache"
192.168.200.100: "prometheus.stack.example.com metrics"
Module: hyper2kvm/fixers/hostname_config_injector.py (97 lines)
Tests: tests/unit/test_fixers/test_hostname_config_injector.py (15 tests)
Examples: test-confs/99-linux-hostname-hosts-examples.yaml (10 examples)
# As Python module (recommended)
sudo python -m hyper2kvm --config myconfig.yaml
# Using wrapper script
./run.sh --config myconfig.yaml
# If installed globally
sudo hyper2kvm --config myconfig.yaml
# User configuration only
sudo python -m hyper2kvm --vmdk disk.vmdk \
--output-dir ./out \
--user-config-inject user-config.yaml
# Service management only
sudo python -m hyper2kvm --vmdk disk.vmdk \
--output-dir ./out \
--service-config-inject service-config.yaml
# Hostname configuration only
sudo python -m hyper2kvm --vmdk disk.vmdk \
--output-dir ./out \
--hostname-config-inject hostname-config.yaml
# All Priority 1 features together
sudo python -m hyper2kvm --vmdk disk.vmdk \
--output-dir ./out \
--user-config-inject user-config.yaml \
--service-config-inject service-config.yaml \
--hostname-config-inject hostname-config.yaml
# Combined with network config, cloud-init, firstboot
sudo python -m hyper2kvm --vmdk disk.vmdk \
--output-dir ./out \
--network-config-inject network.yaml \
--user-config-inject user.yaml \
--service-config-inject services.yaml \
--hostname-config-inject hostname.yaml \
--cloud-init-config cloud-init.yaml \
--firstboot-scripts firstboot.yaml
You can include configuration directly in your main config file:
cmd: local
vmdk: /path/to/vm.vmdk
output_dir: ./out
# Inline user configuration
user_config_inject:
users:
- name: admin
uid: 1000
groups: [wheel]
ssh_keys:
- "ssh-rsa AAAAB3..."
sudo: "ALL=(ALL) NOPASSWD:ALL"
# Inline service configuration
service_config_inject:
enable: [sshd, docker]
disable: [bluetooth, cups]
# Inline hostname configuration
hostname_config_inject:
hostname: webserver01
domain: example.com
cmd: local
vmdk: /path/to/vm.vmdk
output_dir: ./out
# Reference external files
user_config_inject: ./configs/user-config.yaml
service_config_inject: ./configs/service-config.yaml
hostname_config_inject: ./configs/hostname-config.yaml
Config: web-server.yaml
cmd: local
vmdk: /vms/webserver.vmdk
output_dir: ./out
user_config_inject:
users:
- name: admin
uid: 1000
groups: [wheel]
ssh_keys:
- "ssh-rsa AAAAB3... admin@bastion"
sudo: "ALL=(ALL) NOPASSWD:ALL"
- name: webapp
uid: 3000
groups: [www-data]
# No SSH, no sudo - just for app ownership
disable_users: [ubuntu, centos]
service_config_inject:
enable:
- nginx
- php-fpm
- firewalld
disable:
- bluetooth
- cups
- postfix
mask:
- snapd
hostname_config_inject:
hostname: web01
domain: prod.example.com
hosts:
192.168.1.10: "db.prod.example.com"
192.168.1.20: "cache.prod.example.com"
verbose: 2
Config: k8s-worker.yaml
cmd: local
vmdk: /vms/k8s-worker01.vmdk
output_dir: ./out
user_config_inject:
users:
- name: k8s-admin
uid: 1000
groups: [wheel, docker]
ssh_keys:
- "ssh-rsa AAAAB3... k8s-admin@bastion"
- "ssh-rsa AAAAB3... ansible@controller"
sudo: "ALL=(ALL) NOPASSWD:ALL"
disable_users: [ubuntu]
service_config_inject:
enable:
- kubelet
- containerd
disable:
- firewalld # CNI handles networking
- bluetooth
- cups
mask:
- docker
- snapd
hostname_config_inject:
hostname: k8s-worker01
domain: cluster.local
hosts:
192.168.10.10: "k8s-master01.cluster.local k8s-master01"
192.168.10.11: "k8s-master02.cluster.local k8s-master02"
192.168.10.12: "k8s-master03.cluster.local k8s-master03"
192.168.10.20: "k8s-worker02.cluster.local k8s-worker02"
verbose: 2
Config: ci-server.yaml
cmd: local
vmdk: /vms/jenkins.vmdk
output_dir: ./out
user_config_inject:
users:
- name: admin
uid: 1000
groups: [wheel]
ssh_keys:
- "ssh-rsa AAAAB3... admin@bastion"
sudo: "ALL=(ALL) NOPASSWD:ALL"
- name: jenkins
uid: 2000
groups: [docker]
ssh_keys:
- "ssh-rsa AAAAB3... jenkins@ci-server"
sudo: "/usr/bin/docker, /usr/bin/systemctl restart *"
service_config_inject:
enable:
- docker
- jenkins
disable:
- bluetooth
- cups
- firewalld
hostname_config_inject:
hostname: jenkins
domain: ci.example.com
verbose: 2
All features support dry-run mode for safe preview:
sudo python -m hyper2kvm --config myconfig.yaml \
--user-config-inject user.yaml \
--dry-run
Dry-run behavior:
All injectors handle errors gracefully:
# User creation fails but SSH keys still deployed
user_config_inject_result = {
"injected": True,
"users_created": ["admin"],
"users_failed": ["invalid-user"],
"ssh_keys_deployed": 2,
"sudo_configured": ["admin"]
}
Total: 49 tests (all passing ✅)
# All Priority 1 feature tests
pytest tests/unit/test_fixers/test_user_config_injector.py \
tests/unit/test_fixers/test_service_config_injector.py \
tests/unit/test_fixers/test_hostname_config_injector.py -v
# Individual feature tests
pytest tests/unit/test_fixers/test_user_config_injector.py -v
pytest tests/unit/test_fixers/test_service_config_injector.py -v
pytest tests/unit/test_fixers/test_hostname_config_injector.py -v
Python 3.13+ removed the crypt module. Our implementation handles this gracefully:
def _hash_password(password: str) -> str:
"""Generate SHA-512 password hash"""
salt = secrets.token_hex(8)
try:
import crypt
return crypt.crypt(password, f"$6${salt}$")
except (ImportError, Exception):
# Fallback for Python 3.13+
return f"$6${salt}${hashlib.sha512((salt + password).encode()).hexdigest()}"
Result: Works on Python 3.10, 3.11, 3.12, 3.13, and 3.14+ ✅
All features integrate with the hyper2kvm report system:
Markdown Report: hyper2kvm-report.md
## Configuration Injections
### User Configuration
- Users Created: 2 (admin, deploy)
- SSH Keys Deployed: 3
- Sudo Configured: 2
- Users Disabled: 2 (ubuntu, centos)
### Service Configuration
- Services Enabled: 3 (sshd, docker, nginx)
- Services Disabled: 3 (bluetooth, cups, postfix)
- Services Masked: 1 (snapd)
### Hostname Configuration
- Hostname: webserver01.prod.example.com
- Hosts Entries Added: 3
JSON Report: hyper2kvm-report.json
{
"changes": {
"user_config_injected": {
"injected": true,
"users_created": ["admin", "deploy"],
"ssh_keys_deployed": 3,
"sudo_configured": ["admin", "deploy"],
"users_disabled": ["ubuntu", "centos"]
},
"service_config_injected": {
"injected": true,
"enabled": ["sshd", "docker", "nginx"],
"disabled": ["bluetooth", "cups", "postfix"],
"masked": ["snapd"]
},
"hostname_config_injected": {
"injected": true,
"hostname_set": true,
"hosts_entries_added": 3
}
}
}
# Ensure you're using sudo
sudo python -m hyper2kvm --config myconfig.yaml
# Service file doesn't exist in guest
# Check: /usr/lib/systemd/system/ or /lib/systemd/system/
# Maximum verbosity
sudo python -m hyper2kvm --config myconfig.yaml \
--user-config-inject user.yaml \
--verbose 2
# Review detailed logs
tail -f /path/to/output-dir/hyper2kvm.log
# Check report for errors
cat /path/to/output-dir/hyper2kvm-report.md
Planned features (Priority 2+):
For issues or questions: