Module: Support::GuestCustomization

Included in:
CloneVm
Defined in:
lib/support/guest_customization.rb

Constant Summary collapse

DEFAULT_LINUX_TIMEZONE =
"Etc/UTC".freeze
DEFAULT_WINDOWS_ORG =
"TestKitchen".freeze
DEFAULT_WINDOWS_TIMEZONE =

Etc/UTC

0x80000050
DEFAULT_TIMEOUT_TASK =
600
DEFAULT_TIMEOUT_IP =
60
WINDOWS_KMS_KEYS =

Generic Volume License Keys for temporary Windows Server setup.

{
  "Microsoft Windows Server 2019 (64-bit)" => "N69G4-B89J2-4G8F4-WWYCC-J464C",
  "Microsoft Windows Server 2016 (64-bit)" => "WC2BQ-8NRM3-FDDYY-2BFGV-KHKQY",
  "Microsoft Windows Server 2012R2 (64-bit)" => "D2N9P-3P6X9-2R39C-7RTCD-MDVJX",
  "Microsoft Windows Server 2012 (64-bit)" => "BN3D2-R7TKB-3YPBD-8DRP2-27GG4",
}.freeze

Instance Method Summary collapse

Instance Method Details

#guest_customizationObject

Configuration values for Guest Customization



28
29
30
# File 'lib/support/guest_customization.rb', line 28

def guest_customization
  options[:guest_customization]
end

#guest_customization_eventsObject

Filter Customization events for the current VM



332
333
334
# File 'lib/support/guest_customization.rb', line 332

def guest_customization_events
  vm_events %w{CustomizationSucceeded CustomizationFailed CustomizationStartedEvent}
end

#guest_customization_identityObject

Return OS-specific CustomizationIdentity object



129
130
131
132
133
134
135
136
137
# File 'lib/support/guest_customization.rb', line 129

def guest_customization_identity
  if linux?
    guest_customization_identity_linux
  elsif windows?
    guest_customization_identity_windows
  else
    raise Support::GuestCustomizationError.new("Unknown OS, no valid customization found")
  end
end

#guest_customization_identity_linuxObject

Construct Linux-specific customization information



140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
# File 'lib/support/guest_customization.rb', line 140

def guest_customization_identity_linux
  timezone = guest_customization[:timezone]
  if timezone && !valid_linux_timezone?(timezone)
    raise Support::GuestCustomizationError.new <<~ERROR
      Linux customization requires `timezone` in `Area/Location` format.
      See https://kb.vmware.com/s/article/2145518
    ERROR
  end

  Kitchen.logger.warn("Linux guest customization: No timezone passed, assuming UTC") unless timezone

  RbVmomi::VIM::CustomizationLinuxPrep.new(
    domain: guest_customization[:dns_domain],
    hostName: guest_hostname,
    hwClockUTC: true,
    timeZone: timezone || DEFAULT_LINUX_TIMEZONE
  )
end

#guest_customization_identity_windowsObject

Construct Windows-specific customization information



160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
# File 'lib/support/guest_customization.rb', line 160

def guest_customization_identity_windows
  timezone = guest_customization[:timezone]
  if timezone && !valid_windows_timezone?(timezone)
    raise Support::GuestCustomizationOptionsError.new <<~ERROR
      Windows customization requires `timezone` as decimal number or hex number (0x55).
      See https://support.microsoft.com/en-us/help/973627/microsoft-time-zone-index-values
    ERROR
  end

  Kitchen.logger.warn("Windows guest customization: No timezone passed, assuming UTC") unless timezone

  product_id = guest_customization[:product_id]

  # Try to look up and use a known, documented 120-day trial key
  unless product_id
    guest_os = src_vm.guest&.guestFullName
    product_id = windows_kms_for_guest(guest_os)

    Kitchen.logger.warn format("Windows guest customization:: Using KMS Key `%<key>s` for %<os>s", key: product_id, os: guest_os) if product_id
  end

  unless valid_windows_key? product_id
    raise Support::GuestCustomizationOptionsError.new <<~ERROR
      Windows customization requires `product_id` to work. Add a valid product key or
      see https://docs.microsoft.com/en-us/windows-server/get-started/kmsclientkeys for KMS trial keys
    ERROR
  end

  customization_pass = nil
  if guest_customization[:administrator_password]
    customization_pass = RbVmomi::VIM::CustomizationPassword.new(
      plainText: true,
      value: guest_customization[:administrator_password]
    )
  end

  RbVmomi::VIM::CustomizationSysprep.new(
    guiUnattended: RbVmomi::VIM::CustomizationGuiUnattended.new(
      timeZone: timezone.to_i || DEFAULT_WINDOWS_TIMEZONE,
      autoLogon: false,
      autoLogonCount: 1,
      password: customization_pass
    ),
    identification: RbVmomi::VIM::CustomizationIdentification.new,
    userData: RbVmomi::VIM::CustomizationUserData.new(
      computerName: guest_hostname,
      fullName: guest_customization[:org_name] || DEFAULT_WINDOWS_ORG,
      orgName: guest_customization[:org_name] || DEFAULT_WINDOWS_ORG,
      productId: product_id
    )
  )
end

#guest_customization_ip_change?Boolean

Check if an IP change is requested

Returns:

  • (Boolean)


124
125
126
# File 'lib/support/guest_customization.rb', line 124

def guest_customization_ip_change?
  guest_customization[:ip_address]
end

#guest_customization_specObject

Build CustomizationSpec for Guest OS Customization



35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
# File 'lib/support/guest_customization.rb', line 35

def guest_customization_spec
  return unless guest_customization

  guest_customization_validate_options

  if guest_customization[:ip_address]
    customized_ip = RbVmomi::VIM::CustomizationIPSettings.new(
      ip: RbVmomi::VIM::CustomizationFixedIp(ipAddress: guest_customization[:ip_address]),
      gateway: guest_customization[:gateway],
      subnetMask: guest_customization[:subnet_mask],
      dnsDomain: guest_customization[:dns_domain]
    )
  else
    customized_ip = RbVmomi::VIM::CustomizationIPSettings.new(
      ip: RbVmomi::VIM::CustomizationDhcpIpGenerator.new,
      dnsDomain: guest_customization[:dns_domain]
    )
  end

  RbVmomi::VIM::CustomizationSpec.new(
    identity: guest_customization_identity,
    globalIPSettings: RbVmomi::VIM::CustomizationGlobalIPSettings.new(
      dnsServerList: guest_customization[:dns_server_list],
      dnsSuffixList: guest_customization[:dns_suffix_list]
    ),
    nicSettingMap: [RbVmomi::VIM::CustomizationAdapterMapping.new(
      adapter: customized_ip
    )]
  )
end

#guest_customization_validate_optionsObject

Check options for existance and format

Raises:



69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
# File 'lib/support/guest_customization.rb', line 69

def guest_customization_validate_options
  if guest_customization_ip_change?
    unless ip?(guest_customization[:ip_address])
      raise Support::GuestCustomizationOptionsError.new("Parameter `ip_address` is required to be formatted as an IPv4 address")
    end

    unless guest_customization[:subnet_mask]
      raise Support::GuestCustomizationOptionsError.new("Parameter `subnet_mask` is required if assigning a fixed IPv4 address")
    end

    unless ip?(guest_customization[:subnet_mask])
      raise Support::GuestCustomizationOptionsError.new("Parameter `subnet_mask` is required to be formatted as an IPv4 address")
    end

    if up?(guest_customization[:ip_address])
      raise Support::GuestCustomizationOptionsError.new("Parameter `ip_address` points to a host reachable via ICMP") unless guest_customization[:continue_on_ip_conflict]

      Kitchen.logger.warn("Continuing customization despite `ip_address` conflicting with a reachable host per user request")
    end
  end

  if guest_customization[:gateway]
    unless guest_customization[:gateway].is_a?(Array)
      raise Support::GuestCustomizationOptionsError.new("Parameter `gateway` must be an array")
    end

    guest_customization[:gateway].each do |v|
      unless ip?(v)
        raise Support::GuestCustomizationOptionsError.new("Parameter `gateway` is required to be formatted as an IPv4 address")
      end
    end
  end

  required = %i{dns_domain dns_server_list dns_suffix_list}
  missing = required - guest_customization.keys
  unless missing.empty?
    raise Support::GuestCustomizationOptionsError.new("Parameters `#{missing.join("`, `")}` are required to support guest customization")
  end

  guest_customization[:dns_server_list].each do |v|
    unless ip?(v)
      raise Support::GuestCustomizationOptionsError.new("Parameter `dns_server_list` is required to be formatted as an IPv4 address")
    end
  end

  if !guest_customization[:dns_server_list].is_a?(Array)
    raise Support::GuestCustomizationOptionsError.new("Parameter `dns_server_list` must be an array")
  elsif !guest_customization[:dns_suffix_list].is_a?(Array)
    raise Support::GuestCustomizationOptionsError.new("Parameter `dns_suffix_list` must be an array")
  end
end

#guest_customization_waitObject

Wait for vSphere task completion and subsequent IP address update (if any).



275
276
277
278
# File 'lib/support/guest_customization.rb', line 275

def guest_customization_wait
  guest_customization_wait_task(guest_customization[:timeout_task] || DEFAULT_TIMEOUT_TASK)
  guest_customization_wait_ip(guest_customization[:timeout_ip] || DEFAULT_TIMEOUT_IP)
end

#guest_customization_wait_ip(timeout = 30, sleep_time = 1) ⇒ Object

Wait for new IP to be reported, if any.

Parameters:

  • timeout (Integer) (defaults to: 30)

    Timeout in seconds. Tools report every 30 seconds, Default: 30 seconds

  • sleep_time (Integer) (defaults to: 1)

    Time to wait between tries

Raises:



310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
# File 'lib/support/guest_customization.rb', line 310

def guest_customization_wait_ip(timeout = 30, sleep_time = 1)
  return unless guest_customization_ip_change?

  waited_seconds = 0

  Kitchen.logger.info "Waiting for guest customization IP update..."

  while waited_seconds < timeout
    found_ip = wait_for_ip(timeout, 1.0)

    return if found_ip == guest_customization[:ip_address]

    sleep(sleep_time)
    waited_seconds += sleep_time
  end

  raise Support::GuestCustomizationError.new("Customized IP was not reported within #{timeout} seconds.")
end

#guest_customization_wait_task(timeout = 600, sleep_time = 10) ⇒ Object

Wait for Guest customization to finish successfully.

Parameters:

  • timeout (Integer) (defaults to: 600)

    Timeout in seconds

  • sleep_time (Integer) (defaults to: 10)

    Time to wait between tries

Raises:



284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
# File 'lib/support/guest_customization.rb', line 284

def guest_customization_wait_task(timeout = 600, sleep_time = 10)
  waited_seconds = 0

  Kitchen.logger.info "Waiting for guest customization (timeout: #{timeout} seconds)..."

  while waited_seconds < timeout
    events = guest_customization_events

    if events.any? { |event| event.is_a? RbVmomi::VIM::CustomizationSucceeded }
      return
    elsif (failed = events.detect { |event| event.is_a? RbVmomi::VIM::CustomizationFailed })
      # Only matters for Linux, as Windows won't come up at all to report a failure via VMware Tools
      raise Support::GuestCustomizationError.new("Customization of VM failed: #{failed.fullFormattedMessage}")
    end

    sleep(sleep_time)
    waited_seconds += sleep_time
  end

  raise Support::GuestCustomizationError.new("Customization of VM did not complete within #{timeout} seconds.")
end

#guest_hostnameObject

Return Guest hostname to be configured and check for validity.



263
264
265
266
267
268
269
270
271
272
# File 'lib/support/guest_customization.rb', line 263

def guest_hostname
  hostname = guest_customization[:hostname] || options[:vm_name]

  hostname_pattern = /^([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])$/
  unless hostname.match?(hostname_pattern)
    raise Support::GuestCustomizationError.new("Only letters, numbers or hyphens in hostnames allowed")
  end

  RbVmomi::VIM::CustomizationFixedName.new(name: hostname)
end

#up?(host) ⇒ Boolean

Check if a host is reachable

Returns:

  • (Boolean)


214
215
216
217
# File 'lib/support/guest_customization.rb', line 214

def up?(host)
  check = Net::Ping::External.new(host)
  check.ping?
end

#valid_linux_timezone?(input) ⇒ Boolean

Check format of Linux-specific timezone, according to VMware support

Parameters:

  • input (Integer)

    Value to check for validity

Returns:

  • (Boolean)


231
232
233
234
235
236
# File 'lib/support/guest_customization.rb', line 231

def valid_linux_timezone?(input)
  # Specific to VMware: https://kb.vmware.com/s/article/2145518
  linux_timezone_pattern = %r{^[A-Z][A-Za-z]+\/[A-Z][-_+A-Za-z0-9]+$}

  input.to_s.match? linux_timezone_pattern
end

#valid_windows_key?(input) ⇒ Boolean

Check for format of Windows Product IDs

Parameters:

  • input (String)

    String to check

Returns:

  • (Boolean)


254
255
256
257
258
# File 'lib/support/guest_customization.rb', line 254

def valid_windows_key?(input)
  windows_key_pattern = /^[A-Z0-9]{5}-[A-Z0-9]{5}-[A-Z0-9]{5}-[A-Z0-9]{5}-[A-Z0-9]{5}$/

  input.to_s.match? windows_key_pattern
end

#valid_windows_timezone?(input) ⇒ Boolean

Check format of Windows-specific timezone

Parameters:

  • input (Integer)

    Value to check for validity

Returns:

  • (Boolean)


242
243
244
245
246
247
248
# File 'lib/support/guest_customization.rb', line 242

def valid_windows_timezone?(input)
  # Accept decimals and hex
  # See https://support.microsoft.com/en-us/help/973627/microsoft-time-zone-index-values
  windows_timezone_pattern = /^([0-9]+|0x[0-9a-fA-F]+)$/

  input.to_s.match? windows_timezone_pattern
end

#windows_kms_for_guest(name) ⇒ Object

Retrieve a GVLK (evaluation key) for the named OS

Parameters:

  • name (String)

    Name of the OS as reported by VMware



223
224
225
# File 'lib/support/guest_customization.rb', line 223

def windows_kms_for_guest(name)
  WINDOWS_KMS_KEYS.fetch(name, false)
end