Skip to content

Customizing a Guest

The ability to deploy a virtual machine with Kubernetes is nice, but one of the values of VM Operator is its support for popular bootstrap providers such as Cloud-Init, Sysprep, and vAppConfig. This page reviews these bootstrap providers to help inform when to select one over the other.

Bootstrap Providers

There are a number of methods that may be used to bootstrap a virtual machine's (VM) guest operating system:

Provider Network Config Linux Windows Description
Cloud-Init Cloud-Init Network v2 The industry standard, multi-distro method for cross-platform, cloud instance initialization with modern, VM images
LinuxPrep Guest OS Customization (GOSC) LinuxPrep is used by VMware to customize Linux images on first-boot or at runtime
Sysprep Guest OS Customization (GOSC) Microsoft Sysprep is used by VMware to customize Windows images on first-boot
vAppConfig Bespoke For images with bespoke, bootstrap engines driven by vAppConfig properties

Cloud-Init

Cloud-Init is widely recognized as the de facto method for bootstrapping modern VM instances on hyperscalers, including Tanzu with VM Operator.

Windows and Cloud-Init

It is possible to use the Cloud-Init bootstrap provider to deploy a Windows image if it contains Cloudbase-Init, the Windows port of Cloud-Init.

Inline Cloud Config

The VirtualMachine API directly supports specifying a Cloud-Init cloud config for bootstrapping:

  • users
  • commands
  • files
apiVersion: vmoperator.vmware.com/v1alpha2
kind: VirtualMachine
metadata:
  name:      my-vm
  namespace: my-namespace
spec:
  className:    my-vm-class
  imageName:    vmi-0a0044d7c690bcbea
  storageClass: my-storage-class
  bootstrap:
    cloudInit:
      cloudConfig:
        users:
        - name: jdoe
          primary_group: jdoe
          groups: users
          passwd:
            name: my-vm-bootstrap-data
            key:  jdoe-passwd
          ssh_authorized_keys:
          - ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDSL7uWGj...
        runcmd:
        - "ls /"
        - [ "ls", "-a", "-l", "/" ]
        write_files:
        - path: /etc/my-plain-text
          permissions: '0644'
          owner: root:root
          content: |
            Hello, world.
        - path: /etc/my-secret-data
          permissions: '0644'
          owner: root:root
          content:
            name: my-vm-bootstrap-data
            key:  etc-my-secret-data
apiVersion: v1
kind: Secret
metadata:
  name:      my-vm-bootstrap-data
  namespace: my-namespace
stringData:
  jdoe-passwd: my-password
  etc-my-secret-data: |
    My super secret message.

The data in the above Secret is referenced by portions of the VirtualMachine resource's inline cloud config.

Schema Differences

Please note, there are a few differences between the inline Cloud Config and the official format. Please refer to ./api/v1alpha2/cloudinit/cloudconfig.go for up-to-date information on how the inline Cloud Config compares to the official format.

Default User

The inline users list may not include the keyword default to indicate the default user should remain active, ex:

users:
- default
- name: jdoe
  primary_group: jdoe
  ...

Instead, please use the defaultUserEnabled property which is a sibling of users:

defaultUserEnabled: true
users:
- name: jdoe
  primary_group: jdoe
  ...

Please note it is only necessary to use defaultUserEnabled when the users list is non-empty and the default users should still be enabled.

Secret Key Selector

Some values cannot be specified directly so as to disallow the inclusion of sensitive data directly in the VirtualMachine resource. Instead, these field values are SecretKeySelector data types, specifying the name of a Secret resource and name in said resource of the key that contains the value. These fields include:

  • users[].hashed_passwd
  • users[].passwd
  • write_files[].content

For example, here is how a user would specify a password:

users:
- name: jdoe
  primary_group: jdoe
  groups: users
  passwd:
    name: my-vm-bootstrap-data
    key:  jdoe-passwd
  ssh_authorized_keys:
  - ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDSL7uWGj...
Dynamic Properties

Despite the rules and conventions for Kubernetes custom resource definitions (CRD), there are also some nice tricks in place to try and retain many of the dynamic properties from the official Cloud Config format. For example, the write_files[].content field may specify a field from a Secret resource when the data is sensitive, but the field may also directly specify the file's contents as well:

write_files:
- path: /etc/my-plain-text
  permissions: '0644'
  owner: root:root
  content: |
    Hello, world.
- path: /etc/my-secret-data
  permissions: '0644'
  owner: root:root
  content:
    name: my-vm-bootstrap-data
    key:  etc-my-secret-data

Another such field is runcmd, a list of the commands to run at first boot. Normally this would be specified as the following in a CRD:

runcmd:
- ls -al
- echo Hello, world.

However, just like in the official format, the inline run commands may be specified as a string or a list of strings, ex:

runcmd:
- ls -al
- - echo
  - Hello, world.

Raw Cloud Config

When more advanced configurations are required, a raw cloud config may be used via a Secret resource:

apiVersion: vmoperator.vmware.com/v1alpha2
kind: VirtualMachine
metadata:
  name:      my-vm
  namespace: my-namespace
spec:
  className:    my-vm-class
  imageName:    vmi-0a0044d7c690bcbea
  storageClass: my-storage-class
  bootstrap:
    cloudInit:
      rawCloudConfig:
        key:  my-vm-bootstrap-data
        name: user-data
apiVersion: v1
kind: Secret
metadata:
  name:      my-vm-bootstrap-data
  namespace: my-namespace
stringData:
  user-data: |
    #cloud-config
    users:
    - default
    - name: jdoe
      primary_group: jdoe
      groups: users
      passwd: my-password
      ssh_authorized_keys:
      - ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDSL7uWGj...
    runcmd:
    - "ls /"
    - [ "ls", "-a", "-l", "/" ]
    write_files:
    - path: /etc/my-plaintext
      permissions: '0644'
      owner: root:root
      content: |
        Hello, world.
    - path: /etc/my-secret-data
      permissions: '0644'
      owner: root:root
      content: |
        My super secret message.

LinuxPrep

If using Linux and Cloud-Init is not an option, try the LinuxPrep bootstrap provider, which uses VMware tools to bootstrap a Linux guest operating system. It has minimal configuration options, but it supports a wide-range of Linux distributions. The following YAML may be used to bootstrap a guest using LinuxPrep:

apiVersion: vmoperator.vmware.com/v1alpha2
kind: VirtualMachine
metadata:
  name:      my-vm
  namespace: my-namespace
spec:
  className:    my-vm-class
  imageName:    vmi-0a0044d7c690bcbea
  storageClass: my-storage-class
  bootstrap:
    linuxPrep:
      hardwareClockIsUTC: true
      timeZone: America/Chicago

Sysprep

Microsoft originally designed Sysprep as a means to prepare a deployed system for use as a template. It was such a useful tool, VMware utilized it as the means to customize a VM with a Windows guest.

Sysprep State

Deploying Windows images that have not completed their previous Sysprep operation could cause the Guest OS customization to fail. Therefore, it is important to ensure that the image is sealed correctly and in a clean state when using Sysprep. For more information on this issue, please refer to this article.

Inline Sysprep

It is possible to specify the sysprep configuration inline with the VirtualMachine API:

Volume License SKU

The following YAML may be used to bootstrap a Windows image that uses the volume license SKU and does not require a product ID:

apiVersion: vmoperator.vmware.com/v1alpha2
kind: VirtualMachine
metadata:
  name:      my-vm
  namespace: my-namespace
spec:
  className:    my-vm-class
  imageName:    vmi-0a0044d7c690bcbea
  storageClass: my-storage-class
  bootstrap:
    sysprep:
      sysprep: {}

Product ID

The following may be used to bootstrap a Windows image that requires a product ID to activate Windows:

apiVersion: vmoperator.vmware.com/v1alpha2
kind: VirtualMachine
metadata:
  name:      my-vm
  namespace: my-namespace
spec:
  className:    my-vm-class
  imageName:    vmi-0a0044d7c690bcbea
  storageClass: my-storage-class
  bootstrap:
    sysprep:
      sysprep:
        userData:
          fullName: John Doe
          orgName:  Lost and Still Lost
          productID:
            name: my-vm-bootstrap-data
            key:  product-id
apiVersion: v1
kind: Secret
metadata:
  name:      my-vm-bootstrap-data
  namespace: my-namespace
stringData:
  product-id: "0123456789..."

Raw Sysprep

Sometimes it is necessary to provide the sysprep answers file directly, in which case the raw sysprep option is available.

Minimal Config

The following YAML may be used to bootstrap a Windows image with minimal information. For proper network configuration and Guest OS Customization (GOSC) completion, Sysprep unattend data requires a template for providing network info and RunSynchronousCommand to record GOSC status. Both components are essential for Windows Vista and later versions.

Product Key

Please note the image would have to be using a Volume License SKU as the product ID is not provided in the following Sysprep configuration. Please the Activate Windows section below for more information.

apiVersion: vmoperator.vmware.com/v1alpha2
kind: VirtualMachine
metadata:
  name:      my-vm
  namespace: my-namespace
spec:
  className:    my-vm-class
  imageName:    vmi-0a0044d7c690bcbea
  storageClass: my-storage-class
  bootstrap:
    sysprep:
      rawSysprep:
        name: my-vm-bootstrap-data
        key:  unattend
apiVersion: v1
kind: Secret
metadata:
  name: my-vm-bootstrap-data
  namespace: my-namespace
stringData:
  unattend: |
    <?xml version="1.0" encoding="UTF-8"?>
    <unattend xmlns="urn:schemas-microsoft-com:unattend">
      <settings pass="specialize">
        <component name="Microsoft-Windows-TCPIP" processorArchitecture="amd64"
          publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS"
          xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State"
          xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
          <Interfaces>
            <Interface wcm:action="add">
              <Ipv4Settings>
                <DhcpEnabled>false</DhcpEnabled>
              </Ipv4Settings>
              <Ipv6Settings>
                <DhcpEnabled>false</DhcpEnabled>
              </Ipv6Settings>
              <Identifier>{{ V1alpha1_FirstNicMacAddr }}</Identifier>
              <UnicastIpAddresses>
                <IpAddress wcm:action="add" wcm:keyValue="1">{{ V1alpha1_FirstIP }}</IpAddress>
              </UnicastIpAddresses>
              <Routes>
                <Route wcm:action="add">
                  <Identifier>0</Identifier>
                  <Metric>10</Metric>
                  <NextHopAddress>{{ (index .V1alpha1.Net.Devices 0).Gateway4 }}</NextHopAddress>
                  <Prefix>{{ V1alpha1_SubnetMask V1alpha1_FirstIP }}</Prefix>
                </Route>
              </Routes>
            </Interface>
          </Interfaces>
        </component>
        <component name="Microsoft-Windows-DNS-Client" processorArchitecture="amd64"
          publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS"
          xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State"
          xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
          <Interfaces>
            <Interface wcm:action="add">
              <Identifier>{{ V1alpha1_FirstNicMacAddr }}</Identifier>
              <DNSServerSearchOrder> {{ range .V1alpha1.Net.Nameservers }} <IpAddress
                  wcm:action="add"
                  wcm:keyValue="{{.}}"></IpAddress> {{ end }} </DNSServerSearchOrder>
            </Interface>
          </Interfaces>
        </component>
        <component name="Microsoft-Windows-Deployment" processorArchitecture="amd64"
          publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS"
          xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State"
          xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
          <RunSynchronous>
            <RunSynchronousCommand wcm:action="add">
              <Path>C:\sysprep\guestcustutil.exe restoreMountedDevices</Path>
              <Order>1</Order>
            </RunSynchronousCommand>
            <RunSynchronousCommand wcm:action="add">
              <Path>C:\sysprep\guestcustutil.exe flagComplete</Path>
              <Order>2</Order>
            </RunSynchronousCommand>
            <RunSynchronousCommand wcm:action="add">
              <Path>C:\sysprep\guestcustutil.exe deleteContainingFolder</Path>
              <Order>3</Order>
            </RunSynchronousCommand>
          </RunSynchronous>
        </component>
      </settings>
    </unattend>

Activate Windows

The following XML can be used to supply a product key to activate Windows:

<settings pass="windowsPE">
  <component name="Microsoft-Windows-Setup" processorArchitecture="amd64"
    publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS"
    xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
    <UserData>
      <AcceptEula>true</AcceptEula>
      <FullName>jdoe</FullName>
      <Organization>VMware</Organization>
      <ProductKey>
        <Key>1234-5678-9abc-defg</Key>
        <WillShowUI>Never</WillShowUI>
      </ProductKey>
    </UserData>
  </component>
</settings>

User Accounts

The following XML can be used to update Administrator password and create a new local user account:

<settings pass="oobeSystem">
  <component name="Microsoft-Windows-Shell-Setup" processorArchitecture="amd64"
    publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS"
    xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
    <UserAccounts>
      <AdministratorPassword>
        <Value>my-password</Value>
        <PlainText>true</PlainText>
      </AdministratorPassword>
      <LocalAccounts>
        <LocalAccount wcm:action="add">
          <Name>jdoe</Name>
          <Password>
            <Value>vmware</Value>
            <PlainText>true</PlainText>
          </Password>
          <Group>Administrators</Group>
          <DisplayName>jdoe</DisplayName>
          <Description>Local administrator account</Description>
        </LocalAccount>
      </LocalAccounts>
    </UserAccounts>
  </component>
</settings>

Automate OOBE

The following XML can be used to prevent appearing the Windows OOBE screen:

<settings pass="oobeSystem">
  <component name="Microsoft-Windows-International-Core" processorArchitecture="amd64"
    publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
    <InputLocale>0409:00000409</InputLocale>
    <SystemLocale>en-US</SystemLocale>
    <UserLocale>en-US</UserLocale>
    <UILanguage>en-US</UILanguage>
  </component>
  <component name="Microsoft-Windows-Shell-Setup" processorArchitecture="amd64"
    publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS"
    xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
    <OOBE>
      <HideEULAPage>true</HideEULAPage>
      <HideLocalAccountScreen>true</HideLocalAccountScreen>
      <HideOEMRegistrationScreen>true</HideOEMRegistrationScreen>
      <HideOnlineAccountScreens>true</HideOnlineAccountScreens>
      <HideWirelessSetupInOOBE>true</HideWirelessSetupInOOBE>
      <ProtectYourPC>3</ProtectYourPC>
      <SkipMachineOOBE>true</SkipMachineOOBE>
      <SkipUserOOBE>true</SkipUserOOBE>
    </OOBE>
    <TimeZone>Central Standard Time</TimeZone>
  </component>
</settings>

For more information on Sysprep, please refer to Microsoft's official documentation.

vAppConfig

The vAppConfig bootstrap method is useful for legacy, VM images that rely on bespoke, boot-time processes that leverage vAppConfig properties for customizing a guest.

To illustrate, the following YAML can be utilized to deploy a VirtualMachine and bootstrap OVF properties that define the network information:

apiVersion: vmoperator.vmware.com/v1alpha2
kind: VirtualMachine
metadata:
  name:      my-vm
  namespace: my-namespace
spec:
  className:    my-vm-class
  imageName:    vmi-0a0044d7c690bcbea
  storageClass: my-storage-class
  bootstrap:
    vAppConfig:
      properties:
      - key: nameservers
        value:
          value: "{{ (index .V1alpha2.Net.Nameservers 0) }}"
      - key: management_ip
        value:
          value: "{{ (index (index .V1alpha2.Net.Devices 0).IPAddresses 0) }}"
      - key: hostname
        value:
          value: "{{ .V1alpha2.VM.Name }}"
      - key: management_gateway
        value:
          value: "{{ (index .V1alpha2.Net.Devices 0).Gateway4 }}"

Templating

Properties are templated according to the Golang text/template package. Please refer to Go's documentation for a full understanding of how to construct template queries.

Input Object

The object provided to the template engine is the VirtualMachineTemplate data structure.

Pre-Defined Functions

The following table lists the functions VM Operator defines and passes into the template engine to make it easier to construct the information required for vApp properties:

V1alpha1 Prefixed Template Functions

Please note the template functions beginning with V1alpha1 are still supported, but users are encouraged to switch to the V1alpha2 variants.

Query name Signature Description
V1alpha2_FirstIP func () string Get the first, non-loopback IP address (formatted with network length) from the first NIC.
V1alpha2_FirstIPFromNIC func (index int) string Get the first, non-loopback IP address (formatted with network length) from the n'th NIC. If the specified index is out-of-bounds, the template string is not parsed.
V1alpha2_FormatIP func (IP string, netmask string) string This function may be used to format an IP address with or without a network prefix length. If the provided netmask is empty, then the IP address returned does not include a network length. If the provided netmask is non-empty, then it must be either a length, ex. /24, or decimal notation, ex. 255.255.255.0.
V1alpha2_FirstNicMacAddr func() (string, error) Get the MAC address from the first NIC.
V1alpha2_FormatNameservers func (count int, delimiter string) string Format the first occurred count of nameservers with the provided delimiter. Specify a negative number to include all nameservers.
V1alpha2_IP func(IP string) string Format an IP address with the default netmask CIDR. If the specified IP is invalid, the template string is not parsed.
V1alpha2_IPsFromNIC func (index int) []string List all IPs, formatted with the network length, from the n'th NIC. If the specified index is out-of-bounds, the template string is not parsed.
V1alpha2_SubnetMask func(cidr string) (string, error) Get a subnet mask from an IP address formatted with a network length.

Deprecated

The following bootstrap providers are still available, but they are deprecated and are not recommended.

OvfEnv

The OvfEnv method combines VMware's Guest OS Customization (GOSC) for bootstrapping a guest's network and the Cloud-Init, OVF data source to supply a Cloud-Init Cloud Config.

Deprecation Notice

The OvfEnv transport has been deprecated in favor of the CloudInit provider. Apart from the reasons outlined in the Cloud-Init section on why one would want to use Cloud-Init, the OvfEnv provider's reliance on the GOSC APIs for bootstrapping the guest's network and Cloud-Init for additional customizations resulted in a race condition. VMware Tools would reboot the guest to satisfy the GOSC network configuration while Cloud-Init was still running. Thus any image that used OvfEnv needed to have a special fix applied to prevent Cloud-Init from running on first-boot. This method is removed in v1alpha2, and any consumers still relying on this provider should switch to Cloud-Init.

ExtraConfig

The ExtraConfig provider combined VMware's Guest OS Customization (GOSC) for bootstrapping a guest's network allowed clients to set any guestinfo key they wanted in order to influence a guest's bootstrap engine.

Deprecation Notice

When Tanzu Kubernetes was first released, the Cluster API provider that depended upon VM Operator used the ExtraConfig provider for supplying bootstrap information. This method was never intended for wide use, and Tanzu now uses Cloud-Init anyway. To that end, this provider is no longer supported and is removed in v1alpha2. Any consumers still relying on this provider should switch to Cloud-Init.