Startup Scripts

KubeVirt supports the ability to assign a startup script to a VirtualMachineInstance instance which is executed automatically when the VM initializes.

These scripts are commonly used to automate injection of users and SSH keys into VMs in order to provide remote access to the machine. For example, a startup script can be used to inject credentials into a VM that allows an Ansible job running on a remote host to access and provision the VM.

Startup scripts are not limited to any specific use case though. They can be used to run any arbitrary script in a VM on boot.

Cloud-init

cloud-init is a widely adopted project used for early initialization of a VM. Used by cloud providers such as AWS and GCP, cloud-init has established itself as the defacto method of providing startup scripts to VMs.

Cloud-init documentation can be found here: Cloud-init Documentation.

KubeVirt supports cloud-init’s “NoCloud” and “ConfigDrive” datasources which involve injecting startup scripts into a VM instance through the use of an ephemeral disk. VMs with the cloud-init package installed will detect the ephemeral disk and execute custom userdata scripts at boot.

Ignition

Ignition is an alternative to cloud-init which allows for configuring the VM disk on first boot. You can find the Ignition documentation here. You can also find a comparison between cloud-init and Ignition here.

Ignition can be used with Kubevirt by using the cloudInitConfigDrive volume.

Sysprep

Sysprep is an automation tool for Windows that automates Windows installation, setup, and custom software provisioning.

The general flow is:

  1. Seal the vm image with the Sysprep tool, for example by running:

    1. %WINDIR%\system32\sysprep\sysprep.exe /generalize /shutdown /oobe /mode:vm

    Note

    We need to make sure the base vm does not restart, which can be done by setting the vm run strategy as RerunOnFailure.

    VM runStrategy:

    1. spec:
    2. runStrategy: RerunOnFailure

    More information can be found here:

    Note

    It is important that there is no answer file detected when the Sysprep Tool is triggered, because Windows Setup searches for answer files at the beginning of each configuration pass and caches it. If that happens, when the OS will start - it will just use the cached answer file, ignoring the one we provide through the Sysprep API. More information can be found here.

  2. Providing an Answer file named autounattend.xml in an attached media. The answer file can be provided in a ConfigMap or a Secret with the key autounattend.xml

    The configuration file can be generated with Windows SIM or it can be specified manually according to the information found here:

    Note

    There are also many easy to find online tools available for creating an answer file.

Cloud-init Examples

User Data

KubeVirt supports the cloud-init NoCloud and ConfigDrive data sources which involve injecting startup scripts through the use of a disk attached to the VM.

In order to assign a custom userdata script to a VirtualMachineInstance using this method, users must define a disk and a volume for the NoCloud or ConfigDrive datasource in the VirtualMachineInstance’s spec.

Data Sources

Under most circumstances users should stick to the NoCloud data source as it is the simplest cloud-init data source. Only if NoCloud is not supported by the cloud-init implementation (e.g. coreos-cloudinit) users should switch the data source to ConfigDrive.

Switching the cloud-init data source to ConfigDrive is as easy as changing the volume type in the VirtualMachineInstance’s spec from cloudInitNoCloud to cloudInitConfigDrive.

NoCloud data source:

  1. volumes:
  2. - name: cloudinitvolume
  3. cloudInitNoCloud:
  4. userData: "#cloud-config"

ConfigDrive data source:

  1. volumes:
  2. - name: cloudinitvolume
  3. cloudInitConfigDrive:
  4. userData: "#cloud-config"

See the examples below for more complete cloud-init examples.

Cloud-init user-data as clear text

In the example below, a SSH key is stored in the cloudInitNoCloud Volume’s userData field as clean text. There is a corresponding disks entry that references the cloud-init volume and assigns it to the VM’s device.

  1. # Create a VM manifest with the startup script
  2. # a cloudInitNoCloud volume's userData field.
  3. cat << END > my-vmi.yaml
  4. apiVersion: kubevirt.io/v1
  5. kind: VirtualMachineInstance
  6. metadata:
  7. name: myvmi
  8. spec:
  9. terminationGracePeriodSeconds: 5
  10. domain:
  11. resources:
  12. requests:
  13. memory: 64M
  14. devices:
  15. disks:
  16. - name: containerdisk
  17. disk:
  18. bus: virtio
  19. - name: cloudinitdisk
  20. disk:
  21. bus: virtio
  22. volumes:
  23. - name: containerdisk
  24. containerDisk:
  25. image: kubevirt/cirros-container-disk-demo:latest
  26. - name: cloudinitdisk
  27. cloudInitNoCloud:
  28. userData: |
  29. #cloud-config
  30. ssh_authorized_keys:
  31. - ssh-rsa AAAAB3NzaK8L93bWxnyp test@test.com
  32. END
  33. # Post the Virtual Machine spec to KubeVirt.
  34. kubectl create -f my-vmi.yaml

Cloud-init user-data as base64 string

In the example below, a simple bash script is base64 encoded and stored in the cloudInitNoCloud Volume’s userDataBase64 field. There is a corresponding disks entry that references the cloud-init volume and assigns it to the VM’s device.

Users also have the option of storing the startup script in a Kubernetes Secret and referencing the Secret in the VM’s spec. Examples further down in the document illustrate how that is done.

  1. # Create a simple startup script
  2. cat << END > startup-script.sh
  3. #!/bin/bash
  4. echo "Hi from startup script!"
  5. END
  6. # Create a VM manifest with the startup script base64 encoded into
  7. # a cloudInitNoCloud volume's userDataBase64 field.
  8. cat << END > my-vmi.yaml
  9. apiVersion: kubevirt.io/v1
  10. kind: VirtualMachineInstance
  11. metadata:
  12. name: myvmi
  13. spec:
  14. terminationGracePeriodSeconds: 5
  15. domain:
  16. resources:
  17. requests:
  18. memory: 64M
  19. devices:
  20. disks:
  21. - name: containerdisk
  22. disk:
  23. bus: virtio
  24. - name: cloudinitdisk
  25. disk:
  26. bus: virtio
  27. volumes:
  28. - name: containerdisk
  29. containerDisk:
  30. image: kubevirt/cirros-container-disk-demo:latest
  31. - name: cloudinitdisk
  32. cloudInitNoCloud:
  33. userDataBase64: $(cat startup-script.sh | base64 -w0)
  34. END
  35. # Post the Virtual Machine spec to KubeVirt.
  36. kubectl create -f my-vmi.yaml

Cloud-init UserData as k8s Secret

Users who wish to not store the cloud-init userdata directly in the VirtualMachineInstance spec have the option to store the userdata into a Kubernetes Secret and reference that Secret in the spec.

Multiple VirtualMachineInstance specs can reference the same Kubernetes Secret containing cloud-init userdata.

Below is an example of how to create a Kubernetes Secret containing a startup script and reference that Secret in the VM’s spec.

  1. # Create a simple startup script
  2. cat << END > startup-script.sh
  3. #!/bin/bash
  4. echo "Hi from startup script!"
  5. END
  6. # Store the startup script in a Kubernetes Secret
  7. kubectl create secret generic my-vmi-secret --from-file=userdata=startup-script.sh
  8. # Create a VM manifest and reference the Secret's name in the cloudInitNoCloud
  9. # Volume's secretRef field
  10. cat << END > my-vmi.yaml
  11. apiVersion: kubevirt.io/v1
  12. kind: VirtualMachineInstance
  13. metadata:
  14. name: myvmi
  15. spec:
  16. terminationGracePeriodSeconds: 5
  17. domain:
  18. resources:
  19. requests:
  20. memory: 64M
  21. devices:
  22. disks:
  23. - name: containerdisk
  24. disk:
  25. bus: virtio
  26. - name: cloudinitdisk
  27. disk:
  28. bus: virtio
  29. volumes:
  30. - name: containerdisk
  31. containerDisk:
  32. image: kubevirt/cirros-registry-disk-demo:latest
  33. - name: cloudinitdisk
  34. cloudInitNoCloud:
  35. secretRef:
  36. name: my-vmi-secret
  37. END
  38. # Post the VM
  39. kubectl create -f my-vmi.yaml

Injecting SSH keys with Cloud-init’s Cloud-config

In the examples so far, the cloud-init userdata script has been a bash script. Cloud-init has it’s own configuration that can handle some common tasks such as user creation and SSH key injection.

More cloud-config examples can be found here: Cloud-init Examples

Below is an example of using cloud-config to inject an SSH key for the default user (fedora in this case) of a Fedora Atomic disk image.

  1. # Create the cloud-init cloud-config userdata.
  2. cat << END > startup-script
  3. #cloud-config
  4. password: atomic
  5. chpasswd: { expire: False }
  6. ssh_pwauth: False
  7. ssh_authorized_keys:
  8. - ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC6zdgFiLr1uAK7PdcchDd+LseA5fEOcxCCt7TLlr7Mx6h8jUg+G+8L9JBNZuDzTZSF0dR7qwzdBBQjorAnZTmY3BhsKcFr8Gt4KMGrS6r3DNmGruP8GORvegdWZuXgASKVpXeI7nCIjRJwAaK1x+eGHwAWO9Z8ohcboHbLyffOoSZDSIuk2kRIc47+ENRjg0T6x2VRsqX27g6j4DfPKQZGk0zvXkZaYtr1e2tZgqTBWqZUloMJK8miQq6MktCKAS4VtPk0k7teQX57OGwD6D7uo4b+Cl8aYAAwhn0hc0C2USfbuVHgq88ESo2/+NwV4SQcl3sxCW21yGIjAGt4Hy7J fedora@localhost.localdomain
  9. END
  10. # Create the VM spec
  11. cat << END > my-vmi.yaml
  12. apiVersion: kubevirt.io/v1
  13. kind: VirtualMachineInstance
  14. metadata:
  15. name: sshvmi
  16. spec:
  17. terminationGracePeriodSeconds: 0
  18. domain:
  19. resources:
  20. requests:
  21. memory: 1024M
  22. devices:
  23. disks:
  24. - name: containerdisk
  25. disk:
  26. dev: vda
  27. - name: cloudinitdisk
  28. disk:
  29. dev: vdb
  30. volumes:
  31. - name: containerdisk
  32. containerDisk:
  33. image: kubevirt/fedora-atomic-registry-disk-demo:latest
  34. - name: cloudinitdisk
  35. cloudInitNoCloud:
  36. userDataBase64: $(cat startup-script | base64 -w0)
  37. END
  38. # Post the VirtualMachineInstance spec to KubeVirt.
  39. kubectl create -f my-vmi.yaml
  40. # Connect to VM with passwordless SSH key
  41. ssh -i <insert private key here> fedora@<insert ip here>

Inject SSH key using a Custom Shell Script

Depending on the boot image in use, users may have a mixed experience using cloud-init’s cloud-config to create users and inject SSH keys.

Below is an example of creating a user and injecting SSH keys for that user using a script instead of cloud-config.

  1. cat << END > startup-script.sh
  2. #!/bin/bash
  3. export NEW_USER="foo"
  4. export SSH_PUB_KEY="ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC6zdgFiLr1uAK7PdcchDd+LseA5fEOcxCCt7TLlr7Mx6h8jUg+G+8L9JBNZuDzTZSF0dR7qwzdBBQjorAnZTmY3BhsKcFr8Gt4KMGrS6r3DNmGruP8GORvegdWZuXgASKVpXeI7nCIjRJwAaK1x+eGHwAWO9Z8ohcboHbLyffOoSZDSIuk2kRIc47+ENRjg0T6x2VRsqX27g6j4DfPKQZGk0zvXkZaYtr1e2tZgqTBWqZUloMJK8miQq6MktCKAS4VtPk0k7teQX57OGwD6D7uo4b+Cl8aYAAwhn0hc0C2USfbuVHgq88ESo2/+NwV4SQcl3sxCW21yGIjAGt4Hy7J $NEW_USER@localhost.localdomain"
  5. sudo adduser -U -m $NEW_USER
  6. echo "$NEW_USER:atomic" | chpasswd
  7. sudo mkdir /home/$NEW_USER/.ssh
  8. sudo echo "$SSH_PUB_KEY" > /home/$NEW_USER/.ssh/authorized_keys
  9. sudo chown -R ${NEW_USER}: /home/$NEW_USER/.ssh
  10. END
  11. # Create the VM spec
  12. cat << END > my-vmi.yaml
  13. apiVersion: kubevirt.io/v1
  14. kind: VirtualMachineInstance
  15. metadata:
  16. name: sshvmi
  17. spec:
  18. terminationGracePeriodSeconds: 0
  19. domain:
  20. resources:
  21. requests:
  22. memory: 1024M
  23. devices:
  24. disks:
  25. - name: containerdisk
  26. disk:
  27. dev: vda
  28. - name: cloudinitdisk
  29. disk:
  30. dev: vdb
  31. volumes:
  32. - name: containerdisk
  33. containerDisk:
  34. image: kubevirt/fedora-atomic-registry-disk-demo:latest
  35. - name: cloudinitdisk
  36. cloudInitNoCloud:
  37. userDataBase64: $(cat startup-script.sh | base64 -w0)
  38. END
  39. # Post the VirtualMachineInstance spec to KubeVirt.
  40. kubectl create -f my-vmi.yaml
  41. # Connect to VM with passwordless SSH key
  42. ssh -i <insert private key here> foo@<insert ip here>

Network Config

A cloud-init network version 1 configuration can be set to configure the network at boot.

Cloud-init user-data must be set for cloud-init to parse network-config even if it is just the user-data config header:

  1. #cloud-config

Cloud-init network-config as clear text

In the example below, a simple cloud-init network-config is stored in the cloudInitNoCloud Volume’s networkData field as clean text. There is a corresponding disks entry that references the cloud-init volume and assigns it to the VM’s device.

  1. # Create a VM manifest with the network-config in
  2. # a cloudInitNoCloud volume's networkData field.
  3. cat << END > my-vmi.yaml
  4. apiVersion: kubevirt.io/v1alpha2
  5. kind: VirtualMachineInstance
  6. metadata:
  7. name: myvmi
  8. spec:
  9. terminationGracePeriodSeconds: 5
  10. domain:
  11. resources:
  12. requests:
  13. memory: 64M
  14. devices:
  15. disks:
  16. - name: containerdisk
  17. volumeName: registryvolume
  18. disk:
  19. bus: virtio
  20. - name: cloudinitdisk
  21. volumeName: cloudinitvolume
  22. disk:
  23. bus: virtio
  24. volumes:
  25. - name: registryvolume
  26. containerDisk:
  27. image: kubevirt/cirros-container-disk-demo:latest
  28. - name: cloudinitvolume
  29. cloudInitNoCloud:
  30. userData: "#cloud-config"
  31. networkData: |
  32. network:
  33. version: 1
  34. config:
  35. - type: physical
  36. name: eth0
  37. subnets:
  38. - type: dhcp
  39. END
  40. # Post the Virtual Machine spec to KubeVirt.
  41. kubectl create -f my-vmi.yaml

Cloud-init network-config as base64 string

In the example below, a simple network-config is base64 encoded and stored in the cloudInitNoCloud Volume’s networkDataBase64 field. There is a corresponding disks entry that references the cloud-init volume and assigns it to the VM’s device.

Users also have the option of storing the network-config in a Kubernetes Secret and referencing the Secret in the VM’s spec. Examples further down in the document illustrate how that is done.

  1. # Create a simple network-config
  2. cat << END > network-config
  3. network:
  4. version: 1
  5. config:
  6. - type: physical
  7. name: eth0
  8. subnets:
  9. - type: dhcp
  10. END
  11. # Create a VM manifest with the networkData base64 encoded into
  12. # a cloudInitNoCloud volume's networkDataBase64 field.
  13. cat << END > my-vmi.yaml
  14. apiVersion: kubevirt.io/v1alpha2
  15. kind: VirtualMachineInstance
  16. metadata:
  17. name: myvmi
  18. spec:
  19. terminationGracePeriodSeconds: 5
  20. domain:
  21. resources:
  22. requests:
  23. memory: 64M
  24. devices:
  25. disks:
  26. - name: containerdisk
  27. volumeName: registryvolume
  28. disk:
  29. bus: virtio
  30. - name: cloudinitdisk
  31. volumeName: cloudinitvolume
  32. disk:
  33. bus: virtio
  34. volumes:
  35. - name: registryvolume
  36. containerDisk:
  37. image: kubevirt/cirros-container-disk-demo:latest
  38. - name: cloudinitvolume
  39. cloudInitNoCloud:
  40. userData: "#cloud-config"
  41. networkDataBase64: $(cat network-config | base64 -w0)
  42. END
  43. # Post the Virtual Machine spec to KubeVirt.
  44. kubectl create -f my-vmi.yaml

Cloud-init network-config as k8s Secret

Users who wish to not store the cloud-init network-config directly in the VirtualMachineInstance spec have the option to store the network-config into a Kubernetes Secret and reference that Secret in the spec.

Multiple VirtualMachineInstance specs can reference the same Kubernetes Secret containing cloud-init network-config.

Below is an example of how to create a Kubernetes Secret containing a network-config and reference that Secret in the VM’s spec.

  1. # Create a simple network-config
  2. cat << END > network-config
  3. network:
  4. version: 1
  5. config:
  6. - type: physical
  7. name: eth0
  8. subnets:
  9. - type: dhcp
  10. END
  11. # Store the network-config in a Kubernetes Secret
  12. kubectl create secret generic my-vmi-secret --from-file=networkdata=network-config
  13. # Create a VM manifest and reference the Secret's name in the cloudInitNoCloud
  14. # Volume's secretRef field
  15. cat << END > my-vmi.yaml
  16. apiVersion: kubevirt.io/v1alpha2
  17. kind: VirtualMachineInstance
  18. metadata:
  19. name: myvmi
  20. spec:
  21. terminationGracePeriodSeconds: 5
  22. domain:
  23. resources:
  24. requests:
  25. memory: 64M
  26. devices:
  27. disks:
  28. - name: containerdisk
  29. volumeName: registryvolume
  30. disk:
  31. bus: virtio
  32. - name: cloudinitdisk
  33. volumeName: cloudinitvolume
  34. disk:
  35. bus: virtio
  36. volumes:
  37. - name: registryvolume
  38. containerDisk:
  39. image: kubevirt/cirros-registry-disk-demo:latest
  40. - name: cloudinitvolume
  41. cloudInitNoCloud:
  42. userData: "#cloud-config"
  43. networkDataSecretRef:
  44. name: my-vmi-secret
  45. END
  46. # Post the VM
  47. kubectl create -f my-vmi.yaml

Debugging

Depending on the operating system distribution in use, cloud-init output is often printed to the console output on boot up. When developing userdata scripts, users can connect to the VM’s console during boot up to debug.

Example of connecting to console using virtctl:

  1. virtctl console <name of vmi>

Device Role Tagging

KubeVirt provides a mechanism for users to tag devices such as Network Interfaces with a specific role. The tag will be matched to the hardware address of the device and this mapping exposed to the guest OS via cloud-init.

This additional metadata will help the guest OS users with multiple networks interfaces to identify the devices that may have a specific role, such as a network device dedicated to a specific service or a disk intended to be used by a specific application (database, webcache, etc.)

This functionality already exists in platforms such as OpenStack. KubeVirt will provide the data in a similar format, known to users and services like cloud-init.

For example:

  1. kind: VirtualMachineInstance
  2. spec:
  3. domain:
  4. devices:
  5. interfaces:
  6. - masquerade: {}
  7. name: default
  8. - bridge: {}
  9. name: ptp
  10. tag: ptp
  11. - name: sriov-net
  12. sriov: {}
  13. tag: nfvfunc
  14. networks:
  15. - name: default
  16. pod: {}
  17. - multus:
  18. networkName: ptp-conf
  19. name: ptp
  20. networkName: sriov/sriov-network
  21. name: sriov-net
  22. The metadata will be available in the guests config drive `openstack/latest/meta_data.json`
  23. {
  24. "devices": [
  25. {
  26. "type": "nic",
  27. "bus": "pci",
  28. "address": "0000:00:02.0",
  29. "mac": "01:22:22:42:22:21",
  30. "tags": ["ptp"]
  31. },
  32. {
  33. "type": "nic",
  34. "bus": "pci",
  35. "address": "0000:81:10.1",
  36. "mac": "01:22:22:42:22:22",
  37. "tags": ["nfvfunc"]
  38. },
  39. ]
  40. }

Ignition Examples

Ignition data can be passed into a cloudInitConfigDrive source using either clear text, a base64 string or a k8s Secret.

Some examples of Ignition configurations can be found in the examples given by the Ignition documentation.

Ignition as clear text

Here is a complete example of a Kubevirt VM using Ignition to add an ssh key to the coreos user at first boot :

  1. apiVersion: kubevirt.io/v1alpha3
  2. kind: VirtualMachine
  3. metadata:
  4. name: ign-demo
  5. spec:
  6. running: false
  7. template:
  8. metadata:
  9. labels:
  10. kubevirt.io/size: small
  11. kubevirt.io/domain: ign-demo
  12. spec:
  13. domain:
  14. devices:
  15. disks:
  16. - name: containerdisk
  17. disk:
  18. bus: virtio
  19. - name: cloudinitdisk
  20. disk:
  21. bus: virtio
  22. interfaces:
  23. - name: default
  24. masquerade: {}
  25. resources:
  26. requests:
  27. memory: 2G
  28. networks:
  29. - name: default
  30. pod: {}
  31. volumes:
  32. - name: containerdisk
  33. containerDisk:
  34. image: quay.io/containerdisks/rhcos:4.9
  35. - name: cloudinitdisk
  36. cloudInitConfigDrive:
  37. userData: |
  38. {
  39. "ignition": {
  40. "config": {},
  41. "proxy": {},
  42. "security": {},
  43. "timeouts": {},
  44. "version": "3.2.0"
  45. },
  46. "passwd": {
  47. "users": [
  48. {
  49. "name": "coreos",
  50. "sshAuthorizedKeys": [
  51. "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPL3axFGHI3db9iJWkPXVbYzD7OaWTtHuqmxLvj+DztB user@example"
  52. ]
  53. }
  54. ]
  55. },
  56. "storage": {},
  57. "systemd": {}
  58. }

See that the Ignition config is simply passed to the userData annotation of the cloudInitConfigDrive volume.

Ignition as base64

You can also pass the Ignition config as a base64 string by using the userDatabase64 annotation :

  1. ...
  2. cloudInitConfigDrive:
  3. userDataBase64: eyJpZ25pdGlvbiI6eyJjb25maWciOnt9LCJwcm94eSI6e30sInNlY3VyaXR5Ijp7fSwidGltZW91dHMiOnt9LCJ2ZXJzaW9uIjoiMy4yLjAifSwicGFzc3dkIjp7InVzZXJzIjpbeyJuYW1lIjoiY29yZW9zIiwic3NoQXV0aG9yaXplZEtleXMiOlsic3NoLWVkMjU1MTlBQUFBQzNOemFDMWxaREkxTlRFNUFBQUFJUEwzYXhGR0hJM2RiOWlKV2tQWFZiWXpEN09hV1R0SHVxbXhMdmorRHp0QiB1c2VyQGV4YW1wbGUiXX1dfSwic3RvcmFnZSI6e30sInN5c3RlbWQiOnt9fQ==

You can obtain the base64 string by doing cat ignition.json | base64 -w0 in your terminal.

Ignition as k8s Secret

If you do not want to store the Ignition config into the VM configuration, you can use a k8s Secret.

First, create the secret with the ignition data in it :

  1. kubectl create secret generic my-ign-secret --from-file=ignition=ignition.json

Then specify this secret into your VM configuration :

  1. ...
  2. cloudInitConfigDrive:
  3. secretRef:
  4. name: my-ign-secret

Sysprep Examples

Sysprep in a ConfigMap

The answer file can be provided in a ConfigMap:

  1. apiVersion: v1
  2. kind: ConfigMap
  3. metadata:
  4. name: sysprep-config
  5. data:
  6. autounattend.xml: |
  7. <?xml version="1.0" encoding="utf-8"?>
  8. <unattend xmlns="urn:schemas-microsoft-com:unattend">
  9. ...
  10. </unattend>

And attached to the VM like so:

  1. kind: VirtualMachine
  2. metadata:
  3. name: windows-with-sysprep
  4. spec:
  5. running: false
  6. template:
  7. metadata:
  8. labels:
  9. kubevirt.io/domain: windows-with-sysprep
  10. spec:
  11. domain:
  12. cpu:
  13. cores: 3
  14. devices:
  15. disks:
  16. - bootOrder: 1
  17. disk:
  18. bus: virtio
  19. name: harddrive
  20. - name: sysprep
  21. cdrom:
  22. bus: sata
  23. machine:
  24. type: q35
  25. resources:
  26. requests:
  27. memory: 6G
  28. volumes:
  29. - name: harddrive
  30. persistentVolumeClaim:
  31. claimName: windows_pvc
  32. - name: sysprep
  33. sysprep:
  34. configMap:
  35. name: sysprep-config

Sysprep in a Secret

The answer file can be provided in a Secret:

  1. apiVersion: v1
  2. kind: Secret
  3. metadata:
  4. name: sysprep-config
  5. stringData:
  6. data:
  7. autounattend.xml: |
  8. <?xml version="1.0" encoding="utf-8"?>
  9. <unattend xmlns="urn:schemas-microsoft-com:unattend">
  10. ...
  11. </unattend>

And attached to the VM like so:

  1. kind: VirtualMachine
  2. metadata:
  3. name: windows-with-sysprep
  4. spec:
  5. running: false
  6. template:
  7. metadata:
  8. labels:
  9. kubevirt.io/domain: windows-with-sysprep
  10. spec:
  11. domain:
  12. cpu:
  13. cores: 3
  14. devices:
  15. disks:
  16. - bootOrder: 1
  17. disk:
  18. bus: virtio
  19. name: harddrive
  20. - name: sysprep
  21. cdrom:
  22. bus: sata
  23. machine:
  24. type: q35
  25. resources:
  26. requests:
  27. memory: 6G
  28. volumes:
  29. - name: harddrive
  30. persistentVolumeClaim:
  31. claimName: windows_pvc
  32. - name: sysprep
  33. sysprep:
  34. secret:
  35. name: sysprep-secret

Base Sysprep VM

In the example below, a configMap with autounattend.xml file is used to modify the Windows iso image which is downloaded from Microsoft and creates a base installed Windows machine with virtio drivers installed and all the commands executed in post-install.ps1 For the below manifests to work it needs to have win10-iso DataVolume.

  1. apiVersion: v1
  2. kind: ConfigMap
  3. metadata:
  4. name: win10-template-configmap
  5. data:
  6. autounattend.xml: |-
  7. <?xml version="1.0" encoding="utf-8"?>
  8. <unattend xmlns="urn:schemas-microsoft-com:unattend">
  9. <settings pass="windowsPE">
  10. <component xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" name="Microsoft-Windows-International-Core-WinPE" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS">
  11. <SetupUILanguage>
  12. <UILanguage>en-US</UILanguage>
  13. </SetupUILanguage>
  14. <InputLocale>0409:00000409</InputLocale>
  15. <SystemLocale>en-US</SystemLocale>
  16. <UILanguage>en-US</UILanguage>
  17. <UILanguageFallback>en-US</UILanguageFallback>
  18. <UserLocale>en-US</UserLocale>
  19. </component>
  20. <component xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" name="Microsoft-Windows-PnpCustomizationsWinPE" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS">
  21. <DriverPaths>
  22. <PathAndCredentials wcm:keyValue="4b29ba63" wcm:action="add">
  23. <Path>E:\amd64\2k19</Path>
  24. </PathAndCredentials>
  25. <PathAndCredentials wcm:keyValue="25fe51ea" wcm:action="add">
  26. <Path>E:\NetKVM\2k19\amd64</Path>
  27. </PathAndCredentials>
  28. </DriverPaths>
  29. </component>
  30. <component xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" name="Microsoft-Windows-Setup" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS">
  31. <DiskConfiguration>
  32. <Disk wcm:action="add">
  33. <CreatePartitions>
  34. <CreatePartition wcm:action="add">
  35. <Order>1</Order>
  36. <Type>Primary</Type>
  37. <Size>100</Size>
  38. </CreatePartition>
  39. <CreatePartition wcm:action="add">
  40. <Extend>true</Extend>
  41. <Order>2</Order>
  42. <Type>Primary</Type>
  43. </CreatePartition>
  44. </CreatePartitions>
  45. <ModifyPartitions>
  46. <ModifyPartition wcm:action="add">
  47. <Format>NTFS</Format>
  48. <Label>System Reserved</Label>
  49. <Order>1</Order>
  50. <PartitionID>1</PartitionID>
  51. <TypeID>0x27</TypeID>
  52. </ModifyPartition>
  53. <ModifyPartition wcm:action="add">
  54. <Format>NTFS</Format>
  55. <Label>OS</Label>
  56. <Letter>C</Letter>
  57. <Order>2</Order>
  58. <PartitionID>2</PartitionID>
  59. </ModifyPartition>
  60. </ModifyPartitions>
  61. <DiskID>0</DiskID>
  62. <WillWipeDisk>true</WillWipeDisk>
  63. </Disk>
  64. </DiskConfiguration>
  65. <ImageInstall>
  66. <OSImage>
  67. <InstallFrom>
  68. <MetaData wcm:action="add">
  69. <Key>/Image/Description</Key>
  70. <Value>Windows 10 Pro</Value>
  71. </MetaData>
  72. </InstallFrom>
  73. <InstallTo>
  74. <DiskID>0</DiskID>
  75. <PartitionID>2</PartitionID>
  76. </InstallTo>
  77. </OSImage>
  78. </ImageInstall>
  79. <UserData>
  80. <AcceptEula>true</AcceptEula>
  81. <FullName/>
  82. <Organization/>
  83. <ProductKey>
  84. <Key/>
  85. </ProductKey>
  86. </UserData>
  87. </component>
  88. </settings>
  89. <settings pass="offlineServicing">
  90. <component xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" name="Microsoft-Windows-LUA-Settings" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS">
  91. <EnableLUA>false</EnableLUA>
  92. </component>
  93. </settings>
  94. <settings pass="specialize">
  95. <component xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" name="Microsoft-Windows-International-Core" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS">
  96. <InputLocale>0409:00000409</InputLocale>
  97. <SystemLocale>en-US</SystemLocale>
  98. <UILanguage>en-US</UILanguage>
  99. <UILanguageFallback>en-US</UILanguageFallback>
  100. <UserLocale>en-US</UserLocale>
  101. </component>
  102. <component xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" name="Microsoft-Windows-Security-SPP-UX" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS">
  103. <SkipAutoActivation>true</SkipAutoActivation>
  104. </component>
  105. <component xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" name="Microsoft-Windows-SQMApi" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS">
  106. <CEIPEnabled>0</CEIPEnabled>
  107. </component>
  108. </settings>
  109. <settings pass="oobeSystem">
  110. <component xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" name="Microsoft-Windows-Shell-Setup" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS">
  111. <OOBE>
  112. <HideEULAPage>true</HideEULAPage>
  113. <HideOEMRegistrationScreen>true</HideOEMRegistrationScreen>
  114. <HideOnlineAccountScreens>true</HideOnlineAccountScreens>
  115. <HideWirelessSetupInOOBE>true</HideWirelessSetupInOOBE>
  116. <NetworkLocation>Work</NetworkLocation>
  117. <SkipUserOOBE>true</SkipUserOOBE>
  118. <SkipMachineOOBE>true</SkipMachineOOBE>
  119. <ProtectYourPC>3</ProtectYourPC>
  120. </OOBE>
  121. <AutoLogon>
  122. <Password>
  123. <Value>123456</Value>
  124. <PlainText>true</PlainText>
  125. </Password>
  126. <Enabled>true</Enabled>
  127. <Username>Administrator</Username>
  128. </AutoLogon>
  129. <UserAccounts>
  130. <AdministratorPassword>
  131. <Value>123456</Value>
  132. <PlainText>true</PlainText>
  133. </AdministratorPassword>
  134. </UserAccounts>
  135. <RegisteredOrganization/>
  136. <RegisteredOwner/>
  137. <TimeZone>Eastern Standard Time</TimeZone>
  138. <FirstLogonCommands>
  139. <SynchronousCommand wcm:action="add">
  140. <CommandLine>powershell -ExecutionPolicy Bypass -NoExit -NoProfile f:\post-install.ps1</CommandLine>
  141. <RequiresUserInput>false</RequiresUserInput>
  142. <Order>1</Order>
  143. <Description>Post Installation Script</Description>
  144. </SynchronousCommand>
  145. </FirstLogonCommands>
  146. </component>
  147. </settings>
  148. </unattend>
  149. post-install.ps1: |-
  150. # Remove AutoLogin
  151. # https://docs.microsoft.com/en-us/windows-hardware/customize/desktop/unattend/microsoft-windows-shell-setup-autologon-logoncount#logoncount-known-issue
  152. reg add "HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon" /v AutoAdminLogon /t REG_SZ /d 0 /f
  153. # install Qemu Tools (Drivers)
  154. Start-Process msiexec -Wait -ArgumentList '/i e:\virtio-win-gt-x64.msi /qn /passive /norestart'
  155. # install Guest Agent
  156. Start-Process msiexec -Wait -ArgumentList '/i e:\guest-agent\qemu-ga-x86_64.msi /qn /passive /norestart'
  157. # Rename cached unattend.xml to avoid it is picked up by sysprep
  158. mv C:\Windows\Panther\unattend.xml C:\Windows\Panther\unattend.install.xml
  159. # Eject CD, to avoid that the autounattend.xml on the CD is picked up by sysprep
  160. (new-object -COM Shell.Application).NameSpace(17).ParseName('F:').InvokeVerb('Eject')
  161. # Run Sysprep and Shutdown
  162. C:\Windows\System32\Sysprep\sysprep.exe /generalize /oobe /shutdown /mode:vm
  163. ---
  164. apiVersion: kubevirt.io/v1
  165. kind: VirtualMachine
  166. metadata:
  167. annotations:
  168. name.os.template.kubevirt.io/win10: Microsoft Windows 10
  169. vm.kubevirt.io/validations: |
  170. [
  171. {
  172. "name": "minimal-required-memory",
  173. "path": "jsonpath::.spec.domain.resources.requests.memory",
  174. "rule": "integer",
  175. "message": "This VM requires more memory.",
  176. "min": 2147483648
  177. }, {
  178. "name": "windows-virtio-bus",
  179. "path": "jsonpath::.spec.domain.devices.disks[*].disk.bus",
  180. "valid": "jsonpath::.spec.domain.devices.disks[*].disk.bus",
  181. "rule": "enum",
  182. "message": "virto disk bus type has better performance, install virtio drivers in VM and change bus type",
  183. "values": ["virtio"],
  184. "justWarning": true
  185. }, {
  186. "name": "windows-disk-bus",
  187. "path": "jsonpath::.spec.domain.devices.disks[*].disk.bus",
  188. "valid": "jsonpath::.spec.domain.devices.disks[*].disk.bus",
  189. "rule": "enum",
  190. "message": "disk bus has to be either virtio or sata or scsi",
  191. "values": ["virtio", "sata", "scsi"]
  192. }, {
  193. "name": "windows-cd-bus",
  194. "path": "jsonpath::.spec.domain.devices.disks[*].cdrom.bus",
  195. "valid": "jsonpath::.spec.domain.devices.disks[*].cdrom.bus",
  196. "rule": "enum",
  197. "message": "cd bus has to be sata",
  198. "values": ["sata"]
  199. }
  200. ]
  201. name: win10-template
  202. namespace: default
  203. labels:
  204. app: win10-template
  205. flavor.template.kubevirt.io/medium: 'true'
  206. os.template.kubevirt.io/win10: 'true'
  207. vm.kubevirt.io/template: windows10-desktop-medium
  208. vm.kubevirt.io/template.namespace: openshift
  209. vm.kubevirt.io/template.revision: '1'
  210. vm.kubevirt.io/template.version: v0.14.0
  211. workload.template.kubevirt.io/desktop: 'true'
  212. spec:
  213. runStrategy: RerunOnFailure
  214. dataVolumeTemplates:
  215. - metadata:
  216. name: win10-template-windows-iso
  217. spec:
  218. pvc:
  219. accessModes:
  220. - ReadWriteOnce
  221. resources:
  222. requests:
  223. storage: 20Gi
  224. source:
  225. pvc:
  226. name: windows10-iso
  227. namespace: default
  228. - metadata:
  229. name: win10-template
  230. spec:
  231. pvc:
  232. accessModes:
  233. - ReadWriteOnce
  234. resources:
  235. requests:
  236. storage: 25Gi
  237. volumeMode: Filesystem
  238. source:
  239. blank: {}
  240. template:
  241. metadata:
  242. annotations:
  243. vm.kubevirt.io/flavor: medium
  244. vm.kubevirt.io/os: windows10
  245. vm.kubevirt.io/workload: desktop
  246. labels:
  247. flavor.template.kubevirt.io/medium: 'true'
  248. kubevirt.io/domain: win10-template
  249. kubevirt.io/size: medium
  250. os.template.kubevirt.io/win10: 'true'
  251. vm.kubevirt.io/name: win10-template
  252. workload.template.kubevirt.io/desktop: 'true'
  253. spec:
  254. domain:
  255. clock:
  256. timer:
  257. hpet:
  258. present: false
  259. hyperv: {}
  260. pit:
  261. tickPolicy: delay
  262. rtc:
  263. tickPolicy: catchup
  264. utc: {}
  265. cpu:
  266. cores: 1
  267. sockets: 1
  268. threads: 1
  269. devices:
  270. disks:
  271. - bootOrder: 1
  272. disk:
  273. bus: virtio
  274. name: win10-template
  275. - bootOrder: 2
  276. cdrom:
  277. bus: sata
  278. name: windows-iso
  279. - cdrom:
  280. bus: sata
  281. name: windows-guest-tools
  282. - name: sysprep
  283. cdrom:
  284. bus: sata
  285. inputs:
  286. - bus: usb
  287. name: tablet
  288. type: tablet
  289. interfaces:
  290. - masquerade: {}
  291. model: virtio
  292. name: default
  293. features:
  294. acpi: {}
  295. apic: {}
  296. hyperv:
  297. reenlightenment: {}
  298. ipi: {}
  299. synic: {}
  300. synictimer:
  301. direct: {}
  302. spinlocks:
  303. spinlocks: 8191
  304. reset: {}
  305. relaxed: {}
  306. vpindex: {}
  307. runtime: {}
  308. tlbflush: {}
  309. frequencies: {}
  310. vapic: {}
  311. machine:
  312. type: pc-q35-rhel8.4.0
  313. resources:
  314. requests:
  315. memory: 4Gi
  316. hostname: win10-template
  317. networks:
  318. - name: default
  319. pod: {}
  320. volumes:
  321. - dataVolume:
  322. name: win10-iso
  323. name: windows-iso
  324. - dataVolume:
  325. name: win10-template-windows-iso
  326. name: win10-template
  327. - containerDisk:
  328. image: quay.io/kubevirt/virtio-container-disk
  329. name: windows-guest-tools
  330. - name: sysprep
  331. sysprep:
  332. configMap:
  333. name: win10-template-configmap

Launching a VM from template

From the above example after the sysprep command is executed in the post-install.ps1 and the vm is in shutdown state, A new VM can be launched from the base win10-template with additional changes mentioned from the below unattend.xml in sysprep-config. The new VM can take upto 5 minutes to be in running state since Windows goes through oobe setup in the background with the customizations specified in the below unattend.xml file.

  1. apiVersion: v1
  2. kind: ConfigMap
  3. metadata:
  4. name: sysprep-config
  5. data:
  6. autounattend.xml: |-
  7. <?xml version="1.0" encoding="utf-8"?>
  8. <!-- responsible for installing windows, ignored on sysprepped images -->
  9. unattend.xml: |-
  10. <?xml version="1.0" encoding="utf-8"?>
  11. <unattend xmlns="urn:schemas-microsoft-com:unattend">
  12. <settings pass="oobeSystem">
  13. <component xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" name="Microsoft-Windows-Shell-Setup" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS">
  14. <OOBE>
  15. <HideEULAPage>true</HideEULAPage>
  16. <HideOEMRegistrationScreen>true</HideOEMRegistrationScreen>
  17. <HideOnlineAccountScreens>true</HideOnlineAccountScreens>
  18. <HideWirelessSetupInOOBE>true</HideWirelessSetupInOOBE>
  19. <NetworkLocation>Work</NetworkLocation>
  20. <SkipUserOOBE>true</SkipUserOOBE>
  21. <SkipMachineOOBE>true</SkipMachineOOBE>
  22. <ProtectYourPC>3</ProtectYourPC>
  23. </OOBE>
  24. <AutoLogon>
  25. <Password>
  26. <Value>123456</Value>
  27. <PlainText>true</PlainText>
  28. </Password>
  29. <Enabled>true</Enabled>
  30. <Username>Administrator</Username>
  31. </AutoLogon>
  32. <UserAccounts>
  33. <AdministratorPassword>
  34. <Value>123456</Value>
  35. <PlainText>true</PlainText>
  36. </AdministratorPassword>
  37. </UserAccounts>
  38. <RegisteredOrganization>Kuebvirt</RegisteredOrganization>
  39. <RegisteredOwner>Kubevirt</RegisteredOwner>
  40. <TimeZone>Eastern Standard Time</TimeZone>
  41. <FirstLogonCommands>
  42. <SynchronousCommand wcm:action="add">
  43. <CommandLine>powershell -ExecutionPolicy Bypass -NoExit -WindowStyle Hidden -NoProfile d:\customize.ps1</CommandLine>
  44. <RequiresUserInput>false</RequiresUserInput>
  45. <Order>1</Order>
  46. <Description>Customize Script</Description>
  47. </SynchronousCommand>
  48. </FirstLogonCommands>
  49. </component>
  50. </settings>
  51. </unattend>
  52. customize.ps1: |-
  53. # Enable RDP
  54. Set-ItemProperty -Path 'HKLM:\System\CurrentControlSet\Control\Terminal Server' -name "fDenyTSConnections" -value 0
  55. Enable-NetFirewallRule -DisplayGroup "Remote Desktop"
  56. # https://docs.microsoft.com/en-us/windows-server/administration/openssh/openssh_install_firstuse
  57. # Install the OpenSSH Server
  58. Add-WindowsCapability -Online -Name OpenSSH.Server~~~~0.0.1.0
  59. # Start the sshd service
  60. Start-Service sshd
  61. Set-Service -Name sshd -StartupType 'Automatic'
  62. # https://docs.microsoft.com/en-us/windows-server/administration/openssh/openssh_server_configuration
  63. # use powershell as default shell for ssh
  64. New-ItemProperty -Path "HKLM:\SOFTWARE\OpenSSH" -Name DefaultShell -Value "C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe" -PropertyType String -Force
  65. # Add ssh authorized_key for administrator
  66. # https://docs.microsoft.com/en-us/windows-server/administration/openssh/openssh_keymanagement
  67. $MyDir = $MyInvocation.MyCommand.Path | Split-Path -Parent
  68. $PublicKey = Get-Content -Path $MyDir\id_rsa.pub
  69. $authrized_keys_path = $env:ProgramData + "\ssh\administrators_authorized_keys"
  70. Add-Content -Path $authrized_keys_path -Value $PublicKey
  71. icacls.exe $authrized_keys_path /inheritance:r /grant "Administrators:F" /grant "SYSTEM:F"
  72. # install application via exe file installer from url
  73. function Install-Exe {
  74. $dlurl = $args[0]
  75. $installerPath = Join-Path $env:TEMP (Split-Path $dlurl -Leaf)
  76. Invoke-WebRequest -UseBasicParsing $dlurl -OutFile $installerPath
  77. Start-Process -FilePath $installerPath -Args "/S" -Verb RunAs -Wait
  78. Remove-Item $installerPath
  79. }
  80. # Wait for networking before running a task at startup
  81. do {
  82. $ping = test-connection -comp kubevirt.io -count 1 -Quiet
  83. } until ($ping)
  84. # Installing the Latest Notepad++ with PowerShell
  85. $BaseUri = "https://notepad-plus-plus.org"
  86. $BasePage = Invoke-WebRequest -Uri $BaseUri -UseBasicParsing
  87. $ChildPath = $BasePage.Links | Where-Object { $_.outerHTML -like '*Current Version*' } | Select-Object -ExpandProperty href
  88. $DownloadPageUri = $BaseUri + $ChildPath
  89. $DownloadPage = Invoke-WebRequest -Uri $DownloadPageUri -UseBasicParsing
  90. $DownloadUrl = $DownloadPage.Links | Where-Object { $_.outerHTML -like '*npp.*.Installer.x64.exe"*' } | Select-Object -ExpandProperty href
  91. Install-Exe $DownloadUrl
  92. id_rsa.pub: |-
  93. ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC6zdgFiLr1uAK7PdcchDd+LseA5fEOcxCCt7TLlr7Mx6h8jUg+G+8L9JBNZuDzTZSF0dR7qwzdBBQjorAnZTmY3BhsKcFr8Gt4KMGrS6r3DNmGruP8GORvegdWZuXgASKVpXeI7nCIjRJwAaK1x+eGHwAWO9Z8ohcboHbLyffOoSZDSIuk2kRIc47+ENRjg0T6x2VRsqX27g6j4DfPKQZGk0zvXkZaYtr1e2tZgqTBWqZUloMJK8miQq6MktCKAS4VtPk0k7teQX57OGwD6D7uo4b+Cl8aYAAwhn0hc0C2USfbuVHgq88ESo2/+NwV4SQcl3sxCW21yGIjAGt4Hy7J fedora@localhost.localdomain